diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..ede5f1f --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,13 @@ +{ + "typescript.preferences.autoImportFileExcludePatterns": ["repos/**"], + "javascript.preferences.autoImportFileExcludePatterns": ["repos/**"], + "files.exclude": { + "repos/**": true + }, + "files.watcherExclude": { + "repos/**": true + }, + "search.exclude": { + "repos/**": true + } +} diff --git a/CLAUDE.md b/CLAUDE.md index 874e0df..ebf26f9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -11,3 +11,13 @@ Uses default label vocabulary. See `docs/agents/triage-labels.md`. ### Domain docs Single-context layout — one `CONTEXT.md` and `docs/adr/` at the repo root. See `docs/agents/domain.md`. + +### Effect reference source + +The Effect repository is vendored at `repos/effect/` as a git subtree (read-only reference material). Use it to explore APIs, find usage examples, and understand implementation details. **Never modify files under `repos/`.** + +To update the vendored source: + +```bash +git subtree pull --prefix=repos/effect https://github.com/Effect-TS/effect.git main --squash +``` diff --git a/eslint.config.mjs b/eslint.config.mjs index 869ea05..c686f04 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -25,6 +25,7 @@ export default [ '**/functions/**', '**/codegen/**', 'apps/**', + 'repos/**', ], }, ...compat.extends('plugin:@typescript-eslint/recommended', 'plugin:prettier/recommended'), diff --git a/repos/effect/.changeset/config.json b/repos/effect/.changeset/config.json new file mode 100644 index 0000000..c132e23 --- /dev/null +++ b/repos/effect/.changeset/config.json @@ -0,0 +1,14 @@ +{ + "$schema": "https://unpkg.com/@changesets/config@1.6.4/schema.json", + "changelog": ["@changesets/changelog-github", { "repo": "Effect-TS/effect" }], + "commit": false, + "linked": [], + "access": "restricted", + "baseBranch": "main", + "updateInternalDependencies": "patch", + "ignore": ["scratchpad"], + "snapshot": { + "useCalculatedVersion": false, + "prereleaseTemplate": "{tag}-{commit}" + } +} diff --git a/repos/effect/.changeset/fix-cli-completions-hyphen.md b/repos/effect/.changeset/fix-cli-completions-hyphen.md new file mode 100644 index 0000000..3b681f5 --- /dev/null +++ b/repos/effect/.changeset/fix-cli-completions-hyphen.md @@ -0,0 +1,5 @@ +--- +"@effect/cli": patch +--- + +fix(cli): replace all hyphens in shell completion command names diff --git a/repos/effect/.envrc b/repos/effect/.envrc new file mode 100644 index 0000000..44610e5 --- /dev/null +++ b/repos/effect/.envrc @@ -0,0 +1 @@ +use flake; diff --git a/repos/effect/.github/CODEOWNERS b/repos/effect/.github/CODEOWNERS new file mode 100644 index 0000000..8eb1379 --- /dev/null +++ b/repos/effect/.github/CODEOWNERS @@ -0,0 +1,50 @@ +/packages/effect/ @mikearnaldi + +/packages/effect/src/Arbitrary.ts @gcanti +/packages/effect/src/JSONSchema.ts @gcanti +/packages/effect/src/ParseResult.ts @gcanti +/packages/effect/src/Pretty.ts @gcanti +/packages/effect/src/Schema.ts @gcanti +/packages/effect/src/SchemaAST.ts @gcanti +/packages/effect/src/internal/schema/ @gcanti +/packages/effect/test/Schema/ @gcanti + +/packages/ai/ @IMax153 + +/packages/cli/ @IMax153 + +/packages/cluster/ @tim-smart + +/packages/experimental/ @tim-smart + +/packages/opentelemetry/ @tim-smart + +/packages/platform/ @tim-smart +/packages/platform-browser/ @tim-smart +/packages/platform-bun/ @tim-smart +/packages/platform-node/ @tim-smart +/packages/platform-node-shared/ @tim-smart + +/packages/printer/ @IMax153 +/packages/printer-ansi/ @IMax153 + +/packages/rpc/ @tim-smart + +/packages/sql/ @tim-smart +/packages/sql-clickhouse/ @tim-smart +/packages/sql-d1/ @tim-smart +/packages/sql-drizzle/ @tim-smart +/packages/sql-kysely/ @tim-smart +/packages/sql-libsql/ @tim-smart +/packages/sql-mssql/ @tim-smart +/packages/sql-mysql2/ @tim-smart +/packages/sql-pg/ @tim-smart +/packages/sql-sqlite-bun/ @tim-smart +/packages/sql-sqlite-do/ @tim-smart +/packages/sql-sqlite-node/ @tim-smart +/packages/sql-sqlite-react-native/ @tim-smart +/packages/sql-sqlite-wasm/ @tim-smart + +/packages/typeclass/ @gcanti + +/packages/vitest/ @mikearnaldi diff --git a/repos/effect/.github/actions/setup/action.yml b/repos/effect/.github/actions/setup/action.yml new file mode 100644 index 0000000..f102b70 --- /dev/null +++ b/repos/effect/.github/actions/setup/action.yml @@ -0,0 +1,24 @@ +name: Setup +description: Perform standard setup and install dependencies using pnpm. +inputs: + node-version: + description: The version of Node.js to install + required: true + default: 23.7.0 + registry-url: + description: Optional registry to set up for auth. + +runs: + using: composite + steps: + - name: Install pnpm + uses: pnpm/action-setup@v3 + - name: Install node + uses: actions/setup-node@v4 + with: + cache: pnpm + node-version: ${{ inputs.node-version }} + registry-url: ${{ inputs.registry-url }} + - name: Install dependencies + shell: bash + run: pnpm install diff --git a/repos/effect/.github/workflows/check.yml b/repos/effect/.github/workflows/check.yml new file mode 100644 index 0000000..d5a743d --- /dev/null +++ b/repos/effect/.github/workflows/check.yml @@ -0,0 +1,64 @@ +name: Check +on: + workflow_dispatch: + pull_request: + branches: [main, next-minor, next-major] + push: + branches: [main, next-minor, next-major] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: {} + +jobs: + types: + name: Types + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v4 + - name: Install dependencies + uses: ./.github/actions/setup + - run: pnpm check + - run: pnpm test-types --target '>=5.4' + + lint: + name: Lint + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v4 + - name: Install dependencies + uses: ./.github/actions/setup + - run: pnpm circular + - run: pnpm lint + - run: pnpm codegen + - name: Check for codegen changes + run: git diff --exit-code + + test: + name: Test (${{ matrix.runtime }} ${{ matrix.shard }}) + runs-on: ubuntu-latest + timeout-minutes: 10 + strategy: + fail-fast: false + matrix: + shard: [1/4, 2/4, 3/4, 4/4] + # runtime: [Node, Bun] # TODO: Re-enable bun test suite after https://github.com/oven-sh/bun/issues/4145 is resolved + runtime: [Node] + steps: + - uses: actions/checkout@v4 + - name: Install dependencies + uses: ./.github/actions/setup + - uses: oven-sh/setup-bun@v1 + if: matrix.runtime == 'Bun' + with: + bun-version: 1.0.25 + - name: Test + run: pnpm vitest --shard ${{ matrix.shard }} + if: matrix.runtime == 'Node' + - name: Test + run: bun vitest --shard ${{ matrix.shard }} + if: matrix.runtime == 'Bun' diff --git a/repos/effect/.github/workflows/pages.yml b/repos/effect/.github/workflows/pages.yml new file mode 100644 index 0000000..e568bb1 --- /dev/null +++ b/repos/effect/.github/workflows/pages.yml @@ -0,0 +1,50 @@ +name: Pages +on: + workflow_dispatch: + pull_request: + branches: [main, next-minor, next-major] + push: + branches: [main, next-minor, next-major] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: {} + +jobs: + build: + name: Build + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - uses: actions/checkout@v4 + - name: Install dependencies + uses: ./.github/actions/setup + - run: pnpm docgen + - name: Build pages Jekyll + if: github.repository_owner == 'Effect-Ts' && github.event_name == 'push' && github.ref == 'refs/heads/main' + uses: actions/jekyll-build-pages@v1 + with: + source: ./docs + destination: ./_site + - name: Upload pages artifact + if: github.repository_owner == 'Effect-Ts' && github.event_name == 'push' && github.ref == 'refs/heads/main' + uses: actions/upload-pages-artifact@v3 + + deploy: + if: github.repository_owner == 'Effect-Ts' && github.event_name == 'push' && github.ref == 'refs/heads/main' + name: Deploy + runs-on: ubuntu-latest + timeout-minutes: 10 + needs: build + permissions: + pages: write # To deploy to GitHub Pages + id-token: write # To verify the deployment originates from an appropriate source + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/repos/effect/.github/workflows/release-queue.yml b/repos/effect/.github/workflows/release-queue.yml new file mode 100644 index 0000000..305d0bb --- /dev/null +++ b/repos/effect/.github/workflows/release-queue.yml @@ -0,0 +1,35 @@ +name: Release queue +on: + issue_comment: + types: [created] + pull_request_target: + branches: [main, next-minor, next-major] + push: + branches: [main, next-minor, next-major] + +permissions: {} + +jobs: + update: + if: github.repository_owner == 'Effect-Ts' + name: Update + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + contents: write + pull-requests: write + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.EFFECT_BOT_GH }} + - run: gh pr checkout ${{ github.event.pull_request.number }} + if: github.event.pull_request + env: + GITHUB_TOKEN: ${{ secrets.EFFECT_BOT_GH }} + - uses: tim-smart/next-release-action@main + with: + github_token: ${{ secrets.EFFECT_BOT_GH }} + packages: effect,@effect/platform + git_user: effect-bot + git_email: tech-ops@effectful.co diff --git a/repos/effect/.github/workflows/release.yml b/repos/effect/.github/workflows/release.yml new file mode 100644 index 0000000..e44d974 --- /dev/null +++ b/repos/effect/.github/workflows/release.yml @@ -0,0 +1,35 @@ +name: Release +on: + push: + branches: [main] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + +permissions: {} + +jobs: + release: + if: github.repository_owner == 'Effect-Ts' + name: Release + runs-on: ubuntu-latest + timeout-minutes: 30 + permissions: + contents: write + id-token: write + pull-requests: write + steps: + - uses: actions/checkout@v4 + - name: Install dependencies + uses: ./.github/actions/setup + with: + registry-url: "https://registry.npmjs.org" + - name: Upgrade npm for OIDC support + run: npm install -g npm@latest + - name: Create Release Pull Request or Publish + uses: changesets/action@v1 + with: + version: pnpm changeset-version + publish: pnpm changeset-publish + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/repos/effect/.github/workflows/snapshot.yml b/repos/effect/.github/workflows/snapshot.yml new file mode 100644 index 0000000..b084992 --- /dev/null +++ b/repos/effect/.github/workflows/snapshot.yml @@ -0,0 +1,25 @@ +name: Snapshot +on: + pull_request: + branches: [main, next-minor, next-major] + workflow_dispatch: + +permissions: {} + +jobs: + snapshot: + name: Snapshot + if: github.repository_owner == 'Effect-Ts' + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v4 + - name: Install dependencies + uses: ./.github/actions/setup + - name: Run codemods + run: pnpm codemod + - name: Build package + run: pnpm build + - name: Create snapshot + id: snapshot + run: pnpx pkg-pr-new@0.0.28 publish --pnpm --comment=off ./packages/* ./packages/ai/* diff --git a/repos/effect/.github/workflows/ts-nightly.yml b/repos/effect/.github/workflows/ts-nightly.yml new file mode 100644 index 0000000..ed8d2bd --- /dev/null +++ b/repos/effect/.github/workflows/ts-nightly.yml @@ -0,0 +1,34 @@ +name: TypeScript's nightly +on: + workflow_dispatch: + schedule: + - cron: "0 12 * * *" + +permissions: {} + +jobs: + types: + name: Types + # TODO enable after TSTyche adds TypeScript 7 support (see https://github.com/tstyche/tstyche/issues/443) + if: false + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + issues: write + steps: + - uses: actions/checkout@v4 + - name: Install dependencies + uses: ./.github/actions/setup + - name: Run type tests + run: pnpm test-types --target next + - name: Notify on failed run + uses: actions/github-script@v7 + if: failure() + with: + script: | + github.rest.issues.create({ + owner: context.repo.owner, + repo: context.repo.repo, + title: "Failed TypeScript's nightly type test run", + body: `The following type test run against TypeScript's nightly build failed: [#${context.runNumber}](https://github.com/Effect-TS/effect/actions/runs/${context.runId}).` + }) diff --git a/repos/effect/.gitignore b/repos/effect/.gitignore new file mode 100644 index 0000000..8682277 --- /dev/null +++ b/repos/effect/.gitignore @@ -0,0 +1,14 @@ +coverage/ +*.tsbuildinfo +node_modules/ +.DS_Store +tmp/ +dist/ +build/ +docs/ +scratchpad/ +.direnv/ +.idea/ +.env* +.lalph/ +.repos/ diff --git a/repos/effect/.madgerc b/repos/effect/.madgerc new file mode 100644 index 0000000..b407c6b --- /dev/null +++ b/repos/effect/.madgerc @@ -0,0 +1,7 @@ +{ + "detectiveOptions": { + "ts": { + "skipTypeImports": true + } + } +} diff --git a/repos/effect/.prettierignore b/repos/effect/.prettierignore new file mode 100644 index 0000000..e727c7d --- /dev/null +++ b/repos/effect/.prettierignore @@ -0,0 +1,3 @@ +*.js +*.ts +*.cjs diff --git a/repos/effect/.prettierrc.json b/repos/effect/.prettierrc.json new file mode 100644 index 0000000..27b720e --- /dev/null +++ b/repos/effect/.prettierrc.json @@ -0,0 +1,4 @@ +{ + "semi": false, + "trailingComma": "none" +} diff --git a/repos/effect/.vscode/settings.json b/repos/effect/.vscode/settings.json new file mode 100644 index 0000000..a5826e7 --- /dev/null +++ b/repos/effect/.vscode/settings.json @@ -0,0 +1,48 @@ +{ + "typescript.tsdk": "node_modules/typescript/lib", + "typescript.preferences.importModuleSpecifier": "relative", + "typescript.enablePromptUseWorkspaceTsdk": true, + "deno.enable": false, + "biome.enabled": false, + "editor.formatOnSave": true, + "eslint.format.enable": true, + "[json]": { + "editor.defaultFormatter": "vscode.json-language-features" + }, + "[markdown]": { + "editor.defaultFormatter": "esbenp.prettier-vscode", + "prettier.semi": false, + "prettier.trailingComma": "none" + }, + "[javascript]": { + "editor.defaultFormatter": "dbaeumer.vscode-eslint" + }, + "[javascriptreact]": { + "editor.defaultFormatter": "dbaeumer.vscode-eslint" + }, + "[typescript]": { + "editor.defaultFormatter": "dbaeumer.vscode-eslint" + }, + "[typescriptreact]": { + "editor.defaultFormatter": "dbaeumer.vscode-eslint" + }, + "eslint.validate": ["markdown", "javascript", "typescript"], + "editor.codeActionsOnSave": { + "source.fixAll.eslint": "explicit" + }, + "editor.quickSuggestions": { + "other": true, + "comments": false, + "strings": false + }, + "editor.acceptSuggestionOnCommitCharacter": true, + "editor.acceptSuggestionOnEnter": "on", + "editor.quickSuggestionsDelay": 10, + "editor.suggestOnTriggerCharacters": true, + "editor.tabCompletion": "off", + "editor.suggest.localityBonus": true, + "editor.suggestSelection": "recentlyUsed", + "editor.wordBasedSuggestions": "matchingDocuments", + "editor.parameterHints.enabled": true, + "files.insertFinalNewline": true +} diff --git a/repos/effect/.vscode/tasks.json b/repos/effect/.vscode/tasks.json new file mode 100644 index 0000000..bda5ef7 --- /dev/null +++ b/repos/effect/.vscode/tasks.json @@ -0,0 +1,30 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "Watch", + "type": "shell", + "command": "pnpm check -w", + "options": { + "cwd": "${workspaceRoot}" + }, + "group": { + "kind": "build", + "isDefault": true + }, + "isBackground": true, + "presentation": { + "group": "watch-build" + }, + "problemMatcher": [ + { + "base": "$tsc-watch", + "fileLocation": [ + "relative", + "${workspaceRoot}", + ], + } + ] + } + ] +} diff --git a/repos/effect/AGENTS.md b/repos/effect/AGENTS.md new file mode 100644 index 0000000..72fea10 --- /dev/null +++ b/repos/effect/AGENTS.md @@ -0,0 +1,77 @@ +# Agent Instructions + +This is the Effect library repository, focusing on functional programming patterns and effect systems in TypeScript. + +## Development Workflow + +- The git base branch is `main` +- Use `pnpm` as the package manager + +### Core Principles + +- **Zero Tolerance for Errors**: All automated checks must pass +- **Clarity over Cleverness**: Choose clear, maintainable solutions +- **Conciseness**: Keep code and any wording concise and to the point. Sacrifice grammar for the sake of concision. +- **Reduce comments**: Avoid comments unless absolutely required to explain unusual or complex logic. Comments in jsdocs are acceptable. + +### Mandatory Validation Steps + +- Run `pnpm lint-fix` after editing files +- Always run tests after making changes: `pnpm test run ` +- Run type checking: `pnpm check` + - If type checking continues to fail, run `pnpm clean` to clear caches, then re-run `pnpm check` +- Build the project: `pnpm build` +- Check JSDoc examples compile: `pnpm docgen` + +## Code Style Guidelines + +**Always** look at existing code in the repository to learn and follow +established patterns before writing new code. + +Do not worry about getting code formatting perfect while writing. Use `pnpm lint-fix` +to automatically format code according to the project's style guidelines. + +### Barrel files + +The `index.ts` files are automatically generated. Do not manually edit them. Use +`pnpm codegen` to regenerate barrel files after adding or removing modules. + +## Changesets + +All pull requests must include a changeset in the `.changeset/` directory. +This is important for maintaining a clear changelog and ensuring proper versioning of packages. + +## Running test code + +If you need to run some code for testing or debugging purposes, create a new +file in the `scratchpad/` directory at the root of the repository. You can then +run the file with `tsx scratchpad/your-file.ts`. + +Make sure to delete the file after you are done testing. + +## Testing + +Before writing tests, look at existing tests in the codebase for similar +functionality to follow established patterns. + +- Test files are located in `packages/*/test/` directories for each package +- Main Effect library tests: `packages/effect/test/` +- Always verify implementations with tests +- Run specific tests with: `pnpm test ` + +### it.effect Testing Pattern + +- Use `it.effect` for all Effect-based tests, not `Effect.runSync` with regular `it` +- Import `{ assert, describe, it }` from `@effect/vitest` +- Never use `expect` from vitest in Effect tests - use `assert` methods instead +- All tests should use `it.effect("description", () => Effect.gen(function*() { ... }))` + +Before writing tests, look at existing tests in the codebase for similar +functionality to follow established patterns. + +## Learning about "effect" v4 + +If you need to learn more about the new version of effect (version 4), you can +access the repository here: + +\`.repos/effect-v4\` diff --git a/repos/effect/LICENSE b/repos/effect/LICENSE new file mode 100644 index 0000000..be1f5c1 --- /dev/null +++ b/repos/effect/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Effectful Technologies Inc + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/repos/effect/README.md b/repos/effect/README.md new file mode 100644 index 0000000..aa9b4c6 --- /dev/null +++ b/repos/effect/README.md @@ -0,0 +1,219 @@ +![npm version](https://img.shields.io/npm/v/effect) + +# Effect Monorepo + +> An ecosystem of tools to build robust applications in TypeScript + +## Introduction + +Welcome to Effect, a powerful TypeScript framework that provides a fully-fledged functional effect system with a rich standard library. + +Effect consists of several packages that work together to help build robust TypeScript applications. The core package, `effect`, serves as the foundation of the framework, offering primitives for managing side effects, ensuring type safety, and supporting concurrency. + +## Monorepo Structure + +The Effect monorepo is organized into multiple packages, each extending the core functionality. Below is an overview of the packages included: + +| Package | Description | | +| --------------------------------- | ------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- | +| `effect` | Core package | [README](https://github.com/Effect-TS/effect/blob/main/packages/effect/README.md) | +| `@effect/ai` | AI utilities | [README](https://github.com/Effect-TS/effect/blob/main/packages/ai/ai/README.md) | +| `@effect/ai-openai` | OpenAI utilities | [README](https://github.com/Effect-TS/effect/blob/main/packages/ai/openai/README.md) | +| `@effect/ai-anthropic` | Anthropic utilities | [README](https://github.com/Effect-TS/effect/blob/main/packages/ai/anthropic/README.md) | +| `@effect/ai-amazon-bedrock` | Effect modules for working with Amazon Bedrock AI apis | [README](https://github.com/Effect-TS/effect/blob/main/packages/ai/amazon-bedrock/README.md) | +| `@effect/ai-google` | Effect modules for working with Google AI apis | [README](https://github.com/Effect-TS/effect/blob/main/packages/ai/google/README.md) | +| `@effect/cli` | CLI utilities | [README](https://github.com/Effect-TS/effect/blob/main/packages/cli/README.md) | +| `@effect/cluster` | Distributed computing tools | [README](https://github.com/Effect-TS/effect/blob/main/packages/cluster/README.md) | +| `@effect/experimental` | Experimental features and APIs | [README](https://github.com/Effect-TS/effect/blob/main/packages/experimental/README.md) | +| `@effect/opentelemetry` | [OpenTelemetry](https://opentelemetry.io/) integration | [README](https://github.com/Effect-TS/effect/blob/main/packages/opentelemetry/README.md) | +| `@effect/platform` | Cross-platform runtime utilities | [README](https://github.com/Effect-TS/effect/blob/main/packages/platform/README.md) | +| `@effect/platform-browser` | Platform utilities for the browser | [README](https://github.com/Effect-TS/effect/blob/main/packages/platform-browser/README.md) | +| `@effect/platform-bun` | Platform utilities for [Bun](https://bun.sh/) | [README](https://github.com/Effect-TS/effect/blob/main/packages/platform-bun/README.md) | +| `@effect/platform-node` | Platform utilities for [Node.js](https://nodejs.org) | [README](https://github.com/Effect-TS/effect/blob/main/packages/platform-node/README.md) | +| `@effect/platform-node-shared` | Shared utilities for [Node.js](https://nodejs.org) | [README](https://github.com/Effect-TS/effect/blob/main/packages/platform-node-shared/README.md) | +| `@effect/printer` | General-purpose printing utilities | [README](https://github.com/Effect-TS/effect/blob/main/packages/printer/README.md) | +| `@effect/printer-ansi` | ANSI-compatible printing utilities | [README](https://github.com/Effect-TS/effect/blob/main/packages/printer-ansi/README.md) | +| `@effect/rpc` | Remote procedure call (RPC) utilities | [README](https://github.com/Effect-TS/effect/blob/main/packages/rpc/README.md) | +| `@effect/sql` | SQL database utilities | [README](https://github.com/Effect-TS/effect/blob/main/packages/sql/README.md) | +| `@effect/sql-clickhouse` | An `@effect/sql` implementation for [ClickHouse](https://clickhouse.com/). | [README](https://github.com/Effect-TS/effect/blob/main/packages/sql-clickhouse/README.md) | +| `@effect/sql-d1` | An `@effect/sql` implementation for [Cloudflare D1](https://developers.cloudflare.com/d1/). | [README](https://github.com/Effect-TS/effect/blob/main/packages/sql-d1/README.md) | +| `@effect/sql-drizzle` | An `@effect/sql` implementation for [Drizzle](https://orm.drizzle.team/). | [README](https://github.com/Effect-TS/effect/blob/main/packages/sql-drizzle/README.md) | +| `@effect/sql-kysely` | An `@effect/sql` implementation for [Kysely](https://kysely.dev/). | [README](https://github.com/Effect-TS/effect/blob/main/packages/sql-kysely/README.md) | +| `@effect/sql-libsql` | An `@effect/sql` implementation using the `@libsql/client` library. | [README](https://github.com/Effect-TS/effect/blob/main/packages/sql-libsql/README.md) | +| `@effect/sql-mssql` | An `@effect/sql` implementation using the mssql `tedious` library. | [README](https://github.com/Effect-TS/effect/blob/main/packages/sql-mssql/README.md) | +| `@effect/sql-mysql2` | An `@effect/sql` implementation using the `mysql2` library. | [README](https://github.com/Effect-TS/effect/blob/main/packages/sql-mysql2/README.md) | +| `@effect/sql-pg` | An `@effect/sql` implementation using the `postgres.js` library. | [README](https://github.com/Effect-TS/effect/blob/main/packages/sql-pg/README.md) | +| `@effect/sql-sqlite-bun` | An `@effect/sql` implementation using the `bun:sqlite` library. | [README](https://github.com/Effect-TS/effect/blob/main/packages/sql-sqlite-bun/README.md) | +| `@effect/sql-sqlite-do` | An `@effect/sql` implementation for Cloudflare Durable Objects sqlite storage. | [README](https://github.com/Effect-TS/effect/blob/main/packages/sql-sqlite-do/README.md) | +| `@effect/sql-sqlite-node` | An `@effect/sql` implementation using the `better-sqlite3` library. | [README](https://github.com/Effect-TS/effect/blob/main/packages/sql-sqlite-node/README.md) | +| `@effect/sql-sqlite-react-native` | An `@effect/sql` implementation using the `react-native-quick-sqlite` library. | [README](https://github.com/Effect-TS/effect/blob/main/packages/sql-sqlite-react-native/README.md) | +| `@effect/sql-sqlite-wasm` | An `@effect/sql` implementation using the `@sqlite.org/sqlite-wasm` library. | [README](https://github.com/Effect-TS/effect/blob/main/packages/sql-sqlite-wasm/README.md) | +| `@effect/typeclass` | Functional programming type classes | [README](https://github.com/Effect-TS/effect/blob/main/packages/typeclass/README.md) | +| `@effect/vitest` | Testing utilities for [Vitest](https://vitest.dev/) | [README](https://github.com/Effect-TS/effect/blob/main/packages/vitest/README.md) | +| `@effect/workflow` | Durable workflows for Effect | [README](https://github.com/Effect-TS/effect/blob/main/packages/workflow/README.md) | + +# Documentation + +## Website + +For detailed information and usage examples, visit the [Effect website](https://www.effect.website/). + +## API Reference + +For a complete API reference of the core package `effect`, see the [Effect API documentation](https://effect-ts.github.io/effect/). + +## Introduction to Effect + +Get started with Effect by watching our introductory video on YouTube. This video provides an overview of Effect and its key features: + +[![Introduction to Effect](https://img.youtube.com/vi/ViSiXfBKElQ/maxresdefault.jpg)](https://youtu.be/ViSiXfBKElQ) + +# Connect with Our Community + +Join the Effect community on Discord to connect with other developers, ask questions, and share insights: [Join Effect's Discord Community](https://discord.gg/hdt7t7jpvn). + +# Contributing via Pull Requests + +We welcome contributions via pull requests! Here are some guidelines to help you get started: + +## Setting Up Your Environment + +Begin by forking the repository and clone it to your local machine. + +Navigate into the cloned repository and create a new branch for your changes: + +```bash +git checkout -b my-branch +``` + +Ensure all required dependencies are installed by running: + +```bash +pnpm install # Requires pnpm version 10.4.0 +``` + +## Making Changes + +### Implement Your Changes + +Make the changes you propose to the codebase. If your changes impact functionality, please **add corresponding tests** to validate your updates. + +### Validate Your Changes + +Run the following commands to ensure your changes do not introduce any issues: + +- `pnpm codegen` (optional): Re-generate the package entrypoints in case you have changed the structure of a package or introduced a new module. +- `pnpm check`: Confirm that the code compiles without errors. +- `pnpm test`: Execute all unit tests to ensure your changes haven't broken existing functionality. +- `pnpm circular`: Check for any circular dependencies in imports. +- `pnpm lint`: Ensure the code adheres to our coding standards. + - If you encounter style issues, use `pnpm lint-fix` to automatically correct some of these. +- `pnpm test-types`: Run type-level tests. Tests are written using [tstyche](https://tstyche.org/). +- `pnpm docgen`: Ensure the documentation generates correctly and reflects any changes made. + +### Document Your Changes + +#### JSDoc Comments + +When adding a new feature, it's important to document your code using JSDoc comments. This helps other developers understand the purpose and usage of your changes. Include at least the following in your JSDoc comments: + +- **A Short Description**: Summarize the purpose and functionality of the feature. +- **Example**: Provide a usage example under the `@example` tag to demonstrate how to use the feature. +- **Since Version**: Use the `@since` tag to indicate the version in which the feature was introduced. If you're unsure about the version, please consult with a project maintainer. +- **Category (Optional)**: You can categorize the feature with the `@category` tag to help organize the documentation. If you're unsure about what category to assign, ask a project maintainer. + +**Note**: A HTML utility file, [`code2jsdoc-example.html`](/scripts/jsdocs/code2jsdoc-example.html), has been added to assist with creating JSDoc `@example` comments. This web-based interface includes two text areas: + +1. An input textarea for pasting example code. +2. An output textarea that dynamically generates formatted JSDoc `@example` comments. + +This utility helps ensure consistent formatting and streamlines the process of documenting examples. See the following example of its usage: + +Example Input: + +```ts +import { Effect } from "effect" + +console.log(Effect.runSyncExit(Effect.succeed(1))) +/* +Output: +{ + _id: "Exit", + _tag: "Success", + value: 1 +} +*/ +``` + +Output: + +```` +* +* @example +* ```ts +* import { Effect } from "effect" +* +* console.log(Effect.runSyncExit(Effect.succeed(1))) +* // Output: +* // { +* // _id: "Exit", +* // _tag: "Success", +* // value: 1 +* // } +* ``` +* +```` + +By using this utility, you can save time and maintain consistency in your JSDoc comments, especially for complex examples. + +#### Changeset Documentation + +Before committing your changes, document them with a changeset. This process helps in tracking modifications and effectively communicating them to the project team and users: + +```bash +pnpm changeset +``` + +During the changeset creation process, you will be prompted to select the appropriate level for your changes: + +- **patch**: Opt for this if you are making small fixes or minor changes that do not affect the library's overall functionality. +- **minor**: Choose this for new features that enhance functionality but do not disrupt existing features. +- **major**: Select this for any changes that result in backward-incompatible modifications to the library. + +## Finalizing Your Contribution + +### Commit Your Changes + +Once you have documented your changes with a changeset, it’s time to commit them to the repository. Use a clear and descriptive commit message, which could be the same message you used in your changeset: + +```bash +git commit -am 'Add some feature' +``` + +#### Linking to Issues + +If your commit addresses an open issue, reference the issue number directly in your commit message. This helps to link your contribution clearly to specific tasks or bug reports. Additionally, if your commit resolves the issue, you can indicate this by adding a phrase like `", closes #"`. For example: + +```bash +git commit -am 'Add some feature, closes #123' +``` + +This practice not only helps in tracking the progress of issues but also automatically closes the issue when the commit is merged, streamlining project management. + +### Push to Your Fork + +Push the changes up to your GitHub fork: + +```bash +git push origin my-branch +``` + +### Create a Pull Request + +Open a pull request against the appropriate branch on the original repository: + +- `main` branch: For minor patches or bug fixes. +- `next-minor` branch: For new features that are non-breaking. +- `next-major` branch: For changes that introduce breaking modifications. + +Please be patient! We will do our best to review your pull request as soon as possible. diff --git a/repos/effect/docker-compose.yaml b/repos/effect/docker-compose.yaml new file mode 100644 index 0000000..1a41e02 --- /dev/null +++ b/repos/effect/docker-compose.yaml @@ -0,0 +1,18 @@ +services: + pg: + image: postgres:alpine # Using a lightweight Postgres image + environment: + POSTGRES_DB: effect_cluster + POSTGRES_USER: cluster + POSTGRES_PASSWORD: cluster + ports: + - "5432:5432" # Map host port 5432 to container port 5432 + volumes: + - db_data:/var/lib/postgresql/data # Persist data in a named volume + redis: + image: redis:alpine + ports: + - "6379:6379" + +volumes: + db_data: # Define the named volume diff --git a/repos/effect/docs/_config.yml b/repos/effect/docs/_config.yml new file mode 100644 index 0000000..8d04ecd --- /dev/null +++ b/repos/effect/docs/_config.yml @@ -0,0 +1,5 @@ +remote_theme: mikearnaldi/just-the-docs +search_enabled: true +aux_links: + "GitHub": + - "//github.com/effect-ts/effect" diff --git a/repos/effect/docs/index.md b/repos/effect/docs/index.md new file mode 100644 index 0000000..5d05d39 --- /dev/null +++ b/repos/effect/docs/index.md @@ -0,0 +1,79 @@ +--- +title: Introduction +permalink: / +nav_order: 1 +has_children: false +has_toc: false +--- + +# Effect Monorepo + +> An ecosystem of tools to build robust applications in TypeScript + +## Introduction + +Welcome to Effect, a powerful TypeScript framework that provides a fully-fledged functional effect system with a rich standard library. + +Effect consists of several packages that work together to help build robust TypeScript applications. The core package, `effect`, serves as the foundation of the framework, offering primitives for managing side effects, ensuring type safety, and supporting concurrency. + +## Monorepo Structure + +The Effect monorepo is organized into multiple packages, each extending the core functionality. Below is an overview of the packages included: + +| Package | Description | | +| --------------------------------- | ------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- | +| `effect` | Core package | [README](https://github.com/Effect-TS/effect/blob/main/packages/effect/README.md) | +| `@effect/ai` | AI utilities | [README](https://github.com/Effect-TS/effect/blob/main/packages/ai/ai/README.md) | +| `@effect/ai-openai` | OpenAI utilities | [README](https://github.com/Effect-TS/effect/blob/main/packages/ai/openai/README.md) | +| `@effect/cli` | CLI utilities | [README](https://github.com/Effect-TS/effect/blob/main/packages/cli/README.md) | +| `@effect/cluster` | Distributed computing tools | [README](https://github.com/Effect-TS/effect/blob/main/packages/cluster/README.md) | +| `@effect/cluster-browser` | Cluster utilities for the browser | [README](https://github.com/Effect-TS/effect/blob/main/packages/cluster-browser/README.md) | +| `@effect/cluster-node` | Cluster utilities for [Node.js](https://nodejs.org) | [README](https://github.com/Effect-TS/effect/blob/main/packages/cluster-node/README.md) | +| `@effect/cluster-workflow` | Workflow management for clusters | [README](https://github.com/Effect-TS/effect/blob/main/packages/cluster-worflow/README.md) | +| `@effect/experimental` | Experimental features and APIs | [README](https://github.com/Effect-TS/effect/blob/main/packages/experimental/README.md) | +| `@effect/opentelemetry` | [OpenTelemetry](https://opentelemetry.io/) integration | [README](https://github.com/Effect-TS/effect/blob/main/packages/opentelemetry/README.md) | +| `@effect/platform` | Cross-platform runtime utilities | [README](https://github.com/Effect-TS/effect/blob/main/packages/platform/README.md) | +| `@effect/platform-browser` | Platform utilities for the browser | [README](https://github.com/Effect-TS/effect/blob/main/packages/platform-browser/README.md) | +| `@effect/platform-bun` | Platform utilities for [Bun](https://bun.sh/) | [README](https://github.com/Effect-TS/effect/blob/main/packages/platform-bun/README.md) | +| `@effect/platform-node` | Platform utilities for [Node.js](https://nodejs.org) | [README](https://github.com/Effect-TS/effect/blob/main/packages/platform-node/README.md) | +| `@effect/platform-node-shared` | Shared utilities for [Node.js](https://nodejs.org) | [README](https://github.com/Effect-TS/effect/blob/main/packages/platform-node-shared/README.md) | +| `@effect/printer` | General-purpose printing utilities | [README](https://github.com/Effect-TS/effect/blob/main/packages/printer/README.md) | +| `@effect/printer-ansi` | ANSI-compatible printing utilities | [README](https://github.com/Effect-TS/effect/blob/main/packages/printer-ansi/README.md) | +| `@effect/rpc` | Remote procedure call (RPC) utilities | [README](https://github.com/Effect-TS/effect/blob/main/packages/rpc/README.md) | +| `@effect/rpc-http` | HTTP-based RPC utilities | [README](https://github.com/Effect-TS/effect/blob/main/packages/rpc-http/README.md) | +| `@effect/sql` | SQL database utilities | [README](https://github.com/Effect-TS/effect/blob/main/packages/sql/README.md) | +| `@effect/sql-clickhouse` | An `@effect/sql` implementation for [ClickHouse](https://clickhouse.com/). | [README](https://github.com/Effect-TS/effect/blob/main/packages/sql-clickhouse/README.md) | +| `@effect/sql-d1` | An `@effect/sql` implementation for [Cloudflare D1](https://developers.cloudflare.com/d1/). | [README](https://github.com/Effect-TS/effect/blob/main/packages/sql-d1/README.md) | +| `@effect/sql-drizzle` | An `@effect/sql` implementation for [Drizzle](https://orm.drizzle.team/). | [README](https://github.com/Effect-TS/effect/blob/main/packages/sql-drizzle/README.md) | +| `@effect/sql-kysely` | An `@effect/sql` implementation for [Kysely](https://kysely.dev/). | [README](https://github.com/Effect-TS/effect/blob/main/packages/sql-kysely/README.md) | +| `@effect/sql-libsql` | An `@effect/sql` implementation using the `@libsql/client` library. | [README](https://github.com/Effect-TS/effect/blob/main/packages/sql-libsql/README.md) | +| `@effect/sql-mssql` | An `@effect/sql` implementation using the mssql `tedious` library. | [README](https://github.com/Effect-TS/effect/blob/main/packages/sql-mssql/README.md) | +| `@effect/sql-mysql2` | An `@effect/sql` implementation using the `mysql2` library. | [README](https://github.com/Effect-TS/effect/blob/main/packages/sql-mysql2/README.md) | +| `@effect/sql-pg` | An `@effect/sql` implementation using the `postgres.js` library. | [README](https://github.com/Effect-TS/effect/blob/main/packages/sql-pg/README.md) | +| `@effect/sql-sqlite-bun` | An `@effect/sql` implementation using the `bun:sqlite` library. | [README](https://github.com/Effect-TS/effect/blob/main/packages/sql-sqlite-bun/README.md) | +| `@effect/sql-sqlite-do` | An `@effect/sql` implementation for Cloudflare Durable Objects sqlite storage. | [README](https://github.com/Effect-TS/effect/blob/main/packages/sql-sqlite-do/README.md) | +| `@effect/sql-sqlite-node` | An `@effect/sql` implementation using the `better-sqlite3` library. | [README](https://github.com/Effect-TS/effect/blob/main/packages/sql-sqlite-node/README.md) | +| `@effect/sql-sqlite-react-native` | An `@effect/sql` implementation using the `react-native-quick-sqlite` library. | [README](https://github.com/Effect-TS/effect/blob/main/packages/sql-sqlite-react-native/README.md) | +| `@effect/sql-sqlite-wasm` | An `@effect/sql` implementation using the `@sqlite.org/sqlite-wasm` library. | [README](https://github.com/Effect-TS/effect/blob/main/packages/sql-sqlite-wasm/README.md) | +| `@effect/typeclass` | Functional programming type classes | [README](https://github.com/Effect-TS/effect/blob/main/packages/typeclass/README.md) | +| `@effect/vitest` | Testing utilities for [Vitest](https://vitest.dev/) | [README](https://github.com/Effect-TS/effect/blob/main/packages/vitest/README.md) | + +# Documentation + +## Website + +For detailed information and usage examples, visit the [Effect website](https://www.effect.website/). + +## API Reference + +For a complete API reference of the core package `effect`, see the [Effect API documentation](https://effect-ts.github.io/effect/). + +## Introduction to Effect + +Get started with Effect by watching our introductory video on YouTube. This video provides an overview of Effect and its key features: + +[![Introduction to Effect](https://img.youtube.com/vi/ViSiXfBKElQ/maxresdefault.jpg)](https://youtu.be/ViSiXfBKElQ) + +# Connect with Our Community + +Join the Effect community on Discord to connect with other developers, ask questions, and share insights: [Join Effect's Discord Community](https://discord.gg/hdt7t7jpvn). diff --git a/repos/effect/eslint.config.mjs b/repos/effect/eslint.config.mjs new file mode 100644 index 0000000..ff0e6bd --- /dev/null +++ b/repos/effect/eslint.config.mjs @@ -0,0 +1,161 @@ +import * as effectEslint from "@effect/eslint-plugin" +import { fixupPluginRules } from "@eslint/compat" +import { FlatCompat } from "@eslint/eslintrc" +import js from "@eslint/js" +import tsParser from "@typescript-eslint/parser" +import codegen from "eslint-plugin-codegen" +import _import from "eslint-plugin-import" +import simpleImportSort from "eslint-plugin-simple-import-sort" +import sortDestructureKeys from "eslint-plugin-sort-destructure-keys" +import path from "node:path" +import { fileURLToPath } from "node:url" + +const __filename = fileURLToPath(import.meta.url) +const __dirname = path.dirname(__filename) +const compat = new FlatCompat({ + baseDirectory: __dirname, + recommendedConfig: js.configs.recommended, + allConfig: js.configs.all +}) + +export default [ + { + ignores: [ + "**/dist", + "**/build", + "**/docs", + "**/.repos/**", + "**/.lalph/**", + "**/*.md" + ] + }, + ...compat.extends( + "eslint:recommended", + "plugin:@typescript-eslint/eslint-recommended", + "plugin:@typescript-eslint/recommended" + ), + ...effectEslint.configs.dprint, + { + plugins: { + import: fixupPluginRules(_import), + "sort-destructure-keys": sortDestructureKeys, + "simple-import-sort": simpleImportSort, + codegen + }, + + languageOptions: { + parser: tsParser, + ecmaVersion: 2018, + sourceType: "module" + }, + + settings: { + "import/parsers": { + "@typescript-eslint/parser": [".ts", ".tsx"] + }, + + "import/resolver": { + typescript: { + alwaysTryTypes: true + } + } + }, + + rules: { + "codegen/codegen": "error", + "no-fallthrough": "off", + "no-irregular-whitespace": "off", + "object-shorthand": "error", + "prefer-destructuring": "off", + "sort-imports": "off", + + "no-restricted-syntax": [ + "error", + { + selector: + "CallExpression[callee.property.name='push'] > SpreadElement.arguments", + message: "Do not use spread arguments in Array.push" + } + ], + + "no-unused-vars": "off", + "require-yield": "off", + "prefer-rest-params": "off", + "prefer-spread": "off", + "import/first": "error", + "import/newline-after-import": "error", + "import/no-duplicates": "error", + "import/no-unresolved": "off", + "import/order": "off", + "simple-import-sort/imports": "off", + "sort-destructure-keys/sort-destructure-keys": "error", + "deprecation/deprecation": "off", + + "@typescript-eslint/array-type": [ + "warn", + { + default: "generic", + readonly: "generic" + } + ], + + "@typescript-eslint/member-delimiter-style": 0, + "@typescript-eslint/no-non-null-assertion": "off", + "@typescript-eslint/ban-types": "off", + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-empty-object-type": "off", + "@typescript-eslint/no-unsafe-function-type": "off", + "@typescript-eslint/no-wrapper-object-types": "off", + "@typescript-eslint/consistent-type-imports": "warn", + + "@typescript-eslint/no-unused-vars": [ + "error", + { + argsIgnorePattern: "^_", + varsIgnorePattern: "^_" + } + ], + + "@typescript-eslint/ban-ts-comment": "off", + "@typescript-eslint/camelcase": "off", + "@typescript-eslint/explicit-function-return-type": "off", + "@typescript-eslint/explicit-module-boundary-types": "off", + "@typescript-eslint/interface-name-prefix": "off", + "@typescript-eslint/no-array-constructor": "off", + "@typescript-eslint/no-use-before-define": "off", + "@typescript-eslint/no-namespace": "off", + + "@effect/dprint": [ + "error", + { + config: { + indentWidth: 2, + lineWidth: 120, + semiColons: "asi", + quoteStyle: "alwaysDouble", + trailingCommas: "never", + operatorPosition: "maintain", + "arrowFunction.useParentheses": "force" + } + } + ] + } + }, + { + files: ["packages/*/src/**/*", "packages/*/test/**/*"], + rules: { + "no-console": "error" + } + }, + { + files: ["packages/*/src/**/*"], + rules: { + "@effect/no-import-from-barrel-package": [ + "error", + { + packageNames: ["effect", "@effect/platform", "@effect/sql"] + } + ] + } + } +] diff --git a/repos/effect/flake.lock b/repos/effect/flake.lock new file mode 100644 index 0000000..4159b96 --- /dev/null +++ b/repos/effect/flake.lock @@ -0,0 +1,27 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1762361079, + "narHash": "sha256-lz718rr1BDpZBYk7+G8cE6wee3PiBUpn8aomG/vLLiY=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "ffcdcf99d65c61956d882df249a9be53e5902ea5", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/repos/effect/flake.nix b/repos/effect/flake.nix new file mode 100644 index 0000000..9849215 --- /dev/null +++ b/repos/effect/flake.nix @@ -0,0 +1,29 @@ +{ + inputs = { + nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable"; + }; + outputs = + { nixpkgs, ... }: + let + forAllSystems = + function: + nixpkgs.lib.genAttrs nixpkgs.lib.systems.flakeExposed ( + system: function nixpkgs.legacyPackages.${system} + ); + in + { + formatter = forAllSystems (pkgs: pkgs.alejandra); + devShells = forAllSystems (pkgs: { + default = pkgs.mkShell { + packages = with pkgs; [ + bun + corepack + deno + nodejs_24 + python3 + yq-go + ]; + }; + }); + }; +} diff --git a/repos/effect/package.json b/repos/effect/package.json new file mode 100644 index 0000000..d1581ec --- /dev/null +++ b/repos/effect/package.json @@ -0,0 +1,100 @@ +{ + "private": true, + "type": "module", + "packageManager": "pnpm@10.17.1+sha512.17c560fca4867ae9473a3899ad84a88334914f379be46d455cbf92e5cf4b39d34985d452d2583baf19967fa76cb5c17bc9e245529d0b98745721aa7200ecaf7a", + "scripts": { + "clean": "node scripts/clean.mjs", + "codegen": "pnpm --recursive --parallel --filter \"./packages/**/*\" run codegen", + "codemod": "node scripts/codemod.mjs", + "build": "tsc -b tsconfig.build.json && pnpm --recursive --parallel --filter \"./packages/**/*\" run build", + "circular": "node scripts/circular.mjs", + "test": "vitest", + "coverage": "vitest --coverage", + "check": "tsc -b tsconfig.json", + "check-recursive": "pnpm --recursive --filter \"./packages/**/*\" exec tsc -b tsconfig.json", + "lint": "eslint \"**/{src,test,examples,scripts,dtslint}/**/*.{ts,mjs}\"", + "lint-fix": "pnpm lint --fix", + "docgen": "pnpm --recursive --filter \"./packages/**/*\" exec docgen && node scripts/docs.mjs", + "test-types": "tstyche", + "changeset-version": "changeset version && node scripts/version.mjs", + "changeset-publish": "pnpm codemod && pnpm build && TEST_DIST= pnpm vitest && changeset publish" + }, + "resolutions": { + "dependency-tree": "^10.0.9", + "detective-amd": "^5.0.2", + "detective-cjs": "^5.0.1", + "detective-es6": "^4.0.1", + "detective-less": "^1.0.2", + "detective-postcss": "^6.1.3", + "detective-sass": "^5.0.3", + "detective-scss": "^4.0.3", + "detective-stylus": "^4.0.0", + "detective-typescript": "^11.1.0" + }, + "devDependencies": { + "@babel/cli": "^7.27.2", + "@babel/core": "^7.27.1", + "@babel/plugin-transform-export-namespace-from": "^7.27.1", + "@babel/plugin-transform-modules-commonjs": "^7.27.1", + "@changesets/changelog-github": "^0.5.1", + "@changesets/cli": "^2.29.4", + "@edge-runtime/vm": "^5.0.0", + "@effect/build-utils": "^0.8.3", + "@effect/docgen": "https://pkg.pr.new/Effect-TS/docgen/@effect/docgen@e7fe055", + "@effect/eslint-plugin": "^0.3.2", + "@effect/language-service": "^0.23.3", + "@effect/vitest": "workspace:^", + "@eslint/compat": "^1.2.9", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "^9.26.0", + "@types/jscodeshift": "^17.3.0", + "@types/node": "^22.15.18", + "@typescript-eslint/eslint-plugin": "^8.32.1", + "@typescript-eslint/parser": "^8.32.1", + "@vitest/browser": "^3.2.4", + "@vitest/coverage-v8": "^3.2.4", + "@vitest/expect": "^3.2.4", + "@vitest/web-worker": "^3.2.4", + "ast-types": "^0.14.2", + "babel-plugin-annotate-pure-calls": "^0.5.0", + "eslint": "^9.26.0", + "eslint-import-resolver-typescript": "^4.3.4", + "eslint-plugin-codegen": "^0.30.0", + "eslint-plugin-import": "^2.31.0", + "eslint-plugin-simple-import-sort": "^12.1.1", + "eslint-plugin-sort-destructure-keys": "^2.0.0", + "glob": "^11.0.2", + "jscodeshift": "^17.3.0", + "madge": "^8.0.0", + "playwright": "^1.52.0", + "prettier": "^3.5.3", + "rimraf": "^6.0.1", + "tstyche": "^6.0.0-beta.5", + "tsx": "^4.19.4", + "typescript": "^5.8.3", + "vite": "^6.1.1", + "vitest": "^3.2.4" + }, + "pnpm": { + "patchedDependencies": { + "@changesets/assemble-release-plan": "patches/@changesets__assemble-release-plan.patch", + "@changesets/get-github-info@0.6.0": "patches/@changesets__get-github-info@0.6.0.patch" + }, + "ignoredBuiltDependencies": [ + "cpu-features", + "esbuild", + "lmdb", + "msgpackr-extract", + "msw", + "protobufjs", + "ssh2", + "workerd" + ], + "onlyBuiltDependencies": [ + "@parcel/watcher", + "better-sqlite3", + "sharp", + "unrs-resolver" + ] + } +} diff --git a/repos/effect/packages/ai/ai/CHANGELOG.md b/repos/effect/packages/ai/ai/CHANGELOG.md new file mode 100644 index 0000000..eee8a0f --- /dev/null +++ b/repos/effect/packages/ai/ai/CHANGELOG.md @@ -0,0 +1,2489 @@ +# @effect/ai + +## 0.35.0 + +### Patch Changes + +- Updated dependencies [[`f7bb09b`](https://github.com/Effect-TS/effect/commit/f7bb09b022f195d1f2b3c23d49e74b011ec5d109), [`bd7552a`](https://github.com/Effect-TS/effect/commit/bd7552a19cc0ed575507ac6cc0879a57e24ebd31), [`ad1a7eb`](https://github.com/Effect-TS/effect/commit/ad1a7eb7f6bebaf91c80be2443ac0439226d0098), [`0d32048`](https://github.com/Effect-TS/effect/commit/0d32048f9836e2b23a6ba3ec5f43f0a000bb92fb), [`0d32048`](https://github.com/Effect-TS/effect/commit/0d32048f9836e2b23a6ba3ec5f43f0a000bb92fb)]: + - effect@3.21.0 + - @effect/experimental@0.60.0 + - @effect/platform@0.96.0 + - @effect/rpc@0.75.0 + +## 0.34.0 + +### Patch Changes + +- [#6094](https://github.com/Effect-TS/effect/pull/6094) [`88a5260`](https://github.com/Effect-TS/effect/commit/88a5260788d57bb0c31a645fa44b5a68f5c73c38) Thanks @IMax153! - Remove superfluous / defensive check from tool call json schema generation + +- [#6101](https://github.com/Effect-TS/effect/pull/6101) [`cf74940`](https://github.com/Effect-TS/effect/commit/cf749405214282edc916a783b3417c766d2bbfe3) Thanks @IMax153! - Prevent schema validation when directly constructing an `AiError.HttpRequestError` / `AiError.HttpResponseError` + +- Updated dependencies [[`fc82e81`](https://github.com/Effect-TS/effect/commit/fc82e81448bd9136a37580139ce46a2c61b11b54), [`82996bc`](https://github.com/Effect-TS/effect/commit/82996bce8debffcb44feb98bb862cf2662bd56b7), [`4d97a61`](https://github.com/Effect-TS/effect/commit/4d97a61a15b9dd6a0eece65b8f0c035e16d42ada), [`f6b0960`](https://github.com/Effect-TS/effect/commit/f6b0960bf3184109920dfed16ee7dfd7d67bc0f2), [`8798a84`](https://github.com/Effect-TS/effect/commit/8798a843218e6c0c0d3a8eee83360880e370b4da)]: + - effect@3.20.0 + - @effect/experimental@0.59.0 + - @effect/platform@0.95.0 + - @effect/rpc@0.74.0 + +## 0.33.2 + +### Patch Changes + +- [#5944](https://github.com/Effect-TS/effect/pull/5944) [`f21f034`](https://github.com/Effect-TS/effect/commit/f21f03477ea9b6fd4662a93097290cb940ce9c2b) Thanks @IMax153! - Fix Prompt.fromResponseParts when input contains a provider executed tool + +## 0.33.1 + +### Patch Changes + +- [#5931](https://github.com/Effect-TS/effect/pull/5931) [`ba9e790`](https://github.com/Effect-TS/effect/commit/ba9e7908a80a55f24217c88af4f7d89a4f7bc0e4) Thanks @IMax153! - Fix the accumulation logic for response parts in the AI `Chat` module + +- Updated dependencies [[`65e9e35`](https://github.com/Effect-TS/effect/commit/65e9e35157cbdfb40826ddad34555c4ebcf7c0b0), [`ee69cd7`](https://github.com/Effect-TS/effect/commit/ee69cd796feb3d8d1046f52edd8950404cd4ed0e), [`488d6e8`](https://github.com/Effect-TS/effect/commit/488d6e870eda3dfc137f4940bb69416f61ed8fe3)]: + - @effect/platform@0.94.1 + - effect@3.19.14 + +## 0.33.0 + +### Patch Changes + +- Updated dependencies [[`77eeb86`](https://github.com/Effect-TS/effect/commit/77eeb86ddf208e51ec25932af83d52d3b4700371), [`ff7053f`](https://github.com/Effect-TS/effect/commit/ff7053f6d8508567b6145239f97aacc5773b0c53), [`287c32c`](https://github.com/Effect-TS/effect/commit/287c32c9f10da8e96f2b9ef8424316189d9ad4b3)]: + - effect@3.19.13 + - @effect/platform@0.94.0 + - @effect/experimental@0.58.0 + - @effect/rpc@0.73.0 + +## 0.32.1 + +### Patch Changes + +- [#5685](https://github.com/Effect-TS/effect/pull/5685) [`fb53370`](https://github.com/Effect-TS/effect/commit/fb533705e058500a7192810f5373f663c33f0ca4) Thanks @janglad! - Fix Response.Part, AllParts, StreamPart not inferring Schema properly if toolkit is WithHandler + +- Updated dependencies [[`7d28a90`](https://github.com/Effect-TS/effect/commit/7d28a908f965854cff386a19515141aea5b39eb7)]: + - effect@3.19.3 + +## 0.32.0 + +### Patch Changes + +- Updated dependencies [[`3c15d5f`](https://github.com/Effect-TS/effect/commit/3c15d5f99fb8d8470a00c5a33d9ba3cac89dfe4c), [`3863fa8`](https://github.com/Effect-TS/effect/commit/3863fa89f61e63e5529fd961e37333bddf7db64a), [`2a03c76`](https://github.com/Effect-TS/effect/commit/2a03c76c2781ca7e9e228e838eab2eb0d0795b1d), [`24a1685`](https://github.com/Effect-TS/effect/commit/24a1685c70a9ed157468650f95a5c3da3f2c2433)]: + - effect@3.19.0 + - @effect/rpc@0.72.0 + - @effect/platform@0.93.0 + - @effect/experimental@0.57.0 + +## 0.31.1 + +### Patch Changes + +- [#5634](https://github.com/Effect-TS/effect/pull/5634) [`a5d0f2b`](https://github.com/Effect-TS/effect/commit/a5d0f2bd54ec4a791a2ea5b31493bd55882066e0) Thanks @IMax153! - Ensure that tool calls are emitted as soon as possible when streaming + +## 0.31.0 + +### Minor Changes + +- [#5621](https://github.com/Effect-TS/effect/pull/5621) [`4c3bdfb`](https://github.com/Effect-TS/effect/commit/4c3bdfbcbc2dcd7ecd6321df3e4a504af19de825) Thanks @IMax153! - Remove `Either` / `EitherEncoded` from tool call results. + + Specifically, the encoding of tool call results as an `Either` / `EitherEncoded` has been removed and is replaced by encoding the tool call success / failure directly into the `result` property. + + To allow type-safe discrimination between a tool call result which was a success vs. one that was a failure, an `isFailure` property has also been added to the `"tool-result"` part. If `isFailure` is `true`, then the tool call handler result was an error. + + ```ts + import * as AnthropicClient from "@effect/ai-anthropic/AnthropicClient" + import * as AnthropicLanguageModel from "@effect/ai-anthropic/AnthropicLanguageModel" + import * as LanguageModel from "@effect/ai/LanguageModel" + import * as Tool from "@effect/ai/Tool" + import * as Toolkit from "@effect/ai/Toolkit" + import * as NodeHttpClient from "@effect/platform-node/NodeHttpClient" + import { Config, Effect, Layer, Schema, Stream } from "effect" + + const Claude = AnthropicLanguageModel.model("claude-4-sonnet-20250514") + + const MyTool = Tool.make("MyTool", { + description: "An example of a tool with success and failure types", + failureMode: "return", // Return errors in the response + parameters: { bar: Schema.Number }, + success: Schema.Number, + failure: Schema.Struct({ reason: Schema.Literal("reason-1", "reason-2") }) + }) + + const MyToolkit = Toolkit.make(MyTool) + + const MyToolkitLayer = MyToolkit.toLayer({ + MyTool: () => Effect.succeed(42) + }) + + const program = LanguageModel.streamText({ + prompt: "Tell me about the meaning of life", + toolkit: MyToolkit + }).pipe( + Stream.runForEach((part) => { + if (part.type === "tool-result" && part.name === "MyTool") { + // The `isFailure` property can be used to discriminate whether the result + // of a tool call is a success or a failure + if (part.isFailure) { + part.result + // ^? { readonly reason: "reason-1" | "reason-2"; } + } else { + part.result + // ^? number + } + } + return Effect.void + }), + Effect.provide(Claude) + ) + + const Anthropic = AnthropicClient.layerConfig({ + apiKey: Config.redacted("ANTHROPIC_API_KEY") + }).pipe(Layer.provide(NodeHttpClient.layerUndici)) + + program.pipe(Effect.provide([Anthropic, MyToolkitLayer]), Effect.runPromise) + ``` + +## 0.30.0 + +### Minor Changes + +- [#5614](https://github.com/Effect-TS/effect/pull/5614) [`c63e658`](https://github.com/Effect-TS/effect/commit/c63e6582244fbb50d31650c4b4ea0660fe194652) Thanks @IMax153! - Previously, tool call handler errors were _always_ raised as an expected error in the Effect `E` channel at the point of execution of the tool call handler (i.e. when a `generate*` method is invoked on a `LanguageModel`). + + With this PR, the end user now has control over whether tool call handler errors should be raised as an Effect error, or returned by the SDK to allow, for example, sending that error information to another application. + + ### Tool Call Specification + + The `Tool.make` and `Tool.providerDefined` constructors now take an extra optional parameter called `failureMode`, which can be set to either `"error"` or `"return"`. + + ```ts + import { Tool } from "@effect/ai" + import { Schema } from "effect" + + const MyTool = Tool.make("MyTool", { + description: "My special tool", + failureMode: "return" // "error" (default) or "return" + parameters: { + myParam: Schema.String + }, + success: Schema.Struct({ + mySuccess: Schema.String + }), + failure: Schema.Struct({ + myFailure: Schema.String + }) + }) + + ``` + + The semantics of `failureMode` are as follows: + - If set to `"error"` (the default), errors that occur during tool call handler execution will be returned in the error channel of the calling effect + - If set to `"return"`, errors that occur during tool call handler execution will be captured and returned as part of the tool call result + + ### Response - Tool Result Parts + + The `result` field of a `"tool-result"` part of a large language model provider response is now represented as an `Either`. + - If the `result` is a `Left`, the `result` will be the `failure` specified in the tool call specification + - If the `result` is a `Right`, the `result` will be the `success` specified in the tool call specification + + This is only relevant if the end user sets `failureMode` to `"return"`. If set to `"error"` (the default), then the `result` property will always be a `Right` with the successful result of the tool call handler. + + Similarly the `encodedResult` field of a `"tool-result"` part will be represented as an `EitherEncoded`, where: + - `{ _tag: "Left", left: }` represents a tool call handler failure + - `{ _tag: "Right", right: }` represents a tool call handler success + + ### Prompt - Tool Result Parts + + The `result` field of a `"tool-result"` part of a prompt will now only accept an `EitherEncoded` as specified above. + +### Patch Changes + +- Updated dependencies [[`6ae2f5d`](https://github.com/Effect-TS/effect/commit/6ae2f5da45a9ed9832605eca12b3e2bf2e2a1a67)]: + - effect@3.18.4 + +## 0.29.1 + +### Patch Changes + +- [#5587](https://github.com/Effect-TS/effect/pull/5587) [`d628c15`](https://github.com/Effect-TS/effect/commit/d628c1527898dc13176185d52e5ea9ee68fd3934) Thanks @IMax153! - Ensure response schema includes toolkit tools even when tool call resolution is disabled + +## 0.29.0 + +### Minor Changes + +- [#5302](https://github.com/Effect-TS/effect/pull/5302) [`f8b93ac`](https://github.com/Effect-TS/effect/commit/f8b93ac6446efd3dd790778b0fc71d299a38f272) Thanks @timurrakhimzhan! - Added "oneOf" property for generateText and streamText "toolChoice" to be able to pick a subset of tools, which are going to be passed to LLM + +### Patch Changes + +- Updated dependencies [[`1c6ab74`](https://github.com/Effect-TS/effect/commit/1c6ab74b314b2b6df8bb1b1a0cb9527ceda0e3fa), [`70fe803`](https://github.com/Effect-TS/effect/commit/70fe803469db3355ffbf8359b52c351f1c2dc137), [`c296e32`](https://github.com/Effect-TS/effect/commit/c296e32554143b84ae8987046984e1cf1852417c), [`a098ddf`](https://github.com/Effect-TS/effect/commit/a098ddfc551f5aa0a7c36f9b4928372a64d4d9f2)]: + - effect@3.18.0 + - @effect/platform@0.92.0 + - @effect/experimental@0.56.0 + - @effect/rpc@0.71.0 + +## 0.28.4 + +### Patch Changes + +- [#5570](https://github.com/Effect-TS/effect/pull/5570) [`bc83015`](https://github.com/Effect-TS/effect/commit/bc83015e2546957520a91ff6f5728e40c75605b7) Thanks @IMax153! - Redact headers where possible in AiError + +- [#5570](https://github.com/Effect-TS/effect/pull/5570) [`bc83015`](https://github.com/Effect-TS/effect/commit/bc83015e2546957520a91ff6f5728e40c75605b7) Thanks @IMax153! - Fix return type of `Response.AllParts` + +- [#5568](https://github.com/Effect-TS/effect/pull/5568) [`6c2d586`](https://github.com/Effect-TS/effect/commit/6c2d586ae4dab17d9622424f0e95808131888a1a) Thanks @IMax153! - Support expiration of persisted chats via time to live + +## 0.28.3 + +### Patch Changes + +- [#5566](https://github.com/Effect-TS/effect/pull/5566) [`ab57b7a`](https://github.com/Effect-TS/effect/commit/ab57b7af5349fcf22e6c702b793d01079dba69a6) Thanks @IMax153! - Support generation of persistence identifiers for ai chat + +- [#5540](https://github.com/Effect-TS/effect/pull/5540) [`5b13482`](https://github.com/Effect-TS/effect/commit/5b134824c1ce7d754deba0a76ac9e2a37d156ef1) Thanks @IMax153! - ensure decoded response metadata is always set + +- [#5566](https://github.com/Effect-TS/effect/pull/5566) [`ab57b7a`](https://github.com/Effect-TS/effect/commit/ab57b7af5349fcf22e6c702b793d01079dba69a6) Thanks @IMax153! - fix prompt construction from response parts + +## 0.28.2 + +### Patch Changes + +- [#5554](https://github.com/Effect-TS/effect/pull/5554) [`800ab2e`](https://github.com/Effect-TS/effect/commit/800ab2e6d983ed424deb10aebee720cfc666df7a) Thanks @IMax153! - Accept `Prompt.RawInput` in `Prompt.merge` + +- [#5554](https://github.com/Effect-TS/effect/pull/5554) [`800ab2e`](https://github.com/Effect-TS/effect/commit/800ab2e6d983ed424deb10aebee720cfc666df7a) Thanks @IMax153! - Add `Prompt.setSystem`, `Prompt.prependSystem`, and `Prompt.appendSystem` methods + +- [#5554](https://github.com/Effect-TS/effect/pull/5554) [`800ab2e`](https://github.com/Effect-TS/effect/commit/800ab2e6d983ed424deb10aebee720cfc666df7a) Thanks @IMax153! - Improve the information available to the user following a model response error + +- [#5554](https://github.com/Effect-TS/effect/pull/5554) [`800ab2e`](https://github.com/Effect-TS/effect/commit/800ab2e6d983ed424deb10aebee720cfc666df7a) Thanks @IMax153! - Allow raw user and assistant prompt messages to accept plain strings + +- [#5554](https://github.com/Effect-TS/effect/pull/5554) [`800ab2e`](https://github.com/Effect-TS/effect/commit/800ab2e6d983ed424deb10aebee720cfc666df7a) Thanks @IMax153! - Make `Prompt` pipeable + +- [#5554](https://github.com/Effect-TS/effect/pull/5554) [`800ab2e`](https://github.com/Effect-TS/effect/commit/800ab2e6d983ed424deb10aebee720cfc666df7a) Thanks @IMax153! - Fix leakage of provider-defined tool handler requirements + +## 0.28.1 + +### Patch Changes + +- [#5555](https://github.com/Effect-TS/effect/pull/5555) [`aacd472`](https://github.com/Effect-TS/effect/commit/aacd472a9bab389269f74d923ac20cc895c638ee) Thanks @IMax153! - Add support for persisting a `Chat` via `Persistence` + +## 0.28.0 + +### Patch Changes + +- Updated dependencies [[`d4d86a8`](https://github.com/Effect-TS/effect/commit/d4d86a81f02b94e09fce8004ce2c5369c505ca5a)]: + - @effect/platform@0.91.0 + - @effect/rpc@0.70.0 + - @effect/experimental@0.55.0 + +## 0.27.1 + +### Patch Changes + +- [#5521](https://github.com/Effect-TS/effect/pull/5521) [`fa49bc8`](https://github.com/Effect-TS/effect/commit/fa49bc86b14599300d106f306ceaf82a79121b80) Thanks @IMax153! - Fix provider metadata and parse tool call parameters safely + +## 0.27.0 + +### Minor Changes + +- [#5469](https://github.com/Effect-TS/effect/pull/5469) [`42b914a`](https://github.com/Effect-TS/effect/commit/42b914a0e8750350ce17d434afaec7d655ddf4b7) Thanks @IMax153! - Refactor the Effect AI SDK and associated provider packages + + This pull request contains a complete refactor of the base Effect AI SDK package + as well as the associated provider integration packages to improve flexibility + and enhance ergonomics. Major changes are outlined below. + + ## Modules + + All modules in the base Effect AI SDK have had the leading `Ai` prefix dropped + from their name (except for the `AiError` module). + + For example, the `AiLanguageModel` module is now the `LanguageModel` module. + + In addition, the `AiInput` module has been renamed to the `Prompt` module. + + ## Prompts + + The `Prompt` module has been completely redesigned with flexibility in mind. + + The `Prompt` module now supports building a prompt using either the constructors + exposed from the `Prompt` module, or using raw prompt content parts / messages, + which should be familiar to those coming from other AI SDKs. + + In addition, the `system` option has been removed from all `LanguageModel` methods + and must now be provided as part of the prompt. + + **Prompt Constructors** + + ```ts + import { LanguageModel, Prompt } from "@effect/ai" + + const textPart = Prompt.makePart("text", { + text: "What is machine learning?" + }) + + const userMessage = Prompt.makeMessage("user", { + content: [textPart] + }) + + const systemMessage = Prompt.makeMessage("system", { + content: "You are an expert in machine learning" + }) + + const program = LanguageModel.generateText({ + prompt: Prompt.fromMessages([systemMessage, userMessage]) + }) + ``` + + **Raw Prompt Input** + + ```ts + import { LanguageModel } from "@effect/ai" + + const program = LanguageModel.generateText({ + prompt: [ + { role: "system", content: "You are an expert in machine learning" }, + { + role: "user", + content: [{ type: "text", text: "What is machine learning?" }] + } + ] + }) + ``` + + **NOTE**: Providing a plain string as a prompt is still supported, and will be converted + internally into a user message with a single text content part. + + ### Provider-Specific Options + + To support specification of provider-specific options when interacting with large + language model providers, support has been added for adding provider-specific + options to the parts of a `Prompt`. + + ```ts + import { LanguageModel } from "@effect/ai" + import { AnthropicLanguageModel } from "@effect/ai-anthropic" + + const Claude = AnthropicLanguageModel.model("claude-sonnet-4-20250514") + + const program = LanguageModel.generateText({ + prompt: [ + { + role: "user", + content: [{ type: "text", text: "What is machine learning?" }], + options: { + anthropic: { cacheControl: { type: "ephemeral", ttl: "1h" } } + } + } + ] + }).pipe(Effect.provide(Claude)) + ``` + + ## Responses + + The `Response` module has also been completely redesigned to support a wider + variety of response parts, particularly when streaming. + + ### Streaming Responses + + When streaming text via the `LanguageModel.streamText` method, you will now + receive a stream of content parts instead of a stream of responses, which should + make it much simpler to filter down the stream to the parts you are interested in. + + In addition, additional content parts will be present in the stream to allow you to track, + for example, when a text content part starts / ends. + + ### Tool Calls / Tool Call Results + + The decoded parts of a `Response` (as returned by the methods of `LanguageModel`) + are now fully type-safe on tool calls / tool call results. Filtering the content parts of a + response to tool calls will narrow the type of the tool call `params` based on the tool + `name`. Similarly, filtering the response to tool call results will narrow the type of the + tool call `result` based on the tool `name`. + + ```ts + import { LanguageModel, Tool, Toolkit } from "@effect/ai" + import { Effect, Schema } from "effect" + + const DadJokeTool = Tool.make("DadJokeTool", { + parameters: { topic: Schema.String }, + success: Schema.Struct({ joke: Schema.String }) + }) + + const FooTool = Tool.make("FooTool", { + parameters: { foo: Schema.Number }, + success: Schema.Struct({ bar: Schema.Boolean }) + }) + + const MyToolkit = Toolkit.make(DadJokeTool, FooTool) + + const program = Effect.gen(function* () { + const response = yield* LanguageModel.generateText({ + prompt: "Tell me a dad joke", + toolkit: MyToolkit + }) + + for (const toolCall of response.toolCalls) { + if (toolCall.name === "DadJokeTool") { + // ^? "DadJokeTool" | "FooTool" + toolCall.params + // ^? { readonly topic: string } + } + } + + for (const toolResult of response.toolResults) { + if (toolResult.name === "DadJokeTool") { + // ^? "DadJokeTool" | "FooTool" + toolResult.result + // ^? { readonly joke: string } + } + } + }) + ``` + + ### Provider Metadata + + As with provider-specific options, provider-specific metadata is now returned as + part of the response from the large language model provider. + + ```ts + import { LanguageModel } from "@effect/ai" + import { AnthropicLanguageModel } from "@effect/ai-anthropic" + import { Effect } from "effect" + + const Claude = AnthropicLanguageModel.model("claude-4-sonnet-20250514") + + const program = Effect.gen(function* () { + const response = yield* LanguageModel.generateText({ + prompt: "What is the meaning of life?" + }) + + for (const part of response.content) { + // When metadata **is not** defined for a content part, accessing the + // provider's key on the part's metadata will return an untyped record + if (part.type === "text") { + const metadata = part.metadata.anthropic + // ^? { readonly [x: string]: unknown } | undefined + } + // When metadata **is** defined for a content part, accessing the + // provider's key on the part's metadata will return typed metadata + if (part.type === "reasoning") { + const metadata = part.metadata.anthropic + // ^? AnthropicReasoningInfo | undefined + } + } + }).pipe(Effect.provide(Claude)) + ``` + + ## Tool Calls + + The `Tool` module has been enhanced to support provider-defined tools (e.g. + web search, computer use, etc.). Large language model providers which support + calling their own tools now have a separate module present in their provider + integration packages which contain definitions for their tools. + + These provider-defined tools can be included alongside user-defined tools in + existing `Toolkit`s. Provider-defined tools that require a user-space handler + will be raise a type error in the associated `Toolkit` layer if no such handler + is defined. + + ```ts + import { LanguageModel, Tool, Toolkit } from "@effect/ai" + import { AnthropicTool } from "@effect/ai-anthropic" + import { Schema } from "effect" + + const DadJokeTool = Tool.make("DadJokeTool", { + parameters: { topic: Schema.String }, + success: Schema.Struct({ joke: Schema.String }) + }) + + const MyToolkit = Toolkit.make( + DadJokeTool, + AnthropicTool.WebSearch_20250305({ max_uses: 1 }) + ) + + const program = LanguageModel.generateText({ + prompt: "Search the web for a dad joke", + toolkit: MyToolkit + }) + ``` + + ## AiError + + The `AiError` type has been refactored into a union of different error types + which can be raised by the Effect AI SDK. The goal of defining separate error + types is to allow providing the end-user with more granular information about + the error that occurred. + + For now, the following errors have been defined. More error types may be added + over time based upon necessity / use case. + + ```ts + type AiError = + | HttpRequestError, + | HttpResponseError, + | MalformedInput, + | MalformedOutput, + | UnknownError + ``` + +## 0.26.1 + +### Patch Changes + +- [#5453](https://github.com/Effect-TS/effect/pull/5453) [`d6887d5`](https://github.com/Effect-TS/effect/commit/d6887d54a6ec57778cf536b69c2cf96123cb38a6) Thanks @tim-smart! - wait for client to initialize before sending notifications + +- Updated dependencies [[`d6887d5`](https://github.com/Effect-TS/effect/commit/d6887d54a6ec57778cf536b69c2cf96123cb38a6)]: + - @effect/rpc@0.69.2 + +## 0.26.0 + +### Patch Changes + +- Updated dependencies [[`3e163b2`](https://github.com/Effect-TS/effect/commit/3e163b24cc2b647e25566ba29ef25c3f57609042)]: + - @effect/rpc@0.69.0 + - @effect/experimental@0.54.6 + +## 0.25.2 + +### Patch Changes + +- [#5361](https://github.com/Effect-TS/effect/pull/5361) [`292a7c5`](https://github.com/Effect-TS/effect/commit/292a7c54050e3d06a203130f3545207ec5ac633d) Thanks @IMax153! - Add `AiEmbeddingModel.embedMany` + +## 0.25.1 + +### Patch Changes + +- [#5351](https://github.com/Effect-TS/effect/pull/5351) [`fd86f93`](https://github.com/Effect-TS/effect/commit/fd86f93217751363c60f3916d391f71c08ec4a72) Thanks @richburdon! - fixes bug where stream currently ignores disableToolCallResolution flag + +- Updated dependencies [[`a949539`](https://github.com/Effect-TS/effect/commit/a94953971c2e908890dfda00f8560d317306c328)]: + - effect@3.17.7 + - @effect/experimental@0.54.4 + +## 0.25.0 + +### Patch Changes + +- Updated dependencies [[`5a0f4f1`](https://github.com/Effect-TS/effect/commit/5a0f4f176687a39d9fa46bb894bb7ac3175b0e87), [`e9cbd26`](https://github.com/Effect-TS/effect/commit/e9cbd2673401723aa811b0535202e4f57baf6d2c)]: + - effect@3.17.1 + - @effect/rpc@0.68.0 + - @effect/experimental@0.54.0 + +## 0.24.0 + +### Patch Changes + +- Updated dependencies [[`7813640`](https://github.com/Effect-TS/effect/commit/7813640279d9e3a3e7fc0a29bfb5c6d5fb3c270f)]: + - @effect/platform@0.90.0 + - @effect/experimental@0.54.0 + - @effect/rpc@0.67.0 + +## 0.23.0 + +### Patch Changes + +- Updated dependencies [[`40c3c87`](https://github.com/Effect-TS/effect/commit/40c3c875f724264312b43002859c82bed9ad0df9), [`ed2c74a`](https://github.com/Effect-TS/effect/commit/ed2c74ae8fa4ea0dd06ea84a3e58cd32e6916104), [`073a1b8`](https://github.com/Effect-TS/effect/commit/073a1b8be5dbfa87454393ee7346f5bc36a4fd63), [`f382e99`](https://github.com/Effect-TS/effect/commit/f382e99e409838a879246250fc3994b9bf5b3c2c), [`e8c7ba5`](https://github.com/Effect-TS/effect/commit/e8c7ba5fd3eb0c3ae3039fc24c09d69391987989), [`7e10415`](https://github.com/Effect-TS/effect/commit/7e1041599ade25103428703f5d2dfd7378a09636), [`e9bdece`](https://github.com/Effect-TS/effect/commit/e9bdececdc24f60a246be5055eca71a0d49ea7f2), [`8d95eb0`](https://github.com/Effect-TS/effect/commit/8d95eb0356b1d1736204836c275d201a547d208d)]: + - effect@3.17.0 + - @effect/experimental@0.53.0 + - @effect/platform@0.89.0 + - @effect/rpc@0.66.0 + +## 0.22.2 + +### Patch Changes + +- [#5240](https://github.com/Effect-TS/effect/pull/5240) [`e43b1fd`](https://github.com/Effect-TS/effect/commit/e43b1fd9c4e0897bbf97037fe7cce90448340c29) Thanks @tim-smart! - expose the AiChat history Ref + +## 0.22.1 + +### Patch Changes + +- Updated dependencies [[`f5dfabf`](https://github.com/Effect-TS/effect/commit/f5dfabf51ba481a4468c1509c537314978ef6cec), [`17a5ea8`](https://github.com/Effect-TS/effect/commit/17a5ea8fa29785fe6e4c9480f2a2e9c8c59f3f38), [`d25f22b`](https://github.com/Effect-TS/effect/commit/d25f22be7598abe977caf6cdac3b0dd78b438c48)]: + - effect@3.16.14 + - @effect/platform@0.88.1 + - @effect/experimental@0.52.1 + - @effect/rpc@0.65.1 + +## 0.22.0 + +### Patch Changes + +- Updated dependencies [[`27206d7`](https://github.com/Effect-TS/effect/commit/27206d7f0558d7fe28de57bf54f1d0cc83acc92e), [`dbabf5e`](https://github.com/Effect-TS/effect/commit/dbabf5e76fa63b050d2b6c466713c7dc59f07d3c)]: + - @effect/platform@0.88.0 + - @effect/experimental@0.52.0 + - @effect/rpc@0.65.0 + +## 0.21.17 + +### Patch Changes + +- Updated dependencies [[`c1c05a8`](https://github.com/Effect-TS/effect/commit/c1c05a8242fb5df7445b4a12387a60eac7726eb7), [`5b7cd92`](https://github.com/Effect-TS/effect/commit/5b7cd923e786c38a0802faf0fe75498ab3cccf28), [`81fe4a2`](https://github.com/Effect-TS/effect/commit/81fe4a2c81d5e30e180a60e68c52016a27b350db)]: + - effect@3.16.13 + - @effect/rpc@0.64.14 + - @effect/experimental@0.51.14 + - @effect/platform@0.87.13 + +## 0.21.16 + +### Patch Changes + +- Updated dependencies [[`32ba77a`](https://github.com/Effect-TS/effect/commit/32ba77ae304d2161362a73e8b61965332626cf2d), [`d5e25b2`](https://github.com/Effect-TS/effect/commit/d5e25b237f05670ee42b386cb40b2cb448fc11d7)]: + - @effect/platform@0.87.12 + - @effect/experimental@0.51.13 + - @effect/rpc@0.64.13 + +## 0.21.15 + +### Patch Changes + +- Updated dependencies [[`79a1947`](https://github.com/Effect-TS/effect/commit/79a1947359cbd89a47ea315cdd86a3d250f28f43), [`001392b`](https://github.com/Effect-TS/effect/commit/001392ba8bfcad101bb034348a7415012fb12f72), [`7bfb099`](https://github.com/Effect-TS/effect/commit/7bfb099cb5528511b8d63045c4fbb4dc9cb18528)]: + - @effect/rpc@0.64.12 + - @effect/platform@0.87.11 + - @effect/experimental@0.51.12 + +## 0.21.14 + +### Patch Changes + +- Updated dependencies [[`678318d`](https://github.com/Effect-TS/effect/commit/678318d2e88233156b006acda56c2d138ee3ffa0), [`678318d`](https://github.com/Effect-TS/effect/commit/678318d2e88233156b006acda56c2d138ee3ffa0)]: + - @effect/platform@0.87.10 + - @effect/experimental@0.51.11 + - @effect/rpc@0.64.11 + +## 0.21.13 + +### Patch Changes + +- Updated dependencies [[`54514a2`](https://github.com/Effect-TS/effect/commit/54514a2f53166de27ad7e756dbf12194691fd4af)]: + - @effect/platform@0.87.9 + - @effect/experimental@0.51.10 + - @effect/rpc@0.64.10 + +## 0.21.12 + +### Patch Changes + +- Updated dependencies [[`4ce4f82`](https://github.com/Effect-TS/effect/commit/4ce4f824f6fdef492be1d35c05a490ffce518c89)]: + - @effect/platform@0.87.8 + - @effect/experimental@0.51.9 + - @effect/rpc@0.64.9 + +## 0.21.11 + +### Patch Changes + +- [#5029](https://github.com/Effect-TS/effect/pull/5029) [`d92d12a`](https://github.com/Effect-TS/effect/commit/d92d12acb6097a4fa6c9c918faa3cd5c3fb6c778) Thanks @IMax153! - Add support for generating tool call identifiers when none are returned by the LLM provider + +- [#5165](https://github.com/Effect-TS/effect/pull/5165) [`25ca0cf`](https://github.com/Effect-TS/effect/commit/25ca0cf141139cd44ff53081b1c877f8f3ab5e41) Thanks @IMax153! - Ensure that tool call parts are properly merged when combining `AiResponse`s + +- [#5029](https://github.com/Effect-TS/effect/pull/5029) [`d92d12a`](https://github.com/Effect-TS/effect/commit/d92d12acb6097a4fa6c9c918faa3cd5c3fb6c778) Thanks @IMax153! - Cleanup AiLanguageModel construction and finish basic support for gemini + +## 0.21.10 + +### Patch Changes + +- Updated dependencies [[`a9b617f`](https://github.com/Effect-TS/effect/commit/a9b617f125171ed76cd79ab46d7a924daf3b0e70), [`7e26e86`](https://github.com/Effect-TS/effect/commit/7e26e86524abcc93713d6ad7eee486638c98f7c2)]: + - @effect/platform@0.87.7 + - @effect/experimental@0.51.8 + - @effect/rpc@0.64.8 + +## 0.21.9 + +### Patch Changes + +- [#5154](https://github.com/Effect-TS/effect/pull/5154) [`030ac21`](https://github.com/Effect-TS/effect/commit/030ac217eac167d345a095bff26d9c95827fa64c) Thanks @IMax153! - Support disabling tool call resolution to give users more control over resolver execution + +- [#5133](https://github.com/Effect-TS/effect/pull/5133) [`aaae9b1`](https://github.com/Effect-TS/effect/commit/aaae9b10345ab5f867b08e1c6eb21685cfc2b078) Thanks @IMax153! - Support extracting tool call results from `AiResponse.WithToolCallResults` + +- Updated dependencies [[`905da99`](https://github.com/Effect-TS/effect/commit/905da996aad665057b4ca6dba1a4af44fb8835bd)]: + - effect@3.16.12 + - @effect/experimental@0.51.7 + - @effect/platform@0.87.6 + - @effect/rpc@0.64.7 + +## 0.21.8 + +### Patch Changes + +- Updated dependencies [[`96c1292`](https://github.com/Effect-TS/effect/commit/96c129262835410b311a51d0bf7f58b8f6fc9a12)]: + - @effect/experimental@0.51.6 + +## 0.21.7 + +### Patch Changes + +- Updated dependencies [[`2fd8676`](https://github.com/Effect-TS/effect/commit/2fd8676c803cd40000dfc3231f5daecaa0e0ebd2)]: + - @effect/platform@0.87.5 + - @effect/experimental@0.51.5 + - @effect/rpc@0.64.6 + +## 0.21.6 + +### Patch Changes + +- Updated dependencies [[`e82a4fd`](https://github.com/Effect-TS/effect/commit/e82a4fd60f6528d08cef1a4aba0abe0d3ba741ad)]: + - @effect/platform@0.87.4 + - @effect/experimental@0.51.4 + - @effect/rpc@0.64.5 + +## 0.21.5 + +### Patch Changes + +- Updated dependencies [[`1b6e396`](https://github.com/Effect-TS/effect/commit/1b6e396d699f3cbbc56b68f99055cf746529bb9e)]: + - @effect/platform@0.87.3 + - @effect/experimental@0.51.3 + - @effect/rpc@0.64.4 + +## 0.21.4 + +### Patch Changes + +- Updated dependencies [[`4fea68c`](https://github.com/Effect-TS/effect/commit/4fea68ca7a25a3c39a1ab68b3885534513ab0c81), [`b927954`](https://github.com/Effect-TS/effect/commit/b9279543cf5688dd8a577af80456959c615217d0), [`99590a6`](https://github.com/Effect-TS/effect/commit/99590a6ca9128eb1ede265b6670b655311995614), [`6c3e24c`](https://github.com/Effect-TS/effect/commit/6c3e24c2308f7d4a29b8f4270ab81bca22ac6bb4)]: + - @effect/platform@0.87.2 + - effect@3.16.11 + - @effect/experimental@0.51.2 + - @effect/rpc@0.64.3 + +## 0.21.3 + +### Patch Changes + +- Updated dependencies [[`faad30e`](https://github.com/Effect-TS/effect/commit/faad30ec8742916be59f9db642d0fc98225b636c)]: + - effect@3.16.10 + - @effect/experimental@0.51.1 + - @effect/platform@0.87.1 + - @effect/rpc@0.64.2 + +## 0.21.2 + +### Patch Changes + +- Updated dependencies [[`112a93a`](https://github.com/Effect-TS/effect/commit/112a93a9bab73e95e79f7b3502d1a7b1acd668fc)]: + - @effect/rpc@0.64.1 + - @effect/experimental@0.51.0 + +## 0.21.1 + +### Patch Changes + +- [#5088](https://github.com/Effect-TS/effect/pull/5088) [`f667373`](https://github.com/Effect-TS/effect/commit/f667373da3471f1e907366780f8c3ea7f52cc5c8) Thanks @tim-smart! - expose system option in AiChat constructors + +- Updated dependencies []: + - @effect/experimental@0.51.0 + +## 0.21.0 + +### Patch Changes + +- Updated dependencies [[`b5bac9a`](https://github.com/Effect-TS/effect/commit/b5bac9ac2913fcd11b02322624f03b544eef53ba)]: + - @effect/rpc@0.64.0 + - @effect/platform@0.87.0 + - @effect/experimental@0.51.0 + +## 0.20.0 + +### Patch Changes + +- Updated dependencies [[`5137c70`](https://github.com/Effect-TS/effect/commit/5137c703461d8d3b363c112140a6e7f798241d07), [`c23d25c`](https://github.com/Effect-TS/effect/commit/c23d25c3e7c541f1f63b28484d8c461d86c67e99), [`5137c70`](https://github.com/Effect-TS/effect/commit/5137c703461d8d3b363c112140a6e7f798241d07), [`5137c70`](https://github.com/Effect-TS/effect/commit/5137c703461d8d3b363c112140a6e7f798241d07)]: + - effect@3.16.9 + - @effect/platform@0.86.0 + - @effect/experimental@0.50.0 + - @effect/rpc@0.63.0 + +## 0.19.4 + +### Patch Changes + +- [#5056](https://github.com/Effect-TS/effect/pull/5056) [`a8d99b2`](https://github.com/Effect-TS/effect/commit/a8d99b2ec2f55d9aa6e7d00a5138e80380716877) Thanks @tim-smart! - add support for mcp 2025-06-18 + +- Updated dependencies [[`a8d99b2`](https://github.com/Effect-TS/effect/commit/a8d99b2ec2f55d9aa6e7d00a5138e80380716877)]: + - @effect/rpc@0.62.4 + - @effect/experimental@0.49.2 + +## 0.19.3 + +### Patch Changes + +- Updated dependencies [[`914a191`](https://github.com/Effect-TS/effect/commit/914a191e7cb6341a3d0e965bccd27c336cf22e44)]: + - @effect/platform@0.85.2 + - @effect/experimental@0.49.2 + - @effect/rpc@0.62.3 + +## 0.19.2 + +### Patch Changes + +- Updated dependencies [[`ddfd1e4`](https://github.com/Effect-TS/effect/commit/ddfd1e43db60e3b779d18a221344423c5f3c7416)]: + - @effect/rpc@0.62.2 + - @effect/experimental@0.49.1 + +## 0.19.1 + +### Patch Changes + +- Updated dependencies [[`8cb98d5`](https://github.com/Effect-TS/effect/commit/8cb98d53e68330228287ce2a2e0d8a4c86bcab3b), [`db2dd3c`](https://github.com/Effect-TS/effect/commit/db2dd3c3a8a77d791eae19e66153527e1cde4e6e)]: + - effect@3.16.8 + - @effect/experimental@0.49.1 + - @effect/platform@0.85.1 + - @effect/rpc@0.62.1 + +## 0.19.0 + +### Patch Changes + +- Updated dependencies [[`93687dd`](https://github.com/Effect-TS/effect/commit/93687ddbb25ce3b324cd2b83d2ccff225e97307e), [`93687dd`](https://github.com/Effect-TS/effect/commit/93687ddbb25ce3b324cd2b83d2ccff225e97307e), [`93687dd`](https://github.com/Effect-TS/effect/commit/93687ddbb25ce3b324cd2b83d2ccff225e97307e)]: + - @effect/platform@0.85.0 + - @effect/experimental@0.49.0 + - @effect/rpc@0.62.0 + +## 0.18.16 + +### Patch Changes + +- [#5040](https://github.com/Effect-TS/effect/pull/5040) [`daed158`](https://github.com/Effect-TS/effect/commit/daed158f2cf00175633284f075cf611c52aa2a1c) Thanks @tim-smart! - allow undefined mcp payloads + +## 0.18.15 + +### Patch Changes + +- [#5038](https://github.com/Effect-TS/effect/pull/5038) [`c315989`](https://github.com/Effect-TS/effect/commit/c315989cade6c2a5c9cb157ad85f56b492675add) Thanks @tim-smart! - remove McpServer requirement from McpServer.resource + +## 0.18.14 + +### Patch Changes + +- [#5036](https://github.com/Effect-TS/effect/pull/5036) [`cbac1ac`](https://github.com/Effect-TS/effect/commit/cbac1ac61a4e15ad15828563b39eef412bcee66e) Thanks @tim-smart! - add .of helpers to RpcGroup, Entity and AiToolkit + +- [#5037](https://github.com/Effect-TS/effect/pull/5037) [`dd4d380`](https://github.com/Effect-TS/effect/commit/dd4d3802f714d59171b1e9226a7babf9723ea952) Thanks @tim-smart! - eliminate McpServer requirement from resource layers + +- Updated dependencies [[`1bb0d8a`](https://github.com/Effect-TS/effect/commit/1bb0d8ab96782e99434356266b38251554ea0294), [`cbac1ac`](https://github.com/Effect-TS/effect/commit/cbac1ac61a4e15ad15828563b39eef412bcee66e)]: + - effect@3.16.7 + - @effect/rpc@0.61.15 + - @effect/experimental@0.48.12 + - @effect/platform@0.84.11 + +## 0.18.13 + +### Patch Changes + +- [#4961](https://github.com/Effect-TS/effect/pull/4961) [`aa3a819`](https://github.com/Effect-TS/effect/commit/aa3a819707c15dd39b6d9ae4b4293bd87b74e175) Thanks @IMax153! - add McpServer module + + The McpServer module provides a way to implement a MCP server using Effect. + + Here's an example of how to use the McpServer module to create a simple MCP + server with a resource template and a test prompt: + + ```ts + import { McpSchema, McpServer } from "@effect/ai" + import { NodeRuntime, NodeSink, NodeStream } from "@effect/platform-node" + import { Effect, Layer, Logger, Schema } from "effect" + + const idParam = McpSchema.param("id", Schema.NumberFromString) + + // Define a resource template for a README file + const ReadmeTemplate = McpServer.resource`file://readme/${idParam}`({ + name: "README Template", + // You can add auto-completion for the ID parameter + completion: { + id: (_) => Effect.succeed([1, 2, 3, 4, 5]) + }, + content: Effect.fn(function* (_uri, id) { + return `# MCP Server Demo - ID: ${id}` + }) + }) + + // Define a test prompt with parameters + const TestPrompt = McpServer.prompt({ + name: "Test Prompt", + description: "A test prompt to demonstrate MCP server capabilities", + parameters: Schema.Struct({ + flightNumber: Schema.String + }), + completion: { + flightNumber: () => Effect.succeed(["FL123", "FL456", "FL789"]) + }, + content: ({ flightNumber }) => + Effect.succeed( + `Get the booking details for flight number: ${flightNumber}` + ) + }) + + // Merge all the resources and prompts into a single server layer + const ServerLayer = Layer.mergeAll(ReadmeTemplate, TestPrompt).pipe( + // Provide the MCP server implementation + Layer.provide( + McpServer.layerStdio({ + name: "Demo Server", + version: "1.0.0", + stdin: NodeStream.stdin, + stdout: NodeSink.stdout + }) + ), + // add a stderr logger + Layer.provide(Logger.add(Logger.prettyLogger({ stderr: true }))) + ) + + Layer.launch(ServerLayer).pipe(NodeRuntime.runMain) + ``` + +- Updated dependencies [[`a5f7595`](https://github.com/Effect-TS/effect/commit/a5f75956ef9a15a83c416517ef493f0ee2f5ee8a), [`a02470c`](https://github.com/Effect-TS/effect/commit/a02470c75579e91525a25adb3f21b3650d042fdd), [`bf369b2`](https://github.com/Effect-TS/effect/commit/bf369b2902a0e0b195d957c18b9efd180942cf8b), [`f891d45`](https://github.com/Effect-TS/effect/commit/f891d45adffdafd3f94a2eca23faa354e3a409a8)]: + - effect@3.16.6 + - @effect/platform@0.84.10 + - @effect/experimental@0.48.11 + - @effect/rpc@0.61.14 + +## 0.18.12 + +### Patch Changes + +- Updated dependencies [[`bf418ef`](https://github.com/Effect-TS/effect/commit/bf418ef14a0f2ec965535793d5cea8fa8ba177ac)]: + - effect@3.16.5 + - @effect/experimental@0.48.10 + - @effect/platform@0.84.9 + +## 0.18.11 + +### Patch Changes + +- [#5011](https://github.com/Effect-TS/effect/pull/5011) [`2dc5f93`](https://github.com/Effect-TS/effect/commit/2dc5f932f89d260e2f6139c9b89e0548d11d94c2) Thanks @IMax153! - disallow excess options in `AiLanguageModel.generateText` / `AiLanguageModel.streamText` + +- Updated dependencies []: + - @effect/experimental@0.48.9 + +## 0.18.10 + +### Patch Changes + +- Updated dependencies [[`8b9db77`](https://github.com/Effect-TS/effect/commit/8b9db7742846af0f58fd8e8b7acb7f4f5ff487ec)]: + - @effect/platform@0.84.8 + - @effect/experimental@0.48.9 + +## 0.18.9 + +### Patch Changes + +- Updated dependencies [[`74ab9a0`](https://github.com/Effect-TS/effect/commit/74ab9a0a9e16d6e019369d256e1e24175c8bc3f3), [`770008e`](https://github.com/Effect-TS/effect/commit/770008eca3aad2899a2ed951236e575793294b28)]: + - effect@3.16.4 + - @effect/experimental@0.48.8 + - @effect/platform@0.84.7 + +## 0.18.8 + +### Patch Changes + +- Updated dependencies [[`a2d57c9`](https://github.com/Effect-TS/effect/commit/a2d57c9ac596445009ca12859b78e00e5d89b936)]: + - @effect/experimental@0.48.7 + +## 0.18.7 + +### Patch Changes + +- Updated dependencies [[`ceea77a`](https://github.com/Effect-TS/effect/commit/ceea77a13055f145520f763e3fce5b8ff15d728f)]: + - @effect/platform@0.84.6 + - @effect/experimental@0.48.6 + +## 0.18.6 + +### Patch Changes + +- [#4968](https://github.com/Effect-TS/effect/pull/4968) [`85f54ed`](https://github.com/Effect-TS/effect/commit/85f54ed1ecf2f191de8c907247066e3631b5d7e1) Thanks @IMax153! - fix the type of `AiToolkit.Any` + +- Updated dependencies [[`ec52c6a`](https://github.com/Effect-TS/effect/commit/ec52c6a2211e76972462b15b9d5a9d6d56761b7a)]: + - @effect/platform@0.84.5 + - @effect/experimental@0.48.5 + +## 0.18.5 + +### Patch Changes + +- [#4959](https://github.com/Effect-TS/effect/pull/4959) [`4ddb28d`](https://github.com/Effect-TS/effect/commit/4ddb28d230d572735fe34539c1c59005d4932d8a) Thanks @tim-smart! - improve @effect/ai llm schema compatibility + +## 0.18.4 + +### Patch Changes + +- Updated dependencies [[`87722fc`](https://github.com/Effect-TS/effect/commit/87722fce693a9b49284bbddbf82d30714c688261), [`36217ee`](https://github.com/Effect-TS/effect/commit/36217eeb1337edd9ac3f9a635b80a6385d22ae8f)]: + - effect@3.16.3 + - @effect/experimental@0.48.4 + - @effect/platform@0.84.4 + +## 0.18.3 + +### Patch Changes + +- [#4947](https://github.com/Effect-TS/effect/pull/4947) [`52c88c4`](https://github.com/Effect-TS/effect/commit/52c88c4b7d20ea819b9f2efaf112d03de0a4627b) Thanks @tim-smart! - fix AiToolkit handlers type extraction + +- Updated dependencies [[`ab7684f`](https://github.com/Effect-TS/effect/commit/ab7684f1c2a0671bf091f255d220e3a4cc7f528e)]: + - @effect/platform@0.84.3 + - @effect/experimental@0.48.3 + +## 0.18.2 + +### Patch Changes + +- Updated dependencies [[`0ddf148`](https://github.com/Effect-TS/effect/commit/0ddf148a247aa87af043d276b8453a714a400897)]: + - effect@3.16.2 + - @effect/experimental@0.48.2 + - @effect/platform@0.84.2 + +## 0.18.1 + +### Patch Changes + +- Updated dependencies [[`71174d0`](https://github.com/Effect-TS/effect/commit/71174d09691314a9b6b66189e456fd21e3eb6543), [`d615e6e`](https://github.com/Effect-TS/effect/commit/d615e6e5b944f6fd5e627e31752c7ca7e4e1c17d)]: + - @effect/platform@0.84.1 + - effect@3.16.1 + - @effect/experimental@0.48.1 + +## 0.18.0 + +### Minor Changes + +- [#4891](https://github.com/Effect-TS/effect/pull/4891) [`0552674`](https://github.com/Effect-TS/effect/commit/055267461a3076b06dea896258f4bb2154211fcb) Thanks @IMax153! - Make `AiModel` a plain `Layer` and remove `AiPlan` in favor of `ExecutionPlan` + + This release substantially simplifies and improves the ergonomics of using `AiModel` for various providers. With these changes, an `AiModel` now returns a plain `Layer` which can be used to provide services to a program that interacts with large language models. + + **Before** + + ```ts + import { AiLanguageModel } from "@effect/ai" + import { OpenAiClient, OpenAiLanguageModel } from "@effect/ai-openai" + import { NodeHttpClient } from "@effect/platform-node" + import { Config, Console, Effect, Layer } from "effect" + + // Produces an `AiModel` + const Gpt4o = OpenAiLanguageModel.model("gpt-4o") + + // Generate a dad joke + const getDadJoke = AiLanguageModel.generateText({ + prompt: "Tell me a dad joke" + }) + + const program = Effect.gen(function* () { + // Build the `AiModel` into a `Provider` + const gpt4o = yield* Gpt4o + // Use the built `AiModel` to run the program + const response = yield* gpt4o.use(getDadJoke) + // Log the response + yield* Console.log(response.text) + }) + + const OpenAi = OpenAiClient.layerConfig({ + apiKey: Config.redacted("OPENAI_API_KEY") + }).pipe(Layer.provide(NodeHttpClient.layerUndici)) + + program.pipe(Effect.provide(OpenAi), Effect.runPromise) + ``` + + **After** + + ```ts + import { AiLanguageModel } from "@effect/ai" + import { OpenAiClient, OpenAiLanguageModel } from "@effect/ai-openai" + import { NodeHttpClient } from "@effect/platform-node" + import { Config, Console, Effect, Layer } from "effect" + + // Produces a `Layer` + const Gpt4o = OpenAiLanguageModel.model("gpt-4o") + + const program = Effect.gen(function*() { + // Generate a dad joke + const response = yield* AiLanguageModel.generateText({ + prompt: "Tell me a dad joke" + }) + // Log the response + yield* Console.log(response.text) + ).pipe(Effect.provide(Gpt4o)) + + const OpenAi = OpenAiClient.layerConfig({ + apiKey: Config.redacted("OPENAI_API_KEY") + }).pipe(Layer.provide(NodeHttpClient.layerUndici)) + + program.pipe( + Effect.provide(OpenAi), + Effect.runPromise + ) + ``` + + In addition, `AiModel` can be `yield*`'ed to produce a layer with no requirements. + + This shifts the requirements of building the layer into the calling effect, which is particularly useful for creating AI-powered services. + + ```ts + import { AiLanguageModel } from "@effect/ai" + import { OpenAiLanguageModel } from "@effect/ai-openai" + import { Effect } from "effect" + + class DadJokes extends Effect.Service()("DadJokes", { + effect: Effect.gen(function* () { + // Yielding the model will return a layer with no requirements + // + // ┌─── Layer + // ▼ + const model = yield* OpenAiLanguageModel.model("gpt-4o") + + const getDadJoke = AiLanguageModel.generateText({ + prompt: "Generate a dad joke" + }).pipe(Effect.provide(model)) + + return { getDadJoke } as const + }) + }) {} + + // The requirements are lifted into the service constructor + // + // ┌─── Layer + // ▼ + DadJokes.Default + ``` + +### Patch Changes + +- Updated dependencies [[`ee0bd5d`](https://github.com/Effect-TS/effect/commit/ee0bd5d24864752c54cb359f67a67dd903971ec4), [`5189800`](https://github.com/Effect-TS/effect/commit/51898004e11766b8cf6d95e960b636f6d5db79ec), [`58bfeaa`](https://github.com/Effect-TS/effect/commit/58bfeaa64ded8c88f772b184311c0c0dbac10960), [`194d748`](https://github.com/Effect-TS/effect/commit/194d7486943f56f3267ef415395ac220a4b3e634), [`918c9ea`](https://github.com/Effect-TS/effect/commit/918c9ea1a57facb154f0fb26792021f337054dee), [`9198e6f`](https://github.com/Effect-TS/effect/commit/9198e6fcc1a3ff4fefb3363004de558d8de01f40), [`2a370bf`](https://github.com/Effect-TS/effect/commit/2a370bf625fdeede5659721468eb0d527e403279), [`58ccb91`](https://github.com/Effect-TS/effect/commit/58ccb91328c8df5d49808b673738bc09df355201), [`fd47834`](https://github.com/Effect-TS/effect/commit/fd478348203fa89462b0a1d067ce4de034353df4)]: + - effect@3.16.0 + - @effect/experimental@0.48.0 + - @effect/platform@0.84.0 + +## 0.17.0 + +### Patch Changes + +- Updated dependencies [[`5522520`](https://github.com/Effect-TS/effect/commit/55225206ab9af0ad60b1c0654690a8a096d625cd), [`cc5bb2b`](https://github.com/Effect-TS/effect/commit/cc5bb2b918a9450a975f702dabcea891bda382cb)]: + - @effect/platform@0.83.0 + - effect@3.15.5 + - @effect/experimental@0.47.0 + +## 0.16.9 + +### Patch Changes + +- Updated dependencies [[`0617b9d`](https://github.com/Effect-TS/effect/commit/0617b9dc365f1963b36949ad7f9023ab6eb94524)]: + - @effect/platform@0.82.8 + - @effect/experimental@0.46.8 + +## 0.16.8 + +### Patch Changes + +- Updated dependencies [[`f570554`](https://github.com/Effect-TS/effect/commit/f57055459524587b041340577dad85476bb35f81), [`78047e8`](https://github.com/Effect-TS/effect/commit/78047e8dfc8005b66f87afe50bb95981fea51561), [`c20b95a`](https://github.com/Effect-TS/effect/commit/c20b95a99ffe452b4774c844d397a905f713b6d6), [`94ada43`](https://github.com/Effect-TS/effect/commit/94ada430928d5685bdbef513e87562c20774a3a2)]: + - effect@3.15.4 + - @effect/platform@0.82.7 + - @effect/experimental@0.46.7 + +## 0.16.7 + +### Patch Changes + +- Updated dependencies [[`618903b`](https://github.com/Effect-TS/effect/commit/618903ba9ae96e2bfe6ee31f61c4359b915f2a36)]: + - @effect/platform@0.82.6 + - @effect/experimental@0.46.6 + +## 0.16.6 + +### Patch Changes + +- Updated dependencies [[`7764a07`](https://github.com/Effect-TS/effect/commit/7764a07d960c60df81f14e1dc949518f4bbe494a), [`4577f54`](https://github.com/Effect-TS/effect/commit/4577f548d67273e576cdde423bdd34a4b910766a), [`30a0d9c`](https://github.com/Effect-TS/effect/commit/30a0d9cb51c84290d51b1361d72ff5cee33c13c7)]: + - @effect/platform@0.82.5 + - effect@3.15.3 + - @effect/experimental@0.46.5 + +## 0.16.5 + +### Patch Changes + +- Updated dependencies [[`d45e8a8`](https://github.com/Effect-TS/effect/commit/d45e8a8ac8227192f504e39e6d04fdcf4fb1d225), [`d13b68e`](https://github.com/Effect-TS/effect/commit/d13b68e3a9456d0bfee9bca8273a7b44a9c69087)]: + - @effect/platform@0.82.4 + - @effect/experimental@0.46.4 + +## 0.16.4 + +### Patch Changes + +- Updated dependencies [[`b8722b8`](https://github.com/Effect-TS/effect/commit/b8722b817e2306fe8c8245f3f9e32d85b824b961), [`a328f4b`](https://github.com/Effect-TS/effect/commit/a328f4b4fe717dd53e5b04a30f387433c32f7328)]: + - effect@3.15.2 + - @effect/platform@0.82.3 + - @effect/experimental@0.46.3 + +## 0.16.3 + +### Patch Changes + +- Updated dependencies [[`739a3d4`](https://github.com/Effect-TS/effect/commit/739a3d4a4565915fe2e690003f4f9085cb4422fc)]: + - @effect/platform@0.82.2 + - @effect/experimental@0.46.2 + +## 0.16.2 + +### Patch Changes + +- Updated dependencies [[`787ce70`](https://github.com/Effect-TS/effect/commit/787ce7042e35b657963473c6efe47752868cd811), [`1269641`](https://github.com/Effect-TS/effect/commit/1269641a99ae43069f7648ff79ffe8729b54b348), [`1269641`](https://github.com/Effect-TS/effect/commit/1269641a99ae43069f7648ff79ffe8729b54b348)]: + - effect@3.15.1 + - @effect/experimental@0.46.1 + - @effect/platform@0.82.1 + +## 0.16.1 + +### Patch Changes + +- [#4866](https://github.com/Effect-TS/effect/pull/4866) [`cb3c30f`](https://github.com/Effect-TS/effect/commit/cb3c30f540a83dafcd6d841375073b5e069fa417) Thanks @tim-smart! - preserve tool call results in AiResponse.merge + +- Updated dependencies []: + - @effect/experimental@0.46.0 + +## 0.16.0 + +### Patch Changes + +- Updated dependencies [[`c654595`](https://github.com/Effect-TS/effect/commit/c65459587b51da140b78098e81fdbfece65d53e2), [`d9f5dea`](https://github.com/Effect-TS/effect/commit/d9f5deae0f02f5de2b9fcb1cca8b142ba4bc2bba), [`49aa723`](https://github.com/Effect-TS/effect/commit/49aa7236a15e13f818c86edbca08c4af67c8dfaf), [`74c14d0`](https://github.com/Effect-TS/effect/commit/74c14d01d0cb48cf517a1b6e29a373a96ed0ff5b), [`e4f49b6`](https://github.com/Effect-TS/effect/commit/e4f49b66857e01b74ab6a9a0bc7132f44cd04cbb), [`6f02224`](https://github.com/Effect-TS/effect/commit/6f02224b3fc46a682ad2defb1a260841956c6780), [`1dcfd41`](https://github.com/Effect-TS/effect/commit/1dcfd41ff96abd706901293a00c1893cb29dd8fd), [`b21ab16`](https://github.com/Effect-TS/effect/commit/b21ab16b6f773e7ec4369db4e752c35e719f7870), [`fcf1822`](https://github.com/Effect-TS/effect/commit/fcf1822f98fcda60351d64e9d2c2c13563d7e6db), [`0061dd1`](https://github.com/Effect-TS/effect/commit/0061dd140740165e91569a684cce27a77b23229e), [`8421e6e`](https://github.com/Effect-TS/effect/commit/8421e6e49332bca8f96f482dfd48680e238b3a89), [`a9b3fb7`](https://github.com/Effect-TS/effect/commit/a9b3fb78abcfdb525318a956fd02fcadeb56143e), [`fa10f56`](https://github.com/Effect-TS/effect/commit/fa10f56b96bd9af070ba99ebc3279aa93954261e)]: + - effect@3.15.0 + - @effect/platform@0.82.0 + - @effect/experimental@0.46.0 + +## 0.15.0 + +### Minor Changes + +- [#4766](https://github.com/Effect-TS/effect/pull/4766) [`a4d42c5`](https://github.com/Effect-TS/effect/commit/a4d42c55669eff56963d06323d155a5bf3082a70) Thanks @IMax153! - This release includes a complete refactor of the internals of the base `@effect/ai` library, with a focus on flexibility for the end user and incorporation of more information from model providers. + + ## Notable Changes + + ### `AiLanguageModel` and `AiEmbeddingModel` + + The `Completions` service from `@effect/ai` has been renamed to `AiLanguageModel`, and the `Embeddings` service has similarly been renamed to `AiEmbeddingModel`. In addition, `Completions.create` and `Completions.toolkit` have been unified into `AiLanguageModel.generateText`. Similarly, `Completions.stream` and `Completions.toolkitStream` have been unified into `AiLanguageModel.streamText`. + + ### Structured Outputs + + `Completions.structured` has been renamed to `AiLanguageModel.generateObject`, and this method now returns a specialized `AiResponse.WithStructuredOutput` type, which contains a `value` property with the result of the structured output call. This enhancement prevents the end user from having to unnecessarily unwrap an `Option`. + + ### `AiModel` and `AiPlan` + + The `.provide` method on a built `AiModel` / `AiPlan` has been renamed to `.use` to improve clarity given that a user is _using_ the services provided by the model / plan to run a particular piece of code. + + In addition, the `AiPlan.fromModel` constructor has been simplified into `AiPlan.make`, which allows you to create an initial `AiPlan` with multiple steps incorporated. + + For example: + + ```ts + import { AiPlan } from "@effect/ai" + import { OpenAiLanguageModel } from "@effect/ai-openai" + import { AnthropicLanguageModel } from "@effect/ai-anthropic" + import { Effect } from "effect" + + const main = Effect.gen(function* () { + const plan = yield* AiPlan.make( + { + model: OpenAiLanguageModel.model("gpt-4"), + attempts: 1 + }, + { + model: AnthropicLanguageModel.model("claude-3-7-sonnet-latest"), + attempts: 1 + }, + { + model: AnthropicLanguageModel.model("claude-3-5-sonnet-latest"), + attempts: 1 + } + ) + + yield* plan.use(program) + }) + ``` + + ### `AiInput` and `AiResponse` + + The `AiInput` and `AiResponse` types have been refactored to allow inclusion of more information and metadata from model providers where possible, such as reasoning output and prompt cache token utilization. + + In addition, for an `AiResponse` you can now access metadata that is specific to a given provider. For example, when using OpenAi to generate audio, you can check the input and output audio tokens used: + + ```ts + import { OpenAiLanguageModel } from "@effect/ai-openai" + import { Effect, Option } from "effect" + + const getDadJoke = OpenAiLanguageModel.generateText({ + prompt: "Generate a hilarious dad joke" + }) + + Effect.gen(function* () { + const model = yield* OpenAiLanguageModel.model("gpt-4o") + const response = yield* model.use(getDadJoke) + const metadata = response.getProviderMetadata( + OpenAiLanguageModel.ProviderMetadata + ) + if (Option.isSome(metadata)) { + console.log(metadata.value) + } + }) + ``` + + ### `AiTool` and `AiToolkit` + + The `AiToolkit` has been completely refactored to simplify creating a collection of tools and using those tools in requests to model providers. A new `AiTool` data type has also been introduced to simplify defining tools for a toolkit. `AiToolkit.implement` has been renamed to `AiToolkit.toLayer` for clarity, and defining handlers is now very similar to the way handlers are defined in the `@effect/rpc` library. + + A complete example of an `AiToolkit` implementation and usage can be found below: + + ```ts + import { AiLanguageModel, AiTool, AiToolkit } from "@effect/ai" + import { OpenAiClient, OpenAiLanguageModel } from "@effect/ai-openai" + import { + FetchHttpClient, + HttpClient, + HttpClientRequest, + HttpClientResponse + } from "@effect/platform" + import { NodeHttpClient, NodeRuntime } from "@effect/platform-node" + import { Array, Config, Console, Effect, Layer, Schema } from "effect" + + // ============================================================================= + // Domain Models + // ============================================================================= + + const DadJoke = Schema.Struct({ + id: Schema.String, + joke: Schema.String + }) + + const SearchResponse = Schema.Struct({ + current_page: Schema.Int, + limit: Schema.Int, + next_page: Schema.Int, + previous_page: Schema.Int, + search_term: Schema.String, + results: Schema.Array(DadJoke), + status: Schema.Int, + total_jokes: Schema.Int, + total_pages: Schema.Int + }) + + // ============================================================================= + // Service Definitions + // ============================================================================= + + export class ICanHazDadJoke extends Effect.Service()( + "ICanHazDadJoke", + { + dependencies: [FetchHttpClient.layer], + effect: Effect.gen(function* () { + const httpClient = (yield* HttpClient.HttpClient).pipe( + HttpClient.mapRequest( + HttpClientRequest.prependUrl("https://icanhazdadjoke.com") + ) + ) + const httpClientOk = HttpClient.filterStatusOk(httpClient) + + const search = Effect.fn("ICanHazDadJoke.search")(function ( + term: string + ) { + return httpClientOk + .get("/search", { + acceptJson: true, + urlParams: { term } + }) + .pipe( + Effect.flatMap(HttpClientResponse.schemaBodyJson(SearchResponse)), + Effect.orDie + ) + }) + + return { + search + } as const + }) + } + ) {} + + // ============================================================================= + // Toolkit Definition + // ============================================================================= + + export class DadJokeTools extends AiToolkit.make( + AiTool.make("GetDadJoke", { + description: + "Fetch a dad joke based on a search term from the ICanHazDadJoke API", + success: DadJoke, + parameters: Schema.Struct({ + searchTerm: Schema.String + }) + }) + ) {} + + // ============================================================================= + // Toolkit Handlers + // ============================================================================= + + export const DadJokeToolHandlers = DadJokeTools.toLayer( + Effect.gen(function* () { + const icanhazdadjoke = yield* ICanHazDadJoke + return { + GetDadJoke: (params) => + icanhazdadjoke.search(params.searchTerm).pipe( + Effect.flatMap((response) => Array.head(response.results)), + Effect.orDie + ) + } + }) + ).pipe(Layer.provide(ICanHazDadJoke.Default)) + + // ============================================================================= + // Toolkit Usage + // ============================================================================= + + const makeDadJoke = Effect.gen(function* () { + const languageModel = yield* AiLanguageModel.AiLanguageModel + const toolkit = yield* DadJokeTools + + const response = yield* languageModel.generateText({ + prompt: "Come up with a dad joke about pirates", + toolkit + }) + + return yield* languageModel.generateText({ + prompt: response + }) + }) + + const program = Effect.gen(function* () { + const model = yield* OpenAiLanguageModel.model("gpt-4o") + const result = yield* model.provide(makeDadJoke) + yield* Console.log(result.text) + }) + + const OpenAi = OpenAiClient.layerConfig({ + apiKey: Config.redacted("OPENAI_API_KEY") + }).pipe(Layer.provide(NodeHttpClient.layerUndici)) + + program.pipe( + Effect.provide([OpenAi, DadJokeToolHandlers]), + Effect.tapErrorCause(Effect.logError), + NodeRuntime.runMain + ) + ``` + +### Patch Changes + +- Updated dependencies []: + - @effect/experimental@0.45.1 + +## 0.14.1 + +### Patch Changes + +- Updated dependencies [[`24a9ebb`](https://github.com/Effect-TS/effect/commit/24a9ebbb5af598f0bfd6ecc45307e528043fe011)]: + - effect@3.14.22 + - @effect/experimental@0.45.1 + - @effect/platform@0.81.1 + +## 0.14.0 + +### Patch Changes + +- Updated dependencies [[`672920f`](https://github.com/Effect-TS/effect/commit/672920f85da8abd5f9d4ad85e29248a2aca57ed8)]: + - @effect/platform@0.81.0 + - @effect/experimental@0.45.0 + +## 0.13.21 + +### Patch Changes + +- Updated dependencies [[`2f3b7d4`](https://github.com/Effect-TS/effect/commit/2f3b7d4e1fa1ef8790b0ca4da22eb88872ee31df)]: + - effect@3.14.21 + - @effect/experimental@0.44.21 + - @effect/platform@0.80.21 + +## 0.13.20 + +### Patch Changes + +- Updated dependencies [[`17e2f30`](https://github.com/Effect-TS/effect/commit/17e2f3091408cf0fca9414d4af3bdf7b2765b378)]: + - effect@3.14.20 + - @effect/experimental@0.44.20 + - @effect/platform@0.80.20 + +## 0.13.19 + +### Patch Changes + +- Updated dependencies [[`056a910`](https://github.com/Effect-TS/effect/commit/056a910d0a0b8b00b0dc9df4a070466b2b5c2f6c), [`e25e7bb`](https://github.com/Effect-TS/effect/commit/e25e7bbc1797733916f48f501425d9f2ef310d9f), [`3273d57`](https://github.com/Effect-TS/effect/commit/3273d572c2b3175a842677f19efeea4cd65ab016)]: + - effect@3.14.19 + - @effect/platform@0.80.19 + - @effect/experimental@0.44.19 + +## 0.13.18 + +### Patch Changes + +- Updated dependencies [[`b1164d4`](https://github.com/Effect-TS/effect/commit/b1164d49a1dfdf299e9971367b6fc6be4df0ddff)]: + - effect@3.14.18 + - @effect/experimental@0.44.18 + - @effect/platform@0.80.18 + +## 0.13.17 + +### Patch Changes + +- Updated dependencies [[`0b54681`](https://github.com/Effect-TS/effect/commit/0b54681cd89245e211d8f49272be0f1bf2f81813), [`41a59d5`](https://github.com/Effect-TS/effect/commit/41a59d5916a296b12b0d5ead9e859e05f40b4cce)]: + - effect@3.14.17 + - @effect/experimental@0.44.17 + - @effect/platform@0.80.17 + +## 0.13.16 + +### Patch Changes + +- Updated dependencies [[`ee14444`](https://github.com/Effect-TS/effect/commit/ee144441021ec77039e43396eaf90714687bb495), [`f1c8583`](https://github.com/Effect-TS/effect/commit/f1c8583f8c3ea9415f813795ca2940a897c9ba9a)]: + - effect@3.14.16 + - @effect/platform@0.80.16 + - @effect/experimental@0.44.16 + +## 0.13.15 + +### Patch Changes + +- Updated dependencies [[`239cc99`](https://github.com/Effect-TS/effect/commit/239cc995ce645946210a3c3d2cb52bd3547c0687), [`8b6c947`](https://github.com/Effect-TS/effect/commit/8b6c947eaa8e45a67ecb3c37d45cd27f3e41d165), [`c50a63b`](https://github.com/Effect-TS/effect/commit/c50a63bbecb9f560b9cae349c447eed877d1b9b6)]: + - effect@3.14.15 + - @effect/experimental@0.44.15 + - @effect/platform@0.80.15 + +## 0.13.14 + +### Patch Changes + +- Updated dependencies [[`6ed8d15`](https://github.com/Effect-TS/effect/commit/6ed8d1589beb181d30abc79afebdaabc1d101538)]: + - effect@3.14.14 + - @effect/experimental@0.44.14 + - @effect/platform@0.80.14 + +## 0.13.13 + +### Patch Changes + +- Updated dependencies [[`ee77788`](https://github.com/Effect-TS/effect/commit/ee77788747e7ebbde6bfa88256cde49dbbad3608), [`5fce6ba`](https://github.com/Effect-TS/effect/commit/5fce6ba19c3cc63cc0104e737e581ad989dedbf0), [`570e45f`](https://github.com/Effect-TS/effect/commit/570e45f8cb936e42ec48f67f21bb2b7252f36c0c)]: + - effect@3.14.13 + - @effect/experimental@0.44.13 + - @effect/platform@0.80.13 + +## 0.13.12 + +### Patch Changes + +- Updated dependencies [[`c2ad9ee`](https://github.com/Effect-TS/effect/commit/c2ad9ee9f3c4c743390edf35ed9e85a20be33811), [`9c68654`](https://github.com/Effect-TS/effect/commit/9c686542b6eb3ea188cb70673ef2e41223633e89)]: + - effect@3.14.12 + - @effect/experimental@0.44.12 + - @effect/platform@0.80.12 + +## 0.13.11 + +### Patch Changes + +- Updated dependencies [[`e536127`](https://github.com/Effect-TS/effect/commit/e536127c1e6f2fb3a542c73ae919435a629a346b)]: + - effect@3.14.11 + - @effect/experimental@0.44.11 + - @effect/platform@0.80.11 + +## 0.13.10 + +### Patch Changes + +- Updated dependencies [[`bc7efa3`](https://github.com/Effect-TS/effect/commit/bc7efa3b031bb25e1ed3c8f2d3fb5e8da166cadc)]: + - effect@3.14.10 + - @effect/experimental@0.44.10 + - @effect/platform@0.80.10 + +## 0.13.9 + +### Patch Changes + +- Updated dependencies [[`d78249f`](https://github.com/Effect-TS/effect/commit/d78249f0b67f63cf4baf806ff090cba33293daf0)]: + - effect@3.14.9 + - @effect/experimental@0.44.9 + - @effect/platform@0.80.9 + +## 0.13.8 + +### Patch Changes + +- Updated dependencies [[`b3a2d32`](https://github.com/Effect-TS/effect/commit/b3a2d32772e6f7f20eacf2e18128e99324c4d378)]: + - effect@3.14.8 + - @effect/experimental@0.44.8 + - @effect/platform@0.80.8 + +## 0.13.7 + +### Patch Changes + +- Updated dependencies [[`b542a4b`](https://github.com/Effect-TS/effect/commit/b542a4bf195be0c9af1523e1ba96c953decc4d25)]: + - effect@3.14.7 + - @effect/experimental@0.44.7 + - @effect/platform@0.80.7 + +## 0.13.6 + +### Patch Changes + +- Updated dependencies [[`47618c1`](https://github.com/Effect-TS/effect/commit/47618c1ad84ebcc5a51133a3fff5aa5012d49d45), [`6077882`](https://github.com/Effect-TS/effect/commit/60778824a4794336c33807801f813f8751d1c7e4)]: + - effect@3.14.6 + - @effect/experimental@0.44.6 + - @effect/platform@0.80.6 + +## 0.13.5 + +### Patch Changes + +- Updated dependencies [[`40dbfef`](https://github.com/Effect-TS/effect/commit/40dbfeff239b6e567706752114f31b2fce7de4e3), [`85fba81`](https://github.com/Effect-TS/effect/commit/85fba815ac07eb13d4227a69ac76a18e4b94df18), [`5a5ebdd`](https://github.com/Effect-TS/effect/commit/5a5ebdddfaddd259538b4599a6676281faca778e)]: + - effect@3.14.5 + - @effect/platform@0.80.5 + - @effect/experimental@0.44.5 + +## 0.13.4 + +### Patch Changes + +- Updated dependencies [[`e4ba2c6`](https://github.com/Effect-TS/effect/commit/e4ba2c66a878e81b5e295d6d49aaf724b80a28ef)]: + - effect@3.14.4 + - @effect/experimental@0.44.4 + - @effect/platform@0.80.4 + +## 0.13.3 + +### Patch Changes + +- Updated dependencies [[`37aa8e1`](https://github.com/Effect-TS/effect/commit/37aa8e137725a902e70cd1e468ea98b873aa5056), [`34f03d6`](https://github.com/Effect-TS/effect/commit/34f03d66875f21f266f102223a03cd14c2ed6ea6)]: + - effect@3.14.3 + - @effect/experimental@0.44.3 + - @effect/platform@0.80.3 + +## 0.13.2 + +### Patch Changes + +- Updated dependencies [[`f87991b`](https://github.com/Effect-TS/effect/commit/f87991b6d8a2edfaf90b01cebda4b466992ae865), [`f87991b`](https://github.com/Effect-TS/effect/commit/f87991b6d8a2edfaf90b01cebda4b466992ae865), [`0a3e3e1`](https://github.com/Effect-TS/effect/commit/0a3e3e18eea5e0d1882f1a6c906198e6ef226a41)]: + - effect@3.14.2 + - @effect/experimental@0.44.2 + - @effect/platform@0.80.2 + +## 0.13.1 + +### Patch Changes + +- Updated dependencies [[`4a274fe`](https://github.com/Effect-TS/effect/commit/4a274fe9f623182b6b902827e0e83bd89ca3b05c)]: + - effect@3.14.1 + - @effect/experimental@0.44.1 + - @effect/platform@0.80.1 + +## 0.13.0 + +### Patch Changes + +- Updated dependencies [[`1f47e4e`](https://github.com/Effect-TS/effect/commit/1f47e4e12546ab691b29bfb7b5128bb17b93baa5), [`26dd75f`](https://github.com/Effect-TS/effect/commit/26dd75f276a0d8a63eab313bd5a167d5072c9780), [`aba2d1d`](https://github.com/Effect-TS/effect/commit/aba2d1d831ea149481bd4dd755528c0afa8239ce), [`3131f8f`](https://github.com/Effect-TS/effect/commit/3131f8fd12ba9eb31b90fa2f42bf88b12309133c), [`aba2d1d`](https://github.com/Effect-TS/effect/commit/aba2d1d831ea149481bd4dd755528c0afa8239ce), [`04dff2d`](https://github.com/Effect-TS/effect/commit/04dff2d01ac68c260f29a6d4743381825c353c86), [`aba2d1d`](https://github.com/Effect-TS/effect/commit/aba2d1d831ea149481bd4dd755528c0afa8239ce), [`c7fac0c`](https://github.com/Effect-TS/effect/commit/c7fac0cd7eadcd5cc0c3a987051c5b57ad271638), [`aba2d1d`](https://github.com/Effect-TS/effect/commit/aba2d1d831ea149481bd4dd755528c0afa8239ce), [`ffaa3f3`](https://github.com/Effect-TS/effect/commit/ffaa3f3969df26610fcc02ad537340641d44e803), [`ab957c1`](https://github.com/Effect-TS/effect/commit/ab957c1fee714868f56c7ab4e802b9d449e9b666), [`35db9ce`](https://github.com/Effect-TS/effect/commit/35db9ce228f1416c8abacc6dc9c36fbd0f33ef0f), [`aba2d1d`](https://github.com/Effect-TS/effect/commit/aba2d1d831ea149481bd4dd755528c0afa8239ce), [`cf77ea9`](https://github.com/Effect-TS/effect/commit/cf77ea9ab4fc89e66a43f682a9926ccdee6c57ed), [`26dd75f`](https://github.com/Effect-TS/effect/commit/26dd75f276a0d8a63eab313bd5a167d5072c9780), [`baaab60`](https://github.com/Effect-TS/effect/commit/baaab60b737f35dfab8e4a21bce28a195d19e899)]: + - effect@3.14.0 + - @effect/experimental@0.44.0 + - @effect/platform@0.80.0 + +## 0.12.4 + +### Patch Changes + +- Updated dependencies [[`5662363`](https://github.com/Effect-TS/effect/commit/566236361e270e575ef1cbf308ad1967c82a362c), [`5f1fd15`](https://github.com/Effect-TS/effect/commit/5f1fd15308ab154791580059b89877d19a2055c2), [`8bb1460`](https://github.com/Effect-TS/effect/commit/8bb1460c824f66f0f25ebd899c5e74e388089c37)]: + - @effect/platform@0.79.4 + - @effect/experimental@0.43.4 + +## 0.12.3 + +### Patch Changes + +- Updated dependencies [[`0c4803f`](https://github.com/Effect-TS/effect/commit/0c4803fcc69262d11a97ce49d0e9b4288df0651f), [`6f65ac4`](https://github.com/Effect-TS/effect/commit/6f65ac4eac1489cd6ea390e18b0908670722adad)]: + - effect@3.13.12 + - @effect/experimental@0.43.3 + - @effect/platform@0.79.3 + +## 0.12.2 + +### Patch Changes + +- Updated dependencies [[`fad8cca`](https://github.com/Effect-TS/effect/commit/fad8cca9bbfcc2eaeb44b97c15dbe0a1eda75315), [`4296293`](https://github.com/Effect-TS/effect/commit/4296293049414d0cf2d915a26c552b09f946b9a0), [`9c241ab`](https://github.com/Effect-TS/effect/commit/9c241abe47ccf7a5257b98a4a64a63054a12741d), [`082b0c1`](https://github.com/Effect-TS/effect/commit/082b0c1b9f4252bcdd69608f2e4a9226f953ac3f), [`be12983`](https://github.com/Effect-TS/effect/commit/be12983bc7e7537b41cd8910fc4eb7d1da56ab07), [`de88127`](https://github.com/Effect-TS/effect/commit/de88127a5a5906ccece98af74787b5ae0e65e431)]: + - effect@3.13.11 + - @effect/experimental@0.43.2 + - @effect/platform@0.79.2 + +## 0.12.1 + +### Patch Changes + +- Updated dependencies [[`527c964`](https://github.com/Effect-TS/effect/commit/527c9645229f5be9714a7e60a38a9e753c4bbfb1)]: + - effect@3.13.10 + - @effect/experimental@0.43.1 + - @effect/platform@0.79.1 + +## 0.12.0 + +### Patch Changes + +- Updated dependencies [[`88fe129`](https://github.com/Effect-TS/effect/commit/88fe12923740765c0335a6e6203fdcc6a463edca), [`d630249`](https://github.com/Effect-TS/effect/commit/d630249426113088abe8b382db4f14d80f2160c2), [`2976e52`](https://github.com/Effect-TS/effect/commit/2976e52538d9dc9ffdcbc84d4ac748cff9305971)]: + - @effect/platform@0.79.0 + - effect@3.13.9 + - @effect/experimental@0.43.0 + +## 0.11.1 + +### Patch Changes + +- Updated dependencies [[`c65d336`](https://github.com/Effect-TS/effect/commit/c65d3362d07ec815ff3b46278314e8a31706ddc2), [`22d2ebb`](https://github.com/Effect-TS/effect/commit/22d2ebb4b11f5a44351a4736e65da391a3b647d0)]: + - effect@3.13.8 + - @effect/experimental@0.42.1 + - @effect/platform@0.78.1 + +## 0.11.0 + +### Patch Changes + +- Updated dependencies [[`c5bcf53`](https://github.com/Effect-TS/effect/commit/c5bcf53b7cb49dacffdd2a6cd8eb48cc452b417e)]: + - @effect/platform@0.78.0 + - @effect/experimental@0.42.0 + +## 0.10.7 + +### Patch Changes + +- [#4545](https://github.com/Effect-TS/effect/pull/4545) [`a95108a`](https://github.com/Effect-TS/effect/commit/a95108acac7f25fc5e1c0dcdf16bcc638dca5c00) Thanks @IMax153! - Avoid inclusion of `_tag` in tool call JSONSchema + +- Updated dependencies [[`840cc73`](https://github.com/Effect-TS/effect/commit/840cc7329908db7ca693ef47b07d4f845c29cadd), [`9bf8a74`](https://github.com/Effect-TS/effect/commit/9bf8a74b967f18d931743dd5196af326c9118e9c), [`87ba23c`](https://github.com/Effect-TS/effect/commit/87ba23c41c193503ed0c612b0d32d0b253794c64), [`f910880`](https://github.com/Effect-TS/effect/commit/f91088069057f3b4529753f5bc5532b028d726df), [`0d01480`](https://github.com/Effect-TS/effect/commit/0d014803e4f688f74386a80abd65485e1a319244)]: + - @effect/platform@0.77.7 + - effect@3.13.7 + - @effect/experimental@0.41.7 + +## 0.10.6 + +### Patch Changes + +- Updated dependencies [[`3154ce4`](https://github.com/Effect-TS/effect/commit/3154ce4692fa18b804982158d3c4c8a8a5fae386)]: + - effect@3.13.6 + - @effect/experimental@0.41.6 + - @effect/platform@0.77.6 + +## 0.10.5 + +### Patch Changes + +- [#4549](https://github.com/Effect-TS/effect/pull/4549) [`3d6d323`](https://github.com/Effect-TS/effect/commit/3d6d323c2a1028f3caba45453187b9374bac2c36) Thanks @IMax153! - Fix `AiPlan` builder to return correct shape + +- [#4537](https://github.com/Effect-TS/effect/pull/4537) [`975c20e`](https://github.com/Effect-TS/effect/commit/975c20e446186e9bb975f77e7c6ac7b248f7b5f6) Thanks @IMax153! - Allow defects to pass through predicates in `AiPlan` + +- Updated dependencies [[`367bb35`](https://github.com/Effect-TS/effect/commit/367bb35f4c2a254e1fb211d96db2474a7aed9020), [`6cf11c3`](https://github.com/Effect-TS/effect/commit/6cf11c3a75773ceec2877c85ddc760f381f0866d), [`a0acec8`](https://github.com/Effect-TS/effect/commit/a0acec851f72e19466363d24b9cc218acd00006a)]: + - effect@3.13.5 + - @effect/experimental@0.41.5 + - @effect/platform@0.77.5 + +## 0.10.4 + +### Patch Changes + +- Updated dependencies [[`e0746f9`](https://github.com/Effect-TS/effect/commit/e0746f9aa398b69c6542e375910683bf17f49f46), [`17d9e89`](https://github.com/Effect-TS/effect/commit/17d9e89f9851663bdbb6c1e685601d97806114a4)]: + - @effect/platform@0.77.4 + - effect@3.13.4 + - @effect/experimental@0.41.4 + +## 0.10.3 + +### Patch Changes + +- [#4504](https://github.com/Effect-TS/effect/pull/4504) [`a67a8a1`](https://github.com/Effect-TS/effect/commit/a67a8a1a4979fb7a039a060d067d805879da4d4b) Thanks @IMax153! - Introduce `AiModel` and `AiPlan` for describing retry / fallback logic between + models and providers + + For example, the following program builds an `AiPlan` which will attempt to use + OpenAi's chat completions API, and if after three attempts the operation + is still failing, the plan will fallback to utilizing Anthropic's messages API + to resolve the request. + + ```ts + import { AiPlan, Completions } from "@effect/ai" + import { AnthropicClient, AnthropicCompletions } from "@effect/ai-anthropic" + import { OpenAiClient, OpenAiCompletions } from "@effect/ai-openai" + import { NodeHttpClient, NodeRuntime } from "@effect/platform-node" + import { Config, Console, Effect, Layer } from "effect" + + // Create Anthropic client + const Anthropic = AnthropicClient.layerConfig({ + apiKey: Config.redacted("ANTHROPIC_API_KEY") + }).pipe(Layer.provide(NodeHttpClient.layerUndici)) + + // Create OpenAi client + const OpenAi = OpenAiClient.layerConfig({ + apiKey: Config.redacted("OPENAI_API_KEY") + }).pipe(Layer.provide(NodeHttpClient.layerUndici)) + + // Create a plan of request execution + const Plan = AiPlan.fromModel(OpenAiCompletions.model("gpt-4o-mini"), { + attempts: 3 + }).pipe( + AiPlan.withFallback({ + model: AnthropicCompletions.model("claude-3-5-haiku-latest") + }) + ) + + const program = Effect.gen(function* () { + // Build the plan of execution + const plan = yield* Plan + + // Create a program which uses the services provided by the plan + const getDadJoke = Effect.gen(function* () { + const completions = yield* Completions.Completions + const response = yield* completions.create("Tell me a dad joke") + yield* Console.log(response.text) + }) + + // Provide the plan to whichever programs need it + yield* plan.provide(getDadJoke) + }) + + program.pipe(Effect.provide([Anthropic, OpenAi]), NodeRuntime.runMain) + ``` + +- Updated dependencies [[`cc5588d`](https://github.com/Effect-TS/effect/commit/cc5588df07f9103513547cb429ce041b9436a8bd), [`623c8cd`](https://github.com/Effect-TS/effect/commit/623c8cd053ed6ee3d353aaa8778d484670fca2bb), [`00b4eb1`](https://github.com/Effect-TS/effect/commit/00b4eb1ece12a16e222e6220965bb4024d6752ac), [`f2aee98`](https://github.com/Effect-TS/effect/commit/f2aee989b0a600900ce83e7f460d02908620c80f), [`fb798eb`](https://github.com/Effect-TS/effect/commit/fb798eb9061f1191badc017d1aa649360254da20), [`2251b15`](https://github.com/Effect-TS/effect/commit/2251b1528810bb695b37ce388b653cec0c5bf80c), [`2e15c1e`](https://github.com/Effect-TS/effect/commit/2e15c1e33648add0b29fe274fbcb7294b7515085), [`a4979db`](https://github.com/Effect-TS/effect/commit/a4979db021aef16e731be64df196b72088fc4376), [`b74255a`](https://github.com/Effect-TS/effect/commit/b74255a304ad49d60bedb1a260fd697f370af27a), [`d7f6a5c`](https://github.com/Effect-TS/effect/commit/d7f6a5c7d26c1963dcd864ca62360d20d08c7b49), [`9dd8979`](https://github.com/Effect-TS/effect/commit/9dd8979e940915b1cc1b1f264f3d019c77a65a02), [`477b488`](https://github.com/Effect-TS/effect/commit/477b488284f47c5469d7fba3e4065fb7e3b6556e), [`10932cb`](https://github.com/Effect-TS/effect/commit/10932cbf58fc721ada631cebec42f773ce96d3cc), [`9f6c784`](https://github.com/Effect-TS/effect/commit/9f6c78468b3b5e9ebfc38ffdfb70702901ee977b), [`2c639ec`](https://github.com/Effect-TS/effect/commit/2c639ecee332de4266e36022c989c35ae4e02105), [`886aaa8`](https://github.com/Effect-TS/effect/commit/886aaa81e06dfd3cd9391e8ea987d8cd5ada1124)]: + - effect@3.13.3 + - @effect/experimental@0.41.3 + - @effect/platform@0.77.3 + +## 0.10.2 + +### Patch Changes + +- Updated dependencies [[`31be72a`](https://github.com/Effect-TS/effect/commit/31be72ada118cb84a942e67b1663263f8db74a9f), [`3e7ce97`](https://github.com/Effect-TS/effect/commit/3e7ce97f8a41756a039cf635d0b3d9a75d781097), [`31be72a`](https://github.com/Effect-TS/effect/commit/31be72ada118cb84a942e67b1663263f8db74a9f)]: + - effect@3.13.2 + - @effect/platform@0.77.2 + - @effect/experimental@0.41.2 + +## 0.10.1 + +### Patch Changes + +- [#4446](https://github.com/Effect-TS/effect/pull/4446) [`9375c28`](https://github.com/Effect-TS/effect/commit/9375c28ca808325577da6c67cc92af25931027c8) Thanks @IMax153! - Add Anthropic AI provider integration + +- Updated dependencies [[`b56a211`](https://github.com/Effect-TS/effect/commit/b56a2110569fd0ec0b57ac137743e926d49f51cc)]: + - effect@3.13.1 + - @effect/experimental@0.41.1 + - @effect/platform@0.77.1 + +## 0.10.0 + +### Patch Changes + +- Updated dependencies [[`8baef83`](https://github.com/Effect-TS/effect/commit/8baef83e7ff0b7bc0738b680e1ef013065386cff), [`655bfe2`](https://github.com/Effect-TS/effect/commit/655bfe29e44cc3f0fb9b4e53038f50b891c188df), [`d90cbc2`](https://github.com/Effect-TS/effect/commit/d90cbc274e2742d18671fe65aa4764c057eb6cba), [`75632bd`](https://github.com/Effect-TS/effect/commit/75632bd44b8025101d652ccbaeef898c7086c91c), [`c874a2e`](https://github.com/Effect-TS/effect/commit/c874a2e4b17e9d71904ca8375bb77b020975cb1d), [`bf865e5`](https://github.com/Effect-TS/effect/commit/bf865e5833f77fd8f6c06944ca9d507b54488301), [`f98b2b7`](https://github.com/Effect-TS/effect/commit/f98b2b7592cf20f9d85313e7f1e964cb65878138), [`de8ce92`](https://github.com/Effect-TS/effect/commit/de8ce924923eaa4e1b761a97eb45ec967389f3d5), [`cf8b2dd`](https://github.com/Effect-TS/effect/commit/cf8b2dd112f8e092ed99d78fd728db0f91c29050), [`db426a5`](https://github.com/Effect-TS/effect/commit/db426a5fb41ab84d18e3c8753a7329b4de544245), [`6862444`](https://github.com/Effect-TS/effect/commit/6862444094906ad4f2cb077ff3b9cc0b73880c8c), [`5fc8a90`](https://github.com/Effect-TS/effect/commit/5fc8a90ba46a5fd9f3b643f0b5aeadc69d717339), [`546a492`](https://github.com/Effect-TS/effect/commit/546a492e60eb2b8b048a489a474b934ea0877005), [`65c4796`](https://github.com/Effect-TS/effect/commit/65c47966ce39055f02cf5c808daabb3ea6442b0b), [`9760fdc`](https://github.com/Effect-TS/effect/commit/9760fdc37bdaef9da8b150e46b86ddfbe2ad9221), [`5b471e7`](https://github.com/Effect-TS/effect/commit/5b471e7d4317e8ee5d72bbbd3e0c9775160949ab), [`4f810cc`](https://github.com/Effect-TS/effect/commit/4f810cc2770e9f1f266851d2cb6257112c12af49)]: + - effect@3.13.0 + - @effect/experimental@0.41.0 + - @effect/platform@0.77.0 + +## 0.9.1 + +### Patch Changes + +- Updated dependencies [[`4018eae`](https://github.com/Effect-TS/effect/commit/4018eaed2733241676ddb8c52416f463a8c32e35), [`543d36d`](https://github.com/Effect-TS/effect/commit/543d36d1a11452560b01ab966a82529ad5fee8c9), [`c407726`](https://github.com/Effect-TS/effect/commit/c407726f79df4a567a9631cddd8effaa16b3535d), [`f70a65a`](https://github.com/Effect-TS/effect/commit/f70a65ac80c6635d80b12beaf4d32a9cc59fa143), [`ba409f6`](https://github.com/Effect-TS/effect/commit/ba409f69c41aeaa29e475c0630735726eaf4dbac), [`3d2e356`](https://github.com/Effect-TS/effect/commit/3d2e3565e8a43d1bdb5daee8db3b90f56d71d859)]: + - effect@3.12.12 + - @effect/platform@0.76.1 + - @effect/experimental@0.40.1 + +## 0.9.0 + +### Patch Changes + +- Updated dependencies [[`b6a032f`](https://github.com/Effect-TS/effect/commit/b6a032f07bffa020a848c813881879395134fa20), [`42ddd5f`](https://github.com/Effect-TS/effect/commit/42ddd5f144ce9f9d94a036679ebbd626446d37f5), [`2fe447c`](https://github.com/Effect-TS/effect/commit/2fe447c6354d334f9c591b8a8481818f5f0e797e), [`2473ad5`](https://github.com/Effect-TS/effect/commit/2473ad5cf23582e3a41338091fa526ffe611288d)]: + - effect@3.12.11 + - @effect/platform@0.76.0 + - @effect/experimental@0.40.0 + +## 0.8.4 + +### Patch Changes + +- Updated dependencies [[`e30f132`](https://github.com/Effect-TS/effect/commit/e30f132c336c9d0760bad39f82a55c7ce5159eb7), [`33fa667`](https://github.com/Effect-TS/effect/commit/33fa667c2623be1026e1ccee91bd44f73b09020a), [`87f5f28`](https://github.com/Effect-TS/effect/commit/87f5f2842e4196cb88d13f10f443ff0567e82832), [`7d57ecd`](https://github.com/Effect-TS/effect/commit/7d57ecdaf5da2345ebbf9c22df50317578bde0f5), [`4dbd170`](https://github.com/Effect-TS/effect/commit/4dbd170538e8fb7a36aa7c469c6f93b6c7000091)]: + - effect@3.12.10 + - @effect/platform@0.75.4 + - @effect/experimental@0.39.4 + +## 0.8.3 + +### Patch Changes + +- Updated dependencies [[`1b4a4e9`](https://github.com/Effect-TS/effect/commit/1b4a4e904ef5227ec7d9114d4e417eca19eed940)]: + - effect@3.12.9 + - @effect/experimental@0.39.3 + - @effect/platform@0.75.3 + +## 0.8.2 + +### Patch Changes + +- [#4378](https://github.com/Effect-TS/effect/pull/4378) [`f5e3b1b`](https://github.com/Effect-TS/effect/commit/f5e3b1bcdf24b440251ce8425d750353cf022e96) Thanks @IMax153! - Support non-identified schemas in `AiChat.structured` and `Completions.structured`. + + Instead of requiring a `Schema` with either an `identifier` or `_tag` property + for AI APIs that allow for returning structured outputs, you can now optionally + pass a `correlationId` to `AiChat.structured` and `Completions.structured` when + you want to either use a simple schema or inline the schema. + + Example: + + ```ts + import { Completions } from "@effect/ai" + import { OpenAiClient, OpenAiCompletions } from "@effect/ai-openai" + import { NodeHttpClient } from "@effect/platform-node" + import { Config, Effect, Layer, Schema, String } from "effect" + + const OpenAi = OpenAiClient.layerConfig({ + apiKey: Config.redacted("OPENAI_API_KEY") + }).pipe(Layer.provide(NodeHttpClient.layerUndici)) + + const Gpt4oCompletions = OpenAiCompletions.layer({ + model: "gpt-4o" + }).pipe(Layer.provide(OpenAi)) + + const program = Effect.gen(function* () { + const completions = yield* Completions.Completions + + const CalendarEvent = Schema.Struct({ + name: Schema.String, + date: Schema.DateFromString, + participants: Schema.Array(Schema.String) + }) + + yield* completions.structured({ + correlationId: "CalendarEvent", + schema: CalendarEvent, + input: String.stripMargin(` + |Extract event information from the following prose: + | + |Alice and Bob are going to a science fair on Friday. + `) + }) + }) + + program.pipe(Effect.provide(Gpt4oCompletions), Effect.runPromise) + ``` + +- [#4388](https://github.com/Effect-TS/effect/pull/4388) [`fcf3b7c`](https://github.com/Effect-TS/effect/commit/fcf3b7cc07a28635a5b53243b01cdeb6592dab3c) Thanks @IMax153! - Rename correlationId to toolCallId + +- [#4389](https://github.com/Effect-TS/effect/pull/4389) [`f089470`](https://github.com/Effect-TS/effect/commit/f0894708e9d591b70eccf3a50ae91ac6e0f6d6e3) Thanks @IMax153! - Add support for [GenAI telemetry annotations](https://opentelemetry.io/docs/specs/semconv/attributes-registry/gen-ai/). + +- [#4368](https://github.com/Effect-TS/effect/pull/4368) [`a0c85e6`](https://github.com/Effect-TS/effect/commit/a0c85e6601fd3e10d08085969f7453b7b517347b) Thanks @IMax153! - Support creation of embeddings from the AI integration packages. + + For example, the following program will create an OpenAI `Embeddings` service + that will aggregate all embedding requests received within a `500` millisecond + window into a single batch. + + ```ts + import { Embeddings } from "@effect/ai" + import { OpenAiClient, OpenAiEmbeddings } from "@effect/ai-openai" + import { NodeHttpClient } from "@effect/platform-node" + import { Config, Effect, Layer } from "effect" + + // Create the OpenAI client + const OpenAi = OpenAiClient.layerConfig({ + apiKey: Config.redacted("OPENAI_API_KEY") + }).pipe(Layer.provide(NodeHttpClient.layerUndici)) + + // Create an embeddings service for the `text-embedding-3-large` model + const TextEmbeddingsLarge = OpenAiEmbeddings.layerDataLoader({ + model: "text-embedding-3-large", + window: "500 millis", + maxBatchSize: 2048 + }).pipe(Layer.provide(OpenAi)) + + // Use the generic `Embeddings` service interface in your program + const program = Effect.gen(function* () { + const embeddings = yield* Embeddings.Embeddings + const result = yield* embeddings.embed("The input to embed") + }) + + // Provide the specific implementation to use + program.pipe(Effect.provide(TextEmbeddingsLarge), Effect.runPromise) + ``` + +- Updated dependencies [[`59b3cfb`](https://github.com/Effect-TS/effect/commit/59b3cfbbd5713dd9475998e95fad5534c0b21466), [`766113c`](https://github.com/Effect-TS/effect/commit/766113c0ea3512cdb887650ead8ba314236e22ee), [`bb05fb8`](https://github.com/Effect-TS/effect/commit/bb05fb83457355b1ca567228a9e041edfb6fd85d), [`712277f`](https://github.com/Effect-TS/effect/commit/712277f949052a24b46e4aa234063a6abf395c90), [`f269122`](https://github.com/Effect-TS/effect/commit/f269122508693b111142994dd48698ddc75f3d69), [`8f6006a`](https://github.com/Effect-TS/effect/commit/8f6006a610fb6d6c7b8d14209a7323338a8964ff), [`c45b559`](https://github.com/Effect-TS/effect/commit/c45b5592b5fd1189a5c932cfe05bd7d5f6d68508), [`430c846`](https://github.com/Effect-TS/effect/commit/430c846cbac05b187e3d24ac8dfee0cf22506f7c), [`7b03057`](https://github.com/Effect-TS/effect/commit/7b03057507d2dab5e6793beb9c578dedaaeb15fe), [`a9c94c8`](https://github.com/Effect-TS/effect/commit/a9c94c807755610831211a686d2fad849ab38eb4), [`107e6f0`](https://github.com/Effect-TS/effect/commit/107e6f0557a1e2d3b0dce25d62fa1e2601521752), [`c9175ae`](https://github.com/Effect-TS/effect/commit/c9175aef41cb1e3b689d0ac0a4f53d8107376b58), [`65c11b9`](https://github.com/Effect-TS/effect/commit/65c11b9266ec9447c31c26fe3ed35c73bd3b81fd), [`e386d2f`](https://github.com/Effect-TS/effect/commit/e386d2f1b3ab3ac2c14ee76de11f5963d32a3df4), [`9172efb`](https://github.com/Effect-TS/effect/commit/9172efba98bc6a82353e6ec2af61ac08f038ba64)]: + - @effect/platform@0.75.2 + - effect@3.12.8 + - @effect/experimental@0.39.2 + +## 0.8.1 + +### Patch Changes + +- Updated dependencies [[`8dff1d1`](https://github.com/Effect-TS/effect/commit/8dff1d1bff76cdba643cad7f0bf864300f08bc61)]: + - effect@3.12.7 + - @effect/platform@0.75.1 + +## 0.8.0 + +### Minor Changes + +- [#4306](https://github.com/Effect-TS/effect/pull/4306) [`5e43ce5`](https://github.com/Effect-TS/effect/commit/5e43ce50bae116865906112e7f88d390739d778b) Thanks @tim-smart! - eliminate Scope by default in some layer apis + +### Patch Changes + +- Updated dependencies [[`5e43ce5`](https://github.com/Effect-TS/effect/commit/5e43ce50bae116865906112e7f88d390739d778b), [`289c13b`](https://github.com/Effect-TS/effect/commit/289c13b38e8e35b214d46d385d05dead176c87cd), [`76eb7d0`](https://github.com/Effect-TS/effect/commit/76eb7d0fbce3c009c8f77e84c178cb15bbed9709), [`8b4e75d`](https://github.com/Effect-TS/effect/commit/8b4e75d35daea807c447ca760948a717aa66bb52), [`fc5e0f0`](https://github.com/Effect-TS/effect/commit/fc5e0f0d357a0051cfa01c1ede83ffdd3cb41ab1), [`004fd2b`](https://github.com/Effect-TS/effect/commit/004fd2bbd1459e64fb1b57f02eeb791ca5ea1ea5), [`b2a31be`](https://github.com/Effect-TS/effect/commit/b2a31be85c35d891351ce4f9a2cc93ece0c257f6), [`5514d05`](https://github.com/Effect-TS/effect/commit/5514d05b5cd586ff5868b8bd41c959e95e6c33cd), [`bf5f0ae`](https://github.com/Effect-TS/effect/commit/bf5f0ae9daa0170471678e22585e8ec14ce667bb), [`3b19bcf`](https://github.com/Effect-TS/effect/commit/3b19bcfd3aaadb6c9253428622df524537c8e626), [`b064b3b`](https://github.com/Effect-TS/effect/commit/b064b3b293615fd268cc5a5647d0981eb67750b8), [`eb264ed`](https://github.com/Effect-TS/effect/commit/eb264ed8a6e8c92a9dc7006f766c6ca2e5d29e03), [`289c13b`](https://github.com/Effect-TS/effect/commit/289c13b38e8e35b214d46d385d05dead176c87cd), [`f474678`](https://github.com/Effect-TS/effect/commit/f474678bf10b8f1c80e3dc096ddc7ecf20b2b23e), [`ee187d0`](https://github.com/Effect-TS/effect/commit/ee187d098007a402844c94d04f0cd8f07695377a)]: + - @effect/platform@0.75.0 + - effect@3.12.6 + +## 0.7.0 + +### Patch Changes + +- Updated dependencies [[`a8b0ddb`](https://github.com/Effect-TS/effect/commit/a8b0ddb84710054799fc8f57485b95d00093ada1), [`8653072`](https://github.com/Effect-TS/effect/commit/86530720d7a03e118d2c5a8bf5a997cee7e7f3d6), [`507d546`](https://github.com/Effect-TS/effect/commit/507d546bd49db31000425fb5da88c434e4291bea), [`a8b0ddb`](https://github.com/Effect-TS/effect/commit/a8b0ddb84710054799fc8f57485b95d00093ada1), [`8db239b`](https://github.com/Effect-TS/effect/commit/8db239b9c869a3707f6566b9d9dbdf53c4df03fc), [`84a0911`](https://github.com/Effect-TS/effect/commit/84a091181634c3a022c94234cec7764a3aeef1be), [`84a0911`](https://github.com/Effect-TS/effect/commit/84a091181634c3a022c94234cec7764a3aeef1be), [`3179a9f`](https://github.com/Effect-TS/effect/commit/3179a9f65d23369a6a9a1f80f7750566dd28df22), [`6cb9b76`](https://github.com/Effect-TS/effect/commit/6cb9b766396d0b2ed995cf26957359713efd202e), [`1fcbe55`](https://github.com/Effect-TS/effect/commit/1fcbe55345042d8468f6a98c84081bd00b6bcf5a), [`d9a63d9`](https://github.com/Effect-TS/effect/commit/d9a63d9d385653865954cac895065360d54cc56b), [`6cb9b76`](https://github.com/Effect-TS/effect/commit/6cb9b766396d0b2ed995cf26957359713efd202e)]: + - effect@3.12.5 + - @effect/platform@0.74.0 + +## 0.6.1 + +### Patch Changes + +- Updated dependencies [[`5b50ea4`](https://github.com/Effect-TS/effect/commit/5b50ea4a10cf9acd51f9624b2474d9d5ded74019), [`c170a68`](https://github.com/Effect-TS/effect/commit/c170a68b6266100774461fcd6c0e0fabb60112f2), [`a66c2eb`](https://github.com/Effect-TS/effect/commit/a66c2eb473245092cd41f04c2eb2b7b02cf53718), [`c9e5e1b`](https://github.com/Effect-TS/effect/commit/c9e5e1be17c0c84d3d4e2abc3c60215cdb56bbbe), [`7b3d58d`](https://github.com/Effect-TS/effect/commit/7b3d58d7aec2152ec282460871d3e9de45ed254d)]: + - effect@3.12.4 + - @effect/platform@0.73.1 + +## 0.6.0 + +### Patch Changes + +- Updated dependencies [[`d7dac48`](https://github.com/Effect-TS/effect/commit/d7dac48a477cdfeec509dbe9f33fce6a1b02b63d), [`c110032`](https://github.com/Effect-TS/effect/commit/c110032322450a8824ba38ae24335a538cd2ce9a), [`1d7fd2b`](https://github.com/Effect-TS/effect/commit/1d7fd2b7ee8eeecc912d27adf76ed897db236dc5), [`1d7fd2b`](https://github.com/Effect-TS/effect/commit/1d7fd2b7ee8eeecc912d27adf76ed897db236dc5), [`23ac740`](https://github.com/Effect-TS/effect/commit/23ac740c7dd4610b7d265c2071b88b0968419e9a), [`8cd7319`](https://github.com/Effect-TS/effect/commit/8cd7319b6568bfc7a30ca16c104d189e37eac3a0)]: + - effect@3.12.3 + - @effect/platform@0.73.0 + +## 0.5.2 + +### Patch Changes + +- Updated dependencies [[`734af82`](https://github.com/Effect-TS/effect/commit/734af82138e78b9c57a8355b1c6b80e80d38b222), [`b63c780`](https://github.com/Effect-TS/effect/commit/b63c78010893101520448ddda7019c487cf7eedd), [`212e784`](https://github.com/Effect-TS/effect/commit/212e78475f527147ec27c090bd13f789f55add7a), [`f852cb0`](https://github.com/Effect-TS/effect/commit/f852cb02040ea2f165e9b449615b8b1366add5d5), [`7276ae2`](https://github.com/Effect-TS/effect/commit/7276ae21062896adbb7508ac5b2dece95316322f), [`212e784`](https://github.com/Effect-TS/effect/commit/212e78475f527147ec27c090bd13f789f55add7a), [`212e784`](https://github.com/Effect-TS/effect/commit/212e78475f527147ec27c090bd13f789f55add7a), [`212e784`](https://github.com/Effect-TS/effect/commit/212e78475f527147ec27c090bd13f789f55add7a), [`c640d77`](https://github.com/Effect-TS/effect/commit/c640d77b33ad417876f4e8ffe8574ee6cbe5607f), [`0def088`](https://github.com/Effect-TS/effect/commit/0def0887cfdb6755729a64dfd52b3b9f46b0576c)]: + - effect@3.12.2 + - @effect/platform@0.72.2 + +## 0.5.1 + +### Patch Changes + +- Updated dependencies [[`302b57d`](https://github.com/Effect-TS/effect/commit/302b57d2cbf9b9ccc17450945aeebfb33cfe8d43), [`0988083`](https://github.com/Effect-TS/effect/commit/0988083d4594938590df5a287e5b27d38526dd07), [`8b46be6`](https://github.com/Effect-TS/effect/commit/8b46be6a3b8160362ab5ea9171c5e6932505125c), [`bfe8027`](https://github.com/Effect-TS/effect/commit/bfe802734b450a4b4ee069d1125dd37995db2bff), [`16dd657`](https://github.com/Effect-TS/effect/commit/16dd657033d8afac2ffea567b3c8bb27c9b249b6), [`39db211`](https://github.com/Effect-TS/effect/commit/39db211414e90c8db8fdad7dc8ce5b4661bcfaef)]: + - effect@3.12.1 + - @effect/platform@0.72.1 + +## 0.5.0 + +### Patch Changes + +- Updated dependencies [[`abb22a4`](https://github.com/Effect-TS/effect/commit/abb22a429b9c52c31e84856294f175d2064a9b4d), [`f369a89`](https://github.com/Effect-TS/effect/commit/f369a89e98bc682969803b9304adaf4557bb36c2), [`642376c`](https://github.com/Effect-TS/effect/commit/642376c63fd7d78754db991631a4d50a5dc79aa3), [`3d2b7a7`](https://github.com/Effect-TS/effect/commit/3d2b7a7e942a7157afae5b1cdbc6f3fef116428e), [`ef64c6f`](https://github.com/Effect-TS/effect/commit/ef64c6fec0d47da573c04230dde9ea729366d871), [`73f9c6f`](https://github.com/Effect-TS/effect/commit/73f9c6f2ff091512cf904cc54ab59965b86e87c8), [`17cb451`](https://github.com/Effect-TS/effect/commit/17cb4514590e8a86263f7aed009f24da8a237342), [`d801820`](https://github.com/Effect-TS/effect/commit/d80182060c2ee945d7e0e4728812abf9465a0d6a), [`e1eeb2d`](https://github.com/Effect-TS/effect/commit/e1eeb2d7064b3870041dab142f3057970699bbf1), [`c11f3a6`](https://github.com/Effect-TS/effect/commit/c11f3a60a05c3b5fc8e7ce90136728154dc505b0), [`618f7e0`](https://github.com/Effect-TS/effect/commit/618f7e092a1011e5090dca1e69b5e9285689654b), [`c0ba834`](https://github.com/Effect-TS/effect/commit/c0ba834d1995cf5a8b250e4780fd43f3e3881151), [`e1eeb2d`](https://github.com/Effect-TS/effect/commit/e1eeb2d7064b3870041dab142f3057970699bbf1)]: + - effect@3.12.0 + - @effect/platform@0.72.0 + +## 0.4.8 + +### Patch Changes + +- Updated dependencies [[`39457d4`](https://github.com/Effect-TS/effect/commit/39457d4897d9bc7df8af5c05d352866bbeae82eb), [`a475cc2`](https://github.com/Effect-TS/effect/commit/a475cc25fd7c9f26b27a8e98f8fbe43cc9e6ee3e), [`199214e`](https://github.com/Effect-TS/effect/commit/199214e21c616d8a0ccd7ed5f92e944e6c580193), [`b3c160d`](https://github.com/Effect-TS/effect/commit/b3c160d7a1fdfc2d3fb2440530f1ab80efc65133)]: + - effect@3.11.10 + - @effect/platform@0.71.7 + +## 0.4.7 + +### Patch Changes + +- Updated dependencies [[`1c08a0b`](https://github.com/Effect-TS/effect/commit/1c08a0b8505badcffb4d9cade5a746ea90c9557e), [`1ce703b`](https://github.com/Effect-TS/effect/commit/1ce703b041bbd7560c5c437c9b9be48f027937fd), [`1ce703b`](https://github.com/Effect-TS/effect/commit/1ce703b041bbd7560c5c437c9b9be48f027937fd)]: + - effect@3.11.9 + - @effect/platform@0.71.6 + +## 0.4.6 + +### Patch Changes + +- Updated dependencies [[`05d71f8`](https://github.com/Effect-TS/effect/commit/05d71f85622305705d8316817694a09762e60865), [`e66b920`](https://github.com/Effect-TS/effect/commit/e66b9205f25ab425d30640886eb3fb2c4715bc26)]: + - @effect/platform@0.71.5 + +## 0.4.5 + +### Patch Changes + +- Updated dependencies [[`909181a`](https://github.com/Effect-TS/effect/commit/909181a9ce9052a80432ccf52187e0723004bf7f), [`909181a`](https://github.com/Effect-TS/effect/commit/909181a9ce9052a80432ccf52187e0723004bf7f), [`1a6b52d`](https://github.com/Effect-TS/effect/commit/1a6b52dcf020d36e38a7bc90b648152cf5a8ccba)]: + - @effect/platform@0.71.4 + - effect@3.11.8 + +## 0.4.4 + +### Patch Changes + +- Updated dependencies [[`6984508`](https://github.com/Effect-TS/effect/commit/6984508c87f1bd91213b44c19b25ab5e2dcc1ce0), [`883639c`](https://github.com/Effect-TS/effect/commit/883639cc8ce47757f1cd39439391a8028c0812fe)]: + - @effect/platform@0.71.3 + +## 0.4.3 + +### Patch Changes + +- [#4139](https://github.com/Effect-TS/effect/pull/4139) [`1237ae8`](https://github.com/Effect-TS/effect/commit/1237ae847f6f0ff57eb7dcb4723ae6f5073fb925) Thanks @tim-smart! - fix json schema output for Ai completions + +## 0.4.2 + +### Patch Changes + +- Updated dependencies [[`2408616`](https://github.com/Effect-TS/effect/commit/24086163b60b09cc6d0885bd565ef080dcbe866b), [`cec0b4d`](https://github.com/Effect-TS/effect/commit/cec0b4d152ef660be2ccdb0927255f2471436e6e), [`cec0b4d`](https://github.com/Effect-TS/effect/commit/cec0b4d152ef660be2ccdb0927255f2471436e6e), [`8d978c5`](https://github.com/Effect-TS/effect/commit/8d978c53f6fcc98d9d645ecba3e4b55d4297dd36), [`cec0b4d`](https://github.com/Effect-TS/effect/commit/cec0b4d152ef660be2ccdb0927255f2471436e6e), [`cec0b4d`](https://github.com/Effect-TS/effect/commit/cec0b4d152ef660be2ccdb0927255f2471436e6e)]: + - effect@3.11.7 + - @effect/platform@0.71.2 + +## 0.4.1 + +### Patch Changes + +- Updated dependencies [[`1d3df5b`](https://github.com/Effect-TS/effect/commit/1d3df5bc4324e88a392c348db35fd9d029c7b25e)]: + - @effect/platform@0.71.1 + +## 0.4.0 + +### Patch Changes + +- Updated dependencies [[`662d1ce`](https://github.com/Effect-TS/effect/commit/662d1ce6fb7da384a95888d5b2bb5605bdf3208d), [`c99a0f3`](https://github.com/Effect-TS/effect/commit/c99a0f376d049d3793ed33e146d9873b8a5e5b78), [`11fc401`](https://github.com/Effect-TS/effect/commit/11fc401f436f99bf4be95f56d50b0e4bdfe5edea), [`c99a0f3`](https://github.com/Effect-TS/effect/commit/c99a0f376d049d3793ed33e146d9873b8a5e5b78), [`31c62d8`](https://github.com/Effect-TS/effect/commit/31c62d83cbdcf9850a8b5331faa239601c60f78a)]: + - effect@3.11.6 + - @effect/platform@0.71.0 + +## 0.3.7 + +### Patch Changes + +- Updated dependencies [[`9f5a6f7`](https://github.com/Effect-TS/effect/commit/9f5a6f701bf7ba31adccd1f1bcfa8ab5614c9be8), [`22905cf`](https://github.com/Effect-TS/effect/commit/22905cf5addfb1ff3d2a6135c52036be958ae911), [`9f5a6f7`](https://github.com/Effect-TS/effect/commit/9f5a6f701bf7ba31adccd1f1bcfa8ab5614c9be8), [`1e59e4f`](https://github.com/Effect-TS/effect/commit/1e59e4fd778da18296812a2a32f36ca8ae50f60d), [`9f5a6f7`](https://github.com/Effect-TS/effect/commit/9f5a6f701bf7ba31adccd1f1bcfa8ab5614c9be8), [`8d914e5`](https://github.com/Effect-TS/effect/commit/8d914e504e7a22d0ea628e8af265ee450ff9530f), [`03bb00f`](https://github.com/Effect-TS/effect/commit/03bb00faa74f9e168a54a8cc0828a664fbb1ab05), [`9f5a6f7`](https://github.com/Effect-TS/effect/commit/9f5a6f701bf7ba31adccd1f1bcfa8ab5614c9be8), [`14e1149`](https://github.com/Effect-TS/effect/commit/14e1149f1af5a022f06eb8c2e4ba9fec17fe7426), [`9f5a6f7`](https://github.com/Effect-TS/effect/commit/9f5a6f701bf7ba31adccd1f1bcfa8ab5614c9be8), [`9f5a6f7`](https://github.com/Effect-TS/effect/commit/9f5a6f701bf7ba31adccd1f1bcfa8ab5614c9be8)]: + - effect@3.11.5 + - @effect/platform@0.70.7 + +## 0.3.6 + +### Patch Changes + +- Updated dependencies [[`9a5b8e3`](https://github.com/Effect-TS/effect/commit/9a5b8e36d184bd4967a88752cb6e755e1be263af)]: + - @effect/platform@0.70.6 + +## 0.3.5 + +### Patch Changes + +- Updated dependencies [[`415f4c9`](https://github.com/Effect-TS/effect/commit/415f4c98321868531727a83cbaad70164f5e4c40), [`518b258`](https://github.com/Effect-TS/effect/commit/518b258a8a67ecd332a9252c35cc060f8368dee2), [`6e323a3`](https://github.com/Effect-TS/effect/commit/6e323a36faaee46b328c8e3cf60a76b3aff9907f), [`518b258`](https://github.com/Effect-TS/effect/commit/518b258a8a67ecd332a9252c35cc060f8368dee2), [`6e323a3`](https://github.com/Effect-TS/effect/commit/6e323a36faaee46b328c8e3cf60a76b3aff9907f)]: + - @effect/platform@0.70.5 + - effect@3.11.4 + +## 0.3.4 + +### Patch Changes + +- Updated dependencies [[`90906f7`](https://github.com/Effect-TS/effect/commit/90906f7f154b12c7182e8f39e3c55ef3937db857), [`3862cd3`](https://github.com/Effect-TS/effect/commit/3862cd3c7f6a542ed65fb81255b3bd696ce2f567), [`3862cd3`](https://github.com/Effect-TS/effect/commit/3862cd3c7f6a542ed65fb81255b3bd696ce2f567), [`343b6aa`](https://github.com/Effect-TS/effect/commit/343b6aa6ac4a74276bfc7c63ccbf4a1d72bc1bed), [`afba339`](https://github.com/Effect-TS/effect/commit/afba339adc11dad56b5a3b7ca94487e58f34d613)]: + - effect@3.11.3 + - @effect/platform@0.70.4 + +## 0.3.3 + +### Patch Changes + +- [#4071](https://github.com/Effect-TS/effect/pull/4071) [`da3a607`](https://github.com/Effect-TS/effect/commit/da3a607bea16d4f08c5937cadfde0447c4123f40) Thanks @tim-smart! - use openai response_format for structured completions + +- Updated dependencies [[`7044730`](https://github.com/Effect-TS/effect/commit/70447306be1aeeb7d87c230b2a96ec87b993ede9)]: + - @effect/platform@0.70.3 + +## 0.3.2 + +### Patch Changes + +- Updated dependencies [[`01cee56`](https://github.com/Effect-TS/effect/commit/01cee560b58d94b24cc20e98083251b73e658b41), [`c2249ea`](https://github.com/Effect-TS/effect/commit/c2249ea13fd98ab7d9aa628787931356d8ec2860), [`1358aa5`](https://github.com/Effect-TS/effect/commit/1358aa5326eaa85ef13ee8d1fed0b4a4288ed3eb), [`1de3fe7`](https://github.com/Effect-TS/effect/commit/1de3fe7d1cbafd6391eaa38c2300b99e332cc2aa)]: + - effect@3.11.2 + - @effect/platform@0.70.2 + +## 0.3.1 + +### Patch Changes + +- Updated dependencies [[`dd8a2d8`](https://github.com/Effect-TS/effect/commit/dd8a2d8e80d33b16719fc69361eaedf0b59d4620), [`a71bfef`](https://github.com/Effect-TS/effect/commit/a71bfef46f5061bb2502a61a333638a987b62273)]: + - effect@3.11.1 + - @effect/platform@0.70.1 + +## 0.3.0 + +### Patch Changes + +- Updated dependencies [[`147434b`](https://github.com/Effect-TS/effect/commit/147434b03d5e1fd692dd9f126e5ab0910f3b76d3), [`6e69493`](https://github.com/Effect-TS/effect/commit/6e694930048bbaf98110f35f41566aeb9752d471), [`147434b`](https://github.com/Effect-TS/effect/commit/147434b03d5e1fd692dd9f126e5ab0910f3b76d3), [`5eff3f6`](https://github.com/Effect-TS/effect/commit/5eff3f6fa3aae7e86948a62cbfd63b8d6c3bdf92), [`d9fe79b`](https://github.com/Effect-TS/effect/commit/d9fe79bb5a3fe105d8e7a3bc2922a8ad936a5d10), [`251d189`](https://github.com/Effect-TS/effect/commit/251d189420bbba71990574e91098c499065f9a9b), [`5a259f3`](https://github.com/Effect-TS/effect/commit/5a259f3711b4369f55d885b568bdb21136155261), [`b4ce4ea`](https://github.com/Effect-TS/effect/commit/b4ce4ea7fd514a7e572f2dcd879c98f334981b0e), [`15fcc5a`](https://github.com/Effect-TS/effect/commit/15fcc5a0ea4bbf40ab48fa6a04fdda74f76f4c07), [`9bc9a47`](https://github.com/Effect-TS/effect/commit/9bc9a476800dc645903c888a68bb1d3baa3383c6), [`aadb8a4`](https://github.com/Effect-TS/effect/commit/aadb8a48d2cba197c06ec9996505510e48e4e5cb), [`1e2747c`](https://github.com/Effect-TS/effect/commit/1e2747c63a4820d1459cbbc88c71212983bd68bd), [`9264162`](https://github.com/Effect-TS/effect/commit/9264162a82783a651776fb7b87604564a63e7070), [`e0b9b09`](https://github.com/Effect-TS/effect/commit/e0b9b09e70c386b2da17d1f0a15b0511861c89e8), [`c36f3b9`](https://github.com/Effect-TS/effect/commit/c36f3b95df5ce9d71b66f22f26ce12eda8d3e848), [`672bde5`](https://github.com/Effect-TS/effect/commit/672bde5bec51c7d6f9862828e6a654cb2cb6f93d), [`aadb8a4`](https://github.com/Effect-TS/effect/commit/aadb8a48d2cba197c06ec9996505510e48e4e5cb)]: + - effect@3.11.0 + - @effect/platform@0.70.0 + +## 0.2.32 + +### Patch Changes + +- Updated dependencies [[`3069614`](https://github.com/Effect-TS/effect/commit/30696149271129fc618f6f2ccd1d8f2f6c0f9cd7), [`09a5e52`](https://github.com/Effect-TS/effect/commit/09a5e522fd9b221f05d85b1d1c8a740d4973c302)]: + - effect@3.10.20 + - @effect/platform@0.69.32 + +## 0.2.31 + +### Patch Changes + +- Updated dependencies [[`e6d4a37`](https://github.com/Effect-TS/effect/commit/e6d4a37c1d7e657b5ea44063a1cf586808228fe5)]: + - @effect/platform@0.69.31 + +## 0.2.30 + +### Patch Changes + +- Updated dependencies [[`270f199`](https://github.com/Effect-TS/effect/commit/270f199b31810fd643e4c22818698adcbdb5d396)]: + - @effect/platform@0.69.30 + +## 0.2.29 + +### Patch Changes + +- Updated dependencies [[`24cc35e`](https://github.com/Effect-TS/effect/commit/24cc35e26d6ed4a076470bc687ffd99cc50991b3)]: + - @effect/platform@0.69.29 + +## 0.2.28 + +### Patch Changes + +- Updated dependencies [[`edd72be`](https://github.com/Effect-TS/effect/commit/edd72be57b904d60c9cbffc2537901821a9da537), [`a3e2771`](https://github.com/Effect-TS/effect/commit/a3e277170a1f7cf61fd629acb60304c7e81d9498), [`944025b`](https://github.com/Effect-TS/effect/commit/944025bc5ce139f4a85846aa689bf30ec06a8ec1), [`54addee`](https://github.com/Effect-TS/effect/commit/54addee438a644bf010646c52042c7b89c5fc0a7), [`a9e00e4`](https://github.com/Effect-TS/effect/commit/a9e00e43f0b5dd22c1f9d5b78be6383daea09c20)]: + - @effect/platform@0.69.28 + - effect@3.10.19 + +## 0.2.27 + +### Patch Changes + +- Updated dependencies [[`af409cf`](https://github.com/Effect-TS/effect/commit/af409cf1d2ff973be11cc079ea373eaeedca25de), [`beaccae`](https://github.com/Effect-TS/effect/commit/beaccae2d15931e9fe475fb50a0b3638243fe3f7)]: + - effect@3.10.18 + - @effect/platform@0.69.27 + +## 0.2.26 + +### Patch Changes + +- Updated dependencies [[`c963886`](https://github.com/Effect-TS/effect/commit/c963886d5817986fcbd6bfa4ddf50aca8b6c8184), [`42c4ce6`](https://github.com/Effect-TS/effect/commit/42c4ce6f8d8c7d847e97757650a8ad9419a829d7)]: + - @effect/platform@0.69.26 + - effect@3.10.17 + +## 0.2.25 + +### Patch Changes + +- Updated dependencies [[`320557a`](https://github.com/Effect-TS/effect/commit/320557ab18d13c5e22fc7dc0d2a157eae461012f), [`4dca30c`](https://github.com/Effect-TS/effect/commit/4dca30cfcdafe4542e236489f71d6f171a5b4e38), [`1d99867`](https://github.com/Effect-TS/effect/commit/1d998671be3cd11043f232822e91dd8c98fccfa9), [`6dae414`](https://github.com/Effect-TS/effect/commit/6dae4147991a97ec14a99289bd25fadae7541e8d), [`6b0d737`](https://github.com/Effect-TS/effect/commit/6b0d737078bf63b97891e6bc47affc04b28f9cf7), [`d8356aa`](https://github.com/Effect-TS/effect/commit/d8356aad428a0c2290db52380220f81d9ec94232), [`7b93dd6`](https://github.com/Effect-TS/effect/commit/7b93dd622e2ab79c7072d79d0d9611e446202201)]: + - @effect/platform@0.69.25 + - effect@3.10.16 + +## 0.2.24 + +### Patch Changes + +- Updated dependencies [[`3cc6514`](https://github.com/Effect-TS/effect/commit/3cc6514d2dd64e010cb760cc29bfce98c349bb10)]: + - @effect/platform@0.69.24 + +## 0.2.23 + +### Patch Changes + +- Updated dependencies [[`3aff4d3`](https://github.com/Effect-TS/effect/commit/3aff4d38837c213bb2987973dc4b98febb9f92d2)]: + - @effect/platform@0.69.23 + +## 0.2.22 + +### Patch Changes + +- Updated dependencies [[`8398b32`](https://github.com/Effect-TS/effect/commit/8398b3208242a88239d4449910b7baf923cfe3b6), [`72e55b7`](https://github.com/Effect-TS/effect/commit/72e55b7c610784fcebdbadc592c876e23e76a986)]: + - effect@3.10.15 + - @effect/platform@0.69.22 + +## 0.2.21 + +### Patch Changes + +- Updated dependencies [[`f983946`](https://github.com/Effect-TS/effect/commit/f9839467b4cad6e788297764ef9f9f0b9fd203f9), [`2d8a750`](https://github.com/Effect-TS/effect/commit/2d8a75081eb83a0a81f817fdf6f428369c5064ab)]: + - effect@3.10.14 + - @effect/platform@0.69.21 + +## 0.2.20 + +### Patch Changes + +- [#3916](https://github.com/Effect-TS/effect/pull/3916) [`72b0272`](https://github.com/Effect-TS/effect/commit/72b02726d62000756577464273c0dd0876cbe0b5) Thanks @tim-smart! - use effect/JSONSchema for effect/ai & allow http client transforms + +- Updated dependencies [[`995bbdf`](https://github.com/Effect-TS/effect/commit/995bbdffea2e332f203cd5b474cd6a1c77dfa6ae)]: + - effect@3.10.13 + - @effect/platform@0.69.20 + +## 0.2.19 + +### Patch Changes + +- Updated dependencies [[`eb8c52d`](https://github.com/Effect-TS/effect/commit/eb8c52d8b4c5e067ebf0a81eb742f5822e6439b5)]: + - @effect/platform@0.69.19 + +## 0.2.18 + +### Patch Changes + +- Updated dependencies [[`a0584ec`](https://github.com/Effect-TS/effect/commit/a0584ece92ed784bfb139e9c5a699f02d1e71c2d), [`dd14efe`](https://github.com/Effect-TS/effect/commit/dd14efe0ace255f571273aae876adea96267d7e6), [`dd14efe`](https://github.com/Effect-TS/effect/commit/dd14efe0ace255f571273aae876adea96267d7e6)]: + - @effect/platform@0.69.18 + - effect@3.10.12 + +## 0.2.17 + +### Patch Changes + +- Updated dependencies [[`5eef499`](https://github.com/Effect-TS/effect/commit/5eef4998b6ccb7a5404d9e4fef85e57fa35fbb8a), [`8240b1c`](https://github.com/Effect-TS/effect/commit/8240b1c10d45312fc863cb679b1a1e8441af0c1a), [`5eef499`](https://github.com/Effect-TS/effect/commit/5eef4998b6ccb7a5404d9e4fef85e57fa35fbb8a)]: + - effect@3.10.11 + - @effect/platform@0.69.17 + +## 0.2.16 + +### Patch Changes + +- Updated dependencies [[`cd720ae`](https://github.com/Effect-TS/effect/commit/cd720aedf7f2571edec0843d6a633e84e4832b28), [`cd720ae`](https://github.com/Effect-TS/effect/commit/cd720aedf7f2571edec0843d6a633e84e4832b28), [`cd720ae`](https://github.com/Effect-TS/effect/commit/cd720aedf7f2571edec0843d6a633e84e4832b28), [`b631f40`](https://github.com/Effect-TS/effect/commit/b631f40abbe649b2a089764585b5c39f6a695ac6), [`7d89650`](https://github.com/Effect-TS/effect/commit/7d8965036cd2ea435c8441ffec3345488baebf85)]: + - effect@3.10.10 + - @effect/platform@0.69.16 + +## 0.2.15 + +### Patch Changes + +- Updated dependencies [[`8a30e1d`](https://github.com/Effect-TS/effect/commit/8a30e1dfa3a7103bf5414fc6a7fca3088d8c8c00)]: + - @effect/platform@0.69.15 + +## 0.2.14 + +### Patch Changes + +- Updated dependencies [[`a123e80`](https://github.com/Effect-TS/effect/commit/a123e80f111a625428a5b5622b7f55ee1073566b), [`bd5fcd3`](https://github.com/Effect-TS/effect/commit/bd5fcd3e6b603b1e505af90d6a00627c8eca6d41), [`0289d3b`](https://github.com/Effect-TS/effect/commit/0289d3b6391031d00329365bab9791b355031fe3), [`7386b71`](https://github.com/Effect-TS/effect/commit/7386b710e5be570e17f468928a6ed19d549a3e12), [`07c493a`](https://github.com/Effect-TS/effect/commit/07c493a598e096c7810cd06def8cfa43493c46b1), [`257ab1b`](https://github.com/Effect-TS/effect/commit/257ab1b539fa6e930b7ae2583a188376372200d7), [`4211a23`](https://github.com/Effect-TS/effect/commit/4211a2355bb3af3f0e756e2aae9d293379f25662)]: + - effect@3.10.9 + - @effect/platform@0.69.14 + +## 0.2.13 + +### Patch Changes + +- Updated dependencies [[`68b5c9e`](https://github.com/Effect-TS/effect/commit/68b5c9e44f34192cef26e1cadda5e661a027df41), [`9c9928d`](https://github.com/Effect-TS/effect/commit/9c9928dfeacd9ac33dc37eb0ca3d7d8c39175ada), [`6306e66`](https://github.com/Effect-TS/effect/commit/6306e6656092b350d4ede5746da6f245ec9f7e07), [`361c7f3`](https://github.com/Effect-TS/effect/commit/361c7f39a2c10ede9324847c3d3ba192a6f9b20a)]: + - effect@3.10.8 + - @effect/platform@0.69.13 + +## 0.2.12 + +### Patch Changes + +- Updated dependencies [[`33f5b9f`](https://github.com/Effect-TS/effect/commit/33f5b9ffaebea4f1bd0e391b44c41fb6230e743a), [`50f0281`](https://github.com/Effect-TS/effect/commit/50f0281b0d2116726b8927a6217622d5f394f3e4)]: + - effect@3.10.7 + - @effect/platform@0.69.12 + +## 0.2.11 + +### Patch Changes + +- Updated dependencies [[`ce1c21f`](https://github.com/Effect-TS/effect/commit/ce1c21ffc11902ac9ab453a51904207859d38552), [`81ddd45`](https://github.com/Effect-TS/effect/commit/81ddd45fc074b98206fafab416d9a5a28b31e07a)]: + - effect@3.10.6 + - @effect/platform@0.69.11 + +## 0.2.10 + +### Patch Changes + +- Updated dependencies [[`3a6d757`](https://github.com/Effect-TS/effect/commit/3a6d757badeebe00d8ef4d67530d073c8264dcfa), [`59d813a`](https://github.com/Effect-TS/effect/commit/59d813aa4973d1115cfc70cc3667508335f49693)]: + - effect@3.10.5 + - @effect/platform@0.69.10 + +## 0.2.9 + +### Patch Changes + +- Updated dependencies [[`2367708`](https://github.com/Effect-TS/effect/commit/2367708be449f9526a2047e321302d7bfb16f18e)]: + - @effect/platform@0.69.9 + - effect@3.10.4 + +## 0.2.8 + +### Patch Changes + +- Updated dependencies [[`522f7c5`](https://github.com/Effect-TS/effect/commit/522f7c518a5acfb55ef96d6796869f002cc3eaf8)]: + - @effect/platform@0.69.8 + +## 0.2.7 + +### Patch Changes + +- Updated dependencies [[`690d6c5`](https://github.com/Effect-TS/effect/commit/690d6c54d2145adb0af545c447db7d4755bf3c6b), [`b9423d8`](https://github.com/Effect-TS/effect/commit/b9423d8bf8181a2389fdbce1e3c14ac6fe8d54f5), [`279fe3a`](https://github.com/Effect-TS/effect/commit/279fe3a7168fe84e520c2cc88ba189a15f03a2bc)]: + - @effect/platform@0.69.7 + - effect@3.10.3 + +## 0.2.6 + +### Patch Changes + +- Updated dependencies [[`714e119`](https://github.com/Effect-TS/effect/commit/714e11945e45e5a2554ee058e6c43f82a8e309cf), [`c1afd55`](https://github.com/Effect-TS/effect/commit/c1afd55c54e61f9c432823d21b3d016f79160a37), [`42cd72a`](https://github.com/Effect-TS/effect/commit/42cd72a44ca9593e4d81fbb50e8111625fd0fb81)]: + - effect@3.10.2 + - @effect/platform@0.69.6 + +## 0.2.5 + +### Patch Changes + +- Updated dependencies [[`9604d6b`](https://github.com/Effect-TS/effect/commit/9604d6b616435103dafea8b53637a9d1450b4750)]: + - effect@3.10.1 + - @effect/platform@0.69.5 + +## 0.2.4 + +### Patch Changes + +- Updated dependencies [[`c86b1d7`](https://github.com/Effect-TS/effect/commit/c86b1d7cd47b66df190ef9775a475467c1abdbd6)]: + - @effect/platform@0.69.4 + +## 0.2.3 + +### Patch Changes + +- Updated dependencies [[`d5fba63`](https://github.com/Effect-TS/effect/commit/d5fba6391e1005e374aa0238f13edfbd65848313), [`1eb2c30`](https://github.com/Effect-TS/effect/commit/1eb2c30ba064398db5790e376dedcfad55b7b005), [`02d413e`](https://github.com/Effect-TS/effect/commit/02d413e7b6bc1c64885969c37cc3e4e690c94d7d)]: + - @effect/platform@0.69.3 + +## 0.2.2 + +### Patch Changes + +- Updated dependencies [[`e7afc47`](https://github.com/Effect-TS/effect/commit/e7afc47ce83e381c3f4aed2b2974e3b3d86a2340)]: + - @effect/platform@0.69.2 + +## 0.2.1 + +### Patch Changes + +- Updated dependencies [[`7564f56`](https://github.com/Effect-TS/effect/commit/7564f56bb2844cf39d2b0d2d9e93cf9b2205e9a8), [`7564f56`](https://github.com/Effect-TS/effect/commit/7564f56bb2844cf39d2b0d2d9e93cf9b2205e9a8)]: + - @effect/platform@0.69.1 + +## 0.2.0 + +### Patch Changes + +- Updated dependencies [[`4a01828`](https://github.com/Effect-TS/effect/commit/4a01828b66d6213e9bbe18979c893b13f7bb29bf), [`6d9de6b`](https://github.com/Effect-TS/effect/commit/6d9de6b871c5c08e6509a4e830c3d74758faa198), [`4a01828`](https://github.com/Effect-TS/effect/commit/4a01828b66d6213e9bbe18979c893b13f7bb29bf), [`c79c4c1`](https://github.com/Effect-TS/effect/commit/c79c4c178390fe61ff6dda88c9e058862349343a), [`38d30f0`](https://github.com/Effect-TS/effect/commit/38d30f08b8da62f9c3e308b9250738cb8d17bdb5), [`5821ce3`](https://github.com/Effect-TS/effect/commit/5821ce3455b47d25e0a40cae6ce22af9db5fa556)]: + - effect@3.10.0 + - @effect/platform@0.69.0 + +## 0.1.4 + +### Patch Changes + +- Updated dependencies [[`382556f`](https://github.com/Effect-TS/effect/commit/382556f8930780c0634de681077706113a8c8239), [`97cb014`](https://github.com/Effect-TS/effect/commit/97cb0145114b2cd2f378e98f6c4ff5bf2c1865f5)]: + - @effect/schema@0.75.5 + - @effect/platform@0.68.6 + +## 0.1.3 + +### Patch Changes + +- Updated dependencies [[`2036402`](https://github.com/Effect-TS/effect/commit/20364020b8b75a684791aa93d90626758023e9e9)]: + - @effect/platform@0.68.5 + +## 0.1.2 + +### Patch Changes + +- Updated dependencies [[`1b1ef29`](https://github.com/Effect-TS/effect/commit/1b1ef29ae302322f69dc938f9337aa97b4c63266)]: + - @effect/platform@0.68.4 + +## 0.1.1 + +### Patch Changes + +- Updated dependencies [[`61a99b2`](https://github.com/Effect-TS/effect/commit/61a99b2bf9d757870ef0c2ec9d4c877cdd364a3d), [`8c33087`](https://github.com/Effect-TS/effect/commit/8c330879425e80bed2f65e407cd59e991f0d7bec)]: + - effect@3.9.2 + - @effect/platform@0.68.3 + - @effect/schema@0.75.4 + +## 0.1.0 + +### Minor Changes + +- [#3631](https://github.com/Effect-TS/effect/pull/3631) [`bd160a4`](https://github.com/Effect-TS/effect/commit/bd160a4f714b0f1cb5867e458fd70f9131b060d6) Thanks @tim-smart! - add @effect/ai packages + + Experimental modules for working with LLMs, currently only from OpenAI. + +### Patch Changes + +- Updated dependencies [[`360ec14`](https://github.com/Effect-TS/effect/commit/360ec14dd4102c526aef7433a8881ad4d9beab75)]: + - @effect/schema@0.75.3 + - @effect/platform@0.68.2 diff --git a/repos/effect/packages/ai/ai/LICENSE b/repos/effect/packages/ai/ai/LICENSE new file mode 100644 index 0000000..be1f5c1 --- /dev/null +++ b/repos/effect/packages/ai/ai/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Effectful Technologies Inc + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/repos/effect/packages/ai/ai/README.md b/repos/effect/packages/ai/ai/README.md new file mode 100644 index 0000000..b7eaed0 --- /dev/null +++ b/repos/effect/packages/ai/ai/README.md @@ -0,0 +1,6 @@ +# `@effect/ai` + +## Documentation + +- **Introduction**: [Read the introduction and usage guide on our website](https://effect.website/docs/ai/introduction) +- **API Reference**: [View the full documentation](https://effect-ts.github.io/effect/docs/ai/ai). diff --git a/repos/effect/packages/ai/ai/docgen.json b/repos/effect/packages/ai/ai/docgen.json new file mode 100644 index 0000000..e38c718 --- /dev/null +++ b/repos/effect/packages/ai/ai/docgen.json @@ -0,0 +1,24 @@ +{ + "$schema": "../../../node_modules/@effect/docgen/schema.json", + "exclude": ["src/internal/**/*.ts"], + "srcLink": "https://github.com/Effect-TS/effect/tree/main/packages/ai/ai/src/", + "examplesCompilerOptions": { + "noEmit": true, + "strict": true, + "skipLibCheck": true, + "moduleResolution": "Bundler", + "module": "ES2022", + "target": "ES2022", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "paths": { + "effect": ["../../../../effect/src/index.js"], + "effect/*": ["../../../../effect/src/*.js"], + "@effect/platform": ["../../../../platform/src/index.js"], + "@effect/platform/*": ["../../../../platform/src/*.js"], + "@effect/platform-node": ["../../../../platform-node/src/index.js"], + "@effect/platform-node/*": ["../../../../platform-node/src/*.js"], + "@effect/ai": ["../../../ai/src/index.js"], + "@effect/ai/*": ["../../../ai/src/*.js"] + } + } +} diff --git a/repos/effect/packages/ai/ai/package.json b/repos/effect/packages/ai/ai/package.json new file mode 100644 index 0000000..9d73762 --- /dev/null +++ b/repos/effect/packages/ai/ai/package.json @@ -0,0 +1,63 @@ +{ + "name": "@effect/ai", + "type": "module", + "version": "0.35.0", + "license": "MIT", + "description": "Effect modules for working with AI apis", + "homepage": "https://effect.website", + "repository": { + "type": "git", + "url": "https://github.com/Effect-TS/effect.git", + "directory": "packages/ai/ai" + }, + "bugs": { + "url": "https://github.com/Effect-TS/effect/issues" + }, + "tags": [ + "typescript", + "algebraic-data-types", + "functional-programming" + ], + "keywords": [ + "typescript", + "algebraic-data-types", + "functional-programming" + ], + "publishConfig": { + "access": "public", + "provenance": true, + "directory": "dist", + "linkDirectory": false + }, + "exports": { + "./package.json": "./package.json", + ".": "./src/index.ts", + "./*": "./src/*.ts", + "./internal/*": null + }, + "scripts": { + "codegen": "build-utils prepare-v3", + "build": "pnpm build-esm && pnpm build-annotate && pnpm build-cjs && build-utils pack-v3", + "build-esm": "tsc -b tsconfig.build.json", + "build-cjs": "babel build/esm --plugins @babel/transform-export-namespace-from --plugins @babel/transform-modules-commonjs --out-dir build/cjs --source-maps", + "build-annotate": "babel build/esm --plugins annotate-pure-calls --out-dir build/esm --source-maps", + "check": "tsc -b tsconfig.json", + "test": "vitest", + "coverage": "vitest --coverage" + }, + "peerDependencies": { + "@effect/experimental": "workspace:^", + "@effect/platform": "workspace:^", + "@effect/rpc": "workspace:^", + "effect": "workspace:^" + }, + "devDependencies": { + "@effect/experimental": "workspace:^", + "@effect/platform": "workspace:^", + "@effect/rpc": "workspace:^", + "effect": "workspace:^" + }, + "dependencies": { + "find-my-way-ts": "^0.1.6" + } +} diff --git a/repos/effect/packages/ai/ai/src/AiError.ts b/repos/effect/packages/ai/ai/src/AiError.ts new file mode 100644 index 0000000..f87687e --- /dev/null +++ b/repos/effect/packages/ai/ai/src/AiError.ts @@ -0,0 +1,788 @@ +/** + * The `AiError` module provides comprehensive error handling for AI operations. + * + * This module defines a hierarchy of error types that can occur when working + * with AI services, including HTTP request/response errors, input/output + * validation errors, and general runtime errors. All errors follow Effect's + * structured error patterns and provide detailed context for debugging. + * + * ## Error Types + * + * - **HttpRequestError**: Errors occurring during HTTP request processing + * - **HttpResponseError**: Errors occurring during HTTP response processing + * - **MalformedInput**: Errors when input data doesn't match expected format + * - **MalformedOutput**: Errors when output data can't be parsed or validated + * - **UnknownError**: Catch-all for unexpected runtime errors + * + * @example + * ```ts + * import { AiError } from "@effect/ai" + * import { Effect, Match } from "effect" + * + * const handleAiError = Match.type().pipe( + * Match.tag("HttpRequestError", (err) => + * Effect.logError(`Request failed: ${err.message}`) + * ), + * Match.tag("HttpResponseError", (err) => + * Effect.logError(`Response error (${err.response.status}): ${err.message}`) + * ), + * Match.tag("MalformedInput", (err) => + * Effect.logError(`Invalid input: ${err.message}`) + * ), + * Match.tag("MalformedOutput", (err) => + * Effect.logError(`Invalid output: ${err.message}`) + * ), + * Match.orElse((err) => + * Effect.logError(`Unknown error: ${err.message}`) + * ) + * ) + * ``` + * + * @example + * ```ts + * import { AiError } from "@effect/ai" + * import { Effect, Option } from "effect" + * + * const aiOperation = Effect.gen(function* () { + * // Some AI operation that might fail + * return yield* new AiError.HttpRequestError({ + * module: "OpenAI", + * method: "completion", + * reason: "Transport", + * request: { + * method: "POST", + * url: "https://api.openai.com/v1/completions", + * urlParams: [], + * hash: Option.none(), + * headers: { "Content-Type": "application/json" } + * } + * }) + * }) + * + * const program = aiOperation.pipe( + * Effect.catchTag("HttpRequestError", (error) => { + * console.log("Request failed:", error.message) + * return Effect.succeed("fallback response") + * }) + * ) + * ``` + * + * @since 1.0.0 + */ +import type * as HttpClientError from "@effect/platform/HttpClientError" +import * as Effect from "effect/Effect" +import * as Inspectable from "effect/Inspectable" +import type { ParseError } from "effect/ParseResult" +import * as Predicate from "effect/Predicate" +import * as Schema from "effect/Schema" + +/** + * Unique identifier for AI errors. + * + * @since 1.0.0 + * @category Type Ids + */ +export const TypeId = "~@effect/ai/AiError" + +/** + * Type-level representation of the AI error identifier. + * + * @since 1.0.0 + * @category Type Ids + */ +export type TypeId = typeof TypeId + +/** + * Type guard to check if a value is an AI error. + * + * @param u - The value to check + * @returns `true` if the value is an `AiError`, `false` otherwise + * + * @example + * ```ts + * import { AiError } from "@effect/ai" + * + * const someError = new Error("generic error") + * const aiError = new AiError.UnknownError({ + * module: "Test", + * method: "example" + * }) + * + * console.log(AiError.isAiError(someError)) // false + * console.log(AiError.isAiError(aiError)) // true + * ``` + * + * @since 1.0.0 + * @category Guards + */ +export const isAiError = (u: unknown): u is AiError => Predicate.hasProperty(u, TypeId) + +// ============================================================================= +// Http Request Error +// ============================================================================= + +/** + * Schema for HTTP request details used in error reporting. + * + * Captures comprehensive information about HTTP requests that failed, + * enabling detailed error analysis and debugging. + * + * @example + * ```ts + * import { AiError } from "@effect/ai" + * import { Option } from "effect" + * + * const requestDetails: typeof AiError.HttpRequestDetails.Type = { + * method: "POST", + * url: "https://api.openai.com/v1/completions", + * urlParams: [["model", "gpt-4"], ["stream", "false"]], + * hash: Option.some("#section1"), + * headers: { "Content-Type": "application/json" } + * } + * ``` + * + * @since 1.0.0 + * @category Schemas + */ +export const HttpRequestDetails = Schema.Struct({ + method: Schema.Literal("GET", "POST", "PATCH", "PUT", "DELETE", "HEAD", "OPTIONS"), + url: Schema.String, + urlParams: Schema.Array(Schema.Tuple(Schema.String, Schema.String)), + hash: Schema.Option(Schema.String), + headers: Schema.Record({ key: Schema.String, value: Schema.String }) +}).annotations({ identifier: "HttpRequestDetails" }) + +/** + * Error that occurs during HTTP request processing. + * + * This error is raised when issues arise before receiving an HTTP response, + * such as network connectivity problems, request encoding issues, or invalid + * URLs. + * + * @example + * ```ts + * import { AiError } from "@effect/ai" + * import * as Effect from "effect/Effect" + * import * as Option from "effect/Option" + * + * const handleNetworkError = Effect.gen(function* () { + * const error = new AiError.HttpRequestError({ + * module: "OpenAI", + * method: "createCompletion", + * reason: "Transport", + * request: { + * method: "POST", + * url: "https://api.openai.com/v1/completions", + * urlParams: [], + * hash: Option.none(), + * headers: { "Content-Type": "application/json" } + * }, + * description: "Connection timeout after 30 seconds" + * }) + * + * console.log(error.message) + * // "Transport: Connection timeout after 30 seconds (POST https://api.openai.com/v1/completions)" + * }) + * ``` + * + * @since 1.0.0 + * @category Errors + */ +export class HttpRequestError extends Schema.TaggedError( + "@effect/ai/AiError/HttpRequestError" +)("HttpRequestError", { + module: Schema.String, + method: Schema.String, + reason: Schema.Literal("Transport", "Encode", "InvalidUrl"), + request: HttpRequestDetails, + description: Schema.optional(Schema.String), + cause: Schema.optional(Schema.Defect) +}) { + /** + * @since 1.0.0 + */ + readonly [TypeId]: TypeId = TypeId + + /** + * Creates an HttpRequestError from a platform HttpClientError.RequestError. + * + * @example + * ```ts + * import { AiError } from "@effect/ai" + * import { HttpClientError } from "@effect/platform" + * import { Option } from "effect" + * + * declare const platformError: HttpClientError.RequestError + * + * const aiError = AiError.HttpRequestError.fromRequestError({ + * module: "ChatGPT", + * method: "sendMessage", + * error: platformError + * }) + * ``` + * + * @since 1.0.0 + * @category Constructors + */ + static fromRequestError({ error, ...params }: { + readonly module: string + readonly method: string + readonly error: HttpClientError.RequestError + }): HttpRequestError { + return new HttpRequestError({ + ...params, + cause: error, + description: error.description, + reason: error.reason, + request: { + hash: error.request.hash, + headers: Inspectable.redact(error.request.headers) as any, + method: error.request.method, + url: error.request.url, + urlParams: error.request.urlParams + } + }, { disableValidation: true }) + } + + get message(): string { + const methodAndUrl = `${this.request.method} ${this.request.url}` + + let baseMessage = this.description + ? `${this.reason}: ${this.description}` + : `${this.reason}: An HTTP request error occurred.` + + baseMessage += ` (${methodAndUrl})` + + let suggestion = "" + switch (this.reason) { + case "Encode": { + suggestion += "Check that the request body data is properly formatted and matches the expected content type." + break + } + + case "InvalidUrl": { + suggestion += "Verify that the URL format is correct and that all required parameters have been provided." + suggestion += " Check for any special characters that may need encoding." + break + } + + case "Transport": { + suggestion += "Check your network connection and verify that the requested URL is accessible." + break + } + } + + baseMessage += `\n\nSuggestion: ${suggestion}` + + return baseMessage + } +} + +// ============================================================================= +// Http Response Error +// ============================================================================= + +/** + * Schema for HTTP response details used in error reporting. + * + * Captures essential information about HTTP responses that caused errors, + * including status codes and headers for debugging purposes. + * + * @example + * ```ts + * import { AiError } from "@effect/ai" + * + * const responseDetails: typeof AiError.HttpResponseDetails.Type = { + * status: 429, + * headers: { + * "Content-Type": "application/json", + * "X-RateLimit-Remaining": "0", + * "Retry-After": "60" + * } + * } + * ``` + * + * @since 1.0.0 + * @category Schemas + */ +export const HttpResponseDetails = Schema.Struct({ + status: Schema.Number, + headers: Schema.Record({ key: Schema.String, value: Schema.String }) +}).annotations({ identifier: "HttpResponseDetails" }) + +/** + * Error that occurs during HTTP response processing. + * + * This error is thrown when issues arise after receiving an HTTP response, + * such as unexpected status codes, response decoding failures, or empty + * response bodies. + * + * @example + * ```ts + * import { AiError } from "@effect/ai" + * import { Option } from "effect" + * + * const responseError = new AiError.HttpResponseError({ + * module: "OpenAI", + * method: "createCompletion", + * reason: "StatusCode", + * request: { + * method: "POST", + * url: "https://api.openai.com/v1/completions", + * urlParams: [], + * hash: Option.none(), + * headers: { "Content-Type": "application/json" } + * }, + * response: { + * status: 429, + * headers: { "X-RateLimit-Remaining": "0" } + * }, + * description: "Rate limit exceeded" + * }) + * + * console.log(responseError.message) + * // "StatusCode: Rate limit exceeded (429 POST https://api.openai.com/v1/completions)" + * ``` + * + * @since 1.0.0 + * @category Errors + */ +export class HttpResponseError extends Schema.TaggedError( + "@effect/ai/AiError/HttpResponseError" +)("HttpResponseError", { + module: Schema.String, + method: Schema.String, + request: HttpRequestDetails, + response: HttpResponseDetails, + body: Schema.optional(Schema.String), + reason: Schema.Literal("StatusCode", "Decode", "EmptyBody"), + description: Schema.optional(Schema.String) +}) { + /** + * @since 1.0.0 + */ + readonly [TypeId]: TypeId = TypeId + + /** + * Creates an HttpResponseError from a platform HttpClientError.ResponseError. + * + * @example + * ```ts + * import { AiError } from "@effect/ai" + * import { Headers, HttpClientError } from "@effect/platform" + * import { Option } from "effect" + * + * declare const platformError: HttpClientError.ResponseError + * + * const aiError = AiError.HttpResponseError.fromResponseError({ + * module: "OpenAI", + * method: "completion", + * error: platformError + * }) + * ``` + * + * @since 1.0.0 + * @category Constructors + */ + static fromResponseError({ error, ...params }: { + readonly module: string + readonly method: string + readonly error: HttpClientError.ResponseError + }): Effect.Effect { + let body: Effect.Effect = Effect.void + const contentType = error.response.headers["content-type"] ?? "" + if (contentType.includes("application/json")) { + body = error.response.json + } else if (contentType.includes("text/") || contentType.includes("urlencoded")) { + body = error.response.text + } + return Effect.flatMap(Effect.merge(body), (body) => + new HttpResponseError({ + ...params, + description: error.description, + reason: error.reason, + request: { + hash: error.request.hash, + headers: Inspectable.redact(error.request.headers) as any, + method: error.request.method, + url: error.request.url, + urlParams: error.request.urlParams + }, + response: { + headers: Inspectable.redact(error.response.headers) as any, + status: error.response.status + }, + body: Inspectable.format(body) + }, { disableValidation: true })) + } + + get message(): string { + const methodUrlStatus = `${this.response.status} ${this.request.method} ${this.request.url}` + + let baseMessage = this.description + ? `${this.reason}: ${this.description}` + : `${this.reason}: An HTTP response error occurred.` + + baseMessage += ` (${methodUrlStatus})` + + let suggestion = "" + switch (this.reason) { + case "Decode": { + suggestion += "The response format does not match what is expected. " + + "Verify API version compatibility, check response content-type, " + + "and/or examine if the endpoint schema has changed." + break + } + case "EmptyBody": { + suggestion += "The response body was empty. This may indicate a server " + + "issue, API version mismatch, or the endpoint may have changed its response format." + break + } + case "StatusCode": { + suggestion += getStatusCodeSuggestion(this.response.status) + break + } + } + + baseMessage += `\n\n${suggestion}` + + if (Predicate.isNotUndefined(this.body)) { + baseMessage += `\n\nResponse Body: ${this.body}` + } + + return baseMessage + } +} + +// ============================================================================= +// Malformed Input Error +// ============================================================================= + +/** + * Error thrown when input data doesn't match the expected format or schema. + * + * This error occurs when the data provided to an AI operation fails validation, + * is missing required fields, or doesn't conform to the expected structure. + * + * @example + * ```ts + * import { AiError } from "@effect/ai" + * import * as Effect from "effect/Effect" + * + * const validateInput = (data: unknown): Effect.Effect => + * typeof data === "string" && data.length > 0 + * ? Effect.succeed(data) + * : Effect.fail(new AiError.MalformedInput({ + * module: "ChatBot", + * method: "processMessage", + * description: "Input must be a non-empty string" + * })) + * + * const program = validateInput("").pipe( + * Effect.catchTag("MalformedInput", (error) => { + * console.log(`Input validation failed: ${error.description}`) + * return Effect.succeed("Please provide a valid message") + * }) + * ) + * ``` + * + * @since 1.0.0 + * @category Errors + */ +export class MalformedInput extends Schema.TaggedError( + "@effect/ai/AiError/MalformedInput" +)("MalformedInput", { + module: Schema.String, + method: Schema.String, + description: Schema.optional(Schema.String), + cause: Schema.optional(Schema.Defect) +}) { + /** + * @since 1.0.0 + */ + readonly [TypeId]: TypeId = TypeId +} + +// ============================================================================= +// Malformed Output Error +// ============================================================================= + +/** + * Error thrown when output data can't be parsed or validated. + * + * This error occurs when AI service responses don't match the expected format, + * contain invalid data structures, or fail schema validation during parsing. + * + * @example + * ```ts + * import { AiError } from "@effect/ai" + * import { Effect, Schema } from "effect" + * + * const ResponseSchema = Schema.Struct({ + * message: Schema.String, + * tokens: Schema.Number + * }) + * + * const parseResponse = (data: unknown) => + * Schema.decodeUnknown(ResponseSchema)(data).pipe( + * Effect.mapError(parseError => + * new AiError.MalformedOutput({ + * module: "OpenAI", + * method: "completion", + * description: "Response doesn't match expected schema", + * cause: parseError + * }) + * ) + * ) + * + * const program = parseResponse({ invalid: "data" }).pipe( + * Effect.catchTag("MalformedOutput", (error) => { + * console.log(`Parsing failed: ${error.description}`) + * return Effect.succeed({ message: "Error", tokens: 0 }) + * }) + * ) + * ``` + * + * @since 1.0.0 + * @category Errors + */ +export class MalformedOutput extends Schema.TaggedError( + "@effect/ai/AiError/MalformedOutput" +)("MalformedOutput", { + module: Schema.String, + method: Schema.String, + description: Schema.optional(Schema.String), + cause: Schema.optional(Schema.Defect) +}) { + /** + * @since 1.0.0 + */ + readonly [TypeId]: TypeId = TypeId + + /** + * Creates a MalformedOutput error from a Schema ParseError. + * + * @example + * ```ts + * import { AiError } from "@effect/ai" + * import { Effect, Schema } from "effect" + * + * const UserSchema = Schema.Struct({ + * name: Schema.String, + * age: Schema.Number + * }) + * + * const parseUser = (data: unknown) => + * Schema.decodeUnknown(UserSchema)(data).pipe( + * Effect.mapError((parseError) => + * AiError.MalformedOutput.fromParseError({ + * module: "UserService", + * method: "parseUserData", + * error: parseError + * }) + * ) + * ) + * ``` + * + * @since 1.0.0 + * @category Constructors + */ + static fromParseError({ error, ...params }: { + readonly module: string + readonly method: string + readonly description?: string + readonly error: ParseError + }): MalformedOutput { + // TODO(Max): enhance + return new MalformedOutput({ + ...params, + cause: error + }) + } +} + +// ============================================================================= +// Unknown Error +// ============================================================================= + +/** + * Catch-all error for unexpected runtime errors in AI operations. + * + * This error is used when an unexpected exception occurs that doesn't fit + * into the other specific error categories. It provides context about where + * the error occurred and preserves the original cause for debugging. + * + * @example + * ```ts + * import { AiError } from "@effect/ai" + * import { Effect } from "effect" + * + * const riskyOperation = () => { + * try { + * // Some operation that might throw + * throw new Error("Unexpected network issue") + * } catch (cause) { + * return Effect.fail(new AiError.UnknownError({ + * module: "ChatService", + * method: "sendMessage", + * description: "An unexpected error occurred during message processing", + * cause + * })) + * } + * } + * + * const program = riskyOperation().pipe( + * Effect.catchTag("UnknownError", (error) => { + * console.log(error.message) + * // "ChatService.sendMessage: An unexpected error occurred during message processing" + * return Effect.succeed("Service temporarily unavailable") + * }) + * ) + * ``` + * + * @since 1.0.0 + * @category Errors + */ +export class UnknownError extends Schema.TaggedError( + "@effect/ai/UnknownError" +)("UnknownError", { + module: Schema.String, + method: Schema.String, + description: Schema.optional(Schema.String), + cause: Schema.optional(Schema.Defect) +}) { + /** + * @since 1.0.0 + */ + readonly [TypeId]: TypeId = TypeId + + /** + * @since 1.0.0 + */ + get message(): string { + const moduleMethod = `${this.module}.${this.method}` + return Predicate.isUndefined(this.description) + ? `${moduleMethod}: An error occurred` + : `${moduleMethod}: ${this.description}` + } +} + +// ============================================================================= +// AiError +// ============================================================================= + +/** + * Union type representing all possible AI operation errors. + * + * This type encompasses all error cases that can occur during AI operations, + * providing a comprehensive error handling surface for applications. + * + * @example + * ```ts + * import { AiError } from "@effect/ai" + * import { Effect, Match } from "effect" + * + * const handleAnyAiError = Match.type().pipe( + * Match.tag("HttpRequestError", (err) => + * `Network error: ${err.reason}` + * ), + * Match.tag("HttpResponseError", (err) => + * `Server error: HTTP ${err.response.status}` + * ), + * Match.tag("MalformedInput", (err) => + * `Invalid input: ${err.description || "Data validation failed"}` + * ), + * Match.tag("MalformedOutput", (err) => + * `Invalid response: ${err.description || "Response parsing failed"}` + * ), + * Match.orElse((err) => + * `Unknown error: ${err.message}` + * ) + * ) + * ``` + * + * @since 1.0.0 + * @category Models + */ +export type AiError = + | HttpRequestError + | HttpResponseError + | MalformedInput + | MalformedOutput + | UnknownError + +/** + * Schema for validating and parsing AI errors. + * + * This schema can be used to decode unknown values into properly typed AI + * errors, ensuring type safety when handling errors from external sources or + * serialized data. + * + * @example + * ```ts + * import { AiError } from "@effect/ai" + * import { Schema, Effect } from "effect" + * + * const parseAiError = (data: unknown) => + * Schema.decodeUnknown(AiError.AiError)(data).pipe( + * Effect.map(error => { + * console.log(`Parsed AI error: ${error._tag}`) + * return error + * }), + * Effect.catchAll(() => + * Effect.succeed(new AiError.UnknownError({ + * module: "Parser", + * method: "parseAiError", + * description: "Failed to parse error data" + * })) + * ) + * ) + * ``` + * + * @since 1.0.0 + * @category Schemas + */ +export const AiError: Schema.Union<[ + typeof HttpRequestError, + typeof HttpResponseError, + typeof MalformedInput, + typeof MalformedOutput, + typeof UnknownError +]> = Schema.Union( + HttpRequestError, + HttpResponseError, + MalformedInput, + MalformedOutput, + UnknownError +) + +// ============================================================================= +// Utilities +// ============================================================================= + +const getStatusCodeSuggestion = (statusCode: number): string => { + if (statusCode >= 400 && statusCode < 500) { + switch (statusCode) { + case 400: + return "Bad Request - Check request parameters, headers, and body format against API documentation." + case 401: + return "Unauthorized - Verify API key, authentication credentials, or token expiration." + case 403: + return "Forbidden - Check API permissions, usage limits, or resource access rights." + case 404: + return "Not Found - Verify the endpoint URL, API version, and resource identifiers." + case 408: + return "Request Timeout - Consider increasing timeout duration or implementing retry logic." + case 422: + return "Unprocessable Entity - Check request data validation, required fields, and data formats." + case 429: + return "Rate Limited - Implement exponential backoff or reduce request frequency." + default: + return "Client error - Review request format, parameters, and API documentation." + } + } else if (statusCode >= 500) { + return "Server error - This is likely temporary. Implement retry logic with exponential backoff." + } else { + return "Check API documentation for this status code." + } +} diff --git a/repos/effect/packages/ai/ai/src/Chat.ts b/repos/effect/packages/ai/ai/src/Chat.ts new file mode 100644 index 0000000..656944a --- /dev/null +++ b/repos/effect/packages/ai/ai/src/Chat.ts @@ -0,0 +1,853 @@ +/** + * The `Chat` module provides a stateful conversation interface for AI language + * models. + * + * This module enables persistent chat sessions that maintain conversation + * history, support tool calling, and offer both streaming and non-streaming + * text generation. It integrates seamlessly with the Effect AI ecosystem, + * providing type-safe conversational AI capabilities. + * + * @example + * ```ts + * import { Chat, LanguageModel } from "@effect/ai" + * import { Effect, Layer } from "effect" + * + * // Create a new chat session + * const program = Effect.gen(function* () { + * const chat = yield* Chat.empty + * + * // Send a message and get response + * const response = yield* chat.generateText({ + * prompt: "Hello! What can you help me with?" + * }) + * + * console.log(response.content) + * + * return response + * }) + * ``` + * + * @example + * ```ts + * import { Chat, LanguageModel } from "@effect/ai" + * import { Effect, Stream } from "effect" + * + * // Streaming chat with tool support + * const streamingChat = Effect.gen(function* () { + * const chat = yield* Chat.empty + * + * yield* chat.streamText({ + * prompt: "Generate a creative story" + * }).pipe(Stream.runForEach((part) => + * Effect.sync(() => console.log(part)) + * )) + * }) + * ``` + * + * @since 1.0.0 + */ +import type { PersistenceBackingError } from "@effect/experimental/Persistence" +import { BackingPersistence } from "@effect/experimental/Persistence" +import * as Channel from "effect/Channel" +import * as Chunk from "effect/Chunk" +import * as Context from "effect/Context" +import * as Duration from "effect/Duration" +import * as Effect from "effect/Effect" +import * as Layer from "effect/Layer" +import * as Option from "effect/Option" +import type { ParseError } from "effect/ParseResult" +import * as Predicate from "effect/Predicate" +import * as Ref from "effect/Ref" +import * as Schema from "effect/Schema" +import * as Stream from "effect/Stream" +import type { NoExcessProperties } from "effect/Types" +import * as AiError from "./AiError.js" +import * as IdGenerator from "./IdGenerator.js" +import * as LanguageModel from "./LanguageModel.js" +import * as Prompt from "./Prompt.js" +import type * as Response from "./Response.js" +import type * as Tool from "./Tool.js" + +/** + * The `Chat` service tag for dependency injection. + * + * This tag provides access to chat functionality throughout your application, + * enabling persistent conversational AI interactions with full context + * management. + * + * @example + * ```ts + * import { Chat } from "@effect/ai" + * import * as Effect from "effect/Effect" + * + * const useChat = Effect.gen(function* () { + * const chat = yield* Chat.Chat + * const response = yield* chat.generateText({ + * prompt: "Explain quantum computing in simple terms" + * }) + * return response.content + * }) + * ``` + * + * @since 1.0.0 + * @category Context + */ +export class Chat extends Context.Tag("@effect/ai/Chat")< + Chat, + Service +>() {} + +/** + * Represents the interface that the `Chat` service provides. + * + * @since 1.0.0 + * @category Models + */ +export interface Service { + /** + * Reference to the chat history. + * + * Provides direct access to the conversation history for advanced use cases + * like custom history manipulation or inspection. + * + * @example + * ```ts + * import { Chat } from "@effect/ai" + * import { Effect, Ref } from "effect" + * + * const inspectHistory = Effect.gen(function* () { + * const chat = yield* Chat.empty + * const currentHistory = yield* Ref.get(chat.history) + * console.log("Current conversation:", currentHistory) + * return currentHistory + * }) + * ``` + */ + readonly history: Ref.Ref + + /** + * Exports the chat history into a structured format. + * + * Returns the complete conversation history as a structured object + * that can be stored, transmitted, or processed by other systems. + * + * @example + * ```ts + * import { Chat } from "@effect/ai" + * import { Effect } from "effect" + * + * const saveChat = Effect.gen(function* () { + * const chat = yield* Chat.empty + * yield* chat.generateText({ prompt: "Hello!" }) + * + * const exportedData = yield* chat.export + * + * // Save to database or file system + * return exportedData + * }) + * ``` + */ + readonly export: Effect.Effect + + /** + * Exports the chat history as a JSON string. + * + * Provides a convenient way to serialize the entire conversation + * for storage or transmission in JSON format. + * + * @example + * ```ts + * import { Chat } from "@effect/ai" + * import { Effect } from "effect" + * + * const backupChat = Effect.gen(function* () { + * const chat = yield* Chat.empty + * yield* chat.generateText({ prompt: "Explain photosynthesis" }) + * + * const jsonBackup = yield* chat.exportJson + * + * yield* Effect.sync(() => + * localStorage.setItem("chat-backup", jsonBackup) + * ) + * + * return jsonBackup + * }) + * ``` + */ + readonly exportJson: Effect.Effect + + /** + * Generate text using a language model for the specified prompt. + * + * If a toolkit is specified, the language model will have access to tools + * for function calling and enhanced capabilities. Both input and output + * messages are automatically added to the chat history. + * + * @example + * ```ts + * import { Chat } from "@effect/ai" + * import { Effect } from "effect" + * + * const chatWithAI = Effect.gen(function* () { + * const chat = yield* Chat.empty + * + * const response1 = yield* chat.generateText({ + * prompt: "What is the capital of France?" + * }) + * + * const response2 = yield* chat.generateText({ + * prompt: "What's the population of that city?", + * }) + * + * return [response1.content, response2.content] + * }) + * ``` + */ + readonly generateText: < + Options extends NoExcessProperties, Options>, + Tools extends Record = {} + >(options: Options & LanguageModel.GenerateTextOptions) => Effect.Effect< + LanguageModel.GenerateTextResponse, + LanguageModel.ExtractError, + LanguageModel.LanguageModel | LanguageModel.ExtractContext + > + + /** + * Generate text using a language model with streaming output. + * + * Returns a stream of response parts that are emitted as soon as they're + * available from the model. Supports tool calling and maintains chat history. + * + * @example + * ```ts + * import { Chat } from "@effect/ai" + * import { Effect, Stream, Console } from "effect" + * + * const streamingChat = Effect.gen(function* () { + * const chat = yield* Chat.empty + * + * const stream = yield* chat.streamText({ + * prompt: "Write a short story about space exploration" + * }) + * + * yield* Stream.runForEach(stream, (part) => + * part.type === "text-delta" + * ? Effect.sync(() => process.stdout.write(part.delta)) + * : Effect.void + * ) + * }) + * ``` + */ + readonly streamText: < + Options extends NoExcessProperties, Options>, + Tools extends Record = {} + >(options: Options & LanguageModel.GenerateTextOptions) => Stream.Stream< + Response.StreamPart, + LanguageModel.ExtractError, + LanguageModel.LanguageModel | LanguageModel.ExtractContext + > + + /** + * Generate a structured object using a language model and schema. + * + * Forces the model to return data that conforms to the specified schema, + * enabling structured data extraction and type-safe responses. The + * conversation history is maintained across calls. + * + * @example + * ```ts + * import { Chat } from "@effect/ai" + * import { Effect, Schema } from "effect" + * + * const ContactSchema = Schema.Struct({ + * name: Schema.String, + * email: Schema.String, + * phone: Schema.optional(Schema.String) + * }) + * + * const extractContact = Effect.gen(function* () { + * const chat = yield* Chat.empty + * + * const contact = yield* chat.generateObject({ + * prompt: "Extract contact info: John Doe, john@example.com, 555-1234", + * schema: ContactSchema + * }) + * + * console.log(contact.object) + * // { name: "John Doe", email: "john@example.com", phone: "555-1234" } + * + * return contact.object + * }) + * ``` + */ + readonly generateObject: < + A, + I extends Record, + R, + Options extends NoExcessProperties, Options>, + Tools extends Record = {} + >(options: Options & LanguageModel.GenerateObjectOptions) => Effect.Effect< + LanguageModel.GenerateObjectResponse, + LanguageModel.ExtractError, + LanguageModel.LanguageModel | R | LanguageModel.ExtractContext + > +} + +// ============================================================================= +// Constructors +// ============================================================================= + +/** + * Creates a new Chat service with empty conversation history. + * + * This is the most common way to start a fresh chat session without + * any initial context or system prompts. + * + * @example + * ```ts + * import { Chat } from "@effect/ai" + * import { Effect } from "effect" + * + * const freshChat = Effect.gen(function* () { + * const chat = yield* Chat.empty + * + * const response = yield* chat.generateText({ + * prompt: "Hello! Can you introduce yourself?" + * }) + * + * console.log(response.content) + * + * return chat + * }) + * ``` + * + * @since 1.0.0 + * @category Constructors + */ +export const empty: Effect.Effect = Effect.gen(function*() { + const history = yield* Ref.make(Prompt.empty) + const context = yield* Effect.context() + const semaphore = yield* Effect.makeSemaphore(1) + + const provideContext = (effect: Effect.Effect): Effect.Effect => + Effect.mapInputContext(effect, (input) => Context.merge(context, input)) + const provideContextStream = (stream: Stream.Stream): Stream.Stream => + Stream.mapInputContext(stream, (input) => Context.merge(context, input)) + + const encodeHistory = Schema.encode(Prompt.Prompt) + const encodeHistoryJson = Schema.encode(Prompt.FromJson) + + return Chat.of({ + history, + export: Ref.get(history).pipe( + Effect.flatMap(encodeHistory), + Effect.catchTag("ParseError", (error) => + AiError.MalformedOutput.fromParseError({ + module: "Chat", + method: "exportJson", + description: "Failed to encode chat history", + error + })), + Effect.withSpan("Chat.export") + ), + exportJson: Ref.get(history).pipe( + Effect.flatMap(encodeHistoryJson), + Effect.catchTag("ParseError", (error) => + AiError.MalformedOutput.fromParseError({ + module: "Chat", + method: "exportJson", + description: "Failed to encode chat history into JSON", + error + })), + Effect.withSpan("Chat.exportJson") + ), + generateText: Effect.fnUntraced( + function*(options) { + const newPrompt = Prompt.make(options.prompt) + const oldPrompt = yield* Ref.get(history) + const prompt = Prompt.merge(oldPrompt, newPrompt) + + const response = yield* LanguageModel.generateText({ ...options, prompt }) + + const newHistory = Prompt.merge(prompt, Prompt.fromResponseParts(response.content)) + yield* Ref.set(history, newHistory) + + return response + }, + provideContext, + semaphore.withPermits(1), + Effect.withSpan("Chat.generateText", { captureStackTrace: false }) + ), + streamText: Effect.fnUntraced( + function*(options) { + let parts = Chunk.empty() + return Stream.fromChannel(Channel.acquireUseRelease( + semaphore.take(1).pipe( + Effect.zipRight(Ref.get(history)), + Effect.map((history) => Prompt.merge(history, Prompt.make(options.prompt))) + ), + (prompt) => + LanguageModel.streamText({ ...options, prompt }).pipe( + Stream.mapChunks((chunk) => { + parts = Chunk.appendAll(parts, chunk) + return chunk + }), + Stream.toChannel + ), + (prompt) => + Effect.zipRight( + Ref.set( + history, + Prompt.merge(prompt, Prompt.fromResponseParts(Array.from(parts))) + ), + semaphore.release(1) + ) + )).pipe( + provideContextStream, + Stream.withSpan("Chat.streamText", { + captureStackTrace: false + }) + ) + }, + Stream.unwrap + ), + generateObject: Effect.fnUntraced( + function*(options) { + const newPrompt = Prompt.make(options.prompt) + const oldPrompt = yield* Ref.get(history) + const prompt = Prompt.merge(oldPrompt, newPrompt) + + const response = yield* LanguageModel.generateObject({ ...options, prompt }) + + const newHistory = Prompt.merge(prompt, Prompt.fromResponseParts(response.content)) + yield* Ref.set(history, newHistory) + + return response + }, + provideContext, + semaphore.withPermits(1), + (effect, options) => + Effect.withSpan(effect, "Chat.generateObject", { + attributes: { + objectName: "objectName" in options + ? options.objectName + : "_tag" in options.schema + ? options.schema._tag + : (options.schema as any).identifier ?? "generateObject" + }, + captureStackTrace: false + }) + ) + }) +}) + +/** + * Creates a new Chat service from an initial prompt. + * + * This is the primary constructor for creating chat instances. It initializes + * a new conversation with the provided prompt as the starting context. + * + * @example + * ```ts + * import { Chat, Prompt } from "@effect/ai" + * import { Effect } from "effect" + * + * const chatWithSystemPrompt = Effect.gen(function* () { + * const chat = yield* Chat.fromPrompt([{ + * role: "system", + * content: "You are a helpful assistant specialized in mathematics." + * }]) + * + * const response = yield* chat.generateText({ + * prompt: "What is 2+2?" + * }) + * + * return response.content + * }) + * ``` + * + * @example + * ```ts + * import { Chat, Prompt } from "@effect/ai" + * import { Effect } from "effect" + * + * // Initialize with conversation history + * const existingChat = Effect.gen(function* () { + * const chat = yield* Chat.fromPrompt([ + * { role: "user", content: [{ type: "text", text: "What's the weather like?" }] }, + * { role: "assistant", content: [{ type: "text", text: "I don't have access to weather data." }] }, + * { role: "user", content: [{ type: "text", text: "Can you help me with coding?" }] } + * ]) + * + * const response = yield* chat.generateText({ + * prompt: "I need help with TypeScript" + * }) + * + * return response + * }) + * ``` + * + * @since 1.0.0 + * @category Constructors + */ +export const fromPrompt = Effect.fnUntraced( + function*(prompt: Prompt.RawInput) { + const chat = yield* empty + yield* Ref.set(chat.history, Prompt.make(prompt)) + return chat + } +) + +const decodeUnknown = Schema.decodeUnknown(Prompt.Prompt) + +/** + * Creates a Chat service from previously exported chat data. + * + * Restores a chat session from structured data that was previously exported + * using the `export` method. Useful for persisting and restoring conversation + * state. + * + * @example + * ```ts + * import { Chat } from "@effect/ai" + * import { Effect } from "effect" + * + * declare const loadFromDatabase: (sessionId: string) => Effect.Effect + * + * const restoreChat = Effect.gen(function* () { + * // Assume we have previously exported data + * const savedData = yield* loadFromDatabase("chat-session-123") + * + * const restoredChat = yield* Chat.fromExport(savedData) + * + * // Continue the conversation from where it left off + * const response = yield* restoredChat.generateText({ + * prompt: "Let's continue our discussion" + * }) + * }).pipe( + * Effect.catchTag("ParseError", (error) => { + * console.log("Failed to restore chat:", error.message) + * return Effect.void + * }) + * ) + * ``` + * + * @since 1.0.0 + * @category Constructors + */ +export const fromExport = (data: unknown): Effect.Effect => + Effect.flatMap(decodeUnknown(data), fromPrompt) + +const decodeHistoryJson = Schema.decodeUnknown(Prompt.FromJson) + +/** + * Creates a Chat service from previously exported JSON chat data. + * + * Restores a chat session from JSON string that was previously exported + * using the `exportJson` method. This is the most convenient way to + * persist and restore chat sessions to/from storage systems. + * + * @example + * ```ts + * import { Chat } from "@effect/ai" + * import { Effect } from "effect" + * + * const restoreFromJson = Effect.gen(function* () { + * // Load JSON from localStorage or file system + * const jsonData = localStorage.getItem("my-chat-backup") + * if (!jsonData) return yield* Chat.empty + * + * const restoredChat = yield* Chat.fromJson(jsonData) + * + * // Chat history is now restored + * const response = yield* restoredChat.generateText({ + * prompt: "What were we talking about?" + * }) + * + * return response + * }).pipe( + * Effect.catchTag("ParseError", (error) => { + * console.log("Invalid JSON format:", error.message) + * return Chat.empty // Fallback to empty chat + * }) + * ) + * ``` + * + * @since 1.0.0 + * @category Constructors + */ +export const fromJson = (data: string): Effect.Effect => + Effect.flatMap(decodeHistoryJson(data), fromPrompt) + +// ============================================================================= +// Chat Persistence +// ============================================================================= + +/** + * An error that occurs when attempting to retrieve a persisted `Chat` that + * does not exist in the backing persistence store. + * + * @since 1.0.0 + * @category Errors + */ +export class ChatNotFoundError extends Schema.TaggedError( + "@effect/ai/Chat/ChatNotFoundError" +)("ChatNotFoundError", { chatId: Schema.String }) {} + +// @effect-diagnostics effect/leakingRequirements:off +/** + * The context tag for chat persistence. + * + * @since 1.0.0 + * @category Context + */ +export class Persistence extends Context.Tag("@effect/ai/Chat/Persisted")< + Persistence, + Persistence.Service +>() {} + +/** + * @since 1.0.0 + * @category Models + */ +export declare namespace Persistence { + /** + * Represents the backing persistence for a persisted `Chat`. Allows for + * creating and retrieving chats that have been saved to a persistence store. + * + * @since 1.0.0 + * @category Models + */ + export interface Service { + /** + * Attempts to retrieve the persisted chat from the backing persistence + * store with the specified chat identifer. If the chat does not exist in + * the persistence store, a `ChatNotFoundError` will be returned. + */ + readonly get: (chatId: string, options?: { + readonly timeToLive?: Duration.DurationInput | undefined + }) => Effect.Effect< + Persisted, + ChatNotFoundError | PersistenceBackingError + > + + /** + * Attempts to retrieve the persisted chat from the backing persistence + * store with the specified chat identifer. If the chat does not exist in + * the persistence store, an empty chat will be created, saved, and + * returned. + */ + readonly getOrCreate: (chatId: string, options?: { + readonly timeToLive?: Duration.DurationInput | undefined + }) => Effect.Effect< + Persisted, + AiError.MalformedOutput | PersistenceBackingError + > + } +} + +/** + * Represents a `Chat` that is backed by persistence. + * + * When calling a text generation method (e.g. `generateText`), the previous + * chat history as well as the relevent response parts will be saved to the + * backing persistence store. + * + * @since 1.0.0 + * @category Models + */ +export interface Persisted extends Service { + /** + * The identifier for the chat in the backing persistence store. + */ + readonly id: string + + /** + * Saves the current chat history into the backing persistence store. + */ + readonly save: Effect.Effect +} + +/** + * Creates a new chat persistence service. + * + * The provided store identifier will be used to indicate which "store" the + * backing persistence should load chats from. + * + * @since 1.0.0 + * @category Constructors + */ +export const makePersisted = Effect.fnUntraced(function*(options: { + readonly storeId: string +}) { + const persistence = yield* BackingPersistence + const store = yield* persistence.make(options.storeId) + + const toPersisted = Effect.fnUntraced( + function*(chatId: string, chat: Service, ttl: Option.Option) { + const idGenerator = yield* Effect.serviceOption(IdGenerator.IdGenerator).pipe( + Effect.map(Option.getOrElse(() => IdGenerator.defaultIdGenerator)) + ) + + const saveChat = Effect.fnUntraced( + function*(prevHistory: Prompt.Prompt) { + // Get the current chat history + const history = yield* Ref.get(chat.history) + // Get the most recent message stored in the previous chat history + const lastMessage = prevHistory.content[prevHistory.content.length - 1] + // Determine the correct message identifier to use: + let messageId: string | undefined = undefined + // If the most recent message in the chat history is an assistant message, + // use the message identifer stored in that message + if (Predicate.isNotUndefined(lastMessage) && lastMessage.role === "assistant") { + messageId = lastMessage.options[Persistence.key]?.messageId as string | undefined + } + // If the chat history is empty or a message identifier did not exist on + // the most recent message in the chat history, generate a new identifier + if (Predicate.isUndefined(messageId)) { + messageId = yield* idGenerator.generateId() + } + // Mutate the new messages to add the generated message identifier + for (let i = prevHistory.content.length; i < history.content.length; i++) { + const message = history.content[i] + ;(message.options as any)[Persistence.key] = { messageId } + } + // Save the mutated history back to the ref + yield* Ref.set(chat.history, history) + // Export the chat to JSON + const json = yield* chat.exportJson + // Save the chat to the backing store + yield* store.set(chatId, json, ttl) + }, + Effect.catchTag("PersistenceError", (error) => { + // Should never happen because we are using the backing persistence + // store directly, and parse errors can only occur when using result + // persistence + if (error.reason === "ParseError") return Effect.die(error) + return Effect.fail(error) + }) + ) + + const persisted: Persisted = { + ...chat, + id: chatId, + save: Effect.flatMap(Ref.get(chat.history), saveChat), + generateText: Effect.fnUntraced(function*(options) { + const history = yield* Ref.get(chat.history) + return yield* chat.generateText(options).pipe( + Effect.ensuring(Effect.orDie(saveChat(history))) + ) + }), + generateObject: Effect.fnUntraced(function*(options) { + const history = yield* Ref.get(chat.history) + return yield* chat.generateObject(options).pipe( + Effect.ensuring(Effect.orDie(saveChat(history))) + ) + }), + streamText: Effect.fnUntraced(function*(options) { + const history = yield* Ref.get(chat.history) + const stream = chat.streamText(options).pipe( + Stream.ensuring(Effect.orDie(saveChat(history))) + ) + return stream + }, Stream.unwrap) + } + + return persisted + } + ) + + const createChat = Effect.fnUntraced( + function*(chatId: string, ttl: Option.Option) { + // Create an empty chat + const chat = yield* empty + + // Save the history for the newly created chat + const history = yield* chat.exportJson + yield* store.set(chatId, history, ttl) + + // Convert the chat to a persisted chat + return yield* toPersisted(chatId, chat, ttl) + }, + Effect.catchTag("PersistenceError", (error) => { + // Should never happen because we are using the backing persistence + // store directly, and parse errors can only occur when using result + // persistence + if (error.reason === "ParseError") return Effect.die(error) + return Effect.fail(error) + }) + ) + + const getChat = Effect.fnUntraced( + function*(chatId: string, ttl: Option.Option) { + // Create an empty chat + const chat = yield* empty + + // Hydrate the chat history + yield* store.get(chatId).pipe( + Effect.flatMap(Effect.transposeMapOption(decodeHistoryJson)), + Effect.flatten, + Effect.catchTag("NoSuchElementException", () => new ChatNotFoundError({ chatId })), + Effect.flatMap((history) => Ref.set(chat.history, history)) + ) + + // Convert the chat to a persisted chat + return yield* toPersisted(chatId, chat, ttl) + }, + Effect.catchTags({ + ParseError: (error) => Effect.die(error), + PersistenceError: (error) => { + // Should never happen because we are using the backing persistence + // store directly, and parse errors can only occur when using result + // persistence + if (error.reason === "ParseError") return Effect.die(error) + return Effect.fail(error) + } + }) + ) + + const makeTTL = ( + timeToLive?: Duration.DurationInput | undefined + ): Option.Option => + Option.fromNullable(timeToLive).pipe( + Option.map(Duration.decode) + ) + + const get = Effect.fn("PersistedChat.get")( + function*(chatId: string, options?: { + readonly timeToLive?: Duration.DurationInput | undefined + }) { + const ttl = makeTTL(options?.timeToLive) + return yield* getChat(chatId, ttl) + } + ) + + const getOrCreate = Effect.fn("PersistedChat.getOrCreate")( + function*(chatId: string, options?: { + readonly timeToLive?: Duration.DurationInput | undefined + }) { + const ttl = makeTTL(options?.timeToLive) + return yield* getChat(chatId, ttl).pipe( + Effect.catchTag("ChatNotFoundError", () => createChat(chatId, ttl)) + ) + } + ) + + return Persistence.of({ + get, + getOrCreate + }) +}) + +/** + * Creates a `Layer` new chat persistence service. + * + * The provided store identifier will be used to indicate which "store" the + * backing persistence should load chats from. + * + * @since 1.0.0 + * @category Constructors + */ +export const layerPersisted = (options: { + readonly storeId: string +}): Layer.Layer => Layer.scoped(Persistence, makePersisted(options)) diff --git a/repos/effect/packages/ai/ai/src/EmbeddingModel.ts b/repos/effect/packages/ai/ai/src/EmbeddingModel.ts new file mode 100644 index 0000000..8054a7b --- /dev/null +++ b/repos/effect/packages/ai/ai/src/EmbeddingModel.ts @@ -0,0 +1,318 @@ +/** + * The `EmbeddingModel` module provides vector embeddings for text using AI + * models. + * + * This module enables efficient conversion of text into high-dimensional vector + * representations that capture semantic meaning. It supports batching, caching, + * and request optimization for production use cases like semantic search, + * document similarity, and clustering. + * + * @example + * ```ts + * import { EmbeddingModel } from "@effect/ai" + * import { Effect } from "effect" + * + * // Basic embedding usage + * const program = Effect.gen(function* () { + * const embedding = yield* EmbeddingModel.EmbeddingModel + * + * const vector = yield* embedding.embed("Hello world!") + * console.log(vector) // [0.123, -0.456, 0.789, ...] + * + * return vector + * }) + * ``` + * + * @example + * ```ts + * import { EmbeddingModel } from "@effect/ai" + * import { Effect, Duration } from "effect" + * + * declare const generateVectorFor: (text: string) => Array + * + * // Create embedding service with batching and caching + * const embeddingService = EmbeddingModel.make({ + * embedMany: (texts) => Effect.succeed( + * texts.map((text, index) => ({ + * index, + * embeddings: generateVectorFor(text) + * })) + * ), + * maxBatchSize: 50, + * cache: { + * capacity: 1000, + * timeToLive: Duration.minutes(30) + * } + * }) + * ``` + * + * @since 1.0.0 + */ +import { dataLoader } from "@effect/experimental/RequestResolver" +import * as Context from "effect/Context" +import type * as Duration from "effect/Duration" +import * as Effect from "effect/Effect" +import { identity } from "effect/Function" +import * as Option from "effect/Option" +import * as Request from "effect/Request" +import * as RequestResolver from "effect/RequestResolver" +import * as Schema from "effect/Schema" +import type * as Types from "effect/Types" +import * as AiError from "./AiError.js" + +/** + * The `EmbeddingModel` service tag for dependency injection. + * + * This tag provides access to vector embedding functionality throughout your application, + * enabling conversion of text to high-dimensional vectors for semantic analysis. + * + * @example + * ```ts + * import { EmbeddingModel } from "@effect/ai" + * import * as Effect from "effect/Effect" + * + * const cosineSimilarity = (a: ReadonlyArray, b: ReadonlyArray): number => { + * const dot = a.reduce((sum, ai, i) => sum + ai * (b[i] ?? 0), 0) + * const normA = Math.sqrt(a.reduce((sum, ai) => sum + ai * ai, 0)) + * const normB = Math.sqrt(b.reduce((sum, bi) => sum + bi * bi, 0)) + * return normA === 0 || normB === 0 ? 0 : dot / (normA * normB) + * } + * + * const useEmbeddings = Effect.gen(function* () { + * const embedder = yield* EmbeddingModel.EmbeddingModel + * + * const documentVector = yield* embedder.embed("This is a sample document") + * const queryVector = yield* embedder.embed("sample query") + * + * const similarity = cosineSimilarity(documentVector, queryVector) + * return similarity + * }) + * ``` + * + * @since 1.0.0 + * @category Context + */ +export class EmbeddingModel extends Context.Tag("@effect/ai/EmbeddingModel")< + EmbeddingModel, + Service +>() {} + +/** + * The service interface for vector embedding operations. + * + * Defines the contract that all embedding model implementations must fulfill. + * The service provides text-to-vector conversion functionality. + * + * @since 1.0.0 + * @category Models + */ +export interface Service { + /** + * Converts a text string into a vector embedding. + */ + readonly embed: (input: string) => Effect.Effect, AiError.AiError> + /** + * Converts a batch of text strings into a chunk of vector embeddings. + */ + readonly embedMany: (input: ReadonlyArray, options?: { + /** + * The concurrency level to use while batching requests. + */ + readonly concurrency?: Types.Concurrency | undefined + }) => Effect.Effect>, AiError.AiError> +} + +/** + * Represents the result of a batch embedding operation. + * + * Used internally by the batching system to associate embeddings with their + * original request positions in the batch. + * + * @example + * ```ts + * import { EmbeddingModel } from "@effect/ai" + * + * const batchResults: EmbeddingModel.Result[] = [ + * { index: 0, embeddings: [0.1, 0.2, 0.3] }, + * { index: 1, embeddings: [0.4, 0.5, 0.6] }, + * { index: 2, embeddings: [0.7, 0.8, 0.9] } + * ] + * + * // Results correspond to input texts at positions 0, 1, 2 + * ``` + * + * @since 1.0.0 + * @category Models + */ +export interface Result { + /** + * The position index of this result in the original batch request. + */ + readonly index: number + + /** + * The vector embedding for the text at this index. + */ + readonly embeddings: Array +} + +class EmbeddingRequest extends Schema.TaggedRequest( + "@effect/ai/EmbeddingModel/Request" +)("EmbeddingRequest", { + failure: AiError.AiError, + success: Schema.mutable(Schema.Array(Schema.Number)), + payload: { input: Schema.String } +}) {} + +const makeBatchedResolver = ( + embedMany: (input: ReadonlyArray) => Effect.Effect, AiError.AiError> +) => + RequestResolver.makeBatched( + (requests: ReadonlyArray) => + embedMany(requests.map((request) => request.input)).pipe( + Effect.flatMap( + Effect.forEach( + ({ embeddings, index }) => Request.succeed(requests[index], embeddings), + { discard: true } + ) + ), + Effect.catchAll((error) => + Effect.forEach( + requests, + (request) => Request.fail(request, error), + { discard: true } + ) + ) + ) + ) + +/** + * Creates an EmbeddingModel service with batching and caching capabilities. + * + * This is the primary constructor for creating embedding services. It supports + * automatic batching of requests for efficiency and optional caching to reduce + * redundant API calls. + * + * @since 1.0.0 + * @category Constructors + */ +export const make = (options: { + /** + * A method which processes a batch of text inputs and returns embedding + * results. + */ + readonly embedMany: (input: ReadonlyArray) => Effect.Effect, AiError.AiError> + /** + * Optional maximum number of text inputs to process in one batch. + */ + readonly maxBatchSize?: number + /** + * Optional configuration to control how batch request results are cached. + */ + readonly cache?: { + /** + * The capacity of the cache. + */ + readonly capacity: number + /** + * The time-to-live for items in the cache. + */ + readonly timeToLive: Duration.DurationInput + } +}) => + Effect.gen(function*() { + const cache = yield* Option.fromNullable(options.cache).pipe( + Effect.flatMap((config) => Request.makeCache(config)), + Effect.optionFromOptional + ) + + const resolver = makeBatchedResolver(options.embedMany).pipe( + options.maxBatchSize ? RequestResolver.batchN(options.maxBatchSize) : identity + ) + + const embed = (input: string) => { + const request = Effect.request(new EmbeddingRequest({ input }), resolver) + return Option.match(cache, { + onNone: () => request, + onSome: (cache) => + request.pipe( + Effect.withRequestCaching(true), + Effect.withRequestCache(cache) + ) + }) + } + + const embedMany = (inputs: ReadonlyArray, options?: { + readonly concurrency?: Types.Concurrency | undefined + }) => + Effect.forEach(inputs, embed, { + batching: true, + concurrency: options?.concurrency + }) + + return EmbeddingModel.of({ + embed: (input) => + embed(input).pipe( + Effect.withSpan("EmbeddingModel.embed", { captureStackTrace: false }) + ), + embedMany: (inputs) => + embedMany(inputs).pipe( + Effect.withSpan("EmbeddingModel.embedMany", { captureStackTrace: false }) + ) + }) + }) + +/** + * Creates an EmbeddingModel service with time-window based batching. + * + * This constructor creates a service that uses a data loader pattern to batch + * embedding requests within a specified time window. This is optimal for + * high-throughput scenarios where you want to automatically batch requests that + * arrive within a short time period. + * + * @since 1.0.0 + * @category Constructors + */ +export const makeDataLoader = (options: { + /** + * A method which processes a batch of text inputs and returns embedding + * results. + */ + readonly embedMany: (input: ReadonlyArray) => Effect.Effect, AiError.AiError> + /** + * The duration between batch requests during which requests are collected and + * added to the current batch. + */ + readonly window: Duration.DurationInput + /** + * Optional maximum number of requests to add to the batch before a batch + * request must be sent. + */ + readonly maxBatchSize?: number +}) => + Effect.gen(function*() { + const resolver = makeBatchedResolver(options.embedMany) + const resolverDelayed = yield* dataLoader(resolver, { + window: options.window, + maxBatchSize: options.maxBatchSize + }) + + function embed(input: string) { + return Effect.request(new EmbeddingRequest({ input }), resolverDelayed).pipe( + Effect.withSpan("EmbeddingModel.embed", { captureStackTrace: false }) + ) + } + + function embedMany(inputs: ReadonlyArray, options?: { + readonly concurrency?: Types.Concurrency | undefined + }) { + return Effect.forEach(inputs, embed, { batching: true, concurrency: options?.concurrency }).pipe( + Effect.withSpan("EmbeddingModel.embedMany", { captureStackTrace: false }) + ) + } + + return EmbeddingModel.of({ + embed, + embedMany + }) + }) diff --git a/repos/effect/packages/ai/ai/src/IdGenerator.ts b/repos/effect/packages/ai/ai/src/IdGenerator.ts new file mode 100644 index 0000000..bef2b66 --- /dev/null +++ b/repos/effect/packages/ai/ai/src/IdGenerator.ts @@ -0,0 +1,320 @@ +/** + * The `IdGenerator` module provides a pluggable system for generating unique identifiers + * for tool calls and other items in the Effect AI SDKs. + * + * This module offers a flexible and configurable approach to ID generation, supporting + * custom alphabets, prefixes, separators, and sizes. + * + * @example + * ```ts + * import { IdGenerator } from "@effect/ai" + * import { Effect, Layer } from "effect" + * + * // Using the default ID generator + * const program = Effect.gen(function* () { + * const idGen = yield* IdGenerator.IdGenerator + * const toolCallId = yield* idGen.generateId() + * console.log(toolCallId) // "id_A7xK9mP2qR5tY8uV" + * return toolCallId + * }).pipe( + * Effect.provide(Layer.succeed( + * IdGenerator.IdGenerator, + * IdGenerator.defaultIdGenerator + * )) + * ) + * ``` + * + * @example + * ```ts + * import { IdGenerator } from "@effect/ai" + * import { Effect, Layer } from "effect" + * + * // Creating a custom ID generator for AI tool calls + * const customLayer = IdGenerator.layer({ + * alphabet: "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789", + * prefix: "tool_call", + * separator: "-", + * size: 12 + * }) + * + * const program = Effect.gen(function* () { + * const idGen = yield* IdGenerator.IdGenerator + * const id = yield* idGen.generateId() + * console.log(id) // "tool_call-A7XK9MP2QR5T" + * return id + * }).pipe( + * Effect.provide(customLayer) + * ) + * ``` + * + * @since 1.0.0 + */ +import * as Cause from "effect/Cause" +import * as Context from "effect/Context" +import * as Effect from "effect/Effect" +import * as Layer from "effect/Layer" +import * as Predicate from "effect/Predicate" +import * as Random from "effect/Random" + +/** + * The `IdGenerator` service tag for dependency injection. + * + * This tag is used to provide and access ID generation functionality throughout + * the application. It follows Effect's standard service pattern for type-safe + * dependency injection. + * + * @example + * ```ts + * import { IdGenerator } from "@effect/ai" + * import { Effect } from "effect" + * + * const useIdGenerator = Effect.gen(function* () { + * const idGenerator = yield* IdGenerator.IdGenerator + * const newId = yield* idGenerator.generateId() + * return newId + * }) + * ``` + * + * @since 1.0.0 + * @category Models + */ +export class IdGenerator extends Context.Tag("@effect/ai/IdGenerator")< + IdGenerator, + Service +>() {} + +/** + * The service interface for ID generation. + * + * Defines the contract that all ID generator implementations must fulfill. + * The service provides a single method for generating unique identifiers + * in an effectful context. + * + * @example + * ```ts + * import { IdGenerator } from "@effect/ai" + * import { Effect } from "effect" + * + * // Custom implementation + * const customService: IdGenerator.Service = { + * generateId: () => Effect.succeed(`custom_${Date.now()}`) + * } + * + * const program = Effect.gen(function* () { + * const id = yield* customService.generateId() + * console.log(id) // "custom_1234567890" + * return id + * }) + * ``` + * + * @since 1.0.0 + * @category Models + */ +export interface Service { + readonly generateId: () => Effect.Effect +} + +/** + * Configuration options for creating custom ID generators. + * + * @example + * ```ts + * import { IdGenerator } from "@effect/ai" + * + * // Configuration for tool call IDs + * const toolCallOptions: IdGenerator.MakeOptions = { + * alphabet: "0123456789ABCDEF", + * prefix: "tool", + * separator: "_", + * size: 8 + * } + * + * // This will generate IDs like: "tool_A1B2C3D4" + * ``` + * + * @since 1.0.0 + * @category Models + */ +export interface MakeOptions { + /** + * The character set to use for generating the random portion of IDs. + */ + readonly alphabet: string + /** + * Optional prefix to prepend to generated IDs. + */ + readonly prefix?: string | undefined + /** + * Character used to separate the prefix from the random portion. + */ + readonly separator: string + /** + * Length of the random portion of the generated ID. + */ + readonly size: number +} + +const DEFAULT_ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" +const DEFAULT_SEPARATOR = "_" +const DEFAULT_SIZE = 16 + +const makeGenerator = ({ + alphabet = DEFAULT_ALPHABET, + prefix, + separator = DEFAULT_SEPARATOR, + size = DEFAULT_SIZE +}: Partial) => { + const alphabetLength = alphabet.length + return Effect.fnUntraced(function*() { + const chars = new Array(size) + for (let i = 0; i < size; i++) { + chars[i] = alphabet[((yield* Random.next) * alphabetLength) | 0] + } + const identifier = chars.join("") + if (Predicate.isUndefined(prefix)) { + return identifier + } + return `${prefix}${separator}${identifier}` + }) +} + +/** + * Default ID generator service implementation. + * + * Uses the standard configuration with "id" prefix and generates IDs in the + * format "id_XXXXXXXXXXXXXXXX" where X represents random alphanumeric + * characters. + * + * @example + * ```ts + * import { IdGenerator } from "@effect/ai" + * import { Effect, Layer } from "effect" + * + * const program = Effect.gen(function* () { + * const id = yield* IdGenerator.defaultIdGenerator.generateId() + * console.log(id) // "id_A7xK9mP2qR5tY8uV" + * return id + * }) + * + * // Or provide it as a service + * const withDefault = program.pipe( + * Effect.provideService( + * IdGenerator.IdGenerator, + * IdGenerator.defaultIdGenerator + * ) + * ) + * ``` + * + * @since 1.0.0 + * @category Constructors + */ +export const defaultIdGenerator: Service = { + generateId: makeGenerator({ prefix: "id" }) +} + +/** + * Creates a custom ID generator service with the specified options. + * + * Validates the configuration to ensure the separator is not part of the + * alphabet, which would cause ambiguity in parsing generated IDs. + * + * @example + * ```ts + * import { IdGenerator } from "@effect/ai" + * import { Effect } from "effect" + * + * const program = Effect.gen(function* () { + * // Create a generator for AI assistant message IDs + * const messageIdGen = yield* IdGenerator.make({ + * alphabet: "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ", + * prefix: "msg", + * separator: "-", + * size: 10 + * }) + * + * const messageId = yield* messageIdGen.generateId() + * console.log(messageId) // "msg-A7X9K2M5P8" + * return messageId + * }) + * ``` + * + * @example + * ```ts + * import { IdGenerator } from "@effect/ai" + * import { Effect } from "effect" + * + * // This will fail with IllegalArgumentException + * const invalidConfig = IdGenerator.make({ + * alphabet: "ABC123", + * prefix: "test", + * separator: "A", // Error: separator is part of alphabet + * size: 8 + * }) + * + * const program = Effect.gen(function* () { + * const generator = yield* invalidConfig + * return generator + * }).pipe( + * Effect.catchAll(error => + * Effect.succeed(`Configuration error: ${error.message}`) + * ) + * ) + * ``` + * + * @since 1.0.0 + * @category Constructors + */ +export const make = Effect.fnUntraced(function*({ + alphabet = DEFAULT_ALPHABET, + prefix, + separator = DEFAULT_SEPARATOR, + size = DEFAULT_SIZE +}: MakeOptions) { + if (alphabet.includes(separator)) { + const message = `The separator "${separator}" must not be part of the alphabet "${alphabet}".` + return yield* new Cause.IllegalArgumentException(message) + } + + const generateId = makeGenerator({ alphabet, prefix, separator, size }) + + return { + generateId + } as const +}) + +/** + * Creates a Layer that provides the IdGenerator service with custom + * configuration. + * + * This is the recommended way to provide ID generation capabilities to your + * application. The layer will fail during construction if the configuration is + * invalid. + * + * @example + * ```ts + * import { IdGenerator } from "@effect/ai" + * import { Effect, Layer } from "effect" + * + * // Create a layer for generating AI tool call IDs + * const toolCallIdLayer = IdGenerator.layer({ + * alphabet: "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ", + * prefix: "tool_call", + * separator: "_", + * size: 12 + * }) + * + * const program = Effect.gen(function* () { + * const idGen = yield* IdGenerator.IdGenerator + * const toolCallId = yield* idGen.generateId() + * console.log(toolCallId) // "tool_call_A7XK9MP2QR5T" + * return toolCallId + * }).pipe( + * Effect.provide(toolCallIdLayer) + * ) + * ``` + * + * @since 1.0.0 + * @category Constructors + */ +export const layer = (options: MakeOptions): Layer.Layer => + Layer.effect(IdGenerator, make(options)) diff --git a/repos/effect/packages/ai/ai/src/LanguageModel.ts b/repos/effect/packages/ai/ai/src/LanguageModel.ts new file mode 100644 index 0000000..c95c460 --- /dev/null +++ b/repos/effect/packages/ai/ai/src/LanguageModel.ts @@ -0,0 +1,1095 @@ +/** + * The `LanguageModel` module provides AI text generation capabilities with tool + * calling support. + * + * This module offers a comprehensive interface for interacting with large + * language models, supporting both streaming and non-streaming text generation, + * structured output generation, and tool calling functionality. It provides a + * unified API that can be implemented by different AI providers while + * maintaining type safety and effect management. + * + * @example + * ```ts + * import { LanguageModel } from "@effect/ai" + * import { Effect } from "effect" + * + * // Basic text generation + * const program = Effect.gen(function* () { + * const response = yield* LanguageModel.generateText({ + * prompt: "Explain quantum computing" + * }) + * + * console.log(response.text) + * + * return response + * }) + * ``` + * + * @example + * ```ts + * import { LanguageModel } from "@effect/ai" + * import { Effect, Schema } from "effect" + * + * // Structured output generation + * const ContactSchema = Schema.Struct({ + * name: Schema.String, + * email: Schema.String + * }) + * + * const extractContact = Effect.gen(function* () { + * const response = yield* LanguageModel.generateObject({ + * prompt: "Extract contact: John Doe, john@example.com", + * schema: ContactSchema + * }) + * + * return response.value + * }) + * ``` + * + * @since 1.0.0 + */ +import * as Chunk from "effect/Chunk" +import * as Context from "effect/Context" +import * as Effect from "effect/Effect" +import * as Mailbox from "effect/Mailbox" +import * as Option from "effect/Option" +import * as ParseResult from "effect/ParseResult" +import * as Predicate from "effect/Predicate" +import * as Schema from "effect/Schema" +import type * as Scope from "effect/Scope" +import * as Stream from "effect/Stream" +import type { Span } from "effect/Tracer" +import type { Concurrency, Mutable, NoExcessProperties } from "effect/Types" +import * as AiError from "./AiError.js" +import { defaultIdGenerator, IdGenerator } from "./IdGenerator.js" +import * as Prompt from "./Prompt.js" +import * as Response from "./Response.js" +import type { SpanTransformer } from "./Telemetry.js" +import { CurrentSpanTransformer } from "./Telemetry.js" +import type * as Tool from "./Tool.js" +import * as Toolkit from "./Toolkit.js" + +// ============================================================================= +// Service Definition +// ============================================================================= + +/** + * The `LanguageModel` service tag for dependency injection. + * + * This tag provides access to language model functionality throughout your + * application, enabling text generation, streaming, and structured output + * capabilities. + * + * @example + * ```ts + * import { LanguageModel } from "@effect/ai" + * import * as Effect from "effect/Effect" + * + * const useLanguageModel = Effect.gen(function* () { + * const model = yield* LanguageModel.LanguageModel + * const response = yield* model.generateText({ + * prompt: "What is machine learning?" + * }) + * return response.text + * }) + * ``` + * + * @since 1.0.0 + * @category Context + */ +export class LanguageModel extends Context.Tag("@effect/ai/LanguageModel")< + LanguageModel, + Service +>() {} + +/** + * The service interface for language model operations. + * + * Defines the contract that all language model implementations must fulfill, + * providing text generation, structured output, and streaming capabilities. + * + * @since 1.0.0 + * @category Models + */ +export interface Service { + /** + * Generate text using the language model. + */ + readonly generateText: < + Options extends NoExcessProperties, Options>, + Tools extends Record = {} + >(options: Options & GenerateTextOptions) => Effect.Effect< + GenerateTextResponse, + ExtractError, + ExtractContext + > + + /** + * Generate a structured object from a schema using the language model. + */ + readonly generateObject: < + A, + I extends Record, + R, + Options extends NoExcessProperties, Options>, + Tools extends Record = {} + >(options: Options & GenerateObjectOptions) => Effect.Effect< + GenerateObjectResponse, + ExtractError, + R | ExtractContext + > + + /** + * Generate text using the language model with streaming output. + */ + readonly streamText: < + Options extends NoExcessProperties, Options>, + Tools extends Record = {} + >(options: Options & GenerateTextOptions) => Stream.Stream< + Response.StreamPart, + ExtractError, + ExtractContext + > +} + +/** + * Configuration options for text generation. + * + * @since 1.0.0 + * @category Models + */ +export interface GenerateTextOptions> { + /** + * The prompt input to use to generate text. + */ + readonly prompt: Prompt.RawInput + + /** + * A toolkit containing both the tools and the tool call handler to use to + * augment text generation. + */ + readonly toolkit?: Toolkit.WithHandler | Effect.Effect, any, any> | undefined + + /** + * The tool choice mode for the language model. + * - `auto` (default): The model can decide whether or not to call tools, as + * well as which tools to call. + * - `required`: The model **must** call a tool but can decide which tool will + * be called. + * - `none`: The model **must not** call a tool. + * - `{ tool: }`: The model must call the specified tool. + * - `{ mode?: "auto" (default) | "required", "oneOf": [] }`: The + * model is restricted to the subset of tools specified by `oneOf`. When + * `mode` is `"auto"` or omitted, the model can decide whether or not a tool + * from the allowed subset of tools can be called. When `mode` is + * `"required"`, the model **must** call one tool from the allowed subset of + * tools. + */ + readonly toolChoice?: + | ToolChoice<{ [Name in keyof Tools]: Tools[Name]["name"] }[keyof Tools]> + | undefined + + /** + * The concurrency level for resolving tool calls. + */ + readonly concurrency?: Concurrency | undefined + + /** + * When set to `true`, tool calls requested by the large language model + * will not be auto-resolved by the framework. + * + * This option is useful when: + * 1. The user wants to include tool call definitions from an `AiToolkit` + * in requests to the large language model so that the model has the + * capability to call tools + * 2. The user wants to control the execution of tool call resolvers + * instead of having the framework handle tool call resolution + */ + readonly disableToolCallResolution?: boolean | undefined +} + +/** + * Configuration options for structured object generation. + * + * @since 1.0.0 + * @category Models + */ +export interface GenerateObjectOptions, A, I extends Record, R> + extends GenerateTextOptions +{ + /** + * The name of the structured output that should be generated. Used by some + * large language model providers to provide additional guidance to the model. + */ + readonly objectName?: string | undefined + + /** + * The schema to be used to specify the structure of the object to generate. + */ + readonly schema: Schema.Schema +} + +/** + * The tool choice mode for the language model. + * - `auto` (default): The model can decide whether or not to call tools, as + * well as which tools to call. + * - `required`: The model **must** call a tool but can decide which tool will + * be called. + * - `none`: The model **must not** call a tool. + * - `{ tool: }`: The model must call the specified tool. + * - `{ mode?: "auto" (default) | "required", "oneOf": [] }`: The + * model is restricted to the subset of tools specified by `oneOf`. When + * `mode` is `"auto"` or omitted, the model can decide whether or not a tool + * from the allowed subset of tools can be called. When `mode` is + * `"required"`, the model **must** call one tool from the allowed subset of + * tools. + * + * @since 1.0.0 + * @category Models + */ +export type ToolChoice = "auto" | "none" | "required" | { + readonly tool: Tools +} | { + readonly mode?: "auto" | "required" + readonly oneOf: ReadonlyArray +} + +/** + * Response class for text generation operations. + * + * Contains the generated content and provides convenient accessors for + * extracting different types of response parts like text, tool calls, and usage + * information. + * + * @example + * ```ts + * import { LanguageModel } from "@effect/ai" + * import { Effect } from "effect" + * + * const program = Effect.gen(function* () { + * const response = yield* LanguageModel.generateText({ + * prompt: "Explain photosynthesis" + * }) + * + * console.log(response.text) // Generated text content + * console.log(response.finishReason) // "stop", "length", etc. + * console.log(response.usage) // Usage information + * + * return response + * }) + * ``` + * + * @since 1.0.0 + * @category Models + */ +export class GenerateTextResponse> { + readonly content: Array> + + constructor(content: Array>) { + this.content = content + } + + /** + * Extracts and concatenates all text parts from the response. + */ + get text(): string { + const text: Array = [] + for (const part of this.content) { + if (part.type === "text") { + text.push(part.text) + } + } + return text.join("") + } + + /** + * Returns all reasoning parts from the response. + */ + get reasoning(): Array { + return this.content.filter((part) => part.type === "reasoning") + } + + /** + * Extracts and concatenates all reasoning text, or undefined if none exists. + */ + get reasoningText(): string | undefined { + const text: Array = [] + for (const part of this.content) { + if (part.type === "reasoning") { + text.push(part.text) + } + } + return text.length === 0 ? undefined : text.join("") + } + + /** + * Returns all tool call parts from the response. + */ + get toolCalls(): Array> { + return this.content.filter((part) => part.type === "tool-call") + } + + /** + * Returns all tool result parts from the response. + */ + get toolResults(): Array> { + return this.content.filter((part) => part.type === "tool-result") + } + + /** + * The reason why text generation finished. + */ + get finishReason(): Response.FinishReason { + const finishPart = this.content.find((part) => part.type === "finish") + return Predicate.isUndefined(finishPart) ? "unknown" : finishPart.reason + } + + /** + * Token usage statistics for the generation request. + */ + get usage(): Response.Usage { + const finishPart = this.content.find((part) => part.type === "finish") + if (Predicate.isUndefined(finishPart)) { + return new Response.Usage({ + inputTokens: undefined, + outputTokens: undefined, + totalTokens: undefined, + reasoningTokens: undefined, + cachedInputTokens: undefined + }) + } + return finishPart.usage + } +} + +/** + * Response class for structured object generation operations. + * + * @example + * ```ts + * import { LanguageModel } from "@effect/ai" + * import { Effect, Schema } from "effect" + * + * const UserSchema = Schema.Struct({ + * name: Schema.String, + * email: Schema.String + * }) + * + * const program = Effect.gen(function* () { + * const response = yield* LanguageModel.generateObject({ + * prompt: "Create user: John Doe, john@example.com", + * schema: UserSchema + * }) + * + * console.log(response.value) // { name: "John Doe", email: "john@example.com" } + * console.log(response.text) // Raw generated text + * + * return response.value + * }) + * ``` + * + * @since 1.0.0 + * @category Models + */ +export class GenerateObjectResponse, A> extends GenerateTextResponse { + /** + * The parsed structured object that conforms to the provided schema. + */ + readonly value: A + + constructor(value: A, content: Array>) { + super(content) + this.value = value + } +} + +// ============================================================================= +// Utility Types +// ============================================================================= + +/** + * Utility type that extracts the error type from LanguageModel options. + * + * Automatically infers the possible error types based on toolkit configuration + * and tool call resolution settings. + * + * @since 1.0.0 + * @category Utility Types + */ +export type ExtractError = Options extends { + readonly toolkit: Toolkit.WithHandler + readonly disableToolCallResolution: true +} ? AiError.AiError + : Options extends { + readonly toolkit: Effect.Effect, infer _E, infer _R> + readonly disableToolCallResolution: true + } ? AiError.AiError | _E + : Options extends { + readonly toolkit: Toolkit.WithHandler + } ? AiError.AiError | Tool.HandlerError<_Tools[keyof _Tools]> + : Options extends { + readonly toolkit: Effect.Effect, infer _E, infer _R> + } ? AiError.AiError | Tool.HandlerError<_Tools[keyof _Tools]> | _E : + AiError.AiError + +/** + * Utility type that extracts the context requirements from LanguageModel options. + * + * Automatically infers the required services based on the toolkit configuration. + * + * @since 1.0.0 + * @category Utility Types + */ +export type ExtractContext = Options extends { + readonly toolkit: Toolkit.WithHandler +} ? Tool.Requirements<_Tools[keyof _Tools]> + : Options extends { + readonly toolkit: Effect.Effect, infer _E, infer _R> + } ? Tool.Requirements<_Tools[keyof _Tools]> | _R + : never + +// ============================================================================= +// Service Constructor +// ============================================================================= + +/** + * Configuration options passed along to language model provider + * implementations. + * + * This interface defines the normalized options that are passed to the + * underlying provider implementation, regardless of the specific provider being + * used. + * + * @since 1.0.0 + * @category Models + */ +export interface ProviderOptions { + /** + * The prompt messages to use to generate text. + */ + readonly prompt: Prompt.Prompt + + /** + * The tools that the large language model will have available to provide + * additional information which can be incorporated into its text generation. + */ + readonly tools: ReadonlyArray + + /** + * The format which the response should be provided in. + * + * If `"text"` is specified, the large language model response will be + * returned as text. + * + * If `"json"` is specified, the large language model respose will be provided + * as an JSON object that conforms to the shape of the specified schema. + * + * Defaults to `{ type: "text" }`. + */ + readonly responseFormat: + | { + readonly type: "text" + } + | { + readonly type: "json" + readonly objectName: string + readonly schema: Schema.Schema.Any + } + + /** + * The tool choice mode for the language model. + * - `auto` (default): The model can decide whether or not to call tools, as + * well as which tools to call. + * - `required`: The model **must** call a tool but can decide which tool will + * be called. + * - `none`: The model **must not** call a tool. + * - `{ tool: }`: The model must call the specified tool. + * - `{ mode?: "auto" (default) | "required", "oneOf": [] }`: The + * model is restricted to the subset of tools specified by `oneOf`. When + * `mode` is `"auto"` or omitted, the model can decide whether or not a tool + * from the allowed subset of tools can be called. When `mode` is + * `"required"`, the model **must** call one tool from the allowed subset of + * tools. + */ + readonly toolChoice: ToolChoice + + /** + * The span to use to trace interactions with the large language model. + */ + readonly span: Span +} + +/** + * Parameters required to construct a LanguageModel service. + * + * @since 1.0.0 + * @category Models + */ +export interface ConstructorParams { + /** + * A method which requests text generation from the large language model + * provider. + * + * The final result is returned when the large language model provider + * finishes text generation. + */ + readonly generateText: (options: ProviderOptions) => Effect.Effect< + Array, + AiError.AiError, + IdGenerator + > + + /** + * A method which requests text generation from the large language model + * provider. + * + * Intermediate results are streamed from the large language model provider. + */ + readonly streamText: (options: ProviderOptions) => Stream.Stream< + Response.StreamPartEncoded, + AiError.AiError, + IdGenerator + > +} + +/** + * Creates a LanguageModel service from provider-specific implementations. + * + * This constructor takes provider-specific implementations for text generation + * and streaming text generation and returns a LanguageModel service. + * + * @since 1.0.0 + * @category Constructors + */ +export const make: (params: ConstructorParams) => Effect.Effect = Effect.fnUntraced( + function*(params) { + const parentSpanTransformer = yield* Effect.serviceOption(CurrentSpanTransformer) + const getSpanTransformer = Effect.serviceOption(CurrentSpanTransformer).pipe( + Effect.map(Option.orElse(() => parentSpanTransformer)) + ) + + const idGenerator = yield* Effect.serviceOption(IdGenerator).pipe( + Effect.map(Option.getOrElse(() => defaultIdGenerator)) + ) + + const generateText = < + Options extends NoExcessProperties, Options>, + Tools extends Record = {} + >(options: Options & GenerateTextOptions): Effect.Effect< + GenerateTextResponse, + ExtractError, + ExtractContext + > => + Effect.useSpan( + "LanguageModel.generateText", + { + captureStackTrace: false, + attributes: { + concurrency: options.concurrency, + toolChoice: options.toolChoice + } + }, + Effect.fnUntraced( + function*(span) { + const spanTransformer = yield* getSpanTransformer + + const providerOptions: Mutable = { + prompt: Prompt.make(options.prompt), + tools: [], + toolChoice: "none", + responseFormat: { type: "text" }, + span + } + const content = yield* generateContent(options, providerOptions) + + applySpanTransformer(spanTransformer, content as any, providerOptions) + + return new GenerateTextResponse(content) + }, + Effect.catchTag("ParseError", (error) => + AiError.MalformedOutput.fromParseError({ + module: "LanguageModel", + method: "generateText", + error + })), + (effect, span) => Effect.withParentSpan(effect, span), + Effect.provideService(IdGenerator, idGenerator) + ) + ) as any + + const generateObject = < + A, + I extends Record, + R, + Options extends NoExcessProperties, Options>, + Tools extends Record = {} + >(options: Options & GenerateObjectOptions): Effect.Effect< + GenerateObjectResponse, + ExtractError, + R | ExtractContext + > => { + const schema: Schema.Schema = options.schema + const objectName = getObjectName(options.objectName, schema) + return Effect.useSpan( + "LanguageModel.generateObject", + { + captureStackTrace: false, + attributes: { + objectName, + concurrency: options.concurrency, + toolChoice: options.toolChoice + } + }, + Effect.fnUntraced( + function*(span) { + const spanTransformer = yield* getSpanTransformer + + const providerOptions: Mutable = { + prompt: Prompt.make(options.prompt), + tools: [], + toolChoice: "none", + responseFormat: { type: "json", objectName, schema }, + span + } + + const content = yield* generateContent(options, providerOptions) + + applySpanTransformer(spanTransformer, content as any, providerOptions) + + const value = yield* resolveStructuredOutput(content as any, schema) + + return new GenerateObjectResponse(value, content) + }, + Effect.catchTag("ParseError", (error) => + AiError.MalformedOutput.fromParseError({ + module: "LanguageModel", + method: "generateText", + error + })), + (effect, span) => Effect.withParentSpan(effect, span), + Effect.provideService(IdGenerator, idGenerator) + ) + ) as any + } + + const streamText: < + Options extends NoExcessProperties, Options>, + Tools extends Record = {} + >(options: Options & GenerateTextOptions) => Stream.Stream< + Response.StreamPart, + ExtractError, + ExtractContext + > = Effect.fnUntraced( + function*< + Tools extends Record, + Options extends NoExcessProperties, Options> + >(options: Options & GenerateTextOptions) { + const span = yield* Effect.makeSpanScoped("LanguageModel.streamText", { + captureStackTrace: false, + attributes: { concurrency: options.concurrency, toolChoice: options.toolChoice } + }) + + const providerOptions: Mutable = { + prompt: Prompt.make(options.prompt), + tools: [], + toolChoice: "none", + responseFormat: { type: "text" }, + span + } + + // Resolve the content stream for the request + const stream = yield* streamContent(options, providerOptions) + + // Return the stream immediately if there is no span transformer + const spanTransformer = yield* getSpanTransformer + if (Option.isNone(spanTransformer)) { + return stream + } + + // Otherwise aggregate generated content and apply the span transformer + // when the stream is finished + let content: Array> = [] + return stream.pipe( + Stream.mapChunks((chunk) => { + content = [...content, ...chunk] + return chunk + }), + Stream.ensuring(Effect.sync(() => { + spanTransformer.value({ ...providerOptions, response: content as any }) + })) + ) + }, + Stream.unwrapScoped, + Stream.mapError((error) => + ParseResult.isParseError(error) + ? AiError.MalformedOutput.fromParseError({ + module: "LanguageModel", + method: "streamText", + error + }) + : error + ), + Stream.provideService(IdGenerator, idGenerator) + ) as any + + const generateContent: < + Options extends NoExcessProperties, Options>, + Tools extends Record = {} + >(options: Options & GenerateTextOptions, providerOptions: Mutable) => Effect.Effect< + Array>, + AiError.AiError | ParseResult.ParseError, + IdGenerator + > = Effect.fnUntraced( + function*< + Tools extends Record, + Options extends NoExcessProperties, Options> + >(options: Options & GenerateTextOptions, providerOptions: Mutable) { + const toolChoice = options.toolChoice ?? "auto" + + // If there is no toolkit, the generated content can be returned immediately + if (Predicate.isUndefined(options.toolkit)) { + const ResponseSchema = Schema.mutable(Schema.Array(Response.Part(Toolkit.empty))) + const rawContent = yield* params.generateText(providerOptions) + const content = yield* Schema.decodeUnknown(ResponseSchema)(rawContent) + return content as Array> + } + + // If there is a toolkit resolve and apply it to the provider options + const toolkit = yield* resolveToolkit(options.toolkit) + + // If the resolved toolkit is empty, return the generated content immediately + if (Object.values(toolkit.tools).length === 0) { + const ResponseSchema = Schema.mutable(Schema.Array(Response.Part(Toolkit.empty))) + const rawContent = yield* params.generateText(providerOptions) + const content = yield* Schema.decodeUnknown(ResponseSchema)(rawContent) + return content as Array> + } + + const tools = typeof toolChoice === "object" && "oneOf" in toolChoice + ? Object.values(toolkit.tools).filter((tool) => toolChoice.oneOf.includes(tool.name)) + : Object.values(toolkit.tools) + providerOptions.tools = tools + providerOptions.toolChoice = toolChoice + + // Construct the response schema with the tools from the toolkit + const ResponseSchema = Schema.mutable(Schema.Array(Response.Part(toolkit))) + + // If tool call resolution is disabled, return the response without + // resolving the tool calls that were generated + if (options.disableToolCallResolution === true) { + const rawContent = yield* params.generateText(providerOptions) + const content = yield* Schema.decodeUnknown(ResponseSchema)(rawContent) + return content as Array> + } + + const rawContent = yield* params.generateText(providerOptions) + + // Resolve the generated tool calls + const toolResults = yield* resolveToolCalls(rawContent, toolkit, options.concurrency) + const content = yield* Schema.decodeUnknown(ResponseSchema)(rawContent) + + // Return the content merged with the tool call results + return [...content, ...toolResults] as Array> + } + ) + + const streamContent: < + Options extends NoExcessProperties, Options>, + Tools extends Record = {} + >(options: Options & GenerateTextOptions, providerOptions: Mutable) => Effect.Effect< + Stream.Stream, AiError.AiError | ParseResult.ParseError, IdGenerator>, + Options extends { readonly toolkit: Effect.Effect, infer _E, infer _R> } ? _E : never, + | (Options extends { readonly toolkit: Effect.Effect, infer _E, infer _R> } ? _R + : never) + | Scope.Scope + > = Effect.fnUntraced( + function*< + Tools extends Record, + Options extends NoExcessProperties, Options> + >(options: Options & GenerateTextOptions, providerOptions: Mutable) { + const toolChoice = options.toolChoice ?? "auto" + + // If there is no toolkit, return immediately + if (Predicate.isUndefined(options.toolkit)) { + const schema = Schema.ChunkFromSelf(Response.StreamPart(Toolkit.empty)) + const decode = Schema.decode(schema) + return params.streamText(providerOptions).pipe( + Stream.mapChunksEffect(decode) + ) as Stream.Stream, AiError.AiError | ParseResult.ParseError, IdGenerator> + } + + // If there is a toolkit resolve and apply it to the provider options + const toolkit = Effect.isEffect(options.toolkit) ? yield* options.toolkit : options.toolkit + + // If the toolkit is empty, return immediately + if (Object.values(toolkit.tools).length === 0) { + const schema = Schema.ChunkFromSelf(Response.StreamPart(Toolkit.empty)) + const decode = Schema.decode(schema) + return params.streamText(providerOptions).pipe( + Stream.mapChunksEffect(decode) + ) as Stream.Stream, AiError.AiError | ParseResult.ParseError, IdGenerator> + } + + const tools = typeof toolChoice === "object" && "oneOf" in toolChoice + ? Object.values(toolkit.tools).filter((tool) => toolChoice.oneOf.includes(tool.name)) + : Object.values(toolkit.tools) + providerOptions.tools = tools + providerOptions.toolChoice = toolChoice + + // If tool call resolution is disabled, return the response without + // resolving the tool calls that were generated + if (options.disableToolCallResolution === true) { + const schema = Schema.ChunkFromSelf(Response.StreamPart(toolkit)) + const decode = Schema.decode(schema) + return params.streamText(providerOptions).pipe( + Stream.mapChunksEffect(decode) + ) as Stream.Stream, AiError.AiError | ParseResult.ParseError, IdGenerator> + } + + const mailbox = yield* Mailbox.make, AiError.AiError | ParseResult.ParseError>() + const ResponseSchema = Schema.Array(Response.StreamPart(toolkit)) + const decode = Schema.decode(ResponseSchema) + yield* params.streamText(providerOptions).pipe( + Stream.runForEachChunk(Effect.fnUntraced(function*(chunk) { + const rawContent = Chunk.toArray(chunk) + const content = yield* decode(rawContent) + yield* mailbox.offerAll(content) + const toolResults = yield* resolveToolCalls(rawContent, toolkit, options.concurrency) + yield* mailbox.offerAll(toolResults as any) + })), + Mailbox.into(mailbox), + Effect.forkScoped + ) + return Mailbox.toStream(mailbox) + } + ) + + return { + generateText, + generateObject, + streamText + } as const + } +) + +// ============================================================================= +// Accessors +// ============================================================================= + +/** + * Generate text using a language model. + * + * @example + * ```ts + * import { LanguageModel } from "@effect/ai" + * import { Effect } from "effect" + * + * const program = Effect.gen(function* () { + * const response = yield* LanguageModel.generateText({ + * prompt: "Write a haiku about programming", + * toolChoice: "none" + * }) + * + * console.log(response.text) + * console.log(response.usage.totalTokens) + * + * return response + * }) + * ``` + * + * @since 1.0.0 + * @category Functions + */ +export const generateText: < + Options extends NoExcessProperties, Options>, + Tools extends Record = {} +>(options: Options & GenerateTextOptions) => Effect.Effect< + GenerateTextResponse, + ExtractError, + LanguageModel | ExtractContext +> = Effect.serviceFunctionEffect(LanguageModel, (model) => model.generateText) + +/** + * Generate a structured object from a schema using a language model. + * + * @example + * ```ts + * import { LanguageModel } from "@effect/ai" + * import { Effect, Schema } from "effect" + * + * const EventSchema = Schema.Struct({ + * title: Schema.String, + * date: Schema.String, + * location: Schema.String + * }) + * + * const program = Effect.gen(function* () { + * const response = yield* LanguageModel.generateObject({ + * prompt: "Extract event info: Tech Conference on March 15th in San Francisco", + * schema: EventSchema, + * objectName: "event" + * }) + * + * console.log(response.value) + * // { title: "Tech Conference", date: "March 15th", location: "San Francisco" } + * + * return response.value + * }) + * ``` + * + * @since 1.0.0 + * @category Functions + */ +export const generateObject: < + A, + I extends Record, + R, + Options extends NoExcessProperties, Options>, + Tools extends Record = {} +>(options: Options & GenerateObjectOptions) => Effect.Effect< + GenerateObjectResponse, + ExtractError, + LanguageModel | R | ExtractContext +> = Effect.serviceFunctionEffect(LanguageModel, (model) => model.generateObject) + +/** + * Generate text using a language model with streaming output. + * + * Returns a stream of response parts that are emitted as soon as they are + * available from the model, enabling real-time text generation experiences. + * + * @example + * ```ts + * import { LanguageModel } from "@effect/ai" + * import { Effect, Stream, Console } from "effect" + * + * const program = LanguageModel.streamText({ + * prompt: "Write a story about a space explorer" + * }).pipe(Stream.runForEach((part) => { + * if (part.type === "text-delta") { + * return Console.log(part.delta) + * } + * return Effect.void + * })) + * ``` + * + * @since 1.0.0 + * @category Functions + */ +export const streamText = < + Options extends NoExcessProperties, Options>, + Tools extends Record = {} +>(options: Options & GenerateTextOptions): Stream.Stream< + Response.StreamPart, + ExtractError, + LanguageModel | ExtractContext +> => Stream.unwrap(LanguageModel.pipe(Effect.map((model) => model.streamText(options)))) + +// ============================================================================= +// Tool Call Resolution +// ============================================================================= + +const resolveToolCalls = >( + content: ReadonlyArray, + toolkit: Toolkit.WithHandler, + concurrency: Concurrency | undefined +): Effect.Effect< + ReadonlyArray< + Response.ToolResultPart< + Tool.Name, + Tool.Success, + Tool.Failure + > + >, + Tool.HandlerError, + Tool.Requirements +> => { + const toolNames: Array = [] + const toolCalls: Array = [] + + for (const part of content) { + if (part.type === "tool-call") { + toolNames.push(part.name) + if (part.providerExecuted === true) { + continue + } + toolCalls.push(part) + } + } + + return Effect.forEach(toolCalls, (toolCall) => { + return toolkit.handle(toolCall.name, toolCall.params as any).pipe( + Effect.map(({ encodedResult, isFailure, result }) => + Response.makePart("tool-result", { + id: toolCall.id, + name: toolCall.name, + result, + encodedResult, + isFailure, + providerExecuted: false, + ...(toolCall.providerName !== undefined + ? { providerName: toolCall.providerName } + : {}) + }) + ) + ) + }, { concurrency }) +} + +// ============================================================================= +// Utilities +// ============================================================================= + +const resolveToolkit = , E, R>( + toolkit: Toolkit.WithHandler | Effect.Effect, E, R> +): Effect.Effect, E, R> => Effect.isEffect(toolkit) ? toolkit : Effect.succeed(toolkit) + +const getObjectName = , R>( + objectName: string | undefined, + schema: Schema.Schema +): string => + Predicate.isNotUndefined(objectName) + ? objectName + : "_tag" in schema + ? schema._tag as string + : "identifier" in schema + ? schema.identifier as string + : "generateObject" + +const resolveStructuredOutput = Effect.fnUntraced( + function*(response: ReadonlyArray>, ResultSchema: Schema.Schema) { + const text: Array = [] + for (const part of response) { + if (part.type === "text") { + text.push(part.text) + } + } + + if (text.length === 0) { + return yield* new AiError.MalformedOutput({ + module: "LanguageModel", + method: "generateObject", + description: "No object was generated by the large language model" + }) + } + + const decode = Schema.decode(Schema.parseJson(ResultSchema)) + return yield* Effect.mapError(decode(text.join("")), (cause) => + new AiError.MalformedOutput({ + module: "LanguageModel", + method: "generateObject", + description: "Generated object failed to conform to provided schema", + cause + })) + } +) + +const applySpanTransformer = ( + transformer: Option.Option, + response: ReadonlyArray>, + options: ProviderOptions +): void => { + if (Option.isSome(transformer)) { + transformer.value({ ...options, response: response as any }) + } +} diff --git a/repos/effect/packages/ai/ai/src/McpSchema.ts b/repos/effect/packages/ai/ai/src/McpSchema.ts new file mode 100644 index 0000000..f0eec33 --- /dev/null +++ b/repos/effect/packages/ai/ai/src/McpSchema.ts @@ -0,0 +1,2030 @@ +/** + * @since 1.0.0 + */ +import * as Rpc from "@effect/rpc/Rpc" +import type * as RpcClient from "@effect/rpc/RpcClient" +import type { RpcClientError } from "@effect/rpc/RpcClientError" +import * as RpcGroup from "@effect/rpc/RpcGroup" +import * as RpcMiddleware from "@effect/rpc/RpcMiddleware" +import * as Context from "effect/Context" +import type * as Effect from "effect/Effect" +import * as Schema from "effect/Schema" +import type * as Scope from "effect/Scope" + +// ============================================================================= +// Common +// ============================================================================= + +/** + * A uniquely identifying ID for a request in JSON-RPC. + * + * @since 1.0.0 + * @category Common + */ +export const RequestId: Schema.Union<[ + typeof Schema.String, + typeof Schema.Number +]> = Schema.Union(Schema.String, Schema.Number) + +/** + * A uniquely identifying ID for a request in JSON-RPC. + * + * @since 1.0.0 + * @category Common + */ +export type RequestId = typeof RequestId.Type + +/** + * A progress token, used to associate progress notifications with the original + * request. + * + * @since 1.0.0 + * @category Common + */ +export const ProgressToken: Schema.Union<[ + typeof Schema.String, + typeof Schema.Number +]> = Schema.Union(Schema.String, Schema.Number) + +/** + * A progress token, used to associate progress notifications with the original + * request. + * + * @since 1.0.0 + * @category Common + */ +export type ProgressToken = typeof ProgressToken.Type + +/** + * @since 1.0.0 + * @category Common + */ +export class RequestMeta extends Schema.Struct({ + _meta: Schema.optional(Schema.Struct({ + /** + * If specified, the caller is requesting out-of-band progress notifications + * for this request (as represented by notifications/progress). The value of + * this parameter is an opaque token that will be attached to any subsequent + * notifications. The receiver is not obligated to provide these + * notifications. + */ + progressToken: Schema.optional(ProgressToken) + })) +}) {} + +/** + * @since 1.0.0 + * @category Common + */ +export class ResultMeta extends Schema.Struct({ + /** + * This result property is reserved by the protocol to allow clients and + * servers to attach additional metadata to their responses. + */ + _meta: Schema.optional(Schema.Record({ key: Schema.String, value: Schema.Unknown })) +}) {} + +/** + * @since 1.0.0 + * @category Common + */ +export class NotificationMeta extends Schema.Struct({ + /** + * This parameter name is reserved by MCP to allow clients and servers to + * attach additional metadata to their notifications. + */ + _meta: Schema.optional(Schema.Record({ key: Schema.String, value: Schema.Unknown })) +}) {} + +/** + * An opaque token used to represent a cursor for pagination. + * + * @since 1.0.0 + * @category Common + */ +export const Cursor: typeof Schema.String = Schema.String + +/** + * @since 1.0.0 + * @category Common + */ +export type Cursor = typeof Cursor.Type + +/** + * @since 1.0.0 + * @category Common + */ +export class PaginatedRequestMeta extends Schema.Struct({ + ...RequestMeta.fields, + /** + * An opaque token representing the current pagination position. + * If provided, the server should return results starting after this cursor. + */ + cursor: Schema.optional(Cursor) +}) {} + +/** + * @since 1.0.0 + * @category Common + */ +export class PaginatedResultMeta extends Schema.Struct({ + ...ResultMeta.fields, + /** + * An opaque token representing the pagination position after the last returned result. + * If present, there may be more results available. + */ + nextCursor: Schema.optional(Cursor) +}) {} + +/** + * The sender or recipient of messages and data in a conversation. + * @since 1.0.0 + * @category Common + */ +export const Role: Schema.Literal<["user", "assistant"]> = Schema.Literal("user", "assistant") + +/** + * @since 1.0.0 + * @category Common + */ +export type Role = typeof Role.Type + +/** + * Optional annotations for the client. The client can use annotations to + * inform how objects are used or displayed + * + * @since 1.0.0 + * @category Common + */ +export class Annotations extends Schema.Struct({ + /** + * Describes who the intended customer of this object or data is. + * + * It can include multiple entries to indicate content useful for multiple + * audiences (e.g., `["user", "assistant"]`). + */ + audience: Schema.optional(Schema.Array(Role)), + /** + * Describes how important this data is for operating the server. + * + * A value of 1 means "most important," and indicates that the data is + * effectively required, while 0 means "least important," and indicates that + * the data is entirely optional. + */ + priority: Schema.optional(Schema.Number.pipe(Schema.between(0, 1))) +}) {} + +/** + * Describes the name and version of an MCP implementation. + * + * @since 1.0.0 + * @category Common + */ +export class Implementation extends Schema.Struct({ + name: Schema.String, + title: Schema.optional(Schema.String), + version: Schema.String +}) {} + +/** + * Capabilities a client may support. Known capabilities are defined here, in + * this schema, but this is not a closed set: any client can define its own, + * additional capabilities. + * + * @since 1.0.0 + * @category Common + */ +export class ClientCapabilities extends Schema.Class( + "@effect/ai/McpSchema/ClientCapabilities" +)({ + /** + * Experimental, non-standard capabilities that the client supports. + */ + experimental: Schema.optional(Schema.Record({ + key: Schema.String, + value: Schema.Struct({}) + })), + /** + * Present if the client supports listing roots. + */ + roots: Schema.optional(Schema.Struct({ + /** + * Whether the client supports notifications for changes to the roots list. + */ + listChanged: Schema.optional(Schema.Boolean) + })), + /** + * Present if the client supports sampling from an LLM. + */ + sampling: Schema.optional(Schema.Struct({})), + /** + * Present if the client supports elicitation from the server. + */ + elicitation: Schema.optional(Schema.Struct({})) +}) {} + +/** + * Capabilities that a server may support. Known capabilities are defined + * here, in this schema, but this is not a closed set: any server can define + * its own, additional capabilities. + * + * @since 1.0.0 + * @category Common + */ +export class ServerCapabilities extends Schema.Struct({ + /** + * Experimental, non-standard capabilities that the server supports. + */ + experimental: Schema.optional(Schema.Record({ + key: Schema.String, + value: Schema.Struct({}) + })), + /** + * Present if the server supports sending log messages to the client. + */ + logging: Schema.optional(Schema.Struct({})), + /** + * Present if the server supports argument autocompletion suggestions. + */ + completions: Schema.optional(Schema.Struct({})), + /** + * Present if the server offers any prompt templates. + */ + prompts: Schema.optional(Schema.Struct({ + /** + * Whether this server supports notifications for changes to the prompt list. + */ + listChanged: Schema.optional(Schema.Boolean) + })), + /** + * Present if the server offers any resources to read. + */ + resources: Schema.optional(Schema.Struct({ + /** + * Whether this server supports subscribing to resource updates. + */ + subscribe: Schema.optional(Schema.Boolean), + /** + * Whether this server supports notifications for changes to the resource list. + */ + listChanged: Schema.optional(Schema.Boolean) + })), + /** + * Present if the server offers any tools to call. + */ + tools: Schema.optional(Schema.Struct({ + /** + * Whether this server supports notifications for changes to the tool list. + */ + listChanged: Schema.optional(Schema.Boolean) + })) +}) {} + +// ============================================================================= +// Errors +// ============================================================================= + +/** + * @since 1.0.0 + * @category Errors + */ +export class McpError extends Schema.Class( + "@effect/ai/McpSchema/McpError" +)({ + /** + * The error type that occurred. + */ + code: Schema.Number, + /** + * A short description of the error. The message SHOULD be limited to a + * concise single sentence. + */ + message: Schema.String, + /** + * Additional information about the error. The value of this member is + * defined by the sender (e.g. detailed error information, nested errors etc.). + */ + data: Schema.optional(Schema.Unknown) +}) {} + +/** + * @since 1.0.0 + * @category Errors + */ +export const INVALID_REQUEST_ERROR_CODE = -32600 as const +/** + * @since 1.0.0 + * @category Errors + */ +export const METHOD_NOT_FOUND_ERROR_CODE = -32601 as const +/** + * @since 1.0.0 + * @category Errors + */ +export const INVALID_PARAMS_ERROR_CODE = -32602 as const +/** + * @since 1.0.0 + * @category Errors + */ +export const INTERNAL_ERROR_CODE = -32603 as const +/** + * @since 1.0.0 + * @category Errors + */ +export const PARSE_ERROR_CODE = -32700 as const + +/** + * @since 1.0.0 + * @category Errors + */ +export class ParseError extends Schema.TaggedError()("ParseError", { + ...McpError.fields, + code: Schema.tag(PARSE_ERROR_CODE) +}) {} + +/** + * @since 1.0.0 + * @category Errors + */ +export class InvalidRequest extends Schema.TaggedError()("InvalidRequest", { + ...McpError.fields, + code: Schema.tag(INVALID_REQUEST_ERROR_CODE) +}) {} + +/** + * @since 1.0.0 + * @category Errors + */ +export class MethodNotFound extends Schema.TaggedError()("MethodNotFound", { + ...McpError.fields, + code: Schema.tag(METHOD_NOT_FOUND_ERROR_CODE) +}) {} + +/** + * @since 1.0.0 + * @category Errors + */ +export class InvalidParams extends Schema.TaggedError()("InvalidParams", { + ...McpError.fields, + code: Schema.tag(INVALID_PARAMS_ERROR_CODE) +}) {} + +/** + * @since 1.0.0 + * @category Errors + */ +export class InternalError extends Schema.TaggedError()("InternalError", { + ...McpError.fields, + code: Schema.tag(INTERNAL_ERROR_CODE) +}) { + static readonly notImplemented = new InternalError({ message: "Not implemented" }) +} + +// ============================================================================= +// Ping +// ============================================================================= + +/** + * A ping, issued by either the server or the client, to check that the other + * party is still alive. The receiver must promptly respond, or else may be + * disconnected. + * + * @since 1.0.0 + * @category Ping + */ +export class Ping extends Rpc.make("ping", { + success: Schema.Struct({}), + error: McpError, + payload: Schema.UndefinedOr(RequestMeta) +}) {} + +// ============================================================================= +// Initialization +// ============================================================================= + +/** + * After receiving an initialize request from the client, the server sends this + * response. + * + * @since 1.0.0 + * @category Initialization + */ +export class InitializeResult extends Schema.Class( + "@effect/ai/McpSchema/InitializeResult" +)({ + ...ResultMeta.fields, + /** + * The version of the Model Context Protocol that the server wants to use. + * This may not match the version that the client requested. If the client + * cannot support this version, it MUST disconnect. + */ + protocolVersion: Schema.String, + capabilities: ServerCapabilities, + serverInfo: Implementation, + /** + * Instructions describing how to use the server and its features. + * + * This can be used by clients to improve the LLM's understanding of available + * tools, resources, etc. It can be thought of like a "hint" to the model. + * For example, this information MAY be added to the system prompt. + */ + instructions: Schema.optional(Schema.String) +}) {} + +/** + * This request is sent from the client to the server when it first connects, + * asking it to begin initialization. + * + * @since 1.0.0 + * @category Initialization + */ +export class Initialize extends Rpc.make("initialize", { + success: InitializeResult, + error: McpError, + payload: { + ...RequestMeta.fields, + /** + * The latest version of the Model Context Protocol that the client + * supports. The client MAY decide to support older versions as well. + */ + protocolVersion: Schema.String, + /** + * Capabilities a client may support. Known capabilities are defined here, + * in this schema, but this is not a closed set: any client can define its + * own, additional capabilities. + */ + capabilities: ClientCapabilities, + /** + * Describes the name and version of an MCP implementation. + */ + clientInfo: Implementation + } +}) {} + +/** + * This notification is sent from the client to the server after initialization + * has finished. + * + * @since 1.0.0 + * @category Initialization + */ +export class InitializedNotification extends Rpc.make("notifications/initialized", { + payload: Schema.UndefinedOr(NotificationMeta) +}) {} + +// ============================================================================= +// Cancellation +// ============================================================================= + +/** + * @since 1.0.0 + * @category Cancellation + */ +export class CancelledNotification extends Rpc.make("notifications/cancelled", { + payload: { + ...NotificationMeta.fields, + /** + * The ID of the request to cancel. + * + * This MUST correspond to the ID of a request previously issued in the + * same direction. + */ + requestId: RequestId, + /** + * An optional string describing the reason for the cancellation. This MAY + * be logged or presented to the user. + */ + reason: Schema.optional(Schema.String) + } +}) {} + +// ============================================================================= +// Progress +// ============================================================================= + +/** + * An out-of-band notification used to inform the receiver of a progress update + * for a long-running request. + * + * @since 1.0.0 + * @category Progress + */ +export class ProgressNotification extends Rpc.make("notifications/progress", { + payload: { + ...NotificationMeta.fields, + /** + * The progress token which was given in the initial request, used to + * associate this notification with the request that is proceeding. + */ + progressToken: ProgressToken, + /** + * The progress thus far. This should increase every time progress is made, + * even if the total is unknown. + */ + progress: Schema.optional(Schema.Number), + /** + * Total number of items to process (or total progress required), if known. + */ + total: Schema.optional(Schema.Number), + /** + * An optional message describing the current progress. + */ + message: Schema.optional(Schema.String) + } +}) {} + +// ============================================================================= +// Resources +// ============================================================================= + +/** + * A known resource that the server is capable of reading. + * + * @since 1.0.0 + * @category Resources + */ +export class Resource extends Schema.Class( + "@effect/ai/McpSchema/Resource" +)({ + /** + * The URI of this resource. + */ + uri: Schema.String, + /** + * A human-readable name for this resource. + * + * This can be used by clients to populate UI elements. + */ + name: Schema.String, + title: Schema.optional(Schema.String), + /** + * A description of what this resource represents. + * + * This can be used by clients to improve the LLM's understanding of available + * resources. It can be thought of like a "hint" to the model. + */ + description: Schema.optional(Schema.String), + /** + * The MIME type of this resource, if known. + */ + mimeType: Schema.optional(Schema.String), + /** + * Optional annotations for the client. + */ + annotations: Schema.optional(Annotations), + /** + * The size of the raw resource content, in bytes (i.e., before base64 + * encoding or any tokenization), if known. + * + * This can be used by Hosts to display file sizes and estimate context + * window usage. + */ + size: Schema.optional(Schema.Number) +}) {} + +/** + * A template description for resources available on the server. + * + * @since 1.0.0 + * @category Resources + */ +export class ResourceTemplate extends Schema.Class( + "@effect/ai/McpSchema/ResourceTemplate" +)({ + /** + * A URI template (according to RFC 6570) that can be used to construct + * resource URIs. + */ + uriTemplate: Schema.String, + /** + * A human-readable name for the type of resource this template refers to. + * + * This can be used by clients to populate UI elements. + */ + name: Schema.String, + title: Schema.optional(Schema.String), + /** + * A description of what this template is for. + * + * This can be used by clients to improve the LLM's understanding of available + * resources. It can be thought of like a "hint" to the model. + */ + description: Schema.optional(Schema.String), + + /** + * The MIME type for all resources that match this template. This should only + * be included if all resources matching this template have the same type. + */ + mimeType: Schema.optional(Schema.String), + + /** + * Optional annotations for the client. + */ + annotations: Schema.optional(Annotations) +}) {} + +/** + * The contents of a specific resource or sub-resource. + * + * @since 1.0.0 + * @category Resources + */ +export class ResourceContents extends Schema.Struct({ + /** + * The URI of this resource. + */ + uri: Schema.String, + /** + * The MIME type of this resource, if known. + */ + mimeType: Schema.optional(Schema.String) +}) {} + +/** + * The contents of a text resource, which can be represented as a string. + * + * @since 1.0.0 + * @category Resources + */ +export class TextResourceContents extends Schema.Struct({ + ...ResourceContents.fields, + /** + * The text of the item. This must only be set if the item can actually be + * represented as text (not binary data). + */ + text: Schema.String +}) {} + +/** + * The contents of a binary resource, which can be represented as an Uint8Array + * + * @since 1.0.0 + * @category Resources + */ +export class BlobResourceContents extends Schema.Struct({ + ...ResourceContents.fields, + /** + * The binary data of the item decoded from a base64-encoded string. + */ + blob: Schema.Uint8ArrayFromBase64 +}) {} + +/** + * The server's response to a resources/list request from the client. + * + * @since 1.0.0 + * @category Resources + */ +export class ListResourcesResult extends Schema.Class( + "@effect/ai/McpSchema/ListResourcesResult" +)({ + ...PaginatedResultMeta.fields, + resources: Schema.Array(Resource) +}) {} + +/** + * Sent from the client to request a list of resources the server has. + * + * @since 1.0.0 + * @category Resources + */ +export class ListResources extends Rpc.make("resources/list", { + success: ListResourcesResult, + error: McpError, + payload: Schema.UndefinedOr(PaginatedRequestMeta) +}) {} + +/** + * The server's response to a resources/templates/list request from the client. + * + * @since 1.0.0 + * @category Resources + */ +export class ListResourceTemplatesResult extends Schema.Class( + "@effect/ai/McpSchema/ListResourceTemplatesResult" +)({ + ...PaginatedResultMeta.fields, + resourceTemplates: Schema.Array(ResourceTemplate) +}) {} + +/** + * Sent from the client to request a list of resource templates the server has. + * + * @since 1.0.0 + * @category Resources + */ +export class ListResourceTemplates extends Rpc.make("resources/templates/list", { + success: ListResourceTemplatesResult, + error: McpError, + payload: Schema.UndefinedOr(PaginatedRequestMeta) +}) {} + +/** + * The server's response to a resources/read request from the client. + * + * @since 1.0.0 + * @category Resources + */ +export class ReadResourceResult extends Schema.Struct({ + ...ResultMeta.fields, + contents: Schema.Array(Schema.Union( + TextResourceContents, + BlobResourceContents + )) +}) {} + +/** + * Sent from the client to the server, to read a specific resource URI. + * + * @since 1.0.0 + * @category Resources + */ +export class ReadResource extends Rpc.make("resources/read", { + success: ReadResourceResult, + error: McpError, + payload: { + ...RequestMeta.fields, + /** + * The URI of the resource to read. The URI can use any protocol; it is up + * to the server how to interpret it. + */ + uri: Schema.String + } +}) {} + +/** + * An optional notification from the server to the client, informing it that the + * list of resources it can read from has changed. This may be issued by servers + * without any previous subscription from the client. + * + * @since 1.0.0 + * @category Resources + */ +export class ResourceListChangedNotification extends Rpc.make("notifications/resources/list_changed", { + payload: Schema.UndefinedOr(NotificationMeta) +}) {} + +/** + * Sent from the client to request resources/updated notifications from the + * server whenever a particular resource changes. + * + * @since 1.0.0 + * @category Resources + */ +export class Subscribe extends Rpc.make("resources/subscribe", { + error: McpError, + payload: { + ...RequestMeta.fields, + /** + * The URI of the resource to subscribe to. The URI can use any protocol; + * it is up to the server how to interpret it. + */ + uri: Schema.String + } +}) {} + +/** + * Sent from the client to request cancellation of resources/updated + * notifications from the server. This should follow a previous + * resources/subscribe request. + * + * @since 1.0.0 + * @category Resources + */ +export class Unsubscribe extends Rpc.make("resources/unsubscribe", { + error: McpError, + payload: { + ...RequestMeta.fields, + /** + * The URI of the resource to subscribe to. The URI can use any protocol; + * it is up to the server how to interpret it. + */ + uri: Schema.String + } +}) {} + +/** + * @since 1.0.0 + * @category Resources + */ +export class ResourceUpdatedNotification extends Rpc.make("notifications/resources/updated", { + payload: { + ...NotificationMeta.fields, + /** + * The URI of the resource that has been updated. This might be a sub-resource of the one that the client actually subscribed to. + */ + uri: Schema.String + } +}) {} + +// ============================================================================= +// Prompts +// ============================================================================= + +/** + * Describes an argument that a prompt can accept. + * + * @since 1.0.0 + * @category Prompts + */ +export class PromptArgument extends Schema.Struct({ + /** + * The name of the argument. + */ + name: Schema.String, + title: Schema.optional(Schema.String), + /** + * A human-readable description of the argument. + */ + description: Schema.optional(Schema.String), + /** + * Whether this argument must be provided. + */ + required: Schema.optional(Schema.Boolean) +}) {} + +/** + * A prompt or prompt template that the server offers. + * + * @since 1.0.0 + * @category Prompts + */ +export class Prompt extends Schema.Class( + "@effect/ai/McpSchema/Prompt" +)({ + /** + * The name of the prompt or prompt template. + */ + name: Schema.String, + title: Schema.optional(Schema.String), + /** + * An optional description of what this prompt provides + */ + description: Schema.optional(Schema.String), + /** + * A list of arguments to use for templating the prompt. + */ + arguments: Schema.optional(Schema.Array(PromptArgument)) +}) {} + +/** + * Text provided to or from an LLM. + * + * @since 1.0.0 + * @category Prompts + */ +export class TextContent extends Schema.Struct({ + type: Schema.tag("text"), + /** + * The text content of the message. + */ + text: Schema.String, + /** + * Optional annotations for the client. + */ + annotations: Schema.optional(Annotations) +}) {} + +/** + * An image provided to or from an LLM. + * + * @since 1.0.0 + * @category Prompts + */ +export class ImageContent extends Schema.Struct({ + type: Schema.tag("image"), + /** + * The image data. + */ + data: Schema.Uint8ArrayFromBase64, + /** + * The MIME type of the image. Different providers may support different + * image types. + */ + mimeType: Schema.String, + /** + * Optional annotations for the client. + */ + annotations: Schema.optional(Annotations) +}) {} + +/** + * Audio provided to or from an LLM. + * + * @since 1.0.0 + * @category Prompts + */ +export class AudioContent extends Schema.Struct({ + type: Schema.tag("audio"), + /** + * The audio data. + */ + data: Schema.Uint8ArrayFromBase64, + /** + * The MIME type of the audio. Different providers may support different + * audio types. + */ + mimeType: Schema.String, + /** + * Optional annotations for the client. + */ + annotations: Schema.optional(Annotations) +}) {} + +/** + * The contents of a resource, embedded into a prompt or tool call result. + * + * It is up to the client how best to render embedded resources for the benefit + * of the LLM and/or the user. + * + * @since 1.0.0 + * @category Prompts + */ +export class EmbeddedResource extends Schema.Struct({ + type: Schema.tag("resource"), + resource: Schema.Union(TextResourceContents, BlobResourceContents), + /** + * Optional annotations for the client. + */ + annotations: Schema.optional(Annotations) +}) {} + +/** + * A resource that the server is capable of reading, included in a prompt or tool call result. + * + * Note: resource links returned by tools are not guaranteed to appear in the results of `resources/list` requests. + * + * @since 1.0.0 + * @category Prompts + */ +export class ResourceLink extends Schema.Struct({ + ...Resource.fields, + type: Schema.tag("resource_link") +}) {} + +/** + * @since 1.0.0 + * @category Prompts + */ +export class ContentBlock extends Schema.Union( + TextContent, + ImageContent, + AudioContent, + EmbeddedResource, + ResourceLink +) {} + +/** + * Describes a message returned as part of a prompt. + * + * This is similar to `SamplingMessage`, but also supports the embedding of + * resources from the MCP server. + * + * @since 1.0.0 + * @category Prompts + */ +export class PromptMessage extends Schema.Struct({ + role: Role, + content: ContentBlock +}) {} + +/** + * The server's response to a prompts/list request from the client. + * + * @since 1.0.0 + * @category Prompts + */ +export class ListPromptsResult extends Schema.Class( + "@effect/ai/McpSchema/ListPromptsResult" +)({ + ...PaginatedResultMeta.fields, + prompts: Schema.Array(Prompt) +}) {} + +/** + * Sent from the client to request a list of prompts and prompt templates the + * server has. + * + * @since 1.0.0 + * @category Prompts + */ +export class ListPrompts extends Rpc.make("prompts/list", { + success: ListPromptsResult, + error: McpError, + payload: Schema.UndefinedOr(PaginatedRequestMeta) +}) {} + +/** + * The server's response to a prompts/get request from the client. + * + * @since 1.0.0 + * @category Prompts + */ +export class GetPromptResult extends Schema.Class( + "@effect/ai/McpSchema/GetPromptResult" +)({ + ...ResultMeta.fields, + messages: Schema.Array(PromptMessage), + /** + * An optional description for the prompt. + */ + description: Schema.optional(Schema.String) +}) {} + +/** + * Used by the client to get a prompt provided by the server. + * + * @since 1.0.0 + * @category Prompts + */ +export class GetPrompt extends Rpc.make("prompts/get", { + success: GetPromptResult, + error: McpError, + payload: { + ...RequestMeta.fields, + /** + * The name of the prompt or prompt template. + */ + name: Schema.String, + title: Schema.optional(Schema.String), + /** + * Arguments to use for templating the prompt. + */ + arguments: Schema.optional(Schema.Record({ + key: Schema.String, + value: Schema.String + })) + } +}) {} + +/** + * An optional notification from the server to the client, informing it that + * the list of prompts it offers has changed. This may be issued by servers + * without any previous subscription from the client. + * + * @since 1.0.0 + * @category Prompts + */ +export class PromptListChangedNotification extends Rpc.make("notifications/prompts/list_changed", { + payload: Schema.UndefinedOr(NotificationMeta) +}) {} + +// ============================================================================= +// Tools +// ============================================================================= + +/** + * Additional properties describing a Tool to clients. + * + * NOTE: all properties in ToolAnnotations are **hints**. They are not + * guaranteed to provide a faithful description of tool behavior (including + * descriptive properties like `title`). + * + * Clients should never make tool use decisions based on ToolAnnotations + * received from untrusted servers. + * + * @since 1.0.0 + * @category Tools + */ +export class ToolAnnotations extends Schema.Class( + "@effect/ai/McpSchema/ToolAnnotations" +)({ + /** + * A human-readable title for the tool. + */ + title: Schema.optional(Schema.String), + /** + * If true, the tool does not modify its environment. + * + * Default: `false` + */ + readOnlyHint: Schema.optionalWith(Schema.Boolean, { default: () => false }), + /** + * If true, the tool may perform destructive updates to its environment. + * If false, the tool performs only additive updates. + * + * (This property is meaningful only when `readOnlyHint == false`) + * + * Default: `true` + */ + destructiveHint: Schema.optionalWith(Schema.Boolean, { default: () => true }), + /** + * If true, calling the tool repeatedly with the same arguments + * will have no additional effect on the its environment. + * + * (This property is meaningful only when `readOnlyHint == false`) + * + * Default: `false` + */ + idempotentHint: Schema.optionalWith(Schema.Boolean, { default: () => false }), + /** + * If true, this tool may interact with an "open world" of external + * entities. If false, the tool's domain of interaction is closed. + * For example, the world of a web search tool is open, whereas that + * of a memory tool is not. + * + * Default: `true` + */ + openWorldHint: Schema.optionalWith(Schema.Boolean, { default: () => true }) +}) {} + +/** + * Definition for a tool the client can call. + * + * @since 1.0.0 + * @category Tools + */ +export class Tool extends Schema.Class( + "@effect/ai/McpSchema/Tool" +)({ + /** + * The name of the tool. + */ + name: Schema.String, + title: Schema.optional(Schema.String), + /** + * A human-readable description of the tool. + * + * This can be used by clients to improve the LLM's understanding of available tools. It can be thought of like a "hint" to the model. + */ + description: Schema.optional(Schema.String), + /** + * A JSON Schema object defining the expected parameters for the tool. + */ + inputSchema: Schema.Unknown, + /** + * Optional additional tool information. + */ + annotations: Schema.optional(ToolAnnotations) +}) {} + +/** + * The server's response to a tools/list request from the client. + * + * @since 1.0.0 + * @category Tools + */ +export class ListToolsResult extends Schema.Class( + "@effect/ai/McpSchema/ListToolsResult" +)({ + ...PaginatedResultMeta.fields, + tools: Schema.Array(Tool) +}) {} + +/** + * Sent from the client to request a list of tools the server has. + * + * @since 1.0.0 + * @category Tools + */ +export class ListTools extends Rpc.make("tools/list", { + success: ListToolsResult, + error: McpError, + payload: Schema.UndefinedOr(PaginatedRequestMeta) +}) {} + +/** + * The server's response to a tool call. + * + * Any errors that originate from the tool SHOULD be reported inside the result + * object, with `isError` set to true, _not_ as an MCP protocol-level error + * response. Otherwise, the LLM would not be able to see that an error occurred + * and self-correct. + * + * However, any errors in _finding_ the tool, an error indicating that the + * server does not support tool calls, or any other exceptional conditions, + * should be reported as an MCP error response. + * + * @since 1.0.0 + * @category Tools + */ +export class CallToolResult extends Schema.Class("@effect/ai/McpSchema/CallToolResult")({ + ...ResultMeta.fields, + content: Schema.Array(ContentBlock), + structuredContent: Schema.optional(Schema.Unknown), + /** + * Whether the tool call ended in an error. + * + * If not set, this is assumed to be false (the call was successful). + */ + isError: Schema.optional(Schema.Boolean) +}) {} + +/** + * Used by the client to invoke a tool provided by the server. + * + * @since 1.0.0 + * @category Tools + */ +export class CallTool extends Rpc.make("tools/call", { + success: CallToolResult, + error: McpError, + payload: { + ...RequestMeta.fields, + name: Schema.String, + arguments: Schema.Record({ + key: Schema.String, + value: Schema.Unknown + }) + } +}) {} + +/** + * An optional notification from the server to the client, informing it that + * the list of tools it offers has changed. This may be issued by servers + * without any previous subscription from the client. + * + * @since 1.0.0 + * @category Tools + */ +export class ToolListChangedNotification extends Rpc.make("notifications/tools/list_changed", { + payload: Schema.UndefinedOr(NotificationMeta) +}) {} + +// ============================================================================= +// Logging +// ============================================================================= + +/** + * The severity of a log message. + * + * These map to syslog message severities, as specified in RFC-5424: + * https://datatracker.ietf.org/doc/html/rfc5424#section-6.2.1 + * + * @since 1.0.0 + * @category Logging + */ +export const LoggingLevel: Schema.Literal<[ + "debug", + "info", + "notice", + "warning", + "error", + "critical", + "alert", + "emergency" +]> = Schema.Literal( + "debug", + "info", + "notice", + "warning", + "error", + "critical", + "alert", + "emergency" +) + +/** + * The severity of a log message. + * + * These map to syslog message severities, as specified in RFC-5424: + * https://datatracker.ietf.org/doc/html/rfc5424#section-6.2.1 + * + * @since 1.0.0 + * @category Logging + */ +export type LoggingLevel = typeof LoggingLevel.Type + +/** + * A request from the client to the server, to enable or adjust logging. + * + * @since 1.0.0 + * @category Logging + */ +export class SetLevel extends Rpc.make("logging/setLevel", { + payload: { + ...RequestMeta.fields, + /** + * The level of logging that the client wants to receive from the server. + * The server should send all logs at this level and higher (i.e., more + * severe) to the client as notifications/message. + */ + level: LoggingLevel + }, + error: McpError +}) {} + +/** + * @since 1.0.0 + * @category Logging + */ +export class LoggingMessageNotification extends Rpc.make("notifications/message", { + payload: Schema.Struct({ + ...NotificationMeta.fields, + /** + * The severity of this log message. + */ + level: LoggingLevel, + /** + * An optional name of the logger issuing this message. + */ + logger: Schema.optional(Schema.String), + /** + * The data to be logged, such as a string message or an object. Any JSON + * serializable type is allowed here. + */ + data: Schema.Unknown + }) +}) {} + +// ============================================================================= +// Sampling +// ============================================================================= + +/** + * Describes a message issued to or received from an LLM API. + * + * @since 1.0.0 + * @category Sampling + */ +export class SamplingMessage extends Schema.Struct({ + role: Role, + content: Schema.Union(TextContent, ImageContent, AudioContent) +}) {} + +/** + * Hints to use for model selection. + * + * Keys not declared here are currently left unspecified by the spec and are up + * to the client to interpret. + * + * @since 1.0.0 + * @category Sampling + */ +export class ModelHint extends Schema.Struct({ + /** + * A hint for a model name. + * + * The client SHOULD treat this as a substring of a model name; for example: + * - `claude-3-5-sonnet` should match `claude-3-5-sonnet-20241022` + * - `sonnet` should match `claude-3-5-sonnet-20241022`, `claude-3-sonnet-20240229`, etc. + * - `claude` should match any Claude model + * + * The client MAY also map the string to a different provider's model name or + * a different model family, as long as it fills a similar niche; for example: + * - `gemini-1.5-flash` could match `claude-3-haiku-20240307` + */ + name: Schema.optional(Schema.String) +}) {} + +/** + * The server's preferences for model selection, requested of the client during sampling. + * + * Because LLMs can vary along multiple dimensions, choosing the "best" model is + * rarely straightforward. Different models excel in different areas—some are + * faster but less capable, others are more capable but more expensive, and so + * on. This interface allows servers to express their priorities across multiple + * dimensions to help clients make an appropriate selection for their use case. + * + * These preferences are always advisory. The client MAY ignore them. It is also + * up to the client to decide how to interpret these preferences and how to + * balance them against other considerations. + * + * @since 1.0.0 + * @category Sampling + */ +export class ModelPreferences extends Schema.Class( + "@effect/ai/McpSchema/ModelPreferences" +)({ + /** + * Optional hints to use for model selection. + * + * If multiple hints are specified, the client MUST evaluate them in order + * (such that the first match is taken). + * + * The client SHOULD prioritize these hints over the numeric priorities, but + * MAY still use the priorities to select from ambiguous matches. + */ + hints: Schema.optional(Schema.Array(ModelHint)), + /** + * How much to prioritize cost when selecting a model. A value of 0 means cost + * is not important, while a value of 1 means cost is the most important + * factor. + */ + costPriority: Schema.optional(Schema.Number.pipe(Schema.between(0, 1))), + /** + * How much to prioritize sampling speed (latency) when selecting a model. A + * value of 0 means speed is not important, while a value of 1 means speed is + * the most important factor. + */ + speedPriority: Schema.optional(Schema.Number.pipe(Schema.between(0, 1))), + /** + * How much to prioritize intelligence and capabilities when selecting a + * model. A value of 0 means intelligence is not important, while a value of 1 + * means intelligence is the most important factor. + */ + intelligencePriority: Schema.optional(Schema.Number.pipe(Schema.between(0, 1))) +}) {} + +/** + * The client's response to a sampling/create_message request from the server. + * The client should inform the user before returning the sampled message, to + * allow them to inspect the response (human in the loop) and decide whether to + * allow the server to see it. + * + * @since 1.0.0 + * @category Sampling + */ +export class CreateMessageResult extends Schema.Class( + "@effect/ai/McpSchema/CreateMessageResult" +)({ + /** + * The name of the model that generated the message. + */ + model: Schema.String, + /** + * The reason why sampling stopped, if known. + */ + stopReason: Schema.optional(Schema.String) +}) {} + +/** + * A request from the server to sample an LLM via the client. The client has + * full discretion over which model to select. The client should also inform the + * user before beginning sampling, to allow them to inspect the request (human + * in the loop) and decide whether to approve it. + * + * @since 1.0.0 + * @category Sampling + */ +export class CreateMessage extends Rpc.make("sampling/createMessage", { + success: CreateMessageResult, + error: McpError, + payload: { + messages: Schema.Array(SamplingMessage), + /** + * The server's preferences for which model to select. The client MAY ignore + * these preferences. + */ + modelPreferences: Schema.optional(ModelPreferences), + /** + * An optional system prompt the server wants to use for sampling. The + * client MAY modify or omit this prompt. + */ + systemPrompt: Schema.optional(Schema.String), + /** + * A request to include context from one or more MCP servers (including the + * caller), to be attached to the prompt. The client MAY ignore this request. + */ + includeContext: Schema.optional(Schema.Literal("none", "thisServer", "allServers")), + temperature: Schema.optional(Schema.Number), + /** + * The maximum number of tokens to sample, as requested by the server. The + * client MAY choose to sample fewer tokens than requested. + */ + maxTokens: Schema.Number, + stopSequences: Schema.optional(Schema.Array(Schema.String)), + /** + * Optional metadata to pass through to the LLM provider. The format of + * this metadata is provider-specific. + */ + metadata: Schema.Unknown + } +}) {} + +// ============================================================================= +// Autocomplete +// ============================================================================= + +/** + * A reference to a resource or resource template definition. + * + * @since 1.0.0 + * @category Autocomplete + */ +export class ResourceReference extends Schema.Struct({ + type: Schema.tag("ref/resource"), + /** + * The URI or URI template of the resource. + */ + uri: Schema.String +}) {} + +/** + * Identifies a prompt. + * + * @since 1.0.0 + * @category Autocomplete + */ +export class PromptReference extends Schema.Struct({ + type: Schema.tag("ref/prompt"), + /** + * The name of the prompt or prompt template + */ + name: Schema.String, + title: Schema.optional(Schema.String) +}) {} + +/** + * The server's response to a completion/complete request + * + * @since 1.0.0 + * @category Autocomplete + */ +export class CompleteResult extends Schema.Class("@effect/ai/McpSchema/CompleteResult")({ + completion: Schema.Struct({ + /** + * An array of completion values. Must not exceed 100 items. + */ + values: Schema.Array(Schema.String), + /** + * The total number of completion options available. This can exceed the + * number of values actually sent in the response. + */ + total: Schema.optional(Schema.Number), + /** + * Indicates whether there are additional completion options beyond those + * provided in the current response, even if the exact total is unknown. + */ + hasMore: Schema.optional(Schema.Boolean) + }) +}) { + /** + * @since 1.0.0 + */ + static readonly empty = CompleteResult.make({ + completion: { + values: [], + total: 0, + hasMore: false + } + }) +} + +/** + * A request from the client to the server, to ask for completion options. + * + * @since 1.0.0 + * @category Autocomplete + */ +export class Complete extends Rpc.make("completion/complete", { + success: CompleteResult, + error: McpError, + payload: Schema.Struct({ + ref: Schema.Union(PromptReference, ResourceReference), + /** + * The argument's information + */ + argument: Schema.Struct({ + /** + * The name of the argument + */ + name: Schema.String, + /** + * The value of the argument to use for completion matching. + */ + value: Schema.String + }), + /** + * Additional, optional context for completions + */ + context: Schema.optionalWith( + Schema.Struct({ + /** + * Previously-resolved variables in a URI template or prompt. + */ + arguments: Schema.optionalWith( + Schema.Record({ + key: Schema.String, + value: Schema.String + }), + { default: () => ({}) } + ) + }), + { default: () => ({ arguments: {} }) } + ) + }) +}) {} + +// ============================================================================= +// Roots +// ============================================================================= + +/** + * Represents a root directory or file that the server can operate on. + * + * @since 1.0.0 + * @category Roots + */ +export class Root extends Schema.Class( + "@effect/ai/McpSchema/Root" +)({ + /** + * The URI identifying the root. This *must* start with file:// for now. + * This restriction may be relaxed in future versions of the protocol to allow + * other URI schemes. + */ + uri: Schema.String, + /** + * An optional name for the root. This can be used to provide a human-readable + * identifier for the root, which may be useful for display purposes or for + * referencing the root in other parts of the application. + */ + name: Schema.optional(Schema.String) +}) {} + +/** + * The client's response to a roots/list request from the server. This result + * contains an array of Root objects, each representing a root directory or file + * that the server can operate on. + * + * @since 1.0.0 + * @category Roots + */ +export class ListRootsResult extends Schema.Class( + "@effect/ai/McpSchema/ListRootsResult" +)({ + roots: Schema.Array(Root) +}) {} + +/** + * Sent from the server to request a list of root URIs from the client. Roots + * allow servers to ask for specific directories or files to operate on. A + * common example for roots is providing a set of repositories or directories a + * server should operate + * on. + * + * This request is typically used when the server needs to understand the file + * system structure or access specific locations that the client has permission + * to read from. + * + * @since 1.0.0 + * @category Roots + */ +export class ListRoots extends Rpc.make("roots/list", { + success: ListRootsResult, + error: McpError, + payload: Schema.UndefinedOr(RequestMeta) +}) {} + +/** + * A notification from the client to the server, informing it that the list of + * roots has changed. This notification should be sent whenever the client adds, + * removes, or modifies any root. The server should then request an updated list + * of roots using the ListRootsRequest. + * + * @since 1.0.0 + * @category Roots + */ +export class RootsListChangedNotification extends Rpc.make("notifications/roots/list_changed", { + payload: Schema.UndefinedOr(NotificationMeta) +}) {} + +// ============================================================================= +// Elicitation +// ============================================================================= + +/** + * The client's response to an elicitation request + * + * @since 1.0.0 + * @category Elicitation + */ +export class ElicitAcceptResult extends Schema.Class( + "@effect/ai/McpSchema/ElicitAcceptResult" +)({ + ...ResultMeta.fields, + /** + * The user action in response to the elicitation. + * - "accept": User submitted the form/confirmed the action + * - "decline": User explicitly declined the action + * - "cancel": User dismissed without making an explicit choice + */ + action: Schema.Literal("accept"), + /** + * The submitted form data, only present when action is "accept". + * Contains values matching the requested schema. + */ + content: Schema.Unknown +}) {} + +/** + * The client's response to an elicitation request + * + * @since 1.0.0 + * @category Elicitation + */ +export class ElicitDeclineResult extends Schema.Class( + "@effect/ai/McpSchema/ElicitDeclineResult" +)({ + ...ResultMeta.fields, + /** + * The user action in response to the elicitation. + * - "accept": User submitted the form/confirmed the action + * - "decline": User explicitly declined the action + * - "cancel": User dismissed without making an explicit choice + */ + action: Schema.Literal("cancel", "decline") +}) {} + +/** + * The client's response to an elicitation request + * + * @since 1.0.0 + * @category Elicitation + */ +export const ElicitResult = Schema.Union( + ElicitAcceptResult, + ElicitDeclineResult +) + +/** + * @since 1.0.0 + * @category Elicitation + */ +export class Elicit extends Rpc.make("elicitation/create", { + success: ElicitResult, + error: McpError, + payload: { + /** + * A message to display to the user, explaining what they are being + * elicited for. + */ + message: Schema.String, + /** + * A restricted subset of JSON Schema. + * Only top-level properties are allowed, without nesting. + */ + requestedSchema: Schema.Unknown + } +}) {} + +/** + * @since 1.0.0 + * @category Elicitation + */ +export class ElicitationDeclined + extends Schema.TaggedError("@effect/ai/McpSchema/ElicitationDeclined")("ElicitationDeclined", { + request: Elicit.payloadSchema, + cause: Schema.optional(Schema.Defect) + }) +{} + +// ============================================================================= +// McpServerClient +// ============================================================================= + +/** + * @since 1.0.0 + * @category McpServerClient + */ +export class McpServerClient extends Context.Tag("@effect/ai/McpSchema/McpServerClient")< + McpServerClient, + { + readonly clientId: number + readonly getClient: Effect.Effect< + RpcClient.RpcClient, RpcClientError>, + never, + Scope.Scope + > + } +>() {} + +/** + * @since 1.0.0 + * @category McpServerClient + */ +export class McpServerClientMiddleware + extends RpcMiddleware.Tag()("@effect/ai/McpSchema/McpServerClientMiddleware", { + provides: McpServerClient + }) +{} + +// ============================================================================= +// Protocol +// ============================================================================= + +/** + * @since 1.0.0 + * @category Protocol + */ +export type RequestEncoded = RpcGroup.Rpcs< + Group +> extends infer Rpc ? Rpc extends Rpc.Rpc< + infer _Tag, + infer _Payload, + infer _Success, + infer _Error, + infer _Middleware + > ? { + readonly _tag: "Request" + readonly id: string | number + readonly method: _Tag + readonly payload: _Payload["Encoded"] + } + : never + : never + +/** + * @since 1.0.0 + * @category Protocol + */ +export type NotificationEncoded = RpcGroup.Rpcs< + Group +> extends infer Rpc ? Rpc extends Rpc.Rpc< + infer _Tag, + infer _Payload, + infer _Success, + infer _Error, + infer _Middleware + > ? { + readonly _tag: "Notification" + readonly method: _Tag + readonly payload: _Payload["Encoded"] + } + : never + : never + +/** + * @since 1.0.0 + * @category Protocol + */ +export type SuccessEncoded = RpcGroup.Rpcs< + Group +> extends infer Rpc ? Rpc extends Rpc.Rpc< + infer _Tag, + infer _Payload, + infer _Success, + infer _Error, + infer _Middleware + > ? { + readonly _tag: "Success" + readonly id: string | number + readonly result: _Success["Encoded"] + } + : never + : never + +/** + * @since 1.0.0 + * @category Protocol + */ +export type FailureEncoded = RpcGroup.Rpcs< + Group +> extends infer Rpc ? Rpc extends Rpc.Rpc< + infer _Tag, + infer _Payload, + infer _Success, + infer _Error, + infer _Middleware + > ? { + readonly _tag: "Failure" + readonly id: string | number + readonly error: _Error["Encoded"] + } + : never + : never + +/** + * @since 1.0.0 + * @category Protocol + */ +export class ClientRequestRpcs extends RpcGroup.make( + Ping, + Initialize, + Complete, + SetLevel, + GetPrompt, + ListPrompts, + ListResources, + ListResourceTemplates, + ReadResource, + Subscribe, + Unsubscribe, + CallTool, + ListTools +).middleware(McpServerClientMiddleware) {} + +/** + * @since 1.0.0 + * @category Protocol + */ +export type ClientRequestEncoded = RequestEncoded + +/** + * @since 1.0.0 + * @category Protocol + */ +export class ClientNotificationRpcs extends RpcGroup.make( + CancelledNotification, + ProgressNotification, + InitializedNotification, + RootsListChangedNotification +) {} + +/** + * @since 1.0.0 + * @category Protocol + */ +export type ClientNotificationEncoded = NotificationEncoded + +/** + * @since 1.0.0 + * @category Protocol + */ +export class ClientRpcs extends ClientRequestRpcs.merge(ClientNotificationRpcs) {} + +/** + * @since 1.0.0 + * @category Protocol + */ +export type ClientSuccessEncoded = SuccessEncoded + +/** + * @since 1.0.0 + * @category Protocol + */ +export type ClientFailureEncoded = FailureEncoded + +/** + * @since 1.0.0 + * @category Protocol + */ +export class ServerRequestRpcs extends RpcGroup.make( + Ping, + CreateMessage, + ListRoots, + Elicit +) {} + +/** + * @since 1.0.0 + * @category Protocol + */ +export type ServerRequestEncoded = RequestEncoded + +/** + * @since 1.0.0 + * @category Protocol + */ +export class ServerNotificationRpcs extends RpcGroup.make( + CancelledNotification, + ProgressNotification, + LoggingMessageNotification, + ResourceUpdatedNotification, + ResourceListChangedNotification, + ToolListChangedNotification, + PromptListChangedNotification +) {} + +/** + * @since 1.0.0 + * @category Protocol + */ +export type ServerNotificationEncoded = NotificationEncoded + +/** + * @since 1.0.0 + * @category Protocol + */ +export type ServerSuccessEncoded = SuccessEncoded + +/** + * @since 1.0.0 + * @category Protocol + */ +export type ServerFailureEncoded = FailureEncoded + +/** + * @since 1.0.0 + * @category Protocol + */ +export type ServerResultEncoded = ServerSuccessEncoded | ServerFailureEncoded + +/** + * @since 1.0.0 + * @category Protocol + */ +export type FromClientEncoded = ClientRequestEncoded | ClientNotificationEncoded +/** + * @since 1.0.0 + * @category Protocol + */ +export type FromServerEncoded = ServerResultEncoded | ServerNotificationEncoded + +/** + * @since 1.0.0 + * @category Parameters + */ +export const ParamAnnotation: unique symbol = Symbol.for("@effect/ai/McpSchema/ParamNameId") + +/** + * @since 1.0.0 + * @category Parameters + */ +export interface Param + extends Schema.Schema +{ + readonly [ParamAnnotation]: Id +} + +/** + * Helper to create a param for a resource URI template. + * + * @since 1.0.0 + * @category Parameters + */ +export const param = (id: Id, schema: S): Param => + schema.annotations({ + [ParamAnnotation]: id + }) as any diff --git a/repos/effect/packages/ai/ai/src/McpServer.ts b/repos/effect/packages/ai/ai/src/McpServer.ts new file mode 100644 index 0000000..b91cebe --- /dev/null +++ b/repos/effect/packages/ai/ai/src/McpServer.ts @@ -0,0 +1,1370 @@ +/** + * @since 1.0.0 + */ +import * as Headers from "@effect/platform/Headers" +import type * as HttpLayerRouter from "@effect/platform/HttpLayerRouter" +import type * as HttpRouter from "@effect/platform/HttpRouter" +import type { RpcMessage } from "@effect/rpc" +import type * as Rpc from "@effect/rpc/Rpc" +import * as RpcClient from "@effect/rpc/RpcClient" +import type * as RpcGroup from "@effect/rpc/RpcGroup" +import * as RpcSerialization from "@effect/rpc/RpcSerialization" +import * as RpcServer from "@effect/rpc/RpcServer" +import * as Arr from "effect/Array" +import * as Cause from "effect/Cause" +import * as Context from "effect/Context" +import * as Effect from "effect/Effect" +import * as Exit from "effect/Exit" +import * as JsonSchema from "effect/JSONSchema" +import * as Layer from "effect/Layer" +import * as Logger from "effect/Logger" +import * as Mailbox from "effect/Mailbox" +import * as Option from "effect/Option" +import * as RcMap from "effect/RcMap" +import * as Schema from "effect/Schema" +import * as AST from "effect/SchemaAST" +import type { Sink } from "effect/Sink" +import type { Stream } from "effect/Stream" +import type * as Types from "effect/Types" +import * as FindMyWay from "find-my-way-ts" +import type { + Annotations, + CallTool, + Complete, + GetPrompt, + Param, + PromptArgument, + PromptMessage, + ReadResourceResult, + ServerCapabilities +} from "./McpSchema.js" +import { + CallToolResult, + ClientNotificationRpcs, + ClientRpcs, + CompleteResult, + Elicit, + ElicitationDeclined, + GetPromptResult, + InternalError, + InvalidParams, + ListPromptsResult, + ListResourcesResult, + ListResourceTemplatesResult, + ListToolsResult, + McpServerClient, + McpServerClientMiddleware, + ParamAnnotation, + Prompt, + Resource, + ResourceTemplate, + ServerNotificationRpcs, + ServerRequestRpcs, + TextContent, + Tool, + ToolAnnotations +} from "./McpSchema.js" +import * as AiTool from "./Tool.js" +import type * as Toolkit from "./Toolkit.js" + +/** + * @since 1.0.0 + * @category McpServer + */ +export class McpServer extends Context.Tag("@effect/ai/McpServer")< + McpServer, + { + readonly notifications: RpcClient.RpcClient> + readonly notificationsMailbox: Mailbox.ReadonlyMailbox> + readonly initializedClients: Set + + readonly tools: ReadonlyArray + readonly addTool: (options: { + readonly tool: Tool + readonly handle: (payload: any) => Effect.Effect + }) => Effect.Effect + readonly callTool: ( + requests: typeof CallTool.payloadSchema.Type + ) => Effect.Effect + + readonly resources: ReadonlyArray + readonly addResource: ( + resource: Resource, + handle: Effect.Effect + ) => Effect.Effect + + readonly resourceTemplates: ReadonlyArray + readonly addResourceTemplate: (options: { + readonly template: ResourceTemplate + readonly routerPath: string + readonly completions: Record Effect.Effect> + readonly handle: (uri: string, params: Array) => Effect.Effect< + typeof ReadResourceResult.Type, + InvalidParams | InternalError, + McpServerClient + > + }) => Effect.Effect + + readonly findResource: (uri: string) => Effect.Effect< + typeof ReadResourceResult.Type, + InvalidParams | InternalError, + McpServerClient + > + + readonly prompts: ReadonlyArray + readonly addPrompt: (options: { + readonly prompt: Prompt + readonly completions: Record< + string, + (input: string) => Effect.Effect + > + readonly handle: ( + params: Record + ) => Effect.Effect + }) => Effect.Effect + readonly getPromptResult: ( + request: typeof GetPrompt.payloadSchema.Type + ) => Effect.Effect + + readonly completion: ( + complete: typeof Complete.payloadSchema.Type + ) => Effect.Effect + } +>() { + /** + * @since 1.0.0 + */ + static readonly make = Effect.gen(function*() { + const matcher = makeUriMatcher< + | { + readonly _tag: "ResourceTemplate" + readonly handle: ( + uri: string, + params: Array + ) => Effect.Effect + } + | { + readonly _tag: "Resource" + readonly effect: Effect.Effect + } + >() + const tools = Arr.empty() + const toolMap = new Map< + string, + (payload: any) => Effect.Effect + >() + const resources: Array = [] + const resourceTemplates: Array = [] + const prompts: Array = [] + const promptMap = new Map< + string, + (params: Record) => Effect.Effect + >() + const completionsMap = new Map< + string, + (input: string) => Effect.Effect + >() + const notificationsMailbox = yield* Mailbox.make>() + const listChangedHandles = new Map() + const notifications = yield* RpcClient.makeNoSerialization(ServerNotificationRpcs, { + spanPrefix: "McpServer/Notifications", + onFromClient(options): Effect.Effect { + const message = options.message + if (message._tag !== "Request") { + return Effect.void + } + if (message.tag.includes("list_changed")) { + if (!listChangedHandles.has(message.tag)) { + listChangedHandles.set( + message.tag, + setTimeout(() => { + notificationsMailbox.unsafeOffer(message) + listChangedHandles.delete(message.tag) + }, 0) + ) + } + } else { + notificationsMailbox.unsafeOffer(message) + } + return notifications.write({ + clientId: 0, + requestId: message.id, + _tag: "Exit", + exit: Exit.void as any + }) + } + }) + + return McpServer.of({ + notifications: notifications.client, + notificationsMailbox, + initializedClients: new Set(), + get tools() { + return tools + }, + addTool: (options) => + Effect.suspend(() => { + tools.push(options.tool) + toolMap.set(options.tool.name, options.handle) + return notifications.client["notifications/tools/list_changed"]({}) + }), + callTool: (request) => + Effect.suspend((): Effect.Effect => { + const handle = toolMap.get(request.name) + if (!handle) { + return Effect.fail( + new InvalidParams({ + message: `Tool '${request.name}' not found` + }) + ) + } + return handle(request.arguments) + }), + get resources() { + return resources + }, + get resourceTemplates() { + return resourceTemplates + }, + addResource: (resource, effect) => + Effect.suspend(() => { + resources.push(resource) + matcher.add(resource.uri, { _tag: "Resource", effect }) + return notifications.client["notifications/resources/list_changed"]( + {} + ) + }), + addResourceTemplate: ({ completions, handle, routerPath, template }) => + Effect.suspend(() => { + resourceTemplates.push(template) + matcher.add(routerPath, { _tag: "ResourceTemplate", handle }) + for (const [param, handle] of Object.entries(completions)) { + completionsMap.set( + `ref/resource/${template.uriTemplate}/${param}`, + handle + ) + } + return notifications.client["notifications/resources/list_changed"]( + {} + ) + }), + findResource: (uri) => + Effect.suspend(() => { + const match = matcher.find(uri) + if (!match) { + return Effect.succeed({ contents: [] }) + } else if (match.handler._tag === "Resource") { + return match.handler.effect + } + const params: Array = [] + for (const key of Object.keys(match.params)) { + params[Number(key)] = match.params[key]! + } + return match.handler.handle(uri, params) + }), + get prompts() { + return prompts + }, + addPrompt: (options) => + Effect.suspend(() => { + prompts.push(options.prompt) + promptMap.set(options.prompt.name, options.handle) + for (const [param, handle] of Object.entries(options.completions)) { + completionsMap.set( + `ref/prompt/${options.prompt.name}/${param}`, + handle + ) + } + return notifications.client["notifications/prompts/list_changed"]({}) + }), + getPromptResult: Effect.fnUntraced(function*({ arguments: params, name }) { + const handler = promptMap.get(name) + if (!handler) { + return yield* new InvalidParams({ + message: `Prompt '${name}' not found` + }) + } + return yield* handler(params ?? {}) + }), + completion: Effect.fnUntraced(function*(complete) { + const ref = complete.ref + const key = ref.type === "ref/resource" + ? `ref/resource/${ref.uri}/${complete.argument.name}` + : `ref/prompt/${ref.name}/${complete.argument.name}` + const handler = completionsMap.get(key) + return handler + ? yield* handler(complete.argument.value) + : CompleteResult.empty + }) + }) + }) + + /** + * @since 1.0.0 + */ + static readonly layer: Layer.Layer = Layer.scoped(McpServer, McpServer.make) as any +} + +const LATEST_PROTOCOL_VERSION = "2025-06-18" +const SUPPORTED_PROTOCOL_VERSIONS = [ + LATEST_PROTOCOL_VERSION, + "2025-03-26", + "2024-11-05", + "2024-10-07" +] + +/** + * @since 1.0.0 + * @category Constructors + */ +export const run: (options: { + readonly name: string + readonly version: string +}) => Effect.Effect = Effect.fnUntraced(function*(options: { + readonly name: string + readonly version: string +}) { + const protocol = yield* RpcServer.Protocol + const handlers = yield* Layer.build(layerHandlers(options)) + const server = yield* McpServer + + const clients = yield* RcMap.make({ + lookup: Effect.fnUntraced(function*(clientId: number) { + let write!: ( + message: RpcMessage.FromServerEncoded + ) => Effect.Effect + const client = yield* RpcClient.make(ServerRequestRpcs, { + spanPrefix: "McpServer/Client" + }).pipe( + Effect.provideServiceEffect( + RpcClient.Protocol, + RpcClient.Protocol.make( + Effect.fnUntraced(function*(writeResponse) { + write = writeResponse + return { + send(request, _transferables) { + return protocol.send(clientId, { + ...request, + headers: undefined, + traceId: undefined, + spanId: undefined, + sampled: undefined + } as any) + }, + supportsAck: true, + supportsTransferables: false + } + }) + ) + ) + ) + + return { client, write } as const + }), + idleTimeToLive: 10000 + }) + + const clientMiddleware = McpServerClientMiddleware.of(({ clientId }) => + Effect.sync(() => + McpServerClient.of({ + clientId, + getClient: RcMap.get(clients, clientId).pipe( + Effect.map(({ client }) => client) + ) + }) + ) + ) + + const patchedProtocol = RpcServer.Protocol.of({ + ...protocol, + run: (f) => + protocol.run((clientId, request_) => { + const request = request_ as any as + | RpcMessage.FromServerEncoded + | RpcMessage.FromClientEncoded + switch (request._tag) { + case "Request": { + if (ClientNotificationRpcs.requests.has(request.tag)) { + if (request.tag === "notifications/cancelled") { + return f(clientId, { + _tag: "Interrupt", + requestId: String((request.payload as any).requestId) + }) + } + const handler = handlers.unsafeMap.get( + request.tag + ) as Rpc.Handler + return handler + ? handler.handler(request.payload, { + clientId, + headers: Headers.fromInput(request.headers) + }) as Effect.Effect + : Effect.void + } + return f(clientId, request) + } + case "Ping": + case "Ack": + case "Interrupt": + case "Eof": + return f(clientId, request) + case "Pong": + case "Exit": + case "Chunk": + case "ClientProtocolError": + case "Defect": + return RcMap.get(clients, clientId).pipe( + Effect.flatMap(({ write }) => write(request)), + Effect.scoped + ) + } + }) + }) + + const encodeNotification = Schema.encode( + Schema.Union( + ...Array.from( + ServerNotificationRpcs.requests.values(), + (rpc) => rpc.payloadSchema + ) + ) + ) + yield* server.notificationsMailbox.take.pipe( + Effect.flatMap(Effect.fnUntraced(function*(request) { + const encoded = yield* encodeNotification(request.payload) + const message: RpcMessage.RequestEncoded = { + _tag: "Request", + tag: request.tag, + payload: encoded + } as any + const clientIds = yield* patchedProtocol.clientIds + for (const clientId of server.initializedClients) { + if (!clientIds.has(clientId)) { + server.initializedClients.delete(clientId) + continue + } + yield* patchedProtocol.send(clientId, message as any) + } + })), + Effect.catchAllCause(() => Effect.void), + Effect.forever, + Effect.forkScoped + ) + + return yield* RpcServer.make(ClientRpcs, { + spanPrefix: "McpServer", + disableFatalDefects: true + }).pipe( + Effect.provideService(RpcServer.Protocol, patchedProtocol), + Effect.provideService(McpServerClientMiddleware, clientMiddleware), + Effect.provide(handlers) + ) +}, Effect.scoped) + +/** + * @since 1.0.0 + * @category Layers + */ +export const layer = (options: { + readonly name: string + readonly version: string +}): Layer.Layer => + Layer.scopedDiscard(Effect.forkScoped(run(options))).pipe( + Layer.provideMerge(McpServer.layer) + ) + +/** + * Run the McpServer, using stdio for input and output. + * + * ```ts + * import { McpSchema, McpServer } from "@effect/ai" + * import { NodeRuntime, NodeSink, NodeStream } from "@effect/platform-node" + * import { Effect, Layer, Logger, Schema } from "effect" + * + * const idParam = McpSchema.param("id", Schema.NumberFromString) + * + * // Define a resource template for a README file + * const ReadmeTemplate = McpServer.resource`file://readme/${idParam}`({ + * name: "README Template", + * // You can add auto-completion for the ID parameter + * completion: { + * id: (_) => Effect.succeed([1, 2, 3, 4, 5]) + * }, + * content: Effect.fn(function*(_uri, id) { + * return `# MCP Server Demo - ID: ${id}` + * }) + * }) + * + * // Define a test prompt with parameters + * const TestPrompt = McpServer.prompt({ + * name: "Test Prompt", + * description: "A test prompt to demonstrate MCP server capabilities", + * parameters: Schema.Struct({ + * flightNumber: Schema.String + * }), + * completion: { + * flightNumber: () => Effect.succeed(["FL123", "FL456", "FL789"]) + * }, + * content: ({ flightNumber }) => Effect.succeed(`Get the booking details for flight number: ${flightNumber}`) + * }) + * + * // Merge all the resources and prompts into a single server layer + * const ServerLayer = Layer.mergeAll( + * ReadmeTemplate, + * TestPrompt + * ).pipe( + * // Provide the MCP server implementation + * Layer.provide(McpServer.layerStdio({ + * name: "Demo Server", + * version: "1.0.0", + * stdin: NodeStream.stdin, + * stdout: NodeSink.stdout + * })), + * // add a stderr logger + * Layer.provide(Logger.add(Logger.prettyLogger({ stderr: true }))) + * ) + * + * Layer.launch(ServerLayer).pipe(NodeRuntime.runMain) + * ``` + * + * @since 1.0.0 + * @category Layers + */ +export const layerStdio = (options: { + readonly name: string + readonly version: string + readonly stdin: Stream + readonly stdout: Sink +}): Layer.Layer => + layer(options).pipe( + Layer.provide( + RpcServer.layerProtocolStdio({ + stdin: options.stdin, + stdout: options.stdout + }) + ), + Layer.provide(RpcSerialization.layerNdJsonRpc()), + // remove stdout loggers + Layer.provideMerge(Logger.remove(Logger.defaultLogger)), + Layer.provideMerge(Logger.remove(Logger.prettyLoggerDefault)) + ) + +/** + * Run the McpServer, using HTTP for input and output. + * + * ```ts + * import { McpSchema, McpServer } from "@effect/ai" + * import { HttpRouter } from "@effect/platform" + * import { NodeHttpServer, NodeRuntime } from "@effect/platform-node" + * import { Effect, Layer, Schema } from "effect" + * import { createServer } from "node:http" + * + * const idParam = McpSchema.param("id", Schema.NumberFromString) + * + * // Define a resource template for a README file + * const ReadmeTemplate = McpServer.resource`file://readme/${idParam}`({ + * name: "README Template", + * // You can add auto-completion for the ID parameter + * completion: { + * id: (_) => Effect.succeed([1, 2, 3, 4, 5]) + * }, + * content: Effect.fn(function*(_uri, id) { + * return `# MCP Server Demo - ID: ${id}` + * }) + * }) + * + * // Define a test prompt with parameters + * const TestPrompt = McpServer.prompt({ + * name: "Test Prompt", + * description: "A test prompt to demonstrate MCP server capabilities", + * parameters: Schema.Struct({ + * flightNumber: Schema.String + * }), + * completion: { + * flightNumber: () => Effect.succeed(["FL123", "FL456", "FL789"]) + * }, + * content: ({ flightNumber }) => Effect.succeed(`Get the booking details for flight number: ${flightNumber}`) + * }) + * + * // Merge all the resources and prompts into a single server layer + * const ServerLayer = Layer.mergeAll( + * ReadmeTemplate, + * TestPrompt, + * HttpRouter.Default.serve() + * ).pipe( + * // Provide the MCP server implementation + * Layer.provide(McpServer.layerHttp({ + * name: "Demo Server", + * version: "1.0.0", + * path: "/mcp" + * })), + * Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 })) + * ) + * + * Layer.launch(ServerLayer).pipe(NodeRuntime.runMain) + * ``` + * + * @since 1.0.0 + * @category Layers + */ +export const layerHttp = (options: { + readonly name: string + readonly version: string + readonly path: HttpRouter.PathInput + readonly routerTag?: HttpRouter.HttpRouter.TagClass +}): Layer.Layer => + layer(options).pipe( + Layer.provide(RpcServer.layerProtocolHttp(options)), + Layer.provide(RpcSerialization.layerJsonRpc()) + ) + +/** + * Run the McpServer, using HTTP for input and output. + * + * Uses a `HttpLayerRouter` to register the McpServer routes. + * + * @since 1.0.0 + * @category Layers + */ +export const layerHttpRouter = (options: { + readonly name: string + readonly version: string + readonly path: HttpRouter.PathInput +}): Layer.Layer< + McpServer | McpServerClient, + never, + HttpLayerRouter.HttpRouter +> => + layer(options).pipe( + Layer.provide(RpcServer.layerProtocolHttpRouter(options)), + Layer.provide(RpcSerialization.layerJsonRpc()) + ) + +/** + * Register an AiToolkit with the McpServer. + * + * @since 1.0.0 + * @category Tools + */ +export const registerToolkit: >( + toolkit: Toolkit.Toolkit +) => Effect.Effect< + void, + never, + | McpServer + | AiTool.HandlersFor + | Exclude, McpServerClient> +> = Effect.fnUntraced(function*>( + toolkit: Toolkit.Toolkit +) { + const registry = yield* McpServer + const built = yield* toolkit as any as Effect.Effect< + Toolkit.WithHandler, + never, + Exclude, McpServerClient> + > + const context = yield* Effect.context() + for (const tool of Object.values(built.tools)) { + const mcpTool = new Tool({ + name: tool.name, + description: tool.description, + inputSchema: makeJsonSchema(tool.parametersSchema.ast), + annotations: new ToolAnnotations({ + ...Context.getOption(tool.annotations, AiTool.Title).pipe( + Option.map((title) => ({ title })), + Option.getOrUndefined + ), + readOnlyHint: Context.get(tool.annotations, AiTool.Readonly), + destructiveHint: Context.get(tool.annotations, AiTool.Destructive), + idempotentHint: Context.get(tool.annotations, AiTool.Idempotent), + openWorldHint: Context.get(tool.annotations, AiTool.OpenWorld) + }) + }) + yield* registry.addTool({ + tool: mcpTool, + handle(payload) { + return built.handle(tool.name as any, payload).pipe( + Effect.provide(context as Context.Context), + Effect.match({ + onFailure: (error) => + new CallToolResult({ + isError: true, + structuredContent: typeof error === "object" ? error : undefined, + content: [ + { + type: "text", + text: JSON.stringify(error) + } + ] + }), + onSuccess: (result) => + new CallToolResult({ + isError: false, + structuredContent: typeof result.encodedResult === "object" + ? result.encodedResult + : undefined, + content: [ + { + type: "text", + text: JSON.stringify(result.encodedResult) + } + ] + }) + }) + ) as any + } + }) + } +}) + +/** + * Register an AiToolkit with the McpServer. + * + * @since 1.0.0 + * @category Tools + */ +export const toolkit = >( + toolkit: Toolkit.Toolkit +): Layer.Layer< + never, + never, + | AiTool.HandlersFor + | Exclude, McpServerClient> +> => + Layer.effectDiscard(registerToolkit(toolkit)).pipe( + Layer.provide(McpServer.layer) + ) + +/** + * @since 1.0.0 + */ +export type ValidateCompletions< + Completions, + Keys extends string +> = + & Completions + & { + readonly [K in keyof Completions]: K extends Keys ? (input: string) => any + : never + } + +/** + * @since 1.0.0 + */ +export type ResourceCompletions< + Schemas extends ReadonlyArray +> = { + readonly [ + K in Extract< + keyof Schemas, + `${number}` + > as Schemas[K] extends Param ? Id : `param${K}` + ]: ( + input: string + ) => Effect.Effect>, any, any> +} + +/** + * Register a resource with the McpServer. + * + * @since 1.0.0 + * @category Resources + */ +export const registerResource: { + (options: { + readonly uri: string + readonly name: string + readonly description?: string | undefined + readonly mimeType?: string | undefined + readonly audience?: ReadonlyArray<"user" | "assistant"> | undefined + readonly priority?: number | undefined + readonly content: Effect.Effect< + typeof ReadResourceResult.Type | string | Uint8Array, + E, + R + > + }): Effect.Effect | McpServer> + >( + segments: TemplateStringsArray, + ...schemas: + & Schemas + & { + readonly [K in keyof Schemas]: Schema.Schema.Encoded< + Schemas[K] + > extends string ? unknown + : "Schema must be encodable to a string" + } + ): < + E, + R, + const Completions extends Partial> = {} + >(options: { + readonly name: string + readonly description?: string | undefined + readonly mimeType?: string | undefined + readonly audience?: ReadonlyArray<"user" | "assistant"> | undefined + readonly priority?: number | undefined + readonly completion?: + | ValidateCompletions> + | undefined + readonly content: ( + uri: string, + ...params: { readonly [K in keyof Schemas]: Schemas[K]["Type"] } + ) => Effect.Effect< + typeof ReadResourceResult.Type | string | Uint8Array, + E, + R + > + }) => Effect.Effect< + void, + never, + | Exclude< + | R + | (Completions[keyof Completions] extends (input: string) => infer Ret + ? Ret extends Effect.Effect ? _R + : never + : never), + McpServerClient + > + | McpServer + > +} = function() { + if (arguments.length === 1) { + const options = arguments[0] as + & Resource + & typeof Annotations.Type + & { + readonly content: Effect.Effect< + typeof ReadResourceResult.Type | string | Uint8Array + > + } + return Effect.gen(function*() { + const context = yield* Effect.context() + const registry = yield* McpServer + yield* registry.addResource( + new Resource({ + ...options, + annotations: options + }), + options.content.pipe( + Effect.provide(context), + Effect.map((content) => resolveResourceContent(options.uri, content)), + Effect.catchAllCause((cause) => { + const prettyError = Cause.prettyErrors(cause)[0] + return new InternalError({ message: prettyError.message }) + }) + ) + ) + }) + } + const { params, routerPath, schema, uriPath } = compileUriTemplate( + ...(arguments as any as [any, any]) + ) + return Effect.fnUntraced(function*(options: { + readonly name: string + readonly description?: string | undefined + readonly mimeType?: string | undefined + readonly audience?: ReadonlyArray<"user" | "assistant"> | undefined + readonly priority?: number | undefined + readonly completion?: + | Record Effect.Effect> + | undefined + readonly content: ( + uri: string, + ...params: Array + ) => Effect.Effect< + typeof ReadResourceResult.Type | string | Uint8Array, + E, + R + > + }) { + const context = yield* Effect.context() + const registry = yield* McpServer + const decode = Schema.decodeUnknown(schema) + const template = new ResourceTemplate({ + ...options, + uriTemplate: uriPath, + annotations: options + }) + const completions: Record< + string, + (input: string) => Effect.Effect + > = {} + for (const [param, handle] of Object.entries(options.completion ?? {})) { + const encodeArray = Schema.encodeUnknown(Schema.Array(params[param])) + const handler = (input: string) => + handle(input).pipe( + Effect.flatMap(encodeArray), + Effect.map( + (values) => + new CompleteResult({ + completion: { + values: values as Array, + total: values.length, + hasMore: false + } + }) + ), + Effect.catchAllCause((cause) => { + const prettyError = Cause.prettyErrors(cause)[0] + return new InternalError({ message: prettyError.message }) + }), + Effect.provide(context) + ) + completions[param] = handler + } + yield* registry.addResourceTemplate({ + template, + routerPath, + completions, + handle: (uri, params) => + decode(params).pipe( + Effect.mapError( + (error) => new InvalidParams({ message: error.message }) + ), + Effect.flatMap((params) => + options.content(uri, ...params).pipe( + Effect.map((content) => resolveResourceContent(uri, content)), + Effect.catchAllCause((cause) => { + const prettyError = Cause.prettyErrors(cause)[0] + return new InternalError({ message: prettyError.message }) + }) + ) + ), + Effect.provide(context) + ) + }) + }) +} as any + +/** + * Register a resource with the McpServer. + * + * @since 1.0.0 + * @category Resources + */ +export const resource: { + (options: { + readonly uri: string + readonly name: string + readonly description?: string | undefined + readonly mimeType?: string | undefined + readonly audience?: ReadonlyArray<"user" | "assistant"> | undefined + readonly priority?: number | undefined + readonly content: Effect.Effect< + typeof ReadResourceResult.Type | string | Uint8Array, + E, + R + > + }): Layer.Layer> + >( + segments: TemplateStringsArray, + ...schemas: + & Schemas + & { + readonly [K in keyof Schemas]: Schema.Schema.Encoded< + Schemas[K] + > extends string ? unknown + : "Schema must be encodable to a string" + } + ): < + E, + R, + const Completions extends Partial> = {} + >(options: { + readonly name: string + readonly description?: string | undefined + readonly mimeType?: string | undefined + readonly audience?: ReadonlyArray<"user" | "assistant"> | undefined + readonly priority?: number | undefined + readonly completion?: + | ValidateCompletions> + | undefined + readonly content: ( + uri: string, + ...params: { readonly [K in keyof Schemas]: Schemas[K]["Type"] } + ) => Effect.Effect< + typeof ReadResourceResult.Type | string | Uint8Array, + E, + R + > + }) => Layer.Layer< + never, + never, + Exclude< + | R + | (Completions[keyof Completions] extends (input: string) => infer Ret + ? Ret extends Effect.Effect ? _R + : never + : never), + McpServerClient + > + > +} = function() { + if (arguments.length === 1) { + return Layer.effectDiscard(registerResource(arguments[0])).pipe( + Layer.provide(McpServer.layer) + ) + } + const register = registerResource(...(arguments as any as [any, any])) + return (options: any) => Layer.effectDiscard(register(options)).pipe(Layer.provide(McpServer.layer)) +} as any + +/** + * Register a prompt with the McpServer. + * + * @since 1.0.0 + * @category Prompts + */ +export const registerPrompt = < + E, + R, + Params = {}, + ParamsI extends Record = {}, + ParamsR = never, + const Completions extends { + readonly [K in keyof Params]?: ( + input: string + ) => Effect.Effect, any, any> + } = {} +>(options: { + readonly name: string + readonly description?: string | undefined + readonly parameters?: Schema.Schema | undefined + readonly completion?: + | ValidateCompletions> + | undefined + readonly content: ( + params: Params + ) => Effect.Effect | string, E, R> +}): Effect.Effect< + void, + never, + Exclude | McpServer +> => { + const args = Arr.empty() + const props: Record = {} + const propSignatures = options.parameters + ? AST.getPropertySignatures(options.parameters.ast) + : [] + for (const prop of propSignatures) { + args.push({ + name: prop.name as string, + description: Option.getOrUndefined(AST.getDescriptionAnnotation(prop)), + required: !prop.isOptional + }) + props[prop.name as string] = Schema.make(prop.type) + } + const prompt = new Prompt({ + name: options.name, + description: options.description, + arguments: args + }) + const decode = options.parameters + ? Schema.decodeUnknown(options.parameters) + : () => Effect.succeed({} as Params) + const completion: Record Effect.Effect> = options.completion ?? {} + return Effect.gen(function*() { + const registry = yield* McpServer + const context = yield* Effect.context>() + const completions: Record< + string, + ( + input: string + ) => Effect.Effect + > = {} + for (const [param, handle] of Object.entries(completion)) { + const encodeArray = Schema.encodeUnknown(Schema.Array(props[param])) + const handler = (input: string) => + handle(input).pipe( + Effect.flatMap(encodeArray), + Effect.map((values) => ({ + completion: { + values: values as Array, + total: values.length, + hasMore: false + } + })), + Effect.catchAllCause((cause) => { + const prettyError = Cause.prettyErrors(cause)[0] + return new InternalError({ message: prettyError.message }) + }), + Effect.provide(context) + ) + completions[param] = handler as any + } + yield* registry.addPrompt({ + prompt, + completions, + handle: (params) => + decode(params).pipe( + Effect.mapError( + (error) => new InvalidParams({ message: error.message }) + ), + Effect.flatMap((params) => options.content(params)), + Effect.map((messages) => { + messages = typeof messages === "string" + ? [ + { + role: "user", + content: TextContent.make({ text: messages }) + } + ] + : messages + return new GetPromptResult({ + messages, + description: prompt.description + }) + }), + Effect.catchAllCause((cause) => { + const prettyError = Cause.prettyErrors(cause)[0] + return new InternalError({ message: prettyError.message }) + }), + Effect.provide(context as Context.Context) + ) + }) + }) +} + +/** + * Register a prompt with the McpServer. + * + * @since 1.0.0 + * @category Prompts + */ +export const prompt = < + E, + R, + Params = {}, + ParamsI extends Record = {}, + ParamsR = never, + const Completions extends { + readonly [K in keyof Params]?: ( + input: string + ) => Effect.Effect, any, any> + } = {} +>(options: { + readonly name: string + readonly description?: string | undefined + readonly parameters?: Schema.Schema | undefined + readonly completion?: + | ValidateCompletions> + | undefined + readonly content: ( + params: Params + ) => Effect.Effect | string, E, R> +}): Layer.Layer> => + Layer.effectDiscard(registerPrompt(options)).pipe( + Layer.provide(McpServer.layer) + ) + +/** + * Create an elicitation request + * + * @since 1.0.0 + * @category Elicitation + */ +export const elicit: , R>(options: { + readonly message: string + readonly schema: Schema.Schema +}) => Effect.Effect = Effect.fnUntraced( + function*, R>(options: { + readonly message: string + readonly schema: Schema.Schema + }) { + const { getClient } = yield* McpServerClient + const client = yield* getClient + const request = Elicit.payloadSchema.make({ + message: options.message, + requestedSchema: makeJsonSchema(options.schema.ast) + }) + const res = yield* client["elicitation/create"](request).pipe( + Effect.catchAllCause((cause) => + Effect.fail( + new ElicitationDeclined({ cause: Cause.squash(cause), request }) + ) + ) + ) + switch (res.action) { + case "accept": + return yield* Effect.orDie( + Schema.decodeUnknown(options.schema)(res.content) + ) + case "cancel": + return yield* Effect.interrupt + case "decline": + return yield* Effect.fail(new ElicitationDeclined({ request })) + } + }, + Effect.scoped +) + +// ----------------------------------------------------------------------------- +// Internal +// ----------------------------------------------------------------------------- + +const makeUriMatcher = () => { + const router = FindMyWay.make({ + ignoreTrailingSlash: true, + ignoreDuplicateSlashes: true, + caseSensitive: true + }) + const add = (uri: string, value: A) => { + router.on("GET", uri as any, value) + } + const find = (uri: string) => router.find("GET", uri) + + return { add, find } as const +} + +const compileUriTemplate = ( + segments: TemplateStringsArray, + ...schemas: ReadonlyArray +) => { + let routerPath = segments[0].replace(":", "::") + let uriPath = segments[0] + const params: Record = {} + let pathSchema = Schema.Tuple() as Schema.Schema.Any + if (schemas.length > 0) { + const arr: Array = [] + for (let i = 0; i < schemas.length; i++) { + const schema = schemas[i] + const segment = segments[i + 1] + const key = String(i) + arr.push(schema) + routerPath += `:${key}${segment.replace(":", "::")}` + const paramName = AST.getAnnotation(ParamAnnotation)(schema.ast).pipe( + Option.getOrElse(() => `param${key}`) + ) + params[paramName as string] = schema + uriPath += `{${paramName}}${segment}` + } + pathSchema = Schema.Tuple(...arr) + } + return { + routerPath, + uriPath, + schema: pathSchema, + params + } as const +} + +const layerHandlers = (serverInfo: { + readonly name: string + readonly version: string +}) => + ClientRpcs.toLayer( + Effect.gen(function*() { + const server = yield* McpServer + + return { + // Requests + ping: () => Effect.succeed({}), + initialize(params, { clientId }) { + const requestedVersion = params.protocolVersion + const capabilities: Types.DeepMutable< + typeof ServerCapabilities.Type + > = { + completions: {} + } + if (server.tools.length > 0) { + capabilities.tools = { listChanged: true } + } + if ( + server.resources.length > 0 || + server.resourceTemplates.length > 0 + ) { + capabilities.resources = { + listChanged: true, + subscribe: false + } + } + if (server.prompts.length > 0) { + capabilities.prompts = { listChanged: true } + } + server.initializedClients.add(clientId) + return Effect.succeed({ + capabilities, + serverInfo, + protocolVersion: SUPPORTED_PROTOCOL_VERSIONS.includes( + requestedVersion + ) + ? requestedVersion + : LATEST_PROTOCOL_VERSION + }) + }, + "completion/complete": server.completion, + "logging/setLevel": () => InternalError.notImplemented, + "prompts/get": server.getPromptResult, + "prompts/list": () => Effect.sync(() => new ListPromptsResult({ prompts: server.prompts })), + "resources/list": () => + Effect.sync( + () => new ListResourcesResult({ resources: server.resources }) + ), + "resources/read": ({ uri }) => server.findResource(uri), + "resources/subscribe": () => InternalError.notImplemented, + "resources/unsubscribe": () => InternalError.notImplemented, + "resources/templates/list": () => + Effect.sync( + () => + new ListResourceTemplatesResult({ + resourceTemplates: server.resourceTemplates + }) + ), + "tools/call": server.callTool, + "tools/list": () => Effect.sync(() => new ListToolsResult({ tools: server.tools })), + + // Notifications + "notifications/cancelled": (_) => Effect.void, + "notifications/initialized": (_) => Effect.void, + "notifications/progress": (_) => Effect.void, + "notifications/roots/list_changed": (_) => Effect.void + } + }) + ) + +const makeJsonSchema = (ast: AST.AST): JsonSchema.JsonSchema7 => { + const props = AST.getPropertySignatures(ast) + if (props.length === 0) { + return { + type: "object", + properties: {}, + required: [], + additionalProperties: false + } + } + const $defs = {} + const schema = JsonSchema.fromAST(ast, { + definitions: $defs, + topLevelReferenceStrategy: "skip" + }) + if (Object.keys($defs).length === 0) return schema + ;(schema as any).$defs = $defs + return schema +} + +const resolveResourceContent = ( + uri: string, + content: typeof ReadResourceResult.Type | string | Uint8Array +): typeof ReadResourceResult.Type => { + if (typeof content === "string") { + return { + contents: [ + { + uri, + text: content + } + ] + } + } else if (content instanceof Uint8Array) { + return { + contents: [ + { + uri, + blob: content + } + ] + } + } + return content +} diff --git a/repos/effect/packages/ai/ai/src/Model.ts b/repos/effect/packages/ai/ai/src/Model.ts new file mode 100644 index 0000000..5d5a533 --- /dev/null +++ b/repos/effect/packages/ai/ai/src/Model.ts @@ -0,0 +1,155 @@ +/** + * The `Model` module provides a unified interface for AI service providers. + * + * This module enables creation of provider-specific AI models that can be used + * interchangeably within the Effect AI ecosystem. It combines Layer + * functionality with provider identification, allowing for seamless switching + * between different AI service providers while maintaining type safety. + * + * @example + * ```ts + * import { Model, LanguageModel } from "@effect/ai" + * import { Effect, Layer } from "effect" + * + * declare const myAnthropicLayer: Layer.Layer + * + * const anthropicModel = Model.make("anthropic", myAnthropicLayer) + * + * const program = Effect.gen(function* () { + * const response = yield* LanguageModel.generateText({ + * prompt: "Hello, world!" + * }) + * return response.text + * }).pipe( + * Effect.provide(anthropicModel) + * ) + * ``` + * + * @since 1.0.0 + */ +import * as Context from "effect/Context" +import * as Effect from "effect/Effect" +import { CommitPrototype } from "effect/Effectable" +import { identity } from "effect/Function" +import * as Layer from "effect/Layer" + +/** + * Unique identifier for Model instances. + * + * @since 1.0.0 + * @category Type Ids + */ +export const TypeId = "~@effect/ai/Model" + +/** + * Type-level representation of the Model identifier. + * + * @since 1.0.0 + * @category Type Ids + */ +export type TypeId = typeof TypeId + +/** + * A Model represents a provider-specific AI service. + * + * A Model can be used directly as a Layer to provide a particular model + * implementation to an Effect program. + * + * A Model can also be used as an Effect to "lift" dependencies of the Model + * constructor into the parent Effect. This is particularly useful when you + * want to use a Model from within an Effect service. + * + * @template Provider - String literal type identifying the AI provider. + * @template Provides - Services that this model provides. + * @template Requires - Services that this model requires. + * + * @since 1.0.0 + * @category Models + */ +export interface Model + extends + Layer.Layer, + Effect.Effect, never, Requires> +{ + readonly [TypeId]: TypeId + /** + * The provider identifier (e.g., "openai", "anthropic", "amazon-bedrock"). + */ + readonly provider: Provider +} + +/** + * Service tag that provides the current large language model provider name. + * + * This tag is automatically provided by Model instances and can be used to + * access the name of the provider that is currently in use within a given + * Effect program. + * + * @since 1.0.0 + * @category Context + */ +export class ProviderName extends Context.Tag("@effect/ai/Model/ProviderName")< + ProviderName, + string +>() {} + +const ModelProto = { + ...CommitPrototype, + [TypeId]: TypeId, + [Layer.LayerTypeId]: { + _ROut: identity, + _E: identity, + _RIn: identity + }, + commit(this: Model) { + return Effect.contextWith((context: Context.Context) => { + return Layer.provide(this, Layer.succeedContext(context)) + }) + } +} + +/** + * Creates a Model from a provider name and a Layer that constructs AI services. + * + * @example + * ```ts + * import { Model, LanguageModel } from "@effect/ai" + * import { Effect, Layer } from "effect" + * + * declare const bedrockLayer: Layer.Layer + * + * // Model automatically provides ProviderName service + * const checkProviderAndGenerate = Effect.gen(function* () { + * const provider = yield* Model.ProviderName + * + * console.log(`Generating with: ${provider}`) + * + * return yield* LanguageModel.generateText({ + * prompt: `Hello from ${provider}!` + * }) + * }) + * + * const program = checkProviderAndGenerate.pipe( + * Effect.provide(Model.make("amazon-bedrock", bedrockLayer)) + * ) + * // Will log: "Generating with: amazon-bedrock" + * ``` + * + * @since 1.0.0 + * @category Constructors + */ +export const make = ( + /** + * Provider identifier (e.g., "openai", "anthropic", "amazon-bedrock"). + */ + provider: Provider, + /** + * Layer that provides the AI services for this provider. + */ + layer: Layer.Layer +): Model => + Object.assign( + Object.create(ModelProto), + { provider }, + Layer.merge(Layer.succeed(ProviderName, provider), layer) + ) diff --git a/repos/effect/packages/ai/ai/src/Prompt.ts b/repos/effect/packages/ai/ai/src/Prompt.ts new file mode 100644 index 0000000..40e6bed --- /dev/null +++ b/repos/effect/packages/ai/ai/src/Prompt.ts @@ -0,0 +1,1853 @@ +/** + * The `Prompt` module provides several data structures to simplify creating and + * combining prompts. + * + * This module defines the complete structure of a conversation with a large + * language model, including messages, content parts, and provider-specific + * options. It supports rich content types like text, files, tool calls, and + * reasoning. + * + * @example + * ```ts + * import { Prompt } from "@effect/ai" + * + * // Create a structured conversation + * const conversation = Prompt.make([ + * { + * role: "system", + * content: "You are a helpful assistant specialized in mathematics." + * }, + * { + * role: "user", + * content: [{ + * type: "text", + * text: "What is the derivative of x²?" + * }] + * }, + * { + * role: "assistant", + * content: [{ + * type: "text", + * text: "The derivative of x² is 2x." + * }] + * } + * ]) + * ``` + * + * @example + * ```ts + * import { Prompt } from "@effect/ai" + * + * // Merge multiple prompts + * const systemPrompt = Prompt.make([{ + * role: "system", + * content: "You are a coding assistant." + * }]) + * + * const userPrompt = Prompt.make("Help me write a function") + * + * const combined = Prompt.merge(systemPrompt, userPrompt) + * ``` + * + * @since 1.0.0 + */ +import * as Arbitrary from "effect/Arbitrary" +import * as Arr from "effect/Array" +import { constFalse, dual } from "effect/Function" +import * as ParseResult from "effect/ParseResult" +import { type Pipeable, pipeArguments } from "effect/Pipeable" +import * as Predicate from "effect/Predicate" +import * as Schema from "effect/Schema" +import type * as AST from "effect/SchemaAST" +import type * as Response from "./Response.js" + +const constEmptyObject = () => ({}) + +// ============================================================================= +// Options +// ============================================================================= + +/** + * Schema for provider-specific options which can be attached to both content + * parts and messages, enabling provider-specific behavior. + * + * Provider-specific options are namespaced by provider and have the structure: + * + * ``` + * { + * "": { + * // Provider-specific options + * } + * } + * ``` + * + * @since 1.0.0 + * @category Models + */ +export const ProviderOptions = Schema.Record({ + key: Schema.String, + value: Schema.UndefinedOr( + Schema.Record({ + key: Schema.String, + value: Schema.Unknown + }) + ) +}) + +/** + * @since 1.0.0 + * @category Models + */ +export type ProviderOptions = typeof ProviderOptions.Type + +// ============================================================================= +// Base Part +// ============================================================================= + +/** + * Unique identifier for Part instances. + * + * @since 1.0.0 + * @category Type Ids + */ +export const PartTypeId = "~effect/ai/Prompt/Part" + +/** + * Type-level representation of the Part identifier. + * + * @since 1.0.0 + * @category Type Ids + */ +export type PartTypeId = typeof PartTypeId + +/** + * Type guard to check if a value is a Part. + * + * @since 1.0.0 + * @category Guards + */ +export const isPart = (u: unknown): u is Part => Predicate.hasProperty(u, PartTypeId) + +/** + * Union type representing all possible content parts within messages. + * + * Parts are the building blocks of message content, supporting text, files, + * reasoning, tool calls, and tool results. + * + * @since 1.0.0 + * @category Models + */ +export type Part = TextPart | ReasoningPart | FilePart | ToolCallPart | ToolResultPart + +/** + * Encoded representation of a Part. + * + * @since 1.0.0 + * @category Models + */ +export type PartEncoded = + | TextPartEncoded + | ReasoningPartEncoded + | FilePartEncoded + | ToolCallPartEncoded + | ToolResultPartEncoded + +/** + * Base interface for all content parts. + * + * Provides common structure including type and provider options. + * + * @since 1.0.0 + * @category Models + */ +export interface BasePart { + readonly [PartTypeId]: PartTypeId + /** + * The type of this content part. + */ + readonly type: Type + /** + * Provider-specific options for this part. + */ + readonly options: Options +} + +/** + * Base interface for encoded content parts. + * + * @since 1.0.0 + * @category Models + */ +export interface BasePartEncoded { + /** + * The type of this content part. + */ + readonly type: Type + /** + * Provider-specific options for this part. + */ + readonly options?: Options | undefined +} + +/** + * Creates a new content part of the specified type. + * + * @example + * ```ts + * import { Prompt } from "@effect/ai" + * + * const textPart = Prompt.makePart("text", { + * text: "Hello, world!" + * }) + * + * const filePart = Prompt.makePart("file", { + * mediaType: "image/png", + * fileName: "screenshot.png", + * data: new Uint8Array([1, 2, 3]) + * }) + * ``` + * + * @since 1.0.0 + * @category Constructors + */ +export const makePart = ( + /** + * The type of part to create. + */ + type: Type, + /** + * Parameters specific to the part type being created. + */ + params: Omit, PartTypeId | "type" | "options"> & { + /** + * Optional provider-specific options for this part. + */ + readonly options?: Extract["options"] | undefined + } +): Extract => + ({ + ...params, + [PartTypeId]: PartTypeId, + type, + options: params.options ?? {} + }) as any + +/** + * A utility type for specifying the parameters required to construct a + * specific part of a prompt. + * + * @since 1.0.0 + * @category Utility Types + */ +export type PartConstructorParams

= Omit & { + /** + * Optional provider-specific options for this part. + */ + readonly options?: Part["options"] | undefined +} + +// ============================================================================= +// Text Part +// ============================================================================= + +/** + * Content part representing plain text. + * + * The most basic content type used for textual information in messages. + * + * @example + * ```ts + * import { Prompt } from "@effect/ai" + * + * const textPart: Prompt.TextPart = Prompt.makePart("text", { + * text: "Hello, how can I help you today?", + * }) + * ``` + * + * @since 1.0.0 + * @category Models + */ +export interface TextPart extends BasePart<"text", TextPartOptions> { + /** + * The text content. + */ + readonly text: string +} + +/** + * Encoded representation of text parts for serialization. + * + * @since 1.0.0 + * @category Models + */ +export interface TextPartEncoded extends BasePartEncoded<"text", TextPartOptions> { + /** + * The text content. + */ + readonly text: string +} + +/** + * Represents provider-specific options that can be associated with a + * `TextPart` through module augmentation. + * + * @since 1.0.0 + * @category ProviderOptions + */ +export interface TextPartOptions extends ProviderOptions {} + +/** + * Schema for validation and encoding of text parts. + * + * @since 1.0.0 + * @category Schemas + */ +export const TextPart: Schema.Schema = Schema.Struct({ + type: Schema.Literal("text"), + text: Schema.String, + options: Schema.optionalWith(ProviderOptions, { default: constEmptyObject }) +}).pipe( + Schema.attachPropertySignature(PartTypeId, PartTypeId), + Schema.annotations({ identifier: "TextPart" }) +) + +/** + * Constructs a new text part. + * + * @since 1.0.0 + * @category Constructors + */ +export const textPart = (params: PartConstructorParams): TextPart => makePart("text", params) + +// ============================================================================= +// Reasoning Part +// ============================================================================= + +/** + * Content part representing reasoning or chain-of-thought. + * + * @example + * ```ts + * import { Prompt } from "@effect/ai" + * + * const reasoningPart: Prompt.ReasoningPart = Prompt.makePart("reasoning", { + * text: "Let me think step by step: First I need to understand the user's question...", + * }) + * ``` + * + * @since 1.0.0 + * @category Models + */ +export interface ReasoningPart extends BasePart<"reasoning", ReasoningPartOptions> { + /** + * The reasoning or thought process text. + */ + readonly text: string +} + +/** + * Encoded representation of reasoning parts for serialization. + * + * @since 1.0.0 + * @category Models + */ +export interface ReasoningPartEncoded extends BasePartEncoded<"reasoning", ReasoningPartOptions> { + /** + * The reasoning or thought process text. + */ + readonly text: string +} + +/** + * Represents provider-specific options that can be associated with a + * `ReasoningPart` through module augmentation. + * + * @since 1.0.0 + * @category ProviderOptions + */ +export interface ReasoningPartOptions extends ProviderOptions {} + +/** + * Schema for validation and encoding of reasoning parts. + * + * @since 1.0.0 + * @category Schemas + */ +export const ReasoningPart: Schema.Schema = Schema.Struct({ + type: Schema.Literal("reasoning"), + text: Schema.String, + options: Schema.optionalWith(ProviderOptions, { default: constEmptyObject }) +}).pipe( + Schema.attachPropertySignature(PartTypeId, PartTypeId), + Schema.annotations({ identifier: "ReasoningPart" }) +) + +/** + * Constructs a new reasoning part. + * + * @since 1.0.0 + * @category Constructors + */ +export const reasoningPart = (params: PartConstructorParams): ReasoningPart => + makePart("reasoning", params) + +// ============================================================================= +// File Part +// ============================================================================= + +/** + * Content part representing a file attachment. Files can be provided as base64 + * strings of data, byte arrays, or URLs. + * + * Supports various file types including images, documents, and binary data. + * + * @example + * ```ts + * import { Prompt } from "@effect/ai" + * + * const imagePart: Prompt.FilePart = Prompt.makePart("file", { + * mediaType: "image/jpeg", + * fileName: "photo.jpg", + * data: "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQ..." + * }) + * + * const documentPart: Prompt.FilePart = Prompt.makePart("file", { + * mediaType: "application/pdf", + * fileName: "report.pdf", + * data: new Uint8Array([1, 2, 3]) + * }) + * ``` + * + * @since 1.0.0 + * @category Models + */ +export interface FilePart extends BasePart<"file", FilePartOptions> { + /** + * MIME type of the file (e.g., "image/jpeg", "application/pdf"). + */ + readonly mediaType: string + /** + * Optional filename for the file. + */ + readonly fileName?: string | undefined + /** + * File data as base64 string of data, a byte array, or a URL. + */ + readonly data: string | Uint8Array | URL +} + +/** + * Encoded representation of file parts for serialization. + * + * @since 1.0.0 + * @category Models + */ +export interface FilePartEncoded extends BasePartEncoded<"file", FilePartOptions> { + /** + * MIME type of the file (e.g., "image/jpeg", "application/pdf"). + */ + readonly mediaType: string + /** + * Optional filename for the file. + */ + readonly fileName?: string | undefined + /** + * File data as base64 string of data, a byte array, or a URL. + */ + readonly data: string | Uint8Array | URL +} + +/** + * Represents provider-specific options that can be associated with a + * `FilePart` through module augmentation. + * + * @since 1.0.0 + * @category ProviderOptions + */ +export interface FilePartOptions extends ProviderOptions {} + +/** + * Schema for validation and encoding of file parts. + * + * @since 1.0.0 + * @category Schemas + */ +export const FilePart: Schema.Schema = Schema.Struct({ + type: Schema.Literal("file"), + mediaType: Schema.String, + fileName: Schema.optional(Schema.String), + data: Schema.Union(Schema.String, Schema.Uint8ArrayFromSelf, Schema.URLFromSelf), + options: Schema.optionalWith(ProviderOptions, { default: constEmptyObject }) +}).pipe( + Schema.attachPropertySignature(PartTypeId, PartTypeId), + Schema.annotations({ identifier: "FilePart" }) +) + +/** + * Constructs a new file part. + * + * @since 1.0.0 + * @category Constructors + */ +export const filePart = (params: PartConstructorParams): FilePart => makePart("file", params) + +// ============================================================================= +// Tool Call Part +// ============================================================================= + +/** + * Content part representing a tool call request. + * + * @example + * ```ts + * import { Prompt } from "@effect/ai" + * + * const toolCallPart: Prompt.ToolCallPart = Prompt.makePart("tool-call", { + * id: "call_123", + * name: "get_weather", + * params: { city: "San Francisco", units: "celsius" }, + * providerExecuted: false, + * }) + * ``` + * + * @since 1.0.0 + * @category Models + */ +export interface ToolCallPart extends BasePart<"tool-call", ToolCallPartOptions> { + /** + * Unique identifier for this tool call. + */ + readonly id: string + /** + * Name of the tool to invoke. + */ + readonly name: string + /** + * Parameters to pass to the tool. + */ + readonly params: unknown + /** + * Whether the tool was executed by the provider (true) or framework (false). + */ + readonly providerExecuted: boolean +} + +/** + * Encoded representation of tool call parts for serialization. + * + * @since 1.0.0 + * @category Models + */ +export interface ToolCallPartEncoded extends BasePartEncoded<"tool-call", ToolCallPartOptions> { + /** + * Unique identifier for this tool call. + */ + readonly id: string + /** + * Name of the tool to invoke. + */ + readonly name: string + /** + * Parameters to pass to the tool. + */ + readonly params: unknown + /** + * Whether the tool was executed by the provider (true) or framework (false). + */ + readonly providerExecuted?: boolean | undefined +} + +/** + * Represents provider-specific options that can be associated with a + * `ToolCallPart` through module augmentation. + * + * @since 1.0.0 + * @category ProviderOptions + */ +export interface ToolCallPartOptions extends ProviderOptions {} + +/** + * Schema for validation and encoding of tool call parts. + * + * @since 1.0.0 + * @category Schemas + */ +export const ToolCallPart: Schema.Schema = Schema.Struct({ + type: Schema.Literal("tool-call"), + id: Schema.String, + name: Schema.String, + params: Schema.Unknown, + providerExecuted: Schema.optionalWith(Schema.Boolean, { default: constFalse }), + options: Schema.optionalWith(ProviderOptions, { default: constEmptyObject }) +}).pipe( + Schema.attachPropertySignature(PartTypeId, PartTypeId), + Schema.annotations({ identifier: "ToolCallPart" }) +) + +/** + * Constructs a new tool call part. + * + * @since 1.0.0 + * @category Constructors + */ +export const toolCallPart = (params: PartConstructorParams): ToolCallPart => makePart("tool-call", params) + +// ============================================================================= +// Tool Result Part +// ============================================================================= + +/** + * Content part representing the result of a tool call. + * + * @example + * ```ts + * import { Prompt } from "@effect/ai" + * + * const toolResultPart: Prompt.ToolResultPart = Prompt.makePart("tool-result", { + * id: "call_123", + * name: "get_weather", + * isFailure: false, + * result: { + * temperature: 22, + * condition: "sunny", + * humidity: 65 + * }, + * providerExecuted: false + * }) + * ``` + * + * @since 1.0.0 + * @category Models + */ +export interface ToolResultPart extends BasePart<"tool-result", ToolResultPartOptions> { + /** + * Unique identifier matching the original tool call. + */ + readonly id: string + /** + * Name of the tool that was executed. + */ + readonly name: string + /** + * Whether or not the result of executing the tool call handler was an error. + */ + readonly isFailure: boolean + /** + * The result returned by the tool execution. + */ + readonly result: unknown + /** + * Whether the tool was executed by the provider (true) or framework (false). + */ + readonly providerExecuted: boolean +} + +/** + * Encoded representation of tool result parts for serialization. + * + * @since 1.0.0 + * @category Models + */ +export interface ToolResultPartEncoded extends BasePartEncoded<"tool-result", ToolResultPartOptions> { + /** + * Unique identifier matching the original tool call. + */ + readonly id: string + /** + * Name of the tool that was executed. + */ + readonly name: string + /** + * Whether or not the result of executing the tool call handler was an error. + */ + readonly isFailure: boolean + /** + * The result returned by the tool execution. + */ + readonly result: unknown + /** + * Whether the tool was executed by the provider (true) or framework (false). + */ + readonly providerExecuted: boolean +} + +/** + * Represents provider-specific options that can be associated with a + * `ToolResultPart` through module augmentation. + * + * @since 1.0.0 + * @category ProviderOptions + */ +export interface ToolResultPartOptions extends ProviderOptions {} + +/** + * Schema for validation and encoding of tool result parts. + * + * @since 1.0.0 + * @category Schemas + */ +export const ToolResultPart: Schema.Schema = Schema.Struct({ + type: Schema.Literal("tool-result"), + id: Schema.String, + name: Schema.String, + isFailure: Schema.Boolean, + result: Schema.Unknown, + providerExecuted: Schema.Boolean, + options: Schema.optionalWith(ProviderOptions, { default: constEmptyObject }) +}).pipe( + Schema.attachPropertySignature(PartTypeId, PartTypeId), + Schema.annotations({ identifier: "ToolResultPart" }) +) + +/** + * Constructs a new tool result part. + * + * @since 1.0.0 + * @category Constructors + */ +export const toolResultPart = (params: PartConstructorParams): ToolResultPart => + makePart("tool-result", params) + +// ============================================================================= +// Base Message +// ============================================================================= + +/** + * Unique identifier for Message instances. + * + * @since 1.0.0 + * @category Type Ids + */ +export const MessageTypeId = "~effect/ai/Prompt/Message" + +/** + * Type-level representation of the Message identifier. + * + * @since 1.0.0 + * @category Type Ids + */ +export type MessageTypeId = typeof MessageTypeId + +/** + * Type guard to check if a value is a Message. + * + * @since 1.0.0 + * @category Guards + */ +export const isMessage = (u: unknown): u is Message => Predicate.hasProperty(u, MessageTypeId) + +/** + * Base interface for all message types. + * + * Provides common structure including role and provider options. + * + * @since 1.0.0 + * @category Models + */ +export interface BaseMessage { + readonly [MessageTypeId]: MessageTypeId + /** + * The role of the message participant. + */ + readonly role: Role + /** + * Provider-specific options for this message. + */ + readonly options: Options +} + +/** + * Base interface for encoded message types. + * + * @template Role - String literal type for the message role + * + * @since 1.0.0 + * @category Models + */ +export interface BaseMessageEncoded { + /** + * The role of the message participant. + */ + readonly role: Role + /** + * Provider-specific options for this message. + */ + readonly options?: Options | undefined +} + +/** + * Creates a new message with the specified role. + * + * @example + * ```ts + * import { Prompt } from "@effect/ai" + * + * const textPart = Prompt.makePart("text", { + * text: "Hello, world!" + * }) + * + * const filePart = Prompt.makeMessage("user", { + * content: [textPart] + * }) + * ``` + * + * @since 1.0.0 + * @category Constructors + */ +export const makeMessage = ( + role: Role, + params: Omit, MessageTypeId | "role" | "options"> & { + readonly options?: Extract["options"] + } +): Extract => + ({ + ...params, + [MessageTypeId]: MessageTypeId, + role, + options: params.options ?? {} + }) as any + +/** + * A utility type for specifying the parameters required to construct a + * specific message for a prompt. + * + * @since 1.0.0 + * @category Utility Types + */ +export type MessageConstructorParams = Omit & { + /** + * Optional provider-specific options for this message. + */ + readonly options?: Part["options"] | undefined +} + +/** + * Schema for decoding message content (i.e. an array containing a single + * `TextPart`) from a string. + * + * @since 1.0.0 + * @category Schemas + */ +export const MessageContentFromString: Schema.Schema< + Arr.NonEmptyReadonlyArray, + string +> = Schema.transform(Schema.String, Schema.NonEmptyArray(Schema.typeSchema(TextPart)), { + strict: true, + decode: (text) => Arr.of(makePart("text", { text })), + encode: (content) => content[0].text +}) + +// ============================================================================= +// System Message +// ============================================================================= + +/** + * Message representing system instructions or context. + * + * @example + * ```ts + * import { Prompt } from "@effect/ai" + * + * const systemMessage: Prompt.SystemMessage = Prompt.makeMessage("system", { + * content: "You are a helpful assistant specialized in mathematics. " + + * "Always show your work step by step." + * }) + * ``` + * + * @since 1.0.0 + * @category Models + */ +export interface SystemMessage extends BaseMessage<"system", SystemMessageOptions> { + /** + * The system instruction or context as plain text. + */ + readonly content: string +} + +/** + * Encoded representation of system messages for serialization. + * + * @since 1.0.0 + * @category Models + */ +export interface SystemMessageEncoded extends BaseMessageEncoded<"system", SystemMessageOptions> { + /** + * The system instruction or context as plain text. + */ + readonly content: string +} + +/** + * Represents provider-specific options that can be associated with a + * `SystemMessage` through module augmentation. + * + * @since 1.0.0 + * @category ProviderOptions + */ +export interface SystemMessageOptions extends ProviderOptions {} + +/** + * Schema for validation and encoding of system messages. + * + * @since 1.0.0 + * @category Schemas + */ +export const SystemMessage: Schema.Schema = Schema.Struct({ + role: Schema.Literal("system"), + content: Schema.String, + options: Schema.optionalWith(ProviderOptions, { default: constEmptyObject }) +}).pipe( + Schema.attachPropertySignature(MessageTypeId, MessageTypeId), + Schema.annotations({ identifier: "SystemMessage" }) +) + +/** + * Constructs a new system message. + * + * @since 1.0.0 + * @category Constructors + */ +export const systemMessage = (params: MessageConstructorParams): SystemMessage => + makeMessage("system", params) + +// ============================================================================= +// User Message +// ============================================================================= + +/** + * Message representing user input or questions. + * + * @example + * ```ts + * import { Prompt } from "@effect/ai" + * + * const textUserMessage: Prompt.UserMessage = Prompt.makeMessage("user", { + * content: [ + * Prompt.makePart("text", { + * text: "Can you analyze this image for me?" + * }) + * ] + * }) + * + * const multimodalUserMessage: Prompt.UserMessage = Prompt.makeMessage("user", { + * content: [ + * Prompt.makePart("text", { + * text: "What do you see in this image?" + * }), + * Prompt.makePart("file", { + * mediaType: "image/jpeg", + * fileName: "vacation.jpg", + * data: "data:image/jpeg;base64,..." + * }) + * ] + * }) + * ``` + * + * @since 1.0.0 + * @category Models + */ +export interface UserMessage extends BaseMessage<"user", UserMessageOptions> { + /** + * Array of content parts that make up the user's message. + */ + readonly content: ReadonlyArray +} + +/** + * Union type of content parts allowed in user messages. + * + * @since 1.0.0 + * @category Models + */ +export type UserMessagePart = TextPart | FilePart + +/** + * Encoded representation of user messages for serialization. + * + * @since 1.0.0 + * @category Models + */ +export interface UserMessageEncoded extends BaseMessageEncoded<"user", UserMessageOptions> { + /** + * Array of content parts that make up the user's message. + */ + readonly content: string | ReadonlyArray +} + +/** + * Union type of encoded content parts for user messages. + * + * @since 1.0.0 + * @category Models + */ +export type UserMessagePartEncoded = TextPartEncoded | FilePartEncoded + +/** + * Represents provider-specific options that can be associated with a + * `UserMessage` through module augmentation. + * + * @since 1.0.0 + * @category ProviderOptions + */ +export interface UserMessageOptions extends ProviderOptions {} + +/** + * Schema for validation and encoding of user messages. + * + * @since 1.0.0 + * @category Schemas + */ +export const UserMessage: Schema.Schema = Schema.Struct({ + role: Schema.Literal("user"), + content: Schema.Union( + MessageContentFromString, + Schema.Array(Schema.Union(TextPart, FilePart)) + ), + options: Schema.optionalWith(ProviderOptions, { default: constEmptyObject }) +}).pipe( + Schema.attachPropertySignature(MessageTypeId, MessageTypeId), + Schema.annotations({ identifier: "UserMessage" }) +) + +/** + * Constructs a new user message. + * + * @since 1.0.0 + * @category Constructors + */ +export const userMessage = (params: MessageConstructorParams): UserMessage => makeMessage("user", params) + +// ============================================================================= +// Assistant Message +// ============================================================================= + +/** + * Message representing large language model assistant responses. + * + * @example + * ```ts + * import { Prompt } from "@effect/ai" + * + * const assistantMessage: Prompt.AssistantMessage = Prompt.makeMessage("assistant", { + * content: [ + * Prompt.makePart("text", { + * text: "The user is asking about the weather. I should use the weather tool." + * }), + * Prompt.makePart("tool-call", { + * id: "call_123", + * name: "get_weather", + * params: { city: "San Francisco" }, + * providerExecuted: false + * }), + * Prompt.makePart("tool-result", { + * id: "call_123", + * name: "get_weather", + * isFailure: false, + * result: { + * temperature: 72, + * condition: "sunny" + * }, + * providerExecuted: false + * }), + * Prompt.makePart("text", { + * text: "The weather in San Francisco is currently 72°F and sunny." + * }) + * ] + * }) + * ``` + * + * @since 1.0.0 + * @category Models + */ +export interface AssistantMessage extends BaseMessage<"assistant", AssistantMessageOptions> { + /** + * Array of content parts that make up the assistant's response. + */ + readonly content: ReadonlyArray +} + +/** + * Union type of content parts allowed in assistant messages. + * + * @since 1.0.0 + * @category Models + */ +export type AssistantMessagePart = + | TextPart + | FilePart + | ReasoningPart + | ToolCallPart + | ToolResultPart + +/** + * Encoded representation of assistant messages for serialization. + * + * @since 1.0.0 + * @category Models + */ +export interface AssistantMessageEncoded extends BaseMessageEncoded<"assistant", AssistantMessageOptions> { + readonly content: string | ReadonlyArray +} + +/** + * Union type of encoded content parts for assistant messages. + * + * @since 1.0.0 + * @category Models + */ +export type AssistantMessagePartEncoded = + | TextPartEncoded + | FilePartEncoded + | ReasoningPartEncoded + | ToolCallPartEncoded + | ToolResultPartEncoded + +/** + * Represents provider-specific options that can be associated with a + * `AssistantMessage` through module augmentation. + * + * @since 1.0.0 + * @category ProviderOptions + */ +export interface AssistantMessageOptions extends ProviderOptions {} + +/** + * Schema for validation and encoding of assistant messages. + * + * @since 1.0.0 + * @category Schemas + */ +export const AssistantMessage: Schema.Schema = Schema.Struct({ + role: Schema.Literal("assistant"), + content: Schema.Union( + MessageContentFromString, + Schema.Array(Schema.Union(TextPart, FilePart, ReasoningPart, ToolCallPart, ToolResultPart)) + ), + options: Schema.optionalWith(ProviderOptions, { default: constEmptyObject }) +}).pipe( + Schema.attachPropertySignature(MessageTypeId, MessageTypeId), + Schema.annotations({ identifier: "AssistantMessage" }) +) + +/** + * Constructs a new assistant message. + * + * @since 1.0.0 + * @category Constructors + */ +export const assistantMessage = (params: MessageConstructorParams): AssistantMessage => + makeMessage("assistant", params) + +// ============================================================================= +// Tool Message +// ============================================================================= + +/** + * Message representing tool execution results. + * + * @example + * ```ts + * import { Prompt } from "@effect/ai" + * + * const toolMessage: Prompt.ToolMessage = Prompt.makeMessage("tool", { + * content: [ + * Prompt.makePart("tool-result", { + * id: "call_123", + * name: "search_web", + * isFailure: false, + * result: { + * query: "TypeScript best practices", + * results: [ + * { title: "TypeScript Handbook", url: "https://..." }, + * { title: "Effective TypeScript", url: "https://..." } + * ] + * }, + * providerExecuted: false + * }) + * ] + * }) + * ``` + * + * @since 1.0.0 + * @category Models + */ +export interface ToolMessage extends BaseMessage<"tool", ToolMessageOptions> { + /** + * Array of tool result parts. + */ + readonly content: ReadonlyArray +} + +/** + * Union type of content parts allowed in tool messages. + * + * @since 1.0.0 + * @category Models + */ +export type ToolMessagePart = ToolResultPart + +/** + * Encoded representation of tool messages for serialization. + * + * @since 1.0.0 + * @category Models + */ +export interface ToolMessageEncoded extends BaseMessageEncoded<"tool", ToolMessageOptions> { + /** + * Array of tool result parts. + */ + readonly content: ReadonlyArray +} + +/** + * Union type of encoded content parts for tool messages. + * + * @since 1.0.0 + * @category Models + */ +export type ToolMessagePartEncoded = ToolResultPartEncoded + +/** + * Represents provider-specific options that can be associated with a + * `ToolMessage` through module augmentation. + * + * @since 1.0.0 + * @category ProviderOptions + */ +export interface ToolMessageOptions extends ProviderOptions {} + +/** + * Schema for validation and encoding of tool messages. + * + * @since 1.0.0 + * @category Schemas + */ +export const ToolMessage: Schema.Schema = Schema.Struct({ + role: Schema.Literal("tool"), + content: Schema.Array(ToolResultPart), + options: Schema.optionalWith(ProviderOptions, { default: constEmptyObject }) +}).pipe( + Schema.attachPropertySignature(MessageTypeId, MessageTypeId), + Schema.annotations({ identifier: "ToolMessage" }) +) + +/** + * Constructs a new tool message. + * + * @since 1.0.0 + * @category Constructors + */ +export const toolMessage = (params: MessageConstructorParams): ToolMessage => makeMessage("tool", params) + +// ============================================================================= +// Message +// ============================================================================= + +/** + * A type representing all possible message types in a conversation. + * + * @since 1.0.0 + * @category Models + */ +export type Message = + | SystemMessage + | UserMessage + | AssistantMessage + | ToolMessage + +/** + * A type representing all possible encoded message types for serialization. + * + * @since 1.0.0 + * @category Models + */ +export type MessageEncoded = + | SystemMessageEncoded + | UserMessageEncoded + | AssistantMessageEncoded + | ToolMessageEncoded + +/** + * Schema for validation and encoding of messages. + * + * @since 1.0.0 + * @category Schemas + */ +export const Message: Schema.Schema = Schema.Union( + SystemMessage, + UserMessage, + AssistantMessage, + ToolMessage +) + +// ============================================================================= +// Prompt +// ============================================================================= + +/** + * Unique identifier for Prompt instances. + * + * @since 1.0.0 + * @category Type Ids + */ +export const TypeId = "~@effect/ai/Prompt" + +/** + * Type-level representation of the Prompt identifier. + * + * @since 1.0.0 + * @category Type Ids + */ +export type TypeId = typeof TypeId + +/** + * Type guard to check if a value is a Prompt. + * + * @since 1.0.0 + * @category Guards + */ +export const isPrompt = (u: unknown): u is Prompt => Predicate.hasProperty(u, TypeId) + +/** + * A Prompt contains a sequence of messages that form the context of a + * conversation with a large language model. + * + * @since 1.0.0 + * @category Models + */ +export interface Prompt extends Pipeable { + readonly [TypeId]: TypeId + /** + * Array of messages that make up the conversation. + */ + readonly content: ReadonlyArray +} + +/** + * Encoded representation of prompts for serialization. + * + * @since 1.0.0 + * @category Models + */ +export interface PromptEncoded { + /** + * Array of messages that make up the conversation. + */ + readonly content: ReadonlyArray +} + +/** + * Describes a schema that represents a `Prompt` instance. + * + * @since 1.0.0 + * @category Schemas + */ +export class PromptFromSelf extends Schema.declare( + (u) => isPrompt(u), + { + typeConstructor: { _tag: "effect/ai/Prompt" }, + identifier: "PromptFromSelf", + description: "a Prompt instance", + arbitrary: (): Arbitrary.LazyArbitrary => (fc) => + fc.array( + Arbitrary.makeLazy(Message)(fc) + ).map(makePrompt) + } +) {} + +/** + * Schema for validation and encoding of prompts. + * + * @since 1.0.0 + * @category Schemas + */ +export const Prompt: Schema.Schema = Schema.transformOrFail( + Schema.Struct({ content: Schema.Array(Schema.encodedSchema(Message)) }), + PromptFromSelf, + { + strict: true, + decode: (i, _, ast) => decodePrompt(i, ast), + encode: (a, _, ast) => encodePrompt(a, ast) + } +).annotations({ identifier: "Prompt" }) + +const decodeMessages = ParseResult.decodeEither(Schema.Array(Message)) +const encodeMessages = ParseResult.encodeEither(Schema.Array(Message)) + +const decodePrompt = (input: PromptEncoded, ast: AST.AST) => + ParseResult.mapBoth(decodeMessages(input.content), { + onFailure: () => new ParseResult.Type(ast, input, `Unable to decode ${JSON.stringify(input)} into a Prompt`), + onSuccess: makePrompt + }) + +const encodePrompt = (input: Prompt, ast: AST.AST) => + ParseResult.mapBoth(encodeMessages(input.content), { + onFailure: () => new ParseResult.Type(ast, input, `Failed to encode Prompt`), + onSuccess: (messages) => ({ content: messages }) + }) + +/** + * Schema for parsing a Prompt from JSON strings. + * + * @since 1.0.0 + * @category Schemas + */ +export const FromJson = Schema.parseJson(Prompt) + +/** + * Raw input types that can be converted into a Prompt. + * + * Supports various input formats for convenience, including simple strings, + * message arrays, response parts, and existing prompts. + * + * @example + * ```ts + * import { Prompt } from "@effect/ai" + * + * // String input - creates a user message + * const stringInput: Prompt.RawInput = "Hello, world!" + * + * // Message array input + * const messagesInput: Prompt.RawInput = [ + * { role: "system", content: "You are helpful." }, + * { role: "user", content: [{ type: "text", text: "Hi!" }] } + * ] + * + * // Existing prompt + * declare const existingPrompt: Prompt.Prompt + * const promptInput: Prompt.RawInput = existingPrompt + * ``` + * + * @since 1.0.0 + * @category Models + */ +export type RawInput = + | string + | Iterable + | Prompt + +const Proto = { + [TypeId]: TypeId, + pipe() { + return pipeArguments(this, arguments) + } +} + +const makePrompt = (content: ReadonlyArray): Prompt => + Object.assign(Object.create(Proto), { + content + }) + +const decodeMessagesSync = Schema.decodeSync(Schema.Array(Message)) + +/** + * An empty prompt with no messages. + * + * @example + * ```ts + * import { Prompt } from "@effect/ai" + * + * const emptyPrompt = Prompt.empty + * console.log(emptyPrompt.content) // [] + * ``` + * + * @since 1.0.0 + * @category Constructors + */ +export const empty: Prompt = makePrompt([]) + +/** + * Creates a Prompt from an input. + * + * This is the primary constructor for creating prompts, supporting multiple + * input formats for convenience and flexibility. + * + * @example + * ```ts + * import { Prompt } from "@effect/ai" + * + * // From string - creates a user message + * const textPrompt = Prompt.make("Hello, how are you?") + * + * // From messages array + * const structuredPrompt = Prompt.make([ + * { role: "system", content: "You are a helpful assistant." }, + * { role: "user", content: [{ type: "text", text: "Hi!" }] } + * ]) + * + * // From existing prompt + * declare const existingPrompt: Prompt.Prompt + * const copiedPrompt = Prompt.make(existingPrompt) + * ``` + * + * @since 1.0.0 + * @category Constructors + */ +export const make = (input: RawInput): Prompt => { + if (Predicate.isString(input)) { + const part = makePart("text", { text: input }) + const message = makeMessage("user", { content: [part] }) + return makePrompt([message]) + } + + if (Predicate.isIterable(input)) { + return makePrompt(decodeMessagesSync(Arr.fromIterable(input), { + errors: "all" + })) + } + + return input +} + +/** + * Creates a Prompt from an array of messages. + * + * @example + * ```ts + * import { Prompt } from "@effect/ai" + * + * const messages: ReadonlyArray = [ + * Prompt.makeMessage("system", { + * content: "You are a coding assistant." + * }), + * Prompt.makeMessage("user", { + * content: [Prompt.makePart("text", { text: "Help me with TypeScript" })] + * }) + * ] + * + * const prompt = Prompt.fromMessages(messages) + * ``` + * + * @since 1.0.0 + * @category Constructors + */ +export const fromMessages = (messages: ReadonlyArray): Prompt => makePrompt(messages) + +/** + * Creates a Prompt from the response parts of a previous interaction with a + * large language model. + * + * Converts streaming or non-streaming AI response parts into a structured + * prompt, typically for use in conversation history or further processing. + * + * @example + * ```ts + * import { Either } from "effect" + * import { Prompt, Response } from "@effect/ai" + * + * const responseParts: ReadonlyArray = [ + * Response.makePart("text", { + * text: "Hello there!" + * }), + * Response.makePart("tool-call", { + * id: "call_1", + * name: "get_time", + * params: {}, + * providerExecuted: false + * }), + * Response.makePart("tool-result", { + * id: "call_1", + * name: "get_time", + * isFailure: false, + * result: "10:30 AM", + * encodedResult: "10:30 AM", + * providerExecuted: false + * }) + * ] + * + * const prompt = Prompt.fromResponseParts(responseParts) + * // Creates an assistant message with the response content + * ``` + * + * @since 1.0.0 + * @category Constructors + */ +export const fromResponseParts = (parts: ReadonlyArray): Prompt => { + if (parts.length === 0) { + return empty + } + + const assistantParts: Array = [] + const toolParts: Array = [] + + const activeTextDeltas = new Map() + const activeReasoningDeltas = new Map() + + for (const part of parts) { + switch (part.type) { + // Text Parts + case "text": { + assistantParts.push(makePart("text", { text: part.text })) + break + } + + // Text Parts (streaming) + case "text-start": { + activeTextDeltas.set(part.id, { text: "" }) + break + } + case "text-delta": { + if (activeTextDeltas.has(part.id)) { + activeTextDeltas.get(part.id)!.text += part.delta + } + break + } + case "text-end": { + if (activeTextDeltas.has(part.id)) { + assistantParts.push(makePart("text", activeTextDeltas.get(part.id)!)) + } + break + } + + // Reasoning Parts + case "reasoning": { + assistantParts.push(makePart("reasoning", { text: part.text })) + break + } + + // Reasoning Parts (streaming) + case "reasoning-start": { + activeReasoningDeltas.set(part.id, { text: "" }) + break + } + case "reasoning-delta": { + if (activeReasoningDeltas.has(part.id)) { + activeReasoningDeltas.get(part.id)!.text += part.delta + } + break + } + case "reasoning-end": { + if (activeReasoningDeltas.has(part.id)) { + assistantParts.push(makePart("reasoning", activeReasoningDeltas.get(part.id)!)) + } + break + } + + // Tool Call Parts + case "tool-call": { + assistantParts.push(makePart("tool-call", { + id: part.id, + name: part.providerName ?? part.name, + params: part.params, + providerExecuted: part.providerExecuted ?? false + })) + break + } + + // Tool Result Parts + case "tool-result": { + const toolPart = makePart("tool-result", { + id: part.id, + name: part.providerName ?? part.name, + isFailure: part.isFailure, + result: part.encodedResult, + providerExecuted: part.providerExecuted ?? false + }) + if (part.providerExecuted) { + assistantParts.push(toolPart) + } else { + toolParts.push(toolPart) + } + } + } + } + + if (assistantParts.length === 0 && toolParts.length === 0) { + return empty + } + + const messages: Array = [] + + if (assistantParts.length > 0) { + messages.push(makeMessage("assistant", { content: assistantParts })) + } + + if (toolParts.length > 0) { + messages.push(makeMessage("tool", { content: toolParts })) + } + + return makePrompt(messages) +} + +// ============================================================================= +// Merging Prompts +// ============================================================================= + +/** + * Merges a prompt with additional raw input by concatenating messages. + * + * Creates a new prompt containing all messages from both the original prompt, + * and the provided raw input, maintaining the order of messages. + * + * @example + * ```ts + * import { Prompt } from "@effect/ai" + * + * const systemPrompt = Prompt.make([{ + * role: "system", + * content: "You are a helpful assistant." + * }]) + * + * const merged = Prompt.merge(systemPrompt, "Hello, world!") + * ``` + * + * @since 1.0.0 + * @category Combinators + */ +export const merge: { + (input: RawInput): (self: Prompt) => Prompt + (self: Prompt, input: RawInput): Prompt +} = dual(2, (self: Prompt, input: RawInput): Prompt => { + const other = make(input) + if (self.content.length === 0) { + return other + } + if (other.content.length === 0) { + return self + } + return fromMessages([...self.content, ...other.content]) +}) + +// ============================================================================= +// Manipulating Prompts +// ============================================================================= + +/** + * Creates a new prompt from the specified prompt with the system message set + * to the specified text content. + * + * **NOTE**: This method will remove and replace any previous system message + * from the prompt. + * + * @example + * ```ts + * import { Prompt } from "@effect/ai" + * + * const systemPrompt = Prompt.make([{ + * role: "system", + * content: "You are a helpful assistant." + * }]) + * + * const userPrompt = Prompt.make("Hello, world!") + * + * const prompt = Prompt.merge(systemPrompt, userPrompt) + * + * const replaced = Prompt.setSystem( + * prompt, + * "You are an expert in programming" + * ) + * ``` + * + * @since 1.0.0 + * @category Combinators + */ +export const setSystem: { + (content: string): (self: Prompt) => Prompt + (self: Prompt, content: string): Prompt +} = dual(2, (self: Prompt, content: string): Prompt => { + const messages: Array = [makeMessage("system", { content })] + for (const message of self.content) { + if (message.role !== "system") { + messages.push(message) + } + } + return makePrompt(messages) +}) + +/** + * Creates a new prompt from the specified prompt with the provided text content + * prepended to the start of existing system message content. + * + * If no system message exists in the specified prompt, the provided content + * will be used to create a system message. + * + * @example + * ```ts + * import { Prompt } from "@effect/ai" + * + * const systemPrompt = Prompt.make([{ + * role: "system", + * content: "You are an expert in programming." + * }]) + * + * const userPrompt = Prompt.make("Hello, world!") + * + * const prompt = Prompt.merge(systemPrompt, userPrompt) + * + * const replaced = Prompt.prependSystem( + * prompt, + * "You are a helpful assistant. " + * ) + * // result content: "You are a helpful assistant. You are an expert in programming." + * ``` + * + * @since 1.0.0 + * @category Combinators + */ +export const prependSystem: { + (content: string): (self: Prompt) => Prompt + (self: Prompt, content: string): Prompt +} = dual(2, (self: Prompt, content: string): Prompt => { + let system: SystemMessage | undefined = undefined + for (const message of self.content) { + if (message.role === "system") { + system = makeMessage("system", { + content: content + message.content + }) + break + } + } + if (Predicate.isUndefined(system)) { + system = makeMessage("system", { content }) + } + return makePrompt([system, ...self.content]) +}) + +/** + * Creates a new prompt from the specified prompt with the provided text content + * appended to the end of existing system message content. + * + * If no system message exists in the specified prompt, the provided content + * will be used to create a system message. + * + * @example + * ```ts + * import { Prompt } from "@effect/ai" + * + * const systemPrompt = Prompt.make([{ + * role: "system", + * content: "You are an expert in programming." + * }]) + * + * const userPrompt = Prompt.make("Hello, world!") + * + * const prompt = Prompt.merge(systemPrompt, userPrompt) + * + * const replaced = Prompt.appendSystem( + * prompt, + * " You are a helpful assistant." + * ) + * // result content: "You are an expert in programming. You are a helpful assistant." + * ``` + * + * @since 1.0.0 + * @category Combinators + */ +export const appendSystem: { + (content: string): (self: Prompt) => Prompt + (self: Prompt, content: string): Prompt +} = dual(2, (self: Prompt, content: string): Prompt => { + let system: SystemMessage | undefined = undefined + for (const message of self.content) { + if (message.role === "system") { + system = makeMessage("system", { + content: message.content + content + }) + break + } + } + if (Predicate.isUndefined(system)) { + system = makeMessage("system", { content }) + } + return makePrompt([system, ...self.content]) +}) diff --git a/repos/effect/packages/ai/ai/src/Response.ts b/repos/effect/packages/ai/ai/src/Response.ts new file mode 100644 index 0000000..2966f94 --- /dev/null +++ b/repos/effect/packages/ai/ai/src/Response.ts @@ -0,0 +1,2370 @@ +/** + * The `Response` module provides data structures to represent responses from + * large language models. + * + * This module defines the complete structure of AI model responses, including + * various content parts for text, reasoning, tool calls, files, and metadata, + * supporting both streaming and non-streaming responses. + * + * @example + * ```ts + * import { Response } from "@effect/ai" + * + * // Create a simple text response part + * const textResponse = Response.makePart("text", { + * text: "The weather is sunny today!" + * }) + * + * // Create a tool call response part + * const toolCallResponse = Response.makePart("tool-call", { + * id: "call_123", + * name: "get_weather", + * params: { city: "San Francisco" }, + * providerExecuted: false + * }) + * ``` + * + * @since 1.0.0 + */ +import type * as DateTime from "effect/DateTime" +import * as Effect from "effect/Effect" +import { constFalse } from "effect/Function" +import type * as Option from "effect/Option" +import * as ParseResult from "effect/ParseResult" +import * as Predicate from "effect/Predicate" +import * as Schema from "effect/Schema" +import type * as Tool from "./Tool.js" +import type * as Toolkit from "./Toolkit.js" + +const constEmptyObject = () => ({}) + +// ============================================================================= +// All Parts +// ============================================================================= + +/** + * Unique identifier for Response Part instances. + * + * @since 1.0.0 + * @category Type Ids + */ +export const PartTypeId = "~effect/ai/Content/Part" + +/** + * Type-level representation of the Response Part identifier. + * + * @since 1.0.0 + * @category Type Ids + */ +export type PartTypeId = typeof PartTypeId + +/** + * Type guard to check if a value is a Response Part. + * + * @since 1.0.0 + * @category Guards + */ +export const isPart = (u: unknown): u is AnyPart => Predicate.hasProperty(u, PartTypeId) + +/** + * Union type representing all possible response content parts. + * + * @since 1.0.0 + * @category Models + */ +export type AnyPart = + | TextPart + | TextStartPart + | TextDeltaPart + | TextEndPart + | ReasoningPart + | ReasoningStartPart + | ReasoningDeltaPart + | ReasoningEndPart + | ToolParamsStartPart + | ToolParamsDeltaPart + | ToolParamsEndPart + | ToolCallPart + | ToolResultPart + | FilePart + | DocumentSourcePart + | UrlSourcePart + | ResponseMetadataPart + | FinishPart + | ErrorPart + +/** + * Encoded representation of all possible response content parts for serialization. + * + * @since 1.0.0 + * @category Models + */ +export type AnyPartEncoded = + | TextPartEncoded + | TextStartPartEncoded + | TextDeltaPartEncoded + | TextEndPartEncoded + | ReasoningPartEncoded + | ReasoningStartPartEncoded + | ReasoningDeltaPartEncoded + | ReasoningEndPartEncoded + | ToolParamsStartPartEncoded + | ToolParamsDeltaPartEncoded + | ToolParamsEndPartEncoded + | ToolCallPartEncoded + | ToolResultPartEncoded + | FilePartEncoded + | DocumentSourcePartEncoded + | UrlSourcePartEncoded + | ResponseMetadataPartEncoded + | FinishPartEncoded + | ErrorPartEncoded + +/** + * Union type for all response parts with tool-specific typing. + * + * @since 1.0.0 + * @category Models + */ +export type AllParts> = + | TextPart + | TextStartPart + | TextDeltaPart + | TextEndPart + | ReasoningPart + | ReasoningStartPart + | ReasoningDeltaPart + | ReasoningEndPart + | ToolParamsStartPart + | ToolParamsDeltaPart + | ToolParamsEndPart + | ToolCallParts + | ToolResultParts + | FilePart + | DocumentSourcePart + | UrlSourcePart + | ResponseMetadataPart + | FinishPart + | ErrorPart + +/** + * Encoded representation of all response parts for serialization. + * + * @since 1.0.0 + * @category Models + */ +export type AllPartsEncoded = + | TextPartEncoded + | TextStartPartEncoded + | TextDeltaPartEncoded + | TextEndPartEncoded + | ReasoningPartEncoded + | ReasoningStartPartEncoded + | ReasoningDeltaPartEncoded + | ReasoningEndPartEncoded + | ToolParamsStartPartEncoded + | ToolParamsDeltaPartEncoded + | ToolParamsEndPartEncoded + | ToolCallPartEncoded + | ToolResultPartEncoded + | FilePartEncoded + | DocumentSourcePartEncoded + | UrlSourcePartEncoded + | ResponseMetadataPartEncoded + | FinishPartEncoded + | ErrorPartEncoded + +/** + * Creates a Schema for all response parts based on a toolkit. + * + * Generates a schema that includes all possible response parts, with tool call + * and tool result parts dynamically created based on the provided toolkit. + * + * @example + * ```ts + * import { Response, Tool, Toolkit } from "@effect/ai" + * import { Schema } from "effect" + * + * const myToolkit = Toolkit.make( + * Tool.make("GetWeather", { + * parameters: { city: Schema.String }, + * success: Schema.Struct({ temperature: Schema.Number }) + * }) + * ) + * + * const allPartsSchema = Response.AllParts(myToolkit) + * ``` + * + * @since 1.0.0 + * @category Schemas + */ +export const AllParts = >( + toolkit: T +): Schema.Schema : Toolkit.WithHandlerTools>, AllPartsEncoded> => { + const toolCalls: Array, ToolCallPartEncoded>> = [] + const toolCallResults: Array, ToolResultPartEncoded>> = [] + for (const tool of Object.values(toolkit.tools as Record)) { + toolCalls.push(ToolCallPart(tool.name, tool.parametersSchema as any)) + toolCallResults.push(ToolResultPart(tool.name, tool.successSchema, tool.failureSchema)) + } + return Schema.Union( + TextPart, + TextStartPart, + TextDeltaPart, + TextEndPart, + ReasoningPart, + ReasoningStartPart, + ReasoningDeltaPart, + ReasoningEndPart, + ToolParamsStartPart, + ToolParamsDeltaPart, + ToolParamsEndPart, + FilePart, + DocumentSourcePart, + UrlSourcePart, + ResponseMetadataPart, + FinishPart, + ErrorPart, + ...toolCalls, + ...toolCallResults + ) as any +} + +// ============================================================================= +// Generate Parts +// ============================================================================= + +/** + * A type for representing non-streaming response parts with tool-specific + * typing. + * + * @template Tools - Record of tools with their schemas + * + * @since 1.0.0 + * @category Models + */ +export type Part> = + | TextPart + | ReasoningPart + | ToolCallParts + | ToolResultParts + | FilePart + | DocumentSourcePart + | UrlSourcePart + | ResponseMetadataPart + | FinishPart + +/** + * Encoded representation of non-streaming response parts for serialization. + * + * @since 1.0.0 + * @category Models + */ +export type PartEncoded = + | TextPartEncoded + | ReasoningPartEncoded + | ReasoningDeltaPartEncoded + | ReasoningEndPartEncoded + | ToolCallPartEncoded + | ToolResultPartEncoded + | FilePartEncoded + | DocumentSourcePartEncoded + | UrlSourcePartEncoded + | ResponseMetadataPartEncoded + | FinishPartEncoded + +/** + * Creates a Schema for non-streaming response parts based on a toolkit. + * + * @since 1.0.0 + * @category Schemas + */ +export const Part = >( + toolkit: T +): Schema.Schema : Toolkit.WithHandlerTools>, PartEncoded> => { + const toolCalls: Array, ToolCallPartEncoded>> = [] + const toolCallResults: Array, ToolResultPartEncoded>> = [] + for (const tool of Object.values(toolkit.tools as Record)) { + toolCalls.push(ToolCallPart(tool.name, tool.parametersSchema as any)) + toolCallResults.push(ToolResultPart(tool.name, tool.successSchema, tool.failureSchema)) + } + return Schema.Union( + TextPart, + ReasoningPart, + FilePart, + DocumentSourcePart, + UrlSourcePart, + ResponseMetadataPart, + FinishPart, + ...toolCalls, + ...toolCallResults + ) as any +} + +// ============================================================================= +// Stream Parts +// ============================================================================= + +/** + * A type for representing streaming response parts with tool-specific typing. + * + * @template Tools - Record of tools with their schemas + * + * @since 1.0.0 + * @category Models + */ +export type StreamPart> = + | TextStartPart + | TextDeltaPart + | TextEndPart + | ReasoningStartPart + | ReasoningDeltaPart + | ReasoningEndPart + | ToolParamsStartPart + | ToolParamsDeltaPart + | ToolParamsEndPart + | ToolCallParts + | ToolResultParts + | FilePart + | DocumentSourcePart + | UrlSourcePart + | ResponseMetadataPart + | FinishPart + | ErrorPart + +/** + * Encoded representation of streaming response parts for serialization. + * + * @since 1.0.0 + * @category Models + */ +export type StreamPartEncoded = + | TextStartPartEncoded + | TextDeltaPartEncoded + | TextEndPartEncoded + | ReasoningStartPartEncoded + | ReasoningDeltaPartEncoded + | ReasoningEndPartEncoded + | ToolParamsStartPartEncoded + | ToolParamsDeltaPartEncoded + | ToolParamsEndPartEncoded + | ToolCallPartEncoded + | ToolResultPartEncoded + | FilePartEncoded + | DocumentSourcePartEncoded + | UrlSourcePartEncoded + | ResponseMetadataPartEncoded + | FinishPartEncoded + | ErrorPartEncoded + +/** + * Creates a Schema for streaming response parts based on a toolkit. + * + * @since 1.0.0 + * @category Schemas + */ +export const StreamPart = >( + toolkit: T +): Schema.Schema< + StreamPart< + T extends Toolkit.Any ? Toolkit.Tools : Toolkit.WithHandlerTools + >, + StreamPartEncoded +> => { + const toolCalls: Array, ToolCallPartEncoded>> = [] + const toolCallResults: Array, ToolResultPartEncoded>> = [] + for (const tool of Object.values(toolkit.tools as Record)) { + toolCalls.push(ToolCallPart(tool.name, tool.parametersSchema as any)) + toolCallResults.push(ToolResultPart(tool.name, tool.successSchema, tool.failureSchema)) + } + return Schema.Union( + TextStartPart, + TextDeltaPart, + TextEndPart, + ReasoningStartPart, + ReasoningDeltaPart, + ReasoningEndPart, + ToolParamsStartPart, + ToolParamsDeltaPart, + ToolParamsEndPart, + FilePart, + DocumentSourcePart, + UrlSourcePart, + ResponseMetadataPart, + FinishPart, + ErrorPart, + ...toolCalls, + ...toolCallResults + ) as any +} + +// ============================================================================= +// Utility Types +// ============================================================================= + +/** + * Utility type that extracts tool call parts from a set of tools. + * + * @template Tools - Record of tools with their schemas + * + * @since 1.0.0 + * @category Utility Types + */ +export type ToolCallParts> = { + [Name in keyof Tools]: Name extends string ? + ToolCallPart["fields"]>> + : never +}[keyof Tools] + +/** + * Utility type that extracts tool result parts from a set of tools. + * + * @template Tools - Record of tools with their schemas + * + * @since 1.0.0 + * @category Utility Types + */ +export type ToolResultParts> = { + [Name in keyof Tools]: Name extends string ? ToolResultPart< + Name, + Tool.Success, + Tool.Failure + > + : never +}[keyof Tools] + +// ============================================================================= +// Base Part +// ============================================================================= + +/** + * Schema for provider-specific metadata which can be attached to response parts. + * + * Provider-specific metadata is namespaced by provider and has the structure: + * + * ``` + * { + * "": { + * // Provider-specific metadata + * } + * } + * ``` + * + * @since 1.0.0 + * @category Schemas + */ +export const ProviderMetadata = Schema.Record({ + key: Schema.String, + value: Schema.UndefinedOr(Schema.Record({ + key: Schema.String, + value: Schema.Unknown + })) +}) + +/** + * @since 1.0.0 + * @category Models + */ +export type ProviderMetadata = typeof ProviderMetadata.Type + +/** + * Base interface for all response content parts. + * + * Provides common structure including type identifier and optional metadata. + * + * @template Type - String literal type for the part type + * + * @since 1.0.0 + * @category Models + */ +export interface BasePart { + readonly [PartTypeId]: PartTypeId + /** + * The type of this response part. + */ + readonly type: Type + /** + * Optional provider-specific metadata for this part. + */ + readonly metadata: Metadata +} + +/** + * Base interface for encoded response content parts. + * + * @template Type - String literal type for the part type + * + * @since 1.0.0 + * @category Models + */ +export interface BasePartEncoded { + /** + * The type of this response part. + */ + readonly type: Type + /** + * Optional provider-specific metadata for this part. + */ + readonly metadata?: Metadata | undefined +} + +/** + * Creates a new response content part of the specified type. + * + * @example + * ```ts + * import { Response } from "@effect/ai" + * + * const textPart = Response.makePart("text", { + * text: "Hello, world!" + * }) + * + * const toolCallPart = Response.makePart("tool-call", { + * id: "call_123", + * name: "get_weather", + * params: { city: "San Francisco" }, + * providerExecuted: false + * }) + * ``` + * + * @since 1.0.0 + * @category Constructors + */ +export const makePart = ( + /** + * The type of part to create. + */ + type: Type, + /** + * Parameters specific to the part type being created. + */ + params: Omit, PartTypeId | "type" | "metadata"> & { + /** + * Optional provider-specific metadata for this part. + */ + readonly metadata?: Extract["metadata"] | undefined + } +): Extract => + ({ + ...params, + [PartTypeId]: PartTypeId, + type, + metadata: params.metadata ?? {} + }) as any + +/** + * A utility type for specifying the parameters required to construct a + * specific response part. + * + * @since 1.0.0 + * @category Utility Types + */ +export type ConstructorParams = Omit & { + /** + * Optional provider-specific metadata for this part. + */ + readonly metadata?: Part["metadata"] | undefined +} + +// ============================================================================= +// Text Part +// ============================================================================= + +/** + * Response part representing plain text content. + * + * @example + * ```ts + * import { Response } from "@effect/ai" + * + * const textPart: Response.TextPart = Response.makePart("text", { + * text: "The answer to your question is 42.", + * }) + * ``` + * + * @since 1.0.0 + * @category Models + */ +export interface TextPart extends BasePart<"text", TextPartMetadata> { + /** + * The text content. + */ + readonly text: string +} + +/** + * Encoded representation of text parts for serialization. + * + * @since 1.0.0 + * @category Models + */ +export interface TextPartEncoded extends BasePartEncoded<"text", TextPartMetadata> { + /** + * The text content. + */ + readonly text: string +} + +/** + * Represents provider-specific metadata that can be associated with a + * `TextPart` through module augmentation. + * + * @since 1.0.0 + * @category ProviderOptions + */ +export interface TextPartMetadata extends ProviderMetadata {} + +/** + * Schema for validation and encoding of text parts. + * + * @since 1.0.0 + * @category Schemas + */ +export const TextPart: Schema.Schema = Schema.Struct({ + type: Schema.Literal("text"), + text: Schema.String, + metadata: Schema.optionalWith(ProviderMetadata, { default: constEmptyObject }) +}).pipe( + Schema.attachPropertySignature(PartTypeId, PartTypeId), + Schema.annotations({ identifier: "TextPart" }) +) + +/** + * Constructs a new text part. + * + * @since 1.0.0 + * @category Constructors + */ +export const textPart = (params: ConstructorParams): TextPart => makePart("text", params) + +// ============================================================================= +// Text Start Part +// ============================================================================= + +/** + * Response part indicating the start of streaming text content. + * + * Marks the beginning of a text chunk with a unique identifier. + * + * @since 1.0.0 + * @category Models + */ +export interface TextStartPart extends BasePart<"text-start", TextStartPartMetadata> { + /** + * Unique identifier for this text chunk. + */ + readonly id: string +} + +/** + * Encoded representation of text start parts for serialization. + * + * @since 1.0.0 + * @category Models + */ +export interface TextStartPartEncoded extends BasePartEncoded<"text-start", TextStartPartMetadata> { + /** + * Unique identifier for this text chunk. + */ + readonly id: string +} + +/** + * Represents provider-specific metadata that can be associated with a + * `TextStartPart` through module augmentation. + * + * @since 1.0.0 + * @category ProviderOptions + */ +export interface TextStartPartMetadata extends ProviderMetadata {} + +/** + * Schema for validation and encoding of text start parts. + * + * @since 1.0.0 + * @category Schemas + */ +export const TextStartPart: Schema.Schema = Schema.Struct({ + type: Schema.Literal("text-start"), + id: Schema.String, + metadata: Schema.optionalWith(ProviderMetadata, { default: constEmptyObject }) +}).pipe( + Schema.attachPropertySignature(PartTypeId, PartTypeId), + Schema.annotations({ identifier: "TextStartPart" }) +) + +/** + * Constructs a new text start part. + * + * @since 1.0.0 + * @category Constructors + */ +export const textStartPart = (params: ConstructorParams): TextStartPart => makePart("text-start", params) + +// ============================================================================= +// Text Delta Part +// ============================================================================= + +/** + * Response part containing incremental text content to be added to the existing + * text chunk with the same unique identifier. + * + * @since 1.0.0 + * @category Models + */ +export interface TextDeltaPart extends BasePart<"text-delta", TextDeltaPartMetadata> { + /** + * Unique identifier matching the corresponding text chunk. + */ + readonly id: string + /** + * The incremental text content to add. + */ + readonly delta: string +} + +/** + * Encoded representation of text delta parts for serialization. + * + * @since 1.0.0 + * @category Models + */ +export interface TextDeltaPartEncoded extends BasePartEncoded<"text-delta", TextDeltaPartMetadata> { + /** + * Unique identifier matching the corresponding text chunk. + */ + readonly id: string + /** + * The incremental text content to add. + */ + readonly delta: string +} + +/** + * Represents provider-specific metadata that can be associated with a + * `TextDeltaPart` through module augmentation. + * + * @since 1.0.0 + * @category ProviderOptions + */ +export interface TextDeltaPartMetadata extends ProviderMetadata {} + +/** + * Schema for validation and encoding of text delta parts. + * + * @since 1.0.0 + * @category Schemas + */ +export const TextDeltaPart: Schema.Schema = Schema.Struct({ + type: Schema.Literal("text-delta"), + id: Schema.String, + delta: Schema.String, + metadata: Schema.optionalWith(ProviderMetadata, { default: constEmptyObject }) +}).pipe( + Schema.attachPropertySignature(PartTypeId, PartTypeId), + Schema.annotations({ identifier: "TextDeltaPart" }) +) + +/** + * Constructs a new text delta part. + * + * @since 1.0.0 + * @category Constructors + */ +export const textDeltaPart = (params: ConstructorParams): TextDeltaPart => makePart("text-delta", params) + +// ============================================================================= +// Text End Part +// ============================================================================= + +/** + * Response part indicating the end of streaming text content. + * + * Marks the completion of a text chunk. + * + * @since 1.0.0 + * @category Models + */ +export interface TextEndPart extends BasePart<"text-end", TextEndPartMetadata> { + /** + * Unique identifier matching the corresponding text chunk. + */ + readonly id: string +} + +/** + * Encoded representation of text end parts for serialization. + * + * @since 1.0.0 + * @category Models + */ +export interface TextEndPartEncoded extends BasePartEncoded<"text-end", TextEndPartMetadata> { + /** + * Unique identifier matching the corresponding text chunk. + */ + readonly id: string +} + +/** + * Represents provider-specific metadata that can be associated with a + * `TextEndPart` through module augmentation. + * + * @since 1.0.0 + * @category ProviderOptions + */ +export interface TextEndPartMetadata extends ProviderMetadata {} + +/** + * Schema for validation and encoding of text end parts. + * + * @since 1.0.0 + * @category Schemas + */ +export const TextEndPart: Schema.Schema = Schema.Struct({ + type: Schema.Literal("text-end"), + id: Schema.String, + metadata: Schema.optionalWith(ProviderMetadata, { default: constEmptyObject }) +}).pipe( + Schema.attachPropertySignature(PartTypeId, PartTypeId), + Schema.annotations({ identifier: "TextEndPart" }) +) + +/** + * Constructs a new text end part. + * + * @since 1.0.0 + * @category Constructors + */ +export const textEndPart = (params: ConstructorParams): TextEndPart => makePart("text-end", params) + +// ============================================================================= +// Reasoning Part +// ============================================================================= + +/** + * Response part representing reasoning or chain-of-thought content. + * + * Contains the internal reasoning process or explanation from the large + * language model. + * + * @example + * ```ts + * import { Response } from "@effect/ai" + * + * const reasoningPart: Response.ReasoningPart = Response.makePart("reasoning", { + * text: "Let me think step by step: First I need to analyze the user's question...", + * }) + * ``` + * + * @since 1.0.0 + * @category Models + */ +export interface ReasoningPart extends BasePart<"reasoning", ReasoningPartMetadata> { + /** + * The reasoning or thought process text. + */ + readonly text: string +} + +/** + * Encoded representation of reasoning parts for serialization. + * + * @since 1.0.0 + * @category Models + */ +export interface ReasoningPartEncoded extends BasePartEncoded<"reasoning", ReasoningPartMetadata> { + /** + * The reasoning or thought process text. + */ + readonly text: string +} + +/** + * Represents provider-specific metadata that can be associated with a + * `ReasoningPart` through module augmentation. + * + * @since 1.0.0 + * @category ProviderOptions + */ +export interface ReasoningPartMetadata extends ProviderMetadata {} + +/** + * Schema for validation and encoding of reasoning parts. + * + * @since 1.0.0 + * @category Schemas + */ +export const ReasoningPart: Schema.Schema = Schema.Struct({ + type: Schema.Literal("reasoning"), + text: Schema.String, + metadata: Schema.optionalWith(ProviderMetadata, { default: constEmptyObject }) +}).pipe( + Schema.attachPropertySignature(PartTypeId, PartTypeId), + Schema.annotations({ identifier: "ReasoningPart" }) +) + +/** + * Constructs a new reasoning part. + * + * @since 1.0.0 + * @category Constructors + */ +export const reasoningPart = (params: ConstructorParams): ReasoningPart => makePart("reasoning", params) + +// ============================================================================= +// Reasoning Start Part +// ============================================================================= + +/** + * Response part indicating the start of streaming reasoning content. + * + * Marks the beginning of a reasoning chunk with a unique identifier. + * + * @since 1.0.0 + * @category Models + */ +export interface ReasoningStartPart extends BasePart<"reasoning-start", ReasoningStartPartMetadata> { + /** + * Unique identifier for this reasoning chunk. + */ + readonly id: string +} + +/** + * Encoded representation of reasoning start parts for serialization. + * + * @since 1.0.0 + * @category Models + */ +export interface ReasoningStartPartEncoded extends BasePartEncoded<"reasoning-start", ReasoningStartPartMetadata> { + /** + * Unique identifier for this reasoning stream. + */ + readonly id: string +} + +/** + * Represents provider-specific metadata that can be associated with a + * `ReasoningStartPart` through module augmentation. + * + * @since 1.0.0 + * @category ProviderOptions + */ +export interface ReasoningStartPartMetadata extends ProviderMetadata {} + +/** + * Schema for validation and encoding of reasoning start parts. + * + * @since 1.0.0 + * @category Schemas + */ +export const ReasoningStartPart: Schema.Schema = Schema.Struct({ + type: Schema.Literal("reasoning-start"), + id: Schema.String, + metadata: Schema.optionalWith(ProviderMetadata, { default: constEmptyObject }) +}).pipe( + Schema.attachPropertySignature(PartTypeId, PartTypeId), + Schema.annotations({ identifier: "ReasoningStartPart" }) +) + +/** + * Constructs a new reasoning start part. + * + * @since 1.0.0 + * @category Constructors + */ +export const reasoningStartPart = (params: ConstructorParams): ReasoningStartPart => + makePart("reasoning-start", params) + +// ============================================================================= +// Reasoning Delta Part +// ============================================================================= + +/** + * Response part containing incremental reasoning content to be added to the + * existing chunk of reasoning text with the same unique identifier. + * + * @since 1.0.0 + * @category Models + */ +export interface ReasoningDeltaPart extends BasePart<"reasoning-delta", ReasoningDeltaPartMetadata> { + /** + * Unique identifier matching the corresponding reasoning chunk. + */ + readonly id: string + /** + * The incremental reasoning content to add. + */ + readonly delta: string +} + +/** + * Encoded representation of reasoning delta parts for serialization. + * + * @since 1.0.0 + * @category Models + */ +export interface ReasoningDeltaPartEncoded extends BasePartEncoded<"reasoning-delta", ReasoningDeltaPartMetadata> { + /** + * Unique identifier matching the corresponding reasoning chunk. + */ + readonly id: string + /** + * The incremental reasoning content to add. + */ + readonly delta: string +} + +/** + * Represents provider-specific metadata that can be associated with a + * `ReasoningDeltaPart` through module augmentation. + * + * @since 1.0.0 + * @category ProviderOptions + */ +export interface ReasoningDeltaPartMetadata extends ProviderMetadata {} + +/** + * Schema for validation and encoding of reasoning delta parts. + * + * @since 1.0.0 + * @category Schemas + */ +export const ReasoningDeltaPart: Schema.Schema = Schema.Struct({ + type: Schema.Literal("reasoning-delta"), + id: Schema.String, + delta: Schema.String, + metadata: Schema.optionalWith(ProviderMetadata, { default: constEmptyObject }) +}).pipe( + Schema.attachPropertySignature(PartTypeId, PartTypeId), + Schema.annotations({ identifier: "ReasoningDeltaPart" }) +) + +/** + * Constructs a new reasoning delta part. + * + * @since 1.0.0 + * @category Constructors + */ +export const reasoningDeltaPart = (params: ConstructorParams): ReasoningDeltaPart => + makePart("reasoning-delta", params) + +// ============================================================================= +// Reasoning End Part +// ============================================================================= + +/** + * Response part indicating the end of streaming reasoning content. + * + * Marks the completion of a chunk of reasoning content. + * + * @since 1.0.0 + * @category Models + */ +export interface ReasoningEndPart extends BasePart<"reasoning-end", ReasoningEndPartMetadata> { + /** + * Unique identifier matching the corresponding reasoning chunk. + */ + readonly id: string +} + +/** + * Encoded representation of reasoning end parts for serialization. + * + * @since 1.0.0 + * @category Models + */ +export interface ReasoningEndPartEncoded extends BasePartEncoded<"reasoning-end", ReasoningEndPartMetadata> { + /** + * Unique identifier matching the corresponding reasoning chunk. + */ + readonly id: string +} + +/** + * Represents provider-specific metadata that can be associated with a + * `ReasoningEndPart` through module augmentation. + * + * @since 1.0.0 + * @category ProviderOptions + */ +export interface ReasoningEndPartMetadata extends ProviderMetadata {} + +/** + * Schema for validation and encoding of reasoning end parts. + * + * @since 1.0.0 + * @category Schemas + */ +export const ReasoningEndPart: Schema.Schema = Schema.Struct({ + type: Schema.Literal("reasoning-end"), + id: Schema.String, + metadata: Schema.optionalWith(ProviderMetadata, { default: constEmptyObject }) +}).pipe( + Schema.attachPropertySignature(PartTypeId, PartTypeId), + Schema.annotations({ identifier: "ReasoningEndPart" }) +) + +/** + * Constructs a new reasoning end part. + * + * @since 1.0.0 + * @category Constructors + */ +export const reasoningEndPart = (params: ConstructorParams): ReasoningEndPart => + makePart("reasoning-end", params) + +// ============================================================================= +// Tool Params Start Part +// ============================================================================= + +/** + * Response part indicating the start of streaming tool parameters. + * + * Marks the beginning of tool parameter streaming with metadata about the tool + * call. + * + * @since 1.0.0 + * @category Models + */ +export interface ToolParamsStartPart extends BasePart<"tool-params-start", ToolParamsStartPartMetadata> { + /** + * Unique identifier for this tool parameter chunk. + */ + readonly id: string + /** + * Name of the tool being called, which corresponds to the name of the tool + * in the `Toolkit` included with the request. + */ + readonly name: string + /** + * Optional provider-specific name for the tool, which can be useful when the + * name of the tool in the `Toolkit` and the name of the tool used by the + * model are different. + * + * This is usually happens only with provider-defined tools which require a + * user-space handler. + */ + readonly providerName?: string | undefined + /** + * Whether the tool was executed by the provider (true) or framework (false). + */ + readonly providerExecuted: boolean +} + +/** + * Encoded representation of tool params start parts for serialization. + * + * @since 1.0.0 + * @category Models + */ +export interface ToolParamsStartPartEncoded extends BasePartEncoded<"tool-params-start", ToolParamsStartPartMetadata> { + /** + * Unique identifier for this tool parameter chunk. + */ + readonly id: string + /** + * Name of the tool being called, which corresponds to the name of the tool + * in the `Toolkit` included with the request. + */ + readonly name: string + /** + * Optional provider-specific name for the tool, which can be useful when the + * name of the tool in the `Toolkit` and the name of the tool used by the + * model are different. + * + * This is usually happens only with provider-defined tools which require a + * user-space handler. + */ + readonly providerName?: string | undefined + /** + * Whether the tool was executed by the provider (true) or framework (false). + */ + readonly providerExecuted?: boolean +} + +/** + * Represents provider-specific metadata that can be associated with a + * `ToolParamsStartPart` through module augmentation. + * + * @since 1.0.0 + * @category ProviderOptions + */ +export interface ToolParamsStartPartMetadata extends ProviderMetadata {} + +/** + * Schema for validation and encoding of tool params start parts. + * + * @since 1.0.0 + * @category Schemas + */ +export const ToolParamsStartPart: Schema.Schema = Schema.Struct({ + type: Schema.Literal("tool-params-start"), + id: Schema.String, + name: Schema.String, + providerName: Schema.optional(Schema.String), + providerExecuted: Schema.optionalWith(Schema.Boolean, { default: constFalse }), + metadata: Schema.optionalWith(ProviderMetadata, { default: constEmptyObject }) +}).pipe( + Schema.attachPropertySignature(PartTypeId, PartTypeId), + Schema.annotations({ identifier: "ToolParamsStartPart" }) +) + +/** + * Constructs a new tool params start part. + * + * @since 1.0.0 + * @category Constructors + */ +export const toolParamsStartPart = (params: ConstructorParams): ToolParamsStartPart => + makePart("tool-params-start", params) + +// ============================================================================= +// Tool Params Delta Part +// ============================================================================= + +/** + * Response part containing incremental tool parameter content. + * + * Represents a chunk of tool parameters being streamed, containing the + * incremental JSON content that forms the tool parameters. + * + * @since 1.0.0 + * @category Models + */ +export interface ToolParamsDeltaPart extends BasePart<"tool-params-delta", ToolParamsDeltaPartMetadata> { + /** + * Unique identifier matching the corresponding tool parameter chunk. + */ + readonly id: string + /** + * The incremental parameter content (typically JSON fragment) to add. + */ + readonly delta: string +} + +/** + * Encoded representation of tool params delta parts for serialization. + * + * @since 1.0.0 + * @category Models + */ +export interface ToolParamsDeltaPartEncoded extends BasePartEncoded<"tool-params-delta", ToolParamsDeltaPartMetadata> { + /** + * Unique identifier matching the corresponding tool parameter chunk. + */ + readonly id: string + /** + * The incremental parameter content (typically JSON fragment) to add. + */ + readonly delta: string +} + +/** + * Represents provider-specific metadata that can be associated with a + * `ToolParamsDeltaPart` through module augmentation. + * + * @since 1.0.0 + * @category ProviderOptions + */ +export interface ToolParamsDeltaPartMetadata extends ProviderMetadata {} + +/** + * Schema for validation and encoding of tool params delta parts. + * + * @since 1.0.0 + * @category Schemas + */ +export const ToolParamsDeltaPart: Schema.Schema = Schema.Struct({ + type: Schema.Literal("tool-params-delta"), + id: Schema.String, + delta: Schema.String, + metadata: Schema.optionalWith(ProviderMetadata, { default: constEmptyObject }) +}).pipe( + Schema.attachPropertySignature(PartTypeId, PartTypeId), + Schema.annotations({ identifier: "ToolParamsDeltaPart" }) +) + +/** + * Constructs a new tool params delta part. + * + * @since 1.0.0 + * @category Constructors + */ +export const toolParamsDeltaPart = (params: ConstructorParams): ToolParamsDeltaPart => + makePart("tool-params-delta", params) + +// ============================================================================= +// Tool Params End Part +// ============================================================================= + +/** + * Response part indicating the end of streaming tool parameters. + * + * Marks the completion of a tool parameter stream, indicating that all + * parameter data has been sent and the tool call is ready to be executed. + * + * @since 1.0.0 + * @category Models + */ +export interface ToolParamsEndPart extends BasePart<"tool-params-end", ToolParamsEndPartMetadata> { + /** + * Unique identifier matching the corresponding tool parameter chunk. + */ + readonly id: string +} + +/** + * Encoded representation of tool params end parts for serialization. + * + * @since 1.0.0 + * @category Models + */ +export interface ToolParamsEndPartEncoded extends BasePartEncoded<"tool-params-end", ToolParamsEndPartMetadata> { + /** + * Unique identifier matching the corresponding tool parameter stream. + */ + readonly id: string +} + +/** + * Represents provider-specific metadata that can be associated with a + * `ToolParamsEndPart` through module augmentation. + * + * @since 1.0.0 + * @category ProviderOptions + */ +export interface ToolParamsEndPartMetadata extends ProviderMetadata {} + +/** + * Schema for validation and encoding of tool params end parts. + * + * @since 1.0.0 + * @category Schemas + */ +export const ToolParamsEndPart: Schema.Schema = Schema.Struct({ + type: Schema.Literal("tool-params-end"), + id: Schema.String, + metadata: Schema.optionalWith(ProviderMetadata, { default: constEmptyObject }) +}).pipe( + Schema.attachPropertySignature(PartTypeId, PartTypeId), + Schema.annotations({ identifier: "ToolParamsEndPart" }) +) + +/** + * Constructs a new tool params end part. + * + * @since 1.0.0 + * @category Constructors + */ +export const toolParamsEndPart = (params: ConstructorParams): ToolParamsEndPart => + makePart("tool-params-end", params) + +// ============================================================================= +// Tool Call Part +// ============================================================================= + +/** + * Response part representing a tool call request. + * + * @example + * ```ts + * import { Response } from "@effect/ai" + * import { Schema } from "effect" + * + * const weatherParams = Schema.Struct({ + * city: Schema.String, + * units: Schema.optional(Schema.Literal("celsius", "fahrenheit")) + * }) + * + * const toolCallPart: Response.ToolCallPart< + * "get_weather", + * typeof weatherParams.fields + * > = Response.makePart("tool-call", { + * id: "call_123", + * name: "get_weather", + * params: { city: "San Francisco", units: "celsius" }, + * providerExecuted: false, + * }) + * ``` + * + * @template Name - String literal type for the tool name + * @template Params - Schema fields type for the tool parameters + * + * @since 1.0.0 + * @category Models + */ +export interface ToolCallPart extends BasePart<"tool-call", ToolCallPartMetadata> { + /** + * Unique identifier for this tool call. + */ + readonly id: string + /** + * Name of the tool being called, which corresponds to the name of the tool + * in the `Toolkit` included with the request. + */ + readonly name: Name + /** + * Parameters to pass to the tool. + */ + readonly params: Params + /** + * Optional provider-specific name for the tool, which can be useful when the + * name of the tool in the `Toolkit` and the name of the tool used by the + * model are different. + * + * This is usually happens only with provider-defined tools which require a + * user-space handler. + */ + readonly providerName?: string | undefined + /** + * Whether the tool was executed by the provider (true) or framework (false). + */ + readonly providerExecuted: boolean +} + +/** + * Encoded representation of tool call parts for serialization. + * + * @since 1.0.0 + * @category Models + */ +export interface ToolCallPartEncoded extends BasePartEncoded<"tool-call", ToolCallPartMetadata> { + /** + * Unique identifier for this tool call. + */ + readonly id: string + /** + * Name of the tool being called, which corresponds to the name of the tool + * in the `Toolkit` included with the request. + */ + readonly name: string + /** + * Parameters to pass to the tool. + */ + readonly params: unknown + /** + * Optional provider-specific name for the tool, which can be useful when the + * name of the tool in the `Toolkit` and the name of the tool used by the + * model are different. + * + * This is usually happens only with provider-defined tools which require a + * user-space handler. + */ + readonly providerName?: string | undefined + /** + * Whether the tool was executed by the provider (true) or framework (false). + */ + readonly providerExecuted?: boolean | undefined +} + +/** + * Represents provider-specific metadata that can be associated with a + * `ToolCallPart` through module augmentation. + * + * @since 1.0.0 + * @category ProviderOptions + */ +export interface ToolCallPartMetadata extends ProviderMetadata {} + +/** + * Creates a Schema for tool call parts with specific tool name and parameters. + * + * @since 1.0.0 + * @category Schemas + */ +export const ToolCallPart = ( + /** + * Name of the tool. + */ + name: Name, + /** + * Schema for the tool parameters. + */ + params: Schema.Struct +): Schema.Schema, ToolCallPartEncoded> => + Schema.Struct({ + type: Schema.Literal("tool-call"), + id: Schema.String, + name: Schema.Literal(name), + params, + providerName: Schema.optional(Schema.String), + providerExecuted: Schema.optionalWith(Schema.Boolean, { default: constFalse }), + metadata: Schema.optionalWith(ProviderMetadata, { default: constEmptyObject }) + }).pipe( + Schema.attachPropertySignature(PartTypeId, PartTypeId), + Schema.annotations({ identifier: "ToolCallPart" }) + ) as any + +/** + * Constructs a new tool call part. + * + * @since 1.0.0 + * @category Constructors + */ +export const toolCallPart = ( + params: ConstructorParams> +): ToolCallPart => makePart("tool-call", params) + +// ============================================================================= +// Tool Call Result Part +// ============================================================================= + +/** + * The base fields of a tool result part. + * + * @since 1.0.0 + * @category Models + */ +export interface BaseToolResult extends BasePart<"tool-result", ToolResultPartMetadata> { + /** + * Unique identifier matching the original tool call. + */ + readonly id: string + /** + * Name of the tool being called, which corresponds to the name of the tool + * in the `Toolkit` included with the request. + */ + readonly name: Name + /** + * The encoded result for serialization purposes. + */ + readonly encodedResult: unknown + /** + * Optional provider-specific name for the tool, which can be useful when the + * name of the tool in the `Toolkit` and the name of the tool used by the + * model are different. + * + * This is usually happens only with provider-defined tools which require a + * user-space handler. + */ + readonly providerName?: string | undefined + /** + * Whether the tool was executed by the provider (true) or framework (false). + */ + readonly providerExecuted: boolean +} + +/** + * Represents a successful tool call result. + * + * @since 1.0.0 + * @category Models + */ +export interface ToolResultSuccess extends BaseToolResult { + /** + * The decoded success returned by the tool execution. + */ + readonly result: Success + /** + * Whether or not the result of executing the tool call handler was an error. + */ + readonly isFailure: false +} + +/** + * Represents a failed tool call result. + * + * @since 1.0.0 + * @category Models + */ +export interface ToolResultFailure extends BaseToolResult { + /** + * The decoded failure returned by the tool execution. + */ + readonly result: Failure + /** + * Whether or not the result of executing the tool call handler was an error. + */ + readonly isFailure: true +} + +/** + * Response part representing the result of a tool call. + * + * @example + * ```ts + * import { Either } from "effect" + * import { Response } from "@effect/ai" + * + * interface WeatherData { + * temperature: number + * condition: string + * humidity: number + * } + * + * const toolResultPart: Response.ToolResultPart< + * "get_weather", + * WeatherData, + * never + * > = Response.toolResultPart({ + * id: "call_123", + * name: "get_weather", + * isFailure: false, + * result: { + * temperature: 22, + * condition: "sunny", + * humidity: 65 + * }, + * encodedResult: { + * temperature: 22, + * condition: "sunny", + * humidity: 65 + * }, + * providerExecuted: false + * }) + * ``` + * + * @since 1.0.0 + * @category Models + */ +export type ToolResultPart = + | ToolResultSuccess + | ToolResultFailure + +/** + * Encoded representation of tool result parts for serialization. + * + * @since 1.0.0 + * @category Models + */ +export interface ToolResultPartEncoded extends BasePartEncoded<"tool-result", ToolResultPartMetadata> { + /** + * Unique identifier matching the original tool call. + */ + readonly id: string + /** + * Name of the tool being called, which corresponds to the name of the tool + * in the `Toolkit` included with the request. + */ + readonly name: string + /** + * The result returned by the tool execution. + */ + readonly result: unknown + /** + * Whether or not the result of executing the tool call handler was an error. + */ + readonly isFailure: boolean + /** + * Optional provider-specific name for the tool, which can be useful when the + * name of the tool in the `Toolkit` and the name of the tool used by the + * model are different. + * + * This is usually happens only with provider-defined tools which require a + * user-space handler. + */ + readonly providerName?: string | undefined + /** + * Whether the tool was executed by the provider (true) or framework (false). + */ + readonly providerExecuted?: boolean | undefined +} + +/** + * Represents provider-specific metadata that can be associated with a + * `ToolResultPart` through module augmentation. + * + * @since 1.0.0 + * @category ProviderOptions + */ +export interface ToolResultPartMetadata extends ProviderMetadata {} + +/** + * Creates a Schema for tool result parts with specific tool name and result type. + * + * @since 1.0.0 + * @category Schemas + */ +export const ToolResultPart = < + const Name extends string, + Success extends Schema.Schema.Any, + Failure extends Schema.Schema.All +>( + name: Name, + success: Success, + failure: Failure +): Schema.Schema< + ToolResultPart, Schema.Schema.Type>, + ToolResultPartEncoded +> => { + const Base = Schema.Struct({ + id: Schema.String, + type: Schema.Literal("tool-result"), + providerName: Schema.optional(Schema.String), + isFailure: Schema.Boolean + }) + const ResultSchema = Schema.Union(success, failure) + const Encoded = Schema.Struct({ + ...Base.fields, + name: Schema.String, + result: Schema.encodedSchema(ResultSchema), + providerExecuted: Schema.optional(Schema.Boolean), + metadata: Schema.optional(ProviderMetadata) + }) + const Decoded = Schema.Struct({ + ...Base.fields, + [PartTypeId]: Schema.Literal(PartTypeId), + name: Schema.Literal(name), + result: Schema.typeSchema(ResultSchema), + encodedResult: Schema.encodedSchema(ResultSchema), + providerExecuted: Schema.Boolean, + metadata: ProviderMetadata + }) + const decodeResult = ParseResult.decode(ResultSchema as any) + const encodeResult = ParseResult.encode(ResultSchema as any) + return Schema.transformOrFail( + Encoded, + Decoded, + { + strict: true, + decode: Effect.fnUntraced(function*(encoded) { + const decoded = yield* decodeResult(encoded.result) + const providerExecuted = encoded.providerExecuted ?? false + return { + ...encoded, + [PartTypeId]: PartTypeId, + name: encoded.name as Name, + result: decoded, + encodedResult: encoded.result as any, + metadata: encoded.metadata ?? {}, + providerExecuted + } as const + }), + encode: Effect.fnUntraced(function*(decoded) { + const encoded = yield* encodeResult(decoded.result) + return { + ...decoded, + result: encoded, + ...(decoded.metadata ?? {}), + ...(decoded.providerName ? { providerName: decoded.providerName } : {}), + ...(decoded.providerExecuted ? { providerExecuted: true } : {}) + } + }) + } + ).annotations({ identifier: `ToolResultPart(${name})` }) as any +} + +/** + * Constructs a new tool result part. + * + * @since 1.0.0 + * @category Constructors + */ +export const toolResultPart = < + const Params extends ConstructorParams> +>( + params: Params +): Params extends { + readonly name: infer Name extends string + readonly isFailure: false + readonly result: infer Success +} ? ToolResultPart + : Params extends { + readonly name: infer Name extends string + readonly isFailure: true + readonly result: infer Failure + } ? ToolResultPart + : never => makePart("tool-result", params) as any + +// ============================================================================= +// File Part +// ============================================================================= + +/** + * Response part representing a file attachment. + * + * Supports various file types including images, documents, and binary data. + * + * @example + * ```ts + * import { Response } from "@effect/ai" + * + * const imagePart: Response.FilePart = Response.makePart("file", { + * mediaType: "image/jpeg", + * data: new Uint8Array([1, 2, 3]), + * }) + * ``` + * + * @since 1.0.0 + * @category Models + */ +export interface FilePart extends BasePart<"file", FilePartMetadata> { + /** + * MIME type of the file (e.g., "image/jpeg", "application/pdf"). + */ + readonly mediaType: string + /** + * File data as a byte array. + */ + readonly data: Uint8Array +} + +/** + * Encoded representation of file parts for serialization. + * + * @since 1.0.0 + * @category Models + */ +export interface FilePartEncoded extends BasePartEncoded<"file", FilePartMetadata> { + /** + * MIME type of the file (e.g., "image/jpeg", "application/pdf"). + */ + readonly mediaType: string + /** + * File data as a base64 string. + */ + readonly data: string +} + +/** + * Represents provider-specific metadata that can be associated with a + * `FilePart` through module augmentation. + * + * @since 1.0.0 + * @category ProviderOptions + */ +export interface FilePartMetadata extends ProviderMetadata {} + +/** + * Schema for validation and encoding of file parts. + * + * @since 1.0.0 + * @category Schemas + */ +export const FilePart: Schema.Schema = Schema.Struct({ + type: Schema.Literal("file"), + mediaType: Schema.String, + data: Schema.Uint8ArrayFromBase64, + metadata: Schema.optionalWith(ProviderMetadata, { default: constEmptyObject }) +}).pipe( + Schema.attachPropertySignature(PartTypeId, PartTypeId), + Schema.annotations({ identifier: "FilePart" }) +) + +/** + * Constructs a new file part. + * + * @since 1.0.0 + * @category Constructors + */ +export const filePart = (params: ConstructorParams): FilePart => makePart("file", params) + +// ============================================================================= +// Document Source Part +// ============================================================================= + +/** + * Response part representing a document source reference. + * + * Used to reference documents that were used in generating the response. + * + * @since 1.0.0 + * @category Models + */ +export interface DocumentSourcePart extends BasePart<"source", DocumentSourcePartMetadata> { + /** + * Type discriminator for document sources. + */ + readonly sourceType: "document" + /** + * Unique identifier for the document. + */ + readonly id: string + /** + * MIME type of the document. + */ + readonly mediaType: string + /** + * Display title of the document. + */ + readonly title: string + /** + * Optional filename of the document. + */ + readonly fileName?: string +} + +/** + * Encoded representation of document source parts for serialization. + * + * @since 1.0.0 + * @category Models + */ +export interface DocumentSourcePartEncoded extends BasePartEncoded<"source", DocumentSourcePartMetadata> { + /** + * Type discriminator for document sources. + */ + readonly sourceType: "document" + /** + * Unique identifier for the document. + */ + readonly id: string + /** + * MIME type of the document. + */ + readonly mediaType: string + /** + * Display title of the document. + */ + readonly title: string + /** + * Optional filename of the document. + */ + readonly fileName?: string +} + +/** + * Represents provider-specific metadata that can be associated with a + * `DocumentSourcePart` through module augmentation. + * + * @since 1.0.0 + * @category ProviderOptions + */ +export interface DocumentSourcePartMetadata extends ProviderMetadata {} + +/** + * Schema for validation and encoding of document source parts. + * + * @since 1.0.0 + * @category Schemas + */ +export const DocumentSourcePart: Schema.Schema = Schema.Struct({ + type: Schema.Literal("source"), + sourceType: Schema.Literal("document"), + id: Schema.String, + mediaType: Schema.String, + title: Schema.String, + fileName: Schema.optional(Schema.String), + metadata: Schema.optionalWith(ProviderMetadata, { default: constEmptyObject }) +}).pipe( + Schema.attachPropertySignature(PartTypeId, PartTypeId), + Schema.annotations({ identifier: "DocumentSourcePart" }) +) + +/** + * Constructs a new document source part. + * + * @since 1.0.0 + * @category Constructors + */ +export const documentSourcePart = (params: ConstructorParams): DocumentSourcePart => + makePart("source", { ...params, sourceType: "document" }) as any + +// ============================================================================= +// Url Source Part +// ============================================================================= + +/** + * Response part representing a URL source reference. + * + * Used to reference web URLs that were used in generating the response. + * + * @since 1.0.0 + * @category Models + */ +export interface UrlSourcePart extends BasePart<"source", UrlSourcePartMetadata> { + /** + * Type discriminator for URL sources. + */ + readonly sourceType: "url" + /** + * Unique identifier for the URL. + */ + readonly id: string + /** + * The URL that was referenced. + */ + readonly url: URL + /** + * Display title of the URL content. + */ + readonly title: string +} + +/** + * Encoded representation of URL source parts for serialization. + * + * @since 1.0.0 + * @category Models + */ +export interface UrlSourcePartEncoded extends BasePartEncoded<"source", UrlSourcePartMetadata> { + /** + * Type discriminator for URL sources. + */ + readonly sourceType: "url" + /** + * Unique identifier for the URL. + */ + readonly id: string + /** + * The URL that was referenced as a string. + */ + readonly url: string + /** + * Display title of the URL content. + */ + readonly title: string +} + +/** + * Represents provider-specific metadata that can be associated with a + * `UrlSourcePart` through module augmentation. + * + * @since 1.0.0 + * @category ProviderOptions + */ +export interface UrlSourcePartMetadata extends ProviderMetadata {} + +/** + * Schema for validation and encoding of url source parts. + * + * @since 1.0.0 + * @category Schemas + */ +export const UrlSourcePart: Schema.Schema = Schema.Struct({ + type: Schema.Literal("source"), + sourceType: Schema.Literal("url"), + id: Schema.String, + url: Schema.URL, + title: Schema.String, + metadata: Schema.optionalWith(ProviderMetadata, { default: constEmptyObject }) +}).pipe( + Schema.attachPropertySignature(PartTypeId, PartTypeId), + Schema.annotations({ identifier: "UrlSourcePart" }) +) + +/** + * Constructs a new URL source part. + * + * @since 1.0.0 + * @category Constructors + */ +export const urlSourcePart = (params: ConstructorParams): UrlSourcePart => + makePart("source", { ...params, sourceType: "url" }) as any + +// ============================================================================= +// Response Metadata Part +// ============================================================================= + +/** + * Response part containing metadata about the large language model response. + * + * @example + * ```ts + * import { Response } from "@effect/ai" + * import { Option, DateTime } from "effect" + * + * const metadataPart: Response.ResponseMetadataPart = Response.makePart("response-metadata", { + * id: Option.some("resp_123"), + * modelId: Option.some("gpt-4"), + * timestamp: Option.some(DateTime.unsafeNow()) + * }) + * ``` + * + * @since 1.0.0 + * @category Models + */ +export interface ResponseMetadataPart extends BasePart<"response-metadata", ResponseMetadataPartMetadata> { + /** + * Optional unique identifier for this specific response. + */ + readonly id: Option.Option + /** + * Optional identifier of the AI model that generated the response. + */ + readonly modelId: Option.Option + /** + * Optional timestamp when the response was generated. + */ + readonly timestamp: Option.Option +} + +/** + * Encoded representation of response metadata parts for serialization. + * + * @since 1.0.0 + * @category Models + */ +export interface ResponseMetadataPartEncoded + extends BasePartEncoded<"response-metadata", ResponseMetadataPartMetadata> +{ + /** + * Optional unique identifier for this specific response. + */ + readonly id?: string | undefined + /** + * Optional identifier of the AI model that generated the response. + */ + readonly modelId?: string | undefined + /** + * Optional timestamp when the response was generated. + */ + readonly timestamp?: string | undefined +} + +/** + * Represents provider-specific metadata that can be associated with a + * `ResponseMetadataPart` through module augmentation. + * + * @since 1.0.0 + * @category ProviderOptions + */ +export interface ResponseMetadataPartMetadata extends ProviderMetadata {} + +/** + * Schema for validation and encoding of response metadata parts. + * + * @since 1.0.0 + * @category Schemas + */ +export const ResponseMetadataPart: Schema.Schema = Schema.Struct({ + type: Schema.Literal("response-metadata"), + id: Schema.optionalWith(Schema.String, { as: "Option" }), + modelId: Schema.optionalWith(Schema.String, { as: "Option" }), + timestamp: Schema.optionalWith(Schema.DateTimeUtc, { as: "Option" }), + metadata: Schema.optionalWith(ProviderMetadata, { default: constEmptyObject }) +}).pipe( + Schema.attachPropertySignature(PartTypeId, PartTypeId), + Schema.annotations({ identifier: "ResponseMetadataPart" }) +) + +/** + * Constructs a new response metadata part. + * + * @since 1.0.0 + * @category Constructors + */ +export const responseMetadataPart = (params: ConstructorParams): ResponseMetadataPart => + makePart("response-metadata", params) + +// ============================================================================= +// Finish Part +// ============================================================================= + +/** + * Represents the reason why a model finished generation of a response. + * + * Possible finish reasons: + * - `"stop"`: The model generated a stop sequence. + * - `"length"`: The model exceeded its token budget. + * - `"content-filter"`: The model generated content which violated a content filter. + * - `"tool-calls"`: The model triggered a tool call. + * - `"error"`: The model encountered an error. + * - `"pause"`: The model requested to pause execution. + * - `"other"`: The model stopped for a reason not supported by this protocol. + * - `"unknown"`: The model did not specify a finish reason. + * + * @since 1.0.0 + * @category Models + */ +export const FinishReason: Schema.Literal<[ + "stop", + "length", + "content-filter", + "tool-calls", + "error", + "pause", + "other", + "unknown" +]> = Schema.Literal( + "stop", + "length", + "content-filter", + "tool-calls", + "error", + "pause", + "other", + "unknown" +) + +/** + * @since 1.0.0 + * @category Models + */ +export type FinishReason = typeof FinishReason.Type + +/** + * Represents usage information for a request to a large language model provider. + * + * If the model provider returns additional usage information than what is + * specified here, you can generally find that information under the provider + * metadata of the finish part of the response. + * + * @since 1.0.0 + * @category Models + */ +export class Usage extends Schema.Class("@effect/ai/AiResponse/Usage")({ + /** + * The number of tokens sent in the request to the model. + */ + inputTokens: Schema.UndefinedOr(Schema.Number), + /** + * The number of tokens that the model generated for the request. + */ + outputTokens: Schema.UndefinedOr(Schema.Number), + /** + * The total of number of input tokens and output tokens as reported by the + * large language model provider. + * + * **NOTE**: This value may differ from the sum of `inputTokens` and + * `outputTokens` due to inclusion of reasoning tokens or other + * provider-specific overhead. + */ + totalTokens: Schema.UndefinedOr(Schema.Number), + /** + * The number of reasoning tokens that the model used to generate the output + * for the request. + */ + reasoningTokens: Schema.optional(Schema.Number), + /** + * The number of input tokens read from the prompt cache for the request. + */ + cachedInputTokens: Schema.optional(Schema.Number) +}) {} + +/** + * Response part indicating the completion of a response generation. + * + * @example + * ```ts + * import { Response } from "@effect/ai" + * + * const finishPart: Response.FinishPart = Response.makePart("finish", { + * reason: "stop", + * usage: { + * inputTokens: 50, + * outputTokens: 25, + * totalTokens: 75 + * } + * }) + * ``` + * + * @since 1.0.0 + * @category Models + */ +export interface FinishPart extends BasePart<"finish", FinishPartMetadata> { + /** + * The reason why the model finished generating the response. + */ + readonly reason: FinishReason + /** + * Token usage statistics for the request. + */ + readonly usage: Usage +} + +/** + * Encoded representation of finish parts for serialization. + * + * @since 1.0.0 + * @category Models + */ +export interface FinishPartEncoded extends BasePartEncoded<"finish", FinishPartMetadata> { + /** + * The reason why the model finished generating the response. + */ + readonly reason: typeof FinishReason.Encoded + /** + * Token usage statistics for the request. + */ + readonly usage: typeof Usage.Encoded +} + +/** + * Represents provider-specific metadata that can be associated with a + * `FinishPart` through module augmentation. + * + * @since 1.0.0 + * @category ProviderOptions + */ +export interface FinishPartMetadata extends ProviderMetadata {} + +/** + * Schema for validation and encoding of finish parts. + * + * @since 1.0.0 + * @category Schemas + */ +export const FinishPart: Schema.Schema = Schema.Struct({ + type: Schema.Literal("finish"), + reason: FinishReason, + usage: Usage, + metadata: Schema.optionalWith(ProviderMetadata, { default: constEmptyObject }) +}).pipe( + Schema.attachPropertySignature(PartTypeId, PartTypeId), + Schema.annotations({ identifier: "FinishPart" }) +) + +/** + * Constructs a new finish part. + * + * @since 1.0.0 + * @category Constructors + */ +export const finishPart = (params: ConstructorParams): FinishPart => makePart("finish", params) + +// ============================================================================= +// Error Part +// ============================================================================= + +/** + * Response part indicating that an error occurred generating the response. + * + * @example + * ```ts + * import { Response } from "@effect/ai" + * + * const errorPart: Response.ErrorPart = Response.makePart("error", { + * error: new Error("boom") + * }) + * ``` + * + * @since 1.0.0 + * @category Models + */ +export interface ErrorPart extends BasePart<"error", ErrorPartMetadata> { + readonly error: unknown +} + +/** + * Encoded representation of error parts for serialization. + * + * @since 1.0.0 + * @category Models + */ +export interface ErrorPartEncoded extends BasePartEncoded<"error", ErrorPartMetadata> { + readonly error: unknown +} + +/** + * Represents provider-specific metadata that can be associated with a + * `ErrorPart` through module augmentation. + * + * @since 1.0.0 + * @category ProviderOptions + */ +export interface ErrorPartMetadata extends ProviderMetadata {} + +/** + * Schema for validation and encoding of error parts. + * + * @since 1.0.0 + * @category Schemas + */ +export const ErrorPart: Schema.Schema = Schema.Struct({ + type: Schema.Literal("error"), + error: Schema.Unknown, + metadata: Schema.optionalWith(ProviderMetadata, { default: constEmptyObject }) +}).pipe( + Schema.attachPropertySignature(PartTypeId, PartTypeId), + Schema.annotations({ identifier: "ErrorPart" }) +) + +/** + * Constructs a new error part. + * + * @since 1.0.0 + * @category Constructors + */ +export const errorPart = (params: ConstructorParams): ErrorPart => makePart("error", params) diff --git a/repos/effect/packages/ai/ai/src/Telemetry.ts b/repos/effect/packages/ai/ai/src/Telemetry.ts new file mode 100644 index 0000000..d92f940 --- /dev/null +++ b/repos/effect/packages/ai/ai/src/Telemetry.ts @@ -0,0 +1,549 @@ +/** + * The `Telemetry` module provides OpenTelemetry integration for operations + * performed against a large language model provider by defining telemetry + * attributes and utilities that follow the OpenTelemetry GenAI semantic + * conventions. + * + * @example + * ```ts + * import { Telemetry } from "@effect/ai" + * import { Effect } from "effect" + * + * // Add telemetry attributes to a span + * const addTelemetry = Effect.gen(function* () { + * const span = yield* Effect.currentSpan + * + * Telemetry.addGenAIAnnotations(span, { + * system: "openai", + * operation: { name: "chat" }, + * request: { + * model: "gpt-4", + * temperature: 0.7, + * maxTokens: 1000 + * }, + * usage: { + * inputTokens: 100, + * outputTokens: 50 + * } + * }) + * }) + * ``` + * + * @since 1.0.0 + */ +import * as Context from "effect/Context" +import { dual } from "effect/Function" +import * as Predicate from "effect/Predicate" +import * as String from "effect/String" +import type { Span } from "effect/Tracer" +import type { Simplify } from "effect/Types" +import type { ProviderOptions } from "./LanguageModel.js" +import type * as Response from "./Response.js" + +/** + * The attributes used to describe telemetry in the context of Generative + * Artificial Intelligence (GenAI) Models requests and responses. + * + * {@see https://opentelemetry.io/docs/specs/semconv/attributes-registry/gen-ai/} + * + * @since 1.0.0 + * @category Models + */ +export type GenAITelemetryAttributes = Simplify< + & AttributesWithPrefix + & AttributesWithPrefix + & AttributesWithPrefix + & AttributesWithPrefix + & AttributesWithPrefix + & AttributesWithPrefix +> + +/** + * All telemetry attributes which are part of the GenAI specification. + * + * @since 1.0.0 + * @category Models + */ +export type AllAttributes = + & BaseAttributes + & OperationAttributes + & TokenAttributes + & UsageAttributes + & RequestAttributes + & ResponseAttributes + +/** + * Telemetry attributes which are part of the GenAI specification and are + * namespaced by `gen_ai`. + * + * @since 1.0.0 + * @category Models + */ +export interface BaseAttributes { + /** + * The Generative AI product as identified by the client or server + * instrumentation. + */ + readonly system?: (string & {}) | WellKnownSystem | null | undefined +} + +/** + * Telemetry attributes which are part of the GenAI specification and are + * namespaced by `gen_ai.operation`. + * + * @since 1.0.0 + * @category Models + */ +export interface OperationAttributes { + readonly name?: (string & {}) | WellKnownOperationName | null | undefined +} + +/** + * Telemetry attributes which are part of the GenAI specification and are + * namespaced by `gen_ai.token`. + * + * @since 1.0.0 + * @category Models + */ +export interface TokenAttributes { + readonly type?: string | null | undefined +} + +/** + * Telemetry attributes which are part of the GenAI specification and are + * namespaced by `gen_ai.usage`. + * + * @since 1.0.0 + * @category Models + */ +export interface UsageAttributes { + readonly inputTokens?: number | null | undefined + readonly outputTokens?: number | null | undefined +} + +/** + * Telemetry attributes which are part of the GenAI specification and are + * namespaced by `gen_ai.request`. + * + * @since 1.0.0 + * @category Models + */ +export interface RequestAttributes { + /** + * The name of the GenAI model a request is being made to. + */ + readonly model?: string | null | undefined + /** + * The temperature setting for the GenAI request. + */ + readonly temperature?: number | null | undefined + /** + * The temperature setting for the GenAI request. + */ + readonly topK?: number | null | undefined + /** + * The top_k sampling setting for the GenAI request. + */ + readonly topP?: number | null | undefined + /** + * The top_p sampling setting for the GenAI request. + */ + readonly maxTokens?: number | null | undefined + /** + * The encoding formats requested in an embeddings operation, if specified. + */ + readonly encodingFormats?: ReadonlyArray | null | undefined + /** + * List of sequences that the model will use to stop generating further + * tokens. + */ + readonly stopSequences?: ReadonlyArray | null | undefined + /** + * The frequency penalty setting for the GenAI request. + */ + readonly frequencyPenalty?: number | null | undefined + /** + * The presence penalty setting for the GenAI request. + */ + readonly presencePenalty?: number | null | undefined + /** + * The seed setting for the GenAI request. Requests with same seed value + * are more likely to return same result. + */ + readonly seed?: number | null | undefined +} + +/** + * Telemetry attributes which are part of the GenAI specification and are + * namespaced by `gen_ai.response`. + * + * @since 1.0.0 + * @category Models + */ +export interface ResponseAttributes { + /** + * The unique identifier for the completion. + */ + readonly id?: string | null | undefined + /** + * The name of the model that generated the response. + */ + readonly model?: string | null | undefined + /** + * Array of reasons the model stopped generating tokens, corresponding to + * each generation received. + */ + readonly finishReasons?: ReadonlyArray | null | undefined +} + +/** + * The `gen_ai.operation.name` attribute has the following list of well-known + * values. + * + * If one of them applies, then the respective value **MUST** be used; + * otherwise, a custom value **MAY** be used. + * + * @since 1.0.0 + * @category Models + */ +export type WellKnownOperationName = "chat" | "embeddings" | "text_completion" + +/** + * The `gen_ai.system` attribute has the following list of well-known values. + * + * If one of them applies, then the respective value **MUST** be used; + * otherwise, a custom value **MAY** be used. + * + * @since 1.0.0 + * @category Models + */ +export type WellKnownSystem = + | "anthropic" + | "aws.bedrock" + | "az.ai.inference" + | "az.ai.openai" + | "cohere" + | "deepseek" + | "gemini" + | "groq" + | "ibm.watsonx.ai" + | "mistral_ai" + | "openai" + | "perplexity" + | "vertex_ai" + | "xai" + +/** + * Utility type for prefixing attribute names with a namespace. + * + * Transforms attribute keys by adding a prefix and formatting them according to + * OpenTelemetry conventions (camelCase to snake_case). + * + * @template Attributes - Record type containing the attributes to prefix + * @template Prefix - String literal type for the prefix to add + * + * @example + * ```ts + * import { Telemetry } from "@effect/ai" + * + * type RequestAttrs = { + * modelName: string + * maxTokens: number + * } + * + * type PrefixedAttrs = Telemetry.AttributesWithPrefix + * // Results in: { + * // "gen_ai.request.model_name": string + * // "gen_ai.request.max_tokens": number + * // } + * ``` + * + * @since 1.0.0 + * @category Utility Types + */ +export type AttributesWithPrefix, Prefix extends string> = { + [Name in keyof Attributes as `${Prefix}.${FormatAttributeName}`]: Attributes[Name] +} + +/** + * Utility type for converting camelCase names to snake_case format. + * + * This type recursively transforms string literal types from camelCase to + * snake_case, which is the standard format for OpenTelemetry attributes. + * + * @template T - String literal type to format + * + * @example + * ```ts + * import { Telemetry } from "@effect/ai" + * + * type Formatted1 = Telemetry.FormatAttributeName<"modelName"> // "model_name" + * type Formatted2 = Telemetry.FormatAttributeName<"maxTokens"> // "max_tokens" + * type Formatted3 = Telemetry.FormatAttributeName<"temperature"> // "temperature" + * ``` + * + * @since 1.0.0 + * @category Utility Types + */ +export type FormatAttributeName = T extends string ? + T extends `${infer First}${infer Rest}` + ? `${First extends Uppercase ? "_" : ""}${Lowercase}${FormatAttributeName}` + : T : + never + +/** + * Creates a function to add attributes to a span with a given prefix and key transformation. + * + * This utility function is used internally to create specialized functions for adding + * different types of telemetry attributes to OpenTelemetry spans. + * + * @example + * ```ts + * import { Telemetry } from "@effect/ai" + * import { String, Tracer } from "effect" + * + * const addCustomAttributes = Telemetry.addSpanAttributes( + * "custom.ai", + * String.camelToSnake + * ) + * + * // Usage with a span + * declare const span: Tracer.Span + * addCustomAttributes(span, { + * modelName: "gpt-4", + * maxTokens: 1000 + * }) + * // Results in attributes: "custom.ai.model_name" and "custom.ai.max_tokens" + * ``` + * + * @since 1.0.0 + * @category Utilities + */ +export const addSpanAttributes = ( + /** + * The prefix to add to all attribute keys. + */ + keyPrefix: string, + /** + * Function to transform attribute keys (e.g., camelCase to snake_case). + */ + transformKey: (key: string) => string +) => +>( + /** + * The OpenTelemetry span to add attributes to. + */ + span: Span, + /** + * The attributes to add to the span. + */ + attributes: Attributes +): void => { + for (const [key, value] of Object.entries(attributes)) { + if (Predicate.isNotNullable(value)) { + span.attribute(`${keyPrefix}.${transformKey(key)}`, value) + } + } +} + +const addSpanBaseAttributes = addSpanAttributes("gen_ai", String.camelToSnake) +const addSpanOperationAttributes = addSpanAttributes("gen_ai.operation", String.camelToSnake) +const addSpanRequestAttributes = addSpanAttributes("gen_ai.request", String.camelToSnake) +const addSpanResponseAttributes = addSpanAttributes("gen_ai.response", String.camelToSnake) +const addSpanTokenAttributes = addSpanAttributes("gen_ai.token", String.camelToSnake) +const addSpanUsageAttributes = addSpanAttributes("gen_ai.usage", String.camelToSnake) + +/** + * Configuration options for GenAI telemetry attributes. + * + * Combines base attributes with optional grouped attributes for comprehensive + * telemetry coverage of AI operations. + * + * @example + * ```ts + * import { Telemetry } from "@effect/ai" + * + * const telemetryOptions: Telemetry.GenAITelemetryAttributeOptions = { + * system: "openai", + * operation: { + * name: "chat" + * }, + * request: { + * model: "gpt-4-turbo", + * temperature: 0.7, + * maxTokens: 2000 + * }, + * response: { + * id: "chatcmpl-123", + * model: "gpt-4-turbo-2024-04-09", + * finishReasons: ["stop"] + * }, + * usage: { + * inputTokens: 50, + * outputTokens: 25 + * } + * } + * ``` + * + * @since 1.0.0 + * @category Models + */ +export type GenAITelemetryAttributeOptions = BaseAttributes & { + /** + * Operation-specific attributes (e.g., operation name). + */ + readonly operation?: OperationAttributes | undefined + /** + * Request-specific attributes (e.g., model parameters). + */ + readonly request?: RequestAttributes | undefined + /** + * Response-specific attributes (e.g., response metadata). + */ + readonly response?: ResponseAttributes | undefined + /** + * Token-specific attributes. + */ + readonly token?: TokenAttributes | undefined + /** + * Usage statistics attributes (e.g., token counts). + */ + readonly usage?: UsageAttributes | undefined +} + +/** + * Applies GenAI telemetry attributes to an OpenTelemetry span. + * + * This function adds standardized GenAI attributes to a span following OpenTelemetry + * semantic conventions. It supports both curried and direct application patterns. + * + * **Note**: This function mutates the provided span in-place. + * + * @example + * ```ts + * import { Telemetry } from "@effect/ai" + * import { Effect } from "effect" + * + * const directUsage = Effect.gen(function* () { + * const span = yield* Effect.currentSpan + * + * Telemetry.addGenAIAnnotations(span, { + * system: "openai", + * request: { model: "gpt-4", temperature: 0.7 }, + * usage: { inputTokens: 100, outputTokens: 50 } + * }) + * }) + * ``` + * + * @since 1.0.0 + * @category Utilities + */ +export const addGenAIAnnotations: { + ( + /** + * Telemetry attribute options to apply to the span. + */ + options: GenAITelemetryAttributeOptions + ): ( + /** + * OpenTelemetry span to add attributes to. + */ + span: Span + ) => void + ( + /** + * OpenTelemetry span to add attributes to. + */ + span: Span, + /** + * Telemetry attribute options to apply to the span. + */ + options: GenAITelemetryAttributeOptions + ): void +} = dual< + (options: GenAITelemetryAttributeOptions) => (span: Span) => void, + (span: Span, options: GenAITelemetryAttributeOptions) => void +>(2, (span, options) => { + addSpanBaseAttributes(span, { system: options.system }) + if (Predicate.isNotNullable(options.operation)) addSpanOperationAttributes(span, options.operation) + if (Predicate.isNotNullable(options.request)) addSpanRequestAttributes(span, options.request) + if (Predicate.isNotNullable(options.response)) addSpanResponseAttributes(span, options.response) + if (Predicate.isNotNullable(options.token)) addSpanTokenAttributes(span, options.token) + if (Predicate.isNotNullable(options.usage)) addSpanUsageAttributes(span, options.usage) +}) + +/** + * A function that can transform OpenTelemetry spans based on AI operation data. + * + * Span transformers receive the complete request/response context from AI operations + * and can add custom telemetry attributes, metrics, or other observability data. + * + * @example + * ```ts + * import { Telemetry } from "@effect/ai" + * + * const customTransformer: Telemetry.SpanTransformer = (options) => { + * // Add custom attributes based on the response + * const textParts = options.response.filter(part => part.type === "text") + * const totalTextLength = textParts.reduce((sum, part) => + * sum + (part.type === "text" ? part.text.length : 0), 0 + * ) + * + * // Add custom metrics + * console.log(`Generated ${totalTextLength} characters of text`) + * } + * ``` + * + * @since 1.0.0 + * @category Models + */ +export interface SpanTransformer { + ( + options: ProviderOptions & { + /** + * Array of response parts generated by the AI model. + */ + readonly response: ReadonlyArray> + } + ): void +} + +/** + * Context tag for providing a span transformer to large langauge model + * operations. + * + * The CurrentSpanTransformer allows you to inject custom span transformation + * logic into AI operations, enabling application-specific telemetry and + * observability patterns. + * + * @example + * ```ts + * import { Telemetry } from "@effect/ai" + * import * as Effect from "effect/Effect" + * + * declare const myAIOperation: Effect.Effect + * + * // Create a custom span transformer + * const loggingTransformer: Telemetry.SpanTransformer = (options) => { + * console.log(`AI request completed: ${options.response.length} part(s)`) + * options.response.forEach((part, index) => { + * console.log(`Part ${index}: ${part.type}`) + * }) + * } + * + * // Provide the transformer to your AI operations + * const program = myAIOperation.pipe( + * Effect.provideService( + * Telemetry.CurrentSpanTransformer, + * Telemetry.CurrentSpanTransformer.of(loggingTransformer) + * ) + * ) + * ``` + * + * @since 1.0.0 + * @category Context + */ +export class CurrentSpanTransformer extends Context.Tag("@effect/ai/Telemetry/CurrentSpanTransformer")< + CurrentSpanTransformer, + SpanTransformer +>() {} diff --git a/repos/effect/packages/ai/ai/src/Tokenizer.ts b/repos/effect/packages/ai/ai/src/Tokenizer.ts new file mode 100644 index 0000000..e05bf5e --- /dev/null +++ b/repos/effect/packages/ai/ai/src/Tokenizer.ts @@ -0,0 +1,195 @@ +/** + * The `Tokenizer` module provides tokenization and text truncation capabilities + * for large language model text processing workflows. + * + * This module offers services for converting text into tokens and truncating + * prompts based on token limits, essential for managing context length + * constraints in large language models. + * + * @example + * ```ts + * import { Tokenizer, Prompt } from "@effect/ai" + * import { Effect } from "effect" + * + * const tokenizeText = Effect.gen(function* () { + * const tokenizer = yield* Tokenizer.Tokenizer + * const tokens = yield* tokenizer.tokenize("Hello, world!") + * console.log(`Token count: ${tokens.length}`) + * return tokens + * }) + * ``` + * + * @example + * ```ts + * import { Tokenizer, Prompt } from "@effect/ai" + * import { Effect } from "effect" + * + * // Truncate a prompt to fit within token limits + * const truncatePrompt = Effect.gen(function* () { + * const tokenizer = yield* Tokenizer.Tokenizer + * const longPrompt = "This is a very long prompt..." + * const truncated = yield* tokenizer.truncate(longPrompt, 100) + * return truncated + * }) + * ``` + * + * @since 1.0.0 + */ +import * as Context from "effect/Context" +import * as Effect from "effect/Effect" +import * as Predicate from "effect/Predicate" +import type * as AiError from "./AiError.js" +import * as Prompt from "./Prompt.js" + +/** + * The `Tokenizer` service tag for dependency injection. + * + * This tag provides access to tokenization functionality throughout your + * application, enabling token counting and prompt truncation capabilities. + * + * @example + * ```ts + * import { Tokenizer } from "@effect/ai" + * import { Effect } from "effect" + * + * const useTokenizer = Effect.gen(function* () { + * const tokenizer = yield* Tokenizer.Tokenizer + * const tokens = yield* tokenizer.tokenize("Hello, world!") + * return tokens.length + * }) + * ``` + * + * @since 1.0.0 + * @category Context + */ +export class Tokenizer extends Context.Tag("@effect/ai/Tokenizer")< + Tokenizer, + Service +>() {} + +/** + * Tokenizer service interface providing text tokenization and truncation + * operations. + * + * This interface defines the core operations for converting text to tokens and + * managing content length within token limits for AI model compatibility. + * + * @example + * ```ts + * import { Tokenizer, Prompt } from "@effect/ai" + * import { Effect } from "effect" + * + * const customTokenizer: Tokenizer.Service = { + * tokenize: (input) => + * Effect.succeed(input.toString().split(' ').map((_, i) => i)), + * truncate: (input, maxTokens) => + * Effect.succeed(Prompt.make(input.toString().slice(0, maxTokens * 5))) + * } + * ``` + * + * @since 1.0.0 + * @category Models + */ +export interface Service { + /** + * Converts text input into an array of token numbers. + */ + readonly tokenize: ( + /** + * The text input to tokenize. + */ + input: Prompt.RawInput + ) => Effect.Effect, AiError.AiError> + /** + * Truncates text input to fit within the specified token limit. + */ + readonly truncate: ( + /** + * The text input to truncate. + */ + input: Prompt.RawInput, + /** + * Maximum number of tokens to retain. + */ + tokens: number + ) => Effect.Effect +} + +/** + * Creates a Tokenizer service implementation from tokenization options. + * + * This function constructs a complete Tokenizer service by providing a + * tokenization function. The service handles both tokenization and + * truncation operations using the provided tokenizer. + * + * @example + * ```ts + * import { Tokenizer, Prompt } from "@effect/ai" + * import { Effect } from "effect" + * + * // Simple word-based tokenizer + * const wordTokenizer = Tokenizer.make({ + * tokenize: (prompt) => + * Effect.succeed( + * prompt.content + * .flatMap(msg => + * typeof msg.content === "string" + * ? msg.content.split(' ') + * : msg.content.flatMap(part => + * part.type === "text" ? part.text.split(' ') : [] + * ) + * ) + * .map((_, index) => index) + * ) + * }) + * ``` + * + * @since 1.0.0 + * @category Constructors + */ +export const make = (options: { + /** + * Function to tokenize a prompt into an array of token numbers. + */ + readonly tokenize: ( + /** + * The prompt to tokenize. + */ + content: Prompt.Prompt + ) => Effect.Effect, AiError.AiError> +}): Service => + Tokenizer.of({ + tokenize(input) { + return options.tokenize(Prompt.make(input)) + }, + truncate(input, tokens) { + return truncate(Prompt.make(input), options.tokenize, tokens) + } + }) + +const truncate = ( + self: Prompt.Prompt, + tokenize: (input: Prompt.Prompt) => Effect.Effect, AiError.AiError>, + maxTokens: number +): Effect.Effect => + Effect.suspend(() => { + let count = 0 + let inputMessages = self.content + let outputMessages: Array = [] + const loop: Effect.Effect = Effect.suspend(() => { + const message = inputMessages[inputMessages.length - 1] + if (Predicate.isUndefined(message)) { + return Effect.succeed(Prompt.fromMessages(outputMessages)) + } + inputMessages = inputMessages.slice(0, inputMessages.length - 1) + return Effect.flatMap(tokenize(Prompt.fromMessages([message])), (tokens) => { + count += tokens.length + if (count > maxTokens) { + return Effect.succeed(Prompt.fromMessages(outputMessages)) + } + outputMessages = [message, ...outputMessages] + return loop + }) + }) + return loop + }) diff --git a/repos/effect/packages/ai/ai/src/Tool.ts b/repos/effect/packages/ai/ai/src/Tool.ts new file mode 100644 index 0000000..0512806 --- /dev/null +++ b/repos/effect/packages/ai/ai/src/Tool.ts @@ -0,0 +1,1509 @@ +/** + * The `Tool` module provides functionality for defining and managing tools + * that language models can call to augment their capabilities. + * + * This module enables creation of both user-defined and provider-defined tools, + * with full schema validation, type safety, and handler support. Tools allow + * AI models to perform actions like searching databases, calling APIs, or + * executing code within your application context. + * + * @example + * ```ts + * import { Tool } from "@effect/ai" + * import { Schema } from "effect" + * + * // Define a simple calculator tool + * const Calculator = Tool.make("Calculator", { + * description: "Performs basic arithmetic operations", + * parameters: { + * operation: Schema.Literal("add", "subtract", "multiply", "divide"), + * a: Schema.Number, + * b: Schema.Number + * }, + * success: Schema.Number + * }) + * ``` + * + * @since 1.0.0 + */ +import * as Context from "effect/Context" +import type * as Effect from "effect/Effect" +import { constFalse, constTrue, identity } from "effect/Function" +import * as JsonSchema from "effect/JSONSchema" +import * as Option from "effect/Option" +import type { Pipeable } from "effect/Pipeable" +import { pipeArguments } from "effect/Pipeable" +import * as Predicate from "effect/Predicate" +import * as Schema from "effect/Schema" +import * as AST from "effect/SchemaAST" +import type { Covariant } from "effect/Types" +import type * as AiError from "./AiError.js" + +// ============================================================================= +// Type Ids +// ============================================================================= + +/** + * Unique identifier for user-defined tools. + * + * @since 1.0.0 + * @category Type Ids + */ +export const TypeId = "~@effect/ai/Tool" + +/** + * Type-level representation of the user-defined tool identifier. + * + * @since 1.0.0 + * @category Type Ids + */ +export type TypeId = typeof TypeId + +/** + * Unique identifier for provider-defined tools. + * + * @since 1.0.0 + * @category Type Ids + */ +export const ProviderDefinedTypeId = "~@effect/ai/Tool/ProviderDefined" + +/** + * Type-level representation of the provider-defined tool identifier. + * + * @since 1.0.0 + * @category Type Ids + */ +export type ProviderDefinedTypeId = typeof ProviderDefinedTypeId + +// ============================================================================= +// Models +// ============================================================================= + +/** + * A user-defined tool that language models can call to perform actions. + * + * Tools represent actionable capabilities that large language models can invoke + * to extend their functionality beyond text generation. Each tool has a defined + * schema for parameters, results, and failures. + * + * @example + * ```ts + * import { Tool } from "@effect/ai" + * import { Schema } from "effect" + * + * // Create a weather lookup tool + * const GetWeather = Tool.make("GetWeather", { + * description: "Get current weather for a location", + * parameters: { + * location: Schema.String, + * units: Schema.Literal("celsius", "fahrenheit") + * }, + * success: Schema.Struct({ + * temperature: Schema.Number, + * condition: Schema.String, + * humidity: Schema.Number + * }) + * }) + * ``` + * + * @since 1.0.0 + * @category Models + */ +export interface Tool< + Name extends string, + Config extends { + readonly parameters: AnyStructSchema + readonly success: Schema.Schema.Any + readonly failure: Schema.Schema.All + readonly failureMode: FailureMode + }, + Requirements = never +> extends Tool.Variance { + /** + * The tool identifier which is used to uniquely identify the tool. + */ + readonly id: string + + /** + * The name of the tool. + */ + readonly name: Name + + /** + * The optional description of the tool. + */ + readonly description?: string | undefined + + /** + * The strategy used for handling errors returned from tool call handler + * execution. + * + * If set to `"error"` (the default), errors that occur during tool call + * handler execution will be returned in the error channel of the calling + * effect. + * + * If set to `"return"`, errors that occur during tool call handler execution + * will be captured and returned as part of the tool call result. + */ + readonly failureMode: FailureMode + + /** + * A `Schema` representing the parameters that a tool must be called with. + */ + readonly parametersSchema: Config["parameters"] + + /** + * A `Schema` representing the value that a tool must return when called if + * the tool call is successful. + */ + readonly successSchema: Config["success"] + + /** + * A `Schema` representing the value that a tool must return when called if + * it fails. + */ + readonly failureSchema: Config["failure"] + + /** + * A `Context` object containing tool annotations which can store metadata + * about the tool. + */ + readonly annotations: Context.Context + + /** + * Adds a _request-level_ dependency which must be provided before the tool + * call handler can be executed. + * + * This can be useful when you want to enforce that a particular dependency + * **MUST** be provided to each request to the large language model provider + * instead of being provided when creating the tool call handler layer. + */ + addDependency( + tag: Context.Tag + ): Tool + + /** + * Set the schema to use to validate the result of a tool call when successful. + */ + setParameters< + ParametersSchema extends Schema.Struct | Schema.Struct.Fields + >( + schema: ParametersSchema + ): Tool< + Name, + { + readonly parameters: ParametersSchema extends Schema.Struct ? ParametersSchema + : ParametersSchema extends Schema.Struct.Fields ? Schema.Struct + : never + readonly success: Config["success"] + readonly failure: Config["failure"] + readonly failureMode: Config["failureMode"] + }, + Requirements + > + + /** + * Set the schema to use to validate the result of a tool call when successful. + */ + setSuccess( + schema: SuccessSchema + ): Tool< + Name, + { + readonly parameters: Config["parameters"] + readonly success: SuccessSchema + readonly failure: Config["failure"] + readonly failureMode: Config["failureMode"] + }, + Requirements + > + + /** + * Set the schema to use to validate the result of a tool call when it fails. + */ + setFailure( + schema: FailureSchema + ): Tool< + Name, + { + readonly parameters: Config["parameters"] + readonly success: Config["success"] + readonly failure: FailureSchema + readonly failureMode: Config["failureMode"] + }, + Requirements + > + + /** + * Add an annotation to the tool. + */ + annotate( + tag: Context.Tag, + value: S + ): Tool + + /** + * Add many annotations to the tool. + */ + annotateContext( + context: Context.Context + ): Tool +} + +/** + * A provider-defined tool is a tool which is built into a large language model + * provider (e.g. web search, code execution). + * + * These tools are executed by the large language model provider rather than + * by your application. However, they can optionally require custom handlers + * implemented in your application to process provider generated results. + * + * @example + * ```ts + * import { Tool } from "@effect/ai" + * import { Schema } from "effect" + * + * // Define a web search tool provided by OpenAI + * const WebSearch = Tool.providerDefined({ + * id: "openai.web_search", + * toolkitName: "WebSearch", + * providerName: "web_search", + * args: { + * query: Schema.String + * }, + * success: Schema.Struct({ + * results: Schema.Array(Schema.Struct({ + * title: Schema.String, + * url: Schema.String, + * snippet: Schema.String + * })) + * }) + * }) + * ``` + * + * @since 1.0.0 + * @category Models + */ +export interface ProviderDefined< + Name extends string, + Config extends { + readonly args: AnyStructSchema + readonly parameters: AnyStructSchema + readonly success: Schema.Schema.Any + readonly failure: Schema.Schema.All + readonly failureMode: FailureMode + } = { + readonly args: Schema.Struct<{}> + readonly parameters: Schema.Struct<{}> + readonly success: typeof Schema.Void + readonly failure: typeof Schema.Never + readonly failureMode: "error" + }, + RequiresHandler extends boolean = false +> extends + Tool< + Name, + { + readonly parameters: Config["parameters"] + readonly success: Config["success"] + readonly failure: Config["failure"] + readonly failureMode: Config["failureMode"] + } + >, + Tool.ProviderDefinedProto +{ + /** + * The arguments passed to the provider-defined tool. + */ + readonly args: Config["args"]["Encoded"] + + /** + * A `Schema` representing the arguments provided by the end-user which will + * be used to configure the behavior of the provider-defined tool. + */ + readonly argsSchema: Config["args"] + + /** + * Name of the tool as recognized by the large language model provider. + */ + readonly providerName: string + + /** + * If set to `true`, this provider-defined tool will require a user-defined + * tool call handler to be provided when converting the `Toolkit` containing + * this tool into a `Layer`. + */ + readonly requiresHandler: RequiresHandler +} + +/** + * The strategy used for handling errors returned from tool call handler + * execution. + * + * If set to `"error"` (the default), errors that occur during tool call handler + * execution will be returned in the error channel of the calling effect. + * + * If set to `"return"`, errors that occur during tool call handler execution + * will be captured and returned as part of the tool call result. + * + * @since 1.0.0 + * @category Models + */ +export type FailureMode = "error" | "return" + +/** + * @since 1.0.0 + */ +export declare namespace Tool { + /** + * @since 1.0.0 + * @category Models + */ + export interface Variance extends Pipeable { + readonly [TypeId]: VarianceStruct + } + + /** + * @since 1.0.0 + * @category Models + */ + export interface VarianceStruct { + readonly _Requirements: Covariant + } + + /** + * @since 1.0.0 + * @category Models + */ + export interface ProviderDefinedProto { + readonly [ProviderDefinedTypeId]: ProviderDefinedTypeId + } +} + +// ============================================================================= +// Type Guards +// ============================================================================= + +/** + * Type guard to check if a value is a user-defined tool. + * + * @example + * ```ts + * import { Tool } from "@effect/ai" + * import { Schema } from "effect" + * + * const UserDefinedTool = Tool.make("Calculator", { + * description: "Performs basic arithmetic operations", + * parameters: { + * operation: Schema.Literal("add", "subtract", "multiply", "divide"), + * a: Schema.Number, + * b: Schema.Number + * }, + * success: Schema.Number + * }) + * + * const ProviderDefinedTool = Tool.providerDefined({ + * id: "openai.web_search", + * toolkitName: "WebSearch", + * providerName: "web_search", + * args: { + * query: Schema.String + * }, + * success: Schema.Struct({ + * results: Schema.Array(Schema.Struct({ + * title: Schema.String, + * url: Schema.String, + * snippet: Schema.String + * })) + * }) + * }) + * + * console.log(Tool.isUserDefined(UserDefinedTool)) // true + * console.log(Tool.isUserDefined(ProviderDefinedTool)) // false + * ``` + * + * @since 1.0.0 + * @category Guards + */ +export const isUserDefined = (u: unknown): u is Tool => + Predicate.hasProperty(u, TypeId) && !isProviderDefined(u) + +/** + * Type guard to check if a value is a provider-defined tool. + * + * @param u - The value to check + * @returns `true` if the value is a provider-defined `Tool`, `false` otherwise + * + * @example + * ```ts + * import { Tool } from "@effect/ai" + * import { Schema } from "effect" + * + * const UserDefinedTool = Tool.make("Calculator", { + * description: "Performs basic arithmetic operations", + * parameters: { + * operation: Schema.Literal("add", "subtract", "multiply", "divide"), + * a: Schema.Number, + * b: Schema.Number + * }, + * success: Schema.Number + * }) + * + * const ProviderDefinedTool = Tool.providerDefined({ + * id: "openai.web_search", + * toolkitName: "WebSearch", + * providerName: "web_search", + * args: { + * query: Schema.String + * }, + * success: Schema.Struct({ + * results: Schema.Array(Schema.Struct({ + * title: Schema.String, + * url: Schema.String, + * snippet: Schema.String + * })) + * }) + * }) + * + * console.log(Tool.isUserDefined(UserDefinedTool)) // false + * console.log(Tool.isUserDefined(ProviderDefinedTool)) // true + * ``` + * + * @since 1.0.0 + * @category Guards + */ +export const isProviderDefined = ( + u: unknown +): u is ProviderDefined => Predicate.hasProperty(u, ProviderDefinedTypeId) + +// ============================================================================= +// Utility Types +// ============================================================================= + +/** + * A type which represents any `Tool`. + * + * @since 1.0.0 + * @category Utility Types + */ +export interface Any extends Pipeable { + readonly [TypeId]: { + readonly _Requirements: Covariant + } + readonly id: string + readonly name: string + readonly description?: string | undefined + readonly parametersSchema: AnyStructSchema + readonly successSchema: Schema.Schema.Any + readonly failureSchema: Schema.Schema.All + readonly failureMode: FailureMode + readonly annotations: Context.Context +} + +/** + * A type which represents any provider-defined `Tool`. + * + * @since 1.0.0 + * @category Utility Types + */ +export interface AnyProviderDefined extends Any { + readonly args: any + readonly argsSchema: AnyStructSchema + readonly requiresHandler: boolean + readonly providerName: string + readonly decodeResult: ( + result: unknown + ) => Effect.Effect +} + +/** + * @since 1.0.0 + * @category Utility Types + */ +export interface AnyStructSchema extends Pipeable { + readonly [Schema.TypeId]: any + readonly make: any + readonly Type: any + readonly Encoded: any + readonly Context: any + readonly ast: AST.AST + readonly fields: Schema.Struct.Fields + readonly annotations: any +} + +/** + * @since 1.0.0 + * @category Utility Types + */ +export interface AnyTaggedRequestSchema extends AnyStructSchema { + readonly _tag: string + readonly success: Schema.Schema.Any + readonly failure: Schema.Schema.All +} + +/** + * A utility type to convert a `Schema.TaggedRequest` into an `Tool`. + * + * @since 1.0.0 + * @category Utility Types + */ +export interface FromTaggedRequest extends + Tool< + S["_tag"], + { + readonly parameters: S + readonly success: S["success"] + readonly failure: S["failure"] + readonly failureMode: "error" + } + > +{} + +/** + * A utility type to extract the `Name` type from an `Tool`. + * + * @since 1.0.0 + * @category Utility Types + */ +export type Name = T extends Tool< + infer _Name, + infer _Config, + infer _Requirements +> ? _Name + : never + +/** + * A utility type to extract the type of the tool call parameters. + * + * @since 1.0.0 + * @category Utility Types + */ +export type Parameters = T extends Tool< + infer _Name, + infer _Config, + infer _Requirements +> ? Schema.Struct.Type<_Config["parameters"]["fields"]> + : never + +/** + * A utility type to extract the encoded type of the tool call parameters. + * + * @since 1.0.0 + * @category Utility Types + */ +export type ParametersEncoded = T extends Tool< + infer _Name, + infer _Config, + infer _Requirements +> ? Schema.Schema.Encoded<_Config["parameters"]> + : never + +/** + * A utility type to extract the schema for the parameters which an `Tool` + * must be called with. + * + * @since 1.0.0 + * @category Utility Types + */ +export type ParametersSchema = T extends Tool< + infer _Name, + infer _Config, + infer _Requirements +> ? _Config["parameters"] + : never + +/** + * A utility type to extract the type of the tool call result when it succeeds. + * + * @since 1.0.0 + * @category Utility Types + */ +export type Success = T extends Tool< + infer _Name, + infer _Config, + infer _Requirements +> ? Schema.Schema.Type<_Config["success"]> + : never + +/** + * A utility type to extract the encoded type of the tool call result when + * it succeeds. + * + * @since 1.0.0 + * @category Utility Types + */ +export type SuccessEncoded = T extends Tool< + infer _Name, + infer _Config, + infer _Requirements +> ? Schema.Schema.Encoded<_Config["success"]> + : never + +/** + * A utility type to extract the schema for the return type of a tool call when + * the tool call succeeds. + * + * @since 1.0.0 + * @category Utility Types + */ +export type SuccessSchema = T extends Tool< + infer _Name, + infer _Config, + infer _Requirements +> ? _Config["success"] + : never + +/** + * A utility type to extract the type of the tool call result when it fails. + * + * @since 1.0.0 + * @category Utility Types + */ +export type Failure = T extends Tool< + infer _Name, + infer _Config, + infer _Requirements +> ? Schema.Schema.Type<_Config["failure"]> + : never + +/** + * A utility type to extract the encoded type of the tool call result when + * it fails. + * + * @since 1.0.0 + * @category Utility Types + */ +export type FailureEncoded = T extends Tool< + infer _Name, + infer _Config, + infer _Requirements +> ? Schema.Schema.Encoded<_Config["failure"]> + : never + +/** + * A utility type to extract the type of the tool call result whether it + * succeeds or fails. + * + * @since 1.0.0 + * @category Utility Types + */ +export type Result = T extends Tool< + infer _Name, + infer _Config, + infer _Requirements +> ? Success | Failure + : never + +/** + * A utility type to extract the encoded type of the tool call result whether + * it succeeds or fails. + * + * @since 1.0.0 + * @category Utility Types + */ +export type ResultEncoded = T extends Tool< + infer _Name, + infer _Config, + infer _Requirements +> ? SuccessEncoded | FailureEncoded + : never + +/** + * A utility type to extract the requirements of an `Tool`. + * + * @since 1.0.0 + * @category Utility Types + */ +export type Requirements = T extends Tool< + infer _Name, + infer _Config, + infer _Requirements +> ? + | _Config["parameters"]["Context"] + | _Config["success"]["Context"] + | _Config["failure"]["Context"] + | _Requirements + : never + +/** + * Represents an `Tool` that has been implemented within the application. + * + * @since 1.0.0 + * @category Models + */ +export interface Handler { + readonly _: unique symbol + readonly name: Name + readonly context: Context.Context + readonly handler: (params: any) => Effect.Effect +} + +/** + * Represents the result of calling the handler for a particular `Tool`. + * + * @since 1.0.0 + * @category Models + */ +export interface HandlerResult { + /** + * Whether the result of executing the tool call handler was an error or not. + */ + readonly isFailure: boolean + /** + * The result of executing the handler for a particular tool. + */ + readonly result: Result + /** + * The pre-encoded tool call result of executing the handler for a particular + * tool as a JSON-serializable value. The encoded result can be incorporated + * into subsequent requests to the large language model. + */ + readonly encodedResult: unknown +} + +/** + * A utility type which represents the possible errors that can be raised by + * a tool call's handler. + * + * @since 1.0.0 + * @category Utility Types + */ +export type HandlerError = T extends Tool< + infer _Name, + infer _Config, + infer _Requirements +> ? _Config["failureMode"] extends "error" ? _Config["failure"]["Type"] + : never + : never + +/** + * A utility type to create a union of `Handler` types for all tools in a + * record. + * + * @since 1.0.0 + * @category Utility Types + */ +export type HandlersFor> = { + [Name in keyof Tools]: RequiresHandler extends true ? Handler + : never +}[keyof Tools] + +/** + * A utility type to determine if the specified tool requires a user-defined + * handler to be implemented. + * + * @since 1.0.0 + * @category Utility Types + */ +export type RequiresHandler = Tool extends ProviderDefined< + infer _Name, + infer _Config, + infer _RequiresHandler +> ? _RequiresHandler + : true + +// ============================================================================= +// Constructors +// ============================================================================= + +const Proto = { + [TypeId]: { _Requirements: identity }, + pipe() { + return pipeArguments(this, arguments) + }, + addDependency(this: Any) { + return userDefinedProto({ ...this }) + }, + setParameters( + this: Any, + parametersSchema: Schema.Struct | Schema.Struct.Fields + ) { + return userDefinedProto({ + ...this, + parametersSchema: Schema.isSchema(parametersSchema) + ? (parametersSchema as any) + : Schema.Struct(parametersSchema as any) + }) + }, + setSuccess(this: Any, successSchema: Schema.Schema.Any) { + return userDefinedProto({ + ...this, + successSchema + }) + }, + setFailure(this: Any, failureSchema: Schema.Schema.All) { + return userDefinedProto({ + ...this, + failureSchema + }) + }, + annotate(this: Any, tag: Context.Tag, value: S) { + return userDefinedProto({ + ...this, + annotations: Context.add(this.annotations, tag, value) + }) + }, + annotateContext(this: Any, context: Context.Context) { + return userDefinedProto({ + ...this, + annotations: Context.merge(this.annotations, context) + }) + } +} + +const ProviderDefinedProto = { + ...Proto, + [ProviderDefinedTypeId]: ProviderDefinedTypeId +} + +const userDefinedProto = < + const Name extends string, + Parameters extends AnyStructSchema, + Success extends Schema.Schema.Any, + Failure extends Schema.Schema.All, + Mode extends FailureMode +>(options: { + readonly name: Name + readonly description?: string | undefined + readonly parametersSchema: Parameters + readonly successSchema: Success + readonly failureSchema: Failure + readonly annotations: Context.Context + readonly failureMode: Mode +}): Tool< + Name, + { + readonly parameters: Parameters + readonly success: Success + readonly failure: Failure + readonly failureMode: Mode + } +> => { + const self = Object.assign(Object.create(Proto), options) + self.id = `@effect/ai/Tool/${options.name}` + return self +} + +const providerDefinedProto = < + const Name extends string, + Args extends AnyStructSchema, + Parameters extends AnyStructSchema, + Success extends Schema.Schema.Any, + Failure extends Schema.Schema.All, + RequiresHandler extends boolean, + Mode extends FailureMode +>(options: { + readonly id: string + readonly name: Name + readonly providerName: string + readonly args: Args["Encoded"] + readonly argsSchema: Args + readonly requiresHandler: RequiresHandler + readonly parametersSchema: Parameters + readonly successSchema: Success + readonly failureSchema: Failure + readonly failureMode: FailureMode +}): ProviderDefined< + Name, + { + readonly args: Args + readonly parameters: Parameters + readonly success: Success + readonly failure: Failure + readonly failureMode: Mode + }, + RequiresHandler +> => Object.assign(Object.create(ProviderDefinedProto), options) + +const constEmptyStruct = Schema.Struct({}) + +/** + * Creates a user-defined tool with the specified name and configuration. + * + * This is the primary constructor for creating custom tools that AI models + * can call. The tool definition includes parameter validation, success/failure + * schemas, and optional service dependencies. + * + * @example + * ```ts + * import { Tool } from "@effect/ai" + * import { Schema } from "effect" + * + * // Simple tool with no parameters + * const GetCurrentTime = Tool.make("GetCurrentTime", { + * description: "Returns the current timestamp", + * success: Schema.Number + * }) + * ``` + * + * @since 1.0.0 + * @category Constructors + */ +export const make = < + const Name extends string, + Parameters extends Schema.Struct.Fields = {}, + Success extends Schema.Schema.Any = typeof Schema.Void, + Failure extends Schema.Schema.All = typeof Schema.Never, + Mode extends FailureMode | undefined = undefined, + Dependencies extends Array> = [] +>( + /** + * The unique name identifier for this tool. + */ + name: Name, + options?: { + /** + * An optional description explaining what the tool does. + */ + readonly description?: string | undefined + /** + * Schema defining the parameters this tool accepts. + */ + readonly parameters?: Parameters | undefined + /** + * Schema for successful tool execution results. + */ + readonly success?: Success | undefined + /** + * Schema for tool execution failures. + */ + readonly failure?: Failure | undefined + /** + * The strategy used for handling errors returned from tool call handler + * execution. + * + * If set to `"error"` (the default), errors that occur during tool call handler + * execution will be returned in the error channel of the calling effect. + * + * If set to `"return"`, errors that occur during tool call handler execution + * will be captured and returned as part of the tool call result. + */ + readonly failureMode?: Mode + /** + * Service dependencies required by the tool handler. + */ + readonly dependencies?: Dependencies | undefined + } +): Tool< + Name, + { + readonly parameters: Schema.Struct + readonly success: Success + readonly failure: Failure + readonly failureMode: Mode extends undefined ? "error" : Mode + }, + Context.Tag.Identifier +> => { + const successSchema = options?.success ?? Schema.Void + const failureSchema = options?.failure ?? Schema.Never + return userDefinedProto({ + name, + description: options?.description, + parametersSchema: options?.parameters + ? Schema.Struct(options?.parameters as any) + : constEmptyStruct, + successSchema, + failureSchema, + failureMode: options?.failureMode ?? "error", + annotations: Context.empty() + }) as any +} + +/** + * Creates a provider-defined tool which leverages functionality built into a + * large language model provider (e.g. web search, code execution). + * + * These tools are executed by the large language model provider rather than + * by your application. However, they can optionally require custom handlers + * implemented in your application to process provider generated results. + * + * @example + * ```ts + * import { Tool } from "@effect/ai" + * import { Schema } from "effect" + * + * // Web search tool provided by OpenAI + * const WebSearch = Tool.providerDefined({ + * id: "openai.web_search", + * toolkitName: "WebSearch", + * providerName: "web_search", + * args: { + * query: Schema.String + * }, + * success: Schema.Struct({ + * results: Schema.Array(Schema.Struct({ + * title: Schema.String, + * url: Schema.String, + * content: Schema.String + * })) + * }) + * }) + * ``` + * + * @since 1.0.0 + * @category Constructors + */ +export const providerDefined = < + const Name extends string, + Args extends Schema.Struct.Fields = {}, + Parameters extends Schema.Struct.Fields = {}, + Success extends Schema.Schema.Any = typeof Schema.Void, + Failure extends Schema.Schema.All = typeof Schema.Never, + RequiresHandler extends boolean = false +>(options: { + /** + * Unique identifier following format `.`. + */ + readonly id: `${string}.${string}` + /** + * Name used by the Toolkit to identify this tool. + */ + readonly toolkitName: Name + /** + * Name of the tool as recognized by the AI provider. + */ + readonly providerName: string + /** + * Schema for user-provided configuration arguments. + */ + readonly args: Args + /** + * Whether this tool requires a custom handler implementation. + */ + readonly requiresHandler?: RequiresHandler | undefined + /** + * Schema for parameters the provider sends when calling the tool. + */ + readonly parameters?: Parameters | undefined + /** + * Schema for successful tool execution results. + */ + readonly success?: Success | undefined + /** + * Schema for failed tool execution results. + */ + readonly failure?: Failure | undefined +}) => +( + args: RequiresHandler extends true ? Schema.Simplify< + Schema.Struct.Encoded & { + /** + * The strategy used for handling errors returned from tool call handler + * execution. + * + * If set to `"error"` (the default), errors that occur during tool call handler + * execution will be returned in the error channel of the calling effect. + * + * If set to `"return"`, errors that occur during tool call handler execution + * will be captured and returned as part of the tool call result. + */ + readonly failureMode?: Mode + } + > + : Schema.Simplify> +): ProviderDefined< + Name, + { + readonly args: Schema.Struct + readonly parameters: Schema.Struct + readonly success: Success + readonly failure: Failure + readonly failureMode: Mode extends undefined ? "error" : Mode + }, + RequiresHandler +> => { + const failureMode = "failureMode" in args ? args.failureMode : undefined + const successSchema = options?.success ?? Schema.Void + const failureSchema = options?.failure ?? Schema.Never + return providerDefinedProto({ + id: options.id, + name: options.toolkitName, + providerName: options.providerName, + args, + argsSchema: Schema.Struct(options.args as any), + requiresHandler: options.requiresHandler ?? false, + parametersSchema: options?.parameters + ? Schema.Struct(options?.parameters as any) + : constEmptyStruct, + successSchema, + failureSchema, + failureMode: failureMode ?? "error" + }) as any +} + +/** + * Creates a Tool from a Schema.TaggedRequest. + * + * This utility function converts Effect's TaggedRequest schemas into Tool + * definitions, automatically mapping the request parameters, success, and + * failure schemas. + * + * @example + * ```ts + * import { Tool } from "@effect/ai" + * import { Schema } from "effect" + * + * // Define a tagged request for user operations + * class GetUser extends Schema.TaggedRequest()("GetUser", { + * success: Schema.Struct({ + * id: Schema.Number, + * name: Schema.String, + * email: Schema.String + * }), + * failure: Schema.Struct({ + * error: Schema.Literal("UserNotFound", "DatabaseError"), + * message: Schema.String + * }), + * payload: { + * userId: Schema.Number + * } + * }) {} + * + * // Convert to a Tool + * const getUserTool = Tool.fromTaggedRequest(GetUser) + * ``` + * + * @since 1.0.0 + * @category Constructors + */ +export const fromTaggedRequest = ( + schema: S +): FromTaggedRequest => + userDefinedProto({ + name: schema._tag, + description: Option.getOrUndefined( + AST.getDescriptionAnnotation((schema.ast as any).to) + ), + parametersSchema: schema, + successSchema: schema.success, + failureSchema: schema.failure, + failureMode: "error", + annotations: Context.empty() + }) as any + +// ============================================================================= +// Utilities +// ============================================================================= + +/** + * Extracts the description from a tool's metadata. + * + * Returns the tool's description if explicitly set, otherwise attempts to + * extract it from the parameter schema's AST annotations. + * + * @example + * ```ts + * import { Tool } from "@effect/ai" + * + * const myTool = Tool.make("example", { + * description: "This is an example tool" + * }) + * + * const description = Tool.getDescription(myTool) + * console.log(description) // "This is an example tool" + * ``` + * + * @since 1.0.0 + * @category Utilities + */ +export const getDescription = < + Name extends string, + Config extends { + readonly parameters: AnyStructSchema + readonly success: Schema.Schema.Any + readonly failure: Schema.Schema.All + readonly failureMode: FailureMode + } +>( + /** + * The tool to get the description from. + */ + tool: Tool +): string | undefined => { + if (Predicate.isNotUndefined(tool.description)) { + return tool.description + } + return getDescriptionFromSchemaAst(tool.parametersSchema.ast) +} + +/** + * @since 1.0.0 + * @category Utilities + */ +export const getDescriptionFromSchemaAst = ( + ast: AST.AST +): string | undefined => { + const annotations = ast._tag === "Transformation" + ? { + ...ast.to.annotations, + ...ast.annotations + } + : ast.annotations + return AST.DescriptionAnnotationId in annotations + ? (annotations[AST.DescriptionAnnotationId] as string) + : undefined +} + +/** + * Generates a JSON Schema for a tool. + * + * This function creates a JSON Schema representation that can be used by + * large language models to indicate the structure and type of the parameters + * that a given tool call should receive. + * + * @example + * ```ts + * import { Tool } from "@effect/ai" + * import { Schema } from "effect" + * + * const weatherTool = Tool.make("get_weather", { + * parameters: { + * location: Schema.String, + * units: Schema.optional(Schema.Literal("celsius", "fahrenheit")) + * } + * }) + * + * const jsonSchema = Tool.getJsonSchema(weatherTool) + * console.log(jsonSchema) + * // { + * // type: "object", + * // properties: { + * // location: { type: "string" }, + * // units: { type: "string", enum: ["celsius", "fahrenheit"] } + * // }, + * // required: ["location"] + * // } + * ``` + * + * @since 1.0.0 + * @category Utilities + */ +export const getJsonSchema = < + Name extends string, + Config extends { + readonly parameters: AnyStructSchema + readonly success: Schema.Schema.Any + readonly failure: Schema.Schema.All + readonly failureMode: FailureMode + } +>( + tool: Tool +): JsonSchema.JsonSchema7 => getJsonSchemaFromSchemaAst(tool.parametersSchema.ast) + +/** + * @since 1.0.0 + * @category Utilities + */ +export const getJsonSchemaFromSchemaAst = ( + ast: AST.AST +): JsonSchema.JsonSchema7 => { + const $defs = {} + const schema = JsonSchema.fromAST(ast, { + definitions: $defs, + topLevelReferenceStrategy: "skip" + }) + if (Object.keys($defs).length === 0) return schema + ;(schema as any).$defs = $defs + return schema +} + +// ============================================================================= +// Annotations +// ============================================================================= + +/** + * Annotation for providing a human-readable title for tools. + * + * @example + * ```ts + * import { Tool } from "@effect/ai" + * + * const myTool = Tool.make("calculate_tip") + * .annotate(Tool.Title, "Tip Calculator") + * ``` + * + * @since 1.0.0 + * @category Annotations + */ +export class Title extends Context.Tag("@effect/ai/Tool/Title")< + Title, + string +>() {} + +/** + * Annotation indicating whether a tool only reads data without making changes. + * + * @example + * ```ts + * import { Tool } from "@effect/ai" + * + * const readOnlyTool = Tool.make("get_user_info") + * .annotate(Tool.Readonly, true) + * ``` + * + * @since 1.0.0 + * @category Annotations + */ +export class Readonly extends Context.Reference()( + "@effect/ai/Tool/Readonly", + { + defaultValue: constFalse + } +) {} + +/** + * Annotation indicating whether a tool performs destructive operations. + * + * @example + * ```ts + * import { Tool } from "@effect/ai" + * + * const safeTool = Tool.make("search_database") + * .annotate(Tool.Destructive, false) + * ``` + * + * @since 1.0.0 + * @category Annotations + */ +export class Destructive extends Context.Reference()( + "@effect/ai/Tool/Destructive", + { + defaultValue: constTrue + } +) {} + +/** + * Annotation indicating whether a tool can be called multiple times safely. + * + * @example + * ```ts + * import { Tool } from "@effect/ai" + * + * const idempotentTool = Tool.make("get_current_time") + * .annotate(Tool.Idempotent, true) + * ``` + * + * @since 1.0.0 + * @category Annotations + */ +export class Idempotent extends Context.Reference()( + "@effect/ai/Tool/Idempotent", + { + defaultValue: constFalse + } +) {} + +/** + * Annotation indicating whether a tool can handle arbitrary external data. + * + * @example + * ```ts + * import { Tool } from "@effect/ai" + * + * const restrictedTool = Tool.make("internal_operation") + * .annotate(Tool.OpenWorld, false) + * ``` + * + * @since 1.0.0 + * @category Annotations + */ +export class OpenWorld extends Context.Reference()( + "@effect/ai/Tool/OpenWorld", + { + defaultValue: constTrue + } +) {} + +// Licensed under BSD-3-Clause (below code only) +// Code adapted from https://github.com/fastify/secure-json-parse/blob/783fcb1b5434709466759847cec974381939673a/index.js +// +// Copyright (c) Effectful Technologies, Inc (https://effectful.co) +// Copyright (c) 2019 The Fastify Team +// Copyright (c) 2019, Sideway Inc, and project contributors +// All rights reserved. +// +// The complete list of contributors can be found at: +// - https://github.com/hapijs/bourne/graphs/contributors +// - https://github.com/fastify/secure-json-parse/graphs/contributors +// - https://github.com/Effect-TS/effect/commits/main/packages/ai/ai/src/Tool.ts +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +const suspectProtoRx = /"__proto__"\s*:/ +const suspectConstructorRx = /"constructor"\s*:/ + +function _parse(text: string) { + // Parse normally + const obj = JSON.parse(text) + + // Ignore null and non-objects + if (obj === null || typeof obj !== "object") { + return obj + } + + if ( + suspectProtoRx.test(text) === false && + suspectConstructorRx.test(text) === false + ) { + return obj + } + + // Scan result for proto keys + return filter(obj) +} + +function filter(obj: any) { + let next = [obj] + + while (next.length) { + const nodes = next + next = [] + + for (const node of nodes) { + if (Object.prototype.hasOwnProperty.call(node, "__proto__")) { + throw new SyntaxError("Object contains forbidden prototype property") + } + + if ( + Object.prototype.hasOwnProperty.call(node, "constructor") && + Object.prototype.hasOwnProperty.call(node.constructor, "prototype") + ) { + throw new SyntaxError("Object contains forbidden prototype property") + } + + for (const key in node) { + const value = node[key] + if (value && typeof value === "object") { + next.push(value) + } + } + } + } + return obj +} + +/** + * **Unsafe**: This function will throw an error if an insecure property is + * found in the parsed JSON or if the provided JSON text is not parseable. + * + * @since 1.0.0 + * @category Utilities + */ +export const unsafeSecureJsonParse = (text: string): unknown => { + // Performance optimization, see https://github.com/fastify/secure-json-parse/pull/90 + const { stackTraceLimit } = Error + Error.stackTraceLimit = 0 + try { + return _parse(text) + } finally { + Error.stackTraceLimit = stackTraceLimit + } +} diff --git a/repos/effect/packages/ai/ai/src/Toolkit.ts b/repos/effect/packages/ai/ai/src/Toolkit.ts new file mode 100644 index 0000000..4abd57f --- /dev/null +++ b/repos/effect/packages/ai/ai/src/Toolkit.ts @@ -0,0 +1,528 @@ +/** + * The `Toolkit` module allows for creating and implementing a collection of + * `Tool`s which can be used to enhance the capabilities of a large language + * model beyond simple text generation. + * + * @example + * ```ts + * import { Toolkit, Tool } from "@effect/ai" + * import { Effect, Schema } from "effect" + * + * // Create individual tools + * const GetCurrentTime = Tool.make("GetCurrentTime", { + * description: "Get the current timestamp", + * success: Schema.Number + * }) + * + * const GetWeather = Tool.make("GetWeather", { + * description: "Get weather for a location", + * parameters: { location: Schema.String }, + * success: Schema.Struct({ + * temperature: Schema.Number, + * condition: Schema.String + * }) + * }) + * + * // Create a toolkit with multiple tools + * const MyToolkit = Toolkit.make(GetCurrentTime, GetWeather) + * + * const MyToolkitLayer = MyToolkit.toLayer({ + * GetCurrentTime: () => Effect.succeed(Date.now()), + * GetWeather: ({ location }) => Effect.succeed({ + * temperature: 72, + * condition: "sunny" + * }) + * }) + * ``` + * + * @since 1.0.0 + */ +import * as Context from "effect/Context" +import * as Effect from "effect/Effect" +import { CommitPrototype } from "effect/Effectable" +import { identity } from "effect/Function" +import type { Inspectable } from "effect/Inspectable" +import { BaseProto as InspectableProto } from "effect/Inspectable" +import * as Layer from "effect/Layer" +import type { ParseError } from "effect/ParseResult" +import * as ParseResult from "effect/ParseResult" +import { type Pipeable, pipeArguments } from "effect/Pipeable" +import * as Predicate from "effect/Predicate" +import * as Schema from "effect/Schema" +import type * as Scope from "effect/Scope" +import * as AiError from "./AiError.js" +import * as Tool from "./Tool.js" + +/** + * Unique identifier for toolkit instances. + * + * @since 1.0.0 + * @category Type Ids + */ +export const TypeId = "~@effect/ai/Toolkit" + +/** + * Type-level representation of the toolkit identifier. + * + * @since 1.0.0 + * @category Type Ids + */ +export type TypeId = typeof TypeId + +/** + * Represents a collection of tools which can be used to enhance the + * capabilities of a large language model. + * + * @example + * ```ts + * import { Toolkit, Tool } from "@effect/ai" + * import { Effect, Schema } from "effect" + * + * // Create individual tools + * const GetCurrentTime = Tool.make("GetCurrentTime", { + * description: "Get the current timestamp", + * success: Schema.Number + * }) + * + * const GetWeather = Tool.make("GetWeather", { + * description: "Get weather for a location", + * parameters: { location: Schema.String }, + * success: Schema.Struct({ + * temperature: Schema.Number, + * condition: Schema.String + * }) + * }) + * + * // Create a toolkit with multiple tools + * const MyToolkit = Toolkit.make(GetCurrentTime, GetWeather) + * + * const MyToolkitLayer = MyToolkit.toLayer({ + * GetCurrentTime: () => Effect.succeed(Date.now()), + * GetWeather: ({ location }) => Effect.succeed({ + * temperature: 72, + * condition: "sunny" + * }) + * }) + * ``` + * + * @since 1.0.0 + * @category Models + */ +export interface Toolkit> extends + Effect.Effect< + WithHandler, + never, + Tool.HandlersFor + >, + Inspectable, + Pipeable +{ + readonly [TypeId]: TypeId + + new(_: never): {} + + /** + * A record containing all tools in this toolkit. + */ + readonly tools: Tools + + /** + * A helper method which can be used for type-safe handler declarations. + */ + of>(handlers: Handlers): Handlers + + /** + * Converts a toolkit into an Effect Context containing handlers for each tool + * in the toolkit. + */ + toContext, EX = never, RX = never>( + build: Handlers | Effect.Effect + ): Effect.Effect>, EX, RX> + + /** + * Converts a toolkit into a Layer containing handlers for each tool in the + * toolkit. + */ + toLayer, EX = never, RX = never>( + /** + * Handler functions or Effect that produces handlers. + */ + build: Handlers | Effect.Effect + ): Layer.Layer, EX, Exclude> +} + +/** + * A utility type which structurally represents any toolkit instance. + * + * @since 1.0.0 + * @category Utility Types + */ +export interface Any { + readonly [TypeId]: TypeId + readonly tools: Record +} + +/** + * A utility type which can be used to extract the tool definitions from a + * toolkit. + * + * @since 1.0.0 + * @category Utility Types + */ +export type Tools = T extends Toolkit ? Tools : never + +/** + * A utility type which can transforms either a record or an array of tools into + * a record where keys are tool names and values are the tool instances. + * + * @since 1.0.0 + * @category Utility Types + */ +export type ToolsByName = Tools extends Record ? + { readonly [Name in keyof Tools]: Tools[Name] } + : Tools extends ReadonlyArray ? { readonly [Tool in Tools[number] as Tool["name"]]: Tool } + : never + +/** + * A utility type that maps tool names to their required handler functions. + * + * @since 1.0.0 + * @category Utility Types + */ +export type HandlersFrom> = { + readonly [Name in keyof Tools as Tool.RequiresHandler extends true ? Name : never]: ( + params: Tool.Parameters + ) => Effect.Effect< + Tool.Success, + Tool.Failure, + Tool.Requirements + > +} + +/** + * A utility type which can be used to extract the tools from a toolkit with handlers. + * + * @since 1.0.0 + * @category Utility Types + */ + +export type WithHandlerTools = T extends WithHandler ? Tools : never + +/** + * A toolkit instance with registered handlers ready for tool execution. + * + * @since 1.0.0 + * @category Models + */ +export interface WithHandler> { + /** + * The tools available in this toolkit instance. + */ + readonly tools: Tools + + /** + * Handler function for executing tool calls. + * + * Receives a tool name and parameters, validates the input, executes the + * corresponding handler, and returns both the typed result and encoded result. + */ + readonly handle: ( + /** + * The name of the tool to execute. + */ + name: Name, + /** + * Parameters to pass to the tool handler. + */ + params: Tool.Parameters + ) => Effect.Effect< + Tool.HandlerResult, + Tool.Failure, + Tool.Requirements + > +} + +const Proto = { + ...CommitPrototype, + ...InspectableProto, + of: identity, + toContext( + this: Toolkit>, + build: Record any> | Effect.Effect any>> + ) { + return Effect.gen(this, function*() { + const context = yield* Effect.context() + const handlers = Effect.isEffect(build) ? yield* build : build + const contextMap = new Map() + for (const [name, handler] of Object.entries(handlers)) { + const tool = this.tools[name]! + contextMap.set(tool.id, { handler, context }) + } + return Context.unsafeMake(contextMap) + }) + }, + toLayer( + this: Toolkit>, + build: Record any> | Effect.Effect any>> + ) { + return Layer.scopedContext(this.toContext(build)) + }, + commit(this: Toolkit>) { + return Effect.gen(this, function*() { + const tools = this.tools + const context = yield* Effect.context() + const schemasCache = new WeakMap + readonly handler: (params: any) => Effect.Effect + readonly decodeParameters: (u: unknown) => Effect.Effect, ParseError> + readonly validateResult: (u: unknown) => Effect.Effect + readonly encodeResult: (u: unknown) => Effect.Effect + }>() + const getSchemas = (tool: Tool.Any) => { + let schemas = schemasCache.get(tool) + if (Predicate.isUndefined(schemas)) { + const handler = context.unsafeMap.get(tool.id)! as Tool.Handler + const decodeParameters = Schema.decodeUnknown(tool.parametersSchema) as any + const resultSchema = Schema.Union(tool.successSchema, tool.failureSchema) + const validateResult = Schema.validate(resultSchema) as any + const encodeResult = Schema.encodeUnknown(resultSchema) as any + schemas = { + context: handler.context, + handler: handler.handler, + decodeParameters, + validateResult, + encodeResult + } + schemasCache.set(tool, schemas) + } + return schemas + } + const handle = Effect.fn("Toolkit.handle", { captureStackTrace: false })( + function*(name: string, params: unknown) { + yield* Effect.annotateCurrentSpan({ tool: name, parameters: params }) + const tool = tools[name] + if (Predicate.isUndefined(tool)) { + const toolNames = Object.keys(tools).join(",") + return yield* new AiError.MalformedOutput({ + module: "Toolkit", + method: `${name}.handle`, + description: `Failed to find tool with name '${name}' in toolkit - available tools: ${toolNames}` + }) + } + const schemas = getSchemas(tool) + const decodedParams = yield* Effect.mapError( + schemas.decodeParameters(params), + (cause) => + new AiError.MalformedOutput({ + module: "Toolkit", + method: `${name}.handle`, + description: `Failed to decode tool call parameters for tool '${name}' from:\n'${ + JSON.stringify(params, undefined, 2) + }'`, + cause + }) + ) + const { isFailure, result } = yield* schemas.handler(decodedParams).pipe( + Effect.map((result) => ({ result, isFailure: false })), + Effect.catchAll((error) => + // If the tool handler failed, check the tool's failure mode to + // determine how the result should be returned to the end user + tool.failureMode === "error" + ? Effect.fail(error) + : Effect.succeed({ result: error, isFailure: true }) + ), + Effect.tap(({ result }) => schemas.validateResult(result)), + Effect.mapInputContext((input) => Context.merge(schemas.context, input)), + Effect.mapError((cause) => + ParseResult.isParseError(cause) + ? new AiError.MalformedInput({ + module: "Toolkit", + method: `${name}.handle`, + description: `Failed to validate tool call result for tool '${name}'`, + cause + }) + : cause + ) + ) + const encodedResult = yield* Effect.mapError( + schemas.encodeResult(result), + (cause) => + new AiError.MalformedInput({ + module: "Toolkit", + method: `${name}.handle`, + description: `Failed to encode tool call result for tool '${name}'`, + cause + }) + ) + return { + isFailure, + result, + encodedResult + } satisfies Tool.HandlerResult + } + ) + return { + tools, + handle + } satisfies WithHandler> + }) + }, + toJSON(this: Toolkit): unknown { + return { + _id: "@effect/ai/Toolkit", + tools: Array.from(Object.values(this.tools)).map((tool) => (tool as Tool.Any).name) + } + }, + pipe() { + return pipeArguments(this, arguments) + } +} + +const makeProto = >(tools: Tools): Toolkit => + Object.assign(function() {}, Proto, { tools }) as any + +const resolveInput = >( + ...tools: Tools +): Record => { + const output = {} as Record + for (const tool of tools) { + const value = (Schema.isSchema(tool) ? Tool.fromTaggedRequest(tool as any) : tool) as any + output[tool.name] = value + } + return output +} + +/** + * An empty toolkit with no tools. + * + * Useful as a starting point for building toolkits or as a default value. Can + * be extended using the merge function to add tools. + * + * @since 1.0.0 + * @category Constructors + */ +export const empty: Toolkit<{}> = makeProto({}) + +/** + * Creates a new toolkit from the specified tools. + * + * This is the primary constructor for creating toolkits. It accepts multiple tools + * and organizes them into a toolkit that can be provided to AI language models. + * Tools can be either Tool instances or TaggedRequest schemas. + * + * @example + * ```ts + * import { Toolkit, Tool } from "@effect/ai" + * import { Schema } from "effect" + * + * const GetCurrentTime = Tool.make("GetCurrentTime", { + * description: "Get the current timestamp", + * success: Schema.Number + * }) + * + * const GetWeather = Tool.make("get_weather", { + * description: "Get weather information", + * parameters: { location: Schema.String }, + * success: Schema.Struct({ + * temperature: Schema.Number, + * condition: Schema.String + * }) + * }) + * + * const toolkit = Toolkit.make(GetCurrentTime, GetWeather) + * ``` + * + * @since 1.0.0 + * @category Constructors + */ +export const make = >( + ...tools: Tools +): Toolkit> => makeProto(resolveInput(...tools)) as any + +/** + * A utility type which simplifies a record type. + * + * @since 1.0.0 + * @category Utility Types + */ +export type SimplifyRecord = { [K in keyof T]: T[K] } & {} + +/** + * A utility type which merges two records of tools together. + * + * @since 1.0.0 + * @category Utility Types + */ +export type MergeRecords = { + readonly [K in Extract]: Extract< + U extends Record ? V : never, + Tool.Any + > +} + +/** + * A utility type which merges the tool calls of two toolkits into a single + * toolkit. + * + * @since 1.0.0 + * @category Utility Types + */ +export type MergedTools> = SimplifyRecord< + MergeRecords> +> + +/** + * Merges multiple toolkits into a single toolkit. + * + * Combines all tools from the provided toolkits into one unified toolkit. + * If there are naming conflicts, tools from later toolkits will override + * tools from earlier ones. + * + * @example + * ```ts + * import { Toolkit, Tool } from "@effect/ai" + * + * const mathToolkit = Toolkit.make( + * Tool.make("add"), + * Tool.make("subtract") + * ) + * + * const utilityToolkit = Toolkit.make( + * Tool.make("get_time"), + * Tool.make("get_weather") + * ) + * + * const combined = Toolkit.merge(mathToolkit, utilityToolkit) + * // combined now has: add, subtract, get_time, get_weather + * ``` + * + * @example + * ```ts + * import { Toolkit, Tool } from "@effect/ai" + * + * // Incremental toolkit building + * const baseToolkit = Toolkit.make(Tool.make("base_tool")) + * const extendedToolkit = Toolkit.merge( + * baseToolkit, + * Toolkit.make(Tool.make("additional_tool")), + * Toolkit.make(Tool.make("another_tool")) + * ) + * ``` + * + * @since 1.0.0 + * @category Constructors + */ +export const merge = >( + /** + * The toolkits to merge together. + */ + ...toolkits: Toolkits +): Toolkit> => { + const tools = {} as Record + for (const toolkit of toolkits) { + for (const [name, tool] of Object.entries(toolkit.tools)) { + tools[name] = tool + } + } + return makeProto(tools) as any +} diff --git a/repos/effect/packages/ai/ai/src/index.ts b/repos/effect/packages/ai/ai/src/index.ts new file mode 100644 index 0000000..2bfb8c1 --- /dev/null +++ b/repos/effect/packages/ai/ai/src/index.ts @@ -0,0 +1,550 @@ +/** + * The `AiError` module provides comprehensive error handling for AI operations. + * + * This module defines a hierarchy of error types that can occur when working + * with AI services, including HTTP request/response errors, input/output + * validation errors, and general runtime errors. All errors follow Effect's + * structured error patterns and provide detailed context for debugging. + * + * ## Error Types + * + * - **HttpRequestError**: Errors occurring during HTTP request processing + * - **HttpResponseError**: Errors occurring during HTTP response processing + * - **MalformedInput**: Errors when input data doesn't match expected format + * - **MalformedOutput**: Errors when output data can't be parsed or validated + * - **UnknownError**: Catch-all for unexpected runtime errors + * + * @example + * ```ts + * import { AiError } from "@effect/ai" + * import { Effect, Match } from "effect" + * + * const handleAiError = Match.type().pipe( + * Match.tag("HttpRequestError", (err) => + * Effect.logError(`Request failed: ${err.message}`) + * ), + * Match.tag("HttpResponseError", (err) => + * Effect.logError(`Response error (${err.response.status}): ${err.message}`) + * ), + * Match.tag("MalformedInput", (err) => + * Effect.logError(`Invalid input: ${err.message}`) + * ), + * Match.tag("MalformedOutput", (err) => + * Effect.logError(`Invalid output: ${err.message}`) + * ), + * Match.orElse((err) => + * Effect.logError(`Unknown error: ${err.message}`) + * ) + * ) + * ``` + * + * @example + * ```ts + * import { AiError } from "@effect/ai" + * import { Effect, Option } from "effect" + * + * const aiOperation = Effect.gen(function* () { + * // Some AI operation that might fail + * return yield* new AiError.HttpRequestError({ + * module: "OpenAI", + * method: "completion", + * reason: "Transport", + * request: { + * method: "POST", + * url: "https://api.openai.com/v1/completions", + * urlParams: [], + * hash: Option.none(), + * headers: { "Content-Type": "application/json" } + * } + * }) + * }) + * + * const program = aiOperation.pipe( + * Effect.catchTag("HttpRequestError", (error) => { + * console.log("Request failed:", error.message) + * return Effect.succeed("fallback response") + * }) + * ) + * ``` + * + * @since 1.0.0 + */ +export * as AiError from "./AiError.js" + +/** + * The `Chat` module provides a stateful conversation interface for AI language + * models. + * + * This module enables persistent chat sessions that maintain conversation + * history, support tool calling, and offer both streaming and non-streaming + * text generation. It integrates seamlessly with the Effect AI ecosystem, + * providing type-safe conversational AI capabilities. + * + * @example + * ```ts + * import { Chat, LanguageModel } from "@effect/ai" + * import { Effect, Layer } from "effect" + * + * // Create a new chat session + * const program = Effect.gen(function* () { + * const chat = yield* Chat.empty + * + * // Send a message and get response + * const response = yield* chat.generateText({ + * prompt: "Hello! What can you help me with?" + * }) + * + * console.log(response.content) + * + * return response + * }) + * ``` + * + * @example + * ```ts + * import { Chat, LanguageModel } from "@effect/ai" + * import { Effect, Stream } from "effect" + * + * // Streaming chat with tool support + * const streamingChat = Effect.gen(function* () { + * const chat = yield* Chat.empty + * + * yield* chat.streamText({ + * prompt: "Generate a creative story" + * }).pipe(Stream.runForEach((part) => + * Effect.sync(() => console.log(part)) + * )) + * }) + * ``` + * + * @since 1.0.0 + */ +export * as Chat from "./Chat.js" + +/** + * The `EmbeddingModel` module provides vector embeddings for text using AI + * models. + * + * This module enables efficient conversion of text into high-dimensional vector + * representations that capture semantic meaning. It supports batching, caching, + * and request optimization for production use cases like semantic search, + * document similarity, and clustering. + * + * @example + * ```ts + * import { EmbeddingModel } from "@effect/ai" + * import { Effect } from "effect" + * + * // Basic embedding usage + * const program = Effect.gen(function* () { + * const embedding = yield* EmbeddingModel.EmbeddingModel + * + * const vector = yield* embedding.embed("Hello world!") + * console.log(vector) // [0.123, -0.456, 0.789, ...] + * + * return vector + * }) + * ``` + * + * @example + * ```ts + * import { EmbeddingModel } from "@effect/ai" + * import { Effect, Duration } from "effect" + * + * declare const generateVectorFor: (text: string) => Array + * + * // Create embedding service with batching and caching + * const embeddingService = EmbeddingModel.make({ + * embedMany: (texts) => Effect.succeed( + * texts.map((text, index) => ({ + * index, + * embeddings: generateVectorFor(text) + * })) + * ), + * maxBatchSize: 50, + * cache: { + * capacity: 1000, + * timeToLive: Duration.minutes(30) + * } + * }) + * ``` + * + * @since 1.0.0 + */ +export * as EmbeddingModel from "./EmbeddingModel.js" + +/** + * The `IdGenerator` module provides a pluggable system for generating unique identifiers + * for tool calls and other items in the Effect AI SDKs. + * + * This module offers a flexible and configurable approach to ID generation, supporting + * custom alphabets, prefixes, separators, and sizes. + * + * @example + * ```ts + * import { IdGenerator } from "@effect/ai" + * import { Effect, Layer } from "effect" + * + * // Using the default ID generator + * const program = Effect.gen(function* () { + * const idGen = yield* IdGenerator.IdGenerator + * const toolCallId = yield* idGen.generateId() + * console.log(toolCallId) // "id_A7xK9mP2qR5tY8uV" + * return toolCallId + * }).pipe( + * Effect.provide(Layer.succeed( + * IdGenerator.IdGenerator, + * IdGenerator.defaultIdGenerator + * )) + * ) + * ``` + * + * @example + * ```ts + * import { IdGenerator } from "@effect/ai" + * import { Effect, Layer } from "effect" + * + * // Creating a custom ID generator for AI tool calls + * const customLayer = IdGenerator.layer({ + * alphabet: "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789", + * prefix: "tool_call", + * separator: "-", + * size: 12 + * }) + * + * const program = Effect.gen(function* () { + * const idGen = yield* IdGenerator.IdGenerator + * const id = yield* idGen.generateId() + * console.log(id) // "tool_call-A7XK9MP2QR5T" + * return id + * }).pipe( + * Effect.provide(customLayer) + * ) + * ``` + * + * @since 1.0.0 + */ +export * as IdGenerator from "./IdGenerator.js" + +/** + * The `LanguageModel` module provides AI text generation capabilities with tool + * calling support. + * + * This module offers a comprehensive interface for interacting with large + * language models, supporting both streaming and non-streaming text generation, + * structured output generation, and tool calling functionality. It provides a + * unified API that can be implemented by different AI providers while + * maintaining type safety and effect management. + * + * @example + * ```ts + * import { LanguageModel } from "@effect/ai" + * import { Effect } from "effect" + * + * // Basic text generation + * const program = Effect.gen(function* () { + * const response = yield* LanguageModel.generateText({ + * prompt: "Explain quantum computing" + * }) + * + * console.log(response.text) + * + * return response + * }) + * ``` + * + * @example + * ```ts + * import { LanguageModel } from "@effect/ai" + * import { Effect, Schema } from "effect" + * + * // Structured output generation + * const ContactSchema = Schema.Struct({ + * name: Schema.String, + * email: Schema.String + * }) + * + * const extractContact = Effect.gen(function* () { + * const response = yield* LanguageModel.generateObject({ + * prompt: "Extract contact: John Doe, john@example.com", + * schema: ContactSchema + * }) + * + * return response.value + * }) + * ``` + * + * @since 1.0.0 + */ +export * as LanguageModel from "./LanguageModel.js" + +/** + * @since 1.0.0 + */ +export * as McpSchema from "./McpSchema.js" + +/** + * @since 1.0.0 + */ +export * as McpServer from "./McpServer.js" + +/** + * The `Model` module provides a unified interface for AI service providers. + * + * This module enables creation of provider-specific AI models that can be used + * interchangeably within the Effect AI ecosystem. It combines Layer + * functionality with provider identification, allowing for seamless switching + * between different AI service providers while maintaining type safety. + * + * @example + * ```ts + * import { Model, LanguageModel } from "@effect/ai" + * import { Effect, Layer } from "effect" + * + * declare const myAnthropicLayer: Layer.Layer + * + * const anthropicModel = Model.make("anthropic", myAnthropicLayer) + * + * const program = Effect.gen(function* () { + * const response = yield* LanguageModel.generateText({ + * prompt: "Hello, world!" + * }) + * return response.text + * }).pipe( + * Effect.provide(anthropicModel) + * ) + * ``` + * + * @since 1.0.0 + */ +export * as Model from "./Model.js" + +/** + * The `Prompt` module provides several data structures to simplify creating and + * combining prompts. + * + * This module defines the complete structure of a conversation with a large + * language model, including messages, content parts, and provider-specific + * options. It supports rich content types like text, files, tool calls, and + * reasoning. + * + * @example + * ```ts + * import { Prompt } from "@effect/ai" + * + * // Create a structured conversation + * const conversation = Prompt.make([ + * { + * role: "system", + * content: "You are a helpful assistant specialized in mathematics." + * }, + * { + * role: "user", + * content: [{ + * type: "text", + * text: "What is the derivative of x²?" + * }] + * }, + * { + * role: "assistant", + * content: [{ + * type: "text", + * text: "The derivative of x² is 2x." + * }] + * } + * ]) + * ``` + * + * @example + * ```ts + * import { Prompt } from "@effect/ai" + * + * // Merge multiple prompts + * const systemPrompt = Prompt.make([{ + * role: "system", + * content: "You are a coding assistant." + * }]) + * + * const userPrompt = Prompt.make("Help me write a function") + * + * const combined = Prompt.merge(systemPrompt, userPrompt) + * ``` + * + * @since 1.0.0 + */ +export * as Prompt from "./Prompt.js" + +/** + * The `Response` module provides data structures to represent responses from + * large language models. + * + * This module defines the complete structure of AI model responses, including + * various content parts for text, reasoning, tool calls, files, and metadata, + * supporting both streaming and non-streaming responses. + * + * @example + * ```ts + * import { Response } from "@effect/ai" + * + * // Create a simple text response part + * const textResponse = Response.makePart("text", { + * text: "The weather is sunny today!" + * }) + * + * // Create a tool call response part + * const toolCallResponse = Response.makePart("tool-call", { + * id: "call_123", + * name: "get_weather", + * params: { city: "San Francisco" }, + * providerExecuted: false + * }) + * ``` + * + * @since 1.0.0 + */ +export * as Response from "./Response.js" + +/** + * The `Telemetry` module provides OpenTelemetry integration for operations + * performed against a large language model provider by defining telemetry + * attributes and utilities that follow the OpenTelemetry GenAI semantic + * conventions. + * + * @example + * ```ts + * import { Telemetry } from "@effect/ai" + * import { Effect } from "effect" + * + * // Add telemetry attributes to a span + * const addTelemetry = Effect.gen(function* () { + * const span = yield* Effect.currentSpan + * + * Telemetry.addGenAIAnnotations(span, { + * system: "openai", + * operation: { name: "chat" }, + * request: { + * model: "gpt-4", + * temperature: 0.7, + * maxTokens: 1000 + * }, + * usage: { + * inputTokens: 100, + * outputTokens: 50 + * } + * }) + * }) + * ``` + * + * @since 1.0.0 + */ +export * as Telemetry from "./Telemetry.js" + +/** + * The `Tokenizer` module provides tokenization and text truncation capabilities + * for large language model text processing workflows. + * + * This module offers services for converting text into tokens and truncating + * prompts based on token limits, essential for managing context length + * constraints in large language models. + * + * @example + * ```ts + * import { Tokenizer, Prompt } from "@effect/ai" + * import { Effect } from "effect" + * + * const tokenizeText = Effect.gen(function* () { + * const tokenizer = yield* Tokenizer.Tokenizer + * const tokens = yield* tokenizer.tokenize("Hello, world!") + * console.log(`Token count: ${tokens.length}`) + * return tokens + * }) + * ``` + * + * @example + * ```ts + * import { Tokenizer, Prompt } from "@effect/ai" + * import { Effect } from "effect" + * + * // Truncate a prompt to fit within token limits + * const truncatePrompt = Effect.gen(function* () { + * const tokenizer = yield* Tokenizer.Tokenizer + * const longPrompt = "This is a very long prompt..." + * const truncated = yield* tokenizer.truncate(longPrompt, 100) + * return truncated + * }) + * ``` + * + * @since 1.0.0 + */ +export * as Tokenizer from "./Tokenizer.js" + +/** + * The `Tool` module provides functionality for defining and managing tools + * that language models can call to augment their capabilities. + * + * This module enables creation of both user-defined and provider-defined tools, + * with full schema validation, type safety, and handler support. Tools allow + * AI models to perform actions like searching databases, calling APIs, or + * executing code within your application context. + * + * @example + * ```ts + * import { Tool } from "@effect/ai" + * import { Schema } from "effect" + * + * // Define a simple calculator tool + * const Calculator = Tool.make("Calculator", { + * description: "Performs basic arithmetic operations", + * parameters: { + * operation: Schema.Literal("add", "subtract", "multiply", "divide"), + * a: Schema.Number, + * b: Schema.Number + * }, + * success: Schema.Number + * }) + * ``` + * + * @since 1.0.0 + */ +export * as Tool from "./Tool.js" + +/** + * The `Toolkit` module allows for creating and implementing a collection of + * `Tool`s which can be used to enhance the capabilities of a large language + * model beyond simple text generation. + * + * @example + * ```ts + * import { Toolkit, Tool } from "@effect/ai" + * import { Effect, Schema } from "effect" + * + * // Create individual tools + * const GetCurrentTime = Tool.make("GetCurrentTime", { + * description: "Get the current timestamp", + * success: Schema.Number + * }) + * + * const GetWeather = Tool.make("GetWeather", { + * description: "Get weather for a location", + * parameters: { location: Schema.String }, + * success: Schema.Struct({ + * temperature: Schema.Number, + * condition: Schema.String + * }) + * }) + * + * // Create a toolkit with multiple tools + * const MyToolkit = Toolkit.make(GetCurrentTime, GetWeather) + * + * const MyToolkitLayer = MyToolkit.toLayer({ + * GetCurrentTime: () => Effect.succeed(Date.now()), + * GetWeather: ({ location }) => Effect.succeed({ + * temperature: 72, + * condition: "sunny" + * }) + * }) + * ``` + * + * @since 1.0.0 + */ +export * as Toolkit from "./Toolkit.js" diff --git a/repos/effect/packages/ai/ai/test/Chat.test.ts b/repos/effect/packages/ai/ai/test/Chat.test.ts new file mode 100644 index 0000000..fab8749 --- /dev/null +++ b/repos/effect/packages/ai/ai/test/Chat.test.ts @@ -0,0 +1,151 @@ +import * as Chat from "@effect/ai/Chat" +import * as IdGenerator from "@effect/ai/IdGenerator" +import * as Prompt from "@effect/ai/Prompt" +import * as Persistence from "@effect/experimental/Persistence" +import { assert, describe, it } from "@effect/vitest" +import * as Effect from "effect/Effect" +import * as Layer from "effect/Layer" +import * as Option from "effect/Option" +import * as Ref from "effect/Ref" +import * as Schema from "effect/Schema" +import * as TestClock from "effect/TestClock" +import * as TestUtils from "./utilities.js" + +const withConstantIdGenerator = (id: string) => + Effect.provideService(IdGenerator.IdGenerator, { + generateId: () => Effect.succeed(id) + }) + +const PersistenceLayer = Layer.provideMerge( + Chat.layerPersisted({ storeId: "chat" }), + Persistence.layerMemory +) + +describe("Chat", () => { + it.scoped("should persist chat history to the backing persistence store", () => + Effect.gen(function*() { + const storeId = "chat" + const chatId = "1" + + const backing = yield* Persistence.BackingPersistence + const persistence = yield* Chat.Persistence + + const store = yield* backing.make(storeId) + const chat = yield* persistence.getOrCreate(chatId) + + yield* chat.generateText({ prompt: "test user message" }).pipe( + TestUtils.withLanguageModel({ + generateText: [{ + type: "text", + text: "test assistant message" + }] + }) + ) + + const chatHistory = yield* chat.history + const storeHistory = yield* store.get(chatId).pipe( + Effect.flatten, + Effect.flatMap(Schema.decodeUnknown(Prompt.FromJson)) + ) + const options = { [Chat.Persistence.key]: { messageId: "msg_abc123" } } + const expectedHistory = Prompt.make([ + { role: "user", content: [{ type: "text", text: "test user message" }], options }, + { role: "assistant", content: [{ type: "text", text: "test assistant message" }], options } + ]) + + assert.deepStrictEqual(chatHistory, expectedHistory) + assert.deepStrictEqual(chatHistory, storeHistory) + }).pipe(withConstantIdGenerator("msg_abc123"), Effect.provide(PersistenceLayer))) + + it.scoped("should respect the specified time to live", () => + Effect.gen(function*() { + const storeId = "chat" + const chatId = "1" + + const backing = yield* Persistence.BackingPersistence + const persistence = yield* Chat.Persistence + + const store = yield* backing.make(storeId) + const chat = yield* persistence.getOrCreate(chatId, { + timeToLive: "30 days" + }) + + yield* chat.generateText({ prompt: "test user message" }).pipe( + TestUtils.withLanguageModel({ + generateText: [{ + type: "text", + text: "test assistant message" + }] + }) + ) + + const chatHistory = yield* store.get(chatId).pipe( + Effect.flatten, + Effect.flatMap(Schema.decodeUnknown(Prompt.FromJson)) + ) + const options = { [Chat.Persistence.key]: { messageId: "msg_abc123" } } + const expectedHistory = Prompt.make([ + { role: "user", content: [{ type: "text", text: "test user message" }], options }, + { role: "assistant", content: [{ type: "text", text: "test assistant message" }], options } + ]) + + assert.deepStrictEqual(chatHistory, expectedHistory) + + // Simulate chat expiration + yield* TestClock.adjust("30 days") + + const afterExpiration = yield* store.get(chatId) + + assert.deepStrictEqual(afterExpiration, Option.none()) + }).pipe(withConstantIdGenerator("msg_abc123"), Effect.provide(PersistenceLayer))) + + it.scoped("should prefer the message identifier of the most recent assistant message", () => + Effect.gen(function*() { + const storeId = "chat" + const chatId = "2" + + const backing = yield* Persistence.BackingPersistence + const persistence = yield* Chat.Persistence + + const store = yield* backing.make(storeId) + const chat = yield* persistence.getOrCreate(chatId) + + const options = { [Chat.Persistence.key]: { messageId: "msg_123abc" } } + const history = Prompt.make([ + { role: "user", content: "first user message", options }, + { role: "assistant", content: "first assistant message", options } + ]) + yield* Ref.set(chat.history, history) + yield* chat.save + + yield* chat.generateText({ prompt: "second user message" }).pipe( + TestUtils.withLanguageModel({ + generateText: [{ + type: "text", + text: "second assistant message" + }] + }) + ) + + const storeHistory = yield* store.get(chatId).pipe( + Effect.flatten, + Effect.flatMap(Schema.decodeUnknown(Prompt.FromJson)) + ) + const expectedHistory = Prompt.merge(history, [ + { role: "user", content: "second user message", options }, + { role: "assistant", content: "second assistant message", options } + ]) + + assert.deepStrictEqual(storeHistory, expectedHistory) + }).pipe(withConstantIdGenerator("msg_abc123"), Effect.provide(PersistenceLayer))) + + it.scoped("should raise an error when retrieving a chat that does not exist", () => + Effect.gen(function*() { + const persistence = yield* Chat.Persistence + + const result = yield* Effect.flip(persistence.get("chat-321")) + + assert.instanceOf(result, Chat.ChatNotFoundError) + assert.strictEqual(result.chatId, "chat-321") + }).pipe(Effect.provide(PersistenceLayer))) +}) diff --git a/repos/effect/packages/ai/ai/test/LanguageModel.test.ts b/repos/effect/packages/ai/ai/test/LanguageModel.test.ts new file mode 100644 index 0000000..fc72e21 --- /dev/null +++ b/repos/effect/packages/ai/ai/test/LanguageModel.test.ts @@ -0,0 +1,86 @@ +import * as LanguageModel from "@effect/ai/LanguageModel" +import * as Response from "@effect/ai/Response" +import * as Tool from "@effect/ai/Tool" +import * as Toolkit from "@effect/ai/Toolkit" +import { assert, describe, it } from "@effect/vitest" +import * as Effect from "effect/Effect" +import * as Schema from "effect/Schema" +import * as Stream from "effect/Stream" +import * as TestClock from "effect/TestClock" +import * as TestUtils from "./utilities.js" + +const MyTool = Tool.make("MyTool", { + parameters: { testParam: Schema.String }, + success: Schema.Struct({ testSuccess: Schema.String }) +}) + +const MyToolkit = Toolkit.make(MyTool) + +const MyToolkitLayer = MyToolkit.toLayer({ + MyTool: () => + Effect.succeed({ testSuccess: "test-success" }).pipe( + Effect.delay("10 seconds") + ) +}) + +describe("LanguageModel", () => { + describe("streamText", () => { + it.effect("should emit tool calls before executing tool handlers", () => + Effect.gen(function*() { + const parts: Array>> = [] + const latch = yield* Effect.makeLatch() + + const toolCallId = "tool-abc123" + const toolName = "MyTool" + const toolParams = { testParam: "test-param" } + const toolResult = { testSuccess: "test-success" } + + yield* LanguageModel.streamText({ + prompt: [], + toolkit: MyToolkit + }).pipe( + Stream.runForEach((part) => + Effect.andThen(latch.open, () => { + parts.push(part) + }) + ), + TestUtils.withLanguageModel({ + streamText: [ + { + type: "tool-call", + id: toolCallId, + name: toolName, + params: toolParams + } + ] + }), + Effect.provide(MyToolkitLayer), + Effect.fork + ) + + yield* latch.await + + const toolCallPart = Response.makePart("tool-call", { + id: toolCallId, + name: toolName, + params: toolParams, + providerExecuted: false + }) + + const toolResultPart = Response.toolResultPart({ + id: toolCallId, + name: toolName, + result: toolResult, + encodedResult: toolResult, + isFailure: false, + providerExecuted: false + }) + + assert.deepStrictEqual(parts, [toolCallPart]) + + yield* TestClock.adjust("10 seconds") + + assert.deepStrictEqual(parts, [toolCallPart, toolResultPart]) + })) + }) +}) diff --git a/repos/effect/packages/ai/ai/test/Prompt.test.ts b/repos/effect/packages/ai/ai/test/Prompt.test.ts new file mode 100644 index 0000000..eb41323 --- /dev/null +++ b/repos/effect/packages/ai/ai/test/Prompt.test.ts @@ -0,0 +1,199 @@ +import * as Prompt from "@effect/ai/Prompt" +import * as Response from "@effect/ai/Response" +import { assert, describe, it } from "@effect/vitest" + +describe("Prompt", () => { + describe("fromResponseParts", () => { + it("should handle interspersed text and response deltas", () => { + const parts = [ + Response.makePart("text-start", { id: "1" }), + Response.makePart("text-delta", { id: "1", delta: "Hello" }), + Response.makePart("text-delta", { id: "1", delta: ", " }), + Response.makePart("text-delta", { id: "1", delta: "World!" }), + Response.makePart("text-end", { id: "1" }), + Response.makePart("reasoning-start", { id: "2" }), + Response.makePart("reasoning-delta", { id: "2", delta: "I " }), + Response.makePart("reasoning-delta", { id: "2", delta: "am " }), + Response.makePart("reasoning-delta", { id: "2", delta: "thinking" }), + Response.makePart("reasoning-end", { id: "2" }) + ] + const prompt = Prompt.fromResponseParts(parts) + const expected = Prompt.make([ + { + role: "assistant", + content: [ + { type: "text", text: "Hello, World!" }, + { type: "reasoning", text: "I am thinking" } + ] + } + ]) + assert.deepStrictEqual(prompt, expected) + }) + }) + + describe("merge", () => { + it("should sequentially combine the content of two Prompts", () => { + const leftMessages = [ + Prompt.makeMessage("user", { + content: [Prompt.makePart("text", { text: "a" })] + }), + Prompt.makeMessage("assistant", { + content: [Prompt.makePart("text", { text: "b" })] + }) + ] + const rightMessages = [ + Prompt.makeMessage("user", { + content: [Prompt.makePart("text", { text: "c" })] + }), + Prompt.makeMessage("assistant", { + content: [Prompt.makePart("text", { text: "d" })] + }) + ] + const left = Prompt.fromMessages(leftMessages) + const right = Prompt.fromMessages(rightMessages) + const merged = Prompt.merge(left, right) + assert.deepStrictEqual( + merged, + Prompt.fromMessages([ + ...leftMessages, + ...rightMessages + ]) + ) + }) + + it("should return an empty prompt if there are no messages", () => { + const merged = Prompt.merge(Prompt.empty, []) + assert.deepStrictEqual(merged, Prompt.empty) + }) + + it("should handle an empty prompt", () => { + const prompt = Prompt.empty + const merged = Prompt.merge(prompt, [ + { role: "user", content: "a" }, + { role: "assistant", content: "b" } + ]) + assert.deepStrictEqual( + merged, + Prompt.make([ + Prompt.makeMessage("user", { + content: [Prompt.makePart("text", { text: "a" })] + }), + Prompt.makeMessage("assistant", { + content: [Prompt.makePart("text", { text: "b" })] + }) + ]) + ) + }) + + it("should handle empty prompt input", () => { + const messages = [ + Prompt.makeMessage("user", { + content: [Prompt.makePart("text", { text: "a" })] + }), + Prompt.makeMessage("assistant", { + content: [Prompt.makePart("text", { text: "b" })] + }) + ] + const prompt = Prompt.fromMessages(messages) + const merged = Prompt.merge(prompt, []) + assert.deepStrictEqual(merged, prompt) + }) + }) + + describe("appendSystem", () => { + it("should append text to existing system message", () => { + const prompt = Prompt.make([ + { role: "system", content: "You are an expert in programming." }, + { role: "user", content: "Hello, world!" } + ]) + + const result = Prompt.appendSystem(prompt, " You are a helpful assistant.") + + assert.deepStrictEqual( + result.content[0], + Prompt.makeMessage("system", { + content: "You are an expert in programming. You are a helpful assistant." + }) + ) + }) + + it("should create a new system message if none exists", () => { + const prompt = Prompt.make([ + { role: "user", content: "Hello, world!" } + ]) + + const result = Prompt.appendSystem(prompt, "You are a helpful assistant.") + + assert.deepStrictEqual( + result.content[0], + Prompt.makeMessage("system", { + content: "You are a helpful assistant." + }) + ) + }) + + it("should work with empty prompt", () => { + const prompt = Prompt.empty + + const result = Prompt.appendSystem(prompt, "You are a helpful assistant.") + + assert.deepStrictEqual( + result.content[0], + Prompt.makeMessage("system", { content: "You are a helpful assistant." }) + ) + }) + }) + + describe("prependSystem", () => { + it("should prepend text to existing system message", () => { + const prompt = Prompt.make([ + { + role: "system", + content: "You are an expert in programming." + }, + { + role: "user", + content: "Hello, world!" + } + ]) + + const result = Prompt.prependSystem(prompt, "You are a helpful assistant. ") + + assert.deepStrictEqual( + result.content[0], + Prompt.makeMessage("system", { + content: "You are a helpful assistant. You are an expert in programming." + }) + ) + }) + + it("should create a new system message if none exists", () => { + const prompt = Prompt.make([ + { + role: "user", + content: "Hello, world!" + } + ]) + + const result = Prompt.prependSystem(prompt, "You are a helpful assistant.") + + assert.deepStrictEqual( + result.content[0], + Prompt.makeMessage("system", { + content: "You are a helpful assistant." + }) + ) + }) + + it("should work with empty prompt", () => { + const prompt = Prompt.empty + + const result = Prompt.prependSystem(prompt, "You are a helpful assistant.") + + assert.deepStrictEqual( + result.content[0], + Prompt.makeMessage("system", { content: "You are a helpful assistant." }) + ) + }) + }) +}) diff --git a/repos/effect/packages/ai/ai/test/Tool.test.ts b/repos/effect/packages/ai/ai/test/Tool.test.ts new file mode 100644 index 0000000..1693a2a --- /dev/null +++ b/repos/effect/packages/ai/ai/test/Tool.test.ts @@ -0,0 +1,502 @@ +import * as LanguageModel from "@effect/ai/LanguageModel" +import * as Response from "@effect/ai/Response" +import * as Tool from "@effect/ai/Tool" +import * as Toolkit from "@effect/ai/Toolkit" +import { assert, describe, it } from "@effect/vitest" +import * as Effect from "effect/Effect" +import * as Schema from "effect/Schema" +import * as TestUtils from "./utilities.js" + +const FailureModeError = Tool.make("FailureModeError", { + description: "A test tool", + parameters: { + testParam: Schema.String + }, + success: Schema.Struct({ + testSuccess: Schema.String + }), + failure: Schema.Struct({ + testFailure: Schema.String + }) +}) + +const FailureModeReturn = Tool.make("FailureModeReturn", { + description: "A test tool", + failureMode: "return", + parameters: { + testParam: Schema.String + }, + success: Schema.Struct({ + testSuccess: Schema.String + }), + failure: Schema.Struct({ + testFailure: Schema.String + }) +}) + +const NoHandlerRequired = Tool.providerDefined({ + id: "provider.no-handler-required", + toolkitName: "NoHandlerRequired", + providerName: "no_handler_required", + args: { + testArg: Schema.String + }, + parameters: { + testParam: Schema.String + }, + success: Schema.Struct({ + testSuccess: Schema.String + }), + failure: Schema.Struct({ + testFailure: Schema.String + }) +}) + +const HandlerRequired = Tool.providerDefined({ + id: "provider.handler-required", + toolkitName: "HandlerRequired", + providerName: "handler_required", + requiresHandler: true, + args: { + testArg: Schema.String + }, + parameters: { + testParam: Schema.String + }, + success: Schema.Struct({ + testSuccess: Schema.String + }), + failure: Schema.Struct({ + testFailure: Schema.String + }) +}) + +describe("Tool", () => { + describe("User Defined", () => { + it.effect("should return tool call handler success as a Right", () => + Effect.gen(function*() { + const toolkit = Toolkit.make(FailureModeReturn) + + const toolResult = { testSuccess: "failure-mode-return-tool" } + const handlers = toolkit.toLayer({ + FailureModeReturn: () => Effect.succeed(toolResult) + }) + + const toolCallId = "tool-123" + const toolName = "FailureModeReturn" + + const response = yield* LanguageModel.generateText({ + prompt: "Test", + toolkit + }).pipe( + TestUtils.withLanguageModel({ + generateText: [{ + type: "tool-call", + id: toolCallId, + name: toolName, + params: { testParam: "test-param" } + }] + }), + Effect.provide(handlers) + ) + + const expected = Response.makePart("tool-result", { + id: toolCallId, + isFailure: false, + name: toolName, + result: toolResult, + encodedResult: toolResult, + providerExecuted: false + }) + + assert.deepInclude(response.toolResults, expected) + })) + + it.effect("should return tool call handler failure as a Left", () => + Effect.gen(function*() { + const toolkit = Toolkit.make(FailureModeReturn) + + const toolResult = { testFailure: "failure-mode-return-tool" } + const handlers = toolkit.toLayer({ + FailureModeReturn: () => Effect.fail(toolResult) + }) + + const toolCallId = "tool-123" + const toolName = "FailureModeReturn" + + const response = yield* LanguageModel.generateText({ + prompt: "Test", + toolkit + }).pipe( + TestUtils.withLanguageModel({ + generateText: [{ + type: "tool-call", + id: toolCallId, + name: toolName, + params: { testParam: "test-param" } + }] + }), + Effect.provide(handlers) + ) + + const expected = Response.makePart("tool-result", { + id: toolCallId, + name: toolName, + isFailure: true, + result: toolResult, + encodedResult: toolResult, + providerExecuted: false + }) + + assert.deepInclude(response.toolResults, expected) + })) + + it.effect("should raise an error on tool call handler failure", () => + Effect.gen(function*() { + const toolkit = Toolkit.make(FailureModeError) + + const toolResult = { testFailure: "failure-mode-error-tool" } + const handlers = toolkit.toLayer({ + FailureModeError: () => Effect.fail(toolResult) + }) + + const toolCallId = "tool-123" + const toolName = "FailureModeError" + + const response = yield* LanguageModel.generateText({ + prompt: "Test", + toolkit + }).pipe( + TestUtils.withLanguageModel({ + generateText: [{ + type: "tool-call", + id: toolCallId, + name: toolName, + params: { testParam: "test-param" } + }] + }), + Effect.provide(handlers), + Effect.flip + ) + + assert.deepStrictEqual(response, toolResult) + })) + + it.effect("should raise an error on invalid tool call parameters", () => + Effect.gen(function*() { + const toolkit = Toolkit.make(FailureModeReturn) + + const toolResult = { testSuccess: "failure-mode-return-tool" } + const handlers = toolkit.toLayer({ + FailureModeReturn: () => Effect.succeed(toolResult) + }) + + const toolCallId = "tool-123" + const toolName = "FailureModeReturn" + + const response = yield* LanguageModel.generateText({ + prompt: "Test", + toolkit + }).pipe( + TestUtils.withLanguageModel({ + generateText: [{ + type: "tool-call", + id: toolCallId, + name: toolName, + params: {} + }] + }), + Effect.provide(handlers), + Effect.flip + ) + + assert.strictEqual(response._tag, "MalformedOutput") + assert.strictEqual( + response.description, + "Failed to decode tool call parameters for tool 'FailureModeReturn' from:\n'{}'" + ) + })) + }) + + describe("Provider Defined", () => { + it.effect("should return tool call successes from the model as a Right", () => + Effect.gen(function*() { + const tool = NoHandlerRequired({ + testArg: "test-arg" + }) + const toolkit = Toolkit.make(tool) + + const toolCallId = "tool-123" + const toolResult = { testSuccess: "provider-defined-tool" } + + const response = yield* LanguageModel.generateText({ + prompt: "Test", + toolkit + }).pipe( + TestUtils.withLanguageModel({ + generateText: [ + { + type: "tool-call", + id: toolCallId, + name: tool.name, + providerName: tool.providerName, + providerExecuted: true, + params: { testParam: "test-param" } + }, + { + type: "tool-result", + id: toolCallId, + name: tool.name, + isFailure: false, + result: toolResult, + providerName: tool.providerName, + providerExecuted: true + } + ] + }) + ) + + const expected = Response.makePart("tool-result", { + id: toolCallId, + name: tool.name, + isFailure: false, + result: toolResult, + encodedResult: toolResult, + providerName: tool.providerName, + providerExecuted: true + }) + + assert.deepInclude(response.toolResults, expected) + })) + + it.effect("should return tool call errors from the model as a Left", () => + Effect.gen(function*() { + const tool = NoHandlerRequired({ + testArg: "test-arg" + }) + const toolkit = Toolkit.make(tool) + + const toolCallId = "tool-123" + const toolResult = { testFailure: "provider-defined-tool" } + + const response = yield* LanguageModel.generateText({ + prompt: "Test", + toolkit + }).pipe( + TestUtils.withLanguageModel({ + generateText: [ + { + type: "tool-call", + id: toolCallId, + name: tool.name, + providerName: tool.providerName, + providerExecuted: true, + params: { testParam: "test-param" } + }, + { + type: "tool-result", + id: toolCallId, + isFailure: true, + name: tool.name, + result: toolResult, + providerName: tool.providerName, + providerExecuted: true + } + ] + }) + ) + + const expected = Response.makePart("tool-result", { + id: toolCallId, + name: tool.name, + isFailure: true, + result: toolResult, + encodedResult: toolResult, + providerName: tool.providerName, + providerExecuted: true + }) + + assert.deepInclude(response.toolResults, expected) + })) + + it.effect("should return tool call handler success as a Right", () => + Effect.gen(function*() { + const tool = HandlerRequired({ + testArg: "test-arg" + }) + + const toolCallId = "tool-123" + const toolResult = { testSuccess: "provider-defined-tool" } + + const toolkit = Toolkit.make(tool) + const handlers = toolkit.toLayer({ + HandlerRequired: () => Effect.succeed(toolResult) + }) + + const response = yield* LanguageModel.generateText({ + prompt: "Test", + toolkit + }).pipe( + TestUtils.withLanguageModel({ + generateText: [ + { + type: "tool-call", + id: toolCallId, + name: tool.name, + providerName: tool.providerName, + // Given this provider-defined tool requires a user-space + // handler, it is not considered `providerExecuted` + providerExecuted: false, + params: { testParam: "test-param" } + } + ] + }), + Effect.provide(handlers) + ) + + const expected = Response.makePart("tool-result", { + id: toolCallId, + name: tool.name, + isFailure: false, + result: toolResult, + encodedResult: toolResult, + providerName: tool.providerName, + providerExecuted: false + }) + + assert.deepInclude(response.toolResults, expected) + })) + + it.effect("should return tool call handler failure as a Left", () => + Effect.gen(function*() { + const tool = HandlerRequired({ + failureMode: "return", + testArg: "test-arg" + }) + + const toolCallId = "tool-123" + const toolResult = { testFailure: "provider-defined-tool" } + + const toolkit = Toolkit.make(tool) + const handlers = toolkit.toLayer({ + HandlerRequired: () => Effect.fail(toolResult) + }) + + const response = yield* LanguageModel.generateText({ + prompt: "Test", + toolkit + }).pipe( + TestUtils.withLanguageModel({ + generateText: [ + { + type: "tool-call", + id: toolCallId, + name: tool.name, + providerName: tool.providerName, + // Given this provider-defined tool requires a user-space + // handler, it is not considered `providerExecuted` + providerExecuted: false, + params: { testParam: "test-param" } + } + ] + }), + Effect.provide(handlers) + ) + + const expected = Response.makePart("tool-result", { + id: toolCallId, + name: tool.name, + isFailure: true, + result: toolResult, + encodedResult: toolResult, + providerName: tool.providerName, + providerExecuted: false + }) + + assert.deepInclude(response.toolResults, expected) + })) + + it.effect("should raise an error on tool call handler failure", () => + Effect.gen(function*() { + const tool = HandlerRequired({ + testArg: "test-arg" + }) + + const toolCallId = "tool-123" + const toolResult = { testFailure: "provider-defined-tool" } + + const toolkit = Toolkit.make(tool) + const handlers = toolkit.toLayer({ + HandlerRequired: () => Effect.fail(toolResult) + }) + + const response = yield* LanguageModel.generateText({ + prompt: "Test", + toolkit + }).pipe( + TestUtils.withLanguageModel({ + generateText: [ + { + type: "tool-call", + id: toolCallId, + name: tool.name, + providerName: tool.providerName, + // Given this provider-defined tool requires a user-space + // handler, it is not considered `providerExecuted` + providerExecuted: false, + params: { testParam: "test-param" } + } + ] + }), + Effect.provide(handlers), + Effect.flip + ) + + assert.deepStrictEqual(response, toolResult) + })) + + it.effect("should raise an error on invalid tool call parameters", () => + Effect.gen(function*() { + const tool = HandlerRequired({ + failureMode: "return", + testArg: "test-arg" + }) + + const toolCallId = "tool-123" + const toolResult = { testSuccess: "provider-defined-tool" } + + const toolkit = Toolkit.make(tool) + const handlers = toolkit.toLayer({ + HandlerRequired: () => Effect.succeed(toolResult) + }) + + const response = yield* LanguageModel.generateText({ + prompt: "Test", + toolkit + }).pipe( + TestUtils.withLanguageModel({ + generateText: [ + { + type: "tool-call", + id: toolCallId, + name: tool.name, + providerName: tool.providerName, + // Given this provider-defined tool requires a user-space + // handler, it is not considered `providerExecuted` + providerExecuted: false, + params: {} + } + ] + }), + Effect.provide(handlers), + Effect.flip + ) + + assert.strictEqual(response._tag, "MalformedOutput") + assert.strictEqual( + response.description, + "Failed to decode tool call parameters for tool 'HandlerRequired' from:\n'{}'" + ) + })) + }) +}) diff --git a/repos/effect/packages/ai/ai/test/utilities.ts b/repos/effect/packages/ai/ai/test/utilities.ts new file mode 100644 index 0000000..6206906 --- /dev/null +++ b/repos/effect/packages/ai/ai/test/utilities.ts @@ -0,0 +1,70 @@ +import * as LanguageModel from "@effect/ai/LanguageModel" +import type * as Response from "@effect/ai/Response" +import * as Effect from "effect/Effect" +import { dual } from "effect/Function" +import * as Predicate from "effect/Predicate" +import * as Stream from "effect/Stream" + +export const withLanguageModel: { + (options: { + readonly generateText?: + | Array + | ((options: LanguageModel.ProviderOptions) => + | Array + | Effect.Effect>) + readonly streamText?: + | Array + | ((options: LanguageModel.ProviderOptions) => + | Array + | Stream.Stream) + }): (effect: Effect.Effect) => Effect.Effect> + (effect: Effect.Effect, options: { + readonly generateText?: + | Array + | ((options: LanguageModel.ProviderOptions) => + | Array + | Effect.Effect>) + readonly streamText?: + | Array + | ((options: LanguageModel.ProviderOptions) => + | Array + | Stream.Stream) + }): Effect.Effect> +} = dual(2, (effect: Effect.Effect, options: { + readonly generateText?: + | Array + | ((options: LanguageModel.ProviderOptions) => + | Array + | Effect.Effect>) + readonly streamText?: + | Array + | ((options: LanguageModel.ProviderOptions) => + | Array + | Stream.Stream) +}): Effect.Effect> => + Effect.provideServiceEffect( + effect, + LanguageModel.LanguageModel, + LanguageModel.make({ + generateText: (opts) => { + if (Predicate.isUndefined(options.generateText)) { + return Effect.succeed([]) + } + if (Array.isArray(options.generateText)) { + return Effect.succeed(options.generateText) + } + const result = options.generateText(opts) + return Effect.isEffect(result) ? result : Effect.succeed(result) + }, + streamText: (opts) => { + if (Predicate.isUndefined(options.streamText)) { + return Stream.empty + } + if (Array.isArray(options.streamText)) { + return Stream.fromIterable(options.streamText) + } + const result = options.streamText(opts) + return Array.isArray(result) ? Stream.fromIterable(result) : result + } + }) + )) diff --git a/repos/effect/packages/ai/ai/tsconfig.build.json b/repos/effect/packages/ai/ai/tsconfig.build.json new file mode 100644 index 0000000..14647ac --- /dev/null +++ b/repos/effect/packages/ai/ai/tsconfig.build.json @@ -0,0 +1,16 @@ +{ + "extends": "./tsconfig.src.json", + "references": [ + { "path": "../../effect/tsconfig.build.json" }, + { "path": "../../experimental/tsconfig.build.json" }, + { "path": "../../platform/tsconfig.build.json" }, + { "path": "../../rpc/tsconfig.build.json" } + ], + "compilerOptions": { + "tsBuildInfoFile": ".tsbuildinfo/build.tsbuildinfo", + "outDir": "build/esm", + "declarationDir": "build/dts", + "stripInternal": true, + "exactOptionalPropertyTypes": false + } +} diff --git a/repos/effect/packages/ai/ai/tsconfig.json b/repos/effect/packages/ai/ai/tsconfig.json new file mode 100644 index 0000000..f446496 --- /dev/null +++ b/repos/effect/packages/ai/ai/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../../tsconfig.base.json", + "include": [], + "references": [ + { "path": "tsconfig.src.json" }, + { "path": "tsconfig.test.json" } + ] +} diff --git a/repos/effect/packages/ai/ai/tsconfig.src.json b/repos/effect/packages/ai/ai/tsconfig.src.json new file mode 100644 index 0000000..1595593 --- /dev/null +++ b/repos/effect/packages/ai/ai/tsconfig.src.json @@ -0,0 +1,17 @@ +{ + "extends": "../../../tsconfig.base.json", + "include": ["src"], + "references": [ + { "path": "../../effect/tsconfig.src.json" }, + { "path": "../../experimental/tsconfig.src.json" }, + { "path": "../../platform/tsconfig.src.json" }, + { "path": "../../rpc/tsconfig.src.json" } + ], + "compilerOptions": { + "tsBuildInfoFile": ".tsbuildinfo/src.tsbuildinfo", + "rootDir": "src", + "outDir": "build/src", + "exactOptionalPropertyTypes": false, + "types": ["node"] + } +} diff --git a/repos/effect/packages/ai/ai/tsconfig.test.json b/repos/effect/packages/ai/ai/tsconfig.test.json new file mode 100644 index 0000000..95f3ae8 --- /dev/null +++ b/repos/effect/packages/ai/ai/tsconfig.test.json @@ -0,0 +1,14 @@ +{ + "extends": "../../../tsconfig.base.json", + "include": ["test"], + "references": [ + { "path": "tsconfig.src.json" }, + { "path": "../../vitest/tsconfig.src.json" } + ], + "compilerOptions": { + "tsBuildInfoFile": ".tsbuildinfo/test.tsbuildinfo", + "rootDir": "test", + "noEmit": true, + "exactOptionalPropertyTypes": false + } +} diff --git a/repos/effect/packages/ai/ai/vitest.config.ts b/repos/effect/packages/ai/ai/vitest.config.ts new file mode 100644 index 0000000..bf29895 --- /dev/null +++ b/repos/effect/packages/ai/ai/vitest.config.ts @@ -0,0 +1,6 @@ +import { mergeConfig, type ViteUserConfig } from "vitest/config" +import shared from "../../../vitest.shared.js" + +const config: ViteUserConfig = {} + +export default mergeConfig(shared, config) diff --git a/repos/effect/packages/ai/amazon-bedrock/CHANGELOG.md b/repos/effect/packages/ai/amazon-bedrock/CHANGELOG.md new file mode 100644 index 0000000..06ed052 --- /dev/null +++ b/repos/effect/packages/ai/amazon-bedrock/CHANGELOG.md @@ -0,0 +1,679 @@ +# @effect/ai-amazon-bedrock + +## 0.15.0 + +### Patch Changes + +- Updated dependencies [[`f7bb09b`](https://github.com/Effect-TS/effect/commit/f7bb09b022f195d1f2b3c23d49e74b011ec5d109), [`bd7552a`](https://github.com/Effect-TS/effect/commit/bd7552a19cc0ed575507ac6cc0879a57e24ebd31), [`ad1a7eb`](https://github.com/Effect-TS/effect/commit/ad1a7eb7f6bebaf91c80be2443ac0439226d0098), [`0d32048`](https://github.com/Effect-TS/effect/commit/0d32048f9836e2b23a6ba3ec5f43f0a000bb92fb), [`0d32048`](https://github.com/Effect-TS/effect/commit/0d32048f9836e2b23a6ba3ec5f43f0a000bb92fb)]: + - effect@3.21.0 + - @effect/ai@0.35.0 + - @effect/ai-anthropic@0.25.0 + - @effect/experimental@0.60.0 + - @effect/platform@0.96.0 + +## 0.14.0 + +### Patch Changes + +- Updated dependencies [[`fc82e81`](https://github.com/Effect-TS/effect/commit/fc82e81448bd9136a37580139ce46a2c61b11b54), [`82996bc`](https://github.com/Effect-TS/effect/commit/82996bce8debffcb44feb98bb862cf2662bd56b7), [`4d97a61`](https://github.com/Effect-TS/effect/commit/4d97a61a15b9dd6a0eece65b8f0c035e16d42ada), [`f6b0960`](https://github.com/Effect-TS/effect/commit/f6b0960bf3184109920dfed16ee7dfd7d67bc0f2), [`8798a84`](https://github.com/Effect-TS/effect/commit/8798a843218e6c0c0d3a8eee83360880e370b4da)]: + - effect@3.20.0 + - @effect/ai@0.34.0 + - @effect/ai-anthropic@0.24.0 + - @effect/experimental@0.59.0 + - @effect/platform@0.95.0 + +## 0.13.0 + +### Patch Changes + +- Updated dependencies [[`77eeb86`](https://github.com/Effect-TS/effect/commit/77eeb86ddf208e51ec25932af83d52d3b4700371), [`ff7053f`](https://github.com/Effect-TS/effect/commit/ff7053f6d8508567b6145239f97aacc5773b0c53), [`287c32c`](https://github.com/Effect-TS/effect/commit/287c32c9f10da8e96f2b9ef8424316189d9ad4b3)]: + - effect@3.19.13 + - @effect/platform@0.94.0 + - @effect/ai@0.33.0 + - @effect/ai-anthropic@0.23.0 + - @effect/experimental@0.58.0 + +## 0.12.1 + +### Patch Changes + +- [#5891](https://github.com/Effect-TS/effect/pull/5891) [`b1ffd22`](https://github.com/Effect-TS/effect/commit/b1ffd223ab4abc0be3f56bf4d16d99d0f5b4c4b8) Thanks @sbking! - fix cache point support for user and tool messages + +- Updated dependencies [[`65bff45`](https://github.com/Effect-TS/effect/commit/65bff451fc54d47b32995b3bc898ccc5f8b1beb6)]: + - @effect/platform@0.93.7 + +## 0.12.0 + +### Patch Changes + +- Updated dependencies [[`3c15d5f`](https://github.com/Effect-TS/effect/commit/3c15d5f99fb8d8470a00c5a33d9ba3cac89dfe4c), [`3863fa8`](https://github.com/Effect-TS/effect/commit/3863fa89f61e63e5529fd961e37333bddf7db64a), [`2a03c76`](https://github.com/Effect-TS/effect/commit/2a03c76c2781ca7e9e228e838eab2eb0d0795b1d), [`24a1685`](https://github.com/Effect-TS/effect/commit/24a1685c70a9ed157468650f95a5c3da3f2c2433)]: + - effect@3.19.0 + - @effect/platform@0.93.0 + - @effect/ai@0.32.0 + - @effect/ai-anthropic@0.22.0 + - @effect/experimental@0.57.0 + +## 0.11.0 + +### Minor Changes + +- [#5621](https://github.com/Effect-TS/effect/pull/5621) [`4c3bdfb`](https://github.com/Effect-TS/effect/commit/4c3bdfbcbc2dcd7ecd6321df3e4a504af19de825) Thanks @IMax153! - Remove `Either` / `EitherEncoded` from tool call results. + + Specifically, the encoding of tool call results as an `Either` / `EitherEncoded` has been removed and is replaced by encoding the tool call success / failure directly into the `result` property. + + To allow type-safe discrimination between a tool call result which was a success vs. one that was a failure, an `isFailure` property has also been added to the `"tool-result"` part. If `isFailure` is `true`, then the tool call handler result was an error. + + ```ts + import * as AnthropicClient from "@effect/ai-anthropic/AnthropicClient" + import * as AnthropicLanguageModel from "@effect/ai-anthropic/AnthropicLanguageModel" + import * as LanguageModel from "@effect/ai/LanguageModel" + import * as Tool from "@effect/ai/Tool" + import * as Toolkit from "@effect/ai/Toolkit" + import * as NodeHttpClient from "@effect/platform-node/NodeHttpClient" + import { Config, Effect, Layer, Schema, Stream } from "effect" + + const Claude = AnthropicLanguageModel.model("claude-4-sonnet-20250514") + + const MyTool = Tool.make("MyTool", { + description: "An example of a tool with success and failure types", + failureMode: "return", // Return errors in the response + parameters: { bar: Schema.Number }, + success: Schema.Number, + failure: Schema.Struct({ reason: Schema.Literal("reason-1", "reason-2") }) + }) + + const MyToolkit = Toolkit.make(MyTool) + + const MyToolkitLayer = MyToolkit.toLayer({ + MyTool: () => Effect.succeed(42) + }) + + const program = LanguageModel.streamText({ + prompt: "Tell me about the meaning of life", + toolkit: MyToolkit + }).pipe( + Stream.runForEach((part) => { + if (part.type === "tool-result" && part.name === "MyTool") { + // The `isFailure` property can be used to discriminate whether the result + // of a tool call is a success or a failure + if (part.isFailure) { + part.result + // ^? { readonly reason: "reason-1" | "reason-2"; } + } else { + part.result + // ^? number + } + } + return Effect.void + }), + Effect.provide(Claude) + ) + + const Anthropic = AnthropicClient.layerConfig({ + apiKey: Config.redacted("ANTHROPIC_API_KEY") + }).pipe(Layer.provide(NodeHttpClient.layerUndici)) + + program.pipe(Effect.provide([Anthropic, MyToolkitLayer]), Effect.runPromise) + ``` + +### Patch Changes + +- Updated dependencies [[`4c3bdfb`](https://github.com/Effect-TS/effect/commit/4c3bdfbcbc2dcd7ecd6321df3e4a504af19de825)]: + - @effect/ai-anthropic@0.21.0 + - @effect/ai@0.31.0 + +## 0.10.0 + +### Minor Changes + +- [#5614](https://github.com/Effect-TS/effect/pull/5614) [`c63e658`](https://github.com/Effect-TS/effect/commit/c63e6582244fbb50d31650c4b4ea0660fe194652) Thanks @IMax153! - Previously, tool call handler errors were _always_ raised as an expected error in the Effect `E` channel at the point of execution of the tool call handler (i.e. when a `generate*` method is invoked on a `LanguageModel`). + + With this PR, the end user now has control over whether tool call handler errors should be raised as an Effect error, or returned by the SDK to allow, for example, sending that error information to another application. + + ### Tool Call Specification + + The `Tool.make` and `Tool.providerDefined` constructors now take an extra optional parameter called `failureMode`, which can be set to either `"error"` or `"return"`. + + ```ts + import { Tool } from "@effect/ai" + import { Schema } from "effect" + + const MyTool = Tool.make("MyTool", { + description: "My special tool", + failureMode: "return" // "error" (default) or "return" + parameters: { + myParam: Schema.String + }, + success: Schema.Struct({ + mySuccess: Schema.String + }), + failure: Schema.Struct({ + myFailure: Schema.String + }) + }) + + ``` + + The semantics of `failureMode` are as follows: + - If set to `"error"` (the default), errors that occur during tool call handler execution will be returned in the error channel of the calling effect + - If set to `"return"`, errors that occur during tool call handler execution will be captured and returned as part of the tool call result + + ### Response - Tool Result Parts + + The `result` field of a `"tool-result"` part of a large language model provider response is now represented as an `Either`. + - If the `result` is a `Left`, the `result` will be the `failure` specified in the tool call specification + - If the `result` is a `Right`, the `result` will be the `success` specified in the tool call specification + + This is only relevant if the end user sets `failureMode` to `"return"`. If set to `"error"` (the default), then the `result` property will always be a `Right` with the successful result of the tool call handler. + + Similarly the `encodedResult` field of a `"tool-result"` part will be represented as an `EitherEncoded`, where: + - `{ _tag: "Left", left: }` represents a tool call handler failure + - `{ _tag: "Right", right: }` represents a tool call handler success + + ### Prompt - Tool Result Parts + + The `result` field of a `"tool-result"` part of a prompt will now only accept an `EitherEncoded` as specified above. + +### Patch Changes + +- Updated dependencies [[`1d2e92d`](https://github.com/Effect-TS/effect/commit/1d2e92de9a20f39765bd0b338ffc936ba2fd9463), [`6ae2f5d`](https://github.com/Effect-TS/effect/commit/6ae2f5da45a9ed9832605eca12b3e2bf2e2a1a67), [`c63e658`](https://github.com/Effect-TS/effect/commit/c63e6582244fbb50d31650c4b4ea0660fe194652)]: + - @effect/ai-anthropic@0.20.0 + - effect@3.18.4 + - @effect/ai@0.30.0 + +## 0.9.0 + +### Patch Changes + +- Updated dependencies [[`1c6ab74`](https://github.com/Effect-TS/effect/commit/1c6ab74b314b2b6df8bb1b1a0cb9527ceda0e3fa), [`70fe803`](https://github.com/Effect-TS/effect/commit/70fe803469db3355ffbf8359b52c351f1c2dc137), [`c296e32`](https://github.com/Effect-TS/effect/commit/c296e32554143b84ae8987046984e1cf1852417c), [`a098ddf`](https://github.com/Effect-TS/effect/commit/a098ddfc551f5aa0a7c36f9b4928372a64d4d9f2), [`f8b93ac`](https://github.com/Effect-TS/effect/commit/f8b93ac6446efd3dd790778b0fc71d299a38f272)]: + - effect@3.18.0 + - @effect/ai@0.29.0 + - @effect/platform@0.92.0 + - @effect/ai-anthropic@0.19.0 + - @effect/experimental@0.56.0 + +## 0.8.1 + +### Patch Changes + +- [#5571](https://github.com/Effect-TS/effect/pull/5571) [`122aa53`](https://github.com/Effect-TS/effect/commit/122aa53058ff008cf605cc2f0f0675a946c3cae9) Thanks @IMax153! - Ensure that AI provider clients filter response status for stream requests + +- Updated dependencies [[`122aa53`](https://github.com/Effect-TS/effect/commit/122aa53058ff008cf605cc2f0f0675a946c3cae9)]: + - @effect/ai-anthropic@0.18.2 + +## 0.8.0 + +### Patch Changes + +- Updated dependencies [[`d4d86a8`](https://github.com/Effect-TS/effect/commit/d4d86a81f02b94e09fce8004ce2c5369c505ca5a)]: + - @effect/platform@0.91.0 + - @effect/ai@0.28.0 + - @effect/ai-anthropic@0.18.0 + - @effect/experimental@0.55.0 + +## 0.7.1 + +### Patch Changes + +- [#5521](https://github.com/Effect-TS/effect/pull/5521) [`fa49bc8`](https://github.com/Effect-TS/effect/commit/fa49bc86b14599300d106f306ceaf82a79121b80) Thanks @IMax153! - Fix provider metadata and parse tool call parameters safely + +- Updated dependencies [[`fa49bc8`](https://github.com/Effect-TS/effect/commit/fa49bc86b14599300d106f306ceaf82a79121b80)]: + - @effect/ai-anthropic@0.17.1 + - @effect/ai@0.27.1 + +## 0.7.0 + +### Minor Changes + +- [#5469](https://github.com/Effect-TS/effect/pull/5469) [`42b914a`](https://github.com/Effect-TS/effect/commit/42b914a0e8750350ce17d434afaec7d655ddf4b7) Thanks @IMax153! - Refactor the Effect AI SDK and associated provider packages + + This pull request contains a complete refactor of the base Effect AI SDK package + as well as the associated provider integration packages to improve flexibility + and enhance ergonomics. Major changes are outlined below. + + ## Modules + + All modules in the base Effect AI SDK have had the leading `Ai` prefix dropped + from their name (except for the `AiError` module). + + For example, the `AiLanguageModel` module is now the `LanguageModel` module. + + In addition, the `AiInput` module has been renamed to the `Prompt` module. + + ## Prompts + + The `Prompt` module has been completely redesigned with flexibility in mind. + + The `Prompt` module now supports building a prompt using either the constructors + exposed from the `Prompt` module, or using raw prompt content parts / messages, + which should be familiar to those coming from other AI SDKs. + + In addition, the `system` option has been removed from all `LanguageModel` methods + and must now be provided as part of the prompt. + + **Prompt Constructors** + + ```ts + import { LanguageModel, Prompt } from "@effect/ai" + + const textPart = Prompt.makePart("text", { + text: "What is machine learning?" + }) + + const userMessage = Prompt.makeMessage("user", { + content: [textPart] + }) + + const systemMessage = Prompt.makeMessage("system", { + content: "You are an expert in machine learning" + }) + + const program = LanguageModel.generateText({ + prompt: Prompt.fromMessages([systemMessage, userMessage]) + }) + ``` + + **Raw Prompt Input** + + ```ts + import { LanguageModel } from "@effect/ai" + + const program = LanguageModel.generateText({ + prompt: [ + { role: "system", content: "You are an expert in machine learning" }, + { + role: "user", + content: [{ type: "text", text: "What is machine learning?" }] + } + ] + }) + ``` + + **NOTE**: Providing a plain string as a prompt is still supported, and will be converted + internally into a user message with a single text content part. + + ### Provider-Specific Options + + To support specification of provider-specific options when interacting with large + language model providers, support has been added for adding provider-specific + options to the parts of a `Prompt`. + + ```ts + import { LanguageModel } from "@effect/ai" + import { AnthropicLanguageModel } from "@effect/ai-anthropic" + + const Claude = AnthropicLanguageModel.model("claude-sonnet-4-20250514") + + const program = LanguageModel.generateText({ + prompt: [ + { + role: "user", + content: [{ type: "text", text: "What is machine learning?" }], + options: { + anthropic: { cacheControl: { type: "ephemeral", ttl: "1h" } } + } + } + ] + }).pipe(Effect.provide(Claude)) + ``` + + ## Responses + + The `Response` module has also been completely redesigned to support a wider + variety of response parts, particularly when streaming. + + ### Streaming Responses + + When streaming text via the `LanguageModel.streamText` method, you will now + receive a stream of content parts instead of a stream of responses, which should + make it much simpler to filter down the stream to the parts you are interested in. + + In addition, additional content parts will be present in the stream to allow you to track, + for example, when a text content part starts / ends. + + ### Tool Calls / Tool Call Results + + The decoded parts of a `Response` (as returned by the methods of `LanguageModel`) + are now fully type-safe on tool calls / tool call results. Filtering the content parts of a + response to tool calls will narrow the type of the tool call `params` based on the tool + `name`. Similarly, filtering the response to tool call results will narrow the type of the + tool call `result` based on the tool `name`. + + ```ts + import { LanguageModel, Tool, Toolkit } from "@effect/ai" + import { Effect, Schema } from "effect" + + const DadJokeTool = Tool.make("DadJokeTool", { + parameters: { topic: Schema.String }, + success: Schema.Struct({ joke: Schema.String }) + }) + + const FooTool = Tool.make("FooTool", { + parameters: { foo: Schema.Number }, + success: Schema.Struct({ bar: Schema.Boolean }) + }) + + const MyToolkit = Toolkit.make(DadJokeTool, FooTool) + + const program = Effect.gen(function* () { + const response = yield* LanguageModel.generateText({ + prompt: "Tell me a dad joke", + toolkit: MyToolkit + }) + + for (const toolCall of response.toolCalls) { + if (toolCall.name === "DadJokeTool") { + // ^? "DadJokeTool" | "FooTool" + toolCall.params + // ^? { readonly topic: string } + } + } + + for (const toolResult of response.toolResults) { + if (toolResult.name === "DadJokeTool") { + // ^? "DadJokeTool" | "FooTool" + toolResult.result + // ^? { readonly joke: string } + } + } + }) + ``` + + ### Provider Metadata + + As with provider-specific options, provider-specific metadata is now returned as + part of the response from the large language model provider. + + ```ts + import { LanguageModel } from "@effect/ai" + import { AnthropicLanguageModel } from "@effect/ai-anthropic" + import { Effect } from "effect" + + const Claude = AnthropicLanguageModel.model("claude-4-sonnet-20250514") + + const program = Effect.gen(function* () { + const response = yield* LanguageModel.generateText({ + prompt: "What is the meaning of life?" + }) + + for (const part of response.content) { + // When metadata **is not** defined for a content part, accessing the + // provider's key on the part's metadata will return an untyped record + if (part.type === "text") { + const metadata = part.metadata.anthropic + // ^? { readonly [x: string]: unknown } | undefined + } + // When metadata **is** defined for a content part, accessing the + // provider's key on the part's metadata will return typed metadata + if (part.type === "reasoning") { + const metadata = part.metadata.anthropic + // ^? AnthropicReasoningInfo | undefined + } + } + }).pipe(Effect.provide(Claude)) + ``` + + ## Tool Calls + + The `Tool` module has been enhanced to support provider-defined tools (e.g. + web search, computer use, etc.). Large language model providers which support + calling their own tools now have a separate module present in their provider + integration packages which contain definitions for their tools. + + These provider-defined tools can be included alongside user-defined tools in + existing `Toolkit`s. Provider-defined tools that require a user-space handler + will be raise a type error in the associated `Toolkit` layer if no such handler + is defined. + + ```ts + import { LanguageModel, Tool, Toolkit } from "@effect/ai" + import { AnthropicTool } from "@effect/ai-anthropic" + import { Schema } from "effect" + + const DadJokeTool = Tool.make("DadJokeTool", { + parameters: { topic: Schema.String }, + success: Schema.Struct({ joke: Schema.String }) + }) + + const MyToolkit = Toolkit.make( + DadJokeTool, + AnthropicTool.WebSearch_20250305({ max_uses: 1 }) + ) + + const program = LanguageModel.generateText({ + prompt: "Search the web for a dad joke", + toolkit: MyToolkit + }) + ``` + + ## AiError + + The `AiError` type has been refactored into a union of different error types + which can be raised by the Effect AI SDK. The goal of defining separate error + types is to allow providing the end-user with more granular information about + the error that occurred. + + For now, the following errors have been defined. More error types may be added + over time based upon necessity / use case. + + ```ts + type AiError = + | HttpRequestError, + | HttpResponseError, + | MalformedInput, + | MalformedOutput, + | UnknownError + ``` + +### Patch Changes + +- Updated dependencies [[`42b914a`](https://github.com/Effect-TS/effect/commit/42b914a0e8750350ce17d434afaec7d655ddf4b7)]: + - @effect/ai-anthropic@0.17.0 + - @effect/ai@0.27.0 + +## 0.6.2 + +### Patch Changes + +- [#5438](https://github.com/Effect-TS/effect/pull/5438) [`0065a12`](https://github.com/Effect-TS/effect/commit/0065a12bb82e05cb7766de3c6c9c30fabf883fd9) Thanks @IMax153! - Fix the InferenceConfiguration schema in the Amazon Bedrock AI provider package + +- Updated dependencies [[`3b26094`](https://github.com/Effect-TS/effect/commit/3b2609409ac1e8c6939d699584f00b1b99c47e2e), [`a33e491`](https://github.com/Effect-TS/effect/commit/a33e49153d944abd183fed93267fa7e52abae68b)]: + - effect@3.17.10 + +## 0.6.1 + +### Patch Changes + +- [#5424](https://github.com/Effect-TS/effect/pull/5424) [`3a8ba9b`](https://github.com/Effect-TS/effect/commit/3a8ba9b5e894a28e1724a5d5f3a965348caec2f1) Thanks @IMax153! - Fix system content block structure for Amazon Bedrock `AiLanguageModel` + +- Updated dependencies [[`0271f14`](https://github.com/Effect-TS/effect/commit/0271f1450c0c861f589e26ff534a73dea7ea97b7)]: + - effect@3.17.9 + +## 0.6.0 + +### Patch Changes + +- Updated dependencies []: + - @effect/ai@0.26.0 + - @effect/experimental@0.54.6 + +## 0.5.0 + +### Patch Changes + +- Updated dependencies [[`5a0f4f1`](https://github.com/Effect-TS/effect/commit/5a0f4f176687a39d9fa46bb894bb7ac3175b0e87)]: + - effect@3.17.1 + - @effect/ai@0.25.0 + - @effect/experimental@0.54.0 + +## 0.4.0 + +### Patch Changes + +- Updated dependencies [[`7813640`](https://github.com/Effect-TS/effect/commit/7813640279d9e3a3e7fc0a29bfb5c6d5fb3c270f)]: + - @effect/platform@0.90.0 + - @effect/ai@0.24.0 + - @effect/experimental@0.54.0 + +## 0.3.0 + +### Patch Changes + +- Updated dependencies [[`40c3c87`](https://github.com/Effect-TS/effect/commit/40c3c875f724264312b43002859c82bed9ad0df9), [`ed2c74a`](https://github.com/Effect-TS/effect/commit/ed2c74ae8fa4ea0dd06ea84a3e58cd32e6916104), [`073a1b8`](https://github.com/Effect-TS/effect/commit/073a1b8be5dbfa87454393ee7346f5bc36a4fd63), [`f382e99`](https://github.com/Effect-TS/effect/commit/f382e99e409838a879246250fc3994b9bf5b3c2c), [`e8c7ba5`](https://github.com/Effect-TS/effect/commit/e8c7ba5fd3eb0c3ae3039fc24c09d69391987989), [`7e10415`](https://github.com/Effect-TS/effect/commit/7e1041599ade25103428703f5d2dfd7378a09636), [`e9bdece`](https://github.com/Effect-TS/effect/commit/e9bdececdc24f60a246be5055eca71a0d49ea7f2), [`8d95eb0`](https://github.com/Effect-TS/effect/commit/8d95eb0356b1d1736204836c275d201a547d208d)]: + - effect@3.17.0 + - @effect/ai@0.23.0 + - @effect/experimental@0.53.0 + - @effect/platform@0.89.0 + +## 0.2.1 + +### Patch Changes + +- Updated dependencies [[`f5dfabf`](https://github.com/Effect-TS/effect/commit/f5dfabf51ba481a4468c1509c537314978ef6cec), [`17a5ea8`](https://github.com/Effect-TS/effect/commit/17a5ea8fa29785fe6e4c9480f2a2e9c8c59f3f38), [`d25f22b`](https://github.com/Effect-TS/effect/commit/d25f22be7598abe977caf6cdac3b0dd78b438c48)]: + - effect@3.16.14 + - @effect/platform@0.88.1 + - @effect/experimental@0.52.1 + - @effect/ai@0.22.1 + +## 0.2.0 + +### Patch Changes + +- Updated dependencies [[`27206d7`](https://github.com/Effect-TS/effect/commit/27206d7f0558d7fe28de57bf54f1d0cc83acc92e), [`dbabf5e`](https://github.com/Effect-TS/effect/commit/dbabf5e76fa63b050d2b6c466713c7dc59f07d3c)]: + - @effect/platform@0.88.0 + - @effect/ai@0.22.0 + - @effect/experimental@0.52.0 + +## 0.1.14 + +### Patch Changes + +- Updated dependencies [[`c1c05a8`](https://github.com/Effect-TS/effect/commit/c1c05a8242fb5df7445b4a12387a60eac7726eb7), [`81fe4a2`](https://github.com/Effect-TS/effect/commit/81fe4a2c81d5e30e180a60e68c52016a27b350db)]: + - effect@3.16.13 + - @effect/ai@0.21.17 + - @effect/experimental@0.51.14 + - @effect/platform@0.87.13 + +## 0.1.13 + +### Patch Changes + +- [#5186](https://github.com/Effect-TS/effect/pull/5186) [`e5692ab`](https://github.com/Effect-TS/effect/commit/e5692ab2be157b885f449ffb5c5f022eca04a59e) Thanks @IMax153! - Do not use `Config.Wrap` for AI provider `layerConfig` + +- Updated dependencies [[`32ba77a`](https://github.com/Effect-TS/effect/commit/32ba77ae304d2161362a73e8b61965332626cf2d), [`d5e25b2`](https://github.com/Effect-TS/effect/commit/d5e25b237f05670ee42b386cb40b2cb448fc11d7)]: + - @effect/platform@0.87.12 + - @effect/ai@0.21.16 + - @effect/experimental@0.51.13 + +## 0.1.12 + +### Patch Changes + +- Updated dependencies [[`001392b`](https://github.com/Effect-TS/effect/commit/001392ba8bfcad101bb034348a7415012fb12f72), [`7bfb099`](https://github.com/Effect-TS/effect/commit/7bfb099cb5528511b8d63045c4fbb4dc9cb18528)]: + - @effect/platform@0.87.11 + - @effect/ai@0.21.15 + - @effect/experimental@0.51.12 + +## 0.1.11 + +### Patch Changes + +- Updated dependencies [[`678318d`](https://github.com/Effect-TS/effect/commit/678318d2e88233156b006acda56c2d138ee3ffa0), [`678318d`](https://github.com/Effect-TS/effect/commit/678318d2e88233156b006acda56c2d138ee3ffa0)]: + - @effect/platform@0.87.10 + - @effect/ai@0.21.14 + - @effect/experimental@0.51.11 + +## 0.1.10 + +### Patch Changes + +- Updated dependencies [[`54514a2`](https://github.com/Effect-TS/effect/commit/54514a2f53166de27ad7e756dbf12194691fd4af)]: + - @effect/platform@0.87.9 + - @effect/ai@0.21.13 + - @effect/experimental@0.51.10 + +## 0.1.9 + +### Patch Changes + +- Updated dependencies [[`4ce4f82`](https://github.com/Effect-TS/effect/commit/4ce4f824f6fdef492be1d35c05a490ffce518c89)]: + - @effect/platform@0.87.8 + - @effect/experimental@0.51.9 + - @effect/ai@0.21.12 + +## 0.1.8 + +### Patch Changes + +- Updated dependencies [[`d92d12a`](https://github.com/Effect-TS/effect/commit/d92d12acb6097a4fa6c9c918faa3cd5c3fb6c778), [`25ca0cf`](https://github.com/Effect-TS/effect/commit/25ca0cf141139cd44ff53081b1c877f8f3ab5e41), [`d92d12a`](https://github.com/Effect-TS/effect/commit/d92d12acb6097a4fa6c9c918faa3cd5c3fb6c778)]: + - @effect/ai@0.21.11 + +## 0.1.7 + +### Patch Changes + +- Updated dependencies [[`a9b617f`](https://github.com/Effect-TS/effect/commit/a9b617f125171ed76cd79ab46d7a924daf3b0e70), [`7e26e86`](https://github.com/Effect-TS/effect/commit/7e26e86524abcc93713d6ad7eee486638c98f7c2)]: + - @effect/platform@0.87.7 + - @effect/ai@0.21.10 + - @effect/experimental@0.51.8 + +## 0.1.6 + +### Patch Changes + +- Updated dependencies [[`030ac21`](https://github.com/Effect-TS/effect/commit/030ac217eac167d345a095bff26d9c95827fa64c), [`905da99`](https://github.com/Effect-TS/effect/commit/905da996aad665057b4ca6dba1a4af44fb8835bd), [`aaae9b1`](https://github.com/Effect-TS/effect/commit/aaae9b10345ab5f867b08e1c6eb21685cfc2b078)]: + - @effect/ai@0.21.9 + - effect@3.16.12 + - @effect/experimental@0.51.7 + - @effect/platform@0.87.6 + +## 0.1.5 + +### Patch Changes + +- Updated dependencies [[`96c1292`](https://github.com/Effect-TS/effect/commit/96c129262835410b311a51d0bf7f58b8f6fc9a12)]: + - @effect/experimental@0.51.6 + - @effect/ai@0.21.8 + +## 0.1.4 + +### Patch Changes + +- Updated dependencies [[`2fd8676`](https://github.com/Effect-TS/effect/commit/2fd8676c803cd40000dfc3231f5daecaa0e0ebd2)]: + - @effect/platform@0.87.5 + - @effect/ai@0.21.7 + - @effect/experimental@0.51.5 + +## 0.1.3 + +### Patch Changes + +- Updated dependencies [[`e82a4fd`](https://github.com/Effect-TS/effect/commit/e82a4fd60f6528d08cef1a4aba0abe0d3ba741ad)]: + - @effect/platform@0.87.4 + - @effect/ai@0.21.6 + - @effect/experimental@0.51.4 + +## 0.1.2 + +### Patch Changes + +- Updated dependencies [[`1b6e396`](https://github.com/Effect-TS/effect/commit/1b6e396d699f3cbbc56b68f99055cf746529bb9e)]: + - @effect/platform@0.87.3 + - @effect/ai@0.21.5 + - @effect/experimental@0.51.3 + +## 0.1.1 + +### Patch Changes + +- Updated dependencies [[`4fea68c`](https://github.com/Effect-TS/effect/commit/4fea68ca7a25a3c39a1ab68b3885534513ab0c81), [`b927954`](https://github.com/Effect-TS/effect/commit/b9279543cf5688dd8a577af80456959c615217d0), [`99590a6`](https://github.com/Effect-TS/effect/commit/99590a6ca9128eb1ede265b6670b655311995614), [`6c3e24c`](https://github.com/Effect-TS/effect/commit/6c3e24c2308f7d4a29b8f4270ab81bca22ac6bb4)]: + - @effect/platform@0.87.2 + - effect@3.16.11 + - @effect/ai@0.21.4 + - @effect/experimental@0.51.2 + +## 0.1.0 + +### Minor Changes + +- [#5020](https://github.com/Effect-TS/effect/pull/5020) [`530aa65`](https://github.com/Effect-TS/effect/commit/530aa6561b68ea591cef44e30e8629082e42fda2) Thanks @IMax153! - add Amazon Bedrock AI provider package diff --git a/repos/effect/packages/ai/amazon-bedrock/LICENSE b/repos/effect/packages/ai/amazon-bedrock/LICENSE new file mode 100644 index 0000000..be1f5c1 --- /dev/null +++ b/repos/effect/packages/ai/amazon-bedrock/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Effectful Technologies Inc + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/repos/effect/packages/ai/amazon-bedrock/README.md b/repos/effect/packages/ai/amazon-bedrock/README.md new file mode 100644 index 0000000..11e6999 --- /dev/null +++ b/repos/effect/packages/ai/amazon-bedrock/README.md @@ -0,0 +1,5 @@ +# `@effect/ai-amazon-bedrock` + +## Documentation + +- **API Reference**: [View the full documentation](https://effect-ts.github.io/effect/docs/ai/amazon-bedrock). diff --git a/repos/effect/packages/ai/amazon-bedrock/docgen.json b/repos/effect/packages/ai/amazon-bedrock/docgen.json new file mode 100644 index 0000000..7807af7 --- /dev/null +++ b/repos/effect/packages/ai/amazon-bedrock/docgen.json @@ -0,0 +1,28 @@ +{ + "$schema": "../../../node_modules/@effect/docgen/schema.json", + "exclude": ["src/Generated.ts", "src/internal/**/*.ts"], + "srcLink": "https://github.com/Effect-TS/effect/tree/main/packages/ai/amazon-bedrock/src/", + "examplesCompilerOptions": { + "noEmit": true, + "strict": true, + "skipLibCheck": true, + "moduleResolution": "Bundler", + "module": "ES2022", + "target": "ES2022", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "paths": { + "effect": ["../../../../effect/src/index.js"], + "effect/*": ["../../../../effect/src/*.js"], + "@effect/experimental": ["../../../../experimental/src/index.js"], + "@effect/experimental/*": ["../../../../experimental/src/*.js"], + "@effect/platform": ["../../../../platform/src/index.js"], + "@effect/platform/*": ["../../../../platform/src/*.js"], + "@effect/ai": ["../../../ai/src/index.js"], + "@effect/ai/*": ["../../../ai/src/*.js"], + "@effect/ai-anthropic": ["../../../anthropic/src/index.js"], + "@effect/ai-anthropic/*": ["../../../anthropic/src/*.js"], + "@effect/ai-amazon-bedrock": ["../../../amazon-bedrock/src/index.js"], + "@effect/ai-amazon-bedrock/*": ["../../../amazon-bedrock/src/*.js"] + } + } +} diff --git a/repos/effect/packages/ai/amazon-bedrock/package.json b/repos/effect/packages/ai/amazon-bedrock/package.json new file mode 100644 index 0000000..efbae14 --- /dev/null +++ b/repos/effect/packages/ai/amazon-bedrock/package.json @@ -0,0 +1,70 @@ +{ + "name": "@effect/ai-amazon-bedrock", + "type": "module", + "version": "0.15.0", + "license": "MIT", + "description": "Effect modules for working with Amazon Bedrock AI apis", + "homepage": "https://effect.website", + "repository": { + "type": "git", + "url": "https://github.com/Effect-TS/effect.git", + "directory": "packages/ai/amazon-bedrock" + }, + "bugs": { + "url": "https://github.com/Effect-TS/effect/issues" + }, + "tags": [ + "typescript", + "algebraic-data-types", + "functional-programming" + ], + "keywords": [ + "typescript", + "algebraic-data-types", + "functional-programming" + ], + "publishConfig": { + "access": "public", + "provenance": true, + "directory": "dist", + "linkDirectory": false + }, + "exports": { + "./package.json": "./package.json", + ".": "./src/index.ts", + "./*": "./src/*.ts", + "./internal/*": null + }, + "scripts": { + "codegen": "build-utils prepare-v3", + "build": "pnpm build-esm && pnpm build-annotate && pnpm build-cjs && build-utils pack-v3", + "build-esm": "tsc -b tsconfig.build.json", + "build-cjs": "babel build/esm --plugins @babel/transform-export-namespace-from --plugins @babel/transform-modules-commonjs --out-dir build/cjs --source-maps", + "build-annotate": "babel build/esm --plugins annotate-pure-calls --out-dir build/esm --source-maps", + "check": "tsc -b tsconfig.json", + "test": "vitest", + "coverage": "vitest --coverage" + }, + "peerDependencies": { + "@effect/ai": "workspace:^", + "@effect/ai-anthropic": "workspace:^", + "@effect/experimental": "workspace:^", + "@effect/platform": "workspace:^", + "effect": "workspace:^" + }, + "devDependencies": { + "@effect/ai": "workspace:^", + "@effect/ai-anthropic": "workspace:^", + "@effect/experimental": "workspace:^", + "@effect/platform": "workspace:^", + "@effect/platform-node": "workspace:^", + "@tim-smart/openapi-gen": "^0.3.18", + "effect": "workspace:^" + }, + "dependencies": { + "@smithy/eventstream-codec": "^4.0.4", + "@smithy/util-utf8": "^4.0.0", + "aws4fetch": "^1.0.20", + "gpt-tokenizer": "^2.9.0" + } +} diff --git a/repos/effect/packages/ai/amazon-bedrock/src/AmazonBedrockClient.ts b/repos/effect/packages/ai/amazon-bedrock/src/AmazonBedrockClient.ts new file mode 100644 index 0000000..001ad39 --- /dev/null +++ b/repos/effect/packages/ai/amazon-bedrock/src/AmazonBedrockClient.ts @@ -0,0 +1,316 @@ +/** + * @since 1.0.0 + */ +import * as AiError from "@effect/ai/AiError" +import * as Headers from "@effect/platform/Headers" +import * as HttpBody from "@effect/platform/HttpBody" +import * as HttpClient from "@effect/platform/HttpClient" +import * as HttpClientError from "@effect/platform/HttpClientError" +import * as HttpClientRequest from "@effect/platform/HttpClientRequest" +import * as HttpClientResponse from "@effect/platform/HttpClientResponse" +import { AwsV4Signer } from "aws4fetch" +import * as Arr from "effect/Array" +import * as Config from "effect/Config" +import type { ConfigError } from "effect/ConfigError" +import * as Context from "effect/Context" +import * as Effect from "effect/Effect" +import { identity } from "effect/Function" +import * as Layer from "effect/Layer" +import type { ParseError } from "effect/ParseResult" +import * as Redacted from "effect/Redacted" +import type * as Schema from "effect/Schema" +import * as Stream from "effect/Stream" +import { AmazonBedrockConfig } from "./AmazonBedrockConfig.js" +import type { ConverseRequest } from "./AmazonBedrockSchema.js" +import { ConverseResponse, ConverseResponseStreamEvent } from "./AmazonBedrockSchema.js" +import * as EventStreamEncoding from "./EventStreamEncoding.js" + +/** + * @since 1.0.0 + * @category Context + */ +export class AmazonBedrockClient extends Context.Tag( + "@effect/ai-amazon-bedrock/AmazonBedrockClient" +)() {} + +/** + * @since 1.0.0 + * @category Models + */ +export interface Service { + readonly client: Client + + readonly streamRequest: ( + request: HttpClientRequest.HttpClientRequest, + schema: Schema.Schema + ) => Stream.Stream + + readonly converse: (options: { + readonly params?: { "anthropic-beta"?: string | undefined } | undefined + readonly payload: typeof ConverseRequest.Encoded + }) => Effect.Effect + + readonly converseStream: (options: { + readonly params?: { "anthropic-beta"?: string | undefined } | undefined + readonly payload: typeof ConverseRequest.Encoded + }) => Stream.Stream +} + +/** + * @since 1.0.0 + * @category Constructors + */ +export const make = (options: { + readonly apiUrl?: string | undefined + readonly accessKeyId: string + readonly secretAccessKey: Redacted.Redacted + readonly sessionToken?: Redacted.Redacted | undefined + readonly region?: string | undefined + readonly transformClient?: ( + client: HttpClient.HttpClient + ) => HttpClient.HttpClient +}) => + Effect.gen(function*() { + const region = options.region ?? "us-east-1" + + const redactedHeaders = ["X-Amz-Security-Token"] + + yield* Effect.locallyScopedWith(Headers.currentRedactedNames, Arr.appendAll(redactedHeaders)) + + const httpClient = (yield* HttpClient.HttpClient).pipe( + HttpClient.mapRequest((request) => + request.pipe( + HttpClientRequest.prependUrl(options.apiUrl ?? `https://bedrock-runtime.${region}.amazonaws.com`), + HttpClientRequest.acceptJson + ) + ), + HttpClient.mapRequestEffect(Effect.fnUntraced(function*(request) { + const originalHeaders = request.headers + const signer = new AwsV4Signer({ + service: "bedrock", + url: request.url, + method: request.method, + headers: Object.entries(originalHeaders), + body: prepareBody(request.body), + region, + accessKeyId: options.accessKeyId, + secretAccessKey: Redacted.value(options.secretAccessKey), + ...(options.sessionToken ? { sessionToken: Redacted.value(options.sessionToken) } : {}) + }) + const { headers: signedHeaders } = yield* Effect.promise(() => signer.sign()) + const headers = Headers.merge(originalHeaders, Headers.fromInput(signedHeaders)) + return HttpClientRequest.setHeaders(request, headers) + })), + options.transformClient ? options.transformClient : identity + ) + + const httpClientOk = HttpClient.filterStatusOk(httpClient) + + const client = makeClient(httpClient, { + transformClient: (client) => + AmazonBedrockConfig.getOrUndefined.pipe( + Effect.map((config) => config?.transformClient ? config.transformClient(client) : client) + ) + }) + + const converse: (options: { + readonly params?: { "anthropic-beta"?: string | undefined } | undefined + readonly payload: typeof ConverseRequest.Encoded + }) => Effect.Effect = Effect.fnUntraced( + function*(request) { + return yield* client.converse(request).pipe( + Effect.catchTags({ + RequestError: (error) => + AiError.HttpRequestError.fromRequestError({ + module: "AmazonBedrockClient", + method: "converse", + error + }), + ResponseError: (error) => + AiError.HttpResponseError.fromResponseError({ + module: "AmazonBedrockClient", + method: "converse", + error + }), + ParseError: (error) => + AiError.MalformedOutput.fromParseError({ + module: "AmazonBedrockClient", + method: "converse", + error + }) + }) + ) + } + ) + + const streamRequest = ( + request: HttpClientRequest.HttpClientRequest, + schema: Schema.Schema + ): Stream.Stream => + httpClientOk.execute(request).pipe( + Effect.map((r) => r.stream), + Stream.unwrapScoped, + Stream.pipeThroughChannel(EventStreamEncoding.makeChannel(schema)), + Stream.catchTags({ + RequestError: (error) => + AiError.HttpRequestError.fromRequestError({ + module: "AmazonBedrockClient", + method: "streamRequest", + error + }), + ResponseError: (error) => + AiError.HttpResponseError.fromResponseError({ + module: "AmazonBedrockClient", + method: "streamRequest", + error + }), + ParseError: (error) => + AiError.MalformedOutput.fromParseError({ + module: "AmazonBedrockClient", + method: "streamRequest", + error + }) + }) + ) + + const converseStream = (options: { + readonly params?: { "anthropic-beta"?: string | undefined } | undefined + readonly payload: typeof ConverseRequest.Encoded + }): Stream.Stream => { + const { modelId, ...body } = options.payload + const request = HttpClientRequest.post(`/model/${modelId}/converse-stream`, { + headers: Headers.fromInput({ + "anthropic-beta": options.params?.["anthropic-beta"] + }), + body: HttpBody.unsafeJson(body) + }) + return streamRequest(request, ConverseResponseStreamEvent) + } + + return AmazonBedrockClient.of({ + client, + streamRequest, + converse, + converseStream + }) + }) + +/** + * @since 1.0.0 + * @category layers + */ +export const layer = (options: { + readonly apiUrl?: string | undefined + readonly accessKeyId: string + readonly secretAccessKey: Redacted.Redacted + readonly sessionToken?: Redacted.Redacted | undefined + readonly region?: string | undefined + readonly transformClient?: ( + client: HttpClient.HttpClient + ) => HttpClient.HttpClient +}): Layer.Layer => Layer.scoped(AmazonBedrockClient, make(options)) + +/** + * @since 1.0.0 + * @category layers + */ +export const layerConfig = ( + options: { + readonly apiUrl?: Config.Config | undefined + readonly accessKeyId: Config.Config + readonly secretAccessKey: Config.Config + readonly sessionToken?: Config.Config | undefined + readonly region?: Config.Config | undefined + readonly transformClient?: ( + client: HttpClient.HttpClient + ) => HttpClient.HttpClient + } +): Layer.Layer => { + const { transformClient, ...configs } = options + return Config.all(configs).pipe( + Effect.flatMap((configs) => make({ ...configs, transformClient })), + Layer.scoped(AmazonBedrockClient) + ) +} + +// ============================================================================= +// Client +// ============================================================================= + +/** + * @since 1.0.0 + * @category models + */ +export interface Client { + readonly converse: (options: { + readonly params?: { "anthropic-beta"?: string | undefined } + readonly payload: typeof ConverseRequest.Encoded + }) => Effect.Effect +} + +const makeClient = ( + httpClient: HttpClient.HttpClient, + options: { + readonly transformClient?: ((client: HttpClient.HttpClient) => Effect.Effect) | undefined + } +): Client => { + const unexpectedStatus = (response: HttpClientResponse.HttpClientResponse) => + Effect.flatMap( + Effect.orElseSucceed(response.json, () => "Unexpected status code"), + (description) => + Effect.fail( + new HttpClientError.ResponseError({ + request: response.request, + response, + reason: "StatusCode", + description: typeof description === "string" ? description : JSON.stringify(description) + }) + ) + ) + const withResponse: ( + f: (response: HttpClientResponse.HttpClientResponse) => Effect.Effect + ) => ( + request: HttpClientRequest.HttpClientRequest + ) => Effect.Effect = options.transformClient + ? (f) => (request) => + Effect.flatMap( + Effect.flatMap(options.transformClient!(httpClient), (client) => client.execute(request)), + f + ) + : (f) => (request) => Effect.flatMap(httpClient.execute(request), f) + const decodeSuccess = + (schema: Schema.Schema) => (response: HttpClientResponse.HttpClientResponse) => + HttpClientResponse.schemaBodyJson(schema)(response) + return { + converse: ({ params, payload: { modelId, ...payload } }) => + HttpClientRequest.post(`/model/${modelId}/converse`).pipe( + HttpClientRequest.setHeaders({ + "anthropic-beta": params?.["anthropic-beta"] ?? undefined + }), + HttpClientRequest.bodyUnsafeJson(payload), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ConverseResponse), + orElse: unexpectedStatus + })) + ) + } +} + +const prepareBody = (body: HttpBody.HttpBody): string => { + switch (body._tag) { + case "Raw": + case "Uint8Array": { + if (typeof body.body === "string") { + return body.body + } + if (body.body instanceof Uint8Array) { + return new TextDecoder().decode(body.body) + } + if (body.body instanceof ArrayBuffer) { + return new TextDecoder().decode(body.body) + } + return JSON.stringify(body.body) + } + } + throw new Error("Unsupported HttpBody: " + body._tag) +} diff --git a/repos/effect/packages/ai/amazon-bedrock/src/AmazonBedrockConfig.ts b/repos/effect/packages/ai/amazon-bedrock/src/AmazonBedrockConfig.ts new file mode 100644 index 0000000..2efd6b4 --- /dev/null +++ b/repos/effect/packages/ai/amazon-bedrock/src/AmazonBedrockConfig.ts @@ -0,0 +1,56 @@ +/** + * @since 1.0.0 + */ +import type { HttpClient } from "@effect/platform/HttpClient" +import * as Context from "effect/Context" +import * as Effect from "effect/Effect" +import { dual } from "effect/Function" + +/** + * @since 1.0.0 + * @category Context + */ +export class AmazonBedrockConfig extends Context.Tag("@effect/ai-google/AmazonBedrockConfig")< + AmazonBedrockConfig, + AmazonBedrockConfig.Service +>() { + /** + * @since 1.0.0 + */ + static readonly getOrUndefined: Effect.Effect = Effect.map( + Effect.context(), + (context) => context.unsafeMap.get(AmazonBedrockConfig.key) + ) +} + +/** + * @since 1.0.0 + */ +export declare namespace AmazonBedrockConfig { + /** + * @since 1.0. + * @category Models + */ + export interface Service { + readonly transformClient?: (client: HttpClient) => HttpClient + } +} + +/** + * @since 1.0.0 + * @category Configuration + */ +export const withClientTransform: { + (transform: (client: HttpClient) => HttpClient): (self: Effect.Effect) => Effect.Effect + (self: Effect.Effect, transform: (client: HttpClient) => HttpClient): Effect.Effect +} = dual< + (transform: (client: HttpClient) => HttpClient) => (self: Effect.Effect) => Effect.Effect, + (self: Effect.Effect, transform: (client: HttpClient) => HttpClient) => Effect.Effect +>( + 2, + (self, transformClient) => + Effect.flatMap( + AmazonBedrockConfig.getOrUndefined, + (config) => Effect.provideService(self, AmazonBedrockConfig, { ...config, transformClient }) + ) +) diff --git a/repos/effect/packages/ai/amazon-bedrock/src/AmazonBedrockLanguageModel.ts b/repos/effect/packages/ai/amazon-bedrock/src/AmazonBedrockLanguageModel.ts new file mode 100644 index 0000000..dbcadd0 --- /dev/null +++ b/repos/effect/packages/ai/amazon-bedrock/src/AmazonBedrockLanguageModel.ts @@ -0,0 +1,1220 @@ +/** + * @since 1.0.0 + */ +import { prepareTools as prepareAnthropicTools } from "@effect/ai-anthropic/AnthropicLanguageModel" +import * as AnthropicTool from "@effect/ai-anthropic/AnthropicTool" +import * as AiError from "@effect/ai/AiError" +import type * as IdGenerator from "@effect/ai/IdGenerator" +import * as LanguageModel from "@effect/ai/LanguageModel" +import * as AiModel from "@effect/ai/Model" +import type * as Prompt from "@effect/ai/Prompt" +import type * as Response from "@effect/ai/Response" +import { addGenAIAnnotations } from "@effect/ai/Telemetry" +import * as Tool from "@effect/ai/Tool" +import * as Context from "effect/Context" +import * as DateTime from "effect/DateTime" +import * as Effect from "effect/Effect" +import * as Encoding from "effect/Encoding" +import { dual } from "effect/Function" +import * as Layer from "effect/Layer" +import * as Predicate from "effect/Predicate" +import * as Stream from "effect/Stream" +import type { Span } from "effect/Tracer" +import type { Mutable, Simplify } from "effect/Types" +import { AmazonBedrockClient } from "./AmazonBedrockClient.js" +import type { + BedrockFoundationModelId, + CachePointBlock, + ContentBlock, + ConverseRequest, + ConverseResponse, + ConverseResponseStreamEvent, + ConverseTrace, + DocumentFormat, + Message, + SystemContentBlock, + Tool as AmazonBedrockTool, + ToolChoice, + ToolConfiguration +} from "./AmazonBedrockSchema.js" +import { ImageFormat } from "./AmazonBedrockSchema.js" +import * as InternalUtilities from "./internal/utilities.js" + +const BEDROCK_CACHE_POINT: { + readonly cachePoint: typeof CachePointBlock.Encoded +} = { cachePoint: { type: "default" } } + +/** + * @since 1.0.0 + * @category Models + */ +export type Model = typeof BedrockFoundationModelId.Encoded + +// ============================================================================= +// Configuration +// ============================================================================= + +/** + * @since 1.0.0 + * @category Context + */ +export class Config extends Context.Tag( + "@effect/ai-amazon-bedrock/AmazonBedrockLanguageModel/Config" +)() { + /** + * @since 1.0.0 + */ + static readonly getOrUndefined: Effect.Effect = Effect.map( + Effect.context(), + (context) => context.unsafeMap.get(Config.key) + ) +} + +/** + * @since 1.0.0 + */ +export declare namespace Config { + /** + * @since 1.0.0 + * @category Configuration + */ + export interface Service extends + Simplify< + Partial< + Omit< + typeof ConverseRequest.Encoded, + "messages" | "system" | "toolConfig" + > + > + > + {} +} + +// ============================================================================= +// Amazon Bedrock Provider Options / Metadata +// ============================================================================= + +/** + * @since 1.0.0 + * @category Provider Options + */ +export type AmazonBedrockReasoningInfo = { + readonly type: "thinking" + /** + * Thinking content as an encrypted string, which is used to verify + * that thinking content was indeed generated by Amazon Bedrock's API. + */ + readonly signature: string +} | { + readonly type: "redacted_thinking" + /** + * Thinking content which was flagged by Amazon Bedrock's safety systems, and + * was therefore encrypted. + */ + readonly redactedData: string +} + +declare module "@effect/ai/Prompt" { + export interface SystemMessageOptions extends ProviderOptions { + readonly bedrock?: { + /** + * Defines a section of content to be cached for reuse in subsequent API + * calls. + */ + readonly cachePoint?: typeof CachePointBlock.Encoded | undefined + } | undefined + } + + export interface UserMessageOptions extends ProviderOptions { + readonly bedrock?: { + /** + * Defines a section of content to be cached for reuse in subsequent API + * calls. + */ + readonly cachePoint?: typeof CachePointBlock.Encoded | undefined + } | undefined + } + + export interface AssistantMessageOptions extends ProviderOptions { + readonly bedrock?: { + /** + * Defines a section of content to be cached for reuse in subsequent API + * calls. + */ + readonly cachePoint?: typeof CachePointBlock.Encoded | undefined + } | undefined + } + + export interface ToolMessageOptions extends ProviderOptions { + readonly bedrock?: { + /** + * Defines a section of content to be cached for reuse in subsequent API + * calls. + */ + readonly cachePoint?: typeof CachePointBlock.Encoded | undefined + } | undefined + } + + export interface ReasoningPartOptions extends ProviderOptions { + readonly bedrock?: AmazonBedrockReasoningInfo | undefined + } +} + +declare module "@effect/ai/Response" { + export interface ReasoningPartMetadata extends ProviderMetadata { + readonly bedrock?: AmazonBedrockReasoningInfo | undefined + } + + export interface FinishPartMetadata extends ProviderMetadata { + readonly bedrock?: { + readonly trace?: ConverseTrace | undefined + readonly usage: { + readonly cacheWriteInputTokens?: number | undefined + } + } | undefined + } +} + +// ============================================================================= +// Amazon Bedrock Language Model +// ============================================================================= + +/** + * @since 1.0.0 + * @category AiModels + */ +export const model = ( + model: (string & {}) | Model, + config?: Omit +): AiModel.Model<"amazon-bedrock", LanguageModel.LanguageModel, AmazonBedrockClient> => + AiModel.make("amazon-bedrock", layer({ model, config })) + +/** + * @since 1.0.0 + * @category Constructors + */ +export const make = Effect.fnUntraced(function*(options: { + readonly model: (string & {}) | Model + readonly config?: Omit +}) { + const client = yield* AmazonBedrockClient + + const makeRequest = Effect.fnUntraced( + function*(providerOptions: LanguageModel.ProviderOptions) { + const context = yield* Effect.context() + const config = { modelId: options.model, ...options.config, ...context.unsafeMap.get(Config.key) } + const { messages, system } = yield* prepareMessages(providerOptions) + const { additionalTools, betas, toolConfig } = yield* prepareTools(providerOptions, config) + const responseFormat = providerOptions.responseFormat + const request: typeof ConverseRequest.Encoded = { + ...config, + system, + messages, + // Handle tool configuration + ...(responseFormat.type === "json" + ? { + toolConfig: { + tools: [{ + toolSpec: { + name: responseFormat.objectName, + description: Tool.getDescriptionFromSchemaAst(responseFormat.schema.ast) ?? + "Respond with a JSON object", + inputSchema: { + json: Tool.getJsonSchemaFromSchemaAst(responseFormat.schema.ast) as any + } + } + }], + toolChoice: { tool: { name: responseFormat.objectName } } + } + } + : Predicate.isNotUndefined(toolConfig.tools) && toolConfig.tools.length > 0 + ? { toolConfig } + : {}), + // Handle additional model request fields + ...(Predicate.isNotUndefined(additionalTools) + ? { + additionalModelRequestFields: { + ...config.additionalModelRequestFields, + ...additionalTools + } + } + : {}) + } + return { betas, request } + } + ) + + return yield* LanguageModel.make({ + generateText: Effect.fnUntraced( + function*(options) { + const { betas, request } = yield* makeRequest(options) + annotateRequest(options.span, request) + const anthropicBeta = betas.size > 0 ? Array.from(betas).join(",") : undefined + const rawResponse = yield* client.converse({ + params: { "anthropic-beta": anthropicBeta }, + payload: request + }) + annotateResponse(options.span, request, rawResponse) + return yield* makeResponse(request, rawResponse, options) + } + ), + streamText: Effect.fnUntraced( + function*(options) { + const { betas, request } = yield* makeRequest(options) + annotateRequest(options.span, request) + const anthropicBeta = betas.size > 0 ? Array.from(betas).join(",") : undefined + const stream = client.converseStream({ + params: { "anthropic-beta": anthropicBeta }, + payload: request + }) + return { request, stream } + }, + (effect, options) => + effect.pipe( + Effect.flatMap(({ request, stream }) => makeStreamResponse(request, stream, options)), + Stream.unwrap, + Stream.map((response) => { + annotateStreamResponse(options.span, response) + return response + }) + ) + ) + }) +}) + +/** + * @since 1.0.0 + * @category Layers + */ +export const layer = (options: { + readonly model: (string & {}) | Model + readonly config?: Omit +}): Layer.Layer => + Layer.effect(LanguageModel.LanguageModel, make({ model: options.model, config: options.config })) + +/** + * @since 1.0.0 + * @category Configuration + */ +export const withConfigOverride: { + (config: Config.Service): (self: Effect.Effect) => Effect.Effect + (self: Effect.Effect, config: Config.Service): Effect.Effect +} = dual< + (config: Config.Service) => (self: Effect.Effect) => Effect.Effect, + (self: Effect.Effect, config: Config.Service) => Effect.Effect +>(2, (self, overrides) => + Effect.flatMap( + Config.getOrUndefined, + (config) => Effect.provideService(self, Config, { ...config, ...overrides }) + )) + +// ============================================================================= +// Prompt Conversion +// ============================================================================= + +const prepareMessages: (options: LanguageModel.ProviderOptions) => Effect.Effect<{ + readonly system: ReadonlyArray + readonly messages: ReadonlyArray +}, AiError.AiError> = Effect.fnUntraced( + function*(options) { + const groups = groupMessages(options.prompt) + + const system: Array = [] + const messages: Array = [] + + let documentCounter = 0 + const nextDocumentName = () => `document-${++documentCounter}` + + for (let i = 0; i < groups.length; i++) { + const group = groups[i] + const isLastGroup = i === groups.length - 1 + + switch (group.type) { + case "system": { + if (messages.length > 0) { + return yield* new AiError.MalformedInput({ + module: "AmazonBedrockLanguageModel", + method: "prepareMessages", + description: "Multiple system messages separated by user / assistant messages" + }) + } + for (const message of group.messages) { + system.push({ text: message.content }) + if (Predicate.isNotUndefined(getCachePoint(message))) { + system.push(BEDROCK_CACHE_POINT) + } + } + break + } + + case "user": { + const content: Array = [] + + for (const message of group.messages) { + switch (message.role) { + case "user": { + for (let j = 0; j < message.content.length; j++) { + const part = message.content[j] + + switch (part.type) { + case "text": { + content.push({ text: part.text }) + break + } + + case "file": { + if (part.data instanceof URL) { + // TODO(Max): support this + return yield* new AiError.MalformedInput({ + module: "AmazonBedrockLanguageModel", + method: "prepareMessages", + description: "File URL inputs are not supported at this time" + }) + } + if (part.mediaType.startsWith("image/")) { + content.push({ + image: { + format: yield* getImageFormat(part.mediaType), + source: { bytes: convertToBase64(part.data) } + } + }) + } else { + content.push({ + document: { + format: yield* getDocumentFormat(part.mediaType), + name: nextDocumentName(), + source: { bytes: convertToBase64(part.data) } + } + }) + } + break + } + } + } + break + } + + case "tool": { + for (const part of message.content) { + content.push({ + toolResult: { + toolUseId: part.id, + content: [{ text: JSON.stringify(part.result) }] + } + }) + } + break + } + } + + if (getCachePoint(message)) { + content.push(BEDROCK_CACHE_POINT) + } + } + + messages.push({ role: "user", content }) + + break + } + + case "assistant": { + const content: Array = [] + + for (let j = 0; j < group.messages.length; j++) { + const message = group.messages[j] + const isLastMessage = j === group.messages.length - 1 + + for (let k = 0; k < message.content.length; k++) { + const part = message.content[k] + const isLastPart = k === message.content.length - 1 + + switch (part.type) { + case "text": { + // Skip empty text blocks + if (part.text.trim().length === 0) { + break + } + content.push({ + // Amazon Bedrock does not allow trailing whitespace in + // assistant content blocks + text: trimIfLast(isLastGroup, isLastMessage, isLastPart, part.text) + }) + break + } + + case "reasoning": { + const options = part.options.bedrock + if (Predicate.isNotUndefined(options)) { + if (options.type === "thinking") { + content.push({ + reasoningContent: { + reasoningText: { + // Amazon Bedrock does not allow trailing whitespace in + // assistant content blocks + text: trimIfLast(isLastGroup, isLastMessage, isLastPart, part.text), + signature: options.signature + } + } + }) + } + if (options.type === "redacted_thinking") { + content.push({ + reasoningContent: { + redactedContent: options.redactedData + } + }) + } + } + break + } + + case "tool-call": { + content.push({ + toolUse: { + toolUseId: part.id, + name: part.name, + input: part.params + } + }) + break + } + } + } + + if (getCachePoint(message)) { + content.push(BEDROCK_CACHE_POINT) + } + } + + messages.push({ role: "assistant", content }) + + break + } + } + } + + return { system, messages } + } +) + +// ============================================================================= +// Response Conversion +// ============================================================================= + +const makeResponse: ( + request: typeof ConverseRequest.Encoded, + response: ConverseResponse, + options: LanguageModel.ProviderOptions +) => Effect.Effect< + Array, + never, + IdGenerator.IdGenerator +> = Effect.fnUntraced(function*(request, response, options) { + const parts: Array = [] + + parts.push({ + type: "response-metadata", + modelId: request.modelId, + timestamp: DateTime.formatIso(yield* DateTime.now) + }) + + for (const part of response.output.message.content) { + switch (part.type) { + case "text": { + // The text parts should only be added to the response here if the + // response format is `"text"`. If the response format is `"json"`, + // then the text parts must instead be added to the response when a + // tool call is received. + if (options.responseFormat.type === "text") { + parts.push({ + type: "text", + text: part.text + }) + } + break + } + + case "reasoningContent": { + if (part.reasoningContent.type === "reasoning") { + const signature = part.reasoningContent.reasoningText.signature + parts.push({ + type: "reasoning", + text: part.reasoningContent.reasoningText.text, + metadata: Predicate.isNotUndefined(signature) ? + { bedrock: { type: "thinking", signature } } + : undefined + }) + } + if (part.reasoningContent.type === "redacted-reasoning") { + parts.push({ + type: "reasoning", + text: "", + metadata: { + bedrock: { + type: "redacted_thinking", + redactedData: part.reasoningContent.redactedContent + } + } + }) + } + break + } + + case "toolUse": { + // When a `"json"` response format is requested, the JSON that we need + // will be returned by the tool call injected into the request + if (options.responseFormat.type === "json") { + parts.push({ + type: "text", + text: JSON.stringify(part.toolUse.input) + }) + } else { + const providerTool = AnthropicTool.getProviderDefinedToolName(part.toolUse.name) + const name = Predicate.isNotUndefined(providerTool) ? providerTool : part.toolUse.name + const providerName = Predicate.isNotUndefined(providerTool) ? part.toolUse.name : undefined + parts.push({ + type: "tool-call", + id: part.toolUse.toolUseId, + name, + params: part.toolUse.input, + providerName, + providerExecuted: false + }) + } + break + } + } + } + + const finishReason = InternalUtilities.resolveFinishReason(response.stopReason) + + parts.push({ + type: "finish", + reason: finishReason, + usage: { + inputTokens: response.usage.inputTokens, + outputTokens: response.usage.outputTokens, + totalTokens: response.usage.totalTokens, + cachedInputTokens: response.usage.cacheReadInputTokens + }, + metadata: { + bedrock: { + trace: response.trace, + usage: { cacheWriteInputTokens: response.usage.cacheWriteInputTokens } + } + } + }) + + return parts +}) + +const makeStreamResponse: ( + request: typeof ConverseRequest.Encoded, + stream: Stream.Stream, + options: LanguageModel.ProviderOptions +) => Effect.Effect< + Stream.Stream, + never, + IdGenerator.IdGenerator +> = Effect.fnUntraced( + function*(request, stream, options) { + const contentBlocks: Record< + number, + | { + readonly type: "text" + } + | { + readonly type: "reasoning" + } + | { + readonly type: "tool-call" + readonly id: string + readonly name: string + params: string + readonly providerName?: string | undefined + readonly providerExecuted: boolean + } + > = {} + + let trace: ConverseTrace | undefined = undefined + let cacheWriteInputTokens: number | undefined = undefined + const usage: Mutable = { + inputTokens: undefined, + outputTokens: undefined, + totalTokens: undefined + } + + return stream.pipe( + Stream.mapEffect(Effect.fnUntraced(function*(event) { + const parts: Array = [] + + switch (event.type) { + case "messageStart": { + parts.push({ + type: "response-metadata", + modelId: request.modelId, + timestamp: DateTime.formatIso(yield* DateTime.now) + }) + break + } + + case "messageStop": { + const reason = InternalUtilities.resolveFinishReason(event.messageStop.stopReason) + parts.push({ + type: "finish", + reason, + usage, + metadata: { + bedrock: { trace, usage: { cacheWriteInputTokens } } + } + }) + + break + } + + case "contentBlockStart": { + const index = event.contentBlockStart.contentBlockIndex + const block = event.contentBlockStart + if (Predicate.isNotUndefined(block.start.toolUse)) { + const toolUse = block.start.toolUse + const toolName = toolUse.name + const providerTool = AnthropicTool.getProviderDefinedToolName(toolName) + const name = Predicate.isNotUndefined(providerTool) ? providerTool : toolName + const providerName = Predicate.isNotUndefined(providerTool) ? toolName : undefined + + contentBlocks[index] = { + type: "tool-call", + id: toolUse.toolUseId, + name, + params: "", + providerName, + providerExecuted: false + } + // Only emit tool param delta events for text responses + if (options.responseFormat.type === "text") { + parts.push({ + type: "tool-params-start", + id: toolUse.toolUseId, + name: toolUse.name, + providerExecuted: false + }) + } + } else { + contentBlocks[index] = { type: "text" } + parts.push({ + type: "text-start", + id: index.toString() + }) + } + break + } + + case "contentBlockDelta": { + const index = event.contentBlockDelta.contentBlockIndex + const delta = event.contentBlockDelta.delta + + switch (delta.type) { + case "text": { + const block = contentBlocks[index] + if (Predicate.isUndefined(block)) { + contentBlocks[index] = { type: "text" } + // Only emit text delta events for text responses + if (options.responseFormat.type === "text") { + parts.push({ + type: "text-start", + id: index.toString() + }) + } + } + // Only emit text delta events for text responses + if (options.responseFormat.type === "text") { + parts.push({ + type: "text-delta", + id: index.toString(), + delta: delta.text + }) + } + break + } + + case "reasoningContent": { + if ("text" in delta.reasoningContent) { + const block = contentBlocks[index] + if (Predicate.isUndefined(block)) { + contentBlocks[index] = { type: "reasoning" } + parts.push({ + type: "reasoning-start", + id: index.toString() + }) + } + parts.push({ + type: "reasoning-delta", + id: index.toString(), + delta: delta.reasoningContent.text + }) + } else if ("signature" in delta.reasoningContent) { + parts.push({ + type: "reasoning-delta", + id: index.toString(), + delta: "", + metadata: { + bedrock: { + type: "thinking", + signature: delta.reasoningContent.signature + } + } + }) + } else { + parts.push({ + type: "reasoning-delta", + id: index.toString(), + delta: "", + metadata: { + bedrock: { + type: "redacted_thinking", + redactedData: delta.reasoningContent.redactedContent + } + } + }) + } + break + } + + case "toolUse": { + const block = contentBlocks[index] + if (Predicate.isNotUndefined(block) && block.type === "tool-call") { + const params = delta.toolUse.input + // Only emit tool params delta events for text responses + if (options.responseFormat.type === "text") { + parts.push({ + type: "tool-params-delta", + id: block.id, + delta: params + }) + } + block.params += params + } + break + } + } + break + } + + case "contentBlockStop": { + const index = event.contentBlockStop.contentBlockIndex + const block = contentBlocks[index] + if (Predicate.isNotUndefined(block)) { + switch (block.type) { + case "text": { + // Only emit text end events for text responses + if (options.responseFormat.type === "text") { + parts.push({ + type: "text-end", + id: index.toString() + }) + } + break + } + + case "reasoning": { + parts.push({ + type: "reasoning-end", + id: index.toString() + }) + break + } + + case "tool-call": { + // Only emit tool param events for text responses - for JSON + // responses, the structured output will be emitted as text + if (options.responseFormat.type === "text") { + parts.push({ + type: "tool-params-end", + id: block.id + }) + + const toolName = block.name + const toolParams = block.params + + const params = yield* Effect.try({ + try: () => Tool.unsafeSecureJsonParse(toolParams), + catch: (cause) => + new AiError.MalformedOutput({ + module: "AmazonBedrockLanguageModel", + method: "makeStreamResponse", + description: "Failed to securely parse tool call parameters " + + `for tool '${toolName}':\nParameters: ${toolParams}`, + cause + }) + }) + + parts.push({ + type: "tool-call", + id: block.id, + name: toolName, + params, + providerName: block.providerName, + providerExecuted: block.providerExecuted + }) + } else { + parts.push({ + type: "text-start", + id: index.toString() + }) + parts.push({ + type: "text-delta", + id: index.toString(), + delta: block.params + }) + parts.push({ + type: "text-end", + id: index.toString() + }) + } + break + } + } + delete contentBlocks[index] + } + break + } + + case "metadata": { + usage.inputTokens = event.metadata.usage.inputTokens + usage.outputTokens = event.metadata.usage.outputTokens + usage.totalTokens = event.metadata.usage.totalTokens + usage.cachedInputTokens = event.metadata.usage.cacheReadInputTokens + if (Predicate.isNotUndefined(event.metadata.usage.cacheWriteInputTokens)) { + cacheWriteInputTokens = event.metadata.usage.cacheWriteInputTokens + } + if (Predicate.isNotUndefined(event.metadata.trace)) { + trace = event.metadata.trace + } + break + } + + case "internalServerException": { + parts.push({ type: "error", error: event.internalServerException }) + break + } + + case "modelStreamErrorException": { + parts.push({ type: "error", error: event.modelStreamErrorException }) + break + } + + case "serviceUnavailableException": { + parts.push({ type: "error", error: event.serviceUnavailableException }) + break + } + + case "throttlingException": { + parts.push({ type: "error", error: event.throttlingException }) + break + } + + case "validationException": { + parts.push({ type: "error", error: event.validationException }) + break + } + } + + return parts + })), + Stream.flattenIterables + ) + } +) + +// ============================================================================= +// Tool Calling +// ============================================================================= + +const prepareTools: ( + options: LanguageModel.ProviderOptions, + config: Config.Service +) => Effect.Effect<{ + readonly betas: ReadonlySet + readonly toolConfig: Partial + readonly additionalTools?: Record | undefined +}, AiError.AiError> = Effect.fnUntraced(function*(options, config) { + const betas = new Set() + + if (options.tools.length === 0) { + return { toolConfig: {}, betas } + } + + const isAnthropicModel = config.modelId!.includes("anthropic.") + const userDefinedTools: Array = [] + const providerDefinedTools: Array = [] + for (const tool of options.tools) { + if (Tool.isUserDefined(tool)) { + userDefinedTools.push(tool) + } else { + providerDefinedTools.push(tool as Tool.AnyProviderDefined) + } + } + + const hasAnthropicTools = isAnthropicModel && providerDefinedTools.length > 0 + + let tools: Array = [] + let additionalTools: Record | undefined = undefined + + // Handle Anthropic provider-defined tools for Anthropic models on Bedrock + if (hasAnthropicTools) { + const prepared = yield* prepareAnthropicTools(options, {}) + + prepared.betas.forEach((beta) => betas.add(beta)) + + if (Predicate.isNotUndefined(prepared.toolChoice)) { + // For Anthropic tools on Bedrock, only the 'tool_choice' goes into + // the `additionalModelRequestFields` parameter of the request, while + // the tool definitions themselves are sent in the usual `toolConfig` + additionalTools = { + tool_choice: prepared.toolChoice + } + } + + // Handle conversion of provider-defined tools to Amazon Bedrock tool definitions + for (const providerDefinedTool of providerDefinedTools) { + tools.push({ + toolSpec: { + name: providerDefinedTool.providerName, + description: Tool.getDescription(providerDefinedTool as any), + inputSchema: { + json: Tool.getJsonSchema(providerDefinedTool as any) as any + } + } + }) + } + } + + // Handle conversion of user-defined tools to Amazon Bedrock tool definitions + for (const tool of userDefinedTools) { + tools.push({ + toolSpec: { + name: tool.name, + description: Tool.getDescription(tool as any), + inputSchema: { + json: Tool.getJsonSchema(tool as any) as any + } + } + }) + } + + // Handle resolution of tool choice for **Amazon Bedrock** user-defined tools. + // The tool choice for Anthropic provider-defined tools is resolved above and + // inserted into the additional tool configuration object. + let toolChoice: typeof ToolChoice.Encoded | undefined = undefined + if (!hasAnthropicTools && tools.length > 0 && Predicate.isNotUndefined(options.toolChoice)) { + if (options.toolChoice === "none") { + tools.length = 0 + toolChoice = undefined + } else if (options.toolChoice === "auto") { + toolChoice = { auto: {} } + } else if (options.toolChoice === "required") { + toolChoice = { any: {} } + } else if ("tool" in options.toolChoice) { + toolChoice = { tool: { name: options.toolChoice.tool } } + } else { + const allowedTools = new Set(options.toolChoice.oneOf) + tools = tools.filter((tool) => allowedTools.has(tool.toolSpec?.name)) + toolChoice = options.toolChoice.mode === "auto" ? { auto: {} } : { any: {} } + } + } + + const toolConfig: Partial = tools.length > 0 + ? { tools, toolChoice } + : {} + + return { additionalTools, betas, toolConfig } +}) + +// ============================================================================= +// Telemetry +// ============================================================================= + +const annotateRequest = ( + span: Span, + request: typeof ConverseRequest.Encoded +): void => { + addGenAIAnnotations(span, { + system: "anthropic", + operation: { name: "chat" }, + request: { + model: request.modelId, + temperature: request.inferenceConfig?.temperature, + topP: request.inferenceConfig?.topP, + maxTokens: request.inferenceConfig?.maxTokens, + stopSequences: request.inferenceConfig?.stopSequences ?? [] + } + }) +} + +const annotateResponse = ( + span: Span, + request: typeof ConverseRequest.Encoded, + response: ConverseResponse +): void => { + addGenAIAnnotations(span, { + response: { + model: request.modelId, + finishReasons: response.stopReason ? [response.stopReason] : undefined + }, + usage: { + inputTokens: response.usage.inputTokens, + outputTokens: response.usage.outputTokens + } + }) +} + +const annotateStreamResponse = (span: Span, part: Response.StreamPartEncoded) => { + if (part.type === "response-metadata") { + addGenAIAnnotations(span, { + response: { + id: part.id, + model: part.modelId + } + }) + } + if (part.type === "finish") { + addGenAIAnnotations(span, { + response: { + finishReasons: [part.reason] + }, + usage: { + inputTokens: part.usage.inputTokens, + outputTokens: part.usage.outputTokens + } + }) + } +} + +// ============================================================================= +// Utilities +// ============================================================================= + +type ContentGroup = SystemMessageGroup | AssistantMessageGroup | UserMessageGroup + +interface SystemMessageGroup { + readonly type: "system" + readonly messages: Array +} + +interface AssistantMessageGroup { + readonly type: "assistant" + readonly messages: Array +} + +interface UserMessageGroup { + readonly type: "user" + readonly messages: Array +} + +const groupMessages = (prompt: Prompt.Prompt): Array => { + const messages: Array = [] + let current: ContentGroup | undefined = undefined + for (const message of prompt.content) { + switch (message.role) { + case "system": { + if (current?.type !== "system") { + current = { type: "system", messages: [] } + messages.push(current) + } + current.messages.push(message) + break + } + case "assistant": { + if (current?.type !== "assistant") { + current = { type: "assistant", messages: [] } + messages.push(current) + } + current.messages.push(message) + break + } + case "tool": + case "user": { + if (current?.type !== "user") { + current = { type: "user", messages: [] } + messages.push(current) + } + current.messages.push(message) + break + } + } + } + return messages +} + +/** + * Amazon Bedrock does not allow trailing whitespace in pre-fillled assistant + * responses, so we trim the final text part here if it's the last message in + * the group. + */ +const trimIfLast = ( + isLastGroup: boolean, + isLastMessage: boolean, + isLastPart: boolean, + text: string +) => isLastGroup && isLastMessage && isLastPart ? text.trim() : text + +const getCachePoint = ( + part: + | Prompt.SystemMessage + | Prompt.UserMessage + | Prompt.AssistantMessage + | Prompt.ToolMessage +): typeof CachePointBlock.Encoded | undefined => part.options.bedrock?.cachePoint + +const convertToBase64 = (data: string | Uint8Array): string => + typeof data === "string" ? data : Encoding.encodeBase64(data) + +const DOCUMENT_MIME_TYPES: Record = { + "application/pdf": "pdf", + "text/csv": "csv", + "application/msword": "doc", + "application/vnd.openxmlformats-officedocument.wordprocessingml.document": "docx", + "application/vnd.ms-excel": "xls", + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": "xlsx", + "text/html": "html", + "text/plain": "txt", + "text/markdown": "md" +} + +const getDocumentFormat: ( + mediaType: string +) => Effect.Effect = Effect.fnUntraced( + function*(mediaType) { + const format = DOCUMENT_MIME_TYPES[mediaType] + + if (Predicate.isUndefined(format)) { + return yield* new AiError.MalformedInput({ + module: "AmazonBedrockLanguageModel", + method: "getDocumentFormat", + description: `Unsupported document MIME type: ${mediaType} - expected ` + + `one of: ${Object.keys(DOCUMENT_MIME_TYPES)}` + }) + } + + return format + } +) + +const getImageFormat: ( + mediaType: string +) => Effect.Effect = Effect.fnUntraced( + function*(mediaType) { + const format = ImageFormat.literals.find((format) => mediaType === `image/${format}`) + + if (Predicate.isUndefined(format)) { + return yield* new AiError.MalformedInput({ + module: "AmazonBedrockLanguageModel", + method: "getImageFormat", + description: `Unsupported image MIME type: ${mediaType} - expected ` + + `one of: ${ImageFormat.literals.map((format) => `image/${format}`).join(",")}` + }) + } + + return format + } +) diff --git a/repos/effect/packages/ai/amazon-bedrock/src/AmazonBedrockSchema.ts b/repos/effect/packages/ai/amazon-bedrock/src/AmazonBedrockSchema.ts new file mode 100644 index 0000000..d21c01c --- /dev/null +++ b/repos/effect/packages/ai/amazon-bedrock/src/AmazonBedrockSchema.ts @@ -0,0 +1,1126 @@ +/** + * @since 1.0.0 + */ +import * as Schema from "effect/Schema" + +const prefix = "@effect/ai-amazon-bedrock" + +const makeIdentifier = (name: string) => `${prefix}/${name}` + +/** + * The foundation models supported by Amazon Bedrock. + * + * An up-to-date list can be generated with the following command: + * + * ```sh + * aws bedrock list-foundation-models --output json | jq '[.modelSummaries[].modelId]' + * ``` + * + * @since 1.0.0 + * @category Schemas + */ +export class BedrockFoundationModelId extends Schema.Literal( + "amazon.titan-tg1-large", + "amazon.titan-image-generator-v1:0", + "amazon.titan-image-generator-v1", + "amazon.titan-image-generator-v2:0", + "amazon.nova-premier-v1:0:8k", + "amazon.nova-premier-v1:0:20k", + "amazon.nova-premier-v1:0:1000k", + "amazon.nova-premier-v1:0:mm", + "amazon.nova-premier-v1:0", + "amazon.titan-text-premier-v1:0", + "amazon.nova-pro-v1:0:24k", + "amazon.nova-pro-v1:0:300k", + "amazon.nova-pro-v1:0", + "amazon.nova-lite-v1:0:24k", + "amazon.nova-lite-v1:0:300k", + "amazon.nova-lite-v1:0", + "amazon.nova-canvas-v1:0", + "amazon.nova-reel-v1:0", + "amazon.nova-reel-v1:1", + "amazon.nova-micro-v1:0:24k", + "amazon.nova-micro-v1:0:128k", + "amazon.nova-micro-v1:0", + "amazon.nova-sonic-v1:0", + "amazon.titan-embed-g1-text-02", + "amazon.titan-text-lite-v1:0:4k", + "amazon.titan-text-lite-v1", + "amazon.titan-text-express-v1:0:8k", + "amazon.titan-text-express-v1", + "amazon.titan-embed-text-v1:2:8k", + "amazon.titan-embed-text-v1", + "amazon.titan-embed-text-v2:0:8k", + "amazon.titan-embed-text-v2:0", + "amazon.titan-embed-image-v1:0", + "amazon.titan-embed-image-v1", + "stability.stable-diffusion-xl-v1:0", + "stability.stable-diffusion-xl-v1", + "ai21.jamba-instruct-v1:0", + "ai21.jamba-1-5-large-v1:0", + "ai21.jamba-1-5-mini-v1:0", + "anthropic.claude-instant-v1:2:100k", + "anthropic.claude-instant-v1", + "anthropic.claude-v2:0:18k", + "anthropic.claude-v2:0:100k", + "anthropic.claude-v2:1:18k", + "anthropic.claude-v2:1:200k", + "anthropic.claude-v2:1", + "anthropic.claude-v2", + "anthropic.claude-3-sonnet-20240229-v1:0:28k", + "anthropic.claude-3-sonnet-20240229-v1:0:200k", + "anthropic.claude-3-sonnet-20240229-v1:0", + "anthropic.claude-3-haiku-20240307-v1:0:48k", + "anthropic.claude-3-haiku-20240307-v1:0:200k", + "anthropic.claude-3-haiku-20240307-v1:0", + "anthropic.claude-3-opus-20240229-v1:0:12k", + "anthropic.claude-3-opus-20240229-v1:0:28k", + "anthropic.claude-3-opus-20240229-v1:0:200k", + "anthropic.claude-3-opus-20240229-v1:0", + "anthropic.claude-3-5-sonnet-20240620-v1:0", + "anthropic.claude-3-5-sonnet-20241022-v2:0", + "anthropic.claude-3-7-sonnet-20250219-v1:0", + "anthropic.claude-3-5-haiku-20241022-v1:0", + "anthropic.claude-opus-4-20250514-v1:0", + "anthropic.claude-sonnet-4-20250514-v1:0", + "cohere.command-text-v14:7:4k", + "cohere.command-text-v14", + "cohere.command-r-v1:0", + "cohere.command-r-plus-v1:0", + "cohere.command-light-text-v14:7:4k", + "cohere.command-light-text-v14", + "cohere.embed-english-v3:0:512", + "cohere.embed-english-v3", + "cohere.embed-multilingual-v3:0:512", + "cohere.embed-multilingual-v3", + "deepseek.r1-v1:0", + "meta.llama3-8b-instruct-v1:0", + "meta.llama3-70b-instruct-v1:0", + "meta.llama3-1-8b-instruct-v1:0", + "meta.llama3-1-70b-instruct-v1:0", + "meta.llama3-2-11b-instruct-v1:0", + "meta.llama3-2-90b-instruct-v1:0", + "meta.llama3-2-1b-instruct-v1:0", + "meta.llama3-2-3b-instruct-v1:0", + "meta.llama3-3-70b-instruct-v1:0", + "meta.llama4-scout-17b-instruct-v1:0", + "meta.llama4-maverick-17b-instruct-v1:0", + "mistral.mistral-7b-instruct-v0:2", + "mistral.mixtral-8x7b-instruct-v0:1", + "mistral.mistral-large-2402-v1:0", + "mistral.mistral-small-2402-v1:0", + "mistral.pixtral-large-2502-v1:0" +) {} + +/** + * @since 1.0.0 + * @category Schemas + */ +export class CachePointBlock extends Schema.Class(makeIdentifier("CachePointBlock"))({ + type: Schema.Literal("default") +}) {} + +/** + * @since 1.0.0 + * @category Schemas + */ +export const DocumentFormat = Schema.Literal( + "csv", + "doc", + "docx", + "html", + "md", + "pdf", + "txt", + "xls", + "xlsx" +) +/** + * @since 1.0.0 + * @category Schemas + */ +export type DocumentFormat = typeof DocumentFormat.Type + +/** + * @since 1.0.0 + * @category Schemas + */ +export class DocumentBlock extends Schema.Class(makeIdentifier("DocumentBlock"))({ + name: Schema.String.pipe( + Schema.pattern(/^[a-zA-Z0-9()[\]-]+(?: [a-zA-Z0-9()[\]-]+)*$/), + Schema.minLength(1), + Schema.maxLength(200) + ), + format: DocumentFormat, + source: Schema.Struct({ + bytes: Schema.NonEmptyString + }) +}) {} + +/** + * @since 1.0.0 + * @category Schemas + */ +export class GuardrailConverseImageBlock extends Schema.Class( + makeIdentifier("GuardrailConverseImageBlock") +)({ + format: Schema.Literal("png", "jpg"), + source: Schema.Struct({ + bytes: Schema.NonEmptyString + }) +}) {} + +/** + * @since 1.0.0 + * @category Schemas + */ +export class GuardrailConverseTextBlock extends Schema.Class( + makeIdentifier("GuardrailConverseTextBlock") +)({ + text: Schema.String, + qualifiers: Schema.optional(Schema.Array(Schema.Literal("guard_content", "grounding_source", "query"))) +}) {} + +/** + * @since 1.0.0 + * @category Schemas + */ +export class GuardrailConverseContentBlock extends Schema.Union( + GuardrailConverseImageBlock, + GuardrailConverseTextBlock +) {} + +/** + * @since 1.0.0 + * @category Schemas + */ +export const ImageFormat = Schema.Literal("gif", "jpeg", "png", "webp") +/** + * @since 1.0.0 + * @category Schemas + */ +export type ImageFormat = typeof ImageFormat.Type + +/** + * @since 1.0.0 + * @category Schemas + */ +export class ImageBlock extends Schema.Class(makeIdentifier("ImageBlock"))({ + format: ImageFormat, + source: Schema.Struct({ + bytes: Schema.NonEmptyString + }) +}) {} + +/** + * @since 1.0.0 + * @category Schemas + */ +export class JsonBlock extends Schema.Class(makeIdentifier("JsonBlock"))({ + json: Schema.Unknown +}) {} + +/** + * @since 1.0.0 + * @category Schemas + */ +export class ReasoningContentBlock extends Schema.Union( + Schema.Struct({ + reasoningText: Schema.Struct({ + text: Schema.String, + signature: Schema.optional(Schema.String) + }) + }).pipe( + Schema.attachPropertySignature("type", "reasoning"), + Schema.annotations({ identifier: "ReasoningTextContentBlock" }) + ), + Schema.Struct({ + redactedContent: Schema.String + }).pipe( + Schema.attachPropertySignature("type", "redacted-reasoning"), + Schema.annotations({ identifier: "RedactedReasoningContentBlock" }) + ) +) {} + +/** + * @since 1.0.0 + * @category Schemas + */ +export class VideoBlock extends Schema.Class(makeIdentifier("VideoBlock"))({ + format: Schema.Literal("flv", "mkv", "mov", "mp4", "mpg", "mpeg", "three_gp", "webm"), + source: Schema.Union( + Schema.Struct({ + bytes: Schema.NonEmptyString + }), + Schema.Struct({ + s3Location: Schema.Struct({ + uri: Schema.String.pipe( + Schema.pattern(/^s3:\/\/[a-z0-9][.\-a-z0-9]{1,61}[a-z0-9](\/.*)?$/), + Schema.minLength(1), + Schema.maxLength(1024) + ), + bucketOwner: Schema.String.pipe( + Schema.pattern(/^[0-9]{12}$/), + Schema.optional + ) + }) + }) + ) +}) {} + +/** + * @since 1.0.0 + * @category Schemas + */ +export class ToolResultBlock extends Schema.Class(makeIdentifier("ToolResultBlock"))({ + content: Schema.Array(Schema.Union( + Schema.Struct({ document: DocumentBlock }), + Schema.Struct({ image: ImageBlock }), + Schema.Struct({ text: Schema.String }), + Schema.Struct({ json: JsonBlock }), + Schema.Struct({ video: VideoBlock }) + )), + toolUseId: Schema.String.pipe( + Schema.pattern(/^[a-zA-Z0-9_-]+$/), + Schema.minLength(1), + Schema.maxLength(64) + ), + status: Schema.optional(Schema.Literal("success", "error")) +}) {} + +/** + * @since 1.0.0 + * @category Schemas + */ +export class ToolUseBlock extends Schema.Class(makeIdentifier("ToolUseBlock"))({ + name: Schema.String.pipe( + Schema.pattern(/^[a-zA-Z0-9_-]+$/), + Schema.minLength(1), + Schema.maxLength(64) + ), + input: Schema.Unknown, + toolUseId: Schema.String.pipe( + Schema.pattern(/^[a-zA-Z0-9_-]+$/), + Schema.minLength(1), + Schema.maxLength(64) + ) +}) {} + +/** + * @since 1.0.0 + * @category Schemas + */ +export class ContentBlock extends Schema.Union( + Schema.Struct({ cachePoint: CachePointBlock }).pipe( + Schema.attachPropertySignature("type", "cachePoint"), + Schema.annotations({ identifier: "CachePointContentBlock" }) + ), + Schema.Struct({ document: DocumentBlock }).pipe( + Schema.attachPropertySignature("type", "document"), + Schema.annotations({ identifier: "DocumentContentBlock" }) + ), + Schema.Struct({ guardContent: GuardrailConverseContentBlock }).pipe( + Schema.attachPropertySignature("type", "guardContent"), + Schema.annotations({ identifier: "GuardrailContentBlock" }) + ), + Schema.Struct({ image: ImageBlock }).pipe( + Schema.attachPropertySignature("type", "image"), + Schema.annotations({ identifier: "ImageContentBlock" }) + ), + Schema.Struct({ reasoningContent: ReasoningContentBlock }).pipe( + Schema.attachPropertySignature("type", "reasoningContent"), + Schema.annotations({ identifier: "ResponseContentBlock" }) + ), + Schema.Struct({ text: Schema.String }).pipe( + Schema.attachPropertySignature("type", "text"), + Schema.annotations({ identifier: "TextContentBlock" }) + ), + Schema.Struct({ toolResult: ToolResultBlock }).pipe( + Schema.attachPropertySignature("type", "toolResult"), + Schema.annotations({ identifier: "ToolResultContentBlock" }) + ), + Schema.Struct({ toolUse: ToolUseBlock }).pipe( + Schema.attachPropertySignature("type", "toolUse"), + Schema.annotations({ identifier: "ToolUseContentBlock" }) + ), + Schema.Struct({ video: VideoBlock }).pipe( + Schema.attachPropertySignature("type", "video"), + Schema.annotations({ identifier: "VideoContentBlock" }) + ) +) {} + +/** + * @since 1.0.0 + * @category Schemas + */ +export class Message extends Schema.Class(makeIdentifier("Message"))({ + role: Schema.Literal("user", "assistant"), + content: Schema.Array(ContentBlock) +}) {} + +/** + * @since 1.0.0 + * @category Schemas + */ +export class ConverseOutput extends Schema.Class(makeIdentifier("ConverseOutput"))({ + message: Message +}) {} + +/** + * @since 1.0.0 + * @category Schemas + */ +export class ConverseMetrics extends Schema.Class(makeIdentifier("ConverseMetrics"))({ + latencyMs: Schema.DurationFromMillis +}) {} + +/** + * @since 1.0.0 + * @category Schemas + */ +export class GuardrailContentFilter extends Schema.Class( + makeIdentifier("GuardrailContentFilter") +)({ + type: Schema.Literal("HATE", "INSULTS", "MISCONDUCT", "PROMPT_ATTACK", "SEXUAL", "VIOLENCE"), + action: Schema.Literal("BLOCKED", "NONE"), + confidence: Schema.Literal("NONE", "LOW", "MEDIUM", "HIGH"), + detected: Schema.optional(Schema.Boolean), + filterStrength: Schema.optional(Schema.Literal("NONE", "LOW", "MEDIUM", "HIGH")) +}) {} + +/** + * @since 1.0.0 + * @category Schemas + */ +export class GuardrailContentPolicyAssessment extends Schema.Class( + makeIdentifier("GuardrailContentPolicyAssessment") +)({ + filters: Schema.Array(GuardrailContentFilter) +}) {} + +/** + * @since 1.0.0 + * @category Schemas + */ +export class GuardrailContextualGroundingFilter extends Schema.Class( + makeIdentifier("GuardrailContextualGroundingFilter") +)({ + type: Schema.Literal("GROUNDING", "RELEVANCE"), + action: Schema.Literal("BLOCKED", "NONE"), + score: Schema.Number.pipe(Schema.between(0, 1)), + threshold: Schema.Number.pipe(Schema.between(0, 1)), + detected: Schema.optional(Schema.Boolean) +}) {} + +/** + * @since 1.0.0 + * @category Schemas + */ +export class GuardrailContextualGroundingPolicyAssessment + extends Schema.Class( + makeIdentifier("GuardrailContextualGroundingPolicyAssessment") + )({ + filters: Schema.optional(Schema.Array(GuardrailContextualGroundingFilter)) + }) +{} + +/** + * @since 1.0.0 + * @category Schemas + */ +export class GuardrailImageCoverage extends Schema.Class( + makeIdentifier("GuardrailImageCoverage") +)({ + guarded: Schema.optional(Schema.Int), + total: Schema.optional(Schema.Int) +}) {} + +/** + * @since 1.0.0 + * @category Schemas + */ +export class GuardrailTextCharactersCoverage extends Schema.Class( + makeIdentifier("GuardrailTextCharactersCoverage") +)({ + guarded: Schema.optional(Schema.Int), + total: Schema.optional(Schema.Int) +}) {} + +/** + * @since 1.0.0 + * @category Schemas + */ +export class GuardrailCoverage extends Schema.Class(makeIdentifier("GuardrailCoverage"))({ + images: Schema.optional(GuardrailImageCoverage), + textCharacters: Schema.optional(GuardrailTextCharactersCoverage) +}) {} + +/** + * @since 1.0.0 + * @category Schemas + */ +export class GuardrailUsage extends Schema.Class(makeIdentifier("GuardrailUsage"))({ + contentPolicyUnits: Schema.Int, + contextualGroundingPolicyUnits: Schema.Int, + sensitiveInformationPolicyFreeUnits: Schema.Int, + sensitiveInformationPolicyUnits: Schema.Int, + topicPolicyUnits: Schema.Int, + wordPolicyUnits: Schema.Int, + contentPolicyImageUnits: Schema.optional(Schema.Int) +}) {} + +/** + * @since 1.0.0 + * @category Schemas + */ +export class GuardrailInvocationMetrics extends Schema.Class( + makeIdentifier("GuardrailInvocationMetrics") +)({ + guardrailCoverage: Schema.optional(GuardrailCoverage), + guardrailProcessingLatency: Schema.optional(Schema.Number), + usage: Schema.optional(GuardrailUsage) +}) {} + +/** + * @since 1.0.0 + * @category Schemas + */ +export class GuardrailPiiEntityFilter extends Schema.Class( + makeIdentifier("GuardrailPiiEntityFilter") +)({ + type: Schema.Literal( + "ADDRESS", + "AGE", + "AWS_ACCESS_KEY", + "AWS_SECRET_KEY", + "CA_HEALTH_NUMBER", + "CA_SOCIAL_INSURANCE_NUMBER", + "CREDIT_DEBIT_CARD_CVV", + "CREDIT_DEBIT_CARD_EXPIRY", + "CREDIT_DEBIT_CARD_NUMBER", + "DRIVER_ID", + "EMAIL", + "INTERNATIONAL_BANK_ACCOUNT_NUMBER", + "IP_ADDRESS", + "LICENSE_PLATE", + "MAC_ADDRESS", + "NAME", + "PASSWORD", + "PHONE", + "PIN", + "SWIFT_CODE", + "UK_NATIONAL_HEALTH_SERVICE_NUMBER", + "UK_NATIONAL_INSURANCE_NUMBER", + "UK_UNIQUE_TAXPAYER_REFERENCE_NUMBER", + "URL", + "USERNAME", + "US_BANK_ACCOUNT_NUMBER", + "US_BANK_ROUTING_NUMBER", + "US_INDIVIDUAL_TAX_IDENTIFICATION_NUMBER", + "US_PASSPORT_NUMBER", + "US_SOCIAL_SECURITY_NUMBER", + "VEHICLE_IDENTIFICATION_NUMBER" + ), + action: Schema.Literal("ANONYMIZED", "BLOCKED", "NONE"), + match: Schema.String, + detected: Schema.optional(Schema.Boolean) +}) {} + +/** + * @since 1.0.0 + * @category Schemas + */ +export class GuardrailRegexFilter extends Schema.Class( + makeIdentifier("GuardrailRegexFilter") +)({ + action: Schema.Literal("ANONYMIZED", "BLOCKED", "NONE"), + name: Schema.optional(Schema.String), + match: Schema.optional(Schema.String), + regex: Schema.optional(Schema.String), + detected: Schema.optional(Schema.Boolean) +}) {} + +/** + * @since 1.0.0 + * @category Schemas + */ +export class GuardrailSensitiveInformationPolicyAssessment + extends Schema.Class( + makeIdentifier("GuardrailSensitiveInformationPolicyAssessment") + )({ + piiEntities: Schema.Array(GuardrailPiiEntityFilter), + regexes: Schema.Array(GuardrailRegexFilter) + }) +{} + +/** + * @since 1.0.0 + * @category Schemas + */ +export class GuardrailTopic extends Schema.Class(makeIdentifier("GuardrailTopic"))({ + action: Schema.Literal("BLOCKED", "NONE"), + name: Schema.String, + type: Schema.Literal("DENY"), + detected: Schema.optional(Schema.Boolean) +}) {} + +/** + * @since 1.0.0 + * @category Schemas + */ +export class GuardrailTopicPolicyAssessment extends Schema.Class( + makeIdentifier("GuardrailTopicPolicyAssessment") +)({ + topics: Schema.Array(GuardrailTopic) +}) {} + +/** + * @since 1.0.0 + * @category Schemas + */ +export class GuardrailCustomWord extends Schema.Class( + makeIdentifier("GuardrailCustomWord") +)({ + action: Schema.Literal("BLOCKED", "NONE"), + match: Schema.String, + detected: Schema.optional(Schema.Boolean) +}) {} + +/** + * @since 1.0.0 + * @category Schemas + */ +export class GuardrailManagedWord extends Schema.Class( + makeIdentifier("GuardrailManagedWord") +)({ + action: Schema.Literal("BLOCKED", "NONE"), + match: Schema.String, + type: Schema.Literal("PROFANITY"), + detected: Schema.optional(Schema.Boolean) +}) {} + +/** + * @since 1.0.0 + * @category Schemas + */ +export class GuardrailWordPolicyAssessment extends Schema.Class( + makeIdentifier("GuardrailWordPolicyAssessment") +)({ + customWords: GuardrailCustomWord, + managedWordLists: GuardrailManagedWord +}) {} + +/** + * @since 1.0.0 + * @category Schemas + */ +export class GuardrailAssessment extends Schema.Class(makeIdentifier("GuardrailAssessment"))({ + contentPolicy: Schema.optional(GuardrailContentPolicyAssessment), + contextualGroundingPolicy: Schema.optional(GuardrailContextualGroundingPolicyAssessment), + invocationMetrics: Schema.optional(GuardrailInvocationMetrics), + sensitiveInformationPolicy: Schema.optional(GuardrailSensitiveInformationPolicyAssessment), + topicPolicy: Schema.optional(GuardrailTopicPolicyAssessment), + wordPolicy: Schema.optional(GuardrailWordPolicyAssessment) +}) {} + +/** + * @since 1.0.0 + * @category Schemas + */ +export class GuardrailTraceAssessment extends Schema.Class( + makeIdentifier("GuardrailTraceAssessment") +)({ + actionReason: Schema.optional(Schema.String), + inputAssessment: Schema.optional(Schema.Record({ + key: Schema.String, + value: GuardrailAssessment + })), + modelOutput: Schema.optional(Schema.Array(Schema.String)), + outputAssessments: Schema.optional(Schema.Record({ + key: Schema.String, + value: GuardrailAssessment + })) +}) {} + +/** + * @since 1.0.0 + * @category Schemas + */ +export class PromptRouterTrace extends Schema.Class(makeIdentifier("PromptRouterTrace"))({ + invokedModelId: Schema.String.pipe( + Schema.pattern( + /^(arn:aws(-[^:]+)?:bedrock:[a-z0-9-]{1,20}::foundation-model\/[a-z0-9-]{1,63}[.]{1}[a-z0-9-]{1,63}([a-z0-9-]{1,63}[.]){0,2}[a-z0-9-]{1,63}([:][a-z0-9-]{1,63}){0,2})|(arn:aws(|-us-gov|-cn|-iso|-iso-b):bedrock:(|[0-9a-z-]{1,20}):(|[0-9]{12}):inference-profile\/[a-zA-Z0-9-:.]+)$/ + ), + Schema.optional + ) +}) {} + +/** + * @since 1.0.0 + * @category Schemas + */ +export class ConverseTrace extends Schema.Class(makeIdentifier("ConverseTrace"))({ + guardrail: Schema.optional(GuardrailTraceAssessment), + promptRouter: Schema.optional(PromptRouterTrace) +}) {} + +/** + * @since 1.0.0 + * @category Schemas + */ +export const IntZeroOrGreater = Schema.Int.pipe(Schema.greaterThanOrEqualTo(0)) + +/** + * @since 1.0.0 + * @category Schemas + */ +export class TokenUsage extends Schema.Class(makeIdentifier("TokenUsage"))({ + inputTokens: IntZeroOrGreater, + outputTokens: IntZeroOrGreater, + totalTokens: IntZeroOrGreater, + cacheReadInputTokens: Schema.optional(IntZeroOrGreater), + cacheWriteInputTokens: Schema.optional(IntZeroOrGreater) +}) {} + +/** + * @since 1.0.0 + * @category Schemas + */ +export class SystemContentBlock extends Schema.Union( + Schema.Struct({ cachePoint: CachePointBlock }), + Schema.Struct({ guardContent: GuardrailConverseContentBlock }), + Schema.Struct({ text: Schema.String.pipe(Schema.minLength(1)) }) +) {} + +/** + * @since 1.0.0 + * @category Schemas + */ +export class GuardrailConfiguration extends Schema.Class( + makeIdentifier("GuardrailConfiguration") +)({ + guardrailIdentifier: Schema.String.pipe( + Schema.minLength(0), + Schema.maxLength(2048), + Schema.pattern(/^(([a-z0-9]+)|(arn:aws(-[^:]+)?:bedrock:[a-z0-9-]{1,20}:[0-9]{12}:guardrail\/[a-z0-9]+))$/) + ), + guardrailVersion: Schema.String.pipe( + Schema.pattern(/^(([1-9][0-9]{0,7})|(DRAFT))$/) + ), + trace: Schema.optional(Schema.Literal("enabled", "disabled", "enabled_full")) +}) {} + +/** + * @since 1.0.0 + * @category Schemas + */ +export class InferenceConfiguration extends Schema.Class( + makeIdentifier("InferenceConfiguration") +)({ + maxTokens: Schema.optional(Schema.Int.pipe(Schema.greaterThanOrEqualTo(1))), + stopSequences: Schema.optional( + Schema.Array(Schema.String.pipe( + Schema.minLength(1) + )).pipe( + Schema.maxItems(4) + ) + ), + temperature: Schema.optional(Schema.Number.pipe(Schema.between(0, 1))), + topP: Schema.optional(Schema.Number.pipe(Schema.between(0, 1))) +}) {} + +/** + * @since 1.0.0 + * @category Schemas + */ +export class PerformanceConfiguration extends Schema.Class( + makeIdentifier("PerformanceConfiguration") +)({ + latency: Schema.optional(Schema.Literal("standard", "optimized")) +}) {} + +/** + * @since 1.0.0 + * @category Schemas + */ +export class ToolSpecification extends Schema.Class( + makeIdentifier("ToolSpecification") +)({ + name: Schema.String.pipe( + Schema.minLength(1), + Schema.maxLength(64), + Schema.pattern(/^[a-zA-Z0-9_-]+$/) + ), + inputSchema: Schema.Struct({ + json: Schema.Record({ + key: Schema.String, + value: Schema.Unknown + }) + }), + description: Schema.optional(Schema.String.pipe( + Schema.minLength(1) + )) +}) {} + +/** + * @since 1.0.0 + * @category Schemas + */ +export class Tool extends Schema.Class( + makeIdentifier("Tool") +)({ + cachePoint: Schema.optional(CachePointBlock), + toolSpec: Schema.optional(ToolSpecification) +}) {} + +/** + * @since 1.0.0 + * @category Schemas + */ +export class ToolChoice extends Schema.Union( + Schema.Struct({ any: Schema.Struct({}) }), + Schema.Struct({ auto: Schema.Struct({}) }), + Schema.Struct({ + tool: Schema.Struct({ + name: Schema.String.pipe( + Schema.minLength(1), + Schema.maxLength(64), + Schema.pattern(/^[a-zA-Z0-9_-]+$/) + ) + }) + }) +) {} + +/** + * @since 1.0.0 + * @category Schemas + */ +export class ToolConfiguration extends Schema.Class( + makeIdentifier("ToolConfiguration") +)({ + tools: Schema.Array(Tool).pipe(Schema.minItems(1)), + toolChoice: Schema.optional(ToolChoice) +}) {} + +/** + * @since 1.0.0 + * @category Schemas + */ +export class ConverseRequest extends Schema.Class(makeIdentifier("ConverseRequest"))({ + modelId: Schema.String, + messages: Schema.Array(Message), + system: Schema.optional(Schema.Array(SystemContentBlock)), + toolConfig: Schema.optional(ToolConfiguration), + guardrailConfig: Schema.optional(GuardrailConfiguration), + inferenceConfig: Schema.optional(InferenceConfiguration), + performanceConfig: Schema.optional(PerformanceConfiguration), + promptVariables: Schema.optional(Schema.Record({ + key: Schema.String, + value: Schema.Struct({ text: Schema.String }) + })), + requestMetadata: Schema.optional(Schema.Record({ + key: Schema.String.pipe( + Schema.minLength(1), + Schema.maxLength(256), + Schema.pattern(/^[a-zA-Z0-9\s:_@$#=/+,-.]{1,256}$/) + ), + value: Schema.String.pipe( + Schema.minLength(0), + Schema.maxLength(256), + Schema.pattern(/^[a-zA-Z0-9\s:_@$#=/+,-.]{0,256}$/) + ) + })), + additionalModelRequestFields: Schema.optional(Schema.Record({ + key: Schema.String, + value: Schema.Unknown + })), + additionalModelResponseFieldPaths: Schema.optional( + Schema.Array(Schema.String.pipe( + Schema.minLength(1), + Schema.maxLength(256) + )).pipe( + Schema.maxItems(10) + ) + ) +}) {} + +/** + * @since 1.0.0 + * @category Schemas + */ +export class ConverseResponse extends Schema.Class(makeIdentifier("ConverseResponse"))({ + output: ConverseOutput, + metrics: ConverseMetrics, + usage: TokenUsage, + stopReason: Schema.Literal( + "content_filtered", + "end_turn", + "tool_use", + "max_tokens", + "stop_sequence", + "guardrail_intervened" + ), + trace: Schema.optional(ConverseTrace), + performanceConfig: Schema.optional(Schema.Struct({ + latency: Schema.Literal("standard", "optimized") + })), + additionalModelResponseFields: Schema.optional(Schema.Record({ + key: Schema.String, + value: Schema.Unknown + })) +}) {} + +/** + * @since 1.0.0 + * @category Schemas + */ +export class ReasoningContentBlockDelta extends Schema.Union( + Schema.Struct({ redactedContent: Schema.String }), + Schema.Struct({ signature: Schema.String }), + Schema.Struct({ text: Schema.String }) +) {} + +/** + * @since 1.0.0 + * @category Schemas + */ +export class ToolUseBlockStart extends Schema.Class( + makeIdentifier("ToolUseBlockStart") +)({ + name: Schema.String.pipe( + Schema.minLength(1), + Schema.maxLength(64), + Schema.pattern(/^[a-zA-Z0-9_-]+$/) + ), + toolUseId: Schema.String.pipe( + Schema.minLength(1), + Schema.maxLength(64), + Schema.pattern(/^[a-zA-Z0-9_-]+$/) + ) +}) {} + +/** + * @since 1.0.0 + * @category Schemas + */ +export class ContentBlockStart extends Schema.Class( + makeIdentifier("ContentBlockStart") +)({ + toolUse: Schema.optional(ToolUseBlockStart) +}) {} + +/** + * @since 1.0.0 + * @category Schemas + */ +export class ContentBlockStartEvent extends Schema.Class( + makeIdentifier("ContentBlockStartEvent") +)({ + contentBlockIndex: Schema.Int.pipe(Schema.greaterThanOrEqualTo(0)), + start: ContentBlockStart +}) {} + +/** + * @since 1.0.0 + * @category Schemas + */ +export class ContentBlockStopEvent extends Schema.Class( + makeIdentifier("ContentBlockStopEvent") +)({ + contentBlockIndex: Schema.Int.pipe(Schema.greaterThanOrEqualTo(0)) +}) {} + +/** + * @since 1.0.0 + * @category Schemas + */ +export class ToolUseBlockDelta extends Schema.Class( + makeIdentifier("ToolUseBlockDelta") +)({ + input: Schema.String +}) {} + +/** + * @since 1.0.0 + * @category Schemas + */ +export class ContentBlockDelta extends Schema.Union( + Schema.Struct({ reasoningContent: ReasoningContentBlockDelta }).pipe( + Schema.attachPropertySignature("type", "reasoningContent") + ), + Schema.Struct({ text: Schema.String }).pipe( + Schema.attachPropertySignature("type", "text") + ), + Schema.Struct({ toolUse: ToolUseBlockDelta }).pipe( + Schema.attachPropertySignature("type", "toolUse") + ) +) {} + +/** + * @since 1.0.0 + * @category Schemas + */ +export class ContentBlockDeltaEvent extends Schema.Class( + makeIdentifier("ContentBlockDeltaEvent") +)({ + contentBlockIndex: Schema.Int.pipe(Schema.greaterThanOrEqualTo(0)), + delta: ContentBlockDelta +}) {} + +/** + * @since 1.0.0 + * @category Schemas + */ +export class MessageStartEvent extends Schema.Class( + makeIdentifier("MessageStartEvent") +)({ + role: Schema.Literal("user", "assistant") +}) {} + +/** + * @since 1.0.0 + * @category Schemas + */ +export const StopReason = Schema.Literal( + "end_turn", + "tool_use", + "max_tokens", + "stop_sequence", + "guardrail_intervened", + "content_filtered" +) +/** + * @since 1.0.0 + * @category Schemas + */ +export type StopReason = typeof StopReason.Type + +/** + * @since 1.0.0 + * @category Schemas + */ +export class MessageStopEvent extends Schema.Class( + makeIdentifier("MessageStopEvent") +)({ + stopReason: StopReason, + additionalModelResponseFields: Schema.optional(Schema.Record({ + key: Schema.String, + value: Schema.Unknown + })) +}) {} + +/** + * @since 1.0.0 + * @category Schemas + */ +export class ConverseStreamMetrics extends Schema.Class( + makeIdentifier("ConverseStreamMetrics") +)({ + latencyMs: Schema.DurationFromMillis +}) {} + +/** + * @since 1.0.0 + * @category Schemas + */ +export class ConverseStreamTrace extends Schema.Class( + makeIdentifier("ConverseStreamTrace") +)({ + guardrail: Schema.optional(GuardrailTraceAssessment), + promptRouter: Schema.optional(PromptRouterTrace) +}) {} + +/** + * @since 1.0.0 + * @category Schemas + */ +export class ConverseStreamMetadataEvent extends Schema.Class( + makeIdentifier("ConverseStreamMetadataEvent") +)({ + metrics: ConverseStreamMetrics, + usage: TokenUsage, + performanceConfig: Schema.optional(PerformanceConfiguration), + trace: Schema.optional(ConverseStreamTrace) +}) {} + +/** + * @since 1.0.0 + * @category Schemas + */ +export const ConverseResponseStreamEvent = Schema.Union( + Schema.Struct({ messageStart: MessageStartEvent }).pipe( + Schema.attachPropertySignature("type", "messageStart"), + Schema.annotations({ identifier: "MessageStartEvent" }) + ), + Schema.Struct({ messageStop: MessageStopEvent }).pipe( + Schema.attachPropertySignature("type", "messageStop"), + Schema.annotations({ identifier: "MessageStopEvent" }) + ), + Schema.Struct({ contentBlockStart: ContentBlockStartEvent }).pipe( + Schema.attachPropertySignature("type", "contentBlockStart"), + Schema.annotations({ identifier: "ContentBlockStartEvent" }) + ), + Schema.Struct({ contentBlockDelta: ContentBlockDeltaEvent }).pipe( + Schema.attachPropertySignature("type", "contentBlockDelta"), + Schema.annotations({ identifier: "ContentBlockDeltaEvent" }) + ), + Schema.Struct({ contentBlockStop: ContentBlockStopEvent }).pipe( + Schema.attachPropertySignature("type", "contentBlockStop"), + Schema.annotations({ identifier: "ContentBlockDeltaEvent" }) + ), + Schema.Struct({ metadata: ConverseStreamMetadataEvent }).pipe( + Schema.attachPropertySignature("type", "metadata"), + Schema.annotations({ identifier: "ConverseStreamMetadataEvent" }) + ), + Schema.Struct({ + internalServerException: Schema.Record({ + key: Schema.String, + value: Schema.Unknown + }) + }).pipe( + Schema.attachPropertySignature("type", "internalServerException"), + Schema.annotations({ identifier: "InternalServerException" }) + ), + Schema.Struct({ + modelStreamErrorException: Schema.Record({ + key: Schema.String, + value: Schema.Unknown + }) + }).pipe( + Schema.attachPropertySignature("type", "modelStreamErrorException"), + Schema.annotations({ identifier: "ModelStreamErrorException" }) + ), + Schema.Struct({ + serviceUnavailableException: Schema.Record({ + key: Schema.String, + value: Schema.Unknown + }) + }).pipe( + Schema.attachPropertySignature("type", "serviceUnavailableException"), + Schema.annotations({ identifier: "ServiceUnavailableException" }) + ), + Schema.Struct({ + throttlingException: Schema.Record({ + key: Schema.String, + value: Schema.Unknown + }) + }).pipe( + Schema.attachPropertySignature("type", "throttlingException"), + Schema.annotations({ identifier: "ThrottlingException" }) + ), + Schema.Struct({ + validationException: Schema.Record({ + key: Schema.String, + value: Schema.Unknown + }) + }).pipe( + Schema.attachPropertySignature("type", "validationException"), + Schema.annotations({ identifier: "ValidationException" }) + ) +).pipe(Schema.asSchema).annotations({ identifier: "ConverseResponseStreamEvent" }) + +/** + * @since 1.0.0 + * @category Models + */ +export type ConverseResponseStreamEvent = typeof ConverseResponseStreamEvent.Type diff --git a/repos/effect/packages/ai/amazon-bedrock/src/AmazonBedrockTool.ts b/repos/effect/packages/ai/amazon-bedrock/src/AmazonBedrockTool.ts new file mode 100644 index 0000000..1b414eb --- /dev/null +++ b/repos/effect/packages/ai/amazon-bedrock/src/AmazonBedrockTool.ts @@ -0,0 +1,273 @@ +/** + * @since 1.0.0 + */ +import * as AnthropicTool from "@effect/ai-anthropic/AnthropicTool" +import type * as Generated from "@effect/ai-anthropic/Generated" +import type * as Tool from "@effect/ai/Tool" +import type * as Schema from "effect/Schema" + +/** + * @since 1.0.0 + * @catgory Tools + */ +export const AnthropicBash_20241022: ( + args: {} & { readonly failureMode?: Mode | undefined } +) => Tool.ProviderDefined< + "AnthropicBash", + { + readonly args: Schema.Struct<{}> + readonly parameters: Schema.Struct< + { + command: typeof Schema.NonEmptyString + restart: Schema.optional + } + > + readonly success: typeof Schema.String + readonly failure: typeof Schema.Never + readonly failureMode: Mode extends undefined ? "error" : Mode + }, + true +> = AnthropicTool.Bash_20241022 + +/** + * @since 1.0.0 + * @catgory Tools + */ +export const AnthropicBash_20250124: ( + args: {} & { readonly failureMode?: Mode | undefined } +) => Tool.ProviderDefined< + "AnthropicBash", + { + readonly args: Schema.Struct<{}> + readonly parameters: Schema.Struct< + { + command: typeof Schema.NonEmptyString + restart: Schema.optional + } + > + readonly success: typeof Schema.String + readonly failure: typeof Schema.Never + readonly failureMode: Mode extends undefined ? "error" : Mode + }, + true +> = AnthropicTool.Bash_20250124 + +/** + * @since 1.0.0 + * @catgory Tools + */ +export const AnthropicComputerUse_20241022: ( + args: { + readonly display_height_px: number + readonly display_width_px: number + readonly cache_control?: + | { + readonly type: "ephemeral" + readonly ttl?: "5m" | "1h" | null | undefined + } + | null + | undefined + readonly display_number?: number | null | undefined + readonly failureMode?: Mode | undefined + } +) => Tool.ProviderDefined< + "AnthropicComputerUse", + { + readonly args: Schema.Struct< + { + readonly cache_control: Schema.optionalWith + readonly display_height_px: Schema.filter + readonly display_number: Schema.optionalWith, { nullable: true }> + readonly display_width_px: Schema.filter + } + > + readonly parameters: Schema.Struct< + { + action: Schema.Literal<["screenshot", "left_click", "type", "key", "mouse_move"]> + coordinate: Schema.optional> + text: Schema.optional + } + > + readonly success: typeof Schema.String + readonly failure: typeof Schema.Never + readonly failureMode: Mode extends undefined ? "error" : Mode + }, + true +> = AnthropicTool.ComputerUse_20241022 + +/** + * @since 1.0.0 + * @catgory Tools + */ +export const AnthropicComputerUse_20250124: ( + args: { + readonly display_height_px: number + readonly display_width_px: number + readonly cache_control?: + | { + readonly type: "ephemeral" + readonly ttl?: "5m" | "1h" | null | undefined + } + | null + | undefined + readonly display_number?: number | null | undefined + readonly failureMode?: Mode | undefined + } +) => Tool.ProviderDefined< + "AnthropicComputerUse", + { + readonly args: Schema.Struct< + { + readonly cache_control: Schema.optionalWith + readonly display_height_px: Schema.filter + readonly display_number: Schema.optionalWith, { nullable: true }> + readonly display_width_px: Schema.filter + } + > + readonly parameters: Schema.Struct< + { + action: Schema.Literal< + [ + "screenshot", + "left_click", + "type", + "key", + "mouse_move", + "scroll", + "left_click_drag", + "middle_click", + "right_click", + "double_click", + "triple_click", + "left_mouse_down", + "left_mouse_up", + "hold_key", + "wait" + ] + > + coordinate: Schema.optional> + start_coordinate: Schema.optional> + text: Schema.optional + scroll_direction: Schema.optional> + scroll_amount: Schema.optional + duration: Schema.optional + } + > + readonly success: typeof Schema.String + readonly failure: typeof Schema.Never + readonly failureMode: Mode extends undefined ? "error" : Mode + }, + true +> = AnthropicTool.ComputerUse_20250124 + +/** + * @since 1.0.0 + * @catgory Tools + */ +export const AnthropicTextEditor_20241022: ( + args: { readonly failureMode?: Mode | undefined } +) => Tool.ProviderDefined< + "AnthropicTextEditor", + { + readonly args: Schema.Struct<{}> + readonly parameters: Schema.Struct< + { + command: Schema.Literal<["view", "create", "str_replace", "insert", "undo_edit"]> + path: typeof Schema.String + file_text: Schema.optional + insert_line: Schema.optional + new_str: Schema.optional + old_str: Schema.optional + view_range: Schema.optional> + } + > + readonly success: typeof Schema.Void + readonly failure: typeof Schema.Never + readonly failureMode: Mode extends undefined ? "error" : Mode + }, + true +> = AnthropicTool.TextEditor_20241022 + +/** + * @since 1.0.0 + * @catgory Tools + */ +export const AnthropicTextEditor_20250124: ( + args: { readonly failureMode?: Mode | undefined } +) => Tool.ProviderDefined< + "AnthropicTextEditor", + { + readonly args: Schema.Struct<{}> + readonly parameters: Schema.Struct< + { + command: Schema.Literal<["view", "create", "str_replace", "insert", "undo_edit"]> + path: typeof Schema.String + file_text: Schema.optional + insert_line: Schema.optional + new_str: Schema.optional + old_str: Schema.optional + view_range: Schema.optional> + } + > + readonly success: typeof Schema.Void + readonly failure: typeof Schema.Never + readonly failureMode: Mode extends undefined ? "error" : Mode + }, + true +> = AnthropicTool.TextEditor_20250124 + +/** + * @since 1.0.0 + * @catgory Tools + */ +export const AnthropicTextEditor_20250429: ( + args: { readonly failureMode?: Mode | undefined } +) => Tool.ProviderDefined< + "AnthropicTextEditor", + { + readonly args: Schema.Struct<{}> + readonly parameters: Schema.Struct< + { + command: Schema.Literal<["view", "create", "str_replace", "insert"]> + path: typeof Schema.String + file_text: Schema.optional + insert_line: Schema.optional + new_str: Schema.optional + old_str: Schema.optional + view_range: Schema.optional> + } + > + readonly success: typeof Schema.Void + readonly failure: typeof Schema.Never + readonly failureMode: Mode extends undefined ? "error" : Mode + }, + true +> = AnthropicTool.TextEditor_20250429 + +/** + * @since 1.0.0 + * @catgory Tools + */ +export const AnthropicTextEditor_20250728: ( + args: { readonly failureMode?: Mode | undefined } +) => Tool.ProviderDefined< + "AnthropicTextEditor", + { + readonly args: Schema.Struct<{}> + readonly parameters: Schema.Struct< + { + command: Schema.Literal<["view", "create", "str_replace", "insert"]> + path: typeof Schema.String + file_text: Schema.optional + insert_line: Schema.optional + new_str: Schema.optional + old_str: Schema.optional + view_range: Schema.optional> + } + > + readonly success: typeof Schema.Void + readonly failure: typeof Schema.Never + readonly failureMode: Mode extends undefined ? "error" : Mode + }, + true +> = AnthropicTool.TextEditor_20250728 diff --git a/repos/effect/packages/ai/amazon-bedrock/src/EventStreamEncoding.ts b/repos/effect/packages/ai/amazon-bedrock/src/EventStreamEncoding.ts new file mode 100644 index 0000000..4f403bd --- /dev/null +++ b/repos/effect/packages/ai/amazon-bedrock/src/EventStreamEncoding.ts @@ -0,0 +1,118 @@ +/** + * @since 1.0.0 + */ +import { EventStreamCodec } from "@smithy/eventstream-codec" +import { fromUtf8, toUtf8 } from "@smithy/util-utf8" +import * as Channel from "effect/Channel" +import type * as Chunk from "effect/Chunk" +import * as Effect from "effect/Effect" +import * as Mailbox from "effect/Mailbox" +import type { ParseError } from "effect/ParseResult" +import * as Schema from "effect/Schema" +import type * as AsyncInput from "effect/SingleProducerAsyncInput" + +/** + * An event stream encoding parser. + * + * See the [AWS Documentation](https://docs.aws.amazon.com/lexv2/latest/dg/event-stream-encoding.html) + * for more information. + * + * @since 1.0.0 + */ +export const makeChannel: (schema: Schema.Schema, options?: { + readonly bufferSize?: number +}) => Channel.Channel< + Chunk.Chunk, + Chunk.Chunk>, + IE | ParseError, + IE, + void, + Done, + R +> = Effect.fnUntraced( + function*(schema: Schema.Schema, options?: { + readonly bufferSize?: number + }) { + const context = yield* Effect.context() + + const mailbox = yield* Mailbox.make(options?.bufferSize ?? 16) + + const codec = new EventStreamCodec(toUtf8, fromUtf8) + const decodeMessage = Schema.decodeUnknown(schema, { + onExcessProperty: "ignore" + }) + const textDecoder = new TextDecoder() + + let messages: Array = [] + let buffer = new Uint8Array(0) + + const input: AsyncInput.AsyncInputProducer< + IE | ParseError, + Chunk.Chunk, + Done + > = { + awaitRead() { + return Effect.void + }, + emit(chunks) { + return Effect.forEach( + chunks, + Effect.fnUntraced(function*(chunk) { + // Append new chunk to buffer + const newBuffer = new Uint8Array(buffer.length + chunk.length) + newBuffer.set(buffer) + newBuffer.set(chunk, buffer.length) + buffer = newBuffer + + // Try to decode messages from the buffer + while (buffer.length >= 4) { + // The first four bytes are the total length of the message (big-endian) + const totalLength = new DataView( + buffer.buffer, + buffer.byteOffset, + buffer.byteLength + ).getUint32(0, false) + + // If we don't have the full message yet, keep looping + if (buffer.length < totalLength) { + break + } + + // Decode exactly the sub-slice for this event + const subView = buffer.subarray(0, totalLength) + const decoded = codec.decode(subView) + + // Slice the used bytes off the buffer, removing this message + buffer = buffer.slice(totalLength) + + // Process the message + if (decoded.headers[":message-type"]?.value === "event") { + const data = textDecoder.decode(decoded.body) + + // Wrap the data in the `":event-type"` field to match the + // expected schema + const message = yield* decodeMessage({ + [decoded.headers[":event-type"]?.value as string]: JSON.parse(data) + }).pipe(Effect.provide(context)) + + messages.push(message) + } + } + yield* mailbox.offerAll(messages) + messages = [] + }), + { discard: true } + ).pipe(Effect.catchAll((error) => mailbox.fail(error))) + }, + error(cause) { + return mailbox.failCause(cause) + }, + done() { + return mailbox.end + } + } + + return Channel.embedInput(Mailbox.toChannel(mailbox), input) + }, + Channel.unwrap +) as any diff --git a/repos/effect/packages/ai/amazon-bedrock/src/index.ts b/repos/effect/packages/ai/amazon-bedrock/src/index.ts new file mode 100644 index 0000000..1a27e84 --- /dev/null +++ b/repos/effect/packages/ai/amazon-bedrock/src/index.ts @@ -0,0 +1,29 @@ +/** + * @since 1.0.0 + */ +export * as AmazonBedrockClient from "./AmazonBedrockClient.js" + +/** + * @since 1.0.0 + */ +export * as AmazonBedrockConfig from "./AmazonBedrockConfig.js" + +/** + * @since 1.0.0 + */ +export * as AmazonBedrockLanguageModel from "./AmazonBedrockLanguageModel.js" + +/** + * @since 1.0.0 + */ +export * as AmazonBedrockSchema from "./AmazonBedrockSchema.js" + +/** + * @since 1.0.0 + */ +export * as AmazonBedrockTool from "./AmazonBedrockTool.js" + +/** + * @since 1.0.0 + */ +export * as EventStreamEncoding from "./EventStreamEncoding.js" diff --git a/repos/effect/packages/ai/amazon-bedrock/src/internal/utilities.ts b/repos/effect/packages/ai/amazon-bedrock/src/internal/utilities.ts new file mode 100644 index 0000000..d3fb2d6 --- /dev/null +++ b/repos/effect/packages/ai/amazon-bedrock/src/internal/utilities.ts @@ -0,0 +1,24 @@ +import type * as Response from "@effect/ai/Response" +import * as Predicate from "effect/Predicate" +import type { StopReason } from "../AmazonBedrockSchema.js" + +/** @internal */ +export const ProviderOptionsKey = "@effect/ai-amazon-bedrock/AmazonBedrockLanguageModel/ProviderOptions" + +/** @internal */ +export const ProviderMetadataKey = "@effect/ai-amazon-bedrock/AmazonBedrockLanguageModel/ProviderMetadata" + +const finishReasonMap: Record = { + content_filtered: "content-filter", + end_turn: "stop", + guardrail_intervened: "content-filter", + max_tokens: "length", + stop_sequence: "stop", + tool_use: "tool-calls" +} + +/** @internal */ +export const resolveFinishReason = (stopReason: StopReason): Response.FinishReason => { + const reason = finishReasonMap[stopReason] + return Predicate.isUndefined(reason) ? "unknown" : reason +} diff --git a/repos/effect/packages/ai/amazon-bedrock/tsconfig.build.json b/repos/effect/packages/ai/amazon-bedrock/tsconfig.build.json new file mode 100644 index 0000000..b998a9c --- /dev/null +++ b/repos/effect/packages/ai/amazon-bedrock/tsconfig.build.json @@ -0,0 +1,17 @@ +{ + "extends": "./tsconfig.src.json", + "references": [ + { "path": "../ai/tsconfig.build.json" }, + { "path": "../anthropic/tsconfig.build.json" }, + { "path": "../../effect/tsconfig.build.json" }, + { "path": "../../experimental/tsconfig.build.json" }, + { "path": "../../platform/tsconfig.build.json" } + ], + "compilerOptions": { + "tsBuildInfoFile": ".tsbuildinfo/build.tsbuildinfo", + "outDir": "build/esm", + "declarationDir": "build/dts", + "stripInternal": true, + "exactOptionalPropertyTypes": false + } +} diff --git a/repos/effect/packages/ai/amazon-bedrock/tsconfig.json b/repos/effect/packages/ai/amazon-bedrock/tsconfig.json new file mode 100644 index 0000000..f446496 --- /dev/null +++ b/repos/effect/packages/ai/amazon-bedrock/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../../tsconfig.base.json", + "include": [], + "references": [ + { "path": "tsconfig.src.json" }, + { "path": "tsconfig.test.json" } + ] +} diff --git a/repos/effect/packages/ai/amazon-bedrock/tsconfig.src.json b/repos/effect/packages/ai/amazon-bedrock/tsconfig.src.json new file mode 100644 index 0000000..931123c --- /dev/null +++ b/repos/effect/packages/ai/amazon-bedrock/tsconfig.src.json @@ -0,0 +1,17 @@ +{ + "extends": "../../../tsconfig.base.json", + "include": ["src"], + "references": [ + { "path": "../ai/tsconfig.src.json" }, + { "path": "../anthropic/tsconfig.src.json" }, + { "path": "../../effect/tsconfig.src.json" }, + { "path": "../../experimental/tsconfig.src.json" }, + { "path": "../../platform/tsconfig.src.json" } + ], + "compilerOptions": { + "tsBuildInfoFile": ".tsbuildinfo/src.tsbuildinfo", + "rootDir": "src", + "outDir": "build/src", + "exactOptionalPropertyTypes": false + } +} diff --git a/repos/effect/packages/ai/amazon-bedrock/tsconfig.test.json b/repos/effect/packages/ai/amazon-bedrock/tsconfig.test.json new file mode 100644 index 0000000..95f3ae8 --- /dev/null +++ b/repos/effect/packages/ai/amazon-bedrock/tsconfig.test.json @@ -0,0 +1,14 @@ +{ + "extends": "../../../tsconfig.base.json", + "include": ["test"], + "references": [ + { "path": "tsconfig.src.json" }, + { "path": "../../vitest/tsconfig.src.json" } + ], + "compilerOptions": { + "tsBuildInfoFile": ".tsbuildinfo/test.tsbuildinfo", + "rootDir": "test", + "noEmit": true, + "exactOptionalPropertyTypes": false + } +} diff --git a/repos/effect/packages/ai/amazon-bedrock/vitest.config.ts b/repos/effect/packages/ai/amazon-bedrock/vitest.config.ts new file mode 100644 index 0000000..bf29895 --- /dev/null +++ b/repos/effect/packages/ai/amazon-bedrock/vitest.config.ts @@ -0,0 +1,6 @@ +import { mergeConfig, type ViteUserConfig } from "vitest/config" +import shared from "../../../vitest.shared.js" + +const config: ViteUserConfig = {} + +export default mergeConfig(shared, config) diff --git a/repos/effect/packages/ai/anthropic/CHANGELOG.md b/repos/effect/packages/ai/anthropic/CHANGELOG.md new file mode 100644 index 0000000..4e3a897 --- /dev/null +++ b/repos/effect/packages/ai/anthropic/CHANGELOG.md @@ -0,0 +1,1615 @@ +# @effect/ai-anthropic + +## 0.25.0 + +### Patch Changes + +- Updated dependencies [[`f7bb09b`](https://github.com/Effect-TS/effect/commit/f7bb09b022f195d1f2b3c23d49e74b011ec5d109), [`bd7552a`](https://github.com/Effect-TS/effect/commit/bd7552a19cc0ed575507ac6cc0879a57e24ebd31), [`ad1a7eb`](https://github.com/Effect-TS/effect/commit/ad1a7eb7f6bebaf91c80be2443ac0439226d0098), [`0d32048`](https://github.com/Effect-TS/effect/commit/0d32048f9836e2b23a6ba3ec5f43f0a000bb92fb), [`0d32048`](https://github.com/Effect-TS/effect/commit/0d32048f9836e2b23a6ba3ec5f43f0a000bb92fb)]: + - effect@3.21.0 + - @effect/ai@0.35.0 + - @effect/experimental@0.60.0 + - @effect/platform@0.96.0 + +## 0.24.0 + +### Patch Changes + +- Updated dependencies [[`fc82e81`](https://github.com/Effect-TS/effect/commit/fc82e81448bd9136a37580139ce46a2c61b11b54), [`82996bc`](https://github.com/Effect-TS/effect/commit/82996bce8debffcb44feb98bb862cf2662bd56b7), [`4d97a61`](https://github.com/Effect-TS/effect/commit/4d97a61a15b9dd6a0eece65b8f0c035e16d42ada), [`f6b0960`](https://github.com/Effect-TS/effect/commit/f6b0960bf3184109920dfed16ee7dfd7d67bc0f2), [`8798a84`](https://github.com/Effect-TS/effect/commit/8798a843218e6c0c0d3a8eee83360880e370b4da)]: + - effect@3.20.0 + - @effect/ai@0.34.0 + - @effect/experimental@0.59.0 + - @effect/platform@0.95.0 + +## 0.23.0 + +### Patch Changes + +- Updated dependencies [[`77eeb86`](https://github.com/Effect-TS/effect/commit/77eeb86ddf208e51ec25932af83d52d3b4700371), [`ff7053f`](https://github.com/Effect-TS/effect/commit/ff7053f6d8508567b6145239f97aacc5773b0c53), [`287c32c`](https://github.com/Effect-TS/effect/commit/287c32c9f10da8e96f2b9ef8424316189d9ad4b3)]: + - effect@3.19.13 + - @effect/platform@0.94.0 + - @effect/ai@0.33.0 + - @effect/experimental@0.58.0 + +## 0.22.0 + +### Patch Changes + +- Updated dependencies [[`3c15d5f`](https://github.com/Effect-TS/effect/commit/3c15d5f99fb8d8470a00c5a33d9ba3cac89dfe4c), [`3863fa8`](https://github.com/Effect-TS/effect/commit/3863fa89f61e63e5529fd961e37333bddf7db64a), [`2a03c76`](https://github.com/Effect-TS/effect/commit/2a03c76c2781ca7e9e228e838eab2eb0d0795b1d), [`24a1685`](https://github.com/Effect-TS/effect/commit/24a1685c70a9ed157468650f95a5c3da3f2c2433)]: + - effect@3.19.0 + - @effect/platform@0.93.0 + - @effect/ai@0.32.0 + - @effect/experimental@0.57.0 + +## 0.21.1 + +### Patch Changes + +- [#5644](https://github.com/Effect-TS/effect/pull/5644) [`7de0bfc`](https://github.com/Effect-TS/effect/commit/7de0bfc57f9a1d69c342224ab26402752677efcb) Thanks @IMax153! - Ensure that model accepts a string in Anthropic request schemas + +## 0.21.0 + +### Minor Changes + +- [#5621](https://github.com/Effect-TS/effect/pull/5621) [`4c3bdfb`](https://github.com/Effect-TS/effect/commit/4c3bdfbcbc2dcd7ecd6321df3e4a504af19de825) Thanks @IMax153! - Remove `Either` / `EitherEncoded` from tool call results. + + Specifically, the encoding of tool call results as an `Either` / `EitherEncoded` has been removed and is replaced by encoding the tool call success / failure directly into the `result` property. + + To allow type-safe discrimination between a tool call result which was a success vs. one that was a failure, an `isFailure` property has also been added to the `"tool-result"` part. If `isFailure` is `true`, then the tool call handler result was an error. + + ```ts + import * as AnthropicClient from "@effect/ai-anthropic/AnthropicClient" + import * as AnthropicLanguageModel from "@effect/ai-anthropic/AnthropicLanguageModel" + import * as LanguageModel from "@effect/ai/LanguageModel" + import * as Tool from "@effect/ai/Tool" + import * as Toolkit from "@effect/ai/Toolkit" + import * as NodeHttpClient from "@effect/platform-node/NodeHttpClient" + import { Config, Effect, Layer, Schema, Stream } from "effect" + + const Claude = AnthropicLanguageModel.model("claude-4-sonnet-20250514") + + const MyTool = Tool.make("MyTool", { + description: "An example of a tool with success and failure types", + failureMode: "return", // Return errors in the response + parameters: { bar: Schema.Number }, + success: Schema.Number, + failure: Schema.Struct({ reason: Schema.Literal("reason-1", "reason-2") }) + }) + + const MyToolkit = Toolkit.make(MyTool) + + const MyToolkitLayer = MyToolkit.toLayer({ + MyTool: () => Effect.succeed(42) + }) + + const program = LanguageModel.streamText({ + prompt: "Tell me about the meaning of life", + toolkit: MyToolkit + }).pipe( + Stream.runForEach((part) => { + if (part.type === "tool-result" && part.name === "MyTool") { + // The `isFailure` property can be used to discriminate whether the result + // of a tool call is a success or a failure + if (part.isFailure) { + part.result + // ^? { readonly reason: "reason-1" | "reason-2"; } + } else { + part.result + // ^? number + } + } + return Effect.void + }), + Effect.provide(Claude) + ) + + const Anthropic = AnthropicClient.layerConfig({ + apiKey: Config.redacted("ANTHROPIC_API_KEY") + }).pipe(Layer.provide(NodeHttpClient.layerUndici)) + + program.pipe(Effect.provide([Anthropic, MyToolkitLayer]), Effect.runPromise) + ``` + +### Patch Changes + +- Updated dependencies [[`4c3bdfb`](https://github.com/Effect-TS/effect/commit/4c3bdfbcbc2dcd7ecd6321df3e4a504af19de825)]: + - @effect/ai@0.31.0 + +## 0.20.0 + +### Minor Changes + +- [#5614](https://github.com/Effect-TS/effect/pull/5614) [`c63e658`](https://github.com/Effect-TS/effect/commit/c63e6582244fbb50d31650c4b4ea0660fe194652) Thanks @IMax153! - Previously, tool call handler errors were _always_ raised as an expected error in the Effect `E` channel at the point of execution of the tool call handler (i.e. when a `generate*` method is invoked on a `LanguageModel`). + + With this PR, the end user now has control over whether tool call handler errors should be raised as an Effect error, or returned by the SDK to allow, for example, sending that error information to another application. + + ### Tool Call Specification + + The `Tool.make` and `Tool.providerDefined` constructors now take an extra optional parameter called `failureMode`, which can be set to either `"error"` or `"return"`. + + ```ts + import { Tool } from "@effect/ai" + import { Schema } from "effect" + + const MyTool = Tool.make("MyTool", { + description: "My special tool", + failureMode: "return" // "error" (default) or "return" + parameters: { + myParam: Schema.String + }, + success: Schema.Struct({ + mySuccess: Schema.String + }), + failure: Schema.Struct({ + myFailure: Schema.String + }) + }) + + ``` + + The semantics of `failureMode` are as follows: + - If set to `"error"` (the default), errors that occur during tool call handler execution will be returned in the error channel of the calling effect + - If set to `"return"`, errors that occur during tool call handler execution will be captured and returned as part of the tool call result + + ### Response - Tool Result Parts + + The `result` field of a `"tool-result"` part of a large language model provider response is now represented as an `Either`. + - If the `result` is a `Left`, the `result` will be the `failure` specified in the tool call specification + - If the `result` is a `Right`, the `result` will be the `success` specified in the tool call specification + + This is only relevant if the end user sets `failureMode` to `"return"`. If set to `"error"` (the default), then the `result` property will always be a `Right` with the successful result of the tool call handler. + + Similarly the `encodedResult` field of a `"tool-result"` part will be represented as an `EitherEncoded`, where: + - `{ _tag: "Left", left: }` represents a tool call handler failure + - `{ _tag: "Right", right: }` represents a tool call handler success + + ### Prompt - Tool Result Parts + + The `result` field of a `"tool-result"` part of a prompt will now only accept an `EitherEncoded` as specified above. + +### Patch Changes + +- [#5615](https://github.com/Effect-TS/effect/pull/5615) [`1d2e92d`](https://github.com/Effect-TS/effect/commit/1d2e92de9a20f39765bd0b338ffc936ba2fd9463) Thanks @janglad! - Remove accidental commit of debug console.dir + +- Updated dependencies [[`6ae2f5d`](https://github.com/Effect-TS/effect/commit/6ae2f5da45a9ed9832605eca12b3e2bf2e2a1a67), [`c63e658`](https://github.com/Effect-TS/effect/commit/c63e6582244fbb50d31650c4b4ea0660fe194652)]: + - effect@3.18.4 + - @effect/ai@0.30.0 + +## 0.19.2 + +### Patch Changes + +- [#5608](https://github.com/Effect-TS/effect/pull/5608) [`215ed46`](https://github.com/Effect-TS/effect/commit/215ed4642b0c991d47e86fabb62f2118bf5f0231) Thanks @IMax153! - Fix incorrect detection of either result + +- Updated dependencies [[`8ba4757`](https://github.com/Effect-TS/effect/commit/8ba47576c75b8b91be4bf9c1dae13995b37018af)]: + - effect@3.18.2 + +## 0.19.1 + +### Patch Changes + +- [#5599](https://github.com/Effect-TS/effect/pull/5599) [`d7eba97`](https://github.com/Effect-TS/effect/commit/d7eba977288f0a97a1dac5cadb1f16253220b82a) Thanks @IMax153! - Fix provider defined tool results in prompt input + +## 0.19.0 + +### Patch Changes + +- Updated dependencies [[`1c6ab74`](https://github.com/Effect-TS/effect/commit/1c6ab74b314b2b6df8bb1b1a0cb9527ceda0e3fa), [`70fe803`](https://github.com/Effect-TS/effect/commit/70fe803469db3355ffbf8359b52c351f1c2dc137), [`c296e32`](https://github.com/Effect-TS/effect/commit/c296e32554143b84ae8987046984e1cf1852417c), [`a098ddf`](https://github.com/Effect-TS/effect/commit/a098ddfc551f5aa0a7c36f9b4928372a64d4d9f2), [`f8b93ac`](https://github.com/Effect-TS/effect/commit/f8b93ac6446efd3dd790778b0fc71d299a38f272)]: + - effect@3.18.0 + - @effect/ai@0.29.0 + - @effect/platform@0.92.0 + - @effect/experimental@0.56.0 + +## 0.18.2 + +### Patch Changes + +- [#5571](https://github.com/Effect-TS/effect/pull/5571) [`122aa53`](https://github.com/Effect-TS/effect/commit/122aa53058ff008cf605cc2f0f0675a946c3cae9) Thanks @IMax153! - Ensure that AI provider clients filter response status for stream requests + +## 0.18.1 + +### Patch Changes + +- [#5554](https://github.com/Effect-TS/effect/pull/5554) [`800ab2e`](https://github.com/Effect-TS/effect/commit/800ab2e6d983ed424deb10aebee720cfc666df7a) Thanks @IMax153! - Improve the information available to the user following a model response error + +- Updated dependencies [[`800ab2e`](https://github.com/Effect-TS/effect/commit/800ab2e6d983ed424deb10aebee720cfc666df7a), [`800ab2e`](https://github.com/Effect-TS/effect/commit/800ab2e6d983ed424deb10aebee720cfc666df7a), [`800ab2e`](https://github.com/Effect-TS/effect/commit/800ab2e6d983ed424deb10aebee720cfc666df7a), [`800ab2e`](https://github.com/Effect-TS/effect/commit/800ab2e6d983ed424deb10aebee720cfc666df7a), [`800ab2e`](https://github.com/Effect-TS/effect/commit/800ab2e6d983ed424deb10aebee720cfc666df7a), [`800ab2e`](https://github.com/Effect-TS/effect/commit/800ab2e6d983ed424deb10aebee720cfc666df7a)]: + - @effect/ai@0.28.2 + +## 0.18.0 + +### Patch Changes + +- Updated dependencies [[`d4d86a8`](https://github.com/Effect-TS/effect/commit/d4d86a81f02b94e09fce8004ce2c5369c505ca5a)]: + - @effect/platform@0.91.0 + - @effect/ai@0.28.0 + - @effect/experimental@0.55.0 + +## 0.17.1 + +### Patch Changes + +- [#5521](https://github.com/Effect-TS/effect/pull/5521) [`fa49bc8`](https://github.com/Effect-TS/effect/commit/fa49bc86b14599300d106f306ceaf82a79121b80) Thanks @IMax153! - Fix provider metadata and parse tool call parameters safely + +- Updated dependencies [[`fa49bc8`](https://github.com/Effect-TS/effect/commit/fa49bc86b14599300d106f306ceaf82a79121b80)]: + - @effect/ai@0.27.1 + +## 0.17.0 + +### Minor Changes + +- [#5469](https://github.com/Effect-TS/effect/pull/5469) [`42b914a`](https://github.com/Effect-TS/effect/commit/42b914a0e8750350ce17d434afaec7d655ddf4b7) Thanks @IMax153! - Refactor the Effect AI SDK and associated provider packages + + This pull request contains a complete refactor of the base Effect AI SDK package + as well as the associated provider integration packages to improve flexibility + and enhance ergonomics. Major changes are outlined below. + + ## Modules + + All modules in the base Effect AI SDK have had the leading `Ai` prefix dropped + from their name (except for the `AiError` module). + + For example, the `AiLanguageModel` module is now the `LanguageModel` module. + + In addition, the `AiInput` module has been renamed to the `Prompt` module. + + ## Prompts + + The `Prompt` module has been completely redesigned with flexibility in mind. + + The `Prompt` module now supports building a prompt using either the constructors + exposed from the `Prompt` module, or using raw prompt content parts / messages, + which should be familiar to those coming from other AI SDKs. + + In addition, the `system` option has been removed from all `LanguageModel` methods + and must now be provided as part of the prompt. + + **Prompt Constructors** + + ```ts + import { LanguageModel, Prompt } from "@effect/ai" + + const textPart = Prompt.makePart("text", { + text: "What is machine learning?" + }) + + const userMessage = Prompt.makeMessage("user", { + content: [textPart] + }) + + const systemMessage = Prompt.makeMessage("system", { + content: "You are an expert in machine learning" + }) + + const program = LanguageModel.generateText({ + prompt: Prompt.fromMessages([systemMessage, userMessage]) + }) + ``` + + **Raw Prompt Input** + + ```ts + import { LanguageModel } from "@effect/ai" + + const program = LanguageModel.generateText({ + prompt: [ + { role: "system", content: "You are an expert in machine learning" }, + { + role: "user", + content: [{ type: "text", text: "What is machine learning?" }] + } + ] + }) + ``` + + **NOTE**: Providing a plain string as a prompt is still supported, and will be converted + internally into a user message with a single text content part. + + ### Provider-Specific Options + + To support specification of provider-specific options when interacting with large + language model providers, support has been added for adding provider-specific + options to the parts of a `Prompt`. + + ```ts + import { LanguageModel } from "@effect/ai" + import { AnthropicLanguageModel } from "@effect/ai-anthropic" + + const Claude = AnthropicLanguageModel.model("claude-sonnet-4-20250514") + + const program = LanguageModel.generateText({ + prompt: [ + { + role: "user", + content: [{ type: "text", text: "What is machine learning?" }], + options: { + anthropic: { cacheControl: { type: "ephemeral", ttl: "1h" } } + } + } + ] + }).pipe(Effect.provide(Claude)) + ``` + + ## Responses + + The `Response` module has also been completely redesigned to support a wider + variety of response parts, particularly when streaming. + + ### Streaming Responses + + When streaming text via the `LanguageModel.streamText` method, you will now + receive a stream of content parts instead of a stream of responses, which should + make it much simpler to filter down the stream to the parts you are interested in. + + In addition, additional content parts will be present in the stream to allow you to track, + for example, when a text content part starts / ends. + + ### Tool Calls / Tool Call Results + + The decoded parts of a `Response` (as returned by the methods of `LanguageModel`) + are now fully type-safe on tool calls / tool call results. Filtering the content parts of a + response to tool calls will narrow the type of the tool call `params` based on the tool + `name`. Similarly, filtering the response to tool call results will narrow the type of the + tool call `result` based on the tool `name`. + + ```ts + import { LanguageModel, Tool, Toolkit } from "@effect/ai" + import { Effect, Schema } from "effect" + + const DadJokeTool = Tool.make("DadJokeTool", { + parameters: { topic: Schema.String }, + success: Schema.Struct({ joke: Schema.String }) + }) + + const FooTool = Tool.make("FooTool", { + parameters: { foo: Schema.Number }, + success: Schema.Struct({ bar: Schema.Boolean }) + }) + + const MyToolkit = Toolkit.make(DadJokeTool, FooTool) + + const program = Effect.gen(function* () { + const response = yield* LanguageModel.generateText({ + prompt: "Tell me a dad joke", + toolkit: MyToolkit + }) + + for (const toolCall of response.toolCalls) { + if (toolCall.name === "DadJokeTool") { + // ^? "DadJokeTool" | "FooTool" + toolCall.params + // ^? { readonly topic: string } + } + } + + for (const toolResult of response.toolResults) { + if (toolResult.name === "DadJokeTool") { + // ^? "DadJokeTool" | "FooTool" + toolResult.result + // ^? { readonly joke: string } + } + } + }) + ``` + + ### Provider Metadata + + As with provider-specific options, provider-specific metadata is now returned as + part of the response from the large language model provider. + + ```ts + import { LanguageModel } from "@effect/ai" + import { AnthropicLanguageModel } from "@effect/ai-anthropic" + import { Effect } from "effect" + + const Claude = AnthropicLanguageModel.model("claude-4-sonnet-20250514") + + const program = Effect.gen(function* () { + const response = yield* LanguageModel.generateText({ + prompt: "What is the meaning of life?" + }) + + for (const part of response.content) { + // When metadata **is not** defined for a content part, accessing the + // provider's key on the part's metadata will return an untyped record + if (part.type === "text") { + const metadata = part.metadata.anthropic + // ^? { readonly [x: string]: unknown } | undefined + } + // When metadata **is** defined for a content part, accessing the + // provider's key on the part's metadata will return typed metadata + if (part.type === "reasoning") { + const metadata = part.metadata.anthropic + // ^? AnthropicReasoningInfo | undefined + } + } + }).pipe(Effect.provide(Claude)) + ``` + + ## Tool Calls + + The `Tool` module has been enhanced to support provider-defined tools (e.g. + web search, computer use, etc.). Large language model providers which support + calling their own tools now have a separate module present in their provider + integration packages which contain definitions for their tools. + + These provider-defined tools can be included alongside user-defined tools in + existing `Toolkit`s. Provider-defined tools that require a user-space handler + will be raise a type error in the associated `Toolkit` layer if no such handler + is defined. + + ```ts + import { LanguageModel, Tool, Toolkit } from "@effect/ai" + import { AnthropicTool } from "@effect/ai-anthropic" + import { Schema } from "effect" + + const DadJokeTool = Tool.make("DadJokeTool", { + parameters: { topic: Schema.String }, + success: Schema.Struct({ joke: Schema.String }) + }) + + const MyToolkit = Toolkit.make( + DadJokeTool, + AnthropicTool.WebSearch_20250305({ max_uses: 1 }) + ) + + const program = LanguageModel.generateText({ + prompt: "Search the web for a dad joke", + toolkit: MyToolkit + }) + ``` + + ## AiError + + The `AiError` type has been refactored into a union of different error types + which can be raised by the Effect AI SDK. The goal of defining separate error + types is to allow providing the end-user with more granular information about + the error that occurred. + + For now, the following errors have been defined. More error types may be added + over time based upon necessity / use case. + + ```ts + type AiError = + | HttpRequestError, + | HttpResponseError, + | MalformedInput, + | MalformedOutput, + | UnknownError + ``` + +### Patch Changes + +- Updated dependencies [[`42b914a`](https://github.com/Effect-TS/effect/commit/42b914a0e8750350ce17d434afaec7d655ddf4b7)]: + - @effect/ai@0.27.0 + +## 0.16.2 + +### Patch Changes + +- [#5476](https://github.com/Effect-TS/effect/pull/5476) [`18ec398`](https://github.com/Effect-TS/effect/commit/18ec39853b493795fd0bff01a67f36e142cb6f4e) Thanks @richburdon! - fix total token count + +## 0.16.1 + +### Patch Changes + +- [#5474](https://github.com/Effect-TS/effect/pull/5474) [`5f5ae17`](https://github.com/Effect-TS/effect/commit/5f5ae1730510a372f229426aff832ba1c5c5145b) Thanks @IMax153! - Ensure that the finish part is emitted when streaming text from Anthropic + +## 0.16.0 + +### Patch Changes + +- Updated dependencies []: + - @effect/ai@0.26.0 + - @effect/experimental@0.54.6 + +## 0.15.1 + +### Patch Changes + +- [`4bcf799`](https://github.com/Effect-TS/effect/commit/4bcf799275bfc38932c5c5c5947afc271a283fac) Thanks @dmaretskyi! - Fix tools with no parameters not being called + +## 0.15.0 + +### Patch Changes + +- Updated dependencies [[`5a0f4f1`](https://github.com/Effect-TS/effect/commit/5a0f4f176687a39d9fa46bb894bb7ac3175b0e87)]: + - effect@3.17.1 + - @effect/ai@0.25.0 + - @effect/experimental@0.54.0 + +## 0.14.0 + +### Patch Changes + +- Updated dependencies [[`7813640`](https://github.com/Effect-TS/effect/commit/7813640279d9e3a3e7fc0a29bfb5c6d5fb3c270f)]: + - @effect/platform@0.90.0 + - @effect/ai@0.24.0 + - @effect/experimental@0.54.0 + +## 0.13.0 + +### Patch Changes + +- Updated dependencies [[`40c3c87`](https://github.com/Effect-TS/effect/commit/40c3c875f724264312b43002859c82bed9ad0df9), [`ed2c74a`](https://github.com/Effect-TS/effect/commit/ed2c74ae8fa4ea0dd06ea84a3e58cd32e6916104), [`073a1b8`](https://github.com/Effect-TS/effect/commit/073a1b8be5dbfa87454393ee7346f5bc36a4fd63), [`f382e99`](https://github.com/Effect-TS/effect/commit/f382e99e409838a879246250fc3994b9bf5b3c2c), [`e8c7ba5`](https://github.com/Effect-TS/effect/commit/e8c7ba5fd3eb0c3ae3039fc24c09d69391987989), [`7e10415`](https://github.com/Effect-TS/effect/commit/7e1041599ade25103428703f5d2dfd7378a09636), [`e9bdece`](https://github.com/Effect-TS/effect/commit/e9bdececdc24f60a246be5055eca71a0d49ea7f2), [`8d95eb0`](https://github.com/Effect-TS/effect/commit/8d95eb0356b1d1736204836c275d201a547d208d)]: + - effect@3.17.0 + - @effect/ai@0.23.0 + - @effect/experimental@0.53.0 + - @effect/platform@0.89.0 + +## 0.12.2 + +### Patch Changes + +- Updated dependencies [[`f5dfabf`](https://github.com/Effect-TS/effect/commit/f5dfabf51ba481a4468c1509c537314978ef6cec), [`17a5ea8`](https://github.com/Effect-TS/effect/commit/17a5ea8fa29785fe6e4c9480f2a2e9c8c59f3f38), [`d25f22b`](https://github.com/Effect-TS/effect/commit/d25f22be7598abe977caf6cdac3b0dd78b438c48)]: + - effect@3.16.14 + - @effect/platform@0.88.1 + - @effect/experimental@0.52.1 + - @effect/ai@0.22.1 + +## 0.12.1 + +### Patch Changes + +- [#5209](https://github.com/Effect-TS/effect/pull/5209) [`3deaa66`](https://github.com/Effect-TS/effect/commit/3deaa66e022e361a2036ce6bfc9d76f77d9cc948) Thanks @tim-smart! - fix ai layerConfig regression, to allow for conditional Config variables + +## 0.12.0 + +### Patch Changes + +- Updated dependencies [[`27206d7`](https://github.com/Effect-TS/effect/commit/27206d7f0558d7fe28de57bf54f1d0cc83acc92e), [`dbabf5e`](https://github.com/Effect-TS/effect/commit/dbabf5e76fa63b050d2b6c466713c7dc59f07d3c)]: + - @effect/platform@0.88.0 + - @effect/ai@0.22.0 + - @effect/experimental@0.52.0 + +## 0.11.19 + +### Patch Changes + +- Updated dependencies [[`c1c05a8`](https://github.com/Effect-TS/effect/commit/c1c05a8242fb5df7445b4a12387a60eac7726eb7), [`81fe4a2`](https://github.com/Effect-TS/effect/commit/81fe4a2c81d5e30e180a60e68c52016a27b350db)]: + - effect@3.16.13 + - @effect/ai@0.21.17 + - @effect/experimental@0.51.14 + - @effect/platform@0.87.13 + +## 0.11.18 + +### Patch Changes + +- [#5186](https://github.com/Effect-TS/effect/pull/5186) [`e5692ab`](https://github.com/Effect-TS/effect/commit/e5692ab2be157b885f449ffb5c5f022eca04a59e) Thanks @IMax153! - Do not use `Config.Wrap` for AI provider `layerConfig` + +- Updated dependencies [[`32ba77a`](https://github.com/Effect-TS/effect/commit/32ba77ae304d2161362a73e8b61965332626cf2d), [`d5e25b2`](https://github.com/Effect-TS/effect/commit/d5e25b237f05670ee42b386cb40b2cb448fc11d7)]: + - @effect/platform@0.87.12 + - @effect/ai@0.21.16 + - @effect/experimental@0.51.13 + +## 0.11.17 + +### Patch Changes + +- Updated dependencies [[`001392b`](https://github.com/Effect-TS/effect/commit/001392ba8bfcad101bb034348a7415012fb12f72), [`7bfb099`](https://github.com/Effect-TS/effect/commit/7bfb099cb5528511b8d63045c4fbb4dc9cb18528)]: + - @effect/platform@0.87.11 + - @effect/ai@0.21.15 + - @effect/experimental@0.51.12 + +## 0.11.16 + +### Patch Changes + +- Updated dependencies [[`678318d`](https://github.com/Effect-TS/effect/commit/678318d2e88233156b006acda56c2d138ee3ffa0), [`678318d`](https://github.com/Effect-TS/effect/commit/678318d2e88233156b006acda56c2d138ee3ffa0)]: + - @effect/platform@0.87.10 + - @effect/ai@0.21.14 + - @effect/experimental@0.51.11 + +## 0.11.15 + +### Patch Changes + +- Updated dependencies [[`54514a2`](https://github.com/Effect-TS/effect/commit/54514a2f53166de27ad7e756dbf12194691fd4af)]: + - @effect/platform@0.87.9 + - @effect/ai@0.21.13 + - @effect/experimental@0.51.10 + +## 0.11.14 + +### Patch Changes + +- Updated dependencies [[`4ce4f82`](https://github.com/Effect-TS/effect/commit/4ce4f824f6fdef492be1d35c05a490ffce518c89)]: + - @effect/platform@0.87.8 + - @effect/experimental@0.51.9 + - @effect/ai@0.21.12 + +## 0.11.13 + +### Patch Changes + +- [#5029](https://github.com/Effect-TS/effect/pull/5029) [`d92d12a`](https://github.com/Effect-TS/effect/commit/d92d12acb6097a4fa6c9c918faa3cd5c3fb6c778) Thanks @IMax153! - Cleanup AiLanguageModel construction and finish basic support for gemini + +- Updated dependencies [[`d92d12a`](https://github.com/Effect-TS/effect/commit/d92d12acb6097a4fa6c9c918faa3cd5c3fb6c778), [`25ca0cf`](https://github.com/Effect-TS/effect/commit/25ca0cf141139cd44ff53081b1c877f8f3ab5e41), [`d92d12a`](https://github.com/Effect-TS/effect/commit/d92d12acb6097a4fa6c9c918faa3cd5c3fb6c778)]: + - @effect/ai@0.21.11 + +## 0.11.12 + +### Patch Changes + +- Updated dependencies [[`a9b617f`](https://github.com/Effect-TS/effect/commit/a9b617f125171ed76cd79ab46d7a924daf3b0e70), [`7e26e86`](https://github.com/Effect-TS/effect/commit/7e26e86524abcc93713d6ad7eee486638c98f7c2)]: + - @effect/platform@0.87.7 + - @effect/ai@0.21.10 + - @effect/experimental@0.51.8 + +## 0.11.11 + +### Patch Changes + +- Updated dependencies [[`030ac21`](https://github.com/Effect-TS/effect/commit/030ac217eac167d345a095bff26d9c95827fa64c), [`905da99`](https://github.com/Effect-TS/effect/commit/905da996aad665057b4ca6dba1a4af44fb8835bd), [`aaae9b1`](https://github.com/Effect-TS/effect/commit/aaae9b10345ab5f867b08e1c6eb21685cfc2b078)]: + - @effect/ai@0.21.9 + - effect@3.16.12 + - @effect/experimental@0.51.7 + - @effect/platform@0.87.6 + +## 0.11.10 + +### Patch Changes + +- Updated dependencies [[`96c1292`](https://github.com/Effect-TS/effect/commit/96c129262835410b311a51d0bf7f58b8f6fc9a12)]: + - @effect/experimental@0.51.6 + - @effect/ai@0.21.8 + +## 0.11.9 + +### Patch Changes + +- Updated dependencies [[`2fd8676`](https://github.com/Effect-TS/effect/commit/2fd8676c803cd40000dfc3231f5daecaa0e0ebd2)]: + - @effect/platform@0.87.5 + - @effect/ai@0.21.7 + - @effect/experimental@0.51.5 + +## 0.11.8 + +### Patch Changes + +- Updated dependencies [[`e82a4fd`](https://github.com/Effect-TS/effect/commit/e82a4fd60f6528d08cef1a4aba0abe0d3ba741ad)]: + - @effect/platform@0.87.4 + - @effect/ai@0.21.6 + - @effect/experimental@0.51.4 + +## 0.11.7 + +### Patch Changes + +- Updated dependencies [[`1b6e396`](https://github.com/Effect-TS/effect/commit/1b6e396d699f3cbbc56b68f99055cf746529bb9e)]: + - @effect/platform@0.87.3 + - @effect/ai@0.21.5 + - @effect/experimental@0.51.3 + +## 0.11.6 + +### Patch Changes + +- Updated dependencies [[`4fea68c`](https://github.com/Effect-TS/effect/commit/4fea68ca7a25a3c39a1ab68b3885534513ab0c81), [`b927954`](https://github.com/Effect-TS/effect/commit/b9279543cf5688dd8a577af80456959c615217d0), [`99590a6`](https://github.com/Effect-TS/effect/commit/99590a6ca9128eb1ede265b6670b655311995614), [`6c3e24c`](https://github.com/Effect-TS/effect/commit/6c3e24c2308f7d4a29b8f4270ab81bca22ac6bb4)]: + - @effect/platform@0.87.2 + - effect@3.16.11 + - @effect/ai@0.21.4 + - @effect/experimental@0.51.2 + +## 0.11.5 + +### Patch Changes + +- [#5121](https://github.com/Effect-TS/effect/pull/5121) [`8e3c565`](https://github.com/Effect-TS/effect/commit/8e3c565aad2b888badb0b62f109d9b4ec4049305) Thanks @IMax153! - Fix several issues in the generated OpenAPI models for the Anthropic AI provider + package. + + The OpenAPI specification that Anthropic maintains for its API is apparently + [incorrect](https://github.com/anthropics/anthropic-sdk-typescript/issues/605). + Some properties which are marked as nullable but required are sometimes not + returned by the API. This fixes the schemas associated with some of those + properties, though others may exist / be found that require manual adjustment. + +## 0.11.4 + +### Patch Changes + +- [#5020](https://github.com/Effect-TS/effect/pull/5020) [`530aa65`](https://github.com/Effect-TS/effect/commit/530aa6561b68ea591cef44e30e8629082e42fda2) Thanks @IMax153! - add Amazon Bedrock AI provider package + +## 0.11.3 + +### Patch Changes + +- Updated dependencies [[`faad30e`](https://github.com/Effect-TS/effect/commit/faad30ec8742916be59f9db642d0fc98225b636c)]: + - effect@3.16.10 + - @effect/ai@0.21.3 + - @effect/experimental@0.51.1 + - @effect/platform@0.87.1 + +## 0.11.2 + +### Patch Changes + +- Updated dependencies []: + - @effect/ai@0.21.2 + - @effect/experimental@0.51.0 + +## 0.11.1 + +### Patch Changes + +- Updated dependencies [[`f667373`](https://github.com/Effect-TS/effect/commit/f667373da3471f1e907366780f8c3ea7f52cc5c8)]: + - @effect/ai@0.21.1 + - @effect/experimental@0.51.0 + +## 0.11.0 + +### Patch Changes + +- Updated dependencies [[`b5bac9a`](https://github.com/Effect-TS/effect/commit/b5bac9ac2913fcd11b02322624f03b544eef53ba)]: + - @effect/platform@0.87.0 + - @effect/ai@0.21.0 + - @effect/experimental@0.51.0 + +## 0.10.0 + +### Patch Changes + +- Updated dependencies [[`5137c70`](https://github.com/Effect-TS/effect/commit/5137c703461d8d3b363c112140a6e7f798241d07), [`c23d25c`](https://github.com/Effect-TS/effect/commit/c23d25c3e7c541f1f63b28484d8c461d86c67e99), [`5137c70`](https://github.com/Effect-TS/effect/commit/5137c703461d8d3b363c112140a6e7f798241d07), [`5137c70`](https://github.com/Effect-TS/effect/commit/5137c703461d8d3b363c112140a6e7f798241d07)]: + - effect@3.16.9 + - @effect/platform@0.86.0 + - @effect/ai@0.20.0 + - @effect/experimental@0.50.0 + +## 0.9.5 + +### Patch Changes + +- Updated dependencies [[`a8d99b2`](https://github.com/Effect-TS/effect/commit/a8d99b2ec2f55d9aa6e7d00a5138e80380716877)]: + - @effect/ai@0.19.4 + - @effect/experimental@0.49.2 + +## 0.9.4 + +### Patch Changes + +- Updated dependencies [[`914a191`](https://github.com/Effect-TS/effect/commit/914a191e7cb6341a3d0e965bccd27c336cf22e44)]: + - @effect/platform@0.85.2 + - @effect/ai@0.19.3 + - @effect/experimental@0.49.2 + +## 0.9.3 + +### Patch Changes + +- [#5051](https://github.com/Effect-TS/effect/pull/5051) [`0945c0d`](https://github.com/Effect-TS/effect/commit/0945c0d0a20df456c0b0ec53f5e7487480aa62e1) Thanks @IMax153! - Fix the generated Anthropic OpenAPI schemas + +## 0.9.2 + +### Patch Changes + +- Updated dependencies []: + - @effect/ai@0.19.2 + - @effect/experimental@0.49.1 + +## 0.9.1 + +### Patch Changes + +- Updated dependencies [[`8cb98d5`](https://github.com/Effect-TS/effect/commit/8cb98d53e68330228287ce2a2e0d8a4c86bcab3b), [`db2dd3c`](https://github.com/Effect-TS/effect/commit/db2dd3c3a8a77d791eae19e66153527e1cde4e6e)]: + - effect@3.16.8 + - @effect/ai@0.19.1 + - @effect/experimental@0.49.1 + - @effect/platform@0.85.1 + +## 0.9.0 + +### Patch Changes + +- Updated dependencies [[`93687dd`](https://github.com/Effect-TS/effect/commit/93687ddbb25ce3b324cd2b83d2ccff225e97307e), [`93687dd`](https://github.com/Effect-TS/effect/commit/93687ddbb25ce3b324cd2b83d2ccff225e97307e), [`93687dd`](https://github.com/Effect-TS/effect/commit/93687ddbb25ce3b324cd2b83d2ccff225e97307e)]: + - @effect/platform@0.85.0 + - @effect/ai@0.19.0 + - @effect/experimental@0.49.0 + +## 0.8.16 + +### Patch Changes + +- Updated dependencies [[`daed158`](https://github.com/Effect-TS/effect/commit/daed158f2cf00175633284f075cf611c52aa2a1c)]: + - @effect/ai@0.18.16 + +## 0.8.15 + +### Patch Changes + +- Updated dependencies [[`c315989`](https://github.com/Effect-TS/effect/commit/c315989cade6c2a5c9cb157ad85f56b492675add)]: + - @effect/ai@0.18.15 + +## 0.8.14 + +### Patch Changes + +- Updated dependencies [[`1bb0d8a`](https://github.com/Effect-TS/effect/commit/1bb0d8ab96782e99434356266b38251554ea0294), [`cbac1ac`](https://github.com/Effect-TS/effect/commit/cbac1ac61a4e15ad15828563b39eef412bcee66e), [`dd4d380`](https://github.com/Effect-TS/effect/commit/dd4d3802f714d59171b1e9226a7babf9723ea952)]: + - effect@3.16.7 + - @effect/ai@0.18.14 + - @effect/experimental@0.48.12 + - @effect/platform@0.84.11 + +## 0.8.13 + +### Patch Changes + +- Updated dependencies [[`a5f7595`](https://github.com/Effect-TS/effect/commit/a5f75956ef9a15a83c416517ef493f0ee2f5ee8a), [`a02470c`](https://github.com/Effect-TS/effect/commit/a02470c75579e91525a25adb3f21b3650d042fdd), [`aa3a819`](https://github.com/Effect-TS/effect/commit/aa3a819707c15dd39b6d9ae4b4293bd87b74e175), [`bf369b2`](https://github.com/Effect-TS/effect/commit/bf369b2902a0e0b195d957c18b9efd180942cf8b), [`f891d45`](https://github.com/Effect-TS/effect/commit/f891d45adffdafd3f94a2eca23faa354e3a409a8)]: + - effect@3.16.6 + - @effect/ai@0.18.13 + - @effect/platform@0.84.10 + - @effect/experimental@0.48.11 + +## 0.8.12 + +### Patch Changes + +- Updated dependencies [[`bf418ef`](https://github.com/Effect-TS/effect/commit/bf418ef14a0f2ec965535793d5cea8fa8ba177ac)]: + - effect@3.16.5 + - @effect/ai@0.18.12 + - @effect/experimental@0.48.10 + - @effect/platform@0.84.9 + +## 0.8.11 + +### Patch Changes + +- Updated dependencies [[`2dc5f93`](https://github.com/Effect-TS/effect/commit/2dc5f932f89d260e2f6139c9b89e0548d11d94c2)]: + - @effect/ai@0.18.11 + - @effect/experimental@0.48.9 + +## 0.8.10 + +### Patch Changes + +- Updated dependencies [[`8b9db77`](https://github.com/Effect-TS/effect/commit/8b9db7742846af0f58fd8e8b7acb7f4f5ff487ec)]: + - @effect/platform@0.84.8 + - @effect/experimental@0.48.9 + - @effect/ai@0.18.10 + +## 0.8.9 + +### Patch Changes + +- Updated dependencies [[`74ab9a0`](https://github.com/Effect-TS/effect/commit/74ab9a0a9e16d6e019369d256e1e24175c8bc3f3), [`770008e`](https://github.com/Effect-TS/effect/commit/770008eca3aad2899a2ed951236e575793294b28)]: + - effect@3.16.4 + - @effect/ai@0.18.9 + - @effect/experimental@0.48.8 + - @effect/platform@0.84.7 + +## 0.8.8 + +### Patch Changes + +- Updated dependencies [[`a2d57c9`](https://github.com/Effect-TS/effect/commit/a2d57c9ac596445009ca12859b78e00e5d89b936)]: + - @effect/experimental@0.48.7 + - @effect/ai@0.18.8 + +## 0.8.7 + +### Patch Changes + +- Updated dependencies [[`ceea77a`](https://github.com/Effect-TS/effect/commit/ceea77a13055f145520f763e3fce5b8ff15d728f)]: + - @effect/platform@0.84.6 + - @effect/ai@0.18.7 + - @effect/experimental@0.48.6 + +## 0.8.6 + +### Patch Changes + +- Updated dependencies [[`85f54ed`](https://github.com/Effect-TS/effect/commit/85f54ed1ecf2f191de8c907247066e3631b5d7e1), [`ec52c6a`](https://github.com/Effect-TS/effect/commit/ec52c6a2211e76972462b15b9d5a9d6d56761b7a)]: + - @effect/ai@0.18.6 + - @effect/platform@0.84.5 + - @effect/experimental@0.48.5 + +## 0.8.5 + +### Patch Changes + +- Updated dependencies [[`4ddb28d`](https://github.com/Effect-TS/effect/commit/4ddb28d230d572735fe34539c1c59005d4932d8a)]: + - @effect/ai@0.18.5 + +## 0.8.4 + +### Patch Changes + +- Updated dependencies [[`87722fc`](https://github.com/Effect-TS/effect/commit/87722fce693a9b49284bbddbf82d30714c688261), [`36217ee`](https://github.com/Effect-TS/effect/commit/36217eeb1337edd9ac3f9a635b80a6385d22ae8f)]: + - effect@3.16.3 + - @effect/ai@0.18.4 + - @effect/experimental@0.48.4 + - @effect/platform@0.84.4 + +## 0.8.3 + +### Patch Changes + +- Updated dependencies [[`52c88c4`](https://github.com/Effect-TS/effect/commit/52c88c4b7d20ea819b9f2efaf112d03de0a4627b), [`ab7684f`](https://github.com/Effect-TS/effect/commit/ab7684f1c2a0671bf091f255d220e3a4cc7f528e)]: + - @effect/ai@0.18.3 + - @effect/platform@0.84.3 + - @effect/experimental@0.48.3 + +## 0.8.2 + +### Patch Changes + +- Updated dependencies [[`0ddf148`](https://github.com/Effect-TS/effect/commit/0ddf148a247aa87af043d276b8453a714a400897)]: + - effect@3.16.2 + - @effect/ai@0.18.2 + - @effect/experimental@0.48.2 + - @effect/platform@0.84.2 + +## 0.8.1 + +### Patch Changes + +- Updated dependencies [[`71174d0`](https://github.com/Effect-TS/effect/commit/71174d09691314a9b6b66189e456fd21e3eb6543), [`d615e6e`](https://github.com/Effect-TS/effect/commit/d615e6e5b944f6fd5e627e31752c7ca7e4e1c17d)]: + - @effect/platform@0.84.1 + - effect@3.16.1 + - @effect/ai@0.18.1 + - @effect/experimental@0.48.1 + +## 0.8.0 + +### Minor Changes + +- [#4891](https://github.com/Effect-TS/effect/pull/4891) [`0552674`](https://github.com/Effect-TS/effect/commit/055267461a3076b06dea896258f4bb2154211fcb) Thanks @IMax153! - Make `AiModel` a plain `Layer` and remove `AiPlan` in favor of `ExecutionPlan` + + This release substantially simplifies and improves the ergonomics of using `AiModel` for various providers. With these changes, an `AiModel` now returns a plain `Layer` which can be used to provide services to a program that interacts with large language models. + + **Before** + + ```ts + import { AiLanguageModel } from "@effect/ai" + import { OpenAiClient, OpenAiLanguageModel } from "@effect/ai-openai" + import { NodeHttpClient } from "@effect/platform-node" + import { Config, Console, Effect, Layer } from "effect" + + // Produces an `AiModel` + const Gpt4o = OpenAiLanguageModel.model("gpt-4o") + + // Generate a dad joke + const getDadJoke = AiLanguageModel.generateText({ + prompt: "Tell me a dad joke" + }) + + const program = Effect.gen(function* () { + // Build the `AiModel` into a `Provider` + const gpt4o = yield* Gpt4o + // Use the built `AiModel` to run the program + const response = yield* gpt4o.use(getDadJoke) + // Log the response + yield* Console.log(response.text) + }) + + const OpenAi = OpenAiClient.layerConfig({ + apiKey: Config.redacted("OPENAI_API_KEY") + }).pipe(Layer.provide(NodeHttpClient.layerUndici)) + + program.pipe(Effect.provide(OpenAi), Effect.runPromise) + ``` + + **After** + + ```ts + import { AiLanguageModel } from "@effect/ai" + import { OpenAiClient, OpenAiLanguageModel } from "@effect/ai-openai" + import { NodeHttpClient } from "@effect/platform-node" + import { Config, Console, Effect, Layer } from "effect" + + // Produces a `Layer` + const Gpt4o = OpenAiLanguageModel.model("gpt-4o") + + const program = Effect.gen(function*() { + // Generate a dad joke + const response = yield* AiLanguageModel.generateText({ + prompt: "Tell me a dad joke" + }) + // Log the response + yield* Console.log(response.text) + ).pipe(Effect.provide(Gpt4o)) + + const OpenAi = OpenAiClient.layerConfig({ + apiKey: Config.redacted("OPENAI_API_KEY") + }).pipe(Layer.provide(NodeHttpClient.layerUndici)) + + program.pipe( + Effect.provide(OpenAi), + Effect.runPromise + ) + ``` + + In addition, `AiModel` can be `yield*`'ed to produce a layer with no requirements. + + This shifts the requirements of building the layer into the calling effect, which is particularly useful for creating AI-powered services. + + ```ts + import { AiLanguageModel } from "@effect/ai" + import { OpenAiLanguageModel } from "@effect/ai-openai" + import { Effect } from "effect" + + class DadJokes extends Effect.Service()("DadJokes", { + effect: Effect.gen(function* () { + // Yielding the model will return a layer with no requirements + // + // ┌─── Layer + // ▼ + const model = yield* OpenAiLanguageModel.model("gpt-4o") + + const getDadJoke = AiLanguageModel.generateText({ + prompt: "Generate a dad joke" + }).pipe(Effect.provide(model)) + + return { getDadJoke } as const + }) + }) {} + + // The requirements are lifted into the service constructor + // + // ┌─── Layer + // ▼ + DadJokes.Default + ``` + +### Patch Changes + +- Updated dependencies [[`ee0bd5d`](https://github.com/Effect-TS/effect/commit/ee0bd5d24864752c54cb359f67a67dd903971ec4), [`5189800`](https://github.com/Effect-TS/effect/commit/51898004e11766b8cf6d95e960b636f6d5db79ec), [`58bfeaa`](https://github.com/Effect-TS/effect/commit/58bfeaa64ded8c88f772b184311c0c0dbac10960), [`194d748`](https://github.com/Effect-TS/effect/commit/194d7486943f56f3267ef415395ac220a4b3e634), [`0552674`](https://github.com/Effect-TS/effect/commit/055267461a3076b06dea896258f4bb2154211fcb), [`918c9ea`](https://github.com/Effect-TS/effect/commit/918c9ea1a57facb154f0fb26792021f337054dee), [`9198e6f`](https://github.com/Effect-TS/effect/commit/9198e6fcc1a3ff4fefb3363004de558d8de01f40), [`2a370bf`](https://github.com/Effect-TS/effect/commit/2a370bf625fdeede5659721468eb0d527e403279), [`58ccb91`](https://github.com/Effect-TS/effect/commit/58ccb91328c8df5d49808b673738bc09df355201), [`fd47834`](https://github.com/Effect-TS/effect/commit/fd478348203fa89462b0a1d067ce4de034353df4)]: + - effect@3.16.0 + - @effect/ai@0.18.0 + - @effect/experimental@0.48.0 + - @effect/platform@0.84.0 + +## 0.7.0 + +### Patch Changes + +- Updated dependencies [[`5522520`](https://github.com/Effect-TS/effect/commit/55225206ab9af0ad60b1c0654690a8a096d625cd), [`cc5bb2b`](https://github.com/Effect-TS/effect/commit/cc5bb2b918a9450a975f702dabcea891bda382cb)]: + - @effect/platform@0.83.0 + - effect@3.15.5 + - @effect/ai@0.17.0 + - @effect/experimental@0.47.0 + +## 0.6.9 + +### Patch Changes + +- Updated dependencies [[`0617b9d`](https://github.com/Effect-TS/effect/commit/0617b9dc365f1963b36949ad7f9023ab6eb94524)]: + - @effect/platform@0.82.8 + - @effect/ai@0.16.9 + - @effect/experimental@0.46.8 + +## 0.6.8 + +### Patch Changes + +- Updated dependencies [[`f570554`](https://github.com/Effect-TS/effect/commit/f57055459524587b041340577dad85476bb35f81), [`78047e8`](https://github.com/Effect-TS/effect/commit/78047e8dfc8005b66f87afe50bb95981fea51561), [`c20b95a`](https://github.com/Effect-TS/effect/commit/c20b95a99ffe452b4774c844d397a905f713b6d6), [`94ada43`](https://github.com/Effect-TS/effect/commit/94ada430928d5685bdbef513e87562c20774a3a2)]: + - effect@3.15.4 + - @effect/platform@0.82.7 + - @effect/ai@0.16.8 + - @effect/experimental@0.46.7 + +## 0.6.7 + +### Patch Changes + +- Updated dependencies [[`618903b`](https://github.com/Effect-TS/effect/commit/618903ba9ae96e2bfe6ee31f61c4359b915f2a36)]: + - @effect/platform@0.82.6 + - @effect/ai@0.16.7 + - @effect/experimental@0.46.6 + +## 0.6.6 + +### Patch Changes + +- Updated dependencies [[`7764a07`](https://github.com/Effect-TS/effect/commit/7764a07d960c60df81f14e1dc949518f4bbe494a), [`4577f54`](https://github.com/Effect-TS/effect/commit/4577f548d67273e576cdde423bdd34a4b910766a), [`30a0d9c`](https://github.com/Effect-TS/effect/commit/30a0d9cb51c84290d51b1361d72ff5cee33c13c7)]: + - @effect/platform@0.82.5 + - effect@3.15.3 + - @effect/ai@0.16.6 + - @effect/experimental@0.46.5 + +## 0.6.5 + +### Patch Changes + +- [#4899](https://github.com/Effect-TS/effect/pull/4899) [`0a8c0e7`](https://github.com/Effect-TS/effect/commit/0a8c0e762af96d5dbf323d495a06647f39797674) Thanks @alex-dixon! - fix: anthropic tag name + +- Updated dependencies [[`d45e8a8`](https://github.com/Effect-TS/effect/commit/d45e8a8ac8227192f504e39e6d04fdcf4fb1d225), [`d13b68e`](https://github.com/Effect-TS/effect/commit/d13b68e3a9456d0bfee9bca8273a7b44a9c69087)]: + - @effect/platform@0.82.4 + - @effect/ai@0.16.5 + - @effect/experimental@0.46.4 + +## 0.6.4 + +### Patch Changes + +- Updated dependencies [[`b8722b8`](https://github.com/Effect-TS/effect/commit/b8722b817e2306fe8c8245f3f9e32d85b824b961), [`a328f4b`](https://github.com/Effect-TS/effect/commit/a328f4b4fe717dd53e5b04a30f387433c32f7328)]: + - effect@3.15.2 + - @effect/platform@0.82.3 + - @effect/ai@0.16.4 + - @effect/experimental@0.46.3 + +## 0.6.3 + +### Patch Changes + +- Updated dependencies [[`739a3d4`](https://github.com/Effect-TS/effect/commit/739a3d4a4565915fe2e690003f4f9085cb4422fc)]: + - @effect/platform@0.82.2 + - @effect/ai@0.16.3 + - @effect/experimental@0.46.2 + +## 0.6.2 + +### Patch Changes + +- Updated dependencies [[`787ce70`](https://github.com/Effect-TS/effect/commit/787ce7042e35b657963473c6efe47752868cd811), [`1269641`](https://github.com/Effect-TS/effect/commit/1269641a99ae43069f7648ff79ffe8729b54b348), [`1269641`](https://github.com/Effect-TS/effect/commit/1269641a99ae43069f7648ff79ffe8729b54b348)]: + - effect@3.15.1 + - @effect/ai@0.16.2 + - @effect/experimental@0.46.1 + - @effect/platform@0.82.1 + +## 0.6.1 + +### Patch Changes + +- Updated dependencies [[`cb3c30f`](https://github.com/Effect-TS/effect/commit/cb3c30f540a83dafcd6d841375073b5e069fa417)]: + - @effect/ai@0.16.1 + - @effect/experimental@0.46.0 + +## 0.6.0 + +### Patch Changes + +- Updated dependencies [[`c654595`](https://github.com/Effect-TS/effect/commit/c65459587b51da140b78098e81fdbfece65d53e2), [`d9f5dea`](https://github.com/Effect-TS/effect/commit/d9f5deae0f02f5de2b9fcb1cca8b142ba4bc2bba), [`49aa723`](https://github.com/Effect-TS/effect/commit/49aa7236a15e13f818c86edbca08c4af67c8dfaf), [`74c14d0`](https://github.com/Effect-TS/effect/commit/74c14d01d0cb48cf517a1b6e29a373a96ed0ff5b), [`e4f49b6`](https://github.com/Effect-TS/effect/commit/e4f49b66857e01b74ab6a9a0bc7132f44cd04cbb), [`6f02224`](https://github.com/Effect-TS/effect/commit/6f02224b3fc46a682ad2defb1a260841956c6780), [`1dcfd41`](https://github.com/Effect-TS/effect/commit/1dcfd41ff96abd706901293a00c1893cb29dd8fd), [`b21ab16`](https://github.com/Effect-TS/effect/commit/b21ab16b6f773e7ec4369db4e752c35e719f7870), [`fcf1822`](https://github.com/Effect-TS/effect/commit/fcf1822f98fcda60351d64e9d2c2c13563d7e6db), [`0061dd1`](https://github.com/Effect-TS/effect/commit/0061dd140740165e91569a684cce27a77b23229e), [`8421e6e`](https://github.com/Effect-TS/effect/commit/8421e6e49332bca8f96f482dfd48680e238b3a89), [`a9b3fb7`](https://github.com/Effect-TS/effect/commit/a9b3fb78abcfdb525318a956fd02fcadeb56143e), [`fa10f56`](https://github.com/Effect-TS/effect/commit/fa10f56b96bd9af070ba99ebc3279aa93954261e)]: + - effect@3.15.0 + - @effect/platform@0.82.0 + - @effect/ai@0.16.0 + - @effect/experimental@0.46.0 + +## 0.5.0 + +### Minor Changes + +- [#4766](https://github.com/Effect-TS/effect/pull/4766) [`a4d42c5`](https://github.com/Effect-TS/effect/commit/a4d42c55669eff56963d06323d155a5bf3082a70) Thanks @IMax153! - Refactor `@effect/ai-anthropic` to align with changes to `@effect/ai` + +### Patch Changes + +- Updated dependencies [[`a4d42c5`](https://github.com/Effect-TS/effect/commit/a4d42c55669eff56963d06323d155a5bf3082a70)]: + - @effect/ai@0.15.0 + - @effect/experimental@0.45.1 + +## 0.4.1 + +### Patch Changes + +- Updated dependencies [[`24a9ebb`](https://github.com/Effect-TS/effect/commit/24a9ebbb5af598f0bfd6ecc45307e528043fe011)]: + - effect@3.14.22 + - @effect/ai@0.14.1 + - @effect/experimental@0.45.1 + - @effect/platform@0.81.1 + +## 0.4.0 + +### Patch Changes + +- Updated dependencies [[`672920f`](https://github.com/Effect-TS/effect/commit/672920f85da8abd5f9d4ad85e29248a2aca57ed8)]: + - @effect/platform@0.81.0 + - @effect/ai@0.14.0 + - @effect/experimental@0.45.0 + +## 0.3.22 + +### Patch Changes + +- Updated dependencies [[`2f3b7d4`](https://github.com/Effect-TS/effect/commit/2f3b7d4e1fa1ef8790b0ca4da22eb88872ee31df)]: + - effect@3.14.21 + - @effect/ai@0.13.21 + - @effect/experimental@0.44.21 + - @effect/platform@0.80.21 + +## 0.3.21 + +### Patch Changes + +- Updated dependencies [[`17e2f30`](https://github.com/Effect-TS/effect/commit/17e2f3091408cf0fca9414d4af3bdf7b2765b378)]: + - effect@3.14.20 + - @effect/ai@0.13.20 + - @effect/experimental@0.44.20 + - @effect/platform@0.80.20 + +## 0.3.20 + +### Patch Changes + +- Updated dependencies [[`056a910`](https://github.com/Effect-TS/effect/commit/056a910d0a0b8b00b0dc9df4a070466b2b5c2f6c), [`e25e7bb`](https://github.com/Effect-TS/effect/commit/e25e7bbc1797733916f48f501425d9f2ef310d9f), [`3273d57`](https://github.com/Effect-TS/effect/commit/3273d572c2b3175a842677f19efeea4cd65ab016)]: + - effect@3.14.19 + - @effect/platform@0.80.19 + - @effect/ai@0.13.19 + - @effect/experimental@0.44.19 + +## 0.3.19 + +### Patch Changes + +- Updated dependencies [[`b1164d4`](https://github.com/Effect-TS/effect/commit/b1164d49a1dfdf299e9971367b6fc6be4df0ddff)]: + - effect@3.14.18 + - @effect/ai@0.13.18 + - @effect/experimental@0.44.18 + - @effect/platform@0.80.18 + +## 0.3.18 + +### Patch Changes + +- Updated dependencies [[`0b54681`](https://github.com/Effect-TS/effect/commit/0b54681cd89245e211d8f49272be0f1bf2f81813), [`41a59d5`](https://github.com/Effect-TS/effect/commit/41a59d5916a296b12b0d5ead9e859e05f40b4cce)]: + - effect@3.14.17 + - @effect/ai@0.13.17 + - @effect/experimental@0.44.17 + - @effect/platform@0.80.17 + +## 0.3.17 + +### Patch Changes + +- Updated dependencies [[`ee14444`](https://github.com/Effect-TS/effect/commit/ee144441021ec77039e43396eaf90714687bb495), [`f1c8583`](https://github.com/Effect-TS/effect/commit/f1c8583f8c3ea9415f813795ca2940a897c9ba9a)]: + - effect@3.14.16 + - @effect/platform@0.80.16 + - @effect/ai@0.13.16 + - @effect/experimental@0.44.16 + +## 0.3.16 + +### Patch Changes + +- Updated dependencies [[`239cc99`](https://github.com/Effect-TS/effect/commit/239cc995ce645946210a3c3d2cb52bd3547c0687), [`8b6c947`](https://github.com/Effect-TS/effect/commit/8b6c947eaa8e45a67ecb3c37d45cd27f3e41d165), [`c50a63b`](https://github.com/Effect-TS/effect/commit/c50a63bbecb9f560b9cae349c447eed877d1b9b6)]: + - effect@3.14.15 + - @effect/ai@0.13.15 + - @effect/experimental@0.44.15 + - @effect/platform@0.80.15 + +## 0.3.15 + +### Patch Changes + +- Updated dependencies [[`6ed8d15`](https://github.com/Effect-TS/effect/commit/6ed8d1589beb181d30abc79afebdaabc1d101538)]: + - effect@3.14.14 + - @effect/ai@0.13.14 + - @effect/experimental@0.44.14 + - @effect/platform@0.80.14 + +## 0.3.14 + +### Patch Changes + +- Updated dependencies [[`ee77788`](https://github.com/Effect-TS/effect/commit/ee77788747e7ebbde6bfa88256cde49dbbad3608), [`5fce6ba`](https://github.com/Effect-TS/effect/commit/5fce6ba19c3cc63cc0104e737e581ad989dedbf0), [`570e45f`](https://github.com/Effect-TS/effect/commit/570e45f8cb936e42ec48f67f21bb2b7252f36c0c)]: + - effect@3.14.13 + - @effect/ai@0.13.13 + - @effect/experimental@0.44.13 + - @effect/platform@0.80.13 + +## 0.3.13 + +### Patch Changes + +- Updated dependencies [[`c2ad9ee`](https://github.com/Effect-TS/effect/commit/c2ad9ee9f3c4c743390edf35ed9e85a20be33811), [`9c68654`](https://github.com/Effect-TS/effect/commit/9c686542b6eb3ea188cb70673ef2e41223633e89)]: + - effect@3.14.12 + - @effect/ai@0.13.12 + - @effect/experimental@0.44.12 + - @effect/platform@0.80.12 + +## 0.3.12 + +### Patch Changes + +- Updated dependencies [[`e536127`](https://github.com/Effect-TS/effect/commit/e536127c1e6f2fb3a542c73ae919435a629a346b)]: + - effect@3.14.11 + - @effect/ai@0.13.11 + - @effect/experimental@0.44.11 + - @effect/platform@0.80.11 + +## 0.3.11 + +### Patch Changes + +- Updated dependencies [[`bc7efa3`](https://github.com/Effect-TS/effect/commit/bc7efa3b031bb25e1ed3c8f2d3fb5e8da166cadc)]: + - effect@3.14.10 + - @effect/ai@0.13.10 + - @effect/experimental@0.44.10 + - @effect/platform@0.80.10 + +## 0.3.10 + +### Patch Changes + +- Updated dependencies [[`d78249f`](https://github.com/Effect-TS/effect/commit/d78249f0b67f63cf4baf806ff090cba33293daf0)]: + - effect@3.14.9 + - @effect/ai@0.13.9 + - @effect/experimental@0.44.9 + - @effect/platform@0.80.9 + +## 0.3.9 + +### Patch Changes + +- [#4726](https://github.com/Effect-TS/effect/pull/4726) [`b8b0703`](https://github.com/Effect-TS/effect/commit/b8b070382b3290eff922b76125f0d06732b74155) Thanks @dearlordylord! - AnthropicClient.layerConfig has the same ROut as AnthropicClient.layer - specifically, AiModels.AiModels was missing + +## 0.3.8 + +### Patch Changes + +- Updated dependencies [[`b3a2d32`](https://github.com/Effect-TS/effect/commit/b3a2d32772e6f7f20eacf2e18128e99324c4d378)]: + - effect@3.14.8 + - @effect/ai@0.13.8 + - @effect/experimental@0.44.8 + - @effect/platform@0.80.8 + +## 0.3.7 + +### Patch Changes + +- Updated dependencies [[`b542a4b`](https://github.com/Effect-TS/effect/commit/b542a4bf195be0c9af1523e1ba96c953decc4d25)]: + - effect@3.14.7 + - @effect/ai@0.13.7 + - @effect/experimental@0.44.7 + - @effect/platform@0.80.7 + +## 0.3.6 + +### Patch Changes + +- Updated dependencies [[`47618c1`](https://github.com/Effect-TS/effect/commit/47618c1ad84ebcc5a51133a3fff5aa5012d49d45), [`6077882`](https://github.com/Effect-TS/effect/commit/60778824a4794336c33807801f813f8751d1c7e4)]: + - effect@3.14.6 + - @effect/ai@0.13.6 + - @effect/experimental@0.44.6 + - @effect/platform@0.80.6 + +## 0.3.5 + +### Patch Changes + +- Updated dependencies [[`40dbfef`](https://github.com/Effect-TS/effect/commit/40dbfeff239b6e567706752114f31b2fce7de4e3), [`85fba81`](https://github.com/Effect-TS/effect/commit/85fba815ac07eb13d4227a69ac76a18e4b94df18), [`5a5ebdd`](https://github.com/Effect-TS/effect/commit/5a5ebdddfaddd259538b4599a6676281faca778e)]: + - effect@3.14.5 + - @effect/platform@0.80.5 + - @effect/ai@0.13.5 + - @effect/experimental@0.44.5 + +## 0.3.4 + +### Patch Changes + +- Updated dependencies [[`e4ba2c6`](https://github.com/Effect-TS/effect/commit/e4ba2c66a878e81b5e295d6d49aaf724b80a28ef)]: + - effect@3.14.4 + - @effect/ai@0.13.4 + - @effect/experimental@0.44.4 + - @effect/platform@0.80.4 + +## 0.3.3 + +### Patch Changes + +- Updated dependencies [[`37aa8e1`](https://github.com/Effect-TS/effect/commit/37aa8e137725a902e70cd1e468ea98b873aa5056), [`34f03d6`](https://github.com/Effect-TS/effect/commit/34f03d66875f21f266f102223a03cd14c2ed6ea6)]: + - effect@3.14.3 + - @effect/ai@0.13.3 + - @effect/experimental@0.44.3 + - @effect/platform@0.80.3 + +## 0.3.2 + +### Patch Changes + +- Updated dependencies [[`f87991b`](https://github.com/Effect-TS/effect/commit/f87991b6d8a2edfaf90b01cebda4b466992ae865), [`f87991b`](https://github.com/Effect-TS/effect/commit/f87991b6d8a2edfaf90b01cebda4b466992ae865), [`0a3e3e1`](https://github.com/Effect-TS/effect/commit/0a3e3e18eea5e0d1882f1a6c906198e6ef226a41)]: + - effect@3.14.2 + - @effect/ai@0.13.2 + - @effect/experimental@0.44.2 + - @effect/platform@0.80.2 + +## 0.3.1 + +### Patch Changes + +- Updated dependencies [[`4a274fe`](https://github.com/Effect-TS/effect/commit/4a274fe9f623182b6b902827e0e83bd89ca3b05c)]: + - effect@3.14.1 + - @effect/ai@0.13.1 + - @effect/experimental@0.44.1 + - @effect/platform@0.80.1 + +## 0.3.0 + +### Patch Changes + +- Updated dependencies [[`1f47e4e`](https://github.com/Effect-TS/effect/commit/1f47e4e12546ab691b29bfb7b5128bb17b93baa5), [`26dd75f`](https://github.com/Effect-TS/effect/commit/26dd75f276a0d8a63eab313bd5a167d5072c9780), [`aba2d1d`](https://github.com/Effect-TS/effect/commit/aba2d1d831ea149481bd4dd755528c0afa8239ce), [`3131f8f`](https://github.com/Effect-TS/effect/commit/3131f8fd12ba9eb31b90fa2f42bf88b12309133c), [`aba2d1d`](https://github.com/Effect-TS/effect/commit/aba2d1d831ea149481bd4dd755528c0afa8239ce), [`04dff2d`](https://github.com/Effect-TS/effect/commit/04dff2d01ac68c260f29a6d4743381825c353c86), [`aba2d1d`](https://github.com/Effect-TS/effect/commit/aba2d1d831ea149481bd4dd755528c0afa8239ce), [`c7fac0c`](https://github.com/Effect-TS/effect/commit/c7fac0cd7eadcd5cc0c3a987051c5b57ad271638), [`aba2d1d`](https://github.com/Effect-TS/effect/commit/aba2d1d831ea149481bd4dd755528c0afa8239ce), [`ffaa3f3`](https://github.com/Effect-TS/effect/commit/ffaa3f3969df26610fcc02ad537340641d44e803), [`ab957c1`](https://github.com/Effect-TS/effect/commit/ab957c1fee714868f56c7ab4e802b9d449e9b666), [`35db9ce`](https://github.com/Effect-TS/effect/commit/35db9ce228f1416c8abacc6dc9c36fbd0f33ef0f), [`aba2d1d`](https://github.com/Effect-TS/effect/commit/aba2d1d831ea149481bd4dd755528c0afa8239ce), [`cf77ea9`](https://github.com/Effect-TS/effect/commit/cf77ea9ab4fc89e66a43f682a9926ccdee6c57ed), [`26dd75f`](https://github.com/Effect-TS/effect/commit/26dd75f276a0d8a63eab313bd5a167d5072c9780), [`baaab60`](https://github.com/Effect-TS/effect/commit/baaab60b737f35dfab8e4a21bce28a195d19e899)]: + - effect@3.14.0 + - @effect/experimental@0.44.0 + - @effect/platform@0.80.0 + - @effect/ai@0.13.0 + +## 0.2.4 + +### Patch Changes + +- [#4592](https://github.com/Effect-TS/effect/pull/4592) [`5662363`](https://github.com/Effect-TS/effect/commit/566236361e270e575ef1cbf308ad1967c82a362c) Thanks @tim-smart! - update generated ai clients + +- Updated dependencies [[`5662363`](https://github.com/Effect-TS/effect/commit/566236361e270e575ef1cbf308ad1967c82a362c), [`5f1fd15`](https://github.com/Effect-TS/effect/commit/5f1fd15308ab154791580059b89877d19a2055c2), [`8bb1460`](https://github.com/Effect-TS/effect/commit/8bb1460c824f66f0f25ebd899c5e74e388089c37)]: + - @effect/platform@0.79.4 + - @effect/ai@0.12.4 + - @effect/experimental@0.43.4 + +## 0.2.3 + +### Patch Changes + +- Updated dependencies [[`0c4803f`](https://github.com/Effect-TS/effect/commit/0c4803fcc69262d11a97ce49d0e9b4288df0651f), [`6f65ac4`](https://github.com/Effect-TS/effect/commit/6f65ac4eac1489cd6ea390e18b0908670722adad)]: + - effect@3.13.12 + - @effect/ai@0.12.3 + - @effect/experimental@0.43.3 + - @effect/platform@0.79.3 + +## 0.2.2 + +### Patch Changes + +- Updated dependencies [[`fad8cca`](https://github.com/Effect-TS/effect/commit/fad8cca9bbfcc2eaeb44b97c15dbe0a1eda75315), [`4296293`](https://github.com/Effect-TS/effect/commit/4296293049414d0cf2d915a26c552b09f946b9a0), [`9c241ab`](https://github.com/Effect-TS/effect/commit/9c241abe47ccf7a5257b98a4a64a63054a12741d), [`082b0c1`](https://github.com/Effect-TS/effect/commit/082b0c1b9f4252bcdd69608f2e4a9226f953ac3f), [`be12983`](https://github.com/Effect-TS/effect/commit/be12983bc7e7537b41cd8910fc4eb7d1da56ab07), [`de88127`](https://github.com/Effect-TS/effect/commit/de88127a5a5906ccece98af74787b5ae0e65e431)]: + - effect@3.13.11 + - @effect/ai@0.12.2 + - @effect/experimental@0.43.2 + - @effect/platform@0.79.2 + +## 0.2.1 + +### Patch Changes + +- Updated dependencies [[`527c964`](https://github.com/Effect-TS/effect/commit/527c9645229f5be9714a7e60a38a9e753c4bbfb1)]: + - effect@3.13.10 + - @effect/ai@0.12.1 + - @effect/experimental@0.43.1 + - @effect/platform@0.79.1 + +## 0.2.0 + +### Patch Changes + +- Updated dependencies [[`88fe129`](https://github.com/Effect-TS/effect/commit/88fe12923740765c0335a6e6203fdcc6a463edca), [`d630249`](https://github.com/Effect-TS/effect/commit/d630249426113088abe8b382db4f14d80f2160c2), [`2976e52`](https://github.com/Effect-TS/effect/commit/2976e52538d9dc9ffdcbc84d4ac748cff9305971)]: + - @effect/platform@0.79.0 + - effect@3.13.9 + - @effect/experimental@0.43.0 + - @effect/ai@0.12.0 + +## 0.1.1 + +### Patch Changes + +- Updated dependencies [[`c65d336`](https://github.com/Effect-TS/effect/commit/c65d3362d07ec815ff3b46278314e8a31706ddc2), [`22d2ebb`](https://github.com/Effect-TS/effect/commit/22d2ebb4b11f5a44351a4736e65da391a3b647d0)]: + - effect@3.13.8 + - @effect/ai@0.11.1 + - @effect/experimental@0.42.1 + - @effect/platform@0.78.1 + +## 0.1.0 + +### Patch Changes + +- Updated dependencies [[`c5bcf53`](https://github.com/Effect-TS/effect/commit/c5bcf53b7cb49dacffdd2a6cd8eb48cc452b417e)]: + - @effect/platform@0.78.0 + - @effect/ai@0.11.0 + - @effect/experimental@0.42.0 + +## 0.0.7 + +### Patch Changes + +- Updated dependencies [[`840cc73`](https://github.com/Effect-TS/effect/commit/840cc7329908db7ca693ef47b07d4f845c29cadd), [`9bf8a74`](https://github.com/Effect-TS/effect/commit/9bf8a74b967f18d931743dd5196af326c9118e9c), [`87ba23c`](https://github.com/Effect-TS/effect/commit/87ba23c41c193503ed0c612b0d32d0b253794c64), [`f910880`](https://github.com/Effect-TS/effect/commit/f91088069057f3b4529753f5bc5532b028d726df), [`0d01480`](https://github.com/Effect-TS/effect/commit/0d014803e4f688f74386a80abd65485e1a319244), [`a95108a`](https://github.com/Effect-TS/effect/commit/a95108acac7f25fc5e1c0dcdf16bcc638dca5c00)]: + - @effect/platform@0.77.7 + - effect@3.13.7 + - @effect/ai@0.10.7 + - @effect/experimental@0.41.7 + +## 0.0.6 + +### Patch Changes + +- Updated dependencies [[`3154ce4`](https://github.com/Effect-TS/effect/commit/3154ce4692fa18b804982158d3c4c8a8a5fae386)]: + - effect@3.13.6 + - @effect/ai@0.10.6 + - @effect/experimental@0.41.6 + - @effect/platform@0.77.6 + +## 0.0.5 + +### Patch Changes + +- Updated dependencies [[`3d6d323`](https://github.com/Effect-TS/effect/commit/3d6d323c2a1028f3caba45453187b9374bac2c36), [`367bb35`](https://github.com/Effect-TS/effect/commit/367bb35f4c2a254e1fb211d96db2474a7aed9020), [`6cf11c3`](https://github.com/Effect-TS/effect/commit/6cf11c3a75773ceec2877c85ddc760f381f0866d), [`a0acec8`](https://github.com/Effect-TS/effect/commit/a0acec851f72e19466363d24b9cc218acd00006a), [`975c20e`](https://github.com/Effect-TS/effect/commit/975c20e446186e9bb975f77e7c6ac7b248f7b5f6)]: + - @effect/ai@0.10.5 + - effect@3.13.5 + - @effect/experimental@0.41.5 + - @effect/platform@0.77.5 + +## 0.0.4 + +### Patch Changes + +- Updated dependencies [[`e0746f9`](https://github.com/Effect-TS/effect/commit/e0746f9aa398b69c6542e375910683bf17f49f46), [`17d9e89`](https://github.com/Effect-TS/effect/commit/17d9e89f9851663bdbb6c1e685601d97806114a4)]: + - @effect/platform@0.77.4 + - effect@3.13.4 + - @effect/ai@0.10.4 + - @effect/experimental@0.41.4 + +## 0.0.3 + +### Patch Changes + +- [#4504](https://github.com/Effect-TS/effect/pull/4504) [`a67a8a1`](https://github.com/Effect-TS/effect/commit/a67a8a1a4979fb7a039a060d067d805879da4d4b) Thanks @IMax153! - Introduce `AiModel` and `AiPlan` for describing retry / fallback logic between + models and providers + + For example, the following program builds an `AiPlan` which will attempt to use + OpenAi's chat completions API, and if after three attempts the operation + is still failing, the plan will fallback to utilizing Anthropic's messages API + to resolve the request. + + ```ts + import { AiPlan, Completions } from "@effect/ai" + import { AnthropicClient, AnthropicCompletions } from "@effect/ai-anthropic" + import { OpenAiClient, OpenAiCompletions } from "@effect/ai-openai" + import { NodeHttpClient, NodeRuntime } from "@effect/platform-node" + import { Config, Console, Effect, Layer } from "effect" + + // Create Anthropic client + const Anthropic = AnthropicClient.layerConfig({ + apiKey: Config.redacted("ANTHROPIC_API_KEY") + }).pipe(Layer.provide(NodeHttpClient.layerUndici)) + + // Create OpenAi client + const OpenAi = OpenAiClient.layerConfig({ + apiKey: Config.redacted("OPENAI_API_KEY") + }).pipe(Layer.provide(NodeHttpClient.layerUndici)) + + // Create a plan of request execution + const Plan = AiPlan.fromModel(OpenAiCompletions.model("gpt-4o-mini"), { + attempts: 3 + }).pipe( + AiPlan.withFallback({ + model: AnthropicCompletions.model("claude-3-5-haiku-latest") + }) + ) + + const program = Effect.gen(function* () { + // Build the plan of execution + const plan = yield* Plan + + // Create a program which uses the services provided by the plan + const getDadJoke = Effect.gen(function* () { + const completions = yield* Completions.Completions + const response = yield* completions.create("Tell me a dad joke") + yield* Console.log(response.text) + }) + + // Provide the plan to whichever programs need it + yield* plan.provide(getDadJoke) + }) + + program.pipe(Effect.provide([Anthropic, OpenAi]), NodeRuntime.runMain) + ``` + +- Updated dependencies [[`cc5588d`](https://github.com/Effect-TS/effect/commit/cc5588df07f9103513547cb429ce041b9436a8bd), [`623c8cd`](https://github.com/Effect-TS/effect/commit/623c8cd053ed6ee3d353aaa8778d484670fca2bb), [`00b4eb1`](https://github.com/Effect-TS/effect/commit/00b4eb1ece12a16e222e6220965bb4024d6752ac), [`f2aee98`](https://github.com/Effect-TS/effect/commit/f2aee989b0a600900ce83e7f460d02908620c80f), [`fb798eb`](https://github.com/Effect-TS/effect/commit/fb798eb9061f1191badc017d1aa649360254da20), [`2251b15`](https://github.com/Effect-TS/effect/commit/2251b1528810bb695b37ce388b653cec0c5bf80c), [`2e15c1e`](https://github.com/Effect-TS/effect/commit/2e15c1e33648add0b29fe274fbcb7294b7515085), [`a4979db`](https://github.com/Effect-TS/effect/commit/a4979db021aef16e731be64df196b72088fc4376), [`b74255a`](https://github.com/Effect-TS/effect/commit/b74255a304ad49d60bedb1a260fd697f370af27a), [`d7f6a5c`](https://github.com/Effect-TS/effect/commit/d7f6a5c7d26c1963dcd864ca62360d20d08c7b49), [`9dd8979`](https://github.com/Effect-TS/effect/commit/9dd8979e940915b1cc1b1f264f3d019c77a65a02), [`477b488`](https://github.com/Effect-TS/effect/commit/477b488284f47c5469d7fba3e4065fb7e3b6556e), [`10932cb`](https://github.com/Effect-TS/effect/commit/10932cbf58fc721ada631cebec42f773ce96d3cc), [`9f6c784`](https://github.com/Effect-TS/effect/commit/9f6c78468b3b5e9ebfc38ffdfb70702901ee977b), [`2c639ec`](https://github.com/Effect-TS/effect/commit/2c639ecee332de4266e36022c989c35ae4e02105), [`886aaa8`](https://github.com/Effect-TS/effect/commit/886aaa81e06dfd3cd9391e8ea987d8cd5ada1124), [`a67a8a1`](https://github.com/Effect-TS/effect/commit/a67a8a1a4979fb7a039a060d067d805879da4d4b)]: + - effect@3.13.3 + - @effect/ai@0.10.3 + - @effect/experimental@0.41.3 + - @effect/platform@0.77.3 + +## 0.0.2 + +### Patch Changes + +- Updated dependencies [[`31be72a`](https://github.com/Effect-TS/effect/commit/31be72ada118cb84a942e67b1663263f8db74a9f), [`3e7ce97`](https://github.com/Effect-TS/effect/commit/3e7ce97f8a41756a039cf635d0b3d9a75d781097), [`31be72a`](https://github.com/Effect-TS/effect/commit/31be72ada118cb84a942e67b1663263f8db74a9f)]: + - effect@3.13.2 + - @effect/platform@0.77.2 + - @effect/ai@0.10.2 + - @effect/experimental@0.41.2 + +## 0.0.1 + +### Patch Changes + +- [#4446](https://github.com/Effect-TS/effect/pull/4446) [`9375c28`](https://github.com/Effect-TS/effect/commit/9375c28ca808325577da6c67cc92af25931027c8) Thanks @IMax153! - Add Anthropic AI provider integration + +- Updated dependencies [[`b56a211`](https://github.com/Effect-TS/effect/commit/b56a2110569fd0ec0b57ac137743e926d49f51cc), [`9375c28`](https://github.com/Effect-TS/effect/commit/9375c28ca808325577da6c67cc92af25931027c8)]: + - effect@3.13.1 + - @effect/ai@0.10.1 + - @effect/experimental@0.41.1 + - @effect/platform@0.77.1 diff --git a/repos/effect/packages/ai/anthropic/LICENSE b/repos/effect/packages/ai/anthropic/LICENSE new file mode 100644 index 0000000..be1f5c1 --- /dev/null +++ b/repos/effect/packages/ai/anthropic/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Effectful Technologies Inc + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/repos/effect/packages/ai/anthropic/README.md b/repos/effect/packages/ai/anthropic/README.md new file mode 100644 index 0000000..65b3b34 --- /dev/null +++ b/repos/effect/packages/ai/anthropic/README.md @@ -0,0 +1,5 @@ +# `@effect/ai-anthropic` + +## Documentation + +- **API Reference**: [View the full documentation](https://effect-ts.github.io/effect/docs/ai/ai-anthropic). diff --git a/repos/effect/packages/ai/anthropic/docgen.json b/repos/effect/packages/ai/anthropic/docgen.json new file mode 100644 index 0000000..47d7a66 --- /dev/null +++ b/repos/effect/packages/ai/anthropic/docgen.json @@ -0,0 +1,26 @@ +{ + "$schema": "../../../node_modules/@effect/docgen/schema.json", + "exclude": ["src/Generated.ts", "src/internal/**/*.ts"], + "srcLink": "https://github.com/Effect-TS/effect/tree/main/packages/ai/anthropic/src/", + "examplesCompilerOptions": { + "noEmit": true, + "strict": true, + "skipLibCheck": true, + "moduleResolution": "Bundler", + "module": "ES2022", + "target": "ES2022", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "paths": { + "effect": ["../../../../effect/src/index.js"], + "effect/*": ["../../../../effect/src/*.js"], + "@effect/experimental": ["../../../../experimental/src/index.js"], + "@effect/experimental/*": ["../../../../experimental/src/*.js"], + "@effect/platform": ["../../../../platform/src/index.js"], + "@effect/platform/*": ["../../../../platform/src/*.js"], + "@effect/ai": ["../../../ai/src/index.js"], + "@effect/ai/*": ["../../../ai/src/*.js"], + "@effect/ai-anthropic": ["../../../ai-anthropic/src/index.js"], + "@effect/ai-anthropic/*": ["../../../ai-anthropic/src/*.js"] + } + } +} diff --git a/repos/effect/packages/ai/anthropic/package.json b/repos/effect/packages/ai/anthropic/package.json new file mode 100644 index 0000000..3ec6307 --- /dev/null +++ b/repos/effect/packages/ai/anthropic/package.json @@ -0,0 +1,65 @@ +{ + "name": "@effect/ai-anthropic", + "type": "module", + "version": "0.25.0", + "license": "MIT", + "description": "Effect modules for working with AI apis", + "homepage": "https://effect.website", + "repository": { + "type": "git", + "url": "https://github.com/Effect-TS/effect.git", + "directory": "packages/ai/anthropic" + }, + "bugs": { + "url": "https://github.com/Effect-TS/effect/issues" + }, + "tags": [ + "typescript", + "algebraic-data-types", + "functional-programming" + ], + "keywords": [ + "typescript", + "algebraic-data-types", + "functional-programming" + ], + "publishConfig": { + "access": "public", + "provenance": true, + "directory": "dist", + "linkDirectory": false + }, + "exports": { + "./package.json": "./package.json", + ".": "./src/index.ts", + "./*": "./src/*.ts", + "./internal/*": null + }, + "scripts": { + "codegen": "build-utils prepare-v3", + "build": "pnpm build-esm && pnpm build-annotate && pnpm build-cjs && build-utils pack-v3", + "build-esm": "tsc -b tsconfig.build.json", + "build-cjs": "babel build/esm --plugins @babel/transform-export-namespace-from --plugins @babel/transform-modules-commonjs --out-dir build/cjs --source-maps", + "build-annotate": "babel build/esm --plugins annotate-pure-calls --out-dir build/esm --source-maps", + "check": "tsc -b tsconfig.json", + "test": "vitest", + "coverage": "vitest --coverage" + }, + "peerDependencies": { + "@effect/ai": "workspace:^", + "@effect/experimental": "workspace:^", + "@effect/platform": "workspace:^", + "effect": "workspace:^" + }, + "devDependencies": { + "@effect/ai": "workspace:^", + "@effect/experimental": "workspace:^", + "@effect/platform": "workspace:^", + "@effect/platform-node": "workspace:^", + "@tim-smart/openapi-gen": "^0.4.10", + "effect": "workspace:^" + }, + "dependencies": { + "@anthropic-ai/tokenizer": "^0.0.4" + } +} diff --git a/repos/effect/packages/ai/anthropic/scripts/generate.sh b/repos/effect/packages/ai/anthropic/scripts/generate.sh new file mode 100755 index 0000000..e09b4c0 --- /dev/null +++ b/repos/effect/packages/ai/anthropic/scripts/generate.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash + +set -euo pipefail + +SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &> /dev/null && pwd) + +temp_dir=$(mktemp -d) + +cleanup() { + rm -rf "${temp_dir}" +} + +trap cleanup EXIT + +anthropic_stats_url="https://raw.githubusercontent.com/anthropics/anthropic-sdk-typescript/refs/heads/main/.stats.yml" + +openapi_spec_url="$(curl -sSL $anthropic_stats_url | yq ".openapi_spec_url")" + +curl "${openapi_spec_url}" > "${temp_dir}/anthropic.yaml" + +echo "/** + * @since 1.0.0 + */" > src/Generated.ts + +pnpm openapi-gen -s "${temp_dir}/anthropic.yaml" >> src/Generated.ts + +pnpm eslint --fix src/Generated.ts + +# No patch required at this time +git apply --reject --whitespace=fix "${SCRIPT_DIR}/generated.patch" diff --git a/repos/effect/packages/ai/anthropic/scripts/generated.patch b/repos/effect/packages/ai/anthropic/scripts/generated.patch new file mode 100644 index 0000000..2e325f7 --- /dev/null +++ b/repos/effect/packages/ai/anthropic/scripts/generated.patch @@ -0,0 +1,76 @@ +diff --git a/packages/ai/anthropic/src/Generated.ts b/packages/ai/anthropic/src/Generated.ts +index 9015e6427..dcb4afdad 100644 +--- a/packages/ai/anthropic/src/Generated.ts ++++ b/packages/ai/anthropic/src/Generated.ts +@@ -641,7 +641,7 @@ export class WebSearchTool20250305 extends S.Class("WebSe + }) {} + + export class CreateMessageParams extends S.Class("CreateMessageParams")({ +- "model": Model, ++ "model": S.Union(S.String, Model), + /** + * Input messages. + * +@@ -1114,7 +1114,7 @@ export class Message extends S.Class("Message")({ + * ``` + */ + "content": S.Array(ContentBlock), +- "model": Model, ++ "model": S.Union(S.String, Model), + /** + * The reason that we stopped. + * +@@ -1244,7 +1244,7 @@ export class CompletePostParams extends S.Struct({ + }) {} + + export class CompletionRequest extends S.Class("CompletionRequest")({ +- "model": Model, ++ "model": S.Union(S.String, Model), + /** + * The prompt that you want Claude to complete. + * +@@ -1316,7 +1316,7 @@ export class CompletionResponse extends S.Class("CompletionR + * The format and length of IDs may change over time. + */ + "id": S.String, +- "model": Model, ++ "model": S.Union(S.String, Model), + /** + * The reason that we stopped. + * +@@ -1730,7 +1730,7 @@ export class CountMessageTokensParams extends S.Class( + * There is a limit of 100,000 messages in a single request. + */ + "messages": S.Array(InputMessage), +- "model": Model, ++ "model": S.Union(S.String, Model), + /** + * System prompt. + * +@@ -3659,7 +3659,7 @@ export class BetaWebFetchTool20250910 extends S.Class( + }) {} + + export class BetaCreateMessageParams extends S.Class("BetaCreateMessageParams")({ +- "model": Model, ++ "model": S.Union(S.String, Model), + /** + * Input messages. + * +@@ -4517,7 +4517,7 @@ export class BetaMessage extends S.Class("BetaMessage")({ + * ``` + */ + "content": S.Array(BetaContentBlock), +- "model": Model, ++ "model": S.Union(S.String, Model), + /** + * The reason that we stopped. + * +@@ -5099,7 +5099,7 @@ export class BetaCountMessageTokensParams + * There is a limit of 100,000 messages in a single request. + */ + "messages": S.Array(BetaInputMessage), +- "model": Model, ++ "model": S.Union(S.String, Model), + /** + * System prompt. + * diff --git a/repos/effect/packages/ai/anthropic/src/AnthropicClient.ts b/repos/effect/packages/ai/anthropic/src/AnthropicClient.ts new file mode 100644 index 0000000..96ab40c --- /dev/null +++ b/repos/effect/packages/ai/anthropic/src/AnthropicClient.ts @@ -0,0 +1,778 @@ +/** + * @since 1.0.0 + */ +import * as AiError from "@effect/ai/AiError" +import * as Sse from "@effect/experimental/Sse" +import * as Headers from "@effect/platform/Headers" +import * as HttpBody from "@effect/platform/HttpBody" +import * as HttpClient from "@effect/platform/HttpClient" +import * as HttpClientError from "@effect/platform/HttpClientError" +import * as HttpClientRequest from "@effect/platform/HttpClientRequest" +import * as Arr from "effect/Array" +import * as Chunk from "effect/Chunk" +import * as Config from "effect/Config" +import type { ConfigError } from "effect/ConfigError" +import * as Context from "effect/Context" +import * as Effect from "effect/Effect" +import { identity } from "effect/Function" +import * as Layer from "effect/Layer" +import * as Redacted from "effect/Redacted" +import * as Schema from "effect/Schema" +import type * as Scope from "effect/Scope" +import * as Stream from "effect/Stream" +import { AnthropicConfig } from "./AnthropicConfig.js" +import * as Generated from "./Generated.js" + +/** + * @since 1.0.0 + * @category Context + */ +export class AnthropicClient extends Context.Tag( + "@effect/ai-anthropic/AnthropicClient" +)() {} + +/** + * Represents the interface that the `AnthropicClient` service provides. + * + * This service abstracts the complexity of communicating with Anthropic's API, + * providing both high-level text generation methods and low-level HTTP access + * for advanced use cases. + * + * @since 1.0.0 + * @category Models + */ +export interface Service { + /** + * The underlying HTTP client capable of communicating with the Anthropic API. + * + * This client is pre-configured with authentication, base URL, and standard + * headers required for Anthropic API communication. It provides direct access + * to the generated Anthropic API client for operations not covered by the + * higher-level methods. + * + * Use this when you need to: + * - Access provider-specific API endpoints not available through the AI SDK + * - Implement custom request/response handling + * - Use Anthropic API features not yet supported by the Effect AI abstractions + * - Perform batch operations or non-streaming requests + * + * The client automatically handles authentication and follows Anthropic's + * API conventions for request formatting and error handling. + */ + readonly client: Generated.Client + + readonly streamRequest: ( + request: HttpClientRequest.HttpClientRequest, + schema: Schema.Schema + ) => Stream.Stream + + readonly createMessage: (options: { + readonly params?: typeof Generated.BetaMessagesPostParams.Encoded | undefined + readonly payload: typeof Generated.BetaCreateMessageParams.Encoded + }) => Effect.Effect + + readonly createMessageStream: (options: { + readonly params?: typeof Generated.BetaMessagesPostParams.Encoded | undefined + readonly payload: Omit + }) => Stream.Stream +} + +/** + * @since 1.0.0 + * @category Constructors + */ +export const make: (options: { + /** + * The API key that will be used to authenticate with Anthropic's API. + * + * The key is wrapped in a `Redacted` type to prevent accidental logging or + * exposure in debugging output, helping maintain security best practices. + * + * The key is automatically included in the `x-api-key` header for all API + * requests made through this client, which is automatically redacted in logs + * output by Effect loggers. + * + * Leave `undefined` if authentication will be handled through other means + * (e.g., environment-based authentication, proxy authentication, or when + * using a mock server that doesn't require authentication). + */ + readonly apiKey?: Redacted.Redacted | undefined + + /** + * The base URL endpoint used to communicate with Anthropic's API. + * + * This property determines the HTTP destination for all API requests made by + * this client. + * + * Defaults to `"https://api.anthropic.com"`. + * + * Override this value when you need to: + * - Point to a different Anthropic environment (e.g., staging or sandbox + * servers). + * - Use a proxy between your application and Anthropic's API for security, + * caching, or logging. + * - Employ a mock server for local development or testing. + * + * You may leave this property `undefined` to accept the default value. + */ + readonly apiUrl?: string | undefined + + /** + * The Anthropic API version to use for requests. + * + * This version string determines which API schema and features are available + * for your requests. Different versions may have different capabilities, + * request/response formats, or available models. + * + * Defaults to `"2023-06-01"`. + * + * You should specify a version that: + * - Supports the features and models you need + * - Is stable and well-tested for your use case + * - Matches your application's integration requirements + * + * Consult Anthropic's API documentation for available versions and their + * differences. + */ + readonly anthropicVersion?: string | undefined + + /** + * The organization ID to associate with API requests. + * + * This identifier links requests to a specific organization within your + * Anthropic account, enabling proper billing, usage tracking, and access + * control at the organizational level. + * + * Provide this when: + * - Your account belongs to multiple organizations + * - You need to ensure requests are billed to the correct organization + * - Organization-level access policies apply to your use case + * + * Leave `undefined` if you're using a personal account or the default + * organization. + */ + readonly organizationId?: Redacted.Redacted | undefined + + /** + * The project ID to associate with API requests. + * + * This identifier scopes requests to a specific project within your + * organization, enabling granular resource management, billing allocation, + * and access control at the project level. + * + * Specify this when: + * - You have multiple projects and need to separate their API usage + * - Project-level billing or quota management is required + * - Access policies are configured at the project level + * + * Leave `undefined` to use the default project or when project-level + * scoping is not needed. + */ + readonly projectId?: Redacted.Redacted | undefined + + /** + * A function to transform the underlying HTTP client before it's used to send + * API requests. + * + * This transformation function receives the configured HTTP client and returns + * a modified version. It's applied after all standard client configuration + * (authentication, base URL, headers) but before any requests are made. + * + * Use this for: + * - Adding custom middleware (logging, metrics, caching) + * - Modifying request/response processing behavior + * - Adding custom retry logic or error handling + * - Integrating with monitoring or debugging tools + * - Applying organization-specific HTTP client policies + * + * The transformation is applied once during client initialization and affects + * all subsequent API requests made through this client instance. + * + * Leave absent or set to `undefined` if no custom HTTP client behavior is + * needed. + */ + readonly transformClient?: ((client: HttpClient.HttpClient) => HttpClient.HttpClient) | undefined +}) => Effect.Effect< + Service, + never, + HttpClient.HttpClient | Scope.Scope +> = Effect.fnUntraced(function*(options) { + const apiKeyHeader = "x-api-key" + + yield* Effect.locallyScopedWith(Headers.currentRedactedNames, Arr.append(apiKeyHeader)) + + const httpClient = (yield* HttpClient.HttpClient).pipe( + HttpClient.mapRequest((request) => + request.pipe( + HttpClientRequest.prependUrl(options.apiUrl ?? "https://api.anthropic.com"), + options.apiKey + ? HttpClientRequest.setHeader(apiKeyHeader, Redacted.value(options.apiKey)) + : identity, + HttpClientRequest.setHeader("anthropic-version", options.anthropicVersion ?? "2023-06-01"), + HttpClientRequest.acceptJson + ) + ), + options.transformClient ? options.transformClient : identity + ) + + const httpClientOk = HttpClient.filterStatusOk(httpClient) + + const client = Generated.make(httpClient, { + transformClient: (client) => + AnthropicConfig.getOrUndefined.pipe( + Effect.map((config) => config?.transformClient ? config.transformClient(client) : client) + ) + }) + + const streamRequest = ( + request: HttpClientRequest.HttpClientRequest, + schema: Schema.Schema + ): Stream.Stream => { + const decodeEvents = Schema.decode(Schema.ChunkFromSelf(Schema.parseJson(schema))) + return httpClientOk.execute(request).pipe( + Effect.map((r) => r.stream), + Stream.unwrapScoped, + Stream.decodeText(), + Stream.pipeThroughChannel(Sse.makeChannel()), + Stream.mapChunksEffect((chunk) => decodeEvents(Chunk.map(chunk, (event) => event.data))), + Stream.catchTags({ + RequestError: (error) => + AiError.HttpRequestError.fromRequestError({ + module: "AnthropicClient", + method: "streamRequest", + error + }), + ResponseError: (error) => + AiError.HttpResponseError.fromResponseError({ + module: "AnthropicClient", + method: "streamRequest", + error + }), + ParseError: (error) => + AiError.MalformedOutput.fromParseError({ + module: "AnthropicClient", + method: "streamRequest", + error + }) + }) + ) + } + + const createMessage: (options: { + readonly params?: typeof Generated.BetaMessagesPostParams.Encoded | undefined + readonly payload: typeof Generated.BetaCreateMessageParams.Encoded + }) => Effect.Effect = Effect.fnUntraced( + function*(options) { + return yield* client.betaMessagesPost(options).pipe( + Effect.catchTags({ + RequestError: (error) => + AiError.HttpRequestError.fromRequestError({ + module: "AnthropicClient", + method: "createMessage", + error + }), + ResponseError: (error) => + AiError.HttpResponseError.fromResponseError({ + module: "AnthropicClient", + method: "createMessage", + error + }), + BetaErrorResponse: (error) => + AiError.HttpResponseError.fromResponseError({ + module: "AnthropicClient", + method: "createMessage", + error: new HttpClientError.ResponseError({ + reason: "StatusCode", + request: error.request, + response: error.response + }) + }), + ParseError: (error) => + AiError.MalformedOutput.fromParseError({ + module: "AnthropicClient", + method: "createMessage", + error + }) + }) + ) + } + ) + + const createMessageStream = (options: { + readonly params?: typeof Generated.BetaMessagesPostParams.Encoded | undefined + readonly payload: Omit + }): Stream.Stream => { + const request = HttpClientRequest.post("/v1/messages", { + headers: Headers.fromInput({ + "anthropic-beta": options.params?.["anthropic-beta"] ?? undefined + }), + body: HttpBody.unsafeJson({ ...options.payload, stream: true }) + }) + return streamRequest(request, MessageStreamEvent).pipe( + Stream.takeUntil((event) => event.type === "message_stop") + ) + } + + return AnthropicClient.of({ + client, + streamRequest, + createMessage, + createMessageStream + }) +}) + +// ============================================================================= +// Message Stream Schema +// ============================================================================= + +/** + * @since 1.0.0 + * @category Schemas + */ +export class PingEvent extends Schema.Class( + "@effect/ai-anthropic/PingEvent" +)({ + type: Schema.Literal("ping") +}) {} + +/** + * @since 1.0.0 + * @category Schemas + */ +export class ErrorEvent extends Schema.Class( + "@effect/ai-anthropic/ErrorEvent" +)({ + type: Schema.Literal("error"), + error: Schema.Struct({ + type: Schema.Literal( + "invalid_request_error", + "authentication_error", + "permission_error", + "not_found_error", + "request_too_large", + "rate_limit_error", + "api_error", + "overloaded_error" + ), + message: Schema.String + }) +}) {} + +/** + * @since 1.0.0 + * @category Schemas + */ +export class MessageStartEvent extends Schema.Class( + "@effect/ai-anthropic/MessageStartEvent" +)({ + type: Schema.Literal("message_start"), + message: Generated.BetaMessage +}) {} + +/** + * @since 1.0.0 + * @category Schemas + */ +export class ServerToolUsage extends Schema.Class( + "@effect/ai-anthropic/ServerToolUsage" +)({ + /** + * The number of web search tool requests. + */ + web_search_requests: Schema.optionalWith( + Schema.NullOr(Schema.Int.pipe(Schema.greaterThanOrEqualTo(0))), + { default: () => 0 } + ) +}) {} + +/** + * @since 1.0.0 + * @category Schemas + */ +export class MessageDelta extends Schema.Class( + "@effect/ai-anthropic/MessageDelta" +)({ + stop_reason: Schema.optionalWith( + Schema.NullOr( + Schema.Literal( + "end_turn", + "max_tokens", + "stop_sequence", + "tool_use", + "pause_turn", + "refusal" + ) + ), + { default: () => null } + ), + stop_sequence: Schema.optionalWith( + Schema.NullOr(Schema.String), + { default: () => null } + ) +}) {} + +/** + * @since 1.0.0 + * @category Schemas + */ +export class MessageDeltaUsage extends Schema.Class( + "@effect/ai-anthropic/MessageDeltaUsage" +)({ + /** + * The cumulative number of input tokens which were used. + */ + input_tokens: Schema.optionalWith( + Schema.NullOr(Schema.Int.pipe(Schema.greaterThanOrEqualTo(0))), + { default: () => null } + ), + /** + * The cumulative number of output tokens which were used. + */ + output_tokens: Schema.optionalWith( + Schema.NullOr(Schema.Int.pipe(Schema.greaterThanOrEqualTo(0))), + { default: () => null } + ), + /** + * The cumulative number of input tokens used to create the cache entry. + */ + cache_creation_input_tokens: Schema.optionalWith( + Schema.NullOr(Schema.Int.pipe(Schema.greaterThanOrEqualTo(0))), + { default: () => null } + ), + /** + * The cumulative number of input tokens read from the cache. + */ + cache_read_input_tokens: Schema.optionalWith( + Schema.NullOr(Schema.Int.pipe(Schema.greaterThanOrEqualTo(0))), + { default: () => null } + ), + /** + * The number of server tool requests. + */ + server_tool_use: Schema.optionalWith( + Schema.NullOr(ServerToolUsage), + { default: () => null } + ) +}) {} + +/** + * @since 1.0.0 + * @category Schemas + */ +export class MessageDeltaEvent extends Schema.Class( + "@effect/ai-anthropic/MessageDeltaEvent" +)({ + type: Schema.Literal("message_delta"), + delta: MessageDelta, + /** + * Billing and rate-limit usage. + * + * Anthropic's API bills and rate-limits by token counts, as tokens represent + * the underlying cost to our systems. + * + * Under the hood, the API transforms requests into a format suitable for the + * model. The model's output then goes through a parsing stage before becoming + * an API response. As a result, the token counts in `usage` will not match + * one-to-one with the exact visible content of an API request or response. + * + * For example, `output_tokens` will be non-zero, even for an empty string + * response from Claude.\n\nTotal input tokens in a request is the summation + * of `input_tokens`, `cache_creation_input_tokens`, and `cache_read_input_tokens`. + */ + usage: MessageDeltaUsage +}) {} + +/** + * @since 1.0.0 + * @category Schemas + */ +export class MessageStopEvent extends Schema.Class( + "@effect/ai-anthropic/MessageStopEvent" +)({ + type: Schema.Literal("message_stop") +}) {} + +/** + * @since 1.0.0 + * @category Schemas + */ +export class ContentBlockStartEvent extends Schema.Class( + "@effect/ai-anthropic/ContentBlockStartEvent" +)({ + type: Schema.Literal("content_block_start"), + index: Schema.Int, + content_block: Generated.BetaContentBlock +}) {} + +/** + * @since 1.0.0 + * @category Schemas + */ +export class CitationsDelta extends Schema.Class( + "@effect/ai-anthropic/CitationsDelta" +)({ + type: Schema.Literal("citations_delta"), + citation: Schema.Union( + Generated.BetaResponseCharLocationCitation, + Generated.BetaResponsePageLocationCitation, + Generated.BetaResponseContentBlockLocationCitation, + Generated.BetaResponseWebSearchResultLocationCitation, + Generated.BetaResponseSearchResultLocationCitation + ) +}) {} + +/** + * @since 1.0.0 + * @category Schemas + */ +export class InputJsonContentBlockDelta extends Schema.Class( + "@effect/ai-anthropic/InputJsonContentBlockDelta" +)({ + type: Schema.Literal("input_json_delta"), + partial_json: Schema.String +}) {} + +/** + * @since 1.0.0 + * @category Schemas + */ +export class SignatureContentBlockDelta extends Schema.Class( + "@effect/ai-anthropic/SignatureContentBlockDelta" +)({ + type: Schema.Literal("signature_delta"), + signature: Schema.String +}) {} + +/** + * @since 1.0.0 + * @category Schemas + */ +export class TextContentBlockDelta extends Schema.Class( + "@effect/ai-anthropic/TextContentBlockDelta" +)({ + type: Schema.Literal("text_delta"), + text: Schema.String +}) {} + +/** + * @since 1.0.0 + * @category Schemas + */ +export class ThinkingContentBlockDelta extends Schema.Class( + "@effect/ai-anthropic/ThinkingContentBlockDelta" +)({ + type: Schema.Literal("thinking_delta"), + thinking: Schema.String +}) {} + +/** + * @since 1.0.0 + * @category Schemas + */ +export class ContentBlockDeltaEvent extends Schema.Class( + "@effect/ai-anthropic/ContentBlockDeltaEvent" +)({ + type: Schema.Literal("content_block_delta"), + index: Schema.Int, + delta: Schema.Union( + CitationsDelta, + InputJsonContentBlockDelta, + SignatureContentBlockDelta, + TextContentBlockDelta, + ThinkingContentBlockDelta + ) +}) {} + +/** + * @since 1.0.0 + * @category Schemas + */ +export class ContentBlockStopEvent extends Schema.Class( + "@effect/ai-anthropic/ContentBlockStopEvent" +)({ + type: Schema.Literal("content_block_stop"), + index: Schema.Int +}) {} + +/** + * @since 1.0.0 + * @category Schemas + */ +export const MessageStreamEvent = Schema.Union( + PingEvent, + ErrorEvent, + MessageStartEvent, + MessageDeltaEvent, + MessageStopEvent, + ContentBlockStartEvent, + ContentBlockDeltaEvent, + ContentBlockStopEvent +) + +/** + * @since 1.0.0 + * @category Models + */ +export type MessageStreamEvent = typeof MessageStreamEvent.Type + +/** + * @since 1.0.0 + * @category Layers + */ +export const layer = (options: { + /** + * The API key that will be used to authenticate with Anthropic's API. + * + * The key is wrapped in a `Redacted` type to prevent accidental logging or + * exposure in debugging output, helping maintain security best practices. + * + * The key is automatically included in the `x-api-key` header for all API + * requests made through this client, which is automatically redacted in logs + * output by Effect loggers. + * + * Leave `undefined` if authentication will be handled through other means + * (e.g., environment-based authentication, proxy authentication, or when + * using a mock server that doesn't require authentication). + */ + readonly apiKey?: Redacted.Redacted | undefined + /** + * The base URL endpoint used to communicate with Anthropic's API. + * + * This property determines the HTTP destination for all API requests made by + * this client. + * + * Defaults to `"https://api.anthropic.com"`. + * + * Override this value when you need to: + * - Point to a different Anthropic environment (e.g., staging or sandbox + * servers). + * - Use a proxy between your application and Anthropic's API for security, + * caching, or logging. + * - Employ a mock server for local development or testing. + * + * You may leave this property `undefined` to accept the default value. + */ + readonly apiUrl?: string | undefined + /** + * The Anthropic API version to use for requests. + * + * This version string determines which API schema and features are available + * for your requests. Different versions may have different capabilities, + * request/response formats, or available models. + * + * Defaults to `"2023-06-01"`. + * + * You should specify a version that: + * - Supports the features and models you need + * - Is stable and well-tested for your use case + * - Matches your application's integration requirements + * + * Consult Anthropic's API documentation for available versions and their + * differences. + */ + readonly anthropicVersion?: string | undefined + /** + * A function to transform the underlying HTTP client before it's used for API requests. + * + * This transformation function receives the configured HTTP client and returns + * a modified version. It's applied after all standard client configuration + * (authentication, base URL, headers) but before any requests are made. + * + * Use this for: + * - Adding custom middleware (logging, metrics, caching) + * - Modifying request/response processing behavior + * - Adding custom retry logic or error handling + * - Integrating with monitoring or debugging tools + * - Applying organization-specific HTTP client policies + * + * The transformation is applied once during client initialization and affects + * all subsequent API requests made through this client instance. + * + * Leave `undefined` if no custom HTTP client behavior is needed. + */ + readonly transformClient?: ((client: HttpClient.HttpClient) => HttpClient.HttpClient) | undefined +}): Layer.Layer => Layer.scoped(AnthropicClient, make(options)) + +/** + * @since 1.0.0 + * @category Layers + */ +export const layerConfig = (options: { + /** + * The API key that will be used to authenticate with Anthropic's API. + * + * The key is wrapped in a `Redacted` type to prevent accidental logging or + * exposure in debugging output, helping maintain security best practices. + * + * The key is automatically included in the `x-api-key` header for all API + * requests made through this client, which is automatically redacted in logs + * output by Effect loggers. + * + * Leave `undefined` if authentication will be handled through other means + * (e.g., environment-based authentication, proxy authentication, or when + * using a mock server that doesn't require authentication). + */ + readonly apiKey?: Config.Config | undefined + /** + * The base URL endpoint used to communicate with Anthropic's API. + * + * This property determines the HTTP destination for all API requests made by + * this client. + * + * Defaults to `"https://api.anthropic.com"`. + * + * Override this value when you need to: + * - Point to a different Anthropic environment (e.g., staging or sandbox + * servers). + * - Use a proxy between your application and Anthropic's API for security, + * caching, or logging. + * - Employ a mock server for local development or testing. + * + * You may leave this property `undefined` to accept the default value. + */ + readonly apiUrl?: Config.Config | undefined + /** + * The Anthropic API version to use for requests. + * + * This version string determines which API schema and features are available + * for your requests. Different versions may have different capabilities, + * request/response formats, or available models. + * + * Defaults to `"2023-06-01"`. + * + * You should specify a version that: + * - Supports the features and models you need + * - Is stable and well-tested for your use case + * - Matches your application's integration requirements + * + * Consult Anthropic's API documentation for available versions and their + * differences. + */ + readonly anthropicVersion?: Config.Config | undefined + /** + * A function to transform the underlying HTTP client before it's used for API requests. + * + * This transformation function receives the configured HTTP client and returns + * a modified version. It's applied after all standard client configuration + * (authentication, base URL, headers) but before any requests are made. + * + * Use this for: + * - Adding custom middleware (logging, metrics, caching) + * - Modifying request/response processing behavior + * - Adding custom retry logic or error handling + * - Integrating with monitoring or debugging tools + * - Applying organization-specific HTTP client policies + * + * The transformation is applied once during client initialization and affects + * all subsequent API requests made through this client instance. + * + * Leave `undefined` if no custom HTTP client behavior is needed. + */ + readonly transformClient?: ((client: HttpClient.HttpClient) => HttpClient.HttpClient) | undefined +}): Layer.Layer => { + const { transformClient, ...configs } = options + return Config.all(configs).pipe( + Effect.flatMap((configs) => make({ ...configs, transformClient })), + Layer.scoped(AnthropicClient) + ) +} diff --git a/repos/effect/packages/ai/anthropic/src/AnthropicConfig.ts b/repos/effect/packages/ai/anthropic/src/AnthropicConfig.ts new file mode 100644 index 0000000..b6fde44 --- /dev/null +++ b/repos/effect/packages/ai/anthropic/src/AnthropicConfig.ts @@ -0,0 +1,56 @@ +/** + * @since 1.0.0 + */ +import type { HttpClient } from "@effect/platform/HttpClient" +import * as Context from "effect/Context" +import * as Effect from "effect/Effect" +import { dual } from "effect/Function" + +/** + * @since 1.0.0 + * @category Context + */ +export class AnthropicConfig extends Context.Tag("@effect/ai-anthropic/AnthropicConfig")< + AnthropicConfig, + AnthropicConfig.Service +>() { + /** + * @since 1.0.0 + */ + static readonly getOrUndefined: Effect.Effect = Effect.map( + Effect.context(), + (context) => context.unsafeMap.get(AnthropicConfig.key) + ) +} + +/** + * @since 1.0.0 + */ +export declare namespace AnthropicConfig { + /** + * @since 1.0.0 + * @category Models + */ + export interface Service { + readonly transformClient?: (client: HttpClient) => HttpClient + } +} + +/** + * @since 1.0.0 + * @category Configuration + */ +export const withClientTransform: { + (transform: (client: HttpClient) => HttpClient): (self: Effect.Effect) => Effect.Effect + (self: Effect.Effect, transform: (client: HttpClient) => HttpClient): Effect.Effect +} = dual< + (transform: (client: HttpClient) => HttpClient) => (self: Effect.Effect) => Effect.Effect, + (self: Effect.Effect, transform: (client: HttpClient) => HttpClient) => Effect.Effect +>( + 2, + (self, transformClient) => + Effect.flatMap( + AnthropicConfig.getOrUndefined, + (config) => Effect.provideService(self, AnthropicConfig, { ...config, transformClient }) + ) +) diff --git a/repos/effect/packages/ai/anthropic/src/AnthropicLanguageModel.ts b/repos/effect/packages/ai/anthropic/src/AnthropicLanguageModel.ts new file mode 100644 index 0000000..c9ee13b --- /dev/null +++ b/repos/effect/packages/ai/anthropic/src/AnthropicLanguageModel.ts @@ -0,0 +1,1756 @@ +/** + * @since 1.0.0 + */ +import * as AiError from "@effect/ai/AiError" +import * as IdGenerator from "@effect/ai/IdGenerator" +import * as LanguageModel from "@effect/ai/LanguageModel" +import * as AiModel from "@effect/ai/Model" +import type * as Prompt from "@effect/ai/Prompt" +import type * as Response from "@effect/ai/Response" +import { addGenAIAnnotations } from "@effect/ai/Telemetry" +import type * as Tokenizer from "@effect/ai/Tokenizer" +import * as Tool from "@effect/ai/Tool" +import * as Arr from "effect/Array" +import * as Context from "effect/Context" +import * as DateTime from "effect/DateTime" +import * as Effect from "effect/Effect" +import * as Encoding from "effect/Encoding" +import { dual } from "effect/Function" +import * as Layer from "effect/Layer" +import * as Predicate from "effect/Predicate" +import * as Stream from "effect/Stream" +import type { Span } from "effect/Tracer" +import type { Mutable, Simplify } from "effect/Types" +import { AnthropicClient, type MessageStreamEvent } from "./AnthropicClient.js" +import * as AnthropicTokenizer from "./AnthropicTokenizer.js" +import * as AnthropicTool from "./AnthropicTool.js" +import type * as Generated from "./Generated.js" +import * as InternalUtilities from "./internal/utilities.js" + +/** + * @since 1.0.0 + * @category Models + */ +export type Model = typeof Generated.Model.Encoded + +// ============================================================================= +// Configuration +// ============================================================================= + +/** + * @since 1.0.0 + * @category Context + */ +export class Config extends Context.Tag("@effect/ai-anthropic/AnthropicLanguageModel/Config")< + Config, + Config.Service +>() { + /** + * @since 1.0.0 + */ + static readonly getOrUndefined: Effect.Effect = Effect.map( + Effect.context(), + (context) => context.unsafeMap.get(Config.key) + ) +} + +/** + * @since 1.0.0 + */ +export declare namespace Config { + /** + * @since 1.0.0 + * @category Configuration + */ + export interface Service extends + Simplify< + Partial< + Omit< + typeof Generated.CreateMessageParams.Encoded, + "messages" | "tools" | "tool_choice" | "stream" + > + > + > + { + readonly disableParallelToolCalls?: boolean + } +} + +// ============================================================================= +// Anthropic Provider Options / Metadata +// ============================================================================= + +/** + * @since 1.0.0 + * @category Provider Metadata + */ +export type AnthropicReasoningInfo = { + readonly type: "thinking" + /** + * Thinking content as an encrypted string, which is used to verify + * that thinking content was indeed generated by Anthropic's API. + */ + readonly signature: typeof Generated.ResponseThinkingBlock.fields.thinking.Encoded +} | { + readonly type: "redacted_thinking" + /** + * Thinking content which was flagged by Anthropic's safety systems, and + * was therefore encrypted. + */ + readonly redactedData: typeof Generated.RequestRedactedThinkingBlock.fields.data.Encoded +} + +/** + * @since 1.0.0 + * @category Provider Options + */ +declare module "@effect/ai/Prompt" { + export interface SystemMessageOptions extends ProviderOptions { + readonly anthropic?: { + /** + * A breakpoint which marks the end of reusable content eligible for caching. + */ + readonly cacheControl?: typeof Generated.CacheControlEphemeral.Encoded | undefined + } | undefined + } + + export interface UserMessageOptions extends ProviderOptions { + readonly anthropic?: { + /** + * A breakpoint which marks the end of reusable content eligible for caching. + */ + readonly cacheControl?: typeof Generated.CacheControlEphemeral.Encoded | undefined + } | undefined + } + + export interface AssistantMessageOptions extends ProviderOptions { + readonly anthropic?: { + /** + * A breakpoint which marks the end of reusable content eligible for caching. + */ + readonly cacheControl?: typeof Generated.CacheControlEphemeral.Encoded | undefined + } | undefined + } + + export interface ToolMessageOptions extends ProviderOptions { + readonly anthropic?: { + /** + * A breakpoint which marks the end of reusable content eligible for caching. + */ + readonly cacheControl?: typeof Generated.CacheControlEphemeral.Encoded | undefined + } | undefined + } + + export interface TextPartOptions extends ProviderOptions { + readonly anthropic?: { + /** + * A breakpoint which marks the end of reusable content eligible for caching. + */ + readonly cacheControl?: typeof Generated.CacheControlEphemeral.Encoded | undefined + } | undefined + } + + export interface ReasoningPartOptions extends ProviderOptions { + readonly anthropic?: + | Simplify< + AnthropicReasoningInfo & { + /** + * A breakpoint which marks the end of reusable content eligible for caching. + */ + readonly cacheControl?: typeof Generated.CacheControlEphemeral.Encoded | undefined + } + > + | undefined + } + + export interface FilePartOptions extends ProviderOptions { + readonly anthropic?: { + /** + * A breakpoint which marks the end of reusable content eligible for caching. + */ + readonly cacheControl?: typeof Generated.CacheControlEphemeral.Encoded | undefined + /** + * Whether or not citations should be enabled for the file part. + */ + readonly citations?: typeof Generated.RequestCitationsConfig.Encoded | undefined + /** + * A custom title to provide to the document. If omitted, the file part's + * `fileName` property will be used. + */ + readonly documentTitle?: string | undefined + /** + * Additional context about the document that will be forwarded to the + * large language model, but will not be used towards cited content. + * + * Useful for storing additional document metadata as text or stringified JSON. + */ + readonly documentContext?: string | undefined + } | undefined + } + + export interface ToolCallPartOptions extends ProviderOptions { + readonly anthropic?: { + /** + * A breakpoint which marks the end of reusable content eligible for caching. + */ + readonly cacheControl?: typeof Generated.CacheControlEphemeral.Encoded | undefined + } | undefined + } + + export interface ToolResultPartOptions extends ProviderOptions { + readonly anthropic?: { + /** + * A breakpoint which marks the end of reusable content eligible for caching. + */ + readonly cacheControl?: typeof Generated.CacheControlEphemeral.Encoded | undefined + } | undefined + } +} + +declare module "@effect/ai/Response" { + export interface ReasoningPartMetadata extends ProviderMetadata { + readonly anthropic?: AnthropicReasoningInfo | undefined + } + + export interface ReasoningStartPartMetadata extends ProviderMetadata { + readonly anthropic?: AnthropicReasoningInfo | undefined + } + + export interface ReasoningDeltaPartMetadata extends ProviderMetadata { + readonly anthropic?: AnthropicReasoningInfo | undefined + } + + export interface FinishPartMetadata extends ProviderMetadata { + readonly anthropic?: { + /** + * Additional usage information provided by the Anthropic API. + */ + readonly usage?: Generated.BetaUsage | undefined + /** + * Which custom stop sequence was generated, if any. + * + * If one of the custom user-defined stop sequences was generated, the + * value will be a `string` with that stop sequence. + */ + readonly stopSequence?: string | undefined + } | undefined + } + + export interface DocumentSourcePartMetadata extends ProviderMetadata { + readonly anthropic?: { + readonly source: "document" + readonly type: "char_location" + /** + * The text that was cited in the response. + */ + readonly citedText: string + /** + * The 0-indexed starting position of the characters that were cited. + */ + readonly startCharIndex: number + /** + * The exclusive ending position of the characters that were cited. + */ + readonly endCharIndex: number + } | { + readonly source: "document" + readonly type: "page_location" + /** + * The text that was cited in the response. + */ + readonly citedText: string + /** + * The 1-indexed starting page of pages that were cited. + */ + readonly startPageNumber: number + /** + * The exclusive ending position of the pages that were cited. + */ + readonly endPageNumber: number + } | undefined + } + + export interface UrlSourcePartMetadata extends ProviderMetadata { + readonly anthropic?: { + readonly source: "url" + /** + * Up to 150 characters of the text content that was referenced from the + * URL source material. + */ + readonly citedText: string + /** + * An internal reference that must be passed back to the Anthropic API + * during multi-turn conversations. + */ + readonly encryptedIndex: string + } | undefined + } +} + +// ============================================================================= +// Anthropic Language Model +// ============================================================================= + +/** + * @since 1.0.0 + * @category Ai Models + */ +export const model = ( + model: (string & {}) | Model, + config?: Omit +): AiModel.Model<"anthropic", LanguageModel.LanguageModel, AnthropicClient> => + AiModel.make("anthropic", layer({ model, config })) + +/** + * @since 1.0.0 + * @category Ai Models + */ +export const modelWithTokenizer = ( + model: (string & {}) | Model, + config?: Omit +): AiModel.Model<"anthropic", LanguageModel.LanguageModel | Tokenizer.Tokenizer, AnthropicClient> => + AiModel.make("anthropic", layerWithTokenizer({ model, config })) + +/** + * @since 1.0.0 + * @category Constructors + */ +export const make = Effect.fnUntraced(function*(options: { + readonly model: (string & {}) | Model + readonly config?: Omit +}) { + const client = yield* AnthropicClient + + const makeRequest = Effect.fnUntraced( + function*(providerOptions: LanguageModel.ProviderOptions) { + const context = yield* Effect.context() + const config = { model: options.model, ...options.config, ...context.unsafeMap.get(Config.key) } + const { betas: messageBetas, messages, system } = yield* prepareMessages(providerOptions) + const { betas: toolBetas, toolChoice, tools } = yield* prepareTools(providerOptions, config) + const responseFormat = providerOptions.responseFormat + const request: typeof Generated.BetaCreateMessageParams.Encoded = { + max_tokens: 4096, + ...config, + system, + messages, + tools: responseFormat.type === "text" + ? tools + : [{ + name: responseFormat.objectName, + description: Tool.getDescriptionFromSchemaAst(responseFormat.schema.ast) ?? "Respond with a JSON object", + input_schema: Tool.getJsonSchemaFromSchemaAst(responseFormat.schema.ast) as any + }], + tool_choice: responseFormat.type === "text" + ? toolChoice + : { + type: "tool", + name: responseFormat.objectName, + disable_parallel_tool_use: true + } + } + return { betas: new Set([...messageBetas, ...toolBetas]), request } + } + ) + + return yield* LanguageModel.make({ + generateText: Effect.fnUntraced( + function*(options) { + const { betas, request } = yield* makeRequest(options) + annotateRequest(options.span, request) + const anthropicBeta = betas.size > 0 ? Array.from(betas).join(",") : undefined + const rawResponse = yield* client.createMessage({ + params: { "anthropic-beta": anthropicBeta }, + payload: request + }) + annotateResponse(options.span, rawResponse) + return yield* makeResponse(rawResponse, options) + } + ), + streamText: Effect.fnUntraced( + function*(options) { + const { betas, request } = yield* makeRequest(options) + annotateRequest(options.span, request) + const anthropicBeta = betas.size > 0 ? Array.from(betas).join(",") : undefined + return client.createMessageStream({ + params: { "anthropic-beta": anthropicBeta }, + payload: request + }) + }, + (effect, options) => + effect.pipe( + Effect.flatMap((stream) => makeStreamResponse(stream, options)), + Stream.unwrap, + Stream.map((response) => { + annotateStreamResponse(options.span, response) + return response + }) + ) + ) + }) +}) + +/** + * @since 1.0.0 + * @category Layers + */ +export const layer = (options: { + readonly model: (string & {}) | Model + readonly config?: Omit +}): Layer.Layer => + Layer.effect(LanguageModel.LanguageModel, make({ model: options.model, config: options.config })) + +/** + * @since 1.0.0 + * @category Layers + */ +export const layerWithTokenizer = (options: { + readonly model: (string & {}) | Model + readonly config?: Omit +}): Layer.Layer => + Layer.merge(layer(options), AnthropicTokenizer.layer) + +/** + * @since 1.0.0 + * @category Configuration + */ +export const withConfigOverride: { + (config: Config.Service): (self: Effect.Effect) => Effect.Effect + (self: Effect.Effect, config: Config.Service): Effect.Effect +} = dual< + (config: Config.Service) => (self: Effect.Effect) => Effect.Effect, + (self: Effect.Effect, config: Config.Service) => Effect.Effect +>(2, (self, overrides) => + Effect.flatMap( + Config.getOrUndefined, + (config) => Effect.provideService(self, Config, { ...config, ...overrides }) + )) + +// ============================================================================= +// Prompt Conversion +// ============================================================================= + +const prepareMessages: (options: LanguageModel.ProviderOptions) => Effect.Effect<{ + readonly betas: ReadonlySet + readonly system: ReadonlyArray | undefined + readonly messages: ReadonlyArray +}, AiError.AiError> = Effect.fnUntraced(function*(options) { + const betas = new Set() + const groups = groupMessages(options.prompt) + + let system: Array | undefined = undefined + const messages: Array = [] + + for (let i = 0; i < groups.length; i++) { + const group = groups[i] + const isLastGroup = i === groups.length - 1 + + switch (group.type) { + case "system": { + system = group.messages.map((message) => ({ + type: "text", + text: message.content, + cache_control: getCacheControl(message) + })) + break + } + + case "user": { + const content: Array = [] + + for (const message of group.messages) { + switch (message.role) { + case "user": { + for (let j = 0; j < message.content.length; j++) { + const part = message.content[j] + const isLastPart = j === message.content.length - 1 + + // Attempt to get the cache control from the part first. If + // the part does not have cache control defined and we are + // evaluating the last part for this message, also check the + // message for cache control. + const cacheControl = getCacheControl(part) ?? ( + isLastPart ? getCacheControl(message) : undefined + ) + + switch (part.type) { + case "text": { + content.push({ + type: "text", + text: part.text, + cache_control: cacheControl + }) + break + } + + case "file": { + if (part.mediaType.startsWith("image/")) { + const source = part.data instanceof URL ? + { + type: "url", + url: part.data.toString() + } as const : + { + type: "base64", + media_type: part.mediaType === "image/*" + ? "image/jpeg" + : (part.mediaType as typeof Generated.Base64ImageSourceMediaType.Encoded), + data: typeof part.data === "string" ? part.data : Encoding.encodeBase64(part.data) + } as const + + content.push({ + type: "image", + source, + cache_control: cacheControl + }) + } else if (part.mediaType === "application/pdf" || part.mediaType === "text/plain") { + if (part.mediaType === "application/pdf") { + betas.add("pdfs-2024-09-25") + } + + const enableCitations = shouldEnableCitations(part) + const documentOptions = getDocumentMetadata(part) + + const source = part.data instanceof URL + ? { + type: "url", + url: part.data.toString() + } as const + : part.mediaType === "application/pdf" + ? { + type: "base64", + media_type: "application/pdf", + data: typeof part.data === "string" + ? part.data + : Encoding.encodeBase64(part.data) + } as const + : { + type: "text", + media_type: "text/plain", + data: typeof part.data === "string" + ? part.data + : Encoding.encodeBase64(part.data) + } as const + + content.push({ + type: "document", + source, + title: documentOptions?.title ?? part.fileName, + ...(documentOptions?.context ? { context: documentOptions.context } : undefined), + ...(enableCitations ? { citations: { enabled: true } } : undefined), + cache_control: cacheControl + }) + } else { + return yield* new AiError.MalformedInput({ + module: "AnthropicLanguageModel", + method: "prepareMessages", + description: `Detected unsupported media type for file: '${part.mediaType}'` + }) + } + break + } + } + } + + break + } + + // TODO: advanced tool result content parts + case "tool": { + for (let j = 0; j < message.content.length; j++) { + const part = message.content[j] + const isLastPart = j === message.content.length - 1 + + // Attempt to get the cache control from the part first. If + // the part does not have cache control defined and we are + // evaluating the last part for this message, also check the + // message for cache control. + const cacheControl = getCacheControl(part) ?? ( + isLastPart ? getCacheControl(message) : undefined + ) + + content.push({ + type: "tool_result", + tool_use_id: part.id, + content: JSON.stringify(part.result), + is_error: part.isFailure, + cache_control: cacheControl + }) + } + + break + } + } + } + + messages.push({ role: "user", content }) + + break + } + + case "assistant": { + const content: Array = [] + + for (let j = 0; j < group.messages.length; j++) { + const message = group.messages[j] + const isLastMessage = j === group.messages.length - 1 + + for (let k = 0; k < message.content.length; k++) { + const part = message.content[k] + const isLastPart = k === message.content.length - 1 + + // Attempt to get the cache control from the part first. If + // the part does not have cache control defined and we are + // evaluating the last part for this message, also check the + // message for cache control. + const cacheControl = getCacheControl(part) ?? ( + isLastPart ? getCacheControl(message) : undefined + ) + + switch (part.type) { + case "text": { + content.push({ + type: "text", + // Anthropic does not allow trailing whitespace in assistant + // content blocks + text: isLastGroup && isLastMessage && isLastPart + ? part.text.trim() + : part.text + }) + break + } + + case "reasoning": { + const options = part.options.anthropic + if (Predicate.isNotUndefined(options)) { + if (options.type === "thinking") { + content.push({ + type: "thinking", + thinking: part.text, + signature: options.signature + }) + } else { + content.push({ + type: "redacted_thinking", + data: options.redactedData + }) + } + } + break + } + + case "tool-call": { + if (part.providerExecuted) { + if (part.name === "AnthropicCodeExecution") { + content.push({ + type: "server_tool_use", + id: part.id, + name: "code_execution", + input: part.params as any, + cache_control: cacheControl + }) + } + if (part.name === "AnthropicWebSearch") { + content.push({ + type: "server_tool_use", + id: part.id, + name: "web_search", + input: part.params as any, + cache_control: cacheControl + }) + } + } else { + content.push({ + type: "tool_use", + id: part.id, + name: part.name, + input: part.params as any, + cache_control: cacheControl + }) + } + break + } + + case "tool-result": { + if (part.name === "AnthropicCodeExecution") { + content.push({ + type: "code_execution_tool_result", + tool_use_id: part.id, + content: part.result as any, + cache_control: cacheControl + }) + } else if (part.name === "AnthropicWebSearch") { + content.push({ + type: "web_search_tool_result", + tool_use_id: part.id, + content: part.result as any, + cache_control: cacheControl + }) + } else { + return yield* new AiError.MalformedInput({ + module: "AnthropicLanguageModel", + method: "prepareMessages", + description: `Provider executed tool result for tool ${part.name} is not supported in prompt` + }) + } + } + } + } + } + + messages.push({ role: "assistant", content }) + + break + } + } + } + + return { + system, + messages, + betas + } +}) + +// ============================================================================= +// Response Conversion +// ============================================================================= + +const makeResponse: ( + response: Generated.BetaMessage, + options: LanguageModel.ProviderOptions +) => Effect.Effect< + Array, + never, + IdGenerator.IdGenerator +> = Effect.fnUntraced( + function*(response, options) { + const idGenerator = yield* IdGenerator.IdGenerator + const parts: Array = [] + const citableDocuments = extractCitableDocuments(options.prompt) + + parts.push({ + type: "response-metadata", + id: response.id, + modelId: response.model, + timestamp: DateTime.formatIso(yield* DateTime.now) + }) + + for (const part of response.content) { + switch (part.type) { + case "text": { + // The text parts should only be added to the response here if the + // response format is `"text"`. If the response format is `"json"`, + // then the text parts must instead be added to the response when a + // tool call is received. + if (options.responseFormat.type === "text") { + parts.push({ + type: "text", + text: part.text + }) + + if (Predicate.isNotNullable(part.citations)) { + for (const citation of part.citations) { + const source = yield* processCitation(citation, citableDocuments, idGenerator) + if (Predicate.isNotUndefined(source)) { + parts.push(source) + } + } + } + } + + break + } + + case "thinking": { + parts.push({ + type: "reasoning", + text: part.thinking, + metadata: { anthropic: { type: "thinking", signature: part.signature } } + }) + break + } + + case "redacted_thinking": { + parts.push({ + type: "reasoning", + text: "", + metadata: { anthropic: { type: "redacted_thinking", redactedData: part.data } } + }) + break + } + + case "tool_use": { + // When a `"json"` response format is requested, the JSON that we need + // will be returned by the tool call injected into the request + if (options.responseFormat.type === "json") { + parts.push({ + type: "text", + text: JSON.stringify(part.input) + }) + } else { + const providerTool = AnthropicTool.getProviderDefinedToolName(part.name) + const name = Predicate.isNotUndefined(providerTool) ? providerTool : part.name + const providerName = Predicate.isNotUndefined(providerTool) ? part.name : undefined + parts.push({ + type: "tool-call", + id: part.id, + name, + params: part.input, + providerName, + providerExecuted: false + }) + } + + break + } + + case "server_tool_use": { + const providerTool = AnthropicTool.getProviderDefinedToolName(part.name) + if (Predicate.isNotUndefined(providerTool)) { + parts.push({ + type: "tool-call", + id: part.id, + name: providerTool, + params: part.input, + providerName: part.name, + providerExecuted: true + }) + } + + break + } + + case "bash_code_execution_tool_result": { + const isFailure = part.content.type === "bash_code_execution_tool_result_error" + parts.push({ + type: "tool-result", + id: part.tool_use_id, + name: "AnthropicCodeExecution", + isFailure, + result: part.content, + providerName: "code_execution", + providerExecuted: true + }) + break + } + + case "code_execution_tool_result": { + const isFailure = part.content.type === "code_execution_tool_result_error" + parts.push({ + type: "tool-result", + id: part.tool_use_id, + name: "AnthropicCodeExecution", + isFailure, + result: part.content, + providerName: "code_execution", + providerExecuted: true + }) + break + } + + case "text_editor_code_execution_tool_result": { + const isFailure = part.content.type === "text_editor_code_execution_tool_result_error" + parts.push({ + type: "tool-result", + id: part.tool_use_id, + name: "AnthropicCodeExecution", + isFailure, + result: part.content, + providerName: "code_execution", + providerExecuted: true + }) + break + } + + case "web_search_tool_result": { + const isFailure = !Array.isArray(part.content) + parts.push({ + type: "tool-result", + id: part.tool_use_id, + name: "AnthropicWebSearch", + isFailure, + result: part.content, + providerName: "web_search", + providerExecuted: true + }) + break + } + } + } + + // Anthropic always returns a non-null `stop_reason` for non-streaming responses + const finishReason = InternalUtilities.resolveFinishReason( + response.stop_reason!, + options.responseFormat.type === "json" + ) + + parts.push({ + type: "finish", + reason: finishReason, + usage: { + inputTokens: response.usage.input_tokens, + outputTokens: response.usage.output_tokens, + totalTokens: response.usage.input_tokens + response.usage.output_tokens, + cachedInputTokens: response.usage.cache_read_input_tokens ?? undefined + }, + metadata: { + anthropic: { + usage: response.usage, + stopSequence: response.stop_sequence ?? undefined + } + } + }) + + return parts + } +) + +const makeStreamResponse: ( + stream: Stream.Stream, + options: LanguageModel.ProviderOptions +) => Effect.Effect< + Stream.Stream, + never, + IdGenerator.IdGenerator +> = Effect.fnUntraced( + function*(stream, options) { + const idGenerator = yield* IdGenerator.IdGenerator + const citableDocuments = extractCitableDocuments(options.prompt) + + // Setup all requisite state for the streaming response + let finishReason: Response.FinishReason = "unknown" + const contentBlocks: Record< + number, + | { + readonly type: "text" + } + | { + readonly type: "reasoning" + } + | { + readonly type: "tool-call" + readonly id: string + readonly name: string + params: string + readonly providerName: string | undefined + readonly providerExecuted: boolean + } + > = {} + let blockType: + | "text" + | "thinking" + | "redacted_thinking" + | "tool_use" + | "server_tool_use" + | "web_fetch_tool_result" + | "web_search_tool_result" + | "code_execution_tool_result" + | "bash_code_execution_tool_result" + | "text_editor_code_execution_tool_result" + | "mcp_tool_use" + | "mcp_tool_result" + | "container_upload" + | undefined = undefined + const usage: Mutable = { + inputTokens: undefined, + outputTokens: undefined, + totalTokens: undefined + } + let metaUsage: Generated.BetaUsage | undefined = undefined + let stopSequence: string | undefined = undefined + + return stream.pipe( + Stream.mapEffect(Effect.fnUntraced(function*(event) { + const parts: Array = [] + + switch (event.type) { + case "ping": { + break + } + + case "message_start": { + // Track usage metadata + usage.inputTokens = event.message.usage.input_tokens + metaUsage = event.message.usage + + // Track response metadata + parts.push({ + type: "response-metadata", + id: event.message.id, + modelId: event.message.model, + timestamp: DateTime.formatIso(yield* DateTime.now) + }) + + break + } + + case "message_delta": { + // Track usage metadata + if (Predicate.isNotNullable(event.usage.output_tokens)) { + usage.outputTokens = event.usage.output_tokens + } + usage.totalTokens = (usage.inputTokens ?? 0) + (event.usage.output_tokens ?? 0) + + // Track stop sequence metadata + if (Predicate.isNotNullable(event.delta.stop_sequence)) { + stopSequence = event.delta.stop_sequence + } + + // Track the response finish reason + if (Predicate.isNotNullable(event.delta.stop_reason)) { + finishReason = InternalUtilities.resolveFinishReason(event.delta.stop_reason) + } + + break + } + + case "message_stop": { + parts.push({ + type: "finish", + reason: finishReason, + usage, + metadata: { anthropic: { usage: metaUsage, stopSequence } } + }) + + break + } + + case "content_block_start": { + blockType = event.content_block.type + + switch (event.content_block.type) { + case "text": { + contentBlocks[event.index] = { type: "text" } + + parts.push({ + type: "text-start", + id: event.index.toString() + }) + + break + } + + case "thinking": { + contentBlocks[event.index] = { type: "reasoning" } + + parts.push({ + type: "reasoning-start", + id: event.index.toString() + }) + + break + } + + case "redacted_thinking": { + contentBlocks[event.index] = { type: "reasoning" } + + parts.push({ + type: "reasoning-start", + id: event.index.toString(), + metadata: { + anthropic: { + type: "redacted_thinking", + redactedData: event.content_block.data + } + } + }) + + break + } + + case "tool_use": { + const toolName = event.content_block.name + const providerTool = AnthropicTool.getProviderDefinedToolName(toolName) + const name = Predicate.isNotUndefined(providerTool) ? providerTool : toolName + const providerName = Predicate.isNotUndefined(providerTool) ? toolName : undefined + + contentBlocks[event.index] = { + type: "tool-call", + id: event.content_block.id, + name, + params: "", + providerName, + providerExecuted: false + } + + parts.push({ + type: "tool-params-start", + id: event.content_block.id, + name: toolName, + providerName, + providerExecuted: false + }) + + break + } + + case "server_tool_use": { + const toolName = event.content_block.name + const providerTool = AnthropicTool.getProviderDefinedToolName(toolName) + if (Predicate.isNotUndefined(providerTool)) { + contentBlocks[event.index] = { + type: "tool-call", + id: event.content_block.id, + name: providerTool, + params: "", + providerName: toolName, + providerExecuted: true + } + + parts.push({ + type: "tool-params-start", + id: event.content_block.id, + name: providerTool, + providerName: toolName, + providerExecuted: true + }) + } + + break + } + + case "bash_code_execution_tool_result": { + const toolUseId = event.content_block.tool_use_id + const content = event.content_block.content + const isFailure = content.type === "bash_code_execution_tool_result_error" + parts.push({ + type: "tool-result", + id: toolUseId, + name: "AnthropicCodeExecution", + isFailure, + result: content, + providerName: "code_execution", + providerExecuted: true + }) + break + } + + case "code_execution_tool_result": { + const toolUseId = event.content_block.tool_use_id + const content = event.content_block.content + const isFailure = content.type === "code_execution_tool_result_error" + parts.push({ + type: "tool-result", + id: toolUseId, + name: "AnthropicCodeExecution", + isFailure, + result: content, + providerName: "code_execution", + providerExecuted: true + }) + break + } + + case "text_editor_code_execution_tool_result": { + const toolUseId = event.content_block.tool_use_id + const content = event.content_block.content + const isFailure = content.type === "text_editor_code_execution_tool_result_error" + parts.push({ + type: "tool-result", + id: toolUseId, + name: "AnthropicCodeExecution", + isFailure, + result: content, + providerName: "code_execution", + providerExecuted: true + }) + break + } + + case "web_search_tool_result": { + const toolUseId = event.content_block.tool_use_id + const content = event.content_block.content + const isFailure = !Array.isArray(content) + parts.push({ + type: "tool-result", + id: toolUseId, + name: "AnthropicWebSearch", + isFailure, + result: content, + providerName: "web_search", + providerExecuted: true + }) + break + } + } + + break + } + + case "content_block_delta": { + switch (event.delta.type) { + case "text_delta": { + parts.push({ + type: "text-delta", + id: event.index.toString(), + delta: event.delta.text + }) + + break + } + + case "thinking_delta": { + parts.push({ + type: "reasoning-delta", + id: event.index.toString(), + delta: event.delta.thinking + }) + + break + } + + case "signature_delta": { + if (blockType === "thinking") { + parts.push({ + type: "reasoning-delta", + id: event.index.toString(), + delta: "", + metadata: { + anthropic: { + type: "thinking", + signature: event.delta.signature + } + } + }) + } + + break + } + + case "input_json_delta": { + const contentBlock = contentBlocks[event.index] + const delta = event.delta.partial_json + + if (contentBlock.type === "tool-call") { + parts.push({ + type: "tool-params-delta", + id: contentBlock.id, + delta + }) + + contentBlock.params += delta + } + + break + } + + case "citations_delta": { + const citation = event.delta.citation + + const source = yield* processCitation(citation, citableDocuments, idGenerator) + if (Predicate.isNotUndefined(source)) { + parts.push(source) + } + } + } + + break + } + + case "content_block_stop": { + if (Predicate.isNotNullable(contentBlocks[event.index])) { + const contentBlock = contentBlocks[event.index] + + switch (contentBlock.type) { + case "text": { + parts.push({ + type: "text-end", + id: event.index.toString() + }) + break + } + + case "reasoning": { + parts.push({ + type: "reasoning-end", + id: event.index.toString() + }) + break + } + + case "tool-call": { + parts.push({ + type: "tool-params-end", + id: contentBlock.id + }) + + const toolName = contentBlock.name + // If the tool call has no parameters, an empty string is returned + const toolParams = contentBlock.params.length === 0 ? "{}" : contentBlock.params + + const parsedParams = yield* Effect.try({ + try: () => Tool.unsafeSecureJsonParse(toolParams), + catch: (cause) => + new AiError.MalformedOutput({ + module: "AnthropicLanguageModel", + method: "makeStreamResponse", + description: "Failed to securely parse tool call parameters " + + `for tool '${toolName}':\nParameters: ${toolParams}`, + cause + }) + }) + + parts.push({ + type: "tool-call", + id: contentBlock.id, + name: toolName, + params: parsedParams, + providerName: contentBlock.providerName, + providerExecuted: contentBlock.providerExecuted + }) + + break + } + } + + delete contentBlocks[event.index] + } + + blockType = undefined + + break + } + + case "error": { + parts.push({ type: "error", error: event.error }) + + break + } + } + + return parts + })), + Stream.flattenIterables + ) + } +) + +// ============================================================================= +// Telemetry +// ============================================================================= + +const annotateRequest = ( + span: Span, + request: typeof Generated.BetaCreateMessageParams.Encoded +): void => { + addGenAIAnnotations(span, { + system: "anthropic", + operation: { name: "chat" }, + request: { + model: request.model, + temperature: request.temperature, + topK: request.top_k, + topP: request.top_p, + maxTokens: request.max_tokens, + stopSequences: Arr.ensure(request.stop_sequences).filter( + Predicate.isNotNullable + ) + } + }) +} + +const annotateResponse = (span: Span, response: Generated.BetaMessage): void => { + addGenAIAnnotations(span, { + response: { + id: response.id, + model: response.model, + finishReasons: response.stop_reason ? [response.stop_reason] : undefined + }, + usage: { + inputTokens: response.usage.input_tokens, + outputTokens: response.usage.output_tokens + } + }) +} + +const annotateStreamResponse = (span: Span, part: Response.StreamPartEncoded) => { + if (part.type === "response-metadata") { + addGenAIAnnotations(span, { + response: { + id: part.id, + model: part.modelId + } + }) + } + if (part.type === "finish") { + addGenAIAnnotations(span, { + response: { + finishReasons: [part.reason] + }, + usage: { + inputTokens: part.usage.inputTokens, + outputTokens: part.usage.outputTokens + } + }) + } +} + +// ============================================================================= +// Tool Calling +// ============================================================================= + +/** + * Represents all possible Anthropic provider-defined tools. + * + * @since 1.0.0 + * @category Models + */ +export type AnthropicTools = + | typeof Generated.BetaTool.Encoded + | typeof Generated.BetaBashTool20241022.Encoded + | typeof Generated.BetaBashTool20250124.Encoded + | typeof Generated.BetaComputerUseTool20241022.Encoded + | typeof Generated.BetaComputerUseTool20250124.Encoded + | typeof Generated.BetaTextEditor20241022.Encoded + | typeof Generated.BetaTextEditor20250124.Encoded + | typeof Generated.BetaTextEditor20250429.Encoded + | typeof Generated.BetaTextEditor20250728.Encoded + +/** + * A helper method which takes in large language model provider options from + * the base Effect AI SDK as well as Anthropic request configuration options + * and returns the prepared tools, tool choice, and Anthropic betas to include + * in a request. + * + * This method is primarily exposed for use by other Effect provider + * integrations which can utilize Anthropic models (i.e. Amazon Bedrock). + * + * @since 1.0.0 + * @category Tool Calling + */ +export const prepareTools: (options: LanguageModel.ProviderOptions, config: Config.Service) => Effect.Effect<{ + readonly betas: ReadonlySet + readonly tools: ReadonlyArray | undefined + readonly toolChoice: typeof Generated.BetaToolChoice.Encoded | undefined +}, AiError.AiError> = Effect.fnUntraced(function*(options, config) { + // Return immediately if no tools are in the toolkit or a tool choice of + // "none" was specified + if (options.tools.length === 0 || options.toolChoice === "none") { + return { betas: new Set(), tools: undefined, toolChoice: undefined } + } + + const betas = new Set() + let tools: Array = [] + let toolChoice: typeof Generated.BetaToolChoice.Encoded | undefined = undefined + + // Convert the tools in the toolkit to the provider-defined format + for (const tool of options.tools) { + if (Tool.isUserDefined(tool)) { + tools.push({ + name: tool.name, + description: Tool.getDescription(tool as any), + input_schema: Tool.getJsonSchema(tool as any) as any + }) + } + + if (Tool.isProviderDefined(tool)) { + switch (tool.id) { + case "anthropic.bash_20241022": { + betas.add("computer-use-2024-10-22") + tools.push({ + name: "bash", + type: "bash_20241022" + }) + break + } + case "anthropic.bash_20250124": { + betas.add("computer-use-2025-01-24") + tools.push({ + name: "bash", + type: "bash_20250124" + }) + break + } + case "anthropic.code_execution_20250522": { + betas.add("code-execution-2025-05-22") + tools.push({ + ...tool.args, + name: "code_execution", + type: "code_execution_2025522" + }) + break + } + case "anthropic.code_execution_20250825": { + betas.add("code-execution-2025-08-25") + tools.push({ + ...tool.args, + name: "code_execution", + type: "code_execution_20250825" + }) + break + } + case "anthropic.computer_use_20241022": { + betas.add("computer-use-2025-10-22") + tools.push({ + ...tool.args, + name: "computer", + type: "computer_20241022" + }) + break + } + case "anthropic.computer_use_20250124": { + betas.add("computer-use-2025-01-24") + tools.push({ + ...tool.args, + name: "computer", + type: "computer_20250124" + }) + break + } + case "anthropic.text_editor_20241022": { + betas.add("computer-use-2024-10-22") + tools.push({ + name: "str_replace_editor", + type: "text_editor_20241022" + }) + break + } + case "anthropic.text_editor_20250124": { + betas.add("computer-use-2025-01-24") + tools.push({ + name: "str_replace_editor", + type: "text_editor_20250124" + }) + break + } + case "anthropic.text_editor_20250429": { + betas.add("computer-use-2025-01-24") + tools.push({ + name: "str_replace_based_edit_tool", + type: "text_editor_20250429" + }) + break + } + case "anthropic.text_editor_20250728": { + tools.push({ + name: "str_replace_based_edit_tool", + type: "text_editor_20250728" + }) + break + } + case "anthropic.web_search_20250305": { + tools.push({ + ...tool.args, + name: "web_search", + type: "web_search_20250305" + }) + break + } + default: { + return yield* new AiError.MalformedInput({ + module: "AnthropicLanguageModel", + method: "prepareTools", + description: `Received request to call unknown provider-defined tool '${tool.name}'` + }) + } + } + } + } + + // Convert the tool choice to the provider-defined format + if (options.toolChoice === "auto") { + toolChoice = { + type: "auto", + disable_parallel_tool_use: config.disableParallelToolCalls + } + } else if (options.toolChoice === "required") { + toolChoice = { + type: "any", + disable_parallel_tool_use: config.disableParallelToolCalls + } + } else if ("tool" in options.toolChoice) { + toolChoice = { + type: "tool", + name: options.toolChoice.tool, + disable_parallel_tool_use: config.disableParallelToolCalls + } + } else { + const allowedTools = new Set(options.toolChoice.oneOf) + tools = tools.filter((tool) => allowedTools.has(tool.name)) + toolChoice = { + type: options.toolChoice.mode === "required" ? "any" : "auto", + disable_parallel_tool_use: config.disableParallelToolCalls + } + } + + return { betas, tools, toolChoice } +}) + +// ============================================================================= +// Utilities +// ============================================================================= + +type ContentGroup = SystemMessageGroup | AssistantMessageGroup | UserMessageGroup + +interface SystemMessageGroup { + readonly type: "system" + readonly messages: Array +} + +interface AssistantMessageGroup { + readonly type: "assistant" + readonly messages: Array +} + +interface UserMessageGroup { + readonly type: "user" + readonly messages: Array +} + +const groupMessages = (prompt: Prompt.Prompt): Array => { + const messages: Array = [] + let current: ContentGroup | undefined = undefined + for (const message of prompt.content) { + switch (message.role) { + case "system": { + if (current?.type !== "system") { + current = { type: "system", messages: [] } + messages.push(current) + } + current.messages.push(message) + break + } + case "assistant": { + if (current?.type !== "assistant") { + current = { type: "assistant", messages: [] } + messages.push(current) + } + current.messages.push(message) + break + } + case "tool": + case "user": { + if (current?.type !== "user") { + current = { type: "user", messages: [] } + messages.push(current) + } + current.messages.push(message) + break + } + } + } + return messages +} + +const isCitationPart = (part: Prompt.UserMessage["content"][number]): part is Prompt.FilePart => { + if (part.type === "file" && (part.mediaType === "application/pdf" || part.mediaType === "text/plain")) { + return part.options.anthropic?.citations?.enabled ?? false + } + return false +} + +interface CitableDocument { + readonly title: string + readonly fileName: string | undefined + readonly mediaType: string +} + +const extractCitableDocuments = (prompt: Prompt.Prompt): ReadonlyArray => { + const citableDocuments: Array = [] + for (const message of prompt.content) { + if (message.role === "user") { + for (const part of message.content) { + if (isCitationPart(part)) { + citableDocuments.push({ + title: part.fileName ?? "Untitled Document", + fileName: part.fileName, + mediaType: part.mediaType + }) + } + } + } + } + return citableDocuments +} + +const getCacheControl = ( + part: + | Prompt.SystemMessage + | Prompt.UserMessage + | Prompt.AssistantMessage + | Prompt.ToolMessage + | Prompt.UserMessagePart + | Prompt.AssistantMessagePart + | Prompt.ToolMessagePart +): typeof Generated.CacheControlEphemeral.Encoded | undefined => part.options.anthropic?.cacheControl + +const getDocumentMetadata = (part: Prompt.FilePart): { + readonly title: string | undefined + readonly context: string | undefined +} | undefined => { + const options = part.options.anthropic + if (Predicate.isNotUndefined(options)) { + return { + title: options.documentTitle, + context: options.documentContext + } + } + return undefined +} + +const shouldEnableCitations = (part: Prompt.FilePart): boolean => part.options.anthropic?.citations?.enabled ?? false + +const processCitation: ( + citation: + | Generated.ResponseCharLocationCitation + | Generated.ResponsePageLocationCitation + | Generated.ResponseContentBlockLocationCitation + | Generated.ResponseWebSearchResultLocationCitation + | Generated.ResponseSearchResultLocationCitation, + citableDocuments: ReadonlyArray, + idGenerator: IdGenerator.Service +) => Effect.Effect = Effect.fnUntraced( + function*(citation, citableDocuments, idGenerator) { + if (citation.type === "page_location" || citation.type === "char_location") { + const citedDocument = citableDocuments[citation.document_index] + if (Predicate.isNotUndefined(citedDocument)) { + const id = yield* idGenerator.generateId() + + const metadata = citation.type === "char_location" + ? { + source: "document", + type: citation.type, + citedText: citation.cited_text, + startCharIndex: citation.start_char_index, + endCharIndex: citation.end_char_index + } as const + : { + source: "document", + type: citation.type, + citedText: citation.cited_text, + startPageNumber: citation.start_page_number, + endPageNumber: citation.end_page_number + } as const + + return { + type: "source", + sourceType: "document", + id, + mediaType: citedDocument.mediaType, + title: citation.document_title ?? citedDocument.title, + fileName: citedDocument.fileName, + metadata: { anthropic: metadata } + } + } + } + + if (citation.type === "web_search_result_location") { + const id = yield* idGenerator.generateId() + + const metadata = { + source: "url", + citedText: citation.cited_text, + encryptedIndex: citation.encrypted_index + } as const + + return { + type: "source", + sourceType: "url", + id, + url: citation.url, + title: citation.title ?? "Untitled", + metadata: { anthropic: metadata } + } + } + } +) diff --git a/repos/effect/packages/ai/anthropic/src/AnthropicTokenizer.ts b/repos/effect/packages/ai/anthropic/src/AnthropicTokenizer.ts new file mode 100644 index 0000000..46c7e57 --- /dev/null +++ b/repos/effect/packages/ai/anthropic/src/AnthropicTokenizer.ts @@ -0,0 +1,59 @@ +/** + * @since 1.0.0 + */ +import { getTokenizer } from "@anthropic-ai/tokenizer" +import * as AiError from "@effect/ai/AiError" +import type * as Prompt from "@effect/ai/Prompt" +import * as Tokenizer from "@effect/ai/Tokenizer" +import * as Arr from "effect/Array" +import * as Effect from "effect/Effect" +import * as Layer from "effect/Layer" +import * as Option from "effect/Option" + +/** + * @since 1.0.0 + * @category Constructors + */ +export const make = Tokenizer.make({ + tokenize(prompt) { + return Effect.try({ + try: () => { + const tokenizer = getTokenizer() + const text = Arr.flatMap(prompt.content, (message) => + Arr.filterMap( + message.content as Array< + | Prompt.AssistantMessagePart + | Prompt.ToolMessagePart + | Prompt.UserMessagePart + >, + (part) => { + if (part.type === "file" || part.type === "reasoning") { + return Option.none() + } + return Option.some( + part.type === "text" + ? part.text + : JSON.stringify(part.type === "tool-call" ? part.params : part.result) + ) + } + )).join("") + const encoded = tokenizer.encode(text.normalize("NFKC"), "all") + tokenizer.free() + return Array.from(encoded) + }, + catch: (cause) => + new AiError.UnknownError({ + module: "AnthropicTokenizer", + method: "tokenize", + description: "Could not tokenize", + cause + }) + }) + } +}) + +/** + * @since 1.0.0 + * @category Layers + */ +export const layer: Layer.Layer = Layer.succeed(Tokenizer.Tokenizer, make) diff --git a/repos/effect/packages/ai/anthropic/src/AnthropicTool.ts b/repos/effect/packages/ai/anthropic/src/AnthropicTool.ts new file mode 100644 index 0000000..dee601d --- /dev/null +++ b/repos/effect/packages/ai/anthropic/src/AnthropicTool.ts @@ -0,0 +1,553 @@ +/** + * @since 1.0.0 + */ +import * as Tool from "@effect/ai/Tool" +import * as Schema from "effect/Schema" +import * as Struct from "effect/Struct" +import * as Generated from "./Generated.js" + +/** + * @since 1.0.0 + * @category Schemas + */ +export const ProviderDefinedTools: Schema.Union<[ + typeof Generated.BetaBashTool20241022, + typeof Generated.BetaBashTool20250124, + typeof Generated.BetaCodeExecutionTool20250522, + typeof Generated.BetaComputerUseTool20241022, + typeof Generated.BetaComputerUseTool20250124, + typeof Generated.BetaTextEditor20241022, + typeof Generated.BetaTextEditor20250124, + typeof Generated.BetaTextEditor20250429, + typeof Generated.BetaTextEditor20250728, + typeof Generated.BetaWebSearchTool20250305 +]> = Schema.Union( + Generated.BetaBashTool20241022, + Generated.BetaBashTool20250124, + Generated.BetaCodeExecutionTool20250522, + Generated.BetaComputerUseTool20241022, + Generated.BetaComputerUseTool20250124, + Generated.BetaTextEditor20241022, + Generated.BetaTextEditor20250124, + Generated.BetaTextEditor20250429, + Generated.BetaTextEditor20250728, + Generated.BetaWebSearchTool20250305 +) + +/** + * @since 1.0.0 + * @category Schemas + */ +export type ProviderDefinedTools = typeof ProviderDefinedTools.Type + +/** + * @since 1.0.0 + * @category Tools + */ +export const Bash_20241022 = Tool.providerDefined({ + id: "anthropic.bash_20241022", + toolkitName: "AnthropicBash", + providerName: "bash", + args: {}, + requiresHandler: true, + success: Schema.String, + parameters: { + /** + * The Bash command to run. + */ + command: Schema.NonEmptyString, + /** + * If `true`, restart the Bash session. + */ + restart: Schema.optional(Schema.Boolean) + } +}) + +/** + * @since 1.0.0 + * @category Tools + */ +export const Bash_20250124 = Tool.providerDefined({ + id: "anthropic.bash_20250124", + toolkitName: "AnthropicBash", + providerName: "bash", + args: {}, + requiresHandler: true, + success: Schema.String, + parameters: { + /** + * The Bash command to run. + */ + command: Schema.NonEmptyString, + /** + * If `true`, restart the Bash session. + */ + restart: Schema.optional(Schema.Boolean) + } +}) + +/** + * @since 1.0.0 + * @category Tools + */ +export const CodeExecution_20250522 = Tool.providerDefined({ + id: "anthropic.code_execution_20250522", + toolkitName: "AnthropicCodeExecution", + providerName: "code_execution", + args: Struct.omit(Generated.BetaCodeExecutionTool20250522.fields, "name", "type"), + success: Generated.BetaResponseCodeExecutionResultBlock, + failure: Generated.BetaResponseCodeExecutionToolResultError +}) + +/** + * @since 1.0.0 + * @category Tools + */ +export const CodeExecution_20250825 = Tool.providerDefined({ + id: "anthropic.code_execution_20250825", + toolkitName: "AnthropicCodeExecution", + providerName: "code_execution", + args: Struct.omit(Generated.BetaCodeExecutionTool20250825.fields, "name", "type"), + success: Schema.Union( + Generated.BetaResponseBashCodeExecutionResultBlock, + Generated.BetaResponseTextEditorCodeExecutionViewResultBlock, + Generated.BetaResponseTextEditorCodeExecutionCreateResultBlock, + Generated.BetaResponseTextEditorCodeExecutionStrReplaceResultBlock + ), + failure: Schema.Union( + Generated.BetaResponseCodeExecutionToolResultError, + Generated.BetaResponseTextEditorCodeExecutionToolResultError + ) +}) + +/** + * @since 1.0.0 + * @category Models + */ +export const Coordinate = Schema.Tuple(Schema.Number, Schema.Number) + +/** + * Allow Claude to interact with computer environments through the computer use + * tool, which provides screenshot capabilities and mouse/keyboard control for + * autonomous desktop interaction. + * + * @since 1.0.0 + * @category Tools + */ +export const ComputerUse_20241022 = Tool.providerDefined({ + id: "anthropic.computer_use_20241022", + toolkitName: "AnthropicComputerUse", + providerName: "computer", + args: Struct.omit(Generated.BetaComputerUseTool20241022.fields, "name", "type"), + requiresHandler: true, + success: Schema.String, + parameters: { + /** + * The action to perform. The available actions are: + * - `screenshot`: Take a screenshot of the screen. + * - `left_click`: Click the left mouse button at the specified (x, y) pixel + * coordinate on the screen. You can also include a key combination to + * hold down while clicking using the `text` parameter. + * - `type`: Type a string of text on the keyboard. + * - `key`: Press a key or key-combination on the keyboard. + * - This supports xdotool's `key` syntax. + * - Examples: "a", "Return", "alt+Tab", "ctrl+s", "Up", "KP_0" (for the + * numpad 0 key). + * - `mouse_move`: Move the cursor to a specified (x, y) pixel coordinate on + * the screen. + */ + action: Schema.Literal( + "screenshot", + "left_click", + "type", + "key", + "mouse_move" + ), + /** + * The x (pixels from the left edge) and y (pixels from the top edge) + * coordinates to move the mouse to. Required only by `action=mouse_move`. + */ + coordinate: Schema.optional(Coordinate), + /** + * Required only by `action=type` and `action=key`. + */ + text: Schema.optional(Schema.String) + } +}) + +/** + * Allow Claude to interact with computer environments through the computer use + * tool, which provides screenshot capabilities and mouse/keyboard control for + * autonomous desktop interaction. + * + * @since 1.0.0 + * @category Tools + */ +export const ComputerUse_20250124 = Tool.providerDefined({ + id: "anthropic.computer_use_20250124", + toolkitName: "AnthropicComputerUse", + providerName: "computer", + args: Struct.omit(Generated.BetaComputerUseTool20241022.fields, "name", "type"), + requiresHandler: true, + success: Schema.String, + parameters: { + /** + * The action to perform. The available actions are: + * - `screenshot`: Take a screenshot of the screen. + * - `left_click`: Click the left mouse button at the specified (x, y) pixel + * coordinate on the screen. You can also include a key combination to + * hold down while clicking using the `text` parameter. + * - `type`: Type a string of text on the keyboard. + * - `key`: Press a key or key-combination on the keyboard. + * - This supports xdotool's `key` syntax. + * - Examples: "a", "Return", "alt+Tab", "ctrl+s", "Up", "KP_0" (for the + * numpad 0 key). + * - `mouse_move`: Move the cursor to a specified (x, y) pixel coordinate on + * - `scroll`: Scroll the screen in a specified direction by a specified + * amount of clicks of the scroll wheel, at the specified (x, y) pixel + * coordinate. DO NOT use PageUp/PageDown to scroll. + * - `left_click_drag`: Click and drag the cursor from `start_coordinate` + * to a specified (x, y) pixel coordinate on the screen. + * the screen. + * - `middle_click`: Click the middle mouse button at the specified (x, y) + * pixel coordinate on the screen. + * - `right_click`: Click the right mouse button at the specified (x, y) + * pixel coordinate on the screen. + * - `double_click`: Double-click the left mouse button at the specified + * (x, y) pixel coordinate on the screen. + * - `triple_click`: Triple-click the left mouse button at the specified + * (x, y) pixel coordinate on the screen. + * - `left_mouse_down`: Press the left mouse button. + * - `left_mouse_up`: Release the left mouse button. + * - `hold_key`: Hold down a key or multiple keys for a specified duration + * (in seconds). Supports the same syntax as `key`. + * - `wait`: Wait for a specified duration (in seconds). + */ + action: Schema.Literal( + "screenshot", + "left_click", + "type", + "key", + "mouse_move", + "scroll", + "left_click_drag", + "middle_click", + "right_click", + "double_click", + "triple_click", + "left_mouse_down", + "left_mouse_up", + "hold_key", + "wait" + ), + /** + * The x (pixels from the left edge) and y (pixels from the top edge) + * coordinates to move the mouse to. Required only by `action=mouse_move` + * and `action=left_click_drag`. + */ + coordinate: Schema.optional(Coordinate), + /** + * The x (pixels from the left edge) and y (pixels from the top edge) + * coordinates to start the drag from. Required only by + * `action=left_click_drag`. + */ + start_coordinate: Schema.optional(Coordinate), + /** + * Required only by `action=type`, `action=key`, and `action=hold_key`. Can + * also be used by click or scroll actions to hold down keys while clicking + * or scrolling. + */ + text: Schema.optional(Schema.String), + /** + * The direction to scroll the screen. Required only by `action=scroll`. + */ + scroll_direction: Schema.optional(Schema.Literal("up", "down", "left", "right")), + /** + * The number of "clicks" of the scroll wheel to scroll. Required only by + * `action=scroll`. + */ + scroll_amount: Schema.optional(Schema.Number), + /** + * The duration to hold the key down for. Required only by `action=hold_key` + * and `action=wait`. + */ + duration: Schema.optional(Schema.Number) + } +}) + +/** + * Allow Claude to directly interact with your files, providing hands-on + * assistance rather than just suggesting changes. + * + * @since 1.0.0 + * @category Tools + */ +export const TextEditor_20241022 = Tool.providerDefined({ + id: "anthropic.text_editor_20241022", + toolkitName: "AnthropicTextEditor", + providerName: "str_replace_editor", + args: {}, + requiresHandler: true, + parameters: { + /** + * The command to run. + */ + command: Schema.Literal( + "view", + "create", + "str_replace", + "insert", + "undo_edit" + ), + /** + * Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`. + */ + path: Schema.String, + /** + * Required parameter of `create` command, with the content of the file to + * be created. + */ + file_text: Schema.optional(Schema.String), + /** + * Required parameter of `insert` command. The `new_str` will be inserted + * AFTER the line `insert_line` of `path`. + */ + insert_line: Schema.optional(Schema.Number), + /** + * Optional parameter of `str_replace` command containing the new string (if + * not given, no string will be added). Required parameter of `insert` + * command containing the string to insert. + */ + new_str: Schema.optional(Schema.String), + /** + * Required parameter of `str_replace` command containing the string in + * `path` to replace. + */ + old_str: Schema.optional(Schema.String), + /** + * Optional parameter of `view` command when `path` points to a file. If + * none is given, the full file is shown. If provided, the file will be + * shown in the indicated line number range, e.g. [11, 12] will show lines + * 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all + * lines from `start_line` to the end of the file. + */ + view_range: Schema.optional(Schema.Array(Schema.Number)) + } +}) + +/** + * Allow Claude to directly interact with your files, providing hands-on + * assistance rather than just suggesting changes. + * + * @since 1.0.0 + * @category Tools + */ +export const TextEditor_20250124 = Tool.providerDefined({ + id: "anthropic.text_editor_20250124", + toolkitName: "AnthropicTextEditor", + providerName: "str_replace_editor", + args: {}, + requiresHandler: true, + parameters: { + /** + * The command to run. + */ + command: Schema.Literal( + "view", + "create", + "str_replace", + "insert", + "undo_edit" + ), + /** + * Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`. + */ + path: Schema.String, + /** + * Required parameter of `create` command, with the content of the file to + * be created. + */ + file_text: Schema.optional(Schema.String), + /** + * Required parameter of `insert` command. The `new_str` will be inserted + * AFTER the line `insert_line` of `path`. + */ + insert_line: Schema.optional(Schema.Number), + /** + * Optional parameter of `str_replace` command containing the new string (if + * not given, no string will be added). Required parameter of `insert` + * command containing the string to insert. + */ + new_str: Schema.optional(Schema.String), + /** + * Required parameter of `str_replace` command containing the string in + * `path` to replace. + */ + old_str: Schema.optional(Schema.String), + /** + * Optional parameter of `view` command when `path` points to a file. If + * none is given, the full file is shown. If provided, the file will be + * shown in the indicated line number range, e.g. [11, 12] will show lines + * 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all + * lines from `start_line` to the end of the file. + */ + view_range: Schema.optional(Schema.Array(Schema.Number)) + } +}) + +/** + * Allow Claude to directly interact with your files, providing hands-on + * assistance rather than just suggesting changes. + * + * @since 1.0.0 + * @category Tools + */ +export const TextEditor_20250429 = Tool.providerDefined({ + id: "anthropic.text_editor_20250429", + toolkitName: "AnthropicTextEditor", + providerName: "str_replace_based_edit_tool", + args: {}, + requiresHandler: true, + parameters: { + /** + * The command to run. + */ + command: Schema.Literal( + "view", + "create", + "str_replace", + "insert" + ), + /** + * Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`. + */ + path: Schema.String, + /** + * Required parameter of `create` command, with the content of the file to + * be created. + */ + file_text: Schema.optional(Schema.String), + /** + * Required parameter of `insert` command. The `new_str` will be inserted + * AFTER the line `insert_line` of `path`. + */ + insert_line: Schema.optional(Schema.Number), + /** + * Optional parameter of `str_replace` command containing the new string (if + * not given, no string will be added). Required parameter of `insert` + * command containing the string to insert. + */ + new_str: Schema.optional(Schema.String), + /** + * Required parameter of `str_replace` command containing the string in + * `path` to replace. + */ + old_str: Schema.optional(Schema.String), + /** + * Optional parameter of `view` command when `path` points to a file. If + * none is given, the full file is shown. If provided, the file will be + * shown in the indicated line number range, e.g. [11, 12] will show lines + * 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all + * lines from `start_line` to the end of the file. + */ + view_range: Schema.optional(Schema.Array(Schema.Number)) + } +}) + +/** + * Allow Claude to directly interact with your files, providing hands-on + * assistance rather than just suggesting changes. + * + * @since 1.0.0 + * @category Tools + */ +export const TextEditor_20250728 = Tool.providerDefined({ + id: "anthropic.text_editor_20250728", + toolkitName: "AnthropicTextEditor", + providerName: "str_replace_based_edit_tool", + args: {}, + requiresHandler: true, + parameters: { + /** + * The command to run. + */ + command: Schema.Literal( + "view", + "create", + "str_replace", + "insert" + ), + /** + * Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`. + */ + path: Schema.String, + /** + * Required parameter of `create` command, with the content of the file to + * be created. + */ + file_text: Schema.optional(Schema.String), + /** + * Required parameter of `insert` command. The `new_str` will be inserted + * AFTER the line `insert_line` of `path`. + */ + insert_line: Schema.optional(Schema.Number), + /** + * Optional parameter of `str_replace` command containing the new string (if + * not given, no string will be added). Required parameter of `insert` + * command containing the string to insert. + */ + new_str: Schema.optional(Schema.String), + /** + * Required parameter of `str_replace` command containing the string in + * `path` to replace. + */ + old_str: Schema.optional(Schema.String), + /** + * Optional parameter of `view` command when `path` points to a file. If + * none is given, the full file is shown. If provided, the file will be + * shown in the indicated line number range, e.g. [11, 12] will show lines + * 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all + * lines from `start_line` to the end of the file. + */ + view_range: Schema.optional(Schema.Array(Schema.Number)) + } +}) + +/** + * @since 1.0.0 + * @category Tools + */ +export const WebSearch_20250305 = Tool.providerDefined({ + id: "anthropic.web_search_20250305", + toolkitName: "AnthropicWebSearch", + providerName: "web_search", + args: Struct.omit(Generated.WebSearchTool20250305.fields, "name", "type"), + success: Schema.Array(Generated.RequestWebSearchResultBlock), + failure: Generated.ResponseWebSearchToolResultError +}) + +const ProviderToolNamesMap: Map = new Map([ + ["bash", "AnthropicBash"], + ["code_execution", "AnthropicCodeExecution"], + ["computer", "AnthropicComputerUse"], + ["str_replace_based_edit_tool", "AnthropicTextEditor"], + ["str_replace_editor", "AnthropicTextEditor"], + ["web_search", "AnthropicWebSearch"] +]) + +/** + * A helper method which takes in the name of a tool as returned in the response + * from a large language model provider, and returns either the provider-defined + * name for of the tool as found in the corresponding `Toolkit`, or `undefined` + * if the tool name is not a provider-defined tool. + * + * For example, if the large language model provider returns the tool name + * `"web_search"` in a response, calling this method would return `"AnthropicWebSearch"`. + * + * This method is primarily exposed for use by other Effect provider + * integrations which can utilize Anthropic tools (i.e. Amazon Bedrock). + * + * @since 1.0.0 + * @category Tool Calling + */ +export const getProviderDefinedToolName = (name: string): string | undefined => ProviderToolNamesMap.get(name) diff --git a/repos/effect/packages/ai/anthropic/src/Generated.ts b/repos/effect/packages/ai/anthropic/src/Generated.ts new file mode 100644 index 0000000..dcb4afd --- /dev/null +++ b/repos/effect/packages/ai/anthropic/src/Generated.ts @@ -0,0 +1,7225 @@ +/** + * @since 1.0.0 + */ +import type * as HttpClient from "@effect/platform/HttpClient" +import * as HttpClientError from "@effect/platform/HttpClientError" +import * as HttpClientRequest from "@effect/platform/HttpClientRequest" +import * as HttpClientResponse from "@effect/platform/HttpClientResponse" +import * as Data from "effect/Data" +import * as Effect from "effect/Effect" +import type { ParseError } from "effect/ParseResult" +import * as S from "effect/Schema" + +export class MessagesPostParams extends S.Struct({ + /** + * The version of the Claude API you want to use. + * + * Read more about versioning and our version history [here](https://docs.claude.com/en/api/versioning). + */ + "anthropic-version": S.optionalWith(S.String, { nullable: true }) +}) {} + +/** + * The model that will complete your prompt.\n\nSee [models](https://docs.anthropic.com/en/docs/models-overview) for additional details and options. + */ +export class Model extends S.Union( + /** + * High-performance model with early extended thinking + */ + S.Literal("claude-3-7-sonnet-latest"), + /** + * High-performance model with early extended thinking + */ + S.Literal("claude-3-7-sonnet-20250219"), + /** + * Fastest and most compact model for near-instant responsiveness + */ + S.Literal("claude-3-5-haiku-latest"), + /** + * Our fastest model + */ + S.Literal("claude-3-5-haiku-20241022"), + /** + * Hybrid model, capable of near-instant responses and extended thinking + */ + S.Literal("claude-haiku-4-5"), + /** + * Hybrid model, capable of near-instant responses and extended thinking + */ + S.Literal("claude-haiku-4-5-20251001"), + /** + * High-performance model with extended thinking + */ + S.Literal("claude-sonnet-4-20250514"), + /** + * High-performance model with extended thinking + */ + S.Literal("claude-sonnet-4-0"), + /** + * High-performance model with extended thinking + */ + S.Literal("claude-4-sonnet-20250514"), + /** + * Our best model for real-world agents and coding + */ + S.Literal("claude-sonnet-4-5"), + /** + * Our best model for real-world agents and coding + */ + S.Literal("claude-sonnet-4-5-20250929"), + /** + * Our previous most intelligent model + */ + S.Literal("claude-3-5-sonnet-latest"), + /** + * Our previous most intelligent model + */ + S.Literal("claude-3-5-sonnet-20241022"), + S.Literal("claude-3-5-sonnet-20240620"), + /** + * Our most capable model + */ + S.Literal("claude-opus-4-0"), + /** + * Our most capable model + */ + S.Literal("claude-opus-4-20250514"), + /** + * Our most capable model + */ + S.Literal("claude-4-opus-20250514"), + /** + * Our most capable model + */ + S.Literal("claude-opus-4-1-20250805"), + /** + * Excels at writing and complex tasks + */ + S.Literal("claude-3-opus-latest"), + /** + * Excels at writing and complex tasks + */ + S.Literal("claude-3-opus-20240229"), + /** + * Our previous most fast and cost-effective + */ + S.Literal("claude-3-haiku-20240307") +) {} + +/** + * The time-to-live for the cache control breakpoint. + * + * This may be one the following values: + * - `5m`: 5 minutes + * - `1h`: 1 hour + * + * Defaults to `5m`. + */ +export class CacheControlEphemeralTtl extends S.Literal("5m", "1h") {} + +export class CacheControlEphemeral extends S.Class("CacheControlEphemeral")({ + /** + * The time-to-live for the cache control breakpoint. + * + * This may be one the following values: + * - `5m`: 5 minutes + * - `1h`: 1 hour + * + * Defaults to `5m`. + */ + "ttl": S.optionalWith(CacheControlEphemeralTtl, { nullable: true }), + "type": S.Literal("ephemeral") +}) {} + +export class RequestCharLocationCitation extends S.Class("RequestCharLocationCitation")({ + "cited_text": S.String, + "document_index": S.Int.pipe(S.greaterThanOrEqualTo(0)), + "document_title": S.NullOr(S.String.pipe(S.minLength(1), S.maxLength(255))), + "end_char_index": S.Int, + "start_char_index": S.Int.pipe(S.greaterThanOrEqualTo(0)), + "type": S.Literal("char_location") +}) {} + +export class RequestPageLocationCitation extends S.Class("RequestPageLocationCitation")({ + "cited_text": S.String, + "document_index": S.Int.pipe(S.greaterThanOrEqualTo(0)), + "document_title": S.NullOr(S.String.pipe(S.minLength(1), S.maxLength(255))), + "end_page_number": S.Int, + "start_page_number": S.Int.pipe(S.greaterThanOrEqualTo(1)), + "type": S.Literal("page_location") +}) {} + +export class RequestContentBlockLocationCitation + extends S.Class("RequestContentBlockLocationCitation")({ + "cited_text": S.String, + "document_index": S.Int.pipe(S.greaterThanOrEqualTo(0)), + "document_title": S.NullOr(S.String.pipe(S.minLength(1), S.maxLength(255))), + "end_block_index": S.Int, + "start_block_index": S.Int.pipe(S.greaterThanOrEqualTo(0)), + "type": S.Literal("content_block_location") + }) +{} + +export class RequestWebSearchResultLocationCitation + extends S.Class("RequestWebSearchResultLocationCitation")({ + "cited_text": S.String, + "encrypted_index": S.String, + "title": S.NullOr(S.String.pipe(S.minLength(1), S.maxLength(512))), + "type": S.Literal("web_search_result_location"), + "url": S.String.pipe(S.minLength(1), S.maxLength(2048)) + }) +{} + +export class RequestSearchResultLocationCitation + extends S.Class("RequestSearchResultLocationCitation")({ + "cited_text": S.String, + "end_block_index": S.Int, + "search_result_index": S.Int.pipe(S.greaterThanOrEqualTo(0)), + "source": S.String, + "start_block_index": S.Int.pipe(S.greaterThanOrEqualTo(0)), + "title": S.NullOr(S.String), + "type": S.Literal("search_result_location") + }) +{} + +export class RequestTextBlock extends S.Class("RequestTextBlock")({ + /** + * Create a cache control breakpoint at this content block. + */ + "cache_control": S.optionalWith(CacheControlEphemeral, { nullable: true }), + "citations": S.optionalWith( + S.Array( + S.Union( + RequestCharLocationCitation, + RequestPageLocationCitation, + RequestContentBlockLocationCitation, + RequestWebSearchResultLocationCitation, + RequestSearchResultLocationCitation + ) + ), + { nullable: true } + ), + "text": S.String.pipe(S.minLength(1)), + "type": S.Literal("text") +}) {} + +export class Base64ImageSourceMediaType extends S.Literal("image/jpeg", "image/png", "image/gif", "image/webp") {} + +export class Base64ImageSource extends S.Class("Base64ImageSource")({ + "data": S.String, + "media_type": Base64ImageSourceMediaType, + "type": S.Literal("base64") +}) {} + +export class URLImageSource extends S.Class("URLImageSource")({ + "type": S.Literal("url"), + "url": S.String +}) {} + +export class RequestImageBlock extends S.Class("RequestImageBlock")({ + /** + * Create a cache control breakpoint at this content block. + */ + "cache_control": S.optionalWith(CacheControlEphemeral, { nullable: true }), + "source": S.Union(Base64ImageSource, URLImageSource), + "type": S.Literal("image") +}) {} + +export class RequestCitationsConfig extends S.Class("RequestCitationsConfig")({ + "enabled": S.optionalWith(S.Boolean, { nullable: true }) +}) {} + +export class Base64PDFSource extends S.Class("Base64PDFSource")({ + "data": S.String, + "media_type": S.Literal("application/pdf"), + "type": S.Literal("base64") +}) {} + +export class PlainTextSource extends S.Class("PlainTextSource")({ + "data": S.String, + "media_type": S.Literal("text/plain"), + "type": S.Literal("text") +}) {} + +export class ContentBlockSource extends S.Class("ContentBlockSource")({ + "content": S.Union(S.String, S.Array(S.Union(RequestTextBlock, RequestImageBlock))), + "type": S.Literal("content") +}) {} + +export class URLPDFSource extends S.Class("URLPDFSource")({ + "type": S.Literal("url"), + "url": S.String +}) {} + +export class RequestDocumentBlock extends S.Class("RequestDocumentBlock")({ + /** + * Create a cache control breakpoint at this content block. + */ + "cache_control": S.optionalWith(CacheControlEphemeral, { nullable: true }), + "citations": S.optionalWith(RequestCitationsConfig, { nullable: true }), + "context": S.optionalWith(S.String.pipe(S.minLength(1)), { nullable: true }), + "source": S.Union(Base64PDFSource, PlainTextSource, ContentBlockSource, URLPDFSource), + "title": S.optionalWith(S.String.pipe(S.minLength(1), S.maxLength(500)), { nullable: true }), + "type": S.Literal("document") +}) {} + +export class RequestSearchResultBlock extends S.Class("RequestSearchResultBlock")({ + /** + * Create a cache control breakpoint at this content block. + */ + "cache_control": S.optionalWith(CacheControlEphemeral, { nullable: true }), + "citations": S.optionalWith(RequestCitationsConfig, { nullable: true }), + "content": S.Array(RequestTextBlock), + "source": S.String, + "title": S.String, + "type": S.Literal("search_result") +}) {} + +export class RequestThinkingBlock extends S.Class("RequestThinkingBlock")({ + "signature": S.String, + "thinking": S.String, + "type": S.Literal("thinking") +}) {} + +export class RequestRedactedThinkingBlock + extends S.Class("RequestRedactedThinkingBlock")({ + "data": S.String, + "type": S.Literal("redacted_thinking") + }) +{} + +export class RequestToolUseBlock extends S.Class("RequestToolUseBlock")({ + /** + * Create a cache control breakpoint at this content block. + */ + "cache_control": S.optionalWith(CacheControlEphemeral, { nullable: true }), + "id": S.String.pipe(S.pattern(new RegExp("^[a-zA-Z0-9_-]+$"))), + "input": S.Record({ key: S.String, value: S.Unknown }), + "name": S.String.pipe(S.minLength(1), S.maxLength(200)), + "type": S.Literal("tool_use") +}) {} + +export class RequestToolResultBlock extends S.Class("RequestToolResultBlock")({ + /** + * Create a cache control breakpoint at this content block. + */ + "cache_control": S.optionalWith(CacheControlEphemeral, { nullable: true }), + "content": S.optionalWith( + S.Union( + S.String, + S.Array(S.Union(RequestTextBlock, RequestImageBlock, RequestSearchResultBlock, RequestDocumentBlock)) + ), + { nullable: true } + ), + "is_error": S.optionalWith(S.Boolean, { nullable: true }), + "tool_use_id": S.String.pipe(S.pattern(new RegExp("^[a-zA-Z0-9_-]+$"))), + "type": S.Literal("tool_result") +}) {} + +export class RequestServerToolUseBlock extends S.Class("RequestServerToolUseBlock")({ + /** + * Create a cache control breakpoint at this content block. + */ + "cache_control": S.optionalWith(CacheControlEphemeral, { nullable: true }), + "id": S.String.pipe(S.pattern(new RegExp("^srvtoolu_[a-zA-Z0-9_]+$"))), + "input": S.Record({ key: S.String, value: S.Unknown }), + "name": S.Literal("web_search"), + "type": S.Literal("server_tool_use") +}) {} + +export class RequestWebSearchResultBlock extends S.Class("RequestWebSearchResultBlock")({ + "encrypted_content": S.String, + "page_age": S.optionalWith(S.String, { nullable: true }), + "title": S.String, + "type": S.Literal("web_search_result"), + "url": S.String +}) {} + +export class WebSearchToolResultErrorCode + extends S.Literal("invalid_tool_input", "unavailable", "max_uses_exceeded", "too_many_requests", "query_too_long") +{} + +export class RequestWebSearchToolResultError + extends S.Class("RequestWebSearchToolResultError")({ + "error_code": WebSearchToolResultErrorCode, + "type": S.Literal("web_search_tool_result_error") + }) +{} + +export class RequestWebSearchToolResultBlock + extends S.Class("RequestWebSearchToolResultBlock")({ + /** + * Create a cache control breakpoint at this content block. + */ + "cache_control": S.optionalWith(CacheControlEphemeral, { nullable: true }), + "content": S.Union(S.Array(RequestWebSearchResultBlock), RequestWebSearchToolResultError), + "tool_use_id": S.String.pipe(S.pattern(new RegExp("^srvtoolu_[a-zA-Z0-9_]+$"))), + "type": S.Literal("web_search_tool_result") + }) +{} + +export class InputContentBlock extends S.Union( + /** + * Regular text content. + */ + RequestTextBlock, + /** + * Image content specified directly as base64 data or as a reference via a URL. + */ + RequestImageBlock, + /** + * Document content, either specified directly as base64 data, as text, or as a reference via a URL. + */ + RequestDocumentBlock, + /** + * A search result block containing source, title, and content from search operations. + */ + RequestSearchResultBlock, + /** + * A block specifying internal thinking by the model. + */ + RequestThinkingBlock, + /** + * A block specifying internal, redacted thinking by the model. + */ + RequestRedactedThinkingBlock, + /** + * A block indicating a tool use by the model. + */ + RequestToolUseBlock, + /** + * A block specifying the results of a tool use by the model. + */ + RequestToolResultBlock, + RequestServerToolUseBlock, + RequestWebSearchToolResultBlock +) {} + +export class InputMessageRole extends S.Literal("user", "assistant") {} + +export class InputMessage extends S.Class("InputMessage")({ + "content": S.Union(S.String, S.Array(InputContentBlock)), + "role": InputMessageRole +}) {} + +export class Metadata extends S.Class("Metadata")({ + /** + * An external identifier for the user who is associated with the request. + * + * This should be a uuid, hash value, or other opaque identifier. Anthropic may use this id to help detect abuse. Do not include any identifying information such as name, email address, or phone number. + */ + "user_id": S.optionalWith(S.String.pipe(S.maxLength(256)), { nullable: true }) +}) {} + +/** + * Determines whether to use priority capacity (if available) or standard capacity for this request. + * + * Anthropic offers different levels of service for your API requests. See [service-tiers](https://docs.claude.com/en/api/service-tiers) for details. + */ +export class CreateMessageParamsServiceTier extends S.Literal("auto", "standard_only") {} + +export class ThinkingConfigEnabled extends S.Class("ThinkingConfigEnabled")({ + /** + * Determines how many tokens Claude can use for its internal reasoning process. Larger budgets can enable more thorough analysis for complex problems, improving response quality. + * + * Must be ≥1024 and less than `max_tokens`. + * + * See [extended thinking](https://docs.claude.com/en/docs/build-with-claude/extended-thinking) for details. + */ + "budget_tokens": S.Int.pipe(S.greaterThanOrEqualTo(1024)), + "type": S.Literal("enabled") +}) {} + +export class ThinkingConfigDisabled extends S.Class("ThinkingConfigDisabled")({ + "type": S.Literal("disabled") +}) {} + +/** + * Configuration for enabling Claude's extended thinking. + * + * When enabled, responses include `thinking` content blocks showing Claude's thinking process before the final answer. Requires a minimum budget of 1,024 tokens and counts towards your `max_tokens` limit. + * + * See [extended thinking](https://docs.claude.com/en/docs/build-with-claude/extended-thinking) for details. + */ +export class ThinkingConfigParam extends S.Union(ThinkingConfigEnabled, ThinkingConfigDisabled) {} + +/** + * The model will automatically decide whether to use tools. + */ +export class ToolChoiceAuto extends S.Class("ToolChoiceAuto")({ + /** + * Whether to disable parallel tool use. + * + * Defaults to `false`. If set to `true`, the model will output at most one tool use. + */ + "disable_parallel_tool_use": S.optionalWith(S.Boolean, { nullable: true }), + "type": S.Literal("auto") +}) {} + +/** + * The model will use any available tools. + */ +export class ToolChoiceAny extends S.Class("ToolChoiceAny")({ + /** + * Whether to disable parallel tool use. + * + * Defaults to `false`. If set to `true`, the model will output exactly one tool use. + */ + "disable_parallel_tool_use": S.optionalWith(S.Boolean, { nullable: true }), + "type": S.Literal("any") +}) {} + +/** + * The model will use the specified tool with `tool_choice.name`. + */ +export class ToolChoiceTool extends S.Class("ToolChoiceTool")({ + /** + * Whether to disable parallel tool use. + * + * Defaults to `false`. If set to `true`, the model will output exactly one tool use. + */ + "disable_parallel_tool_use": S.optionalWith(S.Boolean, { nullable: true }), + /** + * The name of the tool to use. + */ + "name": S.String, + "type": S.Literal("tool") +}) {} + +/** + * The model will not be allowed to use tools. + */ +export class ToolChoiceNone extends S.Class("ToolChoiceNone")({ + "type": S.Literal("none") +}) {} + +/** + * How the model should use the provided tools. The model can use a specific tool, any available tool, decide by itself, or not use tools at all. + */ +export class ToolChoice extends S.Union(ToolChoiceAuto, ToolChoiceAny, ToolChoiceTool, ToolChoiceNone) {} + +export class InputSchema extends S.Class("InputSchema")({ + "properties": S.optionalWith(S.Record({ key: S.String, value: S.Unknown }), { nullable: true }), + "required": S.optionalWith(S.Array(S.String), { nullable: true }), + "type": S.Literal("object") +}) {} + +export class Tool extends S.Class("Tool")({ + "type": S.optionalWith(S.Literal("custom"), { nullable: true }), + /** + * Description of what this tool does. + * + * Tool descriptions should be as detailed as possible. The more information that the model has about what the tool is and how to use it, the better it will perform. You can use natural language descriptions to reinforce important aspects of the tool input JSON schema. + */ + "description": S.optionalWith(S.String, { nullable: true }), + /** + * Name of the tool. + * + * This is how the tool will be called by the model and in `tool_use` blocks. + */ + "name": S.String.pipe(S.minLength(1), S.maxLength(128), S.pattern(new RegExp("^[a-zA-Z0-9_-]{1,128}$"))), + /** + * [JSON schema](https://json-schema.org/draft/2020-12) for this tool's input. + * + * This defines the shape of the `input` that your tool accepts and that the model will produce. + */ + "input_schema": InputSchema, + /** + * Create a cache control breakpoint at this content block. + */ + "cache_control": S.optionalWith(CacheControlEphemeral, { nullable: true }) +}) {} + +export class BashTool20250124 extends S.Class("BashTool20250124")({ + /** + * Create a cache control breakpoint at this content block. + */ + "cache_control": S.optionalWith(CacheControlEphemeral, { nullable: true }), + /** + * Name of the tool. + * + * This is how the tool will be called by the model and in `tool_use` blocks. + */ + "name": S.Literal("bash"), + "type": S.Literal("bash_20250124") +}) {} + +export class TextEditor20250124 extends S.Class("TextEditor20250124")({ + /** + * Create a cache control breakpoint at this content block. + */ + "cache_control": S.optionalWith(CacheControlEphemeral, { nullable: true }), + /** + * Name of the tool. + * + * This is how the tool will be called by the model and in `tool_use` blocks. + */ + "name": S.Literal("str_replace_editor"), + "type": S.Literal("text_editor_20250124") +}) {} + +export class TextEditor20250429 extends S.Class("TextEditor20250429")({ + /** + * Create a cache control breakpoint at this content block. + */ + "cache_control": S.optionalWith(CacheControlEphemeral, { nullable: true }), + /** + * Name of the tool. + * + * This is how the tool will be called by the model and in `tool_use` blocks. + */ + "name": S.Literal("str_replace_based_edit_tool"), + "type": S.Literal("text_editor_20250429") +}) {} + +export class TextEditor20250728 extends S.Class("TextEditor20250728")({ + /** + * Create a cache control breakpoint at this content block. + */ + "cache_control": S.optionalWith(CacheControlEphemeral, { nullable: true }), + /** + * Maximum number of characters to display when viewing a file. If not specified, defaults to displaying the full file. + */ + "max_characters": S.optionalWith(S.Int.pipe(S.greaterThanOrEqualTo(1)), { nullable: true }), + /** + * Name of the tool. + * + * This is how the tool will be called by the model and in `tool_use` blocks. + */ + "name": S.Literal("str_replace_based_edit_tool"), + "type": S.Literal("text_editor_20250728") +}) {} + +export class UserLocation extends S.Class("UserLocation")({ + /** + * The city of the user. + */ + "city": S.optionalWith(S.String.pipe(S.minLength(1), S.maxLength(255)), { nullable: true }), + /** + * The two letter [ISO country code](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2) of the user. + */ + "country": S.optionalWith(S.String.pipe(S.minLength(2), S.maxLength(2)), { nullable: true }), + /** + * The region of the user. + */ + "region": S.optionalWith(S.String.pipe(S.minLength(1), S.maxLength(255)), { nullable: true }), + /** + * The [IANA timezone](https://nodatime.org/TimeZones) of the user. + */ + "timezone": S.optionalWith(S.String.pipe(S.minLength(1), S.maxLength(255)), { nullable: true }), + "type": S.Literal("approximate") +}) {} + +export class WebSearchTool20250305 extends S.Class("WebSearchTool20250305")({ + /** + * If provided, only these domains will be included in results. Cannot be used alongside `blocked_domains`. + */ + "allowed_domains": S.optionalWith(S.Array(S.String), { nullable: true }), + /** + * If provided, these domains will never appear in results. Cannot be used alongside `allowed_domains`. + */ + "blocked_domains": S.optionalWith(S.Array(S.String), { nullable: true }), + /** + * Create a cache control breakpoint at this content block. + */ + "cache_control": S.optionalWith(CacheControlEphemeral, { nullable: true }), + /** + * Maximum number of times the tool can be used in the API request. + */ + "max_uses": S.optionalWith(S.Int.pipe(S.greaterThan(0)), { nullable: true }), + /** + * Name of the tool. + * + * This is how the tool will be called by the model and in `tool_use` blocks. + */ + "name": S.Literal("web_search"), + "type": S.Literal("web_search_20250305"), + /** + * Parameters for the user's location. Used to provide more relevant search results. + */ + "user_location": S.optionalWith(UserLocation, { nullable: true }) +}) {} + +export class CreateMessageParams extends S.Class("CreateMessageParams")({ + "model": S.Union(S.String, Model), + /** + * Input messages. + * + * Our models are trained to operate on alternating `user` and `assistant` conversational turns. When creating a new `Message`, you specify the prior conversational turns with the `messages` parameter, and the model then generates the next `Message` in the conversation. Consecutive `user` or `assistant` turns in your request will be combined into a single turn. + * + * Each input message must be an object with a `role` and `content`. You can specify a single `user`-role message, or you can include multiple `user` and `assistant` messages. + * + * If the final message uses the `assistant` role, the response content will continue immediately from the content in that message. This can be used to constrain part of the model's response. + * + * Example with a single `user` message: + * + * ```json + * [{"role": "user", "content": "Hello, Claude"}] + * ``` + * + * Example with multiple conversational turns: + * + * ```json + * [ + * {"role": "user", "content": "Hello there."}, + * {"role": "assistant", "content": "Hi, I'm Claude. How can I help you?"}, + * {"role": "user", "content": "Can you explain LLMs in plain English?"}, + * ] + * ``` + * + * Example with a partially-filled response from Claude: + * + * ```json + * [ + * {"role": "user", "content": "What's the Greek name for Sun? (A) Sol (B) Helios (C) Sun"}, + * {"role": "assistant", "content": "The best answer is ("}, + * ] + * ``` + * + * Each input message `content` may be either a single `string` or an array of content blocks, where each block has a specific `type`. Using a `string` for `content` is shorthand for an array of one content block of type `"text"`. The following input messages are equivalent: + * + * ```json + * {"role": "user", "content": "Hello, Claude"} + * ``` + * + * ```json + * {"role": "user", "content": [{"type": "text", "text": "Hello, Claude"}]} + * ``` + * + * See [input examples](https://docs.claude.com/en/api/messages-examples). + * + * Note that if you want to include a [system prompt](https://docs.claude.com/en/docs/system-prompts), you can use the top-level `system` parameter — there is no `"system"` role for input messages in the Messages API. + * + * There is a limit of 100,000 messages in a single request. + */ + "messages": S.Array(InputMessage), + /** + * The maximum number of tokens to generate before stopping. + * + * Note that our models may stop _before_ reaching this maximum. This parameter only specifies the absolute maximum number of tokens to generate. + * + * Different models have different maximum values for this parameter. See [models](https://docs.claude.com/en/docs/models-overview) for details. + */ + "max_tokens": S.Int.pipe(S.greaterThanOrEqualTo(1)), + /** + * An object describing metadata about the request. + */ + "metadata": S.optionalWith(Metadata, { nullable: true }), + /** + * Determines whether to use priority capacity (if available) or standard capacity for this request. + * + * Anthropic offers different levels of service for your API requests. See [service-tiers](https://docs.claude.com/en/api/service-tiers) for details. + */ + "service_tier": S.optionalWith(CreateMessageParamsServiceTier, { nullable: true }), + /** + * Custom text sequences that will cause the model to stop generating. + * + * Our models will normally stop when they have naturally completed their turn, which will result in a response `stop_reason` of `"end_turn"`. + * + * If you want the model to stop generating when it encounters custom strings of text, you can use the `stop_sequences` parameter. If the model encounters one of the custom sequences, the response `stop_reason` value will be `"stop_sequence"` and the response `stop_sequence` value will contain the matched stop sequence. + */ + "stop_sequences": S.optionalWith(S.Array(S.String), { nullable: true }), + /** + * Whether to incrementally stream the response using server-sent events. + * + * See [streaming](https://docs.claude.com/en/api/messages-streaming) for details. + */ + "stream": S.optionalWith(S.Boolean, { nullable: true }), + /** + * System prompt. + * + * A system prompt is a way of providing context and instructions to Claude, such as specifying a particular goal or role. See our [guide to system prompts](https://docs.claude.com/en/docs/system-prompts). + */ + "system": S.optionalWith(S.Union(S.String, S.Array(RequestTextBlock)), { nullable: true }), + /** + * Amount of randomness injected into the response. + * + * Defaults to `1.0`. Ranges from `0.0` to `1.0`. Use `temperature` closer to `0.0` for analytical / multiple choice, and closer to `1.0` for creative and generative tasks. + * + * Note that even with `temperature` of `0.0`, the results will not be fully deterministic. + */ + "temperature": S.optionalWith(S.Number.pipe(S.greaterThanOrEqualTo(0), S.lessThanOrEqualTo(1)), { nullable: true }), + "thinking": S.optionalWith(ThinkingConfigParam, { nullable: true }), + "tool_choice": S.optionalWith(ToolChoice, { nullable: true }), + /** + * Definitions of tools that the model may use. + * + * If you include `tools` in your API request, the model may return `tool_use` content blocks that represent the model's use of those tools. You can then run those tools using the tool input generated by the model and then optionally return results back to the model using `tool_result` content blocks. + * + * There are two types of tools: **client tools** and **server tools**. The behavior described below applies to client tools. For [server tools](https://docs.claude.com/en/docs/agents-and-tools/tool-use/overview\#server-tools), see their individual documentation as each has its own behavior (e.g., the [web search tool](https://docs.claude.com/en/docs/agents-and-tools/tool-use/web-search-tool)). + * + * Each tool definition includes: + * + * * `name`: Name of the tool. + * * `description`: Optional, but strongly-recommended description of the tool. + * * `input_schema`: [JSON schema](https://json-schema.org/draft/2020-12) for the tool `input` shape that the model will produce in `tool_use` output content blocks. + * + * For example, if you defined `tools` as: + * + * ```json + * [ + * { + * "name": "get_stock_price", + * "description": "Get the current stock price for a given ticker symbol.", + * "input_schema": { + * "type": "object", + * "properties": { + * "ticker": { + * "type": "string", + * "description": "The stock ticker symbol, e.g. AAPL for Apple Inc." + * } + * }, + * "required": ["ticker"] + * } + * } + * ] + * ``` + * + * And then asked the model "What's the S&P 500 at today?", the model might produce `tool_use` content blocks in the response like this: + * + * ```json + * [ + * { + * "type": "tool_use", + * "id": "toolu_01D7FLrfh4GYq7yT1ULFeyMV", + * "name": "get_stock_price", + * "input": { "ticker": "^GSPC" } + * } + * ] + * ``` + * + * You might then run your `get_stock_price` tool with `{"ticker": "^GSPC"}` as an input, and return the following back to the model in a subsequent `user` message: + * + * ```json + * [ + * { + * "type": "tool_result", + * "tool_use_id": "toolu_01D7FLrfh4GYq7yT1ULFeyMV", + * "content": "259.75 USD" + * } + * ] + * ``` + * + * Tools can be used for workflows that include running client-side tools and functions, or more generally whenever you want the model to produce a particular JSON structure of output. + * + * See our [guide](https://docs.claude.com/en/docs/tool-use) for more details. + */ + "tools": S.optionalWith( + S.Array( + S.Union(Tool, BashTool20250124, TextEditor20250124, TextEditor20250429, TextEditor20250728, WebSearchTool20250305) + ), + { nullable: true } + ), + /** + * Only sample from the top K options for each subsequent token. + * + * Used to remove "long tail" low probability responses. [Learn more technical details here](https://towardsdatascience.com/how-to-sample-from-language-models-682bceb97277). + * + * Recommended for advanced use cases only. You usually only need to use `temperature`. + */ + "top_k": S.optionalWith(S.Int.pipe(S.greaterThanOrEqualTo(0)), { nullable: true }), + /** + * Use nucleus sampling. + * + * In nucleus sampling, we compute the cumulative distribution over all the options for each subsequent token in decreasing probability order and cut it off once it reaches a particular probability specified by `top_p`. You should either alter `temperature` or `top_p`, but not both. + * + * Recommended for advanced use cases only. You usually only need to use `temperature`. + */ + "top_p": S.optionalWith(S.Number.pipe(S.greaterThanOrEqualTo(0), S.lessThanOrEqualTo(1)), { nullable: true }) +}) {} + +export class ResponseCharLocationCitation + extends S.Class("ResponseCharLocationCitation")({ + "cited_text": S.String, + "document_index": S.Int.pipe(S.greaterThanOrEqualTo(0)), + "document_title": S.NullOr(S.String), + "end_char_index": S.Int, + "file_id": S.optionalWith(S.NullOr(S.String), { default: () => null }), + "start_char_index": S.Int.pipe(S.greaterThanOrEqualTo(0)), + "type": S.Literal("char_location").pipe( + S.propertySignature, + S.withConstructorDefault(() => "char_location" as const) + ) + }) +{} + +export class ResponsePageLocationCitation + extends S.Class("ResponsePageLocationCitation")({ + "cited_text": S.String, + "document_index": S.Int.pipe(S.greaterThanOrEqualTo(0)), + "document_title": S.NullOr(S.String), + "end_page_number": S.Int, + "file_id": S.optionalWith(S.NullOr(S.String), { default: () => null }), + "start_page_number": S.Int.pipe(S.greaterThanOrEqualTo(1)), + "type": S.Literal("page_location").pipe( + S.propertySignature, + S.withConstructorDefault(() => "page_location" as const) + ) + }) +{} + +export class ResponseContentBlockLocationCitation + extends S.Class("ResponseContentBlockLocationCitation")({ + "cited_text": S.String, + "document_index": S.Int.pipe(S.greaterThanOrEqualTo(0)), + "document_title": S.NullOr(S.String), + "end_block_index": S.Int, + "file_id": S.optionalWith(S.NullOr(S.String), { default: () => null }), + "start_block_index": S.Int.pipe(S.greaterThanOrEqualTo(0)), + "type": S.Literal("content_block_location").pipe( + S.propertySignature, + S.withConstructorDefault(() => "content_block_location" as const) + ) + }) +{} + +export class ResponseWebSearchResultLocationCitation + extends S.Class("ResponseWebSearchResultLocationCitation")({ + "cited_text": S.String, + "encrypted_index": S.String, + "title": S.NullOr(S.String.pipe(S.maxLength(512))), + "type": S.Literal("web_search_result_location").pipe( + S.propertySignature, + S.withConstructorDefault(() => "web_search_result_location" as const) + ), + "url": S.String + }) +{} + +export class ResponseSearchResultLocationCitation + extends S.Class("ResponseSearchResultLocationCitation")({ + "cited_text": S.String, + "end_block_index": S.Int, + "search_result_index": S.Int.pipe(S.greaterThanOrEqualTo(0)), + "source": S.String, + "start_block_index": S.Int.pipe(S.greaterThanOrEqualTo(0)), + "title": S.NullOr(S.String), + "type": S.Literal("search_result_location").pipe( + S.propertySignature, + S.withConstructorDefault(() => "search_result_location" as const) + ) + }) +{} + +export class ResponseTextBlock extends S.Class("ResponseTextBlock")({ + /** + * Citations supporting the text block. + * + * The type of citation returned will depend on the type of document being cited. Citing a PDF results in `page_location`, plain text results in `char_location`, and content document results in `content_block_location`. + */ + "citations": S.optionalWith( + S.NullOr( + S.Array( + S.Union( + ResponseCharLocationCitation, + ResponsePageLocationCitation, + ResponseContentBlockLocationCitation, + ResponseWebSearchResultLocationCitation, + ResponseSearchResultLocationCitation + ) + ) + ), + { default: () => null } + ), + "text": S.String.pipe(S.minLength(0), S.maxLength(5000000)), + "type": S.Literal("text").pipe(S.propertySignature, S.withConstructorDefault(() => "text" as const)) +}) {} + +export class ResponseThinkingBlock extends S.Class("ResponseThinkingBlock")({ + "signature": S.String, + "thinking": S.String, + "type": S.Literal("thinking").pipe(S.propertySignature, S.withConstructorDefault(() => "thinking" as const)) +}) {} + +export class ResponseRedactedThinkingBlock + extends S.Class("ResponseRedactedThinkingBlock")({ + "data": S.String, + "type": S.Literal("redacted_thinking").pipe( + S.propertySignature, + S.withConstructorDefault(() => "redacted_thinking" as const) + ) + }) +{} + +export class ResponseToolUseBlock extends S.Class("ResponseToolUseBlock")({ + "id": S.String.pipe(S.pattern(new RegExp("^[a-zA-Z0-9_-]+$"))), + "input": S.Record({ key: S.String, value: S.Unknown }), + "name": S.String.pipe(S.minLength(1)), + "type": S.Literal("tool_use").pipe(S.propertySignature, S.withConstructorDefault(() => "tool_use" as const)) +}) {} + +export class ResponseServerToolUseBlock extends S.Class("ResponseServerToolUseBlock")({ + "id": S.String.pipe(S.pattern(new RegExp("^srvtoolu_[a-zA-Z0-9_]+$"))), + "input": S.Record({ key: S.String, value: S.Unknown }), + "name": S.Literal("web_search"), + "type": S.Literal("server_tool_use").pipe( + S.propertySignature, + S.withConstructorDefault(() => "server_tool_use" as const) + ) +}) {} + +export class ResponseWebSearchToolResultError + extends S.Class("ResponseWebSearchToolResultError")({ + "error_code": WebSearchToolResultErrorCode, + "type": S.Literal("web_search_tool_result_error").pipe( + S.propertySignature, + S.withConstructorDefault(() => "web_search_tool_result_error" as const) + ) + }) +{} + +export class ResponseWebSearchResultBlock + extends S.Class("ResponseWebSearchResultBlock")({ + "encrypted_content": S.String, + "page_age": S.optionalWith(S.NullOr(S.String), { default: () => null }), + "title": S.String, + "type": S.Literal("web_search_result").pipe( + S.propertySignature, + S.withConstructorDefault(() => "web_search_result" as const) + ), + "url": S.String + }) +{} + +export class ResponseWebSearchToolResultBlock + extends S.Class("ResponseWebSearchToolResultBlock")({ + "content": S.Union(ResponseWebSearchToolResultError, S.Array(ResponseWebSearchResultBlock)), + "tool_use_id": S.String.pipe(S.pattern(new RegExp("^srvtoolu_[a-zA-Z0-9_]+$"))), + "type": S.Literal("web_search_tool_result").pipe( + S.propertySignature, + S.withConstructorDefault(() => "web_search_tool_result" as const) + ) + }) +{} + +export class ContentBlock extends S.Union( + ResponseTextBlock, + ResponseThinkingBlock, + ResponseRedactedThinkingBlock, + ResponseToolUseBlock, + ResponseServerToolUseBlock, + ResponseWebSearchToolResultBlock +) {} + +export class StopReason + extends S.Literal("end_turn", "max_tokens", "stop_sequence", "tool_use", "pause_turn", "refusal") +{} + +export class CacheCreation extends S.Class("CacheCreation")({ + /** + * The number of input tokens used to create the 1 hour cache entry. + */ + "ephemeral_1h_input_tokens": S.Int.pipe(S.greaterThanOrEqualTo(0)).pipe( + S.propertySignature, + S.withConstructorDefault(() => 0 as const) + ), + /** + * The number of input tokens used to create the 5 minute cache entry. + */ + "ephemeral_5m_input_tokens": S.Int.pipe(S.greaterThanOrEqualTo(0)).pipe( + S.propertySignature, + S.withConstructorDefault(() => 0 as const) + ) +}) {} + +export class ServerToolUsage extends S.Class("ServerToolUsage")({ + /** + * The number of web search tool requests. + */ + "web_search_requests": S.Int.pipe(S.greaterThanOrEqualTo(0)).pipe( + S.propertySignature, + S.withConstructorDefault(() => 0 as const) + ) +}) {} + +export class UsageServiceTierEnum extends S.Literal("standard", "priority", "batch") {} + +export class Usage extends S.Class("Usage")({ + /** + * Breakdown of cached tokens by TTL + */ + "cache_creation": S.optionalWith(S.NullOr(CacheCreation), { default: () => null }), + /** + * The number of input tokens used to create the cache entry. + */ + "cache_creation_input_tokens": S.optionalWith(S.NullOr(S.Int.pipe(S.greaterThanOrEqualTo(0))), { + default: () => null + }), + /** + * The number of input tokens read from the cache. + */ + "cache_read_input_tokens": S.optionalWith(S.NullOr(S.Int.pipe(S.greaterThanOrEqualTo(0))), { default: () => null }), + /** + * The number of input tokens which were used. + */ + "input_tokens": S.Int.pipe(S.greaterThanOrEqualTo(0)), + /** + * The number of output tokens which were used. + */ + "output_tokens": S.Int.pipe(S.greaterThanOrEqualTo(0)), + /** + * The number of server tool requests. + */ + "server_tool_use": S.optionalWith(S.NullOr(ServerToolUsage), { default: () => null }), + /** + * If the request used the priority, standard, or batch tier. + */ + "service_tier": S.optionalWith(S.NullOr(UsageServiceTierEnum), { default: () => null }) +}) {} + +export class Message extends S.Class("Message")({ + /** + * Unique object identifier. + * + * The format and length of IDs may change over time. + */ + "id": S.String, + /** + * Object type. + * + * For Messages, this is always `"message"`. + */ + "type": S.Literal("message").pipe(S.propertySignature, S.withConstructorDefault(() => "message" as const)), + /** + * Conversational role of the generated message. + * + * This will always be `"assistant"`. + */ + "role": S.Literal("assistant").pipe(S.propertySignature, S.withConstructorDefault(() => "assistant" as const)), + /** + * Content generated by the model. + * + * This is an array of content blocks, each of which has a `type` that determines its shape. + * + * Example: + * + * ```json + * [{"type": "text", "text": "Hi, I'm Claude."}] + * ``` + * + * If the request input `messages` ended with an `assistant` turn, then the response `content` will continue directly from that last turn. You can use this to constrain the model's output. + * + * For example, if the input `messages` were: + * ```json + * [ + * {"role": "user", "content": "What's the Greek name for Sun? (A) Sol (B) Helios (C) Sun"}, + * {"role": "assistant", "content": "The best answer is ("} + * ] + * ``` + * + * Then the response `content` might be: + * + * ```json + * [{"type": "text", "text": "B)"}] + * ``` + */ + "content": S.Array(ContentBlock), + "model": S.Union(S.String, Model), + /** + * The reason that we stopped. + * + * This may be one the following values: + * * `"end_turn"`: the model reached a natural stopping point + * * `"max_tokens"`: we exceeded the requested `max_tokens` or the model's maximum + * * `"stop_sequence"`: one of your provided custom `stop_sequences` was generated + * * `"tool_use"`: the model invoked one or more tools + * * `"pause_turn"`: we paused a long-running turn. You may provide the response back as-is in a subsequent request to let the model continue. + * * `"refusal"`: when streaming classifiers intervene to handle potential policy violations + * + * In non-streaming mode this value is always non-null. In streaming mode, it is null in the `message_start` event and non-null otherwise. + */ + "stop_reason": S.NullOr(StopReason), + /** + * Which custom stop sequence was generated, if any. + * + * This value will be a non-null string if one of your custom stop sequences was generated. + */ + "stop_sequence": S.optionalWith(S.NullOr(S.String), { default: () => null }), + /** + * Billing and rate-limit usage. + * + * Anthropic's API bills and rate-limits by token counts, as tokens represent the underlying cost to our systems. + * + * Under the hood, the API transforms requests into a format suitable for the model. The model's output then goes through a parsing stage before becoming an API response. As a result, the token counts in `usage` will not match one-to-one with the exact visible content of an API request or response. + * + * For example, `output_tokens` will be non-zero, even for an empty string response from Claude. + * + * Total input tokens in a request is the summation of `input_tokens`, `cache_creation_input_tokens`, and `cache_read_input_tokens`. + */ + "usage": Usage +}) {} + +export class InvalidRequestError extends S.Class("InvalidRequestError")({ + "message": S.String.pipe(S.propertySignature, S.withConstructorDefault(() => "Invalid request" as const)), + "type": S.Literal("invalid_request_error").pipe( + S.propertySignature, + S.withConstructorDefault(() => "invalid_request_error" as const) + ) +}) {} + +export class AuthenticationError extends S.Class("AuthenticationError")({ + "message": S.String.pipe(S.propertySignature, S.withConstructorDefault(() => "Authentication error" as const)), + "type": S.Literal("authentication_error").pipe( + S.propertySignature, + S.withConstructorDefault(() => "authentication_error" as const) + ) +}) {} + +export class BillingError extends S.Class("BillingError")({ + "message": S.String.pipe(S.propertySignature, S.withConstructorDefault(() => "Billing error" as const)), + "type": S.Literal("billing_error").pipe(S.propertySignature, S.withConstructorDefault(() => "billing_error" as const)) +}) {} + +export class PermissionError extends S.Class("PermissionError")({ + "message": S.String.pipe(S.propertySignature, S.withConstructorDefault(() => "Permission denied" as const)), + "type": S.Literal("permission_error").pipe( + S.propertySignature, + S.withConstructorDefault(() => "permission_error" as const) + ) +}) {} + +export class NotFoundError extends S.Class("NotFoundError")({ + "message": S.String.pipe(S.propertySignature, S.withConstructorDefault(() => "Not found" as const)), + "type": S.Literal("not_found_error").pipe( + S.propertySignature, + S.withConstructorDefault(() => "not_found_error" as const) + ) +}) {} + +export class RateLimitError extends S.Class("RateLimitError")({ + "message": S.String.pipe(S.propertySignature, S.withConstructorDefault(() => "Rate limited" as const)), + "type": S.Literal("rate_limit_error").pipe( + S.propertySignature, + S.withConstructorDefault(() => "rate_limit_error" as const) + ) +}) {} + +export class GatewayTimeoutError extends S.Class("GatewayTimeoutError")({ + "message": S.String.pipe(S.propertySignature, S.withConstructorDefault(() => "Request timeout" as const)), + "type": S.Literal("timeout_error").pipe(S.propertySignature, S.withConstructorDefault(() => "timeout_error" as const)) +}) {} + +export class APIError extends S.Class("APIError")({ + "message": S.String.pipe(S.propertySignature, S.withConstructorDefault(() => "Internal server error" as const)), + "type": S.Literal("api_error").pipe(S.propertySignature, S.withConstructorDefault(() => "api_error" as const)) +}) {} + +export class OverloadedError extends S.Class("OverloadedError")({ + "message": S.String.pipe(S.propertySignature, S.withConstructorDefault(() => "Overloaded" as const)), + "type": S.Literal("overloaded_error").pipe( + S.propertySignature, + S.withConstructorDefault(() => "overloaded_error" as const) + ) +}) {} + +export class ErrorResponse extends S.Class("ErrorResponse")({ + "error": S.Union( + InvalidRequestError, + AuthenticationError, + BillingError, + PermissionError, + NotFoundError, + RateLimitError, + GatewayTimeoutError, + APIError, + OverloadedError + ), + "request_id": S.optionalWith(S.NullOr(S.String), { default: () => null }), + "type": S.Literal("error").pipe(S.propertySignature, S.withConstructorDefault(() => "error" as const)) +}) {} + +export class CompletePostParams extends S.Struct({ + /** + * The version of the Claude API you want to use. + * + * Read more about versioning and our version history [here](https://docs.claude.com/en/api/versioning). + */ + "anthropic-version": S.optionalWith(S.String, { nullable: true }), + /** + * Optional header to specify the beta version(s) you want to use. + * + * To use multiple betas, use a comma separated list like `beta1,beta2` or specify the header multiple times for each beta. + */ + "anthropic-beta": S.optionalWith(S.String, { nullable: true }) +}) {} + +export class CompletionRequest extends S.Class("CompletionRequest")({ + "model": S.Union(S.String, Model), + /** + * The prompt that you want Claude to complete. + * + * For proper response generation you will need to format your prompt using alternating `\n\nHuman:` and `\n\nAssistant:` conversational turns. For example: + * + * ``` + * "\n\nHuman: {userQuestion}\n\nAssistant:" + * ``` + * + * See [prompt validation](https://docs.claude.com/en/api/prompt-validation) and our guide to [prompt design](https://docs.claude.com/en/docs/intro-to-prompting) for more details. + */ + "prompt": S.String.pipe(S.minLength(1)), + /** + * The maximum number of tokens to generate before stopping. + * + * Note that our models may stop _before_ reaching this maximum. This parameter only specifies the absolute maximum number of tokens to generate. + */ + "max_tokens_to_sample": S.Int.pipe(S.greaterThanOrEqualTo(1)), + /** + * Sequences that will cause the model to stop generating. + * + * Our models stop on `"\n\nHuman:"`, and may include additional built-in stop sequences in the future. By providing the stop_sequences parameter, you may include additional strings that will cause the model to stop generating. + */ + "stop_sequences": S.optionalWith(S.Array(S.String), { nullable: true }), + /** + * Amount of randomness injected into the response. + * + * Defaults to `1.0`. Ranges from `0.0` to `1.0`. Use `temperature` closer to `0.0` for analytical / multiple choice, and closer to `1.0` for creative and generative tasks. + * + * Note that even with `temperature` of `0.0`, the results will not be fully deterministic. + */ + "temperature": S.optionalWith(S.Number.pipe(S.greaterThanOrEqualTo(0), S.lessThanOrEqualTo(1)), { nullable: true }), + /** + * Use nucleus sampling. + * + * In nucleus sampling, we compute the cumulative distribution over all the options for each subsequent token in decreasing probability order and cut it off once it reaches a particular probability specified by `top_p`. You should either alter `temperature` or `top_p`, but not both. + * + * Recommended for advanced use cases only. You usually only need to use `temperature`. + */ + "top_p": S.optionalWith(S.Number.pipe(S.greaterThanOrEqualTo(0), S.lessThanOrEqualTo(1)), { nullable: true }), + /** + * Only sample from the top K options for each subsequent token. + * + * Used to remove "long tail" low probability responses. [Learn more technical details here](https://towardsdatascience.com/how-to-sample-from-language-models-682bceb97277). + * + * Recommended for advanced use cases only. You usually only need to use `temperature`. + */ + "top_k": S.optionalWith(S.Int.pipe(S.greaterThanOrEqualTo(0)), { nullable: true }), + /** + * An object describing metadata about the request. + */ + "metadata": S.optionalWith(Metadata, { nullable: true }), + /** + * Whether to incrementally stream the response using server-sent events. + * + * See [streaming](https://docs.claude.com/en/api/streaming) for details. + */ + "stream": S.optionalWith(S.Boolean, { nullable: true }) +}) {} + +export class CompletionResponse extends S.Class("CompletionResponse")({ + /** + * The resulting completion up to and excluding the stop sequences. + */ + "completion": S.String, + /** + * Unique object identifier. + * + * The format and length of IDs may change over time. + */ + "id": S.String, + "model": S.Union(S.String, Model), + /** + * The reason that we stopped. + * + * This may be one the following values: + * * `"stop_sequence"`: we reached a stop sequence — either provided by you via the `stop_sequences` parameter, or a stop sequence built into the model + * * `"max_tokens"`: we exceeded `max_tokens_to_sample` or the model's maximum + */ + "stop_reason": S.NullOr(S.String), + /** + * Object type. + * + * For Text Completions, this is always `"completion"`. + */ + "type": S.Literal("completion").pipe(S.propertySignature, S.withConstructorDefault(() => "completion" as const)) +}) {} + +export class ModelsListParams extends S.Struct({ + /** + * ID of the object to use as a cursor for pagination. When provided, returns the page of results immediately before this object. + */ + "before_id": S.optionalWith(S.String, { nullable: true }), + /** + * ID of the object to use as a cursor for pagination. When provided, returns the page of results immediately after this object. + */ + "after_id": S.optionalWith(S.String, { nullable: true }), + /** + * Number of items to return per page. + * + * Defaults to `20`. Ranges from `1` to `1000`. + */ + "limit": S.optionalWith(S.Int.pipe(S.greaterThanOrEqualTo(1), S.lessThanOrEqualTo(1000)), { + nullable: true, + default: () => 20 as const + }), + /** + * The version of the Claude API you want to use. + * + * Read more about versioning and our version history [here](https://docs.claude.com/en/api/versioning). + */ + "anthropic-version": S.optionalWith(S.String, { nullable: true }), + /** + * Your unique API key for authentication. + * + * This key is required in the header of all API requests, to authenticate your account and access Anthropic's services. Get your API key through the [Console](https://console.anthropic.com/settings/keys). Each key is scoped to a Workspace. + */ + "x-api-key": S.optionalWith(S.String, { nullable: true }), + /** + * Optional header to specify the beta version(s) you want to use. + * + * To use multiple betas, use a comma separated list like `beta1,beta2` or specify the header multiple times for each beta. + */ + "anthropic-beta": S.optionalWith(S.String, { nullable: true }) +}) {} + +export class ModelInfo extends S.Class("ModelInfo")({ + /** + * RFC 3339 datetime string representing the time at which the model was released. May be set to an epoch value if the release date is unknown. + */ + "created_at": S.String, + /** + * A human-readable name for the model. + */ + "display_name": S.String, + /** + * Unique model identifier. + */ + "id": S.String, + /** + * Object type. + * + * For Models, this is always `"model"`. + */ + "type": S.Literal("model").pipe(S.propertySignature, S.withConstructorDefault(() => "model" as const)) +}) {} + +export class ListResponseModelInfo extends S.Class("ListResponseModelInfo")({ + "data": S.Array(ModelInfo), + /** + * First ID in the `data` list. Can be used as the `before_id` for the previous page. + */ + "first_id": S.NullOr(S.String), + /** + * Indicates if there are more results in the requested page direction. + */ + "has_more": S.Boolean, + /** + * Last ID in the `data` list. Can be used as the `after_id` for the next page. + */ + "last_id": S.NullOr(S.String) +}) {} + +export class ModelsGetParams extends S.Struct({ + /** + * The version of the Claude API you want to use. + * + * Read more about versioning and our version history [here](https://docs.claude.com/en/api/versioning). + */ + "anthropic-version": S.optionalWith(S.String, { nullable: true }), + /** + * Your unique API key for authentication. + * + * This key is required in the header of all API requests, to authenticate your account and access Anthropic's services. Get your API key through the [Console](https://console.anthropic.com/settings/keys). Each key is scoped to a Workspace. + */ + "x-api-key": S.optionalWith(S.String, { nullable: true }), + /** + * Optional header to specify the beta version(s) you want to use. + * + * To use multiple betas, use a comma separated list like `beta1,beta2` or specify the header multiple times for each beta. + */ + "anthropic-beta": S.optionalWith(S.String, { nullable: true }) +}) {} + +export class MessageBatchesListParams extends S.Struct({ + /** + * ID of the object to use as a cursor for pagination. When provided, returns the page of results immediately before this object. + */ + "before_id": S.optionalWith(S.String, { nullable: true }), + /** + * ID of the object to use as a cursor for pagination. When provided, returns the page of results immediately after this object. + */ + "after_id": S.optionalWith(S.String, { nullable: true }), + /** + * Number of items to return per page. + * + * Defaults to `20`. Ranges from `1` to `1000`. + */ + "limit": S.optionalWith(S.Int.pipe(S.greaterThanOrEqualTo(1), S.lessThanOrEqualTo(1000)), { + nullable: true, + default: () => 20 as const + }), + /** + * The version of the Claude API you want to use. + * + * Read more about versioning and our version history [here](https://docs.claude.com/en/api/versioning). + */ + "anthropic-version": S.optionalWith(S.String, { nullable: true }), + /** + * Your unique API key for authentication. + * + * This key is required in the header of all API requests, to authenticate your account and access Anthropic's services. Get your API key through the [Console](https://console.anthropic.com/settings/keys). Each key is scoped to a Workspace. + */ + "x-api-key": S.optionalWith(S.String, { nullable: true }) +}) {} + +/** + * Processing status of the Message Batch. + */ +export class MessageBatchProcessingStatus extends S.Literal("in_progress", "canceling", "ended") {} + +export class RequestCounts extends S.Class("RequestCounts")({ + /** + * Number of requests in the Message Batch that have been canceled. + * + * This is zero until processing of the entire Message Batch has ended. + */ + "canceled": S.Int.pipe(S.propertySignature, S.withConstructorDefault(() => 0 as const)), + /** + * Number of requests in the Message Batch that encountered an error. + * + * This is zero until processing of the entire Message Batch has ended. + */ + "errored": S.Int.pipe(S.propertySignature, S.withConstructorDefault(() => 0 as const)), + /** + * Number of requests in the Message Batch that have expired. + * + * This is zero until processing of the entire Message Batch has ended. + */ + "expired": S.Int.pipe(S.propertySignature, S.withConstructorDefault(() => 0 as const)), + /** + * Number of requests in the Message Batch that are processing. + */ + "processing": S.Int.pipe(S.propertySignature, S.withConstructorDefault(() => 0 as const)), + /** + * Number of requests in the Message Batch that have completed successfully. + * + * This is zero until processing of the entire Message Batch has ended. + */ + "succeeded": S.Int.pipe(S.propertySignature, S.withConstructorDefault(() => 0 as const)) +}) {} + +export class MessageBatch extends S.Class("MessageBatch")({ + /** + * RFC 3339 datetime string representing the time at which the Message Batch was archived and its results became unavailable. + */ + "archived_at": S.NullOr(S.String), + /** + * RFC 3339 datetime string representing the time at which cancellation was initiated for the Message Batch. Specified only if cancellation was initiated. + */ + "cancel_initiated_at": S.NullOr(S.String), + /** + * RFC 3339 datetime string representing the time at which the Message Batch was created. + */ + "created_at": S.String, + /** + * RFC 3339 datetime string representing the time at which processing for the Message Batch ended. Specified only once processing ends. + * + * Processing ends when every request in a Message Batch has either succeeded, errored, canceled, or expired. + */ + "ended_at": S.NullOr(S.String), + /** + * RFC 3339 datetime string representing the time at which the Message Batch will expire and end processing, which is 24 hours after creation. + */ + "expires_at": S.String, + /** + * Unique object identifier. + * + * The format and length of IDs may change over time. + */ + "id": S.String, + /** + * Processing status of the Message Batch. + */ + "processing_status": MessageBatchProcessingStatus, + /** + * Tallies requests within the Message Batch, categorized by their status. + * + * Requests start as `processing` and move to one of the other statuses only once processing of the entire batch ends. The sum of all values always matches the total number of requests in the batch. + */ + "request_counts": RequestCounts, + /** + * URL to a `.jsonl` file containing the results of the Message Batch requests. Specified only once processing ends. + * + * Results in the file are not guaranteed to be in the same order as requests. Use the `custom_id` field to match results to requests. + */ + "results_url": S.NullOr(S.String), + /** + * Object type. + * + * For Message Batches, this is always `"message_batch"`. + */ + "type": S.Literal("message_batch").pipe(S.propertySignature, S.withConstructorDefault(() => "message_batch" as const)) +}) {} + +export class ListResponseMessageBatch extends S.Class("ListResponseMessageBatch")({ + "data": S.Array(MessageBatch), + /** + * First ID in the `data` list. Can be used as the `before_id` for the previous page. + */ + "first_id": S.NullOr(S.String), + /** + * Indicates if there are more results in the requested page direction. + */ + "has_more": S.Boolean, + /** + * Last ID in the `data` list. Can be used as the `after_id` for the next page. + */ + "last_id": S.NullOr(S.String) +}) {} + +export class MessageBatchesPostParams extends S.Struct({ + /** + * The version of the Claude API you want to use. + * + * Read more about versioning and our version history [here](https://docs.claude.com/en/api/versioning). + */ + "anthropic-version": S.optionalWith(S.String, { nullable: true }) +}) {} + +export class MessageBatchIndividualRequestParams + extends S.Class("MessageBatchIndividualRequestParams")({ + /** + * Developer-provided ID created for each request in a Message Batch. Useful for matching results to requests, as results may be given out of request order. + * + * Must be unique for each request within the Message Batch. + */ + "custom_id": S.String.pipe(S.minLength(1), S.maxLength(64), S.pattern(new RegExp("^[a-zA-Z0-9_-]{1,64}$"))), + /** + * Messages API creation parameters for the individual request. + * + * See the [Messages API reference](/en/api/messages) for full documentation on available parameters. + */ + "params": CreateMessageParams + }) +{} + +export class CreateMessageBatchParams extends S.Class("CreateMessageBatchParams")({ + /** + * List of requests for prompt completion. Each is an individual request to create a Message. + */ + "requests": S.NonEmptyArray(MessageBatchIndividualRequestParams).pipe(S.minItems(1), S.maxItems(10000)) +}) {} + +export class MessageBatchesRetrieveParams extends S.Struct({ + /** + * The version of the Claude API you want to use. + * + * Read more about versioning and our version history [here](https://docs.claude.com/en/api/versioning). + */ + "anthropic-version": S.optionalWith(S.String, { nullable: true }), + /** + * Your unique API key for authentication. + * + * This key is required in the header of all API requests, to authenticate your account and access Anthropic's services. Get your API key through the [Console](https://console.anthropic.com/settings/keys). Each key is scoped to a Workspace. + */ + "x-api-key": S.optionalWith(S.String, { nullable: true }) +}) {} + +export class MessageBatchesDeleteParams extends S.Struct({ + /** + * The version of the Claude API you want to use. + * + * Read more about versioning and our version history [here](https://docs.claude.com/en/api/versioning). + */ + "anthropic-version": S.optionalWith(S.String, { nullable: true }), + /** + * Your unique API key for authentication. + * + * This key is required in the header of all API requests, to authenticate your account and access Anthropic's services. Get your API key through the [Console](https://console.anthropic.com/settings/keys). Each key is scoped to a Workspace. + */ + "x-api-key": S.optionalWith(S.String, { nullable: true }) +}) {} + +export class DeleteMessageBatchResponse extends S.Class("DeleteMessageBatchResponse")({ + /** + * ID of the Message Batch. + */ + "id": S.String, + /** + * Deleted object type. + * + * For Message Batches, this is always `"message_batch_deleted"`. + */ + "type": S.Literal("message_batch_deleted").pipe( + S.propertySignature, + S.withConstructorDefault(() => "message_batch_deleted" as const) + ) +}) {} + +export class MessageBatchesCancelParams extends S.Struct({ + /** + * The version of the Claude API you want to use. + * + * Read more about versioning and our version history [here](https://docs.claude.com/en/api/versioning). + */ + "anthropic-version": S.optionalWith(S.String, { nullable: true }) +}) {} + +export class MessageBatchesResultsParams extends S.Struct({ + /** + * The version of the Claude API you want to use. + * + * Read more about versioning and our version history [here](https://docs.claude.com/en/api/versioning). + */ + "anthropic-version": S.optionalWith(S.String, { nullable: true }), + /** + * Your unique API key for authentication. + * + * This key is required in the header of all API requests, to authenticate your account and access Anthropic's services. Get your API key through the [Console](https://console.anthropic.com/settings/keys). Each key is scoped to a Workspace. + */ + "x-api-key": S.optionalWith(S.String, { nullable: true }) +}) {} + +export class MessagesCountTokensPostParams extends S.Struct({ + /** + * The version of the Claude API you want to use. + * + * Read more about versioning and our version history [here](https://docs.claude.com/en/api/versioning). + */ + "anthropic-version": S.optionalWith(S.String, { nullable: true }) +}) {} + +export class CountMessageTokensParams extends S.Class("CountMessageTokensParams")({ + /** + * Input messages. + * + * Our models are trained to operate on alternating `user` and `assistant` conversational turns. When creating a new `Message`, you specify the prior conversational turns with the `messages` parameter, and the model then generates the next `Message` in the conversation. Consecutive `user` or `assistant` turns in your request will be combined into a single turn. + * + * Each input message must be an object with a `role` and `content`. You can specify a single `user`-role message, or you can include multiple `user` and `assistant` messages. + * + * If the final message uses the `assistant` role, the response content will continue immediately from the content in that message. This can be used to constrain part of the model's response. + * + * Example with a single `user` message: + * + * ```json + * [{"role": "user", "content": "Hello, Claude"}] + * ``` + * + * Example with multiple conversational turns: + * + * ```json + * [ + * {"role": "user", "content": "Hello there."}, + * {"role": "assistant", "content": "Hi, I'm Claude. How can I help you?"}, + * {"role": "user", "content": "Can you explain LLMs in plain English?"}, + * ] + * ``` + * + * Example with a partially-filled response from Claude: + * + * ```json + * [ + * {"role": "user", "content": "What's the Greek name for Sun? (A) Sol (B) Helios (C) Sun"}, + * {"role": "assistant", "content": "The best answer is ("}, + * ] + * ``` + * + * Each input message `content` may be either a single `string` or an array of content blocks, where each block has a specific `type`. Using a `string` for `content` is shorthand for an array of one content block of type `"text"`. The following input messages are equivalent: + * + * ```json + * {"role": "user", "content": "Hello, Claude"} + * ``` + * + * ```json + * {"role": "user", "content": [{"type": "text", "text": "Hello, Claude"}]} + * ``` + * + * See [input examples](https://docs.claude.com/en/api/messages-examples). + * + * Note that if you want to include a [system prompt](https://docs.claude.com/en/docs/system-prompts), you can use the top-level `system` parameter — there is no `"system"` role for input messages in the Messages API. + * + * There is a limit of 100,000 messages in a single request. + */ + "messages": S.Array(InputMessage), + "model": S.Union(S.String, Model), + /** + * System prompt. + * + * A system prompt is a way of providing context and instructions to Claude, such as specifying a particular goal or role. See our [guide to system prompts](https://docs.claude.com/en/docs/system-prompts). + */ + "system": S.optionalWith(S.Union(S.String, S.Array(RequestTextBlock)), { nullable: true }), + "thinking": S.optionalWith(ThinkingConfigParam, { nullable: true }), + "tool_choice": S.optionalWith(ToolChoice, { nullable: true }), + /** + * Definitions of tools that the model may use. + * + * If you include `tools` in your API request, the model may return `tool_use` content blocks that represent the model's use of those tools. You can then run those tools using the tool input generated by the model and then optionally return results back to the model using `tool_result` content blocks. + * + * There are two types of tools: **client tools** and **server tools**. The behavior described below applies to client tools. For [server tools](https://docs.claude.com/en/docs/agents-and-tools/tool-use/overview\#server-tools), see their individual documentation as each has its own behavior (e.g., the [web search tool](https://docs.claude.com/en/docs/agents-and-tools/tool-use/web-search-tool)). + * + * Each tool definition includes: + * + * * `name`: Name of the tool. + * * `description`: Optional, but strongly-recommended description of the tool. + * * `input_schema`: [JSON schema](https://json-schema.org/draft/2020-12) for the tool `input` shape that the model will produce in `tool_use` output content blocks. + * + * For example, if you defined `tools` as: + * + * ```json + * [ + * { + * "name": "get_stock_price", + * "description": "Get the current stock price for a given ticker symbol.", + * "input_schema": { + * "type": "object", + * "properties": { + * "ticker": { + * "type": "string", + * "description": "The stock ticker symbol, e.g. AAPL for Apple Inc." + * } + * }, + * "required": ["ticker"] + * } + * } + * ] + * ``` + * + * And then asked the model "What's the S&P 500 at today?", the model might produce `tool_use` content blocks in the response like this: + * + * ```json + * [ + * { + * "type": "tool_use", + * "id": "toolu_01D7FLrfh4GYq7yT1ULFeyMV", + * "name": "get_stock_price", + * "input": { "ticker": "^GSPC" } + * } + * ] + * ``` + * + * You might then run your `get_stock_price` tool with `{"ticker": "^GSPC"}` as an input, and return the following back to the model in a subsequent `user` message: + * + * ```json + * [ + * { + * "type": "tool_result", + * "tool_use_id": "toolu_01D7FLrfh4GYq7yT1ULFeyMV", + * "content": "259.75 USD" + * } + * ] + * ``` + * + * Tools can be used for workflows that include running client-side tools and functions, or more generally whenever you want the model to produce a particular JSON structure of output. + * + * See our [guide](https://docs.claude.com/en/docs/tool-use) for more details. + */ + "tools": S.optionalWith( + S.Array( + S.Union(Tool, BashTool20250124, TextEditor20250124, TextEditor20250429, TextEditor20250728, WebSearchTool20250305) + ), + { nullable: true } + ) +}) {} + +export class CountMessageTokensResponse extends S.Class("CountMessageTokensResponse")({ + /** + * The total number of tokens across the provided list of messages, system prompt, and tools. + */ + "input_tokens": S.Int +}) {} + +export class ListFilesV1FilesGetParams extends S.Struct({ + /** + * ID of the object to use as a cursor for pagination. When provided, returns the page of results immediately before this object. + */ + "before_id": S.optionalWith(S.String, { nullable: true }), + /** + * ID of the object to use as a cursor for pagination. When provided, returns the page of results immediately after this object. + */ + "after_id": S.optionalWith(S.String, { nullable: true }), + /** + * Number of items to return per page. + * + * Defaults to `20`. Ranges from `1` to `1000`. + */ + "limit": S.optionalWith(S.Int.pipe(S.greaterThanOrEqualTo(1), S.lessThanOrEqualTo(1000)), { + nullable: true, + default: () => 20 as const + }), + /** + * Optional header to specify the beta version(s) you want to use. + * + * To use multiple betas, use a comma separated list like `beta1,beta2` or specify the header multiple times for each beta. + */ + "anthropic-beta": S.optionalWith(S.String, { nullable: true }), + /** + * The version of the Claude API you want to use. + * + * Read more about versioning and our version history [here](https://docs.claude.com/en/api/versioning). + */ + "anthropic-version": S.optionalWith(S.String, { nullable: true }), + /** + * Your unique API key for authentication. + * + * This key is required in the header of all API requests, to authenticate your account and access Anthropic's services. Get your API key through the [Console](https://console.anthropic.com/settings/keys). Each key is scoped to a Workspace. + */ + "x-api-key": S.optionalWith(S.String, { nullable: true }) +}) {} + +export class FileMetadataSchema extends S.Class("FileMetadataSchema")({ + /** + * RFC 3339 datetime string representing when the file was created. + */ + "created_at": S.String, + /** + * Whether the file can be downloaded. + */ + "downloadable": S.optionalWith(S.Boolean, { nullable: true, default: () => false as const }), + /** + * Original filename of the uploaded file. + */ + "filename": S.String.pipe(S.minLength(1), S.maxLength(500)), + /** + * Unique object identifier. + * + * The format and length of IDs may change over time. + */ + "id": S.String, + /** + * MIME type of the file. + */ + "mime_type": S.String.pipe(S.minLength(1), S.maxLength(255)), + /** + * Size of the file in bytes. + */ + "size_bytes": S.Int.pipe(S.greaterThanOrEqualTo(0)), + /** + * Object type. + * + * For files, this is always `"file"`. + */ + "type": S.Literal("file") +}) {} + +export class FileListResponse extends S.Class("FileListResponse")({ + /** + * List of file metadata objects. + */ + "data": S.Array(FileMetadataSchema), + /** + * ID of the first file in this page of results. + */ + "first_id": S.optionalWith(S.String, { nullable: true }), + /** + * Whether there are more results available. + */ + "has_more": S.optionalWith(S.Boolean, { nullable: true, default: () => false as const }), + /** + * ID of the last file in this page of results. + */ + "last_id": S.optionalWith(S.String, { nullable: true }) +}) {} + +export class UploadFileV1FilesPostParams extends S.Struct({ + /** + * Optional header to specify the beta version(s) you want to use. + * + * To use multiple betas, use a comma separated list like `beta1,beta2` or specify the header multiple times for each beta. + */ + "anthropic-beta": S.optionalWith(S.String, { nullable: true }), + /** + * The version of the Claude API you want to use. + * + * Read more about versioning and our version history [here](https://docs.claude.com/en/api/versioning). + */ + "anthropic-version": S.optionalWith(S.String, { nullable: true }) +}) {} + +export class UploadFileV1FilesPostRequest + extends S.Class("UploadFileV1FilesPostRequest")({ + /** + * The file to upload + */ + "file": S.instanceOf(globalThis.Blob) + }) +{} + +export class GetFileMetadataV1FilesFileIdGetParams extends S.Struct({ + /** + * Optional header to specify the beta version(s) you want to use. + * + * To use multiple betas, use a comma separated list like `beta1,beta2` or specify the header multiple times for each beta. + */ + "anthropic-beta": S.optionalWith(S.String, { nullable: true }), + /** + * The version of the Claude API you want to use. + * + * Read more about versioning and our version history [here](https://docs.claude.com/en/api/versioning). + */ + "anthropic-version": S.optionalWith(S.String, { nullable: true }), + /** + * Your unique API key for authentication. + * + * This key is required in the header of all API requests, to authenticate your account and access Anthropic's services. Get your API key through the [Console](https://console.anthropic.com/settings/keys). Each key is scoped to a Workspace. + */ + "x-api-key": S.optionalWith(S.String, { nullable: true }) +}) {} + +export class DeleteFileV1FilesFileIdDeleteParams extends S.Struct({ + /** + * Optional header to specify the beta version(s) you want to use. + * + * To use multiple betas, use a comma separated list like `beta1,beta2` or specify the header multiple times for each beta. + */ + "anthropic-beta": S.optionalWith(S.String, { nullable: true }), + /** + * The version of the Claude API you want to use. + * + * Read more about versioning and our version history [here](https://docs.claude.com/en/api/versioning). + */ + "anthropic-version": S.optionalWith(S.String, { nullable: true }), + /** + * Your unique API key for authentication. + * + * This key is required in the header of all API requests, to authenticate your account and access Anthropic's services. Get your API key through the [Console](https://console.anthropic.com/settings/keys). Each key is scoped to a Workspace. + */ + "x-api-key": S.optionalWith(S.String, { nullable: true }) +}) {} + +export class FileDeleteResponse extends S.Class("FileDeleteResponse")({ + /** + * ID of the deleted file. + */ + "id": S.String, + /** + * Deleted object type. + * + * For file deletion, this is always `"file_deleted"`. + */ + "type": S.optionalWith(S.Literal("file_deleted"), { nullable: true, default: () => "file_deleted" as const }) +}) {} + +export class DownloadFileV1FilesFileIdContentGetParams extends S.Struct({ + /** + * Optional header to specify the beta version(s) you want to use. + * + * To use multiple betas, use a comma separated list like `beta1,beta2` or specify the header multiple times for each beta. + */ + "anthropic-beta": S.optionalWith(S.String, { nullable: true }), + /** + * The version of the Claude API you want to use. + * + * Read more about versioning and our version history [here](https://docs.claude.com/en/api/versioning). + */ + "anthropic-version": S.optionalWith(S.String, { nullable: true }), + /** + * Your unique API key for authentication. + * + * This key is required in the header of all API requests, to authenticate your account and access Anthropic's services. Get your API key through the [Console](https://console.anthropic.com/settings/keys). Each key is scoped to a Workspace. + */ + "x-api-key": S.optionalWith(S.String, { nullable: true }) +}) {} + +export class ListSkillsV1SkillsGetParams extends S.Struct({ + /** + * Pagination token for fetching a specific page of results. + * + * Pass the value from a previous response's `next_page` field to get the next page of results. + */ + "page": S.optionalWith(S.String, { nullable: true }), + /** + * Number of results to return per page. + * + * Maximum value is 100. Defaults to 20. + */ + "limit": S.optionalWith(S.Int, { nullable: true, default: () => 20 as const }), + /** + * Filter skills by source. + * + * If provided, only skills from the specified source will be returned: + * * `"custom"`: only return user-created skills + * * `"anthropic"`: only return Anthropic-created skills + */ + "source": S.optionalWith(S.String, { nullable: true }), + /** + * Optional header to specify the beta version(s) you want to use. + * + * To use multiple betas, use a comma separated list like `beta1,beta2` or specify the header multiple times for each beta. + */ + "anthropic-beta": S.optionalWith(S.String, { nullable: true }), + /** + * The version of the Claude API you want to use. + * + * Read more about versioning and our version history [here](https://docs.claude.com/en/api/versioning). + */ + "anthropic-version": S.optionalWith(S.String, { nullable: true }), + /** + * Your unique API key for authentication. + * + * This key is required in the header of all API requests, to authenticate your account and access Anthropic's services. Get your API key through the [Console](https://console.anthropic.com/settings/keys). Each key is scoped to a Workspace. + */ + "x-api-key": S.optionalWith(S.String, { nullable: true }) +}) {} + +export class Skill extends S.Class("Skill")({ + /** + * ISO 8601 timestamp of when the skill was created. + */ + "created_at": S.String, + /** + * Display title for the skill. + * + * This is a human-readable label that is not included in the prompt sent to the model. + */ + "display_title": S.NullOr(S.String), + /** + * Unique identifier for the skill. + * + * The format and length of IDs may change over time. + */ + "id": S.String, + /** + * The latest version identifier for the skill. + * + * This represents the most recent version of the skill that has been created. + */ + "latest_version": S.NullOr(S.String), + /** + * Source of the skill. + * + * This may be one of the following values: + * * `"custom"`: the skill was created by a user + * * `"anthropic"`: the skill was created by Anthropic + */ + "source": S.String, + /** + * Object type. + * + * For Skills, this is always `"skill"`. + */ + "type": S.String.pipe(S.propertySignature, S.withConstructorDefault(() => "skill" as const)), + /** + * ISO 8601 timestamp of when the skill was last updated. + */ + "updated_at": S.String +}) {} + +export class ListSkillsResponse extends S.Class("ListSkillsResponse")({ + /** + * List of skills. + */ + "data": S.Array(Skill), + /** + * Whether there are more results available. + * + * If `true`, there are additional results that can be fetched using the `next_page` token. + */ + "has_more": S.Boolean, + /** + * Token for fetching the next page of results. + * + * If `null`, there are no more results available. Pass this value to the `page_token` parameter in the next request to get the next page. + */ + "next_page": S.NullOr(S.String) +}) {} + +export class CreateSkillV1SkillsPostParams extends S.Struct({ + /** + * Optional header to specify the beta version(s) you want to use. + * + * To use multiple betas, use a comma separated list like `beta1,beta2` or specify the header multiple times for each beta. + */ + "anthropic-beta": S.optionalWith(S.String, { nullable: true }), + /** + * The version of the Claude API you want to use. + * + * Read more about versioning and our version history [here](https://docs.claude.com/en/api/versioning). + */ + "anthropic-version": S.optionalWith(S.String, { nullable: true }) +}) {} + +export class BodyCreateSkillV1SkillsPost extends S.Class("BodyCreateSkillV1SkillsPost")({ + /** + * Display title for the skill. + * + * This is a human-readable label that is not included in the prompt sent to the model. + */ + "display_title": S.optionalWith(S.String, { nullable: true }), + /** + * Files to upload for the skill. + * + * All files must be in the same top-level directory and must include a SKILL.md file at the root of that directory. + */ + "files": S.optionalWith(S.Array(S.instanceOf(globalThis.Blob)), { nullable: true }) +}) {} + +export class CreateSkillResponse extends S.Class("CreateSkillResponse")({ + /** + * ISO 8601 timestamp of when the skill was created. + */ + "created_at": S.String, + /** + * Display title for the skill. + * + * This is a human-readable label that is not included in the prompt sent to the model. + */ + "display_title": S.NullOr(S.String), + /** + * Unique identifier for the skill. + * + * The format and length of IDs may change over time. + */ + "id": S.String, + /** + * The latest version identifier for the skill. + * + * This represents the most recent version of the skill that has been created. + */ + "latest_version": S.NullOr(S.String), + /** + * Source of the skill. + * + * This may be one of the following values: + * * `"custom"`: the skill was created by a user + * * `"anthropic"`: the skill was created by Anthropic + */ + "source": S.String, + /** + * Object type. + * + * For Skills, this is always `"skill"`. + */ + "type": S.String.pipe(S.propertySignature, S.withConstructorDefault(() => "skill" as const)), + /** + * ISO 8601 timestamp of when the skill was last updated. + */ + "updated_at": S.String +}) {} + +export class GetSkillV1SkillsSkillIdGetParams extends S.Struct({ + /** + * Optional header to specify the beta version(s) you want to use. + * + * To use multiple betas, use a comma separated list like `beta1,beta2` or specify the header multiple times for each beta. + */ + "anthropic-beta": S.optionalWith(S.String, { nullable: true }), + /** + * The version of the Claude API you want to use. + * + * Read more about versioning and our version history [here](https://docs.claude.com/en/api/versioning). + */ + "anthropic-version": S.optionalWith(S.String, { nullable: true }), + /** + * Your unique API key for authentication. + * + * This key is required in the header of all API requests, to authenticate your account and access Anthropic's services. Get your API key through the [Console](https://console.anthropic.com/settings/keys). Each key is scoped to a Workspace. + */ + "x-api-key": S.optionalWith(S.String, { nullable: true }) +}) {} + +export class GetSkillResponse extends S.Class("GetSkillResponse")({ + /** + * ISO 8601 timestamp of when the skill was created. + */ + "created_at": S.String, + /** + * Display title for the skill. + * + * This is a human-readable label that is not included in the prompt sent to the model. + */ + "display_title": S.NullOr(S.String), + /** + * Unique identifier for the skill. + * + * The format and length of IDs may change over time. + */ + "id": S.String, + /** + * The latest version identifier for the skill. + * + * This represents the most recent version of the skill that has been created. + */ + "latest_version": S.NullOr(S.String), + /** + * Source of the skill. + * + * This may be one of the following values: + * * `"custom"`: the skill was created by a user + * * `"anthropic"`: the skill was created by Anthropic + */ + "source": S.String, + /** + * Object type. + * + * For Skills, this is always `"skill"`. + */ + "type": S.String.pipe(S.propertySignature, S.withConstructorDefault(() => "skill" as const)), + /** + * ISO 8601 timestamp of when the skill was last updated. + */ + "updated_at": S.String +}) {} + +export class DeleteSkillV1SkillsSkillIdDeleteParams extends S.Struct({ + /** + * Optional header to specify the beta version(s) you want to use. + * + * To use multiple betas, use a comma separated list like `beta1,beta2` or specify the header multiple times for each beta. + */ + "anthropic-beta": S.optionalWith(S.String, { nullable: true }), + /** + * The version of the Claude API you want to use. + * + * Read more about versioning and our version history [here](https://docs.claude.com/en/api/versioning). + */ + "anthropic-version": S.optionalWith(S.String, { nullable: true }), + /** + * Your unique API key for authentication. + * + * This key is required in the header of all API requests, to authenticate your account and access Anthropic's services. Get your API key through the [Console](https://console.anthropic.com/settings/keys). Each key is scoped to a Workspace. + */ + "x-api-key": S.optionalWith(S.String, { nullable: true }) +}) {} + +export class DeleteSkillResponse extends S.Class("DeleteSkillResponse")({ + /** + * Unique identifier for the skill. + * + * The format and length of IDs may change over time. + */ + "id": S.String, + /** + * Deleted object type. + * + * For Skills, this is always `"skill_deleted"`. + */ + "type": S.String.pipe(S.propertySignature, S.withConstructorDefault(() => "skill_deleted" as const)) +}) {} + +export class ListSkillVersionsV1SkillsSkillIdVersionsGetParams extends S.Struct({ + /** + * Optionally set to the `next_page` token from the previous response. + */ + "page": S.optionalWith(S.String, { nullable: true }), + /** + * Number of items to return per page. + * + * Defaults to `20`. Ranges from `1` to `1000`. + */ + "limit": S.optionalWith(S.Int, { nullable: true }), + /** + * Optional header to specify the beta version(s) you want to use. + * + * To use multiple betas, use a comma separated list like `beta1,beta2` or specify the header multiple times for each beta. + */ + "anthropic-beta": S.optionalWith(S.String, { nullable: true }), + /** + * The version of the Claude API you want to use. + * + * Read more about versioning and our version history [here](https://docs.claude.com/en/api/versioning). + */ + "anthropic-version": S.optionalWith(S.String, { nullable: true }), + /** + * Your unique API key for authentication. + * + * This key is required in the header of all API requests, to authenticate your account and access Anthropic's services. Get your API key through the [Console](https://console.anthropic.com/settings/keys). Each key is scoped to a Workspace. + */ + "x-api-key": S.optionalWith(S.String, { nullable: true }) +}) {} + +export class SkillVersion extends S.Class("SkillVersion")({ + /** + * ISO 8601 timestamp of when the skill version was created. + */ + "created_at": S.String, + /** + * Description of the skill version. + * + * This is extracted from the SKILL.md file in the skill upload. + */ + "description": S.String, + /** + * Directory name of the skill version. + * + * This is the top-level directory name that was extracted from the uploaded files. + */ + "directory": S.String, + /** + * Unique identifier for the skill version. + * + * The format and length of IDs may change over time. + */ + "id": S.String, + /** + * Human-readable name of the skill version. + * + * This is extracted from the SKILL.md file in the skill upload. + */ + "name": S.String, + /** + * Identifier for the skill that this version belongs to. + */ + "skill_id": S.String, + /** + * Object type. + * + * For Skill Versions, this is always `"skill_version"`. + */ + "type": S.String.pipe(S.propertySignature, S.withConstructorDefault(() => "skill_version" as const)), + /** + * Version identifier for the skill. + * + * Each version is identified by a Unix epoch timestamp (e.g., "1759178010641129"). + */ + "version": S.String +}) {} + +export class ListSkillVersionsResponse extends S.Class("ListSkillVersionsResponse")({ + /** + * List of skill versions. + */ + "data": S.Array(SkillVersion), + /** + * Indicates if there are more results in the requested page direction. + */ + "has_more": S.Boolean, + /** + * Token to provide in as `page` in the subsequent request to retrieve the next page of data. + */ + "next_page": S.NullOr(S.String) +}) {} + +export class CreateSkillVersionV1SkillsSkillIdVersionsPostParams extends S.Struct({ + /** + * Optional header to specify the beta version(s) you want to use. + * + * To use multiple betas, use a comma separated list like `beta1,beta2` or specify the header multiple times for each beta. + */ + "anthropic-beta": S.optionalWith(S.String, { nullable: true }), + /** + * The version of the Claude API you want to use. + * + * Read more about versioning and our version history [here](https://docs.claude.com/en/api/versioning). + */ + "anthropic-version": S.optionalWith(S.String, { nullable: true }) +}) {} + +export class BodyCreateSkillVersionV1SkillsSkillIdVersionsPost + extends S.Class( + "BodyCreateSkillVersionV1SkillsSkillIdVersionsPost" + )({ + /** + * Files to upload for the skill. + * + * All files must be in the same top-level directory and must include a SKILL.md file at the root of that directory. + */ + "files": S.optionalWith(S.Array(S.instanceOf(globalThis.Blob)), { nullable: true }) + }) +{} + +export class CreateSkillVersionResponse extends S.Class("CreateSkillVersionResponse")({ + /** + * ISO 8601 timestamp of when the skill version was created. + */ + "created_at": S.String, + /** + * Description of the skill version. + * + * This is extracted from the SKILL.md file in the skill upload. + */ + "description": S.String, + /** + * Directory name of the skill version. + * + * This is the top-level directory name that was extracted from the uploaded files. + */ + "directory": S.String, + /** + * Unique identifier for the skill version. + * + * The format and length of IDs may change over time. + */ + "id": S.String, + /** + * Human-readable name of the skill version. + * + * This is extracted from the SKILL.md file in the skill upload. + */ + "name": S.String, + /** + * Identifier for the skill that this version belongs to. + */ + "skill_id": S.String, + /** + * Object type. + * + * For Skill Versions, this is always `"skill_version"`. + */ + "type": S.String.pipe(S.propertySignature, S.withConstructorDefault(() => "skill_version" as const)), + /** + * Version identifier for the skill. + * + * Each version is identified by a Unix epoch timestamp (e.g., "1759178010641129"). + */ + "version": S.String +}) {} + +export class GetSkillVersionV1SkillsSkillIdVersionsVersionGetParams extends S.Struct({ + /** + * Optional header to specify the beta version(s) you want to use. + * + * To use multiple betas, use a comma separated list like `beta1,beta2` or specify the header multiple times for each beta. + */ + "anthropic-beta": S.optionalWith(S.String, { nullable: true }), + /** + * The version of the Claude API you want to use. + * + * Read more about versioning and our version history [here](https://docs.claude.com/en/api/versioning). + */ + "anthropic-version": S.optionalWith(S.String, { nullable: true }), + /** + * Your unique API key for authentication. + * + * This key is required in the header of all API requests, to authenticate your account and access Anthropic's services. Get your API key through the [Console](https://console.anthropic.com/settings/keys). Each key is scoped to a Workspace. + */ + "x-api-key": S.optionalWith(S.String, { nullable: true }) +}) {} + +export class GetSkillVersionResponse extends S.Class("GetSkillVersionResponse")({ + /** + * ISO 8601 timestamp of when the skill version was created. + */ + "created_at": S.String, + /** + * Description of the skill version. + * + * This is extracted from the SKILL.md file in the skill upload. + */ + "description": S.String, + /** + * Directory name of the skill version. + * + * This is the top-level directory name that was extracted from the uploaded files. + */ + "directory": S.String, + /** + * Unique identifier for the skill version. + * + * The format and length of IDs may change over time. + */ + "id": S.String, + /** + * Human-readable name of the skill version. + * + * This is extracted from the SKILL.md file in the skill upload. + */ + "name": S.String, + /** + * Identifier for the skill that this version belongs to. + */ + "skill_id": S.String, + /** + * Object type. + * + * For Skill Versions, this is always `"skill_version"`. + */ + "type": S.String.pipe(S.propertySignature, S.withConstructorDefault(() => "skill_version" as const)), + /** + * Version identifier for the skill. + * + * Each version is identified by a Unix epoch timestamp (e.g., "1759178010641129"). + */ + "version": S.String +}) {} + +export class DeleteSkillVersionV1SkillsSkillIdVersionsVersionDeleteParams extends S.Struct({ + /** + * Optional header to specify the beta version(s) you want to use. + * + * To use multiple betas, use a comma separated list like `beta1,beta2` or specify the header multiple times for each beta. + */ + "anthropic-beta": S.optionalWith(S.String, { nullable: true }), + /** + * The version of the Claude API you want to use. + * + * Read more about versioning and our version history [here](https://docs.claude.com/en/api/versioning). + */ + "anthropic-version": S.optionalWith(S.String, { nullable: true }), + /** + * Your unique API key for authentication. + * + * This key is required in the header of all API requests, to authenticate your account and access Anthropic's services. Get your API key through the [Console](https://console.anthropic.com/settings/keys). Each key is scoped to a Workspace. + */ + "x-api-key": S.optionalWith(S.String, { nullable: true }) +}) {} + +export class DeleteSkillVersionResponse extends S.Class("DeleteSkillVersionResponse")({ + /** + * Version identifier for the skill. + * + * Each version is identified by a Unix epoch timestamp (e.g., "1759178010641129"). + */ + "id": S.String, + /** + * Deleted object type. + * + * For Skill Versions, this is always `"skill_version_deleted"`. + */ + "type": S.String.pipe(S.propertySignature, S.withConstructorDefault(() => "skill_version_deleted" as const)) +}) {} + +export class BetaMessagesPostParams extends S.Struct({ + /** + * Optional header to specify the beta version(s) you want to use. + * + * To use multiple betas, use a comma separated list like `beta1,beta2` or specify the header multiple times for each beta. + */ + "anthropic-beta": S.optionalWith(S.String, { nullable: true }), + /** + * The version of the Claude API you want to use. + * + * Read more about versioning and our version history [here](https://docs.claude.com/en/api/versioning). + */ + "anthropic-version": S.optionalWith(S.String, { nullable: true }) +}) {} + +/** + * The time-to-live for the cache control breakpoint. + * + * This may be one the following values: + * - `5m`: 5 minutes + * - `1h`: 1 hour + * + * Defaults to `5m`. + */ +export class BetaCacheControlEphemeralTtl extends S.Literal("5m", "1h") {} + +export class BetaCacheControlEphemeral extends S.Class("BetaCacheControlEphemeral")({ + /** + * The time-to-live for the cache control breakpoint. + * + * This may be one the following values: + * - `5m`: 5 minutes + * - `1h`: 1 hour + * + * Defaults to `5m`. + */ + "ttl": S.optionalWith(BetaCacheControlEphemeralTtl, { nullable: true }), + "type": S.Literal("ephemeral") +}) {} + +export class BetaRequestCharLocationCitation + extends S.Class("BetaRequestCharLocationCitation")({ + "cited_text": S.String, + "document_index": S.Int.pipe(S.greaterThanOrEqualTo(0)), + "document_title": S.NullOr(S.String.pipe(S.minLength(1), S.maxLength(255))), + "end_char_index": S.Int, + "start_char_index": S.Int.pipe(S.greaterThanOrEqualTo(0)), + "type": S.Literal("char_location") + }) +{} + +export class BetaRequestPageLocationCitation + extends S.Class("BetaRequestPageLocationCitation")({ + "cited_text": S.String, + "document_index": S.Int.pipe(S.greaterThanOrEqualTo(0)), + "document_title": S.NullOr(S.String.pipe(S.minLength(1), S.maxLength(255))), + "end_page_number": S.Int, + "start_page_number": S.Int.pipe(S.greaterThanOrEqualTo(1)), + "type": S.Literal("page_location") + }) +{} + +export class BetaRequestContentBlockLocationCitation + extends S.Class("BetaRequestContentBlockLocationCitation")({ + "cited_text": S.String, + "document_index": S.Int.pipe(S.greaterThanOrEqualTo(0)), + "document_title": S.NullOr(S.String.pipe(S.minLength(1), S.maxLength(255))), + "end_block_index": S.Int, + "start_block_index": S.Int.pipe(S.greaterThanOrEqualTo(0)), + "type": S.Literal("content_block_location") + }) +{} + +export class BetaRequestWebSearchResultLocationCitation + extends S.Class("BetaRequestWebSearchResultLocationCitation")({ + "cited_text": S.String, + "encrypted_index": S.String, + "title": S.NullOr(S.String.pipe(S.minLength(1), S.maxLength(512))), + "type": S.Literal("web_search_result_location"), + "url": S.String.pipe(S.minLength(1), S.maxLength(2048)) + }) +{} + +export class BetaRequestSearchResultLocationCitation + extends S.Class("BetaRequestSearchResultLocationCitation")({ + "cited_text": S.String, + "end_block_index": S.Int, + "search_result_index": S.Int.pipe(S.greaterThanOrEqualTo(0)), + "source": S.String, + "start_block_index": S.Int.pipe(S.greaterThanOrEqualTo(0)), + "title": S.NullOr(S.String), + "type": S.Literal("search_result_location") + }) +{} + +export class BetaRequestTextBlock extends S.Class("BetaRequestTextBlock")({ + /** + * Create a cache control breakpoint at this content block. + */ + "cache_control": S.optionalWith(BetaCacheControlEphemeral, { nullable: true }), + "citations": S.optionalWith( + S.Array( + S.Union( + BetaRequestCharLocationCitation, + BetaRequestPageLocationCitation, + BetaRequestContentBlockLocationCitation, + BetaRequestWebSearchResultLocationCitation, + BetaRequestSearchResultLocationCitation + ) + ), + { nullable: true } + ), + "text": S.String.pipe(S.minLength(1)), + "type": S.Literal("text") +}) {} + +export class BetaBase64ImageSourceMediaType extends S.Literal("image/jpeg", "image/png", "image/gif", "image/webp") {} + +export class BetaBase64ImageSource extends S.Class("BetaBase64ImageSource")({ + "data": S.String, + "media_type": BetaBase64ImageSourceMediaType, + "type": S.Literal("base64") +}) {} + +export class BetaURLImageSource extends S.Class("BetaURLImageSource")({ + "type": S.Literal("url"), + "url": S.String +}) {} + +export class BetaFileImageSource extends S.Class("BetaFileImageSource")({ + "file_id": S.String, + "type": S.Literal("file") +}) {} + +export class BetaRequestImageBlock extends S.Class("BetaRequestImageBlock")({ + /** + * Create a cache control breakpoint at this content block. + */ + "cache_control": S.optionalWith(BetaCacheControlEphemeral, { nullable: true }), + "source": S.Union(BetaBase64ImageSource, BetaURLImageSource, BetaFileImageSource), + "type": S.Literal("image") +}) {} + +export class BetaRequestCitationsConfig extends S.Class("BetaRequestCitationsConfig")({ + "enabled": S.optionalWith(S.Boolean, { nullable: true }) +}) {} + +export class BetaBase64PDFSource extends S.Class("BetaBase64PDFSource")({ + "data": S.String, + "media_type": S.Literal("application/pdf"), + "type": S.Literal("base64") +}) {} + +export class BetaPlainTextSource extends S.Class("BetaPlainTextSource")({ + "data": S.String, + "media_type": S.Literal("text/plain"), + "type": S.Literal("text") +}) {} + +export class BetaContentBlockSource extends S.Class("BetaContentBlockSource")({ + "content": S.Union(S.String, S.Array(S.Union(BetaRequestTextBlock, BetaRequestImageBlock))), + "type": S.Literal("content") +}) {} + +export class BetaURLPDFSource extends S.Class("BetaURLPDFSource")({ + "type": S.Literal("url"), + "url": S.String +}) {} + +export class BetaFileDocumentSource extends S.Class("BetaFileDocumentSource")({ + "file_id": S.String, + "type": S.Literal("file") +}) {} + +export class BetaRequestDocumentBlock extends S.Class("BetaRequestDocumentBlock")({ + /** + * Create a cache control breakpoint at this content block. + */ + "cache_control": S.optionalWith(BetaCacheControlEphemeral, { nullable: true }), + "citations": S.optionalWith(BetaRequestCitationsConfig, { nullable: true }), + "context": S.optionalWith(S.String.pipe(S.minLength(1)), { nullable: true }), + "source": S.Union( + BetaBase64PDFSource, + BetaPlainTextSource, + BetaContentBlockSource, + BetaURLPDFSource, + BetaFileDocumentSource + ), + "title": S.optionalWith(S.String.pipe(S.minLength(1), S.maxLength(500)), { nullable: true }), + "type": S.Literal("document") +}) {} + +export class BetaRequestSearchResultBlock + extends S.Class("BetaRequestSearchResultBlock")({ + /** + * Create a cache control breakpoint at this content block. + */ + "cache_control": S.optionalWith(BetaCacheControlEphemeral, { nullable: true }), + "citations": S.optionalWith(BetaRequestCitationsConfig, { nullable: true }), + "content": S.Array(BetaRequestTextBlock), + "source": S.String, + "title": S.String, + "type": S.Literal("search_result") + }) +{} + +export class BetaRequestThinkingBlock extends S.Class("BetaRequestThinkingBlock")({ + "signature": S.String, + "thinking": S.String, + "type": S.Literal("thinking") +}) {} + +export class BetaRequestRedactedThinkingBlock + extends S.Class("BetaRequestRedactedThinkingBlock")({ + "data": S.String, + "type": S.Literal("redacted_thinking") + }) +{} + +export class BetaRequestToolUseBlock extends S.Class("BetaRequestToolUseBlock")({ + /** + * Create a cache control breakpoint at this content block. + */ + "cache_control": S.optionalWith(BetaCacheControlEphemeral, { nullable: true }), + "id": S.String.pipe(S.pattern(new RegExp("^[a-zA-Z0-9_-]+$"))), + "input": S.Record({ key: S.String, value: S.Unknown }), + "name": S.String.pipe(S.minLength(1), S.maxLength(200)), + "type": S.Literal("tool_use") +}) {} + +export class BetaRequestToolResultBlock extends S.Class("BetaRequestToolResultBlock")({ + /** + * Create a cache control breakpoint at this content block. + */ + "cache_control": S.optionalWith(BetaCacheControlEphemeral, { nullable: true }), + "content": S.optionalWith( + S.Union( + S.String, + S.Array( + S.Union(BetaRequestTextBlock, BetaRequestImageBlock, BetaRequestSearchResultBlock, BetaRequestDocumentBlock) + ) + ), + { nullable: true } + ), + "is_error": S.optionalWith(S.Boolean, { nullable: true }), + "tool_use_id": S.String.pipe(S.pattern(new RegExp("^[a-zA-Z0-9_-]+$"))), + "type": S.Literal("tool_result") +}) {} + +export class BetaRequestServerToolUseBlockName + extends S.Literal("web_search", "web_fetch", "code_execution", "bash_code_execution", "text_editor_code_execution") +{} + +export class BetaRequestServerToolUseBlock + extends S.Class("BetaRequestServerToolUseBlock")({ + /** + * Create a cache control breakpoint at this content block. + */ + "cache_control": S.optionalWith(BetaCacheControlEphemeral, { nullable: true }), + "id": S.String.pipe(S.pattern(new RegExp("^srvtoolu_[a-zA-Z0-9_]+$"))), + "input": S.Record({ key: S.String, value: S.Unknown }), + "name": BetaRequestServerToolUseBlockName, + "type": S.Literal("server_tool_use") + }) +{} + +export class BetaRequestWebSearchResultBlock + extends S.Class("BetaRequestWebSearchResultBlock")({ + "encrypted_content": S.String, + "page_age": S.optionalWith(S.String, { nullable: true }), + "title": S.String, + "type": S.Literal("web_search_result"), + "url": S.String + }) +{} + +export class BetaWebSearchToolResultErrorCode + extends S.Literal("invalid_tool_input", "unavailable", "max_uses_exceeded", "too_many_requests", "query_too_long") +{} + +export class BetaRequestWebSearchToolResultError + extends S.Class("BetaRequestWebSearchToolResultError")({ + "error_code": BetaWebSearchToolResultErrorCode, + "type": S.Literal("web_search_tool_result_error") + }) +{} + +export class BetaRequestWebSearchToolResultBlock + extends S.Class("BetaRequestWebSearchToolResultBlock")({ + /** + * Create a cache control breakpoint at this content block. + */ + "cache_control": S.optionalWith(BetaCacheControlEphemeral, { nullable: true }), + "content": S.Union(S.Array(BetaRequestWebSearchResultBlock), BetaRequestWebSearchToolResultError), + "tool_use_id": S.String.pipe(S.pattern(new RegExp("^srvtoolu_[a-zA-Z0-9_]+$"))), + "type": S.Literal("web_search_tool_result") + }) +{} + +export class BetaWebFetchToolResultErrorCode extends S.Literal( + "invalid_tool_input", + "url_too_long", + "url_not_allowed", + "url_not_accessible", + "unsupported_content_type", + "too_many_requests", + "max_uses_exceeded", + "unavailable" +) {} + +export class BetaRequestWebFetchToolResultError + extends S.Class("BetaRequestWebFetchToolResultError")({ + "error_code": BetaWebFetchToolResultErrorCode, + "type": S.Literal("web_fetch_tool_result_error") + }) +{} + +export class BetaRequestWebFetchResultBlock + extends S.Class("BetaRequestWebFetchResultBlock")({ + "content": BetaRequestDocumentBlock, + /** + * ISO 8601 timestamp when the content was retrieved + */ + "retrieved_at": S.optionalWith(S.String, { nullable: true }), + "type": S.Literal("web_fetch_result"), + /** + * Fetched content URL + */ + "url": S.String + }) +{} + +export class BetaRequestWebFetchToolResultBlock + extends S.Class("BetaRequestWebFetchToolResultBlock")({ + /** + * Create a cache control breakpoint at this content block. + */ + "cache_control": S.optionalWith(BetaCacheControlEphemeral, { nullable: true }), + "content": S.Union(BetaRequestWebFetchToolResultError, BetaRequestWebFetchResultBlock), + "tool_use_id": S.String.pipe(S.pattern(new RegExp("^srvtoolu_[a-zA-Z0-9_]+$"))), + "type": S.Literal("web_fetch_tool_result") + }) +{} + +export class BetaCodeExecutionToolResultErrorCode + extends S.Literal("invalid_tool_input", "unavailable", "too_many_requests", "execution_time_exceeded") +{} + +export class BetaRequestCodeExecutionToolResultError + extends S.Class("BetaRequestCodeExecutionToolResultError")({ + "error_code": BetaCodeExecutionToolResultErrorCode, + "type": S.Literal("code_execution_tool_result_error") + }) +{} + +export class BetaRequestCodeExecutionOutputBlock + extends S.Class("BetaRequestCodeExecutionOutputBlock")({ + "file_id": S.String, + "type": S.Literal("code_execution_output") + }) +{} + +export class BetaRequestCodeExecutionResultBlock + extends S.Class("BetaRequestCodeExecutionResultBlock")({ + "content": S.Array(BetaRequestCodeExecutionOutputBlock), + "return_code": S.Int, + "stderr": S.String, + "stdout": S.String, + "type": S.Literal("code_execution_result") + }) +{} + +export class BetaRequestCodeExecutionToolResultBlock + extends S.Class("BetaRequestCodeExecutionToolResultBlock")({ + /** + * Create a cache control breakpoint at this content block. + */ + "cache_control": S.optionalWith(BetaCacheControlEphemeral, { nullable: true }), + "content": S.Union(BetaRequestCodeExecutionToolResultError, BetaRequestCodeExecutionResultBlock), + "tool_use_id": S.String.pipe(S.pattern(new RegExp("^srvtoolu_[a-zA-Z0-9_]+$"))), + "type": S.Literal("code_execution_tool_result") + }) +{} + +export class BetaBashCodeExecutionToolResultErrorCode extends S.Literal( + "invalid_tool_input", + "unavailable", + "too_many_requests", + "execution_time_exceeded", + "output_file_too_large" +) {} + +export class BetaRequestBashCodeExecutionToolResultError + extends S.Class("BetaRequestBashCodeExecutionToolResultError")({ + "error_code": BetaBashCodeExecutionToolResultErrorCode, + "type": S.Literal("bash_code_execution_tool_result_error") + }) +{} + +export class BetaRequestBashCodeExecutionOutputBlock + extends S.Class("BetaRequestBashCodeExecutionOutputBlock")({ + "file_id": S.String, + "type": S.Literal("bash_code_execution_output") + }) +{} + +export class BetaRequestBashCodeExecutionResultBlock + extends S.Class("BetaRequestBashCodeExecutionResultBlock")({ + "content": S.Array(BetaRequestBashCodeExecutionOutputBlock), + "return_code": S.Int, + "stderr": S.String, + "stdout": S.String, + "type": S.Literal("bash_code_execution_result") + }) +{} + +export class BetaRequestBashCodeExecutionToolResultBlock + extends S.Class("BetaRequestBashCodeExecutionToolResultBlock")({ + /** + * Create a cache control breakpoint at this content block. + */ + "cache_control": S.optionalWith(BetaCacheControlEphemeral, { nullable: true }), + "content": S.Union(BetaRequestBashCodeExecutionToolResultError, BetaRequestBashCodeExecutionResultBlock), + "tool_use_id": S.String.pipe(S.pattern(new RegExp("^srvtoolu_[a-zA-Z0-9_]+$"))), + "type": S.Literal("bash_code_execution_tool_result") + }) +{} + +export class BetaTextEditorCodeExecutionToolResultErrorCode extends S.Literal( + "invalid_tool_input", + "unavailable", + "too_many_requests", + "execution_time_exceeded", + "file_not_found" +) {} + +export class BetaRequestTextEditorCodeExecutionToolResultError + extends S.Class( + "BetaRequestTextEditorCodeExecutionToolResultError" + )({ + "error_code": BetaTextEditorCodeExecutionToolResultErrorCode, + "error_message": S.optionalWith(S.String, { nullable: true }), + "type": S.Literal("text_editor_code_execution_tool_result_error") + }) +{} + +export class BetaRequestTextEditorCodeExecutionViewResultBlockFileType extends S.Literal("text", "image", "pdf") {} + +export class BetaRequestTextEditorCodeExecutionViewResultBlock + extends S.Class( + "BetaRequestTextEditorCodeExecutionViewResultBlock" + )({ + "content": S.String, + "file_type": BetaRequestTextEditorCodeExecutionViewResultBlockFileType, + "num_lines": S.optionalWith(S.Int, { nullable: true }), + "start_line": S.optionalWith(S.Int, { nullable: true }), + "total_lines": S.optionalWith(S.Int, { nullable: true }), + "type": S.Literal("text_editor_code_execution_view_result") + }) +{} + +export class BetaRequestTextEditorCodeExecutionCreateResultBlock + extends S.Class( + "BetaRequestTextEditorCodeExecutionCreateResultBlock" + )({ + "is_file_update": S.Boolean, + "type": S.Literal("text_editor_code_execution_create_result") + }) +{} + +export class BetaRequestTextEditorCodeExecutionStrReplaceResultBlock + extends S.Class( + "BetaRequestTextEditorCodeExecutionStrReplaceResultBlock" + )({ + "lines": S.optionalWith(S.Array(S.String), { nullable: true }), + "new_lines": S.optionalWith(S.Int, { nullable: true }), + "new_start": S.optionalWith(S.Int, { nullable: true }), + "old_lines": S.optionalWith(S.Int, { nullable: true }), + "old_start": S.optionalWith(S.Int, { nullable: true }), + "type": S.Literal("text_editor_code_execution_str_replace_result") + }) +{} + +export class BetaRequestTextEditorCodeExecutionToolResultBlock + extends S.Class( + "BetaRequestTextEditorCodeExecutionToolResultBlock" + )({ + /** + * Create a cache control breakpoint at this content block. + */ + "cache_control": S.optionalWith(BetaCacheControlEphemeral, { nullable: true }), + "content": S.Union( + BetaRequestTextEditorCodeExecutionToolResultError, + BetaRequestTextEditorCodeExecutionViewResultBlock, + BetaRequestTextEditorCodeExecutionCreateResultBlock, + BetaRequestTextEditorCodeExecutionStrReplaceResultBlock + ), + "tool_use_id": S.String.pipe(S.pattern(new RegExp("^srvtoolu_[a-zA-Z0-9_]+$"))), + "type": S.Literal("text_editor_code_execution_tool_result") + }) +{} + +export class BetaRequestMCPToolUseBlock extends S.Class("BetaRequestMCPToolUseBlock")({ + /** + * Create a cache control breakpoint at this content block. + */ + "cache_control": S.optionalWith(BetaCacheControlEphemeral, { nullable: true }), + "id": S.String.pipe(S.pattern(new RegExp("^[a-zA-Z0-9_-]+$"))), + "input": S.Record({ key: S.String, value: S.Unknown }), + "name": S.String, + /** + * The name of the MCP server + */ + "server_name": S.String, + "type": S.Literal("mcp_tool_use") +}) {} + +export class BetaRequestMCPToolResultBlock + extends S.Class("BetaRequestMCPToolResultBlock")({ + /** + * Create a cache control breakpoint at this content block. + */ + "cache_control": S.optionalWith(BetaCacheControlEphemeral, { nullable: true }), + "content": S.optionalWith(S.Union(S.String, S.Array(BetaRequestTextBlock)), { nullable: true }), + "is_error": S.optionalWith(S.Boolean, { nullable: true }), + "tool_use_id": S.String.pipe(S.pattern(new RegExp("^[a-zA-Z0-9_-]+$"))), + "type": S.Literal("mcp_tool_result") + }) +{} + +/** + * A content block that represents a file to be uploaded to the container + * Files uploaded via this block will be available in the container's input directory. + */ +export class BetaRequestContainerUploadBlock + extends S.Class("BetaRequestContainerUploadBlock")({ + /** + * Create a cache control breakpoint at this content block. + */ + "cache_control": S.optionalWith(BetaCacheControlEphemeral, { nullable: true }), + "file_id": S.String, + "type": S.Literal("container_upload") + }) +{} + +export class BetaInputContentBlock extends S.Union( + /** + * Regular text content. + */ + BetaRequestTextBlock, + /** + * Image content specified directly as base64 data or as a reference via a URL. + */ + BetaRequestImageBlock, + /** + * Document content, either specified directly as base64 data, as text, or as a reference via a URL. + */ + BetaRequestDocumentBlock, + /** + * A search result block containing source, title, and content from search operations. + */ + BetaRequestSearchResultBlock, + /** + * A block specifying internal thinking by the model. + */ + BetaRequestThinkingBlock, + /** + * A block specifying internal, redacted thinking by the model. + */ + BetaRequestRedactedThinkingBlock, + /** + * A block indicating a tool use by the model. + */ + BetaRequestToolUseBlock, + /** + * A block specifying the results of a tool use by the model. + */ + BetaRequestToolResultBlock, + BetaRequestServerToolUseBlock, + BetaRequestWebSearchToolResultBlock, + BetaRequestWebFetchToolResultBlock, + BetaRequestCodeExecutionToolResultBlock, + BetaRequestBashCodeExecutionToolResultBlock, + BetaRequestTextEditorCodeExecutionToolResultBlock, + BetaRequestMCPToolUseBlock, + BetaRequestMCPToolResultBlock, + BetaRequestContainerUploadBlock +) {} + +export class BetaInputMessageRole extends S.Literal("user", "assistant") {} + +export class BetaInputMessage extends S.Class("BetaInputMessage")({ + "content": S.Union(S.String, S.Array(BetaInputContentBlock)), + "role": BetaInputMessageRole +}) {} + +/** + * Type of skill - either 'anthropic' (built-in) or 'custom' (user-defined) + */ +export class BetaSkillParamsType extends S.Literal("anthropic", "custom") {} + +/** + * Specification for a skill to be loaded in a container (request model). + */ +export class BetaSkillParams extends S.Class("BetaSkillParams")({ + /** + * Skill ID + */ + "skill_id": S.String.pipe(S.minLength(1), S.maxLength(64)), + /** + * Type of skill - either 'anthropic' (built-in) or 'custom' (user-defined) + */ + "type": BetaSkillParamsType, + /** + * Skill version or 'latest' for most recent version + */ + "version": S.optionalWith(S.String.pipe(S.minLength(1), S.maxLength(64)), { nullable: true }) +}) {} + +/** + * Container parameters with skills to be loaded. + */ +export class BetaContainerParams extends S.Class("BetaContainerParams")({ + /** + * Container id + */ + "id": S.optionalWith(S.String, { nullable: true }), + /** + * List of skills to load in the container + */ + "skills": S.optionalWith(S.Array(BetaSkillParams).pipe(S.maxItems(8)), { nullable: true }) +}) {} + +export class BetaInputTokensClearAtLeast extends S.Class("BetaInputTokensClearAtLeast")({ + "type": S.Literal("input_tokens"), + "value": S.Int.pipe(S.greaterThanOrEqualTo(0)) +}) {} + +export class BetaToolUsesKeep extends S.Class("BetaToolUsesKeep")({ + "type": S.Literal("tool_uses"), + "value": S.Int.pipe(S.greaterThanOrEqualTo(0)) +}) {} + +export class BetaInputTokensTrigger extends S.Class("BetaInputTokensTrigger")({ + "type": S.Literal("input_tokens"), + "value": S.Int.pipe(S.greaterThanOrEqualTo(1)) +}) {} + +export class BetaToolUsesTrigger extends S.Class("BetaToolUsesTrigger")({ + "type": S.Literal("tool_uses"), + "value": S.Int.pipe(S.greaterThanOrEqualTo(1)) +}) {} + +export class BetaClearToolUses20250919 extends S.Class("BetaClearToolUses20250919")({ + /** + * Minimum number of tokens that must be cleared when triggered. Context will only be modified if at least this many tokens can be removed. + */ + "clear_at_least": S.optionalWith(BetaInputTokensClearAtLeast, { nullable: true }), + /** + * Whether to clear all tool inputs (bool) or specific tool inputs to clear (list) + */ + "clear_tool_inputs": S.optionalWith(S.Union(S.Boolean, S.Array(S.String)), { nullable: true }), + /** + * Tool names whose uses are preserved from clearing + */ + "exclude_tools": S.optionalWith(S.Array(S.String), { nullable: true }), + /** + * Number of tool uses to retain in the conversation + */ + "keep": S.optionalWith(BetaToolUsesKeep, { nullable: true }), + /** + * Condition that triggers the context management strategy + */ + "trigger": S.optionalWith(S.Union(BetaInputTokensTrigger, BetaToolUsesTrigger), { nullable: true }), + "type": S.Literal("clear_tool_uses_20250919") +}) {} + +export class BetaContextManagementConfig extends S.Class("BetaContextManagementConfig")({ + /** + * List of context management edits to apply + */ + "edits": S.optionalWith(S.Array(BetaClearToolUses20250919), { nullable: true }) +}) {} + +export class BetaRequestMCPServerToolConfiguration + extends S.Class("BetaRequestMCPServerToolConfiguration")({ + "allowed_tools": S.optionalWith(S.Array(S.String), { nullable: true }), + "enabled": S.optionalWith(S.Boolean, { nullable: true }) + }) +{} + +export class BetaRequestMCPServerURLDefinition + extends S.Class("BetaRequestMCPServerURLDefinition")({ + "authorization_token": S.optionalWith(S.String, { nullable: true }), + "name": S.String, + "tool_configuration": S.optionalWith(BetaRequestMCPServerToolConfiguration, { nullable: true }), + "type": S.Literal("url"), + "url": S.String + }) +{} + +export class BetaMetadata extends S.Class("BetaMetadata")({ + /** + * An external identifier for the user who is associated with the request. + * + * This should be a uuid, hash value, or other opaque identifier. Anthropic may use this id to help detect abuse. Do not include any identifying information such as name, email address, or phone number. + */ + "user_id": S.optionalWith(S.String.pipe(S.maxLength(256)), { nullable: true }) +}) {} + +/** + * Determines whether to use priority capacity (if available) or standard capacity for this request. + * + * Anthropic offers different levels of service for your API requests. See [service-tiers](https://docs.claude.com/en/api/service-tiers) for details. + */ +export class BetaCreateMessageParamsServiceTier extends S.Literal("auto", "standard_only") {} + +export class BetaThinkingConfigEnabled extends S.Class("BetaThinkingConfigEnabled")({ + /** + * Determines how many tokens Claude can use for its internal reasoning process. Larger budgets can enable more thorough analysis for complex problems, improving response quality. + * + * Must be ≥1024 and less than `max_tokens`. + * + * See [extended thinking](https://docs.claude.com/en/docs/build-with-claude/extended-thinking) for details. + */ + "budget_tokens": S.Int.pipe(S.greaterThanOrEqualTo(1024)), + "type": S.Literal("enabled") +}) {} + +export class BetaThinkingConfigDisabled extends S.Class("BetaThinkingConfigDisabled")({ + "type": S.Literal("disabled") +}) {} + +/** + * Configuration for enabling Claude's extended thinking. + * + * When enabled, responses include `thinking` content blocks showing Claude's thinking process before the final answer. Requires a minimum budget of 1,024 tokens and counts towards your `max_tokens` limit. + * + * See [extended thinking](https://docs.claude.com/en/docs/build-with-claude/extended-thinking) for details. + */ +export class BetaThinkingConfigParam extends S.Union(BetaThinkingConfigEnabled, BetaThinkingConfigDisabled) {} + +/** + * The model will automatically decide whether to use tools. + */ +export class BetaToolChoiceAuto extends S.Class("BetaToolChoiceAuto")({ + /** + * Whether to disable parallel tool use. + * + * Defaults to `false`. If set to `true`, the model will output at most one tool use. + */ + "disable_parallel_tool_use": S.optionalWith(S.Boolean, { nullable: true }), + "type": S.Literal("auto") +}) {} + +/** + * The model will use any available tools. + */ +export class BetaToolChoiceAny extends S.Class("BetaToolChoiceAny")({ + /** + * Whether to disable parallel tool use. + * + * Defaults to `false`. If set to `true`, the model will output exactly one tool use. + */ + "disable_parallel_tool_use": S.optionalWith(S.Boolean, { nullable: true }), + "type": S.Literal("any") +}) {} + +/** + * The model will use the specified tool with `tool_choice.name`. + */ +export class BetaToolChoiceTool extends S.Class("BetaToolChoiceTool")({ + /** + * Whether to disable parallel tool use. + * + * Defaults to `false`. If set to `true`, the model will output exactly one tool use. + */ + "disable_parallel_tool_use": S.optionalWith(S.Boolean, { nullable: true }), + /** + * The name of the tool to use. + */ + "name": S.String, + "type": S.Literal("tool") +}) {} + +/** + * The model will not be allowed to use tools. + */ +export class BetaToolChoiceNone extends S.Class("BetaToolChoiceNone")({ + "type": S.Literal("none") +}) {} + +/** + * How the model should use the provided tools. The model can use a specific tool, any available tool, decide by itself, or not use tools at all. + */ +export class BetaToolChoice + extends S.Union(BetaToolChoiceAuto, BetaToolChoiceAny, BetaToolChoiceTool, BetaToolChoiceNone) +{} + +export class BetaInputSchema extends S.Class("BetaInputSchema")({ + "properties": S.optionalWith(S.Record({ key: S.String, value: S.Unknown }), { nullable: true }), + "required": S.optionalWith(S.Array(S.String), { nullable: true }), + "type": S.Literal("object") +}) {} + +export class BetaTool extends S.Class("BetaTool")({ + "type": S.optionalWith(S.Literal("custom"), { nullable: true }), + /** + * Description of what this tool does. + * + * Tool descriptions should be as detailed as possible. The more information that the model has about what the tool is and how to use it, the better it will perform. You can use natural language descriptions to reinforce important aspects of the tool input JSON schema. + */ + "description": S.optionalWith(S.String, { nullable: true }), + /** + * Name of the tool. + * + * This is how the tool will be called by the model and in `tool_use` blocks. + */ + "name": S.String.pipe(S.minLength(1), S.maxLength(128), S.pattern(new RegExp("^[a-zA-Z0-9_-]{1,128}$"))), + /** + * [JSON schema](https://json-schema.org/draft/2020-12) for this tool's input. + * + * This defines the shape of the `input` that your tool accepts and that the model will produce. + */ + "input_schema": BetaInputSchema, + /** + * Create a cache control breakpoint at this content block. + */ + "cache_control": S.optionalWith(BetaCacheControlEphemeral, { nullable: true }) +}) {} + +export class BetaBashTool20241022 extends S.Class("BetaBashTool20241022")({ + /** + * Create a cache control breakpoint at this content block. + */ + "cache_control": S.optionalWith(BetaCacheControlEphemeral, { nullable: true }), + /** + * Name of the tool. + * + * This is how the tool will be called by the model and in `tool_use` blocks. + */ + "name": S.Literal("bash"), + "type": S.Literal("bash_20241022") +}) {} + +export class BetaBashTool20250124 extends S.Class("BetaBashTool20250124")({ + /** + * Create a cache control breakpoint at this content block. + */ + "cache_control": S.optionalWith(BetaCacheControlEphemeral, { nullable: true }), + /** + * Name of the tool. + * + * This is how the tool will be called by the model and in `tool_use` blocks. + */ + "name": S.Literal("bash"), + "type": S.Literal("bash_20250124") +}) {} + +export class BetaCodeExecutionTool20250522 + extends S.Class("BetaCodeExecutionTool20250522")({ + /** + * Create a cache control breakpoint at this content block. + */ + "cache_control": S.optionalWith(BetaCacheControlEphemeral, { nullable: true }), + /** + * Name of the tool. + * + * This is how the tool will be called by the model and in `tool_use` blocks. + */ + "name": S.Literal("code_execution"), + "type": S.Literal("code_execution_20250522") + }) +{} + +export class BetaCodeExecutionTool20250825 + extends S.Class("BetaCodeExecutionTool20250825")({ + /** + * Create a cache control breakpoint at this content block. + */ + "cache_control": S.optionalWith(BetaCacheControlEphemeral, { nullable: true }), + /** + * Name of the tool. + * + * This is how the tool will be called by the model and in `tool_use` blocks. + */ + "name": S.Literal("code_execution"), + "type": S.Literal("code_execution_20250825") + }) +{} + +export class BetaComputerUseTool20241022 extends S.Class("BetaComputerUseTool20241022")({ + /** + * Create a cache control breakpoint at this content block. + */ + "cache_control": S.optionalWith(BetaCacheControlEphemeral, { nullable: true }), + /** + * The height of the display in pixels. + */ + "display_height_px": S.Int.pipe(S.greaterThanOrEqualTo(1)), + /** + * The X11 display number (e.g. 0, 1) for the display. + */ + "display_number": S.optionalWith(S.Int.pipe(S.greaterThanOrEqualTo(0)), { nullable: true }), + /** + * The width of the display in pixels. + */ + "display_width_px": S.Int.pipe(S.greaterThanOrEqualTo(1)), + /** + * Name of the tool. + * + * This is how the tool will be called by the model and in `tool_use` blocks. + */ + "name": S.Literal("computer"), + "type": S.Literal("computer_20241022") +}) {} + +export class BetaMemoryTool20250818 extends S.Class("BetaMemoryTool20250818")({ + /** + * Create a cache control breakpoint at this content block. + */ + "cache_control": S.optionalWith(BetaCacheControlEphemeral, { nullable: true }), + /** + * Name of the tool. + * + * This is how the tool will be called by the model and in `tool_use` blocks. + */ + "name": S.Literal("memory"), + "type": S.Literal("memory_20250818") +}) {} + +export class BetaComputerUseTool20250124 extends S.Class("BetaComputerUseTool20250124")({ + /** + * Create a cache control breakpoint at this content block. + */ + "cache_control": S.optionalWith(BetaCacheControlEphemeral, { nullable: true }), + /** + * The height of the display in pixels. + */ + "display_height_px": S.Int.pipe(S.greaterThanOrEqualTo(1)), + /** + * The X11 display number (e.g. 0, 1) for the display. + */ + "display_number": S.optionalWith(S.Int.pipe(S.greaterThanOrEqualTo(0)), { nullable: true }), + /** + * The width of the display in pixels. + */ + "display_width_px": S.Int.pipe(S.greaterThanOrEqualTo(1)), + /** + * Name of the tool. + * + * This is how the tool will be called by the model and in `tool_use` blocks. + */ + "name": S.Literal("computer"), + "type": S.Literal("computer_20250124") +}) {} + +export class BetaTextEditor20241022 extends S.Class("BetaTextEditor20241022")({ + /** + * Create a cache control breakpoint at this content block. + */ + "cache_control": S.optionalWith(BetaCacheControlEphemeral, { nullable: true }), + /** + * Name of the tool. + * + * This is how the tool will be called by the model and in `tool_use` blocks. + */ + "name": S.Literal("str_replace_editor"), + "type": S.Literal("text_editor_20241022") +}) {} + +export class BetaTextEditor20250124 extends S.Class("BetaTextEditor20250124")({ + /** + * Create a cache control breakpoint at this content block. + */ + "cache_control": S.optionalWith(BetaCacheControlEphemeral, { nullable: true }), + /** + * Name of the tool. + * + * This is how the tool will be called by the model and in `tool_use` blocks. + */ + "name": S.Literal("str_replace_editor"), + "type": S.Literal("text_editor_20250124") +}) {} + +export class BetaTextEditor20250429 extends S.Class("BetaTextEditor20250429")({ + /** + * Create a cache control breakpoint at this content block. + */ + "cache_control": S.optionalWith(BetaCacheControlEphemeral, { nullable: true }), + /** + * Name of the tool. + * + * This is how the tool will be called by the model and in `tool_use` blocks. + */ + "name": S.Literal("str_replace_based_edit_tool"), + "type": S.Literal("text_editor_20250429") +}) {} + +export class BetaTextEditor20250728 extends S.Class("BetaTextEditor20250728")({ + /** + * Create a cache control breakpoint at this content block. + */ + "cache_control": S.optionalWith(BetaCacheControlEphemeral, { nullable: true }), + /** + * Maximum number of characters to display when viewing a file. If not specified, defaults to displaying the full file. + */ + "max_characters": S.optionalWith(S.Int.pipe(S.greaterThanOrEqualTo(1)), { nullable: true }), + /** + * Name of the tool. + * + * This is how the tool will be called by the model and in `tool_use` blocks. + */ + "name": S.Literal("str_replace_based_edit_tool"), + "type": S.Literal("text_editor_20250728") +}) {} + +export class BetaUserLocation extends S.Class("BetaUserLocation")({ + /** + * The city of the user. + */ + "city": S.optionalWith(S.String.pipe(S.minLength(1), S.maxLength(255)), { nullable: true }), + /** + * The two letter [ISO country code](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2) of the user. + */ + "country": S.optionalWith(S.String.pipe(S.minLength(2), S.maxLength(2)), { nullable: true }), + /** + * The region of the user. + */ + "region": S.optionalWith(S.String.pipe(S.minLength(1), S.maxLength(255)), { nullable: true }), + /** + * The [IANA timezone](https://nodatime.org/TimeZones) of the user. + */ + "timezone": S.optionalWith(S.String.pipe(S.minLength(1), S.maxLength(255)), { nullable: true }), + "type": S.Literal("approximate") +}) {} + +export class BetaWebSearchTool20250305 extends S.Class("BetaWebSearchTool20250305")({ + /** + * If provided, only these domains will be included in results. Cannot be used alongside `blocked_domains`. + */ + "allowed_domains": S.optionalWith(S.Array(S.String), { nullable: true }), + /** + * If provided, these domains will never appear in results. Cannot be used alongside `allowed_domains`. + */ + "blocked_domains": S.optionalWith(S.Array(S.String), { nullable: true }), + /** + * Create a cache control breakpoint at this content block. + */ + "cache_control": S.optionalWith(BetaCacheControlEphemeral, { nullable: true }), + /** + * Maximum number of times the tool can be used in the API request. + */ + "max_uses": S.optionalWith(S.Int.pipe(S.greaterThan(0)), { nullable: true }), + /** + * Name of the tool. + * + * This is how the tool will be called by the model and in `tool_use` blocks. + */ + "name": S.Literal("web_search"), + "type": S.Literal("web_search_20250305"), + /** + * Parameters for the user's location. Used to provide more relevant search results. + */ + "user_location": S.optionalWith(BetaUserLocation, { nullable: true }) +}) {} + +export class BetaWebFetchTool20250910 extends S.Class("BetaWebFetchTool20250910")({ + /** + * List of domains to allow fetching from + */ + "allowed_domains": S.optionalWith(S.Array(S.String), { nullable: true }), + /** + * List of domains to block fetching from + */ + "blocked_domains": S.optionalWith(S.Array(S.String), { nullable: true }), + /** + * Create a cache control breakpoint at this content block. + */ + "cache_control": S.optionalWith(BetaCacheControlEphemeral, { nullable: true }), + /** + * Citations configuration for fetched documents. Citations are disabled by default. + */ + "citations": S.optionalWith(BetaRequestCitationsConfig, { nullable: true }), + /** + * Maximum number of tokens used by including web page text content in the context. The limit is approximate and does not apply to binary content such as PDFs. + */ + "max_content_tokens": S.optionalWith(S.Int.pipe(S.greaterThan(0)), { nullable: true }), + /** + * Maximum number of times the tool can be used in the API request. + */ + "max_uses": S.optionalWith(S.Int.pipe(S.greaterThan(0)), { nullable: true }), + /** + * Name of the tool. + * + * This is how the tool will be called by the model and in `tool_use` blocks. + */ + "name": S.Literal("web_fetch"), + "type": S.Literal("web_fetch_20250910") +}) {} + +export class BetaCreateMessageParams extends S.Class("BetaCreateMessageParams")({ + "model": S.Union(S.String, Model), + /** + * Input messages. + * + * Our models are trained to operate on alternating `user` and `assistant` conversational turns. When creating a new `Message`, you specify the prior conversational turns with the `messages` parameter, and the model then generates the next `Message` in the conversation. Consecutive `user` or `assistant` turns in your request will be combined into a single turn. + * + * Each input message must be an object with a `role` and `content`. You can specify a single `user`-role message, or you can include multiple `user` and `assistant` messages. + * + * If the final message uses the `assistant` role, the response content will continue immediately from the content in that message. This can be used to constrain part of the model's response. + * + * Example with a single `user` message: + * + * ```json + * [{"role": "user", "content": "Hello, Claude"}] + * ``` + * + * Example with multiple conversational turns: + * + * ```json + * [ + * {"role": "user", "content": "Hello there."}, + * {"role": "assistant", "content": "Hi, I'm Claude. How can I help you?"}, + * {"role": "user", "content": "Can you explain LLMs in plain English?"}, + * ] + * ``` + * + * Example with a partially-filled response from Claude: + * + * ```json + * [ + * {"role": "user", "content": "What's the Greek name for Sun? (A) Sol (B) Helios (C) Sun"}, + * {"role": "assistant", "content": "The best answer is ("}, + * ] + * ``` + * + * Each input message `content` may be either a single `string` or an array of content blocks, where each block has a specific `type`. Using a `string` for `content` is shorthand for an array of one content block of type `"text"`. The following input messages are equivalent: + * + * ```json + * {"role": "user", "content": "Hello, Claude"} + * ``` + * + * ```json + * {"role": "user", "content": [{"type": "text", "text": "Hello, Claude"}]} + * ``` + * + * See [input examples](https://docs.claude.com/en/api/messages-examples). + * + * Note that if you want to include a [system prompt](https://docs.claude.com/en/docs/system-prompts), you can use the top-level `system` parameter — there is no `"system"` role for input messages in the Messages API. + * + * There is a limit of 100,000 messages in a single request. + */ + "messages": S.Array(BetaInputMessage), + /** + * Container identifier for reuse across requests. + */ + "container": S.optionalWith(S.Union(BetaContainerParams, S.String), { nullable: true }), + /** + * Context management configuration. + * + * This allows you to control how Claude manages context across multiple requests, such as whether to clear function results or not. + */ + "context_management": S.optionalWith(BetaContextManagementConfig, { nullable: true }), + /** + * The maximum number of tokens to generate before stopping. + * + * Note that our models may stop _before_ reaching this maximum. This parameter only specifies the absolute maximum number of tokens to generate. + * + * Different models have different maximum values for this parameter. See [models](https://docs.claude.com/en/docs/models-overview) for details. + */ + "max_tokens": S.Int.pipe(S.greaterThanOrEqualTo(1)), + /** + * MCP servers to be utilized in this request + */ + "mcp_servers": S.optionalWith(S.Array(BetaRequestMCPServerURLDefinition).pipe(S.maxItems(20)), { nullable: true }), + /** + * An object describing metadata about the request. + */ + "metadata": S.optionalWith(BetaMetadata, { nullable: true }), + /** + * Determines whether to use priority capacity (if available) or standard capacity for this request. + * + * Anthropic offers different levels of service for your API requests. See [service-tiers](https://docs.claude.com/en/api/service-tiers) for details. + */ + "service_tier": S.optionalWith(BetaCreateMessageParamsServiceTier, { nullable: true }), + /** + * Custom text sequences that will cause the model to stop generating. + * + * Our models will normally stop when they have naturally completed their turn, which will result in a response `stop_reason` of `"end_turn"`. + * + * If you want the model to stop generating when it encounters custom strings of text, you can use the `stop_sequences` parameter. If the model encounters one of the custom sequences, the response `stop_reason` value will be `"stop_sequence"` and the response `stop_sequence` value will contain the matched stop sequence. + */ + "stop_sequences": S.optionalWith(S.Array(S.String), { nullable: true }), + /** + * Whether to incrementally stream the response using server-sent events. + * + * See [streaming](https://docs.claude.com/en/api/messages-streaming) for details. + */ + "stream": S.optionalWith(S.Boolean, { nullable: true }), + /** + * System prompt. + * + * A system prompt is a way of providing context and instructions to Claude, such as specifying a particular goal or role. See our [guide to system prompts](https://docs.claude.com/en/docs/system-prompts). + */ + "system": S.optionalWith(S.Union(S.String, S.Array(BetaRequestTextBlock)), { nullable: true }), + /** + * Amount of randomness injected into the response. + * + * Defaults to `1.0`. Ranges from `0.0` to `1.0`. Use `temperature` closer to `0.0` for analytical / multiple choice, and closer to `1.0` for creative and generative tasks. + * + * Note that even with `temperature` of `0.0`, the results will not be fully deterministic. + */ + "temperature": S.optionalWith(S.Number.pipe(S.greaterThanOrEqualTo(0), S.lessThanOrEqualTo(1)), { nullable: true }), + "thinking": S.optionalWith(BetaThinkingConfigParam, { nullable: true }), + "tool_choice": S.optionalWith(BetaToolChoice, { nullable: true }), + /** + * Definitions of tools that the model may use. + * + * If you include `tools` in your API request, the model may return `tool_use` content blocks that represent the model's use of those tools. You can then run those tools using the tool input generated by the model and then optionally return results back to the model using `tool_result` content blocks. + * + * There are two types of tools: **client tools** and **server tools**. The behavior described below applies to client tools. For [server tools](https://docs.claude.com/en/docs/agents-and-tools/tool-use/overview\#server-tools), see their individual documentation as each has its own behavior (e.g., the [web search tool](https://docs.claude.com/en/docs/agents-and-tools/tool-use/web-search-tool)). + * + * Each tool definition includes: + * + * * `name`: Name of the tool. + * * `description`: Optional, but strongly-recommended description of the tool. + * * `input_schema`: [JSON schema](https://json-schema.org/draft/2020-12) for the tool `input` shape that the model will produce in `tool_use` output content blocks. + * + * For example, if you defined `tools` as: + * + * ```json + * [ + * { + * "name": "get_stock_price", + * "description": "Get the current stock price for a given ticker symbol.", + * "input_schema": { + * "type": "object", + * "properties": { + * "ticker": { + * "type": "string", + * "description": "The stock ticker symbol, e.g. AAPL for Apple Inc." + * } + * }, + * "required": ["ticker"] + * } + * } + * ] + * ``` + * + * And then asked the model "What's the S&P 500 at today?", the model might produce `tool_use` content blocks in the response like this: + * + * ```json + * [ + * { + * "type": "tool_use", + * "id": "toolu_01D7FLrfh4GYq7yT1ULFeyMV", + * "name": "get_stock_price", + * "input": { "ticker": "^GSPC" } + * } + * ] + * ``` + * + * You might then run your `get_stock_price` tool with `{"ticker": "^GSPC"}` as an input, and return the following back to the model in a subsequent `user` message: + * + * ```json + * [ + * { + * "type": "tool_result", + * "tool_use_id": "toolu_01D7FLrfh4GYq7yT1ULFeyMV", + * "content": "259.75 USD" + * } + * ] + * ``` + * + * Tools can be used for workflows that include running client-side tools and functions, or more generally whenever you want the model to produce a particular JSON structure of output. + * + * See our [guide](https://docs.claude.com/en/docs/tool-use) for more details. + */ + "tools": S.optionalWith( + S.Array( + S.Union( + BetaTool, + BetaBashTool20241022, + BetaBashTool20250124, + BetaCodeExecutionTool20250522, + BetaCodeExecutionTool20250825, + BetaComputerUseTool20241022, + BetaMemoryTool20250818, + BetaComputerUseTool20250124, + BetaTextEditor20241022, + BetaTextEditor20250124, + BetaTextEditor20250429, + BetaTextEditor20250728, + BetaWebSearchTool20250305, + BetaWebFetchTool20250910 + ) + ), + { nullable: true } + ), + /** + * Only sample from the top K options for each subsequent token. + * + * Used to remove "long tail" low probability responses. [Learn more technical details here](https://towardsdatascience.com/how-to-sample-from-language-models-682bceb97277). + * + * Recommended for advanced use cases only. You usually only need to use `temperature`. + */ + "top_k": S.optionalWith(S.Int.pipe(S.greaterThanOrEqualTo(0)), { nullable: true }), + /** + * Use nucleus sampling. + * + * In nucleus sampling, we compute the cumulative distribution over all the options for each subsequent token in decreasing probability order and cut it off once it reaches a particular probability specified by `top_p`. You should either alter `temperature` or `top_p`, but not both. + * + * Recommended for advanced use cases only. You usually only need to use `temperature`. + */ + "top_p": S.optionalWith(S.Number.pipe(S.greaterThanOrEqualTo(0), S.lessThanOrEqualTo(1)), { nullable: true }) +}) {} + +export class BetaResponseCharLocationCitation + extends S.Class("BetaResponseCharLocationCitation")({ + "cited_text": S.String, + "document_index": S.Int.pipe(S.greaterThanOrEqualTo(0)), + "document_title": S.NullOr(S.String), + "end_char_index": S.Int, + "file_id": S.optionalWith(S.NullOr(S.String), { default: () => null }), + "start_char_index": S.Int.pipe(S.greaterThanOrEqualTo(0)), + "type": S.Literal("char_location").pipe( + S.propertySignature, + S.withConstructorDefault(() => "char_location" as const) + ) + }) +{} + +export class BetaResponsePageLocationCitation + extends S.Class("BetaResponsePageLocationCitation")({ + "cited_text": S.String, + "document_index": S.Int.pipe(S.greaterThanOrEqualTo(0)), + "document_title": S.NullOr(S.String), + "end_page_number": S.Int, + "file_id": S.optionalWith(S.NullOr(S.String), { default: () => null }), + "start_page_number": S.Int.pipe(S.greaterThanOrEqualTo(1)), + "type": S.Literal("page_location").pipe( + S.propertySignature, + S.withConstructorDefault(() => "page_location" as const) + ) + }) +{} + +export class BetaResponseContentBlockLocationCitation + extends S.Class("BetaResponseContentBlockLocationCitation")({ + "cited_text": S.String, + "document_index": S.Int.pipe(S.greaterThanOrEqualTo(0)), + "document_title": S.NullOr(S.String), + "end_block_index": S.Int, + "file_id": S.optionalWith(S.NullOr(S.String), { default: () => null }), + "start_block_index": S.Int.pipe(S.greaterThanOrEqualTo(0)), + "type": S.Literal("content_block_location").pipe( + S.propertySignature, + S.withConstructorDefault(() => "content_block_location" as const) + ) + }) +{} + +export class BetaResponseWebSearchResultLocationCitation + extends S.Class("BetaResponseWebSearchResultLocationCitation")({ + "cited_text": S.String, + "encrypted_index": S.String, + "title": S.NullOr(S.String.pipe(S.maxLength(512))), + "type": S.Literal("web_search_result_location").pipe( + S.propertySignature, + S.withConstructorDefault(() => "web_search_result_location" as const) + ), + "url": S.String + }) +{} + +export class BetaResponseSearchResultLocationCitation + extends S.Class("BetaResponseSearchResultLocationCitation")({ + "cited_text": S.String, + "end_block_index": S.Int, + "search_result_index": S.Int.pipe(S.greaterThanOrEqualTo(0)), + "source": S.String, + "start_block_index": S.Int.pipe(S.greaterThanOrEqualTo(0)), + "title": S.NullOr(S.String), + "type": S.Literal("search_result_location").pipe( + S.propertySignature, + S.withConstructorDefault(() => "search_result_location" as const) + ) + }) +{} + +export class BetaResponseTextBlock extends S.Class("BetaResponseTextBlock")({ + /** + * Citations supporting the text block. + * + * The type of citation returned will depend on the type of document being cited. Citing a PDF results in `page_location`, plain text results in `char_location`, and content document results in `content_block_location`. + */ + "citations": S.optionalWith( + S.NullOr( + S.Array( + S.Union( + BetaResponseCharLocationCitation, + BetaResponsePageLocationCitation, + BetaResponseContentBlockLocationCitation, + BetaResponseWebSearchResultLocationCitation, + BetaResponseSearchResultLocationCitation + ) + ) + ), + { default: () => null } + ), + "text": S.String.pipe(S.minLength(0), S.maxLength(5000000)), + "type": S.Literal("text").pipe(S.propertySignature, S.withConstructorDefault(() => "text" as const)) +}) {} + +export class BetaResponseThinkingBlock extends S.Class("BetaResponseThinkingBlock")({ + "signature": S.String, + "thinking": S.String, + "type": S.Literal("thinking").pipe(S.propertySignature, S.withConstructorDefault(() => "thinking" as const)) +}) {} + +export class BetaResponseRedactedThinkingBlock + extends S.Class("BetaResponseRedactedThinkingBlock")({ + "data": S.String, + "type": S.Literal("redacted_thinking").pipe( + S.propertySignature, + S.withConstructorDefault(() => "redacted_thinking" as const) + ) + }) +{} + +export class BetaResponseToolUseBlock extends S.Class("BetaResponseToolUseBlock")({ + "id": S.String.pipe(S.pattern(new RegExp("^[a-zA-Z0-9_-]+$"))), + "input": S.Record({ key: S.String, value: S.Unknown }), + "name": S.String.pipe(S.minLength(1)), + "type": S.Literal("tool_use").pipe(S.propertySignature, S.withConstructorDefault(() => "tool_use" as const)) +}) {} + +export class BetaResponseServerToolUseBlockName + extends S.Literal("web_search", "web_fetch", "code_execution", "bash_code_execution", "text_editor_code_execution") +{} + +export class BetaResponseServerToolUseBlock + extends S.Class("BetaResponseServerToolUseBlock")({ + "id": S.String.pipe(S.pattern(new RegExp("^srvtoolu_[a-zA-Z0-9_]+$"))), + "input": S.Record({ key: S.String, value: S.Unknown }), + "name": BetaResponseServerToolUseBlockName, + "type": S.Literal("server_tool_use").pipe( + S.propertySignature, + S.withConstructorDefault(() => "server_tool_use" as const) + ) + }) +{} + +export class BetaResponseWebSearchToolResultError + extends S.Class("BetaResponseWebSearchToolResultError")({ + "error_code": BetaWebSearchToolResultErrorCode, + "type": S.Literal("web_search_tool_result_error").pipe( + S.propertySignature, + S.withConstructorDefault(() => "web_search_tool_result_error" as const) + ) + }) +{} + +export class BetaResponseWebSearchResultBlock + extends S.Class("BetaResponseWebSearchResultBlock")({ + "encrypted_content": S.String, + "page_age": S.optionalWith(S.NullOr(S.String), { default: () => null }), + "title": S.String, + "type": S.Literal("web_search_result").pipe( + S.propertySignature, + S.withConstructorDefault(() => "web_search_result" as const) + ), + "url": S.String + }) +{} + +export class BetaResponseWebSearchToolResultBlock + extends S.Class("BetaResponseWebSearchToolResultBlock")({ + "content": S.Union(BetaResponseWebSearchToolResultError, S.Array(BetaResponseWebSearchResultBlock)), + "tool_use_id": S.String.pipe(S.pattern(new RegExp("^srvtoolu_[a-zA-Z0-9_]+$"))), + "type": S.Literal("web_search_tool_result").pipe( + S.propertySignature, + S.withConstructorDefault(() => "web_search_tool_result" as const) + ) + }) +{} + +export class BetaResponseWebFetchToolResultError + extends S.Class("BetaResponseWebFetchToolResultError")({ + "error_code": BetaWebFetchToolResultErrorCode, + "type": S.Literal("web_fetch_tool_result_error").pipe( + S.propertySignature, + S.withConstructorDefault(() => "web_fetch_tool_result_error" as const) + ) + }) +{} + +export class BetaResponseCitationsConfig extends S.Class("BetaResponseCitationsConfig")({ + "enabled": S.Boolean.pipe(S.propertySignature, S.withConstructorDefault(() => false as const)) +}) {} + +export class BetaResponseDocumentBlock extends S.Class("BetaResponseDocumentBlock")({ + /** + * Citation configuration for the document + */ + "citations": S.optionalWith(S.NullOr(BetaResponseCitationsConfig), { default: () => null }), + "source": S.Union(BetaBase64PDFSource, BetaPlainTextSource), + /** + * The title of the document + */ + "title": S.optionalWith(S.NullOr(S.String), { default: () => null }), + "type": S.Literal("document").pipe(S.propertySignature, S.withConstructorDefault(() => "document" as const)) +}) {} + +export class BetaResponseWebFetchResultBlock + extends S.Class("BetaResponseWebFetchResultBlock")({ + "content": BetaResponseDocumentBlock, + /** + * ISO 8601 timestamp when the content was retrieved + */ + "retrieved_at": S.optionalWith(S.NullOr(S.String), { default: () => null }), + "type": S.Literal("web_fetch_result").pipe( + S.propertySignature, + S.withConstructorDefault(() => "web_fetch_result" as const) + ), + /** + * Fetched content URL + */ + "url": S.String + }) +{} + +export class BetaResponseWebFetchToolResultBlock + extends S.Class("BetaResponseWebFetchToolResultBlock")({ + "content": S.Union(BetaResponseWebFetchToolResultError, BetaResponseWebFetchResultBlock), + "tool_use_id": S.String.pipe(S.pattern(new RegExp("^srvtoolu_[a-zA-Z0-9_]+$"))), + "type": S.Literal("web_fetch_tool_result").pipe( + S.propertySignature, + S.withConstructorDefault(() => "web_fetch_tool_result" as const) + ) + }) +{} + +export class BetaResponseCodeExecutionToolResultError + extends S.Class("BetaResponseCodeExecutionToolResultError")({ + "error_code": BetaCodeExecutionToolResultErrorCode, + "type": S.Literal("code_execution_tool_result_error").pipe( + S.propertySignature, + S.withConstructorDefault(() => "code_execution_tool_result_error" as const) + ) + }) +{} + +export class BetaResponseCodeExecutionOutputBlock + extends S.Class("BetaResponseCodeExecutionOutputBlock")({ + "file_id": S.String, + "type": S.Literal("code_execution_output").pipe( + S.propertySignature, + S.withConstructorDefault(() => "code_execution_output" as const) + ) + }) +{} + +export class BetaResponseCodeExecutionResultBlock + extends S.Class("BetaResponseCodeExecutionResultBlock")({ + "content": S.Array(BetaResponseCodeExecutionOutputBlock), + "return_code": S.Int, + "stderr": S.String, + "stdout": S.String, + "type": S.Literal("code_execution_result").pipe( + S.propertySignature, + S.withConstructorDefault(() => "code_execution_result" as const) + ) + }) +{} + +export class BetaResponseCodeExecutionToolResultBlock + extends S.Class("BetaResponseCodeExecutionToolResultBlock")({ + "content": S.Union(BetaResponseCodeExecutionToolResultError, BetaResponseCodeExecutionResultBlock), + "tool_use_id": S.String.pipe(S.pattern(new RegExp("^srvtoolu_[a-zA-Z0-9_]+$"))), + "type": S.Literal("code_execution_tool_result").pipe( + S.propertySignature, + S.withConstructorDefault(() => "code_execution_tool_result" as const) + ) + }) +{} + +export class BetaResponseBashCodeExecutionToolResultError + extends S.Class("BetaResponseBashCodeExecutionToolResultError")({ + "error_code": BetaBashCodeExecutionToolResultErrorCode, + "type": S.Literal("bash_code_execution_tool_result_error").pipe( + S.propertySignature, + S.withConstructorDefault(() => "bash_code_execution_tool_result_error" as const) + ) + }) +{} + +export class BetaResponseBashCodeExecutionOutputBlock + extends S.Class("BetaResponseBashCodeExecutionOutputBlock")({ + "file_id": S.String, + "type": S.Literal("bash_code_execution_output").pipe( + S.propertySignature, + S.withConstructorDefault(() => "bash_code_execution_output" as const) + ) + }) +{} + +export class BetaResponseBashCodeExecutionResultBlock + extends S.Class("BetaResponseBashCodeExecutionResultBlock")({ + "content": S.Array(BetaResponseBashCodeExecutionOutputBlock), + "return_code": S.Int, + "stderr": S.String, + "stdout": S.String, + "type": S.Literal("bash_code_execution_result").pipe( + S.propertySignature, + S.withConstructorDefault(() => "bash_code_execution_result" as const) + ) + }) +{} + +export class BetaResponseBashCodeExecutionToolResultBlock + extends S.Class("BetaResponseBashCodeExecutionToolResultBlock")({ + "content": S.Union(BetaResponseBashCodeExecutionToolResultError, BetaResponseBashCodeExecutionResultBlock), + "tool_use_id": S.String.pipe(S.pattern(new RegExp("^srvtoolu_[a-zA-Z0-9_]+$"))), + "type": S.Literal("bash_code_execution_tool_result").pipe( + S.propertySignature, + S.withConstructorDefault(() => "bash_code_execution_tool_result" as const) + ) + }) +{} + +export class BetaResponseTextEditorCodeExecutionToolResultError + extends S.Class( + "BetaResponseTextEditorCodeExecutionToolResultError" + )({ + "error_code": BetaTextEditorCodeExecutionToolResultErrorCode, + "error_message": S.optionalWith(S.NullOr(S.String), { default: () => null }), + "type": S.Literal("text_editor_code_execution_tool_result_error").pipe( + S.propertySignature, + S.withConstructorDefault(() => "text_editor_code_execution_tool_result_error" as const) + ) + }) +{} + +export class BetaResponseTextEditorCodeExecutionViewResultBlockFileType extends S.Literal("text", "image", "pdf") {} + +export class BetaResponseTextEditorCodeExecutionViewResultBlock + extends S.Class( + "BetaResponseTextEditorCodeExecutionViewResultBlock" + )({ + "content": S.String, + "file_type": BetaResponseTextEditorCodeExecutionViewResultBlockFileType, + "num_lines": S.optionalWith(S.NullOr(S.Int), { default: () => null }), + "start_line": S.optionalWith(S.NullOr(S.Int), { default: () => null }), + "total_lines": S.optionalWith(S.NullOr(S.Int), { default: () => null }), + "type": S.Literal("text_editor_code_execution_view_result").pipe( + S.propertySignature, + S.withConstructorDefault(() => "text_editor_code_execution_view_result" as const) + ) + }) +{} + +export class BetaResponseTextEditorCodeExecutionCreateResultBlock + extends S.Class( + "BetaResponseTextEditorCodeExecutionCreateResultBlock" + )({ + "is_file_update": S.Boolean, + "type": S.Literal("text_editor_code_execution_create_result").pipe( + S.propertySignature, + S.withConstructorDefault(() => "text_editor_code_execution_create_result" as const) + ) + }) +{} + +export class BetaResponseTextEditorCodeExecutionStrReplaceResultBlock + extends S.Class( + "BetaResponseTextEditorCodeExecutionStrReplaceResultBlock" + )({ + "lines": S.optionalWith(S.NullOr(S.Array(S.String)), { default: () => null }), + "new_lines": S.optionalWith(S.NullOr(S.Int), { default: () => null }), + "new_start": S.optionalWith(S.NullOr(S.Int), { default: () => null }), + "old_lines": S.optionalWith(S.NullOr(S.Int), { default: () => null }), + "old_start": S.optionalWith(S.NullOr(S.Int), { default: () => null }), + "type": S.Literal("text_editor_code_execution_str_replace_result").pipe( + S.propertySignature, + S.withConstructorDefault(() => "text_editor_code_execution_str_replace_result" as const) + ) + }) +{} + +export class BetaResponseTextEditorCodeExecutionToolResultBlock + extends S.Class( + "BetaResponseTextEditorCodeExecutionToolResultBlock" + )({ + "content": S.Union( + BetaResponseTextEditorCodeExecutionToolResultError, + BetaResponseTextEditorCodeExecutionViewResultBlock, + BetaResponseTextEditorCodeExecutionCreateResultBlock, + BetaResponseTextEditorCodeExecutionStrReplaceResultBlock + ), + "tool_use_id": S.String.pipe(S.pattern(new RegExp("^srvtoolu_[a-zA-Z0-9_]+$"))), + "type": S.Literal("text_editor_code_execution_tool_result").pipe( + S.propertySignature, + S.withConstructorDefault(() => "text_editor_code_execution_tool_result" as const) + ) + }) +{} + +export class BetaResponseMCPToolUseBlock extends S.Class("BetaResponseMCPToolUseBlock")({ + "id": S.String.pipe(S.pattern(new RegExp("^[a-zA-Z0-9_-]+$"))), + "input": S.Record({ key: S.String, value: S.Unknown }), + /** + * The name of the MCP tool + */ + "name": S.String, + /** + * The name of the MCP server + */ + "server_name": S.String, + "type": S.Literal("mcp_tool_use").pipe(S.propertySignature, S.withConstructorDefault(() => "mcp_tool_use" as const)) +}) {} + +export class BetaResponseMCPToolResultBlock + extends S.Class("BetaResponseMCPToolResultBlock")({ + "content": S.Union(S.String, S.Array(BetaResponseTextBlock)), + "is_error": S.Boolean.pipe(S.propertySignature, S.withConstructorDefault(() => false as const)), + "tool_use_id": S.String.pipe(S.pattern(new RegExp("^[a-zA-Z0-9_-]+$"))), + "type": S.Literal("mcp_tool_result").pipe( + S.propertySignature, + S.withConstructorDefault(() => "mcp_tool_result" as const) + ) + }) +{} + +/** + * Response model for a file uploaded to the container. + */ +export class BetaResponseContainerUploadBlock + extends S.Class("BetaResponseContainerUploadBlock")({ + "file_id": S.String, + "type": S.Literal("container_upload").pipe( + S.propertySignature, + S.withConstructorDefault(() => "container_upload" as const) + ) + }) +{} + +export class BetaContentBlock extends S.Union( + BetaResponseTextBlock, + BetaResponseThinkingBlock, + BetaResponseRedactedThinkingBlock, + BetaResponseToolUseBlock, + BetaResponseServerToolUseBlock, + BetaResponseWebSearchToolResultBlock, + BetaResponseWebFetchToolResultBlock, + BetaResponseCodeExecutionToolResultBlock, + BetaResponseBashCodeExecutionToolResultBlock, + BetaResponseTextEditorCodeExecutionToolResultBlock, + BetaResponseMCPToolUseBlock, + BetaResponseMCPToolResultBlock, + BetaResponseContainerUploadBlock +) {} + +export class BetaStopReason extends S.Literal( + "end_turn", + "max_tokens", + "stop_sequence", + "tool_use", + "pause_turn", + "refusal", + "model_context_window_exceeded" +) {} + +export class BetaCacheCreation extends S.Class("BetaCacheCreation")({ + /** + * The number of input tokens used to create the 1 hour cache entry. + */ + "ephemeral_1h_input_tokens": S.Int.pipe(S.greaterThanOrEqualTo(0)).pipe( + S.propertySignature, + S.withConstructorDefault(() => 0 as const) + ), + /** + * The number of input tokens used to create the 5 minute cache entry. + */ + "ephemeral_5m_input_tokens": S.Int.pipe(S.greaterThanOrEqualTo(0)).pipe( + S.propertySignature, + S.withConstructorDefault(() => 0 as const) + ) +}) {} + +export class BetaServerToolUsage extends S.Class("BetaServerToolUsage")({ + /** + * The number of web fetch tool requests. + */ + "web_fetch_requests": S.Int.pipe(S.greaterThanOrEqualTo(0)).pipe( + S.propertySignature, + S.withConstructorDefault(() => 0 as const) + ), + /** + * The number of web search tool requests. + */ + "web_search_requests": S.Int.pipe(S.greaterThanOrEqualTo(0)).pipe( + S.propertySignature, + S.withConstructorDefault(() => 0 as const) + ) +}) {} + +export class BetaUsageServiceTierEnum extends S.Literal("standard", "priority", "batch") {} + +export class BetaUsage extends S.Class("BetaUsage")({ + /** + * Breakdown of cached tokens by TTL + */ + "cache_creation": S.optionalWith(S.NullOr(BetaCacheCreation), { default: () => null }), + /** + * The number of input tokens used to create the cache entry. + */ + "cache_creation_input_tokens": S.optionalWith(S.NullOr(S.Int.pipe(S.greaterThanOrEqualTo(0))), { + default: () => null + }), + /** + * The number of input tokens read from the cache. + */ + "cache_read_input_tokens": S.optionalWith(S.NullOr(S.Int.pipe(S.greaterThanOrEqualTo(0))), { default: () => null }), + /** + * The number of input tokens which were used. + */ + "input_tokens": S.Int.pipe(S.greaterThanOrEqualTo(0)), + /** + * The number of output tokens which were used. + */ + "output_tokens": S.Int.pipe(S.greaterThanOrEqualTo(0)), + /** + * The number of server tool requests. + */ + "server_tool_use": S.optionalWith(S.NullOr(BetaServerToolUsage), { default: () => null }), + /** + * If the request used the priority, standard, or batch tier. + */ + "service_tier": S.optionalWith(S.NullOr(BetaUsageServiceTierEnum), { default: () => null }) +}) {} + +export class BetaResponseClearToolUses20250919Edit + extends S.Class("BetaResponseClearToolUses20250919Edit")({ + /** + * Number of input tokens cleared by this edit. + */ + "cleared_input_tokens": S.Int.pipe(S.greaterThanOrEqualTo(0)), + /** + * Number of tool uses that were cleared. + */ + "cleared_tool_uses": S.Int.pipe(S.greaterThanOrEqualTo(0)), + /** + * The type of context management edit applied. + */ + "type": S.Literal("clear_tool_uses_20250919").pipe( + S.propertySignature, + S.withConstructorDefault(() => "clear_tool_uses_20250919" as const) + ) + }) +{} + +export class BetaResponseContextManagement + extends S.Class("BetaResponseContextManagement")({ + /** + * List of context management edits that were applied. + */ + "applied_edits": S.Array(BetaResponseClearToolUses20250919Edit) + }) +{} + +/** + * Type of skill - either 'anthropic' (built-in) or 'custom' (user-defined) + */ +export class BetaSkillType extends S.Literal("anthropic", "custom") {} + +/** + * A skill that was loaded in a container (response model). + */ +export class BetaSkill extends S.Class("BetaSkill")({ + /** + * Skill ID + */ + "skill_id": S.String.pipe(S.minLength(1), S.maxLength(64)), + /** + * Type of skill - either 'anthropic' (built-in) or 'custom' (user-defined) + */ + "type": BetaSkillType, + /** + * Skill version or 'latest' for most recent version + */ + "version": S.String.pipe(S.minLength(1), S.maxLength(64)) +}) {} + +/** + * Information about the container used in the request (for the code execution tool) + */ +export class BetaContainer extends S.Class("BetaContainer")({ + /** + * The time at which the container will expire. + */ + "expires_at": S.String, + /** + * Identifier for the container used in this request + */ + "id": S.String, + /** + * Skills loaded in the container + */ + "skills": S.optionalWith(S.NullOr(S.Array(BetaSkill)), { default: () => null }) +}) {} + +export class BetaMessage extends S.Class("BetaMessage")({ + /** + * Unique object identifier. + * + * The format and length of IDs may change over time. + */ + "id": S.String, + /** + * Object type. + * + * For Messages, this is always `"message"`. + */ + "type": S.Literal("message").pipe(S.propertySignature, S.withConstructorDefault(() => "message" as const)), + /** + * Conversational role of the generated message. + * + * This will always be `"assistant"`. + */ + "role": S.Literal("assistant").pipe(S.propertySignature, S.withConstructorDefault(() => "assistant" as const)), + /** + * Content generated by the model. + * + * This is an array of content blocks, each of which has a `type` that determines its shape. + * + * Example: + * + * ```json + * [{"type": "text", "text": "Hi, I'm Claude."}] + * ``` + * + * If the request input `messages` ended with an `assistant` turn, then the response `content` will continue directly from that last turn. You can use this to constrain the model's output. + * + * For example, if the input `messages` were: + * ```json + * [ + * {"role": "user", "content": "What's the Greek name for Sun? (A) Sol (B) Helios (C) Sun"}, + * {"role": "assistant", "content": "The best answer is ("} + * ] + * ``` + * + * Then the response `content` might be: + * + * ```json + * [{"type": "text", "text": "B)"}] + * ``` + */ + "content": S.Array(BetaContentBlock), + "model": S.Union(S.String, Model), + /** + * The reason that we stopped. + * + * This may be one the following values: + * * `"end_turn"`: the model reached a natural stopping point + * * `"max_tokens"`: we exceeded the requested `max_tokens` or the model's maximum + * * `"stop_sequence"`: one of your provided custom `stop_sequences` was generated + * * `"tool_use"`: the model invoked one or more tools + * * `"pause_turn"`: we paused a long-running turn. You may provide the response back as-is in a subsequent request to let the model continue. + * * `"refusal"`: when streaming classifiers intervene to handle potential policy violations + * + * In non-streaming mode this value is always non-null. In streaming mode, it is null in the `message_start` event and non-null otherwise. + */ + "stop_reason": S.NullOr(BetaStopReason), + /** + * Which custom stop sequence was generated, if any. + * + * This value will be a non-null string if one of your custom stop sequences was generated. + */ + "stop_sequence": S.optionalWith(S.NullOr(S.String), { default: () => null }), + /** + * Billing and rate-limit usage. + * + * Anthropic's API bills and rate-limits by token counts, as tokens represent the underlying cost to our systems. + * + * Under the hood, the API transforms requests into a format suitable for the model. The model's output then goes through a parsing stage before becoming an API response. As a result, the token counts in `usage` will not match one-to-one with the exact visible content of an API request or response. + * + * For example, `output_tokens` will be non-zero, even for an empty string response from Claude. + * + * Total input tokens in a request is the summation of `input_tokens`, `cache_creation_input_tokens`, and `cache_read_input_tokens`. + */ + "usage": BetaUsage, + /** + * Context management response. + * + * Information about context management strategies applied during the request. + */ + "context_management": S.optionalWith(S.NullOr(BetaResponseContextManagement), { default: () => null }), + /** + * Information about the container used in this request. + * + * This will be non-null if a container tool (e.g. code execution) was used. + */ + "container": S.optionalWith(S.NullOr(BetaContainer), { default: () => null }) +}) {} + +export class BetaInvalidRequestError extends S.Class("BetaInvalidRequestError")({ + "message": S.String.pipe(S.propertySignature, S.withConstructorDefault(() => "Invalid request" as const)), + "type": S.Literal("invalid_request_error").pipe( + S.propertySignature, + S.withConstructorDefault(() => "invalid_request_error" as const) + ) +}) {} + +export class BetaAuthenticationError extends S.Class("BetaAuthenticationError")({ + "message": S.String.pipe(S.propertySignature, S.withConstructorDefault(() => "Authentication error" as const)), + "type": S.Literal("authentication_error").pipe( + S.propertySignature, + S.withConstructorDefault(() => "authentication_error" as const) + ) +}) {} + +export class BetaBillingError extends S.Class("BetaBillingError")({ + "message": S.String.pipe(S.propertySignature, S.withConstructorDefault(() => "Billing error" as const)), + "type": S.Literal("billing_error").pipe(S.propertySignature, S.withConstructorDefault(() => "billing_error" as const)) +}) {} + +export class BetaPermissionError extends S.Class("BetaPermissionError")({ + "message": S.String.pipe(S.propertySignature, S.withConstructorDefault(() => "Permission denied" as const)), + "type": S.Literal("permission_error").pipe( + S.propertySignature, + S.withConstructorDefault(() => "permission_error" as const) + ) +}) {} + +export class BetaNotFoundError extends S.Class("BetaNotFoundError")({ + "message": S.String.pipe(S.propertySignature, S.withConstructorDefault(() => "Not found" as const)), + "type": S.Literal("not_found_error").pipe( + S.propertySignature, + S.withConstructorDefault(() => "not_found_error" as const) + ) +}) {} + +export class BetaRateLimitError extends S.Class("BetaRateLimitError")({ + "message": S.String.pipe(S.propertySignature, S.withConstructorDefault(() => "Rate limited" as const)), + "type": S.Literal("rate_limit_error").pipe( + S.propertySignature, + S.withConstructorDefault(() => "rate_limit_error" as const) + ) +}) {} + +export class BetaGatewayTimeoutError extends S.Class("BetaGatewayTimeoutError")({ + "message": S.String.pipe(S.propertySignature, S.withConstructorDefault(() => "Request timeout" as const)), + "type": S.Literal("timeout_error").pipe(S.propertySignature, S.withConstructorDefault(() => "timeout_error" as const)) +}) {} + +export class BetaAPIError extends S.Class("BetaAPIError")({ + "message": S.String.pipe(S.propertySignature, S.withConstructorDefault(() => "Internal server error" as const)), + "type": S.Literal("api_error").pipe(S.propertySignature, S.withConstructorDefault(() => "api_error" as const)) +}) {} + +export class BetaOverloadedError extends S.Class("BetaOverloadedError")({ + "message": S.String.pipe(S.propertySignature, S.withConstructorDefault(() => "Overloaded" as const)), + "type": S.Literal("overloaded_error").pipe( + S.propertySignature, + S.withConstructorDefault(() => "overloaded_error" as const) + ) +}) {} + +export class BetaErrorResponse extends S.Class("BetaErrorResponse")({ + "error": S.Union( + BetaInvalidRequestError, + BetaAuthenticationError, + BetaBillingError, + BetaPermissionError, + BetaNotFoundError, + BetaRateLimitError, + BetaGatewayTimeoutError, + BetaAPIError, + BetaOverloadedError + ), + "request_id": S.optionalWith(S.NullOr(S.String), { default: () => null }), + "type": S.Literal("error").pipe(S.propertySignature, S.withConstructorDefault(() => "error" as const)) +}) {} + +export class BetaModelsListParams extends S.Struct({ + /** + * ID of the object to use as a cursor for pagination. When provided, returns the page of results immediately before this object. + */ + "before_id": S.optionalWith(S.String, { nullable: true }), + /** + * ID of the object to use as a cursor for pagination. When provided, returns the page of results immediately after this object. + */ + "after_id": S.optionalWith(S.String, { nullable: true }), + /** + * Number of items to return per page. + * + * Defaults to `20`. Ranges from `1` to `1000`. + */ + "limit": S.optionalWith(S.Int.pipe(S.greaterThanOrEqualTo(1), S.lessThanOrEqualTo(1000)), { + nullable: true, + default: () => 20 as const + }), + /** + * The version of the Claude API you want to use. + * + * Read more about versioning and our version history [here](https://docs.claude.com/en/api/versioning). + */ + "anthropic-version": S.optionalWith(S.String, { nullable: true }), + /** + * Your unique API key for authentication. + * + * This key is required in the header of all API requests, to authenticate your account and access Anthropic's services. Get your API key through the [Console](https://console.anthropic.com/settings/keys). Each key is scoped to a Workspace. + */ + "x-api-key": S.optionalWith(S.String, { nullable: true }), + /** + * Optional header to specify the beta version(s) you want to use. + * + * To use multiple betas, use a comma separated list like `beta1,beta2` or specify the header multiple times for each beta. + */ + "anthropic-beta": S.optionalWith(S.String, { nullable: true }) +}) {} + +export class BetaModelInfo extends S.Class("BetaModelInfo")({ + /** + * RFC 3339 datetime string representing the time at which the model was released. May be set to an epoch value if the release date is unknown. + */ + "created_at": S.String, + /** + * A human-readable name for the model. + */ + "display_name": S.String, + /** + * Unique model identifier. + */ + "id": S.String, + /** + * Object type. + * + * For Models, this is always `"model"`. + */ + "type": S.Literal("model").pipe(S.propertySignature, S.withConstructorDefault(() => "model" as const)) +}) {} + +export class BetaListResponseModelInfo extends S.Class("BetaListResponseModelInfo")({ + "data": S.Array(BetaModelInfo), + /** + * First ID in the `data` list. Can be used as the `before_id` for the previous page. + */ + "first_id": S.NullOr(S.String), + /** + * Indicates if there are more results in the requested page direction. + */ + "has_more": S.Boolean, + /** + * Last ID in the `data` list. Can be used as the `after_id` for the next page. + */ + "last_id": S.NullOr(S.String) +}) {} + +export class BetaModelsGetParams extends S.Struct({ + /** + * The version of the Claude API you want to use. + * + * Read more about versioning and our version history [here](https://docs.claude.com/en/api/versioning). + */ + "anthropic-version": S.optionalWith(S.String, { nullable: true }), + /** + * Your unique API key for authentication. + * + * This key is required in the header of all API requests, to authenticate your account and access Anthropic's services. Get your API key through the [Console](https://console.anthropic.com/settings/keys). Each key is scoped to a Workspace. + */ + "x-api-key": S.optionalWith(S.String, { nullable: true }), + /** + * Optional header to specify the beta version(s) you want to use. + * + * To use multiple betas, use a comma separated list like `beta1,beta2` or specify the header multiple times for each beta. + */ + "anthropic-beta": S.optionalWith(S.String, { nullable: true }) +}) {} + +export class BetaMessageBatchesListParams extends S.Struct({ + /** + * ID of the object to use as a cursor for pagination. When provided, returns the page of results immediately before this object. + */ + "before_id": S.optionalWith(S.String, { nullable: true }), + /** + * ID of the object to use as a cursor for pagination. When provided, returns the page of results immediately after this object. + */ + "after_id": S.optionalWith(S.String, { nullable: true }), + /** + * Number of items to return per page. + * + * Defaults to `20`. Ranges from `1` to `1000`. + */ + "limit": S.optionalWith(S.Int.pipe(S.greaterThanOrEqualTo(1), S.lessThanOrEqualTo(1000)), { + nullable: true, + default: () => 20 as const + }), + /** + * Optional header to specify the beta version(s) you want to use. + * + * To use multiple betas, use a comma separated list like `beta1,beta2` or specify the header multiple times for each beta. + */ + "anthropic-beta": S.optionalWith(S.String, { nullable: true }), + /** + * The version of the Claude API you want to use. + * + * Read more about versioning and our version history [here](https://docs.claude.com/en/api/versioning). + */ + "anthropic-version": S.optionalWith(S.String, { nullable: true }), + /** + * Your unique API key for authentication. + * + * This key is required in the header of all API requests, to authenticate your account and access Anthropic's services. Get your API key through the [Console](https://console.anthropic.com/settings/keys). Each key is scoped to a Workspace. + */ + "x-api-key": S.optionalWith(S.String, { nullable: true }) +}) {} + +/** + * Processing status of the Message Batch. + */ +export class BetaMessageBatchProcessingStatus extends S.Literal("in_progress", "canceling", "ended") {} + +export class BetaRequestCounts extends S.Class("BetaRequestCounts")({ + /** + * Number of requests in the Message Batch that have been canceled. + * + * This is zero until processing of the entire Message Batch has ended. + */ + "canceled": S.Int.pipe(S.propertySignature, S.withConstructorDefault(() => 0 as const)), + /** + * Number of requests in the Message Batch that encountered an error. + * + * This is zero until processing of the entire Message Batch has ended. + */ + "errored": S.Int.pipe(S.propertySignature, S.withConstructorDefault(() => 0 as const)), + /** + * Number of requests in the Message Batch that have expired. + * + * This is zero until processing of the entire Message Batch has ended. + */ + "expired": S.Int.pipe(S.propertySignature, S.withConstructorDefault(() => 0 as const)), + /** + * Number of requests in the Message Batch that are processing. + */ + "processing": S.Int.pipe(S.propertySignature, S.withConstructorDefault(() => 0 as const)), + /** + * Number of requests in the Message Batch that have completed successfully. + * + * This is zero until processing of the entire Message Batch has ended. + */ + "succeeded": S.Int.pipe(S.propertySignature, S.withConstructorDefault(() => 0 as const)) +}) {} + +export class BetaMessageBatch extends S.Class("BetaMessageBatch")({ + /** + * RFC 3339 datetime string representing the time at which the Message Batch was archived and its results became unavailable. + */ + "archived_at": S.NullOr(S.String), + /** + * RFC 3339 datetime string representing the time at which cancellation was initiated for the Message Batch. Specified only if cancellation was initiated. + */ + "cancel_initiated_at": S.NullOr(S.String), + /** + * RFC 3339 datetime string representing the time at which the Message Batch was created. + */ + "created_at": S.String, + /** + * RFC 3339 datetime string representing the time at which processing for the Message Batch ended. Specified only once processing ends. + * + * Processing ends when every request in a Message Batch has either succeeded, errored, canceled, or expired. + */ + "ended_at": S.NullOr(S.String), + /** + * RFC 3339 datetime string representing the time at which the Message Batch will expire and end processing, which is 24 hours after creation. + */ + "expires_at": S.String, + /** + * Unique object identifier. + * + * The format and length of IDs may change over time. + */ + "id": S.String, + /** + * Processing status of the Message Batch. + */ + "processing_status": BetaMessageBatchProcessingStatus, + /** + * Tallies requests within the Message Batch, categorized by their status. + * + * Requests start as `processing` and move to one of the other statuses only once processing of the entire batch ends. The sum of all values always matches the total number of requests in the batch. + */ + "request_counts": BetaRequestCounts, + /** + * URL to a `.jsonl` file containing the results of the Message Batch requests. Specified only once processing ends. + * + * Results in the file are not guaranteed to be in the same order as requests. Use the `custom_id` field to match results to requests. + */ + "results_url": S.NullOr(S.String), + /** + * Object type. + * + * For Message Batches, this is always `"message_batch"`. + */ + "type": S.Literal("message_batch").pipe(S.propertySignature, S.withConstructorDefault(() => "message_batch" as const)) +}) {} + +export class BetaListResponseMessageBatch + extends S.Class("BetaListResponseMessageBatch")({ + "data": S.Array(BetaMessageBatch), + /** + * First ID in the `data` list. Can be used as the `before_id` for the previous page. + */ + "first_id": S.NullOr(S.String), + /** + * Indicates if there are more results in the requested page direction. + */ + "has_more": S.Boolean, + /** + * Last ID in the `data` list. Can be used as the `after_id` for the next page. + */ + "last_id": S.NullOr(S.String) + }) +{} + +export class BetaMessageBatchesPostParams extends S.Struct({ + /** + * Optional header to specify the beta version(s) you want to use. + * + * To use multiple betas, use a comma separated list like `beta1,beta2` or specify the header multiple times for each beta. + */ + "anthropic-beta": S.optionalWith(S.String, { nullable: true }), + /** + * The version of the Claude API you want to use. + * + * Read more about versioning and our version history [here](https://docs.claude.com/en/api/versioning). + */ + "anthropic-version": S.optionalWith(S.String, { nullable: true }) +}) {} + +export class BetaMessageBatchIndividualRequestParams + extends S.Class("BetaMessageBatchIndividualRequestParams")({ + /** + * Developer-provided ID created for each request in a Message Batch. Useful for matching results to requests, as results may be given out of request order. + * + * Must be unique for each request within the Message Batch. + */ + "custom_id": S.String.pipe(S.minLength(1), S.maxLength(64), S.pattern(new RegExp("^[a-zA-Z0-9_-]{1,64}$"))), + /** + * Messages API creation parameters for the individual request. + * + * See the [Messages API reference](/en/api/messages) for full documentation on available parameters. + */ + "params": BetaCreateMessageParams + }) +{} + +export class BetaCreateMessageBatchParams + extends S.Class("BetaCreateMessageBatchParams")({ + /** + * List of requests for prompt completion. Each is an individual request to create a Message. + */ + "requests": S.NonEmptyArray(BetaMessageBatchIndividualRequestParams).pipe(S.minItems(1), S.maxItems(10000)) + }) +{} + +export class BetaMessageBatchesRetrieveParams extends S.Struct({ + /** + * Optional header to specify the beta version(s) you want to use. + * + * To use multiple betas, use a comma separated list like `beta1,beta2` or specify the header multiple times for each beta. + */ + "anthropic-beta": S.optionalWith(S.String, { nullable: true }), + /** + * The version of the Claude API you want to use. + * + * Read more about versioning and our version history [here](https://docs.claude.com/en/api/versioning). + */ + "anthropic-version": S.optionalWith(S.String, { nullable: true }), + /** + * Your unique API key for authentication. + * + * This key is required in the header of all API requests, to authenticate your account and access Anthropic's services. Get your API key through the [Console](https://console.anthropic.com/settings/keys). Each key is scoped to a Workspace. + */ + "x-api-key": S.optionalWith(S.String, { nullable: true }) +}) {} + +export class BetaMessageBatchesDeleteParams extends S.Struct({ + /** + * Optional header to specify the beta version(s) you want to use. + * + * To use multiple betas, use a comma separated list like `beta1,beta2` or specify the header multiple times for each beta. + */ + "anthropic-beta": S.optionalWith(S.String, { nullable: true }), + /** + * The version of the Claude API you want to use. + * + * Read more about versioning and our version history [here](https://docs.claude.com/en/api/versioning). + */ + "anthropic-version": S.optionalWith(S.String, { nullable: true }), + /** + * Your unique API key for authentication. + * + * This key is required in the header of all API requests, to authenticate your account and access Anthropic's services. Get your API key through the [Console](https://console.anthropic.com/settings/keys). Each key is scoped to a Workspace. + */ + "x-api-key": S.optionalWith(S.String, { nullable: true }) +}) {} + +export class BetaDeleteMessageBatchResponse + extends S.Class("BetaDeleteMessageBatchResponse")({ + /** + * ID of the Message Batch. + */ + "id": S.String, + /** + * Deleted object type. + * + * For Message Batches, this is always `"message_batch_deleted"`. + */ + "type": S.Literal("message_batch_deleted").pipe( + S.propertySignature, + S.withConstructorDefault(() => "message_batch_deleted" as const) + ) + }) +{} + +export class BetaMessageBatchesCancelParams extends S.Struct({ + /** + * Optional header to specify the beta version(s) you want to use. + * + * To use multiple betas, use a comma separated list like `beta1,beta2` or specify the header multiple times for each beta. + */ + "anthropic-beta": S.optionalWith(S.String, { nullable: true }), + /** + * The version of the Claude API you want to use. + * + * Read more about versioning and our version history [here](https://docs.claude.com/en/api/versioning). + */ + "anthropic-version": S.optionalWith(S.String, { nullable: true }) +}) {} + +export class BetaMessageBatchesResultsParams extends S.Struct({ + /** + * Optional header to specify the beta version(s) you want to use. + * + * To use multiple betas, use a comma separated list like `beta1,beta2` or specify the header multiple times for each beta. + */ + "anthropic-beta": S.optionalWith(S.String, { nullable: true }), + /** + * The version of the Claude API you want to use. + * + * Read more about versioning and our version history [here](https://docs.claude.com/en/api/versioning). + */ + "anthropic-version": S.optionalWith(S.String, { nullable: true }), + /** + * Your unique API key for authentication. + * + * This key is required in the header of all API requests, to authenticate your account and access Anthropic's services. Get your API key through the [Console](https://console.anthropic.com/settings/keys). Each key is scoped to a Workspace. + */ + "x-api-key": S.optionalWith(S.String, { nullable: true }) +}) {} + +export class BetaMessagesCountTokensPostParams extends S.Struct({ + /** + * Optional header to specify the beta version(s) you want to use. + * + * To use multiple betas, use a comma separated list like `beta1,beta2` or specify the header multiple times for each beta. + */ + "anthropic-beta": S.optionalWith(S.String, { nullable: true }), + /** + * The version of the Claude API you want to use. + * + * Read more about versioning and our version history [here](https://docs.claude.com/en/api/versioning). + */ + "anthropic-version": S.optionalWith(S.String, { nullable: true }) +}) {} + +export class BetaCountMessageTokensParams + extends S.Class("BetaCountMessageTokensParams")({ + /** + * Context management configuration. + * + * This allows you to control how Claude manages context across multiple requests, such as whether to clear function results or not. + */ + "context_management": S.optionalWith(BetaContextManagementConfig, { nullable: true }), + /** + * MCP servers to be utilized in this request + */ + "mcp_servers": S.optionalWith(S.Array(BetaRequestMCPServerURLDefinition).pipe(S.maxItems(20)), { nullable: true }), + /** + * Input messages. + * + * Our models are trained to operate on alternating `user` and `assistant` conversational turns. When creating a new `Message`, you specify the prior conversational turns with the `messages` parameter, and the model then generates the next `Message` in the conversation. Consecutive `user` or `assistant` turns in your request will be combined into a single turn. + * + * Each input message must be an object with a `role` and `content`. You can specify a single `user`-role message, or you can include multiple `user` and `assistant` messages. + * + * If the final message uses the `assistant` role, the response content will continue immediately from the content in that message. This can be used to constrain part of the model's response. + * + * Example with a single `user` message: + * + * ```json + * [{"role": "user", "content": "Hello, Claude"}] + * ``` + * + * Example with multiple conversational turns: + * + * ```json + * [ + * {"role": "user", "content": "Hello there."}, + * {"role": "assistant", "content": "Hi, I'm Claude. How can I help you?"}, + * {"role": "user", "content": "Can you explain LLMs in plain English?"}, + * ] + * ``` + * + * Example with a partially-filled response from Claude: + * + * ```json + * [ + * {"role": "user", "content": "What's the Greek name for Sun? (A) Sol (B) Helios (C) Sun"}, + * {"role": "assistant", "content": "The best answer is ("}, + * ] + * ``` + * + * Each input message `content` may be either a single `string` or an array of content blocks, where each block has a specific `type`. Using a `string` for `content` is shorthand for an array of one content block of type `"text"`. The following input messages are equivalent: + * + * ```json + * {"role": "user", "content": "Hello, Claude"} + * ``` + * + * ```json + * {"role": "user", "content": [{"type": "text", "text": "Hello, Claude"}]} + * ``` + * + * See [input examples](https://docs.claude.com/en/api/messages-examples). + * + * Note that if you want to include a [system prompt](https://docs.claude.com/en/docs/system-prompts), you can use the top-level `system` parameter — there is no `"system"` role for input messages in the Messages API. + * + * There is a limit of 100,000 messages in a single request. + */ + "messages": S.Array(BetaInputMessage), + "model": S.Union(S.String, Model), + /** + * System prompt. + * + * A system prompt is a way of providing context and instructions to Claude, such as specifying a particular goal or role. See our [guide to system prompts](https://docs.claude.com/en/docs/system-prompts). + */ + "system": S.optionalWith(S.Union(S.String, S.Array(BetaRequestTextBlock)), { nullable: true }), + "thinking": S.optionalWith(BetaThinkingConfigParam, { nullable: true }), + "tool_choice": S.optionalWith(BetaToolChoice, { nullable: true }), + /** + * Definitions of tools that the model may use. + * + * If you include `tools` in your API request, the model may return `tool_use` content blocks that represent the model's use of those tools. You can then run those tools using the tool input generated by the model and then optionally return results back to the model using `tool_result` content blocks. + * + * There are two types of tools: **client tools** and **server tools**. The behavior described below applies to client tools. For [server tools](https://docs.claude.com/en/docs/agents-and-tools/tool-use/overview\#server-tools), see their individual documentation as each has its own behavior (e.g., the [web search tool](https://docs.claude.com/en/docs/agents-and-tools/tool-use/web-search-tool)). + * + * Each tool definition includes: + * + * * `name`: Name of the tool. + * * `description`: Optional, but strongly-recommended description of the tool. + * * `input_schema`: [JSON schema](https://json-schema.org/draft/2020-12) for the tool `input` shape that the model will produce in `tool_use` output content blocks. + * + * For example, if you defined `tools` as: + * + * ```json + * [ + * { + * "name": "get_stock_price", + * "description": "Get the current stock price for a given ticker symbol.", + * "input_schema": { + * "type": "object", + * "properties": { + * "ticker": { + * "type": "string", + * "description": "The stock ticker symbol, e.g. AAPL for Apple Inc." + * } + * }, + * "required": ["ticker"] + * } + * } + * ] + * ``` + * + * And then asked the model "What's the S&P 500 at today?", the model might produce `tool_use` content blocks in the response like this: + * + * ```json + * [ + * { + * "type": "tool_use", + * "id": "toolu_01D7FLrfh4GYq7yT1ULFeyMV", + * "name": "get_stock_price", + * "input": { "ticker": "^GSPC" } + * } + * ] + * ``` + * + * You might then run your `get_stock_price` tool with `{"ticker": "^GSPC"}` as an input, and return the following back to the model in a subsequent `user` message: + * + * ```json + * [ + * { + * "type": "tool_result", + * "tool_use_id": "toolu_01D7FLrfh4GYq7yT1ULFeyMV", + * "content": "259.75 USD" + * } + * ] + * ``` + * + * Tools can be used for workflows that include running client-side tools and functions, or more generally whenever you want the model to produce a particular JSON structure of output. + * + * See our [guide](https://docs.claude.com/en/docs/tool-use) for more details. + */ + "tools": S.optionalWith( + S.Array( + S.Union( + BetaTool, + BetaBashTool20241022, + BetaBashTool20250124, + BetaCodeExecutionTool20250522, + BetaCodeExecutionTool20250825, + BetaComputerUseTool20241022, + BetaMemoryTool20250818, + BetaComputerUseTool20250124, + BetaTextEditor20241022, + BetaTextEditor20250124, + BetaTextEditor20250429, + BetaTextEditor20250728, + BetaWebSearchTool20250305, + BetaWebFetchTool20250910 + ) + ), + { nullable: true } + ) + }) +{} + +export class BetaContextManagementResponse + extends S.Class("BetaContextManagementResponse")({ + /** + * The original token count before context management was applied + */ + "original_input_tokens": S.Int + }) +{} + +export class BetaCountMessageTokensResponse + extends S.Class("BetaCountMessageTokensResponse")({ + /** + * Information about context management applied to the message. + */ + "context_management": S.NullOr(BetaContextManagementResponse), + /** + * The total number of tokens across the provided list of messages, system prompt, and tools. + */ + "input_tokens": S.Int + }) +{} + +export class BetaListFilesV1FilesGetParams extends S.Struct({ + /** + * ID of the object to use as a cursor for pagination. When provided, returns the page of results immediately before this object. + */ + "before_id": S.optionalWith(S.String, { nullable: true }), + /** + * ID of the object to use as a cursor for pagination. When provided, returns the page of results immediately after this object. + */ + "after_id": S.optionalWith(S.String, { nullable: true }), + /** + * Number of items to return per page. + * + * Defaults to `20`. Ranges from `1` to `1000`. + */ + "limit": S.optionalWith(S.Int.pipe(S.greaterThanOrEqualTo(1), S.lessThanOrEqualTo(1000)), { + nullable: true, + default: () => 20 as const + }), + /** + * Optional header to specify the beta version(s) you want to use. + * + * To use multiple betas, use a comma separated list like `beta1,beta2` or specify the header multiple times for each beta. + */ + "anthropic-beta": S.optionalWith(S.String, { nullable: true }), + /** + * The version of the Claude API you want to use. + * + * Read more about versioning and our version history [here](https://docs.claude.com/en/api/versioning). + */ + "anthropic-version": S.optionalWith(S.String, { nullable: true }), + /** + * Your unique API key for authentication. + * + * This key is required in the header of all API requests, to authenticate your account and access Anthropic's services. Get your API key through the [Console](https://console.anthropic.com/settings/keys). Each key is scoped to a Workspace. + */ + "x-api-key": S.optionalWith(S.String, { nullable: true }) +}) {} + +export class BetaFileMetadataSchema extends S.Class("BetaFileMetadataSchema")({ + /** + * RFC 3339 datetime string representing when the file was created. + */ + "created_at": S.String, + /** + * Whether the file can be downloaded. + */ + "downloadable": S.optionalWith(S.Boolean, { nullable: true, default: () => false as const }), + /** + * Original filename of the uploaded file. + */ + "filename": S.String.pipe(S.minLength(1), S.maxLength(500)), + /** + * Unique object identifier. + * + * The format and length of IDs may change over time. + */ + "id": S.String, + /** + * MIME type of the file. + */ + "mime_type": S.String.pipe(S.minLength(1), S.maxLength(255)), + /** + * Size of the file in bytes. + */ + "size_bytes": S.Int.pipe(S.greaterThanOrEqualTo(0)), + /** + * Object type. + * + * For files, this is always `"file"`. + */ + "type": S.Literal("file") +}) {} + +export class BetaFileListResponse extends S.Class("BetaFileListResponse")({ + /** + * List of file metadata objects. + */ + "data": S.Array(BetaFileMetadataSchema), + /** + * ID of the first file in this page of results. + */ + "first_id": S.optionalWith(S.String, { nullable: true }), + /** + * Whether there are more results available. + */ + "has_more": S.optionalWith(S.Boolean, { nullable: true, default: () => false as const }), + /** + * ID of the last file in this page of results. + */ + "last_id": S.optionalWith(S.String, { nullable: true }) +}) {} + +export class BetaUploadFileV1FilesPostParams extends S.Struct({ + /** + * Optional header to specify the beta version(s) you want to use. + * + * To use multiple betas, use a comma separated list like `beta1,beta2` or specify the header multiple times for each beta. + */ + "anthropic-beta": S.optionalWith(S.String, { nullable: true }), + /** + * The version of the Claude API you want to use. + * + * Read more about versioning and our version history [here](https://docs.claude.com/en/api/versioning). + */ + "anthropic-version": S.optionalWith(S.String, { nullable: true }) +}) {} + +export class BetaUploadFileV1FilesPostRequest + extends S.Class("BetaUploadFileV1FilesPostRequest")({ + /** + * The file to upload + */ + "file": S.instanceOf(globalThis.Blob) + }) +{} + +export class BetaGetFileMetadataV1FilesFileIdGetParams extends S.Struct({ + /** + * Optional header to specify the beta version(s) you want to use. + * + * To use multiple betas, use a comma separated list like `beta1,beta2` or specify the header multiple times for each beta. + */ + "anthropic-beta": S.optionalWith(S.String, { nullable: true }), + /** + * The version of the Claude API you want to use. + * + * Read more about versioning and our version history [here](https://docs.claude.com/en/api/versioning). + */ + "anthropic-version": S.optionalWith(S.String, { nullable: true }), + /** + * Your unique API key for authentication. + * + * This key is required in the header of all API requests, to authenticate your account and access Anthropic's services. Get your API key through the [Console](https://console.anthropic.com/settings/keys). Each key is scoped to a Workspace. + */ + "x-api-key": S.optionalWith(S.String, { nullable: true }) +}) {} + +export class BetaDeleteFileV1FilesFileIdDeleteParams extends S.Struct({ + /** + * Optional header to specify the beta version(s) you want to use. + * + * To use multiple betas, use a comma separated list like `beta1,beta2` or specify the header multiple times for each beta. + */ + "anthropic-beta": S.optionalWith(S.String, { nullable: true }), + /** + * The version of the Claude API you want to use. + * + * Read more about versioning and our version history [here](https://docs.claude.com/en/api/versioning). + */ + "anthropic-version": S.optionalWith(S.String, { nullable: true }), + /** + * Your unique API key for authentication. + * + * This key is required in the header of all API requests, to authenticate your account and access Anthropic's services. Get your API key through the [Console](https://console.anthropic.com/settings/keys). Each key is scoped to a Workspace. + */ + "x-api-key": S.optionalWith(S.String, { nullable: true }) +}) {} + +export class BetaFileDeleteResponse extends S.Class("BetaFileDeleteResponse")({ + /** + * ID of the deleted file. + */ + "id": S.String, + /** + * Deleted object type. + * + * For file deletion, this is always `"file_deleted"`. + */ + "type": S.optionalWith(S.Literal("file_deleted"), { nullable: true, default: () => "file_deleted" as const }) +}) {} + +export class BetaDownloadFileV1FilesFileIdContentGetParams extends S.Struct({ + /** + * Optional header to specify the beta version(s) you want to use. + * + * To use multiple betas, use a comma separated list like `beta1,beta2` or specify the header multiple times for each beta. + */ + "anthropic-beta": S.optionalWith(S.String, { nullable: true }), + /** + * The version of the Claude API you want to use. + * + * Read more about versioning and our version history [here](https://docs.claude.com/en/api/versioning). + */ + "anthropic-version": S.optionalWith(S.String, { nullable: true }), + /** + * Your unique API key for authentication. + * + * This key is required in the header of all API requests, to authenticate your account and access Anthropic's services. Get your API key through the [Console](https://console.anthropic.com/settings/keys). Each key is scoped to a Workspace. + */ + "x-api-key": S.optionalWith(S.String, { nullable: true }) +}) {} + +export class BetaListSkillsV1SkillsGetParams extends S.Struct({ + /** + * Pagination token for fetching a specific page of results. + * + * Pass the value from a previous response's `next_page` field to get the next page of results. + */ + "page": S.optionalWith(S.String, { nullable: true }), + /** + * Number of results to return per page. + * + * Maximum value is 100. Defaults to 20. + */ + "limit": S.optionalWith(S.Int, { nullable: true, default: () => 20 as const }), + /** + * Filter skills by source. + * + * If provided, only skills from the specified source will be returned: + * * `"custom"`: only return user-created skills + * * `"anthropic"`: only return Anthropic-created skills + */ + "source": S.optionalWith(S.String, { nullable: true }), + /** + * Optional header to specify the beta version(s) you want to use. + * + * To use multiple betas, use a comma separated list like `beta1,beta2` or specify the header multiple times for each beta. + */ + "anthropic-beta": S.optionalWith(S.String, { nullable: true }), + /** + * The version of the Claude API you want to use. + * + * Read more about versioning and our version history [here](https://docs.claude.com/en/api/versioning). + */ + "anthropic-version": S.optionalWith(S.String, { nullable: true }), + /** + * Your unique API key for authentication. + * + * This key is required in the header of all API requests, to authenticate your account and access Anthropic's services. Get your API key through the [Console](https://console.anthropic.com/settings/keys). Each key is scoped to a Workspace. + */ + "x-api-key": S.optionalWith(S.String, { nullable: true }) +}) {} + +export class BetaapiSchemasSkillsSkill extends S.Class("BetaapiSchemasSkillsSkill")({ + /** + * ISO 8601 timestamp of when the skill was created. + */ + "created_at": S.String, + /** + * Display title for the skill. + * + * This is a human-readable label that is not included in the prompt sent to the model. + */ + "display_title": S.NullOr(S.String), + /** + * Unique identifier for the skill. + * + * The format and length of IDs may change over time. + */ + "id": S.String, + /** + * The latest version identifier for the skill. + * + * This represents the most recent version of the skill that has been created. + */ + "latest_version": S.NullOr(S.String), + /** + * Source of the skill. + * + * This may be one of the following values: + * * `"custom"`: the skill was created by a user + * * `"anthropic"`: the skill was created by Anthropic + */ + "source": S.String, + /** + * Object type. + * + * For Skills, this is always `"skill"`. + */ + "type": S.String.pipe(S.propertySignature, S.withConstructorDefault(() => "skill" as const)), + /** + * ISO 8601 timestamp of when the skill was last updated. + */ + "updated_at": S.String +}) {} + +export class BetaListSkillsResponse extends S.Class("BetaListSkillsResponse")({ + /** + * List of skills. + */ + "data": S.Array(BetaapiSchemasSkillsSkill), + /** + * Whether there are more results available. + * + * If `true`, there are additional results that can be fetched using the `next_page` token. + */ + "has_more": S.Boolean, + /** + * Token for fetching the next page of results. + * + * If `null`, there are no more results available. Pass this value to the `page_token` parameter in the next request to get the next page. + */ + "next_page": S.NullOr(S.String) +}) {} + +export class BetaCreateSkillV1SkillsPostParams extends S.Struct({ + /** + * Optional header to specify the beta version(s) you want to use. + * + * To use multiple betas, use a comma separated list like `beta1,beta2` or specify the header multiple times for each beta. + */ + "anthropic-beta": S.optionalWith(S.String, { nullable: true }), + /** + * The version of the Claude API you want to use. + * + * Read more about versioning and our version history [here](https://docs.claude.com/en/api/versioning). + */ + "anthropic-version": S.optionalWith(S.String, { nullable: true }) +}) {} + +export class BetaBodyCreateSkillV1SkillsPost + extends S.Class("BetaBodyCreateSkillV1SkillsPost")({ + /** + * Display title for the skill. + * + * This is a human-readable label that is not included in the prompt sent to the model. + */ + "display_title": S.optionalWith(S.String, { nullable: true }), + /** + * Files to upload for the skill. + * + * All files must be in the same top-level directory and must include a SKILL.md file at the root of that directory. + */ + "files": S.optionalWith(S.Array(S.instanceOf(globalThis.Blob)), { nullable: true }) + }) +{} + +export class BetaCreateSkillResponse extends S.Class("BetaCreateSkillResponse")({ + /** + * ISO 8601 timestamp of when the skill was created. + */ + "created_at": S.String, + /** + * Display title for the skill. + * + * This is a human-readable label that is not included in the prompt sent to the model. + */ + "display_title": S.NullOr(S.String), + /** + * Unique identifier for the skill. + * + * The format and length of IDs may change over time. + */ + "id": S.String, + /** + * The latest version identifier for the skill. + * + * This represents the most recent version of the skill that has been created. + */ + "latest_version": S.NullOr(S.String), + /** + * Source of the skill. + * + * This may be one of the following values: + * * `"custom"`: the skill was created by a user + * * `"anthropic"`: the skill was created by Anthropic + */ + "source": S.String, + /** + * Object type. + * + * For Skills, this is always `"skill"`. + */ + "type": S.String.pipe(S.propertySignature, S.withConstructorDefault(() => "skill" as const)), + /** + * ISO 8601 timestamp of when the skill was last updated. + */ + "updated_at": S.String +}) {} + +export class BetaGetSkillV1SkillsSkillIdGetParams extends S.Struct({ + /** + * Optional header to specify the beta version(s) you want to use. + * + * To use multiple betas, use a comma separated list like `beta1,beta2` or specify the header multiple times for each beta. + */ + "anthropic-beta": S.optionalWith(S.String, { nullable: true }), + /** + * The version of the Claude API you want to use. + * + * Read more about versioning and our version history [here](https://docs.claude.com/en/api/versioning). + */ + "anthropic-version": S.optionalWith(S.String, { nullable: true }), + /** + * Your unique API key for authentication. + * + * This key is required in the header of all API requests, to authenticate your account and access Anthropic's services. Get your API key through the [Console](https://console.anthropic.com/settings/keys). Each key is scoped to a Workspace. + */ + "x-api-key": S.optionalWith(S.String, { nullable: true }) +}) {} + +export class BetaGetSkillResponse extends S.Class("BetaGetSkillResponse")({ + /** + * ISO 8601 timestamp of when the skill was created. + */ + "created_at": S.String, + /** + * Display title for the skill. + * + * This is a human-readable label that is not included in the prompt sent to the model. + */ + "display_title": S.NullOr(S.String), + /** + * Unique identifier for the skill. + * + * The format and length of IDs may change over time. + */ + "id": S.String, + /** + * The latest version identifier for the skill. + * + * This represents the most recent version of the skill that has been created. + */ + "latest_version": S.NullOr(S.String), + /** + * Source of the skill. + * + * This may be one of the following values: + * * `"custom"`: the skill was created by a user + * * `"anthropic"`: the skill was created by Anthropic + */ + "source": S.String, + /** + * Object type. + * + * For Skills, this is always `"skill"`. + */ + "type": S.String.pipe(S.propertySignature, S.withConstructorDefault(() => "skill" as const)), + /** + * ISO 8601 timestamp of when the skill was last updated. + */ + "updated_at": S.String +}) {} + +export class BetaDeleteSkillV1SkillsSkillIdDeleteParams extends S.Struct({ + /** + * Optional header to specify the beta version(s) you want to use. + * + * To use multiple betas, use a comma separated list like `beta1,beta2` or specify the header multiple times for each beta. + */ + "anthropic-beta": S.optionalWith(S.String, { nullable: true }), + /** + * The version of the Claude API you want to use. + * + * Read more about versioning and our version history [here](https://docs.claude.com/en/api/versioning). + */ + "anthropic-version": S.optionalWith(S.String, { nullable: true }), + /** + * Your unique API key for authentication. + * + * This key is required in the header of all API requests, to authenticate your account and access Anthropic's services. Get your API key through the [Console](https://console.anthropic.com/settings/keys). Each key is scoped to a Workspace. + */ + "x-api-key": S.optionalWith(S.String, { nullable: true }) +}) {} + +export class BetaDeleteSkillResponse extends S.Class("BetaDeleteSkillResponse")({ + /** + * Unique identifier for the skill. + * + * The format and length of IDs may change over time. + */ + "id": S.String, + /** + * Deleted object type. + * + * For Skills, this is always `"skill_deleted"`. + */ + "type": S.String.pipe(S.propertySignature, S.withConstructorDefault(() => "skill_deleted" as const)) +}) {} + +export class BetaListSkillVersionsV1SkillsSkillIdVersionsGetParams extends S.Struct({ + /** + * Optionally set to the `next_page` token from the previous response. + */ + "page": S.optionalWith(S.String, { nullable: true }), + /** + * Number of items to return per page. + * + * Defaults to `20`. Ranges from `1` to `1000`. + */ + "limit": S.optionalWith(S.Int, { nullable: true }), + /** + * Optional header to specify the beta version(s) you want to use. + * + * To use multiple betas, use a comma separated list like `beta1,beta2` or specify the header multiple times for each beta. + */ + "anthropic-beta": S.optionalWith(S.String, { nullable: true }), + /** + * The version of the Claude API you want to use. + * + * Read more about versioning and our version history [here](https://docs.claude.com/en/api/versioning). + */ + "anthropic-version": S.optionalWith(S.String, { nullable: true }), + /** + * Your unique API key for authentication. + * + * This key is required in the header of all API requests, to authenticate your account and access Anthropic's services. Get your API key through the [Console](https://console.anthropic.com/settings/keys). Each key is scoped to a Workspace. + */ + "x-api-key": S.optionalWith(S.String, { nullable: true }) +}) {} + +export class BetaSkillVersion extends S.Class("BetaSkillVersion")({ + /** + * ISO 8601 timestamp of when the skill version was created. + */ + "created_at": S.String, + /** + * Description of the skill version. + * + * This is extracted from the SKILL.md file in the skill upload. + */ + "description": S.String, + /** + * Directory name of the skill version. + * + * This is the top-level directory name that was extracted from the uploaded files. + */ + "directory": S.String, + /** + * Unique identifier for the skill version. + * + * The format and length of IDs may change over time. + */ + "id": S.String, + /** + * Human-readable name of the skill version. + * + * This is extracted from the SKILL.md file in the skill upload. + */ + "name": S.String, + /** + * Identifier for the skill that this version belongs to. + */ + "skill_id": S.String, + /** + * Object type. + * + * For Skill Versions, this is always `"skill_version"`. + */ + "type": S.String.pipe(S.propertySignature, S.withConstructorDefault(() => "skill_version" as const)), + /** + * Version identifier for the skill. + * + * Each version is identified by a Unix epoch timestamp (e.g., "1759178010641129"). + */ + "version": S.String +}) {} + +export class BetaListSkillVersionsResponse + extends S.Class("BetaListSkillVersionsResponse")({ + /** + * List of skill versions. + */ + "data": S.Array(BetaSkillVersion), + /** + * Indicates if there are more results in the requested page direction. + */ + "has_more": S.Boolean, + /** + * Token to provide in as `page` in the subsequent request to retrieve the next page of data. + */ + "next_page": S.NullOr(S.String) + }) +{} + +export class BetaCreateSkillVersionV1SkillsSkillIdVersionsPostParams extends S.Struct({ + /** + * Optional header to specify the beta version(s) you want to use. + * + * To use multiple betas, use a comma separated list like `beta1,beta2` or specify the header multiple times for each beta. + */ + "anthropic-beta": S.optionalWith(S.String, { nullable: true }), + /** + * The version of the Claude API you want to use. + * + * Read more about versioning and our version history [here](https://docs.claude.com/en/api/versioning). + */ + "anthropic-version": S.optionalWith(S.String, { nullable: true }) +}) {} + +export class BetaBodyCreateSkillVersionV1SkillsSkillIdVersionsPost + extends S.Class( + "BetaBodyCreateSkillVersionV1SkillsSkillIdVersionsPost" + )({ + /** + * Files to upload for the skill. + * + * All files must be in the same top-level directory and must include a SKILL.md file at the root of that directory. + */ + "files": S.optionalWith(S.Array(S.instanceOf(globalThis.Blob)), { nullable: true }) + }) +{} + +export class BetaCreateSkillVersionResponse + extends S.Class("BetaCreateSkillVersionResponse")({ + /** + * ISO 8601 timestamp of when the skill version was created. + */ + "created_at": S.String, + /** + * Description of the skill version. + * + * This is extracted from the SKILL.md file in the skill upload. + */ + "description": S.String, + /** + * Directory name of the skill version. + * + * This is the top-level directory name that was extracted from the uploaded files. + */ + "directory": S.String, + /** + * Unique identifier for the skill version. + * + * The format and length of IDs may change over time. + */ + "id": S.String, + /** + * Human-readable name of the skill version. + * + * This is extracted from the SKILL.md file in the skill upload. + */ + "name": S.String, + /** + * Identifier for the skill that this version belongs to. + */ + "skill_id": S.String, + /** + * Object type. + * + * For Skill Versions, this is always `"skill_version"`. + */ + "type": S.String.pipe(S.propertySignature, S.withConstructorDefault(() => "skill_version" as const)), + /** + * Version identifier for the skill. + * + * Each version is identified by a Unix epoch timestamp (e.g., "1759178010641129"). + */ + "version": S.String + }) +{} + +export class BetaGetSkillVersionV1SkillsSkillIdVersionsVersionGetParams extends S.Struct({ + /** + * Optional header to specify the beta version(s) you want to use. + * + * To use multiple betas, use a comma separated list like `beta1,beta2` or specify the header multiple times for each beta. + */ + "anthropic-beta": S.optionalWith(S.String, { nullable: true }), + /** + * The version of the Claude API you want to use. + * + * Read more about versioning and our version history [here](https://docs.claude.com/en/api/versioning). + */ + "anthropic-version": S.optionalWith(S.String, { nullable: true }), + /** + * Your unique API key for authentication. + * + * This key is required in the header of all API requests, to authenticate your account and access Anthropic's services. Get your API key through the [Console](https://console.anthropic.com/settings/keys). Each key is scoped to a Workspace. + */ + "x-api-key": S.optionalWith(S.String, { nullable: true }) +}) {} + +export class BetaGetSkillVersionResponse extends S.Class("BetaGetSkillVersionResponse")({ + /** + * ISO 8601 timestamp of when the skill version was created. + */ + "created_at": S.String, + /** + * Description of the skill version. + * + * This is extracted from the SKILL.md file in the skill upload. + */ + "description": S.String, + /** + * Directory name of the skill version. + * + * This is the top-level directory name that was extracted from the uploaded files. + */ + "directory": S.String, + /** + * Unique identifier for the skill version. + * + * The format and length of IDs may change over time. + */ + "id": S.String, + /** + * Human-readable name of the skill version. + * + * This is extracted from the SKILL.md file in the skill upload. + */ + "name": S.String, + /** + * Identifier for the skill that this version belongs to. + */ + "skill_id": S.String, + /** + * Object type. + * + * For Skill Versions, this is always `"skill_version"`. + */ + "type": S.String.pipe(S.propertySignature, S.withConstructorDefault(() => "skill_version" as const)), + /** + * Version identifier for the skill. + * + * Each version is identified by a Unix epoch timestamp (e.g., "1759178010641129"). + */ + "version": S.String +}) {} + +export class BetaDeleteSkillVersionV1SkillsSkillIdVersionsVersionDeleteParams extends S.Struct({ + /** + * Optional header to specify the beta version(s) you want to use. + * + * To use multiple betas, use a comma separated list like `beta1,beta2` or specify the header multiple times for each beta. + */ + "anthropic-beta": S.optionalWith(S.String, { nullable: true }), + /** + * The version of the Claude API you want to use. + * + * Read more about versioning and our version history [here](https://docs.claude.com/en/api/versioning). + */ + "anthropic-version": S.optionalWith(S.String, { nullable: true }), + /** + * Your unique API key for authentication. + * + * This key is required in the header of all API requests, to authenticate your account and access Anthropic's services. Get your API key through the [Console](https://console.anthropic.com/settings/keys). Each key is scoped to a Workspace. + */ + "x-api-key": S.optionalWith(S.String, { nullable: true }) +}) {} + +export class BetaDeleteSkillVersionResponse + extends S.Class("BetaDeleteSkillVersionResponse")({ + /** + * Version identifier for the skill. + * + * Each version is identified by a Unix epoch timestamp (e.g., "1759178010641129"). + */ + "id": S.String, + /** + * Deleted object type. + * + * For Skill Versions, this is always `"skill_version_deleted"`. + */ + "type": S.String.pipe(S.propertySignature, S.withConstructorDefault(() => "skill_version_deleted" as const)) + }) +{} + +export const make = ( + httpClient: HttpClient.HttpClient, + options: { + readonly transformClient?: ((client: HttpClient.HttpClient) => Effect.Effect) | undefined + } = {} +): Client => { + const unexpectedStatus = (response: HttpClientResponse.HttpClientResponse) => + Effect.flatMap( + Effect.orElseSucceed(response.json, () => "Unexpected status code"), + (description) => + Effect.fail( + new HttpClientError.ResponseError({ + request: response.request, + response, + reason: "StatusCode", + description: typeof description === "string" ? description : JSON.stringify(description) + }) + ) + ) + const withResponse: ( + f: (response: HttpClientResponse.HttpClientResponse) => Effect.Effect + ) => ( + request: HttpClientRequest.HttpClientRequest + ) => Effect.Effect = options.transformClient + ? (f) => (request) => + Effect.flatMap( + Effect.flatMap(options.transformClient!(httpClient), (client) => client.execute(request)), + f + ) + : (f) => (request) => Effect.flatMap(httpClient.execute(request), f) + const decodeSuccess = (schema: S.Schema) => (response: HttpClientResponse.HttpClientResponse) => + HttpClientResponse.schemaBodyJson(schema)(response) + const decodeError = + (tag: Tag, schema: S.Schema) => + (response: HttpClientResponse.HttpClientResponse) => + Effect.flatMap( + HttpClientResponse.schemaBodyJson(schema)(response), + (cause) => Effect.fail(ClientError(tag, cause, response)) + ) + return { + httpClient, + "messagesPost": (options) => + HttpClientRequest.post(`/v1/messages`).pipe( + HttpClientRequest.setHeaders({ "anthropic-version": options.params?.["anthropic-version"] ?? undefined }), + HttpClientRequest.bodyUnsafeJson(options.payload), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(Message), + "4xx": decodeError("ErrorResponse", ErrorResponse), + orElse: unexpectedStatus + })) + ), + "completePost": (options) => + HttpClientRequest.post(`/v1/complete`).pipe( + HttpClientRequest.setHeaders({ + "anthropic-version": options.params?.["anthropic-version"] ?? undefined, + "anthropic-beta": options.params?.["anthropic-beta"] ?? undefined + }), + HttpClientRequest.bodyUnsafeJson(options.payload), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(CompletionResponse), + "4xx": decodeError("ErrorResponse", ErrorResponse), + orElse: unexpectedStatus + })) + ), + "modelsList": (options) => + HttpClientRequest.get(`/v1/models`).pipe( + HttpClientRequest.setUrlParams({ + "before_id": options?.["before_id"] as any, + "after_id": options?.["after_id"] as any, + "limit": options?.["limit"] as any + }), + HttpClientRequest.setHeaders({ + "anthropic-version": options?.["anthropic-version"] ?? undefined, + "x-api-key": options?.["x-api-key"] ?? undefined, + "anthropic-beta": options?.["anthropic-beta"] ?? undefined + }), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ListResponseModelInfo), + "4xx": decodeError("ErrorResponse", ErrorResponse), + orElse: unexpectedStatus + })) + ), + "modelsGet": (modelId, options) => + HttpClientRequest.get(`/v1/models/${modelId}`).pipe( + HttpClientRequest.setHeaders({ + "anthropic-version": options?.["anthropic-version"] ?? undefined, + "x-api-key": options?.["x-api-key"] ?? undefined, + "anthropic-beta": options?.["anthropic-beta"] ?? undefined + }), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ModelInfo), + "4xx": decodeError("ErrorResponse", ErrorResponse), + orElse: unexpectedStatus + })) + ), + "messageBatchesList": (options) => + HttpClientRequest.get(`/v1/messages/batches`).pipe( + HttpClientRequest.setUrlParams({ + "before_id": options?.["before_id"] as any, + "after_id": options?.["after_id"] as any, + "limit": options?.["limit"] as any + }), + HttpClientRequest.setHeaders({ + "anthropic-version": options?.["anthropic-version"] ?? undefined, + "x-api-key": options?.["x-api-key"] ?? undefined + }), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ListResponseMessageBatch), + "4xx": decodeError("ErrorResponse", ErrorResponse), + orElse: unexpectedStatus + })) + ), + "messageBatchesPost": (options) => + HttpClientRequest.post(`/v1/messages/batches`).pipe( + HttpClientRequest.setHeaders({ "anthropic-version": options.params?.["anthropic-version"] ?? undefined }), + HttpClientRequest.bodyUnsafeJson(options.payload), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(MessageBatch), + "4xx": decodeError("ErrorResponse", ErrorResponse), + orElse: unexpectedStatus + })) + ), + "messageBatchesRetrieve": (messageBatchId, options) => + HttpClientRequest.get(`/v1/messages/batches/${messageBatchId}`).pipe( + HttpClientRequest.setHeaders({ + "anthropic-version": options?.["anthropic-version"] ?? undefined, + "x-api-key": options?.["x-api-key"] ?? undefined + }), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(MessageBatch), + "4xx": decodeError("ErrorResponse", ErrorResponse), + orElse: unexpectedStatus + })) + ), + "messageBatchesDelete": (messageBatchId, options) => + HttpClientRequest.del(`/v1/messages/batches/${messageBatchId}`).pipe( + HttpClientRequest.setHeaders({ + "anthropic-version": options?.["anthropic-version"] ?? undefined, + "x-api-key": options?.["x-api-key"] ?? undefined + }), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(DeleteMessageBatchResponse), + "4xx": decodeError("ErrorResponse", ErrorResponse), + orElse: unexpectedStatus + })) + ), + "messageBatchesCancel": (messageBatchId, options) => + HttpClientRequest.post(`/v1/messages/batches/${messageBatchId}/cancel`).pipe( + HttpClientRequest.setHeaders({ "anthropic-version": options?.["anthropic-version"] ?? undefined }), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(MessageBatch), + "4xx": decodeError("ErrorResponse", ErrorResponse), + orElse: unexpectedStatus + })) + ), + "messageBatchesResults": (messageBatchId, options) => + HttpClientRequest.get(`/v1/messages/batches/${messageBatchId}/results`).pipe( + HttpClientRequest.setHeaders({ + "anthropic-version": options?.["anthropic-version"] ?? undefined, + "x-api-key": options?.["x-api-key"] ?? undefined + }), + withResponse(HttpClientResponse.matchStatus({ + "4xx": decodeError("ErrorResponse", ErrorResponse), + orElse: unexpectedStatus + })) + ), + "messagesCountTokensPost": (options) => + HttpClientRequest.post(`/v1/messages/count_tokens`).pipe( + HttpClientRequest.setHeaders({ "anthropic-version": options.params?.["anthropic-version"] ?? undefined }), + HttpClientRequest.bodyUnsafeJson(options.payload), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(CountMessageTokensResponse), + "4xx": decodeError("ErrorResponse", ErrorResponse), + orElse: unexpectedStatus + })) + ), + "listFilesV1FilesGet": (options) => + HttpClientRequest.get(`/v1/files`).pipe( + HttpClientRequest.setUrlParams({ + "before_id": options?.["before_id"] as any, + "after_id": options?.["after_id"] as any, + "limit": options?.["limit"] as any + }), + HttpClientRequest.setHeaders({ + "anthropic-beta": options?.["anthropic-beta"] ?? undefined, + "anthropic-version": options?.["anthropic-version"] ?? undefined, + "x-api-key": options?.["x-api-key"] ?? undefined + }), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(FileListResponse), + "4xx": decodeError("ErrorResponse", ErrorResponse), + orElse: unexpectedStatus + })) + ), + "uploadFileV1FilesPost": (options) => + HttpClientRequest.post(`/v1/files`).pipe( + HttpClientRequest.setHeaders({ + "anthropic-beta": options.params?.["anthropic-beta"] ?? undefined, + "anthropic-version": options.params?.["anthropic-version"] ?? undefined + }), + HttpClientRequest.bodyFormDataRecord(options.payload as any), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(FileMetadataSchema), + "4xx": decodeError("ErrorResponse", ErrorResponse), + orElse: unexpectedStatus + })) + ), + "getFileMetadataV1FilesFileIdGet": (fileId, options) => + HttpClientRequest.get(`/v1/files/${fileId}`).pipe( + HttpClientRequest.setHeaders({ + "anthropic-beta": options?.["anthropic-beta"] ?? undefined, + "anthropic-version": options?.["anthropic-version"] ?? undefined, + "x-api-key": options?.["x-api-key"] ?? undefined + }), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(FileMetadataSchema), + "4xx": decodeError("ErrorResponse", ErrorResponse), + orElse: unexpectedStatus + })) + ), + "deleteFileV1FilesFileIdDelete": (fileId, options) => + HttpClientRequest.del(`/v1/files/${fileId}`).pipe( + HttpClientRequest.setHeaders({ + "anthropic-beta": options?.["anthropic-beta"] ?? undefined, + "anthropic-version": options?.["anthropic-version"] ?? undefined, + "x-api-key": options?.["x-api-key"] ?? undefined + }), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(FileDeleteResponse), + "4xx": decodeError("ErrorResponse", ErrorResponse), + orElse: unexpectedStatus + })) + ), + "downloadFileV1FilesFileIdContentGet": (fileId, options) => + HttpClientRequest.get(`/v1/files/${fileId}/content`).pipe( + HttpClientRequest.setHeaders({ + "anthropic-beta": options?.["anthropic-beta"] ?? undefined, + "anthropic-version": options?.["anthropic-version"] ?? undefined, + "x-api-key": options?.["x-api-key"] ?? undefined + }), + withResponse(HttpClientResponse.matchStatus({ + orElse: unexpectedStatus + })) + ), + "listSkillsV1SkillsGet": (options) => + HttpClientRequest.get(`/v1/skills`).pipe( + HttpClientRequest.setUrlParams({ + "page": options?.["page"] as any, + "limit": options?.["limit"] as any, + "source": options?.["source"] as any + }), + HttpClientRequest.setHeaders({ + "anthropic-beta": options?.["anthropic-beta"] ?? undefined, + "anthropic-version": options?.["anthropic-version"] ?? undefined, + "x-api-key": options?.["x-api-key"] ?? undefined + }), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ListSkillsResponse), + "4xx": decodeError("ErrorResponse", ErrorResponse), + orElse: unexpectedStatus + })) + ), + "createSkillV1SkillsPost": (options) => + HttpClientRequest.post(`/v1/skills`).pipe( + HttpClientRequest.setHeaders({ + "anthropic-beta": options.params?.["anthropic-beta"] ?? undefined, + "anthropic-version": options.params?.["anthropic-version"] ?? undefined + }), + HttpClientRequest.bodyFormDataRecord(options.payload as any), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(CreateSkillResponse), + "4xx": decodeError("ErrorResponse", ErrorResponse), + orElse: unexpectedStatus + })) + ), + "getSkillV1SkillsSkillIdGet": (skillId, options) => + HttpClientRequest.get(`/v1/skills/${skillId}`).pipe( + HttpClientRequest.setHeaders({ + "anthropic-beta": options?.["anthropic-beta"] ?? undefined, + "anthropic-version": options?.["anthropic-version"] ?? undefined, + "x-api-key": options?.["x-api-key"] ?? undefined + }), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(GetSkillResponse), + "4xx": decodeError("ErrorResponse", ErrorResponse), + orElse: unexpectedStatus + })) + ), + "deleteSkillV1SkillsSkillIdDelete": (skillId, options) => + HttpClientRequest.del(`/v1/skills/${skillId}`).pipe( + HttpClientRequest.setHeaders({ + "anthropic-beta": options?.["anthropic-beta"] ?? undefined, + "anthropic-version": options?.["anthropic-version"] ?? undefined, + "x-api-key": options?.["x-api-key"] ?? undefined + }), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(DeleteSkillResponse), + "4xx": decodeError("ErrorResponse", ErrorResponse), + orElse: unexpectedStatus + })) + ), + "listSkillVersionsV1SkillsSkillIdVersionsGet": (skillId, options) => + HttpClientRequest.get(`/v1/skills/${skillId}/versions`).pipe( + HttpClientRequest.setUrlParams({ "page": options?.["page"] as any, "limit": options?.["limit"] as any }), + HttpClientRequest.setHeaders({ + "anthropic-beta": options?.["anthropic-beta"] ?? undefined, + "anthropic-version": options?.["anthropic-version"] ?? undefined, + "x-api-key": options?.["x-api-key"] ?? undefined + }), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ListSkillVersionsResponse), + "4xx": decodeError("ErrorResponse", ErrorResponse), + orElse: unexpectedStatus + })) + ), + "createSkillVersionV1SkillsSkillIdVersionsPost": (skillId, options) => + HttpClientRequest.post(`/v1/skills/${skillId}/versions`).pipe( + HttpClientRequest.setHeaders({ + "anthropic-beta": options.params?.["anthropic-beta"] ?? undefined, + "anthropic-version": options.params?.["anthropic-version"] ?? undefined + }), + HttpClientRequest.bodyFormDataRecord(options.payload as any), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(CreateSkillVersionResponse), + "4xx": decodeError("ErrorResponse", ErrorResponse), + orElse: unexpectedStatus + })) + ), + "getSkillVersionV1SkillsSkillIdVersionsVersionGet": (skillId, version, options) => + HttpClientRequest.get(`/v1/skills/${skillId}/versions/${version}`).pipe( + HttpClientRequest.setHeaders({ + "anthropic-beta": options?.["anthropic-beta"] ?? undefined, + "anthropic-version": options?.["anthropic-version"] ?? undefined, + "x-api-key": options?.["x-api-key"] ?? undefined + }), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(GetSkillVersionResponse), + "4xx": decodeError("ErrorResponse", ErrorResponse), + orElse: unexpectedStatus + })) + ), + "deleteSkillVersionV1SkillsSkillIdVersionsVersionDelete": (skillId, version, options) => + HttpClientRequest.del(`/v1/skills/${skillId}/versions/${version}`).pipe( + HttpClientRequest.setHeaders({ + "anthropic-beta": options?.["anthropic-beta"] ?? undefined, + "anthropic-version": options?.["anthropic-version"] ?? undefined, + "x-api-key": options?.["x-api-key"] ?? undefined + }), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(DeleteSkillVersionResponse), + "4xx": decodeError("ErrorResponse", ErrorResponse), + orElse: unexpectedStatus + })) + ), + "betaMessagesPost": (options) => + HttpClientRequest.post(`/v1/messages?beta=true`).pipe( + HttpClientRequest.setHeaders({ + "anthropic-beta": options.params?.["anthropic-beta"] ?? undefined, + "anthropic-version": options.params?.["anthropic-version"] ?? undefined + }), + HttpClientRequest.bodyUnsafeJson(options.payload), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(BetaMessage), + "4xx": decodeError("BetaErrorResponse", BetaErrorResponse), + orElse: unexpectedStatus + })) + ), + "betaModelsList": (options) => + HttpClientRequest.get(`/v1/models?beta=true`).pipe( + HttpClientRequest.setUrlParams({ + "before_id": options?.["before_id"] as any, + "after_id": options?.["after_id"] as any, + "limit": options?.["limit"] as any + }), + HttpClientRequest.setHeaders({ + "anthropic-version": options?.["anthropic-version"] ?? undefined, + "x-api-key": options?.["x-api-key"] ?? undefined, + "anthropic-beta": options?.["anthropic-beta"] ?? undefined + }), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(BetaListResponseModelInfo), + "4xx": decodeError("BetaErrorResponse", BetaErrorResponse), + orElse: unexpectedStatus + })) + ), + "betaModelsGet": (modelId, options) => + HttpClientRequest.get(`/v1/models/${modelId}?beta=true`).pipe( + HttpClientRequest.setHeaders({ + "anthropic-version": options?.["anthropic-version"] ?? undefined, + "x-api-key": options?.["x-api-key"] ?? undefined, + "anthropic-beta": options?.["anthropic-beta"] ?? undefined + }), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(BetaModelInfo), + "4xx": decodeError("BetaErrorResponse", BetaErrorResponse), + orElse: unexpectedStatus + })) + ), + "betaMessageBatchesList": (options) => + HttpClientRequest.get(`/v1/messages/batches?beta=true`).pipe( + HttpClientRequest.setUrlParams({ + "before_id": options?.["before_id"] as any, + "after_id": options?.["after_id"] as any, + "limit": options?.["limit"] as any + }), + HttpClientRequest.setHeaders({ + "anthropic-beta": options?.["anthropic-beta"] ?? undefined, + "anthropic-version": options?.["anthropic-version"] ?? undefined, + "x-api-key": options?.["x-api-key"] ?? undefined + }), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(BetaListResponseMessageBatch), + "4xx": decodeError("BetaErrorResponse", BetaErrorResponse), + orElse: unexpectedStatus + })) + ), + "betaMessageBatchesPost": (options) => + HttpClientRequest.post(`/v1/messages/batches?beta=true`).pipe( + HttpClientRequest.setHeaders({ + "anthropic-beta": options.params?.["anthropic-beta"] ?? undefined, + "anthropic-version": options.params?.["anthropic-version"] ?? undefined + }), + HttpClientRequest.bodyUnsafeJson(options.payload), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(BetaMessageBatch), + "4xx": decodeError("BetaErrorResponse", BetaErrorResponse), + orElse: unexpectedStatus + })) + ), + "betaMessageBatchesRetrieve": (messageBatchId, options) => + HttpClientRequest.get(`/v1/messages/batches/${messageBatchId}?beta=true`).pipe( + HttpClientRequest.setHeaders({ + "anthropic-beta": options?.["anthropic-beta"] ?? undefined, + "anthropic-version": options?.["anthropic-version"] ?? undefined, + "x-api-key": options?.["x-api-key"] ?? undefined + }), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(BetaMessageBatch), + "4xx": decodeError("BetaErrorResponse", BetaErrorResponse), + orElse: unexpectedStatus + })) + ), + "betaMessageBatchesDelete": (messageBatchId, options) => + HttpClientRequest.del(`/v1/messages/batches/${messageBatchId}?beta=true`).pipe( + HttpClientRequest.setHeaders({ + "anthropic-beta": options?.["anthropic-beta"] ?? undefined, + "anthropic-version": options?.["anthropic-version"] ?? undefined, + "x-api-key": options?.["x-api-key"] ?? undefined + }), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(BetaDeleteMessageBatchResponse), + "4xx": decodeError("BetaErrorResponse", BetaErrorResponse), + orElse: unexpectedStatus + })) + ), + "betaMessageBatchesCancel": (messageBatchId, options) => + HttpClientRequest.post(`/v1/messages/batches/${messageBatchId}/cancel?beta=true`).pipe( + HttpClientRequest.setHeaders({ + "anthropic-beta": options?.["anthropic-beta"] ?? undefined, + "anthropic-version": options?.["anthropic-version"] ?? undefined + }), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(BetaMessageBatch), + "4xx": decodeError("BetaErrorResponse", BetaErrorResponse), + orElse: unexpectedStatus + })) + ), + "betaMessageBatchesResults": (messageBatchId, options) => + HttpClientRequest.get(`/v1/messages/batches/${messageBatchId}/results?beta=true`).pipe( + HttpClientRequest.setHeaders({ + "anthropic-beta": options?.["anthropic-beta"] ?? undefined, + "anthropic-version": options?.["anthropic-version"] ?? undefined, + "x-api-key": options?.["x-api-key"] ?? undefined + }), + withResponse(HttpClientResponse.matchStatus({ + "4xx": decodeError("BetaErrorResponse", BetaErrorResponse), + orElse: unexpectedStatus + })) + ), + "betaMessagesCountTokensPost": (options) => + HttpClientRequest.post(`/v1/messages/count_tokens?beta=true`).pipe( + HttpClientRequest.setHeaders({ + "anthropic-beta": options.params?.["anthropic-beta"] ?? undefined, + "anthropic-version": options.params?.["anthropic-version"] ?? undefined + }), + HttpClientRequest.bodyUnsafeJson(options.payload), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(BetaCountMessageTokensResponse), + "4xx": decodeError("BetaErrorResponse", BetaErrorResponse), + orElse: unexpectedStatus + })) + ), + "betaListFilesV1FilesGet": (options) => + HttpClientRequest.get(`/v1/files?beta=true`).pipe( + HttpClientRequest.setUrlParams({ + "before_id": options?.["before_id"] as any, + "after_id": options?.["after_id"] as any, + "limit": options?.["limit"] as any + }), + HttpClientRequest.setHeaders({ + "anthropic-beta": options?.["anthropic-beta"] ?? undefined, + "anthropic-version": options?.["anthropic-version"] ?? undefined, + "x-api-key": options?.["x-api-key"] ?? undefined + }), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(BetaFileListResponse), + "4xx": decodeError("BetaErrorResponse", BetaErrorResponse), + orElse: unexpectedStatus + })) + ), + "betaUploadFileV1FilesPost": (options) => + HttpClientRequest.post(`/v1/files?beta=true`).pipe( + HttpClientRequest.setHeaders({ + "anthropic-beta": options.params?.["anthropic-beta"] ?? undefined, + "anthropic-version": options.params?.["anthropic-version"] ?? undefined + }), + HttpClientRequest.bodyFormDataRecord(options.payload as any), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(BetaFileMetadataSchema), + "4xx": decodeError("BetaErrorResponse", BetaErrorResponse), + orElse: unexpectedStatus + })) + ), + "betaGetFileMetadataV1FilesFileIdGet": (fileId, options) => + HttpClientRequest.get(`/v1/files/${fileId}?beta=true`).pipe( + HttpClientRequest.setHeaders({ + "anthropic-beta": options?.["anthropic-beta"] ?? undefined, + "anthropic-version": options?.["anthropic-version"] ?? undefined, + "x-api-key": options?.["x-api-key"] ?? undefined + }), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(BetaFileMetadataSchema), + "4xx": decodeError("BetaErrorResponse", BetaErrorResponse), + orElse: unexpectedStatus + })) + ), + "betaDeleteFileV1FilesFileIdDelete": (fileId, options) => + HttpClientRequest.del(`/v1/files/${fileId}?beta=true`).pipe( + HttpClientRequest.setHeaders({ + "anthropic-beta": options?.["anthropic-beta"] ?? undefined, + "anthropic-version": options?.["anthropic-version"] ?? undefined, + "x-api-key": options?.["x-api-key"] ?? undefined + }), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(BetaFileDeleteResponse), + "4xx": decodeError("BetaErrorResponse", BetaErrorResponse), + orElse: unexpectedStatus + })) + ), + "betaDownloadFileV1FilesFileIdContentGet": (fileId, options) => + HttpClientRequest.get(`/v1/files/${fileId}/content?beta=true`).pipe( + HttpClientRequest.setHeaders({ + "anthropic-beta": options?.["anthropic-beta"] ?? undefined, + "anthropic-version": options?.["anthropic-version"] ?? undefined, + "x-api-key": options?.["x-api-key"] ?? undefined + }), + withResponse(HttpClientResponse.matchStatus({ + orElse: unexpectedStatus + })) + ), + "betaListSkillsV1SkillsGet": (options) => + HttpClientRequest.get(`/v1/skills?beta=true`).pipe( + HttpClientRequest.setUrlParams({ + "page": options?.["page"] as any, + "limit": options?.["limit"] as any, + "source": options?.["source"] as any + }), + HttpClientRequest.setHeaders({ + "anthropic-beta": options?.["anthropic-beta"] ?? undefined, + "anthropic-version": options?.["anthropic-version"] ?? undefined, + "x-api-key": options?.["x-api-key"] ?? undefined + }), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(BetaListSkillsResponse), + "4xx": decodeError("BetaErrorResponse", BetaErrorResponse), + orElse: unexpectedStatus + })) + ), + "betaCreateSkillV1SkillsPost": (options) => + HttpClientRequest.post(`/v1/skills?beta=true`).pipe( + HttpClientRequest.setHeaders({ + "anthropic-beta": options.params?.["anthropic-beta"] ?? undefined, + "anthropic-version": options.params?.["anthropic-version"] ?? undefined + }), + HttpClientRequest.bodyFormDataRecord(options.payload as any), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(BetaCreateSkillResponse), + "4xx": decodeError("BetaErrorResponse", BetaErrorResponse), + orElse: unexpectedStatus + })) + ), + "betaGetSkillV1SkillsSkillIdGet": (skillId, options) => + HttpClientRequest.get(`/v1/skills/${skillId}?beta=true`).pipe( + HttpClientRequest.setHeaders({ + "anthropic-beta": options?.["anthropic-beta"] ?? undefined, + "anthropic-version": options?.["anthropic-version"] ?? undefined, + "x-api-key": options?.["x-api-key"] ?? undefined + }), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(BetaGetSkillResponse), + "4xx": decodeError("BetaErrorResponse", BetaErrorResponse), + orElse: unexpectedStatus + })) + ), + "betaDeleteSkillV1SkillsSkillIdDelete": (skillId, options) => + HttpClientRequest.del(`/v1/skills/${skillId}?beta=true`).pipe( + HttpClientRequest.setHeaders({ + "anthropic-beta": options?.["anthropic-beta"] ?? undefined, + "anthropic-version": options?.["anthropic-version"] ?? undefined, + "x-api-key": options?.["x-api-key"] ?? undefined + }), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(BetaDeleteSkillResponse), + "4xx": decodeError("BetaErrorResponse", BetaErrorResponse), + orElse: unexpectedStatus + })) + ), + "betaListSkillVersionsV1SkillsSkillIdVersionsGet": (skillId, options) => + HttpClientRequest.get(`/v1/skills/${skillId}/versions?beta=true`).pipe( + HttpClientRequest.setUrlParams({ "page": options?.["page"] as any, "limit": options?.["limit"] as any }), + HttpClientRequest.setHeaders({ + "anthropic-beta": options?.["anthropic-beta"] ?? undefined, + "anthropic-version": options?.["anthropic-version"] ?? undefined, + "x-api-key": options?.["x-api-key"] ?? undefined + }), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(BetaListSkillVersionsResponse), + "4xx": decodeError("BetaErrorResponse", BetaErrorResponse), + orElse: unexpectedStatus + })) + ), + "betaCreateSkillVersionV1SkillsSkillIdVersionsPost": (skillId, options) => + HttpClientRequest.post(`/v1/skills/${skillId}/versions?beta=true`).pipe( + HttpClientRequest.setHeaders({ + "anthropic-beta": options.params?.["anthropic-beta"] ?? undefined, + "anthropic-version": options.params?.["anthropic-version"] ?? undefined + }), + HttpClientRequest.bodyFormDataRecord(options.payload as any), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(BetaCreateSkillVersionResponse), + "4xx": decodeError("BetaErrorResponse", BetaErrorResponse), + orElse: unexpectedStatus + })) + ), + "betaGetSkillVersionV1SkillsSkillIdVersionsVersionGet": (skillId, version, options) => + HttpClientRequest.get(`/v1/skills/${skillId}/versions/${version}?beta=true`).pipe( + HttpClientRequest.setHeaders({ + "anthropic-beta": options?.["anthropic-beta"] ?? undefined, + "anthropic-version": options?.["anthropic-version"] ?? undefined, + "x-api-key": options?.["x-api-key"] ?? undefined + }), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(BetaGetSkillVersionResponse), + "4xx": decodeError("BetaErrorResponse", BetaErrorResponse), + orElse: unexpectedStatus + })) + ), + "betaDeleteSkillVersionV1SkillsSkillIdVersionsVersionDelete": (skillId, version, options) => + HttpClientRequest.del(`/v1/skills/${skillId}/versions/${version}?beta=true`).pipe( + HttpClientRequest.setHeaders({ + "anthropic-beta": options?.["anthropic-beta"] ?? undefined, + "anthropic-version": options?.["anthropic-version"] ?? undefined, + "x-api-key": options?.["x-api-key"] ?? undefined + }), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(BetaDeleteSkillVersionResponse), + "4xx": decodeError("BetaErrorResponse", BetaErrorResponse), + orElse: unexpectedStatus + })) + ) + } +} + +export interface Client { + readonly httpClient: HttpClient.HttpClient + /** + * Send a structured list of input messages with text and/or image content, and the model will generate the next message in the conversation. + * + * The Messages API can be used for either single queries or stateless multi-turn conversations. + * + * Learn more about the Messages API in our [user guide](/en/docs/initial-setup) + */ + readonly "messagesPost": ( + options: { + readonly params?: typeof MessagesPostParams.Encoded | undefined + readonly payload: typeof CreateMessageParams.Encoded + } + ) => Effect.Effect< + typeof Message.Type, + HttpClientError.HttpClientError | ParseError | ClientError<"ErrorResponse", typeof ErrorResponse.Type> + > + /** + * [Legacy] Create a Text Completion. + * + * The Text Completions API is a legacy API. We recommend using the [Messages API](https://docs.claude.com/en/api/messages) going forward. + * + * Future models and features will not be compatible with Text Completions. See our [migration guide](https://docs.claude.com/en/api/migrating-from-text-completions-to-messages) for guidance in migrating from Text Completions to Messages. + */ + readonly "completePost": ( + options: { + readonly params?: typeof CompletePostParams.Encoded | undefined + readonly payload: typeof CompletionRequest.Encoded + } + ) => Effect.Effect< + typeof CompletionResponse.Type, + HttpClientError.HttpClientError | ParseError | ClientError<"ErrorResponse", typeof ErrorResponse.Type> + > + /** + * List available models. + * + * The Models API response can be used to determine which models are available for use in the API. More recently released models are listed first. + */ + readonly "modelsList": ( + options?: typeof ModelsListParams.Encoded | undefined + ) => Effect.Effect< + typeof ListResponseModelInfo.Type, + HttpClientError.HttpClientError | ParseError | ClientError<"ErrorResponse", typeof ErrorResponse.Type> + > + /** + * Get a specific model. + * + * The Models API response can be used to determine information about a specific model or resolve a model alias to a model ID. + */ + readonly "modelsGet": ( + modelId: string, + options?: typeof ModelsGetParams.Encoded | undefined + ) => Effect.Effect< + typeof ModelInfo.Type, + HttpClientError.HttpClientError | ParseError | ClientError<"ErrorResponse", typeof ErrorResponse.Type> + > + /** + * List all Message Batches within a Workspace. Most recently created batches are returned first. + * + * Learn more about the Message Batches API in our [user guide](/en/docs/build-with-claude/batch-processing) + */ + readonly "messageBatchesList": ( + options?: typeof MessageBatchesListParams.Encoded | undefined + ) => Effect.Effect< + typeof ListResponseMessageBatch.Type, + HttpClientError.HttpClientError | ParseError | ClientError<"ErrorResponse", typeof ErrorResponse.Type> + > + /** + * Send a batch of Message creation requests. + * + * The Message Batches API can be used to process multiple Messages API requests at once. Once a Message Batch is created, it begins processing immediately. Batches can take up to 24 hours to complete. + * + * Learn more about the Message Batches API in our [user guide](/en/docs/build-with-claude/batch-processing) + */ + readonly "messageBatchesPost": ( + options: { + readonly params?: typeof MessageBatchesPostParams.Encoded | undefined + readonly payload: typeof CreateMessageBatchParams.Encoded + } + ) => Effect.Effect< + typeof MessageBatch.Type, + HttpClientError.HttpClientError | ParseError | ClientError<"ErrorResponse", typeof ErrorResponse.Type> + > + /** + * This endpoint is idempotent and can be used to poll for Message Batch completion. To access the results of a Message Batch, make a request to the `results_url` field in the response. + * + * Learn more about the Message Batches API in our [user guide](/en/docs/build-with-claude/batch-processing) + */ + readonly "messageBatchesRetrieve": ( + messageBatchId: string, + options?: typeof MessageBatchesRetrieveParams.Encoded | undefined + ) => Effect.Effect< + typeof MessageBatch.Type, + HttpClientError.HttpClientError | ParseError | ClientError<"ErrorResponse", typeof ErrorResponse.Type> + > + /** + * Delete a Message Batch. + * + * Message Batches can only be deleted once they've finished processing. If you'd like to delete an in-progress batch, you must first cancel it. + * + * Learn more about the Message Batches API in our [user guide](/en/docs/build-with-claude/batch-processing) + */ + readonly "messageBatchesDelete": ( + messageBatchId: string, + options?: typeof MessageBatchesDeleteParams.Encoded | undefined + ) => Effect.Effect< + typeof DeleteMessageBatchResponse.Type, + HttpClientError.HttpClientError | ParseError | ClientError<"ErrorResponse", typeof ErrorResponse.Type> + > + /** + * Batches may be canceled any time before processing ends. Once cancellation is initiated, the batch enters a `canceling` state, at which time the system may complete any in-progress, non-interruptible requests before finalizing cancellation. + * + * The number of canceled requests is specified in `request_counts`. To determine which requests were canceled, check the individual results within the batch. Note that cancellation may not result in any canceled requests if they were non-interruptible. + * + * Learn more about the Message Batches API in our [user guide](/en/docs/build-with-claude/batch-processing) + */ + readonly "messageBatchesCancel": ( + messageBatchId: string, + options?: typeof MessageBatchesCancelParams.Encoded | undefined + ) => Effect.Effect< + typeof MessageBatch.Type, + HttpClientError.HttpClientError | ParseError | ClientError<"ErrorResponse", typeof ErrorResponse.Type> + > + /** + * Streams the results of a Message Batch as a `.jsonl` file. + * + * Each line in the file is a JSON object containing the result of a single request in the Message Batch. Results are not guaranteed to be in the same order as requests. Use the `custom_id` field to match results to requests. + * + * Learn more about the Message Batches API in our [user guide](/en/docs/build-with-claude/batch-processing) + */ + readonly "messageBatchesResults": ( + messageBatchId: string, + options?: typeof MessageBatchesResultsParams.Encoded | undefined + ) => Effect.Effect< + void, + HttpClientError.HttpClientError | ParseError | ClientError<"ErrorResponse", typeof ErrorResponse.Type> + > + /** + * Count the number of tokens in a Message. + * + * The Token Count API can be used to count the number of tokens in a Message, including tools, images, and documents, without creating it. + * + * Learn more about token counting in our [user guide](/en/docs/build-with-claude/token-counting) + */ + readonly "messagesCountTokensPost": ( + options: { + readonly params?: typeof MessagesCountTokensPostParams.Encoded | undefined + readonly payload: typeof CountMessageTokensParams.Encoded + } + ) => Effect.Effect< + typeof CountMessageTokensResponse.Type, + HttpClientError.HttpClientError | ParseError | ClientError<"ErrorResponse", typeof ErrorResponse.Type> + > + /** + * List Files + */ + readonly "listFilesV1FilesGet": ( + options?: typeof ListFilesV1FilesGetParams.Encoded | undefined + ) => Effect.Effect< + typeof FileListResponse.Type, + HttpClientError.HttpClientError | ParseError | ClientError<"ErrorResponse", typeof ErrorResponse.Type> + > + /** + * Upload File + */ + readonly "uploadFileV1FilesPost": ( + options: { + readonly params?: typeof UploadFileV1FilesPostParams.Encoded | undefined + readonly payload: typeof UploadFileV1FilesPostRequest.Encoded + } + ) => Effect.Effect< + typeof FileMetadataSchema.Type, + HttpClientError.HttpClientError | ParseError | ClientError<"ErrorResponse", typeof ErrorResponse.Type> + > + /** + * Get File Metadata + */ + readonly "getFileMetadataV1FilesFileIdGet": ( + fileId: string, + options?: typeof GetFileMetadataV1FilesFileIdGetParams.Encoded | undefined + ) => Effect.Effect< + typeof FileMetadataSchema.Type, + HttpClientError.HttpClientError | ParseError | ClientError<"ErrorResponse", typeof ErrorResponse.Type> + > + /** + * Delete File + */ + readonly "deleteFileV1FilesFileIdDelete": ( + fileId: string, + options?: typeof DeleteFileV1FilesFileIdDeleteParams.Encoded | undefined + ) => Effect.Effect< + typeof FileDeleteResponse.Type, + HttpClientError.HttpClientError | ParseError | ClientError<"ErrorResponse", typeof ErrorResponse.Type> + > + /** + * Download File + */ + readonly "downloadFileV1FilesFileIdContentGet": ( + fileId: string, + options?: typeof DownloadFileV1FilesFileIdContentGetParams.Encoded | undefined + ) => Effect.Effect + /** + * List Skills + */ + readonly "listSkillsV1SkillsGet": ( + options?: typeof ListSkillsV1SkillsGetParams.Encoded | undefined + ) => Effect.Effect< + typeof ListSkillsResponse.Type, + HttpClientError.HttpClientError | ParseError | ClientError<"ErrorResponse", typeof ErrorResponse.Type> + > + /** + * Create Skill + */ + readonly "createSkillV1SkillsPost": ( + options: { + readonly params?: typeof CreateSkillV1SkillsPostParams.Encoded | undefined + readonly payload: typeof BodyCreateSkillV1SkillsPost.Encoded + } + ) => Effect.Effect< + typeof CreateSkillResponse.Type, + HttpClientError.HttpClientError | ParseError | ClientError<"ErrorResponse", typeof ErrorResponse.Type> + > + /** + * Get Skill + */ + readonly "getSkillV1SkillsSkillIdGet": ( + skillId: string, + options?: typeof GetSkillV1SkillsSkillIdGetParams.Encoded | undefined + ) => Effect.Effect< + typeof GetSkillResponse.Type, + HttpClientError.HttpClientError | ParseError | ClientError<"ErrorResponse", typeof ErrorResponse.Type> + > + /** + * Delete Skill + */ + readonly "deleteSkillV1SkillsSkillIdDelete": ( + skillId: string, + options?: typeof DeleteSkillV1SkillsSkillIdDeleteParams.Encoded | undefined + ) => Effect.Effect< + typeof DeleteSkillResponse.Type, + HttpClientError.HttpClientError | ParseError | ClientError<"ErrorResponse", typeof ErrorResponse.Type> + > + /** + * List Skill Versions + */ + readonly "listSkillVersionsV1SkillsSkillIdVersionsGet": ( + skillId: string, + options?: typeof ListSkillVersionsV1SkillsSkillIdVersionsGetParams.Encoded | undefined + ) => Effect.Effect< + typeof ListSkillVersionsResponse.Type, + HttpClientError.HttpClientError | ParseError | ClientError<"ErrorResponse", typeof ErrorResponse.Type> + > + /** + * Create Skill Version + */ + readonly "createSkillVersionV1SkillsSkillIdVersionsPost": ( + skillId: string, + options: { + readonly params?: typeof CreateSkillVersionV1SkillsSkillIdVersionsPostParams.Encoded | undefined + readonly payload: typeof BodyCreateSkillVersionV1SkillsSkillIdVersionsPost.Encoded + } + ) => Effect.Effect< + typeof CreateSkillVersionResponse.Type, + HttpClientError.HttpClientError | ParseError | ClientError<"ErrorResponse", typeof ErrorResponse.Type> + > + /** + * Get Skill Version + */ + readonly "getSkillVersionV1SkillsSkillIdVersionsVersionGet": ( + skillId: string, + version: string, + options?: typeof GetSkillVersionV1SkillsSkillIdVersionsVersionGetParams.Encoded | undefined + ) => Effect.Effect< + typeof GetSkillVersionResponse.Type, + HttpClientError.HttpClientError | ParseError | ClientError<"ErrorResponse", typeof ErrorResponse.Type> + > + /** + * Delete Skill Version + */ + readonly "deleteSkillVersionV1SkillsSkillIdVersionsVersionDelete": ( + skillId: string, + version: string, + options?: typeof DeleteSkillVersionV1SkillsSkillIdVersionsVersionDeleteParams.Encoded | undefined + ) => Effect.Effect< + typeof DeleteSkillVersionResponse.Type, + HttpClientError.HttpClientError | ParseError | ClientError<"ErrorResponse", typeof ErrorResponse.Type> + > + /** + * Send a structured list of input messages with text and/or image content, and the model will generate the next message in the conversation. + * + * The Messages API can be used for either single queries or stateless multi-turn conversations. + * + * Learn more about the Messages API in our [user guide](/en/docs/initial-setup) + */ + readonly "betaMessagesPost": ( + options: { + readonly params?: typeof BetaMessagesPostParams.Encoded | undefined + readonly payload: typeof BetaCreateMessageParams.Encoded + } + ) => Effect.Effect< + typeof BetaMessage.Type, + HttpClientError.HttpClientError | ParseError | ClientError<"BetaErrorResponse", typeof BetaErrorResponse.Type> + > + /** + * List available models. + * + * The Models API response can be used to determine which models are available for use in the API. More recently released models are listed first. + */ + readonly "betaModelsList": ( + options?: typeof BetaModelsListParams.Encoded | undefined + ) => Effect.Effect< + typeof BetaListResponseModelInfo.Type, + HttpClientError.HttpClientError | ParseError | ClientError<"BetaErrorResponse", typeof BetaErrorResponse.Type> + > + /** + * Get a specific model. + * + * The Models API response can be used to determine information about a specific model or resolve a model alias to a model ID. + */ + readonly "betaModelsGet": ( + modelId: string, + options?: typeof BetaModelsGetParams.Encoded | undefined + ) => Effect.Effect< + typeof BetaModelInfo.Type, + HttpClientError.HttpClientError | ParseError | ClientError<"BetaErrorResponse", typeof BetaErrorResponse.Type> + > + /** + * List all Message Batches within a Workspace. Most recently created batches are returned first. + * + * Learn more about the Message Batches API in our [user guide](/en/docs/build-with-claude/batch-processing) + */ + readonly "betaMessageBatchesList": ( + options?: typeof BetaMessageBatchesListParams.Encoded | undefined + ) => Effect.Effect< + typeof BetaListResponseMessageBatch.Type, + HttpClientError.HttpClientError | ParseError | ClientError<"BetaErrorResponse", typeof BetaErrorResponse.Type> + > + /** + * Send a batch of Message creation requests. + * + * The Message Batches API can be used to process multiple Messages API requests at once. Once a Message Batch is created, it begins processing immediately. Batches can take up to 24 hours to complete. + * + * Learn more about the Message Batches API in our [user guide](/en/docs/build-with-claude/batch-processing) + */ + readonly "betaMessageBatchesPost": ( + options: { + readonly params?: typeof BetaMessageBatchesPostParams.Encoded | undefined + readonly payload: typeof BetaCreateMessageBatchParams.Encoded + } + ) => Effect.Effect< + typeof BetaMessageBatch.Type, + HttpClientError.HttpClientError | ParseError | ClientError<"BetaErrorResponse", typeof BetaErrorResponse.Type> + > + /** + * This endpoint is idempotent and can be used to poll for Message Batch completion. To access the results of a Message Batch, make a request to the `results_url` field in the response. + * + * Learn more about the Message Batches API in our [user guide](/en/docs/build-with-claude/batch-processing) + */ + readonly "betaMessageBatchesRetrieve": ( + messageBatchId: string, + options?: typeof BetaMessageBatchesRetrieveParams.Encoded | undefined + ) => Effect.Effect< + typeof BetaMessageBatch.Type, + HttpClientError.HttpClientError | ParseError | ClientError<"BetaErrorResponse", typeof BetaErrorResponse.Type> + > + /** + * Delete a Message Batch. + * + * Message Batches can only be deleted once they've finished processing. If you'd like to delete an in-progress batch, you must first cancel it. + * + * Learn more about the Message Batches API in our [user guide](/en/docs/build-with-claude/batch-processing) + */ + readonly "betaMessageBatchesDelete": ( + messageBatchId: string, + options?: typeof BetaMessageBatchesDeleteParams.Encoded | undefined + ) => Effect.Effect< + typeof BetaDeleteMessageBatchResponse.Type, + HttpClientError.HttpClientError | ParseError | ClientError<"BetaErrorResponse", typeof BetaErrorResponse.Type> + > + /** + * Batches may be canceled any time before processing ends. Once cancellation is initiated, the batch enters a `canceling` state, at which time the system may complete any in-progress, non-interruptible requests before finalizing cancellation. + * + * The number of canceled requests is specified in `request_counts`. To determine which requests were canceled, check the individual results within the batch. Note that cancellation may not result in any canceled requests if they were non-interruptible. + * + * Learn more about the Message Batches API in our [user guide](/en/docs/build-with-claude/batch-processing) + */ + readonly "betaMessageBatchesCancel": ( + messageBatchId: string, + options?: typeof BetaMessageBatchesCancelParams.Encoded | undefined + ) => Effect.Effect< + typeof BetaMessageBatch.Type, + HttpClientError.HttpClientError | ParseError | ClientError<"BetaErrorResponse", typeof BetaErrorResponse.Type> + > + /** + * Streams the results of a Message Batch as a `.jsonl` file. + * + * Each line in the file is a JSON object containing the result of a single request in the Message Batch. Results are not guaranteed to be in the same order as requests. Use the `custom_id` field to match results to requests. + * + * Learn more about the Message Batches API in our [user guide](/en/docs/build-with-claude/batch-processing) + */ + readonly "betaMessageBatchesResults": ( + messageBatchId: string, + options?: typeof BetaMessageBatchesResultsParams.Encoded | undefined + ) => Effect.Effect< + void, + HttpClientError.HttpClientError | ParseError | ClientError<"BetaErrorResponse", typeof BetaErrorResponse.Type> + > + /** + * Count the number of tokens in a Message. + * + * The Token Count API can be used to count the number of tokens in a Message, including tools, images, and documents, without creating it. + * + * Learn more about token counting in our [user guide](/en/docs/build-with-claude/token-counting) + */ + readonly "betaMessagesCountTokensPost": ( + options: { + readonly params?: typeof BetaMessagesCountTokensPostParams.Encoded | undefined + readonly payload: typeof BetaCountMessageTokensParams.Encoded + } + ) => Effect.Effect< + typeof BetaCountMessageTokensResponse.Type, + HttpClientError.HttpClientError | ParseError | ClientError<"BetaErrorResponse", typeof BetaErrorResponse.Type> + > + /** + * List Files + */ + readonly "betaListFilesV1FilesGet": ( + options?: typeof BetaListFilesV1FilesGetParams.Encoded | undefined + ) => Effect.Effect< + typeof BetaFileListResponse.Type, + HttpClientError.HttpClientError | ParseError | ClientError<"BetaErrorResponse", typeof BetaErrorResponse.Type> + > + /** + * Upload File + */ + readonly "betaUploadFileV1FilesPost": ( + options: { + readonly params?: typeof BetaUploadFileV1FilesPostParams.Encoded | undefined + readonly payload: typeof BetaUploadFileV1FilesPostRequest.Encoded + } + ) => Effect.Effect< + typeof BetaFileMetadataSchema.Type, + HttpClientError.HttpClientError | ParseError | ClientError<"BetaErrorResponse", typeof BetaErrorResponse.Type> + > + /** + * Get File Metadata + */ + readonly "betaGetFileMetadataV1FilesFileIdGet": ( + fileId: string, + options?: typeof BetaGetFileMetadataV1FilesFileIdGetParams.Encoded | undefined + ) => Effect.Effect< + typeof BetaFileMetadataSchema.Type, + HttpClientError.HttpClientError | ParseError | ClientError<"BetaErrorResponse", typeof BetaErrorResponse.Type> + > + /** + * Delete File + */ + readonly "betaDeleteFileV1FilesFileIdDelete": ( + fileId: string, + options?: typeof BetaDeleteFileV1FilesFileIdDeleteParams.Encoded | undefined + ) => Effect.Effect< + typeof BetaFileDeleteResponse.Type, + HttpClientError.HttpClientError | ParseError | ClientError<"BetaErrorResponse", typeof BetaErrorResponse.Type> + > + /** + * Download File + */ + readonly "betaDownloadFileV1FilesFileIdContentGet": ( + fileId: string, + options?: typeof BetaDownloadFileV1FilesFileIdContentGetParams.Encoded | undefined + ) => Effect.Effect + /** + * List Skills + */ + readonly "betaListSkillsV1SkillsGet": ( + options?: typeof BetaListSkillsV1SkillsGetParams.Encoded | undefined + ) => Effect.Effect< + typeof BetaListSkillsResponse.Type, + HttpClientError.HttpClientError | ParseError | ClientError<"BetaErrorResponse", typeof BetaErrorResponse.Type> + > + /** + * Create Skill + */ + readonly "betaCreateSkillV1SkillsPost": ( + options: { + readonly params?: typeof BetaCreateSkillV1SkillsPostParams.Encoded | undefined + readonly payload: typeof BetaBodyCreateSkillV1SkillsPost.Encoded + } + ) => Effect.Effect< + typeof BetaCreateSkillResponse.Type, + HttpClientError.HttpClientError | ParseError | ClientError<"BetaErrorResponse", typeof BetaErrorResponse.Type> + > + /** + * Get Skill + */ + readonly "betaGetSkillV1SkillsSkillIdGet": ( + skillId: string, + options?: typeof BetaGetSkillV1SkillsSkillIdGetParams.Encoded | undefined + ) => Effect.Effect< + typeof BetaGetSkillResponse.Type, + HttpClientError.HttpClientError | ParseError | ClientError<"BetaErrorResponse", typeof BetaErrorResponse.Type> + > + /** + * Delete Skill + */ + readonly "betaDeleteSkillV1SkillsSkillIdDelete": ( + skillId: string, + options?: typeof BetaDeleteSkillV1SkillsSkillIdDeleteParams.Encoded | undefined + ) => Effect.Effect< + typeof BetaDeleteSkillResponse.Type, + HttpClientError.HttpClientError | ParseError | ClientError<"BetaErrorResponse", typeof BetaErrorResponse.Type> + > + /** + * List Skill Versions + */ + readonly "betaListSkillVersionsV1SkillsSkillIdVersionsGet": ( + skillId: string, + options?: typeof BetaListSkillVersionsV1SkillsSkillIdVersionsGetParams.Encoded | undefined + ) => Effect.Effect< + typeof BetaListSkillVersionsResponse.Type, + HttpClientError.HttpClientError | ParseError | ClientError<"BetaErrorResponse", typeof BetaErrorResponse.Type> + > + /** + * Create Skill Version + */ + readonly "betaCreateSkillVersionV1SkillsSkillIdVersionsPost": ( + skillId: string, + options: { + readonly params?: typeof BetaCreateSkillVersionV1SkillsSkillIdVersionsPostParams.Encoded | undefined + readonly payload: typeof BetaBodyCreateSkillVersionV1SkillsSkillIdVersionsPost.Encoded + } + ) => Effect.Effect< + typeof BetaCreateSkillVersionResponse.Type, + HttpClientError.HttpClientError | ParseError | ClientError<"BetaErrorResponse", typeof BetaErrorResponse.Type> + > + /** + * Get Skill Version + */ + readonly "betaGetSkillVersionV1SkillsSkillIdVersionsVersionGet": ( + skillId: string, + version: string, + options?: typeof BetaGetSkillVersionV1SkillsSkillIdVersionsVersionGetParams.Encoded | undefined + ) => Effect.Effect< + typeof BetaGetSkillVersionResponse.Type, + HttpClientError.HttpClientError | ParseError | ClientError<"BetaErrorResponse", typeof BetaErrorResponse.Type> + > + /** + * Delete Skill Version + */ + readonly "betaDeleteSkillVersionV1SkillsSkillIdVersionsVersionDelete": ( + skillId: string, + version: string, + options?: typeof BetaDeleteSkillVersionV1SkillsSkillIdVersionsVersionDeleteParams.Encoded | undefined + ) => Effect.Effect< + typeof BetaDeleteSkillVersionResponse.Type, + HttpClientError.HttpClientError | ParseError | ClientError<"BetaErrorResponse", typeof BetaErrorResponse.Type> + > +} + +export interface ClientError { + readonly _tag: Tag + readonly request: HttpClientRequest.HttpClientRequest + readonly response: HttpClientResponse.HttpClientResponse + readonly cause: E +} + +class ClientErrorImpl extends Data.Error<{ + _tag: string + cause: any + request: HttpClientRequest.HttpClientRequest + response: HttpClientResponse.HttpClientResponse +}> {} + +export const ClientError = ( + tag: Tag, + cause: E, + response: HttpClientResponse.HttpClientResponse +): ClientError => + new ClientErrorImpl({ + _tag: tag, + cause, + response, + request: response.request + }) as any diff --git a/repos/effect/packages/ai/anthropic/src/index.ts b/repos/effect/packages/ai/anthropic/src/index.ts new file mode 100644 index 0000000..0615304 --- /dev/null +++ b/repos/effect/packages/ai/anthropic/src/index.ts @@ -0,0 +1,29 @@ +/** + * @since 1.0.0 + */ +export * as AnthropicClient from "./AnthropicClient.js" + +/** + * @since 1.0.0 + */ +export * as AnthropicConfig from "./AnthropicConfig.js" + +/** + * @since 1.0.0 + */ +export * as AnthropicLanguageModel from "./AnthropicLanguageModel.js" + +/** + * @since 1.0.0 + */ +export * as AnthropicTokenizer from "./AnthropicTokenizer.js" + +/** + * @since 1.0.0 + */ +export * as AnthropicTool from "./AnthropicTool.js" + +/** + * @since 1.0.0 + */ +export * as Generated from "./Generated.js" diff --git a/repos/effect/packages/ai/anthropic/src/internal/utilities.ts b/repos/effect/packages/ai/anthropic/src/internal/utilities.ts new file mode 100644 index 0000000..3f2ecfe --- /dev/null +++ b/repos/effect/packages/ai/anthropic/src/internal/utilities.ts @@ -0,0 +1,26 @@ +import type * as Response from "@effect/ai/Response" +import * as Predicate from "effect/Predicate" + +const finishReasonMap: Record = { + end_turn: "stop", + max_tokens: "length", + pause_turn: "pause", + refusal: "content-filter", + stop_sequence: "stop", + tool_use: "tool-calls" +} + +/** @internal */ +export const resolveFinishReason = ( + finishReason: string, + isJsonResponse: boolean = false +): Response.FinishReason => { + const reason = finishReasonMap[finishReason] + if (Predicate.isUndefined(reason)) { + return "unknown" + } + if (isJsonResponse && reason === "tool-calls") { + return "stop" + } + return reason +} diff --git a/repos/effect/packages/ai/anthropic/tsconfig.build.json b/repos/effect/packages/ai/anthropic/tsconfig.build.json new file mode 100644 index 0000000..4021ab2 --- /dev/null +++ b/repos/effect/packages/ai/anthropic/tsconfig.build.json @@ -0,0 +1,16 @@ +{ + "extends": "./tsconfig.src.json", + "references": [ + { "path": "../ai/tsconfig.build.json" }, + { "path": "../../effect/tsconfig.build.json" }, + { "path": "../../experimental/tsconfig.build.json" }, + { "path": "../../platform/tsconfig.build.json" } + ], + "compilerOptions": { + "tsBuildInfoFile": ".tsbuildinfo/build.tsbuildinfo", + "outDir": "build/esm", + "declarationDir": "build/dts", + "stripInternal": true, + "exactOptionalPropertyTypes": false + } +} diff --git a/repos/effect/packages/ai/anthropic/tsconfig.json b/repos/effect/packages/ai/anthropic/tsconfig.json new file mode 100644 index 0000000..f446496 --- /dev/null +++ b/repos/effect/packages/ai/anthropic/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../../tsconfig.base.json", + "include": [], + "references": [ + { "path": "tsconfig.src.json" }, + { "path": "tsconfig.test.json" } + ] +} diff --git a/repos/effect/packages/ai/anthropic/tsconfig.src.json b/repos/effect/packages/ai/anthropic/tsconfig.src.json new file mode 100644 index 0000000..01ab4fc --- /dev/null +++ b/repos/effect/packages/ai/anthropic/tsconfig.src.json @@ -0,0 +1,16 @@ +{ + "extends": "../../../tsconfig.base.json", + "include": ["src"], + "references": [ + { "path": "../ai/tsconfig.src.json" }, + { "path": "../../effect/tsconfig.src.json" }, + { "path": "../../experimental/tsconfig.src.json" }, + { "path": "../../platform/tsconfig.src.json" } + ], + "compilerOptions": { + "tsBuildInfoFile": ".tsbuildinfo/src.tsbuildinfo", + "rootDir": "src", + "outDir": "build/src", + "exactOptionalPropertyTypes": false + } +} diff --git a/repos/effect/packages/ai/anthropic/tsconfig.test.json b/repos/effect/packages/ai/anthropic/tsconfig.test.json new file mode 100644 index 0000000..95f3ae8 --- /dev/null +++ b/repos/effect/packages/ai/anthropic/tsconfig.test.json @@ -0,0 +1,14 @@ +{ + "extends": "../../../tsconfig.base.json", + "include": ["test"], + "references": [ + { "path": "tsconfig.src.json" }, + { "path": "../../vitest/tsconfig.src.json" } + ], + "compilerOptions": { + "tsBuildInfoFile": ".tsbuildinfo/test.tsbuildinfo", + "rootDir": "test", + "noEmit": true, + "exactOptionalPropertyTypes": false + } +} diff --git a/repos/effect/packages/ai/anthropic/vitest.config.ts b/repos/effect/packages/ai/anthropic/vitest.config.ts new file mode 100644 index 0000000..bf29895 --- /dev/null +++ b/repos/effect/packages/ai/anthropic/vitest.config.ts @@ -0,0 +1,6 @@ +import { mergeConfig, type ViteUserConfig } from "vitest/config" +import shared from "../../../vitest.shared.js" + +const config: ViteUserConfig = {} + +export default mergeConfig(shared, config) diff --git a/repos/effect/packages/ai/google/CHANGELOG.md b/repos/effect/packages/ai/google/CHANGELOG.md new file mode 100644 index 0000000..b7fcb78 --- /dev/null +++ b/repos/effect/packages/ai/google/CHANGELOG.md @@ -0,0 +1,596 @@ +# @effect/ai-google + +## 0.14.0 + +### Patch Changes + +- Updated dependencies [[`f7bb09b`](https://github.com/Effect-TS/effect/commit/f7bb09b022f195d1f2b3c23d49e74b011ec5d109), [`bd7552a`](https://github.com/Effect-TS/effect/commit/bd7552a19cc0ed575507ac6cc0879a57e24ebd31), [`ad1a7eb`](https://github.com/Effect-TS/effect/commit/ad1a7eb7f6bebaf91c80be2443ac0439226d0098), [`0d32048`](https://github.com/Effect-TS/effect/commit/0d32048f9836e2b23a6ba3ec5f43f0a000bb92fb), [`0d32048`](https://github.com/Effect-TS/effect/commit/0d32048f9836e2b23a6ba3ec5f43f0a000bb92fb)]: + - effect@3.21.0 + - @effect/ai@0.35.0 + - @effect/experimental@0.60.0 + - @effect/platform@0.96.0 + +## 0.13.0 + +### Patch Changes + +- Updated dependencies [[`fc82e81`](https://github.com/Effect-TS/effect/commit/fc82e81448bd9136a37580139ce46a2c61b11b54), [`82996bc`](https://github.com/Effect-TS/effect/commit/82996bce8debffcb44feb98bb862cf2662bd56b7), [`4d97a61`](https://github.com/Effect-TS/effect/commit/4d97a61a15b9dd6a0eece65b8f0c035e16d42ada), [`f6b0960`](https://github.com/Effect-TS/effect/commit/f6b0960bf3184109920dfed16ee7dfd7d67bc0f2), [`8798a84`](https://github.com/Effect-TS/effect/commit/8798a843218e6c0c0d3a8eee83360880e370b4da)]: + - effect@3.20.0 + - @effect/ai@0.34.0 + - @effect/experimental@0.59.0 + - @effect/platform@0.95.0 + +## 0.12.1 + +### Patch Changes + +- [#5958](https://github.com/Effect-TS/effect/pull/5958) [`c0a43bd`](https://github.com/Effect-TS/effect/commit/c0a43bd1456b2141a5383782091260e1c801c068) Thanks @izakfilmalter! - - `thinkingLevel` field on `ThinkingConfig` with values: `THINKING_LEVEL_UNSPECIFIED`, `MINIMAL`, `LOW`, `MEDIUM`, `HIGH` + - New `CandidateFinishReason` enum values: `IMAGE_PROHIBITED_CONTENT`, `IMAGE_OTHER`, `NO_IMAGE`, `IMAGE_RECITATION`, `MISSING_THOUGHT_SIGNATURE` + - Updated `finishReasonMap` in utilities to handle the new finish reason values + +## 0.12.0 + +### Patch Changes + +- Updated dependencies [[`77eeb86`](https://github.com/Effect-TS/effect/commit/77eeb86ddf208e51ec25932af83d52d3b4700371), [`ff7053f`](https://github.com/Effect-TS/effect/commit/ff7053f6d8508567b6145239f97aacc5773b0c53), [`287c32c`](https://github.com/Effect-TS/effect/commit/287c32c9f10da8e96f2b9ef8424316189d9ad4b3)]: + - effect@3.19.13 + - @effect/platform@0.94.0 + - @effect/ai@0.33.0 + - @effect/experimental@0.58.0 + +## 0.11.1 + +### Patch Changes + +- [#5834](https://github.com/Effect-TS/effect/pull/5834) [`59760c1`](https://github.com/Effect-TS/effect/commit/59760c1d10b72a539ba174a3ca78d29821b01818) Thanks @timurrakhimzhan! - Fixed "oneOf" toolChoice for google language model + +- Updated dependencies [[`65bff45`](https://github.com/Effect-TS/effect/commit/65bff451fc54d47b32995b3bc898ccc5f8b1beb6)]: + - @effect/platform@0.93.7 + +## 0.11.0 + +### Patch Changes + +- Updated dependencies [[`3c15d5f`](https://github.com/Effect-TS/effect/commit/3c15d5f99fb8d8470a00c5a33d9ba3cac89dfe4c), [`3863fa8`](https://github.com/Effect-TS/effect/commit/3863fa89f61e63e5529fd961e37333bddf7db64a), [`2a03c76`](https://github.com/Effect-TS/effect/commit/2a03c76c2781ca7e9e228e838eab2eb0d0795b1d), [`24a1685`](https://github.com/Effect-TS/effect/commit/24a1685c70a9ed157468650f95a5c3da3f2c2433)]: + - effect@3.19.0 + - @effect/platform@0.93.0 + - @effect/ai@0.32.0 + - @effect/experimental@0.57.0 + +## 0.10.0 + +### Minor Changes + +- [#5621](https://github.com/Effect-TS/effect/pull/5621) [`4c3bdfb`](https://github.com/Effect-TS/effect/commit/4c3bdfbcbc2dcd7ecd6321df3e4a504af19de825) Thanks @IMax153! - Remove `Either` / `EitherEncoded` from tool call results. + + Specifically, the encoding of tool call results as an `Either` / `EitherEncoded` has been removed and is replaced by encoding the tool call success / failure directly into the `result` property. + + To allow type-safe discrimination between a tool call result which was a success vs. one that was a failure, an `isFailure` property has also been added to the `"tool-result"` part. If `isFailure` is `true`, then the tool call handler result was an error. + + ```ts + import * as AnthropicClient from "@effect/ai-anthropic/AnthropicClient" + import * as AnthropicLanguageModel from "@effect/ai-anthropic/AnthropicLanguageModel" + import * as LanguageModel from "@effect/ai/LanguageModel" + import * as Tool from "@effect/ai/Tool" + import * as Toolkit from "@effect/ai/Toolkit" + import * as NodeHttpClient from "@effect/platform-node/NodeHttpClient" + import { Config, Effect, Layer, Schema, Stream } from "effect" + + const Claude = AnthropicLanguageModel.model("claude-4-sonnet-20250514") + + const MyTool = Tool.make("MyTool", { + description: "An example of a tool with success and failure types", + failureMode: "return", // Return errors in the response + parameters: { bar: Schema.Number }, + success: Schema.Number, + failure: Schema.Struct({ reason: Schema.Literal("reason-1", "reason-2") }) + }) + + const MyToolkit = Toolkit.make(MyTool) + + const MyToolkitLayer = MyToolkit.toLayer({ + MyTool: () => Effect.succeed(42) + }) + + const program = LanguageModel.streamText({ + prompt: "Tell me about the meaning of life", + toolkit: MyToolkit + }).pipe( + Stream.runForEach((part) => { + if (part.type === "tool-result" && part.name === "MyTool") { + // The `isFailure` property can be used to discriminate whether the result + // of a tool call is a success or a failure + if (part.isFailure) { + part.result + // ^? { readonly reason: "reason-1" | "reason-2"; } + } else { + part.result + // ^? number + } + } + return Effect.void + }), + Effect.provide(Claude) + ) + + const Anthropic = AnthropicClient.layerConfig({ + apiKey: Config.redacted("ANTHROPIC_API_KEY") + }).pipe(Layer.provide(NodeHttpClient.layerUndici)) + + program.pipe(Effect.provide([Anthropic, MyToolkitLayer]), Effect.runPromise) + ``` + +### Patch Changes + +- Updated dependencies [[`4c3bdfb`](https://github.com/Effect-TS/effect/commit/4c3bdfbcbc2dcd7ecd6321df3e4a504af19de825)]: + - @effect/ai@0.31.0 + +## 0.9.0 + +### Minor Changes + +- [#5614](https://github.com/Effect-TS/effect/pull/5614) [`c63e658`](https://github.com/Effect-TS/effect/commit/c63e6582244fbb50d31650c4b4ea0660fe194652) Thanks @IMax153! - Previously, tool call handler errors were _always_ raised as an expected error in the Effect `E` channel at the point of execution of the tool call handler (i.e. when a `generate*` method is invoked on a `LanguageModel`). + + With this PR, the end user now has control over whether tool call handler errors should be raised as an Effect error, or returned by the SDK to allow, for example, sending that error information to another application. + + ### Tool Call Specification + + The `Tool.make` and `Tool.providerDefined` constructors now take an extra optional parameter called `failureMode`, which can be set to either `"error"` or `"return"`. + + ```ts + import { Tool } from "@effect/ai" + import { Schema } from "effect" + + const MyTool = Tool.make("MyTool", { + description: "My special tool", + failureMode: "return" // "error" (default) or "return" + parameters: { + myParam: Schema.String + }, + success: Schema.Struct({ + mySuccess: Schema.String + }), + failure: Schema.Struct({ + myFailure: Schema.String + }) + }) + + ``` + + The semantics of `failureMode` are as follows: + - If set to `"error"` (the default), errors that occur during tool call handler execution will be returned in the error channel of the calling effect + - If set to `"return"`, errors that occur during tool call handler execution will be captured and returned as part of the tool call result + + ### Response - Tool Result Parts + + The `result` field of a `"tool-result"` part of a large language model provider response is now represented as an `Either`. + - If the `result` is a `Left`, the `result` will be the `failure` specified in the tool call specification + - If the `result` is a `Right`, the `result` will be the `success` specified in the tool call specification + + This is only relevant if the end user sets `failureMode` to `"return"`. If set to `"error"` (the default), then the `result` property will always be a `Right` with the successful result of the tool call handler. + + Similarly the `encodedResult` field of a `"tool-result"` part will be represented as an `EitherEncoded`, where: + - `{ _tag: "Left", left: }` represents a tool call handler failure + - `{ _tag: "Right", right: }` represents a tool call handler success + + ### Prompt - Tool Result Parts + + The `result` field of a `"tool-result"` part of a prompt will now only accept an `EitherEncoded` as specified above. + +### Patch Changes + +- Updated dependencies [[`6ae2f5d`](https://github.com/Effect-TS/effect/commit/6ae2f5da45a9ed9832605eca12b3e2bf2e2a1a67), [`c63e658`](https://github.com/Effect-TS/effect/commit/c63e6582244fbb50d31650c4b4ea0660fe194652)]: + - effect@3.18.4 + - @effect/ai@0.30.0 + +## 0.8.0 + +### Patch Changes + +- Updated dependencies [[`1c6ab74`](https://github.com/Effect-TS/effect/commit/1c6ab74b314b2b6df8bb1b1a0cb9527ceda0e3fa), [`70fe803`](https://github.com/Effect-TS/effect/commit/70fe803469db3355ffbf8359b52c351f1c2dc137), [`c296e32`](https://github.com/Effect-TS/effect/commit/c296e32554143b84ae8987046984e1cf1852417c), [`a098ddf`](https://github.com/Effect-TS/effect/commit/a098ddfc551f5aa0a7c36f9b4928372a64d4d9f2), [`f8b93ac`](https://github.com/Effect-TS/effect/commit/f8b93ac6446efd3dd790778b0fc71d299a38f272)]: + - effect@3.18.0 + - @effect/ai@0.29.0 + - @effect/platform@0.92.0 + - @effect/experimental@0.56.0 + +## 0.7.1 + +### Patch Changes + +- [#5571](https://github.com/Effect-TS/effect/pull/5571) [`122aa53`](https://github.com/Effect-TS/effect/commit/122aa53058ff008cf605cc2f0f0675a946c3cae9) Thanks @IMax153! - Ensure that AI provider clients filter response status for stream requests + +## 0.7.0 + +### Patch Changes + +- Updated dependencies [[`d4d86a8`](https://github.com/Effect-TS/effect/commit/d4d86a81f02b94e09fce8004ce2c5369c505ca5a)]: + - @effect/platform@0.91.0 + - @effect/ai@0.28.0 + - @effect/experimental@0.55.0 + +## 0.6.1 + +### Patch Changes + +- [#5521](https://github.com/Effect-TS/effect/pull/5521) [`fa49bc8`](https://github.com/Effect-TS/effect/commit/fa49bc86b14599300d106f306ceaf82a79121b80) Thanks @IMax153! - Fix provider metadata and parse tool call parameters safely + +- [#5523](https://github.com/Effect-TS/effect/pull/5523) [`3128917`](https://github.com/Effect-TS/effect/commit/3128917009e639555f684668825e5d93a97618dd) Thanks @IMax153! - Fix handling of anyOf conversion from JSONSchema to OpenAPI schema + +- Updated dependencies [[`fa49bc8`](https://github.com/Effect-TS/effect/commit/fa49bc86b14599300d106f306ceaf82a79121b80)]: + - @effect/ai@0.27.1 + +## 0.6.0 + +### Minor Changes + +- [#5469](https://github.com/Effect-TS/effect/pull/5469) [`42b914a`](https://github.com/Effect-TS/effect/commit/42b914a0e8750350ce17d434afaec7d655ddf4b7) Thanks @IMax153! - Refactor the Effect AI SDK and associated provider packages + + This pull request contains a complete refactor of the base Effect AI SDK package + as well as the associated provider integration packages to improve flexibility + and enhance ergonomics. Major changes are outlined below. + + ## Modules + + All modules in the base Effect AI SDK have had the leading `Ai` prefix dropped + from their name (except for the `AiError` module). + + For example, the `AiLanguageModel` module is now the `LanguageModel` module. + + In addition, the `AiInput` module has been renamed to the `Prompt` module. + + ## Prompts + + The `Prompt` module has been completely redesigned with flexibility in mind. + + The `Prompt` module now supports building a prompt using either the constructors + exposed from the `Prompt` module, or using raw prompt content parts / messages, + which should be familiar to those coming from other AI SDKs. + + In addition, the `system` option has been removed from all `LanguageModel` methods + and must now be provided as part of the prompt. + + **Prompt Constructors** + + ```ts + import { LanguageModel, Prompt } from "@effect/ai" + + const textPart = Prompt.makePart("text", { + text: "What is machine learning?" + }) + + const userMessage = Prompt.makeMessage("user", { + content: [textPart] + }) + + const systemMessage = Prompt.makeMessage("system", { + content: "You are an expert in machine learning" + }) + + const program = LanguageModel.generateText({ + prompt: Prompt.fromMessages([systemMessage, userMessage]) + }) + ``` + + **Raw Prompt Input** + + ```ts + import { LanguageModel } from "@effect/ai" + + const program = LanguageModel.generateText({ + prompt: [ + { role: "system", content: "You are an expert in machine learning" }, + { + role: "user", + content: [{ type: "text", text: "What is machine learning?" }] + } + ] + }) + ``` + + **NOTE**: Providing a plain string as a prompt is still supported, and will be converted + internally into a user message with a single text content part. + + ### Provider-Specific Options + + To support specification of provider-specific options when interacting with large + language model providers, support has been added for adding provider-specific + options to the parts of a `Prompt`. + + ```ts + import { LanguageModel } from "@effect/ai" + import { AnthropicLanguageModel } from "@effect/ai-anthropic" + + const Claude = AnthropicLanguageModel.model("claude-sonnet-4-20250514") + + const program = LanguageModel.generateText({ + prompt: [ + { + role: "user", + content: [{ type: "text", text: "What is machine learning?" }], + options: { + anthropic: { cacheControl: { type: "ephemeral", ttl: "1h" } } + } + } + ] + }).pipe(Effect.provide(Claude)) + ``` + + ## Responses + + The `Response` module has also been completely redesigned to support a wider + variety of response parts, particularly when streaming. + + ### Streaming Responses + + When streaming text via the `LanguageModel.streamText` method, you will now + receive a stream of content parts instead of a stream of responses, which should + make it much simpler to filter down the stream to the parts you are interested in. + + In addition, additional content parts will be present in the stream to allow you to track, + for example, when a text content part starts / ends. + + ### Tool Calls / Tool Call Results + + The decoded parts of a `Response` (as returned by the methods of `LanguageModel`) + are now fully type-safe on tool calls / tool call results. Filtering the content parts of a + response to tool calls will narrow the type of the tool call `params` based on the tool + `name`. Similarly, filtering the response to tool call results will narrow the type of the + tool call `result` based on the tool `name`. + + ```ts + import { LanguageModel, Tool, Toolkit } from "@effect/ai" + import { Effect, Schema } from "effect" + + const DadJokeTool = Tool.make("DadJokeTool", { + parameters: { topic: Schema.String }, + success: Schema.Struct({ joke: Schema.String }) + }) + + const FooTool = Tool.make("FooTool", { + parameters: { foo: Schema.Number }, + success: Schema.Struct({ bar: Schema.Boolean }) + }) + + const MyToolkit = Toolkit.make(DadJokeTool, FooTool) + + const program = Effect.gen(function* () { + const response = yield* LanguageModel.generateText({ + prompt: "Tell me a dad joke", + toolkit: MyToolkit + }) + + for (const toolCall of response.toolCalls) { + if (toolCall.name === "DadJokeTool") { + // ^? "DadJokeTool" | "FooTool" + toolCall.params + // ^? { readonly topic: string } + } + } + + for (const toolResult of response.toolResults) { + if (toolResult.name === "DadJokeTool") { + // ^? "DadJokeTool" | "FooTool" + toolResult.result + // ^? { readonly joke: string } + } + } + }) + ``` + + ### Provider Metadata + + As with provider-specific options, provider-specific metadata is now returned as + part of the response from the large language model provider. + + ```ts + import { LanguageModel } from "@effect/ai" + import { AnthropicLanguageModel } from "@effect/ai-anthropic" + import { Effect } from "effect" + + const Claude = AnthropicLanguageModel.model("claude-4-sonnet-20250514") + + const program = Effect.gen(function* () { + const response = yield* LanguageModel.generateText({ + prompt: "What is the meaning of life?" + }) + + for (const part of response.content) { + // When metadata **is not** defined for a content part, accessing the + // provider's key on the part's metadata will return an untyped record + if (part.type === "text") { + const metadata = part.metadata.anthropic + // ^? { readonly [x: string]: unknown } | undefined + } + // When metadata **is** defined for a content part, accessing the + // provider's key on the part's metadata will return typed metadata + if (part.type === "reasoning") { + const metadata = part.metadata.anthropic + // ^? AnthropicReasoningInfo | undefined + } + } + }).pipe(Effect.provide(Claude)) + ``` + + ## Tool Calls + + The `Tool` module has been enhanced to support provider-defined tools (e.g. + web search, computer use, etc.). Large language model providers which support + calling their own tools now have a separate module present in their provider + integration packages which contain definitions for their tools. + + These provider-defined tools can be included alongside user-defined tools in + existing `Toolkit`s. Provider-defined tools that require a user-space handler + will be raise a type error in the associated `Toolkit` layer if no such handler + is defined. + + ```ts + import { LanguageModel, Tool, Toolkit } from "@effect/ai" + import { AnthropicTool } from "@effect/ai-anthropic" + import { Schema } from "effect" + + const DadJokeTool = Tool.make("DadJokeTool", { + parameters: { topic: Schema.String }, + success: Schema.Struct({ joke: Schema.String }) + }) + + const MyToolkit = Toolkit.make( + DadJokeTool, + AnthropicTool.WebSearch_20250305({ max_uses: 1 }) + ) + + const program = LanguageModel.generateText({ + prompt: "Search the web for a dad joke", + toolkit: MyToolkit + }) + ``` + + ## AiError + + The `AiError` type has been refactored into a union of different error types + which can be raised by the Effect AI SDK. The goal of defining separate error + types is to allow providing the end-user with more granular information about + the error that occurred. + + For now, the following errors have been defined. More error types may be added + over time based upon necessity / use case. + + ```ts + type AiError = + | HttpRequestError, + | HttpResponseError, + | MalformedInput, + | MalformedOutput, + | UnknownError + ``` + +### Patch Changes + +- Updated dependencies [[`42b914a`](https://github.com/Effect-TS/effect/commit/42b914a0e8750350ce17d434afaec7d655ddf4b7)]: + - @effect/ai@0.27.0 + +## 0.5.0 + +### Patch Changes + +- Updated dependencies []: + - @effect/ai@0.26.0 + - @effect/experimental@0.54.6 + +## 0.4.0 + +### Patch Changes + +- Updated dependencies [[`5a0f4f1`](https://github.com/Effect-TS/effect/commit/5a0f4f176687a39d9fa46bb894bb7ac3175b0e87)]: + - effect@3.17.1 + - @effect/ai@0.25.0 + - @effect/experimental@0.54.0 + +## 0.3.0 + +### Patch Changes + +- Updated dependencies [[`7813640`](https://github.com/Effect-TS/effect/commit/7813640279d9e3a3e7fc0a29bfb5c6d5fb3c270f)]: + - @effect/platform@0.90.0 + - @effect/ai@0.24.0 + - @effect/experimental@0.54.0 + +## 0.2.0 + +### Patch Changes + +- Updated dependencies [[`40c3c87`](https://github.com/Effect-TS/effect/commit/40c3c875f724264312b43002859c82bed9ad0df9), [`ed2c74a`](https://github.com/Effect-TS/effect/commit/ed2c74ae8fa4ea0dd06ea84a3e58cd32e6916104), [`073a1b8`](https://github.com/Effect-TS/effect/commit/073a1b8be5dbfa87454393ee7346f5bc36a4fd63), [`f382e99`](https://github.com/Effect-TS/effect/commit/f382e99e409838a879246250fc3994b9bf5b3c2c), [`e8c7ba5`](https://github.com/Effect-TS/effect/commit/e8c7ba5fd3eb0c3ae3039fc24c09d69391987989), [`7e10415`](https://github.com/Effect-TS/effect/commit/7e1041599ade25103428703f5d2dfd7378a09636), [`e9bdece`](https://github.com/Effect-TS/effect/commit/e9bdececdc24f60a246be5055eca71a0d49ea7f2), [`8d95eb0`](https://github.com/Effect-TS/effect/commit/8d95eb0356b1d1736204836c275d201a547d208d)]: + - effect@3.17.0 + - @effect/ai@0.23.0 + - @effect/experimental@0.53.0 + - @effect/platform@0.89.0 + +## 0.1.2 + +### Patch Changes + +- Updated dependencies [[`f5dfabf`](https://github.com/Effect-TS/effect/commit/f5dfabf51ba481a4468c1509c537314978ef6cec), [`17a5ea8`](https://github.com/Effect-TS/effect/commit/17a5ea8fa29785fe6e4c9480f2a2e9c8c59f3f38), [`d25f22b`](https://github.com/Effect-TS/effect/commit/d25f22be7598abe977caf6cdac3b0dd78b438c48)]: + - effect@3.16.14 + - @effect/platform@0.88.1 + - @effect/experimental@0.52.1 + - @effect/ai@0.22.1 + +## 0.1.1 + +### Patch Changes + +- [#5209](https://github.com/Effect-TS/effect/pull/5209) [`3deaa66`](https://github.com/Effect-TS/effect/commit/3deaa66e022e361a2036ce6bfc9d76f77d9cc948) Thanks @tim-smart! - fix ai layerConfig regression, to allow for conditional Config variables + +## 0.1.0 + +### Patch Changes + +- Updated dependencies [[`27206d7`](https://github.com/Effect-TS/effect/commit/27206d7f0558d7fe28de57bf54f1d0cc83acc92e), [`dbabf5e`](https://github.com/Effect-TS/effect/commit/dbabf5e76fa63b050d2b6c466713c7dc59f07d3c)]: + - @effect/platform@0.88.0 + - @effect/ai@0.22.0 + - @effect/experimental@0.52.0 + +## 0.0.7 + +### Patch Changes + +- Updated dependencies [[`c1c05a8`](https://github.com/Effect-TS/effect/commit/c1c05a8242fb5df7445b4a12387a60eac7726eb7), [`81fe4a2`](https://github.com/Effect-TS/effect/commit/81fe4a2c81d5e30e180a60e68c52016a27b350db)]: + - effect@3.16.13 + - @effect/ai@0.21.17 + - @effect/experimental@0.51.14 + - @effect/platform@0.87.13 + +## 0.0.6 + +### Patch Changes + +- [#5186](https://github.com/Effect-TS/effect/pull/5186) [`e5692ab`](https://github.com/Effect-TS/effect/commit/e5692ab2be157b885f449ffb5c5f022eca04a59e) Thanks @IMax153! - Do not use `Config.Wrap` for AI provider `layerConfig` + +- Updated dependencies [[`32ba77a`](https://github.com/Effect-TS/effect/commit/32ba77ae304d2161362a73e8b61965332626cf2d), [`d5e25b2`](https://github.com/Effect-TS/effect/commit/d5e25b237f05670ee42b386cb40b2cb448fc11d7)]: + - @effect/platform@0.87.12 + - @effect/ai@0.21.16 + - @effect/experimental@0.51.13 + +## 0.0.5 + +### Patch Changes + +- Updated dependencies [[`001392b`](https://github.com/Effect-TS/effect/commit/001392ba8bfcad101bb034348a7415012fb12f72), [`7bfb099`](https://github.com/Effect-TS/effect/commit/7bfb099cb5528511b8d63045c4fbb4dc9cb18528)]: + - @effect/platform@0.87.11 + - @effect/ai@0.21.15 + - @effect/experimental@0.51.12 + +## 0.0.4 + +### Patch Changes + +- Updated dependencies [[`678318d`](https://github.com/Effect-TS/effect/commit/678318d2e88233156b006acda56c2d138ee3ffa0), [`678318d`](https://github.com/Effect-TS/effect/commit/678318d2e88233156b006acda56c2d138ee3ffa0)]: + - @effect/platform@0.87.10 + - @effect/ai@0.21.14 + - @effect/experimental@0.51.11 + +## 0.0.3 + +### Patch Changes + +- Updated dependencies [[`54514a2`](https://github.com/Effect-TS/effect/commit/54514a2f53166de27ad7e756dbf12194691fd4af)]: + - @effect/platform@0.87.9 + - @effect/ai@0.21.13 + - @effect/experimental@0.51.10 + +## 0.0.2 + +### Patch Changes + +- Updated dependencies [[`4ce4f82`](https://github.com/Effect-TS/effect/commit/4ce4f824f6fdef492be1d35c05a490ffce518c89)]: + - @effect/platform@0.87.8 + - @effect/experimental@0.51.9 + - @effect/ai@0.21.12 + +## 0.0.1 + +### Patch Changes + +- [#5029](https://github.com/Effect-TS/effect/pull/5029) [`d92d12a`](https://github.com/Effect-TS/effect/commit/d92d12acb6097a4fa6c9c918faa3cd5c3fb6c778) Thanks @IMax153! - Cleanup AiLanguageModel construction and finish basic support for gemini + +- Updated dependencies [[`d92d12a`](https://github.com/Effect-TS/effect/commit/d92d12acb6097a4fa6c9c918faa3cd5c3fb6c778), [`25ca0cf`](https://github.com/Effect-TS/effect/commit/25ca0cf141139cd44ff53081b1c877f8f3ab5e41), [`d92d12a`](https://github.com/Effect-TS/effect/commit/d92d12acb6097a4fa6c9c918faa3cd5c3fb6c778)]: + - @effect/ai@0.21.11 diff --git a/repos/effect/packages/ai/google/LICENSE b/repos/effect/packages/ai/google/LICENSE new file mode 100644 index 0000000..be1f5c1 --- /dev/null +++ b/repos/effect/packages/ai/google/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Effectful Technologies Inc + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/repos/effect/packages/ai/google/README.md b/repos/effect/packages/ai/google/README.md new file mode 100644 index 0000000..198472b --- /dev/null +++ b/repos/effect/packages/ai/google/README.md @@ -0,0 +1,5 @@ +# `@effect/ai-google` + +## Documentation + +- **API Reference**: [View the full documentation](https://effect-ts.github.io/effect/docs/ai/google). diff --git a/repos/effect/packages/ai/google/docgen.json b/repos/effect/packages/ai/google/docgen.json new file mode 100644 index 0000000..f260e5c --- /dev/null +++ b/repos/effect/packages/ai/google/docgen.json @@ -0,0 +1,26 @@ +{ + "$schema": "../../../node_modules/@effect/docgen/schema.json", + "exclude": ["src/Generated.ts", "src/internal/**/*.ts"], + "srcLink": "https://github.com/Effect-TS/effect/tree/main/packages/ai/google/src/", + "examplesCompilerOptions": { + "noEmit": true, + "strict": true, + "skipLibCheck": true, + "moduleResolution": "Bundler", + "module": "ES2022", + "target": "ES2022", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "paths": { + "effect": ["../../../../effect/src/index.js"], + "effect/*": ["../../../../effect/src/*.js"], + "@effect/experimental": ["../../../../experimental/src/index.js"], + "@effect/experimental/*": ["../../../../experimental/src/*.js"], + "@effect/platform": ["../../../../platform/src/index.js"], + "@effect/platform/*": ["../../../../platform/src/*.js"], + "@effect/ai": ["../../../ai/src/index.js"], + "@effect/ai/*": ["../../../ai/src/*.js"], + "@effect/ai-google": ["../../../ai-google/src/index.js"], + "@effect/ai-google/*": ["../../../ai-google/src/*.js"] + } + } +} diff --git a/repos/effect/packages/ai/google/package.json b/repos/effect/packages/ai/google/package.json new file mode 100644 index 0000000..5d2d2e7 --- /dev/null +++ b/repos/effect/packages/ai/google/package.json @@ -0,0 +1,62 @@ +{ + "name": "@effect/ai-google", + "type": "module", + "version": "0.14.0", + "license": "MIT", + "description": "Effect modules for working with AI apis", + "homepage": "https://effect.website", + "repository": { + "type": "git", + "url": "https://github.com/Effect-TS/effect.git", + "directory": "packages/ai/google" + }, + "bugs": { + "url": "https://github.com/Effect-TS/effect/issues" + }, + "tags": [ + "typescript", + "algebraic-data-types", + "functional-programming" + ], + "keywords": [ + "typescript", + "algebraic-data-types", + "functional-programming" + ], + "publishConfig": { + "access": "public", + "provenance": true, + "directory": "dist", + "linkDirectory": false + }, + "exports": { + "./package.json": "./package.json", + ".": "./src/index.ts", + "./*": "./src/*.ts", + "./internal/*": null + }, + "scripts": { + "codegen": "build-utils prepare-v3", + "build": "pnpm build-esm && pnpm build-annotate && pnpm build-cjs && build-utils pack-v3", + "build-esm": "tsc -b tsconfig.build.json", + "build-cjs": "babel build/esm --plugins @babel/transform-export-namespace-from --plugins @babel/transform-modules-commonjs --out-dir build/cjs --source-maps", + "build-annotate": "babel build/esm --plugins annotate-pure-calls --out-dir build/esm --source-maps", + "check": "tsc -b tsconfig.json", + "test": "vitest", + "coverage": "vitest --coverage" + }, + "peerDependencies": { + "@effect/ai": "workspace:^", + "@effect/experimental": "workspace:^", + "@effect/platform": "workspace:^", + "effect": "workspace:^" + }, + "devDependencies": { + "@effect/ai": "workspace:^", + "@effect/experimental": "workspace:^", + "@effect/platform": "workspace:^", + "@effect/platform-node": "workspace:^", + "@tim-smart/openapi-gen": "^0.4.10", + "effect": "workspace:^" + } +} diff --git a/repos/effect/packages/ai/google/scripts/generate.sh b/repos/effect/packages/ai/google/scripts/generate.sh new file mode 100755 index 0000000..6fb7f03 --- /dev/null +++ b/repos/effect/packages/ai/google/scripts/generate.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash + +set -euo pipefail + +SCRIPT_DIR=$(cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd) + +if [[ -z "${GOOGLE_API_KEY:-}" ]]; then + echo "Error: GOOGLE_API_KEY environment variable is not set." + exit 1 +fi + +base_url="https://generativelanguage.googleapis.com/" +temp_dir=$(mktemp -d) + +cleanup() { + rm -rf "${temp_dir}" +} + +trap cleanup EXIT + +curl "${base_url}\$discovery/OPENAPI3_0?version=v1beta&key=${GOOGLE_API_KEY}" > "${temp_dir}/openapi.json" + +echo "/** + * @since 1.0.0 + */" > src/Generated.ts + +pnpm openapi-gen -s "${temp_dir}/openapi.json" >> src/Generated.ts + +git apply --reject --whitespace=fix "${SCRIPT_DIR}/generated.patch" + +pnpm eslint --fix src/Generated.ts diff --git a/repos/effect/packages/ai/google/scripts/generated.patch b/repos/effect/packages/ai/google/scripts/generated.patch new file mode 100644 index 0000000..adc71b9 --- /dev/null +++ b/repos/effect/packages/ai/google/scripts/generated.patch @@ -0,0 +1,229 @@ +diff --git a/packages/ai/google/src/Generated.ts b/packages/ai/google/src/Generated.ts +index 52b1a4a9e..47b58417a 100644 +--- a/packages/ai/google/src/Generated.ts ++++ b/packages/ai/google/src/Generated.ts +@@ -383,102 +383,123 @@ export class Content extends S.Class("Content")({ + + export class Type extends S.Literal("TYPE_UNSPECIFIED", "STRING", "NUMBER", "INTEGER", "BOOLEAN", "ARRAY", "OBJECT", "NULL") {} + ++const schemaFields = { ++ /** ++ * Optional. Maximum value of the Type.INTEGER and Type.NUMBER ++ */ ++ "maximum": S.optionalWith(S.Number, { nullable: true }), ++ /** ++ * Optional. Minimum number of the properties for Type.OBJECT. ++ */ ++ "minProperties": S.optionalWith(S.String, { nullable: true }), ++ /** ++ * Required. Data type. ++ */ ++ "type": Type, ++ /** ++ * Optional. A brief description of the parameter. This could contain examples of use. ++ * Parameter description may be formatted as Markdown. ++ */ ++ "description": S.optionalWith(S.String, { nullable: true }), ++ /** ++ * Optional. Maximum number of the elements for Type.ARRAY. ++ */ ++ "maxItems": S.optionalWith(S.String, { nullable: true }), ++ /** ++ * Optional. The order of the properties. ++ * Not a standard field in open api spec. Used to determine the order of the ++ * properties in the response. ++ */ ++ "propertyOrdering": S.optionalWith(S.Array(S.String), { nullable: true }), ++ /** ++ * Optional. The format of the data. This is used only for primitive datatypes. ++ * Supported formats: ++ * for NUMBER type: float, double ++ * for INTEGER type: int32, int64 ++ * for STRING type: enum, date-time ++ */ ++ "format": S.optionalWith(S.String, { nullable: true }), ++ /** ++ * Optional. Maximum length of the Type.STRING ++ */ ++ "maxLength": S.optionalWith(S.String, { nullable: true }), ++ /** ++ * Optional. The title of the schema. ++ */ ++ "title": S.optionalWith(S.String, { nullable: true }), ++ /** ++ * Optional. SCHEMA FIELDS FOR TYPE INTEGER and NUMBER ++ * Minimum value of the Type.INTEGER and Type.NUMBER ++ */ ++ "minimum": S.optionalWith(S.Number, { nullable: true }), ++ /** ++ * Optional. Maximum number of the properties for Type.OBJECT. ++ */ ++ "maxProperties": S.optionalWith(S.String, { nullable: true }), ++ /** ++ * Optional. Possible values of the element of Type.STRING with enum format. ++ * For example we can define an Enum Direction as : ++ * {type:STRING, format:enum, enum:["EAST", NORTH", "SOUTH", "WEST"]} ++ */ ++ "enum": S.optionalWith(S.Array(S.String), { nullable: true }), ++ /** ++ * Optional. Required properties of Type.OBJECT. ++ */ ++ "required": S.optionalWith(S.Array(S.String), { nullable: true }), ++ /** ++ * Optional. Indicates if the value may be null. ++ */ ++ "nullable": S.optionalWith(S.Boolean, { nullable: true }), ++ /** ++ * Optional. Properties of Type.OBJECT. ++ */ ++ "properties": S.optionalWith(S.Record({ key: S.String, value: S.Unknown }), { nullable: true }), ++ /** ++ * Optional. Minimum number of the elements for Type.ARRAY. ++ */ ++ "minItems": S.optionalWith(S.String, { nullable: true }), ++ /** ++ * Optional. SCHEMA FIELDS FOR TYPE STRING ++ * Minimum length of the Type.STRING ++ */ ++ "minLength": S.optionalWith(S.String, { nullable: true }), ++ /** ++ * Optional. Pattern of the Type.STRING to restrict a string to a regular expression. ++ */ ++ "pattern": S.optionalWith(S.String, { nullable: true }) ++} ++ + /** +-* The `Schema` object allows the definition of input and output data types. +-* These types can be objects, but also primitives and arrays. +-* Represents a select subset of an [OpenAPI 3.0 schema +-* object](https://spec.openapis.org/oas/v3.0.3#schema). +-*/ +-export class Schema extends S.Class("Schema")({ +- /** +-* Required. Data type. +-*/ +-"type": Type, +- /** +-* Optional. The format of the data. Any value is allowed, but most do not trigger any +-* special functionality. +-*/ +-"format": S.optionalWith(S.String, { nullable: true }), +- /** +-* Optional. The title of the schema. +-*/ +-"title": S.optionalWith(S.String, { nullable: true }), +- /** +-* Optional. A brief description of the parameter. This could contain examples of use. +-* Parameter description may be formatted as Markdown. +-*/ +-"description": S.optionalWith(S.String, { nullable: true }), +- /** +-* Optional. Indicates if the value may be null. +-*/ +-"nullable": S.optionalWith(S.Boolean, { nullable: true }), +- /** +-* Optional. Possible values of the element of Type.STRING with enum format. +-* For example we can define an Enum Direction as : +-* {type:STRING, format:enum, enum:["EAST", NORTH", "SOUTH", "WEST"]} +-*/ +-"enum": S.optionalWith(S.Array(S.String), { nullable: true }), +- /** +-* Optional. Schema of the elements of Type.ARRAY. +-*/ +-"items": S.optionalWith(Schema, { nullable: true }), +- /** +-* Optional. Maximum number of the elements for Type.ARRAY. +-*/ +-"maxItems": S.optionalWith(S.String, { nullable: true }), +- /** +-* Optional. Minimum number of the elements for Type.ARRAY. +-*/ +-"minItems": S.optionalWith(S.String, { nullable: true }), +- /** +-* Optional. Properties of Type.OBJECT. +-*/ +-"properties": S.optionalWith(S.Record({ key: S.String, value: S.Unknown }), { nullable: true }), +- /** +-* Optional. Required properties of Type.OBJECT. +-*/ +-"required": S.optionalWith(S.Array(S.String), { nullable: true }), +- /** +-* Optional. Minimum number of the properties for Type.OBJECT. +-*/ +-"minProperties": S.optionalWith(S.String, { nullable: true }), +- /** +-* Optional. Maximum number of the properties for Type.OBJECT. +-*/ +-"maxProperties": S.optionalWith(S.String, { nullable: true }), +- /** +-* Optional. SCHEMA FIELDS FOR TYPE INTEGER and NUMBER +-* Minimum value of the Type.INTEGER and Type.NUMBER +-*/ +-"minimum": S.optionalWith(S.Number, { nullable: true }), +- /** +-* Optional. Maximum value of the Type.INTEGER and Type.NUMBER +-*/ +-"maximum": S.optionalWith(S.Number, { nullable: true }), +- /** +-* Optional. SCHEMA FIELDS FOR TYPE STRING +-* Minimum length of the Type.STRING +-*/ +-"minLength": S.optionalWith(S.String, { nullable: true }), +- /** +-* Optional. Maximum length of the Type.STRING +-*/ +-"maxLength": S.optionalWith(S.String, { nullable: true }), ++ * The \`Schema\` object allows the definition of input and output data types. ++ * These types can be objects, but also primitives and arrays. ++ * Represents a select subset of an [OpenAPI 3.0 schema ++ * object](https://spec.openapis.org/oas/v3.0.3#schema). ++ */ ++export interface SchemaEncoded extends S.Struct.Encoded { + /** +-* Optional. Pattern of the Type.STRING to restrict a string to a regular expression. +-*/ +-"pattern": S.optionalWith(S.String, { nullable: true }), ++ * Optional. The value should be validated against any (one or more) of the subschemas ++ * in the list. ++ */ ++ readonly "anyOf"?: ReadonlyArray | undefined | null + /** +-* Optional. The value should be validated against any (one or more) of the subschemas +-* in the list. +-*/ +-"anyOf": S.optionalWith(S.Array(Schema), { nullable: true }), ++ * Optional. Schema of the elements of Type.ARRAY. ++ */ ++ readonly "items"?: SchemaEncoded | undefined | null ++} ++ ++/** ++ * The \`Schema\` object allows the definition of input and output data types. ++ * These types can be objects, but also primitives and arrays. ++ * Represents a select subset of an [OpenAPI 3.0 schema ++ * object](https://spec.openapis.org/oas/v3.0.3#schema). ++ */ ++export class Schema extends S.Class("Schema")({ ++ ...schemaFields, ++ "anyOf": S.optionalWith(S.Array(S.suspend((): S.Schema => Schema)), { nullable: true }), + /** +-* Optional. The order of the properties. +-* Not a standard field in open api spec. Used to determine the order of the +-* properties in the response. +-*/ +-"propertyOrdering": S.optionalWith(S.Array(S.String), { nullable: true }) ++ * Optional. Schema of the elements of Type.ARRAY. ++ */ ++ "items": S.optionalWith(S.suspend((): S.Schema => Schema), { nullable: true }) + }) {} + + /** +@@ -4237,6 +4258,8 @@ export const make = ( + (schema: S.Schema) => + (response: HttpClientResponse.HttpClientResponse) => + HttpClientResponse.schemaBodyJson(schema)(response) ++ // @ts-expect-error ++ // eslint-disable-next-line @typescript-eslint/no-unused-vars + const decodeError = + (tag: Tag, schema: S.Schema) => + (response: HttpClientResponse.HttpClientResponse) => diff --git a/repos/effect/packages/ai/google/src/Generated.ts b/repos/effect/packages/ai/google/src/Generated.ts new file mode 100644 index 0000000..234d2e1 --- /dev/null +++ b/repos/effect/packages/ai/google/src/Generated.ts @@ -0,0 +1,6074 @@ +/** + * @since 1.0.0 + */ +import type * as HttpClient from "@effect/platform/HttpClient" +import * as HttpClientError from "@effect/platform/HttpClientError" +import * as HttpClientRequest from "@effect/platform/HttpClientRequest" +import * as HttpClientResponse from "@effect/platform/HttpClientResponse" +import * as Data from "effect/Data" +import * as Effect from "effect/Effect" +import type { ParseError } from "effect/ParseResult" +import * as S from "effect/Schema" + +export class ListOperationsParams extends S.Struct({ + "filter": S.optionalWith(S.String, { nullable: true }), + "pageSize": S.optionalWith(S.Int, { nullable: true }), + "pageToken": S.optionalWith(S.String, { nullable: true }), + "returnPartialSuccess": S.optionalWith(S.Boolean, { nullable: true }) +}) {} + +/** + * The `Status` type defines a logical error model that is suitable for + * different programming environments, including REST APIs and RPC APIs. It is + * used by [gRPC](https://github.com/grpc). Each `Status` message contains + * three pieces of data: error code, error message, and error details. + * + * You can find out more about this error model and how to work with it in the + * [API Design Guide](https://cloud.google.com/apis/design/errors). + */ +export class Status extends S.Class("Status")({ + /** + * The status code, which should be an enum value of google.rpc.Code. + */ + "code": S.optionalWith(S.Int, { nullable: true }), + /** + * A developer-facing error message, which should be in English. Any + * user-facing error message should be localized and sent in the + * google.rpc.Status.details field, or localized by the client. + */ + "message": S.optionalWith(S.String, { nullable: true }), + /** + * A list of messages that carry the error details. There is a common set of + * message types for APIs to use. + */ + "details": S.optionalWith(S.Array(S.Record({ key: S.String, value: S.Unknown })), { nullable: true }) +}) {} + +/** + * This resource represents a long-running operation that is the result of a + * network API call. + */ +export class Operation extends S.Class("Operation")({ + /** + * Service-specific metadata associated with the operation. It typically + * contains progress information and common metadata such as create time. + * Some services might not provide such metadata. Any method that returns a + * long-running operation should document the metadata type, if any. + */ + "metadata": S.optionalWith(S.Record({ key: S.String, value: S.Unknown }), { nullable: true }), + /** + * The normal, successful response of the operation. If the original + * method returns no data on success, such as `Delete`, the response is + * `google.protobuf.Empty`. If the original method is standard + * `Get`/`Create`/`Update`, the response should be the resource. For other + * methods, the response should have the type `XxxResponse`, where `Xxx` + * is the original method name. For example, if the original method name + * is `TakeSnapshot()`, the inferred response type is + * `TakeSnapshotResponse`. + */ + "response": S.optionalWith(S.Record({ key: S.String, value: S.Unknown }), { nullable: true }), + /** + * The server-assigned name, which is only unique within the same service that + * originally returns it. If you use the default HTTP mapping, the + * `name` should be a resource name ending with `operations/{unique_id}`. + */ + "name": S.optionalWith(S.String, { nullable: true }), + /** + * If the value is `false`, it means the operation is still in progress. + * If `true`, the operation is completed, and either `error` or `response` is + * available. + */ + "done": S.optionalWith(S.Boolean, { nullable: true }), + /** + * The error result of the operation in case of failure or cancellation. + */ + "error": S.optionalWith(Status, { nullable: true }) +}) {} + +/** + * The response message for Operations.ListOperations. + */ +export class ListOperationsResponse extends S.Class("ListOperationsResponse")({ + /** + * A list of operations that matches the specified filter in the request. + */ + "operations": S.optionalWith(S.Array(Operation), { nullable: true }), + /** + * The standard List next-page token. + */ + "nextPageToken": S.optionalWith(S.String, { nullable: true }), + /** + * Unordered list. Unreachable resources. Populated when the request sets + * `ListOperationsRequest.return_partial_success` and reads across + * collections. For example, when attempting to list all resources across all + * supported locations. + */ + "unreachable": S.optionalWith(S.Array(S.String), { nullable: true }) +}) {} + +export class ListOperationsByParams extends S.Struct({ + "filter": S.optionalWith(S.String, { nullable: true }), + "pageSize": S.optionalWith(S.Int, { nullable: true }), + "pageToken": S.optionalWith(S.String, { nullable: true }), + "returnPartialSuccess": S.optionalWith(S.Boolean, { nullable: true }) +}) {} + +export class ListOperationsByModelParams extends S.Struct({ + "filter": S.optionalWith(S.String, { nullable: true }), + "pageSize": S.optionalWith(S.Int, { nullable: true }), + "pageToken": S.optionalWith(S.String, { nullable: true }), + "returnPartialSuccess": S.optionalWith(S.Boolean, { nullable: true }) +}) {} + +/** + * A generic empty message that you can re-use to avoid defining duplicated + * empty messages in your APIs. A typical example is to use it as the request + * or the response type of an API method. For instance: + * + * service Foo { + * rpc Bar(google.protobuf.Empty) returns (google.protobuf.Empty); + * } + */ +export class Empty extends S.Record({ key: S.String, value: S.Unknown }) {} + +/** + * Raw media bytes. + * + * Text should not be sent as raw bytes, use the 'text' field. + */ +export class Blob extends S.Class("Blob")({ + /** + * The IANA standard MIME type of the source data. + * Examples: + * - image/png + * - image/jpeg + * If an unsupported MIME type is provided, an error will be returned. For a + * complete list of supported types, see [Supported file + * formats](https://ai.google.dev/gemini-api/docs/prompting_with_media#supported_file_formats). + */ + "mimeType": S.optionalWith(S.String, { nullable: true }), + /** + * Raw bytes for media formats. + */ + "data": S.optionalWith(S.String, { nullable: true }) +}) {} + +/** + * A predicted `FunctionCall` returned from the model that contains + * a string representing the `FunctionDeclaration.name` with the + * arguments and their values. + */ +export class FunctionCall extends S.Class("FunctionCall")({ + /** + * Optional. The unique id of the function call. If populated, the client to execute the + * `function_call` and return the response with the matching `id`. + */ + "id": S.optionalWith(S.String, { nullable: true }), + /** + * Required. The name of the function to call. + * Must be a-z, A-Z, 0-9, or contain underscores and dashes, with a maximum + * length of 64. + */ + "name": S.String, + /** + * Optional. The function parameters and values in JSON object format. + */ + "args": S.optionalWith(S.Record({ key: S.String, value: S.Unknown }), { nullable: true }) +}) {} + +/** + * Raw media bytes for function response. + * + * Text should not be sent as raw bytes, use the 'FunctionResponse.response' + * field. + */ +export class FunctionResponseBlob extends S.Class("FunctionResponseBlob")({ + /** + * The IANA standard MIME type of the source data. + * Examples: + * - image/png + * - image/jpeg + * If an unsupported MIME type is provided, an error will be returned. For a + * complete list of supported types, see [Supported file + * formats](https://ai.google.dev/gemini-api/docs/prompting_with_media#supported_file_formats). + */ + "mimeType": S.optionalWith(S.String, { nullable: true }), + /** + * Raw bytes for media formats. + */ + "data": S.optionalWith(S.String, { nullable: true }) +}) {} + +/** + * A datatype containing media that is part of a `FunctionResponse` message. + * + * A `FunctionResponsePart` consists of data which has an associated datatype. A + * `FunctionResponsePart` can only contain one of the accepted types in + * `FunctionResponsePart.data`. + * + * A `FunctionResponsePart` must have a fixed IANA MIME type identifying the + * type and subtype of the media if the `inline_data` field is filled with raw + * bytes. + */ +export class FunctionResponsePart extends S.Class("FunctionResponsePart")({ + /** + * Inline media bytes. + */ + "inlineData": S.optionalWith(FunctionResponseBlob, { nullable: true }) +}) {} + +/** + * Optional. Specifies how the response should be scheduled in the conversation. + * Only applicable to NON_BLOCKING function calls, is ignored otherwise. + * Defaults to WHEN_IDLE. + */ +export class FunctionResponseScheduling + extends S.Literal("SCHEDULING_UNSPECIFIED", "SILENT", "WHEN_IDLE", "INTERRUPT") +{} + +/** + * The result output from a `FunctionCall` that contains a string + * representing the `FunctionDeclaration.name` and a structured JSON + * object containing any output from the function is used as context to + * the model. This should contain the result of a`FunctionCall` made + * based on model prediction. + */ +export class FunctionResponse extends S.Class("FunctionResponse")({ + /** + * Optional. The id of the function call this response is for. Populated by the client + * to match the corresponding function call `id`. + */ + "id": S.optionalWith(S.String, { nullable: true }), + /** + * Required. The name of the function to call. + * Must be a-z, A-Z, 0-9, or contain underscores and dashes, with a maximum + * length of 64. + */ + "name": S.String, + /** + * Required. The function response in JSON object format. + * Callers can use any keys of their choice that fit the function's syntax + * to return the function output, e.g. "output", "result", etc. + * In particular, if the function call failed to execute, the response can + * have an "error" key to return error details to the model. + */ + "response": S.Record({ key: S.String, value: S.Unknown }), + /** + * Optional. Ordered `Parts` that constitute a function response. Parts may have + * different IANA MIME types. + */ + "parts": S.optionalWith(S.Array(FunctionResponsePart), { nullable: true }), + /** + * Optional. Signals that function call continues, and more responses will be + * returned, turning the function call into a generator. + * Is only applicable to NON_BLOCKING function calls, is ignored otherwise. + * If set to false, future responses will not be considered. + * It is allowed to return empty `response` with `will_continue=False` to + * signal that the function call is finished. This may still trigger the model + * generation. To avoid triggering the generation and finish the function + * call, additionally set `scheduling` to `SILENT`. + */ + "willContinue": S.optionalWith(S.Boolean, { nullable: true }), + /** + * Optional. Specifies how the response should be scheduled in the conversation. + * Only applicable to NON_BLOCKING function calls, is ignored otherwise. + * Defaults to WHEN_IDLE. + */ + "scheduling": S.optionalWith(FunctionResponseScheduling, { nullable: true }) +}) {} + +/** + * URI based data. + */ +export class FileData extends S.Class("FileData")({ + /** + * Optional. The IANA standard MIME type of the source data. + */ + "mimeType": S.optionalWith(S.String, { nullable: true }), + /** + * Required. URI. + */ + "fileUri": S.String +}) {} + +/** + * Required. Programming language of the `code`. + */ +export class ExecutableCodeLanguage extends S.Literal("LANGUAGE_UNSPECIFIED", "PYTHON") {} + +/** + * Code generated by the model that is meant to be executed, and the result + * returned to the model. + * + * Only generated when using the `CodeExecution` tool, in which the code will + * be automatically executed, and a corresponding `CodeExecutionResult` will + * also be generated. + */ +export class ExecutableCode extends S.Class("ExecutableCode")({ + /** + * Required. Programming language of the `code`. + */ + "language": ExecutableCodeLanguage, + /** + * Required. The code to be executed. + */ + "code": S.String +}) {} + +/** + * Required. Outcome of the code execution. + */ +export class CodeExecutionResultOutcome + extends S.Literal("OUTCOME_UNSPECIFIED", "OUTCOME_OK", "OUTCOME_FAILED", "OUTCOME_DEADLINE_EXCEEDED") +{} + +/** + * Result of executing the `ExecutableCode`. + * + * Only generated when using the `CodeExecution`, and always follows a `part` + * containing the `ExecutableCode`. + */ +export class CodeExecutionResult extends S.Class("CodeExecutionResult")({ + /** + * Required. Outcome of the code execution. + */ + "outcome": CodeExecutionResultOutcome, + /** + * Optional. Contains stdout when code execution is successful, stderr or other + * description otherwise. + */ + "output": S.optionalWith(S.String, { nullable: true }) +}) {} + +/** + * Metadata describes the input video content. + */ +export class VideoMetadata extends S.Class("VideoMetadata")({ + /** + * Optional. The start offset of the video. + */ + "startOffset": S.optionalWith(S.String, { nullable: true }), + /** + * Optional. The end offset of the video. + */ + "endOffset": S.optionalWith(S.String, { nullable: true }), + /** + * Optional. The frame rate of the video sent to the model. If not specified, the + * default value will be 1.0. + * The fps range is (0.0, 24.0]. + */ + "fps": S.optionalWith(S.Number, { nullable: true }) +}) {} + +export class MediaResolutionLevel extends S.Literal( + "MEDIA_RESOLUTION_UNSPECIFIED", + "MEDIA_RESOLUTION_LOW", + "MEDIA_RESOLUTION_MEDIUM", + "MEDIA_RESOLUTION_HIGH", + "MEDIA_RESOLUTION_ULTRA_HIGH" +) {} + +/** + * Media resolution for the input media. + */ +export class MediaResolution extends S.Class("MediaResolution")({ + "level": S.optionalWith(MediaResolutionLevel, { nullable: true }) +}) {} + +/** + * A datatype containing media that is part of a multi-part `Content` message. + * + * A `Part` consists of data which has an associated datatype. A `Part` can only + * contain one of the accepted types in `Part.data`. + * + * A `Part` must have a fixed IANA MIME type identifying the type and subtype + * of the media if the `inline_data` field is filled with raw bytes. + */ +export class Part extends S.Class("Part")({ + /** + * Inline text. + */ + "text": S.optionalWith(S.String, { nullable: true }), + /** + * Inline media bytes. + */ + "inlineData": S.optionalWith(Blob, { nullable: true }), + /** + * A predicted `FunctionCall` returned from the model that contains + * a string representing the `FunctionDeclaration.name` with the + * arguments and their values. + */ + "functionCall": S.optionalWith(FunctionCall, { nullable: true }), + /** + * The result output of a `FunctionCall` that contains a string + * representing the `FunctionDeclaration.name` and a structured JSON + * object containing any output from the function is used as context to + * the model. + */ + "functionResponse": S.optionalWith(FunctionResponse, { nullable: true }), + /** + * URI based data. + */ + "fileData": S.optionalWith(FileData, { nullable: true }), + /** + * Code generated by the model that is meant to be executed. + */ + "executableCode": S.optionalWith(ExecutableCode, { nullable: true }), + /** + * Result of executing the `ExecutableCode`. + */ + "codeExecutionResult": S.optionalWith(CodeExecutionResult, { nullable: true }), + /** + * Optional. Video metadata. The metadata should only be specified while the video + * data is presented in inline_data or file_data. + */ + "videoMetadata": S.optionalWith(VideoMetadata, { nullable: true }), + /** + * Optional. Indicates if the part is thought from the model. + */ + "thought": S.optionalWith(S.Boolean, { nullable: true }), + /** + * Optional. An opaque signature for the thought so it can be reused in subsequent + * requests. + */ + "thoughtSignature": S.optionalWith(S.String, { nullable: true }), + /** + * Custom metadata associated with the Part. + * Agents using genai.Part as content representation may need to keep track + * of the additional information. For example it can be name of a file/source + * from which the Part originates or a way to multiplex multiple Part streams. + */ + "partMetadata": S.optionalWith(S.Record({ key: S.String, value: S.Unknown }), { nullable: true }), + /** + * Optional. Media resolution for the input media. + */ + "mediaResolution": S.optionalWith(MediaResolution, { nullable: true }) +}) {} + +/** + * The base structured datatype containing multi-part content of a message. + * + * A `Content` includes a `role` field designating the producer of the `Content` + * and a `parts` field containing multi-part data that contains the content of + * the message turn. + */ +export class Content extends S.Class("Content")({ + /** + * Ordered `Parts` that constitute a single message. Parts may have different + * MIME types. + */ + "parts": S.optionalWith(S.Array(Part), { nullable: true }), + /** + * Optional. The producer of the content. Must be either 'user' or 'model'. + * + * Useful to set for multi-turn conversations, otherwise can be left blank + * or unset. + */ + "role": S.optionalWith(S.String, { nullable: true }) +}) {} + +export class Type + extends S.Literal("TYPE_UNSPECIFIED", "STRING", "NUMBER", "INTEGER", "BOOLEAN", "ARRAY", "OBJECT", "NULL") +{} + +const schemaFields = { + /** + * Optional. Maximum value of the Type.INTEGER and Type.NUMBER + */ + "maximum": S.optionalWith(S.Number, { nullable: true }), + /** + * Optional. Minimum number of the properties for Type.OBJECT. + */ + "minProperties": S.optionalWith(S.String, { nullable: true }), + /** + * Required. Data type. + */ + "type": Type, + /** + * Optional. A brief description of the parameter. This could contain examples of use. + * Parameter description may be formatted as Markdown. + */ + "description": S.optionalWith(S.String, { nullable: true }), + /** + * Optional. Maximum number of the elements for Type.ARRAY. + */ + "maxItems": S.optionalWith(S.String, { nullable: true }), + /** + * Optional. The order of the properties. + * Not a standard field in open api spec. Used to determine the order of the + * properties in the response. + */ + "propertyOrdering": S.optionalWith(S.Array(S.String), { nullable: true }), + /** + * Optional. The format of the data. This is used only for primitive datatypes. + * Supported formats: + * for NUMBER type: float, double + * for INTEGER type: int32, int64 + * for STRING type: enum, date-time + */ + "format": S.optionalWith(S.String, { nullable: true }), + /** + * Optional. Maximum length of the Type.STRING + */ + "maxLength": S.optionalWith(S.String, { nullable: true }), + /** + * Optional. The title of the schema. + */ + "title": S.optionalWith(S.String, { nullable: true }), + /** + * Optional. SCHEMA FIELDS FOR TYPE INTEGER and NUMBER + * Minimum value of the Type.INTEGER and Type.NUMBER + */ + "minimum": S.optionalWith(S.Number, { nullable: true }), + /** + * Optional. Maximum number of the properties for Type.OBJECT. + */ + "maxProperties": S.optionalWith(S.String, { nullable: true }), + /** + * Optional. Possible values of the element of Type.STRING with enum format. + * For example we can define an Enum Direction as : + * {type:STRING, format:enum, enum:["EAST", NORTH", "SOUTH", "WEST"]} + */ + "enum": S.optionalWith(S.Array(S.String), { nullable: true }), + /** + * Optional. Required properties of Type.OBJECT. + */ + "required": S.optionalWith(S.Array(S.String), { nullable: true }), + /** + * Optional. Indicates if the value may be null. + */ + "nullable": S.optionalWith(S.Boolean, { nullable: true }), + /** + * Optional. Properties of Type.OBJECT. + */ + "properties": S.optionalWith(S.Record({ key: S.String, value: S.Unknown }), { nullable: true }), + /** + * Optional. Minimum number of the elements for Type.ARRAY. + */ + "minItems": S.optionalWith(S.String, { nullable: true }), + /** + * Optional. SCHEMA FIELDS FOR TYPE STRING + * Minimum length of the Type.STRING + */ + "minLength": S.optionalWith(S.String, { nullable: true }), + /** + * Optional. Pattern of the Type.STRING to restrict a string to a regular expression. + */ + "pattern": S.optionalWith(S.String, { nullable: true }) +} + +/** + * The \`Schema\` object allows the definition of input and output data types. + * These types can be objects, but also primitives and arrays. + * Represents a select subset of an [OpenAPI 3.0 schema + * object](https://spec.openapis.org/oas/v3.0.3#schema). + */ +export interface SchemaEncoded extends S.Struct.Encoded { + /** + * Optional. The value should be validated against any (one or more) of the subschemas + * in the list. + */ + readonly "anyOf"?: ReadonlyArray | undefined | null + /** + * Optional. Schema of the elements of Type.ARRAY. + */ + readonly "items"?: SchemaEncoded | undefined | null +} + +/** + * The \`Schema\` object allows the definition of input and output data types. + * These types can be objects, but also primitives and arrays. + * Represents a select subset of an [OpenAPI 3.0 schema + * object](https://spec.openapis.org/oas/v3.0.3#schema). + */ +export class Schema extends S.Class("Schema")({ + ...schemaFields, + "anyOf": S.optionalWith(S.Array(S.suspend((): S.Schema => Schema)), { nullable: true }), + /** + * Optional. Schema of the elements of Type.ARRAY. + */ + "items": S.optionalWith(S.suspend((): S.Schema => Schema), { nullable: true }) +}) {} + +/** + * Optional. Specifies the function Behavior. + * Currently only supported by the BidiGenerateContent method. + */ +export class FunctionDeclarationBehavior extends S.Literal("UNSPECIFIED", "BLOCKING", "NON_BLOCKING") {} + +/** + * Structured representation of a function declaration as defined by the + * [OpenAPI 3.03 specification](https://spec.openapis.org/oas/v3.0.3). Included + * in this declaration are the function name and parameters. This + * FunctionDeclaration is a representation of a block of code that can be used + * as a `Tool` by the model and executed by the client. + */ +export class FunctionDeclaration extends S.Class("FunctionDeclaration")({ + /** + * Required. The name of the function. + * Must be a-z, A-Z, 0-9, or contain underscores, colons, dots, and dashes, + * with a maximum length of 64. + */ + "name": S.String, + /** + * Required. A brief description of the function. + */ + "description": S.String, + /** + * Optional. Describes the parameters to this function. Reflects the Open API 3.03 + * Parameter Object string Key: the name of the parameter. Parameter names are + * case sensitive. Schema Value: the Schema defining the type used for the + * parameter. + */ + "parameters": S.optionalWith(Schema, { nullable: true }), + /** + * Optional. Describes the output from this function in JSON Schema format. Reflects the + * Open API 3.03 Response Object. The Schema defines the type used for the + * response value of the function. + */ + "response": S.optionalWith(Schema, { nullable: true }), + /** + * Optional. Specifies the function Behavior. + * Currently only supported by the BidiGenerateContent method. + */ + "behavior": S.optionalWith(FunctionDeclarationBehavior, { nullable: true }) +}) {} + +/** + * The mode of the predictor to be used in dynamic retrieval. + */ +export class DynamicRetrievalConfigMode extends S.Literal("MODE_UNSPECIFIED", "MODE_DYNAMIC") {} + +/** + * Describes the options to customize dynamic retrieval. + */ +export class DynamicRetrievalConfig extends S.Class("DynamicRetrievalConfig")({ + /** + * The mode of the predictor to be used in dynamic retrieval. + */ + "mode": S.optionalWith(DynamicRetrievalConfigMode, { nullable: true }), + /** + * The threshold to be used in dynamic retrieval. + * If not set, a system default value is used. + */ + "dynamicThreshold": S.optionalWith(S.Number, { nullable: true }) +}) {} + +/** + * Tool to retrieve public web data for grounding, powered by Google. + */ +export class GoogleSearchRetrieval extends S.Class("GoogleSearchRetrieval")({ + /** + * Specifies the dynamic retrieval configuration for the given source. + */ + "dynamicRetrievalConfig": S.optionalWith(DynamicRetrievalConfig, { nullable: true }) +}) {} + +/** + * Tool that executes code generated by the model, and automatically returns + * the result to the model. + * + * See also `ExecutableCode` and `CodeExecutionResult` which are only generated + * when using this tool. + */ +export class CodeExecution extends S.Record({ key: S.String, value: S.Unknown }) {} + +/** + * Represents a time interval, encoded as a Timestamp start (inclusive) and a + * Timestamp end (exclusive). + * + * The start must be less than or equal to the end. + * When the start equals the end, the interval is empty (matches no time). + * When both start and end are unspecified, the interval matches any time. + */ +export class Interval extends S.Class("Interval")({ + /** + * Optional. Inclusive start of the interval. + * + * If specified, a Timestamp matching this interval will have to be the same + * or after the start. + */ + "startTime": S.optionalWith(S.String, { nullable: true }), + /** + * Optional. Exclusive end of the interval. + * + * If specified, a Timestamp matching this interval will have to be before the + * end. + */ + "endTime": S.optionalWith(S.String, { nullable: true }) +}) {} + +/** + * GoogleSearch tool type. + * Tool to support Google Search in Model. Powered by Google. + */ +export class GoogleSearch extends S.Class("GoogleSearch")({ + /** + * Optional. Filter search results to a specific time range. + * If customers set a start time, they must set an end time (and vice + * versa). + */ + "timeRangeFilter": S.optionalWith(Interval, { nullable: true }) +}) {} + +/** + * Required. The environment being operated. + */ +export class ComputerUseEnvironment extends S.Literal("ENVIRONMENT_UNSPECIFIED", "ENVIRONMENT_BROWSER") {} + +/** + * Computer Use tool type. + */ +export class ComputerUse extends S.Class("ComputerUse")({ + /** + * Required. The environment being operated. + */ + "environment": ComputerUseEnvironment, + /** + * Optional. By default, predefined functions are included in the final model + * call. + * Some of them can be explicitly excluded from being automatically + * included. This can serve two purposes: + * 1. Using a more restricted / different action space. + * 2. Improving the definitions / instructions of predefined functions. + */ + "excludedPredefinedFunctions": S.optionalWith(S.Array(S.String), { nullable: true }) +}) {} + +/** + * Tool to support URL context retrieval. + */ +export class UrlContext extends S.Record({ key: S.String, value: S.Unknown }) {} + +/** + * The FileSearch tool that retrieves knowledge from Semantic Retrieval corpora. + * Files are imported to Semantic Retrieval corpora using the ImportFile API. + */ +export class FileSearch extends S.Class("FileSearch")({ + /** + * Required. The names of the file_search_stores to retrieve from. + * Example: `fileSearchStores/my-file-search-store-123` + */ + "fileSearchStoreNames": S.Array(S.String), + /** + * Optional. The number of semantic retrieval chunks to retrieve. + */ + "topK": S.optionalWith(S.Int, { nullable: true }), + /** + * Optional. Metadata filter to apply to the semantic retrieval documents and chunks. + */ + "metadataFilter": S.optionalWith(S.String, { nullable: true }) +}) {} + +/** + * A transport that can stream HTTP requests and responses. + * Next ID: 6 + */ +export class StreamableHttpTransport extends S.Class("StreamableHttpTransport")({ + /** + * The full URL for the MCPServer endpoint. + * Example: "https://api.example.com/mcp" + */ + "url": S.optionalWith(S.String, { nullable: true }), + /** + * Optional: Fields for authentication headers, timeouts, etc., if needed. + */ + "headers": S.optionalWith(S.Record({ key: S.String, value: S.Unknown }), { nullable: true }), + /** + * HTTP timeout for regular operations. + */ + "timeout": S.optionalWith(S.String, { nullable: true }), + /** + * Timeout for SSE read operations. + */ + "sseReadTimeout": S.optionalWith(S.String, { nullable: true }), + /** + * Whether to close the client session when the transport closes. + */ + "terminateOnClose": S.optionalWith(S.Boolean, { nullable: true }) +}) {} + +/** + * A MCPServer is a server that can be called by the model to perform actions. + * It is a server that implements the MCP protocol. + * Next ID: 5 + */ +export class McpServer extends S.Class("McpServer")({ + /** + * A transport that can stream HTTP requests and responses. + */ + "streamableHttpTransport": S.optionalWith(StreamableHttpTransport, { nullable: true }), + /** + * The name of the MCPServer. + */ + "name": S.optionalWith(S.String, { nullable: true }) +}) {} + +/** + * The GoogleMaps Tool that provides geospatial context for the user's query. + */ +export class GoogleMaps extends S.Class("GoogleMaps")({ + /** + * Optional. Whether to return a widget context token in the GroundingMetadata of the + * response. Developers can use the widget context token to render a Google + * Maps widget with geospatial context related to the places that the model + * references in the response. + */ + "enableWidget": S.optionalWith(S.Boolean, { nullable: true }) +}) {} + +/** + * Tool details that the model may use to generate response. + * + * A `Tool` is a piece of code that enables the system to interact with + * external systems to perform an action, or set of actions, outside of + * knowledge and scope of the model. + * + * Next ID: 14 + */ +export class Tool extends S.Class("Tool")({ + /** + * Optional. A list of `FunctionDeclarations` available to the model that can be used + * for function calling. + * + * The model or system does not execute the function. Instead the defined + * function may be returned as a FunctionCall + * with arguments to the client side for execution. The model may decide to + * call a subset of these functions by populating + * FunctionCall in the response. The next + * conversation turn may contain a + * FunctionResponse + * with the Content.role "function" generation context for the next model + * turn. + */ + "functionDeclarations": S.optionalWith(S.Array(FunctionDeclaration), { nullable: true }), + /** + * Optional. Retrieval tool that is powered by Google search. + */ + "googleSearchRetrieval": S.optionalWith(GoogleSearchRetrieval, { nullable: true }), + /** + * Optional. Enables the model to execute code as part of generation. + */ + "codeExecution": S.optionalWith(CodeExecution, { nullable: true }), + /** + * Optional. GoogleSearch tool type. + * Tool to support Google Search in Model. Powered by Google. + */ + "googleSearch": S.optionalWith(GoogleSearch, { nullable: true }), + /** + * Optional. Tool to support the model interacting directly with the computer. + * If enabled, it automatically populates computer-use specific Function + * Declarations. + */ + "computerUse": S.optionalWith(ComputerUse, { nullable: true }), + /** + * Optional. Tool to support URL context retrieval. + */ + "urlContext": S.optionalWith(UrlContext, { nullable: true }), + /** + * Optional. FileSearch tool type. + * Tool to retrieve knowledge from Semantic Retrieval corpora. + */ + "fileSearch": S.optionalWith(FileSearch, { nullable: true }), + /** + * Optional. MCP Servers to connect to. + */ + "mcpServers": S.optionalWith(S.Array(McpServer), { nullable: true }), + /** + * Optional. Tool that allows grounding the model's response with geospatial context + * related to the user's query. + */ + "googleMaps": S.optionalWith(GoogleMaps, { nullable: true }) +}) {} + +/** + * Optional. Specifies the mode in which function calling should execute. If + * unspecified, the default value will be set to AUTO. + */ +export class FunctionCallingConfigMode extends S.Literal("MODE_UNSPECIFIED", "AUTO", "ANY", "NONE", "VALIDATED") {} + +/** + * Configuration for specifying function calling behavior. + */ +export class FunctionCallingConfig extends S.Class("FunctionCallingConfig")({ + /** + * Optional. Specifies the mode in which function calling should execute. If + * unspecified, the default value will be set to AUTO. + */ + "mode": S.optionalWith(FunctionCallingConfigMode, { nullable: true }), + /** + * Optional. A set of function names that, when provided, limits the functions the model + * will call. + * + * This should only be set when the Mode is ANY or VALIDATED. Function names + * should match [FunctionDeclaration.name]. When set, model will + * predict a function call from only allowed function names. + */ + "allowedFunctionNames": S.optionalWith(S.Array(S.String), { nullable: true }) +}) {} + +/** + * An object that represents a latitude/longitude pair. This is expressed as a + * pair of doubles to represent degrees latitude and degrees longitude. Unless + * specified otherwise, this object must conform to the + * WGS84 standard. Values must be within normalized ranges. + */ +export class LatLng extends S.Class("LatLng")({ + /** + * The latitude in degrees. It must be in the range [-90.0, +90.0]. + */ + "latitude": S.optionalWith(S.Number, { nullable: true }), + /** + * The longitude in degrees. It must be in the range [-180.0, +180.0]. + */ + "longitude": S.optionalWith(S.Number, { nullable: true }) +}) {} + +/** + * Retrieval config. + */ +export class RetrievalConfig extends S.Class("RetrievalConfig")({ + /** + * Optional. The location of the user. + */ + "latLng": S.optionalWith(LatLng, { nullable: true }), + /** + * Optional. The language code of the user. + * Language code for content. Use language tags defined by + * [BCP47](https://www.rfc-editor.org/rfc/bcp/bcp47.txt). + */ + "languageCode": S.optionalWith(S.String, { nullable: true }) +}) {} + +/** + * The Tool configuration containing parameters for specifying `Tool` use + * in the request. + */ +export class ToolConfig extends S.Class("ToolConfig")({ + /** + * Optional. Function calling config. + */ + "functionCallingConfig": S.optionalWith(FunctionCallingConfig, { nullable: true }), + /** + * Optional. Retrieval config. + */ + "retrievalConfig": S.optionalWith(RetrievalConfig, { nullable: true }) +}) {} + +export class HarmCategory extends S.Literal( + "HARM_CATEGORY_UNSPECIFIED", + "HARM_CATEGORY_DEROGATORY", + "HARM_CATEGORY_TOXICITY", + "HARM_CATEGORY_VIOLENCE", + "HARM_CATEGORY_SEXUAL", + "HARM_CATEGORY_MEDICAL", + "HARM_CATEGORY_DANGEROUS", + "HARM_CATEGORY_HARASSMENT", + "HARM_CATEGORY_HATE_SPEECH", + "HARM_CATEGORY_SEXUALLY_EXPLICIT", + "HARM_CATEGORY_DANGEROUS_CONTENT", + "HARM_CATEGORY_CIVIC_INTEGRITY" +) {} + +/** + * Required. Controls the probability threshold at which harm is blocked. + */ +export class SafetySettingThreshold extends S.Literal( + "HARM_BLOCK_THRESHOLD_UNSPECIFIED", + "BLOCK_LOW_AND_ABOVE", + "BLOCK_MEDIUM_AND_ABOVE", + "BLOCK_ONLY_HIGH", + "BLOCK_NONE", + "OFF" +) {} + +/** + * Safety setting, affecting the safety-blocking behavior. + * + * Passing a safety setting for a category changes the allowed probability that + * content is blocked. + */ +export class SafetySetting extends S.Class("SafetySetting")({ + /** + * Required. The category for this setting. + */ + "category": HarmCategory, + /** + * Required. Controls the probability threshold at which harm is blocked. + */ + "threshold": SafetySettingThreshold +}) {} + +/** + * The configuration for the prebuilt speaker to use. + */ +export class PrebuiltVoiceConfig extends S.Class("PrebuiltVoiceConfig")({ + /** + * The name of the preset voice to use. + */ + "voiceName": S.optionalWith(S.String, { nullable: true }) +}) {} + +/** + * The configuration for the voice to use. + */ +export class VoiceConfig extends S.Class("VoiceConfig")({ + /** + * The configuration for the prebuilt voice to use. + */ + "prebuiltVoiceConfig": S.optionalWith(PrebuiltVoiceConfig, { nullable: true }) +}) {} + +/** + * The configuration for a single speaker in a multi speaker setup. + */ +export class SpeakerVoiceConfig extends S.Class("SpeakerVoiceConfig")({ + /** + * Required. The name of the speaker to use. Should be the same as in the prompt. + */ + "speaker": S.String, + /** + * Required. The configuration for the voice to use. + */ + "voiceConfig": VoiceConfig +}) {} + +/** + * The configuration for the multi-speaker setup. + */ +export class MultiSpeakerVoiceConfig extends S.Class("MultiSpeakerVoiceConfig")({ + /** + * Required. All the enabled speaker voices. + */ + "speakerVoiceConfigs": S.Array(SpeakerVoiceConfig) +}) {} + +/** + * The speech generation config. + */ +export class SpeechConfig extends S.Class("SpeechConfig")({ + /** + * The configuration in case of single-voice output. + */ + "voiceConfig": S.optionalWith(VoiceConfig, { nullable: true }), + /** + * Optional. The configuration for the multi-speaker setup. + * It is mutually exclusive with the voice_config field. + */ + "multiSpeakerVoiceConfig": S.optionalWith(MultiSpeakerVoiceConfig, { nullable: true }), + /** + * Optional. Language code (in BCP 47 format, e.g. "en-US") for speech synthesis. + * + * Valid values are: de-DE, en-AU, en-GB, en-IN, en-US, es-US, fr-FR, hi-IN, + * pt-BR, ar-XA, es-ES, fr-CA, id-ID, it-IT, ja-JP, tr-TR, vi-VN, bn-IN, + * gu-IN, kn-IN, ml-IN, mr-IN, ta-IN, te-IN, nl-NL, ko-KR, cmn-CN, pl-PL, + * ru-RU, and th-TH. + */ + "languageCode": S.optionalWith(S.String, { nullable: true }) +}) {} + +/** + * Optional. Controls the maximum depth of the model's internal reasoning process before + * it produces a response. If not specified, the default is HIGH. Recommended + * for Gemini 3 or later models. Use with earlier models results in an error. + */ +export class ThinkingConfigThinkingLevel + extends S.Literal("THINKING_LEVEL_UNSPECIFIED", "MINIMAL", "LOW", "MEDIUM", "HIGH") +{} + +/** + * Config for thinking features. + */ +export class ThinkingConfig extends S.Class("ThinkingConfig")({ + /** + * Indicates whether to include thoughts in the response. + * If true, thoughts are returned only when available. + */ + "includeThoughts": S.optionalWith(S.Boolean, { nullable: true }), + /** + * The number of thoughts tokens that the model should generate. + */ + "thinkingBudget": S.optionalWith(S.Int, { nullable: true }), + /** + * Optional. Controls the maximum depth of the model's internal reasoning process before + * it produces a response. If not specified, the default is HIGH. Recommended + * for Gemini 3 or later models. Use with earlier models results in an error. + */ + "thinkingLevel": S.optionalWith(ThinkingConfigThinkingLevel, { nullable: true }) +}) {} + +/** + * Config for image generation features. + */ +export class ImageConfig extends S.Class("ImageConfig")({ + /** + * Optional. The aspect ratio of the image to generate. Supported aspect ratios: 1:1, + * 2:3, 3:2, 3:4, 4:3, 9:16, 16:9, 21:9. + * + * If not specified, the model will choose a default aspect ratio based on any + * reference images provided. + */ + "aspectRatio": S.optionalWith(S.String, { nullable: true }), + /** + * Optional. Specifies the size of generated images. Supported values are `1K`, `2K`, + * `4K`. If not specified, the model will use default value `1K`. + */ + "imageSize": S.optionalWith(S.String, { nullable: true }) +}) {} + +/** + * Optional. If specified, the media resolution specified will be used. + */ +export class GenerationConfigMediaResolution extends S.Literal( + "MEDIA_RESOLUTION_UNSPECIFIED", + "MEDIA_RESOLUTION_LOW", + "MEDIA_RESOLUTION_MEDIUM", + "MEDIA_RESOLUTION_HIGH" +) {} + +/** + * Configuration options for model generation and outputs. Not all parameters + * are configurable for every model. + */ +export class GenerationConfig extends S.Class("GenerationConfig")({ + /** + * Optional. Number of generated responses to return. If unset, this will default + * to 1. Please note that this doesn't work for previous generation + * models (Gemini 1.0 family) + */ + "candidateCount": S.optionalWith(S.Int, { nullable: true }), + /** + * Optional. The set of character sequences (up to 5) that will stop output generation. + * If specified, the API will stop at the first appearance of a + * `stop_sequence`. The stop sequence will not be included as part of the + * response. + */ + "stopSequences": S.optionalWith(S.Array(S.String), { nullable: true }), + /** + * Optional. The maximum number of tokens to include in a response candidate. + * + * Note: The default value varies by model, see the `Model.output_token_limit` + * attribute of the `Model` returned from the `getModel` function. + */ + "maxOutputTokens": S.optionalWith(S.Int, { nullable: true }), + /** + * Optional. Controls the randomness of the output. + * + * Note: The default value varies by model, see the `Model.temperature` + * attribute of the `Model` returned from the `getModel` function. + * + * Values can range from [0.0, 2.0]. + */ + "temperature": S.optionalWith(S.Number, { nullable: true }), + /** + * Optional. The maximum cumulative probability of tokens to consider when sampling. + * + * The model uses combined Top-k and Top-p (nucleus) sampling. + * + * Tokens are sorted based on their assigned probabilities so that only the + * most likely tokens are considered. Top-k sampling directly limits the + * maximum number of tokens to consider, while Nucleus sampling limits the + * number of tokens based on the cumulative probability. + * + * Note: The default value varies by `Model` and is specified by + * the`Model.top_p` attribute returned from the `getModel` function. An empty + * `top_k` attribute indicates that the model doesn't apply top-k sampling + * and doesn't allow setting `top_k` on requests. + */ + "topP": S.optionalWith(S.Number, { nullable: true }), + /** + * Optional. The maximum number of tokens to consider when sampling. + * + * Gemini models use Top-p (nucleus) sampling or a combination of Top-k and + * nucleus sampling. Top-k sampling considers the set of `top_k` most probable + * tokens. Models running with nucleus sampling don't allow top_k setting. + * + * Note: The default value varies by `Model` and is specified by + * the`Model.top_p` attribute returned from the `getModel` function. An empty + * `top_k` attribute indicates that the model doesn't apply top-k sampling + * and doesn't allow setting `top_k` on requests. + */ + "topK": S.optionalWith(S.Int, { nullable: true }), + /** + * Optional. Seed used in decoding. If not set, the request uses a randomly generated + * seed. + */ + "seed": S.optionalWith(S.Int, { nullable: true }), + /** + * Optional. MIME type of the generated candidate text. + * Supported MIME types are: + * `text/plain`: (default) Text output. + * `application/json`: JSON response in the response candidates. + * `text/x.enum`: ENUM as a string response in the response candidates. + * Refer to the + * [docs](https://ai.google.dev/gemini-api/docs/prompting_with_media#plain_text_formats) + * for a list of all supported text MIME types. + */ + "responseMimeType": S.optionalWith(S.String, { nullable: true }), + /** + * Optional. Output schema of the generated candidate text. Schemas must be a + * subset of the [OpenAPI schema](https://spec.openapis.org/oas/v3.0.3#schema) + * and can be objects, primitives or arrays. + * + * If set, a compatible `response_mime_type` must also be set. + * Compatible MIME types: + * `application/json`: Schema for JSON response. + * Refer to the [JSON text generation + * guide](https://ai.google.dev/gemini-api/docs/json-mode) for more details. + */ + "responseSchema": S.optionalWith(Schema, { nullable: true }), + /** + * Optional. Presence penalty applied to the next token's logprobs if the token has + * already been seen in the response. + * + * This penalty is binary on/off and not dependant on the number of times the + * token is used (after the first). Use + * frequency_penalty + * for a penalty that increases with each use. + * + * A positive penalty will discourage the use of tokens that have already + * been used in the response, increasing the vocabulary. + * + * A negative penalty will encourage the use of tokens that have already been + * used in the response, decreasing the vocabulary. + */ + "presencePenalty": S.optionalWith(S.Number, { nullable: true }), + /** + * Optional. Frequency penalty applied to the next token's logprobs, multiplied by the + * number of times each token has been seen in the respponse so far. + * + * A positive penalty will discourage the use of tokens that have already + * been used, proportional to the number of times the token has been used: + * The more a token is used, the more difficult it is for the model to use + * that token again increasing the vocabulary of responses. + * + * Caution: A _negative_ penalty will encourage the model to reuse tokens + * proportional to the number of times the token has been used. Small + * negative values will reduce the vocabulary of a response. Larger negative + * values will cause the model to start repeating a common token until it + * hits the max_output_tokens + * limit. + */ + "frequencyPenalty": S.optionalWith(S.Number, { nullable: true }), + /** + * Optional. If true, export the logprobs results in response. + */ + "responseLogprobs": S.optionalWith(S.Boolean, { nullable: true }), + /** + * Optional. Only valid if response_logprobs=True. + * This sets the number of top logprobs to return at each decoding step in the + * Candidate.logprobs_result. The number must be in the range of [0, 20]. + */ + "logprobs": S.optionalWith(S.Int, { nullable: true }), + /** + * Optional. Enables enhanced civic answers. It may not be available for all models. + */ + "enableEnhancedCivicAnswers": S.optionalWith(S.Boolean, { nullable: true }), + /** + * Optional. The requested modalities of the response. Represents the set of modalities + * that the model can return, and should be expected in the response. This is + * an exact match to the modalities of the response. + * + * A model may have multiple combinations of supported modalities. If the + * requested modalities do not match any of the supported combinations, an + * error will be returned. + * + * An empty list is equivalent to requesting only text. + */ + "responseModalities": S.optionalWith(S.Array(S.Literal("MODALITY_UNSPECIFIED", "TEXT", "IMAGE", "AUDIO")), { + nullable: true + }), + /** + * Optional. The speech generation config. + */ + "speechConfig": S.optionalWith(SpeechConfig, { nullable: true }), + /** + * Optional. Config for thinking features. + * An error will be returned if this field is set for models that don't + * support thinking. + */ + "thinkingConfig": S.optionalWith(ThinkingConfig, { nullable: true }), + /** + * Optional. Config for image generation. + * An error will be returned if this field is set for models that don't + * support these config options. + */ + "imageConfig": S.optionalWith(ImageConfig, { nullable: true }), + /** + * Optional. If specified, the media resolution specified will be used. + */ + "mediaResolution": S.optionalWith(GenerationConfigMediaResolution, { nullable: true }) +}) {} + +/** + * Request to generate a completion from the model. + */ +export class GenerateContentRequest extends S.Class("GenerateContentRequest")({ + /** + * Required. The name of the `Model` to use for generating the completion. + * + * Format: `models/{model}`. + */ + "model": S.String, + /** + * Optional. Developer set [system + * instruction(s)](https://ai.google.dev/gemini-api/docs/system-instructions). + * Currently, text only. + */ + "systemInstruction": S.optionalWith(Content, { nullable: true }), + /** + * Required. The content of the current conversation with the model. + * + * For single-turn queries, this is a single instance. For multi-turn queries + * like [chat](https://ai.google.dev/gemini-api/docs/text-generation#chat), + * this is a repeated field that contains the conversation history and the + * latest request. + */ + "contents": S.Array(Content), + /** + * Optional. A list of `Tools` the `Model` may use to generate the next response. + * + * A `Tool` is a piece of code that enables the system to interact with + * external systems to perform an action, or set of actions, outside of + * knowledge and scope of the `Model`. Supported `Tool`s are `Function` and + * `code_execution`. Refer to the [Function + * calling](https://ai.google.dev/gemini-api/docs/function-calling) and the + * [Code execution](https://ai.google.dev/gemini-api/docs/code-execution) + * guides to learn more. + */ + "tools": S.optionalWith(S.Array(Tool), { nullable: true }), + /** + * Optional. Tool configuration for any `Tool` specified in the request. Refer to the + * [Function calling + * guide](https://ai.google.dev/gemini-api/docs/function-calling#function_calling_mode) + * for a usage example. + */ + "toolConfig": S.optionalWith(ToolConfig, { nullable: true }), + /** + * Optional. A list of unique `SafetySetting` instances for blocking unsafe content. + * + * This will be enforced on the `GenerateContentRequest.contents` and + * `GenerateContentResponse.candidates`. There should not be more than one + * setting for each `SafetyCategory` type. The API will block any contents and + * responses that fail to meet the thresholds set by these settings. This list + * overrides the default settings for each `SafetyCategory` specified in the + * safety_settings. If there is no `SafetySetting` for a given + * `SafetyCategory` provided in the list, the API will use the default safety + * setting for that category. Harm categories HARM_CATEGORY_HATE_SPEECH, + * HARM_CATEGORY_SEXUALLY_EXPLICIT, HARM_CATEGORY_DANGEROUS_CONTENT, + * HARM_CATEGORY_HARASSMENT, HARM_CATEGORY_CIVIC_INTEGRITY are supported. + * Refer to the [guide](https://ai.google.dev/gemini-api/docs/safety-settings) + * for detailed information on available safety settings. Also refer to the + * [Safety guidance](https://ai.google.dev/gemini-api/docs/safety-guidance) to + * learn how to incorporate safety considerations in your AI applications. + */ + "safetySettings": S.optionalWith(S.Array(SafetySetting), { nullable: true }), + /** + * Optional. Configuration options for model generation and outputs. + */ + "generationConfig": S.optionalWith(GenerationConfig, { nullable: true }), + /** + * Optional. The name of the content + * [cached](https://ai.google.dev/gemini-api/docs/caching) to use as context + * to serve the prediction. Format: `cachedContents/{cachedContent}` + */ + "cachedContent": S.optionalWith(S.String, { nullable: true }) +}) {} + +/** + * Optional. Output only. The reason why the model stopped generating tokens. + * + * If empty, the model has not stopped generating tokens. + */ +export class CandidateFinishReason extends S.Literal( + "FINISH_REASON_UNSPECIFIED", + "STOP", + "MAX_TOKENS", + "SAFETY", + "RECITATION", + "LANGUAGE", + "OTHER", + "BLOCKLIST", + "PROHIBITED_CONTENT", + "SPII", + "MALFORMED_FUNCTION_CALL", + "IMAGE_SAFETY", + "IMAGE_PROHIBITED_CONTENT", + "IMAGE_OTHER", + "NO_IMAGE", + "IMAGE_RECITATION", + "UNEXPECTED_TOOL_CALL", + "TOO_MANY_TOOL_CALLS", + "MISSING_THOUGHT_SIGNATURE" +) {} + +/** + * Required. The probability of harm for this content. + */ +export class SafetyRatingProbability + extends S.Literal("HARM_PROBABILITY_UNSPECIFIED", "NEGLIGIBLE", "LOW", "MEDIUM", "HIGH") +{} + +/** + * Safety rating for a piece of content. + * + * The safety rating contains the category of harm and the + * harm probability level in that category for a piece of content. + * Content is classified for safety across a number of + * harm categories and the probability of the harm classification is included + * here. + */ +export class SafetyRating extends S.Class("SafetyRating")({ + /** + * Required. The category for this rating. + */ + "category": HarmCategory, + /** + * Required. The probability of harm for this content. + */ + "probability": SafetyRatingProbability, + /** + * Was this content blocked because of this rating? + */ + "blocked": S.optionalWith(S.Boolean, { nullable: true }) +}) {} + +/** + * A citation to a source for a portion of a specific response. + */ +export class CitationSource extends S.Class("CitationSource")({ + /** + * Optional. Start of segment of the response that is attributed to this source. + * + * Index indicates the start of the segment, measured in bytes. + */ + "startIndex": S.optionalWith(S.Int, { nullable: true }), + /** + * Optional. End of the attributed segment, exclusive. + */ + "endIndex": S.optionalWith(S.Int, { nullable: true }), + /** + * Optional. URI that is attributed as a source for a portion of the text. + */ + "uri": S.optionalWith(S.String, { nullable: true }), + /** + * Optional. License for the GitHub project that is attributed as a source for segment. + * + * License info is required for code citations. + */ + "license": S.optionalWith(S.String, { nullable: true }) +}) {} + +/** + * A collection of source attributions for a piece of content. + */ +export class CitationMetadata extends S.Class("CitationMetadata")({ + /** + * Citations to sources for a specific response. + */ + "citationSources": S.optionalWith(S.Array(CitationSource), { nullable: true }) +}) {} + +/** + * Identifier for a part within a `GroundingPassage`. + */ +export class GroundingPassageId extends S.Class("GroundingPassageId")({ + /** + * Output only. ID of the passage matching the `GenerateAnswerRequest`'s + * `GroundingPassage.id`. + */ + "passageId": S.optionalWith(S.String, { nullable: true }), + /** + * Output only. Index of the part within the `GenerateAnswerRequest`'s + * `GroundingPassage.content`. + */ + "partIndex": S.optionalWith(S.Int, { nullable: true }) +}) {} + +/** + * Identifier for a `Chunk` retrieved via Semantic Retriever specified in the + * `GenerateAnswerRequest` using `SemanticRetrieverConfig`. + */ +export class SemanticRetrieverChunk extends S.Class("SemanticRetrieverChunk")({ + /** + * Output only. Name of the source matching the request's + * `SemanticRetrieverConfig.source`. Example: `corpora/123` or + * `corpora/123/documents/abc` + */ + "source": S.optionalWith(S.String, { nullable: true }), + /** + * Output only. Name of the `Chunk` containing the attributed text. + * Example: `corpora/123/documents/abc/chunks/xyz` + */ + "chunk": S.optionalWith(S.String, { nullable: true }) +}) {} + +/** + * Identifier for the source contributing to this attribution. + */ +export class AttributionSourceId extends S.Class("AttributionSourceId")({ + /** + * Identifier for an inline passage. + */ + "groundingPassage": S.optionalWith(GroundingPassageId, { nullable: true }), + /** + * Identifier for a `Chunk` fetched via Semantic Retriever. + */ + "semanticRetrieverChunk": S.optionalWith(SemanticRetrieverChunk, { nullable: true }) +}) {} + +/** + * Attribution for a source that contributed to an answer. + */ +export class GroundingAttribution extends S.Class("GroundingAttribution")({ + /** + * Output only. Identifier for the source contributing to this attribution. + */ + "sourceId": S.optionalWith(AttributionSourceId, { nullable: true }), + /** + * Grounding source content that makes up this attribution. + */ + "content": S.optionalWith(Content, { nullable: true }) +}) {} + +/** + * Google search entry point. + */ +export class SearchEntryPoint extends S.Class("SearchEntryPoint")({ + /** + * Optional. Web content snippet that can be embedded in a web page or an app webview. + */ + "renderedContent": S.optionalWith(S.String, { nullable: true }), + /** + * Optional. Base64 encoded JSON representing array of tuple. + */ + "sdkBlob": S.optionalWith(S.String, { nullable: true }) +}) {} + +/** + * Chunk from the web. + */ +export class Web extends S.Class("Web")({ + /** + * URI reference of the chunk. + */ + "uri": S.optionalWith(S.String, { nullable: true }), + /** + * Title of the chunk. + */ + "title": S.optionalWith(S.String, { nullable: true }) +}) {} + +/** + * Chunk from context retrieved by the file search tool. + */ +export class RetrievedContext extends S.Class("RetrievedContext")({ + /** + * Optional. URI reference of the semantic retrieval document. + */ + "uri": S.optionalWith(S.String, { nullable: true }), + /** + * Optional. Title of the document. + */ + "title": S.optionalWith(S.String, { nullable: true }), + /** + * Optional. Text of the chunk. + */ + "text": S.optionalWith(S.String, { nullable: true }), + /** + * Optional. Name of the `FileSearchStore` containing the document. + * Example: `fileSearchStores/123` + */ + "fileSearchStore": S.optionalWith(S.String, { nullable: true }) +}) {} + +/** + * Encapsulates a snippet of a user review that answers a question about + * the features of a specific place in Google Maps. + */ +export class ReviewSnippet extends S.Class("ReviewSnippet")({ + /** + * The ID of the review snippet. + */ + "reviewId": S.optionalWith(S.String, { nullable: true }), + /** + * A link that corresponds to the user review on Google Maps. + */ + "googleMapsUri": S.optionalWith(S.String, { nullable: true }), + /** + * Title of the review. + */ + "title": S.optionalWith(S.String, { nullable: true }) +}) {} + +/** + * Collection of sources that provide answers about the features of a given + * place in Google Maps. Each PlaceAnswerSources message corresponds to a + * specific place in Google Maps. The Google Maps tool used these sources in + * order to answer questions about features of the place (e.g: "does Bar Foo + * have Wifi" or "is Foo Bar wheelchair accessible?"). Currently we only + * support review snippets as sources. + */ +export class PlaceAnswerSources extends S.Class("PlaceAnswerSources")({ + /** + * Snippets of reviews that are used to generate answers about the + * features of a given place in Google Maps. + */ + "reviewSnippets": S.optionalWith(S.Array(ReviewSnippet), { nullable: true }) +}) {} + +/** + * A grounding chunk from Google Maps. A Maps chunk corresponds to a single + * place. + */ +export class Maps extends S.Class("Maps")({ + /** + * URI reference of the place. + */ + "uri": S.optionalWith(S.String, { nullable: true }), + /** + * Title of the place. + */ + "title": S.optionalWith(S.String, { nullable: true }), + /** + * Text description of the place answer. + */ + "text": S.optionalWith(S.String, { nullable: true }), + /** + * This ID of the place, in `places/{place_id}` format. A user can use this + * ID to look up that place. + */ + "placeId": S.optionalWith(S.String, { nullable: true }), + /** + * Sources that provide answers about the features of a given place in + * Google Maps. + */ + "placeAnswerSources": S.optionalWith(PlaceAnswerSources, { nullable: true }) +}) {} + +/** + * Grounding chunk. + */ +export class GroundingChunk extends S.Class("GroundingChunk")({ + /** + * Grounding chunk from the web. + */ + "web": S.optionalWith(Web, { nullable: true }), + /** + * Optional. Grounding chunk from context retrieved by the file search tool. + */ + "retrievedContext": S.optionalWith(RetrievedContext, { nullable: true }), + /** + * Optional. Grounding chunk from Google Maps. + */ + "maps": S.optionalWith(Maps, { nullable: true }) +}) {} + +/** + * Segment of the content. + */ +export class GoogleAiGenerativelanguageV1BetaSegment + extends S.Class("GoogleAiGenerativelanguageV1BetaSegment")({ + /** + * The index of a Part object within its parent Content object. + */ + "partIndex": S.optionalWith(S.Int, { nullable: true }), + /** + * Start index in the given Part, measured in bytes. Offset from the start of + * the Part, inclusive, starting at zero. + */ + "startIndex": S.optionalWith(S.Int, { nullable: true }), + /** + * End index in the given Part, measured in bytes. Offset from the start of + * the Part, exclusive, starting at zero. + */ + "endIndex": S.optionalWith(S.Int, { nullable: true }), + /** + * The text corresponding to the segment from the response. + */ + "text": S.optionalWith(S.String, { nullable: true }) + }) +{} + +/** + * Grounding support. + */ +export class GoogleAiGenerativelanguageV1BetaGroundingSupport + extends S.Class("GoogleAiGenerativelanguageV1BetaGroundingSupport")( + { + /** + * Segment of the content this support belongs to. + */ + "segment": S.optionalWith(GoogleAiGenerativelanguageV1BetaSegment, { nullable: true }), + /** + * Optional. A list of indices (into 'grounding_chunk') specifying the + * citations associated with the claim. For instance [1,3,4] means + * that grounding_chunk[1], grounding_chunk[3], + * grounding_chunk[4] are the retrieved content attributed to the claim. + */ + "groundingChunkIndices": S.optionalWith(S.Array(S.Int), { nullable: true }), + /** + * Optional. Confidence score of the support references. Ranges from 0 to 1. 1 is the + * most confident. This list must have the same size as the + * grounding_chunk_indices. + */ + "confidenceScores": S.optionalWith(S.Array(S.Number), { nullable: true }) + } + ) +{} + +/** + * Metadata related to retrieval in the grounding flow. + */ +export class RetrievalMetadata extends S.Class("RetrievalMetadata")({ + /** + * Optional. Score indicating how likely information from google search could help + * answer the prompt. The score is in the range [0, 1], where 0 is the least + * likely and 1 is the most likely. This score is only populated when + * google search grounding and dynamic retrieval is enabled. It will be + * compared to the threshold to determine whether to trigger google search. + */ + "googleSearchDynamicRetrievalScore": S.optionalWith(S.Number, { nullable: true }) +}) {} + +/** + * Metadata returned to client when grounding is enabled. + */ +export class GroundingMetadata extends S.Class("GroundingMetadata")({ + /** + * Optional. Google search entry for the following-up web searches. + */ + "searchEntryPoint": S.optionalWith(SearchEntryPoint, { nullable: true }), + /** + * List of supporting references retrieved from specified grounding source. + */ + "groundingChunks": S.optionalWith(S.Array(GroundingChunk), { nullable: true }), + /** + * List of grounding support. + */ + "groundingSupports": S.optionalWith(S.Array(GoogleAiGenerativelanguageV1BetaGroundingSupport), { nullable: true }), + /** + * Metadata related to retrieval in the grounding flow. + */ + "retrievalMetadata": S.optionalWith(RetrievalMetadata, { nullable: true }), + /** + * Web search queries for the following-up web search. + */ + "webSearchQueries": S.optionalWith(S.Array(S.String), { nullable: true }), + /** + * Optional. Resource name of the Google Maps widget context token that can be used + * with the PlacesContextElement widget in order to render contextual data. + * Only populated in the case that grounding with Google Maps is enabled. + */ + "googleMapsWidgetContextToken": S.optionalWith(S.String, { nullable: true }) +}) {} + +/** + * Candidate for the logprobs token and score. + */ +export class LogprobsResultCandidate extends S.Class("LogprobsResultCandidate")({ + /** + * The candidate’s token string value. + */ + "token": S.optionalWith(S.String, { nullable: true }), + /** + * The candidate’s token id value. + */ + "tokenId": S.optionalWith(S.Int, { nullable: true }), + /** + * The candidate's log probability. + */ + "logProbability": S.optionalWith(S.Number, { nullable: true }) +}) {} + +/** + * Candidates with top log probabilities at each decoding step. + */ +export class TopCandidates extends S.Class("TopCandidates")({ + /** + * Sorted by log probability in descending order. + */ + "candidates": S.optionalWith(S.Array(LogprobsResultCandidate), { nullable: true }) +}) {} + +/** + * Logprobs Result + */ +export class LogprobsResult extends S.Class("LogprobsResult")({ + /** + * Sum of log probabilities for all tokens. + */ + "logProbabilitySum": S.optionalWith(S.Number, { nullable: true }), + /** + * Length = total number of decoding steps. + */ + "topCandidates": S.optionalWith(S.Array(TopCandidates), { nullable: true }), + /** + * Length = total number of decoding steps. + * The chosen candidates may or may not be in top_candidates. + */ + "chosenCandidates": S.optionalWith(S.Array(LogprobsResultCandidate), { nullable: true }) +}) {} + +/** + * Status of the url retrieval. + */ +export class UrlMetadataUrlRetrievalStatus extends S.Literal( + "URL_RETRIEVAL_STATUS_UNSPECIFIED", + "URL_RETRIEVAL_STATUS_SUCCESS", + "URL_RETRIEVAL_STATUS_ERROR", + "URL_RETRIEVAL_STATUS_PAYWALL", + "URL_RETRIEVAL_STATUS_UNSAFE" +) {} + +/** + * Context of the a single url retrieval. + */ +export class UrlMetadata extends S.Class("UrlMetadata")({ + /** + * Retrieved url by the tool. + */ + "retrievedUrl": S.optionalWith(S.String, { nullable: true }), + /** + * Status of the url retrieval. + */ + "urlRetrievalStatus": S.optionalWith(UrlMetadataUrlRetrievalStatus, { nullable: true }) +}) {} + +/** + * Metadata related to url context retrieval tool. + */ +export class UrlContextMetadata extends S.Class("UrlContextMetadata")({ + /** + * List of url context. + */ + "urlMetadata": S.optionalWith(S.Array(UrlMetadata), { nullable: true }) +}) {} + +/** + * A response candidate generated from the model. + */ +export class Candidate extends S.Class("Candidate")({ + /** + * Output only. Index of the candidate in the list of response candidates. + */ + "index": S.optionalWith(S.Int, { nullable: true }), + /** + * Output only. Generated content returned from the model. + */ + "content": S.optionalWith(Content, { nullable: true }), + /** + * Optional. Output only. The reason why the model stopped generating tokens. + * + * If empty, the model has not stopped generating tokens. + */ + "finishReason": S.optionalWith(CandidateFinishReason, { nullable: true }), + /** + * Optional. Output only. Details the reason why the model stopped generating tokens. + * This is populated only when `finish_reason` is set. + */ + "finishMessage": S.optionalWith(S.String, { nullable: true }), + /** + * List of ratings for the safety of a response candidate. + * + * There is at most one rating per category. + */ + "safetyRatings": S.optionalWith(S.Array(SafetyRating), { nullable: true }), + /** + * Output only. Citation information for model-generated candidate. + * + * This field may be populated with recitation information for any text + * included in the `content`. These are passages that are "recited" from + * copyrighted material in the foundational LLM's training data. + */ + "citationMetadata": S.optionalWith(CitationMetadata, { nullable: true }), + /** + * Output only. Token count for this candidate. + */ + "tokenCount": S.optionalWith(S.Int, { nullable: true }), + /** + * Output only. Attribution information for sources that contributed to a grounded answer. + * + * This field is populated for `GenerateAnswer` calls. + */ + "groundingAttributions": S.optionalWith(S.Array(GroundingAttribution), { nullable: true }), + /** + * Output only. Grounding metadata for the candidate. + * + * This field is populated for `GenerateContent` calls. + */ + "groundingMetadata": S.optionalWith(GroundingMetadata, { nullable: true }), + /** + * Output only. Average log probability score of the candidate. + */ + "avgLogprobs": S.optionalWith(S.Number, { nullable: true }), + /** + * Output only. Log-likelihood scores for the response tokens and top tokens + */ + "logprobsResult": S.optionalWith(LogprobsResult, { nullable: true }), + /** + * Output only. Metadata related to url context retrieval tool. + */ + "urlContextMetadata": S.optionalWith(UrlContextMetadata, { nullable: true }) +}) {} + +/** + * Optional. If set, the prompt was blocked and no candidates are returned. + * Rephrase the prompt. + */ +export class PromptFeedbackBlockReason + extends S.Literal("BLOCK_REASON_UNSPECIFIED", "SAFETY", "OTHER", "BLOCKLIST", "PROHIBITED_CONTENT", "IMAGE_SAFETY") +{} + +/** + * A set of the feedback metadata the prompt specified in + * `GenerateContentRequest.content`. + */ +export class PromptFeedback extends S.Class("PromptFeedback")({ + /** + * Optional. If set, the prompt was blocked and no candidates are returned. + * Rephrase the prompt. + */ + "blockReason": S.optionalWith(PromptFeedbackBlockReason, { nullable: true }), + /** + * Ratings for safety of the prompt. + * There is at most one rating per category. + */ + "safetyRatings": S.optionalWith(S.Array(SafetyRating), { nullable: true }) +}) {} + +export class GenerativeLanguageModality + extends S.Literal("MODALITY_UNSPECIFIED", "TEXT", "IMAGE", "VIDEO", "AUDIO", "DOCUMENT") +{} + +/** + * Represents token counting info for a single modality. + */ +export class ModalityTokenCount extends S.Class("ModalityTokenCount")({ + /** + * The modality associated with this token count. + */ + "modality": S.optionalWith(GenerativeLanguageModality, { nullable: true }), + /** + * Number of tokens. + */ + "tokenCount": S.optionalWith(S.Int, { nullable: true }) +}) {} + +/** + * Metadata on the generation request's token usage. + */ +export class UsageMetadata extends S.Class("UsageMetadata")({ + /** + * Number of tokens in the prompt. When `cached_content` is set, this is + * still the total effective prompt size meaning this includes the number of + * tokens in the cached content. + */ + "promptTokenCount": S.optionalWith(S.Int, { nullable: true }), + /** + * Number of tokens in the cached part of the prompt (the cached content) + */ + "cachedContentTokenCount": S.optionalWith(S.Int, { nullable: true }), + /** + * Total number of tokens across all the generated response candidates. + */ + "candidatesTokenCount": S.optionalWith(S.Int, { nullable: true }), + /** + * Output only. Number of tokens present in tool-use prompt(s). + */ + "toolUsePromptTokenCount": S.optionalWith(S.Int, { nullable: true }), + /** + * Output only. Number of tokens of thoughts for thinking models. + */ + "thoughtsTokenCount": S.optionalWith(S.Int, { nullable: true }), + /** + * Total token count for the generation request (prompt + response + * candidates). + */ + "totalTokenCount": S.optionalWith(S.Int, { nullable: true }), + /** + * Output only. List of modalities that were processed in the request input. + */ + "promptTokensDetails": S.optionalWith(S.Array(ModalityTokenCount), { nullable: true }), + /** + * Output only. List of modalities of the cached content in the request input. + */ + "cacheTokensDetails": S.optionalWith(S.Array(ModalityTokenCount), { nullable: true }), + /** + * Output only. List of modalities that were returned in the response. + */ + "candidatesTokensDetails": S.optionalWith(S.Array(ModalityTokenCount), { nullable: true }), + /** + * Output only. List of modalities that were processed for tool-use request inputs. + */ + "toolUsePromptTokensDetails": S.optionalWith(S.Array(ModalityTokenCount), { nullable: true }) +}) {} + +/** + * Response from the model supporting multiple candidate responses. + * + * Safety ratings and content filtering are reported for both + * prompt in `GenerateContentResponse.prompt_feedback` and for each candidate + * in `finish_reason` and in `safety_ratings`. The API: + * - Returns either all requested candidates or none of them + * - Returns no candidates at all only if there was something wrong with the + * prompt (check `prompt_feedback`) + * - Reports feedback on each candidate in `finish_reason` and + * `safety_ratings`. + */ +export class GenerateContentResponse extends S.Class("GenerateContentResponse")({ + /** + * Candidate responses from the model. + */ + "candidates": S.optionalWith(S.Array(Candidate), { nullable: true }), + /** + * Returns the prompt's feedback related to the content filters. + */ + "promptFeedback": S.optionalWith(PromptFeedback, { nullable: true }), + /** + * Output only. Metadata on the generation requests' token usage. + */ + "usageMetadata": S.optionalWith(UsageMetadata, { nullable: true }), + /** + * Output only. The model version used to generate the response. + */ + "modelVersion": S.optionalWith(S.String, { nullable: true }), + /** + * Output only. response_id is used to identify each response. + */ + "responseId": S.optionalWith(S.String, { nullable: true }) +}) {} + +/** + * Passage included inline with a grounding configuration. + */ +export class GroundingPassage extends S.Class("GroundingPassage")({ + /** + * Identifier for the passage for attributing this passage in grounded + * answers. + */ + "id": S.optionalWith(S.String, { nullable: true }), + /** + * Content of the passage. + */ + "content": S.optionalWith(Content, { nullable: true }) +}) {} + +/** + * A repeated list of passages. + */ +export class GroundingPassages extends S.Class("GroundingPassages")({ + /** + * List of passages. + */ + "passages": S.optionalWith(S.Array(GroundingPassage), { nullable: true }) +}) {} + +/** + * Required. Operator applied to the given key-value pair to trigger the condition. + */ +export class ConditionOperation extends S.Literal( + "OPERATOR_UNSPECIFIED", + "LESS", + "LESS_EQUAL", + "EQUAL", + "GREATER_EQUAL", + "GREATER", + "NOT_EQUAL", + "INCLUDES", + "EXCLUDES" +) {} + +/** + * Filter condition applicable to a single key. + */ +export class Condition extends S.Class("Condition")({ + /** + * The string value to filter the metadata on. + */ + "stringValue": S.optionalWith(S.String, { nullable: true }), + /** + * The numeric value to filter the metadata on. + */ + "numericValue": S.optionalWith(S.Number, { nullable: true }), + /** + * Required. Operator applied to the given key-value pair to trigger the condition. + */ + "operation": ConditionOperation +}) {} + +/** + * User provided filter to limit retrieval based on `Chunk` or `Document` level + * metadata values. + * Example (genre = drama OR genre = action): + * key = "document.custom_metadata.genre" + * conditions = [{string_value = "drama", operation = EQUAL}, + * {string_value = "action", operation = EQUAL}] + */ +export class MetadataFilter extends S.Class("MetadataFilter")({ + /** + * Required. The key of the metadata to filter on. + */ + "key": S.String, + /** + * Required. The `Condition`s for the given key that will trigger this filter. Multiple + * `Condition`s are joined by logical ORs. + */ + "conditions": S.Array(Condition) +}) {} + +/** + * Configuration for retrieving grounding content from a `Corpus` or + * `Document` created using the Semantic Retriever API. + */ +export class SemanticRetrieverConfig extends S.Class("SemanticRetrieverConfig")({ + /** + * Required. Name of the resource for retrieval. Example: `corpora/123` or + * `corpora/123/documents/abc`. + */ + "source": S.String, + /** + * Required. Query to use for matching `Chunk`s in the given resource by similarity. + */ + "query": Content, + /** + * Optional. Filters for selecting `Document`s and/or `Chunk`s from the resource. + */ + "metadataFilters": S.optionalWith(S.Array(MetadataFilter), { nullable: true }), + /** + * Optional. Maximum number of relevant `Chunk`s to retrieve. + */ + "maxChunksCount": S.optionalWith(S.Int, { nullable: true }), + /** + * Optional. Minimum relevance score for retrieved relevant `Chunk`s. + */ + "minimumRelevanceScore": S.optionalWith(S.Number, { nullable: true }) +}) {} + +/** + * Required. Style in which answers should be returned. + */ +export class GenerateAnswerRequestAnswerStyle + extends S.Literal("ANSWER_STYLE_UNSPECIFIED", "ABSTRACTIVE", "EXTRACTIVE", "VERBOSE") +{} + +/** + * Request to generate a grounded answer from the `Model`. + */ +export class GenerateAnswerRequest extends S.Class("GenerateAnswerRequest")({ + /** + * Passages provided inline with the request. + */ + "inlinePassages": S.optionalWith(GroundingPassages, { nullable: true }), + /** + * Content retrieved from resources created via the Semantic Retriever + * API. + */ + "semanticRetriever": S.optionalWith(SemanticRetrieverConfig, { nullable: true }), + /** + * Required. The content of the current conversation with the `Model`. For single-turn + * queries, this is a single question to answer. For multi-turn queries, this + * is a repeated field that contains conversation history and the last + * `Content` in the list containing the question. + * + * Note: `GenerateAnswer` only supports queries in English. + */ + "contents": S.Array(Content), + /** + * Required. Style in which answers should be returned. + */ + "answerStyle": GenerateAnswerRequestAnswerStyle, + /** + * Optional. A list of unique `SafetySetting` instances for blocking unsafe content. + * + * This will be enforced on the `GenerateAnswerRequest.contents` and + * `GenerateAnswerResponse.candidate`. There should not be more than one + * setting for each `SafetyCategory` type. The API will block any contents and + * responses that fail to meet the thresholds set by these settings. This list + * overrides the default settings for each `SafetyCategory` specified in the + * safety_settings. If there is no `SafetySetting` for a given + * `SafetyCategory` provided in the list, the API will use the default safety + * setting for that category. Harm categories HARM_CATEGORY_HATE_SPEECH, + * HARM_CATEGORY_SEXUALLY_EXPLICIT, HARM_CATEGORY_DANGEROUS_CONTENT, + * HARM_CATEGORY_HARASSMENT are supported. + * Refer to the + * [guide](https://ai.google.dev/gemini-api/docs/safety-settings) + * for detailed information on available safety settings. Also refer to the + * [Safety guidance](https://ai.google.dev/gemini-api/docs/safety-guidance) to + * learn how to incorporate safety considerations in your AI applications. + */ + "safetySettings": S.optionalWith(S.Array(SafetySetting), { nullable: true }), + /** + * Optional. Controls the randomness of the output. + * + * Values can range from [0.0,1.0], inclusive. A value closer to 1.0 will + * produce responses that are more varied and creative, while a value closer + * to 0.0 will typically result in more straightforward responses from the + * model. A low temperature (~0.2) is usually recommended for + * Attributed-Question-Answering use cases. + */ + "temperature": S.optionalWith(S.Number, { nullable: true }) +}) {} + +/** + * Optional. If set, the input was blocked and no candidates are returned. + * Rephrase the input. + */ +export class InputFeedbackBlockReason extends S.Literal("BLOCK_REASON_UNSPECIFIED", "SAFETY", "OTHER") {} + +/** + * Feedback related to the input data used to answer the question, as opposed + * to the model-generated response to the question. + */ +export class InputFeedback extends S.Class("InputFeedback")({ + /** + * Optional. If set, the input was blocked and no candidates are returned. + * Rephrase the input. + */ + "blockReason": S.optionalWith(InputFeedbackBlockReason, { nullable: true }), + /** + * Ratings for safety of the input. + * There is at most one rating per category. + */ + "safetyRatings": S.optionalWith(S.Array(SafetyRating), { nullable: true }) +}) {} + +/** + * Response from the model for a grounded answer. + */ +export class GenerateAnswerResponse extends S.Class("GenerateAnswerResponse")({ + /** + * Candidate answer from the model. + * + * Note: The model *always* attempts to provide a grounded answer, even when + * the answer is unlikely to be answerable from the given passages. + * In that case, a low-quality or ungrounded answer may be provided, along + * with a low `answerable_probability`. + */ + "answer": S.optionalWith(Candidate, { nullable: true }), + /** + * Output only. The model's estimate of the probability that its answer is correct and + * grounded in the input passages. + * + * A low `answerable_probability` indicates that the answer might not be + * grounded in the sources. + * + * When `answerable_probability` is low, you may want to: + * + * * Display a message to the effect of "We couldn’t answer that question" to + * the user. + * * Fall back to a general-purpose LLM that answers the question from world + * knowledge. The threshold and nature of such fallbacks will depend on + * individual use cases. `0.5` is a good starting threshold. + */ + "answerableProbability": S.optionalWith(S.Number, { nullable: true }), + /** + * Output only. Feedback related to the input data used to answer the question, as opposed + * to the model-generated response to the question. + * + * The input data can be one or more of the following: + * + * - Question specified by the last entry in `GenerateAnswerRequest.content` + * - Conversation history specified by the other entries in + * `GenerateAnswerRequest.content` + * - Grounding sources (`GenerateAnswerRequest.semantic_retriever` or + * `GenerateAnswerRequest.inline_passages`) + */ + "inputFeedback": S.optionalWith(InputFeedback, { nullable: true }) +}) {} + +export class TaskType extends S.Literal( + "TASK_TYPE_UNSPECIFIED", + "RETRIEVAL_QUERY", + "RETRIEVAL_DOCUMENT", + "SEMANTIC_SIMILARITY", + "CLASSIFICATION", + "CLUSTERING", + "QUESTION_ANSWERING", + "FACT_VERIFICATION", + "CODE_RETRIEVAL_QUERY" +) {} + +/** + * Request containing the `Content` for the model to embed. + */ +export class EmbedContentRequest extends S.Class("EmbedContentRequest")({ + /** + * Required. The model's resource name. This serves as an ID for the Model to use. + * + * This name should match a model name returned by the `ListModels` method. + * + * Format: `models/{model}` + */ + "model": S.String, + /** + * Required. The content to embed. Only the `parts.text` fields will be counted. + */ + "content": Content, + /** + * Optional. Optional task type for which the embeddings will be used. Not supported on + * earlier models (`models/embedding-001`). + */ + "taskType": S.optionalWith(TaskType, { nullable: true }), + /** + * Optional. An optional title for the text. Only applicable when TaskType is + * `RETRIEVAL_DOCUMENT`. + * + * Note: Specifying a `title` for `RETRIEVAL_DOCUMENT` provides better quality + * embeddings for retrieval. + */ + "title": S.optionalWith(S.String, { nullable: true }), + /** + * Optional. Optional reduced dimension for the output embedding. If set, excessive + * values in the output embedding are truncated from the end. Supported by + * newer models since 2024 only. You cannot set this value if using the + * earlier model (`models/embedding-001`). + */ + "outputDimensionality": S.optionalWith(S.Int, { nullable: true }) +}) {} + +/** + * A list of floats representing an embedding. + */ +export class ContentEmbedding extends S.Class("ContentEmbedding")({ + /** + * The embedding values. This is for 3P users only and will not be populated + * for 1P calls. + */ + "values": S.optionalWith(S.Array(S.Number), { nullable: true }), + /** + * This field stores the soft tokens tensor frame shape + * (e.g. [1, 1, 256, 2048]). + */ + "shape": S.optionalWith(S.Array(S.Int), { nullable: true }) +}) {} + +/** + * The response to an `EmbedContentRequest`. + */ +export class EmbedContentResponse extends S.Class("EmbedContentResponse")({ + /** + * Output only. The embedding generated from the input content. + */ + "embedding": S.optionalWith(ContentEmbedding, { nullable: true }) +}) {} + +/** + * Batch request to get embeddings from the model for a list of prompts. + */ +export class BatchEmbedContentsRequest extends S.Class("BatchEmbedContentsRequest")({ + /** + * Required. Embed requests for the batch. The model in each of these requests must + * match the model specified `BatchEmbedContentsRequest.model`. + */ + "requests": S.Array(EmbedContentRequest) +}) {} + +/** + * The response to a `BatchEmbedContentsRequest`. + */ +export class BatchEmbedContentsResponse extends S.Class("BatchEmbedContentsResponse")({ + /** + * Output only. The embeddings for each request, in the same order as provided in the batch + * request. + */ + "embeddings": S.optionalWith(S.Array(ContentEmbedding), { nullable: true }) +}) {} + +/** + * Counts the number of tokens in the `prompt` sent to a model. + * + * Models may tokenize text differently, so each model may return a different + * `token_count`. + */ +export class CountTokensRequest extends S.Class("CountTokensRequest")({ + /** + * Optional. The input given to the model as a prompt. This field is ignored when + * `generate_content_request` is set. + */ + "contents": S.optionalWith(S.Array(Content), { nullable: true }), + /** + * Optional. The overall input given to the `Model`. This includes the prompt as well as + * other model steering information like [system + * instructions](https://ai.google.dev/gemini-api/docs/system-instructions), + * and/or function declarations for [function + * calling](https://ai.google.dev/gemini-api/docs/function-calling). + * `Model`s/`Content`s and `generate_content_request`s are mutually + * exclusive. You can either send `Model` + `Content`s or a + * `generate_content_request`, but never both. + */ + "generateContentRequest": S.optionalWith(GenerateContentRequest, { nullable: true }) +}) {} + +/** + * A response from `CountTokens`. + * + * It returns the model's `token_count` for the `prompt`. + */ +export class CountTokensResponse extends S.Class("CountTokensResponse")({ + /** + * The number of tokens that the `Model` tokenizes the `prompt` into. Always + * non-negative. + */ + "totalTokens": S.optionalWith(S.Int, { nullable: true }), + /** + * Number of tokens in the cached part of the prompt (the cached content). + */ + "cachedContentTokenCount": S.optionalWith(S.Int, { nullable: true }), + /** + * Output only. List of modalities that were processed in the request input. + */ + "promptTokensDetails": S.optionalWith(S.Array(ModalityTokenCount), { nullable: true }), + /** + * Output only. List of modalities that were processed in the cached content. + */ + "cacheTokensDetails": S.optionalWith(S.Array(ModalityTokenCount), { nullable: true }) +}) {} + +/** + * The request to be processed in the batch. + */ +export class InlinedRequest extends S.Class("InlinedRequest")({ + /** + * Required. The request to be processed in the batch. + */ + "request": GenerateContentRequest, + /** + * Optional. The metadata to be associated with the request. + */ + "metadata": S.optionalWith(S.Record({ key: S.String, value: S.Unknown }), { nullable: true }) +}) {} + +/** + * The requests to be processed in the batch if provided as part of the + * batch creation request. + */ +export class InlinedRequests extends S.Class("InlinedRequests")({ + /** + * Required. The requests to be processed in the batch. + */ + "requests": S.Array(InlinedRequest) +}) {} + +/** + * Configures the input to the batch request. + */ +export class InputConfig extends S.Class("InputConfig")({ + /** + * The name of the `File` containing the input requests. + */ + "fileName": S.optionalWith(S.String, { nullable: true }), + /** + * The requests to be processed in the batch. + */ + "requests": S.optionalWith(InlinedRequests, { nullable: true }) +}) {} + +/** + * The response to a single request in the batch. + */ +export class InlinedResponse extends S.Class("InlinedResponse")({ + /** + * Output only. The error encountered while processing the request. + */ + "error": S.optionalWith(Status, { nullable: true }), + /** + * Output only. The response to the request. + */ + "response": S.optionalWith(GenerateContentResponse, { nullable: true }), + /** + * Output only. The metadata associated with the request. + */ + "metadata": S.optionalWith(S.Record({ key: S.String, value: S.Unknown }), { nullable: true }) +}) {} + +/** + * The responses to the requests in the batch. + */ +export class InlinedResponses extends S.Class("InlinedResponses")({ + /** + * Output only. The responses to the requests in the batch. + */ + "inlinedResponses": S.optionalWith(S.Array(InlinedResponse), { nullable: true }) +}) {} + +/** + * The output of a batch request. This is returned in the + * `BatchGenerateContentResponse` or the `GenerateContentBatch.output` field. + */ +export class GenerateContentBatchOutput extends S.Class("GenerateContentBatchOutput")({ + /** + * Output only. The file ID of the file containing the responses. + * The file will be a JSONL file with a single response per line. + * The responses will be `GenerateContentResponse` messages formatted as + * JSON. + * The responses will be written in the same order as the input requests. + */ + "responsesFile": S.optionalWith(S.String, { nullable: true }), + /** + * Output only. The responses to the requests in the batch. Returned when the batch was + * built using inlined requests. The responses will be in the same order as + * the input requests. + */ + "inlinedResponses": S.optionalWith(InlinedResponses, { nullable: true }) +}) {} + +/** + * Stats about the batch. + */ +export class BatchStats extends S.Class("BatchStats")({ + /** + * Output only. The number of requests in the batch. + */ + "requestCount": S.optionalWith(S.String, { nullable: true }), + /** + * Output only. The number of requests that were successfully processed. + */ + "successfulRequestCount": S.optionalWith(S.String, { nullable: true }), + /** + * Output only. The number of requests that failed to be processed. + */ + "failedRequestCount": S.optionalWith(S.String, { nullable: true }), + /** + * Output only. The number of requests that are still pending processing. + */ + "pendingRequestCount": S.optionalWith(S.String, { nullable: true }) +}) {} + +export class BatchState extends S.Literal( + "BATCH_STATE_UNSPECIFIED", + "BATCH_STATE_PENDING", + "BATCH_STATE_RUNNING", + "BATCH_STATE_SUCCEEDED", + "BATCH_STATE_FAILED", + "BATCH_STATE_CANCELLED", + "BATCH_STATE_EXPIRED" +) {} + +/** + * A resource representing a batch of `GenerateContent` requests. + */ +export class GenerateContentBatch extends S.Class("GenerateContentBatch")({ + /** + * Required. The name of the `Model` to use for generating the completion. + * + * Format: `models/{model}`. + */ + "model": S.String, + /** + * Output only. Identifier. Resource name of the batch. + * + * Format: `batches/{batch_id}`. + */ + "name": S.optionalWith(S.String, { nullable: true }), + /** + * Required. The user-defined name of this batch. + */ + "displayName": S.String, + /** + * Required. Input configuration of the instances on which batch processing + * are performed. + */ + "inputConfig": InputConfig, + /** + * Output only. The output of the batch request. + */ + "output": S.optionalWith(GenerateContentBatchOutput, { nullable: true }), + /** + * Output only. The time at which the batch was created. + */ + "createTime": S.optionalWith(S.String, { nullable: true }), + /** + * Output only. The time at which the batch processing completed. + */ + "endTime": S.optionalWith(S.String, { nullable: true }), + /** + * Output only. The time at which the batch was last updated. + */ + "updateTime": S.optionalWith(S.String, { nullable: true }), + /** + * Output only. Stats about the batch. + */ + "batchStats": S.optionalWith(BatchStats, { nullable: true }), + /** + * Output only. The state of the batch. + */ + "state": S.optionalWith(BatchState, { nullable: true }), + /** + * Optional. The priority of the batch. Batches with a higher priority value will be + * processed before batches with a lower priority value. Negative values are + * allowed. Default is 0. + */ + "priority": S.optionalWith(S.String, { nullable: true }) +}) {} + +/** + * Request for a `BatchGenerateContent` operation. + */ +export class BatchGenerateContentRequest extends S.Class("BatchGenerateContentRequest")({ + /** + * Required. The batch to create. + */ + "batch": GenerateContentBatch +}) {} + +/** + * Response for a `BatchGenerateContent` operation. + */ +export class BatchGenerateContentResponse + extends S.Class("BatchGenerateContentResponse")({ + /** + * Output only. The output of the batch request. + */ + "output": S.optionalWith(GenerateContentBatchOutput, { nullable: true }) + }) +{} + +/** + * This resource represents a long-running operation that is the result of a + * network API call. + */ +export class BatchGenerateContentOperation + extends S.Class("BatchGenerateContentOperation")({ + "metadata": S.optionalWith(GenerateContentBatch, { nullable: true }), + "response": S.optionalWith(BatchGenerateContentResponse, { nullable: true }), + /** + * The server-assigned name, which is only unique within the same service that + * originally returns it. If you use the default HTTP mapping, the + * `name` should be a resource name ending with `operations/{unique_id}`. + */ + "name": S.optionalWith(S.String, { nullable: true }), + /** + * If the value is `false`, it means the operation is still in progress. + * If `true`, the operation is completed, and either `error` or `response` is + * available. + */ + "done": S.optionalWith(S.Boolean, { nullable: true }), + /** + * The error result of the operation in case of failure or cancellation. + */ + "error": S.optionalWith(Status, { nullable: true }) + }) +{} + +/** + * The request to be processed in the batch. + */ +export class InlinedEmbedContentRequest extends S.Class("InlinedEmbedContentRequest")({ + /** + * Required. The request to be processed in the batch. + */ + "request": EmbedContentRequest, + /** + * Optional. The metadata to be associated with the request. + */ + "metadata": S.optionalWith(S.Record({ key: S.String, value: S.Unknown }), { nullable: true }) +}) {} + +/** + * The requests to be processed in the batch if provided as part of the + * batch creation request. + */ +export class InlinedEmbedContentRequests extends S.Class("InlinedEmbedContentRequests")({ + /** + * Required. The requests to be processed in the batch. + */ + "requests": S.Array(InlinedEmbedContentRequest) +}) {} + +/** + * Configures the input to the batch request. + */ +export class InputEmbedContentConfig extends S.Class("InputEmbedContentConfig")({ + /** + * The name of the `File` containing the input requests. + */ + "fileName": S.optionalWith(S.String, { nullable: true }), + /** + * The requests to be processed in the batch. + */ + "requests": S.optionalWith(InlinedEmbedContentRequests, { nullable: true }) +}) {} + +/** + * The response to a single request in the batch. + */ +export class InlinedEmbedContentResponse extends S.Class("InlinedEmbedContentResponse")({ + /** + * Output only. The error encountered while processing the request. + */ + "error": S.optionalWith(Status, { nullable: true }), + /** + * Output only. The response to the request. + */ + "response": S.optionalWith(EmbedContentResponse, { nullable: true }), + /** + * Output only. The metadata associated with the request. + */ + "metadata": S.optionalWith(S.Record({ key: S.String, value: S.Unknown }), { nullable: true }) +}) {} + +/** + * The responses to the requests in the batch. + */ +export class InlinedEmbedContentResponses + extends S.Class("InlinedEmbedContentResponses")({ + /** + * Output only. The responses to the requests in the batch. + */ + "inlinedResponses": S.optionalWith(S.Array(InlinedEmbedContentResponse), { nullable: true }) + }) +{} + +/** + * The output of a batch request. This is returned in the + * `AsyncBatchEmbedContentResponse` or the `EmbedContentBatch.output` field. + */ +export class EmbedContentBatchOutput extends S.Class("EmbedContentBatchOutput")({ + /** + * Output only. The file ID of the file containing the responses. + * The file will be a JSONL file with a single response per line. + * The responses will be `EmbedContentResponse` messages formatted as JSON. + * The responses will be written in the same order as the input requests. + */ + "responsesFile": S.optionalWith(S.String, { nullable: true }), + /** + * Output only. The responses to the requests in the batch. Returned when the batch was + * built using inlined requests. The responses will be in the same order as + * the input requests. + */ + "inlinedResponses": S.optionalWith(InlinedEmbedContentResponses, { nullable: true }) +}) {} + +/** + * Stats about the batch. + */ +export class EmbedContentBatchStats extends S.Class("EmbedContentBatchStats")({ + /** + * Output only. The number of requests in the batch. + */ + "requestCount": S.optionalWith(S.String, { nullable: true }), + /** + * Output only. The number of requests that were successfully processed. + */ + "successfulRequestCount": S.optionalWith(S.String, { nullable: true }), + /** + * Output only. The number of requests that failed to be processed. + */ + "failedRequestCount": S.optionalWith(S.String, { nullable: true }), + /** + * Output only. The number of requests that are still pending processing. + */ + "pendingRequestCount": S.optionalWith(S.String, { nullable: true }) +}) {} + +/** + * A resource representing a batch of `EmbedContent` requests. + */ +export class EmbedContentBatch extends S.Class("EmbedContentBatch")({ + /** + * Required. The name of the `Model` to use for generating the completion. + * + * Format: `models/{model}`. + */ + "model": S.String, + /** + * Output only. Identifier. Resource name of the batch. + * + * Format: `batches/{batch_id}`. + */ + "name": S.optionalWith(S.String, { nullable: true }), + /** + * Required. The user-defined name of this batch. + */ + "displayName": S.String, + /** + * Required. Input configuration of the instances on which batch processing + * are performed. + */ + "inputConfig": InputEmbedContentConfig, + /** + * Output only. The output of the batch request. + */ + "output": S.optionalWith(EmbedContentBatchOutput, { nullable: true }), + /** + * Output only. The time at which the batch was created. + */ + "createTime": S.optionalWith(S.String, { nullable: true }), + /** + * Output only. The time at which the batch processing completed. + */ + "endTime": S.optionalWith(S.String, { nullable: true }), + /** + * Output only. The time at which the batch was last updated. + */ + "updateTime": S.optionalWith(S.String, { nullable: true }), + /** + * Output only. Stats about the batch. + */ + "batchStats": S.optionalWith(EmbedContentBatchStats, { nullable: true }), + /** + * Output only. The state of the batch. + */ + "state": S.optionalWith(BatchState, { nullable: true }), + /** + * Optional. The priority of the batch. Batches with a higher priority value will be + * processed before batches with a lower priority value. Negative values are + * allowed. Default is 0. + */ + "priority": S.optionalWith(S.String, { nullable: true }) +}) {} + +/** + * Request for an `AsyncBatchEmbedContent` operation. + */ +export class AsyncBatchEmbedContentRequest + extends S.Class("AsyncBatchEmbedContentRequest")({ + /** + * Required. The batch to create. + */ + "batch": EmbedContentBatch + }) +{} + +/** + * Response for a `BatchGenerateContent` operation. + */ +export class AsyncBatchEmbedContentResponse + extends S.Class("AsyncBatchEmbedContentResponse")({ + /** + * Output only. The output of the batch request. + */ + "output": S.optionalWith(EmbedContentBatchOutput, { nullable: true }) + }) +{} + +/** + * This resource represents a long-running operation that is the result of a + * network API call. + */ +export class AsyncBatchEmbedContentOperation + extends S.Class("AsyncBatchEmbedContentOperation")({ + "metadata": S.optionalWith(EmbedContentBatch, { nullable: true }), + "response": S.optionalWith(AsyncBatchEmbedContentResponse, { nullable: true }), + /** + * The server-assigned name, which is only unique within the same service that + * originally returns it. If you use the default HTTP mapping, the + * `name` should be a resource name ending with `operations/{unique_id}`. + */ + "name": S.optionalWith(S.String, { nullable: true }), + /** + * If the value is `false`, it means the operation is still in progress. + * If `true`, the operation is completed, and either `error` or `response` is + * available. + */ + "done": S.optionalWith(S.Boolean, { nullable: true }), + /** + * The error result of the operation in case of failure or cancellation. + */ + "error": S.optionalWith(Status, { nullable: true }) + }) +{} + +export class UpdateGenerateContentBatchParams extends S.Struct({ + "updateMask": S.optionalWith(S.String.pipe(S.pattern(new RegExp("^(\\s*[^,\\s.]+(\\s*[,.]\\s*[^,\\s.]+)*)?$"))), { + nullable: true + }) +}) {} + +export class UpdateEmbedContentBatchParams extends S.Struct({ + "updateMask": S.optionalWith(S.String.pipe(S.pattern(new RegExp("^(\\s*[^,\\s.]+(\\s*[,.]\\s*[^,\\s.]+)*)?$"))), { + nullable: true + }) +}) {} + +export class ListCachedContentsParams extends S.Struct({ + "pageSize": S.optionalWith(S.Int, { nullable: true }), + "pageToken": S.optionalWith(S.String, { nullable: true }) +}) {} + +/** + * Metadata on the usage of the cached content. + */ +export class CachedContentUsageMetadata extends S.Class("CachedContentUsageMetadata")({ + /** + * Total number of tokens that the cached content consumes. + */ + "totalTokenCount": S.optionalWith(S.Int, { nullable: true }) +}) {} + +/** + * Content that has been preprocessed and can be used in subsequent request + * to GenerativeService. + * + * Cached content can be only used with model it was created for. + */ +export class CachedContent extends S.Class("CachedContent")({ + /** + * Timestamp in UTC of when this resource is considered expired. + * This is *always* provided on output, regardless of what was sent + * on input. + */ + "expireTime": S.optionalWith(S.String, { nullable: true }), + /** + * Input only. New TTL for this resource, input only. + */ + "ttl": S.optionalWith(S.String, { nullable: true }), + /** + * Output only. Identifier. The resource name referring to the cached content. + * Format: `cachedContents/{id}` + */ + "name": S.optionalWith(S.String, { nullable: true }), + /** + * Optional. Immutable. The user-generated meaningful display name of the cached content. Maximum + * 128 Unicode characters. + */ + "displayName": S.optionalWith(S.String, { nullable: true }), + /** + * Required. Immutable. The name of the `Model` to use for cached content + * Format: `models/{model}` + */ + "model": S.String, + /** + * Optional. Input only. Immutable. Developer set system instruction. Currently text only. + */ + "systemInstruction": S.optionalWith(Content, { nullable: true }), + /** + * Optional. Input only. Immutable. The content to cache. + */ + "contents": S.optionalWith(S.Array(Content), { nullable: true }), + /** + * Optional. Input only. Immutable. A list of `Tools` the model may use to generate the next response + */ + "tools": S.optionalWith(S.Array(Tool), { nullable: true }), + /** + * Optional. Input only. Immutable. Tool config. This config is shared for all tools. + */ + "toolConfig": S.optionalWith(ToolConfig, { nullable: true }), + /** + * Output only. Creation time of the cache entry. + */ + "createTime": S.optionalWith(S.String, { nullable: true }), + /** + * Output only. When the cache entry was last updated in UTC time. + */ + "updateTime": S.optionalWith(S.String, { nullable: true }), + /** + * Output only. Metadata on the usage of the cached content. + */ + "usageMetadata": S.optionalWith(CachedContentUsageMetadata, { nullable: true }) +}) {} + +/** + * Response with CachedContents list. + */ +export class ListCachedContentsResponse extends S.Class("ListCachedContentsResponse")({ + /** + * List of cached contents. + */ + "cachedContents": S.optionalWith(S.Array(CachedContent), { nullable: true }), + /** + * A token, which can be sent as `page_token` to retrieve the next page. + * If this field is omitted, there are no subsequent pages. + */ + "nextPageToken": S.optionalWith(S.String, { nullable: true }) +}) {} + +export class UpdateCachedContentParams extends S.Struct({ + "updateMask": S.optionalWith(S.String.pipe(S.pattern(new RegExp("^(\\s*[^,\\s.]+(\\s*[,.]\\s*[^,\\s.]+)*)?$"))), { + nullable: true + }) +}) {} + +/** + * The base unit of structured text. + * + * A `Message` includes an `author` and the `content` of + * the `Message`. + * + * The `author` is used to tag messages when they are fed to the + * model as text. + */ +export class Message extends S.Class("Message")({ + /** + * Optional. The author of this Message. + * + * This serves as a key for tagging + * the content of this Message when it is fed to the model as text. + * + * The author can be any alphanumeric string. + */ + "author": S.optionalWith(S.String, { nullable: true }), + /** + * Required. The text content of the structured `Message`. + */ + "content": S.String, + /** + * Output only. Citation information for model-generated `content` in this `Message`. + * + * If this `Message` was generated as output from the model, this field may be + * populated with attribution information for any text included in the + * `content`. This field is used only on output. + */ + "citationMetadata": S.optionalWith(CitationMetadata, { nullable: true }) +}) {} + +/** + * An input/output example used to instruct the Model. + * + * It demonstrates how the model should respond or format its response. + */ +export class Example extends S.Class("Example")({ + /** + * Required. An example of an input `Message` from the user. + */ + "input": Message, + /** + * Required. An example of what the model should output given the input. + */ + "output": Message +}) {} + +/** + * All of the structured input text passed to the model as a prompt. + * + * A `MessagePrompt` contains a structured set of fields that provide context + * for the conversation, examples of user input/model output message pairs that + * prime the model to respond in different ways, and the conversation history + * or list of messages representing the alternating turns of the conversation + * between the user and the model. + */ +export class MessagePrompt extends S.Class("MessagePrompt")({ + /** + * Optional. Text that should be provided to the model first to ground the response. + * + * If not empty, this `context` will be given to the model first before the + * `examples` and `messages`. When using a `context` be sure to provide it + * with every request to maintain continuity. + * + * This field can be a description of your prompt to the model to help provide + * context and guide the responses. Examples: "Translate the phrase from + * English to French." or "Given a statement, classify the sentiment as happy, + * sad or neutral." + * + * Anything included in this field will take precedence over message history + * if the total input size exceeds the model's `input_token_limit` and the + * input request is truncated. + */ + "context": S.optionalWith(S.String, { nullable: true }), + /** + * Optional. Examples of what the model should generate. + * + * This includes both user input and the response that the model should + * emulate. + * + * These `examples` are treated identically to conversation messages except + * that they take precedence over the history in `messages`: + * If the total input size exceeds the model's `input_token_limit` the input + * will be truncated. Items will be dropped from `messages` before `examples`. + */ + "examples": S.optionalWith(S.Array(Example), { nullable: true }), + /** + * Required. A snapshot of the recent conversation history sorted chronologically. + * + * Turns alternate between two authors. + * + * If the total input size exceeds the model's `input_token_limit` the input + * will be truncated: The oldest items will be dropped from `messages`. + */ + "messages": S.Array(Message) +}) {} + +/** + * Request to generate a message response from the model. + */ +export class GenerateMessageRequest extends S.Class("GenerateMessageRequest")({ + /** + * Required. The structured textual input given to the model as a prompt. + * + * Given a + * prompt, the model will return what it predicts is the next message in the + * discussion. + */ + "prompt": MessagePrompt, + /** + * Optional. Controls the randomness of the output. + * + * Values can range over `[0.0,1.0]`, + * inclusive. A value closer to `1.0` will produce responses that are more + * varied, while a value closer to `0.0` will typically result in + * less surprising responses from the model. + */ + "temperature": S.optionalWith(S.Number, { nullable: true }), + /** + * Optional. The number of generated response messages to return. + * + * This value must be between + * `[1, 8]`, inclusive. If unset, this will default to `1`. + */ + "candidateCount": S.optionalWith(S.Int, { nullable: true }), + /** + * Optional. The maximum cumulative probability of tokens to consider when sampling. + * + * The model uses combined Top-k and nucleus sampling. + * + * Nucleus sampling considers the smallest set of tokens whose probability + * sum is at least `top_p`. + */ + "topP": S.optionalWith(S.Number, { nullable: true }), + /** + * Optional. The maximum number of tokens to consider when sampling. + * + * The model uses combined Top-k and nucleus sampling. + * + * Top-k sampling considers the set of `top_k` most probable tokens. + */ + "topK": S.optionalWith(S.Int, { nullable: true }) +}) {} + +/** + * The reason content was blocked during request processing. + */ +export class ContentFilterReason extends S.Literal("BLOCKED_REASON_UNSPECIFIED", "SAFETY", "OTHER") {} + +/** + * Content filtering metadata associated with processing a single request. + * + * ContentFilter contains a reason and an optional supporting string. The reason + * may be unspecified. + */ +export class ContentFilter extends S.Class("ContentFilter")({ + /** + * The reason content was blocked during request processing. + */ + "reason": S.optionalWith(ContentFilterReason, { nullable: true }), + /** + * A string that describes the filtering behavior in more detail. + */ + "message": S.optionalWith(S.String, { nullable: true }) +}) {} + +/** + * The response from the model. + * + * This includes candidate messages and + * conversation history in the form of chronologically-ordered messages. + */ +export class GenerateMessageResponse extends S.Class("GenerateMessageResponse")({ + /** + * Candidate response messages from the model. + */ + "candidates": S.optionalWith(S.Array(Message), { nullable: true }), + /** + * The conversation history used by the model. + */ + "messages": S.optionalWith(S.Array(Message), { nullable: true }), + /** + * A set of content filtering metadata for the prompt and response + * text. + * + * This indicates which `SafetyCategory`(s) blocked a + * candidate from this response, the lowest `HarmProbability` + * that triggered a block, and the HarmThreshold setting for that category. + */ + "filters": S.optionalWith(S.Array(ContentFilter), { nullable: true }) +}) {} + +/** + * Counts the number of tokens in the `prompt` sent to a model. + * + * Models may tokenize text differently, so each model may return a different + * `token_count`. + */ +export class CountMessageTokensRequest extends S.Class("CountMessageTokensRequest")({ + /** + * Required. The prompt, whose token count is to be returned. + */ + "prompt": MessagePrompt +}) {} + +/** + * A response from `CountMessageTokens`. + * + * It returns the model's `token_count` for the `prompt`. + */ +export class CountMessageTokensResponse extends S.Class("CountMessageTokensResponse")({ + /** + * The number of tokens that the `model` tokenizes the `prompt` into. + * + * Always non-negative. + */ + "tokenCount": S.optionalWith(S.Int, { nullable: true }) +}) {} + +export class ListFilesParams extends S.Struct({ + "pageSize": S.optionalWith(S.Int, { nullable: true }), + "pageToken": S.optionalWith(S.String, { nullable: true }) +}) {} + +/** + * Metadata for a video `File`. + */ +export class VideoFileMetadata extends S.Class("VideoFileMetadata")({ + /** + * Duration of the video. + */ + "videoDuration": S.optionalWith(S.String, { nullable: true }) +}) {} + +/** + * Output only. Processing state of the File. + */ +export class FileState extends S.Literal("STATE_UNSPECIFIED", "PROCESSING", "ACTIVE", "FAILED") {} + +/** + * Source of the File. + */ +export class FileSource extends S.Literal("SOURCE_UNSPECIFIED", "UPLOADED", "GENERATED", "REGISTERED") {} + +/** + * A file uploaded to the API. + * Next ID: 15 + */ +export class File extends S.Class("File")({ + /** + * Output only. Metadata for a video. + */ + "videoMetadata": S.optionalWith(VideoFileMetadata, { nullable: true }), + /** + * Immutable. Identifier. The `File` resource name. The ID (name excluding the "files/" prefix) can + * contain up to 40 characters that are lowercase alphanumeric or dashes (-). + * The ID cannot start or end with a dash. If the name is empty on create, a + * unique name will be generated. + * Example: `files/123-456` + */ + "name": S.optionalWith(S.String, { nullable: true }), + /** + * Optional. The human-readable display name for the `File`. The display name must be + * no more than 512 characters in length, including spaces. + * Example: "Welcome Image" + */ + "displayName": S.optionalWith(S.String, { nullable: true }), + /** + * Output only. MIME type of the file. + */ + "mimeType": S.optionalWith(S.String, { nullable: true }), + /** + * Output only. Size of the file in bytes. + */ + "sizeBytes": S.optionalWith(S.String, { nullable: true }), + /** + * Output only. The timestamp of when the `File` was created. + */ + "createTime": S.optionalWith(S.String, { nullable: true }), + /** + * Output only. The timestamp of when the `File` was last updated. + */ + "updateTime": S.optionalWith(S.String, { nullable: true }), + /** + * Output only. The timestamp of when the `File` will be deleted. Only set if the `File` is + * scheduled to expire. + */ + "expirationTime": S.optionalWith(S.String, { nullable: true }), + /** + * Output only. SHA-256 hash of the uploaded bytes. + */ + "sha256Hash": S.optionalWith(S.String, { nullable: true }), + /** + * Output only. The uri of the `File`. + */ + "uri": S.optionalWith(S.String, { nullable: true }), + /** + * Output only. The download uri of the `File`. + */ + "downloadUri": S.optionalWith(S.String, { nullable: true }), + /** + * Output only. Processing state of the File. + */ + "state": S.optionalWith(FileState, { nullable: true }), + /** + * Source of the File. + */ + "source": S.optionalWith(FileSource, { nullable: true }), + /** + * Output only. Error status if File processing failed. + */ + "error": S.optionalWith(Status, { nullable: true }) +}) {} + +/** + * Response for `ListFiles`. + */ +export class ListFilesResponse extends S.Class("ListFilesResponse")({ + /** + * The list of `File`s. + */ + "files": S.optionalWith(S.Array(File), { nullable: true }), + /** + * A token that can be sent as a `page_token` into a subsequent `ListFiles` + * call. + */ + "nextPageToken": S.optionalWith(S.String, { nullable: true }) +}) {} + +/** + * Request for `CreateFile`. + */ +export class CreateFileRequest extends S.Class("CreateFileRequest")({ + /** + * Optional. Metadata for the file to create. + */ + "file": S.optionalWith(File, { nullable: true }) +}) {} + +/** + * Response for `CreateFile`. + */ +export class CreateFileResponse extends S.Class("CreateFileResponse")({ + /** + * Metadata for the created file. + */ + "file": S.optionalWith(File, { nullable: true }) +}) {} + +/** + * Response for `DownloadFile`. + */ +export class DownloadFileResponse extends S.Record({ key: S.String, value: S.Unknown }) {} + +/** + * Output only. The state of the GeneratedFile. + */ +export class GeneratedFileState extends S.Literal("STATE_UNSPECIFIED", "GENERATING", "GENERATED", "FAILED") {} + +/** + * A file generated on behalf of a user. + */ +export class GeneratedFile extends S.Class("GeneratedFile")({ + /** + * Identifier. The name of the generated file. + * Example: `generatedFiles/abc-123` + */ + "name": S.optionalWith(S.String, { nullable: true }), + /** + * MIME type of the generatedFile. + */ + "mimeType": S.optionalWith(S.String, { nullable: true }), + /** + * Output only. The state of the GeneratedFile. + */ + "state": S.optionalWith(GeneratedFileState, { nullable: true }), + /** + * Error details if the GeneratedFile ends up in the STATE_FAILED state. + */ + "error": S.optionalWith(Status, { nullable: true }) +}) {} + +export class ListGeneratedFilesParams extends S.Struct({ + "pageSize": S.optionalWith(S.Int, { nullable: true }), + "pageToken": S.optionalWith(S.String, { nullable: true }) +}) {} + +/** + * Response for `ListGeneratedFiles`. + */ +export class ListGeneratedFilesResponse extends S.Class("ListGeneratedFilesResponse")({ + /** + * The list of `GeneratedFile`s. + */ + "generatedFiles": S.optionalWith(S.Array(GeneratedFile), { nullable: true }), + /** + * A token that can be sent as a `page_token` into a subsequent + * `ListGeneratedFiles` call. + */ + "nextPageToken": S.optionalWith(S.String, { nullable: true }) +}) {} + +/** + * Information about a Generative Language Model. + */ +export class Model extends S.Class("Model")({ + /** + * Required. The resource name of the `Model`. Refer to [Model + * variants](https://ai.google.dev/gemini-api/docs/models/gemini#model-variations) + * for all allowed values. + * + * Format: `models/{model}` with a `{model}` naming convention of: + * + * * "{base_model_id}-{version}" + * + * Examples: + * + * * `models/gemini-1.5-flash-001` + */ + "name": S.String, + /** + * Required. The name of the base model, pass this to the generation request. + * + * Examples: + * + * * `gemini-1.5-flash` + */ + "baseModelId": S.String, + /** + * Required. The version number of the model. + * + * This represents the major version (`1.0` or `1.5`) + */ + "version": S.String, + /** + * The human-readable name of the model. E.g. "Gemini 1.5 Flash". + * + * The name can be up to 128 characters long and can consist of any UTF-8 + * characters. + */ + "displayName": S.optionalWith(S.String, { nullable: true }), + /** + * A short description of the model. + */ + "description": S.optionalWith(S.String, { nullable: true }), + /** + * Maximum number of input tokens allowed for this model. + */ + "inputTokenLimit": S.optionalWith(S.Int, { nullable: true }), + /** + * Maximum number of output tokens available for this model. + */ + "outputTokenLimit": S.optionalWith(S.Int, { nullable: true }), + /** + * The model's supported generation methods. + * + * The corresponding API method names are defined as Pascal case + * strings, such as `generateMessage` and `generateContent`. + */ + "supportedGenerationMethods": S.optionalWith(S.Array(S.String), { nullable: true }), + /** + * Controls the randomness of the output. + * + * Values can range over `[0.0,max_temperature]`, inclusive. A higher value + * will produce responses that are more varied, while a value closer to `0.0` + * will typically result in less surprising responses from the model. + * This value specifies default to be used by the backend while making the + * call to the model. + */ + "temperature": S.optionalWith(S.Number, { nullable: true }), + /** + * The maximum temperature this model can use. + */ + "maxTemperature": S.optionalWith(S.Number, { nullable: true }), + /** + * For [Nucleus + * sampling](https://ai.google.dev/gemini-api/docs/prompting-strategies#top-p). + * + * Nucleus sampling considers the smallest set of tokens whose probability + * sum is at least `top_p`. + * This value specifies default to be used by the backend while making the + * call to the model. + */ + "topP": S.optionalWith(S.Number, { nullable: true }), + /** + * For Top-k sampling. + * + * Top-k sampling considers the set of `top_k` most probable tokens. + * This value specifies default to be used by the backend while making the + * call to the model. + * If empty, indicates the model doesn't use top-k sampling, and `top_k` isn't + * allowed as a generation parameter. + */ + "topK": S.optionalWith(S.Int, { nullable: true }), + /** + * Whether the model supports thinking. + */ + "thinking": S.optionalWith(S.Boolean, { nullable: true }) +}) {} + +export class ListModelsParams extends S.Struct({ + "pageSize": S.optionalWith(S.Int, { nullable: true }), + "pageToken": S.optionalWith(S.String, { nullable: true }) +}) {} + +/** + * Response from `ListModel` containing a paginated list of Models. + */ +export class ListModelsResponse extends S.Class("ListModelsResponse")({ + /** + * The returned Models. + */ + "models": S.optionalWith(S.Array(Model), { nullable: true }), + /** + * A token, which can be sent as `page_token` to retrieve the next page. + * + * If this field is omitted, there are no more pages. + */ + "nextPageToken": S.optionalWith(S.String, { nullable: true }) +}) {} + +/** + * Tuned model as a source for training a new model. + */ +export class TunedModelSource extends S.Class("TunedModelSource")({ + /** + * Immutable. The name of the `TunedModel` to use as the starting point for + * training the new model. + * Example: `tunedModels/my-tuned-model` + */ + "tunedModel": S.optionalWith(S.String, { nullable: true }), + /** + * Output only. The name of the base `Model` this `TunedModel` was tuned from. + * Example: `models/gemini-1.5-flash-001` + */ + "baseModel": S.optionalWith(S.String, { nullable: true }) +}) {} + +/** + * Output only. The state of the tuned model. + */ +export class TunedModelState extends S.Literal("STATE_UNSPECIFIED", "CREATING", "ACTIVE", "FAILED") {} + +/** + * Record for a single tuning step. + */ +export class TuningSnapshot extends S.Class("TuningSnapshot")({ + /** + * Output only. The tuning step. + */ + "step": S.optionalWith(S.Int, { nullable: true }), + /** + * Output only. The epoch this step was part of. + */ + "epoch": S.optionalWith(S.Int, { nullable: true }), + /** + * Output only. The mean loss of the training examples for this step. + */ + "meanLoss": S.optionalWith(S.Number, { nullable: true }), + /** + * Output only. The timestamp when this metric was computed. + */ + "computeTime": S.optionalWith(S.String, { nullable: true }) +}) {} + +/** + * A single example for tuning. + */ +export class TuningExample extends S.Class("TuningExample")({ + /** + * Optional. Text model input. + */ + "textInput": S.optionalWith(S.String, { nullable: true }), + /** + * Required. The expected model output. + */ + "output": S.String +}) {} + +/** + * A set of tuning examples. Can be training or validation data. + */ +export class TuningExamples extends S.Class("TuningExamples")({ + /** + * The examples. Example input can be for text or discuss, but all examples + * in a set must be of the same type. + */ + "examples": S.optionalWith(S.Array(TuningExample), { nullable: true }) +}) {} + +/** + * Dataset for training or validation. + */ +export class Dataset extends S.Class("Dataset")({ + /** + * Optional. Inline examples with simple input/output text. + */ + "examples": S.optionalWith(TuningExamples, { nullable: true }) +}) {} + +/** + * Hyperparameters controlling the tuning process. Read more at + * https://ai.google.dev/docs/model_tuning_guidance + */ +export class Hyperparameters extends S.Class("Hyperparameters")({ + /** + * Optional. Immutable. The learning rate hyperparameter for tuning. + * If not set, a default of 0.001 or 0.0002 will be calculated based on the + * number of training examples. + */ + "learningRate": S.optionalWith(S.Number, { nullable: true }), + /** + * Optional. Immutable. The learning rate multiplier is used to calculate a final learning_rate + * based on the default (recommended) value. + * Actual learning rate := learning_rate_multiplier * default learning rate + * Default learning rate is dependent on base model and dataset size. + * If not set, a default of 1.0 will be used. + */ + "learningRateMultiplier": S.optionalWith(S.Number, { nullable: true }), + /** + * Immutable. The number of training epochs. An epoch is one pass through the training + * data. + * If not set, a default of 5 will be used. + */ + "epochCount": S.optionalWith(S.Int, { nullable: true }), + /** + * Immutable. The batch size hyperparameter for tuning. + * If not set, a default of 4 or 16 will be used based on the number of + * training examples. + */ + "batchSize": S.optionalWith(S.Int, { nullable: true }) +}) {} + +/** + * Tuning tasks that create tuned models. + */ +export class TuningTask extends S.Class("TuningTask")({ + /** + * Output only. The timestamp when tuning this model started. + */ + "startTime": S.optionalWith(S.String, { nullable: true }), + /** + * Output only. The timestamp when tuning this model completed. + */ + "completeTime": S.optionalWith(S.String, { nullable: true }), + /** + * Output only. Metrics collected during tuning. + */ + "snapshots": S.optionalWith(S.Array(TuningSnapshot), { nullable: true }), + /** + * Required. Input only. Immutable. The model training data. + */ + "trainingData": Dataset, + /** + * Immutable. Hyperparameters controlling the tuning process. If not provided, default + * values will be used. + */ + "hyperparameters": S.optionalWith(Hyperparameters, { nullable: true }) +}) {} + +/** + * A fine-tuned model created using ModelService.CreateTunedModel. + */ +export class TunedModel extends S.Class("TunedModel")({ + /** + * Optional. TunedModel to use as the starting point for training the new model. + */ + "tunedModelSource": S.optionalWith(TunedModelSource, { nullable: true }), + /** + * Immutable. The name of the `Model` to tune. + * Example: `models/gemini-1.5-flash-001` + */ + "baseModel": S.optionalWith(S.String, { nullable: true }), + /** + * Output only. The tuned model name. A unique name will be generated on create. + * Example: `tunedModels/az2mb0bpw6i` + * If display_name is set on create, the id portion of the name will be set + * by concatenating the words of the display_name with hyphens and adding a + * random portion for uniqueness. + * + * Example: + * + * * display_name = `Sentence Translator` + * * name = `tunedModels/sentence-translator-u3b7m` + */ + "name": S.optionalWith(S.String, { nullable: true }), + /** + * Optional. The name to display for this model in user interfaces. + * The display name must be up to 40 characters including spaces. + */ + "displayName": S.optionalWith(S.String, { nullable: true }), + /** + * Optional. A short description of this model. + */ + "description": S.optionalWith(S.String, { nullable: true }), + /** + * Optional. Controls the randomness of the output. + * + * Values can range over `[0.0,1.0]`, inclusive. A value closer to `1.0` will + * produce responses that are more varied, while a value closer to `0.0` will + * typically result in less surprising responses from the model. + * + * This value specifies default to be the one used by the base model while + * creating the model. + */ + "temperature": S.optionalWith(S.Number, { nullable: true }), + /** + * Optional. For Nucleus sampling. + * + * Nucleus sampling considers the smallest set of tokens whose probability + * sum is at least `top_p`. + * + * This value specifies default to be the one used by the base model while + * creating the model. + */ + "topP": S.optionalWith(S.Number, { nullable: true }), + /** + * Optional. For Top-k sampling. + * + * Top-k sampling considers the set of `top_k` most probable tokens. + * This value specifies default to be used by the backend while making the + * call to the model. + * + * This value specifies default to be the one used by the base model while + * creating the model. + */ + "topK": S.optionalWith(S.Int, { nullable: true }), + /** + * Output only. The state of the tuned model. + */ + "state": S.optionalWith(TunedModelState, { nullable: true }), + /** + * Output only. The timestamp when this model was created. + */ + "createTime": S.optionalWith(S.String, { nullable: true }), + /** + * Output only. The timestamp when this model was updated. + */ + "updateTime": S.optionalWith(S.String, { nullable: true }), + /** + * Required. The tuning task that creates the tuned model. + */ + "tuningTask": TuningTask, + /** + * Optional. List of project numbers that have read access to the tuned model. + */ + "readerProjectNumbers": S.optionalWith(S.Array(S.String), { nullable: true }) +}) {} + +export class UpdateTunedModelParams extends S.Struct({ + "updateMask": S.optionalWith(S.String.pipe(S.pattern(new RegExp("^(\\s*[^,\\s.]+(\\s*[,.]\\s*[^,\\s.]+)*)?$"))), { + nullable: true + }) +}) {} + +export class ListTunedModelsParams extends S.Struct({ + "pageSize": S.optionalWith(S.Int, { nullable: true }), + "pageToken": S.optionalWith(S.String, { nullable: true }), + "filter": S.optionalWith(S.String, { nullable: true }) +}) {} + +/** + * Response from `ListTunedModels` containing a paginated list of Models. + */ +export class ListTunedModelsResponse extends S.Class("ListTunedModelsResponse")({ + /** + * The returned Models. + */ + "tunedModels": S.optionalWith(S.Array(TunedModel), { nullable: true }), + /** + * A token, which can be sent as `page_token` to retrieve the next page. + * + * If this field is omitted, there are no more pages. + */ + "nextPageToken": S.optionalWith(S.String, { nullable: true }) +}) {} + +export class CreateTunedModelParams extends S.Struct({ + "tunedModelId": S.optionalWith(S.String, { nullable: true }) +}) {} + +/** + * Metadata about the state and progress of creating a tuned model returned from + * the long-running operation + */ +export class CreateTunedModelMetadata extends S.Class("CreateTunedModelMetadata")({ + /** + * Name of the tuned model associated with the tuning operation. + */ + "tunedModel": S.optionalWith(S.String, { nullable: true }), + /** + * The total number of tuning steps. + */ + "totalSteps": S.optionalWith(S.Int, { nullable: true }), + /** + * The number of steps completed. + */ + "completedSteps": S.optionalWith(S.Int, { nullable: true }), + /** + * The completed percentage for the tuning operation. + */ + "completedPercent": S.optionalWith(S.Number, { nullable: true }), + /** + * Metrics collected during tuning. + */ + "snapshots": S.optionalWith(S.Array(TuningSnapshot), { nullable: true }) +}) {} + +/** + * This resource represents a long-running operation that is the result of a + * network API call. + */ +export class CreateTunedModelOperation extends S.Class("CreateTunedModelOperation")({ + "metadata": S.optionalWith(CreateTunedModelMetadata, { nullable: true }), + "response": S.optionalWith(TunedModel, { nullable: true }), + /** + * The server-assigned name, which is only unique within the same service that + * originally returns it. If you use the default HTTP mapping, the + * `name` should be a resource name ending with `operations/{unique_id}`. + */ + "name": S.optionalWith(S.String, { nullable: true }), + /** + * If the value is `false`, it means the operation is still in progress. + * If `true`, the operation is completed, and either `error` or `response` is + * available. + */ + "done": S.optionalWith(S.Boolean, { nullable: true }), + /** + * The error result of the operation in case of failure or cancellation. + */ + "error": S.optionalWith(Status, { nullable: true }) +}) {} + +export class ListPermissionsParams extends S.Struct({ + "pageSize": S.optionalWith(S.Int, { nullable: true }), + "pageToken": S.optionalWith(S.String, { nullable: true }) +}) {} + +/** + * Optional. Immutable. The type of the grantee. + */ +export class PermissionGranteeType extends S.Literal("GRANTEE_TYPE_UNSPECIFIED", "USER", "GROUP", "EVERYONE") {} + +/** + * Required. The role granted by this permission. + */ +export class PermissionRole extends S.Literal("ROLE_UNSPECIFIED", "OWNER", "WRITER", "READER") {} + +/** + * Permission resource grants user, group or the rest of the world access to the + * PaLM API resource (e.g. a tuned model, corpus). + * + * A role is a collection of permitted operations that allows users to perform + * specific actions on PaLM API resources. To make them available to users, + * groups, or service accounts, you assign roles. When you assign a role, you + * grant permissions that the role contains. + * + * There are three concentric roles. Each role is a superset of the previous + * role's permitted operations: + * + * - reader can use the resource (e.g. tuned model, corpus) for inference + * - writer has reader's permissions and additionally can edit and share + * - owner has writer's permissions and additionally can delete + */ +export class Permission extends S.Class("Permission")({ + /** + * Output only. Identifier. The permission name. A unique name will be generated on create. + * Examples: + * tunedModels/{tuned_model}/permissions/{permission} + * corpora/{corpus}/permissions/{permission} + * Output only. + */ + "name": S.optionalWith(S.String, { nullable: true }), + /** + * Optional. Immutable. The type of the grantee. + */ + "granteeType": S.optionalWith(PermissionGranteeType, { nullable: true }), + /** + * Optional. Immutable. The email address of the user of group which this permission refers. + * Field is not set when permission's grantee type is EVERYONE. + */ + "emailAddress": S.optionalWith(S.String, { nullable: true }), + /** + * Required. The role granted by this permission. + */ + "role": PermissionRole +}) {} + +/** + * Response from `ListPermissions` containing a paginated list of + * permissions. + */ +export class ListPermissionsResponse extends S.Class("ListPermissionsResponse")({ + /** + * Returned permissions. + */ + "permissions": S.optionalWith(S.Array(Permission), { nullable: true }), + /** + * A token, which can be sent as `page_token` to retrieve the next page. + * + * If this field is omitted, there are no more pages. + */ + "nextPageToken": S.optionalWith(S.String, { nullable: true }) +}) {} + +export class ListPermissionsByCorpusParams extends S.Struct({ + "pageSize": S.optionalWith(S.Int, { nullable: true }), + "pageToken": S.optionalWith(S.String, { nullable: true }) +}) {} + +export class UpdatePermissionParams extends S.Struct({ + "updateMask": S.String.pipe(S.pattern(new RegExp("^(\\s*[^,\\s.]+(\\s*[,.]\\s*[^,\\s.]+)*)?$"))) +}) {} + +export class UpdatePermissionByCorpusAndPermissionParams extends S.Struct({ + "updateMask": S.String.pipe(S.pattern(new RegExp("^(\\s*[^,\\s.]+(\\s*[,.]\\s*[^,\\s.]+)*)?$"))) +}) {} + +/** + * Request to transfer the ownership of the tuned model. + */ +export class TransferOwnershipRequest extends S.Class("TransferOwnershipRequest")({ + /** + * Required. The email address of the user to whom the tuned model is being transferred + * to. + */ + "emailAddress": S.String +}) {} + +/** + * Response from `TransferOwnership`. + */ +export class TransferOwnershipResponse extends S.Record({ key: S.String, value: S.Unknown }) {} + +/** + * Request message for PredictionService.Predict. + */ +export class PredictRequest extends S.Class("PredictRequest")({}) {} + +/** + * Response message for [PredictionService.Predict]. + */ +export class PredictResponse extends S.Class("PredictResponse")({}) {} + +/** + * Request message for [PredictionService.PredictLongRunning]. + */ +export class PredictLongRunningRequest extends S.Class("PredictLongRunningRequest")({}) {} + +/** + * Metadata for PredictLongRunning long running operations. + */ +export class PredictLongRunningMetadata extends S.Record({ key: S.String, value: S.Unknown }) {} + +/** + * Representation of a video. + */ +export class Video extends S.Class

("Summary")({ + /** + * The type of the object. Always `summary_text`. + */ + "type": SummaryType.pipe(S.propertySignature, S.withConstructorDefault(() => "summary_text" as const)), + /** + * A summary of the reasoning output from the model so far. + */ + "text": S.String +}) {} + +/** + * The status of the item. One of `in_progress`, `completed`, or + * `incomplete`. Populated when items are returned via API. + */ +export class ReasoningItemStatus extends S.Literal("in_progress", "completed", "incomplete") {} + +/** + * A description of the chain of thought used by a reasoning model while generating + * a response. Be sure to include these items in your `input` to the Responses API + * for subsequent turns of a conversation if you are manually + * [managing context](https://platform.openai.com/docs/guides/conversation-state). + */ +export class ReasoningItem extends S.Class("ReasoningItem")({ + /** + * The type of the object. Always `reasoning`. + */ + "type": ReasoningItemType, + /** + * The unique identifier of the reasoning content. + */ + "id": S.String, + "encrypted_content": S.optionalWith(S.String, { nullable: true }), + /** + * Reasoning summary content. + */ + "summary": S.Array(Summary), + /** + * Reasoning text content. + */ + "content": S.optionalWith(S.Array(ReasoningTextContent), { nullable: true }), + /** + * The status of the item. One of `in_progress`, `completed`, or + * `incomplete`. Populated when items are returned via API. + */ + "status": S.optionalWith(ReasoningItemStatus, { nullable: true }) +}) {} + +/** + * The type of the code interpreter tool call. Always `code_interpreter_call`. + */ +export class CodeInterpreterToolCallType extends S.Literal("code_interpreter_call") {} + +/** + * The status of the code interpreter tool call. Valid values are `in_progress`, `completed`, `incomplete`, `interpreting`, and `failed`. + */ +export class CodeInterpreterToolCallStatus + extends S.Literal("in_progress", "completed", "incomplete", "interpreting", "failed") +{} + +/** + * The type of the output. Always `logs`. + */ +export class CodeInterpreterOutputLogsType extends S.Literal("logs") {} + +/** + * The logs output from the code interpreter. + */ +export class CodeInterpreterOutputLogs extends S.Class("CodeInterpreterOutputLogs")({ + /** + * The type of the output. Always `logs`. + */ + "type": CodeInterpreterOutputLogsType.pipe(S.propertySignature, S.withConstructorDefault(() => "logs" as const)), + /** + * The logs output from the code interpreter. + */ + "logs": S.String +}) {} + +/** + * The type of the output. Always `image`. + */ +export class CodeInterpreterOutputImageType extends S.Literal("image") {} + +/** + * The image output from the code interpreter. + */ +export class CodeInterpreterOutputImage extends S.Class("CodeInterpreterOutputImage")({ + /** + * The type of the output. Always `image`. + */ + "type": CodeInterpreterOutputImageType.pipe(S.propertySignature, S.withConstructorDefault(() => "image" as const)), + /** + * The URL of the image output from the code interpreter. + */ + "url": S.String +}) {} + +/** + * A tool call to run code. + */ +export class CodeInterpreterToolCall extends S.Class("CodeInterpreterToolCall")({ + /** + * The type of the code interpreter tool call. Always `code_interpreter_call`. + */ + "type": CodeInterpreterToolCallType.pipe( + S.propertySignature, + S.withConstructorDefault(() => "code_interpreter_call" as const) + ), + /** + * The unique ID of the code interpreter tool call. + */ + "id": S.String, + /** + * The status of the code interpreter tool call. Valid values are `in_progress`, `completed`, `incomplete`, `interpreting`, and `failed`. + */ + "status": CodeInterpreterToolCallStatus, + /** + * The ID of the container used to run the code. + */ + "container_id": S.String, + "code": S.NullOr(S.String), + "outputs": S.NullOr(S.Array(S.Union(CodeInterpreterOutputLogs, CodeInterpreterOutputImage))) +}) {} + +/** + * The type of the local shell call. Always `local_shell_call`. + */ +export class LocalShellToolCallType extends S.Literal("local_shell_call") {} + +/** + * The type of the local shell action. Always `exec`. + */ +export class LocalShellExecActionType extends S.Literal("exec") {} + +/** + * Execute a shell command on the server. + */ +export class LocalShellExecAction extends S.Class("LocalShellExecAction")({ + /** + * The type of the local shell action. Always `exec`. + */ + "type": LocalShellExecActionType.pipe(S.propertySignature, S.withConstructorDefault(() => "exec" as const)), + /** + * The command to run. + */ + "command": S.Array(S.String), + "timeout_ms": S.optionalWith(S.Int, { nullable: true }), + "working_directory": S.optionalWith(S.String, { nullable: true }), + /** + * Environment variables to set for the command. + */ + "env": S.Record({ key: S.String, value: S.Unknown }), + "user": S.optionalWith(S.String, { nullable: true }) +}) {} + +/** + * The status of the local shell call. + */ +export class LocalShellToolCallStatus extends S.Literal("in_progress", "completed", "incomplete") {} + +/** + * A tool call to run a command on the local shell. + */ +export class LocalShellToolCall extends S.Class("LocalShellToolCall")({ + /** + * The type of the local shell call. Always `local_shell_call`. + */ + "type": LocalShellToolCallType, + /** + * The unique ID of the local shell call. + */ + "id": S.String, + /** + * The unique ID of the local shell tool call generated by the model. + */ + "call_id": S.String, + "action": LocalShellExecAction, + /** + * The status of the local shell call. + */ + "status": LocalShellToolCallStatus +}) {} + +/** + * The type of the local shell tool call output. Always `local_shell_call_output`. + */ +export class LocalShellToolCallOutputType extends S.Literal("local_shell_call_output") {} + +/** + * The status of the item. One of `in_progress`, `completed`, or `incomplete`. + */ +export class LocalShellToolCallOutputStatusEnum extends S.Literal("in_progress", "completed", "incomplete") {} + +/** + * The output of a local shell tool call. + */ +export class LocalShellToolCallOutput extends S.Class("LocalShellToolCallOutput")({ + /** + * The type of the local shell tool call output. Always `local_shell_call_output`. + */ + "type": LocalShellToolCallOutputType, + /** + * The unique ID of the local shell tool call generated by the model. + */ + "id": S.String, + /** + * A JSON string of the output of the local shell tool call. + */ + "output": S.String, + "status": S.optionalWith(LocalShellToolCallOutputStatusEnum, { nullable: true }) +}) {} + +/** + * The type of the item. Always `shell_call`. + */ +export class FunctionShellCallType extends S.Literal("shell_call") {} + +/** + * Execute a shell command. + */ +export class FunctionShellAction extends S.Class("FunctionShellAction")({ + "commands": S.Array(S.String), + "timeout_ms": S.NullOr(S.Int), + "max_output_length": S.NullOr(S.Int) +}) {} + +export class LocalShellCallStatus extends S.Literal("in_progress", "completed", "incomplete") {} + +/** + * A tool call that executes one or more shell commands in a managed environment. + */ +export class FunctionShellCall extends S.Class("FunctionShellCall")({ + /** + * The type of the item. Always `shell_call`. + */ + "type": FunctionShellCallType.pipe(S.propertySignature, S.withConstructorDefault(() => "shell_call" as const)), + /** + * The unique ID of the function shell tool call. Populated when this item is returned via API. + */ + "id": S.String, + /** + * The unique ID of the function shell tool call generated by the model. + */ + "call_id": S.String, + /** + * The shell commands and limits that describe how to run the tool call. + */ + "action": FunctionShellAction, + /** + * The status of the shell call. One of `in_progress`, `completed`, or `incomplete`. + */ + "status": LocalShellCallStatus, + /** + * The ID of the entity that created this tool call. + */ + "created_by": S.optionalWith(S.String, { nullable: true }) +}) {} + +/** + * The type of the shell call output. Always `shell_call_output`. + */ +export class FunctionShellCallOutputType extends S.Literal("shell_call_output") {} + +/** + * The outcome type. Always `timeout`. + */ +export class FunctionShellCallOutputTimeoutOutcomeType extends S.Literal("timeout") {} + +/** + * Indicates that the function shell call exceeded its configured time limit. + */ +export class FunctionShellCallOutputTimeoutOutcome + extends S.Class("FunctionShellCallOutputTimeoutOutcome")({ + /** + * The outcome type. Always `timeout`. + */ + "type": FunctionShellCallOutputTimeoutOutcomeType.pipe( + S.propertySignature, + S.withConstructorDefault(() => "timeout" as const) + ) + }) +{} + +/** + * The outcome type. Always `exit`. + */ +export class FunctionShellCallOutputExitOutcomeType extends S.Literal("exit") {} + +/** + * Indicates that the shell commands finished and returned an exit code. + */ +export class FunctionShellCallOutputExitOutcome + extends S.Class("FunctionShellCallOutputExitOutcome")({ + /** + * The outcome type. Always `exit`. + */ + "type": FunctionShellCallOutputExitOutcomeType.pipe( + S.propertySignature, + S.withConstructorDefault(() => "exit" as const) + ), + /** + * Exit code from the shell process. + */ + "exit_code": S.Int + }) +{} + +/** + * The content of a shell call output. + */ +export class FunctionShellCallOutputContent + extends S.Class("FunctionShellCallOutputContent")({ + "stdout": S.String, + "stderr": S.String, + /** + * Represents either an exit outcome (with an exit code) or a timeout outcome for a shell call output chunk. + */ + "outcome": S.Union(FunctionShellCallOutputTimeoutOutcome, FunctionShellCallOutputExitOutcome), + "created_by": S.optionalWith(S.String, { nullable: true }) + }) +{} + +/** + * The output of a shell tool call. + */ +export class FunctionShellCallOutput extends S.Class("FunctionShellCallOutput")({ + /** + * The type of the shell call output. Always `shell_call_output`. + */ + "type": FunctionShellCallOutputType.pipe( + S.propertySignature, + S.withConstructorDefault(() => "shell_call_output" as const) + ), + /** + * The unique ID of the shell call output. Populated when this item is returned via API. + */ + "id": S.String, + /** + * The unique ID of the shell tool call generated by the model. + */ + "call_id": S.String, + /** + * An array of shell call output contents + */ + "output": S.Array(FunctionShellCallOutputContent), + "max_output_length": S.NullOr(S.Int), + "created_by": S.optionalWith(S.String, { nullable: true }) +}) {} + +/** + * The type of the item. Always `apply_patch_call`. + */ +export class ApplyPatchToolCallType extends S.Literal("apply_patch_call") {} + +export class ApplyPatchCallStatus extends S.Literal("in_progress", "completed") {} + +/** + * Create a new file with the provided diff. + */ +export class ApplyPatchCreateFileOperationType extends S.Literal("create_file") {} + +/** + * Instruction describing how to create a file via the apply_patch tool. + */ +export class ApplyPatchCreateFileOperation + extends S.Class("ApplyPatchCreateFileOperation")({ + /** + * Create a new file with the provided diff. + */ + "type": ApplyPatchCreateFileOperationType.pipe( + S.propertySignature, + S.withConstructorDefault(() => "create_file" as const) + ), + /** + * Path of the file to create. + */ + "path": S.String, + /** + * Diff to apply. + */ + "diff": S.String + }) +{} + +/** + * Delete the specified file. + */ +export class ApplyPatchDeleteFileOperationType extends S.Literal("delete_file") {} + +/** + * Instruction describing how to delete a file via the apply_patch tool. + */ +export class ApplyPatchDeleteFileOperation + extends S.Class("ApplyPatchDeleteFileOperation")({ + /** + * Delete the specified file. + */ + "type": ApplyPatchDeleteFileOperationType.pipe( + S.propertySignature, + S.withConstructorDefault(() => "delete_file" as const) + ), + /** + * Path of the file to delete. + */ + "path": S.String + }) +{} + +/** + * Update an existing file with the provided diff. + */ +export class ApplyPatchUpdateFileOperationType extends S.Literal("update_file") {} + +/** + * Instruction describing how to update a file via the apply_patch tool. + */ +export class ApplyPatchUpdateFileOperation + extends S.Class("ApplyPatchUpdateFileOperation")({ + /** + * Update an existing file with the provided diff. + */ + "type": ApplyPatchUpdateFileOperationType.pipe( + S.propertySignature, + S.withConstructorDefault(() => "update_file" as const) + ), + /** + * Path of the file to update. + */ + "path": S.String, + /** + * Diff to apply. + */ + "diff": S.String + }) +{} + +/** + * A tool call that applies file diffs by creating, deleting, or updating files. + */ +export class ApplyPatchToolCall extends S.Class("ApplyPatchToolCall")({ + /** + * The type of the item. Always `apply_patch_call`. + */ + "type": ApplyPatchToolCallType.pipe(S.propertySignature, S.withConstructorDefault(() => "apply_patch_call" as const)), + /** + * The unique ID of the apply patch tool call. Populated when this item is returned via API. + */ + "id": S.String, + /** + * The unique ID of the apply patch tool call generated by the model. + */ + "call_id": S.String, + /** + * The status of the apply patch tool call. One of `in_progress` or `completed`. + */ + "status": ApplyPatchCallStatus, + /** + * One of the create_file, delete_file, or update_file operations applied via apply_patch. + */ + "operation": S.Union(ApplyPatchCreateFileOperation, ApplyPatchDeleteFileOperation, ApplyPatchUpdateFileOperation), + /** + * The ID of the entity that created this tool call. + */ + "created_by": S.optionalWith(S.String, { nullable: true }) +}) {} + +/** + * The type of the item. Always `apply_patch_call_output`. + */ +export class ApplyPatchToolCallOutputType extends S.Literal("apply_patch_call_output") {} + +export class ApplyPatchCallOutputStatus extends S.Literal("completed", "failed") {} + +/** + * The output emitted by an apply patch tool call. + */ +export class ApplyPatchToolCallOutput extends S.Class("ApplyPatchToolCallOutput")({ + /** + * The type of the item. Always `apply_patch_call_output`. + */ + "type": ApplyPatchToolCallOutputType.pipe( + S.propertySignature, + S.withConstructorDefault(() => "apply_patch_call_output" as const) + ), + /** + * The unique ID of the apply patch tool call output. Populated when this item is returned via API. + */ + "id": S.String, + /** + * The unique ID of the apply patch tool call generated by the model. + */ + "call_id": S.String, + /** + * The status of the apply patch tool call output. One of `completed` or `failed`. + */ + "status": ApplyPatchCallOutputStatus, + "output": S.optionalWith(S.String, { nullable: true }), + /** + * The ID of the entity that created this tool call output. + */ + "created_by": S.optionalWith(S.String, { nullable: true }) +}) {} + +/** + * The type of the item. Always `mcp_list_tools`. + */ +export class MCPListToolsType extends S.Literal("mcp_list_tools") {} + +/** + * A tool available on an MCP server. + */ +export class MCPListToolsTool extends S.Class("MCPListToolsTool")({ + /** + * The name of the tool. + */ + "name": S.String, + "description": S.optionalWith(S.String, { nullable: true }), + /** + * The JSON schema describing the tool's input. + */ + "input_schema": S.Record({ key: S.String, value: S.Unknown }), + "annotations": S.optionalWith(S.Record({ key: S.String, value: S.Unknown }), { nullable: true }) +}) {} + +/** + * A list of tools available on an MCP server. + */ +export class MCPListTools extends S.Class("MCPListTools")({ + /** + * The type of the item. Always `mcp_list_tools`. + */ + "type": MCPListToolsType, + /** + * The unique ID of the list. + */ + "id": S.String, + /** + * The label of the MCP server. + */ + "server_label": S.String, + /** + * The tools available on the server. + */ + "tools": S.Array(MCPListToolsTool), + "error": S.optionalWith(S.String, { nullable: true }) +}) {} + +/** + * The type of the item. Always `mcp_approval_request`. + */ +export class MCPApprovalRequestType extends S.Literal("mcp_approval_request") {} + +/** + * A request for human approval of a tool invocation. + */ +export class MCPApprovalRequest extends S.Class("MCPApprovalRequest")({ + /** + * The type of the item. Always `mcp_approval_request`. + */ + "type": MCPApprovalRequestType, + /** + * The unique ID of the approval request. + */ + "id": S.String, + /** + * The label of the MCP server making the request. + */ + "server_label": S.String, + /** + * The name of the tool to run. + */ + "name": S.String, + /** + * A JSON string of arguments for the tool. + */ + "arguments": S.String +}) {} + +/** + * The type of the item. Always `mcp_approval_response`. + */ +export class MCPApprovalResponseResourceType extends S.Literal("mcp_approval_response") {} + +/** + * A response to an MCP approval request. + */ +export class MCPApprovalResponseResource extends S.Class("MCPApprovalResponseResource")({ + /** + * The type of the item. Always `mcp_approval_response`. + */ + "type": MCPApprovalResponseResourceType, + /** + * The unique ID of the approval response + */ + "id": S.String, + /** + * The ID of the approval request being answered. + */ + "approval_request_id": S.String, + /** + * Whether the request was approved. + */ + "approve": S.Boolean, + "reason": S.optionalWith(S.String, { nullable: true }) +}) {} + +/** + * The type of the item. Always `mcp_call`. + */ +export class MCPToolCallType extends S.Literal("mcp_call") {} + +export class MCPToolCallStatus extends S.Literal("in_progress", "completed", "incomplete", "calling", "failed") {} + +/** + * An invocation of a tool on an MCP server. + */ +export class MCPToolCall extends S.Class("MCPToolCall")({ + /** + * The type of the item. Always `mcp_call`. + */ + "type": MCPToolCallType, + /** + * The unique ID of the tool call. + */ + "id": S.String, + /** + * The label of the MCP server running the tool. + */ + "server_label": S.String, + /** + * The name of the tool that was run. + */ + "name": S.String, + /** + * A JSON string of the arguments passed to the tool. + */ + "arguments": S.String, + "output": S.optionalWith(S.String, { nullable: true }), + "error": S.optionalWith(S.String, { nullable: true }), + /** + * The status of the tool call. One of `in_progress`, `completed`, `incomplete`, `calling`, or `failed`. + */ + "status": S.optionalWith(MCPToolCallStatus, { nullable: true }), + "approval_request_id": S.optionalWith(S.String, { nullable: true }) +}) {} + +/** + * The type of the custom tool call. Always `custom_tool_call`. + */ +export class CustomToolCallType extends S.Literal("custom_tool_call") {} + +/** + * A call to a custom tool created by the model. + */ +export class CustomToolCall extends S.Class("CustomToolCall")({ + /** + * The type of the custom tool call. Always `custom_tool_call`. + */ + "type": CustomToolCallType, + /** + * The unique ID of the custom tool call in the OpenAI platform. + */ + "id": S.optionalWith(S.String, { nullable: true }), + /** + * An identifier used to map this custom tool call to a tool call output. + */ + "call_id": S.String, + /** + * The name of the custom tool being called. + */ + "name": S.String, + /** + * The input for the custom tool call generated by the model. + */ + "input": S.String +}) {} + +/** + * The type of the custom tool call output. Always `custom_tool_call_output`. + */ +export class CustomToolCallOutputType extends S.Literal("custom_tool_call_output") {} + +/** + * The output of a custom tool call from your code, being sent back to the model. + */ +export class CustomToolCallOutput extends S.Class("CustomToolCallOutput")({ + /** + * The type of the custom tool call output. Always `custom_tool_call_output`. + */ + "type": CustomToolCallOutputType, + /** + * The unique ID of the custom tool call output in the OpenAI platform. + */ + "id": S.optionalWith(S.String, { nullable: true }), + /** + * The call ID, used to map this custom tool call output to a custom tool call. + */ + "call_id": S.String, + /** + * The output from the custom tool call generated by your code. + * Can be a string or an list of output content. + */ + "output": S.Union( + /** + * A string of the output of the custom tool call. + */ + S.String, + /** + * Text, image, or file output of the custom tool call. + */ + S.Array(FunctionAndCustomToolCallOutput) + ) +}) {} + +/** + * A single item within a conversation. The set of possible types are the same as the `output` type of a [Response object](https://platform.openai.com/docs/api-reference/responses/object#responses/object-output). + */ +export class ConversationItem extends S.Union( + Message, + FunctionToolCallResource, + FunctionToolCallOutputResource, + FileSearchToolCall, + WebSearchToolCall, + ImageGenToolCall, + ComputerToolCall, + ComputerToolCallOutputResource, + ReasoningItem, + CodeInterpreterToolCall, + LocalShellToolCall, + LocalShellToolCallOutput, + FunctionShellCall, + FunctionShellCallOutput, + ApplyPatchToolCall, + ApplyPatchToolCallOutput, + MCPListTools, + MCPApprovalRequest, + MCPApprovalResponseResource, + MCPToolCall, + CustomToolCall, + CustomToolCallOutput +) {} + +/** + * A list of Conversation items. + */ +export class ConversationItemList extends S.Class("ConversationItemList")({ + /** + * The type of object returned, must be `list`. + */ + "object": S.Literal("list"), + /** + * A list of conversation items. + */ + "data": S.Array(ConversationItem), + /** + * Whether there are more items available. + */ + "has_more": S.Boolean, + /** + * The ID of the first item in the list. + */ + "first_id": S.String, + /** + * The ID of the last item in the list. + */ + "last_id": S.String +}) {} + +export class CreateConversationItemsParams extends S.Struct({ + "include": S.optionalWith(S.Array(IncludeEnum), { nullable: true }) +}) {} + +/** + * The role of the message input. One of `user`, `assistant`, `system`, or + * `developer`. + */ +export class EasyInputMessageRole extends S.Literal("user", "assistant", "system", "developer") {} + +export class InputContent extends S.Union(InputTextContent, InputImageContent, InputFileContent) {} + +/** + * A list of one or many input items to the model, containing different content + * types. + */ +export class InputMessageContentList extends S.Array(InputContent) {} + +/** + * The type of the message input. Always `message`. + */ +export class EasyInputMessageType extends S.Literal("message") {} + +/** + * A message input to the model with a role indicating instruction following + * hierarchy. Instructions given with the `developer` or `system` role take + * precedence over instructions given with the `user` role. Messages with the + * `assistant` role are presumed to have been generated by the model in previous + * interactions. + */ +export class EasyInputMessage extends S.Class("EasyInputMessage")({ + /** + * The role of the message input. One of `user`, `assistant`, `system`, or + * `developer`. + */ + "role": EasyInputMessageRole, + /** + * Text, image, or audio input to the model, used to generate a response. + * Can also contain previous assistant responses. + */ + "content": S.Union( + /** + * A text input to the model. + */ + S.String, + InputMessageContentList + ), + /** + * The type of the message input. Always `message`. + */ + "type": S.optionalWith(EasyInputMessageType, { nullable: true }) +}) {} + +/** + * The type of the message input. Always set to `message`. + */ +export class InputMessageType extends S.Literal("message") {} + +/** + * The role of the message input. One of `user`, `system`, or `developer`. + */ +export class InputMessageRole extends S.Literal("user", "system", "developer") {} + +/** + * The status of item. One of `in_progress`, `completed`, or + * `incomplete`. Populated when items are returned via API. + */ +export class InputMessageStatus extends S.Literal("in_progress", "completed", "incomplete") {} + +/** + * A message input to the model with a role indicating instruction following + * hierarchy. Instructions given with the `developer` or `system` role take + * precedence over instructions given with the `user` role. + */ +export class InputMessage extends S.Class("InputMessage")({ + /** + * The type of the message input. Always set to `message`. + */ + "type": S.optionalWith(InputMessageType, { nullable: true }), + /** + * The role of the message input. One of `user`, `system`, or `developer`. + */ + "role": InputMessageRole, + /** + * The status of item. One of `in_progress`, `completed`, or + * `incomplete`. Populated when items are returned via API. + */ + "status": S.optionalWith(InputMessageStatus, { nullable: true }), + "content": InputMessageContentList +}) {} + +/** + * The type of the output message. Always `message`. + */ +export class OutputMessageType extends S.Literal("message") {} + +/** + * The role of the output message. Always `assistant`. + */ +export class OutputMessageRole extends S.Literal("assistant") {} + +export class OutputMessageContent extends S.Union(OutputTextContent, RefusalContent) {} + +/** + * The status of the message input. One of `in_progress`, `completed`, or + * `incomplete`. Populated when input items are returned via API. + */ +export class OutputMessageStatus extends S.Literal("in_progress", "completed", "incomplete") {} + +/** + * An output message from the model. + */ +export class OutputMessage extends S.Class("OutputMessage")({ + /** + * The unique ID of the output message. + */ + "id": S.String, + /** + * The type of the output message. Always `message`. + */ + "type": OutputMessageType, + /** + * The role of the output message. Always `assistant`. + */ + "role": OutputMessageRole, + /** + * The content of the output message. + */ + "content": S.Array(OutputMessageContent), + /** + * The status of the message input. One of `in_progress`, `completed`, or + * `incomplete`. Populated when input items are returned via API. + */ + "status": OutputMessageStatus +}) {} + +/** + * The type of the computer tool call output. Always `computer_call_output`. + */ +export class ComputerCallOutputItemParamType extends S.Literal("computer_call_output") {} + +export class FunctionCallItemStatus extends S.Literal("in_progress", "completed", "incomplete") {} + +/** + * The output of a computer tool call. + */ +export class ComputerCallOutputItemParam extends S.Class("ComputerCallOutputItemParam")({ + "id": S.optionalWith(S.String, { nullable: true }), + /** + * The ID of the computer tool call that produced the output. + */ + "call_id": S.String.pipe(S.minLength(1), S.maxLength(64)), + /** + * The type of the computer tool call output. Always `computer_call_output`. + */ + "type": ComputerCallOutputItemParamType.pipe( + S.propertySignature, + S.withConstructorDefault(() => "computer_call_output" as const) + ), + "output": ComputerScreenshotImage, + "acknowledged_safety_checks": S.optionalWith(S.Array(ComputerCallSafetyCheckParam), { nullable: true }), + "status": S.optionalWith(FunctionCallItemStatus, { nullable: true }) +}) {} + +/** + * The type of the function tool call. Always `function_call`. + */ +export class FunctionToolCallType extends S.Literal("function_call") {} + +/** + * The status of the item. One of `in_progress`, `completed`, or + * `incomplete`. Populated when items are returned via API. + */ +export class FunctionToolCallStatus extends S.Literal("in_progress", "completed", "incomplete") {} + +/** + * A tool call to run a function. See the + * [function calling guide](https://platform.openai.com/docs/guides/function-calling) for more information. + */ +export class FunctionToolCall extends S.Class("FunctionToolCall")({ + /** + * The unique ID of the function tool call. + */ + "id": S.optionalWith(S.String, { nullable: true }), + /** + * The type of the function tool call. Always `function_call`. + */ + "type": FunctionToolCallType, + /** + * The unique ID of the function tool call generated by the model. + */ + "call_id": S.String, + /** + * The name of the function to run. + */ + "name": S.String, + /** + * A JSON string of the arguments to pass to the function. + */ + "arguments": S.String, + /** + * The status of the item. One of `in_progress`, `completed`, or + * `incomplete`. Populated when items are returned via API. + */ + "status": S.optionalWith(FunctionToolCallStatus, { nullable: true }) +}) {} + +/** + * The type of the function tool call output. Always `function_call_output`. + */ +export class FunctionCallOutputItemParamType extends S.Literal("function_call_output") {} + +/** + * The type of the input item. Always `input_text`. + */ +export class InputTextContentParamType extends S.Literal("input_text") {} + +/** + * A text input to the model. + */ +export class InputTextContentParam extends S.Class("InputTextContentParam")({ + /** + * The type of the input item. Always `input_text`. + */ + "type": InputTextContentParamType.pipe(S.propertySignature, S.withConstructorDefault(() => "input_text" as const)), + /** + * The text input to the model. + */ + "text": S.String.pipe(S.maxLength(10485760)) +}) {} + +/** + * The type of the input item. Always `input_image`. + */ +export class InputImageContentParamAutoParamType extends S.Literal("input_image") {} + +export class DetailEnum extends S.Literal("low", "high", "auto") {} + +/** + * An image input to the model. Learn about [image inputs](https://platform.openai.com/docs/guides/vision) + */ +export class InputImageContentParamAutoParam + extends S.Class("InputImageContentParamAutoParam")({ + /** + * The type of the input item. Always `input_image`. + */ + "type": InputImageContentParamAutoParamType.pipe( + S.propertySignature, + S.withConstructorDefault(() => "input_image" as const) + ), + "image_url": S.optionalWith(S.String.pipe(S.maxLength(20971520)), { nullable: true }), + "file_id": S.optionalWith(S.String, { nullable: true }), + "detail": S.optionalWith(DetailEnum, { nullable: true }) + }) +{} + +/** + * The type of the input item. Always `input_file`. + */ +export class InputFileContentParamType extends S.Literal("input_file") {} + +/** + * A file input to the model. + */ +export class InputFileContentParam extends S.Class("InputFileContentParam")({ + /** + * The type of the input item. Always `input_file`. + */ + "type": InputFileContentParamType.pipe(S.propertySignature, S.withConstructorDefault(() => "input_file" as const)), + "file_id": S.optionalWith(S.String, { nullable: true }), + "filename": S.optionalWith(S.String, { nullable: true }), + "file_data": S.optionalWith(S.String.pipe(S.maxLength(33554432)), { nullable: true }), + "file_url": S.optionalWith(S.String, { nullable: true }) +}) {} + +/** + * The output of a function tool call. + */ +export class FunctionCallOutputItemParam extends S.Class("FunctionCallOutputItemParam")({ + "id": S.optionalWith(S.String, { nullable: true }), + /** + * The unique ID of the function tool call generated by the model. + */ + "call_id": S.String.pipe(S.minLength(1), S.maxLength(64)), + /** + * The type of the function tool call output. Always `function_call_output`. + */ + "type": FunctionCallOutputItemParamType.pipe( + S.propertySignature, + S.withConstructorDefault(() => "function_call_output" as const) + ), + /** + * Text, image, or file output of the function tool call. + */ + "output": S.Union( + /** + * A JSON string of the output of the function tool call. + */ + S.String.pipe(S.maxLength(10485760)), + S.Array(S.Union(InputTextContentParam, InputImageContentParamAutoParam, InputFileContentParam)) + ), + "status": S.optionalWith(FunctionCallItemStatus, { nullable: true }) +}) {} + +/** + * The type of the item. Always `function_shell_call`. + */ +export class FunctionShellCallItemParamType extends S.Literal("shell_call") {} + +/** + * Commands and limits describing how to run the function shell tool call. + */ +export class FunctionShellActionParam extends S.Class("FunctionShellActionParam")({ + /** + * Ordered shell commands for the execution environment to run. + */ + "commands": S.Array(S.String), + "timeout_ms": S.optionalWith(S.Int, { nullable: true }), + "max_output_length": S.optionalWith(S.Int, { nullable: true }) +}) {} + +/** + * Status values reported for function shell tool calls. + */ +export class FunctionShellCallItemStatus extends S.Literal("in_progress", "completed", "incomplete") {} + +/** + * A tool representing a request to execute one or more shell commands. + */ +export class FunctionShellCallItemParam extends S.Class("FunctionShellCallItemParam")({ + "id": S.optionalWith(S.String, { nullable: true }), + /** + * The unique ID of the function shell tool call generated by the model. + */ + "call_id": S.String.pipe(S.minLength(1), S.maxLength(64)), + /** + * The type of the item. Always `function_shell_call`. + */ + "type": FunctionShellCallItemParamType.pipe( + S.propertySignature, + S.withConstructorDefault(() => "shell_call" as const) + ), + /** + * The shell commands and limits that describe how to run the tool call. + */ + "action": FunctionShellActionParam, + "status": S.optionalWith(FunctionShellCallItemStatus, { nullable: true }) +}) {} + +/** + * The type of the item. Always `function_shell_call_output`. + */ +export class FunctionShellCallOutputItemParamType extends S.Literal("shell_call_output") {} + +/** + * The outcome type. Always `timeout`. + */ +export class FunctionShellCallOutputTimeoutOutcomeParamType extends S.Literal("timeout") {} + +/** + * Indicates that the function shell call exceeded its configured time limit. + */ +export class FunctionShellCallOutputTimeoutOutcomeParam + extends S.Class("FunctionShellCallOutputTimeoutOutcomeParam")({ + /** + * The outcome type. Always `timeout`. + */ + "type": FunctionShellCallOutputTimeoutOutcomeParamType.pipe( + S.propertySignature, + S.withConstructorDefault(() => "timeout" as const) + ) + }) +{} + +/** + * The outcome type. Always `exit`. + */ +export class FunctionShellCallOutputExitOutcomeParamType extends S.Literal("exit") {} + +/** + * Indicates that the shell commands finished and returned an exit code. + */ +export class FunctionShellCallOutputExitOutcomeParam + extends S.Class("FunctionShellCallOutputExitOutcomeParam")({ + /** + * The outcome type. Always `exit`. + */ + "type": FunctionShellCallOutputExitOutcomeParamType.pipe( + S.propertySignature, + S.withConstructorDefault(() => "exit" as const) + ), + /** + * The exit code returned by the shell process. + */ + "exit_code": S.Int + }) +{} + +/** + * The exit or timeout outcome associated with this chunk. + */ +export class FunctionShellCallOutputOutcomeParam + extends S.Union(FunctionShellCallOutputTimeoutOutcomeParam, FunctionShellCallOutputExitOutcomeParam) +{} + +/** + * Captured stdout and stderr for a portion of a function shell tool call output. + */ +export class FunctionShellCallOutputContentParam + extends S.Class("FunctionShellCallOutputContentParam")({ + /** + * Captured stdout output for this chunk of the shell call. + */ + "stdout": S.String.pipe(S.maxLength(10485760)), + /** + * Captured stderr output for this chunk of the shell call. + */ + "stderr": S.String.pipe(S.maxLength(10485760)), + /** + * The exit or timeout outcome associated with this chunk. + */ + "outcome": FunctionShellCallOutputOutcomeParam + }) +{} + +/** + * The streamed output items emitted by a function shell tool call. + */ +export class FunctionShellCallOutputItemParam + extends S.Class("FunctionShellCallOutputItemParam")({ + "id": S.optionalWith(S.String, { nullable: true }), + /** + * The unique ID of the function shell tool call generated by the model. + */ + "call_id": S.String.pipe(S.minLength(1), S.maxLength(64)), + /** + * The type of the item. Always `function_shell_call_output`. + */ + "type": FunctionShellCallOutputItemParamType.pipe( + S.propertySignature, + S.withConstructorDefault(() => "shell_call_output" as const) + ), + /** + * Captured chunks of stdout and stderr output, along with their associated outcomes. + */ + "output": S.Array(FunctionShellCallOutputContentParam), + "max_output_length": S.optionalWith(S.Int, { nullable: true }) + }) +{} + +/** + * The type of the item. Always `apply_patch_call`. + */ +export class ApplyPatchToolCallItemParamType extends S.Literal("apply_patch_call") {} + +/** + * Status values reported for apply_patch tool calls. + */ +export class ApplyPatchCallStatusParam extends S.Literal("in_progress", "completed") {} + +/** + * The operation type. Always `create_file`. + */ +export class ApplyPatchCreateFileOperationParamType extends S.Literal("create_file") {} + +/** + * Instruction for creating a new file via the apply_patch tool. + */ +export class ApplyPatchCreateFileOperationParam + extends S.Class("ApplyPatchCreateFileOperationParam")({ + /** + * The operation type. Always `create_file`. + */ + "type": ApplyPatchCreateFileOperationParamType.pipe( + S.propertySignature, + S.withConstructorDefault(() => "create_file" as const) + ), + /** + * Path of the file to create relative to the workspace root. + */ + "path": S.String.pipe(S.minLength(1)), + /** + * Unified diff content to apply when creating the file. + */ + "diff": S.String.pipe(S.maxLength(10485760)) + }) +{} + +/** + * The operation type. Always `delete_file`. + */ +export class ApplyPatchDeleteFileOperationParamType extends S.Literal("delete_file") {} + +/** + * Instruction for deleting an existing file via the apply_patch tool. + */ +export class ApplyPatchDeleteFileOperationParam + extends S.Class("ApplyPatchDeleteFileOperationParam")({ + /** + * The operation type. Always `delete_file`. + */ + "type": ApplyPatchDeleteFileOperationParamType.pipe( + S.propertySignature, + S.withConstructorDefault(() => "delete_file" as const) + ), + /** + * Path of the file to delete relative to the workspace root. + */ + "path": S.String.pipe(S.minLength(1)) + }) +{} + +/** + * The operation type. Always `update_file`. + */ +export class ApplyPatchUpdateFileOperationParamType extends S.Literal("update_file") {} + +/** + * Instruction for updating an existing file via the apply_patch tool. + */ +export class ApplyPatchUpdateFileOperationParam + extends S.Class("ApplyPatchUpdateFileOperationParam")({ + /** + * The operation type. Always `update_file`. + */ + "type": ApplyPatchUpdateFileOperationParamType.pipe( + S.propertySignature, + S.withConstructorDefault(() => "update_file" as const) + ), + /** + * Path of the file to update relative to the workspace root. + */ + "path": S.String.pipe(S.minLength(1)), + /** + * Unified diff content to apply to the existing file. + */ + "diff": S.String.pipe(S.maxLength(10485760)) + }) +{} + +/** + * One of the create_file, delete_file, or update_file operations supplied to the apply_patch tool. + */ +export class ApplyPatchOperationParam extends S.Union( + ApplyPatchCreateFileOperationParam, + ApplyPatchDeleteFileOperationParam, + ApplyPatchUpdateFileOperationParam +) {} + +/** + * A tool call representing a request to create, delete, or update files using diff patches. + */ +export class ApplyPatchToolCallItemParam extends S.Class("ApplyPatchToolCallItemParam")({ + /** + * The type of the item. Always `apply_patch_call`. + */ + "type": ApplyPatchToolCallItemParamType.pipe( + S.propertySignature, + S.withConstructorDefault(() => "apply_patch_call" as const) + ), + "id": S.optionalWith(S.String, { nullable: true }), + /** + * The unique ID of the apply patch tool call generated by the model. + */ + "call_id": S.String.pipe(S.minLength(1), S.maxLength(64)), + /** + * The status of the apply patch tool call. One of `in_progress` or `completed`. + */ + "status": ApplyPatchCallStatusParam, + /** + * The specific create, delete, or update instruction for the apply_patch tool call. + */ + "operation": ApplyPatchOperationParam +}) {} + +/** + * The type of the item. Always `apply_patch_call_output`. + */ +export class ApplyPatchToolCallOutputItemParamType extends S.Literal("apply_patch_call_output") {} + +/** + * Outcome values reported for apply_patch tool call outputs. + */ +export class ApplyPatchCallOutputStatusParam extends S.Literal("completed", "failed") {} + +/** + * The streamed output emitted by an apply patch tool call. + */ +export class ApplyPatchToolCallOutputItemParam + extends S.Class("ApplyPatchToolCallOutputItemParam")({ + /** + * The type of the item. Always `apply_patch_call_output`. + */ + "type": ApplyPatchToolCallOutputItemParamType.pipe( + S.propertySignature, + S.withConstructorDefault(() => "apply_patch_call_output" as const) + ), + "id": S.optionalWith(S.String, { nullable: true }), + /** + * The unique ID of the apply patch tool call generated by the model. + */ + "call_id": S.String.pipe(S.minLength(1), S.maxLength(64)), + /** + * The status of the apply patch tool call output. One of `completed` or `failed`. + */ + "status": ApplyPatchCallOutputStatusParam, + "output": S.optionalWith(S.String.pipe(S.maxLength(10485760)), { nullable: true }) + }) +{} + +/** + * The type of the item. Always `mcp_approval_response`. + */ +export class MCPApprovalResponseType extends S.Literal("mcp_approval_response") {} + +/** + * A response to an MCP approval request. + */ +export class MCPApprovalResponse extends S.Class("MCPApprovalResponse")({ + /** + * The type of the item. Always `mcp_approval_response`. + */ + "type": MCPApprovalResponseType, + "id": S.optionalWith(S.String, { nullable: true }), + /** + * The ID of the approval request being answered. + */ + "approval_request_id": S.String, + /** + * Whether the request was approved. + */ + "approve": S.Boolean, + "reason": S.optionalWith(S.String, { nullable: true }) +}) {} + +/** + * Content item used to generate a response. + */ +export class Item extends S.Record({ key: S.String, value: S.Unknown }) {} + +/** + * The type of item to reference. Always `item_reference`. + */ +export class ItemReferenceParamTypeEnum extends S.Literal("item_reference") {} + +/** + * An internal identifier for an item to reference. + */ +export class ItemReferenceParam extends S.Class("ItemReferenceParam")({ + "type": S.optionalWith(ItemReferenceParamTypeEnum, { nullable: true }), + /** + * The ID of the item to reference. + */ + "id": S.String +}) {} + +export class InputItem extends S.Union( + EasyInputMessage, + /** + * An item representing part of the context for the response to be + * generated by the model. Can contain text, images, and audio inputs, + * as well as previous assistant responses and tool call outputs. + */ + S.Record({ key: S.String, value: S.Unknown }), + ItemReferenceParam +) {} + +export class CreateConversationItemsRequest + extends S.Class("CreateConversationItemsRequest")({ + /** + * The items to add to the conversation. You may add up to 20 items at a time. + */ + "items": S.Array(InputItem).pipe(S.maxItems(20)) + }) +{} + +export class GetConversationItemParams extends S.Struct({ + "include": S.optionalWith(S.Array(IncludeEnum), { nullable: true }) +}) {} + +/** + * The object type, which is always `conversation`. + */ +export class ConversationResourceObject extends S.Literal("conversation") {} + +export class ConversationResource extends S.Class("ConversationResource")({ + /** + * The unique ID of the conversation. + */ + "id": S.String, + /** + * The object type, which is always `conversation`. + */ + "object": ConversationResourceObject.pipe( + S.propertySignature, + S.withConstructorDefault(() => "conversation" as const) + ), + /** + * The time at which the conversation was created, measured in seconds since the Unix epoch. + */ + "created_at": S.Int +}) {} + +export class CreateEmbeddingRequestModelEnum + extends S.Literal("text-embedding-ada-002", "text-embedding-3-small", "text-embedding-3-large") +{} + +/** + * The format to return the embeddings in. Can be either `float` or [`base64`](https://pypi.org/project/pybase64/). + */ +export class CreateEmbeddingRequestEncodingFormat extends S.Literal("float", "base64") {} + +export class CreateEmbeddingRequest extends S.Class("CreateEmbeddingRequest")({ + /** + * Input text to embed, encoded as a string or array of tokens. To embed multiple inputs in a single request, pass an array of strings or array of token arrays. The input must not exceed the max input tokens for the model (8192 tokens for all embedding models), cannot be an empty string, and any array must be 2048 dimensions or less. [Example Python code](https://cookbook.openai.com/examples/how_to_count_tokens_with_tiktoken) for counting tokens. In addition to the per-input token limit, all embedding models enforce a maximum of 300,000 tokens summed across all inputs in a single request. + */ + "input": S.Union( + /** + * The string that will be turned into an embedding. + */ + S.String, + /** + * The array of strings that will be turned into an embedding. + */ + S.NonEmptyArray(S.String).pipe(S.minItems(1), S.maxItems(2048)), + /** + * The array of integers that will be turned into an embedding. + */ + S.NonEmptyArray(S.Int).pipe(S.minItems(1), S.maxItems(2048)), + /** + * The array of arrays containing integers that will be turned into an embedding. + */ + S.NonEmptyArray(S.NonEmptyArray(S.Int).pipe(S.minItems(1))).pipe(S.minItems(1), S.maxItems(2048)) + ), + /** + * ID of the model to use. You can use the [List models](https://platform.openai.com/docs/api-reference/models/list) API to see all of your available models, or see our [Model overview](https://platform.openai.com/docs/models) for descriptions of them. + */ + "model": S.Union(S.String, CreateEmbeddingRequestModelEnum), + /** + * The format to return the embeddings in. Can be either `float` or [`base64`](https://pypi.org/project/pybase64/). + */ + "encoding_format": S.optionalWith(CreateEmbeddingRequestEncodingFormat, { + nullable: true, + default: () => "float" as const + }), + /** + * The number of dimensions the resulting output embeddings should have. Only supported in `text-embedding-3` and later models. + */ + "dimensions": S.optionalWith(S.Int.pipe(S.greaterThanOrEqualTo(1)), { nullable: true }), + /** + * A unique identifier representing your end-user, which can help OpenAI to monitor and detect abuse. [Learn more](https://platform.openai.com/docs/guides/safety-best-practices#end-user-ids). + */ + "user": S.optionalWith(S.String, { nullable: true }) +}) {} + +/** + * The object type, which is always "embedding". + */ +export class EmbeddingObject extends S.Literal("embedding") {} + +/** + * Represents an embedding vector returned by embedding endpoint. + */ +export class Embedding extends S.Class("Embedding")({ + /** + * The index of the embedding in the list of embeddings. + */ + "index": S.Int, + /** + * The embedding vector, which is a list of floats. The length of vector depends on the model as listed in the [embedding guide](https://platform.openai.com/docs/guides/embeddings). + */ + "embedding": S.Array(S.Number), + /** + * The object type, which is always "embedding". + */ + "object": EmbeddingObject +}) {} + +/** + * The object type, which is always "list". + */ +export class CreateEmbeddingResponseObject extends S.Literal("list") {} + +export class CreateEmbeddingResponse extends S.Class("CreateEmbeddingResponse")({ + /** + * The list of embeddings generated by the model. + */ + "data": S.Array(Embedding), + /** + * The name of the model used to generate the embedding. + */ + "model": S.String, + /** + * The object type, which is always "list". + */ + "object": CreateEmbeddingResponseObject, + /** + * The usage information for the request. + */ + "usage": S.Struct({ + /** + * The number of tokens used by the prompt. + */ + "prompt_tokens": S.Int, + /** + * The total number of tokens used by the request. + */ + "total_tokens": S.Int + }) +}) {} + +export class ListEvalsParamsOrder extends S.Literal("asc", "desc") {} + +export class ListEvalsParamsOrderBy extends S.Literal("created_at", "updated_at") {} + +export class ListEvalsParams extends S.Struct({ + "after": S.optionalWith(S.String, { nullable: true }), + "limit": S.optionalWith(S.Int, { nullable: true, default: () => 20 as const }), + "order": S.optionalWith(ListEvalsParamsOrder, { nullable: true, default: () => "asc" as const }), + "order_by": S.optionalWith(ListEvalsParamsOrderBy, { nullable: true, default: () => "created_at" as const }) +}) {} + +/** + * The type of this object. It is always set to "list". + */ +export class EvalListObject extends S.Literal("list") {} + +/** + * The object type. + */ +export class EvalObject extends S.Literal("eval") {} + +/** + * The type of data source. Always `custom`. + */ +export class EvalCustomDataSourceConfigType extends S.Literal("custom") {} + +/** + * A CustomDataSourceConfig which specifies the schema of your `item` and optionally `sample` namespaces. + * The response schema defines the shape of the data that will be: + * - Used to define your testing criteria and + * - What data is required when creating a run + */ +export class EvalCustomDataSourceConfig extends S.Class("EvalCustomDataSourceConfig")({ + /** + * The type of data source. Always `custom`. + */ + "type": EvalCustomDataSourceConfigType.pipe(S.propertySignature, S.withConstructorDefault(() => "custom" as const)), + /** + * The json schema for the run data source items. + * Learn how to build JSON schemas [here](https://json-schema.org/). + */ + "schema": S.Record({ key: S.String, value: S.Unknown }) +}) {} + +/** + * The type of data source. Always `logs`. + */ +export class EvalLogsDataSourceConfigType extends S.Literal("logs") {} + +/** + * A LogsDataSourceConfig which specifies the metadata property of your logs query. + * This is usually metadata like `usecase=chatbot` or `prompt-version=v2`, etc. + * The schema returned by this data source config is used to defined what variables are available in your evals. + * `item` and `sample` are both defined when using this data source config. + */ +export class EvalLogsDataSourceConfig extends S.Class("EvalLogsDataSourceConfig")({ + /** + * The type of data source. Always `logs`. + */ + "type": EvalLogsDataSourceConfigType.pipe(S.propertySignature, S.withConstructorDefault(() => "logs" as const)), + "metadata": S.optionalWith(S.Record({ key: S.String, value: S.Unknown }), { nullable: true }), + /** + * The json schema for the run data source items. + * Learn how to build JSON schemas [here](https://json-schema.org/). + */ + "schema": S.Record({ key: S.String, value: S.Unknown }) +}) {} + +/** + * The type of data source. Always `stored_completions`. + */ +export class EvalStoredCompletionsDataSourceConfigType extends S.Literal("stored_completions") {} + +/** + * Deprecated in favor of LogsDataSourceConfig. + */ +export class EvalStoredCompletionsDataSourceConfig + extends S.Class("EvalStoredCompletionsDataSourceConfig")({ + /** + * The type of data source. Always `stored_completions`. + */ + "type": EvalStoredCompletionsDataSourceConfigType.pipe( + S.propertySignature, + S.withConstructorDefault(() => "stored_completions" as const) + ), + "metadata": S.optionalWith(S.Record({ key: S.String, value: S.Unknown }), { nullable: true }), + /** + * The json schema for the run data source items. + * Learn how to build JSON schemas [here](https://json-schema.org/). + */ + "schema": S.Record({ key: S.String, value: S.Unknown }) + }) +{} + +/** + * The object type, which is always `label_model`. + */ +export class EvalGraderLabelModelType extends S.Literal("label_model") {} + +/** + * The role of the message input. One of `user`, `assistant`, `system`, or + * `developer`. + */ +export class EvalItemRole extends S.Literal("user", "assistant", "system", "developer") {} + +/** + * The type of the image input. Always `input_image`. + */ +export class EvalItemContentEnumType extends S.Literal("input_image") {} + +/** + * The type of the input item. Always `input_audio`. + */ +export class InputAudioType extends S.Literal("input_audio") {} + +/** + * The format of the audio data. Currently supported formats are `mp3` and + * `wav`. + */ +export class InputAudioInputAudioFormat extends S.Literal("mp3", "wav") {} + +/** + * An audio input to the model. + */ +export class InputAudio extends S.Class("InputAudio")({ + /** + * The type of the input item. Always `input_audio`. + */ + "type": InputAudioType, + "input_audio": S.Struct({ + /** + * Base64-encoded audio data. + */ + "data": S.String, + /** + * The format of the audio data. Currently supported formats are `mp3` and + * `wav`. + */ + "format": InputAudioInputAudioFormat + }) +}) {} + +/** + * The type of the message input. Always `message`. + */ +export class EvalItemType extends S.Literal("message") {} + +/** + * A message input to the model with a role indicating instruction following + * hierarchy. Instructions given with the `developer` or `system` role take + * precedence over instructions given with the `user` role. Messages with the + * `assistant` role are presumed to have been generated by the model in previous + * interactions. + */ +export class EvalItem extends S.Class("EvalItem")({ + /** + * The role of the message input. One of `user`, `assistant`, `system`, or + * `developer`. + */ + "role": EvalItemRole, + /** + * Inputs to the model - can contain template strings. + */ + "content": S.Union( + /** + * A text input to the model. + */ + S.String, + InputTextContent, + /** + * A text output from the model. + */ + S.Struct({ + /** + * The type of the output text. Always `output_text`. + */ + "type": EvalItemContentEnumType, + /** + * The text output from the model. + */ + "text": S.String + }), + /** + * An image input to the model. + */ + S.Struct({ + /** + * The type of the image input. Always `input_image`. + */ + "type": EvalItemContentEnumType, + /** + * The URL of the image input. + */ + "image_url": S.String, + /** + * The detail level of the image to be sent to the model. One of `high`, `low`, or `auto`. Defaults to `auto`. + */ + "detail": S.optionalWith(S.String, { nullable: true }) + }), + InputAudio + ), + /** + * The type of the message input. Always `message`. + */ + "type": S.optionalWith(EvalItemType, { nullable: true }) +}) {} + +/** + * A LabelModelGrader object which uses a model to assign labels to each item + * in the evaluation. + */ +export class EvalGraderLabelModel extends S.Class("EvalGraderLabelModel")({ + /** + * The object type, which is always `label_model`. + */ + "type": EvalGraderLabelModelType, + /** + * The name of the grader. + */ + "name": S.String, + /** + * The model to use for the evaluation. Must support structured outputs. + */ + "model": S.String, + "input": S.Array(EvalItem), + /** + * The labels to assign to each item in the evaluation. + */ + "labels": S.Array(S.String), + /** + * The labels that indicate a passing result. Must be a subset of labels. + */ + "passing_labels": S.Array(S.String) +}) {} + +/** + * The object type, which is always `string_check`. + */ +export class EvalGraderStringCheckType extends S.Literal("string_check") {} + +/** + * The string check operation to perform. One of `eq`, `ne`, `like`, or `ilike`. + */ +export class EvalGraderStringCheckOperation extends S.Literal("eq", "ne", "like", "ilike") {} + +/** + * A StringCheckGrader object that performs a string comparison between input and reference using a specified operation. + */ +export class EvalGraderStringCheck extends S.Class("EvalGraderStringCheck")({ + /** + * The object type, which is always `string_check`. + */ + "type": EvalGraderStringCheckType, + /** + * The name of the grader. + */ + "name": S.String, + /** + * The input text. This may include template strings. + */ + "input": S.String, + /** + * The reference text. This may include template strings. + */ + "reference": S.String, + /** + * The string check operation to perform. One of `eq`, `ne`, `like`, or `ilike`. + */ + "operation": EvalGraderStringCheckOperation +}) {} + +/** + * The type of grader. + */ +export class EvalGraderTextSimilarityType extends S.Literal("text_similarity") {} + +/** + * The evaluation metric to use. One of `cosine`, `fuzzy_match`, `bleu`, + * `gleu`, `meteor`, `rouge_1`, `rouge_2`, `rouge_3`, `rouge_4`, `rouge_5`, + * or `rouge_l`. + */ +export class EvalGraderTextSimilarityEvaluationMetric extends S.Literal( + "cosine", + "fuzzy_match", + "bleu", + "gleu", + "meteor", + "rouge_1", + "rouge_2", + "rouge_3", + "rouge_4", + "rouge_5", + "rouge_l" +) {} + +/** + * A TextSimilarityGrader object which grades text based on similarity metrics. + */ +export class EvalGraderTextSimilarity extends S.Class("EvalGraderTextSimilarity")({ + /** + * The threshold for the score. + */ + "pass_threshold": S.Number, + /** + * The type of grader. + */ + "type": EvalGraderTextSimilarityType.pipe( + S.propertySignature, + S.withConstructorDefault(() => "text_similarity" as const) + ), + /** + * The name of the grader. + */ + "name": S.String, + /** + * The text being graded. + */ + "input": S.String, + /** + * The text being graded against. + */ + "reference": S.String, + /** + * The evaluation metric to use. One of `cosine`, `fuzzy_match`, `bleu`, + * `gleu`, `meteor`, `rouge_1`, `rouge_2`, `rouge_3`, `rouge_4`, `rouge_5`, + * or `rouge_l`. + */ + "evaluation_metric": EvalGraderTextSimilarityEvaluationMetric +}) {} + +/** + * The object type, which is always `python`. + */ +export class EvalGraderPythonType extends S.Literal("python") {} + +/** + * A PythonGrader object that runs a python script on the input. + */ +export class EvalGraderPython extends S.Class("EvalGraderPython")({ + /** + * The threshold for the score. + */ + "pass_threshold": S.optionalWith(S.Number, { nullable: true }), + /** + * The object type, which is always `python`. + */ + "type": EvalGraderPythonType, + /** + * The name of the grader. + */ + "name": S.String, + /** + * The source code of the python script. + */ + "source": S.String, + /** + * The image tag to use for the python script. + */ + "image_tag": S.optionalWith(S.String, { nullable: true }) +}) {} + +/** + * The object type, which is always `score_model`. + */ +export class EvalGraderScoreModelType extends S.Literal("score_model") {} + +/** + * A ScoreModelGrader object that uses a model to assign a score to the input. + */ +export class EvalGraderScoreModel extends S.Class("EvalGraderScoreModel")({ + /** + * The threshold for the score. + */ + "pass_threshold": S.optionalWith(S.Number, { nullable: true }), + /** + * The object type, which is always `score_model`. + */ + "type": EvalGraderScoreModelType, + /** + * The name of the grader. + */ + "name": S.String, + /** + * The model to use for the evaluation. + */ + "model": S.String, + /** + * The sampling parameters for the model. + */ + "sampling_params": S.optionalWith( + S.Struct({ + "seed": S.optionalWith(S.Int, { nullable: true }), + "top_p": S.optionalWith(S.Number, { nullable: true }), + "temperature": S.optionalWith(S.Number, { nullable: true }), + "max_completions_tokens": S.optionalWith(S.Int.pipe(S.greaterThanOrEqualTo(1)), { nullable: true }), + "reasoning_effort": S.optionalWith(S.Literal("none", "minimal", "low", "medium", "high"), { nullable: true }) + }), + { nullable: true } + ), + /** + * The input text. This may include template strings. + */ + "input": S.Array(EvalItem), + /** + * The range of the score. Defaults to `[0, 1]`. + */ + "range": S.optionalWith(S.Array(S.Number), { nullable: true }) +}) {} + +/** + * An Eval object with a data source config and testing criteria. + * An Eval represents a task to be done for your LLM integration. + * Like: + * - Improve the quality of my chatbot + * - See how well my chatbot handles customer support + * - Check if o4-mini is better at my usecase than gpt-4o + */ +export class Eval extends S.Class("Eval")({ + /** + * The object type. + */ + "object": EvalObject.pipe(S.propertySignature, S.withConstructorDefault(() => "eval" as const)), + /** + * Unique identifier for the evaluation. + */ + "id": S.String, + /** + * The name of the evaluation. + */ + "name": S.String, + /** + * Configuration of data sources used in runs of the evaluation. + */ + "data_source_config": S.Record({ key: S.String, value: S.Unknown }), + /** + * A list of testing criteria. + */ + "testing_criteria": S.Array( + S.Union( + EvalGraderLabelModel, + EvalGraderStringCheck, + EvalGraderTextSimilarity, + EvalGraderPython, + EvalGraderScoreModel + ) + ), + /** + * The Unix timestamp (in seconds) for when the eval was created. + */ + "created_at": S.Int, + "metadata": S.NullOr(S.Record({ key: S.String, value: S.Unknown })) +}) {} + +/** + * An object representing a list of evals. + */ +export class EvalList extends S.Class("EvalList")({ + /** + * The type of this object. It is always set to "list". + */ + "object": EvalListObject.pipe(S.propertySignature, S.withConstructorDefault(() => "list" as const)), + /** + * An array of eval objects. + */ + "data": S.Array(Eval), + /** + * The identifier of the first eval in the data array. + */ + "first_id": S.String, + /** + * The identifier of the last eval in the data array. + */ + "last_id": S.String, + /** + * Indicates whether there are more evals available. + */ + "has_more": S.Boolean +}) {} + +/** + * The type of data source. Always `custom`. + */ +export class CreateEvalCustomDataSourceConfigType extends S.Literal("custom") {} + +/** + * A CustomDataSourceConfig object that defines the schema for the data source used for the evaluation runs. + * This schema is used to define the shape of the data that will be: + * - Used to define your testing criteria and + * - What data is required when creating a run + */ +export class CreateEvalCustomDataSourceConfig + extends S.Class("CreateEvalCustomDataSourceConfig")({ + /** + * The type of data source. Always `custom`. + */ + "type": CreateEvalCustomDataSourceConfigType.pipe( + S.propertySignature, + S.withConstructorDefault(() => "custom" as const) + ), + /** + * The json schema for each row in the data source. + */ + "item_schema": S.Record({ key: S.String, value: S.Unknown }), + /** + * Whether the eval should expect you to populate the sample namespace (ie, by generating responses off of your data source) + */ + "include_sample_schema": S.optionalWith(S.Boolean, { nullable: true, default: () => false as const }) + }) +{} + +/** + * The type of data source. Always `logs`. + */ +export class CreateEvalLogsDataSourceConfigType extends S.Literal("logs") {} + +/** + * A data source config which specifies the metadata property of your logs query. + * This is usually metadata like `usecase=chatbot` or `prompt-version=v2`, etc. + */ +export class CreateEvalLogsDataSourceConfig + extends S.Class("CreateEvalLogsDataSourceConfig")({ + /** + * The type of data source. Always `logs`. + */ + "type": CreateEvalLogsDataSourceConfigType.pipe( + S.propertySignature, + S.withConstructorDefault(() => "logs" as const) + ), + /** + * Metadata filters for the logs data source. + */ + "metadata": S.optionalWith(S.Record({ key: S.String, value: S.Unknown }), { nullable: true }) + }) +{} + +/** + * The type of data source. Always `stored_completions`. + */ +export class CreateEvalStoredCompletionsDataSourceConfigType extends S.Literal("stored_completions") {} + +/** + * Deprecated in favor of LogsDataSourceConfig. + */ +export class CreateEvalStoredCompletionsDataSourceConfig + extends S.Class("CreateEvalStoredCompletionsDataSourceConfig")({ + /** + * The type of data source. Always `stored_completions`. + */ + "type": CreateEvalStoredCompletionsDataSourceConfigType.pipe( + S.propertySignature, + S.withConstructorDefault(() => "stored_completions" as const) + ), + /** + * Metadata filters for the stored completions data source. + */ + "metadata": S.optionalWith(S.Record({ key: S.String, value: S.Unknown }), { nullable: true }) + }) +{} + +/** + * The object type, which is always `label_model`. + */ +export class CreateEvalLabelModelGraderType extends S.Literal("label_model") {} + +/** + * A chat message that makes up the prompt or context. May include variable references to the `item` namespace, ie {{item.name}}. + */ +export class CreateEvalItem extends S.Record({ key: S.String, value: S.Unknown }) {} + +/** + * A LabelModelGrader object which uses a model to assign labels to each item + * in the evaluation. + */ +export class CreateEvalLabelModelGrader extends S.Class("CreateEvalLabelModelGrader")({ + /** + * The object type, which is always `label_model`. + */ + "type": CreateEvalLabelModelGraderType, + /** + * The name of the grader. + */ + "name": S.String, + /** + * The model to use for the evaluation. Must support structured outputs. + */ + "model": S.String, + /** + * A list of chat messages forming the prompt or context. May include variable references to the `item` namespace, ie {{item.name}}. + */ + "input": S.Array(CreateEvalItem), + /** + * The labels to classify to each item in the evaluation. + */ + "labels": S.Array(S.String), + /** + * The labels that indicate a passing result. Must be a subset of labels. + */ + "passing_labels": S.Array(S.String) +}) {} + +export class CreateEvalRequest extends S.Class("CreateEvalRequest")({ + /** + * The name of the evaluation. + */ + "name": S.optionalWith(S.String, { nullable: true }), + "metadata": S.optionalWith(S.Record({ key: S.String, value: S.Unknown }), { nullable: true }), + /** + * The configuration for the data source used for the evaluation runs. Dictates the schema of the data used in the evaluation. + */ + "data_source_config": S.Record({ key: S.String, value: S.Unknown }), + /** + * A list of graders for all eval runs in this group. Graders can reference variables in the data source using double curly braces notation, like `{{item.variable_name}}`. To reference the model's output, use the `sample` namespace (ie, `{{sample.output_text}}`). + */ + "testing_criteria": S.Array( + S.Union( + CreateEvalLabelModelGrader, + EvalGraderStringCheck, + EvalGraderTextSimilarity, + EvalGraderPython, + EvalGraderScoreModel + ) + ) +}) {} + +export class UpdateEvalRequest extends S.Class("UpdateEvalRequest")({ + /** + * Rename the evaluation. + */ + "name": S.optionalWith(S.String, { nullable: true }), + "metadata": S.optionalWith(S.Record({ key: S.String, value: S.Unknown }), { nullable: true }) +}) {} + +export class DeleteEval200 extends S.Struct({ + "object": S.String, + "deleted": S.Boolean, + "eval_id": S.String +}) {} + +export class Error extends S.Class("Error")({ + "code": S.NullOr(S.String), + "message": S.String, + "param": S.NullOr(S.String), + "type": S.String +}) {} + +export class GetEvalRunsParamsOrder extends S.Literal("asc", "desc") {} + +export class GetEvalRunsParamsStatus extends S.Literal("queued", "in_progress", "completed", "canceled", "failed") {} + +export class GetEvalRunsParams extends S.Struct({ + "after": S.optionalWith(S.String, { nullable: true }), + "limit": S.optionalWith(S.Int, { nullable: true, default: () => 20 as const }), + "order": S.optionalWith(GetEvalRunsParamsOrder, { nullable: true, default: () => "asc" as const }), + "status": S.optionalWith(GetEvalRunsParamsStatus, { nullable: true }) +}) {} + +/** + * The type of this object. It is always set to "list". + */ +export class EvalRunListObject extends S.Literal("list") {} + +/** + * The type of the object. Always "eval.run". + */ +export class EvalRunObject extends S.Literal("eval.run") {} + +/** + * The type of data source. Always `jsonl`. + */ +export class CreateEvalJsonlRunDataSourceType extends S.Literal("jsonl") {} + +/** + * The type of jsonl source. Always `file_content`. + */ +export class EvalJsonlFileContentSourceType extends S.Literal("file_content") {} + +export class EvalJsonlFileContentSource extends S.Class("EvalJsonlFileContentSource")({ + /** + * The type of jsonl source. Always `file_content`. + */ + "type": EvalJsonlFileContentSourceType.pipe( + S.propertySignature, + S.withConstructorDefault(() => "file_content" as const) + ), + /** + * The content of the jsonl file. + */ + "content": S.Array(S.Struct({ + "item": S.Record({ key: S.String, value: S.Unknown }), + "sample": S.optionalWith(S.Record({ key: S.String, value: S.Unknown }), { nullable: true }) + })) +}) {} + +/** + * The type of jsonl source. Always `file_id`. + */ +export class EvalJsonlFileIdSourceType extends S.Literal("file_id") {} + +export class EvalJsonlFileIdSource extends S.Class("EvalJsonlFileIdSource")({ + /** + * The type of jsonl source. Always `file_id`. + */ + "type": EvalJsonlFileIdSourceType.pipe(S.propertySignature, S.withConstructorDefault(() => "file_id" as const)), + /** + * The identifier of the file. + */ + "id": S.String +}) {} + +/** + * A JsonlRunDataSource object with that specifies a JSONL file that matches the eval + */ +export class CreateEvalJsonlRunDataSource + extends S.Class("CreateEvalJsonlRunDataSource")({ + /** + * The type of data source. Always `jsonl`. + */ + "type": CreateEvalJsonlRunDataSourceType.pipe( + S.propertySignature, + S.withConstructorDefault(() => "jsonl" as const) + ), + /** + * Determines what populates the `item` namespace in the data source. + */ + "source": S.Union(EvalJsonlFileContentSource, EvalJsonlFileIdSource) + }) +{} + +/** + * The type of run data source. Always `completions`. + */ +export class CreateEvalCompletionsRunDataSourceType extends S.Literal("completions") {} + +/** + * The type of input messages. Always `item_reference`. + */ +export class CreateEvalCompletionsRunDataSourceInputMessagesEnumType extends S.Literal("item_reference") {} + +/** + * The type of source. Always `stored_completions`. + */ +export class EvalStoredCompletionsSourceType extends S.Literal("stored_completions") {} + +/** + * A StoredCompletionsRunDataSource configuration describing a set of filters + */ +export class EvalStoredCompletionsSource extends S.Class("EvalStoredCompletionsSource")({ + /** + * The type of source. Always `stored_completions`. + */ + "type": EvalStoredCompletionsSourceType.pipe( + S.propertySignature, + S.withConstructorDefault(() => "stored_completions" as const) + ), + "metadata": S.optionalWith(S.Record({ key: S.String, value: S.Unknown }), { nullable: true }), + "model": S.optionalWith(S.String, { nullable: true }), + "created_after": S.optionalWith(S.Int, { nullable: true }), + "created_before": S.optionalWith(S.Int, { nullable: true }), + "limit": S.optionalWith(S.Int, { nullable: true }) +}) {} + +/** + * A CompletionsRunDataSource object describing a model sampling configuration. + */ +export class CreateEvalCompletionsRunDataSource + extends S.Class("CreateEvalCompletionsRunDataSource")({ + /** + * The type of run data source. Always `completions`. + */ + "type": CreateEvalCompletionsRunDataSourceType.pipe( + S.propertySignature, + S.withConstructorDefault(() => "completions" as const) + ), + /** + * Used when sampling from a model. Dictates the structure of the messages passed into the model. Can either be a reference to a prebuilt trajectory (ie, `item.input_trajectory`), or a template with variable references to the `item` namespace. + */ + "input_messages": S.optionalWith( + S.Union( + S.Struct({ + /** + * The type of input messages. Always `template`. + */ + "type": CreateEvalCompletionsRunDataSourceInputMessagesEnumType, + /** + * A list of chat messages forming the prompt or context. May include variable references to the `item` namespace, ie {{item.name}}. + */ + "template": S.Array(S.Union(EasyInputMessage, EvalItem)) + }), + S.Struct({ + /** + * The type of input messages. Always `item_reference`. + */ + "type": CreateEvalCompletionsRunDataSourceInputMessagesEnumType, + /** + * A reference to a variable in the `item` namespace. Ie, "item.input_trajectory" + */ + "item_reference": S.String + }) + ), + { nullable: true } + ), + "sampling_params": S.optionalWith( + S.Struct({ + "reasoning_effort": S.optionalWith(S.Literal("none", "minimal", "low", "medium", "high"), { nullable: true }), + /** + * A higher temperature increases randomness in the outputs. + */ + "temperature": S.optionalWith(S.Number, { nullable: true, default: () => 1 as const }), + /** + * The maximum number of tokens in the generated output. + */ + "max_completion_tokens": S.optionalWith(S.Int, { nullable: true }), + /** + * An alternative to temperature for nucleus sampling; 1.0 includes all tokens. + */ + "top_p": S.optionalWith(S.Number, { nullable: true, default: () => 1 as const }), + /** + * A seed value to initialize the randomness, during sampling. + */ + "seed": S.optionalWith(S.Int, { nullable: true, default: () => 42 as const }), + /** + * An object specifying the format that the model must output. + * + * Setting to `{ "type": "json_schema", "json_schema": {...} }` enables + * Structured Outputs which ensures the model will match your supplied JSON + * schema. Learn more in the [Structured Outputs + * guide](https://platform.openai.com/docs/guides/structured-outputs). + * + * Setting to `{ "type": "json_object" }` enables the older JSON mode, which + * ensures the message the model generates is valid JSON. Using `json_schema` + * is preferred for models that support it. + */ + "response_format": S.optionalWith( + S.Union(ResponseFormatText, ResponseFormatJsonSchema, ResponseFormatJsonObject), + { nullable: true } + ), + /** + * A list of tools the model may call. Currently, only functions are supported as a tool. Use this to provide a list of functions the model may generate JSON inputs for. A max of 128 functions are supported. + */ + "tools": S.optionalWith(S.Array(ChatCompletionTool), { nullable: true }) + }), + { nullable: true } + ), + /** + * The name of the model to use for generating completions (e.g. "o3-mini"). + */ + "model": S.optionalWith(S.String, { nullable: true }), + /** + * Determines what populates the `item` namespace in this run's data source. + */ + "source": S.Union(EvalJsonlFileContentSource, EvalJsonlFileIdSource, EvalStoredCompletionsSource) + }) +{} + +/** + * The type of run data source. Always `responses`. + */ +export class CreateEvalResponsesRunDataSourceType extends S.Literal("responses") {} + +/** + * The type of input messages. Always `item_reference`. + */ +export class CreateEvalResponsesRunDataSourceInputMessagesEnumType extends S.Literal("item_reference") {} + +/** + * The type of the function tool. Always `function`. + */ +export class FunctionToolType extends S.Literal("function") {} + +/** + * Defines a function in your own code the model can choose to call. Learn more about [function calling](https://platform.openai.com/docs/guides/function-calling). + */ +export class FunctionTool extends S.Class("FunctionTool")({ + /** + * The type of the function tool. Always `function`. + */ + "type": FunctionToolType.pipe(S.propertySignature, S.withConstructorDefault(() => "function" as const)), + /** + * The name of the function to call. + */ + "name": S.String, + "description": S.optionalWith(S.String, { nullable: true }), + "parameters": S.NullOr(S.Record({ key: S.String, value: S.Unknown })), + "strict": S.NullOr(S.Boolean) +}) {} + +/** + * The type of the file search tool. Always `file_search`. + */ +export class FileSearchToolType extends S.Literal("file_search") {} + +export class RankerVersionType extends S.Literal("auto", "default-2024-11-15") {} + +export class HybridSearchOptions extends S.Class("HybridSearchOptions")({ + /** + * The weight of the embedding in the reciprocal ranking fusion. + */ + "embedding_weight": S.Number, + /** + * The weight of the text in the reciprocal ranking fusion. + */ + "text_weight": S.Number +}) {} + +export class RankingOptions extends S.Class("RankingOptions")({ + /** + * The ranker to use for the file search. + */ + "ranker": S.optionalWith(RankerVersionType, { nullable: true }), + /** + * The score threshold for the file search, a number between 0 and 1. Numbers closer to 1 will attempt to return only the most relevant results, but may return fewer results. + */ + "score_threshold": S.optionalWith(S.Number, { nullable: true }), + /** + * Weights that control how reciprocal rank fusion balances semantic embedding matches versus sparse keyword matches when hybrid search is enabled. + */ + "hybrid_search": S.optionalWith(HybridSearchOptions, { nullable: true }) +}) {} + +/** + * Specifies the comparison operator: `eq`, `ne`, `gt`, `gte`, `lt`, `lte`, `in`, `nin`. + * - `eq`: equals + * - `ne`: not equal + * - `gt`: greater than + * - `gte`: greater than or equal + * - `lt`: less than + * - `lte`: less than or equal + * - `in`: in + * - `nin`: not in + */ +export class ComparisonFilterType extends S.Literal("eq", "ne", "gt", "gte", "lt", "lte") {} + +export class ComparisonFilterValueItems extends S.Union(S.String, S.Number) {} + +/** + * A filter used to compare a specified attribute key to a given value using a defined comparison operation. + */ +export class ComparisonFilter extends S.Class("ComparisonFilter")({ + /** + * Specifies the comparison operator: `eq`, `ne`, `gt`, `gte`, `lt`, `lte`, `in`, `nin`. + * - `eq`: equals + * - `ne`: not equal + * - `gt`: greater than + * - `gte`: greater than or equal + * - `lt`: less than + * - `lte`: less than or equal + * - `in`: in + * - `nin`: not in + */ + "type": ComparisonFilterType.pipe(S.propertySignature, S.withConstructorDefault(() => "eq" as const)), + /** + * The key to compare against the value. + */ + "key": S.String, + /** + * The value to compare against the attribute key; supports string, number, or boolean types. + */ + "value": S.Union(S.String, S.Number, S.Boolean, S.Array(ComparisonFilterValueItems)) +}) {} + +/** + * Type of operation: `and` or `or`. + */ +export class CompoundFilterType extends S.Literal("and", "or") {} + +/** + * Combine multiple filters using `and` or `or`. + */ +export class CompoundFilter extends S.Class("CompoundFilter")({ + /** + * Type of operation: `and` or `or`. + */ + "type": CompoundFilterType, + /** + * Array of filters to combine. Items can be `ComparisonFilter` or `CompoundFilter`. + */ + "filters": S.Array(ComparisonFilter) +}) {} + +export class Filters extends S.Union(ComparisonFilter, CompoundFilter) {} + +/** + * A tool that searches for relevant content from uploaded files. Learn more about the [file search tool](https://platform.openai.com/docs/guides/tools-file-search). + */ +export class FileSearchTool extends S.Class("FileSearchTool")({ + /** + * The type of the file search tool. Always `file_search`. + */ + "type": FileSearchToolType.pipe(S.propertySignature, S.withConstructorDefault(() => "file_search" as const)), + /** + * The IDs of the vector stores to search. + */ + "vector_store_ids": S.Array(S.String), + /** + * The maximum number of results to return. This number should be between 1 and 50 inclusive. + */ + "max_num_results": S.optionalWith(S.Int, { nullable: true }), + /** + * Ranking options for search. + */ + "ranking_options": S.optionalWith(RankingOptions, { nullable: true }), + "filters": S.optionalWith(Filters, { nullable: true }) +}) {} + +/** + * The type of the computer use tool. Always `computer_use_preview`. + */ +export class ComputerUsePreviewToolType extends S.Literal("computer_use_preview") {} + +export class ComputerEnvironment extends S.Literal("windows", "mac", "linux", "ubuntu", "browser") {} + +/** + * A tool that controls a virtual computer. Learn more about the [computer tool](https://platform.openai.com/docs/guides/tools-computer-use). + */ +export class ComputerUsePreviewTool extends S.Class("ComputerUsePreviewTool")({ + /** + * The type of the computer use tool. Always `computer_use_preview`. + */ + "type": ComputerUsePreviewToolType.pipe( + S.propertySignature, + S.withConstructorDefault(() => "computer_use_preview" as const) + ), + /** + * The type of computer environment to control. + */ + "environment": ComputerEnvironment, + /** + * The width of the computer display. + */ + "display_width": S.Int, + /** + * The height of the computer display. + */ + "display_height": S.Int +}) {} + +/** + * The type of the web search tool. One of `web_search` or `web_search_2025_08_26`. + */ +export class WebSearchToolType extends S.Literal("web_search", "web_search_2025_08_26") {} + +/** + * The type of location approximation. Always `approximate`. + */ +export class WebSearchApproximateLocationEnumType extends S.Literal("approximate") {} + +export class WebSearchApproximateLocation extends S.Union( + /** + * The approximate location of the user. + */ + S.Struct({ + /** + * The type of location approximation. Always `approximate`. + */ + "type": S.optionalWith(WebSearchApproximateLocationEnumType, { + nullable: true, + default: () => "approximate" as const + }), + "country": S.optionalWith(S.String, { nullable: true }), + "region": S.optionalWith(S.String, { nullable: true }), + "city": S.optionalWith(S.String, { nullable: true }), + "timezone": S.optionalWith(S.String, { nullable: true }) + }), + S.Null +) {} + +/** + * High level guidance for the amount of context window space to use for the search. One of `low`, `medium`, or `high`. `medium` is the default. + */ +export class WebSearchToolSearchContextSize extends S.Literal("low", "medium", "high") {} + +/** + * Search the Internet for sources related to the prompt. Learn more about the + * [web search tool](https://platform.openai.com/docs/guides/tools-web-search). + */ +export class WebSearchTool extends S.Class("WebSearchTool")({ + /** + * The type of the web search tool. One of `web_search` or `web_search_2025_08_26`. + */ + "type": WebSearchToolType.pipe(S.propertySignature, S.withConstructorDefault(() => "web_search" as const)), + "filters": S.optionalWith( + S.Struct({ + "allowed_domains": S.optionalWith(S.Array(S.String), { nullable: true }) + }), + { nullable: true } + ), + "user_location": S.optionalWith( + S.Struct({ + /** + * The type of location approximation. Always `approximate`. + */ + "type": S.optionalWith(S.Literal("approximate"), { nullable: true, default: () => "approximate" as const }), + "country": S.optionalWith(S.String, { nullable: true }), + "region": S.optionalWith(S.String, { nullable: true }), + "city": S.optionalWith(S.String, { nullable: true }), + "timezone": S.optionalWith(S.String, { nullable: true }) + }), + { nullable: true } + ), + /** + * High level guidance for the amount of context window space to use for the search. One of `low`, `medium`, or `high`. `medium` is the default. + */ + "search_context_size": S.optionalWith(WebSearchToolSearchContextSize, { + nullable: true, + default: () => "medium" as const + }) +}) {} + +/** + * The type of the MCP tool. Always `mcp`. + */ +export class MCPToolType extends S.Literal("mcp") {} + +/** + * Identifier for service connectors, like those available in ChatGPT. One of + * `server_url` or `connector_id` must be provided. Learn more about service + * connectors [here](https://platform.openai.com/docs/guides/tools-remote-mcp#connectors). + * + * Currently supported `connector_id` values are: + * + * - Dropbox: `connector_dropbox` + * - Gmail: `connector_gmail` + * - Google Calendar: `connector_googlecalendar` + * - Google Drive: `connector_googledrive` + * - Microsoft Teams: `connector_microsoftteams` + * - Outlook Calendar: `connector_outlookcalendar` + * - Outlook Email: `connector_outlookemail` + * - SharePoint: `connector_sharepoint` + */ +export class MCPToolConnectorId extends S.Literal( + "connector_dropbox", + "connector_gmail", + "connector_googlecalendar", + "connector_googledrive", + "connector_microsoftteams", + "connector_outlookcalendar", + "connector_outlookemail", + "connector_sharepoint" +) {} + +/** + * A filter object to specify which tools are allowed. + */ +export class MCPToolFilter extends S.Class("MCPToolFilter")({ + /** + * List of allowed tool names. + */ + "tool_names": S.optionalWith(S.Array(S.String), { nullable: true }), + /** + * Indicates whether or not a tool modifies data or is read-only. If an + * MCP server is [annotated with `readOnlyHint`](https://modelcontextprotocol.io/specification/2025-06-18/schema#toolannotations-readonlyhint), + * it will match this filter. + */ + "read_only": S.optionalWith(S.Boolean, { nullable: true }) +}) {} + +/** + * Specify a single approval policy for all tools. One of `always` or + * `never`. When set to `always`, all tools will require approval. When + * set to `never`, all tools will not require approval. + */ +export class MCPToolRequireApprovalEnum extends S.Literal("always", "never") {} + +/** + * Give the model access to additional tools via remote Model Context Protocol + * (MCP) servers. [Learn more about MCP](https://platform.openai.com/docs/guides/tools-remote-mcp). + */ +export class MCPTool extends S.Class("MCPTool")({ + /** + * The type of the MCP tool. Always `mcp`. + */ + "type": MCPToolType, + /** + * A label for this MCP server, used to identify it in tool calls. + */ + "server_label": S.String, + /** + * The URL for the MCP server. One of `server_url` or `connector_id` must be + * provided. + */ + "server_url": S.optionalWith(S.String, { nullable: true }), + /** + * Identifier for service connectors, like those available in ChatGPT. One of + * `server_url` or `connector_id` must be provided. Learn more about service + * connectors [here](https://platform.openai.com/docs/guides/tools-remote-mcp#connectors). + * + * Currently supported `connector_id` values are: + * + * - Dropbox: `connector_dropbox` + * - Gmail: `connector_gmail` + * - Google Calendar: `connector_googlecalendar` + * - Google Drive: `connector_googledrive` + * - Microsoft Teams: `connector_microsoftteams` + * - Outlook Calendar: `connector_outlookcalendar` + * - Outlook Email: `connector_outlookemail` + * - SharePoint: `connector_sharepoint` + */ + "connector_id": S.optionalWith(MCPToolConnectorId, { nullable: true }), + /** + * An OAuth access token that can be used with a remote MCP server, either + * with a custom MCP server URL or a service connector. Your application + * must handle the OAuth authorization flow and provide the token here. + */ + "authorization": S.optionalWith(S.String, { nullable: true }), + /** + * Optional description of the MCP server, used to provide more context. + */ + "server_description": S.optionalWith(S.String, { nullable: true }), + "headers": S.optionalWith(S.Record({ key: S.String, value: S.Unknown }), { nullable: true }), + "allowed_tools": S.optionalWith( + S.Union( + /** + * A string array of allowed tool names + */ + S.Array(S.String), + MCPToolFilter + ), + { nullable: true } + ), + "require_approval": S.optionalWith( + S.Union( + /** + * Specify which of the MCP server's tools require approval. Can be + * `always`, `never`, or a filter object associated with tools + * that require approval. + */ + S.Struct({ + "always": S.optionalWith(MCPToolFilter, { nullable: true }), + "never": S.optionalWith(MCPToolFilter, { nullable: true }) + }), + /** + * Specify a single approval policy for all tools. One of `always` or + * `never`. When set to `always`, all tools will require approval. When + * set to `never`, all tools will not require approval. + */ + MCPToolRequireApprovalEnum + ), + { nullable: true } + ) +}) {} + +/** + * The type of the code interpreter tool. Always `code_interpreter`. + */ +export class CodeInterpreterToolType extends S.Literal("code_interpreter") {} + +/** + * Always `auto`. + */ +export class CodeInterpreterContainerAutoType extends S.Literal("auto") {} + +export class ContainerMemoryLimit extends S.Literal("1g", "4g", "16g", "64g") {} + +/** + * Configuration for a code interpreter container. Optionally specify the IDs of the files to run the code on. + */ +export class CodeInterpreterContainerAuto + extends S.Class("CodeInterpreterContainerAuto")({ + /** + * Always `auto`. + */ + "type": CodeInterpreterContainerAutoType.pipe(S.propertySignature, S.withConstructorDefault(() => "auto" as const)), + /** + * An optional list of uploaded files to make available to your code. + */ + "file_ids": S.optionalWith(S.Array(S.String).pipe(S.maxItems(50)), { nullable: true }), + "memory_limit": S.optionalWith(ContainerMemoryLimit, { nullable: true }) + }) +{} + +/** + * A tool that runs Python code to help generate a response to a prompt. + */ +export class CodeInterpreterTool extends S.Class("CodeInterpreterTool")({ + /** + * The type of the code interpreter tool. Always `code_interpreter`. + */ + "type": CodeInterpreterToolType, + /** + * The code interpreter container. Can be a container ID or an object that + * specifies uploaded file IDs to make available to your code. + */ + "container": S.Union( + /** + * The container ID. + */ + S.String, + CodeInterpreterContainerAuto + ) +}) {} + +/** + * The type of the image generation tool. Always `image_generation`. + */ +export class ImageGenToolType extends S.Literal("image_generation") {} + +/** + * The image generation model to use. Default: `gpt-image-1`. + */ +export class ImageGenToolModel extends S.Literal("gpt-image-1", "gpt-image-1-mini") {} + +/** + * The quality of the generated image. One of `low`, `medium`, `high`, + * or `auto`. Default: `auto`. + */ +export class ImageGenToolQuality extends S.Literal("low", "medium", "high", "auto") {} + +/** + * The size of the generated image. One of `1024x1024`, `1024x1536`, + * `1536x1024`, or `auto`. Default: `auto`. + */ +export class ImageGenToolSize extends S.Literal("1024x1024", "1024x1536", "1536x1024", "auto") {} + +/** + * The output format of the generated image. One of `png`, `webp`, or + * `jpeg`. Default: `png`. + */ +export class ImageGenToolOutputFormat extends S.Literal("png", "webp", "jpeg") {} + +/** + * Moderation level for the generated image. Default: `auto`. + */ +export class ImageGenToolModeration extends S.Literal("auto", "low") {} + +/** + * Background type for the generated image. One of `transparent`, + * `opaque`, or `auto`. Default: `auto`. + */ +export class ImageGenToolBackground extends S.Literal("transparent", "opaque", "auto") {} + +/** + * Control how much effort the model will exert to match the style and features, especially facial features, of input images. This parameter is only supported for `gpt-image-1`. Unsupported for `gpt-image-1-mini`. Supports `high` and `low`. Defaults to `low`. + */ +export class InputFidelity extends S.Literal("high", "low") {} + +/** + * A tool that generates images using a model like `gpt-image-1`. + */ +export class ImageGenTool extends S.Class("ImageGenTool")({ + /** + * The type of the image generation tool. Always `image_generation`. + */ + "type": ImageGenToolType, + /** + * The image generation model to use. Default: `gpt-image-1`. + */ + "model": S.optionalWith(ImageGenToolModel, { nullable: true, default: () => "gpt-image-1" as const }), + /** + * The quality of the generated image. One of `low`, `medium`, `high`, + * or `auto`. Default: `auto`. + */ + "quality": S.optionalWith(ImageGenToolQuality, { nullable: true, default: () => "auto" as const }), + /** + * The size of the generated image. One of `1024x1024`, `1024x1536`, + * `1536x1024`, or `auto`. Default: `auto`. + */ + "size": S.optionalWith(ImageGenToolSize, { nullable: true, default: () => "auto" as const }), + /** + * The output format of the generated image. One of `png`, `webp`, or + * `jpeg`. Default: `png`. + */ + "output_format": S.optionalWith(ImageGenToolOutputFormat, { nullable: true, default: () => "png" as const }), + /** + * Compression level for the output image. Default: 100. + */ + "output_compression": S.optionalWith(S.Int.pipe(S.greaterThanOrEqualTo(0), S.lessThanOrEqualTo(100)), { + nullable: true, + default: () => 100 as const + }), + /** + * Moderation level for the generated image. Default: `auto`. + */ + "moderation": S.optionalWith(ImageGenToolModeration, { nullable: true, default: () => "auto" as const }), + /** + * Background type for the generated image. One of `transparent`, + * `opaque`, or `auto`. Default: `auto`. + */ + "background": S.optionalWith(ImageGenToolBackground, { nullable: true, default: () => "auto" as const }), + "input_fidelity": S.optionalWith(InputFidelity, { nullable: true }), + /** + * Optional mask for inpainting. Contains `image_url` + * (string, optional) and `file_id` (string, optional). + */ + "input_image_mask": S.optionalWith( + S.Struct({ + /** + * Base64-encoded mask image. + */ + "image_url": S.optionalWith(S.String, { nullable: true }), + /** + * File ID for the mask image. + */ + "file_id": S.optionalWith(S.String, { nullable: true }) + }), + { nullable: true } + ), + /** + * Number of partial images to generate in streaming mode, from 0 (default value) to 3. + */ + "partial_images": S.optionalWith(S.Int.pipe(S.greaterThanOrEqualTo(0), S.lessThanOrEqualTo(3)), { + nullable: true, + default: () => 0 as const + }) +}) {} + +/** + * The type of the local shell tool. Always `local_shell`. + */ +export class LocalShellToolParamType extends S.Literal("local_shell") {} + +/** + * A tool that allows the model to execute shell commands in a local environment. + */ +export class LocalShellToolParam extends S.Class("LocalShellToolParam")({ + /** + * The type of the local shell tool. Always `local_shell`. + */ + "type": LocalShellToolParamType.pipe(S.propertySignature, S.withConstructorDefault(() => "local_shell" as const)) +}) {} + +/** + * The type of the shell tool. Always `shell`. + */ +export class FunctionShellToolParamType extends S.Literal("shell") {} + +/** + * A tool that allows the model to execute shell commands. + */ +export class FunctionShellToolParam extends S.Class("FunctionShellToolParam")({ + /** + * The type of the shell tool. Always `shell`. + */ + "type": FunctionShellToolParamType.pipe(S.propertySignature, S.withConstructorDefault(() => "shell" as const)) +}) {} + +/** + * The type of the custom tool. Always `custom`. + */ +export class CustomToolParamType extends S.Literal("custom") {} + +/** + * Unconstrained text format. Always `text`. + */ +export class CustomTextFormatParamType extends S.Literal("text") {} + +/** + * Unconstrained free-form text. + */ +export class CustomTextFormatParam extends S.Class("CustomTextFormatParam")({ + /** + * Unconstrained text format. Always `text`. + */ + "type": CustomTextFormatParamType.pipe(S.propertySignature, S.withConstructorDefault(() => "text" as const)) +}) {} + +/** + * Grammar format. Always `grammar`. + */ +export class CustomGrammarFormatParamType extends S.Literal("grammar") {} + +export class GrammarSyntax1 extends S.Literal("lark", "regex") {} + +/** + * A grammar defined by the user. + */ +export class CustomGrammarFormatParam extends S.Class("CustomGrammarFormatParam")({ + /** + * Grammar format. Always `grammar`. + */ + "type": CustomGrammarFormatParamType.pipe(S.propertySignature, S.withConstructorDefault(() => "grammar" as const)), + /** + * The syntax of the grammar definition. One of `lark` or `regex`. + */ + "syntax": GrammarSyntax1, + /** + * The grammar definition. + */ + "definition": S.String +}) {} + +/** + * A custom tool that processes input using a specified format. Learn more about [custom tools](https://platform.openai.com/docs/guides/function-calling#custom-tools) + */ +export class CustomToolParam extends S.Class("CustomToolParam")({ + /** + * The type of the custom tool. Always `custom`. + */ + "type": CustomToolParamType.pipe(S.propertySignature, S.withConstructorDefault(() => "custom" as const)), + /** + * The name of the custom tool, used to identify it in tool calls. + */ + "name": S.String, + /** + * Optional description of the custom tool, used to provide more context. + */ + "description": S.optionalWith(S.String, { nullable: true }), + /** + * The input format for the custom tool. Default is unconstrained text. + */ + "format": S.optionalWith(S.Union(CustomTextFormatParam, CustomGrammarFormatParam), { nullable: true }) +}) {} + +/** + * The type of the web search tool. One of `web_search_preview` or `web_search_preview_2025_03_11`. + */ +export class WebSearchPreviewToolType extends S.Literal("web_search_preview", "web_search_preview_2025_03_11") {} + +/** + * The type of location approximation. Always `approximate`. + */ +export class ApproximateLocationType extends S.Literal("approximate") {} + +export class ApproximateLocation extends S.Class("ApproximateLocation")({ + /** + * The type of location approximation. Always `approximate`. + */ + "type": ApproximateLocationType.pipe(S.propertySignature, S.withConstructorDefault(() => "approximate" as const)), + "country": S.optionalWith(S.String, { nullable: true }), + "region": S.optionalWith(S.String, { nullable: true }), + "city": S.optionalWith(S.String, { nullable: true }), + "timezone": S.optionalWith(S.String, { nullable: true }) +}) {} + +export class SearchContextSize extends S.Literal("low", "medium", "high") {} + +/** + * This tool searches the web for relevant results to use in a response. Learn more about the [web search tool](https://platform.openai.com/docs/guides/tools-web-search). + */ +export class WebSearchPreviewTool extends S.Class("WebSearchPreviewTool")({ + /** + * The type of the web search tool. One of `web_search_preview` or `web_search_preview_2025_03_11`. + */ + "type": WebSearchPreviewToolType.pipe( + S.propertySignature, + S.withConstructorDefault(() => "web_search_preview" as const) + ), + "user_location": S.optionalWith(ApproximateLocation, { nullable: true }), + /** + * High level guidance for the amount of context window space to use for the search. One of `low`, `medium`, or `high`. `medium` is the default. + */ + "search_context_size": S.optionalWith(SearchContextSize, { nullable: true }) +}) {} + +/** + * The type of the tool. Always `apply_patch`. + */ +export class ApplyPatchToolParamType extends S.Literal("apply_patch") {} + +/** + * Allows the assistant to create, delete, or update files using unified diffs. + */ +export class ApplyPatchToolParam extends S.Class("ApplyPatchToolParam")({ + /** + * The type of the tool. Always `apply_patch`. + */ + "type": ApplyPatchToolParamType.pipe(S.propertySignature, S.withConstructorDefault(() => "apply_patch" as const)) +}) {} + +/** + * A tool that can be used to generate a response. + */ +export class Tool extends S.Union( + FunctionTool, + FileSearchTool, + ComputerUsePreviewTool, + WebSearchTool, + MCPTool, + CodeInterpreterTool, + ImageGenTool, + LocalShellToolParam, + FunctionShellToolParam, + CustomToolParam, + WebSearchPreviewTool, + ApplyPatchToolParam +) {} + +/** + * The type of response format being defined. Always `json_schema`. + */ +export class TextResponseFormatJsonSchemaType extends S.Literal("json_schema") {} + +/** + * JSON Schema response format. Used to generate structured JSON responses. + * Learn more about [Structured Outputs](https://platform.openai.com/docs/guides/structured-outputs). + */ +export class TextResponseFormatJsonSchema + extends S.Class("TextResponseFormatJsonSchema")({ + /** + * The type of response format being defined. Always `json_schema`. + */ + "type": TextResponseFormatJsonSchemaType, + /** + * A description of what the response format is for, used by the model to + * determine how to respond in the format. + */ + "description": S.optionalWith(S.String, { nullable: true }), + /** + * The name of the response format. Must be a-z, A-Z, 0-9, or contain + * underscores and dashes, with a maximum length of 64. + */ + "name": S.String, + "schema": ResponseFormatJsonSchemaSchema, + "strict": S.optionalWith(S.Boolean, { nullable: true }) + }) +{} + +/** + * An object specifying the format that the model must output. + * + * Configuring `{ "type": "json_schema" }` enables Structured Outputs, + * which ensures the model will match your supplied JSON schema. Learn more in the + * [Structured Outputs guide](https://platform.openai.com/docs/guides/structured-outputs). + * + * The default format is `{ "type": "text" }` with no additional options. + * + * **Not recommended for gpt-4o and newer models:** + * + * Setting to `{ "type": "json_object" }` enables the older JSON mode, which + * ensures the message the model generates is valid JSON. Using `json_schema` + * is preferred for models that support it. + */ +export class TextResponseFormatConfiguration + extends S.Union(ResponseFormatText, TextResponseFormatJsonSchema, ResponseFormatJsonObject) +{} + +/** + * The type of run data source. Always `responses`. + */ +export class EvalResponsesSourceType extends S.Literal("responses") {} + +/** + * A EvalResponsesSource object describing a run data source configuration. + */ +export class EvalResponsesSource extends S.Class("EvalResponsesSource")({ + /** + * The type of run data source. Always `responses`. + */ + "type": EvalResponsesSourceType, + "metadata": S.optionalWith(S.Record({ key: S.String, value: S.Unknown }), { nullable: true }), + "model": S.optionalWith(S.String, { nullable: true }), + "instructions_search": S.optionalWith(S.String, { nullable: true }), + "created_after": S.optionalWith(S.Int.pipe(S.greaterThanOrEqualTo(0)), { nullable: true }), + "created_before": S.optionalWith(S.Int.pipe(S.greaterThanOrEqualTo(0)), { nullable: true }), + "reasoning_effort": S.optionalWith(ReasoningEffort, { nullable: true }), + "temperature": S.optionalWith(S.Number, { nullable: true }), + "top_p": S.optionalWith(S.Number, { nullable: true }), + "users": S.optionalWith(S.Array(S.String), { nullable: true }), + "tools": S.optionalWith(S.Array(S.String), { nullable: true }) +}) {} + +/** + * A ResponsesRunDataSource object describing a model sampling configuration. + */ +export class CreateEvalResponsesRunDataSource + extends S.Class("CreateEvalResponsesRunDataSource")({ + /** + * The type of run data source. Always `responses`. + */ + "type": CreateEvalResponsesRunDataSourceType.pipe( + S.propertySignature, + S.withConstructorDefault(() => "responses" as const) + ), + /** + * Used when sampling from a model. Dictates the structure of the messages passed into the model. Can either be a reference to a prebuilt trajectory (ie, `item.input_trajectory`), or a template with variable references to the `item` namespace. + */ + "input_messages": S.optionalWith( + S.Union( + S.Struct({ + /** + * The type of input messages. Always `template`. + */ + "type": CreateEvalResponsesRunDataSourceInputMessagesEnumType, + /** + * A list of chat messages forming the prompt or context. May include variable references to the `item` namespace, ie {{item.name}}. + */ + "template": S.Array(S.Union( + S.Struct({ + /** + * The role of the message (e.g. "system", "assistant", "user"). + */ + "role": S.String, + /** + * The content of the message. + */ + "content": S.String + }), + EvalItem + )) + }), + S.Struct({ + /** + * The type of input messages. Always `item_reference`. + */ + "type": CreateEvalResponsesRunDataSourceInputMessagesEnumType, + /** + * A reference to a variable in the `item` namespace. Ie, "item.name" + */ + "item_reference": S.String + }) + ), + { nullable: true } + ), + "sampling_params": S.optionalWith( + S.Struct({ + "reasoning_effort": S.optionalWith(S.Literal("none", "minimal", "low", "medium", "high"), { nullable: true }), + /** + * A higher temperature increases randomness in the outputs. + */ + "temperature": S.optionalWith(S.Number, { nullable: true, default: () => 1 as const }), + /** + * The maximum number of tokens in the generated output. + */ + "max_completion_tokens": S.optionalWith(S.Int, { nullable: true }), + /** + * An alternative to temperature for nucleus sampling; 1.0 includes all tokens. + */ + "top_p": S.optionalWith(S.Number, { nullable: true, default: () => 1 as const }), + /** + * A seed value to initialize the randomness, during sampling. + */ + "seed": S.optionalWith(S.Int, { nullable: true, default: () => 42 as const }), + /** + * An array of tools the model may call while generating a response. You + * can specify which tool to use by setting the `tool_choice` parameter. + * + * The two categories of tools you can provide the model are: + * + * - **Built-in tools**: Tools that are provided by OpenAI that extend the + * model's capabilities, like [web search](https://platform.openai.com/docs/guides/tools-web-search) + * or [file search](https://platform.openai.com/docs/guides/tools-file-search). Learn more about + * [built-in tools](https://platform.openai.com/docs/guides/tools). + * - **Function calls (custom tools)**: Functions that are defined by you, + * enabling the model to call your own code. Learn more about + * [function calling](https://platform.openai.com/docs/guides/function-calling). + */ + "tools": S.optionalWith(S.Array(Tool), { nullable: true }), + /** + * Configuration options for a text response from the model. Can be plain + * text or structured JSON data. Learn more: + * - [Text inputs and outputs](https://platform.openai.com/docs/guides/text) + * - [Structured Outputs](https://platform.openai.com/docs/guides/structured-outputs) + */ + "text": S.optionalWith( + S.Struct({ + "format": S.optionalWith(TextResponseFormatConfiguration, { nullable: true }) + }), + { nullable: true } + ) + }), + { nullable: true } + ), + /** + * The name of the model to use for generating completions (e.g. "o3-mini"). + */ + "model": S.optionalWith(S.String, { nullable: true }), + /** + * Determines what populates the `item` namespace in this run's data source. + */ + "source": S.Union(EvalJsonlFileContentSource, EvalJsonlFileIdSource, EvalResponsesSource) + }) +{} + +/** + * An object representing an error response from the Eval API. + */ +export class EvalApiError extends S.Class("EvalApiError")({ + /** + * The error code. + */ + "code": S.String, + /** + * The error message. + */ + "message": S.String +}) {} + +/** + * A schema representing an evaluation run. + */ +export class EvalRun extends S.Class("EvalRun")({ + /** + * The type of the object. Always "eval.run". + */ + "object": EvalRunObject.pipe(S.propertySignature, S.withConstructorDefault(() => "eval.run" as const)), + /** + * Unique identifier for the evaluation run. + */ + "id": S.String, + /** + * The identifier of the associated evaluation. + */ + "eval_id": S.String, + /** + * The status of the evaluation run. + */ + "status": S.String, + /** + * The model that is evaluated, if applicable. + */ + "model": S.String, + /** + * The name of the evaluation run. + */ + "name": S.String, + /** + * Unix timestamp (in seconds) when the evaluation run was created. + */ + "created_at": S.Int, + /** + * The URL to the rendered evaluation run report on the UI dashboard. + */ + "report_url": S.String, + /** + * Counters summarizing the outcomes of the evaluation run. + */ + "result_counts": S.Struct({ + /** + * Total number of executed output items. + */ + "total": S.Int, + /** + * Number of output items that resulted in an error. + */ + "errored": S.Int, + /** + * Number of output items that failed to pass the evaluation. + */ + "failed": S.Int, + /** + * Number of output items that passed the evaluation. + */ + "passed": S.Int + }), + /** + * Usage statistics for each model during the evaluation run. + */ + "per_model_usage": S.Array(S.Struct({ + /** + * The name of the model. + */ + "model_name": S.String, + /** + * The number of invocations. + */ + "invocation_count": S.Int, + /** + * The number of prompt tokens used. + */ + "prompt_tokens": S.Int, + /** + * The number of completion tokens generated. + */ + "completion_tokens": S.Int, + /** + * The total number of tokens used. + */ + "total_tokens": S.Int, + /** + * The number of tokens retrieved from cache. + */ + "cached_tokens": S.Int + })), + /** + * Results per testing criteria applied during the evaluation run. + */ + "per_testing_criteria_results": S.Array(S.Struct({ + /** + * A description of the testing criteria. + */ + "testing_criteria": S.String, + /** + * Number of tests passed for this criteria. + */ + "passed": S.Int, + /** + * Number of tests failed for this criteria. + */ + "failed": S.Int + })), + /** + * Information about the run's data source. + */ + "data_source": S.Record({ key: S.String, value: S.Unknown }), + "metadata": S.NullOr(S.Record({ key: S.String, value: S.Unknown })), + "error": EvalApiError +}) {} + +/** + * An object representing a list of runs for an evaluation. + */ +export class EvalRunList extends S.Class("EvalRunList")({ + /** + * The type of this object. It is always set to "list". + */ + "object": EvalRunListObject.pipe(S.propertySignature, S.withConstructorDefault(() => "list" as const)), + /** + * An array of eval run objects. + */ + "data": S.Array(EvalRun), + /** + * The identifier of the first eval run in the data array. + */ + "first_id": S.String, + /** + * The identifier of the last eval run in the data array. + */ + "last_id": S.String, + /** + * Indicates whether there are more evals available. + */ + "has_more": S.Boolean +}) {} + +export class CreateEvalRunRequest extends S.Class("CreateEvalRunRequest")({ + /** + * The name of the run. + */ + "name": S.optionalWith(S.String, { nullable: true }), + "metadata": S.optionalWith(S.Record({ key: S.String, value: S.Unknown }), { nullable: true }), + /** + * Details about the run's data source. + */ + "data_source": S.Record({ key: S.String, value: S.Unknown }) +}) {} + +export class DeleteEvalRun200 extends S.Struct({ + "object": S.optionalWith(S.String, { nullable: true }), + "deleted": S.optionalWith(S.Boolean, { nullable: true }), + "run_id": S.optionalWith(S.String, { nullable: true }) +}) {} + +export class GetEvalRunOutputItemsParamsStatus extends S.Literal("fail", "pass") {} + +export class GetEvalRunOutputItemsParamsOrder extends S.Literal("asc", "desc") {} + +export class GetEvalRunOutputItemsParams extends S.Struct({ + "after": S.optionalWith(S.String, { nullable: true }), + "limit": S.optionalWith(S.Int, { nullable: true, default: () => 20 as const }), + "status": S.optionalWith(GetEvalRunOutputItemsParamsStatus, { nullable: true }), + "order": S.optionalWith(GetEvalRunOutputItemsParamsOrder, { nullable: true, default: () => "asc" as const }) +}) {} + +/** + * The type of this object. It is always set to "list". + */ +export class EvalRunOutputItemListObject extends S.Literal("list") {} + +/** + * The type of the object. Always "eval.run.output_item". + */ +export class EvalRunOutputItemObject extends S.Literal("eval.run.output_item") {} + +/** + * A single grader result for an evaluation run output item. + */ +export class EvalRunOutputItemResult extends S.Class("EvalRunOutputItemResult")({ + /** + * The name of the grader. + */ + "name": S.String, + /** + * The grader type (for example, "string-check-grader"). + */ + "type": S.optionalWith(S.String, { nullable: true }), + /** + * The numeric score produced by the grader. + */ + "score": S.Number, + /** + * Whether the grader considered the output a pass. + */ + "passed": S.Boolean, + /** + * Optional sample or intermediate data produced by the grader. + */ + "sample": S.optionalWith(S.Record({ key: S.String, value: S.Unknown }), { nullable: true }) +}) {} + +/** + * A schema representing an evaluation run output item. + */ +export class EvalRunOutputItem extends S.Class("EvalRunOutputItem")({ + /** + * The type of the object. Always "eval.run.output_item". + */ + "object": EvalRunOutputItemObject.pipe( + S.propertySignature, + S.withConstructorDefault(() => "eval.run.output_item" as const) + ), + /** + * Unique identifier for the evaluation run output item. + */ + "id": S.String, + /** + * The identifier of the evaluation run associated with this output item. + */ + "run_id": S.String, + /** + * The identifier of the evaluation group. + */ + "eval_id": S.String, + /** + * Unix timestamp (in seconds) when the evaluation run was created. + */ + "created_at": S.Int, + /** + * The status of the evaluation run. + */ + "status": S.String, + /** + * The identifier for the data source item. + */ + "datasource_item_id": S.Int, + /** + * Details of the input data source item. + */ + "datasource_item": S.Record({ key: S.String, value: S.Unknown }), + /** + * A list of grader results for this output item. + */ + "results": S.Array(EvalRunOutputItemResult), + /** + * A sample containing the input and output of the evaluation run. + */ + "sample": S.Struct({ + /** + * An array of input messages. + */ + "input": S.Array(S.Struct({ + /** + * The role of the message sender (e.g., system, user, developer). + */ + "role": S.String, + /** + * The content of the message. + */ + "content": S.String + })), + /** + * An array of output messages. + */ + "output": S.Array(S.Struct({ + /** + * The role of the message (e.g. "system", "assistant", "user"). + */ + "role": S.optionalWith(S.String, { nullable: true }), + /** + * The content of the message. + */ + "content": S.optionalWith(S.String, { nullable: true }) + })), + /** + * The reason why the sample generation was finished. + */ + "finish_reason": S.String, + /** + * The model used for generating the sample. + */ + "model": S.String, + /** + * Token usage details for the sample. + */ + "usage": S.Struct({ + /** + * The total number of tokens used. + */ + "total_tokens": S.Int, + /** + * The number of completion tokens generated. + */ + "completion_tokens": S.Int, + /** + * The number of prompt tokens used. + */ + "prompt_tokens": S.Int, + /** + * The number of tokens retrieved from cache. + */ + "cached_tokens": S.Int + }), + "error": EvalApiError, + /** + * The sampling temperature used. + */ + "temperature": S.Number, + /** + * The maximum number of tokens allowed for completion. + */ + "max_completion_tokens": S.Int, + /** + * The top_p value used for sampling. + */ + "top_p": S.Number, + /** + * The seed used for generating the sample. + */ + "seed": S.Int + }) +}) {} + +/** + * An object representing a list of output items for an evaluation run. + */ +export class EvalRunOutputItemList extends S.Class("EvalRunOutputItemList")({ + /** + * The type of this object. It is always set to "list". + */ + "object": EvalRunOutputItemListObject.pipe(S.propertySignature, S.withConstructorDefault(() => "list" as const)), + /** + * An array of eval run output item objects. + */ + "data": S.Array(EvalRunOutputItem), + /** + * The identifier of the first eval run output item in the data array. + */ + "first_id": S.String, + /** + * The identifier of the last eval run output item in the data array. + */ + "last_id": S.String, + /** + * Indicates whether there are more eval run output items available. + */ + "has_more": S.Boolean +}) {} + +export class ListFilesParamsOrder extends S.Literal("asc", "desc") {} + +export class ListFilesParams extends S.Struct({ + "purpose": S.optionalWith(S.String, { nullable: true }), + "limit": S.optionalWith(S.Int, { nullable: true, default: () => 10000 as const }), + "order": S.optionalWith(ListFilesParamsOrder, { nullable: true, default: () => "desc" as const }), + "after": S.optionalWith(S.String, { nullable: true }) +}) {} + +/** + * The object type, which is always `file`. + */ +export class OpenAIFileObject extends S.Literal("file") {} + +/** + * The intended purpose of the file. Supported values are `assistants`, `assistants_output`, `batch`, `batch_output`, `fine-tune`, `fine-tune-results`, `vision`, and `user_data`. + */ +export class OpenAIFilePurpose extends S.Literal( + "assistants", + "assistants_output", + "batch", + "batch_output", + "fine-tune", + "fine-tune-results", + "vision", + "user_data" +) {} + +/** + * Deprecated. The current status of the file, which can be either `uploaded`, `processed`, or `error`. + */ +export class OpenAIFileStatus extends S.Literal("uploaded", "processed", "error") {} + +/** + * The `File` object represents a document that has been uploaded to OpenAI. + */ +export class OpenAIFile extends S.Class("OpenAIFile")({ + /** + * The file identifier, which can be referenced in the API endpoints. + */ + "id": S.String, + /** + * The size of the file, in bytes. + */ + "bytes": S.Int, + /** + * The Unix timestamp (in seconds) for when the file was created. + */ + "created_at": S.Int, + /** + * The Unix timestamp (in seconds) for when the file will expire. + */ + "expires_at": S.optionalWith(S.Int, { nullable: true }), + /** + * The name of the file. + */ + "filename": S.String, + /** + * The object type, which is always `file`. + */ + "object": OpenAIFileObject, + /** + * The intended purpose of the file. Supported values are `assistants`, `assistants_output`, `batch`, `batch_output`, `fine-tune`, `fine-tune-results`, `vision`, and `user_data`. + */ + "purpose": OpenAIFilePurpose, + /** + * Deprecated. The current status of the file, which can be either `uploaded`, `processed`, or `error`. + */ + "status": OpenAIFileStatus, + /** + * Deprecated. For details on why a fine-tuning training file failed validation, see the `error` field on `fine_tuning.job`. + */ + "status_details": S.optionalWith(S.String, { nullable: true }) +}) {} + +export class ListFilesResponse extends S.Class("ListFilesResponse")({ + "object": S.String, + "data": S.Array(OpenAIFile), + "first_id": S.String, + "last_id": S.String, + "has_more": S.Boolean +}) {} + +/** + * The intended purpose of the uploaded file. One of: - `assistants`: Used in the Assistants API - `batch`: Used in the Batch API - `fine-tune`: Used for fine-tuning - `vision`: Images used for vision fine-tuning - `user_data`: Flexible file type for any purpose - `evals`: Used for eval data sets + */ +export class FilePurpose extends S.Literal("assistants", "batch", "fine-tune", "vision", "user_data", "evals") {} + +/** + * Anchor timestamp after which the expiration policy applies. Supported anchors: `created_at`. + */ +export class FileExpirationAfterAnchor extends S.Literal("created_at") {} + +/** + * The expiration policy for a file. By default, files with `purpose=batch` expire after 30 days and all other files are persisted until they are manually deleted. + */ +export class FileExpirationAfter extends S.Class("FileExpirationAfter")({ + /** + * Anchor timestamp after which the expiration policy applies. Supported anchors: `created_at`. + */ + "anchor": FileExpirationAfterAnchor, + /** + * The number of seconds after the anchor time that the file will expire. Must be between 3600 (1 hour) and 2592000 (30 days). + */ + "seconds": S.Int.pipe(S.greaterThanOrEqualTo(3600), S.lessThanOrEqualTo(2592000)) +}) {} + +export class CreateFileRequest extends S.Class("CreateFileRequest")({ + /** + * The File object (not file name) to be uploaded. + */ + "file": S.instanceOf(globalThis.Blob), + "purpose": FilePurpose, + "expires_after": S.optionalWith(FileExpirationAfter, { nullable: true }) +}) {} + +export class DeleteFileResponseObject extends S.Literal("file") {} + +export class DeleteFileResponse extends S.Class("DeleteFileResponse")({ + "id": S.String, + "object": DeleteFileResponseObject, + "deleted": S.Boolean +}) {} + +export class DownloadFile200 extends S.String {} + +/** + * The object type, which is always `string_check`. + */ +export class GraderStringCheckType extends S.Literal("string_check") {} + +/** + * The string check operation to perform. One of `eq`, `ne`, `like`, or `ilike`. + */ +export class GraderStringCheckOperation extends S.Literal("eq", "ne", "like", "ilike") {} + +/** + * A StringCheckGrader object that performs a string comparison between input and reference using a specified operation. + */ +export class GraderStringCheck extends S.Class("GraderStringCheck")({ + /** + * The object type, which is always `string_check`. + */ + "type": GraderStringCheckType, + /** + * The name of the grader. + */ + "name": S.String, + /** + * The input text. This may include template strings. + */ + "input": S.String, + /** + * The reference text. This may include template strings. + */ + "reference": S.String, + /** + * The string check operation to perform. One of `eq`, `ne`, `like`, or `ilike`. + */ + "operation": GraderStringCheckOperation +}) {} + +/** + * The type of grader. + */ +export class GraderTextSimilarityType extends S.Literal("text_similarity") {} + +/** + * The evaluation metric to use. One of `cosine`, `fuzzy_match`, `bleu`, + * `gleu`, `meteor`, `rouge_1`, `rouge_2`, `rouge_3`, `rouge_4`, `rouge_5`, + * or `rouge_l`. + */ +export class GraderTextSimilarityEvaluationMetric extends S.Literal( + "cosine", + "fuzzy_match", + "bleu", + "gleu", + "meteor", + "rouge_1", + "rouge_2", + "rouge_3", + "rouge_4", + "rouge_5", + "rouge_l" +) {} + +/** + * A TextSimilarityGrader object which grades text based on similarity metrics. + */ +export class GraderTextSimilarity extends S.Class("GraderTextSimilarity")({ + /** + * The type of grader. + */ + "type": GraderTextSimilarityType.pipe( + S.propertySignature, + S.withConstructorDefault(() => "text_similarity" as const) + ), + /** + * The name of the grader. + */ + "name": S.String, + /** + * The text being graded. + */ + "input": S.String, + /** + * The text being graded against. + */ + "reference": S.String, + /** + * The evaluation metric to use. One of `cosine`, `fuzzy_match`, `bleu`, + * `gleu`, `meteor`, `rouge_1`, `rouge_2`, `rouge_3`, `rouge_4`, `rouge_5`, + * or `rouge_l`. + */ + "evaluation_metric": GraderTextSimilarityEvaluationMetric +}) {} + +/** + * The object type, which is always `python`. + */ +export class GraderPythonType extends S.Literal("python") {} + +/** + * A PythonGrader object that runs a python script on the input. + */ +export class GraderPython extends S.Class("GraderPython")({ + /** + * The object type, which is always `python`. + */ + "type": GraderPythonType, + /** + * The name of the grader. + */ + "name": S.String, + /** + * The source code of the python script. + */ + "source": S.String, + /** + * The image tag to use for the python script. + */ + "image_tag": S.optionalWith(S.String, { nullable: true }) +}) {} + +/** + * The object type, which is always `score_model`. + */ +export class GraderScoreModelType extends S.Literal("score_model") {} + +/** + * A ScoreModelGrader object that uses a model to assign a score to the input. + */ +export class GraderScoreModel extends S.Class("GraderScoreModel")({ + /** + * The object type, which is always `score_model`. + */ + "type": GraderScoreModelType, + /** + * The name of the grader. + */ + "name": S.String, + /** + * The model to use for the evaluation. + */ + "model": S.String, + /** + * The sampling parameters for the model. + */ + "sampling_params": S.optionalWith( + S.Struct({ + "seed": S.optionalWith(S.Int, { nullable: true }), + "top_p": S.optionalWith(S.Number, { nullable: true }), + "temperature": S.optionalWith(S.Number, { nullable: true }), + "max_completions_tokens": S.optionalWith(S.Int.pipe(S.greaterThanOrEqualTo(1)), { nullable: true }), + "reasoning_effort": S.optionalWith(S.Literal("none", "minimal", "low", "medium", "high"), { nullable: true }) + }), + { nullable: true } + ), + /** + * The input text. This may include template strings. + */ + "input": S.Array(EvalItem), + /** + * The range of the score. Defaults to `[0, 1]`. + */ + "range": S.optionalWith(S.Array(S.Number), { nullable: true }) +}) {} + +/** + * The object type, which is always `multi`. + */ +export class GraderMultiType extends S.Literal("multi") {} + +/** + * The object type, which is always `label_model`. + */ +export class GraderLabelModelType extends S.Literal("label_model") {} + +/** + * A LabelModelGrader object which uses a model to assign labels to each item + * in the evaluation. + */ +export class GraderLabelModel extends S.Class("GraderLabelModel")({ + /** + * The object type, which is always `label_model`. + */ + "type": GraderLabelModelType, + /** + * The name of the grader. + */ + "name": S.String, + /** + * The model to use for the evaluation. Must support structured outputs. + */ + "model": S.String, + "input": S.Array(EvalItem), + /** + * The labels to assign to each item in the evaluation. + */ + "labels": S.Array(S.String), + /** + * The labels that indicate a passing result. Must be a subset of labels. + */ + "passing_labels": S.Array(S.String) +}) {} + +/** + * A MultiGrader object combines the output of multiple graders to produce a single score. + */ +export class GraderMulti extends S.Class("GraderMulti")({ + /** + * The object type, which is always `multi`. + */ + "type": GraderMultiType.pipe(S.propertySignature, S.withConstructorDefault(() => "multi" as const)), + /** + * The name of the grader. + */ + "name": S.String, + "graders": S.Union(GraderStringCheck, GraderTextSimilarity, GraderPython, GraderScoreModel, GraderLabelModel), + /** + * A formula to calculate the output based on grader results. + */ + "calculate_output": S.String +}) {} + +export class RunGraderRequest extends S.Class("RunGraderRequest")({ + /** + * The grader used for the fine-tuning job. + */ + "grader": S.Record({ key: S.String, value: S.Unknown }), + /** + * The dataset item provided to the grader. This will be used to populate + * the `item` namespace. See [the guide](https://platform.openai.com/docs/guides/graders) for more details. + */ + "item": S.optionalWith(S.Record({ key: S.String, value: S.Unknown }), { nullable: true }), + /** + * The model sample to be evaluated. This value will be used to populate + * the `sample` namespace. See [the guide](https://platform.openai.com/docs/guides/graders) for more details. + * The `output_json` variable will be populated if the model sample is a + * valid JSON string. + */ + "model_sample": S.String +}) {} + +export class RunGraderResponse extends S.Class("RunGraderResponse")({ + "reward": S.Number, + "metadata": S.Struct({ + "name": S.String, + "type": S.String, + "errors": S.Struct({ + "formula_parse_error": S.Boolean, + "sample_parse_error": S.Boolean, + "truncated_observation_error": S.Boolean, + "unresponsive_reward_error": S.Boolean, + "invalid_variable_error": S.Boolean, + "other_error": S.Boolean, + "python_grader_server_error": S.Boolean, + "python_grader_server_error_type": S.NullOr(S.String), + "python_grader_runtime_error": S.Boolean, + "python_grader_runtime_error_details": S.NullOr(S.String), + "model_grader_server_error": S.Boolean, + "model_grader_refusal_error": S.Boolean, + "model_grader_parse_error": S.Boolean, + "model_grader_server_error_details": S.NullOr(S.String) + }), + "execution_time": S.Number, + "scores": S.Record({ key: S.String, value: S.Unknown }), + "token_usage": S.NullOr(S.Int), + "sampled_model_name": S.NullOr(S.String) + }), + "sub_rewards": S.Record({ key: S.String, value: S.Unknown }), + "model_grader_token_usage_per_model": S.Record({ key: S.String, value: S.Unknown }) +}) {} + +export class ValidateGraderRequest extends S.Class("ValidateGraderRequest")({ + /** + * The grader used for the fine-tuning job. + */ + "grader": S.Record({ key: S.String, value: S.Unknown }) +}) {} + +export class ValidateGraderResponse extends S.Class("ValidateGraderResponse")({ + /** + * The grader used for the fine-tuning job. + */ + "grader": S.optionalWith(S.Record({ key: S.String, value: S.Unknown }), { nullable: true }) +}) {} + +export class ListFineTuningCheckpointPermissionsParamsOrder extends S.Literal("ascending", "descending") {} + +export class ListFineTuningCheckpointPermissionsParams extends S.Struct({ + "project_id": S.optionalWith(S.String, { nullable: true }), + "after": S.optionalWith(S.String, { nullable: true }), + "limit": S.optionalWith(S.Int, { nullable: true, default: () => 10 as const }), + "order": S.optionalWith(ListFineTuningCheckpointPermissionsParamsOrder, { + nullable: true, + default: () => "descending" as const + }) +}) {} + +/** + * The object type, which is always "checkpoint.permission". + */ +export class FineTuningCheckpointPermissionObject extends S.Literal("checkpoint.permission") {} + +/** + * The `checkpoint.permission` object represents a permission for a fine-tuned model checkpoint. + */ +export class FineTuningCheckpointPermission + extends S.Class("FineTuningCheckpointPermission")({ + /** + * The permission identifier, which can be referenced in the API endpoints. + */ + "id": S.String, + /** + * The Unix timestamp (in seconds) for when the permission was created. + */ + "created_at": S.Int, + /** + * The project identifier that the permission is for. + */ + "project_id": S.String, + /** + * The object type, which is always "checkpoint.permission". + */ + "object": FineTuningCheckpointPermissionObject + }) +{} + +export class ListFineTuningCheckpointPermissionResponseObject extends S.Literal("list") {} + +export class ListFineTuningCheckpointPermissionResponse + extends S.Class("ListFineTuningCheckpointPermissionResponse")({ + "data": S.Array(FineTuningCheckpointPermission), + "object": ListFineTuningCheckpointPermissionResponseObject, + "first_id": S.optionalWith(S.String, { nullable: true }), + "last_id": S.optionalWith(S.String, { nullable: true }), + "has_more": S.Boolean + }) +{} + +export class CreateFineTuningCheckpointPermissionRequest + extends S.Class("CreateFineTuningCheckpointPermissionRequest")({ + /** + * The project identifiers to grant access to. + */ + "project_ids": S.Array(S.String) + }) +{} + +/** + * The object type, which is always "checkpoint.permission". + */ +export class DeleteFineTuningCheckpointPermissionResponseObject extends S.Literal("checkpoint.permission") {} + +export class DeleteFineTuningCheckpointPermissionResponse + extends S.Class("DeleteFineTuningCheckpointPermissionResponse")({ + /** + * The ID of the fine-tuned model checkpoint permission that was deleted. + */ + "id": S.String, + /** + * The object type, which is always "checkpoint.permission". + */ + "object": DeleteFineTuningCheckpointPermissionResponseObject, + /** + * Whether the fine-tuned model checkpoint permission was successfully deleted. + */ + "deleted": S.Boolean + }) +{} + +export class ListPaginatedFineTuningJobsParams extends S.Struct({ + "after": S.optionalWith(S.String, { nullable: true }), + "limit": S.optionalWith(S.Int, { nullable: true, default: () => 20 as const }), + "metadata": S.optionalWith(S.Record({ key: S.String, value: S.Unknown }), { nullable: true }) +}) {} + +export class FineTuningJobHyperparametersBatchSizeEnum extends S.Literal("auto") {} + +export class FineTuningJobHyperparametersLearningRateMultiplierEnum extends S.Literal("auto") {} + +export class FineTuningJobHyperparametersNEpochsEnum extends S.Literal("auto") {} + +/** + * The object type, which is always "fine_tuning.job". + */ +export class FineTuningJobObject extends S.Literal("fine_tuning.job") {} + +/** + * The current status of the fine-tuning job, which can be either `validating_files`, `queued`, `running`, `succeeded`, `failed`, or `cancelled`. + */ +export class FineTuningJobStatus + extends S.Literal("validating_files", "queued", "running", "succeeded", "failed", "cancelled") +{} + +/** + * The type of the integration being enabled for the fine-tuning job + */ +export class FineTuningIntegrationType extends S.Literal("wandb") {} + +export class FineTuningIntegration extends S.Class("FineTuningIntegration")({ + /** + * The type of the integration being enabled for the fine-tuning job + */ + "type": FineTuningIntegrationType, + /** + * The settings for your integration with Weights and Biases. This payload specifies the project that + * metrics will be sent to. Optionally, you can set an explicit display name for your run, add tags + * to your run, and set a default entity (team, username, etc) to be associated with your run. + */ + "wandb": S.Struct({ + /** + * The name of the project that the new run will be created under. + */ + "project": S.String, + "name": S.optionalWith(S.String, { nullable: true }), + "entity": S.optionalWith(S.String, { nullable: true }), + /** + * A list of tags to be attached to the newly created run. These tags are passed through directly to WandB. Some + * default tags are generated by OpenAI: "openai/finetune", "openai/{base-model}", "openai/{ftjob-abcdef}". + */ + "tags": S.optionalWith(S.Array(S.String), { nullable: true }) + }) +}) {} + +/** + * The type of method. Is either `supervised`, `dpo`, or `reinforcement`. + */ +export class FineTuneMethodType extends S.Literal("supervised", "dpo", "reinforcement") {} + +export class FineTuneSupervisedHyperparametersBatchSizeEnum extends S.Literal("auto") {} + +export class FineTuneSupervisedHyperparametersLearningRateMultiplierEnum extends S.Literal("auto") {} + +export class FineTuneSupervisedHyperparametersNEpochsEnum extends S.Literal("auto") {} + +/** + * The hyperparameters used for the fine-tuning job. + */ +export class FineTuneSupervisedHyperparameters + extends S.Class("FineTuneSupervisedHyperparameters")({ + /** + * Number of examples in each batch. A larger batch size means that model parameters are updated less frequently, but with lower variance. + */ + "batch_size": S.optionalWith( + S.Union( + FineTuneSupervisedHyperparametersBatchSizeEnum, + S.Int.pipe(S.greaterThanOrEqualTo(1), S.lessThanOrEqualTo(256)) + ), + { nullable: true, default: () => "auto" as const } + ), + /** + * Scaling factor for the learning rate. A smaller learning rate may be useful to avoid overfitting. + */ + "learning_rate_multiplier": S.optionalWith( + S.Union(FineTuneSupervisedHyperparametersLearningRateMultiplierEnum, S.Number.pipe(S.greaterThan(0))), + { nullable: true } + ), + /** + * The number of epochs to train the model for. An epoch refers to one full cycle through the training dataset. + */ + "n_epochs": S.optionalWith( + S.Union( + FineTuneSupervisedHyperparametersNEpochsEnum, + S.Int.pipe(S.greaterThanOrEqualTo(1), S.lessThanOrEqualTo(50)) + ), + { nullable: true, default: () => "auto" as const } + ) + }) +{} + +/** + * Configuration for the supervised fine-tuning method. + */ +export class FineTuneSupervisedMethod extends S.Class("FineTuneSupervisedMethod")({ + "hyperparameters": S.optionalWith(FineTuneSupervisedHyperparameters, { nullable: true }) +}) {} + +export class FineTuneDPOHyperparametersBetaEnum extends S.Literal("auto") {} + +export class FineTuneDPOHyperparametersBatchSizeEnum extends S.Literal("auto") {} + +export class FineTuneDPOHyperparametersLearningRateMultiplierEnum extends S.Literal("auto") {} + +export class FineTuneDPOHyperparametersNEpochsEnum extends S.Literal("auto") {} + +/** + * The hyperparameters used for the DPO fine-tuning job. + */ +export class FineTuneDPOHyperparameters extends S.Class("FineTuneDPOHyperparameters")({ + /** + * The beta value for the DPO method. A higher beta value will increase the weight of the penalty between the policy and reference model. + */ + "beta": S.optionalWith( + S.Union(FineTuneDPOHyperparametersBetaEnum, S.Number.pipe(S.greaterThan(0), S.lessThanOrEqualTo(2))), + { nullable: true } + ), + /** + * Number of examples in each batch. A larger batch size means that model parameters are updated less frequently, but with lower variance. + */ + "batch_size": S.optionalWith( + S.Union(FineTuneDPOHyperparametersBatchSizeEnum, S.Int.pipe(S.greaterThanOrEqualTo(1), S.lessThanOrEqualTo(256))), + { + nullable: true, + default: () => "auto" as const + } + ), + /** + * Scaling factor for the learning rate. A smaller learning rate may be useful to avoid overfitting. + */ + "learning_rate_multiplier": S.optionalWith( + S.Union(FineTuneDPOHyperparametersLearningRateMultiplierEnum, S.Number.pipe(S.greaterThan(0))), + { nullable: true } + ), + /** + * The number of epochs to train the model for. An epoch refers to one full cycle through the training dataset. + */ + "n_epochs": S.optionalWith( + S.Union(FineTuneDPOHyperparametersNEpochsEnum, S.Int.pipe(S.greaterThanOrEqualTo(1), S.lessThanOrEqualTo(50))), + { + nullable: true, + default: () => "auto" as const + } + ) +}) {} + +/** + * Configuration for the DPO fine-tuning method. + */ +export class FineTuneDPOMethod extends S.Class("FineTuneDPOMethod")({ + "hyperparameters": S.optionalWith(FineTuneDPOHyperparameters, { nullable: true }) +}) {} + +export class FineTuneReinforcementHyperparametersBatchSizeEnum extends S.Literal("auto") {} + +export class FineTuneReinforcementHyperparametersLearningRateMultiplierEnum extends S.Literal("auto") {} + +export class FineTuneReinforcementHyperparametersNEpochsEnum extends S.Literal("auto") {} + +/** + * Level of reasoning effort. + */ +export class FineTuneReinforcementHyperparametersReasoningEffort + extends S.Literal("default", "low", "medium", "high") +{} + +export class FineTuneReinforcementHyperparametersComputeMultiplierEnum extends S.Literal("auto") {} + +export class FineTuneReinforcementHyperparametersEvalIntervalEnum extends S.Literal("auto") {} + +export class FineTuneReinforcementHyperparametersEvalSamplesEnum extends S.Literal("auto") {} + +/** + * The hyperparameters used for the reinforcement fine-tuning job. + */ +export class FineTuneReinforcementHyperparameters + extends S.Class("FineTuneReinforcementHyperparameters")({ + /** + * Number of examples in each batch. A larger batch size means that model parameters are updated less frequently, but with lower variance. + */ + "batch_size": S.optionalWith( + S.Union( + FineTuneReinforcementHyperparametersBatchSizeEnum, + S.Int.pipe(S.greaterThanOrEqualTo(1), S.lessThanOrEqualTo(256)) + ), + { nullable: true, default: () => "auto" as const } + ), + /** + * Scaling factor for the learning rate. A smaller learning rate may be useful to avoid overfitting. + */ + "learning_rate_multiplier": S.optionalWith( + S.Union(FineTuneReinforcementHyperparametersLearningRateMultiplierEnum, S.Number.pipe(S.greaterThan(0))), + { nullable: true } + ), + /** + * The number of epochs to train the model for. An epoch refers to one full cycle through the training dataset. + */ + "n_epochs": S.optionalWith( + S.Union( + FineTuneReinforcementHyperparametersNEpochsEnum, + S.Int.pipe(S.greaterThanOrEqualTo(1), S.lessThanOrEqualTo(50)) + ), + { nullable: true, default: () => "auto" as const } + ), + /** + * Level of reasoning effort. + */ + "reasoning_effort": S.optionalWith(FineTuneReinforcementHyperparametersReasoningEffort, { + nullable: true, + default: () => "default" as const + }), + /** + * Multiplier on amount of compute used for exploring search space during training. + */ + "compute_multiplier": S.optionalWith( + S.Union( + FineTuneReinforcementHyperparametersComputeMultiplierEnum, + S.Number.pipe(S.greaterThan(0.00001), S.lessThanOrEqualTo(10)) + ), + { nullable: true } + ), + /** + * The number of training steps between evaluation runs. + */ + "eval_interval": S.optionalWith( + S.Union(FineTuneReinforcementHyperparametersEvalIntervalEnum, S.Int.pipe(S.greaterThanOrEqualTo(1))), + { + nullable: true, + default: () => "auto" as const + } + ), + /** + * Number of evaluation samples to generate per training step. + */ + "eval_samples": S.optionalWith( + S.Union(FineTuneReinforcementHyperparametersEvalSamplesEnum, S.Int.pipe(S.greaterThanOrEqualTo(1))), + { + nullable: true, + default: () => "auto" as const + } + ) + }) +{} + +/** + * Configuration for the reinforcement fine-tuning method. + */ +export class FineTuneReinforcementMethod extends S.Class("FineTuneReinforcementMethod")({ + /** + * The grader used for the fine-tuning job. + */ + "grader": S.Record({ key: S.String, value: S.Unknown }), + "hyperparameters": S.optionalWith(FineTuneReinforcementHyperparameters, { nullable: true }) +}) {} + +/** + * The method used for fine-tuning. + */ +export class FineTuneMethod extends S.Class("FineTuneMethod")({ + /** + * The type of method. Is either `supervised`, `dpo`, or `reinforcement`. + */ + "type": FineTuneMethodType, + "supervised": S.optionalWith(FineTuneSupervisedMethod, { nullable: true }), + "dpo": S.optionalWith(FineTuneDPOMethod, { nullable: true }), + "reinforcement": S.optionalWith(FineTuneReinforcementMethod, { nullable: true }) +}) {} + +/** + * The `fine_tuning.job` object represents a fine-tuning job that has been created through the API. + */ +export class FineTuningJob extends S.Class("FineTuningJob")({ + /** + * The object identifier, which can be referenced in the API endpoints. + */ + "id": S.String, + /** + * The Unix timestamp (in seconds) for when the fine-tuning job was created. + */ + "created_at": S.Int, + "error": S.NullOr(S.Struct({ + /** + * A machine-readable error code. + */ + "code": S.String, + /** + * A human-readable error message. + */ + "message": S.String, + "param": S.NullOr(S.String) + })), + "fine_tuned_model": S.NullOr(S.String), + "finished_at": S.NullOr(S.Int), + /** + * The hyperparameters used for the fine-tuning job. This value will only be returned when running `supervised` jobs. + */ + "hyperparameters": S.Struct({ + "batch_size": S.optionalWith( + S.Union( + FineTuningJobHyperparametersBatchSizeEnum, + S.Int.pipe(S.greaterThanOrEqualTo(1), S.lessThanOrEqualTo(256)) + ), + { nullable: true } + ), + /** + * Scaling factor for the learning rate. A smaller learning rate may be useful to avoid + * overfitting. + */ + "learning_rate_multiplier": S.optionalWith( + S.Union(FineTuningJobHyperparametersLearningRateMultiplierEnum, S.Number.pipe(S.greaterThan(0))), + { nullable: true } + ), + /** + * The number of epochs to train the model for. An epoch refers to one full cycle + * through the training dataset. + */ + "n_epochs": S.optionalWith( + S.Union(FineTuningJobHyperparametersNEpochsEnum, S.Int.pipe(S.greaterThanOrEqualTo(1), S.lessThanOrEqualTo(50))), + { + nullable: true, + default: () => "auto" as const + } + ) + }), + /** + * The base model that is being fine-tuned. + */ + "model": S.String, + /** + * The object type, which is always "fine_tuning.job". + */ + "object": FineTuningJobObject, + /** + * The organization that owns the fine-tuning job. + */ + "organization_id": S.String, + /** + * The compiled results file ID(s) for the fine-tuning job. You can retrieve the results with the [Files API](https://platform.openai.com/docs/api-reference/files/retrieve-contents). + */ + "result_files": S.Array(S.String), + /** + * The current status of the fine-tuning job, which can be either `validating_files`, `queued`, `running`, `succeeded`, `failed`, or `cancelled`. + */ + "status": FineTuningJobStatus, + "trained_tokens": S.NullOr(S.Int), + /** + * The file ID used for training. You can retrieve the training data with the [Files API](https://platform.openai.com/docs/api-reference/files/retrieve-contents). + */ + "training_file": S.String, + "validation_file": S.NullOr(S.String), + "integrations": S.optionalWith(S.Array(FineTuningIntegration).pipe(S.maxItems(5)), { nullable: true }), + /** + * The seed used for the fine-tuning job. + */ + "seed": S.Int, + "estimated_finish": S.optionalWith(S.Int, { nullable: true }), + "method": S.optionalWith(FineTuneMethod, { nullable: true }), + "metadata": S.optionalWith(S.Record({ key: S.String, value: S.Unknown }), { nullable: true }) +}) {} + +export class ListPaginatedFineTuningJobsResponseObject extends S.Literal("list") {} + +export class ListPaginatedFineTuningJobsResponse + extends S.Class("ListPaginatedFineTuningJobsResponse")({ + "data": S.Array(FineTuningJob), + "has_more": S.Boolean, + "object": ListPaginatedFineTuningJobsResponseObject + }) +{} + +export class CreateFineTuningJobRequestModelEnum + extends S.Literal("babbage-002", "davinci-002", "gpt-3.5-turbo", "gpt-4o-mini") +{} + +export class CreateFineTuningJobRequestHyperparametersBatchSizeEnum extends S.Literal("auto") {} + +export class CreateFineTuningJobRequestHyperparametersLearningRateMultiplierEnum extends S.Literal("auto") {} + +export class CreateFineTuningJobRequestHyperparametersNEpochsEnum extends S.Literal("auto") {} + +export class CreateFineTuningJobRequest extends S.Class("CreateFineTuningJobRequest")({ + /** + * The name of the model to fine-tune. You can select one of the + * [supported models](https://platform.openai.com/docs/guides/fine-tuning#which-models-can-be-fine-tuned). + */ + "model": S.Union(S.String, CreateFineTuningJobRequestModelEnum), + /** + * The ID of an uploaded file that contains training data. + * + * See [upload file](https://platform.openai.com/docs/api-reference/files/create) for how to upload a file. + * + * Your dataset must be formatted as a JSONL file. Additionally, you must upload your file with the purpose `fine-tune`. + * + * The contents of the file should differ depending on if the model uses the [chat](https://platform.openai.com/docs/api-reference/fine-tuning/chat-input), [completions](https://platform.openai.com/docs/api-reference/fine-tuning/completions-input) format, or if the fine-tuning method uses the [preference](https://platform.openai.com/docs/api-reference/fine-tuning/preference-input) format. + * + * See the [fine-tuning guide](https://platform.openai.com/docs/guides/model-optimization) for more details. + */ + "training_file": S.String, + /** + * The hyperparameters used for the fine-tuning job. + * This value is now deprecated in favor of `method`, and should be passed in under the `method` parameter. + */ + "hyperparameters": S.optionalWith( + S.Struct({ + /** + * Number of examples in each batch. A larger batch size means that model parameters + * are updated less frequently, but with lower variance. + */ + "batch_size": S.optionalWith( + S.Union( + CreateFineTuningJobRequestHyperparametersBatchSizeEnum, + S.Int.pipe(S.greaterThanOrEqualTo(1), S.lessThanOrEqualTo(256)) + ), + { nullable: true, default: () => "auto" as const } + ), + /** + * Scaling factor for the learning rate. A smaller learning rate may be useful to avoid + * overfitting. + */ + "learning_rate_multiplier": S.optionalWith( + S.Union(CreateFineTuningJobRequestHyperparametersLearningRateMultiplierEnum, S.Number.pipe(S.greaterThan(0))), + { nullable: true } + ), + /** + * The number of epochs to train the model for. An epoch refers to one full cycle + * through the training dataset. + */ + "n_epochs": S.optionalWith( + S.Union( + CreateFineTuningJobRequestHyperparametersNEpochsEnum, + S.Int.pipe(S.greaterThanOrEqualTo(1), S.lessThanOrEqualTo(50)) + ), + { nullable: true, default: () => "auto" as const } + ) + }), + { nullable: true } + ), + /** + * A string of up to 64 characters that will be added to your fine-tuned model name. + * + * For example, a `suffix` of "custom-model-name" would produce a model name like `ft:gpt-4o-mini:openai:custom-model-name:7p4lURel`. + */ + "suffix": S.optionalWith(S.NullOr(S.String.pipe(S.minLength(1), S.maxLength(64))), { default: () => null }), + /** + * The ID of an uploaded file that contains validation data. + * + * If you provide this file, the data is used to generate validation + * metrics periodically during fine-tuning. These metrics can be viewed in + * the fine-tuning results file. + * The same data should not be present in both train and validation files. + * + * Your dataset must be formatted as a JSONL file. You must upload your file with the purpose `fine-tune`. + * + * See the [fine-tuning guide](https://platform.openai.com/docs/guides/model-optimization) for more details. + */ + "validation_file": S.optionalWith(S.String, { nullable: true }), + /** + * A list of integrations to enable for your fine-tuning job. + */ + "integrations": S.optionalWith( + S.Array(S.Struct({ + /** + * The type of integration to enable. Currently, only "wandb" (Weights and Biases) is supported. + */ + "type": S.Literal("wandb"), + /** + * The settings for your integration with Weights and Biases. This payload specifies the project that + * metrics will be sent to. Optionally, you can set an explicit display name for your run, add tags + * to your run, and set a default entity (team, username, etc) to be associated with your run. + */ + "wandb": S.Struct({ + /** + * The name of the project that the new run will be created under. + */ + "project": S.String, + /** + * A display name to set for the run. If not set, we will use the Job ID as the name. + */ + "name": S.optionalWith(S.String, { nullable: true }), + /** + * The entity to use for the run. This allows you to set the team or username of the WandB user that you would + * like associated with the run. If not set, the default entity for the registered WandB API key is used. + */ + "entity": S.optionalWith(S.String, { nullable: true }), + /** + * A list of tags to be attached to the newly created run. These tags are passed through directly to WandB. Some + * default tags are generated by OpenAI: "openai/finetune", "openai/{base-model}", "openai/{ftjob-abcdef}". + */ + "tags": S.optionalWith(S.Array(S.String), { nullable: true }) + }) + })), + { nullable: true } + ), + /** + * The seed controls the reproducibility of the job. Passing in the same seed and job parameters should produce the same results, but may differ in rare cases. + * If a seed is not specified, one will be generated for you. + */ + "seed": S.optionalWith(S.Int.pipe(S.greaterThanOrEqualTo(0), S.lessThanOrEqualTo(2147483647)), { nullable: true }), + "method": S.optionalWith(FineTuneMethod, { nullable: true }), + "metadata": S.optionalWith(S.Record({ key: S.String, value: S.Unknown }), { nullable: true }) +}) {} + +export class ListFineTuningJobCheckpointsParams extends S.Struct({ + "after": S.optionalWith(S.String, { nullable: true }), + "limit": S.optionalWith(S.Int, { nullable: true, default: () => 10 as const }) +}) {} + +/** + * The object type, which is always "fine_tuning.job.checkpoint". + */ +export class FineTuningJobCheckpointObject extends S.Literal("fine_tuning.job.checkpoint") {} + +/** + * The `fine_tuning.job.checkpoint` object represents a model checkpoint for a fine-tuning job that is ready to use. + */ +export class FineTuningJobCheckpoint extends S.Class("FineTuningJobCheckpoint")({ + /** + * The checkpoint identifier, which can be referenced in the API endpoints. + */ + "id": S.String, + /** + * The Unix timestamp (in seconds) for when the checkpoint was created. + */ + "created_at": S.Int, + /** + * The name of the fine-tuned checkpoint model that is created. + */ + "fine_tuned_model_checkpoint": S.String, + /** + * The step number that the checkpoint was created at. + */ + "step_number": S.Int, + /** + * Metrics at the step number during the fine-tuning job. + */ + "metrics": S.Struct({ + "step": S.optionalWith(S.Number, { nullable: true }), + "train_loss": S.optionalWith(S.Number, { nullable: true }), + "train_mean_token_accuracy": S.optionalWith(S.Number, { nullable: true }), + "valid_loss": S.optionalWith(S.Number, { nullable: true }), + "valid_mean_token_accuracy": S.optionalWith(S.Number, { nullable: true }), + "full_valid_loss": S.optionalWith(S.Number, { nullable: true }), + "full_valid_mean_token_accuracy": S.optionalWith(S.Number, { nullable: true }) + }), + /** + * The name of the fine-tuning job that this checkpoint was created from. + */ + "fine_tuning_job_id": S.String, + /** + * The object type, which is always "fine_tuning.job.checkpoint". + */ + "object": FineTuningJobCheckpointObject +}) {} + +export class ListFineTuningJobCheckpointsResponseObject extends S.Literal("list") {} + +export class ListFineTuningJobCheckpointsResponse + extends S.Class("ListFineTuningJobCheckpointsResponse")({ + "data": S.Array(FineTuningJobCheckpoint), + "object": ListFineTuningJobCheckpointsResponseObject, + "first_id": S.optionalWith(S.String, { nullable: true }), + "last_id": S.optionalWith(S.String, { nullable: true }), + "has_more": S.Boolean + }) +{} + +export class ListFineTuningEventsParams extends S.Struct({ + "after": S.optionalWith(S.String, { nullable: true }), + "limit": S.optionalWith(S.Int, { nullable: true, default: () => 20 as const }) +}) {} + +/** + * The object type, which is always "fine_tuning.job.event". + */ +export class FineTuningJobEventObject extends S.Literal("fine_tuning.job.event") {} + +/** + * The log level of the event. + */ +export class FineTuningJobEventLevel extends S.Literal("info", "warn", "error") {} + +/** + * The type of event. + */ +export class FineTuningJobEventType extends S.Literal("message", "metrics") {} + +/** + * Fine-tuning job event object + */ +export class FineTuningJobEvent extends S.Class("FineTuningJobEvent")({ + /** + * The object type, which is always "fine_tuning.job.event". + */ + "object": FineTuningJobEventObject, + /** + * The object identifier. + */ + "id": S.String, + /** + * The Unix timestamp (in seconds) for when the fine-tuning job was created. + */ + "created_at": S.Int, + /** + * The log level of the event. + */ + "level": FineTuningJobEventLevel, + /** + * The message of the event. + */ + "message": S.String, + /** + * The type of event. + */ + "type": S.optionalWith(FineTuningJobEventType, { nullable: true }), + /** + * The data associated with the event. + */ + "data": S.optionalWith(S.Record({ key: S.String, value: S.Unknown }), { nullable: true }) +}) {} + +export class ListFineTuningJobEventsResponseObject extends S.Literal("list") {} + +export class ListFineTuningJobEventsResponse + extends S.Class("ListFineTuningJobEventsResponse")({ + "data": S.Array(FineTuningJobEvent), + "object": ListFineTuningJobEventsResponseObject, + "has_more": S.Boolean + }) +{} + +/** + * Allows to set transparency for the background of the generated image(s). + * This parameter is only supported for `gpt-image-1`. Must be one of + * `transparent`, `opaque` or `auto` (default value). When `auto` is used, the + * model will automatically determine the best background for the image. + * + * If `transparent`, the output format needs to support transparency, so it + * should be set to either `png` (default value) or `webp`. + */ +export class CreateImageEditRequestBackground extends S.Literal("transparent", "opaque", "auto") {} + +export class CreateImageEditRequestModelEnum extends S.Literal("dall-e-2", "gpt-image-1", "gpt-image-1-mini") {} + +/** + * The size of the generated images. Must be one of `1024x1024`, `1536x1024` (landscape), `1024x1536` (portrait), or `auto` (default value) for `gpt-image-1`, and one of `256x256`, `512x512`, or `1024x1024` for `dall-e-2`. + */ +export class CreateImageEditRequestSize + extends S.Literal("256x256", "512x512", "1024x1024", "1536x1024", "1024x1536", "auto") +{} + +/** + * The format in which the generated images are returned. Must be one of `url` or `b64_json`. URLs are only valid for 60 minutes after the image has been generated. This parameter is only supported for `dall-e-2`, as `gpt-image-1` will always return base64-encoded images. + */ +export class CreateImageEditRequestResponseFormat extends S.Literal("url", "b64_json") {} + +/** + * The format in which the generated images are returned. This parameter is + * only supported for `gpt-image-1`. Must be one of `png`, `jpeg`, or `webp`. + * The default value is `png`. + */ +export class CreateImageEditRequestOutputFormat extends S.Literal("png", "jpeg", "webp") {} + +export class PartialImages extends S.Union( + /** + * The number of partial images to generate. This parameter is used for + * streaming responses that return partial images. Value must be between 0 and 3. + * When set to 0, the response will be a single image sent in one streaming event. + * + * Note that the final image may be sent before the full number of partial images + * are generated if the full image is generated more quickly. + */ + S.Int.pipe(S.greaterThanOrEqualTo(0), S.lessThanOrEqualTo(3)), + S.Null +) {} + +/** + * The quality of the image that will be generated. `high`, `medium` and `low` are only supported for `gpt-image-1`. `dall-e-2` only supports `standard` quality. Defaults to `auto`. + */ +export class CreateImageEditRequestQuality extends S.Literal("standard", "low", "medium", "high", "auto") {} + +export class CreateImageEditRequest extends S.Class("CreateImageEditRequest")({ + /** + * The image(s) to edit. Must be a supported image file or an array of images. + * + * For `gpt-image-1`, each image should be a `png`, `webp`, or `jpg` file less + * than 50MB. You can provide up to 16 images. + * + * For `dall-e-2`, you can only provide one image, and it should be a square + * `png` file less than 4MB. + */ + "image": S.Union(S.instanceOf(globalThis.Blob), S.Array(S.instanceOf(globalThis.Blob)).pipe(S.maxItems(16))), + /** + * A text description of the desired image(s). The maximum length is 1000 characters for `dall-e-2`, and 32000 characters for `gpt-image-1`. + */ + "prompt": S.String, + /** + * An additional image whose fully transparent areas (e.g. where alpha is zero) indicate where `image` should be edited. If there are multiple images provided, the mask will be applied on the first image. Must be a valid PNG file, less than 4MB, and have the same dimensions as `image`. + */ + "mask": S.optionalWith(S.instanceOf(globalThis.Blob), { nullable: true }), + /** + * Allows to set transparency for the background of the generated image(s). + * This parameter is only supported for `gpt-image-1`. Must be one of + * `transparent`, `opaque` or `auto` (default value). When `auto` is used, the + * model will automatically determine the best background for the image. + * + * If `transparent`, the output format needs to support transparency, so it + * should be set to either `png` (default value) or `webp`. + */ + "background": S.optionalWith(CreateImageEditRequestBackground, { nullable: true, default: () => "auto" as const }), + /** + * The model to use for image generation. Only `dall-e-2` and `gpt-image-1` are supported. Defaults to `dall-e-2` unless a parameter specific to `gpt-image-1` is used. + */ + "model": S.optionalWith(S.Union(S.String, CreateImageEditRequestModelEnum), { nullable: true }), + /** + * The number of images to generate. Must be between 1 and 10. + */ + "n": S.optionalWith(S.Int.pipe(S.greaterThanOrEqualTo(1), S.lessThanOrEqualTo(10)), { + nullable: true, + default: () => 1 as const + }), + /** + * The size of the generated images. Must be one of `1024x1024`, `1536x1024` (landscape), `1024x1536` (portrait), or `auto` (default value) for `gpt-image-1`, and one of `256x256`, `512x512`, or `1024x1024` for `dall-e-2`. + */ + "size": S.optionalWith(CreateImageEditRequestSize, { nullable: true, default: () => "1024x1024" as const }), + /** + * The format in which the generated images are returned. Must be one of `url` or `b64_json`. URLs are only valid for 60 minutes after the image has been generated. This parameter is only supported for `dall-e-2`, as `gpt-image-1` will always return base64-encoded images. + */ + "response_format": S.optionalWith(CreateImageEditRequestResponseFormat, { + nullable: true, + default: () => "url" as const + }), + /** + * The format in which the generated images are returned. This parameter is + * only supported for `gpt-image-1`. Must be one of `png`, `jpeg`, or `webp`. + * The default value is `png`. + */ + "output_format": S.optionalWith(CreateImageEditRequestOutputFormat, { + nullable: true, + default: () => "png" as const + }), + /** + * The compression level (0-100%) for the generated images. This parameter + * is only supported for `gpt-image-1` with the `webp` or `jpeg` output + * formats, and defaults to 100. + */ + "output_compression": S.optionalWith(S.Int, { nullable: true, default: () => 100 as const }), + /** + * A unique identifier representing your end-user, which can help OpenAI to monitor and detect abuse. [Learn more](https://platform.openai.com/docs/guides/safety-best-practices#end-user-ids). + */ + "user": S.optionalWith(S.String, { nullable: true }), + "input_fidelity": S.optionalWith(InputFidelity, { nullable: true }), + /** + * Edit the image in streaming mode. Defaults to `false`. See the + * [Image generation guide](https://platform.openai.com/docs/guides/image-generation) for more information. + */ + "stream": S.optionalWith(S.Boolean, { nullable: true, default: () => false as const }), + "partial_images": S.optionalWith(S.Int.pipe(S.greaterThanOrEqualTo(0), S.lessThanOrEqualTo(3)), { nullable: true }), + /** + * The quality of the image that will be generated. `high`, `medium` and `low` are only supported for `gpt-image-1`. `dall-e-2` only supports `standard` quality. Defaults to `auto`. + */ + "quality": S.optionalWith(CreateImageEditRequestQuality, { nullable: true, default: () => "auto" as const }) +}) {} + +/** + * Represents the content or the URL of an image generated by the OpenAI API. + */ +export class Image extends S.Class("Image")({ + /** + * The base64-encoded JSON of the generated image. Default value for `gpt-image-1`, and only present if `response_format` is set to `b64_json` for `dall-e-2` and `dall-e-3`. + */ + "b64_json": S.optionalWith(S.String, { nullable: true }), + /** + * When using `dall-e-2` or `dall-e-3`, the URL of the generated image if `response_format` is set to `url` (default value). Unsupported for `gpt-image-1`. + */ + "url": S.optionalWith(S.String, { nullable: true }), + /** + * For `dall-e-3` only, the revised prompt that was used to generate the image. + */ + "revised_prompt": S.optionalWith(S.String, { nullable: true }) +}) {} + +/** + * The background parameter used for the image generation. Either `transparent` or `opaque`. + */ +export class ImagesResponseBackground extends S.Literal("transparent", "opaque") {} + +/** + * The output format of the image generation. Either `png`, `webp`, or `jpeg`. + */ +export class ImagesResponseOutputFormat extends S.Literal("png", "webp", "jpeg") {} + +/** + * The size of the image generated. Either `1024x1024`, `1024x1536`, or `1536x1024`. + */ +export class ImagesResponseSize extends S.Literal("1024x1024", "1024x1536", "1536x1024") {} + +/** + * The quality of the image generated. Either `low`, `medium`, or `high`. + */ +export class ImagesResponseQuality extends S.Literal("low", "medium", "high") {} + +/** + * The input tokens detailed information for the image generation. + */ +export class ImageGenInputUsageDetails extends S.Class("ImageGenInputUsageDetails")({ + /** + * The number of text tokens in the input prompt. + */ + "text_tokens": S.Int, + /** + * The number of image tokens in the input prompt. + */ + "image_tokens": S.Int +}) {} + +/** + * For `gpt-image-1` only, the token usage information for the image generation. + */ +export class ImageGenUsage extends S.Class("ImageGenUsage")({ + /** + * The number of tokens (images and text) in the input prompt. + */ + "input_tokens": S.Int, + /** + * The total number of tokens (images and text) used for the image generation. + */ + "total_tokens": S.Int, + /** + * The number of output tokens generated by the model. + */ + "output_tokens": S.Int, + "input_tokens_details": ImageGenInputUsageDetails +}) {} + +/** + * The response from the image generation endpoint. + */ +export class ImagesResponse extends S.Class("ImagesResponse")({ + /** + * The Unix timestamp (in seconds) of when the image was created. + */ + "created": S.Int, + /** + * The list of generated images. + */ + "data": S.optionalWith(S.Array(Image), { nullable: true }), + /** + * The background parameter used for the image generation. Either `transparent` or `opaque`. + */ + "background": S.optionalWith(ImagesResponseBackground, { nullable: true }), + /** + * The output format of the image generation. Either `png`, `webp`, or `jpeg`. + */ + "output_format": S.optionalWith(ImagesResponseOutputFormat, { nullable: true }), + /** + * The size of the image generated. Either `1024x1024`, `1024x1536`, or `1536x1024`. + */ + "size": S.optionalWith(ImagesResponseSize, { nullable: true }), + /** + * The quality of the image generated. Either `low`, `medium`, or `high`. + */ + "quality": S.optionalWith(ImagesResponseQuality, { nullable: true }), + "usage": S.optionalWith(ImageGenUsage, { nullable: true }) +}) {} + +export class CreateImageRequestModelEnum extends S.Literal("dall-e-2", "dall-e-3", "gpt-image-1", "gpt-image-1-mini") {} + +/** + * The quality of the image that will be generated. + * + * - `auto` (default value) will automatically select the best quality for the given model. + * - `high`, `medium` and `low` are supported for `gpt-image-1`. + * - `hd` and `standard` are supported for `dall-e-3`. + * - `standard` is the only option for `dall-e-2`. + */ +export class CreateImageRequestQuality extends S.Literal("standard", "hd", "low", "medium", "high", "auto") {} + +/** + * The format in which generated images with `dall-e-2` and `dall-e-3` are returned. Must be one of `url` or `b64_json`. URLs are only valid for 60 minutes after the image has been generated. This parameter isn't supported for `gpt-image-1` which will always return base64-encoded images. + */ +export class CreateImageRequestResponseFormat extends S.Literal("url", "b64_json") {} + +/** + * The format in which the generated images are returned. This parameter is only supported for `gpt-image-1`. Must be one of `png`, `jpeg`, or `webp`. + */ +export class CreateImageRequestOutputFormat extends S.Literal("png", "jpeg", "webp") {} + +/** + * The size of the generated images. Must be one of `1024x1024`, `1536x1024` (landscape), `1024x1536` (portrait), or `auto` (default value) for `gpt-image-1`, one of `256x256`, `512x512`, or `1024x1024` for `dall-e-2`, and one of `1024x1024`, `1792x1024`, or `1024x1792` for `dall-e-3`. + */ +export class CreateImageRequestSize + extends S.Literal("auto", "1024x1024", "1536x1024", "1024x1536", "256x256", "512x512", "1792x1024", "1024x1792") +{} + +/** + * Control the content-moderation level for images generated by `gpt-image-1`. Must be either `low` for less restrictive filtering or `auto` (default value). + */ +export class CreateImageRequestModeration extends S.Literal("low", "auto") {} + +/** + * Allows to set transparency for the background of the generated image(s). + * This parameter is only supported for `gpt-image-1`. Must be one of + * `transparent`, `opaque` or `auto` (default value). When `auto` is used, the + * model will automatically determine the best background for the image. + * + * If `transparent`, the output format needs to support transparency, so it + * should be set to either `png` (default value) or `webp`. + */ +export class CreateImageRequestBackground extends S.Literal("transparent", "opaque", "auto") {} + +/** + * The style of the generated images. This parameter is only supported for `dall-e-3`. Must be one of `vivid` or `natural`. Vivid causes the model to lean towards generating hyper-real and dramatic images. Natural causes the model to produce more natural, less hyper-real looking images. + */ +export class CreateImageRequestStyle extends S.Literal("vivid", "natural") {} + +export class CreateImageRequest extends S.Class("CreateImageRequest")({ + /** + * A text description of the desired image(s). The maximum length is 32000 characters for `gpt-image-1`, 1000 characters for `dall-e-2` and 4000 characters for `dall-e-3`. + */ + "prompt": S.String, + /** + * The model to use for image generation. One of `dall-e-2`, `dall-e-3`, or `gpt-image-1`. Defaults to `dall-e-2` unless a parameter specific to `gpt-image-1` is used. + */ + "model": S.optionalWith(S.Union(S.String, CreateImageRequestModelEnum), { nullable: true }), + /** + * The number of images to generate. Must be between 1 and 10. For `dall-e-3`, only `n=1` is supported. + */ + "n": S.optionalWith(S.Int.pipe(S.greaterThanOrEqualTo(1), S.lessThanOrEqualTo(10)), { + nullable: true, + default: () => 1 as const + }), + /** + * The quality of the image that will be generated. + * + * - `auto` (default value) will automatically select the best quality for the given model. + * - `high`, `medium` and `low` are supported for `gpt-image-1`. + * - `hd` and `standard` are supported for `dall-e-3`. + * - `standard` is the only option for `dall-e-2`. + */ + "quality": S.optionalWith(CreateImageRequestQuality, { nullable: true, default: () => "auto" as const }), + /** + * The format in which generated images with `dall-e-2` and `dall-e-3` are returned. Must be one of `url` or `b64_json`. URLs are only valid for 60 minutes after the image has been generated. This parameter isn't supported for `gpt-image-1` which will always return base64-encoded images. + */ + "response_format": S.optionalWith(CreateImageRequestResponseFormat, { + nullable: true, + default: () => "url" as const + }), + /** + * The format in which the generated images are returned. This parameter is only supported for `gpt-image-1`. Must be one of `png`, `jpeg`, or `webp`. + */ + "output_format": S.optionalWith(CreateImageRequestOutputFormat, { nullable: true, default: () => "png" as const }), + /** + * The compression level (0-100%) for the generated images. This parameter is only supported for `gpt-image-1` with the `webp` or `jpeg` output formats, and defaults to 100. + */ + "output_compression": S.optionalWith(S.Int, { nullable: true, default: () => 100 as const }), + /** + * Generate the image in streaming mode. Defaults to `false`. See the + * [Image generation guide](https://platform.openai.com/docs/guides/image-generation) for more information. + * This parameter is only supported for `gpt-image-1`. + */ + "stream": S.optionalWith(S.Boolean, { nullable: true, default: () => false as const }), + "partial_images": S.optionalWith(S.Int.pipe(S.greaterThanOrEqualTo(0), S.lessThanOrEqualTo(3)), { nullable: true }), + /** + * The size of the generated images. Must be one of `1024x1024`, `1536x1024` (landscape), `1024x1536` (portrait), or `auto` (default value) for `gpt-image-1`, one of `256x256`, `512x512`, or `1024x1024` for `dall-e-2`, and one of `1024x1024`, `1792x1024`, or `1024x1792` for `dall-e-3`. + */ + "size": S.optionalWith(CreateImageRequestSize, { nullable: true, default: () => "auto" as const }), + /** + * Control the content-moderation level for images generated by `gpt-image-1`. Must be either `low` for less restrictive filtering or `auto` (default value). + */ + "moderation": S.optionalWith(CreateImageRequestModeration, { nullable: true, default: () => "auto" as const }), + /** + * Allows to set transparency for the background of the generated image(s). + * This parameter is only supported for `gpt-image-1`. Must be one of + * `transparent`, `opaque` or `auto` (default value). When `auto` is used, the + * model will automatically determine the best background for the image. + * + * If `transparent`, the output format needs to support transparency, so it + * should be set to either `png` (default value) or `webp`. + */ + "background": S.optionalWith(CreateImageRequestBackground, { nullable: true, default: () => "auto" as const }), + /** + * The style of the generated images. This parameter is only supported for `dall-e-3`. Must be one of `vivid` or `natural`. Vivid causes the model to lean towards generating hyper-real and dramatic images. Natural causes the model to produce more natural, less hyper-real looking images. + */ + "style": S.optionalWith(CreateImageRequestStyle, { nullable: true, default: () => "vivid" as const }), + /** + * A unique identifier representing your end-user, which can help OpenAI to monitor and detect abuse. [Learn more](https://platform.openai.com/docs/guides/safety-best-practices#end-user-ids). + */ + "user": S.optionalWith(S.String, { nullable: true }) +}) {} + +export class CreateImageVariationRequestModelEnum extends S.Literal("dall-e-2") {} + +/** + * The format in which the generated images are returned. Must be one of `url` or `b64_json`. URLs are only valid for 60 minutes after the image has been generated. + */ +export class CreateImageVariationRequestResponseFormat extends S.Literal("url", "b64_json") {} + +/** + * The size of the generated images. Must be one of `256x256`, `512x512`, or `1024x1024`. + */ +export class CreateImageVariationRequestSize extends S.Literal("256x256", "512x512", "1024x1024") {} + +export class CreateImageVariationRequest extends S.Class("CreateImageVariationRequest")({ + /** + * The image to use as the basis for the variation(s). Must be a valid PNG file, less than 4MB, and square. + */ + "image": S.instanceOf(globalThis.Blob), + /** + * The model to use for image generation. Only `dall-e-2` is supported at this time. + */ + "model": S.optionalWith(S.Union(S.String, CreateImageVariationRequestModelEnum), { nullable: true }), + /** + * The number of images to generate. Must be between 1 and 10. + */ + "n": S.optionalWith(S.Int.pipe(S.greaterThanOrEqualTo(1), S.lessThanOrEqualTo(10)), { + nullable: true, + default: () => 1 as const + }), + /** + * The format in which the generated images are returned. Must be one of `url` or `b64_json`. URLs are only valid for 60 minutes after the image has been generated. + */ + "response_format": S.optionalWith(CreateImageVariationRequestResponseFormat, { + nullable: true, + default: () => "url" as const + }), + /** + * The size of the generated images. Must be one of `256x256`, `512x512`, or `1024x1024`. + */ + "size": S.optionalWith(CreateImageVariationRequestSize, { nullable: true, default: () => "1024x1024" as const }), + /** + * A unique identifier representing your end-user, which can help OpenAI to monitor and detect abuse. [Learn more](https://platform.openai.com/docs/guides/safety-best-practices#end-user-ids). + */ + "user": S.optionalWith(S.String, { nullable: true }) +}) {} + +export class ListModelsResponseObject extends S.Literal("list") {} + +/** + * The object type, which is always "model". + */ +export class ModelObject extends S.Literal("model") {} + +/** + * Describes an OpenAI model offering that can be used with the API. + */ +export class Model extends S.Class("Model")({ + /** + * The model identifier, which can be referenced in the API endpoints. + */ + "id": S.String, + /** + * The Unix timestamp (in seconds) when the model was created. + */ + "created": S.Int, + /** + * The object type, which is always "model". + */ + "object": ModelObject, + /** + * The organization that owns the model. + */ + "owned_by": S.String +}) {} + +export class ListModelsResponse extends S.Class("ListModelsResponse")({ + "object": ListModelsResponseObject, + "data": S.Array(Model) +}) {} + +export class DeleteModelResponse extends S.Class("DeleteModelResponse")({ + "id": S.String, + "deleted": S.Boolean, + "object": S.String +}) {} + +/** + * Always `image_url`. + */ +export class ModerationImageURLInputType extends S.Literal("image_url") {} + +/** + * An object describing an image to classify. + */ +export class ModerationImageURLInput extends S.Class("ModerationImageURLInput")({ + /** + * Always `image_url`. + */ + "type": ModerationImageURLInputType, + /** + * Contains either an image URL or a data URL for a base64 encoded image. + */ + "image_url": S.Struct({ + /** + * Either a URL of the image or the base64 encoded image data. + */ + "url": S.String + }) +}) {} + +/** + * Always `text`. + */ +export class ModerationTextInputType extends S.Literal("text") {} + +/** + * An object describing text to classify. + */ +export class ModerationTextInput extends S.Class("ModerationTextInput")({ + /** + * Always `text`. + */ + "type": ModerationTextInputType, + /** + * A string of text to classify. + */ + "text": S.String +}) {} + +export class CreateModerationRequestModelEnum extends S.Literal( + "omni-moderation-latest", + "omni-moderation-2024-09-26", + "text-moderation-latest", + "text-moderation-stable" +) {} + +export class CreateModerationRequest extends S.Class("CreateModerationRequest")({ + /** + * Input (or inputs) to classify. Can be a single string, an array of strings, or + * an array of multi-modal input objects similar to other models. + */ + "input": S.Union( + /** + * A string of text to classify for moderation. + */ + S.String, + /** + * An array of strings to classify for moderation. + */ + S.Array(S.String), + /** + * An array of multi-modal inputs to the moderation model. + */ + S.Array(S.Union(ModerationImageURLInput, ModerationTextInput)) + ), + /** + * The content moderation model you would like to use. Learn more in + * [the moderation guide](https://platform.openai.com/docs/guides/moderation), and learn about + * available models [here](https://platform.openai.com/docs/models#moderation). + */ + "model": S.optionalWith(S.Union(S.String, CreateModerationRequestModelEnum), { nullable: true }) +}) {} + +/** + * Represents if a given text input is potentially harmful. + */ +export class CreateModerationResponse extends S.Class("CreateModerationResponse")({ + /** + * The unique identifier for the moderation request. + */ + "id": S.String, + /** + * The model used to generate the moderation results. + */ + "model": S.String, + /** + * A list of moderation objects. + */ + "results": S.Array(S.Struct({ + /** + * Whether any of the below categories are flagged. + */ + "flagged": S.Boolean, + /** + * A list of the categories, and whether they are flagged or not. + */ + "categories": S.Struct({ + /** + * Content that expresses, incites, or promotes hate based on race, gender, ethnicity, religion, nationality, sexual orientation, disability status, or caste. Hateful content aimed at non-protected groups (e.g., chess players) is harassment. + */ + "hate": S.Boolean, + /** + * Hateful content that also includes violence or serious harm towards the targeted group based on race, gender, ethnicity, religion, nationality, sexual orientation, disability status, or caste. + */ + "hate/threatening": S.Boolean, + /** + * Content that expresses, incites, or promotes harassing language towards any target. + */ + "harassment": S.Boolean, + /** + * Harassment content that also includes violence or serious harm towards any target. + */ + "harassment/threatening": S.Boolean, + "illicit": S.NullOr(S.Boolean), + "illicit/violent": S.NullOr(S.Boolean), + /** + * Content that promotes, encourages, or depicts acts of self-harm, such as suicide, cutting, and eating disorders. + */ + "self-harm": S.Boolean, + /** + * Content where the speaker expresses that they are engaging or intend to engage in acts of self-harm, such as suicide, cutting, and eating disorders. + */ + "self-harm/intent": S.Boolean, + /** + * Content that encourages performing acts of self-harm, such as suicide, cutting, and eating disorders, or that gives instructions or advice on how to commit such acts. + */ + "self-harm/instructions": S.Boolean, + /** + * Content meant to arouse sexual excitement, such as the description of sexual activity, or that promotes sexual services (excluding sex education and wellness). + */ + "sexual": S.Boolean, + /** + * Sexual content that includes an individual who is under 18 years old. + */ + "sexual/minors": S.Boolean, + /** + * Content that depicts death, violence, or physical injury. + */ + "violence": S.Boolean, + /** + * Content that depicts death, violence, or physical injury in graphic detail. + */ + "violence/graphic": S.Boolean + }), + /** + * A list of the categories along with their scores as predicted by model. + */ + "category_scores": S.Struct({ + /** + * The score for the category 'hate'. + */ + "hate": S.Number, + /** + * The score for the category 'hate/threatening'. + */ + "hate/threatening": S.Number, + /** + * The score for the category 'harassment'. + */ + "harassment": S.Number, + /** + * The score for the category 'harassment/threatening'. + */ + "harassment/threatening": S.Number, + /** + * The score for the category 'illicit'. + */ + "illicit": S.Number, + /** + * The score for the category 'illicit/violent'. + */ + "illicit/violent": S.Number, + /** + * The score for the category 'self-harm'. + */ + "self-harm": S.Number, + /** + * The score for the category 'self-harm/intent'. + */ + "self-harm/intent": S.Number, + /** + * The score for the category 'self-harm/instructions'. + */ + "self-harm/instructions": S.Number, + /** + * The score for the category 'sexual'. + */ + "sexual": S.Number, + /** + * The score for the category 'sexual/minors'. + */ + "sexual/minors": S.Number, + /** + * The score for the category 'violence'. + */ + "violence": S.Number, + /** + * The score for the category 'violence/graphic'. + */ + "violence/graphic": S.Number + }), + /** + * A list of the categories along with the input type(s) that the score applies to. + */ + "category_applied_input_types": S.Struct({ + /** + * The applied input type(s) for the category 'hate'. + */ + "hate": S.Array(S.Literal("text")), + /** + * The applied input type(s) for the category 'hate/threatening'. + */ + "hate/threatening": S.Array(S.Literal("text")), + /** + * The applied input type(s) for the category 'harassment'. + */ + "harassment": S.Array(S.Literal("text")), + /** + * The applied input type(s) for the category 'harassment/threatening'. + */ + "harassment/threatening": S.Array(S.Literal("text")), + /** + * The applied input type(s) for the category 'illicit'. + */ + "illicit": S.Array(S.Literal("text")), + /** + * The applied input type(s) for the category 'illicit/violent'. + */ + "illicit/violent": S.Array(S.Literal("text")), + /** + * The applied input type(s) for the category 'self-harm'. + */ + "self-harm": S.Array(S.Literal("text", "image")), + /** + * The applied input type(s) for the category 'self-harm/intent'. + */ + "self-harm/intent": S.Array(S.Literal("text", "image")), + /** + * The applied input type(s) for the category 'self-harm/instructions'. + */ + "self-harm/instructions": S.Array(S.Literal("text", "image")), + /** + * The applied input type(s) for the category 'sexual'. + */ + "sexual": S.Array(S.Literal("text", "image")), + /** + * The applied input type(s) for the category 'sexual/minors'. + */ + "sexual/minors": S.Array(S.Literal("text")), + /** + * The applied input type(s) for the category 'violence'. + */ + "violence": S.Array(S.Literal("text", "image")), + /** + * The applied input type(s) for the category 'violence/graphic'. + */ + "violence/graphic": S.Array(S.Literal("text", "image")) + }) + })) +}) {} + +/** + * Order results by creation time, ascending or descending. + */ +export class AdminApiKeysListParamsOrder extends S.Literal("asc", "desc") {} + +export class AdminApiKeysListParams extends S.Struct({ + /** + * Return keys with IDs that come after this ID in the pagination order. + */ + "after": S.optionalWith(S.String, { nullable: true }), + /** + * Order results by creation time, ascending or descending. + */ + "order": S.optionalWith(AdminApiKeysListParamsOrder, { nullable: true, default: () => "asc" as const }), + /** + * Maximum number of keys to return. + */ + "limit": S.optionalWith(S.Int, { nullable: true, default: () => 20 as const }) +}) {} + +/** + * Represents an individual Admin API key in an org. + */ +export class AdminApiKey extends S.Class("AdminApiKey")({ + /** + * The object type, which is always `organization.admin_api_key` + */ + "object": S.String, + /** + * The identifier, which can be referenced in API endpoints + */ + "id": S.String, + /** + * The name of the API key + */ + "name": S.String, + /** + * The redacted value of the API key + */ + "redacted_value": S.String, + /** + * The value of the API key. Only shown on create. + */ + "value": S.optionalWith(S.String, { nullable: true }), + /** + * The Unix timestamp (in seconds) of when the API key was created + */ + "created_at": S.Int, + "last_used_at": S.NullOr(S.Int), + "owner": S.Struct({ + /** + * Always `user` + */ + "type": S.optionalWith(S.String, { nullable: true }), + /** + * The object type, which is always organization.user + */ + "object": S.optionalWith(S.String, { nullable: true }), + /** + * The identifier, which can be referenced in API endpoints + */ + "id": S.optionalWith(S.String, { nullable: true }), + /** + * The name of the user + */ + "name": S.optionalWith(S.String, { nullable: true }), + /** + * The Unix timestamp (in seconds) of when the user was created + */ + "created_at": S.optionalWith(S.Int, { nullable: true }), + /** + * Always `owner` + */ + "role": S.optionalWith(S.String, { nullable: true }) + }) +}) {} + +export class ApiKeyList extends S.Class("ApiKeyList")({ + "object": S.optionalWith(S.String, { nullable: true }), + "data": S.optionalWith(S.Array(AdminApiKey), { nullable: true }), + "has_more": S.optionalWith(S.Boolean, { nullable: true }), + "first_id": S.optionalWith(S.String, { nullable: true }), + "last_id": S.optionalWith(S.String, { nullable: true }) +}) {} + +export class AdminApiKeysCreateRequest extends S.Class("AdminApiKeysCreateRequest")({ + "name": S.String +}) {} + +export class AdminApiKeysDelete200 extends S.Struct({ + "id": S.optionalWith(S.String, { nullable: true }), + "object": S.optionalWith(S.String, { nullable: true }), + "deleted": S.optionalWith(S.Boolean, { nullable: true }) +}) {} + +/** + * The event type. + */ +export class AuditLogEventType extends S.Literal( + "api_key.created", + "api_key.updated", + "api_key.deleted", + "certificate.created", + "certificate.updated", + "certificate.deleted", + "certificates.activated", + "certificates.deactivated", + "checkpoint.permission.created", + "checkpoint.permission.deleted", + "external_key.registered", + "external_key.removed", + "group.created", + "group.updated", + "group.deleted", + "invite.sent", + "invite.accepted", + "invite.deleted", + "ip_allowlist.created", + "ip_allowlist.updated", + "ip_allowlist.deleted", + "ip_allowlist.config.activated", + "ip_allowlist.config.deactivated", + "login.succeeded", + "login.failed", + "logout.succeeded", + "logout.failed", + "organization.updated", + "project.created", + "project.updated", + "project.archived", + "project.deleted", + "rate_limit.updated", + "rate_limit.deleted", + "resource.deleted", + "role.created", + "role.updated", + "role.deleted", + "role.assignment.created", + "role.assignment.deleted", + "scim.enabled", + "scim.disabled", + "service_account.created", + "service_account.updated", + "service_account.deleted", + "user.added", + "user.updated", + "user.deleted" +) {} + +export class ListAuditLogsParams extends S.Struct({ + /** + * Return only events whose `effective_at` (Unix seconds) is greater than this value. + */ + "effective_at[gt]": S.optionalWith(S.Int, { nullable: true }), + /** + * Return only events whose `effective_at` (Unix seconds) is greater than or equal to this value. + */ + "effective_at[gte]": S.optionalWith(S.Int, { nullable: true }), + /** + * Return only events whose `effective_at` (Unix seconds) is less than this value. + */ + "effective_at[lt]": S.optionalWith(S.Int, { nullable: true }), + /** + * Return only events whose `effective_at` (Unix seconds) is less than or equal to this value. + */ + "effective_at[lte]": S.optionalWith(S.Int, { nullable: true }), + "project_ids[]": S.optionalWith(S.Array(S.String), { nullable: true }), + "event_types[]": S.optionalWith(S.Array(AuditLogEventType), { nullable: true }), + "actor_ids[]": S.optionalWith(S.Array(S.String), { nullable: true }), + "actor_emails[]": S.optionalWith(S.Array(S.String), { nullable: true }), + "resource_ids[]": S.optionalWith(S.Array(S.String), { nullable: true }), + "limit": S.optionalWith(S.Int, { nullable: true, default: () => 20 as const }), + "after": S.optionalWith(S.String, { nullable: true }), + "before": S.optionalWith(S.String, { nullable: true }) +}) {} + +export class ListAuditLogsResponseObject extends S.Literal("list") {} + +/** + * The type of actor. Is either `session` or `api_key`. + */ +export class AuditLogActorType extends S.Literal("session", "api_key") {} + +/** + * The user who performed the audit logged action. + */ +export class AuditLogActorUser extends S.Class("AuditLogActorUser")({ + /** + * The user id. + */ + "id": S.optionalWith(S.String, { nullable: true }), + /** + * The user email. + */ + "email": S.optionalWith(S.String, { nullable: true }) +}) {} + +/** + * The session in which the audit logged action was performed. + */ +export class AuditLogActorSession extends S.Class("AuditLogActorSession")({ + "user": S.optionalWith(AuditLogActorUser, { nullable: true }), + /** + * The IP address from which the action was performed. + */ + "ip_address": S.optionalWith(S.String, { nullable: true }) +}) {} + +/** + * The type of API key. Can be either `user` or `service_account`. + */ +export class AuditLogActorApiKeyType extends S.Literal("user", "service_account") {} + +/** + * The service account that performed the audit logged action. + */ +export class AuditLogActorServiceAccount extends S.Class("AuditLogActorServiceAccount")({ + /** + * The service account id. + */ + "id": S.optionalWith(S.String, { nullable: true }) +}) {} + +/** + * The API Key used to perform the audit logged action. + */ +export class AuditLogActorApiKey extends S.Class("AuditLogActorApiKey")({ + /** + * The tracking id of the API key. + */ + "id": S.optionalWith(S.String, { nullable: true }), + /** + * The type of API key. Can be either `user` or `service_account`. + */ + "type": S.optionalWith(AuditLogActorApiKeyType, { nullable: true }), + "user": S.optionalWith(AuditLogActorUser, { nullable: true }), + "service_account": S.optionalWith(AuditLogActorServiceAccount, { nullable: true }) +}) {} + +/** + * The actor who performed the audit logged action. + */ +export class AuditLogActor extends S.Class("AuditLogActor")({ + /** + * The type of actor. Is either `session` or `api_key`. + */ + "type": S.optionalWith(AuditLogActorType, { nullable: true }), + "session": S.optionalWith(AuditLogActorSession, { nullable: true }), + "api_key": S.optionalWith(AuditLogActorApiKey, { nullable: true }) +}) {} + +/** + * A log of a user action or configuration change within this organization. + */ +export class AuditLog extends S.Class("AuditLog")({ + /** + * The ID of this log. + */ + "id": S.String, + "type": AuditLogEventType, + /** + * The Unix timestamp (in seconds) of the event. + */ + "effective_at": S.Int, + /** + * The project that the action was scoped to. Absent for actions not scoped to projects. Note that any admin actions taken via Admin API keys are associated with the default project. + */ + "project": S.optionalWith( + S.Struct({ + /** + * The project ID. + */ + "id": S.optionalWith(S.String, { nullable: true }), + /** + * The project title. + */ + "name": S.optionalWith(S.String, { nullable: true }) + }), + { nullable: true } + ), + "actor": AuditLogActor, + /** + * The details for events with this `type`. + */ + "api_key.created": S.optionalWith( + S.Struct({ + /** + * The tracking ID of the API key. + */ + "id": S.optionalWith(S.String, { nullable: true }), + /** + * The payload used to create the API key. + */ + "data": S.optionalWith( + S.Struct({ + /** + * A list of scopes allowed for the API key, e.g. `["api.model.request"]` + */ + "scopes": S.optionalWith(S.Array(S.String), { nullable: true }) + }), + { nullable: true } + ) + }), + { nullable: true } + ), + /** + * The details for events with this `type`. + */ + "api_key.updated": S.optionalWith( + S.Struct({ + /** + * The tracking ID of the API key. + */ + "id": S.optionalWith(S.String, { nullable: true }), + /** + * The payload used to update the API key. + */ + "changes_requested": S.optionalWith( + S.Struct({ + /** + * A list of scopes allowed for the API key, e.g. `["api.model.request"]` + */ + "scopes": S.optionalWith(S.Array(S.String), { nullable: true }) + }), + { nullable: true } + ) + }), + { nullable: true } + ), + /** + * The details for events with this `type`. + */ + "api_key.deleted": S.optionalWith( + S.Struct({ + /** + * The tracking ID of the API key. + */ + "id": S.optionalWith(S.String, { nullable: true }) + }), + { nullable: true } + ), + /** + * The project and fine-tuned model checkpoint that the checkpoint permission was created for. + */ + "checkpoint.permission.created": S.optionalWith( + S.Struct({ + /** + * The ID of the checkpoint permission. + */ + "id": S.optionalWith(S.String, { nullable: true }), + /** + * The payload used to create the checkpoint permission. + */ + "data": S.optionalWith( + S.Struct({ + /** + * The ID of the project that the checkpoint permission was created for. + */ + "project_id": S.optionalWith(S.String, { nullable: true }), + /** + * The ID of the fine-tuned model checkpoint. + */ + "fine_tuned_model_checkpoint": S.optionalWith(S.String, { nullable: true }) + }), + { nullable: true } + ) + }), + { nullable: true } + ), + /** + * The details for events with this `type`. + */ + "checkpoint.permission.deleted": S.optionalWith( + S.Struct({ + /** + * The ID of the checkpoint permission. + */ + "id": S.optionalWith(S.String, { nullable: true }) + }), + { nullable: true } + ), + /** + * The details for events with this `type`. + */ + "external_key.registered": S.optionalWith( + S.Struct({ + /** + * The ID of the external key configuration. + */ + "id": S.optionalWith(S.String, { nullable: true }), + /** + * The configuration for the external key. + */ + "data": S.optionalWith(S.Record({ key: S.String, value: S.Unknown }), { nullable: true }) + }), + { nullable: true } + ), + /** + * The details for events with this `type`. + */ + "external_key.removed": S.optionalWith( + S.Struct({ + /** + * The ID of the external key configuration. + */ + "id": S.optionalWith(S.String, { nullable: true }) + }), + { nullable: true } + ), + /** + * The details for events with this `type`. + */ + "group.created": S.optionalWith( + S.Struct({ + /** + * The ID of the group. + */ + "id": S.optionalWith(S.String, { nullable: true }), + /** + * Information about the created group. + */ + "data": S.optionalWith( + S.Struct({ + /** + * The group name. + */ + "group_name": S.optionalWith(S.String, { nullable: true }) + }), + { nullable: true } + ) + }), + { nullable: true } + ), + /** + * The details for events with this `type`. + */ + "group.updated": S.optionalWith( + S.Struct({ + /** + * The ID of the group. + */ + "id": S.optionalWith(S.String, { nullable: true }), + /** + * The payload used to update the group. + */ + "changes_requested": S.optionalWith( + S.Struct({ + /** + * The updated group name. + */ + "group_name": S.optionalWith(S.String, { nullable: true }) + }), + { nullable: true } + ) + }), + { nullable: true } + ), + /** + * The details for events with this `type`. + */ + "group.deleted": S.optionalWith( + S.Struct({ + /** + * The ID of the group. + */ + "id": S.optionalWith(S.String, { nullable: true }) + }), + { nullable: true } + ), + /** + * The details for events with this `type`. + */ + "scim.enabled": S.optionalWith( + S.Struct({ + /** + * The ID of the SCIM was enabled for. + */ + "id": S.optionalWith(S.String, { nullable: true }) + }), + { nullable: true } + ), + /** + * The details for events with this `type`. + */ + "scim.disabled": S.optionalWith( + S.Struct({ + /** + * The ID of the SCIM was disabled for. + */ + "id": S.optionalWith(S.String, { nullable: true }) + }), + { nullable: true } + ), + /** + * The details for events with this `type`. + */ + "invite.sent": S.optionalWith( + S.Struct({ + /** + * The ID of the invite. + */ + "id": S.optionalWith(S.String, { nullable: true }), + /** + * The payload used to create the invite. + */ + "data": S.optionalWith( + S.Struct({ + /** + * The email invited to the organization. + */ + "email": S.optionalWith(S.String, { nullable: true }), + /** + * The role the email was invited to be. Is either `owner` or `member`. + */ + "role": S.optionalWith(S.String, { nullable: true }) + }), + { nullable: true } + ) + }), + { nullable: true } + ), + /** + * The details for events with this `type`. + */ + "invite.accepted": S.optionalWith( + S.Struct({ + /** + * The ID of the invite. + */ + "id": S.optionalWith(S.String, { nullable: true }) + }), + { nullable: true } + ), + /** + * The details for events with this `type`. + */ + "invite.deleted": S.optionalWith( + S.Struct({ + /** + * The ID of the invite. + */ + "id": S.optionalWith(S.String, { nullable: true }) + }), + { nullable: true } + ), + /** + * The details for events with this `type`. + */ + "ip_allowlist.created": S.optionalWith( + S.Struct({ + /** + * The ID of the IP allowlist configuration. + */ + "id": S.optionalWith(S.String, { nullable: true }), + /** + * The name of the IP allowlist configuration. + */ + "name": S.optionalWith(S.String, { nullable: true }), + /** + * The IP addresses or CIDR ranges included in the configuration. + */ + "allowed_ips": S.optionalWith(S.Array(S.String), { nullable: true }) + }), + { nullable: true } + ), + /** + * The details for events with this `type`. + */ + "ip_allowlist.updated": S.optionalWith( + S.Struct({ + /** + * The ID of the IP allowlist configuration. + */ + "id": S.optionalWith(S.String, { nullable: true }), + /** + * The updated set of IP addresses or CIDR ranges in the configuration. + */ + "allowed_ips": S.optionalWith(S.Array(S.String), { nullable: true }) + }), + { nullable: true } + ), + /** + * The details for events with this `type`. + */ + "ip_allowlist.deleted": S.optionalWith( + S.Struct({ + /** + * The ID of the IP allowlist configuration. + */ + "id": S.optionalWith(S.String, { nullable: true }), + /** + * The name of the IP allowlist configuration. + */ + "name": S.optionalWith(S.String, { nullable: true }), + /** + * The IP addresses or CIDR ranges that were in the configuration. + */ + "allowed_ips": S.optionalWith(S.Array(S.String), { nullable: true }) + }), + { nullable: true } + ), + /** + * The details for events with this `type`. + */ + "ip_allowlist.config.activated": S.optionalWith( + S.Struct({ + /** + * The configurations that were activated. + */ + "configs": S.optionalWith( + S.Array(S.Struct({ + /** + * The ID of the IP allowlist configuration. + */ + "id": S.optionalWith(S.String, { nullable: true }), + /** + * The name of the IP allowlist configuration. + */ + "name": S.optionalWith(S.String, { nullable: true }) + })), + { nullable: true } + ) + }), + { nullable: true } + ), + /** + * The details for events with this `type`. + */ + "ip_allowlist.config.deactivated": S.optionalWith( + S.Struct({ + /** + * The configurations that were deactivated. + */ + "configs": S.optionalWith( + S.Array(S.Struct({ + /** + * The ID of the IP allowlist configuration. + */ + "id": S.optionalWith(S.String, { nullable: true }), + /** + * The name of the IP allowlist configuration. + */ + "name": S.optionalWith(S.String, { nullable: true }) + })), + { nullable: true } + ) + }), + { nullable: true } + ), + /** + * This event has no additional fields beyond the standard audit log attributes. + */ + "login.succeeded": S.optionalWith(S.Record({ key: S.String, value: S.Unknown }), { nullable: true }), + /** + * The details for events with this `type`. + */ + "login.failed": S.optionalWith( + S.Struct({ + /** + * The error code of the failure. + */ + "error_code": S.optionalWith(S.String, { nullable: true }), + /** + * The error message of the failure. + */ + "error_message": S.optionalWith(S.String, { nullable: true }) + }), + { nullable: true } + ), + /** + * This event has no additional fields beyond the standard audit log attributes. + */ + "logout.succeeded": S.optionalWith(S.Record({ key: S.String, value: S.Unknown }), { nullable: true }), + /** + * The details for events with this `type`. + */ + "logout.failed": S.optionalWith( + S.Struct({ + /** + * The error code of the failure. + */ + "error_code": S.optionalWith(S.String, { nullable: true }), + /** + * The error message of the failure. + */ + "error_message": S.optionalWith(S.String, { nullable: true }) + }), + { nullable: true } + ), + /** + * The details for events with this `type`. + */ + "organization.updated": S.optionalWith( + S.Struct({ + /** + * The organization ID. + */ + "id": S.optionalWith(S.String, { nullable: true }), + /** + * The payload used to update the organization settings. + */ + "changes_requested": S.optionalWith( + S.Struct({ + /** + * The organization title. + */ + "title": S.optionalWith(S.String, { nullable: true }), + /** + * The organization description. + */ + "description": S.optionalWith(S.String, { nullable: true }), + /** + * The organization name. + */ + "name": S.optionalWith(S.String, { nullable: true }), + /** + * Visibility of the threads page which shows messages created with the Assistants API and Playground. One of `ANY_ROLE`, `OWNERS`, or `NONE`. + */ + "threads_ui_visibility": S.optionalWith(S.String, { nullable: true }), + /** + * Visibility of the usage dashboard which shows activity and costs for your organization. One of `ANY_ROLE` or `OWNERS`. + */ + "usage_dashboard_visibility": S.optionalWith(S.String, { nullable: true }), + /** + * How your organization logs data from supported API calls. One of `disabled`, `enabled_per_call`, `enabled_for_all_projects`, or `enabled_for_selected_projects` + */ + "api_call_logging": S.optionalWith(S.String, { nullable: true }), + /** + * The list of project ids if api_call_logging is set to `enabled_for_selected_projects` + */ + "api_call_logging_project_ids": S.optionalWith(S.String, { nullable: true }) + }), + { nullable: true } + ) + }), + { nullable: true } + ), + /** + * The details for events with this `type`. + */ + "project.created": S.optionalWith( + S.Struct({ + /** + * The project ID. + */ + "id": S.optionalWith(S.String, { nullable: true }), + /** + * The payload used to create the project. + */ + "data": S.optionalWith( + S.Struct({ + /** + * The project name. + */ + "name": S.optionalWith(S.String, { nullable: true }), + /** + * The title of the project as seen on the dashboard. + */ + "title": S.optionalWith(S.String, { nullable: true }) + }), + { nullable: true } + ) + }), + { nullable: true } + ), + /** + * The details for events with this `type`. + */ + "project.updated": S.optionalWith( + S.Struct({ + /** + * The project ID. + */ + "id": S.optionalWith(S.String, { nullable: true }), + /** + * The payload used to update the project. + */ + "changes_requested": S.optionalWith( + S.Struct({ + /** + * The title of the project as seen on the dashboard. + */ + "title": S.optionalWith(S.String, { nullable: true }) + }), + { nullable: true } + ) + }), + { nullable: true } + ), + /** + * The details for events with this `type`. + */ + "project.archived": S.optionalWith( + S.Struct({ + /** + * The project ID. + */ + "id": S.optionalWith(S.String, { nullable: true }) + }), + { nullable: true } + ), + /** + * The details for events with this `type`. + */ + "project.deleted": S.optionalWith( + S.Struct({ + /** + * The project ID. + */ + "id": S.optionalWith(S.String, { nullable: true }) + }), + { nullable: true } + ), + /** + * The details for events with this `type`. + */ + "rate_limit.updated": S.optionalWith( + S.Struct({ + /** + * The rate limit ID + */ + "id": S.optionalWith(S.String, { nullable: true }), + /** + * The payload used to update the rate limits. + */ + "changes_requested": S.optionalWith( + S.Struct({ + /** + * The maximum requests per minute. + */ + "max_requests_per_1_minute": S.optionalWith(S.Int, { nullable: true }), + /** + * The maximum tokens per minute. + */ + "max_tokens_per_1_minute": S.optionalWith(S.Int, { nullable: true }), + /** + * The maximum images per minute. Only relevant for certain models. + */ + "max_images_per_1_minute": S.optionalWith(S.Int, { nullable: true }), + /** + * The maximum audio megabytes per minute. Only relevant for certain models. + */ + "max_audio_megabytes_per_1_minute": S.optionalWith(S.Int, { nullable: true }), + /** + * The maximum requests per day. Only relevant for certain models. + */ + "max_requests_per_1_day": S.optionalWith(S.Int, { nullable: true }), + /** + * The maximum batch input tokens per day. Only relevant for certain models. + */ + "batch_1_day_max_input_tokens": S.optionalWith(S.Int, { nullable: true }) + }), + { nullable: true } + ) + }), + { nullable: true } + ), + /** + * The details for events with this `type`. + */ + "rate_limit.deleted": S.optionalWith( + S.Struct({ + /** + * The rate limit ID + */ + "id": S.optionalWith(S.String, { nullable: true }) + }), + { nullable: true } + ), + /** + * The details for events with this `type`. + */ + "role.created": S.optionalWith( + S.Struct({ + /** + * The role ID. + */ + "id": S.optionalWith(S.String, { nullable: true }), + /** + * The name of the role. + */ + "role_name": S.optionalWith(S.String, { nullable: true }), + /** + * The permissions granted by the role. + */ + "permissions": S.optionalWith(S.Array(S.String), { nullable: true }), + /** + * The type of resource the role belongs to. + */ + "resource_type": S.optionalWith(S.String, { nullable: true }), + /** + * The resource the role is scoped to. + */ + "resource_id": S.optionalWith(S.String, { nullable: true }) + }), + { nullable: true } + ), + /** + * The details for events with this `type`. + */ + "role.updated": S.optionalWith( + S.Struct({ + /** + * The role ID. + */ + "id": S.optionalWith(S.String, { nullable: true }), + /** + * The payload used to update the role. + */ + "changes_requested": S.optionalWith( + S.Struct({ + /** + * The updated role name, when provided. + */ + "role_name": S.optionalWith(S.String, { nullable: true }), + /** + * The resource the role is scoped to. + */ + "resource_id": S.optionalWith(S.String, { nullable: true }), + /** + * The type of resource the role belongs to. + */ + "resource_type": S.optionalWith(S.String, { nullable: true }), + /** + * The permissions added to the role. + */ + "permissions_added": S.optionalWith(S.Array(S.String), { nullable: true }), + /** + * The permissions removed from the role. + */ + "permissions_removed": S.optionalWith(S.Array(S.String), { nullable: true }), + /** + * The updated role description, when provided. + */ + "description": S.optionalWith(S.String, { nullable: true }), + /** + * Additional metadata stored on the role. + */ + "metadata": S.optionalWith(S.Record({ key: S.String, value: S.Unknown }), { nullable: true }) + }), + { nullable: true } + ) + }), + { nullable: true } + ), + /** + * The details for events with this `type`. + */ + "role.deleted": S.optionalWith( + S.Struct({ + /** + * The role ID. + */ + "id": S.optionalWith(S.String, { nullable: true }) + }), + { nullable: true } + ), + /** + * The details for events with this `type`. + */ + "role.assignment.created": S.optionalWith( + S.Struct({ + /** + * The identifier of the role assignment. + */ + "id": S.optionalWith(S.String, { nullable: true }), + /** + * The principal (user or group) that received the role. + */ + "principal_id": S.optionalWith(S.String, { nullable: true }), + /** + * The type of principal (user or group) that received the role. + */ + "principal_type": S.optionalWith(S.String, { nullable: true }), + /** + * The resource the role assignment is scoped to. + */ + "resource_id": S.optionalWith(S.String, { nullable: true }), + /** + * The type of resource the role assignment is scoped to. + */ + "resource_type": S.optionalWith(S.String, { nullable: true }) + }), + { nullable: true } + ), + /** + * The details for events with this `type`. + */ + "role.assignment.deleted": S.optionalWith( + S.Struct({ + /** + * The identifier of the role assignment. + */ + "id": S.optionalWith(S.String, { nullable: true }), + /** + * The principal (user or group) that had the role removed. + */ + "principal_id": S.optionalWith(S.String, { nullable: true }), + /** + * The type of principal (user or group) that had the role removed. + */ + "principal_type": S.optionalWith(S.String, { nullable: true }), + /** + * The resource the role assignment was scoped to. + */ + "resource_id": S.optionalWith(S.String, { nullable: true }), + /** + * The type of resource the role assignment was scoped to. + */ + "resource_type": S.optionalWith(S.String, { nullable: true }) + }), + { nullable: true } + ), + /** + * The details for events with this `type`. + */ + "service_account.created": S.optionalWith( + S.Struct({ + /** + * The service account ID. + */ + "id": S.optionalWith(S.String, { nullable: true }), + /** + * The payload used to create the service account. + */ + "data": S.optionalWith( + S.Struct({ + /** + * The role of the service account. Is either `owner` or `member`. + */ + "role": S.optionalWith(S.String, { nullable: true }) + }), + { nullable: true } + ) + }), + { nullable: true } + ), + /** + * The details for events with this `type`. + */ + "service_account.updated": S.optionalWith( + S.Struct({ + /** + * The service account ID. + */ + "id": S.optionalWith(S.String, { nullable: true }), + /** + * The payload used to updated the service account. + */ + "changes_requested": S.optionalWith( + S.Struct({ + /** + * The role of the service account. Is either `owner` or `member`. + */ + "role": S.optionalWith(S.String, { nullable: true }) + }), + { nullable: true } + ) + }), + { nullable: true } + ), + /** + * The details for events with this `type`. + */ + "service_account.deleted": S.optionalWith( + S.Struct({ + /** + * The service account ID. + */ + "id": S.optionalWith(S.String, { nullable: true }) + }), + { nullable: true } + ), + /** + * The details for events with this `type`. + */ + "user.added": S.optionalWith( + S.Struct({ + /** + * The user ID. + */ + "id": S.optionalWith(S.String, { nullable: true }), + /** + * The payload used to add the user to the project. + */ + "data": S.optionalWith( + S.Struct({ + /** + * The role of the user. Is either `owner` or `member`. + */ + "role": S.optionalWith(S.String, { nullable: true }) + }), + { nullable: true } + ) + }), + { nullable: true } + ), + /** + * The details for events with this `type`. + */ + "user.updated": S.optionalWith( + S.Struct({ + /** + * The project ID. + */ + "id": S.optionalWith(S.String, { nullable: true }), + /** + * The payload used to update the user. + */ + "changes_requested": S.optionalWith( + S.Struct({ + /** + * The role of the user. Is either `owner` or `member`. + */ + "role": S.optionalWith(S.String, { nullable: true }) + }), + { nullable: true } + ) + }), + { nullable: true } + ), + /** + * The details for events with this `type`. + */ + "user.deleted": S.optionalWith( + S.Struct({ + /** + * The user ID. + */ + "id": S.optionalWith(S.String, { nullable: true }) + }), + { nullable: true } + ), + /** + * The details for events with this `type`. + */ + "certificate.created": S.optionalWith( + S.Struct({ + /** + * The certificate ID. + */ + "id": S.optionalWith(S.String, { nullable: true }), + /** + * The name of the certificate. + */ + "name": S.optionalWith(S.String, { nullable: true }) + }), + { nullable: true } + ), + /** + * The details for events with this `type`. + */ + "certificate.updated": S.optionalWith( + S.Struct({ + /** + * The certificate ID. + */ + "id": S.optionalWith(S.String, { nullable: true }), + /** + * The name of the certificate. + */ + "name": S.optionalWith(S.String, { nullable: true }) + }), + { nullable: true } + ), + /** + * The details for events with this `type`. + */ + "certificate.deleted": S.optionalWith( + S.Struct({ + /** + * The certificate ID. + */ + "id": S.optionalWith(S.String, { nullable: true }), + /** + * The name of the certificate. + */ + "name": S.optionalWith(S.String, { nullable: true }), + /** + * The certificate content in PEM format. + */ + "certificate": S.optionalWith(S.String, { nullable: true }) + }), + { nullable: true } + ), + /** + * The details for events with this `type`. + */ + "certificates.activated": S.optionalWith( + S.Struct({ + "certificates": S.optionalWith( + S.Array(S.Struct({ + /** + * The certificate ID. + */ + "id": S.optionalWith(S.String, { nullable: true }), + /** + * The name of the certificate. + */ + "name": S.optionalWith(S.String, { nullable: true }) + })), + { nullable: true } + ) + }), + { nullable: true } + ), + /** + * The details for events with this `type`. + */ + "certificates.deactivated": S.optionalWith( + S.Struct({ + "certificates": S.optionalWith( + S.Array(S.Struct({ + /** + * The certificate ID. + */ + "id": S.optionalWith(S.String, { nullable: true }), + /** + * The name of the certificate. + */ + "name": S.optionalWith(S.String, { nullable: true }) + })), + { nullable: true } + ) + }), + { nullable: true } + ) +}) {} + +export class ListAuditLogsResponse extends S.Class("ListAuditLogsResponse")({ + "object": ListAuditLogsResponseObject, + "data": S.Array(AuditLog), + "first_id": S.String, + "last_id": S.String, + "has_more": S.Boolean +}) {} + +export class ListOrganizationCertificatesParamsOrder extends S.Literal("asc", "desc") {} + +export class ListOrganizationCertificatesParams extends S.Struct({ + "limit": S.optionalWith(S.Int, { nullable: true, default: () => 20 as const }), + "after": S.optionalWith(S.String, { nullable: true }), + "order": S.optionalWith(ListOrganizationCertificatesParamsOrder, { nullable: true, default: () => "desc" as const }) +}) {} + +/** + * The object type. + * + * - If creating, updating, or getting a specific certificate, the object type is `certificate`. + * - If listing, activating, or deactivating certificates for the organization, the object type is `organization.certificate`. + * - If listing, activating, or deactivating certificates for a project, the object type is `organization.project.certificate`. + */ +export class CertificateObject + extends S.Literal("certificate", "organization.certificate", "organization.project.certificate") +{} + +/** + * Represents an individual `certificate` uploaded to the organization. + */ +export class Certificate extends S.Class("Certificate")({ + /** + * The object type. + * + * - If creating, updating, or getting a specific certificate, the object type is `certificate`. + * - If listing, activating, or deactivating certificates for the organization, the object type is `organization.certificate`. + * - If listing, activating, or deactivating certificates for a project, the object type is `organization.project.certificate`. + */ + "object": CertificateObject, + /** + * The identifier, which can be referenced in API endpoints + */ + "id": S.String, + /** + * The name of the certificate. + */ + "name": S.String, + /** + * The Unix timestamp (in seconds) of when the certificate was uploaded. + */ + "created_at": S.Int, + "certificate_details": S.Struct({ + /** + * The Unix timestamp (in seconds) of when the certificate becomes valid. + */ + "valid_at": S.optionalWith(S.Int, { nullable: true }), + /** + * The Unix timestamp (in seconds) of when the certificate expires. + */ + "expires_at": S.optionalWith(S.Int, { nullable: true }), + /** + * The content of the certificate in PEM format. + */ + "content": S.optionalWith(S.String, { nullable: true }) + }), + /** + * Whether the certificate is currently active at the specified scope. Not returned when getting details for a specific certificate. + */ + "active": S.optionalWith(S.Boolean, { nullable: true }) +}) {} + +export class ListCertificatesResponseObject extends S.Literal("list") {} + +export class ListCertificatesResponse extends S.Class("ListCertificatesResponse")({ + "data": S.Array(Certificate), + "first_id": S.optionalWith(S.String, { nullable: true }), + "last_id": S.optionalWith(S.String, { nullable: true }), + "has_more": S.Boolean, + "object": ListCertificatesResponseObject +}) {} + +export class UploadCertificateRequest extends S.Class("UploadCertificateRequest")({ + /** + * An optional name for the certificate + */ + "name": S.optionalWith(S.String, { nullable: true }), + /** + * The certificate content in PEM format + */ + "content": S.String +}) {} + +export class ToggleCertificatesRequest extends S.Class("ToggleCertificatesRequest")({ + "certificate_ids": S.NonEmptyArray(S.String).pipe(S.minItems(1), S.maxItems(10)) +}) {} + +export class GetCertificateParams extends S.Struct({ + "include": S.optionalWith(S.Array(S.Literal("content")), { nullable: true }) +}) {} + +export class ModifyCertificateRequest extends S.Class("ModifyCertificateRequest")({ + /** + * The updated name for the certificate + */ + "name": S.String +}) {} + +export class DeleteCertificateResponse extends S.Class("DeleteCertificateResponse")({ + /** + * The object type, must be `certificate.deleted`. + */ + "object": S.Literal("certificate.deleted"), + /** + * The ID of the certificate that was deleted. + */ + "id": S.String +}) {} + +export class UsageCostsParamsBucketWidth extends S.Literal("1d") {} + +export class UsageCostsParams extends S.Struct({ + "start_time": S.Int, + "end_time": S.optionalWith(S.Int, { nullable: true }), + "bucket_width": S.optionalWith(UsageCostsParamsBucketWidth, { nullable: true, default: () => "1d" as const }), + "project_ids": S.optionalWith(S.Array(S.String), { nullable: true }), + "group_by": S.optionalWith(S.Array(S.Literal("project_id", "line_item")), { nullable: true }), + "limit": S.optionalWith(S.Int, { nullable: true, default: () => 7 as const }), + "page": S.optionalWith(S.String, { nullable: true }) +}) {} + +export class UsageResponseObject extends S.Literal("page") {} + +export class UsageTimeBucketObject extends S.Literal("bucket") {} + +export class UsageCompletionsResultObject extends S.Literal("organization.usage.completions.result") {} + +/** + * The aggregated completions usage details of the specific time bucket. + */ +export class UsageCompletionsResult extends S.Class("UsageCompletionsResult")({ + "object": UsageCompletionsResultObject, + /** + * The aggregated number of text input tokens used, including cached tokens. For customers subscribe to scale tier, this includes scale tier tokens. + */ + "input_tokens": S.Int, + /** + * The aggregated number of text input tokens that has been cached from previous requests. For customers subscribe to scale tier, this includes scale tier tokens. + */ + "input_cached_tokens": S.optionalWith(S.Int, { nullable: true }), + /** + * The aggregated number of text output tokens used. For customers subscribe to scale tier, this includes scale tier tokens. + */ + "output_tokens": S.Int, + /** + * The aggregated number of audio input tokens used, including cached tokens. + */ + "input_audio_tokens": S.optionalWith(S.Int, { nullable: true }), + /** + * The aggregated number of audio output tokens used. + */ + "output_audio_tokens": S.optionalWith(S.Int, { nullable: true }), + /** + * The count of requests made to the model. + */ + "num_model_requests": S.Int, + "project_id": S.optionalWith(S.String, { nullable: true }), + "user_id": S.optionalWith(S.String, { nullable: true }), + "api_key_id": S.optionalWith(S.String, { nullable: true }), + "model": S.optionalWith(S.String, { nullable: true }), + "batch": S.optionalWith(S.Boolean, { nullable: true }), + "service_tier": S.optionalWith(S.String, { nullable: true }) +}) {} + +export class UsageEmbeddingsResultObject extends S.Literal("organization.usage.embeddings.result") {} + +/** + * The aggregated embeddings usage details of the specific time bucket. + */ +export class UsageEmbeddingsResult extends S.Class("UsageEmbeddingsResult")({ + "object": UsageEmbeddingsResultObject, + /** + * The aggregated number of input tokens used. + */ + "input_tokens": S.Int, + /** + * The count of requests made to the model. + */ + "num_model_requests": S.Int, + "project_id": S.optionalWith(S.String, { nullable: true }), + "user_id": S.optionalWith(S.String, { nullable: true }), + "api_key_id": S.optionalWith(S.String, { nullable: true }), + "model": S.optionalWith(S.String, { nullable: true }) +}) {} + +export class UsageModerationsResultObject extends S.Literal("organization.usage.moderations.result") {} + +/** + * The aggregated moderations usage details of the specific time bucket. + */ +export class UsageModerationsResult extends S.Class("UsageModerationsResult")({ + "object": UsageModerationsResultObject, + /** + * The aggregated number of input tokens used. + */ + "input_tokens": S.Int, + /** + * The count of requests made to the model. + */ + "num_model_requests": S.Int, + "project_id": S.optionalWith(S.String, { nullable: true }), + "user_id": S.optionalWith(S.String, { nullable: true }), + "api_key_id": S.optionalWith(S.String, { nullable: true }), + "model": S.optionalWith(S.String, { nullable: true }) +}) {} + +export class UsageImagesResultObject extends S.Literal("organization.usage.images.result") {} + +/** + * The aggregated images usage details of the specific time bucket. + */ +export class UsageImagesResult extends S.Class("UsageImagesResult")({ + "object": UsageImagesResultObject, + /** + * The number of images processed. + */ + "images": S.Int, + /** + * The count of requests made to the model. + */ + "num_model_requests": S.Int, + "source": S.optionalWith(S.String, { nullable: true }), + "size": S.optionalWith(S.String, { nullable: true }), + "project_id": S.optionalWith(S.String, { nullable: true }), + "user_id": S.optionalWith(S.String, { nullable: true }), + "api_key_id": S.optionalWith(S.String, { nullable: true }), + "model": S.optionalWith(S.String, { nullable: true }) +}) {} + +export class UsageAudioSpeechesResultObject extends S.Literal("organization.usage.audio_speeches.result") {} + +/** + * The aggregated audio speeches usage details of the specific time bucket. + */ +export class UsageAudioSpeechesResult extends S.Class("UsageAudioSpeechesResult")({ + "object": UsageAudioSpeechesResultObject, + /** + * The number of characters processed. + */ + "characters": S.Int, + /** + * The count of requests made to the model. + */ + "num_model_requests": S.Int, + "project_id": S.optionalWith(S.String, { nullable: true }), + "user_id": S.optionalWith(S.String, { nullable: true }), + "api_key_id": S.optionalWith(S.String, { nullable: true }), + "model": S.optionalWith(S.String, { nullable: true }) +}) {} + +export class UsageAudioTranscriptionsResultObject extends S.Literal("organization.usage.audio_transcriptions.result") {} + +/** + * The aggregated audio transcriptions usage details of the specific time bucket. + */ +export class UsageAudioTranscriptionsResult + extends S.Class("UsageAudioTranscriptionsResult")({ + "object": UsageAudioTranscriptionsResultObject, + /** + * The number of seconds processed. + */ + "seconds": S.Int, + /** + * The count of requests made to the model. + */ + "num_model_requests": S.Int, + "project_id": S.optionalWith(S.String, { nullable: true }), + "user_id": S.optionalWith(S.String, { nullable: true }), + "api_key_id": S.optionalWith(S.String, { nullable: true }), + "model": S.optionalWith(S.String, { nullable: true }) + }) +{} + +export class UsageVectorStoresResultObject extends S.Literal("organization.usage.vector_stores.result") {} + +/** + * The aggregated vector stores usage details of the specific time bucket. + */ +export class UsageVectorStoresResult extends S.Class("UsageVectorStoresResult")({ + "object": UsageVectorStoresResultObject, + /** + * The vector stores usage in bytes. + */ + "usage_bytes": S.Int, + "project_id": S.optionalWith(S.String, { nullable: true }) +}) {} + +export class UsageCodeInterpreterSessionsResultObject + extends S.Literal("organization.usage.code_interpreter_sessions.result") +{} + +/** + * The aggregated code interpreter sessions usage details of the specific time bucket. + */ +export class UsageCodeInterpreterSessionsResult + extends S.Class("UsageCodeInterpreterSessionsResult")({ + "object": UsageCodeInterpreterSessionsResultObject, + /** + * The number of code interpreter sessions. + */ + "num_sessions": S.optionalWith(S.Int, { nullable: true }), + "project_id": S.optionalWith(S.String, { nullable: true }) + }) +{} + +export class CostsResultObject extends S.Literal("organization.costs.result") {} + +/** + * The aggregated costs details of the specific time bucket. + */ +export class CostsResult extends S.Class("CostsResult")({ + "object": CostsResultObject, + /** + * The monetary value in its associated currency. + */ + "amount": S.optionalWith( + S.Struct({ + /** + * The numeric value of the cost. + */ + "value": S.optionalWith(S.Number, { nullable: true }), + /** + * Lowercase ISO-4217 currency e.g. "usd" + */ + "currency": S.optionalWith(S.String, { nullable: true }) + }), + { nullable: true } + ), + "line_item": S.optionalWith(S.String, { nullable: true }), + "project_id": S.optionalWith(S.String, { nullable: true }) +}) {} + +export class UsageTimeBucket extends S.Class("UsageTimeBucket")({ + "object": UsageTimeBucketObject, + "start_time": S.Int, + "end_time": S.Int, + "result": S.Array( + S.Union( + UsageCompletionsResult, + UsageEmbeddingsResult, + UsageModerationsResult, + UsageImagesResult, + UsageAudioSpeechesResult, + UsageAudioTranscriptionsResult, + UsageVectorStoresResult, + UsageCodeInterpreterSessionsResult, + CostsResult + ) + ) +}) {} + +export class UsageResponse extends S.Class("UsageResponse")({ + "object": UsageResponseObject, + "data": S.Array(UsageTimeBucket), + "has_more": S.Boolean, + "next_page": S.String +}) {} + +export class ListGroupsParamsOrder extends S.Literal("asc", "desc") {} + +export class ListGroupsParams extends S.Struct({ + "limit": S.optionalWith(S.Int.pipe(S.greaterThanOrEqualTo(0), S.lessThanOrEqualTo(1000)), { + nullable: true, + default: () => 100 as const + }), + "after": S.optionalWith(S.String, { nullable: true }), + "order": S.optionalWith(ListGroupsParamsOrder, { nullable: true, default: () => "asc" as const }) +}) {} + +/** + * Always `list`. + */ +export class GroupListResourceObject extends S.Literal("list") {} + +/** + * Details about an organization group. + */ +export class GroupResponse extends S.Class("GroupResponse")({ + /** + * Identifier for the group. + */ + "id": S.String, + /** + * Display name of the group. + */ + "name": S.String, + /** + * Unix timestamp (in seconds) when the group was created. + */ + "created_at": S.Int, + /** + * Whether the group is managed through SCIM and controlled by your identity provider. + */ + "is_scim_managed": S.Boolean +}) {} + +/** + * Paginated list of organization groups. + */ +export class GroupListResource extends S.Class("GroupListResource")({ + /** + * Always `list`. + */ + "object": GroupListResourceObject, + /** + * Groups returned in the current page. + */ + "data": S.Array(GroupResponse), + /** + * Whether additional groups are available when paginating. + */ + "has_more": S.Boolean, + /** + * Cursor to fetch the next page of results, or `null` if there are no more results. + */ + "next": S.NullOr(S.String) +}) {} + +/** + * Request payload for creating a new group in the organization. + */ +export class CreateGroupBody extends S.Class("CreateGroupBody")({ + /** + * Human readable name for the group. + */ + "name": S.String.pipe(S.minLength(1), S.maxLength(255)) +}) {} + +/** + * Request payload for updating the details of an existing group. + */ +export class UpdateGroupBody extends S.Class("UpdateGroupBody")({ + /** + * New display name for the group. + */ + "name": S.String.pipe(S.minLength(1), S.maxLength(255)) +}) {} + +/** + * Response returned after updating a group. + */ +export class GroupResourceWithSuccess extends S.Class("GroupResourceWithSuccess")({ + /** + * Identifier for the group. + */ + "id": S.String, + /** + * Updated display name for the group. + */ + "name": S.String, + /** + * Unix timestamp (in seconds) when the group was created. + */ + "created_at": S.Int, + /** + * Whether the group is managed through SCIM and controlled by your identity provider. + */ + "is_scim_managed": S.Boolean +}) {} + +/** + * Always `group.deleted`. + */ +export class GroupDeletedResourceObject extends S.Literal("group.deleted") {} + +/** + * Confirmation payload returned after deleting a group. + */ +export class GroupDeletedResource extends S.Class("GroupDeletedResource")({ + /** + * Always `group.deleted`. + */ + "object": GroupDeletedResourceObject, + /** + * Identifier of the deleted group. + */ + "id": S.String, + /** + * Whether the group was deleted. + */ + "deleted": S.Boolean +}) {} + +export class ListGroupRoleAssignmentsParamsOrder extends S.Literal("asc", "desc") {} + +export class ListGroupRoleAssignmentsParams extends S.Struct({ + "limit": S.optionalWith(S.Int.pipe(S.greaterThanOrEqualTo(0), S.lessThanOrEqualTo(1000)), { nullable: true }), + "after": S.optionalWith(S.String, { nullable: true }), + "order": S.optionalWith(ListGroupRoleAssignmentsParamsOrder, { nullable: true }) +}) {} + +/** + * Always `list`. + */ +export class RoleListResourceObject extends S.Literal("list") {} + +/** + * Detailed information about a role assignment entry returned when listing assignments. + */ +export class AssignedRoleDetails extends S.Class("AssignedRoleDetails")({ + /** + * Identifier for the role. + */ + "id": S.String, + /** + * Name of the role. + */ + "name": S.String, + /** + * Permissions associated with the role. + */ + "permissions": S.Array(S.String), + /** + * Resource type the role applies to. + */ + "resource_type": S.String, + /** + * Whether the role is predefined by OpenAI. + */ + "predefined_role": S.Boolean, + /** + * Description of the role. + */ + "description": S.NullOr(S.String), + /** + * When the role was created. + */ + "created_at": S.NullOr(S.Int), + /** + * When the role was last updated. + */ + "updated_at": S.NullOr(S.Int), + /** + * Identifier of the actor who created the role. + */ + "created_by": S.NullOr(S.String), + /** + * User details for the actor that created the role, when available. + */ + "created_by_user_obj": S.NullOr(S.Record({ key: S.String, value: S.Unknown })), + /** + * Arbitrary metadata stored on the role. + */ + "metadata": S.NullOr(S.Record({ key: S.String, value: S.Unknown })) +}) {} + +/** + * Paginated list of roles assigned to a principal. + */ +export class RoleListResource extends S.Class("RoleListResource")({ + /** + * Always `list`. + */ + "object": RoleListResourceObject, + /** + * Role assignments returned in the current page. + */ + "data": S.Array(AssignedRoleDetails), + /** + * Whether additional assignments are available when paginating. + */ + "has_more": S.Boolean, + /** + * Cursor to fetch the next page of results, or `null` when there are no more assignments. + */ + "next": S.NullOr(S.String) +}) {} + +/** + * Request payload for assigning a role to a group or user. + */ +export class PublicAssignOrganizationGroupRoleBody + extends S.Class("PublicAssignOrganizationGroupRoleBody")({ + /** + * Identifier of the role to assign. + */ + "role_id": S.String + }) +{} + +/** + * Always `group.role`. + */ +export class GroupRoleAssignmentObject extends S.Literal("group.role") {} + +/** + * Always `group`. + */ +export class GroupObject extends S.Literal("group") {} + +/** + * Summary information about a group returned in role assignment responses. + */ +export class Group extends S.Class("Group")({ + /** + * Always `group`. + */ + "object": GroupObject, + /** + * Identifier for the group. + */ + "id": S.String, + /** + * Display name of the group. + */ + "name": S.String, + /** + * Unix timestamp (in seconds) when the group was created. + */ + "created_at": S.Int, + /** + * Whether the group is managed through SCIM. + */ + "scim_managed": S.Boolean +}) {} + +/** + * Always `role`. + */ +export class RoleObject extends S.Literal("role") {} + +/** + * Details about a role that can be assigned through the public Roles API. + */ +export class Role extends S.Class("Role")({ + /** + * Always `role`. + */ + "object": RoleObject, + /** + * Identifier for the role. + */ + "id": S.String, + /** + * Unique name for the role. + */ + "name": S.String, + /** + * Optional description of the role. + */ + "description": S.NullOr(S.String), + /** + * Permissions granted by the role. + */ + "permissions": S.Array(S.String), + /** + * Resource type the role is bound to (for example `api.organization` or `api.project`). + */ + "resource_type": S.String, + /** + * Whether the role is predefined and managed by OpenAI. + */ + "predefined_role": S.Boolean +}) {} + +/** + * Role assignment linking a group to a role. + */ +export class GroupRoleAssignment extends S.Class("GroupRoleAssignment")({ + /** + * Always `group.role`. + */ + "object": GroupRoleAssignmentObject, + "group": Group, + "role": Role +}) {} + +/** + * Confirmation payload returned after unassigning a role. + */ +export class DeletedRoleAssignmentResource + extends S.Class("DeletedRoleAssignmentResource")({ + /** + * Identifier for the deleted assignment, such as `group.role.deleted` or `user.role.deleted`. + */ + "object": S.String, + /** + * Whether the assignment was removed. + */ + "deleted": S.Boolean + }) +{} + +export class ListGroupUsersParamsOrder extends S.Literal("asc", "desc") {} + +export class ListGroupUsersParams extends S.Struct({ + "limit": S.optionalWith(S.Int.pipe(S.greaterThanOrEqualTo(0), S.lessThanOrEqualTo(1000)), { + nullable: true, + default: () => 100 as const + }), + "after": S.optionalWith(S.String, { nullable: true }), + "order": S.optionalWith(ListGroupUsersParamsOrder, { nullable: true, default: () => "desc" as const }) +}) {} + +/** + * Always `list`. + */ +export class UserListResourceObject extends S.Literal("list") {} + +/** + * The object type, which is always `organization.user` + */ +export class UserObject extends S.Literal("organization.user") {} + +/** + * `owner` or `reader` + */ +export class UserRole extends S.Literal("owner", "reader") {} + +/** + * Represents an individual `user` within an organization. + */ +export class User extends S.Class("User")({ + /** + * The object type, which is always `organization.user` + */ + "object": UserObject, + /** + * The identifier, which can be referenced in API endpoints + */ + "id": S.String, + /** + * The name of the user + */ + "name": S.String, + /** + * The email address of the user + */ + "email": S.String, + /** + * `owner` or `reader` + */ + "role": UserRole, + /** + * The Unix timestamp (in seconds) of when the user was added. + */ + "added_at": S.Int +}) {} + +/** + * Paginated list of user objects returned when inspecting group membership. + */ +export class UserListResource extends S.Class("UserListResource")({ + /** + * Always `list`. + */ + "object": UserListResourceObject, + /** + * Users in the current page. + */ + "data": S.Array(User), + /** + * Whether more users are available when paginating. + */ + "has_more": S.Boolean, + /** + * Cursor to fetch the next page of results, or `null` when no further users are available. + */ + "next": S.NullOr(S.String) +}) {} + +/** + * Request payload for adding a user to a group. + */ +export class CreateGroupUserBody extends S.Class("CreateGroupUserBody")({ + /** + * Identifier of the user to add to the group. + */ + "user_id": S.String +}) {} + +/** + * Always `group.user`. + */ +export class GroupUserAssignmentObject extends S.Literal("group.user") {} + +/** + * Confirmation payload returned after adding a user to a group. + */ +export class GroupUserAssignment extends S.Class("GroupUserAssignment")({ + /** + * Always `group.user`. + */ + "object": GroupUserAssignmentObject, + /** + * Identifier of the user that was added. + */ + "user_id": S.String, + /** + * Identifier of the group the user was added to. + */ + "group_id": S.String +}) {} + +/** + * Always `group.user.deleted`. + */ +export class GroupUserDeletedResourceObject extends S.Literal("group.user.deleted") {} + +/** + * Confirmation payload returned after removing a user from a group. + */ +export class GroupUserDeletedResource extends S.Class("GroupUserDeletedResource")({ + /** + * Always `group.user.deleted`. + */ + "object": GroupUserDeletedResourceObject, + /** + * Whether the group membership was removed. + */ + "deleted": S.Boolean +}) {} + +export class ListInvitesParams extends S.Struct({ + "limit": S.optionalWith(S.Int, { nullable: true, default: () => 20 as const }), + "after": S.optionalWith(S.String, { nullable: true }) +}) {} + +/** + * The object type, which is always `list` + */ +export class InviteListResponseObject extends S.Literal("list") {} + +/** + * The object type, which is always `organization.invite` + */ +export class InviteObject extends S.Literal("organization.invite") {} + +/** + * `owner` or `reader` + */ +export class InviteRole extends S.Literal("owner", "reader") {} + +/** + * `accepted`,`expired`, or `pending` + */ +export class InviteStatus extends S.Literal("accepted", "expired", "pending") {} + +/** + * Represents an individual `invite` to the organization. + */ +export class Invite extends S.Class("Invite")({ + /** + * The object type, which is always `organization.invite` + */ + "object": InviteObject, + /** + * The identifier, which can be referenced in API endpoints + */ + "id": S.String, + /** + * The email address of the individual to whom the invite was sent + */ + "email": S.String, + /** + * `owner` or `reader` + */ + "role": InviteRole, + /** + * `accepted`,`expired`, or `pending` + */ + "status": InviteStatus, + /** + * The Unix timestamp (in seconds) of when the invite was sent. + */ + "invited_at": S.Int, + /** + * The Unix timestamp (in seconds) of when the invite expires. + */ + "expires_at": S.Int, + /** + * The Unix timestamp (in seconds) of when the invite was accepted. + */ + "accepted_at": S.optionalWith(S.Int, { nullable: true }), + /** + * The projects that were granted membership upon acceptance of the invite. + */ + "projects": S.optionalWith( + S.Array(S.Struct({ + /** + * Project's public ID + */ + "id": S.optionalWith(S.String, { nullable: true }), + /** + * Project membership role + */ + "role": S.optionalWith(S.Literal("member", "owner"), { nullable: true }) + })), + { nullable: true } + ) +}) {} + +export class InviteListResponse extends S.Class("InviteListResponse")({ + /** + * The object type, which is always `list` + */ + "object": InviteListResponseObject, + "data": S.Array(Invite), + /** + * The first `invite_id` in the retrieved `list` + */ + "first_id": S.optionalWith(S.String, { nullable: true }), + /** + * The last `invite_id` in the retrieved `list` + */ + "last_id": S.optionalWith(S.String, { nullable: true }), + /** + * The `has_more` property is used for pagination to indicate there are additional results. + */ + "has_more": S.optionalWith(S.Boolean, { nullable: true }) +}) {} + +/** + * `owner` or `reader` + */ +export class InviteRequestRole extends S.Literal("reader", "owner") {} + +export class InviteRequest extends S.Class("InviteRequest")({ + /** + * Send an email to this address + */ + "email": S.String, + /** + * `owner` or `reader` + */ + "role": InviteRequestRole, + /** + * An array of projects to which membership is granted at the same time the org invite is accepted. If omitted, the user will be invited to the default project for compatibility with legacy behavior. + */ + "projects": S.optionalWith( + S.Array(S.Struct({ + /** + * Project's public ID + */ + "id": S.String, + /** + * Project membership role + */ + "role": S.Literal("member", "owner") + })), + { nullable: true } + ) +}) {} + +/** + * The object type, which is always `organization.invite.deleted` + */ +export class InviteDeleteResponseObject extends S.Literal("organization.invite.deleted") {} + +export class InviteDeleteResponse extends S.Class("InviteDeleteResponse")({ + /** + * The object type, which is always `organization.invite.deleted` + */ + "object": InviteDeleteResponseObject, + "id": S.String, + "deleted": S.Boolean +}) {} + +export class ListProjectsParams extends S.Struct({ + "limit": S.optionalWith(S.Int, { nullable: true, default: () => 20 as const }), + "after": S.optionalWith(S.String, { nullable: true }), + "include_archived": S.optionalWith(S.Boolean, { nullable: true, default: () => false as const }) +}) {} + +export class ProjectListResponseObject extends S.Literal("list") {} + +/** + * The object type, which is always `organization.project` + */ +export class ProjectObject extends S.Literal("organization.project") {} + +/** + * `active` or `archived` + */ +export class ProjectStatus extends S.Literal("active", "archived") {} + +/** + * Represents an individual project. + */ +export class Project extends S.Class("Project")({ + /** + * The identifier, which can be referenced in API endpoints + */ + "id": S.String, + /** + * The object type, which is always `organization.project` + */ + "object": ProjectObject, + /** + * The name of the project. This appears in reporting. + */ + "name": S.String, + /** + * The Unix timestamp (in seconds) of when the project was created. + */ + "created_at": S.Int, + "archived_at": S.optionalWith(S.Int, { nullable: true }), + /** + * `active` or `archived` + */ + "status": ProjectStatus +}) {} + +export class ProjectListResponse extends S.Class("ProjectListResponse")({ + "object": ProjectListResponseObject, + "data": S.Array(Project), + "first_id": S.String, + "last_id": S.String, + "has_more": S.Boolean +}) {} + +/** + * Create the project with the specified data residency region. Your organization must have access to Data residency functionality in order to use. See [data residency controls](https://platform.openai.com/docs/guides/your-data#data-residency-controls) to review the functionality and limitations of setting this field. + */ +export class ProjectCreateRequestGeography extends S.Literal("US", "EU", "JP", "IN", "KR", "CA", "AU", "SG") {} + +export class ProjectCreateRequest extends S.Class("ProjectCreateRequest")({ + /** + * The friendly name of the project, this name appears in reports. + */ + "name": S.String, + /** + * Create the project with the specified data residency region. Your organization must have access to Data residency functionality in order to use. See [data residency controls](https://platform.openai.com/docs/guides/your-data#data-residency-controls) to review the functionality and limitations of setting this field. + */ + "geography": S.optionalWith(ProjectCreateRequestGeography, { nullable: true }) +}) {} + +export class ProjectUpdateRequest extends S.Class("ProjectUpdateRequest")({ + /** + * The updated name of the project, this name appears in reports. + */ + "name": S.String +}) {} + +export class ErrorResponse extends S.Class("ErrorResponse")({ + "error": Error +}) {} + +export class ListProjectApiKeysParams extends S.Struct({ + "limit": S.optionalWith(S.Int, { nullable: true, default: () => 20 as const }), + "after": S.optionalWith(S.String, { nullable: true }) +}) {} + +export class ProjectApiKeyListResponseObject extends S.Literal("list") {} + +/** + * The object type, which is always `organization.project.api_key` + */ +export class ProjectApiKeyObject extends S.Literal("organization.project.api_key") {} + +/** + * `user` or `service_account` + */ +export class ProjectApiKeyOwnerType extends S.Literal("user", "service_account") {} + +/** + * The object type, which is always `organization.project.user` + */ +export class ProjectUserObject extends S.Literal("organization.project.user") {} + +/** + * `owner` or `member` + */ +export class ProjectUserRole extends S.Literal("owner", "member") {} + +/** + * Represents an individual user in a project. + */ +export class ProjectUser extends S.Class("ProjectUser")({ + /** + * The object type, which is always `organization.project.user` + */ + "object": ProjectUserObject, + /** + * The identifier, which can be referenced in API endpoints + */ + "id": S.String, + /** + * The name of the user + */ + "name": S.String, + /** + * The email address of the user + */ + "email": S.String, + /** + * `owner` or `member` + */ + "role": ProjectUserRole, + /** + * The Unix timestamp (in seconds) of when the project was added. + */ + "added_at": S.Int +}) {} + +/** + * The object type, which is always `organization.project.service_account` + */ +export class ProjectServiceAccountObject extends S.Literal("organization.project.service_account") {} + +/** + * `owner` or `member` + */ +export class ProjectServiceAccountRole extends S.Literal("owner", "member") {} + +/** + * Represents an individual service account in a project. + */ +export class ProjectServiceAccount extends S.Class("ProjectServiceAccount")({ + /** + * The object type, which is always `organization.project.service_account` + */ + "object": ProjectServiceAccountObject, + /** + * The identifier, which can be referenced in API endpoints + */ + "id": S.String, + /** + * The name of the service account + */ + "name": S.String, + /** + * `owner` or `member` + */ + "role": ProjectServiceAccountRole, + /** + * The Unix timestamp (in seconds) of when the service account was created + */ + "created_at": S.Int +}) {} + +/** + * Represents an individual API key in a project. + */ +export class ProjectApiKey extends S.Class("ProjectApiKey")({ + /** + * The object type, which is always `organization.project.api_key` + */ + "object": ProjectApiKeyObject, + /** + * The redacted value of the API key + */ + "redacted_value": S.String, + /** + * The name of the API key + */ + "name": S.String, + /** + * The Unix timestamp (in seconds) of when the API key was created + */ + "created_at": S.Int, + /** + * The Unix timestamp (in seconds) of when the API key was last used. + */ + "last_used_at": S.Int, + /** + * The identifier, which can be referenced in API endpoints + */ + "id": S.String, + "owner": S.Struct({ + /** + * `user` or `service_account` + */ + "type": S.optionalWith(ProjectApiKeyOwnerType, { nullable: true }), + "user": S.optionalWith(ProjectUser, { nullable: true }), + "service_account": S.optionalWith(ProjectServiceAccount, { nullable: true }) + }) +}) {} + +export class ProjectApiKeyListResponse extends S.Class("ProjectApiKeyListResponse")({ + "object": ProjectApiKeyListResponseObject, + "data": S.Array(ProjectApiKey), + "first_id": S.String, + "last_id": S.String, + "has_more": S.Boolean +}) {} + +export class ProjectApiKeyDeleteResponseObject extends S.Literal("organization.project.api_key.deleted") {} + +export class ProjectApiKeyDeleteResponse extends S.Class("ProjectApiKeyDeleteResponse")({ + "object": ProjectApiKeyDeleteResponseObject, + "id": S.String, + "deleted": S.Boolean +}) {} + +export class ListProjectCertificatesParamsOrder extends S.Literal("asc", "desc") {} + +export class ListProjectCertificatesParams extends S.Struct({ + "limit": S.optionalWith(S.Int, { nullable: true, default: () => 20 as const }), + "after": S.optionalWith(S.String, { nullable: true }), + "order": S.optionalWith(ListProjectCertificatesParamsOrder, { nullable: true, default: () => "desc" as const }) +}) {} + +export class ListProjectGroupsParamsOrder extends S.Literal("asc", "desc") {} + +export class ListProjectGroupsParams extends S.Struct({ + "limit": S.optionalWith(S.Int.pipe(S.greaterThanOrEqualTo(0), S.lessThanOrEqualTo(100)), { + nullable: true, + default: () => 20 as const + }), + "after": S.optionalWith(S.String, { nullable: true }), + "order": S.optionalWith(ListProjectGroupsParamsOrder, { nullable: true, default: () => "asc" as const }) +}) {} + +/** + * Always `list`. + */ +export class ProjectGroupListResourceObject extends S.Literal("list") {} + +/** + * Always `project.group`. + */ +export class ProjectGroupObject extends S.Literal("project.group") {} + +/** + * Details about a group's membership in a project. + */ +export class ProjectGroup extends S.Class("ProjectGroup")({ + /** + * Always `project.group`. + */ + "object": ProjectGroupObject, + /** + * Identifier of the project. + */ + "project_id": S.String, + /** + * Identifier of the group that has access to the project. + */ + "group_id": S.String, + /** + * Display name of the group. + */ + "group_name": S.String, + /** + * Unix timestamp (in seconds) when the group was granted project access. + */ + "created_at": S.Int +}) {} + +/** + * Paginated list of groups that have access to a project. + */ +export class ProjectGroupListResource extends S.Class("ProjectGroupListResource")({ + /** + * Always `list`. + */ + "object": ProjectGroupListResourceObject, + /** + * Project group memberships returned in the current page. + */ + "data": S.Array(ProjectGroup), + /** + * Whether additional project group memberships are available. + */ + "has_more": S.Boolean, + /** + * Cursor to fetch the next page of results, or `null` when there are no more results. + */ + "next": S.NullOr(S.String) +}) {} + +/** + * Request payload for granting a group access to a project. + */ +export class InviteProjectGroupBody extends S.Class("InviteProjectGroupBody")({ + /** + * Identifier of the group to add to the project. + */ + "group_id": S.String, + /** + * Identifier of the project role to grant to the group. + */ + "role": S.String +}) {} + +/** + * Always `project.group.deleted`. + */ +export class ProjectGroupDeletedResourceObject extends S.Literal("project.group.deleted") {} + +/** + * Confirmation payload returned after removing a group from a project. + */ +export class ProjectGroupDeletedResource extends S.Class("ProjectGroupDeletedResource")({ + /** + * Always `project.group.deleted`. + */ + "object": ProjectGroupDeletedResourceObject, + /** + * Whether the group membership in the project was removed. + */ + "deleted": S.Boolean +}) {} + +export class ListProjectRateLimitsParams extends S.Struct({ + "limit": S.optionalWith(S.Int, { nullable: true, default: () => 100 as const }), + "after": S.optionalWith(S.String, { nullable: true }), + "before": S.optionalWith(S.String, { nullable: true }) +}) {} + +export class ProjectRateLimitListResponseObject extends S.Literal("list") {} + +/** + * The object type, which is always `project.rate_limit` + */ +export class ProjectRateLimitObject extends S.Literal("project.rate_limit") {} + +/** + * Represents a project rate limit config. + */ +export class ProjectRateLimit extends S.Class("ProjectRateLimit")({ + /** + * The object type, which is always `project.rate_limit` + */ + "object": ProjectRateLimitObject, + /** + * The identifier, which can be referenced in API endpoints. + */ + "id": S.String, + /** + * The model this rate limit applies to. + */ + "model": S.String, + /** + * The maximum requests per minute. + */ + "max_requests_per_1_minute": S.Int, + /** + * The maximum tokens per minute. + */ + "max_tokens_per_1_minute": S.Int, + /** + * The maximum images per minute. Only present for relevant models. + */ + "max_images_per_1_minute": S.optionalWith(S.Int, { nullable: true }), + /** + * The maximum audio megabytes per minute. Only present for relevant models. + */ + "max_audio_megabytes_per_1_minute": S.optionalWith(S.Int, { nullable: true }), + /** + * The maximum requests per day. Only present for relevant models. + */ + "max_requests_per_1_day": S.optionalWith(S.Int, { nullable: true }), + /** + * The maximum batch input tokens per day. Only present for relevant models. + */ + "batch_1_day_max_input_tokens": S.optionalWith(S.Int, { nullable: true }) +}) {} + +export class ProjectRateLimitListResponse + extends S.Class("ProjectRateLimitListResponse")({ + "object": ProjectRateLimitListResponseObject, + "data": S.Array(ProjectRateLimit), + "first_id": S.String, + "last_id": S.String, + "has_more": S.Boolean + }) +{} + +export class ProjectRateLimitUpdateRequest + extends S.Class("ProjectRateLimitUpdateRequest")({ + /** + * The maximum requests per minute. + */ + "max_requests_per_1_minute": S.optionalWith(S.Int, { nullable: true }), + /** + * The maximum tokens per minute. + */ + "max_tokens_per_1_minute": S.optionalWith(S.Int, { nullable: true }), + /** + * The maximum images per minute. Only relevant for certain models. + */ + "max_images_per_1_minute": S.optionalWith(S.Int, { nullable: true }), + /** + * The maximum audio megabytes per minute. Only relevant for certain models. + */ + "max_audio_megabytes_per_1_minute": S.optionalWith(S.Int, { nullable: true }), + /** + * The maximum requests per day. Only relevant for certain models. + */ + "max_requests_per_1_day": S.optionalWith(S.Int, { nullable: true }), + /** + * The maximum batch input tokens per day. Only relevant for certain models. + */ + "batch_1_day_max_input_tokens": S.optionalWith(S.Int, { nullable: true }) + }) +{} + +export class ListProjectServiceAccountsParams extends S.Struct({ + "limit": S.optionalWith(S.Int, { nullable: true, default: () => 20 as const }), + "after": S.optionalWith(S.String, { nullable: true }) +}) {} + +export class ProjectServiceAccountListResponseObject extends S.Literal("list") {} + +export class ProjectServiceAccountListResponse + extends S.Class("ProjectServiceAccountListResponse")({ + "object": ProjectServiceAccountListResponseObject, + "data": S.Array(ProjectServiceAccount), + "first_id": S.String, + "last_id": S.String, + "has_more": S.Boolean + }) +{} + +export class ProjectServiceAccountCreateRequest + extends S.Class("ProjectServiceAccountCreateRequest")({ + /** + * The name of the service account being created. + */ + "name": S.String + }) +{} + +export class ProjectServiceAccountCreateResponseObject extends S.Literal("organization.project.service_account") {} + +/** + * Service accounts can only have one role of type `member` + */ +export class ProjectServiceAccountCreateResponseRole extends S.Literal("member") {} + +/** + * The object type, which is always `organization.project.service_account.api_key` + */ +export class ProjectServiceAccountApiKeyObject extends S.Literal("organization.project.service_account.api_key") {} + +export class ProjectServiceAccountApiKey extends S.Class("ProjectServiceAccountApiKey")({ + /** + * The object type, which is always `organization.project.service_account.api_key` + */ + "object": ProjectServiceAccountApiKeyObject, + "value": S.String, + "name": S.String, + "created_at": S.Int, + "id": S.String +}) {} + +export class ProjectServiceAccountCreateResponse + extends S.Class("ProjectServiceAccountCreateResponse")({ + "object": ProjectServiceAccountCreateResponseObject, + "id": S.String, + "name": S.String, + /** + * Service accounts can only have one role of type `member` + */ + "role": ProjectServiceAccountCreateResponseRole, + "created_at": S.Int, + "api_key": ProjectServiceAccountApiKey + }) +{} + +export class ProjectServiceAccountDeleteResponseObject + extends S.Literal("organization.project.service_account.deleted") +{} + +export class ProjectServiceAccountDeleteResponse + extends S.Class("ProjectServiceAccountDeleteResponse")({ + "object": ProjectServiceAccountDeleteResponseObject, + "id": S.String, + "deleted": S.Boolean + }) +{} + +export class ListProjectUsersParams extends S.Struct({ + "limit": S.optionalWith(S.Int, { nullable: true, default: () => 20 as const }), + "after": S.optionalWith(S.String, { nullable: true }) +}) {} + +export class ProjectUserListResponse extends S.Class("ProjectUserListResponse")({ + "object": S.String, + "data": S.Array(ProjectUser), + "first_id": S.String, + "last_id": S.String, + "has_more": S.Boolean +}) {} + +/** + * `owner` or `member` + */ +export class ProjectUserCreateRequestRole extends S.Literal("owner", "member") {} + +export class ProjectUserCreateRequest extends S.Class("ProjectUserCreateRequest")({ + /** + * The ID of the user. + */ + "user_id": S.String, + /** + * `owner` or `member` + */ + "role": ProjectUserCreateRequestRole +}) {} + +/** + * `owner` or `member` + */ +export class ProjectUserUpdateRequestRole extends S.Literal("owner", "member") {} + +export class ProjectUserUpdateRequest extends S.Class("ProjectUserUpdateRequest")({ + /** + * `owner` or `member` + */ + "role": ProjectUserUpdateRequestRole +}) {} + +export class ProjectUserDeleteResponseObject extends S.Literal("organization.project.user.deleted") {} + +export class ProjectUserDeleteResponse extends S.Class("ProjectUserDeleteResponse")({ + "object": ProjectUserDeleteResponseObject, + "id": S.String, + "deleted": S.Boolean +}) {} + +export class ListRolesParamsOrder extends S.Literal("asc", "desc") {} + +export class ListRolesParams extends S.Struct({ + "limit": S.optionalWith(S.Int.pipe(S.greaterThanOrEqualTo(0), S.lessThanOrEqualTo(1000)), { + nullable: true, + default: () => 1000 as const + }), + "after": S.optionalWith(S.String, { nullable: true }), + "order": S.optionalWith(ListRolesParamsOrder, { nullable: true, default: () => "asc" as const }) +}) {} + +/** + * Always `list`. + */ +export class PublicRoleListResourceObject extends S.Literal("list") {} + +/** + * Paginated list of roles available on an organization or project. + */ +export class PublicRoleListResource extends S.Class("PublicRoleListResource")({ + /** + * Always `list`. + */ + "object": PublicRoleListResourceObject, + /** + * Roles returned in the current page. + */ + "data": S.Array(Role), + /** + * Whether more roles are available when paginating. + */ + "has_more": S.Boolean, + /** + * Cursor to fetch the next page of results, or `null` when there are no additional roles. + */ + "next": S.NullOr(S.String) +}) {} + +/** + * Request payload for creating a custom role. + */ +export class PublicCreateOrganizationRoleBody + extends S.Class("PublicCreateOrganizationRoleBody")({ + /** + * Unique name for the role. + */ + "role_name": S.String, + /** + * Permissions to grant to the role. + */ + "permissions": S.Array(S.String), + /** + * Optional description of the role. + */ + "description": S.optionalWith(S.String, { nullable: true }) + }) +{} + +/** + * Request payload for updating an existing role. + */ +export class PublicUpdateOrganizationRoleBody + extends S.Class("PublicUpdateOrganizationRoleBody")({ + /** + * Updated set of permissions for the role. + */ + "permissions": S.optionalWith(S.Array(S.String), { nullable: true }), + /** + * New description for the role. + */ + "description": S.optionalWith(S.String, { nullable: true }), + /** + * New name for the role. + */ + "role_name": S.optionalWith(S.String, { nullable: true }) + }) +{} + +/** + * Always `role.deleted`. + */ +export class RoleDeletedResourceObject extends S.Literal("role.deleted") {} + +/** + * Confirmation payload returned after deleting a role. + */ +export class RoleDeletedResource extends S.Class("RoleDeletedResource")({ + /** + * Always `role.deleted`. + */ + "object": RoleDeletedResourceObject, + /** + * Identifier of the deleted role. + */ + "id": S.String, + /** + * Whether the role was deleted. + */ + "deleted": S.Boolean +}) {} + +export class UsageAudioSpeechesParamsBucketWidth extends S.Literal("1m", "1h", "1d") {} + +export class UsageAudioSpeechesParams extends S.Struct({ + "start_time": S.Int, + "end_time": S.optionalWith(S.Int, { nullable: true }), + "bucket_width": S.optionalWith(UsageAudioSpeechesParamsBucketWidth, { nullable: true, default: () => "1d" as const }), + "project_ids": S.optionalWith(S.Array(S.String), { nullable: true }), + "user_ids": S.optionalWith(S.Array(S.String), { nullable: true }), + "api_key_ids": S.optionalWith(S.Array(S.String), { nullable: true }), + "models": S.optionalWith(S.Array(S.String), { nullable: true }), + "group_by": S.optionalWith(S.Array(S.Literal("project_id", "user_id", "api_key_id", "model")), { nullable: true }), + "limit": S.optionalWith(S.Int, { nullable: true }), + "page": S.optionalWith(S.String, { nullable: true }) +}) {} + +export class UsageAudioTranscriptionsParamsBucketWidth extends S.Literal("1m", "1h", "1d") {} + +export class UsageAudioTranscriptionsParams extends S.Struct({ + "start_time": S.Int, + "end_time": S.optionalWith(S.Int, { nullable: true }), + "bucket_width": S.optionalWith(UsageAudioTranscriptionsParamsBucketWidth, { + nullable: true, + default: () => "1d" as const + }), + "project_ids": S.optionalWith(S.Array(S.String), { nullable: true }), + "user_ids": S.optionalWith(S.Array(S.String), { nullable: true }), + "api_key_ids": S.optionalWith(S.Array(S.String), { nullable: true }), + "models": S.optionalWith(S.Array(S.String), { nullable: true }), + "group_by": S.optionalWith(S.Array(S.Literal("project_id", "user_id", "api_key_id", "model")), { nullable: true }), + "limit": S.optionalWith(S.Int, { nullable: true }), + "page": S.optionalWith(S.String, { nullable: true }) +}) {} + +export class UsageCodeInterpreterSessionsParamsBucketWidth extends S.Literal("1m", "1h", "1d") {} + +export class UsageCodeInterpreterSessionsParams extends S.Struct({ + "start_time": S.Int, + "end_time": S.optionalWith(S.Int, { nullable: true }), + "bucket_width": S.optionalWith(UsageCodeInterpreterSessionsParamsBucketWidth, { + nullable: true, + default: () => "1d" as const + }), + "project_ids": S.optionalWith(S.Array(S.String), { nullable: true }), + "group_by": S.optionalWith(S.Array(S.Literal("project_id")), { nullable: true }), + "limit": S.optionalWith(S.Int, { nullable: true }), + "page": S.optionalWith(S.String, { nullable: true }) +}) {} + +export class UsageCompletionsParamsBucketWidth extends S.Literal("1m", "1h", "1d") {} + +export class UsageCompletionsParams extends S.Struct({ + "start_time": S.Int, + "end_time": S.optionalWith(S.Int, { nullable: true }), + "bucket_width": S.optionalWith(UsageCompletionsParamsBucketWidth, { nullable: true, default: () => "1d" as const }), + "project_ids": S.optionalWith(S.Array(S.String), { nullable: true }), + "user_ids": S.optionalWith(S.Array(S.String), { nullable: true }), + "api_key_ids": S.optionalWith(S.Array(S.String), { nullable: true }), + "models": S.optionalWith(S.Array(S.String), { nullable: true }), + "batch": S.optionalWith(S.Boolean, { nullable: true }), + "group_by": S.optionalWith( + S.Array(S.Literal("project_id", "user_id", "api_key_id", "model", "batch", "service_tier")), + { nullable: true } + ), + "limit": S.optionalWith(S.Int, { nullable: true }), + "page": S.optionalWith(S.String, { nullable: true }) +}) {} + +export class UsageEmbeddingsParamsBucketWidth extends S.Literal("1m", "1h", "1d") {} + +export class UsageEmbeddingsParams extends S.Struct({ + "start_time": S.Int, + "end_time": S.optionalWith(S.Int, { nullable: true }), + "bucket_width": S.optionalWith(UsageEmbeddingsParamsBucketWidth, { nullable: true, default: () => "1d" as const }), + "project_ids": S.optionalWith(S.Array(S.String), { nullable: true }), + "user_ids": S.optionalWith(S.Array(S.String), { nullable: true }), + "api_key_ids": S.optionalWith(S.Array(S.String), { nullable: true }), + "models": S.optionalWith(S.Array(S.String), { nullable: true }), + "group_by": S.optionalWith(S.Array(S.Literal("project_id", "user_id", "api_key_id", "model")), { nullable: true }), + "limit": S.optionalWith(S.Int, { nullable: true }), + "page": S.optionalWith(S.String, { nullable: true }) +}) {} + +export class UsageImagesParamsBucketWidth extends S.Literal("1m", "1h", "1d") {} + +export class UsageImagesParams extends S.Struct({ + "start_time": S.Int, + "end_time": S.optionalWith(S.Int, { nullable: true }), + "bucket_width": S.optionalWith(UsageImagesParamsBucketWidth, { nullable: true, default: () => "1d" as const }), + "sources": S.optionalWith(S.Array(S.Literal("image.generation", "image.edit", "image.variation")), { + nullable: true + }), + "sizes": S.optionalWith(S.Array(S.Literal("256x256", "512x512", "1024x1024", "1792x1792", "1024x1792")), { + nullable: true + }), + "project_ids": S.optionalWith(S.Array(S.String), { nullable: true }), + "user_ids": S.optionalWith(S.Array(S.String), { nullable: true }), + "api_key_ids": S.optionalWith(S.Array(S.String), { nullable: true }), + "models": S.optionalWith(S.Array(S.String), { nullable: true }), + "group_by": S.optionalWith(S.Array(S.Literal("project_id", "user_id", "api_key_id", "model", "size", "source")), { + nullable: true + }), + "limit": S.optionalWith(S.Int, { nullable: true }), + "page": S.optionalWith(S.String, { nullable: true }) +}) {} + +export class UsageModerationsParamsBucketWidth extends S.Literal("1m", "1h", "1d") {} + +export class UsageModerationsParams extends S.Struct({ + "start_time": S.Int, + "end_time": S.optionalWith(S.Int, { nullable: true }), + "bucket_width": S.optionalWith(UsageModerationsParamsBucketWidth, { nullable: true, default: () => "1d" as const }), + "project_ids": S.optionalWith(S.Array(S.String), { nullable: true }), + "user_ids": S.optionalWith(S.Array(S.String), { nullable: true }), + "api_key_ids": S.optionalWith(S.Array(S.String), { nullable: true }), + "models": S.optionalWith(S.Array(S.String), { nullable: true }), + "group_by": S.optionalWith(S.Array(S.Literal("project_id", "user_id", "api_key_id", "model")), { nullable: true }), + "limit": S.optionalWith(S.Int, { nullable: true }), + "page": S.optionalWith(S.String, { nullable: true }) +}) {} + +export class UsageVectorStoresParamsBucketWidth extends S.Literal("1m", "1h", "1d") {} + +export class UsageVectorStoresParams extends S.Struct({ + "start_time": S.Int, + "end_time": S.optionalWith(S.Int, { nullable: true }), + "bucket_width": S.optionalWith(UsageVectorStoresParamsBucketWidth, { nullable: true, default: () => "1d" as const }), + "project_ids": S.optionalWith(S.Array(S.String), { nullable: true }), + "group_by": S.optionalWith(S.Array(S.Literal("project_id")), { nullable: true }), + "limit": S.optionalWith(S.Int, { nullable: true }), + "page": S.optionalWith(S.String, { nullable: true }) +}) {} + +export class ListUsersParams extends S.Struct({ + "limit": S.optionalWith(S.Int, { nullable: true, default: () => 20 as const }), + "after": S.optionalWith(S.String, { nullable: true }), + "emails": S.optionalWith(S.Array(S.String), { nullable: true }) +}) {} + +export class UserListResponseObject extends S.Literal("list") {} + +export class UserListResponse extends S.Class("UserListResponse")({ + "object": UserListResponseObject, + "data": S.Array(User), + "first_id": S.String, + "last_id": S.String, + "has_more": S.Boolean +}) {} + +/** + * `owner` or `reader` + */ +export class UserRoleUpdateRequestRole extends S.Literal("owner", "reader") {} + +export class UserRoleUpdateRequest extends S.Class("UserRoleUpdateRequest")({ + /** + * `owner` or `reader` + */ + "role": UserRoleUpdateRequestRole +}) {} + +export class UserDeleteResponseObject extends S.Literal("organization.user.deleted") {} + +export class UserDeleteResponse extends S.Class("UserDeleteResponse")({ + "object": UserDeleteResponseObject, + "id": S.String, + "deleted": S.Boolean +}) {} + +export class ListUserRoleAssignmentsParamsOrder extends S.Literal("asc", "desc") {} + +export class ListUserRoleAssignmentsParams extends S.Struct({ + "limit": S.optionalWith(S.Int.pipe(S.greaterThanOrEqualTo(0), S.lessThanOrEqualTo(1000)), { nullable: true }), + "after": S.optionalWith(S.String, { nullable: true }), + "order": S.optionalWith(ListUserRoleAssignmentsParamsOrder, { nullable: true }) +}) {} + +/** + * Always `user.role`. + */ +export class UserRoleAssignmentObject extends S.Literal("user.role") {} + +/** + * Role assignment linking a user to a role. + */ +export class UserRoleAssignment extends S.Class("UserRoleAssignment")({ + /** + * Always `user.role`. + */ + "object": UserRoleAssignmentObject, + "user": User, + "role": Role +}) {} + +export class ListProjectGroupRoleAssignmentsParamsOrder extends S.Literal("asc", "desc") {} + +export class ListProjectGroupRoleAssignmentsParams extends S.Struct({ + "limit": S.optionalWith(S.Int.pipe(S.greaterThanOrEqualTo(0), S.lessThanOrEqualTo(1000)), { nullable: true }), + "after": S.optionalWith(S.String, { nullable: true }), + "order": S.optionalWith(ListProjectGroupRoleAssignmentsParamsOrder, { nullable: true }) +}) {} + +export class ListProjectRolesParamsOrder extends S.Literal("asc", "desc") {} + +export class ListProjectRolesParams extends S.Struct({ + "limit": S.optionalWith(S.Int.pipe(S.greaterThanOrEqualTo(0), S.lessThanOrEqualTo(1000)), { + nullable: true, + default: () => 1000 as const + }), + "after": S.optionalWith(S.String, { nullable: true }), + "order": S.optionalWith(ListProjectRolesParamsOrder, { nullable: true, default: () => "asc" as const }) +}) {} + +export class ListProjectUserRoleAssignmentsParamsOrder extends S.Literal("asc", "desc") {} + +export class ListProjectUserRoleAssignmentsParams extends S.Struct({ + "limit": S.optionalWith(S.Int.pipe(S.greaterThanOrEqualTo(0), S.lessThanOrEqualTo(1000)), { nullable: true }), + "after": S.optionalWith(S.String, { nullable: true }), + "order": S.optionalWith(ListProjectUserRoleAssignmentsParamsOrder, { nullable: true }) +}) {} + +/** + * The type of session to create. Always `realtime` for the Realtime API. + */ +export class RealtimeSessionCreateRequestGAType extends S.Literal("realtime") {} + +export class RealtimeSessionCreateRequestGAModelEnum extends S.Literal( + "gpt-realtime", + "gpt-realtime-2025-08-28", + "gpt-4o-realtime-preview", + "gpt-4o-realtime-preview-2024-10-01", + "gpt-4o-realtime-preview-2024-12-17", + "gpt-4o-realtime-preview-2025-06-03", + "gpt-4o-mini-realtime-preview", + "gpt-4o-mini-realtime-preview-2024-12-17", + "gpt-realtime-mini", + "gpt-realtime-mini-2025-10-06", + "gpt-audio-mini", + "gpt-audio-mini-2025-10-06" +) {} + +/** + * The audio format. Always `audio/pcma`. + */ +export class RealtimeAudioFormatsEnumType extends S.Literal("audio/pcma") {} + +/** + * The sample rate of the audio. Always `24000`. + */ +export class RealtimeAudioFormatsEnumRate extends S.Literal(24000) {} + +export class RealtimeAudioFormats extends S.Union( + /** + * The PCM audio format. Only a 24kHz sample rate is supported. + */ + S.Struct({ + /** + * The audio format. Always `audio/pcm`. + */ + "type": S.optionalWith(RealtimeAudioFormatsEnumType, { nullable: true }), + /** + * The sample rate of the audio. Always `24000`. + */ + "rate": S.optionalWith(RealtimeAudioFormatsEnumRate, { nullable: true }) + }), + /** + * The G.711 μ-law format. + */ + S.Struct({ + /** + * The audio format. Always `audio/pcmu`. + */ + "type": S.optionalWith(RealtimeAudioFormatsEnumType, { nullable: true }) + }), + /** + * The G.711 A-law format. + */ + S.Struct({ + /** + * The audio format. Always `audio/pcma`. + */ + "type": S.optionalWith(RealtimeAudioFormatsEnumType, { nullable: true }) + }) +) {} + +/** + * The model to use for transcription. Current options are `whisper-1`, `gpt-4o-mini-transcribe`, `gpt-4o-transcribe`, and `gpt-4o-transcribe-diarize`. Use `gpt-4o-transcribe-diarize` when you need diarization with speaker labels. + */ +export class AudioTranscriptionModel + extends S.Literal("whisper-1", "gpt-4o-mini-transcribe", "gpt-4o-transcribe", "gpt-4o-transcribe-diarize") +{} + +export class AudioTranscription extends S.Class("AudioTranscription")({ + /** + * The model to use for transcription. Current options are `whisper-1`, `gpt-4o-mini-transcribe`, `gpt-4o-transcribe`, and `gpt-4o-transcribe-diarize`. Use `gpt-4o-transcribe-diarize` when you need diarization with speaker labels. + */ + "model": S.optionalWith(AudioTranscriptionModel, { nullable: true }), + /** + * The language of the input audio. Supplying the input language in + * [ISO-639-1](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) (e.g. `en`) format + * will improve accuracy and latency. + */ + "language": S.optionalWith(S.String, { nullable: true }), + /** + * An optional text to guide the model's style or continue a previous audio + * segment. + * For `whisper-1`, the [prompt is a list of keywords](https://platform.openai.com/docs/guides/speech-to-text#prompting). + * For `gpt-4o-transcribe` models (excluding `gpt-4o-transcribe-diarize`), the prompt is a free text string, for example "expect words related to technology". + */ + "prompt": S.optionalWith(S.String, { nullable: true }) +}) {} + +/** + * Type of noise reduction. `near_field` is for close-talking microphones such as headphones, `far_field` is for far-field microphones such as laptop or conference room microphones. + */ +export class NoiseReductionType extends S.Literal("near_field", "far_field") {} + +/** + * Used only for `semantic_vad` mode. The eagerness of the model to respond. `low` will wait longer for the user to continue speaking, `high` will respond more quickly. `auto` is the default and is equivalent to `medium`. `low`, `medium`, and `high` have max timeouts of 8s, 4s, and 2s respectively. + */ +export class RealtimeTurnDetectionEnumEagerness extends S.Literal("low", "medium", "high", "auto") {} + +export class RealtimeTurnDetection extends S.Union( + /** + * Configuration for turn detection, ether Server VAD or Semantic VAD. This can be set to `null` to turn off, in which case the client must manually trigger model response. + * + * Server VAD means that the model will detect the start and end of speech based on audio volume and respond at the end of user speech. + * + * Semantic VAD is more advanced and uses a turn detection model (in conjunction with VAD) to semantically estimate whether the user has finished speaking, then dynamically sets a timeout based on this probability. For example, if user audio trails off with "uhhm", the model will score a low probability of turn end and wait longer for the user to continue speaking. This can be useful for more natural conversations, but may have a higher latency. + */ + S.Union( + /** + * Server-side voice activity detection (VAD) which flips on when user speech is detected and off after a period of silence. + */ + S.Struct({ + /** + * Type of turn detection, `server_vad` to turn on simple Server VAD. + */ + "type": S.Literal("server_vad").pipe(S.propertySignature, S.withConstructorDefault(() => "server_vad" as const)), + /** + * Used only for `server_vad` mode. Activation threshold for VAD (0.0 to 1.0), this defaults to 0.5. A + * higher threshold will require louder audio to activate the model, and + * thus might perform better in noisy environments. + */ + "threshold": S.optionalWith(S.Number, { nullable: true }), + /** + * Used only for `server_vad` mode. Amount of audio to include before the VAD detected speech (in + * milliseconds). Defaults to 300ms. + */ + "prefix_padding_ms": S.optionalWith(S.Int, { nullable: true }), + /** + * Used only for `server_vad` mode. Duration of silence to detect speech stop (in milliseconds). Defaults + * to 500ms. With shorter values the model will respond more quickly, + * but may jump in on short pauses from the user. + */ + "silence_duration_ms": S.optionalWith(S.Int, { nullable: true }), + /** + * Whether or not to automatically generate a response when a VAD stop event occurs. + */ + "create_response": S.optionalWith(S.Boolean, { nullable: true, default: () => true as const }), + /** + * Whether or not to automatically interrupt any ongoing response with output to the default + * conversation (i.e. `conversation` of `auto`) when a VAD start event occurs. + */ + "interrupt_response": S.optionalWith(S.Boolean, { nullable: true, default: () => true as const }), + "idle_timeout_ms": S.optionalWith(S.Int.pipe(S.greaterThanOrEqualTo(5000), S.lessThanOrEqualTo(30000)), { + nullable: true + }) + }), + /** + * Server-side semantic turn detection which uses a model to determine when the user has finished speaking. + */ + S.Struct({ + /** + * Type of turn detection, `semantic_vad` to turn on Semantic VAD. + */ + "type": S.Literal("semantic_vad"), + /** + * Used only for `semantic_vad` mode. The eagerness of the model to respond. `low` will wait longer for the user to continue speaking, `high` will respond more quickly. `auto` is the default and is equivalent to `medium`. `low`, `medium`, and `high` have max timeouts of 8s, 4s, and 2s respectively. + */ + "eagerness": S.optionalWith(S.Literal("low", "medium", "high", "auto"), { + nullable: true, + default: () => "auto" as const + }), + /** + * Whether or not to automatically generate a response when a VAD stop event occurs. + */ + "create_response": S.optionalWith(S.Boolean, { nullable: true, default: () => true as const }), + /** + * Whether or not to automatically interrupt any ongoing response with output to the default + * conversation (i.e. `conversation` of `auto`) when a VAD start event occurs. + */ + "interrupt_response": S.optionalWith(S.Boolean, { nullable: true, default: () => true as const }) + }) + ), + S.Null +) {} + +/** + * Enables tracing and sets default values for tracing configuration options. Always `auto`. + */ +export class RealtimeSessionCreateRequestGATracingEnum extends S.Literal("auto") {} + +/** + * The type of the tool, i.e. `function`. + */ +export class RealtimeFunctionToolType extends S.Literal("function") {} + +export class RealtimeFunctionTool extends S.Class("RealtimeFunctionTool")({ + /** + * The type of the tool, i.e. `function`. + */ + "type": S.optionalWith(RealtimeFunctionToolType, { nullable: true }), + /** + * The name of the function. + */ + "name": S.optionalWith(S.String, { nullable: true }), + /** + * The description of the function, including guidance on when and how + * to call it, and guidance about what to tell the user when calling + * (if anything). + */ + "description": S.optionalWith(S.String, { nullable: true }), + /** + * Parameters of the function in JSON Schema. + */ + "parameters": S.optionalWith(S.Record({ key: S.String, value: S.Unknown }), { nullable: true }) +}) {} + +/** + * Controls which (if any) tool is called by the model. + * + * `none` means the model will not call any tool and instead generates a message. + * + * `auto` means the model can pick between generating a message or calling one or + * more tools. + * + * `required` means the model must call one or more tools. + */ +export class ToolChoiceOptions extends S.Literal("none", "auto", "required") {} + +/** + * For function calling, the type is always `function`. + */ +export class ToolChoiceFunctionType extends S.Literal("function") {} + +/** + * Use this option to force the model to call a specific function. + */ +export class ToolChoiceFunction extends S.Class("ToolChoiceFunction")({ + /** + * For function calling, the type is always `function`. + */ + "type": ToolChoiceFunctionType, + /** + * The name of the function to call. + */ + "name": S.String +}) {} + +/** + * For MCP tools, the type is always `mcp`. + */ +export class ToolChoiceMCPType extends S.Literal("mcp") {} + +/** + * Use this option to force the model to call a specific tool on a remote MCP server. + */ +export class ToolChoiceMCP extends S.Class("ToolChoiceMCP")({ + /** + * For MCP tools, the type is always `mcp`. + */ + "type": ToolChoiceMCPType, + /** + * The label of the MCP server to use. + */ + "server_label": S.String, + "name": S.optionalWith(S.String, { nullable: true }) +}) {} + +export class RealtimeSessionCreateRequestGAMaxOutputTokensEnum extends S.Literal("inf") {} + +/** + * The truncation strategy to use for the session. `auto` is the default truncation strategy. `disabled` will disable truncation and emit errors when the conversation exceeds the input token limit. + */ +export class RealtimeTruncationEnum extends S.Literal("auto", "disabled") {} + +/** + * Use retention ratio truncation. + */ +export class RealtimeTruncationEnumType extends S.Literal("retention_ratio") {} + +/** + * When the number of tokens in a conversation exceeds the model's input token limit, the conversation be truncated, meaning messages (starting from the oldest) will not be included in the model's context. A 32k context model with 4,096 max output tokens can only include 28,224 tokens in the context before truncation occurs. + * Clients can configure truncation behavior to truncate with a lower max token limit, which is an effective way to control token usage and cost. + * Truncation will reduce the number of cached tokens on the next turn (busting the cache), since messages are dropped from the beginning of the context. However, clients can also configure truncation to retain messages up to a fraction of the maximum context size, which will reduce the need for future truncations and thus improve the cache rate. + * Truncation can be disabled entirely, which means the server will never truncate but would instead return an error if the conversation exceeds the model's input token limit. + */ +export class RealtimeTruncation extends S.Union( + /** + * The truncation strategy to use for the session. `auto` is the default truncation strategy. `disabled` will disable truncation and emit errors when the conversation exceeds the input token limit. + */ + RealtimeTruncationEnum, + /** + * Retain a fraction of the conversation tokens when the conversation exceeds the input token limit. This allows you to amortize truncations across multiple turns, which can help improve cached token usage. + */ + S.Struct({ + /** + * Use retention ratio truncation. + */ + "type": RealtimeTruncationEnumType, + /** + * Fraction of post-instruction conversation tokens to retain (`0.0` - `1.0`) when the conversation exceeds the input token limit. Setting this to `0.8` means that messages will be dropped until 80% of the maximum allowed tokens are used. This helps reduce the frequency of truncations and improve cache rates. + */ + "retention_ratio": S.Number.pipe(S.greaterThanOrEqualTo(0), S.lessThanOrEqualTo(1)), + /** + * Optional custom token limits for this truncation strategy. If not provided, the model's default token limits will be used. + */ + "token_limits": S.optionalWith( + S.Struct({ + /** + * Maximum tokens allowed in the conversation after instructions (which including tool definitions). For example, setting this to 5,000 would mean that truncation would occur when the conversation exceeds 5,000 tokens after instructions. This cannot be higher than the model's context window size minus the maximum output tokens. + */ + "post_instructions": S.optionalWith(S.Int.pipe(S.greaterThanOrEqualTo(0)), { nullable: true }) + }), + { nullable: true } + ) + }) +) {} + +export class ResponsePromptVariables extends S.Union( + /** + * Optional map of values to substitute in for variables in your + * prompt. The substitution values can either be strings, or other + * Response input types like images or files. + */ + S.Record({ key: S.String, value: S.Unknown }), + S.Null +) {} + +export class Prompt extends S.Union( + /** + * Reference to a prompt template and its variables. + * [Learn more](https://platform.openai.com/docs/guides/text?api-mode=responses#reusable-prompts). + */ + S.Struct({ + /** + * The unique identifier of the prompt template to use. + */ + "id": S.String, + "version": S.optionalWith(S.String, { nullable: true }), + "variables": S.optionalWith(S.Record({ key: S.String, value: S.Unknown }), { nullable: true }) + }), + S.Null +) {} + +/** + * Realtime session object configuration. + */ +export class RealtimeSessionCreateRequestGA + extends S.Class("RealtimeSessionCreateRequestGA")({ + /** + * The type of session to create. Always `realtime` for the Realtime API. + */ + "type": RealtimeSessionCreateRequestGAType, + /** + * The set of modalities the model can respond with. It defaults to `["audio"]`, indicating + * that the model will respond with audio plus a transcript. `["text"]` can be used to make + * the model respond with text only. It is not possible to request both `text` and `audio` at the same time. + */ + "output_modalities": S.optionalWith(S.Array(S.Literal("text", "audio")), { + nullable: true, + default: () => ["audio"] as const + }), + /** + * The Realtime model used for this session. + */ + "model": S.optionalWith(S.Union(S.String, RealtimeSessionCreateRequestGAModelEnum), { nullable: true }), + /** + * The default system instructions (i.e. system message) prepended to model calls. This field allows the client to guide the model on desired responses. The model can be instructed on response content and format, (e.g. "be extremely succinct", "act friendly", "here are examples of good responses") and on audio behavior (e.g. "talk quickly", "inject emotion into your voice", "laugh frequently"). The instructions are not guaranteed to be followed by the model, but they provide guidance to the model on the desired behavior. + * + * Note that the server sets default instructions which will be used if this field is not set and are visible in the `session.created` event at the start of the session. + */ + "instructions": S.optionalWith(S.String, { nullable: true }), + /** + * Configuration for input and output audio. + */ + "audio": S.optionalWith( + S.Struct({ + "input": S.optionalWith( + S.Struct({ + /** + * The format of the input audio. + */ + "format": S.optionalWith(RealtimeAudioFormats, { nullable: true }), + /** + * Configuration for input audio transcription, defaults to off and can be set to `null` to turn off once on. Input audio transcription is not native to the model, since the model consumes audio directly. Transcription runs asynchronously through [the /audio/transcriptions endpoint](https://platform.openai.com/docs/api-reference/audio/createTranscription) and should be treated as guidance of input audio content rather than precisely what the model heard. The client can optionally set the language and prompt for transcription, these offer additional guidance to the transcription service. + */ + "transcription": S.optionalWith(AudioTranscription, { nullable: true }), + /** + * Configuration for input audio noise reduction. This can be set to `null` to turn off. + * Noise reduction filters audio added to the input audio buffer before it is sent to VAD and the model. + * Filtering the audio can improve VAD and turn detection accuracy (reducing false positives) and model performance by improving perception of the input audio. + */ + "noise_reduction": S.optionalWith( + S.Struct({ + "type": S.optionalWith(NoiseReductionType, { nullable: true }) + }), + { nullable: true } + ), + "turn_detection": S.optionalWith( + S.Union( + /** + * Server-side voice activity detection (VAD) which flips on when user speech is detected and off after a period of silence. + */ + S.Struct({ + /** + * Type of turn detection, `server_vad` to turn on simple Server VAD. + */ + "type": S.Literal("server_vad").pipe( + S.propertySignature, + S.withConstructorDefault(() => "server_vad" as const) + ), + /** + * Used only for `server_vad` mode. Activation threshold for VAD (0.0 to 1.0), this defaults to 0.5. A + * higher threshold will require louder audio to activate the model, and + * thus might perform better in noisy environments. + */ + "threshold": S.optionalWith(S.Number, { nullable: true }), + /** + * Used only for `server_vad` mode. Amount of audio to include before the VAD detected speech (in + * milliseconds). Defaults to 300ms. + */ + "prefix_padding_ms": S.optionalWith(S.Int, { nullable: true }), + /** + * Used only for `server_vad` mode. Duration of silence to detect speech stop (in milliseconds). Defaults + * to 500ms. With shorter values the model will respond more quickly, + * but may jump in on short pauses from the user. + */ + "silence_duration_ms": S.optionalWith(S.Int, { nullable: true }), + /** + * Whether or not to automatically generate a response when a VAD stop event occurs. + */ + "create_response": S.optionalWith(S.Boolean, { nullable: true, default: () => true as const }), + /** + * Whether or not to automatically interrupt any ongoing response with output to the default + * conversation (i.e. `conversation` of `auto`) when a VAD start event occurs. + */ + "interrupt_response": S.optionalWith(S.Boolean, { nullable: true, default: () => true as const }), + "idle_timeout_ms": S.optionalWith( + S.Int.pipe(S.greaterThanOrEqualTo(5000), S.lessThanOrEqualTo(30000)), + { nullable: true } + ) + }), + /** + * Server-side semantic turn detection which uses a model to determine when the user has finished speaking. + */ + S.Struct({ + /** + * Type of turn detection, `semantic_vad` to turn on Semantic VAD. + */ + "type": S.Literal("semantic_vad"), + /** + * Used only for `semantic_vad` mode. The eagerness of the model to respond. `low` will wait longer for the user to continue speaking, `high` will respond more quickly. `auto` is the default and is equivalent to `medium`. `low`, `medium`, and `high` have max timeouts of 8s, 4s, and 2s respectively. + */ + "eagerness": S.optionalWith(S.Literal("low", "medium", "high", "auto"), { + nullable: true, + default: () => "auto" as const + }), + /** + * Whether or not to automatically generate a response when a VAD stop event occurs. + */ + "create_response": S.optionalWith(S.Boolean, { nullable: true, default: () => true as const }), + /** + * Whether or not to automatically interrupt any ongoing response with output to the default + * conversation (i.e. `conversation` of `auto`) when a VAD start event occurs. + */ + "interrupt_response": S.optionalWith(S.Boolean, { nullable: true, default: () => true as const }) + }) + ), + { nullable: true } + ) + }), + { nullable: true } + ), + "output": S.optionalWith( + S.Struct({ + /** + * The format of the output audio. + */ + "format": S.optionalWith(RealtimeAudioFormats, { nullable: true }), + /** + * The voice the model uses to respond. Voice cannot be changed during the + * session once the model has responded with audio at least once. Current + * voice options are `alloy`, `ash`, `ballad`, `coral`, `echo`, `sage`, + * `shimmer`, `verse`, `marin`, and `cedar`. We recommend `marin` and `cedar` for + * best quality. + */ + "voice": S.optionalWith(VoiceIdsShared, { nullable: true }), + /** + * The speed of the model's spoken response as a multiple of the original speed. + * 1.0 is the default speed. 0.25 is the minimum speed. 1.5 is the maximum speed. This value can only be changed in between model turns, not while a response is in progress. + * + * This parameter is a post-processing adjustment to the audio after it is generated, it's + * also possible to prompt the model to speak faster or slower. + */ + "speed": S.optionalWith(S.Number.pipe(S.greaterThanOrEqualTo(0.25), S.lessThanOrEqualTo(1.5)), { + nullable: true, + default: () => 1 as const + }) + }), + { nullable: true } + ) + }), + { nullable: true } + ), + /** + * Additional fields to include in server outputs. + * + * `item.input_audio_transcription.logprobs`: Include logprobs for input audio transcription. + */ + "include": S.optionalWith(S.Array(S.Literal("item.input_audio_transcription.logprobs")), { nullable: true }), + /** + * Realtime API can write session traces to the [Traces Dashboard](/logs?api=traces). Set to null to disable tracing. Once + * tracing is enabled for a session, the configuration cannot be modified. + * + * `auto` will create a trace for the session with default values for the + * workflow name, group id, and metadata. + */ + "tracing": S.optionalWith( + S.Union( + /** + * Enables tracing and sets default values for tracing configuration options. Always `auto`. + */ + RealtimeSessionCreateRequestGATracingEnum, + /** + * Granular configuration for tracing. + */ + S.Struct({ + /** + * The name of the workflow to attach to this trace. This is used to + * name the trace in the Traces Dashboard. + */ + "workflow_name": S.optionalWith(S.String, { nullable: true }), + /** + * The group id to attach to this trace to enable filtering and + * grouping in the Traces Dashboard. + */ + "group_id": S.optionalWith(S.String, { nullable: true }), + /** + * The arbitrary metadata to attach to this trace to enable + * filtering in the Traces Dashboard. + */ + "metadata": S.optionalWith(S.Record({ key: S.String, value: S.Unknown }), { nullable: true }) + }) + ), + { nullable: true } + ), + /** + * Tools available to the model. + */ + "tools": S.optionalWith(S.Array(S.Union(RealtimeFunctionTool, MCPTool)), { nullable: true }), + /** + * How the model chooses tools. Provide one of the string modes or force a specific + * function/MCP tool. + */ + "tool_choice": S.optionalWith(S.Union(ToolChoiceOptions, ToolChoiceFunction, ToolChoiceMCP), { + nullable: true, + default: () => "auto" as const + }), + /** + * Maximum number of output tokens for a single assistant response, + * inclusive of tool calls. Provide an integer between 1 and 4096 to + * limit output tokens, or `inf` for the maximum available tokens for a + * given model. Defaults to `inf`. + */ + "max_output_tokens": S.optionalWith(S.Union(S.Int, RealtimeSessionCreateRequestGAMaxOutputTokensEnum), { + nullable: true + }), + "truncation": S.optionalWith(RealtimeTruncation, { nullable: true }), + "prompt": S.optionalWith( + S.Struct({ + /** + * The unique identifier of the prompt template to use. + */ + "id": S.String, + "version": S.optionalWith(S.String, { nullable: true }), + "variables": S.optionalWith(S.Record({ key: S.String, value: S.Unknown }), { nullable: true }) + }), + { nullable: true } + ) + }) +{} + +/** + * Parameters required to initiate a realtime call and receive the SDP answer + * needed to complete a WebRTC peer connection. Provide an SDP offer generated + * by your client and optionally configure the session that will answer the call. + */ +export class RealtimeCallCreateRequest extends S.Class("RealtimeCallCreateRequest")({ + /** + * WebRTC Session Description Protocol (SDP) offer generated by the caller. + */ + "sdp": S.String, + /** + * Optional session configuration to apply before the realtime session is + * created. Use the same parameters you would send in a [`create client secret`](https://platform.openai.com/docs/api-reference/realtime-sessions/create-realtime-client-secret) + * request. + */ + "session": S.optionalWith(RealtimeSessionCreateRequestGA, { nullable: true }) +}) {} + +/** + * Parameters required to transfer a SIP call to a new destination using the + * Realtime API. + */ +export class RealtimeCallReferRequest extends S.Class("RealtimeCallReferRequest")({ + /** + * URI that should appear in the SIP Refer-To header. Supports values like + * `tel:+14155550123` or `sip:agent@example.com`. + */ + "target_uri": S.String +}) {} + +/** + * Parameters used to decline an incoming SIP call handled by the Realtime API. + */ +export class RealtimeCallRejectRequest extends S.Class("RealtimeCallRejectRequest")({ + /** + * SIP response code to send back to the caller. Defaults to `603` (Decline) + * when omitted. + */ + "status_code": S.optionalWith(S.Int, { nullable: true }) +}) {} + +/** + * The anchor point for the client secret expiration, meaning that `seconds` will be added to the `created_at` time of the client secret to produce an expiration timestamp. Only `created_at` is currently supported. + */ +export class RealtimeCreateClientSecretRequestExpiresAfterAnchor extends S.Literal("created_at") {} + +/** + * The type of session to create. Always `transcription` for transcription sessions. + */ +export class RealtimeTranscriptionSessionCreateRequestGAType extends S.Literal("transcription") {} + +/** + * Realtime transcription session object configuration. + */ +export class RealtimeTranscriptionSessionCreateRequestGA + extends S.Class("RealtimeTranscriptionSessionCreateRequestGA")({ + /** + * The type of session to create. Always `transcription` for transcription sessions. + */ + "type": RealtimeTranscriptionSessionCreateRequestGAType, + /** + * Configuration for input and output audio. + */ + "audio": S.optionalWith( + S.Struct({ + "input": S.optionalWith( + S.Struct({ + "format": S.optionalWith(RealtimeAudioFormats, { nullable: true }), + /** + * Configuration for input audio transcription, defaults to off and can be set to `null` to turn off once on. Input audio transcription is not native to the model, since the model consumes audio directly. Transcription runs asynchronously through [the /audio/transcriptions endpoint](https://platform.openai.com/docs/api-reference/audio/createTranscription) and should be treated as guidance of input audio content rather than precisely what the model heard. The client can optionally set the language and prompt for transcription, these offer additional guidance to the transcription service. + */ + "transcription": S.optionalWith(AudioTranscription, { nullable: true }), + /** + * Configuration for input audio noise reduction. This can be set to `null` to turn off. + * Noise reduction filters audio added to the input audio buffer before it is sent to VAD and the model. + * Filtering the audio can improve VAD and turn detection accuracy (reducing false positives) and model performance by improving perception of the input audio. + */ + "noise_reduction": S.optionalWith( + S.Struct({ + "type": S.optionalWith(NoiseReductionType, { nullable: true }) + }), + { nullable: true } + ), + "turn_detection": S.optionalWith( + S.Union( + /** + * Server-side voice activity detection (VAD) which flips on when user speech is detected and off after a period of silence. + */ + S.Struct({ + /** + * Type of turn detection, `server_vad` to turn on simple Server VAD. + */ + "type": S.Literal("server_vad").pipe( + S.propertySignature, + S.withConstructorDefault(() => "server_vad" as const) + ), + /** + * Used only for `server_vad` mode. Activation threshold for VAD (0.0 to 1.0), this defaults to 0.5. A + * higher threshold will require louder audio to activate the model, and + * thus might perform better in noisy environments. + */ + "threshold": S.optionalWith(S.Number, { nullable: true }), + /** + * Used only for `server_vad` mode. Amount of audio to include before the VAD detected speech (in + * milliseconds). Defaults to 300ms. + */ + "prefix_padding_ms": S.optionalWith(S.Int, { nullable: true }), + /** + * Used only for `server_vad` mode. Duration of silence to detect speech stop (in milliseconds). Defaults + * to 500ms. With shorter values the model will respond more quickly, + * but may jump in on short pauses from the user. + */ + "silence_duration_ms": S.optionalWith(S.Int, { nullable: true }), + /** + * Whether or not to automatically generate a response when a VAD stop event occurs. + */ + "create_response": S.optionalWith(S.Boolean, { nullable: true, default: () => true as const }), + /** + * Whether or not to automatically interrupt any ongoing response with output to the default + * conversation (i.e. `conversation` of `auto`) when a VAD start event occurs. + */ + "interrupt_response": S.optionalWith(S.Boolean, { nullable: true, default: () => true as const }), + "idle_timeout_ms": S.optionalWith( + S.Int.pipe(S.greaterThanOrEqualTo(5000), S.lessThanOrEqualTo(30000)), + { nullable: true } + ) + }), + /** + * Server-side semantic turn detection which uses a model to determine when the user has finished speaking. + */ + S.Struct({ + /** + * Type of turn detection, `semantic_vad` to turn on Semantic VAD. + */ + "type": S.Literal("semantic_vad"), + /** + * Used only for `semantic_vad` mode. The eagerness of the model to respond. `low` will wait longer for the user to continue speaking, `high` will respond more quickly. `auto` is the default and is equivalent to `medium`. `low`, `medium`, and `high` have max timeouts of 8s, 4s, and 2s respectively. + */ + "eagerness": S.optionalWith(S.Literal("low", "medium", "high", "auto"), { + nullable: true, + default: () => "auto" as const + }), + /** + * Whether or not to automatically generate a response when a VAD stop event occurs. + */ + "create_response": S.optionalWith(S.Boolean, { nullable: true, default: () => true as const }), + /** + * Whether or not to automatically interrupt any ongoing response with output to the default + * conversation (i.e. `conversation` of `auto`) when a VAD start event occurs. + */ + "interrupt_response": S.optionalWith(S.Boolean, { nullable: true, default: () => true as const }) + }) + ), + { nullable: true } + ) + }), + { nullable: true } + ) + }), + { nullable: true } + ), + /** + * Additional fields to include in server outputs. + * + * `item.input_audio_transcription.logprobs`: Include logprobs for input audio transcription. + */ + "include": S.optionalWith(S.Array(S.Literal("item.input_audio_transcription.logprobs")), { nullable: true }) + }) +{} + +/** + * Create a session and client secret for the Realtime API. The request can specify + * either a realtime or a transcription session configuration. + * [Learn more about the Realtime API](https://platform.openai.com/docs/guides/realtime). + */ +export class RealtimeCreateClientSecretRequest + extends S.Class("RealtimeCreateClientSecretRequest")({ + /** + * Configuration for the client secret expiration. Expiration refers to the time after which + * a client secret will no longer be valid for creating sessions. The session itself may + * continue after that time once started. A secret can be used to create multiple sessions + * until it expires. + */ + "expires_after": S.optionalWith( + S.Struct({ + /** + * The anchor point for the client secret expiration, meaning that `seconds` will be added to the `created_at` time of the client secret to produce an expiration timestamp. Only `created_at` is currently supported. + */ + "anchor": S.optionalWith(RealtimeCreateClientSecretRequestExpiresAfterAnchor, { + nullable: true, + default: () => "created_at" as const + }), + /** + * The number of seconds from the anchor point to the expiration. Select a value between `10` and `7200` (2 hours). This default to 600 seconds (10 minutes) if not specified. + */ + "seconds": S.optionalWith(S.Int.pipe(S.greaterThanOrEqualTo(10), S.lessThanOrEqualTo(7200)), { + nullable: true, + default: () => 600 as const + }) + }), + { nullable: true } + ), + /** + * Session configuration to use for the client secret. Choose either a realtime + * session or a transcription session. + */ + "session": S.optionalWith(S.Union(RealtimeSessionCreateRequestGA, RealtimeTranscriptionSessionCreateRequestGA), { + nullable: true + }) + }) +{} + +/** + * The type of session to create. Always `realtime` for the Realtime API. + */ +export class RealtimeSessionCreateResponseGAType extends S.Literal("realtime") {} + +export class RealtimeSessionCreateResponseGAModelEnum extends S.Literal( + "gpt-realtime", + "gpt-realtime-2025-08-28", + "gpt-4o-realtime-preview", + "gpt-4o-realtime-preview-2024-10-01", + "gpt-4o-realtime-preview-2024-12-17", + "gpt-4o-realtime-preview-2025-06-03", + "gpt-4o-mini-realtime-preview", + "gpt-4o-mini-realtime-preview-2024-12-17", + "gpt-realtime-mini", + "gpt-realtime-mini-2025-10-06", + "gpt-audio-mini", + "gpt-audio-mini-2025-10-06" +) {} + +/** + * Enables tracing and sets default values for tracing configuration options. Always `auto`. + */ +export class RealtimeSessionCreateResponseGATracingEnum extends S.Literal("auto") {} + +export class RealtimeSessionCreateResponseGAMaxOutputTokensEnum extends S.Literal("inf") {} + +/** + * A new Realtime session configuration, with an ephemeral key. Default TTL + * for keys is one minute. + */ +export class RealtimeSessionCreateResponseGA + extends S.Class("RealtimeSessionCreateResponseGA")({ + /** + * Ephemeral key returned by the API. + */ + "client_secret": S.Struct({ + /** + * Ephemeral key usable in client environments to authenticate connections to the Realtime API. Use this in client-side environments rather than a standard API token, which should only be used server-side. + */ + "value": S.String, + /** + * Timestamp for when the token expires. Currently, all tokens expire + * after one minute. + */ + "expires_at": S.Int + }), + /** + * The type of session to create. Always `realtime` for the Realtime API. + */ + "type": RealtimeSessionCreateResponseGAType, + /** + * The set of modalities the model can respond with. It defaults to `["audio"]`, indicating + * that the model will respond with audio plus a transcript. `["text"]` can be used to make + * the model respond with text only. It is not possible to request both `text` and `audio` at the same time. + */ + "output_modalities": S.optionalWith(S.Array(S.Literal("text", "audio")), { + nullable: true, + default: () => ["audio"] as const + }), + /** + * The Realtime model used for this session. + */ + "model": S.optionalWith(S.Union(S.String, RealtimeSessionCreateResponseGAModelEnum), { nullable: true }), + /** + * The default system instructions (i.e. system message) prepended to model calls. This field allows the client to guide the model on desired responses. The model can be instructed on response content and format, (e.g. "be extremely succinct", "act friendly", "here are examples of good responses") and on audio behavior (e.g. "talk quickly", "inject emotion into your voice", "laugh frequently"). The instructions are not guaranteed to be followed by the model, but they provide guidance to the model on the desired behavior. + * + * Note that the server sets default instructions which will be used if this field is not set and are visible in the `session.created` event at the start of the session. + */ + "instructions": S.optionalWith(S.String, { nullable: true }), + /** + * Configuration for input and output audio. + */ + "audio": S.optionalWith( + S.Struct({ + "input": S.optionalWith( + S.Struct({ + /** + * The format of the input audio. + */ + "format": S.optionalWith(RealtimeAudioFormats, { nullable: true }), + /** + * Configuration for input audio transcription, defaults to off and can be set to `null` to turn off once on. Input audio transcription is not native to the model, since the model consumes audio directly. Transcription runs asynchronously through [the /audio/transcriptions endpoint](https://platform.openai.com/docs/api-reference/audio/createTranscription) and should be treated as guidance of input audio content rather than precisely what the model heard. The client can optionally set the language and prompt for transcription, these offer additional guidance to the transcription service. + */ + "transcription": S.optionalWith(AudioTranscription, { nullable: true }), + /** + * Configuration for input audio noise reduction. This can be set to `null` to turn off. + * Noise reduction filters audio added to the input audio buffer before it is sent to VAD and the model. + * Filtering the audio can improve VAD and turn detection accuracy (reducing false positives) and model performance by improving perception of the input audio. + */ + "noise_reduction": S.optionalWith( + S.Struct({ + "type": S.optionalWith(NoiseReductionType, { nullable: true }) + }), + { nullable: true } + ), + "turn_detection": S.optionalWith( + S.Union( + /** + * Server-side voice activity detection (VAD) which flips on when user speech is detected and off after a period of silence. + */ + S.Struct({ + /** + * Type of turn detection, `server_vad` to turn on simple Server VAD. + */ + "type": S.Literal("server_vad").pipe( + S.propertySignature, + S.withConstructorDefault(() => "server_vad" as const) + ), + /** + * Used only for `server_vad` mode. Activation threshold for VAD (0.0 to 1.0), this defaults to 0.5. A + * higher threshold will require louder audio to activate the model, and + * thus might perform better in noisy environments. + */ + "threshold": S.optionalWith(S.Number, { nullable: true }), + /** + * Used only for `server_vad` mode. Amount of audio to include before the VAD detected speech (in + * milliseconds). Defaults to 300ms. + */ + "prefix_padding_ms": S.optionalWith(S.Int, { nullable: true }), + /** + * Used only for `server_vad` mode. Duration of silence to detect speech stop (in milliseconds). Defaults + * to 500ms. With shorter values the model will respond more quickly, + * but may jump in on short pauses from the user. + */ + "silence_duration_ms": S.optionalWith(S.Int, { nullable: true }), + /** + * Whether or not to automatically generate a response when a VAD stop event occurs. + */ + "create_response": S.optionalWith(S.Boolean, { nullable: true, default: () => true as const }), + /** + * Whether or not to automatically interrupt any ongoing response with output to the default + * conversation (i.e. `conversation` of `auto`) when a VAD start event occurs. + */ + "interrupt_response": S.optionalWith(S.Boolean, { nullable: true, default: () => true as const }), + "idle_timeout_ms": S.optionalWith( + S.Int.pipe(S.greaterThanOrEqualTo(5000), S.lessThanOrEqualTo(30000)), + { nullable: true } + ) + }), + /** + * Server-side semantic turn detection which uses a model to determine when the user has finished speaking. + */ + S.Struct({ + /** + * Type of turn detection, `semantic_vad` to turn on Semantic VAD. + */ + "type": S.Literal("semantic_vad"), + /** + * Used only for `semantic_vad` mode. The eagerness of the model to respond. `low` will wait longer for the user to continue speaking, `high` will respond more quickly. `auto` is the default and is equivalent to `medium`. `low`, `medium`, and `high` have max timeouts of 8s, 4s, and 2s respectively. + */ + "eagerness": S.optionalWith(S.Literal("low", "medium", "high", "auto"), { + nullable: true, + default: () => "auto" as const + }), + /** + * Whether or not to automatically generate a response when a VAD stop event occurs. + */ + "create_response": S.optionalWith(S.Boolean, { nullable: true, default: () => true as const }), + /** + * Whether or not to automatically interrupt any ongoing response with output to the default + * conversation (i.e. `conversation` of `auto`) when a VAD start event occurs. + */ + "interrupt_response": S.optionalWith(S.Boolean, { nullable: true, default: () => true as const }) + }) + ), + { nullable: true } + ) + }), + { nullable: true } + ), + "output": S.optionalWith( + S.Struct({ + /** + * The format of the output audio. + */ + "format": S.optionalWith(RealtimeAudioFormats, { nullable: true }), + /** + * The voice the model uses to respond. Voice cannot be changed during the + * session once the model has responded with audio at least once. Current + * voice options are `alloy`, `ash`, `ballad`, `coral`, `echo`, `sage`, + * `shimmer`, `verse`, `marin`, and `cedar`. We recommend `marin` and `cedar` for + * best quality. + */ + "voice": S.optionalWith(VoiceIdsShared, { nullable: true }), + /** + * The speed of the model's spoken response as a multiple of the original speed. + * 1.0 is the default speed. 0.25 is the minimum speed. 1.5 is the maximum speed. This value can only be changed in between model turns, not while a response is in progress. + * + * This parameter is a post-processing adjustment to the audio after it is generated, it's + * also possible to prompt the model to speak faster or slower. + */ + "speed": S.optionalWith(S.Number.pipe(S.greaterThanOrEqualTo(0.25), S.lessThanOrEqualTo(1.5)), { + nullable: true, + default: () => 1 as const + }) + }), + { nullable: true } + ) + }), + { nullable: true } + ), + /** + * Additional fields to include in server outputs. + * + * `item.input_audio_transcription.logprobs`: Include logprobs for input audio transcription. + */ + "include": S.optionalWith(S.Array(S.Literal("item.input_audio_transcription.logprobs")), { nullable: true }), + "tracing": S.optionalWith( + S.Union( + /** + * Enables tracing and sets default values for tracing configuration options. Always `auto`. + */ + RealtimeSessionCreateResponseGATracingEnum, + /** + * Granular configuration for tracing. + */ + S.Struct({ + /** + * The name of the workflow to attach to this trace. This is used to + * name the trace in the Traces Dashboard. + */ + "workflow_name": S.optionalWith(S.String, { nullable: true }), + /** + * The group id to attach to this trace to enable filtering and + * grouping in the Traces Dashboard. + */ + "group_id": S.optionalWith(S.String, { nullable: true }), + /** + * The arbitrary metadata to attach to this trace to enable + * filtering in the Traces Dashboard. + */ + "metadata": S.optionalWith(S.Record({ key: S.String, value: S.Unknown }), { nullable: true }) + }) + ), + { nullable: true } + ), + /** + * Tools available to the model. + */ + "tools": S.optionalWith(S.Array(S.Union(RealtimeFunctionTool, MCPTool)), { nullable: true }), + /** + * How the model chooses tools. Provide one of the string modes or force a specific + * function/MCP tool. + */ + "tool_choice": S.optionalWith(S.Union(ToolChoiceOptions, ToolChoiceFunction, ToolChoiceMCP), { + nullable: true, + default: () => "auto" as const + }), + /** + * Maximum number of output tokens for a single assistant response, + * inclusive of tool calls. Provide an integer between 1 and 4096 to + * limit output tokens, or `inf` for the maximum available tokens for a + * given model. Defaults to `inf`. + */ + "max_output_tokens": S.optionalWith(S.Union(S.Int, RealtimeSessionCreateResponseGAMaxOutputTokensEnum), { + nullable: true + }), + "truncation": S.optionalWith(RealtimeTruncation, { nullable: true }), + "prompt": S.optionalWith( + S.Struct({ + /** + * The unique identifier of the prompt template to use. + */ + "id": S.String, + "version": S.optionalWith(S.String, { nullable: true }), + "variables": S.optionalWith(S.Record({ key: S.String, value: S.Unknown }), { nullable: true }) + }), + { nullable: true } + ) + }) +{} + +/** + * The type of session. Always `transcription` for transcription sessions. + */ +export class RealtimeTranscriptionSessionCreateResponseGAType extends S.Literal("transcription") {} + +/** + * A Realtime transcription session configuration object. + */ +export class RealtimeTranscriptionSessionCreateResponseGA + extends S.Class("RealtimeTranscriptionSessionCreateResponseGA")({ + /** + * The type of session. Always `transcription` for transcription sessions. + */ + "type": RealtimeTranscriptionSessionCreateResponseGAType, + /** + * Unique identifier for the session that looks like `sess_1234567890abcdef`. + */ + "id": S.String, + /** + * The object type. Always `realtime.transcription_session`. + */ + "object": S.String, + /** + * Expiration timestamp for the session, in seconds since epoch. + */ + "expires_at": S.optionalWith(S.Int, { nullable: true }), + /** + * Additional fields to include in server outputs. + * - `item.input_audio_transcription.logprobs`: Include logprobs for input audio transcription. + */ + "include": S.optionalWith(S.Array(S.Literal("item.input_audio_transcription.logprobs")), { nullable: true }), + /** + * Configuration for input audio for the session. + */ + "audio": S.optionalWith( + S.Struct({ + "input": S.optionalWith( + S.Struct({ + "format": S.optionalWith(RealtimeAudioFormats, { nullable: true }), + /** + * Configuration of the transcription model. + */ + "transcription": S.optionalWith(AudioTranscription, { nullable: true }), + /** + * Configuration for input audio noise reduction. + */ + "noise_reduction": S.optionalWith( + S.Struct({ + "type": S.optionalWith(NoiseReductionType, { nullable: true }) + }), + { nullable: true } + ), + /** + * Configuration for turn detection. Can be set to `null` to turn off. Server + * VAD means that the model will detect the start and end of speech based on + * audio volume and respond at the end of user speech. + */ + "turn_detection": S.optionalWith( + S.Struct({ + /** + * Type of turn detection, only `server_vad` is currently supported. + */ + "type": S.optionalWith(S.String, { nullable: true }), + /** + * Activation threshold for VAD (0.0 to 1.0), this defaults to 0.5. A + * higher threshold will require louder audio to activate the model, and + * thus might perform better in noisy environments. + */ + "threshold": S.optionalWith(S.Number, { nullable: true }), + /** + * Amount of audio to include before the VAD detected speech (in + * milliseconds). Defaults to 300ms. + */ + "prefix_padding_ms": S.optionalWith(S.Int, { nullable: true }), + /** + * Duration of silence to detect speech stop (in milliseconds). Defaults + * to 500ms. With shorter values the model will respond more quickly, + * but may jump in on short pauses from the user. + */ + "silence_duration_ms": S.optionalWith(S.Int, { nullable: true }) + }), + { nullable: true } + ) + }), + { nullable: true } + ) + }), + { nullable: true } + ) + }) +{} + +/** + * Response from creating a session and client secret for the Realtime API. + */ +export class RealtimeCreateClientSecretResponse + extends S.Class("RealtimeCreateClientSecretResponse")({ + /** + * The generated client secret value. + */ + "value": S.String, + /** + * Expiration timestamp for the client secret, in seconds since epoch. + */ + "expires_at": S.Int, + /** + * The session configuration for either a realtime or transcription session. + */ + "session": S.Union(RealtimeSessionCreateResponseGA, RealtimeTranscriptionSessionCreateResponseGA) + }) +{} + +/** + * Default tracing mode for the session. + */ +export class RealtimeSessionCreateRequestTracingEnum extends S.Literal("auto") {} + +export class RealtimeSessionCreateRequestMaxResponseOutputTokensEnum extends S.Literal("inf") {} + +/** + * A new Realtime session configuration, with an ephemeral key. Default TTL + * for keys is one minute. + */ +export class RealtimeSessionCreateRequest + extends S.Class("RealtimeSessionCreateRequest")({ + /** + * Ephemeral key returned by the API. + */ + "client_secret": S.Struct({ + /** + * Ephemeral key usable in client environments to authenticate connections + * to the Realtime API. Use this in client-side environments rather than + * a standard API token, which should only be used server-side. + */ + "value": S.String, + /** + * Timestamp for when the token expires. Currently, all tokens expire + * after one minute. + */ + "expires_at": S.Int + }), + /** + * The default system instructions (i.e. system message) prepended to model calls. This field allows the client to guide the model on desired responses. The model can be instructed on response content and format, (e.g. "be extremely succinct", "act friendly", "here are examples of good responses") and on audio behavior (e.g. "talk quickly", "inject emotion into your voice", "laugh frequently"). The instructions are not guaranteed to be followed by the model, but they provide guidance to the model on the desired behavior. + * Note that the server sets default instructions which will be used if this field is not set and are visible in the `session.created` event at the start of the session. + */ + "instructions": S.optionalWith(S.String, { nullable: true }), + /** + * The voice the model uses to respond. Voice cannot be changed during the + * session once the model has responded with audio at least once. Current + * voice options are `alloy`, `ash`, `ballad`, `coral`, `echo`, `sage`, + * `shimmer`, and `verse`. + */ + "voice": S.optionalWith(VoiceIdsShared, { nullable: true }), + /** + * The format of input audio. Options are `pcm16`, `g711_ulaw`, or `g711_alaw`. + */ + "input_audio_format": S.optionalWith(S.String, { nullable: true }), + /** + * The format of output audio. Options are `pcm16`, `g711_ulaw`, or `g711_alaw`. + */ + "output_audio_format": S.optionalWith(S.String, { nullable: true }), + /** + * Configuration for input audio transcription, defaults to off and can be + * set to `null` to turn off once on. Input audio transcription is not native + * to the model, since the model consumes audio directly. Transcription runs + * asynchronously and should be treated as rough guidance + * rather than the representation understood by the model. + */ + "input_audio_transcription": S.optionalWith( + S.Struct({ + /** + * The model to use for transcription. + */ + "model": S.optionalWith(S.String, { nullable: true }) + }), + { nullable: true } + ), + /** + * The speed of the model's spoken response. 1.0 is the default speed. 0.25 is + * the minimum speed. 1.5 is the maximum speed. This value can only be changed + * in between model turns, not while a response is in progress. + */ + "speed": S.optionalWith(S.Number.pipe(S.greaterThanOrEqualTo(0.25), S.lessThanOrEqualTo(1.5)), { + nullable: true, + default: () => 1 as const + }), + /** + * Configuration options for tracing. Set to null to disable tracing. Once + * tracing is enabled for a session, the configuration cannot be modified. + * + * `auto` will create a trace for the session with default values for the + * workflow name, group id, and metadata. + */ + "tracing": S.optionalWith( + S.Union( + /** + * Default tracing mode for the session. + */ + RealtimeSessionCreateRequestTracingEnum, + /** + * Granular configuration for tracing. + */ + S.Struct({ + /** + * The name of the workflow to attach to this trace. This is used to + * name the trace in the traces dashboard. + */ + "workflow_name": S.optionalWith(S.String, { nullable: true }), + /** + * The group id to attach to this trace to enable filtering and + * grouping in the traces dashboard. + */ + "group_id": S.optionalWith(S.String, { nullable: true }), + /** + * The arbitrary metadata to attach to this trace to enable + * filtering in the traces dashboard. + */ + "metadata": S.optionalWith(S.Record({ key: S.String, value: S.Unknown }), { nullable: true }) + }) + ), + { nullable: true } + ), + /** + * Configuration for turn detection. Can be set to `null` to turn off. Server + * VAD means that the model will detect the start and end of speech based on + * audio volume and respond at the end of user speech. + */ + "turn_detection": S.optionalWith( + S.Struct({ + /** + * Type of turn detection, only `server_vad` is currently supported. + */ + "type": S.optionalWith(S.String, { nullable: true }), + /** + * Activation threshold for VAD (0.0 to 1.0), this defaults to 0.5. A + * higher threshold will require louder audio to activate the model, and + * thus might perform better in noisy environments. + */ + "threshold": S.optionalWith(S.Number, { nullable: true }), + /** + * Amount of audio to include before the VAD detected speech (in + * milliseconds). Defaults to 300ms. + */ + "prefix_padding_ms": S.optionalWith(S.Int, { nullable: true }), + /** + * Duration of silence to detect speech stop (in milliseconds). Defaults + * to 500ms. With shorter values the model will respond more quickly, + * but may jump in on short pauses from the user. + */ + "silence_duration_ms": S.optionalWith(S.Int, { nullable: true }) + }), + { nullable: true } + ), + /** + * Tools (functions) available to the model. + */ + "tools": S.optionalWith( + S.Array(S.Struct({ + /** + * The type of the tool, i.e. `function`. + */ + "type": S.optionalWith(S.Literal("function"), { nullable: true }), + /** + * The name of the function. + */ + "name": S.optionalWith(S.String, { nullable: true }), + /** + * The description of the function, including guidance on when and how + * to call it, and guidance about what to tell the user when calling + * (if anything). + */ + "description": S.optionalWith(S.String, { nullable: true }), + /** + * Parameters of the function in JSON Schema. + */ + "parameters": S.optionalWith(S.Record({ key: S.String, value: S.Unknown }), { nullable: true }) + })), + { nullable: true } + ), + /** + * How the model chooses tools. Options are `auto`, `none`, `required`, or + * specify a function. + */ + "tool_choice": S.optionalWith(S.String, { nullable: true }), + /** + * Sampling temperature for the model, limited to [0.6, 1.2]. Defaults to 0.8. + */ + "temperature": S.optionalWith(S.Number, { nullable: true }), + /** + * Maximum number of output tokens for a single assistant response, + * inclusive of tool calls. Provide an integer between 1 and 4096 to + * limit output tokens, or `inf` for the maximum available tokens for a + * given model. Defaults to `inf`. + */ + "max_response_output_tokens": S.optionalWith( + S.Union(S.Int, RealtimeSessionCreateRequestMaxResponseOutputTokensEnum), + { nullable: true } + ), + "truncation": S.optionalWith(RealtimeTruncation, { nullable: true }), + "prompt": S.optionalWith( + S.Struct({ + /** + * The unique identifier of the prompt template to use. + */ + "id": S.String, + "version": S.optionalWith(S.String, { nullable: true }), + "variables": S.optionalWith(S.Record({ key: S.String, value: S.Unknown }), { nullable: true }) + }), + { nullable: true } + ) + }) +{} + +/** + * Default tracing mode for the session. + */ +export class RealtimeSessionCreateResponseTracingEnum extends S.Literal("auto") {} + +export class RealtimeSessionCreateResponseMaxOutputTokensEnum extends S.Literal("inf") {} + +/** + * A Realtime session configuration object. + */ +export class RealtimeSessionCreateResponse + extends S.Class("RealtimeSessionCreateResponse")({ + /** + * Unique identifier for the session that looks like `sess_1234567890abcdef`. + */ + "id": S.optionalWith(S.String, { nullable: true }), + /** + * The object type. Always `realtime.session`. + */ + "object": S.optionalWith(S.String, { nullable: true }), + /** + * Expiration timestamp for the session, in seconds since epoch. + */ + "expires_at": S.optionalWith(S.Int, { nullable: true }), + /** + * Additional fields to include in server outputs. + * - `item.input_audio_transcription.logprobs`: Include logprobs for input audio transcription. + */ + "include": S.optionalWith(S.Array(S.Literal("item.input_audio_transcription.logprobs")), { nullable: true }), + /** + * The Realtime model used for this session. + */ + "model": S.optionalWith(S.String, { nullable: true }), + /** + * The default system instructions (i.e. system message) prepended to model + * calls. This field allows the client to guide the model on desired + * responses. The model can be instructed on response content and format, + * (e.g. "be extremely succinct", "act friendly", "here are examples of good + * responses") and on audio behavior (e.g. "talk quickly", "inject emotion + * into your voice", "laugh frequently"). The instructions are not guaranteed + * to be followed by the model, but they provide guidance to the model on the + * desired behavior. + * + * Note that the server sets default instructions which will be used if this + * field is not set and are visible in the `session.created` event at the + * start of the session. + */ + "instructions": S.optionalWith(S.String, { nullable: true }), + /** + * Configuration for input and output audio for the session. + */ + "audio": S.optionalWith( + S.Struct({ + "input": S.optionalWith( + S.Struct({ + "format": S.optionalWith(RealtimeAudioFormats, { nullable: true }), + /** + * Configuration for input audio transcription. + */ + "transcription": S.optionalWith(AudioTranscription, { nullable: true }), + /** + * Configuration for input audio noise reduction. + */ + "noise_reduction": S.optionalWith( + S.Struct({ + "type": S.optionalWith(NoiseReductionType, { nullable: true }) + }), + { nullable: true } + ), + /** + * Configuration for turn detection. + */ + "turn_detection": S.optionalWith( + S.Struct({ + /** + * Type of turn detection, only `server_vad` is currently supported. + */ + "type": S.optionalWith(S.String, { nullable: true }), + "threshold": S.optionalWith(S.Number, { nullable: true }), + "prefix_padding_ms": S.optionalWith(S.Int, { nullable: true }), + "silence_duration_ms": S.optionalWith(S.Int, { nullable: true }) + }), + { nullable: true } + ) + }), + { nullable: true } + ), + "output": S.optionalWith( + S.Struct({ + "format": S.optionalWith(RealtimeAudioFormats, { nullable: true }), + "voice": S.optionalWith(VoiceIdsShared, { nullable: true }), + "speed": S.optionalWith(S.Number, { nullable: true }) + }), + { nullable: true } + ) + }), + { nullable: true } + ), + /** + * Configuration options for tracing. Set to null to disable tracing. Once + * tracing is enabled for a session, the configuration cannot be modified. + * + * `auto` will create a trace for the session with default values for the + * workflow name, group id, and metadata. + */ + "tracing": S.optionalWith( + S.Union( + /** + * Default tracing mode for the session. + */ + RealtimeSessionCreateResponseTracingEnum, + /** + * Granular configuration for tracing. + */ + S.Struct({ + /** + * The name of the workflow to attach to this trace. This is used to + * name the trace in the traces dashboard. + */ + "workflow_name": S.optionalWith(S.String, { nullable: true }), + /** + * The group id to attach to this trace to enable filtering and + * grouping in the traces dashboard. + */ + "group_id": S.optionalWith(S.String, { nullable: true }), + /** + * The arbitrary metadata to attach to this trace to enable + * filtering in the traces dashboard. + */ + "metadata": S.optionalWith(S.Record({ key: S.String, value: S.Unknown }), { nullable: true }) + }) + ), + { nullable: true } + ), + /** + * Configuration for turn detection. Can be set to `null` to turn off. Server + * VAD means that the model will detect the start and end of speech based on + * audio volume and respond at the end of user speech. + */ + "turn_detection": S.optionalWith( + S.Struct({ + /** + * Type of turn detection, only `server_vad` is currently supported. + */ + "type": S.optionalWith(S.String, { nullable: true }), + /** + * Activation threshold for VAD (0.0 to 1.0), this defaults to 0.5. A + * higher threshold will require louder audio to activate the model, and + * thus might perform better in noisy environments. + */ + "threshold": S.optionalWith(S.Number, { nullable: true }), + /** + * Amount of audio to include before the VAD detected speech (in + * milliseconds). Defaults to 300ms. + */ + "prefix_padding_ms": S.optionalWith(S.Int, { nullable: true }), + /** + * Duration of silence to detect speech stop (in milliseconds). Defaults + * to 500ms. With shorter values the model will respond more quickly, + * but may jump in on short pauses from the user. + */ + "silence_duration_ms": S.optionalWith(S.Int, { nullable: true }) + }), + { nullable: true } + ), + /** + * Tools (functions) available to the model. + */ + "tools": S.optionalWith(S.Array(RealtimeFunctionTool), { nullable: true }), + /** + * How the model chooses tools. Options are `auto`, `none`, `required`, or + * specify a function. + */ + "tool_choice": S.optionalWith(S.String, { nullable: true }), + /** + * Maximum number of output tokens for a single assistant response, + * inclusive of tool calls. Provide an integer between 1 and 4096 to + * limit output tokens, or `inf` for the maximum available tokens for a + * given model. Defaults to `inf`. + */ + "max_output_tokens": S.optionalWith(S.Union(S.Int, RealtimeSessionCreateResponseMaxOutputTokensEnum), { + nullable: true + }) + }) +{} + +/** + * Type of turn detection. Only `server_vad` is currently supported for transcription sessions. + */ +export class RealtimeTranscriptionSessionCreateRequestTurnDetectionType extends S.Literal("server_vad") {} + +/** + * The format of input audio. Options are `pcm16`, `g711_ulaw`, or `g711_alaw`. + * For `pcm16`, input audio must be 16-bit PCM at a 24kHz sample rate, + * single channel (mono), and little-endian byte order. + */ +export class RealtimeTranscriptionSessionCreateRequestInputAudioFormat + extends S.Literal("pcm16", "g711_ulaw", "g711_alaw") +{} + +/** + * Realtime transcription session object configuration. + */ +export class RealtimeTranscriptionSessionCreateRequest + extends S.Class("RealtimeTranscriptionSessionCreateRequest")({ + /** + * Configuration for turn detection. Can be set to `null` to turn off. Server VAD means that the model will detect the start and end of speech based on audio volume and respond at the end of user speech. + */ + "turn_detection": S.optionalWith( + S.Struct({ + /** + * Type of turn detection. Only `server_vad` is currently supported for transcription sessions. + */ + "type": S.optionalWith(RealtimeTranscriptionSessionCreateRequestTurnDetectionType, { nullable: true }), + /** + * Activation threshold for VAD (0.0 to 1.0), this defaults to 0.5. A + * higher threshold will require louder audio to activate the model, and + * thus might perform better in noisy environments. + */ + "threshold": S.optionalWith(S.Number, { nullable: true }), + /** + * Amount of audio to include before the VAD detected speech (in + * milliseconds). Defaults to 300ms. + */ + "prefix_padding_ms": S.optionalWith(S.Int, { nullable: true }), + /** + * Duration of silence to detect speech stop (in milliseconds). Defaults + * to 500ms. With shorter values the model will respond more quickly, + * but may jump in on short pauses from the user. + */ + "silence_duration_ms": S.optionalWith(S.Int, { nullable: true }) + }), + { nullable: true } + ), + /** + * Configuration for input audio noise reduction. This can be set to `null` to turn off. + * Noise reduction filters audio added to the input audio buffer before it is sent to VAD and the model. + * Filtering the audio can improve VAD and turn detection accuracy (reducing false positives) and model performance by improving perception of the input audio. + */ + "input_audio_noise_reduction": S.optionalWith( + S.Struct({ + "type": S.optionalWith(NoiseReductionType, { nullable: true }) + }), + { nullable: true } + ), + /** + * The format of input audio. Options are `pcm16`, `g711_ulaw`, or `g711_alaw`. + * For `pcm16`, input audio must be 16-bit PCM at a 24kHz sample rate, + * single channel (mono), and little-endian byte order. + */ + "input_audio_format": S.optionalWith(RealtimeTranscriptionSessionCreateRequestInputAudioFormat, { + nullable: true, + default: () => "pcm16" as const + }), + /** + * Configuration for input audio transcription. The client can optionally set the language and prompt for transcription, these offer additional guidance to the transcription service. + */ + "input_audio_transcription": S.optionalWith(AudioTranscription, { nullable: true }), + /** + * The set of items to include in the transcription. Current available items are: + * `item.input_audio_transcription.logprobs` + */ + "include": S.optionalWith(S.Array(S.Literal("item.input_audio_transcription.logprobs")), { nullable: true }) + }) +{} + +/** + * A new Realtime transcription session configuration. + * + * When a session is created on the server via REST API, the session object + * also contains an ephemeral key. Default TTL for keys is 10 minutes. This + * property is not present when a session is updated via the WebSocket API. + */ +export class RealtimeTranscriptionSessionCreateResponse + extends S.Class("RealtimeTranscriptionSessionCreateResponse")({ + /** + * Ephemeral key returned by the API. Only present when the session is + * created on the server via REST API. + */ + "client_secret": S.Struct({ + /** + * Ephemeral key usable in client environments to authenticate connections + * to the Realtime API. Use this in client-side environments rather than + * a standard API token, which should only be used server-side. + */ + "value": S.String, + /** + * Timestamp for when the token expires. Currently, all tokens expire + * after one minute. + */ + "expires_at": S.Int + }), + /** + * The format of input audio. Options are `pcm16`, `g711_ulaw`, or `g711_alaw`. + */ + "input_audio_format": S.optionalWith(S.String, { nullable: true }), + /** + * Configuration of the transcription model. + */ + "input_audio_transcription": S.optionalWith(AudioTranscription, { nullable: true }), + /** + * Configuration for turn detection. Can be set to `null` to turn off. Server + * VAD means that the model will detect the start and end of speech based on + * audio volume and respond at the end of user speech. + */ + "turn_detection": S.optionalWith( + S.Struct({ + /** + * Type of turn detection, only `server_vad` is currently supported. + */ + "type": S.optionalWith(S.String, { nullable: true }), + /** + * Activation threshold for VAD (0.0 to 1.0), this defaults to 0.5. A + * higher threshold will require louder audio to activate the model, and + * thus might perform better in noisy environments. + */ + "threshold": S.optionalWith(S.Number, { nullable: true }), + /** + * Amount of audio to include before the VAD detected speech (in + * milliseconds). Defaults to 300ms. + */ + "prefix_padding_ms": S.optionalWith(S.Int, { nullable: true }), + /** + * Duration of silence to detect speech stop (in milliseconds). Defaults + * to 500ms. With shorter values the model will respond more quickly, + * but may jump in on short pauses from the user. + */ + "silence_duration_ms": S.optionalWith(S.Int, { nullable: true }) + }), + { nullable: true } + ) + }) +{} + +/** + * Text, image, or file inputs to the model, used to generate a response. + * + * Learn more: + * - [Text inputs and outputs](https://platform.openai.com/docs/guides/text) + * - [Image inputs](https://platform.openai.com/docs/guides/images) + * - [File inputs](https://platform.openai.com/docs/guides/pdf-files) + * - [Conversation state](https://platform.openai.com/docs/guides/conversation-state) + * - [Function calling](https://platform.openai.com/docs/guides/function-calling) + */ +export class InputParam extends S.Union( + /** + * A text input to the model, equivalent to a text input with the + * `user` role. + */ + S.String, + /** + * A list of one or many input items to the model, containing + * different content types. + */ + S.Array(InputItem) +) {} + +export class ResponseStreamOptions extends S.Union( + /** + * Options for streaming responses. Only set this when you set `stream: true`. + */ + S.Struct({ + /** + * When true, stream obfuscation will be enabled. Stream obfuscation adds + * random characters to an `obfuscation` field on streaming delta events to + * normalize payload sizes as a mitigation to certain side-channel attacks. + * These obfuscation fields are included by default, but add a small amount + * of overhead to the data stream. You can set `include_obfuscation` to + * false to optimize for bandwidth if you trust the network links between + * your application and the OpenAI API. + */ + "include_obfuscation": S.optionalWith(S.Boolean, { nullable: true }) + }), + S.Null +) {} + +/** + * The conversation that this response belongs to. + */ +export class ConversationParam2 extends S.Class("ConversationParam2")({ + /** + * The unique ID of the conversation. + */ + "id": S.String +}) {} + +/** + * The conversation that this response belongs to. Items from this conversation are prepended to `input_items` for this response request. + * Input items and output items from this response are automatically added to this conversation after this response completes. + */ +export class ConversationParam extends S.Union( + /** + * The unique ID of the conversation. + */ + S.String, + ConversationParam2 +) {} + +export class ModelIdsResponsesEnum extends S.Literal( + "o1-pro", + "o1-pro-2025-03-19", + "o3-pro", + "o3-pro-2025-06-10", + "o3-deep-research", + "o3-deep-research-2025-06-26", + "o4-mini-deep-research", + "o4-mini-deep-research-2025-06-26", + "computer-use-preview", + "computer-use-preview-2025-03-11", + "gpt-5-codex", + "gpt-5-pro", + "gpt-5-pro-2025-10-06" +) {} + +export class ModelIdsResponses extends S.Union(ModelIdsShared, ModelIdsResponsesEnum) {} + +/** + * A summary of the reasoning performed by the model. This can be + * useful for debugging and understanding the model's reasoning process. + * One of `auto`, `concise`, or `detailed`. + * + * `concise` is only supported for `computer-use-preview` models. + */ +export class ReasoningSummaryEnum extends S.Literal("auto", "concise", "detailed") {} + +/** + * **Deprecated:** use `summary` instead. + * + * A summary of the reasoning performed by the model. This can be + * useful for debugging and understanding the model's reasoning process. + * One of `auto`, `concise`, or `detailed`. + */ +export class ReasoningGenerateSummaryEnum extends S.Literal("auto", "concise", "detailed") {} + +/** + * **gpt-5 and o-series models only** + * + * Configuration options for + * [reasoning models](https://platform.openai.com/docs/guides/reasoning). + */ +export class Reasoning extends S.Class("Reasoning")({ + "effort": S.optionalWith(ReasoningEffortEnum, { nullable: true }), + "summary": S.optionalWith(ReasoningSummaryEnum, { nullable: true }), + "generate_summary": S.optionalWith(ReasoningGenerateSummaryEnum, { nullable: true }) +}) {} + +/** + * Configuration options for a text response from the model. Can be plain + * text or structured JSON data. Learn more: + * - [Text inputs and outputs](https://platform.openai.com/docs/guides/text) + * - [Structured Outputs](https://platform.openai.com/docs/guides/structured-outputs) + */ +export class ResponseTextParam extends S.Class("ResponseTextParam")({ + "format": S.optionalWith(TextResponseFormatConfiguration, { nullable: true }), + "verbosity": S.optionalWith(S.Literal("low", "medium", "high"), { nullable: true }) +}) {} + +/** + * An array of tools the model may call while generating a response. You + * can specify which tool to use by setting the `tool_choice` parameter. + * + * We support the following categories of tools: + * - **Built-in tools**: Tools that are provided by OpenAI that extend the + * model's capabilities, like [web search](https://platform.openai.com/docs/guides/tools-web-search) + * or [file search](https://platform.openai.com/docs/guides/tools-file-search). Learn more about + * [built-in tools](https://platform.openai.com/docs/guides/tools). + * - **MCP Tools**: Integrations with third-party systems via custom MCP servers + * or predefined connectors such as Google Drive and SharePoint. Learn more about + * [MCP Tools](https://platform.openai.com/docs/guides/tools-connectors-mcp). + * - **Function calls (custom tools)**: Functions that are defined by you, + * enabling the model to call your own code with strongly typed arguments + * and outputs. Learn more about + * [function calling](https://platform.openai.com/docs/guides/function-calling). You can also use + * custom tools to call your own code. + */ +export class ToolsArray extends S.Array(Tool) {} + +/** + * Allowed tool configuration type. Always `allowed_tools`. + */ +export class ToolChoiceAllowedType extends S.Literal("allowed_tools") {} + +/** + * Constrains the tools available to the model to a pre-defined set. + * + * `auto` allows the model to pick from among the allowed tools and generate a + * message. + * + * `required` requires the model to call one or more of the allowed tools. + */ +export class ToolChoiceAllowedMode extends S.Literal("auto", "required") {} + +/** + * Constrains the tools available to the model to a pre-defined set. + */ +export class ToolChoiceAllowed extends S.Class("ToolChoiceAllowed")({ + /** + * Allowed tool configuration type. Always `allowed_tools`. + */ + "type": ToolChoiceAllowedType, + /** + * Constrains the tools available to the model to a pre-defined set. + * + * `auto` allows the model to pick from among the allowed tools and generate a + * message. + * + * `required` requires the model to call one or more of the allowed tools. + */ + "mode": ToolChoiceAllowedMode, + /** + * A list of tool definitions that the model should be allowed to call. + * + * For the Responses API, the list of tool definitions might look like: + * ```json + * [ + * { "type": "function", "name": "get_weather" }, + * { "type": "mcp", "server_label": "deepwiki" }, + * { "type": "image_generation" } + * ] + * ``` + */ + "tools": S.Array(S.Record({ key: S.String, value: S.Unknown })) +}) {} + +/** + * The type of hosted tool the model should to use. Learn more about + * [built-in tools](https://platform.openai.com/docs/guides/tools). + * + * Allowed values are: + * - `file_search` + * - `web_search_preview` + * - `computer_use_preview` + * - `code_interpreter` + * - `image_generation` + */ +export class ToolChoiceTypesType extends S.Literal( + "file_search", + "web_search_preview", + "computer_use_preview", + "web_search_preview_2025_03_11", + "image_generation", + "code_interpreter" +) {} + +/** + * Indicates that the model should use a built-in tool to generate a response. + * [Learn more about built-in tools](https://platform.openai.com/docs/guides/tools). + */ +export class ToolChoiceTypes extends S.Class("ToolChoiceTypes")({ + /** + * The type of hosted tool the model should to use. Learn more about + * [built-in tools](https://platform.openai.com/docs/guides/tools). + * + * Allowed values are: + * - `file_search` + * - `web_search_preview` + * - `computer_use_preview` + * - `code_interpreter` + * - `image_generation` + */ + "type": ToolChoiceTypesType +}) {} + +/** + * For custom tool calling, the type is always `custom`. + */ +export class ToolChoiceCustomType extends S.Literal("custom") {} + +/** + * Use this option to force the model to call a specific custom tool. + */ +export class ToolChoiceCustom extends S.Class("ToolChoiceCustom")({ + /** + * For custom tool calling, the type is always `custom`. + */ + "type": ToolChoiceCustomType, + /** + * The name of the custom tool to call. + */ + "name": S.String +}) {} + +/** + * The tool to call. Always `apply_patch`. + */ +export class SpecificApplyPatchParamType extends S.Literal("apply_patch") {} + +/** + * Forces the model to call the apply_patch tool when executing a tool call. + */ +export class SpecificApplyPatchParam extends S.Class("SpecificApplyPatchParam")({ + /** + * The tool to call. Always `apply_patch`. + */ + "type": SpecificApplyPatchParamType.pipe(S.propertySignature, S.withConstructorDefault(() => "apply_patch" as const)) +}) {} + +/** + * The tool to call. Always `shell`. + */ +export class SpecificFunctionShellParamType extends S.Literal("shell") {} + +/** + * Forces the model to call the function shell tool when a tool call is required. + */ +export class SpecificFunctionShellParam extends S.Class("SpecificFunctionShellParam")({ + /** + * The tool to call. Always `shell`. + */ + "type": SpecificFunctionShellParamType.pipe(S.propertySignature, S.withConstructorDefault(() => "shell" as const)) +}) {} + +/** + * How the model should select which tool (or tools) to use when generating + * a response. See the `tools` parameter to see how to specify which tools + * the model can call. + */ +export class ToolChoiceParam extends S.Union( + ToolChoiceOptions, + ToolChoiceAllowed, + ToolChoiceTypes, + ToolChoiceFunction, + ToolChoiceMCP, + ToolChoiceCustom, + SpecificApplyPatchParam, + SpecificFunctionShellParam +) {} + +/** + * The truncation strategy to use for the model response. + * - `auto`: If the input to this Response exceeds + * the model's context window size, the model will truncate the + * response to fit the context window by dropping items from the beginning of the conversation. + * - `disabled` (default): If the input size will exceed the context window + * size for a model, the request will fail with a 400 error. + */ +export class CreateResponseTruncationEnum extends S.Literal("auto", "disabled") {} + +/** + * The retention policy for the prompt cache. Set to `24h` to enable extended prompt caching, which keeps cached prefixes active for longer, up to a maximum of 24 hours. [Learn more](https://platform.openai.com/docs/guides/prompt-caching#prompt-cache-retention). + */ +export class CreateResponsePromptCacheRetentionEnum extends S.Literal("in_memory", "24h") {} + +export class CreateResponse extends S.Class("CreateResponse")({ + "input": S.optionalWith(InputParam, { nullable: true }), + "include": S.optionalWith(S.Array(IncludeEnum), { nullable: true }), + "parallel_tool_calls": S.optionalWith(S.Boolean, { nullable: true }), + "store": S.optionalWith(S.Boolean, { nullable: true }), + "instructions": S.optionalWith(S.String, { nullable: true }), + "stream": S.optionalWith(S.Boolean, { nullable: true }), + "stream_options": S.optionalWith( + S.Struct({ + /** + * When true, stream obfuscation will be enabled. Stream obfuscation adds + * random characters to an `obfuscation` field on streaming delta events to + * normalize payload sizes as a mitigation to certain side-channel attacks. + * These obfuscation fields are included by default, but add a small amount + * of overhead to the data stream. You can set `include_obfuscation` to + * false to optimize for bandwidth if you trust the network links between + * your application and the OpenAI API. + */ + "include_obfuscation": S.optionalWith(S.Boolean, { nullable: true }) + }), + { nullable: true } + ), + "conversation": S.optionalWith(ConversationParam, { nullable: true }), + "previous_response_id": S.optionalWith(S.String, { nullable: true }), + /** + * Model ID used to generate the response, like `gpt-4o` or `o3`. OpenAI + * offers a wide range of models with different capabilities, performance + * characteristics, and price points. Refer to the [model guide](https://platform.openai.com/docs/models) + * to browse and compare available models. + */ + "model": S.optionalWith(ModelIdsResponses, { nullable: true }), + "reasoning": S.optionalWith(Reasoning, { nullable: true }), + "background": S.optionalWith(S.Boolean, { nullable: true }), + "max_output_tokens": S.optionalWith(S.Int, { nullable: true }), + "max_tool_calls": S.optionalWith(S.Int, { nullable: true }), + "text": S.optionalWith(ResponseTextParam, { nullable: true }), + "tools": S.optionalWith(ToolsArray, { nullable: true }), + "tool_choice": S.optionalWith(ToolChoiceParam, { nullable: true }), + "prompt": S.optionalWith( + S.Struct({ + /** + * The unique identifier of the prompt template to use. + */ + "id": S.String, + "version": S.optionalWith(S.String, { nullable: true }), + "variables": S.optionalWith(S.Record({ key: S.String, value: S.Unknown }), { nullable: true }) + }), + { nullable: true } + ), + "truncation": S.optionalWith(CreateResponseTruncationEnum, { nullable: true }), + "top_logprobs": S.optionalWith(S.Int.pipe(S.greaterThanOrEqualTo(0), S.lessThanOrEqualTo(20)), { nullable: true }), + "metadata": S.optionalWith(S.Record({ key: S.String, value: S.Unknown }), { nullable: true }), + "temperature": S.optionalWith(S.Number.pipe(S.greaterThanOrEqualTo(0), S.lessThanOrEqualTo(2)), { nullable: true }), + "top_p": S.optionalWith(S.Number.pipe(S.greaterThanOrEqualTo(0), S.lessThanOrEqualTo(1)), { nullable: true }), + /** + * This field is being replaced by `safety_identifier` and `prompt_cache_key`. Use `prompt_cache_key` instead to maintain caching optimizations. + * A stable identifier for your end-users. + * Used to boost cache hit rates by better bucketing similar requests and to help OpenAI detect and prevent abuse. [Learn more](https://platform.openai.com/docs/guides/safety-best-practices#safety-identifiers). + */ + "user": S.optionalWith(S.String, { nullable: true }), + /** + * A stable identifier used to help detect users of your application that may be violating OpenAI's usage policies. + * The IDs should be a string that uniquely identifies each user. We recommend hashing their username or email address, in order to avoid sending us any identifying information. [Learn more](https://platform.openai.com/docs/guides/safety-best-practices#safety-identifiers). + */ + "safety_identifier": S.optionalWith(S.String, { nullable: true }), + /** + * Used by OpenAI to cache responses for similar requests to optimize your cache hit rates. Replaces the `user` field. [Learn more](https://platform.openai.com/docs/guides/prompt-caching). + */ + "prompt_cache_key": S.optionalWith(S.String, { nullable: true }), + "service_tier": S.optionalWith(S.Literal("auto", "default", "flex", "scale", "priority"), { nullable: true }), + "prompt_cache_retention": S.optionalWith(CreateResponsePromptCacheRetentionEnum, { nullable: true }) +}) {} + +/** + * The object type of this resource - always set to `response`. + */ +export class ResponseObject extends S.Literal("response") {} + +/** + * The status of the response generation. One of `completed`, `failed`, + * `in_progress`, `cancelled`, `queued`, or `incomplete`. + */ +export class ResponseStatus + extends S.Literal("completed", "failed", "in_progress", "cancelled", "queued", "incomplete") +{} + +/** + * The error code for the response. + */ +export class ResponseErrorCode extends S.Literal( + "server_error", + "rate_limit_exceeded", + "invalid_prompt", + "vector_store_timeout", + "invalid_image", + "invalid_image_format", + "invalid_base64_image", + "invalid_image_url", + "image_too_large", + "image_too_small", + "image_parse_error", + "image_content_policy_violation", + "invalid_image_mode", + "image_file_too_large", + "unsupported_image_media_type", + "empty_image_file", + "failed_to_download_image", + "image_file_not_found" +) {} + +export class ResponseError extends S.Union( + /** + * An error object returned when the model fails to generate a Response. + */ + S.Struct({ + "code": ResponseErrorCode, + /** + * A human-readable description of the error. + */ + "message": S.String + }), + S.Null +) {} + +/** + * The reason why the response is incomplete. + */ +export class ResponseIncompleteDetailsEnumReason extends S.Literal("max_output_tokens", "content_filter") {} + +export class OutputItem extends S.Union( + OutputMessage, + FileSearchToolCall, + FunctionToolCall, + WebSearchToolCall, + ComputerToolCall, + ReasoningItem, + ImageGenToolCall, + CodeInterpreterToolCall, + LocalShellToolCall, + FunctionShellCall, + FunctionShellCallOutput, + ApplyPatchToolCall, + ApplyPatchToolCallOutput, + MCPToolCall, + MCPListTools, + MCPApprovalRequest, + CustomToolCall +) {} + +/** + * Represents token usage details including input tokens, output tokens, + * a breakdown of output tokens, and the total tokens used. + */ +export class ResponseUsage extends S.Class("ResponseUsage")({ + /** + * The number of input tokens. + */ + "input_tokens": S.Int, + /** + * A detailed breakdown of the input tokens. + */ + "input_tokens_details": S.Struct({ + /** + * The number of tokens that were retrieved from the cache. + * [More on prompt caching](https://platform.openai.com/docs/guides/prompt-caching). + */ + "cached_tokens": S.Int + }), + /** + * The number of output tokens. + */ + "output_tokens": S.Int, + /** + * A detailed breakdown of the output tokens. + */ + "output_tokens_details": S.Struct({ + /** + * The number of reasoning tokens. + */ + "reasoning_tokens": S.Int + }), + /** + * The total number of tokens used. + */ + "total_tokens": S.Int +}) {} + +/** + * The conversation that this response belongs to. Input items and output items from this response are automatically added to this conversation. + */ +export class Conversation2 extends S.Class("Conversation2")({ + /** + * The unique ID of the conversation. + */ + "id": S.String +}) {} + +/** + * The truncation strategy to use for the model response. + * - `auto`: If the input to this Response exceeds + * the model's context window size, the model will truncate the + * response to fit the context window by dropping items from the beginning of the conversation. + * - `disabled` (default): If the input size will exceed the context window + * size for a model, the request will fail with a 400 error. + */ +export class ResponseTruncationEnum extends S.Literal("auto", "disabled") {} + +/** + * The retention policy for the prompt cache. Set to `24h` to enable extended prompt caching, which keeps cached prefixes active for longer, up to a maximum of 24 hours. [Learn more](https://platform.openai.com/docs/guides/prompt-caching#prompt-cache-retention). + */ +export class ResponsePromptCacheRetentionEnum extends S.Literal("in_memory", "24h") {} + +export class Response extends S.Class("Response")({ + /** + * Unique identifier for this Response. + */ + "id": S.String, + /** + * The object type of this resource - always set to `response`. + */ + "object": ResponseObject, + /** + * The status of the response generation. One of `completed`, `failed`, + * `in_progress`, `cancelled`, `queued`, or `incomplete`. + */ + "status": S.optionalWith(ResponseStatus, { nullable: true }), + /** + * Unix timestamp (in seconds) of when this Response was created. + */ + "created_at": S.Number, + "error": S.NullOr(S.Struct({ + "code": ResponseErrorCode, + /** + * A human-readable description of the error. + */ + "message": S.String + })), + "incomplete_details": S.NullOr(S.Struct({ + /** + * The reason why the response is incomplete. + */ + "reason": S.optionalWith(S.Literal("max_output_tokens", "content_filter"), { nullable: true }) + })), + /** + * An array of content items generated by the model. + * + * - The length and order of items in the `output` array is dependent + * on the model's response. + * - Rather than accessing the first item in the `output` array and + * assuming it's an `assistant` message with the content generated by + * the model, you might consider using the `output_text` property where + * supported in SDKs. + */ + "output": S.Array(OutputItem), + "instructions": S.NullOr(S.Union( + /** + * A text input to the model, equivalent to a text input with the + * `developer` role. + */ + S.String, + /** + * A list of one or many input items to the model, containing + * different content types. + */ + S.Array(InputItem) + )), + "output_text": S.optionalWith(S.String, { nullable: true }), + "usage": S.optionalWith(ResponseUsage, { nullable: true }), + /** + * Whether to allow the model to run tool calls in parallel. + */ + "parallel_tool_calls": S.Boolean.pipe(S.propertySignature, S.withConstructorDefault(() => true as const)), + "conversation": S.optionalWith(Conversation2, { nullable: true }), + "previous_response_id": S.optionalWith(S.String, { nullable: true }), + /** + * Model ID used to generate the response, like `gpt-4o` or `o3`. OpenAI + * offers a wide range of models with different capabilities, performance + * characteristics, and price points. Refer to the [model guide](https://platform.openai.com/docs/models) + * to browse and compare available models. + */ + "model": ModelIdsResponses, + "reasoning": S.optionalWith(Reasoning, { nullable: true }), + "background": S.optionalWith(S.Boolean, { nullable: true }), + "max_output_tokens": S.optionalWith(S.Int, { nullable: true }), + "max_tool_calls": S.optionalWith(S.Int, { nullable: true }), + "text": S.optionalWith(ResponseTextParam, { nullable: true }), + "tools": ToolsArray, + "tool_choice": ToolChoiceParam, + "prompt": S.optionalWith( + S.Struct({ + /** + * The unique identifier of the prompt template to use. + */ + "id": S.String, + "version": S.optionalWith(S.String, { nullable: true }), + "variables": S.optionalWith(S.Record({ key: S.String, value: S.Unknown }), { nullable: true }) + }), + { nullable: true } + ), + "truncation": S.optionalWith(ResponseTruncationEnum, { nullable: true }), + "metadata": S.NullOr(S.Record({ key: S.String, value: S.Unknown })), + "top_logprobs": S.optionalWith(S.Int.pipe(S.greaterThanOrEqualTo(0), S.lessThanOrEqualTo(20)), { nullable: true }), + "temperature": S.NullOr(S.Number.pipe(S.greaterThanOrEqualTo(0), S.lessThanOrEqualTo(2))), + "top_p": S.NullOr(S.Number.pipe(S.greaterThanOrEqualTo(0), S.lessThanOrEqualTo(1))), + /** + * This field is being replaced by `safety_identifier` and `prompt_cache_key`. Use `prompt_cache_key` instead to maintain caching optimizations. + * A stable identifier for your end-users. + * Used to boost cache hit rates by better bucketing similar requests and to help OpenAI detect and prevent abuse. [Learn more](https://platform.openai.com/docs/guides/safety-best-practices#safety-identifiers). + */ + "user": S.optionalWith(S.String, { nullable: true }), + /** + * A stable identifier used to help detect users of your application that may be violating OpenAI's usage policies. + * The IDs should be a string that uniquely identifies each user. We recommend hashing their username or email address, in order to avoid sending us any identifying information. [Learn more](https://platform.openai.com/docs/guides/safety-best-practices#safety-identifiers). + */ + "safety_identifier": S.optionalWith(S.String, { nullable: true }), + /** + * Used by OpenAI to cache responses for similar requests to optimize your cache hit rates. Replaces the `user` field. [Learn more](https://platform.openai.com/docs/guides/prompt-caching). + */ + "prompt_cache_key": S.optionalWith(S.String, { nullable: true }), + "service_tier": S.optionalWith(S.Literal("auto", "default", "flex", "scale", "priority"), { nullable: true }), + "prompt_cache_retention": S.optionalWith(ResponsePromptCacheRetentionEnum, { nullable: true }) +}) {} + +export class GetResponseParams extends S.Struct({ + "include": S.optionalWith(S.Array(IncludeEnum), { nullable: true }), + "stream": S.optionalWith(S.Boolean, { nullable: true }), + "starting_after": S.optionalWith(S.Int, { nullable: true }), + "include_obfuscation": S.optionalWith(S.Boolean, { nullable: true }) +}) {} + +export class ListInputItemsParamsOrder extends S.Literal("asc", "desc") {} + +export class ListInputItemsParams extends S.Struct({ + "limit": S.optionalWith(S.Int, { nullable: true, default: () => 20 as const }), + "order": S.optionalWith(ListInputItemsParamsOrder, { nullable: true }), + "after": S.optionalWith(S.String, { nullable: true }), + "include": S.optionalWith(S.Array(IncludeEnum), { nullable: true }) +}) {} + +/** + * The type of the message input. Always set to `message`. + */ +export class InputMessageResourceType extends S.Literal("message") {} + +/** + * The role of the message input. One of `user`, `system`, or `developer`. + */ +export class InputMessageResourceRole extends S.Literal("user", "system", "developer") {} + +/** + * The status of item. One of `in_progress`, `completed`, or + * `incomplete`. Populated when items are returned via API. + */ +export class InputMessageResourceStatus extends S.Literal("in_progress", "completed", "incomplete") {} + +/** + * A message input to the model with a role indicating instruction following + * hierarchy. Instructions given with the `developer` or `system` role take + * precedence over instructions given with the `user` role. + */ +export class InputMessageResource extends S.Class("InputMessageResource")({ + /** + * The unique ID of the message input. + */ + "id": S.String, + /** + * The type of the message input. Always set to `message`. + */ + "type": S.optionalWith(InputMessageResourceType, { nullable: true }), + /** + * The role of the message input. One of `user`, `system`, or `developer`. + */ + "role": InputMessageResourceRole, + /** + * The status of item. One of `in_progress`, `completed`, or + * `incomplete`. Populated when items are returned via API. + */ + "status": S.optionalWith(InputMessageResourceStatus, { nullable: true }), + "content": InputMessageContentList +}) {} + +/** + * Content item used to generate a response. + */ +export class ItemResource extends S.Union( + InputMessageResource, + OutputMessage, + FileSearchToolCall, + ComputerToolCall, + ComputerToolCallOutputResource, + WebSearchToolCall, + FunctionToolCallResource, + FunctionToolCallOutputResource, + ImageGenToolCall, + CodeInterpreterToolCall, + LocalShellToolCall, + LocalShellToolCallOutput, + FunctionShellCall, + FunctionShellCallOutput, + ApplyPatchToolCall, + ApplyPatchToolCallOutput, + MCPListTools, + MCPApprovalRequest, + MCPApprovalResponseResource, + MCPToolCall +) {} + +/** + * A list of Response items. + */ +export class ResponseItemList extends S.Class("ResponseItemList")({ + /** + * The type of object returned, must be `list`. + */ + "object": S.Literal("list"), + /** + * A list of items used to generate this response. + */ + "data": S.Array(ItemResource), + /** + * Whether there are more items available. + */ + "has_more": S.Boolean, + /** + * The ID of the first item in the list. + */ + "first_id": S.String, + /** + * The ID of the last item in the list. + */ + "last_id": S.String +}) {} + +/** + * The role of the entity that is creating the message. Allowed values include: + * - `user`: Indicates the message is sent by an actual user and should be used in most cases to represent user-generated messages. + * - `assistant`: Indicates the message is generated by the assistant. Use this value to insert messages from the assistant into the conversation. + */ +export class CreateMessageRequestRole extends S.Literal("user", "assistant") {} + +/** + * Always `image_file`. + */ +export class MessageContentImageFileObjectType extends S.Literal("image_file") {} + +/** + * Specifies the detail level of the image if specified by the user. `low` uses fewer tokens, you can opt in to high resolution using `high`. + */ +export class MessageContentImageFileObjectImageFileDetail extends S.Literal("auto", "low", "high") {} + +/** + * References an image [File](https://platform.openai.com/docs/api-reference/files) in the content of a message. + */ +export class MessageContentImageFileObject + extends S.Class("MessageContentImageFileObject")({ + /** + * Always `image_file`. + */ + "type": MessageContentImageFileObjectType, + "image_file": S.Struct({ + /** + * The [File](https://platform.openai.com/docs/api-reference/files) ID of the image in the message content. Set `purpose="vision"` when uploading the File if you need to later display the file content. + */ + "file_id": S.String, + /** + * Specifies the detail level of the image if specified by the user. `low` uses fewer tokens, you can opt in to high resolution using `high`. + */ + "detail": S.optionalWith(MessageContentImageFileObjectImageFileDetail, { + nullable: true, + default: () => "auto" as const + }) + }) + }) +{} + +/** + * The type of the content part. + */ +export class MessageContentImageUrlObjectType extends S.Literal("image_url") {} + +/** + * Specifies the detail level of the image. `low` uses fewer tokens, you can opt in to high resolution using `high`. Default value is `auto` + */ +export class MessageContentImageUrlObjectImageUrlDetail extends S.Literal("auto", "low", "high") {} + +/** + * References an image URL in the content of a message. + */ +export class MessageContentImageUrlObject + extends S.Class("MessageContentImageUrlObject")({ + /** + * The type of the content part. + */ + "type": MessageContentImageUrlObjectType, + "image_url": S.Struct({ + /** + * The external URL of the image, must be a supported image types: jpeg, jpg, png, gif, webp. + */ + "url": S.String, + /** + * Specifies the detail level of the image. `low` uses fewer tokens, you can opt in to high resolution using `high`. Default value is `auto` + */ + "detail": S.optionalWith(MessageContentImageUrlObjectImageUrlDetail, { + nullable: true, + default: () => "auto" as const + }) + }) + }) +{} + +/** + * Always `text`. + */ +export class MessageRequestContentTextObjectType extends S.Literal("text") {} + +/** + * The text content that is part of a message. + */ +export class MessageRequestContentTextObject + extends S.Class("MessageRequestContentTextObject")({ + /** + * Always `text`. + */ + "type": MessageRequestContentTextObjectType, + /** + * Text content to be sent to the model + */ + "text": S.String + }) +{} + +/** + * The type of tool being defined: `file_search` + */ +export class AssistantToolsFileSearchTypeOnlyType extends S.Literal("file_search") {} + +export class AssistantToolsFileSearchTypeOnly + extends S.Class("AssistantToolsFileSearchTypeOnly")({ + /** + * The type of tool being defined: `file_search` + */ + "type": AssistantToolsFileSearchTypeOnlyType + }) +{} + +export class CreateMessageRequest extends S.Class("CreateMessageRequest")({ + /** + * The role of the entity that is creating the message. Allowed values include: + * - `user`: Indicates the message is sent by an actual user and should be used in most cases to represent user-generated messages. + * - `assistant`: Indicates the message is generated by the assistant. Use this value to insert messages from the assistant into the conversation. + */ + "role": CreateMessageRequestRole, + "content": S.Union( + /** + * The text contents of the message. + */ + S.String, + /** + * An array of content parts with a defined type, each can be of type `text` or images can be passed with `image_url` or `image_file`. Image types are only supported on [Vision-compatible models](https://platform.openai.com/docs/models). + */ + S.NonEmptyArray( + S.Union(MessageContentImageFileObject, MessageContentImageUrlObject, MessageRequestContentTextObject) + ).pipe(S.minItems(1)) + ), + "attachments": S.optionalWith( + S.Array(S.Struct({ + /** + * The ID of the file to attach to the message. + */ + "file_id": S.optionalWith(S.String, { nullable: true }), + /** + * The tools to add this file to. + */ + "tools": S.optionalWith(S.Array(S.Union(AssistantToolsCode, AssistantToolsFileSearchTypeOnly)), { + nullable: true + }) + })), + { nullable: true } + ), + "metadata": S.optionalWith(S.Record({ key: S.String, value: S.Unknown }), { nullable: true }) +}) {} + +/** + * Options to create a new thread. If no thread is provided when running a + * request, an empty thread will be created. + */ +export class CreateThreadRequest extends S.Class("CreateThreadRequest")({ + /** + * A list of [messages](https://platform.openai.com/docs/api-reference/messages) to start the thread with. + */ + "messages": S.optionalWith(S.Array(CreateMessageRequest), { nullable: true }), + "tool_resources": S.optionalWith( + S.Struct({ + "code_interpreter": S.optionalWith( + S.Struct({ + /** + * A list of [file](https://platform.openai.com/docs/api-reference/files) IDs made available to the `code_interpreter` tool. There can be a maximum of 20 files associated with the tool. + */ + "file_ids": S.optionalWith(S.Array(S.String).pipe(S.maxItems(20)), { + nullable: true, + default: () => [] as const + }) + }), + { nullable: true } + ), + "file_search": S.optionalWith( + S.Struct({ + /** + * The [vector store](https://platform.openai.com/docs/api-reference/vector-stores/object) attached to this thread. There can be a maximum of 1 vector store attached to the thread. + */ + "vector_store_ids": S.optionalWith(S.Array(S.String).pipe(S.maxItems(1)), { nullable: true }), + /** + * A helper to create a [vector store](https://platform.openai.com/docs/api-reference/vector-stores/object) with file_ids and attach it to this thread. There can be a maximum of 1 vector store attached to the thread. + */ + "vector_stores": S.optionalWith( + S.Array(S.Struct({ + /** + * A list of [file](https://platform.openai.com/docs/api-reference/files) IDs to add to the vector store. There can be a maximum of 10000 files in a vector store. + */ + "file_ids": S.optionalWith(S.Array(S.String).pipe(S.maxItems(10000)), { nullable: true }), + /** + * The chunking strategy used to chunk the file(s). If not set, will use the `auto` strategy. + */ + "chunking_strategy": S.optionalWith(S.Record({ key: S.String, value: S.Unknown }), { nullable: true }), + "metadata": S.optionalWith(S.Record({ key: S.String, value: S.Unknown }), { nullable: true }) + })).pipe(S.maxItems(1)), + { nullable: true } + ) + }), + { nullable: true } + ) + }), + { nullable: true } + ), + "metadata": S.optionalWith(S.Record({ key: S.String, value: S.Unknown }), { nullable: true }) +}) {} + +/** + * The object type, which is always `thread`. + */ +export class ThreadObjectObject extends S.Literal("thread") {} + +/** + * Represents a thread that contains [messages](https://platform.openai.com/docs/api-reference/messages). + */ +export class ThreadObject extends S.Class("ThreadObject")({ + /** + * The identifier, which can be referenced in API endpoints. + */ + "id": S.String, + /** + * The object type, which is always `thread`. + */ + "object": ThreadObjectObject, + /** + * The Unix timestamp (in seconds) for when the thread was created. + */ + "created_at": S.Int, + "tool_resources": S.NullOr(S.Struct({ + "code_interpreter": S.optionalWith( + S.Struct({ + /** + * A list of [file](https://platform.openai.com/docs/api-reference/files) IDs made available to the `code_interpreter` tool. There can be a maximum of 20 files associated with the tool. + */ + "file_ids": S.optionalWith(S.Array(S.String).pipe(S.maxItems(20)), { + nullable: true, + default: () => [] as const + }) + }), + { nullable: true } + ), + "file_search": S.optionalWith( + S.Struct({ + /** + * The [vector store](https://platform.openai.com/docs/api-reference/vector-stores/object) attached to this thread. There can be a maximum of 1 vector store attached to the thread. + */ + "vector_store_ids": S.optionalWith(S.Array(S.String).pipe(S.maxItems(1)), { nullable: true }) + }), + { nullable: true } + ) + })), + "metadata": S.NullOr(S.Record({ key: S.String, value: S.Unknown })) +}) {} + +export class CreateThreadAndRunRequestModelEnum extends S.Literal( + "gpt-5", + "gpt-5-mini", + "gpt-5-nano", + "gpt-5-2025-08-07", + "gpt-5-mini-2025-08-07", + "gpt-5-nano-2025-08-07", + "gpt-4.1", + "gpt-4.1-mini", + "gpt-4.1-nano", + "gpt-4.1-2025-04-14", + "gpt-4.1-mini-2025-04-14", + "gpt-4.1-nano-2025-04-14", + "gpt-4o", + "gpt-4o-2024-11-20", + "gpt-4o-2024-08-06", + "gpt-4o-2024-05-13", + "gpt-4o-mini", + "gpt-4o-mini-2024-07-18", + "gpt-4.5-preview", + "gpt-4.5-preview-2025-02-27", + "gpt-4-turbo", + "gpt-4-turbo-2024-04-09", + "gpt-4-0125-preview", + "gpt-4-turbo-preview", + "gpt-4-1106-preview", + "gpt-4-vision-preview", + "gpt-4", + "gpt-4-0314", + "gpt-4-0613", + "gpt-4-32k", + "gpt-4-32k-0314", + "gpt-4-32k-0613", + "gpt-3.5-turbo", + "gpt-3.5-turbo-16k", + "gpt-3.5-turbo-0613", + "gpt-3.5-turbo-1106", + "gpt-3.5-turbo-0125", + "gpt-3.5-turbo-16k-0613" +) {} + +/** + * The truncation strategy to use for the thread. The default is `auto`. If set to `last_messages`, the thread will be truncated to the n most recent messages in the thread. When set to `auto`, messages in the middle of the thread will be dropped to fit the context length of the model, `max_prompt_tokens`. + */ +export class CreateThreadAndRunRequestTruncationStrategyEnumType extends S.Literal("auto", "last_messages") {} + +/** + * Controls for how a thread will be truncated prior to the run. Use this to control the initial context window of the run. + */ +export class CreateThreadAndRunRequestTruncationStrategy extends S.Struct({ + /** + * The truncation strategy to use for the thread. The default is `auto`. If set to `last_messages`, the thread will be truncated to the n most recent messages in the thread. When set to `auto`, messages in the middle of the thread will be dropped to fit the context length of the model, `max_prompt_tokens`. + */ + "type": S.Literal("auto", "last_messages"), + "last_messages": S.optionalWith(S.Int.pipe(S.greaterThanOrEqualTo(1)), { nullable: true }) +}) {} + +/** + * `none` means the model will not call any tools and instead generates a message. `auto` means the model can pick between generating a message or calling one or more tools. `required` means the model must call one or more tools before responding to the user. + */ +export class CreateThreadAndRunRequestToolChoiceEnum extends S.Literal("none", "auto", "required") {} + +/** + * The type of the tool. If type is `function`, the function name must be set + */ +export class AssistantsNamedToolChoiceType extends S.Literal("function", "code_interpreter", "file_search") {} + +/** + * Specifies a tool the model should use. Use to force the model to call a specific tool. + */ +export class AssistantsNamedToolChoice extends S.Class("AssistantsNamedToolChoice")({ + /** + * The type of the tool. If type is `function`, the function name must be set + */ + "type": AssistantsNamedToolChoiceType, + "function": S.optionalWith( + S.Struct({ + /** + * The name of the function to call. + */ + "name": S.String + }), + { nullable: true } + ) +}) {} + +/** + * Controls which (if any) tool is called by the model. + * `none` means the model will not call any tools and instead generates a message. + * `auto` is the default value and means the model can pick between generating a message or calling one or more tools. + * `required` means the model must call one or more tools before responding to the user. + * Specifying a particular tool like `{"type": "file_search"}` or `{"type": "function", "function": {"name": "my_function"}}` forces the model to call that tool. + */ +export class CreateThreadAndRunRequestToolChoice extends S.Union( + /** + * `none` means the model will not call any tools and instead generates a message. `auto` means the model can pick between generating a message or calling one or more tools. `required` means the model must call one or more tools before responding to the user. + */ + CreateThreadAndRunRequestToolChoiceEnum, + AssistantsNamedToolChoice +) {} + +export class CreateThreadAndRunRequest extends S.Class("CreateThreadAndRunRequest")({ + /** + * The ID of the [assistant](https://platform.openai.com/docs/api-reference/assistants) to use to execute this run. + */ + "assistant_id": S.String, + "thread": S.optionalWith(CreateThreadRequest, { nullable: true }), + /** + * The ID of the [Model](https://platform.openai.com/docs/api-reference/models) to be used to execute this run. If a value is provided here, it will override the model associated with the assistant. If not, the model associated with the assistant will be used. + */ + "model": S.optionalWith(S.Union(S.String, CreateThreadAndRunRequestModelEnum), { nullable: true }), + /** + * Override the default system message of the assistant. This is useful for modifying the behavior on a per-run basis. + */ + "instructions": S.optionalWith(S.String, { nullable: true }), + /** + * Override the tools the assistant can use for this run. This is useful for modifying the behavior on a per-run basis. + */ + "tools": S.optionalWith(S.Array(AssistantTool).pipe(S.maxItems(20)), { nullable: true }), + /** + * A set of resources that are used by the assistant's tools. The resources are specific to the type of tool. For example, the `code_interpreter` tool requires a list of file IDs, while the `file_search` tool requires a list of vector store IDs. + */ + "tool_resources": S.optionalWith( + S.Struct({ + "code_interpreter": S.optionalWith( + S.Struct({ + /** + * A list of [file](https://platform.openai.com/docs/api-reference/files) IDs made available to the `code_interpreter` tool. There can be a maximum of 20 files associated with the tool. + */ + "file_ids": S.optionalWith(S.Array(S.String).pipe(S.maxItems(20)), { + nullable: true, + default: () => [] as const + }) + }), + { nullable: true } + ), + "file_search": S.optionalWith( + S.Struct({ + /** + * The ID of the [vector store](https://platform.openai.com/docs/api-reference/vector-stores/object) attached to this assistant. There can be a maximum of 1 vector store attached to the assistant. + */ + "vector_store_ids": S.optionalWith(S.Array(S.String).pipe(S.maxItems(1)), { nullable: true }) + }), + { nullable: true } + ) + }), + { nullable: true } + ), + "metadata": S.optionalWith(S.Record({ key: S.String, value: S.Unknown }), { nullable: true }), + /** + * What sampling temperature to use, between 0 and 2. Higher values like 0.8 will make the output more random, while lower values like 0.2 will make it more focused and deterministic. + */ + "temperature": S.optionalWith(S.Number.pipe(S.greaterThanOrEqualTo(0), S.lessThanOrEqualTo(2)), { + nullable: true, + default: () => 1 as const + }), + /** + * An alternative to sampling with temperature, called nucleus sampling, where the model considers the results of the tokens with top_p probability mass. So 0.1 means only the tokens comprising the top 10% probability mass are considered. + * + * We generally recommend altering this or temperature but not both. + */ + "top_p": S.optionalWith(S.Number.pipe(S.greaterThanOrEqualTo(0), S.lessThanOrEqualTo(1)), { + nullable: true, + default: () => 1 as const + }), + /** + * If `true`, returns a stream of events that happen during the Run as server-sent events, terminating when the Run enters a terminal state with a `data: [DONE]` message. + */ + "stream": S.optionalWith(S.Boolean, { nullable: true }), + /** + * The maximum number of prompt tokens that may be used over the course of the run. The run will make a best effort to use only the number of prompt tokens specified, across multiple turns of the run. If the run exceeds the number of prompt tokens specified, the run will end with status `incomplete`. See `incomplete_details` for more info. + */ + "max_prompt_tokens": S.optionalWith(S.Int.pipe(S.greaterThanOrEqualTo(256)), { nullable: true }), + /** + * The maximum number of completion tokens that may be used over the course of the run. The run will make a best effort to use only the number of completion tokens specified, across multiple turns of the run. If the run exceeds the number of completion tokens specified, the run will end with status `incomplete`. See `incomplete_details` for more info. + */ + "max_completion_tokens": S.optionalWith(S.Int.pipe(S.greaterThanOrEqualTo(256)), { nullable: true }), + "truncation_strategy": S.optionalWith(CreateThreadAndRunRequestTruncationStrategy, { nullable: true }), + "tool_choice": S.optionalWith(CreateThreadAndRunRequestToolChoice, { nullable: true }), + "parallel_tool_calls": S.optionalWith(ParallelToolCalls, { nullable: true, default: () => true as const }), + "response_format": S.optionalWith(AssistantsApiResponseFormatOption, { nullable: true }) +}) {} + +/** + * The object type, which is always `thread.run`. + */ +export class RunObjectObject extends S.Literal("thread.run") {} + +/** + * The status of the run, which can be either `queued`, `in_progress`, `requires_action`, `cancelling`, `cancelled`, `failed`, `completed`, `incomplete`, or `expired`. + */ +export class RunStatus extends S.Literal( + "queued", + "in_progress", + "requires_action", + "cancelling", + "cancelled", + "failed", + "completed", + "incomplete", + "expired" +) {} + +/** + * For now, this is always `submit_tool_outputs`. + */ +export class RunObjectRequiredActionType extends S.Literal("submit_tool_outputs") {} + +/** + * The type of tool call the output is required for. For now, this is always `function`. + */ +export class RunToolCallObjectType extends S.Literal("function") {} + +/** + * Tool call objects + */ +export class RunToolCallObject extends S.Class("RunToolCallObject")({ + /** + * The ID of the tool call. This ID must be referenced when you submit the tool outputs in using the [Submit tool outputs to run](https://platform.openai.com/docs/api-reference/runs/submitToolOutputs) endpoint. + */ + "id": S.String, + /** + * The type of tool call the output is required for. For now, this is always `function`. + */ + "type": RunToolCallObjectType, + /** + * The function definition. + */ + "function": S.Struct({ + /** + * The name of the function. + */ + "name": S.String, + /** + * The arguments that the model expects you to pass to the function. + */ + "arguments": S.String + }) +}) {} + +/** + * One of `server_error`, `rate_limit_exceeded`, or `invalid_prompt`. + */ +export class RunObjectLastErrorCode extends S.Literal("server_error", "rate_limit_exceeded", "invalid_prompt") {} + +/** + * The reason why the run is incomplete. This will point to which specific token limit was reached over the course of the run. + */ +export class RunObjectIncompleteDetailsReason extends S.Literal("max_completion_tokens", "max_prompt_tokens") {} + +export class RunCompletionUsage extends S.Union( + /** + * Usage statistics related to the run. This value will be `null` if the run is not in a terminal state (i.e. `in_progress`, `queued`, etc.). + */ + S.Struct({ + /** + * Number of completion tokens used over the course of the run. + */ + "completion_tokens": S.Int, + /** + * Number of prompt tokens used over the course of the run. + */ + "prompt_tokens": S.Int, + /** + * Total number of tokens used (prompt + completion). + */ + "total_tokens": S.Int + }), + S.Null +) {} + +/** + * The truncation strategy to use for the thread. The default is `auto`. If set to `last_messages`, the thread will be truncated to the n most recent messages in the thread. When set to `auto`, messages in the middle of the thread will be dropped to fit the context length of the model, `max_prompt_tokens`. + */ +export class RunObjectTruncationStrategyEnumType extends S.Literal("auto", "last_messages") {} + +/** + * Controls for how a thread will be truncated prior to the run. Use this to control the initial context window of the run. + */ +export class RunObjectTruncationStrategy extends S.Struct({ + /** + * The truncation strategy to use for the thread. The default is `auto`. If set to `last_messages`, the thread will be truncated to the n most recent messages in the thread. When set to `auto`, messages in the middle of the thread will be dropped to fit the context length of the model, `max_prompt_tokens`. + */ + "type": S.Literal("auto", "last_messages"), + "last_messages": S.optionalWith(S.Int.pipe(S.greaterThanOrEqualTo(1)), { nullable: true }) +}) {} + +/** + * `none` means the model will not call any tools and instead generates a message. `auto` means the model can pick between generating a message or calling one or more tools. `required` means the model must call one or more tools before responding to the user. + */ +export class RunObjectToolChoiceEnum extends S.Literal("none", "auto", "required") {} + +/** + * Controls which (if any) tool is called by the model. + * `none` means the model will not call any tools and instead generates a message. + * `auto` is the default value and means the model can pick between generating a message or calling one or more tools. + * `required` means the model must call one or more tools before responding to the user. + * Specifying a particular tool like `{"type": "file_search"}` or `{"type": "function", "function": {"name": "my_function"}}` forces the model to call that tool. + */ +export class RunObjectToolChoice extends S.Union( + /** + * `none` means the model will not call any tools and instead generates a message. `auto` means the model can pick between generating a message or calling one or more tools. `required` means the model must call one or more tools before responding to the user. + */ + RunObjectToolChoiceEnum, + AssistantsNamedToolChoice +) {} + +/** + * Represents an execution run on a [thread](https://platform.openai.com/docs/api-reference/threads). + */ +export class RunObject extends S.Class("RunObject")({ + /** + * The identifier, which can be referenced in API endpoints. + */ + "id": S.String, + /** + * The object type, which is always `thread.run`. + */ + "object": RunObjectObject, + /** + * The Unix timestamp (in seconds) for when the run was created. + */ + "created_at": S.Int, + /** + * The ID of the [thread](https://platform.openai.com/docs/api-reference/threads) that was executed on as a part of this run. + */ + "thread_id": S.String, + /** + * The ID of the [assistant](https://platform.openai.com/docs/api-reference/assistants) used for execution of this run. + */ + "assistant_id": S.String, + "status": RunStatus, + /** + * Details on the action required to continue the run. Will be `null` if no action is required. + */ + "required_action": S.NullOr(S.Struct({ + /** + * For now, this is always `submit_tool_outputs`. + */ + "type": RunObjectRequiredActionType, + /** + * Details on the tool outputs needed for this run to continue. + */ + "submit_tool_outputs": S.Struct({ + /** + * A list of the relevant tool calls. + */ + "tool_calls": S.Array(RunToolCallObject) + }) + })), + /** + * The last error associated with this run. Will be `null` if there are no errors. + */ + "last_error": S.NullOr(S.Struct({ + /** + * One of `server_error`, `rate_limit_exceeded`, or `invalid_prompt`. + */ + "code": RunObjectLastErrorCode, + /** + * A human-readable description of the error. + */ + "message": S.String + })), + /** + * The Unix timestamp (in seconds) for when the run will expire. + */ + "expires_at": S.NullOr(S.Int), + /** + * The Unix timestamp (in seconds) for when the run was started. + */ + "started_at": S.NullOr(S.Int), + /** + * The Unix timestamp (in seconds) for when the run was cancelled. + */ + "cancelled_at": S.NullOr(S.Int), + /** + * The Unix timestamp (in seconds) for when the run failed. + */ + "failed_at": S.NullOr(S.Int), + /** + * The Unix timestamp (in seconds) for when the run was completed. + */ + "completed_at": S.NullOr(S.Int), + /** + * Details on why the run is incomplete. Will be `null` if the run is not incomplete. + */ + "incomplete_details": S.NullOr(S.Struct({ + /** + * The reason why the run is incomplete. This will point to which specific token limit was reached over the course of the run. + */ + "reason": S.optionalWith(RunObjectIncompleteDetailsReason, { nullable: true }) + })), + /** + * The model that the [assistant](https://platform.openai.com/docs/api-reference/assistants) used for this run. + */ + "model": S.String, + /** + * The instructions that the [assistant](https://platform.openai.com/docs/api-reference/assistants) used for this run. + */ + "instructions": S.String, + /** + * The list of tools that the [assistant](https://platform.openai.com/docs/api-reference/assistants) used for this run. + */ + "tools": S.Array(AssistantTool).pipe(S.maxItems(20)).pipe( + S.propertySignature, + S.withConstructorDefault(() => [] as const) + ), + "metadata": S.NullOr(S.Record({ key: S.String, value: S.Unknown })), + "usage": S.NullOr(S.Struct({ + /** + * Number of completion tokens used over the course of the run. + */ + "completion_tokens": S.Int, + /** + * Number of prompt tokens used over the course of the run. + */ + "prompt_tokens": S.Int, + /** + * Total number of tokens used (prompt + completion). + */ + "total_tokens": S.Int + })), + /** + * The sampling temperature used for this run. If not set, defaults to 1. + */ + "temperature": S.optionalWith(S.Number, { nullable: true }), + /** + * The nucleus sampling value used for this run. If not set, defaults to 1. + */ + "top_p": S.optionalWith(S.Number, { nullable: true }), + /** + * The maximum number of prompt tokens specified to have been used over the course of the run. + */ + "max_prompt_tokens": S.NullOr(S.Int.pipe(S.greaterThanOrEqualTo(256))), + /** + * The maximum number of completion tokens specified to have been used over the course of the run. + */ + "max_completion_tokens": S.NullOr(S.Int.pipe(S.greaterThanOrEqualTo(256))), + "truncation_strategy": RunObjectTruncationStrategy, + "tool_choice": RunObjectToolChoice, + "parallel_tool_calls": ParallelToolCalls.pipe(S.propertySignature, S.withConstructorDefault(() => true as const)), + "response_format": AssistantsApiResponseFormatOption +}) {} + +export class ModifyThreadRequest extends S.Class("ModifyThreadRequest")({ + "tool_resources": S.optionalWith( + S.Struct({ + "code_interpreter": S.optionalWith( + S.Struct({ + /** + * A list of [file](https://platform.openai.com/docs/api-reference/files) IDs made available to the `code_interpreter` tool. There can be a maximum of 20 files associated with the tool. + */ + "file_ids": S.optionalWith(S.Array(S.String).pipe(S.maxItems(20)), { + nullable: true, + default: () => [] as const + }) + }), + { nullable: true } + ), + "file_search": S.optionalWith( + S.Struct({ + /** + * The [vector store](https://platform.openai.com/docs/api-reference/vector-stores/object) attached to this thread. There can be a maximum of 1 vector store attached to the thread. + */ + "vector_store_ids": S.optionalWith(S.Array(S.String).pipe(S.maxItems(1)), { nullable: true }) + }), + { nullable: true } + ) + }), + { nullable: true } + ), + "metadata": S.optionalWith(S.Record({ key: S.String, value: S.Unknown }), { nullable: true }) +}) {} + +export class DeleteThreadResponseObject extends S.Literal("thread.deleted") {} + +export class DeleteThreadResponse extends S.Class("DeleteThreadResponse")({ + "id": S.String, + "deleted": S.Boolean, + "object": DeleteThreadResponseObject +}) {} + +export class ListMessagesParamsOrder extends S.Literal("asc", "desc") {} + +export class ListMessagesParams extends S.Struct({ + "limit": S.optionalWith(S.Int, { nullable: true, default: () => 20 as const }), + "order": S.optionalWith(ListMessagesParamsOrder, { nullable: true, default: () => "desc" as const }), + "after": S.optionalWith(S.String, { nullable: true }), + "before": S.optionalWith(S.String, { nullable: true }), + "run_id": S.optionalWith(S.String, { nullable: true }) +}) {} + +/** + * The object type, which is always `thread.message`. + */ +export class MessageObjectObject extends S.Literal("thread.message") {} + +/** + * The status of the message, which can be either `in_progress`, `incomplete`, or `completed`. + */ +export class MessageObjectStatus extends S.Literal("in_progress", "incomplete", "completed") {} + +/** + * The reason the message is incomplete. + */ +export class MessageObjectIncompleteDetailsEnumReason + extends S.Literal("content_filter", "max_tokens", "run_cancelled", "run_expired", "run_failed") +{} + +/** + * The entity that produced the message. One of `user` or `assistant`. + */ +export class MessageObjectRole extends S.Literal("user", "assistant") {} + +/** + * Always `text`. + */ +export class MessageContentTextObjectType extends S.Literal("text") {} + +/** + * Always `file_citation`. + */ +export class MessageContentTextAnnotationsFileCitationObjectType extends S.Literal("file_citation") {} + +/** + * A citation within the message that points to a specific quote from a specific File associated with the assistant or the message. Generated when the assistant uses the "file_search" tool to search files. + */ +export class MessageContentTextAnnotationsFileCitationObject + extends S.Class("MessageContentTextAnnotationsFileCitationObject")({ + /** + * Always `file_citation`. + */ + "type": MessageContentTextAnnotationsFileCitationObjectType, + /** + * The text in the message content that needs to be replaced. + */ + "text": S.String, + "file_citation": S.Struct({ + /** + * The ID of the specific File the citation is from. + */ + "file_id": S.String + }), + "start_index": S.Int.pipe(S.greaterThanOrEqualTo(0)), + "end_index": S.Int.pipe(S.greaterThanOrEqualTo(0)) + }) +{} + +/** + * Always `file_path`. + */ +export class MessageContentTextAnnotationsFilePathObjectType extends S.Literal("file_path") {} + +/** + * A URL for the file that's generated when the assistant used the `code_interpreter` tool to generate a file. + */ +export class MessageContentTextAnnotationsFilePathObject + extends S.Class("MessageContentTextAnnotationsFilePathObject")({ + /** + * Always `file_path`. + */ + "type": MessageContentTextAnnotationsFilePathObjectType, + /** + * The text in the message content that needs to be replaced. + */ + "text": S.String, + "file_path": S.Struct({ + /** + * The ID of the file that was generated. + */ + "file_id": S.String + }), + "start_index": S.Int.pipe(S.greaterThanOrEqualTo(0)), + "end_index": S.Int.pipe(S.greaterThanOrEqualTo(0)) + }) +{} + +export class TextAnnotation + extends S.Union(MessageContentTextAnnotationsFileCitationObject, MessageContentTextAnnotationsFilePathObject) +{} + +/** + * The text content that is part of a message. + */ +export class MessageContentTextObject extends S.Class("MessageContentTextObject")({ + /** + * Always `text`. + */ + "type": MessageContentTextObjectType, + "text": S.Struct({ + /** + * The data that makes up the text. + */ + "value": S.String, + "annotations": S.Array(TextAnnotation) + }) +}) {} + +/** + * Always `refusal`. + */ +export class MessageContentRefusalObjectType extends S.Literal("refusal") {} + +/** + * The refusal content generated by the assistant. + */ +export class MessageContentRefusalObject extends S.Class("MessageContentRefusalObject")({ + /** + * Always `refusal`. + */ + "type": MessageContentRefusalObjectType, + "refusal": S.String +}) {} + +export class MessageContent extends S.Union( + MessageContentImageFileObject, + MessageContentImageUrlObject, + MessageContentTextObject, + MessageContentRefusalObject +) {} + +/** + * Represents a message within a [thread](https://platform.openai.com/docs/api-reference/threads). + */ +export class MessageObject extends S.Class("MessageObject")({ + /** + * The identifier, which can be referenced in API endpoints. + */ + "id": S.String, + /** + * The object type, which is always `thread.message`. + */ + "object": MessageObjectObject, + /** + * The Unix timestamp (in seconds) for when the message was created. + */ + "created_at": S.Int, + /** + * The [thread](https://platform.openai.com/docs/api-reference/threads) ID that this message belongs to. + */ + "thread_id": S.String, + /** + * The status of the message, which can be either `in_progress`, `incomplete`, or `completed`. + */ + "status": MessageObjectStatus, + "incomplete_details": S.NullOr(S.Struct({ + /** + * The reason the message is incomplete. + */ + "reason": S.Literal("content_filter", "max_tokens", "run_cancelled", "run_expired", "run_failed") + })), + "completed_at": S.NullOr(S.Int), + "incomplete_at": S.NullOr(S.Int), + /** + * The entity that produced the message. One of `user` or `assistant`. + */ + "role": MessageObjectRole, + /** + * The content of the message in array of text and/or images. + */ + "content": S.Array(MessageContent), + "assistant_id": S.NullOr(S.String), + "run_id": S.NullOr(S.String), + "attachments": S.NullOr(S.Array(S.Struct({ + /** + * The ID of the file to attach to the message. + */ + "file_id": S.optionalWith(S.String, { nullable: true }), + /** + * The tools to add this file to. + */ + "tools": S.optionalWith(S.Array(S.Union(AssistantToolsCode, AssistantToolsFileSearchTypeOnly)), { nullable: true }) + }))), + "metadata": S.NullOr(S.Record({ key: S.String, value: S.Unknown })) +}) {} + +export class ListMessagesResponse extends S.Class("ListMessagesResponse")({ + "object": S.String, + "data": S.Array(MessageObject), + "first_id": S.String, + "last_id": S.String, + "has_more": S.Boolean +}) {} + +export class ModifyMessageRequest extends S.Class("ModifyMessageRequest")({ + "metadata": S.optionalWith(S.Record({ key: S.String, value: S.Unknown }), { nullable: true }) +}) {} + +export class DeleteMessageResponseObject extends S.Literal("thread.message.deleted") {} + +export class DeleteMessageResponse extends S.Class("DeleteMessageResponse")({ + "id": S.String, + "deleted": S.Boolean, + "object": DeleteMessageResponseObject +}) {} + +export class ListRunsParamsOrder extends S.Literal("asc", "desc") {} + +export class ListRunsParams extends S.Struct({ + "limit": S.optionalWith(S.Int, { nullable: true, default: () => 20 as const }), + "order": S.optionalWith(ListRunsParamsOrder, { nullable: true, default: () => "desc" as const }), + "after": S.optionalWith(S.String, { nullable: true }), + "before": S.optionalWith(S.String, { nullable: true }) +}) {} + +export class ListRunsResponse extends S.Class("ListRunsResponse")({ + "object": S.String, + "data": S.Array(RunObject), + "first_id": S.String, + "last_id": S.String, + "has_more": S.Boolean +}) {} + +export class CreateRunParams extends S.Struct({ + "include[]": S.optionalWith(S.Array(S.Literal("step_details.tool_calls[*].file_search.results[*].content")), { + nullable: true + }) +}) {} + +/** + * The truncation strategy to use for the thread. The default is `auto`. If set to `last_messages`, the thread will be truncated to the n most recent messages in the thread. When set to `auto`, messages in the middle of the thread will be dropped to fit the context length of the model, `max_prompt_tokens`. + */ +export class CreateRunRequestTruncationStrategyEnumType extends S.Literal("auto", "last_messages") {} + +/** + * Controls for how a thread will be truncated prior to the run. Use this to control the initial context window of the run. + */ +export class CreateRunRequestTruncationStrategy extends S.Struct({ + /** + * The truncation strategy to use for the thread. The default is `auto`. If set to `last_messages`, the thread will be truncated to the n most recent messages in the thread. When set to `auto`, messages in the middle of the thread will be dropped to fit the context length of the model, `max_prompt_tokens`. + */ + "type": S.Literal("auto", "last_messages"), + "last_messages": S.optionalWith(S.Int.pipe(S.greaterThanOrEqualTo(1)), { nullable: true }) +}) {} + +/** + * `none` means the model will not call any tools and instead generates a message. `auto` means the model can pick between generating a message or calling one or more tools. `required` means the model must call one or more tools before responding to the user. + */ +export class CreateRunRequestToolChoiceEnum extends S.Literal("none", "auto", "required") {} + +/** + * Controls which (if any) tool is called by the model. + * `none` means the model will not call any tools and instead generates a message. + * `auto` is the default value and means the model can pick between generating a message or calling one or more tools. + * `required` means the model must call one or more tools before responding to the user. + * Specifying a particular tool like `{"type": "file_search"}` or `{"type": "function", "function": {"name": "my_function"}}` forces the model to call that tool. + */ +export class CreateRunRequestToolChoice extends S.Union( + /** + * `none` means the model will not call any tools and instead generates a message. `auto` means the model can pick between generating a message or calling one or more tools. `required` means the model must call one or more tools before responding to the user. + */ + CreateRunRequestToolChoiceEnum, + AssistantsNamedToolChoice +) {} + +export class CreateRunRequest extends S.Class("CreateRunRequest")({ + /** + * The ID of the [assistant](https://platform.openai.com/docs/api-reference/assistants) to use to execute this run. + */ + "assistant_id": S.String, + /** + * The ID of the [Model](https://platform.openai.com/docs/api-reference/models) to be used to execute this run. If a value is provided here, it will override the model associated with the assistant. If not, the model associated with the assistant will be used. + */ + "model": S.optionalWith(S.Union(S.String, AssistantSupportedModels), { nullable: true }), + "reasoning_effort": S.optionalWith(S.Literal("none", "minimal", "low", "medium", "high"), { nullable: true }), + /** + * Overrides the [instructions](https://platform.openai.com/docs/api-reference/assistants/createAssistant) of the assistant. This is useful for modifying the behavior on a per-run basis. + */ + "instructions": S.optionalWith(S.String, { nullable: true }), + /** + * Appends additional instructions at the end of the instructions for the run. This is useful for modifying the behavior on a per-run basis without overriding other instructions. + */ + "additional_instructions": S.optionalWith(S.String, { nullable: true }), + /** + * Adds additional messages to the thread before creating the run. + */ + "additional_messages": S.optionalWith(S.Array(CreateMessageRequest), { nullable: true }), + /** + * Override the tools the assistant can use for this run. This is useful for modifying the behavior on a per-run basis. + */ + "tools": S.optionalWith(S.Array(AssistantTool).pipe(S.maxItems(20)), { nullable: true }), + "metadata": S.optionalWith(S.Record({ key: S.String, value: S.Unknown }), { nullable: true }), + /** + * What sampling temperature to use, between 0 and 2. Higher values like 0.8 will make the output more random, while lower values like 0.2 will make it more focused and deterministic. + */ + "temperature": S.optionalWith(S.Number.pipe(S.greaterThanOrEqualTo(0), S.lessThanOrEqualTo(2)), { + nullable: true, + default: () => 1 as const + }), + /** + * An alternative to sampling with temperature, called nucleus sampling, where the model considers the results of the tokens with top_p probability mass. So 0.1 means only the tokens comprising the top 10% probability mass are considered. + * + * We generally recommend altering this or temperature but not both. + */ + "top_p": S.optionalWith(S.Number.pipe(S.greaterThanOrEqualTo(0), S.lessThanOrEqualTo(1)), { + nullable: true, + default: () => 1 as const + }), + /** + * If `true`, returns a stream of events that happen during the Run as server-sent events, terminating when the Run enters a terminal state with a `data: [DONE]` message. + */ + "stream": S.optionalWith(S.Boolean, { nullable: true }), + /** + * The maximum number of prompt tokens that may be used over the course of the run. The run will make a best effort to use only the number of prompt tokens specified, across multiple turns of the run. If the run exceeds the number of prompt tokens specified, the run will end with status `incomplete`. See `incomplete_details` for more info. + */ + "max_prompt_tokens": S.optionalWith(S.Int.pipe(S.greaterThanOrEqualTo(256)), { nullable: true }), + /** + * The maximum number of completion tokens that may be used over the course of the run. The run will make a best effort to use only the number of completion tokens specified, across multiple turns of the run. If the run exceeds the number of completion tokens specified, the run will end with status `incomplete`. See `incomplete_details` for more info. + */ + "max_completion_tokens": S.optionalWith(S.Int.pipe(S.greaterThanOrEqualTo(256)), { nullable: true }), + "truncation_strategy": S.optionalWith(CreateRunRequestTruncationStrategy, { nullable: true }), + "tool_choice": S.optionalWith(CreateRunRequestToolChoice, { nullable: true }), + "parallel_tool_calls": S.optionalWith(ParallelToolCalls, { nullable: true, default: () => true as const }), + "response_format": S.optionalWith(AssistantsApiResponseFormatOption, { nullable: true }) +}) {} + +export class ModifyRunRequest extends S.Class("ModifyRunRequest")({ + "metadata": S.optionalWith(S.Record({ key: S.String, value: S.Unknown }), { nullable: true }) +}) {} + +export class ListRunStepsParamsOrder extends S.Literal("asc", "desc") {} + +export class ListRunStepsParams extends S.Struct({ + "limit": S.optionalWith(S.Int, { nullable: true, default: () => 20 as const }), + "order": S.optionalWith(ListRunStepsParamsOrder, { nullable: true, default: () => "desc" as const }), + "after": S.optionalWith(S.String, { nullable: true }), + "before": S.optionalWith(S.String, { nullable: true }), + "include[]": S.optionalWith(S.Array(S.Literal("step_details.tool_calls[*].file_search.results[*].content")), { + nullable: true + }) +}) {} + +/** + * The object type, which is always `thread.run.step`. + */ +export class RunStepObjectObject extends S.Literal("thread.run.step") {} + +/** + * The type of run step, which can be either `message_creation` or `tool_calls`. + */ +export class RunStepObjectType extends S.Literal("message_creation", "tool_calls") {} + +/** + * The status of the run step, which can be either `in_progress`, `cancelled`, `failed`, `completed`, or `expired`. + */ +export class RunStepObjectStatus extends S.Literal("in_progress", "cancelled", "failed", "completed", "expired") {} + +/** + * Always `message_creation`. + */ +export class RunStepDetailsMessageCreationObjectType extends S.Literal("message_creation") {} + +/** + * Details of the message creation by the run step. + */ +export class RunStepDetailsMessageCreationObject + extends S.Class("RunStepDetailsMessageCreationObject")({ + /** + * Always `message_creation`. + */ + "type": RunStepDetailsMessageCreationObjectType, + "message_creation": S.Struct({ + /** + * The ID of the message that was created by this run step. + */ + "message_id": S.String + }) + }) +{} + +/** + * Always `tool_calls`. + */ +export class RunStepDetailsToolCallsObjectType extends S.Literal("tool_calls") {} + +/** + * The type of tool call. This is always going to be `code_interpreter` for this type of tool call. + */ +export class RunStepDetailsToolCallsCodeObjectType extends S.Literal("code_interpreter") {} + +/** + * Always `logs`. + */ +export class RunStepDetailsToolCallsCodeOutputLogsObjectType extends S.Literal("logs") {} + +/** + * Text output from the Code Interpreter tool call as part of a run step. + */ +export class RunStepDetailsToolCallsCodeOutputLogsObject + extends S.Class("RunStepDetailsToolCallsCodeOutputLogsObject")({ + /** + * Always `logs`. + */ + "type": RunStepDetailsToolCallsCodeOutputLogsObjectType, + /** + * The text output from the Code Interpreter tool call. + */ + "logs": S.String + }) +{} + +/** + * Always `image`. + */ +export class RunStepDetailsToolCallsCodeOutputImageObjectType extends S.Literal("image") {} + +export class RunStepDetailsToolCallsCodeOutputImageObject + extends S.Class("RunStepDetailsToolCallsCodeOutputImageObject")({ + /** + * Always `image`. + */ + "type": RunStepDetailsToolCallsCodeOutputImageObjectType, + "image": S.Struct({ + /** + * The [file](https://platform.openai.com/docs/api-reference/files) ID of the image. + */ + "file_id": S.String + }) + }) +{} + +/** + * Details of the Code Interpreter tool call the run step was involved in. + */ +export class RunStepDetailsToolCallsCodeObject + extends S.Class("RunStepDetailsToolCallsCodeObject")({ + /** + * The ID of the tool call. + */ + "id": S.String, + /** + * The type of tool call. This is always going to be `code_interpreter` for this type of tool call. + */ + "type": RunStepDetailsToolCallsCodeObjectType, + /** + * The Code Interpreter tool call definition. + */ + "code_interpreter": S.Struct({ + /** + * The input to the Code Interpreter tool call. + */ + "input": S.String, + /** + * The outputs from the Code Interpreter tool call. Code Interpreter can output one or more items, including text (`logs`) or images (`image`). Each of these are represented by a different object type. + */ + "outputs": S.Array(S.Record({ key: S.String, value: S.Unknown })) + }) + }) +{} + +/** + * The type of tool call. This is always going to be `file_search` for this type of tool call. + */ +export class RunStepDetailsToolCallsFileSearchObjectType extends S.Literal("file_search") {} + +/** + * The ranking options for the file search. + */ +export class RunStepDetailsToolCallsFileSearchRankingOptionsObject + extends S.Class( + "RunStepDetailsToolCallsFileSearchRankingOptionsObject" + )({ + "ranker": FileSearchRanker, + /** + * The score threshold for the file search. All values must be a floating point number between 0 and 1. + */ + "score_threshold": S.Number.pipe(S.greaterThanOrEqualTo(0), S.lessThanOrEqualTo(1)) + }) +{} + +/** + * A result instance of the file search. + */ +export class RunStepDetailsToolCallsFileSearchResultObject + extends S.Class("RunStepDetailsToolCallsFileSearchResultObject")({ + /** + * The ID of the file that result was found in. + */ + "file_id": S.String, + /** + * The name of the file that result was found in. + */ + "file_name": S.String, + /** + * The score of the result. All values must be a floating point number between 0 and 1. + */ + "score": S.Number.pipe(S.greaterThanOrEqualTo(0), S.lessThanOrEqualTo(1)), + /** + * The content of the result that was found. The content is only included if requested via the include query parameter. + */ + "content": S.optionalWith( + S.Array(S.Struct({ + /** + * The type of the content. + */ + "type": S.optionalWith(S.Literal("text"), { nullable: true }), + /** + * The text content of the file. + */ + "text": S.optionalWith(S.String, { nullable: true }) + })), + { nullable: true } + ) + }) +{} + +export class RunStepDetailsToolCallsFileSearchObject + extends S.Class("RunStepDetailsToolCallsFileSearchObject")({ + /** + * The ID of the tool call object. + */ + "id": S.String, + /** + * The type of tool call. This is always going to be `file_search` for this type of tool call. + */ + "type": RunStepDetailsToolCallsFileSearchObjectType, + /** + * For now, this is always going to be an empty object. + */ + "file_search": S.Struct({ + "ranking_options": S.optionalWith(RunStepDetailsToolCallsFileSearchRankingOptionsObject, { nullable: true }), + /** + * The results of the file search. + */ + "results": S.optionalWith(S.Array(RunStepDetailsToolCallsFileSearchResultObject), { nullable: true }) + }) + }) +{} + +/** + * The type of tool call. This is always going to be `function` for this type of tool call. + */ +export class RunStepDetailsToolCallsFunctionObjectType extends S.Literal("function") {} + +export class RunStepDetailsToolCallsFunctionObject + extends S.Class("RunStepDetailsToolCallsFunctionObject")({ + /** + * The ID of the tool call object. + */ + "id": S.String, + /** + * The type of tool call. This is always going to be `function` for this type of tool call. + */ + "type": RunStepDetailsToolCallsFunctionObjectType, + /** + * The definition of the function that was called. + */ + "function": S.Struct({ + /** + * The name of the function. + */ + "name": S.String, + /** + * The arguments passed to the function. + */ + "arguments": S.String, + "output": S.NullOr(S.String) + }) + }) +{} + +export class RunStepDetailsToolCall extends S.Union( + RunStepDetailsToolCallsCodeObject, + RunStepDetailsToolCallsFileSearchObject, + RunStepDetailsToolCallsFunctionObject +) {} + +/** + * Details of the tool call. + */ +export class RunStepDetailsToolCallsObject + extends S.Class("RunStepDetailsToolCallsObject")({ + /** + * Always `tool_calls`. + */ + "type": RunStepDetailsToolCallsObjectType, + /** + * An array of tool calls the run step was involved in. These can be associated with one of three types of tools: `code_interpreter`, `file_search`, or `function`. + */ + "tool_calls": S.Array(RunStepDetailsToolCall) + }) +{} + +/** + * One of `server_error` or `rate_limit_exceeded`. + */ +export class RunStepObjectLastErrorEnumCode extends S.Literal("server_error", "rate_limit_exceeded") {} + +export class RunStepCompletionUsage extends S.Union( + /** + * Usage statistics related to the run step. This value will be `null` while the run step's status is `in_progress`. + */ + S.Struct({ + /** + * Number of completion tokens used over the course of the run step. + */ + "completion_tokens": S.Int, + /** + * Number of prompt tokens used over the course of the run step. + */ + "prompt_tokens": S.Int, + /** + * Total number of tokens used (prompt + completion). + */ + "total_tokens": S.Int + }), + S.Null +) {} + +/** + * Represents a step in execution of a run. + */ +export class RunStepObject extends S.Class("RunStepObject")({ + /** + * The identifier of the run step, which can be referenced in API endpoints. + */ + "id": S.String, + /** + * The object type, which is always `thread.run.step`. + */ + "object": RunStepObjectObject, + /** + * The Unix timestamp (in seconds) for when the run step was created. + */ + "created_at": S.Int, + /** + * The ID of the [assistant](https://platform.openai.com/docs/api-reference/assistants) associated with the run step. + */ + "assistant_id": S.String, + /** + * The ID of the [thread](https://platform.openai.com/docs/api-reference/threads) that was run. + */ + "thread_id": S.String, + /** + * The ID of the [run](https://platform.openai.com/docs/api-reference/runs) that this run step is a part of. + */ + "run_id": S.String, + /** + * The type of run step, which can be either `message_creation` or `tool_calls`. + */ + "type": RunStepObjectType, + /** + * The status of the run step, which can be either `in_progress`, `cancelled`, `failed`, `completed`, or `expired`. + */ + "status": RunStepObjectStatus, + /** + * The details of the run step. + */ + "step_details": S.Record({ key: S.String, value: S.Unknown }), + "last_error": S.NullOr(S.Struct({ + /** + * One of `server_error` or `rate_limit_exceeded`. + */ + "code": S.Literal("server_error", "rate_limit_exceeded"), + /** + * A human-readable description of the error. + */ + "message": S.String + })), + "expired_at": S.NullOr(S.Int), + "cancelled_at": S.NullOr(S.Int), + "failed_at": S.NullOr(S.Int), + "completed_at": S.NullOr(S.Int), + "metadata": S.NullOr(S.Record({ key: S.String, value: S.Unknown })), + "usage": S.NullOr(S.Struct({ + /** + * Number of completion tokens used over the course of the run step. + */ + "completion_tokens": S.Int, + /** + * Number of prompt tokens used over the course of the run step. + */ + "prompt_tokens": S.Int, + /** + * Total number of tokens used (prompt + completion). + */ + "total_tokens": S.Int + })) +}) {} + +export class ListRunStepsResponse extends S.Class("ListRunStepsResponse")({ + "object": S.String, + "data": S.Array(RunStepObject), + "first_id": S.String, + "last_id": S.String, + "has_more": S.Boolean +}) {} + +export class GetRunStepParams extends S.Struct({ + "include[]": S.optionalWith(S.Array(S.Literal("step_details.tool_calls[*].file_search.results[*].content")), { + nullable: true + }) +}) {} + +export class SubmitToolOutputsRunRequest extends S.Class("SubmitToolOutputsRunRequest")({ + /** + * A list of tools for which the outputs are being submitted. + */ + "tool_outputs": S.Array(S.Struct({ + /** + * The ID of the tool call in the `required_action` object within the run object the output is being submitted for. + */ + "tool_call_id": S.optionalWith(S.String, { nullable: true }), + /** + * The output of the tool call to be submitted to continue the run. + */ + "output": S.optionalWith(S.String, { nullable: true }) + })), + "stream": S.optionalWith(S.Boolean, { nullable: true }) +}) {} + +/** + * The intended purpose of the uploaded file. + * + * See the [documentation on File purposes](https://platform.openai.com/docs/api-reference/files/create#files-create-purpose). + */ +export class CreateUploadRequestPurpose extends S.Literal("assistants", "batch", "fine-tune", "vision") {} + +export class CreateUploadRequest extends S.Class("CreateUploadRequest")({ + /** + * The name of the file to upload. + */ + "filename": S.String, + /** + * The intended purpose of the uploaded file. + * + * See the [documentation on File purposes](https://platform.openai.com/docs/api-reference/files/create#files-create-purpose). + */ + "purpose": CreateUploadRequestPurpose, + /** + * The number of bytes in the file you are uploading. + */ + "bytes": S.Int, + /** + * The MIME type of the file. + * + * This must fall within the supported MIME types for your file purpose. See the supported MIME types for assistants and vision. + */ + "mime_type": S.String, + "expires_after": S.optionalWith(FileExpirationAfter, { nullable: true }) +}) {} + +/** + * The status of the Upload. + */ +export class UploadStatus extends S.Literal("pending", "completed", "cancelled", "expired") {} + +/** + * The object type, which is always "upload". + */ +export class UploadObject extends S.Literal("upload") {} + +/** + * The object type, which is always `file`. + */ +export class UploadFileEnumObject extends S.Literal("file") {} + +/** + * The intended purpose of the file. Supported values are `assistants`, `assistants_output`, `batch`, `batch_output`, `fine-tune`, `fine-tune-results`, `vision`, and `user_data`. + */ +export class UploadFileEnumPurpose extends S.Literal( + "assistants", + "assistants_output", + "batch", + "batch_output", + "fine-tune", + "fine-tune-results", + "vision", + "user_data" +) {} + +/** + * Deprecated. The current status of the file, which can be either `uploaded`, `processed`, or `error`. + */ +export class UploadFileEnumStatus extends S.Literal("uploaded", "processed", "error") {} + +/** + * The ready File object after the Upload is completed. + */ +export class UploadFile extends S.Struct({ + /** + * The file identifier, which can be referenced in the API endpoints. + */ + "id": S.String, + /** + * The size of the file, in bytes. + */ + "bytes": S.Int, + /** + * The Unix timestamp (in seconds) for when the file was created. + */ + "created_at": S.Int, + /** + * The Unix timestamp (in seconds) for when the file will expire. + */ + "expires_at": S.optionalWith(S.Int, { nullable: true }), + /** + * The name of the file. + */ + "filename": S.String, + /** + * The object type, which is always `file`. + */ + "object": S.Literal("file"), + /** + * The intended purpose of the file. Supported values are `assistants`, `assistants_output`, `batch`, `batch_output`, `fine-tune`, `fine-tune-results`, `vision`, and `user_data`. + */ + "purpose": S.Literal( + "assistants", + "assistants_output", + "batch", + "batch_output", + "fine-tune", + "fine-tune-results", + "vision", + "user_data" + ), + /** + * Deprecated. The current status of the file, which can be either `uploaded`, `processed`, or `error`. + */ + "status": S.Literal("uploaded", "processed", "error"), + /** + * Deprecated. For details on why a fine-tuning training file failed validation, see the `error` field on `fine_tuning.job`. + */ + "status_details": S.optionalWith(S.String, { nullable: true }) +}) {} + +/** + * The Upload object can accept byte chunks in the form of Parts. + */ +export class Upload extends S.Class("Upload")({ + /** + * The Upload unique identifier, which can be referenced in API endpoints. + */ + "id": S.String, + /** + * The Unix timestamp (in seconds) for when the Upload was created. + */ + "created_at": S.Int, + /** + * The name of the file to be uploaded. + */ + "filename": S.String, + /** + * The intended number of bytes to be uploaded. + */ + "bytes": S.Int, + /** + * The intended purpose of the file. [Please refer here](https://platform.openai.com/docs/api-reference/files/object#files/object-purpose) for acceptable values. + */ + "purpose": S.String, + /** + * The status of the Upload. + */ + "status": UploadStatus, + /** + * The Unix timestamp (in seconds) for when the Upload will expire. + */ + "expires_at": S.Int, + /** + * The object type, which is always "upload". + */ + "object": UploadObject, + "file": S.optionalWith(UploadFile, { nullable: true }) +}) {} + +export class CompleteUploadRequest extends S.Class("CompleteUploadRequest")({ + /** + * The ordered list of Part IDs. + */ + "part_ids": S.Array(S.String), + /** + * The optional md5 checksum for the file contents to verify if the bytes uploaded matches what you expect. + */ + "md5": S.optionalWith(S.String, { nullable: true }) +}) {} + +export class AddUploadPartRequest extends S.Class("AddUploadPartRequest")({ + /** + * The chunk of bytes for this Part. + */ + "data": S.instanceOf(globalThis.Blob) +}) {} + +/** + * The object type, which is always `upload.part`. + */ +export class UploadPartObject extends S.Literal("upload.part") {} + +/** + * The upload Part represents a chunk of bytes we can add to an Upload object. + */ +export class UploadPart extends S.Class("UploadPart")({ + /** + * The upload Part unique identifier, which can be referenced in API endpoints. + */ + "id": S.String, + /** + * The Unix timestamp (in seconds) for when the Part was created. + */ + "created_at": S.Int, + /** + * The ID of the Upload object that this Part was added to. + */ + "upload_id": S.String, + /** + * The object type, which is always `upload.part`. + */ + "object": UploadPartObject +}) {} + +export class ListVectorStoresParamsOrder extends S.Literal("asc", "desc") {} + +export class ListVectorStoresParams extends S.Struct({ + "limit": S.optionalWith(S.Int, { nullable: true, default: () => 20 as const }), + "order": S.optionalWith(ListVectorStoresParamsOrder, { nullable: true, default: () => "desc" as const }), + "after": S.optionalWith(S.String, { nullable: true }), + "before": S.optionalWith(S.String, { nullable: true }) +}) {} + +/** + * The object type, which is always `vector_store`. + */ +export class VectorStoreObjectObject extends S.Literal("vector_store") {} + +/** + * The status of the vector store, which can be either `expired`, `in_progress`, or `completed`. A status of `completed` indicates that the vector store is ready for use. + */ +export class VectorStoreObjectStatus extends S.Literal("expired", "in_progress", "completed") {} + +/** + * Anchor timestamp after which the expiration policy applies. Supported anchors: `last_active_at`. + */ +export class VectorStoreExpirationAfterAnchor extends S.Literal("last_active_at") {} + +/** + * The expiration policy for a vector store. + */ +export class VectorStoreExpirationAfter extends S.Class("VectorStoreExpirationAfter")({ + /** + * Anchor timestamp after which the expiration policy applies. Supported anchors: `last_active_at`. + */ + "anchor": VectorStoreExpirationAfterAnchor, + /** + * The number of days after the anchor time that the vector store will expire. + */ + "days": S.Int.pipe(S.greaterThanOrEqualTo(1), S.lessThanOrEqualTo(365)) +}) {} + +/** + * A vector store is a collection of processed files can be used by the `file_search` tool. + */ +export class VectorStoreObject extends S.Class("VectorStoreObject")({ + /** + * The identifier, which can be referenced in API endpoints. + */ + "id": S.String, + /** + * The object type, which is always `vector_store`. + */ + "object": VectorStoreObjectObject, + /** + * The Unix timestamp (in seconds) for when the vector store was created. + */ + "created_at": S.Int, + /** + * The name of the vector store. + */ + "name": S.String, + /** + * The total number of bytes used by the files in the vector store. + */ + "usage_bytes": S.Int, + "file_counts": S.Struct({ + /** + * The number of files that are currently being processed. + */ + "in_progress": S.Int, + /** + * The number of files that have been successfully processed. + */ + "completed": S.Int, + /** + * The number of files that have failed to process. + */ + "failed": S.Int, + /** + * The number of files that were cancelled. + */ + "cancelled": S.Int, + /** + * The total number of files. + */ + "total": S.Int + }), + /** + * The status of the vector store, which can be either `expired`, `in_progress`, or `completed`. A status of `completed` indicates that the vector store is ready for use. + */ + "status": VectorStoreObjectStatus, + "expires_after": S.optionalWith(VectorStoreExpirationAfter, { nullable: true }), + "expires_at": S.optionalWith(S.Int, { nullable: true }), + "last_active_at": S.NullOr(S.Int), + "metadata": S.NullOr(S.Record({ key: S.String, value: S.Unknown })) +}) {} + +export class ListVectorStoresResponse extends S.Class("ListVectorStoresResponse")({ + "object": S.String, + "data": S.Array(VectorStoreObject), + "first_id": S.String, + "last_id": S.String, + "has_more": S.Boolean +}) {} + +/** + * Always `auto`. + */ +export class AutoChunkingStrategyRequestParamType extends S.Literal("auto") {} + +/** + * The default strategy. This strategy currently uses a `max_chunk_size_tokens` of `800` and `chunk_overlap_tokens` of `400`. + */ +export class AutoChunkingStrategyRequestParam + extends S.Class("AutoChunkingStrategyRequestParam")({ + /** + * Always `auto`. + */ + "type": AutoChunkingStrategyRequestParamType + }) +{} + +/** + * Always `static`. + */ +export class StaticChunkingStrategyRequestParamType extends S.Literal("static") {} + +export class StaticChunkingStrategy extends S.Class("StaticChunkingStrategy")({ + /** + * The maximum number of tokens in each chunk. The default value is `800`. The minimum value is `100` and the maximum value is `4096`. + */ + "max_chunk_size_tokens": S.Int.pipe(S.greaterThanOrEqualTo(100), S.lessThanOrEqualTo(4096)), + /** + * The number of tokens that overlap between chunks. The default value is `400`. + * + * Note that the overlap must not exceed half of `max_chunk_size_tokens`. + */ + "chunk_overlap_tokens": S.Int +}) {} + +/** + * Customize your own chunking strategy by setting chunk size and chunk overlap. + */ +export class StaticChunkingStrategyRequestParam + extends S.Class("StaticChunkingStrategyRequestParam")({ + /** + * Always `static`. + */ + "type": StaticChunkingStrategyRequestParamType, + "static": StaticChunkingStrategy + }) +{} + +/** + * The chunking strategy used to chunk the file(s). If not set, will use the `auto` strategy. Only applicable if `file_ids` is non-empty. + */ +export class ChunkingStrategyRequestParam extends S.Record({ key: S.String, value: S.Unknown }) {} + +export class CreateVectorStoreRequest extends S.Class("CreateVectorStoreRequest")({ + /** + * A list of [File](https://platform.openai.com/docs/api-reference/files) IDs that the vector store should use. Useful for tools like `file_search` that can access files. + */ + "file_ids": S.optionalWith(S.Array(S.String).pipe(S.maxItems(500)), { nullable: true }), + /** + * The name of the vector store. + */ + "name": S.optionalWith(S.String, { nullable: true }), + /** + * A description for the vector store. Can be used to describe the vector store's purpose. + */ + "description": S.optionalWith(S.String, { nullable: true }), + "expires_after": S.optionalWith(VectorStoreExpirationAfter, { nullable: true }), + "chunking_strategy": S.optionalWith(ChunkingStrategyRequestParam, { nullable: true }), + "metadata": S.optionalWith(S.Record({ key: S.String, value: S.Unknown }), { nullable: true }) +}) {} + +/** + * Anchor timestamp after which the expiration policy applies. Supported anchors: `last_active_at`. + */ +export class UpdateVectorStoreRequestExpiresAfterEnumAnchor extends S.Literal("last_active_at") {} + +/** + * The expiration policy for a vector store. + */ +export class UpdateVectorStoreRequestExpiresAfter extends S.Struct({ + /** + * Anchor timestamp after which the expiration policy applies. Supported anchors: `last_active_at`. + */ + "anchor": S.Literal("last_active_at"), + /** + * The number of days after the anchor time that the vector store will expire. + */ + "days": S.Int.pipe(S.greaterThanOrEqualTo(1), S.lessThanOrEqualTo(365)) +}) {} + +export class UpdateVectorStoreRequest extends S.Class("UpdateVectorStoreRequest")({ + /** + * The name of the vector store. + */ + "name": S.optionalWith(S.String, { nullable: true }), + "expires_after": S.optionalWith(UpdateVectorStoreRequestExpiresAfter, { nullable: true }), + "metadata": S.optionalWith(S.Record({ key: S.String, value: S.Unknown }), { nullable: true }) +}) {} + +export class DeleteVectorStoreResponseObject extends S.Literal("vector_store.deleted") {} + +export class DeleteVectorStoreResponse extends S.Class("DeleteVectorStoreResponse")({ + "id": S.String, + "deleted": S.Boolean, + "object": DeleteVectorStoreResponseObject +}) {} + +export class CreateVectorStoreFileRequest + extends S.Class("CreateVectorStoreFileRequest")({ + /** + * A [File](https://platform.openai.com/docs/api-reference/files) ID that the vector store should use. Useful for tools like `file_search` that can access files. + */ + "file_id": S.String, + "chunking_strategy": S.optionalWith(ChunkingStrategyRequestParam, { nullable: true }), + "attributes": S.optionalWith(S.Record({ key: S.String, value: S.Unknown }), { nullable: true }) + }) +{} + +export class CreateVectorStoreFileBatchRequest + extends S.Class("CreateVectorStoreFileBatchRequest")({ + /** + * A list of [File](https://platform.openai.com/docs/api-reference/files) IDs that the vector store should use. Useful for tools like `file_search` that can access files. If `attributes` or `chunking_strategy` are provided, they will be applied to all files in the batch. Mutually exclusive with `files`. + */ + "file_ids": S.optionalWith(S.NonEmptyArray(S.String).pipe(S.minItems(1), S.maxItems(500)), { nullable: true }), + /** + * A list of objects that each include a `file_id` plus optional `attributes` or `chunking_strategy`. Use this when you need to override metadata for specific files. The global `attributes` or `chunking_strategy` will be ignored and must be specified for each file. Mutually exclusive with `file_ids`. + */ + "files": S.optionalWith(S.NonEmptyArray(CreateVectorStoreFileRequest).pipe(S.minItems(1), S.maxItems(500)), { + nullable: true + }), + "chunking_strategy": S.optionalWith(ChunkingStrategyRequestParam, { nullable: true }), + "attributes": S.optionalWith(S.Record({ key: S.String, value: S.Unknown }), { nullable: true }) + }) +{} + +/** + * The object type, which is always `vector_store.file_batch`. + */ +export class VectorStoreFileBatchObjectObject extends S.Literal("vector_store.files_batch") {} + +/** + * The status of the vector store files batch, which can be either `in_progress`, `completed`, `cancelled` or `failed`. + */ +export class VectorStoreFileBatchObjectStatus extends S.Literal("in_progress", "completed", "cancelled", "failed") {} + +/** + * A batch of files attached to a vector store. + */ +export class VectorStoreFileBatchObject extends S.Class("VectorStoreFileBatchObject")({ + /** + * The identifier, which can be referenced in API endpoints. + */ + "id": S.String, + /** + * The object type, which is always `vector_store.file_batch`. + */ + "object": VectorStoreFileBatchObjectObject, + /** + * The Unix timestamp (in seconds) for when the vector store files batch was created. + */ + "created_at": S.Int, + /** + * The ID of the [vector store](https://platform.openai.com/docs/api-reference/vector-stores/object) that the [File](https://platform.openai.com/docs/api-reference/files) is attached to. + */ + "vector_store_id": S.String, + /** + * The status of the vector store files batch, which can be either `in_progress`, `completed`, `cancelled` or `failed`. + */ + "status": VectorStoreFileBatchObjectStatus, + "file_counts": S.Struct({ + /** + * The number of files that are currently being processed. + */ + "in_progress": S.Int, + /** + * The number of files that have been processed. + */ + "completed": S.Int, + /** + * The number of files that have failed to process. + */ + "failed": S.Int, + /** + * The number of files that where cancelled. + */ + "cancelled": S.Int, + /** + * The total number of files. + */ + "total": S.Int + }) +}) {} + +export class ListFilesInVectorStoreBatchParamsOrder extends S.Literal("asc", "desc") {} + +export class ListFilesInVectorStoreBatchParamsFilter + extends S.Literal("in_progress", "completed", "failed", "cancelled") +{} + +export class ListFilesInVectorStoreBatchParams extends S.Struct({ + "limit": S.optionalWith(S.Int, { nullable: true, default: () => 20 as const }), + "order": S.optionalWith(ListFilesInVectorStoreBatchParamsOrder, { nullable: true, default: () => "desc" as const }), + "after": S.optionalWith(S.String, { nullable: true }), + "before": S.optionalWith(S.String, { nullable: true }), + "filter": S.optionalWith(ListFilesInVectorStoreBatchParamsFilter, { nullable: true }) +}) {} + +/** + * The object type, which is always `vector_store.file`. + */ +export class VectorStoreFileObjectObject extends S.Literal("vector_store.file") {} + +/** + * The status of the vector store file, which can be either `in_progress`, `completed`, `cancelled`, or `failed`. The status `completed` indicates that the vector store file is ready for use. + */ +export class VectorStoreFileObjectStatus extends S.Literal("in_progress", "completed", "cancelled", "failed") {} + +/** + * One of `server_error`, `unsupported_file`, or `invalid_file`. + */ +export class VectorStoreFileObjectLastErrorEnumCode + extends S.Literal("server_error", "unsupported_file", "invalid_file") +{} + +/** + * Always `static`. + */ +export class StaticChunkingStrategyResponseParamType extends S.Literal("static") {} + +export class StaticChunkingStrategyResponseParam + extends S.Class("StaticChunkingStrategyResponseParam")({ + /** + * Always `static`. + */ + "type": StaticChunkingStrategyResponseParamType, + "static": StaticChunkingStrategy + }) +{} + +/** + * Always `other`. + */ +export class OtherChunkingStrategyResponseParamType extends S.Literal("other") {} + +/** + * This is returned when the chunking strategy is unknown. Typically, this is because the file was indexed before the `chunking_strategy` concept was introduced in the API. + */ +export class OtherChunkingStrategyResponseParam + extends S.Class("OtherChunkingStrategyResponseParam")({ + /** + * Always `other`. + */ + "type": OtherChunkingStrategyResponseParamType + }) +{} + +/** + * The strategy used to chunk the file. + */ +export class ChunkingStrategyResponse extends S.Record({ key: S.String, value: S.Unknown }) {} + +/** + * A list of files attached to a vector store. + */ +export class VectorStoreFileObject extends S.Class("VectorStoreFileObject")({ + /** + * The identifier, which can be referenced in API endpoints. + */ + "id": S.String, + /** + * The object type, which is always `vector_store.file`. + */ + "object": VectorStoreFileObjectObject, + /** + * The total vector store usage in bytes. Note that this may be different from the original file size. + */ + "usage_bytes": S.Int, + /** + * The Unix timestamp (in seconds) for when the vector store file was created. + */ + "created_at": S.Int, + /** + * The ID of the [vector store](https://platform.openai.com/docs/api-reference/vector-stores/object) that the [File](https://platform.openai.com/docs/api-reference/files) is attached to. + */ + "vector_store_id": S.String, + /** + * The status of the vector store file, which can be either `in_progress`, `completed`, `cancelled`, or `failed`. The status `completed` indicates that the vector store file is ready for use. + */ + "status": VectorStoreFileObjectStatus, + "last_error": S.NullOr(S.Struct({ + /** + * One of `server_error`, `unsupported_file`, or `invalid_file`. + */ + "code": S.Literal("server_error", "unsupported_file", "invalid_file"), + /** + * A human-readable description of the error. + */ + "message": S.String + })), + "chunking_strategy": S.optionalWith(ChunkingStrategyResponse, { nullable: true }), + "attributes": S.optionalWith(S.Record({ key: S.String, value: S.Unknown }), { nullable: true }) +}) {} + +export class ListVectorStoreFilesResponse + extends S.Class("ListVectorStoreFilesResponse")({ + "object": S.String, + "data": S.Array(VectorStoreFileObject), + "first_id": S.String, + "last_id": S.String, + "has_more": S.Boolean + }) +{} + +export class ListVectorStoreFilesParamsOrder extends S.Literal("asc", "desc") {} + +export class ListVectorStoreFilesParamsFilter extends S.Literal("in_progress", "completed", "failed", "cancelled") {} + +export class ListVectorStoreFilesParams extends S.Struct({ + "limit": S.optionalWith(S.Int, { nullable: true, default: () => 20 as const }), + "order": S.optionalWith(ListVectorStoreFilesParamsOrder, { nullable: true, default: () => "desc" as const }), + "after": S.optionalWith(S.String, { nullable: true }), + "before": S.optionalWith(S.String, { nullable: true }), + "filter": S.optionalWith(ListVectorStoreFilesParamsFilter, { nullable: true }) +}) {} + +export class UpdateVectorStoreFileAttributesRequest + extends S.Class("UpdateVectorStoreFileAttributesRequest")({ + "attributes": S.NullOr(S.Record({ key: S.String, value: S.Unknown })) + }) +{} + +export class DeleteVectorStoreFileResponseObject extends S.Literal("vector_store.file.deleted") {} + +export class DeleteVectorStoreFileResponse + extends S.Class("DeleteVectorStoreFileResponse")({ + "id": S.String, + "deleted": S.Boolean, + "object": DeleteVectorStoreFileResponseObject + }) +{} + +/** + * The object type, which is always `vector_store.file_content.page` + */ +export class VectorStoreFileContentResponseObject extends S.Literal("vector_store.file_content.page") {} + +/** + * Represents the parsed content of a vector store file. + */ +export class VectorStoreFileContentResponse + extends S.Class("VectorStoreFileContentResponse")({ + /** + * The object type, which is always `vector_store.file_content.page` + */ + "object": VectorStoreFileContentResponseObject, + /** + * Parsed content of the file. + */ + "data": S.Array(S.Struct({ + /** + * The content type (currently only `"text"`) + */ + "type": S.optionalWith(S.String, { nullable: true }), + /** + * The text content + */ + "text": S.optionalWith(S.String, { nullable: true }) + })), + /** + * Indicates if there are more content pages to fetch. + */ + "has_more": S.Boolean, + "next_page": S.NullOr(S.String) + }) +{} + +/** + * Enable re-ranking; set to `none` to disable, which can help reduce latency. + */ +export class VectorStoreSearchRequestRankingOptionsRanker extends S.Literal("none", "auto", "default-2024-11-15") {} + +export class VectorStoreSearchRequest extends S.Class("VectorStoreSearchRequest")({ + /** + * A query string for a search + */ + "query": S.Union(S.String, S.Array(S.String)), + /** + * Whether to rewrite the natural language query for vector search. + */ + "rewrite_query": S.optionalWith(S.Boolean, { nullable: true, default: () => false as const }), + /** + * The maximum number of results to return. This number should be between 1 and 50 inclusive. + */ + "max_num_results": S.optionalWith(S.Int.pipe(S.greaterThanOrEqualTo(1), S.lessThanOrEqualTo(50)), { + nullable: true, + default: () => 10 as const + }), + /** + * A filter to apply based on file attributes. + */ + "filters": S.optionalWith(S.Union(ComparisonFilter, CompoundFilter), { nullable: true }), + /** + * Ranking options for search. + */ + "ranking_options": S.optionalWith( + S.Struct({ + /** + * Enable re-ranking; set to `none` to disable, which can help reduce latency. + */ + "ranker": S.optionalWith(VectorStoreSearchRequestRankingOptionsRanker, { + nullable: true, + default: () => "auto" as const + }), + "score_threshold": S.optionalWith(S.Number.pipe(S.greaterThanOrEqualTo(0), S.lessThanOrEqualTo(1)), { + nullable: true, + default: () => 0 as const + }) + }), + { nullable: true } + ) +}) {} + +/** + * The object type, which is always `vector_store.search_results.page` + */ +export class VectorStoreSearchResultsPageObject extends S.Literal("vector_store.search_results.page") {} + +/** + * The type of content. + */ +export class VectorStoreSearchResultContentObjectType extends S.Literal("text") {} + +export class VectorStoreSearchResultContentObject + extends S.Class("VectorStoreSearchResultContentObject")({ + /** + * The type of content. + */ + "type": VectorStoreSearchResultContentObjectType, + /** + * The text content returned from search. + */ + "text": S.String + }) +{} + +export class VectorStoreSearchResultItem extends S.Class("VectorStoreSearchResultItem")({ + /** + * The ID of the vector store file. + */ + "file_id": S.String, + /** + * The name of the vector store file. + */ + "filename": S.String, + /** + * The similarity score for the result. + */ + "score": S.Number.pipe(S.greaterThanOrEqualTo(0), S.lessThanOrEqualTo(1)), + "attributes": S.NullOr(S.Record({ key: S.String, value: S.Unknown })), + /** + * Content chunks from the file. + */ + "content": S.Array(VectorStoreSearchResultContentObject) +}) {} + +export class VectorStoreSearchResultsPage + extends S.Class("VectorStoreSearchResultsPage")({ + /** + * The object type, which is always `vector_store.search_results.page` + */ + "object": VectorStoreSearchResultsPageObject, + "search_query": S.Array(S.String), + /** + * The list of search result items. + */ + "data": S.Array(VectorStoreSearchResultItem), + /** + * Indicates if there are more results to fetch. + */ + "has_more": S.Boolean, + "next_page": S.NullOr(S.String) + }) +{} + +export class CreateConversationBody extends S.Class("CreateConversationBody")({ + "metadata": S.optionalWith(Metadata, { nullable: true }), + "items": S.optionalWith(S.Array(InputItem).pipe(S.maxItems(20)), { nullable: true }) +}) {} + +export class UpdateConversationBody extends S.Class("UpdateConversationBody")({ + /** + * Set of 16 key-value pairs that can be attached to an object. This can be useful for storing additional information about the object in a structured format, and querying for objects via API or the dashboard. + * Keys are strings with a maximum length of 64 characters. Values are strings with a maximum length of 512 characters. + */ + "metadata": S.NullOr(S.Record({ key: S.String, value: S.Unknown })) +}) {} + +export class DeletedConversationResourceObject extends S.Literal("conversation.deleted") {} + +export class DeletedConversationResource extends S.Class("DeletedConversationResource")({ + "object": DeletedConversationResourceObject.pipe( + S.propertySignature, + S.withConstructorDefault(() => "conversation.deleted" as const) + ), + "deleted": S.Boolean, + "id": S.String +}) {} + +export class OrderEnum extends S.Literal("asc", "desc") {} + +export class ListVideosParams extends S.Struct({ + "limit": S.optionalWith(S.Int.pipe(S.greaterThanOrEqualTo(0), S.lessThanOrEqualTo(100)), { nullable: true }), + "order": S.optionalWith(OrderEnum, { nullable: true }), + /** + * Identifier for the last item from the previous pagination request + */ + "after": S.optionalWith(S.String, { nullable: true }) +}) {} + +/** + * The object type, which is always `video`. + */ +export class VideoResourceObject extends S.Literal("video") {} + +export class VideoModel extends S.Literal("sora-2", "sora-2-pro") {} + +export class VideoStatus extends S.Literal("queued", "in_progress", "completed", "failed") {} + +export class VideoSize extends S.Literal("720x1280", "1280x720", "1024x1792", "1792x1024") {} + +export class VideoSeconds extends S.Literal("4", "8", "12") {} + +export class Error2 extends S.Class("Error2")({ + "code": S.String, + "message": S.String +}) {} + +/** + * Structured information describing a generated video job. + */ +export class VideoResource extends S.Class("VideoResource")({ + /** + * Unique identifier for the video job. + */ + "id": S.String, + /** + * The object type, which is always `video`. + */ + "object": VideoResourceObject.pipe(S.propertySignature, S.withConstructorDefault(() => "video" as const)), + /** + * The video generation model that produced the job. + */ + "model": VideoModel, + /** + * Current lifecycle status of the video job. + */ + "status": VideoStatus, + /** + * Approximate completion percentage for the generation task. + */ + "progress": S.Int, + /** + * Unix timestamp (seconds) for when the job was created. + */ + "created_at": S.Int, + "completed_at": S.NullOr(S.Int), + "expires_at": S.NullOr(S.Int), + "prompt": S.NullOr(S.String), + /** + * The resolution of the generated video. + */ + "size": VideoSize, + /** + * Duration of the generated clip in seconds. + */ + "seconds": VideoSeconds, + "remixed_from_video_id": S.NullOr(S.String), + "error": S.NullOr(Error2) +}) {} + +export class VideoListResource extends S.Class("VideoListResource")({ + /** + * The type of object returned, must be `list`. + */ + "object": S.Literal("list").pipe(S.propertySignature, S.withConstructorDefault(() => "list" as const)), + /** + * A list of items + */ + "data": S.Array(VideoResource), + "first_id": S.NullOr(S.String), + "last_id": S.NullOr(S.String), + /** + * Whether there are more items available. + */ + "has_more": S.Boolean +}) {} + +/** + * Parameters for creating a new video generation job. + */ +export class CreateVideoBody extends S.Class("CreateVideoBody")({ + /** + * The video generation model to use. Defaults to `sora-2`. + */ + "model": S.optionalWith(VideoModel, { nullable: true }), + /** + * Text prompt that describes the video to generate. + */ + "prompt": S.String.pipe(S.minLength(1), S.maxLength(32000)), + /** + * Optional image reference that guides generation. + */ + "input_reference": S.optionalWith(S.instanceOf(globalThis.Blob), { nullable: true }), + /** + * Clip duration in seconds. Defaults to 4 seconds. + */ + "seconds": S.optionalWith(VideoSeconds, { nullable: true }), + /** + * Output resolution formatted as width x height. Defaults to 720x1280. + */ + "size": S.optionalWith(VideoSize, { nullable: true }) +}) {} + +/** + * The object type that signals the deletion response. + */ +export class DeletedVideoResourceObject extends S.Literal("video.deleted") {} + +/** + * Confirmation payload returned after deleting a video. + */ +export class DeletedVideoResource extends S.Class("DeletedVideoResource")({ + /** + * The object type that signals the deletion response. + */ + "object": DeletedVideoResourceObject.pipe( + S.propertySignature, + S.withConstructorDefault(() => "video.deleted" as const) + ), + /** + * Indicates that the video resource was deleted. + */ + "deleted": S.Boolean, + /** + * Identifier of the deleted video. + */ + "id": S.String +}) {} + +export class VideoContentVariant extends S.Literal("video", "thumbnail", "spritesheet") {} + +export class RetrieveVideoContentParams extends S.Struct({ + "variant": S.optionalWith(VideoContentVariant, { nullable: true }) +}) {} + +export class RetrieveVideoContent200 extends S.String {} + +/** + * Parameters for remixing an existing generated video. + */ +export class CreateVideoRemixBody extends S.Class("CreateVideoRemixBody")({ + /** + * Updated text prompt that directs the remix generation. + */ + "prompt": S.String.pipe(S.minLength(1), S.maxLength(32000)) +}) {} + +export class TruncationEnum extends S.Literal("auto", "disabled") {} + +export class TokenCountsBody extends S.Class("TokenCountsBody")({ + "model": S.optionalWith(S.String, { nullable: true }), + "input": S.optionalWith( + S.Union( + /** + * A text input to the model, equivalent to a text input with the `user` role. + */ + S.String.pipe(S.maxLength(10485760)), + S.Array(InputItem) + ), + { nullable: true } + ), + "previous_response_id": S.optionalWith(S.String, { nullable: true }), + "tools": S.optionalWith(S.Array(Tool), { nullable: true }), + "text": S.optionalWith(ResponseTextParam, { nullable: true }), + "reasoning": S.optionalWith(Reasoning, { nullable: true }), + /** + * The truncation strategy to use for the model response. - `auto`: If the input to this Response exceeds the model's context window size, the model will truncate the response to fit the context window by dropping items from the beginning of the conversation. - `disabled` (default): If the input size will exceed the context window size for a model, the request will fail with a 400 error. + */ + "truncation": S.optionalWith(TruncationEnum, { nullable: true }), + "instructions": S.optionalWith(S.String, { nullable: true }), + "conversation": S.optionalWith(ConversationParam, { nullable: true }), + "tool_choice": S.optionalWith(ToolChoiceParam, { nullable: true }), + "parallel_tool_calls": S.optionalWith(S.Boolean, { nullable: true }) +}) {} + +export class TokenCountsResourceObject extends S.Literal("response.input_tokens") {} + +export class TokenCountsResource extends S.Class("TokenCountsResource")({ + "object": TokenCountsResourceObject.pipe( + S.propertySignature, + S.withConstructorDefault(() => "response.input_tokens" as const) + ), + "input_tokens": S.Int +}) {} + +/** + * Type discriminator that is always `chatkit.session`. + */ +export class ChatSessionResourceObject extends S.Literal("chatkit.session") {} + +/** + * Controls diagnostic tracing during the session. + */ +export class ChatkitWorkflowTracing extends S.Class("ChatkitWorkflowTracing")({ + /** + * Indicates whether tracing is enabled. + */ + "enabled": S.Boolean +}) {} + +/** + * Workflow metadata and state returned for the session. + */ +export class ChatkitWorkflow extends S.Class("ChatkitWorkflow")({ + /** + * Identifier of the workflow backing the session. + */ + "id": S.String, + "version": S.NullOr(S.String), + "state_variables": S.NullOr(S.Record({ key: S.String, value: S.Unknown })), + /** + * Tracing settings applied to the workflow. + */ + "tracing": ChatkitWorkflowTracing +}) {} + +/** + * Active per-minute request limit for the session. + */ +export class ChatSessionRateLimits extends S.Class("ChatSessionRateLimits")({ + /** + * Maximum allowed requests per one-minute window. + */ + "max_requests_per_1_minute": S.Int +}) {} + +export class ChatSessionStatus extends S.Literal("active", "expired", "cancelled") {} + +/** + * Automatic thread title preferences for the session. + */ +export class ChatSessionAutomaticThreadTitling + extends S.Class("ChatSessionAutomaticThreadTitling")({ + /** + * Whether automatic thread titling is enabled. + */ + "enabled": S.Boolean + }) +{} + +/** + * Upload permissions and limits applied to the session. + */ +export class ChatSessionFileUpload extends S.Class("ChatSessionFileUpload")({ + /** + * Indicates if uploads are enabled for the session. + */ + "enabled": S.Boolean, + "max_file_size": S.NullOr(S.Int), + "max_files": S.NullOr(S.Int) +}) {} + +/** + * History retention preferences returned for the session. + */ +export class ChatSessionHistory extends S.Class("ChatSessionHistory")({ + /** + * Indicates if chat history is persisted for the session. + */ + "enabled": S.Boolean, + "recent_threads": S.NullOr(S.Int) +}) {} + +/** + * ChatKit configuration for the session. + */ +export class ChatSessionChatkitConfiguration + extends S.Class("ChatSessionChatkitConfiguration")({ + /** + * Automatic thread titling preferences. + */ + "automatic_thread_titling": ChatSessionAutomaticThreadTitling, + /** + * Upload settings for the session. + */ + "file_upload": ChatSessionFileUpload, + /** + * History retention configuration. + */ + "history": ChatSessionHistory + }) +{} + +/** + * Represents a ChatKit session and its resolved configuration. + */ +export class ChatSessionResource extends S.Class("ChatSessionResource")({ + /** + * Identifier for the ChatKit session. + */ + "id": S.String, + /** + * Type discriminator that is always `chatkit.session`. + */ + "object": ChatSessionResourceObject.pipe( + S.propertySignature, + S.withConstructorDefault(() => "chatkit.session" as const) + ), + /** + * Unix timestamp (in seconds) for when the session expires. + */ + "expires_at": S.Int, + /** + * Ephemeral client secret that authenticates session requests. + */ + "client_secret": S.String, + /** + * Workflow metadata for the session. + */ + "workflow": ChatkitWorkflow, + /** + * User identifier associated with the session. + */ + "user": S.String, + /** + * Resolved rate limit values. + */ + "rate_limits": ChatSessionRateLimits, + /** + * Convenience copy of the per-minute request limit. + */ + "max_requests_per_1_minute": S.Int, + /** + * Current lifecycle state of the session. + */ + "status": ChatSessionStatus, + /** + * Resolved ChatKit feature configuration for the session. + */ + "chatkit_configuration": ChatSessionChatkitConfiguration +}) {} + +/** + * Controls diagnostic tracing during the session. + */ +export class WorkflowTracingParam extends S.Class("WorkflowTracingParam")({ + /** + * Whether tracing is enabled during the session. Defaults to true. + */ + "enabled": S.optionalWith(S.Boolean, { nullable: true }) +}) {} + +/** + * Workflow reference and overrides applied to the chat session. + */ +export class WorkflowParam extends S.Class("WorkflowParam")({ + /** + * Identifier for the workflow invoked by the session. + */ + "id": S.String, + /** + * Specific workflow version to run. Defaults to the latest deployed version. + */ + "version": S.optionalWith(S.String, { nullable: true }), + /** + * State variables forwarded to the workflow. Keys may be up to 64 characters, values must be primitive types, and the map defaults to an empty object. + */ + "state_variables": S.optionalWith(S.Record({ key: S.String, value: S.Unknown }), { nullable: true }), + /** + * Optional tracing overrides for the workflow invocation. When omitted, tracing is enabled by default. + */ + "tracing": S.optionalWith(WorkflowTracingParam, { nullable: true }) +}) {} + +/** + * Base timestamp used to calculate expiration. Currently fixed to `created_at`. + */ +export class ExpiresAfterParamAnchor extends S.Literal("created_at") {} + +/** + * Controls when the session expires relative to an anchor timestamp. + */ +export class ExpiresAfterParam extends S.Class("ExpiresAfterParam")({ + /** + * Base timestamp used to calculate expiration. Currently fixed to `created_at`. + */ + "anchor": ExpiresAfterParamAnchor.pipe(S.propertySignature, S.withConstructorDefault(() => "created_at" as const)), + /** + * Number of seconds after the anchor when the session expires. + */ + "seconds": S.Int.pipe(S.greaterThanOrEqualTo(1), S.lessThanOrEqualTo(600)) +}) {} + +/** + * Controls request rate limits for the session. + */ +export class RateLimitsParam extends S.Class("RateLimitsParam")({ + /** + * Maximum number of requests allowed per minute for the session. Defaults to 10. + */ + "max_requests_per_1_minute": S.optionalWith(S.Int.pipe(S.greaterThanOrEqualTo(1)), { nullable: true }) +}) {} + +/** + * Controls whether ChatKit automatically generates thread titles. + */ +export class AutomaticThreadTitlingParam extends S.Class("AutomaticThreadTitlingParam")({ + /** + * Enable automatic thread title generation. Defaults to true. + */ + "enabled": S.optionalWith(S.Boolean, { nullable: true }) +}) {} + +/** + * Controls whether users can upload files. + */ +export class FileUploadParam extends S.Class("FileUploadParam")({ + /** + * Enable uploads for this session. Defaults to false. + */ + "enabled": S.optionalWith(S.Boolean, { nullable: true }), + /** + * Maximum size in megabytes for each uploaded file. Defaults to 512 MB, which is the maximum allowable size. + */ + "max_file_size": S.optionalWith(S.Int.pipe(S.greaterThanOrEqualTo(1), S.lessThanOrEqualTo(512)), { nullable: true }), + /** + * Maximum number of files that can be uploaded to the session. Defaults to 10. + */ + "max_files": S.optionalWith(S.Int.pipe(S.greaterThanOrEqualTo(1)), { nullable: true }) +}) {} + +/** + * Controls how much historical context is retained for the session. + */ +export class HistoryParam extends S.Class("HistoryParam")({ + /** + * Enables chat users to access previous ChatKit threads. Defaults to true. + */ + "enabled": S.optionalWith(S.Boolean, { nullable: true }), + /** + * Number of recent ChatKit threads users have access to. Defaults to unlimited when unset. + */ + "recent_threads": S.optionalWith(S.Int.pipe(S.greaterThanOrEqualTo(1)), { nullable: true }) +}) {} + +/** + * Optional per-session configuration settings for ChatKit behavior. + */ +export class ChatkitConfigurationParam extends S.Class("ChatkitConfigurationParam")({ + /** + * Configuration for automatic thread titling. When omitted, automatic thread titling is enabled by default. + */ + "automatic_thread_titling": S.optionalWith(AutomaticThreadTitlingParam, { nullable: true }), + /** + * Configuration for upload enablement and limits. When omitted, uploads are disabled by default (max_files 10, max_file_size 512 MB). + */ + "file_upload": S.optionalWith(FileUploadParam, { nullable: true }), + /** + * Configuration for chat history retention. When omitted, history is enabled by default with no limit on recent_threads (null). + */ + "history": S.optionalWith(HistoryParam, { nullable: true }) +}) {} + +/** + * Parameters for provisioning a new ChatKit session. + */ +export class CreateChatSessionBody extends S.Class("CreateChatSessionBody")({ + /** + * Workflow that powers the session. + */ + "workflow": WorkflowParam, + /** + * A free-form string that identifies your end user; ensures this Session can access other objects that have the same `user` scope. + */ + "user": S.String.pipe(S.minLength(1)), + /** + * Optional override for session expiration timing in seconds from creation. Defaults to 10 minutes. + */ + "expires_after": S.optionalWith(ExpiresAfterParam, { nullable: true }), + /** + * Optional override for per-minute request limits. When omitted, defaults to 10. + */ + "rate_limits": S.optionalWith(RateLimitsParam, { nullable: true }), + /** + * Optional overrides for ChatKit runtime configuration features + */ + "chatkit_configuration": S.optionalWith(ChatkitConfigurationParam, { nullable: true }) +}) {} + +export class ListThreadItemsMethodParams extends S.Struct({ + "limit": S.optionalWith(S.Int.pipe(S.greaterThanOrEqualTo(0), S.lessThanOrEqualTo(100)), { nullable: true }), + "order": S.optionalWith(OrderEnum, { nullable: true }), + /** + * List items created after this thread item ID. Defaults to null for the first page. + */ + "after": S.optionalWith(S.String, { nullable: true }), + /** + * List items created before this thread item ID. Defaults to null for the newest results. + */ + "before": S.optionalWith(S.String, { nullable: true }) +}) {} + +/** + * Type discriminator that is always `chatkit.thread_item`. + */ +export class UserMessageItemObject extends S.Literal("chatkit.thread_item") {} + +export class UserMessageItemType extends S.Literal("chatkit.user_message") {} + +/** + * Type discriminator that is always `input_text`. + */ +export class UserMessageInputTextType extends S.Literal("input_text") {} + +/** + * Text block that a user contributed to the thread. + */ +export class UserMessageInputText extends S.Class("UserMessageInputText")({ + /** + * Type discriminator that is always `input_text`. + */ + "type": UserMessageInputTextType.pipe(S.propertySignature, S.withConstructorDefault(() => "input_text" as const)), + /** + * Plain-text content supplied by the user. + */ + "text": S.String +}) {} + +/** + * Type discriminator that is always `quoted_text`. + */ +export class UserMessageQuotedTextType extends S.Literal("quoted_text") {} + +/** + * Quoted snippet that the user referenced in their message. + */ +export class UserMessageQuotedText extends S.Class("UserMessageQuotedText")({ + /** + * Type discriminator that is always `quoted_text`. + */ + "type": UserMessageQuotedTextType.pipe(S.propertySignature, S.withConstructorDefault(() => "quoted_text" as const)), + /** + * Quoted text content. + */ + "text": S.String +}) {} + +export class AttachmentType extends S.Literal("image", "file") {} + +/** + * Attachment metadata included on thread items. + */ +export class Attachment extends S.Class("Attachment")({ + /** + * Attachment discriminator. + */ + "type": AttachmentType, + /** + * Identifier for the attachment. + */ + "id": S.String, + /** + * Original display name for the attachment. + */ + "name": S.String, + /** + * MIME type of the attachment. + */ + "mime_type": S.String, + "preview_url": S.NullOr(S.String) +}) {} + +/** + * Tool selection that the assistant should honor when executing the item. + */ +export class ToolChoice extends S.Class("ToolChoice")({ + /** + * Identifier of the requested tool. + */ + "id": S.String +}) {} + +/** + * Model and tool overrides applied when generating the assistant response. + */ +export class InferenceOptions extends S.Class("InferenceOptions")({ + "tool_choice": S.NullOr(ToolChoice), + "model": S.NullOr(S.String) +}) {} + +/** + * User-authored messages within a thread. + */ +export class UserMessageItem extends S.Class("UserMessageItem")({ + /** + * Identifier of the thread item. + */ + "id": S.String, + /** + * Type discriminator that is always `chatkit.thread_item`. + */ + "object": UserMessageItemObject.pipe( + S.propertySignature, + S.withConstructorDefault(() => "chatkit.thread_item" as const) + ), + /** + * Unix timestamp (in seconds) for when the item was created. + */ + "created_at": S.Int, + /** + * Identifier of the parent thread. + */ + "thread_id": S.String, + "type": UserMessageItemType.pipe( + S.propertySignature, + S.withConstructorDefault(() => "chatkit.user_message" as const) + ), + /** + * Ordered content elements supplied by the user. + */ + "content": S.Array(S.Union(UserMessageInputText, UserMessageQuotedText)), + /** + * Attachments associated with the user message. Defaults to an empty list. + */ + "attachments": S.Array(Attachment), + "inference_options": S.NullOr(InferenceOptions) +}) {} + +/** + * Type discriminator that is always `chatkit.thread_item`. + */ +export class AssistantMessageItemObject extends S.Literal("chatkit.thread_item") {} + +/** + * Type discriminator that is always `chatkit.assistant_message`. + */ +export class AssistantMessageItemType extends S.Literal("chatkit.assistant_message") {} + +/** + * Type discriminator that is always `output_text`. + */ +export class ResponseOutputTextType extends S.Literal("output_text") {} + +/** + * Type discriminator that is always `file` for this annotation. + */ +export class FileAnnotationType extends S.Literal("file") {} + +/** + * Type discriminator that is always `file`. + */ +export class FileAnnotationSourceType extends S.Literal("file") {} + +/** + * Attachment source referenced by an annotation. + */ +export class FileAnnotationSource extends S.Class("FileAnnotationSource")({ + /** + * Type discriminator that is always `file`. + */ + "type": FileAnnotationSourceType.pipe(S.propertySignature, S.withConstructorDefault(() => "file" as const)), + /** + * Filename referenced by the annotation. + */ + "filename": S.String +}) {} + +/** + * Annotation that references an uploaded file. + */ +export class FileAnnotation extends S.Class("FileAnnotation")({ + /** + * Type discriminator that is always `file` for this annotation. + */ + "type": FileAnnotationType.pipe(S.propertySignature, S.withConstructorDefault(() => "file" as const)), + /** + * File attachment referenced by the annotation. + */ + "source": FileAnnotationSource +}) {} + +/** + * Type discriminator that is always `url` for this annotation. + */ +export class UrlAnnotationType extends S.Literal("url") {} + +/** + * Type discriminator that is always `url`. + */ +export class UrlAnnotationSourceType extends S.Literal("url") {} + +/** + * URL backing an annotation entry. + */ +export class UrlAnnotationSource extends S.Class("UrlAnnotationSource")({ + /** + * Type discriminator that is always `url`. + */ + "type": UrlAnnotationSourceType.pipe(S.propertySignature, S.withConstructorDefault(() => "url" as const)), + /** + * URL referenced by the annotation. + */ + "url": S.String +}) {} + +/** + * Annotation that references a URL. + */ +export class UrlAnnotation extends S.Class("UrlAnnotation")({ + /** + * Type discriminator that is always `url` for this annotation. + */ + "type": UrlAnnotationType.pipe(S.propertySignature, S.withConstructorDefault(() => "url" as const)), + /** + * URL referenced by the annotation. + */ + "source": UrlAnnotationSource +}) {} + +/** + * Assistant response text accompanied by optional annotations. + */ +export class ResponseOutputText extends S.Class("ResponseOutputText")({ + /** + * Type discriminator that is always `output_text`. + */ + "type": ResponseOutputTextType.pipe(S.propertySignature, S.withConstructorDefault(() => "output_text" as const)), + /** + * Assistant generated text. + */ + "text": S.String, + /** + * Ordered list of annotations attached to the response text. + */ + "annotations": S.Array(S.Union(FileAnnotation, UrlAnnotation)) +}) {} + +/** + * Assistant-authored message within a thread. + */ +export class AssistantMessageItem extends S.Class("AssistantMessageItem")({ + /** + * Identifier of the thread item. + */ + "id": S.String, + /** + * Type discriminator that is always `chatkit.thread_item`. + */ + "object": AssistantMessageItemObject.pipe( + S.propertySignature, + S.withConstructorDefault(() => "chatkit.thread_item" as const) + ), + /** + * Unix timestamp (in seconds) for when the item was created. + */ + "created_at": S.Int, + /** + * Identifier of the parent thread. + */ + "thread_id": S.String, + /** + * Type discriminator that is always `chatkit.assistant_message`. + */ + "type": AssistantMessageItemType.pipe( + S.propertySignature, + S.withConstructorDefault(() => "chatkit.assistant_message" as const) + ), + /** + * Ordered assistant response segments. + */ + "content": S.Array(ResponseOutputText) +}) {} + +/** + * Type discriminator that is always `chatkit.thread_item`. + */ +export class WidgetMessageItemObject extends S.Literal("chatkit.thread_item") {} + +/** + * Type discriminator that is always `chatkit.widget`. + */ +export class WidgetMessageItemType extends S.Literal("chatkit.widget") {} + +/** + * Thread item that renders a widget payload. + */ +export class WidgetMessageItem extends S.Class("WidgetMessageItem")({ + /** + * Identifier of the thread item. + */ + "id": S.String, + /** + * Type discriminator that is always `chatkit.thread_item`. + */ + "object": WidgetMessageItemObject.pipe( + S.propertySignature, + S.withConstructorDefault(() => "chatkit.thread_item" as const) + ), + /** + * Unix timestamp (in seconds) for when the item was created. + */ + "created_at": S.Int, + /** + * Identifier of the parent thread. + */ + "thread_id": S.String, + /** + * Type discriminator that is always `chatkit.widget`. + */ + "type": WidgetMessageItemType.pipe(S.propertySignature, S.withConstructorDefault(() => "chatkit.widget" as const)), + /** + * Serialized widget payload rendered in the UI. + */ + "widget": S.String +}) {} + +/** + * Type discriminator that is always `chatkit.thread_item`. + */ +export class ClientToolCallItemObject extends S.Literal("chatkit.thread_item") {} + +/** + * Type discriminator that is always `chatkit.client_tool_call`. + */ +export class ClientToolCallItemType extends S.Literal("chatkit.client_tool_call") {} + +export class ClientToolCallStatus extends S.Literal("in_progress", "completed") {} + +/** + * Record of a client side tool invocation initiated by the assistant. + */ +export class ClientToolCallItem extends S.Class("ClientToolCallItem")({ + /** + * Identifier of the thread item. + */ + "id": S.String, + /** + * Type discriminator that is always `chatkit.thread_item`. + */ + "object": ClientToolCallItemObject.pipe( + S.propertySignature, + S.withConstructorDefault(() => "chatkit.thread_item" as const) + ), + /** + * Unix timestamp (in seconds) for when the item was created. + */ + "created_at": S.Int, + /** + * Identifier of the parent thread. + */ + "thread_id": S.String, + /** + * Type discriminator that is always `chatkit.client_tool_call`. + */ + "type": ClientToolCallItemType.pipe( + S.propertySignature, + S.withConstructorDefault(() => "chatkit.client_tool_call" as const) + ), + /** + * Execution status for the tool call. + */ + "status": ClientToolCallStatus, + /** + * Identifier for the client tool call. + */ + "call_id": S.String, + /** + * Tool name that was invoked. + */ + "name": S.String, + /** + * JSON-encoded arguments that were sent to the tool. + */ + "arguments": S.String, + "output": S.NullOr(S.String) +}) {} + +/** + * Type discriminator that is always `chatkit.thread_item`. + */ +export class TaskItemObject extends S.Literal("chatkit.thread_item") {} + +/** + * Type discriminator that is always `chatkit.task`. + */ +export class TaskItemType extends S.Literal("chatkit.task") {} + +export class TaskType extends S.Literal("custom", "thought") {} + +/** + * Task emitted by the workflow to show progress and status updates. + */ +export class TaskItem extends S.Class("TaskItem")({ + /** + * Identifier of the thread item. + */ + "id": S.String, + /** + * Type discriminator that is always `chatkit.thread_item`. + */ + "object": TaskItemObject.pipe(S.propertySignature, S.withConstructorDefault(() => "chatkit.thread_item" as const)), + /** + * Unix timestamp (in seconds) for when the item was created. + */ + "created_at": S.Int, + /** + * Identifier of the parent thread. + */ + "thread_id": S.String, + /** + * Type discriminator that is always `chatkit.task`. + */ + "type": TaskItemType.pipe(S.propertySignature, S.withConstructorDefault(() => "chatkit.task" as const)), + /** + * Subtype for the task. + */ + "task_type": TaskType, + "heading": S.NullOr(S.String), + "summary": S.NullOr(S.String) +}) {} + +/** + * Type discriminator that is always `chatkit.thread_item`. + */ +export class TaskGroupItemObject extends S.Literal("chatkit.thread_item") {} + +/** + * Type discriminator that is always `chatkit.task_group`. + */ +export class TaskGroupItemType extends S.Literal("chatkit.task_group") {} + +/** + * Task entry that appears within a TaskGroup. + */ +export class TaskGroupTask extends S.Class("TaskGroupTask")({ + /** + * Subtype for the grouped task. + */ + "type": TaskType, + "heading": S.NullOr(S.String), + "summary": S.NullOr(S.String) +}) {} + +/** + * Collection of workflow tasks grouped together in the thread. + */ +export class TaskGroupItem extends S.Class("TaskGroupItem")({ + /** + * Identifier of the thread item. + */ + "id": S.String, + /** + * Type discriminator that is always `chatkit.thread_item`. + */ + "object": TaskGroupItemObject.pipe( + S.propertySignature, + S.withConstructorDefault(() => "chatkit.thread_item" as const) + ), + /** + * Unix timestamp (in seconds) for when the item was created. + */ + "created_at": S.Int, + /** + * Identifier of the parent thread. + */ + "thread_id": S.String, + /** + * Type discriminator that is always `chatkit.task_group`. + */ + "type": TaskGroupItemType.pipe(S.propertySignature, S.withConstructorDefault(() => "chatkit.task_group" as const)), + /** + * Tasks included in the group. + */ + "tasks": S.Array(TaskGroupTask) +}) {} + +export class ThreadItem + extends S.Union(UserMessageItem, AssistantMessageItem, WidgetMessageItem, ClientToolCallItem, TaskItem, TaskGroupItem) +{} + +/** + * A paginated list of thread items rendered for the ChatKit API. + */ +export class ThreadItemListResource extends S.Class("ThreadItemListResource")({ + /** + * The type of object returned, must be `list`. + */ + "object": S.Literal("list").pipe(S.propertySignature, S.withConstructorDefault(() => "list" as const)), + /** + * A list of items + */ + "data": S.Array(ThreadItem), + "first_id": S.NullOr(S.String), + "last_id": S.NullOr(S.String), + /** + * Whether there are more items available. + */ + "has_more": S.Boolean +}) {} + +/** + * Type discriminator that is always `chatkit.thread`. + */ +export class ThreadResourceObject extends S.Literal("chatkit.thread") {} + +/** + * Status discriminator that is always `active`. + */ +export class ActiveStatusType extends S.Literal("active") {} + +/** + * Indicates that a thread is active. + */ +export class ActiveStatus extends S.Class("ActiveStatus")({ + /** + * Status discriminator that is always `active`. + */ + "type": ActiveStatusType.pipe(S.propertySignature, S.withConstructorDefault(() => "active" as const)) +}) {} + +/** + * Status discriminator that is always `locked`. + */ +export class LockedStatusType extends S.Literal("locked") {} + +/** + * Indicates that a thread is locked and cannot accept new input. + */ +export class LockedStatus extends S.Class("LockedStatus")({ + /** + * Status discriminator that is always `locked`. + */ + "type": LockedStatusType.pipe(S.propertySignature, S.withConstructorDefault(() => "locked" as const)), + "reason": S.NullOr(S.String) +}) {} + +/** + * Status discriminator that is always `closed`. + */ +export class ClosedStatusType extends S.Literal("closed") {} + +/** + * Indicates that a thread has been closed. + */ +export class ClosedStatus extends S.Class("ClosedStatus")({ + /** + * Status discriminator that is always `closed`. + */ + "type": ClosedStatusType.pipe(S.propertySignature, S.withConstructorDefault(() => "closed" as const)), + "reason": S.NullOr(S.String) +}) {} + +/** + * Represents a ChatKit thread and its current status. + */ +export class ThreadResource extends S.Class("ThreadResource")({ + /** + * Identifier of the thread. + */ + "id": S.String, + /** + * Type discriminator that is always `chatkit.thread`. + */ + "object": ThreadResourceObject.pipe(S.propertySignature, S.withConstructorDefault(() => "chatkit.thread" as const)), + /** + * Unix timestamp (in seconds) for when the thread was created. + */ + "created_at": S.Int, + "title": S.NullOr(S.String), + /** + * Current status for the thread. Defaults to `active` for newly created threads. + */ + "status": S.Union(ActiveStatus, LockedStatus, ClosedStatus), + /** + * Free-form string that identifies your end user who owns the thread. + */ + "user": S.String +}) {} + +/** + * Type discriminator that is always `chatkit.thread.deleted`. + */ +export class DeletedThreadResourceObject extends S.Literal("chatkit.thread.deleted") {} + +/** + * Confirmation payload returned after deleting a thread. + */ +export class DeletedThreadResource extends S.Class("DeletedThreadResource")({ + /** + * Identifier of the deleted thread. + */ + "id": S.String, + /** + * Type discriminator that is always `chatkit.thread.deleted`. + */ + "object": DeletedThreadResourceObject.pipe( + S.propertySignature, + S.withConstructorDefault(() => "chatkit.thread.deleted" as const) + ), + /** + * Indicates that the thread has been deleted. + */ + "deleted": S.Boolean +}) {} + +export class ListThreadsMethodParams extends S.Struct({ + "limit": S.optionalWith(S.Int.pipe(S.greaterThanOrEqualTo(0), S.lessThanOrEqualTo(100)), { nullable: true }), + "order": S.optionalWith(OrderEnum, { nullable: true }), + /** + * List items created after this thread item ID. Defaults to null for the first page. + */ + "after": S.optionalWith(S.String, { nullable: true }), + /** + * List items created before this thread item ID. Defaults to null for the newest results. + */ + "before": S.optionalWith(S.String, { nullable: true }), + /** + * Filter threads that belong to this user identifier. Defaults to null to return all users. + */ + "user": S.optionalWith(S.String.pipe(S.minLength(1), S.maxLength(512)), { nullable: true }) +}) {} + +/** + * A paginated list of ChatKit threads. + */ +export class ThreadListResource extends S.Class("ThreadListResource")({ + /** + * The type of object returned, must be `list`. + */ + "object": S.Literal("list").pipe(S.propertySignature, S.withConstructorDefault(() => "list" as const)), + /** + * A list of items + */ + "data": S.Array(ThreadResource), + "first_id": S.NullOr(S.String), + "last_id": S.NullOr(S.String), + /** + * Whether there are more items available. + */ + "has_more": S.Boolean +}) {} + +export const make = ( + httpClient: HttpClient.HttpClient, + options: { + readonly transformClient?: ((client: HttpClient.HttpClient) => Effect.Effect) | undefined + } = {} +): Client => { + const unexpectedStatus = (response: HttpClientResponse.HttpClientResponse) => + Effect.flatMap( + Effect.orElseSucceed(response.json, () => "Unexpected status code"), + (description) => + Effect.fail( + new HttpClientError.ResponseError({ + request: response.request, + response, + reason: "StatusCode", + description: typeof description === "string" ? description : JSON.stringify(description) + }) + ) + ) + const withResponse: ( + f: (response: HttpClientResponse.HttpClientResponse) => Effect.Effect + ) => ( + request: HttpClientRequest.HttpClientRequest + ) => Effect.Effect = options.transformClient + ? (f) => (request) => + Effect.flatMap( + Effect.flatMap(options.transformClient!(httpClient), (client) => client.execute(request)), + f + ) + : (f) => (request) => Effect.flatMap(httpClient.execute(request), f) + const decodeSuccess = (schema: S.Schema) => (response: HttpClientResponse.HttpClientResponse) => + HttpClientResponse.schemaBodyJson(schema)(response) + const decodeError = + (tag: Tag, schema: S.Schema) => + (response: HttpClientResponse.HttpClientResponse) => + Effect.flatMap( + HttpClientResponse.schemaBodyJson(schema)(response), + (cause) => Effect.fail(ClientError(tag, cause, response)) + ) + return { + httpClient, + "listAssistants": (options) => + HttpClientRequest.get(`/assistants`).pipe( + HttpClientRequest.setUrlParams({ + "limit": options?.["limit"] as any, + "order": options?.["order"] as any, + "after": options?.["after"] as any, + "before": options?.["before"] as any + }), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ListAssistantsResponse), + orElse: unexpectedStatus + })) + ), + "createAssistant": (options) => + HttpClientRequest.post(`/assistants`).pipe( + HttpClientRequest.bodyUnsafeJson(options), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(AssistantObject), + orElse: unexpectedStatus + })) + ), + "getAssistant": (assistantId) => + HttpClientRequest.get(`/assistants/${assistantId}`).pipe( + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(AssistantObject), + orElse: unexpectedStatus + })) + ), + "modifyAssistant": (assistantId, options) => + HttpClientRequest.post(`/assistants/${assistantId}`).pipe( + HttpClientRequest.bodyUnsafeJson(options), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(AssistantObject), + orElse: unexpectedStatus + })) + ), + "deleteAssistant": (assistantId) => + HttpClientRequest.del(`/assistants/${assistantId}`).pipe( + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(DeleteAssistantResponse), + orElse: unexpectedStatus + })) + ), + "createSpeech": (options) => + HttpClientRequest.post(`/audio/speech`).pipe( + HttpClientRequest.bodyUnsafeJson(options), + withResponse(HttpClientResponse.matchStatus({ + orElse: unexpectedStatus + })) + ), + "createTranscription": (options) => + HttpClientRequest.post(`/audio/transcriptions`).pipe( + HttpClientRequest.bodyFormDataRecord(options as any), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(CreateTranscription200), + orElse: unexpectedStatus + })) + ), + "createTranslation": (options) => + HttpClientRequest.post(`/audio/translations`).pipe( + HttpClientRequest.bodyFormDataRecord(options as any), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(CreateTranslation200), + orElse: unexpectedStatus + })) + ), + "listBatches": (options) => + HttpClientRequest.get(`/batches`).pipe( + HttpClientRequest.setUrlParams({ "after": options?.["after"] as any, "limit": options?.["limit"] as any }), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ListBatchesResponse), + orElse: unexpectedStatus + })) + ), + "createBatch": (options) => + HttpClientRequest.post(`/batches`).pipe( + HttpClientRequest.bodyUnsafeJson(options), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(Batch), + orElse: unexpectedStatus + })) + ), + "retrieveBatch": (batchId) => + HttpClientRequest.get(`/batches/${batchId}`).pipe( + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(Batch), + orElse: unexpectedStatus + })) + ), + "cancelBatch": (batchId) => + HttpClientRequest.post(`/batches/${batchId}/cancel`).pipe( + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(Batch), + orElse: unexpectedStatus + })) + ), + "listChatCompletions": (options) => + HttpClientRequest.get(`/chat/completions`).pipe( + HttpClientRequest.setUrlParams({ + "model": options?.["model"] as any, + "metadata": options?.["metadata"] as any, + "after": options?.["after"] as any, + "limit": options?.["limit"] as any, + "order": options?.["order"] as any + }), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ChatCompletionList), + orElse: unexpectedStatus + })) + ), + "createChatCompletion": (options) => + HttpClientRequest.post(`/chat/completions`).pipe( + HttpClientRequest.bodyUnsafeJson(options), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(CreateChatCompletionResponse), + orElse: unexpectedStatus + })) + ), + "getChatCompletion": (completionId) => + HttpClientRequest.get(`/chat/completions/${completionId}`).pipe( + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(CreateChatCompletionResponse), + orElse: unexpectedStatus + })) + ), + "updateChatCompletion": (completionId, options) => + HttpClientRequest.post(`/chat/completions/${completionId}`).pipe( + HttpClientRequest.bodyUnsafeJson(options), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(CreateChatCompletionResponse), + orElse: unexpectedStatus + })) + ), + "deleteChatCompletion": (completionId) => + HttpClientRequest.del(`/chat/completions/${completionId}`).pipe( + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ChatCompletionDeleted), + orElse: unexpectedStatus + })) + ), + "getChatCompletionMessages": (completionId, options) => + HttpClientRequest.get(`/chat/completions/${completionId}/messages`).pipe( + HttpClientRequest.setUrlParams({ + "after": options?.["after"] as any, + "limit": options?.["limit"] as any, + "order": options?.["order"] as any + }), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ChatCompletionMessageList), + orElse: unexpectedStatus + })) + ), + "createCompletion": (options) => + HttpClientRequest.post(`/completions`).pipe( + HttpClientRequest.bodyUnsafeJson(options), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(CreateCompletionResponse), + orElse: unexpectedStatus + })) + ), + "ListContainers": (options) => + HttpClientRequest.get(`/containers`).pipe( + HttpClientRequest.setUrlParams({ + "limit": options?.["limit"] as any, + "order": options?.["order"] as any, + "after": options?.["after"] as any + }), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ContainerListResource), + orElse: unexpectedStatus + })) + ), + "CreateContainer": (options) => + HttpClientRequest.post(`/containers`).pipe( + HttpClientRequest.bodyUnsafeJson(options), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ContainerResource), + orElse: unexpectedStatus + })) + ), + "RetrieveContainer": (containerId) => + HttpClientRequest.get(`/containers/${containerId}`).pipe( + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ContainerResource), + orElse: unexpectedStatus + })) + ), + "DeleteContainer": (containerId) => + HttpClientRequest.del(`/containers/${containerId}`).pipe( + withResponse(HttpClientResponse.matchStatus({ + "200": () => Effect.void, + orElse: unexpectedStatus + })) + ), + "ListContainerFiles": (containerId, options) => + HttpClientRequest.get(`/containers/${containerId}/files`).pipe( + HttpClientRequest.setUrlParams({ + "limit": options?.["limit"] as any, + "order": options?.["order"] as any, + "after": options?.["after"] as any + }), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ContainerFileListResource), + orElse: unexpectedStatus + })) + ), + "CreateContainerFile": (containerId, options) => + HttpClientRequest.post(`/containers/${containerId}/files`).pipe( + HttpClientRequest.bodyFormDataRecord(options as any), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ContainerFileResource), + orElse: unexpectedStatus + })) + ), + "RetrieveContainerFile": (containerId, fileId) => + HttpClientRequest.get(`/containers/${containerId}/files/${fileId}`).pipe( + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ContainerFileResource), + orElse: unexpectedStatus + })) + ), + "DeleteContainerFile": (containerId, fileId) => + HttpClientRequest.del(`/containers/${containerId}/files/${fileId}`).pipe( + withResponse(HttpClientResponse.matchStatus({ + "200": () => Effect.void, + orElse: unexpectedStatus + })) + ), + "RetrieveContainerFileContent": (containerId, fileId) => + HttpClientRequest.get(`/containers/${containerId}/files/${fileId}/content`).pipe( + withResponse(HttpClientResponse.matchStatus({ + "200": () => Effect.void, + orElse: unexpectedStatus + })) + ), + "listConversationItems": (conversationId, options) => + HttpClientRequest.get(`/conversations/${conversationId}/items`).pipe( + HttpClientRequest.setUrlParams({ + "limit": options?.["limit"] as any, + "order": options?.["order"] as any, + "after": options?.["after"] as any, + "include": options?.["include"] as any + }), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ConversationItemList), + orElse: unexpectedStatus + })) + ), + "createConversationItems": (conversationId, options) => + HttpClientRequest.post(`/conversations/${conversationId}/items`).pipe( + HttpClientRequest.setUrlParams({ "include": options.params?.["include"] as any }), + HttpClientRequest.bodyUnsafeJson(options.payload), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ConversationItemList), + orElse: unexpectedStatus + })) + ), + "getConversationItem": (conversationId, itemId, options) => + HttpClientRequest.get(`/conversations/${conversationId}/items/${itemId}`).pipe( + HttpClientRequest.setUrlParams({ "include": options?.["include"] as any }), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ConversationItem), + orElse: unexpectedStatus + })) + ), + "deleteConversationItem": (conversationId, itemId) => + HttpClientRequest.del(`/conversations/${conversationId}/items/${itemId}`).pipe( + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ConversationResource), + orElse: unexpectedStatus + })) + ), + "createEmbedding": (options) => + HttpClientRequest.post(`/embeddings`).pipe( + HttpClientRequest.bodyUnsafeJson(options), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(CreateEmbeddingResponse), + orElse: unexpectedStatus + })) + ), + "listEvals": (options) => + HttpClientRequest.get(`/evals`).pipe( + HttpClientRequest.setUrlParams({ + "after": options?.["after"] as any, + "limit": options?.["limit"] as any, + "order": options?.["order"] as any, + "order_by": options?.["order_by"] as any + }), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(EvalList), + orElse: unexpectedStatus + })) + ), + "createEval": (options) => + HttpClientRequest.post(`/evals`).pipe( + HttpClientRequest.bodyUnsafeJson(options), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(Eval), + orElse: unexpectedStatus + })) + ), + "getEval": (evalId) => + HttpClientRequest.get(`/evals/${evalId}`).pipe( + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(Eval), + orElse: unexpectedStatus + })) + ), + "updateEval": (evalId, options) => + HttpClientRequest.post(`/evals/${evalId}`).pipe( + HttpClientRequest.bodyUnsafeJson(options), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(Eval), + orElse: unexpectedStatus + })) + ), + "deleteEval": (evalId) => + HttpClientRequest.del(`/evals/${evalId}`).pipe( + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(DeleteEval200), + "404": decodeError("Error", Error), + orElse: unexpectedStatus + })) + ), + "getEvalRuns": (evalId, options) => + HttpClientRequest.get(`/evals/${evalId}/runs`).pipe( + HttpClientRequest.setUrlParams({ + "after": options?.["after"] as any, + "limit": options?.["limit"] as any, + "order": options?.["order"] as any, + "status": options?.["status"] as any + }), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(EvalRunList), + orElse: unexpectedStatus + })) + ), + "createEvalRun": (evalId, options) => + HttpClientRequest.post(`/evals/${evalId}/runs`).pipe( + HttpClientRequest.bodyUnsafeJson(options), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(EvalRun), + "400": decodeError("Error", Error), + orElse: unexpectedStatus + })) + ), + "getEvalRun": (evalId, runId) => + HttpClientRequest.get(`/evals/${evalId}/runs/${runId}`).pipe( + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(EvalRun), + orElse: unexpectedStatus + })) + ), + "cancelEvalRun": (evalId, runId) => + HttpClientRequest.post(`/evals/${evalId}/runs/${runId}`).pipe( + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(EvalRun), + orElse: unexpectedStatus + })) + ), + "deleteEvalRun": (evalId, runId) => + HttpClientRequest.del(`/evals/${evalId}/runs/${runId}`).pipe( + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(DeleteEvalRun200), + "404": decodeError("Error", Error), + orElse: unexpectedStatus + })) + ), + "getEvalRunOutputItems": (evalId, runId, options) => + HttpClientRequest.get(`/evals/${evalId}/runs/${runId}/output_items`).pipe( + HttpClientRequest.setUrlParams({ + "after": options?.["after"] as any, + "limit": options?.["limit"] as any, + "status": options?.["status"] as any, + "order": options?.["order"] as any + }), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(EvalRunOutputItemList), + orElse: unexpectedStatus + })) + ), + "getEvalRunOutputItem": (evalId, runId, outputItemId) => + HttpClientRequest.get(`/evals/${evalId}/runs/${runId}/output_items/${outputItemId}`).pipe( + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(EvalRunOutputItem), + orElse: unexpectedStatus + })) + ), + "listFiles": (options) => + HttpClientRequest.get(`/files`).pipe( + HttpClientRequest.setUrlParams({ + "purpose": options?.["purpose"] as any, + "limit": options?.["limit"] as any, + "order": options?.["order"] as any, + "after": options?.["after"] as any + }), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ListFilesResponse), + orElse: unexpectedStatus + })) + ), + "createFile": (options) => + HttpClientRequest.post(`/files`).pipe( + HttpClientRequest.bodyFormDataRecord(options as any), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(OpenAIFile), + orElse: unexpectedStatus + })) + ), + "retrieveFile": (fileId) => + HttpClientRequest.get(`/files/${fileId}`).pipe( + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(OpenAIFile), + orElse: unexpectedStatus + })) + ), + "deleteFile": (fileId) => + HttpClientRequest.del(`/files/${fileId}`).pipe( + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(DeleteFileResponse), + orElse: unexpectedStatus + })) + ), + "downloadFile": (fileId) => + HttpClientRequest.get(`/files/${fileId}/content`).pipe( + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(DownloadFile200), + orElse: unexpectedStatus + })) + ), + "runGrader": (options) => + HttpClientRequest.post(`/fine_tuning/alpha/graders/run`).pipe( + HttpClientRequest.bodyUnsafeJson(options), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(RunGraderResponse), + orElse: unexpectedStatus + })) + ), + "validateGrader": (options) => + HttpClientRequest.post(`/fine_tuning/alpha/graders/validate`).pipe( + HttpClientRequest.bodyUnsafeJson(options), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ValidateGraderResponse), + orElse: unexpectedStatus + })) + ), + "listFineTuningCheckpointPermissions": (fineTunedModelCheckpoint, options) => + HttpClientRequest.get(`/fine_tuning/checkpoints/${fineTunedModelCheckpoint}/permissions`).pipe( + HttpClientRequest.setUrlParams({ + "project_id": options?.["project_id"] as any, + "after": options?.["after"] as any, + "limit": options?.["limit"] as any, + "order": options?.["order"] as any + }), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ListFineTuningCheckpointPermissionResponse), + orElse: unexpectedStatus + })) + ), + "createFineTuningCheckpointPermission": (fineTunedModelCheckpoint, options) => + HttpClientRequest.post(`/fine_tuning/checkpoints/${fineTunedModelCheckpoint}/permissions`).pipe( + HttpClientRequest.bodyUnsafeJson(options), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ListFineTuningCheckpointPermissionResponse), + orElse: unexpectedStatus + })) + ), + "deleteFineTuningCheckpointPermission": (fineTunedModelCheckpoint, permissionId) => + HttpClientRequest.del(`/fine_tuning/checkpoints/${fineTunedModelCheckpoint}/permissions/${permissionId}`).pipe( + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(DeleteFineTuningCheckpointPermissionResponse), + orElse: unexpectedStatus + })) + ), + "listPaginatedFineTuningJobs": (options) => + HttpClientRequest.get(`/fine_tuning/jobs`).pipe( + HttpClientRequest.setUrlParams({ + "after": options?.["after"] as any, + "limit": options?.["limit"] as any, + "metadata": options?.["metadata"] as any + }), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ListPaginatedFineTuningJobsResponse), + orElse: unexpectedStatus + })) + ), + "createFineTuningJob": (options) => + HttpClientRequest.post(`/fine_tuning/jobs`).pipe( + HttpClientRequest.bodyUnsafeJson(options), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(FineTuningJob), + orElse: unexpectedStatus + })) + ), + "retrieveFineTuningJob": (fineTuningJobId) => + HttpClientRequest.get(`/fine_tuning/jobs/${fineTuningJobId}`).pipe( + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(FineTuningJob), + orElse: unexpectedStatus + })) + ), + "cancelFineTuningJob": (fineTuningJobId) => + HttpClientRequest.post(`/fine_tuning/jobs/${fineTuningJobId}/cancel`).pipe( + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(FineTuningJob), + orElse: unexpectedStatus + })) + ), + "listFineTuningJobCheckpoints": (fineTuningJobId, options) => + HttpClientRequest.get(`/fine_tuning/jobs/${fineTuningJobId}/checkpoints`).pipe( + HttpClientRequest.setUrlParams({ "after": options?.["after"] as any, "limit": options?.["limit"] as any }), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ListFineTuningJobCheckpointsResponse), + orElse: unexpectedStatus + })) + ), + "listFineTuningEvents": (fineTuningJobId, options) => + HttpClientRequest.get(`/fine_tuning/jobs/${fineTuningJobId}/events`).pipe( + HttpClientRequest.setUrlParams({ "after": options?.["after"] as any, "limit": options?.["limit"] as any }), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ListFineTuningJobEventsResponse), + orElse: unexpectedStatus + })) + ), + "pauseFineTuningJob": (fineTuningJobId) => + HttpClientRequest.post(`/fine_tuning/jobs/${fineTuningJobId}/pause`).pipe( + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(FineTuningJob), + orElse: unexpectedStatus + })) + ), + "resumeFineTuningJob": (fineTuningJobId) => + HttpClientRequest.post(`/fine_tuning/jobs/${fineTuningJobId}/resume`).pipe( + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(FineTuningJob), + orElse: unexpectedStatus + })) + ), + "createImageEdit": (options) => + HttpClientRequest.post(`/images/edits`).pipe( + HttpClientRequest.bodyFormDataRecord(options as any), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ImagesResponse), + orElse: unexpectedStatus + })) + ), + "createImage": (options) => + HttpClientRequest.post(`/images/generations`).pipe( + HttpClientRequest.bodyUnsafeJson(options), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ImagesResponse), + orElse: unexpectedStatus + })) + ), + "createImageVariation": (options) => + HttpClientRequest.post(`/images/variations`).pipe( + HttpClientRequest.bodyFormDataRecord(options as any), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ImagesResponse), + orElse: unexpectedStatus + })) + ), + "listModels": () => + HttpClientRequest.get(`/models`).pipe( + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ListModelsResponse), + orElse: unexpectedStatus + })) + ), + "retrieveModel": (model) => + HttpClientRequest.get(`/models/${model}`).pipe( + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(Model), + orElse: unexpectedStatus + })) + ), + "deleteModel": (model) => + HttpClientRequest.del(`/models/${model}`).pipe( + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(DeleteModelResponse), + orElse: unexpectedStatus + })) + ), + "createModeration": (options) => + HttpClientRequest.post(`/moderations`).pipe( + HttpClientRequest.bodyUnsafeJson(options), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(CreateModerationResponse), + orElse: unexpectedStatus + })) + ), + "adminApiKeysList": (options) => + HttpClientRequest.get(`/organization/admin_api_keys`).pipe( + HttpClientRequest.setUrlParams({ + "after": options?.["after"] as any, + "order": options?.["order"] as any, + "limit": options?.["limit"] as any + }), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ApiKeyList), + orElse: unexpectedStatus + })) + ), + "adminApiKeysCreate": (options) => + HttpClientRequest.post(`/organization/admin_api_keys`).pipe( + HttpClientRequest.bodyUnsafeJson(options), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(AdminApiKey), + orElse: unexpectedStatus + })) + ), + "adminApiKeysGet": (keyId) => + HttpClientRequest.get(`/organization/admin_api_keys/${keyId}`).pipe( + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(AdminApiKey), + orElse: unexpectedStatus + })) + ), + "adminApiKeysDelete": (keyId) => + HttpClientRequest.del(`/organization/admin_api_keys/${keyId}`).pipe( + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(AdminApiKeysDelete200), + orElse: unexpectedStatus + })) + ), + "listAuditLogs": (options) => + HttpClientRequest.get(`/organization/audit_logs`).pipe( + HttpClientRequest.setUrlParams({ + "effective_at[gt]": options?.["effective_at[gt]"] as any, + "effective_at[gte]": options?.["effective_at[gte]"] as any, + "effective_at[lt]": options?.["effective_at[lt]"] as any, + "effective_at[lte]": options?.["effective_at[lte]"] as any, + "project_ids[]": options?.["project_ids[]"] as any, + "event_types[]": options?.["event_types[]"] as any, + "actor_ids[]": options?.["actor_ids[]"] as any, + "actor_emails[]": options?.["actor_emails[]"] as any, + "resource_ids[]": options?.["resource_ids[]"] as any, + "limit": options?.["limit"] as any, + "after": options?.["after"] as any, + "before": options?.["before"] as any + }), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ListAuditLogsResponse), + orElse: unexpectedStatus + })) + ), + "listOrganizationCertificates": (options) => + HttpClientRequest.get(`/organization/certificates`).pipe( + HttpClientRequest.setUrlParams({ + "limit": options?.["limit"] as any, + "after": options?.["after"] as any, + "order": options?.["order"] as any + }), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ListCertificatesResponse), + orElse: unexpectedStatus + })) + ), + "uploadCertificate": (options) => + HttpClientRequest.post(`/organization/certificates`).pipe( + HttpClientRequest.bodyUnsafeJson(options), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(Certificate), + orElse: unexpectedStatus + })) + ), + "activateOrganizationCertificates": (options) => + HttpClientRequest.post(`/organization/certificates/activate`).pipe( + HttpClientRequest.bodyUnsafeJson(options), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ListCertificatesResponse), + orElse: unexpectedStatus + })) + ), + "deactivateOrganizationCertificates": (options) => + HttpClientRequest.post(`/organization/certificates/deactivate`).pipe( + HttpClientRequest.bodyUnsafeJson(options), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ListCertificatesResponse), + orElse: unexpectedStatus + })) + ), + "getCertificate": (certificateId, options) => + HttpClientRequest.get(`/organization/certificates/${certificateId}`).pipe( + HttpClientRequest.setUrlParams({ "include": options?.["include"] as any }), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(Certificate), + orElse: unexpectedStatus + })) + ), + "modifyCertificate": (certificateId, options) => + HttpClientRequest.post(`/organization/certificates/${certificateId}`).pipe( + HttpClientRequest.bodyUnsafeJson(options), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(Certificate), + orElse: unexpectedStatus + })) + ), + "deleteCertificate": (certificateId) => + HttpClientRequest.del(`/organization/certificates/${certificateId}`).pipe( + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(DeleteCertificateResponse), + orElse: unexpectedStatus + })) + ), + "usageCosts": (options) => + HttpClientRequest.get(`/organization/costs`).pipe( + HttpClientRequest.setUrlParams({ + "start_time": options?.["start_time"] as any, + "end_time": options?.["end_time"] as any, + "bucket_width": options?.["bucket_width"] as any, + "project_ids": options?.["project_ids"] as any, + "group_by": options?.["group_by"] as any, + "limit": options?.["limit"] as any, + "page": options?.["page"] as any + }), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(UsageResponse), + orElse: unexpectedStatus + })) + ), + "listGroups": (options) => + HttpClientRequest.get(`/organization/groups`).pipe( + HttpClientRequest.setUrlParams({ + "limit": options?.["limit"] as any, + "after": options?.["after"] as any, + "order": options?.["order"] as any + }), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(GroupListResource), + orElse: unexpectedStatus + })) + ), + "createGroup": (options) => + HttpClientRequest.post(`/organization/groups`).pipe( + HttpClientRequest.bodyUnsafeJson(options), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(GroupResponse), + orElse: unexpectedStatus + })) + ), + "updateGroup": (groupId, options) => + HttpClientRequest.post(`/organization/groups/${groupId}`).pipe( + HttpClientRequest.bodyUnsafeJson(options), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(GroupResourceWithSuccess), + orElse: unexpectedStatus + })) + ), + "deleteGroup": (groupId) => + HttpClientRequest.del(`/organization/groups/${groupId}`).pipe( + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(GroupDeletedResource), + orElse: unexpectedStatus + })) + ), + "listGroupRoleAssignments": (groupId, options) => + HttpClientRequest.get(`/organization/groups/${groupId}/roles`).pipe( + HttpClientRequest.setUrlParams({ + "limit": options?.["limit"] as any, + "after": options?.["after"] as any, + "order": options?.["order"] as any + }), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(RoleListResource), + orElse: unexpectedStatus + })) + ), + "assignGroupRole": (groupId, options) => + HttpClientRequest.post(`/organization/groups/${groupId}/roles`).pipe( + HttpClientRequest.bodyUnsafeJson(options), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(GroupRoleAssignment), + orElse: unexpectedStatus + })) + ), + "unassignGroupRole": (groupId, roleId) => + HttpClientRequest.del(`/organization/groups/${groupId}/roles/${roleId}`).pipe( + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(DeletedRoleAssignmentResource), + orElse: unexpectedStatus + })) + ), + "listGroupUsers": (groupId, options) => + HttpClientRequest.get(`/organization/groups/${groupId}/users`).pipe( + HttpClientRequest.setUrlParams({ + "limit": options?.["limit"] as any, + "after": options?.["after"] as any, + "order": options?.["order"] as any + }), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(UserListResource), + orElse: unexpectedStatus + })) + ), + "addGroupUser": (groupId, options) => + HttpClientRequest.post(`/organization/groups/${groupId}/users`).pipe( + HttpClientRequest.bodyUnsafeJson(options), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(GroupUserAssignment), + orElse: unexpectedStatus + })) + ), + "removeGroupUser": (groupId, userId) => + HttpClientRequest.del(`/organization/groups/${groupId}/users/${userId}`).pipe( + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(GroupUserDeletedResource), + orElse: unexpectedStatus + })) + ), + "listInvites": (options) => + HttpClientRequest.get(`/organization/invites`).pipe( + HttpClientRequest.setUrlParams({ "limit": options?.["limit"] as any, "after": options?.["after"] as any }), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(InviteListResponse), + orElse: unexpectedStatus + })) + ), + "inviteUser": (options) => + HttpClientRequest.post(`/organization/invites`).pipe( + HttpClientRequest.bodyUnsafeJson(options), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(Invite), + orElse: unexpectedStatus + })) + ), + "retrieveInvite": (inviteId) => + HttpClientRequest.get(`/organization/invites/${inviteId}`).pipe( + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(Invite), + orElse: unexpectedStatus + })) + ), + "deleteInvite": (inviteId) => + HttpClientRequest.del(`/organization/invites/${inviteId}`).pipe( + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(InviteDeleteResponse), + orElse: unexpectedStatus + })) + ), + "listProjects": (options) => + HttpClientRequest.get(`/organization/projects`).pipe( + HttpClientRequest.setUrlParams({ + "limit": options?.["limit"] as any, + "after": options?.["after"] as any, + "include_archived": options?.["include_archived"] as any + }), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ProjectListResponse), + orElse: unexpectedStatus + })) + ), + "createProject": (options) => + HttpClientRequest.post(`/organization/projects`).pipe( + HttpClientRequest.bodyUnsafeJson(options), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(Project), + orElse: unexpectedStatus + })) + ), + "retrieveProject": (projectId) => + HttpClientRequest.get(`/organization/projects/${projectId}`).pipe( + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(Project), + orElse: unexpectedStatus + })) + ), + "modifyProject": (projectId, options) => + HttpClientRequest.post(`/organization/projects/${projectId}`).pipe( + HttpClientRequest.bodyUnsafeJson(options), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(Project), + "400": decodeError("ErrorResponse", ErrorResponse), + orElse: unexpectedStatus + })) + ), + "listProjectApiKeys": (projectId, options) => + HttpClientRequest.get(`/organization/projects/${projectId}/api_keys`).pipe( + HttpClientRequest.setUrlParams({ "limit": options?.["limit"] as any, "after": options?.["after"] as any }), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ProjectApiKeyListResponse), + orElse: unexpectedStatus + })) + ), + "retrieveProjectApiKey": (projectId, keyId) => + HttpClientRequest.get(`/organization/projects/${projectId}/api_keys/${keyId}`).pipe( + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ProjectApiKey), + orElse: unexpectedStatus + })) + ), + "deleteProjectApiKey": (projectId, keyId) => + HttpClientRequest.del(`/organization/projects/${projectId}/api_keys/${keyId}`).pipe( + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ProjectApiKeyDeleteResponse), + "400": decodeError("ErrorResponse", ErrorResponse), + orElse: unexpectedStatus + })) + ), + "archiveProject": (projectId) => + HttpClientRequest.post(`/organization/projects/${projectId}/archive`).pipe( + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(Project), + orElse: unexpectedStatus + })) + ), + "listProjectCertificates": (projectId, options) => + HttpClientRequest.get(`/organization/projects/${projectId}/certificates`).pipe( + HttpClientRequest.setUrlParams({ + "limit": options?.["limit"] as any, + "after": options?.["after"] as any, + "order": options?.["order"] as any + }), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ListCertificatesResponse), + orElse: unexpectedStatus + })) + ), + "activateProjectCertificates": (projectId, options) => + HttpClientRequest.post(`/organization/projects/${projectId}/certificates/activate`).pipe( + HttpClientRequest.bodyUnsafeJson(options), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ListCertificatesResponse), + orElse: unexpectedStatus + })) + ), + "deactivateProjectCertificates": (projectId, options) => + HttpClientRequest.post(`/organization/projects/${projectId}/certificates/deactivate`).pipe( + HttpClientRequest.bodyUnsafeJson(options), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ListCertificatesResponse), + orElse: unexpectedStatus + })) + ), + "listProjectGroups": (projectId, options) => + HttpClientRequest.get(`/organization/projects/${projectId}/groups`).pipe( + HttpClientRequest.setUrlParams({ + "limit": options?.["limit"] as any, + "after": options?.["after"] as any, + "order": options?.["order"] as any + }), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ProjectGroupListResource), + orElse: unexpectedStatus + })) + ), + "addProjectGroup": (projectId, options) => + HttpClientRequest.post(`/organization/projects/${projectId}/groups`).pipe( + HttpClientRequest.bodyUnsafeJson(options), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ProjectGroup), + orElse: unexpectedStatus + })) + ), + "removeProjectGroup": (projectId, groupId) => + HttpClientRequest.del(`/organization/projects/${projectId}/groups/${groupId}`).pipe( + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ProjectGroupDeletedResource), + orElse: unexpectedStatus + })) + ), + "listProjectRateLimits": (projectId, options) => + HttpClientRequest.get(`/organization/projects/${projectId}/rate_limits`).pipe( + HttpClientRequest.setUrlParams({ + "limit": options?.["limit"] as any, + "after": options?.["after"] as any, + "before": options?.["before"] as any + }), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ProjectRateLimitListResponse), + orElse: unexpectedStatus + })) + ), + "updateProjectRateLimits": (projectId, rateLimitId, options) => + HttpClientRequest.post(`/organization/projects/${projectId}/rate_limits/${rateLimitId}`).pipe( + HttpClientRequest.bodyUnsafeJson(options), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ProjectRateLimit), + "400": decodeError("ErrorResponse", ErrorResponse), + orElse: unexpectedStatus + })) + ), + "listProjectServiceAccounts": (projectId, options) => + HttpClientRequest.get(`/organization/projects/${projectId}/service_accounts`).pipe( + HttpClientRequest.setUrlParams({ "limit": options?.["limit"] as any, "after": options?.["after"] as any }), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ProjectServiceAccountListResponse), + "400": decodeError("ErrorResponse", ErrorResponse), + orElse: unexpectedStatus + })) + ), + "createProjectServiceAccount": (projectId, options) => + HttpClientRequest.post(`/organization/projects/${projectId}/service_accounts`).pipe( + HttpClientRequest.bodyUnsafeJson(options), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ProjectServiceAccountCreateResponse), + "400": decodeError("ErrorResponse", ErrorResponse), + orElse: unexpectedStatus + })) + ), + "retrieveProjectServiceAccount": (projectId, serviceAccountId) => + HttpClientRequest.get(`/organization/projects/${projectId}/service_accounts/${serviceAccountId}`).pipe( + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ProjectServiceAccount), + orElse: unexpectedStatus + })) + ), + "deleteProjectServiceAccount": (projectId, serviceAccountId) => + HttpClientRequest.del(`/organization/projects/${projectId}/service_accounts/${serviceAccountId}`).pipe( + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ProjectServiceAccountDeleteResponse), + orElse: unexpectedStatus + })) + ), + "listProjectUsers": (projectId, options) => + HttpClientRequest.get(`/organization/projects/${projectId}/users`).pipe( + HttpClientRequest.setUrlParams({ "limit": options?.["limit"] as any, "after": options?.["after"] as any }), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ProjectUserListResponse), + "400": decodeError("ErrorResponse", ErrorResponse), + orElse: unexpectedStatus + })) + ), + "createProjectUser": (projectId, options) => + HttpClientRequest.post(`/organization/projects/${projectId}/users`).pipe( + HttpClientRequest.bodyUnsafeJson(options), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ProjectUser), + "400": decodeError("ErrorResponse", ErrorResponse), + orElse: unexpectedStatus + })) + ), + "retrieveProjectUser": (projectId, userId) => + HttpClientRequest.get(`/organization/projects/${projectId}/users/${userId}`).pipe( + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ProjectUser), + orElse: unexpectedStatus + })) + ), + "modifyProjectUser": (projectId, userId, options) => + HttpClientRequest.post(`/organization/projects/${projectId}/users/${userId}`).pipe( + HttpClientRequest.bodyUnsafeJson(options), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ProjectUser), + "400": decodeError("ErrorResponse", ErrorResponse), + orElse: unexpectedStatus + })) + ), + "deleteProjectUser": (projectId, userId) => + HttpClientRequest.del(`/organization/projects/${projectId}/users/${userId}`).pipe( + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ProjectUserDeleteResponse), + "400": decodeError("ErrorResponse", ErrorResponse), + orElse: unexpectedStatus + })) + ), + "listRoles": (options) => + HttpClientRequest.get(`/organization/roles`).pipe( + HttpClientRequest.setUrlParams({ + "limit": options?.["limit"] as any, + "after": options?.["after"] as any, + "order": options?.["order"] as any + }), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(PublicRoleListResource), + orElse: unexpectedStatus + })) + ), + "createRole": (options) => + HttpClientRequest.post(`/organization/roles`).pipe( + HttpClientRequest.bodyUnsafeJson(options), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(Role), + orElse: unexpectedStatus + })) + ), + "updateRole": (roleId, options) => + HttpClientRequest.post(`/organization/roles/${roleId}`).pipe( + HttpClientRequest.bodyUnsafeJson(options), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(Role), + orElse: unexpectedStatus + })) + ), + "deleteRole": (roleId) => + HttpClientRequest.del(`/organization/roles/${roleId}`).pipe( + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(RoleDeletedResource), + orElse: unexpectedStatus + })) + ), + "usageAudioSpeeches": (options) => + HttpClientRequest.get(`/organization/usage/audio_speeches`).pipe( + HttpClientRequest.setUrlParams({ + "start_time": options?.["start_time"] as any, + "end_time": options?.["end_time"] as any, + "bucket_width": options?.["bucket_width"] as any, + "project_ids": options?.["project_ids"] as any, + "user_ids": options?.["user_ids"] as any, + "api_key_ids": options?.["api_key_ids"] as any, + "models": options?.["models"] as any, + "group_by": options?.["group_by"] as any, + "limit": options?.["limit"] as any, + "page": options?.["page"] as any + }), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(UsageResponse), + orElse: unexpectedStatus + })) + ), + "usageAudioTranscriptions": (options) => + HttpClientRequest.get(`/organization/usage/audio_transcriptions`).pipe( + HttpClientRequest.setUrlParams({ + "start_time": options?.["start_time"] as any, + "end_time": options?.["end_time"] as any, + "bucket_width": options?.["bucket_width"] as any, + "project_ids": options?.["project_ids"] as any, + "user_ids": options?.["user_ids"] as any, + "api_key_ids": options?.["api_key_ids"] as any, + "models": options?.["models"] as any, + "group_by": options?.["group_by"] as any, + "limit": options?.["limit"] as any, + "page": options?.["page"] as any + }), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(UsageResponse), + orElse: unexpectedStatus + })) + ), + "usageCodeInterpreterSessions": (options) => + HttpClientRequest.get(`/organization/usage/code_interpreter_sessions`).pipe( + HttpClientRequest.setUrlParams({ + "start_time": options?.["start_time"] as any, + "end_time": options?.["end_time"] as any, + "bucket_width": options?.["bucket_width"] as any, + "project_ids": options?.["project_ids"] as any, + "group_by": options?.["group_by"] as any, + "limit": options?.["limit"] as any, + "page": options?.["page"] as any + }), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(UsageResponse), + orElse: unexpectedStatus + })) + ), + "usageCompletions": (options) => + HttpClientRequest.get(`/organization/usage/completions`).pipe( + HttpClientRequest.setUrlParams({ + "start_time": options?.["start_time"] as any, + "end_time": options?.["end_time"] as any, + "bucket_width": options?.["bucket_width"] as any, + "project_ids": options?.["project_ids"] as any, + "user_ids": options?.["user_ids"] as any, + "api_key_ids": options?.["api_key_ids"] as any, + "models": options?.["models"] as any, + "batch": options?.["batch"] as any, + "group_by": options?.["group_by"] as any, + "limit": options?.["limit"] as any, + "page": options?.["page"] as any + }), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(UsageResponse), + orElse: unexpectedStatus + })) + ), + "usageEmbeddings": (options) => + HttpClientRequest.get(`/organization/usage/embeddings`).pipe( + HttpClientRequest.setUrlParams({ + "start_time": options?.["start_time"] as any, + "end_time": options?.["end_time"] as any, + "bucket_width": options?.["bucket_width"] as any, + "project_ids": options?.["project_ids"] as any, + "user_ids": options?.["user_ids"] as any, + "api_key_ids": options?.["api_key_ids"] as any, + "models": options?.["models"] as any, + "group_by": options?.["group_by"] as any, + "limit": options?.["limit"] as any, + "page": options?.["page"] as any + }), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(UsageResponse), + orElse: unexpectedStatus + })) + ), + "usageImages": (options) => + HttpClientRequest.get(`/organization/usage/images`).pipe( + HttpClientRequest.setUrlParams({ + "start_time": options?.["start_time"] as any, + "end_time": options?.["end_time"] as any, + "bucket_width": options?.["bucket_width"] as any, + "sources": options?.["sources"] as any, + "sizes": options?.["sizes"] as any, + "project_ids": options?.["project_ids"] as any, + "user_ids": options?.["user_ids"] as any, + "api_key_ids": options?.["api_key_ids"] as any, + "models": options?.["models"] as any, + "group_by": options?.["group_by"] as any, + "limit": options?.["limit"] as any, + "page": options?.["page"] as any + }), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(UsageResponse), + orElse: unexpectedStatus + })) + ), + "usageModerations": (options) => + HttpClientRequest.get(`/organization/usage/moderations`).pipe( + HttpClientRequest.setUrlParams({ + "start_time": options?.["start_time"] as any, + "end_time": options?.["end_time"] as any, + "bucket_width": options?.["bucket_width"] as any, + "project_ids": options?.["project_ids"] as any, + "user_ids": options?.["user_ids"] as any, + "api_key_ids": options?.["api_key_ids"] as any, + "models": options?.["models"] as any, + "group_by": options?.["group_by"] as any, + "limit": options?.["limit"] as any, + "page": options?.["page"] as any + }), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(UsageResponse), + orElse: unexpectedStatus + })) + ), + "usageVectorStores": (options) => + HttpClientRequest.get(`/organization/usage/vector_stores`).pipe( + HttpClientRequest.setUrlParams({ + "start_time": options?.["start_time"] as any, + "end_time": options?.["end_time"] as any, + "bucket_width": options?.["bucket_width"] as any, + "project_ids": options?.["project_ids"] as any, + "group_by": options?.["group_by"] as any, + "limit": options?.["limit"] as any, + "page": options?.["page"] as any + }), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(UsageResponse), + orElse: unexpectedStatus + })) + ), + "listUsers": (options) => + HttpClientRequest.get(`/organization/users`).pipe( + HttpClientRequest.setUrlParams({ + "limit": options?.["limit"] as any, + "after": options?.["after"] as any, + "emails": options?.["emails"] as any + }), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(UserListResponse), + orElse: unexpectedStatus + })) + ), + "retrieveUser": (userId) => + HttpClientRequest.get(`/organization/users/${userId}`).pipe( + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(User), + orElse: unexpectedStatus + })) + ), + "modifyUser": (userId, options) => + HttpClientRequest.post(`/organization/users/${userId}`).pipe( + HttpClientRequest.bodyUnsafeJson(options), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(User), + orElse: unexpectedStatus + })) + ), + "deleteUser": (userId) => + HttpClientRequest.del(`/organization/users/${userId}`).pipe( + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(UserDeleteResponse), + orElse: unexpectedStatus + })) + ), + "listUserRoleAssignments": (userId, options) => + HttpClientRequest.get(`/organization/users/${userId}/roles`).pipe( + HttpClientRequest.setUrlParams({ + "limit": options?.["limit"] as any, + "after": options?.["after"] as any, + "order": options?.["order"] as any + }), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(RoleListResource), + orElse: unexpectedStatus + })) + ), + "assignUserRole": (userId, options) => + HttpClientRequest.post(`/organization/users/${userId}/roles`).pipe( + HttpClientRequest.bodyUnsafeJson(options), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(UserRoleAssignment), + orElse: unexpectedStatus + })) + ), + "unassignUserRole": (userId, roleId) => + HttpClientRequest.del(`/organization/users/${userId}/roles/${roleId}`).pipe( + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(DeletedRoleAssignmentResource), + orElse: unexpectedStatus + })) + ), + "listProjectGroupRoleAssignments": (projectId, groupId, options) => + HttpClientRequest.get(`/projects/${projectId}/groups/${groupId}/roles`).pipe( + HttpClientRequest.setUrlParams({ + "limit": options?.["limit"] as any, + "after": options?.["after"] as any, + "order": options?.["order"] as any + }), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(RoleListResource), + orElse: unexpectedStatus + })) + ), + "assignProjectGroupRole": (projectId, groupId, options) => + HttpClientRequest.post(`/projects/${projectId}/groups/${groupId}/roles`).pipe( + HttpClientRequest.bodyUnsafeJson(options), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(GroupRoleAssignment), + orElse: unexpectedStatus + })) + ), + "unassignProjectGroupRole": (projectId, groupId, roleId) => + HttpClientRequest.del(`/projects/${projectId}/groups/${groupId}/roles/${roleId}`).pipe( + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(DeletedRoleAssignmentResource), + orElse: unexpectedStatus + })) + ), + "listProjectRoles": (projectId, options) => + HttpClientRequest.get(`/projects/${projectId}/roles`).pipe( + HttpClientRequest.setUrlParams({ + "limit": options?.["limit"] as any, + "after": options?.["after"] as any, + "order": options?.["order"] as any + }), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(PublicRoleListResource), + orElse: unexpectedStatus + })) + ), + "createProjectRole": (projectId, options) => + HttpClientRequest.post(`/projects/${projectId}/roles`).pipe( + HttpClientRequest.bodyUnsafeJson(options), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(Role), + orElse: unexpectedStatus + })) + ), + "updateProjectRole": (projectId, roleId, options) => + HttpClientRequest.post(`/projects/${projectId}/roles/${roleId}`).pipe( + HttpClientRequest.bodyUnsafeJson(options), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(Role), + orElse: unexpectedStatus + })) + ), + "deleteProjectRole": (projectId, roleId) => + HttpClientRequest.del(`/projects/${projectId}/roles/${roleId}`).pipe( + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(RoleDeletedResource), + orElse: unexpectedStatus + })) + ), + "listProjectUserRoleAssignments": (projectId, userId, options) => + HttpClientRequest.get(`/projects/${projectId}/users/${userId}/roles`).pipe( + HttpClientRequest.setUrlParams({ + "limit": options?.["limit"] as any, + "after": options?.["after"] as any, + "order": options?.["order"] as any + }), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(RoleListResource), + orElse: unexpectedStatus + })) + ), + "assignProjectUserRole": (projectId, userId, options) => + HttpClientRequest.post(`/projects/${projectId}/users/${userId}/roles`).pipe( + HttpClientRequest.bodyUnsafeJson(options), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(UserRoleAssignment), + orElse: unexpectedStatus + })) + ), + "unassignProjectUserRole": (projectId, userId, roleId) => + HttpClientRequest.del(`/projects/${projectId}/users/${userId}/roles/${roleId}`).pipe( + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(DeletedRoleAssignmentResource), + orElse: unexpectedStatus + })) + ), + "createRealtimeCall": (options) => + HttpClientRequest.post(`/realtime/calls`).pipe( + HttpClientRequest.bodyFormDataRecord(options as any), + withResponse(HttpClientResponse.matchStatus({ + orElse: unexpectedStatus + })) + ), + "acceptRealtimeCall": (callId, options) => + HttpClientRequest.post(`/realtime/calls/${callId}/accept`).pipe( + HttpClientRequest.bodyUnsafeJson(options), + withResponse(HttpClientResponse.matchStatus({ + "200": () => Effect.void, + orElse: unexpectedStatus + })) + ), + "hangupRealtimeCall": (callId) => + HttpClientRequest.post(`/realtime/calls/${callId}/hangup`).pipe( + withResponse(HttpClientResponse.matchStatus({ + "200": () => Effect.void, + orElse: unexpectedStatus + })) + ), + "referRealtimeCall": (callId, options) => + HttpClientRequest.post(`/realtime/calls/${callId}/refer`).pipe( + HttpClientRequest.bodyUnsafeJson(options), + withResponse(HttpClientResponse.matchStatus({ + "200": () => Effect.void, + orElse: unexpectedStatus + })) + ), + "rejectRealtimeCall": (callId, options) => + HttpClientRequest.post(`/realtime/calls/${callId}/reject`).pipe( + HttpClientRequest.bodyUnsafeJson(options), + withResponse(HttpClientResponse.matchStatus({ + "200": () => Effect.void, + orElse: unexpectedStatus + })) + ), + "createRealtimeClientSecret": (options) => + HttpClientRequest.post(`/realtime/client_secrets`).pipe( + HttpClientRequest.bodyUnsafeJson(options), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(RealtimeCreateClientSecretResponse), + orElse: unexpectedStatus + })) + ), + "createRealtimeSession": (options) => + HttpClientRequest.post(`/realtime/sessions`).pipe( + HttpClientRequest.bodyUnsafeJson(options), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(RealtimeSessionCreateResponse), + orElse: unexpectedStatus + })) + ), + "createRealtimeTranscriptionSession": (options) => + HttpClientRequest.post(`/realtime/transcription_sessions`).pipe( + HttpClientRequest.bodyUnsafeJson(options), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(RealtimeTranscriptionSessionCreateResponse), + orElse: unexpectedStatus + })) + ), + "createResponse": (options) => + HttpClientRequest.post(`/responses`).pipe( + HttpClientRequest.bodyUnsafeJson(options), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(Response), + orElse: unexpectedStatus + })) + ), + "getResponse": (responseId, options) => + HttpClientRequest.get(`/responses/${responseId}`).pipe( + HttpClientRequest.setUrlParams({ + "include": options?.["include"] as any, + "stream": options?.["stream"] as any, + "starting_after": options?.["starting_after"] as any, + "include_obfuscation": options?.["include_obfuscation"] as any + }), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(Response), + orElse: unexpectedStatus + })) + ), + "deleteResponse": (responseId) => + HttpClientRequest.del(`/responses/${responseId}`).pipe( + withResponse(HttpClientResponse.matchStatus({ + "404": decodeError("Error", Error), + "200": () => Effect.void, + orElse: unexpectedStatus + })) + ), + "cancelResponse": (responseId) => + HttpClientRequest.post(`/responses/${responseId}/cancel`).pipe( + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(Response), + "404": decodeError("Error", Error), + orElse: unexpectedStatus + })) + ), + "listInputItems": (responseId, options) => + HttpClientRequest.get(`/responses/${responseId}/input_items`).pipe( + HttpClientRequest.setUrlParams({ + "limit": options?.["limit"] as any, + "order": options?.["order"] as any, + "after": options?.["after"] as any, + "include": options?.["include"] as any + }), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ResponseItemList), + orElse: unexpectedStatus + })) + ), + "createThread": (options) => + HttpClientRequest.post(`/threads`).pipe( + HttpClientRequest.bodyUnsafeJson(options), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ThreadObject), + orElse: unexpectedStatus + })) + ), + "createThreadAndRun": (options) => + HttpClientRequest.post(`/threads/runs`).pipe( + HttpClientRequest.bodyUnsafeJson(options), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(RunObject), + orElse: unexpectedStatus + })) + ), + "getThread": (threadId) => + HttpClientRequest.get(`/threads/${threadId}`).pipe( + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ThreadObject), + orElse: unexpectedStatus + })) + ), + "modifyThread": (threadId, options) => + HttpClientRequest.post(`/threads/${threadId}`).pipe( + HttpClientRequest.bodyUnsafeJson(options), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ThreadObject), + orElse: unexpectedStatus + })) + ), + "deleteThread": (threadId) => + HttpClientRequest.del(`/threads/${threadId}`).pipe( + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(DeleteThreadResponse), + orElse: unexpectedStatus + })) + ), + "listMessages": (threadId, options) => + HttpClientRequest.get(`/threads/${threadId}/messages`).pipe( + HttpClientRequest.setUrlParams({ + "limit": options?.["limit"] as any, + "order": options?.["order"] as any, + "after": options?.["after"] as any, + "before": options?.["before"] as any, + "run_id": options?.["run_id"] as any + }), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ListMessagesResponse), + orElse: unexpectedStatus + })) + ), + "createMessage": (threadId, options) => + HttpClientRequest.post(`/threads/${threadId}/messages`).pipe( + HttpClientRequest.bodyUnsafeJson(options), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(MessageObject), + orElse: unexpectedStatus + })) + ), + "getMessage": (threadId, messageId) => + HttpClientRequest.get(`/threads/${threadId}/messages/${messageId}`).pipe( + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(MessageObject), + orElse: unexpectedStatus + })) + ), + "modifyMessage": (threadId, messageId, options) => + HttpClientRequest.post(`/threads/${threadId}/messages/${messageId}`).pipe( + HttpClientRequest.bodyUnsafeJson(options), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(MessageObject), + orElse: unexpectedStatus + })) + ), + "deleteMessage": (threadId, messageId) => + HttpClientRequest.del(`/threads/${threadId}/messages/${messageId}`).pipe( + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(DeleteMessageResponse), + orElse: unexpectedStatus + })) + ), + "listRuns": (threadId, options) => + HttpClientRequest.get(`/threads/${threadId}/runs`).pipe( + HttpClientRequest.setUrlParams({ + "limit": options?.["limit"] as any, + "order": options?.["order"] as any, + "after": options?.["after"] as any, + "before": options?.["before"] as any + }), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ListRunsResponse), + orElse: unexpectedStatus + })) + ), + "createRun": (threadId, options) => + HttpClientRequest.post(`/threads/${threadId}/runs`).pipe( + HttpClientRequest.setUrlParams({ "include[]": options.params?.["include[]"] as any }), + HttpClientRequest.bodyUnsafeJson(options.payload), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(RunObject), + orElse: unexpectedStatus + })) + ), + "getRun": (threadId, runId) => + HttpClientRequest.get(`/threads/${threadId}/runs/${runId}`).pipe( + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(RunObject), + orElse: unexpectedStatus + })) + ), + "modifyRun": (threadId, runId, options) => + HttpClientRequest.post(`/threads/${threadId}/runs/${runId}`).pipe( + HttpClientRequest.bodyUnsafeJson(options), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(RunObject), + orElse: unexpectedStatus + })) + ), + "cancelRun": (threadId, runId) => + HttpClientRequest.post(`/threads/${threadId}/runs/${runId}/cancel`).pipe( + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(RunObject), + orElse: unexpectedStatus + })) + ), + "listRunSteps": (threadId, runId, options) => + HttpClientRequest.get(`/threads/${threadId}/runs/${runId}/steps`).pipe( + HttpClientRequest.setUrlParams({ + "limit": options?.["limit"] as any, + "order": options?.["order"] as any, + "after": options?.["after"] as any, + "before": options?.["before"] as any, + "include[]": options?.["include[]"] as any + }), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ListRunStepsResponse), + orElse: unexpectedStatus + })) + ), + "getRunStep": (threadId, runId, stepId, options) => + HttpClientRequest.get(`/threads/${threadId}/runs/${runId}/steps/${stepId}`).pipe( + HttpClientRequest.setUrlParams({ "include[]": options?.["include[]"] as any }), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(RunStepObject), + orElse: unexpectedStatus + })) + ), + "submitToolOuputsToRun": (threadId, runId, options) => + HttpClientRequest.post(`/threads/${threadId}/runs/${runId}/submit_tool_outputs`).pipe( + HttpClientRequest.bodyUnsafeJson(options), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(RunObject), + orElse: unexpectedStatus + })) + ), + "createUpload": (options) => + HttpClientRequest.post(`/uploads`).pipe( + HttpClientRequest.bodyUnsafeJson(options), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(Upload), + orElse: unexpectedStatus + })) + ), + "cancelUpload": (uploadId) => + HttpClientRequest.post(`/uploads/${uploadId}/cancel`).pipe( + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(Upload), + orElse: unexpectedStatus + })) + ), + "completeUpload": (uploadId, options) => + HttpClientRequest.post(`/uploads/${uploadId}/complete`).pipe( + HttpClientRequest.bodyUnsafeJson(options), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(Upload), + orElse: unexpectedStatus + })) + ), + "addUploadPart": (uploadId, options) => + HttpClientRequest.post(`/uploads/${uploadId}/parts`).pipe( + HttpClientRequest.bodyFormDataRecord(options as any), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(UploadPart), + orElse: unexpectedStatus + })) + ), + "listVectorStores": (options) => + HttpClientRequest.get(`/vector_stores`).pipe( + HttpClientRequest.setUrlParams({ + "limit": options?.["limit"] as any, + "order": options?.["order"] as any, + "after": options?.["after"] as any, + "before": options?.["before"] as any + }), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ListVectorStoresResponse), + orElse: unexpectedStatus + })) + ), + "createVectorStore": (options) => + HttpClientRequest.post(`/vector_stores`).pipe( + HttpClientRequest.bodyUnsafeJson(options), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(VectorStoreObject), + orElse: unexpectedStatus + })) + ), + "getVectorStore": (vectorStoreId) => + HttpClientRequest.get(`/vector_stores/${vectorStoreId}`).pipe( + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(VectorStoreObject), + orElse: unexpectedStatus + })) + ), + "modifyVectorStore": (vectorStoreId, options) => + HttpClientRequest.post(`/vector_stores/${vectorStoreId}`).pipe( + HttpClientRequest.bodyUnsafeJson(options), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(VectorStoreObject), + orElse: unexpectedStatus + })) + ), + "deleteVectorStore": (vectorStoreId) => + HttpClientRequest.del(`/vector_stores/${vectorStoreId}`).pipe( + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(DeleteVectorStoreResponse), + orElse: unexpectedStatus + })) + ), + "createVectorStoreFileBatch": (vectorStoreId, options) => + HttpClientRequest.post(`/vector_stores/${vectorStoreId}/file_batches`).pipe( + HttpClientRequest.bodyUnsafeJson(options), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(VectorStoreFileBatchObject), + orElse: unexpectedStatus + })) + ), + "getVectorStoreFileBatch": (vectorStoreId, batchId) => + HttpClientRequest.get(`/vector_stores/${vectorStoreId}/file_batches/${batchId}`).pipe( + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(VectorStoreFileBatchObject), + orElse: unexpectedStatus + })) + ), + "cancelVectorStoreFileBatch": (vectorStoreId, batchId) => + HttpClientRequest.post(`/vector_stores/${vectorStoreId}/file_batches/${batchId}/cancel`).pipe( + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(VectorStoreFileBatchObject), + orElse: unexpectedStatus + })) + ), + "listFilesInVectorStoreBatch": (vectorStoreId, batchId, options) => + HttpClientRequest.get(`/vector_stores/${vectorStoreId}/file_batches/${batchId}/files`).pipe( + HttpClientRequest.setUrlParams({ + "limit": options?.["limit"] as any, + "order": options?.["order"] as any, + "after": options?.["after"] as any, + "before": options?.["before"] as any, + "filter": options?.["filter"] as any + }), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ListVectorStoreFilesResponse), + orElse: unexpectedStatus + })) + ), + "listVectorStoreFiles": (vectorStoreId, options) => + HttpClientRequest.get(`/vector_stores/${vectorStoreId}/files`).pipe( + HttpClientRequest.setUrlParams({ + "limit": options?.["limit"] as any, + "order": options?.["order"] as any, + "after": options?.["after"] as any, + "before": options?.["before"] as any, + "filter": options?.["filter"] as any + }), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ListVectorStoreFilesResponse), + orElse: unexpectedStatus + })) + ), + "createVectorStoreFile": (vectorStoreId, options) => + HttpClientRequest.post(`/vector_stores/${vectorStoreId}/files`).pipe( + HttpClientRequest.bodyUnsafeJson(options), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(VectorStoreFileObject), + orElse: unexpectedStatus + })) + ), + "getVectorStoreFile": (vectorStoreId, fileId) => + HttpClientRequest.get(`/vector_stores/${vectorStoreId}/files/${fileId}`).pipe( + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(VectorStoreFileObject), + orElse: unexpectedStatus + })) + ), + "updateVectorStoreFileAttributes": (vectorStoreId, fileId, options) => + HttpClientRequest.post(`/vector_stores/${vectorStoreId}/files/${fileId}`).pipe( + HttpClientRequest.bodyUnsafeJson(options), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(VectorStoreFileObject), + orElse: unexpectedStatus + })) + ), + "deleteVectorStoreFile": (vectorStoreId, fileId) => + HttpClientRequest.del(`/vector_stores/${vectorStoreId}/files/${fileId}`).pipe( + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(DeleteVectorStoreFileResponse), + orElse: unexpectedStatus + })) + ), + "retrieveVectorStoreFileContent": (vectorStoreId, fileId) => + HttpClientRequest.get(`/vector_stores/${vectorStoreId}/files/${fileId}/content`).pipe( + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(VectorStoreFileContentResponse), + orElse: unexpectedStatus + })) + ), + "searchVectorStore": (vectorStoreId, options) => + HttpClientRequest.post(`/vector_stores/${vectorStoreId}/search`).pipe( + HttpClientRequest.bodyUnsafeJson(options), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(VectorStoreSearchResultsPage), + orElse: unexpectedStatus + })) + ), + "createConversation": (options) => + HttpClientRequest.post(`/conversations`).pipe( + HttpClientRequest.bodyUnsafeJson(options), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ConversationResource), + orElse: unexpectedStatus + })) + ), + "getConversation": (conversationId) => + HttpClientRequest.get(`/conversations/${conversationId}`).pipe( + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ConversationResource), + orElse: unexpectedStatus + })) + ), + "updateConversation": (conversationId, options) => + HttpClientRequest.post(`/conversations/${conversationId}`).pipe( + HttpClientRequest.bodyUnsafeJson(options), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ConversationResource), + orElse: unexpectedStatus + })) + ), + "deleteConversation": (conversationId) => + HttpClientRequest.del(`/conversations/${conversationId}`).pipe( + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(DeletedConversationResource), + orElse: unexpectedStatus + })) + ), + "ListVideos": (options) => + HttpClientRequest.get(`/videos`).pipe( + HttpClientRequest.setUrlParams({ + "limit": options?.["limit"] as any, + "order": options?.["order"] as any, + "after": options?.["after"] as any + }), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(VideoListResource), + orElse: unexpectedStatus + })) + ), + "createVideo": (options) => + HttpClientRequest.post(`/videos`).pipe( + HttpClientRequest.bodyUnsafeJson(options), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(VideoResource), + orElse: unexpectedStatus + })) + ), + "GetVideo": (videoId) => + HttpClientRequest.get(`/videos/${videoId}`).pipe( + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(VideoResource), + orElse: unexpectedStatus + })) + ), + "DeleteVideo": (videoId) => + HttpClientRequest.del(`/videos/${videoId}`).pipe( + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(DeletedVideoResource), + orElse: unexpectedStatus + })) + ), + "RetrieveVideoContent": (videoId, options) => + HttpClientRequest.get(`/videos/${videoId}/content`).pipe( + HttpClientRequest.setUrlParams({ "variant": options?.["variant"] as any }), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(RetrieveVideoContent200), + orElse: unexpectedStatus + })) + ), + "CreateVideoRemix": (videoId, options) => + HttpClientRequest.post(`/videos/${videoId}/remix`).pipe( + HttpClientRequest.bodyUnsafeJson(options), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(VideoResource), + orElse: unexpectedStatus + })) + ), + "Getinputtokencounts": (options) => + HttpClientRequest.post(`/responses/input_tokens`).pipe( + HttpClientRequest.bodyUnsafeJson(options), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(TokenCountsResource), + orElse: unexpectedStatus + })) + ), + "CancelChatSessionMethod": (sessionId) => + HttpClientRequest.post(`/chatkit/sessions/${sessionId}/cancel`).pipe( + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ChatSessionResource), + orElse: unexpectedStatus + })) + ), + "CreateChatSessionMethod": (options) => + HttpClientRequest.post(`/chatkit/sessions`).pipe( + HttpClientRequest.bodyUnsafeJson(options), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ChatSessionResource), + orElse: unexpectedStatus + })) + ), + "ListThreadItemsMethod": (threadId, options) => + HttpClientRequest.get(`/chatkit/threads/${threadId}/items`).pipe( + HttpClientRequest.setUrlParams({ + "limit": options?.["limit"] as any, + "order": options?.["order"] as any, + "after": options?.["after"] as any, + "before": options?.["before"] as any + }), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ThreadItemListResource), + orElse: unexpectedStatus + })) + ), + "GetThreadMethod": (threadId) => + HttpClientRequest.get(`/chatkit/threads/${threadId}`).pipe( + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ThreadResource), + orElse: unexpectedStatus + })) + ), + "DeleteThreadMethod": (threadId) => + HttpClientRequest.del(`/chatkit/threads/${threadId}`).pipe( + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(DeletedThreadResource), + orElse: unexpectedStatus + })) + ), + "ListThreadsMethod": (options) => + HttpClientRequest.get(`/chatkit/threads`).pipe( + HttpClientRequest.setUrlParams({ + "limit": options?.["limit"] as any, + "order": options?.["order"] as any, + "after": options?.["after"] as any, + "before": options?.["before"] as any, + "user": options?.["user"] as any + }), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ThreadListResource), + orElse: unexpectedStatus + })) + ) + } +} + +export interface Client { + readonly httpClient: HttpClient.HttpClient + /** + * Returns a list of assistants. + */ + readonly "listAssistants": ( + options?: typeof ListAssistantsParams.Encoded | undefined + ) => Effect.Effect + /** + * Create an assistant with a model and instructions. + */ + readonly "createAssistant": ( + options: typeof CreateAssistantRequest.Encoded + ) => Effect.Effect + /** + * Retrieves an assistant. + */ + readonly "getAssistant": ( + assistantId: string + ) => Effect.Effect + /** + * Modifies an assistant. + */ + readonly "modifyAssistant": ( + assistantId: string, + options: typeof ModifyAssistantRequest.Encoded + ) => Effect.Effect + /** + * Delete an assistant. + */ + readonly "deleteAssistant": ( + assistantId: string + ) => Effect.Effect + /** + * Generates audio from the input text. + */ + readonly "createSpeech": ( + options: typeof CreateSpeechRequest.Encoded + ) => Effect.Effect + /** + * Transcribes audio into the input language. + */ + readonly "createTranscription": ( + options: typeof CreateTranscriptionRequest.Encoded + ) => Effect.Effect + /** + * Translates audio into English. + */ + readonly "createTranslation": ( + options: typeof CreateTranslationRequest.Encoded + ) => Effect.Effect + /** + * List your organization's batches. + */ + readonly "listBatches": ( + options?: typeof ListBatchesParams.Encoded | undefined + ) => Effect.Effect + /** + * Creates and executes a batch from an uploaded file of requests + */ + readonly "createBatch": ( + options: typeof CreateBatchRequest.Encoded + ) => Effect.Effect + /** + * Retrieves a batch. + */ + readonly "retrieveBatch": ( + batchId: string + ) => Effect.Effect + /** + * Cancels an in-progress batch. The batch will be in status `cancelling` for up to 10 minutes, before changing to `cancelled`, where it will have partial results (if any) available in the output file. + */ + readonly "cancelBatch": ( + batchId: string + ) => Effect.Effect + /** + * List stored Chat Completions. Only Chat Completions that have been stored + * with the `store` parameter set to `true` will be returned. + */ + readonly "listChatCompletions": ( + options?: typeof ListChatCompletionsParams.Encoded | undefined + ) => Effect.Effect + /** + * **Starting a new project?** We recommend trying [Responses](https://platform.openai.com/docs/api-reference/responses) + * to take advantage of the latest OpenAI platform features. Compare + * [Chat Completions with Responses](https://platform.openai.com/docs/guides/responses-vs-chat-completions?api-mode=responses). + * + * --- + * + * Creates a model response for the given chat conversation. Learn more in the + * [text generation](https://platform.openai.com/docs/guides/text-generation), [vision](https://platform.openai.com/docs/guides/vision), + * and [audio](https://platform.openai.com/docs/guides/audio) guides. + * + * Parameter support can differ depending on the model used to generate the + * response, particularly for newer reasoning models. Parameters that are only + * supported for reasoning models are noted below. For the current state of + * unsupported parameters in reasoning models, + * [refer to the reasoning guide](https://platform.openai.com/docs/guides/reasoning). + */ + readonly "createChatCompletion": ( + options: typeof CreateChatCompletionRequest.Encoded + ) => Effect.Effect + /** + * Get a stored chat completion. Only Chat Completions that have been created + * with the `store` parameter set to `true` will be returned. + */ + readonly "getChatCompletion": ( + completionId: string + ) => Effect.Effect + /** + * Modify a stored chat completion. Only Chat Completions that have been + * created with the `store` parameter set to `true` can be modified. Currently, + * the only supported modification is to update the `metadata` field. + */ + readonly "updateChatCompletion": ( + completionId: string, + options: typeof UpdateChatCompletionRequest.Encoded + ) => Effect.Effect + /** + * Delete a stored chat completion. Only Chat Completions that have been + * created with the `store` parameter set to `true` can be deleted. + */ + readonly "deleteChatCompletion": ( + completionId: string + ) => Effect.Effect + /** + * Get the messages in a stored chat completion. Only Chat Completions that + * have been created with the `store` parameter set to `true` will be + * returned. + */ + readonly "getChatCompletionMessages": ( + completionId: string, + options?: typeof GetChatCompletionMessagesParams.Encoded | undefined + ) => Effect.Effect + /** + * Creates a completion for the provided prompt and parameters. + */ + readonly "createCompletion": ( + options: typeof CreateCompletionRequest.Encoded + ) => Effect.Effect + /** + * List Containers + */ + readonly "ListContainers": ( + options?: typeof ListContainersParams.Encoded | undefined + ) => Effect.Effect + /** + * Create Container + */ + readonly "CreateContainer": ( + options: typeof CreateContainerBody.Encoded + ) => Effect.Effect + /** + * Retrieve Container + */ + readonly "RetrieveContainer": ( + containerId: string + ) => Effect.Effect + /** + * Delete Container + */ + readonly "DeleteContainer": (containerId: string) => Effect.Effect + /** + * List Container files + */ + readonly "ListContainerFiles": ( + containerId: string, + options?: typeof ListContainerFilesParams.Encoded | undefined + ) => Effect.Effect + /** + * Create a Container File + * + * You can send either a multipart/form-data request with the raw file content, or a JSON request with a file ID. + */ + readonly "CreateContainerFile": ( + containerId: string, + options: typeof CreateContainerFileBody.Encoded + ) => Effect.Effect + /** + * Retrieve Container File + */ + readonly "RetrieveContainerFile": ( + containerId: string, + fileId: string + ) => Effect.Effect + /** + * Delete Container File + */ + readonly "DeleteContainerFile": ( + containerId: string, + fileId: string + ) => Effect.Effect + /** + * Retrieve Container File Content + */ + readonly "RetrieveContainerFileContent": ( + containerId: string, + fileId: string + ) => Effect.Effect + /** + * List all items for a conversation with the given ID. + */ + readonly "listConversationItems": ( + conversationId: string, + options?: typeof ListConversationItemsParams.Encoded | undefined + ) => Effect.Effect + /** + * Create items in a conversation with the given ID. + */ + readonly "createConversationItems": ( + conversationId: string, + options: { + readonly params?: typeof CreateConversationItemsParams.Encoded | undefined + readonly payload: typeof CreateConversationItemsRequest.Encoded + } + ) => Effect.Effect + /** + * Get a single item from a conversation with the given IDs. + */ + readonly "getConversationItem": ( + conversationId: string, + itemId: string, + options?: typeof GetConversationItemParams.Encoded | undefined + ) => Effect.Effect + /** + * Delete an item from a conversation with the given IDs. + */ + readonly "deleteConversationItem": ( + conversationId: string, + itemId: string + ) => Effect.Effect + /** + * Creates an embedding vector representing the input text. + */ + readonly "createEmbedding": ( + options: typeof CreateEmbeddingRequest.Encoded + ) => Effect.Effect + /** + * List evaluations for a project. + */ + readonly "listEvals": ( + options?: typeof ListEvalsParams.Encoded | undefined + ) => Effect.Effect + /** + * Create the structure of an evaluation that can be used to test a model's performance. + * An evaluation is a set of testing criteria and the config for a data source, which dictates the schema of the data used in the evaluation. After creating an evaluation, you can run it on different models and model parameters. We support several types of graders and datasources. + * For more information, see the [Evals guide](https://platform.openai.com/docs/guides/evals). + */ + readonly "createEval": ( + options: typeof CreateEvalRequest.Encoded + ) => Effect.Effect + /** + * Get an evaluation by ID. + */ + readonly "getEval": (evalId: string) => Effect.Effect + /** + * Update certain properties of an evaluation. + */ + readonly "updateEval": ( + evalId: string, + options: typeof UpdateEvalRequest.Encoded + ) => Effect.Effect + /** + * Delete an evaluation. + */ + readonly "deleteEval": ( + evalId: string + ) => Effect.Effect< + typeof DeleteEval200.Type, + HttpClientError.HttpClientError | ParseError | ClientError<"Error", typeof Error.Type> + > + /** + * Get a list of runs for an evaluation. + */ + readonly "getEvalRuns": ( + evalId: string, + options?: typeof GetEvalRunsParams.Encoded | undefined + ) => Effect.Effect + /** + * Kicks off a new run for a given evaluation, specifying the data source, and what model configuration to use to test. The datasource will be validated against the schema specified in the config of the evaluation. + */ + readonly "createEvalRun": ( + evalId: string, + options: typeof CreateEvalRunRequest.Encoded + ) => Effect.Effect< + typeof EvalRun.Type, + HttpClientError.HttpClientError | ParseError | ClientError<"Error", typeof Error.Type> + > + /** + * Get an evaluation run by ID. + */ + readonly "getEvalRun": ( + evalId: string, + runId: string + ) => Effect.Effect + /** + * Cancel an ongoing evaluation run. + */ + readonly "cancelEvalRun": ( + evalId: string, + runId: string + ) => Effect.Effect + /** + * Delete an eval run. + */ + readonly "deleteEvalRun": ( + evalId: string, + runId: string + ) => Effect.Effect< + typeof DeleteEvalRun200.Type, + HttpClientError.HttpClientError | ParseError | ClientError<"Error", typeof Error.Type> + > + /** + * Get a list of output items for an evaluation run. + */ + readonly "getEvalRunOutputItems": ( + evalId: string, + runId: string, + options?: typeof GetEvalRunOutputItemsParams.Encoded | undefined + ) => Effect.Effect + /** + * Get an evaluation run output item by ID. + */ + readonly "getEvalRunOutputItem": ( + evalId: string, + runId: string, + outputItemId: string + ) => Effect.Effect + /** + * Returns a list of files. + */ + readonly "listFiles": ( + options?: typeof ListFilesParams.Encoded | undefined + ) => Effect.Effect + /** + * Upload a file that can be used across various endpoints. Individual files + * can be up to 512 MB, and the size of all files uploaded by one organization + * can be up to 1 TB. + * + * - The Assistants API supports files up to 2 million tokens and of specific + * file types. See the [Assistants Tools guide](https://platform.openai.com/docs/assistants/tools) for + * details. + * - The Fine-tuning API only supports `.jsonl` files. The input also has + * certain required formats for fine-tuning + * [chat](https://platform.openai.com/docs/api-reference/fine-tuning/chat-input) or + * [completions](https://platform.openai.com/docs/api-reference/fine-tuning/completions-input) models. + * - The Batch API only supports `.jsonl` files up to 200 MB in size. The input + * also has a specific required + * [format](https://platform.openai.com/docs/api-reference/batch/request-input). + * + * Please [contact us](https://help.openai.com/) if you need to increase these + * storage limits. + */ + readonly "createFile": ( + options: typeof CreateFileRequest.Encoded + ) => Effect.Effect + /** + * Returns information about a specific file. + */ + readonly "retrieveFile": ( + fileId: string + ) => Effect.Effect + /** + * Delete a file and remove it from all vector stores. + */ + readonly "deleteFile": ( + fileId: string + ) => Effect.Effect + /** + * Returns the contents of the specified file. + */ + readonly "downloadFile": ( + fileId: string + ) => Effect.Effect + /** + * Run a grader. + */ + readonly "runGrader": ( + options: typeof RunGraderRequest.Encoded + ) => Effect.Effect + /** + * Validate a grader. + */ + readonly "validateGrader": ( + options: typeof ValidateGraderRequest.Encoded + ) => Effect.Effect + /** + * **NOTE:** This endpoint requires an [admin API key](../admin-api-keys). + * + * Organization owners can use this endpoint to view all permissions for a fine-tuned model checkpoint. + */ + readonly "listFineTuningCheckpointPermissions": ( + fineTunedModelCheckpoint: string, + options?: typeof ListFineTuningCheckpointPermissionsParams.Encoded | undefined + ) => Effect.Effect< + typeof ListFineTuningCheckpointPermissionResponse.Type, + HttpClientError.HttpClientError | ParseError + > + /** + * **NOTE:** Calling this endpoint requires an [admin API key](../admin-api-keys). + * + * This enables organization owners to share fine-tuned models with other projects in their organization. + */ + readonly "createFineTuningCheckpointPermission": ( + fineTunedModelCheckpoint: string, + options: typeof CreateFineTuningCheckpointPermissionRequest.Encoded + ) => Effect.Effect< + typeof ListFineTuningCheckpointPermissionResponse.Type, + HttpClientError.HttpClientError | ParseError + > + /** + * **NOTE:** This endpoint requires an [admin API key](../admin-api-keys). + * + * Organization owners can use this endpoint to delete a permission for a fine-tuned model checkpoint. + */ + readonly "deleteFineTuningCheckpointPermission": ( + fineTunedModelCheckpoint: string, + permissionId: string + ) => Effect.Effect< + typeof DeleteFineTuningCheckpointPermissionResponse.Type, + HttpClientError.HttpClientError | ParseError + > + /** + * List your organization's fine-tuning jobs + */ + readonly "listPaginatedFineTuningJobs": ( + options?: typeof ListPaginatedFineTuningJobsParams.Encoded | undefined + ) => Effect.Effect + /** + * Creates a fine-tuning job which begins the process of creating a new model from a given dataset. + * + * Response includes details of the enqueued job including job status and the name of the fine-tuned models once complete. + * + * [Learn more about fine-tuning](https://platform.openai.com/docs/guides/model-optimization) + */ + readonly "createFineTuningJob": ( + options: typeof CreateFineTuningJobRequest.Encoded + ) => Effect.Effect + /** + * Get info about a fine-tuning job. + * + * [Learn more about fine-tuning](https://platform.openai.com/docs/guides/model-optimization) + */ + readonly "retrieveFineTuningJob": ( + fineTuningJobId: string + ) => Effect.Effect + /** + * Immediately cancel a fine-tune job. + */ + readonly "cancelFineTuningJob": ( + fineTuningJobId: string + ) => Effect.Effect + /** + * List checkpoints for a fine-tuning job. + */ + readonly "listFineTuningJobCheckpoints": ( + fineTuningJobId: string, + options?: typeof ListFineTuningJobCheckpointsParams.Encoded | undefined + ) => Effect.Effect + /** + * Get status updates for a fine-tuning job. + */ + readonly "listFineTuningEvents": ( + fineTuningJobId: string, + options?: typeof ListFineTuningEventsParams.Encoded | undefined + ) => Effect.Effect + /** + * Pause a fine-tune job. + */ + readonly "pauseFineTuningJob": ( + fineTuningJobId: string + ) => Effect.Effect + /** + * Resume a fine-tune job. + */ + readonly "resumeFineTuningJob": ( + fineTuningJobId: string + ) => Effect.Effect + /** + * Creates an edited or extended image given one or more source images and a prompt. This endpoint only supports `gpt-image-1` and `dall-e-2`. + */ + readonly "createImageEdit": ( + options: typeof CreateImageEditRequest.Encoded + ) => Effect.Effect + /** + * Creates an image given a prompt. [Learn more](https://platform.openai.com/docs/guides/images). + */ + readonly "createImage": ( + options: typeof CreateImageRequest.Encoded + ) => Effect.Effect + /** + * Creates a variation of a given image. This endpoint only supports `dall-e-2`. + */ + readonly "createImageVariation": ( + options: typeof CreateImageVariationRequest.Encoded + ) => Effect.Effect + /** + * Lists the currently available models, and provides basic information about each one such as the owner and availability. + */ + readonly "listModels": () => Effect.Effect< + typeof ListModelsResponse.Type, + HttpClientError.HttpClientError | ParseError + > + /** + * Retrieves a model instance, providing basic information about the model such as the owner and permissioning. + */ + readonly "retrieveModel": ( + model: string + ) => Effect.Effect + /** + * Delete a fine-tuned model. You must have the Owner role in your organization to delete a model. + */ + readonly "deleteModel": ( + model: string + ) => Effect.Effect + /** + * Classifies if text and/or image inputs are potentially harmful. Learn + * more in the [moderation guide](https://platform.openai.com/docs/guides/moderation). + */ + readonly "createModeration": ( + options: typeof CreateModerationRequest.Encoded + ) => Effect.Effect + /** + * List organization API keys + */ + readonly "adminApiKeysList": ( + options?: typeof AdminApiKeysListParams.Encoded | undefined + ) => Effect.Effect + /** + * Create an organization admin API key + */ + readonly "adminApiKeysCreate": ( + options: typeof AdminApiKeysCreateRequest.Encoded + ) => Effect.Effect + /** + * Retrieve a single organization API key + */ + readonly "adminApiKeysGet": ( + keyId: string + ) => Effect.Effect + /** + * Delete an organization admin API key + */ + readonly "adminApiKeysDelete": ( + keyId: string + ) => Effect.Effect + /** + * List user actions and configuration changes within this organization. + */ + readonly "listAuditLogs": ( + options?: typeof ListAuditLogsParams.Encoded | undefined + ) => Effect.Effect + /** + * List uploaded certificates for this organization. + */ + readonly "listOrganizationCertificates": ( + options?: typeof ListOrganizationCertificatesParams.Encoded | undefined + ) => Effect.Effect + /** + * Upload a certificate to the organization. This does **not** automatically activate the certificate. + * + * Organizations can upload up to 50 certificates. + */ + readonly "uploadCertificate": ( + options: typeof UploadCertificateRequest.Encoded + ) => Effect.Effect + /** + * Activate certificates at the organization level. + * + * You can atomically and idempotently activate up to 10 certificates at a time. + */ + readonly "activateOrganizationCertificates": ( + options: typeof ToggleCertificatesRequest.Encoded + ) => Effect.Effect + /** + * Deactivate certificates at the organization level. + * + * You can atomically and idempotently deactivate up to 10 certificates at a time. + */ + readonly "deactivateOrganizationCertificates": ( + options: typeof ToggleCertificatesRequest.Encoded + ) => Effect.Effect + /** + * Get a certificate that has been uploaded to the organization. + * + * You can get a certificate regardless of whether it is active or not. + */ + readonly "getCertificate": ( + certificateId: string, + options?: typeof GetCertificateParams.Encoded | undefined + ) => Effect.Effect + /** + * Modify a certificate. Note that only the name can be modified. + */ + readonly "modifyCertificate": ( + certificateId: string, + options: typeof ModifyCertificateRequest.Encoded + ) => Effect.Effect + /** + * Delete a certificate from the organization. + * + * The certificate must be inactive for the organization and all projects. + */ + readonly "deleteCertificate": ( + certificateId: string + ) => Effect.Effect + /** + * Get costs details for the organization. + */ + readonly "usageCosts": ( + options: typeof UsageCostsParams.Encoded + ) => Effect.Effect + /** + * Lists all groups in the organization. + */ + readonly "listGroups": ( + options?: typeof ListGroupsParams.Encoded | undefined + ) => Effect.Effect + /** + * Creates a new group in the organization. + */ + readonly "createGroup": ( + options: typeof CreateGroupBody.Encoded + ) => Effect.Effect + /** + * Updates a group's information. + */ + readonly "updateGroup": ( + groupId: string, + options: typeof UpdateGroupBody.Encoded + ) => Effect.Effect + /** + * Deletes a group from the organization. + */ + readonly "deleteGroup": ( + groupId: string + ) => Effect.Effect + /** + * Lists the organization roles assigned to a group within the organization. + */ + readonly "listGroupRoleAssignments": ( + groupId: string, + options?: typeof ListGroupRoleAssignmentsParams.Encoded | undefined + ) => Effect.Effect + /** + * Assigns an organization role to a group within the organization. + */ + readonly "assignGroupRole": ( + groupId: string, + options: typeof PublicAssignOrganizationGroupRoleBody.Encoded + ) => Effect.Effect + /** + * Unassigns an organization role from a group within the organization. + */ + readonly "unassignGroupRole": ( + groupId: string, + roleId: string + ) => Effect.Effect + /** + * Lists the users assigned to a group. + */ + readonly "listGroupUsers": ( + groupId: string, + options?: typeof ListGroupUsersParams.Encoded | undefined + ) => Effect.Effect + /** + * Adds a user to a group. + */ + readonly "addGroupUser": ( + groupId: string, + options: typeof CreateGroupUserBody.Encoded + ) => Effect.Effect + /** + * Removes a user from a group. + */ + readonly "removeGroupUser": ( + groupId: string, + userId: string + ) => Effect.Effect + /** + * Returns a list of invites in the organization. + */ + readonly "listInvites": ( + options?: typeof ListInvitesParams.Encoded | undefined + ) => Effect.Effect + /** + * Create an invite for a user to the organization. The invite must be accepted by the user before they have access to the organization. + */ + readonly "inviteUser": ( + options: typeof InviteRequest.Encoded + ) => Effect.Effect + /** + * Retrieves an invite. + */ + readonly "retrieveInvite": ( + inviteId: string + ) => Effect.Effect + /** + * Delete an invite. If the invite has already been accepted, it cannot be deleted. + */ + readonly "deleteInvite": ( + inviteId: string + ) => Effect.Effect + /** + * Returns a list of projects. + */ + readonly "listProjects": ( + options?: typeof ListProjectsParams.Encoded | undefined + ) => Effect.Effect + /** + * Create a new project in the organization. Projects can be created and archived, but cannot be deleted. + */ + readonly "createProject": ( + options: typeof ProjectCreateRequest.Encoded + ) => Effect.Effect + /** + * Retrieves a project. + */ + readonly "retrieveProject": ( + projectId: string + ) => Effect.Effect + /** + * Modifies a project in the organization. + */ + readonly "modifyProject": ( + projectId: string, + options: typeof ProjectUpdateRequest.Encoded + ) => Effect.Effect< + typeof Project.Type, + HttpClientError.HttpClientError | ParseError | ClientError<"ErrorResponse", typeof ErrorResponse.Type> + > + /** + * Returns a list of API keys in the project. + */ + readonly "listProjectApiKeys": ( + projectId: string, + options?: typeof ListProjectApiKeysParams.Encoded | undefined + ) => Effect.Effect + /** + * Retrieves an API key in the project. + */ + readonly "retrieveProjectApiKey": ( + projectId: string, + keyId: string + ) => Effect.Effect + /** + * Deletes an API key from the project. + */ + readonly "deleteProjectApiKey": ( + projectId: string, + keyId: string + ) => Effect.Effect< + typeof ProjectApiKeyDeleteResponse.Type, + HttpClientError.HttpClientError | ParseError | ClientError<"ErrorResponse", typeof ErrorResponse.Type> + > + /** + * Archives a project in the organization. Archived projects cannot be used or updated. + */ + readonly "archiveProject": ( + projectId: string + ) => Effect.Effect + /** + * List certificates for this project. + */ + readonly "listProjectCertificates": ( + projectId: string, + options?: typeof ListProjectCertificatesParams.Encoded | undefined + ) => Effect.Effect + /** + * Activate certificates at the project level. + * + * You can atomically and idempotently activate up to 10 certificates at a time. + */ + readonly "activateProjectCertificates": ( + projectId: string, + options: typeof ToggleCertificatesRequest.Encoded + ) => Effect.Effect + /** + * Deactivate certificates at the project level. You can atomically and + * idempotently deactivate up to 10 certificates at a time. + */ + readonly "deactivateProjectCertificates": ( + projectId: string, + options: typeof ToggleCertificatesRequest.Encoded + ) => Effect.Effect + /** + * Lists the groups that have access to a project. + */ + readonly "listProjectGroups": ( + projectId: string, + options?: typeof ListProjectGroupsParams.Encoded | undefined + ) => Effect.Effect + /** + * Grants a group access to a project. + */ + readonly "addProjectGroup": ( + projectId: string, + options: typeof InviteProjectGroupBody.Encoded + ) => Effect.Effect + /** + * Revokes a group's access to a project. + */ + readonly "removeProjectGroup": ( + projectId: string, + groupId: string + ) => Effect.Effect + /** + * Returns the rate limits per model for a project. + */ + readonly "listProjectRateLimits": ( + projectId: string, + options?: typeof ListProjectRateLimitsParams.Encoded | undefined + ) => Effect.Effect + /** + * Updates a project rate limit. + */ + readonly "updateProjectRateLimits": ( + projectId: string, + rateLimitId: string, + options: typeof ProjectRateLimitUpdateRequest.Encoded + ) => Effect.Effect< + typeof ProjectRateLimit.Type, + HttpClientError.HttpClientError | ParseError | ClientError<"ErrorResponse", typeof ErrorResponse.Type> + > + /** + * Returns a list of service accounts in the project. + */ + readonly "listProjectServiceAccounts": ( + projectId: string, + options?: typeof ListProjectServiceAccountsParams.Encoded | undefined + ) => Effect.Effect< + typeof ProjectServiceAccountListResponse.Type, + HttpClientError.HttpClientError | ParseError | ClientError<"ErrorResponse", typeof ErrorResponse.Type> + > + /** + * Creates a new service account in the project. This also returns an unredacted API key for the service account. + */ + readonly "createProjectServiceAccount": ( + projectId: string, + options: typeof ProjectServiceAccountCreateRequest.Encoded + ) => Effect.Effect< + typeof ProjectServiceAccountCreateResponse.Type, + HttpClientError.HttpClientError | ParseError | ClientError<"ErrorResponse", typeof ErrorResponse.Type> + > + /** + * Retrieves a service account in the project. + */ + readonly "retrieveProjectServiceAccount": ( + projectId: string, + serviceAccountId: string + ) => Effect.Effect + /** + * Deletes a service account from the project. + */ + readonly "deleteProjectServiceAccount": ( + projectId: string, + serviceAccountId: string + ) => Effect.Effect + /** + * Returns a list of users in the project. + */ + readonly "listProjectUsers": ( + projectId: string, + options?: typeof ListProjectUsersParams.Encoded | undefined + ) => Effect.Effect< + typeof ProjectUserListResponse.Type, + HttpClientError.HttpClientError | ParseError | ClientError<"ErrorResponse", typeof ErrorResponse.Type> + > + /** + * Adds a user to the project. Users must already be members of the organization to be added to a project. + */ + readonly "createProjectUser": ( + projectId: string, + options: typeof ProjectUserCreateRequest.Encoded + ) => Effect.Effect< + typeof ProjectUser.Type, + HttpClientError.HttpClientError | ParseError | ClientError<"ErrorResponse", typeof ErrorResponse.Type> + > + /** + * Retrieves a user in the project. + */ + readonly "retrieveProjectUser": ( + projectId: string, + userId: string + ) => Effect.Effect + /** + * Modifies a user's role in the project. + */ + readonly "modifyProjectUser": ( + projectId: string, + userId: string, + options: typeof ProjectUserUpdateRequest.Encoded + ) => Effect.Effect< + typeof ProjectUser.Type, + HttpClientError.HttpClientError | ParseError | ClientError<"ErrorResponse", typeof ErrorResponse.Type> + > + /** + * Deletes a user from the project. + */ + readonly "deleteProjectUser": ( + projectId: string, + userId: string + ) => Effect.Effect< + typeof ProjectUserDeleteResponse.Type, + HttpClientError.HttpClientError | ParseError | ClientError<"ErrorResponse", typeof ErrorResponse.Type> + > + /** + * Lists the roles configured for the organization. + */ + readonly "listRoles": ( + options?: typeof ListRolesParams.Encoded | undefined + ) => Effect.Effect + /** + * Creates a custom role for the organization. + */ + readonly "createRole": ( + options: typeof PublicCreateOrganizationRoleBody.Encoded + ) => Effect.Effect + /** + * Updates an existing organization role. + */ + readonly "updateRole": ( + roleId: string, + options: typeof PublicUpdateOrganizationRoleBody.Encoded + ) => Effect.Effect + /** + * Deletes a custom role from the organization. + */ + readonly "deleteRole": ( + roleId: string + ) => Effect.Effect + /** + * Get audio speeches usage details for the organization. + */ + readonly "usageAudioSpeeches": ( + options: typeof UsageAudioSpeechesParams.Encoded + ) => Effect.Effect + /** + * Get audio transcriptions usage details for the organization. + */ + readonly "usageAudioTranscriptions": ( + options: typeof UsageAudioTranscriptionsParams.Encoded + ) => Effect.Effect + /** + * Get code interpreter sessions usage details for the organization. + */ + readonly "usageCodeInterpreterSessions": ( + options: typeof UsageCodeInterpreterSessionsParams.Encoded + ) => Effect.Effect + /** + * Get completions usage details for the organization. + */ + readonly "usageCompletions": ( + options: typeof UsageCompletionsParams.Encoded + ) => Effect.Effect + /** + * Get embeddings usage details for the organization. + */ + readonly "usageEmbeddings": ( + options: typeof UsageEmbeddingsParams.Encoded + ) => Effect.Effect + /** + * Get images usage details for the organization. + */ + readonly "usageImages": ( + options: typeof UsageImagesParams.Encoded + ) => Effect.Effect + /** + * Get moderations usage details for the organization. + */ + readonly "usageModerations": ( + options: typeof UsageModerationsParams.Encoded + ) => Effect.Effect + /** + * Get vector stores usage details for the organization. + */ + readonly "usageVectorStores": ( + options: typeof UsageVectorStoresParams.Encoded + ) => Effect.Effect + /** + * Lists all of the users in the organization. + */ + readonly "listUsers": ( + options?: typeof ListUsersParams.Encoded | undefined + ) => Effect.Effect + /** + * Retrieves a user by their identifier. + */ + readonly "retrieveUser": ( + userId: string + ) => Effect.Effect + /** + * Modifies a user's role in the organization. + */ + readonly "modifyUser": ( + userId: string, + options: typeof UserRoleUpdateRequest.Encoded + ) => Effect.Effect + /** + * Deletes a user from the organization. + */ + readonly "deleteUser": ( + userId: string + ) => Effect.Effect + /** + * Lists the organization roles assigned to a user within the organization. + */ + readonly "listUserRoleAssignments": ( + userId: string, + options?: typeof ListUserRoleAssignmentsParams.Encoded | undefined + ) => Effect.Effect + /** + * Assigns an organization role to a user within the organization. + */ + readonly "assignUserRole": ( + userId: string, + options: typeof PublicAssignOrganizationGroupRoleBody.Encoded + ) => Effect.Effect + /** + * Unassigns an organization role from a user within the organization. + */ + readonly "unassignUserRole": ( + userId: string, + roleId: string + ) => Effect.Effect + /** + * Lists the project roles assigned to a group within a project. + */ + readonly "listProjectGroupRoleAssignments": ( + projectId: string, + groupId: string, + options?: typeof ListProjectGroupRoleAssignmentsParams.Encoded | undefined + ) => Effect.Effect + /** + * Assigns a project role to a group within a project. + */ + readonly "assignProjectGroupRole": ( + projectId: string, + groupId: string, + options: typeof PublicAssignOrganizationGroupRoleBody.Encoded + ) => Effect.Effect + /** + * Unassigns a project role from a group within a project. + */ + readonly "unassignProjectGroupRole": ( + projectId: string, + groupId: string, + roleId: string + ) => Effect.Effect + /** + * Lists the roles configured for a project. + */ + readonly "listProjectRoles": ( + projectId: string, + options?: typeof ListProjectRolesParams.Encoded | undefined + ) => Effect.Effect + /** + * Creates a custom role for a project. + */ + readonly "createProjectRole": ( + projectId: string, + options: typeof PublicCreateOrganizationRoleBody.Encoded + ) => Effect.Effect + /** + * Updates an existing project role. + */ + readonly "updateProjectRole": ( + projectId: string, + roleId: string, + options: typeof PublicUpdateOrganizationRoleBody.Encoded + ) => Effect.Effect + /** + * Deletes a custom role from a project. + */ + readonly "deleteProjectRole": ( + projectId: string, + roleId: string + ) => Effect.Effect + /** + * Lists the project roles assigned to a user within a project. + */ + readonly "listProjectUserRoleAssignments": ( + projectId: string, + userId: string, + options?: typeof ListProjectUserRoleAssignmentsParams.Encoded | undefined + ) => Effect.Effect + /** + * Assigns a project role to a user within a project. + */ + readonly "assignProjectUserRole": ( + projectId: string, + userId: string, + options: typeof PublicAssignOrganizationGroupRoleBody.Encoded + ) => Effect.Effect + /** + * Unassigns a project role from a user within a project. + */ + readonly "unassignProjectUserRole": ( + projectId: string, + userId: string, + roleId: string + ) => Effect.Effect + /** + * Create a new Realtime API call over WebRTC and receive the SDP answer needed + * to complete the peer connection. + */ + readonly "createRealtimeCall": ( + options: typeof RealtimeCallCreateRequest.Encoded + ) => Effect.Effect + /** + * Accept an incoming SIP call and configure the realtime session that will + * handle it. + */ + readonly "acceptRealtimeCall": ( + callId: string, + options: typeof RealtimeSessionCreateRequestGA.Encoded + ) => Effect.Effect + /** + * End an active Realtime API call, whether it was initiated over SIP or + * WebRTC. + */ + readonly "hangupRealtimeCall": (callId: string) => Effect.Effect + /** + * Transfer an active SIP call to a new destination using the SIP REFER verb. + */ + readonly "referRealtimeCall": ( + callId: string, + options: typeof RealtimeCallReferRequest.Encoded + ) => Effect.Effect + /** + * Decline an incoming SIP call by returning a SIP status code to the caller. + */ + readonly "rejectRealtimeCall": ( + callId: string, + options: typeof RealtimeCallRejectRequest.Encoded + ) => Effect.Effect + /** + * Create a Realtime client secret with an associated session configuration. + */ + readonly "createRealtimeClientSecret": ( + options: typeof RealtimeCreateClientSecretRequest.Encoded + ) => Effect.Effect + /** + * Create an ephemeral API token for use in client-side applications with the + * Realtime API. Can be configured with the same session parameters as the + * `session.update` client event. + * + * It responds with a session object, plus a `client_secret` key which contains + * a usable ephemeral API token that can be used to authenticate browser clients + * for the Realtime API. + */ + readonly "createRealtimeSession": ( + options: typeof RealtimeSessionCreateRequest.Encoded + ) => Effect.Effect + /** + * Create an ephemeral API token for use in client-side applications with the + * Realtime API specifically for realtime transcriptions. + * Can be configured with the same session parameters as the `transcription_session.update` client event. + * + * It responds with a session object, plus a `client_secret` key which contains + * a usable ephemeral API token that can be used to authenticate browser clients + * for the Realtime API. + */ + readonly "createRealtimeTranscriptionSession": ( + options: typeof RealtimeTranscriptionSessionCreateRequest.Encoded + ) => Effect.Effect< + typeof RealtimeTranscriptionSessionCreateResponse.Type, + HttpClientError.HttpClientError | ParseError + > + /** + * Creates a model response. Provide [text](https://platform.openai.com/docs/guides/text) or + * [image](https://platform.openai.com/docs/guides/images) inputs to generate [text](https://platform.openai.com/docs/guides/text) + * or [JSON](https://platform.openai.com/docs/guides/structured-outputs) outputs. Have the model call + * your own [custom code](https://platform.openai.com/docs/guides/function-calling) or use built-in + * [tools](https://platform.openai.com/docs/guides/tools) like [web search](https://platform.openai.com/docs/guides/tools-web-search) + * or [file search](https://platform.openai.com/docs/guides/tools-file-search) to use your own data + * as input for the model's response. + */ + readonly "createResponse": ( + options: typeof CreateResponse.Encoded + ) => Effect.Effect + /** + * Retrieves a model response with the given ID. + */ + readonly "getResponse": ( + responseId: string, + options?: typeof GetResponseParams.Encoded | undefined + ) => Effect.Effect + /** + * Deletes a model response with the given ID. + */ + readonly "deleteResponse": ( + responseId: string + ) => Effect.Effect> + /** + * Cancels a model response with the given ID. Only responses created with + * the `background` parameter set to `true` can be cancelled. + * [Learn more](https://platform.openai.com/docs/guides/background). + */ + readonly "cancelResponse": ( + responseId: string + ) => Effect.Effect< + typeof Response.Type, + HttpClientError.HttpClientError | ParseError | ClientError<"Error", typeof Error.Type> + > + /** + * Returns a list of input items for a given response. + */ + readonly "listInputItems": ( + responseId: string, + options?: typeof ListInputItemsParams.Encoded | undefined + ) => Effect.Effect + /** + * Create a thread. + */ + readonly "createThread": ( + options: typeof CreateThreadRequest.Encoded + ) => Effect.Effect + /** + * Create a thread and run it in one request. + */ + readonly "createThreadAndRun": ( + options: typeof CreateThreadAndRunRequest.Encoded + ) => Effect.Effect + /** + * Retrieves a thread. + */ + readonly "getThread": ( + threadId: string + ) => Effect.Effect + /** + * Modifies a thread. + */ + readonly "modifyThread": ( + threadId: string, + options: typeof ModifyThreadRequest.Encoded + ) => Effect.Effect + /** + * Delete a thread. + */ + readonly "deleteThread": ( + threadId: string + ) => Effect.Effect + /** + * Returns a list of messages for a given thread. + */ + readonly "listMessages": ( + threadId: string, + options?: typeof ListMessagesParams.Encoded | undefined + ) => Effect.Effect + /** + * Create a message. + */ + readonly "createMessage": ( + threadId: string, + options: typeof CreateMessageRequest.Encoded + ) => Effect.Effect + /** + * Retrieve a message. + */ + readonly "getMessage": ( + threadId: string, + messageId: string + ) => Effect.Effect + /** + * Modifies a message. + */ + readonly "modifyMessage": ( + threadId: string, + messageId: string, + options: typeof ModifyMessageRequest.Encoded + ) => Effect.Effect + /** + * Deletes a message. + */ + readonly "deleteMessage": ( + threadId: string, + messageId: string + ) => Effect.Effect + /** + * Returns a list of runs belonging to a thread. + */ + readonly "listRuns": ( + threadId: string, + options?: typeof ListRunsParams.Encoded | undefined + ) => Effect.Effect + /** + * Create a run. + */ + readonly "createRun": ( + threadId: string, + options: { + readonly params?: typeof CreateRunParams.Encoded | undefined + readonly payload: typeof CreateRunRequest.Encoded + } + ) => Effect.Effect + /** + * Retrieves a run. + */ + readonly "getRun": ( + threadId: string, + runId: string + ) => Effect.Effect + /** + * Modifies a run. + */ + readonly "modifyRun": ( + threadId: string, + runId: string, + options: typeof ModifyRunRequest.Encoded + ) => Effect.Effect + /** + * Cancels a run that is `in_progress`. + */ + readonly "cancelRun": ( + threadId: string, + runId: string + ) => Effect.Effect + /** + * Returns a list of run steps belonging to a run. + */ + readonly "listRunSteps": ( + threadId: string, + runId: string, + options?: typeof ListRunStepsParams.Encoded | undefined + ) => Effect.Effect + /** + * Retrieves a run step. + */ + readonly "getRunStep": ( + threadId: string, + runId: string, + stepId: string, + options?: typeof GetRunStepParams.Encoded | undefined + ) => Effect.Effect + /** + * When a run has the `status: "requires_action"` and `required_action.type` is `submit_tool_outputs`, this endpoint can be used to submit the outputs from the tool calls once they're all completed. All outputs must be submitted in a single request. + */ + readonly "submitToolOuputsToRun": ( + threadId: string, + runId: string, + options: typeof SubmitToolOutputsRunRequest.Encoded + ) => Effect.Effect + /** + * Creates an intermediate [Upload](https://platform.openai.com/docs/api-reference/uploads/object) object + * that you can add [Parts](https://platform.openai.com/docs/api-reference/uploads/part-object) to. + * Currently, an Upload can accept at most 8 GB in total and expires after an + * hour after you create it. + * + * Once you complete the Upload, we will create a + * [File](https://platform.openai.com/docs/api-reference/files/object) object that contains all the parts + * you uploaded. This File is usable in the rest of our platform as a regular + * File object. + * + * For certain `purpose` values, the correct `mime_type` must be specified. + * Please refer to documentation for the + * [supported MIME types for your use case](https://platform.openai.com/docs/assistants/tools/file-search#supported-files). + * + * For guidance on the proper filename extensions for each purpose, please + * follow the documentation on [creating a + * File](https://platform.openai.com/docs/api-reference/files/create). + */ + readonly "createUpload": ( + options: typeof CreateUploadRequest.Encoded + ) => Effect.Effect + /** + * Cancels the Upload. No Parts may be added after an Upload is cancelled. + */ + readonly "cancelUpload": ( + uploadId: string + ) => Effect.Effect + /** + * Completes the [Upload](https://platform.openai.com/docs/api-reference/uploads/object). + * + * Within the returned Upload object, there is a nested [File](https://platform.openai.com/docs/api-reference/files/object) object that is ready to use in the rest of the platform. + * + * You can specify the order of the Parts by passing in an ordered list of the Part IDs. + * + * The number of bytes uploaded upon completion must match the number of bytes initially specified when creating the Upload object. No Parts may be added after an Upload is completed. + */ + readonly "completeUpload": ( + uploadId: string, + options: typeof CompleteUploadRequest.Encoded + ) => Effect.Effect + /** + * Adds a [Part](https://platform.openai.com/docs/api-reference/uploads/part-object) to an [Upload](https://platform.openai.com/docs/api-reference/uploads/object) object. A Part represents a chunk of bytes from the file you are trying to upload. + * + * Each Part can be at most 64 MB, and you can add Parts until you hit the Upload maximum of 8 GB. + * + * It is possible to add multiple Parts in parallel. You can decide the intended order of the Parts when you [complete the Upload](https://platform.openai.com/docs/api-reference/uploads/complete). + */ + readonly "addUploadPart": ( + uploadId: string, + options: typeof AddUploadPartRequest.Encoded + ) => Effect.Effect + /** + * Returns a list of vector stores. + */ + readonly "listVectorStores": ( + options?: typeof ListVectorStoresParams.Encoded | undefined + ) => Effect.Effect + /** + * Create a vector store. + */ + readonly "createVectorStore": ( + options: typeof CreateVectorStoreRequest.Encoded + ) => Effect.Effect + /** + * Retrieves a vector store. + */ + readonly "getVectorStore": ( + vectorStoreId: string + ) => Effect.Effect + /** + * Modifies a vector store. + */ + readonly "modifyVectorStore": ( + vectorStoreId: string, + options: typeof UpdateVectorStoreRequest.Encoded + ) => Effect.Effect + /** + * Delete a vector store. + */ + readonly "deleteVectorStore": ( + vectorStoreId: string + ) => Effect.Effect + /** + * Create a vector store file batch. + */ + readonly "createVectorStoreFileBatch": ( + vectorStoreId: string, + options: typeof CreateVectorStoreFileBatchRequest.Encoded + ) => Effect.Effect + /** + * Retrieves a vector store file batch. + */ + readonly "getVectorStoreFileBatch": ( + vectorStoreId: string, + batchId: string + ) => Effect.Effect + /** + * Cancel a vector store file batch. This attempts to cancel the processing of files in this batch as soon as possible. + */ + readonly "cancelVectorStoreFileBatch": ( + vectorStoreId: string, + batchId: string + ) => Effect.Effect + /** + * Returns a list of vector store files in a batch. + */ + readonly "listFilesInVectorStoreBatch": ( + vectorStoreId: string, + batchId: string, + options?: typeof ListFilesInVectorStoreBatchParams.Encoded | undefined + ) => Effect.Effect + /** + * Returns a list of vector store files. + */ + readonly "listVectorStoreFiles": ( + vectorStoreId: string, + options?: typeof ListVectorStoreFilesParams.Encoded | undefined + ) => Effect.Effect + /** + * Create a vector store file by attaching a [File](https://platform.openai.com/docs/api-reference/files) to a [vector store](https://platform.openai.com/docs/api-reference/vector-stores/object). + */ + readonly "createVectorStoreFile": ( + vectorStoreId: string, + options: typeof CreateVectorStoreFileRequest.Encoded + ) => Effect.Effect + /** + * Retrieves a vector store file. + */ + readonly "getVectorStoreFile": ( + vectorStoreId: string, + fileId: string + ) => Effect.Effect + /** + * Update attributes on a vector store file. + */ + readonly "updateVectorStoreFileAttributes": ( + vectorStoreId: string, + fileId: string, + options: typeof UpdateVectorStoreFileAttributesRequest.Encoded + ) => Effect.Effect + /** + * Delete a vector store file. This will remove the file from the vector store but the file itself will not be deleted. To delete the file, use the [delete file](https://platform.openai.com/docs/api-reference/files/delete) endpoint. + */ + readonly "deleteVectorStoreFile": ( + vectorStoreId: string, + fileId: string + ) => Effect.Effect + /** + * Retrieve the parsed contents of a vector store file. + */ + readonly "retrieveVectorStoreFileContent": ( + vectorStoreId: string, + fileId: string + ) => Effect.Effect + /** + * Search a vector store for relevant chunks based on a query and file attributes filter. + */ + readonly "searchVectorStore": ( + vectorStoreId: string, + options: typeof VectorStoreSearchRequest.Encoded + ) => Effect.Effect + /** + * Create a conversation. + */ + readonly "createConversation": ( + options: typeof CreateConversationBody.Encoded + ) => Effect.Effect + /** + * Get a conversation + */ + readonly "getConversation": ( + conversationId: string + ) => Effect.Effect + /** + * Update a conversation + */ + readonly "updateConversation": ( + conversationId: string, + options: typeof UpdateConversationBody.Encoded + ) => Effect.Effect + /** + * Delete a conversation. Items in the conversation will not be deleted. + */ + readonly "deleteConversation": ( + conversationId: string + ) => Effect.Effect + /** + * List videos + */ + readonly "ListVideos": ( + options?: typeof ListVideosParams.Encoded | undefined + ) => Effect.Effect + /** + * Create a video + */ + readonly "createVideo": ( + options: typeof CreateVideoBody.Encoded + ) => Effect.Effect + /** + * Retrieve a video + */ + readonly "GetVideo": ( + videoId: string + ) => Effect.Effect + /** + * Delete a video + */ + readonly "DeleteVideo": ( + videoId: string + ) => Effect.Effect + /** + * Download video content + */ + readonly "RetrieveVideoContent": ( + videoId: string, + options?: typeof RetrieveVideoContentParams.Encoded | undefined + ) => Effect.Effect + /** + * Create a video remix + */ + readonly "CreateVideoRemix": ( + videoId: string, + options: typeof CreateVideoRemixBody.Encoded + ) => Effect.Effect + /** + * Get input token counts + */ + readonly "Getinputtokencounts": ( + options: typeof TokenCountsBody.Encoded + ) => Effect.Effect + /** + * Cancel a ChatKit session + */ + readonly "CancelChatSessionMethod": ( + sessionId: string + ) => Effect.Effect + /** + * Create a ChatKit session + */ + readonly "CreateChatSessionMethod": ( + options: typeof CreateChatSessionBody.Encoded + ) => Effect.Effect + /** + * List ChatKit thread items + */ + readonly "ListThreadItemsMethod": ( + threadId: string, + options?: typeof ListThreadItemsMethodParams.Encoded | undefined + ) => Effect.Effect + /** + * Retrieve a ChatKit thread + */ + readonly "GetThreadMethod": ( + threadId: string + ) => Effect.Effect + /** + * Delete a ChatKit thread + */ + readonly "DeleteThreadMethod": ( + threadId: string + ) => Effect.Effect + /** + * List ChatKit threads + */ + readonly "ListThreadsMethod": ( + options?: typeof ListThreadsMethodParams.Encoded | undefined + ) => Effect.Effect +} + +export interface ClientError { + readonly _tag: Tag + readonly request: HttpClientRequest.HttpClientRequest + readonly response: HttpClientResponse.HttpClientResponse + readonly cause: E +} + +class ClientErrorImpl extends Data.Error<{ + _tag: string + cause: any + request: HttpClientRequest.HttpClientRequest + response: HttpClientResponse.HttpClientResponse +}> {} + +export const ClientError = ( + tag: Tag, + cause: E, + response: HttpClientResponse.HttpClientResponse +): ClientError => + new ClientErrorImpl({ + _tag: tag, + cause, + response, + request: response.request + }) as any diff --git a/repos/effect/packages/ai/openai/src/OpenAiClient.ts b/repos/effect/packages/ai/openai/src/OpenAiClient.ts new file mode 100644 index 0000000..f9c74d3 --- /dev/null +++ b/repos/effect/packages/ai/openai/src/OpenAiClient.ts @@ -0,0 +1,1925 @@ +/** + * @since 1.0.0 + */ +import * as AiError from "@effect/ai/AiError" +import * as Sse from "@effect/experimental/Sse" +import * as Headers from "@effect/platform/Headers" +import * as HttpBody from "@effect/platform/HttpBody" +import * as HttpClient from "@effect/platform/HttpClient" +import * as HttpClientRequest from "@effect/platform/HttpClientRequest" +import * as Arr from "effect/Array" +import * as Config from "effect/Config" +import type { ConfigError } from "effect/ConfigError" +import * as Context from "effect/Context" +import * as Effect from "effect/Effect" +import { identity } from "effect/Function" +import * as Layer from "effect/Layer" +import * as Redacted from "effect/Redacted" +import * as Schema from "effect/Schema" +import type * as Scope from "effect/Scope" +import * as Stream from "effect/Stream" +import * as Generated from "./Generated.js" +import { OpenAiConfig } from "./OpenAiConfig.js" + +/** + * @since 1.0.0 + * @category Context + */ +export class OpenAiClient extends Context.Tag( + "@effect/ai-openai/OpenAiClient" +)() {} + +/** + * @since 1.0.0 + * @category Models + */ +export interface Service { + readonly client: Generated.Client + + readonly streamRequest: ( + request: HttpClientRequest.HttpClientRequest, + schema: Schema.Schema + ) => Stream.Stream + + readonly createResponse: ( + options: typeof Generated.CreateResponse.Encoded + ) => Effect.Effect + + readonly createResponseStream: ( + options: Omit + ) => Stream.Stream + + readonly createEmbedding: ( + options: typeof Generated.CreateEmbeddingRequest.Encoded + ) => Effect.Effect +} + +/** + * @since 1.0.0 + * @category Models + */ +export type StreamCompletionRequest = Omit + +/** + * @since 1.0.0 + * @category Constructors + */ +export const make = (options: { + /** + * The API key to use to communicate with the OpenAi API. + */ + readonly apiKey?: Redacted.Redacted | undefined + /** + * The URL to use to communicate with the OpenAi API. + */ + readonly apiUrl?: string | undefined + /** + * The OpenAi organization identifier to use when communicating with the + * OpenAi API. + */ + readonly organizationId?: Redacted.Redacted | undefined + /** + * The OpenAi project identifier to use when communicating with the OpenAi + * API. + */ + readonly projectId?: Redacted.Redacted | undefined + /** + * A method which can be used to transform the underlying `HttpClient` which + * will be used to communicate with the OpenAi API. + */ + readonly transformClient?: ((client: HttpClient.HttpClient) => HttpClient.HttpClient) | undefined +}): Effect.Effect => + Effect.gen(function*() { + const organizationHeader = "OpenAI-Organization" + const projectHeader = "OpenAI-Project" + + yield* Effect.locallyScopedWith(Headers.currentRedactedNames, Arr.appendAll([organizationHeader, projectHeader])) + + const httpClient = (yield* HttpClient.HttpClient).pipe( + HttpClient.mapRequest((request) => + request.pipe( + HttpClientRequest.prependUrl(options.apiUrl ?? "https://api.openai.com/v1"), + options.apiKey ? HttpClientRequest.bearerToken(options.apiKey) : identity, + options.organizationId !== undefined + ? HttpClientRequest.setHeader(organizationHeader, Redacted.value(options.organizationId)) + : identity, + options.projectId !== undefined + ? HttpClientRequest.setHeader(projectHeader, Redacted.value(options.projectId)) + : identity, + HttpClientRequest.acceptJson + ) + ), + options.transformClient ? options.transformClient : identity + ) + + const httpClientOk = HttpClient.filterStatusOk(httpClient) + + const client = Generated.make(httpClient, { + transformClient: (client) => + OpenAiConfig.getOrUndefined.pipe( + Effect.map((config) => config?.transformClient ? config.transformClient(client) : client) + ) + }) + + const streamRequest = ( + request: HttpClientRequest.HttpClientRequest, + schema: Schema.Schema + ): Stream.Stream => { + const decodeEvent = Schema.decode(Schema.parseJson(schema)) + return httpClientOk.execute(request).pipe( + Effect.map((r) => r.stream), + Stream.unwrapScoped, + Stream.decodeText(), + Stream.pipeThroughChannel(Sse.makeChannel()), + Stream.mapEffect((event) => decodeEvent(event.data)), + Stream.catchTags({ + RequestError: (error) => + AiError.HttpRequestError.fromRequestError({ + module: "OpenAiClient", + method: "streamRequest", + error + }), + ResponseError: (error) => + AiError.HttpResponseError.fromResponseError({ + module: "OpenAiClient", + method: "streamRequest", + error + }), + ParseError: (error) => + AiError.MalformedOutput.fromParseError({ + module: "OpenAiClient", + method: "streamRequest", + error + }) + }) + ) + } + + const createResponse = ( + options: typeof Generated.CreateResponse.Encoded + ): Effect.Effect => + client.createResponse(options).pipe( + Effect.catchTags({ + RequestError: (error) => + AiError.HttpRequestError.fromRequestError({ + module: "OpenAiClient", + method: "createResponse", + error + }), + ResponseError: (error) => + AiError.HttpResponseError.fromResponseError({ + module: "OpenAiClient", + method: "createResponse", + error + }), + ParseError: (error) => + AiError.MalformedOutput.fromParseError({ + module: "OpenAiClient", + method: "createResponse", + error + }) + }) + ) + + const createResponseStream = ( + options: Omit + ): Stream.Stream => { + const request = HttpClientRequest.post("/responses", { + body: HttpBody.unsafeJson({ ...options, stream: true }) + }) + return streamRequest(request, ResponseStreamEvent).pipe( + Stream.takeUntil((event) => event.type === "response.completed" || event.type === "response.incomplete") + ) + } + + const createEmbedding = ( + options: typeof Generated.CreateEmbeddingRequest.Encoded + ): Effect.Effect => + client.createEmbedding(options).pipe( + Effect.catchTags({ + RequestError: (error) => + AiError.HttpRequestError.fromRequestError({ + module: "OpenAiClient", + method: "createResponse", + error + }), + ResponseError: (error) => + AiError.HttpResponseError.fromResponseError({ + module: "OpenAiClient", + method: "createResponse", + error + }), + ParseError: (error) => + AiError.MalformedOutput.fromParseError({ + module: "OpenAiClient", + method: "createResponse", + error + }) + }) + ) + + return OpenAiClient.of({ + client, + streamRequest, + createResponse, + createResponseStream, + createEmbedding + }) + }) + +/** + * @since 1.0.0 + * @category Layers + */ +export const layer = (options: { + readonly apiKey?: Redacted.Redacted | undefined + readonly apiUrl?: string | undefined + readonly organizationId?: Redacted.Redacted | undefined + readonly projectId?: Redacted.Redacted | undefined + readonly transformClient?: (client: HttpClient.HttpClient) => HttpClient.HttpClient +}): Layer.Layer => Layer.scoped(OpenAiClient, make(options)) + +/** + * @since 1.0.0 + * @category Layers + */ +export const layerConfig = ( + options: { + readonly apiKey?: Config.Config | undefined + readonly apiUrl?: Config.Config | undefined + readonly organizationId?: Config.Config | undefined + readonly projectId?: Config.Config | undefined + readonly transformClient?: (client: HttpClient.HttpClient) => HttpClient.HttpClient + } +): Layer.Layer => { + const { transformClient, ...configs } = options + return Config.all(configs).pipe( + Effect.flatMap((configs) => make({ ...configs, transformClient })), + Layer.scoped(OpenAiClient) + ) +} + +// ============================================================================= +// Response Stream Schema +// ============================================================================= + +/** + * An event that is emitted when a response is created. + * + * @since 1.0.0 + * @category Schemas + */ +export class ResponseCreatedEvent extends Schema.Class( + "@effect/ai-openai/ResponseCreatedEvent" +)({ + /** + * The type of the event. Always `"response.created"`. + */ + type: Schema.Literal("response.created"), + /** + * The sequence number for this event. + */ + sequence_number: Schema.Int, + /** + * The response that was created. + */ + response: Generated.Response +}) {} + +/** + * Emitted when a response is queued and waiting to be processed. + * + * @since 1.0.0 + * @category Schemas + */ +export class ResponseQueuedEvent extends Schema.Class( + "@effect/ai-openai/ResponseQueuedEvent" +)({ + /** + * The type of the event. Always `"response.queued"`. + */ + type: Schema.Literal("response.queued"), + /** + * The sequence number for this event. + */ + sequence_number: Schema.Int, + /** + * The full response object that is queued. + */ + response: Generated.Response +}) {} + +/** + * Emitted when the response is in progress. + * + * @since 1.0.0 + * @category Schemas + */ +export class ResponseInProgressEvent extends Schema.Class( + "@effect/ai-openai/ResponseInProgressEvent" +)({ + /** + * The type of the event. Always `"response.in_progress"`. + */ + type: Schema.Literal("response.in_progress"), + /** + * The sequence number for this event. + */ + sequence_number: Schema.Int, + /** + * The response that is in progress. + */ + response: Generated.Response +}) {} + +/** + * Emitted when the model response is complete. + * + * @since 1.0.0 + * @category Schemas + */ +export class ResponseCompletedEvent extends Schema.Class( + "@effect/ai-openai/ResponseCompletedEvent" +)({ + /** + * The type of the event. Always `"response.completed"`. + */ + type: Schema.Literal("response.completed"), + /** + * The sequence number for this event. + */ + sequence_number: Schema.Int, + /** + * Properties of the completed response. + */ + response: Generated.Response +}) {} + +/** + * An event that is emitted when a response finishes as incomplete. + * + * @since 1.0.0 + * @category Schemas + */ +export class ResponseIncompleteEvent extends Schema.Class( + "@effect/ai-openai/ResponseIncompleteEvent" +)({ + /** + * The type of the event. Always `"response.incomplete"`. + */ + type: Schema.Literal("response.incomplete"), + /** + * The sequence number for this event. + */ + sequence_number: Schema.Int, + /** + * The response that was incomplete. + */ + response: Generated.Response +}) {} + +/** + * An event that is emitted when a response fails. + * + * @since 1.0.0 + * @category Schemas + */ +export class ResponseFailedEvent extends Schema.Class( + "@effect/ai-openai/ResponseFailedEvent" +)({ + /** + * The type of the event. Always `"response.failed"`. + */ + type: Schema.Literal("response.failed"), + /** + * The sequence number for this event. + */ + sequence_number: Schema.Int, + /** + * The response that failed. + */ + response: Generated.Response +}) {} + +const WebSearchToolCallForAddEvent = Schema.asSchema( + Generated.WebSearchToolCall.pipe( + Schema.omit("action") + ) +) + +const AddEventOutputItem = Schema.Union( + Generated.OutputItem, + WebSearchToolCallForAddEvent +) + +/** + * Emitted when a new output item is added. + * + * @since 1.0.0 + * @category Schemas + */ +export class ResponseOutputItemAddedEvent extends Schema.Class( + "@effect/ai-openai/ResponseOutputItemAddedEvent" +)({ + /** + * The type of the event. Always `"response.output_item.added"`. + */ + type: Schema.Literal("response.output_item.added"), + /** + * The sequence number for this event. + */ + sequence_number: Schema.Int, + /** + * The index of the output item that was added. + */ + output_index: Schema.Int, + /** + * The output item that was added. + */ + item: AddEventOutputItem +}) {} + +/** + * Emitted when an output item is marked done. + * + * @since 1.0.0 + * @category Schemas + */ +export class ResponseOutputItemDoneEvent extends Schema.Class( + "@effect/ai-openai/ResponseOutputItemDoneEvent" +)({ + /** + * The type of the event. Always `"response.output_item.done"`. + */ + type: Schema.Literal("response.output_item.done"), + /** + * The sequence number for this event. + */ + sequence_number: Schema.Int, + /** + * The index of the output item that was marked done. + */ + output_index: Schema.Int, + /** + * The output item that was marked done. + */ + item: Generated.OutputItem +}) {} + +/** + * Emitted when a new content part is added. + * + * @since 1.0.0 + * @category Schemas + */ +export class ResponseContentPartAddedEvent extends Schema.Class( + "@effect/ai-openai/ResponseContentPartAddedEvent" +)({ + /** + * The type of the event. Always `"response.content_part.added"`. + */ + type: Schema.Literal("response.content_part.added"), + /** + * The sequence number for this event. + */ + sequence_number: Schema.Int, + /** + * The index of the output item that the content part was added to. + */ + output_index: Schema.Int, + /** + * The index of the content part that was added. + */ + content_index: Schema.Int, + /** + * The ID of the output item that the content part was added to. + */ + item_id: Schema.String, + /** + * The content part that was added. + */ + part: Schema.Union( + Generated.OutputTextContent, + Generated.RefusalContent, + Generated.ReasoningTextContent + ) +}) {} + +/** + * Emitted when a content part is done. + * + * @since 1.0.0 + * @category Schemas + */ +export class ResponseContentPartDoneEvent extends Schema.Class( + "@effect/ai-openai/ResponseContentPartDoneEvent" +)({ + /** + * The type of the event. Always `"response.content_part.done"`. + */ + type: Schema.Literal("response.content_part.done"), + /** + * The sequence number for this event. + */ + sequence_number: Schema.Int, + /** + * The index of the output item that the content part was added to. + */ + output_index: Schema.Int, + /** + * The index of the content part that is done. + */ + content_index: Schema.Int, + /** + * The ID of the output item that the content part was added to. + */ + item_id: Schema.String, + /** + * The content part that was added. + */ + part: Schema.Union( + Generated.OutputTextContent, + Generated.RefusalContent, + Generated.ReasoningTextContent + ) +}) {} + +/** + * @since 1.0.0 + * @category Schemas + */ +export class LogProbs extends Schema.Class( + "@effect/ai-openai/LogProbs" +)({ + /** + * The log probability of this token. + */ + logprob: Schema.Number, + /** + * A possible text token. + */ + token: Schema.String, + /** + * The log probability of the top 20 most likely tokens. + */ + top_logprobs: Schema.Array(Schema.Struct({ + /** + * The log probability of this token. + */ + logprob: Schema.Number, + /** + * A possible text token. + */ + token: Schema.String + })) +}) {} + +/** + * Emitted when there is an additional text delta. + * + * @since 1.0.0 + * @category Schemas + */ +export class ResponseOutputTextDeltaEvent extends Schema.Class( + "@effect/ai-openai/ResponseOutputTextDeltaEvent" +)({ + /** + * The type of the event. Always `"response.output_text.delta"`. + */ + type: Schema.Literal("response.output_text.delta"), + /** + * The sequence number for this event. + */ + sequence_number: Schema.Int, + /** + * The index of the output item that the text delta was added to. + */ + output_index: Schema.Int, + /** + * The index of the content part that the text delta was added to. + */ + content_index: Schema.Int, + /** + * The ID of the output item that the text delta was added to. + */ + item_id: Schema.String, + /** + * The text delta that was added. + */ + delta: Schema.String, + /** + * The log probabilities of the tokens in the delta. + */ + logprobs: Schema.optional(Schema.NullOr(Schema.Array(LogProbs))) +}) {} + +/** + * Emitted when text content is finalized. + * + * @since 1.0.0 + * @category Schemas + */ +export class ResponseOutputTextDoneEvent extends Schema.Class( + "@effect/ai-openai/ResponseOutputTextDoneEvent" +)({ + /** + * The type of the event. Always `"response.output_text.done"`. + */ + type: Schema.Literal("response.output_text.done"), + /** + * The sequence number for this event. + */ + sequence_number: Schema.Int, + /** + * The index of the output item that the text content is finalized. + */ + output_index: Schema.Int, + /** + * The index of the content part that the text content is finalized. + */ + content_index: Schema.Int, + /** + * The ID of the output item that the text content is finalized. + */ + item_id: Schema.String, + /** + * The text content that is finalized. + */ + text: Schema.String, + /** + * The log probabilities of the tokens in the delta. + */ + logprobs: Schema.optional(Schema.NullOr(Schema.Array(LogProbs))) +}) {} + +/** + * Emitted when an annotation is added to output text content. + * + * @since 1.0.0 + * @category Schemas + */ +export class ResponseOutputTextAnnotationAddedEvent extends Schema.Class( + "@effect/ai-openai/ResponseOutputTextAnnotationAddedEvent" +)({ + /** + * The type of the event. Always `"response.output_text.annotation.added"`. + */ + type: Schema.Literal("response.output_text.annotation.added"), + /** + * The sequence number for this event. + */ + sequence_number: Schema.Int, + /** + * The index of the output item in the response's output array. + */ + output_index: Schema.Int, + /** + * The index of the content part within the output item. + */ + content_index: Schema.Int, + /** + * The index of the annotation within the content part. + */ + annotation_index: Schema.Int, + /** + * The unique identifier of the item to which the annotation is being added. + */ + item_id: Schema.String, + /** + * The annotation object being added. (See annotation schema for details.) + */ + annotation: Generated.Annotation +}) {} + +/** + * Emitted when there is a partial refusal text. + * + * @since 1.0.0 + * @category Schemas + */ +export class ResponseRefusalDeltaEvent extends Schema.Class( + "@effect/ai-openai/ResponseRefusalDeltaEvent" +)({ + /** + * The type of the event. Always `"response.refusal.delta"`. + */ + type: Schema.Literal("response.refusal.delta"), + /** + * The sequence number for this event. + */ + sequence_number: Schema.Int, + /** + * The index of the output item that the refusal text is added to. + */ + output_index: Schema.Int, + /** + * The index of the content part that the refusal text is added to. + */ + content_index: Schema.Int, + /** + * The ID of the output item that the refusal text is added to. + */ + item_id: Schema.String, + /** + * The refusal text that is added. + */ + delta: Schema.String +}) {} + +/** + * Emitted when refusal text is finalized. + * + * @since 1.0.0 + * @category Schemas + */ +export class ResponseRefusalDoneEvent extends Schema.Class( + "@effect/ai-openai/ResponseRefusalDoneEvent" +)({ + /** + * The type of the event. Always `"response.refusal.done"`. + */ + type: Schema.Literal("response.refusal.done"), + /** + * The sequence number for this event. + */ + sequence_number: Schema.Int, + /** + * The index of the output item that the refusal text is finalized. + */ + output_index: Schema.Int, + /** + * The index of the content part that the refusal text is finalized. + */ + content_index: Schema.Int, + /** + * The index of the output item that the refusal text is added to. + */ + item_id: Schema.String, + /** + * The refusal text that is finalized. + */ + refusal: Schema.String +}) {} + +/** + * Emitted when there is a partial function-call arguments delta. + * + * @since 1.0.0 + * @category Schemas + */ +export class ResponseFunctionCallArgumentsDeltaEvent extends Schema.Class( + "@effect/ai-openai/ResponseFunctionCallArgumentsDeltaEvent" +)({ + /** + * The type of the event. Always `"response.function_call_arguments.delta"`. + */ + type: Schema.Literal("response.function_call_arguments.delta"), + /** + * The sequence number for this event. + */ + sequence_number: Schema.Int, + /** + * The index of the output item that the function-call arguments delta is added to. + */ + output_index: Schema.Int, + /** + * The ID of the output item that the function-call arguments delta is added to. + */ + item_id: Schema.String, + /** + * The function-call arguments delta that is added. + */ + delta: Schema.String +}) {} + +/** + * Emitted when function-call arguments are finalized. + * + * @since 1.0.0 + * @category Schemas + */ +export class ResponseFunctionCallArgumentsDoneEvent extends Schema.Class( + "@effect/ai-openai/ResponseFunctionCallArgumentsDoneEvent" +)({ + /** + * The type of the event. Always `"response.function_call_arguments.done"`. + */ + type: Schema.Literal("response.function_call_arguments.done"), + /** + * The sequence number for this event. + */ + sequence_number: Schema.Int, + /** + * The index of the output item. + */ + output_index: Schema.Int, + /** + * The ID of the item. + */ + item_id: Schema.String, + /** + * The function-call arguments. + */ + arguments: Schema.String +}) {} + +/** + * Emitted when a file search call is initiated. + * + * @since 1.0.0 + * @category Schemas + */ +export class ResponseFileSearchCallInProgressEvent extends Schema.Class( + "@effect/ai-openai/ResponseFileSearchCallInProgressEvent" +)({ + /** + * The type of the event. Always `"response.file_search_call.in_progress"`. + */ + type: Schema.Literal("response.file_search_call.in_progress"), + /** + * The sequence number for this event. + */ + sequence_number: Schema.Int, + /** + * The index of the output item that the file search call is initiated. + */ + output_index: Schema.Int, + /** + * The ID of the output item that the file search call is initiated. + */ + item_id: Schema.String +}) {} + +/** + * Emitted when a file search is currently searching. + * + * @since 1.0.0 + * @category Schemas + */ +export class ResponseFileSearchCallSearchingEvent extends Schema.Class( + "@effect/ai-openai/ResponseFileSearchCallSearchingEvent" +)({ + /** + * The type of the event. Always `"response.file_search_call.searching"`. + */ + type: Schema.Literal("response.file_search_call.searching"), + /** + * The sequence number for this event. + */ + sequence_number: Schema.Int, + /** + * The index of the output item that the file search call is searching. + */ + output_index: Schema.Int, + /** + * The ID of the output item that the file search call is initiated. + */ + item_id: Schema.String +}) {} + +/** + * Emitted when a file search call is completed (results found). + * + * @since 1.0.0 + * @category Schemas + */ +export class ResponseFileSearchCallCompletedEvent extends Schema.Class( + "@effect/ai-openai/ResponseFileSearchCallCompletedEvent" +)({ + /** + * The type of the event. Always `"response.file_search_call.completed"`. + */ + type: Schema.Literal("response.file_search_call.completed"), + /** + * The sequence number for this event. + */ + sequence_number: Schema.Int, + /** + * The index of the output item that the file search call is initiated. + */ + output_index: Schema.Int, + /** + * The ID of the output item that the file search call is initiated. + */ + item_id: Schema.String +}) {} + +/** + * Emitted when a web search call is initiated. + * + * @since 1.0.0 + * @category Schemas + */ +export class ResponseWebSearchCallInProgressEvent extends Schema.Class( + "@effect/ai-openai/ResponseWebSearchCallInProgressEvent" +)({ + /** + * The type of the event. Always `"response.web_search_call.in_progress"`. + */ + type: Schema.Literal("response.web_search_call.in_progress"), + /** + * The sequence number for this event. + */ + sequence_number: Schema.Int, + /** + * The index of the output item that the web search call is associated with. + */ + output_index: Schema.Int, + /** + * Unique ID for the output item associated with the web search call. + */ + item_id: Schema.String +}) {} + +/** + * Emitted when a web search call is executing. + * + * @since 1.0.0 + * @category Schemas + */ +export class ResponseWebSearchCallSearchingEvent extends Schema.Class( + "@effect/ai-openai/ResponseWebSearchCallSearchingEvent" +)({ + /** + * The type of the event. Always `"response.web_search_call.searching"`. + */ + type: Schema.Literal("response.web_search_call.searching"), + /** + * The sequence number for this event. + */ + sequence_number: Schema.Int, + /** + * The index of the output item that the web search call is associated with. + */ + output_index: Schema.Int, + /** + * Unique ID for the output item associated with the web search call. + */ + item_id: Schema.String +}) {} + +/** + * Emitted when a web search call is completed. + * + * @since 1.0.0 + * @category Schemas + */ +export class ResponseWebSearchCallCompletedEvent extends Schema.Class( + "@effect/ai-openai/ResponseWebSearchCallCompletedEvent" +)({ + /** + * The type of the event. Always `"response.web_search_call.completed"`. + */ + type: Schema.Literal("response.web_search_call.completed"), + /** + * The sequence number for this event. + */ + sequence_number: Schema.Int, + /** + * The index of the output item that the web search call is associated with. + */ + output_index: Schema.Int, + /** + * Unique ID for the output item associated with the web search call. + */ + item_id: Schema.String +}) {} + +/** + * @since 1.0.0 + * @category Schemas + */ +export class SummaryPart extends Schema.Class( + "@effect/ai-openai/SummaryPart" +)({ + /** + * The type of the summary part. Always `"summary_text"`. + */ + type: Schema.Literal("summary_text"), + /** + * The text of the summary part. + */ + text: Schema.String +}) {} + +/** + * Emitted when a reasoning summary part is completed. + * + * @since 1.0.0 + * @category Schemas + */ +export class ResponseReasoningSummaryPartAddedEvent extends Schema.Class( + "@effect/ai-openai/ResponseReasoningSummaryPartAddedEvent" +)({ + /** + * The type of the event. Always `"response.reasoning_summary_part.added"`. + */ + type: Schema.Literal("response.reasoning_summary_part.added"), + /** + * The sequence number for this event. + */ + sequence_number: Schema.Int, + /** + * The index of the summary part within the reasoning summary. + */ + summary_index: Schema.Int, + /** + * The index of the output item this summary part is associated with. + */ + output_index: Schema.Int, + /** + * The ID of the item this summary part is associated with. + */ + item_id: Schema.String, + /** + * The summary part that was added. + */ + part: SummaryPart +}) {} + +/** + * Emitted when a new reasoning summary part is added. + * + * @since 1.0.0 + * @category Schemas + */ +export class ResponseReasoningSummaryPartDoneEvent extends Schema.Class( + "@effect/ai-openai/ResponseReasoningSummaryPartDoneEvent" +)({ + /** + * The type of the event. Always `"response.reasoning_summary_part.done"`. + */ + type: Schema.Literal("response.reasoning_summary_part.done"), + /** + * The sequence number for this event. + */ + sequence_number: Schema.Int, + /** + * The index of the summary part within the reasoning summary. + */ + summary_index: Schema.Int, + /** + * The index of the output item this summary part is associated with. + */ + output_index: Schema.Int, + /** + * The ID of the item this summary part is associated with. + */ + item_id: Schema.String, + /** + * The completed summary part. + */ + part: SummaryPart +}) {} + +/** + * Emitted when a delta is added to a reasoning summary text. + * + * @since 1.0.0 + * @category Schemas + */ +export class ResponseReasoningSummaryTextDeltaEvent extends Schema.Class( + "@effect/ai-openai/ResponseReasoningSummaryTextDeltaEvent" +)({ + /** + * The type of the event. Always `"response.reasoning_summary_text.delta"`. + */ + type: Schema.Literal("response.reasoning_summary_text.delta"), + /** + * The sequence number for this event. + */ + sequence_number: Schema.Int, + /** + * The index of the summary part within the reasoning summary. + */ + summary_index: Schema.Int, + /** + * The index of the output item this summary text delta is associated with. + */ + output_index: Schema.Int, + /** + * The ID of the item this summary text delta is associated with. + */ + item_id: Schema.String, + /** + * The text delta that was added to the summary. + */ + delta: Schema.String +}) {} + +/** + * Emitted when a reasoning summary text is completed. + * + * @since 1.0.0 + * @category Schemas + */ +export class ResponseReasoningSummaryTextDoneEvent extends Schema.Class( + "@effect/ai-openai/ResponseReasoningSummaryTextDoneEvent" +)({ + /** + * The type of the event. Always `"response.reasoning_summary_text.done"`. + */ + type: Schema.Literal("response.reasoning_summary_text.done"), + /** + * The sequence number for this event. + */ + sequence_number: Schema.Int, + /** + * The index of the summary part within the reasoning summary. + */ + summary_index: Schema.Int, + /** + * The index of the output item this summary text is associated with. + */ + output_index: Schema.Int, + /** + * The ID of the item this summary text is associated with. + */ + item_id: Schema.String, + /** + * The full text of the completed reasoning summary. + */ + text: Schema.String +}) {} + +/** + * Emitted when a delta is added to a reasoning text. + * + * @since 1.0.0 + * @category Schemas + */ +export class ResponseReasoningTextDeltaEvent extends Schema.Class( + "@effect/ai-openai/ResponseReasoningTextDeltaEvent" +)({ + /** + * The type of the event. Always `"response.reasoning_text.delta"`. + */ + type: Schema.Literal("response.reasoning_text.delta"), + /** + * The sequence number for this event. + */ + sequence_number: Schema.Int, + /** + * The index of the reasoning content part this delta is associated with. + */ + content_index: Schema.Int, + /** + * The index of the output item this reasoning text delta is associated with. + */ + output_index: Schema.Int, + /** + * The ID of the item this reasoning text delta is associated with. + */ + item_id: Schema.String, + /** + * The text delta that was added to the reasoning content. + */ + delta: Schema.String +}) {} + +/** + * Emitted when a reasoning text is completed. + * + * @since 1.0.0 + * @category Schemas + */ +export class ResponseReasoningTextDoneEvent extends Schema.Class( + "@effect/ai-openai/ResponseReasoningPartDoneEvent" +)({ + /** + * The type of the event. Always `"response.reasoning_text.done"`. + */ + type: Schema.Literal("response.reasoning_text.done"), + /** + * The sequence number for this event. + */ + sequence_number: Schema.Int, + /** + * The index of the reasoning content part. + */ + content_index: Schema.Int, + /** + * The index of the output item this reasoning text is associated with. + */ + output_index: Schema.Int, + /** + * The ID of the item this reasoning text is associated with. + */ + item_id: Schema.String, + /** + * The full text of the completed reasoning content. + */ + text: Schema.String +}) {} + +/** + * Emitted when an image generation tool call is in progress. + * + * @since 1.0.0 + * @category Schemas + */ +export class ResponseImageGenerationCallInProgressEvent + extends Schema.Class( + "@effect/ai-openai/ResponseImageGenerationCallInProgressEvent" + )({ + /** + * The type of the event. Always `"response.image_generation_call.in_progress"`. + */ + type: Schema.Literal("response.image_generation_call.in_progress"), + /** + * The sequence number for this event. + */ + sequence_number: Schema.Int, + /** + * The index of the output item in the response's output array. + */ + output_index: Schema.Int, + /** + * The unique identifier of the image generation item being processed. + */ + item_id: Schema.String + }) +{} + +/** + * Emitted when an image generation tool call is actively generating an image + * (intermediate state). + * + * @since 1.0.0 + * @category Schemas + */ +export class ResponseImageGenerationCallGeneratingEvent + extends Schema.Class( + "@effect/ai-openai/ResponseImageGenerationCallGeneratingEvent" + )({ + /** + * The type of the event. Always `"response.image_generation_call.generating"`. + */ + type: Schema.Literal("response.image_generation_call.generating"), + /** + * The sequence number for this event. + */ + sequence_number: Schema.Int, + /** + * The index of the output item in the response's output array. + */ + output_index: Schema.Int, + /** + * The unique identifier of the image generation item being processed. + */ + item_id: Schema.String + }) +{} + +/** + * Emitted when a partial image is available during image generation streaming. + * + * @since 1.0.0 + * @category Schemas + */ +export class ResponseImageGenerationCallPartialImageEvent + extends Schema.Class( + "@effect/ai-openai/ResponseImageGenerationCallPartialImageEvent" + )({ + /** + * The type of the event. Always `"response.image_generation_call.partial_image"`. + */ + type: Schema.Literal("response.image_generation_call.partial_image"), + /** + * The sequence number for this event. + */ + sequence_number: Schema.Int, + /** + * The index of the output item in the response's output array. + */ + output_index: Schema.Int, + /** + * The unique identifier of the image generation item being processed. + */ + item_id: Schema.String, + /** + * `0`-based index for the partial image (backend is `1`-based, but this is + * `0`-based for the user). + */ + partial_image_index: Schema.Int, + /** + * Base64-encoded partial image data, suitable for rendering as an image. + */ + partial_image_b64: Schema.String + }) +{} + +/** + * Emitted when an image generation tool call has completed and the final image + * is available. + * + * @since 1.0.0 + * @category Schemas + */ +export class ResponseImageGenerationCallCompletedEvent extends Schema.Class( + "@effect/ai-openai/ResponseImageGenerationCallCompletedEvent" +)({ + /** + * The type of the event. Always `"response.image_generation_call.completed"`. + */ + type: Schema.Literal("response.image_generation_call.completed"), + /** + * The sequence number for this event. + */ + sequence_number: Schema.Int, + /** + * The index of the output item in the response's output array. + */ + output_index: Schema.Int, + /** + * The unique identifier of the image generation item being processed. + */ + item_id: Schema.String +}) {} + +/** + * Emitted when there is a delta (partial update) to the arguments of an MCP + * tool call. + * + * @since 1.0.0 + * @category Schemas + */ +export class ResponseMcpCallArgumentsDeltaEvent extends Schema.Class( + "@effect/ai-openai/ResponseMcpCallArgumentsDeltaEvent" +)({ + /** + * The type of the event. Always `"response.mcp_call_arguments.delta"`. + */ + type: Schema.Literal("response.mcp_call_arguments.delta"), + /** + * The sequence number for this event. + */ + sequence_number: Schema.Int, + /** + * The index of the output item in the response's output array. + */ + output_index: Schema.Int, + /** + * The unique identifier of the MCP tool call item being processed. + */ + item_id: Schema.String, + /** + * A JSON string containing the partial update to the arguments for the MCP + * tool call. + */ + delta: Schema.String +}) {} + +/** + * Emitted when the arguments for an MCP tool call are finalized. + * + * @since 1.0.0 + * @category Schemas + */ +export class ResponseMcpCallArgumentsDoneEvent extends Schema.Class( + "@effect/ai-openai/ResponseMcpCallArgumentsDoneEvent" +)({ + /** + * The type of the event. Always `"response.mcp_call_arguments.done"`. + */ + type: Schema.Literal("response.mcp_call_arguments.done"), + /** + * The sequence number for this event. + */ + sequence_number: Schema.Int, + /** + * The index of the output item in the response's output array. + */ + output_index: Schema.Int, + /** + * The unique identifier of the MCP tool call item being processed. + */ + item_id: Schema.String, + /** + * A JSON string containing the finalized arguments for the MCP tool call. + */ + arguments: Schema.String +}) {} + +/** + * Emitted when an MCP tool call is in progress. + * + * @since 1.0.0 + * @category Schemas + */ +export class ResponseMcpCallInProgressEvent extends Schema.Class( + "@effect/ai-openai/ResponseMcpCallInProgressEvent" +)({ + /** + * The type of the event. Always `"response.mcp_call.in_progress"`. + */ + type: Schema.Literal("response.mcp_call.in_progress"), + /** + * The sequence number for this event. + */ + sequence_number: Schema.Int, + /** + * The index of the output item in the response's output array. + */ + output_index: Schema.Int, + /** + * The unique identifier of the MCP tool call item being processed. + */ + item_id: Schema.String +}) {} + +/** + * Emitted when an MCP tool call has completed successfully. + * + * @since 1.0.0 + * @category Schemas + */ +export class ResponseMcpCallCompletedEvent extends Schema.Class( + "@effect/ai-openai/ResponseMcpCallCompletedEvent" +)({ + /** + * The type of the event. Always `"response.mcp_call.completed"`. + */ + type: Schema.Literal("response.mcp_call.completed"), + /** + * The sequence number for this event. + */ + sequence_number: Schema.Int, + /** + * The index of the output item that completed. + */ + output_index: Schema.Int, + /** + * The ID of the MCP tool call item that completed. + */ + item_id: Schema.String +}) {} + +/** + * Emitted when an MCP tool call has failed. + * + * @since 1.0.0 + * @category Schemas + */ +export class ResponseMcpCallFailedEvent extends Schema.Class( + "@effect/ai-openai/ResponseMcpCallFailedEvent" +)({ + /** + * The type of the event. Always `"response.mcp_call.failed"`. + */ + type: Schema.Literal("response.mcp_call.failed"), + /** + * The sequence number for this event. + */ + sequence_number: Schema.Int, + /** + * The index of the output item that failed. + */ + output_index: Schema.Int, + /** + * The ID of the MCP tool call item that failed. + */ + item_id: Schema.String +}) {} + +/** + * Emitted when the system is in the process of retrieving the list of available + * MCP tools. + * + * @since 1.0.0 + * @category Schemas + */ +export class ResponseMcpListToolsInProgressEvent extends Schema.Class( + "@effect/ai-openai/ResponseMcpListToolsInProgressEvent" +)({ + /** + * The type of the event. Always `"response.mcp_list_tools.in_progress"`. + */ + type: Schema.Literal("response.mcp_list_tools.in_progress"), + /** + * The sequence number for this event. + */ + sequence_number: Schema.Int, + /** + * The index of the output item that is being processed. + */ + output_index: Schema.Int, + /** + * The ID of the MCP tool call item that is being processed. + */ + item_id: Schema.String +}) {} + +/** + * Emitted when the list of available MCP tools has been successfully retrieved. + * + * @since 1.0.0 + * @category Schemas + */ +export class ResponseMcpListToolsCompletedEvent extends Schema.Class( + "@effect/ai-openai/ResponseMcpListToolsCompletedEvent" +)({ + /** + * The type of the event. Always `"response.mcp_list_tools.completed"`. + */ + type: Schema.Literal("response.mcp_list_tools.completed"), + /** + * The sequence number for this event. + */ + sequence_number: Schema.Int, + /** + * The index of the output item that was processed. + */ + output_index: Schema.Int, + /** + * The ID of the MCP tool call item that produced this output. + */ + item_id: Schema.String +}) {} + +/** + * Emitted when the attempt to list available MCP tools has failed. + * + * @since 1.0.0 + * @category Schemas + */ +export class ResponseMcpListToolsFailedEvent extends Schema.Class( + "@effect/ai-openai/ResponseMcpListToolsFailedEvent" +)({ + /** + * The type of the event. Always `"response.mcp_list_tools.failed"`. + */ + type: Schema.Literal("response.mcp_list_tools.failed"), + /** + * The sequence number for this event. + */ + sequence_number: Schema.Int, + /** + * The index of the output item that failed. + */ + output_index: Schema.Int, + /** + * The ID of the MCP tool call item that failed. + */ + item_id: Schema.String +}) {} + +/** + * Emitted when a code interpreter call is in progress. + * + * @since 1.0.0 + * @category Schemas + */ +export class ResponseCodeInterpreterCallInProgressEvent + extends Schema.Class( + "@effect/ai-openai/ResponseCodeInterpreterCallInProgressEvent" + )({ + /** + * The type of the event. Always `"response.code_interpreter_call.in_progress"`. + */ + type: Schema.Literal("response.code_interpreter_call.in_progress"), + /** + * The sequence number for this event. + */ + sequence_number: Schema.Int, + /** + * The index of the output item in the response for which the code interpreter + * call is in progress. + */ + output_index: Schema.Int, + /** + * The unique identifier of the code interpreter tool call item. + */ + item_id: Schema.String + }) +{} + +/** + * Emitted when the code interpreter is actively interpreting the code snippet. + * + * @since 1.0.0 + * @category Schemas + */ +export class ResponseCodeInterpreterCallInterpretingEvent + extends Schema.Class( + "@effect/ai-openai/ResponseCodeInterpreterCallInterpretingEvent" + )({ + /** + * The type of the event. Always `"response.code_interpreter_call.interpreting"`. + */ + type: Schema.Literal("response.code_interpreter_call.interpreting"), + /** + * The sequence number for this event. + */ + sequence_number: Schema.Int, + /** + * The index of the output item in the response for which the code + * interpreter is interpreting code. + */ + output_index: Schema.Int, + /** + * The unique identifier of the code interpreter tool call item. + */ + item_id: Schema.String + }) +{} + +/** + * Emitted when the code interpreter call is completed. + * + * @since 1.0.0 + * @category Schemas + */ +export class ResponseCodeInterpreterCallCompletedEvent extends Schema.Class( + "@effect/ai-openai/ResponseCodeInterpreterCallCompletedEvent" +)({ + /** + * The type of the event. Always `"response.code_interpreter_call.completed"`. + */ + type: Schema.Literal("response.code_interpreter_call.completed"), + /** + * The sequence number for this event. + */ + sequence_number: Schema.Int, + /** + * The index of the output item in the response for which the code interpreter + * call is completed. + */ + output_index: Schema.Int, + /** + * The unique identifier of the code interpreter tool call item. + */ + item_id: Schema.String +}) {} + +/** + * Emitted when a partial code snippet is streamed by the code interpreter. + * + * @since 1.0.0 + * @category Schemas + */ +export class ResponseCodeInterpreterCallCodeDeltaEvent extends Schema.Class( + "@effect/ai-openai/ResponseCodeInterpreterCallCodeDeltaEvent" +)({ + /** + * The type of the event. Always `"response.code_interpreter_call_code.delta"`. + */ + type: Schema.Literal("response.code_interpreter_call_code.delta"), + /** + * The sequence number for this event. + */ + sequence_number: Schema.Int, + /** + * The index of the output item in the response for which the code is being + * streamed. + */ + output_index: Schema.Int, + /** + * The unique identifier of the code interpreter tool call item. + */ + item_id: Schema.String, + /** + * The partial code snippet being streamed by the code interpreter. + */ + delta: Schema.String +}) {} + +/** + * Emitted when the code snippet is finalized by the code interpreter. + * + * @since 1.0.0 + * @category Schemas + */ +export class ResponseCodeInterpreterCallCodeDoneEvent extends Schema.Class( + "@effect/ai-openai/ResponseCodeInterpreterCallCodeDoneEvent" +)({ + /** + * The type of the event. Always `"response.code_interpreter_call_code.done"`. + */ + type: Schema.Literal("response.code_interpreter_call_code.done"), + /** + * The sequence number for this event. + */ + sequence_number: Schema.Int, + /** + * The index of the output item in the response for which the code is finalized. + */ + output_index: Schema.Int, + /** + * The unique identifier of the code interpreter tool call item. + */ + item_id: Schema.String, + /** + * The final code snippet output by the code interpreter. + */ + code: Schema.String +}) {} + +/** + * Event representing a delta (partial update) to the input of a custom tool call. + * + * @since 1.0.0 + * @category Schemas + */ +export class ResponseCustomToolCallInputDeltaEvent extends Schema.Class( + "@effect/ai-openai/ResponseCustomToolCallInputDeltaEvent" +)({ + /** + * The type of the event. Always `"response.custom_tool_call_input.delta"`. + */ + type: Schema.Literal("response.custom_tool_call_input.delta"), + /** + * The sequence number for this event. + */ + sequence_number: Schema.Int, + /** + * The index of the output this delta applies to. + */ + output_index: Schema.Int, + /** + * Unique identifier for the API item associated with this event. + */ + item_id: Schema.String, + /** + * The incremental input data (delta) for the custom tool call. + */ + delta: Schema.String +}) {} + +/** + * Event indicating that input for a custom tool call is complete. + * + * @since 1.0.0 + * @category Schemas + */ +export class ResponseCustomToolCallInputDoneEvent extends Schema.Class( + "@effect/ai-openai/ResponseCustomToolCallInputDoneEvent" +)({ + /** + * The type of the event. Always `"response.custom_tool_call_input.done"`. + */ + type: Schema.Literal("response.custom_tool_call_input.done"), + /** + * The sequence number for this event. + */ + sequence_number: Schema.Int, + /** + * The index of the output this event applies to. + */ + output_index: Schema.Int, + /** + * Unique identifier for the API item associated with this event. + */ + item_id: Schema.String, + /** + * The complete input data for the custom tool call. + */ + input: Schema.String +}) {} + +/** + * Emitted when an error occurs. + * + * @since 1.0.0 + * @category Schemas + */ +export class ResponseErrorEvent extends Schema.Class( + "@effect/ai-openai/ResponseErrorEvent" +)({ + /** + * The type of the event. Always `"error"`. + */ + type: Schema.Literal("error"), + /** + * The sequence number for this event. + */ + sequence_number: Schema.Int, + /** + * The error code. + */ + code: Schema.optional(Schema.NullOr(Schema.String)), + /** + * The error message. + */ + message: Schema.String, + /** + * The error parameter. + */ + param: Schema.optional(Schema.NullOr(Schema.String)) +}) {} + +/** + * Represents the events that can be emitted during a streaming response. + * + * @since 1.0.0 + * @category Schemas + */ +export const ResponseStreamEvent: Schema.Union<[ + typeof ResponseCreatedEvent, + typeof ResponseQueuedEvent, + typeof ResponseInProgressEvent, + typeof ResponseCompletedEvent, + typeof ResponseIncompleteEvent, + typeof ResponseFailedEvent, + typeof ResponseOutputItemAddedEvent, + typeof ResponseOutputItemDoneEvent, + typeof ResponseContentPartAddedEvent, + typeof ResponseContentPartDoneEvent, + typeof ResponseOutputTextDeltaEvent, + typeof ResponseOutputTextDoneEvent, + typeof ResponseOutputTextAnnotationAddedEvent, + typeof ResponseRefusalDeltaEvent, + typeof ResponseRefusalDoneEvent, + typeof ResponseFunctionCallArgumentsDeltaEvent, + typeof ResponseFunctionCallArgumentsDoneEvent, + typeof ResponseFileSearchCallInProgressEvent, + typeof ResponseFileSearchCallSearchingEvent, + typeof ResponseFileSearchCallCompletedEvent, + typeof ResponseWebSearchCallInProgressEvent, + typeof ResponseWebSearchCallSearchingEvent, + typeof ResponseWebSearchCallCompletedEvent, + typeof ResponseReasoningSummaryPartAddedEvent, + typeof ResponseReasoningSummaryPartDoneEvent, + typeof ResponseReasoningSummaryTextDeltaEvent, + typeof ResponseReasoningSummaryTextDoneEvent, + typeof ResponseReasoningTextDeltaEvent, + typeof ResponseReasoningTextDoneEvent, + typeof ResponseImageGenerationCallInProgressEvent, + typeof ResponseImageGenerationCallGeneratingEvent, + typeof ResponseImageGenerationCallPartialImageEvent, + typeof ResponseImageGenerationCallCompletedEvent, + typeof ResponseMcpCallArgumentsDeltaEvent, + typeof ResponseMcpCallArgumentsDoneEvent, + typeof ResponseMcpCallInProgressEvent, + typeof ResponseMcpCallCompletedEvent, + typeof ResponseMcpCallFailedEvent, + typeof ResponseMcpListToolsInProgressEvent, + typeof ResponseMcpListToolsCompletedEvent, + typeof ResponseMcpListToolsFailedEvent, + typeof ResponseCodeInterpreterCallInProgressEvent, + typeof ResponseCodeInterpreterCallInterpretingEvent, + typeof ResponseCodeInterpreterCallCompletedEvent, + typeof ResponseCodeInterpreterCallCodeDeltaEvent, + typeof ResponseCodeInterpreterCallCodeDoneEvent, + typeof ResponseCustomToolCallInputDeltaEvent, + typeof ResponseCustomToolCallInputDoneEvent, + typeof ResponseErrorEvent +]> = Schema.Union( + ResponseCreatedEvent, + ResponseQueuedEvent, + ResponseInProgressEvent, + ResponseCompletedEvent, + ResponseIncompleteEvent, + ResponseFailedEvent, + ResponseOutputItemAddedEvent, + ResponseOutputItemDoneEvent, + ResponseContentPartAddedEvent, + ResponseContentPartDoneEvent, + ResponseOutputTextDeltaEvent, + ResponseOutputTextDoneEvent, + ResponseOutputTextAnnotationAddedEvent, + ResponseRefusalDeltaEvent, + ResponseRefusalDoneEvent, + ResponseFunctionCallArgumentsDeltaEvent, + ResponseFunctionCallArgumentsDoneEvent, + ResponseFileSearchCallInProgressEvent, + ResponseFileSearchCallSearchingEvent, + ResponseFileSearchCallCompletedEvent, + ResponseWebSearchCallInProgressEvent, + ResponseWebSearchCallSearchingEvent, + ResponseWebSearchCallCompletedEvent, + ResponseReasoningSummaryPartAddedEvent, + ResponseReasoningSummaryPartDoneEvent, + ResponseReasoningSummaryTextDeltaEvent, + ResponseReasoningSummaryTextDoneEvent, + ResponseReasoningTextDeltaEvent, + ResponseReasoningTextDoneEvent, + ResponseImageGenerationCallInProgressEvent, + ResponseImageGenerationCallGeneratingEvent, + ResponseImageGenerationCallPartialImageEvent, + ResponseImageGenerationCallCompletedEvent, + ResponseMcpCallArgumentsDeltaEvent, + ResponseMcpCallArgumentsDoneEvent, + ResponseMcpCallInProgressEvent, + ResponseMcpCallCompletedEvent, + ResponseMcpCallFailedEvent, + ResponseMcpListToolsInProgressEvent, + ResponseMcpListToolsCompletedEvent, + ResponseMcpListToolsFailedEvent, + ResponseCodeInterpreterCallInProgressEvent, + ResponseCodeInterpreterCallInterpretingEvent, + ResponseCodeInterpreterCallCompletedEvent, + ResponseCodeInterpreterCallCodeDeltaEvent, + ResponseCodeInterpreterCallCodeDoneEvent, + ResponseCustomToolCallInputDeltaEvent, + ResponseCustomToolCallInputDoneEvent, + ResponseErrorEvent +) + +/** + * Represents the events that can be emitted during a streaming response. + * + * @since 1.0.0 + * @category Models + */ +export type ResponseStreamEvent = typeof ResponseStreamEvent.Type diff --git a/repos/effect/packages/ai/openai/src/OpenAiConfig.ts b/repos/effect/packages/ai/openai/src/OpenAiConfig.ts new file mode 100644 index 0000000..17dcceb --- /dev/null +++ b/repos/effect/packages/ai/openai/src/OpenAiConfig.ts @@ -0,0 +1,56 @@ +/** + * @since 1.0.0 + */ +import type { HttpClient } from "@effect/platform/HttpClient" +import * as Context from "effect/Context" +import * as Effect from "effect/Effect" +import { dual } from "effect/Function" + +/** + * @since 1.0.0 + * @category Context + */ +export class OpenAiConfig extends Context.Tag("@effect/ai-openai/OpenAiConfig")< + OpenAiConfig, + OpenAiConfig.Service +>() { + /** + * @since 1.0.0 + */ + static readonly getOrUndefined: Effect.Effect = Effect.map( + Effect.context(), + (context) => context.unsafeMap.get(OpenAiConfig.key) + ) +} + +/** + * @since 1.0.0 + */ +export declare namespace OpenAiConfig { + /** + * @since 1.0. + * @category Models + */ + export interface Service { + readonly transformClient?: (client: HttpClient) => HttpClient + } +} + +/** + * @since 1.0.0 + * @category Configuration + */ +export const withClientTransform: { + (transform: (client: HttpClient) => HttpClient): (self: Effect.Effect) => Effect.Effect + (self: Effect.Effect, transform: (client: HttpClient) => HttpClient): Effect.Effect +} = dual< + (transform: (client: HttpClient) => HttpClient) => (self: Effect.Effect) => Effect.Effect, + (self: Effect.Effect, transform: (client: HttpClient) => HttpClient) => Effect.Effect +>( + 2, + (self, transformClient) => + Effect.flatMap( + OpenAiConfig.getOrUndefined, + (config) => Effect.provideService(self, OpenAiConfig, { ...config, transformClient }) + ) +) diff --git a/repos/effect/packages/ai/openai/src/OpenAiEmbeddingModel.ts b/repos/effect/packages/ai/openai/src/OpenAiEmbeddingModel.ts new file mode 100644 index 0000000..d911027 --- /dev/null +++ b/repos/effect/packages/ai/openai/src/OpenAiEmbeddingModel.ts @@ -0,0 +1,227 @@ +/** + * @since 1.0.0 + */ +import * as EmbeddingModel from "@effect/ai/EmbeddingModel" +import * as AiModel from "@effect/ai/Model" +import * as Context from "effect/Context" +import type * as Duration from "effect/Duration" +import * as Effect from "effect/Effect" +import { dual } from "effect/Function" +import * as Layer from "effect/Layer" +import type { Simplify } from "effect/Types" +import type * as Generated from "./Generated.js" +import * as OpenAiClient from "./OpenAiClient.js" + +/** + * @since 1.0.0 + * @category Models + */ +export type Model = typeof Generated.CreateEmbeddingRequestModelEnum.Encoded + +// ============================================================================= +// Configuration +// ============================================================================= + +/** + * @since 1.0.0 + * @category Context + */ +export class Config extends Context.Tag("@effect/ai-openai/OpenAiEmbeddingModel/Config")< + Config, + Config.Service +>() { + /** + * @since 1.0.0 + */ + static readonly getOrUndefined: Effect.Effect = Effect.map( + Effect.context(), + (context) => context.unsafeMap.get(Config.key) + ) +} + +/** + * @since 1.0.0 + */ +export declare namespace Config { + /** + * @since 1.0. + * @category Configuration + */ + export interface Service extends + Simplify< + Partial< + Omit< + typeof Generated.CreateEmbeddingRequest.Encoded, + "input" + > + > + > + {} + + /** + * @since 1.0. + * @category Configuration + */ + export interface Batched extends Omit { + readonly maxBatchSize?: number + readonly cache?: { + readonly capacity: number + readonly timeToLive: Duration.DurationInput + } + } + + /** + * @since 1.0. + * @category Configuration + */ + export interface DataLoader extends Omit { + readonly window: Duration.DurationInput + readonly maxBatchSize?: number + } +} + +// ============================================================================= +// OpenAi Embedding Model +// ============================================================================= + +/** + * @since 1.0.0 + * @category Models + */ +export const model = ( + model: (string & {}) | Model, + { mode, ...config }: Simplify< + ( + | ({ readonly mode: "batched" } & Config.Batched) + | ({ readonly mode: "data-loader" } & Config.DataLoader) + ) + > +): AiModel.Model<"openai", EmbeddingModel.EmbeddingModel, OpenAiClient.OpenAiClient> => { + return AiModel.make( + "openai", + mode === "batched" + ? layerBatched({ model, config: config as Config.Batched }) + : layerDataLoader({ model, config: config as Config.DataLoader }) + ) +} + +/** + * @since 1.0.0 + * @category Constructors + */ +const makeBatched = Effect.fnUntraced(function*(options: { + readonly model: (string & {}) | Model + readonly config?: Config.Batched +}) { + const client = yield* OpenAiClient.OpenAiClient + + const { config = {}, model } = options + const { cache, maxBatchSize = 2048, ...globalConfig } = config + + const makeRequest = Effect.fnUntraced( + function*(input: ReadonlyArray) { + const context = yield* Effect.context() + const requestConfig = context.unsafeMap.get(Config.key) + const request: typeof Generated.CreateEmbeddingRequest.Encoded = { + model, + ...globalConfig, + ...requestConfig, + input + } + return request + } + ) + + return yield* EmbeddingModel.make({ + cache, + maxBatchSize, + embedMany: Effect.fnUntraced(function*(input) { + const request = yield* makeRequest(input) + const response = yield* client.createEmbedding(request) + return makeResults(response) + }) + }) +}) + +/** + * @since 1.0.0 + * @category Constructors + */ +export const makeDataLoader = Effect.fnUntraced(function*(options: { + readonly model: (string & {}) | Model + readonly config: Config.DataLoader +}) { + const client = yield* OpenAiClient.OpenAiClient + const { config, model } = options + const { maxBatchSize = 2048, window, ...globalConfig } = config + + const makeRequest = Effect.fnUntraced( + function*(input: ReadonlyArray) { + const context = yield* Effect.context() + const requestConfig = context.unsafeMap.get(Config.key) + const request: typeof Generated.CreateEmbeddingRequest.Encoded = { + model, + ...globalConfig, + ...requestConfig, + input + } + return request + } + ) + + return yield* EmbeddingModel.makeDataLoader({ + window, + maxBatchSize, + embedMany: Effect.fnUntraced(function*(input) { + const request = yield* makeRequest(input) + const response = yield* client.createEmbedding(request) + return makeResults(response) + }) + }) +}) + +/** + * @since 1.0.0 + * @category Layers + */ +export const layerBatched = (options: { + readonly model: (string & {}) | Model + readonly config?: Config.Batched +}): Layer.Layer => + Layer.effect(EmbeddingModel.EmbeddingModel, makeBatched(options)) + +/** + * @since 1.0.0 + * @category Layers + */ +export const layerDataLoader = (options: { + readonly model: (string & {}) | Model + readonly config: Config.DataLoader +}): Layer.Layer => + Layer.scoped(EmbeddingModel.EmbeddingModel, makeDataLoader(options)) + +/** + * @since 1.0.0 + * @category Configuration + */ +export const withConfigOverride: { + (config: Config.Service): (self: Effect.Effect) => Effect.Effect + (self: Effect.Effect, config: Config.Service): Effect.Effect +} = dual< + (config: Config.Service) => (self: Effect.Effect) => Effect.Effect, + (self: Effect.Effect, config: Config.Service) => Effect.Effect +>(2, (self, overrides) => + Effect.flatMap( + Config.getOrUndefined, + (config) => Effect.provideService(self, Config, { ...config, ...overrides }) + )) + +// ============================================================================= +// Response Conversion +// ============================================================================= + +const makeResults = (response: Generated.CreateEmbeddingResponse): Array => + response.data.map(({ embedding, index }) => ({ + embeddings: embedding as Array, + index + })) diff --git a/repos/effect/packages/ai/openai/src/OpenAiLanguageModel.ts b/repos/effect/packages/ai/openai/src/OpenAiLanguageModel.ts new file mode 100644 index 0000000..a2931b9 --- /dev/null +++ b/repos/effect/packages/ai/openai/src/OpenAiLanguageModel.ts @@ -0,0 +1,1451 @@ +/** + * @since 1.0.0 + */ +import * as AiError from "@effect/ai/AiError" +import * as IdGenerator from "@effect/ai/IdGenerator" +import * as LanguageModel from "@effect/ai/LanguageModel" +import * as AiModel from "@effect/ai/Model" +import type * as Prompt from "@effect/ai/Prompt" +import type * as Response from "@effect/ai/Response" +import type * as Tokenizer from "@effect/ai/Tokenizer" +import * as Tool from "@effect/ai/Tool" +import * as Context from "effect/Context" +import * as DateTime from "effect/DateTime" +import * as Effect from "effect/Effect" +import * as Encoding from "effect/Encoding" +import { dual } from "effect/Function" +import * as Layer from "effect/Layer" +import * as Predicate from "effect/Predicate" +import * as Stream from "effect/Stream" +import type { Span } from "effect/Tracer" +import type { DeepMutable, Mutable, Simplify } from "effect/Types" +import type * as Generated from "./Generated.js" +import * as InternalUtilities from "./internal/utilities.js" +import type { ResponseStreamEvent } from "./OpenAiClient.js" +import { OpenAiClient } from "./OpenAiClient.js" +import { addGenAIAnnotations } from "./OpenAiTelemetry.js" +import * as OpenAiTokenizer from "./OpenAiTokenizer.js" +import * as OpenAiTool from "./OpenAiTool.js" + +/** + * @since 1.0.0 + * @category Models + */ +export type Model = typeof Generated.ChatModel.Encoded | typeof Generated.ModelIdsResponsesEnum.Encoded + +// ============================================================================= +// Configuration +// ============================================================================= + +/** + * @since 1.0.0 + * @category Context + */ +export class Config extends Context.Tag("@effect/ai-openai/OpenAiLanguageModel/Config")< + Config, + Config.Service +>() { + /** + * @since 1.0.0 + */ + static readonly getOrUndefined: Effect.Effect = Effect.map( + Effect.context(), + (context) => context.unsafeMap.get(Config.key) + ) +} + +/** + * @since 1.0.0 + */ +export declare namespace Config { + /** + * @since 1.0.0 + * @category Models + */ + export interface Service extends + Simplify< + Partial< + Omit< + typeof Generated.CreateResponse.Encoded, + "input" | "tools" | "tool_choice" | "stream" | "text" + > + > + > + { + /** + * File ID prefixes used to identify file IDs in Responses API. + * When undefined, all file data is treated as base64 content. + * + * Examples: + * - OpenAI: ['file-'] for IDs like 'file-abc123' + * - Azure OpenAI: ['assistant-'] for IDs like 'assistant-abc123' + */ + readonly fileIdPrefixes?: ReadonlyArray + /** + * Configuration options for a text response from the model. + */ + readonly text?: { + /** + * Constrains the verbosity of the model's response. Lower values will + * result in more concise responses, while higher values will result in + * more verbose responses. + * + * Defaults to `"medium"`. + */ + readonly verbosity?: "low" | "medium" | "high" + } + + /** + * Controls whether tool and response format schemas are sent with + * `strict: true` to enable OpenAI's structured outputs mode. + * + * When `true` (default), OpenAI validates that tool schemas comply with + * strict mode requirements (all properties in `required`, + * `additionalProperties: false` on all objects, etc.). + * + * Set to `false` to send tool schemas without strict mode validation, + * which is useful when tool parameter schemas use `Schema.optional()` + * (optional properties are not listed in `required` by Effect's JSON + * schema generator, which strict mode rejects). + * + * Defaults to `true` for backward compatibility. + */ + readonly strict?: boolean + } +} + +// ============================================================================= +// OpenAI Provider Options / Metadata +// ============================================================================= + +declare module "@effect/ai/Prompt" { + export interface FilePartOptions extends ProviderOptions { + readonly openai?: { + /** + * The detail level of the image to be sent to the model. One of `high`, `low`, or `auto`. Defaults to `auto`. + */ + readonly imageDetail?: typeof Generated.ImageDetail.Encoded | undefined + } | undefined + } + + export interface ReasoningPartOptions extends ProviderOptions { + readonly openai?: { + /** + * The ID of the item to reference. + */ + readonly itemId?: string | undefined + /** + * The encrypted content of the reasoning item - populated when a response + * is generated with `reasoning.encrypted_content` in the `include` + * parameter. + */ + readonly encryptedContent?: string | undefined + } | undefined + } + + export interface ToolCallPartOptions extends ProviderOptions { + readonly openai?: { + /** + * The ID of the item to reference. + */ + readonly itemId?: string | undefined + } | undefined + } + + export interface TextPartOptions extends ProviderOptions { + readonly openai?: { + /** + * The ID of the item to reference. + */ + readonly itemId?: string | undefined + } | undefined + } +} + +declare module "@effect/ai/Response" { + export interface TextPartMetadata extends ProviderMetadata { + readonly openai?: { + readonly itemId?: string | undefined + /** + * If the model emits a refusal content part, the refusal explanation + * from the model will be contained in the metadata of an empty text + * part. + */ + readonly refusal?: string | undefined + } | undefined + } + + export interface TextStartPartMetadata extends ProviderMetadata { + readonly openai?: { + readonly itemId?: string | undefined + } | undefined + } + + export interface ReasoningPartMetadata extends ProviderMetadata { + readonly openai?: { + readonly itemId?: string | undefined + readonly encryptedContent?: string | undefined + } | undefined + } + + export interface ReasoningStartPartMetadata extends ProviderMetadata { + readonly openai?: { + readonly itemId?: string | undefined + readonly encryptedContent?: string | undefined + } | undefined + } + + export interface ReasoningDeltaPartMetadata extends ProviderMetadata { + readonly openai?: { + readonly itemId?: string | undefined + } | undefined + } + + export interface ReasoningEndPartMetadata extends ProviderMetadata { + readonly openai?: { + readonly itemId?: string | undefined + readonly encryptedContent?: string | undefined + } | undefined + } + + export interface ToolCallPartMetadata extends ProviderMetadata { + readonly openai?: { + readonly itemId?: string | undefined + } | undefined + } + + export interface DocumentSourcePartMetadata extends ProviderMetadata { + readonly openai?: { + readonly type: "file_citation" + /** + * The index of the file in the list of files. + */ + readonly index: number + } | undefined + } + + export interface UrlSourcePartMetadata extends ProviderMetadata { + readonly openai?: { + readonly type: "url_citation" + /** + * The index of the first character of the URL citation in the message. + */ + readonly startIndex: number + /** + * The index of the last character of the URL citation in the message. + */ + readonly endIndex: number + } | undefined + } + + export interface FinishPartMetadata extends ProviderMetadata { + readonly openai?: { + readonly serviceTier?: "default" | "auto" | "flex" | "scale" | "priority" | undefined + } | undefined + } +} + +/** + * @since 1.0.0 + */ +export declare namespace ProviderMetadata { + /** + * @since 1.0.0 + * @category Provider Metadata + */ + export interface Service { + "source": {} | {} + } +} + +// ============================================================================= +// OpenAI Language Model +// ============================================================================= + +/** + * @since 1.0.0 + * @category Ai Models + */ +export const model = ( + model: (string & {}) | Model, + config?: Omit +): AiModel.Model<"openai", LanguageModel.LanguageModel, OpenAiClient> => + AiModel.make("openai", layer({ model, config })) + +/** + * @since 1.0.0 + * @category Ai Models + */ +export const modelWithTokenizer = ( + model: (string & {}) | Model, + config?: Omit +): AiModel.Model<"openai", LanguageModel.LanguageModel | Tokenizer.Tokenizer, OpenAiClient> => + AiModel.make("openai", layerWithTokenizer({ model, config })) + +/** + * @since 1.0.0 + * @category Constructors + */ +export const make = Effect.fnUntraced(function*(options: { + readonly model: (string & {}) | Model + readonly config?: Omit +}) { + const client = yield* OpenAiClient + + const makeRequest: (providerOptions: LanguageModel.ProviderOptions) => Effect.Effect< + typeof Generated.CreateResponse.Encoded, + AiError.AiError + > = Effect.fnUntraced( + function*(providerOptions) { + const context = yield* Effect.context() + const config = { model: options.model, ...options.config, ...context.unsafeMap.get(Config.key) } + const messages = yield* prepareMessages(providerOptions, config) + const { toolChoice, tools } = yield* prepareTools(providerOptions, config) + const include = prepareInclude(providerOptions, config) + const responseFormat = prepareResponseFormat(providerOptions, config) + const verbosity = config.text?.verbosity + const { strict: _strict, ...requestConfig } = config + const request: typeof Generated.CreateResponse.Encoded = { + ...requestConfig, + input: messages, + include, + text: { format: responseFormat, verbosity }, + tools, + tool_choice: toolChoice + } + return request + } + ) + + return yield* LanguageModel.make({ + generateText: Effect.fnUntraced( + function*(options) { + const request = yield* makeRequest(options) + annotateRequest(options.span, request) + const rawResponse = yield* client.createResponse(request) + annotateResponse(options.span, rawResponse) + return yield* makeResponse(rawResponse, options) + } + ), + streamText: Effect.fnUntraced( + function*(options) { + const request = yield* makeRequest(options) + annotateRequest(options.span, request) + return client.createResponseStream(request) + }, + (effect, options) => + effect.pipe( + Effect.flatMap((stream) => makeStreamResponse(stream, options)), + Stream.unwrap, + Stream.map((response) => { + annotateStreamResponse(options.span, response) + return response + }) + ) + ) + }) +}) + +/** + * @since 1.0.0 + * @category Layers + */ +export const layer = (options: { + readonly model: (string & {}) | Model + readonly config?: Omit +}): Layer.Layer => + Layer.effect(LanguageModel.LanguageModel, make({ model: options.model, config: options.config })) + +/** + * @since 1.0.0 + * @category Layers + */ +export const layerWithTokenizer = (options: { + readonly model: (string & {}) | Model + readonly config?: Omit +}): Layer.Layer => + Layer.merge(layer(options), OpenAiTokenizer.layer(options)) + +/** + * @since 1.0.0 + * @category Configuration + */ +export const withConfigOverride: { + (overrides: Config.Service): (self: Effect.Effect) => Effect.Effect + (self: Effect.Effect, overrides: Config.Service): Effect.Effect +} = dual< + (overrides: Config.Service) => (self: Effect.Effect) => Effect.Effect, + (self: Effect.Effect, overrides: Config.Service) => Effect.Effect +>(2, (self, overrides) => + Effect.flatMap( + Config.getOrUndefined, + (config) => Effect.provideService(self, Config, { ...config, ...overrides }) + )) + +// ============================================================================= +// Prompt Conversion +// ============================================================================= + +const getSystemMessageMode = (model: string): "system" | "developer" => + model.startsWith("o") || + model.startsWith("gpt-5") || + model.startsWith("codex-") || + model.startsWith("computer-use") + ? "developer" + : "system" + +const prepareMessages: ( + options: LanguageModel.ProviderOptions, + config: Config.Service +) => Effect.Effect< + ReadonlyArray, + AiError.AiError +> = Effect.fnUntraced(function*(options, config) { + const messages: Array = [] + + for (const message of options.prompt.content) { + switch (message.role) { + case "system": { + messages.push({ + role: getSystemMessageMode(config.model!), + content: message.content + }) + break + } + + case "user": { + const content: Array = [] + + for (let index = 0; index < message.content.length; index++) { + const part = message.content[index] + + switch (part.type) { + case "text": { + content.push({ type: "input_text", text: part.text }) + break + } + + case "file": { + if (part.mediaType.startsWith("image/")) { + const detail = getImageDetail(part) + const mediaType = part.mediaType === "image/*" ? "image/jpeg" : part.mediaType + + if (typeof part.data === "string" && isFileId(part.data, config)) { + content.push({ type: "input_image", file_id: part.data, detail }) + } + + if (part.data instanceof URL) { + content.push({ type: "input_image", image_url: part.data.toString(), detail }) + } + + if (part.data instanceof Uint8Array) { + const base64 = Encoding.encodeBase64(part.data) + const imageUrl = `data:${mediaType};base64,${base64}` + content.push({ type: "input_image", image_url: imageUrl, detail }) + } + } else if (part.mediaType === "application/pdf") { + if (typeof part.data === "string" && isFileId(part.data, config)) { + content.push({ type: "input_file", file_id: part.data }) + } + + if (part.data instanceof URL) { + content.push({ type: "input_file", file_url: part.data.toString() }) + } + + if (part.data instanceof Uint8Array) { + const base64 = Encoding.encodeBase64(part.data) + const fileName = part.fileName ?? `part-${index}.pdf` + const fileData = `data:application/pdf;base64,${base64}` + content.push({ type: "input_file", filename: fileName, file_data: fileData }) + } + } else { + return yield* new AiError.MalformedInput({ + module: "OpenAiLanguageModel", + method: "prepareMessages", + description: `Detected unsupported media type for file: '${part.mediaType}'` + }) + } + } + } + } + + messages.push({ role: "user", content }) + + break + } + + case "assistant": { + const reasoningMessages: Record> = {} + + for (const part of message.content) { + switch (part.type) { + case "text": { + messages.push({ + role: "assistant", + content: [{ type: "output_text", text: part.text }], + id: getItemId(part) + }) + break + } + + case "reasoning": { + const options = part.options.openai + + if (Predicate.isNotUndefined(options?.itemId)) { + const reasoningMessage = reasoningMessages[options.itemId] + const summaryParts: Mutable = [] + + if (part.text.length > 0) { + summaryParts.push({ type: "summary_text", text: part.text }) + } + + if (Predicate.isUndefined(reasoningMessage)) { + reasoningMessages[options.itemId] = { + id: options.itemId, + type: "reasoning", + summary: summaryParts, + encrypted_content: options.encryptedContent + } + messages.push(reasoningMessages[options.itemId]) + } else { + for (const summaryPart of summaryParts) { + reasoningMessage.summary.push(summaryPart) + } + } + } + + break + } + + case "tool-call": { + if (!part.providerExecuted) { + messages.push({ + id: getItemId(part), + type: "function_call", + call_id: part.id, + name: part.name, + arguments: JSON.stringify(part.params) + }) + } + + break + } + } + } + + break + } + + case "tool": { + for (const part of message.content) { + messages.push({ + type: "function_call_output", + call_id: part.id, + output: JSON.stringify(part.result) + }) + } + + break + } + } + } + + return messages +}) + +// ============================================================================= +// Response Conversion +// ============================================================================= + +const makeResponse: ( + response: Generated.Response, + options: LanguageModel.ProviderOptions +) => Effect.Effect< + Array, + AiError.AiError, + IdGenerator.IdGenerator +> = Effect.fnUntraced( + function*(response, options) { + const idGenerator = yield* IdGenerator.IdGenerator + + const webSearchTool = options.tools.find((tool) => + Tool.isProviderDefined(tool) && + (tool.id === "openai.web_search" || + tool.id === "openai.web_search_preview") + ) as Tool.AnyProviderDefined | undefined + + let hasToolCalls = false + const parts: Array = [] + + const createdAt = new Date(response.created_at * 1000) + parts.push({ + type: "response-metadata", + id: response.id, + modelId: response.model, + timestamp: DateTime.formatIso(DateTime.unsafeFromDate(createdAt)) + }) + + // Deduplicate output items by ID to handle OpenAI Responses API bug + // where duplicate OutputMessage items appear in response.output + const seenOutputIds = new Set() + for (const part of response.output) { + if (part.id && seenOutputIds.has(part.id)) { + continue + } + if (part.id) { + seenOutputIds.add(part.id) + } + switch (part.type) { + case "message": { + for (const contentPart of part.content) { + switch (contentPart.type) { + case "output_text": { + parts.push({ + type: "text", + text: contentPart.text, + metadata: { openai: { itemId: part.id } } + }) + + for (const annotation of contentPart.annotations) { + if (annotation.type === "file_citation") { + const metadata = { + type: annotation.type, + index: annotation.index + } + + parts.push({ + type: "source", + sourceType: "document", + id: yield* idGenerator.generateId(), + mediaType: "text/plain", + title: annotation.filename ?? "Untitled Document", + metadata: { openai: metadata } + }) + } + + if (annotation.type === "url_citation") { + const metadata = { + type: annotation.type, + startIndex: annotation.start_index, + endIndex: annotation.end_index + } + + parts.push({ + type: "source", + sourceType: "url", + id: yield* idGenerator.generateId(), + url: annotation.url, + title: annotation.title, + metadata: { openai: metadata } + }) + } + } + + break + } + case "refusal": { + parts.push({ + type: "text", + text: "", + metadata: { openai: { refusal: contentPart.refusal } } + }) + + break + } + } + } + + break + } + + case "function_call": { + hasToolCalls = true + + const toolName = part.name + const toolParams = part.arguments + + const params = yield* Effect.try({ + try: () => Tool.unsafeSecureJsonParse(toolParams), + catch: (cause) => + new AiError.MalformedOutput({ + module: "OpenAiLanguageModel", + method: "makeResponse", + description: "Failed to securely parse tool call parameters " + + `for tool '${toolName}':\nParameters: ${toolParams}`, + cause + }) + }) + + parts.push({ + type: "tool-call", + id: part.call_id, + name: toolName, + params, + metadata: { openai: { itemId: part.id } } + }) + + break + } + + case "code_interpreter_call": { + parts.push({ + type: "tool-call", + id: part.id, + name: "OpenAiCodeInterpreter", + params: { code: part.code, container_id: part.container_id }, + providerName: "code_interpreter", + providerExecuted: true + }) + + parts.push({ + type: "tool-result", + id: part.id, + name: "OpenAiCodeInterpreter", + isFailure: false, + result: part.outputs, + providerName: "code_interpreter", + providerExecuted: true + }) + + break + } + + case "file_search_call": { + parts.push({ + type: "tool-call", + id: part.id, + name: "OpenAiFileSearch", + params: {}, + providerName: "file_search", + providerExecuted: true + }) + + parts.push({ + type: "tool-result", + id: part.id, + name: "OpenAiFileSearch", + isFailure: false, + result: { + status: part.status, + queries: part.queries, + ...(part.results && { results: part.results }) + }, + providerName: "file_search", + providerExecuted: true + }) + + break + } + + case "web_search_call": { + parts.push({ + type: "tool-call", + id: part.id, + name: webSearchTool?.name ?? "OpenAiWebSearch", + params: { action: part.action }, + providerName: webSearchTool?.providerName ?? "web_search", + providerExecuted: true + }) + + parts.push({ + type: "tool-result", + id: part.id, + name: webSearchTool?.name ?? "OpenAiWebSearch", + isFailure: false, + result: { status: part.status }, + providerName: webSearchTool?.providerName ?? "web_search", + providerExecuted: true + }) + + break + } + + // TODO(Max): support computer use + // case "computer_call": { + // parts.push({ + // type: "tool-call", + // id: part.id, + // name: "OpenAiComputerUse", + // params: { action: part.action }, + // providerName: webSearchTool?.providerName ?? "web_search", + // providerExecuted: true + // }) + // + // parts.push({ + // type: "tool-result", + // id: part.id, + // name: webSearchTool?.name ?? "OpenAiWebSearch", + // result: { status: part.status }, + // providerName: webSearchTool?.providerName ?? "web_search", + // providerExecuted: true + // }) + // break + // } + + case "reasoning": { + // If there are no summary parts, we have to add an empty one to + // propagate the part identifier + if (part.summary.length === 0) { + parts.push({ + type: "reasoning", + text: "", + metadata: { openai: { itemId: part.id } } + }) + } else { + for (const summary of part.summary) { + const metadata = { + itemId: part.id, + encryptedContent: part.encrypted_content ?? undefined + } + parts.push({ + type: "reasoning", + text: summary.text, + metadata: { openai: metadata } + }) + } + } + + break + } + } + } + + const finishReason = InternalUtilities.resolveFinishReason( + response.incomplete_details?.reason, + hasToolCalls + ) + + const metadata = { + serviceTier: response.service_tier + } + + parts.push({ + type: "finish", + reason: finishReason, + usage: { + inputTokens: response.usage?.input_tokens, + outputTokens: response.usage?.output_tokens, + totalTokens: (response.usage?.input_tokens ?? 0) + (response.usage?.output_tokens ?? 0), + reasoningTokens: response.usage?.output_tokens_details?.reasoning_tokens, + cachedInputTokens: response.usage?.input_tokens_details?.cached_tokens + }, + metadata: { openai: metadata } + }) + + return parts + } +) + +const makeStreamResponse: ( + stream: Stream.Stream, + options: LanguageModel.ProviderOptions +) => Effect.Effect< + Stream.Stream, + never, + IdGenerator.IdGenerator +> = Effect.fnUntraced( + function*(stream, options) { + const idGenerator = yield* IdGenerator.IdGenerator + + let hasToolCalls = false + + const activeReasoning: Record + readonly encryptedContent: string | undefined + }> = {} + + const activeToolCalls: Record = {} + + const webSearchTool = options.tools.find((tool) => + Tool.isProviderDefined(tool) && + (tool.id === "openai.web_search" || + tool.id === "openai.web_search_preview") + ) as Tool.AnyProviderDefined | undefined + + return stream.pipe( + Stream.mapEffect(Effect.fnUntraced(function*(event) { + const parts: Array = [] + + switch (event.type) { + case "response.created": { + const createdAt = new Date(event.response.created_at * 1000) + parts.push({ + type: "response-metadata", + id: event.response.id, + modelId: event.response.model, + timestamp: DateTime.formatIso(DateTime.unsafeFromDate(createdAt)) + }) + break + } + + case "error": { + parts.push({ type: "error", error: event }) + break + } + + case "response.completed": + case "response.incomplete": + case "response.failed": { + parts.push({ + type: "finish", + reason: InternalUtilities.resolveFinishReason( + event.response.incomplete_details?.reason, + hasToolCalls + ), + usage: { + inputTokens: event.response.usage?.input_tokens, + outputTokens: event.response.usage?.output_tokens, + totalTokens: (event.response.usage?.input_tokens ?? 0) + (event.response.usage?.output_tokens ?? 0), + reasoningTokens: event.response.usage?.output_tokens_details?.reasoning_tokens, + cachedInputTokens: event.response.usage?.input_tokens_details?.cached_tokens + }, + metadata: { openai: { serviceTier: event.response.service_tier } } + }) + break + } + + case "response.output_item.added": { + switch (event.item.type) { + case "computer_call": { + // TODO(Max): support computer use + break + } + + case "file_search_call": { + activeToolCalls[event.output_index] = { + id: event.item.id, + name: "OpenAiFileSearch" + } + parts.push({ + type: "tool-params-start", + id: event.item.id, + name: "OpenAiFileSearch", + providerName: "file_search", + providerExecuted: true + }) + break + } + + case "function_call": { + activeToolCalls[event.output_index] = { + id: event.item.call_id, + name: event.item.name + } + parts.push({ + type: "tool-params-start", + id: event.item.call_id, + name: event.item.name + }) + break + } + + case "message": { + parts.push({ + type: "text-start", + id: event.item.id, + metadata: { openai: { itemId: event.item.id } } + }) + break + } + + case "reasoning": { + activeReasoning[event.item.id] = { + summaryParts: [0], + encryptedContent: event.item.encrypted_content + } + parts.push({ + type: "reasoning-start", + id: `${event.item.id}:0`, + metadata: { + openai: { + itemId: event.item.id, + encryptedContent: event.item.encrypted_content + } + } + }) + break + } + + case "web_search_call": { + activeToolCalls[event.output_index] = { + id: event.item.id, + name: webSearchTool?.name ?? "OpenAiWebSearch" + } + parts.push({ + type: "tool-params-start", + id: event.item.id, + name: webSearchTool?.name ?? "OpenAiWebSearch", + providerName: webSearchTool?.providerName ?? "web_search", + providerExecuted: true + }) + break + } + } + + break + } + + case "response.output_item.done": { + switch (event.item.type) { + case "code_interpreter_call": { + parts.push({ + type: "tool-call", + id: event.item.id, + name: "OpenAiCodeInterpreter", + params: { code: event.item.code, container_id: event.item.container_id }, + providerName: "code_interpreter", + providerExecuted: true + }) + parts.push({ + type: "tool-result", + id: event.item.id, + name: "OpenAiCodeInterpreter", + isFailure: false, + result: { outputs: event.item.outputs }, + providerName: "code_interpreter", + providerExecuted: true + }) + break + } + + // TODO(Max): support computer use + case "computer_call": { + break + } + + case "file_search_call": { + delete activeToolCalls[event.output_index] + parts.push({ + type: "tool-params-end", + id: event.item.id + }) + parts.push({ + type: "tool-call", + id: event.item.id, + name: "OpenAiFileSearch", + params: {}, + providerName: "file_search", + providerExecuted: true + }) + parts.push({ + type: "tool-result", + id: event.item.id, + name: "OpenAiFileSearch", + isFailure: false, + result: { + status: event.item.status, + queries: event.item.queries, + ...(event.item.results && { results: event.item.results }) + }, + providerName: "file_search", + providerExecuted: true + }) + break + } + + case "function_call": { + hasToolCalls = true + + const toolName = event.item.name + const toolParams = event.item.arguments + + const params = yield* Effect.try({ + try: () => Tool.unsafeSecureJsonParse(toolParams), + catch: (cause) => + new AiError.MalformedOutput({ + module: "OpenAiLanguageModel", + method: "makeStreamResponse", + description: "Failed to securely parse tool call parameters " + + `for tool '${toolName}':\nParameters: ${toolParams}`, + cause + }) + }) + + parts.push({ + type: "tool-params-end", + id: event.item.call_id + }) + + parts.push({ + type: "tool-call", + id: event.item.call_id, + name: toolName, + params, + metadata: { openai: { itemId: event.item.id } } + }) + + delete activeToolCalls[event.output_index] + + break + } + + case "message": { + parts.push({ + type: "text-end", + id: event.item.id + }) + break + } + + case "reasoning": { + const reasoningPart = activeReasoning[event.item.id] + for (const summaryIndex of reasoningPart.summaryParts) { + parts.push({ + type: "reasoning-end", + id: `${event.item.id}:${summaryIndex}`, + metadata: { + openai: { + itemId: event.item.id, + encryptedContent: event.item.encrypted_content + } + } + }) + } + delete activeReasoning[event.item.id] + break + } + + case "web_search_call": { + delete activeToolCalls[event.output_index] + parts.push({ + type: "tool-params-end", + id: event.item.id + }) + parts.push({ + type: "tool-call", + id: event.item.id, + name: "OpenAiWebSearch", + params: { action: event.item.action }, + providerName: "web_search", + providerExecuted: true + }) + parts.push({ + type: "tool-result", + id: event.item.id, + name: "OpenAiWebSearch", + isFailure: false, + result: { status: event.item.status }, + providerName: "web_search", + providerExecuted: true + }) + break + } + } + + break + } + + case "response.output_text.delta": { + parts.push({ + type: "text-delta", + id: event.item_id, + delta: event.delta + }) + break + } + + case "response.output_text.annotation.added": { + if (event.annotation.type === "file_citation") { + parts.push({ + type: "source", + sourceType: "document", + id: yield* idGenerator.generateId(), + mediaType: "text/plain", + title: event.annotation.filename ?? "Untitled Document", + fileName: event.annotation.filename ?? event.annotation.file_id + }) + } + if (event.annotation.type === "url_citation") { + parts.push({ + type: "source", + sourceType: "url", + id: yield* idGenerator.generateId(), + url: event.annotation.url, + title: event.annotation.title + }) + } + break + } + + case "response.function_call_arguments.delta": { + const toolCallPart = activeToolCalls[event.output_index] + if (Predicate.isNotUndefined(toolCallPart)) { + parts.push({ + type: "tool-params-delta", + id: toolCallPart.id, + delta: event.delta + }) + } + break + } + + case "response.reasoning_summary_part.added": { + // The first reasoning start is pushed in the `response.output_item.added` block + if (event.summary_index > 0) { + const reasoningPart = activeReasoning[event.item_id] + if (Predicate.isNotUndefined(reasoningPart)) { + reasoningPart.summaryParts.push(event.summary_index) + } + parts.push({ + type: "reasoning-start", + id: `${event.item_id}:${event.summary_index}`, + metadata: { + openai: { + itemId: event.item_id, + encryptedContent: reasoningPart?.encryptedContent + } + } + }) + } + break + } + + case "response.reasoning_summary_text.delta": { + parts.push({ + type: "reasoning-delta", + id: `${event.item_id}:${event.summary_index}`, + delta: event.delta, + metadata: { openai: { itemId: event.item_id } } + }) + break + } + } + + return parts + })), + Stream.flattenIterables + ) + } +) + +// ============================================================================= +// Telemetry +// ============================================================================= + +const annotateRequest = ( + span: Span, + request: typeof Generated.CreateResponse.Encoded +): void => { + addGenAIAnnotations(span, { + system: "openai", + operation: { name: "chat" }, + request: { + model: request.model, + temperature: request.temperature, + topP: request.top_p, + maxTokens: request.max_output_tokens + }, + openai: { + request: { + responseFormat: request.text?.format?.type, + serviceTier: request.service_tier + } + } + }) +} + +const annotateResponse = (span: Span, response: Generated.Response): void => { + const finishReason = response.incomplete_details?.reason + addGenAIAnnotations(span, { + response: { + id: response.id, + model: response.model, + finishReasons: Predicate.isNotUndefined(finishReason) ? [finishReason] : undefined + }, + usage: { + inputTokens: response.usage?.input_tokens, + outputTokens: response.usage?.output_tokens + }, + openai: { + response: { + serviceTier: response.service_tier + } + } + }) +} + +const annotateStreamResponse = (span: Span, part: Response.StreamPartEncoded) => { + if (part.type === "response-metadata") { + addGenAIAnnotations(span, { + response: { + id: part.id, + model: part.modelId + } + }) + } + if (part.type === "finish") { + const serviceTier = part.metadata?.openai?.serviceTier as string | undefined + addGenAIAnnotations(span, { + response: { + finishReasons: [part.reason] + }, + usage: { + inputTokens: part.usage.inputTokens, + outputTokens: part.usage.outputTokens + }, + openai: { + response: { serviceTier } + } + }) + } +} + +// ============================================================================= +// Tool Calling +// ============================================================================= + +type OpenAiToolChoice = typeof Generated.CreateResponse.fields.tool_choice.from.Encoded + +const prepareTools: (options: LanguageModel.ProviderOptions, config: Config.Service) => Effect.Effect<{ + readonly tools: ReadonlyArray | undefined + readonly toolChoice: OpenAiToolChoice | undefined +}, AiError.AiError> = Effect.fnUntraced(function*(options, config) { + // Return immediately if no tools are in the toolkit + if (options.tools.length === 0) { + return { tools: undefined, toolChoice: undefined } + } + + const tools: Array = [] + let toolChoice: OpenAiToolChoice | undefined = undefined + + // Filter the incoming tools down to the set of allowed tools as indicated by + // the tool choice. This must be done here given that there is no tool name + // in OpenAI's provider-defined tools, so there would be no way to perform + // this filter otherwise + let allowedTools = options.tools + if (typeof options.toolChoice === "object" && "oneOf" in options.toolChoice) { + const allowedToolNames = new Set(options.toolChoice.oneOf) + allowedTools = options.tools.filter((tool) => allowedToolNames.has(tool.name)) + toolChoice = options.toolChoice.mode === "required" ? "required" : "auto" + } + + // Convert the tools in the toolkit to the provider-defined format + for (const tool of allowedTools) { + if (Tool.isUserDefined(tool)) { + tools.push({ + type: "function", + name: tool.name, + description: Tool.getDescription(tool as any), + parameters: Tool.getJsonSchema(tool as any) as any, + strict: config.strict ?? true + }) + } + + if (Tool.isProviderDefined(tool)) { + switch (tool.id) { + case "openai.code_interpreter": { + tools.push({ + ...tool.args, + type: "code_interpreter" + }) + break + } + case "openai.file_search": { + tools.push({ + ...tool.args, + type: "file_search" + }) + break + } + case "openai.web_search": { + tools.push({ + ...tool.args, + type: "web_search" + }) + break + } + case "openai.web_search_preview": { + tools.push({ + ...tool.args, + type: "web_search_preview" + }) + break + } + default: { + return yield* new AiError.MalformedInput({ + module: "AnthropicLanguageModel", + method: "prepareTools", + description: `Received request to call unknown provider-defined tool '${tool.name}'` + }) + } + } + } + } + + if (options.toolChoice === "auto" || options.toolChoice === "none" || options.toolChoice === "required") { + toolChoice = options.toolChoice + } + + if (typeof options.toolChoice === "object" && "tool" in options.toolChoice) { + toolChoice = Predicate.isUndefined(OpenAiTool.getProviderDefinedToolName(options.toolChoice.tool)) + ? { type: "function", name: options.toolChoice.tool } + : { type: options.toolChoice.tool } + } + + return { tools, toolChoice } +}) + +// ============================================================================= +// Utilities +// ============================================================================= + +const isFileId = (data: string, config: Config.Service): boolean => + Predicate.isNotUndefined(config.fileIdPrefixes) && config.fileIdPrefixes.some((prefix) => data.startsWith(prefix)) + +const getItemId = ( + part: + | Prompt.TextPart + | Prompt.ToolCallPart +): string | undefined => part.options.openai?.itemId + +const getImageDetail = (part: Prompt.FilePart): typeof Generated.ImageDetail.Encoded => + part.options.openai?.imageDetail ?? "auto" + +const prepareInclude = ( + options: LanguageModel.ProviderOptions, + config: Config.Service +): ReadonlyArray => { + const include: Set = new Set(config.include ?? []) + + const codeInterpreterTool = options.tools.find((tool) => + Tool.isProviderDefined(tool) && + tool.id === "openai.code_interpreter" + ) as Tool.AnyProviderDefined | undefined + + if (Predicate.isNotUndefined(codeInterpreterTool)) { + include.add("code_interpreter_call.outputs") + } + + const webSearchTool = options.tools.find((tool) => + Tool.isProviderDefined(tool) && + (tool.id === "openai.web_search" || + tool.id === "openai.web_search_preview") + ) as Tool.AnyProviderDefined | undefined + + if (Predicate.isNotUndefined(webSearchTool)) { + include.add("web_search_call.action.sources") + } + + return Array.from(include) +} + +const prepareResponseFormat = ( + options: LanguageModel.ProviderOptions, + config: Config.Service +): typeof Generated.TextResponseFormatConfiguration.Encoded => { + if (options.responseFormat.type === "json") { + const name = options.responseFormat.objectName + const schema = options.responseFormat.schema + return { + type: "json_schema", + name, + description: Tool.getDescriptionFromSchemaAst(schema.ast) ?? "Response with a JSON object", + schema: Tool.getJsonSchemaFromSchemaAst(schema.ast) as any, + strict: config.strict ?? true + } + } + return { type: "text" } +} diff --git a/repos/effect/packages/ai/openai/src/OpenAiTelemetry.ts b/repos/effect/packages/ai/openai/src/OpenAiTelemetry.ts new file mode 100644 index 0000000..0aebafe --- /dev/null +++ b/repos/effect/packages/ai/openai/src/OpenAiTelemetry.ts @@ -0,0 +1,136 @@ +/** + * @since 1.0.0 + */ +import * as Telemetry from "@effect/ai/Telemetry" +import { dual } from "effect/Function" +import * as Predicate from "effect/Predicate" +import * as String from "effect/String" +import type { Span } from "effect/Tracer" +import type { Simplify } from "effect/Types" + +/** + * The attributes used to describe telemetry in the context of Generative + * Artificial Intelligence (GenAI) Models requests and responses. + * + * {@see https://opentelemetry.io/docs/specs/semconv/attributes-registry/gen-ai/} + * + * @since 1.0.0 + * @category Models + */ +export type OpenAiTelemetryAttributes = Simplify< + & Telemetry.GenAITelemetryAttributes + & Telemetry.AttributesWithPrefix + & Telemetry.AttributesWithPrefix +> + +/** + * All telemetry attributes which are part of the GenAI specification, + * including the OpenAi-specific attributes. + * + * @since 1.0.0 + * @category Models + */ +export type AllAttributes = Telemetry.AllAttributes & RequestAttributes & ResponseAttributes + +/** + * Telemetry attributes which are part of the GenAI specification and are + * namespaced by `gen_ai.openai.request`. + * + * @since 1.0.0 + * @category Models + */ +export interface RequestAttributes { + /** + * The response format that is requested. + */ + readonly responseFormat?: (string & {}) | WellKnownResponseFormat | null | undefined + /** + * The service tier requested. May be a specific tier, `default`, or `auto`. + */ + readonly serviceTier?: (string & {}) | WellKnownServiceTier | null | undefined +} + +/** + * Telemetry attributes which are part of the GenAI specification and are + * namespaced by `gen_ai.openai.response`. + * + * @since 1.0.0 + * @category Models + */ +export interface ResponseAttributes { + /** + * The service tier used for the response. + */ + readonly serviceTier?: string | null | undefined + /** + * A fingerprint to track any eventual change in the Generative AI + * environment. + */ + readonly systemFingerprint?: string | null | undefined +} + +/** + * The `gen_ai.openai.request.response_format` attribute has the following + * list of well-known values. + * + * If one of them applies, then the respective value **MUST** be used; + * otherwise, a custom value **MAY** be used. + * + * @since 1.0.0 + * @category Models + */ +export type WellKnownResponseFormat = "json_object" | "json_schema" | "text" + +/** + * The `gen_ai.openai.request.service_tier` attribute has the following + * list of well-known values. + * + * If one of them applies, then the respective value **MUST** be used; + * otherwise, a custom value **MAY** be used. + * + * @since 1.0.0 + * @category Models + */ +export type WellKnownServiceTier = "auto" | "default" + +/** + * @since 1.0.0 + * @since Models + */ +export type OpenAiTelemetryAttributeOptions = Telemetry.GenAITelemetryAttributeOptions & { + openai?: { + request?: RequestAttributes | undefined + response?: ResponseAttributes | undefined + } | undefined +} + +const addOpenAiRequestAttributes = Telemetry.addSpanAttributes("gen_ai.openai.request", String.camelToSnake)< + RequestAttributes +> +const addOpenAiResponseAttributes = Telemetry.addSpanAttributes("gen_ai.openai.response", String.camelToSnake)< + ResponseAttributes +> + +/** + * Applies the specified OpenAi GenAI telemetry attributes to the provided + * `Span`. + * + * **NOTE**: This method will mutate the `Span` **in-place**. + * + * @since 1.0.0 + * @since Utilities + */ +export const addGenAIAnnotations = dual< + (options: OpenAiTelemetryAttributeOptions) => (span: Span) => void, + (span: Span, options: OpenAiTelemetryAttributeOptions) => void +>(2, (span, options) => { + Telemetry.addGenAIAnnotations(span, options) + if (Predicate.isNotNullable(options.openai)) { + if (Predicate.isNotNullable(options.openai.request)) { + addOpenAiRequestAttributes(span, options.openai.request) + } + if (Predicate.isNotNullable(options.openai.response)) { + addOpenAiResponseAttributes(span, options.openai.response) + } + } +}) diff --git a/repos/effect/packages/ai/openai/src/OpenAiTokenizer.ts b/repos/effect/packages/ai/openai/src/OpenAiTokenizer.ts new file mode 100644 index 0000000..9203a9f --- /dev/null +++ b/repos/effect/packages/ai/openai/src/OpenAiTokenizer.ts @@ -0,0 +1,70 @@ +/** + * @since 1.0.0 + */ +import * as AiError from "@effect/ai/AiError" +import type * as Prompt from "@effect/ai/Prompt" +import * as Tokenizer from "@effect/ai/Tokenizer" +import * as Effect from "effect/Effect" +import * as Layer from "effect/Layer" +import * as GptTokenizer from "gpt-tokenizer" + +/** + * @since 1.0.0 + * @category Constructors + */ +export const make = (options: { readonly model: string }) => + Tokenizer.make({ + tokenize(prompt) { + return Effect.try({ + try: () => { + const content: Array<{ + readonly role: "assistant" | "system" | "user" + readonly content: string + }> = [] + + for (const message of prompt.content) { + if (message.role === "system") { + content.push({ role: getRole(message), content: message.content }) + continue + } + + for (const part of message.content) { + switch (part.type) { + case "reasoning": + case "text": { + content.push({ role: getRole(message), content: part.text }) + break + } + case "tool-call": { + content.push({ role: getRole(message), content: JSON.stringify(part.params) }) + break + } + case "tool-result": { + content.push({ role: getRole(message), content: JSON.stringify(part.result) }) + break + } + } + } + } + return GptTokenizer.encodeChat(content, options.model as any) + }, + catch: (cause) => + new AiError.UnknownError({ + module: "OpenAiTokenizer", + method: "tokenize", + description: "Could not tokenize", + cause + }) + }) + } + }) + +/** + * @since 1.0.0 + * @category Layers + */ +export const layer = (options: { readonly model: string }): Layer.Layer => + Layer.succeed(Tokenizer.Tokenizer, make(options)) + +const getRole = (message: Prompt.Message): "assistant" | "system" | "user" => + message.role === "tool" ? "assistant" : message.role diff --git a/repos/effect/packages/ai/openai/src/OpenAiTool.ts b/repos/effect/packages/ai/openai/src/OpenAiTool.ts new file mode 100644 index 0000000..136c34f --- /dev/null +++ b/repos/effect/packages/ai/openai/src/OpenAiTool.ts @@ -0,0 +1,110 @@ +/** + * @since 1.0.0 + */ +import * as Tool from "@effect/ai/Tool" +import * as Schema from "effect/Schema" +import * as Struct from "effect/Struct" +import * as Generated from "./Generated.js" + +/** + * @since 1.0.0 + * @category Tools + */ +export const CodeInterpreter = Tool.providerDefined({ + id: "openai.code_interpreter", + toolkitName: "OpenAiCodeInterpreter", + providerName: "code_interpreter", + args: { + /** + * The configuration for the code interpreter container. + * + * Can be either a string which specifies the ID of a container created via + * the `/v1/containers` endpoint, or an object which specifies an optional + * list of files to make available to the container. + */ + container: Schema.Union( + Schema.String, + Schema.Struct({ + type: Schema.Literal("auto"), + /** + * An optional list of uploaded files to make available to your code. + */ + file_ids: Schema.optional(Schema.Array(Schema.String)) + }) + ) + }, + parameters: { + code: Schema.NullOr(Schema.String), + container_id: Schema.String + }, + success: Schema.NullOr(Schema.Array(Schema.Union( + Generated.CodeInterpreterOutputLogs, + Generated.CodeInterpreterOutputImage + ))) +}) + +/** + * @since 1.0.0 + * @category Tools + */ +export const FileSearch = Tool.providerDefined({ + id: "openai.file_search", + toolkitName: "OpenAiFileSearch", + providerName: "file_search", + args: Struct.omit(Generated.FileSearchTool.fields, "type"), + success: Generated.FileSearchToolCall.pipe(Schema.omit("id", "type")) +}) + +/** + * @since 1.0.0 + * @category Tools + */ +export const WebSearch = Tool.providerDefined({ + id: "openai.web_search", + toolkitName: "OpenAiWebSearch", + providerName: "web_search", + args: Struct.omit(Generated.WebSearchTool.fields, "type"), + parameters: { + action: Schema.Union( + Generated.WebSearchActionSearch, + Generated.WebSearchActionOpenPage, + Generated.WebSearchActionFind + ) + }, + success: Schema.Struct({ + status: Generated.WebSearchToolCallStatus + }) +}) + +/** + * @since 1.0.0 + * @category Tools + */ +export const WebSearchPreview = Tool.providerDefined({ + id: "openai.web_search_preview", + toolkitName: "OpenAiWebSearchPreview", + providerName: "web_search_preview", + args: Struct.omit(Generated.WebSearchPreviewTool.fields, "type"), + parameters: { + action: Schema.Union( + Generated.WebSearchActionSearch, + Generated.WebSearchActionOpenPage, + Generated.WebSearchActionFind + ) + }, + success: Schema.Struct({ + status: Generated.WebSearchToolCallStatus + }) +}) + +type ProviderToolNames = "code_interpreter" | "file_search" | "web_search" | "web_search_preview" + +const ProviderToolNamesMap: Map = new Map([ + ["code_interpreter", "OpenAiCodeInterpreter"], + ["file_search", "OpenAiFileSearch"], + ["web_search", "OpenAiWebSearch"], + ["web_search_preview", "OpenAiWebSearchPreview"] +]) + +/** @internal */ +export const getProviderDefinedToolName = (name: string): string | undefined => ProviderToolNamesMap.get(name) diff --git a/repos/effect/packages/ai/openai/src/index.ts b/repos/effect/packages/ai/openai/src/index.ts new file mode 100644 index 0000000..e12b42d --- /dev/null +++ b/repos/effect/packages/ai/openai/src/index.ts @@ -0,0 +1,39 @@ +/** + * @since 1.0.0 + */ +export * as Generated from "./Generated.js" + +/** + * @since 1.0.0 + */ +export * as OpenAiClient from "./OpenAiClient.js" + +/** + * @since 1.0.0 + */ +export * as OpenAiConfig from "./OpenAiConfig.js" + +/** + * @since 1.0.0 + */ +export * as OpenAiEmbeddingModel from "./OpenAiEmbeddingModel.js" + +/** + * @since 1.0.0 + */ +export * as OpenAiLanguageModel from "./OpenAiLanguageModel.js" + +/** + * @since 1.0.0 + */ +export * as OpenAiTelemetry from "./OpenAiTelemetry.js" + +/** + * @since 1.0.0 + */ +export * as OpenAiTokenizer from "./OpenAiTokenizer.js" + +/** + * @since 1.0.0 + */ +export * as OpenAiTool from "./OpenAiTool.js" diff --git a/repos/effect/packages/ai/openai/src/internal/utilities.ts b/repos/effect/packages/ai/openai/src/internal/utilities.ts new file mode 100644 index 0000000..076b03c --- /dev/null +++ b/repos/effect/packages/ai/openai/src/internal/utilities.ts @@ -0,0 +1,31 @@ +import type * as Response from "@effect/ai/Response" +import * as Predicate from "effect/Predicate" + +/** @internal */ +export const ProviderOptionsKey = "@effect/ai-openai/OpenAiLanguageModel/ProviderOptions" + +/** @internal */ +export const ProviderMetadataKey = "@effect/ai-openai/OpenAiLanguageModel/ProviderMetadata" + +const finishReasonMap: Record = { + content_filter: "content-filter", + function_call: "tool-calls", + length: "length", + stop: "stop", + tool_calls: "tool-calls" +} + +/** @internal */ +export const resolveFinishReason = ( + finishReason: string | undefined, + hasToolCalls: boolean +): Response.FinishReason => { + if (Predicate.isNullable(finishReason)) { + return hasToolCalls ? "tool-calls" : "stop" + } + const reason = finishReasonMap[finishReason] + if (Predicate.isNullable(reason)) { + return hasToolCalls ? "tool-calls" : "unknown" + } + return reason +} diff --git a/repos/effect/packages/ai/openai/tsconfig.build.json b/repos/effect/packages/ai/openai/tsconfig.build.json new file mode 100644 index 0000000..4021ab2 --- /dev/null +++ b/repos/effect/packages/ai/openai/tsconfig.build.json @@ -0,0 +1,16 @@ +{ + "extends": "./tsconfig.src.json", + "references": [ + { "path": "../ai/tsconfig.build.json" }, + { "path": "../../effect/tsconfig.build.json" }, + { "path": "../../experimental/tsconfig.build.json" }, + { "path": "../../platform/tsconfig.build.json" } + ], + "compilerOptions": { + "tsBuildInfoFile": ".tsbuildinfo/build.tsbuildinfo", + "outDir": "build/esm", + "declarationDir": "build/dts", + "stripInternal": true, + "exactOptionalPropertyTypes": false + } +} diff --git a/repos/effect/packages/ai/openai/tsconfig.json b/repos/effect/packages/ai/openai/tsconfig.json new file mode 100644 index 0000000..f446496 --- /dev/null +++ b/repos/effect/packages/ai/openai/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../../tsconfig.base.json", + "include": [], + "references": [ + { "path": "tsconfig.src.json" }, + { "path": "tsconfig.test.json" } + ] +} diff --git a/repos/effect/packages/ai/openai/tsconfig.src.json b/repos/effect/packages/ai/openai/tsconfig.src.json new file mode 100644 index 0000000..01ab4fc --- /dev/null +++ b/repos/effect/packages/ai/openai/tsconfig.src.json @@ -0,0 +1,16 @@ +{ + "extends": "../../../tsconfig.base.json", + "include": ["src"], + "references": [ + { "path": "../ai/tsconfig.src.json" }, + { "path": "../../effect/tsconfig.src.json" }, + { "path": "../../experimental/tsconfig.src.json" }, + { "path": "../../platform/tsconfig.src.json" } + ], + "compilerOptions": { + "tsBuildInfoFile": ".tsbuildinfo/src.tsbuildinfo", + "rootDir": "src", + "outDir": "build/src", + "exactOptionalPropertyTypes": false + } +} diff --git a/repos/effect/packages/ai/openai/tsconfig.test.json b/repos/effect/packages/ai/openai/tsconfig.test.json new file mode 100644 index 0000000..95f3ae8 --- /dev/null +++ b/repos/effect/packages/ai/openai/tsconfig.test.json @@ -0,0 +1,14 @@ +{ + "extends": "../../../tsconfig.base.json", + "include": ["test"], + "references": [ + { "path": "tsconfig.src.json" }, + { "path": "../../vitest/tsconfig.src.json" } + ], + "compilerOptions": { + "tsBuildInfoFile": ".tsbuildinfo/test.tsbuildinfo", + "rootDir": "test", + "noEmit": true, + "exactOptionalPropertyTypes": false + } +} diff --git a/repos/effect/packages/ai/openai/vitest.config.ts b/repos/effect/packages/ai/openai/vitest.config.ts new file mode 100644 index 0000000..bf29895 --- /dev/null +++ b/repos/effect/packages/ai/openai/vitest.config.ts @@ -0,0 +1,6 @@ +import { mergeConfig, type ViteUserConfig } from "vitest/config" +import shared from "../../../vitest.shared.js" + +const config: ViteUserConfig = {} + +export default mergeConfig(shared, config) diff --git a/repos/effect/packages/ai/openrouter/CHANGELOG.md b/repos/effect/packages/ai/openrouter/CHANGELOG.md new file mode 100644 index 0000000..0ad9823 --- /dev/null +++ b/repos/effect/packages/ai/openrouter/CHANGELOG.md @@ -0,0 +1,286 @@ +# @effect/ai-openrouter + +## 0.10.1 + +### Patch Changes + +- [#6145](https://github.com/Effect-TS/effect/pull/6145) [`6c39a34`](https://github.com/Effect-TS/effect/commit/6c39a34c6145811f5c41292f03bf7939cfa8e70d) Thanks @LikiosSedo! - Fix typo in HTTP header name: `HTTP-Referrer` → `HTTP-Referer`. The HTTP spec spells it "Referer" (single r), and OpenRouter expects this exact header name for app attribution. + +- Updated dependencies [[`f99048e`](https://github.com/Effect-TS/effect/commit/f99048e9f4b89ce1afe31e1827dee5d751ddaa5b)]: + - effect@3.21.1 + +## 0.10.0 + +### Patch Changes + +- Updated dependencies [[`f7bb09b`](https://github.com/Effect-TS/effect/commit/f7bb09b022f195d1f2b3c23d49e74b011ec5d109), [`bd7552a`](https://github.com/Effect-TS/effect/commit/bd7552a19cc0ed575507ac6cc0879a57e24ebd31), [`ad1a7eb`](https://github.com/Effect-TS/effect/commit/ad1a7eb7f6bebaf91c80be2443ac0439226d0098), [`0d32048`](https://github.com/Effect-TS/effect/commit/0d32048f9836e2b23a6ba3ec5f43f0a000bb92fb), [`0d32048`](https://github.com/Effect-TS/effect/commit/0d32048f9836e2b23a6ba3ec5f43f0a000bb92fb)]: + - effect@3.21.0 + - @effect/ai@0.35.0 + - @effect/experimental@0.60.0 + - @effect/platform@0.96.0 + +## 0.9.1 + +### Patch Changes + +- [#6131](https://github.com/Effect-TS/effect/pull/6131) [`5c80e57`](https://github.com/Effect-TS/effect/commit/5c80e578bd95e0cf6fceffc72fa0b130ca11ec8e) Thanks @fabstorres! - Allow partial tool_call deltas in OpenRouter streaming + +- Updated dependencies [[`add06f4`](https://github.com/Effect-TS/effect/commit/add06f4521403cbf4b9a692f9b59fb9d3d48293c), [`a03b6a2`](https://github.com/Effect-TS/effect/commit/a03b6a29ed0b983b0440b8ef4be47f47c57d73d7)]: + - effect@3.20.1 + +## 0.9.0 + +### Patch Changes + +- [#6117](https://github.com/Effect-TS/effect/pull/6117) [`7103e24`](https://github.com/Effect-TS/effect/commit/7103e2473db805cc9f0024d4744c77c16d81e2f1) Thanks @nickbreaton! - Fix OpenRouter streaming finalization for usage-only terminal chunks. + +- Updated dependencies [[`fc82e81`](https://github.com/Effect-TS/effect/commit/fc82e81448bd9136a37580139ce46a2c61b11b54), [`82996bc`](https://github.com/Effect-TS/effect/commit/82996bce8debffcb44feb98bb862cf2662bd56b7), [`4d97a61`](https://github.com/Effect-TS/effect/commit/4d97a61a15b9dd6a0eece65b8f0c035e16d42ada), [`f6b0960`](https://github.com/Effect-TS/effect/commit/f6b0960bf3184109920dfed16ee7dfd7d67bc0f2), [`8798a84`](https://github.com/Effect-TS/effect/commit/8798a843218e6c0c0d3a8eee83360880e370b4da)]: + - effect@3.20.0 + - @effect/ai@0.34.0 + - @effect/experimental@0.59.0 + - @effect/platform@0.95.0 + +## 0.8.4 + +### Patch Changes + +- [#6071](https://github.com/Effect-TS/effect/pull/6071) [`a91364d`](https://github.com/Effect-TS/effect/commit/a91364d0ef48de0f66afe801c4da13bfe8a5aeed) Thanks @marbemac! - Fix `ChatStreamingMessageToolCall` schema rejecting valid streaming tool call chunks. + + The OpenAI streaming spec splits tool calls across multiple SSE chunks — `function.name` is only present on the first chunk, but the schema required it on every chunk, causing a `MalformedOutput` error whenever the model returned a tool call. + + Made `function.name` optional to match `id` which was already optional. + +## 0.8.3 + +### Patch Changes + +- [#6060](https://github.com/Effect-TS/effect/pull/6060) [`c3e706f`](https://github.com/Effect-TS/effect/commit/c3e706ff4d01c70ae1754b13c9cbc1f001c09068) Thanks @nvonbulow! - fix(ai-openrouter): deduplicate reasoning parts when both `reasoning` and `reasoning_details` are present in a stream delta + +- Updated dependencies [[`d67c708`](https://github.com/Effect-TS/effect/commit/d67c7089ba8616b2d48ef7324312267a2a6f310a), [`a8c436f`](https://github.com/Effect-TS/effect/commit/a8c436f7004cc2a8ce2daec589ea7256b91c324f)]: + - @effect/platform@0.94.5 + - effect@3.19.17 + +## 0.8.2 + +### Patch Changes + +- [#6026](https://github.com/Effect-TS/effect/pull/6026) [`38241de`](https://github.com/Effect-TS/effect/commit/38241dee2319d051f3ab15781f73f838d626ac24) Thanks @IMax153! - Fix the OpenRouter AI provider schemas + +- Updated dependencies [[`0023c19`](https://github.com/Effect-TS/effect/commit/0023c19c63c402c050d496817ba92aceea7f25b7), [`e71889f`](https://github.com/Effect-TS/effect/commit/e71889f35b081d13b7da2c04d2f81d6933056b49), [`9a96b87`](https://github.com/Effect-TS/effect/commit/9a96b87a33a75ebc277c585e60758ab4409c0d9e)]: + - @effect/platform@0.94.3 + - effect@3.19.16 + +## 0.8.1 + +### Patch Changes + +- [#5928](https://github.com/Effect-TS/effect/pull/5928) [`34fbbb1`](https://github.com/Effect-TS/effect/commit/34fbbb18e34cbad6ee5f0f396b3e27ba590925b8) Thanks @harrysolovay! - Regenerate OpenRouter schemas to fix schema validation. + +- Updated dependencies [[`65e9e35`](https://github.com/Effect-TS/effect/commit/65e9e35157cbdfb40826ddad34555c4ebcf7c0b0), [`ee69cd7`](https://github.com/Effect-TS/effect/commit/ee69cd796feb3d8d1046f52edd8950404cd4ed0e), [`488d6e8`](https://github.com/Effect-TS/effect/commit/488d6e870eda3dfc137f4940bb69416f61ed8fe3), [`ba9e790`](https://github.com/Effect-TS/effect/commit/ba9e7908a80a55f24217c88af4f7d89a4f7bc0e4)]: + - @effect/platform@0.94.1 + - effect@3.19.14 + - @effect/ai@0.33.1 + +## 0.8.0 + +### Patch Changes + +- Updated dependencies [[`77eeb86`](https://github.com/Effect-TS/effect/commit/77eeb86ddf208e51ec25932af83d52d3b4700371), [`ff7053f`](https://github.com/Effect-TS/effect/commit/ff7053f6d8508567b6145239f97aacc5773b0c53), [`287c32c`](https://github.com/Effect-TS/effect/commit/287c32c9f10da8e96f2b9ef8424316189d9ad4b3)]: + - effect@3.19.13 + - @effect/platform@0.94.0 + - @effect/ai@0.33.0 + - @effect/experimental@0.58.0 + +## 0.7.1 + +### Patch Changes + +- [#5799](https://github.com/Effect-TS/effect/pull/5799) [`5d7c9d8`](https://github.com/Effect-TS/effect/commit/5d7c9d8bb89b955b79303e7445c713ce56b06977) Thanks @subtleGradient! - Add support for google-gemini-v1 reasoning format + +- Updated dependencies [[`65bff45`](https://github.com/Effect-TS/effect/commit/65bff451fc54d47b32995b3bc898ccc5f8b1beb6)]: + - @effect/platform@0.93.7 + +## 0.7.0 + +### Minor Changes + +- [#5849](https://github.com/Effect-TS/effect/pull/5849) [`2dcbf98`](https://github.com/Effect-TS/effect/commit/2dcbf98b0b426536f71dfb33cbe6f310d7ad4e77) Thanks @IMax153! - Update generated schema definitions and apply patch fixes + +### Patch Changes + +- Updated dependencies [[`96c9537`](https://github.com/Effect-TS/effect/commit/96c9537f73a87a651c348488bdce7efbfd8360d1)]: + - @effect/experimental@0.57.10 + +## 0.6.0 + +### Patch Changes + +- Updated dependencies [[`3c15d5f`](https://github.com/Effect-TS/effect/commit/3c15d5f99fb8d8470a00c5a33d9ba3cac89dfe4c), [`3863fa8`](https://github.com/Effect-TS/effect/commit/3863fa89f61e63e5529fd961e37333bddf7db64a), [`2a03c76`](https://github.com/Effect-TS/effect/commit/2a03c76c2781ca7e9e228e838eab2eb0d0795b1d), [`24a1685`](https://github.com/Effect-TS/effect/commit/24a1685c70a9ed157468650f95a5c3da3f2c2433)]: + - effect@3.19.0 + - @effect/platform@0.93.0 + - @effect/ai@0.32.0 + - @effect/experimental@0.57.0 + +## 0.5.0 + +### Minor Changes + +- [#5621](https://github.com/Effect-TS/effect/pull/5621) [`4c3bdfb`](https://github.com/Effect-TS/effect/commit/4c3bdfbcbc2dcd7ecd6321df3e4a504af19de825) Thanks @IMax153! - Remove `Either` / `EitherEncoded` from tool call results. + + Specifically, the encoding of tool call results as an `Either` / `EitherEncoded` has been removed and is replaced by encoding the tool call success / failure directly into the `result` property. + + To allow type-safe discrimination between a tool call result which was a success vs. one that was a failure, an `isFailure` property has also been added to the `"tool-result"` part. If `isFailure` is `true`, then the tool call handler result was an error. + + ```ts + import * as AnthropicClient from "@effect/ai-anthropic/AnthropicClient" + import * as AnthropicLanguageModel from "@effect/ai-anthropic/AnthropicLanguageModel" + import * as LanguageModel from "@effect/ai/LanguageModel" + import * as Tool from "@effect/ai/Tool" + import * as Toolkit from "@effect/ai/Toolkit" + import * as NodeHttpClient from "@effect/platform-node/NodeHttpClient" + import { Config, Effect, Layer, Schema, Stream } from "effect" + + const Claude = AnthropicLanguageModel.model("claude-4-sonnet-20250514") + + const MyTool = Tool.make("MyTool", { + description: "An example of a tool with success and failure types", + failureMode: "return", // Return errors in the response + parameters: { bar: Schema.Number }, + success: Schema.Number, + failure: Schema.Struct({ reason: Schema.Literal("reason-1", "reason-2") }) + }) + + const MyToolkit = Toolkit.make(MyTool) + + const MyToolkitLayer = MyToolkit.toLayer({ + MyTool: () => Effect.succeed(42) + }) + + const program = LanguageModel.streamText({ + prompt: "Tell me about the meaning of life", + toolkit: MyToolkit + }).pipe( + Stream.runForEach((part) => { + if (part.type === "tool-result" && part.name === "MyTool") { + // The `isFailure` property can be used to discriminate whether the result + // of a tool call is a success or a failure + if (part.isFailure) { + part.result + // ^? { readonly reason: "reason-1" | "reason-2"; } + } else { + part.result + // ^? number + } + } + return Effect.void + }), + Effect.provide(Claude) + ) + + const Anthropic = AnthropicClient.layerConfig({ + apiKey: Config.redacted("ANTHROPIC_API_KEY") + }).pipe(Layer.provide(NodeHttpClient.layerUndici)) + + program.pipe(Effect.provide([Anthropic, MyToolkitLayer]), Effect.runPromise) + ``` + +### Patch Changes + +- Updated dependencies [[`4c3bdfb`](https://github.com/Effect-TS/effect/commit/4c3bdfbcbc2dcd7ecd6321df3e4a504af19de825)]: + - @effect/ai@0.31.0 + +## 0.4.0 + +### Minor Changes + +- [#5614](https://github.com/Effect-TS/effect/pull/5614) [`c63e658`](https://github.com/Effect-TS/effect/commit/c63e6582244fbb50d31650c4b4ea0660fe194652) Thanks @IMax153! - Previously, tool call handler errors were _always_ raised as an expected error in the Effect `E` channel at the point of execution of the tool call handler (i.e. when a `generate*` method is invoked on a `LanguageModel`). + + With this PR, the end user now has control over whether tool call handler errors should be raised as an Effect error, or returned by the SDK to allow, for example, sending that error information to another application. + + ### Tool Call Specification + + The `Tool.make` and `Tool.providerDefined` constructors now take an extra optional parameter called `failureMode`, which can be set to either `"error"` or `"return"`. + + ```ts + import { Tool } from "@effect/ai" + import { Schema } from "effect" + + const MyTool = Tool.make("MyTool", { + description: "My special tool", + failureMode: "return" // "error" (default) or "return" + parameters: { + myParam: Schema.String + }, + success: Schema.Struct({ + mySuccess: Schema.String + }), + failure: Schema.Struct({ + myFailure: Schema.String + }) + }) + + ``` + + The semantics of `failureMode` are as follows: + - If set to `"error"` (the default), errors that occur during tool call handler execution will be returned in the error channel of the calling effect + - If set to `"return"`, errors that occur during tool call handler execution will be captured and returned as part of the tool call result + + ### Response - Tool Result Parts + + The `result` field of a `"tool-result"` part of a large language model provider response is now represented as an `Either`. + - If the `result` is a `Left`, the `result` will be the `failure` specified in the tool call specification + - If the `result` is a `Right`, the `result` will be the `success` specified in the tool call specification + + This is only relevant if the end user sets `failureMode` to `"return"`. If set to `"error"` (the default), then the `result` property will always be a `Right` with the successful result of the tool call handler. + + Similarly the `encodedResult` field of a `"tool-result"` part will be represented as an `EitherEncoded`, where: + - `{ _tag: "Left", left: }` represents a tool call handler failure + - `{ _tag: "Right", right: }` represents a tool call handler success + + ### Prompt - Tool Result Parts + + The `result` field of a `"tool-result"` part of a prompt will now only accept an `EitherEncoded` as specified above. + +### Patch Changes + +- Updated dependencies [[`6ae2f5d`](https://github.com/Effect-TS/effect/commit/6ae2f5da45a9ed9832605eca12b3e2bf2e2a1a67), [`c63e658`](https://github.com/Effect-TS/effect/commit/c63e6582244fbb50d31650c4b4ea0660fe194652)]: + - effect@3.18.4 + - @effect/ai@0.30.0 + +## 0.3.0 + +### Patch Changes + +- Updated dependencies [[`1c6ab74`](https://github.com/Effect-TS/effect/commit/1c6ab74b314b2b6df8bb1b1a0cb9527ceda0e3fa), [`70fe803`](https://github.com/Effect-TS/effect/commit/70fe803469db3355ffbf8359b52c351f1c2dc137), [`c296e32`](https://github.com/Effect-TS/effect/commit/c296e32554143b84ae8987046984e1cf1852417c), [`a098ddf`](https://github.com/Effect-TS/effect/commit/a098ddfc551f5aa0a7c36f9b4928372a64d4d9f2), [`f8b93ac`](https://github.com/Effect-TS/effect/commit/f8b93ac6446efd3dd790778b0fc71d299a38f272)]: + - effect@3.18.0 + - @effect/ai@0.29.0 + - @effect/platform@0.92.0 + - @effect/experimental@0.56.0 + +## 0.2.1 + +### Patch Changes + +- [#5571](https://github.com/Effect-TS/effect/pull/5571) [`122aa53`](https://github.com/Effect-TS/effect/commit/122aa53058ff008cf605cc2f0f0675a946c3cae9) Thanks @IMax153! - Ensure that AI provider clients filter response status for stream requests + +## 0.2.0 + +### Patch Changes + +- Updated dependencies [[`d4d86a8`](https://github.com/Effect-TS/effect/commit/d4d86a81f02b94e09fce8004ce2c5369c505ca5a)]: + - @effect/platform@0.91.0 + - @effect/ai@0.28.0 + - @effect/experimental@0.55.0 + +## 0.1.0 + +### Minor Changes + +- [#5521](https://github.com/Effect-TS/effect/pull/5521) [`fa49bc8`](https://github.com/Effect-TS/effect/commit/fa49bc86b14599300d106f306ceaf82a79121b80) Thanks @IMax153! - Add Effect AI SDK provider integration package for OpenRouter + +### Patch Changes + +- [#5521](https://github.com/Effect-TS/effect/pull/5521) [`fa49bc8`](https://github.com/Effect-TS/effect/commit/fa49bc86b14599300d106f306ceaf82a79121b80) Thanks @IMax153! - Fix provider metadata and parse tool call parameters safely + +- Updated dependencies [[`fa49bc8`](https://github.com/Effect-TS/effect/commit/fa49bc86b14599300d106f306ceaf82a79121b80)]: + - @effect/ai@0.27.1 diff --git a/repos/effect/packages/ai/openrouter/LICENSE b/repos/effect/packages/ai/openrouter/LICENSE new file mode 100644 index 0000000..be1f5c1 --- /dev/null +++ b/repos/effect/packages/ai/openrouter/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Effectful Technologies Inc + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/repos/effect/packages/ai/openrouter/README.md b/repos/effect/packages/ai/openrouter/README.md new file mode 100644 index 0000000..6db1204 --- /dev/null +++ b/repos/effect/packages/ai/openrouter/README.md @@ -0,0 +1,5 @@ +# `@effect/ai-openrouter` + +## Documentation + +- **API Reference**: [View the full documentation](https://effect-ts.github.io/effect/docs/ai/openrouter). diff --git a/repos/effect/packages/ai/openrouter/docgen.json b/repos/effect/packages/ai/openrouter/docgen.json new file mode 100644 index 0000000..fc7b23a --- /dev/null +++ b/repos/effect/packages/ai/openrouter/docgen.json @@ -0,0 +1,24 @@ +{ + "$schema": "../../../node_modules/@effect/docgen/schema.json", + "exclude": ["src/Generated.ts", "src/internal/**/*.ts"], + "srcLink": "https://github.com/Effect-TS/effect/tree/main/packages/ai/openrouter/src/", + "examplesCompilerOptions": { + "noEmit": true, + "strict": true, + "skipLibCheck": true, + "moduleResolution": "Bundler", + "module": "ES2022", + "target": "ES2022", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "paths": { + "effect": ["../../../../effect/src/index.js"], + "effect/*": ["../../../../effect/src/*.js"], + "@effect/experimental": ["../../../../experimental/src/index.js"], + "@effect/experimental/*": ["../../../../experimental/src/*.js"], + "@effect/platform": ["../../../../platform/src/index.js"], + "@effect/platform/*": ["../../../../platform/src/*.js"], + "@effect/ai": ["../../../ai/src/index.js"], + "@effect/ai/*": ["../../../ai/src/*.js"] + } + } +} diff --git a/repos/effect/packages/ai/openrouter/package.json b/repos/effect/packages/ai/openrouter/package.json new file mode 100644 index 0000000..540003d --- /dev/null +++ b/repos/effect/packages/ai/openrouter/package.json @@ -0,0 +1,62 @@ +{ + "name": "@effect/ai-openrouter", + "type": "module", + "version": "0.10.1", + "license": "MIT", + "description": "Effect modules for working with AI apis", + "homepage": "https://effect.website", + "repository": { + "type": "git", + "url": "https://github.com/Effect-TS/effect.git", + "directory": "packages/ai/openrouter" + }, + "bugs": { + "url": "https://github.com/Effect-TS/effect/issues" + }, + "tags": [ + "typescript", + "algebraic-data-types", + "functional-programming" + ], + "keywords": [ + "typescript", + "algebraic-data-types", + "functional-programming" + ], + "publishConfig": { + "access": "public", + "provenance": true, + "directory": "dist", + "linkDirectory": false + }, + "exports": { + "./package.json": "./package.json", + ".": "./src/index.ts", + "./*": "./src/*.ts", + "./internal/*": null + }, + "scripts": { + "codegen": "build-utils prepare-v3", + "build": "pnpm build-esm && pnpm build-annotate && pnpm build-cjs && build-utils pack-v3", + "build-esm": "tsc -b tsconfig.build.json", + "build-cjs": "babel build/esm --plugins @babel/transform-export-namespace-from --plugins @babel/transform-modules-commonjs --out-dir build/cjs --source-maps", + "build-annotate": "babel build/esm --plugins annotate-pure-calls --out-dir build/esm --source-maps", + "check": "tsc -b tsconfig.json", + "test": "vitest", + "coverage": "vitest --coverage" + }, + "peerDependencies": { + "@effect/ai": "workspace:^", + "@effect/experimental": "workspace:^", + "@effect/platform": "workspace:^", + "effect": "workspace:^" + }, + "devDependencies": { + "@effect/ai": "workspace:^", + "@effect/experimental": "workspace:^", + "@effect/platform": "workspace:^", + "@effect/platform-node": "workspace:^", + "@tim-smart/openapi-gen": "^0.4.10", + "effect": "workspace:^" + } +} diff --git a/repos/effect/packages/ai/openrouter/scripts/generate.sh b/repos/effect/packages/ai/openrouter/scripts/generate.sh new file mode 100755 index 0000000..e46f3ce --- /dev/null +++ b/repos/effect/packages/ai/openrouter/scripts/generate.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash + +set -euo pipefail + +SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &> /dev/null && pwd) + +temp_dir=$(mktemp -d) + +cleanup() { + rm -rf "${temp_dir}" +} + +trap cleanup EXIT + +openapi_spec_url="https://openrouter.ai/openapi.yaml" +# openapi_spec_url="https://spec.speakeasy.com/openrouter/sdk/open-router-chat-completions-api-with-code-samples" +temp_file="${temp_dir}/openrouter.yaml" + +touch "${temp_file}" +curl "${openapi_spec_url}" > "${temp_file}" + +echo "/** + * @since 1.0.0 + */" > src/Generated.ts + +pnpm openapi-gen -s "${temp_file}" >> src/Generated.ts + +pnpm eslint --fix src/Generated.ts + +git apply --reject --whitespace=fix "${SCRIPT_DIR}/generated.patch" diff --git a/repos/effect/packages/ai/openrouter/scripts/generated.patch b/repos/effect/packages/ai/openrouter/scripts/generated.patch new file mode 100644 index 0000000..bb96a0b --- /dev/null +++ b/repos/effect/packages/ai/openrouter/scripts/generated.patch @@ -0,0 +1,142 @@ +diff --git a/packages/ai/openrouter/src/Generated.ts b/packages/ai/openrouter/src/Generated.ts +index 92a756c..837d7dd 100644 +--- a/packages/ai/openrouter/src/Generated.ts ++++ b/packages/ai/openrouter/src/Generated.ts +@@ -10,6 +10,10 @@ import * as Effect from "effect/Effect" + import type { ParseError } from "effect/ParseResult" + import * as S from "effect/Schema" + ++export class CacheControlEphemeral extends S.Class("CacheControlEphemeral")({ ++ "type": S.Literal("ephemeral") ++}) {} ++ + export class OpenResponsesReasoningFormat extends S.Literal( + "unknown", + "openai-responses-v1", +@@ -51,6 +55,70 @@ export class OpenResponsesReasoning extends S.Class("Ope + ) + }) {} + ++export class ReasoningDetailSummary extends S.Class("ReasoningDetailSummary")({ ++ id: S.optionalWith(S.String, { nullable: true }), ++ type: S.Literal("reasoning.summary"), ++ index: S.optional(S.Number), ++ format: S.optionalWith(OpenResponsesReasoningFormat, { nullable: true }), ++ summary: S.String ++}) {} ++ ++export class ReasoningDetailEncrypted extends S.Class("ReasoningDetailEncrypted")({ ++ id: S.optionalWith(S.String, { nullable: true }), ++ type: S.Literal("reasoning.encrypted"), ++ index: S.optional(S.Number), ++ format: S.optionalWith(OpenResponsesReasoningFormat, { nullable: true }), ++ data: S.String ++}) {} ++ ++export class ReasoningDetailText extends S.Class("ReasoningDetailText")({ ++ id: S.optionalWith(S.String, { nullable: true }), ++ type: S.Literal("reasoning.text"), ++ index: S.optional(S.Number), ++ format: S.optionalWith(OpenResponsesReasoningFormat, { nullable: true }), ++ text: S.optionalWith(S.String, { nullable: true }), ++ signature: S.optionalWith(S.String, { nullable: true }) ++}) {} ++ ++export class ReasoningDetail extends S.Union( ++ ReasoningDetailSummary, ++ ReasoningDetailEncrypted, ++ ReasoningDetailText ++) {} ++ ++export class FileAnnotationDetail extends S.Class("FileAnnotationDetail")({ ++ "type": S.Literal("file"), ++ "file": S.Struct({ ++ "hash": S.String, ++ "name": S.optionalWith(S.String, { nullable: true }), ++ "content": S.Array(S.Union( ++ S.Struct({ ++ "type": S.Literal("text"), ++ "text": S.String ++ }), ++ S.Struct({ ++ "type": S.Literal("image_url"), ++ "image_url": S.Struct({ ++ "url": S.String ++ }) ++ }) ++ )) ++ }) ++}) {} ++ ++export class URLCitationAnnotationDetail extends S.Class("URLCitationAnnotationDetail")({ ++ "type": S.Literal("url_citation"), ++ "url_citation": S.Struct({ ++ "end_index": S.Number, ++ "start_index": S.Number, ++ "title": S.String, ++ "url": S.String, ++ "content": S.optionalWith(S.String, { nullable: true }) ++ }) ++}) {} ++ ++export class AnnotationDetail extends S.Union(FileAnnotationDetail, URLCitationAnnotationDetail) {} ++ + export class OpenResponsesEasyInputMessageType extends S.Literal("message") {} + + export class OpenResponsesEasyInputMessageRoleEnum extends S.Literal("developer") {} +@@ -4637,7 +4705,7 @@ export class AssistantMessage extends S.Class("AssistantMessag + "tool_calls": S.optionalWith(S.Array(ChatMessageToolCall), { nullable: true }), + "refusal": S.optionalWith(S.String, { nullable: true }), + "reasoning": S.optionalWith(S.String, { nullable: true }), +- "reasoning_details": S.optionalWith(S.Array(Schema2), { nullable: true }), ++ "reasoning_details": S.optionalWith(S.Array(ReasoningDetail), { nullable: true }), + "images": S.optionalWith( + S.Array(S.Struct({ + "image_url": S.Struct({ +@@ -4645,7 +4713,8 @@ export class AssistantMessage extends S.Class("AssistantMessag + }) + })), + { nullable: true } +- ) ++ ), ++ "annotations": S.optionalWith(S.Array(AnnotationDetail), { nullable: true }) + }) {} + + export class ToolResponseMessage extends S.Class("ToolResponseMessage")({ +@@ -4873,15 +4942,15 @@ export class ChatMessageTokenLogprob extends S.Class("C + }) {} + + export class ChatMessageTokenLogprobs extends S.Class("ChatMessageTokenLogprobs")({ +- "content": S.NullOr(S.Array(ChatMessageTokenLogprob)), +- "refusal": S.NullOr(S.Array(ChatMessageTokenLogprob)) ++ "content": S.optionalWith(S.Array(ChatMessageTokenLogprob), { nullable: true }), ++ "refusal": S.optionalWith(S.Array(ChatMessageTokenLogprob), { nullable: true }) + }) {} + + export class ChatResponseChoice extends S.Class("ChatResponseChoice")({ + "finish_reason": S.NullOr(ChatCompletionFinishReason), + "index": S.Number, + "message": AssistantMessage, +- "logprobs": S.optionalWith(ChatMessageTokenLogprobs, { nullable: true }) ++ "logprobs": S.optionalWith(ChatMessageTokenLogprobs, { nullable: true }), + }) {} + + export class ChatGenerationTokenUsage extends S.Class("ChatGenerationTokenUsage")({ +@@ -4905,11 +4974,16 @@ export class ChatGenerationTokenUsage extends S.Class( + "video_tokens": S.optionalWith(S.Number, { nullable: true }) + }), + { nullable: true } +- ) ++ ), ++ "cost": S.optionalWith(S.Number, { nullable: true }), ++ "cost_details": S.optionalWith(S.Struct({ upstream_inference_cost: S.optionalWith(S.Number, { nullable: true }) }), { ++ nullable: true ++ }) + }) {} + + export class ChatResponse extends S.Class("ChatResponse")({ + "id": S.String, ++ "provider": S.optionalWith(S.String, { nullable: true }), + "choices": S.Array(ChatResponseChoice), + "created": S.Number, + "model": S.String, diff --git a/repos/effect/packages/ai/openrouter/src/Generated.ts b/repos/effect/packages/ai/openrouter/src/Generated.ts new file mode 100644 index 0000000..5ddbf14 --- /dev/null +++ b/repos/effect/packages/ai/openrouter/src/Generated.ts @@ -0,0 +1,6081 @@ +/** + * @since 1.0.0 + */ +import type * as HttpClient from "@effect/platform/HttpClient" +import * as HttpClientError from "@effect/platform/HttpClientError" +import * as HttpClientRequest from "@effect/platform/HttpClientRequest" +import * as HttpClientResponse from "@effect/platform/HttpClientResponse" +import * as Data from "effect/Data" +import * as Effect from "effect/Effect" +import type { ParseError } from "effect/ParseResult" +import * as S from "effect/Schema" + +export class CacheControlEphemeral extends S.Class("CacheControlEphemeral")({ + "type": S.Literal("ephemeral") +}) {} + +export class OpenResponsesReasoningFormat extends S.Literal( + "unknown", + "openai-responses-v1", + "azure-openai-responses-v1", + "xai-responses-v1", + "anthropic-claude-v1", + "google-gemini-v1" +) {} + +export class OpenResponsesReasoningType extends S.Literal("reasoning") {} + +export class ReasoningTextContentType extends S.Literal("reasoning_text") {} + +export class ReasoningTextContent extends S.Class("ReasoningTextContent")({ + "type": ReasoningTextContentType, + "text": S.String +}) {} + +export class ReasoningSummaryTextType extends S.Literal("summary_text") {} + +export class ReasoningSummaryText extends S.Class("ReasoningSummaryText")({ + "type": ReasoningSummaryTextType, + "text": S.String +}) {} + +export class OpenResponsesReasoningStatusEnum extends S.Literal("in_progress") {} + +export class OpenResponsesReasoning extends S.Class("OpenResponsesReasoning")({ + "signature": S.optionalWith(S.String, { nullable: true }), + "format": S.optionalWith(OpenResponsesReasoningFormat, { nullable: true }), + "type": OpenResponsesReasoningType, + "id": S.String, + "content": S.optionalWith(S.Array(ReasoningTextContent), { nullable: true }), + "summary": S.Array(ReasoningSummaryText), + "encrypted_content": S.optionalWith(S.String, { nullable: true }), + "status": S.optionalWith( + S.Union(OpenResponsesReasoningStatusEnum, OpenResponsesReasoningStatusEnum, OpenResponsesReasoningStatusEnum), + { nullable: true } + ) +}) {} + +export class ReasoningDetailSummary extends S.Class("ReasoningDetailSummary")({ + id: S.optionalWith(S.String, { nullable: true }), + type: S.Literal("reasoning.summary"), + index: S.optional(S.Number), + format: S.optionalWith(OpenResponsesReasoningFormat, { nullable: true }), + summary: S.String +}) {} + +export class ReasoningDetailEncrypted extends S.Class("ReasoningDetailEncrypted")({ + id: S.optionalWith(S.String, { nullable: true }), + type: S.Literal("reasoning.encrypted"), + index: S.optional(S.Number), + format: S.optionalWith(OpenResponsesReasoningFormat, { nullable: true }), + data: S.String +}) {} + +export class ReasoningDetailText extends S.Class("ReasoningDetailText")({ + id: S.optionalWith(S.String, { nullable: true }), + type: S.Literal("reasoning.text"), + index: S.optional(S.Number), + format: S.optionalWith(OpenResponsesReasoningFormat, { nullable: true }), + text: S.optionalWith(S.String, { nullable: true }), + signature: S.optionalWith(S.String, { nullable: true }) +}) {} + +export class ReasoningDetail extends S.Union( + ReasoningDetailSummary, + ReasoningDetailEncrypted, + ReasoningDetailText +) {} + +export class FileAnnotationDetail extends S.Class("FileAnnotationDetail")({ + "type": S.Literal("file"), + "file": S.Struct({ + "hash": S.String, + "name": S.optionalWith(S.String, { nullable: true }), + "content": S.Array(S.Union( + S.Struct({ + "type": S.Literal("text"), + "text": S.String + }), + S.Struct({ + "type": S.Literal("image_url"), + "image_url": S.Struct({ + "url": S.String + }) + }) + )) + }) +}) {} + +export class URLCitationAnnotationDetail extends S.Class("URLCitationAnnotationDetail")({ + "type": S.Literal("url_citation"), + "url_citation": S.Struct({ + "end_index": S.Number, + "start_index": S.Number, + "title": S.String, + "url": S.String, + "content": S.optionalWith(S.String, { nullable: true }) + }) +}) {} + +export class AnnotationDetail extends S.Union(FileAnnotationDetail, URLCitationAnnotationDetail) {} + +export class OpenResponsesEasyInputMessageType extends S.Literal("message") {} + +export class OpenResponsesEasyInputMessageRoleEnum extends S.Literal("developer") {} + +export class ResponseInputTextType extends S.Literal("input_text") {} + +/** + * Text input content item + */ +export class ResponseInputText extends S.Class("ResponseInputText")({ + "type": ResponseInputTextType, + "text": S.String +}) {} + +export class ResponseInputFileType extends S.Literal("input_file") {} + +/** + * File input content item + */ +export class ResponseInputFile extends S.Class("ResponseInputFile")({ + "type": ResponseInputFileType, + "file_id": S.optionalWith(S.String, { nullable: true }), + "file_data": S.optionalWith(S.String, { nullable: true }), + "filename": S.optionalWith(S.String, { nullable: true }), + "file_url": S.optionalWith(S.String, { nullable: true }) +}) {} + +export class ResponseInputAudioType extends S.Literal("input_audio") {} + +export class ResponseInputAudioInputAudioFormat extends S.Literal("mp3", "wav") {} + +/** + * Audio input content item + */ +export class ResponseInputAudio extends S.Class("ResponseInputAudio")({ + "type": ResponseInputAudioType, + "input_audio": S.Struct({ + "data": S.String, + "format": ResponseInputAudioInputAudioFormat + }) +}) {} + +export class ResponseInputVideoType extends S.Literal("input_video") {} + +/** + * Video input content item + */ +export class ResponseInputVideo extends S.Class("ResponseInputVideo")({ + "type": ResponseInputVideoType, + /** + * A base64 data URL or remote URL that resolves to a video file + */ + "video_url": S.String +}) {} + +export class OpenResponsesEasyInputMessage + extends S.Class("OpenResponsesEasyInputMessage")({ + "type": S.optionalWith(OpenResponsesEasyInputMessageType, { nullable: true }), + "role": S.Union( + OpenResponsesEasyInputMessageRoleEnum, + OpenResponsesEasyInputMessageRoleEnum, + OpenResponsesEasyInputMessageRoleEnum, + OpenResponsesEasyInputMessageRoleEnum + ), + "content": S.Union( + S.Array(S.Union( + ResponseInputText, + /** + * Image input content item + */ + S.Struct({ + "type": S.Literal("input_image"), + "detail": S.Literal("auto", "high", "low"), + "image_url": S.optionalWith(S.String, { nullable: true }) + }), + ResponseInputFile, + ResponseInputAudio, + ResponseInputVideo + )), + S.String + ) + }) +{} + +export class OpenResponsesInputMessageItemType extends S.Literal("message") {} + +export class OpenResponsesInputMessageItemRoleEnum extends S.Literal("developer") {} + +export class OpenResponsesInputMessageItem + extends S.Class("OpenResponsesInputMessageItem")({ + "id": S.optionalWith(S.String, { nullable: true }), + "type": S.optionalWith(OpenResponsesInputMessageItemType, { nullable: true }), + "role": S.Union( + OpenResponsesInputMessageItemRoleEnum, + OpenResponsesInputMessageItemRoleEnum, + OpenResponsesInputMessageItemRoleEnum + ), + "content": S.Array(S.Union( + ResponseInputText, + /** + * Image input content item + */ + S.Struct({ + "type": S.Literal("input_image"), + "detail": S.Literal("auto", "high", "low"), + "image_url": S.optionalWith(S.String, { nullable: true }) + }), + ResponseInputFile, + ResponseInputAudio, + ResponseInputVideo + )) + }) +{} + +export class OpenResponsesFunctionToolCallType extends S.Literal("function_call") {} + +export class ToolCallStatus extends S.Literal("in_progress", "completed", "incomplete") {} + +/** + * A function call initiated by the model + */ +export class OpenResponsesFunctionToolCall + extends S.Class("OpenResponsesFunctionToolCall")({ + "type": OpenResponsesFunctionToolCallType, + "call_id": S.String, + "name": S.String, + "arguments": S.String, + "id": S.String, + "status": S.optionalWith(ToolCallStatus, { nullable: true }) + }) +{} + +export class OpenResponsesFunctionCallOutputType extends S.Literal("function_call_output") {} + +/** + * The output from a function call execution + */ +export class OpenResponsesFunctionCallOutput + extends S.Class("OpenResponsesFunctionCallOutput")({ + "type": OpenResponsesFunctionCallOutputType, + "id": S.optionalWith(S.String, { nullable: true }), + "call_id": S.String, + "output": S.String, + "status": S.optionalWith(ToolCallStatus, { nullable: true }) + }) +{} + +export class ResponsesOutputMessageRole extends S.Literal("assistant") {} + +export class ResponsesOutputMessageType extends S.Literal("message") {} + +export class ResponsesOutputMessageStatusEnum extends S.Literal("in_progress") {} + +export class ResponseOutputTextType extends S.Literal("output_text") {} + +export class FileCitationType extends S.Literal("file_citation") {} + +export class FileCitation extends S.Class("FileCitation")({ + "type": FileCitationType, + "file_id": S.String, + "filename": S.String, + "index": S.Number +}) {} + +export class URLCitationType extends S.Literal("url_citation") {} + +export class URLCitation extends S.Class("URLCitation")({ + "type": URLCitationType, + "url": S.String, + "title": S.String, + "start_index": S.Number, + "end_index": S.Number +}) {} + +export class FilePathType extends S.Literal("file_path") {} + +export class FilePath extends S.Class("FilePath")({ + "type": FilePathType, + "file_id": S.String, + "index": S.Number +}) {} + +export class OpenAIResponsesAnnotation extends S.Union(FileCitation, URLCitation, FilePath) {} + +export class ResponseOutputText extends S.Class("ResponseOutputText")({ + "type": ResponseOutputTextType, + "text": S.String, + "annotations": S.optionalWith(S.Array(OpenAIResponsesAnnotation), { nullable: true }), + "logprobs": S.optionalWith( + S.Array(S.Struct({ + "token": S.String, + "bytes": S.Array(S.Number), + "logprob": S.Number, + "top_logprobs": S.Array(S.Struct({ + "token": S.String, + "bytes": S.Array(S.Number), + "logprob": S.Number + })) + })), + { nullable: true } + ) +}) {} + +export class OpenAIResponsesRefusalContentType extends S.Literal("refusal") {} + +export class OpenAIResponsesRefusalContent + extends S.Class("OpenAIResponsesRefusalContent")({ + "type": OpenAIResponsesRefusalContentType, + "refusal": S.String + }) +{} + +export class ResponsesOutputMessage extends S.Class("ResponsesOutputMessage")({ + "id": S.String, + "role": ResponsesOutputMessageRole, + "type": ResponsesOutputMessageType, + "status": S.optionalWith( + S.Union(ResponsesOutputMessageStatusEnum, ResponsesOutputMessageStatusEnum, ResponsesOutputMessageStatusEnum), + { nullable: true } + ), + "content": S.Array(S.Union(ResponseOutputText, OpenAIResponsesRefusalContent)) +}) {} + +/** + * The format of the reasoning content + */ +export class ResponsesOutputItemReasoningFormat extends S.Literal( + "unknown", + "openai-responses-v1", + "azure-openai-responses-v1", + "xai-responses-v1", + "anthropic-claude-v1", + "google-gemini-v1" +) {} + +export class ResponsesOutputItemReasoningType extends S.Literal("reasoning") {} + +export class ResponsesOutputItemReasoningStatusEnum extends S.Literal("in_progress") {} + +export class ResponsesOutputItemReasoning + extends S.Class("ResponsesOutputItemReasoning")({ + /** + * A signature for the reasoning content, used for verification + */ + "signature": S.optionalWith(S.String, { nullable: true }), + /** + * The format of the reasoning content + */ + "format": S.optionalWith(ResponsesOutputItemReasoningFormat, { nullable: true }), + "type": ResponsesOutputItemReasoningType, + "id": S.String, + "content": S.optionalWith(S.Array(ReasoningTextContent), { nullable: true }), + "summary": S.Array(ReasoningSummaryText), + "encrypted_content": S.optionalWith(S.String, { nullable: true }), + "status": S.optionalWith( + S.Union( + ResponsesOutputItemReasoningStatusEnum, + ResponsesOutputItemReasoningStatusEnum, + ResponsesOutputItemReasoningStatusEnum + ), + { nullable: true } + ) + }) +{} + +export class ResponsesOutputItemFunctionCallType extends S.Literal("function_call") {} + +export class ResponsesOutputItemFunctionCallStatusEnum extends S.Literal("in_progress") {} + +export class ResponsesOutputItemFunctionCall + extends S.Class("ResponsesOutputItemFunctionCall")({ + "type": ResponsesOutputItemFunctionCallType, + "id": S.optionalWith(S.String, { nullable: true }), + "name": S.String, + "arguments": S.String, + "call_id": S.String, + "status": S.optionalWith( + S.Union( + ResponsesOutputItemFunctionCallStatusEnum, + ResponsesOutputItemFunctionCallStatusEnum, + ResponsesOutputItemFunctionCallStatusEnum + ), + { nullable: true } + ) + }) +{} + +export class ResponsesWebSearchCallOutputType extends S.Literal("web_search_call") {} + +export class WebSearchStatus extends S.Literal("completed", "searching", "in_progress", "failed") {} + +export class ResponsesWebSearchCallOutput + extends S.Class("ResponsesWebSearchCallOutput")({ + "type": ResponsesWebSearchCallOutputType, + "id": S.String, + "status": WebSearchStatus + }) +{} + +export class ResponsesOutputItemFileSearchCallType extends S.Literal("file_search_call") {} + +export class ResponsesOutputItemFileSearchCall + extends S.Class("ResponsesOutputItemFileSearchCall")({ + "type": ResponsesOutputItemFileSearchCallType, + "id": S.String, + "queries": S.Array(S.String), + "status": WebSearchStatus + }) +{} + +export class ResponsesImageGenerationCallType extends S.Literal("image_generation_call") {} + +export class ImageGenerationStatus extends S.Literal("in_progress", "completed", "generating", "failed") {} + +export class ResponsesImageGenerationCall + extends S.Class("ResponsesImageGenerationCall")({ + "type": ResponsesImageGenerationCallType, + "id": S.String, + "result": S.optionalWith(S.NullOr(S.String), { default: () => null }), + "status": ImageGenerationStatus + }) +{} + +/** + * Input for a response request - can be a string or array of items + */ +export class OpenResponsesInput extends S.Union( + S.String, + S.Array( + S.Union( + OpenResponsesReasoning, + OpenResponsesEasyInputMessage, + OpenResponsesInputMessageItem, + OpenResponsesFunctionToolCall, + OpenResponsesFunctionCallOutput, + ResponsesOutputMessage, + ResponsesOutputItemReasoning, + ResponsesOutputItemFunctionCall, + ResponsesWebSearchCallOutput, + ResponsesOutputItemFileSearchCall, + ResponsesImageGenerationCall + ) + ) +) {} + +/** + * Metadata key-value pairs for the request. Keys must be ≤64 characters and cannot contain brackets. Values must be ≤512 characters. Maximum 16 pairs allowed. + */ +export class OpenResponsesRequestMetadata extends S.Record({ key: S.String, value: S.Unknown }) {} + +export class OpenResponsesWebSearchPreviewToolType extends S.Literal("web_search_preview") {} + +/** + * Size of the search context for web search tools + */ +export class ResponsesSearchContextSize extends S.Literal("low", "medium", "high") {} + +export class WebSearchPreviewToolUserLocationType extends S.Literal("approximate") {} + +export class WebSearchPreviewToolUserLocation + extends S.Class("WebSearchPreviewToolUserLocation")({ + "type": WebSearchPreviewToolUserLocationType, + "city": S.optionalWith(S.String, { nullable: true }), + "country": S.optionalWith(S.String, { nullable: true }), + "region": S.optionalWith(S.String, { nullable: true }), + "timezone": S.optionalWith(S.String, { nullable: true }) + }) +{} + +/** + * Web search preview tool configuration + */ +export class OpenResponsesWebSearchPreviewTool + extends S.Class("OpenResponsesWebSearchPreviewTool")({ + "type": OpenResponsesWebSearchPreviewToolType, + "search_context_size": S.optionalWith(ResponsesSearchContextSize, { nullable: true }), + "user_location": S.optionalWith(WebSearchPreviewToolUserLocation, { nullable: true }) + }) +{} + +export class OpenResponsesWebSearchPreview20250311ToolType extends S.Literal("web_search_preview_2025_03_11") {} + +/** + * Web search preview tool configuration (2025-03-11 version) + */ +export class OpenResponsesWebSearchPreview20250311Tool + extends S.Class("OpenResponsesWebSearchPreview20250311Tool")({ + "type": OpenResponsesWebSearchPreview20250311ToolType, + "search_context_size": S.optionalWith(ResponsesSearchContextSize, { nullable: true }), + "user_location": S.optionalWith(WebSearchPreviewToolUserLocation, { nullable: true }) + }) +{} + +export class OpenResponsesWebSearchToolType extends S.Literal("web_search") {} + +export class ResponsesWebSearchUserLocationType extends S.Literal("approximate") {} + +/** + * User location information for web search + */ +export class ResponsesWebSearchUserLocation + extends S.Class("ResponsesWebSearchUserLocation")({ + "type": S.optionalWith(ResponsesWebSearchUserLocationType, { nullable: true }), + "city": S.optionalWith(S.String, { nullable: true }), + "country": S.optionalWith(S.String, { nullable: true }), + "region": S.optionalWith(S.String, { nullable: true }), + "timezone": S.optionalWith(S.String, { nullable: true }) + }) +{} + +/** + * Web search tool configuration + */ +export class OpenResponsesWebSearchTool extends S.Class("OpenResponsesWebSearchTool")({ + "type": OpenResponsesWebSearchToolType, + "filters": S.optionalWith( + S.Struct({ + "allowed_domains": S.optionalWith(S.Array(S.String), { nullable: true }) + }), + { nullable: true } + ), + "search_context_size": S.optionalWith(ResponsesSearchContextSize, { nullable: true }), + "user_location": S.optionalWith(ResponsesWebSearchUserLocation, { nullable: true }) +}) {} + +export class OpenResponsesWebSearch20250826ToolType extends S.Literal("web_search_2025_08_26") {} + +/** + * Web search tool configuration (2025-08-26 version) + */ +export class OpenResponsesWebSearch20250826Tool + extends S.Class("OpenResponsesWebSearch20250826Tool")({ + "type": OpenResponsesWebSearch20250826ToolType, + "filters": S.optionalWith( + S.Struct({ + "allowed_domains": S.optionalWith(S.Array(S.String), { nullable: true }) + }), + { nullable: true } + ), + "search_context_size": S.optionalWith(ResponsesSearchContextSize, { nullable: true }), + "user_location": S.optionalWith(ResponsesWebSearchUserLocation, { nullable: true }) + }) +{} + +export class OpenAIResponsesToolChoiceEnum extends S.Literal("required") {} + +export class OpenAIResponsesToolChoiceEnumType extends S.Literal("function") {} + +export class OpenAIResponsesToolChoiceEnumTypeEnum extends S.Literal("web_search_preview") {} + +export class OpenAIResponsesToolChoice extends S.Union( + OpenAIResponsesToolChoiceEnum, + OpenAIResponsesToolChoiceEnum, + OpenAIResponsesToolChoiceEnum, + S.Struct({ + "type": OpenAIResponsesToolChoiceEnumType, + "name": S.String + }), + S.Struct({ + "type": S.Union(OpenAIResponsesToolChoiceEnumTypeEnum, OpenAIResponsesToolChoiceEnumTypeEnum) + }) +) {} + +export class ResponsesFormatTextType extends S.Literal("text") {} + +/** + * Plain text response format + */ +export class ResponsesFormatText extends S.Class("ResponsesFormatText")({ + "type": ResponsesFormatTextType +}) {} + +export class ResponsesFormatJSONObjectType extends S.Literal("json_object") {} + +/** + * JSON object response format + */ +export class ResponsesFormatJSONObject extends S.Class("ResponsesFormatJSONObject")({ + "type": ResponsesFormatJSONObjectType +}) {} + +export class ResponsesFormatTextJSONSchemaConfigType extends S.Literal("json_schema") {} + +/** + * JSON schema constrained response format + */ +export class ResponsesFormatTextJSONSchemaConfig + extends S.Class("ResponsesFormatTextJSONSchemaConfig")({ + "type": ResponsesFormatTextJSONSchemaConfigType, + "name": S.String, + "description": S.optionalWith(S.String, { nullable: true }), + "strict": S.optionalWith(S.Boolean, { nullable: true }), + "schema": S.Record({ key: S.String, value: S.Unknown }) + }) +{} + +/** + * Text response format configuration + */ +export class ResponseFormatTextConfig + extends S.Union(ResponsesFormatText, ResponsesFormatJSONObject, ResponsesFormatTextJSONSchemaConfig) +{} + +export class OpenResponsesResponseTextVerbosity extends S.Literal("high", "low", "medium") {} + +/** + * Text output configuration including format and verbosity + */ +export class OpenResponsesResponseText extends S.Class("OpenResponsesResponseText")({ + "format": S.optionalWith(ResponseFormatTextConfig, { nullable: true }), + "verbosity": S.optionalWith(OpenResponsesResponseTextVerbosity, { nullable: true }) +}) {} + +export class OpenAIResponsesReasoningEffort extends S.Literal("xhigh", "high", "medium", "low", "minimal", "none") {} + +export class ReasoningSummaryVerbosity extends S.Literal("auto", "concise", "detailed") {} + +export class OpenResponsesReasoningConfig + extends S.Class("OpenResponsesReasoningConfig")({ + "max_tokens": S.optionalWith(S.Number, { nullable: true }), + "enabled": S.optionalWith(S.Boolean, { nullable: true }), + "effort": S.optionalWith(OpenAIResponsesReasoningEffort, { nullable: true }), + "summary": S.optionalWith(ReasoningSummaryVerbosity, { nullable: true }) + }) +{} + +export class ResponsesOutputModality extends S.Literal("text", "image") {} + +export class OpenAIResponsesPrompt extends S.Class("OpenAIResponsesPrompt")({ + "id": S.String, + "variables": S.optionalWith(S.Record({ key: S.String, value: S.Unknown }), { nullable: true }) +}) {} + +export class OpenAIResponsesIncludable extends S.Literal( + "file_search_call.results", + "message.input_image.image_url", + "computer_call_output.output.image_url", + "reasoning.encrypted_content", + "code_interpreter_call.outputs" +) {} + +export class OpenResponsesRequestServiceTier extends S.Literal("auto") {} + +export class OpenResponsesRequestTruncationEnum extends S.Literal("auto", "disabled") {} + +export class OpenResponsesRequestTruncation extends OpenResponsesRequestTruncationEnum {} + +/** + * Data collection setting. If no available model provider meets the requirement, your request will return an error. + * - allow: (default) allow providers which store user data non-transiently and may train on it + * + * - deny: use only providers which do not collect user data. + */ +export class DataCollection extends S.Literal("deny", "allow") {} + +export class ProviderName extends S.Literal( + "AI21", + "AionLabs", + "Alibaba", + "Amazon Bedrock", + "Amazon Nova", + "Anthropic", + "Arcee AI", + "AtlasCloud", + "Avian", + "Azure", + "BaseTen", + "BytePlus", + "Black Forest Labs", + "Cerebras", + "Chutes", + "Cirrascale", + "Clarifai", + "Cloudflare", + "Cohere", + "Crusoe", + "DeepInfra", + "DeepSeek", + "Featherless", + "Fireworks", + "Friendli", + "GMICloud", + "Google", + "Google AI Studio", + "Groq", + "Hyperbolic", + "Inception", + "Inceptron", + "InferenceNet", + "Infermatic", + "Inflection", + "Liquid", + "Mara", + "Mancer 2", + "Minimax", + "ModelRun", + "Mistral", + "Modular", + "Moonshot AI", + "Morph", + "NCompass", + "Nebius", + "NextBit", + "Novita", + "Nvidia", + "OpenAI", + "OpenInference", + "Parasail", + "Perplexity", + "Phala", + "Relace", + "SambaNova", + "Seed", + "SiliconFlow", + "Sourceful", + "Stealth", + "StreamLake", + "Switchpoint", + "Together", + "Upstage", + "Venice", + "WandB", + "Xiaomi", + "xAI", + "Z.AI", + "FakeProvider" +) {} + +export class Quantization extends S.Literal("int4", "int8", "fp4", "fp6", "fp8", "fp16", "bf16", "fp32", "unknown") {} + +export class ProviderSort extends S.Literal("price", "throughput", "latency") {} + +export class ProviderSortConfigPartitionEnum extends S.Literal("model", "none") {} + +export class ProviderSortConfig extends S.Class("ProviderSortConfig")({ + "by": S.optionalWith(ProviderSort, { nullable: true }), + "partition": S.optionalWith(ProviderSortConfigPartitionEnum, { nullable: true }) +}) {} + +/** + * A value in string format that is a large number + */ +export class BigNumberUnion extends S.String {} + +/** + * Percentile-based throughput cutoffs. All specified cutoffs must be met for an endpoint to be preferred. + */ +export class PercentileThroughputCutoffs extends S.Class("PercentileThroughputCutoffs")({ + /** + * Minimum p50 throughput (tokens/sec) + */ + "p50": S.optionalWith(S.Number, { nullable: true }), + /** + * Minimum p75 throughput (tokens/sec) + */ + "p75": S.optionalWith(S.Number, { nullable: true }), + /** + * Minimum p90 throughput (tokens/sec) + */ + "p90": S.optionalWith(S.Number, { nullable: true }), + /** + * Minimum p99 throughput (tokens/sec) + */ + "p99": S.optionalWith(S.Number, { nullable: true }) +}) {} + +/** + * Preferred minimum throughput (in tokens per second). Can be a number (applies to p50) or an object with percentile-specific cutoffs. Endpoints below the threshold(s) may still be used, but are deprioritized in routing. When using fallback models, this may cause a fallback model to be used instead of the primary model if it meets the threshold. + */ +export class PreferredMinThroughput extends S.Union(S.Number, PercentileThroughputCutoffs) {} + +/** + * Percentile-based latency cutoffs. All specified cutoffs must be met for an endpoint to be preferred. + */ +export class PercentileLatencyCutoffs extends S.Class("PercentileLatencyCutoffs")({ + /** + * Maximum p50 latency (seconds) + */ + "p50": S.optionalWith(S.Number, { nullable: true }), + /** + * Maximum p75 latency (seconds) + */ + "p75": S.optionalWith(S.Number, { nullable: true }), + /** + * Maximum p90 latency (seconds) + */ + "p90": S.optionalWith(S.Number, { nullable: true }), + /** + * Maximum p99 latency (seconds) + */ + "p99": S.optionalWith(S.Number, { nullable: true }) +}) {} + +/** + * Preferred maximum latency (in seconds). Can be a number (applies to p50) or an object with percentile-specific cutoffs. Endpoints above the threshold(s) may still be used, but are deprioritized in routing. When using fallback models, this may cause a fallback model to be used instead of the primary model if it meets the threshold. + */ +export class PreferredMaxLatency extends S.Union(S.Number, PercentileLatencyCutoffs) {} + +/** + * The search engine to use for web search. + */ +export class WebSearchEngine extends S.Literal("native", "exa") {} + +/** + * The engine to use for parsing PDF files. + */ +export class PDFParserEngine extends S.Literal("mistral-ocr", "pdf-text", "native") {} + +/** + * Options for PDF parsing. + */ +export class PDFParserOptions extends S.Class("PDFParserOptions")({ + "engine": S.optionalWith(PDFParserEngine, { nullable: true }) +}) {} + +/** + * **DEPRECATED** Use providers.sort.partition instead. Backwards-compatible alias for providers.sort.partition. Accepts legacy values: "fallback" (maps to "model"), "sort" (maps to "none"). + */ +export class OpenResponsesRequestRoute extends S.Literal("fallback", "sort") {} + +/** + * Request schema for Responses endpoint + */ +export class OpenResponsesRequest extends S.Class("OpenResponsesRequest")({ + "input": S.optionalWith(OpenResponsesInput, { nullable: true }), + "instructions": S.optionalWith(S.String, { nullable: true }), + "metadata": S.optionalWith(OpenResponsesRequestMetadata, { nullable: true }), + "tools": S.optionalWith( + S.Array(S.Union( + /** + * Function tool definition + */ + S.Struct({}), + OpenResponsesWebSearchPreviewTool, + OpenResponsesWebSearchPreview20250311Tool, + OpenResponsesWebSearchTool, + OpenResponsesWebSearch20250826Tool + )), + { nullable: true } + ), + "tool_choice": S.optionalWith(OpenAIResponsesToolChoice, { nullable: true }), + "parallel_tool_calls": S.optionalWith(S.Boolean, { nullable: true }), + "model": S.optionalWith(S.String, { nullable: true }), + "models": S.optionalWith(S.Array(S.String), { nullable: true }), + "text": S.optionalWith(OpenResponsesResponseText, { nullable: true }), + "reasoning": S.optionalWith(OpenResponsesReasoningConfig, { nullable: true }), + "max_output_tokens": S.optionalWith(S.Number, { nullable: true }), + "temperature": S.optionalWith(S.Number.pipe(S.greaterThanOrEqualTo(0), S.lessThanOrEqualTo(2)), { nullable: true }), + "top_p": S.optionalWith(S.Number.pipe(S.greaterThanOrEqualTo(0)), { nullable: true }), + "top_logprobs": S.optionalWith(S.Int.pipe(S.greaterThanOrEqualTo(0), S.lessThanOrEqualTo(20)), { nullable: true }), + "max_tool_calls": S.optionalWith(S.Int, { nullable: true }), + "presence_penalty": S.optionalWith(S.Number.pipe(S.greaterThanOrEqualTo(-2), S.lessThanOrEqualTo(2)), { + nullable: true + }), + "frequency_penalty": S.optionalWith(S.Number.pipe(S.greaterThanOrEqualTo(-2), S.lessThanOrEqualTo(2)), { + nullable: true + }), + "top_k": S.optionalWith(S.Number, { nullable: true }), + /** + * Provider-specific image configuration options. Keys and values vary by model/provider. See https://openrouter.ai/docs/features/multimodal/image-generation for more details. + */ + "image_config": S.optionalWith(S.Record({ key: S.String, value: S.Unknown }), { nullable: true }), + /** + * Output modalities for the response. Supported values are "text" and "image". + */ + "modalities": S.optionalWith(S.Array(ResponsesOutputModality), { nullable: true }), + "prompt_cache_key": S.optionalWith(S.String, { nullable: true }), + "previous_response_id": S.optionalWith(S.String, { nullable: true }), + "prompt": S.optionalWith(OpenAIResponsesPrompt, { nullable: true }), + "include": S.optionalWith(S.Array(OpenAIResponsesIncludable), { nullable: true }), + "background": S.optionalWith(S.Boolean, { nullable: true }), + "safety_identifier": S.optionalWith(S.String, { nullable: true }), + "store": S.optionalWith(S.Literal(false), { nullable: true, default: () => false as const }), + "service_tier": S.optionalWith(OpenResponsesRequestServiceTier, { nullable: true, default: () => "auto" as const }), + "truncation": S.optionalWith(OpenResponsesRequestTruncation, { nullable: true }), + "stream": S.optionalWith(S.Boolean, { nullable: true, default: () => false as const }), + /** + * When multiple model providers are available, optionally indicate your routing preference. + */ + "provider": S.optionalWith( + S.Struct({ + /** + * Whether to allow backup providers to serve requests + * - true: (default) when the primary provider (or your custom providers in "order") is unavailable, use the next best provider. + * - false: use only the primary/custom provider, and return the upstream error if it's unavailable. + */ + "allow_fallbacks": S.optionalWith(S.Boolean, { nullable: true }), + /** + * Whether to filter providers to only those that support the parameters you've provided. If this setting is omitted or set to false, then providers will receive only the parameters they support, and ignore the rest. + */ + "require_parameters": S.optionalWith(S.Boolean, { nullable: true }), + "data_collection": S.optionalWith(DataCollection, { nullable: true }), + /** + * Whether to restrict routing to only ZDR (Zero Data Retention) endpoints. When true, only endpoints that do not retain prompts will be used. + */ + "zdr": S.optionalWith(S.Boolean, { nullable: true }), + /** + * Whether to restrict routing to only models that allow text distillation. When true, only models where the author has allowed distillation will be used. + */ + "enforce_distillable_text": S.optionalWith(S.Boolean, { nullable: true }), + /** + * An ordered list of provider slugs. The router will attempt to use the first provider in the subset of this list that supports your requested model, and fall back to the next if it is unavailable. If no providers are available, the request will fail with an error message. + */ + "order": S.optionalWith(S.Array(S.Union(ProviderName, S.String)), { nullable: true }), + /** + * List of provider slugs to allow. If provided, this list is merged with your account-wide allowed provider settings for this request. + */ + "only": S.optionalWith(S.Array(S.Union(ProviderName, S.String)), { nullable: true }), + /** + * List of provider slugs to ignore. If provided, this list is merged with your account-wide ignored provider settings for this request. + */ + "ignore": S.optionalWith(S.Array(S.Union(ProviderName, S.String)), { nullable: true }), + /** + * A list of quantization levels to filter the provider by. + */ + "quantizations": S.optionalWith(S.Array(Quantization), { nullable: true }), + /** + * The sorting strategy to use for this request, if "order" is not specified. When set, no load balancing is performed. + */ + "sort": S.optionalWith(S.Union(ProviderSort, ProviderSortConfig), { nullable: true }), + /** + * The object specifying the maximum price you want to pay for this request. USD price per million tokens, for prompt and completion. + */ + "max_price": S.optionalWith( + S.Struct({ + "prompt": S.optionalWith(BigNumberUnion, { nullable: true }), + "completion": S.optionalWith(BigNumberUnion, { nullable: true }), + "image": S.optionalWith(BigNumberUnion, { nullable: true }), + "audio": S.optionalWith(BigNumberUnion, { nullable: true }), + "request": S.optionalWith(BigNumberUnion, { nullable: true }) + }), + { nullable: true } + ), + "preferred_min_throughput": S.optionalWith(PreferredMinThroughput, { nullable: true }), + "preferred_max_latency": S.optionalWith(PreferredMaxLatency, { nullable: true }) + }), + { nullable: true } + ), + /** + * Plugins you want to enable for this request, including their settings. + */ + "plugins": S.optionalWith( + S.Array(S.Union( + S.Struct({ + "id": S.Literal("auto-router"), + /** + * Set to false to disable the auto-router plugin for this request. Defaults to true. + */ + "enabled": S.optionalWith(S.Boolean, { nullable: true }), + /** + * List of model patterns to filter which models the auto-router can route between. Supports wildcards (e.g., "anthropic/*" matches all Anthropic models). When not specified, uses the default supported models list. + */ + "allowed_models": S.optionalWith(S.Array(S.String), { nullable: true }) + }), + S.Struct({ + "id": S.Literal("moderation") + }), + S.Struct({ + "id": S.Literal("web"), + /** + * Set to false to disable the web-search plugin for this request. Defaults to true. + */ + "enabled": S.optionalWith(S.Boolean, { nullable: true }), + "max_results": S.optionalWith(S.Number, { nullable: true }), + "search_prompt": S.optionalWith(S.String, { nullable: true }), + "engine": S.optionalWith(WebSearchEngine, { nullable: true }) + }), + S.Struct({ + "id": S.Literal("file-parser"), + /** + * Set to false to disable the file-parser plugin for this request. Defaults to true. + */ + "enabled": S.optionalWith(S.Boolean, { nullable: true }), + "pdf": S.optionalWith(PDFParserOptions, { nullable: true }) + }), + S.Struct({ + "id": S.Literal("response-healing"), + /** + * Set to false to disable the response-healing plugin for this request. Defaults to true. + */ + "enabled": S.optionalWith(S.Boolean, { nullable: true }) + }) + )), + { nullable: true } + ), + /** + * **DEPRECATED** Use providers.sort.partition instead. Backwards-compatible alias for providers.sort.partition. Accepts legacy values: "fallback" (maps to "model"), "sort" (maps to "none"). + */ + "route": S.optionalWith(OpenResponsesRequestRoute, { nullable: true }), + /** + * A unique identifier representing your end-user, which helps distinguish between different users of your app. This allows your app to identify specific users in case of abuse reports, preventing your entire app from being affected by the actions of individual users. Maximum of 128 characters. + */ + "user": S.optionalWith(S.String.pipe(S.maxLength(128)), { nullable: true }), + /** + * A unique identifier for grouping related requests (e.g., a conversation or agent workflow) for observability. If provided in both the request body and the x-session-id header, the body value takes precedence. Maximum of 128 characters. + */ + "session_id": S.optionalWith(S.String.pipe(S.maxLength(128)), { nullable: true }) +}) {} + +export class OutputMessageRole extends S.Literal("assistant") {} + +export class OutputMessageType extends S.Literal("message") {} + +export class OutputMessageStatusEnum extends S.Literal("in_progress") {} + +export class OutputMessage extends S.Class("OutputMessage")({ + "id": S.String, + "role": OutputMessageRole, + "type": OutputMessageType, + "status": S.optionalWith(S.Union(OutputMessageStatusEnum, OutputMessageStatusEnum, OutputMessageStatusEnum), { + nullable: true + }), + "content": S.Array(S.Union(ResponseOutputText, OpenAIResponsesRefusalContent)) +}) {} + +export class OutputItemReasoningType extends S.Literal("reasoning") {} + +export class OutputItemReasoningStatusEnum extends S.Literal("in_progress") {} + +export class OutputItemReasoning extends S.Class("OutputItemReasoning")({ + "type": OutputItemReasoningType, + "id": S.String, + "content": S.optionalWith(S.Array(ReasoningTextContent), { nullable: true }), + "summary": S.Array(ReasoningSummaryText), + "encrypted_content": S.optionalWith(S.String, { nullable: true }), + "status": S.optionalWith( + S.Union(OutputItemReasoningStatusEnum, OutputItemReasoningStatusEnum, OutputItemReasoningStatusEnum), + { nullable: true } + ) +}) {} + +export class OutputItemFunctionCallType extends S.Literal("function_call") {} + +export class OutputItemFunctionCallStatusEnum extends S.Literal("in_progress") {} + +export class OutputItemFunctionCall extends S.Class("OutputItemFunctionCall")({ + "type": OutputItemFunctionCallType, + "id": S.optionalWith(S.String, { nullable: true }), + "name": S.String, + "arguments": S.String, + "call_id": S.String, + "status": S.optionalWith( + S.Union(OutputItemFunctionCallStatusEnum, OutputItemFunctionCallStatusEnum, OutputItemFunctionCallStatusEnum), + { nullable: true } + ) +}) {} + +export class OutputItemWebSearchCallType extends S.Literal("web_search_call") {} + +export class OutputItemWebSearchCall extends S.Class("OutputItemWebSearchCall")({ + "type": OutputItemWebSearchCallType, + "id": S.String, + "status": WebSearchStatus +}) {} + +export class OutputItemFileSearchCallType extends S.Literal("file_search_call") {} + +export class OutputItemFileSearchCall extends S.Class("OutputItemFileSearchCall")({ + "type": OutputItemFileSearchCallType, + "id": S.String, + "queries": S.Array(S.String), + "status": WebSearchStatus +}) {} + +export class OutputItemImageGenerationCallType extends S.Literal("image_generation_call") {} + +export class OutputItemImageGenerationCall + extends S.Class("OutputItemImageGenerationCall")({ + "type": OutputItemImageGenerationCallType, + "id": S.String, + "result": S.optionalWith(S.NullOr(S.String), { default: () => null }), + "status": ImageGenerationStatus + }) +{} + +export class OpenAIResponsesUsage extends S.Class("OpenAIResponsesUsage")({ + "input_tokens": S.Number, + "input_tokens_details": S.Struct({ + "cached_tokens": S.Number + }), + "output_tokens": S.Number, + "output_tokens_details": S.Struct({ + "reasoning_tokens": S.Number + }), + "total_tokens": S.Number +}) {} + +export class OpenResponsesNonStreamingResponseObject extends S.Literal("response") {} + +export class OpenAIResponsesResponseStatus + extends S.Literal("completed", "incomplete", "in_progress", "failed", "cancelled", "queued") +{} + +export class ResponsesErrorFieldCode extends S.Literal( + "server_error", + "rate_limit_exceeded", + "invalid_prompt", + "vector_store_timeout", + "invalid_image", + "invalid_image_format", + "invalid_base64_image", + "invalid_image_url", + "image_too_large", + "image_too_small", + "image_parse_error", + "image_content_policy_violation", + "invalid_image_mode", + "image_file_too_large", + "unsupported_image_media_type", + "empty_image_file", + "failed_to_download_image", + "image_file_not_found" +) {} + +/** + * Error information returned from the API + */ +export class ResponsesErrorField extends S.Class("ResponsesErrorField")({ + "code": ResponsesErrorFieldCode, + "message": S.String +}) {} + +export class OpenAIResponsesIncompleteDetailsReason extends S.Literal("max_output_tokens", "content_filter") {} + +export class OpenAIResponsesIncompleteDetails + extends S.Class("OpenAIResponsesIncompleteDetails")({ + "reason": S.optionalWith(OpenAIResponsesIncompleteDetailsReason, { nullable: true }) + }) +{} + +export class ResponseInputImageType extends S.Literal("input_image") {} + +export class ResponseInputImageDetail extends S.Literal("auto", "high", "low") {} + +/** + * Image input content item + */ +export class ResponseInputImage extends S.Class("ResponseInputImage")({ + "type": ResponseInputImageType, + "detail": ResponseInputImageDetail, + "image_url": S.optionalWith(S.String, { nullable: true }) +}) {} + +export class OpenAIResponsesInput extends S.Union( + S.String, + S.Array(S.Union( + S.Struct({ + "type": S.optionalWith(S.Literal("message"), { nullable: true }), + "role": S.Union(S.Literal("user"), S.Literal("system"), S.Literal("assistant"), S.Literal("developer")), + "content": S.Union( + S.Array(S.Union(ResponseInputText, ResponseInputImage, ResponseInputFile, ResponseInputAudio)), + S.String + ) + }), + S.Struct({ + "id": S.String, + "type": S.optionalWith(S.Literal("message"), { nullable: true }), + "role": S.Union(S.Literal("user"), S.Literal("system"), S.Literal("developer")), + "content": S.Array(S.Union(ResponseInputText, ResponseInputImage, ResponseInputFile, ResponseInputAudio)) + }), + S.Struct({ + "type": S.Literal("function_call_output"), + "id": S.optionalWith(S.String, { nullable: true }), + "call_id": S.String, + "output": S.String, + "status": S.optionalWith(ToolCallStatus, { nullable: true }) + }), + S.Struct({ + "type": S.Literal("function_call"), + "call_id": S.String, + "name": S.String, + "arguments": S.String, + "id": S.optionalWith(S.String, { nullable: true }), + "status": S.optionalWith(ToolCallStatus, { nullable: true }) + }), + OutputItemImageGenerationCall, + OutputMessage + )) +) {} + +export class OpenAIResponsesReasoningConfig + extends S.Class("OpenAIResponsesReasoningConfig")({ + "effort": S.optionalWith(OpenAIResponsesReasoningEffort, { nullable: true }), + "summary": S.optionalWith(ReasoningSummaryVerbosity, { nullable: true }) + }) +{} + +export class OpenAIResponsesServiceTier extends S.Literal("auto", "default", "flex", "priority", "scale") {} + +export class OpenAIResponsesTruncation extends S.Literal("auto", "disabled") {} + +export class ResponseTextConfigVerbosity extends S.Literal("high", "low", "medium") {} + +/** + * Text output configuration including format and verbosity + */ +export class ResponseTextConfig extends S.Class("ResponseTextConfig")({ + "format": S.optionalWith(ResponseFormatTextConfig, { nullable: true }), + "verbosity": S.optionalWith(ResponseTextConfigVerbosity, { nullable: true }) +}) {} + +export class OpenResponsesNonStreamingResponse + extends S.Class("OpenResponsesNonStreamingResponse")({ + "output": S.Array( + S.Union( + OutputMessage, + OutputItemReasoning, + OutputItemFunctionCall, + OutputItemWebSearchCall, + OutputItemFileSearchCall, + OutputItemImageGenerationCall + ) + ), + "usage": S.optionalWith(OpenAIResponsesUsage, { nullable: true }), + "id": S.String, + "object": OpenResponsesNonStreamingResponseObject, + "created_at": S.Number, + "model": S.String, + "status": OpenAIResponsesResponseStatus, + "completed_at": S.NullOr(S.Number), + "user": S.optionalWith(S.String, { nullable: true }), + "output_text": S.optionalWith(S.String, { nullable: true }), + "prompt_cache_key": S.optionalWith(S.String, { nullable: true }), + "safety_identifier": S.optionalWith(S.String, { nullable: true }), + "error": S.NullOr(ResponsesErrorField), + "incomplete_details": S.NullOr(OpenAIResponsesIncompleteDetails), + "max_tool_calls": S.optionalWith(S.Number, { nullable: true }), + "top_logprobs": S.optionalWith(S.Number, { nullable: true }), + "max_output_tokens": S.optionalWith(S.Number, { nullable: true }), + "temperature": S.NullOr(S.Number), + "top_p": S.NullOr(S.Number), + "presence_penalty": S.NullOr(S.Number), + "frequency_penalty": S.NullOr(S.Number), + "instructions": OpenAIResponsesInput, + "metadata": S.NullOr(OpenResponsesRequestMetadata), + "tools": S.Array(S.Union( + /** + * Function tool definition + */ + S.Struct({}), + OpenResponsesWebSearchPreviewTool, + OpenResponsesWebSearchPreview20250311Tool, + OpenResponsesWebSearchTool, + OpenResponsesWebSearch20250826Tool + )), + "tool_choice": OpenAIResponsesToolChoice, + "parallel_tool_calls": S.Boolean, + "prompt": S.optionalWith(OpenAIResponsesPrompt, { nullable: true }), + "background": S.optionalWith(S.Boolean, { nullable: true }), + "previous_response_id": S.optionalWith(S.String, { nullable: true }), + "reasoning": S.optionalWith(OpenAIResponsesReasoningConfig, { nullable: true }), + "service_tier": S.optionalWith(OpenAIResponsesServiceTier, { nullable: true }), + "store": S.optionalWith(S.Boolean, { nullable: true }), + "truncation": S.optionalWith(OpenAIResponsesTruncation, { nullable: true }), + "text": S.optionalWith(ResponseTextConfig, { nullable: true }) + }) +{} + +/** + * Error data for BadRequestResponse + */ +export class BadRequestResponseErrorData extends S.Class("BadRequestResponseErrorData")({ + "code": S.Int, + "message": S.String, + "metadata": S.optionalWith(S.Record({ key: S.String, value: S.Unknown }), { nullable: true }) +}) {} + +/** + * Bad Request - Invalid request parameters or malformed input + */ +export class BadRequestResponse extends S.Class("BadRequestResponse")({ + "error": BadRequestResponseErrorData, + "user_id": S.optionalWith(S.String, { nullable: true }) +}) {} + +/** + * Error data for UnauthorizedResponse + */ +export class UnauthorizedResponseErrorData + extends S.Class("UnauthorizedResponseErrorData")({ + "code": S.Int, + "message": S.String, + "metadata": S.optionalWith(S.Record({ key: S.String, value: S.Unknown }), { nullable: true }) + }) +{} + +/** + * Unauthorized - Authentication required or invalid credentials + */ +export class UnauthorizedResponse extends S.Class("UnauthorizedResponse")({ + "error": UnauthorizedResponseErrorData, + "user_id": S.optionalWith(S.String, { nullable: true }) +}) {} + +/** + * Error data for PaymentRequiredResponse + */ +export class PaymentRequiredResponseErrorData + extends S.Class("PaymentRequiredResponseErrorData")({ + "code": S.Int, + "message": S.String, + "metadata": S.optionalWith(S.Record({ key: S.String, value: S.Unknown }), { nullable: true }) + }) +{} + +/** + * Payment Required - Insufficient credits or quota to complete request + */ +export class PaymentRequiredResponse extends S.Class("PaymentRequiredResponse")({ + "error": PaymentRequiredResponseErrorData, + "user_id": S.optionalWith(S.String, { nullable: true }) +}) {} + +/** + * Error data for NotFoundResponse + */ +export class NotFoundResponseErrorData extends S.Class("NotFoundResponseErrorData")({ + "code": S.Int, + "message": S.String, + "metadata": S.optionalWith(S.Record({ key: S.String, value: S.Unknown }), { nullable: true }) +}) {} + +/** + * Not Found - Resource does not exist + */ +export class NotFoundResponse extends S.Class("NotFoundResponse")({ + "error": NotFoundResponseErrorData, + "user_id": S.optionalWith(S.String, { nullable: true }) +}) {} + +/** + * Error data for RequestTimeoutResponse + */ +export class RequestTimeoutResponseErrorData + extends S.Class("RequestTimeoutResponseErrorData")({ + "code": S.Int, + "message": S.String, + "metadata": S.optionalWith(S.Record({ key: S.String, value: S.Unknown }), { nullable: true }) + }) +{} + +/** + * Request Timeout - Operation exceeded time limit + */ +export class RequestTimeoutResponse extends S.Class("RequestTimeoutResponse")({ + "error": RequestTimeoutResponseErrorData, + "user_id": S.optionalWith(S.String, { nullable: true }) +}) {} + +/** + * Error data for PayloadTooLargeResponse + */ +export class PayloadTooLargeResponseErrorData + extends S.Class("PayloadTooLargeResponseErrorData")({ + "code": S.Int, + "message": S.String, + "metadata": S.optionalWith(S.Record({ key: S.String, value: S.Unknown }), { nullable: true }) + }) +{} + +/** + * Payload Too Large - Request payload exceeds size limits + */ +export class PayloadTooLargeResponse extends S.Class("PayloadTooLargeResponse")({ + "error": PayloadTooLargeResponseErrorData, + "user_id": S.optionalWith(S.String, { nullable: true }) +}) {} + +/** + * Error data for UnprocessableEntityResponse + */ +export class UnprocessableEntityResponseErrorData + extends S.Class("UnprocessableEntityResponseErrorData")({ + "code": S.Int, + "message": S.String, + "metadata": S.optionalWith(S.Record({ key: S.String, value: S.Unknown }), { nullable: true }) + }) +{} + +/** + * Unprocessable Entity - Semantic validation failure + */ +export class UnprocessableEntityResponse extends S.Class("UnprocessableEntityResponse")({ + "error": UnprocessableEntityResponseErrorData, + "user_id": S.optionalWith(S.String, { nullable: true }) +}) {} + +/** + * Error data for TooManyRequestsResponse + */ +export class TooManyRequestsResponseErrorData + extends S.Class("TooManyRequestsResponseErrorData")({ + "code": S.Int, + "message": S.String, + "metadata": S.optionalWith(S.Record({ key: S.String, value: S.Unknown }), { nullable: true }) + }) +{} + +/** + * Too Many Requests - Rate limit exceeded + */ +export class TooManyRequestsResponse extends S.Class("TooManyRequestsResponse")({ + "error": TooManyRequestsResponseErrorData, + "user_id": S.optionalWith(S.String, { nullable: true }) +}) {} + +/** + * Error data for InternalServerResponse + */ +export class InternalServerResponseErrorData + extends S.Class("InternalServerResponseErrorData")({ + "code": S.Int, + "message": S.String, + "metadata": S.optionalWith(S.Record({ key: S.String, value: S.Unknown }), { nullable: true }) + }) +{} + +/** + * Internal Server Error - Unexpected server error + */ +export class InternalServerResponse extends S.Class("InternalServerResponse")({ + "error": InternalServerResponseErrorData, + "user_id": S.optionalWith(S.String, { nullable: true }) +}) {} + +/** + * Error data for BadGatewayResponse + */ +export class BadGatewayResponseErrorData extends S.Class("BadGatewayResponseErrorData")({ + "code": S.Int, + "message": S.String, + "metadata": S.optionalWith(S.Record({ key: S.String, value: S.Unknown }), { nullable: true }) +}) {} + +/** + * Bad Gateway - Provider/upstream API failure + */ +export class BadGatewayResponse extends S.Class("BadGatewayResponse")({ + "error": BadGatewayResponseErrorData, + "user_id": S.optionalWith(S.String, { nullable: true }) +}) {} + +/** + * Error data for ServiceUnavailableResponse + */ +export class ServiceUnavailableResponseErrorData + extends S.Class("ServiceUnavailableResponseErrorData")({ + "code": S.Int, + "message": S.String, + "metadata": S.optionalWith(S.Record({ key: S.String, value: S.Unknown }), { nullable: true }) + }) +{} + +/** + * Service Unavailable - Service temporarily unavailable + */ +export class ServiceUnavailableResponse extends S.Class("ServiceUnavailableResponse")({ + "error": ServiceUnavailableResponseErrorData, + "user_id": S.optionalWith(S.String, { nullable: true }) +}) {} + +/** + * Error data for EdgeNetworkTimeoutResponse + */ +export class EdgeNetworkTimeoutResponseErrorData + extends S.Class("EdgeNetworkTimeoutResponseErrorData")({ + "code": S.Int, + "message": S.String, + "metadata": S.optionalWith(S.Record({ key: S.String, value: S.Unknown }), { nullable: true }) + }) +{} + +/** + * Infrastructure Timeout - Provider request timed out at edge network + */ +export class EdgeNetworkTimeoutResponse extends S.Class("EdgeNetworkTimeoutResponse")({ + "error": EdgeNetworkTimeoutResponseErrorData, + "user_id": S.optionalWith(S.String, { nullable: true }) +}) {} + +/** + * Error data for ProviderOverloadedResponse + */ +export class ProviderOverloadedResponseErrorData + extends S.Class("ProviderOverloadedResponseErrorData")({ + "code": S.Int, + "message": S.String, + "metadata": S.optionalWith(S.Record({ key: S.String, value: S.Unknown }), { nullable: true }) + }) +{} + +/** + * Provider Overloaded - Provider is temporarily overloaded + */ +export class ProviderOverloadedResponse extends S.Class("ProviderOverloadedResponse")({ + "error": ProviderOverloadedResponseErrorData, + "user_id": S.optionalWith(S.String, { nullable: true }) +}) {} + +export class OpenRouterAnthropicMessageParamRole extends S.Literal("user", "assistant") {} + +/** + * Anthropic message with OpenRouter extensions + */ +export class OpenRouterAnthropicMessageParam + extends S.Class("OpenRouterAnthropicMessageParam")({ + "role": OpenRouterAnthropicMessageParamRole, + "content": S.Union( + S.String, + S.Array(S.Union( + S.Struct({ + "type": S.Literal("text"), + "text": S.String, + "citations": S.optionalWith( + S.Array(S.Union( + S.Struct({ + "type": S.Literal("char_location"), + "cited_text": S.String, + "document_index": S.Number, + "document_title": S.NullOr(S.String), + "start_char_index": S.Number, + "end_char_index": S.Number + }), + S.Struct({ + "type": S.Literal("page_location"), + "cited_text": S.String, + "document_index": S.Number, + "document_title": S.NullOr(S.String), + "start_page_number": S.Number, + "end_page_number": S.Number + }), + S.Struct({ + "type": S.Literal("content_block_location"), + "cited_text": S.String, + "document_index": S.Number, + "document_title": S.NullOr(S.String), + "start_block_index": S.Number, + "end_block_index": S.Number + }), + S.Struct({ + "type": S.Literal("web_search_result_location"), + "cited_text": S.String, + "encrypted_index": S.String, + "title": S.NullOr(S.String), + "url": S.String + }), + S.Struct({ + "type": S.Literal("search_result_location"), + "cited_text": S.String, + "search_result_index": S.Number, + "source": S.String, + "title": S.NullOr(S.String), + "start_block_index": S.Number, + "end_block_index": S.Number + }) + )), + { nullable: true } + ), + "cache_control": S.optionalWith( + S.Struct({ + "type": S.Literal("ephemeral"), + "ttl": S.optionalWith(S.Literal("5m", "1h"), { nullable: true }) + }), + { nullable: true } + ) + }), + S.Struct({ + "type": S.Literal("image"), + "source": S.Union( + S.Struct({ + "type": S.Literal("base64"), + "media_type": S.Literal("image/jpeg", "image/png", "image/gif", "image/webp"), + "data": S.String + }), + S.Struct({ + "type": S.Literal("url"), + "url": S.String + }) + ), + "cache_control": S.optionalWith( + S.Struct({ + "type": S.Literal("ephemeral"), + "ttl": S.optionalWith(S.Literal("5m", "1h"), { nullable: true }) + }), + { nullable: true } + ) + }), + S.Struct({ + "type": S.Literal("document"), + "source": S.Union( + S.Struct({ + "type": S.Literal("base64"), + "media_type": S.Literal("application/pdf"), + "data": S.String + }), + S.Struct({ + "type": S.Literal("text"), + "media_type": S.Literal("text/plain"), + "data": S.String + }), + S.Struct({ + "type": S.Literal("content"), + "content": S.Union( + S.String, + S.Array(S.Union( + S.Struct({ + "type": S.Literal("text"), + "text": S.String, + "citations": S.optionalWith( + S.Array(S.Union( + S.Struct({ + "type": S.Literal("char_location"), + "cited_text": S.String, + "document_index": S.Number, + "document_title": S.NullOr(S.String), + "start_char_index": S.Number, + "end_char_index": S.Number + }), + S.Struct({ + "type": S.Literal("page_location"), + "cited_text": S.String, + "document_index": S.Number, + "document_title": S.NullOr(S.String), + "start_page_number": S.Number, + "end_page_number": S.Number + }), + S.Struct({ + "type": S.Literal("content_block_location"), + "cited_text": S.String, + "document_index": S.Number, + "document_title": S.NullOr(S.String), + "start_block_index": S.Number, + "end_block_index": S.Number + }), + S.Struct({ + "type": S.Literal("web_search_result_location"), + "cited_text": S.String, + "encrypted_index": S.String, + "title": S.NullOr(S.String), + "url": S.String + }), + S.Struct({ + "type": S.Literal("search_result_location"), + "cited_text": S.String, + "search_result_index": S.Number, + "source": S.String, + "title": S.NullOr(S.String), + "start_block_index": S.Number, + "end_block_index": S.Number + }) + )), + { nullable: true } + ), + "cache_control": S.optionalWith( + S.Struct({ + "type": S.Literal("ephemeral"), + "ttl": S.optionalWith(S.Literal("5m", "1h"), { nullable: true }) + }), + { nullable: true } + ) + }), + S.Struct({ + "type": S.Literal("image"), + "source": S.Union( + S.Struct({ + "type": S.Literal("base64"), + "media_type": S.Literal("image/jpeg", "image/png", "image/gif", "image/webp"), + "data": S.String + }), + S.Struct({ + "type": S.Literal("url"), + "url": S.String + }) + ), + "cache_control": S.optionalWith( + S.Struct({ + "type": S.Literal("ephemeral"), + "ttl": S.optionalWith(S.Literal("5m", "1h"), { nullable: true }) + }), + { nullable: true } + ) + }) + )) + ) + }), + S.Struct({ + "type": S.Literal("url"), + "url": S.String + }) + ), + "citations": S.optionalWith( + S.Struct({ + "enabled": S.optionalWith(S.Boolean, { nullable: true }) + }), + { nullable: true } + ), + "context": S.optionalWith(S.String, { nullable: true }), + "title": S.optionalWith(S.String, { nullable: true }), + "cache_control": S.optionalWith( + S.Struct({ + "type": S.Literal("ephemeral"), + "ttl": S.optionalWith(S.Literal("5m", "1h"), { nullable: true }) + }), + { nullable: true } + ) + }), + S.Struct({ + "type": S.Literal("tool_use"), + "id": S.String, + "name": S.String, + "cache_control": S.optionalWith( + S.Struct({ + "type": S.Literal("ephemeral"), + "ttl": S.optionalWith(S.Literal("5m", "1h"), { nullable: true }) + }), + { nullable: true } + ) + }), + S.Struct({ + "type": S.Literal("tool_result"), + "tool_use_id": S.String, + "content": S.optionalWith( + S.Union( + S.String, + S.Array(S.Union( + S.Struct({ + "type": S.Literal("text"), + "text": S.String, + "citations": S.optionalWith( + S.Array(S.Union( + S.Struct({ + "type": S.Literal("char_location"), + "cited_text": S.String, + "document_index": S.Number, + "document_title": S.NullOr(S.String), + "start_char_index": S.Number, + "end_char_index": S.Number + }), + S.Struct({ + "type": S.Literal("page_location"), + "cited_text": S.String, + "document_index": S.Number, + "document_title": S.NullOr(S.String), + "start_page_number": S.Number, + "end_page_number": S.Number + }), + S.Struct({ + "type": S.Literal("content_block_location"), + "cited_text": S.String, + "document_index": S.Number, + "document_title": S.NullOr(S.String), + "start_block_index": S.Number, + "end_block_index": S.Number + }), + S.Struct({ + "type": S.Literal("web_search_result_location"), + "cited_text": S.String, + "encrypted_index": S.String, + "title": S.NullOr(S.String), + "url": S.String + }), + S.Struct({ + "type": S.Literal("search_result_location"), + "cited_text": S.String, + "search_result_index": S.Number, + "source": S.String, + "title": S.NullOr(S.String), + "start_block_index": S.Number, + "end_block_index": S.Number + }) + )), + { nullable: true } + ), + "cache_control": S.optionalWith( + S.Struct({ + "type": S.Literal("ephemeral"), + "ttl": S.optionalWith(S.Literal("5m", "1h"), { nullable: true }) + }), + { nullable: true } + ) + }), + S.Struct({ + "type": S.Literal("image"), + "source": S.Union( + S.Struct({ + "type": S.Literal("base64"), + "media_type": S.Literal("image/jpeg", "image/png", "image/gif", "image/webp"), + "data": S.String + }), + S.Struct({ + "type": S.Literal("url"), + "url": S.String + }) + ), + "cache_control": S.optionalWith( + S.Struct({ + "type": S.Literal("ephemeral"), + "ttl": S.optionalWith(S.Literal("5m", "1h"), { nullable: true }) + }), + { nullable: true } + ) + }) + )) + ), + { nullable: true } + ), + "is_error": S.optionalWith(S.Boolean, { nullable: true }), + "cache_control": S.optionalWith( + S.Struct({ + "type": S.Literal("ephemeral"), + "ttl": S.optionalWith(S.Literal("5m", "1h"), { nullable: true }) + }), + { nullable: true } + ) + }), + S.Struct({ + "type": S.Literal("thinking"), + "thinking": S.String, + "signature": S.String + }), + S.Struct({ + "type": S.Literal("redacted_thinking"), + "data": S.String + }), + S.Struct({ + "type": S.Literal("server_tool_use"), + "id": S.String, + "name": S.Literal("web_search"), + "cache_control": S.optionalWith( + S.Struct({ + "type": S.Literal("ephemeral"), + "ttl": S.optionalWith(S.Literal("5m", "1h"), { nullable: true }) + }), + { nullable: true } + ) + }), + S.Struct({ + "type": S.Literal("web_search_tool_result"), + "tool_use_id": S.String, + "content": S.Union( + S.Array(S.Struct({ + "type": S.Literal("web_search_result"), + "encrypted_content": S.String, + "title": S.String, + "url": S.String, + "page_age": S.optionalWith(S.String, { nullable: true }) + })), + S.Struct({ + "type": S.Literal("web_search_tool_result_error"), + "error_code": S.Literal( + "invalid_tool_input", + "unavailable", + "max_uses_exceeded", + "too_many_requests", + "query_too_long" + ) + }) + ), + "cache_control": S.optionalWith( + S.Struct({ + "type": S.Literal("ephemeral"), + "ttl": S.optionalWith(S.Literal("5m", "1h"), { nullable: true }) + }), + { nullable: true } + ) + }), + S.Struct({ + "type": S.Literal("search_result"), + "source": S.String, + "title": S.String, + "content": S.Array(S.Struct({ + "type": S.Literal("text"), + "text": S.String, + "citations": S.optionalWith( + S.Array(S.Union( + S.Struct({ + "type": S.Literal("char_location"), + "cited_text": S.String, + "document_index": S.Number, + "document_title": S.NullOr(S.String), + "start_char_index": S.Number, + "end_char_index": S.Number + }), + S.Struct({ + "type": S.Literal("page_location"), + "cited_text": S.String, + "document_index": S.Number, + "document_title": S.NullOr(S.String), + "start_page_number": S.Number, + "end_page_number": S.Number + }), + S.Struct({ + "type": S.Literal("content_block_location"), + "cited_text": S.String, + "document_index": S.Number, + "document_title": S.NullOr(S.String), + "start_block_index": S.Number, + "end_block_index": S.Number + }), + S.Struct({ + "type": S.Literal("web_search_result_location"), + "cited_text": S.String, + "encrypted_index": S.String, + "title": S.NullOr(S.String), + "url": S.String + }), + S.Struct({ + "type": S.Literal("search_result_location"), + "cited_text": S.String, + "search_result_index": S.Number, + "source": S.String, + "title": S.NullOr(S.String), + "start_block_index": S.Number, + "end_block_index": S.Number + }) + )), + { nullable: true } + ), + "cache_control": S.optionalWith( + S.Struct({ + "type": S.Literal("ephemeral"), + "ttl": S.optionalWith(S.Literal("5m", "1h"), { nullable: true }) + }), + { nullable: true } + ) + })), + "citations": S.optionalWith( + S.Struct({ + "enabled": S.optionalWith(S.Boolean, { nullable: true }) + }), + { nullable: true } + ), + "cache_control": S.optionalWith( + S.Struct({ + "type": S.Literal("ephemeral"), + "ttl": S.optionalWith(S.Literal("5m", "1h"), { nullable: true }) + }), + { nullable: true } + ) + }) + )) + ) + }) +{} + +export class AnthropicMessagesRequestToolChoiceEnumType extends S.Literal("tool") {} + +export class AnthropicMessagesRequestThinkingEnumType extends S.Literal("disabled") {} + +export class AnthropicMessagesRequestServiceTier extends S.Literal("auto", "standard_only") {} + +/** + * The sorting strategy to use for this request, if "order" is not specified. When set, no load balancing is performed. + */ +export class AnthropicMessagesRequestProviderSort extends S.Literal("price", "throughput", "latency") {} + +/** + * **DEPRECATED** Use providers.sort.partition instead. Backwards-compatible alias for providers.sort.partition. Accepts legacy values: "fallback" (maps to "model"), "sort" (maps to "none"). + */ +export class AnthropicMessagesRequestRoute extends S.Literal("fallback", "sort") {} + +/** + * Request schema for Anthropic Messages API endpoint + */ +export class AnthropicMessagesRequest extends S.Class("AnthropicMessagesRequest")({ + "model": S.String, + "max_tokens": S.Number, + "messages": S.Array(OpenRouterAnthropicMessageParam), + "system": S.optionalWith( + S.Union( + S.String, + S.Array(S.Struct({ + "type": S.Literal("text"), + "text": S.String, + "citations": S.optionalWith( + S.Array(S.Union( + S.Struct({ + "type": S.Literal("char_location"), + "cited_text": S.String, + "document_index": S.Number, + "document_title": S.NullOr(S.String), + "start_char_index": S.Number, + "end_char_index": S.Number + }), + S.Struct({ + "type": S.Literal("page_location"), + "cited_text": S.String, + "document_index": S.Number, + "document_title": S.NullOr(S.String), + "start_page_number": S.Number, + "end_page_number": S.Number + }), + S.Struct({ + "type": S.Literal("content_block_location"), + "cited_text": S.String, + "document_index": S.Number, + "document_title": S.NullOr(S.String), + "start_block_index": S.Number, + "end_block_index": S.Number + }), + S.Struct({ + "type": S.Literal("web_search_result_location"), + "cited_text": S.String, + "encrypted_index": S.String, + "title": S.NullOr(S.String), + "url": S.String + }), + S.Struct({ + "type": S.Literal("search_result_location"), + "cited_text": S.String, + "search_result_index": S.Number, + "source": S.String, + "title": S.NullOr(S.String), + "start_block_index": S.Number, + "end_block_index": S.Number + }) + )), + { nullable: true } + ), + "cache_control": S.optionalWith( + S.Struct({ + "type": S.Literal("ephemeral"), + "ttl": S.optionalWith(S.Literal("5m", "1h"), { nullable: true }) + }), + { nullable: true } + ) + })) + ), + { nullable: true } + ), + "metadata": S.optionalWith( + S.Struct({ + "user_id": S.optionalWith(S.String, { nullable: true }) + }), + { nullable: true } + ), + "stop_sequences": S.optionalWith(S.Array(S.String), { nullable: true }), + "stream": S.optionalWith(S.Boolean, { nullable: true }), + "temperature": S.optionalWith(S.Number, { nullable: true }), + "top_p": S.optionalWith(S.Number, { nullable: true }), + "top_k": S.optionalWith(S.Number, { nullable: true }), + "tools": S.optionalWith( + S.Array(S.Union( + S.Struct({ + "name": S.String, + "description": S.optionalWith(S.String, { nullable: true }), + "input_schema": S.Struct({ + "type": S.Literal("object"), + "required": S.optionalWith(S.Array(S.String), { nullable: true }) + }), + "type": S.optionalWith(S.Literal("custom"), { nullable: true }), + "cache_control": S.optionalWith( + S.Struct({ + "type": S.Literal("ephemeral"), + "ttl": S.optionalWith(S.Literal("5m", "1h"), { nullable: true }) + }), + { nullable: true } + ) + }), + S.Struct({ + "type": S.Literal("bash_20250124"), + "name": S.Literal("bash"), + "cache_control": S.optionalWith( + S.Struct({ + "type": S.Literal("ephemeral"), + "ttl": S.optionalWith(S.Literal("5m", "1h"), { nullable: true }) + }), + { nullable: true } + ) + }), + S.Struct({ + "type": S.Literal("text_editor_20250124"), + "name": S.Literal("str_replace_editor"), + "cache_control": S.optionalWith( + S.Struct({ + "type": S.Literal("ephemeral"), + "ttl": S.optionalWith(S.Literal("5m", "1h"), { nullable: true }) + }), + { nullable: true } + ) + }), + S.Struct({ + "type": S.Literal("web_search_20250305"), + "name": S.Literal("web_search"), + "allowed_domains": S.optionalWith(S.Array(S.String), { nullable: true }), + "blocked_domains": S.optionalWith(S.Array(S.String), { nullable: true }), + "max_uses": S.optionalWith(S.Number, { nullable: true }), + "user_location": S.optionalWith( + S.Struct({ + "type": S.Literal("approximate"), + "city": S.optionalWith(S.String, { nullable: true }), + "country": S.optionalWith(S.String, { nullable: true }), + "region": S.optionalWith(S.String, { nullable: true }), + "timezone": S.optionalWith(S.String, { nullable: true }) + }), + { nullable: true } + ), + "cache_control": S.optionalWith( + S.Struct({ + "type": S.Literal("ephemeral"), + "ttl": S.optionalWith(S.Literal("5m", "1h"), { nullable: true }) + }), + { nullable: true } + ) + }) + )), + { nullable: true } + ), + "tool_choice": S.optionalWith( + S.Union( + S.Struct({ + "type": AnthropicMessagesRequestToolChoiceEnumType, + "disable_parallel_tool_use": S.optionalWith(S.Boolean, { nullable: true }) + }), + S.Struct({ + "type": AnthropicMessagesRequestToolChoiceEnumType, + "disable_parallel_tool_use": S.optionalWith(S.Boolean, { nullable: true }) + }), + S.Struct({ + "type": AnthropicMessagesRequestToolChoiceEnumType + }), + S.Struct({ + "type": AnthropicMessagesRequestToolChoiceEnumType, + "name": S.String, + "disable_parallel_tool_use": S.optionalWith(S.Boolean, { nullable: true }) + }) + ), + { nullable: true } + ), + "thinking": S.optionalWith( + S.Union( + S.Struct({ + "type": AnthropicMessagesRequestThinkingEnumType, + "budget_tokens": S.Number + }), + S.Struct({ + "type": AnthropicMessagesRequestThinkingEnumType + }) + ), + { nullable: true } + ), + "service_tier": S.optionalWith(AnthropicMessagesRequestServiceTier, { nullable: true }), + /** + * When multiple model providers are available, optionally indicate your routing preference. + */ + "provider": S.optionalWith( + S.Struct({ + /** + * Whether to allow backup providers to serve requests + * - true: (default) when the primary provider (or your custom providers in "order") is unavailable, use the next best provider. + * - false: use only the primary/custom provider, and return the upstream error if it's unavailable. + */ + "allow_fallbacks": S.optionalWith(S.Boolean, { nullable: true }), + /** + * Whether to filter providers to only those that support the parameters you've provided. If this setting is omitted or set to false, then providers will receive only the parameters they support, and ignore the rest. + */ + "require_parameters": S.optionalWith(S.Boolean, { nullable: true }), + "data_collection": S.optionalWith(DataCollection, { nullable: true }), + /** + * Whether to restrict routing to only ZDR (Zero Data Retention) endpoints. When true, only endpoints that do not retain prompts will be used. + */ + "zdr": S.optionalWith(S.Boolean, { nullable: true }), + /** + * Whether to restrict routing to only models that allow text distillation. When true, only models where the author has allowed distillation will be used. + */ + "enforce_distillable_text": S.optionalWith(S.Boolean, { nullable: true }), + /** + * An ordered list of provider slugs. The router will attempt to use the first provider in the subset of this list that supports your requested model, and fall back to the next if it is unavailable. If no providers are available, the request will fail with an error message. + */ + "order": S.optionalWith(S.Array(S.Union(ProviderName, S.String)), { nullable: true }), + /** + * List of provider slugs to allow. If provided, this list is merged with your account-wide allowed provider settings for this request. + */ + "only": S.optionalWith(S.Array(S.Union(ProviderName, S.String)), { nullable: true }), + /** + * List of provider slugs to ignore. If provided, this list is merged with your account-wide ignored provider settings for this request. + */ + "ignore": S.optionalWith(S.Array(S.Union(ProviderName, S.String)), { nullable: true }), + /** + * A list of quantization levels to filter the provider by. + */ + "quantizations": S.optionalWith(S.Array(Quantization), { nullable: true }), + "sort": S.optionalWith(AnthropicMessagesRequestProviderSort, { nullable: true }), + /** + * The object specifying the maximum price you want to pay for this request. USD price per million tokens, for prompt and completion. + */ + "max_price": S.optionalWith( + S.Struct({ + "prompt": S.optionalWith(BigNumberUnion, { nullable: true }), + "completion": S.optionalWith(BigNumberUnion, { nullable: true }), + "image": S.optionalWith(BigNumberUnion, { nullable: true }), + "audio": S.optionalWith(BigNumberUnion, { nullable: true }), + "request": S.optionalWith(BigNumberUnion, { nullable: true }) + }), + { nullable: true } + ), + "preferred_min_throughput": S.optionalWith(PreferredMinThroughput, { nullable: true }), + "preferred_max_latency": S.optionalWith(PreferredMaxLatency, { nullable: true }) + }), + { nullable: true } + ), + /** + * Plugins you want to enable for this request, including their settings. + */ + "plugins": S.optionalWith( + S.Array(S.Union( + S.Struct({ + "id": S.Literal("auto-router"), + /** + * Set to false to disable the auto-router plugin for this request. Defaults to true. + */ + "enabled": S.optionalWith(S.Boolean, { nullable: true }), + /** + * List of model patterns to filter which models the auto-router can route between. Supports wildcards (e.g., "anthropic/*" matches all Anthropic models). When not specified, uses the default supported models list. + */ + "allowed_models": S.optionalWith(S.Array(S.String), { nullable: true }) + }), + S.Struct({ + "id": S.Literal("moderation") + }), + S.Struct({ + "id": S.Literal("web"), + /** + * Set to false to disable the web-search plugin for this request. Defaults to true. + */ + "enabled": S.optionalWith(S.Boolean, { nullable: true }), + "max_results": S.optionalWith(S.Number, { nullable: true }), + "search_prompt": S.optionalWith(S.String, { nullable: true }), + "engine": S.optionalWith(WebSearchEngine, { nullable: true }) + }), + S.Struct({ + "id": S.Literal("file-parser"), + /** + * Set to false to disable the file-parser plugin for this request. Defaults to true. + */ + "enabled": S.optionalWith(S.Boolean, { nullable: true }), + "pdf": S.optionalWith(PDFParserOptions, { nullable: true }) + }), + S.Struct({ + "id": S.Literal("response-healing"), + /** + * Set to false to disable the response-healing plugin for this request. Defaults to true. + */ + "enabled": S.optionalWith(S.Boolean, { nullable: true }) + }) + )), + { nullable: true } + ), + /** + * **DEPRECATED** Use providers.sort.partition instead. Backwards-compatible alias for providers.sort.partition. Accepts legacy values: "fallback" (maps to "model"), "sort" (maps to "none"). + */ + "route": S.optionalWith(AnthropicMessagesRequestRoute, { nullable: true }), + /** + * A unique identifier representing your end-user, which helps distinguish between different users of your app. This allows your app to identify specific users in case of abuse reports, preventing your entire app from being affected by the actions of individual users. Maximum of 128 characters. + */ + "user": S.optionalWith(S.String.pipe(S.maxLength(128)), { nullable: true }), + /** + * A unique identifier for grouping related requests (e.g., a conversation or agent workflow) for observability. If provided in both the request body and the x-session-id header, the body value takes precedence. Maximum of 128 characters. + */ + "session_id": S.optionalWith(S.String.pipe(S.maxLength(128)), { nullable: true }), + "models": S.optionalWith(S.Array(S.String), { nullable: true }) +}) {} + +export class AnthropicMessagesResponseType extends S.Literal("message") {} + +export class AnthropicMessagesResponseRole extends S.Literal("assistant") {} + +export class AnthropicMessagesResponseStopReason extends S.Literal( + "end_turn", + "max_tokens", + "stop_sequence", + "tool_use", + "pause_turn", + "refusal", + "model_context_window_exceeded" +) {} + +export class AnthropicMessagesResponseUsageServiceTier extends S.Literal("standard", "priority", "batch") {} + +export class AnthropicMessagesResponse extends S.Class("AnthropicMessagesResponse")({ + "id": S.String, + "type": AnthropicMessagesResponseType, + "role": AnthropicMessagesResponseRole, + "content": S.Array(S.Union( + S.Struct({ + "type": S.Literal("text"), + "text": S.String, + "citations": S.NullOr(S.Array(S.Union( + S.Struct({ + "type": S.Literal("char_location"), + "cited_text": S.String, + "document_index": S.Number, + "document_title": S.NullOr(S.String), + "start_char_index": S.Number, + "end_char_index": S.Number, + "file_id": S.NullOr(S.String) + }), + S.Struct({ + "type": S.Literal("page_location"), + "cited_text": S.String, + "document_index": S.Number, + "document_title": S.NullOr(S.String), + "start_page_number": S.Number, + "end_page_number": S.Number, + "file_id": S.NullOr(S.String) + }), + S.Struct({ + "type": S.Literal("content_block_location"), + "cited_text": S.String, + "document_index": S.Number, + "document_title": S.NullOr(S.String), + "start_block_index": S.Number, + "end_block_index": S.Number, + "file_id": S.NullOr(S.String) + }), + S.Struct({ + "type": S.Literal("web_search_result_location"), + "cited_text": S.String, + "encrypted_index": S.String, + "title": S.NullOr(S.String), + "url": S.String + }), + S.Struct({ + "type": S.Literal("search_result_location"), + "cited_text": S.String, + "search_result_index": S.Number, + "source": S.String, + "title": S.NullOr(S.String), + "start_block_index": S.Number, + "end_block_index": S.Number + }) + ))) + }), + S.Struct({ + "type": S.Literal("tool_use"), + "id": S.String, + "name": S.String + }), + S.Struct({ + "type": S.Literal("thinking"), + "thinking": S.String, + "signature": S.String + }), + S.Struct({ + "type": S.Literal("redacted_thinking"), + "data": S.String + }), + S.Struct({ + "type": S.Literal("server_tool_use"), + "id": S.String, + "name": S.Literal("web_search") + }), + S.Struct({ + "type": S.Literal("web_search_tool_result"), + "tool_use_id": S.String, + "content": S.Union( + S.Array(S.Struct({ + "type": S.Literal("web_search_result"), + "encrypted_content": S.String, + "page_age": S.NullOr(S.String), + "title": S.String, + "url": S.String + })), + S.Struct({ + "type": S.Literal("web_search_tool_result_error"), + "error_code": S.Literal( + "invalid_tool_input", + "unavailable", + "max_uses_exceeded", + "too_many_requests", + "query_too_long" + ) + }) + ) + }) + )), + "model": S.String, + "stop_reason": S.NullOr(AnthropicMessagesResponseStopReason), + "stop_sequence": S.NullOr(S.String), + "usage": S.Struct({ + "input_tokens": S.Number, + "output_tokens": S.Number, + "cache_creation_input_tokens": S.NullOr(S.Number), + "cache_read_input_tokens": S.NullOr(S.Number), + "cache_creation": S.NullOr(S.Struct({ + "ephemeral_5m_input_tokens": S.Number, + "ephemeral_1h_input_tokens": S.Number + })), + "server_tool_use": S.NullOr(S.Struct({ + "web_search_requests": S.Number + })), + "service_tier": S.NullOr(AnthropicMessagesResponseUsageServiceTier) + }) +}) {} + +export class CreateMessages400Type extends S.Literal("error") {} + +export class CreateMessages400 extends S.Struct({ + "type": CreateMessages400Type, + "error": S.Struct({ + "type": S.String, + "message": S.String + }) +}) {} + +export class CreateMessages401Type extends S.Literal("error") {} + +export class CreateMessages401 extends S.Struct({ + "type": CreateMessages401Type, + "error": S.Struct({ + "type": S.String, + "message": S.String + }) +}) {} + +export class CreateMessages403Type extends S.Literal("error") {} + +export class CreateMessages403 extends S.Struct({ + "type": CreateMessages403Type, + "error": S.Struct({ + "type": S.String, + "message": S.String + }) +}) {} + +export class CreateMessages404Type extends S.Literal("error") {} + +export class CreateMessages404 extends S.Struct({ + "type": CreateMessages404Type, + "error": S.Struct({ + "type": S.String, + "message": S.String + }) +}) {} + +export class CreateMessages429Type extends S.Literal("error") {} + +export class CreateMessages429 extends S.Struct({ + "type": CreateMessages429Type, + "error": S.Struct({ + "type": S.String, + "message": S.String + }) +}) {} + +export class CreateMessages500Type extends S.Literal("error") {} + +export class CreateMessages500 extends S.Struct({ + "type": CreateMessages500Type, + "error": S.Struct({ + "type": S.String, + "message": S.String + }) +}) {} + +export class CreateMessages503Type extends S.Literal("error") {} + +export class CreateMessages503 extends S.Struct({ + "type": CreateMessages503Type, + "error": S.Struct({ + "type": S.String, + "message": S.String + }) +}) {} + +export class CreateMessages529Type extends S.Literal("error") {} + +export class CreateMessages529 extends S.Struct({ + "type": CreateMessages529Type, + "error": S.Struct({ + "type": S.String, + "message": S.String + }) +}) {} + +export class GetUserActivityParams extends S.Struct({ + /** + * Filter by a single UTC date in the last 30 days (YYYY-MM-DD format). + */ + "date": S.optionalWith(S.String, { nullable: true }) +}) {} + +export class ActivityItem extends S.Class("ActivityItem")({ + /** + * Date of the activity (YYYY-MM-DD format) + */ + "date": S.String, + /** + * Model slug (e.g., "openai/gpt-4.1") + */ + "model": S.String, + /** + * Model permaslug (e.g., "openai/gpt-4.1-2025-04-14") + */ + "model_permaslug": S.String, + /** + * Unique identifier for the endpoint + */ + "endpoint_id": S.String, + /** + * Name of the provider serving this endpoint + */ + "provider_name": S.String, + /** + * Total cost in USD (OpenRouter credits spent) + */ + "usage": S.Number, + /** + * BYOK inference cost in USD (external credits spent) + */ + "byok_usage_inference": S.Number, + /** + * Number of requests made + */ + "requests": S.Number, + /** + * Total prompt tokens used + */ + "prompt_tokens": S.Number, + /** + * Total completion tokens generated + */ + "completion_tokens": S.Number, + /** + * Total reasoning tokens used + */ + "reasoning_tokens": S.Number +}) {} + +export class GetUserActivity200 extends S.Struct({ + /** + * List of activity items + */ + "data": S.Array(ActivityItem) +}) {} + +/** + * Error data for ForbiddenResponse + */ +export class ForbiddenResponseErrorData extends S.Class("ForbiddenResponseErrorData")({ + "code": S.Int, + "message": S.String, + "metadata": S.optionalWith(S.Record({ key: S.String, value: S.Unknown }), { nullable: true }) +}) {} + +/** + * Forbidden - Authentication successful but insufficient permissions + */ +export class ForbiddenResponse extends S.Class("ForbiddenResponse")({ + "error": ForbiddenResponseErrorData, + "user_id": S.optionalWith(S.String, { nullable: true }) +}) {} + +/** + * Total credits purchased and used + */ +export class GetCredits200 extends S.Struct({ + "data": S.Struct({ + /** + * Total credits purchased + */ + "total_credits": S.Number, + /** + * Total credits used + */ + "total_usage": S.Number + }) +}) {} + +export class CreateChargeRequestChainId extends S.Literal(1, 137, 8453) {} + +/** + * Create a Coinbase charge for crypto payment + */ +export class CreateChargeRequest extends S.Class("CreateChargeRequest")({ + "amount": S.Number, + "sender": S.String, + "chain_id": CreateChargeRequestChainId +}) {} + +export class CreateCoinbaseCharge200 extends S.Struct({ + "data": S.Struct({ + "id": S.String, + "created_at": S.String, + "expires_at": S.String, + "web3_data": S.Struct({ + "transfer_intent": S.Struct({ + "call_data": S.Struct({ + "deadline": S.String, + "fee_amount": S.String, + "id": S.String, + "operator": S.String, + "prefix": S.String, + "recipient": S.String, + "recipient_amount": S.String, + "recipient_currency": S.String, + "refund_destination": S.String, + "signature": S.String + }), + "metadata": S.Struct({ + "chain_id": S.Number, + "contract_address": S.String, + "sender": S.String + }) + }) + }) + }) +}) {} + +export class CreateEmbeddingsRequestEncodingFormat extends S.Literal("float", "base64") {} + +/** + * The sorting strategy to use for this request, if "order" is not specified. When set, no load balancing is performed. + */ +export class ProviderPreferencesSort extends S.Literal("price", "throughput", "latency") {} + +/** + * Provider routing preferences for the request. + */ +export class ProviderPreferences extends S.Class("ProviderPreferences")({ + /** + * Whether to allow backup providers to serve requests + * - true: (default) when the primary provider (or your custom providers in "order") is unavailable, use the next best provider. + * - false: use only the primary/custom provider, and return the upstream error if it's unavailable. + */ + "allow_fallbacks": S.optionalWith(S.Boolean, { nullable: true }), + /** + * Whether to filter providers to only those that support the parameters you've provided. If this setting is omitted or set to false, then providers will receive only the parameters they support, and ignore the rest. + */ + "require_parameters": S.optionalWith(S.Boolean, { nullable: true }), + "data_collection": S.optionalWith(DataCollection, { nullable: true }), + /** + * Whether to restrict routing to only ZDR (Zero Data Retention) endpoints. When true, only endpoints that do not retain prompts will be used. + */ + "zdr": S.optionalWith(S.Boolean, { nullable: true }), + /** + * Whether to restrict routing to only models that allow text distillation. When true, only models where the author has allowed distillation will be used. + */ + "enforce_distillable_text": S.optionalWith(S.Boolean, { nullable: true }), + /** + * An ordered list of provider slugs. The router will attempt to use the first provider in the subset of this list that supports your requested model, and fall back to the next if it is unavailable. If no providers are available, the request will fail with an error message. + */ + "order": S.optionalWith(S.Array(S.Union(ProviderName, S.String)), { nullable: true }), + /** + * List of provider slugs to allow. If provided, this list is merged with your account-wide allowed provider settings for this request. + */ + "only": S.optionalWith(S.Array(S.Union(ProviderName, S.String)), { nullable: true }), + /** + * List of provider slugs to ignore. If provided, this list is merged with your account-wide ignored provider settings for this request. + */ + "ignore": S.optionalWith(S.Array(S.Union(ProviderName, S.String)), { nullable: true }), + /** + * A list of quantization levels to filter the provider by. + */ + "quantizations": S.optionalWith(S.Array(Quantization), { nullable: true }), + "sort": S.optionalWith(ProviderPreferencesSort, { nullable: true }), + /** + * The object specifying the maximum price you want to pay for this request. USD price per million tokens, for prompt and completion. + */ + "max_price": S.optionalWith( + S.Struct({ + "prompt": S.optionalWith(BigNumberUnion, { nullable: true }), + "completion": S.optionalWith(BigNumberUnion, { nullable: true }), + "image": S.optionalWith(BigNumberUnion, { nullable: true }), + "audio": S.optionalWith(BigNumberUnion, { nullable: true }), + "request": S.optionalWith(BigNumberUnion, { nullable: true }) + }), + { nullable: true } + ), + "preferred_min_throughput": S.optionalWith(PreferredMinThroughput, { nullable: true }), + "preferred_max_latency": S.optionalWith(PreferredMaxLatency, { nullable: true }) +}) {} + +export class CreateEmbeddingsRequest extends S.Class("CreateEmbeddingsRequest")({ + "input": S.Union( + S.String, + S.Array(S.String), + S.Array(S.Number), + S.Array(S.Array(S.Number)), + S.Array(S.Struct({ + "content": S.Array(S.Union( + S.Struct({ + "type": S.Literal("text"), + "text": S.String + }), + S.Struct({ + "type": S.Literal("image_url"), + "image_url": S.Struct({ + "url": S.String + }) + }) + )) + })) + ), + "model": S.String, + "encoding_format": S.optionalWith(CreateEmbeddingsRequestEncodingFormat, { nullable: true }), + "dimensions": S.optionalWith(S.Int.pipe(S.greaterThan(0)), { nullable: true }), + "user": S.optionalWith(S.String, { nullable: true }), + "provider": S.optionalWith(ProviderPreferences, { nullable: true }), + "input_type": S.optionalWith(S.String, { nullable: true }) +}) {} + +export class CreateEmbeddings200Object extends S.Literal("list") {} + +export class CreateEmbeddings200 extends S.Struct({ + "id": S.optionalWith(S.String, { nullable: true }), + "object": CreateEmbeddings200Object, + "data": S.Array(S.Struct({ + "object": S.Literal("embedding"), + "embedding": S.Union(S.Array(S.Number), S.String), + "index": S.optionalWith(S.Number, { nullable: true }) + })), + "model": S.String, + "usage": S.optionalWith( + S.Struct({ + "prompt_tokens": S.Number, + "total_tokens": S.Number, + "cost": S.optionalWith(S.Number, { nullable: true }) + }), + { nullable: true } + ) +}) {} + +/** + * Pricing information for the model + */ +export class PublicPricing extends S.Class("PublicPricing")({ + "prompt": BigNumberUnion, + "completion": BigNumberUnion, + "request": S.optionalWith(BigNumberUnion, { nullable: true }), + "image": S.optionalWith(BigNumberUnion, { nullable: true }), + "image_token": S.optionalWith(BigNumberUnion, { nullable: true }), + "image_output": S.optionalWith(BigNumberUnion, { nullable: true }), + "audio": S.optionalWith(BigNumberUnion, { nullable: true }), + "audio_output": S.optionalWith(BigNumberUnion, { nullable: true }), + "input_audio_cache": S.optionalWith(BigNumberUnion, { nullable: true }), + "web_search": S.optionalWith(BigNumberUnion, { nullable: true }), + "internal_reasoning": S.optionalWith(BigNumberUnion, { nullable: true }), + "input_cache_read": S.optionalWith(BigNumberUnion, { nullable: true }), + "input_cache_write": S.optionalWith(BigNumberUnion, { nullable: true }), + "discount": S.optionalWith(S.Number, { nullable: true }) +}) {} + +/** + * Tokenizer type used by the model + */ +export class ModelGroup extends S.Literal( + "Router", + "Media", + "Other", + "GPT", + "Claude", + "Gemini", + "Grok", + "Cohere", + "Nova", + "Qwen", + "Yi", + "DeepSeek", + "Mistral", + "Llama2", + "Llama3", + "Llama4", + "PaLM", + "RWKV", + "Qwen3" +) {} + +/** + * Instruction format type + */ +export class ModelArchitectureInstructType extends S.Literal( + "none", + "airoboros", + "alpaca", + "alpaca-modif", + "chatml", + "claude", + "code-llama", + "gemma", + "llama2", + "llama3", + "mistral", + "nemotron", + "neural", + "openchat", + "phi3", + "rwkv", + "vicuna", + "zephyr", + "deepseek-r1", + "deepseek-v3.1", + "qwq", + "qwen3" +) {} + +export class InputModality extends S.Literal("text", "image", "file", "audio", "video") {} + +export class OutputModality extends S.Literal("text", "image", "embeddings", "audio") {} + +/** + * Model architecture information + */ +export class ModelArchitecture extends S.Class("ModelArchitecture")({ + "tokenizer": S.optionalWith(ModelGroup, { nullable: true }), + /** + * Instruction format type + */ + "instruct_type": S.optionalWith(ModelArchitectureInstructType, { nullable: true }), + /** + * Primary modality of the model + */ + "modality": S.NullOr(S.String), + /** + * Supported input modalities + */ + "input_modalities": S.Array(InputModality), + /** + * Supported output modalities + */ + "output_modalities": S.Array(OutputModality) +}) {} + +/** + * Information about the top provider for this model + */ +export class TopProviderInfo extends S.Class("TopProviderInfo")({ + /** + * Context length from the top provider + */ + "context_length": S.optionalWith(S.Number, { nullable: true }), + /** + * Maximum completion tokens from the top provider + */ + "max_completion_tokens": S.optionalWith(S.Number, { nullable: true }), + /** + * Whether the top provider moderates content + */ + "is_moderated": S.Boolean +}) {} + +/** + * Per-request token limits + */ +export class PerRequestLimits extends S.Class("PerRequestLimits")({ + /** + * Maximum prompt tokens per request + */ + "prompt_tokens": S.Number, + /** + * Maximum completion tokens per request + */ + "completion_tokens": S.Number +}) {} + +export class Parameter extends S.Literal( + "temperature", + "top_p", + "top_k", + "min_p", + "top_a", + "frequency_penalty", + "presence_penalty", + "repetition_penalty", + "max_tokens", + "logit_bias", + "logprobs", + "top_logprobs", + "seed", + "response_format", + "structured_outputs", + "stop", + "tools", + "tool_choice", + "parallel_tool_calls", + "include_reasoning", + "reasoning", + "reasoning_effort", + "web_search_options", + "verbosity" +) {} + +/** + * Default parameters for this model + */ +export class DefaultParameters extends S.Class("DefaultParameters")({ + "temperature": S.optionalWith(S.Number.pipe(S.greaterThanOrEqualTo(0), S.lessThanOrEqualTo(2)), { nullable: true }), + "top_p": S.optionalWith(S.Number.pipe(S.greaterThanOrEqualTo(0), S.lessThanOrEqualTo(1)), { nullable: true }), + "frequency_penalty": S.optionalWith(S.Number.pipe(S.greaterThanOrEqualTo(-2), S.lessThanOrEqualTo(2)), { + nullable: true + }) +}) {} + +/** + * Information about an AI model available on OpenRouter + */ +export class Model extends S.Class("Model")({ + /** + * Unique identifier for the model + */ + "id": S.String, + /** + * Canonical slug for the model + */ + "canonical_slug": S.String, + /** + * Hugging Face model identifier, if applicable + */ + "hugging_face_id": S.optionalWith(S.String, { nullable: true }), + /** + * Display name of the model + */ + "name": S.String, + /** + * Unix timestamp of when the model was created + */ + "created": S.Number, + /** + * Description of the model + */ + "description": S.optionalWith(S.String, { nullable: true }), + "pricing": PublicPricing, + /** + * Maximum context length in tokens + */ + "context_length": S.NullOr(S.Number), + "architecture": ModelArchitecture, + "top_provider": TopProviderInfo, + "per_request_limits": S.NullOr(PerRequestLimits), + /** + * List of supported parameters for this model + */ + "supported_parameters": S.Array(Parameter), + "default_parameters": S.NullOr(DefaultParameters), + /** + * The date after which the model may be removed. ISO 8601 date string (YYYY-MM-DD) or null if no expiration. + */ + "expiration_date": S.optionalWith(S.String, { nullable: true }) +}) {} + +/** + * List of available models + */ +export class ModelsListResponseData extends S.Array(Model) {} + +/** + * List of available models + */ +export class ModelsListResponse extends S.Class("ModelsListResponse")({ + "data": ModelsListResponseData +}) {} + +export class GetGenerationParams extends S.Struct({ + "id": S.String.pipe(S.minLength(1)) +}) {} + +/** + * Type of API used for the generation + */ +export class GetGeneration200DataApiType extends S.Literal("completions", "embeddings") {} + +/** + * Generation response + */ +export class GetGeneration200 extends S.Struct({ + /** + * Generation data + */ + "data": S.Struct({ + /** + * Unique identifier for the generation + */ + "id": S.String, + /** + * Upstream provider's identifier for this generation + */ + "upstream_id": S.NullOr(S.String), + /** + * Total cost of the generation in USD + */ + "total_cost": S.Number, + /** + * Discount applied due to caching + */ + "cache_discount": S.NullOr(S.Number), + /** + * Cost charged by the upstream provider + */ + "upstream_inference_cost": S.NullOr(S.Number), + /** + * ISO 8601 timestamp of when the generation was created + */ + "created_at": S.String, + /** + * Model used for the generation + */ + "model": S.String, + /** + * ID of the app that made the request + */ + "app_id": S.NullOr(S.Number), + /** + * Whether the response was streamed + */ + "streamed": S.NullOr(S.Boolean), + /** + * Whether the generation was cancelled + */ + "cancelled": S.NullOr(S.Boolean), + /** + * Name of the provider that served the request + */ + "provider_name": S.NullOr(S.String), + /** + * Total latency in milliseconds + */ + "latency": S.NullOr(S.Number), + /** + * Moderation latency in milliseconds + */ + "moderation_latency": S.NullOr(S.Number), + /** + * Time taken for generation in milliseconds + */ + "generation_time": S.NullOr(S.Number), + /** + * Reason the generation finished + */ + "finish_reason": S.NullOr(S.String), + /** + * Number of tokens in the prompt + */ + "tokens_prompt": S.NullOr(S.Number), + /** + * Number of tokens in the completion + */ + "tokens_completion": S.NullOr(S.Number), + /** + * Native prompt tokens as reported by provider + */ + "native_tokens_prompt": S.NullOr(S.Number), + /** + * Native completion tokens as reported by provider + */ + "native_tokens_completion": S.NullOr(S.Number), + /** + * Native completion image tokens as reported by provider + */ + "native_tokens_completion_images": S.NullOr(S.Number), + /** + * Native reasoning tokens as reported by provider + */ + "native_tokens_reasoning": S.NullOr(S.Number), + /** + * Native cached tokens as reported by provider + */ + "native_tokens_cached": S.NullOr(S.Number), + /** + * Number of media items in the prompt + */ + "num_media_prompt": S.NullOr(S.Number), + /** + * Number of audio inputs in the prompt + */ + "num_input_audio_prompt": S.NullOr(S.Number), + /** + * Number of media items in the completion + */ + "num_media_completion": S.NullOr(S.Number), + /** + * Number of search results included + */ + "num_search_results": S.NullOr(S.Number), + /** + * Origin URL of the request + */ + "origin": S.String, + /** + * Usage amount in USD + */ + "usage": S.Number, + /** + * Whether this used bring-your-own-key + */ + "is_byok": S.Boolean, + /** + * Native finish reason as reported by provider + */ + "native_finish_reason": S.NullOr(S.String), + /** + * External user identifier + */ + "external_user": S.NullOr(S.String), + /** + * Type of API used for the generation + */ + "api_type": S.NullOr(GetGeneration200DataApiType), + /** + * Router used for the request (e.g., openrouter/auto) + */ + "router": S.NullOr(S.String) + }) +}) {} + +/** + * Model count data + */ +export class ModelsCountResponse extends S.Class("ModelsCountResponse")({ + /** + * Model count data + */ + "data": S.Struct({ + /** + * Total number of available models + */ + "count": S.Number + }) +}) {} + +/** + * Filter models by use case category + */ +export class GetModelsParamsCategory extends S.Literal( + "programming", + "roleplay", + "marketing", + "marketing/seo", + "technology", + "science", + "translation", + "legal", + "finance", + "health", + "trivia", + "academia" +) {} + +export class GetModelsParams extends S.Struct({ + /** + * Filter models by use case category + */ + "category": S.optionalWith(GetModelsParamsCategory, { nullable: true }), + "supported_parameters": S.optionalWith(S.String, { nullable: true }) +}) {} + +/** + * Instruction format type + */ +export class ListEndpointsResponseArchitectureEnumInstructType extends S.Literal( + "none", + "airoboros", + "alpaca", + "alpaca-modif", + "chatml", + "claude", + "code-llama", + "gemma", + "llama2", + "llama3", + "mistral", + "nemotron", + "neural", + "openchat", + "phi3", + "rwkv", + "vicuna", + "zephyr", + "deepseek-r1", + "deepseek-v3.1", + "qwq", + "qwen3" +) {} + +/** + * Model architecture information + */ +export class ListEndpointsResponseArchitecture extends S.Struct({ + "tokenizer": ModelGroup, + /** + * Instruction format type + */ + "instruct_type": S.NullOr( + S.Literal( + "none", + "airoboros", + "alpaca", + "alpaca-modif", + "chatml", + "claude", + "code-llama", + "gemma", + "llama2", + "llama3", + "mistral", + "nemotron", + "neural", + "openchat", + "phi3", + "rwkv", + "vicuna", + "zephyr", + "deepseek-r1", + "deepseek-v3.1", + "qwq", + "qwen3" + ) + ), + /** + * Primary modality of the model + */ + "modality": S.NullOr(S.String), + /** + * Supported input modalities + */ + "input_modalities": S.Array(InputModality), + /** + * Supported output modalities + */ + "output_modalities": S.Array(OutputModality) +}) {} + +export class PublicEndpointQuantizationEnum + extends S.Literal("int4", "int8", "fp4", "fp6", "fp8", "fp16", "bf16", "fp32", "unknown") +{} + +export class PublicEndpointQuantization extends PublicEndpointQuantizationEnum {} + +export class EndpointStatus extends S.Literal(0, -1, -2, -3, -5, -10) {} + +/** + * Latency percentiles in milliseconds over the last 30 minutes. Latency measures time to first token. Only visible when authenticated with an API key or cookie; returns null for unauthenticated requests. + */ +export class PercentileStats extends S.Class("PercentileStats")({ + /** + * Median (50th percentile) + */ + "p50": S.Number, + /** + * 75th percentile + */ + "p75": S.Number, + /** + * 90th percentile + */ + "p90": S.Number, + /** + * 99th percentile + */ + "p99": S.Number +}) {} + +/** + * Throughput percentiles in tokens per second over the last 30 minutes. Throughput measures output token generation speed. Only visible when authenticated with an API key or cookie; returns null for unauthenticated requests. + */ +export class PublicEndpointThroughputLast30M extends S.Struct({ + /** + * Median (50th percentile) + */ + "p50": S.Number, + /** + * 75th percentile + */ + "p75": S.Number, + /** + * 90th percentile + */ + "p90": S.Number, + /** + * 99th percentile + */ + "p99": S.Number +}) {} + +/** + * Information about a specific model endpoint + */ +export class PublicEndpoint extends S.Class("PublicEndpoint")({ + "name": S.String, + /** + * The unique identifier for the model (permaslug) + */ + "model_id": S.String, + "model_name": S.String, + "context_length": S.Number, + "pricing": S.Struct({ + "prompt": BigNumberUnion, + "completion": BigNumberUnion, + "request": S.optionalWith(BigNumberUnion, { nullable: true }), + "image": S.optionalWith(BigNumberUnion, { nullable: true }), + "image_token": S.optionalWith(BigNumberUnion, { nullable: true }), + "image_output": S.optionalWith(BigNumberUnion, { nullable: true }), + "audio": S.optionalWith(BigNumberUnion, { nullable: true }), + "audio_output": S.optionalWith(BigNumberUnion, { nullable: true }), + "input_audio_cache": S.optionalWith(BigNumberUnion, { nullable: true }), + "web_search": S.optionalWith(BigNumberUnion, { nullable: true }), + "internal_reasoning": S.optionalWith(BigNumberUnion, { nullable: true }), + "input_cache_read": S.optionalWith(BigNumberUnion, { nullable: true }), + "input_cache_write": S.optionalWith(BigNumberUnion, { nullable: true }), + "discount": S.optionalWith(S.Number, { nullable: true }) + }), + "provider_name": ProviderName, + "tag": S.String, + "quantization": PublicEndpointQuantization, + "max_completion_tokens": S.NullOr(S.Number), + "max_prompt_tokens": S.NullOr(S.Number), + "supported_parameters": S.Array(Parameter), + "status": S.optionalWith(EndpointStatus, { nullable: true }), + "uptime_last_30m": S.NullOr(S.Number), + "supports_implicit_caching": S.Boolean, + "latency_last_30m": S.NullOr(PercentileStats), + "throughput_last_30m": PublicEndpointThroughputLast30M +}) {} + +/** + * List of available endpoints for a model + */ +export class ListEndpointsResponse extends S.Class("ListEndpointsResponse")({ + /** + * Unique identifier for the model + */ + "id": S.String, + /** + * Display name of the model + */ + "name": S.String, + /** + * Unix timestamp of when the model was created + */ + "created": S.Number, + /** + * Description of the model + */ + "description": S.String, + "architecture": ListEndpointsResponseArchitecture, + /** + * List of available endpoints for this model + */ + "endpoints": S.Array(PublicEndpoint) +}) {} + +export class ListEndpoints200 extends S.Struct({ + "data": ListEndpointsResponse +}) {} + +export class ListEndpointsZdr200 extends S.Struct({ + "data": S.Array(PublicEndpoint) +}) {} + +export class ListProviders200 extends S.Struct({ + "data": S.Array(S.Struct({ + /** + * Display name of the provider + */ + "name": S.String, + /** + * URL-friendly identifier for the provider + */ + "slug": S.String, + /** + * URL to the provider's privacy policy + */ + "privacy_policy_url": S.NullOr(S.String), + /** + * URL to the provider's terms of service + */ + "terms_of_service_url": S.optionalWith(S.String, { nullable: true }), + /** + * URL to the provider's status page + */ + "status_page_url": S.optionalWith(S.String, { nullable: true }) + })) +}) {} + +export class ListParams extends S.Struct({ + /** + * Whether to include disabled API keys in the response + */ + "include_disabled": S.optionalWith(S.String, { nullable: true }), + /** + * Number of API keys to skip for pagination + */ + "offset": S.optionalWith(S.String, { nullable: true }) +}) {} + +export class List200 extends S.Struct({ + /** + * List of API keys + */ + "data": S.Array(S.Struct({ + /** + * Unique hash identifier for the API key + */ + "hash": S.String, + /** + * Name of the API key + */ + "name": S.String, + /** + * Human-readable label for the API key + */ + "label": S.String, + /** + * Whether the API key is disabled + */ + "disabled": S.Boolean, + /** + * Spending limit for the API key in USD + */ + "limit": S.NullOr(S.Number), + /** + * Remaining spending limit in USD + */ + "limit_remaining": S.NullOr(S.Number), + /** + * Type of limit reset for the API key + */ + "limit_reset": S.NullOr(S.String), + /** + * Whether to include external BYOK usage in the credit limit + */ + "include_byok_in_limit": S.Boolean, + /** + * Total OpenRouter credit usage (in USD) for the API key + */ + "usage": S.Number, + /** + * OpenRouter credit usage (in USD) for the current UTC day + */ + "usage_daily": S.Number, + /** + * OpenRouter credit usage (in USD) for the current UTC week (Monday-Sunday) + */ + "usage_weekly": S.Number, + /** + * OpenRouter credit usage (in USD) for the current UTC month + */ + "usage_monthly": S.Number, + /** + * Total external BYOK usage (in USD) for the API key + */ + "byok_usage": S.Number, + /** + * External BYOK usage (in USD) for the current UTC day + */ + "byok_usage_daily": S.Number, + /** + * External BYOK usage (in USD) for the current UTC week (Monday-Sunday) + */ + "byok_usage_weekly": S.Number, + /** + * External BYOK usage (in USD) for current UTC month + */ + "byok_usage_monthly": S.Number, + /** + * ISO 8601 timestamp of when the API key was created + */ + "created_at": S.String, + /** + * ISO 8601 timestamp of when the API key was last updated + */ + "updated_at": S.NullOr(S.String), + /** + * ISO 8601 UTC timestamp when the API key expires, or null if no expiration + */ + "expires_at": S.optionalWith(S.String, { nullable: true }) + })) +}) {} + +/** + * Type of limit reset for the API key (daily, weekly, monthly, or null for no reset). Resets happen automatically at midnight UTC, and weeks are Monday through Sunday. + */ +export class CreateKeysRequestLimitReset extends S.Literal("daily", "weekly", "monthly") {} + +export class CreateKeysRequest extends S.Class("CreateKeysRequest")({ + /** + * Name for the new API key + */ + "name": S.String.pipe(S.minLength(1)), + /** + * Optional spending limit for the API key in USD + */ + "limit": S.optionalWith(S.Number, { nullable: true }), + /** + * Type of limit reset for the API key (daily, weekly, monthly, or null for no reset). Resets happen automatically at midnight UTC, and weeks are Monday through Sunday. + */ + "limit_reset": S.optionalWith(CreateKeysRequestLimitReset, { nullable: true }), + /** + * Whether to include BYOK usage in the limit + */ + "include_byok_in_limit": S.optionalWith(S.Boolean, { nullable: true }), + /** + * Optional ISO 8601 UTC timestamp when the API key should expire. Must be UTC, other timezones will be rejected + */ + "expires_at": S.optionalWith(S.String, { nullable: true }) +}) {} + +export class CreateKeys201 extends S.Struct({ + /** + * The created API key information + */ + "data": S.Struct({ + /** + * Unique hash identifier for the API key + */ + "hash": S.String, + /** + * Name of the API key + */ + "name": S.String, + /** + * Human-readable label for the API key + */ + "label": S.String, + /** + * Whether the API key is disabled + */ + "disabled": S.Boolean, + /** + * Spending limit for the API key in USD + */ + "limit": S.NullOr(S.Number), + /** + * Remaining spending limit in USD + */ + "limit_remaining": S.NullOr(S.Number), + /** + * Type of limit reset for the API key + */ + "limit_reset": S.NullOr(S.String), + /** + * Whether to include external BYOK usage in the credit limit + */ + "include_byok_in_limit": S.Boolean, + /** + * Total OpenRouter credit usage (in USD) for the API key + */ + "usage": S.Number, + /** + * OpenRouter credit usage (in USD) for the current UTC day + */ + "usage_daily": S.Number, + /** + * OpenRouter credit usage (in USD) for the current UTC week (Monday-Sunday) + */ + "usage_weekly": S.Number, + /** + * OpenRouter credit usage (in USD) for the current UTC month + */ + "usage_monthly": S.Number, + /** + * Total external BYOK usage (in USD) for the API key + */ + "byok_usage": S.Number, + /** + * External BYOK usage (in USD) for the current UTC day + */ + "byok_usage_daily": S.Number, + /** + * External BYOK usage (in USD) for the current UTC week (Monday-Sunday) + */ + "byok_usage_weekly": S.Number, + /** + * External BYOK usage (in USD) for current UTC month + */ + "byok_usage_monthly": S.Number, + /** + * ISO 8601 timestamp of when the API key was created + */ + "created_at": S.String, + /** + * ISO 8601 timestamp of when the API key was last updated + */ + "updated_at": S.NullOr(S.String), + /** + * ISO 8601 UTC timestamp when the API key expires, or null if no expiration + */ + "expires_at": S.optionalWith(S.String, { nullable: true }) + }), + /** + * The actual API key string (only shown once) + */ + "key": S.String +}) {} + +export class GetKey200 extends S.Struct({ + /** + * The API key information + */ + "data": S.Struct({ + /** + * Unique hash identifier for the API key + */ + "hash": S.String, + /** + * Name of the API key + */ + "name": S.String, + /** + * Human-readable label for the API key + */ + "label": S.String, + /** + * Whether the API key is disabled + */ + "disabled": S.Boolean, + /** + * Spending limit for the API key in USD + */ + "limit": S.NullOr(S.Number), + /** + * Remaining spending limit in USD + */ + "limit_remaining": S.NullOr(S.Number), + /** + * Type of limit reset for the API key + */ + "limit_reset": S.NullOr(S.String), + /** + * Whether to include external BYOK usage in the credit limit + */ + "include_byok_in_limit": S.Boolean, + /** + * Total OpenRouter credit usage (in USD) for the API key + */ + "usage": S.Number, + /** + * OpenRouter credit usage (in USD) for the current UTC day + */ + "usage_daily": S.Number, + /** + * OpenRouter credit usage (in USD) for the current UTC week (Monday-Sunday) + */ + "usage_weekly": S.Number, + /** + * OpenRouter credit usage (in USD) for the current UTC month + */ + "usage_monthly": S.Number, + /** + * Total external BYOK usage (in USD) for the API key + */ + "byok_usage": S.Number, + /** + * External BYOK usage (in USD) for the current UTC day + */ + "byok_usage_daily": S.Number, + /** + * External BYOK usage (in USD) for the current UTC week (Monday-Sunday) + */ + "byok_usage_weekly": S.Number, + /** + * External BYOK usage (in USD) for current UTC month + */ + "byok_usage_monthly": S.Number, + /** + * ISO 8601 timestamp of when the API key was created + */ + "created_at": S.String, + /** + * ISO 8601 timestamp of when the API key was last updated + */ + "updated_at": S.NullOr(S.String), + /** + * ISO 8601 UTC timestamp when the API key expires, or null if no expiration + */ + "expires_at": S.optionalWith(S.String, { nullable: true }) + }) +}) {} + +export class DeleteKeys200 extends S.Struct({ + /** + * Confirmation that the API key was deleted + */ + "deleted": S.Literal(true) +}) {} + +/** + * New limit reset type for the API key (daily, weekly, monthly, or null for no reset). Resets happen automatically at midnight UTC, and weeks are Monday through Sunday. + */ +export class UpdateKeysRequestLimitReset extends S.Literal("daily", "weekly", "monthly") {} + +export class UpdateKeysRequest extends S.Class("UpdateKeysRequest")({ + /** + * New name for the API key + */ + "name": S.optionalWith(S.String, { nullable: true }), + /** + * Whether to disable the API key + */ + "disabled": S.optionalWith(S.Boolean, { nullable: true }), + /** + * New spending limit for the API key in USD + */ + "limit": S.optionalWith(S.Number, { nullable: true }), + /** + * New limit reset type for the API key (daily, weekly, monthly, or null for no reset). Resets happen automatically at midnight UTC, and weeks are Monday through Sunday. + */ + "limit_reset": S.optionalWith(UpdateKeysRequestLimitReset, { nullable: true }), + /** + * Whether to include BYOK usage in the limit + */ + "include_byok_in_limit": S.optionalWith(S.Boolean, { nullable: true }) +}) {} + +export class UpdateKeys200 extends S.Struct({ + /** + * The updated API key information + */ + "data": S.Struct({ + /** + * Unique hash identifier for the API key + */ + "hash": S.String, + /** + * Name of the API key + */ + "name": S.String, + /** + * Human-readable label for the API key + */ + "label": S.String, + /** + * Whether the API key is disabled + */ + "disabled": S.Boolean, + /** + * Spending limit for the API key in USD + */ + "limit": S.NullOr(S.Number), + /** + * Remaining spending limit in USD + */ + "limit_remaining": S.NullOr(S.Number), + /** + * Type of limit reset for the API key + */ + "limit_reset": S.NullOr(S.String), + /** + * Whether to include external BYOK usage in the credit limit + */ + "include_byok_in_limit": S.Boolean, + /** + * Total OpenRouter credit usage (in USD) for the API key + */ + "usage": S.Number, + /** + * OpenRouter credit usage (in USD) for the current UTC day + */ + "usage_daily": S.Number, + /** + * OpenRouter credit usage (in USD) for the current UTC week (Monday-Sunday) + */ + "usage_weekly": S.Number, + /** + * OpenRouter credit usage (in USD) for the current UTC month + */ + "usage_monthly": S.Number, + /** + * Total external BYOK usage (in USD) for the API key + */ + "byok_usage": S.Number, + /** + * External BYOK usage (in USD) for the current UTC day + */ + "byok_usage_daily": S.Number, + /** + * External BYOK usage (in USD) for the current UTC week (Monday-Sunday) + */ + "byok_usage_weekly": S.Number, + /** + * External BYOK usage (in USD) for current UTC month + */ + "byok_usage_monthly": S.Number, + /** + * ISO 8601 timestamp of when the API key was created + */ + "created_at": S.String, + /** + * ISO 8601 timestamp of when the API key was last updated + */ + "updated_at": S.NullOr(S.String), + /** + * ISO 8601 UTC timestamp when the API key expires, or null if no expiration + */ + "expires_at": S.optionalWith(S.String, { nullable: true }) + }) +}) {} + +export class ListGuardrailsParams extends S.Struct({ + /** + * Number of records to skip for pagination + */ + "offset": S.optionalWith(S.String, { nullable: true }), + /** + * Maximum number of records to return (max 100) + */ + "limit": S.optionalWith(S.String, { nullable: true }) +}) {} + +export class ListGuardrails200 extends S.Struct({ + /** + * List of guardrails + */ + "data": S.Array(S.Struct({ + /** + * Unique identifier for the guardrail + */ + "id": S.String, + /** + * Name of the guardrail + */ + "name": S.String, + /** + * Description of the guardrail + */ + "description": S.optionalWith(S.String, { nullable: true }), + /** + * Spending limit in USD + */ + "limit_usd": S.optionalWith(S.Number.pipe(S.greaterThan(0)), { nullable: true }), + /** + * Interval at which the limit resets (daily, weekly, monthly) + */ + "reset_interval": S.optionalWith(S.Literal("daily", "weekly", "monthly"), { nullable: true }), + /** + * List of allowed provider IDs + */ + "allowed_providers": S.optionalWith(S.Array(S.String), { nullable: true }), + /** + * Array of model canonical_slugs (immutable identifiers) + */ + "allowed_models": S.optionalWith(S.Array(S.String), { nullable: true }), + /** + * Whether to enforce zero data retention + */ + "enforce_zdr": S.optionalWith(S.Boolean, { nullable: true }), + /** + * ISO 8601 timestamp of when the guardrail was created + */ + "created_at": S.String, + /** + * ISO 8601 timestamp of when the guardrail was last updated + */ + "updated_at": S.optionalWith(S.String, { nullable: true }) + })), + /** + * Total number of guardrails + */ + "total_count": S.Number +}) {} + +/** + * Interval at which the limit resets (daily, weekly, monthly) + */ +export class CreateGuardrailRequestResetInterval extends S.Literal("daily", "weekly", "monthly") {} + +export class CreateGuardrailRequest extends S.Class("CreateGuardrailRequest")({ + /** + * Name for the new guardrail + */ + "name": S.String.pipe(S.minLength(1), S.maxLength(200)), + /** + * Description of the guardrail + */ + "description": S.optionalWith(S.String.pipe(S.maxLength(1000)), { nullable: true }), + /** + * Spending limit in USD + */ + "limit_usd": S.optionalWith(S.Number.pipe(S.greaterThan(0)), { nullable: true }), + /** + * Interval at which the limit resets (daily, weekly, monthly) + */ + "reset_interval": S.optionalWith(CreateGuardrailRequestResetInterval, { nullable: true }), + /** + * List of allowed provider IDs + */ + "allowed_providers": S.optionalWith(S.NonEmptyArray(S.String).pipe(S.minItems(1)), { nullable: true }), + /** + * Array of model identifiers (slug or canonical_slug accepted) + */ + "allowed_models": S.optionalWith(S.NonEmptyArray(S.String).pipe(S.minItems(1)), { nullable: true }), + /** + * Whether to enforce zero data retention + */ + "enforce_zdr": S.optionalWith(S.Boolean, { nullable: true }) +}) {} + +/** + * Interval at which the limit resets (daily, weekly, monthly) + */ +export class CreateGuardrail201DataResetInterval extends S.Literal("daily", "weekly", "monthly") {} + +export class CreateGuardrail201 extends S.Struct({ + /** + * The created guardrail + */ + "data": S.Struct({ + /** + * Unique identifier for the guardrail + */ + "id": S.String, + /** + * Name of the guardrail + */ + "name": S.String, + /** + * Description of the guardrail + */ + "description": S.optionalWith(S.String, { nullable: true }), + /** + * Spending limit in USD + */ + "limit_usd": S.optionalWith(S.Number.pipe(S.greaterThan(0)), { nullable: true }), + /** + * Interval at which the limit resets (daily, weekly, monthly) + */ + "reset_interval": S.optionalWith(CreateGuardrail201DataResetInterval, { nullable: true }), + /** + * List of allowed provider IDs + */ + "allowed_providers": S.optionalWith(S.Array(S.String), { nullable: true }), + /** + * Array of model canonical_slugs (immutable identifiers) + */ + "allowed_models": S.optionalWith(S.Array(S.String), { nullable: true }), + /** + * Whether to enforce zero data retention + */ + "enforce_zdr": S.optionalWith(S.Boolean, { nullable: true }), + /** + * ISO 8601 timestamp of when the guardrail was created + */ + "created_at": S.String, + /** + * ISO 8601 timestamp of when the guardrail was last updated + */ + "updated_at": S.optionalWith(S.String, { nullable: true }) + }) +}) {} + +/** + * Interval at which the limit resets (daily, weekly, monthly) + */ +export class GetGuardrail200DataResetInterval extends S.Literal("daily", "weekly", "monthly") {} + +export class GetGuardrail200 extends S.Struct({ + /** + * The guardrail + */ + "data": S.Struct({ + /** + * Unique identifier for the guardrail + */ + "id": S.String, + /** + * Name of the guardrail + */ + "name": S.String, + /** + * Description of the guardrail + */ + "description": S.optionalWith(S.String, { nullable: true }), + /** + * Spending limit in USD + */ + "limit_usd": S.optionalWith(S.Number.pipe(S.greaterThan(0)), { nullable: true }), + /** + * Interval at which the limit resets (daily, weekly, monthly) + */ + "reset_interval": S.optionalWith(GetGuardrail200DataResetInterval, { nullable: true }), + /** + * List of allowed provider IDs + */ + "allowed_providers": S.optionalWith(S.Array(S.String), { nullable: true }), + /** + * Array of model canonical_slugs (immutable identifiers) + */ + "allowed_models": S.optionalWith(S.Array(S.String), { nullable: true }), + /** + * Whether to enforce zero data retention + */ + "enforce_zdr": S.optionalWith(S.Boolean, { nullable: true }), + /** + * ISO 8601 timestamp of when the guardrail was created + */ + "created_at": S.String, + /** + * ISO 8601 timestamp of when the guardrail was last updated + */ + "updated_at": S.optionalWith(S.String, { nullable: true }) + }) +}) {} + +export class DeleteGuardrail200 extends S.Struct({ + /** + * Confirmation that the guardrail was deleted + */ + "deleted": S.Literal(true) +}) {} + +/** + * Interval at which the limit resets (daily, weekly, monthly) + */ +export class UpdateGuardrailRequestResetInterval extends S.Literal("daily", "weekly", "monthly") {} + +export class UpdateGuardrailRequest extends S.Class("UpdateGuardrailRequest")({ + /** + * New name for the guardrail + */ + "name": S.optionalWith(S.String.pipe(S.minLength(1), S.maxLength(200)), { nullable: true }), + /** + * New description for the guardrail + */ + "description": S.optionalWith(S.String.pipe(S.maxLength(1000)), { nullable: true }), + /** + * New spending limit in USD + */ + "limit_usd": S.optionalWith(S.Number.pipe(S.greaterThan(0)), { nullable: true }), + /** + * Interval at which the limit resets (daily, weekly, monthly) + */ + "reset_interval": S.optionalWith(UpdateGuardrailRequestResetInterval, { nullable: true }), + /** + * New list of allowed provider IDs + */ + "allowed_providers": S.optionalWith(S.NonEmptyArray(S.String).pipe(S.minItems(1)), { nullable: true }), + /** + * Array of model identifiers (slug or canonical_slug accepted) + */ + "allowed_models": S.optionalWith(S.NonEmptyArray(S.String).pipe(S.minItems(1)), { nullable: true }), + /** + * Whether to enforce zero data retention + */ + "enforce_zdr": S.optionalWith(S.Boolean, { nullable: true }) +}) {} + +/** + * Interval at which the limit resets (daily, weekly, monthly) + */ +export class UpdateGuardrail200DataResetInterval extends S.Literal("daily", "weekly", "monthly") {} + +export class UpdateGuardrail200 extends S.Struct({ + /** + * The updated guardrail + */ + "data": S.Struct({ + /** + * Unique identifier for the guardrail + */ + "id": S.String, + /** + * Name of the guardrail + */ + "name": S.String, + /** + * Description of the guardrail + */ + "description": S.optionalWith(S.String, { nullable: true }), + /** + * Spending limit in USD + */ + "limit_usd": S.optionalWith(S.Number.pipe(S.greaterThan(0)), { nullable: true }), + /** + * Interval at which the limit resets (daily, weekly, monthly) + */ + "reset_interval": S.optionalWith(UpdateGuardrail200DataResetInterval, { nullable: true }), + /** + * List of allowed provider IDs + */ + "allowed_providers": S.optionalWith(S.Array(S.String), { nullable: true }), + /** + * Array of model canonical_slugs (immutable identifiers) + */ + "allowed_models": S.optionalWith(S.Array(S.String), { nullable: true }), + /** + * Whether to enforce zero data retention + */ + "enforce_zdr": S.optionalWith(S.Boolean, { nullable: true }), + /** + * ISO 8601 timestamp of when the guardrail was created + */ + "created_at": S.String, + /** + * ISO 8601 timestamp of when the guardrail was last updated + */ + "updated_at": S.optionalWith(S.String, { nullable: true }) + }) +}) {} + +export class ListKeyAssignmentsParams extends S.Struct({ + /** + * Number of records to skip for pagination + */ + "offset": S.optionalWith(S.String, { nullable: true }), + /** + * Maximum number of records to return (max 100) + */ + "limit": S.optionalWith(S.String, { nullable: true }) +}) {} + +export class ListKeyAssignments200 extends S.Struct({ + /** + * List of key assignments + */ + "data": S.Array(S.Struct({ + /** + * Unique identifier for the assignment + */ + "id": S.String, + /** + * Hash of the assigned API key + */ + "key_hash": S.String, + /** + * ID of the guardrail + */ + "guardrail_id": S.String, + /** + * Name of the API key + */ + "key_name": S.String, + /** + * Label of the API key + */ + "key_label": S.String, + /** + * User ID of who made the assignment + */ + "assigned_by": S.NullOr(S.String), + /** + * ISO 8601 timestamp of when the assignment was created + */ + "created_at": S.String + })), + /** + * Total number of key assignments for this guardrail + */ + "total_count": S.Number +}) {} + +export class ListMemberAssignmentsParams extends S.Struct({ + /** + * Number of records to skip for pagination + */ + "offset": S.optionalWith(S.String, { nullable: true }), + /** + * Maximum number of records to return (max 100) + */ + "limit": S.optionalWith(S.String, { nullable: true }) +}) {} + +export class ListMemberAssignments200 extends S.Struct({ + /** + * List of member assignments + */ + "data": S.Array(S.Struct({ + /** + * Unique identifier for the assignment + */ + "id": S.String, + /** + * Clerk user ID of the assigned member + */ + "user_id": S.String, + /** + * Organization ID + */ + "organization_id": S.String, + /** + * ID of the guardrail + */ + "guardrail_id": S.String, + /** + * User ID of who made the assignment + */ + "assigned_by": S.NullOr(S.String), + /** + * ISO 8601 timestamp of when the assignment was created + */ + "created_at": S.String + })), + /** + * Total number of member assignments + */ + "total_count": S.Number +}) {} + +export class ListGuardrailKeyAssignmentsParams extends S.Struct({ + /** + * Number of records to skip for pagination + */ + "offset": S.optionalWith(S.String, { nullable: true }), + /** + * Maximum number of records to return (max 100) + */ + "limit": S.optionalWith(S.String, { nullable: true }) +}) {} + +export class ListGuardrailKeyAssignments200 extends S.Struct({ + /** + * List of key assignments + */ + "data": S.Array(S.Struct({ + /** + * Unique identifier for the assignment + */ + "id": S.String, + /** + * Hash of the assigned API key + */ + "key_hash": S.String, + /** + * ID of the guardrail + */ + "guardrail_id": S.String, + /** + * Name of the API key + */ + "key_name": S.String, + /** + * Label of the API key + */ + "key_label": S.String, + /** + * User ID of who made the assignment + */ + "assigned_by": S.NullOr(S.String), + /** + * ISO 8601 timestamp of when the assignment was created + */ + "created_at": S.String + })), + /** + * Total number of key assignments for this guardrail + */ + "total_count": S.Number +}) {} + +export class BulkAssignKeysToGuardrailRequest + extends S.Class("BulkAssignKeysToGuardrailRequest")({ + /** + * Array of API key hashes to assign to the guardrail + */ + "key_hashes": S.NonEmptyArray(S.String.pipe(S.minLength(1))).pipe(S.minItems(1)) + }) +{} + +export class BulkAssignKeysToGuardrail200 extends S.Struct({ + /** + * Number of keys successfully assigned + */ + "assigned_count": S.Number +}) {} + +export class ListGuardrailMemberAssignmentsParams extends S.Struct({ + /** + * Number of records to skip for pagination + */ + "offset": S.optionalWith(S.String, { nullable: true }), + /** + * Maximum number of records to return (max 100) + */ + "limit": S.optionalWith(S.String, { nullable: true }) +}) {} + +export class ListGuardrailMemberAssignments200 extends S.Struct({ + /** + * List of member assignments + */ + "data": S.Array(S.Struct({ + /** + * Unique identifier for the assignment + */ + "id": S.String, + /** + * Clerk user ID of the assigned member + */ + "user_id": S.String, + /** + * Organization ID + */ + "organization_id": S.String, + /** + * ID of the guardrail + */ + "guardrail_id": S.String, + /** + * User ID of who made the assignment + */ + "assigned_by": S.NullOr(S.String), + /** + * ISO 8601 timestamp of when the assignment was created + */ + "created_at": S.String + })), + /** + * Total number of member assignments + */ + "total_count": S.Number +}) {} + +export class BulkAssignMembersToGuardrailRequest + extends S.Class("BulkAssignMembersToGuardrailRequest")({ + /** + * Array of member user IDs to assign to the guardrail + */ + "member_user_ids": S.NonEmptyArray(S.String.pipe(S.minLength(1))).pipe(S.minItems(1)) + }) +{} + +export class BulkAssignMembersToGuardrail200 extends S.Struct({ + /** + * Number of members successfully assigned + */ + "assigned_count": S.Number +}) {} + +export class BulkUnassignKeysFromGuardrailRequest + extends S.Class("BulkUnassignKeysFromGuardrailRequest")({ + /** + * Array of API key hashes to unassign from the guardrail + */ + "key_hashes": S.NonEmptyArray(S.String.pipe(S.minLength(1))).pipe(S.minItems(1)) + }) +{} + +export class BulkUnassignKeysFromGuardrail200 extends S.Struct({ + /** + * Number of keys successfully unassigned + */ + "unassigned_count": S.Number +}) {} + +export class BulkUnassignMembersFromGuardrailRequest + extends S.Class("BulkUnassignMembersFromGuardrailRequest")({ + /** + * Array of member user IDs to unassign from the guardrail + */ + "member_user_ids": S.NonEmptyArray(S.String.pipe(S.minLength(1))).pipe(S.minItems(1)) + }) +{} + +export class BulkUnassignMembersFromGuardrail200 extends S.Struct({ + /** + * Number of members successfully unassigned + */ + "unassigned_count": S.Number +}) {} + +export class GetCurrentKey200 extends S.Struct({ + /** + * Current API key information + */ + "data": S.Struct({ + /** + * Human-readable label for the API key + */ + "label": S.String, + /** + * Spending limit for the API key in USD + */ + "limit": S.NullOr(S.Number), + /** + * Total OpenRouter credit usage (in USD) for the API key + */ + "usage": S.Number, + /** + * OpenRouter credit usage (in USD) for the current UTC day + */ + "usage_daily": S.Number, + /** + * OpenRouter credit usage (in USD) for the current UTC week (Monday-Sunday) + */ + "usage_weekly": S.Number, + /** + * OpenRouter credit usage (in USD) for the current UTC month + */ + "usage_monthly": S.Number, + /** + * Total external BYOK usage (in USD) for the API key + */ + "byok_usage": S.Number, + /** + * External BYOK usage (in USD) for the current UTC day + */ + "byok_usage_daily": S.Number, + /** + * External BYOK usage (in USD) for the current UTC week (Monday-Sunday) + */ + "byok_usage_weekly": S.Number, + /** + * External BYOK usage (in USD) for current UTC month + */ + "byok_usage_monthly": S.Number, + /** + * Whether this is a free tier API key + */ + "is_free_tier": S.Boolean, + /** + * Whether this is a provisioning key + */ + "is_provisioning_key": S.Boolean, + /** + * Remaining spending limit in USD + */ + "limit_remaining": S.NullOr(S.Number), + /** + * Type of limit reset for the API key + */ + "limit_reset": S.NullOr(S.String), + /** + * Whether to include external BYOK usage in the credit limit + */ + "include_byok_in_limit": S.Boolean, + /** + * ISO 8601 UTC timestamp when the API key expires, or null if no expiration + */ + "expires_at": S.optionalWith(S.String, { nullable: true }), + /** + * Legacy rate limit information about a key. Will always return -1. + */ + "rate_limit": S.Struct({ + /** + * Number of requests allowed per interval + */ + "requests": S.Number, + /** + * Rate limit interval + */ + "interval": S.String, + /** + * Note about the rate limit + */ + "note": S.String + }) + }) +}) {} + +/** + * The method used to generate the code challenge + */ +export class ExchangeAuthCodeForAPIKeyRequestCodeChallengeMethod extends S.Literal("S256", "plain") {} + +export class ExchangeAuthCodeForAPIKeyRequest + extends S.Class("ExchangeAuthCodeForAPIKeyRequest")({ + /** + * The authorization code received from the OAuth redirect + */ + "code": S.String, + /** + * The code verifier if code_challenge was used in the authorization request + */ + "code_verifier": S.optionalWith(S.String, { nullable: true }), + /** + * The method used to generate the code challenge + */ + "code_challenge_method": S.optionalWith(ExchangeAuthCodeForAPIKeyRequestCodeChallengeMethod, { nullable: true }) + }) +{} + +export class ExchangeAuthCodeForAPIKey200 extends S.Struct({ + /** + * The API key to use for OpenRouter requests + */ + "key": S.String, + /** + * User ID associated with the API key + */ + "user_id": S.NullOr(S.String) +}) {} + +/** + * The method used to generate the code challenge + */ +export class CreateAuthKeysCodeRequestCodeChallengeMethod extends S.Literal("S256", "plain") {} + +export class CreateAuthKeysCodeRequest extends S.Class("CreateAuthKeysCodeRequest")({ + /** + * The callback URL to redirect to after authorization. Note, only https URLs on ports 443 and 3000 are allowed. + */ + "callback_url": S.String, + /** + * PKCE code challenge for enhanced security + */ + "code_challenge": S.optionalWith(S.String, { nullable: true }), + /** + * The method used to generate the code challenge + */ + "code_challenge_method": S.optionalWith(CreateAuthKeysCodeRequestCodeChallengeMethod, { nullable: true }), + /** + * Credit limit for the API key to be created + */ + "limit": S.optionalWith(S.Number, { nullable: true }), + /** + * Optional expiration time for the API key to be created + */ + "expires_at": S.optionalWith(S.String, { nullable: true }) +}) {} + +export class CreateAuthKeysCode200 extends S.Struct({ + /** + * Auth code data + */ + "data": S.Struct({ + /** + * The authorization code ID to use in the exchange request + */ + "id": S.String, + /** + * The application ID associated with this auth code + */ + "app_id": S.Number, + /** + * ISO 8601 timestamp of when the auth code was created + */ + "created_at": S.String + }) +}) {} + +export class ChatGenerationParamsProviderEnumDataCollectionEnum extends S.Literal("deny", "allow") {} + +export class Schema0 extends S.Array( + S.Union( + S.Literal( + "AI21", + "AionLabs", + "Alibaba", + "Amazon Bedrock", + "Amazon Nova", + "Anthropic", + "Arcee AI", + "AtlasCloud", + "Avian", + "Azure", + "BaseTen", + "BytePlus", + "Black Forest Labs", + "Cerebras", + "Chutes", + "Cirrascale", + "Clarifai", + "Cloudflare", + "Cohere", + "Crusoe", + "DeepInfra", + "DeepSeek", + "Featherless", + "Fireworks", + "Friendli", + "GMICloud", + "Google", + "Google AI Studio", + "Groq", + "Hyperbolic", + "Inception", + "Inceptron", + "InferenceNet", + "Infermatic", + "Inflection", + "Liquid", + "Mara", + "Mancer 2", + "Minimax", + "ModelRun", + "Mistral", + "Modular", + "Moonshot AI", + "Morph", + "NCompass", + "Nebius", + "NextBit", + "Novita", + "Nvidia", + "OpenAI", + "OpenInference", + "Parasail", + "Perplexity", + "Phala", + "Relace", + "SambaNova", + "Seed", + "SiliconFlow", + "Sourceful", + "Stealth", + "StreamLake", + "Switchpoint", + "Together", + "Upstage", + "Venice", + "WandB", + "Xiaomi", + "xAI", + "Z.AI", + "FakeProvider" + ), + S.String + ) +) {} + +export class ProviderSortUnion extends S.Union(ProviderSort, ProviderSortConfig) {} + +export class Schema1 extends S.Union(S.Number, S.String, S.Number) {} + +export class ChatGenerationParamsRouteEnum extends S.Literal("fallback", "sort") {} + +export class ChatMessageContentItemCacheControlTtl extends S.Literal("5m", "1h") {} + +export class ChatMessageContentItemCacheControl + extends S.Class("ChatMessageContentItemCacheControl")({ + "type": S.Literal("ephemeral"), + "ttl": S.optionalWith(ChatMessageContentItemCacheControlTtl, { nullable: true }) + }) +{} + +export class ChatMessageContentItemText extends S.Class("ChatMessageContentItemText")({ + "type": S.Literal("text"), + "text": S.String, + "cache_control": S.optionalWith(ChatMessageContentItemCacheControl, { nullable: true }) +}) {} + +export class SystemMessage extends S.Class("SystemMessage")({ + "role": S.Literal("system"), + "content": S.Union(S.String, S.Array(ChatMessageContentItemText)), + "name": S.optionalWith(S.String, { nullable: true }) +}) {} + +export class ChatMessageContentItemImageImageUrlDetail extends S.Literal("auto", "low", "high") {} + +export class ChatMessageContentItemImage extends S.Class("ChatMessageContentItemImage")({ + "type": S.Literal("image_url"), + "image_url": S.Struct({ + "url": S.String, + "detail": S.optionalWith(ChatMessageContentItemImageImageUrlDetail, { nullable: true }) + }) +}) {} + +export class ChatMessageContentItemAudio extends S.Class("ChatMessageContentItemAudio")({ + "type": S.Literal("input_audio"), + "input_audio": S.Struct({ + "data": S.String, + "format": S.String + }) +}) {} + +export class ChatMessageContentItemVideo extends S.Record({ key: S.String, value: S.Unknown }) {} + +export class ChatMessageContentItem extends S.Record({ key: S.String, value: S.Unknown }) {} + +export class UserMessage extends S.Class("UserMessage")({ + "role": S.Literal("user"), + "content": S.Union(S.String, S.Array(ChatMessageContentItem)), + "name": S.optionalWith(S.String, { nullable: true }) +}) {} + +export class ChatMessageToolCall extends S.Class("ChatMessageToolCall")({ + "id": S.String, + "type": S.Literal("function"), + "function": S.Struct({ + "name": S.String, + "arguments": S.String + }) +}) {} + +export class Schema3 extends S.Union(S.String, S.Null) {} + +export class Schema4Enum extends S.Literal( + "unknown", + "openai-responses-v1", + "azure-openai-responses-v1", + "xai-responses-v1", + "anthropic-claude-v1", + "google-gemini-v1" +) {} + +export class Schema4 extends S.Union(Schema4Enum, S.Null) {} + +export class Schema5 extends S.Number {} + +export class Schema2 extends S.Record({ key: S.String, value: S.Unknown }) {} + +export class AssistantMessage extends S.Class("AssistantMessage")({ + "role": S.Literal("assistant"), + "content": S.optionalWith(S.Union(S.String, S.Array(ChatMessageContentItem)), { nullable: true }), + "name": S.optionalWith(S.String, { nullable: true }), + "tool_calls": S.optionalWith(S.Array(ChatMessageToolCall), { nullable: true }), + "refusal": S.optionalWith(S.String, { nullable: true }), + "reasoning": S.optionalWith(S.String, { nullable: true }), + "reasoning_details": S.optionalWith(S.Array(ReasoningDetail), { nullable: true }), + "images": S.optionalWith( + S.Array(S.Struct({ + "image_url": S.Struct({ + "url": S.String + }) + })), + { nullable: true } + ), + "annotations": S.optionalWith(S.Array(AnnotationDetail), { nullable: true }) +}) {} + +export class ToolResponseMessage extends S.Class("ToolResponseMessage")({ + "role": S.Literal("tool"), + "content": S.Union(S.String, S.Array(ChatMessageContentItem)), + "tool_call_id": S.String +}) {} + +export class Message extends S.Record({ key: S.String, value: S.Unknown }) {} + +export class ModelName extends S.String {} + +export class ChatGenerationParamsReasoningEffortEnum + extends S.Literal("xhigh", "high", "medium", "low", "minimal", "none") +{} + +export class JSONSchemaConfig extends S.Class("JSONSchemaConfig")({ + "name": S.String.pipe(S.maxLength(64)), + "description": S.optionalWith(S.String, { nullable: true }), + "schema": S.optionalWith(S.Record({ key: S.String, value: S.Unknown }), { nullable: true }), + "strict": S.optionalWith(S.Boolean, { nullable: true }) +}) {} + +export class ResponseFormatJSONSchema extends S.Class("ResponseFormatJSONSchema")({ + "type": S.Literal("json_schema"), + "json_schema": JSONSchemaConfig +}) {} + +export class ResponseFormatTextGrammar extends S.Class("ResponseFormatTextGrammar")({ + "type": S.Literal("grammar"), + "grammar": S.String +}) {} + +export class ChatStreamOptions extends S.Class("ChatStreamOptions")({ + "include_usage": S.optionalWith(S.Boolean, { nullable: true }) +}) {} + +export class NamedToolChoice extends S.Class("NamedToolChoice")({ + "type": S.Literal("function"), + "function": S.Struct({ + "name": S.String + }) +}) {} + +export class ToolChoiceOption + extends S.Union(S.Literal("none"), S.Literal("auto"), S.Literal("required"), NamedToolChoice) +{} + +export class ToolDefinitionJson extends S.Class("ToolDefinitionJson")({ + "type": S.Literal("function"), + "function": S.Struct({ + "name": S.String.pipe(S.maxLength(64)), + "description": S.optionalWith(S.String, { nullable: true }), + "parameters": S.optionalWith(S.Record({ key: S.String, value: S.Unknown }), { nullable: true }), + "strict": S.optionalWith(S.Boolean, { nullable: true }) + }) +}) {} + +export class ChatGenerationParams extends S.Class("ChatGenerationParams")({ + /** + * When multiple model providers are available, optionally indicate your routing preference. + */ + "provider": S.optionalWith( + S.Struct({ + /** + * Whether to allow backup providers to serve requests + * - true: (default) when the primary provider (or your custom providers in "order") is unavailable, use the next best provider. + * - false: use only the primary/custom provider, and return the upstream error if it's unavailable. + */ + "allow_fallbacks": S.optionalWith(S.Boolean, { nullable: true }), + /** + * Whether to filter providers to only those that support the parameters you've provided. If this setting is omitted or set to false, then providers will receive only the parameters they support, and ignore the rest. + */ + "require_parameters": S.optionalWith(S.Boolean, { nullable: true }), + /** + * Data collection setting. If no available model provider meets the requirement, your request will return an error. + * - allow: (default) allow providers which store user data non-transiently and may train on it + * + * - deny: use only providers which do not collect user data. + */ + "data_collection": S.optionalWith(S.Literal("deny", "allow"), { nullable: true }), + "zdr": S.optionalWith(S.Boolean, { nullable: true }), + "enforce_distillable_text": S.optionalWith(S.Boolean, { nullable: true }), + /** + * An ordered list of provider slugs. The router will attempt to use the first provider in the subset of this list that supports your requested model, and fall back to the next if it is unavailable. If no providers are available, the request will fail with an error message. + */ + "order": S.optionalWith(Schema0, { nullable: true }), + /** + * List of provider slugs to allow. If provided, this list is merged with your account-wide allowed provider settings for this request. + */ + "only": S.optionalWith(Schema0, { nullable: true }), + /** + * List of provider slugs to ignore. If provided, this list is merged with your account-wide ignored provider settings for this request. + */ + "ignore": S.optionalWith(Schema0, { nullable: true }), + /** + * A list of quantization levels to filter the provider by. + */ + "quantizations": S.optionalWith( + S.Array(S.Literal("int4", "int8", "fp4", "fp6", "fp8", "fp16", "bf16", "fp32", "unknown")), + { nullable: true } + ), + /** + * The sorting strategy to use for this request, if "order" is not specified. When set, no load balancing is performed. + */ + "sort": S.optionalWith(ProviderSortUnion, { nullable: true }), + /** + * The object specifying the maximum price you want to pay for this request. USD price per million tokens, for prompt and completion. + */ + "max_price": S.optionalWith( + S.Struct({ + "prompt": S.optionalWith(Schema1, { nullable: true }), + "completion": S.optionalWith(Schema1, { nullable: true }), + "image": S.optionalWith(Schema1, { nullable: true }), + "audio": S.optionalWith(Schema1, { nullable: true }), + "request": S.optionalWith(Schema1, { nullable: true }) + }), + { nullable: true } + ), + /** + * Preferred minimum throughput (in tokens per second). Can be a number (applies to p50) or an object with percentile-specific cutoffs. Endpoints below the threshold(s) may still be used, but are deprioritized in routing. When using fallback models, this may cause a fallback model to be used instead of the primary model if it meets the threshold. + */ + "preferred_min_throughput": S.optionalWith( + S.Union( + S.Number, + S.Struct({ + "p50": S.optionalWith(S.Number, { nullable: true }), + "p75": S.optionalWith(S.Number, { nullable: true }), + "p90": S.optionalWith(S.Number, { nullable: true }), + "p99": S.optionalWith(S.Number, { nullable: true }) + }) + ), + { nullable: true } + ), + /** + * Preferred maximum latency (in seconds). Can be a number (applies to p50) or an object with percentile-specific cutoffs. Endpoints above the threshold(s) may still be used, but are deprioritized in routing. When using fallback models, this may cause a fallback model to be used instead of the primary model if it meets the threshold. + */ + "preferred_max_latency": S.optionalWith( + S.Union( + S.Number, + S.Struct({ + "p50": S.optionalWith(S.Number, { nullable: true }), + "p75": S.optionalWith(S.Number, { nullable: true }), + "p90": S.optionalWith(S.Number, { nullable: true }), + "p99": S.optionalWith(S.Number, { nullable: true }) + }) + ), + { nullable: true } + ) + }), + { nullable: true } + ), + /** + * Plugins you want to enable for this request, including their settings. + */ + "plugins": S.optionalWith(S.Array(S.Record({ key: S.String, value: S.Unknown })), { nullable: true }), + "route": S.optionalWith(ChatGenerationParamsRouteEnum, { nullable: true }), + "user": S.optionalWith(S.String, { nullable: true }), + /** + * A unique identifier for grouping related requests (e.g., a conversation or agent workflow) for observability. If provided in both the request body and the x-session-id header, the body value takes precedence. Maximum of 128 characters. + */ + "session_id": S.optionalWith(S.String.pipe(S.maxLength(128)), { nullable: true }), + "messages": S.NonEmptyArray(Message).pipe(S.minItems(1)), + "model": S.optionalWith(ModelName, { nullable: true }), + "models": S.optionalWith(S.Array(ModelName), { nullable: true }), + "frequency_penalty": S.optionalWith(S.Number.pipe(S.greaterThanOrEqualTo(-2), S.lessThanOrEqualTo(2)), { + nullable: true + }), + "logit_bias": S.optionalWith(S.Record({ key: S.String, value: S.Unknown }), { nullable: true }), + "logprobs": S.optionalWith(S.Boolean, { nullable: true }), + "top_logprobs": S.optionalWith(S.Number.pipe(S.greaterThanOrEqualTo(0), S.lessThanOrEqualTo(20)), { nullable: true }), + "max_completion_tokens": S.optionalWith(S.Number.pipe(S.greaterThanOrEqualTo(1)), { nullable: true }), + "max_tokens": S.optionalWith(S.Number.pipe(S.greaterThanOrEqualTo(1)), { nullable: true }), + "metadata": S.optionalWith(S.Record({ key: S.String, value: S.Unknown }), { nullable: true }), + "presence_penalty": S.optionalWith(S.Number.pipe(S.greaterThanOrEqualTo(-2), S.lessThanOrEqualTo(2)), { + nullable: true + }), + "reasoning": S.optionalWith( + S.Struct({ + "effort": S.optionalWith(ChatGenerationParamsReasoningEffortEnum, { nullable: true }), + "summary": S.optionalWith(ReasoningSummaryVerbosity, { nullable: true }) + }), + { nullable: true } + ), + "response_format": S.optionalWith(S.Record({ key: S.String, value: S.Unknown }), { nullable: true }), + "seed": S.optionalWith(S.Int.pipe(S.greaterThanOrEqualTo(-9007199254740991), S.lessThanOrEqualTo(9007199254740991)), { + nullable: true + }), + "stop": S.optionalWith(S.Union(S.String, S.Array(S.String).pipe(S.maxItems(4))), { nullable: true }), + "stream": S.optionalWith(S.Boolean, { nullable: true, default: () => false as const }), + "stream_options": S.optionalWith(ChatStreamOptions, { nullable: true }), + "temperature": S.optionalWith(S.Number.pipe(S.greaterThanOrEqualTo(0), S.lessThanOrEqualTo(2)), { + nullable: true, + default: () => 1 as const + }), + "tool_choice": S.optionalWith(ToolChoiceOption, { nullable: true }), + "tools": S.optionalWith(S.Array(ToolDefinitionJson), { nullable: true }), + "top_p": S.optionalWith(S.Number.pipe(S.greaterThanOrEqualTo(0), S.lessThanOrEqualTo(1)), { + nullable: true, + default: () => 1 as const + }), + "debug": S.optionalWith( + S.Struct({ + "echo_upstream_body": S.optionalWith(S.Boolean, { nullable: true }) + }), + { nullable: true } + ), + "image_config": S.optionalWith(S.Record({ key: S.String, value: S.Unknown }), { nullable: true }), + "modalities": S.optionalWith(S.Array(S.Literal("text", "image")), { nullable: true }) +}) {} + +export class ChatCompletionFinishReason extends S.Literal("tool_calls", "stop", "length", "content_filter", "error") {} + +export class Schema6 extends S.Union(ChatCompletionFinishReason, S.Null) {} + +export class ChatMessageTokenLogprob extends S.Class("ChatMessageTokenLogprob")({ + "token": S.String, + "logprob": S.Number, + "bytes": S.NullOr(S.Array(S.Number)), + "top_logprobs": S.Array(S.Struct({ + "token": S.String, + "logprob": S.Number, + "bytes": S.NullOr(S.Array(S.Number)) + })) +}) {} + +export class ChatMessageTokenLogprobs extends S.Class("ChatMessageTokenLogprobs")({ + "content": S.optionalWith(S.Array(ChatMessageTokenLogprob), { nullable: true }), + "refusal": S.optionalWith(S.Array(ChatMessageTokenLogprob), { nullable: true }) +}) {} + +export class ChatResponseChoice extends S.Class("ChatResponseChoice")({ + "finish_reason": S.NullOr(ChatCompletionFinishReason), + "index": S.Number, + "message": AssistantMessage, + "logprobs": S.optionalWith(ChatMessageTokenLogprobs, { nullable: true }) +}) {} + +export class ChatGenerationTokenUsage extends S.Class("ChatGenerationTokenUsage")({ + "completion_tokens": S.Number, + "prompt_tokens": S.Number, + "total_tokens": S.Number, + "completion_tokens_details": S.optionalWith( + S.Struct({ + "reasoning_tokens": S.optionalWith(S.Number, { nullable: true }), + "audio_tokens": S.optionalWith(S.Number, { nullable: true }), + "accepted_prediction_tokens": S.optionalWith(S.Number, { nullable: true }), + "rejected_prediction_tokens": S.optionalWith(S.Number, { nullable: true }) + }), + { nullable: true } + ), + "prompt_tokens_details": S.optionalWith( + S.Struct({ + "cached_tokens": S.optionalWith(S.Number, { nullable: true }), + "cache_write_tokens": S.optionalWith(S.Number, { nullable: true }), + "audio_tokens": S.optionalWith(S.Number, { nullable: true }), + "video_tokens": S.optionalWith(S.Number, { nullable: true }) + }), + { nullable: true } + ), + "cost": S.optionalWith(S.Number, { nullable: true }), + "cost_details": S.optionalWith(S.Struct({ upstream_inference_cost: S.optionalWith(S.Number, { nullable: true }) }), { + nullable: true + }) +}) {} + +export class ChatResponse extends S.Class("ChatResponse")({ + "id": S.String, + "provider": S.optionalWith(S.String, { nullable: true }), + "choices": S.Array(ChatResponseChoice), + "created": S.Number, + "model": S.String, + "object": S.Literal("chat.completion"), + "system_fingerprint": S.optionalWith(S.String, { nullable: true }), + "usage": S.optionalWith(ChatGenerationTokenUsage, { nullable: true }) +}) {} + +export class ChatError extends S.Class("ChatError")({ + "error": S.Struct({ + "code": S.NullOr(S.Union(S.String, S.Number)), + "message": S.String, + "param": S.optionalWith(S.String, { nullable: true }), + "type": S.optionalWith(S.String, { nullable: true }) + }) +}) {} + +export class CompletionCreateParams extends S.Class("CompletionCreateParams")({ + "model": S.optionalWith(ModelName, { nullable: true }), + "models": S.optionalWith(S.Array(ModelName), { nullable: true }), + "prompt": S.Union(S.String, S.Array(S.String), S.Array(S.Number), S.Array(S.Array(S.Number))), + "best_of": S.optionalWith(S.Int.pipe(S.greaterThanOrEqualTo(1), S.lessThanOrEqualTo(20)), { nullable: true }), + "echo": S.optionalWith(S.Boolean, { nullable: true }), + "frequency_penalty": S.optionalWith(S.Number.pipe(S.greaterThanOrEqualTo(-2), S.lessThanOrEqualTo(2)), { + nullable: true + }), + "logit_bias": S.optionalWith(S.Record({ key: S.String, value: S.Unknown }), { nullable: true }), + "logprobs": S.optionalWith(S.Int.pipe(S.greaterThanOrEqualTo(0), S.lessThanOrEqualTo(5)), { nullable: true }), + "max_tokens": S.optionalWith(S.Int.pipe(S.greaterThanOrEqualTo(1), S.lessThanOrEqualTo(9007199254740991)), { + nullable: true + }), + "n": S.optionalWith(S.Int.pipe(S.greaterThanOrEqualTo(1), S.lessThanOrEqualTo(128)), { nullable: true }), + "presence_penalty": S.optionalWith(S.Number.pipe(S.greaterThanOrEqualTo(-2), S.lessThanOrEqualTo(2)), { + nullable: true + }), + "seed": S.optionalWith(S.Int.pipe(S.greaterThanOrEqualTo(-9007199254740991), S.lessThanOrEqualTo(9007199254740991)), { + nullable: true + }), + "stop": S.optionalWith(S.Union(S.String, S.Array(S.String)), { nullable: true }), + "stream": S.optionalWith(S.Boolean, { nullable: true, default: () => false as const }), + "stream_options": S.optionalWith( + S.Struct({ + "include_usage": S.optionalWith(S.Boolean, { nullable: true }) + }), + { nullable: true } + ), + "suffix": S.optionalWith(S.String, { nullable: true }), + "temperature": S.optionalWith(S.Number.pipe(S.greaterThanOrEqualTo(0), S.lessThanOrEqualTo(2)), { nullable: true }), + "top_p": S.optionalWith(S.Number.pipe(S.greaterThanOrEqualTo(0), S.lessThanOrEqualTo(1)), { nullable: true }), + "user": S.optionalWith(S.String, { nullable: true }), + "metadata": S.optionalWith(S.Record({ key: S.String, value: S.Unknown }), { nullable: true }), + "response_format": S.optionalWith(S.Record({ key: S.String, value: S.Unknown }), { nullable: true }) +}) {} + +export class CompletionLogprobs extends S.Class("CompletionLogprobs")({ + "tokens": S.Array(S.String), + "token_logprobs": S.Array(S.Number), + "top_logprobs": S.NullOr(S.Array(S.Record({ key: S.String, value: S.Unknown }))), + "text_offset": S.Array(S.Number) +}) {} + +export class CompletionFinishReasonEnum extends S.Literal("stop", "length", "content_filter") {} + +export class CompletionFinishReason extends S.Union(CompletionFinishReasonEnum, S.Null) {} + +export class CompletionChoice extends S.Class("CompletionChoice")({ + "text": S.String, + "index": S.Number, + "logprobs": S.NullOr(CompletionLogprobs), + "finish_reason": S.NullOr(S.Literal("stop", "length", "content_filter")), + "native_finish_reason": S.optionalWith(S.String, { nullable: true }), + "reasoning": S.optionalWith(S.String, { nullable: true }) +}) {} + +export class CompletionUsage extends S.Class("CompletionUsage")({ + "prompt_tokens": S.Number, + "completion_tokens": S.Number, + "total_tokens": S.Number +}) {} + +export class CompletionResponse extends S.Class("CompletionResponse")({ + "id": S.String, + "object": S.Literal("text_completion"), + "created": S.Number, + "model": S.String, + "provider": S.optionalWith(S.String, { nullable: true }), + "system_fingerprint": S.optionalWith(S.String, { nullable: true }), + "choices": S.Array(CompletionChoice), + "usage": S.optionalWith(CompletionUsage, { nullable: true }) +}) {} + +export const make = ( + httpClient: HttpClient.HttpClient, + options: { + readonly transformClient?: ((client: HttpClient.HttpClient) => Effect.Effect) | undefined + } = {} +): Client => { + const unexpectedStatus = (response: HttpClientResponse.HttpClientResponse) => + Effect.flatMap( + Effect.orElseSucceed(response.json, () => "Unexpected status code"), + (description) => + Effect.fail( + new HttpClientError.ResponseError({ + request: response.request, + response, + reason: "StatusCode", + description: typeof description === "string" ? description : JSON.stringify(description) + }) + ) + ) + const withResponse: ( + f: (response: HttpClientResponse.HttpClientResponse) => Effect.Effect + ) => ( + request: HttpClientRequest.HttpClientRequest + ) => Effect.Effect = options.transformClient + ? (f) => (request) => + Effect.flatMap( + Effect.flatMap(options.transformClient!(httpClient), (client) => client.execute(request)), + f + ) + : (f) => (request) => Effect.flatMap(httpClient.execute(request), f) + const decodeSuccess = (schema: S.Schema) => (response: HttpClientResponse.HttpClientResponse) => + HttpClientResponse.schemaBodyJson(schema)(response) + const decodeError = + (tag: Tag, schema: S.Schema) => + (response: HttpClientResponse.HttpClientResponse) => + Effect.flatMap( + HttpClientResponse.schemaBodyJson(schema)(response), + (cause) => Effect.fail(ClientError(tag, cause, response)) + ) + return { + httpClient, + "createResponses": (options) => + HttpClientRequest.post(`/responses`).pipe( + HttpClientRequest.bodyUnsafeJson(options), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(OpenResponsesNonStreamingResponse), + "400": decodeError("BadRequestResponse", BadRequestResponse), + "401": decodeError("UnauthorizedResponse", UnauthorizedResponse), + "402": decodeError("PaymentRequiredResponse", PaymentRequiredResponse), + "404": decodeError("NotFoundResponse", NotFoundResponse), + "408": decodeError("RequestTimeoutResponse", RequestTimeoutResponse), + "413": decodeError("PayloadTooLargeResponse", PayloadTooLargeResponse), + "422": decodeError("UnprocessableEntityResponse", UnprocessableEntityResponse), + "429": decodeError("TooManyRequestsResponse", TooManyRequestsResponse), + "500": decodeError("InternalServerResponse", InternalServerResponse), + "502": decodeError("BadGatewayResponse", BadGatewayResponse), + "503": decodeError("ServiceUnavailableResponse", ServiceUnavailableResponse), + "524": decodeError("EdgeNetworkTimeoutResponse", EdgeNetworkTimeoutResponse), + "529": decodeError("ProviderOverloadedResponse", ProviderOverloadedResponse), + orElse: unexpectedStatus + })) + ), + "createMessages": (options) => + HttpClientRequest.post(`/messages`).pipe( + HttpClientRequest.bodyUnsafeJson(options), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(AnthropicMessagesResponse), + "400": decodeError("CreateMessages400", CreateMessages400), + "401": decodeError("CreateMessages401", CreateMessages401), + "403": decodeError("CreateMessages403", CreateMessages403), + "404": decodeError("CreateMessages404", CreateMessages404), + "429": decodeError("CreateMessages429", CreateMessages429), + "500": decodeError("CreateMessages500", CreateMessages500), + "503": decodeError("CreateMessages503", CreateMessages503), + "529": decodeError("CreateMessages529", CreateMessages529), + orElse: unexpectedStatus + })) + ), + "getUserActivity": (options) => + HttpClientRequest.get(`/activity`).pipe( + HttpClientRequest.setUrlParams({ "date": options?.["date"] as any }), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(GetUserActivity200), + "400": decodeError("BadRequestResponse", BadRequestResponse), + "401": decodeError("UnauthorizedResponse", UnauthorizedResponse), + "403": decodeError("ForbiddenResponse", ForbiddenResponse), + "500": decodeError("InternalServerResponse", InternalServerResponse), + orElse: unexpectedStatus + })) + ), + "getCredits": () => + HttpClientRequest.get(`/credits`).pipe( + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(GetCredits200), + "401": decodeError("UnauthorizedResponse", UnauthorizedResponse), + "403": decodeError("ForbiddenResponse", ForbiddenResponse), + "500": decodeError("InternalServerResponse", InternalServerResponse), + orElse: unexpectedStatus + })) + ), + "createCoinbaseCharge": (options) => + HttpClientRequest.post(`/credits/coinbase`).pipe( + HttpClientRequest.bodyUnsafeJson(options), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(CreateCoinbaseCharge200), + "400": decodeError("BadRequestResponse", BadRequestResponse), + "401": decodeError("UnauthorizedResponse", UnauthorizedResponse), + "429": decodeError("TooManyRequestsResponse", TooManyRequestsResponse), + "500": decodeError("InternalServerResponse", InternalServerResponse), + orElse: unexpectedStatus + })) + ), + "createEmbeddings": (options) => + HttpClientRequest.post(`/embeddings`).pipe( + HttpClientRequest.bodyUnsafeJson(options), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(CreateEmbeddings200), + "400": decodeError("BadRequestResponse", BadRequestResponse), + "401": decodeError("UnauthorizedResponse", UnauthorizedResponse), + "402": decodeError("PaymentRequiredResponse", PaymentRequiredResponse), + "404": decodeError("NotFoundResponse", NotFoundResponse), + "429": decodeError("TooManyRequestsResponse", TooManyRequestsResponse), + "500": decodeError("InternalServerResponse", InternalServerResponse), + "502": decodeError("BadGatewayResponse", BadGatewayResponse), + "503": decodeError("ServiceUnavailableResponse", ServiceUnavailableResponse), + "524": decodeError("EdgeNetworkTimeoutResponse", EdgeNetworkTimeoutResponse), + "529": decodeError("ProviderOverloadedResponse", ProviderOverloadedResponse), + orElse: unexpectedStatus + })) + ), + "listEmbeddingsModels": () => + HttpClientRequest.get(`/embeddings/models`).pipe( + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ModelsListResponse), + "400": decodeError("BadRequestResponse", BadRequestResponse), + "500": decodeError("InternalServerResponse", InternalServerResponse), + orElse: unexpectedStatus + })) + ), + "getGeneration": (options) => + HttpClientRequest.get(`/generation`).pipe( + HttpClientRequest.setUrlParams({ "id": options?.["id"] as any }), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(GetGeneration200), + "401": decodeError("UnauthorizedResponse", UnauthorizedResponse), + "402": decodeError("PaymentRequiredResponse", PaymentRequiredResponse), + "404": decodeError("NotFoundResponse", NotFoundResponse), + "429": decodeError("TooManyRequestsResponse", TooManyRequestsResponse), + "500": decodeError("InternalServerResponse", InternalServerResponse), + "502": decodeError("BadGatewayResponse", BadGatewayResponse), + "524": decodeError("EdgeNetworkTimeoutResponse", EdgeNetworkTimeoutResponse), + "529": decodeError("ProviderOverloadedResponse", ProviderOverloadedResponse), + orElse: unexpectedStatus + })) + ), + "listModelsCount": () => + HttpClientRequest.get(`/models/count`).pipe( + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ModelsCountResponse), + "500": decodeError("InternalServerResponse", InternalServerResponse), + orElse: unexpectedStatus + })) + ), + "getModels": (options) => + HttpClientRequest.get(`/models`).pipe( + HttpClientRequest.setUrlParams({ + "category": options?.["category"] as any, + "supported_parameters": options?.["supported_parameters"] as any + }), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ModelsListResponse), + "400": decodeError("BadRequestResponse", BadRequestResponse), + "500": decodeError("InternalServerResponse", InternalServerResponse), + orElse: unexpectedStatus + })) + ), + "listModelsUser": () => + HttpClientRequest.get(`/models/user`).pipe( + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ModelsListResponse), + "401": decodeError("UnauthorizedResponse", UnauthorizedResponse), + "500": decodeError("InternalServerResponse", InternalServerResponse), + orElse: unexpectedStatus + })) + ), + "listEndpoints": (author, slug) => + HttpClientRequest.get(`/models/${author}/${slug}/endpoints`).pipe( + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ListEndpoints200), + "404": decodeError("NotFoundResponse", NotFoundResponse), + "500": decodeError("InternalServerResponse", InternalServerResponse), + orElse: unexpectedStatus + })) + ), + "listEndpointsZdr": () => + HttpClientRequest.get(`/endpoints/zdr`).pipe( + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ListEndpointsZdr200), + "500": decodeError("InternalServerResponse", InternalServerResponse), + orElse: unexpectedStatus + })) + ), + "listProviders": () => + HttpClientRequest.get(`/providers`).pipe( + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ListProviders200), + "500": decodeError("InternalServerResponse", InternalServerResponse), + orElse: unexpectedStatus + })) + ), + "list": (options) => + HttpClientRequest.get(`/keys`).pipe( + HttpClientRequest.setUrlParams({ + "include_disabled": options?.["include_disabled"] as any, + "offset": options?.["offset"] as any + }), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(List200), + "401": decodeError("UnauthorizedResponse", UnauthorizedResponse), + "429": decodeError("TooManyRequestsResponse", TooManyRequestsResponse), + "500": decodeError("InternalServerResponse", InternalServerResponse), + orElse: unexpectedStatus + })) + ), + "createKeys": (options) => + HttpClientRequest.post(`/keys`).pipe( + HttpClientRequest.bodyUnsafeJson(options), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(CreateKeys201), + "400": decodeError("BadRequestResponse", BadRequestResponse), + "401": decodeError("UnauthorizedResponse", UnauthorizedResponse), + "429": decodeError("TooManyRequestsResponse", TooManyRequestsResponse), + "500": decodeError("InternalServerResponse", InternalServerResponse), + orElse: unexpectedStatus + })) + ), + "getKey": (hash) => + HttpClientRequest.get(`/keys/${hash}`).pipe( + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(GetKey200), + "401": decodeError("UnauthorizedResponse", UnauthorizedResponse), + "404": decodeError("NotFoundResponse", NotFoundResponse), + "429": decodeError("TooManyRequestsResponse", TooManyRequestsResponse), + "500": decodeError("InternalServerResponse", InternalServerResponse), + orElse: unexpectedStatus + })) + ), + "deleteKeys": (hash) => + HttpClientRequest.del(`/keys/${hash}`).pipe( + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(DeleteKeys200), + "401": decodeError("UnauthorizedResponse", UnauthorizedResponse), + "404": decodeError("NotFoundResponse", NotFoundResponse), + "429": decodeError("TooManyRequestsResponse", TooManyRequestsResponse), + "500": decodeError("InternalServerResponse", InternalServerResponse), + orElse: unexpectedStatus + })) + ), + "updateKeys": (hash, options) => + HttpClientRequest.patch(`/keys/${hash}`).pipe( + HttpClientRequest.bodyUnsafeJson(options), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(UpdateKeys200), + "400": decodeError("BadRequestResponse", BadRequestResponse), + "401": decodeError("UnauthorizedResponse", UnauthorizedResponse), + "404": decodeError("NotFoundResponse", NotFoundResponse), + "429": decodeError("TooManyRequestsResponse", TooManyRequestsResponse), + "500": decodeError("InternalServerResponse", InternalServerResponse), + orElse: unexpectedStatus + })) + ), + "listGuardrails": (options) => + HttpClientRequest.get(`/guardrails`).pipe( + HttpClientRequest.setUrlParams({ "offset": options?.["offset"] as any, "limit": options?.["limit"] as any }), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ListGuardrails200), + "401": decodeError("UnauthorizedResponse", UnauthorizedResponse), + "500": decodeError("InternalServerResponse", InternalServerResponse), + orElse: unexpectedStatus + })) + ), + "createGuardrail": (options) => + HttpClientRequest.post(`/guardrails`).pipe( + HttpClientRequest.bodyUnsafeJson(options), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(CreateGuardrail201), + "400": decodeError("BadRequestResponse", BadRequestResponse), + "401": decodeError("UnauthorizedResponse", UnauthorizedResponse), + "500": decodeError("InternalServerResponse", InternalServerResponse), + orElse: unexpectedStatus + })) + ), + "getGuardrail": (id) => + HttpClientRequest.get(`/guardrails/${id}`).pipe( + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(GetGuardrail200), + "401": decodeError("UnauthorizedResponse", UnauthorizedResponse), + "404": decodeError("NotFoundResponse", NotFoundResponse), + "500": decodeError("InternalServerResponse", InternalServerResponse), + orElse: unexpectedStatus + })) + ), + "deleteGuardrail": (id) => + HttpClientRequest.del(`/guardrails/${id}`).pipe( + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(DeleteGuardrail200), + "401": decodeError("UnauthorizedResponse", UnauthorizedResponse), + "404": decodeError("NotFoundResponse", NotFoundResponse), + "500": decodeError("InternalServerResponse", InternalServerResponse), + orElse: unexpectedStatus + })) + ), + "updateGuardrail": (id, options) => + HttpClientRequest.patch(`/guardrails/${id}`).pipe( + HttpClientRequest.bodyUnsafeJson(options), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(UpdateGuardrail200), + "400": decodeError("BadRequestResponse", BadRequestResponse), + "401": decodeError("UnauthorizedResponse", UnauthorizedResponse), + "404": decodeError("NotFoundResponse", NotFoundResponse), + "500": decodeError("InternalServerResponse", InternalServerResponse), + orElse: unexpectedStatus + })) + ), + "listKeyAssignments": (options) => + HttpClientRequest.get(`/guardrails/assignments/keys`).pipe( + HttpClientRequest.setUrlParams({ "offset": options?.["offset"] as any, "limit": options?.["limit"] as any }), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ListKeyAssignments200), + "401": decodeError("UnauthorizedResponse", UnauthorizedResponse), + "500": decodeError("InternalServerResponse", InternalServerResponse), + orElse: unexpectedStatus + })) + ), + "listMemberAssignments": (options) => + HttpClientRequest.get(`/guardrails/assignments/members`).pipe( + HttpClientRequest.setUrlParams({ "offset": options?.["offset"] as any, "limit": options?.["limit"] as any }), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ListMemberAssignments200), + "401": decodeError("UnauthorizedResponse", UnauthorizedResponse), + "500": decodeError("InternalServerResponse", InternalServerResponse), + orElse: unexpectedStatus + })) + ), + "listGuardrailKeyAssignments": (id, options) => + HttpClientRequest.get(`/guardrails/${id}/assignments/keys`).pipe( + HttpClientRequest.setUrlParams({ "offset": options?.["offset"] as any, "limit": options?.["limit"] as any }), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ListGuardrailKeyAssignments200), + "401": decodeError("UnauthorizedResponse", UnauthorizedResponse), + "404": decodeError("NotFoundResponse", NotFoundResponse), + "500": decodeError("InternalServerResponse", InternalServerResponse), + orElse: unexpectedStatus + })) + ), + "bulkAssignKeysToGuardrail": (id, options) => + HttpClientRequest.post(`/guardrails/${id}/assignments/keys`).pipe( + HttpClientRequest.bodyUnsafeJson(options), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(BulkAssignKeysToGuardrail200), + "400": decodeError("BadRequestResponse", BadRequestResponse), + "401": decodeError("UnauthorizedResponse", UnauthorizedResponse), + "404": decodeError("NotFoundResponse", NotFoundResponse), + "500": decodeError("InternalServerResponse", InternalServerResponse), + orElse: unexpectedStatus + })) + ), + "listGuardrailMemberAssignments": (id, options) => + HttpClientRequest.get(`/guardrails/${id}/assignments/members`).pipe( + HttpClientRequest.setUrlParams({ "offset": options?.["offset"] as any, "limit": options?.["limit"] as any }), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ListGuardrailMemberAssignments200), + "401": decodeError("UnauthorizedResponse", UnauthorizedResponse), + "404": decodeError("NotFoundResponse", NotFoundResponse), + "500": decodeError("InternalServerResponse", InternalServerResponse), + orElse: unexpectedStatus + })) + ), + "bulkAssignMembersToGuardrail": (id, options) => + HttpClientRequest.post(`/guardrails/${id}/assignments/members`).pipe( + HttpClientRequest.bodyUnsafeJson(options), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(BulkAssignMembersToGuardrail200), + "400": decodeError("BadRequestResponse", BadRequestResponse), + "401": decodeError("UnauthorizedResponse", UnauthorizedResponse), + "404": decodeError("NotFoundResponse", NotFoundResponse), + "500": decodeError("InternalServerResponse", InternalServerResponse), + orElse: unexpectedStatus + })) + ), + "bulkUnassignKeysFromGuardrail": (id, options) => + HttpClientRequest.post(`/guardrails/${id}/assignments/keys/remove`).pipe( + HttpClientRequest.bodyUnsafeJson(options), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(BulkUnassignKeysFromGuardrail200), + "400": decodeError("BadRequestResponse", BadRequestResponse), + "401": decodeError("UnauthorizedResponse", UnauthorizedResponse), + "404": decodeError("NotFoundResponse", NotFoundResponse), + "500": decodeError("InternalServerResponse", InternalServerResponse), + orElse: unexpectedStatus + })) + ), + "bulkUnassignMembersFromGuardrail": (id, options) => + HttpClientRequest.post(`/guardrails/${id}/assignments/members/remove`).pipe( + HttpClientRequest.bodyUnsafeJson(options), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(BulkUnassignMembersFromGuardrail200), + "400": decodeError("BadRequestResponse", BadRequestResponse), + "401": decodeError("UnauthorizedResponse", UnauthorizedResponse), + "404": decodeError("NotFoundResponse", NotFoundResponse), + "500": decodeError("InternalServerResponse", InternalServerResponse), + orElse: unexpectedStatus + })) + ), + "getCurrentKey": () => + HttpClientRequest.get(`/key`).pipe( + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(GetCurrentKey200), + "401": decodeError("UnauthorizedResponse", UnauthorizedResponse), + "500": decodeError("InternalServerResponse", InternalServerResponse), + orElse: unexpectedStatus + })) + ), + "exchangeAuthCodeForAPIKey": (options) => + HttpClientRequest.post(`/auth/keys`).pipe( + HttpClientRequest.bodyUnsafeJson(options), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ExchangeAuthCodeForAPIKey200), + "400": decodeError("BadRequestResponse", BadRequestResponse), + "403": decodeError("ForbiddenResponse", ForbiddenResponse), + "500": decodeError("InternalServerResponse", InternalServerResponse), + orElse: unexpectedStatus + })) + ), + "createAuthKeysCode": (options) => + HttpClientRequest.post(`/auth/keys/code`).pipe( + HttpClientRequest.bodyUnsafeJson(options), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(CreateAuthKeysCode200), + "400": decodeError("BadRequestResponse", BadRequestResponse), + "401": decodeError("UnauthorizedResponse", UnauthorizedResponse), + "500": decodeError("InternalServerResponse", InternalServerResponse), + orElse: unexpectedStatus + })) + ), + "sendChatCompletionRequest": (options) => + HttpClientRequest.post(`/chat/completions`).pipe( + HttpClientRequest.bodyUnsafeJson(options), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ChatResponse), + "400": decodeError("ChatError", ChatError), + "401": decodeError("ChatError", ChatError), + "429": decodeError("ChatError", ChatError), + "500": decodeError("ChatError", ChatError), + orElse: unexpectedStatus + })) + ), + "createCompletions": (options) => + HttpClientRequest.post(`/completions`).pipe( + HttpClientRequest.bodyUnsafeJson(options), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(CompletionResponse), + "400": decodeError("ChatError", ChatError), + "401": decodeError("ChatError", ChatError), + "429": decodeError("ChatError", ChatError), + "500": decodeError("ChatError", ChatError), + orElse: unexpectedStatus + })) + ) + } +} + +export interface Client { + readonly httpClient: HttpClient.HttpClient + /** + * Creates a streaming or non-streaming response using OpenResponses API format + */ + readonly "createResponses": ( + options: typeof OpenResponsesRequest.Encoded + ) => Effect.Effect< + typeof OpenResponsesNonStreamingResponse.Type, + | HttpClientError.HttpClientError + | ParseError + | ClientError<"BadRequestResponse", typeof BadRequestResponse.Type> + | ClientError<"UnauthorizedResponse", typeof UnauthorizedResponse.Type> + | ClientError<"PaymentRequiredResponse", typeof PaymentRequiredResponse.Type> + | ClientError<"NotFoundResponse", typeof NotFoundResponse.Type> + | ClientError<"RequestTimeoutResponse", typeof RequestTimeoutResponse.Type> + | ClientError<"PayloadTooLargeResponse", typeof PayloadTooLargeResponse.Type> + | ClientError<"UnprocessableEntityResponse", typeof UnprocessableEntityResponse.Type> + | ClientError<"TooManyRequestsResponse", typeof TooManyRequestsResponse.Type> + | ClientError<"InternalServerResponse", typeof InternalServerResponse.Type> + | ClientError<"BadGatewayResponse", typeof BadGatewayResponse.Type> + | ClientError<"ServiceUnavailableResponse", typeof ServiceUnavailableResponse.Type> + | ClientError<"EdgeNetworkTimeoutResponse", typeof EdgeNetworkTimeoutResponse.Type> + | ClientError<"ProviderOverloadedResponse", typeof ProviderOverloadedResponse.Type> + > + /** + * Creates a message using the Anthropic Messages API format. Supports text, images, PDFs, tools, and extended thinking. + */ + readonly "createMessages": ( + options: typeof AnthropicMessagesRequest.Encoded + ) => Effect.Effect< + typeof AnthropicMessagesResponse.Type, + | HttpClientError.HttpClientError + | ParseError + | ClientError<"CreateMessages400", typeof CreateMessages400.Type> + | ClientError<"CreateMessages401", typeof CreateMessages401.Type> + | ClientError<"CreateMessages403", typeof CreateMessages403.Type> + | ClientError<"CreateMessages404", typeof CreateMessages404.Type> + | ClientError<"CreateMessages429", typeof CreateMessages429.Type> + | ClientError<"CreateMessages500", typeof CreateMessages500.Type> + | ClientError<"CreateMessages503", typeof CreateMessages503.Type> + | ClientError<"CreateMessages529", typeof CreateMessages529.Type> + > + /** + * Returns user activity data grouped by endpoint for the last 30 (completed) UTC days. [Provisioning key](/docs/guides/overview/auth/provisioning-api-keys) required. + */ + readonly "getUserActivity": ( + options?: typeof GetUserActivityParams.Encoded | undefined + ) => Effect.Effect< + typeof GetUserActivity200.Type, + | HttpClientError.HttpClientError + | ParseError + | ClientError<"BadRequestResponse", typeof BadRequestResponse.Type> + | ClientError<"UnauthorizedResponse", typeof UnauthorizedResponse.Type> + | ClientError<"ForbiddenResponse", typeof ForbiddenResponse.Type> + | ClientError<"InternalServerResponse", typeof InternalServerResponse.Type> + > + /** + * Get total credits purchased and used for the authenticated user. [Provisioning key](/docs/guides/overview/auth/provisioning-api-keys) required. + */ + readonly "getCredits": () => Effect.Effect< + typeof GetCredits200.Type, + | HttpClientError.HttpClientError + | ParseError + | ClientError<"UnauthorizedResponse", typeof UnauthorizedResponse.Type> + | ClientError<"ForbiddenResponse", typeof ForbiddenResponse.Type> + | ClientError<"InternalServerResponse", typeof InternalServerResponse.Type> + > + /** + * Create a Coinbase charge for crypto payment + */ + readonly "createCoinbaseCharge": ( + options: typeof CreateChargeRequest.Encoded + ) => Effect.Effect< + typeof CreateCoinbaseCharge200.Type, + | HttpClientError.HttpClientError + | ParseError + | ClientError<"BadRequestResponse", typeof BadRequestResponse.Type> + | ClientError<"UnauthorizedResponse", typeof UnauthorizedResponse.Type> + | ClientError<"TooManyRequestsResponse", typeof TooManyRequestsResponse.Type> + | ClientError<"InternalServerResponse", typeof InternalServerResponse.Type> + > + /** + * Submits an embedding request to the embeddings router + */ + readonly "createEmbeddings": ( + options: typeof CreateEmbeddingsRequest.Encoded + ) => Effect.Effect< + typeof CreateEmbeddings200.Type, + | HttpClientError.HttpClientError + | ParseError + | ClientError<"BadRequestResponse", typeof BadRequestResponse.Type> + | ClientError<"UnauthorizedResponse", typeof UnauthorizedResponse.Type> + | ClientError<"PaymentRequiredResponse", typeof PaymentRequiredResponse.Type> + | ClientError<"NotFoundResponse", typeof NotFoundResponse.Type> + | ClientError<"TooManyRequestsResponse", typeof TooManyRequestsResponse.Type> + | ClientError<"InternalServerResponse", typeof InternalServerResponse.Type> + | ClientError<"BadGatewayResponse", typeof BadGatewayResponse.Type> + | ClientError<"ServiceUnavailableResponse", typeof ServiceUnavailableResponse.Type> + | ClientError<"EdgeNetworkTimeoutResponse", typeof EdgeNetworkTimeoutResponse.Type> + | ClientError<"ProviderOverloadedResponse", typeof ProviderOverloadedResponse.Type> + > + /** + * Returns a list of all available embeddings models and their properties + */ + readonly "listEmbeddingsModels": () => Effect.Effect< + typeof ModelsListResponse.Type, + | HttpClientError.HttpClientError + | ParseError + | ClientError<"BadRequestResponse", typeof BadRequestResponse.Type> + | ClientError<"InternalServerResponse", typeof InternalServerResponse.Type> + > + /** + * Get request & usage metadata for a generation + */ + readonly "getGeneration": ( + options: typeof GetGenerationParams.Encoded + ) => Effect.Effect< + typeof GetGeneration200.Type, + | HttpClientError.HttpClientError + | ParseError + | ClientError<"UnauthorizedResponse", typeof UnauthorizedResponse.Type> + | ClientError<"PaymentRequiredResponse", typeof PaymentRequiredResponse.Type> + | ClientError<"NotFoundResponse", typeof NotFoundResponse.Type> + | ClientError<"TooManyRequestsResponse", typeof TooManyRequestsResponse.Type> + | ClientError<"InternalServerResponse", typeof InternalServerResponse.Type> + | ClientError<"BadGatewayResponse", typeof BadGatewayResponse.Type> + | ClientError<"EdgeNetworkTimeoutResponse", typeof EdgeNetworkTimeoutResponse.Type> + | ClientError<"ProviderOverloadedResponse", typeof ProviderOverloadedResponse.Type> + > + /** + * Get total count of available models + */ + readonly "listModelsCount": () => Effect.Effect< + typeof ModelsCountResponse.Type, + | HttpClientError.HttpClientError + | ParseError + | ClientError<"InternalServerResponse", typeof InternalServerResponse.Type> + > + /** + * List all models and their properties + */ + readonly "getModels": ( + options?: typeof GetModelsParams.Encoded | undefined + ) => Effect.Effect< + typeof ModelsListResponse.Type, + | HttpClientError.HttpClientError + | ParseError + | ClientError<"BadRequestResponse", typeof BadRequestResponse.Type> + | ClientError<"InternalServerResponse", typeof InternalServerResponse.Type> + > + /** + * List models filtered by user provider preferences + */ + readonly "listModelsUser": () => Effect.Effect< + typeof ModelsListResponse.Type, + | HttpClientError.HttpClientError + | ParseError + | ClientError<"UnauthorizedResponse", typeof UnauthorizedResponse.Type> + | ClientError<"InternalServerResponse", typeof InternalServerResponse.Type> + > + /** + * List all endpoints for a model + */ + readonly "listEndpoints": ( + author: string, + slug: string + ) => Effect.Effect< + typeof ListEndpoints200.Type, + | HttpClientError.HttpClientError + | ParseError + | ClientError<"NotFoundResponse", typeof NotFoundResponse.Type> + | ClientError<"InternalServerResponse", typeof InternalServerResponse.Type> + > + /** + * Preview the impact of ZDR on the available endpoints + */ + readonly "listEndpointsZdr": () => Effect.Effect< + typeof ListEndpointsZdr200.Type, + | HttpClientError.HttpClientError + | ParseError + | ClientError<"InternalServerResponse", typeof InternalServerResponse.Type> + > + /** + * List all providers + */ + readonly "listProviders": () => Effect.Effect< + typeof ListProviders200.Type, + | HttpClientError.HttpClientError + | ParseError + | ClientError<"InternalServerResponse", typeof InternalServerResponse.Type> + > + /** + * List all API keys for the authenticated user. [Provisioning key](/docs/guides/overview/auth/provisioning-api-keys) required. + */ + readonly "list": ( + options?: typeof ListParams.Encoded | undefined + ) => Effect.Effect< + typeof List200.Type, + | HttpClientError.HttpClientError + | ParseError + | ClientError<"UnauthorizedResponse", typeof UnauthorizedResponse.Type> + | ClientError<"TooManyRequestsResponse", typeof TooManyRequestsResponse.Type> + | ClientError<"InternalServerResponse", typeof InternalServerResponse.Type> + > + /** + * Create a new API key for the authenticated user. [Provisioning key](/docs/guides/overview/auth/provisioning-api-keys) required. + */ + readonly "createKeys": ( + options: typeof CreateKeysRequest.Encoded + ) => Effect.Effect< + typeof CreateKeys201.Type, + | HttpClientError.HttpClientError + | ParseError + | ClientError<"BadRequestResponse", typeof BadRequestResponse.Type> + | ClientError<"UnauthorizedResponse", typeof UnauthorizedResponse.Type> + | ClientError<"TooManyRequestsResponse", typeof TooManyRequestsResponse.Type> + | ClientError<"InternalServerResponse", typeof InternalServerResponse.Type> + > + /** + * Get a single API key by hash. [Provisioning key](/docs/guides/overview/auth/provisioning-api-keys) required. + */ + readonly "getKey": ( + hash: string + ) => Effect.Effect< + typeof GetKey200.Type, + | HttpClientError.HttpClientError + | ParseError + | ClientError<"UnauthorizedResponse", typeof UnauthorizedResponse.Type> + | ClientError<"NotFoundResponse", typeof NotFoundResponse.Type> + | ClientError<"TooManyRequestsResponse", typeof TooManyRequestsResponse.Type> + | ClientError<"InternalServerResponse", typeof InternalServerResponse.Type> + > + /** + * Delete an existing API key. [Provisioning key](/docs/guides/overview/auth/provisioning-api-keys) required. + */ + readonly "deleteKeys": ( + hash: string + ) => Effect.Effect< + typeof DeleteKeys200.Type, + | HttpClientError.HttpClientError + | ParseError + | ClientError<"UnauthorizedResponse", typeof UnauthorizedResponse.Type> + | ClientError<"NotFoundResponse", typeof NotFoundResponse.Type> + | ClientError<"TooManyRequestsResponse", typeof TooManyRequestsResponse.Type> + | ClientError<"InternalServerResponse", typeof InternalServerResponse.Type> + > + /** + * Update an existing API key. [Provisioning key](/docs/guides/overview/auth/provisioning-api-keys) required. + */ + readonly "updateKeys": ( + hash: string, + options: typeof UpdateKeysRequest.Encoded + ) => Effect.Effect< + typeof UpdateKeys200.Type, + | HttpClientError.HttpClientError + | ParseError + | ClientError<"BadRequestResponse", typeof BadRequestResponse.Type> + | ClientError<"UnauthorizedResponse", typeof UnauthorizedResponse.Type> + | ClientError<"NotFoundResponse", typeof NotFoundResponse.Type> + | ClientError<"TooManyRequestsResponse", typeof TooManyRequestsResponse.Type> + | ClientError<"InternalServerResponse", typeof InternalServerResponse.Type> + > + /** + * List all guardrails for the authenticated user. [Provisioning key](/docs/guides/overview/auth/provisioning-api-keys) required. + */ + readonly "listGuardrails": ( + options?: typeof ListGuardrailsParams.Encoded | undefined + ) => Effect.Effect< + typeof ListGuardrails200.Type, + | HttpClientError.HttpClientError + | ParseError + | ClientError<"UnauthorizedResponse", typeof UnauthorizedResponse.Type> + | ClientError<"InternalServerResponse", typeof InternalServerResponse.Type> + > + /** + * Create a new guardrail for the authenticated user. [Provisioning key](/docs/guides/overview/auth/provisioning-api-keys) required. + */ + readonly "createGuardrail": ( + options: typeof CreateGuardrailRequest.Encoded + ) => Effect.Effect< + typeof CreateGuardrail201.Type, + | HttpClientError.HttpClientError + | ParseError + | ClientError<"BadRequestResponse", typeof BadRequestResponse.Type> + | ClientError<"UnauthorizedResponse", typeof UnauthorizedResponse.Type> + | ClientError<"InternalServerResponse", typeof InternalServerResponse.Type> + > + /** + * Get a single guardrail by ID. [Provisioning key](/docs/guides/overview/auth/provisioning-api-keys) required. + */ + readonly "getGuardrail": ( + id: string + ) => Effect.Effect< + typeof GetGuardrail200.Type, + | HttpClientError.HttpClientError + | ParseError + | ClientError<"UnauthorizedResponse", typeof UnauthorizedResponse.Type> + | ClientError<"NotFoundResponse", typeof NotFoundResponse.Type> + | ClientError<"InternalServerResponse", typeof InternalServerResponse.Type> + > + /** + * Delete an existing guardrail. [Provisioning key](/docs/guides/overview/auth/provisioning-api-keys) required. + */ + readonly "deleteGuardrail": ( + id: string + ) => Effect.Effect< + typeof DeleteGuardrail200.Type, + | HttpClientError.HttpClientError + | ParseError + | ClientError<"UnauthorizedResponse", typeof UnauthorizedResponse.Type> + | ClientError<"NotFoundResponse", typeof NotFoundResponse.Type> + | ClientError<"InternalServerResponse", typeof InternalServerResponse.Type> + > + /** + * Update an existing guardrail. [Provisioning key](/docs/guides/overview/auth/provisioning-api-keys) required. + */ + readonly "updateGuardrail": ( + id: string, + options: typeof UpdateGuardrailRequest.Encoded + ) => Effect.Effect< + typeof UpdateGuardrail200.Type, + | HttpClientError.HttpClientError + | ParseError + | ClientError<"BadRequestResponse", typeof BadRequestResponse.Type> + | ClientError<"UnauthorizedResponse", typeof UnauthorizedResponse.Type> + | ClientError<"NotFoundResponse", typeof NotFoundResponse.Type> + | ClientError<"InternalServerResponse", typeof InternalServerResponse.Type> + > + /** + * List all API key guardrail assignments for the authenticated user. [Provisioning key](/docs/guides/overview/auth/provisioning-api-keys) required. + */ + readonly "listKeyAssignments": ( + options?: typeof ListKeyAssignmentsParams.Encoded | undefined + ) => Effect.Effect< + typeof ListKeyAssignments200.Type, + | HttpClientError.HttpClientError + | ParseError + | ClientError<"UnauthorizedResponse", typeof UnauthorizedResponse.Type> + | ClientError<"InternalServerResponse", typeof InternalServerResponse.Type> + > + /** + * List all organization member guardrail assignments for the authenticated user. [Provisioning key](/docs/guides/overview/auth/provisioning-api-keys) required. + */ + readonly "listMemberAssignments": ( + options?: typeof ListMemberAssignmentsParams.Encoded | undefined + ) => Effect.Effect< + typeof ListMemberAssignments200.Type, + | HttpClientError.HttpClientError + | ParseError + | ClientError<"UnauthorizedResponse", typeof UnauthorizedResponse.Type> + | ClientError<"InternalServerResponse", typeof InternalServerResponse.Type> + > + /** + * List all API key assignments for a specific guardrail. [Provisioning key](/docs/guides/overview/auth/provisioning-api-keys) required. + */ + readonly "listGuardrailKeyAssignments": ( + id: string, + options?: typeof ListGuardrailKeyAssignmentsParams.Encoded | undefined + ) => Effect.Effect< + typeof ListGuardrailKeyAssignments200.Type, + | HttpClientError.HttpClientError + | ParseError + | ClientError<"UnauthorizedResponse", typeof UnauthorizedResponse.Type> + | ClientError<"NotFoundResponse", typeof NotFoundResponse.Type> + | ClientError<"InternalServerResponse", typeof InternalServerResponse.Type> + > + /** + * Assign multiple API keys to a specific guardrail. [Provisioning key](/docs/guides/overview/auth/provisioning-api-keys) required. + */ + readonly "bulkAssignKeysToGuardrail": ( + id: string, + options: typeof BulkAssignKeysToGuardrailRequest.Encoded + ) => Effect.Effect< + typeof BulkAssignKeysToGuardrail200.Type, + | HttpClientError.HttpClientError + | ParseError + | ClientError<"BadRequestResponse", typeof BadRequestResponse.Type> + | ClientError<"UnauthorizedResponse", typeof UnauthorizedResponse.Type> + | ClientError<"NotFoundResponse", typeof NotFoundResponse.Type> + | ClientError<"InternalServerResponse", typeof InternalServerResponse.Type> + > + /** + * List all organization member assignments for a specific guardrail. [Provisioning key](/docs/guides/overview/auth/provisioning-api-keys) required. + */ + readonly "listGuardrailMemberAssignments": ( + id: string, + options?: typeof ListGuardrailMemberAssignmentsParams.Encoded | undefined + ) => Effect.Effect< + typeof ListGuardrailMemberAssignments200.Type, + | HttpClientError.HttpClientError + | ParseError + | ClientError<"UnauthorizedResponse", typeof UnauthorizedResponse.Type> + | ClientError<"NotFoundResponse", typeof NotFoundResponse.Type> + | ClientError<"InternalServerResponse", typeof InternalServerResponse.Type> + > + /** + * Assign multiple organization members to a specific guardrail. [Provisioning key](/docs/guides/overview/auth/provisioning-api-keys) required. + */ + readonly "bulkAssignMembersToGuardrail": ( + id: string, + options: typeof BulkAssignMembersToGuardrailRequest.Encoded + ) => Effect.Effect< + typeof BulkAssignMembersToGuardrail200.Type, + | HttpClientError.HttpClientError + | ParseError + | ClientError<"BadRequestResponse", typeof BadRequestResponse.Type> + | ClientError<"UnauthorizedResponse", typeof UnauthorizedResponse.Type> + | ClientError<"NotFoundResponse", typeof NotFoundResponse.Type> + | ClientError<"InternalServerResponse", typeof InternalServerResponse.Type> + > + /** + * Unassign multiple API keys from a specific guardrail. [Provisioning key](/docs/guides/overview/auth/provisioning-api-keys) required. + */ + readonly "bulkUnassignKeysFromGuardrail": ( + id: string, + options: typeof BulkUnassignKeysFromGuardrailRequest.Encoded + ) => Effect.Effect< + typeof BulkUnassignKeysFromGuardrail200.Type, + | HttpClientError.HttpClientError + | ParseError + | ClientError<"BadRequestResponse", typeof BadRequestResponse.Type> + | ClientError<"UnauthorizedResponse", typeof UnauthorizedResponse.Type> + | ClientError<"NotFoundResponse", typeof NotFoundResponse.Type> + | ClientError<"InternalServerResponse", typeof InternalServerResponse.Type> + > + /** + * Unassign multiple organization members from a specific guardrail. [Provisioning key](/docs/guides/overview/auth/provisioning-api-keys) required. + */ + readonly "bulkUnassignMembersFromGuardrail": ( + id: string, + options: typeof BulkUnassignMembersFromGuardrailRequest.Encoded + ) => Effect.Effect< + typeof BulkUnassignMembersFromGuardrail200.Type, + | HttpClientError.HttpClientError + | ParseError + | ClientError<"BadRequestResponse", typeof BadRequestResponse.Type> + | ClientError<"UnauthorizedResponse", typeof UnauthorizedResponse.Type> + | ClientError<"NotFoundResponse", typeof NotFoundResponse.Type> + | ClientError<"InternalServerResponse", typeof InternalServerResponse.Type> + > + /** + * Get information on the API key associated with the current authentication session + */ + readonly "getCurrentKey": () => Effect.Effect< + typeof GetCurrentKey200.Type, + | HttpClientError.HttpClientError + | ParseError + | ClientError<"UnauthorizedResponse", typeof UnauthorizedResponse.Type> + | ClientError<"InternalServerResponse", typeof InternalServerResponse.Type> + > + /** + * Exchange an authorization code from the PKCE flow for a user-controlled API key + */ + readonly "exchangeAuthCodeForAPIKey": ( + options: typeof ExchangeAuthCodeForAPIKeyRequest.Encoded + ) => Effect.Effect< + typeof ExchangeAuthCodeForAPIKey200.Type, + | HttpClientError.HttpClientError + | ParseError + | ClientError<"BadRequestResponse", typeof BadRequestResponse.Type> + | ClientError<"ForbiddenResponse", typeof ForbiddenResponse.Type> + | ClientError<"InternalServerResponse", typeof InternalServerResponse.Type> + > + /** + * Create an authorization code for the PKCE flow to generate a user-controlled API key + */ + readonly "createAuthKeysCode": ( + options: typeof CreateAuthKeysCodeRequest.Encoded + ) => Effect.Effect< + typeof CreateAuthKeysCode200.Type, + | HttpClientError.HttpClientError + | ParseError + | ClientError<"BadRequestResponse", typeof BadRequestResponse.Type> + | ClientError<"UnauthorizedResponse", typeof UnauthorizedResponse.Type> + | ClientError<"InternalServerResponse", typeof InternalServerResponse.Type> + > + /** + * Sends a request for a model response for the given chat conversation. Supports both streaming and non-streaming modes. + */ + readonly "sendChatCompletionRequest": ( + options: typeof ChatGenerationParams.Encoded + ) => Effect.Effect< + typeof ChatResponse.Type, + | HttpClientError.HttpClientError + | ParseError + | ClientError<"ChatError", typeof ChatError.Type> + | ClientError<"ChatError", typeof ChatError.Type> + | ClientError<"ChatError", typeof ChatError.Type> + | ClientError<"ChatError", typeof ChatError.Type> + > + /** + * Creates a completion for the provided prompt and parameters. Supports both streaming and non-streaming modes. + */ + readonly "createCompletions": ( + options: typeof CompletionCreateParams.Encoded + ) => Effect.Effect< + typeof CompletionResponse.Type, + | HttpClientError.HttpClientError + | ParseError + | ClientError<"ChatError", typeof ChatError.Type> + | ClientError<"ChatError", typeof ChatError.Type> + | ClientError<"ChatError", typeof ChatError.Type> + | ClientError<"ChatError", typeof ChatError.Type> + > +} + +export interface ClientError { + readonly _tag: Tag + readonly request: HttpClientRequest.HttpClientRequest + readonly response: HttpClientResponse.HttpClientResponse + readonly cause: E +} + +class ClientErrorImpl extends Data.Error<{ + _tag: string + cause: any + request: HttpClientRequest.HttpClientRequest + response: HttpClientResponse.HttpClientResponse +}> {} + +export const ClientError = ( + tag: Tag, + cause: E, + response: HttpClientResponse.HttpClientResponse +): ClientError => + new ClientErrorImpl({ + _tag: tag, + cause, + response, + request: response.request + }) as any diff --git a/repos/effect/packages/ai/openrouter/src/OpenRouterClient.ts b/repos/effect/packages/ai/openrouter/src/OpenRouterClient.ts new file mode 100644 index 0000000..9cd54a6 --- /dev/null +++ b/repos/effect/packages/ai/openrouter/src/OpenRouterClient.ts @@ -0,0 +1,372 @@ +/** + * @since 1.0.0 + */ +import * as AiError from "@effect/ai/AiError" +import * as Sse from "@effect/experimental/Sse" +import * as HttpBody from "@effect/platform/HttpBody" +import * as HttpClient from "@effect/platform/HttpClient" +import * as HttpClientRequest from "@effect/platform/HttpClientRequest" +import * as Config from "effect/Config" +import type { ConfigError } from "effect/ConfigError" +import * as Context from "effect/Context" +import * as Effect from "effect/Effect" +import { identity } from "effect/Function" +import * as Layer from "effect/Layer" +import type * as Redacted from "effect/Redacted" +import * as Schema from "effect/Schema" +import * as Stream from "effect/Stream" +import * as Generated from "./Generated.js" +import { OpenRouterConfig } from "./OpenRouterConfig.js" + +/** + * @since 1.0.0 + * @category Context + */ +export class OpenRouterClient extends Context.Tag( + "@effect/ai-openrouter/OpenRouterClient" +)() {} + +/** + * @since 1.0.0 + * @category Models + */ +export interface Service { + /** + * The underlying HTTP client capable of communicating with the OpenRouter API. + * + * This client is pre-configured with authentication, base URL, and standard + * headers required for OpenRouter API communication. It provides direct access + * to the generated OpenRouter API client for operations not covered by the + * higher-level methods. + * + * Use this when you need to: + * - Access provider-specific API endpoints not available through the AI SDK + * - Implement custom request/response handling + * - Use OpenRouter API features not yet supported by the Effect AI abstractions + * - Perform batch operations or non-streaming requests + * + * The client automatically handles authentication and follows OpenRouter's + * API conventions for request formatting and error handling. + */ + readonly client: Generated.Client + + readonly createChatCompletion: ( + options: typeof Generated.ChatGenerationParams.Encoded + ) => Effect.Effect + + readonly createChatCompletionStream: ( + options: Omit + ) => Stream.Stream +} + +/** + * @since 1.0.0 + * @category Constructors + */ +export const make: (options: { + readonly apiKey?: Redacted.Redacted | undefined + readonly apiUrl?: string | undefined + /** + * Optional URL of your site for rankings on `openrouter.ai`. + */ + readonly referrer?: string | undefined + /** + * Optional title of your site for rankings on `openrouter.ai`. + */ + readonly title?: string | undefined + /** + * A function to transform the underlying HTTP client before it's used to send + * API requests. + * + * This transformation function receives the configured HTTP client and returns + * a modified version. It's applied after all standard client configuration + * (authentication, base URL, headers) but before any requests are made. + * + * Use this for: + * - Adding custom middleware (logging, metrics, caching) + * - Modifying request/response processing behavior + * - Adding custom retry logic or error handling + * - Integrating with monitoring or debugging tools + * - Applying organization-specific HTTP client policies + * + * The transformation is applied once during client initialization and affects + * all subsequent API requests made through this client instance. + * + * Leave absent or set to `undefined` if no custom HTTP client behavior is + * needed. + */ + readonly transformClient?: ((client: HttpClient.HttpClient) => HttpClient.HttpClient) | undefined +}) => Effect.Effect = Effect.fnUntraced(function*(options) { + const httpClient = (yield* HttpClient.HttpClient).pipe( + HttpClient.mapRequest((request) => + request.pipe( + HttpClientRequest.prependUrl(options.apiUrl ?? "https://openrouter.ai/api/v1"), + options.apiKey ? HttpClientRequest.bearerToken(options.apiKey) : identity, + options.referrer ? HttpClientRequest.setHeader("HTTP-Referer", options.referrer) : identity, + options.title ? HttpClientRequest.setHeader("X-Title", options.title) : identity, + HttpClientRequest.acceptJson + ) + ), + options.transformClient ?? identity + ) + + const httpClientOk = HttpClient.filterStatusOk(httpClient) + + const client = Generated.make(httpClient, { + transformClient: (client) => + OpenRouterConfig.getOrUndefined.pipe( + Effect.map((config) => config?.transformClient ? config.transformClient(client) : client) + ) + }) + + const streamRequest = ( + request: HttpClientRequest.HttpClientRequest, + schema: Schema.Schema + ): Stream.Stream => { + const decodeEvent = Schema.decode(Schema.parseJson(schema)) + return httpClientOk.execute(request).pipe( + Effect.map((r) => r.stream), + Stream.unwrapScoped, + Stream.decodeText(), + Stream.pipeThroughChannel(Sse.makeChannel()), + Stream.takeWhile((event) => event.data !== "[DONE]"), + Stream.mapEffect((event) => decodeEvent(event.data)), + Stream.catchTags({ + RequestError: (error) => + AiError.HttpRequestError.fromRequestError({ + module: "OpenRouterClient", + method: "streamRequest", + error + }), + ResponseError: (error) => + AiError.HttpResponseError.fromResponseError({ + module: "OpenRouterClient", + method: "streamRequest", + error + }), + ParseError: (error) => + AiError.MalformedOutput.fromParseError({ + module: "OpenRouterClient", + method: "streamRequest", + error + }) + }) + ) + } + + const createChatCompletion: ( + options: typeof Generated.ChatGenerationParams.Encoded + ) => Effect.Effect = Effect.fnUntraced( + function*(options) { + return yield* client.sendChatCompletionRequest(options).pipe( + Effect.catchTag("ChatError", (error) => + new AiError.HttpResponseError({ + module: "OpenRouterClient", + method: "createChatCompletion", + reason: "StatusCode", + request: { + hash: error.request.hash, + headers: error.request.headers, + method: error.request.method, + url: error.request.url, + urlParams: error.request.urlParams + }, + response: { + headers: error.response.headers, + status: error.response.status + } + })), + Effect.catchTags({ + RequestError: (error) => + AiError.HttpRequestError.fromRequestError({ + module: "OpenRouterClient", + method: "createChatCompletion", + error + }), + ResponseError: (error) => + AiError.HttpResponseError.fromResponseError({ + module: "OpenRouterClient", + method: "createChatCompletion", + error + }), + ParseError: (error) => + AiError.MalformedOutput.fromParseError({ + module: "OpenRouterClient", + method: "createChatCompletion", + error + }) + }) + ) + } + ) + + const createChatCompletionStream = ( + options: Omit + ): Stream.Stream => { + const request = HttpClientRequest.post("/chat/completions", { + body: HttpBody.unsafeJson({ + ...options, + stream: true, + stream_options: { include_usage: true } + }) + }) + return streamRequest(request, ChatStreamingResponseChunk) + } + + return OpenRouterClient.of({ + client, + createChatCompletion, + createChatCompletionStream + }) +}) + +/** + * @since 1.0.0 + * @category Layers + */ +export const layer = (options: { + readonly apiKey?: Redacted.Redacted | undefined + readonly apiUrl?: string | undefined + /** + * Optional URL of your site for rankings on `openrouter.ai`. + */ + readonly referrer?: string | undefined + /** + * Optional title of your site for rankings on `openrouter.ai`. + */ + readonly title?: string | undefined + /** + * A function to transform the underlying HTTP client before it's used to send + * API requests. + * + * This transformation function receives the configured HTTP client and returns + * a modified version. It's applied after all standard client configuration + * (authentication, base URL, headers) but before any requests are made. + * + * Use this for: + * - Adding custom middleware (logging, metrics, caching) + * - Modifying request/response processing behavior + * - Adding custom retry logic or error handling + * - Integrating with monitoring or debugging tools + * - Applying organization-specific HTTP client policies + * + * The transformation is applied once during client initialization and affects + * all subsequent API requests made through this client instance. + * + * Leave absent or set to `undefined` if no custom HTTP client behavior is + * needed. + */ + readonly transformClient?: ((client: HttpClient.HttpClient) => HttpClient.HttpClient) | undefined +}): Layer.Layer => Layer.effect(OpenRouterClient, make(options)) + +/** + * @since 1.0.0 + * @category Layers + */ +export const layerConfig = (options: { + readonly apiKey?: Config.Config | undefined + readonly apiUrl?: Config.Config | undefined + /** + * Optional URL of your site for rankings on `openrouter.ai`. + */ + readonly referrer?: Config.Config | undefined + /** + * Optional title of your site for rankings on `openrouter.ai`. + */ + readonly title?: Config.Config | undefined + /** + * A function to transform the underlying HTTP client before it's used to send + * API requests. + * + * This transformation function receives the configured HTTP client and returns + * a modified version. It's applied after all standard client configuration + * (authentication, base URL, headers) but before any requests are made. + * + * Use this for: + * - Adding custom middleware (logging, metrics, caching) + * - Modifying request/response processing behavior + * - Adding custom retry logic or error handling + * - Integrating with monitoring or debugging tools + * - Applying organization-specific HTTP client policies + * + * The transformation is applied once during client initialization and affects + * all subsequent API requests made through this client instance. + * + * Leave absent or set to `undefined` if no custom HTTP client behavior is + * needed. + */ + readonly transformClient?: ((client: HttpClient.HttpClient) => HttpClient.HttpClient) | undefined +}): Layer.Layer => { + const { transformClient, ...configs } = options + return Config.all(configs).pipe( + Effect.flatMap((configs) => make({ ...configs, transformClient })), + Layer.effect(OpenRouterClient) + ) +} + +/** + * @since 1.0.0 + * @category Schemas + */ +export class ChatStreamingMessageToolCall extends Schema.Class( + "@effect/ai-openrouter/ChatStreamingMessageToolCall" +)({ + index: Schema.Number, + id: Schema.optionalWith(Schema.String, { nullable: true }), + type: Schema.optionalWith(Schema.Literal("function"), { nullable: true }), + function: Schema.Struct({ + name: Schema.optionalWith(Schema.String, { nullable: true }), + arguments: Schema.optionalWith(Schema.String, { nullable: true }) + }) +}) {} + +/** + * @since 1.0.0 + * @category Schemas + */ +export class ChatStreamingMessageChunk extends Schema.Class( + "@effect/ai-openrouter/ChatStreamingMessageChunk" +)({ + role: Schema.optionalWith(Schema.Literal("assistant"), { nullable: true }), + content: Schema.optionalWith(Schema.String, { nullable: true }), + reasoning: Schema.optionalWith(Schema.String, { nullable: true }), + reasoning_details: Schema.optionalWith(Schema.Array(Generated.ReasoningDetail), { nullable: true }), + images: Schema.optionalWith(Schema.Array(Generated.ChatMessageContentItemImage), { nullable: true }), + refusal: Schema.optionalWith(Schema.String, { nullable: true }), + tool_calls: Schema.optionalWith(Schema.Array(ChatStreamingMessageToolCall), { nullable: true }), + annotations: Schema.optionalWith(Schema.Array(Generated.AnnotationDetail), { nullable: true }) +}) {} + +/** + * @since 1.0.0 + * @category Schemas + */ +export class ChatStreamingChoice extends Schema.Class( + "@effect/ai-openrouter/ChatStreamingChoice" +)({ + index: Schema.Number, + delta: Schema.optionalWith(ChatStreamingMessageChunk, { nullable: true }), + finish_reason: Schema.optionalWith(Generated.ChatCompletionFinishReason, { nullable: true }), + native_finish_reason: Schema.optionalWith(Schema.String, { nullable: true }), + logprobs: Schema.optionalWith(Generated.ChatMessageTokenLogprobs, { nullable: true }) +}) {} + +/** + * @since 1.0.0 + * @category Schemas + */ +export class ChatStreamingResponseChunk extends Schema.Class( + "@effect/ai-openrouter/ChatStreamingResponseChunk" +)({ + id: Schema.optionalWith(Schema.String, { nullable: true }), + model: Schema.optionalWith( + Schema.TemplateLiteral(Schema.String, Schema.Literal("/"), Schema.String), + { nullable: true } + ), + provider: Schema.optionalWith(Schema.String, { nullable: true }), + created: Schema.DateTimeUtcFromNumber, + choices: Schema.Array(ChatStreamingChoice), + error: Schema.optionalWith(Generated.ChatError.fields.error, { nullable: true }), + system_fingerprint: Schema.optionalWith(Schema.String, { nullable: true }), + usage: Schema.optionalWith(Generated.ChatGenerationTokenUsage, { nullable: true }) +}) {} diff --git a/repos/effect/packages/ai/openrouter/src/OpenRouterConfig.ts b/repos/effect/packages/ai/openrouter/src/OpenRouterConfig.ts new file mode 100644 index 0000000..ed73b3b --- /dev/null +++ b/repos/effect/packages/ai/openrouter/src/OpenRouterConfig.ts @@ -0,0 +1,56 @@ +/** + * @since 1.0.0 + */ +import type { HttpClient } from "@effect/platform/HttpClient" +import * as Context from "effect/Context" +import * as Effect from "effect/Effect" +import { dual } from "effect/Function" + +/** + * @since 1.0.0 + * @category Context + */ +export class OpenRouterConfig extends Context.Tag("@effect/ai-openrouter/OpenRouterConfig")< + OpenRouterConfig, + OpenRouterConfig.Service +>() { + /** + * @since 1.0.0 + */ + static readonly getOrUndefined: Effect.Effect = Effect.map( + Effect.context(), + (context) => context.unsafeMap.get(OpenRouterConfig.key) + ) +} + +/** + * @since 1.0.0 + */ +export declare namespace OpenRouterConfig { + /** + * @since 1.0.0 + * @category Models + */ + export interface Service { + readonly transformClient?: (client: HttpClient) => HttpClient + } +} + +/** + * @since 1.0.0 + * @category Configuration + */ +export const withClientTransform: { + (transform: (client: HttpClient) => HttpClient): (self: Effect.Effect) => Effect.Effect + (self: Effect.Effect, transform: (client: HttpClient) => HttpClient): Effect.Effect +} = dual< + (transform: (client: HttpClient) => HttpClient) => (self: Effect.Effect) => Effect.Effect, + (self: Effect.Effect, transform: (client: HttpClient) => HttpClient) => Effect.Effect +>( + 2, + (self, transformClient) => + Effect.flatMap( + OpenRouterConfig.getOrUndefined, + (config) => Effect.provideService(self, OpenRouterConfig, { ...config, transformClient }) + ) +) diff --git a/repos/effect/packages/ai/openrouter/src/OpenRouterLanguageModel.ts b/repos/effect/packages/ai/openrouter/src/OpenRouterLanguageModel.ts new file mode 100644 index 0000000..bf03db9 --- /dev/null +++ b/repos/effect/packages/ai/openrouter/src/OpenRouterLanguageModel.ts @@ -0,0 +1,1117 @@ +/** + * @since 1.0.0 + */ +import * as AiError from "@effect/ai/AiError" +import * as LanguageModel from "@effect/ai/LanguageModel" +import * as AiModel from "@effect/ai/Model" +import type * as Prompt from "@effect/ai/Prompt" +import type * as Response from "@effect/ai/Response" +import { addGenAIAnnotations } from "@effect/ai/Telemetry" +import * as Tool from "@effect/ai/Tool" +import * as Arr from "effect/Array" +import * as Context from "effect/Context" +import * as DateTime from "effect/DateTime" +import * as Effect from "effect/Effect" +import * as Encoding from "effect/Encoding" +import { dual } from "effect/Function" +import * as Layer from "effect/Layer" +import * as Predicate from "effect/Predicate" +import * as Stream from "effect/Stream" +import type { Span } from "effect/Tracer" +import type { Simplify } from "effect/Types" +import type * as Generated from "./Generated.js" +import * as InternalUtilities from "./internal/utilities.js" +import type { ChatStreamingResponseChunk } from "./OpenRouterClient.js" +import { OpenRouterClient } from "./OpenRouterClient.js" + +// ============================================================================= +// Configuration +// ============================================================================= + +/** + * @since 1.0.0 + * @category Context + */ +export class Config extends Context.Tag( + "@effect/ai-openrouter/OpenRouterLanguageModel/Config" +)() { + /** + * @since 1.0.0 + */ + static readonly getOrUndefined: Effect.Effect = Effect.map( + Effect.context(), + (context) => context.unsafeMap.get(Config.key) + ) +} + +/** + * @since 1.0.0 + */ +export declare namespace Config { + /** + * @since 1.0.0 + * @category Configuration + */ + export interface Service extends + Simplify< + Partial< + Omit< + typeof Generated.ChatGenerationParams.Encoded, + "messages" | "response_format" | "tools" | "tool_choice" | "stream" + > + > + > + {} +} + +// ============================================================================= +// OpenRouter Provider Options / Metadata +// ============================================================================= + +/** + * @since 1.0.0 + * @category Provider Metadata + */ +export type OpenRouterReasoningInfo = { + readonly type: "reasoning" + readonly signature: string | undefined +} | { + readonly type: "encrypted_reasoning" + readonly format: typeof Generated.ReasoningDetailSummary.Type["format"] + readonly redactedData: string +} + +/** + * @since 1.0.0 + * @category Provider Options + */ +declare module "@effect/ai/Prompt" { + export interface SystemMessageOptions extends ProviderOptions { + readonly openrouter?: { + /** + * A breakpoint which marks the end of reusable content eligible for caching. + */ + readonly cacheControl?: typeof Generated.CacheControlEphemeral.Encoded | undefined + } | undefined + } + + export interface UserMessageOptions extends ProviderOptions { + readonly openrouter?: { + /** + * A breakpoint which marks the end of reusable content eligible for caching. + */ + readonly cacheControl?: typeof Generated.CacheControlEphemeral.Encoded | undefined + } | undefined + } + + export interface AssistantMessageOptions extends ProviderOptions { + readonly openrouter?: { + /** + * A breakpoint which marks the end of reusable content eligible for caching. + */ + readonly cacheControl?: typeof Generated.CacheControlEphemeral.Encoded | undefined + } | undefined + } + + export interface ToolMessageOptions extends ProviderOptions { + readonly openrouter?: { + /** + * A breakpoint which marks the end of reusable content eligible for caching. + */ + readonly cacheControl?: typeof Generated.CacheControlEphemeral.Encoded | undefined + } | undefined + } + + export interface TextPartOptions extends ProviderOptions { + readonly openrouter?: { + /** + * A breakpoint which marks the end of reusable content eligible for caching. + */ + readonly cacheControl?: typeof Generated.CacheControlEphemeral.Encoded | undefined + } | undefined + } + + export interface ReasoningPartOptions extends ProviderOptions { + readonly openrouter?: { + /** + * A breakpoint which marks the end of reusable content eligible for caching. + */ + readonly cacheControl?: typeof Generated.CacheControlEphemeral.Encoded | undefined + } | undefined + } + + export interface FilePartOptions extends ProviderOptions { + readonly openrouter?: { + /** + * The name to give to the file. Will be prioritized over the file name + * associated with the file part, if present. + */ + readonly fileName?: string | undefined + /** + * A breakpoint which marks the end of reusable content eligible for caching. + */ + readonly cacheControl?: typeof Generated.CacheControlEphemeral.Encoded | undefined + } | undefined + } + + export interface ToolResultPartOptions extends ProviderOptions { + readonly openrouter?: { + /** + * A breakpoint which marks the end of reusable content eligible for caching. + */ + readonly cacheControl?: typeof Generated.CacheControlEphemeral.Encoded | undefined + } | undefined + } +} + +/** + * @since 1.0.0 + * @category Provider Metadata + */ +declare module "@effect/ai/Response" { + export interface ReasoningPartMetadata extends ProviderMetadata { + readonly openrouter?: OpenRouterReasoningInfo | undefined + } + + export interface ReasoningStartPartMetadata extends ProviderMetadata { + readonly openrouter?: OpenRouterReasoningInfo | undefined + } + + export interface ReasoningDeltaPartMetadata extends ProviderMetadata { + readonly openrouter?: OpenRouterReasoningInfo | undefined + } + + export interface UrlSourcePartMetadata extends ProviderMetadata { + readonly openrouter?: { + readonly content?: string | undefined + } | undefined + } + + export interface FinishPartMetadata extends ProviderMetadata { + readonly openrouter?: { + /** + * The provider used to generate the response. + */ + readonly provider?: string | undefined + /** + * Additional usage information. + */ + readonly usage?: { + /** + * The total cost of generating the response. + */ + readonly cost?: number | undefined + /** + * Additional details about cost. + */ + readonly costDetails?: { + readonly upstream_inference_cost?: number | undefined + } | undefined + /** + * Additional details about prompt token usage. + */ + readonly promptTokensDetails?: { + readonly audio_tokens?: number | undefined + readonly cached_tokens?: number | undefined + } + /** + * Additional details about completion token usage. + */ + readonly completionTokensDetails?: { + readonly reasoning_tokens?: number | undefined + readonly audio_tokens?: number | undefined + readonly accepted_prediction_tokens?: number | undefined + readonly rejected_prediction_tokens?: number | undefined + } | undefined + } | undefined + } | undefined + } +} + +// ============================================================================= +// OpenRouter Language Model +// ============================================================================= + +/** + * @since 1.0.0 + * @category Ai Models + */ +export const model = ( + model: string, + config?: Omit +): AiModel.Model<"openrouter", LanguageModel.LanguageModel, OpenRouterClient> => + AiModel.make("openrouter", layer({ model, config })) + +/** + * @since 1.0.0 + * @category Constructors + */ +export const make = Effect.fnUntraced(function*(options: { + readonly model: string + readonly config?: Omit +}) { + const client = yield* OpenRouterClient + + const makeRequest = Effect.fnUntraced( + function*(providerOptions: LanguageModel.ProviderOptions) { + const context = yield* Effect.context() + const config = { model: options.model, ...options.config, ...context.unsafeMap.get(Config.key) } + const messages = yield* prepareMessages(providerOptions) + const { toolChoice, tools } = yield* prepareTools(providerOptions) + const responseFormat = providerOptions.responseFormat + const request: typeof Generated.ChatGenerationParams.Encoded = { + ...config, + messages, + tools, + tool_choice: toolChoice, + response_format: responseFormat.type === "text" ? undefined : { + type: "json_schema", + json_schema: { + name: responseFormat.objectName, + description: Tool.getDescriptionFromSchemaAst(responseFormat.schema.ast) ?? "Respond with a JSON object", + schema: Tool.getJsonSchemaFromSchemaAst(responseFormat.schema.ast), + strict: true + } + } + } + return request + } + ) + + return yield* LanguageModel.make({ + generateText: Effect.fnUntraced( + function*(options) { + const request = yield* makeRequest(options) + annotateRequest(options.span, request) + const rawResponse = yield* client.createChatCompletion(request) + annotateResponse(options.span, rawResponse) + return yield* makeResponse(rawResponse) + } + ), + streamText: Effect.fnUntraced( + function*(options) { + const request = yield* makeRequest(options) + annotateRequest(options.span, request) + return client.createChatCompletionStream(request) + }, + (effect, options) => + effect.pipe( + Effect.flatMap((stream) => makeStreamResponse(stream)), + Stream.unwrap, + Stream.map((response) => { + annotateStreamResponse(options.span, response) + return response + }) + ) + ) + }) +}) + +/** + * @since 1.0.0 + * @category Layers + */ +export const layer = (options: { + readonly model: string + readonly config?: Omit +}): Layer.Layer => + Layer.effect(LanguageModel.LanguageModel, make({ model: options.model, config: options.config })) + +/** + * @since 1.0.0 + * @category Configuration + */ +export const withConfigOverride: { + (config: Config.Service): (self: Effect.Effect) => Effect.Effect + (self: Effect.Effect, config: Config.Service): Effect.Effect +} = dual< + (config: Config.Service) => (self: Effect.Effect) => Effect.Effect, + (self: Effect.Effect, config: Config.Service) => Effect.Effect +>(2, (self, overrides) => + Effect.flatMap( + Config.getOrUndefined, + (config) => Effect.provideService(self, Config, { ...config, ...overrides }) + )) + +// ============================================================================= +// Prompt Conversion +// ============================================================================= + +const prepareMessages: (options: LanguageModel.ProviderOptions) => Effect.Effect< + ReadonlyArray, + AiError.AiError +> = Effect.fnUntraced(function*(options) { + const messages: Array = [] + + for (const message of options.prompt.content) { + switch (message.role) { + case "system": { + messages.push({ + role: "system", + content: message.content, + cache_control: getCacheControl(message) + }) + break + } + + case "user": { + if (message.content.length === 1 && message.content[0].type === "text") { + const part = message.content[0] + const cacheControl = getCacheControl(message) ?? getCacheControl(part) + messages.push({ + role: "user", + content: Predicate.isNotUndefined(cacheControl) + ? [{ type: "text", text: part.text, cache_control: cacheControl }] + : part.text + }) + } else { + const content: Array = [] + const messageCacheControl = getCacheControl(message) + for (const part of message.content) { + const partCacheControl = getCacheControl(part) + const cacheControl = partCacheControl ?? messageCacheControl + switch (part.type) { + case "text": { + content.push({ + type: "text", + text: part.text, + cache_control: cacheControl + }) + break + } + case "file": { + if (part.mediaType.startsWith("image/")) { + const mediaType = part.mediaType === "image/*" ? "image/jpeg" : part.mediaType + content.push({ + type: "image_url", + image_url: { + url: part.data instanceof URL + ? part.data.toString() + : part.data instanceof Uint8Array + ? `data:${mediaType};base64,${Encoding.encodeBase64(part.data)}` + : part.data + }, + cache_control: cacheControl + }) + } else { + const options = part.options.openrouter + const fileName = options?.fileName ?? part.fileName ?? "" + content.push({ + type: "file", + file: { + filename: fileName, + file_data: part.data instanceof URL + ? part.data.toString() + : part.data instanceof Uint8Array + ? `data:${part.mediaType};base64,${Encoding.encodeBase64(part.data)}` + : part.data + }, + cache_control: part.data instanceof URL ? cacheControl : undefined + }) + } + break + } + } + } + messages.push({ + role: "user", + content + }) + } + break + } + + case "assistant": { + let text = "" + let reasoning = "" + const reasoningDetails: Array = [] + const toolCalls: Array = [] + const cacheControl = getCacheControl(message) + for (const part of message.content) { + switch (part.type) { + case "text": { + text += part.text + break + } + case "reasoning": { + reasoning += part.text + reasoningDetails.push({ + type: "reasoning.text", + text: part.text + }) + break + } + case "tool-call": { + toolCalls.push({ + id: part.id, + type: "function", + function: { + name: part.name, + arguments: JSON.stringify(part.params) + } + }) + break + } + } + } + messages.push({ + role: "assistant", + content: text, + tool_calls: toolCalls.length > 0 ? toolCalls : undefined, + reasoning: reasoning.length > 0 ? reasoning : undefined, + reasoning_details: reasoningDetails.length > 0 ? reasoningDetails : undefined, + cache_control: cacheControl + }) + break + } + + case "tool": { + const cacheControl = getCacheControl(message) + for (const part of message.content) { + messages.push({ + role: "tool", + tool_call_id: part.id, + content: JSON.stringify(part.result), + cache_control: cacheControl + }) + } + break + } + } + } + + return messages +}) + +// ============================================================================= +// Tool Conversion +// ============================================================================= + +const prepareTools: (options: LanguageModel.ProviderOptions) => Effect.Effect<{ + readonly tools: ReadonlyArray | undefined + readonly toolChoice: typeof Generated.ToolChoiceOption.Encoded | undefined +}, AiError.AiError> = Effect.fnUntraced( + function*(options: LanguageModel.ProviderOptions) { + if (options.tools.length === 0) { + return { tools: undefined, toolChoice: undefined } + } + + const hasProviderDefinedTools = options.tools.some((tool) => Tool.isProviderDefined(tool)) + if (hasProviderDefinedTools) { + return yield* new AiError.MalformedInput({ + module: "OpenRouterLanguageModel", + method: "prepareTools", + description: "Provider-defined tools are unsupported by the OpenRouter " + + "provider integration at this time" + }) + } + + let tools: Array = [] + let toolChoice: typeof Generated.ToolChoiceOption.Encoded | undefined = undefined + + for (const tool of options.tools) { + tools.push({ + type: "function", + function: { + name: tool.name, + description: Tool.getDescription(tool as any), + parameters: Tool.getJsonSchema(tool as any) as any, + strict: true + } + }) + } + + if (options.toolChoice === "none") { + toolChoice = "none" + } else if (options.toolChoice === "auto") { + toolChoice = "auto" + } else if (options.toolChoice === "required") { + toolChoice = "required" + } else if ("tool" in options.toolChoice) { + toolChoice = { type: "function", function: { name: options.toolChoice.tool } } + } else { + const allowedTools = new Set(options.toolChoice.oneOf) + tools = tools.filter((tool) => allowedTools.has(tool.function.name)) + toolChoice = options.toolChoice.mode === "auto" ? "auto" : "required" + } + + return { tools, toolChoice } + } +) + +// ============================================================================= +// Response Conversion +// ============================================================================= + +const makeResponse: (response: Generated.ChatResponse) => Effect.Effect< + Array, + AiError.AiError +> = Effect.fnUntraced( + function*(response) { + const choice = response.choices[0] + + if (Predicate.isUndefined(choice)) { + return yield* new AiError.MalformedOutput({ + module: "OpenRouterLanguageModel", + method: "makeResponse", + description: "Received response with no valid choices" + }) + } + + const parts: Array = [] + const message = choice.message + + const createdAt = new Date(response.created * 1000) + parts.push({ + type: "response-metadata", + id: response.id, + modelId: response.model, + timestamp: DateTime.formatIso(DateTime.unsafeFromDate(createdAt)) + }) + + if (Predicate.isNotNullable(message.reasoning) && message.reasoning.length > 0) { + parts.push({ + type: "reasoning", + text: message.reasoning + }) + } + + if (Predicate.isNotNullable(message.reasoning_details) && message.reasoning_details.length > 0) { + for (const detail of message.reasoning_details) { + switch (detail.type) { + case "reasoning.summary": { + if (Predicate.isNotUndefined(detail.summary) && detail.summary.length > 0) { + parts.push({ + type: "reasoning", + text: detail.summary + }) + } + break + } + case "reasoning.encrypted": { + if (Predicate.isNotUndefined(detail.data) && detail.data.length > 0) { + parts.push({ + type: "reasoning", + text: "", + metadata: { + openrouter: { + type: "encrypted_reasoning", + format: detail.format, + redactedData: detail.data + } + } + }) + } + break + } + case "reasoning.text": { + if (Predicate.isNotUndefined(detail.text) && detail.text.length > 0) { + parts.push({ + type: "reasoning", + text: detail.text, + metadata: { + openrouter: { + type: "reasoning", + signature: detail.signature + } + } + }) + } + break + } + } + } + } + + if (Predicate.isNotNullable(message.content) && message.content.length > 0) { + parts.push({ + type: "text", + text: message.content as string + }) + } + + if (Predicate.isNotNullable(message.tool_calls)) { + for (const toolCall of message.tool_calls) { + const toolName = toolCall.function.name + const toolParams = toolCall.function.arguments + const params = yield* Effect.try({ + try: () => Tool.unsafeSecureJsonParse(toolParams), + catch: (cause) => + new AiError.MalformedOutput({ + module: "OpenRouterLanguageModel", + method: "makeResponse", + description: "Failed to securely parse tool call parameters " + + `for tool '${toolName}':\nParameters: ${toolParams}`, + cause + }) + }) + parts.push({ + type: "tool-call", + id: toolCall.id, + name: toolName, + params + }) + } + } + + if (Predicate.isNotNullable(message.annotations)) { + for (const annotation of message.annotations) { + if (annotation.type === "url_citation") { + parts.push({ + type: "source", + sourceType: "url", + id: annotation.url_citation.url, + url: annotation.url_citation.url, + title: annotation.url_citation.title, + metadata: { + openrouter: { + content: annotation.url_citation.content + } + } + }) + } + } + } + + if (Predicate.isNotNullable(message.images)) { + for (const image of message.images) { + parts.push({ + type: "file", + mediaType: getMediaType(image.image_url.url) ?? "image/jpeg", + data: getBase64FromDataUrl(image.image_url.url) + }) + } + } + + parts.push({ + type: "finish", + reason: InternalUtilities.resolveFinishReason(choice.finish_reason), + usage: { + inputTokens: response.usage?.prompt_tokens, + outputTokens: response.usage?.completion_tokens, + totalTokens: response.usage?.total_tokens, + reasoningTokens: response.usage?.completion_tokens_details?.reasoning_tokens, + cachedInputTokens: response.usage?.prompt_tokens_details?.cached_tokens + }, + metadata: { + openrouter: { + provider: response.provider, + usage: { + cost: response.usage?.cost, + promptTokensDetails: response.usage?.prompt_tokens_details, + completionTokensDetails: response.usage?.completion_tokens_details, + costDetails: response.usage?.cost_details + } + } + } + }) + + return parts + } +) + +const makeStreamResponse: (stream: Stream.Stream) => Effect.Effect< + Stream.Stream +> = Effect.fnUntraced( + function*(stream) { + let idCounter = 0 + let activeTextId: string | undefined = undefined + let activeReasoningId: string | undefined = undefined + let finishReason: Response.FinishReason = "unknown" + let responseMetadataEmitted = false + + const activeToolCalls: Record = {} + + return stream.pipe( + Stream.mapEffect(Effect.fnUntraced(function*(event) { + const parts: Array = [] + + if ("error" in event) { + parts.push({ + type: "error", + error: event.error + }) + return parts + } + + // Response Metadata + + if (Predicate.isNotUndefined(event.id) && !responseMetadataEmitted) { + parts.push({ + type: "response-metadata", + id: event.id, + modelId: event.model, + timestamp: DateTime.formatIso(yield* DateTime.now) + }) + responseMetadataEmitted = true + } + + const choice = event.choices[0] + + if (Predicate.isUndefined(choice) && Predicate.isUndefined(event.usage)) { + return yield* new AiError.MalformedOutput({ + module: "OpenRouterLanguageModel", + method: "makeResponse", + description: "Received response with no valid choices" + }) + } + + const delta = choice?.delta + + // Reasoning Parts + + const emitReasoningPart = (delta: string, metadata: OpenRouterReasoningInfo | undefined = undefined) => { + // End in-progress text part if present before starting reasoning + if (Predicate.isNotUndefined(activeTextId)) { + parts.push({ + type: "text-end", + id: activeTextId + }) + activeTextId = undefined + } + // Start a new reasoning part if necessary + if (Predicate.isUndefined(activeReasoningId)) { + activeReasoningId = (idCounter++).toString() + parts.push({ + type: "reasoning-start", + id: activeReasoningId, + metadata: { openrouter: metadata } + }) + } + // Emit the reasoning delta + parts.push({ + type: "reasoning-delta", + id: activeReasoningId, + delta, + metadata: { openrouter: metadata } + }) + } + + if (Predicate.isNotNullable(delta?.reasoning_details) && delta.reasoning_details.length > 0) { + for (const detail of delta.reasoning_details) { + switch (detail.type) { + case "reasoning.summary": { + if (Predicate.isNotUndefined(detail.summary) && detail.summary.length > 0) { + emitReasoningPart(detail.summary) + } + break + } + case "reasoning.encrypted": { + if (Predicate.isNotUndefined(detail.data) && detail.data.length > 0) { + emitReasoningPart("", { + type: "encrypted_reasoning", + format: detail.format, + redactedData: detail.data + }) + } + break + } + case "reasoning.text": { + if (Predicate.isNotUndefined(detail.text) && detail.text.length > 0) { + emitReasoningPart(detail.text, { + type: "reasoning", + signature: detail.signature + }) + } + break + } + } + } + } else if (Predicate.isNotNullable(delta?.reasoning) && delta.reasoning.length > 0) { + emitReasoningPart(delta.reasoning) + } + + // Text Parts + + if (Predicate.isNotNullable(delta?.content) && delta.content.length > 0) { + // End in-progress reasoning part if present before starting text + if (Predicate.isNotUndefined(activeReasoningId)) { + parts.push({ + type: "reasoning-end", + id: activeReasoningId + }) + activeReasoningId = undefined + } + // Start a new text part if necessary + if (Predicate.isUndefined(activeTextId)) { + activeTextId = (idCounter++).toString() + parts.push({ + type: "text-start", + id: activeTextId + }) + } + // Emit the text delta + parts.push({ + type: "text-delta", + id: activeTextId, + delta: delta.content + }) + } + + // Source Parts + + if (Predicate.isNotNullable(delta?.annotations)) { + for (const annotation of delta.annotations) { + if (annotation.type === "url_citation") { + parts.push({ + type: "source", + sourceType: "url", + id: annotation.url_citation.url, + url: annotation.url_citation.url, + title: annotation.url_citation.title, + metadata: { + openrouter: { + content: annotation.url_citation.content + } + } + }) + } + } + } + + // Tool Call Parts + + if (Predicate.isNotNullable(delta?.tool_calls) && delta.tool_calls.length > 0) { + for (const toolCall of delta.tool_calls) { + // Get the active tool call, if present + let activeToolCall = activeToolCalls[toolCall.index] + + // If no active tool call was found, start a new active tool call + if (Predicate.isUndefined(activeToolCall)) { + // The tool call id and function name always come back with the + // first tool call delta + activeToolCall = { + index: toolCall.index, + id: toolCall.id!, + name: toolCall.function.name!, + params: toolCall.function.arguments ?? "" + } + + activeToolCalls[toolCall.index] = activeToolCall + + parts.push({ + type: "tool-params-start", + id: activeToolCall.id, + name: activeToolCall.name + }) + + // Emit a tool call delta part if parameters were also sent + if (activeToolCall.params.length > 0) { + parts.push({ + type: "tool-params-delta", + id: activeToolCall.id, + delta: activeToolCall.params + }) + } + } else { + // If an active tool call was found, update and emit the delta for + // the tool call's parameters + activeToolCall.params += toolCall.function.arguments + parts.push({ + type: "tool-params-delta", + id: activeToolCall.id, + delta: activeToolCall.params + }) + } + + // Check if the tool call is complete + try { + const params = Tool.unsafeSecureJsonParse(activeToolCall.params) + parts.push({ + type: "tool-params-end", + id: activeToolCall.id + }) + parts.push({ + type: "tool-call", + id: activeToolCall.id, + name: activeToolCall.name, + params + }) + delete activeToolCalls[toolCall.index] + } catch { + // Tool call incomplete, continue parsing + continue + } + } + } + + // File Parts + + if (Predicate.isNotNullable(delta?.images)) { + for (const image of delta.images) { + parts.push({ + type: "file", + mediaType: getMediaType(image.image_url.url) ?? "image/jpeg", + data: getBase64FromDataUrl(image.image_url.url) + }) + } + } + + // Finish Parts + + if (Predicate.isNotNullable(choice?.finish_reason)) { + finishReason = InternalUtilities.resolveFinishReason(choice.finish_reason) + } + + // Usage is only emitted by the last part of the stream, so we need to + // handle flushing any remaining text / reasoning / tool calls + if (Predicate.isNotUndefined(event.usage)) { + // Complete any remaining tool calls if the finish reason is tool-calls + if (finishReason === "tool-calls") { + for (const toolCall of Object.values(activeToolCalls)) { + // Coerce invalid tool call parameters to an empty object + const params = yield* Effect.try(() => Tool.unsafeSecureJsonParse(toolCall.params)).pipe( + Effect.catchAll(() => Effect.succeed({})) + ) + parts.push({ + type: "tool-params-end", + id: toolCall.id + }) + parts.push({ + type: "tool-call", + id: toolCall.id, + name: toolCall.name, + params + }) + delete activeToolCalls[toolCall.index] + } + } + + // Flush remaining reasoning parts + if (Predicate.isNotUndefined(activeReasoningId)) { + parts.push({ + type: "reasoning-end", + id: activeReasoningId + }) + activeReasoningId = undefined + } + + // Flush remaining text parts + if (Predicate.isNotUndefined(activeTextId)) { + parts.push({ + type: "text-end", + id: activeTextId + }) + activeTextId = undefined + } + + parts.push({ + type: "finish", + reason: finishReason, + usage: { + inputTokens: event.usage?.prompt_tokens, + outputTokens: event.usage?.completion_tokens, + totalTokens: event.usage?.total_tokens, + reasoningTokens: event.usage?.completion_tokens_details?.reasoning_tokens, + cachedInputTokens: event.usage?.prompt_tokens_details?.cached_tokens + }, + metadata: { + openrouter: { + provider: event.provider, + usage: { + cost: event.usage?.cost, + promptTokensDetails: event.usage?.prompt_tokens_details, + completionTokensDetails: event.usage?.completion_tokens_details, + costDetails: event.usage?.cost_details + } + } + } + }) + } + + return parts + })), + Stream.flattenIterables + ) + } +) + +// ============================================================================= +// Telemetry +// ============================================================================= + +const annotateRequest = ( + span: Span, + request: typeof Generated.ChatGenerationParams.Encoded +): void => { + addGenAIAnnotations(span, { + system: "openrouter", + operation: { name: "chat" }, + request: { + model: request.model, + temperature: request.temperature, + topP: request.top_p, + maxTokens: request.max_tokens, + stopSequences: Arr.ensure(request.stop).filter( + Predicate.isNotNullable + ) + } + }) +} + +const annotateResponse = (span: Span, response: Generated.ChatResponse): void => { + addGenAIAnnotations(span, { + response: { + id: response.id, + model: response.model, + finishReasons: response.choices.map((choice) => choice.finish_reason).filter(Predicate.isNotNullable) + }, + usage: { + inputTokens: response.usage?.prompt_tokens, + outputTokens: response.usage?.completion_tokens + } + }) +} + +const annotateStreamResponse = (span: Span, part: Response.StreamPartEncoded) => { + if (part.type === "response-metadata") { + addGenAIAnnotations(span, { + response: { + id: part.id, + model: part.modelId + } + }) + } + if (part.type === "finish") { + addGenAIAnnotations(span, { + response: { + finishReasons: [part.reason] + }, + usage: { + inputTokens: part.usage.inputTokens, + outputTokens: part.usage.outputTokens + } + }) + } +} + +// ============================================================================= +// Utilities +// ============================================================================= + +const getCacheControl = ( + part: + | Prompt.SystemMessage + | Prompt.UserMessage + | Prompt.AssistantMessage + | Prompt.ToolMessage + | Prompt.TextPart + | Prompt.ReasoningPart + | Prompt.FilePart + | Prompt.ToolResultPart +): typeof Generated.CacheControlEphemeral.Encoded | undefined => part.options.openrouter?.cacheControl + +const getMediaType = (dataUrl: string): string | undefined => { + const match = dataUrl.match(/^data:([^;]+)/) + return match ? match[1] : undefined +} + +const getBase64FromDataUrl = (dataUrl: string): string => { + const match = dataUrl.match(/^data:[^;]*;base64,(.+)$/) + return match ? match[1]! : dataUrl +} diff --git a/repos/effect/packages/ai/openrouter/src/index.ts b/repos/effect/packages/ai/openrouter/src/index.ts new file mode 100644 index 0000000..8d2a1b0 --- /dev/null +++ b/repos/effect/packages/ai/openrouter/src/index.ts @@ -0,0 +1,19 @@ +/** + * @since 1.0.0 + */ +export * as Generated from "./Generated.js" + +/** + * @since 1.0.0 + */ +export * as OpenRouterClient from "./OpenRouterClient.js" + +/** + * @since 1.0.0 + */ +export * as OpenRouterConfig from "./OpenRouterConfig.js" + +/** + * @since 1.0.0 + */ +export * as OpenRouterLanguageModel from "./OpenRouterLanguageModel.js" diff --git a/repos/effect/packages/ai/openrouter/src/internal/utilities.ts b/repos/effect/packages/ai/openrouter/src/internal/utilities.ts new file mode 100644 index 0000000..1b594e7 --- /dev/null +++ b/repos/effect/packages/ai/openrouter/src/internal/utilities.ts @@ -0,0 +1,26 @@ +import type * as Response from "@effect/ai/Response" +import * as Predicate from "effect/Predicate" +import type * as Generated from "../Generated.js" + +const finishReasonMap: Record = { + content_filter: "content-filter", + error: "error", + function_call: "tool-calls", + tool_calls: "tool-calls", + length: "length", + stop: "stop" +} + +/** @internal */ +export const resolveFinishReason = ( + finishReason: typeof Generated.ChatCompletionFinishReason.Type | null +): Response.FinishReason => { + if (Predicate.isNull(finishReason)) { + return "unknown" + } + const reason = finishReasonMap[finishReason] + if (Predicate.isUndefined(reason)) { + return "unknown" + } + return reason +} diff --git a/repos/effect/packages/ai/openrouter/tsconfig.build.json b/repos/effect/packages/ai/openrouter/tsconfig.build.json new file mode 100644 index 0000000..4021ab2 --- /dev/null +++ b/repos/effect/packages/ai/openrouter/tsconfig.build.json @@ -0,0 +1,16 @@ +{ + "extends": "./tsconfig.src.json", + "references": [ + { "path": "../ai/tsconfig.build.json" }, + { "path": "../../effect/tsconfig.build.json" }, + { "path": "../../experimental/tsconfig.build.json" }, + { "path": "../../platform/tsconfig.build.json" } + ], + "compilerOptions": { + "tsBuildInfoFile": ".tsbuildinfo/build.tsbuildinfo", + "outDir": "build/esm", + "declarationDir": "build/dts", + "stripInternal": true, + "exactOptionalPropertyTypes": false + } +} diff --git a/repos/effect/packages/ai/openrouter/tsconfig.json b/repos/effect/packages/ai/openrouter/tsconfig.json new file mode 100644 index 0000000..f446496 --- /dev/null +++ b/repos/effect/packages/ai/openrouter/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../../tsconfig.base.json", + "include": [], + "references": [ + { "path": "tsconfig.src.json" }, + { "path": "tsconfig.test.json" } + ] +} diff --git a/repos/effect/packages/ai/openrouter/tsconfig.src.json b/repos/effect/packages/ai/openrouter/tsconfig.src.json new file mode 100644 index 0000000..01ab4fc --- /dev/null +++ b/repos/effect/packages/ai/openrouter/tsconfig.src.json @@ -0,0 +1,16 @@ +{ + "extends": "../../../tsconfig.base.json", + "include": ["src"], + "references": [ + { "path": "../ai/tsconfig.src.json" }, + { "path": "../../effect/tsconfig.src.json" }, + { "path": "../../experimental/tsconfig.src.json" }, + { "path": "../../platform/tsconfig.src.json" } + ], + "compilerOptions": { + "tsBuildInfoFile": ".tsbuildinfo/src.tsbuildinfo", + "rootDir": "src", + "outDir": "build/src", + "exactOptionalPropertyTypes": false + } +} diff --git a/repos/effect/packages/ai/openrouter/tsconfig.test.json b/repos/effect/packages/ai/openrouter/tsconfig.test.json new file mode 100644 index 0000000..95f3ae8 --- /dev/null +++ b/repos/effect/packages/ai/openrouter/tsconfig.test.json @@ -0,0 +1,14 @@ +{ + "extends": "../../../tsconfig.base.json", + "include": ["test"], + "references": [ + { "path": "tsconfig.src.json" }, + { "path": "../../vitest/tsconfig.src.json" } + ], + "compilerOptions": { + "tsBuildInfoFile": ".tsbuildinfo/test.tsbuildinfo", + "rootDir": "test", + "noEmit": true, + "exactOptionalPropertyTypes": false + } +} diff --git a/repos/effect/packages/ai/openrouter/vitest.config.ts b/repos/effect/packages/ai/openrouter/vitest.config.ts new file mode 100644 index 0000000..bf29895 --- /dev/null +++ b/repos/effect/packages/ai/openrouter/vitest.config.ts @@ -0,0 +1,6 @@ +import { mergeConfig, type ViteUserConfig } from "vitest/config" +import shared from "../../../vitest.shared.js" + +const config: ViteUserConfig = {} + +export default mergeConfig(shared, config) diff --git a/repos/effect/packages/cli/CHANGELOG.md b/repos/effect/packages/cli/CHANGELOG.md new file mode 100644 index 0000000..497d206 --- /dev/null +++ b/repos/effect/packages/cli/CHANGELOG.md @@ -0,0 +1,4122 @@ +# @effect/cli + +## 0.75.1 + +### Patch Changes + +- [#6144](https://github.com/Effect-TS/effect/pull/6144) [`ec5c505`](https://github.com/Effect-TS/effect/commit/ec5c50507b1a2f5ad712c148a7da0fadb8cb9f52) Thanks @LikiosSedo! - Fix `--log-level=value` equals syntax incorrectly swallowing the next argument. Only skip the next arg when the previous arg is exactly `--log-level` (space-separated form). + +- Updated dependencies [[`f99048e`](https://github.com/Effect-TS/effect/commit/f99048e9f4b89ce1afe31e1827dee5d751ddaa5b)]: + - effect@3.21.1 + +## 0.75.0 + +### Patch Changes + +- Updated dependencies [[`f7bb09b`](https://github.com/Effect-TS/effect/commit/f7bb09b022f195d1f2b3c23d49e74b011ec5d109), [`bd7552a`](https://github.com/Effect-TS/effect/commit/bd7552a19cc0ed575507ac6cc0879a57e24ebd31), [`ad1a7eb`](https://github.com/Effect-TS/effect/commit/ad1a7eb7f6bebaf91c80be2443ac0439226d0098), [`0d32048`](https://github.com/Effect-TS/effect/commit/0d32048f9836e2b23a6ba3ec5f43f0a000bb92fb), [`0d32048`](https://github.com/Effect-TS/effect/commit/0d32048f9836e2b23a6ba3ec5f43f0a000bb92fb)]: + - effect@3.21.0 + - @effect/platform@0.96.0 + - @effect/printer@0.49.0 + - @effect/printer-ansi@0.49.0 + +## 0.74.0 + +### Patch Changes + +- Updated dependencies [[`fc82e81`](https://github.com/Effect-TS/effect/commit/fc82e81448bd9136a37580139ce46a2c61b11b54), [`82996bc`](https://github.com/Effect-TS/effect/commit/82996bce8debffcb44feb98bb862cf2662bd56b7), [`4d97a61`](https://github.com/Effect-TS/effect/commit/4d97a61a15b9dd6a0eece65b8f0c035e16d42ada), [`f6b0960`](https://github.com/Effect-TS/effect/commit/f6b0960bf3184109920dfed16ee7dfd7d67bc0f2), [`8798a84`](https://github.com/Effect-TS/effect/commit/8798a843218e6c0c0d3a8eee83360880e370b4da)]: + - effect@3.20.0 + - @effect/platform@0.95.0 + - @effect/printer@0.48.0 + - @effect/printer-ansi@0.48.0 + +## 0.73.2 + +### Patch Changes + +- [#6022](https://github.com/Effect-TS/effect/pull/6022) [`5df4da1`](https://github.com/Effect-TS/effect/commit/5df4da10de444f379a166f4b28721e75100bb838) Thanks @m9tdev! - Fixed `Prompt.text` rendering duplicate lines when input text wraps to a new terminal line. + +- Updated dependencies [[`0023c19`](https://github.com/Effect-TS/effect/commit/0023c19c63c402c050d496817ba92aceea7f25b7), [`e71889f`](https://github.com/Effect-TS/effect/commit/e71889f35b081d13b7da2c04d2f81d6933056b49), [`9a96b87`](https://github.com/Effect-TS/effect/commit/9a96b87a33a75ebc277c585e60758ab4409c0d9e)]: + - @effect/platform@0.94.3 + - effect@3.19.16 + +## 0.73.1 + +### Patch Changes + +- [#5983](https://github.com/Effect-TS/effect/pull/5983) [`0d1a44f`](https://github.com/Effect-TS/effect/commit/0d1a44fa142c0da25fe36a1ac35675f666944803) Thanks @cevr! - Allow options to appear after positional arguments + + Previously, `@effect/cli` required all options to appear before positional arguments. For example, `cmd --force staging` worked but `cmd staging --force` failed with "Received unknown argument". + + This change updates the option parsing logic to scan through all arguments to find options, regardless of their position relative to positional arguments. This aligns with the behavior of most CLI tools (git, npm, docker, etc.) which allow options anywhere in the command. + + **Before:** + + ```bash + myapp deploy --force staging # worked + myapp deploy staging --force # failed: "Received unknown argument: '--force'" + ``` + + **After:** + + ```bash + myapp deploy --force staging # works + myapp deploy staging --force # works + ``` + +- Updated dependencies [[`7e925ea`](https://github.com/Effect-TS/effect/commit/7e925eae4a9db556bcbf7e8b6a762ccf8588aa3b), [`118e7a4`](https://github.com/Effect-TS/effect/commit/118e7a4af5b86f6d707a40d3b03157b6bf5827e7), [`d7e75d6`](https://github.com/Effect-TS/effect/commit/d7e75d6d15294bbcd7ac49a0e9005848379ea86f), [`4860d1e`](https://github.com/Effect-TS/effect/commit/4860d1e09b436061ea4aeca07605a669793560fc)]: + - effect@3.19.15 + - @effect/platform@0.94.2 + +## 0.73.0 + +### Patch Changes + +- [#5853](https://github.com/Effect-TS/effect/pull/5853) [`1b23741`](https://github.com/Effect-TS/effect/commit/1b23741a3d43acfa99ffa385b9c496d411704d0c) Thanks @Masty88! - handle executable paths with spaces in CLI arguments + +- Updated dependencies [[`77eeb86`](https://github.com/Effect-TS/effect/commit/77eeb86ddf208e51ec25932af83d52d3b4700371), [`ff7053f`](https://github.com/Effect-TS/effect/commit/ff7053f6d8508567b6145239f97aacc5773b0c53), [`287c32c`](https://github.com/Effect-TS/effect/commit/287c32c9f10da8e96f2b9ef8424316189d9ad4b3)]: + - effect@3.19.13 + - @effect/platform@0.94.0 + +## 0.72.1 + +### Patch Changes + +- [#5677](https://github.com/Effect-TS/effect/pull/5677) [`f8273c9`](https://github.com/Effect-TS/effect/commit/f8273c9de13ffa6d96c08686f28951177b59f430) Thanks @nemmtor! - fix log level cli arg infinite loop + +- Updated dependencies [[`7d28a90`](https://github.com/Effect-TS/effect/commit/7d28a908f965854cff386a19515141aea5b39eb7)]: + - effect@3.19.3 + +## 0.72.0 + +### Patch Changes + +- Updated dependencies [[`3c15d5f`](https://github.com/Effect-TS/effect/commit/3c15d5f99fb8d8470a00c5a33d9ba3cac89dfe4c), [`3863fa8`](https://github.com/Effect-TS/effect/commit/3863fa89f61e63e5529fd961e37333bddf7db64a), [`2a03c76`](https://github.com/Effect-TS/effect/commit/2a03c76c2781ca7e9e228e838eab2eb0d0795b1d), [`24a1685`](https://github.com/Effect-TS/effect/commit/24a1685c70a9ed157468650f95a5c3da3f2c2433)]: + - effect@3.19.0 + - @effect/platform@0.93.0 + - @effect/printer@0.47.0 + - @effect/printer-ansi@0.47.0 + +## 0.71.0 + +### Patch Changes + +- Updated dependencies [[`1c6ab74`](https://github.com/Effect-TS/effect/commit/1c6ab74b314b2b6df8bb1b1a0cb9527ceda0e3fa), [`70fe803`](https://github.com/Effect-TS/effect/commit/70fe803469db3355ffbf8359b52c351f1c2dc137), [`c296e32`](https://github.com/Effect-TS/effect/commit/c296e32554143b84ae8987046984e1cf1852417c), [`a098ddf`](https://github.com/Effect-TS/effect/commit/a098ddfc551f5aa0a7c36f9b4928372a64d4d9f2)]: + - effect@3.18.0 + - @effect/platform@0.92.0 + - @effect/printer@0.46.0 + - @effect/printer-ansi@0.46.0 + +## 0.70.0 + +### Patch Changes + +- Updated dependencies [[`d4d86a8`](https://github.com/Effect-TS/effect/commit/d4d86a81f02b94e09fce8004ce2c5369c505ca5a)]: + - @effect/platform@0.91.0 + +## 0.69.2 + +### Patch Changes + +- [#5410](https://github.com/Effect-TS/effect/pull/5410) [`fef9771`](https://github.com/Effect-TS/effect/commit/fef9771eab24af6415be946df0c9f64eba01cef7) Thanks @beeman! - export isQuitExection function from @effect/platform/Terminal + +- Updated dependencies [[`84bc300`](https://github.com/Effect-TS/effect/commit/84bc3003b42ad51210e9e1248efd04c5d0e3dd1e), [`fef9771`](https://github.com/Effect-TS/effect/commit/fef9771eab24af6415be946df0c9f64eba01cef7)]: + - effect@3.17.8 + - @effect/platform@0.90.5 + +## 0.69.1 + +### Patch Changes + +- [#5400](https://github.com/Effect-TS/effect/pull/5400) [`0e296f5`](https://github.com/Effect-TS/effect/commit/0e296f532afa7b69a44d9bff88a56976e248cb43) Thanks @kitlangton! - cli: multiSelect supports per-choice default selection via `selected: true`; single select honors one default + - Add `selected?: boolean` to `Prompt.SelectChoice` + - Seed initial selection in `Prompt.multiSelect` from choices marked `selected: true` + - Allow `Prompt.select` to honor a single `selected: true` choice and throw an error if multiple defaults are provided + - Add tests covering both behaviors + +## 0.69.0 + +### Patch Changes + +- Updated dependencies [[`7813640`](https://github.com/Effect-TS/effect/commit/7813640279d9e3a3e7fc0a29bfb5c6d5fb3c270f)]: + - @effect/platform@0.90.0 + +## 0.68.0 + +### Patch Changes + +- Updated dependencies [[`40c3c87`](https://github.com/Effect-TS/effect/commit/40c3c875f724264312b43002859c82bed9ad0df9), [`ed2c74a`](https://github.com/Effect-TS/effect/commit/ed2c74ae8fa4ea0dd06ea84a3e58cd32e6916104), [`073a1b8`](https://github.com/Effect-TS/effect/commit/073a1b8be5dbfa87454393ee7346f5bc36a4fd63), [`f382e99`](https://github.com/Effect-TS/effect/commit/f382e99e409838a879246250fc3994b9bf5b3c2c), [`e8c7ba5`](https://github.com/Effect-TS/effect/commit/e8c7ba5fd3eb0c3ae3039fc24c09d69391987989), [`7e10415`](https://github.com/Effect-TS/effect/commit/7e1041599ade25103428703f5d2dfd7378a09636), [`e9bdece`](https://github.com/Effect-TS/effect/commit/e9bdececdc24f60a246be5055eca71a0d49ea7f2), [`8d95eb0`](https://github.com/Effect-TS/effect/commit/8d95eb0356b1d1736204836c275d201a547d208d)]: + - effect@3.17.0 + - @effect/platform@0.89.0 + - @effect/printer@0.45.0 + - @effect/printer-ansi@0.45.0 + +## 0.67.1 + +### Patch Changes + +- Updated dependencies [[`f5dfabf`](https://github.com/Effect-TS/effect/commit/f5dfabf51ba481a4468c1509c537314978ef6cec), [`17a5ea8`](https://github.com/Effect-TS/effect/commit/17a5ea8fa29785fe6e4c9480f2a2e9c8c59f3f38), [`d25f22b`](https://github.com/Effect-TS/effect/commit/d25f22be7598abe977caf6cdac3b0dd78b438c48)]: + - effect@3.16.14 + - @effect/platform@0.88.1 + - @effect/printer@0.44.14 + - @effect/printer-ansi@0.44.14 + +## 0.67.0 + +### Patch Changes + +- Updated dependencies [[`27206d7`](https://github.com/Effect-TS/effect/commit/27206d7f0558d7fe28de57bf54f1d0cc83acc92e), [`dbabf5e`](https://github.com/Effect-TS/effect/commit/dbabf5e76fa63b050d2b6c466713c7dc59f07d3c)]: + - @effect/platform@0.88.0 + +## 0.66.13 + +### Patch Changes + +- Updated dependencies [[`c1c05a8`](https://github.com/Effect-TS/effect/commit/c1c05a8242fb5df7445b4a12387a60eac7726eb7), [`81fe4a2`](https://github.com/Effect-TS/effect/commit/81fe4a2c81d5e30e180a60e68c52016a27b350db)]: + - effect@3.16.13 + - @effect/platform@0.87.13 + - @effect/printer@0.44.13 + - @effect/printer-ansi@0.44.13 + +## 0.66.12 + +### Patch Changes + +- Updated dependencies [[`32ba77a`](https://github.com/Effect-TS/effect/commit/32ba77ae304d2161362a73e8b61965332626cf2d), [`d5e25b2`](https://github.com/Effect-TS/effect/commit/d5e25b237f05670ee42b386cb40b2cb448fc11d7)]: + - @effect/platform@0.87.12 + +## 0.66.11 + +### Patch Changes + +- Updated dependencies [[`001392b`](https://github.com/Effect-TS/effect/commit/001392ba8bfcad101bb034348a7415012fb12f72), [`7bfb099`](https://github.com/Effect-TS/effect/commit/7bfb099cb5528511b8d63045c4fbb4dc9cb18528)]: + - @effect/platform@0.87.11 + +## 0.66.10 + +### Patch Changes + +- Updated dependencies [[`678318d`](https://github.com/Effect-TS/effect/commit/678318d2e88233156b006acda56c2d138ee3ffa0), [`678318d`](https://github.com/Effect-TS/effect/commit/678318d2e88233156b006acda56c2d138ee3ffa0)]: + - @effect/platform@0.87.10 + +## 0.66.9 + +### Patch Changes + +- Updated dependencies [[`54514a2`](https://github.com/Effect-TS/effect/commit/54514a2f53166de27ad7e756dbf12194691fd4af)]: + - @effect/platform@0.87.9 + +## 0.66.8 + +### Patch Changes + +- Updated dependencies [[`4ce4f82`](https://github.com/Effect-TS/effect/commit/4ce4f824f6fdef492be1d35c05a490ffce518c89)]: + - @effect/platform@0.87.8 + +## 0.66.7 + +### Patch Changes + +- Updated dependencies [[`a9b617f`](https://github.com/Effect-TS/effect/commit/a9b617f125171ed76cd79ab46d7a924daf3b0e70), [`7e26e86`](https://github.com/Effect-TS/effect/commit/7e26e86524abcc93713d6ad7eee486638c98f7c2)]: + - @effect/platform@0.87.7 + +## 0.66.6 + +### Patch Changes + +- Updated dependencies [[`905da99`](https://github.com/Effect-TS/effect/commit/905da996aad665057b4ca6dba1a4af44fb8835bd)]: + - effect@3.16.12 + - @effect/platform@0.87.6 + - @effect/printer@0.44.12 + - @effect/printer-ansi@0.44.12 + +## 0.66.5 + +### Patch Changes + +- Updated dependencies [[`2fd8676`](https://github.com/Effect-TS/effect/commit/2fd8676c803cd40000dfc3231f5daecaa0e0ebd2)]: + - @effect/platform@0.87.5 + +## 0.66.4 + +### Patch Changes + +- Updated dependencies [[`e82a4fd`](https://github.com/Effect-TS/effect/commit/e82a4fd60f6528d08cef1a4aba0abe0d3ba741ad)]: + - @effect/platform@0.87.4 + +## 0.66.3 + +### Patch Changes + +- Updated dependencies [[`1b6e396`](https://github.com/Effect-TS/effect/commit/1b6e396d699f3cbbc56b68f99055cf746529bb9e)]: + - @effect/platform@0.87.3 + +## 0.66.2 + +### Patch Changes + +- Updated dependencies [[`4fea68c`](https://github.com/Effect-TS/effect/commit/4fea68ca7a25a3c39a1ab68b3885534513ab0c81), [`b927954`](https://github.com/Effect-TS/effect/commit/b9279543cf5688dd8a577af80456959c615217d0), [`99590a6`](https://github.com/Effect-TS/effect/commit/99590a6ca9128eb1ede265b6670b655311995614), [`6c3e24c`](https://github.com/Effect-TS/effect/commit/6c3e24c2308f7d4a29b8f4270ab81bca22ac6bb4)]: + - @effect/platform@0.87.2 + - effect@3.16.11 + - @effect/printer@0.44.11 + - @effect/printer-ansi@0.44.11 + +## 0.66.1 + +### Patch Changes + +- Updated dependencies [[`faad30e`](https://github.com/Effect-TS/effect/commit/faad30ec8742916be59f9db642d0fc98225b636c)]: + - effect@3.16.10 + - @effect/platform@0.87.1 + - @effect/printer@0.44.10 + - @effect/printer-ansi@0.44.10 + +## 0.66.0 + +### Patch Changes + +- Updated dependencies [[`b5bac9a`](https://github.com/Effect-TS/effect/commit/b5bac9ac2913fcd11b02322624f03b544eef53ba)]: + - @effect/platform@0.87.0 + +## 0.65.0 + +### Patch Changes + +- Updated dependencies [[`5137c70`](https://github.com/Effect-TS/effect/commit/5137c703461d8d3b363c112140a6e7f798241d07), [`c23d25c`](https://github.com/Effect-TS/effect/commit/c23d25c3e7c541f1f63b28484d8c461d86c67e99), [`5137c70`](https://github.com/Effect-TS/effect/commit/5137c703461d8d3b363c112140a6e7f798241d07), [`5137c70`](https://github.com/Effect-TS/effect/commit/5137c703461d8d3b363c112140a6e7f798241d07)]: + - effect@3.16.9 + - @effect/platform@0.86.0 + - @effect/printer@0.44.9 + - @effect/printer-ansi@0.44.9 + +## 0.64.2 + +### Patch Changes + +- Updated dependencies [[`914a191`](https://github.com/Effect-TS/effect/commit/914a191e7cb6341a3d0e965bccd27c336cf22e44)]: + - @effect/platform@0.85.2 + +## 0.64.1 + +### Patch Changes + +- Updated dependencies [[`8cb98d5`](https://github.com/Effect-TS/effect/commit/8cb98d53e68330228287ce2a2e0d8a4c86bcab3b), [`db2dd3c`](https://github.com/Effect-TS/effect/commit/db2dd3c3a8a77d791eae19e66153527e1cde4e6e)]: + - effect@3.16.8 + - @effect/platform@0.85.1 + - @effect/printer@0.44.8 + - @effect/printer-ansi@0.44.8 + +## 0.64.0 + +### Patch Changes + +- Updated dependencies [[`93687dd`](https://github.com/Effect-TS/effect/commit/93687ddbb25ce3b324cd2b83d2ccff225e97307e), [`93687dd`](https://github.com/Effect-TS/effect/commit/93687ddbb25ce3b324cd2b83d2ccff225e97307e), [`93687dd`](https://github.com/Effect-TS/effect/commit/93687ddbb25ce3b324cd2b83d2ccff225e97307e)]: + - @effect/platform@0.85.0 + +## 0.63.11 + +### Patch Changes + +- Updated dependencies [[`1bb0d8a`](https://github.com/Effect-TS/effect/commit/1bb0d8ab96782e99434356266b38251554ea0294)]: + - effect@3.16.7 + - @effect/platform@0.84.11 + - @effect/printer@0.44.7 + - @effect/printer-ansi@0.44.7 + +## 0.63.10 + +### Patch Changes + +- Updated dependencies [[`a5f7595`](https://github.com/Effect-TS/effect/commit/a5f75956ef9a15a83c416517ef493f0ee2f5ee8a), [`a02470c`](https://github.com/Effect-TS/effect/commit/a02470c75579e91525a25adb3f21b3650d042fdd), [`bf369b2`](https://github.com/Effect-TS/effect/commit/bf369b2902a0e0b195d957c18b9efd180942cf8b), [`f891d45`](https://github.com/Effect-TS/effect/commit/f891d45adffdafd3f94a2eca23faa354e3a409a8)]: + - effect@3.16.6 + - @effect/platform@0.84.10 + - @effect/printer@0.44.6 + - @effect/printer-ansi@0.44.6 + +## 0.63.9 + +### Patch Changes + +- Updated dependencies [[`bf418ef`](https://github.com/Effect-TS/effect/commit/bf418ef14a0f2ec965535793d5cea8fa8ba177ac)]: + - effect@3.16.5 + - @effect/platform@0.84.9 + - @effect/printer@0.44.5 + - @effect/printer-ansi@0.44.5 + +## 0.63.8 + +### Patch Changes + +- Updated dependencies [[`8b9db77`](https://github.com/Effect-TS/effect/commit/8b9db7742846af0f58fd8e8b7acb7f4f5ff487ec)]: + - @effect/platform@0.84.8 + +## 0.63.7 + +### Patch Changes + +- Updated dependencies [[`74ab9a0`](https://github.com/Effect-TS/effect/commit/74ab9a0a9e16d6e019369d256e1e24175c8bc3f3), [`770008e`](https://github.com/Effect-TS/effect/commit/770008eca3aad2899a2ed951236e575793294b28)]: + - effect@3.16.4 + - @effect/platform@0.84.7 + - @effect/printer@0.44.4 + - @effect/printer-ansi@0.44.4 + +## 0.63.6 + +### Patch Changes + +- Updated dependencies [[`ceea77a`](https://github.com/Effect-TS/effect/commit/ceea77a13055f145520f763e3fce5b8ff15d728f)]: + - @effect/platform@0.84.6 + +## 0.63.5 + +### Patch Changes + +- Updated dependencies [[`ec52c6a`](https://github.com/Effect-TS/effect/commit/ec52c6a2211e76972462b15b9d5a9d6d56761b7a)]: + - @effect/platform@0.84.5 + +## 0.63.4 + +### Patch Changes + +- Updated dependencies [[`87722fc`](https://github.com/Effect-TS/effect/commit/87722fce693a9b49284bbddbf82d30714c688261), [`36217ee`](https://github.com/Effect-TS/effect/commit/36217eeb1337edd9ac3f9a635b80a6385d22ae8f)]: + - effect@3.16.3 + - @effect/platform@0.84.4 + - @effect/printer@0.44.3 + - @effect/printer-ansi@0.44.3 + +## 0.63.3 + +### Patch Changes + +- Updated dependencies [[`ab7684f`](https://github.com/Effect-TS/effect/commit/ab7684f1c2a0671bf091f255d220e3a4cc7f528e)]: + - @effect/platform@0.84.3 + +## 0.63.2 + +### Patch Changes + +- Updated dependencies [[`0ddf148`](https://github.com/Effect-TS/effect/commit/0ddf148a247aa87af043d276b8453a714a400897)]: + - effect@3.16.2 + - @effect/platform@0.84.2 + - @effect/printer@0.44.2 + - @effect/printer-ansi@0.44.2 + +## 0.63.1 + +### Patch Changes + +- Updated dependencies [[`71174d0`](https://github.com/Effect-TS/effect/commit/71174d09691314a9b6b66189e456fd21e3eb6543), [`d615e6e`](https://github.com/Effect-TS/effect/commit/d615e6e5b944f6fd5e627e31752c7ca7e4e1c17d)]: + - @effect/platform@0.84.1 + - effect@3.16.1 + - @effect/printer@0.44.1 + - @effect/printer-ansi@0.44.1 + +## 0.63.0 + +### Patch Changes + +- Updated dependencies [[`ee0bd5d`](https://github.com/Effect-TS/effect/commit/ee0bd5d24864752c54cb359f67a67dd903971ec4), [`5189800`](https://github.com/Effect-TS/effect/commit/51898004e11766b8cf6d95e960b636f6d5db79ec), [`58bfeaa`](https://github.com/Effect-TS/effect/commit/58bfeaa64ded8c88f772b184311c0c0dbac10960), [`194d748`](https://github.com/Effect-TS/effect/commit/194d7486943f56f3267ef415395ac220a4b3e634), [`918c9ea`](https://github.com/Effect-TS/effect/commit/918c9ea1a57facb154f0fb26792021f337054dee), [`9198e6f`](https://github.com/Effect-TS/effect/commit/9198e6fcc1a3ff4fefb3363004de558d8de01f40), [`2a370bf`](https://github.com/Effect-TS/effect/commit/2a370bf625fdeede5659721468eb0d527e403279), [`58ccb91`](https://github.com/Effect-TS/effect/commit/58ccb91328c8df5d49808b673738bc09df355201), [`fd47834`](https://github.com/Effect-TS/effect/commit/fd478348203fa89462b0a1d067ce4de034353df4)]: + - effect@3.16.0 + - @effect/platform@0.84.0 + - @effect/printer@0.44.0 + - @effect/printer-ansi@0.44.0 + +## 0.62.0 + +### Patch Changes + +- Updated dependencies [[`5522520`](https://github.com/Effect-TS/effect/commit/55225206ab9af0ad60b1c0654690a8a096d625cd), [`cc5bb2b`](https://github.com/Effect-TS/effect/commit/cc5bb2b918a9450a975f702dabcea891bda382cb)]: + - @effect/platform@0.83.0 + - effect@3.15.5 + - @effect/printer@0.43.5 + - @effect/printer-ansi@0.43.5 + +## 0.61.8 + +### Patch Changes + +- Updated dependencies [[`0617b9d`](https://github.com/Effect-TS/effect/commit/0617b9dc365f1963b36949ad7f9023ab6eb94524)]: + - @effect/platform@0.82.8 + +## 0.61.7 + +### Patch Changes + +- Updated dependencies [[`f570554`](https://github.com/Effect-TS/effect/commit/f57055459524587b041340577dad85476bb35f81), [`78047e8`](https://github.com/Effect-TS/effect/commit/78047e8dfc8005b66f87afe50bb95981fea51561), [`c20b95a`](https://github.com/Effect-TS/effect/commit/c20b95a99ffe452b4774c844d397a905f713b6d6), [`94ada43`](https://github.com/Effect-TS/effect/commit/94ada430928d5685bdbef513e87562c20774a3a2)]: + - effect@3.15.4 + - @effect/platform@0.82.7 + - @effect/printer@0.43.4 + - @effect/printer-ansi@0.43.4 + +## 0.61.6 + +### Patch Changes + +- Updated dependencies [[`618903b`](https://github.com/Effect-TS/effect/commit/618903ba9ae96e2bfe6ee31f61c4359b915f2a36)]: + - @effect/platform@0.82.6 + +## 0.61.5 + +### Patch Changes + +- Updated dependencies [[`7764a07`](https://github.com/Effect-TS/effect/commit/7764a07d960c60df81f14e1dc949518f4bbe494a), [`4577f54`](https://github.com/Effect-TS/effect/commit/4577f548d67273e576cdde423bdd34a4b910766a), [`30a0d9c`](https://github.com/Effect-TS/effect/commit/30a0d9cb51c84290d51b1361d72ff5cee33c13c7)]: + - @effect/platform@0.82.5 + - effect@3.15.3 + - @effect/printer@0.43.3 + - @effect/printer-ansi@0.43.3 + +## 0.61.4 + +### Patch Changes + +- Updated dependencies [[`d45e8a8`](https://github.com/Effect-TS/effect/commit/d45e8a8ac8227192f504e39e6d04fdcf4fb1d225), [`d13b68e`](https://github.com/Effect-TS/effect/commit/d13b68e3a9456d0bfee9bca8273a7b44a9c69087)]: + - @effect/platform@0.82.4 + +## 0.61.3 + +### Patch Changes + +- Updated dependencies [[`b8722b8`](https://github.com/Effect-TS/effect/commit/b8722b817e2306fe8c8245f3f9e32d85b824b961), [`a328f4b`](https://github.com/Effect-TS/effect/commit/a328f4b4fe717dd53e5b04a30f387433c32f7328)]: + - effect@3.15.2 + - @effect/platform@0.82.3 + - @effect/printer@0.43.2 + - @effect/printer-ansi@0.43.2 + +## 0.61.2 + +### Patch Changes + +- Updated dependencies [[`739a3d4`](https://github.com/Effect-TS/effect/commit/739a3d4a4565915fe2e690003f4f9085cb4422fc)]: + - @effect/platform@0.82.2 + +## 0.61.1 + +### Patch Changes + +- Updated dependencies [[`787ce70`](https://github.com/Effect-TS/effect/commit/787ce7042e35b657963473c6efe47752868cd811), [`1269641`](https://github.com/Effect-TS/effect/commit/1269641a99ae43069f7648ff79ffe8729b54b348), [`1269641`](https://github.com/Effect-TS/effect/commit/1269641a99ae43069f7648ff79ffe8729b54b348)]: + - effect@3.15.1 + - @effect/platform@0.82.1 + - @effect/printer@0.43.1 + - @effect/printer-ansi@0.43.1 + +## 0.61.0 + +### Patch Changes + +- Updated dependencies [[`c654595`](https://github.com/Effect-TS/effect/commit/c65459587b51da140b78098e81fdbfece65d53e2), [`d9f5dea`](https://github.com/Effect-TS/effect/commit/d9f5deae0f02f5de2b9fcb1cca8b142ba4bc2bba), [`49aa723`](https://github.com/Effect-TS/effect/commit/49aa7236a15e13f818c86edbca08c4af67c8dfaf), [`74c14d0`](https://github.com/Effect-TS/effect/commit/74c14d01d0cb48cf517a1b6e29a373a96ed0ff5b), [`e4f49b6`](https://github.com/Effect-TS/effect/commit/e4f49b66857e01b74ab6a9a0bc7132f44cd04cbb), [`6f02224`](https://github.com/Effect-TS/effect/commit/6f02224b3fc46a682ad2defb1a260841956c6780), [`1dcfd41`](https://github.com/Effect-TS/effect/commit/1dcfd41ff96abd706901293a00c1893cb29dd8fd), [`b21ab16`](https://github.com/Effect-TS/effect/commit/b21ab16b6f773e7ec4369db4e752c35e719f7870), [`fcf1822`](https://github.com/Effect-TS/effect/commit/fcf1822f98fcda60351d64e9d2c2c13563d7e6db), [`0061dd1`](https://github.com/Effect-TS/effect/commit/0061dd140740165e91569a684cce27a77b23229e), [`8421e6e`](https://github.com/Effect-TS/effect/commit/8421e6e49332bca8f96f482dfd48680e238b3a89), [`a9b3fb7`](https://github.com/Effect-TS/effect/commit/a9b3fb78abcfdb525318a956fd02fcadeb56143e), [`fa10f56`](https://github.com/Effect-TS/effect/commit/fa10f56b96bd9af070ba99ebc3279aa93954261e)]: + - effect@3.15.0 + - @effect/platform@0.82.0 + - @effect/printer@0.43.0 + - @effect/printer-ansi@0.43.0 + +## 0.60.1 + +### Patch Changes + +- Updated dependencies [[`24a9ebb`](https://github.com/Effect-TS/effect/commit/24a9ebbb5af598f0bfd6ecc45307e528043fe011)]: + - effect@3.14.22 + - @effect/platform@0.81.1 + - @effect/printer@0.42.22 + - @effect/printer-ansi@0.42.22 + +## 0.60.0 + +### Patch Changes + +- Updated dependencies [[`672920f`](https://github.com/Effect-TS/effect/commit/672920f85da8abd5f9d4ad85e29248a2aca57ed8)]: + - @effect/platform@0.81.0 + +## 0.59.21 + +### Patch Changes + +- Updated dependencies [[`2f3b7d4`](https://github.com/Effect-TS/effect/commit/2f3b7d4e1fa1ef8790b0ca4da22eb88872ee31df)]: + - effect@3.14.21 + - @effect/platform@0.80.21 + - @effect/printer@0.42.21 + - @effect/printer-ansi@0.42.21 + +## 0.59.20 + +### Patch Changes + +- Updated dependencies [[`17e2f30`](https://github.com/Effect-TS/effect/commit/17e2f3091408cf0fca9414d4af3bdf7b2765b378)]: + - effect@3.14.20 + - @effect/platform@0.80.20 + - @effect/printer@0.42.20 + - @effect/printer-ansi@0.42.20 + +## 0.59.19 + +### Patch Changes + +- Updated dependencies [[`056a910`](https://github.com/Effect-TS/effect/commit/056a910d0a0b8b00b0dc9df4a070466b2b5c2f6c), [`e25e7bb`](https://github.com/Effect-TS/effect/commit/e25e7bbc1797733916f48f501425d9f2ef310d9f), [`3273d57`](https://github.com/Effect-TS/effect/commit/3273d572c2b3175a842677f19efeea4cd65ab016)]: + - effect@3.14.19 + - @effect/platform@0.80.19 + - @effect/printer@0.42.19 + - @effect/printer-ansi@0.42.19 + +## 0.59.18 + +### Patch Changes + +- Updated dependencies [[`b1164d4`](https://github.com/Effect-TS/effect/commit/b1164d49a1dfdf299e9971367b6fc6be4df0ddff)]: + - effect@3.14.18 + - @effect/platform@0.80.18 + - @effect/printer@0.42.18 + - @effect/printer-ansi@0.42.18 + +## 0.59.17 + +### Patch Changes + +- Updated dependencies [[`0b54681`](https://github.com/Effect-TS/effect/commit/0b54681cd89245e211d8f49272be0f1bf2f81813), [`41a59d5`](https://github.com/Effect-TS/effect/commit/41a59d5916a296b12b0d5ead9e859e05f40b4cce)]: + - effect@3.14.17 + - @effect/platform@0.80.17 + - @effect/printer@0.42.17 + - @effect/printer-ansi@0.42.17 + +## 0.59.16 + +### Patch Changes + +- Updated dependencies [[`ee14444`](https://github.com/Effect-TS/effect/commit/ee144441021ec77039e43396eaf90714687bb495), [`f1c8583`](https://github.com/Effect-TS/effect/commit/f1c8583f8c3ea9415f813795ca2940a897c9ba9a)]: + - effect@3.14.16 + - @effect/platform@0.80.16 + - @effect/printer@0.42.16 + - @effect/printer-ansi@0.42.16 + +## 0.59.15 + +### Patch Changes + +- Updated dependencies [[`239cc99`](https://github.com/Effect-TS/effect/commit/239cc995ce645946210a3c3d2cb52bd3547c0687), [`8b6c947`](https://github.com/Effect-TS/effect/commit/8b6c947eaa8e45a67ecb3c37d45cd27f3e41d165), [`c50a63b`](https://github.com/Effect-TS/effect/commit/c50a63bbecb9f560b9cae349c447eed877d1b9b6)]: + - effect@3.14.15 + - @effect/platform@0.80.15 + - @effect/printer@0.42.15 + - @effect/printer-ansi@0.42.15 + +## 0.59.14 + +### Patch Changes + +- Updated dependencies [[`6ed8d15`](https://github.com/Effect-TS/effect/commit/6ed8d1589beb181d30abc79afebdaabc1d101538)]: + - effect@3.14.14 + - @effect/platform@0.80.14 + - @effect/printer@0.42.14 + - @effect/printer-ansi@0.42.14 + +## 0.59.13 + +### Patch Changes + +- Updated dependencies [[`ee77788`](https://github.com/Effect-TS/effect/commit/ee77788747e7ebbde6bfa88256cde49dbbad3608), [`5fce6ba`](https://github.com/Effect-TS/effect/commit/5fce6ba19c3cc63cc0104e737e581ad989dedbf0), [`570e45f`](https://github.com/Effect-TS/effect/commit/570e45f8cb936e42ec48f67f21bb2b7252f36c0c)]: + - effect@3.14.13 + - @effect/platform@0.80.13 + - @effect/printer@0.42.13 + - @effect/printer-ansi@0.42.13 + +## 0.59.12 + +### Patch Changes + +- Updated dependencies [[`c2ad9ee`](https://github.com/Effect-TS/effect/commit/c2ad9ee9f3c4c743390edf35ed9e85a20be33811), [`9c68654`](https://github.com/Effect-TS/effect/commit/9c686542b6eb3ea188cb70673ef2e41223633e89)]: + - effect@3.14.12 + - @effect/platform@0.80.12 + - @effect/printer@0.42.12 + - @effect/printer-ansi@0.42.12 + +## 0.59.11 + +### Patch Changes + +- Updated dependencies [[`e536127`](https://github.com/Effect-TS/effect/commit/e536127c1e6f2fb3a542c73ae919435a629a346b)]: + - effect@3.14.11 + - @effect/platform@0.80.11 + - @effect/printer@0.42.11 + - @effect/printer-ansi@0.42.11 + +## 0.59.10 + +### Patch Changes + +- Updated dependencies [[`bc7efa3`](https://github.com/Effect-TS/effect/commit/bc7efa3b031bb25e1ed3c8f2d3fb5e8da166cadc)]: + - effect@3.14.10 + - @effect/platform@0.80.10 + - @effect/printer@0.42.10 + - @effect/printer-ansi@0.42.10 + +## 0.59.9 + +### Patch Changes + +- Updated dependencies [[`d78249f`](https://github.com/Effect-TS/effect/commit/d78249f0b67f63cf4baf806ff090cba33293daf0)]: + - effect@3.14.9 + - @effect/platform@0.80.9 + - @effect/printer@0.42.9 + - @effect/printer-ansi@0.42.9 + +## 0.59.8 + +### Patch Changes + +- Updated dependencies [[`b3a2d32`](https://github.com/Effect-TS/effect/commit/b3a2d32772e6f7f20eacf2e18128e99324c4d378)]: + - effect@3.14.8 + - @effect/platform@0.80.8 + - @effect/printer@0.42.8 + - @effect/printer-ansi@0.42.8 + +## 0.59.7 + +### Patch Changes + +- Updated dependencies [[`b542a4b`](https://github.com/Effect-TS/effect/commit/b542a4bf195be0c9af1523e1ba96c953decc4d25)]: + - effect@3.14.7 + - @effect/platform@0.80.7 + - @effect/printer@0.42.7 + - @effect/printer-ansi@0.42.7 + +## 0.59.6 + +### Patch Changes + +- Updated dependencies [[`47618c1`](https://github.com/Effect-TS/effect/commit/47618c1ad84ebcc5a51133a3fff5aa5012d49d45), [`6077882`](https://github.com/Effect-TS/effect/commit/60778824a4794336c33807801f813f8751d1c7e4)]: + - effect@3.14.6 + - @effect/platform@0.80.6 + - @effect/printer@0.42.6 + - @effect/printer-ansi@0.42.6 + +## 0.59.5 + +### Patch Changes + +- Updated dependencies [[`40dbfef`](https://github.com/Effect-TS/effect/commit/40dbfeff239b6e567706752114f31b2fce7de4e3), [`85fba81`](https://github.com/Effect-TS/effect/commit/85fba815ac07eb13d4227a69ac76a18e4b94df18), [`5a5ebdd`](https://github.com/Effect-TS/effect/commit/5a5ebdddfaddd259538b4599a6676281faca778e)]: + - effect@3.14.5 + - @effect/platform@0.80.5 + - @effect/printer@0.42.5 + - @effect/printer-ansi@0.42.5 + +## 0.59.4 + +### Patch Changes + +- Updated dependencies [[`e4ba2c6`](https://github.com/Effect-TS/effect/commit/e4ba2c66a878e81b5e295d6d49aaf724b80a28ef)]: + - effect@3.14.4 + - @effect/platform@0.80.4 + - @effect/printer@0.42.4 + - @effect/printer-ansi@0.42.4 + +## 0.59.3 + +### Patch Changes + +- Updated dependencies [[`37aa8e1`](https://github.com/Effect-TS/effect/commit/37aa8e137725a902e70cd1e468ea98b873aa5056), [`34f03d6`](https://github.com/Effect-TS/effect/commit/34f03d66875f21f266f102223a03cd14c2ed6ea6)]: + - effect@3.14.3 + - @effect/platform@0.80.3 + - @effect/printer@0.42.3 + - @effect/printer-ansi@0.42.3 + +## 0.59.2 + +### Patch Changes + +- Updated dependencies [[`f87991b`](https://github.com/Effect-TS/effect/commit/f87991b6d8a2edfaf90b01cebda4b466992ae865), [`f87991b`](https://github.com/Effect-TS/effect/commit/f87991b6d8a2edfaf90b01cebda4b466992ae865), [`0a3e3e1`](https://github.com/Effect-TS/effect/commit/0a3e3e18eea5e0d1882f1a6c906198e6ef226a41)]: + - effect@3.14.2 + - @effect/platform@0.80.2 + - @effect/printer@0.42.2 + - @effect/printer-ansi@0.42.2 + +## 0.59.1 + +### Patch Changes + +- Updated dependencies [[`4a274fe`](https://github.com/Effect-TS/effect/commit/4a274fe9f623182b6b902827e0e83bd89ca3b05c)]: + - effect@3.14.1 + - @effect/platform@0.80.1 + - @effect/printer@0.42.1 + - @effect/printer-ansi@0.42.1 + +## 0.59.0 + +### Patch Changes + +- Updated dependencies [[`1f47e4e`](https://github.com/Effect-TS/effect/commit/1f47e4e12546ab691b29bfb7b5128bb17b93baa5), [`26dd75f`](https://github.com/Effect-TS/effect/commit/26dd75f276a0d8a63eab313bd5a167d5072c9780), [`aba2d1d`](https://github.com/Effect-TS/effect/commit/aba2d1d831ea149481bd4dd755528c0afa8239ce), [`3131f8f`](https://github.com/Effect-TS/effect/commit/3131f8fd12ba9eb31b90fa2f42bf88b12309133c), [`aba2d1d`](https://github.com/Effect-TS/effect/commit/aba2d1d831ea149481bd4dd755528c0afa8239ce), [`04dff2d`](https://github.com/Effect-TS/effect/commit/04dff2d01ac68c260f29a6d4743381825c353c86), [`c7fac0c`](https://github.com/Effect-TS/effect/commit/c7fac0cd7eadcd5cc0c3a987051c5b57ad271638), [`aba2d1d`](https://github.com/Effect-TS/effect/commit/aba2d1d831ea149481bd4dd755528c0afa8239ce), [`ffaa3f3`](https://github.com/Effect-TS/effect/commit/ffaa3f3969df26610fcc02ad537340641d44e803), [`ab957c1`](https://github.com/Effect-TS/effect/commit/ab957c1fee714868f56c7ab4e802b9d449e9b666), [`35db9ce`](https://github.com/Effect-TS/effect/commit/35db9ce228f1416c8abacc6dc9c36fbd0f33ef0f), [`aba2d1d`](https://github.com/Effect-TS/effect/commit/aba2d1d831ea149481bd4dd755528c0afa8239ce), [`cf77ea9`](https://github.com/Effect-TS/effect/commit/cf77ea9ab4fc89e66a43f682a9926ccdee6c57ed), [`26dd75f`](https://github.com/Effect-TS/effect/commit/26dd75f276a0d8a63eab313bd5a167d5072c9780), [`baaab60`](https://github.com/Effect-TS/effect/commit/baaab60b737f35dfab8e4a21bce28a195d19e899)]: + - effect@3.14.0 + - @effect/platform@0.80.0 + - @effect/printer@0.42.0 + - @effect/printer-ansi@0.42.0 + +## 0.58.4 + +### Patch Changes + +- Updated dependencies [[`5662363`](https://github.com/Effect-TS/effect/commit/566236361e270e575ef1cbf308ad1967c82a362c), [`5f1fd15`](https://github.com/Effect-TS/effect/commit/5f1fd15308ab154791580059b89877d19a2055c2), [`8bb1460`](https://github.com/Effect-TS/effect/commit/8bb1460c824f66f0f25ebd899c5e74e388089c37)]: + - @effect/platform@0.79.4 + +## 0.58.3 + +### Patch Changes + +- Updated dependencies [[`0c4803f`](https://github.com/Effect-TS/effect/commit/0c4803fcc69262d11a97ce49d0e9b4288df0651f), [`6f65ac4`](https://github.com/Effect-TS/effect/commit/6f65ac4eac1489cd6ea390e18b0908670722adad)]: + - effect@3.13.12 + - @effect/platform@0.79.3 + - @effect/printer@0.41.12 + - @effect/printer-ansi@0.41.12 + +## 0.58.2 + +### Patch Changes + +- Updated dependencies [[`fad8cca`](https://github.com/Effect-TS/effect/commit/fad8cca9bbfcc2eaeb44b97c15dbe0a1eda75315), [`4296293`](https://github.com/Effect-TS/effect/commit/4296293049414d0cf2d915a26c552b09f946b9a0), [`9c241ab`](https://github.com/Effect-TS/effect/commit/9c241abe47ccf7a5257b98a4a64a63054a12741d), [`082b0c1`](https://github.com/Effect-TS/effect/commit/082b0c1b9f4252bcdd69608f2e4a9226f953ac3f), [`be12983`](https://github.com/Effect-TS/effect/commit/be12983bc7e7537b41cd8910fc4eb7d1da56ab07), [`de88127`](https://github.com/Effect-TS/effect/commit/de88127a5a5906ccece98af74787b5ae0e65e431)]: + - effect@3.13.11 + - @effect/platform@0.79.2 + - @effect/printer@0.41.11 + - @effect/printer-ansi@0.41.11 + +## 0.58.1 + +### Patch Changes + +- Updated dependencies [[`527c964`](https://github.com/Effect-TS/effect/commit/527c9645229f5be9714a7e60a38a9e753c4bbfb1)]: + - effect@3.13.10 + - @effect/platform@0.79.1 + - @effect/printer@0.41.10 + - @effect/printer-ansi@0.41.10 + +## 0.58.0 + +### Patch Changes + +- Updated dependencies [[`88fe129`](https://github.com/Effect-TS/effect/commit/88fe12923740765c0335a6e6203fdcc6a463edca), [`d630249`](https://github.com/Effect-TS/effect/commit/d630249426113088abe8b382db4f14d80f2160c2), [`2976e52`](https://github.com/Effect-TS/effect/commit/2976e52538d9dc9ffdcbc84d4ac748cff9305971)]: + - @effect/platform@0.79.0 + - effect@3.13.9 + - @effect/printer@0.41.9 + - @effect/printer-ansi@0.41.9 + +## 0.57.1 + +### Patch Changes + +- Updated dependencies [[`c65d336`](https://github.com/Effect-TS/effect/commit/c65d3362d07ec815ff3b46278314e8a31706ddc2), [`22d2ebb`](https://github.com/Effect-TS/effect/commit/22d2ebb4b11f5a44351a4736e65da391a3b647d0)]: + - effect@3.13.8 + - @effect/platform@0.78.1 + - @effect/printer@0.41.8 + - @effect/printer-ansi@0.41.8 + +## 0.57.0 + +### Patch Changes + +- Updated dependencies [[`c5bcf53`](https://github.com/Effect-TS/effect/commit/c5bcf53b7cb49dacffdd2a6cd8eb48cc452b417e)]: + - @effect/platform@0.78.0 + +## 0.56.7 + +### Patch Changes + +- Updated dependencies [[`840cc73`](https://github.com/Effect-TS/effect/commit/840cc7329908db7ca693ef47b07d4f845c29cadd), [`9bf8a74`](https://github.com/Effect-TS/effect/commit/9bf8a74b967f18d931743dd5196af326c9118e9c), [`87ba23c`](https://github.com/Effect-TS/effect/commit/87ba23c41c193503ed0c612b0d32d0b253794c64), [`f910880`](https://github.com/Effect-TS/effect/commit/f91088069057f3b4529753f5bc5532b028d726df), [`0d01480`](https://github.com/Effect-TS/effect/commit/0d014803e4f688f74386a80abd65485e1a319244)]: + - @effect/platform@0.77.7 + - effect@3.13.7 + - @effect/printer@0.41.7 + - @effect/printer-ansi@0.41.7 + +## 0.56.6 + +### Patch Changes + +- Updated dependencies [[`3154ce4`](https://github.com/Effect-TS/effect/commit/3154ce4692fa18b804982158d3c4c8a8a5fae386)]: + - effect@3.13.6 + - @effect/platform@0.77.6 + - @effect/printer@0.41.6 + - @effect/printer-ansi@0.41.6 + +## 0.56.5 + +### Patch Changes + +- Updated dependencies [[`367bb35`](https://github.com/Effect-TS/effect/commit/367bb35f4c2a254e1fb211d96db2474a7aed9020), [`6cf11c3`](https://github.com/Effect-TS/effect/commit/6cf11c3a75773ceec2877c85ddc760f381f0866d), [`a0acec8`](https://github.com/Effect-TS/effect/commit/a0acec851f72e19466363d24b9cc218acd00006a)]: + - effect@3.13.5 + - @effect/platform@0.77.5 + - @effect/printer@0.41.5 + - @effect/printer-ansi@0.41.5 + +## 0.56.4 + +### Patch Changes + +- Updated dependencies [[`e0746f9`](https://github.com/Effect-TS/effect/commit/e0746f9aa398b69c6542e375910683bf17f49f46), [`17d9e89`](https://github.com/Effect-TS/effect/commit/17d9e89f9851663bdbb6c1e685601d97806114a4)]: + - @effect/platform@0.77.4 + - effect@3.13.4 + - @effect/printer@0.41.4 + - @effect/printer-ansi@0.41.4 + +## 0.56.3 + +### Patch Changes + +- Updated dependencies [[`cc5588d`](https://github.com/Effect-TS/effect/commit/cc5588df07f9103513547cb429ce041b9436a8bd), [`623c8cd`](https://github.com/Effect-TS/effect/commit/623c8cd053ed6ee3d353aaa8778d484670fca2bb), [`00b4eb1`](https://github.com/Effect-TS/effect/commit/00b4eb1ece12a16e222e6220965bb4024d6752ac), [`f2aee98`](https://github.com/Effect-TS/effect/commit/f2aee989b0a600900ce83e7f460d02908620c80f), [`fb798eb`](https://github.com/Effect-TS/effect/commit/fb798eb9061f1191badc017d1aa649360254da20), [`2251b15`](https://github.com/Effect-TS/effect/commit/2251b1528810bb695b37ce388b653cec0c5bf80c), [`2e15c1e`](https://github.com/Effect-TS/effect/commit/2e15c1e33648add0b29fe274fbcb7294b7515085), [`a4979db`](https://github.com/Effect-TS/effect/commit/a4979db021aef16e731be64df196b72088fc4376), [`b74255a`](https://github.com/Effect-TS/effect/commit/b74255a304ad49d60bedb1a260fd697f370af27a), [`d7f6a5c`](https://github.com/Effect-TS/effect/commit/d7f6a5c7d26c1963dcd864ca62360d20d08c7b49), [`9dd8979`](https://github.com/Effect-TS/effect/commit/9dd8979e940915b1cc1b1f264f3d019c77a65a02), [`477b488`](https://github.com/Effect-TS/effect/commit/477b488284f47c5469d7fba3e4065fb7e3b6556e), [`10932cb`](https://github.com/Effect-TS/effect/commit/10932cbf58fc721ada631cebec42f773ce96d3cc), [`9f6c784`](https://github.com/Effect-TS/effect/commit/9f6c78468b3b5e9ebfc38ffdfb70702901ee977b), [`2c639ec`](https://github.com/Effect-TS/effect/commit/2c639ecee332de4266e36022c989c35ae4e02105), [`886aaa8`](https://github.com/Effect-TS/effect/commit/886aaa81e06dfd3cd9391e8ea987d8cd5ada1124)]: + - effect@3.13.3 + - @effect/platform@0.77.3 + - @effect/printer@0.41.3 + - @effect/printer-ansi@0.41.3 + +## 0.56.2 + +### Patch Changes + +- Updated dependencies [[`31be72a`](https://github.com/Effect-TS/effect/commit/31be72ada118cb84a942e67b1663263f8db74a9f), [`3e7ce97`](https://github.com/Effect-TS/effect/commit/3e7ce97f8a41756a039cf635d0b3d9a75d781097), [`31be72a`](https://github.com/Effect-TS/effect/commit/31be72ada118cb84a942e67b1663263f8db74a9f)]: + - effect@3.13.2 + - @effect/platform@0.77.2 + - @effect/printer@0.41.2 + - @effect/printer-ansi@0.41.2 + +## 0.56.1 + +### Patch Changes + +- Updated dependencies [[`b56a211`](https://github.com/Effect-TS/effect/commit/b56a2110569fd0ec0b57ac137743e926d49f51cc)]: + - effect@3.13.1 + - @effect/platform@0.77.1 + - @effect/printer@0.41.1 + - @effect/printer-ansi@0.41.1 + +## 0.56.0 + +### Patch Changes + +- Updated dependencies [[`8baef83`](https://github.com/Effect-TS/effect/commit/8baef83e7ff0b7bc0738b680e1ef013065386cff), [`655bfe2`](https://github.com/Effect-TS/effect/commit/655bfe29e44cc3f0fb9b4e53038f50b891c188df), [`d90cbc2`](https://github.com/Effect-TS/effect/commit/d90cbc274e2742d18671fe65aa4764c057eb6cba), [`75632bd`](https://github.com/Effect-TS/effect/commit/75632bd44b8025101d652ccbaeef898c7086c91c), [`c874a2e`](https://github.com/Effect-TS/effect/commit/c874a2e4b17e9d71904ca8375bb77b020975cb1d), [`bf865e5`](https://github.com/Effect-TS/effect/commit/bf865e5833f77fd8f6c06944ca9d507b54488301), [`f98b2b7`](https://github.com/Effect-TS/effect/commit/f98b2b7592cf20f9d85313e7f1e964cb65878138), [`de8ce92`](https://github.com/Effect-TS/effect/commit/de8ce924923eaa4e1b761a97eb45ec967389f3d5), [`cf8b2dd`](https://github.com/Effect-TS/effect/commit/cf8b2dd112f8e092ed99d78fd728db0f91c29050), [`db426a5`](https://github.com/Effect-TS/effect/commit/db426a5fb41ab84d18e3c8753a7329b4de544245), [`6862444`](https://github.com/Effect-TS/effect/commit/6862444094906ad4f2cb077ff3b9cc0b73880c8c), [`5fc8a90`](https://github.com/Effect-TS/effect/commit/5fc8a90ba46a5fd9f3b643f0b5aeadc69d717339), [`546a492`](https://github.com/Effect-TS/effect/commit/546a492e60eb2b8b048a489a474b934ea0877005), [`65c4796`](https://github.com/Effect-TS/effect/commit/65c47966ce39055f02cf5c808daabb3ea6442b0b), [`9760fdc`](https://github.com/Effect-TS/effect/commit/9760fdc37bdaef9da8b150e46b86ddfbe2ad9221), [`5b471e7`](https://github.com/Effect-TS/effect/commit/5b471e7d4317e8ee5d72bbbd3e0c9775160949ab), [`4f810cc`](https://github.com/Effect-TS/effect/commit/4f810cc2770e9f1f266851d2cb6257112c12af49)]: + - effect@3.13.0 + - @effect/platform@0.77.0 + - @effect/printer@0.41.0 + - @effect/printer-ansi@0.41.0 + +## 0.55.1 + +### Patch Changes + +- Updated dependencies [[`4018eae`](https://github.com/Effect-TS/effect/commit/4018eaed2733241676ddb8c52416f463a8c32e35), [`543d36d`](https://github.com/Effect-TS/effect/commit/543d36d1a11452560b01ab966a82529ad5fee8c9), [`c407726`](https://github.com/Effect-TS/effect/commit/c407726f79df4a567a9631cddd8effaa16b3535d), [`f70a65a`](https://github.com/Effect-TS/effect/commit/f70a65ac80c6635d80b12beaf4d32a9cc59fa143), [`ba409f6`](https://github.com/Effect-TS/effect/commit/ba409f69c41aeaa29e475c0630735726eaf4dbac), [`3d2e356`](https://github.com/Effect-TS/effect/commit/3d2e3565e8a43d1bdb5daee8db3b90f56d71d859)]: + - effect@3.12.12 + - @effect/platform@0.76.1 + - @effect/printer@0.40.12 + - @effect/printer-ansi@0.40.12 + +## 0.55.0 + +### Patch Changes + +- Updated dependencies [[`b6a032f`](https://github.com/Effect-TS/effect/commit/b6a032f07bffa020a848c813881879395134fa20), [`42ddd5f`](https://github.com/Effect-TS/effect/commit/42ddd5f144ce9f9d94a036679ebbd626446d37f5), [`2fe447c`](https://github.com/Effect-TS/effect/commit/2fe447c6354d334f9c591b8a8481818f5f0e797e), [`2473ad5`](https://github.com/Effect-TS/effect/commit/2473ad5cf23582e3a41338091fa526ffe611288d)]: + - effect@3.12.11 + - @effect/platform@0.76.0 + - @effect/printer@0.40.11 + - @effect/printer-ansi@0.40.11 + +## 0.54.4 + +### Patch Changes + +- [#4402](https://github.com/Effect-TS/effect/pull/4402) [`3e56745`](https://github.com/Effect-TS/effect/commit/3e56745421e3ad7186cf3e6772c691c49be5c71a) Thanks @Delusion2056! - Fix CLI help output to correctly display default values + +- Updated dependencies [[`e30f132`](https://github.com/Effect-TS/effect/commit/e30f132c336c9d0760bad39f82a55c7ce5159eb7), [`33fa667`](https://github.com/Effect-TS/effect/commit/33fa667c2623be1026e1ccee91bd44f73b09020a), [`87f5f28`](https://github.com/Effect-TS/effect/commit/87f5f2842e4196cb88d13f10f443ff0567e82832), [`7d57ecd`](https://github.com/Effect-TS/effect/commit/7d57ecdaf5da2345ebbf9c22df50317578bde0f5), [`4dbd170`](https://github.com/Effect-TS/effect/commit/4dbd170538e8fb7a36aa7c469c6f93b6c7000091)]: + - effect@3.12.10 + - @effect/platform@0.75.4 + - @effect/printer@0.40.10 + - @effect/printer-ansi@0.40.10 + +## 0.54.3 + +### Patch Changes + +- Updated dependencies [[`1b4a4e9`](https://github.com/Effect-TS/effect/commit/1b4a4e904ef5227ec7d9114d4e417eca19eed940)]: + - effect@3.12.9 + - @effect/platform@0.75.3 + - @effect/printer@0.40.9 + - @effect/printer-ansi@0.40.9 + +## 0.54.2 + +### Patch Changes + +- [#4380](https://github.com/Effect-TS/effect/pull/4380) [`c45b559`](https://github.com/Effect-TS/effect/commit/c45b5592b5fd1189a5c932cfe05bd7d5f6d68508) Thanks @fubhy! - Fixed module imports + +- Updated dependencies [[`59b3cfb`](https://github.com/Effect-TS/effect/commit/59b3cfbbd5713dd9475998e95fad5534c0b21466), [`766113c`](https://github.com/Effect-TS/effect/commit/766113c0ea3512cdb887650ead8ba314236e22ee), [`bb05fb8`](https://github.com/Effect-TS/effect/commit/bb05fb83457355b1ca567228a9e041edfb6fd85d), [`712277f`](https://github.com/Effect-TS/effect/commit/712277f949052a24b46e4aa234063a6abf395c90), [`f269122`](https://github.com/Effect-TS/effect/commit/f269122508693b111142994dd48698ddc75f3d69), [`8f6006a`](https://github.com/Effect-TS/effect/commit/8f6006a610fb6d6c7b8d14209a7323338a8964ff), [`c45b559`](https://github.com/Effect-TS/effect/commit/c45b5592b5fd1189a5c932cfe05bd7d5f6d68508), [`430c846`](https://github.com/Effect-TS/effect/commit/430c846cbac05b187e3d24ac8dfee0cf22506f7c), [`7b03057`](https://github.com/Effect-TS/effect/commit/7b03057507d2dab5e6793beb9c578dedaaeb15fe), [`a9c94c8`](https://github.com/Effect-TS/effect/commit/a9c94c807755610831211a686d2fad849ab38eb4), [`107e6f0`](https://github.com/Effect-TS/effect/commit/107e6f0557a1e2d3b0dce25d62fa1e2601521752), [`c9175ae`](https://github.com/Effect-TS/effect/commit/c9175aef41cb1e3b689d0ac0a4f53d8107376b58), [`65c11b9`](https://github.com/Effect-TS/effect/commit/65c11b9266ec9447c31c26fe3ed35c73bd3b81fd), [`e386d2f`](https://github.com/Effect-TS/effect/commit/e386d2f1b3ab3ac2c14ee76de11f5963d32a3df4), [`9172efb`](https://github.com/Effect-TS/effect/commit/9172efba98bc6a82353e6ec2af61ac08f038ba64)]: + - @effect/platform@0.75.2 + - effect@3.12.8 + - @effect/printer@0.40.8 + - @effect/printer-ansi@0.40.8 + +## 0.54.1 + +### Patch Changes + +- Updated dependencies [[`8dff1d1`](https://github.com/Effect-TS/effect/commit/8dff1d1bff76cdba643cad7f0bf864300f08bc61)]: + - effect@3.12.7 + - @effect/platform@0.75.1 + - @effect/printer@0.40.7 + - @effect/printer-ansi@0.40.7 + +## 0.54.0 + +### Patch Changes + +- Updated dependencies [[`5e43ce5`](https://github.com/Effect-TS/effect/commit/5e43ce50bae116865906112e7f88d390739d778b), [`289c13b`](https://github.com/Effect-TS/effect/commit/289c13b38e8e35b214d46d385d05dead176c87cd), [`76eb7d0`](https://github.com/Effect-TS/effect/commit/76eb7d0fbce3c009c8f77e84c178cb15bbed9709), [`8b4e75d`](https://github.com/Effect-TS/effect/commit/8b4e75d35daea807c447ca760948a717aa66bb52), [`fc5e0f0`](https://github.com/Effect-TS/effect/commit/fc5e0f0d357a0051cfa01c1ede83ffdd3cb41ab1), [`004fd2b`](https://github.com/Effect-TS/effect/commit/004fd2bbd1459e64fb1b57f02eeb791ca5ea1ea5), [`b2a31be`](https://github.com/Effect-TS/effect/commit/b2a31be85c35d891351ce4f9a2cc93ece0c257f6), [`5514d05`](https://github.com/Effect-TS/effect/commit/5514d05b5cd586ff5868b8bd41c959e95e6c33cd), [`bf5f0ae`](https://github.com/Effect-TS/effect/commit/bf5f0ae9daa0170471678e22585e8ec14ce667bb), [`3b19bcf`](https://github.com/Effect-TS/effect/commit/3b19bcfd3aaadb6c9253428622df524537c8e626), [`b064b3b`](https://github.com/Effect-TS/effect/commit/b064b3b293615fd268cc5a5647d0981eb67750b8), [`eb264ed`](https://github.com/Effect-TS/effect/commit/eb264ed8a6e8c92a9dc7006f766c6ca2e5d29e03), [`289c13b`](https://github.com/Effect-TS/effect/commit/289c13b38e8e35b214d46d385d05dead176c87cd), [`f474678`](https://github.com/Effect-TS/effect/commit/f474678bf10b8f1c80e3dc096ddc7ecf20b2b23e), [`ee187d0`](https://github.com/Effect-TS/effect/commit/ee187d098007a402844c94d04f0cd8f07695377a)]: + - @effect/platform@0.75.0 + - effect@3.12.6 + - @effect/printer@0.40.6 + - @effect/printer-ansi@0.40.6 + +## 0.53.0 + +### Patch Changes + +- [#4275](https://github.com/Effect-TS/effect/pull/4275) [`8645754`](https://github.com/Effect-TS/effect/commit/86457544ed6c144c6de938400f8c5a3fe7369c8f) Thanks @IMax153! - Ensure `ConfigError` is surfaced by the CLI when a fallback `Config` fails to parse + +- Updated dependencies [[`a8b0ddb`](https://github.com/Effect-TS/effect/commit/a8b0ddb84710054799fc8f57485b95d00093ada1), [`8653072`](https://github.com/Effect-TS/effect/commit/86530720d7a03e118d2c5a8bf5a997cee7e7f3d6), [`507d546`](https://github.com/Effect-TS/effect/commit/507d546bd49db31000425fb5da88c434e4291bea), [`a8b0ddb`](https://github.com/Effect-TS/effect/commit/a8b0ddb84710054799fc8f57485b95d00093ada1), [`8db239b`](https://github.com/Effect-TS/effect/commit/8db239b9c869a3707f6566b9d9dbdf53c4df03fc), [`84a0911`](https://github.com/Effect-TS/effect/commit/84a091181634c3a022c94234cec7764a3aeef1be), [`84a0911`](https://github.com/Effect-TS/effect/commit/84a091181634c3a022c94234cec7764a3aeef1be), [`3179a9f`](https://github.com/Effect-TS/effect/commit/3179a9f65d23369a6a9a1f80f7750566dd28df22), [`6cb9b76`](https://github.com/Effect-TS/effect/commit/6cb9b766396d0b2ed995cf26957359713efd202e), [`1fcbe55`](https://github.com/Effect-TS/effect/commit/1fcbe55345042d8468f6a98c84081bd00b6bcf5a), [`d9a63d9`](https://github.com/Effect-TS/effect/commit/d9a63d9d385653865954cac895065360d54cc56b), [`6cb9b76`](https://github.com/Effect-TS/effect/commit/6cb9b766396d0b2ed995cf26957359713efd202e)]: + - effect@3.12.5 + - @effect/platform@0.74.0 + - @effect/printer@0.40.5 + - @effect/printer-ansi@0.40.5 + +## 0.52.1 + +### Patch Changes + +- Updated dependencies [[`5b50ea4`](https://github.com/Effect-TS/effect/commit/5b50ea4a10cf9acd51f9624b2474d9d5ded74019), [`c170a68`](https://github.com/Effect-TS/effect/commit/c170a68b6266100774461fcd6c0e0fabb60112f2), [`a66c2eb`](https://github.com/Effect-TS/effect/commit/a66c2eb473245092cd41f04c2eb2b7b02cf53718), [`c9e5e1b`](https://github.com/Effect-TS/effect/commit/c9e5e1be17c0c84d3d4e2abc3c60215cdb56bbbe), [`7b3d58d`](https://github.com/Effect-TS/effect/commit/7b3d58d7aec2152ec282460871d3e9de45ed254d)]: + - effect@3.12.4 + - @effect/platform@0.73.1 + - @effect/printer@0.40.4 + - @effect/printer-ansi@0.40.4 + +## 0.52.0 + +### Patch Changes + +- Updated dependencies [[`d7dac48`](https://github.com/Effect-TS/effect/commit/d7dac48a477cdfeec509dbe9f33fce6a1b02b63d), [`c110032`](https://github.com/Effect-TS/effect/commit/c110032322450a8824ba38ae24335a538cd2ce9a), [`1d7fd2b`](https://github.com/Effect-TS/effect/commit/1d7fd2b7ee8eeecc912d27adf76ed897db236dc5), [`1d7fd2b`](https://github.com/Effect-TS/effect/commit/1d7fd2b7ee8eeecc912d27adf76ed897db236dc5), [`23ac740`](https://github.com/Effect-TS/effect/commit/23ac740c7dd4610b7d265c2071b88b0968419e9a), [`8cd7319`](https://github.com/Effect-TS/effect/commit/8cd7319b6568bfc7a30ca16c104d189e37eac3a0)]: + - effect@3.12.3 + - @effect/platform@0.73.0 + - @effect/printer@0.40.3 + - @effect/printer-ansi@0.40.3 + +## 0.51.2 + +### Patch Changes + +- Updated dependencies [[`734af82`](https://github.com/Effect-TS/effect/commit/734af82138e78b9c57a8355b1c6b80e80d38b222), [`b63c780`](https://github.com/Effect-TS/effect/commit/b63c78010893101520448ddda7019c487cf7eedd), [`212e784`](https://github.com/Effect-TS/effect/commit/212e78475f527147ec27c090bd13f789f55add7a), [`f852cb0`](https://github.com/Effect-TS/effect/commit/f852cb02040ea2f165e9b449615b8b1366add5d5), [`7276ae2`](https://github.com/Effect-TS/effect/commit/7276ae21062896adbb7508ac5b2dece95316322f), [`212e784`](https://github.com/Effect-TS/effect/commit/212e78475f527147ec27c090bd13f789f55add7a), [`212e784`](https://github.com/Effect-TS/effect/commit/212e78475f527147ec27c090bd13f789f55add7a), [`212e784`](https://github.com/Effect-TS/effect/commit/212e78475f527147ec27c090bd13f789f55add7a), [`c640d77`](https://github.com/Effect-TS/effect/commit/c640d77b33ad417876f4e8ffe8574ee6cbe5607f), [`0def088`](https://github.com/Effect-TS/effect/commit/0def0887cfdb6755729a64dfd52b3b9f46b0576c)]: + - effect@3.12.2 + - @effect/platform@0.72.2 + - @effect/printer@0.40.2 + - @effect/printer-ansi@0.40.2 + +## 0.51.1 + +### Patch Changes + +- Updated dependencies [[`302b57d`](https://github.com/Effect-TS/effect/commit/302b57d2cbf9b9ccc17450945aeebfb33cfe8d43), [`0988083`](https://github.com/Effect-TS/effect/commit/0988083d4594938590df5a287e5b27d38526dd07), [`8b46be6`](https://github.com/Effect-TS/effect/commit/8b46be6a3b8160362ab5ea9171c5e6932505125c), [`bfe8027`](https://github.com/Effect-TS/effect/commit/bfe802734b450a4b4ee069d1125dd37995db2bff), [`16dd657`](https://github.com/Effect-TS/effect/commit/16dd657033d8afac2ffea567b3c8bb27c9b249b6), [`39db211`](https://github.com/Effect-TS/effect/commit/39db211414e90c8db8fdad7dc8ce5b4661bcfaef)]: + - effect@3.12.1 + - @effect/platform@0.72.1 + - @effect/printer@0.40.1 + - @effect/printer-ansi@0.40.1 + +## 0.51.0 + +### Patch Changes + +- Updated dependencies [[`abb22a4`](https://github.com/Effect-TS/effect/commit/abb22a429b9c52c31e84856294f175d2064a9b4d), [`f369a89`](https://github.com/Effect-TS/effect/commit/f369a89e98bc682969803b9304adaf4557bb36c2), [`642376c`](https://github.com/Effect-TS/effect/commit/642376c63fd7d78754db991631a4d50a5dc79aa3), [`3d2b7a7`](https://github.com/Effect-TS/effect/commit/3d2b7a7e942a7157afae5b1cdbc6f3fef116428e), [`ef64c6f`](https://github.com/Effect-TS/effect/commit/ef64c6fec0d47da573c04230dde9ea729366d871), [`73f9c6f`](https://github.com/Effect-TS/effect/commit/73f9c6f2ff091512cf904cc54ab59965b86e87c8), [`17cb451`](https://github.com/Effect-TS/effect/commit/17cb4514590e8a86263f7aed009f24da8a237342), [`d801820`](https://github.com/Effect-TS/effect/commit/d80182060c2ee945d7e0e4728812abf9465a0d6a), [`e1eeb2d`](https://github.com/Effect-TS/effect/commit/e1eeb2d7064b3870041dab142f3057970699bbf1), [`c11f3a6`](https://github.com/Effect-TS/effect/commit/c11f3a60a05c3b5fc8e7ce90136728154dc505b0), [`618f7e0`](https://github.com/Effect-TS/effect/commit/618f7e092a1011e5090dca1e69b5e9285689654b), [`c0ba834`](https://github.com/Effect-TS/effect/commit/c0ba834d1995cf5a8b250e4780fd43f3e3881151), [`e1eeb2d`](https://github.com/Effect-TS/effect/commit/e1eeb2d7064b3870041dab142f3057970699bbf1)]: + - effect@3.12.0 + - @effect/platform@0.72.0 + - @effect/printer@0.40.0 + - @effect/printer-ansi@0.40.0 + +## 0.50.7 + +### Patch Changes + +- Updated dependencies [[`39457d4`](https://github.com/Effect-TS/effect/commit/39457d4897d9bc7df8af5c05d352866bbeae82eb), [`a475cc2`](https://github.com/Effect-TS/effect/commit/a475cc25fd7c9f26b27a8e98f8fbe43cc9e6ee3e), [`199214e`](https://github.com/Effect-TS/effect/commit/199214e21c616d8a0ccd7ed5f92e944e6c580193), [`b3c160d`](https://github.com/Effect-TS/effect/commit/b3c160d7a1fdfc2d3fb2440530f1ab80efc65133)]: + - effect@3.11.10 + - @effect/platform@0.71.7 + - @effect/printer@0.39.10 + - @effect/printer-ansi@0.39.10 + +## 0.50.6 + +### Patch Changes + +- Updated dependencies [[`1c08a0b`](https://github.com/Effect-TS/effect/commit/1c08a0b8505badcffb4d9cade5a746ea90c9557e), [`1ce703b`](https://github.com/Effect-TS/effect/commit/1ce703b041bbd7560c5c437c9b9be48f027937fd), [`1ce703b`](https://github.com/Effect-TS/effect/commit/1ce703b041bbd7560c5c437c9b9be48f027937fd)]: + - effect@3.11.9 + - @effect/platform@0.71.6 + - @effect/printer@0.39.9 + - @effect/printer-ansi@0.39.9 + +## 0.50.5 + +### Patch Changes + +- Updated dependencies [[`05d71f8`](https://github.com/Effect-TS/effect/commit/05d71f85622305705d8316817694a09762e60865), [`e66b920`](https://github.com/Effect-TS/effect/commit/e66b9205f25ab425d30640886eb3fb2c4715bc26)]: + - @effect/platform@0.71.5 + +## 0.50.4 + +### Patch Changes + +- Updated dependencies [[`909181a`](https://github.com/Effect-TS/effect/commit/909181a9ce9052a80432ccf52187e0723004bf7f), [`909181a`](https://github.com/Effect-TS/effect/commit/909181a9ce9052a80432ccf52187e0723004bf7f), [`1a6b52d`](https://github.com/Effect-TS/effect/commit/1a6b52dcf020d36e38a7bc90b648152cf5a8ccba)]: + - @effect/platform@0.71.4 + - effect@3.11.8 + - @effect/printer@0.39.8 + - @effect/printer-ansi@0.39.8 + +## 0.50.3 + +### Patch Changes + +- Updated dependencies [[`6984508`](https://github.com/Effect-TS/effect/commit/6984508c87f1bd91213b44c19b25ab5e2dcc1ce0), [`883639c`](https://github.com/Effect-TS/effect/commit/883639cc8ce47757f1cd39439391a8028c0812fe)]: + - @effect/platform@0.71.3 + +## 0.50.2 + +### Patch Changes + +- Updated dependencies [[`2408616`](https://github.com/Effect-TS/effect/commit/24086163b60b09cc6d0885bd565ef080dcbe866b), [`cec0b4d`](https://github.com/Effect-TS/effect/commit/cec0b4d152ef660be2ccdb0927255f2471436e6e), [`cec0b4d`](https://github.com/Effect-TS/effect/commit/cec0b4d152ef660be2ccdb0927255f2471436e6e), [`8d978c5`](https://github.com/Effect-TS/effect/commit/8d978c53f6fcc98d9d645ecba3e4b55d4297dd36), [`cec0b4d`](https://github.com/Effect-TS/effect/commit/cec0b4d152ef660be2ccdb0927255f2471436e6e), [`cec0b4d`](https://github.com/Effect-TS/effect/commit/cec0b4d152ef660be2ccdb0927255f2471436e6e)]: + - effect@3.11.7 + - @effect/platform@0.71.2 + - @effect/printer@0.39.7 + - @effect/printer-ansi@0.39.7 + +## 0.50.1 + +### Patch Changes + +- Updated dependencies [[`1d3df5b`](https://github.com/Effect-TS/effect/commit/1d3df5bc4324e88a392c348db35fd9d029c7b25e)]: + - @effect/platform@0.71.1 + +## 0.50.0 + +### Patch Changes + +- Updated dependencies [[`662d1ce`](https://github.com/Effect-TS/effect/commit/662d1ce6fb7da384a95888d5b2bb5605bdf3208d), [`c99a0f3`](https://github.com/Effect-TS/effect/commit/c99a0f376d049d3793ed33e146d9873b8a5e5b78), [`11fc401`](https://github.com/Effect-TS/effect/commit/11fc401f436f99bf4be95f56d50b0e4bdfe5edea), [`c99a0f3`](https://github.com/Effect-TS/effect/commit/c99a0f376d049d3793ed33e146d9873b8a5e5b78), [`31c62d8`](https://github.com/Effect-TS/effect/commit/31c62d83cbdcf9850a8b5331faa239601c60f78a)]: + - effect@3.11.6 + - @effect/platform@0.71.0 + - @effect/printer@0.39.6 + - @effect/printer-ansi@0.39.6 + +## 0.49.7 + +### Patch Changes + +- Updated dependencies [[`9f5a6f7`](https://github.com/Effect-TS/effect/commit/9f5a6f701bf7ba31adccd1f1bcfa8ab5614c9be8), [`22905cf`](https://github.com/Effect-TS/effect/commit/22905cf5addfb1ff3d2a6135c52036be958ae911), [`9f5a6f7`](https://github.com/Effect-TS/effect/commit/9f5a6f701bf7ba31adccd1f1bcfa8ab5614c9be8), [`1e59e4f`](https://github.com/Effect-TS/effect/commit/1e59e4fd778da18296812a2a32f36ca8ae50f60d), [`9f5a6f7`](https://github.com/Effect-TS/effect/commit/9f5a6f701bf7ba31adccd1f1bcfa8ab5614c9be8), [`8d914e5`](https://github.com/Effect-TS/effect/commit/8d914e504e7a22d0ea628e8af265ee450ff9530f), [`03bb00f`](https://github.com/Effect-TS/effect/commit/03bb00faa74f9e168a54a8cc0828a664fbb1ab05), [`9f5a6f7`](https://github.com/Effect-TS/effect/commit/9f5a6f701bf7ba31adccd1f1bcfa8ab5614c9be8), [`14e1149`](https://github.com/Effect-TS/effect/commit/14e1149f1af5a022f06eb8c2e4ba9fec17fe7426), [`9f5a6f7`](https://github.com/Effect-TS/effect/commit/9f5a6f701bf7ba31adccd1f1bcfa8ab5614c9be8), [`9f5a6f7`](https://github.com/Effect-TS/effect/commit/9f5a6f701bf7ba31adccd1f1bcfa8ab5614c9be8)]: + - effect@3.11.5 + - @effect/platform@0.70.7 + - @effect/printer@0.39.5 + - @effect/printer-ansi@0.39.5 + +## 0.49.6 + +### Patch Changes + +- Updated dependencies [[`9a5b8e3`](https://github.com/Effect-TS/effect/commit/9a5b8e36d184bd4967a88752cb6e755e1be263af)]: + - @effect/platform@0.70.6 + +## 0.49.5 + +### Patch Changes + +- Updated dependencies [[`415f4c9`](https://github.com/Effect-TS/effect/commit/415f4c98321868531727a83cbaad70164f5e4c40), [`518b258`](https://github.com/Effect-TS/effect/commit/518b258a8a67ecd332a9252c35cc060f8368dee2), [`6e323a3`](https://github.com/Effect-TS/effect/commit/6e323a36faaee46b328c8e3cf60a76b3aff9907f), [`518b258`](https://github.com/Effect-TS/effect/commit/518b258a8a67ecd332a9252c35cc060f8368dee2), [`6e323a3`](https://github.com/Effect-TS/effect/commit/6e323a36faaee46b328c8e3cf60a76b3aff9907f)]: + - @effect/platform@0.70.5 + - effect@3.11.4 + - @effect/printer@0.39.4 + - @effect/printer-ansi@0.39.4 + +## 0.49.4 + +### Patch Changes + +- Updated dependencies [[`90906f7`](https://github.com/Effect-TS/effect/commit/90906f7f154b12c7182e8f39e3c55ef3937db857), [`3862cd3`](https://github.com/Effect-TS/effect/commit/3862cd3c7f6a542ed65fb81255b3bd696ce2f567), [`3862cd3`](https://github.com/Effect-TS/effect/commit/3862cd3c7f6a542ed65fb81255b3bd696ce2f567), [`343b6aa`](https://github.com/Effect-TS/effect/commit/343b6aa6ac4a74276bfc7c63ccbf4a1d72bc1bed), [`afba339`](https://github.com/Effect-TS/effect/commit/afba339adc11dad56b5a3b7ca94487e58f34d613)]: + - effect@3.11.3 + - @effect/platform@0.70.4 + - @effect/printer@0.39.3 + - @effect/printer-ansi@0.39.3 + +## 0.49.3 + +### Patch Changes + +- Updated dependencies [[`7044730`](https://github.com/Effect-TS/effect/commit/70447306be1aeeb7d87c230b2a96ec87b993ede9)]: + - @effect/platform@0.70.3 + +## 0.49.2 + +### Patch Changes + +- Updated dependencies [[`01cee56`](https://github.com/Effect-TS/effect/commit/01cee560b58d94b24cc20e98083251b73e658b41), [`c2249ea`](https://github.com/Effect-TS/effect/commit/c2249ea13fd98ab7d9aa628787931356d8ec2860), [`1358aa5`](https://github.com/Effect-TS/effect/commit/1358aa5326eaa85ef13ee8d1fed0b4a4288ed3eb), [`1de3fe7`](https://github.com/Effect-TS/effect/commit/1de3fe7d1cbafd6391eaa38c2300b99e332cc2aa)]: + - effect@3.11.2 + - @effect/platform@0.70.2 + - @effect/printer@0.39.2 + - @effect/printer-ansi@0.39.2 + +## 0.49.1 + +### Patch Changes + +- Updated dependencies [[`dd8a2d8`](https://github.com/Effect-TS/effect/commit/dd8a2d8e80d33b16719fc69361eaedf0b59d4620), [`a71bfef`](https://github.com/Effect-TS/effect/commit/a71bfef46f5061bb2502a61a333638a987b62273)]: + - effect@3.11.1 + - @effect/platform@0.70.1 + - @effect/printer@0.39.1 + - @effect/printer-ansi@0.39.1 + +## 0.49.0 + +### Patch Changes + +- Updated dependencies [[`147434b`](https://github.com/Effect-TS/effect/commit/147434b03d5e1fd692dd9f126e5ab0910f3b76d3), [`6e69493`](https://github.com/Effect-TS/effect/commit/6e694930048bbaf98110f35f41566aeb9752d471), [`147434b`](https://github.com/Effect-TS/effect/commit/147434b03d5e1fd692dd9f126e5ab0910f3b76d3), [`5eff3f6`](https://github.com/Effect-TS/effect/commit/5eff3f6fa3aae7e86948a62cbfd63b8d6c3bdf92), [`d9fe79b`](https://github.com/Effect-TS/effect/commit/d9fe79bb5a3fe105d8e7a3bc2922a8ad936a5d10), [`251d189`](https://github.com/Effect-TS/effect/commit/251d189420bbba71990574e91098c499065f9a9b), [`5a259f3`](https://github.com/Effect-TS/effect/commit/5a259f3711b4369f55d885b568bdb21136155261), [`b4ce4ea`](https://github.com/Effect-TS/effect/commit/b4ce4ea7fd514a7e572f2dcd879c98f334981b0e), [`15fcc5a`](https://github.com/Effect-TS/effect/commit/15fcc5a0ea4bbf40ab48fa6a04fdda74f76f4c07), [`9bc9a47`](https://github.com/Effect-TS/effect/commit/9bc9a476800dc645903c888a68bb1d3baa3383c6), [`aadb8a4`](https://github.com/Effect-TS/effect/commit/aadb8a48d2cba197c06ec9996505510e48e4e5cb), [`1e2747c`](https://github.com/Effect-TS/effect/commit/1e2747c63a4820d1459cbbc88c71212983bd68bd), [`9264162`](https://github.com/Effect-TS/effect/commit/9264162a82783a651776fb7b87604564a63e7070), [`e0b9b09`](https://github.com/Effect-TS/effect/commit/e0b9b09e70c386b2da17d1f0a15b0511861c89e8), [`c36f3b9`](https://github.com/Effect-TS/effect/commit/c36f3b95df5ce9d71b66f22f26ce12eda8d3e848), [`672bde5`](https://github.com/Effect-TS/effect/commit/672bde5bec51c7d6f9862828e6a654cb2cb6f93d), [`aadb8a4`](https://github.com/Effect-TS/effect/commit/aadb8a48d2cba197c06ec9996505510e48e4e5cb)]: + - effect@3.11.0 + - @effect/platform@0.70.0 + - @effect/printer@0.39.0 + - @effect/printer-ansi@0.39.0 + +## 0.48.32 + +### Patch Changes + +- Updated dependencies [[`3069614`](https://github.com/Effect-TS/effect/commit/30696149271129fc618f6f2ccd1d8f2f6c0f9cd7), [`09a5e52`](https://github.com/Effect-TS/effect/commit/09a5e522fd9b221f05d85b1d1c8a740d4973c302)]: + - effect@3.10.20 + - @effect/platform@0.69.32 + - @effect/printer@0.38.20 + - @effect/printer-ansi@0.38.20 + +## 0.48.31 + +### Patch Changes + +- Updated dependencies [[`e6d4a37`](https://github.com/Effect-TS/effect/commit/e6d4a37c1d7e657b5ea44063a1cf586808228fe5)]: + - @effect/platform@0.69.31 + +## 0.48.30 + +### Patch Changes + +- Updated dependencies [[`270f199`](https://github.com/Effect-TS/effect/commit/270f199b31810fd643e4c22818698adcbdb5d396)]: + - @effect/platform@0.69.30 + +## 0.48.29 + +### Patch Changes + +- Updated dependencies [[`24cc35e`](https://github.com/Effect-TS/effect/commit/24cc35e26d6ed4a076470bc687ffd99cc50991b3)]: + - @effect/platform@0.69.29 + +## 0.48.28 + +### Patch Changes + +- [#4007](https://github.com/Effect-TS/effect/pull/4007) [`944025b`](https://github.com/Effect-TS/effect/commit/944025bc5ce139f4a85846aa689bf30ec06a8ec1) Thanks @gcanti! - Wrap JSDoc @example tags with a TypeScript fence, closes #4002 + +- Updated dependencies [[`edd72be`](https://github.com/Effect-TS/effect/commit/edd72be57b904d60c9cbffc2537901821a9da537), [`a3e2771`](https://github.com/Effect-TS/effect/commit/a3e277170a1f7cf61fd629acb60304c7e81d9498), [`944025b`](https://github.com/Effect-TS/effect/commit/944025bc5ce139f4a85846aa689bf30ec06a8ec1), [`54addee`](https://github.com/Effect-TS/effect/commit/54addee438a644bf010646c52042c7b89c5fc0a7), [`a9e00e4`](https://github.com/Effect-TS/effect/commit/a9e00e43f0b5dd22c1f9d5b78be6383daea09c20)]: + - @effect/platform@0.69.28 + - @effect/printer@0.38.19 + - effect@3.10.19 + - @effect/printer-ansi@0.38.19 + +## 0.48.27 + +### Patch Changes + +- Updated dependencies [[`af409cf`](https://github.com/Effect-TS/effect/commit/af409cf1d2ff973be11cc079ea373eaeedca25de), [`beaccae`](https://github.com/Effect-TS/effect/commit/beaccae2d15931e9fe475fb50a0b3638243fe3f7)]: + - effect@3.10.18 + - @effect/platform@0.69.27 + - @effect/printer@0.38.18 + - @effect/printer-ansi@0.38.18 + +## 0.48.26 + +### Patch Changes + +- [#3992](https://github.com/Effect-TS/effect/pull/3992) [`a1cf616`](https://github.com/Effect-TS/effect/commit/a1cf6167687dab5070bc944c2fef48269275014e) Thanks @IMax153! - fix multi-select prompt not displaying choice description + +- Updated dependencies [[`c963886`](https://github.com/Effect-TS/effect/commit/c963886d5817986fcbd6bfa4ddf50aca8b6c8184), [`42c4ce6`](https://github.com/Effect-TS/effect/commit/42c4ce6f8d8c7d847e97757650a8ad9419a829d7)]: + - @effect/platform@0.69.26 + - effect@3.10.17 + - @effect/printer@0.38.17 + - @effect/printer-ansi@0.38.17 + +## 0.48.25 + +### Patch Changes + +- [#3929](https://github.com/Effect-TS/effect/pull/3929) [`71f7d4e`](https://github.com/Effect-TS/effect/commit/71f7d4ecb944dbc50b9a5d1491b2e34075faec1b) Thanks @joepjoosten! - added prompt multi select + +- [#3976](https://github.com/Effect-TS/effect/pull/3976) [`a36a250`](https://github.com/Effect-TS/effect/commit/a36a25069fbf47d30046df1deef8468dfb9db7a8) Thanks @IMax153! - Ensure that command-line arguments are parsed correctly for prompt-based commands + +- Updated dependencies [[`320557a`](https://github.com/Effect-TS/effect/commit/320557ab18d13c5e22fc7dc0d2a157eae461012f), [`4dca30c`](https://github.com/Effect-TS/effect/commit/4dca30cfcdafe4542e236489f71d6f171a5b4e38), [`1d99867`](https://github.com/Effect-TS/effect/commit/1d998671be3cd11043f232822e91dd8c98fccfa9), [`6dae414`](https://github.com/Effect-TS/effect/commit/6dae4147991a97ec14a99289bd25fadae7541e8d), [`6b0d737`](https://github.com/Effect-TS/effect/commit/6b0d737078bf63b97891e6bc47affc04b28f9cf7), [`d8356aa`](https://github.com/Effect-TS/effect/commit/d8356aad428a0c2290db52380220f81d9ec94232), [`7b93dd6`](https://github.com/Effect-TS/effect/commit/7b93dd622e2ab79c7072d79d0d9611e446202201)]: + - @effect/platform@0.69.25 + - effect@3.10.16 + - @effect/printer@0.38.16 + - @effect/printer-ansi@0.38.16 + +## 0.48.24 + +### Patch Changes + +- Updated dependencies [[`3cc6514`](https://github.com/Effect-TS/effect/commit/3cc6514d2dd64e010cb760cc29bfce98c349bb10)]: + - @effect/platform@0.69.24 + +## 0.48.23 + +### Patch Changes + +- Updated dependencies [[`3aff4d3`](https://github.com/Effect-TS/effect/commit/3aff4d38837c213bb2987973dc4b98febb9f92d2)]: + - @effect/platform@0.69.23 + +## 0.48.22 + +### Patch Changes + +- Updated dependencies [[`8398b32`](https://github.com/Effect-TS/effect/commit/8398b3208242a88239d4449910b7baf923cfe3b6), [`72e55b7`](https://github.com/Effect-TS/effect/commit/72e55b7c610784fcebdbadc592c876e23e76a986)]: + - effect@3.10.15 + - @effect/platform@0.69.22 + - @effect/printer@0.38.15 + - @effect/printer-ansi@0.38.15 + +## 0.48.21 + +### Patch Changes + +- Updated dependencies [[`f983946`](https://github.com/Effect-TS/effect/commit/f9839467b4cad6e788297764ef9f9f0b9fd203f9), [`2d8a750`](https://github.com/Effect-TS/effect/commit/2d8a75081eb83a0a81f817fdf6f428369c5064ab)]: + - effect@3.10.14 + - @effect/platform@0.69.21 + - @effect/printer@0.38.14 + - @effect/printer-ansi@0.38.14 + +## 0.48.20 + +### Patch Changes + +- Updated dependencies [[`995bbdf`](https://github.com/Effect-TS/effect/commit/995bbdffea2e332f203cd5b474cd6a1c77dfa6ae)]: + - effect@3.10.13 + - @effect/platform@0.69.20 + - @effect/printer@0.38.13 + - @effect/printer-ansi@0.38.13 + +## 0.48.19 + +### Patch Changes + +- Updated dependencies [[`eb8c52d`](https://github.com/Effect-TS/effect/commit/eb8c52d8b4c5e067ebf0a81eb742f5822e6439b5)]: + - @effect/platform@0.69.19 + +## 0.48.18 + +### Patch Changes + +- Updated dependencies [[`a0584ec`](https://github.com/Effect-TS/effect/commit/a0584ece92ed784bfb139e9c5a699f02d1e71c2d), [`dd14efe`](https://github.com/Effect-TS/effect/commit/dd14efe0ace255f571273aae876adea96267d7e6), [`dd14efe`](https://github.com/Effect-TS/effect/commit/dd14efe0ace255f571273aae876adea96267d7e6)]: + - @effect/platform@0.69.18 + - effect@3.10.12 + - @effect/printer@0.38.12 + - @effect/printer-ansi@0.38.12 + +## 0.48.17 + +### Patch Changes + +- Updated dependencies [[`5eef499`](https://github.com/Effect-TS/effect/commit/5eef4998b6ccb7a5404d9e4fef85e57fa35fbb8a), [`8240b1c`](https://github.com/Effect-TS/effect/commit/8240b1c10d45312fc863cb679b1a1e8441af0c1a), [`5eef499`](https://github.com/Effect-TS/effect/commit/5eef4998b6ccb7a5404d9e4fef85e57fa35fbb8a)]: + - effect@3.10.11 + - @effect/platform@0.69.17 + - @effect/printer@0.38.11 + - @effect/printer-ansi@0.38.11 + +## 0.48.16 + +### Patch Changes + +- Updated dependencies [[`cd720ae`](https://github.com/Effect-TS/effect/commit/cd720aedf7f2571edec0843d6a633e84e4832b28), [`cd720ae`](https://github.com/Effect-TS/effect/commit/cd720aedf7f2571edec0843d6a633e84e4832b28), [`cd720ae`](https://github.com/Effect-TS/effect/commit/cd720aedf7f2571edec0843d6a633e84e4832b28), [`b631f40`](https://github.com/Effect-TS/effect/commit/b631f40abbe649b2a089764585b5c39f6a695ac6), [`7d89650`](https://github.com/Effect-TS/effect/commit/7d8965036cd2ea435c8441ffec3345488baebf85)]: + - effect@3.10.10 + - @effect/platform@0.69.16 + - @effect/printer@0.38.10 + - @effect/printer-ansi@0.38.10 + +## 0.48.15 + +### Patch Changes + +- Updated dependencies [[`8a30e1d`](https://github.com/Effect-TS/effect/commit/8a30e1dfa3a7103bf5414fc6a7fca3088d8c8c00)]: + - @effect/platform@0.69.15 + +## 0.48.14 + +### Patch Changes + +- Updated dependencies [[`a123e80`](https://github.com/Effect-TS/effect/commit/a123e80f111a625428a5b5622b7f55ee1073566b), [`bd5fcd3`](https://github.com/Effect-TS/effect/commit/bd5fcd3e6b603b1e505af90d6a00627c8eca6d41), [`0289d3b`](https://github.com/Effect-TS/effect/commit/0289d3b6391031d00329365bab9791b355031fe3), [`7386b71`](https://github.com/Effect-TS/effect/commit/7386b710e5be570e17f468928a6ed19d549a3e12), [`07c493a`](https://github.com/Effect-TS/effect/commit/07c493a598e096c7810cd06def8cfa43493c46b1), [`257ab1b`](https://github.com/Effect-TS/effect/commit/257ab1b539fa6e930b7ae2583a188376372200d7), [`4211a23`](https://github.com/Effect-TS/effect/commit/4211a2355bb3af3f0e756e2aae9d293379f25662)]: + - effect@3.10.9 + - @effect/platform@0.69.14 + - @effect/printer@0.38.9 + - @effect/printer-ansi@0.38.9 + +## 0.48.13 + +### Patch Changes + +- Updated dependencies [[`68b5c9e`](https://github.com/Effect-TS/effect/commit/68b5c9e44f34192cef26e1cadda5e661a027df41), [`9c9928d`](https://github.com/Effect-TS/effect/commit/9c9928dfeacd9ac33dc37eb0ca3d7d8c39175ada), [`6306e66`](https://github.com/Effect-TS/effect/commit/6306e6656092b350d4ede5746da6f245ec9f7e07), [`361c7f3`](https://github.com/Effect-TS/effect/commit/361c7f39a2c10ede9324847c3d3ba192a6f9b20a)]: + - effect@3.10.8 + - @effect/platform@0.69.13 + - @effect/printer@0.38.8 + - @effect/printer-ansi@0.38.8 + +## 0.48.12 + +### Patch Changes + +- Updated dependencies [[`33f5b9f`](https://github.com/Effect-TS/effect/commit/33f5b9ffaebea4f1bd0e391b44c41fb6230e743a), [`50f0281`](https://github.com/Effect-TS/effect/commit/50f0281b0d2116726b8927a6217622d5f394f3e4)]: + - effect@3.10.7 + - @effect/platform@0.69.12 + - @effect/printer@0.38.7 + - @effect/printer-ansi@0.38.7 + +## 0.48.11 + +### Patch Changes + +- Updated dependencies [[`ce1c21f`](https://github.com/Effect-TS/effect/commit/ce1c21ffc11902ac9ab453a51904207859d38552), [`81ddd45`](https://github.com/Effect-TS/effect/commit/81ddd45fc074b98206fafab416d9a5a28b31e07a)]: + - effect@3.10.6 + - @effect/platform@0.69.11 + - @effect/printer@0.38.6 + - @effect/printer-ansi@0.38.6 + +## 0.48.10 + +### Patch Changes + +- Updated dependencies [[`3a6d757`](https://github.com/Effect-TS/effect/commit/3a6d757badeebe00d8ef4d67530d073c8264dcfa), [`59d813a`](https://github.com/Effect-TS/effect/commit/59d813aa4973d1115cfc70cc3667508335f49693)]: + - effect@3.10.5 + - @effect/platform@0.69.10 + - @effect/printer@0.38.5 + - @effect/printer-ansi@0.38.5 + +## 0.48.9 + +### Patch Changes + +- Updated dependencies [[`2367708`](https://github.com/Effect-TS/effect/commit/2367708be449f9526a2047e321302d7bfb16f18e)]: + - @effect/platform@0.69.9 + - effect@3.10.4 + - @effect/printer@0.38.4 + - @effect/printer-ansi@0.38.4 + +## 0.48.8 + +### Patch Changes + +- Updated dependencies [[`522f7c5`](https://github.com/Effect-TS/effect/commit/522f7c518a5acfb55ef96d6796869f002cc3eaf8)]: + - @effect/platform@0.69.8 + +## 0.48.7 + +### Patch Changes + +- Updated dependencies [[`690d6c5`](https://github.com/Effect-TS/effect/commit/690d6c54d2145adb0af545c447db7d4755bf3c6b), [`b9423d8`](https://github.com/Effect-TS/effect/commit/b9423d8bf8181a2389fdbce1e3c14ac6fe8d54f5), [`279fe3a`](https://github.com/Effect-TS/effect/commit/279fe3a7168fe84e520c2cc88ba189a15f03a2bc)]: + - @effect/platform@0.69.7 + - effect@3.10.3 + - @effect/printer@0.38.3 + - @effect/printer-ansi@0.38.3 + +## 0.48.6 + +### Patch Changes + +- Updated dependencies [[`714e119`](https://github.com/Effect-TS/effect/commit/714e11945e45e5a2554ee058e6c43f82a8e309cf), [`c1afd55`](https://github.com/Effect-TS/effect/commit/c1afd55c54e61f9c432823d21b3d016f79160a37), [`42cd72a`](https://github.com/Effect-TS/effect/commit/42cd72a44ca9593e4d81fbb50e8111625fd0fb81)]: + - effect@3.10.2 + - @effect/platform@0.69.6 + - @effect/printer@0.38.2 + - @effect/printer-ansi@0.38.2 + +## 0.48.5 + +### Patch Changes + +- Updated dependencies [[`9604d6b`](https://github.com/Effect-TS/effect/commit/9604d6b616435103dafea8b53637a9d1450b4750)]: + - effect@3.10.1 + - @effect/platform@0.69.5 + - @effect/printer@0.38.1 + - @effect/printer-ansi@0.38.1 + +## 0.48.4 + +### Patch Changes + +- Updated dependencies [[`c86b1d7`](https://github.com/Effect-TS/effect/commit/c86b1d7cd47b66df190ef9775a475467c1abdbd6)]: + - @effect/platform@0.69.4 + +## 0.48.3 + +### Patch Changes + +- [#3773](https://github.com/Effect-TS/effect/pull/3773) [`a619704`](https://github.com/Effect-TS/effect/commit/a619704fd4bdaf0dc948e32601577983e355c0fe) Thanks @joepjoosten! - Repeated options can now be parsed independend of the order of the arguments + +- Updated dependencies [[`d5fba63`](https://github.com/Effect-TS/effect/commit/d5fba6391e1005e374aa0238f13edfbd65848313), [`1eb2c30`](https://github.com/Effect-TS/effect/commit/1eb2c30ba064398db5790e376dedcfad55b7b005), [`02d413e`](https://github.com/Effect-TS/effect/commit/02d413e7b6bc1c64885969c37cc3e4e690c94d7d)]: + - @effect/platform@0.69.3 + +## 0.48.2 + +### Patch Changes + +- Updated dependencies [[`e7afc47`](https://github.com/Effect-TS/effect/commit/e7afc47ce83e381c3f4aed2b2974e3b3d86a2340)]: + - @effect/platform@0.69.2 + +## 0.48.1 + +### Patch Changes + +- Updated dependencies [[`7564f56`](https://github.com/Effect-TS/effect/commit/7564f56bb2844cf39d2b0d2d9e93cf9b2205e9a8), [`7564f56`](https://github.com/Effect-TS/effect/commit/7564f56bb2844cf39d2b0d2d9e93cf9b2205e9a8)]: + - @effect/platform@0.69.1 + +## 0.48.0 + +### Patch Changes + +- Updated dependencies [[`4a01828`](https://github.com/Effect-TS/effect/commit/4a01828b66d6213e9bbe18979c893b13f7bb29bf), [`6d9de6b`](https://github.com/Effect-TS/effect/commit/6d9de6b871c5c08e6509a4e830c3d74758faa198), [`4a01828`](https://github.com/Effect-TS/effect/commit/4a01828b66d6213e9bbe18979c893b13f7bb29bf), [`c79c4c1`](https://github.com/Effect-TS/effect/commit/c79c4c178390fe61ff6dda88c9e058862349343a), [`38d30f0`](https://github.com/Effect-TS/effect/commit/38d30f08b8da62f9c3e308b9250738cb8d17bdb5), [`5821ce3`](https://github.com/Effect-TS/effect/commit/5821ce3455b47d25e0a40cae6ce22af9db5fa556)]: + - effect@3.10.0 + - @effect/platform@0.69.0 + - @effect/printer@0.38.0 + - @effect/printer-ansi@0.38.0 + +## 0.47.6 + +### Patch Changes + +- Updated dependencies [[`382556f`](https://github.com/Effect-TS/effect/commit/382556f8930780c0634de681077706113a8c8239), [`97cb014`](https://github.com/Effect-TS/effect/commit/97cb0145114b2cd2f378e98f6c4ff5bf2c1865f5)]: + - @effect/schema@0.75.5 + - @effect/platform@0.68.6 + +## 0.47.5 + +### Patch Changes + +- Updated dependencies [[`2036402`](https://github.com/Effect-TS/effect/commit/20364020b8b75a684791aa93d90626758023e9e9)]: + - @effect/platform@0.68.5 + +## 0.47.4 + +### Patch Changes + +- Updated dependencies [[`1b1ef29`](https://github.com/Effect-TS/effect/commit/1b1ef29ae302322f69dc938f9337aa97b4c63266)]: + - @effect/platform@0.68.4 + +## 0.47.3 + +### Patch Changes + +- Updated dependencies [[`61a99b2`](https://github.com/Effect-TS/effect/commit/61a99b2bf9d757870ef0c2ec9d4c877cdd364a3d), [`8c33087`](https://github.com/Effect-TS/effect/commit/8c330879425e80bed2f65e407cd59e991f0d7bec)]: + - effect@3.9.2 + - @effect/platform@0.68.3 + - @effect/printer@0.37.2 + - @effect/printer-ansi@0.37.2 + - @effect/schema@0.75.4 + +## 0.47.2 + +### Patch Changes + +- Updated dependencies [[`360ec14`](https://github.com/Effect-TS/effect/commit/360ec14dd4102c526aef7433a8881ad4d9beab75)]: + - @effect/schema@0.75.3 + - @effect/platform@0.68.2 + +## 0.47.1 + +### Patch Changes + +- Updated dependencies [[`b75ac5d`](https://github.com/Effect-TS/effect/commit/b75ac5d0909115507bedc90f18f2d34deb217769)]: + - @effect/platform@0.68.1 + +## 0.47.0 + +### Patch Changes + +- Updated dependencies [[`f02b354`](https://github.com/Effect-TS/effect/commit/f02b354ab5b0451143b82bb73dc866be29adec85), [`90ceeab`](https://github.com/Effect-TS/effect/commit/90ceeab3a04051b740af18c8af8bd73ee8ec6363), [`90ceeab`](https://github.com/Effect-TS/effect/commit/90ceeab3a04051b740af18c8af8bd73ee8ec6363), [`90ceeab`](https://github.com/Effect-TS/effect/commit/90ceeab3a04051b740af18c8af8bd73ee8ec6363)]: + - @effect/schema@0.75.2 + - @effect/platform@0.68.0 + +## 0.46.1 + +### Patch Changes + +- Updated dependencies [[`3b2ad1d`](https://github.com/Effect-TS/effect/commit/3b2ad1d58a2e33dc1a72b7037396bd25ca1702a9)]: + - @effect/platform@0.67.1 + - effect@3.9.1 + - @effect/schema@0.75.1 + - @effect/printer@0.37.1 + - @effect/printer-ansi@0.37.1 + +## 0.46.0 + +### Patch Changes + +- Updated dependencies [[`ff3d1aa`](https://github.com/Effect-TS/effect/commit/ff3d1aab290b4d1173b2dfc7e4c76abb4babdc16), [`0ba66f2`](https://github.com/Effect-TS/effect/commit/0ba66f2451641fd6990e02ec1ed01c014db9dab0), [`bf77f51`](https://github.com/Effect-TS/effect/commit/bf77f51b323c383224ebf08adf77a7a6e8c9b3cd), [`016f9ad`](https://github.com/Effect-TS/effect/commit/016f9ad931a4b3d09a34e5caf13d87c5b8e9c984), [`0779681`](https://github.com/Effect-TS/effect/commit/07796813f07de035719728733096ba64ce333469), [`534129f`](https://github.com/Effect-TS/effect/commit/534129f8113ce1a8ec50828083e16da9c86326c6), [`d75140c`](https://github.com/Effect-TS/effect/commit/d75140c7a664ceda43142d999f4ff8dcd36d6dda), [`be0451c`](https://github.com/Effect-TS/effect/commit/be0451c149b6618af79cb839cdf04af2db1efb03), [`9237ac6`](https://github.com/Effect-TS/effect/commit/9237ac69bc07de5b3b60076a0ad2921c21de7457), [`be0451c`](https://github.com/Effect-TS/effect/commit/be0451c149b6618af79cb839cdf04af2db1efb03), [`5b36494`](https://github.com/Effect-TS/effect/commit/5b364942e9a9003fdb8217324f8a2d8369c969da), [`c716adb`](https://github.com/Effect-TS/effect/commit/c716adb250ebbea1d1048d818ef7fed4f621d186), [`4986391`](https://github.com/Effect-TS/effect/commit/49863919cd8628c962a712fb1df30d2983820933), [`d75140c`](https://github.com/Effect-TS/effect/commit/d75140c7a664ceda43142d999f4ff8dcd36d6dda), [`d1387ae`](https://github.com/Effect-TS/effect/commit/d1387aebd1ff01bbebde26be46d488956e4daef6)]: + - effect@3.9.0 + - @effect/platform@0.67.0 + - @effect/schema@0.75.0 + - @effect/printer@0.37.0 + - @effect/printer-ansi@0.37.0 + +## 0.45.3 + +### Patch Changes + +- Updated dependencies [[`88e85db`](https://github.com/Effect-TS/effect/commit/88e85db34bd402526e27a323e950d053fa34d232), [`f40da15`](https://github.com/Effect-TS/effect/commit/f40da15fbeb7c491840b8f409d47de79720891c3), [`8e94585`](https://github.com/Effect-TS/effect/commit/8e94585abe62753bf3af28bfae77926a7c570ac3), [`83887ca`](https://github.com/Effect-TS/effect/commit/83887ca1b1793916913d8550a4db4450cd14a044), [`5266b6c`](https://github.com/Effect-TS/effect/commit/5266b6cd86d76c3886da041c8829bca04b1a3110), [`cdead5c`](https://github.com/Effect-TS/effect/commit/cdead5c9cfd54dc6c4f215d9732f654c4a12e991), [`f40da15`](https://github.com/Effect-TS/effect/commit/f40da15fbeb7c491840b8f409d47de79720891c3), [`766a8af`](https://github.com/Effect-TS/effect/commit/766a8af307b414aca3648d91c4eab7493a5ec862)]: + - effect@3.8.5 + - @effect/platform@0.66.3 + - @effect/printer@0.36.5 + - @effect/printer-ansi@0.36.5 + - @effect/schema@0.74.2 + +## 0.45.2 + +### Patch Changes + +- Updated dependencies [[`734eae6`](https://github.com/Effect-TS/effect/commit/734eae654f215e4adca457d04d2a1728b1a55c83), [`fd83d0e`](https://github.com/Effect-TS/effect/commit/fd83d0e548feff9ea2d53d370a0b626c4a1d940e), [`4509656`](https://github.com/Effect-TS/effect/commit/45096569d50262275ee984f44c456f5c83b62683), [`ad7e1de`](https://github.com/Effect-TS/effect/commit/ad7e1de948745c0751bfdac96671028ff4b7a727), [`090e41c`](https://github.com/Effect-TS/effect/commit/090e41c636d720b1c7d89684a739855765ed4382), [`090e41c`](https://github.com/Effect-TS/effect/commit/090e41c636d720b1c7d89684a739855765ed4382)]: + - @effect/schema@0.74.1 + - @effect/platform@0.66.2 + - effect@3.8.4 + - @effect/printer@0.36.4 + - @effect/printer-ansi@0.36.4 + +## 0.45.1 + +### Patch Changes + +- Updated dependencies [[`3812788`](https://github.com/Effect-TS/effect/commit/3812788d79caaab8f559a62fd443018a04ac5647)]: + - @effect/platform@0.66.1 + +## 0.45.0 + +### Patch Changes + +- Updated dependencies [[`de48aa5`](https://github.com/Effect-TS/effect/commit/de48aa54e98d97722a8a4c2c8f9e1fe1d4560ea2)]: + - @effect/schema@0.74.0 + - @effect/platform@0.66.0 + +## 0.44.5 + +### Patch Changes + +- Updated dependencies [[`321b201`](https://github.com/Effect-TS/effect/commit/321b201adcb6bbbeb806b3467dd0b4cf063ccda8), [`bb5ec6b`](https://github.com/Effect-TS/effect/commit/bb5ec6b4b6a6f537394596c5a596faf52cb2aef4)]: + - @effect/platform@0.65.5 + - effect@3.8.3 + - @effect/printer@0.36.3 + - @effect/printer-ansi@0.36.3 + - @effect/schema@0.73.4 + +## 0.44.4 + +### Patch Changes + +- Updated dependencies [[`e6440a7`](https://github.com/Effect-TS/effect/commit/e6440a74fb3f12f6422ed794c07cb44af91cbacc)]: + - @effect/schema@0.73.3 + - @effect/platform@0.65.4 + +## 0.44.3 + +### Patch Changes + +- Updated dependencies [[`f0d8ef1`](https://github.com/Effect-TS/effect/commit/f0d8ef1ce97ec2a87b09b3e24150cfeab85d6e2f)]: + - effect@3.8.2 + - @effect/platform@0.65.3 + - @effect/printer@0.36.2 + - @effect/printer-ansi@0.36.2 + - @effect/schema@0.73.2 + +## 0.44.2 + +### Patch Changes + +- Updated dependencies [[`10bf621`](https://github.com/Effect-TS/effect/commit/10bf6213f36d8ddb00f058a4609b85220f3d8334), [`f56ab78`](https://github.com/Effect-TS/effect/commit/f56ab785cbee0c1c43bd2c182c35602f486f61f0), [`ae36fa6`](https://github.com/Effect-TS/effect/commit/ae36fa68f754eeab9a54b6dc0f8b44db513aa2b6)]: + - effect@3.8.1 + - @effect/schema@0.73.1 + - @effect/platform@0.65.2 + - @effect/printer@0.36.1 + - @effect/printer-ansi@0.36.1 + +## 0.44.1 + +### Patch Changes + +- Updated dependencies [[`e44c5f2`](https://github.com/Effect-TS/effect/commit/e44c5f228215738fe4e75023c7461bf9521249cb)]: + - @effect/platform@0.65.1 + +## 0.44.0 + +### Patch Changes + +- [#3541](https://github.com/Effect-TS/effect/pull/3541) [`aa1fa53`](https://github.com/Effect-TS/effect/commit/aa1fa5301e886b9657c8eb0d38cb87cef92a8305) Thanks @vinassefranche! - Add Number.round + +- Updated dependencies [[`fcfa6ee`](https://github.com/Effect-TS/effect/commit/fcfa6ee30ffd07d998bf22799357bf58580a116f), [`bb9931b`](https://github.com/Effect-TS/effect/commit/bb9931b62e249a3b801f2cb9d097aec0c8511af7), [`5798f76`](https://github.com/Effect-TS/effect/commit/5798f7619529de33e5ba06f551806f68fedc19db), [`5f0bfa1`](https://github.com/Effect-TS/effect/commit/5f0bfa17205398d4e4818bfbcf9e1b505b3b1fc5), [`7fdf9d9`](https://github.com/Effect-TS/effect/commit/7fdf9d9aa1e2c1c125cbf87991e6efbf4abb7b07), [`812a4e8`](https://github.com/Effect-TS/effect/commit/812a4e86e2d1aa23b477ef5829aa0e5c07784936), [`6a128f6`](https://github.com/Effect-TS/effect/commit/6a128f63f9b41fec2db70790b3bbb96cb9afa1ab), [`273565e`](https://github.com/Effect-TS/effect/commit/273565e7901639e8d0541930ab715aea9c80fbaa), [`adf7d7a`](https://github.com/Effect-TS/effect/commit/adf7d7a7dfce3a7021e9f3b0d847dc85be89d754), [`569a801`](https://github.com/Effect-TS/effect/commit/569a8017ef0a0bc203e4312867cbdd37b0effbd7), [`aa1fa53`](https://github.com/Effect-TS/effect/commit/aa1fa5301e886b9657c8eb0d38cb87cef92a8305), [`02f6b06`](https://github.com/Effect-TS/effect/commit/02f6b0660e12bee1069532a9cc18d3ab855257be), [`12b893e`](https://github.com/Effect-TS/effect/commit/12b893e63cc6dfada4aca7773b4783940e2edf25), [`bbad27e`](https://github.com/Effect-TS/effect/commit/bbad27ec0a90860593f759405caa877e7f4a655f), [`7041393`](https://github.com/Effect-TS/effect/commit/7041393cff132e96566d3f36da0483a6ff6195e4), [`e0d21a5`](https://github.com/Effect-TS/effect/commit/e0d21a54c8323728fbb75a32f4820a9996257809), [`adf7d7a`](https://github.com/Effect-TS/effect/commit/adf7d7a7dfce3a7021e9f3b0d847dc85be89d754), [`007289a`](https://github.com/Effect-TS/effect/commit/007289a52d5877f8e90e2dacf38171ff9bf603fd), [`42a8f99`](https://github.com/Effect-TS/effect/commit/42a8f99740eefdaf2c4544d2c345313f97547a36), [`eebfd29`](https://github.com/Effect-TS/effect/commit/eebfd29633fd5d38b505c5c0842036f61f05e913), [`040703d`](https://github.com/Effect-TS/effect/commit/040703d0e100cd5511e52d812c15492414262b5e)]: + - effect@3.8.0 + - @effect/schema@0.73.0 + - @effect/platform@0.65.0 + - @effect/printer@0.36.0 + - @effect/printer-ansi@0.36.0 + +## 0.43.3 + +### Patch Changes + +- Updated dependencies [[`35a0f81`](https://github.com/Effect-TS/effect/commit/35a0f813141652d696461cd5d19fd146adaf85be), [`8261c5a`](https://github.com/Effect-TS/effect/commit/8261c5ae6fe86872292ec1fc1a58ab9cea2f5f51)]: + - effect@3.7.3 + - @effect/platform@0.64.1 + - @effect/printer@0.35.3 + - @effect/printer-ansi@0.35.3 + - @effect/schema@0.72.4 + +## 0.43.2 + +### Patch Changes + +- [#3569](https://github.com/Effect-TS/effect/pull/3569) [`06989e9`](https://github.com/Effect-TS/effect/commit/06989e9969ef78132c4c2dde3ac4892ff60fdb4c) Thanks @IMax153! - Ensure `QuitException` terminates command-line processing. + + A `QuitException` raised by a `Prompt` that is executing as a fallback for a CLI option will terminate processing of the command line. + +## 0.43.1 + +### Patch Changes + +- [#3561](https://github.com/Effect-TS/effect/pull/3561) [`2df49c4`](https://github.com/Effect-TS/effect/commit/2df49c44ee9c2504eb96507ce5dfb1fa57b33b18) Thanks @IMax153! - Add `Options.withFallbackPrompt` to CLI + + You can now specify that a command-line option should fallback to prompting the + user for a value if no value is specified. + + ```ts + import * as Options from "@effect/cli/Options" + import * as Prompt from "@effect/cli/Prompt" + + const name = Options.text("name").pipe( + Options.withFallbackPrompt( + Prompt.text({ + message: "Please provide your name" + }) + ) + ) + ``` + +## 0.43.0 + +### Patch Changes + +- Updated dependencies [[`f6acb71`](https://github.com/Effect-TS/effect/commit/f6acb71b17a0e6b0d449e7f661c9e2c3d335fcac), [`90ac8f6`](https://github.com/Effect-TS/effect/commit/90ac8f6f6053a2e4498f8b0cc56fe12777d02e1a), [`90ac8f6`](https://github.com/Effect-TS/effect/commit/90ac8f6f6053a2e4498f8b0cc56fe12777d02e1a), [`3791e24`](https://github.com/Effect-TS/effect/commit/3791e241636b1dfe924a56f380ebc9a7ff0827a9), [`3791e24`](https://github.com/Effect-TS/effect/commit/3791e241636b1dfe924a56f380ebc9a7ff0827a9)]: + - @effect/schema@0.72.3 + - @effect/platform@0.64.0 + +## 0.42.3 + +### Patch Changes + +- Updated dependencies [[`4a701c4`](https://github.com/Effect-TS/effect/commit/4a701c406da032563fedae459536c00ae5cfe3c7)]: + - @effect/platform@0.63.3 + +## 0.42.2 + +### Patch Changes + +- Updated dependencies [[`8a601d7`](https://github.com/Effect-TS/effect/commit/8a601d7a1f8ffe52ac9e6d67e9282a1495fe59c9), [`353ba19`](https://github.com/Effect-TS/effect/commit/353ba19f9b2b9e959f0a00d058c6d40a4bc02db7)]: + - effect@3.7.2 + - @effect/platform@0.63.2 + - @effect/printer@0.35.2 + - @effect/printer-ansi@0.35.2 + - @effect/schema@0.72.2 + +## 0.42.1 + +### Patch Changes + +- Updated dependencies [[`79859e7`](https://github.com/Effect-TS/effect/commit/79859e71040d8edf1868b8530b90c650f4321eff), [`f6a469c`](https://github.com/Effect-TS/effect/commit/f6a469c190b9f00eee5ea0cd4d5912a0ef8b46f5), [`dcb9ec0`](https://github.com/Effect-TS/effect/commit/dcb9ec0db443894dd204d87450f779c44b9ad7f1), [`79aa6b1`](https://github.com/Effect-TS/effect/commit/79aa6b136e1f29b36f34e88cb2ff162bff2bb4ed)]: + - effect@3.7.1 + - @effect/platform@0.63.1 + - @effect/printer@0.35.1 + - @effect/printer-ansi@0.35.1 + - @effect/schema@0.72.1 + +## 0.42.0 + +### Patch Changes + +- Updated dependencies [[`db89601`](https://github.com/Effect-TS/effect/commit/db89601ee9c1050c4e762b7bd7ec65a6a2799dfe), [`2f456cc`](https://github.com/Effect-TS/effect/commit/2f456cce5012b9fcb6b4e039190d527813b75b92), [`8745e41`](https://github.com/Effect-TS/effect/commit/8745e41ed96e3765dc6048efc2a9afbe05c8a1e9), [`e557838`](https://github.com/Effect-TS/effect/commit/e55783886b046d3c5f33447f455f9ccf2fa75922), [`d6e7e40`](https://github.com/Effect-TS/effect/commit/d6e7e40b1e2ad0c59aa02f07344d28601b14ebdc), [`8356321`](https://github.com/Effect-TS/effect/commit/8356321598da04bd77c1001f45a4e447bec5591d), [`192f2eb`](https://github.com/Effect-TS/effect/commit/192f2ebb2c4ddbf4bfd8baedd32140b2376868f4), [`718cb70`](https://github.com/Effect-TS/effect/commit/718cb70038629a6d58d02e407760e341f7c94474), [`e9d0310`](https://github.com/Effect-TS/effect/commit/e9d03107acbf204d9304f3e8aea0816b7d3c7dfb), [`6bf28f7`](https://github.com/Effect-TS/effect/commit/6bf28f7e3b1e5e0608ff567205fea0581d11666f)]: + - effect@3.7.0 + - @effect/platform@0.63.0 + - @effect/printer@0.35.0 + - @effect/printer-ansi@0.35.0 + - @effect/schema@0.72.0 + +## 0.41.5 + +### Patch Changes + +- [#3508](https://github.com/Effect-TS/effect/pull/3508) [`8e64b1a`](https://github.com/Effect-TS/effect/commit/8e64b1a6ed4310fbc767910c6141adda9d19463c) Thanks @cdierkens! - Renders the default for all `Prompt` types that accepts `TextOptions`. + - The default value will be rendered as ghost text for `Prompt.text` and `Prompt.list`. + - The default value will be rendered as redacted ghost text for `Prompt.password`. + - The default value will remain hidden for `Prompt.hidden`. + +- Updated dependencies [[`e809286`](https://github.com/Effect-TS/effect/commit/e8092865900608c4df7a6b7991b1c13cc1e4ca2d)]: + - effect@3.6.8 + - @effect/platform@0.62.5 + - @effect/printer@0.34.8 + - @effect/printer-ansi@0.34.8 + - @effect/schema@0.71.4 + +## 0.41.4 + +### Patch Changes + +- Updated dependencies [[`e7a65e3`](https://github.com/Effect-TS/effect/commit/e7a65e3c6a08636bbfce3d3af3098bf28474364d), [`50ec889`](https://github.com/Effect-TS/effect/commit/50ec8897a49b7d1fe84f63107f89d543c52f3dfc)]: + - @effect/platform@0.62.4 + - effect@3.6.7 + - @effect/printer@0.34.7 + - @effect/printer-ansi@0.34.7 + - @effect/schema@0.71.3 + +## 0.41.3 + +### Patch Changes + +- Updated dependencies [[`f960bf4`](https://github.com/Effect-TS/effect/commit/f960bf45239e9badac6e0ad3a602f4174cd7bbdf), [`46a575f`](https://github.com/Effect-TS/effect/commit/46a575f48a05457b782fb21f7827d338c9b59320)]: + - effect@3.6.6 + - @effect/platform@0.62.3 + - @effect/printer@0.34.6 + - @effect/printer-ansi@0.34.6 + - @effect/schema@0.71.2 + +## 0.41.2 + +### Patch Changes + +- [#3466](https://github.com/Effect-TS/effect/pull/3466) [`72dd535`](https://github.com/Effect-TS/effect/commit/72dd53566a2050ef2fa21153a07520df1969edf8) Thanks @cdierkens! - Respect the `Prompt.TextOptions.default` for a prompt created with `Prompt.text` + +- Updated dependencies [[`413994c`](https://github.com/Effect-TS/effect/commit/413994c9792f16d9d57cca3ae6eb254bf93bd261), [`14a47a8`](https://github.com/Effect-TS/effect/commit/14a47a8c1f3cff2186b8fe7a919a1d773888fb5b), [`0c09841`](https://github.com/Effect-TS/effect/commit/0c0984173be3d58f050b300a1a8aa89d76ba49ae)]: + - @effect/platform@0.62.2 + - effect@3.6.5 + - @effect/printer@0.34.5 + - @effect/printer-ansi@0.34.5 + - @effect/schema@0.71.1 + +## 0.41.1 + +### Patch Changes + +- Updated dependencies [[`9efe0e5`](https://github.com/Effect-TS/effect/commit/9efe0e5b57ac557399be620822c21cc6e9add285)]: + - @effect/platform@0.62.1 + +## 0.41.0 + +### Patch Changes + +- [#3450](https://github.com/Effect-TS/effect/pull/3450) [`0e42a8f`](https://github.com/Effect-TS/effect/commit/0e42a8f045ecb1fd3d080edf3d49fef16a9b0ca1) Thanks @tim-smart! - update dependencies + +- Updated dependencies [[`c1987e2`](https://github.com/Effect-TS/effect/commit/c1987e25c8f5c48bdc9ad223d7a6f2c32f93f5a1), [`8295281`](https://github.com/Effect-TS/effect/commit/8295281ae9bd7441e680402540bf3c8682ec417b), [`c940df6`](https://github.com/Effect-TS/effect/commit/c940df63800bf3c4396d91cf28ec34938642fd2c), [`00b6c6d`](https://github.com/Effect-TS/effect/commit/00b6c6d4001f5de728b7d990a1b14560b4961a63), [`1ceed14`](https://github.com/Effect-TS/effect/commit/1ceed149dc64f4874e64b5cf2f954eba0a5a1f12), [`f8d95a6`](https://github.com/Effect-TS/effect/commit/f8d95a61ad0762147933c5c32bb6d7237e18eef4), [`0e42a8f`](https://github.com/Effect-TS/effect/commit/0e42a8f045ecb1fd3d080edf3d49fef16a9b0ca1)]: + - @effect/schema@0.71.0 + - effect@3.6.4 + - @effect/platform@0.62.0 + - @effect/printer@0.34.4 + - @effect/printer-ansi@0.34.4 + +## 0.40.8 + +### Patch Changes + +- Updated dependencies [[`04adcac`](https://github.com/Effect-TS/effect/commit/04adcace913e6fc483df266874a68005e9e04ccf)]: + - effect@3.6.3 + - @effect/platform@0.61.8 + - @effect/printer@0.34.3 + - @effect/printer-ansi@0.34.3 + - @effect/schema@0.70.4 + +## 0.40.7 + +### Patch Changes + +- Updated dependencies [[`17245a4`](https://github.com/Effect-TS/effect/commit/17245a4e783c19dee51529600b3b40f164fa59bc), [`630d40e`](https://github.com/Effect-TS/effect/commit/630d40eaa7eb4d2f8b6705b16d4f426bc28a7d09), [`630d40e`](https://github.com/Effect-TS/effect/commit/630d40eaa7eb4d2f8b6705b16d4f426bc28a7d09)]: + - @effect/platform@0.61.7 + +## 0.40.6 + +### Patch Changes + +- Updated dependencies [[`99ad841`](https://github.com/Effect-TS/effect/commit/99ad8415293a82d08bd7043c563b29e2b468ca74), [`d829b57`](https://github.com/Effect-TS/effect/commit/d829b576357f2e3b203ab7e107a1492de903a106), [`fd4b2f6`](https://github.com/Effect-TS/effect/commit/fd4b2f6516b325740dde615f1cf0229edf13ca0c)]: + - @effect/schema@0.70.3 + - @effect/platform@0.61.6 + - effect@3.6.2 + - @effect/printer@0.34.2 + - @effect/printer-ansi@0.34.2 + +## 0.40.5 + +### Patch Changes + +- Updated dependencies [[`056b710`](https://github.com/Effect-TS/effect/commit/056b7108978e70612176c23991916f678d947f38)]: + - @effect/platform@0.61.5 + +## 0.40.4 + +### Patch Changes + +- Updated dependencies [[`e7cb109`](https://github.com/Effect-TS/effect/commit/e7cb109d0754207024a64d55b6bd2a674dd8ed7d)]: + - @effect/platform@0.61.4 + +## 0.40.3 + +### Patch Changes + +- Updated dependencies [[`510a34d`](https://github.com/Effect-TS/effect/commit/510a34d4cc5d2f51347a53847f6c7db84d2b17c6), [`fb9f786`](https://github.com/Effect-TS/effect/commit/fb9f7867f0c895e63f9ef23e8d0941248c42179d), [`45dbb9f`](https://github.com/Effect-TS/effect/commit/45dbb9ffeaf93d9e4df99d0cd4920e41ba9a3978)]: + - effect@3.6.1 + - @effect/platform@0.61.3 + - @effect/printer@0.34.1 + - @effect/printer-ansi@0.34.1 + - @effect/schema@0.70.2 + +## 0.40.2 + +### Patch Changes + +- Updated dependencies [[`3dce357`](https://github.com/Effect-TS/effect/commit/3dce357efe4a4451d7d29859d08ac11713999b1a), [`657fc48`](https://github.com/Effect-TS/effect/commit/657fc48bb32daf2dc09c9335b3cbc3152bcbdd3b)]: + - @effect/schema@0.70.1 + - @effect/platform@0.61.2 + +## 0.40.1 + +### Patch Changes + +- Updated dependencies [[`11223bf`](https://github.com/Effect-TS/effect/commit/11223bf9cbf5b822e0bf9a9fb2b35b2ad88af692)]: + - @effect/platform@0.61.1 + +## 0.40.0 + +### Patch Changes + +- [#3380](https://github.com/Effect-TS/effect/pull/3380) [`7d02174`](https://github.com/Effect-TS/effect/commit/7d02174af3bcbf054e5cdddb821c91d0f47e8285) Thanks @fubhy! - Changed various function signatures to return `Array` instead of `ReadonlyArray` + +- Updated dependencies [[`1e0fe80`](https://github.com/Effect-TS/effect/commit/1e0fe802b36c257971296617473ce0abe730e8dc), [`8135294`](https://github.com/Effect-TS/effect/commit/8135294b591ea94fde7e6f94a504608f0e630520), [`cd255a4`](https://github.com/Effect-TS/effect/commit/cd255a48872d8fb924cf713ef73f0883a9cc6987), [`3845646`](https://github.com/Effect-TS/effect/commit/3845646828e98f3c7cda1217f6cfe5f642ac0603), [`2d09078`](https://github.com/Effect-TS/effect/commit/2d09078c5948b37fc2f79ef858fe4ca3e4814085), [`4bce5a0`](https://github.com/Effect-TS/effect/commit/4bce5a0274203550ccf117d830721891b0a3d182), [`4ddbff0`](https://github.com/Effect-TS/effect/commit/4ddbff0bb4e3ffddfeb509c59835b83245fb975e), [`e74cc38`](https://github.com/Effect-TS/effect/commit/e74cc38cb420a320c4d7ef98180f19d452a8b316), [`bb069b4`](https://github.com/Effect-TS/effect/commit/bb069b49ef291c532a02c1e8e74271f6d1bb32ec), [`cd255a4`](https://github.com/Effect-TS/effect/commit/cd255a48872d8fb924cf713ef73f0883a9cc6987), [`7d02174`](https://github.com/Effect-TS/effect/commit/7d02174af3bcbf054e5cdddb821c91d0f47e8285)]: + - effect@3.6.0 + - @effect/schema@0.70.0 + - @effect/platform@0.61.0 + - @effect/printer@0.34.0 + - @effect/printer-ansi@0.34.0 + +## 0.39.3 + +### Patch Changes + +- Updated dependencies [[`7c0da50`](https://github.com/Effect-TS/effect/commit/7c0da5050d30cb804f4eacb15995d0fb7f3a28d2), [`2fc0ff4`](https://github.com/Effect-TS/effect/commit/2fc0ff4c59c25977018f6ac70ced99b04a8c7b2b), [`6359644`](https://github.com/Effect-TS/effect/commit/635964446323cf55d4060559337e710e4a24496e), [`f262665`](https://github.com/Effect-TS/effect/commit/f262665c2773492c01e5dd0e8d6db235aafaaad8), [`7f41e42`](https://github.com/Effect-TS/effect/commit/7f41e428830bf3043b8be0d28dcd235d5747c942), [`9bbe7a6`](https://github.com/Effect-TS/effect/commit/9bbe7a681430ebf5c10167bb7140ba3742e46bb7), [`f566fd1`](https://github.com/Effect-TS/effect/commit/f566fd1d7eea531a0d981dd24037f14a603a1273)]: + - @effect/schema@0.69.3 + - effect@3.5.9 + - @effect/platform@0.60.3 + - @effect/printer@0.33.52 + - @effect/printer-ansi@0.33.52 + +## 0.39.2 + +### Patch Changes + +- Updated dependencies [[`eb4d014`](https://github.com/Effect-TS/effect/commit/eb4d014c559e1b4c95b3fb9295fe77593c17ed7a), [`1ba640c`](https://github.com/Effect-TS/effect/commit/1ba640c702f187a866023bf043c26e25cce941ef), [`c8c71bd`](https://github.com/Effect-TS/effect/commit/c8c71bd20eb87d23133dac6156b83bb08941597c), [`a26ce58`](https://github.com/Effect-TS/effect/commit/a26ce581ca7d407e1e81439b58c8045b3fa65231), [`fc20f73`](https://github.com/Effect-TS/effect/commit/fc20f73c69e577981cb64714de2adc97e1004dae)]: + - @effect/platform@0.60.2 + - effect@3.5.8 + - @effect/printer@0.33.51 + - @effect/printer-ansi@0.33.51 + - @effect/schema@0.69.2 + +## 0.39.1 + +### Patch Changes + +- [#3329](https://github.com/Effect-TS/effect/pull/3329) [`b8e3ab6`](https://github.com/Effect-TS/effect/commit/b8e3ab6739d122c31fb9b91ea5cbfc1afff1180b) Thanks @IMax153! - The `Prompt.all` method now supports taking in a record of `Prompt`s to be more + consistent with other `all` APIs throughout the Effect ecosystem. + + You can now do: + + ```ts + import * as Prompt from "@effect/cli/Prompt" + import * as NodeContext from "@effect/platform-node/NodeContext" + import * as Runtime from "@effect/platform-node/NodeRuntime" + import * as Effect from "effect/Effect" + + const program = Prompt.all({ + username: Prompt.text({ + message: "Enter your username" + }), + password: Prompt.password({ + message: "Enter your password: ", + validate: (value) => + value.length === 0 + ? Effect.fail("Password cannot be empty") + : Effect.succeed(value) + }) + }) + + program.pipe( + Effect.flatMap(({ username, password }) => /* Your logic here */ ), + Effect.provide(NodeContext.layer), + NodeRuntime.runMain + ) + ``` + +- [#3332](https://github.com/Effect-TS/effect/pull/3332) [`c8f8690`](https://github.com/Effect-TS/effect/commit/c8f86905859c39ed2735890eebe8d73ceb2eb12e) Thanks @IMax153! - improve the `Prompt.all` JSDoc and add example + +- Updated dependencies [[`f241154`](https://github.com/Effect-TS/effect/commit/f241154added5d91e95866c39481f09cdb13bd4d)]: + - @effect/schema@0.69.1 + - @effect/platform@0.60.1 + +## 0.39.0 + +### Patch Changes + +- Updated dependencies [[`20807a4`](https://github.com/Effect-TS/effect/commit/20807a45edeb4334e903dca5d708cd62a71702d8)]: + - @effect/schema@0.69.0 + - @effect/platform@0.60.0 + +## 0.38.3 + +### Patch Changes + +- [#3310](https://github.com/Effect-TS/effect/pull/3310) [`99bddcf`](https://github.com/Effect-TS/effect/commit/99bddcfb3d6eab4d489d055404e26ad81afe52fc) Thanks @fubhy! - Added additional pure annotations to improve tree-shakeability + +- Updated dependencies [[`3afcc93`](https://github.com/Effect-TS/effect/commit/3afcc93413a3d910beb69e4ce9ae120e4adaffd5), [`99bddcf`](https://github.com/Effect-TS/effect/commit/99bddcfb3d6eab4d489d055404e26ad81afe52fc), [`6921c4f`](https://github.com/Effect-TS/effect/commit/6921c4fb8c45badff09b493043b85ca71302b560)]: + - effect@3.5.7 + - @effect/platform@0.59.3 + - @effect/printer@0.33.50 + - @effect/printer-ansi@0.33.50 + - @effect/schema@0.68.27 + +## 0.38.2 + +### Patch Changes + +- Updated dependencies [[`f0285d3`](https://github.com/Effect-TS/effect/commit/f0285d3af6a18829123bc1818331c67206becbc4), [`8ec4955`](https://github.com/Effect-TS/effect/commit/8ec49555ed3b3c98093fa4d135a4c57a3f16ebd1), [`3ac2d76`](https://github.com/Effect-TS/effect/commit/3ac2d76048da09e876cf6c3aee3397febd843fe9), [`cc327a1`](https://github.com/Effect-TS/effect/commit/cc327a1bccd22a4ee27ec7e58b53205e93b23e2c), [`4bfe4fb`](https://github.com/Effect-TS/effect/commit/4bfe4fb5c82f597c9beea9baa92e772593598b60), [`2b14d18`](https://github.com/Effect-TS/effect/commit/2b14d181462cad8359da4fa6bc6dfda0f742c398)]: + - @effect/schema@0.68.26 + - effect@3.5.6 + - @effect/platform@0.59.2 + - @effect/printer@0.33.49 + - @effect/printer-ansi@0.33.49 + +## 0.38.1 + +### Patch Changes + +- [#2755](https://github.com/Effect-TS/effect/pull/2755) [`51e9c5c`](https://github.com/Effect-TS/effect/commit/51e9c5c87f849ef1a013de4b4c35d397acb1e147) Thanks @dpnova! - Allow using the equals (`=`) character inside aliased key value params. + +- Updated dependencies [[`a9d7800`](https://github.com/Effect-TS/effect/commit/a9d7800f6a253192b653d77778b0674f39b1ca39), [`fcecff7`](https://github.com/Effect-TS/effect/commit/fcecff7f7e12b295a252f124861b801c73072151), [`adbf753`](https://github.com/Effect-TS/effect/commit/adbf75340a9db15dc5cadc66e911a8978a195a65), [`adbf753`](https://github.com/Effect-TS/effect/commit/adbf75340a9db15dc5cadc66e911a8978a195a65)]: + - effect@3.5.5 + - @effect/platform@0.59.1 + - @effect/printer@0.33.48 + - @effect/printer-ansi@0.33.48 + - @effect/schema@0.68.25 + +## 0.38.0 + +### Patch Changes + +- [#3253](https://github.com/Effect-TS/effect/pull/3253) [`ed0dde4`](https://github.com/Effect-TS/effect/commit/ed0dde4888e6f1a97ad5bba06b755d26a6a1c52e) Thanks @tim-smart! - update dependencies + +- Updated dependencies [[`53c0db0`](https://github.com/Effect-TS/effect/commit/53c0db06872d5b5edea2a706e83249908385325c), [`ed0dde4`](https://github.com/Effect-TS/effect/commit/ed0dde4888e6f1a97ad5bba06b755d26a6a1c52e), [`ada68b3`](https://github.com/Effect-TS/effect/commit/ada68b3e61c67907c2a281c024c84d818186ca4c), [`ca775ce`](https://github.com/Effect-TS/effect/commit/ca775cec53baebc1a43d9b8852a3ac6726178498), [`5be9cc0`](https://github.com/Effect-TS/effect/commit/5be9cc044025a9541b9b7acefa2d3fc05fa1301b), [`203658f`](https://github.com/Effect-TS/effect/commit/203658f8001c132b25764ab70344b171683b554c), [`eb1c4d4`](https://github.com/Effect-TS/effect/commit/eb1c4d44e54b9d8d201a366d1ff94face2a6dcd3)]: + - @effect/platform@0.59.0 + - effect@3.5.4 + - @effect/schema@0.68.24 + - @effect/printer@0.33.47 + - @effect/printer-ansi@0.33.47 + +## 0.37.10 + +### Patch Changes + +- Updated dependencies [[`edb0da3`](https://github.com/Effect-TS/effect/commit/edb0da383746d760f35d8582f5fb0cc0eeca9217), [`edb0da3`](https://github.com/Effect-TS/effect/commit/edb0da383746d760f35d8582f5fb0cc0eeca9217), [`a1db40a`](https://github.com/Effect-TS/effect/commit/a1db40a650ab842e778654f0d88e80f2ef4fd6f3), [`c8d3fb0`](https://github.com/Effect-TS/effect/commit/c8d3fb0fe23585f6efb724af51fbab3ba1ad6e83), [`dabd028`](https://github.com/Effect-TS/effect/commit/dabd028decf9b7983ca16ebe0f48c05c11a84b68), [`786b2ab`](https://github.com/Effect-TS/effect/commit/786b2ab29d525c877bb84035dac9e2d6499339d1), [`fc57354`](https://github.com/Effect-TS/effect/commit/fc573547d41667016fce05eaee75960fcc6dce4d)]: + - effect@3.5.3 + - @effect/schema@0.68.23 + - @effect/platform@0.58.27 + - @effect/printer@0.33.46 + - @effect/printer-ansi@0.33.46 + +## 0.37.9 + +### Patch Changes + +- Updated dependencies [[`639208e`](https://github.com/Effect-TS/effect/commit/639208eeb8a44622994f832bc2d45d06ab636bc8), [`6684b4c`](https://github.com/Effect-TS/effect/commit/6684b4c27d77a7fcc7af2e261a450edf971b62b5), [`6684b4c`](https://github.com/Effect-TS/effect/commit/6684b4c27d77a7fcc7af2e261a450edf971b62b5), [`6684b4c`](https://github.com/Effect-TS/effect/commit/6684b4c27d77a7fcc7af2e261a450edf971b62b5)]: + - effect@3.5.2 + - @effect/platform@0.58.26 + - @effect/printer@0.33.45 + - @effect/printer-ansi@0.33.45 + - @effect/schema@0.68.22 + +## 0.37.8 + +### Patch Changes + +- Updated dependencies [[`0623fca`](https://github.com/Effect-TS/effect/commit/0623fca41679b0e3c5a10dd0f8985f91670bd721)]: + - @effect/platform@0.58.25 + +## 0.37.7 + +### Patch Changes + +- Updated dependencies [[`55fdd76`](https://github.com/Effect-TS/effect/commit/55fdd761ee95afd73b6a892c13fee92b36c02837)]: + - effect@3.5.1 + - @effect/platform@0.58.24 + - @effect/printer@0.33.44 + - @effect/printer-ansi@0.33.44 + - @effect/schema@0.68.21 + +## 0.37.6 + +### Patch Changes + +- Updated dependencies [[`a1f5b83`](https://github.com/Effect-TS/effect/commit/a1f5b831a1bc7535988b370d68d0b3eb1123e0ce), [`a1f5b83`](https://github.com/Effect-TS/effect/commit/a1f5b831a1bc7535988b370d68d0b3eb1123e0ce), [`a1f5b83`](https://github.com/Effect-TS/effect/commit/a1f5b831a1bc7535988b370d68d0b3eb1123e0ce), [`60bc3d0`](https://github.com/Effect-TS/effect/commit/60bc3d0867b13e48b24dc22604b4dd2e7b2c1ca4), [`5ab348f`](https://github.com/Effect-TS/effect/commit/5ab348f265db3d283aa091ddca6d2d49137c16f2), [`60bc3d0`](https://github.com/Effect-TS/effect/commit/60bc3d0867b13e48b24dc22604b4dd2e7b2c1ca4), [`3e04bf8`](https://github.com/Effect-TS/effect/commit/3e04bf8a7127e956cadb7684a8f4c661df57663b), [`e7fc45f`](https://github.com/Effect-TS/effect/commit/e7fc45f0c7002aafdaec7878149ac064cd104ea3), [`a1f5b83`](https://github.com/Effect-TS/effect/commit/a1f5b831a1bc7535988b370d68d0b3eb1123e0ce), [`4626de5`](https://github.com/Effect-TS/effect/commit/4626de59c25b384216faa0be87bf0b8cd36357d0), [`f01e7db`](https://github.com/Effect-TS/effect/commit/f01e7db317827255d7901f523f2e28b43298e8df), [`60bc3d0`](https://github.com/Effect-TS/effect/commit/60bc3d0867b13e48b24dc22604b4dd2e7b2c1ca4), [`79d2d91`](https://github.com/Effect-TS/effect/commit/79d2d91464d95dde0e9444d43e7a7f309f05d6e6), [`ac71f37`](https://github.com/Effect-TS/effect/commit/ac71f378f2413e5aa91c95f649ffe898d6a26114), [`8432360`](https://github.com/Effect-TS/effect/commit/8432360ce68614a419bb328083a4109d0fc8aa93), [`e4bf1bf`](https://github.com/Effect-TS/effect/commit/e4bf1bf2b4a970eacd77c9b77b5ea8c68bc84498), [`13cb861`](https://github.com/Effect-TS/effect/commit/13cb861a5eded15c55c6cdcf6a8acde8320367a6), [`79d2d91`](https://github.com/Effect-TS/effect/commit/79d2d91464d95dde0e9444d43e7a7f309f05d6e6), [`e7fc45f`](https://github.com/Effect-TS/effect/commit/e7fc45f0c7002aafdaec7878149ac064cd104ea3), [`9f66825`](https://github.com/Effect-TS/effect/commit/9f66825f1fce0fe8d10420c285f7dc4c71e8af8d)]: + - effect@3.5.0 + - @effect/platform@0.58.23 + - @effect/printer@0.33.43 + - @effect/printer-ansi@0.33.43 + - @effect/schema@0.68.20 + +## 0.37.5 + +### Patch Changes + +- Updated dependencies [[`7af137c`](https://github.com/Effect-TS/effect/commit/7af137c9433f6e74959b3887561ec1e6f12e10ee), [`ee4b3dc`](https://github.com/Effect-TS/effect/commit/ee4b3dc5f68d19dc3ae1c2d12901c5b8ffbebabb), [`366f2ee`](https://github.com/Effect-TS/effect/commit/366f2ee3fb6f712a44e8f84fc188612e5ecc016d), [`366f2ee`](https://github.com/Effect-TS/effect/commit/366f2ee3fb6f712a44e8f84fc188612e5ecc016d), [`097d25c`](https://github.com/Effect-TS/effect/commit/097d25cb5d13c049e01789651be56b09620186ef)]: + - effect@3.4.9 + - @effect/platform@0.58.22 + - @effect/printer@0.33.42 + - @effect/printer-ansi@0.33.42 + - @effect/schema@0.68.19 + +## 0.37.4 + +### Patch Changes + +- Updated dependencies [[`5d5cc6c`](https://github.com/Effect-TS/effect/commit/5d5cc6cfd7d63b07081290fb189b364999201fc5), [`a435e0f`](https://github.com/Effect-TS/effect/commit/a435e0fc5378b33a49bcec92ee235df6f16a2419), [`b5554db`](https://github.com/Effect-TS/effect/commit/b5554db36c4dd6f64fa5e6a62a29b2759c54217a), [`359ff8a`](https://github.com/Effect-TS/effect/commit/359ff8aa2e4e6389bf56d759baa804e2a7674a16), [`a9c4fb3`](https://github.com/Effect-TS/effect/commit/a9c4fb3bf3c6e92cd1c142b0605fddf7eb3c697c), [`f7534b9`](https://github.com/Effect-TS/effect/commit/f7534b94cba06b143a3d4f29275d92874a939559)]: + - @effect/schema@0.68.18 + - effect@3.4.8 + - @effect/platform@0.58.21 + - @effect/printer@0.33.41 + - @effect/printer-ansi@0.33.41 + +## 0.37.3 + +### Patch Changes + +- Updated dependencies []: + - @effect/printer@0.33.40 + - @effect/printer-ansi@0.33.40 + +## 0.37.2 + +### Patch Changes + +- [#3167](https://github.com/Effect-TS/effect/pull/3167) [`749b903`](https://github.com/Effect-TS/effect/commit/749b90345f38d8e61a2f1d6861e55d632daa169e) Thanks @IMax153! - ensure that file selector properly traverses up directories + +## 0.37.1 + +### Patch Changes + +- Updated dependencies [[`15967cf`](https://github.com/Effect-TS/effect/commit/15967cf18931fb6ede3083eb687a8dfff371cc56), [`2328e17`](https://github.com/Effect-TS/effect/commit/2328e17577112db17c29b7756942a0ff64a70ee0), [`a5737d6`](https://github.com/Effect-TS/effect/commit/a5737d6db2b921605c332eabbc5402ee3d17357b)]: + - @effect/schema@0.68.17 + - effect@3.4.7 + - @effect/platform@0.58.20 + - @effect/printer@0.33.39 + - @effect/printer-ansi@0.33.39 + +## 0.37.0 + +### Minor Changes + +- [#3153](https://github.com/Effect-TS/effect/pull/3153) [`51bb7d5`](https://github.com/Effect-TS/effect/commit/51bb7d5677069e51f0c9b0f7e57c4ea2f41401b7) Thanks @IMax153! - Refactors the `Prompt.custom` constructor to make it easier to create custom + `Prompt`s. + + The `Prompt.custom` constructor allows for creation of a custom `Prompt` from + the provided initial state and handlers. + + ```ts + export const custom: ( + initialState: State | Effect, + handlers: { + readonly render: ( + state: State, + action: Action + ) => Effect + readonly process: ( + input: UserInput, + state: State + ) => Effect, never, Environment> + readonly clear: ( + state: State, + action: Action + ) => Effect + } + ) => Prompt = InternalPrompt.custom + ``` + + The initial state of a `Prompt` can either be a pure value or an `Effect`. This + is particularly useful when the initial state of the `Prompt` must be computed + by performing some effectful computation, such as reading data from the file + system. + + A `Prompt` is essentially a render loop where user input triggers a new frame + to be rendered to the `Terminal`. The `handlers` of a custom prompt are used + to control what is rendered to the `Terminal` each frame. During each frame, + the following occurs: + 1. The `render` handler is called with this frame's prompt state and prompt + action and returns an ANSI escape string to be rendered to the + `Terminal` + 2. The `Terminal` obtains input from the user + 3. The `process` handler is called with the input obtained from the user + and this frame's prompt state and returns the next prompt action that + should be performed + 4. The `clear` handler is called with this frame's prompt state and prompt + action and returns an ANSI escape string used to clear the screen of + the `Terminal` + +- [#3153](https://github.com/Effect-TS/effect/pull/3153) [`51bb7d5`](https://github.com/Effect-TS/effect/commit/51bb7d5677069e51f0c9b0f7e57c4ea2f41401b7) Thanks @IMax153! - Utilize the `Prompt.file` constructor in Effect CLI's wizard mode for file and directory `Options` + +- [#3153](https://github.com/Effect-TS/effect/pull/3153) [`51bb7d5`](https://github.com/Effect-TS/effect/commit/51bb7d5677069e51f0c9b0f7e57c4ea2f41401b7) Thanks @IMax153! - Adds a `Prompt.file` constructor to the `Prompt` module which allows the user to select a file + +## 0.36.72 + +### Patch Changes + +- Updated dependencies [[`d006cec`](https://github.com/Effect-TS/effect/commit/d006cec022e8524dbfd6dc6df751fe4c86b10042), [`cb22726`](https://github.com/Effect-TS/effect/commit/cb2272656881aa5878a1c3fc0b12d8fbc66eb63c), [`e911cfd`](https://github.com/Effect-TS/effect/commit/e911cfdc79418462d7e9000976fded15ea6b738d)]: + - @effect/schema@0.68.16 + - @effect/platform@0.58.19 + +## 0.36.71 + +### Patch Changes + +- Updated dependencies [[`7f8900a`](https://github.com/Effect-TS/effect/commit/7f8900a1de9addeb0d371103a2c5c2aa3e4ff95e)]: + - @effect/platform@0.58.18 + +## 0.36.70 + +### Patch Changes + +- Updated dependencies [[`5c0ceb0`](https://github.com/Effect-TS/effect/commit/5c0ceb00826cce9e50bf9d41d83e191d5352c030), [`5c0ceb0`](https://github.com/Effect-TS/effect/commit/5c0ceb00826cce9e50bf9d41d83e191d5352c030), [`34faeb6`](https://github.com/Effect-TS/effect/commit/34faeb6305ba52af4d6f8bdd2e633bb6a5a7a35b), [`33735b1`](https://github.com/Effect-TS/effect/commit/33735b16b41bd26929d8f4754c190925db6323b7), [`5c0ceb0`](https://github.com/Effect-TS/effect/commit/5c0ceb00826cce9e50bf9d41d83e191d5352c030), [`139d4b3`](https://github.com/Effect-TS/effect/commit/139d4b39fb3bff2eeaa7c0c809c581da42425a83)]: + - effect@3.4.6 + - @effect/schema@0.68.15 + - @effect/platform@0.58.17 + - @effect/printer@0.33.38 + - @effect/printer-ansi@0.33.38 + +## 0.36.69 + +### Patch Changes + +- Updated dependencies [[`61e5964`](https://github.com/Effect-TS/effect/commit/61e59640fd993216cca8ace0ac8abd9104e213ce)]: + - @effect/schema@0.68.14 + - @effect/platform@0.58.16 + +## 0.36.68 + +### Patch Changes + +- [#3107](https://github.com/Effect-TS/effect/pull/3107) [`fcb7411`](https://github.com/Effect-TS/effect/commit/fcb7411be7ccc8b62da7c5e2b2044365ae879a0e) Thanks @IMax153! - Adds a `--log-level` built-in option to all commands which can be used to control the minimum `LogLevel` of the command's associated handler + +- Updated dependencies [[`cb76bcb`](https://github.com/Effect-TS/effect/commit/cb76bcb2f8858a90db4f785efee262cea1b9844e), [`baa90df`](https://github.com/Effect-TS/effect/commit/baa90df9663f5f37d7b6814dad25142d53dbc720)]: + - @effect/schema@0.68.13 + - @effect/platform@0.58.15 + +## 0.36.67 + +### Patch Changes + +- [#3106](https://github.com/Effect-TS/effect/pull/3106) [`0cb3e9d`](https://github.com/Effect-TS/effect/commit/0cb3e9de39f9e381a122939b32af4a59b84ea8e7) Thanks @IMax153! - ensure that the built-in option for displaying the CLI's help documentation always respects `CliConfig.showBuiltIns` + +- Updated dependencies [[`52a87c7`](https://github.com/Effect-TS/effect/commit/52a87c7a0b9536398deaf8ec507e53a82c607219), [`6d2280e`](https://github.com/Effect-TS/effect/commit/6d2280e9497c95cb0e965ca462c825345074eedf)]: + - @effect/platform@0.58.14 + +## 0.36.66 + +### Patch Changes + +- Updated dependencies [[`a047af9`](https://github.com/Effect-TS/effect/commit/a047af99447dfffc729e9c8ef0ca143537927e91), [`dbd53ea`](https://github.com/Effect-TS/effect/commit/dbd53ea363c71a24449cb068251054c3a1acf864), [`d990544`](https://github.com/Effect-TS/effect/commit/d9905444b9e800850cb65899114ca0e502e68fe8)]: + - effect@3.4.5 + - @effect/platform@0.58.13 + - @effect/schema@0.68.12 + - @effect/printer@0.33.37 + - @effect/printer-ansi@0.33.37 + +## 0.36.65 + +### Patch Changes + +- Updated dependencies [[`74e0ad2`](https://github.com/Effect-TS/effect/commit/74e0ad23b4c36f41b7fd10856b20f8b701bc4044), [`74e0ad2`](https://github.com/Effect-TS/effect/commit/74e0ad23b4c36f41b7fd10856b20f8b701bc4044), [`72638e3`](https://github.com/Effect-TS/effect/commit/72638e3d99f0e93a24febf6c225256ce92d4a20b), [`d7dde2b`](https://github.com/Effect-TS/effect/commit/d7dde2b4af08b37af859d4c327c1f5c6f00cf9d9), [`9b2fc3b`](https://github.com/Effect-TS/effect/commit/9b2fc3b9dfd304a2bd0508ef2313cfc54357be0c), [`d71c192`](https://github.com/Effect-TS/effect/commit/d71c192b89fd1162423acddc5fd3d6270fbf2ef6)]: + - @effect/platform@0.58.12 + - effect@3.4.4 + - @effect/schema@0.68.11 + - @effect/printer@0.33.36 + - @effect/printer-ansi@0.33.36 + +## 0.36.64 + +### Patch Changes + +- Updated dependencies [[`a5b95b5`](https://github.com/Effect-TS/effect/commit/a5b95b548284e4798654ae7ce6883fa49108f0ea), [`5e29579`](https://github.com/Effect-TS/effect/commit/5e29579187cb8420ea4930b3999fec984f8999f4)]: + - @effect/platform@0.58.11 + +## 0.36.63 + +### Patch Changes + +- [#3079](https://github.com/Effect-TS/effect/pull/3079) [`bbdd365`](https://github.com/Effect-TS/effect/commit/bbdd36567706c94cdec45bacea825941c347b6cd) Thanks @tim-smart! - update dependencies + +- [#3079](https://github.com/Effect-TS/effect/pull/3079) [`bbdd365`](https://github.com/Effect-TS/effect/commit/bbdd36567706c94cdec45bacea825941c347b6cd) Thanks @tim-smart! - update to typescript 5.5 + +- Updated dependencies [[`a48ee84`](https://github.com/Effect-TS/effect/commit/a48ee845ac21bbde9baf938af9e97a98322211c9), [`c342739`](https://github.com/Effect-TS/effect/commit/c3427396226e1ad7b95b40595a23f9bdff3e3365), [`8898e5e`](https://github.com/Effect-TS/effect/commit/8898e5e238622f6337583d91ee23609c1f5ccdf7), [`a48ee84`](https://github.com/Effect-TS/effect/commit/a48ee845ac21bbde9baf938af9e97a98322211c9), [`a48ee84`](https://github.com/Effect-TS/effect/commit/a48ee845ac21bbde9baf938af9e97a98322211c9), [`ff78636`](https://github.com/Effect-TS/effect/commit/ff786367c522975f40f0f179a0ecdfcfab7ecbdb), [`c86bd4e`](https://github.com/Effect-TS/effect/commit/c86bd4e134c23146c216f9ff97e03781d55991b6), [`bbdd365`](https://github.com/Effect-TS/effect/commit/bbdd36567706c94cdec45bacea825941c347b6cd), [`ab3180f`](https://github.com/Effect-TS/effect/commit/ab3180f827041d0ea3b2d72254a1a8683e99e056), [`bbdd365`](https://github.com/Effect-TS/effect/commit/bbdd36567706c94cdec45bacea825941c347b6cd)]: + - @effect/platform@0.58.10 + - effect@3.4.3 + - @effect/schema@0.68.10 + - @effect/printer@0.33.35 + - @effect/printer-ansi@0.33.35 + +## 0.36.62 + +### Patch Changes + +- Updated dependencies [[`0b47fdf`](https://github.com/Effect-TS/effect/commit/0b47fdfe449f42de89e0e88b61ae5140f629e5c4)]: + - @effect/schema@0.68.9 + - @effect/platform@0.58.9 + +## 0.36.61 + +### Patch Changes + +- Updated dependencies [[`192261b`](https://github.com/Effect-TS/effect/commit/192261b2aec94e9913ceed83683fdcfbc9fca66f), [`3da1497`](https://github.com/Effect-TS/effect/commit/3da1497b5c9cc886d300258bc928fd68a4fefe6f)]: + - @effect/schema@0.68.8 + - effect@3.4.2 + - @effect/platform@0.58.8 + - @effect/printer@0.33.34 + - @effect/printer-ansi@0.33.34 + +## 0.36.60 + +### Patch Changes + +- Updated dependencies [[`027004a`](https://github.com/Effect-TS/effect/commit/027004a897f654791e75faa28eefb50dd0244b6e)]: + - @effect/platform@0.58.7 + +## 0.36.59 + +### Patch Changes + +- Updated dependencies [[`66a1910`](https://github.com/Effect-TS/effect/commit/66a19109ff90c4252123b8809b8c8a74681dba6a), [`2e8e252`](https://github.com/Effect-TS/effect/commit/2e8e2520cac712f0eb644553bd476429ebd674e4)]: + - effect@3.4.1 + - @effect/platform@0.58.6 + - @effect/printer@0.33.33 + - @effect/printer-ansi@0.33.33 + - @effect/schema@0.68.7 + +## 0.36.58 + +### Patch Changes + +- Updated dependencies [[`37a07a2`](https://github.com/Effect-TS/effect/commit/37a07a2d8d1ce09ab965c0ada84a3fae9a6aba05)]: + - @effect/platform@0.58.5 + +## 0.36.57 + +### Patch Changes + +- Updated dependencies [[`b77fb0a`](https://github.com/Effect-TS/effect/commit/b77fb0a811ec1ad0e794917077c9a90824515db8)]: + - @effect/platform@0.58.4 + +## 0.36.56 + +### Patch Changes + +- Updated dependencies [[`530fa9e`](https://github.com/Effect-TS/effect/commit/530fa9e36b8532589b948fc4faa37593f36b7f42)]: + - @effect/schema@0.68.6 + - @effect/platform@0.58.3 + +## 0.36.55 + +### Patch Changes + +- Updated dependencies [[`1d62815`](https://github.com/Effect-TS/effect/commit/1d62815a50f34115606940ffa397442d75a20c81)]: + - @effect/schema@0.68.5 + - @effect/platform@0.58.2 + +## 0.36.54 + +### Patch Changes + +- Updated dependencies [[`5a248aa`](https://github.com/Effect-TS/effect/commit/5a248aa5ab2db3f7131ebc79bb9871a76de57973)]: + - @effect/platform@0.58.1 + +## 0.36.53 + +### Patch Changes + +- Updated dependencies [[`c0ce180`](https://github.com/Effect-TS/effect/commit/c0ce180861ad0938053c0e6145e813fa6404df3b), [`63dd0c3`](https://github.com/Effect-TS/effect/commit/63dd0c3af45876c1caad7d03356c74daf551c628), [`61707b6`](https://github.com/Effect-TS/effect/commit/61707b6ffc7397c2ba0dce22512b44955724f60f), [`9c1b5b3`](https://github.com/Effect-TS/effect/commit/9c1b5b39e6c19604ce834f072a114ad392c50a06), [`a35faf8`](https://github.com/Effect-TS/effect/commit/a35faf8d116f94899bfc03feab33b004c8ddfdf7), [`ff73c0c`](https://github.com/Effect-TS/effect/commit/ff73c0cacd66132bfad2e5211b3eae347729c667), [`984d516`](https://github.com/Effect-TS/effect/commit/984d516ccd9412dc41188f6a46b748dd20dd5848), [`8c3b8a2`](https://github.com/Effect-TS/effect/commit/8c3b8a2ce208eab753b6206a51605a424f104e98), [`017e2f9`](https://github.com/Effect-TS/effect/commit/017e2f9b371ce24ea4945e5d7390c934ad3c39cf), [`91bf8a2`](https://github.com/Effect-TS/effect/commit/91bf8a2e9d1959393b3cf7366cc1d584d3e666b7), [`c6a4a26`](https://github.com/Effect-TS/effect/commit/c6a4a266606575fd2c7165940c4072ad4c57d01f)]: + - effect@3.4.0 + - @effect/platform@0.58.0 + - @effect/printer@0.33.32 + - @effect/printer-ansi@0.33.32 + - @effect/schema@0.68.4 + +## 0.36.52 + +### Patch Changes + +- Updated dependencies [[`3ba7ea1`](https://github.com/Effect-TS/effect/commit/3ba7ea1c3c2923e85bf2f17e41176f8f8796d203)]: + - @effect/platform@0.57.8 + +## 0.36.51 + +### Patch Changes + +- Updated dependencies [[`d473800`](https://github.com/Effect-TS/effect/commit/d47380012c3241d7287b66968d33a2414275ce7b)]: + - @effect/schema@0.68.3 + - @effect/platform@0.57.7 + +## 0.36.50 + +### Patch Changes + +- Updated dependencies [[`eb341b3`](https://github.com/Effect-TS/effect/commit/eb341b3eb34ad64499371bc08b7f59e429979d8a)]: + - @effect/schema@0.68.2 + - @effect/platform@0.57.6 + +## 0.36.49 + +### Patch Changes + +- Updated dependencies [[`b8ea6aa`](https://github.com/Effect-TS/effect/commit/b8ea6aa479006358042b4256ee0a1c5cfbe57acb)]: + - @effect/platform@0.57.5 + +## 0.36.48 + +### Patch Changes + +- Updated dependencies [[`b51e266`](https://github.com/Effect-TS/effect/commit/b51e26662b879b55d2c5164b7c97742739aa9446), [`6c89408`](https://github.com/Effect-TS/effect/commit/6c89408cd7b9204ec4c5828a46cd5312d8afb5e7)]: + - @effect/schema@0.68.1 + - effect@3.3.5 + - @effect/platform@0.57.4 + - @effect/printer@0.33.31 + - @effect/printer-ansi@0.33.31 + +## 0.36.47 + +### Patch Changes + +- Updated dependencies [[`f6c7977`](https://github.com/Effect-TS/effect/commit/f6c79772e632c440b7e5221bb75f0ef9d3c3b005), [`a67b8fe`](https://github.com/Effect-TS/effect/commit/a67b8fe2ace08419424811b5f0d9a5378eaea352)]: + - @effect/schema@0.68.0 + - effect@3.3.4 + - @effect/platform@0.57.3 + - @effect/printer@0.33.30 + - @effect/printer-ansi@0.33.30 + +## 0.36.46 + +### Patch Changes + +- Updated dependencies [[`3b15e1b`](https://github.com/Effect-TS/effect/commit/3b15e1b505c0b0e62a03b4a3605d42a9932cc99c), [`06ede85`](https://github.com/Effect-TS/effect/commit/06ede85d6e84710e6622463be95ff3927fb30dad), [`3a750b2`](https://github.com/Effect-TS/effect/commit/3a750b25b1ed92094a7f7ebc332a6bcfb212871b), [`7204ca5`](https://github.com/Effect-TS/effect/commit/7204ca5761c2b1d27999a624db23aa10b6e0504d)]: + - @effect/schema@0.67.24 + - effect@3.3.3 + - @effect/platform@0.57.2 + - @effect/printer@0.33.29 + - @effect/printer-ansi@0.33.29 + +## 0.36.45 + +### Patch Changes + +- Updated dependencies [[`2ee4f2b`](https://github.com/Effect-TS/effect/commit/2ee4f2be7fd63074a9cbac6dcdfb533b6683533a), [`07e12ec`](https://github.com/Effect-TS/effect/commit/07e12ecdb0e20b9763bd9e9058e567a7c8862efc), [`3572646`](https://github.com/Effect-TS/effect/commit/3572646d5e0804f85bc7f64633fb95722533f9dd), [`1aed347`](https://github.com/Effect-TS/effect/commit/1aed347a125ed3847ec90863424810d6759cbc85), [`df4bf4b`](https://github.com/Effect-TS/effect/commit/df4bf4b62e7b316c6647da0271fc5544a84e7ba2), [`f085f92`](https://github.com/Effect-TS/effect/commit/f085f92dfa204afb41823ffc27d437225137643d), [`9b3b4ac`](https://github.com/Effect-TS/effect/commit/9b3b4ac639d98aae33883926bece1e31fa280d22)]: + - @effect/schema@0.67.23 + - @effect/platform@0.57.1 + - effect@3.3.2 + - @effect/printer@0.33.28 + - @effect/printer-ansi@0.33.28 + +## 0.36.44 + +### Patch Changes + +- Updated dependencies [[`eb98c5b`](https://github.com/Effect-TS/effect/commit/eb98c5b79ab50aa0cde239bd4e660dd19dbab612), [`184fed8`](https://github.com/Effect-TS/effect/commit/184fed83ac36cba05a75a5a8013f740f9f696e3b), [`4d3fbe8`](https://github.com/Effect-TS/effect/commit/4d3fbe82e8cec13ccd0cd0b2096deac6818fb59a), [`6068e07`](https://github.com/Effect-TS/effect/commit/6068e073d4cc8b3c8583583fd5eb3efe43f7d5ba), [`3a77e20`](https://github.com/Effect-TS/effect/commit/3a77e209783933bac3aaddba1b05ff6a9ac72b36), [`d79ca17`](https://github.com/Effect-TS/effect/commit/d79ca17d9fa432571c69714776cab5cf8fef9c34)]: + - effect@3.3.1 + - @effect/platform@0.57.0 + - @effect/schema@0.67.22 + - @effect/printer@0.33.27 + - @effect/printer-ansi@0.33.27 + +## 0.36.43 + +### Patch Changes + +- [#2837](https://github.com/Effect-TS/effect/pull/2837) [`67f160a`](https://github.com/Effect-TS/effect/commit/67f160a213de0219a565d4bf653b3cbf24f58e8f) Thanks @KhraksMamtsov! - add .redacted apis to /cli package + +- Updated dependencies [[`2b9ddfc`](https://github.com/Effect-TS/effect/commit/2b9ddfcbac505d98551e764a43923854907ca5c1), [`1f4ac00`](https://github.com/Effect-TS/effect/commit/1f4ac00a91c336c9c9c9b8c3ed9ceb9920ebc9bd), [`9305b76`](https://github.com/Effect-TS/effect/commit/9305b764cceeae4f16564435ae7172f79c2bf822), [`0f40d98`](https://github.com/Effect-TS/effect/commit/0f40d989da10f68df3ecd72b36849401ad679bfb), [`188f0a5`](https://github.com/Effect-TS/effect/commit/188f0a5c57ed0d7c9e5852e0c1c998f1b95810a1), [`b761ef0`](https://github.com/Effect-TS/effect/commit/b761ef00eaf6c67b7ffe34798b98aae5347ab376), [`b53f69b`](https://github.com/Effect-TS/effect/commit/b53f69bff1452a487b21198cd83961f844e02d36), [`0f40d98`](https://github.com/Effect-TS/effect/commit/0f40d989da10f68df3ecd72b36849401ad679bfb), [`5bd549e`](https://github.com/Effect-TS/effect/commit/5bd549e4bd7144727db438ecca6b8dc9b3ef7e22), [`67f160a`](https://github.com/Effect-TS/effect/commit/67f160a213de0219a565d4bf653b3cbf24f58e8f), [`67f160a`](https://github.com/Effect-TS/effect/commit/67f160a213de0219a565d4bf653b3cbf24f58e8f)]: + - @effect/platform@0.56.0 + - effect@3.3.0 + - @effect/schema@0.67.21 + - @effect/printer@0.33.26 + - @effect/printer-ansi@0.33.26 + +## 0.36.42 + +### Patch Changes + +- [#2920](https://github.com/Effect-TS/effect/pull/2920) [`61e8edb`](https://github.com/Effect-TS/effect/commit/61e8edbf95ac9f1b2b0e9f4c2532bac42ac1f10b) Thanks @Iri-Hor! - in case of error let cli print to stderr instead of stdout (#1812) + +- Updated dependencies [[`4c6bc7f`](https://github.com/Effect-TS/effect/commit/4c6bc7f190c142dc9db70b365a2bf30715a98e62), [`a67d602`](https://github.com/Effect-TS/effect/commit/a67d60276f96cd20b76145b4cee13efca6c6158a)]: + - @effect/schema@0.67.20 + - @effect/platform@0.55.7 + +## 0.36.41 + +### Patch Changes + +- Updated dependencies [[`8c5d280`](https://github.com/Effect-TS/effect/commit/8c5d280c0402284a4e58372867a15a431cb99461), [`6ba6d26`](https://github.com/Effect-TS/effect/commit/6ba6d269f5891e6b11aa35c5281dde4bf3273004), [`cd7496b`](https://github.com/Effect-TS/effect/commit/cd7496ba214eabac2e3c297f513fcbd5b11f0e91), [`3f28bf2`](https://github.com/Effect-TS/effect/commit/3f28bf274333611906175446b772243f34f1b6d5), [`5817820`](https://github.com/Effect-TS/effect/commit/58178204a770d1a78c06945ef438f9fffbb50afa), [`349a036`](https://github.com/Effect-TS/effect/commit/349a036ffb08351481c060655660a6ccf26473de), [`799aa20`](https://github.com/Effect-TS/effect/commit/799aa20b4f618736ba33a5297fda90a75d4c26c6)]: + - effect@3.2.9 + - @effect/schema@0.67.19 + - @effect/platform@0.55.6 + - @effect/printer@0.33.25 + - @effect/printer-ansi@0.33.25 + +## 0.36.40 + +### Patch Changes + +- Updated dependencies [[`a0dd1c1`](https://github.com/Effect-TS/effect/commit/a0dd1c1ede2a1e856ecb0e67826ec992016fef97)]: + - @effect/schema@0.67.18 + - @effect/platform@0.55.5 + +## 0.36.39 + +### Patch Changes + +- Updated dependencies [[`d9d22e7`](https://github.com/Effect-TS/effect/commit/d9d22e7c4d5e31d5b46644c729b027796e467c16), [`3c080f7`](https://github.com/Effect-TS/effect/commit/3c080f74b2e2290edb6143c3aa01026e57f87a2a), [`3c080f7`](https://github.com/Effect-TS/effect/commit/3c080f74b2e2290edb6143c3aa01026e57f87a2a), [`7d6d875`](https://github.com/Effect-TS/effect/commit/7d6d8750077d9c8379f37240745240d7f3b7a4f8), [`70cda70`](https://github.com/Effect-TS/effect/commit/70cda704e8e31c80737b95121c8199e726ea132f), [`fb91f17`](https://github.com/Effect-TS/effect/commit/fb91f17098b48497feca9ec976feb87e4a82451b)]: + - @effect/schema@0.67.17 + - effect@3.2.8 + - @effect/platform@0.55.4 + - @effect/printer@0.33.24 + - @effect/printer-ansi@0.33.24 + +## 0.36.38 + +### Patch Changes + +- Updated dependencies [[`5745886`](https://github.com/Effect-TS/effect/commit/57458869859943410221ccc87f8cecfba7c79d92), [`6801fca`](https://github.com/Effect-TS/effect/commit/6801fca44366be3ee1b6b99f54bd4f38a1b5e4f4)]: + - @effect/schema@0.67.16 + - effect@3.2.7 + - @effect/platform@0.55.3 + - @effect/printer@0.33.23 + - @effect/printer-ansi@0.33.23 + +## 0.36.37 + +### Patch Changes + +- Updated dependencies [[`2c2280b`](https://github.com/Effect-TS/effect/commit/2c2280b98a11fc002663c55792a4fa5781cd5fb6), [`e2740fc`](https://github.com/Effect-TS/effect/commit/e2740fc4e212ba85a90541e8c8d85b0bcd5c2e7c), [`cc8ac50`](https://github.com/Effect-TS/effect/commit/cc8ac5080daba8622ca2ff5dab5c37ddfab732ba), [`60fe3d5`](https://github.com/Effect-TS/effect/commit/60fe3d5fb2be168dd35c6d0cb8ac8f55deb30fc0)]: + - @effect/platform@0.55.2 + - @effect/schema@0.67.15 + - effect@3.2.6 + - @effect/printer@0.33.22 + - @effect/printer-ansi@0.33.22 + +## 0.36.36 + +### Patch Changes + +- [#2847](https://github.com/Effect-TS/effect/pull/2847) [`7000173`](https://github.com/Effect-TS/effect/commit/70001732f4b04678772249377f06dbe448f15d6c) Thanks @ricardo-valero! - display a nicer message in the cli wizard when using Option + +- Updated dependencies [[`c5846e9`](https://github.com/Effect-TS/effect/commit/c5846e99137e9eb02efd31865e26f49f0d2c7c03)]: + - @effect/schema@0.67.14 + - @effect/platform@0.55.1 + +## 0.36.35 + +### Patch Changes + +- Updated dependencies [[`608b01f`](https://github.com/Effect-TS/effect/commit/608b01fc342dbae2a642b308a67b84ead530ecea), [`031c712`](https://github.com/Effect-TS/effect/commit/031c7122a24ac42e48d6a434646b4f5d279d7442), [`a44e532`](https://github.com/Effect-TS/effect/commit/a44e532cf3a6a498b12a5aacf8124aa267e24ba0), [`5133ca9`](https://github.com/Effect-TS/effect/commit/5133ca9dc4b8da0e28951316da9ab55dfbe0fbb9)]: + - effect@3.2.5 + - @effect/platform@0.55.0 + - @effect/printer@0.33.21 + - @effect/printer-ansi@0.33.21 + - @effect/schema@0.67.13 + +## 0.36.34 + +### Patch Changes + +- Updated dependencies [[`1af94df`](https://github.com/Effect-TS/effect/commit/1af94df6b74aeb4f6ebcbe80e074b4cb252e62e3), [`1af94df`](https://github.com/Effect-TS/effect/commit/1af94df6b74aeb4f6ebcbe80e074b4cb252e62e3), [`f8038ca`](https://github.com/Effect-TS/effect/commit/f8038cadd5f50d397469e5fdbc70dd8f69671f50), [`e376641`](https://github.com/Effect-TS/effect/commit/e3766411b60ebb45d31e9c9d94efa099121d4d58), [`c07e0ce`](https://github.com/Effect-TS/effect/commit/c07e0cea8ce165887e2c9dfa5d669eba9b2fb798), [`e313a01`](https://github.com/Effect-TS/effect/commit/e313a01b7e80f6cb7704055a190e5623c9d22c6d), [`1af94df`](https://github.com/Effect-TS/effect/commit/1af94df6b74aeb4f6ebcbe80e074b4cb252e62e3)]: + - effect@3.2.4 + - @effect/platform@0.54.0 + - @effect/schema@0.67.12 + - @effect/printer@0.33.20 + - @effect/printer-ansi@0.33.20 + +## 0.36.33 + +### Patch Changes + +- Updated dependencies [[`5af633e`](https://github.com/Effect-TS/effect/commit/5af633eb5ff6560a64d87263d1692bb9c75f7b3c), [`45578e8`](https://github.com/Effect-TS/effect/commit/45578e8faa80ae33d23e08f6f19467f818b7788f)]: + - @effect/schema@0.67.11 + - effect@3.2.3 + - @effect/platform@0.53.14 + - @effect/printer@0.33.19 + - @effect/printer-ansi@0.33.19 + +## 0.36.32 + +### Patch Changes + +- Updated dependencies [[`5d9266e`](https://github.com/Effect-TS/effect/commit/5d9266e8c740746ac9e186c3df6090a1b57fbe2a), [`9f8122e`](https://github.com/Effect-TS/effect/commit/9f8122e78884ab47c5e5f364d86eee1d1543cc61), [`6a6f670`](https://github.com/Effect-TS/effect/commit/6a6f6706b8613c8c7c10971b8d81a0f9e440a6f2), [`c1eaef9`](https://github.com/Effect-TS/effect/commit/c1eaef910420dae416923d172ee58d219e921d0f), [`78ffc27`](https://github.com/Effect-TS/effect/commit/78ffc27ee3fa708433c25fa118c53d38d90d08bc)]: + - effect@3.2.2 + - @effect/platform@0.53.13 + - @effect/schema@0.67.10 + - @effect/printer@0.33.18 + - @effect/printer-ansi@0.33.18 + +## 0.36.31 + +### Patch Changes + +- Updated dependencies [[`5432fff`](https://github.com/Effect-TS/effect/commit/5432fff7c9a69d43910426c1053ebfc3b73ebed6)]: + - @effect/schema@0.67.9 + - @effect/platform@0.53.12 + +## 0.36.30 + +### Patch Changes + +- Updated dependencies [[`c1e991d`](https://github.com/Effect-TS/effect/commit/c1e991dd5ba87901cd0e05697a8b4a267e7e954a)]: + - effect@3.2.1 + - @effect/platform@0.53.11 + - @effect/printer@0.33.17 + - @effect/printer-ansi@0.33.17 + - @effect/schema@0.67.8 + +## 0.36.29 + +### Patch Changes + +- Updated dependencies [[`146cadd`](https://github.com/Effect-TS/effect/commit/146cadd9d004634a3ff85c480bf92cf975c853e2), [`7135748`](https://github.com/Effect-TS/effect/commit/713574813a0f64085db0b5240ba39e7a0a7c137e), [`7135748`](https://github.com/Effect-TS/effect/commit/713574813a0f64085db0b5240ba39e7a0a7c137e), [`963b4e7`](https://github.com/Effect-TS/effect/commit/963b4e7ac87e2468feb6a344f7ab4ee4ad711198), [`64c9414`](https://github.com/Effect-TS/effect/commit/64c9414e960e82058ca09bbb3976d6fbef303a8e), [`7135748`](https://github.com/Effect-TS/effect/commit/713574813a0f64085db0b5240ba39e7a0a7c137e), [`2cbb76b`](https://github.com/Effect-TS/effect/commit/2cbb76bb52500a3f4bf27d1c91482518cbea56d7), [`870c5fa`](https://github.com/Effect-TS/effect/commit/870c5fa52cd61e745e8e828d38c3f09f00737553), [`146cadd`](https://github.com/Effect-TS/effect/commit/146cadd9d004634a3ff85c480bf92cf975c853e2), [`7135748`](https://github.com/Effect-TS/effect/commit/713574813a0f64085db0b5240ba39e7a0a7c137e), [`64c9414`](https://github.com/Effect-TS/effect/commit/64c9414e960e82058ca09bbb3976d6fbef303a8e)]: + - effect@3.2.0 + - @effect/platform@0.53.10 + - @effect/printer@0.33.16 + - @effect/printer-ansi@0.33.16 + - @effect/schema@0.67.7 + +## 0.36.28 + +### Patch Changes + +- Updated dependencies [[`17da864`](https://github.com/Effect-TS/effect/commit/17da864e4a6f80becdb82db7dece2ba583bfdda3), [`17fc22e`](https://github.com/Effect-TS/effect/commit/17fc22e132593c5caa563705a4748ba0f04a853c), [`810f222`](https://github.com/Effect-TS/effect/commit/810f222268792b13067c7a7bf317b93a9bb8917b), [`596aaea`](https://github.com/Effect-TS/effect/commit/596aaea022648b2e06fb1ec22f1652043d6fe64e), [`ff0efa0`](https://github.com/Effect-TS/effect/commit/ff0efa0a1415a41d4a4312a16cf7a63def86db3f)]: + - @effect/schema@0.67.6 + - @effect/platform@0.53.9 + - effect@3.1.6 + - @effect/printer@0.33.15 + - @effect/printer-ansi@0.33.15 + +## 0.36.27 + +### Patch Changes + +- Updated dependencies [[`9c514de`](https://github.com/Effect-TS/effect/commit/9c514de28152696edff008324d2d7e67d55afd56)]: + - @effect/schema@0.67.5 + - @effect/platform@0.53.8 + +## 0.36.26 + +### Patch Changes + +- Updated dependencies [[`ee08593`](https://github.com/Effect-TS/effect/commit/ee0859398ecc2589cab0d017bef6a17e00c34dfd), [`da6d7d8`](https://github.com/Effect-TS/effect/commit/da6d7d845246e9d04631d64fa7694944b6010d09)]: + - @effect/schema@0.67.4 + - @effect/platform@0.53.7 + +## 0.36.25 + +### Patch Changes + +- Updated dependencies [[`6ac4847`](https://github.com/Effect-TS/effect/commit/6ac48479447c01a4f35d655552af93e47e562610), [`6ac4847`](https://github.com/Effect-TS/effect/commit/6ac48479447c01a4f35d655552af93e47e562610)]: + - @effect/platform@0.53.6 + - effect@3.1.5 + - @effect/printer@0.33.14 + - @effect/printer-ansi@0.33.14 + - @effect/schema@0.67.3 + +## 0.36.24 + +### Patch Changes + +- Updated dependencies [[`89a3afb`](https://github.com/Effect-TS/effect/commit/89a3afbe191c83b84b17bfaa95519aff0749afbe), [`992c8e2`](https://github.com/Effect-TS/effect/commit/992c8e21535db9f0c66e81d32fee8af56a96274f)]: + - @effect/schema@0.67.2 + - @effect/platform@0.53.5 + +## 0.36.23 + +### Patch Changes + +- Updated dependencies [[`e41e911`](https://github.com/Effect-TS/effect/commit/e41e91122fa6dd12fc81e50dcad0db891be67146)]: + - effect@3.1.4 + - @effect/platform@0.53.4 + - @effect/printer@0.33.13 + - @effect/printer-ansi@0.33.13 + - @effect/schema@0.67.1 + +## 0.36.22 + +### Patch Changes + +- Updated dependencies [[`d7e4997`](https://github.com/Effect-TS/effect/commit/d7e49971fe97b7ee5fb7991f3f5ac4d627a26338)]: + - @effect/schema@0.67.0 + - @effect/platform@0.53.3 + +## 0.36.21 + +### Patch Changes + +- Updated dependencies [[`1f6dc96`](https://github.com/Effect-TS/effect/commit/1f6dc96f51c7bb9c8d11415358308604ba7c7c8e)]: + - effect@3.1.3 + - @effect/platform@0.53.2 + - @effect/printer@0.33.12 + - @effect/printer-ansi@0.33.12 + - @effect/schema@0.66.16 + +## 0.36.20 + +### Patch Changes + +- Updated dependencies [[`121d6d9`](https://github.com/Effect-TS/effect/commit/121d6d93755138c7510ba3ab4f0019ec0cb91890)]: + - @effect/schema@0.66.15 + - @effect/platform@0.53.1 + +## 0.36.19 + +### Patch Changes + +- Updated dependencies [[`d57fbbb`](https://github.com/Effect-TS/effect/commit/d57fbbbd6c466936213a671fc3cd2390064f864e)]: + - @effect/platform@0.53.0 + +## 0.36.18 + +### Patch Changes + +- Updated dependencies [[`5866c62`](https://github.com/Effect-TS/effect/commit/5866c621d7eb4cc84e4ba972bfdfd219734cd45d)]: + - @effect/platform@0.52.3 + +## 0.36.17 + +### Patch Changes + +- Updated dependencies [[`2e1cdf6`](https://github.com/Effect-TS/effect/commit/2e1cdf67d141281288fffe9a5c10d1379a800513)]: + - @effect/platform@0.52.2 + - effect@3.1.2 + - @effect/schema@0.66.14 + - @effect/printer@0.33.11 + - @effect/printer-ansi@0.33.11 + +## 0.36.16 + +### Patch Changes + +- Updated dependencies [[`e5e56d1`](https://github.com/Effect-TS/effect/commit/e5e56d138dbed3204636f605229c6685f89659fc)]: + - effect@3.1.1 + - @effect/platform@0.52.1 + - @effect/printer@0.33.10 + - @effect/printer-ansi@0.33.10 + - @effect/schema@0.66.13 + +## 0.36.15 + +### Patch Changes + +- Updated dependencies [[`9deab0a`](https://github.com/Effect-TS/effect/commit/9deab0aec9e99501f9441843e34df9afa10c5be9), [`7719b8a`](https://github.com/Effect-TS/effect/commit/7719b8a7350c14e952ffe685bfd5308773b3e271)]: + - @effect/platform@0.52.0 + +## 0.36.14 + +### Patch Changes + +- Updated dependencies [[`c3c12c6`](https://github.com/Effect-TS/effect/commit/c3c12c6625633fe80e79f9db75a3b8cf8ca8b11d), [`ba64ea6`](https://github.com/Effect-TS/effect/commit/ba64ea6757810c5e74cad3863a7d19d4d38af66b), [`b5de2d2`](https://github.com/Effect-TS/effect/commit/b5de2d2ce5b1afe8be90827bf898a95cec40eb2b), [`a1c7ab8`](https://github.com/Effect-TS/effect/commit/a1c7ab8ffedacd18c1fc784f4ff5844f79498b83), [`a023f28`](https://github.com/Effect-TS/effect/commit/a023f28336f3865687d9a30c1883e36909906d85), [`0ec93cb`](https://github.com/Effect-TS/effect/commit/0ec93cb4f166e7401c171c2f8e8276ce958d9a57), [`1c9454d`](https://github.com/Effect-TS/effect/commit/1c9454d532eae79b9f759aea77f59332cc6d18ed), [`a023f28`](https://github.com/Effect-TS/effect/commit/a023f28336f3865687d9a30c1883e36909906d85), [`92d56db`](https://github.com/Effect-TS/effect/commit/92d56dbb3f33e36636c2a2f1030c56492e39cf4d)]: + - effect@3.1.0 + - @effect/platform@0.51.0 + - @effect/printer@0.33.9 + - @effect/printer-ansi@0.33.9 + - @effect/schema@0.66.12 + +## 0.36.13 + +### Patch Changes + +- [#2656](https://github.com/Effect-TS/effect/pull/2656) [`557707b`](https://github.com/Effect-TS/effect/commit/557707bc9e5f230c8964d2757012075c34339b5c) Thanks [@tim-smart](https://github.com/tim-smart)! - update dependencies + +- Updated dependencies [[`16039a0`](https://github.com/Effect-TS/effect/commit/16039a08f04f11545e2fdf40952788a8f9cef04f), [`557707b`](https://github.com/Effect-TS/effect/commit/557707bc9e5f230c8964d2757012075c34339b5c), [`d1d33e1`](https://github.com/Effect-TS/effect/commit/d1d33e10b25109f44b5ab1c6e4d778a59c0d3eeb), [`f4ed306`](https://github.com/Effect-TS/effect/commit/f4ed3068a70b50302d078a30d18ca3cfd2bc679c), [`661004f`](https://github.com/Effect-TS/effect/commit/661004f4bf5f8b25f5a0678c21a3a822188ce461), [`e79cb83`](https://github.com/Effect-TS/effect/commit/e79cb83d3b19098bc40a3012e2a059b8426306c2)]: + - @effect/platform@0.50.8 + - effect@3.0.8 + - @effect/schema@0.66.11 + - @effect/printer@0.33.8 + - @effect/printer-ansi@0.33.8 + +## 0.36.12 + +### Patch Changes + +- Updated dependencies [[`18de56b`](https://github.com/Effect-TS/effect/commit/18de56b4a6b6d1f99230dfabf9147d59ea4dd759)]: + - effect@3.0.7 + - @effect/platform@0.50.7 + - @effect/printer@0.33.7 + - @effect/printer-ansi@0.33.7 + - @effect/schema@0.66.10 + +## 0.36.11 + +### Patch Changes + +- Updated dependencies [[`ffe4f4e`](https://github.com/Effect-TS/effect/commit/ffe4f4e95db35fff6869e360b072e3837befa0a1), [`027418e`](https://github.com/Effect-TS/effect/commit/027418edaa6aa6c0ae4861b95832827b45adace4), [`ac1898e`](https://github.com/Effect-TS/effect/commit/ac1898eb7bc96880f911c276048e2ea3d6fe9c50), [`ffe4f4e`](https://github.com/Effect-TS/effect/commit/ffe4f4e95db35fff6869e360b072e3837befa0a1), [`8206529`](https://github.com/Effect-TS/effect/commit/8206529d6a7bbf3e3c6f670afb0381e83176736e)]: + - effect@3.0.6 + - @effect/schema@0.66.9 + - @effect/platform@0.50.6 + - @effect/printer@0.33.6 + - @effect/printer-ansi@0.33.6 + +## 0.36.10 + +### Patch Changes + +- Updated dependencies [[`6222404`](https://github.com/Effect-TS/effect/commit/62224044678751829ed2f128e05133a91c6b0569), [`868ed2a`](https://github.com/Effect-TS/effect/commit/868ed2a8fe94ee7f4206a6070f29dcf2a5ba1dc3)]: + - effect@3.0.5 + - @effect/platform@0.50.5 + - @effect/printer@0.33.5 + - @effect/printer-ansi@0.33.5 + - @effect/schema@0.66.8 + +## 0.36.9 + +### Patch Changes + +- Updated dependencies [[`dd41c6c`](https://github.com/Effect-TS/effect/commit/dd41c6c725b1c1c980683275d8fa69779902187e), [`9a24667`](https://github.com/Effect-TS/effect/commit/9a246672008a2b668d43fbfd2fe5508c54b2b920)]: + - @effect/schema@0.66.7 + - effect@3.0.4 + - @effect/platform@0.50.4 + - @effect/printer@0.33.4 + - @effect/printer-ansi@0.33.4 + +## 0.36.8 + +### Patch Changes + +- Updated dependencies [[`9dfc156`](https://github.com/Effect-TS/effect/commit/9dfc156dc13fb4da9c777aae3acece4b5ecf0064), [`80271bd`](https://github.com/Effect-TS/effect/commit/80271bdc648e9efa659ce66b2c255754a6a1a8b0), [`b3b51a2`](https://github.com/Effect-TS/effect/commit/b3b51a2ea0c6ab92a363db46ebaa7e1176d089f5), [`e4ba97d`](https://github.com/Effect-TS/effect/commit/e4ba97d060c16bdf4e3b5bd5db6777f121a6768c)]: + - @effect/schema@0.66.6 + - @effect/platform@0.50.3 + +## 0.36.7 + +### Patch Changes + +- Updated dependencies [[`b3fe829`](https://github.com/Effect-TS/effect/commit/b3fe829e8b12726afe94086b5375968f41a26411), [`a58b7de`](https://github.com/Effect-TS/effect/commit/a58b7deb8bb1d3b0dd636decf5d16f115f37eb72), [`d90e8c3`](https://github.com/Effect-TS/effect/commit/d90e8c3090cbc78e2bc7b51c974df66ffefacdfa)]: + - @effect/schema@0.66.5 + - @effect/platform@0.50.2 + +## 0.36.6 + +### Patch Changes + +- Updated dependencies [[`773b8e0`](https://github.com/Effect-TS/effect/commit/773b8e01521e8fa7c38ff15d92d21d6fd6dad56f)]: + - @effect/schema@0.66.4 + - @effect/platform@0.50.1 + +## 0.36.5 + +### Patch Changes + +- Updated dependencies [[`6f38dff`](https://github.com/Effect-TS/effect/commit/6f38dff41ffa34532cc2f25b90446550c5730bb6), [`a7b4b84`](https://github.com/Effect-TS/effect/commit/a7b4b84bd5a25f51aba922f9259c3a58c98c6a4e), [`a3b0e6c`](https://github.com/Effect-TS/effect/commit/a3b0e6c490772e6d44b5d98dcf2729c4d5310ecc), [`6f38dff`](https://github.com/Effect-TS/effect/commit/6f38dff41ffa34532cc2f25b90446550c5730bb6)]: + - @effect/platform@0.50.0 + - effect@3.0.3 + - @effect/printer@0.33.3 + - @effect/printer-ansi@0.33.3 + - @effect/schema@0.66.3 + +## 0.36.4 + +### Patch Changes + +- [#2562](https://github.com/Effect-TS/effect/pull/2562) [`2cecdbd`](https://github.com/Effect-TS/effect/commit/2cecdbd1cf30befce4e84796ccd953ea55ecfb86) Thanks [@fubhy](https://github.com/fubhy)! - Added provenance publishing + +- Updated dependencies [[`2cecdbd`](https://github.com/Effect-TS/effect/commit/2cecdbd1cf30befce4e84796ccd953ea55ecfb86)]: + - @effect/printer-ansi@0.33.2 + - @effect/platform@0.49.4 + - @effect/printer@0.33.2 + - effect@3.0.2 + - @effect/schema@0.66.2 + +## 0.36.3 + +### Patch Changes + +- Updated dependencies [[`8d39d65`](https://github.com/Effect-TS/effect/commit/8d39d6554af548228ad767112ce2e0b1f68fa8e1)]: + - @effect/platform@0.49.3 + +## 0.36.2 + +### Patch Changes + +- Updated dependencies [[`5ef0a1a`](https://github.com/Effect-TS/effect/commit/5ef0a1ae9b773fa2481550cb0d43ff7a0e03cd44)]: + - @effect/platform@0.49.2 + +## 0.36.1 + +### Patch Changes + +- [#2555](https://github.com/Effect-TS/effect/pull/2555) [`8edacca`](https://github.com/Effect-TS/effect/commit/8edacca37f8e37c01a63fec332b06d9361efaa7b) Thanks [@tim-smart](https://github.com/tim-smart)! - prevent use of `Array` as import name to solve bundler issues + +- Updated dependencies [[`3da0cfa`](https://github.com/Effect-TS/effect/commit/3da0cfa12c407fd930dc480be1ecc9217a8058f8), [`570e8d8`](https://github.com/Effect-TS/effect/commit/570e8d87e7c0e9ad4cd2686462fdb9b4812f7716), [`b2b5d66`](https://github.com/Effect-TS/effect/commit/b2b5d6626b18eb5289f364ffab5240e84b04d085), [`87c5687`](https://github.com/Effect-TS/effect/commit/87c5687de0782dab177b7861217fa3b040046282), [`8edacca`](https://github.com/Effect-TS/effect/commit/8edacca37f8e37c01a63fec332b06d9361efaa7b)]: + - effect@3.0.1 + - @effect/schema@0.66.1 + - @effect/platform@0.49.1 + - @effect/printer-ansi@0.33.1 + - @effect/printer@0.33.1 + +## 0.36.0 + +### Minor Changes + +- [#2207](https://github.com/Effect-TS/effect/pull/2207) [`5a2314b`](https://github.com/Effect-TS/effect/commit/5a2314b70ec79c2c02b51cef45a5ddec8327daa1) Thanks [@github-actions](https://github.com/apps/github-actions)! - replace use of `unit` terminology with `void` + + For all the data types. + + ```ts + Effect.unit // => Effect.void + Stream.unit // => Stream.void + + // etc + ``` + +- [#2207](https://github.com/Effect-TS/effect/pull/2207) [`2fb7d9c`](https://github.com/Effect-TS/effect/commit/2fb7d9ca15037ff62a578bb9fe5732da5f4f317d) Thanks [@github-actions](https://github.com/apps/github-actions)! - Release Effect 3.0 🎉 + +### Patch Changes + +- [#2529](https://github.com/Effect-TS/effect/pull/2529) [`78b767c`](https://github.com/Effect-TS/effect/commit/78b767c2b1625186e17131761a0edbac25d21850) Thanks [@fubhy](https://github.com/fubhy)! - Renamed `ReadonlyArray` and `ReadonlyRecord` modules for better discoverability. + +- Updated dependencies [[`1b5f0c7`](https://github.com/Effect-TS/effect/commit/1b5f0c77e7fd477a0026071e82129a948227f4b3), [`d50a652`](https://github.com/Effect-TS/effect/commit/d50a652479f4d1d64f48da05c79fa847e6e51548), [`9aeae46`](https://github.com/Effect-TS/effect/commit/9aeae461fdf9265389cf3dfe4e428b037215ba5f), [`9a3bd47`](https://github.com/Effect-TS/effect/commit/9a3bd47ebd0750c7e498162734f6d21895de0cb2), [`6460414`](https://github.com/Effect-TS/effect/commit/6460414351a45fb8e0a457c63f3653422efee766), [`cf69f46`](https://github.com/Effect-TS/effect/commit/cf69f46690058d71eeada03cfb40dc744573e9e4), [`cf69f46`](https://github.com/Effect-TS/effect/commit/cf69f46690058d71eeada03cfb40dc744573e9e4), [`e542371`](https://github.com/Effect-TS/effect/commit/e542371981f8b4b484979feaad8a25b1f45e2df0), [`be9d025`](https://github.com/Effect-TS/effect/commit/be9d025e42355260ace02dd135851a8935a4deba), [`78b767c`](https://github.com/Effect-TS/effect/commit/78b767c2b1625186e17131761a0edbac25d21850), [`aa4a3b5`](https://github.com/Effect-TS/effect/commit/aa4a3b550da1c1020265ac389ed3f309388994a2), [`1499974`](https://github.com/Effect-TS/effect/commit/14999741d2e19c1747f6a7e19d68977f6429cdb8), [`1b5f0c7`](https://github.com/Effect-TS/effect/commit/1b5f0c77e7fd477a0026071e82129a948227f4b3), [`5c2b561`](https://github.com/Effect-TS/effect/commit/5c2b5614f583b88784ed68126ae939832fb3c092), [`6c6087a`](https://github.com/Effect-TS/effect/commit/6c6087a4a897b64252346426660782d31c13f769), [`a18f594`](https://github.com/Effect-TS/effect/commit/a18f5948f1439a147232448b2c443472fda0eceb), [`1499974`](https://github.com/Effect-TS/effect/commit/14999741d2e19c1747f6a7e19d68977f6429cdb8), [`2f96d93`](https://github.com/Effect-TS/effect/commit/2f96d938b90f8c19377583279e3c7afd9b509c50), [`5a2314b`](https://github.com/Effect-TS/effect/commit/5a2314b70ec79c2c02b51cef45a5ddec8327daa1), [`25d74f8`](https://github.com/Effect-TS/effect/commit/25d74f8c4d2dd4a9e5ec57ce2f20d36dedd25343), [`6c6087a`](https://github.com/Effect-TS/effect/commit/6c6087a4a897b64252346426660782d31c13f769), [`271b79f`](https://github.com/Effect-TS/effect/commit/271b79fc0b66a6c11e07a8779ff8800493a7eac2), [`1b5f0c7`](https://github.com/Effect-TS/effect/commit/1b5f0c77e7fd477a0026071e82129a948227f4b3), [`2fb7d9c`](https://github.com/Effect-TS/effect/commit/2fb7d9ca15037ff62a578bb9fe5732da5f4f317d), [`53d1c2a`](https://github.com/Effect-TS/effect/commit/53d1c2a77559081fbb89667e343346375c6d6650), [`e7e1bbe`](https://github.com/Effect-TS/effect/commit/e7e1bbe68486fdf31c8f84b0880522d39adcaad3), [`10c169e`](https://github.com/Effect-TS/effect/commit/10c169eadc874e91b4defca3f467b4e6a50fd8f3), [`6424181`](https://github.com/Effect-TS/effect/commit/64241815fe6a939e91e6947253e7dceea1306aa8)]: + - effect@3.0.0 + - @effect/schema@0.66.0 + - @effect/platform@0.49.0 + - @effect/printer-ansi@0.33.0 + - @effect/printer@0.33.0 + +## 0.35.31 + +### Patch Changes + +- Updated dependencies [[`b79cc59`](https://github.com/Effect-TS/effect/commit/b79cc59dbe64b9a0a7742dc9100a9d36c8e46b72)]: + - @effect/platform@0.48.29 + +## 0.35.30 + +### Patch Changes + +- Updated dependencies [[`0aee906`](https://github.com/Effect-TS/effect/commit/0aee906f034539344db6fbac08919de3e28eccde), [`41c8102`](https://github.com/Effect-TS/effect/commit/41c810228b1a50e4b41f19e735d7c62fe8d36871), [`4c37001`](https://github.com/Effect-TS/effect/commit/4c370013417e18c4f564818de1341a8fccb43b4c), [`776ef2b`](https://github.com/Effect-TS/effect/commit/776ef2bb66db9aa9f68b7beab14f6986f9c1288b), [`217147e`](https://github.com/Effect-TS/effect/commit/217147ea67c5c42c96f024775c41e5b070f81e4c), [`8a69b4e`](https://github.com/Effect-TS/effect/commit/8a69b4ef6a3a06d2e21fe2e11a626038beefb4e1), [`90776ec`](https://github.com/Effect-TS/effect/commit/90776ec8e8671d835b65fc33ead1de6c864b81b9), [`b3acf47`](https://github.com/Effect-TS/effect/commit/b3acf47f9c9dfae1c99377aa906097aaa2d47d44), [`8709856`](https://github.com/Effect-TS/effect/commit/870985694ae985c3cb9360ad8a25c60e6f785f55), [`232c353`](https://github.com/Effect-TS/effect/commit/232c353c2e6f743f38e57639ee30e324ffa9c2a9), [`0d3231a`](https://github.com/Effect-TS/effect/commit/0d3231a195202635ecc0bf6bbf6a08fc017d0d69), [`0ca835c`](https://github.com/Effect-TS/effect/commit/0ca835cbac8e69072a93ace83b534219faba24e8), [`8709856`](https://github.com/Effect-TS/effect/commit/870985694ae985c3cb9360ad8a25c60e6f785f55), [`d590094`](https://github.com/Effect-TS/effect/commit/d5900943489ec1e0891836aeafb5ce99fb9c75c7), [`c22b019`](https://github.com/Effect-TS/effect/commit/c22b019e5eaf9d3a937a3d99cadbb8f8e9116a70), [`e983740`](https://github.com/Effect-TS/effect/commit/e9837401145605aff5bc2ec7e73004f397c5d2d1), [`e3e0924`](https://github.com/Effect-TS/effect/commit/e3e09247d46a35430fc60e4aa4032cc50814f212)]: + - @effect/schema@0.65.0 + - effect@2.4.19 + - @effect/platform@0.48.28 + - @effect/printer@0.32.4 + - @effect/printer-ansi@0.32.28 + +## 0.35.29 + +### Patch Changes + +- Updated dependencies [[`c6dd3c6`](https://github.com/Effect-TS/effect/commit/c6dd3c6909cafe05adc8450c5a499260e17e60d3), [`672f137`](https://github.com/Effect-TS/effect/commit/672f13747ddf6dac3ba304fd4511b1df44ab566d), [`42b3651`](https://github.com/Effect-TS/effect/commit/42b36519f356bae9258a1ea1d416e2902b973e85), [`672f137`](https://github.com/Effect-TS/effect/commit/672f13747ddf6dac3ba304fd4511b1df44ab566d)]: + - @effect/platform@0.48.27 + - @effect/schema@0.64.20 + +## 0.35.28 + +### Patch Changes + +- Updated dependencies [[`365a486`](https://github.com/Effect-TS/effect/commit/365a4865de5e47ce09f4cfd51fc0f67438f82a57)]: + - @effect/platform@0.48.26 + +## 0.35.27 + +### Patch Changes + +- Updated dependencies [[`dadc690`](https://github.com/Effect-TS/effect/commit/dadc6906121c512bc32be22b52adbd1ada834594), [`58f66fe`](https://github.com/Effect-TS/effect/commit/58f66fecd4e646c6c8f10995df9faab17022eb8f), [`3cad21d`](https://github.com/Effect-TS/effect/commit/3cad21daa5d2332d33692498c87b7ffff979e304), [`d209171`](https://github.com/Effect-TS/effect/commit/d2091714a786820ebae4bef04a9d67d25dd08e88)]: + - effect@2.4.18 + - @effect/schema@0.64.19 + - @effect/platform@0.48.25 + - @effect/printer@0.32.3 + - @effect/printer-ansi@0.32.27 + +## 0.35.26 + +### Patch Changes + +- Updated dependencies [[`9c6a500`](https://github.com/Effect-TS/effect/commit/9c6a5001b467b6255c68a922f4b6e8d692b63d01), [`8fdfda6`](https://github.com/Effect-TS/effect/commit/8fdfda6618be848c01b399d13bc05a9a3adfb613), [`607b2e7`](https://github.com/Effect-TS/effect/commit/607b2e7a7fd9318c57acf4e50ec61747eea74ad7), [`8fdfda6`](https://github.com/Effect-TS/effect/commit/8fdfda6618be848c01b399d13bc05a9a3adfb613), [`35ad0ba`](https://github.com/Effect-TS/effect/commit/35ad0ba9f3ba27c60453620e514b980f819f92af), [`8206caf`](https://github.com/Effect-TS/effect/commit/8206caf7c2d22c68be4313318b61cfdacf6222b6), [`7ddd654`](https://github.com/Effect-TS/effect/commit/7ddd65415b65ccb654ad04f4dbefe39402f15117), [`7ddd654`](https://github.com/Effect-TS/effect/commit/7ddd65415b65ccb654ad04f4dbefe39402f15117), [`8fdfda6`](https://github.com/Effect-TS/effect/commit/8fdfda6618be848c01b399d13bc05a9a3adfb613), [`f456ba2`](https://github.com/Effect-TS/effect/commit/f456ba273bae21a6dcf8c966c50c97b5f0897d9f)]: + - @effect/platform@0.48.24 + - effect@2.4.17 + - @effect/printer@0.32.2 + - @effect/printer-ansi@0.32.26 + - @effect/schema@0.64.18 + +## 0.35.25 + +### Patch Changes + +- Updated dependencies [[`5170ce7`](https://github.com/Effect-TS/effect/commit/5170ce708c606283e8a30d273950f1a21c7eddc2), [`63a1df2`](https://github.com/Effect-TS/effect/commit/63a1df2e4de3766f48f15676fbd0360ab9c27816), [`5170ce7`](https://github.com/Effect-TS/effect/commit/5170ce708c606283e8a30d273950f1a21c7eddc2), [`62a7f23`](https://github.com/Effect-TS/effect/commit/62a7f23937c0dfaca67a7b2f055b85cfde25ed11), [`7cc2b41`](https://github.com/Effect-TS/effect/commit/7cc2b41d6c551fdca2590b06681c5ad9832aba46), [`74a5dae`](https://github.com/Effect-TS/effect/commit/74a5daed0e65b32a36e026bfcf66d02269cb967a), [`8b46fde`](https://github.com/Effect-TS/effect/commit/8b46fdebf2c075a74cd2cd29dfb69531d20fc154)]: + - @effect/platform@0.48.23 + - effect@2.4.16 + - @effect/schema@0.64.17 + - @effect/printer@0.32.1 + - @effect/printer-ansi@0.32.25 + +## 0.35.24 + +### Patch Changes + +- Updated dependencies [[`a31917a`](https://github.com/Effect-TS/effect/commit/a31917aa4b05b1189b7a8e0bedb60bb3d49262ad), [`4cd2bed`](https://github.com/Effect-TS/effect/commit/4cd2bedf978f864bddd289d1c524c8e868bf587b), [`6cc6267`](https://github.com/Effect-TS/effect/commit/6cc6267026d9bfb1a9882cddf534787327e86ec1)]: + - @effect/schema@0.64.16 + - @effect/platform@0.48.22 + +## 0.35.23 + +### Patch Changes + +- Updated dependencies [[`d7688c0`](https://github.com/Effect-TS/effect/commit/d7688c0c72717fe7876c871567f6946dabfc0546), [`b3a4fac`](https://github.com/Effect-TS/effect/commit/b3a4face2acaca422f0b0530436e8f13129f3b3a), [`0dd62a7`](https://github.com/Effect-TS/effect/commit/0dd62a701934b44c4c78e2d7878afdccfe414c39), [`5ded019`](https://github.com/Effect-TS/effect/commit/5ded019970169e3c1f2a375d0876b95fb1ff67f5), [`0dd62a7`](https://github.com/Effect-TS/effect/commit/0dd62a701934b44c4c78e2d7878afdccfe414c39), [`0dd62a7`](https://github.com/Effect-TS/effect/commit/0dd62a701934b44c4c78e2d7878afdccfe414c39)]: + - effect@2.4.15 + - @effect/printer@0.32.0 + - @effect/schema@0.64.15 + - @effect/printer-ansi@0.32.24 + - @effect/platform@0.48.21 + +## 0.35.22 + +### Patch Changes + +- Updated dependencies [[`4789083`](https://github.com/Effect-TS/effect/commit/4789083283bdaec456982d614ebc4a496ea0e7f7), [`4789083`](https://github.com/Effect-TS/effect/commit/4789083283bdaec456982d614ebc4a496ea0e7f7)]: + - @effect/platform@0.48.20 + +## 0.35.21 + +### Patch Changes + +- Updated dependencies [[`fb7285e`](https://github.com/Effect-TS/effect/commit/fb7285e8d6a70527df7137a6a3efdd03ae61cb8b)]: + - @effect/platform@0.48.19 + +## 0.35.20 + +### Patch Changes + +- Updated dependencies [[`26435ec`](https://github.com/Effect-TS/effect/commit/26435ecfa06569dc18d1801ccf38213a43b7c334), [`a76e5e1`](https://github.com/Effect-TS/effect/commit/a76e5e131a35c88a72771fb745df08f60fbc0e18), [`6180c0c`](https://github.com/Effect-TS/effect/commit/6180c0cc51dee785cfce72220a52c9fc3b9bf9aa)]: + - @effect/platform@0.48.18 + - @effect/schema@0.64.14 + - effect@2.4.14 + - @effect/printer@0.31.23 + - @effect/printer-ansi@0.32.23 + +## 0.35.19 + +### Patch Changes + +- Updated dependencies [[`47a8f1b`](https://github.com/Effect-TS/effect/commit/47a8f1b644d8294692d92cacd3c8c7543edbfabe), [`8c9abe2`](https://github.com/Effect-TS/effect/commit/8c9abe2b35c46d8891d4b2c14ff9eb46302a14f3), [`8c9abe2`](https://github.com/Effect-TS/effect/commit/8c9abe2b35c46d8891d4b2c14ff9eb46302a14f3), [`3336287`](https://github.com/Effect-TS/effect/commit/3336287ff55a25e56d759b83847bfaa21c40f499), [`54b7c00`](https://github.com/Effect-TS/effect/commit/54b7c0077fa784ad2646b812d6a44641f672edcd), [`3336287`](https://github.com/Effect-TS/effect/commit/3336287ff55a25e56d759b83847bfaa21c40f499), [`47a8f1b`](https://github.com/Effect-TS/effect/commit/47a8f1b644d8294692d92cacd3c8c7543edbfabe)]: + - @effect/platform@0.48.17 + - effect@2.4.13 + - @effect/printer@0.31.22 + - @effect/printer-ansi@0.32.22 + - @effect/schema@0.64.13 + +## 0.35.18 + +### Patch Changes + +- [#2385](https://github.com/Effect-TS/effect/pull/2385) [`3307729`](https://github.com/Effect-TS/effect/commit/3307729de162a033fa9caa8e14c111013dcf0d87) Thanks [@tim-smart](https://github.com/tim-smart)! - update typescript to 5.4 + +- Updated dependencies [[`9392de6`](https://github.com/Effect-TS/effect/commit/9392de6baa6861662abc2bd3171897145f5ea073), [`75a8d16`](https://github.com/Effect-TS/effect/commit/75a8d16247cc14860cdd7fd948ef542c50c2d55e), [`3307729`](https://github.com/Effect-TS/effect/commit/3307729de162a033fa9caa8e14c111013dcf0d87), [`9392de6`](https://github.com/Effect-TS/effect/commit/9392de6baa6861662abc2bd3171897145f5ea073), [`3307729`](https://github.com/Effect-TS/effect/commit/3307729de162a033fa9caa8e14c111013dcf0d87), [`d17a427`](https://github.com/Effect-TS/effect/commit/d17a427c4427972fb55c45a058780716dc408631)]: + - @effect/schema@0.64.12 + - @effect/platform@0.48.16 + - effect@2.4.12 + - @effect/printer@0.31.21 + - @effect/printer-ansi@0.32.21 + +## 0.35.17 + +### Patch Changes + +- [#2384](https://github.com/Effect-TS/effect/pull/2384) [`2f488c4`](https://github.com/Effect-TS/effect/commit/2f488c436de52576562803c57ebc132ef40ccdd8) Thanks [@tim-smart](https://github.com/tim-smart)! - update dependencies + +- Updated dependencies [[`2f488c4`](https://github.com/Effect-TS/effect/commit/2f488c436de52576562803c57ebc132ef40ccdd8), [`37ca592`](https://github.com/Effect-TS/effect/commit/37ca592a4101ad90adbf8c8b3f727faf3110cae5), [`317b5b8`](https://github.com/Effect-TS/effect/commit/317b5b8e8c8c2207469b3ebfcf72bf3a9f7cbc60)]: + - effect@2.4.11 + - @effect/schema@0.64.11 + - @effect/platform@0.48.15 + - @effect/printer@0.31.20 + - @effect/printer-ansi@0.32.20 + +## 0.35.16 + +### Patch Changes + +- Updated dependencies [[`9bab1f9`](https://github.com/Effect-TS/effect/commit/9bab1f9fa5b999740755e4e82485cb77c638643a), [`9bbde5b`](https://github.com/Effect-TS/effect/commit/9bbde5be9a0168d1c2a0308bfc27167ed62f3968)]: + - effect@2.4.10 + - @effect/platform@0.48.14 + - @effect/printer@0.31.19 + - @effect/printer-ansi@0.32.19 + - @effect/schema@0.64.10 + +## 0.35.15 + +### Patch Changes + +- Updated dependencies [[`dc7e497`](https://github.com/Effect-TS/effect/commit/dc7e49720df416870a7483f48adc40aeb23fe32d), [`ffaf7c3`](https://github.com/Effect-TS/effect/commit/ffaf7c36514f88496cdd2fdfdf0bc7ba5a2e5cd4)]: + - @effect/schema@0.64.9 + - @effect/platform@0.48.13 + +## 0.35.14 + +### Patch Changes + +- Updated dependencies [[`e0af20e`](https://github.com/Effect-TS/effect/commit/e0af20ec5f6d0b19d66c5ebf610969d55bfc6c22)]: + - @effect/schema@0.64.8 + - @effect/platform@0.48.12 + +## 0.35.13 + +### Patch Changes + +- Updated dependencies [[`0f6c7b4`](https://github.com/Effect-TS/effect/commit/0f6c7b426eb3432f60e3a17f8cd92ceac91597bf)]: + - @effect/platform@0.48.11 + +## 0.35.12 + +### Patch Changes + +- Updated dependencies [[`71fd528`](https://github.com/Effect-TS/effect/commit/71fd5287500f9ce155a7d9f0df6ee3e0ac3aeb99)]: + - @effect/platform@0.48.10 + - effect@2.4.9 + - @effect/printer@0.31.18 + - @effect/printer-ansi@0.32.18 + - @effect/schema@0.64.7 + +## 0.35.11 + +### Patch Changes + +- [#2352](https://github.com/Effect-TS/effect/pull/2352) [`63f8372`](https://github.com/Effect-TS/effect/commit/63f83722b137e2ab7fdf8ef947c65ed107221353) Thanks [@tim-smart](https://github.com/tim-smart)! - remove use of `__proto__` + +- Updated dependencies [[`595140a`](https://github.com/Effect-TS/effect/commit/595140a13bda09bf22c669196440868e8a274599), [`5f5fcd9`](https://github.com/Effect-TS/effect/commit/5f5fcd969ae30ed6fe61d566a571498d9e895e16), [`bb0b69e`](https://github.com/Effect-TS/effect/commit/bb0b69e519698c7c76aa68217de423c78ad16566), [`7a45ad0`](https://github.com/Effect-TS/effect/commit/7a45ad0a5f715d64a69b28a8ee3573e5f86909c3), [`5c3b1cc`](https://github.com/Effect-TS/effect/commit/5c3b1ccba182d0f636a973729f9c6bfb12539dc8), [`6f7dfc9`](https://github.com/Effect-TS/effect/commit/6f7dfc9637bd641beb93b14e027dcfcb5d2c8feb), [`88b8583`](https://github.com/Effect-TS/effect/commit/88b85838e03d4f33036f9d16c9c00a487fa99bd8), [`cb20824`](https://github.com/Effect-TS/effect/commit/cb20824416cbf251188395d0aad3622e3a5d7ff2), [`6b20bad`](https://github.com/Effect-TS/effect/commit/6b20badebb3a7ca4d38857753e8ecaa09d02ccfb), [`4e64e9b`](https://github.com/Effect-TS/effect/commit/4e64e9b9876de6bfcbabe39e18a91a08e5f3fbb0), [`3851a02`](https://github.com/Effect-TS/effect/commit/3851a022c481006aec1db36651e4b4fd727aa742), [`5f5fcd9`](https://github.com/Effect-TS/effect/commit/5f5fcd969ae30ed6fe61d566a571498d9e895e16), [`814e5b8`](https://github.com/Effect-TS/effect/commit/814e5b828f68210b9e8f336fd6ac688646835dd9), [`a45a525`](https://github.com/Effect-TS/effect/commit/a45a525e7ccf07704dff1666f1e390282b5bac91)]: + - @effect/schema@0.64.6 + - effect@2.4.8 + - @effect/platform@0.48.9 + - @effect/printer@0.31.17 + - @effect/printer-ansi@0.32.17 + +## 0.35.10 + +### Patch Changes + +- Updated dependencies [[`69d27bb`](https://github.com/Effect-TS/effect/commit/69d27bb633884b6b50f9c3d9e95c29f09b4860b5), [`d0f56c6`](https://github.com/Effect-TS/effect/commit/d0f56c68e604b1cf8dd4e761a3f3cf3631b3cec1)]: + - @effect/platform@0.48.8 + - @effect/schema@0.64.5 + +## 0.35.9 + +### Patch Changes + +- Updated dependencies [[`f908948`](https://github.com/Effect-TS/effect/commit/f908948fd05771a670c0b746e2dd9caa9408ef83)]: + - @effect/platform@0.48.7 + +## 0.35.8 + +### Patch Changes + +- Updated dependencies [[`eb93283`](https://github.com/Effect-TS/effect/commit/eb93283985913d7b04ca750e36ac8513e7b6cef6)]: + - effect@2.4.7 + - @effect/platform@0.48.6 + - @effect/printer@0.31.16 + - @effect/printer-ansi@0.32.16 + - @effect/schema@0.64.4 + +## 0.35.7 + +### Patch Changes + +- Updated dependencies [[`e006e4a`](https://github.com/Effect-TS/effect/commit/e006e4a538c97bae6ca1efa74802159e8a688fcb)]: + - @effect/platform@0.48.5 + +## 0.35.6 + +### Patch Changes + +- Updated dependencies [[`cfef6ec`](https://github.com/Effect-TS/effect/commit/cfef6ecd1fe801cec1a3cbfb7f064fc394b0ad73)]: + - @effect/schema@0.64.3 + - @effect/platform@0.48.4 + +## 0.35.5 + +### Patch Changes + +- Updated dependencies [[`c362e06`](https://github.com/Effect-TS/effect/commit/c362e066550252d5a9fcbc31a4b34d0e17c50699), [`83ddd6f`](https://github.com/Effect-TS/effect/commit/83ddd6f41029724b2cbd144cf309463967ed1164)]: + - @effect/platform@0.48.3 + +## 0.35.4 + +### Patch Changes + +- Updated dependencies [[`89748c9`](https://github.com/Effect-TS/effect/commit/89748c90b36cb5eb880a9ab9323b252338dee848), [`4f35a7e`](https://github.com/Effect-TS/effect/commit/4f35a7e7c4eba598924aff24d1158b9056bb24be), [`9971186`](https://github.com/Effect-TS/effect/commit/99711862722188fbb5ed3ee75126ad5edf13f72f)]: + - @effect/schema@0.64.2 + - @effect/platform@0.48.2 + - effect@2.4.6 + - @effect/printer@0.31.15 + - @effect/printer-ansi@0.32.15 + +## 0.35.3 + +### Patch Changes + +- Updated dependencies [[`d10f876`](https://github.com/Effect-TS/effect/commit/d10f876cd98da275bc5dc5750a91a7fc95e97541), [`743ae6d`](https://github.com/Effect-TS/effect/commit/743ae6d12b249f0b35b31b65b2f7ec91d83ee387), [`a75bc48`](https://github.com/Effect-TS/effect/commit/a75bc48e0e3278d0f70665fedecc5ae7ec447e24), [`bce21c5`](https://github.com/Effect-TS/effect/commit/bce21c5ded2177114666ba229bd5029fa000dee3), [`c7d3036`](https://github.com/Effect-TS/effect/commit/c7d303630b7f0825cb2e584557c5767a67214d9f)]: + - @effect/schema@0.64.1 + - effect@2.4.5 + - @effect/platform@0.48.1 + - @effect/printer@0.31.14 + - @effect/printer-ansi@0.32.14 + +## 0.35.2 + +### Patch Changes + +- Updated dependencies [[`5d47ee0`](https://github.com/Effect-TS/effect/commit/5d47ee0855e492532085b6092879b1b952d84949), [`5d47ee0`](https://github.com/Effect-TS/effect/commit/5d47ee0855e492532085b6092879b1b952d84949), [`1cb7f9c`](https://github.com/Effect-TS/effect/commit/1cb7f9cff7c2272a32fc7a324d87b02e2cd8a2f5), [`5d47ee0`](https://github.com/Effect-TS/effect/commit/5d47ee0855e492532085b6092879b1b952d84949), [`817a04c`](https://github.com/Effect-TS/effect/commit/817a04cb2df0f4140984dc97eb3e1bb14a6c4a38), [`d90a99d`](https://github.com/Effect-TS/effect/commit/d90a99d03d074adc7cd2533f15419138264da5a2), [`dd05faa`](https://github.com/Effect-TS/effect/commit/dd05faa621555ef3585ecd914ac13ecd89b710f4), [`509be1a`](https://github.com/Effect-TS/effect/commit/509be1a0817118489750cf028523134677e44a8a), [`1cb7f9c`](https://github.com/Effect-TS/effect/commit/1cb7f9cff7c2272a32fc7a324d87b02e2cd8a2f5), [`dd05faa`](https://github.com/Effect-TS/effect/commit/dd05faa621555ef3585ecd914ac13ecd89b710f4), [`e7ca973`](https://github.com/Effect-TS/effect/commit/e7ca973c5430ae60716701e58bedd4632ff971fd), [`a1f44cb`](https://github.com/Effect-TS/effect/commit/a1f44cb5112713ff9a3ac3d91a63a2c99d6b7fc1), [`d910dd2`](https://github.com/Effect-TS/effect/commit/d910dd2ca1e8e5aa2f09d9bf3694ede745758f99), [`e7ca973`](https://github.com/Effect-TS/effect/commit/e7ca973c5430ae60716701e58bedd4632ff971fd), [`802674b`](https://github.com/Effect-TS/effect/commit/802674b379b7559ad3ff09b33388891445a9e48b), [`bdff193`](https://github.com/Effect-TS/effect/commit/bdff193365dd9ec2863573b08eb960aa8dee5c93)]: + - @effect/schema@0.64.0 + - effect@2.4.4 + - @effect/platform@0.48.0 + - @effect/printer@0.31.13 + - @effect/printer-ansi@0.32.13 + +## 0.35.1 + +### Patch Changes + +- Updated dependencies [[`0680545`](https://github.com/Effect-TS/effect/commit/068054540f19bb23a79c7c021ed8b2fe34f3e19f), [`20e63fb`](https://github.com/Effect-TS/effect/commit/20e63fb9207210f3fe2d136ec40d0a2dbff3225e), [`20e63fb`](https://github.com/Effect-TS/effect/commit/20e63fb9207210f3fe2d136ec40d0a2dbff3225e)]: + - @effect/platform@0.47.1 + - effect@2.4.3 + - @effect/printer@0.31.12 + - @effect/printer-ansi@0.32.12 + - @effect/schema@0.63.4 + +## 0.35.0 + +### Minor Changes + +- [#2240](https://github.com/Effect-TS/effect/pull/2240) [`0c2da75`](https://github.com/Effect-TS/effect/commit/0c2da75f4964338d1165e7f51e33c2795d16c8de) Thanks [@ccntrq](https://github.com/ccntrq)! - Remove duplicate 'should' in wizard mode prompt for variadic arguments + +- [#2252](https://github.com/Effect-TS/effect/pull/2252) [`89b5d1c`](https://github.com/Effect-TS/effect/commit/89b5d1c4bbd05ef9558272eba80c6f5b0707c562) Thanks [@ccntrq](https://github.com/ccntrq)! - Print unknown argument errors + +### Patch Changes + +- Updated dependencies [[`e03811e`](https://github.com/Effect-TS/effect/commit/e03811e80c93e986e6348b3b67ac2ed6d5fefff0), [`ac41d84`](https://github.com/Effect-TS/effect/commit/ac41d84776484cdce8165b7ca2c9c9b6377eee2d), [`0f3d99c`](https://github.com/Effect-TS/effect/commit/0f3d99c27521ec6b221b644a0fffc79199c3acca), [`6137533`](https://github.com/Effect-TS/effect/commit/613753300c7705518ab1fea2f370b032851c2750), [`f373529`](https://github.com/Effect-TS/effect/commit/f373529999f4b8bc92b634f6ea14f19271388eed), [`1bf9f31`](https://github.com/Effect-TS/effect/commit/1bf9f31f07667de677673f7c29a4e7a26ebad3c8), [`e3ff789`](https://github.com/Effect-TS/effect/commit/e3ff789226f89e71eb28ca38ce79f90af6a03f1a), [`6137533`](https://github.com/Effect-TS/effect/commit/613753300c7705518ab1fea2f370b032851c2750), [`507ba40`](https://github.com/Effect-TS/effect/commit/507ba4060ff043c1a8d541dae723fa6940633b00), [`4064ea0`](https://github.com/Effect-TS/effect/commit/4064ea04e0b3fa23108ee471cd89ab2482b2f6e5), [`e466afe`](https://github.com/Effect-TS/effect/commit/e466afe32f2de598ceafd8982bd0cfbd388e5671), [`465be79`](https://github.com/Effect-TS/effect/commit/465be7926afe98169837d8a4ed5ebc059a732d21), [`f373529`](https://github.com/Effect-TS/effect/commit/f373529999f4b8bc92b634f6ea14f19271388eed), [`de74eb8`](https://github.com/Effect-TS/effect/commit/de74eb80a79eebde5ff645033765e7a617e92f27), [`d8e6940`](https://github.com/Effect-TS/effect/commit/d8e694040f67da6fefc0f5c98fc8e15c0b48822e), [`fa9663c`](https://github.com/Effect-TS/effect/commit/fa9663cb854ca03dba672d7857ecff84f1140c9e), [`fa9663c`](https://github.com/Effect-TS/effect/commit/fa9663cb854ca03dba672d7857ecff84f1140c9e)]: + - effect@2.4.2 + - @effect/platform@0.47.0 + - @effect/schema@0.63.3 + - @effect/printer@0.31.11 + - @effect/printer-ansi@0.32.11 + +## 0.34.3 + +### Patch Changes + +- Updated dependencies [[`a4a0006`](https://github.com/Effect-TS/effect/commit/a4a0006c7f19fc261df5cda16963d73457e4d6ac), [`7535080`](https://github.com/Effect-TS/effect/commit/7535080f2e2f9859711031161600c01807cc43ea), [`39f583e`](https://github.com/Effect-TS/effect/commit/39f583eaeb29eecd6eaec3b113b24d9d413153df), [`f428198`](https://github.com/Effect-TS/effect/commit/f428198725d4b9e304ecd5ff8bad8f92d871dbe3), [`0a37676`](https://github.com/Effect-TS/effect/commit/0a37676aa0eb2a21e17af2e6df9f81f52bbc8831), [`bd1d7ac`](https://github.com/Effect-TS/effect/commit/bd1d7ac75eea57a94d5e2d8e1edccb3136e84899), [`c035972`](https://github.com/Effect-TS/effect/commit/c035972dfabdd3cb3372b5ab468aa2fd0d808f4d), [`6f503b7`](https://github.com/Effect-TS/effect/commit/6f503b774d893bf2af34f66202e270d8c45d5f31)]: + - effect@2.4.1 + - @effect/platform@0.46.3 + - @effect/schema@0.63.2 + - @effect/printer@0.31.10 + - @effect/printer-ansi@0.32.10 + +## 0.34.2 + +### Patch Changes + +- Updated dependencies [[`5d30853`](https://github.com/Effect-TS/effect/commit/5d308534cac6f187227185393c0bac9eb27f90ab), [`6e350ed`](https://github.com/Effect-TS/effect/commit/6e350ed611feb0341e00aafd3c3905cd5ba53f07)]: + - @effect/schema@0.63.1 + - @effect/platform@0.46.2 + +## 0.34.1 + +### Patch Changes + +- Updated dependencies [[`aa6556f`](https://github.com/Effect-TS/effect/commit/aa6556f007117caea84d6965aa30846a11879e9d)]: + - @effect/platform@0.46.1 + +## 0.34.0 + +### Minor Changes + +- [#2101](https://github.com/Effect-TS/effect/pull/2101) [`a025b12`](https://github.com/Effect-TS/effect/commit/a025b121235ba01cfce8d62a775491880c575561) Thanks [@github-actions](https://github.com/apps/github-actions)! - Swap type params of Either from `Either` to `Either`. + + Along the same line of the other changes this allows to shorten the most common types such as: + + ```ts + import { Either } from "effect" + + const right: Either.Either = Either.right("ok") + ``` + +### Patch Changes + +- Updated dependencies [[`5de7be5`](https://github.com/Effect-TS/effect/commit/5de7be5beca2e963b503e6029dcc3217848187d2), [`489fcf3`](https://github.com/Effect-TS/effect/commit/489fcf363ff2b2a953166b740cb9a62d7fc2a101), [`7d9c3bf`](https://github.com/Effect-TS/effect/commit/7d9c3bff6c18d451e0e4781042945ec5c7be1b9f), [`d8d278b`](https://github.com/Effect-TS/effect/commit/d8d278b2efb2966947029885e01f7b68348a021f), [`14c5711`](https://github.com/Effect-TS/effect/commit/14c57110078f0862b8da5c7a2c5d980f54447484), [`5de7be5`](https://github.com/Effect-TS/effect/commit/5de7be5beca2e963b503e6029dcc3217848187d2), [`54ddbb7`](https://github.com/Effect-TS/effect/commit/54ddbb720aeeb657537b01ae221cdcd5e919c1a6), [`b9cb3a9`](https://github.com/Effect-TS/effect/commit/b9cb3a9c9bfdd75536bd70b4e8b557c12d4923ff), [`585fcce`](https://github.com/Effect-TS/effect/commit/585fcce162d0f07a48d7cd984a9b722966fbebbe), [`93b412d`](https://github.com/Effect-TS/effect/commit/93b412d4a9ed762dc9fa5807e51fad0fc78a614a), [`55b26a6`](https://github.com/Effect-TS/effect/commit/55b26a6342b4826f1116e7a1eb660118c274458e), [`136ef40`](https://github.com/Effect-TS/effect/commit/136ef40fe4a394abfa5c6a7ec103eea57251423e), [`a025b12`](https://github.com/Effect-TS/effect/commit/a025b121235ba01cfce8d62a775491880c575561), [`2097739`](https://github.com/Effect-TS/effect/commit/20977393d2383bff709304e81ec7d51cafd57108), [`f24ac9f`](https://github.com/Effect-TS/effect/commit/f24ac9f0c2c520add58f09fbdcec5defda03bd52)]: + - effect@2.4.0 + - @effect/platform@0.46.0 + - @effect/schema@0.63.0 + - @effect/printer@0.31.9 + - @effect/printer-ansi@0.32.9 + +## 0.33.14 + +### Patch Changes + +- [#2187](https://github.com/Effect-TS/effect/pull/2187) [`e6d36c0`](https://github.com/Effect-TS/effect/commit/e6d36c0813d836f17eabb6a9c7849baffca12dbf) Thanks [@tim-smart](https://github.com/tim-smart)! - update development dependencies + +- Updated dependencies [[`5ad2eec`](https://github.com/Effect-TS/effect/commit/5ad2eece0280b6db6a749d25cac1dcf6d33659a9), [`e6d36c0`](https://github.com/Effect-TS/effect/commit/e6d36c0813d836f17eabb6a9c7849baffca12dbf)]: + - effect@2.3.8 + - @effect/schema@0.62.9 + - @effect/platform@0.45.6 + - @effect/printer@0.31.8 + - @effect/printer-ansi@0.32.8 + +## 0.33.13 + +### Patch Changes + +- [#2175](https://github.com/Effect-TS/effect/pull/2175) [`bbb097b`](https://github.com/Effect-TS/effect/commit/bbb097b767013bd5be0d17162e391d614ff4b23e) Thanks [@IMax153](https://github.com/IMax153)! - Ensure wizard mode does not prompt further for 0 input to variadic arg / option + +- [#2175](https://github.com/Effect-TS/effect/pull/2175) [`bbb097b`](https://github.com/Effect-TS/effect/commit/bbb097b767013bd5be0d17162e391d614ff4b23e) Thanks [@IMax153](https://github.com/IMax153)! - Fix the display of CLI wizard mode to show the root command name and not the current executable + +- Updated dependencies [[`6daf084`](https://github.com/Effect-TS/effect/commit/6daf0845de008772011db8d7c75b7c37a6b4d334)]: + - @effect/platform@0.45.5 + +## 0.33.12 + +### Patch Changes + +- [#2173](https://github.com/Effect-TS/effect/pull/2173) [`cbd1a5a`](https://github.com/Effect-TS/effect/commit/cbd1a5a3c47a37f9ee842446d5d66086693c2a74) Thanks [@IMax153](https://github.com/IMax153)! - Fixes the root command name in generated shell completion scripts + +- Updated dependencies [[`bc8404d`](https://github.com/Effect-TS/effect/commit/bc8404d54fd42072d200c0399cb39672837afa9f), [`abcb7d9`](https://github.com/Effect-TS/effect/commit/abcb7d983a4a85b43b7175e952f5b331b9019aea), [`2c5cbcd`](https://github.com/Effect-TS/effect/commit/2c5cbcd1161b4f40dab184999291e817314107de), [`6565916`](https://github.com/Effect-TS/effect/commit/6565916ef254bf910e47d25fd0ef55e7cb420241), [`abcb7d9`](https://github.com/Effect-TS/effect/commit/abcb7d983a4a85b43b7175e952f5b331b9019aea)]: + - effect@2.3.7 + - @effect/platform@0.45.4 + - @effect/printer@0.31.7 + - @effect/printer-ansi@0.32.7 + - @effect/schema@0.62.8 + +## 0.33.11 + +### Patch Changes + +- Updated dependencies [[`09532a8`](https://github.com/Effect-TS/effect/commit/09532a86b7d0cc23557c89158f0342753dfce4b0)]: + - @effect/platform@0.45.3 + +## 0.33.10 + +### Patch Changes + +- Updated dependencies [[`b1163b2`](https://github.com/Effect-TS/effect/commit/b1163b2bd67b65bafbbb39fc4c67576e5cbaf444), [`b46b869`](https://github.com/Effect-TS/effect/commit/b46b869e59a6da5aa235a9fcc25e1e0d24e9e8f8), [`dbff62c`](https://github.com/Effect-TS/effect/commit/dbff62c3026054350a671f6210058ec5844c285e), [`de1b226`](https://github.com/Effect-TS/effect/commit/de1b226282b5ab6c2809dd93f3bdb066f24a1333), [`a663390`](https://github.com/Effect-TS/effect/commit/a66339090ae7b960f8a8b90a0dcdc505de5aaf3e), [`ff88f80`](https://github.com/Effect-TS/effect/commit/ff88f808c4ed9947a148045849e7410b00acad0a), [`11be07b`](https://github.com/Effect-TS/effect/commit/11be07bf65d82cfdf994cdb9d8ca937f995cb4f0), [`c568645`](https://github.com/Effect-TS/effect/commit/c5686451c87d26382135a1c63b00ef171bb24f62), [`88835e5`](https://github.com/Effect-TS/effect/commit/88835e575a0bfbeff9a3696a332f32192c940e12), [`e572b07`](https://github.com/Effect-TS/effect/commit/e572b076e9b4369d9cc8e55414006eef376c93d9), [`e787a57`](https://github.com/Effect-TS/effect/commit/e787a5772e30d8b840cb98b49d36996e7d659a6c), [`b415577`](https://github.com/Effect-TS/effect/commit/b415577f6c576073733929c858e5aac27b6d5880), [`ff8046f`](https://github.com/Effect-TS/effect/commit/ff8046f57dfd073eba60ce6d3144ab060fbf93ce), [`44c3b43`](https://github.com/Effect-TS/effect/commit/44c3b43653e64d7e425d39815d8ff405acec9b99)]: + - effect@2.3.6 + - @effect/schema@0.62.7 + - @effect/platform@0.45.2 + - @effect/printer@0.31.6 + - @effect/printer-ansi@0.32.6 + +## 0.33.9 + +### Patch Changes + +- Updated dependencies [[`65895ab`](https://github.com/Effect-TS/effect/commit/65895ab982e0917ac92f0827e387e7cf61be1e69)]: + - @effect/platform@0.45.1 + +## 0.33.8 + +### Patch Changes + +- Updated dependencies [[`2b62548`](https://github.com/Effect-TS/effect/commit/2b6254845882f399636d24223c483e5489e3cff4)]: + - @effect/platform@0.45.0 + +## 0.33.7 + +### Patch Changes + +- Updated dependencies [[`aef2b8b`](https://github.com/Effect-TS/effect/commit/aef2b8bb636ada07224dc9cf491bebe622c1aeda), [`b881365`](https://github.com/Effect-TS/effect/commit/b8813650355322ea2fc1fbaa4f846bd87a7a05f3), [`7eecb1c`](https://github.com/Effect-TS/effect/commit/7eecb1c6cebe36550df3cca85a46867adbcaa2ca)]: + - @effect/schema@0.62.6 + - effect@2.3.5 + - @effect/platform@0.44.7 + - @effect/printer@0.31.5 + - @effect/printer-ansi@0.32.5 + +## 0.33.6 + +### Patch Changes + +- Updated dependencies [[`17bda66`](https://github.com/Effect-TS/effect/commit/17bda66431c999a546920c10adb205e6c8bea7d1)]: + - effect@2.3.4 + - @effect/platform@0.44.6 + - @effect/printer@0.31.4 + - @effect/printer-ansi@0.32.4 + - @effect/schema@0.62.5 + +## 0.33.5 + +### Patch Changes + +- Updated dependencies [[`1c6d18b`](https://github.com/Effect-TS/effect/commit/1c6d18b422b0bd800f2ed036dba9cb78db296c03), [`13d3266`](https://github.com/Effect-TS/effect/commit/13d3266f331f7aa49b55dd244d4e749a82255274), [`a344b42`](https://github.com/Effect-TS/effect/commit/a344b420862f71532a28c72f00b7ba54776d744d)]: + - @effect/schema@0.62.4 + - @effect/platform@0.44.5 + +## 0.33.4 + +### Patch Changes + +- Updated dependencies [[`efd41d8`](https://github.com/Effect-TS/effect/commit/efd41d8131c3d90867608969ef7c4eef490eb5e6), [`0f83515`](https://github.com/Effect-TS/effect/commit/0f83515a9c01d13c7c15a3f026e02d22c3c6bb7f), [`0f83515`](https://github.com/Effect-TS/effect/commit/0f83515a9c01d13c7c15a3f026e02d22c3c6bb7f)]: + - effect@2.3.3 + - @effect/platform@0.44.4 + - @effect/printer@0.31.3 + - @effect/printer-ansi@0.32.3 + - @effect/schema@0.62.3 + +## 0.33.3 + +### Patch Changes + +- Updated dependencies [[`6654f5f`](https://github.com/Effect-TS/effect/commit/6654f5f0f6b9d97165ede5e04ca16776e2599328), [`2eb11b4`](https://github.com/Effect-TS/effect/commit/2eb11b47752cedf233ef4c4395d9c4efc9b9e180), [`56c09bd`](https://github.com/Effect-TS/effect/commit/56c09bd369279a6a7785209d172739935818cba6), [`71aa5b1`](https://github.com/Effect-TS/effect/commit/71aa5b1c180dcb8b53aefe232d12a97bd06b5447), [`1700af8`](https://github.com/Effect-TS/effect/commit/1700af8af1131602887da721914c8562b6342393)]: + - effect@2.3.2 + - @effect/platform@0.44.3 + - @effect/printer@0.31.2 + - @effect/printer-ansi@0.32.2 + - @effect/schema@0.62.2 + +## 0.33.2 + +### Patch Changes + +- Updated dependencies [[`29739dd`](https://github.com/Effect-TS/effect/commit/29739dde8e6232824d49c4c7f8856de245249c5c)]: + - @effect/platform@0.44.2 + +## 0.33.1 + +### Patch Changes + +- Updated dependencies [[`b5a8215`](https://github.com/Effect-TS/effect/commit/b5a8215ee2a97a8865d69ee55ce1b9835948c922)]: + - effect@2.3.1 + - @effect/platform@0.44.1 + - @effect/printer@0.31.1 + - @effect/printer-ansi@0.32.1 + - @effect/schema@0.62.1 + +## 0.33.0 + +### Minor Changes + +- [#2006](https://github.com/Effect-TS/effect/pull/2006) [`9a2d1c1`](https://github.com/Effect-TS/effect/commit/9a2d1c1468ea0789b34767ad683da074f061ea9c) Thanks [@github-actions](https://github.com/apps/github-actions)! - With this change we now require a string key to be provided for all tags and renames the dear old `Tag` to `GenericTag`, so when previously you could do: + + ```ts + import { Effect, Context } from "effect" + interface Service { + readonly _: unique symbol + } + const Service = Context.Tag< + Service, + { + number: Effect.Effect + } + >() + ``` + + you are now mandated to do: + + ```ts + import { Effect, Context } from "effect" + interface Service { + readonly _: unique symbol + } + const Service = Context.GenericTag< + Service, + { + number: Effect.Effect + } + >("Service") + ``` + + This makes by default all tags globals and ensures better debuggaility when unexpected errors arise. + + Furthermore we introduce a new way of constructing tags that should be considered the new default: + + ```ts + import { Effect, Context } from "effect" + class Service extends Context.Tag("Service")< + Service, + { + number: Effect.Effect + } + >() {} + + const program = Effect.flatMap(Service, ({ number }) => number).pipe( + Effect.flatMap((_) => Effect.log(`number: ${_}`)) + ) + ``` + + this will use "Service" as the key and will create automatically an opaque identifier (the class) to be used at the type level, it does something similar to the above in a single shot. + +- [#2006](https://github.com/Effect-TS/effect/pull/2006) [`1a77f72`](https://github.com/Effect-TS/effect/commit/1a77f72cdaf43d6cdc91b6060f82832edcdbbcb3) Thanks [@github-actions](https://github.com/apps/github-actions)! - change `Effect` type parameters order from `Effect` to `Effect` + +- [#2006](https://github.com/Effect-TS/effect/pull/2006) [`a34dbdc`](https://github.com/Effect-TS/effect/commit/a34dbdc1552c73c1b612676f262a0c735ce444a7) Thanks [@github-actions](https://github.com/apps/github-actions)! - - Schema: change type parameters order from `Schema` to `Schema` + - Serializable: change type parameters order from `Serializable` to `Serializable` + - Class: change type parameters order from `Class` to `Class` + - PropertySignature: change type parameters order from `PropertySignature` to `PropertySignature` + +- [#2006](https://github.com/Effect-TS/effect/pull/2006) [`b1e2086`](https://github.com/Effect-TS/effect/commit/b1e2086ea8bf410e4ad75d71c0760825924e8f9f) Thanks [@github-actions](https://github.com/apps/github-actions)! - remove re-exports from platform packages + +- [#2006](https://github.com/Effect-TS/effect/pull/2006) [`02c3461`](https://github.com/Effect-TS/effect/commit/02c34615d02f91269ea04036d0306fccf4e39e18) Thanks [@github-actions](https://github.com/apps/github-actions)! - With this change we remove the `Data.Data` type and we make `Equal.Equal` & `Hash.Hash` implicit traits. + + The main reason is that `Data.Data` was structurally equivalent to `A & Equal.Equal` but extending `Equal.Equal` doesn't mean that the equality is implemented by-value, so the type was simply adding noise without gaining any level of safety. + + The module `Data` remains unchanged at the value level, all the functions previously available are supposed to work in exactly the same manner. + + At the type level instead the functions return `Readonly` variants, so for example we have: + + ```ts + import { Data } from "effect" + + const obj = Data.struct({ + a: 0, + b: 1 + }) + ``` + + will have the `obj` typed as: + + ```ts + declare const obj: { + readonly a: number + readonly b: number + } + ``` + +- [#2006](https://github.com/Effect-TS/effect/pull/2006) [`9a2d1c1`](https://github.com/Effect-TS/effect/commit/9a2d1c1468ea0789b34767ad683da074f061ea9c) Thanks [@github-actions](https://github.com/apps/github-actions)! - This change enables `Effect.serviceConstants` and `Effect.serviceMembers` to access any constant in the service, not only the effects, namely it is now possible to do: + + ```ts + import { Effect, Context } from "effect" + + class NumberRepo extends Context.TagClass("NumberRepo")< + NumberRepo, + { + readonly numbers: Array + } + >() { + static numbers = Effect.serviceConstants(NumberRepo).numbers + } + ``` + +### Patch Changes + +- Updated dependencies [[`96bcee2`](https://github.com/Effect-TS/effect/commit/96bcee21021aecd8ffd86440a2c9be353c4668e3), [`b1e2086`](https://github.com/Effect-TS/effect/commit/b1e2086ea8bf410e4ad75d71c0760825924e8f9f), [`4cd6e14`](https://github.com/Effect-TS/effect/commit/4cd6e144945b6c398f5f5abe3471ff7fb3372bfd), [`96bcee2`](https://github.com/Effect-TS/effect/commit/96bcee21021aecd8ffd86440a2c9be353c4668e3), [`c77f635`](https://github.com/Effect-TS/effect/commit/c77f635f8a26ca6d83cb569d911f8eee79033fd9), [`e343a74`](https://github.com/Effect-TS/effect/commit/e343a74843dd9edf879417fa94cb51de7ed5b402), [`acf1894`](https://github.com/Effect-TS/effect/commit/acf1894f45945dbe5c39451e36aabb4b5092f257), [`9dc04c8`](https://github.com/Effect-TS/effect/commit/9dc04c88a2ea9c68122cb2632a76f0f4be40329a), [`9a2d1c1`](https://github.com/Effect-TS/effect/commit/9a2d1c1468ea0789b34767ad683da074f061ea9c), [`1a77f72`](https://github.com/Effect-TS/effect/commit/1a77f72cdaf43d6cdc91b6060f82832edcdbbcb3), [`af47aa3`](https://github.com/Effect-TS/effect/commit/af47aa37196ad542c9c23a4896d8ef98147e1205), [`d3f9f4d`](https://github.com/Effect-TS/effect/commit/d3f9f4d4032b1131c62f4ddb21a4583e4e8d7c18), [`c986f0e`](https://github.com/Effect-TS/effect/commit/c986f0e0ce4d22ba08177ed351152718479ab63c), [`96bcee2`](https://github.com/Effect-TS/effect/commit/96bcee21021aecd8ffd86440a2c9be353c4668e3), [`d3f9f4d`](https://github.com/Effect-TS/effect/commit/d3f9f4d4032b1131c62f4ddb21a4583e4e8d7c18), [`d3f9f4d`](https://github.com/Effect-TS/effect/commit/d3f9f4d4032b1131c62f4ddb21a4583e4e8d7c18), [`d3f9f4d`](https://github.com/Effect-TS/effect/commit/d3f9f4d4032b1131c62f4ddb21a4583e4e8d7c18), [`70dde23`](https://github.com/Effect-TS/effect/commit/70dde238f81125e353fd7bde5fc24ecd8969bf97), [`a34dbdc`](https://github.com/Effect-TS/effect/commit/a34dbdc1552c73c1b612676f262a0c735ce444a7), [`d3f9f4d`](https://github.com/Effect-TS/effect/commit/d3f9f4d4032b1131c62f4ddb21a4583e4e8d7c18), [`81b7425`](https://github.com/Effect-TS/effect/commit/81b7425320cbbe2a6cf547a3e3ab3549cdba14cf), [`b1e2086`](https://github.com/Effect-TS/effect/commit/b1e2086ea8bf410e4ad75d71c0760825924e8f9f), [`02c3461`](https://github.com/Effect-TS/effect/commit/02c34615d02f91269ea04036d0306fccf4e39e18), [`0e56e99`](https://github.com/Effect-TS/effect/commit/0e56e998ab9815c4d096c239a553cb86a0f99af9), [`8b0ded9`](https://github.com/Effect-TS/effect/commit/8b0ded9f10ba0d96fcb9af24eff2dbd9341f85e3), [`8dd83e8`](https://github.com/Effect-TS/effect/commit/8dd83e854bfcaa6dab876994c5f813dcfb486c28), [`5127afe`](https://github.com/Effect-TS/effect/commit/5127afec1c519e0a3d7460844a9101a96272f29e), [`d75f6fe`](https://github.com/Effect-TS/effect/commit/d75f6fe6499deb0a5ee9ec94af3b5fd4eb03a2d0), [`7356e5c`](https://github.com/Effect-TS/effect/commit/7356e5cc16e9d70f18c02dee1dcb4ad539fd130a), [`3077cde`](https://github.com/Effect-TS/effect/commit/3077cde08a60246821a940964a84dd7f7c8b9f54), [`be19ce0`](https://github.com/Effect-TS/effect/commit/be19ce0b8bdf1fac80bb8d7e0b06a86986b47409), [`4a5d01a`](https://github.com/Effect-TS/effect/commit/4a5d01a409e9b6dd53893e65f8e5c9247f568021), [`78f47ab`](https://github.com/Effect-TS/effect/commit/78f47abfe3cb0a8bbde818b1c5fc603270538b47), [`6361ee2`](https://github.com/Effect-TS/effect/commit/6361ee2e83bdfead24045c3d058a7298efc18113), [`52e5d20`](https://github.com/Effect-TS/effect/commit/52e5d2077582bf51f25861c7139fc920c2c24166), [`c6137ec`](https://github.com/Effect-TS/effect/commit/c6137ec62c6b5542d5062ae1a3c936cb915dee22), [`86f665d`](https://github.com/Effect-TS/effect/commit/86f665d7bd25ba0a3f046a2384798378310dcf0c), [`f5ae081`](https://github.com/Effect-TS/effect/commit/f5ae08195e68e76faeac258c565d79da4e01e7d6), [`4a5d01a`](https://github.com/Effect-TS/effect/commit/4a5d01a409e9b6dd53893e65f8e5c9247f568021), [`60686f5`](https://github.com/Effect-TS/effect/commit/60686f5c38bef1b93a3a0dda9b6596d46aceab03), [`9a2d1c1`](https://github.com/Effect-TS/effect/commit/9a2d1c1468ea0789b34767ad683da074f061ea9c), [`5127afe`](https://github.com/Effect-TS/effect/commit/5127afec1c519e0a3d7460844a9101a96272f29e), [`56b8691`](https://github.com/Effect-TS/effect/commit/56b86916bf3da18002f3655d859dbc487eb5a6de), [`8ee2931`](https://github.com/Effect-TS/effect/commit/8ee293159b4f7cb7af8558287a0a047f3a69743d), [`6727474`](https://github.com/Effect-TS/effect/commit/672747497490a30d36dd49c06db19aabf09dc7f0), [`5127afe`](https://github.com/Effect-TS/effect/commit/5127afec1c519e0a3d7460844a9101a96272f29e)]: + - effect@2.3.0 + - @effect/platform@0.44.0 + - @effect/schema@0.62.0 + - @effect/printer-ansi@0.32.0 + - @effect/printer@0.31.0 + +## 0.32.2 + +### Patch Changes + +- Updated dependencies [[`3ddfdbf`](https://github.com/Effect-TS/effect/commit/3ddfdbf914edea536aef207cec6695f33496258c), [`3ddfdbf`](https://github.com/Effect-TS/effect/commit/3ddfdbf914edea536aef207cec6695f33496258c)]: + - effect@2.2.5 + - @effect/platform@0.43.11 + - @effect/printer@0.30.14 + - @effect/printer-ansi@0.31.14 + - @effect/schema@0.61.7 + +## 0.32.1 + +### Patch Changes + +- Updated dependencies [[`d0b911c`](https://github.com/Effect-TS/effect/commit/d0b911c75f284c7aa87f25aa96926e6bde7690d0), [`330e1a4`](https://github.com/Effect-TS/effect/commit/330e1a4e2c1fc0af6c80c80c81dd38c3e50fab78), [`6928a2b`](https://github.com/Effect-TS/effect/commit/6928a2b0bae86a4bdfbece0aa32924207c2d5a70), [`296bc1c`](https://github.com/Effect-TS/effect/commit/296bc1c9d24986d299d2669115d584cb27b73c60)]: + - effect@2.2.4 + - @effect/platform@0.43.10 + - @effect/schema@0.61.6 + - @effect/printer@0.30.13 + - @effect/printer-ansi@0.31.13 + +## 0.32.0 + +### Minor Changes + +- [#2047](https://github.com/Effect-TS/effect/pull/2047) [`eb1f787`](https://github.com/Effect-TS/effect/commit/eb1f7878c9e5f52f17fa4ed8a13151ab70df6b12) Thanks [@tim-smart](https://github.com/tim-smart)! - make array types in cli more permissive + + This change removes NonEmpty\* arrays as input parameters, and removes use of ReadonlyArray as a return type (prefering Array instead). + + This allows more interop with the existing js ecosystem. + +## 0.31.9 + +### Patch Changes + +- Updated dependencies [[`1b841a9`](https://github.com/Effect-TS/effect/commit/1b841a91fed86825cd2867cf1e68e41d8ff26b4e)]: + - @effect/platform@0.43.9 + +## 0.31.8 + +### Patch Changes + +- Updated dependencies [[`32bf796`](https://github.com/Effect-TS/effect/commit/32bf796c3e5db1b2b68e8b1b20db664295991643)]: + - @effect/platform@0.43.8 + +## 0.31.7 + +### Patch Changes + +- Updated dependencies [[`cde08f3`](https://github.com/Effect-TS/effect/commit/cde08f354ed2ff2921d1d98bd539c7d65a2ddd73)]: + - @effect/platform@0.43.7 + +## 0.31.6 + +### Patch Changes + +- Updated dependencies [[`c96bb17`](https://github.com/Effect-TS/effect/commit/c96bb17043e2cec1eaeb319614a4c2904d876beb)]: + - @effect/platform@0.43.6 + +## 0.31.5 + +### Patch Changes + +- Updated dependencies [[`f1ff44b`](https://github.com/Effect-TS/effect/commit/f1ff44b58cdb1886b38681e8fedc309eb9ac6853), [`13785cf`](https://github.com/Effect-TS/effect/commit/13785cf4a5082d8d9cf8d7c991141dee0d2b4d31)]: + - @effect/schema@0.61.5 + - @effect/platform@0.43.5 + +## 0.31.4 + +### Patch Changes + +- [#2001](https://github.com/Effect-TS/effect/pull/2001) [`aab2e4e`](https://github.com/Effect-TS/effect/commit/aab2e4e156207e0977c0529a7afdcae2992a08ff) Thanks [@IMax153](https://github.com/IMax153)! - ensure single invalid variadic option is reported as an error + +- Updated dependencies [[`22794e0`](https://github.com/Effect-TS/effect/commit/22794e0ba00e40281f30a22fa84412003c24877d), [`f73e6c0`](https://github.com/Effect-TS/effect/commit/f73e6c033fb0729a9cfa5eb4bc39f79d3126e247), [`6bf02c7`](https://github.com/Effect-TS/effect/commit/6bf02c70fe10a04d1b34d6666f95416e42a6225a), [`78f5921`](https://github.com/Effect-TS/effect/commit/78f59211502ded6fcbe15a49d6fde941cccc9d52)]: + - effect@2.2.3 + - @effect/schema@0.61.4 + - @effect/platform@0.43.4 + - @effect/printer@0.30.12 + - @effect/printer-ansi@0.31.12 + +## 0.31.3 + +### Patch Changes + +- [#1990](https://github.com/Effect-TS/effect/pull/1990) [`003bb69`](https://github.com/Effect-TS/effect/commit/003bb691f2059ef596121c78b556196f22ab2a1e) Thanks [@IMax153](https://github.com/IMax153)! - fix stack overflow exception when nesting cli options / args in a command config + +## 0.31.2 + +### Patch Changes + +- Updated dependencies [[`9863e2f`](https://github.com/Effect-TS/effect/commit/9863e2fb3561dc019965aeccd6584a418fc8b401)]: + - @effect/schema@0.61.3 + - @effect/platform@0.43.3 + +## 0.31.1 + +### Patch Changes + +- Updated dependencies [[`64f710a`](https://github.com/Effect-TS/effect/commit/64f710aa49dec6ffcd33ee23438d0774f5489733)]: + - @effect/schema@0.61.2 + - @effect/platform@0.43.2 + +## 0.31.0 + +### Minor Changes + +- [#1984](https://github.com/Effect-TS/effect/pull/1984) [`eaab2e8`](https://github.com/Effect-TS/effect/commit/eaab2e81be72df9ded2e01e4c6d40b2bb159a349) Thanks [@IMax153](https://github.com/IMax153)! - default Options.repeated to return an empty array if option is not provided + +### Patch Changes + +- [#1980](https://github.com/Effect-TS/effect/pull/1980) [`9cf3782`](https://github.com/Effect-TS/effect/commit/9cf3782a17f38097f7b1a0024bd7ec7db8aeb2d0) Thanks [@IMax153](https://github.com/IMax153)! - fix CLI argument parsing to properly handle the case when a repeated option is not provided + +## 0.30.6 + +### Patch Changes + +- Updated dependencies [[`c7550f9`](https://github.com/Effect-TS/effect/commit/c7550f96e1006eee832ce5025bf0c197a65935ea), [`8d1f6e4`](https://github.com/Effect-TS/effect/commit/8d1f6e4bb13e221804fb1762ef19e02bcefc8f61), [`d404561`](https://github.com/Effect-TS/effect/commit/d404561e47ec2fa5f68709a308ee5d2ee959141d), [`7b84a3c`](https://github.com/Effect-TS/effect/commit/7b84a3c7e4b9c8dc02294b0e3cc3ae3becea977b), [`1a84dee`](https://github.com/Effect-TS/effect/commit/1a84dee0e9ddbfaf2610e4d7c00c7020c427171a), [`ac30bf4`](https://github.com/Effect-TS/effect/commit/ac30bf4cd53de0663784f65ae6bee8279333df97)]: + - @effect/schema@0.61.1 + - effect@2.2.2 + - @effect/platform@0.43.1 + - @effect/printer@0.30.11 + - @effect/printer-ansi@0.31.11 + +## 0.30.5 + +### Patch Changes + +- [#1963](https://github.com/Effect-TS/effect/pull/1963) [`de4cb04`](https://github.com/Effect-TS/effect/commit/de4cb049a39923d673fa4acd3db62dd60d341887) Thanks [@IMax153](https://github.com/IMax153)! - fix the parsed letter case for variadic and key/value flags + +- [#1922](https://github.com/Effect-TS/effect/pull/1922) [`62b40e8`](https://github.com/Effect-TS/effect/commit/62b40e8479371d6663c0255aaca56a1ae0d59764) Thanks [@gcanti](https://github.com/gcanti)! - add context tracking to Schema, closes #1873 + +- Updated dependencies [[`84da31f`](https://github.com/Effect-TS/effect/commit/84da31f0643e8651b9d311b30526b1e4edfbdfb8), [`62b40e8`](https://github.com/Effect-TS/effect/commit/62b40e8479371d6663c0255aaca56a1ae0d59764), [`645bea2`](https://github.com/Effect-TS/effect/commit/645bea2551129f94a5b0e38347e28067dee531bb), [`62b40e8`](https://github.com/Effect-TS/effect/commit/62b40e8479371d6663c0255aaca56a1ae0d59764)]: + - effect@2.2.1 + - @effect/schema@0.61.0 + - @effect/platform@0.43.0 + - @effect/printer@0.30.10 + - @effect/printer-ansi@0.31.10 + +## 0.30.4 + +### Patch Changes + +- Updated dependencies [[`202befc`](https://github.com/Effect-TS/effect/commit/202befc2ecbeb117c4fa85ef9b12a3d3a48273d2), [`fe05ad7`](https://github.com/Effect-TS/effect/commit/fe05ad7bcb3b88d47800ab69ebf53641023676f1), [`ee4ff8a`](https://github.com/Effect-TS/effect/commit/ee4ff8a943141fcf2877af92c5877ee87a989fb9), [`ee4ff8a`](https://github.com/Effect-TS/effect/commit/ee4ff8a943141fcf2877af92c5877ee87a989fb9), [`ee4ff8a`](https://github.com/Effect-TS/effect/commit/ee4ff8a943141fcf2877af92c5877ee87a989fb9), [`ee4ff8a`](https://github.com/Effect-TS/effect/commit/ee4ff8a943141fcf2877af92c5877ee87a989fb9), [`10df798`](https://github.com/Effect-TS/effect/commit/10df798639e556f9d88265ef7fc3cf8a3bbe3874)]: + - effect@2.2.0 + - @effect/platform@0.42.7 + - @effect/printer@0.30.9 + - @effect/printer-ansi@0.31.9 + - @effect/schema@0.60.7 + +## 0.30.3 + +### Patch Changes + +- Updated dependencies [[`21b9edd`](https://github.com/Effect-TS/effect/commit/21b9edde464f7c5624ef54ad1b5e264204a37625)]: + - effect@2.1.2 + - @effect/platform@0.42.6 + - @effect/printer@0.30.8 + - @effect/printer-ansi@0.31.8 + - @effect/schema@0.60.6 + +## 0.30.2 + +### Patch Changes + +- Updated dependencies [[`3bf67cf`](https://github.com/Effect-TS/effect/commit/3bf67cf64ff27ffaa811b07751875cb161ac3385)]: + - @effect/schema@0.60.5 + - @effect/platform@0.42.5 + +## 0.30.1 + +### Patch Changes + +- [#1942](https://github.com/Effect-TS/effect/pull/1942) [`d21e028`](https://github.com/Effect-TS/effect/commit/d21e028fe2628b42e681eee641547b0bc01a70d1) Thanks [@tim-smart](https://github.com/tim-smart)! - fix Options.mapEffect export + +## 0.30.0 + +### Minor Changes + +- [#1938](https://github.com/Effect-TS/effect/pull/1938) [`9a0d61f`](https://github.com/Effect-TS/effect/commit/9a0d61f674b70ff17c8bcffbd27fea9d5ec57857) Thanks [@IMax153](https://github.com/IMax153)! - rename mapOrFail to mapEffect for Command, Options, and Args modules + +## 0.29.0 + +### Minor Changes + +- [#1925](https://github.com/Effect-TS/effect/pull/1925) [`86180cc`](https://github.com/Effect-TS/effect/commit/86180cc96102627a42397d2e4f84fb3a55c3038e) Thanks [@IMax153](https://github.com/IMax153)! - adds optional `executable` parameter to `CliApp.make` + + **NOTE**: This means that users are no longer required to manually remove the executable from the CLI arguments (i.e. `process.argv.slice(2)`). The executable is stripped from the CLI arguments internally within `CliApp.make`, so all command-line arguments can be provided directly to the CLI application. + +### Patch Changes + +- Updated dependencies [[`0d1af1e`](https://github.com/Effect-TS/effect/commit/0d1af1e38c11b94e152beaccd0ff7569a1b3f5b7), [`0d1af1e`](https://github.com/Effect-TS/effect/commit/0d1af1e38c11b94e152beaccd0ff7569a1b3f5b7), [`a222524`](https://github.com/Effect-TS/effect/commit/a2225247e9de2e013d287320790fde88c081dbbd)]: + - @effect/schema@0.60.4 + - effect@2.1.1 + - @effect/platform@0.42.4 + - @effect/printer@0.30.7 + - @effect/printer-ansi@0.31.7 + +## 0.28.9 + +### Patch Changes + +- Updated dependencies [[`d543221`](https://github.com/Effect-TS/effect/commit/d5432213e91ab620aa66e0fd92a6593134d18940), [`2530d47`](https://github.com/Effect-TS/effect/commit/2530d470b0ad5df7e636921eedfb1cbe42821f94), [`f493929`](https://github.com/Effect-TS/effect/commit/f493929ab88d2ea137ca5fbff70bdc6c9d804d80), [`5911fa9`](https://github.com/Effect-TS/effect/commit/5911fa9c9440dd3bc1ee38542bcd15f8c75a4637)]: + - @effect/schema@0.60.3 + - @effect/platform@0.42.3 + +## 0.28.8 + +### Patch Changes + +- Updated dependencies [[`05c44b3`](https://github.com/Effect-TS/effect/commit/05c44b30662554dde50b70bad79f13ae895fda02), [`05c44b3`](https://github.com/Effect-TS/effect/commit/05c44b30662554dde50b70bad79f13ae895fda02), [`05c44b3`](https://github.com/Effect-TS/effect/commit/05c44b30662554dde50b70bad79f13ae895fda02), [`05c44b3`](https://github.com/Effect-TS/effect/commit/05c44b30662554dde50b70bad79f13ae895fda02), [`05c44b3`](https://github.com/Effect-TS/effect/commit/05c44b30662554dde50b70bad79f13ae895fda02), [`05c44b3`](https://github.com/Effect-TS/effect/commit/05c44b30662554dde50b70bad79f13ae895fda02)]: + - effect@2.1.0 + - @effect/platform@0.42.2 + - @effect/printer@0.30.6 + - @effect/printer-ansi@0.31.6 + - @effect/schema@0.60.2 + +## 0.28.7 + +### Patch Changes + +- Updated dependencies [[`f7f19f6`](https://github.com/Effect-TS/effect/commit/f7f19f66a5fa349baa2412c1f9f15111c437df09)]: + - effect@2.0.5 + - @effect/platform@0.42.1 + - @effect/printer@0.30.5 + - @effect/printer-ansi@0.31.5 + - @effect/schema@0.60.1 + +## 0.28.6 + +### Patch Changes + +- [#1907](https://github.com/Effect-TS/effect/pull/1907) [`d1c7cf5`](https://github.com/Effect-TS/effect/commit/d1c7cf54fd9c269cca57652391158b6f5ab19628) Thanks [@tim-smart](https://github.com/tim-smart)! - add ConfigFile module to cli + +- [#1899](https://github.com/Effect-TS/effect/pull/1899) [`4863253`](https://github.com/Effect-TS/effect/commit/4863253bfc07d43aec357d214d18879743549ac5) Thanks [@tim-smart](https://github.com/tim-smart)! - add file parsing apis to cli + +- [#1898](https://github.com/Effect-TS/effect/pull/1898) [`4ef1e6f`](https://github.com/Effect-TS/effect/commit/4ef1e6f4e0376532957208d3f4c82a8ed277ffd6) Thanks [@tim-smart](https://github.com/tim-smart)! - add Schema apis to cli Options & Args + +- Updated dependencies [[`ec2bdfa`](https://github.com/Effect-TS/effect/commit/ec2bdfae2da717f28147b9d6820d3494cb240945), [`687e02e`](https://github.com/Effect-TS/effect/commit/687e02e7d84dc06957844160761fda90929470ab), [`536c1df`](https://github.com/Effect-TS/effect/commit/536c1dfb7833961dfb2fbd6bcd2dbdfa2f208d51), [`8eec87e`](https://github.com/Effect-TS/effect/commit/8eec87e311ce55281a98517e6df0ef103b43e8a8), [`540b294`](https://github.com/Effect-TS/effect/commit/540b2941dd0a81e9688311583ce7e2e140d6e7a5), [`8eec87e`](https://github.com/Effect-TS/effect/commit/8eec87e311ce55281a98517e6df0ef103b43e8a8), [`536c1df`](https://github.com/Effect-TS/effect/commit/536c1dfb7833961dfb2fbd6bcd2dbdfa2f208d51), [`a3f96d6`](https://github.com/Effect-TS/effect/commit/a3f96d615b8b3e238dbfa01ef713c87e6f4532be), [`0c397e7`](https://github.com/Effect-TS/effect/commit/0c397e762008a0de40c7526c9d99ff2cfe4f7a6a), [`8eec87e`](https://github.com/Effect-TS/effect/commit/8eec87e311ce55281a98517e6df0ef103b43e8a8), [`b557a10`](https://github.com/Effect-TS/effect/commit/b557a10b773e321bea77fc4951f0ef171dd193c9), [`536c1df`](https://github.com/Effect-TS/effect/commit/536c1dfb7833961dfb2fbd6bcd2dbdfa2f208d51), [`8eec87e`](https://github.com/Effect-TS/effect/commit/8eec87e311ce55281a98517e6df0ef103b43e8a8), [`8eec87e`](https://github.com/Effect-TS/effect/commit/8eec87e311ce55281a98517e6df0ef103b43e8a8), [`74b9094`](https://github.com/Effect-TS/effect/commit/74b90940e571c73a6b76cafa88ffb8a1c949cb4c), [`337e80f`](https://github.com/Effect-TS/effect/commit/337e80f69bc36966f889c439b819db2f84cae496), [`25adce7`](https://github.com/Effect-TS/effect/commit/25adce7ae76ce834096dca1ed70a60ad1a349217), [`536c1df`](https://github.com/Effect-TS/effect/commit/536c1dfb7833961dfb2fbd6bcd2dbdfa2f208d51), [`48a3d40`](https://github.com/Effect-TS/effect/commit/48a3d40aed0f923f567b8911dade732ff472d981)]: + - @effect/schema@0.60.0 + - effect@2.0.4 + - @effect/platform@0.42.0 + - @effect/printer@0.30.4 + - @effect/printer-ansi@0.31.4 + +## 0.28.5 + +### Patch Changes + +- Updated dependencies [[`5b46e99`](https://github.com/Effect-TS/effect/commit/5b46e996d30e2497eb23095e2c21eee04438edf5), [`87f7ef2`](https://github.com/Effect-TS/effect/commit/87f7ef28a3c27e2e4f2fcfa465f85bb2a45a3d6b), [`1d3a06b`](https://github.com/Effect-TS/effect/commit/1d3a06bb58ad1ac123ae8f9d42b4345f9c9c53c0), [`210d27e`](https://github.com/Effect-TS/effect/commit/210d27e999e066ea9b907301150c65f9ff080b39), [`1d3a06b`](https://github.com/Effect-TS/effect/commit/1d3a06bb58ad1ac123ae8f9d42b4345f9c9c53c0)]: + - @effect/schema@0.59.1 + - effect@2.0.3 + - @effect/platform@0.41.0 + - @effect/printer@0.30.3 + - @effect/printer-ansi@0.31.3 + +## 0.28.4 + +### Patch Changes + +- Updated dependencies [[`c4b84f7`](https://github.com/Effect-TS/effect/commit/c4b84f724ae809f3450d71c3ea5d629205fc479f), [`c4b84f7`](https://github.com/Effect-TS/effect/commit/c4b84f724ae809f3450d71c3ea5d629205fc479f), [`c4b84f7`](https://github.com/Effect-TS/effect/commit/c4b84f724ae809f3450d71c3ea5d629205fc479f), [`c4b84f7`](https://github.com/Effect-TS/effect/commit/c4b84f724ae809f3450d71c3ea5d629205fc479f)]: + - @effect/schema@0.59.0 + - @effect/platform@0.40.4 + +## 0.28.3 + +### Patch Changes + +- Updated dependencies [[`7b2f874`](https://github.com/Effect-TS/effect/commit/7b2f8743d96753c3e24ac4cc6715a4a7f4a2ca0c), [`a904a73`](https://github.com/Effect-TS/effect/commit/a904a739459bfd0fa7844b00b902d2fa984fb014), [`7b2f874`](https://github.com/Effect-TS/effect/commit/7b2f8743d96753c3e24ac4cc6715a4a7f4a2ca0c), [`7b2f874`](https://github.com/Effect-TS/effect/commit/7b2f8743d96753c3e24ac4cc6715a4a7f4a2ca0c), [`92c0322`](https://github.com/Effect-TS/effect/commit/92c0322a58bf7e5b8dbb602186030839e89df5af), [`7b2f874`](https://github.com/Effect-TS/effect/commit/7b2f8743d96753c3e24ac4cc6715a4a7f4a2ca0c), [`7b2f874`](https://github.com/Effect-TS/effect/commit/7b2f8743d96753c3e24ac4cc6715a4a7f4a2ca0c)]: + - @effect/schema@0.58.0 + - @effect/platform@0.40.3 + +## 0.28.2 + +### Patch Changes + +- Updated dependencies [[`4c90c54`](https://github.com/Effect-TS/effect/commit/4c90c54d87c91f75f3ad114926cdf3b0c25df091), [`d5a1949`](https://github.com/Effect-TS/effect/commit/d5a19499aac7c1d147674a35ac69992177c7536c), [`d3d3bda`](https://github.com/Effect-TS/effect/commit/d3d3bda74c794153def9027e0c40896e72cd5d14)]: + - @effect/platform@0.40.2 + - effect@2.0.2 + - @effect/printer@0.30.2 + - @effect/printer-ansi@0.31.2 + - @effect/schema@0.57.2 + +## 0.28.1 + +### Patch Changes + +- Updated dependencies [[`16bd87d`](https://github.com/Effect-TS/effect/commit/16bd87d32611b966dc42ea4fc979764f97a49071)]: + - effect@2.0.1 + - @effect/platform@0.40.1 + - @effect/printer@0.30.1 + - @effect/printer-ansi@0.31.1 + - @effect/schema@0.57.1 + +## 0.28.0 + +### Minor Changes + +- [`d0471ca`](https://github.com/Effect-TS/effect/commit/d0471ca7b544746674b9e1750202da72b0a21233) Thanks [@mikearnaldi](https://github.com/mikearnaldi)! - Switch to monorepo structure + +- [#1846](https://github.com/Effect-TS/effect/pull/1846) [`693b8f3`](https://github.com/Effect-TS/effect/commit/693b8f3a3dfd43ae61f0d9292cdf356be7329f2f) Thanks [@fubhy](https://github.com/fubhy)! - Enabled `exactOptionalPropertyTypes` throughout + +### Patch Changes + +- [`d987daa`](https://github.com/Effect-TS/effect/commit/d987daafaddd43b6ade74916a08236c19ea0a9fa) Thanks [@mikearnaldi](https://github.com/mikearnaldi)! - Switch effect dependency to caret + +- [#1848](https://github.com/Effect-TS/effect/pull/1848) [`04fb8b4`](https://github.com/Effect-TS/effect/commit/04fb8b428b19bba85a2c79910c5e363340d074e7) Thanks [@fubhy](https://github.com/fubhy)! - Avoid default parameter initilization + +- [#1853](https://github.com/Effect-TS/effect/pull/1853) [`78fec17`](https://github.com/Effect-TS/effect/commit/78fec17bf1210e3ce35b4e96f3a23cbef2f65c79) Thanks [@tim-smart](https://github.com/tim-smart)! - fix Args.optional returning Option + +- Updated dependencies [[`d987daa`](https://github.com/Effect-TS/effect/commit/d987daafaddd43b6ade74916a08236c19ea0a9fa), [`7b5eaa3`](https://github.com/Effect-TS/effect/commit/7b5eaa3838c79bf4bdccf91b94d61bbc38a2ec95), [`0724211`](https://github.com/Effect-TS/effect/commit/072421149c36010748ff6b6ee19c15c6cffefe09), [`9f2bc5a`](https://github.com/Effect-TS/effect/commit/9f2bc5a19e0b678a0a85e84daac290922b0fd57d), [`04fb8b4`](https://github.com/Effect-TS/effect/commit/04fb8b428b19bba85a2c79910c5e363340d074e7), [`d0471ca`](https://github.com/Effect-TS/effect/commit/d0471ca7b544746674b9e1750202da72b0a21233), [`bcf0900`](https://github.com/Effect-TS/effect/commit/bcf0900b58f449262556f80bff21e771a37272aa), [`6299b84`](https://github.com/Effect-TS/effect/commit/6299b84c11e5d1fe79fa538df8935018c7613747), [`c0aeb5e`](https://github.com/Effect-TS/effect/commit/c0aeb5e302869bcd7d7627f8cc5b630d07c12d10), [`693b8f3`](https://github.com/Effect-TS/effect/commit/693b8f3a3dfd43ae61f0d9292cdf356be7329f2f)]: + - @effect/printer-ansi@0.31.0 + - @effect/platform@0.40.0 + - @effect/printer@0.30.0 + - @effect/schema@0.57.0 + - effect@2.0.0 + +## 0.27.0 + +### Minor Changes + +- [#432](https://github.com/Effect-TS/cli/pull/432) [`66fe7a0`](https://github.com/Effect-TS/cli/commit/66fe7a078ce3fa9d9fa412599fb6a9d416d7fd03) Thanks [@tim-smart](https://github.com/tim-smart)! - update effect + +## 0.26.0 + +### Minor Changes + +- [#430](https://github.com/Effect-TS/cli/pull/430) [`859b1e7`](https://github.com/Effect-TS/cli/commit/859b1e7cdb8b454ef3d6514889a0e1dc9b24966f) Thanks [@tim-smart](https://github.com/tim-smart)! - update effect + +## 0.25.4 + +### Patch Changes + +- [#428](https://github.com/Effect-TS/cli/pull/428) [`ff2d006`](https://github.com/Effect-TS/cli/commit/ff2d006495fa317a53763302846ebce0cd9a620b) Thanks [@tim-smart](https://github.com/tim-smart)! - fix for multiple handler transforms + +## 0.25.3 + +### Patch Changes + +- [#426](https://github.com/Effect-TS/cli/pull/426) [`5ac4637`](https://github.com/Effect-TS/cli/commit/5ac4637c37b779363115afb94b477e5f7558cf6b) Thanks [@tim-smart](https://github.com/tim-smart)! - add Command.provideSync + +- [#426](https://github.com/Effect-TS/cli/pull/426) [`5ac4637`](https://github.com/Effect-TS/cli/commit/5ac4637c37b779363115afb94b477e5f7558cf6b) Thanks [@tim-smart](https://github.com/tim-smart)! - add Command.provideEffect + +## 0.25.2 + +### Patch Changes + +- [#424](https://github.com/Effect-TS/cli/pull/424) [`960cc02`](https://github.com/Effect-TS/cli/commit/960cc02998c177462b22c566d714b8114a5a1cff) Thanks [@tim-smart](https://github.com/tim-smart)! - update /platform + +## 0.25.1 + +### Patch Changes + +- [#422](https://github.com/Effect-TS/cli/pull/422) [`ca7dcd5`](https://github.com/Effect-TS/cli/commit/ca7dcd5fe5cc23527639e971d19d13e555912a37) Thanks [@tim-smart](https://github.com/tim-smart)! - add Command.withHandler,transformHandler,provide,provideEffectDiscard + +## 0.25.0 + +### Minor Changes + +- [#417](https://github.com/Effect-TS/cli/pull/417) [`486dcdd`](https://github.com/Effect-TS/cli/commit/486dcddf60ee603fb02ca30d09e984826c1f66e5) Thanks [@tim-smart](https://github.com/tim-smart)! - update effect + +### Patch Changes + +- [#411](https://github.com/Effect-TS/cli/pull/411) [`07b3529`](https://github.com/Effect-TS/cli/commit/07b35297b18401a3b3600bd6ccdbfd8dc496c353) Thanks [@IMax153](https://github.com/IMax153)! - default `CliConfig.finalCheckBuiltIn` to `false` + +- [#404](https://github.com/Effect-TS/cli/pull/404) [`70fc225`](https://github.com/Effect-TS/cli/commit/70fc225a2e463ec5b2cea6692491e036ec41fd5b) Thanks [@IMax153](https://github.com/IMax153)! - remove `"type"` option from `Prompt.text` and add `Prompt.password` and `Prompt.hidden` which return `Secret` + +- [#416](https://github.com/Effect-TS/cli/pull/416) [`234c3f7`](https://github.com/Effect-TS/cli/commit/234c3f780cd9409386b5b4fbcccaadbe7035c2b9) Thanks [@IMax153](https://github.com/IMax153)! - Make help documentation print built-in options by default + + The printing of built-in options in the help documentation can be disabled by providing a custom + `CliConfig` to your CLI application with `showBuiltIns` set to `false`. + +## 0.24.0 + +### Minor Changes + +- [#410](https://github.com/Effect-TS/cli/pull/410) [`686ce6c`](https://github.com/Effect-TS/cli/commit/686ce6c7caf6be6f0c6b37e8b83e746cac95a1cd) Thanks [@tim-smart](https://github.com/tim-smart)! - update effect + +### Patch Changes + +- [#407](https://github.com/Effect-TS/cli/pull/407) [`77b31e8`](https://github.com/Effect-TS/cli/commit/77b31e891d0a246db709cf7dba81dd7cd19a5d44) Thanks [@tim-smart](https://github.com/tim-smart)! - fix Subcommand type extraction + +## 0.23.1 + +### Patch Changes + +- [#403](https://github.com/Effect-TS/cli/pull/403) [`26ab5b5`](https://github.com/Effect-TS/cli/commit/26ab5b5304c84faf73e4d9e7a2443332e7a6b640) Thanks [@tim-smart](https://github.com/tim-smart)! - add Args/Options.withFallbackConfig + +## 0.23.0 + +### Minor Changes + +- [#373](https://github.com/Effect-TS/cli/pull/373) [`e6b790d`](https://github.com/Effect-TS/cli/commit/e6b790d0c05be67a6eccb4673d803ebf4faec832) Thanks [@IMax153](https://github.com/IMax153)! - implement `--wizard` mode for cli applications + +- [#373](https://github.com/Effect-TS/cli/pull/373) [`e6b790d`](https://github.com/Effect-TS/cli/commit/e6b790d0c05be67a6eccb4673d803ebf4faec832) Thanks [@IMax153](https://github.com/IMax153)! - implement completion script generation for cli applications + +- [#390](https://github.com/Effect-TS/cli/pull/390) [`1512ce7`](https://github.com/Effect-TS/cli/commit/1512ce7c9da71c1bf122b4e11205f2b158c8f04e) Thanks [@tim-smart](https://github.com/tim-smart)! - add localized handlers for Command's + +- [#398](https://github.com/Effect-TS/cli/pull/398) [`3e21194`](https://github.com/Effect-TS/cli/commit/3e21194f61de4144161eeaa1bfcb54946b588b0f) Thanks [@tim-smart](https://github.com/tim-smart)! - update effect + +- [#388](https://github.com/Effect-TS/cli/pull/388) [`0502e7e`](https://github.com/Effect-TS/cli/commit/0502e7e176606069a46ad0c09d2ce8db0468a835) Thanks [@tim-smart](https://github.com/tim-smart)! - update effect + +### Patch Changes + +- [#382](https://github.com/Effect-TS/cli/pull/382) [`d24623b`](https://github.com/Effect-TS/cli/commit/d24623bfce76bb407d03ff1f61a3936bd0902d64) Thanks [@IMax153](https://github.com/IMax153)! - fix the type signature of `Options.keyValueMap` + +- [#397](https://github.com/Effect-TS/cli/pull/397) [`48db351`](https://github.com/Effect-TS/cli/commit/48db351b51a74f634779b453f299376a526da911) Thanks [@tim-smart](https://github.com/tim-smart)! - fix withDescription for mapped commands + +- [#375](https://github.com/Effect-TS/cli/pull/375) [`ab92954`](https://github.com/Effect-TS/cli/commit/ab92954a8d3dc22970712af5ce487c004d004737) Thanks [@IMax153](https://github.com/IMax153)! - cleanup readonly tuple types + +- [#385](https://github.com/Effect-TS/cli/pull/385) [`fec4166`](https://github.com/Effect-TS/cli/commit/fec416627e389f111cd82f0dbe0e512ac48b9d8b) Thanks [@IMax153](https://github.com/IMax153)! - support multi-valued arguments appearing anywhere in command-line arguments + +- [#383](https://github.com/Effect-TS/cli/pull/383) [`714fe74`](https://github.com/Effect-TS/cli/commit/714fe74dfe919b79384480cd62d1a2f62f537932) Thanks [@IMax153](https://github.com/IMax153)! - add support for variadic options + +- [#384](https://github.com/Effect-TS/cli/pull/384) [`3fd5804`](https://github.com/Effect-TS/cli/commit/3fd58041e5b45c20205bee48eca28eedf20e154b) Thanks [@IMax153](https://github.com/IMax153)! - implement withDefault for Args + +- [#381](https://github.com/Effect-TS/cli/pull/381) [`fb0bb00`](https://github.com/Effect-TS/cli/commit/fb0bb00cf7b4c3fcda8dccb3783df67e3e8f474b) Thanks [@IMax153](https://github.com/IMax153)! - introduce Args.optional + +- [#375](https://github.com/Effect-TS/cli/pull/375) [`ab92954`](https://github.com/Effect-TS/cli/commit/ab92954a8d3dc22970712af5ce487c004d004737) Thanks [@IMax153](https://github.com/IMax153)! - convert all modules to better support tree-shaking + +- [#378](https://github.com/Effect-TS/cli/pull/378) [`2cc9d15`](https://github.com/Effect-TS/cli/commit/2cc9d15541011e20b8d4bc1a7971c84f179589f8) Thanks [@IMax153](https://github.com/IMax153)! - fix completion script generation + +## 0.22.0 + +### Minor Changes + +- [#370](https://github.com/Effect-TS/cli/pull/370) [`10eceaa`](https://github.com/Effect-TS/cli/commit/10eceaa9c558166eaa8c4090cc4950fbb8c2de9f) Thanks [@tim-smart](https://github.com/tim-smart)! - update effect + +## 0.21.0 + +### Minor Changes + +- [#365](https://github.com/Effect-TS/cli/pull/365) [`cb01813`](https://github.com/Effect-TS/cli/commit/cb01813803a1458248cfda1c3f23844bedf9ff40) Thanks [@fubhy](https://github.com/fubhy)! - Fixed exports of public module at subpaths + +- [#353](https://github.com/Effect-TS/cli/pull/353) [`a09fa9f`](https://github.com/Effect-TS/cli/commit/a09fa9feaf9dfdd19b4a3b3a15ad2854e190391e) Thanks [@IMax153](https://github.com/IMax153)! - refactor library internals to fix a number of different bugs + +### Patch Changes + +- [#358](https://github.com/Effect-TS/cli/pull/358) [`07eaa9d`](https://github.com/Effect-TS/cli/commit/07eaa9db5b828f1515fba9aa01265d05f507b748) Thanks [@IMax153](https://github.com/IMax153)! - add support for auto-generating completions for a cli program + +## 0.20.1 + +### Patch Changes + +- [#351](https://github.com/Effect-TS/cli/pull/351) [`95d2057`](https://github.com/Effect-TS/cli/commit/95d2057f831c625c39b3f6a791c6979c8c887c75) Thanks [@tim-smart](https://github.com/tim-smart)! - update effect + +## 0.20.0 + +### Minor Changes + +- [#349](https://github.com/Effect-TS/cli/pull/349) [`af7a22f`](https://github.com/Effect-TS/cli/commit/af7a22f751f368368f07e8cd99a9f7522fae194e) Thanks [@tim-smart](https://github.com/tim-smart)! - update effect + +## 0.19.0 + +### Minor Changes + +- [#345](https://github.com/Effect-TS/cli/pull/345) [`0be387a`](https://github.com/Effect-TS/cli/commit/0be387af00d114bb70c4c2089eabedca30e0d9c2) Thanks [@tim-smart](https://github.com/tim-smart)! - update effect + +## 0.18.0 + +### Minor Changes + +- [#343](https://github.com/Effect-TS/cli/pull/343) [`f3facf4`](https://github.com/Effect-TS/cli/commit/f3facf4a99772098b90a51f173de514fbcf8a717) Thanks [@tim-smart](https://github.com/tim-smart)! - update effect + +## 0.17.0 + +### Minor Changes + +- [#341](https://github.com/Effect-TS/cli/pull/341) [`263180c`](https://github.com/Effect-TS/cli/commit/263180cbdc7016f377e793de6c68c6c3a9c75cff) Thanks [@tim-smart](https://github.com/tim-smart)! - update effect + +## 0.16.0 + +### Minor Changes + +- [#338](https://github.com/Effect-TS/cli/pull/338) [`94b219e`](https://github.com/Effect-TS/cli/commit/94b219ed3985891dfed82e42ebc4a26a429e8169) Thanks [@tim-smart](https://github.com/tim-smart)! - use preconstruct + +### Patch Changes + +- [#336](https://github.com/Effect-TS/cli/pull/336) [`06934e8`](https://github.com/Effect-TS/cli/commit/06934e8254c93a0488ec1f3a70a61f106630215b) Thanks [@IMax153](https://github.com/IMax153)! - add Prompt module + +## 0.15.1 + +### Patch Changes + +- [#333](https://github.com/Effect-TS/cli/pull/333) [`333baa6`](https://github.com/Effect-TS/cli/commit/333baa60cbd847f39d7ab3303a7d866b467cb896) Thanks [@tim-smart](https://github.com/tim-smart)! - update effect + +## 0.15.0 + +### Minor Changes + +- [#330](https://github.com/Effect-TS/cli/pull/330) [`7e8267a`](https://github.com/Effect-TS/cli/commit/7e8267aed27cf352831e12f6fbcdf844376e6262) Thanks [@tim-smart](https://github.com/tim-smart)! - update to effect package + +## 0.14.0 + +### Minor Changes + +- [#328](https://github.com/Effect-TS/cli/pull/328) [`469a824`](https://github.com/Effect-TS/cli/commit/469a8242e1ab774ff55b12c8bab65a6f2fbd2881) Thanks [@tim-smart](https://github.com/tim-smart)! - update dependencies + +## 0.13.0 + +### Minor Changes + +- [#326](https://github.com/Effect-TS/cli/pull/326) [`22dd35f`](https://github.com/Effect-TS/cli/commit/22dd35fe0eb7f24e7d19015ebd83d4c300cc5422) Thanks [@IMax153](https://github.com/IMax153)! - use builtin Console service + +## 0.12.0 + +### Minor Changes + +- [#324](https://github.com/Effect-TS/cli/pull/324) [`70edc03`](https://github.com/Effect-TS/cli/commit/70edc03b932b3f4cf068ff14afd5b585ec8beeed) Thanks [@tim-smart](https://github.com/tim-smart)! - update dependencies + +## 0.11.0 + +### Minor Changes + +- [#322](https://github.com/Effect-TS/cli/pull/322) [`79befce`](https://github.com/Effect-TS/cli/commit/79befceef82614438589746ae5bddc6571705518) Thanks [@tim-smart](https://github.com/tim-smart)! - update dependencies + +## 0.10.1 + +### Patch Changes + +- [#320](https://github.com/Effect-TS/cli/pull/320) [`2ebdf8c`](https://github.com/Effect-TS/cli/commit/2ebdf8c870f23fd20e0aa1b1c1cb5581056e73cc) Thanks [@tim-smart](https://github.com/tim-smart)! - move /printer to peer deps and fix version + +## 0.10.0 + +### Minor Changes + +- [#319](https://github.com/Effect-TS/cli/pull/319) [`6dd210a`](https://github.com/Effect-TS/cli/commit/6dd210a46e1f2f16b8c8ac85e746c64ea5f00b57) Thanks [@tim-smart](https://github.com/tim-smart)! - update dependencies + +### Patch Changes + +- [#316](https://github.com/Effect-TS/cli/pull/316) [`3f4e8c3`](https://github.com/Effect-TS/cli/commit/3f4e8c3337d0ca1c1960a060654a4f86e9b23685) Thanks [@fubhy](https://github.com/fubhy)! - Made `Command`, `Option`, `Args` and `Primitive` pipeable + +- [#318](https://github.com/Effect-TS/cli/pull/318) [`4a0fbae`](https://github.com/Effect-TS/cli/commit/4a0fbae24f85dd396ee36c3f185ccc84026839b7) Thanks [@tim-smart](https://github.com/tim-smart)! - use peer deps for /data and /io + +- [#314](https://github.com/Effect-TS/cli/pull/314) [`886f1fe`](https://github.com/Effect-TS/cli/commit/886f1fe3666aacd1fb54e5b0cde85f8b6fdb88d8) Thanks [@fubhy](https://github.com/fubhy)! - Fixed `withDefault` types + +- [#317](https://github.com/Effect-TS/cli/pull/317) [`eca403d`](https://github.com/Effect-TS/cli/commit/eca403d834ec0dc2918828f0140ef8c7052c80ff) Thanks [@tim-smart](https://github.com/tim-smart)! - update build tools + +## 0.9.0 + +### Minor Changes + +- [#312](https://github.com/Effect-TS/cli/pull/312) [`5385275`](https://github.com/Effect-TS/cli/commit/5385275f9f151b99a0a5fa4f2364b3a7417e0509) Thanks [@tim-smart](https://github.com/tim-smart)! - update dependencies + +## 0.8.0 + +### Minor Changes + +- [#309](https://github.com/Effect-TS/cli/pull/309) [`dca0f8f`](https://github.com/Effect-TS/cli/commit/dca0f8fc721b5f85ddf9bf1cf7c3d5978ac63bef) Thanks [@tim-smart](https://github.com/tim-smart)! - update dependencies + +## 0.7.0 + +### Minor Changes + +- [#307](https://github.com/Effect-TS/cli/pull/307) [`81da44d`](https://github.com/Effect-TS/cli/commit/81da44deb77c52ce203ba1716f3be972ac9a3594) Thanks [@IMax153](https://github.com/IMax153)! - upgrade to latest effect packages + +## 0.6.0 + +### Minor Changes + +- [#305](https://github.com/Effect-TS/cli/pull/305) [`51a1bda`](https://github.com/Effect-TS/cli/commit/51a1bda139217fcaefccdf0145e0cb7665906931) Thanks [@IMax153](https://github.com/IMax153)! - upgrade to @effect/data@0.13.5, @effect/io@0.31.3, and @effect/printer{-ansi}@0.9.0 + +## 0.5.0 + +### Minor Changes + +- [#303](https://github.com/Effect-TS/cli/pull/303) [`9c3cf14`](https://github.com/Effect-TS/cli/commit/9c3cf1437709a13f35a127629cdd3b112edebc29) Thanks [@tim-smart](https://github.com/tim-smart)! - update dependencies + +### Patch Changes + +- [#301](https://github.com/Effect-TS/cli/pull/301) [`bb8f3b5`](https://github.com/Effect-TS/cli/commit/bb8f3b5457586c4060b2af500bbb365b74f3c3d1) Thanks [@IMax153](https://github.com/IMax153)! - separate Options.choice and Options.choiceWithValue + +## 0.4.1 + +### Patch Changes + +- [#299](https://github.com/Effect-TS/cli/pull/299) [`06267a8`](https://github.com/Effect-TS/cli/commit/06267a864e4636bf5ff79f2abecc47940954db5f) Thanks [@IMax153](https://github.com/IMax153)! - update dependencies + +## 0.4.0 + +### Minor Changes + +- [#296](https://github.com/Effect-TS/cli/pull/296) [`13cbed7`](https://github.com/Effect-TS/cli/commit/13cbed7013035b74f37a34de50794d6a41c29f8e) Thanks [@tim-smart](https://github.com/tim-smart)! - update /io and /data + +## 0.3.0 + +### Minor Changes + +- [#295](https://github.com/Effect-TS/cli/pull/295) [`dfb0b05`](https://github.com/Effect-TS/cli/commit/dfb0b05fde9bbf3b4de43fab45112cd343033ea3) Thanks [@tim-smart](https://github.com/tim-smart)! - update dependencies + +### Patch Changes + +- [#293](https://github.com/Effect-TS/cli/pull/293) [`f702bb9`](https://github.com/Effect-TS/cli/commit/f702bb9d1a38b1b632fb3461e0d8b335d2c63c79) Thanks [@tim-smart](https://github.com/tim-smart)! - non empty chunks for more than one element + +## 0.2.0 + +### Minor Changes + +- [#289](https://github.com/Effect-TS/cli/pull/289) [`39c90e9`](https://github.com/Effect-TS/cli/commit/39c90e9b70bfb8a34a82e34811ca48279c3f0326) Thanks [@tim-smart](https://github.com/tim-smart)! - add variadic Options + +### Patch Changes + +- [#288](https://github.com/Effect-TS/cli/pull/288) [`9b14798`](https://github.com/Effect-TS/cli/commit/9b14798ee6ad1bf0e12d3b907195ffd6d79397e7) Thanks [@tim-smart](https://github.com/tim-smart)! - improve optional message if default is Option + +- [#292](https://github.com/Effect-TS/cli/pull/292) [`e15ef09`](https://github.com/Effect-TS/cli/commit/e15ef0943a002165c4109a0b8178e55c48cef3a6) Thanks [@tim-smart](https://github.com/tim-smart)! - update /printer + +## 0.1.0 + +### Minor Changes + +- [#286](https://github.com/Effect-TS/cli/pull/286) [`9000a03`](https://github.com/Effect-TS/cli/commit/9000a03306d1aecca5e06efea475cccf68d37707) Thanks [@tim-smart](https://github.com/tim-smart)! - update dependencies + +## 0.0.1 + +### Patch Changes + +- [#284](https://github.com/Effect-TS/cli/pull/284) [`5fc66c6`](https://github.com/Effect-TS/cli/commit/5fc66c66c2a6f6c8910cb38000f2f71b7ac2a715) Thanks [@IMax153](https://github.com/IMax153)! - initial release diff --git a/repos/effect/packages/cli/LICENSE b/repos/effect/packages/cli/LICENSE new file mode 100644 index 0000000..be1f5c1 --- /dev/null +++ b/repos/effect/packages/cli/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Effectful Technologies Inc + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/repos/effect/packages/cli/README.md b/repos/effect/packages/cli/README.md new file mode 100644 index 0000000..2d13874 --- /dev/null +++ b/repos/effect/packages/cli/README.md @@ -0,0 +1,1245 @@ +# Installation + +Installing `@effect/cli` is straightforward and can be done using any of the popular package managers. Follow the steps below to get started with setting up `@effect/cli` on your system. + +### Step 1: Install `@effect/cli` + +Choose your preferred package manager and run one of the following commands in your terminal: + +- **Using npm:** + + ```sh + npm install @effect/cli + ``` + +- **Using pnpm:** + + ```sh + pnpm add @effect/cli + ``` + +- **Using yarn:** + ```sh + yarn add @effect/cli + ``` + +### Step 2: Install Platform-Specific Packages + +`@effect/cli` interacts directly with various platform-specific services like the file system and the terminal. Depending on the environment where you'll run your command-line application, you need to install the appropriate `@effect/platform` package. + +#### For Node.js Environments + +If your application will run in a Node.js environment, you'll need to install `@effect/platform-node`. This package ensures that `@effect/cli` can effectively interact with Node.js-specific functionalities. + +Run one of the following commands based on your package manager: + +- **Using npm:** + + ```sh + npm install @effect/platform-node + ``` + +- **Using pnpm:** + + ```sh + pnpm add @effect/platform-node + ``` + +- **Using yarn:** + ```sh + yarn add @effect/platform-node + ``` + +### Step 3: Configure Your Application + +After installing the necessary packages, you must configure your application to use the `NodeContext.layer` from `@effect/platform-node`. This step is crucial as it grants `@effect/cli` access to all necessary Node.js services and APIs, ensuring your CLI tool functions correctly within the Node.js environment. + +Here's how you can incorporate `NodeContext.layer` into your application: + +```ts +import { NodeContext, NodeRuntime } from "@effect/platform-node" +// Your application's setup code here +``` + +This configuration will make sure that your CLI application is fully integrated with the Node.js runtime, allowing it to perform optimally with access to system resources and services. + +For a more detailed walkthrough, take a read through the [Tutorial](#tutorial) below. + +# Built-In Options + +`@effect/cli` comes equipped with several powerful built-in options that enhance the functionality of your CLI applications without the need for additional coding. These options are ready to use immediately after installation and are designed to simplify common tasks and improve the user experience. + +### Overview of Built-In Options + +Here's a breakdown of the key built-in options available in `@effect/cli`: + +- **Log Level (`[--log-level]`)**: + + - **Description**: Sets the **minimum** log level for a `Command`'s handler method + - **Usage**: `--log-level (all | trace | debug | info | warning | error | fatal | none)` + - **Functionality**: Allows you to specify the **minimum** log level for a `Command`'s handler method. By setting this option, you can control the verbosity of the log output, ensuring that only logs of a certain priority or higher are output by your program. + +- **Shell Completions (`[--completions]`)**: + + - **Description**: Automatically generates shell completion scripts to enhance user experience. Shell completions suggest possible command options when you type a command and hit the tab key. + - **Usage**: `--completions (bash | sh | fish | zsh)` + - **Functionality**: Depending on your shell environment (bash, sh, fish, or zsh), this option generates a script that, when sourced, provides tab completions for your CLI commands. + +- **Help (`[-h | --help]`)**: + + - **Description**: Instantly generates and displays helpful documentation about your CLI application's commands and options. + - **Usage**: `-h` or `--help` + - **Functionality**: When this option is used, it displays all available commands and options along with descriptions, usage patterns, and examples if available. This is crucial for new users or when you need a quick reminder about the tool's capabilities. + +- **Version (`[--version]`)**: + + - **Description**: Displays the current version number of your CLI application. + - **Usage**: `--version` + - **Functionality**: This is particularly useful for debugging and ensuring compatibility, as it lets you confirm the version of the CLI tool you are currently using. + +- **Wizard Mode (`[--wizard]`)**: + - **Description**: Activates a guided interface to help users construct commands. + - **Usage**: `--wizard` + - **Functionality**: This interactive mode takes users step-by-step through the process of building a command, making it ideal for newcomers or complex commands. It asks questions and uses the responses to form the correct command syntax, which can then be executed or edited further. + +### Practical Applications + +These built-in options are designed to make the CLI user-friendly and more accessible, especially for those who are new to command-line interfaces. They reduce the learning curve and provide immediate assistance, enhancing productivity and user engagement. + +For instance, a new user can type the following to get a list of all commands and options: + +```sh +your-cli-app --help +``` + +Or, to quickly add command completion to their shell, they might use: + +```sh +source <(your-cli-app --completions bash) +``` + +# Overview + +`@effect/cli` is a powerful framework designed to simplify the development of command-line applications in TypeScript. It employs a modular architecture that allows developers to create scalable and maintainable CLI tools. Below is a table highlighting its key features: + +| Feature | Description | +| ----------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | +| Command Structure | Supports a hierarchical command structure with a top-level command and potentially multiple nested subcommands. | +| Parsing Arguments | Built-in support for parsing command-line arguments efficiently. | +| Generating Help Text | Automatically generates and displays help documentation for each command. | +| Handling Subcommands | Facilitates the management and execution of nested subcommands. | +| Built-in Options | Includes built-in options such as `--help`, `--version`, and shell completions for enhanced usability. | +| Wizard Mode | Offers a Wizard Mode that guides users through constructing commands interactively. | +| Platform-Specific Integration | Integrates with platform-specific services via `@effect/platform` packages, ensuring compatibility with diverse environments like Node.js. and Bun | + +## Command Structure + +Every command-line application built with `@effect/cli` consists of one or more commands (`Command`). There is always a top-level command representing the application itself, and potentially multiple nested subcommands: + +- **Top-Level Command**: This is the main command that represents your application. It's always of type `Command`. +- **Subcommands**: These are nested commands under the top-level command. Each subcommand is also of type `Command`, allowing you to organize functionality into distinct actions. +- **Options**: Commands can have zero or more options (such as `--help` or `--version`). These are specified using `Options` and can be either required or optional. Options can be boolean flags or can accept values from user input. +- **Arguments**: Commands can also have zero or more arguments (such as ``). These are specified using `Args` and represent the data that users need to provide to commands. +- **Command Handler**: Each command has a command handler, which is a function responsible for the actual execution of the command. This is where you define what the command does when it runs. + +This structure allows you to build complex CLI tools that are easy to extend and maintain. Whether you are adding new options to existing commands, creating new subcommands, or handling user inputs, `@effect/cli` provides a structured and intuitive way to scale your application. + +# Getting Started with Your First CLI Application + +## Creating a Simple "Hello World" CLI + +Starting with a basic "Hello World" application is a great way to get familiar with the `@effect/cli`. Below is a step-by-step guide to creating your first command-line interface (CLI) application. + +### 1. Set Up Your Project + +Begin by creating a new file for your project: + +- **File Name**: `hello-world.ts` +- **Purpose**: This file will hold all the necessary code for your CLI application. + +### 2. Write the CLI Code + +Now, let's write the code for your CLI. Open your `hello-world.ts` file in your favorite code editor and insert the following TypeScript code: + +```ts +// Import necessary modules from the libraries +import { Command } from "@effect/cli" +import { NodeContext, NodeRuntime } from "@effect/platform-node" +import { Console, Effect } from "effect" + +// Define the top-level command +const command = Command.make("hello-world", {}, () => + Console.log("Hello World") +) + +// Set up the CLI application +const cli = Command.run(command, { + name: "Hello World CLI", + version: "v1.0.0" +}) + +// Prepare and run the CLI application +cli(process.argv).pipe(Effect.provide(NodeContext.layer), NodeRuntime.runMain) +``` + +**Explanation of Code:** + +- **Import Statements**: + + - `Command` from `@effect/cli` allows you to define commands and subcommands. + - `NodeContext` and `NodeRuntime` from `@effect/platform-node` enable your CLI to interact and integrate seamlessly with the Node.js runtime environment. This setup is crucial for running your CLI on Node.js, as it ensures that all Node-specific APIs and functionalities are accessible. + - `Console` and `Effect` from `effect` provide utilities for logging and managing effects, which are essential for handling asynchronous operations and side effects in your CLI. + +- **Command Definition**: + + - The `Command.make` function creates a new command named "hello-world". This command is configured to print "Hello World" when executed, serving as the basic functionality of your CLI. + +- **CLI Configuration**: + + - The `Command.run` function initializes your CLI application with a specific name and version, preparing it for execution. + +- **Execution Setup**: + - The `cli(process.argv)` call processes the command-line arguments. + - It uses `Effect.provide` to inject the `NodeContext.layer`, which integrates the CLI with the Node.js environment, allowing your application to utilize Node-specific features and settings. + - `NodeRuntime.runMain` ensures that your application is executed within the Node.js main runtime, handling any asynchronous tasks and managing the lifecycle of your CLI. + +### 3. Run Your CLI + +After saving your `hello-world.ts` file, you can run your CLI application directly from your terminal to see it in action. Here's how: + +```sh +npx tsx hello-world.ts +# Expected Output: Hello World +``` + +**Explanation of the Command:** + +- **`tsx`**: This is a command-line tool that enables direct execution of TypeScript files. It simplifies the development process by eliminating the need to manually compile TypeScript (`*.ts`) files into JavaScript (`*.js`) before running them. `tsx` automatically compiles the TypeScript code on-the-fly and executes it, leveraging the Node.js environment. + +- **`npx`**: Part of the npm (Node Package Manager) suite, `npx` is used to execute packages. When you run `npx tsx`, it temporarily installs `tsx` if it isn't already present in your project's local `node_modules` folder or globally on your machine. Then, `npx` executes `tsx` with the specified TypeScript file as its argument. + +- **Usage in Your CLI**: By using `npx tsx hello-world.ts`, you're instructing `npx` to execute your TypeScript file using `tsx`. This command is especially useful for quick testing and development purposes, as it allows you to run your code directly without setting up a full TypeScript compilation workflow beforehand. + +## Exploring CLI Features + +Your new CLI comes with a variety of built-in features designed to enhance usability and help you manage your application effectively: + +### Check the Version + +You can display the version of your CLI by using the `--version` option: + +```sh +npx tsx hello-world.ts --version +# Output: v1.0.0 +``` + +### Access Help Information + +Your CLI automatically generates help documentation that describes available commands and options. This feature is useful when you need guidance on how the CLI operates or when you want to learn more about its capabilities. + +```sh +npx tsx hello-world.ts --help # or -h +``` + +When you request help, the CLI will display information like this: + +``` +Hello World CLI + +Hello World CLI v1.0.0 + +USAGE + +$ hello-world + +OPTIONS + +--completions sh | bash | fish | zsh + + One of the following: sh, bash, fish, zsh + + Generate a completion script for a specific shell + + This setting is optional. + +(-h, --help) + + A true or false value. + + Show the help documentation for a command + + This setting is optional. + +--wizard + + A true or false value. + + Start wizard mode for a command + + This setting is optional. + +--version + + A true or false value. + + Show the version of the application + + This setting is optional. +``` + +### Using the Wizard Mode + +The `--wizard` option activates the Wizard Mode in your CLI application, which provides a guided process for constructing commands. This is especially helpful for users who are new to your CLI or need assistance in building the correct command syntax. + +To initiate the Wizard Mode, run your CLI application with the `--wizard` option like this: + +```sh +npx tsx hello-world.ts --wizard +``` + +When you start the Wizard Mode, the CLI will interactively guide you through the process of setting up a command. Here's what the interaction might look like: + +``` +Wizard Mode for CLI Application: Hello World CLI (v1.0.0) + +Instructions + + The wizard mode will assist you with constructing commands for Hello World CLI (v1.0.0). + + Please answer all prompts provided by the wizard. + +COMMAND: hello-world + +Wizard Mode Complete! + +You may now execute your command directly with the following options and arguments: + + hello-world + +✔ Would you like to run the command? … yes / no +``` + +# Basic Usage + +## Adding Arguments to Commands + +Adding arguments to your commands allows your CLI applications to accept and process user input dynamically. Let's create a simple `echo` CLI that echoes back whatever text you pass to it. + +### Setting Up Your CLI + +Begin by creating a new TypeScript file named `echo.ts`. This file will contain all the code necessary to define a command that accepts user input as an argument. + +```ts +// Import the necessary modules from the Effect libraries +import { Args, Command } from "@effect/cli" +import { NodeContext, NodeRuntime } from "@effect/platform-node" +import { Console, Effect } from "effect" + +// Define a text argument +const text = Args.text({ name: "text" }) + +// Create a command that logs the provided text argument to the console +const command = Command.make("echo", { text }, ({ text }) => Console.log(text)) + +// Configure and initialize the CLI application +const cli = Command.run(command, { + name: "Echo CLI", + version: "v0.0.1" +}) + +// Prepare and run the CLI application, providing necessary context and runtime +cli(process.argv).pipe(Effect.provide(NodeContext.layer), NodeRuntime.runMain) +``` + +**Understanding the Code:** + +- **Arguments**: The `text` declaration creates an input parameter that users need to provide when they run your CLI. +- **Command Definition**: The `Command.make` function sets up your CLI command. It's designed to take the `text` argument and display it using `Console.log`. +- **Command Execution:** By using `Command.run`, the CLI application is formally structured with a specified name and version, and is ready to execute based on the defined command structure. +- **Execution Setup**: The `cli(process.argv)` call processes the command-line arguments and runs the CLI application. The `Effect.provide(NodeContext.layer)` injects the necessary Node.js context, and `NodeRuntime.runMain` ensures proper execution within the Node.js environment. + +### Running the CLI + +With your `echo.ts` script ready, you can run it to interact with the argument you've set up. + +**Run Without Arguments** + +If you try running the CLI without specifying any arguments, it will remind you to provide the required text: + +```sh +npx tsx echo.ts +# Output: Missing argument +``` + +**Run With Arguments** + +To pass the text argument correctly, wrap your input in quotes to treat it as a single string: + +```sh +npx tsx echo.ts "This is a test" +# Output: This is a test +``` + +**Common Mistake** + +Forgetting to use quotes can cause issues since each word might be interpreted as a separate argument: + +```sh +npx tsx echo.ts This is a test +# Output: Received unknown argument: 'is' +``` + +## Adding Options to Commands + +Let's enhance the `echo` CLI we built earlier by introducing an option that allows text to be displayed in bold. This tutorial will guide you on adding a `--bold` option, abbreviated as `-b`, to your command. + +### Modify Your TypeScript File + +First, open the TypeScript file (`echo.ts`) where your `echo` command is defined. We will add a new option that enables users to choose whether their text output should be bold. + +### Update the Code + +Below is the updated version of your code with the bold option included: + +```ts +import { Args, Command, Options } from "@effect/cli" +import { NodeContext, NodeRuntime } from "@effect/platform-node" +import { Console, Effect } from "effect" + +const text = Args.text({ name: "text" }) + +// Define the 'bold' option with an alias '-b' +const bold = Options.boolean("bold").pipe(Options.withAlias("b")) + +// Create the command that outputs the text with bold formatting if the bold option is used +const command = Command.make("echo", { text, bold }, ({ bold, text }) => + Console.log(bold ? `\x1b[1m${text}\x1b[0m` : text) +) + +const cli = Command.run(command, { + name: "Echo CLI", + version: "v0.0.2" +}) + +cli(process.argv).pipe(Effect.provide(NodeContext.layer), NodeRuntime.runMain) +``` + +### Explanation of the Changes + +- **Bold Option**: The `bold` declaration creates a boolean option that users can toggle. Using `Options.withAlias("b")` allows the option to be invoked with a shorter `-b` flag. This makes the command easier to type and remember. + +- **Command Functionality**: Within the function of the command, there's a check to see if the `bold` option is true. If it is, the text is formatted with ANSI escape codes (`\x1b[1m` and `\x1b[0m`) to appear bold in the terminal. This formatting adds visual emphasis and can help distinguish output in complex console applications. + +### Running the Updated CLI + +With the command updated, you can now see the effect of the `--bold` or `-b` option by running: + +```sh +npx tsx echo.ts --bold "This is a test" +``` + +or + +```sh +npx tsx echo.ts -b "This is a test" +``` + +**Expected Output:** + +Both commands will display "This is a test" in bold text, assuming your terminal supports ANSI escape codes. + +## Important Note on Argument Order + +When using your CLI, it's crucial to understand the order in which you specify options and arguments. By default, the `@effect/cli` parses `Options` and `Args` **before** any subcommands. This means that options need to be placed directly after the main command, and before any subcommands or additional arguments. + +For example, the command: + +```sh +npx tsx echo.ts "This is a test" -b +``` + +**would not work** because the `-b` option appears after the text argument `"This is a test"`. The parser expects options to be specified before any standalone arguments or subcommands. This ensures that the options are correctly associated with the main command and not misinterpreted as arguments for a subcommand or additional text. + +## Adding Valued Options to Commands + +In this section, we will continue to improve our `echo` CLI by introducing options to customize the color of the output text. This enhancement not only adds visual customization but also demonstrates how to use valued options to modify the behavior of commands. + +### Update Your TypeScript File + +Start by opening the TypeScript file where your `echo` command is defined. We will add new functionalities that allow for text coloring and optional bold formatting. + +### Code Implementation + +Below is the modified version of your code, now including options for text color: + +```ts +import { Args, Command, Options } from "@effect/cli" +import { NodeContext, NodeRuntime } from "@effect/platform-node" +import { Console, Effect, Option } from "effect" + +// Define a text argument +const text = Args.text({ name: "text" }) + +// Define the 'bold' option with an alias '-b' +const bold = Options.boolean("bold").pipe(Options.withAlias("b")) + +// Color codes for ANSI escape sequences +const colorToAnsiSequence = { + red: "\x1b[31m", + green: "\x1b[32m", + blue: "\x1b[34m" +} as const +const resetCode = "\x1b[0m" + +type SupportedColor = keyof typeof colorToAnsiSequence +const supportedColors = Object.keys(colorToAnsiSequence) as SupportedColor[] + +// Define the 'color' option with choices and an alias '-c' +const color = Options.choice("color", supportedColors).pipe( + Options.withAlias("c"), + Options.optional +) + +// Function to apply ANSI color codes based on user input +const applyColor = ( + text: string, + color: Option.Option +): string => + Option.match(color, { + onNone: () => text, + onSome: (color) => `${colorToAnsiSequence[color]}${text}${resetCode}` + }) + +// Create the command that outputs formatted text +const command = Command.make( + "echo", + { text, bold, color }, + ({ bold, color, text }) => { + let formattedText = applyColor(text, color) + if (bold) { + formattedText = `\x1b[1m${formattedText}\x1b[0m` + } + return Console.log(formattedText) + } +) + +const cli = Command.run(command, { + name: "Echo CLI", + version: "v0.0.3" +}) + +cli(process.argv).pipe(Effect.provide(NodeContext.layer), NodeRuntime.runMain) +``` + +### Explanation of the Changes + +- **Color Options**: The `color` option enables users to specify the text color as "red", "green", or "blue", enhancing the visual aspect of the output. This option uses `Options.choice` to provide a list of possible values. + +- **applyColor Function**: This function applies the chosen ANSI color code to the text. If no color is selected, the text remains unchanged. This allows for dynamic customization of the output based on user preference. + +### Running the Enhanced CLI + +With the new color and bold options, you can now run the CLI and see colored text output. Here's how to use these options: + +```sh +npx tsx echo.ts --bold --color red "This is a test" +npx tsx echo.ts -b -c green "Another test" +``` + +**Expected Output:** + +These commands will print "This is a test" in bold red and "Another test" in bold green, provided your terminal supports ANSI colors. + +## Adding Subcommands + +Let's enhance the functionality of your `echo` CLI by adding a new subcommand called `repeat`. This subcommand will allow users to repeat a specified message multiple times, providing a practical example of how to extend a CLI application. + +### Update Your TypeScript File + +Open the TypeScript file where your `echo` command is defined. We'll incorporate the `repeat` subcommand into this setup. + +### Implementing the `Repeat` Subcommand + +Here is how you can update your code to include the `repeat` subcommand: + +```ts +import { Args, Command, Options } from "@effect/cli" +import { NodeContext, NodeRuntime } from "@effect/platform-node" +import { Console, Effect, Option } from "effect" + +const text = Args.text({ name: "text" }) + +const bold = Options.boolean("bold").pipe(Options.withAlias("b")) + +const colorToAnsiSequence = { + red: "\x1b[31m", + green: "\x1b[32m", + blue: "\x1b[34m" +} as const +const resetCode = "\x1b[0m" + +type SupportedColor = keyof typeof colorToAnsiSequence +const supportedColors = Object.keys(colorToAnsiSequence) as SupportedColor[] + +const color = Options.choice("color", supportedColors).pipe( + Options.withAlias("c"), + Options.optional +) + +const applyColor = ( + text: string, + color: Option.Option +): string => + Option.match(color, { + onNone: () => text, + onSome: (color) => `${colorToAnsiSequence[color]}${text}${resetCode}` + }) + +// Argument for the number of repetitions +const count = Args.integer().pipe(Args.withDefault(1)) + +// Creating the repeat subcommand +const repeat = Command.make("repeat", { count }, ({ count }) => + echo.pipe( + Effect.andThen((config) => Effect.repeatN(echo.handler(config), count - 1)) + ) +) + +// Main echo command +const echo = Command.make( + "echo", + { text, bold, color }, + ({ bold, color, text }) => { + let formattedText = applyColor(text, color) + if (bold) { + formattedText = `\x1b[1m${formattedText}\x1b[0m` + } + return Console.log(formattedText) + } +) + +// Combining commands +const command = echo.pipe(Command.withSubcommands([repeat])) + +const cli = Command.run(command, { + name: "Echo CLI", + version: "v0.0.4" +}) + +cli(process.argv).pipe(Effect.provide(NodeContext.layer), NodeRuntime.runMain) +``` + +### Explanation of the Changes + +- **Count Argument**: We introduced a new argument named `count`. This argument specifies the number of times the echo message will be repeated. +- **Repeat Command**: This new subcommand leverages the `count` argument to repeat the message the specified number of times. +- **Integration with Main Command**: We integrated the `repeat` subcommand into the existing `echo` command structure. This means users can activate this feature by simply appending the `repeat` keyword followed by the desired count after their message in the command line. For example, `echo "Hello" repeat 5` would output "Hello" five times. + +> [!NOTE] +> Since `Command` is a subtype of `Effect`, you can use `Effect.andThen` within a subcommand's handler to directly access and utilize the `Config` from a parent command, and subsequently apply its handler. + +### Running the `Repeat` Subcommand + +With the `repeat` subcommand added, you can now use it to repeat messages: + +```sh +npx tsx echo.ts -b -c red "This is a test" repeat 3 +``` + +**Expected Output:** + +This command will output "This is a test" in bold red text three times, demonstrating both the color and repeat functionalities. + +# Tutorial: Building Your Own Git-Style CLI + +In this tutorial, we will create a basic version of a Git-like command-line interface (CLI) called `minigit` using the powerful `@effect/cli` library. Our goal is to replicate a small set of Git commands to demonstrate how you can build structured CLI tools: + +``` +minigit [-v | --version] [-h | --help] [-c =] +minigit add [-v | --verbose] [--] [...] +minigit clone [--depth ] [--] [] +``` + +> [!NOTE] +> This guide focuses on the setup and parsing of commands. Implementing the actual functionality of these commands is beyond the scope of this tutorial but can be developed further based on the patterns shown here. + +The full code for this CLI application can be found in our [examples directory](./examples/minigit.ts). + +## Creating the Command-Line Application + +Begin by creating a TypeScript file named `minigit.ts`. This file will host all your CLI application code. We will structure our CLI with three main commands, demonstrating the powerful features of the `@effect/cli` library. + +### Setting Up the Main Command + +Let's start by setting up the primary command for our CLI, named `minigit`. To do this, we use the `Command.make` constructor which is pivotal for structuring commands in the `@effect/cli` framework. + +```ts +import { Command } from "@effect/cli" +import { Effect } from "effect" + +// Define the main 'minigit' command +const minigit = Command.make( + "minigit", + // Configuration object for the command + {}, + // Handler function that executes the command + (config) => Effect.succeed("Welcome to Minigit!") +) +``` + +#### Breakdown of Key Components + +- **`Command.make` Function:** This function constructs a new `Command`. It requires three parameters: + - **Name:** This is the command name, like 'minigit', which you use to call the command from the command line. + - **Configuration:** This object specifies the options (`Options`) and arguments (`Args`) that the command can accept. + - **Handler:** This function is executed when the command is called. It receives the parsed configuration and carries out the command's core functionalities. + +#### Understanding the Command Type Signature + +The `Command` interface in `@effect/cli` is structured as follows: + +```ts +export interface Command { + readonly handler: (_: A) => Effect + // Additional properties and methods... +} +``` + +- **`Name` (`Name extends string`):** A unique string that identifies the command. +- **`R` (Environment):** Defines the dependencies or the environment needed by the command's handler. +- **`E` (Expected Errors):** Specifies the types of errors the command might expect during its execution. +- **`A` (Arguments/Configuration):** Represents the configuration object the handler receives. + +### Detailed Explanation of Parameters + +**Command Name:** + +The `name` parameter in `Command.make` designates the command's identifier. This name is crucial as it is used to invoke the command from the command line. For example, if we have a CLI application called `my-cli-app` with a single subcommand named `foo`, then executing the following command will run the `foo` command in your CLI application: + +```sh +my-cli-app foo +``` + +**Command Configuration:** + +The configuration parameter allows you to define what options and arguments the command can accept. This setup includes both simple flags and more complex objects. The `Config` is an object of key/value pairs where the keys are just identifiers and the values are the `Options` and `Args` that the `Command` may receive. The `Config` object can have nested `Config` objects or arrays of `Config` objects. When the CLI application is actually executed, the command `Config` is parsed from the command-line options and arguments following the command name. + +**Command Handler:** + +This function is where the action happens. It takes the parsed configuration and executes the core functionality of the command, utilizing the full capabilities of the `Effect` framework for managing effects and asynchronous operations. + +### Our First Command + +Let's apply what we've learned from using the `Command.make` method by defining the primary command for our `minigit` CLI application. This command will include configurations that handle various options like version, help, and custom key-value pairs. + +```ts +import { Command, Options } from "@effect/cli" +import { Console, Option } from "effect" + +// minigit [--version] [-h | --help] [-c =] +const configs = Options.keyValueMap("c").pipe(Options.optional) + +// Define the main 'minigit' command +const minigit = Command.make( + "minigit", + // Configuration object for the command + { configs }, + // Handler function that executes the command + ({ configs }) => + Option.match(configs, { + onNone: () => Console.log("Running 'minigit'"), + onSome: (configs) => { + const keyValuePairs = Array.from( + configs, + ([key, value]) => `${key}=${value}` + ).join(", ") + return Console.log( + `Running 'minigit' with the following configs: ${keyValuePairs}` + ) + } + }) +) +``` + +#### Key Aspects of the Code: + +- **Options Configuration:** + + - **`Options.keyValueMap("c")`:** This line sets up an option that accepts key-value pairs, allowing the user to input configurations in the format `-c key=value`. + - **`Options.optional`:** By chaining this combinator, the `-c` option becomes optional, meaning the CLI will operate correctly whether or not this option is provided. + +- **Command Execution:** + - The command handler utilizes the `Option.match` function to determine how to respond based on whether the user has provided any key-value configurations. + - If no configurations are provided (`onNone`), it simply logs "Running 'minigit'." + - If configurations are provided (`onSome`), it logs these configurations in a readable string format, enhancing user feedback and interaction. + +#### Built-In Options: + +You may have noticed the lack of explicit version and help options in our command setup. This is due to `@effect/cli`'s design, which includes several built-in options such as `--version` and `--help` (see [Built-In Options](#built-in-options). These are automatically available and do not need to be manually configured, simplifying the setup of common CLI functionalities. + +### Expanding the `minigit` CLI with Subcommands + +Building on our basic `minigit` CLI, we'll now introduce two key subcommands: `add` and `clone`. These subcommands will demonstrate how to handle more complex command structures and multiple parameters using the `@effect/cli` library. + +#### Adding Subcommands + +We'll continue our `minigit` CLI development by incorporating `add` and `clone` subcommands to handle specific actions, much like the original Git commands. + +```ts +import { Args, Command, Options } from "@effect/cli" +import { Console, Option, Array } from "effect" + +// minigit [--version] [-h | --help] [-c =] +const configs = Options.keyValueMap("c").pipe(Options.optional) + +const minigit = Command.make("minigit", { configs }, ({ configs }) => + Option.match(configs, { + onNone: () => Console.log("Running 'minigit'"), + onSome: (configs) => { + const keyValuePairs = Array.fromIterable(configs) + .map(([key, value]) => `${key}=${value}`) + .join(", ") + return Console.log( + `Running 'minigit' with the following configs: ${keyValuePairs}` + ) + } + }) +) + +// minigit add [-v | --verbose] [--] [...] +const pathspec = Args.text({ name: "pathspec" }).pipe(Args.repeated) +const verbose = Options.boolean("verbose").pipe(Options.withAlias("v")) +const minigitAdd = Command.make( + "add", + { pathspec, verbose }, + ({ pathspec, verbose }) => { + const paths = Array.match(pathspec, { + onEmpty: () => "", + onNonEmpty: (paths) => ` ${Array.join(paths, " ")}` + }) + return Console.log( + `Running 'minigit add${paths}' with '--verbose ${verbose}'` + ) + } +) + +// minigit clone [--depth ] [--] [] +const repository = Args.text({ name: "repository" }) +const directory = Args.text({ name: "directory" }).pipe(Args.optional) +const depth = Options.integer("depth").pipe(Options.optional) +const minigitClone = Command.make( + "clone", + { repository, directory, depth }, + (config) => { + const depth = Option.map(config.depth, (depth) => `--depth ${depth}`) + const repository = Option.some(config.repository) + const optionsAndArgs = Array.getSomes([depth, repository, config.directory]) + return Console.log( + "Running 'minigit clone' with the following options and arguments: " + + `'${Array.join(optionsAndArgs, ", ")}'` + ) + } +) +``` + +#### Key Points to Note: + +1. **Importing Modules:** + + - The `Args` module from `@effect/cli` is utilized to define positional arguments for both `add` and `clone` subcommands. + - The `Array` module from `effect` is used to handle arrays and provide utility functions like `Array.match` and `Array.join`. + +2. **Configuring Commands:** + + - Both subcommands utilize options and arguments that allow for detailed configuration, reflecting common use cases in command-line interfaces. + - The `Options.withAlias` method simplifies command usage by providing shorthand aliases like `-v` for `--verbose`. + +3. **Command Handlers:** + - Each subcommand has a handler that logs execution details, which helps in understanding the flow and actions of the CLI commands. + +### Assembling Your CLI Application + +Now that you've defined all the necessary commands for your CLI application, it's time to assemble them into a fully functional command-line interface. This section will guide you through setting up the CLI to run within a NodeJS environment, assuming that you've already installed `@effect/platform-node` as described in the [Installation](#installation) guide. + +Let's put together the `minigit` CLI: + +```ts +import { Args, Command, Options } from "@effect/cli" +import { NodeContext, NodeRuntime } from "@effect/platform-node" +import { Console, Effect, Option, Array } from "effect" + +// minigit [--version] [-h | --help] [-c =] +const configs = Options.keyValueMap("c").pipe(Options.optional) +const minigit = Command.make("minigit", { configs }, ({ configs }) => + Option.match(configs, { + onNone: () => Console.log("Running 'minigit'"), + onSome: (configs) => { + const keyValuePairs = Array.fromIterable(configs) + .map(([key, value]) => `${key}=${value}`) + .join(", ") + return Console.log( + `Running 'minigit' with the following configs: ${keyValuePairs}` + ) + } + }) +) + +// minigit add [-v | --verbose] [--] [...] +const pathspec = Args.text({ name: "pathspec" }).pipe(Args.repeated) +const verbose = Options.boolean("verbose").pipe(Options.withAlias("v")) +const minigitAdd = Command.make( + "add", + { pathspec, verbose }, + ({ pathspec, verbose }) => { + const paths = Array.match(pathspec, { + onEmpty: () => "", + onNonEmpty: (paths) => ` ${Array.join(paths, " ")}` + }) + return Console.log( + `Running 'minigit add${paths}' with '--verbose ${verbose}'` + ) + } +) + +// minigit clone [--depth ] [--] [] +const repository = Args.text({ name: "repository" }) +const directory = Args.text({ name: "directory" }).pipe(Args.optional) +const depth = Options.integer("depth").pipe(Options.optional) +const minigitClone = Command.make( + "clone", + { repository, directory, depth }, + (config) => { + const depth = Option.map(config.depth, (depth) => `--depth ${depth}`) + const repository = Option.some(config.repository) + const optionsAndArgs = Array.getSomes([depth, repository, config.directory]) + return Console.log( + "Running 'minigit clone' with the following options and arguments: " + + `'${Array.join(optionsAndArgs, ", ")}'` + ) + } +) + +// Combine all commands into the main 'minigit' command +const command = minigit.pipe( + Command.withSubcommands([minigitAdd, minigitClone]) +) + +// Initialize and run the CLI application +const cli = Command.run(command, { + name: "Minigit Distributed Version Control", + version: "v1.0.0" +}) + +cli(process.argv).pipe(Effect.provide(NodeContext.layer), NodeRuntime.runMain) +``` + +#### Key Features and Configuration: + +- **Command Integration:** The `minigit` command integrates both `add` and `clone` as subcommands, enabling a structured approach to handle different functionalities within the same CLI application. +- **Environment Setup:** We've imported `NodeContext` and `NodeRuntime` to ensure that our CLI application correctly interacts with NodeJS's runtime environment, making full use of Node-specific features. +- **Command Execution:** By using `Command.run`, the CLI application is formally structured with a specified name and version, and is ready to execute based on the defined command structure. +- **Execution Setup**: The `cli(process.argv)` call processes the command-line arguments and runs the CLI application. The `Effect.provide(NodeContext.layer)` injects the necessary Node.js context, and `NodeRuntime.runMain` ensures proper execution within the Node.js environment. + +### Running the CLI Application + +Now that your CLI application, `minigit`, is fully assembled and prepared, it's time to see it in action! This section will guide you through running `minigit` directly from the command line, using the example file `minigit.ts`. If you are experimenting using the `minigit` example from the provided [repository](./examples/minigit.ts), the same instructions apply, just replace the command file name with `npx tsx ./examples/minigit.ts`. + +#### Executing Built-In Options + +First, let’s test out the built-in options to understand the core functionalities of our CLI application. + +Run the following command to display the current version of your CLI application: + +```sh +npx tsx minigit.ts --version +# Output: v1.0.0 +``` + +To view the help documentation and understand the usage of each command within `minigit`, use the `-h` or `--help` option: + +```sh +npx tsx minigit.ts --help +``` + +Output: + +``` +Minigit Distributed Version Control + +Minigit Distributed Version Control v1.0.0 + +USAGE + +$ minigit [-c text] + +OPTIONS + +-c text + + A user-defined piece of text. + + This setting is a property argument which: + + - May be specified a single time: '-c key1=value key2=value2' + + - May be specified multiple times: '-c key1=value -c key2=value2' + + This setting is optional. + +--completions sh | bash | fish | zsh + + One of the following: sh, bash, fish, zsh + + Generate a completion script for a specific shell + + This setting is optional. + +(-h, --help) + + A true or false value. + + Show the help documentation for a command + + This setting is optional. + +--wizard + + A true or false value. + + Start wizard mode for a command + + This setting is optional. + +--version + + A true or false value. + + Show the version of the application + + This setting is optional. + +COMMANDS + + - add [(-v, --verbose)] ... + + - clone [--depth integer] [] +``` + +This output provides a breakdown of all available commands and options, helping users navigate through the functionalities offered by `minigit`. + +To get more detailed help on subcommands like `add`, simply append `--help` after the subcommand: + +```sh +npx tsx minigit.ts add --help +``` + +Output: + +``` +Minigit Distributed Version Control + +Minigit Distributed Version Control v1.0.0 + +USAGE + +$ add [(-v, --verbose)] ... + +ARGUMENTS + +... + + A user-defined piece of text. + + This argument may be repeated zero or more times. + +OPTIONS + +(-v, --verbose) + + A true or false value. + + This setting is optional. + +--completions sh | bash | fish | zsh + + One of the following: sh, bash, fish, zsh + + Generate a completion script for a specific shell + + This setting is optional. + +(-h, --help) + + A true or false value. + + Show the help documentation for a command + + This setting is optional. + +--wizard + + A true or false value. + + Start wizard mode for a command + + This setting is optional. + +--version + + A true or false value. + + Show the version of the application + + This setting is optional. +``` + +#### Executing User-Defined Commands + +Beyond viewing documentation, `minigit` allows you to execute commands with specific options to tailor its behavior. + +Here's how you can add files with the verbose option enabled or disabled: + +```sh +npx tsx minigit.ts add . +# Output: Running 'minigit add .' with '--verbose false' +``` + +```sh +npx tsx minigit.ts add --verbose . +# Output: Running 'minigit add .' with '--verbose true' +``` + +```sh +npx tsx minigit.ts clone --depth 1 https://github.com/Effect-TS/cli.git +# Output: Running 'minigit clone' with the following options and arguments: '--depth 1, https://github.com/Effect-TS/cli.git' +``` + +## Accessing Parent Arguments in Subcommands + +In certain scenarios, you may want your subcommands to have access to the `Options` and `Args` passed to their parent commands. + +Since `Command` is a subtype of `Effect`, you can use `Effect.flatMap` within a subcommand's handler to extract the `Config` from a parent command. This technique allows subcommands to utilize the configuration parameters specified at higher levels in the command hierarchy. + +For example, let's say our `minigit clone` subcommand needs access to the configuration parameters passed to the parent `minigit` command via `minigit -c key=value`. We can accomplish this by modifying the `clone` command's handler to use `Effect.flatMap` with the parent `minigit` command: + +```ts +const repository = Args.text({ name: "repository" }) +const directory = Args.directory().pipe(Args.optional) +const depth = Options.integer("depth").pipe(Options.optional) +const minigitClone = Command.make( + "clone", + { repository, directory, depth }, + (subcommandConfig) => + // By using `Effect.flatMap` on the parent command, we get access to its parsed config + Effect.flatMap(minigit, (parentConfig) => { + const depth = Option.map( + subcommandConfig.depth, + (depth) => `--depth ${depth}` + ) + const repository = Option.some(subcommandConfig.repository) + const optionsAndArgs = Array.getSomes([ + depth, + repository, + subcommandConfig.directory + ]) + const configs = Option.match(parentConfig.configs, { + onNone: () => "", + onSome: (map) => + Array.fromIterable(map) + .map(([key, value]) => `${key}=${value}`) + .join(", ") + }) + return Console.log( + "Running 'minigit clone' with the following options and arguments: " + + `'${Array.join(optionsAndArgs, ", ")}'\n` + + `and the following configuration parameters: ${configs}` + ) + }) +) +``` + +By examining the type of `minigitClone` after incorporating the parent command, you can see the added context: + +```ts +const minigitClone: Command.Command< + "clone", + // The parent `minigit` command has been added to the environment required by + // the subcommand's handler + Command.Command.Context<"minigit">, + never, + { + readonly repository: string + readonly directory: Option.Option + readonly depth: Option.Option + } +> +``` + +The parent command's context will be "erased" from the subcommand's environment when using `Command.withSubcommands`: + +```ts +const command = minigit.pipe(Command.withSubcommands([minigitClone])) +// ^? Command<"minigit", never, ..., ...> +``` + +Finally, run the command with some configuration parameters to see the result: + +```sh +npx tsx minigit.ts -c key1=value1 clone --depth 1 https://github.com/Effect-TS/cli.git +# Running 'minigit clone' with the following options and arguments: '--depth 1, https://github.com/Effect-TS/cli.git' +# and the following configuration parameters: key1=value1 +``` + +# Frequently Asked Questions (FAQ) + +## Command-Line Argument Parsing Specification + +Understanding how command-line arguments are parsed in your applications is crucial for designing effective and user-friendly command interfaces. Here are the key rules that the internal command-line argument parser follows: + +### 1. Order of Options and Subcommands + +Options and arguments (collectively referred to as `Options` / `Args`) associated with a command must be specified **before** any subcommands. This rule helps the parser determine which command the options apply to. + +**Examples:** + +- **Correct Usage**: + + ```sh + program -v subcommand + ``` + + In this example, the `-v` option applies to the main program before the subcommand is processed. + +- **Incorrect Usage**: + ```sh + program subcommand -v + ``` + Here, placing `-v` after the subcommand causes confusion as to whether `-v` applies to the main program or the subcommand. + +### 2. Parsing Options Before Positional Arguments + +The parser is designed to recognize options before any positional arguments. This ordering ensures clarity and prevents confusion between options and regular arguments. + +**Examples:** + +- **Valid Command**: + + ```sh + program --option arg + ``` + + This command correctly places the `--option` before the positional argument `arg`. + +- **Invalid Command**: + ```sh + program arg --option + ``` + Placing an argument before an option is not allowed and can lead to errors in command processing. + +### 3. Handling Excess Arguments + +If there are excess arguments that do not fit the expected structure of the command, the parser will return a `ValidationError`. This safeguard prevents the execution of malformed or potentially harmful commands. + +# API Reference + +- https://effect-ts.github.io/effect/docs/cli diff --git a/repos/effect/packages/cli/docgen.json b/repos/effect/packages/cli/docgen.json new file mode 100644 index 0000000..56425ad --- /dev/null +++ b/repos/effect/packages/cli/docgen.json @@ -0,0 +1,36 @@ +{ + "$schema": "../../node_modules/@effect/docgen/schema.json", + "exclude": ["src/internal/**/*.ts"], + "srcLink": "https://github.com/Effect-TS/effect/tree/main/packages/cli/src/", + "examplesCompilerOptions": { + "noEmit": true, + "strict": true, + "skipLibCheck": true, + "moduleResolution": "Bundler", + "module": "ES2022", + "target": "ES2022", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "paths": { + "effect": ["../../../effect/src/index.js"], + "effect/*": ["../../../effect/src/*.js"], + "@effect/platform": ["../../../platform/src/index.js"], + "@effect/platform/*": ["../../../platform/src/*.js"], + "@effect/platform-node": ["../../../platform-node/src/index.js"], + "@effect/platform-node/*": ["../../../platform-node/src/*.js"], + "@effect/platform-node-shared": [ + "../../../platform-node-shared/src/index.js" + ], + "@effect/platform-node-shared/*": [ + "../../../platform-node-shared/src/*.js" + ], + "@effect/printer": ["../../../printer/src/index.js"], + "@effect/printer/*": ["../../../printer/src/*.js"], + "@effect/printer-ansi": ["../../../printer-ansi/src/index.js"], + "@effect/printer-ansi/*": ["../../../printer-ansi/src/*.js"], + "@effect/typeclass": ["../../../typeclass/src/index.js"], + "@effect/typeclass/*": ["../../../typeclass/src/*.js"], + "@effect/cli": ["../../../cli/src/index.js"], + "@effect/cli/*": ["../../../cli/src/*.js"] + } + } +} diff --git a/repos/effect/packages/cli/examples/minigit.ts b/repos/effect/packages/cli/examples/minigit.ts new file mode 100644 index 0000000..2dd73d9 --- /dev/null +++ b/repos/effect/packages/cli/examples/minigit.ts @@ -0,0 +1,70 @@ +import { Args, Command, Options } from "@effect/cli" +import { NodeContext, NodeRuntime } from "@effect/platform-node" +import { Array, Config, ConfigProvider, Console, Effect, Option } from "effect" + +// minigit [--version] [-h | --help] [-c =] +const configs = Options.keyValueMap("c").pipe(Options.optional) +const minigit = Command.make("minigit", { configs }, ({ configs }) => + Option.match(configs, { + onNone: () => Console.log("Running 'minigit'"), + onSome: (configs) => { + const keyValuePairs = Array.fromIterable(configs) + .map(([key, value]) => `${key}=${value}`) + .join(", ") + return Console.log(`Running 'minigit' with the following configs: ${keyValuePairs}`) + } + })) + +// minigit add [-v | --verbose] [--] [...] +const pathspec = Args.text({ name: "pathspec" }).pipe(Args.repeated) +const verbose = Options.boolean("verbose").pipe( + Options.withAlias("v"), + Options.withFallbackConfig(Config.boolean("VERBOSE")) +) +const minigitAdd = Command.make("add", { pathspec, verbose }, ({ pathspec, verbose }) => { + const paths = Array.match(pathspec, { + onEmpty: () => "", + onNonEmpty: (paths) => ` ${Array.join(paths, " ")}` + }) + return Console.log(`Running 'minigit add${paths}' with '--verbose ${verbose}'`) +}) + +// minigit clone [--depth ] [--] [] +const repository = Args.text({ name: "repository" }) +const directory = Args.directory().pipe(Args.optional) +const depth = Options.integer("depth").pipe( + Options.withFallbackConfig(Config.integer("DEPTH")), + Options.optional +) +const minigitClone = Command.make( + "clone", + { repository, directory, depth }, + (subcommandConfig) => + Effect.flatMap(minigit, (parentConfig) => { + const depth = Option.map(subcommandConfig.depth, (depth) => `--depth ${depth}`) + const repository = Option.some(subcommandConfig.repository) + const optionsAndArgs = Array.getSomes([depth, repository, subcommandConfig.directory]) + const configs = Option.match(parentConfig.configs, { + onNone: () => "", + onSome: (map) => Array.fromIterable(map).map(([key, value]) => `${key}=${value}`).join(", ") + }) + return Console.log( + "Running 'minigit clone' with the following options and arguments: " + + `'${Array.join(optionsAndArgs, ", ")}'\n` + + `and the following configuration parameters: ${configs}` + ) + }) +) + +const command = minigit.pipe(Command.withSubcommands([minigitAdd, minigitClone])) + +const cli = Command.run(command, { + name: "Minigit Distributed Version Control", + version: "v1.0.0" +}) + +Effect.suspend(() => cli(process.argv)).pipe( + Effect.withConfigProvider(ConfigProvider.nested(ConfigProvider.fromEnv(), "GIT")), + Effect.provide(NodeContext.layer), + NodeRuntime.runMain +) diff --git a/repos/effect/packages/cli/examples/naval-fate.ts b/repos/effect/packages/cli/examples/naval-fate.ts new file mode 100644 index 0000000..d7dba9d --- /dev/null +++ b/repos/effect/packages/cli/examples/naval-fate.ts @@ -0,0 +1,127 @@ +import { Args, CliConfig, Command, Options } from "@effect/cli" +import { NodeContext, NodeKeyValueStore, NodeRuntime } from "@effect/platform-node" +import * as Console from "effect/Console" +import * as Effect from "effect/Effect" +import * as Layer from "effect/Layer" +import * as NavalFateStore from "./naval-fate/store.js" + +const { createShip, moveShip, removeMine, setMine, shoot } = Effect.serviceFunctions( + NavalFateStore.NavalFateStore +) + +// naval_fate [-h | --help] [--version] +// naval_fate ship new ... +// naval_fate ship move [--speed=] +// naval_fate ship shoot +// naval_fate mine set [--moored] +// naval_fate mine remove [--moored] + +const nameArg = Args.text({ name: "name" }).pipe(Args.withDescription("The name of the ship")) +const xArg = Args.integer({ name: "x" }).pipe(Args.withDescription("The x coordinate")) +const yArg = Args.integer({ name: "y" }).pipe(Args.withDescription("The y coordinate")) +const coordinatesArg = { x: xArg, y: yArg } +const nameAndCoordinatesArg = { name: nameArg, ...coordinatesArg } + +const mooredOption = Options.boolean("moored").pipe( + Options.withDescription("Whether the mine is moored (anchored) or drifting") +) +const speedOption = Options.integer("speed").pipe( + Options.withDescription("Speed in knots"), + Options.withDefault(10) +) + +const shipCommand = Command.make("ship", { + verbose: Options.boolean("verbose") +}).pipe(Command.withDescription("Controls a ship in Naval Fate")) + +const newShipCommand = Command.make("new", { + name: nameArg +}, ({ name }) => + Effect.gen(function*() { + const { verbose } = yield* shipCommand + yield* createShip(name) + yield* Console.log(`Created ship: '${name}'`) + if (verbose) { + yield* Console.log(`Verbose mode enabled`) + } + })).pipe(Command.withDescription("Create a new ship")) + +const moveShipCommand = Command.make("move", { + ...nameAndCoordinatesArg, + speed: speedOption +}, ({ name, speed, x, y }) => + Effect.gen(function*() { + yield* moveShip(name, x, y) + yield* Console.log(`Moving ship '${name}' to coordinates (${x}, ${y}) at ${speed} knots`) + })).pipe(Command.withDescription("Move a ship")) + +const shootShipCommand = Command.make( + "shoot", + { ...coordinatesArg }, + ({ x, y }) => + Effect.gen(function*() { + yield* shoot(x, y) + yield* Console.log(`Shot cannons at coordinates (${x}, ${y})`) + }) +).pipe(Command.withDescription("Shoot from a ship")) + +const mineCommand = Command.make("mine").pipe( + Command.withDescription("Controls mines in Naval Fate") +) + +const setMineCommand = Command.make("set", { + ...coordinatesArg, + moored: mooredOption +}, ({ moored, x, y }) => + Effect.gen(function*() { + yield* setMine(x, y) + yield* Console.log(`Set ${moored ? "moored" : "drifting"} mine at coordinates (${x}, ${y})`) + })).pipe(Command.withDescription("Set a mine at specific coordinates")) + +const removeMineCommand = Command.make("remove", { + ...coordinatesArg +}, ({ x, y }) => + Effect.gen(function*() { + yield* removeMine(x, y) + yield* Console.log(`Removing mine at coordinates (${x}, ${y}), if present`) + })).pipe(Command.withDescription("Remove a mine at specific coordinates")) + +const command = Command.make("naval_fate").pipe( + Command.withDescription("An implementation of the Naval Fate CLI application."), + Command.withSubcommands([ + shipCommand.pipe(Command.withSubcommands([ + newShipCommand, + moveShipCommand, + shootShipCommand + ])), + mineCommand.pipe(Command.withSubcommands([ + setMineCommand, + removeMineCommand + ])) + ]) +) + +const ConfigLive = CliConfig.layer({ + showBuiltIns: false +}) + +const NavalFateLive = NavalFateStore.layer.pipe( + Layer.provide(NodeKeyValueStore.layerFileSystem("naval-fate-store")) +) + +const MainLayer = Layer.mergeAll( + ConfigLive, + NavalFateLive, + NodeContext.layer +) + +const cli = Command.run(command, { + name: "Naval Fate", + version: "1.0.0" +}) + +Effect.suspend(() => cli(process.argv)).pipe( + Effect.provide(MainLayer), + Effect.tapErrorCause(Effect.logError), + NodeRuntime.runMain +) diff --git a/repos/effect/packages/cli/examples/naval-fate/domain.ts b/repos/effect/packages/cli/examples/naval-fate/domain.ts new file mode 100644 index 0000000..7cec03f --- /dev/null +++ b/repos/effect/packages/cli/examples/naval-fate/domain.ts @@ -0,0 +1,80 @@ +import * as Data from "effect/Data" +import * as Schema from "effect/Schema" + +/** + * An error that occurs when attempting to create a Naval Fate ship that already + * exists. + */ +export class ShipExistsError extends Data.TaggedError("ShipExistsError")<{ + readonly name: string +}> { + toString(): string { + return `ShipExistsError: ship with name '${this.name}' already exists` + } +} + +/** + * An error that occurs when attempting to move a Naval Fate ship that does not + * exist. + */ +export class ShipNotFoundError extends Data.TaggedError("ShipNotFoundError")<{ + readonly name: string + readonly x: number + readonly y: number +}> { + toString(): string { + return `ShipNotFoundError: ship with name '${this.name}' does not exist` + } +} + +/** + * An error that occurs when attempting to move a Naval Fate ship to coordinates + * already occupied by another ship. + */ +export class CoordinatesOccupiedError extends Data.TaggedError("CoordinatesOccupiedError")<{ + readonly name: string + readonly x: number + readonly y: number +}> { + toString(): string { + return `CoordinatesOccupiedError: ship with name '${this.name}' already occupies coordinates (${this.x}, ${this.y})` + } +} + +/** + * Represents a Naval Fate ship. + */ +export class Ship extends Schema.Class("Ship")({ + name: Schema.String, + x: Schema.Number, + y: Schema.Number, + status: Schema.Literal("sailing", "destroyed") +}) { + static readonly create = (name: string) => new Ship({ name, x: 0, y: 0, status: "sailing" }) + + hasCoordinates(x: number, y: number): boolean { + return this.x === x && this.y === y + } + + move(x: number, y: number): Ship { + return new Ship({ name: this.name, x, y, status: this.status }) + } + + destroy(): Ship { + return new Ship({ name: this.name, x: this.x, y: this.y, status: "destroyed" }) + } +} + +/** + * Represents a Naval Fate mine. + */ +export class Mine extends Schema.Class("Mine")({ + x: Schema.Number, + y: Schema.Number +}) { + static readonly create = (x: number, y: number) => new Mine({ x, y }) + + hasCoordinates(x: number, y: number): boolean { + return this.x === x && this.y === y + } +} diff --git a/repos/effect/packages/cli/examples/naval-fate/store.ts b/repos/effect/packages/cli/examples/naval-fate/store.ts new file mode 100644 index 0000000..f043a91 --- /dev/null +++ b/repos/effect/packages/cli/examples/naval-fate/store.ts @@ -0,0 +1,132 @@ +import * as KeyValueStore from "@effect/platform/KeyValueStore" +import * as Arr from "effect/Array" +import * as Context from "effect/Context" +import * as Effect from "effect/Effect" +import { pipe } from "effect/Function" +import * as Layer from "effect/Layer" +import * as Option from "effect/Option" +import * as Schema from "effect/Schema" +import { CoordinatesOccupiedError, Mine, Ship, ShipExistsError, ShipNotFoundError } from "./domain.js" + +/** + * Represents the storage layer for the Naval Fate command-line application. + */ +export interface NavalFateStore { + createShip(name: string): Effect.Effect + moveShip( + name: string, + x: number, + y: number + ): Effect.Effect + shoot(x: number, y: number): Effect.Effect + setMine(x: number, y: number): Effect.Effect + removeMine(x: number, y: number): Effect.Effect +} + +export const NavalFateStore = Context.GenericTag("NavalFateStore") + +export const make = Effect.gen(function*() { + const shipsStore = yield* Effect.map( + KeyValueStore.KeyValueStore, + (store) => store.forSchema(Schema.ReadonlyMap({ key: Schema.String, value: Ship })) + ) + const minesStore = yield* Effect.map( + KeyValueStore.KeyValueStore, + (store) => store.forSchema(Schema.Array(Mine)) + ) + + const getShips = shipsStore.get("ships").pipe( + Effect.map(Option.getOrElse>(() => new Map())), + Effect.orDie + ) + const getMines = minesStore.get("mines").pipe( + Effect.map(Option.getOrElse>(() => [])), + Effect.orDie + ) + const setShips = (ships: ReadonlyMap) => shipsStore.set("ships", ships).pipe(Effect.orDie) + const setMines = (mines: ReadonlyArray) => minesStore.set("mines", mines).pipe(Effect.orDie) + + const createShip: NavalFateStore["createShip"] = (name) => + Effect.gen(function*() { + const oldShips = yield* getShips + const foundShip = Option.fromNullable(oldShips.get(name)) + if (Option.isSome(foundShip)) { + return yield* Effect.fail(new ShipExistsError({ name })) + } + const ship = Ship.create(name) + const newShips = new Map(oldShips).set(name, ship) + yield* setShips(newShips) + return ship + }) + + const moveShip: NavalFateStore["moveShip"] = (name, x, y) => + Effect.gen(function*() { + const oldShips = yield* getShips + const foundShip = Option.fromNullable(oldShips.get(name)) + if (Option.isNone(foundShip)) { + return yield* Effect.fail(new ShipNotFoundError({ name, x, y })) + } + const shipAtCoords = pipe( + Arr.fromIterable(oldShips.values()), + Arr.findFirst((ship) => ship.hasCoordinates(x, y)) + ) + if (Option.isSome(shipAtCoords)) { + return yield* Effect.fail( + new CoordinatesOccupiedError({ name: shipAtCoords.value.name, x, y }) + ) + } + const mines = yield* getMines + const mineAtCoords = Arr.findFirst(mines, (mine) => mine.hasCoordinates(x, y)) + const ship = Option.isSome(mineAtCoords) + ? foundShip.value.move(x, y).destroy() + : foundShip.value.move(x, y) + const newShips = new Map(oldShips).set(name, ship) + yield* setShips(newShips) + return ship + }) + + const shoot: NavalFateStore["shoot"] = (x, y) => + Effect.gen(function*() { + const oldShips = yield* getShips + const shipAtCoords = pipe( + Arr.fromIterable(oldShips.values()), + Arr.findFirst((ship) => ship.hasCoordinates(x, y)) + ) + if (Option.isSome(shipAtCoords)) { + const ship = shipAtCoords.value.destroy() + const newShips = new Map(oldShips).set(ship.name, ship) + yield* setShips(newShips) + } + }) + + const setMine: NavalFateStore["setMine"] = (x, y) => + Effect.gen(function*() { + const mines = yield* getMines + const mineAtCoords = Arr.findFirst(mines, (mine) => mine.hasCoordinates(x, y)) + if (Option.isNone(mineAtCoords)) { + const mine = Mine.create(x, y) + const newMines = Arr.append(mines, mine) + yield* setMines(newMines) + } + }) + + const removeMine: NavalFateStore["removeMine"] = (x, y) => + Effect.gen(function*() { + const mines = yield* getMines + const mineAtCoords = Arr.findFirstIndex(mines, (mine) => mine.hasCoordinates(x, y)) + if (Option.isSome(mineAtCoords)) { + const newMines = Arr.remove(mines, mineAtCoords.value) + yield* setMines(newMines) + } + }) + + return NavalFateStore.of({ + createShip, + moveShip, + shoot, + setMine, + removeMine + }) +}) + +export const layer = Layer.effect(NavalFateStore, make) diff --git a/repos/effect/packages/cli/examples/prompt.ts b/repos/effect/packages/cli/examples/prompt.ts new file mode 100644 index 0000000..fdac8be --- /dev/null +++ b/repos/effect/packages/cli/examples/prompt.ts @@ -0,0 +1,71 @@ +import * as Command from "@effect/cli/Command" +import * as Prompt from "@effect/cli/Prompt" +import * as NodeContext from "@effect/platform-node/NodeContext" +import * as Runtime from "@effect/platform-node/NodeRuntime" +import * as Effect from "effect/Effect" + +const colorPrompt = Prompt.select({ + message: "Pick your favorite color", + choices: [ + { + title: "Red", + value: "#ff0000", + description: "This option has a description" + }, + { title: "Green", value: "#00ff00", description: "So does this one" }, + { title: "Blue", value: "#0000ff", disabled: true } + ] +}) + +const confirmPrompt = Prompt.confirm({ + message: "Can you please confirm?" +}) + +const datePrompt = Prompt.date({ + message: "What's your birth day?", + dateMask: "\"Year:\" YYYY, \"Month:\" MM, \"Day:\" DD \\\\\\\\||// \\Hour: HH, \\Minute: mm, \"Seconds:\" ss", + validate: (date) => + date.getTime() > Date.now() + ? Effect.fail("Your birth day can't be in the future") + : Effect.succeed(date) +}) + +const numberPrompt = Prompt.float({ + message: `What is your favorite number?`, + validate: (n) => n > 0 ? Effect.succeed(n) : Effect.fail("must be greater than 0") +}) + +const passwordPrompt = Prompt.password({ + message: "Enter your password: ", + validate: (value) => + value.length === 0 + ? Effect.fail("Password cannot be empty") + : Effect.succeed(value) +}) + +const togglePrompt = Prompt.toggle({ + message: "Yes or no?", + active: "yes", + inactive: "no" +}) + +const prompt = Prompt.all([ + colorPrompt, + confirmPrompt, + datePrompt, + numberPrompt, + passwordPrompt, + togglePrompt +]) + +const command = Command.prompt("favorites", prompt, Effect.log) + +const cli = Command.run(command, { + name: "Prompt Examples", + version: "0.0.1" +}) + +Effect.suspend(() => cli(process.argv)).pipe( + Effect.provide(NodeContext.layer), + Runtime.runMain +) diff --git a/repos/effect/packages/cli/package.json b/repos/effect/packages/cli/package.json new file mode 100644 index 0000000..939a961 --- /dev/null +++ b/repos/effect/packages/cli/package.json @@ -0,0 +1,76 @@ +{ + "name": "@effect/cli", + "version": "0.75.1", + "type": "module", + "license": "MIT", + "description": "A library for building command-line interfaces with Effect", + "homepage": "https://effect.website", + "repository": { + "type": "git", + "url": "https://github.com/Effect-TS/effect.git", + "directory": "packages/cli" + }, + "bugs": { + "url": "https://github.com/Effect-TS/effect/issues" + }, + "tags": [ + "cli", + "typescript", + "algebraic-data-types", + "functional-programming" + ], + "keywords": [ + "cli", + "typescript", + "algebraic-data-types", + "functional-programming" + ], + "publishConfig": { + "access": "public", + "provenance": true, + "directory": "dist", + "linkDirectory": false + }, + "exports": { + "./package.json": "./package.json", + ".": "./src/index.ts", + "./*": "./src/*.ts", + "./internal/*": null + }, + "scripts": { + "codegen": "build-utils prepare-v3", + "build": "pnpm build-esm && pnpm build-annotate && pnpm build-cjs && build-utils pack-v3", + "build-esm": "tsc -b tsconfig.build.json", + "build-cjs": "babel build/esm --plugins @babel/transform-export-namespace-from --plugins @babel/transform-modules-commonjs --out-dir build/cjs --source-maps", + "build-annotate": "babel build/esm --plugins annotate-pure-calls --out-dir build/esm --source-maps", + "check": "tsc -b tsconfig.json", + "test": "vitest", + "coverage": "vitest --coverage" + }, + "peerDependencies": { + "@effect/platform": "workspace:^", + "@effect/printer": "workspace:^", + "@effect/printer-ansi": "workspace:^", + "effect": "workspace:^" + }, + "devDependencies": { + "@effect/platform": "workspace:^", + "@effect/platform-node": "workspace:^", + "@effect/printer": "workspace:^", + "@effect/printer-ansi": "workspace:^", + "@types/ini": "^4.1.1", + "effect": "workspace:^" + }, + "dependencies": { + "ini": "^4.1.3", + "toml": "^3.0.0", + "yaml": "^2.5.0" + }, + "effect": { + "generateIndex": { + "include": [ + "**/*" + ] + } + } +} diff --git a/repos/effect/packages/cli/src/Args.ts b/repos/effect/packages/cli/src/Args.ts new file mode 100644 index 0000000..28b8865 --- /dev/null +++ b/repos/effect/packages/cli/src/Args.ts @@ -0,0 +1,492 @@ +/** + * @since 1.0.0 + */ +import type { FileSystem } from "@effect/platform/FileSystem" +import type { Path } from "@effect/platform/Path" +import type { QuitException, Terminal } from "@effect/platform/Terminal" +import type { NonEmptyArray } from "effect/Array" +import type { Config } from "effect/Config" +import type { Effect } from "effect/Effect" +import type { Option } from "effect/Option" +import type { Pipeable } from "effect/Pipeable" +import type { Redacted } from "effect/Redacted" +import type { Schema } from "effect/Schema" +import type { Secret } from "effect/Secret" +import type { CliConfig } from "./CliConfig.js" +import type { HelpDoc } from "./HelpDoc.js" +import * as InternalArgs from "./internal/args.js" +import type { Primitive } from "./Primitive.js" +import type { Usage } from "./Usage.js" +import type { ValidationError } from "./ValidationError.js" + +/** + * @since 1.0.0 + * @category symbols + */ +export const ArgsTypeId: unique symbol = InternalArgs.ArgsTypeId + +/** + * @since 1.0.0 + * @category symbols + */ +export type ArgsTypeId = typeof ArgsTypeId + +/** + * Represents arguments that can be passed to a command-line application. + * + * @since 1.0.0 + * @category models + */ +export interface Args extends Args.Variance, Pipeable {} + +/** + * @since 1.0.0 + */ +export declare namespace Args { + /** + * @since 1.0.0 + * @category models + */ + export interface Variance { + readonly [ArgsTypeId]: { + readonly _A: (_: never) => A + } + } + + /** + * @since 1.0.0 + * @category models + */ + export interface BaseArgsConfig { + readonly name?: string + } + + /** + * @since 1.0.0 + * @category models + */ + export interface PathArgsConfig extends BaseArgsConfig { + readonly exists?: Primitive.PathExists + } + + /** + * @since 1.0.0 + * @category models + */ + export interface FormatArgsConfig extends BaseArgsConfig { + readonly format?: "json" | "yaml" | "ini" | "toml" + } +} + +/** + * @since 1.0.0 + */ +export declare namespace All { + /** + * @since 1.0.0 + */ + export type ArgsAny = Args + + /** + * @since 1.0.0 + */ + export type ReturnIterable> = [T] extends [Iterable>] ? + Args> + : never + + /** + * @since 1.0.0 + */ + export type ReturnTuple> = Args< + T[number] extends never ? [] + : { + -readonly [K in keyof T]: [T[K]] extends [Args.Variance] ? _A : never + } + > extends infer X ? X : never + + /** + * @since 1.0.0 + */ + export type ReturnObject = [T] extends [{ [K: string]: ArgsAny }] ? Args< + { + -readonly [K in keyof T]: [T[K]] extends [Args.Variance] ? _A : never + } + > + : never + + /** + * @since 1.0.0 + */ + export type Return< + Arg extends Iterable | Record + > = [Arg] extends [ReadonlyArray] ? ReturnTuple + : [Arg] extends [Iterable] ? ReturnIterable + : [Arg] extends [Record] ? ReturnObject + : never +} + +/** + * @since 1.0.0 + * @category refinements + */ +export const isArgs: (u: unknown) => u is Args = InternalArgs.isArgs + +/** + * @since 1.0.0 + * @category constructors + */ +export const all: > | Record>>( + arg: Arg +) => All.Return = InternalArgs.all + +/** + * @since 1.0.0 + * @category combinators + */ +export const atLeast: { + (times: 0): (self: Args) => Args> + (times: number): (self: Args) => Args> + (self: Args, times: 0): Args> + (self: Args, times: number): Args> +} = InternalArgs.atLeast + +/** + * @since 1.0.0 + * @category combinators + */ +export const atMost: { + (times: number): (self: Args) => Args> + (self: Args, times: number): Args> +} = InternalArgs.atMost + +/** + * @since 1.0.0 + * @category combinators + */ +export const between: { + (min: 0, max: number): (self: Args) => Args> + (min: number, max: number): (self: Args) => Args> + (self: Args, min: 0, max: number): Args> + (self: Args, min: number, max: number): Args> +} = InternalArgs.between + +/** + * Creates a boolean argument. + * + * Can optionally provide a custom argument name (defaults to `"boolean"`). + * + * @since 1.0.0 + * @category constructors + */ +export const boolean: (options?: Args.BaseArgsConfig) => Args = InternalArgs.boolean + +/** + * Creates a choice argument. + * + * Can optionally provide a custom argument name (defaults to `"choice"`). + * + * @since 1.0.0 + * @category constructors + */ +export const choice: ( + choices: ReadonlyArray<[string, A]>, + config?: Args.BaseArgsConfig +) => Args = InternalArgs.choice + +/** + * Creates a date argument. + * + * Can optionally provide a custom argument name (defaults to `"date"`). + * + * @since 1.0.0 + * @category constructors + */ +export const date: (config?: Args.BaseArgsConfig) => Args = InternalArgs.date + +/** + * Creates a directory argument. + * + * Can optionally provide a custom argument name (defaults to `"directory"`). + * + * @since 1.0.0 + * @category constructors + */ +export const directory: (config?: Args.PathArgsConfig) => Args = InternalArgs.directory + +/** + * Creates a file argument. + * + * Can optionally provide a custom argument name (defaults to `"file"`). + * + * @since 1.0.0 + * @category constructors + */ +export const file: (config?: Args.PathArgsConfig) => Args = InternalArgs.file + +/** + * Creates a file argument that reads its contents. + * + * Can optionally provide a custom argument name (defaults to `"file"`). + * + * @since 1.0.0 + * @category constructors + */ +export const fileContent: ( + config?: Args.BaseArgsConfig | undefined +) => Args = InternalArgs.fileContent + +/** + * Creates a file argument that reads and parses its contents. + * + * Can optionally provide a custom argument name (defaults to `"file"`). + * + * @since 1.0.0 + * @category constructors + */ +export const fileParse: (config?: Args.FormatArgsConfig | undefined) => Args = InternalArgs.fileParse + +/** + * Creates a file argument that reads, parses and validates its contents. + * + * Can optionally provide a custom argument name (defaults to `"file"`). + * + * @since 1.0.0 + * @category constructors + */ +export const fileSchema: ( + schema: Schema, + config?: Args.FormatArgsConfig | undefined +) => Args = InternalArgs.fileSchema + +/** + * Creates a file argument that reads it's contents. + * + * Can optionally provide a custom argument name (defaults to `"file"`). + * + * @since 1.0.0 + * @category constructors + */ +export const fileText: ( + config?: Args.BaseArgsConfig | undefined +) => Args = InternalArgs.fileText + +/** + * Creates a floating point number argument. + * + * Can optionally provide a custom argument name (defaults to `"float"`). + * + * @since 1.0.0 + * @category constructors + */ +export const float: (config?: Args.BaseArgsConfig) => Args = InternalArgs.float + +/** + * @since 1.0.0 + * @category combinators + */ +export const getHelp: (self: Args) => HelpDoc = InternalArgs.getHelp + +/** + * @since 1.0.0 + * @category combinators + */ +export const getIdentifier: (self: Args) => Option = InternalArgs.getIdentifier + +/** + * @since 1.0.0 + * @category combinators + */ +export const getMinSize: (self: Args) => number = InternalArgs.getMinSize + +/** + * @since 1.0.0 + * @category combinators + */ +export const getMaxSize: (self: Args) => number = InternalArgs.getMaxSize + +/** + * @since 1.0.0 + * @category combinators + */ +export const getUsage: (self: Args) => Usage = InternalArgs.getUsage + +/** + * Creates an integer argument. + * + * Can optionally provide a custom argument name (defaults to `"integer"`). + * + * @since 1.0.0 + * @category constructors + */ +export const integer: (config?: Args.BaseArgsConfig) => Args = InternalArgs.integer + +/** + * @since 1.0.0 + * @category mapping + */ +export const map: { + (f: (a: A) => B): (self: Args) => Args + (self: Args, f: (a: A) => B): Args +} = InternalArgs.map + +/** + * @since 1.0.0 + * @category mapping + */ +export const mapEffect: { + (f: (a: A) => Effect): (self: Args) => Args + (self: Args, f: (a: A) => Effect): Args +} = InternalArgs.mapEffect + +/** + * @since 1.0.0 + * @category mapping + */ +export const mapTryCatch: { + (f: (a: A) => B, onError: (e: unknown) => HelpDoc): (self: Args) => Args + (self: Args, f: (a: A) => B, onError: (e: unknown) => HelpDoc): Args +} = InternalArgs.mapTryCatch + +/** + * @since 1.0.0 + * @category combinators + */ +export const optional: (self: Args) => Args> = InternalArgs.optional + +/** + * Creates an empty argument. + * + * @since 1.0.0 + * @category constructors + */ +export const none: Args = InternalArgs.none + +/** + * Creates a path argument. + * + * Can optionally provide a custom argument name (defaults to `"path"`). + * + * @since 1.0.0 + * @category constructors + */ +export const path: (config?: Args.PathArgsConfig) => Args = InternalArgs.path + +/** + * @since 1.0.0 + * @category combinators + */ +export const repeated: (self: Args) => Args> = InternalArgs.repeated + +/** + * Creates a text argument. + * + * Can optionally provide a custom argument name (defaults to `"redacted"`). + * + * @since 1.0.0 + * @category constructors + */ +export const redacted: (config?: Args.BaseArgsConfig) => Args = InternalArgs.redacted + +/** + * Creates a text argument. + * + * Can optionally provide a custom argument name (defaults to `"secret"`). + * + * @since 1.0.0 + * @category constructors + */ +export const secret: (config?: Args.BaseArgsConfig) => Args = InternalArgs.secret + +/** + * Creates a text argument. + * + * Can optionally provide a custom argument name (defaults to `"text"`). + * + * @since 1.0.0 + * @category constructors + */ +export const text: (config?: Args.BaseArgsConfig) => Args = InternalArgs.text + +/** + * @since 1.0.0 + * @category combinators + */ +export const validate: { + ( + args: ReadonlyArray, + config: CliConfig + ): (self: Args) => Effect< + [Array, A], + ValidationError, + FileSystem | Path | Terminal + > + ( + self: Args, + args: ReadonlyArray, + config: CliConfig + ): Effect< + [Array, A], + ValidationError, + FileSystem | Path | Terminal + > +} = InternalArgs.validate + +/** + * @since 1.0.0 + * @category combinators + */ +export const withDefault: { + (fallback: B): (self: Args) => Args + (self: Args, fallback: B): Args +} = InternalArgs.withDefault + +/** + * @since 1.0.0 + * @category combinators + */ +export const withFallbackConfig: { + (config: Config): (self: Args) => Args + (self: Args, config: Config): Args +} = InternalArgs.withFallbackConfig + +/** + * @since 1.0.0 + * @category combinators + */ +export const withDescription: { + (description: string): (self: Args) => Args + (self: Args, description: string): Args +} = InternalArgs.withDescription + +/** + * @since 1.0.0 + * @category combinators + */ +export const withSchema: { + (schema: Schema): (self: Args) => Args + (self: Args, schema: Schema): Args +} = InternalArgs.withSchema + +/** + * @since 1.0.0 + * @category combinators + */ +export const wizard: { + ( + config: CliConfig + ): ( + self: Args + ) => Effect< + Array, + ValidationError | QuitException, + FileSystem | Path | Terminal + > + ( + self: Args, + config: CliConfig + ): Effect< + Array, + ValidationError | QuitException, + FileSystem | Path | Terminal + > +} = InternalArgs.wizard diff --git a/repos/effect/packages/cli/src/AutoCorrect.ts b/repos/effect/packages/cli/src/AutoCorrect.ts new file mode 100644 index 0000000..f182dbd --- /dev/null +++ b/repos/effect/packages/cli/src/AutoCorrect.ts @@ -0,0 +1,13 @@ +/** + * @since 1.0.0 + */ + +import type { CliConfig } from "./CliConfig.js" +import * as InternalAutoCorrect from "./internal/autoCorrect.js" + +/** + * @since 1.0.0 + * @category utilities + */ +export const levensteinDistance: (first: string, second: string, config: CliConfig) => number = + InternalAutoCorrect.levensteinDistance diff --git a/repos/effect/packages/cli/src/BuiltInOptions.ts b/repos/effect/packages/cli/src/BuiltInOptions.ts new file mode 100644 index 0000000..c18fecc --- /dev/null +++ b/repos/effect/packages/cli/src/BuiltInOptions.ts @@ -0,0 +1,138 @@ +/** + * @since 1.0.0 + */ + +import type { LogLevel } from "effect/LogLevel" +import type { Option } from "effect/Option" +import type { Command } from "./CommandDescriptor.js" +import type { HelpDoc } from "./HelpDoc.js" +import * as InternalBuiltInOptions from "./internal/builtInOptions.js" +import type { Options } from "./Options.js" +import type { Usage } from "./Usage.js" + +/** + * @since 1.0.0 + * @category models + */ +export type BuiltInOptions = + | SetLogLevel + | ShowHelp + | ShowCompletions + | ShowWizard + | ShowVersion + +/** + * @since 1.0.0 + * @category models + */ +export interface SetLogLevel { + readonly _tag: "SetLogLevel" + readonly level: LogLevel +} + +/** + * @since 1.0.0 + * @category models + */ +export interface ShowHelp { + readonly _tag: "ShowHelp" + readonly usage: Usage + readonly helpDoc: HelpDoc +} + +/** + * @since 1.0.0 + * @category models + */ +export interface ShowCompletions { + readonly _tag: "ShowCompletions" + readonly shellType: BuiltInOptions.ShellType +} + +/** + * @since 1.0.0 + * @category models + */ +export interface ShowWizard { + readonly _tag: "ShowWizard" + readonly command: Command +} + +/** + * @since 1.0.0 + * @category models + */ +export interface ShowVersion { + readonly _tag: "ShowVersion" +} + +/** + * @since 1.0.0 + */ +export declare namespace BuiltInOptions { + /** + * @since 1.0.0 + * @category models + */ + export type ShellType = "bash" | "fish" | "zsh" +} + +/** + * @since 1.0.0 + * @category options + */ +export const builtInOptions: ( + command: Command, + usage: Usage, + helpDoc: HelpDoc +) => Options> = InternalBuiltInOptions.builtInOptions + +/** + * @since 1.0.0 + * @category refinements + */ +export const isShowCompletions: (self: BuiltInOptions) => self is ShowCompletions = + InternalBuiltInOptions.isShowCompletions + +/** + * @since 1.0.0 + * @category refinements + */ +export const isShowHelp: (self: BuiltInOptions) => self is ShowHelp = InternalBuiltInOptions.isShowHelp + +/** + * @since 1.0.0 + * @category refinements + */ +export const isShowWizard: (self: BuiltInOptions) => self is ShowWizard = InternalBuiltInOptions.isShowWizard + +/** + * @since 1.0.0 + * @category refinements + */ +export const isShowVersion: (self: BuiltInOptions) => self is ShowVersion = InternalBuiltInOptions.isShowVersion + +/** + * @since 1.0.0 + * @category constructors + */ +export const showCompletions: (shellType: BuiltInOptions.ShellType) => BuiltInOptions = + InternalBuiltInOptions.showCompletions + +/** + * @since 1.0.0 + * @category constructors + */ +export const showHelp: (usage: Usage, helpDoc: HelpDoc) => BuiltInOptions = InternalBuiltInOptions.showHelp + +/** + * @since 1.0.0 + * @category constructors + */ +export const showWizard: (command: Command) => BuiltInOptions = InternalBuiltInOptions.showWizard + +/** + * @since 1.0.0 + * @category constructors + */ +export const showVersion: BuiltInOptions = InternalBuiltInOptions.showVersion diff --git a/repos/effect/packages/cli/src/CliApp.ts b/repos/effect/packages/cli/src/CliApp.ts new file mode 100644 index 0000000..44ec7f9 --- /dev/null +++ b/repos/effect/packages/cli/src/CliApp.ts @@ -0,0 +1,74 @@ +/** + * @since 1.0.0 + */ +import type { FileSystem } from "@effect/platform/FileSystem" +import type { Path } from "@effect/platform/Path" +import type { Terminal } from "@effect/platform/Terminal" +import type { Effect } from "effect/Effect" +import type { Pipeable } from "effect/Pipeable" +import type { Command } from "./CommandDescriptor.js" +import type { HelpDoc } from "./HelpDoc.js" +import type { Span } from "./HelpDoc/Span.js" +import * as InternalCliApp from "./internal/cliApp.js" +import type { ValidationError } from "./ValidationError.js" + +/** + * A `CliApp` is a complete description of a command-line application. + * + * @since 1.0.0 + * @category models + */ +export interface CliApp extends Pipeable { + readonly name: string + readonly version: string + readonly executable: string + readonly command: Command + readonly summary: Span + readonly footer: HelpDoc +} + +/** + * @since 1.0.0 + */ +export declare namespace CliApp { + /** + * @since 1.0.0 + * @category models + */ + export type Environment = FileSystem | Path | Terminal + + /** + * @since 1.0.0 + * @category models + */ + export interface ConstructorArgs { + readonly name: string + readonly version: string + readonly command: Command + readonly executable?: string | undefined + readonly summary?: Span | undefined + readonly footer?: HelpDoc | undefined + } +} + +/** + * @since 1.0.0 + * @category constructors + */ +export const make: (config: CliApp.ConstructorArgs) => CliApp = InternalCliApp.make + +/** + * @since 1.0.0 + * @category execution + */ +export const run: { + ( + args: ReadonlyArray, + execute: (a: A) => Effect + ): (self: CliApp) => Effect + ( + self: CliApp, + args: ReadonlyArray, + execute: (a: A) => Effect + ): Effect +} = InternalCliApp.run diff --git a/repos/effect/packages/cli/src/CliConfig.ts b/repos/effect/packages/cli/src/CliConfig.ts new file mode 100644 index 0000000..1457928 --- /dev/null +++ b/repos/effect/packages/cli/src/CliConfig.ts @@ -0,0 +1,94 @@ +/** + * @since 1.0.0 + */ +import type * as Context from "effect/Context" +import type * as Layer from "effect/Layer" +import * as InternalCliConfig from "./internal/cliConfig.js" + +/** + * Represents how arguments from the command-line are to be parsed. + * + * @since 1.0.0 + * @category models + */ +export interface CliConfig { + /** + * Whether or not the argument parser should be case sensitive. + * + * Defaults to `false`. + */ + readonly isCaseSensitive: boolean + /** + * Levenstein distance threshold for when to show auto correct suggestions. + * + * Defaults to `2`. + */ + readonly autoCorrectLimit: number + /** + * Whether or not to perform a final check of the command-line arguments for + * a built-in option, even if the provided command is not valid. + * + * Defaults to `false`. + */ + readonly finalCheckBuiltIn: boolean + /** + * Whether or not to display all the names of an option in the usage of a + * particular command. + * + * Defaults to `true`. + */ + readonly showAllNames: boolean + /** + * Whether or not to display built-in options in the help documentation + * generated for a `Command`. + * + * Defaults to `true`. + */ + readonly showBuiltIns: boolean + /** + * Whether or not to display the type of an option in the usage of a + * particular command. + * + * Defaults to `true`. + */ + readonly showTypes: boolean +} + +/** + * @since 1.0.0 + * @category context + */ +export const CliConfig: Context.Tag = InternalCliConfig.Tag + +/** + * @since 1.0.0 + * @category constructors + */ +export const defaultConfig: CliConfig = InternalCliConfig.defaultConfig + +/** + * @since 1.0.0 + * @category context + */ +export const defaultLayer: Layer.Layer = InternalCliConfig.defaultLayer + +/** + * @since 1.0.0 + * @category context + */ +export const layer: (config?: Partial) => Layer.Layer = InternalCliConfig.layer + +/** + * @since 1.0.0 + * @category constructors + */ +export const make: (params: Partial) => CliConfig = InternalCliConfig.make + +/** + * @since 1.0.0 + * @category utilities + */ +export const normalizeCase: { + (text: string): (self: CliConfig) => string + (self: CliConfig, text: string): string +} = InternalCliConfig.normalizeCase diff --git a/repos/effect/packages/cli/src/Command.ts b/repos/effect/packages/cli/src/Command.ts new file mode 100644 index 0000000..9f4d1a2 --- /dev/null +++ b/repos/effect/packages/cli/src/Command.ts @@ -0,0 +1,443 @@ +/** + * @since 1.0.0 + */ +import type { FileSystem } from "@effect/platform/FileSystem" +import type { Path } from "@effect/platform/Path" +import type { QuitException, Terminal } from "@effect/platform/Terminal" +import type { Tag } from "effect/Context" +import type { Effect } from "effect/Effect" +import type { HashMap } from "effect/HashMap" +import type { HashSet } from "effect/HashSet" +import type { Layer } from "effect/Layer" +import type { Option } from "effect/Option" +import { type Pipeable } from "effect/Pipeable" +import type * as Types from "effect/Types" +import type { Args } from "./Args.js" +import type { CliApp } from "./CliApp.js" +import type { CliConfig } from "./CliConfig.js" +import type * as Descriptor from "./CommandDescriptor.js" +import type { HelpDoc } from "./HelpDoc.js" +import * as Internal from "./internal/command.js" +import type { Options } from "./Options.js" +import type { Prompt } from "./Prompt.js" +import type { Usage } from "./Usage.js" +import type { ValidationError } from "./ValidationError.js" + +/** + * @since 1.0.0 + * @category type ids + */ +export const TypeId: unique symbol = Internal.TypeId + +/** + * @since 1.0.0 + * @category type ids + */ +export type TypeId = typeof TypeId + +/** + * @since 1.0.0 + * @category models + */ +export interface Command extends Pipeable, Effect> { + readonly [TypeId]: TypeId + readonly descriptor: Descriptor.Command + readonly handler: (_: A) => Effect + readonly tag: Tag, A> + readonly transform: Command.Transform +} + +/** + * @since 1.0.0 + * @category models + */ +export declare namespace Command { + /** + * @since 1.0.0 + * @category models + */ + export interface Context { + readonly _: unique symbol + readonly name: Name + } + + /** + * @since 1.0.0 + * @category models + */ + export interface Config { + readonly [key: string]: + | Args + | Options + | ReadonlyArray | Options | Config> + | Config + } + + /** + * @since 1.0.0 + * @category models + */ + export type ParseConfig = Types.Simplify< + { readonly [Key in keyof A]: ParseConfigValue } + > + + type ParseConfigValue = A extends ReadonlyArray ? + { readonly [Key in keyof A]: ParseConfigValue } : + A extends Args ? Value + : A extends Options ? Value + : A extends Config ? ParseConfig + : never + + interface ParsedConfigTree { + [key: string]: ParsedConfigNode + } + + type ParsedConfigNode = { + readonly _tag: "Args" + readonly index: number + } | { + readonly _tag: "Options" + readonly index: number + } | { + readonly _tag: "Array" + readonly children: ReadonlyArray + } | { + readonly _tag: "ParsedConfig" + readonly tree: ParsedConfigTree + } + + /** + * @since 1.0.0 + * @category models + */ + export interface ParsedConfig { + readonly args: ReadonlyArray> + readonly options: ReadonlyArray> + readonly tree: ParsedConfigTree + } + + /** + * @since 1.0.0 + * @category models + */ + export type Transform = (effect: Effect, config: A) => Effect +} + +/** + * @since 1.0.0 + * @category constructors + */ +export const fromDescriptor: { + (): ( + command: Descriptor.Command + ) => Command + + ( + handler: (_: A) => Effect + ): (command: Descriptor.Command) => Command + + ( + descriptor: Descriptor.Command + ): Command + + ( + descriptor: Descriptor.Command, + handler: (_: A) => Effect + ): Command +} = Internal.fromDescriptor + +/** + * @since 1.0.0 + * @category accessors + */ +export const getHelp: ( + self: Command, + config: CliConfig +) => HelpDoc = Internal.getHelp + +/** + * @since 1.0.0 + * @category accessors + */ +export const getNames: ( + self: Command +) => HashSet = Internal.getNames + +/** + * @since 1.0.0 + * @category accessors + */ +export const getBashCompletions: ( + self: Command, + programName: string +) => Effect> = Internal.getBashCompletions + +/** + * @since 1.0.0 + * @category accessors + */ +export const getFishCompletions: ( + self: Command, + programName: string +) => Effect> = Internal.getFishCompletions + +/** + * @since 1.0.0 + * @category accessors + */ +export const getZshCompletions: ( + self: Command, + programName: string +) => Effect> = Internal.getZshCompletions + +/** + * @since 1.0.0 + * @category accessors + */ +export const getSubcommands: ( + self: Command +) => HashMap> = Internal.getSubcommands + +/** + * @since 1.0.0 + * @category accessors + */ +export const getUsage: (self: Command) => Usage = Internal.getUsage + +/** + * @since 1.0.0 + * @category constructors + */ +export const make: { + (name: Name): Command< + Name, + never, + never, + {} + > + + ( + name: Name, + config: Config + ): Command< + Name, + never, + never, + Types.Simplify> + > + + ( + name: Name, + config: Config, + handler: (_: Types.Simplify>) => Effect + ): Command< + Name, + R, + E, + Types.Simplify> + > +} = Internal.make + +/** + * @since 1.0.0 + * @category constructors + */ +export const prompt: ( + name: Name, + prompt: Prompt, + handler: (_: A) => Effect +) => Command = Internal.prompt + +/** + * @since 1.0.0 + * @category combinators + */ +export const provide: { + ( + layer: Layer | ((_: A) => Layer) + ): ( + self: Command + ) => Command, LE | E, A> + ( + self: Command, + layer: Layer | ((_: A) => Layer) + ): Command, E | LE, A> +} = Internal.provide + +/** + * @since 1.0.0 + * @category combinators + */ +export const provideEffect: { + ( + tag: Tag, + effect: Effect | ((_: A) => Effect) + ): ( + self: Command + ) => Command, E2 | E, A> + ( + self: Command, + tag: Tag, + effect: Effect | ((_: A) => Effect) + ): Command, E | E2, A> +} = Internal.provideEffect + +/** + * @since 1.0.0 + * @category combinators + */ +export const provideEffectDiscard: { + ( + effect: Effect<_, E2, R2> | ((_: A) => Effect<_, E2, R2>) + ): (self: Command) => Command + ( + self: Command, + effect: Effect<_, E2, R2> | ((_: A) => Effect<_, E2, R2>) + ): Command +} = Internal.provideEffectDiscard + +/** + * @since 1.0.0 + * @category combinators + */ +export const provideSync: { + ( + tag: Tag, + service: S | ((_: A) => S) + ): (self: Command) => Command, E, A> + ( + self: Command, + tag: Tag, + service: S | ((_: A) => S) + ): Command, E, A> +} = Internal.provideSync + +/** + * @since 1.0.0 + * @category combinators + */ +export const transformHandler: { + ( + f: (effect: Effect, config: A) => Effect + ): (self: Command) => Command + ( + self: Command, + f: (effect: Effect, config: A) => Effect + ): Command +} = Internal.transformHandler + +/** + * @since 1.0.0 + * @category combinators + */ +export const withDescription: { + ( + help: string | HelpDoc + ): (self: Command) => Command + ( + self: Command, + help: string | HelpDoc + ): Command +} = Internal.withDescription + +/** + * @since 1.0.0 + * @category combinators + */ +export const withHandler: { + ( + handler: (_: A) => Effect + ): (self: Command) => Command + ( + self: Command, + handler: (_: A) => Effect + ): Command +} = Internal.withHandler + +/** + * @since 1.0.0 + * @category combinators + */ +export const withSubcommands: { + < + Subcommand extends readonly [Command, ...Array>] + >( + subcommands: Subcommand + ): ( + self: Command + ) => Command< + Name, + | R + | Exclude>, Command.Context>, + E | Effect.Error>, + Descriptor.Command.ComputeParsedType< + & A + & Readonly< + { subcommand: Option> } + > + > + > + < + Name extends string, + R, + E, + A, + Subcommand extends readonly [Command, ...Array>] + >( + self: Command, + subcommands: Subcommand + ): Command< + Name, + | R + | Exclude>, Command.Context>, + E | Effect.Error>, + Descriptor.Command.ComputeParsedType< + & A + & Readonly< + { subcommand: Option> } + > + > + > +} = Internal.withSubcommands + +/** + * @since 1.0.0 + * @category accessors + */ +export const wizard: { + ( + prefix: ReadonlyArray, + config: CliConfig + ): ( + self: Command + ) => Effect< + Array, + QuitException | ValidationError, + FileSystem | Path | Terminal + > + ( + self: Command, + prefix: ReadonlyArray, + config: CliConfig + ): Effect< + Array, + QuitException | ValidationError, + FileSystem | Path | Terminal + > +} = Internal.wizard + +/** + * @since 1.0.0 + * @category conversions + */ +export const run: { + ( + config: Omit, "command"> + ): ( + self: Command + ) => (args: ReadonlyArray) => Effect + ( + self: Command, + config: Omit, "command"> + ): (args: ReadonlyArray) => Effect +} = Internal.run diff --git a/repos/effect/packages/cli/src/CommandDescriptor.ts b/repos/effect/packages/cli/src/CommandDescriptor.ts new file mode 100644 index 0000000..ab9e626 --- /dev/null +++ b/repos/effect/packages/cli/src/CommandDescriptor.ts @@ -0,0 +1,278 @@ +/** + * @since 1.0.0 + */ +import type { FileSystem } from "@effect/platform/FileSystem" +import type { Path } from "@effect/platform/Path" +import type { QuitException, Terminal } from "@effect/platform/Terminal" +import type { NonEmptyReadonlyArray } from "effect/Array" +import type { Effect } from "effect/Effect" +import type { HashMap } from "effect/HashMap" +import type { HashSet } from "effect/HashSet" +import type { Option } from "effect/Option" +import type { Pipeable } from "effect/Pipeable" +import type { Args } from "./Args.js" +import type { CliConfig } from "./CliConfig.js" +import type { CommandDirective } from "./CommandDirective.js" +import type { HelpDoc } from "./HelpDoc.js" +import * as Internal from "./internal/commandDescriptor.js" +import type { Options } from "./Options.js" +import type { Prompt } from "./Prompt.js" +import type { Usage } from "./Usage.js" +import type { ValidationError } from "./ValidationError.js" + +/** + * @since 1.0.0 + * @category symbols + */ +export const TypeId: unique symbol = Internal.TypeId + +/** + * @since 1.0.0 + * @category symbols + */ +export type TypeId = typeof TypeId + +/** + * A `Command` represents a command in a command-line application. + * + * Every command-line application will have at least one command: the + * application itself. Other command-line applications may support multiple + * commands. + * + * @since 1.0.0 + * @category models + */ +export interface Command extends Command.Variance, Pipeable {} + +/** + * @since 1.0.0 + */ +export declare namespace Command { + /** + * @since 1.0.0 + * @category models + */ + export interface Variance { + readonly [TypeId]: { + readonly _A: (_: never) => A + } + } + + /** + * @since 1.0.0 + * @category models + */ + export type ParsedStandardCommand = Command.ComputeParsedType<{ + readonly name: Name + readonly options: OptionsType + readonly args: ArgsType + }> + + /** + * @since 1.0.0 + * @category models + */ + export type ParsedUserInputCommand = Command.ComputeParsedType<{ + readonly name: Name + readonly value: ValueType + }> + + /** + * @since 1.0.0 + * @category models + */ + export type GetParsedType = C extends Command ? P : never + + /** + * @since 1.0.0 + * @category models + */ + export type ComputeParsedType = { [K in keyof A]: A[K] } extends infer X ? X : never + + /** + * @since 1.0.0 + * @category models + */ + export type Subcommands< + A extends NonEmptyReadonlyArray]> + > = { + [I in keyof A]: A[I] extends readonly [infer Id, Command] ? readonly [id: Id, value: Value] + : never + }[number] +} + +/** + * @since 1.0.0 + * @category combinators + */ +export const getHelp: (self: Command, config: CliConfig) => HelpDoc = Internal.getHelp + +/** + * @since 1.0.0 + * @category combinators + */ +export const getBashCompletions: ( + self: Command, + programName: string +) => Effect> = Internal.getBashCompletions + +/** + * @since 1.0.0 + * @category combinators + */ +export const getFishCompletions: ( + self: Command, + programName: string +) => Effect> = Internal.getFishCompletions + +/** + * @since 1.0.0 + * @category combinators + */ +export const getZshCompletions: ( + self: Command, + programName: string +) => Effect> = Internal.getZshCompletions + +/** + * @since 1.0.0 + * @category combinators + */ +export const getNames: (self: Command) => HashSet = Internal.getNames + +/** + * @since 1.0.0 + * @category combinators + */ +export const getSubcommands: (self: Command) => HashMap> = Internal.getSubcommands + +/** + * @since 1.0.0 + * @category combinators + */ +export const getUsage: (self: Command) => Usage = Internal.getUsage + +/** + * @since 1.0.0 + * @category combinators + */ +export const map: { + (f: (a: A) => B): (self: Command) => Command + (self: Command, f: (a: A) => B): Command +} = Internal.map + +/** + * @since 1.0.0 + * @category combinators + */ +export const mapEffect: { + (f: (a: A) => Effect): (self: Command) => Command + (self: Command, f: (a: A) => Effect): Command +} = Internal.mapEffect + +/** + * @since 1.0.0 + * @category combinators + */ +export const parse: { + ( + args: ReadonlyArray, + config: CliConfig + ): ( + self: Command + ) => Effect, ValidationError, FileSystem | Path | Terminal> + ( + self: Command, + args: ReadonlyArray, + config: CliConfig + ): Effect, ValidationError, FileSystem | Path | Terminal> +} = Internal.parse + +/** + * @since 1.0.0 + * @category constructors + */ +export const prompt: ( + name: Name, + prompt: Prompt +) => Command<{ readonly name: Name; readonly value: A }> = Internal.prompt + +/** + * @since 1.0.0 + * @category constructors + */ +export const make: ( + name: Name, + options?: Options, + args?: Args +) => Command<{ readonly name: Name; readonly options: OptionsType; readonly args: ArgsType }> = Internal.make + +/** + * @since 1.0.0 + * @category combinators + */ +export const withDescription: { + (description: string | HelpDoc): (self: Command) => Command + (self: Command, description: string | HelpDoc): Command +} = Internal.withDescription + +/** + * @since 1.0.0 + * @category combinators + */ +export const withSubcommands: { + < + const Subcommands extends readonly [ + readonly [id: unknown, command: Command], + ...Array]> + ] + >( + subcommands: [...Subcommands] + ): ( + self: Command + ) => Command< + Command.ComputeParsedType< + A & Readonly<{ subcommand: Option> }> + > + > + < + A, + const Subcommands extends readonly [ + readonly [id: unknown, command: Command], + ...Array]> + ] + >( + self: Command, + subcommands: [...Subcommands] + ): Command< + Command.ComputeParsedType< + A & Readonly<{ subcommand: Option> }> + > + > +} = Internal.withSubcommands + +/** + * @since 1.0.0 + * @category combinators + */ +export const wizard: { + ( + prefix: ReadonlyArray, + config: CliConfig + ): ( + self: Command + ) => Effect< + Array, + ValidationError | QuitException, + FileSystem | Path | Terminal + > + ( + self: Command, + prefix: ReadonlyArray, + config: CliConfig + ): Effect< + Array, + ValidationError | QuitException, + FileSystem | Path | Terminal + > +} = Internal.wizard diff --git a/repos/effect/packages/cli/src/CommandDirective.ts b/repos/effect/packages/cli/src/CommandDirective.ts new file mode 100644 index 0000000..1cd79f0 --- /dev/null +++ b/repos/effect/packages/cli/src/CommandDirective.ts @@ -0,0 +1,65 @@ +/** + * @since 1.0.0 + */ +import type { BuiltInOptions } from "./BuiltInOptions.js" +import * as InternalCommandDirective from "./internal/commandDirective.js" + +/** + * @since 1.0.0 + * @category models + */ +export type CommandDirective = BuiltIn | UserDefined + +/** + * @since 1.0.0 + * @category models + */ +export interface BuiltIn { + readonly _tag: "BuiltIn" + readonly option: BuiltInOptions +} + +/** + * @since 1.0.0 + * @category models + */ +export interface UserDefined { + readonly _tag: "UserDefined" + readonly leftover: ReadonlyArray + readonly value: A +} + +/** + * @since 1.0.0 + * @category constructors + */ +export const builtIn: (option: BuiltInOptions) => CommandDirective = InternalCommandDirective.builtIn + +/** + * @since 1.0.0 + * @category refinements + */ +export const isBuiltIn: (self: CommandDirective) => self is BuiltIn = InternalCommandDirective.isBuiltIn + +/** + * @since 1.0.0 + * @category refinements + */ +export const isUserDefined: (self: CommandDirective) => self is UserDefined = + InternalCommandDirective.isUserDefined + +/** + * @since 1.0.0 + * @category mapping + */ +export const map: { + (f: (a: A) => B): (self: CommandDirective) => CommandDirective + (self: CommandDirective, f: (a: A) => B): CommandDirective +} = InternalCommandDirective.map + +/** + * @since 1.0.0 + * @category constructors + */ +export const userDefined: (leftover: ReadonlyArray, value: A) => CommandDirective = + InternalCommandDirective.userDefined diff --git a/repos/effect/packages/cli/src/ConfigFile.ts b/repos/effect/packages/cli/src/ConfigFile.ts new file mode 100644 index 0000000..5a676fd --- /dev/null +++ b/repos/effect/packages/cli/src/ConfigFile.ts @@ -0,0 +1,72 @@ +/** + * @since 2.0.0 + */ +import type { FileSystem } from "@effect/platform/FileSystem" +import type { Path } from "@effect/platform/Path" +import type { YieldableError } from "effect/Cause" +import type { ConfigProvider } from "effect/ConfigProvider" +import type { Effect } from "effect/Effect" +import type { Layer } from "effect/Layer" +import * as Internal from "./internal/configFile.js" + +/** + * @since 2.0.0 + * @category models + */ +export type Kind = "json" | "yaml" | "ini" | "toml" + +/** + * @since 2.0.0 + * @category errors + */ +export const ConfigErrorTypeId: unique symbol = Internal.ConfigErrorTypeId + +/** + * @since 2.0.0 + * @category errors + */ +export type ConfigErrorTypeId = typeof ConfigErrorTypeId + +/** + * @since 2.0.0 + * @category errors + */ +export interface ConfigFileError extends YieldableError { + readonly [ConfigErrorTypeId]: ConfigErrorTypeId + readonly _tag: "ConfigFileError" + readonly message: string +} + +/** + * @since 2.0.0 + * @category errors + */ +export const ConfigFileError: (message: string) => ConfigFileError = Internal.ConfigFileError + +/** + * @since 2.0.0 + * @category constructors + */ +export const makeProvider: ( + fileName: string, + options?: + | { + readonly formats?: ReadonlyArray + readonly searchPaths?: ReadonlyArray + } + | undefined +) => Effect = Internal.makeProvider + +/** + * @since 2.0.0 + * @category layers + */ +export const layer: ( + fileName: string, + options?: + | { + readonly formats?: ReadonlyArray + readonly searchPaths?: ReadonlyArray + } + | undefined +) => Layer = Internal.layer diff --git a/repos/effect/packages/cli/src/HelpDoc.ts b/repos/effect/packages/cli/src/HelpDoc.ts new file mode 100644 index 0000000..05d9596 --- /dev/null +++ b/repos/effect/packages/cli/src/HelpDoc.ts @@ -0,0 +1,209 @@ +/** + * @since 1.0.0 + */ +import type { AnsiDoc } from "@effect/printer-ansi/AnsiDoc" +import type { NonEmptyReadonlyArray } from "effect/Array" +import type { Span } from "./HelpDoc/Span.js" +import * as InternalHelpDoc from "./internal/helpDoc.js" + +/** + * A `HelpDoc` models the full documentation for a command-line application. + * + * `HelpDoc` is composed of optional header and footers, and in-between, a + * list of HelpDoc-level content items. + * + * HelpDoc-level content items, in turn, can be headers, paragraphs, description + * lists, and enumerations. + * + * A `HelpDoc` can be converted into plaintext, JSON, and HTML. + * + * @since 1.0.0 + * @category models + */ +export type HelpDoc = Empty | Header | Paragraph | DescriptionList | Enumeration | Sequence + +/** + * @since 1.0.0 + * @category models + */ +export interface Empty { + readonly _tag: "Empty" +} + +/** + * @since 1.0.0 + * @category models + */ +export interface Header { + readonly _tag: "Header" + readonly value: Span + readonly level: number +} + +/** + * @since 1.0.0 + * @category models + */ +export interface Paragraph { + readonly _tag: "Paragraph" + readonly value: Span +} + +/** + * @since 1.0.0 + * @category models + */ +export interface DescriptionList { + readonly _tag: "DescriptionList" + readonly definitions: NonEmptyReadonlyArray +} + +/** + * @since 1.0.0 + * @category models + */ +export interface Enumeration { + readonly _tag: "Enumeration" + readonly elements: NonEmptyReadonlyArray +} + +/** + * @since 1.0.0 + * @category models + */ +export interface Sequence { + readonly _tag: "Sequence" + readonly left: HelpDoc + readonly right: HelpDoc +} + +/** + * @since 1.0.0 + * @category refinements + */ +export const isEmpty: (helpDoc: HelpDoc) => helpDoc is Empty = InternalHelpDoc.isEmpty + +/** + * @since 1.0.0 + * @category refinements + */ +export const isHeader: (helpDoc: HelpDoc) => helpDoc is Header = InternalHelpDoc.isHeader + +/** + * @since 1.0.0 + * @category refinements + */ +export const isParagraph: (helpDoc: HelpDoc) => helpDoc is Paragraph = InternalHelpDoc.isParagraph + +/** + * @since 1.0.0 + * @category refinements + */ +export const isDescriptionList: (helpDoc: HelpDoc) => helpDoc is DescriptionList = InternalHelpDoc.isDescriptionList + +/** + * @since 1.0.0 + * @category refinements + */ +export const isEnumeration: (helpDoc: HelpDoc) => helpDoc is Enumeration = InternalHelpDoc.isEnumeration + +/** + * @since 1.0.0 + * @category refinements + */ +export const isSequence: (helpDoc: HelpDoc) => helpDoc is Sequence = InternalHelpDoc.isSequence + +/** + * @since 1.0.0 + * @category constructors + */ +export const empty: HelpDoc = InternalHelpDoc.empty + +/** + * @since 1.0.0 + * @category constructors + */ +export const blocks: (helpDocs: Iterable) => HelpDoc = InternalHelpDoc.blocks + +/** + * @since 1.0.0 + * @category constructors + */ +export const h1: (value: string | Span) => HelpDoc = InternalHelpDoc.h1 + +/** + * @since 1.0.0 + * @category constructors + */ +export const h2: (value: string | Span) => HelpDoc = InternalHelpDoc.h2 + +/** + * @since 1.0.0 + * @category constructors + */ +export const h3: (value: string | Span) => HelpDoc = InternalHelpDoc.h3 + +/** + * @since 1.0.0 + * @category constructors + */ +export const p: (value: string | Span) => HelpDoc = InternalHelpDoc.p + +/** + * @since 1.0.0 + * @category constructors + */ +export const descriptionList: ( + definitions: NonEmptyReadonlyArray<[Span, HelpDoc]> +) => HelpDoc = InternalHelpDoc.descriptionList + +/** + * @since 1.0.0 + * @category constructors + */ +export const enumeration: (elements: NonEmptyReadonlyArray) => HelpDoc = InternalHelpDoc.enumeration + +/** + * @since 1.0.0 + * @category getters + */ +export const getSpan: (self: HelpDoc) => Span = InternalHelpDoc.getSpan + +/** + * @since 1.0.0 + * @category combinators + */ +export const sequence: { + (that: HelpDoc): (self: HelpDoc) => HelpDoc + (self: HelpDoc, that: HelpDoc): HelpDoc +} = InternalHelpDoc.sequence + +/** + * @since 1.0.0 + * @category combinators + */ +export const orElse: { + (that: HelpDoc): (self: HelpDoc) => HelpDoc + (self: HelpDoc, that: HelpDoc): HelpDoc +} = InternalHelpDoc.orElse + +/** + * @since 1.0.0 + * @category mapping + */ +export const mapDescriptionList: { + (f: (span: Span, helpDoc: HelpDoc) => [Span, HelpDoc]): (self: HelpDoc) => HelpDoc + (self: HelpDoc, f: (span: Span, helpDoc: HelpDoc) => [Span, HelpDoc]): HelpDoc +} = InternalHelpDoc.mapDescriptionList + +/** + * @since 1.0.0 + * @category rendering + */ +export const toAnsiDoc: (self: HelpDoc) => AnsiDoc = InternalHelpDoc.toAnsiDoc + +/** + * @since 1.0.0 + * @category rendering + */ +export const toAnsiText: (self: HelpDoc) => string = InternalHelpDoc.toAnsiText diff --git a/repos/effect/packages/cli/src/HelpDoc/Span.ts b/repos/effect/packages/cli/src/HelpDoc/Span.ts new file mode 100644 index 0000000..3f2812d --- /dev/null +++ b/repos/effect/packages/cli/src/HelpDoc/Span.ts @@ -0,0 +1,160 @@ +/** + * @since 1.0.0 + */ +import type { Color } from "@effect/printer-ansi/Color" +import * as InternalSpan from "../internal/helpDoc/span.js" + +/** + * @since 1.0.0 + * @category models + */ +export type Span = Highlight | Sequence | Strong | Text | URI | Weak + +/** + * @since 1.0.0 + * @category models + */ +export interface Highlight { + readonly _tag: "Highlight" + readonly value: Span + readonly color: Color +} + +/** + * @since 1.0.0 + * @category models + */ +export interface Sequence { + readonly _tag: "Sequence" + readonly left: Span + readonly right: Span +} + +/** + * @since 1.0.0 + * @category models + */ +export interface Strong { + readonly _tag: "Strong" + readonly value: Span +} + +/** + * @since 1.0.0 + * @category models + */ +export interface Text { + readonly _tag: "Text" + readonly value: string +} + +/** + * @since 1.0.0 + * @category models + */ +export interface URI { + readonly _tag: "URI" + readonly value: string +} + +/** + * @since 1.0.0 + * @category models + */ +export interface Weak { + readonly _tag: "Weak" + readonly value: Span +} + +/** + * @since 1.0.0 + * @category refinements + */ +export const isSequence: (self: Span) => self is Sequence = InternalSpan.isSequence + +/** + * @since 1.0.0 + * @category refinements + */ +export const isStrong: (self: Span) => self is Strong = InternalSpan.isStrong + +/** + * @since 1.0.0 + * @category refinements + */ +export const isText: (self: Span) => self is Text = InternalSpan.isText + +/** + * @since 1.0.0 + * @category refinements + */ +export const isUri: (self: Span) => self is URI = InternalSpan.isUri + +/** + * @since 1.0.0 + * @category refinements + */ +export const isWeak: (self: Span) => self is Weak = InternalSpan.isWeak + +/** + * @since 1.0.0 + * @category constructors + */ +export const empty: Span = InternalSpan.empty + +/** + * @since 1.0.0 + * @category constructors + */ +export const space: Span = InternalSpan.space + +/** + * @since 1.0.0 + * @category constructors + */ +export const text: (value: string) => Span = InternalSpan.text + +/** + * @since 1.0.0 + * @category constructors + */ +export const code: (value: string | Span) => Span = InternalSpan.code + +/** + * @since 1.0.0 + * @category constructors + */ +export const error: (value: string | Span) => Span = InternalSpan.error + +/** + * @since 1.0.0 + * @category constructors + */ +export const weak: (value: string | Span) => Span = InternalSpan.weak + +/** + * @since 1.0.0 + * @category constructors + */ +export const strong: (value: string | Span) => Span = InternalSpan.strong + +/** + * @since 1.0.0 + * @category constructors + */ +export const uri: (value: string) => Span = InternalSpan.uri + +/** + * @since 1.0.0 + * @category combinators + */ +export const concat: { + (that: Span): (self: Span) => Span + (self: Span, that: Span): Span +} = InternalSpan.concat + +/** + * @since 1.0.0 + * @category combinators + */ +export const spans: (spans: Iterable) => Span = InternalSpan.spans diff --git a/repos/effect/packages/cli/src/Options.ts b/repos/effect/packages/cli/src/Options.ts new file mode 100644 index 0000000..4a01a4a --- /dev/null +++ b/repos/effect/packages/cli/src/Options.ts @@ -0,0 +1,590 @@ +/** + * @since 1.0.0 + */ +import type { FileSystem } from "@effect/platform/FileSystem" +import type { Path } from "@effect/platform/Path" +import type { QuitException, Terminal } from "@effect/platform/Terminal" +import type { NonEmptyArray } from "effect/Array" +import type { Config } from "effect/Config" +import type { Effect } from "effect/Effect" +import type { Either } from "effect/Either" +import type { HashMap } from "effect/HashMap" +import type { Option } from "effect/Option" +import type { Pipeable } from "effect/Pipeable" +import type { Redacted } from "effect/Redacted" +import type { Schema } from "effect/Schema" +import type { Secret } from "effect/Secret" +import type { CliConfig } from "./CliConfig.js" +import type { HelpDoc } from "./HelpDoc.js" +import * as InternalOptions from "./internal/options.js" +import type { Primitive } from "./Primitive.js" +import type { Prompt } from "./Prompt.js" +import type { Usage } from "./Usage.js" +import type { ValidationError } from "./ValidationError.js" + +/** + * @since 1.0.0 + * @category symbols + */ +export const OptionsTypeId: unique symbol = InternalOptions.OptionsTypeId + +/** + * @since 1.0.0 + * @category symbols + */ +export type OptionsTypeId = typeof OptionsTypeId + +/** + * @since 1.0.0 + * @category models + */ +export interface Options extends Options.Variance, Pipeable {} + +/** + * @since 1.0.0 + */ +export declare namespace Options { + /** + * @since 1.0.0 + * @category models + */ + export interface Variance { + readonly [OptionsTypeId]: { + _A: (_: never) => A + } + } + + /** + * @since 1.0.0 + * @category models + */ + export interface BooleanOptionsConfig { + readonly ifPresent?: boolean + readonly negationNames?: ReadonlyArray + readonly aliases?: ReadonlyArray + } + + /** + * @since 1.0.0 + * @category models + */ + export interface PathOptionsConfig { + readonly exists?: Primitive.PathExists + } +} + +/** + * @since 1.0.0 + */ +export declare namespace All { + /** + * @since 1.0.0 + */ + export type OptionsAny = Options + + /** + * @since 1.0.0 + */ + export type ReturnIterable> = [T] extends [Iterable>] ? + Options> + : never + + /** + * @since 1.0.0 + */ + export type ReturnTuple> = Options< + T[number] extends never ? [] + : { + -readonly [K in keyof T]: [T[K]] extends [Options.Variance] ? _A : never + } + > extends infer X ? X : never + + /** + * @since 1.0.0 + */ + export type ReturnObject = [T] extends [{ [K: string]: OptionsAny }] ? Options< + { + -readonly [K in keyof T]: [T[K]] extends [Options.Variance] ? _A : never + } + > + : never + + /** + * @since 1.0.0 + */ + export type Return< + Arg extends Iterable | Record + > = [Arg] extends [ReadonlyArray] ? ReturnTuple + : [Arg] extends [Iterable] ? ReturnIterable + : [Arg] extends [Record] ? ReturnObject + : never +} + +// ============================================================================= +// Refinements +// ============================================================================= + +/** + * @since 1.0.0 + * @category refinements + */ +export const isOptions: (u: unknown) => u is Options = InternalOptions.isOptions + +// ============================================================================= +// Constructors +// ============================================================================= + +/** + * @since 1.0.0 + * @category constructors + */ +export const all: < + const Arg extends Iterable> | Record> +>(arg: Arg) => All.Return = InternalOptions.all + +/** + * @since 1.0.0 + * @category constructors + */ +export const boolean: (name: string, options?: Options.BooleanOptionsConfig) => Options = + InternalOptions.boolean + +/** + * Constructs command-line `Options` that represent a choice between several + * inputs. The input will be mapped to it's associated value during parsing. + * + * **Example** + * + * ```ts + * import * as Options from "@effect/cli/Options" + * + * export const animal: Options.Options<"dog" | "cat"> = Options.choice( + * "animal", + * ["dog", "cat"] + * ) + * ``` + * + * @since 1.0.0 + * @category constructors + */ +export const choice: >( + name: string, + choices: C +) => Options = InternalOptions.choice + +/** + * Constructs command-line `Options` that represent a choice between several + * inputs. The input will be mapped to it's associated value during parsing. + * + * **Example** + * + * ```ts + * import * as Options from "@effect/cli/Options" + * import * as Data from "effect/Data" + * + * export type Animal = Dog | Cat + * + * export interface Dog { + * readonly _tag: "Dog" + * } + * + * export const Dog = Data.tagged("Dog") + * + * export interface Cat { + * readonly _tag: "Cat" + * } + * + * export const Cat = Data.tagged("Cat") + * + * export const animal: Options.Options = Options.choiceWithValue("animal", [ + * ["dog", Dog()], + * ["cat", Cat()], + * ]) + * ``` + * + * @since 1.0.0 + * @category constructors + */ +export const choiceWithValue: >( + name: string, + choices: C +) => Options = InternalOptions.choiceWithValue + +/** + * @since 1.0.0 + * @category constructors + */ +export const date: (name: string) => Options = InternalOptions.date + +/** + * Creates a parameter expecting path to a directory. + * + * @since 1.0.0 + * @category constructors + */ +export const directory: (name: string, config?: Options.PathOptionsConfig) => Options = + InternalOptions.directory + +/** + * Creates a parameter expecting path to a file. + * + * @since 1.0.0 + * @category constructors + */ +export const file: (name: string, config?: Options.PathOptionsConfig) => Options = InternalOptions.file + +/** + * Creates a parameter expecting path to a file and reads its contents. + * + * @since 1.0.0 + * @category constructors + */ +export const fileContent: (name: string) => Options = + InternalOptions.fileContent + +/** + * Creates a parameter expecting path to a file and parse its contents. + * + * @since 1.0.0 + * @category constructors + */ +export const fileParse: (name: string, format?: "json" | "yaml" | "ini" | "toml" | undefined) => Options = + InternalOptions.fileParse + +/** + * Creates a parameter expecting path to a file, parse its contents and validate + * it with a Schema. + * + * @since 1.0.0 + * @category constructors + */ +export const fileSchema: ( + name: string, + schema: Schema, + format?: "json" | "yaml" | "ini" | "toml" | undefined +) => Options = InternalOptions.fileSchema + +/** + * Creates a parameter expecting path to a file and reads its contents. + * + * @since 1.0.0 + * @category constructors + */ +export const fileText: (name: string) => Options = InternalOptions.fileText + +/** + * @since 1.0.0 + * @category constructors + */ +export const float: (name: string) => Options = InternalOptions.float + +/** + * @since 1.0.0 + * @category combinators + */ +export const getHelp: (self: Options) => HelpDoc = InternalOptions.getHelp + +/** + * @since 1.0.0 + * @category combinators + */ +export const getIdentifier: (self: Options) => Option = InternalOptions.getIdentifier + +/** + * @since 1.0.0 + * @category combinators + */ +export const getUsage: (self: Options) => Usage = InternalOptions.getUsage + +/** + * @since 1.0.0 + * @category constructors + */ +export const integer: (name: string) => Options = InternalOptions.integer + +/** + * @since 1.0.0 + * @category constructors + */ +export const keyValueMap: (option: string | Options) => Options> = + InternalOptions.keyValueMap + +/** + * @since 1.0.0 + * @category constructors + */ +export const none: Options = InternalOptions.none + +/** + * @since 1.0.0 + * @category constructors + */ +export const redacted: (name: string) => Options = InternalOptions.redacted + +/** + * @since 1.0.0 + * @category constructors + * @deprecated + */ +export const secret: (name: string) => Options = InternalOptions.secret + +/** + * @since 1.0.0 + * @category constructors + */ +export const text: (name: string) => Options = InternalOptions.text + +// ============================================================================= +// Combinators +// ============================================================================= + +/** + * @since 1.0.0 + * @category combinators + */ +export const atMost: { + (times: number): (self: Options) => Options> + (self: Options, times: number): Options> +} = InternalOptions.atMost + +/** + * @since 1.0.0 + * @category combinators + */ +export const atLeast: { + (times: 0): (self: Options) => Options> + (times: number): (self: Options) => Options> + (self: Options, times: 0): Options> + (self: Options, times: number): Options> +} = InternalOptions.atLeast + +/** + * @since 1.0.0 + * @category combinators + */ +export const between: { + (min: 0, max: number): (self: Options) => Options> + (min: number, max: number): (self: Options) => Options> + (self: Options, min: 0, max: number): Options> + (self: Options, min: number, max: number): Options> +} = InternalOptions.between + +/** + * @since 1.0.0 + * @category combinators + */ +export const filterMap: { + (f: (a: A) => Option, message: string): (self: Options) => Options + (self: Options, f: (a: A) => Option, message: string): Options +} = InternalOptions.filterMap + +/** + * Returns `true` if the specified `Options` is a boolean flag, `false` + * otherwise. + * + * @since 1.0.0 + * @category combinators + */ +export const isBool: (self: Options) => boolean = InternalOptions.isBool + +/** + * @since 1.0.0 + * @category combinators + */ +export const map: { + (f: (a: A) => B): (self: Options) => Options + (self: Options, f: (a: A) => B): Options +} = InternalOptions.map + +/** + * @since 1.0.0 + * @category combinators + */ +export const mapEffect: { + (f: (a: A) => Effect): (self: Options) => Options + (self: Options, f: (a: A) => Effect): Options +} = InternalOptions.mapEffect + +/** + * @since 1.0.0 + * @category combinators + */ +export const mapTryCatch: { + (f: (a: A) => B, onError: (e: unknown) => HelpDoc): (self: Options) => Options + (self: Options, f: (a: A) => B, onError: (e: unknown) => HelpDoc): Options +} = InternalOptions.mapTryCatch + +/** + * @since 1.0.0 + * @category combinators + */ +export const optional: (self: Options) => Options> = InternalOptions.optional + +/** + * @since 1.0.0 + * @category combinators + */ +export const orElse: { + (that: Options): (self: Options) => Options + (self: Options, that: Options): Options +} = InternalOptions.orElse + +/** + * @since 1.0.0 + * @category combinators + */ +export const orElseEither: { + (that: Options): (self: Options) => Options> + (self: Options, that: Options): Options> +} = InternalOptions.orElseEither + +/** + * @since 1.0.0 + * @category combinators + */ +export const parse: { + ( + args: HashMap>, + config: CliConfig + ): (self: Options) => Effect + ( + self: Options, + args: HashMap>, + config: CliConfig + ): Effect +} = InternalOptions.parse + +/** + * Indicates that the specified command-line option can be repeated `0` or more + * times. + * + * **NOTE**: if the command-line option is not provided, and empty array will be + * returned as the value for said option. + * + * @since 1.0.0 + * @category combinators + */ +export const repeated: (self: Options) => Options> = InternalOptions.repeated + +/** + * Processes the provided command-line arguments, searching for the specified + * `Options`. + * + * Returns an `Option`, any leftover arguments, and the + * constructed value of type `A`. The possible error inside + * `Option` would only be triggered if there is an error when + * parsing the command-line arguments. This is because `ValidationError`s are + * also used internally to control the end of the command-line arguments (i.e. + * the command-line symbol `--`) corresponding to options. + * + * @since 1.0.0 + * @category combinators + */ +export const processCommandLine: { + ( + args: ReadonlyArray, + config: CliConfig + ): ( + self: Options + ) => Effect< + [Option, Array, A], + ValidationError, + FileSystem | Path | Terminal + > + ( + self: Options, + args: ReadonlyArray, + config: CliConfig + ): Effect< + [Option, Array, A], + ValidationError, + FileSystem | Path | Terminal + > +} = InternalOptions.processCommandLine + +/** + * @since 1.0.0 + * @category combinators + */ +export const withAlias: { + (alias: string): (self: Options) => Options + (self: Options, alias: string): Options +} = InternalOptions.withAlias + +/** + * @since 1.0.0 + * @category combinators + */ +export const withDefault: { + (fallback: B): (self: Options) => Options + (self: Options, fallback: B): Options +} = InternalOptions.withDefault + +/** + * @since 1.0.0 + * @category combinators + */ +export const withFallbackConfig: { + (config: Config): (self: Options) => Options + (self: Options, config: Config): Options +} = InternalOptions.withFallbackConfig + +/** + * @since 1.0.0 + * @category combinators + */ +export const withFallbackPrompt: { + (prompt: Prompt): (self: Options) => Options + (self: Options, prompt: Prompt): Options +} = InternalOptions.withFallbackPrompt + +/** + * @since 1.0.0 + * @category combinators + */ +export const withDescription: { + (description: string): (self: Options) => Options + (self: Options, description: string): Options +} = InternalOptions.withDescription + +/** + * @since 1.0.0 + * @category combinators + */ +export const withPseudoName: { + (pseudoName: string): (self: Options) => Options + (self: Options, pseudoName: string): Options +} = InternalOptions.withPseudoName + +/** + * @since 1.0.0 + * @category combinators + */ +export const withSchema: { + (schema: Schema): (self: Options) => Options + (self: Options, schema: Schema): Options +} = InternalOptions.withSchema + +/** + * @since 1.0.0 + * @category combinators + */ +export const wizard: { + ( + config: CliConfig + ): ( + self: Options + ) => Effect< + Array, + QuitException | ValidationError, + FileSystem | Path | Terminal + > + ( + self: Options, + config: CliConfig + ): Effect< + Array, + QuitException | ValidationError, + FileSystem | Path | Terminal + > +} = InternalOptions.wizard diff --git a/repos/effect/packages/cli/src/Primitive.ts b/repos/effect/packages/cli/src/Primitive.ts new file mode 100644 index 0000000..2c77c8f --- /dev/null +++ b/repos/effect/packages/cli/src/Primitive.ts @@ -0,0 +1,183 @@ +/** + * @since 1.0.0 + */ +import type { FileSystem } from "@effect/platform/FileSystem" +import type { Effect } from "effect/Effect" +import type { Option } from "effect/Option" +import type { Pipeable } from "effect/Pipeable" +import type { CliConfig } from "./CliConfig.js" +import type { HelpDoc } from "./HelpDoc.js" +import type { Span } from "./HelpDoc/Span.js" +import * as InternalPrimitive from "./internal/primitive.js" +import type { Prompt } from "./Prompt.js" + +/** + * @since 1.0.0 + * @category symbol + */ +export const PrimitiveTypeId: unique symbol = InternalPrimitive.PrimitiveTypeId as PrimitiveTypeId + +/** + * @since 1.0.0 + * @category symbol + */ +export type PrimitiveTypeId = typeof PrimitiveTypeId + +/** + * A `Primitive` represents the primitive types supported by Effect CLI. + * + * Each primitive type has a way to parse and validate from a string. + * + * @since 1.0.0 + * @category models + */ +export interface Primitive extends Primitive.Variance {} + +/** + * @since 1.0.0 + */ +export declare namespace Primitive { + /** + * @since 1.0.0 + * @category models + */ + export interface Variance extends Pipeable { + readonly [PrimitiveTypeId]: { + readonly _A: (_: never) => A + } + } + + /** + * @since 1.0.0 + * @category models + */ + export type PathExists = "yes" | "no" | "either" + + /** + * @since 1.0.0 + * @category models + */ + export type PathType = "file" | "directory" | "either" + + /** + * @since 1.0.0 + * @category models + */ + export type ValueType

= [P] extends [{ + readonly [PrimitiveTypeId]: { + readonly _A: (_: never) => infer A + } + }] ? A + : never +} + +/** + * @since 1.0.0 + * @category Predicates + */ +export const isBool: (self: Primitive) => boolean = InternalPrimitive.isBool + +/** + * Represents a boolean value. + * + * True values can be passed as one of: `["true", "1", "y", "yes" or "on"]`. + * False value can be passed as one of: `["false", "o", "n", "no" or "off"]`. + * + * @since 1.0.0 + * @category constructors + */ +export const boolean: (defaultValue: Option) => Primitive = InternalPrimitive.boolean + +/** + * @since 1.0.0 + * @category constructors + */ +export const choice: (alternatives: ReadonlyArray<[string, A]>) => Primitive = InternalPrimitive.choice + +/** + * Represents a date in ISO-8601 format, such as `2007-12-03T10:15:30`. + * + * @since 1.0.0 + * @category constructors + */ +export const date: Primitive = InternalPrimitive.date + +/** + * Represents a floating point number. + * + * @since 1.0.0 + * @category constructors + */ +export const float: Primitive = InternalPrimitive.float + +/** + * Returns a text representation of the valid choices for a primitive type, if + * any. + * + * @since 1.0.0 + * @category combinators + */ +export const getChoices: (self: Primitive) => Option = InternalPrimitive.getChoices + +/** + * Returns help documentation for a primitive type. + * + * @since 1.0.0 + * @category combinators + */ +export const getHelp: (self: Primitive) => Span = InternalPrimitive.getHelp + +/** + * Returns a string representation of the primitive type. + * + * @since 1.0.0 + * @category combinators + */ +export const getTypeName: (self: Primitive) => string = InternalPrimitive.getTypeName + +/** + * Represents an integer. + * + * @since 1.0.0 + * @category constructors + */ +export const integer: Primitive = InternalPrimitive.integer + +/** + * Represents a user-defined piece of text. + * + * @since 1.0.0 + * @category constructors + */ +export const text: Primitive = InternalPrimitive.text + +/** + * Validates that the specified value, if any, matches the specified primitive + * type. + * + * @since 1.0.0 + * @category combinators + */ +export const validate: { + ( + value: Option, + config: CliConfig + ): (self: Primitive) => Effect + ( + self: Primitive, + value: Option, + config: CliConfig + ): Effect +} = InternalPrimitive.validate + +/** + * Runs a wizard that will prompt the user for input matching the specified + * primitive type. + * + * @since 1.0.0 + * @category combinators + */ +export const wizard: { + (help: HelpDoc): (self: Primitive) => Prompt + (self: Primitive, help: HelpDoc): Prompt +} = InternalPrimitive.wizard diff --git a/repos/effect/packages/cli/src/Prompt.ts b/repos/effect/packages/cli/src/Prompt.ts new file mode 100644 index 0000000..22e05ae --- /dev/null +++ b/repos/effect/packages/cli/src/Prompt.ts @@ -0,0 +1,695 @@ +/** + * @since 1.0.0 + */ +import type { FileSystem } from "@effect/platform/FileSystem" +import type { Path } from "@effect/platform/Path" +import type { QuitException, Terminal, UserInput } from "@effect/platform/Terminal" +import type { TaggedEnum } from "effect/Data" +import type { Effect } from "effect/Effect" +import type { Pipeable } from "effect/Pipeable" +import type { Redacted } from "effect/Redacted" +import * as InternalPrompt from "./internal/prompt.js" +import * as InternalConfirmPrompt from "./internal/prompt/confirm.js" +import * as InternalDatePrompt from "./internal/prompt/date.js" +import * as InternalFilePrompt from "./internal/prompt/file.js" +import * as InternalListPrompt from "./internal/prompt/list.js" +import * as InternalMultiSelectPrompt from "./internal/prompt/multi-select.js" +import * as InternalNumberPrompt from "./internal/prompt/number.js" +import * as InternalSelectPrompt from "./internal/prompt/select.js" +import * as InternalTextPrompt from "./internal/prompt/text.js" +import * as InternalTogglePrompt from "./internal/prompt/toggle.js" +import type { Primitive } from "./Primitive.js" + +/** + * @since 1.0.0 + * @category symbols + */ +export const PromptTypeId: unique symbol = InternalPrompt.PromptTypeId + +/** + * @since 1.0.0 + * @category symbols + */ +export type PromptTypeId = typeof PromptTypeId + +/** + * @since 1.0.0 + * @category models + */ +export interface Prompt extends Prompt.Variance, Pipeable, Effect {} + +/** + * @since 1.0.0 + */ +export declare namespace Prompt { + /** + * @since 1.0.0 + * @category models + */ + export interface Variance { + readonly [PromptTypeId]: Prompt.VarianceStruct + } + + /** + * @since 1.0.0 + * @category models + */ + export interface VarianceStruct { + readonly _Output: (_: never) => Output + } + + /** + * Represents the services available to a custom `Prompt`. + * + * @since 1.0.0 + * @category models + */ + export type Environment = FileSystem | Path | Terminal + + /** + * Represents the action that should be taken by a `Prompt` based upon the + * user input received during the current frame. + * + * @since 1.0.0 + * @category models + */ + export type Action = TaggedEnum<{ + readonly Beep: {} + readonly NextFrame: { readonly state: State } + readonly Submit: { readonly value: Output } + }> + + /** + * Represents the definition of an `Action`. + * + * Required to create a `Data.TaggedEnum` with generic type arguments. + * + * @since 1.0.0 + * @category models + */ + export interface ActionDefinition extends TaggedEnum.WithGenerics<2> { + readonly taggedEnum: Action + } + + /** + * Represents the set of handlers used by a `Prompt` to: + * + * - Render the current frame of the prompt + * - Process user input and determine the next `Prompt.Action` to take + * - Clear the terminal screen before the next frame + * + * @since 1.0.0 + * @category models + */ + export interface Handlers { + /** + * A function that is called to render the current frame of the `Prompt`. + * + * @param state The current state of the prompt. + * @param action The `Prompt.Action` for the current frame. + * @returns An ANSI escape code sequence to display in the terminal screen. + */ + readonly render: ( + state: State, + action: Action + ) => Effect + /** + * A function that is called to process user input and determine the next + * `Prompt.Action` that should be taken. + * + * @param input The input the user provided for the current frame. + * @param state The current state of the prompt. + * @returns The next `Prompt.Action` that should be taken. + */ + readonly process: ( + input: UserInput, + state: State + ) => Effect, never, Environment> + /** + * A function that is called to clear the terminal screen before rendering + * the next frame of the `Prompt`. + * + * @param action The `Prompt.Action` for the current frame. + * @param columns The current number of columns available in the `Terminal`. + * @returns An ANSI escape code sequence used to clear the terminal screen. + */ + readonly clear: ( + state: State, + action: Action + ) => Effect + } + + /** + * @since 1.0.0 + * @category models + */ + export interface ConfirmOptions { + /** + * The message to display in the prompt. + */ + readonly message: string + /** + * The intitial value of the confirm prompt (defaults to `false`). + */ + readonly initial?: boolean + /** + * The label to display after a user has responded to the prompt. + */ + readonly label?: { + /** + * The label used if the prompt is confirmed (defaults to `"yes"`). + */ + readonly confirm: string + /** + * The label used if the prompt is not confirmed (defaults to `"no"`). + */ + readonly deny: string + } + /** + * The placeholder to display when a user is responding to the prompt. + */ + readonly placeholder?: { + /** + * The placeholder to use if the `initial` value of the prompt is `true` + * (defaults to `"(Y/n)"`). + */ + readonly defaultConfirm?: string + /** + * The placeholder to use if the `initial` value of the prompt is `false` + * (defaults to `"(y/N)"`). + */ + readonly defaultDeny?: string + } + } + + /** + * @since 1.0.0 + * @category models + */ + export interface DateOptions { + /** + * The message to display in the prompt. + */ + readonly message: string + /** + * The initial date value to display in the prompt (defaults to the current + * date). + */ + readonly initial?: globalThis.Date + /** + * The format mask of the date (defaults to `YYYY-MM-DD HH:mm:ss`). + */ + readonly dateMask?: string + /** + * An effectful function that can be used to validate the value entered into + * the prompt before final submission. + */ + readonly validate?: (value: globalThis.Date) => Effect + /** + * Custom locales that can be used in place of the defaults. + */ + readonly locales?: { + /** + * The full names of each month of the year. + */ + readonly months: [ + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string + ] + /** + * The short names of each month of the year. + */ + readonly monthsShort: [ + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string + ] + /** + * The full names of each day of the week. + */ + readonly weekdays: [string, string, string, string, string, string, string] + /** + * The short names of each day of the week. + */ + readonly weekdaysShort: [string, string, string, string, string, string, string] + } + } + + /** + * @since 1.0.0 + * @category models + */ + export interface IntegerOptions { + /** + * The message to display in the prompt. + */ + readonly message: string + /** + * The minimum value that can be entered by the user (defaults to `-Infinity`). + */ + readonly min?: number + /** + * The maximum value that can be entered by the user (defaults to `Infinity`). + */ + readonly max?: number + /** + * The value that will be used to increment the prompt value when using the + * up arrow key (defaults to `1`). + */ + readonly incrementBy?: number + /** + * The value that will be used to decrement the prompt value when using the + * down arrow key (defaults to `1`). + */ + readonly decrementBy?: number + /** + * An effectful function that can be used to validate the value entered into + * the prompt before final submission. + */ + readonly validate?: (value: number) => Effect + } + + /** + * @since 1.0.0 + * @category models + */ + export interface FloatOptions extends IntegerOptions { + /** + * The precision to use for the floating point value (defaults to `2`). + */ + readonly precision?: number + } + + /** + * @since 1.0.0 + * @category models + */ + export interface ListOptions extends TextOptions { + /** + * The delimiter that separates list entries. + */ + readonly delimiter?: string + } + + /** + * @since 1.0.0 + * @category models + */ + export interface FileOptions { + /** + * The path type that will be selected. + * + * Defaults to `"file"`. + */ + readonly type?: Primitive.PathType + /** + * The message to display in the prompt. + * + * Defaults to `"Choose a file"`. + */ + readonly message?: string + /** + * Where the user will initially be prompted to select files from. + * + * Defaults to the current working directory. + */ + readonly startingPath?: string + /** + * The number of choices to display at one time + * + * Defaults to `10`. + */ + readonly maxPerPage?: number + /** + * A function which removes any file from the prompt display where the + * specified predicate returns `true`. + * + * Defaults to returning all files. + */ + readonly filter?: (file: string) => boolean | Effect + } + + /** + * @since 1.0.0 + * @category models + */ + export interface SelectOptions { + /** + * The message to display in the prompt. + */ + readonly message: string + /** + * The choices to display to the user. + */ + readonly choices: ReadonlyArray> + /** + * The number of choices to display at one time (defaults to `10`). + */ + readonly maxPerPage?: number + } + + /** + * @since 1.0.0 + * @category models + */ + export interface MultiSelectOptions { + /** + * Text for the "Select All" option (defaults to "Select All"). + */ + readonly selectAll?: string + /** + * Text for the "Select None" option (defaults to "Select None"). + */ + readonly selectNone?: string + /** + * Text for the "Inverse Selection" option (defaults to "Inverse Selection"). + */ + readonly inverseSelection?: string + /** + * The minimum number of choices that must be selected. + */ + readonly min?: number + /** + * The maximum number of choices that can be selected. + */ + readonly max?: number + } + + /** + * @since 1.0.0 + * @category models + */ + export interface SelectChoice { + /** + * The name of the select option that is displayed to the user. + */ + readonly title: string + /** + * The underlying value of the select option. + */ + readonly value: A + /** + * An optional description for the select option which will be displayed + * to the user. + */ + readonly description?: string + /** + * Whether or not this select option is disabled. + */ + readonly disabled?: boolean + /** + * Whether this option should be selected by default (only used by MultiSelect). + */ + readonly selected?: boolean + } + + /** + * @since 1.0.0 + * @category models + */ + export interface TextOptions { + /** + * The message to display in the prompt. + */ + readonly message: string + /** + * The default value of the text option. + */ + readonly default?: string + /** + * An effectful function that can be used to validate the value entered into + * the prompt before final submission. + */ + readonly validate?: (value: string) => Effect + } + + /** + * @since 1.0.0 + * @category models + */ + export interface ToggleOptions { + /** + * The message to display in the prompt. + */ + readonly message: string + /** + * The intitial value of the toggle prompt (defaults to `false`). + */ + readonly initial?: boolean + /** + * The text to display when the toggle is in the active state (defaults to + * `on`). + */ + readonly active?: string + /** + * The text to display when the toggle is in the inactive state (defaults to + * `off`). + */ + readonly inactive?: string + } +} + +/** + * @since 1.0.0 + */ +export declare namespace All { + /** + * @since 1.0.0 + */ + export type PromptAny = Prompt + + /** + * @since 1.0.0 + */ + export type ReturnIterable> = [T] extends [Iterable>] ? + Prompt> + : never + + /** + * @since 1.0.0 + */ + export type ReturnTuple> = Prompt< + T[number] extends never ? [] + : { -readonly [K in keyof T]: [T[K]] extends [Prompt.Variance] ? _A : never } + > extends infer X ? X : never + + /** + * @since 1.0.0 + */ + export type ReturnObject = [T] extends [{ [K: string]: PromptAny }] ? Prompt< + { + -readonly [K in keyof T]: [T[K]] extends [Prompt.Variance] ? _A : never + } + > + : never + + /** + * @since 1.0.0 + */ + export type Return< + Arg extends Iterable | Record + > = [Arg] extends [ReadonlyArray] ? ReturnTuple + : [Arg] extends [Iterable] ? ReturnIterable + : [Arg] extends [Record] ? ReturnObject + : never +} + +/** + * Runs all the provided prompts in sequence respecting the structure provided + * in input. + * + * Supports either a tuple / iterable of prompts or a record / struct of prompts + * as an argument. + * + * **Example** + * + * ```ts + * import * as Prompt from "@effect/cli/Prompt" + * import * as Effect from "effect/Effect" + * + * const username = Prompt.text({ + * message: "Enter your username: " + * }) + * + * const password = Prompt.password({ + * message: "Enter your password: ", + * validate: (value) => + * value.length === 0 + * ? Effect.fail("Password cannot be empty") + * : Effect.succeed(value) + * }) + * + * const allWithTuple = Prompt.all([username, password]) + * + * const allWithRecord = Prompt.all({ username, password }) + * ``` + * + * @since 1.0.0 + * @category collecting & elements + */ +export const all: > | Record>>(arg: Arg) => All.Return = + InternalPrompt.all + +/** + * @since 1.0.0 + * @category constructors + */ +export const confirm: (options: Prompt.ConfirmOptions) => Prompt = InternalConfirmPrompt.confirm + +/** + * Creates a custom `Prompt` from the specified initial state and handlers. + * + * The initial state can either be a pure value or an `Effect`. This is + * particularly useful when the initial state of the `Prompt` must be computed + * by performing some effectful computation, such as reading data from the file + * system. + * + * A `Prompt` is essentially a render loop where user input triggers a new frame + * to be rendered to the `Terminal`. The `handlers` of a custom prompt are used + * to control what is rendered to the `Terminal` each frame. During each frame, + * the following occurs: + * + * 1. The `render` handler is called with this frame's prompt state and prompt + * action and returns an ANSI escape string to be rendered to the + * `Terminal` + * 2. The `Terminal` obtains input from the user + * 3. The `process` handler is called with the input obtained from the user + * and this frame's prompt state and returns the next prompt action that + * should be performed + * 4. The `clear` handler is called with this frame's prompt state and prompt + * action and returns an ANSI escape string used to clear the screen of + * the `Terminal` + * + * @since 1.0.0 + * @category constructors + */ +export const custom: ( + initialState: State | Effect, + handlers: Prompt.Handlers +) => Prompt = InternalPrompt.custom + +/** + * @since 1.0.0 + * @category constructors + */ +export const date: (options: Prompt.DateOptions) => Prompt = InternalDatePrompt.date + +/** + * @since 1.0.0 + * @category constructors + */ +export const file: (options?: Prompt.FileOptions) => Prompt = InternalFilePrompt.file + +/** + * @since 1.0.0 + * @category combinators + */ +export const flatMap: { + ( + f: (output: Output) => Prompt + ): (self: Prompt) => Prompt + (self: Prompt, f: (output: Output) => Prompt): Prompt +} = InternalPrompt.flatMap + +/** + * @since 1.0.0 + * @category constructors + */ +export const float: (options: Prompt.FloatOptions) => Prompt = InternalNumberPrompt.float + +/** + * @since 1.0.0 + * @category constructors + */ +export const hidden: (options: Prompt.TextOptions) => Prompt = InternalTextPrompt.hidden + +/** + * @since 1.0.0 + * @category constructors + */ +export const integer: (options: Prompt.IntegerOptions) => Prompt = InternalNumberPrompt.integer + +/** + * @since 1.0.0 + * @category constructors + */ +export const list: (options: Prompt.ListOptions) => Prompt> = InternalListPrompt.list + +/** + * @since 1.0.0 + * @category combinators + */ +export const map: { + (f: (output: Output) => Output2): (self: Prompt) => Prompt + (self: Prompt, f: (output: Output) => Output2): Prompt +} = InternalPrompt.map + +/** + * @since 1.0.0 + * @category constructors + */ +export const password: (options: Prompt.TextOptions) => Prompt = InternalTextPrompt.password + +/** + * Executes the specified `Prompt`. + * + * @since 1.0.0 + * @category execution + */ +export const run: (self: Prompt) => Effect = + InternalPrompt.run + +/** + * @since 1.0.0 + * @category constructors + */ +export const select: (options: Prompt.SelectOptions) => Prompt = InternalSelectPrompt.select + +/** + * @since 1.0.0 + * @category constructors + */ +export const multiSelect: (options: Prompt.SelectOptions & Prompt.MultiSelectOptions) => Prompt> = + InternalMultiSelectPrompt.multiSelect + +/** + * Creates a `Prompt` which immediately succeeds with the specified value. + * + * **NOTE**: This method will not attempt to obtain user input or render + * anything to the screen. + * + * @since 1.0.0 + * @category constructors + */ +export const succeed: (value: A) => Prompt = InternalPrompt.succeed + +/** + * @since 1.0.0 + * @category constructors + */ +export const text: (options: Prompt.TextOptions) => Prompt = InternalTextPrompt.text + +/** + * @since 1.0.0 + * @category constructors + */ +export const toggle: (options: Prompt.ToggleOptions) => Prompt = InternalTogglePrompt.toggle diff --git a/repos/effect/packages/cli/src/Usage.ts b/repos/effect/packages/cli/src/Usage.ts new file mode 100644 index 0000000..75a5454 --- /dev/null +++ b/repos/effect/packages/cli/src/Usage.ts @@ -0,0 +1,141 @@ +/** + * @since 1.0.0 + */ +import type { Option } from "effect/Option" +import type { CliConfig } from "./CliConfig.js" +import type { HelpDoc } from "./HelpDoc.js" +import type { Span } from "./HelpDoc/Span.js" +import * as InternalUsage from "./internal/usage.js" + +/** + * @since 1.0.0 + * @category models + */ +export type Usage = Empty | Mixed | Named | Optional | Repeated | Alternation | Concat + +/** + * @since 1.0.0 + * @category models + */ +export interface Empty { + readonly _tag: "Empty" +} + +/** + * @since 1.0.0 + * @category models + */ +export interface Mixed { + readonly _tag: "Mixed" +} + +/** + * @since 1.0.0 + * @category models + */ +export interface Named { + readonly _tag: "Named" + readonly names: ReadonlyArray + readonly acceptedValues: Option +} + +/** + * @since 1.0.0 + * @category models + */ +export interface Optional { + readonly _tag: "Optional" + readonly usage: Usage +} + +/** + * @since 1.0.0 + * @category models + */ +export interface Repeated { + readonly _tag: "Repeated" + readonly usage: Usage +} + +/** + * @since 1.0.0 + * @category models + */ +export interface Alternation { + readonly _tag: "Alternation" + readonly left: Usage + readonly right: Usage +} + +/** + * @since 1.0.0 + * @category models + */ +export interface Concat { + readonly _tag: "Concat" + readonly left: Usage + readonly right: Usage +} + +/** + * @since 1.0.0 + * @category combinators + */ +export const alternation: { + (that: Usage): (self: Usage) => Usage + (self: Usage, that: Usage): Usage +} = InternalUsage.alternation + +/** + * @since 1.0.0 + * @category combinators + */ +export const concat: { + (that: Usage): (self: Usage) => Usage + (self: Usage, that: Usage): Usage +} = InternalUsage.concat + +/** + * @since 1.0.0 + * @category constructors + */ +export const empty: Usage = InternalUsage.empty + +/** + * @since 1.0.0 + * @category constructors + */ +export const enumerate: { + (config: CliConfig): (self: Usage) => Array + (self: Usage, config: CliConfig): Array +} = InternalUsage.enumerate + +/** + * @since 1.0.0 + * @category combinators + */ +export const getHelp: (self: Usage) => HelpDoc = InternalUsage.getHelp + +/** + * @since 1.0.0 + * @category constructors + */ +export const mixed: Usage = InternalUsage.mixed + +/** + * @since 1.0.0 + * @category constructors + */ +export const named: (names: ReadonlyArray, acceptedValues: Option) => Usage = InternalUsage.named + +/** + * @since 1.0.0 + * @category combinators + */ +export const optional: (self: Usage) => Usage = InternalUsage.optional + +/** + * @since 1.0.0 + * @category combinators + */ +export const repeated: (self: Usage) => Usage = InternalUsage.repeated diff --git a/repos/effect/packages/cli/src/ValidationError.ts b/repos/effect/packages/cli/src/ValidationError.ts new file mode 100644 index 0000000..30727ea --- /dev/null +++ b/repos/effect/packages/cli/src/ValidationError.ts @@ -0,0 +1,303 @@ +/** + * @since 1.0.0 + */ +import type { Command } from "./CommandDescriptor.js" +import type { HelpDoc } from "./HelpDoc.js" +import * as InternalCommand from "./internal/commandDescriptor.js" +import * as InternalValidationError from "./internal/validationError.js" + +/** + * @since 1.0.0 + * @category symbols + */ +export const ValidationErrorTypeId: unique symbol = InternalValidationError.ValidationErrorTypeId + +/** + * @since 1.0.0 + * @category symbols + */ +export type ValidationErrorTypeId = typeof ValidationErrorTypeId + +/** + * @since 1.0.0 + * @category models + */ +export type ValidationError = + | CommandMismatch + | CorrectedFlag + | HelpRequested + | InvalidArgument + | InvalidValue + | MissingValue + | MissingFlag + | MultipleValuesDetected + | MissingSubcommand + | NoBuiltInMatch + | UnclusteredFlag + +/** + * @since 1.0.0 + * @category models + */ +export interface CommandMismatch extends ValidationError.Proto { + readonly _tag: "CommandMismatch" + readonly error: HelpDoc +} + +/** + * @since 1.0.0 + * @category models + */ +export interface CorrectedFlag extends ValidationError.Proto { + readonly _tag: "CorrectedFlag" + readonly error: HelpDoc +} + +/** + * @since 1.0.0 + * @category models + */ +export interface HelpRequested extends ValidationError.Proto { + readonly _tag: "HelpRequested" + readonly error: HelpDoc + readonly command: Command +} + +/** + * @since 1.0.0 + * @category models + */ +export interface InvalidArgument extends ValidationError.Proto { + readonly _tag: "InvalidArgument" + readonly error: HelpDoc +} + +/** + * @since 1.0.0 + * @category models + */ +export interface InvalidValue extends ValidationError.Proto { + readonly _tag: "InvalidValue" + readonly error: HelpDoc +} + +/** + * @since 1.0.0 + * @category models + */ +export interface MissingFlag extends ValidationError.Proto { + readonly _tag: "MissingFlag" + readonly error: HelpDoc +} + +/** + * @since 1.0.0 + * @category models + */ +export interface MissingValue extends ValidationError.Proto { + readonly _tag: "MissingValue" + readonly error: HelpDoc +} + +/** + * @since 1.0.0 + * @category models + */ +export interface MissingSubcommand extends ValidationError.Proto { + readonly _tag: "MissingSubcommand" + readonly error: HelpDoc +} + +/** + * @since 1.0.0 + * @category models + */ +export interface MultipleValuesDetected extends ValidationError.Proto { + readonly _tag: "MultipleValuesDetected" + readonly error: HelpDoc + readonly values: ReadonlyArray +} + +/** + * @since 1.0.0 + * @category models + */ +export interface NoBuiltInMatch extends ValidationError.Proto { + readonly _tag: "NoBuiltInMatch" + readonly error: HelpDoc +} + +/** + * @since 1.0.0 + * @category models + */ +export interface UnclusteredFlag extends ValidationError.Proto { + readonly _tag: "UnclusteredFlag" + readonly error: HelpDoc + readonly unclustered: ReadonlyArray + readonly rest: ReadonlyArray +} + +/** + * @since 1.0.0 + */ +export declare namespace ValidationError { + /** + * @since 1.0.0 + * @category models + */ + export interface Proto { + readonly [ValidationErrorTypeId]: ValidationErrorTypeId + } +} + +/** + * @since 1.0.0 + * @category refinements + */ +export const isValidationError: (u: unknown) => u is ValidationError = InternalValidationError.isValidationError + +/** + * @since 1.0.0 + * @category refinements + */ +export const isCommandMismatch: (self: ValidationError) => self is CommandMismatch = + InternalValidationError.isCommandMismatch + +/** + * @since 1.0.0 + * @category refinements + */ +export const isCorrectedFlag: (self: ValidationError) => self is CorrectedFlag = InternalValidationError.isCorrectedFlag + +/** + * @since 1.0.0 + * @category refinements + */ +export const isHelpRequested: (self: ValidationError) => self is HelpRequested = InternalValidationError.isHelpRequested + +/** + * @since 1.0.0 + * @category refinements + */ +export const isInvalidArgument: (self: ValidationError) => self is InvalidArgument = + InternalValidationError.isInvalidArgument + +/** + * @since 1.0.0 + * @category refinements + */ +export const isInvalidValue: (self: ValidationError) => self is InvalidValue = InternalValidationError.isInvalidValue + +/** + * @since 1.0.0 + * @category refinements + */ +export const isMultipleValuesDetected: (self: ValidationError) => self is MultipleValuesDetected = + InternalValidationError.isMultipleValuesDetected + +/** + * @since 1.0.0 + * @category refinements + */ +export const isMissingFlag: (self: ValidationError) => self is MissingFlag = InternalValidationError.isMissingFlag + +/** + * @since 1.0.0 + * @category refinements + */ +export const isMissingValue: (self: ValidationError) => self is MissingValue = InternalValidationError.isMissingValue + +/** + * @since 1.0.0 + * @category refinements + */ +export const isMissingSubcommand: (self: ValidationError) => self is MissingSubcommand = + InternalValidationError.isMissingSubcommand + +/** + * @since 1.0.0 + * @category refinements + */ +export const isNoBuiltInMatch: (self: ValidationError) => self is NoBuiltInMatch = + InternalValidationError.isNoBuiltInMatch + +/** + * @since 1.0.0 + * @category refinements + */ +export const isUnclusteredFlag: (self: ValidationError) => self is UnclusteredFlag = + InternalValidationError.isUnclusteredFlag + +/** + * @since 1.0.0 + * @category constructors + */ +export const commandMismatch: (error: HelpDoc) => ValidationError = InternalValidationError.commandMismatch + +/** + * @since 1.0.0 + * @category constructors + */ +export const correctedFlag: (error: HelpDoc) => ValidationError = InternalValidationError.correctedFlag + +/** + * @since 1.0.0 + * @category constructors + */ +export const helpRequested: (command: Command) => ValidationError = InternalCommand.helpRequestedError + +/** + * @since 1.0.0 + * @category constructors + */ +export const invalidArgument: (error: HelpDoc) => ValidationError = InternalValidationError.invalidArgument + +/** + * @since 1.0.0 + * @category constructors + */ +export const invalidValue: (error: HelpDoc) => ValidationError = InternalValidationError.invalidValue + +/** + * @since 1.0.0 + * @category constructors + */ +export const keyValuesDetected: ( + error: HelpDoc, + keyValues: ReadonlyArray +) => ValidationError = InternalValidationError.multipleValuesDetected + +/** + * @since 1.0.0 + * @category constructors + */ +export const missingFlag: (error: HelpDoc) => ValidationError = InternalValidationError.missingFlag + +/** + * @since 1.0.0 + * @category constructors + */ +export const missingValue: (error: HelpDoc) => ValidationError = InternalValidationError.missingValue + +/** + * @since 1.0.0 + * @category constructors + */ +export const missingSubcommand: (error: HelpDoc) => ValidationError = InternalValidationError.missingSubcommand + +/** + * @since 1.0.0 + * @category constructors + */ +export const noBuiltInMatch: (error: HelpDoc) => ValidationError = InternalValidationError.noBuiltInMatch + +/** + * @since 1.0.0 + * @category constructors + */ +export const unclusteredFlag: ( + error: HelpDoc, + unclustered: ReadonlyArray, + rest: ReadonlyArray +) => ValidationError = InternalValidationError.unclusteredFlag diff --git a/repos/effect/packages/cli/src/index.ts b/repos/effect/packages/cli/src/index.ts new file mode 100644 index 0000000..e3dd47d --- /dev/null +++ b/repos/effect/packages/cli/src/index.ts @@ -0,0 +1,79 @@ +/** + * @since 1.0.0 + */ +export * as Args from "./Args.js" + +/** + * @since 1.0.0 + */ +export * as AutoCorrect from "./AutoCorrect.js" + +/** + * @since 1.0.0 + */ +export * as BuiltInOptions from "./BuiltInOptions.js" + +/** + * @since 1.0.0 + */ +export * as CliApp from "./CliApp.js" + +/** + * @since 1.0.0 + */ +export * as CliConfig from "./CliConfig.js" + +/** + * @since 1.0.0 + */ +export * as Command from "./Command.js" + +/** + * @since 1.0.0 + */ +export * as CommandDescriptor from "./CommandDescriptor.js" + +/** + * @since 1.0.0 + */ +export * as CommandDirective from "./CommandDirective.js" + +/** + * @since 2.0.0 + */ +export * as ConfigFile from "./ConfigFile.js" + +/** + * @since 1.0.0 + */ +export * as HelpDoc from "./HelpDoc.js" + +/** + * @since 1.0.0 + */ +export * as Options from "./Options.js" + +/** + * @since 1.0.0 + */ +export * as Primitive from "./Primitive.js" + +/** + * @since 1.0.0 + */ +export * as Prompt from "./Prompt.js" + +/** + * @since 1.0.0 + */ +export * as Usage from "./Usage.js" + +/** + * @since 1.0.0 + */ +export * as ValidationError from "./ValidationError.js" + +/** + * @since 1.0.0 + */ +export * as Span from "./HelpDoc/Span.js" diff --git a/repos/effect/packages/cli/src/internal/args.ts b/repos/effect/packages/cli/src/internal/args.ts new file mode 100644 index 0000000..892af09 --- /dev/null +++ b/repos/effect/packages/cli/src/internal/args.ts @@ -0,0 +1,1111 @@ +import type * as FileSystem from "@effect/platform/FileSystem" +import type * as Path from "@effect/platform/Path" +import type * as Terminal from "@effect/platform/Terminal" +import * as Arr from "effect/Array" +import type * as Config from "effect/Config" +import * as ConfigError from "effect/ConfigError" +import * as Console from "effect/Console" +import * as Effect from "effect/Effect" +import * as Either from "effect/Either" +import { dual, pipe } from "effect/Function" +import * as Inspectable from "effect/Inspectable" +import * as Option from "effect/Option" +import * as ParseResult from "effect/ParseResult" +import { pipeArguments } from "effect/Pipeable" +import * as Predicate from "effect/Predicate" +import type * as Redacted from "effect/Redacted" +import * as Ref from "effect/Ref" +import type * as Schema from "effect/Schema" +import type * as Secret from "effect/Secret" +import type * as Args from "../Args.js" +import type * as CliConfig from "../CliConfig.js" +import type * as HelpDoc from "../HelpDoc.js" +import type * as Primitive from "../Primitive.js" +import type * as Usage from "../Usage.js" +import type * as ValidationError from "../ValidationError.js" +import * as InternalFiles from "./files.js" +import * as InternalHelpDoc from "./helpDoc.js" +import * as InternalSpan from "./helpDoc/span.js" +import * as InternalPrimitive from "./primitive.js" +import * as InternalNumberPrompt from "./prompt/number.js" +import * as InternalSelectPrompt from "./prompt/select.js" +import * as InternalUsage from "./usage.js" +import * as InternalValidationError from "./validationError.js" + +const ArgsSymbolKey = "@effect/cli/Args" + +/** @internal */ +export const ArgsTypeId: Args.ArgsTypeId = Symbol.for( + ArgsSymbolKey +) as Args.ArgsTypeId + +/** @internal */ +export type Op = Args.Args & Body & { + readonly _tag: Tag +} + +const proto = { + [ArgsTypeId]: { + _A: (_: never) => _ + }, + pipe() { + return pipeArguments(this, arguments) + } +} + +/** @internal */ +export type Instruction = + | Empty + | Single + | Map + | Both + | Variadic + | WithDefault + | WithFallbackConfig + +/** @internal */ +export interface Empty extends Op<"Empty", {}> {} + +/** @internal */ +export interface Single extends + Op<"Single", { + readonly name: string + readonly pseudoName: Option.Option + readonly primitiveType: Primitive.Primitive + readonly description: HelpDoc.HelpDoc + }> +{} + +/** @internal */ +export interface Map extends + Op<"Map", { + readonly args: Args.Args + readonly f: (value: unknown) => Effect.Effect< + unknown, + HelpDoc.HelpDoc, + FileSystem.FileSystem | Path.Path | Terminal.Terminal + > + }> +{} + +/** @internal */ +export interface Both extends + Op<"Both", { + readonly left: Args.Args + readonly right: Args.Args + }> +{} + +/** @internal */ +export interface Variadic extends + Op<"Variadic", { + readonly args: Args.Args + readonly min: Option.Option + readonly max: Option.Option + }> +{} + +/** @internal */ +export interface WithDefault extends + Op<"WithDefault", { + readonly args: Args.Args + readonly fallback: unknown + }> +{} + +/** @internal */ +export interface WithFallbackConfig extends + Op<"WithFallbackConfig", { + readonly args: Args.Args + readonly config: Config.Config + }> +{} + +// ============================================================================= +// Refinements +// ============================================================================= + +/** @internal */ +export const isArgs = (u: unknown): u is Args.Args => typeof u === "object" && u != null && ArgsTypeId in u + +/** @internal */ +export const isInstruction = <_>(self: Args.Args<_>): self is Instruction => self as any + +/** @internal */ +export const isEmpty = (self: Instruction): self is Empty => self._tag === "Empty" + +/** @internal */ +export const isSingle = (self: Instruction): self is Single => self._tag === "Single" + +/** @internal */ +export const isBoth = (self: Instruction): self is Both => self._tag === "Both" + +/** @internal */ +export const isMap = (self: Instruction): self is Map => self._tag === "Map" + +/** @internal */ +export const isVariadic = (self: Instruction): self is Variadic => self._tag === "Variadic" + +/** @internal */ +export const isWithDefault = (self: Instruction): self is WithDefault => self._tag === "WithDefault" + +/** @internal */ +export const isWithFallbackConfig = (self: Instruction): self is WithFallbackConfig => + self._tag === "WithFallbackConfig" + +// ============================================================================= +// Constructors +// ============================================================================= + +/** @internal */ +export const all: < + const Arg extends Iterable> | Record> +>(arg: Arg) => Args.All.Return = function() { + if (arguments.length === 1) { + if (isArgs(arguments[0])) { + return map(arguments[0], (x) => [x]) as any + } else if (Arr.isArray(arguments[0])) { + return allTupled(arguments[0] as Array) as any + } else { + const entries = Object.entries(arguments[0] as Readonly<{ [K: string]: Args.Args }>) + let result = map(entries[0][1], (value) => ({ [entries[0][0]]: value })) + if (entries.length === 1) { + return result as any + } + const rest = entries.slice(1) + for (const [key, options] of rest) { + result = map(makeBoth(result, options), ([record, value]) => ({ + ...record, + [key]: value + })) + } + return result as any + } + } + return allTupled(arguments[0]) as any +} + +/** @internal */ +export const boolean = (config?: Args.Args.BaseArgsConfig): Args.Args => + makeSingle(Option.fromNullable(config?.name), InternalPrimitive.boolean(Option.none())) + +/** @internal */ +export const choice = ( + choices: ReadonlyArray<[string, A]>, + config?: Args.Args.BaseArgsConfig +): Args.Args => makeSingle(Option.fromNullable(config?.name), InternalPrimitive.choice(choices)) + +/** @internal */ +export const date = (config?: Args.Args.BaseArgsConfig): Args.Args => + makeSingle(Option.fromNullable(config?.name), InternalPrimitive.date) + +/** @internal */ +export const directory = (config?: Args.Args.PathArgsConfig): Args.Args => + makeSingle( + Option.fromNullable(config?.name), + InternalPrimitive.path("directory", config?.exists || "either") + ) + +/** @internal */ +export const file = (config?: Args.Args.PathArgsConfig): Args.Args => + makeSingle( + Option.fromNullable(config?.name), + InternalPrimitive.path("file", config?.exists || "either") + ) + +/** @internal */ +export const fileContent = ( + config?: Args.Args.BaseArgsConfig +): Args.Args => + mapEffect( + file({ ...config, exists: "yes" }), + (path) => Effect.mapError(InternalFiles.read(path), (e) => InternalHelpDoc.p(e)) + ) + +/** @internal */ +export const fileParse = ( + config?: Args.Args.FormatArgsConfig +): Args.Args => + mapEffect(fileText(config), ([path, content]) => + Effect.mapError( + InternalFiles.parse(path, content, config?.format), + (e) => InternalHelpDoc.p(e) + )) + +/** @internal */ +export const fileSchema = ( + schema: Schema.Schema, + config?: Args.Args.FormatArgsConfig +): Args.Args => withSchema(fileParse(config), schema) + +/** @internal */ +export const fileText = ( + config?: Args.Args.BaseArgsConfig +): Args.Args => + mapEffect(file({ ...config, exists: "yes" }), (path) => + Effect.mapError( + InternalFiles.readString(path), + (e) => InternalHelpDoc.p(e) + )) + +/** @internal */ +export const float = (config?: Args.Args.BaseArgsConfig): Args.Args => + makeSingle(Option.fromNullable(config?.name), InternalPrimitive.float) + +/** @internal */ +export const integer = (config?: Args.Args.BaseArgsConfig): Args.Args => + makeSingle(Option.fromNullable(config?.name), InternalPrimitive.integer) + +/** @internal */ +export const none: Args.Args = (() => { + const op = Object.create(proto) + op._tag = "Empty" + return op +})() + +/** @internal */ +export const path = (config?: Args.Args.PathArgsConfig): Args.Args => + makeSingle( + Option.fromNullable(config?.name), + InternalPrimitive.path("either", config?.exists || "either") + ) + +/** @internal */ +export const redacted = ( + config?: Args.Args.BaseArgsConfig +): Args.Args => makeSingle(Option.fromNullable(config?.name), InternalPrimitive.redacted) + +/** @internal */ +export const secret = ( + config?: Args.Args.BaseArgsConfig +): Args.Args => makeSingle(Option.fromNullable(config?.name), InternalPrimitive.secret) + +/** @internal */ +export const text = (config?: Args.Args.BaseArgsConfig): Args.Args => + makeSingle(Option.fromNullable(config?.name), InternalPrimitive.text) + +// ============================================================================= +// Combinators +// ============================================================================= + +/** @internal */ +export const atLeast = dual< + { + (times: 0): (self: Args.Args) => Args.Args> + (times: number): (self: Args.Args) => Args.Args> + }, + { + (self: Args.Args, times: 0): Args.Args> + (self: Args.Args, times: number): Args.Args> + } +>(2, (self, times) => makeVariadic(self, Option.some(times), Option.none()) as any) + +/** @internal */ +export const atMost = dual< + (times: number) => (self: Args.Args) => Args.Args>, + (self: Args.Args, times: number) => Args.Args> +>(2, (self, times) => makeVariadic(self, Option.none(), Option.some(times))) + +/** @internal */ +export const between = dual< + { + (min: 0, max: number): (self: Args.Args) => Args.Args> + ( + min: number, + max: number + ): (self: Args.Args) => Args.Args> + }, + { + (self: Args.Args, min: 0, max: number): Args.Args> + ( + self: Args.Args, + min: number, + max: number + ): Args.Args> + } +>(3, (self, min, max) => makeVariadic(self, Option.some(min), Option.some(max)) as any) + +/** @internal */ +export const getHelp = (self: Args.Args): HelpDoc.HelpDoc => getHelpInternal(self as Instruction) + +/** @internal */ +export const getIdentifier = (self: Args.Args): Option.Option => + getIdentifierInternal(self as Instruction) + +/** @internal */ +export const getMinSize = (self: Args.Args): number => getMinSizeInternal(self as Instruction) + +/** @internal */ +export const getMaxSize = (self: Args.Args): number => getMaxSizeInternal(self as Instruction) + +/** @internal */ +export const getUsage = (self: Args.Args): Usage.Usage => getUsageInternal(self as Instruction) + +/** @internal */ +export const map = dual< + (f: (a: A) => B) => (self: Args.Args) => Args.Args, + (self: Args.Args, f: (a: A) => B) => Args.Args +>(2, (self, f) => mapEffect(self, (a) => Effect.succeed(f(a)))) + +/** @internal */ +export const mapEffect = dual< + ( + f: (a: A) => Effect.Effect + ) => (self: Args.Args) => Args.Args, + ( + self: Args.Args, + f: (a: A) => Effect.Effect + ) => Args.Args +>(2, (self, f) => makeMap(self, f)) + +/** @internal */ +export const mapTryCatch = dual< + ( + f: (a: A) => B, + onError: (e: unknown) => HelpDoc.HelpDoc + ) => (self: Args.Args) => Args.Args, + ( + self: Args.Args, + f: (a: A) => B, + onError: (e: unknown) => HelpDoc.HelpDoc + ) => Args.Args +>(3, (self, f, onError) => + mapEffect(self, (a) => { + try { + return Either.right(f(a)) + } catch (e) { + return Either.left(onError(e)) + } + })) + +/** @internal */ +export const optional = (self: Args.Args): Args.Args> => + makeWithDefault(map(self, Option.some), Option.none()) + +/** @internal */ +export const repeated = (self: Args.Args): Args.Args> => makeVariadic(self, Option.none(), Option.none()) + +/** @internal */ +export const validate = dual< + ( + args: ReadonlyArray, + config: CliConfig.CliConfig + ) => (self: Args.Args) => Effect.Effect< + [Array, A], + ValidationError.ValidationError, + FileSystem.FileSystem | Path.Path | Terminal.Terminal + >, + ( + self: Args.Args, + args: ReadonlyArray, + config: CliConfig.CliConfig + ) => Effect.Effect< + [Array, A], + ValidationError.ValidationError, + FileSystem.FileSystem | Path.Path | Terminal.Terminal + > +>(3, (self, args, config) => validateInternal(self as Instruction, args, config)) + +/** @internal */ +export const withDefault = dual< + (fallback: B) => (self: Args.Args) => Args.Args, + (self: Args.Args, fallback: B) => Args.Args +>(2, (self, fallback) => makeWithDefault(self, fallback)) + +/** @internal */ +export const withFallbackConfig: { + (config: Config.Config): (self: Args.Args) => Args.Args + (self: Args.Args, config: Config.Config): Args.Args +} = dual< + (config: Config.Config) => (self: Args.Args) => Args.Args, + (self: Args.Args, config: Config.Config) => Args.Args +>(2, (self, config) => { + if (isInstruction(self) && isWithDefault(self)) { + return makeWithDefault( + withFallbackConfig(self.args, config), + self.fallback as any + ) + } + return makeWithFallbackConfig(self, config) +}) + +/** @internal */ +export const withSchema = dual< + ( + schema: Schema.Schema + ) => (self: Args.Args) => Args.Args, + ( + self: Args.Args, + schema: Schema.Schema + ) => Args.Args +>(2, (self, schema) => { + const decode = ParseResult.decode(schema) + return mapEffect(self, (_) => + Effect.mapError( + decode(_ as any), + (issue) => InternalHelpDoc.p(ParseResult.TreeFormatter.formatIssueSync(issue)) + )) +}) + +/** @internal */ +export const withDescription = dual< + (description: string) => (self: Args.Args) => Args.Args, + (self: Args.Args, description: string) => Args.Args +>(2, (self, description) => withDescriptionInternal(self as Instruction, description)) + +/** @internal */ +export const wizard = dual< + (config: CliConfig.CliConfig) => (self: Args.Args) => Effect.Effect< + Array, + Terminal.QuitException | ValidationError.ValidationError, + FileSystem.FileSystem | Path.Path | Terminal.Terminal + >, + (self: Args.Args, config: CliConfig.CliConfig) => Effect.Effect< + Array, + Terminal.QuitException | ValidationError.ValidationError, + FileSystem.FileSystem | Path.Path | Terminal.Terminal + > +>(2, (self, config) => wizardInternal(self as Instruction, config)) + +// ============================================================================= +// Internals +// ============================================================================= + +const allTupled = >>(arg: T): Args.Args< + { + [K in keyof T]: [T[K]] extends [Args.Args] ? A : never + } +> => { + if (arg.length === 0) { + return none as any + } + if (arg.length === 1) { + return map(arg[0], (x) => [x]) as any + } + let result = map(arg[0], (x) => [x]) + for (let i = 1; i < arg.length; i++) { + const curr = arg[i] + result = map(makeBoth(result, curr), ([a, b]) => [...a, b]) + } + return result as any +} + +const getHelpInternal = (self: Instruction): HelpDoc.HelpDoc => { + switch (self._tag) { + case "Empty": { + return InternalHelpDoc.empty + } + case "Single": { + return InternalHelpDoc.descriptionList([[ + InternalSpan.weak(self.name), + InternalHelpDoc.sequence( + InternalHelpDoc.p(InternalPrimitive.getHelp(self.primitiveType)), + self.description + ) + ]]) + } + case "Map": { + return getHelpInternal(self.args as Instruction) + } + case "Both": { + return InternalHelpDoc.sequence( + getHelpInternal(self.left as Instruction), + getHelpInternal(self.right as Instruction) + ) + } + case "Variadic": { + const help = getHelpInternal(self.args as Instruction) + return InternalHelpDoc.mapDescriptionList(help, (oldSpan, oldBlock) => { + const min = getMinSizeInternal(self as Instruction) + const max = getMaxSizeInternal(self as Instruction) + const newSpan = InternalSpan.text( + Option.isSome(self.max) ? ` ${min} - ${max}` : min === 0 ? "..." : ` ${min}+` + ) + const newBlock = InternalHelpDoc.p( + Option.isSome(self.max) + ? `This argument must be repeated at least ${min} times and may be repeated up to ${max} times.` + : min === 0 + ? "This argument may be repeated zero or more times." + : `This argument must be repeated at least ${min} times.` + ) + return [InternalSpan.concat(oldSpan, newSpan), InternalHelpDoc.sequence(oldBlock, newBlock)] + }) + } + case "WithDefault": { + return InternalHelpDoc.mapDescriptionList( + getHelpInternal(self.args as Instruction), + (span, block) => { + const optionalDescription = Option.isOption(self.fallback) + ? Option.match(self.fallback, { + onNone: () => InternalHelpDoc.p("This setting is optional."), + onSome: (fallbackValue) => { + const inspectableValue = Predicate.isObject(fallbackValue) ? fallbackValue : String(fallbackValue) + const displayValue = Inspectable.toStringUnknown(inspectableValue, 0) + return InternalHelpDoc.p(`This setting is optional. Defaults to: ${displayValue}`) + } + }) + : InternalHelpDoc.p("This setting is optional.") + return [span, InternalHelpDoc.sequence(block, optionalDescription)] + } + ) + } + case "WithFallbackConfig": { + return InternalHelpDoc.mapDescriptionList( + getHelpInternal(self.args as Instruction), + (span, block) => [ + span, + InternalHelpDoc.sequence( + block, + InternalHelpDoc.p( + "This argument can be set from environment variables." + ) + ) + ] + ) + } + } +} + +const getIdentifierInternal = (self: Instruction): Option.Option => { + switch (self._tag) { + case "Empty": { + return Option.none() + } + case "Single": { + return Option.some(self.name) + } + case "Map": + case "Variadic": + case "WithDefault": + case "WithFallbackConfig": { + return getIdentifierInternal(self.args as Instruction) + } + case "Both": { + const ids = Arr.getSomes([ + getIdentifierInternal(self.left as Instruction), + getIdentifierInternal(self.right as Instruction) + ]) + return Arr.match(ids, { + onEmpty: () => Option.none(), + onNonEmpty: (ids) => Option.some(Arr.join(ids, ", ")) + }) + } + } +} + +const getMinSizeInternal = (self: Instruction): number => { + switch (self._tag) { + case "Empty": + case "WithDefault": + case "WithFallbackConfig": { + return 0 + } + case "Single": { + return 1 + } + case "Map": { + return getMinSizeInternal(self.args as Instruction) + } + case "Both": { + const leftMinSize = getMinSizeInternal(self.left as Instruction) + const rightMinSize = getMinSizeInternal(self.right as Instruction) + return leftMinSize + rightMinSize + } + case "Variadic": { + const argsMinSize = getMinSizeInternal(self.args as Instruction) + return Math.floor(Option.getOrElse(self.min, () => 0) * argsMinSize) + } + } +} + +const getMaxSizeInternal = (self: Instruction): number => { + switch (self._tag) { + case "Empty": { + return 0 + } + case "Single": { + return 1 + } + case "Map": + case "WithDefault": + case "WithFallbackConfig": { + return getMaxSizeInternal(self.args as Instruction) + } + case "Both": { + const leftMaxSize = getMaxSizeInternal(self.left as Instruction) + const rightMaxSize = getMaxSizeInternal(self.right as Instruction) + return leftMaxSize + rightMaxSize + } + case "Variadic": { + const argsMaxSize = getMaxSizeInternal(self.args as Instruction) + return Math.floor(Option.getOrElse(self.max, () => Number.MAX_SAFE_INTEGER / 2) * argsMaxSize) + } + } +} + +const getUsageInternal = (self: Instruction): Usage.Usage => { + switch (self._tag) { + case "Empty": { + return InternalUsage.empty + } + case "Single": { + return InternalUsage.named( + Arr.of(self.name), + InternalPrimitive.getChoices(self.primitiveType) + ) + } + case "Map": { + return getUsageInternal(self.args as Instruction) + } + case "Both": { + return InternalUsage.concat( + getUsageInternal(self.left as Instruction), + getUsageInternal(self.right as Instruction) + ) + } + case "Variadic": { + return InternalUsage.repeated(getUsageInternal(self.args as Instruction)) + } + case "WithDefault": + case "WithFallbackConfig": { + return InternalUsage.optional(getUsageInternal(self.args as Instruction)) + } + } +} + +const makeSingle = ( + pseudoName: Option.Option, + primitiveType: Primitive.Primitive, + description: HelpDoc.HelpDoc = InternalHelpDoc.empty +): Args.Args => { + const op = Object.create(proto) + op._tag = "Single" + op.name = `<${Option.getOrElse(pseudoName, () => InternalPrimitive.getTypeName(primitiveType))}>` + op.pseudoName = pseudoName + op.primitiveType = primitiveType + op.description = description + return op +} + +const makeMap = ( + self: Args.Args, + f: (value: A) => Effect.Effect +): Args.Args => { + const op = Object.create(proto) + op._tag = "Map" + op.args = self + op.f = f + return op +} + +const makeBoth = (left: Args.Args, right: Args.Args): Args.Args<[A, B]> => { + const op = Object.create(proto) + op._tag = "Both" + op.left = left + op.right = right + return op +} + +const makeWithDefault = ( + self: Args.Args, + fallback: B +): Args.Args => { + const op = Object.create(proto) + op._tag = "WithDefault" + op.args = self + op.fallback = fallback + return op +} + +const makeWithFallbackConfig = ( + args: Args.Args, + config: Config.Config +): Args.Args => { + const op = Object.create(proto) + op._tag = "WithFallbackConfig" + op.args = args + op.config = config + return op +} + +const makeVariadic = ( + args: Args.Args, + min: Option.Option, + max: Option.Option +): Args.Args> => { + const op = Object.create(proto) + op._tag = "Variadic" + op.args = args + op.min = min + op.max = max + return op +} + +const validateInternal = ( + self: Instruction, + args: ReadonlyArray, + config: CliConfig.CliConfig +): Effect.Effect< + [Array, any], + ValidationError.ValidationError, + FileSystem.FileSystem | Path.Path | Terminal.Terminal +> => { + switch (self._tag) { + case "Empty": { + return Effect.succeed([args as Array, undefined]) + } + case "Single": { + return Effect.suspend(() => { + return Arr.matchLeft(args, { + onEmpty: () => { + const choices = InternalPrimitive.getChoices(self.primitiveType) + if (Option.isSome(self.pseudoName) && Option.isSome(choices)) { + return Effect.fail(InternalValidationError.missingValue(InternalHelpDoc.p( + `Missing argument <${self.pseudoName.value}> with choices ${choices.value}` + ))) + } + if (Option.isSome(self.pseudoName)) { + return Effect.fail(InternalValidationError.missingValue(InternalHelpDoc.p( + `Missing argument <${self.pseudoName.value}>` + ))) + } + if (Option.isSome(choices)) { + return Effect.fail(InternalValidationError.missingValue(InternalHelpDoc.p( + `Missing argument ${InternalPrimitive.getTypeName(self.primitiveType)} with choices ${choices.value}` + ))) + } + return Effect.fail(InternalValidationError.missingValue(InternalHelpDoc.p( + `Missing argument ${InternalPrimitive.getTypeName(self.primitiveType)}` + ))) + }, + onNonEmpty: (head, tail) => + InternalPrimitive.validate(self.primitiveType, Option.some(head), config).pipe( + Effect.mapBoth({ + onFailure: (text) => InternalValidationError.invalidArgument(InternalHelpDoc.p(text)), + onSuccess: (a) => [tail, a] + }) + ) + }) + }) + } + case "Map": { + return validateInternal(self.args as Instruction, args, config).pipe( + Effect.flatMap(([leftover, a]) => + Effect.matchEffect(self.f(a), { + onFailure: (doc) => Effect.fail(InternalValidationError.invalidArgument(doc)), + onSuccess: (b) => Effect.succeed([leftover, b]) + }) + ) + ) + } + case "Both": { + return validateInternal(self.left as Instruction, args, config).pipe( + Effect.flatMap(([args, a]) => + validateInternal(self.right as Instruction, args, config).pipe( + Effect.map(([args, b]) => [args, [a, b]]) + ) + ) + ) + } + case "Variadic": { + const min1 = Option.getOrElse(self.min, () => 0) + const max1 = Option.getOrElse(self.max, () => Number.MAX_SAFE_INTEGER) + const loop = ( + args: ReadonlyArray, + acc: ReadonlyArray + ): Effect.Effect< + [ReadonlyArray, ReadonlyArray], + ValidationError.ValidationError, + FileSystem.FileSystem | Path.Path | Terminal.Terminal + > => { + if (acc.length >= max1) { + return Effect.succeed([args, acc]) + } + return validateInternal(self.args as Instruction, args, config).pipe(Effect.matchEffect({ + onFailure: (failure) => + acc.length >= min1 && Arr.isEmptyReadonlyArray(args) + ? Effect.succeed([args, acc]) + : Effect.fail(failure), + onSuccess: ([args, a]) => loop(args, Arr.append(acc, a)) + })) + } + return loop(args, Arr.empty()).pipe( + Effect.map(([args, acc]) => [args as Array, acc]) + ) + } + case "WithDefault": { + return validateInternal(self.args as Instruction, args, config).pipe( + Effect.catchTag("MissingValue", () => + Effect.succeed<[Array, any]>([ + args as Array, + self.fallback + ])) + ) + } + case "WithFallbackConfig": { + return validateInternal(self.args as Instruction, args, config).pipe( + Effect.catchTag("MissingValue", (e) => + Effect.map( + Effect.catchAll(self.config, (e2) => { + if (ConfigError.isMissingDataOnly(e2)) { + const help = InternalHelpDoc.p(String(e2)) + const error = InternalValidationError.invalidValue(help) + return Effect.fail(error) + } + return Effect.fail(e) + }), + (value) => [args, value] as [Array, any] + )) + ) + } + } +} + +const withDescriptionInternal = (self: Instruction, description: string): Args.Args => { + switch (self._tag) { + case "Empty": { + return none + } + case "Single": { + const desc = InternalHelpDoc.sequence(self.description, InternalHelpDoc.p(description)) + return makeSingle(self.pseudoName, self.primitiveType, desc) + } + case "Map": { + return makeMap(withDescriptionInternal(self.args as Instruction, description), self.f) + } + case "Both": { + return makeBoth( + withDescriptionInternal(self.left as Instruction, description), + withDescriptionInternal(self.right as Instruction, description) + ) + } + case "Variadic": { + return makeVariadic( + withDescriptionInternal(self.args as Instruction, description), + self.min, + self.max + ) + } + case "WithDefault": { + return makeWithDefault( + withDescriptionInternal(self.args as Instruction, description), + self.fallback + ) + } + case "WithFallbackConfig": { + return makeWithFallbackConfig( + withDescriptionInternal(self.args as Instruction, description), + self.config + ) + } + } +} + +const wizardInternal = (self: Instruction, config: CliConfig.CliConfig): Effect.Effect< + Array, + Terminal.QuitException | ValidationError.ValidationError, + FileSystem.FileSystem | Path.Path | Terminal.Terminal +> => { + switch (self._tag) { + case "Empty": { + return Effect.succeed(Arr.empty()) + } + case "Single": { + const help = getHelpInternal(self) + return InternalPrimitive.wizard(self.primitiveType, help).pipe( + Effect.zipLeft(Console.log()), + Effect.flatMap((input) => { + const args = Arr.of(input as string) + return validateInternal(self, args, config).pipe(Effect.as(args)) + }) + ) + } + case "Map": { + return wizardInternal(self.args as Instruction, config).pipe( + Effect.tap((args) => validateInternal(self.args as Instruction, args, config)) + ) + } + case "Both": { + return Effect.zipWith( + wizardInternal(self.left as Instruction, config), + wizardInternal(self.right as Instruction, config), + (left, right) => Arr.appendAll(left, right) + ).pipe(Effect.tap((args) => validateInternal(self, args, config))) + } + case "Variadic": { + const repeatHelp = InternalHelpDoc.p( + "How many times should this argument should be repeated?" + ) + const message = pipe( + getHelpInternal(self), + InternalHelpDoc.sequence(repeatHelp) + ) + return InternalNumberPrompt.integer({ + message: InternalHelpDoc.toAnsiText(message).trimEnd(), + min: getMinSizeInternal(self), + max: getMaxSizeInternal(self) + }).pipe( + Effect.zipLeft(Console.log()), + Effect.flatMap((n) => + n <= 0 + ? Effect.succeed(Arr.empty()) + : Ref.make(Arr.empty()).pipe( + Effect.flatMap((ref) => + wizardInternal(self.args as Instruction, config).pipe( + Effect.flatMap((args) => Ref.update(ref, Arr.appendAll(args))), + Effect.repeatN(n - 1), + Effect.zipRight(Ref.get(ref)), + Effect.tap((args) => validateInternal(self, args, config)) + ) + ) + ) + ) + ) + } + case "WithDefault": { + const defaultHelp = InternalHelpDoc.p(`This argument is optional - use the default?`) + const message = pipe( + getHelpInternal(self.args as Instruction), + InternalHelpDoc.sequence(defaultHelp) + ) + return InternalSelectPrompt.select({ + message: InternalHelpDoc.toAnsiText(message).trimEnd(), + choices: [ + { title: `Default ['${JSON.stringify(self.fallback)}']`, value: true }, + { title: "Custom", value: false } + ] + }).pipe( + Effect.zipLeft(Console.log()), + Effect.flatMap((useFallback) => + useFallback + ? Effect.succeed(Arr.empty()) + : wizardInternal(self.args as Instruction, config) + ) + ) + } + case "WithFallbackConfig": { + const defaultHelp = InternalHelpDoc.p(`Try load this option from the environment?`) + const message = pipe( + getHelpInternal(self.args as Instruction), + InternalHelpDoc.sequence(defaultHelp) + ) + return InternalSelectPrompt.select({ + message: InternalHelpDoc.toAnsiText(message).trimEnd(), + choices: [ + { title: `Use environment variables`, value: true }, + { title: "Custom", value: false } + ] + }).pipe( + Effect.zipLeft(Console.log()), + Effect.flatMap((useFallback) => + useFallback + ? Effect.succeed(Arr.empty()) + : wizardInternal(self.args as Instruction, config) + ) + ) + } + } +} + +// ============================================================================= +// Completion Internals +// ============================================================================= + +const getShortDescription = (self: Instruction): string => { + switch (self._tag) { + case "Empty": + case "Both": { + return "" + } + case "Single": { + return InternalSpan.getText(InternalHelpDoc.getSpan(self.description)) + } + case "Map": + case "Variadic": + case "WithDefault": + case "WithFallbackConfig": { + return getShortDescription(self.args as Instruction) + } + } +} + +/** @internal */ +export const getFishCompletions = (self: Instruction): Array => { + switch (self._tag) { + case "Empty": { + return Arr.empty() + } + case "Single": { + const description = getShortDescription(self) + return pipe( + InternalPrimitive.getFishCompletions( + self.primitiveType as InternalPrimitive.Instruction + ), + Arr.appendAll( + description.length === 0 + ? Arr.empty() + : Arr.of(`-d '${description}'`) + ), + Arr.join(" "), + Arr.of + ) + } + case "Both": { + return pipe( + getFishCompletions(self.left as Instruction), + Arr.appendAll(getFishCompletions(self.right as Instruction)) + ) + } + case "Map": + case "Variadic": + case "WithDefault": + case "WithFallbackConfig": { + return getFishCompletions(self.args as Instruction) + } + } +} + +interface ZshCompletionState { + readonly multiple: boolean + readonly optional: boolean +} + +export const getZshCompletions = ( + self: Instruction, + state: ZshCompletionState = { multiple: false, optional: false } +): Array => { + switch (self._tag) { + case "Empty": { + return Arr.empty() + } + case "Single": { + const multiple = state.multiple ? "*" : "" + const optional = state.optional ? "::" : ":" + const shortDescription = getShortDescription(self) + const description = shortDescription.length > 0 ? ` -- ${shortDescription}` : "" + const possibleValues = InternalPrimitive.getZshCompletions( + self.primitiveType as InternalPrimitive.Instruction + ) + return possibleValues.length === 0 + ? Arr.empty() + : Arr.of(`${multiple}${optional}${self.name}${description}${possibleValues}`) + } + case "Map": { + return getZshCompletions(self.args as Instruction, state) + } + case "Both": { + const left = getZshCompletions(self.left as Instruction, state) + const right = getZshCompletions(self.right as Instruction, state) + return Arr.appendAll(left, right) + } + case "Variadic": { + return Option.isSome(self.max) && self.max.value > 1 + ? getZshCompletions(self.args as Instruction, { ...state, multiple: true }) + : getZshCompletions(self.args as Instruction, state) + } + case "WithDefault": + case "WithFallbackConfig": { + return getZshCompletions(self.args as Instruction, { ...state, optional: true }) + } + } +} diff --git a/repos/effect/packages/cli/src/internal/autoCorrect.ts b/repos/effect/packages/cli/src/internal/autoCorrect.ts new file mode 100644 index 0000000..50c3277 --- /dev/null +++ b/repos/effect/packages/cli/src/internal/autoCorrect.ts @@ -0,0 +1,44 @@ +import type * as CliConfig from "../CliConfig.js" +import * as cliConfig from "./cliConfig.js" + +/** @internal */ +export const levensteinDistance = ( + first: string, + second: string, + config: CliConfig.CliConfig +): number => { + if (first.length === 0 && second.length === 0) { + return 0 + } + if (first.length === 0) { + return second.length + } + if (second.length === 0) { + return first.length + } + const rowCount = first.length + const columnCount = second.length + const matrix = new Array>(rowCount) + const normalFirst = cliConfig.normalizeCase(config, first) + const normalSecond = cliConfig.normalizeCase(config, second) + // Increment each row in the first column + for (let x = 0; x <= rowCount; x++) { + matrix[x] = new Array(columnCount) + matrix[x][0] = x + } + // Increment each column in the first row + for (let y = 0; y <= columnCount; y++) { + matrix[0][y] = y + } + // Fill in the rest of the matrix + for (let row = 1; row <= rowCount; row++) { + for (let col = 1; col <= columnCount; col++) { + const cost = normalFirst.charAt(row - 1) === normalSecond.charAt(col - 1) ? 0 : 1 + matrix[row][col] = Math.min( + matrix[row][col - 1] + 1, + Math.min(matrix[row - 1][col] + 1, matrix[row - 1][col - 1] + cost) + ) + } + } + return matrix[rowCount][columnCount] +} diff --git a/repos/effect/packages/cli/src/internal/builtInOptions.ts b/repos/effect/packages/cli/src/internal/builtInOptions.ts new file mode 100644 index 0000000..12819b2 --- /dev/null +++ b/repos/effect/packages/cli/src/internal/builtInOptions.ts @@ -0,0 +1,138 @@ +import * as LogLevel from "effect/LogLevel" +import * as Option from "effect/Option" +import type * as BuiltInOptions from "../BuiltInOptions.js" +import type * as Command from "../CommandDescriptor.js" +import type * as HelpDoc from "../HelpDoc.js" +import type * as Options from "../Options.js" +import type * as Usage from "../Usage.js" +import * as InternalOptions from "./options.js" + +/** @internal */ +export const setLogLevel = ( + level: LogLevel.LogLevel +): BuiltInOptions.BuiltInOptions => ({ + _tag: "SetLogLevel", + level +}) + +/** @internal */ +export const showCompletions = ( + shellType: BuiltInOptions.BuiltInOptions.ShellType +): BuiltInOptions.BuiltInOptions => ({ + _tag: "ShowCompletions", + shellType +}) + +/** @internal */ +export const showHelp = ( + usage: Usage.Usage, + helpDoc: HelpDoc.HelpDoc +): BuiltInOptions.BuiltInOptions => ({ + _tag: "ShowHelp", + usage, + helpDoc +}) + +/** @internal */ +export const showWizard = (command: Command.Command): BuiltInOptions.BuiltInOptions => ({ + _tag: "ShowWizard", + command +}) + +/** @internal */ +export const showVersion: BuiltInOptions.BuiltInOptions = { + _tag: "ShowVersion" +} + +/** @internal */ +export const isShowCompletions = ( + self: BuiltInOptions.BuiltInOptions +): self is BuiltInOptions.ShowCompletions => self._tag === "ShowCompletions" + +/** @internal */ +export const isShowHelp = (self: BuiltInOptions.BuiltInOptions): self is BuiltInOptions.ShowHelp => + self._tag === "ShowHelp" + +/** @internal */ +export const isShowWizard = ( + self: BuiltInOptions.BuiltInOptions +): self is BuiltInOptions.ShowWizard => self._tag === "ShowWizard" + +/** @internal */ +export const isShowVersion = ( + self: BuiltInOptions.BuiltInOptions +): self is BuiltInOptions.ShowVersion => self._tag === "ShowVersion" + +/** @internal */ +export const completionsOptions: Options.Options< + Option.Option +> = InternalOptions.choiceWithValue("completions", [ + ["sh", "bash" as const], + ["bash", "bash" as const], + ["fish", "fish" as const], + ["zsh", "zsh" as const] +]).pipe( + InternalOptions.optional, + InternalOptions.withDescription("Generate a completion script for a specific shell.") +) + +/** @internal */ +export const logLevelOptions: Options.Options< + Option.Option +> = InternalOptions.choiceWithValue( + "log-level", + LogLevel.allLevels.map((level) => [level._tag.toLowerCase(), level] as const) +).pipe( + InternalOptions.optional, + InternalOptions.withDescription("Sets the minimum log level for a command.") +) + +/** @internal */ +export const helpOptions: Options.Options = InternalOptions.boolean("help").pipe( + InternalOptions.withAlias("h"), + InternalOptions.withDescription("Show the help documentation for a command.") +) + +/** @internal */ +export const versionOptions: Options.Options = InternalOptions.boolean("version").pipe( + InternalOptions.withDescription("Show the version of the application.") +) + +/** @internal */ +export const wizardOptions: Options.Options = InternalOptions.boolean("wizard").pipe( + InternalOptions.withDescription("Start wizard mode for a command.") +) + +/** @internal */ +export const builtIns = InternalOptions.all({ + completions: completionsOptions, + logLevel: logLevelOptions, + help: helpOptions, + wizard: wizardOptions, + version: versionOptions +}) + +/** @internal */ +export const builtInOptions = ( + command: Command.Command, + usage: Usage.Usage, + helpDoc: HelpDoc.HelpDoc +): Options.Options> => + InternalOptions.map(builtIns, (builtIn) => { + if (Option.isSome(builtIn.completions)) { + return Option.some(showCompletions(builtIn.completions.value)) + } + if (Option.isSome(builtIn.logLevel)) { + return Option.some(setLogLevel(builtIn.logLevel.value)) + } + if (builtIn.help) { + return Option.some(showHelp(usage, helpDoc)) + } + if (builtIn.wizard) { + return Option.some(showWizard(command)) + } + if (builtIn.version) { + return Option.some(showVersion) + } + return Option.none() + }) diff --git a/repos/effect/packages/cli/src/internal/cliApp.ts b/repos/effect/packages/cli/src/internal/cliApp.ts new file mode 100644 index 0000000..0daf4c7 --- /dev/null +++ b/repos/effect/packages/cli/src/internal/cliApp.ts @@ -0,0 +1,368 @@ +import * as Terminal from "@effect/platform/Terminal" +import * as Color from "@effect/printer-ansi/Color" +import * as Arr from "effect/Array" +import * as Console from "effect/Console" +import * as Context from "effect/Context" +import * as Effect from "effect/Effect" +import { dual, pipe } from "effect/Function" +import * as HashMap from "effect/HashMap" +import * as Logger from "effect/Logger" +import * as Option from "effect/Option" +import { pipeArguments } from "effect/Pipeable" +import * as Unify from "effect/Unify" +import type * as BuiltInOptions from "../BuiltInOptions.js" +import type * as CliApp from "../CliApp.js" +import type * as CliConfig from "../CliConfig.js" +import type * as Command from "../CommandDescriptor.js" +import type * as HelpDoc from "../HelpDoc.js" +import type * as ValidationError from "../ValidationError.js" +import * as InternalBuiltInOptions from "./builtInOptions.js" +import * as InternalCliConfig from "./cliConfig.js" +import * as InternalCommand from "./commandDescriptor.js" +import * as InternalHelpDoc from "./helpDoc.js" +import * as InternalSpan from "./helpDoc/span.js" +import * as InternalTogglePrompt from "./prompt/toggle.js" +import * as InternalUsage from "./usage.js" +import * as InternalValidationError from "./validationError.js" + +const proto = { + pipe() { + return pipeArguments(this, arguments) + } +} + +// ============================================================================= +// Constructors +// ============================================================================= + +/** @internal */ +export const make = (config: CliApp.CliApp.ConstructorArgs): CliApp.CliApp => { + const op = Object.create(proto) + op.name = config.name + op.version = config.version + op.executable = config.executable + op.command = config.command + op.summary = config.summary || InternalSpan.empty + op.footer = config.footer || InternalHelpDoc.empty + return op +} + +// ============================================================================= +// Combinators +// ============================================================================= + +/** @internal */ +export const run = dual< + ( + args: ReadonlyArray, + execute: (a: A) => Effect.Effect + ) => ( + self: CliApp.CliApp + ) => Effect.Effect, + ( + self: CliApp.CliApp, + args: ReadonlyArray, + execute: (a: A) => Effect.Effect + ) => Effect.Effect +>(3, ( + self: CliApp.CliApp, + args: ReadonlyArray, + execute: (a: A) => Effect.Effect +): Effect.Effect => + Effect.contextWithEffect((context: Context.Context) => { + // Attempt to parse the CliConfig from the environment, falling back to the + // default CliConfig if none was provided + const config = Option.getOrElse( + Context.getOption(context, InternalCliConfig.Tag), + () => InternalCliConfig.defaultConfig + ) + // Remove the executable from the command line arguments + const [executable, filteredArgs] = splitExecutable(self, args) + // Prefix the command name to the command line arguments + const prefixedArgs = Arr.appendAll(prefixCommand(self.command), filteredArgs) + // Handle the command + return Effect.matchEffect(InternalCommand.parse(self.command, prefixedArgs, config), { + onFailure: (e) => Effect.zipRight(printDocs(e.error), Effect.fail(e)), + onSuccess: Unify.unify((directive) => { + switch (directive._tag) { + case "UserDefined": { + return Arr.matchLeft(directive.leftover, { + onEmpty: () => + execute(directive.value).pipe( + Effect.catchSome((e) => + InternalValidationError.isValidationError(e) && + InternalValidationError.isHelpRequested(e) + ? Option.some( + handleBuiltInOption( + self, + executable, + filteredArgs, + InternalBuiltInOptions.showHelp( + InternalCommand.getUsage(e.command), + InternalCommand.getHelp(e.command, config) + ), + execute, + config, + args + ) + ) + : Option.none() + ) + ), + onNonEmpty: (head) => { + const error = InternalHelpDoc.p(`Received unknown argument: '${head}'`) + return Effect.zipRight(printDocs(error), Effect.fail(InternalValidationError.invalidValue(error))) + } + }) + } + case "BuiltIn": { + return handleBuiltInOption(self, executable, filteredArgs, directive.option, execute, config, args).pipe( + Effect.catchSome((e) => + InternalValidationError.isValidationError(e) + ? Option.some(Effect.zipRight(printDocs(e.error), Effect.fail(e))) + : Option.none() + ) + ) + } + } + }) + }) + })) + +// ============================================================================= +// Internals +// ============================================================================= + +const splitExecutable = (self: CliApp.CliApp, args: ReadonlyArray): [ + executable: string, + args: ReadonlyArray +] => { + if (self.executable !== undefined) { + return [self.executable, Arr.drop(args, 2)] + } + const [[runtime, script], optionsAndArgs] = Arr.splitAt(args, 2) + return [`${runtime} ${script}`, optionsAndArgs] +} + +const printDocs = (error: HelpDoc.HelpDoc): Effect.Effect => Console.error(InternalHelpDoc.toAnsiText(error)) + +const handleBuiltInOption = ( + self: CliApp.CliApp, + executable: string, + args: ReadonlyArray, + builtIn: BuiltInOptions.BuiltInOptions, + execute: (a: A) => Effect.Effect, + config: CliConfig.CliConfig, + originalArgs: ReadonlyArray +): Effect.Effect< + void, + E | ValidationError.ValidationError, + R | CliApp.CliApp.Environment | Terminal.Terminal +> => { + switch (builtIn._tag) { + case "SetLogLevel": { + // Use first 2 elements from originalArgs (runtime + script) to preserve paths with spaces + // Filter out --log-level from args before re-executing + const baseArgs = Arr.take(originalArgs, 2) + const filteredArgs: Array = [] + for (let i = 0; i < args.length; i++) { + if (isLogLevelArg(args[i]) || args[i - 1] === "--log-level") { + continue + } + filteredArgs.push(args[i]) + } + const nextArgs = Arr.appendAll(baseArgs, filteredArgs) + return run(self, nextArgs, execute).pipe( + Logger.withMinimumLogLevel(builtIn.level) + ) + } + case "ShowHelp": { + const banner = InternalHelpDoc.h1(InternalSpan.code(self.name)) + const header = InternalHelpDoc.p(InternalSpan.spans([ + InternalSpan.text(`${self.name} ${self.version}`), + InternalSpan.isEmpty(self.summary) + ? InternalSpan.empty + : InternalSpan.spans([ + InternalSpan.space, + InternalSpan.text("--"), + InternalSpan.space, + self.summary + ]) + ])) + const usage = InternalHelpDoc.sequence( + InternalHelpDoc.h1("USAGE"), + pipe( + InternalUsage.enumerate(builtIn.usage, config), + Arr.map((span) => InternalHelpDoc.p(InternalSpan.concat(InternalSpan.text("$ "), span))), + Arr.reduceRight( + InternalHelpDoc.empty, + (left, right) => InternalHelpDoc.sequence(left, right) + ) + ) + ) + const helpDoc = pipe( + banner, + InternalHelpDoc.sequence(header), + InternalHelpDoc.sequence(usage), + InternalHelpDoc.sequence(builtIn.helpDoc), + InternalHelpDoc.sequence(self.footer) + ) + return Console.log(InternalHelpDoc.toAnsiText(helpDoc)) + } + case "ShowCompletions": { + const command = Arr.fromIterable(InternalCommand.getNames(self.command))[0]! + switch (builtIn.shellType) { + case "bash": { + return InternalCommand.getBashCompletions(self.command, command).pipe( + Effect.flatMap((completions) => Console.log(Arr.join(completions, "\n"))) + ) + } + case "fish": { + return InternalCommand.getFishCompletions(self.command, command).pipe( + Effect.flatMap((completions) => Console.log(Arr.join(completions, "\n"))) + ) + } + case "zsh": + return InternalCommand.getZshCompletions(self.command, command).pipe( + Effect.flatMap((completions) => Console.log(Arr.join(completions, "\n"))) + ) + } + } + case "ShowWizard": { + const summary = InternalSpan.isEmpty(self.summary) + ? InternalSpan.empty + : InternalSpan.spans([ + InternalSpan.space, + InternalSpan.text("--"), + InternalSpan.space, + self.summary + ]) + const instructions = InternalHelpDoc.sequence( + InternalHelpDoc.p(InternalSpan.spans([ + InternalSpan.text("The wizard mode will assist you with constructing commands for"), + InternalSpan.space, + InternalSpan.code(`${self.name} (${self.version})`), + InternalSpan.text(".") + ])), + InternalHelpDoc.p("Please answer all prompts provided by the wizard.") + ) + const description = InternalHelpDoc.descriptionList([[ + InternalSpan.text("Instructions"), + instructions + ]]) + const header = InternalHelpDoc.h1( + InternalSpan.spans([ + InternalSpan.code("Wizard Mode for CLI Application:"), + InternalSpan.space, + InternalSpan.code(self.name), + InternalSpan.space, + InternalSpan.code(`(${self.version})`), + summary + ]) + ) + const help = InternalHelpDoc.sequence(header, description) + const text = InternalHelpDoc.toAnsiText(help) + const command = Arr.fromIterable(InternalCommand.getNames(self.command))[0]! + const wizardPrefix = getWizardPrefix(builtIn, command, args) + return Console.log(text).pipe( + Effect.zipRight(InternalCommand.wizard(builtIn.command, wizardPrefix, config)), + Effect.tap((args) => Console.log(InternalHelpDoc.toAnsiText(renderWizardArgs(args)))), + Effect.flatMap((args) => + InternalTogglePrompt.toggle({ + message: "Would you like to run the command?", + initial: true, + active: "yes", + inactive: "no" + }).pipe(Effect.flatMap((shouldRunCommand) => { + // Use first 2 elements from originalArgs (runtime + script) to preserve paths with spaces + // This mimics executable.split() behavior but without breaking Windows paths + const baseArgs = Arr.take(originalArgs, 2) + const wizardArgs = Arr.drop(args, 1) + const finalArgs = Arr.appendAll(baseArgs, wizardArgs) + return shouldRunCommand + ? Console.log().pipe(Effect.zipRight(run(self, finalArgs, execute))) + : Effect.void + })) + ), + Effect.catchAll((e) => { + if (Terminal.isQuitException(e)) { + const message = InternalHelpDoc.p(InternalSpan.error("\n\nQuitting wizard mode...")) + return Console.log(InternalHelpDoc.toAnsiText(message)) + } + return Effect.fail(e) + }) + ) + } + case "ShowVersion": { + const help = InternalHelpDoc.p(self.version) + return Console.log(InternalHelpDoc.toAnsiText(help)) + } + } +} + +const prefixCommand = (self: Command.Command): ReadonlyArray => { + let command: InternalCommand.Instruction | undefined = self as InternalCommand.Instruction + let prefix: ReadonlyArray = Arr.empty() + while (command !== undefined) { + switch (command._tag) { + case "Standard": { + prefix = Arr.of(command.name) + command = undefined + break + } + case "GetUserInput": { + prefix = Arr.of(command.name) + command = undefined + break + } + case "Map": { + command = command.command + break + } + case "Subcommands": { + command = command.parent + break + } + } + } + return prefix +} + +const getWizardPrefix = ( + builtIn: BuiltInOptions.ShowWizard, + rootCommand: string, + commandLineArgs: ReadonlyArray +): ReadonlyArray => { + const subcommands = InternalCommand.getSubcommands(builtIn.command) + const [parentArgs, childArgs] = Arr.span( + commandLineArgs, + (name) => !HashMap.has(subcommands, name) + ) + const args = Arr.matchLeft(childArgs, { + onEmpty: () => Arr.filter(parentArgs, (arg) => arg !== "--wizard"), + onNonEmpty: (head) => Arr.append(parentArgs, head) + }) + return Arr.appendAll(rootCommand.split(/\s+/), args) +} + +const renderWizardArgs = (args: ReadonlyArray) => { + const params = pipe( + Arr.filter(args, (param) => param.length > 0), + Arr.join(" ") + ) + const executeMsg = InternalSpan.text( + "You may now execute your command directly with the following options and arguments:" + ) + return InternalHelpDoc.blocks([ + InternalHelpDoc.p(InternalSpan.strong(InternalSpan.code("Wizard Mode Complete!"))), + InternalHelpDoc.p(executeMsg), + InternalHelpDoc.p(InternalSpan.concat( + InternalSpan.text(" "), + InternalSpan.highlight(params, Color.cyan) + )) + ]) +} + +const isLogLevelArg = (arg?: string) => { + return arg && (arg === "--log-level" || arg.startsWith("--log-level=")) +} diff --git a/repos/effect/packages/cli/src/internal/cliConfig.ts b/repos/effect/packages/cli/src/internal/cliConfig.ts new file mode 100644 index 0000000..d8a34ff --- /dev/null +++ b/repos/effect/packages/cli/src/internal/cliConfig.ts @@ -0,0 +1,40 @@ +import * as Context from "effect/Context" +import { dual } from "effect/Function" +import * as Layer from "effect/Layer" +import type * as CliConfig from "../CliConfig.js" + +/** @internal */ +export const make = (params?: Partial): CliConfig.CliConfig => ({ + ...defaultConfig, + ...params +}) + +/** @internal */ +export const Tag = Context.GenericTag("@effect/cli/CliConfig") + +/** @internal */ +export const defaultConfig: CliConfig.CliConfig = { + isCaseSensitive: false, + autoCorrectLimit: 2, + finalCheckBuiltIn: false, + showAllNames: true, + showBuiltIns: true, + showTypes: true +} + +/** @internal */ +export const defaultLayer: Layer.Layer = Layer.succeed( + Tag, + defaultConfig +) + +/** @internal */ +export const layer = ( + config?: Partial +): Layer.Layer => Layer.succeed(Tag, make(config)) + +/** @internal */ +export const normalizeCase = dual< + (text: string) => (self: CliConfig.CliConfig) => string, + (self: CliConfig.CliConfig, text: string) => string +>(2, (self, text) => self.isCaseSensitive ? text : text.toLowerCase()) diff --git a/repos/effect/packages/cli/src/internal/command.ts b/repos/effect/packages/cli/src/internal/command.ts new file mode 100644 index 0000000..1ad4c0c --- /dev/null +++ b/repos/effect/packages/cli/src/internal/command.ts @@ -0,0 +1,560 @@ +import type * as FileSystem from "@effect/platform/FileSystem" +import type * as Path from "@effect/platform/Path" +import type * as Terminal from "@effect/platform/Terminal" +import * as Arr from "effect/Array" +import * as Context from "effect/Context" +import * as Effect from "effect/Effect" +import * as Effectable from "effect/Effectable" +import { dual, identity } from "effect/Function" +import { globalValue } from "effect/GlobalValue" +import type * as HashMap from "effect/HashMap" +import type * as HashSet from "effect/HashSet" +import type * as Layer from "effect/Layer" +import type * as Option from "effect/Option" +import { pipeArguments } from "effect/Pipeable" +import type * as Types from "effect/Types" +import type * as Args from "../Args.js" +import type * as CliApp from "../CliApp.js" +import type * as CliConfig from "../CliConfig.js" +import type * as Command from "../Command.js" +import type * as Descriptor from "../CommandDescriptor.js" +import type * as HelpDoc from "../HelpDoc.js" +import type * as Options from "../Options.js" +import type * as Prompt from "../Prompt.js" +import type * as Usage from "../Usage.js" +import * as ValidationError from "../ValidationError.js" +import * as InternalArgs from "./args.js" +import * as InternalCliApp from "./cliApp.js" +import * as InternalDescriptor from "./commandDescriptor.js" +import * as InternalOptions from "./options.js" + +const CommandSymbolKey = "@effect/cli/Command" + +/** @internal */ +export const TypeId: Command.TypeId = Symbol.for( + CommandSymbolKey +) as Command.TypeId + +const parseConfig = (config: Command.Command.Config): Command.Command.ParsedConfig => { + const args: Array> = [] + let argsIndex = 0 + const options: Array> = [] + let optionsIndex = 0 + + function parse(config: Command.Command.Config) { + const tree: Command.Command.ParsedConfigTree = {} + for (const key in config) { + tree[key] = parseValue(config[key]) + } + return tree + } + + function parseValue( + value: + | Args.Args + | Options.Options + | ReadonlyArray | Options.Options | Command.Command.Config> + | Command.Command.Config + ): Command.Command.ParsedConfigNode { + if (Arr.isArray(value)) { + return { + _tag: "Array", + children: Arr.map(value as Array, parseValue) + } + } else if (InternalArgs.isArgs(value)) { + args.push(value) + return { + _tag: "Args", + index: argsIndex++ + } + } else if (InternalOptions.isOptions(value)) { + options.push(value) + return { + _tag: "Options", + index: optionsIndex++ + } + } else { + return { + _tag: "ParsedConfig", + tree: parse(value as any) + } + } + } + + return { + args, + options, + tree: parse(config) + } +} + +const reconstructConfigTree = ( + tree: Command.Command.ParsedConfigTree, + args: ReadonlyArray, + options: ReadonlyArray +): Record => { + const output: Record = {} + + for (const key in tree) { + output[key] = nodeValue(tree[key]) + } + + return output + + function nodeValue(node: Command.Command.ParsedConfigNode): any { + if (node._tag === "Args") { + return args[node.index] + } else if (node._tag === "Options") { + return options[node.index] + } else if (node._tag === "Array") { + return Arr.map(node.children, nodeValue) + } else { + return reconstructConfigTree(node.tree, args, options) + } + } +} + +const Prototype = { + ...Effectable.CommitPrototype, + [TypeId]: TypeId, + commit(this: Command.Command) { + return this.tag + }, + pipe() { + return pipeArguments(this, arguments) + } +} + +const registeredDescriptors = globalValue( + "@effect/cli/Command/registeredDescriptors", + () => new WeakMap, Descriptor.Command>() +) + +const getDescriptor = (self: Command.Command) => + registeredDescriptors.get(self.tag) ?? self.descriptor + +const makeProto = ( + descriptor: Descriptor.Command, + handler: (_: A) => Effect.Effect, + tag: Context.Tag, + transform: Command.Command.Transform = identity +): Command.Command => { + const self = Object.create(Prototype) + self.descriptor = descriptor + self.handler = handler + self.transform = transform + self.tag = tag + return self +} + +const makeDerive = ( + self: Command.Command, + options: { + readonly descriptor?: Descriptor.Command + readonly handler?: (_: A) => Effect.Effect + readonly transform?: Command.Command.Transform + } +): Command.Command => { + const command = Object.create(Prototype) + command.descriptor = options.descriptor ?? self.descriptor + command.handler = options.handler ?? self.handler + command.transform = options.transform + ? ((effect: Effect.Effect, opts: A) => options.transform!(self.transform(effect, opts), opts)) + : self.transform + command.tag = self.tag + return command +} + +/** @internal */ +export const fromDescriptor = dual< + { + (): ( + command: Descriptor.Command + ) => Command.Command + ( + handler: (_: A) => Effect.Effect + ): (command: Descriptor.Command) => Command.Command + }, + { + ( + descriptor: Descriptor.Command + ): Command.Command + ( + descriptor: Descriptor.Command, + handler: (_: A) => Effect.Effect + ): Command.Command + } +>( + (args) => InternalDescriptor.isCommand(args[0]), + (descriptor: Descriptor.Command, handler?: (_: any) => Effect.Effect) => { + const self: Command.Command = makeProto( + descriptor, + handler ?? + ((_) => Effect.failSync(() => ValidationError.helpRequested(getDescriptor(self)))), + Context.GenericTag( + `@effect/cli/Command/(${Arr.fromIterable(InternalDescriptor.getNames(descriptor)).join("|")})` + ) + ) + return self as any + } +) + +const makeDescriptor = ( + name: string, + config: Config +): Descriptor.Command>> => { + const { args, options, tree } = parseConfig(config) + return InternalDescriptor.map( + InternalDescriptor.make(name, InternalOptions.all(options), InternalArgs.all(args)), + ({ args, options }) => reconstructConfigTree(tree, args, options) + ) as any +} + +/** @internal */ +export const make: { + (name: Name): Command.Command< + Name, + never, + never, + {} + > + + ( + name: Name, + config: Config + ): Command.Command< + Name, + never, + never, + Types.Simplify> + > + + ( + name: Name, + config: Config, + handler: (_: Types.Simplify>) => Effect.Effect + ): Command.Command< + Name, + R, + E, + Types.Simplify> + > +} = ( + name: string, + config: Command.Command.Config = {}, + handler?: (_: any) => Effect.Effect +) => fromDescriptor(makeDescriptor(name, config) as any, handler as any) as any + +/** @internal */ +export const getHelp = ( + self: Command.Command, + config: CliConfig.CliConfig +): HelpDoc.HelpDoc => InternalDescriptor.getHelp(self.descriptor, config) + +/** @internal */ +export const getNames = ( + self: Command.Command +): HashSet.HashSet => InternalDescriptor.getNames(self.descriptor) + +/** @internal */ +export const getBashCompletions = ( + self: Command.Command, + programName: string +): Effect.Effect> => InternalDescriptor.getBashCompletions(self.descriptor, programName) + +/** @internal */ +export const getFishCompletions = ( + self: Command.Command, + programName: string +): Effect.Effect> => InternalDescriptor.getFishCompletions(self.descriptor, programName) + +/** @internal */ +export const getZshCompletions = ( + self: Command.Command, + programName: string +): Effect.Effect> => InternalDescriptor.getZshCompletions(self.descriptor, programName) + +/** @internal */ +export const getSubcommands = ( + self: Command.Command +): HashMap.HashMap> => InternalDescriptor.getSubcommands(self.descriptor) + +/** @internal */ +export const getUsage = ( + self: Command.Command +): Usage.Usage => InternalDescriptor.getUsage(self.descriptor) + +const mapDescriptor = dual< + (f: (_: Descriptor.Command) => Descriptor.Command) => ( + self: Command.Command + ) => Command.Command, + ( + self: Command.Command, + f: (_: Descriptor.Command) => Descriptor.Command + ) => Command.Command +>(2, (self, f) => makeDerive(self, { descriptor: f(self.descriptor) })) + +/** @internal */ +export const prompt = ( + name: Name, + prompt: Prompt.Prompt, + handler: (_: A) => Effect.Effect +) => + makeProto( + InternalDescriptor.map( + InternalDescriptor.prompt(name, prompt), + (_) => _.value + ), + handler, + Context.GenericTag(`@effect/cli/Prompt/${name}`) + ) + +/** @internal */ +export const withHandler = dual< + ( + handler: (_: A) => Effect.Effect + ) => ( + self: Command.Command + ) => Command.Command, + ( + self: Command.Command, + handler: (_: A) => Effect.Effect + ) => Command.Command +>(2, (self, handler) => makeDerive(self, { handler, transform: identity })) + +/** @internal */ +export const transformHandler = dual< + ( + f: (effect: Effect.Effect, config: A) => Effect.Effect + ) => ( + self: Command.Command + ) => Command.Command, + ( + self: Command.Command, + f: (effect: Effect.Effect, config: A) => Effect.Effect + ) => Command.Command +>(2, (self, f) => makeDerive(self, { transform: f })) + +/** @internal */ +export const provide = dual< + ( + layer: Layer.Layer | ((_: A) => Layer.Layer) + ) => ( + self: Command.Command + ) => Command.Command | LR, E | LE, A>, + ( + self: Command.Command, + layer: Layer.Layer | ((_: A) => Layer.Layer) + ) => Command.Command | LR, E | LE, A> +>(2, (self, layer) => + makeDerive(self, { + transform: (effect, config) => Effect.provide(effect, typeof layer === "function" ? layer(config) : layer) + })) + +/** @internal */ +export const provideEffect = dual< + ( + tag: Context.Tag, + effect: Effect.Effect | ((_: A) => Effect.Effect) + ) => ( + self: Command.Command + ) => Command.Command | R2, E | E2, A>, + ( + self: Command.Command, + tag: Context.Tag, + effect: Effect.Effect | ((_: A) => Effect.Effect) + ) => Command.Command | R2, E | E2, A> +>(3, (self, tag, effect_) => + makeDerive(self, { + transform: (self, config) => { + const effect = typeof effect_ === "function" ? effect_(config) : effect_ + return Effect.provideServiceEffect(self, tag, effect) + } + })) + +/** @internal */ +export const provideEffectDiscard = dual< + ( + effect: Effect.Effect<_, E2, R2> | ((_: A) => Effect.Effect<_, E2, R2>) + ) => ( + self: Command.Command + ) => Command.Command, + ( + self: Command.Command, + effect: Effect.Effect<_, E2, R2> | ((_: A) => Effect.Effect<_, E2, R2>) + ) => Command.Command +>(2, (self, effect_) => + makeDerive(self, { + transform: (self, config) => { + const effect = typeof effect_ === "function" ? effect_(config) : effect_ + return Effect.zipRight(effect, self) + } + })) + +/** @internal */ +export const provideSync = dual< + ( + tag: Context.Tag, + service: S | ((_: A) => S) + ) => ( + self: Command.Command + ) => Command.Command, E, A>, + ( + self: Command.Command, + tag: Context.Tag, + service: S | ((_: A) => S) + ) => Command.Command, E, A> +>(3, (self, tag, f) => + makeDerive(self, { + transform: (self, config) => { + const service = typeof f === "function" ? (f as any)(config) : f + return Effect.provideService(self, tag, service) + } + })) + +/** @internal */ +export const withDescription = dual< + ( + help: string | HelpDoc.HelpDoc + ) => ( + self: Command.Command + ) => Command.Command, + ( + self: Command.Command, + help: string | HelpDoc.HelpDoc + ) => Command.Command +>(2, (self, help) => mapDescriptor(self, InternalDescriptor.withDescription(help))) + +/** @internal */ +export const withSubcommands = dual< + >>( + subcommands: Subcommand + ) => (self: Command.Command) => Command.Command< + Name, + | R + | Exclude< + Effect.Effect.Context>, + Descriptor.Command + >, + E | Effect.Effect.Error>, + Descriptor.Command.ComputeParsedType< + & A + & Readonly< + { + subcommand: Option.Option< + Descriptor.Command.GetParsedType + > + } + > + > + >, + < + Name extends string, + R, + E, + A, + Subcommand extends Arr.NonEmptyReadonlyArray> + >( + self: Command.Command, + subcommands: Subcommand + ) => Command.Command< + Name, + | R + | Exclude< + Effect.Effect.Context>, + Descriptor.Command + >, + E | Effect.Effect.Error>, + Descriptor.Command.ComputeParsedType< + & A + & Readonly< + { + subcommand: Option.Option< + Descriptor.Command.GetParsedType + > + } + > + > + > +>(2, (self, subcommands) => { + const command = InternalDescriptor.withSubcommands( + self.descriptor, + Arr.map(subcommands, (_) => [_.tag, _.descriptor]) + ) + const subcommandMap = Arr.reduce( + subcommands, + new Map, Command.Command>(), + (handlers, subcommand) => { + handlers.set(subcommand.tag, subcommand) + registeredDescriptors.set(subcommand.tag, subcommand.descriptor) + return handlers + } + ) + function handler( + args: { + readonly name: string + readonly subcommand: Option.Option, value: unknown]> + } + ) { + if (args.subcommand._tag === "Some") { + const [tag, value] = args.subcommand.value + const subcommand = subcommandMap.get(tag)! + const subcommandEffect = subcommand.transform(subcommand.handler(value), value) + return Effect.provideService( + subcommandEffect, + self.tag, + args as any + ) + } + return self.handler(args as any) + } + return makeDerive(self as any, { descriptor: command as any, handler }) as any +}) + +/** @internal */ +export const wizard = dual< + ( + prefix: ReadonlyArray, + config: CliConfig.CliConfig + ) => ( + self: Command.Command + ) => Effect.Effect< + Array, + Terminal.QuitException | ValidationError.ValidationError, + FileSystem.FileSystem | Path.Path | Terminal.Terminal + >, + ( + self: Command.Command, + prefix: ReadonlyArray, + config: CliConfig.CliConfig + ) => Effect.Effect< + Array, + Terminal.QuitException | ValidationError.ValidationError, + FileSystem.FileSystem | Path.Path | Terminal.Terminal + > +>(3, (self, prefix, config) => InternalDescriptor.wizard(self.descriptor, prefix, config)) + +/** @internal */ +export const run = dual< + ( + config: Omit, "command"> + ) => ( + self: Command.Command + ) => ( + args: ReadonlyArray + ) => Effect.Effect, + ( + self: Command.Command, + config: Omit, "command"> + ) => ( + args: ReadonlyArray + ) => Effect.Effect +>(2, (self, config) => { + const app = InternalCliApp.make({ + ...config, + command: self.descriptor + }) + registeredDescriptors.set(self.tag, self.descriptor) + const handler = (args: any) => self.transform(self.handler(args), args) + return (args) => InternalCliApp.run(app, args, handler) +}) diff --git a/repos/effect/packages/cli/src/internal/commandDescriptor.ts b/repos/effect/packages/cli/src/internal/commandDescriptor.ts new file mode 100644 index 0000000..f696d8b --- /dev/null +++ b/repos/effect/packages/cli/src/internal/commandDescriptor.ts @@ -0,0 +1,1408 @@ +import type * as FileSystem from "@effect/platform/FileSystem" +import type * as Path from "@effect/platform/Path" +import type * as Terminal from "@effect/platform/Terminal" +import * as Color from "@effect/printer-ansi/Color" +import * as Arr from "effect/Array" +import * as Console from "effect/Console" +import * as Effect from "effect/Effect" +import * as Either from "effect/Either" +import { dual, pipe } from "effect/Function" +import * as HashMap from "effect/HashMap" +import * as HashSet from "effect/HashSet" +import * as Option from "effect/Option" +import * as Order from "effect/Order" +import { pipeArguments } from "effect/Pipeable" +import * as Ref from "effect/Ref" +import * as SynchronizedRef from "effect/SynchronizedRef" +import type * as Args from "../Args.js" +import type * as CliConfig from "../CliConfig.js" +import type * as Descriptor from "../CommandDescriptor.js" +import type * as Directive from "../CommandDirective.js" +import * as HelpDoc from "../HelpDoc.js" +import type * as Span from "../HelpDoc/Span.js" +import * as Options from "../Options.js" +import type * as Prompt from "../Prompt.js" +import type * as Usage from "../Usage.js" +import type * as ValidationError from "../ValidationError.js" +import * as InternalArgs from "./args.js" +import * as InternalBuiltInOptions from "./builtInOptions.js" +import * as InternalCliConfig from "./cliConfig.js" +import * as InternalCommandDirective from "./commandDirective.js" +import * as InternalHelpDoc from "./helpDoc.js" +import * as InternalSpan from "./helpDoc/span.js" +import * as InternalOptions from "./options.js" +import * as InternalPrompt from "./prompt.js" +import * as InternalSelectPrompt from "./prompt/select.js" +import * as InternalUsage from "./usage.js" +import * as InternalValidationError from "./validationError.js" + +const CommandDescriptorSymbolKey = "@effect/cli/CommandDescriptor" + +/** @internal */ +export const TypeId: Descriptor.TypeId = Symbol.for( + CommandDescriptorSymbolKey +) as Descriptor.TypeId + +/** @internal */ +export type Op = Descriptor.Command & Body & { + readonly _tag: Tag +} + +const proto = { + [TypeId]: { + _A: (_: never) => _ + }, + pipe() { + return pipeArguments(this, arguments) + } +} + +/** @internal */ +export type Instruction = + | Standard + | GetUserInput + | Map + | Subcommands + +/** @internal */ +export interface Standard extends + Op<"Standard", { + readonly name: string + readonly description: HelpDoc.HelpDoc + readonly options: Options.Options + readonly args: Args.Args + }> +{} + +/** @internal */ +export interface GetUserInput extends + Op<"GetUserInput", { + readonly name: string + readonly description: HelpDoc.HelpDoc + readonly prompt: Prompt.Prompt + }> +{} + +/** @internal */ +export interface Map extends + Op<"Map", { + readonly command: Instruction + readonly f: (value: unknown) => Effect.Effect< + unknown, + ValidationError.ValidationError, + FileSystem.FileSystem | Path.Path | Terminal.Terminal + > + }> +{} + +/** @internal */ +export interface Subcommands extends + Op<"Subcommands", { + readonly parent: Instruction + readonly children: Arr.NonEmptyReadonlyArray + }> +{} + +// ============================================================================= +// Refinements +// ============================================================================= + +/** @internal */ +export const isCommand = (u: unknown): u is Descriptor.Command => + typeof u === "object" && u != null && TypeId in u + +/** @internal */ +export const isStandard = (self: Instruction): self is Standard => self._tag === "Standard" + +/** @internal */ +export const isGetUserInput = (self: Instruction): self is GetUserInput => self._tag === "GetUserInput" + +/** @internal */ +export const isMap = (self: Instruction): self is Map => self._tag === "Map" + +/** @internal */ +export const isSubcommands = (self: Instruction): self is Subcommands => self._tag === "Subcommands" + +// ============================================================================= +// Constructors +// ============================================================================= + +/** @internal */ +export const make = ( + name: Name, + options: Options.Options = InternalOptions.none as any, + args: Args.Args = InternalArgs.none as any +): Descriptor.Command< + Descriptor.Command.ParsedStandardCommand +> => { + const op = Object.create(proto) + op._tag = "Standard" + op.name = name + op.description = InternalHelpDoc.empty + op.options = options + op.args = args + return op +} + +/** @internal */ +export const prompt = ( + name: Name, + prompt: Prompt.Prompt +): Descriptor.Command> => { + const op = Object.create(proto) + op._tag = "GetUserInput" + op.name = name + op.description = InternalHelpDoc.empty + op.prompt = prompt + return op +} + +// ============================================================================= +// Combinators +// ============================================================================= + +/** @internal */ +export const getHelp = ( + self: Descriptor.Command, + config: CliConfig.CliConfig +): HelpDoc.HelpDoc => getHelpInternal(self as Instruction, config) + +/** @internal */ +export const getNames = (self: Descriptor.Command): HashSet.HashSet => + HashSet.fromIterable(getNamesInternal(self as Instruction)) + +/** @internal */ +export const getBashCompletions = ( + self: Descriptor.Command, + executable: string +): Effect.Effect> => getBashCompletionsInternal(self as Instruction, executable) + +/** @internal */ +export const getFishCompletions = ( + self: Descriptor.Command, + executable: string +): Effect.Effect> => getFishCompletionsInternal(self as Instruction, executable) + +/** @internal */ +export const getZshCompletions = ( + self: Descriptor.Command, + executable: string +): Effect.Effect> => getZshCompletionsInternal(self as Instruction, executable) + +/** @internal */ +export const getSubcommands = ( + self: Descriptor.Command +): HashMap.HashMap> => + HashMap.fromIterable(getSubcommandsInternal(self as Instruction)) + +/** @internal */ +export const getUsage = (self: Descriptor.Command): Usage.Usage => getUsageInternal(self as Instruction) + +/** @internal */ +export const map = dual< + (f: (a: A) => B) => (self: Descriptor.Command) => Descriptor.Command, + (self: Descriptor.Command, f: (a: A) => B) => Descriptor.Command +>(2, (self, f) => mapEffect(self, (a) => Either.right(f(a)))) + +/** @internal */ +export const mapEffect = dual< + ( + f: (a: A) => Effect.Effect< + B, + ValidationError.ValidationError, + FileSystem.FileSystem | Path.Path | Terminal.Terminal + > + ) => (self: Descriptor.Command) => Descriptor.Command, + ( + self: Descriptor.Command, + f: (a: A) => Effect.Effect< + B, + ValidationError.ValidationError, + FileSystem.FileSystem | Path.Path | Terminal.Terminal + > + ) => Descriptor.Command +>(2, (self, f) => { + const op = Object.create(proto) + op._tag = "Map" + op.command = self + op.f = f + return op +}) + +/** @internal */ +export const parse = dual< + ( + args: ReadonlyArray, + config: CliConfig.CliConfig + ) => (self: Descriptor.Command) => Effect.Effect< + Directive.CommandDirective, + ValidationError.ValidationError, + FileSystem.FileSystem | Path.Path | Terminal.Terminal + >, + ( + self: Descriptor.Command, + args: ReadonlyArray, + config: CliConfig.CliConfig + ) => Effect.Effect< + Directive.CommandDirective, + ValidationError.ValidationError, + FileSystem.FileSystem | Path.Path | Terminal.Terminal + > +>(3, (self, args, config) => parseInternal(self as Instruction, args, config)) + +/** @internal */ +export const withDescription = dual< + ( + help: string | HelpDoc.HelpDoc + ) => (self: Descriptor.Command) => Descriptor.Command, + ( + self: Descriptor.Command, + help: string | HelpDoc.HelpDoc + ) => Descriptor.Command +>(2, (self, help) => withDescriptionInternal(self as Instruction, help)) + +/** @internal */ +export const withSubcommands = dual< + < + const Subcommands extends Arr.NonEmptyReadonlyArray< + readonly [id: unknown, command: Descriptor.Command] + > + >( + subcommands: [...Subcommands] + ) => ( + self: Descriptor.Command + ) => Descriptor.Command< + Descriptor.Command.ComputeParsedType< + & A + & Readonly<{ subcommand: Option.Option> }> + > + >, + < + A, + const Subcommands extends Arr.NonEmptyReadonlyArray< + readonly [id: unknown, command: Descriptor.Command] + > + >( + self: Descriptor.Command, + subcommands: [...Subcommands] + ) => Descriptor.Command< + Descriptor.Command.ComputeParsedType< + & A + & Readonly<{ subcommand: Option.Option> }> + > + > +>(2, (self, subcommands) => { + const op = Object.create(proto) + op._tag = "Subcommands" + op.parent = self + op.children = Arr.map(subcommands, ([id, command]) => map(command, (a) => [id, a])) + return op +}) + +/** @internal */ +export const wizard = dual< + ( + prefix: ReadonlyArray, + config: CliConfig.CliConfig + ) => (self: Descriptor.Command) => Effect.Effect< + Array, + Terminal.QuitException | ValidationError.ValidationError, + FileSystem.FileSystem | Path.Path | Terminal.Terminal + >, + ( + self: Descriptor.Command, + prefix: ReadonlyArray, + config: CliConfig.CliConfig + ) => Effect.Effect< + Array, + Terminal.QuitException | ValidationError.ValidationError, + FileSystem.FileSystem | Path.Path | Terminal.Terminal + > +>(3, (self, prefix, config) => wizardInternal(self as Instruction, prefix, config)) + +// ============================================================================= +// Internals +// ============================================================================= + +const getHelpInternal = (self: Instruction, config: CliConfig.CliConfig): HelpDoc.HelpDoc => { + switch (self._tag) { + case "Standard": { + const header = InternalHelpDoc.isEmpty(self.description) + ? InternalHelpDoc.empty + : InternalHelpDoc.sequence(InternalHelpDoc.h1("DESCRIPTION"), self.description) + const argsHelp = InternalArgs.getHelp(self.args) + const argsSection = InternalHelpDoc.isEmpty(argsHelp) + ? InternalHelpDoc.empty + : InternalHelpDoc.sequence(InternalHelpDoc.h1("ARGUMENTS"), argsHelp) + const options = config.showBuiltIns + ? Options.all([self.options, InternalBuiltInOptions.builtIns]) + : self.options + const optionsHelp = InternalOptions.getHelp(options) + const optionsSection = InternalHelpDoc.isEmpty(optionsHelp) + ? InternalHelpDoc.empty + : InternalHelpDoc.sequence(InternalHelpDoc.h1("OPTIONS"), optionsHelp) + return InternalHelpDoc.sequence(header, InternalHelpDoc.sequence(argsSection, optionsSection)) + } + case "GetUserInput": { + return InternalHelpDoc.isEmpty(self.description) + ? InternalHelpDoc.empty + : InternalHelpDoc.sequence(InternalHelpDoc.h1("DESCRIPTION"), self.description) + } + case "Map": { + return getHelpInternal(self.command, config) + } + case "Subcommands": { + const getUsage = ( + command: Instruction, + preceding: ReadonlyArray + ): ReadonlyArray<[Span.Span, Span.Span]> => { + switch (command._tag) { + case "Standard": + case "GetUserInput": { + const usage = InternalHelpDoc.getSpan(InternalUsage.getHelp(getUsageInternal(command))) + const usages = Arr.append(preceding, usage) + const finalUsage = Arr.reduce( + usages, + InternalSpan.empty, + (acc, next) => + InternalSpan.isText(acc) && acc.value === "" + ? next + : InternalSpan.isText(next) && next.value === "" + ? acc + : InternalSpan.spans([acc, InternalSpan.space, next]) + ) + const description = InternalHelpDoc.getSpan(command.description) + return Arr.of([finalUsage, description]) + } + case "Map": { + return getUsage(command.command, preceding) + } + case "Subcommands": { + const parentUsage = getUsage(command.parent, preceding) + return Option.match(Arr.head(parentUsage), { + onNone: () => + Arr.flatMap( + command.children, + (child) => getUsage(child, preceding) + ), + onSome: ([usage]) => { + const childrenUsage = Arr.flatMap( + command.children, + (child) => getUsage(child, Arr.append(preceding, usage)) + ) + return Arr.appendAll(parentUsage, childrenUsage) + } + }) + } + } + } + const printSubcommands = ( + subcommands: ReadonlyArray<[Span.Span, Span.Span]> + ): HelpDoc.HelpDoc => { + const maxUsageLength = Arr.reduceRight( + subcommands, + 0, + (max, [usage]) => Math.max(InternalSpan.size(usage), max) + ) + const documents = Arr.map(subcommands, ([usage, desc]) => + InternalHelpDoc.p( + InternalSpan.spans([ + usage, + InternalSpan.text(" ".repeat(maxUsageLength - InternalSpan.size(usage) + 2)), + desc + ]) + )) + if (Arr.isNonEmptyReadonlyArray(documents)) { + return InternalHelpDoc.enumeration(documents) + } + throw new Error("[BUG]: Subcommands.usage - received empty list of subcommands to print") + } + return InternalHelpDoc.sequence( + getHelpInternal(self.parent, config), + InternalHelpDoc.sequence( + InternalHelpDoc.h1("COMMANDS"), + printSubcommands(Arr.flatMap( + self.children, + (child) => getUsage(child, Arr.empty()) + )) + ) + ) + } + } +} + +const getNamesInternal = (self: Instruction): Array => { + switch (self._tag) { + case "Standard": + case "GetUserInput": { + return Arr.of(self.name) + } + case "Map": { + return getNamesInternal(self.command) + } + case "Subcommands": { + return getNamesInternal(self.parent) + } + } +} + +const getSubcommandsInternal = ( + self: Instruction +): Array<[string, GetUserInput | Standard]> => { + const loop = ( + self: Instruction, + isSubcommand: boolean + ): Array<[string, GetUserInput | Standard]> => { + switch (self._tag) { + case "Standard": + case "GetUserInput": { + return Arr.of([self.name, self]) + } + case "Map": { + return loop(self.command, isSubcommand) + } + case "Subcommands": { + // Ensure that we only traverse the subcommands one level deep from the + // parent command + return isSubcommand + ? loop(self.parent, false) + : Arr.flatMap(self.children, (child) => loop(child, true)) + } + } + } + return loop(self, false) +} + +const getUsageInternal = (self: Instruction): Usage.Usage => { + switch (self._tag) { + case "Standard": { + return InternalUsage.concat( + InternalUsage.named(Arr.of(self.name), Option.none()), + InternalUsage.concat( + InternalOptions.getUsage(self.options), + InternalArgs.getUsage(self.args) + ) + ) + } + case "GetUserInput": { + return InternalUsage.named(Arr.of(self.name), Option.none()) + } + case "Map": { + return getUsageInternal(self.command) + } + case "Subcommands": { + return InternalUsage.concat( + getUsageInternal(self.parent), + InternalUsage.mixed + ) + } + } +} + +const parseInternal = ( + self: Instruction, + args: ReadonlyArray, + config: CliConfig.CliConfig +): Effect.Effect< + Directive.CommandDirective, + ValidationError.ValidationError, + FileSystem.FileSystem | Path.Path | Terminal.Terminal +> => { + const parseCommandLine = ( + self: Standard | GetUserInput, + args: ReadonlyArray + ): Effect.Effect, ValidationError.ValidationError> => + Arr.matchLeft(args, { + onEmpty: () => { + const error = InternalHelpDoc.p(`Missing command name: '${self.name}'`) + return Effect.fail(InternalValidationError.commandMismatch(error)) + }, + onNonEmpty: (head, tail) => { + const normalizedArgv0 = InternalCliConfig.normalizeCase(config, head) + const normalizedCommandName = InternalCliConfig.normalizeCase(config, self.name) + return Effect.succeed(tail).pipe( + Effect.when(() => normalizedArgv0 === normalizedCommandName), + Effect.flatten, + Effect.catchTag("NoSuchElementException", () => { + const error = InternalHelpDoc.p(`Missing command name: '${self.name}'`) + return Effect.fail(InternalValidationError.commandMismatch(error)) + }) + ) + } + }) + switch (self._tag) { + case "Standard": { + const parseBuiltInArgs = ( + args: ReadonlyArray + ): Effect.Effect< + Directive.CommandDirective, + ValidationError.ValidationError, + FileSystem.FileSystem | Path.Path | Terminal.Terminal + > => + Arr.matchLeft(args, { + onEmpty: () => { + const error = InternalHelpDoc.p(`Missing command name: '${self.name}'`) + return Effect.fail(InternalValidationError.commandMismatch(error)) + }, + onNonEmpty: (argv0) => { + const normalizedArgv0 = InternalCliConfig.normalizeCase(config, argv0) + const normalizedCommandName = InternalCliConfig.normalizeCase(config, self.name) + if (normalizedArgv0 === normalizedCommandName) { + const help = getHelpInternal(self, config) + const usage = getUsageInternal(self) + const options = InternalBuiltInOptions.builtInOptions(self, usage, help) + const argsWithoutCommand = Arr.drop(args, 1) + return InternalOptions.processCommandLine(options, argsWithoutCommand, config) + .pipe( + Effect.flatMap((tuple) => tuple[2]), + Effect.catchTag("NoSuchElementException", () => { + const error = InternalHelpDoc.p("No built-in option was matched") + return Effect.fail(InternalValidationError.noBuiltInMatch(error)) + }), + Effect.map(InternalCommandDirective.builtIn) + ) + } + const error = InternalHelpDoc.p(`Missing command name: '${self.name}'`) + return Effect.fail(InternalValidationError.commandMismatch(error)) + } + }) + const parseUserDefinedArgs = ( + args: ReadonlyArray + ): Effect.Effect< + Directive.CommandDirective, + ValidationError.ValidationError, + FileSystem.FileSystem | Path.Path | Terminal.Terminal + > => + parseCommandLine(self, args).pipe(Effect.flatMap((commandOptionsAndArgs) => { + const [optionsAndArgs, forcedCommandArgs] = splitForcedArgs(commandOptionsAndArgs) + return InternalOptions.processCommandLine(self.options, optionsAndArgs, config).pipe( + Effect.flatMap(([error, commandArgs, optionsType]) => + InternalArgs.validate( + self.args, + Arr.appendAll(commandArgs, forcedCommandArgs), + config + ).pipe( + Effect.catchAll((e) => + Option.match(error, { + onNone: () => Effect.fail(e), + onSome: (err) => Effect.fail(err) + }) + ), + Effect.map(([argsLeftover, argsType]) => + InternalCommandDirective.userDefined(argsLeftover, { + name: self.name, + options: optionsType, + args: argsType + }) + ) + ) + ) + ) + })) + const exhaustiveSearch = ( + args: ReadonlyArray + ): Effect.Effect< + Directive.CommandDirective, + ValidationError.ValidationError, + FileSystem.FileSystem | Path.Path | Terminal.Terminal + > => { + if (Arr.contains(args, "--help") || Arr.contains(args, "-h")) { + return parseBuiltInArgs(Arr.make(self.name, "--help")) + } + if (Arr.contains(args, "--wizard")) { + return parseBuiltInArgs(Arr.make(self.name, "--wizard")) + } + if (Arr.contains(args, "--version")) { + return parseBuiltInArgs(Arr.make(self.name, "--version")) + } + const error = InternalHelpDoc.p(`Missing command name: '${self.name}'`) + return Effect.fail(InternalValidationError.commandMismatch(error)) + } + return parseBuiltInArgs(args).pipe( + Effect.orElse(() => parseUserDefinedArgs(args)), + Effect.catchSome((e) => { + if (InternalValidationError.isValidationError(e)) { + if (config.finalCheckBuiltIn) { + return Option.some( + exhaustiveSearch(args).pipe( + Effect.catchSome((_) => + InternalValidationError.isValidationError(_) + ? Option.some(Effect.fail(e)) + : Option.none() + ) + ) + ) + } + return Option.some(Effect.fail(e)) + } + return Option.none() + }) + ) + } + case "GetUserInput": { + return parseCommandLine(self, args).pipe( + Effect.zipRight(InternalPrompt.run(self.prompt)), + Effect.catchTag("QuitException", (e) => Effect.die(e)), + Effect.map((value) => + InternalCommandDirective.userDefined(Arr.drop(args, 1), { + name: self.name, + value + }) + ) + ) + } + case "Map": { + return parseInternal(self.command, args, config).pipe( + Effect.flatMap((directive) => { + if (InternalCommandDirective.isUserDefined(directive)) { + return self.f(directive.value).pipe(Effect.map((value) => + InternalCommandDirective.userDefined( + directive.leftover, + value + ) + )) + } + return Effect.succeed(directive) + }) + ) + } + case "Subcommands": { + const names = getNamesInternal(self) + const subcommands = getSubcommandsInternal(self) + const [parentArgs, childArgs] = Arr.span( + args, + (arg) => !Arr.some(subcommands, ([name]) => name === arg) + ) + const parseChildrenWith = (argsForChildren: ReadonlyArray) => + Effect.suspend(() => { + const iterator = self.children[Symbol.iterator]() + const loop = ( + next: Instruction + ): Effect.Effect< + Directive.CommandDirective, + ValidationError.ValidationError, + FileSystem.FileSystem | Path.Path | Terminal.Terminal + > => { + return parseInternal(next, argsForChildren, config).pipe( + Effect.catchIf(InternalValidationError.isCommandMismatch, (e) => { + const next = iterator.next() + return next.done ? Effect.fail(e) : loop(next.value) + }) + ) + } + return loop(iterator.next().value!) + }) + const parseChildren = parseChildrenWith(childArgs) + const helpDirectiveForParent = Effect.sync(() => { + return InternalCommandDirective.builtIn(InternalBuiltInOptions.showHelp( + getUsageInternal(self), + getHelpInternal(self, config) + )) + }) + const helpDirectiveForChild = parseChildren.pipe( + Effect.flatMap((directive) => { + if ( + InternalCommandDirective.isBuiltIn(directive) && + InternalBuiltInOptions.isShowHelp(directive.option) + ) { + const parentName = Option.getOrElse(Arr.head(names), () => "") + const newDirective = InternalCommandDirective.builtIn(InternalBuiltInOptions.showHelp( + InternalUsage.concat( + InternalUsage.named(Arr.of(parentName), Option.none()), + directive.option.usage + ), + directive.option.helpDoc + )) + return Effect.succeed(newDirective) + } + return Effect.fail(InternalValidationError.invalidArgument(InternalHelpDoc.empty)) + }) + ) + const wizardDirectiveForParent = Effect.sync(() => + InternalCommandDirective.builtIn(InternalBuiltInOptions.showWizard(self)) + ) + const wizardDirectiveForChild = parseChildren.pipe( + Effect.flatMap((directive) => { + if ( + InternalCommandDirective.isBuiltIn(directive) && + InternalBuiltInOptions.isShowWizard(directive.option) + ) { + return Effect.succeed(directive) + } + return Effect.fail(InternalValidationError.invalidArgument(InternalHelpDoc.empty)) + }) + ) + return Effect.suspend(() => + parseInternal(self.parent, parentArgs, config).pipe( + Effect.flatMap((directive) => { + switch (directive._tag) { + case "BuiltIn": { + if (InternalBuiltInOptions.isShowHelp(directive.option)) { + // We do not want to display the child help docs if there are + // no arguments indicating the CLI command was for the child + return Arr.isNonEmptyReadonlyArray(childArgs) + ? Effect.orElse(helpDirectiveForChild, () => helpDirectiveForParent) + : helpDirectiveForParent + } + if (InternalBuiltInOptions.isShowWizard(directive.option)) { + return Effect.orElse(wizardDirectiveForChild, () => wizardDirectiveForParent) + } + return Effect.succeed(directive) + } + case "UserDefined": { + const args = Arr.appendAll(directive.leftover, childArgs) + if (Arr.isNonEmptyReadonlyArray(args)) { + return parseChildrenWith(args).pipe( + Effect.flatMap((childDirective) => { + if (!InternalCommandDirective.isUserDefined(childDirective)) { + return Effect.succeed(childDirective) + } + const childLeftover = childDirective.leftover + if (Arr.isEmptyReadonlyArray(childLeftover)) { + return Effect.succeed(InternalCommandDirective.userDefined(childLeftover, { + ...directive.value as any, + subcommand: Option.some(childDirective.value) + })) + } + const parentArgsWithLeftover = Arr.appendAll(parentArgs, childLeftover) + return parseInternal(self.parent, parentArgsWithLeftover, config).pipe( + Effect.flatMap((reParsedParentDirective) => { + if (!InternalCommandDirective.isUserDefined(reParsedParentDirective)) { + return Effect.succeed(InternalCommandDirective.userDefined(childLeftover, { + ...directive.value as any, + subcommand: Option.some(childDirective.value) + })) + } + return Effect.succeed(InternalCommandDirective.userDefined( + reParsedParentDirective.leftover as Array, + { + ...reParsedParentDirective.value as any, + subcommand: Option.some(childDirective.value) + } + )) + }), + Effect.catchAll(() => + Effect.succeed(InternalCommandDirective.userDefined(childLeftover, { + ...directive.value as any, + subcommand: Option.some(childDirective.value) + })) + ) + ) + }), + Effect.catchAll((err) => { + if (InternalValidationError.isCommandMismatch(err)) { + const parentName = Option.getOrElse(Arr.head(names), () => "") + const childNames = Arr.map(subcommands, ([name]) => `'${name}'`) + const oneOf = childNames.length === 1 ? "" : " one of" + const error = InternalHelpDoc.p( + `Invalid subcommand for ${parentName} - use${oneOf} ${Arr.join(childNames, ", ")}` + ) + return Effect.fail(InternalValidationError.commandMismatch(error)) + } + return Effect.fail(err) + }) + ) + } + return Effect.succeed(InternalCommandDirective.userDefined(directive.leftover, { + ...directive.value as any, + subcommand: Option.none() + })) + } + } + }), + Effect.catchSome(() => + Arr.isEmptyReadonlyArray(args) + ? Option.some(helpDirectiveForParent) : + Option.none() + ) + ) + ) + } + } +} + +const splitForcedArgs = ( + args: ReadonlyArray +): [Array, Array] => { + const [remainingArgs, forcedArgs] = Arr.span(args, (str) => str !== "--") + return [remainingArgs, Arr.drop(forcedArgs, 1)] +} + +const withDescriptionInternal = ( + self: Instruction, + description: string | HelpDoc.HelpDoc +): Descriptor.Command => { + switch (self._tag) { + case "Standard": { + const helpDoc = typeof description === "string" ? HelpDoc.p(description) : description + const op = Object.create(proto) + op._tag = "Standard" + op.name = self.name + op.description = helpDoc + op.options = self.options + op.args = self.args + return op + } + case "GetUserInput": { + const helpDoc = typeof description === "string" ? HelpDoc.p(description) : description + const op = Object.create(proto) + op._tag = "GetUserInput" + op.name = self.name + op.description = helpDoc + op.prompt = self.prompt + return op + } + case "Map": { + return mapEffect(withDescriptionInternal(self.command, description), self.f) + } + case "Subcommands": { + const op = Object.create(proto) + op._tag = "Subcommands" + op.parent = withDescriptionInternal(self.parent, description) + op.children = self.children.slice() + return op + } + } +} + +const argsWizardHeader = InternalSpan.code("Args Wizard - ") +const optionsWizardHeader = InternalSpan.code("Options Wizard - ") + +const wizardInternal = ( + self: Instruction, + prefix: ReadonlyArray, + config: CliConfig.CliConfig +): Effect.Effect< + Array, + Terminal.QuitException | ValidationError.ValidationError, + FileSystem.FileSystem | Path.Path | Terminal.Terminal +> => { + const loop = ( + self: Instruction, + commandLineRef: Ref.Ref> + ): Effect.Effect< + ReadonlyArray, + Terminal.QuitException | ValidationError.ValidationError, + FileSystem.FileSystem | Path.Path | Terminal.Terminal + > => { + switch (self._tag) { + case "GetUserInput": + case "Standard": { + return Effect.gen(function*() { + const logCurrentCommand = Ref.get(commandLineRef).pipe(Effect.flatMap((commandLine) => { + const currentCommand = InternalHelpDoc.p(pipe( + InternalSpan.strong(InternalSpan.highlight("COMMAND:", Color.cyan)), + InternalSpan.concat(InternalSpan.space), + InternalSpan.concat(InternalSpan.highlight( + Arr.join(commandLine, " "), + Color.magenta + )) + )) + return Console.log(InternalHelpDoc.toAnsiText(currentCommand)) + })) + if (isStandard(self)) { + // Log the current command line arguments + yield* logCurrentCommand + const commandName = InternalSpan.highlight(self.name, Color.magenta) + // If the command has options, run the wizard for them + if (!InternalOptions.isEmpty(self.options as InternalOptions.Instruction)) { + const message = InternalHelpDoc.p( + InternalSpan.concat(optionsWizardHeader, commandName) + ) + yield* Console.log(InternalHelpDoc.toAnsiText(message)) + const options = yield* InternalOptions.wizard(self.options, config) + yield* Ref.updateAndGet(commandLineRef, Arr.appendAll(options)) + yield* logCurrentCommand + } + // If the command has args, run the wizard for them + if (!InternalArgs.isEmpty(self.args as InternalArgs.Instruction)) { + const message = InternalHelpDoc.p( + InternalSpan.concat(argsWizardHeader, commandName) + ) + yield* Console.log(InternalHelpDoc.toAnsiText(message)) + const options = yield* InternalArgs.wizard(self.args, config) + yield* Ref.updateAndGet(commandLineRef, Arr.appendAll(options)) + yield* logCurrentCommand + } + } + return yield* Ref.get(commandLineRef) + }) + } + case "Map": { + return loop(self.command, commandLineRef) + } + case "Subcommands": { + const description = InternalHelpDoc.p("Select which command you would like to execute") + const message = InternalHelpDoc.toAnsiText(description).trimEnd() + const makeChoice = (title: string, index: number) => ({ + title, + value: [title, index] as const + }) + const choices = pipe( + getSubcommandsInternal(self), + Arr.map(([name], index) => makeChoice(name, index)) + ) + return loop(self.parent, commandLineRef).pipe( + Effect.zipRight( + InternalSelectPrompt.select({ message, choices }).pipe( + Effect.tap(([name]) => Ref.update(commandLineRef, Arr.append(name))), + Effect.zipLeft(Console.log()), + Effect.flatMap(([, nextIndex]) => loop(self.children[nextIndex], commandLineRef)) + ) + ) + ) + } + } + } + return Ref.make(prefix).pipe( + Effect.flatMap((commandLineRef) => + loop(self, commandLineRef).pipe( + Effect.zipRight(Ref.get(commandLineRef) as Effect.Effect>) + ) + ) + ) +} + +// ============================================================================= +// Completion Internals +// ============================================================================= + +const getShortDescription = (self: Instruction): string => { + switch (self._tag) { + case "Standard": { + return InternalSpan.getText(InternalHelpDoc.getSpan(self.description)) + } + case "GetUserInput": { + return InternalSpan.getText(InternalHelpDoc.getSpan(self.description)) + } + case "Map": { + return getShortDescription(self.command) + } + case "Subcommands": { + return "" + } + } +} + +interface CommandInfo { + readonly command: Standard | GetUserInput + readonly parentCommands: ReadonlyArray + readonly subcommands: ReadonlyArray<[string, Standard | GetUserInput]> + readonly level: number +} + +/** + * Allows for linear traversal of a `Command` data structure, accumulating state + * based on information acquired from the command. + */ +const traverseCommand = ( + self: Instruction, + initialState: S, + f: (state: S, info: CommandInfo) => Effect.Effect +): Effect.Effect => + SynchronizedRef.make(initialState).pipe(Effect.flatMap((ref) => { + const loop = ( + self: Instruction, + parentCommands: ReadonlyArray, + subcommands: ReadonlyArray<[string, Standard | GetUserInput]>, + level: number + ): Effect.Effect => { + switch (self._tag) { + case "Standard": { + const info: CommandInfo = { + command: self, + parentCommands, + subcommands, + level + } + return SynchronizedRef.updateEffect(ref, (state) => f(state, info)) + } + case "GetUserInput": { + const info: CommandInfo = { + command: self, + parentCommands, + subcommands, + level + } + return SynchronizedRef.updateEffect(ref, (state) => f(state, info)) + } + case "Map": { + return loop(self.command, parentCommands, subcommands, level) + } + case "Subcommands": { + const parentNames = getNamesInternal(self.parent) + const nextSubcommands = getSubcommandsInternal(self) + const nextParentCommands = Arr.appendAll(parentCommands, parentNames) + // Traverse the parent command using old parent names and next subcommands + return loop(self.parent, parentCommands, nextSubcommands, level).pipe( + Effect.zipRight(Effect.forEach(self.children, (child) => + // Traverse the child command using next parent names and old subcommands + loop(child, nextParentCommands, subcommands, level + 1))) + ) + } + } + } + return Effect.suspend(() => loop(self, Arr.empty(), Arr.empty(), 0)).pipe( + Effect.zipRight(SynchronizedRef.get(ref)) + ) + })) + +const indentAll = dual< + (indent: number) => (self: ReadonlyArray) => Array, + (self: ReadonlyArray, indent: number) => Array +>(2, (self: ReadonlyArray, indent: number): Array => { + const indentation = Arr.allocate(indent + 1).join(" ") + return Arr.map(self, (line) => `${indentation}${line}`) +}) + +const getBashCompletionsInternal = ( + self: Instruction, + executable: string +): Effect.Effect> => + traverseCommand( + self, + Arr.empty<[ReadonlyArray, ReadonlyArray]>(), + (state, info) => { + const options = isStandard(info.command) + ? Options.all([info.command.options, InternalBuiltInOptions.builtIns]) + : InternalBuiltInOptions.builtIns + const optionNames = InternalOptions.getNames(options as InternalOptions.Instruction) + const optionCases = isStandard(info.command) + ? InternalOptions.getBashCompletions(info.command.options as InternalOptions.Instruction) + : Arr.empty() + const subcommandNames = pipe( + info.subcommands, + Arr.map(([name]) => name), + Arr.sort(Order.string) + ) + const wordList = Arr.appendAll(optionNames, subcommandNames) + const preformatted = Arr.isEmptyReadonlyArray(info.parentCommands) + ? Arr.of(info.command.name) + : pipe( + info.parentCommands, + Arr.append(info.command.name), + Arr.map((command) => command.replaceAll("-", "__")) + ) + const caseName = Arr.join(preformatted, ",") + const funcName = Arr.join(preformatted, "__") + const funcLines = Arr.isEmptyReadonlyArray(info.parentCommands) + ? Arr.empty() + : [ + `${caseName})`, + ` cmd="${funcName}"`, + " ;;" + ] + const cmdLines = [ + `${funcName})`, + ` opts="${Arr.join(wordList, " ")}"`, + ` if [[ \${cur} == -* || \${COMP_CWORD} -eq ${info.level + 1} ]] ; then`, + " COMPREPLY=( $(compgen -W \"${opts}\" -- \"${cur}\") )", + " return 0", + " fi", + " case \"${prev}\" in", + ...indentAll(optionCases, 8), + " *)", + " COMPREPLY=()", + " ;;", + " esac", + " COMPREPLY=( $(compgen -W \"${opts}\" -- \"${cur}\") )", + " return 0", + " ;;" + ] + const lines = Arr.append( + state, + [funcLines, cmdLines] as [ReadonlyArray, ReadonlyArray] + ) + return Effect.succeed(lines) + } + ).pipe(Effect.map((lines) => { + const rootCommand = Arr.unsafeGet(getNamesInternal(self), 0) + const scriptName = `_${rootCommand}_bash_completions` + const funcCases = Arr.flatMap(lines, ([funcLines]) => funcLines) + const cmdCases = Arr.flatMap(lines, ([, cmdLines]) => cmdLines) + return [ + `function ${scriptName}() {`, + " local i cur prev opts cmd", + " COMPREPLY=()", + " cur=\"${COMP_WORDS[COMP_CWORD]}\"", + " prev=\"${COMP_WORDS[COMP_CWORD-1]}\"", + " cmd=\"\"", + " opts=\"\"", + " for i in \"${COMP_WORDS[@]}\"; do", + " case \"${cmd},${i}\" in", + " \",$1\")", + ` cmd="${executable}"`, + " ;;", + ...indentAll(funcCases, 12), + " *)", + " ;;", + " esac", + " done", + " case \"${cmd}\" in", + ...indentAll(cmdCases, 8), + " esac", + "}", + `complete -F ${scriptName} -o nosort -o bashdefault -o default ${rootCommand}` + ] + })) + +const getFishCompletionsInternal = ( + self: Instruction, + executable: string +): Effect.Effect> => + traverseCommand(self, Arr.empty(), (state, info) => { + const baseTemplate = Arr.make("complete", "-c", executable) + const options = isStandard(info.command) + ? InternalOptions.all([InternalBuiltInOptions.builtIns, info.command.options]) + : InternalBuiltInOptions.builtIns + const optionsCompletions = InternalOptions.getFishCompletions( + options as InternalOptions.Instruction + ) + const argsCompletions = isStandard(info.command) + ? InternalArgs.getFishCompletions(info.command.args as InternalArgs.Instruction) + : Arr.empty() + const rootCompletions = (conditionals: ReadonlyArray) => + pipe( + Arr.map(optionsCompletions, (option) => + pipe( + baseTemplate, + Arr.appendAll(conditionals), + Arr.append(option), + Arr.join(" ") + )), + Arr.appendAll( + Arr.map(argsCompletions, (option) => + pipe( + baseTemplate, + Arr.appendAll(conditionals), + Arr.append(option), + Arr.join(" ") + )) + ) + ) + const subcommandCompletions = (conditionals: ReadonlyArray) => + Arr.map(info.subcommands, ([name, subcommand]) => { + const description = getShortDescription(subcommand) + return pipe( + baseTemplate, + Arr.appendAll(conditionals), + Arr.appendAll(Arr.make("-f", "-a", `"${name}"`)), + Arr.appendAll( + description.length === 0 + ? Arr.empty() + : Arr.make("-d", `'${description}'`) + ), + Arr.join(" ") + ) + }) + // If parent commands are empty, then the info is for the root command + if (Arr.isEmptyReadonlyArray(info.parentCommands)) { + const conditionals = Arr.make("-n", "\"__fish_use_subcommand\"") + return Effect.succeed(pipe( + state, + Arr.appendAll(rootCompletions(conditionals)), + Arr.appendAll(subcommandCompletions(conditionals)) + )) + } + // Otherwise the info is for a subcommand + const parentConditionals = pipe( + info.parentCommands, + // Drop the root command name from the subcommand conditionals + Arr.drop(1), + Arr.append(info.command.name), + Arr.map((command) => `__fish_seen_subcommand_from ${command}`) + ) + const subcommandConditionals = Arr.map( + info.subcommands, + ([name]) => `not __fish_seen_subcommand_from ${name}` + ) + const baseConditionals = pipe( + Arr.appendAll(parentConditionals, subcommandConditionals), + Arr.join("; and ") + ) + const conditionals = Arr.make("-n", `"${baseConditionals}"`) + return Effect.succeed(pipe( + state, + Arr.appendAll(rootCompletions(conditionals)), + Arr.appendAll(subcommandCompletions(conditionals)) + )) + }) + +const getZshCompletionsInternal = ( + self: Instruction, + executable: string +): Effect.Effect> => + traverseCommand(self, Arr.empty(), (state, info) => { + const preformatted = Arr.isEmptyReadonlyArray(info.parentCommands) + ? Arr.of(info.command.name) + : pipe( + info.parentCommands, + Arr.append(info.command.name), + Arr.map((command) => command.replaceAll("-", "__")) + ) + const underscoreName = Arr.join(preformatted, "__") + const spaceName = Arr.join(preformatted, " ") + const subcommands = pipe( + info.subcommands, + Arr.map(([name, subcommand]) => { + const desc = getShortDescription(subcommand) + return `'${name}:${desc}' \\` + }) + ) + const commands = Arr.isEmptyReadonlyArray(subcommands) + ? `commands=()` + : `commands=(\n${Arr.join(indentAll(subcommands, 8), "\n")}\n )` + const handlerLines = [ + `(( $+functions[_${underscoreName}_commands] )) ||`, + `_${underscoreName}_commands() {`, + ` local commands; ${commands}`, + ` _describe -t commands '${spaceName} commands' commands "$@"`, + "}" + ] + return Effect.succeed(Arr.appendAll(state, handlerLines)) + }).pipe(Effect.map((handlers) => { + const rootCommand = Arr.unsafeGet(getNamesInternal(self), 0) + const cases = getZshSubcommandCases(self, Arr.empty(), Arr.empty()) + const scriptName = `_${rootCommand}_zsh_completions` + return [ + `#compdef ${executable}`, + "", + "autoload -U is-at-least", + "", + `function ${scriptName}() {`, + " typeset -A opt_args", + " typeset -a _arguments_options", + " local ret=1", + "", + " if is-at-least 5.2; then", + " _arguments_options=(-s -S -C)", + " else", + " _arguments_options=(-s -C)", + " fi", + "", + " local context curcontext=\"$curcontext\" state line", + ...indentAll(cases, 4), + "}", + "", + ...handlers, + "", + `if [ "$funcstack[1]" = "${scriptName}" ]; then`, + ` ${scriptName} "$@"`, + "else", + ` compdef ${scriptName} ${rootCommand}`, + "fi" + ] + })) + +const getZshSubcommandCases = ( + self: Instruction, + parentCommands: ReadonlyArray, + subcommands: ReadonlyArray<[string, Standard | GetUserInput]> +): Array => { + switch (self._tag) { + case "Standard": + case "GetUserInput": { + const options = isStandard(self) + ? InternalOptions.all([InternalBuiltInOptions.builtIns, self.options]) + : InternalBuiltInOptions.builtIns + const args = isStandard(self) ? self.args : InternalArgs.none + const optionCompletions = pipe( + InternalOptions.getZshCompletions(options as InternalOptions.Instruction), + Arr.map((completion) => `'${completion}' \\`) + ) + const argCompletions = pipe( + InternalArgs.getZshCompletions(args as InternalArgs.Instruction), + Arr.map((completion) => `'${completion}' \\`) + ) + if (Arr.isEmptyReadonlyArray(parentCommands)) { + return [ + "_arguments \"${_arguments_options[@]}\" \\", + ...indentAll(optionCompletions, 4), + ...indentAll(argCompletions, 4), + ` ":: :_${self.name}_commands" \\`, + ` "*::: :->${self.name}" \\`, + " && ret=0" + ] + } + if (Arr.isEmptyReadonlyArray(subcommands)) { + return [ + `(${self.name})`, + "_arguments \"${_arguments_options[@]}\" \\", + ...indentAll(optionCompletions, 4), + ...indentAll(argCompletions, 4), + " && ret=0", + ";;" + ] + } + return [ + `(${self.name})`, + "_arguments \"${_arguments_options[@]}\" \\", + ...indentAll(optionCompletions, 4), + ...indentAll(argCompletions, 4), + ` ":: :_${Arr.append(parentCommands, self.name).join("__")}_commands" \\`, + ` "*::: :->${self.name}" \\`, + " && ret=0" + ] + } + case "Map": { + return getZshSubcommandCases(self.command, parentCommands, subcommands) + } + case "Subcommands": { + const nextSubcommands = getSubcommandsInternal(self) + const parentNames = getNamesInternal(self.parent) + const parentLines = getZshSubcommandCases( + self.parent, + parentCommands, + Arr.appendAll(subcommands, nextSubcommands) + ) + const childCases = pipe( + self.children, + Arr.flatMap((child) => + getZshSubcommandCases( + child, + Arr.appendAll(parentCommands, parentNames), + subcommands + ) + ) + ) + const hyphenName = pipe( + Arr.appendAll(parentCommands, parentNames), + Arr.join("-") + ) + const childLines = pipe( + parentNames, + Arr.flatMap((parentName) => [ + "case $state in", + ` (${parentName})`, + ` words=($line[1] "\${words[@]}")`, + " (( CURRENT += 1 ))", + ` curcontext="\${curcontext%:*:*}:${hyphenName}-command-$line[1]:"`, + ` case $line[1] in`, + ...indentAll(childCases, 8), + " esac", + " ;;", + "esac" + ]), + Arr.appendAll( + Arr.isEmptyReadonlyArray(parentCommands) + ? Arr.empty() + : Arr.of(";;") + ) + ) + return Arr.appendAll(parentLines, childLines) + } + } +} + +// Circular with ValidationError + +/** @internal */ +export const helpRequestedError = ( + command: Descriptor.Command +): ValidationError.ValidationError => { + const op = Object.create(InternalValidationError.proto) + op._tag = "HelpRequested" + op.error = InternalHelpDoc.empty + op.command = command + return op +} diff --git a/repos/effect/packages/cli/src/internal/commandDirective.ts b/repos/effect/packages/cli/src/internal/commandDirective.ts new file mode 100644 index 0000000..e8cb351 --- /dev/null +++ b/repos/effect/packages/cli/src/internal/commandDirective.ts @@ -0,0 +1,42 @@ +import { dual } from "effect/Function" +import type * as BuiltInOption from "../BuiltInOptions.js" +import type * as CommandDirective from "../CommandDirective.js" + +/** @internal */ +export const builtIn = ( + option: BuiltInOption.BuiltInOptions +): CommandDirective.CommandDirective => ({ + _tag: "BuiltIn", + option +}) + +/** @internal */ +export const userDefined = ( + leftover: ReadonlyArray, + value: A +): CommandDirective.CommandDirective => ({ + _tag: "UserDefined", + leftover, + value +}) + +/** @internal */ +export const isBuiltIn = ( + self: CommandDirective.CommandDirective +): self is CommandDirective.BuiltIn => self._tag === "BuiltIn" + +/** @internal */ +export const isUserDefined = ( + self: CommandDirective.CommandDirective +): self is CommandDirective.UserDefined => self._tag === "UserDefined" + +/** @internal */ +export const map = dual< + ( + f: (a: A) => B + ) => (self: CommandDirective.CommandDirective) => CommandDirective.CommandDirective, + ( + self: CommandDirective.CommandDirective, + f: (a: A) => B + ) => CommandDirective.CommandDirective +>(2, (self, f) => isUserDefined(self) ? userDefined(self.leftover, f(self.value)) : self) diff --git a/repos/effect/packages/cli/src/internal/configFile.ts b/repos/effect/packages/cli/src/internal/configFile.ts new file mode 100644 index 0000000..5ba572b --- /dev/null +++ b/repos/effect/packages/cli/src/internal/configFile.ts @@ -0,0 +1,91 @@ +import * as FileSystem from "@effect/platform/FileSystem" +import * as Path from "@effect/platform/Path" +import * as Cause from "effect/Cause" +import * as ConfigProvider from "effect/ConfigProvider" +import * as Context from "effect/Context" +import * as DefaultServices from "effect/DefaultServices" +import * as Effect from "effect/Effect" +import { pipe } from "effect/Function" +import * as Layer from "effect/Layer" +import type * as ConfigFile from "../ConfigFile.js" +import * as InternalFiles from "./files.js" + +const fileExtensions: Record> = { + json: ["json"], + yaml: ["yaml", "yml"], + ini: ["ini"], + toml: ["toml", "tml"] +} + +const allFileExtensions = Object.values(fileExtensions).flat() + +/** @internal */ +export const makeProvider = (fileName: string, options?: { + readonly formats?: ReadonlyArray + readonly searchPaths?: ReadonlyArray +}): Effect.Effect => + Effect.gen(function*() { + const path = yield* Path.Path + const fs = yield* FileSystem.FileSystem + const searchPaths = options?.searchPaths && options.searchPaths.length ? options.searchPaths : ["."] + const extensions = options?.formats && options.formats.length + ? options.formats.flatMap((_) => fileExtensions[_]) + : allFileExtensions + const filePaths = yield* Effect.filter( + searchPaths.flatMap( + (searchPath) => extensions.map((ext) => path.join(searchPath, `${fileName}.${ext}`)) + ), + (path) => Effect.orElseSucceed(fs.exists(path), () => false) + ) + const providers = yield* Effect.forEach(filePaths, (path) => + pipe( + fs.readFileString(path), + Effect.mapError((_) => ConfigFileError(`Could not read file (${path})`)), + Effect.flatMap((content) => + Effect.mapError( + InternalFiles.parse(path, content), + (message) => ConfigFileError(message) + ) + ), + Effect.map((data) => ConfigProvider.fromJson(data)) + )) + + if (providers.length === 0) { + return ConfigProvider.fromMap(new Map()) + } + + return providers.reduce((acc, provider) => ConfigProvider.orElse(acc, () => provider)) + }) + +/** @internal */ +export const layer = (fileName: string, options?: { + readonly formats?: ReadonlyArray + readonly searchPaths?: ReadonlyArray +}): Layer.Layer => + pipe( + makeProvider(fileName, options), + Effect.map((provider) => + Layer.fiberRefLocallyScopedWith(DefaultServices.currentServices, (services) => { + const current = Context.get(services, ConfigProvider.ConfigProvider) + return Context.add(services, ConfigProvider.ConfigProvider, ConfigProvider.orElse(current, () => provider)) + }) + ), + Layer.unwrapEffect + ) + +/** @internal */ +export const ConfigErrorTypeId: ConfigFile.ConfigErrorTypeId = Symbol.for( + "@effect/cli/ConfigFile/ConfigFileError" +) as ConfigFile.ConfigErrorTypeId + +const ConfigFileErrorProto = Object.assign(Object.create(Cause.YieldableError.prototype), { + [ConfigErrorTypeId]: ConfigErrorTypeId +}) + +/** @internal */ +export const ConfigFileError = (message: string): ConfigFile.ConfigFileError => { + const self = Object.create(ConfigFileErrorProto) + self._tag = "ConfigFileError" + self.message = message + return self +} diff --git a/repos/effect/packages/cli/src/internal/files.ts b/repos/effect/packages/cli/src/internal/files.ts new file mode 100644 index 0000000..7e8bc32 --- /dev/null +++ b/repos/effect/packages/cli/src/internal/files.ts @@ -0,0 +1,58 @@ +import * as FileSystem from "@effect/platform/FileSystem" +import * as Effect from "effect/Effect" +import * as Ini from "ini" +import * as Toml from "toml" +import * as Yaml from "yaml" + +/** @internal */ +export const fileParsers: Record unknown> = { + json: (content: string) => JSON.parse(content), + yaml: (content: string) => Yaml.parse(content), + yml: (content: string) => Yaml.parse(content), + ini: (content: string) => Ini.parse(content), + toml: (content: string) => Toml.parse(content), + tml: (content: string) => Toml.parse(content) +} + +/** @internal */ +export const read = ( + path: string +): Effect.Effect => + Effect.flatMap( + FileSystem.FileSystem, + (fs) => + Effect.matchEffect(fs.readFile(path), { + onFailure: (error) => Effect.fail(`Could not read file (${path}): ${error}`), + onSuccess: (content) => Effect.succeed([path, content] as const) + }) + ) + +/** @internal */ +export const readString = ( + path: string +): Effect.Effect => + Effect.flatMap( + FileSystem.FileSystem, + (fs) => + Effect.matchEffect(fs.readFileString(path), { + onFailure: (error) => Effect.fail(`Could not read file (${path}): ${error}`), + onSuccess: (content) => Effect.succeed([path, content] as const) + }) + ) + +/** @internal */ +export const parse = ( + path: string, + content: string, + format?: "json" | "yaml" | "ini" | "toml" +): Effect.Effect => { + const parser = fileParsers[format ?? path.split(".").pop() as string] + if (parser === undefined) { + return Effect.fail(`Unsupported file format: ${format}`) + } + + return Effect.try({ + try: () => parser(content), + catch: (e) => `Could not parse ${format} file (${path}): ${e}` + }) +} diff --git a/repos/effect/packages/cli/src/internal/helpDoc.ts b/repos/effect/packages/cli/src/internal/helpDoc.ts new file mode 100644 index 0000000..ed4529b --- /dev/null +++ b/repos/effect/packages/cli/src/internal/helpDoc.ts @@ -0,0 +1,178 @@ +import * as Ansi from "@effect/printer-ansi/Ansi" +import * as Doc from "@effect/printer-ansi/AnsiDoc" +import * as Optimize from "@effect/printer/Optimize" +import * as Arr from "effect/Array" +import { dual, pipe } from "effect/Function" +import type * as HelpDoc from "../HelpDoc.js" +import type * as Span from "../HelpDoc/Span.js" +import * as InternalSpan from "./helpDoc/span.js" + +/** @internal */ +export const isEmpty = (helpDoc: HelpDoc.HelpDoc): helpDoc is HelpDoc.Empty => helpDoc._tag === "Empty" + +/** @internal */ +export const isHeader = (helpDoc: HelpDoc.HelpDoc): helpDoc is HelpDoc.Header => helpDoc._tag === "Header" + +/** @internal */ +export const isParagraph = (helpDoc: HelpDoc.HelpDoc): helpDoc is HelpDoc.Paragraph => helpDoc._tag === "Paragraph" + +/** @internal */ +export const isDescriptionList = (helpDoc: HelpDoc.HelpDoc): helpDoc is HelpDoc.DescriptionList => + helpDoc._tag === "DescriptionList" + +/** @internal */ +export const isEnumeration = (helpDoc: HelpDoc.HelpDoc): helpDoc is HelpDoc.Enumeration => + helpDoc._tag === "Enumeration" + +/** @internal */ +export const isSequence = (helpDoc: HelpDoc.HelpDoc): helpDoc is HelpDoc.Sequence => helpDoc._tag === "Sequence" + +/** @internal */ +export const empty: HelpDoc.HelpDoc = { + _tag: "Empty" +} + +/** @internal */ +export const sequence = dual< + (that: HelpDoc.HelpDoc) => (self: HelpDoc.HelpDoc) => HelpDoc.HelpDoc, + (self: HelpDoc.HelpDoc, that: HelpDoc.HelpDoc) => HelpDoc.HelpDoc +>(2, (self, that) => { + if (isEmpty(self)) { + return that + } + if (isEmpty(that)) { + return self + } + return { + _tag: "Sequence", + left: self, + right: that + } +}) + +/** @internal */ +export const orElse = dual< + (that: HelpDoc.HelpDoc) => (self: HelpDoc.HelpDoc) => HelpDoc.HelpDoc, + (self: HelpDoc.HelpDoc, that: HelpDoc.HelpDoc) => HelpDoc.HelpDoc +>(2, (self, that) => isEmpty(self) ? that : self) + +/** @internal */ +export const blocks = (helpDocs: Iterable): HelpDoc.HelpDoc => { + const elements = Arr.fromIterable(helpDocs) + if (Arr.isNonEmptyReadonlyArray(elements)) { + return elements.slice(1).reduce(sequence, elements[0]) + } + return empty +} + +/** @internal */ +export const getSpan = (self: HelpDoc.HelpDoc): Span.Span => + isHeader(self) || isParagraph(self) ? self.value : InternalSpan.empty + +/** @internal */ +export const descriptionList = ( + definitions: Arr.NonEmptyReadonlyArray<[Span.Span, HelpDoc.HelpDoc]> +): HelpDoc.HelpDoc => ({ + _tag: "DescriptionList", + definitions +}) + +/** @internal */ +export const enumeration = ( + elements: Arr.NonEmptyReadonlyArray +): HelpDoc.HelpDoc => ({ + _tag: "Enumeration", + elements +}) + +/** @internal */ +export const h1 = (value: string | Span.Span): HelpDoc.HelpDoc => ({ + _tag: "Header", + value: typeof value === "string" ? InternalSpan.text(value) : value, + level: 1 +}) + +/** @internal */ +export const h2 = (value: string | Span.Span): HelpDoc.HelpDoc => ({ + _tag: "Header", + value: typeof value === "string" ? InternalSpan.text(value) : value, + level: 2 +}) + +/** @internal */ +export const h3 = (value: string | Span.Span): HelpDoc.HelpDoc => ({ + _tag: "Header", + value: typeof value === "string" ? InternalSpan.text(value) : value, + level: 3 +}) + +/** @internal */ +export const p = (value: string | Span.Span): HelpDoc.HelpDoc => ({ + _tag: "Paragraph", + value: typeof value === "string" ? InternalSpan.text(value) : value +}) + +/** @internal */ +export const mapDescriptionList = dual< + ( + f: (span: Span.Span, helpDoc: HelpDoc.HelpDoc) => [Span.Span, HelpDoc.HelpDoc] + ) => (self: HelpDoc.HelpDoc) => HelpDoc.HelpDoc, + ( + self: HelpDoc.HelpDoc, + f: (span: Span.Span, helpDoc: HelpDoc.HelpDoc) => [Span.Span, HelpDoc.HelpDoc] + ) => HelpDoc.HelpDoc +>(2, (self, f) => + isDescriptionList(self) + ? descriptionList(Arr.map(self.definitions, ([span, helpDoc]) => f(span, helpDoc))) + : self) + +/** @internal */ +export const toAnsiDoc = (self: HelpDoc.HelpDoc): Doc.AnsiDoc => + Optimize.optimize(toAnsiDocInternal(self), Optimize.Deep) + +/** @internal */ +export const toAnsiText = (self: HelpDoc.HelpDoc): string => Doc.render(toAnsiDoc(self), { style: "pretty" }) + +// ============================================================================= +// Internals +// ============================================================================= + +const toAnsiDocInternal = (self: HelpDoc.HelpDoc): Doc.AnsiDoc => { + switch (self._tag) { + case "Empty": { + return Doc.empty + } + case "Header": { + return pipe( + Doc.annotate(InternalSpan.toAnsiDoc(self.value), Ansi.bold), + Doc.cat(Doc.hardLine) + ) + } + case "Paragraph": { + return pipe( + InternalSpan.toAnsiDoc(self.value), + Doc.cat(Doc.hardLine) + ) + } + case "DescriptionList": { + const definitions = self.definitions.map(([span, doc]) => + Doc.cats([ + Doc.annotate(InternalSpan.toAnsiDoc(span), Ansi.bold), + Doc.empty, + Doc.indent(toAnsiDocInternal(doc), 2) + ]) + ) + return Doc.vsep(definitions) + } + case "Enumeration": { + const elements = self.elements.map((doc) => Doc.cat(Doc.text("- "), toAnsiDocInternal(doc))) + return Doc.indent(Doc.vsep(elements), 2) + } + case "Sequence": { + return Doc.vsep([ + toAnsiDocInternal(self.left), + toAnsiDocInternal(self.right) + ]) + } + } +} diff --git a/repos/effect/packages/cli/src/internal/helpDoc/span.ts b/repos/effect/packages/cli/src/internal/helpDoc/span.ts new file mode 100644 index 0000000..249ea31 --- /dev/null +++ b/repos/effect/packages/cli/src/internal/helpDoc/span.ts @@ -0,0 +1,145 @@ +import * as Ansi from "@effect/printer-ansi/Ansi" +import * as Doc from "@effect/printer-ansi/AnsiDoc" +import * as Color from "@effect/printer-ansi/Color" +import * as Arr from "effect/Array" +import { dual } from "effect/Function" +import type * as Span from "../../HelpDoc/Span.js" + +/** @internal */ +export const text = (value: string): Span.Span => ({ + _tag: "Text", + value +}) + +/** @internal */ +export const empty: Span.Span = text("") + +/** @internal */ +export const space: Span.Span = text(" ") + +/** @internal */ +export const code = (value: Span.Span | string): Span.Span => highlight(value, Color.white) + +/** @internal */ +export const error = (value: Span.Span | string): Span.Span => highlight(value, Color.red) + +/** @internal */ +export const highlight = (value: Span.Span | string, color: Color.Color): Span.Span => ({ + _tag: "Highlight", + value: typeof value === "string" ? text(value) : value, + color +}) + +/** @internal */ +export const strong = (value: Span.Span | string): Span.Span => ({ + _tag: "Strong", + value: typeof value === "string" ? text(value) : value +}) + +/** @internal */ +export const uri = (value: string): Span.Span => ({ + _tag: "URI", + value +}) + +/** @internal */ +export const weak = (value: Span.Span | string): Span.Span => ({ + _tag: "Weak", + value: typeof value === "string" ? text(value) : value +}) + +/** @internal */ +export const isSequence = (self: Span.Span): self is Span.Sequence => self._tag === "Sequence" + +/** @internal */ +export const isStrong = (self: Span.Span): self is Span.Strong => self._tag === "Strong" + +/** @internal */ +export const isText = (self: Span.Span): self is Span.Text => self._tag === "Text" + +/** @internal */ +export const isUri = (self: Span.Span): self is Span.URI => self._tag === "URI" + +/** @internal */ +export const isWeak = (self: Span.Span): self is Span.Weak => self._tag === "Weak" + +/** @internal */ +export const concat = dual< + (that: Span.Span) => (self: Span.Span) => Span.Span, + (self: Span.Span, that: Span.Span) => Span.Span +>(2, (self, that): Span.Span => ({ + _tag: "Sequence", + left: self, + right: that +})) + +export const getText = (self: Span.Span): string => { + switch (self._tag) { + case "Text": + case "URI": { + return self.value + } + case "Highlight": + case "Weak": + case "Strong": { + return getText(self.value) + } + case "Sequence": { + return getText(self.left) + getText(self.right) + } + } +} + +/** @internal */ +export const spans = (spans: Iterable): Span.Span => { + const elements = Arr.fromIterable(spans) + if (Arr.isNonEmptyReadonlyArray(elements)) { + return elements.slice(1).reduce(concat, elements[0]) + } + return empty +} + +/** @internal */ +export const isEmpty = (self: Span.Span): boolean => size(self) === 0 + +/** @internal */ +export const size = (self: Span.Span): number => { + switch (self._tag) { + case "Text": + case "URI": { + return self.value.length + } + case "Highlight": + case "Strong": + case "Weak": { + return size(self.value) + } + case "Sequence": { + return size(self.left) + size(self.right) + } + } +} + +/** @internal */ +export const toAnsiDoc = (self: Span.Span): Doc.AnsiDoc => { + switch (self._tag) { + case "Highlight": { + return Doc.annotate(toAnsiDoc(self.value), Ansi.color(self.color)) + } + case "Sequence": { + return Doc.cat(toAnsiDoc(self.left), toAnsiDoc(self.right)) + } + case "Strong": { + return Doc.annotate(toAnsiDoc(self.value), Ansi.bold) + } + case "Text": { + return Doc.text(self.value) + } + case "URI": { + return Doc.annotate(Doc.text(self.value), Ansi.underlined) + } + case "Weak": { + return Doc.annotate(toAnsiDoc(self.value), Ansi.black) + } + } +} diff --git a/repos/effect/packages/cli/src/internal/options.ts b/repos/effect/packages/cli/src/internal/options.ts new file mode 100644 index 0000000..d6f5207 --- /dev/null +++ b/repos/effect/packages/cli/src/internal/options.ts @@ -0,0 +1,2219 @@ +import type * as FileSystem from "@effect/platform/FileSystem" +import type * as Path from "@effect/platform/Path" +import type * as Terminal from "@effect/platform/Terminal" +import * as Arr from "effect/Array" +import * as Config from "effect/Config" +import * as ConfigError from "effect/ConfigError" +import * as Console from "effect/Console" +import * as Effect from "effect/Effect" +import * as Either from "effect/Either" +import { dual, pipe } from "effect/Function" +import * as HashMap from "effect/HashMap" +import * as Inspectable from "effect/Inspectable" +import * as Option from "effect/Option" +import * as Order from "effect/Order" +import * as ParseResult from "effect/ParseResult" +import { pipeArguments } from "effect/Pipeable" +import * as Predicate from "effect/Predicate" +import type * as Redacted from "effect/Redacted" +import * as Ref from "effect/Ref" +import type * as Schema from "effect/Schema" +import type * as Secret from "effect/Secret" +import type * as CliConfig from "../CliConfig.js" +import type * as HelpDoc from "../HelpDoc.js" +import type * as Options from "../Options.js" +import type * as Primitive from "../Primitive.js" +import type * as Prompt from "../Prompt.js" +import type * as Usage from "../Usage.js" +import type * as ValidationError from "../ValidationError.js" +import * as InternalAutoCorrect from "./autoCorrect.js" +import * as InternalCliConfig from "./cliConfig.js" +import * as InternalFiles from "./files.js" +import * as InternalHelpDoc from "./helpDoc.js" +import * as InternalSpan from "./helpDoc/span.js" +import * as InternalPrimitive from "./primitive.js" +import * as InternalPrompt from "./prompt.js" +import * as InternalListPrompt from "./prompt/list.js" +import * as InternalNumberPrompt from "./prompt/number.js" +import * as InternalSelectPrompt from "./prompt/select.js" +import * as InternalUsage from "./usage.js" +import * as InternalValidationError from "./validationError.js" + +const OptionsSymbolKey = "@effect/cli/Options" + +/** @internal */ +export const OptionsTypeId: Options.OptionsTypeId = Symbol.for( + OptionsSymbolKey +) as Options.OptionsTypeId + +/** @internal */ +export type Op = Options.Options & Body & { + readonly _tag: Tag +} + +const proto = { + [OptionsTypeId]: { + _A: (_: never) => _ + }, + pipe() { + return pipeArguments(this, arguments) + } +} + +/** @internal */ +export type Instruction = + | Empty + | Single + | KeyValueMap + | Map + | Both + | OrElse + | WithFallback + | Variadic + | WithDefault + +/** @internal */ +export type ParseableInstruction = Single | KeyValueMap | Variadic + +/** @internal */ +export interface Empty extends Op<"Empty", {}> {} + +/** @internal */ +export interface Single extends + Op<"Single", { + readonly name: string + readonly fullName: string + readonly placeholder: string + readonly aliases: ReadonlyArray + readonly primitiveType: Primitive.Primitive + readonly description: HelpDoc.HelpDoc + readonly pseudoName: Option.Option + }> +{} + +/** @internal */ +export interface KeyValueMap extends + Op<"KeyValueMap", { + readonly argumentOption: Single + }> +{} + +/** @internal */ +export interface Map extends + Op<"Map", { + readonly options: Options.Options + readonly f: (a: unknown) => Effect.Effect< + unknown, + ValidationError.ValidationError, + FileSystem.FileSystem | Path.Path | Terminal.Terminal + > + }> +{} + +/** @internal */ +export interface Both extends + Op<"Both", { + readonly left: Options.Options + readonly right: Options.Options + }> +{} + +/** @internal */ +export interface OrElse extends + Op<"OrElse", { + readonly left: Options.Options + readonly right: Options.Options + }> +{} + +/** @internal */ +export interface WithFallback extends + Op<"WithFallback", { + readonly options: Options.Options + readonly effect: Effect.Effect< + unknown, + unknown, + FileSystem.FileSystem | Path.Path | Terminal.Terminal + > + }> +{} + +/** @internal */ +export interface Variadic extends + Op<"Variadic", { + readonly argumentOption: Single + readonly min: Option.Option + readonly max: Option.Option + }> +{} + +/** @internal */ +export interface WithDefault extends + Op<"WithDefault", { + readonly options: Options.Options + readonly fallback: unknown + }> +{} + +// ============================================================================= +// Refinements +// ============================================================================= + +/** @internal */ +export const isOptions = (u: unknown): u is Options.Options => + typeof u === "object" && u != null && OptionsTypeId in u + +/** @internal */ +export const isInstruction = <_>(self: Options.Options<_>): self is Instruction => self as any + +/** @internal */ +export const isEmpty = (self: Instruction): self is Empty => self._tag === "Empty" + +/** @internal */ +export const isSingle = (self: Instruction): self is Single => self._tag === "Single" + +/** @internal */ +export const isKeyValueMap = (self: Instruction): self is KeyValueMap => self._tag === "KeyValueMap" + +/** @internal */ +export const isMap = (self: Instruction): self is Map => self._tag === "Map" + +/** @internal */ +export const isBoth = (self: Instruction): self is Both => self._tag === "Both" + +/** @internal */ +export const isOrElse = (self: Instruction): self is OrElse => self._tag === "OrElse" + +/** @internal */ +export const isWithDefault = (self: Instruction): self is WithDefault => self._tag === "WithDefault" + +/** @internal */ +export const isWithFallback = (self: Instruction): self is WithFallback => self._tag === "WithFallback" + +// ============================================================================= +// Constructors +// ============================================================================= + +/** @internal */ +export const all: < + const Arg extends Iterable> | Record> +>(arg: Arg) => Options.All.Return = function() { + if (arguments.length === 1) { + if (isOptions(arguments[0])) { + return map(arguments[0], (x) => [x]) as any + } else if (Arr.isArray(arguments[0])) { + return allTupled(arguments[0] as Array) as any + } else { + const entries = Object.entries( + arguments[0] as Readonly<{ [K: string]: Options.Options }> + ) + let result = map(entries[0][1], (value) => ({ [entries[0][0]]: value })) + if (entries.length === 1) { + return result as any + } + const rest = entries.slice(1) + for (const [key, options] of rest) { + result = map(makeBoth(result, options), ([record, value]) => ({ + ...record, + [key]: value + })) + } + return result as any + } + } + return allTupled(arguments[0]) as any +} + +const defaultBooleanOptions = { + ifPresent: true, + negationNames: [], + aliases: [] +} + +/** @internal */ +export const boolean = ( + name: string, + options?: Options.Options.BooleanOptionsConfig +): Options.Options => { + const { aliases, ifPresent, negationNames } = { ...defaultBooleanOptions, ...options } + const option = makeSingle( + name, + aliases, + InternalPrimitive.boolean(Option.some(ifPresent)) + ) + if (Arr.isNonEmptyReadonlyArray(negationNames)) { + const head = Arr.headNonEmpty(negationNames) + const tail = Arr.tailNonEmpty(negationNames) + const negationOption = makeSingle( + head, + tail, + InternalPrimitive.boolean(Option.some(!ifPresent)) + ) + return withDefault( + orElse(option, negationOption), + !ifPresent + ) + } + return withDefault(option, !ifPresent) +} + +/** @internal */ +export const choice = >( + name: string, + choices: C +): Options.Options => { + const primitive = InternalPrimitive.choice( + Arr.map(choices, (choice) => [choice, choice]) + ) + return makeSingle(name, Arr.empty(), primitive) +} + +/** @internal */ +export const choiceWithValue = >( + name: string, + choices: C +): Options.Options => makeSingle(name, Arr.empty(), InternalPrimitive.choice(choices)) + +/** @internal */ +export const date = (name: string): Options.Options => makeSingle(name, Arr.empty(), InternalPrimitive.date) + +/** @internal */ +export const directory = ( + name: string, + config?: Options.Options.PathOptionsConfig +): Options.Options => + makeSingle( + name, + Arr.empty(), + InternalPrimitive.path("directory", config?.exists ?? "either") + ) + +/** @internal */ +export const file = ( + name: string, + config?: Options.Options.PathOptionsConfig +): Options.Options => + makeSingle( + name, + Arr.empty(), + InternalPrimitive.path("file", config?.exists ?? "either") + ) + +/** @internal */ +export const fileContent = ( + name: string +): Options.Options => + mapEffect(file(name, { exists: "yes" }), (path) => + Effect.mapError( + InternalFiles.read(path), + (msg) => InternalValidationError.invalidValue(InternalHelpDoc.p(msg)) + )) + +/** @internal */ +export const fileParse = ( + name: string, + format?: "json" | "yaml" | "ini" | "toml" +): Options.Options => + mapEffect(fileText(name), ([path, content]) => + Effect.mapError( + InternalFiles.parse(path, content, format), + (error) => InternalValidationError.invalidValue(InternalHelpDoc.p(error)) + )) + +/** @internal */ +export const fileSchema = ( + name: string, + schema: Schema.Schema, + format?: "json" | "yaml" | "ini" | "toml" +): Options.Options => withSchema(fileParse(name, format), schema) + +/** @internal */ +export const fileText = ( + name: string +): Options.Options => + mapEffect(file(name, { exists: "yes" }), (path) => + Effect.mapError( + InternalFiles.readString(path), + (error) => InternalValidationError.invalidValue(InternalHelpDoc.p(error)) + )) + +/** @internal */ +export const filterMap = dual< + ( + f: (a: A) => Option.Option, + message: string + ) => (self: Options.Options) => Options.Options, + ( + self: Options.Options, + f: (a: A) => Option.Option, + message: string + ) => Options.Options +>(3, (self, f, message) => + mapEffect(self, (a) => + Option.match(f(a), { + onNone: () => Either.left(InternalValidationError.invalidValue(InternalHelpDoc.p(message))), + onSome: Either.right + }))) + +/** @internal */ +export const float = (name: string): Options.Options => makeSingle(name, Arr.empty(), InternalPrimitive.float) + +/** @internal */ +export const integer = (name: string): Options.Options => + makeSingle(name, Arr.empty(), InternalPrimitive.integer) + +/** @internal */ +export const keyValueMap = ( + option: string | Options.Options +): Options.Options> => { + if (typeof option === "string") { + const single = makeSingle(option, Arr.empty(), InternalPrimitive.text) + return makeKeyValueMap(single as Single) + } + if (!isSingle(option as Instruction)) { + throw new Error("InvalidArgumentException: only single options can be key/value maps") + } else { + return makeKeyValueMap(option as Single) + } +} + +/** @internal */ +export const none: Options.Options = (() => { + const op = Object.create(proto) + op._tag = "Empty" + return op +})() + +/** @internal */ +export const redacted = (name: string): Options.Options => + makeSingle(name, Arr.empty(), InternalPrimitive.redacted) + +/** @internal */ +export const secret = (name: string): Options.Options => + makeSingle(name, Arr.empty(), InternalPrimitive.secret) + +/** @internal */ +export const text = (name: string): Options.Options => makeSingle(name, Arr.empty(), InternalPrimitive.text) + +// ============================================================================= +// Combinators +// ============================================================================= + +/** @internal */ +export const atLeast = dual< + { + (times: 0): (self: Options.Options) => Options.Options> + ( + times: number + ): (self: Options.Options) => Options.Options> + }, + { + (self: Options.Options, times: 0): Options.Options> + ( + self: Options.Options, + times: number + ): Options.Options> + } +>(2, (self, times) => makeVariadic(self, Option.some(times), Option.none()) as any) + +/** @internal */ +export const atMost = dual< + (times: number) => (self: Options.Options) => Options.Options>, + (self: Options.Options, times: number) => Options.Options> +>(2, (self, times) => makeVariadic(self, Option.none(), Option.some(times)) as any) + +/** @internal */ +export const between = dual< + { + (min: 0, max: number): (self: Options.Options) => Options.Options> + ( + min: number, + max: number + ): (self: Options.Options) => Options.Options> + }, + { + (self: Options.Options, min: 0, max: number): Options.Options> + ( + self: Options.Options, + min: number, + max: number + ): Options.Options> + } +>(3, (self, min, max) => makeVariadic(self, Option.some(min), Option.some(max)) as any) + +/** @internal */ +export const isBool = (self: Options.Options): boolean => isBoolInternal(self as Instruction) + +/** @internal */ +export const getHelp = (self: Options.Options): HelpDoc.HelpDoc => getHelpInternal(self as Instruction) + +/** @internal */ +export const getIdentifier = (self: Options.Options): Option.Option => + getIdentifierInternal(self as Instruction) + +/** @internal */ +export const getMinSize = (self: Options.Options): number => getMinSizeInternal(self as Instruction) + +/** @internal */ +export const getMaxSize = (self: Options.Options): number => getMaxSizeInternal(self as Instruction) + +/** @internal */ +export const getUsage = (self: Options.Options): Usage.Usage => getUsageInternal(self as Instruction) + +/** @internal */ +export const map = dual< + (f: (a: A) => B) => (self: Options.Options) => Options.Options, + (self: Options.Options, f: (a: A) => B) => Options.Options +>(2, (self, f) => makeMap(self, (a) => Either.right(f(a)))) + +/** @internal */ +export const mapEffect = dual< + ( + f: ( + a: A + ) => Effect.Effect + ) => (self: Options.Options) => Options.Options, + ( + self: Options.Options, + f: ( + a: A + ) => Effect.Effect + ) => Options.Options +>(2, (self, f) => makeMap(self, f)) + +/** @internal */ +export const mapTryCatch = dual< + ( + f: (a: A) => B, + onError: (e: unknown) => HelpDoc.HelpDoc + ) => (self: Options.Options) => Options.Options, + ( + self: Options.Options, + f: (a: A) => B, + onError: (e: unknown) => HelpDoc.HelpDoc + ) => Options.Options +>(3, (self, f, onError) => + mapEffect(self, (a) => { + try { + return Either.right(f(a)) + } catch (e) { + return Either.left(InternalValidationError.invalidValue(onError(e))) + } + })) + +/** @internal */ +export const optional = (self: Options.Options): Options.Options> => + withDefault(map(self, Option.some), Option.none()) + +/** @internal */ +export const orElse = dual< + (that: Options.Options) => (self: Options.Options) => Options.Options, + (self: Options.Options, that: Options.Options) => Options.Options +>(2, (self, that) => orElseEither(self, that).pipe(map(Either.merge))) + +/** @internal */ +export const orElseEither = dual< + ( + that: Options.Options + ) => (self: Options.Options) => Options.Options>, + (self: Options.Options, that: Options.Options) => Options.Options> +>(2, (self, that) => makeOrElse(self, that)) + +/** @internal */ +export const parse = dual< + ( + args: HashMap.HashMap>, + config: CliConfig.CliConfig + ) => ( + self: Options.Options + ) => Effect.Effect, + ( + self: Options.Options, + args: HashMap.HashMap>, + config: CliConfig.CliConfig + ) => Effect.Effect +>(3, (self, args, config) => parseInternal(self as Instruction, args, config) as any) + +/** @internal */ +export const processCommandLine = dual< + ( + args: ReadonlyArray, + config: CliConfig.CliConfig + ) => ( + self: Options.Options + ) => Effect.Effect< + [Option.Option, Array, A], + ValidationError.ValidationError, + FileSystem.FileSystem | Path.Path | Terminal.Terminal + >, + ( + self: Options.Options, + args: ReadonlyArray, + config: CliConfig.CliConfig + ) => Effect.Effect< + [Option.Option, Array, A], + ValidationError.ValidationError, + FileSystem.FileSystem | Path.Path | Terminal.Terminal + > +>( + 3, + (self, args, config) => + matchOptions(args, toParseableInstruction(self as Instruction), config).pipe( + Effect.flatMap(([error, commandArgs, matchedOptions]) => + parseInternal(self as Instruction, matchedOptions, config).pipe( + Effect.catchAll((e) => + Option.match(error, { + onNone: () => Effect.fail(e), + onSome: (err) => Effect.fail(err) + }) + ), + Effect.map((a) => [error, commandArgs as Array, a as any]) + ) + ) + ) +) + +/** @internal */ +export const repeated = (self: Options.Options): Options.Options> => + makeVariadic(self, Option.none(), Option.none()) + +/** @internal */ +export const withAlias = dual< + (alias: string) => (self: Options.Options) => Options.Options, + (self: Options.Options, alias: string) => Options.Options +>(2, (self, alias) => + modifySingle(self as Instruction, (single) => { + const aliases = Arr.append(single.aliases, alias) + return makeSingle( + single.name, + aliases, + single.primitiveType, + single.description, + single.pseudoName + ) as Single + })) + +/** @internal */ +export const withDefault = dual< + (fallback: B) => (self: Options.Options) => Options.Options, + (self: Options.Options, fallback: B) => Options.Options +>(2, (self, fallback) => makeWithDefault(self, fallback)) + +/** @internal */ +export const withFallbackConfig: { + (config: Config.Config): (self: Options.Options) => Options.Options + (self: Options.Options, config: Config.Config): Options.Options +} = dual< + (config: Config.Config) => (self: Options.Options) => Options.Options, + (self: Options.Options, config: Config.Config) => Options.Options +>(2, (self, config) => { + if (isInstruction(self) && isWithDefault(self)) { + return makeWithDefault( + withFallbackConfig(self.options, config), + self.fallback as any + ) + } + return makeWithFallback(self, config) +}) + +/** @internal */ +export const withFallbackPrompt: { + (prompt: Prompt.Prompt): (self: Options.Options) => Options.Options + (self: Options.Options, prompt: Prompt.Prompt): Options.Options +} = dual< + (prompt: Prompt.Prompt) => (self: Options.Options) => Options.Options, + (self: Options.Options, prompt: Prompt.Prompt) => Options.Options +>(2, (self, prompt) => { + if (isInstruction(self) && isWithDefault(self)) { + return makeWithDefault( + withFallbackPrompt(self.options, prompt), + self.fallback as any + ) + } + return makeWithFallback(self, prompt) +}) + +/** @internal */ +export const withDescription = dual< + (description: string) => (self: Options.Options) => Options.Options, + (self: Options.Options, description: string) => Options.Options +>(2, (self, desc) => + modifySingle(self as Instruction, (single) => { + const description = InternalHelpDoc.sequence(single.description, InternalHelpDoc.p(desc)) + return makeSingle( + single.name, + single.aliases, + single.primitiveType, + description, + single.pseudoName + ) as Single + })) + +/** @internal */ +export const withPseudoName = dual< + (pseudoName: string) => (self: Options.Options) => Options.Options, + (self: Options.Options, pseudoName: string) => Options.Options +>(2, (self, pseudoName) => + modifySingle(self as Instruction, (single) => + makeSingle( + single.name, + single.aliases, + single.primitiveType, + single.description, + Option.some(pseudoName) + ) as Single)) + +/** @internal */ +export const withSchema = dual< + ( + schema: Schema.Schema + ) => (self: Options.Options) => Options.Options, + ( + self: Options.Options, + schema: Schema.Schema + ) => Options.Options +>(2, (self, schema) => { + const decode = ParseResult.decode(schema) + return mapEffect(self, (_) => + Effect.mapError( + decode(_ as any), + (issue) => + InternalValidationError.invalidValue(InternalHelpDoc.p(ParseResult.TreeFormatter.formatIssueSync(issue))) + )) +}) + +/** @internal */ +export const wizard = dual< + (config: CliConfig.CliConfig) => (self: Options.Options) => Effect.Effect< + Array, + Terminal.QuitException | ValidationError.ValidationError, + FileSystem.FileSystem | Path.Path | Terminal.Terminal + >, + (self: Options.Options, config: CliConfig.CliConfig) => Effect.Effect< + Array, + Terminal.QuitException | ValidationError.ValidationError, + FileSystem.FileSystem | Path.Path | Terminal.Terminal + > +>(2, (self, config) => wizardInternal(self as Instruction, config)) + +// ============================================================================= +// Internals +// ============================================================================= + +const allTupled = >>(arg: T): Options.Options< + { + [K in keyof T]: [T[K]] extends [Options.Options] ? A : never + } +> => { + if (arg.length === 0) { + return none as any + } + if (arg.length === 1) { + return map(arg[0], (x) => [x]) as any + } + let result = map(arg[0], (x) => [x]) + for (let i = 1; i < arg.length; i++) { + const curr = arg[i] + result = map(makeBoth(result, curr), ([a, b]) => [...a, b]) + } + return result as any +} + +const getHelpInternal = (self: Instruction): HelpDoc.HelpDoc => { + switch (self._tag) { + case "Empty": { + return InternalHelpDoc.empty + } + case "Single": { + return InternalHelpDoc.descriptionList(Arr.of([ + InternalHelpDoc.getSpan(InternalUsage.getHelp(getUsageInternal(self))), + InternalHelpDoc.sequence( + InternalHelpDoc.p(InternalPrimitive.getHelp(self.primitiveType)), + self.description + ) + ])) + } + case "KeyValueMap": { + // Single options always have an identifier, so we can safely `getOrThrow` + const identifier = Option.getOrThrow( + getIdentifierInternal(self.argumentOption as Instruction) + ) + return InternalHelpDoc.mapDescriptionList( + getHelpInternal(self.argumentOption as Instruction), + (span, oldBlock) => { + const header = InternalHelpDoc.p("This setting is a property argument which:") + const single = `${identifier} key1=value key2=value2` + const multiple = `${identifier} key1=value ${identifier} key2=value2` + const description = InternalHelpDoc.enumeration([ + InternalHelpDoc.p(`May be specified a single time: '${single}'`), + InternalHelpDoc.p(`May be specified multiple times: '${multiple}'`) + ]) + const newBlock = pipe( + oldBlock, + InternalHelpDoc.sequence(header), + InternalHelpDoc.sequence(description) + ) + return [span, newBlock] + } + ) + } + case "Map": { + return getHelpInternal(self.options as Instruction) + } + case "Both": + case "OrElse": { + return InternalHelpDoc.sequence( + getHelpInternal(self.left as Instruction), + getHelpInternal(self.right as Instruction) + ) + } + case "Variadic": { + const help = getHelpInternal(self.argumentOption as Instruction) + return InternalHelpDoc.mapDescriptionList(help, (oldSpan, oldBlock) => { + const min = getMinSizeInternal(self as Instruction) + const max = getMaxSizeInternal(self as Instruction) + const newSpan = InternalSpan.text( + Option.isSome(self.max) ? ` ${min} - ${max}` : min === 0 ? "..." : ` ${min}+` + ) + const newBlock = InternalHelpDoc.p( + Option.isSome(self.max) + ? `This option must be repeated at least ${min} times and may be repeated up to ${max} times.` + : min === 0 + ? "This option may be repeated zero or more times." + : `This option must be repeated at least ${min} times.` + ) + return [InternalSpan.concat(oldSpan, newSpan), InternalHelpDoc.sequence(oldBlock, newBlock)] + }) + } + case "WithDefault": { + return InternalHelpDoc.mapDescriptionList( + getHelpInternal(self.options as Instruction), + (span, block) => { + const optionalDescription = Option.isOption(self.fallback) + ? Option.match(self.fallback, { + onNone: () => InternalHelpDoc.p("This setting is optional."), + onSome: (fallbackValue) => { + const inspectableValue = Predicate.isObject(fallbackValue) ? fallbackValue : String(fallbackValue) + const displayValue = Inspectable.toStringUnknown(inspectableValue, 0) + return InternalHelpDoc.p(`This setting is optional. Defaults to: ${displayValue}`) + } + }) + : InternalHelpDoc.p("This setting is optional.") + return [span, InternalHelpDoc.sequence(block, optionalDescription)] + } + ) + } + case "WithFallback": { + const helpDoc: HelpDoc.HelpDoc = Config.isConfig(self.effect) + ? InternalHelpDoc.p("This option can be set from environment variables.") + : InternalPrompt.isPrompt(self.effect) + ? InternalHelpDoc.p("Will prompt the user for input if this option is not provided.") + : InternalHelpDoc.empty + return InternalHelpDoc.mapDescriptionList( + getHelpInternal(self.options as Instruction), + (span, block) => [span, InternalHelpDoc.sequence(block, helpDoc)] + ) + } + } +} + +const getIdentifierInternal = (self: Instruction): Option.Option => { + switch (self._tag) { + case "Empty": { + return Option.none() + } + case "Single": { + return Option.some(self.fullName) + } + case "Both": + case "OrElse": { + const ids = Arr.getSomes([ + getIdentifierInternal(self.left as Instruction), + getIdentifierInternal(self.right as Instruction) + ]) + return Arr.match(ids, { + onEmpty: () => Option.none(), + onNonEmpty: (ids) => Option.some(Arr.join(ids, ", ")) + }) + } + case "KeyValueMap": + case "Variadic": { + return getIdentifierInternal(self.argumentOption as Instruction) + } + case "Map": + case "WithFallback": + case "WithDefault": { + return getIdentifierInternal(self.options as Instruction) + } + } +} + +const getMinSizeInternal = (self: Instruction): number => { + switch (self._tag) { + case "Empty": + case "WithDefault": + case "WithFallback": { + return 0 + } + case "Single": + case "KeyValueMap": { + return 1 + } + case "Map": { + return getMinSizeInternal(self.options as Instruction) + } + case "Both": { + const leftMinSize = getMinSizeInternal(self.left as Instruction) + const rightMinSize = getMinSizeInternal(self.right as Instruction) + return leftMinSize + rightMinSize + } + case "OrElse": { + const leftMinSize = getMinSizeInternal(self.left as Instruction) + const rightMinSize = getMinSizeInternal(self.right as Instruction) + return Math.min(leftMinSize, rightMinSize) + } + case "Variadic": { + const selfMinSize = Option.getOrElse(self.min, () => 0) + const argumentOptionMinSize = getMinSizeInternal(self.argumentOption as Instruction) + return selfMinSize * argumentOptionMinSize + } + } +} + +const getMaxSizeInternal = (self: Instruction): number => { + switch (self._tag) { + case "Empty": { + return 0 + } + case "Single": { + return 1 + } + case "KeyValueMap": { + return Number.MAX_SAFE_INTEGER + } + case "Map": + case "WithDefault": + case "WithFallback": { + return getMaxSizeInternal(self.options as Instruction) + } + case "Both": { + const leftMaxSize = getMaxSizeInternal(self.left as Instruction) + const rightMaxSize = getMaxSizeInternal(self.right as Instruction) + return leftMaxSize + rightMaxSize + } + case "OrElse": { + const leftMin = getMaxSizeInternal(self.left as Instruction) + const rightMin = getMaxSizeInternal(self.right as Instruction) + return Math.min(leftMin, rightMin) + } + case "Variadic": { + const selfMaxSize = Option.getOrElse(self.max, () => Number.MAX_SAFE_INTEGER / 2) + const optionsMaxSize = getMaxSizeInternal(self.argumentOption as Instruction) + return Math.floor(selfMaxSize * optionsMaxSize) + } + } +} + +const getUsageInternal = (self: Instruction): Usage.Usage => { + switch (self._tag) { + case "Empty": { + return InternalUsage.empty + } + case "Single": { + const acceptedValues = InternalPrimitive.isBool(self.primitiveType) + ? Option.none() + : Option.orElse( + InternalPrimitive.getChoices(self.primitiveType), + () => Option.some(self.placeholder) + ) + return InternalUsage.named(getNames(self), acceptedValues) + } + case "KeyValueMap": { + return getUsageInternal(self.argumentOption as Instruction) + } + case "Map": { + return getUsageInternal(self.options as Instruction) + } + case "Both": { + return InternalUsage.concat( + getUsageInternal(self.left as Instruction), + getUsageInternal(self.right as Instruction) + ) + } + case "OrElse": { + return InternalUsage.alternation( + getUsageInternal(self.left as Instruction), + getUsageInternal(self.right as Instruction) + ) + } + case "Variadic": { + return InternalUsage.repeated(getUsageInternal(self.argumentOption as Instruction)) + } + case "WithDefault": + case "WithFallback": { + return InternalUsage.optional(getUsageInternal(self.options as Instruction)) + } + } +} + +const isBoolInternal = (self: Instruction): boolean => { + switch (self._tag) { + case "Single": { + return InternalPrimitive.isBool(self.primitiveType) + } + case "Map": { + return isBoolInternal(self.options as Instruction) + } + case "WithDefault": { + return isBoolInternal(self.options as Instruction) + } + default: { + return false + } + } +} + +const makeBoth = ( + left: Options.Options, + right: Options.Options +): Options.Options<[A, B]> => { + const op = Object.create(proto) + op._tag = "Both" + op.left = left + op.right = right + return op +} + +const makeFullName = (str: string): [boolean, string] => str.length === 1 ? [true, `-${str}`] : [false, `--${str}`] + +const makeKeyValueMap = ( + argumentOption: Single +): Options.Options> => { + const op = Object.create(proto) + op._tag = "KeyValueMap" + op.argumentOption = argumentOption + return op +} + +const makeMap = ( + options: Options.Options, + f: (a: A) => Effect.Effect +): Options.Options => { + const op = Object.create(proto) + op._tag = "Map" + op.options = options + op.f = f + return op +} + +const makeOrElse = ( + left: Options.Options, + right: Options.Options +): Options.Options> => { + const op = Object.create(proto) + op._tag = "OrElse" + op.left = left + op.right = right + return op +} + +const makeSingle = ( + name: string, + aliases: ReadonlyArray, + primitiveType: Primitive.Primitive, + description: HelpDoc.HelpDoc = InternalHelpDoc.empty, + pseudoName: Option.Option = Option.none() +): Options.Options => { + const op = Object.create(proto) + op._tag = "Single" + op.name = name + op.fullName = makeFullName(name)[1] + op.placeholder = `${Option.getOrElse(pseudoName, () => InternalPrimitive.getTypeName(primitiveType))}` + op.aliases = aliases + op.primitiveType = primitiveType + op.description = description + op.pseudoName = pseudoName + return op +} + +const makeVariadic = ( + argumentOption: Options.Options, + min: Option.Option, + max: Option.Option +): Options.Options> => { + if (!isSingle(argumentOption as Instruction)) { + throw new Error("InvalidArgumentException: only single options can be variadic") + } + const op = Object.create(proto) + op._tag = "Variadic" + op.argumentOption = argumentOption + op.min = min + op.max = max + return op +} + +const makeWithDefault = ( + options: Options.Options, + fallback: B +): Options.Options => { + const op = Object.create(proto) + op._tag = "WithDefault" + op.options = options + op.fallback = fallback + return op +} + +const makeWithFallback = ( + options: Options.Options, + effect: Effect.Effect +): Options.Options => { + const op = Object.create(proto) + op._tag = "WithFallback" + op.options = options + op.effect = effect + return op +} + +const modifySingle = (self: Instruction, f: (single: Single) => Single): Options.Options => { + switch (self._tag) { + case "Empty": { + return none + } + case "Single": { + return f(self) + } + case "KeyValueMap": { + return makeKeyValueMap(f(self.argumentOption)) + } + case "Map": { + return makeMap(modifySingle(self.options as Instruction, f), self.f) + } + case "Both": { + return makeBoth( + modifySingle(self.left as Instruction, f), + modifySingle(self.right as Instruction, f) + ) + } + case "OrElse": { + return makeOrElse( + modifySingle(self.left as Instruction, f), + modifySingle(self.right as Instruction, f) + ) + } + case "Variadic": { + return makeVariadic(f(self.argumentOption), self.min, self.max) + } + case "WithDefault": { + return makeWithDefault(modifySingle(self.options as Instruction, f), self.fallback) + } + case "WithFallback": { + return makeWithFallback( + modifySingle(self.options as Instruction, f), + self.effect + ) + } + } +} + +/** @internal */ +export const getNames = (self: Instruction): Array => { + const loop = (self: Instruction): ReadonlyArray => { + switch (self._tag) { + case "Empty": { + return Arr.empty() + } + case "Single": { + return Arr.prepend(self.aliases, self.name) + } + case "KeyValueMap": + case "Variadic": { + return loop(self.argumentOption as Instruction) + } + case "Map": + case "WithDefault": + case "WithFallback": { + return loop(self.options as Instruction) + } + case "Both": + case "OrElse": { + const left = loop(self.left as Instruction) + const right = loop(self.right as Instruction) + return Arr.appendAll(left, right) + } + } + } + const order = Order.mapInput( + Order.boolean, + (tuple: [boolean, string]) => !tuple[0] + ) + return pipe( + loop(self), + Arr.map((str) => makeFullName(str)), + Arr.sort(order), + Arr.map((tuple) => tuple[1]) + ) +} + +const toParseableInstruction = (self: Instruction): Array => { + switch (self._tag) { + case "Empty": { + return Arr.empty() + } + case "Single": + case "KeyValueMap": + case "Variadic": { + return Arr.of(self) + } + case "Map": + case "WithDefault": + case "WithFallback": { + return toParseableInstruction(self.options as Instruction) + } + case "Both": + case "OrElse": { + return Arr.appendAll( + toParseableInstruction(self.left as Instruction), + toParseableInstruction(self.right as Instruction) + ) + } + } +} + +/** @internal */ +const keyValueSplitter = /=(.*)/ + +const parseInternal = ( + self: Instruction, + args: HashMap.HashMap>, + config: CliConfig.CliConfig +): Effect.Effect< + unknown, + ValidationError.ValidationError, + FileSystem.FileSystem | Path.Path | Terminal.Terminal +> => { + switch (self._tag) { + case "Empty": { + return Effect.void + } + case "Single": { + const singleNames = Arr.filterMap(getNames(self), (name) => HashMap.get(args, name)) + if (Arr.isNonEmptyReadonlyArray(singleNames)) { + const head = Arr.headNonEmpty(singleNames) + const tail = Arr.tailNonEmpty(singleNames) + if (Arr.isEmptyReadonlyArray(tail)) { + if (Arr.isEmptyReadonlyArray(head)) { + return InternalPrimitive.validate(self.primitiveType, Option.none(), config).pipe( + Effect.mapError((e) => InternalValidationError.invalidValue(InternalHelpDoc.p(e))) + ) + } + if ( + Arr.isNonEmptyReadonlyArray(head) && + Arr.isEmptyReadonlyArray(Arr.tailNonEmpty(head)) + ) { + const value = Arr.headNonEmpty(head) + return InternalPrimitive.validate(self.primitiveType, Option.some(value), config).pipe( + Effect.mapError((e) => InternalValidationError.invalidValue(InternalHelpDoc.p(e))) + ) + } + return Effect.fail( + InternalValidationError.multipleValuesDetected(InternalHelpDoc.empty, head) + ) + } + const error = InternalHelpDoc.p( + `More than one reference to option '${self.fullName}' detected` + ) + return Effect.fail(InternalValidationError.invalidValue(error)) + } + const error = InternalHelpDoc.p(`Expected to find option: '${self.fullName}'`) + return Effect.fail(InternalValidationError.missingValue(error)) + } + case "KeyValueMap": { + const extractKeyValue = ( + value: string + ): Effect.Effect<[string, string], ValidationError.ValidationError> => { + const split = value.trim().split(keyValueSplitter, 2) + if (Arr.isNonEmptyReadonlyArray(split) && split.length === 2 && split[1] !== "") { + return Effect.succeed(split as unknown as [string, string]) + } + const error = InternalHelpDoc.p(`Expected a key/value pair but received '${value}'`) + return Effect.fail(InternalValidationError.invalidArgument(error)) + } + return parseInternal(self.argumentOption, args, config).pipe(Effect.matchEffect({ + onFailure: (e) => + InternalValidationError.isMultipleValuesDetected(e) + ? Effect.forEach(e.values, (kv) => extractKeyValue(kv)).pipe( + Effect.map(HashMap.fromIterable) + ) + : Effect.fail(e), + onSuccess: (kv) => extractKeyValue(kv as string).pipe(Effect.map(HashMap.make)) + })) + } + case "Map": { + return parseInternal(self.options as Instruction, args, config).pipe( + Effect.flatMap((a) => self.f(a)) + ) + } + case "Both": { + return parseInternal(self.left as Instruction, args, config).pipe( + Effect.catchAll((err1) => + parseInternal(self.right as Instruction, args, config).pipe(Effect.matchEffect({ + onFailure: (err2) => { + const error = InternalHelpDoc.sequence(err1.error, err2.error) + return Effect.fail(InternalValidationError.missingValue(error)) + }, + onSuccess: () => Effect.fail(err1) + })) + ), + Effect.zip(parseInternal(self.right as Instruction, args, config)) + ) + } + case "OrElse": { + return parseInternal(self.left as Instruction, args, config).pipe( + Effect.matchEffect({ + onFailure: (err1) => + parseInternal(self.right as Instruction, args, config).pipe( + Effect.mapBoth({ + onFailure: (err2) => + // orElse option is only missing in case neither option was given + InternalValidationError.isMissingValue(err1) && + InternalValidationError.isMissingValue(err2) + ? InternalValidationError.missingValue( + InternalHelpDoc.sequence(err1.error, err2.error) + ) + : InternalValidationError.invalidValue( + InternalHelpDoc.sequence(err1.error, err2.error) + ), + onSuccess: (b) => Either.right(b) + }) + ), + onSuccess: (a) => + parseInternal(self.right as Instruction, args, config).pipe(Effect.matchEffect({ + onFailure: () => Effect.succeed(Either.left(a)), + onSuccess: () => { + // The `identifier` will only be `None` for `Options.Empty`, which + // means the user would have had to purposefully compose + // `Options.Empty | otherArgument` + const leftUid = Option.getOrElse( + getIdentifierInternal(self.left as Instruction), + () => "???" + ) + const rightUid = Option.getOrElse( + getIdentifierInternal(self.right as Instruction), + () => "???" + ) + const error = InternalHelpDoc.p( + "Collision between two options detected - you can only specify " + + `one of either: ['${leftUid}', '${rightUid}']` + ) + return Effect.fail(InternalValidationError.invalidValue(error)) + } + })) + }) + ) + } + case "Variadic": { + const min = Option.getOrElse(self.min, () => 0) + const max = Option.getOrElse(self.max, () => Number.MAX_SAFE_INTEGER) + const matchedArgument = Arr.filterMap(getNames(self), (name) => HashMap.get(args, name)) + const validateMinMax = (values: ReadonlyArray) => { + if (values.length < min) { + const name = self.argumentOption.fullName + const error = `Expected at least ${min} value(s) for option: '${name}'` + return Effect.fail(InternalValidationError.invalidValue(InternalHelpDoc.p(error))) + } + if (values.length > max) { + const name = self.argumentOption.fullName + const error = `Expected at most ${max} value(s) for option: '${name}'` + return Effect.fail(InternalValidationError.invalidValue(InternalHelpDoc.p(error))) + } + const primitive = self.argumentOption.primitiveType + const validatePrimitive = (value: string) => + InternalPrimitive.validate(primitive, Option.some(value), config).pipe( + Effect.mapError((e) => InternalValidationError.invalidValue(InternalHelpDoc.p(e))) + ) + return Effect.forEach(values, (value) => validatePrimitive(value)) + } + // If we did not receive any variadic arguments then perform the bounds + // checks with an empty array + if (Arr.every(matchedArgument, Arr.isEmptyReadonlyArray)) { + return validateMinMax(Arr.empty()) + } + return parseInternal(self.argumentOption, args, config).pipe(Effect.matchEffect({ + onFailure: (error) => + InternalValidationError.isMultipleValuesDetected(error) + ? validateMinMax(error.values) + : Effect.fail(error), + onSuccess: (value) => validateMinMax(Arr.of(value as string)) + })) + } + case "WithDefault": { + return parseInternal(self.options as Instruction, args, config).pipe( + Effect.catchTag("MissingValue", () => Effect.succeed(self.fallback)) + ) + } + case "WithFallback": { + return parseInternal(self.options as Instruction, args, config).pipe( + Effect.catchTag("MissingValue", (e) => + self.effect.pipe(Effect.catchAll((e2) => { + if (Predicate.isTagged(e2, "QuitException")) { + return Effect.die(e2) + } + if (ConfigError.isConfigError(e2) && !ConfigError.isMissingDataOnly(e2)) { + const help = InternalHelpDoc.p(String(e2)) + const error = InternalValidationError.invalidValue(help) + return Effect.fail(error) + } + return Effect.fail(e) + }))) + ) + } + } +} + +const wizardInternal = (self: Instruction, config: CliConfig.CliConfig): Effect.Effect< + Array, + Terminal.QuitException | ValidationError.ValidationError, + FileSystem.FileSystem | Path.Path | Terminal.Terminal +> => { + switch (self._tag) { + case "Empty": { + return Effect.succeed(Arr.empty()) + } + case "Single": { + const help = getHelpInternal(self) + return InternalPrimitive.wizard(self.primitiveType, help).pipe( + Effect.flatMap((input) => { + // There will always be at least one name in names + const args = Arr.make(getNames(self)[0]!, input as string) + return parseCommandLine(self, args, config).pipe(Effect.as(args)) + }), + Effect.zipLeft(Console.log()) + ) + } + case "KeyValueMap": { + const message = InternalHelpDoc.p("Enter `key=value` pairs separated by spaces") + return InternalListPrompt.list({ + message: InternalHelpDoc.toAnsiText(message).trim(), + delimiter: " " + }).pipe( + Effect.flatMap((args) => { + const identifier = Option.getOrElse(getIdentifierInternal(self), () => "") + return parseInternal(self, HashMap.make([identifier, args]), config).pipe( + Effect.as(Arr.prepend(args, identifier)) + ) + }), + Effect.zipLeft(Console.log()) + ) + } + case "Map": { + return wizardInternal(self.options as Instruction, config) + } + case "Both": { + return Effect.zipWith( + wizardInternal(self.left as Instruction, config), + wizardInternal(self.right as Instruction, config), + (left, right) => Arr.appendAll(left, right) + ) + } + case "OrElse": { + const alternativeHelp = InternalHelpDoc.p("Select which option you would like to use") + const message = pipe( + getHelpInternal(self), + InternalHelpDoc.sequence(alternativeHelp) + ) + const makeChoice = (title: string, value: Instruction) => ({ title, value }) + const choices = Arr.getSomes([ + Option.map( + getIdentifierInternal(self.left as Instruction), + (title) => makeChoice(title, self.left as Instruction) + ), + Option.map( + getIdentifierInternal(self.right as Instruction), + (title) => makeChoice(title, self.right as Instruction) + ) + ]) + return InternalSelectPrompt.select({ + message: InternalHelpDoc.toAnsiText(message).trimEnd(), + choices + }).pipe(Effect.flatMap((option) => wizardInternal(option, config))) + } + case "Variadic": { + const repeatHelp = InternalHelpDoc.p( + "How many times should this argument be repeated?" + ) + const message = pipe( + getHelpInternal(self), + InternalHelpDoc.sequence(repeatHelp) + ) + return InternalNumberPrompt.integer({ + message: InternalHelpDoc.toAnsiText(message).trimEnd(), + min: getMinSizeInternal(self), + max: getMaxSizeInternal(self) + }).pipe( + Effect.flatMap((n) => + n <= 0 + ? Effect.succeed(Arr.empty()) + : Ref.make(Arr.empty()).pipe( + Effect.flatMap((ref) => + wizardInternal(self.argumentOption as Instruction, config).pipe( + Effect.flatMap((args) => Ref.update(ref, Arr.appendAll(args))), + Effect.repeatN(n - 1), + Effect.zipRight(Ref.get(ref)) + ) + ) + ) + ) + ) + } + case "WithDefault": { + if (isBoolInternal(self.options as Instruction)) { + return wizardInternal(self.options as Instruction, config) + } + const defaultHelp = InternalHelpDoc.p(`This option is optional - use the default?`) + const message = pipe( + getHelpInternal(self.options as Instruction), + InternalHelpDoc.sequence(defaultHelp) + ) + return InternalSelectPrompt.select({ + message: InternalHelpDoc.toAnsiText(message).trimEnd(), + choices: [ + { + title: "Yes", + value: true, + description: `use the default ${ + Option.isOption(self.fallback) + ? Option.match(self.fallback, { + onNone: () => "", + onSome: (a) => `(${JSON.stringify(a)})` + }) + : `(${JSON.stringify(self.fallback)})` + }` + }, + { title: "No", value: false, description: "use a custom value" } + ] + }).pipe( + Effect.zipLeft(Console.log()), + Effect.flatMap((useFallback) => + useFallback + ? Effect.succeed(Arr.empty()) + : wizardInternal(self.options as Instruction, config) + ) + ) + } + case "WithFallback": { + if (isBoolInternal(self.options as Instruction)) { + return wizardInternal(self.options as Instruction, config) + } + // TODO: should we use the prompt directly here? + if (InternalPrompt.isPrompt(self.effect)) { + return wizardInternal(self.options as Instruction, config) + } + const defaultHelp = InternalHelpDoc.p(`Try load this option from the environment?`) + const message = pipe( + getHelpInternal(self.options as Instruction), + InternalHelpDoc.sequence(defaultHelp) + ) + return InternalSelectPrompt.select({ + message: InternalHelpDoc.toAnsiText(message).trimEnd(), + choices: [ + { title: `Use environment variables`, value: true }, + { title: "Custom", value: false } + ] + }).pipe( + Effect.zipLeft(Console.log()), + Effect.flatMap((useFallback) => + useFallback + ? Effect.succeed(Arr.empty()) + : wizardInternal(self.options as Instruction, config) + ) + ) + } + } +} + +// ============================================================================= +// Parsing Internals +// ============================================================================= + +/** + * Returns a possible `ValidationError` when parsing the commands, leftover + * arguments from `input` and a mapping between each flag and its values. + */ +const matchOptions = ( + input: ReadonlyArray, + options: ReadonlyArray, + config: CliConfig.CliConfig +): Effect.Effect< + [ + Option.Option, + ReadonlyArray, + HashMap.HashMap> + ] +> => { + if (Arr.isNonEmptyReadonlyArray(options)) { + return findOptions(input, options, config).pipe( + Effect.flatMap(([otherArgs, otherOptions, map1]) => { + if (HashMap.isEmpty(map1)) { + return Effect.succeed([Option.none(), input, map1] as [ + Option.Option, + ReadonlyArray, + HashMap.HashMap> + ]) + } + return matchOptions(otherArgs, otherOptions, config).pipe( + Effect.map(([error, otherArgs, map2]) => + [error, otherArgs, merge(map1, Arr.fromIterable(map2))] as [ + Option.Option, + ReadonlyArray, + HashMap.HashMap> + ] + ) + ) + }), + Effect.catchAll((e) => + Effect.succeed([Option.some(e), input, HashMap.empty()] as [ + Option.Option, + ReadonlyArray, + HashMap.HashMap> + ]) + ) + ) + } + return Arr.isEmptyReadonlyArray(input) + ? Effect.succeed([Option.none(), Arr.empty(), HashMap.empty()] as [ + Option.Option, + ReadonlyArray, + HashMap.HashMap> + ]) + : Effect.succeed([Option.none(), input, HashMap.empty()] as [ + Option.Option, + ReadonlyArray, + HashMap.HashMap> + ]) +} + +/** + * Returns the leftover arguments, leftover options, and a mapping between the + * first argument with its values if it corresponds to an option flag. + */ +const findOptions = ( + input: ReadonlyArray, + options: ReadonlyArray, + config: CliConfig.CliConfig +): Effect.Effect< + [ + ReadonlyArray, + ReadonlyArray, + HashMap.HashMap> + ], + ValidationError.ValidationError +> => + Arr.matchLeft(options, { + onEmpty: () => Effect.succeed([input, Arr.empty(), HashMap.empty()]), + onNonEmpty: (head, tail) => + parseCommandLine(head, input, config).pipe( + Effect.flatMap(({ leftover, parsed }) => + Option.match(parsed, { + onNone: () => + findOptions(leftover, tail, config).pipe(Effect.map(([nextArgs, nextOptions, map]) => + [nextArgs, Arr.prepend(nextOptions, head), map] as [ + ReadonlyArray, + ReadonlyArray, + HashMap.HashMap> + ] + )), + onSome: ({ name, values }) => + Effect.succeed([leftover, tail, HashMap.make([name, values])] as [ + ReadonlyArray, + ReadonlyArray, + HashMap.HashMap> + ]) + }) + ), + Effect.catchTags({ + CorrectedFlag: (e) => + findOptions(input, tail, config).pipe( + Effect.catchSome(() => Option.some(Effect.fail(e))), + Effect.flatMap(([otherArgs, otherOptions, map]) => + Effect.fail(e).pipe( + Effect.when(() => HashMap.isEmpty(map)), + Effect.as([otherArgs, Arr.prepend(otherOptions, head), map] as [ + ReadonlyArray, + ReadonlyArray, + HashMap.HashMap> + ]) + ) + ) + ), + MissingFlag: () => + findOptions(input, tail, config).pipe( + Effect.map(([otherArgs, otherOptions, map]) => + [otherArgs, Arr.prepend(otherOptions, head), map] as [ + ReadonlyArray, + ReadonlyArray, + HashMap.HashMap> + ] + ) + ), + UnclusteredFlag: (e) => + matchUnclustered(e.unclustered, e.rest, options, config).pipe( + Effect.catchAll(() => Effect.fail(e)) + ) + }) + ) + }) + +interface ParsedCommandLine { + readonly parsed: Option.Option<{ + readonly name: string + readonly values: ReadonlyArray + }> + readonly leftover: ReadonlyArray +} + +const CLUSTERED_REGEX = /^-{1}([^-]{2,}$)/ +const FLAG_REGEX = /^(--[^=]+)(?:=(.+))?$/ + +/** + * Normalizes the leading command-line argument by performing the following: + * 1. If a clustered series of short command-line options is encountered, + * uncluster the options and return a `ValidationError.UnclusteredFlag` + * to be handled later on in the parsing algorithm + * 2. If a long command-line option with a value is encountered, ensure that + * the option and it's value are separated (i.e. `--option=value` becomes + * ["--option", "value"]) + */ +const processArgs = ( + args: ReadonlyArray +): Effect.Effect, ValidationError.ValidationError> => + Arr.matchLeft(args, { + onEmpty: () => Effect.succeed(Arr.empty()), + onNonEmpty: (head, tail) => { + const value = head.trim() + // Attempt to match clustered short command-line arguments (i.e. `-abc`) + if (CLUSTERED_REGEX.test(value)) { + const unclustered = value.substring(1).split("").map((c) => `-${c}`) + return Effect.fail(InternalValidationError.unclusteredFlag( + InternalHelpDoc.empty, + unclustered, + tail + )) + } + // Attempt to match a long command-line argument and ensure the option and + // it's value have been separated and added back to the arguments + if (FLAG_REGEX.test(value)) { + const result = FLAG_REGEX.exec(value) + if (result !== null && result[2] !== undefined) { + return Effect.succeed>( + Arr.appendAll([result[1], result[2]], tail) + ) + } + } + // Otherwise return the original command-line arguments + return Effect.succeed(args) + } + }) + +/** + * Processes the command-line arguments for a parseable option, returning the + * parsed command line results, which inclue: + * - The name of the option and its associated value(s), if any + * - Any leftover command-line arguments + */ +const parseCommandLine = ( + self: ParseableInstruction, + args: ReadonlyArray, + config: CliConfig.CliConfig +): Effect.Effect => { + switch (self._tag) { + case "Single": { + return processArgs(args).pipe(Effect.flatMap((args) => + Arr.matchLeft(args, { + onEmpty: () => { + const error = InternalHelpDoc.p(`Expected to find option: '${self.fullName}'`) + return Effect.fail(InternalValidationError.missingFlag(error)) + }, + onNonEmpty: (head, tail) => { + const normalize = (value: string) => InternalCliConfig.normalizeCase(config, value) + const normalizedHead = normalize(head) + const normalizedNames = Arr.map(getNames(self), (name) => normalize(name)) + + if (Arr.contains(normalizedNames, normalizedHead)) { + if (InternalPrimitive.isBool(self.primitiveType)) { + return Arr.matchLeft(tail, { + onEmpty: () => { + const parsed = Option.some({ name: head, values: Arr.empty() }) + return Effect.succeed({ parsed, leftover: tail }) + }, + onNonEmpty: (value, leftover) => { + if (InternalPrimitive.isTrueValue(value)) { + const parsed = Option.some({ name: head, values: Arr.of("true") }) + return Effect.succeed({ parsed, leftover }) + } + if (InternalPrimitive.isFalseValue(value)) { + const parsed = Option.some({ name: head, values: Arr.of("false") }) + return Effect.succeed({ parsed, leftover }) + } + const parsed = Option.some({ name: head, values: Arr.empty() }) + return Effect.succeed({ parsed, leftover: tail }) + } + }) + } + return Arr.matchLeft(tail, { + onEmpty: () => { + const error = InternalHelpDoc.p( + `Expected a value following option: '${self.fullName}'` + ) + return Effect.fail(InternalValidationError.missingValue(error)) + }, + onNonEmpty: (value, leftover) => { + const parsed = Option.some({ name: head, values: Arr.of(value) }) + return Effect.succeed({ parsed, leftover }) + } + }) + } + + if (head.startsWith("-")) { + if ( + self.name.length > config.autoCorrectLimit + 1 && + InternalAutoCorrect.levensteinDistance(head, self.fullName, config) <= + config.autoCorrectLimit + ) { + const error = InternalHelpDoc.p( + `The flag '${head}' is not recognized. Did you mean '${self.fullName}'?` + ) + return Effect.fail(InternalValidationError.correctedFlag(error)) + } + const error = InternalHelpDoc.p(`Expected to find option: '${self.fullName}'`) + return Effect.fail(InternalValidationError.missingFlag(error)) + } + + let optionIndex = -1 + let equalsValue: string | undefined = undefined + for (let i = 0; i < tail.length; i++) { + const arg = tail[i] + const normalizedArg = normalize(arg) + if (Arr.contains(normalizedNames, normalizedArg)) { + optionIndex = i + break + } + const flagMatch = FLAG_REGEX.exec(arg) + if (flagMatch !== null) { + const normalizedFlag = normalize(flagMatch[1]) + if (Arr.contains(normalizedNames, normalizedFlag)) { + optionIndex = i + equalsValue = flagMatch[2] + break + } + } + } + + if (optionIndex === -1) { + const error = InternalHelpDoc.p(`Expected to find option: '${self.fullName}'`) + return Effect.fail(InternalValidationError.missingFlag(error)) + } + + const rawArg = tail[optionIndex] + const optionName = equalsValue !== undefined ? FLAG_REGEX.exec(rawArg)![1] : rawArg + const beforeOption = Arr.prepend(tail.slice(0, optionIndex), head) + const afterOption = tail.slice(optionIndex + 1) + + if (InternalPrimitive.isBool(self.primitiveType)) { + if (equalsValue !== undefined) { + if (InternalPrimitive.isTrueValue(equalsValue)) { + const parsed = Option.some({ name: optionName, values: Arr.of("true") }) + const leftover = Arr.appendAll(beforeOption, afterOption) + return Effect.succeed({ parsed, leftover }) + } + if (InternalPrimitive.isFalseValue(equalsValue)) { + const parsed = Option.some({ name: optionName, values: Arr.of("false") }) + const leftover = Arr.appendAll(beforeOption, afterOption) + return Effect.succeed({ parsed, leftover }) + } + } + if (afterOption.length > 0) { + const nextValue = afterOption[0] + if (InternalPrimitive.isTrueValue(nextValue)) { + const parsed = Option.some({ name: optionName, values: Arr.of("true") }) + const leftover = Arr.appendAll(beforeOption, afterOption.slice(1)) + return Effect.succeed({ parsed, leftover }) + } + if (InternalPrimitive.isFalseValue(nextValue)) { + const parsed = Option.some({ name: optionName, values: Arr.of("false") }) + const leftover = Arr.appendAll(beforeOption, afterOption.slice(1)) + return Effect.succeed({ parsed, leftover }) + } + } + const parsed = Option.some({ name: optionName, values: Arr.empty() }) + const leftover = Arr.appendAll(beforeOption, afterOption) + return Effect.succeed({ parsed, leftover }) + } + + if (equalsValue !== undefined) { + const parsed = Option.some({ name: optionName, values: Arr.of(equalsValue) }) + const leftover = Arr.appendAll(beforeOption, afterOption) + return Effect.succeed({ parsed, leftover }) + } + + if (afterOption.length === 0) { + const error = InternalHelpDoc.p( + `Expected a value following option: '${self.fullName}'` + ) + return Effect.fail(InternalValidationError.missingValue(error)) + } + + const optionValue = afterOption[0] + const parsed = Option.some({ name: optionName, values: Arr.of(optionValue) }) + const leftover = Arr.appendAll(beforeOption, afterOption.slice(1)) + return Effect.succeed({ parsed, leftover }) + } + }) + )) + } + case "KeyValueMap": { + const normalizedNames = Arr.map( + getNames(self.argumentOption), + (name) => InternalCliConfig.normalizeCase(config, name) + ) + return Arr.matchLeft(args, { + onEmpty: () => Effect.succeed({ parsed: Option.none(), leftover: args }), + onNonEmpty: (head, tail) => { + const loop = ( + args: ReadonlyArray + ): [ReadonlyArray, ReadonlyArray] => { + let keyValues = Arr.empty() + let leftover = args as ReadonlyArray + while (Arr.isNonEmptyReadonlyArray(leftover)) { + const name = Arr.headNonEmpty(leftover).trim() + const normalizedName = InternalCliConfig.normalizeCase(config, name) + // Can be in the form of "--flag key1=value1 --flag key2=value2" + if (leftover.length >= 2 && Arr.contains(normalizedNames, normalizedName)) { + const keyValue = leftover[1].trim() + const [key, value] = keyValue.split("=") + if (key !== undefined && value !== undefined && value.length > 0) { + keyValues = Arr.append(keyValues, keyValue) + leftover = leftover.slice(2) + continue + } + } + // Can be in the form of "--flag key1=value1 key2=value2") + if (name.includes("=")) { + const [key, value] = name.split("=") + if (key !== undefined && value !== undefined && value.length > 0) { + keyValues = Arr.append(keyValues, name) + leftover = leftover.slice(1) + continue + } + } + break + } + return [keyValues, leftover] + } + const normalizedName = InternalCliConfig.normalizeCase(config, head) + if (Arr.contains(normalizedNames, normalizedName)) { + const [values, leftover] = loop(tail) + return Effect.succeed({ parsed: Option.some({ name: head, values }), leftover }) + } + + if (head.startsWith("-")) { + return Effect.succeed({ parsed: Option.none(), leftover: args }) + } + + let optionIndex = -1 + for (let i = 0; i < tail.length; i++) { + const arg = tail[i] + const normalizedArg = InternalCliConfig.normalizeCase(config, arg) + if (Arr.contains(normalizedNames, normalizedArg)) { + optionIndex = i + break + } + } + + if (optionIndex === -1) { + return Effect.succeed({ parsed: Option.none(), leftover: args }) + } + + const optionName = tail[optionIndex] + const beforeOption = Arr.prepend(tail.slice(0, optionIndex), head) + const afterOption = tail.slice(optionIndex + 1) + const [values, remaining] = loop(afterOption) + const leftover = Arr.appendAll(beforeOption, remaining) + return Effect.succeed({ parsed: Option.some({ name: optionName, values }), leftover }) + } + }) + } + case "Variadic": { + const normalizedNames = Arr.map( + getNames(self.argumentOption), + (name) => InternalCliConfig.normalizeCase(config, name) + ) + let optionName: string | undefined = undefined + let values = Arr.empty() + let unparsed = args as ReadonlyArray + let leftover = Arr.empty() + while (Arr.isNonEmptyReadonlyArray(unparsed)) { + const name = Arr.headNonEmpty(unparsed) + const normalizedName = InternalCliConfig.normalizeCase(config, name) + + if (Arr.contains(normalizedNames, normalizedName)) { + if (optionName === undefined) { + optionName = name + } + const value = unparsed[1] + if (value !== undefined && value.length > 0) { + values = Arr.append(values, value.trim()) + } + unparsed = unparsed.slice(2) + } else { + leftover = Arr.append(leftover, Arr.headNonEmpty(unparsed)) + unparsed = unparsed.slice(1) + } + } + const parsed = Option.fromNullable(optionName).pipe( + Option.orElse(() => Option.some(self.argumentOption.fullName)), + Option.map((name) => ({ name, values })) + ) + return Effect.succeed({ parsed, leftover }) + } + } +} + +const matchUnclustered = ( + input: ReadonlyArray, + tail: ReadonlyArray, + options: ReadonlyArray, + config: CliConfig.CliConfig +): Effect.Effect< + [ + ReadonlyArray, + ReadonlyArray, + HashMap.HashMap> + ], + ValidationError.ValidationError +> => { + if (Arr.isNonEmptyReadonlyArray(input)) { + const flag = Arr.headNonEmpty(input) + const otherFlags = Arr.tailNonEmpty(input) + return findOptions(Arr.of(flag), options, config).pipe( + Effect.flatMap(([_, opts1, map1]) => { + if (HashMap.isEmpty(map1)) { + return Effect.fail( + InternalValidationError.unclusteredFlag( + InternalHelpDoc.empty, + Arr.empty(), + tail + ) + ) + } + return matchUnclustered(otherFlags, tail, opts1, config).pipe( + Effect.map(( + [_, opts2, map2] + ) => [tail, opts2, merge(map1, Arr.fromIterable(map2))]) + ) + }) + ) + } + return Effect.succeed([tail, options, HashMap.empty()]) +} + +/** + * Sums the list associated with the same key. + */ +const merge = ( + map1: HashMap.HashMap>, + map2: ReadonlyArray<[string, ReadonlyArray]> +): HashMap.HashMap> => { + if (Arr.isNonEmptyReadonlyArray(map2)) { + const head = Arr.headNonEmpty(map2) + const tail = Arr.tailNonEmpty(map2) + const newMap = Option.match(HashMap.get(map1, head[0]), { + onNone: () => HashMap.set(map1, head[0], head[1]), + onSome: (elems) => HashMap.set(map1, head[0], Arr.appendAll(elems, head[1])) + }) + return merge(newMap, tail) + } + return map1 +} + +// ============================================================================= +// Completion Internals +// ============================================================================= + +const escape = (string: string): string => + string + .replaceAll("\\", "\\\\") + .replaceAll("'", "'\\''") + .replaceAll("[", "\\[") + .replaceAll("]", "\\]") + .replaceAll(":", "\\:") + .replaceAll("$", "\\$") + .replaceAll("`", "\\`") + .replaceAll("(", "\\(") + .replaceAll(")", "\\)") + +const getShortDescription = (self: Instruction): string => { + switch (self._tag) { + case "Empty": + case "Both": + case "OrElse": { + return "" + } + case "Single": { + return InternalSpan.getText(InternalHelpDoc.getSpan(self.description)) + } + case "KeyValueMap": + case "Variadic": { + return getShortDescription(self.argumentOption as Instruction) + } + case "Map": + case "WithDefault": + case "WithFallback": { + return getShortDescription(self.options as Instruction) + } + } +} + +/** @internal */ +export const getBashCompletions = (self: Instruction): ReadonlyArray => { + switch (self._tag) { + case "Empty": { + return Arr.empty() + } + case "Single": { + const names = getNames(self) + const cases = Arr.join(names, "|") + const compgen = InternalPrimitive.getBashCompletions( + self.primitiveType as InternalPrimitive.Instruction + ) + return Arr.make( + `${cases})`, + ` COMPREPLY=( ${compgen} )`, + ` return 0`, + ` ;;` + ) + } + case "KeyValueMap": + case "Variadic": { + return getBashCompletions(self.argumentOption as Instruction) + } + case "Map": + case "WithDefault": + case "WithFallback": { + return getBashCompletions(self.options as Instruction) + } + case "Both": + case "OrElse": { + const left = getBashCompletions(self.left as Instruction) + const right = getBashCompletions(self.right as Instruction) + return Arr.appendAll(left, right) + } + } +} + +/** @internal */ +export const getFishCompletions = (self: Instruction): Array => { + switch (self._tag) { + case "Empty": { + return Arr.empty() + } + case "Single": { + const description = getShortDescription(self) + const order = Order.mapInput(Order.boolean, (tuple: readonly [boolean, string]) => !tuple[0]) + return pipe( + Arr.prepend(self.aliases, self.name), + Arr.map((name) => [name.length === 1, name] as const), + Arr.sort(order), + Arr.flatMap(([isShort, name]) => Arr.make(isShort ? "-s" : "-l", name)), + Arr.appendAll(InternalPrimitive.getFishCompletions( + self.primitiveType as InternalPrimitive.Instruction + )), + Arr.appendAll( + description.length === 0 + ? Arr.empty() + : Arr.of(`-d '${description}'`) + ), + Arr.join(" "), + Arr.of + ) + } + case "KeyValueMap": + case "Variadic": { + return getFishCompletions(self.argumentOption as Instruction) + } + case "Map": + case "WithDefault": + case "WithFallback": { + return getFishCompletions(self.options as Instruction) + } + case "Both": + case "OrElse": { + return pipe( + getFishCompletions(self.left as Instruction), + Arr.appendAll(getFishCompletions(self.right as Instruction)) + ) + } + } +} + +interface ZshCompletionState { + readonly conflicts: ReadonlyArray + readonly multiple: boolean +} + +/** @internal */ +export const getZshCompletions = ( + self: Instruction, + state: ZshCompletionState = { conflicts: Arr.empty(), multiple: false } +): Array => { + switch (self._tag) { + case "Empty": { + return Arr.empty() + } + case "Single": { + const names = getNames(self) + const description = getShortDescription(self) + const possibleValues = InternalPrimitive.getZshCompletions( + self.primitiveType as InternalPrimitive.Instruction + ) + const multiple = state.multiple ? "*" : "" + const conflicts = Arr.isNonEmptyReadonlyArray(state.conflicts) + ? `(${Arr.join(state.conflicts, " ")})` + : "" + return Arr.map( + names, + (name) => `${conflicts}${multiple}${name}[${escape(description)}]${possibleValues}` + ) + } + case "KeyValueMap": { + return getZshCompletions(self.argumentOption as Instruction, { ...state, multiple: true }) + } + case "Map": + case "WithDefault": + case "WithFallback": { + return getZshCompletions(self.options as Instruction, state) + } + case "Both": { + const left = getZshCompletions(self.left as Instruction, state) + const right = getZshCompletions(self.right as Instruction, state) + return Arr.appendAll(left, right) + } + case "OrElse": { + const leftNames = getNames(self.left as Instruction) + const rightNames = getNames(self.right as Instruction) + const left = getZshCompletions( + self.left as Instruction, + { ...state, conflicts: Arr.appendAll(state.conflicts, rightNames) } + ) + const right = getZshCompletions( + self.right as Instruction, + { ...state, conflicts: Arr.appendAll(state.conflicts, leftNames) } + ) + return Arr.appendAll(left, right) + } + case "Variadic": { + return Option.isSome(self.max) && self.max.value > 1 + ? getZshCompletions(self.argumentOption as Instruction, { ...state, multiple: true }) + : getZshCompletions(self.argumentOption as Instruction, state) + } + } +} diff --git a/repos/effect/packages/cli/src/internal/primitive.ts b/repos/effect/packages/cli/src/internal/primitive.ts new file mode 100644 index 0000000..d3fb2f6 --- /dev/null +++ b/repos/effect/packages/cli/src/internal/primitive.ts @@ -0,0 +1,763 @@ +import * as FileSystem from "@effect/platform/FileSystem" +import * as Arr from "effect/Array" +import * as Effect from "effect/Effect" +import { dual, pipe } from "effect/Function" +import * as Option from "effect/Option" +import { pipeArguments } from "effect/Pipeable" +import * as EffectRedacted from "effect/Redacted" +import * as Schema from "effect/Schema" +import * as EffectSecret from "effect/Secret" +import type * as CliConfig from "../CliConfig.js" +import type * as HelpDoc from "../HelpDoc.js" +import type * as Span from "../HelpDoc/Span.js" +import type * as Primitive from "../Primitive.js" +import type * as Prompt from "../Prompt.js" +import * as InternalCliConfig from "./cliConfig.js" +import * as InternalHelpDoc from "./helpDoc.js" +import * as InternalSpan from "./helpDoc/span.js" +import * as InternalPrompt from "./prompt.js" +import * as InternalDatePrompt from "./prompt/date.js" +import * as InternalFilePrompt from "./prompt/file.js" +import * as InternalNumberPrompt from "./prompt/number.js" +import * as InternalSelectPrompt from "./prompt/select.js" +import * as InternalTextPrompt from "./prompt/text.js" +import * as InternalTogglePrompt from "./prompt/toggle.js" + +const PrimitiveSymbolKey = "@effect/cli/Primitive" + +/** @internal */ +export const PrimitiveTypeId: Primitive.PrimitiveTypeId = Symbol.for( + PrimitiveSymbolKey +) as Primitive.PrimitiveTypeId + +/** @internal */ +export type Op = Primitive.Primitive & Body & { + readonly _tag: Tag +} + +const proto = { + [PrimitiveTypeId]: { + _A: (_: never) => _ + }, + pipe() { + return pipeArguments(this, arguments) + } +} + +/** @internal */ +export type Instruction = + | Bool + | Choice + | DateTime + | Float + | Integer + | Path + | Secret + | Redacted + | Text + +/** @internal */ +export interface Bool extends + Op<"Bool", { + readonly defaultValue: Option.Option + }> +{} + +/** @internal */ +export interface Choice extends + Op<"Choice", { + readonly alternatives: ReadonlyArray<[string, unknown]> + }> +{} + +/** @internal */ +export interface DateTime extends Op<"DateTime", {}> {} + +/** @internal */ +export interface Float extends Op<"Float", {}> {} + +/** @internal */ +export interface Integer extends Op<"Integer", {}> {} + +/** @internal */ +export interface Path extends + Op<"Path", { + readonly pathType: Primitive.Primitive.PathType + readonly pathExists: Primitive.Primitive.PathExists + }> +{} + +/** @internal */ +export interface Redacted extends Op<"Redacted", {}> {} + +/** @internal */ +export interface Secret extends Op<"Secret", {}> {} + +/** @internal */ +export interface Text extends Op<"Text", {}> {} + +// ============================================================================= +// Refinements +// ============================================================================= + +/** @internal */ +export const isPrimitive = (u: unknown): u is Primitive.Primitive => + typeof u === "object" && u != null && PrimitiveTypeId in u + +/** @internal */ +export const isBool = (self: Primitive.Primitive): boolean => isPrimitive(self) && isBoolType(self as Instruction) + +/** @internal */ +export const isBoolType = (self: Instruction): self is Bool => self._tag === "Bool" + +/** @internal */ +export const isChoiceType = (self: Instruction): self is Choice => self._tag === "Choice" + +/** @internal */ +export const isDateTimeType = (self: Instruction): self is DateTime => self._tag === "DateTime" + +/** @internal */ +export const isFloatType = (self: Instruction): self is Float => self._tag === "Float" + +/** @internal */ +export const isIntegerType = (self: Instruction): self is Integer => self._tag === "Integer" + +/** @internal */ +export const isPathType = (self: Instruction): self is Path => self._tag === "Path" + +/** @internal */ +export const isSecretType = (self: Instruction): self is Path => self._tag === "Path" + +/** @internal */ +export const isTextType = (self: Instruction): self is Text => self._tag === "Text" + +// ============================================================================= +// Constructors +// ============================================================================= + +/** @internal */ +export const trueValues = Schema.Literal("true", "1", "y", "yes", "on") + +/** @internal */ +export const isTrueValue = Schema.is(trueValues) + +/** @internal */ +export const falseValues = Schema.Literal("false", "0", "n", "no", "off") + +/** @internal */ +export const isFalseValue = Schema.is(falseValues) + +/** @internal */ +export const boolean = (defaultValue: Option.Option): Primitive.Primitive => { + const op = Object.create(proto) + op._tag = "Bool" + op.defaultValue = defaultValue + return op +} + +/** @internal */ +export const choice = ( + alternatives: ReadonlyArray<[string, A]> +): Primitive.Primitive => { + const op = Object.create(proto) + op._tag = "Choice" + op.alternatives = alternatives + return op +} + +/** @internal */ +export const date: Primitive.Primitive = (() => { + const op = Object.create(proto) + op._tag = "DateTime" + return op +})() + +/** @internal */ +export const float: Primitive.Primitive = (() => { + const op = Object.create(proto) + op._tag = "Float" + return op +})() + +/** @internal */ +export const integer: Primitive.Primitive = (() => { + const op = Object.create(proto) + op._tag = "Integer" + return op +})() + +/** @internal */ +export const path = ( + pathType: Primitive.Primitive.PathType, + pathExists: Primitive.Primitive.PathExists +): Primitive.Primitive => { + const op = Object.create(proto) + op._tag = "Path" + op.pathType = pathType + op.pathExists = pathExists + return op +} + +/** @internal */ +export const redacted: Primitive.Primitive = (() => { + const op = Object.create(proto) + op._tag = "Redacted" + return op +})() + +/** @internal */ +export const secret: Primitive.Primitive = (() => { + const op = Object.create(proto) + op._tag = "Secret" + return op +})() + +/** @internal */ +export const text: Primitive.Primitive = (() => { + const op = Object.create(proto) + op._tag = "Text" + return op +})() + +// ============================================================================= +// Combinators +// ============================================================================= + +/** @internal */ +export const getChoices = (self: Primitive.Primitive): Option.Option => + getChoicesInternal(self as Instruction) + +/** @internal */ +export const getHelp = (self: Primitive.Primitive): Span.Span => getHelpInternal(self as Instruction) + +/** @internal */ +export const getTypeName = (self: Primitive.Primitive): string => getTypeNameInternal(self as Instruction) + +/** @internal */ +export const validate = dual< + ( + value: Option.Option, + config: CliConfig.CliConfig + ) => (self: Primitive.Primitive) => Effect.Effect< + A, + string, + FileSystem.FileSystem + >, + ( + self: Primitive.Primitive, + value: Option.Option, + config: CliConfig.CliConfig + ) => Effect.Effect< + A, + string, + FileSystem.FileSystem + > +>(3, (self, value, config) => validateInternal(self as Instruction, value, config)) + +/** @internal */ +export const wizard = dual< + (help: HelpDoc.HelpDoc) => (self: Primitive.Primitive) => Prompt.Prompt, + (self: Primitive.Primitive, help: HelpDoc.HelpDoc) => Prompt.Prompt +>(2, (self, help) => wizardInternal(self as Instruction, help)) + +// ============================================================================= +// Internals +// ============================================================================= + +const getChoicesInternal = (self: Instruction): Option.Option => { + switch (self._tag) { + case "Bool": { + return Option.some("true | false") + } + case "Choice": { + const choices = pipe( + Arr.map(self.alternatives, ([choice]) => choice), + Arr.join(" | ") + ) + return Option.some(choices) + } + case "DateTime": { + return Option.some("date") + } + case "Float": + case "Integer": + case "Path": + case "Redacted": + case "Secret": + case "Text": { + return Option.none() + } + } +} + +const getHelpInternal = (self: Instruction): Span.Span => { + switch (self._tag) { + case "Bool": { + return InternalSpan.text("A true or false value.") + } + case "Choice": { + const choices = pipe( + Arr.map(self.alternatives, ([choice]) => choice), + Arr.join(", ") + ) + return InternalSpan.text(`One of the following: ${choices}`) + } + case "DateTime": { + return InternalSpan.text( + "A date without a time-zone in the ISO-8601 format, such as 2007-12-03T10:15:30." + ) + } + case "Float": { + return InternalSpan.text("A floating point number.") + } + case "Integer": { + return InternalSpan.text("An integer.") + } + case "Path": { + if (self.pathType === "either" && self.pathExists === "yes") { + return InternalSpan.text("An existing file or directory.") + } + if (self.pathType === "file" && self.pathExists === "yes") { + return InternalSpan.text("An existing file.") + } + if (self.pathType === "directory" && self.pathExists === "yes") { + return InternalSpan.text("An existing directory.") + } + if (self.pathType === "either" && self.pathExists === "no") { + return InternalSpan.text("A file or directory that must not exist.") + } + if (self.pathType === "file" && self.pathExists === "no") { + return InternalSpan.text("A file that must not exist.") + } + if (self.pathType === "directory" && self.pathExists === "no") { + return InternalSpan.text("A directory that must not exist.") + } + if (self.pathType === "either" && self.pathExists === "either") { + return InternalSpan.text("A file or directory.") + } + if (self.pathType === "file" && self.pathExists === "either") { + return InternalSpan.text("A file.") + } + if (self.pathType === "directory" && self.pathExists === "either") { + return InternalSpan.text("A directory.") + } + throw new Error( + "[BUG]: Path.help - encountered invalid combination of path type " + + `('${self.pathType}') and path existence ('${self.pathExists}')` + ) + } + case "Secret": + case "Redacted": { + return InternalSpan.text("A user-defined piece of text that is confidential.") + } + case "Text": { + return InternalSpan.text("A user-defined piece of text.") + } + } +} + +const getTypeNameInternal = (self: Instruction): string => { + switch (self._tag) { + case "Bool": { + return "boolean" + } + case "Choice": { + return "choice" + } + case "DateTime": { + return "date" + } + case "Float": { + return "float" + } + case "Integer": { + return "integer" + } + case "Path": { + if (self.pathType === "either") { + return "path" + } + return self.pathType + } + case "Redacted": { + return "redacted" + } + case "Secret": { + return "secret" + } + case "Text": { + return "text" + } + } +} + +const validateInternal = ( + self: Instruction, + value: Option.Option, + config: CliConfig.CliConfig +): Effect.Effect => { + switch (self._tag) { + case "Bool": { + return Option.map(value, (str) => InternalCliConfig.normalizeCase(config, str)).pipe( + Option.match({ + onNone: () => + Effect.orElseFail( + self.defaultValue, + () => `Missing default value for boolean parameter` + ), + onSome: (value) => + isTrueValue(value) + ? Effect.succeed(true) + : isFalseValue(value) + ? Effect.succeed(false) + : Effect.fail(`Unable to recognize '${value}' as a valid boolean`) + }) + ) + } + case "Choice": { + return Effect.orElseFail( + value, + () => `Choice options to not have a default value` + ).pipe( + Effect.flatMap((value) => Arr.findFirst(self.alternatives, ([choice]) => choice === value)), + Effect.mapBoth({ + onFailure: () => { + const choices = pipe( + Arr.map(self.alternatives, ([choice]) => choice), + Arr.join(", ") + ) + return `Expected one of the following cases: ${choices}` + }, + onSuccess: ([, value]) => value + }) + ) + } + case "DateTime": { + return attempt(value, getTypeNameInternal(self), Schema.decodeUnknown(Schema.Date)) + } + case "Float": { + return attempt(value, getTypeNameInternal(self), Schema.decodeUnknown(Schema.NumberFromString)) + } + case "Integer": { + const intFromString = Schema.compose(Schema.NumberFromString, Schema.Int) + return attempt(value, getTypeNameInternal(self), Schema.decodeUnknown(intFromString)) + } + case "Path": { + return Effect.flatMap(FileSystem.FileSystem, (fileSystem) => { + const errorMsg = "Path options do not have a default value" + return Effect.orElseFail(value, () => errorMsg).pipe( + Effect.tap((path) => + Effect.orDie(fileSystem.exists(path)).pipe( + Effect.tap((pathExists) => + validatePathExistence(path, self.pathExists, pathExists).pipe( + Effect.zipRight( + validatePathType(path, self.pathType, fileSystem).pipe( + Effect.when(() => self.pathExists !== "no" && pathExists) + ) + ) + ) + ) + ) + ) + ) + }) + } + case "Redacted": { + return attempt(value, getTypeNameInternal(self), Schema.decodeUnknown(Schema.String)).pipe( + Effect.map((value) => EffectRedacted.make(value)) + ) + } + case "Secret": { + return attempt(value, getTypeNameInternal(self), Schema.decodeUnknown(Schema.String)).pipe( + Effect.map((value) => EffectSecret.fromString(value)) + ) + } + case "Text": { + return attempt(value, getTypeNameInternal(self), Schema.decodeUnknown(Schema.String)) + } + } +} + +const attempt = ( + option: Option.Option, + typeName: string, + parse: (value: string) => Effect.Effect +): Effect.Effect => + Effect.orElseFail( + option, + () => `${typeName} options do not have a default value` + ).pipe( + Effect.flatMap((value) => + Effect.orElseFail( + parse(value), + () => `'${value}' is not a ${typeName}` + ) + ) + ) + +const validatePathExistence = ( + path: string, + shouldPathExist: Primitive.Primitive.PathExists, + pathExists: boolean +): Effect.Effect => { + if (shouldPathExist === "no" && pathExists) { + return Effect.fail(`Path '${path}' must not exist`) + } + if (shouldPathExist === "yes" && !pathExists) { + return Effect.fail(`Path '${path}' must exist`) + } + return Effect.void +} + +const validatePathType = ( + path: string, + pathType: Primitive.Primitive.PathType, + fileSystem: FileSystem.FileSystem +): Effect.Effect => { + switch (pathType) { + case "file": { + const checkIsFile = fileSystem.stat(path).pipe( + Effect.map((info) => info.type === "File"), + Effect.orDie + ) + return Effect.fail(`Expected path '${path}' to be a regular file`).pipe( + Effect.unlessEffect(checkIsFile), + Effect.asVoid + ) + } + case "directory": { + const checkIsDirectory = fileSystem.stat(path).pipe( + Effect.map((info) => info.type === "Directory"), + Effect.orDie + ) + return Effect.fail(`Expected path '${path}' to be a directory`).pipe( + Effect.unlessEffect(checkIsDirectory), + Effect.asVoid + ) + } + case "either": { + return Effect.void + } + } +} + +const wizardInternal = (self: Instruction, help: HelpDoc.HelpDoc): Prompt.Prompt => { + switch (self._tag) { + case "Bool": { + const primitiveHelp = InternalHelpDoc.p("Select true or false") + const message = InternalHelpDoc.sequence(help, primitiveHelp) + const initial = Option.getOrElse(self.defaultValue, () => false) + return InternalTogglePrompt.toggle({ + message: InternalHelpDoc.toAnsiText(message).trimEnd(), + initial, + active: "true", + inactive: "false" + }).pipe(InternalPrompt.map((bool) => `${bool}`)) + } + case "Choice": { + const primitiveHelp = InternalHelpDoc.p("Select one of the following choices") + const message = InternalHelpDoc.sequence(help, primitiveHelp) + return InternalSelectPrompt.select({ + message: InternalHelpDoc.toAnsiText(message).trimEnd(), + choices: Arr.map( + self.alternatives, + ([title]) => ({ title, value: title }) + ) + }) + } + case "DateTime": { + const primitiveHelp = InternalHelpDoc.p("Enter a date") + const message = InternalHelpDoc.sequence(help, primitiveHelp) + return InternalDatePrompt.date({ + message: InternalHelpDoc.toAnsiText(message).trimEnd() + }).pipe(InternalPrompt.map((date) => date.toISOString())) + } + case "Float": { + const primitiveHelp = InternalHelpDoc.p("Enter a floating point value") + const message = InternalHelpDoc.sequence(help, primitiveHelp) + return InternalNumberPrompt.float({ + message: InternalHelpDoc.toAnsiText(message).trimEnd() + }).pipe(InternalPrompt.map((value) => `${value}`)) + } + case "Integer": { + const primitiveHelp = InternalHelpDoc.p("Enter an integer") + const message = InternalHelpDoc.sequence(help, primitiveHelp) + return InternalNumberPrompt.integer({ + message: InternalHelpDoc.toAnsiText(message).trimEnd() + }).pipe(InternalPrompt.map((value) => `${value}`)) + } + case "Path": { + const primitiveHelp = InternalHelpDoc.p("Select a file system path") + const message = InternalHelpDoc.sequence(help, primitiveHelp) + return InternalFilePrompt.file({ + type: self.pathType, + message: InternalHelpDoc.toAnsiText(message).trimEnd() + }) + } + case "Redacted": { + const primitiveHelp = InternalHelpDoc.p("Enter some text (value will be redacted)") + const message = InternalHelpDoc.sequence(help, primitiveHelp) + return InternalTextPrompt.hidden({ + message: InternalHelpDoc.toAnsiText(message).trimEnd() + }) + } + case "Secret": { + const primitiveHelp = InternalHelpDoc.p("Enter some text (value will be redacted)") + const message = InternalHelpDoc.sequence(help, primitiveHelp) + return InternalTextPrompt.hidden({ + message: InternalHelpDoc.toAnsiText(message).trimEnd() + }) + } + case "Text": { + const primitiveHelp = InternalHelpDoc.p("Enter some text") + const message = InternalHelpDoc.sequence(help, primitiveHelp) + return InternalTextPrompt.text({ + message: InternalHelpDoc.toAnsiText(message).trimEnd() + }) + } + } +} + +// ============================================================================= +// Completion Internals +// ============================================================================= + +/** @internal */ +export const getBashCompletions = (self: Instruction): string => { + switch (self._tag) { + case "Bool": { + return "\"${cur}\"" + } + case "DateTime": + case "Float": + case "Integer": + case "Secret": + case "Redacted": + case "Text": { + return "$(compgen -f \"${cur}\")" + } + case "Path": { + switch (self.pathType) { + case "file": { + return self.pathExists === "yes" || self.pathExists === "either" + ? "$(compgen -f \"${cur}\")" + : "" + } + case "directory": { + return self.pathExists === "yes" || self.pathExists === "either" + ? "$(compgen -d \"${cur}\")" + : "" + } + case "either": { + return self.pathExists === "yes" || self.pathExists === "either" + ? "$(compgen -f \"${cur}\")" + : "" + } + } + } + case "Choice": { + const choices = pipe( + Arr.map(self.alternatives, ([choice]) => choice), + Arr.join(",") + ) + return `$(compgen -W "${choices}" -- "\${cur}")` + } + } +} + +/** @internal */ +export const getFishCompletions = (self: Instruction): Array => { + switch (self._tag) { + case "Bool": { + return Arr.empty() + } + case "DateTime": + case "Float": + case "Integer": + case "Redacted": + case "Secret": + case "Text": { + return Arr.make("-r", "-f") + } + case "Path": { + switch (self.pathType) { + case "file": { + return self.pathExists === "yes" || self.pathExists === "either" + ? Arr.make("-r", "-F") + : Arr.make("-r") + } + case "directory": { + return self.pathExists === "yes" || self.pathExists === "either" + ? Arr.make( + "-r", + "-f", + "-a", + `"(__fish_complete_directories (commandline -ct))"` + ) + : Arr.make("-r") + } + case "either": { + return self.pathExists === "yes" || self.pathExists === "either" + ? Arr.make("-r", "-F") + : Arr.make("-r") + } + } + } + case "Choice": { + const choices = pipe( + Arr.map(self.alternatives, ([choice]) => `${choice}''`), + Arr.join(",") + ) + return Arr.make("-r", "-f", "-a", `"{${choices}}"`) + } + } +} + +/** @internal */ +export const getZshCompletions = (self: Instruction): string => { + switch (self._tag) { + case "Bool": { + return "" + } + case "Choice": { + const choices = pipe( + Arr.map(self.alternatives, ([name]) => name), + Arr.join(" ") + ) + return `:CHOICE:(${choices})` + } + case "DateTime": { + return "" + } + case "Float": { + return "" + } + case "Integer": { + return "" + } + case "Path": { + switch (self.pathType) { + case "file": { + return self.pathExists === "yes" || self.pathExists === "either" + ? ":PATH:_files" + : "" + } + case "directory": { + return self.pathExists === "yes" || self.pathExists === "either" + ? ":PATH:_files -/" + : "" + } + case "either": { + return self.pathExists === "yes" || self.pathExists === "either" + ? ":PATH:_files" + : "" + } + } + } + case "Redacted": + case "Secret": + case "Text": { + return "" + } + } +} diff --git a/repos/effect/packages/cli/src/internal/prompt.ts b/repos/effect/packages/cli/src/internal/prompt.ts new file mode 100644 index 0000000..76b22c7 --- /dev/null +++ b/repos/effect/packages/cli/src/internal/prompt.ts @@ -0,0 +1,253 @@ +import * as Terminal from "@effect/platform/Terminal" +import * as Doc from "@effect/printer-ansi/AnsiDoc" +import type * as Cause from "effect/Cause" +import * as Effect from "effect/Effect" +import * as Effectable from "effect/Effectable" +import { dual } from "effect/Function" +import type * as Mailbox from "effect/Mailbox" +import * as Pipeable from "effect/Pipeable" +import type * as Prompt from "../Prompt.js" +import { Action } from "./prompt/action.js" + +/** @internal */ +const PromptSymbolKey = "@effect/cli/Prompt" + +/** @internal */ +export const PromptTypeId: Prompt.PromptTypeId = Symbol.for( + PromptSymbolKey +) as Prompt.PromptTypeId + +/** @internal */ +const proto = { + ...Effectable.CommitPrototype, + [PromptTypeId]: { + _Output: (_: never) => _ + }, + commit(): Effect.Effect { + return run(this as any) + }, + pipe() { + return Pipeable.pipeArguments(this, arguments) + } +} + +/** @internal */ +type Op = Prompt.Prompt & Body & { + readonly _tag: Tag +} + +/** @internal */ +export type Primitive = Loop | OnSuccess | Succeed + +/** @internal */ +export interface Loop extends + Op<"Loop", { + readonly initialState: unknown | Effect.Effect + readonly render: Prompt.Prompt.Handlers["render"] + readonly process: Prompt.Prompt.Handlers["process"] + readonly clear: Prompt.Prompt.Handlers["clear"] + }> +{} + +/** @internal */ +export interface OnSuccess extends + Op<"OnSuccess", { + readonly prompt: Primitive + readonly onSuccess: (value: unknown) => Prompt.Prompt + }> +{} + +/** @internal */ +export interface Succeed extends + Op<"Succeed", { + readonly value: unknown + }> +{} + +/** @internal */ +export const isPrompt = (u: unknown): u is Prompt.Prompt => + typeof u === "object" && u != null && PromptTypeId in u + +const allTupled = >>(arg: T): Prompt.Prompt< + { + [K in keyof T]: [T[K]] extends [Prompt.Prompt] ? A : never + } +> => { + if (arg.length === 0) { + return succeed([]) as any + } + if (arg.length === 1) { + return map(arg[0], (x) => [x]) as any + } + let result = map(arg[0], (x) => [x]) + for (let i = 1; i < arg.length; i++) { + const curr = arg[i] + result = flatMap(result, (tuple) => map(curr, (a) => [...tuple, a])) + } + return result as any +} + +/** @internal */ +export const all: < + const Arg extends Iterable> | Record> +>(arg: Arg) => Prompt.All.Return = function() { + if (arguments.length === 1) { + if (isPrompt(arguments[0])) { + return map(arguments[0], (x) => [x]) as any + } else if (Array.isArray(arguments[0])) { + return allTupled(arguments[0]) as any + } else { + const entries = Object.entries(arguments[0] as Readonly<{ [K: string]: Prompt.Prompt }>) + let result = map(entries[0][1], (value) => ({ [entries[0][0]]: value })) + if (entries.length === 1) { + return result as any + } + const rest = entries.slice(1) + for (const [key, prompt] of rest) { + result = result.pipe( + flatMap((record) => + prompt.pipe(map((value) => ({ + ...record, + [key]: value + }))) + ) + ) + } + return result as any + } + } + return allTupled(arguments[0]) as any +} + +/** @internal */ +export const custom = ( + initialState: State | Effect.Effect, + handlers: Prompt.Prompt.Handlers +): Prompt.Prompt => { + const op = Object.create(proto) + op._tag = "Loop" + op.initialState = initialState + op.render = handlers.render + op.process = handlers.process + op.clear = handlers.clear + return op +} + +/** @internal */ +export const map = dual< + ( + f: (output: Output) => Output2 + ) => ( + self: Prompt.Prompt + ) => Prompt.Prompt, + ( + self: Prompt.Prompt, + f: (output: Output) => Output2 + ) => Prompt.Prompt +>(2, (self, f) => flatMap(self, (a) => succeed(f(a)))) + +/** @internal */ +export const flatMap = dual< + ( + f: (output: Output) => Prompt.Prompt + ) => ( + self: Prompt.Prompt + ) => Prompt.Prompt, + ( + self: Prompt.Prompt, + f: (output: Output) => Prompt.Prompt + ) => Prompt.Prompt +>(2, (self, f) => { + const op = Object.create(proto) + op._tag = "OnSuccess" + op.prompt = self + op.onSuccess = f + return op +}) + +/** @internal */ +export const run: ( + self: Prompt.Prompt +) => Effect.Effect< + Output, + Terminal.QuitException, + Prompt.Prompt.Environment +> = Effect.fnUntraced( + function*(self: Prompt.Prompt) { + const terminal = yield* Terminal.Terminal + const input = yield* terminal.readInput + return yield* runWithInput(self, terminal, input) + }, + Effect.mapError(() => new Terminal.QuitException()), + Effect.scoped +) + +const runWithInput = ( + prompt: Prompt.Prompt, + terminal: Terminal.Terminal, + input: Mailbox.ReadonlyMailbox +): Effect.Effect => + Effect.suspend(() => { + const op = prompt as Primitive + switch (op._tag) { + case "Loop": { + return runLoop(op, terminal, input) + } + case "OnSuccess": { + return Effect.flatMap( + runWithInput(op.prompt, terminal, input), + (a) => runWithInput(op.onSuccess(a), terminal, input) + ) as any + } + case "Succeed": { + return Effect.succeed(op.value) + } + } + }) + +const runLoop = Effect.fnUntraced( + function*( + loop: Loop, + terminal: Terminal.Terminal, + input: Mailbox.ReadonlyMailbox + ) { + let state = Effect.isEffect(loop.initialState) ? yield* loop.initialState : loop.initialState + let action: Prompt.Prompt.Action = Action.NextFrame({ state }) + while (true) { + const msg = yield* loop.render(state, action) + yield* Effect.orDie(terminal.display(msg)) + const event = yield* input.take + action = yield* loop.process(event, state) + switch (action._tag) { + case "Beep": + continue + case "NextFrame": { + yield* Effect.orDie(terminal.display(yield* loop.clear(state, action))) + state = action.state + continue + } + case "Submit": { + yield* Effect.orDie(terminal.display(yield* loop.clear(state, action))) + const msg = yield* loop.render(state, action) + yield* Effect.orDie(terminal.display(msg)) + return action.value + } + } + } + }, + (effect, _, terminal) => + Effect.ensuring( + effect, + Effect.orDie( + terminal.display(Doc.render(Doc.cursorShow, { style: "pretty" })) + ) + ) +) + +/** @internal */ +export const succeed = (value: A): Prompt.Prompt => { + const op = Object.create(proto) + op._tag = "Succeed" + op.value = value + return op +} diff --git a/repos/effect/packages/cli/src/internal/prompt/action.ts b/repos/effect/packages/cli/src/internal/prompt/action.ts new file mode 100644 index 0000000..63ebb20 --- /dev/null +++ b/repos/effect/packages/cli/src/internal/prompt/action.ts @@ -0,0 +1,5 @@ +import * as Data from "effect/Data" +import type { Prompt } from "../../Prompt.js" + +/** @internal */ +export const Action = Data.taggedEnum() diff --git a/repos/effect/packages/cli/src/internal/prompt/ansi-utils.ts b/repos/effect/packages/cli/src/internal/prompt/ansi-utils.ts new file mode 100644 index 0000000..bb7534b --- /dev/null +++ b/repos/effect/packages/cli/src/internal/prompt/ansi-utils.ts @@ -0,0 +1,72 @@ +import * as Doc from "@effect/printer-ansi/AnsiDoc" +import * as Arr from "effect/Array" +import * as Effect from "effect/Effect" +import { pipe } from "effect/Function" + +const defaultFigures = { + arrowUp: Doc.text("↑"), + arrowDown: Doc.text("↓"), + arrowLeft: Doc.text("←"), + arrowRight: Doc.text("→"), + radioOn: Doc.text("◉"), + radioOff: Doc.text("◯"), + checkboxOn: Doc.text("☒"), + checkboxOff: Doc.text("☐"), + tick: Doc.text("✔"), + cross: Doc.text("✖"), + ellipsis: Doc.text("…"), + pointerSmall: Doc.text("›"), + line: Doc.text("─"), + pointer: Doc.text("❯") +} + +const windowsFigures = { + arrowUp: defaultFigures.arrowUp, + arrowDown: defaultFigures.arrowDown, + arrowLeft: defaultFigures.arrowLeft, + arrowRight: defaultFigures.arrowRight, + radioOn: Doc.text("(*)"), + radioOff: Doc.text("( )"), + checkboxOn: Doc.text("[*]"), + checkboxOff: Doc.text("[ ]"), + tick: Doc.text("√"), + cross: Doc.text("×"), + ellipsis: Doc.text("..."), + pointerSmall: Doc.text("»"), + line: Doc.text("─"), + pointer: Doc.text(">") +} + +/** @internal */ +export const figures = Effect.map( + Effect.sync(() => process.platform === "win32"), + (isWindows) => isWindows ? windowsFigures : defaultFigures +) + +/** + * Clears all lines taken up by the specified `text`. + * + * @internal + */ +export function eraseText(text: string, columns: number): Doc.AnsiDoc { + if (columns === 0) { + return Doc.cat(Doc.eraseLine, Doc.cursorTo(0)) + } + let rows = 0 + const lines = text.split(/\r?\n/) + for (const line of lines) { + rows += 1 + Math.floor(Math.max(line.length - 1, 0) / columns) + } + return Doc.eraseLines(rows) +} + +/** @internal */ +export function lines(prompt: string, columns: number): number { + const lines = prompt.split(/\r?\n/) + return columns === 0 + ? lines.length + : pipe( + Arr.map(lines, (line) => Math.ceil(line.length / columns)), + Arr.reduce(0, (left, right) => left + right) + ) +} diff --git a/repos/effect/packages/cli/src/internal/prompt/confirm.ts b/repos/effect/packages/cli/src/internal/prompt/confirm.ts new file mode 100644 index 0000000..f5807be --- /dev/null +++ b/repos/effect/packages/cli/src/internal/prompt/confirm.ts @@ -0,0 +1,150 @@ +import * as Terminal from "@effect/platform/Terminal" +import * as Ansi from "@effect/printer-ansi/Ansi" +import * as Doc from "@effect/printer-ansi/AnsiDoc" +import * as Optimize from "@effect/printer/Optimize" +import * as Arr from "effect/Array" +import * as Effect from "effect/Effect" +import * as Option from "effect/Option" +import type * as Prompt from "../../Prompt.js" +import * as InternalPrompt from "../prompt.js" +import { Action } from "./action.js" +import * as InternalAnsiUtils from "./ansi-utils.js" + +interface Options extends Required {} + +interface State { + readonly value: boolean +} + +const renderBeep = Doc.render(Doc.beep, { style: "pretty" }) + +function handleClear(options: Options) { + return Effect.gen(function*() { + const terminal = yield* Terminal.Terminal + const columns = yield* terminal.columns + const clearOutput = InternalAnsiUtils.eraseText(options.message, columns) + const resetCurrentLine = Doc.cat(Doc.eraseLine, Doc.cursorLeft) + return clearOutput.pipe( + Doc.cat(resetCurrentLine), + Optimize.optimize(Optimize.Deep), + Doc.render({ style: "pretty", options: { lineWidth: columns } }) + ) + }) +} + +const NEWLINE_REGEX = /\r?\n/ + +function renderOutput( + confirm: Doc.AnsiDoc, + leadingSymbol: Doc.AnsiDoc, + trailingSymbol: Doc.AnsiDoc, + options: Options +) { + const annotateLine = (line: string): Doc.AnsiDoc => Doc.annotate(Doc.text(line), Ansi.bold) + const prefix = Doc.cat(leadingSymbol, Doc.space) + return Arr.match(options.message.split(NEWLINE_REGEX), { + onEmpty: () => Doc.hsep([prefix, trailingSymbol, confirm]), + onNonEmpty: (promptLines) => { + const lines = Arr.map(promptLines, (line) => annotateLine(line)) + return prefix.pipe( + Doc.cat(Doc.nest(Doc.vsep(lines), 2)), + Doc.cat(Doc.space), + Doc.cat(trailingSymbol), + Doc.cat(Doc.space), + Doc.cat(confirm) + ) + } + }) +} + +function renderNextFrame(state: State, options: Options) { + return Effect.gen(function*() { + const terminal = yield* Terminal.Terminal + const columns = yield* terminal.columns + const figures = yield* InternalAnsiUtils.figures + const leadingSymbol = Doc.annotate(Doc.text("?"), Ansi.cyanBright) + const trailingSymbol = Doc.annotate(figures.pointerSmall, Ansi.blackBright) + // Marking these explicitly as present with `!` because they always will be + // and there is really no value in adding a `DeepRequired` type helper just + // for these internal cases + const confirmMessage = state.value + ? options.placeholder.defaultConfirm! + : options.placeholder.defaultDeny! + const confirm = Doc.annotate(Doc.text(confirmMessage), Ansi.blackBright) + const promptMsg = renderOutput(confirm, leadingSymbol, trailingSymbol, options) + return Doc.cursorHide.pipe( + Doc.cat(promptMsg), + Optimize.optimize(Optimize.Deep), + Doc.render({ style: "pretty", options: { lineWidth: columns } }) + ) + }) +} + +function renderSubmission(value: boolean, options: Options) { + return Effect.gen(function*() { + const terminal = yield* Terminal.Terminal + const columns = yield* terminal.columns + const figures = yield* InternalAnsiUtils.figures + const leadingSymbol = Doc.annotate(figures.tick, Ansi.green) + const trailingSymbol = Doc.annotate(figures.ellipsis, Ansi.blackBright) + const confirmMessage = value ? options.label.confirm : options.label.deny + const confirm = Doc.text(confirmMessage) + const promptMsg = renderOutput(confirm, leadingSymbol, trailingSymbol, options) + return promptMsg.pipe( + Doc.cat(Doc.hardLine), + Optimize.optimize(Optimize.Deep), + Doc.render({ style: "pretty", options: { lineWidth: columns } }) + ) + }) +} + +function handleRender(options: Options) { + return (_: State, action: Prompt.Prompt.Action) => { + return Action.$match(action, { + Beep: () => Effect.succeed(renderBeep), + NextFrame: ({ state }) => renderNextFrame(state, options), + Submit: ({ value }) => renderSubmission(value, options) + }) + } +} + +const TRUE_VALUE_REGEX = /^y|t$/ +const FALSE_VALUE_REGEX = /^n|f$/ + +function handleProcess(input: Terminal.UserInput, defaultValue: boolean) { + const value = Option.getOrElse(input.input, () => "") + if (input.key.name === "enter" || input.key.name === "return") { + return Effect.succeed(Action.Submit({ value: defaultValue })) + } + if (TRUE_VALUE_REGEX.test(value.toLowerCase())) { + return Effect.succeed(Action.Submit({ value: true })) + } + if (FALSE_VALUE_REGEX.test(value.toLowerCase())) { + return Effect.succeed(Action.Submit({ value: false })) + } + return Effect.succeed(Action.Beep()) +} + +/** @internal */ +export const confirm = (options: Prompt.Prompt.ConfirmOptions): Prompt.Prompt => { + const opts: Required = { + initial: false, + ...options, + label: { + confirm: "yes", + deny: "no", + ...options.label + }, + placeholder: { + defaultConfirm: "(Y/n)", + defaultDeny: "(y/N)", + ...options.placeholder + } + } + const initialState: State = { value: opts.initial } + return InternalPrompt.custom(initialState, { + render: handleRender(opts), + process: (input) => handleProcess(input, opts.initial), + clear: () => handleClear(opts) + }) +} diff --git a/repos/effect/packages/cli/src/internal/prompt/date.ts b/repos/effect/packages/cli/src/internal/prompt/date.ts new file mode 100644 index 0000000..8aca15a --- /dev/null +++ b/repos/effect/packages/cli/src/internal/prompt/date.ts @@ -0,0 +1,629 @@ +import * as Terminal from "@effect/platform/Terminal" +import * as Ansi from "@effect/printer-ansi/Ansi" +import * as Doc from "@effect/printer-ansi/AnsiDoc" +import * as Optimize from "@effect/printer/Optimize" +import * as Arr from "effect/Array" +import * as Effect from "effect/Effect" +import * as Match from "effect/Match" +import * as Option from "effect/Option" +import type * as Prompt from "../../Prompt.js" +import * as InternalPrompt from "../prompt.js" +import { Action } from "./action.js" +import * as InternalAnsiUtils from "./ansi-utils.js" + +interface DateOptions extends Required {} + +interface State { + readonly typed: string + readonly cursor: number + readonly value: globalThis.Date + readonly dateParts: ReadonlyArray + readonly error: Option.Option +} + +const renderBeep = Doc.render(Doc.beep, { style: "pretty" }) + +function handleClear(options: DateOptions) { + return (state: State, _: Prompt.Prompt.Action) => { + return Effect.gen(function*() { + const terminal = yield* Terminal.Terminal + const columns = yield* terminal.columns + const resetCurrentLine = Doc.cat(Doc.eraseLine, Doc.cursorLeft) + const clearError = Option.match(state.error, { + onNone: () => Doc.empty, + onSome: (error) => + Doc.cursorDown(InternalAnsiUtils.lines(error, columns)).pipe( + Doc.cat(InternalAnsiUtils.eraseText(`\n${error}`, columns)) + ) + }) + const clearOutput = InternalAnsiUtils.eraseText(options.message, columns) + return clearError.pipe( + Doc.cat(clearOutput), + Doc.cat(resetCurrentLine), + Optimize.optimize(Optimize.Deep), + Doc.render({ style: "pretty", options: { lineWidth: columns } }) + ) + }) + } +} + +const NEWLINE_REGEX = /\r?\n/ + +function renderError(state: State, pointer: Doc.AnsiDoc) { + return Option.match(state.error, { + onNone: () => Doc.empty, + onSome: (error) => { + const errorLines = error.split(NEWLINE_REGEX) + if (Arr.isNonEmptyReadonlyArray(errorLines)) { + const annotateLine = (line: string): Doc.AnsiDoc => + Doc.annotate(Doc.text(line), Ansi.combine(Ansi.italicized, Ansi.red)) + const prefix = Doc.cat(Doc.annotate(pointer, Ansi.red), Doc.space) + const lines = Arr.map(errorLines, (str) => annotateLine(str)) + return Doc.cursorSavePosition.pipe( + Doc.cat(Doc.hardLine), + Doc.cat(prefix), + Doc.cat(Doc.align(Doc.vsep(lines))), + Doc.cat(Doc.cursorRestorePosition) + ) + } + return Doc.empty + } + }) +} + +function renderParts(state: State, submitted: boolean = false) { + return Arr.reduce( + state.dateParts, + Doc.empty as Doc.AnsiDoc, + (doc, part, currentIndex) => { + const partDoc = Doc.text(part.toString()) + if (currentIndex === state.cursor && !submitted) { + const annotation = Ansi.combine(Ansi.underlined, Ansi.cyanBright) + return Doc.cat(doc, Doc.annotate(partDoc, annotation)) + } + return Doc.cat(doc, partDoc) + } + ) +} + +function renderOutput( + leadingSymbol: Doc.AnsiDoc, + trailingSymbol: Doc.AnsiDoc, + parts: Doc.AnsiDoc, + options: DateOptions +) { + const annotateLine = (line: string): Doc.AnsiDoc => Doc.annotate(Doc.text(line), Ansi.bold) + const prefix = Doc.cat(leadingSymbol, Doc.space) + return Arr.match(options.message.split(/\r?\n/), { + onEmpty: () => Doc.hsep([prefix, trailingSymbol, parts]), + onNonEmpty: (promptLines) => { + const lines = Arr.map(promptLines, (line) => annotateLine(line)) + return prefix.pipe( + Doc.cat(Doc.nest(Doc.vsep(lines), 2)), + Doc.cat(Doc.space), + Doc.cat(trailingSymbol), + Doc.cat(Doc.space), + Doc.cat(parts) + ) + } + }) +} + +function renderNextFrame(state: State, options: DateOptions) { + return Effect.gen(function*() { + const terminal = yield* Terminal.Terminal + const columns = yield* terminal.columns + const figures = yield* InternalAnsiUtils.figures + const leadingSymbol = Doc.annotate(Doc.text("?"), Ansi.cyanBright) + const trailingSymbol = Doc.annotate(figures.pointerSmall, Ansi.blackBright) + const parts = renderParts(state) + const promptMsg = renderOutput(leadingSymbol, trailingSymbol, parts, options) + const errorMsg = renderError(state, figures.pointerSmall) + return Doc.cursorHide.pipe( + Doc.cat(promptMsg), + Doc.cat(errorMsg), + Optimize.optimize(Optimize.Deep), + Doc.render({ style: "pretty", options: { lineWidth: columns } }) + ) + }) +} + +function renderSubmission(state: State, options: DateOptions) { + return Effect.gen(function*() { + const terminal = yield* Terminal.Terminal + const columns = yield* terminal.columns + const figures = yield* InternalAnsiUtils.figures + const leadingSymbol = Doc.annotate(figures.tick, Ansi.green) + const trailingSymbol = Doc.annotate(figures.ellipsis, Ansi.blackBright) + const parts = renderParts(state, true) + const promptMsg = renderOutput(leadingSymbol, trailingSymbol, parts, options) + return promptMsg.pipe( + Doc.cat(Doc.hardLine), + Optimize.optimize(Optimize.Deep), + Doc.render({ style: "pretty", options: { lineWidth: columns } }) + ) + }) +} + +function processUp(state: State) { + state.dateParts[state.cursor].increment() + return Action.NextFrame({ + state: { ...state, typed: "" } + }) +} + +function processDown(state: State) { + state.dateParts[state.cursor].decrement() + return Action.NextFrame({ + state: { ...state, typed: "" } + }) +} + +function processCursorLeft(state: State) { + const previousPart = state.dateParts[state.cursor].previousPart() + return Option.match(previousPart, { + onNone: () => Action.Beep(), + onSome: (previous) => + Action.NextFrame({ + state: { + ...state, + typed: "", + cursor: state.dateParts.indexOf(previous) + } + }) + }) +} + +function processCursorRight(state: State) { + const nextPart = state.dateParts[state.cursor].nextPart() + return Option.match(nextPart, { + onNone: () => Action.Beep(), + onSome: (next) => + Action.NextFrame({ + state: { + ...state, + typed: "", + cursor: state.dateParts.indexOf(next) + } + }) + }) +} + +function processNext(state: State) { + const nextPart = state.dateParts[state.cursor].nextPart() + const cursor = Option.match(nextPart, { + onNone: () => state.dateParts.findIndex((part) => !part.isToken()), + onSome: (next) => state.dateParts.indexOf(next) + }) + return Action.NextFrame({ + state: { ...state, cursor } + }) +} + +function defaultProcessor(value: string, state: State) { + if (/\d/.test(value)) { + const typed = state.typed + value + state.dateParts[state.cursor].setValue(typed) + return Action.NextFrame({ + state: { ...state, typed } + }) + } + return Action.Beep() +} + +const defaultLocales: Prompt.Prompt.DateOptions["locales"] = { + months: [ + "January", + "February", + "March", + "April", + "May", + "June", + "July", + "August", + "September", + "October", + "November", + "December" + ], + monthsShort: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"], + weekdays: ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"], + weekdaysShort: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"] +} + +function handleRender(options: DateOptions) { + return (state: State, action: Prompt.Prompt.Action) => { + return Action.$match(action, { + Beep: () => Effect.succeed(renderBeep), + NextFrame: ({ state }) => renderNextFrame(state, options), + Submit: () => renderSubmission(state, options) + }) + } +} + +function handleProcess(options: DateOptions) { + return (input: Terminal.UserInput, state: State) => { + switch (input.key.name) { + case "left": { + return Effect.succeed(processCursorLeft(state)) + } + case "right": { + return Effect.succeed(processCursorRight(state)) + } + case "k": + case "up": { + return Effect.succeed(processUp(state)) + } + case "j": + case "down": { + return Effect.succeed(processDown(state)) + } + case "tab": { + return Effect.succeed(processNext(state)) + } + case "enter": + case "return": { + return Effect.match(options.validate(state.value), { + onFailure: (error) => + Action.NextFrame({ + state: { + ...state, + error: Option.some(error) + } + }), + onSuccess: (value) => Action.Submit({ value }) + }) + } + default: { + const value = Option.getOrElse(input.input, () => "") + return Effect.succeed(defaultProcessor(value, state)) + } + } + } +} + +/** @internal */ +export const date = (options: Prompt.Prompt.DateOptions): Prompt.Prompt => { + const opts: Required = { + initial: new Date(), + dateMask: "YYYY-MM-DD HH:mm:ss", + validate: Effect.succeed, + ...options, + locales: { + ...defaultLocales, + ...options.locales + } + } + const dateParts = makeDateParts(opts.dateMask, opts.initial, opts.locales) + const initialCursorPosition = dateParts.findIndex((part) => !part.isToken()) + const initialState: State = { + dateParts, + typed: "", + cursor: initialCursorPosition, + value: opts.initial, + error: Option.none() + } + return InternalPrompt.custom(initialState, { + render: handleRender(opts), + process: handleProcess(opts), + clear: handleClear(opts) + }) +} + +const DATE_PART_REGEX = + /\\(.)|"((?:\\["\\]|[^"])+)"|(D[Do]?|d{3,4}|d)|(M{1,4})|(YY(?:YY)?)|([aA])|([Hh]{1,2})|(m{1,2})|(s{1,2})|(S{1,4})|./g + +const regexGroups: Record DatePart> = { + 1: ({ token, ...opts }) => new Token({ token: token.replace(/\\(.)/g, "$1"), ...opts }), + 2: (opts) => new Day(opts), + 3: (opts) => new Month(opts), + 4: (opts) => new Year(opts), + 5: (opts) => new Meridiem(opts), + 6: (opts) => new Hours(opts), + 7: (opts) => new Minutes(opts), + 8: (opts) => new Seconds(opts), + 9: (opts) => new Milliseconds(opts) +} + +const makeDateParts = ( + dateMask: string, + date: globalThis.Date, + locales: Prompt.Prompt.DateOptions["locales"] +) => { + const parts: Array = [] + let result: RegExpExecArray | null = null + // eslint-disable-next-line no-cond-assign + while (result = DATE_PART_REGEX.exec(dateMask)) { + const match = result.shift() + const index = result.findIndex((group) => group !== undefined) + if (index in regexGroups) { + const token = (result[index] || match)! + parts.push(regexGroups[index]({ token, date, parts, locales })) + } else { + parts.push(new Token({ token: (result[index] || match)!, date, parts, locales })) + } + } + const orderedParts = parts.reduce((array, element) => { + const lastElement = array[array.length - 1] + if (element.isToken() && lastElement !== undefined && lastElement.isToken()) { + lastElement.setValue(element.token) + } else { + array.push(element) + } + return array + }, Arr.empty()) + parts.splice(0, parts.length, ...orderedParts) + return parts +} + +interface DatePartParams { + readonly token: string + readonly locales: Prompt.Prompt.DateOptions["locales"] + readonly date?: globalThis.Date + readonly parts?: ReadonlyArray +} + +abstract class DatePart { + token: string + readonly date: globalThis.Date + readonly parts: ReadonlyArray + readonly locales: Prompt.Prompt.DateOptions["locales"] + + constructor(params: DatePartParams) { + this.token = params.token + this.locales = params.locales + this.date = params.date || new Date() + this.parts = params.parts || [this] + } + + /** + * Increments this date part. + */ + abstract increment(): void + + /** + * Decrements this date part. + */ + abstract decrement(): void + + /** + * Sets the current value of this date part to the provided value. + */ + abstract setValue(value: string): void + + /** + * Returns `true` if this `DatePart` is a `Token`, `false` otherwise. + */ + isToken(): this is Token { + return false + } + + /** + * Retrieves the next date part in the list of parts. + */ + nextPart(): Option.Option { + return Arr.findFirstIndex(this.parts, (part) => part === this).pipe( + Option.flatMap((currentPartIndex) => + Arr.findFirst(this.parts.slice(currentPartIndex + 1), (part) => !part.isToken()) + ) + ) + } + + /** + * Retrieves the previous date part in the list of parts. + */ + previousPart(): Option.Option { + return Arr.findFirstIndex(this.parts, (part) => part === this).pipe( + Option.flatMap((currentPartIndex) => + Arr.findLast(this.parts.slice(0, currentPartIndex), (part) => !part.isToken()) + ) + ) + } + + toString() { + return String(this.date) + } +} + +class Token extends DatePart { + increment(): void {} + + decrement(): void {} + + setValue(value: string): void { + this.token = this.token + value + } + + isToken(): this is Token { + return true + } + + toString() { + return this.token + } +} + +class Milliseconds extends DatePart { + increment(): void { + this.date.setMilliseconds(this.date.getMilliseconds() + 1) + } + + decrement(): void { + this.date.setMilliseconds(this.date.getMilliseconds() - 1) + } + + setValue(value: string): void { + this.date.setMilliseconds(Number.parseInt(value.slice(-this.token.length))) + } + + toString() { + const millis = `${this.date.getMilliseconds()}` + return millis.padStart(4, "0").substring(0, this.token.length) + } +} + +class Seconds extends DatePart { + increment(): void { + this.date.setSeconds(this.date.getSeconds() + 1) + } + + decrement(): void { + this.date.setSeconds(this.date.getSeconds() - 1) + } + + setValue(value: string): void { + this.date.setSeconds(Number.parseInt(value.slice(-2))) + } + + toString() { + const seconds = `${this.date.getSeconds()}` + return this.token.length > 1 + ? seconds.padStart(2, "0") + : seconds + } +} + +class Minutes extends DatePart { + increment(): void { + this.date.setMinutes(this.date.getMinutes() + 1) + } + + decrement(): void { + this.date.setMinutes(this.date.getMinutes() - 1) + } + + setValue(value: string): void { + this.date.setMinutes(Number.parseInt(value.slice(-2))) + } + + toString() { + const minutes = `${this.date.getMinutes()}` + return this.token.length > 1 + ? minutes.padStart(2, "0") : + minutes + } +} + +class Hours extends DatePart { + increment(): void { + this.date.setHours(this.date.getHours() + 1) + } + + decrement(): void { + this.date.setHours(this.date.getHours() - 1) + } + + setValue(value: string): void { + this.date.setHours(Number.parseInt(value.slice(-2))) + } + + toString() { + const hours = /h/.test(this.token) + ? this.date.getHours() % 12 || 12 + : this.date.getHours() + return this.token.length > 1 + ? `${hours}`.padStart(2, "0") + : `${hours}` + } +} + +class Day extends DatePart { + increment(): void { + this.date.setDate(this.date.getDate() + 1) + } + + decrement(): void { + this.date.setDate(this.date.getDate() - 1) + } + + setValue(value: string): void { + this.date.setDate(Number.parseInt(value.slice(-2))) + } + + toString() { + const date = this.date.getDate() + const day = this.date.getDay() + return Match.value(this.token).pipe( + Match.when("DD", () => `${date}`.padStart(2, "0")), + Match.when("Do", () => `${date}${this.ordinalIndicator(date)}`), + Match.when("d", () => `${day + 1}`), + Match.when("ddd", () => this.locales!.weekdaysShort[day]!), + Match.when("dddd", () => this.locales!.weekdays[day]!), + Match.orElse(() => `${date}`) + ) + } + + private ordinalIndicator(day: number): string { + return Match.value(day % 10).pipe( + Match.when(1, () => "st"), + Match.when(2, () => "nd"), + Match.when(3, () => "rd"), + Match.orElse(() => "th") + ) + } +} + +class Month extends DatePart { + increment(): void { + this.date.setMonth(this.date.getMonth() + 1) + } + + decrement(): void { + this.date.setMonth(this.date.getMonth() - 1) + } + + setValue(value: string): void { + const month = Number.parseInt(value.slice(-2)) - 1 + this.date.setMonth(month < 0 ? 0 : month) + } + + toString() { + const month = this.date.getMonth() + return Match.value(this.token.length).pipe( + Match.when(2, () => `${month + 1}`.padStart(2, "0")), + Match.when(3, () => this.locales!.monthsShort[month]!), + Match.when(4, () => this.locales!.months[month]!), + Match.orElse(() => `${month + 1}`) + ) + } +} + +class Year extends DatePart { + increment(): void { + this.date.setFullYear(this.date.getFullYear() + 1) + } + + decrement(): void { + this.date.setFullYear(this.date.getFullYear() - 1) + } + + setValue(value: string): void { + this.date.setFullYear(Number.parseInt(value.slice(-4))) + } + + toString() { + const year = `${this.date.getFullYear()}`.padStart(4, "0") + return this.token.length === 2 + ? year.substring(-2) + : year + } +} + +class Meridiem extends DatePart { + increment(): void { + this.date.setHours((this.date.getHours() + 12) % 24) + } + + decrement(): void { + this.increment() + } + + setValue(_value: string): void {} + + toString() { + const meridiem = this.date.getHours() > 12 ? "pm" : "am" + return /A/.test(this.token) + ? meridiem.toUpperCase() + : meridiem + } +} diff --git a/repos/effect/packages/cli/src/internal/prompt/file.ts b/repos/effect/packages/cli/src/internal/prompt/file.ts new file mode 100644 index 0000000..a47f23d --- /dev/null +++ b/repos/effect/packages/cli/src/internal/prompt/file.ts @@ -0,0 +1,375 @@ +import * as FileSystem from "@effect/platform/FileSystem" +import * as Path from "@effect/platform/Path" +import * as Terminal from "@effect/platform/Terminal" +import * as Ansi from "@effect/printer-ansi/Ansi" +import * as Doc from "@effect/printer-ansi/AnsiDoc" +import * as Optimize from "@effect/printer/Optimize" +import * as Arr from "effect/Array" +import * as Data from "effect/Data" +import * as Effect from "effect/Effect" +import { identity } from "effect/Function" +import * as Option from "effect/Option" +import type * as Prompt from "../../Prompt.js" +import * as InternalPrompt from "../prompt.js" +import { Action } from "./action.js" +import * as InternalAnsiUtils from "./ansi-utils.js" +import { entriesToDisplay } from "./utils.js" + +interface FileOptions extends Required> { + readonly startingPath: Option.Option +} + +interface State { + readonly cursor: number + readonly files: ReadonlyArray + readonly path: Option.Option + readonly confirm: Confirm +} + +const CONFIRM_MESSAGE = "The selected directory contains files. Would you like to traverse the selected directory?" +type Confirm = Data.TaggedEnum<{ + readonly Show: {} + readonly Hide: {} +}> +const Confirm = Data.taggedEnum() + +const showConfirmation = Confirm.$is("Show") + +const renderBeep = Doc.render(Doc.beep, { style: "pretty" }) + +function resolveCurrentPath( + path: Option.Option, + options: FileOptions +): Effect.Effect { + return Option.match(path, { + onNone: () => + Option.match(options.startingPath, { + onNone: () => Effect.sync(() => process.cwd()), + onSome: (path) => + Effect.flatMap(FileSystem.FileSystem, (fs) => + // Ensure the user provided starting path exists + Effect.orDie(fs.exists(path)).pipe( + Effect.filterOrDieMessage( + identity, + `The provided starting path '${path}' does not exist` + ), + Effect.as(path) + )) + }), + onSome: (path) => Effect.succeed(path) + }) +} + +function getFileList(directory: string, options: FileOptions) { + return Effect.gen(function*() { + const fs = yield* FileSystem.FileSystem + const path = yield* Path.Path + const files = yield* Effect.orDie(fs.readDirectory(directory)).pipe( + // Always prepend the `".."` option to the file list but allow it + // to be filtered out if the user so desires + Effect.map((files) => ["..", ...files]) + ) + return yield* Effect.filter(files, (file) => { + const result = options.filter(file) + const userDefinedFilter = Effect.isEffect(result) + ? result + : Effect.succeed(result) + const directoryFilter = options.type === "directory" + ? Effect.map( + Effect.orDie(fs.stat(path.join(directory, file))), + (info) => info.type === "Directory" + ) + : Effect.succeed(true) + return Effect.zipWith(userDefinedFilter, directoryFilter, (a, b) => a && b) + }, { concurrency: files.length }) + }) +} + +function handleClear(options: FileOptions) { + return (state: State, _: Prompt.Prompt.Action) => { + return Effect.gen(function*() { + const terminal = yield* Terminal.Terminal + const columns = yield* terminal.columns + const currentPath = yield* resolveCurrentPath(state.path, options) + const text = "\n".repeat(Math.min(state.files.length, options.maxPerPage)) + const clearPath = InternalAnsiUtils.eraseText(currentPath, columns) + const message = showConfirmation(state.confirm) ? CONFIRM_MESSAGE : options.message + const clearPrompt = InternalAnsiUtils.eraseText(`\n${message}`, columns) + const clearOptions = InternalAnsiUtils.eraseText(text, columns) + return clearOptions.pipe( + Doc.cat(clearPath), + Doc.cat(clearPrompt), + Optimize.optimize(Optimize.Deep), + Doc.render({ style: "pretty", options: { lineWidth: columns } }) + ) + }) + } +} + +const NEWLINE_REGEX = /\r?\n/ + +function renderPrompt( + confirm: Doc.AnsiDoc, + message: string, + leadingSymbol: Doc.AnsiDoc, + trailingSymbol: Doc.AnsiDoc +) { + const annotateLine = (line: string): Doc.AnsiDoc => Doc.annotate(Doc.text(line), Ansi.bold) + const prefix = Doc.cat(leadingSymbol, Doc.space) + return Arr.match(message.split(NEWLINE_REGEX), { + onEmpty: () => Doc.hsep([prefix, trailingSymbol, confirm]), + onNonEmpty: (promptLines) => { + const lines = Arr.map(promptLines, (line) => annotateLine(line)) + return prefix.pipe( + Doc.cat(Doc.nest(Doc.vsep(lines), 2)), + Doc.cat(Doc.space), + Doc.cat(trailingSymbol), + Doc.cat(Doc.space), + Doc.cat(confirm) + ) + } + }) +} + +function renderPrefix( + state: State, + toDisplay: { readonly startIndex: number; readonly endIndex: number }, + currentIndex: number, + length: number, + figures: Effect.Effect.Success +) { + let prefix: Doc.AnsiDoc = Doc.space + if (currentIndex === toDisplay.startIndex && toDisplay.startIndex > 0) { + prefix = figures.arrowUp + } else if (currentIndex === toDisplay.endIndex - 1 && toDisplay.endIndex < length) { + prefix = figures.arrowDown + } + return state.cursor === currentIndex + ? figures.pointer.pipe(Doc.annotate(Ansi.cyanBright), Doc.cat(prefix)) + : prefix.pipe(Doc.cat(Doc.space)) +} + +function renderFileName(file: string, isSelected: boolean) { + return isSelected + ? Doc.annotate(Doc.text(file), Ansi.combine(Ansi.underlined, Ansi.cyanBright)) + : Doc.text(file) +} + +function renderFiles( + state: State, + files: ReadonlyArray, + figures: Effect.Effect.Success, + options: FileOptions +) { + const length = files.length + const toDisplay = entriesToDisplay(state.cursor, length, options.maxPerPage) + const documents: Array = [] + for (let index = toDisplay.startIndex; index < toDisplay.endIndex; index++) { + const isSelected = state.cursor === index + const prefix = renderPrefix(state, toDisplay, index, length, figures) + const fileName = renderFileName(files[index], isSelected) + documents.push(Doc.cat(prefix, fileName)) + } + return Doc.vsep(documents) +} + +function renderNextFrame(state: State, options: FileOptions) { + return Effect.gen(function*() { + const path = yield* Path.Path + const terminal = yield* Terminal.Terminal + const columns = yield* terminal.columns + const figures = yield* InternalAnsiUtils.figures + const currentPath = yield* resolveCurrentPath(state.path, options) + const selectedPath = state.files[state.cursor] + const resolvedPath = path.resolve(currentPath, selectedPath) + const resolvedPathMsg = figures.pointerSmall.pipe( + Doc.cat(Doc.space), + Doc.cat(Doc.text(resolvedPath)), + Doc.annotate(Ansi.blackBright) + ) + if (showConfirmation(state.confirm)) { + const leadingSymbol = Doc.annotate(Doc.text("?"), Ansi.cyanBright) + const trailingSymbol = Doc.annotate(figures.pointerSmall, Ansi.blackBright) + const confirm = Doc.annotate(Doc.text("(Y/n)"), Ansi.blackBright) + const promptMsg = renderPrompt(confirm, CONFIRM_MESSAGE, leadingSymbol, trailingSymbol) + return Doc.cursorHide.pipe( + Doc.cat(promptMsg), + Doc.cat(Doc.hardLine), + Doc.cat(resolvedPathMsg), + Optimize.optimize(Optimize.Deep), + Doc.render({ style: "pretty", options: { lineWidth: columns } }) + ) + } + const leadingSymbol = Doc.annotate(figures.tick, Ansi.green) + const trailingSymbol = Doc.annotate(figures.ellipsis, Ansi.blackBright) + const promptMsg = renderPrompt(Doc.empty, options.message, leadingSymbol, trailingSymbol) + const files = renderFiles(state, state.files, figures, options) + return Doc.cursorHide.pipe( + Doc.cat(promptMsg), + Doc.cat(Doc.hardLine), + Doc.cat(resolvedPathMsg), + Doc.cat(Doc.hardLine), + Doc.cat(files), + Optimize.optimize(Optimize.Deep), + Doc.render({ style: "pretty", options: { lineWidth: columns } }) + ) + }) +} + +function renderSubmission(value: string, options: FileOptions) { + return Effect.gen(function*() { + const terminal = yield* Terminal.Terminal + const columns = yield* terminal.columns + const figures = yield* InternalAnsiUtils.figures + const leadingSymbol = Doc.annotate(figures.tick, Ansi.green) + const trailingSymbol = Doc.annotate(figures.ellipsis, Ansi.blackBright) + const promptMsg = renderPrompt(Doc.empty, options.message, leadingSymbol, trailingSymbol) + return promptMsg.pipe( + Doc.cat(Doc.space), + Doc.cat(Doc.annotate(Doc.text(value), Ansi.white)), + Doc.cat(Doc.hardLine), + Doc.render({ style: "pretty", options: { lineWidth: columns } }) + ) + }) +} + +function handleRender(options: FileOptions) { + return (_: State, action: Prompt.Prompt.Action) => { + return Action.$match(action, { + Beep: () => Effect.succeed(renderBeep), + NextFrame: ({ state }) => renderNextFrame(state, options), + Submit: ({ value }) => renderSubmission(value, options) + }) + } +} + +function processCursorUp(state: State) { + const cursor = state.cursor - 1 + return Effect.succeed(Action.NextFrame({ + state: { ...state, cursor: cursor < 0 ? state.files.length - 1 : cursor } + })) +} + +function processCursorDown(state: State) { + return Effect.succeed(Action.NextFrame({ + state: { ...state, cursor: (state.cursor + 1) % state.files.length } + })) +} + +function processSelection(state: State, options: FileOptions) { + return Effect.gen(function*() { + const fs = yield* FileSystem.FileSystem + const path = yield* Path.Path + const currentPath = yield* resolveCurrentPath(state.path, options) + const selectedPath = state.files[state.cursor] + const resolvedPath = path.resolve(currentPath, selectedPath) + const info = yield* Effect.orDie(fs.stat(resolvedPath)) + if (info.type === "Directory") { + const files = yield* getFileList(resolvedPath, options) + const filesWithoutParent = files.filter((file) => file !== "..") + // If the user selected a directory AND the prompt type can result with + // a directory, we must confirm: + // - If the selected directory has any files + // - Confirm whether or not the user wants to traverse those files + if (options.type === "directory" || options.type === "either") { + return filesWithoutParent.length === 0 + // Directory is empty so it's safe to select it + ? Action.Submit({ value: resolvedPath }) + // Directory has contents - show confirmation to user + : Action.NextFrame({ + state: { ...state, confirm: Confirm.Show() } + }) + } + return Action.NextFrame({ + state: { + cursor: 0, + files, + path: Option.some(resolvedPath), + confirm: Confirm.Hide() + } + }) + } + return Action.Submit({ value: resolvedPath }) + }) +} + +function handleProcess(options: FileOptions) { + return (input: Terminal.UserInput, state: State) => + Effect.gen(function*() { + switch (input.key.name) { + case "k": + case "up": { + return yield* processCursorUp(state) + } + case "j": + case "down": + case "tab": { + return yield* processCursorDown(state) + } + case "enter": + case "return": { + return yield* processSelection(state, options) + } + case "y": + case "t": { + if (showConfirmation(state.confirm)) { + const path = yield* Path.Path + const currentPath = yield* resolveCurrentPath(state.path, options) + const selectedPath = state.files[state.cursor] + const resolvedPath = path.resolve(currentPath, selectedPath) + const files = yield* getFileList(resolvedPath, options) + return Action.NextFrame({ + state: { + cursor: 0, + files, + path: Option.some(resolvedPath), + confirm: Confirm.Hide() + } + }) + } + return Action.Beep() + } + case "n": + case "f": { + if (showConfirmation(state.confirm)) { + const path = yield* Path.Path + const currentPath = yield* resolveCurrentPath(state.path, options) + const selectedPath = state.files[state.cursor] + const resolvedPath = path.resolve(currentPath, selectedPath) + return Action.Submit({ value: resolvedPath }) + } + return Action.Beep() + } + default: { + return Action.Beep() + } + } + }) +} + +/** @internal */ +export const file = (options: Prompt.Prompt.FileOptions = {}): Prompt.Prompt => { + const opts: FileOptions = { + type: options.type ?? "file", + message: options.message ?? `Choose a file`, + startingPath: Option.fromNullable(options.startingPath), + maxPerPage: options.maxPerPage ?? 10, + filter: options.filter ?? (() => Effect.succeed(true)) + } + const initialState: Effect.Effect< + State, + never, + Prompt.Prompt.Environment + > = Effect.gen(function*() { + const path = Option.none() + const currentPath = yield* resolveCurrentPath(path, opts) + const files = yield* getFileList(currentPath, opts) + const confirm = Confirm.Hide() + return { cursor: 0, files, path, confirm } + }) + return InternalPrompt.custom(initialState, { + render: handleRender(opts), + process: handleProcess(opts), + clear: handleClear(opts) + }) +} diff --git a/repos/effect/packages/cli/src/internal/prompt/list.ts b/repos/effect/packages/cli/src/internal/prompt/list.ts new file mode 100644 index 0000000..9734e94 --- /dev/null +++ b/repos/effect/packages/cli/src/internal/prompt/list.ts @@ -0,0 +1,9 @@ +import type * as Prompt from "../../Prompt.js" +import * as InternalPrompt from "../prompt.js" +import * as InternalTextPrompt from "./text.js" + +/** @internal */ +export const list = (options: Prompt.Prompt.ListOptions): Prompt.Prompt> => + InternalTextPrompt.text(options).pipe( + InternalPrompt.map((output) => output.split(options.delimiter || ",")) + ) diff --git a/repos/effect/packages/cli/src/internal/prompt/multi-select.ts b/repos/effect/packages/cli/src/internal/prompt/multi-select.ts new file mode 100644 index 0000000..7f6b8a2 --- /dev/null +++ b/repos/effect/packages/cli/src/internal/prompt/multi-select.ts @@ -0,0 +1,326 @@ +import * as Terminal from "@effect/platform/Terminal" +import * as Ansi from "@effect/printer-ansi/Ansi" +import * as Doc from "@effect/printer-ansi/AnsiDoc" +import * as Optimize from "@effect/printer/Optimize" +import * as Arr from "effect/Array" +import * as Effect from "effect/Effect" +import * as Number from "effect/Number" +import * as Option from "effect/Option" +import type * as Prompt from "../../Prompt.js" +import * as InternalPrompt from "../prompt.js" +import { Action } from "./action.js" +import * as InternalAnsiUtils from "./ansi-utils.js" +import { entriesToDisplay } from "./utils.js" + +interface SelectOptions extends Required> {} +interface MultiSelectOptions extends Prompt.Prompt.MultiSelectOptions {} + +type State = { + index: number + selectedIndices: Set + error: Option.Option +} + +const renderBeep = Doc.render(Doc.beep, { style: "pretty" }) + +const NEWLINE_REGEX = /\r?\n/ + +function renderOutput( + leadingSymbol: Doc.AnsiDoc, + trailingSymbol: Doc.AnsiDoc, + options: SelectOptions +) { + const annotateLine = (line: string): Doc.AnsiDoc => Doc.annotate(Doc.text(line), Ansi.bold) + const prefix = Doc.cat(leadingSymbol, Doc.space) + return Arr.match(options.message.split(NEWLINE_REGEX), { + onEmpty: () => Doc.hsep([prefix, trailingSymbol]), + onNonEmpty: (promptLines) => { + const lines = Arr.map(promptLines, (line) => annotateLine(line)) + return prefix.pipe( + Doc.cat(Doc.nest(Doc.vsep(lines), 2)), + Doc.cat(Doc.space), + Doc.cat(trailingSymbol), + Doc.cat(Doc.space) + ) + } + }) +} + +function renderError(state: State, pointer: Doc.AnsiDoc) { + return Option.match(state.error, { + onNone: () => Doc.empty, + onSome: (error) => + Arr.match(error.split(NEWLINE_REGEX), { + onEmpty: () => Doc.empty, + onNonEmpty: (errorLines) => { + const annotateLine = (line: string): Doc.AnsiDoc => + Doc.annotate(Doc.text(line), Ansi.combine(Ansi.italicized, Ansi.red)) + const prefix = Doc.cat(Doc.annotate(pointer, Ansi.red), Doc.space) + const lines = Arr.map(errorLines, (str) => annotateLine(str)) + return Doc.cursorSavePosition.pipe( + Doc.cat(Doc.hardLine), + Doc.cat(prefix), + Doc.cat(Doc.align(Doc.vsep(lines))), + Doc.cat(Doc.cursorRestorePosition) + ) + } + }) + }) +} + +function renderChoiceDescription( + choice: Prompt.Prompt.SelectChoice, + isHighlighted: boolean +) { + if (!choice.disabled && choice.description && isHighlighted) { + return Doc.char("-").pipe( + Doc.cat(Doc.space), + Doc.cat(Doc.text(choice.description)), + Doc.annotate(Ansi.blackBright) + ) + } + return Doc.empty +} + +const metaOptionsCount = 2 + +function renderChoices( + state: State, + options: SelectOptions & MultiSelectOptions, + figures: Effect.Effect.Success +) { + const choices = options.choices + const totalChoices = choices.length + const selectedCount = state.selectedIndices.size + const allSelected = selectedCount === totalChoices + + const selectAllText = allSelected + ? options?.selectNone ?? "Select None" + : options?.selectAll ?? "Select All" + + const inverseSelectionText = options?.inverseSelection ?? "Inverse Selection" + + const metaOptions = [ + { title: selectAllText }, + { title: inverseSelectionText } + ] + const allChoices = [...metaOptions, ...choices] + const toDisplay = entriesToDisplay(state.index, allChoices.length, options.maxPerPage) + const documents: Array = [] + for (let index = toDisplay.startIndex; index < toDisplay.endIndex; index++) { + const choice = allChoices[index] + const isHighlighted = state.index === index + let prefix: Doc.AnsiDoc = Doc.space + if (index === toDisplay.startIndex && toDisplay.startIndex > 0) { + prefix = figures.arrowUp + } else if (index === toDisplay.endIndex - 1 && toDisplay.endIndex < allChoices.length) { + prefix = figures.arrowDown + } + if (index < metaOptions.length) { + // Meta options + const title = isHighlighted + ? Doc.annotate(Doc.text(choice.title), Ansi.cyanBright) + : Doc.text(choice.title) + documents.push( + prefix.pipe( + Doc.cat(Doc.space), + Doc.cat(title) + ) + ) + } else { + // Regular choices + const choiceIndex = index - metaOptions.length + const isSelected = state.selectedIndices.has(choiceIndex) + const checkbox = isSelected ? figures.checkboxOn : figures.checkboxOff + const annotatedCheckbox = isHighlighted + ? Doc.annotate(checkbox, Ansi.cyanBright) + : checkbox + const title = Doc.text(choice.title) + const description = renderChoiceDescription(choice as Prompt.Prompt.SelectChoice, isHighlighted) + documents.push( + prefix.pipe( + Doc.cat(Doc.space), + Doc.cat(annotatedCheckbox), + Doc.cat(Doc.space), + Doc.cat(title), + Doc.cat(Doc.space), + Doc.cat(description) + ) + ) + } + } + return Doc.vsep(documents) +} + +function renderNextFrame(state: State, options: SelectOptions) { + return Effect.gen(function*() { + const terminal = yield* Terminal.Terminal + const columns = yield* terminal.columns + const figures = yield* InternalAnsiUtils.figures + const choices = renderChoices(state, options, figures) + const leadingSymbol = Doc.annotate(Doc.text("?"), Ansi.cyanBright) + const trailingSymbol = Doc.annotate(figures.pointerSmall, Ansi.blackBright) + const promptMsg = renderOutput(leadingSymbol, trailingSymbol, options) + const error = renderError(state, figures.pointer) + return Doc.cursorHide.pipe( + Doc.cat(promptMsg), + Doc.cat(Doc.hardLine), + Doc.cat(choices), + Doc.cat(error), + Doc.render({ style: "pretty", options: { lineWidth: columns } }) + ) + }) +} + +function renderSubmission(state: State, options: SelectOptions) { + return Effect.gen(function*() { + const terminal = yield* Terminal.Terminal + const columns = yield* terminal.columns + const figures = yield* InternalAnsiUtils.figures + const selectedChoices = Array.from(state.selectedIndices).sort(Number.Order).map((index) => + options.choices[index].title + ) + const selectedText = selectedChoices.join(", ") + const selected = Doc.text(selectedText) + const leadingSymbol = Doc.annotate(figures.tick, Ansi.green) + const trailingSymbol = Doc.annotate(figures.ellipsis, Ansi.blackBright) + const promptMsg = renderOutput(leadingSymbol, trailingSymbol, options) + return promptMsg.pipe( + Doc.cat(Doc.space), + Doc.cat(Doc.annotate(selected, Ansi.white)), + Doc.cat(Doc.hardLine), + Doc.render({ style: "pretty", options: { lineWidth: columns } }) + ) + }) +} + +function processCursorUp(state: State, totalChoices: number) { + const newIndex = state.index === 0 ? totalChoices - 1 : state.index - 1 + return Effect.succeed(Action.NextFrame({ state: { ...state, index: newIndex } })) +} + +function processCursorDown(state: State, totalChoices: number) { + const newIndex = (state.index + 1) % totalChoices + return Effect.succeed(Action.NextFrame({ state: { ...state, index: newIndex } })) +} + +function processSpace( + state: State, + options: SelectOptions +) { + const selectedIndices = new Set(state.selectedIndices) + if (state.index === 0) { + if (state.selectedIndices.size === options.choices.length) { + selectedIndices.clear() + } else { + for (let i = 0; i < options.choices.length; i++) { + selectedIndices.add(i) + } + } + } else if (state.index === 1) { + for (let i = 0; i < options.choices.length; i++) { + if (state.selectedIndices.has(i)) { + selectedIndices.delete(i) + } else { + selectedIndices.add(i) + } + } + } else { + const choiceIndex = state.index - metaOptionsCount + if (selectedIndices.has(choiceIndex)) { + selectedIndices.delete(choiceIndex) + } else { + selectedIndices.add(choiceIndex) + } + } + return Effect.succeed(Action.NextFrame({ state: { ...state, selectedIndices } })) +} + +export function handleClear(options: SelectOptions) { + return Effect.gen(function*() { + const terminal = yield* Terminal.Terminal + const columns = yield* terminal.columns + const clearPrompt = Doc.cat(Doc.eraseLine, Doc.cursorLeft) + const text = "\n".repeat(Math.min(options.choices.length + 2, options.maxPerPage)) + options.message + 1 + const clearOutput = InternalAnsiUtils.eraseText(text, columns) + return clearOutput.pipe( + Doc.cat(clearPrompt), + Optimize.optimize(Optimize.Deep), + Doc.render({ style: "pretty", options: { lineWidth: columns } }) + ) + }) +} + +function handleProcess(options: SelectOptions & MultiSelectOptions) { + return (input: Terminal.UserInput, state: State) => { + const totalChoices = options.choices.length + metaOptionsCount + switch (input.key.name) { + case "k": + case "up": { + return processCursorUp({ ...state, error: Option.none() }, totalChoices) + } + case "j": + case "down": + case "tab": { + return processCursorDown({ ...state, error: Option.none() }, totalChoices) + } + case "space": { + return processSpace(state, options) + } + case "enter": + case "return": { + const selectedCount = state.selectedIndices.size + if (options.min !== undefined && selectedCount < options.min) { + return Effect.succeed( + Action.NextFrame({ state: { ...state, error: Option.some(`At least ${options.min} are required`) } }) + ) + } + if (options.max !== undefined && selectedCount > options.max) { + return Effect.succeed( + Action.NextFrame({ state: { ...state, error: Option.some(`At most ${options.max} choices are allowed`) } }) + ) + } + const selectedValues = Array.from(state.selectedIndices).sort(Number.Order).map((index) => + options.choices[index].value + ) + return Effect.succeed(Action.Submit({ value: selectedValues })) + } + default: { + return Effect.succeed(Action.Beep()) + } + } + } +} + +function handleRender(options: SelectOptions) { + return (state: State, action: Prompt.Prompt.Action>) => { + return Action.$match(action, { + Beep: () => Effect.succeed(renderBeep), + NextFrame: ({ state }) => renderNextFrame(state, options), + Submit: () => renderSubmission(state, options) + }) + } +} + +/** @internal */ +export const multiSelect = ( + options: Prompt.Prompt.SelectOptions & Prompt.Prompt.MultiSelectOptions +): Prompt.Prompt> => { + const opts: SelectOptions & MultiSelectOptions = { + maxPerPage: 10, + ...options + } + // Seed initial selection from choices marked as selected: true + const initialSelected = new Set() + for (let i = 0; i < opts.choices.length; i++) { + const choice = opts.choices[i] as Prompt.Prompt.SelectChoice + if (choice.selected === true) { + initialSelected.add(i) + } + } + return InternalPrompt.custom({ index: 0, selectedIndices: initialSelected, error: Option.none() }, { + render: handleRender(opts), + process: handleProcess(opts), + clear: () => handleClear(opts) + }) +} diff --git a/repos/effect/packages/cli/src/internal/prompt/number.ts b/repos/effect/packages/cli/src/internal/prompt/number.ts new file mode 100644 index 0000000..8726aaa --- /dev/null +++ b/repos/effect/packages/cli/src/internal/prompt/number.ts @@ -0,0 +1,398 @@ +import * as Terminal from "@effect/platform/Terminal" +import * as Ansi from "@effect/printer-ansi/Ansi" +import * as Doc from "@effect/printer-ansi/AnsiDoc" +import * as Optimize from "@effect/printer/Optimize" +import * as Arr from "effect/Array" +import * as Effect from "effect/Effect" +import * as EffectNumber from "effect/Number" +import * as Option from "effect/Option" +import * as Schema from "effect/Schema" +import type * as Prompt from "../../Prompt.js" +import * as InternalPrompt from "../prompt.js" +import { Action } from "./action.js" +import * as InternalAnsiUtils from "./ansi-utils.js" + +interface IntegerOptions extends Required {} +interface FloatOptions extends Required {} + +interface State { + readonly cursor: number + readonly value: string + readonly error: Option.Option +} + +const parseInt = Schema.NumberFromString.pipe( + Schema.int(), + Schema.decodeUnknown +) + +const parseFloat = Schema.decodeUnknown(Schema.NumberFromString) + +const renderBeep = Doc.render(Doc.beep, { style: "pretty" }) + +function handleClear(options: IntegerOptions) { + return (state: State, _: Prompt.Prompt.Action) => { + return Effect.gen(function*() { + const terminal = yield* Terminal.Terminal + const columns = yield* terminal.columns + const resetCurrentLine = Doc.cat(Doc.eraseLine, Doc.cursorLeft) + const clearError = Option.match(state.error, { + onNone: () => Doc.empty, + onSome: (error) => + Doc.cursorDown(InternalAnsiUtils.lines(error, columns)).pipe( + Doc.cat(InternalAnsiUtils.eraseText(`\n${error}`, columns)) + ) + }) + const clearOutput = InternalAnsiUtils.eraseText(options.message, columns) + return clearError.pipe( + Doc.cat(clearOutput), + Doc.cat(resetCurrentLine), + Optimize.optimize(Optimize.Deep), + Doc.render({ style: "pretty", options: { lineWidth: columns } }) + ) + }) + } +} + +function renderInput(state: State, submitted: boolean) { + const annotation = Option.match(state.error, { + onNone: () => Ansi.combine(Ansi.underlined, Ansi.cyanBright), + onSome: () => Ansi.red + }) + const value = state.value === "" ? Doc.empty : Doc.text(`${state.value}`) + return submitted ? value : Doc.annotate(value, annotation) +} + +const NEWLINE_REGEX = /\r?\n/ + +function renderError(state: State, pointer: Doc.AnsiDoc) { + return Option.match(state.error, { + onNone: () => Doc.empty, + onSome: (error) => + Arr.match(error.split(NEWLINE_REGEX), { + onEmpty: () => Doc.empty, + onNonEmpty: (errorLines) => { + const annotateLine = (line: string): Doc.AnsiDoc => + Doc.annotate(Doc.text(line), Ansi.combine(Ansi.italicized, Ansi.red)) + const prefix = Doc.cat(Doc.annotate(pointer, Ansi.red), Doc.space) + const lines = Arr.map(errorLines, (str) => annotateLine(str)) + return Doc.cursorSavePosition.pipe( + Doc.cat(Doc.hardLine), + Doc.cat(prefix), + Doc.cat(Doc.align(Doc.vsep(lines))), + Doc.cat(Doc.cursorRestorePosition) + ) + } + }) + }) +} + +function renderOutput( + state: State, + leadingSymbol: Doc.AnsiDoc, + trailingSymbol: Doc.AnsiDoc, + options: IntegerOptions, + submitted: boolean = false +) { + const annotateLine = (line: string): Doc.AnsiDoc => Doc.annotate(Doc.text(line), Ansi.bold) + const prefix = Doc.cat(leadingSymbol, Doc.space) + return Arr.match(options.message.split(/\r?\n/), { + onEmpty: () => Doc.hsep([prefix, trailingSymbol, renderInput(state, submitted)]), + onNonEmpty: (promptLines) => { + const lines = Arr.map(promptLines, (line) => annotateLine(line)) + return prefix.pipe( + Doc.cat(Doc.nest(Doc.vsep(lines), 2)), + Doc.cat(Doc.space), + Doc.cat(trailingSymbol), + Doc.cat(Doc.space), + Doc.cat(renderInput(state, submitted)) + ) + } + }) +} + +function renderNextFrame(state: State, options: IntegerOptions) { + return Effect.gen(function*() { + const terminal = yield* Terminal.Terminal + const columns = yield* terminal.columns + const figures = yield* InternalAnsiUtils.figures + const leadingSymbol = Doc.annotate(Doc.text("?"), Ansi.cyanBright) + const trailingSymbol = Doc.annotate(figures.pointerSmall, Ansi.blackBright) + const errorMsg = renderError(state, figures.pointerSmall) + const promptMsg = renderOutput(state, leadingSymbol, trailingSymbol, options) + return promptMsg.pipe( + Doc.cat(errorMsg), + Optimize.optimize(Optimize.Deep), + Doc.render({ style: "pretty", options: { lineWidth: columns } }) + ) + }) +} + +function renderSubmission(nextState: State, options: IntegerOptions) { + return Effect.gen(function*() { + const terminal = yield* Terminal.Terminal + const columns = yield* terminal.columns + const figures = yield* InternalAnsiUtils.figures + const leadingSymbol = Doc.annotate(figures.tick, Ansi.green) + const trailingSymbol = Doc.annotate(figures.ellipsis, Ansi.blackBright) + const promptMsg = renderOutput(nextState, leadingSymbol, trailingSymbol, options, true) + return promptMsg.pipe( + Doc.cat(Doc.hardLine), + Optimize.optimize(Optimize.Deep), + Doc.render({ style: "pretty", options: { lineWidth: columns } }) + ) + }) +} + +function processBackspace(state: State) { + if (state.value.length <= 0) { + return Effect.succeed(Action.Beep()) + } + const value = state.value.slice(0, state.value.length - 1) + return Effect.succeed(Action.NextFrame({ + state: { ...state, value, error: Option.none() } + })) +} + +function defaultIntProcessor(state: State, input: string) { + if (state.value.length === 0 && input === "-") { + return Effect.succeed(Action.NextFrame({ + state: { ...state, value: "-", error: Option.none() } + })) + } + return Effect.match(parseInt(state.value + input), { + onFailure: () => Action.Beep(), + onSuccess: (value) => + Action.NextFrame({ + state: { ...state, value: `${value}`, error: Option.none() } + }) + }) +} + +function defaultFloatProcessor( + state: State, + input: string +) { + if (input === "." && state.value.includes(".")) { + return Effect.succeed(Action.Beep()) + } + if (state.value.length === 0 && input === "-") { + return Effect.succeed(Action.NextFrame({ + state: { ...state, value: "-", error: Option.none() } + })) + } + return Effect.match(parseFloat(state.value + input), { + onFailure: () => Action.Beep(), + onSuccess: (value) => + Action.NextFrame({ + state: { + ...state, + value: input === "." ? `${value}.` : `${value}`, + error: Option.none() + } + }) + }) +} + +const initialState: State = { + cursor: 0, + value: "", + error: Option.none() +} + +function handleRenderInteger(options: IntegerOptions) { + return (state: State, action: Prompt.Prompt.Action) => { + return Action.$match(action, { + Beep: () => Effect.succeed(renderBeep), + NextFrame: ({ state }) => renderNextFrame(state, options), + Submit: () => renderSubmission(state, options) + }) + } +} + +function handleProcessInteger(options: IntegerOptions) { + return (input: Terminal.UserInput, state: State) => { + switch (input.key.name) { + case "backspace": { + return processBackspace(state) + } + case "k": + case "up": { + return Effect.succeed(Action.NextFrame({ + state: { + ...state, + value: state.value === "" || state.value === "-" + ? `${options.incrementBy}` + : `${Number.parseInt(state.value) + options.incrementBy}`, + error: Option.none() + } + })) + } + case "j": + case "down": { + return Effect.succeed(Action.NextFrame({ + state: { + ...state, + value: state.value === "" || state.value === "-" + ? `-${options.decrementBy}` + : `${Number.parseInt(state.value) - options.decrementBy}`, + error: Option.none() + } + })) + } + case "enter": + case "return": { + return Effect.matchEffect(parseInt(state.value), { + onFailure: () => + Effect.succeed(Action.NextFrame({ + state: { + ...state, + error: Option.some("Must provide an integer value") + } + })), + onSuccess: (n) => + Effect.match(options.validate(n), { + onFailure: (error) => + Action.NextFrame({ + state: { + ...state, + error: Option.some(error) + } + }), + onSuccess: (value) => Action.Submit({ value }) + }) + }) + } + default: { + const value = Option.getOrElse(input.input, () => "") + return defaultIntProcessor(state, value) + } + } + } +} + +/** @internal */ +export const integer = (options: Prompt.Prompt.IntegerOptions): Prompt.Prompt => { + const opts: IntegerOptions = { + min: Number.NEGATIVE_INFINITY, + max: Number.POSITIVE_INFINITY, + incrementBy: 1, + decrementBy: 1, + validate: (n) => { + if (n < opts.min) { + return Effect.fail(`${n} must be greater than or equal to ${opts.min}`) + } + if (n > opts.max) { + return Effect.fail(`${n} must be less than or equal to ${opts.max}`) + } + return Effect.succeed(n) + }, + ...options + } + return InternalPrompt.custom(initialState, { + render: handleRenderInteger(opts), + process: handleProcessInteger(opts), + clear: handleClear(opts) + }) +} + +function handleRenderFloat(options: FloatOptions) { + return (state: State, action: Prompt.Prompt.Action) => { + return Action.$match(action, { + Beep: () => Effect.succeed(renderBeep), + NextFrame: ({ state }) => renderNextFrame(state, options), + Submit: () => renderSubmission(state, options) + }) + } +} + +function handleProcessFloat(options: FloatOptions) { + return (input: Terminal.UserInput, state: State) => { + switch (input.key.name) { + case "backspace": { + return processBackspace(state) + } + case "k": + case "up": { + return Effect.succeed(Action.NextFrame({ + state: { + ...state, + value: state.value === "" || state.value === "-" + ? `${options.incrementBy}` + : `${Number.parseFloat(state.value) + options.incrementBy}`, + error: Option.none() + } + })) + } + case "j": + case "down": { + return Effect.succeed(Action.NextFrame({ + state: { + ...state, + value: state.value === "" || state.value === "-" + ? `-${options.decrementBy}` + : `${Number.parseFloat(state.value) - options.decrementBy}`, + error: Option.none() + } + })) + } + case "enter": + case "return": { + return Effect.matchEffect(parseFloat(state.value), { + onFailure: () => + Effect.succeed(Action.NextFrame({ + state: { + ...state, + error: Option.some("Must provide a floating point value") + } + })), + onSuccess: (n) => + Effect.flatMap( + Effect.sync(() => EffectNumber.round(n, options.precision)), + (rounded) => + Effect.match(options.validate(rounded), { + onFailure: (error) => + Action.NextFrame({ + state: { + ...state, + error: Option.some(error) + } + }), + onSuccess: (value) => Action.Submit({ value }) + }) + ) + }) + } + default: { + const value = Option.getOrElse(input.input, () => "") + return defaultFloatProcessor(state, value) + } + } + } +} + +/** @internal */ +export const float = (options: Prompt.Prompt.FloatOptions): Prompt.Prompt => { + const opts: FloatOptions = { + min: Number.NEGATIVE_INFINITY, + max: Number.POSITIVE_INFINITY, + incrementBy: 1, + decrementBy: 1, + precision: 2, + validate: (n) => { + if (n < opts.min) { + return Effect.fail(`${n} must be greater than or equal to ${opts.min}`) + } + if (n > opts.max) { + return Effect.fail(`${n} must be less than or equal to ${opts.max}`) + } + return Effect.succeed(n) + }, + ...options + } + return InternalPrompt.custom(initialState, { + render: handleRenderFloat(opts), + process: handleProcessFloat(opts), + clear: handleClear(opts) + }) +} diff --git a/repos/effect/packages/cli/src/internal/prompt/select.ts b/repos/effect/packages/cli/src/internal/prompt/select.ts new file mode 100644 index 0000000..beb4e39 --- /dev/null +++ b/repos/effect/packages/cli/src/internal/prompt/select.ts @@ -0,0 +1,248 @@ +import * as Terminal from "@effect/platform/Terminal" +import * as Ansi from "@effect/printer-ansi/Ansi" +import * as Doc from "@effect/printer-ansi/AnsiDoc" +import * as Optimize from "@effect/printer/Optimize" +import * as Arr from "effect/Array" +import * as Effect from "effect/Effect" +import type * as Prompt from "../../Prompt.js" +import * as InternalPrompt from "../prompt.js" +import { Action } from "./action.js" +import * as InternalAnsiUtils from "./ansi-utils.js" +import { entriesToDisplay } from "./utils.js" + +type State = number + +interface SelectOptions extends Required> {} + +const renderBeep = Doc.render(Doc.beep, { style: "pretty" }) + +const NEWLINE_REGEX = /\r?\n/ + +function renderOutput( + leadingSymbol: Doc.AnsiDoc, + trailingSymbol: Doc.AnsiDoc, + options: SelectOptions +) { + const annotateLine = (line: string): Doc.AnsiDoc => Doc.annotate(Doc.text(line), Ansi.bold) + const prefix = Doc.cat(leadingSymbol, Doc.space) + return Arr.match(options.message.split(NEWLINE_REGEX), { + onEmpty: () => Doc.hsep([prefix, trailingSymbol]), + onNonEmpty: (promptLines) => { + const lines = Arr.map(promptLines, (line) => annotateLine(line)) + return prefix.pipe( + Doc.cat(Doc.nest(Doc.vsep(lines), 2)), + Doc.cat(Doc.space), + Doc.cat(trailingSymbol), + Doc.cat(Doc.space) + ) + } + }) +} + +function renderChoicePrefix( + state: State, + choices: SelectOptions["choices"], + toDisplay: { readonly startIndex: number; readonly endIndex: number }, + currentIndex: number, + figures: Effect.Effect.Success +) { + let prefix: Doc.AnsiDoc = Doc.space + if (currentIndex === toDisplay.startIndex && toDisplay.startIndex > 0) { + prefix = figures.arrowUp + } else if (currentIndex === toDisplay.endIndex - 1 && toDisplay.endIndex < choices.length) { + prefix = figures.arrowDown + } + if (choices[currentIndex].disabled) { + const annotation = Ansi.combine(Ansi.bold, Ansi.blackBright) + return state === currentIndex + ? figures.pointer.pipe(Doc.annotate(annotation), Doc.cat(prefix)) + : prefix.pipe(Doc.cat(Doc.space)) + } + return state === currentIndex + ? figures.pointer.pipe(Doc.annotate(Ansi.cyanBright), Doc.cat(prefix)) + : prefix.pipe(Doc.cat(Doc.space)) +} + +function renderChoiceTitle( + choice: Prompt.Prompt.SelectChoice, + isSelected: boolean +) { + const title = Doc.text(choice.title) + if (isSelected) { + return choice.disabled + ? Doc.annotate(title, Ansi.combine(Ansi.underlined, Ansi.blackBright)) + : Doc.annotate(title, Ansi.combine(Ansi.underlined, Ansi.cyanBright)) + } + return choice.disabled + ? Doc.annotate(title, Ansi.combine(Ansi.strikethrough, Ansi.blackBright)) + : title +} + +function renderChoiceDescription( + choice: Prompt.Prompt.SelectChoice, + isSelected: boolean +) { + if (!choice.disabled && choice.description && isSelected) { + return Doc.char("-").pipe( + Doc.cat(Doc.space), + Doc.cat(Doc.text(choice.description)), + Doc.annotate(Ansi.blackBright) + ) + } + return Doc.empty +} + +function renderChoices( + state: State, + options: SelectOptions, + figures: Effect.Effect.Success +) { + const choices = options.choices + const toDisplay = entriesToDisplay(state, choices.length, options.maxPerPage) + const documents: Array = [] + for (let index = toDisplay.startIndex; index < toDisplay.endIndex; index++) { + const choice = choices[index] + const isSelected = state === index + const prefix = renderChoicePrefix(state, choices, toDisplay, index, figures) + const title = renderChoiceTitle(choice, isSelected) + const description = renderChoiceDescription(choice, isSelected) + documents.push(prefix.pipe(Doc.cat(title), Doc.cat(Doc.space), Doc.cat(description))) + } + return Doc.vsep(documents) +} + +function renderNextFrame(state: State, options: SelectOptions) { + return Effect.gen(function*() { + const terminal = yield* Terminal.Terminal + const columns = yield* terminal.columns + const figures = yield* InternalAnsiUtils.figures + const choices = renderChoices(state, options, figures) + const leadingSymbol = Doc.annotate(Doc.text("?"), Ansi.cyanBright) + const trailingSymbol = Doc.annotate(figures.pointerSmall, Ansi.blackBright) + const promptMsg = renderOutput(leadingSymbol, trailingSymbol, options) + return Doc.cursorHide.pipe( + Doc.cat(promptMsg), + Doc.cat(Doc.hardLine), + Doc.cat(choices), + Doc.render({ style: "pretty", options: { lineWidth: columns } }) + ) + }) +} + +function renderSubmission(state: State, options: SelectOptions) { + return Effect.gen(function*() { + const terminal = yield* Terminal.Terminal + const columns = yield* terminal.columns + const figures = yield* InternalAnsiUtils.figures + const selected = Doc.text(options.choices[state].title) + const leadingSymbol = Doc.annotate(figures.tick, Ansi.green) + const trailingSymbol = Doc.annotate(figures.ellipsis, Ansi.blackBright) + const promptMsg = renderOutput(leadingSymbol, trailingSymbol, options) + return promptMsg.pipe( + Doc.cat(Doc.space), + Doc.cat(Doc.annotate(selected, Ansi.white)), + Doc.cat(Doc.hardLine), + Doc.render({ style: "pretty", options: { lineWidth: columns } }) + ) + }) +} + +function processCursorUp(state: State, choices: Prompt.Prompt.SelectOptions["choices"]) { + if (state === 0) { + return Effect.succeed(Action.NextFrame({ state: choices.length - 1 })) + } + return Effect.succeed(Action.NextFrame({ state: state - 1 })) +} + +function processCursorDown(state: State, choices: Prompt.Prompt.SelectOptions["choices"]) { + if (state === choices.length - 1) { + return Effect.succeed(Action.NextFrame({ state: 0 })) + } + return Effect.succeed(Action.NextFrame({ state: state + 1 })) +} + +function processNext(state: State, choices: Prompt.Prompt.SelectOptions["choices"]) { + return Effect.succeed(Action.NextFrame({ state: (state + 1) % choices.length })) +} + +function handleRender(options: SelectOptions) { + return (state: State, action: Prompt.Prompt.Action) => { + return Action.$match(action, { + Beep: () => Effect.succeed(renderBeep), + NextFrame: ({ state }) => renderNextFrame(state, options), + Submit: () => renderSubmission(state, options) + }) + } +} + +export function handleClear(options: SelectOptions) { + return Effect.gen(function*() { + const terminal = yield* Terminal.Terminal + const columns = yield* terminal.columns + const clearPrompt = Doc.cat(Doc.eraseLine, Doc.cursorLeft) + const text = "\n".repeat(Math.min(options.choices.length, options.maxPerPage)) + options.message + const clearOutput = InternalAnsiUtils.eraseText(text, columns) + return clearOutput.pipe( + Doc.cat(clearPrompt), + Optimize.optimize(Optimize.Deep), + Doc.render({ style: "pretty", options: { lineWidth: columns } }) + ) + }) +} + +function handleProcess(options: SelectOptions) { + return (input: Terminal.UserInput, state: State) => { + switch (input.key.name) { + case "k": + case "up": { + return processCursorUp(state, options.choices) + } + case "j": + case "down": { + return processCursorDown(state, options.choices) + } + case "tab": { + return processNext(state, options.choices) + } + case "enter": + case "return": { + const selected = options.choices[state] + if (selected.disabled) { + return Effect.succeed(Action.Beep()) + } + return Effect.succeed(Action.Submit({ value: selected.value })) + } + default: { + return Effect.succeed(Action.Beep()) + } + } + } +} + +/** @internal */ +export const select = (options: Prompt.Prompt.SelectOptions): Prompt.Prompt => { + const opts: SelectOptions = { + maxPerPage: 10, + ...options + } + // Validate and seed initial index from any choice marked selected: true + let initialIndex = 0 + let seenSelected = -1 + for (let i = 0; i < opts.choices.length; i++) { + const choice = opts.choices[i] as Prompt.Prompt.SelectChoice + if (choice.selected === true) { + if (seenSelected !== -1) { + throw new Error("InvalidArgumentException: only a single choice can be selected by default for Prompt.select") + } + seenSelected = i + } + } + if (seenSelected !== -1) { + initialIndex = seenSelected + } + return InternalPrompt.custom(initialIndex, { + render: handleRender(opts), + process: handleProcess(opts), + clear: () => handleClear(opts) + }) +} diff --git a/repos/effect/packages/cli/src/internal/prompt/text.ts b/repos/effect/packages/cli/src/internal/prompt/text.ts new file mode 100644 index 0000000..1cfc356 --- /dev/null +++ b/repos/effect/packages/cli/src/internal/prompt/text.ts @@ -0,0 +1,330 @@ +import * as Terminal from "@effect/platform/Terminal" +import * as Ansi from "@effect/printer-ansi/Ansi" +import * as Doc from "@effect/printer-ansi/AnsiDoc" +import * as Optimize from "@effect/printer/Optimize" +import * as Arr from "effect/Array" +import * as Effect from "effect/Effect" +import * as Option from "effect/Option" +import * as Redacted from "effect/Redacted" +import type * as Prompt from "../../Prompt.js" +import * as InternalPrompt from "../prompt.js" +import { Action } from "./action.js" +import * as InternalAnsiUtils from "./ansi-utils.js" + +interface Options extends Required { + /** + * The type of the text option. + */ + readonly type: "hidden" | "password" | "text" +} + +interface State { + readonly cursor: number + readonly value: string + readonly error: Option.Option +} + +function getValue(state: State, options: Options): string { + return state.value.length > 0 ? state.value : options.default +} + +const renderBeep = Doc.render(Doc.beep, { style: "pretty" }) + +function renderClearScreen(state: State, options: Options) { + return Effect.gen(function*() { + const terminal = yield* Terminal.Terminal + const columns = yield* terminal.columns + // Erase the current line and place the cursor in column one + const resetCurrentLine = Doc.cat(Doc.eraseLine, Doc.cursorLeft) + // Check for any error output + const clearError = Option.match(state.error, { + onNone: () => Doc.empty, + onSome: (error) => + // If there was an error, move the cursor down to the final error line and + // then clear all lines of error output + Doc.cursorDown(InternalAnsiUtils.lines(error, columns)).pipe( + // Add a leading newline to the error message to ensure that the corrrect + // number of error lines are erased + Doc.cat(InternalAnsiUtils.eraseText(`\n${error}`, columns)) + ) + }) + // Ensure that the prior prompt output is cleaned up + // Calculate full rendered line: "? " + message + " › " + input + const inputValue = state.value.length > 0 ? state.value : options.default + const fullLine = `? ${options.message} \u203a ${inputValue}` + const clearOutput = InternalAnsiUtils.eraseText(fullLine, columns) + // Concatenate and render all documents + return clearError.pipe( + Doc.cat(clearOutput), + Doc.cat(resetCurrentLine), + Optimize.optimize(Optimize.Deep), + Doc.render({ style: "pretty", options: { lineWidth: columns } }) + ) + }) +} + +function renderInput(nextState: State, options: Options, submitted: boolean) { + const text = getValue(nextState, options) + + const annotation = Option.match(nextState.error, { + onNone: () => { + if (submitted) { + return Ansi.white + } + + if (nextState.value.length === 0) { + return Ansi.blackBright + } + + return Ansi.combine(Ansi.underlined, Ansi.cyanBright) + }, + onSome: () => Ansi.red + }) + + switch (options.type) { + case "hidden": { + return Doc.empty + } + case "password": { + return Doc.annotate(Doc.text("*".repeat(text.length)), annotation) + } + case "text": { + return Doc.annotate(Doc.text(text), annotation) + } + } +} + +function renderError(nextState: State, pointer: Doc.AnsiDoc) { + return Option.match(nextState.error, { + onNone: () => Doc.empty, + onSome: (error) => + Arr.match(error.split(/\r?\n/), { + onEmpty: () => Doc.empty, + onNonEmpty: (errorLines) => { + const annotateLine = (line: string): Doc.AnsiDoc => + Doc.text(line).pipe( + Doc.annotate(Ansi.combine(Ansi.italicized, Ansi.red)) + ) + const prefix = Doc.cat(Doc.annotate(pointer, Ansi.red), Doc.space) + const lines = Arr.map(errorLines, (str) => annotateLine(str)) + return Doc.cursorSavePosition.pipe( + Doc.cat(Doc.hardLine), + Doc.cat(prefix), + Doc.cat(Doc.align(Doc.vsep(lines))), + Doc.cat(Doc.cursorRestorePosition) + ) + } + }) + }) +} + +function renderOutput( + nextState: State, + leadingSymbol: Doc.AnsiDoc, + trailingSymbol: Doc.AnsiDoc, + options: Options, + submitted: boolean = false +) { + const annotateLine = (line: string): Doc.AnsiDoc => Doc.annotate(Doc.text(line), Ansi.bold) + const promptLines = options.message.split(/\r?\n/) + const prefix = Doc.cat(leadingSymbol, Doc.space) + if (Arr.isNonEmptyReadonlyArray(promptLines)) { + const lines = Arr.map(promptLines, (line) => annotateLine(line)) + return prefix.pipe( + Doc.cat(Doc.nest(Doc.vsep(lines), 2)), + Doc.cat(Doc.space), + Doc.cat(trailingSymbol), + Doc.cat(Doc.space), + Doc.cat(renderInput(nextState, options, submitted)) + ) + } + return Doc.hsep([prefix, trailingSymbol, renderInput(nextState, options, submitted)]) +} + +function renderNextFrame(state: State, options: Options) { + return Effect.gen(function*() { + const terminal = yield* Terminal.Terminal + const columns = yield* terminal.columns + const figures = yield* InternalAnsiUtils.figures + const leadingSymbol = Doc.annotate(Doc.text("?"), Ansi.cyanBright) + const trailingSymbol = Doc.annotate(figures.pointerSmall, Ansi.blackBright) + const promptMsg = renderOutput(state, leadingSymbol, trailingSymbol, options) + const errorMsg = renderError(state, figures.pointerSmall) + const offset = state.cursor - state.value.length + return promptMsg.pipe( + Doc.cat(errorMsg), + Doc.cat(Doc.cursorMove(offset)), + Optimize.optimize(Optimize.Deep), + Doc.render({ style: "pretty", options: { lineWidth: columns } }) + ) + }) +} + +function renderSubmission(state: State, options: Options) { + return Effect.gen(function*() { + const terminal = yield* Terminal.Terminal + const columns = yield* terminal.columns + const figures = yield* InternalAnsiUtils.figures + const leadingSymbol = Doc.annotate(figures.tick, Ansi.green) + const trailingSymbol = Doc.annotate(figures.ellipsis, Ansi.blackBright) + const promptMsg = renderOutput(state, leadingSymbol, trailingSymbol, options, true) + return promptMsg.pipe( + Doc.cat(Doc.hardLine), + Optimize.optimize(Optimize.Deep), + Doc.render({ style: "pretty", options: { lineWidth: columns } }) + ) + }) +} + +function processBackspace(state: State) { + if (state.cursor <= 0) { + return Effect.succeed(Action.Beep()) + } + const beforeCursor = state.value.slice(0, state.cursor - 1) + const afterCursor = state.value.slice(state.cursor) + const cursor = state.cursor - 1 + const value = `${beforeCursor}${afterCursor}` + return Effect.succeed( + Action.NextFrame({ + state: { ...state, cursor, value, error: Option.none() } + }) + ) +} + +function processCursorLeft(state: State) { + if (state.cursor <= 0) { + return Effect.succeed(Action.Beep()) + } + const cursor = state.cursor - 1 + return Effect.succeed( + Action.NextFrame({ + state: { ...state, cursor, error: Option.none() } + }) + ) +} + +function processCursorRight(state: State) { + if (state.cursor >= state.value.length) { + return Effect.succeed(Action.Beep()) + } + const cursor = Math.min(state.cursor + 1, state.value.length) + return Effect.succeed( + Action.NextFrame({ + state: { ...state, cursor, error: Option.none() } + }) + ) +} + +function processTab(state: State, options: Options) { + if (state.value === options.default) { + return Effect.succeed(Action.Beep()) + } + const value = getValue(state, options) + const cursor = value.length + return Effect.succeed( + Action.NextFrame({ + state: { ...state, value, cursor, error: Option.none() } + }) + ) +} + +function defaultProcessor(input: string, state: State) { + const beforeCursor = state.value.slice(0, state.cursor) + const afterCursor = state.value.slice(state.cursor) + const value = `${beforeCursor}${input}${afterCursor}` + const cursor = state.cursor + input.length + return Effect.succeed( + Action.NextFrame({ + state: { ...state, cursor, value, error: Option.none() } + }) + ) +} + +const initialState: State = { + cursor: 0, + value: "", + error: Option.none() +} + +function handleRender(options: Options) { + return (state: State, action: Prompt.Prompt.Action) => { + return Action.$match(action, { + Beep: () => Effect.succeed(renderBeep), + NextFrame: ({ state }) => renderNextFrame(state, options), + Submit: () => renderSubmission(state, options) + }) + } +} + +function handleProcess(options: Options) { + return (input: Terminal.UserInput, state: State) => { + switch (input.key.name) { + case "backspace": { + return processBackspace(state) + } + case "left": { + return processCursorLeft(state) + } + case "right": { + return processCursorRight(state) + } + case "enter": + case "return": { + const value = getValue(state, options) + return Effect.match(options.validate(value), { + onFailure: (error) => + Action.NextFrame({ + state: { ...state, value, error: Option.some(error) } + }), + onSuccess: (value) => Action.Submit({ value }) + }) + } + case "tab": { + return processTab(state, options) + } + default: { + const value = Option.getOrElse(input.input, () => "") + return defaultProcessor(value, state) + } + } + } +} + +function handleClear(options: Options) { + return (state: State, _: Prompt.Prompt.Action) => { + return renderClearScreen(state, options) + } +} + +function basePrompt( + options: Prompt.Prompt.TextOptions, + type: Options["type"] +): Prompt.Prompt { + const opts: Options = { + default: "", + type, + validate: Effect.succeed, + ...options + } + + return InternalPrompt.custom(initialState, { + render: handleRender(opts), + process: handleProcess(opts), + clear: handleClear(opts) + }) +} + +/** @internal */ +export const hidden = ( + options: Prompt.Prompt.TextOptions +): Prompt.Prompt => basePrompt(options, "hidden").pipe(InternalPrompt.map(Redacted.make)) + +/** @internal */ +export const password = ( + options: Prompt.Prompt.TextOptions +): Prompt.Prompt => basePrompt(options, "password").pipe(InternalPrompt.map(Redacted.make)) + +/** @internal */ +export const text = ( + options: Prompt.Prompt.TextOptions +): Prompt.Prompt => basePrompt(options, "text") diff --git a/repos/effect/packages/cli/src/internal/prompt/toggle.ts b/repos/effect/packages/cli/src/internal/prompt/toggle.ts new file mode 100644 index 0000000..20e25af --- /dev/null +++ b/repos/effect/packages/cli/src/internal/prompt/toggle.ts @@ -0,0 +1,165 @@ +import * as Terminal from "@effect/platform/Terminal" +import * as Ansi from "@effect/printer-ansi/Ansi" +import * as Doc from "@effect/printer-ansi/AnsiDoc" +import * as Optimize from "@effect/printer/Optimize" +import * as Arr from "effect/Array" +import * as Effect from "effect/Effect" +import type * as Prompt from "../../Prompt.js" +import * as InternalPrompt from "../prompt.js" +import { Action } from "./action.js" +import * as InternalAnsiUtils from "./ansi-utils.js" + +interface ToggleOptions extends Required {} + +type State = boolean + +const renderBeep = Doc.render(Doc.beep, { style: "pretty" }) + +function handleClear(options: ToggleOptions) { + return Effect.gen(function*() { + const terminal = yield* Terminal.Terminal + const columns = yield* terminal.columns + const clearPrompt = Doc.cat(Doc.eraseLine, Doc.cursorLeft) + const clearOutput = InternalAnsiUtils.eraseText(options.message, columns) + return clearOutput.pipe( + Doc.cat(clearPrompt), + Optimize.optimize(Optimize.Deep), + Doc.render({ style: "pretty", options: { lineWidth: columns } }) + ) + }) +} + +function renderToggle( + value: boolean, + options: ToggleOptions, + submitted: boolean = false +) { + const separator = Doc.annotate(Doc.char("/"), Ansi.blackBright) + const selectedAnnotation = Ansi.combine(Ansi.underlined, submitted ? Ansi.white : Ansi.cyanBright) + const inactive = value + ? Doc.text(options.inactive) + : Doc.annotate(Doc.text(options.inactive), selectedAnnotation) + const active = value + ? Doc.annotate(Doc.text(options.active), selectedAnnotation) + : Doc.text(options.active) + return Doc.hsep([active, separator, inactive]) +} + +function renderOutput( + toggle: Doc.AnsiDoc, + leadingSymbol: Doc.AnsiDoc, + trailingSymbol: Doc.AnsiDoc, + options: ToggleOptions +) { + const annotateLine = (line: string): Doc.AnsiDoc => Doc.annotate(Doc.text(line), Ansi.bold) + const promptLines = options.message.split(/\r?\n/) + const prefix = Doc.cat(leadingSymbol, Doc.space) + if (Arr.isNonEmptyReadonlyArray(promptLines)) { + const lines = Arr.map(promptLines, (line) => annotateLine(line)) + return prefix.pipe( + Doc.cat(Doc.nest(Doc.vsep(lines), 2)), + Doc.cat(Doc.space), + Doc.cat(trailingSymbol), + Doc.cat(Doc.space), + Doc.cat(toggle) + ) + } + return Doc.hsep([prefix, trailingSymbol, toggle]) +} + +function renderNextFrame(state: State, options: ToggleOptions) { + return Effect.gen(function*() { + const terminal = yield* Terminal.Terminal + const figures = yield* InternalAnsiUtils.figures + const columns = yield* terminal.columns + const leadingSymbol = Doc.annotate(Doc.text("?"), Ansi.cyanBright) + const trailingSymbol = Doc.annotate(figures.pointerSmall, Ansi.blackBright) + const toggle = renderToggle(state, options) + const promptMsg = renderOutput(toggle, leadingSymbol, trailingSymbol, options) + return Doc.cursorHide.pipe( + Doc.cat(promptMsg), + Optimize.optimize(Optimize.Deep), + Doc.render({ style: "pretty", options: { lineWidth: columns } }) + ) + }) +} + +function renderSubmission(value: boolean, options: ToggleOptions) { + return Effect.gen(function*() { + const terminal = yield* Terminal.Terminal + const figures = yield* InternalAnsiUtils.figures + const columns = yield* terminal.columns + const leadingSymbol = Doc.annotate(figures.tick, Ansi.green) + const trailingSymbol = Doc.annotate(figures.ellipsis, Ansi.blackBright) + const toggle = renderToggle(value, options, true) + const promptMsg = renderOutput(toggle, leadingSymbol, trailingSymbol, options) + return promptMsg.pipe( + Doc.cat(Doc.hardLine), + Optimize.optimize(Optimize.Deep), + Doc.render({ style: "pretty", options: { lineWidth: columns } }) + ) + }) +} + +const activate = Effect.succeed(Action.NextFrame({ state: true })) +const deactivate = Effect.succeed(Action.NextFrame({ state: false })) + +function handleRender(options: ToggleOptions) { + return (state: State, action: Prompt.Prompt.Action) => { + switch (action._tag) { + case "Beep": { + return Effect.succeed(renderBeep) + } + case "NextFrame": { + return renderNextFrame(state, options) + } + case "Submit": { + return renderSubmission(state, options) + } + } + } +} + +function handleProcess(input: Terminal.UserInput, state: State) { + switch (input.key.name) { + case "0": + case "j": + case "delete": + case "right": + case "down": { + return deactivate + } + case "1": + case "k": + case "left": + case "up": { + return activate + } + case " ": + case "tab": { + return state ? deactivate : activate + } + case "enter": + case "return": { + return Effect.succeed(Action.Submit({ value: state })) + } + default: { + return Effect.succeed(Action.Beep()) + } + } +} + +/** @internal */ +export const toggle = (options: Prompt.Prompt.ToggleOptions): Prompt.Prompt => { + const opts: ToggleOptions = { + initial: false, + active: "on", + inactive: "off", + ...options + } + return InternalPrompt.custom(opts.initial, { + render: handleRender(opts), + process: handleProcess, + clear: () => handleClear(opts) + }) +} diff --git a/repos/effect/packages/cli/src/internal/prompt/utils.ts b/repos/effect/packages/cli/src/internal/prompt/utils.ts new file mode 100644 index 0000000..f651a44 --- /dev/null +++ b/repos/effect/packages/cli/src/internal/prompt/utils.ts @@ -0,0 +1,10 @@ +/** @internal */ +export const entriesToDisplay = (cursor: number, total: number, maxVisible?: number) => { + const max = maxVisible === undefined ? total : maxVisible + let startIndex = Math.min(total - max, cursor - Math.floor(max / 2)) + if (startIndex < 0) { + startIndex = 0 + } + const endIndex = Math.min(startIndex + max, total) + return { startIndex, endIndex } +} diff --git a/repos/effect/packages/cli/src/internal/usage.ts b/repos/effect/packages/cli/src/internal/usage.ts new file mode 100644 index 0000000..593a6ca --- /dev/null +++ b/repos/effect/packages/cli/src/internal/usage.ts @@ -0,0 +1,236 @@ +import * as Arr from "effect/Array" +import { dual, pipe } from "effect/Function" +import * as Option from "effect/Option" +import type * as CliConfig from "../CliConfig.js" +import type * as HelpDoc from "../HelpDoc.js" +import type * as Span from "../HelpDoc/Span.js" +import type * as Usage from "../Usage.js" +import * as InternalCliConfig from "./cliConfig.js" +import * as InternalHelpDoc from "./helpDoc.js" +import * as InternalSpan from "./helpDoc/span.js" + +// ============================================================================= +// Constructors +// ============================================================================= + +/** @internal */ +export const empty: Usage.Usage = { + _tag: "Empty" +} + +/** @internal */ +export const mixed: Usage.Usage = { + _tag: "Empty" +} + +/** @internal */ +export const named = ( + names: ReadonlyArray, + acceptedValues: Option.Option +): Usage.Usage => ({ + _tag: "Named", + names, + acceptedValues +}) + +/** @internal */ +export const optional = (self: Usage.Usage): Usage.Usage => ({ + _tag: "Optional", + usage: self +}) + +/** @internal */ +export const repeated = (self: Usage.Usage): Usage.Usage => ({ + _tag: "Repeated", + usage: self +}) + +export const alternation = dual< + (that: Usage.Usage) => (self: Usage.Usage) => Usage.Usage, + (self: Usage.Usage, that: Usage.Usage) => Usage.Usage +>(2, (self, that) => ({ + _tag: "Alternation", + left: self, + right: that +})) + +/** @internal */ +export const concat = dual< + (that: Usage.Usage) => (self: Usage.Usage) => Usage.Usage, + (self: Usage.Usage, that: Usage.Usage) => Usage.Usage +>(2, (self, that) => ({ + _tag: "Concat", + left: self, + right: that +})) + +// ============================================================================= +// Combinators +// ============================================================================= + +/** @internal */ +export const getHelp = (self: Usage.Usage): HelpDoc.HelpDoc => { + const spans = enumerate(self, InternalCliConfig.defaultConfig) + if (Arr.isNonEmptyReadonlyArray(spans)) { + const head = Arr.headNonEmpty(spans) + const tail = Arr.tailNonEmpty(spans) + if (Arr.isNonEmptyReadonlyArray(tail)) { + return pipe( + Arr.map(spans, (span) => InternalHelpDoc.p(span)), + Arr.reduceRight( + InternalHelpDoc.empty, + (left, right) => InternalHelpDoc.sequence(left, right) + ) + ) + } + return InternalHelpDoc.p(head) + } + return InternalHelpDoc.empty +} + +/** @internal */ +export const enumerate = dual< + (config: CliConfig.CliConfig) => (self: Usage.Usage) => Array, + (self: Usage.Usage, config: CliConfig.CliConfig) => Array +>(2, (self, config) => render(simplify(self, config), config)) + +// ============================================================================= +// Internals +// ============================================================================= + +const simplify = (self: Usage.Usage, config: CliConfig.CliConfig): Usage.Usage => { + switch (self._tag) { + case "Empty": { + return empty + } + case "Mixed": { + return mixed + } + case "Named": { + if (Option.isNone(Arr.head(render(self, config)))) { + return empty + } + return self + } + case "Optional": { + if (self.usage._tag === "Empty") { + return empty + } + const usage = simplify(self.usage, config) + // No need to do anything for empty usage + return usage._tag === "Empty" + ? empty + // Avoid re-wrapping the usage in an optional instruction + : usage._tag === "Optional" + ? usage + : optional(usage) + } + case "Repeated": { + const usage = simplify(self.usage, config) + return usage._tag === "Empty" ? empty : repeated(usage) + } + case "Alternation": { + const leftUsage = simplify(self.left, config) + const rightUsage = simplify(self.right, config) + return leftUsage._tag === "Empty" + ? rightUsage + : rightUsage._tag === "Empty" + ? leftUsage + : alternation(leftUsage, rightUsage) + } + case "Concat": { + const leftUsage = simplify(self.left, config) + const rightUsage = simplify(self.right, config) + return leftUsage._tag === "Empty" + ? rightUsage + : rightUsage._tag === "Empty" + ? leftUsage + : concat(leftUsage, rightUsage) + } + } +} + +const render = (self: Usage.Usage, config: CliConfig.CliConfig): Array => { + switch (self._tag) { + case "Empty": { + return Arr.of(InternalSpan.text("")) + } + case "Mixed": { + return Arr.of(InternalSpan.text("")) + } + case "Named": { + const typeInfo = config.showTypes + ? Option.match(self.acceptedValues, { + onNone: () => InternalSpan.empty, + onSome: (s) => InternalSpan.concat(InternalSpan.space, InternalSpan.text(s)) + }) + : InternalSpan.empty + const namesToShow = config.showAllNames + ? self.names + : self.names.length > 1 + ? pipe( + Arr.filter(self.names, (name) => name.startsWith("--")), + Arr.head, + Option.map(Arr.of), + Option.getOrElse(() => self.names) + ) + : self.names + const nameInfo = InternalSpan.text(Arr.join(namesToShow, ", ")) + return config.showAllNames && self.names.length > 1 + ? Arr.of(InternalSpan.spans([ + InternalSpan.text("("), + nameInfo, + typeInfo, + InternalSpan.text(")") + ])) + : Arr.of(InternalSpan.concat(nameInfo, typeInfo)) + } + case "Optional": { + return Arr.map(render(self.usage, config), (span) => + InternalSpan.spans([ + InternalSpan.text("["), + span, + InternalSpan.text("]") + ])) + } + case "Repeated": { + return Arr.map( + render(self.usage, config), + (span) => InternalSpan.concat(span, InternalSpan.text("...")) + ) + } + case "Alternation": { + if ( + self.left._tag === "Repeated" || + self.right._tag === "Repeated" || + self.left._tag === "Concat" || + self.right._tag === "Concat" + ) { + return Arr.appendAll( + render(self.left, config), + render(self.right, config) + ) + } + return Arr.flatMap( + render(self.left, config), + (left) => + Arr.map( + render(self.right, config), + (right) => InternalSpan.spans([left, InternalSpan.text("|"), right]) + ) + ) + } + case "Concat": { + const leftSpan = render(self.left, config) + const rightSpan = render(self.right, config) + const separator = Arr.isNonEmptyReadonlyArray(leftSpan) && + Arr.isNonEmptyReadonlyArray(rightSpan) + ? InternalSpan.space + : InternalSpan.empty + return Arr.flatMap( + leftSpan, + (left) => Arr.map(rightSpan, (right) => InternalSpan.spans([left, separator, right])) + ) + } + } +} diff --git a/repos/effect/packages/cli/src/internal/validationError.ts b/repos/effect/packages/cli/src/internal/validationError.ts new file mode 100644 index 0000000..7ac8c5c --- /dev/null +++ b/repos/effect/packages/cli/src/internal/validationError.ts @@ -0,0 +1,163 @@ +import type * as HelpDoc from "../HelpDoc.js" +import type * as ValidationError from "../ValidationError.js" + +const ValidationErrorSymbolKey = "@effect/cli/ValidationError" + +/** @internal */ +export const ValidationErrorTypeId: ValidationError.ValidationErrorTypeId = Symbol.for( + ValidationErrorSymbolKey +) as ValidationError.ValidationErrorTypeId + +/** @internal */ +export const proto: ValidationError.ValidationError.Proto = { + [ValidationErrorTypeId]: ValidationErrorTypeId +} + +/** @internal */ +export const isValidationError = (u: unknown): u is ValidationError.ValidationError => + typeof u === "object" && u != null && ValidationErrorTypeId in u + +/** @internal */ +export const isCommandMismatch = ( + self: ValidationError.ValidationError +): self is ValidationError.CommandMismatch => self._tag === "CommandMismatch" + +/** @internal */ +export const isCorrectedFlag = ( + self: ValidationError.ValidationError +): self is ValidationError.CorrectedFlag => self._tag === "CorrectedFlag" + +/** @internal */ +export const isHelpRequested = ( + self: ValidationError.ValidationError +): self is ValidationError.HelpRequested => self._tag === "HelpRequested" + +/** @internal */ +export const isInvalidArgument = ( + self: ValidationError.ValidationError +): self is ValidationError.InvalidArgument => self._tag === "InvalidArgument" + +/** @internal */ +export const isInvalidValue = ( + self: ValidationError.ValidationError +): self is ValidationError.InvalidValue => self._tag === "InvalidValue" + +/** @internal */ +export const isMultipleValuesDetected = ( + self: ValidationError.ValidationError +): self is ValidationError.MultipleValuesDetected => self._tag === "MultipleValuesDetected" + +/** @internal */ +export const isMissingFlag = ( + self: ValidationError.ValidationError +): self is ValidationError.MissingFlag => self._tag === "MissingFlag" + +/** @internal */ +export const isMissingValue = ( + self: ValidationError.ValidationError +): self is ValidationError.MissingValue => self._tag === "MissingValue" + +/** @internal */ +export const isMissingSubcommand = ( + self: ValidationError.ValidationError +): self is ValidationError.MissingSubcommand => self._tag === "MissingSubcommand" + +/** @internal */ +export const isNoBuiltInMatch = ( + self: ValidationError.ValidationError +): self is ValidationError.NoBuiltInMatch => self._tag === "NoBuiltInMatch" + +/** @internal */ +export const isUnclusteredFlag = ( + self: ValidationError.ValidationError +): self is ValidationError.UnclusteredFlag => self._tag === "UnclusteredFlag" + +/** @internal */ +export const commandMismatch = (error: HelpDoc.HelpDoc): ValidationError.ValidationError => { + const op = Object.create(proto) + op._tag = "CommandMismatch" + op.error = error + return op +} + +/** @internal */ +export const correctedFlag = (error: HelpDoc.HelpDoc): ValidationError.ValidationError => { + const op = Object.create(proto) + op._tag = "CorrectedFlag" + op.error = error + return op +} + +/** @internal */ +export const invalidArgument = (error: HelpDoc.HelpDoc): ValidationError.ValidationError => { + const op = Object.create(proto) + op._tag = "InvalidArgument" + op.error = error + return op +} + +/** @internal */ +export const invalidValue = (error: HelpDoc.HelpDoc): ValidationError.ValidationError => { + const op = Object.create(proto) + op._tag = "InvalidValue" + op.error = error + return op +} + +/** @internal */ +export const missingFlag = (error: HelpDoc.HelpDoc): ValidationError.ValidationError => { + const op = Object.create(proto) + op._tag = "MissingFlag" + op.error = error + return op +} + +/** @internal */ +export const missingValue = (error: HelpDoc.HelpDoc): ValidationError.ValidationError => { + const op = Object.create(proto) + op._tag = "MissingValue" + op.error = error + return op +} + +/** @internal */ +export const missingSubcommand = (error: HelpDoc.HelpDoc): ValidationError.ValidationError => { + const op = Object.create(proto) + op._tag = "MissingSubcommand" + op.error = error + return op +} + +/** @internal */ +export const multipleValuesDetected = ( + error: HelpDoc.HelpDoc, + values: ReadonlyArray +): ValidationError.ValidationError => { + const op = Object.create(proto) + op._tag = "MultipleValuesDetected" + op.error = error + op.values = values + return op +} + +/** @internal */ +export const noBuiltInMatch = (error: HelpDoc.HelpDoc): ValidationError.ValidationError => { + const op = Object.create(proto) + op._tag = "NoBuiltInMatch" + op.error = error + return op +} + +/** @internal */ +export const unclusteredFlag = ( + error: HelpDoc.HelpDoc, + unclustered: ReadonlyArray, + rest: ReadonlyArray +): ValidationError.ValidationError => { + const op = Object.create(proto) + op._tag = "UnclusteredFlag" + op.error = error + op.unclustered = unclustered + op.rest = rest + return op +} diff --git a/repos/effect/packages/cli/test/Args.test.ts b/repos/effect/packages/cli/test/Args.test.ts new file mode 100644 index 0000000..d3459f2 --- /dev/null +++ b/repos/effect/packages/cli/test/Args.test.ts @@ -0,0 +1,174 @@ +import * as Args from "@effect/cli/Args" +import * as CliConfig from "@effect/cli/CliConfig" +import * as HelpDoc from "@effect/cli/HelpDoc" +import * as ValidationError from "@effect/cli/ValidationError" +import { FileSystem, Path } from "@effect/platform" +import { NodeContext } from "@effect/platform-node" +import { describe, expect, it } from "@effect/vitest" +import { pipe } from "effect" +import * as Array from "effect/Array" +import * as Effect from "effect/Effect" +import * as Option from "effect/Option" +import * as Schema from "effect/Schema" + +const runEffect = ( + self: Effect.Effect +): Promise => Effect.provide(self, NodeContext.layer).pipe(Effect.runPromise) + +describe("Args", () => { + it("validates an valid argument with a default", () => + Effect.gen(function*() { + const args = Args.integer().pipe(Args.withDefault(0)) + const result = yield* Args.validate(args, Array.empty(), CliConfig.defaultConfig) + expect(result).toEqual([Array.empty(), 0]) + }).pipe(runEffect)) + + it("validates an valid optional argument", () => + Effect.gen(function*() { + const args = Args.integer().pipe(Args.optional) + let result = yield* Args.validate(args, Array.empty(), CliConfig.defaultConfig) + expect(result).toEqual([Array.empty(), Option.none()]) + + result = yield* Args.validate(args, ["123"], CliConfig.defaultConfig) + expect(result).toEqual([Array.empty(), Option.some(123)]) + }).pipe(runEffect)) + + it("does not validate an invalid argument even when there is a default", () => + Effect.gen(function*() { + const args = Args.integer().pipe(Args.withDefault(0)) + const result = yield* Effect.flip( + Args.validate(args, Array.of("abc"), CliConfig.defaultConfig) + ) + expect(result).toEqual(ValidationError.invalidArgument(HelpDoc.p("'abc' is not a integer"))) + }).pipe(runEffect)) + + it("should validate an existing file that is expected to exist", () => + Effect.gen(function*() { + const path = yield* Path.Path + const filePath = path.join(__dirname, "Args.test.ts") + const args = Args.file({ name: "files", exists: "yes" }).pipe(Args.repeated) + const result = yield* Args.validate(args, Array.of(filePath), CliConfig.defaultConfig) + expect(result).toEqual([Array.empty(), Array.of(filePath)]) + }).pipe(runEffect)) + + it("should return an error when a file that is expected to exist is not found", () => + Effect.gen(function*() { + const path = yield* Path.Path + const filePath = path.join(__dirname, "NotExist.test.ts") + const args = Args.file({ name: "files", exists: "yes" }).pipe(Args.repeated) + const result = yield* Effect.flip(Args.validate(args, Array.of(filePath), CliConfig.defaultConfig)) + expect(result).toEqual(ValidationError.invalidArgument(HelpDoc.p( + `Path '${filePath}' must exist` + ))) + }).pipe(runEffect)) + + it("should validate a non-existent file that is expected not to exist", () => + Effect.gen(function*() { + const path = yield* Path.Path + const filePath = path.join(__dirname, "NotExist.test.ts") + const args = Args.file({ name: "files", exists: "no" }).pipe(Args.repeated) + const result = yield* Args.validate(args, Array.of(filePath), CliConfig.defaultConfig) + expect(result).toEqual([Array.empty(), Array.of(filePath)]) + }).pipe(runEffect)) + + it("should validate a series of files", () => + Effect.gen(function*() { + const path = yield* Path.Path + const filePath = path.join(__dirname, "NotExist.test.ts") + const args = Args.file({ name: "files", exists: "no" }).pipe(Args.repeated) + const result = yield* Args.validate(args, Array.make(filePath, filePath), CliConfig.defaultConfig) + expect(result).toEqual([Array.empty(), Array.make(filePath, filePath)]) + }).pipe(runEffect)) + + it("validates an valid argument with a Schema", () => + Effect.gen(function*() { + const args = Args.integer().pipe(Args.withSchema(Schema.Positive)) + const result = yield* Args.validate(args, ["123"], CliConfig.defaultConfig) + expect(result).toEqual([Array.empty(), 123]) + }).pipe(runEffect)) + + it("does not validate an invalid argument with a Schema", () => + Effect.gen(function*() { + const args = Args.integer().pipe(Args.withSchema(Schema.Positive)) + const result = yield* Effect.flip( + Args.validate(args, Array.of("-123"), CliConfig.defaultConfig) + ) + expect(result).toEqual(ValidationError.invalidArgument(HelpDoc.p( + "Positive\n" + + "└─ Predicate refinement failure\n" + + " └─ Expected a positive number, actual -123" + ))) + }).pipe(runEffect)) + + it("fileContent", () => + Effect.gen(function*() { + const fs = yield* FileSystem.FileSystem + const path = yield* Path.Path + const filePath = path.join(__dirname, "fixtures/config.json") + const content = yield* fs.readFile(filePath) + const args = Args.fileContent({ name: "files" }).pipe(Args.repeated) + const result = yield* Args.validate(args, Array.of(filePath), CliConfig.defaultConfig) + expect(result).toEqual([Array.empty(), Array.of([filePath, content])]) + }).pipe(runEffect)) + + it("fileText", () => + Effect.gen(function*() { + const fs = yield* FileSystem.FileSystem + const path = yield* Path.Path + const filePath = path.join(__dirname, "fixtures/config.json") + const content = yield* fs.readFileString(filePath) + const args = Args.fileText({ name: "files" }).pipe(Args.repeated) + const result = yield* Args.validate(args, Array.of(filePath), CliConfig.defaultConfig) + expect(result).toEqual([Array.empty(), Array.of([filePath, content])]) + }).pipe(runEffect)) + + it("fileParse", () => + Effect.gen(function*() { + const fs = yield* FileSystem.FileSystem + const path = yield* Path.Path + const filePath = path.join(__dirname, "fixtures/config.json") + const content = yield* pipe(fs.readFileString(filePath), Effect.map(JSON.parse)) + const args = Args.fileParse({ name: "files" }).pipe(Args.repeated) + const result = yield* Args.validate(args, Array.of(filePath), CliConfig.defaultConfig) + expect(result).toEqual([Array.empty(), Array.of(content)]) + }).pipe(runEffect)) + + it("fileSchema", () => + Effect.gen(function*() { + const fs = yield* FileSystem.FileSystem + const path = yield* Path.Path + const filePath = path.join(__dirname, "fixtures/config.json") + const content = yield* pipe(fs.readFileString(filePath), Effect.map(JSON.parse)) + const args = Args.fileSchema( + Schema.Struct({ + foo: Schema.Boolean, + bar: Schema.Literal("baz") + }), + { name: "files" } + ).pipe(Args.repeated) + const result = yield* Args.validate(args, Array.of(filePath), CliConfig.defaultConfig) + expect(result).toEqual([Array.empty(), Array.of(content)]) + }).pipe(runEffect)) + + it("displays default value in help when default wrapped in Option.Some (primitive)", () => + Effect.gen(function*() { + const option = Args.withDefault(Args.integer({ name: "value" }), Option.some(123)) + const helpDoc = Args.getHelp(option) + yield* Effect.promise(() => expect(helpDoc).toMatchFileSnapshot("./snapshots/help-output/args-default-primitive")) + }).pipe(runEffect)) + + it("displays default value in help when default wrapped in Option.Some (object)", () => + Effect.gen(function*() { + const defaultObject = { key: "value", number: 456 } + const option = Args.withDefault(Args.text({ name: "config" }), Option.some(defaultObject)) + const helpDoc = Args.getHelp(option) + yield* Effect.promise(() => expect(helpDoc).toMatchFileSnapshot("./snapshots/help-output/args-default-object")) + }).pipe(runEffect)) + + it("displays no default value in help when default is not Option.Some", () => + Effect.gen(function*() { + const option = Args.withDefault(Args.text({ name: "name" }), Option.none()) + const helpDoc = Args.getHelp(option) + yield* Effect.promise(() => expect(helpDoc).toMatchFileSnapshot("./snapshots/help-output/args-no-default")) + }).pipe(runEffect)) +}) diff --git a/repos/effect/packages/cli/test/AutoCorrect.test.ts b/repos/effect/packages/cli/test/AutoCorrect.test.ts new file mode 100644 index 0000000..27afdd7 --- /dev/null +++ b/repos/effect/packages/cli/test/AutoCorrect.test.ts @@ -0,0 +1,29 @@ +import * as AutoCorrect from "@effect/cli/AutoCorrect" +import * as CliConfig from "@effect/cli/CliConfig" +import { describe, expect, it } from "@effect/vitest" + +describe("AutoCorrect", () => { + it("should calculate the correct Levenstein distance between two strings", () => { + expect(AutoCorrect.levensteinDistance("", "", CliConfig.defaultConfig)).toBe(0) + expect(AutoCorrect.levensteinDistance("--force", "", CliConfig.defaultConfig)).toBe(7) + expect(AutoCorrect.levensteinDistance("", "--force", CliConfig.defaultConfig)).toBe(7) + expect(AutoCorrect.levensteinDistance("--force", "force", CliConfig.defaultConfig)).toBe(2) + expect(AutoCorrect.levensteinDistance("--force", "--forc", CliConfig.defaultConfig)).toBe(1) + expect(AutoCorrect.levensteinDistance("foo", "bar", CliConfig.defaultConfig)).toBe(3) + // By default, the configuration is case-insensitive so options are normalized + expect(AutoCorrect.levensteinDistance("--force", "--Force", CliConfig.defaultConfig)).toBe(0) + }) + + it("should take into account the provided case-sensitivity", () => { + const config = CliConfig.make({ isCaseSensitive: true }) + expect(AutoCorrect.levensteinDistance("--force", "--force", config)).toBe(0) + expect(AutoCorrect.levensteinDistance("--FORCE", "--force", config)).toBe(5) + }) + + it("should calculate the correct Levenstein distance for non-ASCII characters", () => { + expect(AutoCorrect.levensteinDistance("とんかつ", "とかつ", CliConfig.defaultConfig)).toBe(1) + expect(AutoCorrect.levensteinDistance("¯\\_(ツ)_/¯", "_(ツ)_/¯", CliConfig.defaultConfig)).toBe( + 2 + ) + }) +}) diff --git a/repos/effect/packages/cli/test/CliApp.test.ts b/repos/effect/packages/cli/test/CliApp.test.ts new file mode 100644 index 0000000..432d0ea --- /dev/null +++ b/repos/effect/packages/cli/test/CliApp.test.ts @@ -0,0 +1,167 @@ +import * as Args from "@effect/cli/Args" +import type * as CliApp from "@effect/cli/CliApp" +import * as CliConfig from "@effect/cli/CliConfig" +import * as Command from "@effect/cli/Command" +import * as HelpDoc from "@effect/cli/HelpDoc" +import * as ValidationError from "@effect/cli/ValidationError" +import { NodeContext } from "@effect/platform-node" +import { describe, expect, it } from "@effect/vitest" +import { Array, Console, Effect, FiberRef, Layer, LogLevel } from "effect" +import * as MockConsole from "./services/MockConsole.js" + +const MainLive = Effect.gen(function*() { + const console = yield* MockConsole.make + return Layer.mergeAll( + Console.setConsole(console), + NodeContext.layer + ) +}).pipe(Layer.unwrapEffect) + +const runEffect = ( + self: Effect.Effect +): Promise => + Effect.provide(self, MainLive).pipe( + Effect.runPromise + ) + +describe("CliApp", () => { + it("should return an error if excess arguments are provided", () => + Effect.gen(function*() { + const cli = Command.run(Command.make("foo"), { + name: "Test", + version: "1.0.0" + }) + const args = Array.make("node", "test.js", "--bar") + const result = yield* Effect.flip(cli(args)) + expect(result).toEqual(ValidationError.invalidValue(HelpDoc.p( + "Received unknown argument: '--bar'" + ))) + }).pipe(runEffect)) + + describe("Built-In Options Processing", () => { + it("should display built-in options in help if `CliConfig.showBuiltIns` is true", () => { + const CliConfigLive = CliConfig.layer({ + showBuiltIns: true // this is the default + }) + return Effect.gen(function*() { + const cli = Command.run(Command.make("foo"), { + name: "Test", + version: "1.0.0" + }) + yield* cli([]) + const lines = yield* MockConsole.getLines() + const output = lines.join("\n") + expect(output).toContain("--completions sh | bash | fish | zsh") + expect(output).toContain("(-h, --help)") + expect(output).toContain("--wizard") + expect(output).toContain("--version") + }).pipe( + Effect.provide(Layer.mergeAll(MainLive, CliConfigLive)), + Effect.runPromise + ) + }) + + it("should not display built-in options in help if `CliConfig.showBuiltIns` is false", () => { + const CliConfigLive = CliConfig.layer({ + showBuiltIns: false + }) + return Effect.gen(function*() { + const cli = Command.run(Command.make("foo"), { + name: "Test", + version: "1.0.0" + }) + yield* cli([]) + const lines = yield* MockConsole.getLines() + const output = lines.join("\n") + expect(output).not.toContain("--completions sh | bash | fish | zsh") + expect(output).not.toContain("(-h, --help)") + expect(output).not.toContain("--wizard") + expect(output).not.toContain("--version") + }).pipe( + Effect.provide(Layer.mergeAll(MainLive, CliConfigLive)), + Effect.runPromise + ) + }) + + it("should set the minimum log level for a command", () => + Effect.gen(function*() { + let logLevel: LogLevel.LogLevel | undefined = undefined + const logging = Command.make("logging").pipe(Command.withHandler(() => + Effect.gen(function*() { + logLevel = yield* FiberRef.get(FiberRef.currentMinimumLogLevel) + }) + )) + const cli = Command.run(logging, { + name: "Test", + version: "1.0.0" + }) + yield* cli(["node", "logging.js", "--log-level", "debug"]) + expect(logLevel).toEqual(LogLevel.Debug) + }).pipe(runEffect)) + + it("should set the minimum log level when using equals syntax (--log-level=...)", () => + Effect.gen(function*() { + let logLevel: LogLevel.LogLevel | undefined = undefined + const logging = Command.make("logging").pipe(Command.withHandler(() => + Effect.gen(function*() { + logLevel = yield* FiberRef.get(FiberRef.currentMinimumLogLevel) + }) + )) + const cli = Command.run(logging, { + name: "Test", + version: "1.0.0" + }) + yield* cli(["node", "logging.js", "--log-level=debug"]) + expect(logLevel).toEqual(LogLevel.Debug) + }).pipe(runEffect)) + + it("should handle paths with spaces when using --log-level", () => + Effect.gen(function*() { + let executedValue: string | undefined = undefined + const cmd = Command.make("test", { value: Args.text() }, ({ value }) => + Effect.sync(() => { + executedValue = value + })) + const cli = Command.run(cmd, { + name: "Test", + version: "1.0.0" + }) + // Simulate Windows path with spaces (e.g., "C:\Program Files\nodejs\node.exe") + yield* cli(["C:\\Program Files\\node.exe", "C:\\My Scripts\\test.js", "--log-level", "info", "hello"]) + expect(executedValue).toEqual("hello") + }).pipe(runEffect)) + + it("should not swallow the next argument when using --log-level=value equals syntax", () => + Effect.gen(function*() { + let executedValue: string | undefined = undefined + const cmd = Command.make("test", { value: Args.text() }, ({ value }) => + Effect.sync(() => { + executedValue = value + })) + const cli = Command.run(cmd, { + name: "Test", + version: "1.0.0" + }) + yield* cli(["node", "test.js", "--log-level=debug", "hello"]) + expect(executedValue).toEqual("hello") + }).pipe(runEffect)) + + it("should set log level and preserve argument with --log-level=value combined", () => + Effect.gen(function*() { + let logLevel: LogLevel.LogLevel | undefined = undefined + let executedValue: string | undefined = undefined + const cmd = Command.make("test", { value: Args.text() }, ({ value }) => + Effect.gen(function*() { + logLevel = yield* FiberRef.get(FiberRef.currentMinimumLogLevel) + executedValue = value + })) + const cli = Command.run(cmd, { + name: "Test", + version: "1.0.0" + }) + yield* cli(["node", "test.js", "--log-level=info", "hello"]) + expect(logLevel).toEqual(LogLevel.Info) + expect(executedValue).toEqual("hello") + }).pipe(runEffect)) + }) +}) diff --git a/repos/effect/packages/cli/test/Command.test.ts b/repos/effect/packages/cli/test/Command.test.ts new file mode 100644 index 0000000..8466fc3 --- /dev/null +++ b/repos/effect/packages/cli/test/Command.test.ts @@ -0,0 +1,171 @@ +import { Args, Command, Options } from "@effect/cli" +import { NodeContext } from "@effect/platform-node" +import { assert, describe, it } from "@effect/vitest" +import { Config, ConfigProvider, Context, Effect, Layer } from "effect" + +const git = Command.make("git", { + verbose: Options.boolean("verbose").pipe( + Options.withAlias("v"), + Options.withFallbackConfig(Config.boolean("VERBOSE")) + ) +}).pipe( + Command.withDescription("the stupid content tracker"), + Command.provideEffectDiscard(() => + Effect.flatMap( + Messages, + (_) => _.log("shared") + ) + ) +) + +const clone = Command.make("clone", { + repository: Args.text({ name: "repository" }).pipe( + Args.withFallbackConfig(Config.string("REPOSITORY")) + ) +}, ({ repository }) => + Effect.gen(function*() { + const { log } = yield* Messages + const { verbose } = yield* git + if (verbose) { + yield* log(`Cloning ${repository}`) + } else { + yield* log("Cloning") + } + })).pipe(Command.withDescription("Clone a repository into a new directory")) + +const AddService = Context.GenericTag<"AddService">("AddService") + +const add = Command.make("add", { + pathspec: Args.text({ name: "pathspec" }) +}).pipe( + Command.withHandler(({ pathspec }) => + Effect.gen(function*() { + yield* AddService + const { log } = yield* Messages + const { verbose } = yield* git + if (verbose) { + yield* log(`Adding ${pathspec}`) + } else { + yield* log(`Adding`) + } + }) + ), + Command.withDescription("Add file contents to the index"), + Command.provideEffect(AddService, (_) => Effect.succeed("AddService" as const)) +) + +const run = git.pipe( + Command.withSubcommands([clone, add]), + Command.run({ + name: "git", + version: "1.0.0" + }) +) + +describe("Command", () => { + describe("git", () => { + it("no sub-command", () => + Effect.gen(function*() { + const messages = yield* Messages + yield* run(["--verbose"]) + yield* run([]) + assert.deepStrictEqual(yield* messages.messages, ["shared", "shared"]) + }).pipe(Effect.provide(EnvLive), Effect.runPromise)) + + it("add", () => + Effect.gen(function*() { + const messages = yield* Messages + yield* run(["node", "git.js", "add", "file"]) + yield* run(["node", "git.js", "--verbose", "add", "file"]) + assert.deepStrictEqual(yield* messages.messages, [ + "shared", + "Adding", + "shared", + "Adding file" + ]) + }).pipe(Effect.provide(EnvLive), Effect.runPromise)) + + it("clone", () => + Effect.gen(function*() { + const messages = yield* Messages + yield* run(["node", "git.js", "clone", "repo"]) + yield* run(["node", "git.js", "--verbose", "clone", "repo"]) + assert.deepStrictEqual(yield* messages.messages, [ + "shared", + "Cloning", + "shared", + "Cloning repo" + ]) + }).pipe(Effect.provide(EnvLive), Effect.runPromise)) + + it("withFallbackConfig Options boolean", () => + Effect.gen(function*() { + const messages = yield* Messages + yield* run(["node", "git.js", "clone", "repo"]) + assert.deepStrictEqual(yield* messages.messages, [ + "shared", + "Cloning repo" + ]) + }).pipe( + Effect.withConfigProvider(ConfigProvider.fromMap( + new Map([["VERBOSE", "true"]]) + )), + Effect.provide(EnvLive), + Effect.runPromise + )) + + it("withFallbackConfig Args", () => + Effect.gen(function*() { + const messages = yield* Messages + yield* run(["node", "git.js", "clone"]) + assert.deepStrictEqual(yield* messages.messages, [ + "shared", + "Cloning repo" + ]) + }).pipe( + Effect.withConfigProvider(ConfigProvider.fromMap( + new Map([["VERBOSE", "true"], ["REPOSITORY", "repo"]]) + )), + Effect.provide(EnvLive), + Effect.runPromise + )) + + it("options after positional args", () => + Effect.gen(function*() { + const messages = yield* Messages + // --verbose after the positional arg "repo" + yield* run(["node", "git.js", "clone", "repo", "--verbose"]) + assert.deepStrictEqual(yield* messages.messages, [ + "shared", + "Cloning repo" + ]) + }).pipe(Effect.provide(EnvLive), Effect.runPromise)) + + it("options after positional args with alias", () => + Effect.gen(function*() { + const messages = yield* Messages + // -v after the positional arg "repo" + yield* run(["node", "git.js", "clone", "repo", "-v"]) + assert.deepStrictEqual(yield* messages.messages, [ + "shared", + "Cloning repo" + ]) + }).pipe(Effect.provide(EnvLive), Effect.runPromise)) + }) +}) + +// -- + +interface Messages { + readonly log: (message: string) => Effect.Effect + readonly messages: Effect.Effect> +} +const Messages = Context.GenericTag("Messages") +const MessagesLive = Layer.sync(Messages, () => { + const messages: Array = [] + return Messages.of({ + log: (message) => Effect.sync(() => messages.push(message)), + messages: Effect.sync(() => messages) + }) +}) +const EnvLive = Layer.mergeAll(MessagesLive, NodeContext.layer) diff --git a/repos/effect/packages/cli/test/CommandDescriptor.test.ts b/repos/effect/packages/cli/test/CommandDescriptor.test.ts new file mode 100644 index 0000000..aee8946 --- /dev/null +++ b/repos/effect/packages/cli/test/CommandDescriptor.test.ts @@ -0,0 +1,464 @@ +import * as Args from "@effect/cli/Args" +import * as BuiltInOptions from "@effect/cli/BuiltInOptions" +import * as CliConfig from "@effect/cli/CliConfig" +import * as Descriptor from "@effect/cli/CommandDescriptor" +import * as CommandDirective from "@effect/cli/CommandDirective" +import * as HelpDoc from "@effect/cli/HelpDoc" +import * as Options from "@effect/cli/Options" +import * as ValidationError from "@effect/cli/ValidationError" +import * as NodeContext from "@effect/platform-node/NodeContext" +import * as Doc from "@effect/printer/Doc" +import { describe, expect, it } from "@effect/vitest" +import { Array, Effect, Option, pipe, String } from "effect" +import * as Grep from "./utils/grep.js" +import * as Tail from "./utils/tail.js" +import * as WordCount from "./utils/wc.js" + +const runEffect = ( + self: Effect.Effect +): Promise => + Effect.provide(self, NodeContext.layer).pipe( + Effect.runPromise + ) + +describe("Command", () => { + describe("Standard Commands", () => { + it("should validate a command with options followed by arguments", () => + Effect.gen(function*() { + const args1 = Array.make("tail", "-n", "100", "foo.log") + const args2 = Array.make("grep", "--after", "2", "--before", "3", "fooBar") + const result1 = yield* Descriptor.parse(Tail.command, args1, CliConfig.defaultConfig) + const result2 = yield* Descriptor.parse(Grep.command, args2, CliConfig.defaultConfig) + const expected1 = { name: "tail", options: 100, args: "foo.log" } + const expected2 = { name: "grep", options: [2, 3], args: "fooBar" } + expect(result1).toEqual(CommandDirective.userDefined(Array.empty(), expected1)) + expect(result2).toEqual(CommandDirective.userDefined(Array.empty(), expected2)) + }).pipe(runEffect)) + + it("should provide auto-correct suggestions for misspelled options", () => + Effect.gen(function*() { + const args1 = Array.make("grep", "--afte", "2", "--before", "3", "fooBar") + const args2 = Array.make("grep", "--after", "2", "--efore", "3", "fooBar") + const args3 = Array.make("grep", "--afte", "2", "--efore", "3", "fooBar") + const result1 = yield* Effect.flip(Descriptor.parse(Grep.command, args1, CliConfig.defaultConfig)) + const result2 = yield* Effect.flip(Descriptor.parse(Grep.command, args2, CliConfig.defaultConfig)) + const result3 = yield* Effect.flip(Descriptor.parse(Grep.command, args3, CliConfig.defaultConfig)) + expect(result1).toEqual(ValidationError.correctedFlag(HelpDoc.p( + "The flag '--afte' is not recognized. Did you mean '--after'?" + ))) + expect(result2).toEqual(ValidationError.correctedFlag(HelpDoc.p( + "The flag '--efore' is not recognized. Did you mean '--before'?" + ))) + expect(result3).toEqual(ValidationError.correctedFlag(HelpDoc.p( + "The flag '--afte' is not recognized. Did you mean '--after'?" + ))) + }).pipe(runEffect)) + + it("should return an error if an option is missing", () => + Effect.gen(function*() { + const args = Array.make("grep", "--a", "2", "--before", "3", "fooBar") + const result = yield* Effect.flip(Descriptor.parse(Grep.command, args, CliConfig.defaultConfig)) + expect(result).toEqual(ValidationError.missingValue(HelpDoc.sequence( + HelpDoc.p("Expected to find option: '--after'"), + HelpDoc.p("Expected to find option: '--before'") + ))) + }).pipe(runEffect)) + }) + + describe("Commands with Clustered Options", () => { + it("should treat clustered boolean options as un-clustered options", () => + Effect.gen(function*() { + const args1 = Array.make("wc", "-clw", "filename") + const args2 = Array.make("wc", "-c", "-l", "-w", "filename") + const result1 = yield* Descriptor.parse(WordCount.command, args1, CliConfig.defaultConfig) + const result2 = yield* Descriptor.parse(WordCount.command, args2, CliConfig.defaultConfig) + const expected = { name: "wc", options: [true, true, true, true], args: ["filename"] } + expect(result1).toEqual(CommandDirective.userDefined(Array.empty(), expected)) + expect(result2).toEqual(CommandDirective.userDefined(Array.empty(), expected)) + }).pipe(runEffect)) + + it("should not uncluster wrong clusters", () => + Effect.gen(function*() { + const args = Array.make("wc", "-clk") + const result = yield* Descriptor.parse(WordCount.command, args, CliConfig.defaultConfig) + const expected = { name: "wc", options: [false, false, false, true], args: ["-clk"] } + expect(result).toEqual(CommandDirective.userDefined(Array.empty(), expected)) + }).pipe(runEffect)) + + it("should not alter '-'", () => + Effect.gen(function*() { + const args = Array.make("wc", "-") + const result = yield* Descriptor.parse(WordCount.command, args, CliConfig.defaultConfig) + const expected = { name: "wc", options: [false, false, false, true], args: ["-"] } + expect(result).toEqual(CommandDirective.userDefined(Array.empty(), expected)) + }).pipe(runEffect)) + }) + + describe("Subcommands without Options or Arguments", () => { + const options = Options.boolean("verbose").pipe(Options.withAlias("v")) + + const git = Descriptor.make("git", options).pipe(Descriptor.withSubcommands([ + ["remote", Descriptor.make("remote")], + ["log", Descriptor.make("log")] + ])) + + it("should match the top-level command if no subcommands are specified", () => + Effect.gen(function*() { + const args = Array.make("git", "-v") + const result = yield* Descriptor.parse(git, args, CliConfig.defaultConfig) + const expected = { name: "git", options: true, args: void 0, subcommand: Option.none() } + expect(result).toEqual(CommandDirective.userDefined([], expected)) + }).pipe(runEffect)) + + it("should match the first subcommand without any surplus arguments", () => + Effect.gen(function*() { + const args = Array.make("git", "remote") + const result = yield* Descriptor.parse(git, args, CliConfig.defaultConfig) + const expected = { + name: "git", + options: false, + args: void 0, + subcommand: Option.some(["remote", { name: "remote", options: void 0, args: void 0 }]) + } + expect(result).toEqual(CommandDirective.userDefined(Array.empty(), expected)) + }).pipe(runEffect)) + + it("matches the first subcommand with parent option after subcommand", () => + Effect.gen(function*() { + const args = Array.make("git", "remote", "-v") + const result = yield* Descriptor.parse(git, args, CliConfig.defaultConfig) + // -v is recognized as git's verbose option, even after the subcommand + const expected = { + name: "git", + options: true, + args: void 0, + subcommand: Option.some(["remote", { name: "remote", options: void 0, args: void 0 }]) + } + expect(result).toEqual(CommandDirective.userDefined(Array.empty(), expected)) + }).pipe(runEffect)) + + it("matches the second subcommand without any surplus arguments", () => + Effect.gen(function*() { + const args = Array.make("git", "log") + const result = yield* Descriptor.parse(git, args, CliConfig.defaultConfig) + const expected = { + name: "git", + options: false, + args: void 0, + subcommand: Option.some(["log", { name: "log", options: void 0, args: void 0 }]) + } + expect(result).toEqual(CommandDirective.userDefined(Array.empty(), expected)) + }).pipe(runEffect)) + + it("should return an error message for an unknown subcommand", () => + Effect.gen(function*() { + const args = Array.make("git", "abc") + const result = yield* Effect.flip(Descriptor.parse(git, args, CliConfig.defaultConfig)) + expect(result).toEqual(ValidationError.commandMismatch(HelpDoc.p( + "Invalid subcommand for git - use one of 'remote', 'log'" + ))) + }).pipe(runEffect)) + }) + + describe("Subcommands with Options and Arguments", () => { + const options = Options.all([ + Options.boolean("i"), + Options.text("empty").pipe(Options.withDefault("drop")) + ]) + + const args = Args.all([Args.text(), Args.text()]) + + const git = Descriptor.make("git").pipe(Descriptor.withSubcommands([ + ["rebase", Descriptor.make("rebase", options, args)] + ])) + + it("should parse a subcommand with required options and arguments", () => + Effect.gen(function*() { + const args = Array.make("git", "rebase", "-i", "upstream", "branch") + const result = yield* Descriptor.parse(git, args, CliConfig.defaultConfig) + const expected = { + name: "git", + options: void 0, + args: void 0, + subcommand: Option.some(["rebase", { + name: "rebase", + options: [true, "drop"], + args: ["upstream", "branch"] + }]) + } + expect(result).toEqual(CommandDirective.userDefined(Array.empty(), expected)) + }).pipe(runEffect)) + + it("should parse a subcommand with required and optional options and arguments", () => + Effect.gen(function*() { + const args = Array.make( + "git", + "rebase", + "-i", + "--empty", + "ask", + "upstream", + "branch" + ) + const result = yield* Descriptor.parse(git, args, CliConfig.defaultConfig) + const expected = { + name: "git", + options: void 0, + args: void 0, + subcommand: Option.some(["rebase", { + name: "rebase", + options: [true, "ask"], + args: ["upstream", "branch"] + }]) + } + expect(result).toEqual(CommandDirective.userDefined(Array.empty(), expected)) + }).pipe(runEffect)) + }) + + describe("Nested Subcommands", () => { + const command = Descriptor.make("command").pipe(Descriptor.withSubcommands([ + [ + "sub", + Descriptor.make("sub").pipe(Descriptor.withSubcommands([ + ["subsub", Descriptor.make("subsub", Options.boolean("i"), Args.text())] + ])) + ] + ])) + + it("should properly parse deeply nested subcommands with options and arguments", () => + Effect.gen(function*() { + const args = Array.make("command", "sub", "subsub", "-i", "text") + const result = yield* Descriptor.parse(command, args, CliConfig.defaultConfig) + const expected = { + name: "command", + options: void 0, + args: void 0, + subcommand: Option.some(["sub", { + name: "sub", + options: void 0, + args: void 0, + subcommand: Option.some(["subsub", { + name: "subsub", + options: true, + args: "text" + }]) + }]) + } + expect(result).toEqual(CommandDirective.userDefined(Array.empty(), expected)) + }).pipe(runEffect)) + }) + + describe("Help Documentation", () => { + it("should allow adding help documentation to a command", () => + Effect.gen(function*() { + const config = CliConfig.make({ showBuiltIns: false }) + const cmd = Descriptor.make("tldr").pipe(Descriptor.withDescription("this is some help")) + const args = Array.of("tldr") + const result = yield* Descriptor.parse(cmd, args, CliConfig.defaultConfig) + const expectedValue = { name: "tldr", options: void 0, args: void 0 } + const expectedDoc = HelpDoc.sequence( + HelpDoc.h1("DESCRIPTION"), + HelpDoc.p("this is some help") + ) + expect(result).toEqual(CommandDirective.userDefined(Array.empty(), expectedValue)) + expect(Descriptor.getHelp(cmd, config)).toEqual(expectedDoc) + }).pipe(runEffect)) + + it("should allow adding help documentation to subcommands", () => { + const config = CliConfig.make({ showBuiltIns: false }) + const cmd = Descriptor.make("command").pipe(Descriptor.withSubcommands([ + ["sub", Descriptor.make("sub").pipe(Descriptor.withDescription("this is some help"))] + ])) + const expected = HelpDoc.sequence(HelpDoc.h1("DESCRIPTION"), HelpDoc.p("this is some help")) + expect(Descriptor.getHelp(cmd, config)).not.toEqual(expected) + }) + + it("should correctly display help documentation for a command", () => { + const config = CliConfig.make({ showBuiltIns: false }) + const child2 = Descriptor.make("child2").pipe( + Descriptor.withDescription("help 2") + ) + const child1 = Descriptor.make("child1").pipe( + Descriptor.withSubcommands([["child2", child2]]), + Descriptor.withDescription("help 1") + ) + const parent = Descriptor.make("parent").pipe( + Descriptor.withSubcommands([["child1", child1]]) + ) + const result = Doc.render( + Doc.unAnnotate(HelpDoc.toAnsiDoc(Descriptor.getHelp(parent, config))), + { style: "pretty" } + ) + expect(result).toBe(String.stripMargin( + `|COMMANDS + | + | - child1 help 1 + | + | - child1 child2 help 2 + |` + )) + }) + }) + + describe("Built-In Options Processing", () => { + const command = Descriptor.make("command", Options.text("a")) + const params1 = Array.make("command", "--help") + const params2 = Array.make("command", "-h") + const params3 = Array.make("command", "--wizard") + const params4 = Array.make("command", "--completions", "sh") + const params5 = Array.make("command", "-a", "--help") + const params6 = Array.make("command", "--help", "--wizard", "-b") + const params7 = Array.make("command", "-hdf", "--help") + const params8 = Array.make("command", "-af", "asdgf", "--wizard") + + const directiveType = (directive: CommandDirective.CommandDirective): string => { + if (CommandDirective.isBuiltIn(directive)) { + if (BuiltInOptions.isShowHelp(directive.option)) { + return "help" + } + if (BuiltInOptions.isShowWizard(directive.option)) { + return "wizard" + } + if (BuiltInOptions.isShowCompletions(directive.option)) { + return "completions" + } + } + return "user" + } + + it("should trigger built-in options if they are alone", () => + Effect.gen(function*() { + const result1 = yield* pipe( + Descriptor.parse(command, params1, CliConfig.defaultConfig), + Effect.map(directiveType) + ) + const result2 = yield* pipe( + Descriptor.parse(command, params2, CliConfig.defaultConfig), + Effect.map(directiveType) + ) + const result3 = yield* pipe( + Descriptor.parse(command, params3, CliConfig.defaultConfig), + Effect.map(directiveType) + ) + const result4 = yield* pipe( + Descriptor.parse(command, params4, CliConfig.defaultConfig), + Effect.map(directiveType) + ) + expect(result1).toBe("help") + expect(result2).toBe("help") + expect(result3).toBe("wizard") + expect(result4).toBe("completions") + }).pipe(runEffect)) + + it("should not trigger help if an option matches", () => + Effect.gen(function*() { + const result = yield* pipe( + Descriptor.parse(command, params5, CliConfig.defaultConfig), + Effect.map(directiveType) + ) + expect(result).toBe("user") + }).pipe(runEffect)) + + it("should trigger help even if not alone", () => + Effect.gen(function*() { + const config = CliConfig.make({ finalCheckBuiltIn: true }) + const result1 = yield* pipe( + Descriptor.parse(command, params6, config), + Effect.map(directiveType) + ) + const result2 = yield* pipe( + Descriptor.parse(command, params7, config), + Effect.map(directiveType) + ) + expect(result1).toBe("help") + expect(result2).toBe("help") + }).pipe(runEffect)) + + it("should trigger wizard even if not alone", () => + Effect.gen(function*() { + const config = CliConfig.make({ finalCheckBuiltIn: true }) + const result = yield* pipe( + Descriptor.parse(command, params8, config), + Effect.map(directiveType) + ) + expect(result).toBe("wizard") + }).pipe(runEffect)) + }) + + describe("End of Command Options Symbol", () => { + const command = Descriptor.make( + "cmd", + Options.all([ + Options.optional(Options.text("something")), + Options.boolean("verbose").pipe(Options.withAlias("v")) + ]), + Args.repeated(Args.text()) + ) + + it("should properly handle the end of command options symbol", () => + Effect.gen(function*() { + const args1 = Array.make("cmd", "-v", "--something", "abc", "something") + const args2 = Array.make("cmd", "-v", "--", "--something", "abc", "something") + const args3 = Array.make("cmd", "--", "-v", "--something", "abc", "something") + const result1 = yield* Descriptor.parse(command, args1, CliConfig.defaultConfig) + const result2 = yield* Descriptor.parse(command, args2, CliConfig.defaultConfig) + const result3 = yield* Descriptor.parse(command, args3, CliConfig.defaultConfig) + const expected1 = { + name: "cmd", + options: [Option.some("abc"), true], + args: Array.of("something") + } + const expected2 = { + name: "cmd", + options: [Option.none(), true], + args: Array.make("--something", "abc", "something") + } + const expected3 = { + name: "cmd", + options: [Option.none(), false], + args: Array.make("-v", "--something", "abc", "something") + } + expect(result1).toEqual(CommandDirective.userDefined(Array.empty(), expected1)) + expect(result2).toEqual(CommandDirective.userDefined(Array.empty(), expected2)) + expect(result3).toEqual(CommandDirective.userDefined(Array.empty(), expected3)) + }).pipe(runEffect)) + }) + + describe("Completions", () => { + const command = Descriptor.make("forge").pipe(Descriptor.withSubcommands([ + [ + "cache", + Descriptor.make( + "cache", + Options.boolean("verbose").pipe( + Options.withDescription("Output in verbose mode") + ) + ).pipe( + Descriptor.withDescription("The cache command does cache things"), + Descriptor.withSubcommands([ + ["clean", Descriptor.make("clean")], + ["ls", Descriptor.make("ls")] + ]) + ) + ] + ])) + + it("should create completions for the bash shell", () => + Effect.gen(function*() { + const result = yield* Descriptor.getBashCompletions(command, "forge") + yield* Effect.promise(() => expect(result).toMatchFileSnapshot("./snapshots/bash-completions")) + }).pipe(runEffect)) + + it("should create completions for the zsh shell", () => + Effect.gen(function*() { + const result = yield* Descriptor.getZshCompletions(command, "forge") + yield* Effect.promise(() => expect(result).toMatchFileSnapshot("./snapshots/zsh-completions")) + }).pipe(runEffect)) + + it("should create completions for the fish shell", () => + Effect.gen(function*() { + const result = yield* Descriptor.getFishCompletions(command, "forge") + yield* Effect.promise(() => expect(result).toMatchFileSnapshot("./snapshots/fish-completions")) + }).pipe(runEffect)) + }) +}) diff --git a/repos/effect/packages/cli/test/ConfigFile.test.ts b/repos/effect/packages/cli/test/ConfigFile.test.ts new file mode 100644 index 0000000..3217750 --- /dev/null +++ b/repos/effect/packages/cli/test/ConfigFile.test.ts @@ -0,0 +1,39 @@ +import * as ConfigFile from "@effect/cli/ConfigFile" +import type { FileSystem } from "@effect/platform" +import { Path } from "@effect/platform" +import { NodeContext } from "@effect/platform-node" +import { assert, describe, it } from "@effect/vitest" +import * as Config from "effect/Config" +import * as Effect from "effect/Effect" + +const runEffect = ( + self: Effect.Effect +): Promise => Effect.provide(self, NodeContext.layer).pipe(Effect.runPromise) + +describe("ConfigFile", () => { + it("loads json files", () => + Effect.gen(function*() { + const path = yield* Path.Path + const result = yield* Config.all([ + Config.boolean("foo"), + Config.string("bar") + ]).pipe( + Effect.provide(ConfigFile.layer("config", { + searchPaths: [path.join(__dirname, "fixtures")], + formats: ["json"] + })) + ) + assert.deepStrictEqual(result, [true, "baz"]) + }).pipe(runEffect)) + + it("loads yaml", () => + Effect.gen(function*() { + const path = yield* Path.Path + const result = yield* Config.integer("foo").pipe( + Effect.provide(ConfigFile.layer("config-file", { + searchPaths: [path.join(__dirname, "fixtures")] + })) + ) + assert.deepStrictEqual(result, 123) + }).pipe(runEffect)) +}) diff --git a/repos/effect/packages/cli/test/Options.test.ts b/repos/effect/packages/cli/test/Options.test.ts new file mode 100644 index 0000000..e31383a --- /dev/null +++ b/repos/effect/packages/cli/test/Options.test.ts @@ -0,0 +1,788 @@ +import * as CliConfig from "@effect/cli/CliConfig" +import * as HelpDoc from "@effect/cli/HelpDoc" +import * as Options from "@effect/cli/Options" +import * as ValidationError from "@effect/cli/ValidationError" +import * as NodeContext from "@effect/platform-node/NodeContext" +import * as FileSystem from "@effect/platform/FileSystem" +import * as Path from "@effect/platform/Path" +import { assert, describe, expect, it } from "@effect/vitest" +import { BigDecimal, pipe } from "effect" +import * as Array from "effect/Array" +import * as Data from "effect/Data" +import * as Effect from "effect/Effect" +import * as Either from "effect/Either" +import { identity } from "effect/Function" +import * as HashMap from "effect/HashMap" +import * as Option from "effect/Option" +import * as Schema from "effect/Schema" + +const firstName = Options.text("firstName").pipe(Options.withAlias("f")) +const lastName = Options.text("lastName") +const age = Options.integer("age") +const balance = Options.text("balance").pipe(Options.withSchema(Schema.BigDecimal)) +const ageOptional = Options.optional(age) +const verbose = Options.boolean("verbose", { ifPresent: true }) +const defs = Options.keyValueMap("defs").pipe(Options.withAlias("d")) + +const runEffect = ( + self: Effect.Effect +): Promise => Effect.provide(self, NodeContext.layer).pipe(Effect.runPromise) + +const process = ( + options: Options.Options, + args: ReadonlyArray, + config: CliConfig.CliConfig +): Effect.Effect< + [ReadonlyArray, A], + ValidationError.ValidationError, + NodeContext.NodeContext +> => + Options.processCommandLine(options, args, config).pipe( + Effect.flatMap(([err, rest, a]) => + Option.match(err, { + onNone: () => Effect.succeed([rest, a]), + onSome: Effect.fail + }) + ) + ) + +describe("Options", () => { + it("should validate without ambiguity", () => + Effect.gen(function*() { + const args = Array.make("--firstName", "--lastName", "--lastName", "--firstName") + const result1 = yield* process(Options.all([firstName, lastName]), args, CliConfig.defaultConfig) + const result2 = yield* process(Options.all([lastName, firstName]), args, CliConfig.defaultConfig) + const expected1 = [Array.empty(), Array.make("--lastName", "--firstName")] + const expected2 = [Array.empty(), Array.make("--firstName", "--lastName")] + expect(result1).toEqual(expected1) + expect(result2).toEqual(expected2) + }).pipe(runEffect)) + + it("should not uncluster values", () => + Effect.gen(function*() { + const args = Array.make("--firstName", "-ab") + const result = yield* process(firstName, args, CliConfig.defaultConfig) + const expected = [Array.empty(), "-ab"] + expect(result).toEqual(expected) + }).pipe(runEffect)) + + it("should return a HelpDoc if an option is not an exact match and it's a short option", () => + Effect.gen(function*() { + const args = Array.make("--ag", "20") + const result = yield* Effect.flip(process(age, args, CliConfig.defaultConfig)) + expect(result).toEqual(ValidationError.missingValue(HelpDoc.p( + "Expected to find option: '--age'" + ))) + }).pipe(runEffect)) + + it("should return a HelpDoc if there is a collision between arguments", () => + Effect.gen(function*() { + const options = Options.orElse( + Options.text("a").pipe(Options.map(identity)), + Options.text("b").pipe(Options.map(identity)) + ) + const args = Array.make("-a", "a", "-b", "b") + const result = yield* Effect.flip(process(options, args, CliConfig.defaultConfig)) + expect(result).toEqual(ValidationError.invalidValue(HelpDoc.p( + "Collision between two options detected - you can only " + + "specify one of either: ['-a', '-b']" + ))) + }).pipe(runEffect)) + + it("validates a boolean option without a value", () => + Effect.gen(function*() { + const args = Array.make("--verbose") + const result = yield* process(verbose, args, CliConfig.defaultConfig) + const expected = [Array.empty(), true] + expect(result).toEqual(expected) + }).pipe(runEffect)) + + it("validates a boolean option with a followup option", () => + Effect.gen(function*() { + const options = Options.all([Options.boolean("help"), Options.boolean("v")]) + const args1 = Array.empty() + const args2 = Array.make("--help") + const args3 = Array.make("--help", "-v") + const result1 = yield* process(options, args1, CliConfig.defaultConfig) + const result2 = yield* process(options, args2, CliConfig.defaultConfig) + const result3 = yield* process(options, args3, CliConfig.defaultConfig) + const expected1 = [Array.empty(), [false, false]] + const expected2 = [Array.empty(), [true, false]] + const expected3 = [Array.empty(), [true, true]] + expect(result1).toEqual(expected1) + expect(result2).toEqual(expected2) + expect(result3).toEqual(expected3) + }).pipe(runEffect)) + + it("validates a boolean option with negation", () => + Effect.gen(function*() { + const option = Options.boolean("verbose", { aliases: ["v"], negationNames: ["silent", "s"] }) + const result1 = yield* process(option, [], CliConfig.defaultConfig) + const result2 = yield* process(option, ["--verbose"], CliConfig.defaultConfig) + const result3 = yield* process(option, ["-v"], CliConfig.defaultConfig) + const result4 = yield* process(option, ["--silent"], CliConfig.defaultConfig) + const result5 = yield* process(option, ["-s"], CliConfig.defaultConfig) + const result6 = yield* Effect.flip(process(option, ["--verbose", "--silent"], CliConfig.defaultConfig)) + const result7 = yield* Effect.flip(process(option, ["-v", "-s"], CliConfig.defaultConfig)) + expect(result1).toEqual([[], false]) + expect(result2).toEqual([[], true]) + expect(result3).toEqual([[], true]) + expect(result4).toEqual([[], false]) + expect(result5).toEqual([[], false]) + expect(result6).toEqual(ValidationError.invalidValue(HelpDoc.p( + "Collision between two options detected - " + + "you can only specify one of either: ['--verbose', '--silent']" + ))) + expect(result7).toEqual(ValidationError.invalidValue(HelpDoc.p( + "Collision between two options detected - " + + "you can only specify one of either: ['--verbose', '--silent']" + ))) + }).pipe(runEffect)) + + it("does not validate collision of boolean options with negation", () => + Effect.gen(function*() { + const option = Options.boolean("v", { negationNames: ["s"] }) + const args = Array.make("-v", "-s") + const result = yield* Effect.flip(process(option, args, CliConfig.defaultConfig)) + expect(result).toEqual(ValidationError.invalidValue(HelpDoc.p( + "Collision between two options detected - " + + "you can only specify one of either: ['-v', '-s']" + ))) + }).pipe(runEffect)) + + it("validates a option with choices", () => + Effect.gen(function*() { + const option = Options.choice("animal", ["cat", "dog"]) + const args1 = Array.make("--animal", "cat") + const args2 = Array.make("--animal", "dog") + const result1 = yield* process(option, args1, CliConfig.defaultConfig) + const result2 = yield* process(option, args2, CliConfig.defaultConfig) + expect(result1).toEqual([[], "cat"]) + expect(result2).toEqual([[], "dog"]) + }).pipe(runEffect)) + + it("validates an option with choices that map to values", () => + Effect.gen(function*() { + type Animal = Dog | Cat + class Dog extends Data.TaggedClass("Dog")<{}> {} + class Cat extends Data.TaggedClass("Dog")<{}> {} + const cat = new Cat() + const dog = new Dog() + const option: Options.Options = Options.choiceWithValue("animal", [ + ["dog", dog], + ["cat", cat] + ]) + const args1 = Array.make("--animal", "cat") + const args2 = Array.make("--animal", "dog") + const result1 = yield* process(option, args1, CliConfig.defaultConfig) + const result2 = yield* process(option, args2, CliConfig.defaultConfig) + expect(result1).toEqual([[], cat]) + expect(result2).toEqual([[], dog]) + }).pipe(runEffect)) + + it("validates a text option", () => + Effect.gen(function*() { + const result = yield* process(firstName, ["--firstName", "John"], CliConfig.defaultConfig) + expect(result).toEqual([[], "John"]) + }).pipe(runEffect)) + + it("validates a text option with an alternative format", () => + Effect.gen(function*() { + const result = yield* process(firstName, ["--firstName=John"], CliConfig.defaultConfig) + expect(result).toEqual([[], "John"]) + }).pipe(runEffect)) + + it("validates a text option with an alias", () => + Effect.gen(function*() { + const result = yield* process(firstName, ["-f", "John"], CliConfig.defaultConfig) + expect(result).toEqual([[], "John"]) + }).pipe(runEffect)) + + it("validates an integer option", () => + Effect.gen(function*() { + const result = yield* process(age, ["--age", "100"], CliConfig.defaultConfig) + expect(result).toEqual([[], 100]) + }).pipe(runEffect)) + + it("validates an option and returns the remainder", () => + Effect.gen(function*() { + const args = Array.make("--firstName", "John", "--lastName", "Doe") + const result = yield* process(firstName, args, CliConfig.defaultConfig) + expect(result).toEqual([["--lastName", "Doe"], "John"]) + }).pipe(runEffect)) + + it("does not validate when no valid values are passed", () => + Effect.gen(function*() { + const args = Array.make("--lastName", "Doe") + const result = yield* Effect.either(process(firstName, args, CliConfig.defaultConfig)) + expect(result).toEqual(Either.left(ValidationError.missingValue(HelpDoc.p( + "Expected to find option: '--firstName'" + )))) + }).pipe(runEffect)) + + it("does not validate when an option is passed without a corresponding value", () => + Effect.gen(function*() { + const args = Array.make("--firstName") + const result = yield* Effect.either(process(firstName, args, CliConfig.defaultConfig)) + expect(result).toEqual(Either.left(ValidationError.missingValue(HelpDoc.p( + "Expected a value following option: '--firstName'" + )))) + }).pipe(runEffect)) + + it("does not validate an invalid option value", () => + Effect.gen(function*() { + const option = Options.integer("t") + const args = Array.make("-t", "abc") + const result = yield* Effect.flip(process(option, args, CliConfig.defaultConfig)) + expect(result).toEqual(ValidationError.invalidValue(HelpDoc.p("'abc' is not a integer"))) + }).pipe(runEffect)) + + it("does not validate an invalid option value even when there is a default", () => + Effect.gen(function*() { + const option = Options.withDefault(Options.integer("t"), 0) + const args = Array.make("-t", "abc") + const result = yield* Effect.flip(process(option, args, CliConfig.defaultConfig)) + expect(result).toEqual(ValidationError.invalidValue(HelpDoc.p("'abc' is not a integer"))) + }).pipe(runEffect)) + + it("validates with case-sensitive configuration", () => + Effect.gen(function*() { + const config = CliConfig.make({ isCaseSensitive: true, autoCorrectLimit: 2 }) + const option = Options.text("Firstname").pipe(Options.withAlias("F")) + const args1 = Array.make("--Firstname", "John") + const args2 = Array.make("-F", "John") + const args3 = Array.make("--firstname", "John") + const args4 = Array.make("-f", "John") + const result1 = yield* process(option, args1, config) + const result2 = yield* process(option, args2, config) + const result3 = yield* Effect.flip(process(option, args3, config)) + const result4 = yield* Effect.flip(process(option, args4, config)) + expect(result1).toEqual([[], "John"]) + expect(result2).toEqual([[], "John"]) + expect(result3).toEqual(ValidationError.correctedFlag(HelpDoc.p( + "The flag '--firstname' is not recognized. Did you mean '--Firstname'?" + ))) + expect(result4).toEqual(ValidationError.missingValue(HelpDoc.p( + "Expected to find option: '--Firstname'" + ))) + }).pipe(runEffect)) + + it("validates an unsupplied optional option", () => + Effect.gen(function*() { + const result = yield* process(ageOptional, [], CliConfig.defaultConfig) + expect(result).toEqual([[], Option.none()]) + }).pipe(runEffect)) + + it("validates an unsupplied optional option with remainder", () => + Effect.gen(function*() { + const args = Array.make("--bar", "baz") + const result = yield* process(ageOptional, args, CliConfig.defaultConfig) + expect(result).toEqual([args, Option.none()]) + }).pipe(runEffect)) + + it("validates a supplied optional option", () => + Effect.gen(function*() { + const args = Array.make("--age", "20") + const result = yield* process(ageOptional, args, CliConfig.defaultConfig) + expect(result).toEqual([[], Option.some(20)]) + }).pipe(runEffect)) + + it("validates using all and returns the specified structure", () => + Effect.gen(function*() { + const option1 = Options.all({ + firstName: Options.text("firstName"), + lastName: Options.text("lastName") + }) + const option2 = Options.all([Options.text("firstName"), Options.text("lastName")]) + const args = Array.make("--firstName", "John", "--lastName", "Doe") + const result1 = yield* process(option1, args, CliConfig.defaultConfig) + const result2 = yield* process(option2, args, CliConfig.defaultConfig) + expect(result1).toEqual([[], { firstName: "John", lastName: "Doe" }]) + expect(result2).toEqual([[], ["John", "Doe"]]) + }).pipe(runEffect)) + + it("validate provides a suggestion if a provided option is close to a specified option", () => + Effect.gen(function*() { + const args = Array.make("--firstme", "Alice") + const result = yield* Effect.flip(process(firstName, args, CliConfig.defaultConfig)) + expect(result).toEqual(ValidationError.correctedFlag(HelpDoc.p( + "The flag '--firstme' is not recognized. Did you mean '--firstName'?" + ))) + }).pipe(runEffect)) + + it("validate provides a suggestion if a provided option with a default is close to a specified option", () => + Effect.gen(function*() { + const option = firstName.pipe(Options.withDefault("Jack")) + const args = Array.make("--firstme", "Alice") + const result = yield* Effect.flip(process(option, args, CliConfig.defaultConfig)) + expect(result).toEqual(ValidationError.invalidValue(HelpDoc.p( + "The flag '--firstme' is not recognized. Did you mean '--firstName'?" + ))) + })) + + it("orElse - two options", () => + Effect.gen(function*() { + const option = Options.text("string").pipe( + Options.map(Either.left), + Options.orElse( + Options.integer("integer").pipe( + Options.map(Either.right) + ) + ) + ) + const args1 = Array.make("--integer", "2") + const args2 = Array.make("--string", "two") + const result1 = yield* process(option, args1, CliConfig.defaultConfig) + const result2 = yield* process(option, args2, CliConfig.defaultConfig) + expect(result1).toEqual([[], Either.right(2)]) + expect(result2).toEqual([[], Either.left("two")]) + }).pipe(runEffect)) + + it("orElse - option collision", () => + Effect.gen(function*() { + const option = Options.orElse(Options.text("string"), Options.integer("integer")) + const args = Array.make("--integer", "2", "--string", "two") + const result = yield* Effect.flip(process(option, args, CliConfig.defaultConfig)) + expect(result).toEqual(ValidationError.invalidValue(HelpDoc.p( + "Collision between two options detected - " + + "you can only specify one of either: ['--string', '--integer']" + ))) + }).pipe(runEffect)) + + it("orElse - no options provided", () => + Effect.gen(function*() { + const option = Options.orElse(Options.text("string"), Options.integer("integer")) + const result = yield* Effect.flip(process(option, [], CliConfig.defaultConfig)) + const error = ValidationError.missingValue(HelpDoc.sequence( + HelpDoc.p("Expected to find option: '--string'"), + HelpDoc.p("Expected to find option: '--integer'") + )) + expect(result).toEqual(error) + }).pipe(runEffect)) + + it("orElse - invalid option provided with a default", () => + Effect.gen(function*() { + const option = Options.integer("min").pipe( + Options.orElse(Options.integer("max")), + Options.withDefault(0) + ) + const args = Array.make("--min", "abc") + const result = yield* Effect.flip(process(option, args, CliConfig.defaultConfig)) + const error = ValidationError.invalidValue(HelpDoc.sequence( + HelpDoc.p("'abc' is not a integer"), + HelpDoc.p("Expected to find option: '--max'") + )) + expect(result).toEqual(error) + }).pipe(runEffect)) + + it("keyValueMap - validates a missing option", () => + Effect.gen(function*() { + const result = yield* Effect.flip(process(defs, [], CliConfig.defaultConfig)) + expect(result).toEqual(ValidationError.missingValue(HelpDoc.p( + "Expected to find option: '--defs'" + ))) + }).pipe(runEffect)) + + it("keyValueMap - validates repeated values", () => + Effect.gen(function*() { + const args = Array.make("-d", "key1=v1", "-d", "key2=v2", "--verbose") + const result = yield* process(defs, args, CliConfig.defaultConfig) + expect(result).toEqual([["--verbose"], HashMap.make(["key1", "v1"], ["key2", "v2"])]) + }).pipe(runEffect)) + + it("keyValueMap - validates different key/values", () => + Effect.gen(function*() { + const args = Array.make("--defs", "key1=v1", "key2=v2", "--verbose") + const result = yield* process(defs, args, CliConfig.defaultConfig) + expect(result).toEqual([["--verbose"], HashMap.make(["key1", "v1"], ["key2", "v2"])]) + }).pipe(runEffect)) + + it("keyValueMap - validates different key/values with alias", () => + Effect.gen(function*() { + const args = Array.make("-d", "key1=v1", "key2=v2", "--verbose") + const result = yield* process(defs, args, CliConfig.defaultConfig) + expect(result).toEqual([["--verbose"], HashMap.make(["key1", "v1"], ["key2", "v2"])]) + }).pipe(runEffect)) + + it("keyValueMap - validates key/values with equals in alias value", () => + Effect.gen(function*() { + const args = Array.make("-d", "key1=v1", "key2=v2=vv", "--verbose") + const result = yield* process(defs, args, CliConfig.defaultConfig) + expect(result).toEqual([["--verbose"], HashMap.make(["key1", "v1"], ["key2", "v2=vv"])]) + }).pipe(runEffect)) + + it("keyValueMap - validates key/values with equals in aliased longer value", () => + Effect.gen(function*() { + const args = Array.make("-d", "key1=v1", "key2=v2=1+1", "--verbose") + const result = yield* process(defs, args, CliConfig.defaultConfig) + expect(result).toEqual([["--verbose"], HashMap.make(["key1", "v1"], ["key2", "v2=1+1"])]) + }).pipe(runEffect)) + + it("keyValueMap - validate should keep non-key-value parameters that follow the key-value pairs (each preceded by alias -d)", () => + Effect.gen(function*() { + const args = Array.make( + "-d", + "key1=val1", + "-d", + "key2=val2", + "-d", + "key3=val3", + "arg1", + "arg2", + "--verbose" + ) + const result = yield* process(defs, args, CliConfig.defaultConfig) + expect(result).toEqual([ + ["arg1", "arg2", "--verbose"], + HashMap.make(["key1", "val1"], ["key2", "val2"], ["key3", "val3"]) + ]) + }).pipe(runEffect)) + + it("keyValueMap - validate should keep non-key-value parameters that follow the key-value pairs (only the first key/value pair is preceded by alias)", () => + Effect.gen(function*() { + const args = Array.make( + "-d", + "key1=val1", + "key2=val2", + "key3=val3", + "arg1", + "arg2", + "--verbose" + ) + const result = yield* process(defs, args, CliConfig.defaultConfig) + expect(result).toEqual([ + ["arg1", "arg2", "--verbose"], + HashMap.make(["key1", "val1"], ["key2", "val2"], ["key3", "val3"]) + ]) + }).pipe(runEffect)) + + it("keyValueMap - validate should return an error for invalid key/value pairs", () => + Effect.gen(function*() { + const args = Array.make( + "-d", + "key1=val1", + "key2=val2", + "--defs", + "key3=val3", + "key4=", + "arg1", + "arg2", + "--verbose" + ) + const result = yield* process(defs, args, CliConfig.defaultConfig) + expect(result).toEqual([ + ["key4=", "arg1", "arg2", "--verbose"], + HashMap.make(["key1", "val1"], ["key2", "val2"], ["key3", "val3"]) + ]) + }).pipe(runEffect)) + + it("repeated", () => + Effect.gen(function*() { + const option = Options.integer("foo").pipe(Options.repeated) + const args2 = ["--foo", "1", "--foo", "2", "--foo", "3"] + const args3 = ["--foo", "v2"] + const args4 = ["--foo", "1", "--foo", "v2", "--foo", "3"] + const args5 = ["--foo", "1", "-d", "--foo", "2"] + const args6 = ["--foo", "1", "-f", "firstName", "--foo", "2"] + const result1 = yield* process(option, [], CliConfig.defaultConfig) + const result2 = yield* process(option, args2, CliConfig.defaultConfig) + const result3 = yield* Effect.flip(process(option, args3, CliConfig.defaultConfig)) + const result4 = yield* Effect.flip(process(option, args4, CliConfig.defaultConfig)) + const result5 = yield* process(option, args5, CliConfig.defaultConfig) + const result6 = yield* process(option, args6, CliConfig.defaultConfig) + expect(result1).toEqual([Array.empty(), []]) + expect(result2).toEqual([Array.empty(), [1, 2, 3]]) + expect(result3).toEqual(ValidationError.invalidValue(HelpDoc.p("'v2' is not a integer"))) + expect(result4).toEqual(ValidationError.invalidValue(HelpDoc.p("'v2' is not a integer"))) + expect(result5).toEqual([["-d"], [1, 2]]) + expect(result6).toEqual([["-f", "firstName"], [1, 2]]) + }).pipe(runEffect)) + + it("atLeast", () => + Effect.gen(function*() { + const option = Options.integer("foo").pipe(Options.atLeast(2)) + const args1 = ["--foo", "1", "--foo", "2"] + const args2 = ["--foo", "1"] + const result1 = yield* process(option, args1, CliConfig.defaultConfig) + const result2 = yield* Effect.flip(process(option, args2, CliConfig.defaultConfig)) + expect(result1).toEqual([Array.empty(), [1, 2]]) + expect(result2).toEqual(ValidationError.invalidValue(HelpDoc.p( + "Expected at least 2 value(s) for option: '--foo'" + ))) + }).pipe(runEffect)) + + it("atMost", () => + Effect.gen(function*() { + const option = Options.integer("foo").pipe(Options.atMost(2)) + const args1 = ["--foo", "1", "--foo", "2"] + const args2 = ["--foo", "1", "--foo", "2", "--foo", "3"] + const result1 = yield* process(option, args1, CliConfig.defaultConfig) + const result2 = yield* Effect.flip(process(option, args2, CliConfig.defaultConfig)) + expect(result1).toEqual([Array.empty(), [1, 2]]) + expect(result2).toEqual(ValidationError.invalidValue(HelpDoc.p( + "Expected at most 2 value(s) for option: '--foo'" + ))) + }).pipe(runEffect)) + + it("between", () => + Effect.gen(function*() { + const option = Options.integer("foo").pipe(Options.between(2, 3)) + const args1 = ["--foo", "1"] + const args2 = ["--foo", "1", "--foo", "2"] + const args3 = ["--foo", "1", "--foo", "2", "--foo", "3"] + const args4 = ["--foo", "1", "--foo", "2", "--foo", "3", "--foo", "4"] + const result1 = yield* Effect.flip(process(option, args1, CliConfig.defaultConfig)) + const result2 = yield* process(option, args2, CliConfig.defaultConfig) + const result3 = yield* process(option, args3, CliConfig.defaultConfig) + const result4 = yield* Effect.flip(process(option, args4, CliConfig.defaultConfig)) + expect(result1).toEqual(ValidationError.invalidValue(HelpDoc.p( + "Expected at least 2 value(s) for option: '--foo'" + ))) + expect(result2).toEqual([Array.empty(), [1, 2]]) + expect(result3).toEqual([Array.empty(), [1, 2, 3]]) + expect(result4).toEqual(ValidationError.invalidValue(HelpDoc.p( + "Expected at most 3 value(s) for option: '--foo'" + ))) + }).pipe(runEffect)) + + it("validates with a Schema", () => + Effect.gen(function*() { + const result = yield* process(balance, ["--balance", "100.50"], CliConfig.defaultConfig) + assert.deepStrictEqual(result, [[], BigDecimal.unsafeFromString("100.50").pipe(BigDecimal.normalize)]) + }).pipe(runEffect)) + + it("failure with a Schema", () => + Effect.gen(function*() { + const result = yield* process(balance, ["--balance", "abc"], CliConfig.defaultConfig).pipe(Effect.flip) + assert.deepStrictEqual( + result, + ValidationError.invalidValue(HelpDoc.p( + "BigDecimal\n" + + "└─ Transformation process failure\n" + + " └─ Unable to decode \"abc\" into a BigDecimal" + )) + ) + }).pipe(runEffect)) + + it("fileContent", () => + Effect.gen(function*() { + const fs = yield* FileSystem.FileSystem + const path = yield* Path.Path + const filePath = path.join(__dirname, "fixtures/config.json") + const result = yield* process(Options.fileContent("config"), ["--config", filePath], CliConfig.defaultConfig) + const content = yield* fs.readFile(filePath) + assert.deepStrictEqual(result, [[], [filePath, content]]) + }).pipe(runEffect)) + + it("fileText", () => + Effect.gen(function*() { + const fs = yield* FileSystem.FileSystem + const path = yield* Path.Path + const filePath = path.join(__dirname, "fixtures/config.json") + const result = yield* process(Options.fileText("config"), ["--config", filePath], CliConfig.defaultConfig) + const content = yield* pipe(fs.readFileString(filePath)) + assert.deepStrictEqual(result, [[], [filePath, content]]) + }).pipe(runEffect)) + + it("fileParse", () => + Effect.gen(function*() { + const fs = yield* FileSystem.FileSystem + const path = yield* Path.Path + const filePath = path.join(__dirname, "fixtures/config.json") + const result = yield* process(Options.fileParse("config"), ["--config", filePath], CliConfig.defaultConfig) + const content = yield* pipe(fs.readFileString(filePath), Effect.map(JSON.parse)) + assert.deepStrictEqual(result, [[], content]) + }).pipe(runEffect)) + + it("fileSchema", () => + Effect.gen(function*() { + const fs = yield* FileSystem.FileSystem + const path = yield* Path.Path + const filePath = path.join(__dirname, "fixtures/config.json") + const result = yield* process( + Options.fileSchema( + "config", + Schema.Struct({ + foo: Schema.Boolean, + bar: Schema.Literal("baz") + }) + ), + ["--config", filePath], + CliConfig.defaultConfig + ) + const content = yield* pipe(fs.readFileString(filePath), Effect.map(JSON.parse)) + assert.deepStrictEqual(result, [[], content]) + }).pipe(runEffect)) + + it("fileSchema yaml", () => + Effect.gen(function*() { + const fs = yield* FileSystem.FileSystem + const path = yield* Path.Path + const filePath = path.join(__dirname, "fixtures/config.yaml") + const jsonPath = path.join(__dirname, "fixtures/config.json") + const result = yield* process( + Options.fileSchema( + "config", + Schema.Struct({ + foo: Schema.Boolean, + bar: Schema.Literal("baz") + }) + ), + ["--config", filePath], + CliConfig.defaultConfig + ) + const content = yield* pipe(fs.readFileString(jsonPath), Effect.map(JSON.parse)) + assert.deepStrictEqual(result, [[], content]) + }).pipe(runEffect)) + + it("fileSchema ini", () => + Effect.gen(function*() { + const fs = yield* FileSystem.FileSystem + const path = yield* Path.Path + const filePath = path.join(__dirname, "fixtures/config.ini") + const jsonPath = path.join(__dirname, "fixtures/config.json") + const result = yield* process( + Options.fileSchema( + "config", + Schema.Struct({ + foo: Schema.Boolean, + bar: Schema.Literal("baz") + }) + ), + ["--config", filePath], + CliConfig.defaultConfig + ) + const content = yield* pipe(fs.readFileString(jsonPath), Effect.map(JSON.parse)) + assert.deepStrictEqual(result, [[], content]) + }).pipe(runEffect)) + + it("fileSchema toml", () => + Effect.gen(function*() { + const fs = yield* FileSystem.FileSystem + const path = yield* Path.Path + const filePath = path.join(__dirname, "fixtures/config.toml") + const jsonPath = path.join(__dirname, "fixtures/config.json") + const result = yield* process( + Options.fileSchema( + "config", + Schema.Struct({ + foo: Schema.Boolean, + bar: Schema.Literal("baz") + }) + ), + ["--config", filePath], + CliConfig.defaultConfig + ) + const content = yield* pipe(fs.readFileString(jsonPath), Effect.map(JSON.parse)) + assert.deepStrictEqual(result, [[], content]) + }).pipe(runEffect)) + + it("displays default value in help when default wrapped in Option.Some (primitive)", () => + Effect.gen(function*() { + const option = Options.withDefault(Options.integer("value"), Option.some(123)) + const helpDoc = Options.getHelp(option) + yield* Effect.promise(() => + expect(helpDoc).toMatchFileSnapshot("./snapshots/help-output/options-default-primitive") + ) + }).pipe(runEffect)) + + it("displays default value in help when default wrapped in Option.Some (object)", () => + Effect.gen(function*() { + const defaultObject = { key: "value", number: 456 } + const option = Options.withDefault(Options.text("config"), Option.some(defaultObject)) + const helpDoc = Options.getHelp(option) + yield* Effect.promise(() => expect(helpDoc).toMatchFileSnapshot("./snapshots/help-output/options-default-object")) + }).pipe(runEffect)) + + it("displays no default value in help when default is not Option.Some", () => + Effect.gen(function*() { + const option = Options.withDefault(Options.text("name"), Option.none()) + const helpDoc = Options.getHelp(option) + yield* Effect.promise(() => expect(helpDoc).toMatchFileSnapshot("./snapshots/help-output/options-no-default")) + }).pipe(runEffect)) + + describe("options after positional arguments", () => { + it("parses a text option that appears after positional args", () => + Effect.gen(function*() { + // Simulating: cmd positional --firstName John + const args = Array.make("positional", "--firstName", "John") + const result = yield* process(firstName, args, CliConfig.defaultConfig) + // The "positional" should be in the leftover, firstName should be parsed + expect(result).toEqual([["positional"], "John"]) + }).pipe(runEffect)) + + it("parses a text option with alias that appears after positional args", () => + Effect.gen(function*() { + // Simulating: cmd positional -f John + const args = Array.make("positional", "-f", "John") + const result = yield* process(firstName, args, CliConfig.defaultConfig) + expect(result).toEqual([["positional"], "John"]) + }).pipe(runEffect)) + + it("parses a boolean option that appears after positional args", () => + Effect.gen(function*() { + // Simulating: cmd positional --verbose + const args = Array.make("positional", "--verbose") + const result = yield* process(verbose, args, CliConfig.defaultConfig) + expect(result).toEqual([["positional"], true]) + }).pipe(runEffect)) + + it("parses multiple options when some appear after positional args", () => + Effect.gen(function*() { + const options = Options.all([firstName, lastName]) + // Simulating: cmd --firstName John positional --lastName Doe + const args = Array.make("--firstName", "John", "positional", "--lastName", "Doe") + const result = yield* process(options, args, CliConfig.defaultConfig) + expect(result).toEqual([["positional"], ["John", "Doe"]]) + }).pipe(runEffect)) + + it("parses options interspersed with multiple positional args", () => + Effect.gen(function*() { + const options = Options.all([firstName, verbose]) + // Simulating: cmd pos1 --firstName John pos2 --verbose pos3 + const args = Array.make("pos1", "--firstName", "John", "pos2", "--verbose", "pos3") + const result = yield* process(options, args, CliConfig.defaultConfig) + expect(result).toEqual([["pos1", "pos2", "pos3"], ["John", true]]) + }).pipe(runEffect)) + + it("parses text option with = syntax after positional args", () => + Effect.gen(function*() { + // Simulating: cmd positional --firstName=John + const args = Array.make("positional", "--firstName=John") + const result = yield* process(firstName, args, CliConfig.defaultConfig) + expect(result).toEqual([["positional"], "John"]) + }).pipe(runEffect)) + + it("parses boolean option with explicit true value after positional args", () => + Effect.gen(function*() { + // Simulating: cmd positional --verbose true + const args = Array.make("positional", "--verbose", "true") + const result = yield* process(verbose, args, CliConfig.defaultConfig) + expect(result).toEqual([["positional"], true]) + }).pipe(runEffect)) + + it("parses boolean option with explicit false value after positional args", () => + Effect.gen(function*() { + // Simulating: cmd positional --verbose false + const args = Array.make("positional", "--verbose", "false") + const result = yield* process(verbose, args, CliConfig.defaultConfig) + expect(result).toEqual([["positional"], false]) + }).pipe(runEffect)) + + it("parses keyValueMap option that appears after positional args", () => + Effect.gen(function*() { + // Simulating: cmd positional --defs key=value + const args = Array.make("positional", "--defs", "key=value") + const result = yield* process(defs, args, CliConfig.defaultConfig) + expect(result).toEqual([["positional"], HashMap.make(["key", "value"])]) + }).pipe(runEffect)) + + it("parses keyValueMap option with multiple values after positional args", () => + Effect.gen(function*() { + // Simulating: cmd positional --defs key1=value1 key2=value2 + const args = Array.make("positional", "-d", "key1=value1", "key2=value2") + const result = yield* process(defs, args, CliConfig.defaultConfig) + expect(result).toEqual([["positional"], HashMap.make(["key1", "value1"], ["key2", "value2"])]) + }).pipe(runEffect)) + }) +}) diff --git a/repos/effect/packages/cli/test/Primitive.test.ts b/repos/effect/packages/cli/test/Primitive.test.ts new file mode 100644 index 0000000..ac2ffcf --- /dev/null +++ b/repos/effect/packages/cli/test/Primitive.test.ts @@ -0,0 +1,151 @@ +import * as CliConfig from "@effect/cli/CliConfig" +import * as Primitive from "@effect/cli/Primitive" +import type { FileSystem } from "@effect/platform" +import { NodeFileSystem } from "@effect/platform-node" +import { describe, expect, it } from "@effect/vitest" +import { Array, Effect, Equal, Function, Option } from "effect" +import * as fc from "effect/FastCheck" + +const runEffect = (self: Effect.Effect): Promise => + Effect.provide(self, NodeFileSystem.layer).pipe(Effect.runPromise) + +describe("Primitive", () => { + describe("Bool", () => { + it("validates that truthy text representations of a boolean return true", () => + fc.assert(fc.asyncProperty(trueValuesArb, (str) => + Effect.gen(function*() { + const bool = Primitive.boolean(Option.none()) + const result = yield* Primitive.validate( + bool, + Option.some(str), + CliConfig.defaultConfig + ) + expect(result).toBe(true) + }).pipe(runEffect)))) + + it("validates that falsy text representations of a boolean return false", () => + fc.assert(fc.asyncProperty(falseValuesArb, (str) => + Effect.gen(function*() { + const bool = Primitive.boolean(Option.none()) + const result = yield* Primitive.validate( + bool, + Option.some(str), + CliConfig.defaultConfig + ) + expect(result).toBe(false) + }).pipe(runEffect)))) + + it("validates that invalid boolean representations are rejected", () => + Effect.gen(function*() { + const bool = Primitive.boolean(Option.none()) + const result = yield* Effect.flip(Primitive.validate(bool, Option.some("bad"), CliConfig.defaultConfig)) + expect(result).toBe("Unable to recognize 'bad' as a valid boolean") + }).pipe(runEffect)) + + it("validates that the default value will be used if a value is not provided", () => + fc.assert(fc.asyncProperty(fc.boolean(), (value) => + Effect.gen(function*() { + const bool = Primitive.boolean(Option.some(value)) + const result = yield* Primitive.validate(bool, Option.none(), CliConfig.defaultConfig) + expect(result).toBe(value) + }).pipe(runEffect)))) + }) + + describe("Choice", () => { + it("validates a choice that is one of the alternatives", () => + fc.assert( + fc.asyncProperty(pairsArb, ([[selectedName, selectedValue], pairs]) => + Effect.gen(function*() { + const alternatives = Function.unsafeCoerce< + ReadonlyArray<[string, number]>, + Array.NonEmptyReadonlyArray<[string, number]> + >(pairs) + const choice = Primitive.choice(alternatives) + const result = yield* Primitive.validate( + choice, + Option.some(selectedName), + CliConfig.defaultConfig + ) + expect(result).toEqual(selectedValue) + }).pipe(runEffect)) + )) + + it("does not validate a choice that is not one of the alternatives", () => + fc.assert(fc.asyncProperty(pairsArb, ([tuple, pairs]) => + Effect.gen(function*() { + const selectedName = tuple[0] + const alternatives = Function.unsafeCoerce< + ReadonlyArray<[string, number]>, + Array.NonEmptyReadonlyArray<[string, number]> + >(Array.filter(pairs, (pair) => !Equal.equals(tuple, pair))) + const choice = Primitive.choice(alternatives) + const result = yield* Effect.flip(Primitive.validate( + choice, + Option.some(selectedName), + CliConfig.defaultConfig + )) + expect(result).toMatch(/^Expected one of the following cases:\s.*/) + }).pipe(runEffect)))) + }) + + simplePrimitiveTestSuite(Primitive.date, fc.date({ noInvalidDate: true }), "Date") + + simplePrimitiveTestSuite( + Primitive.float, + fc.float({ noNaN: true }).filter((n) => n !== 0), + "Float" + ) + + simplePrimitiveTestSuite(Primitive.integer, fc.integer(), "Integer") + + describe("Text", () => { + it("validates all user-defined text", () => + fc.assert(fc.asyncProperty(fc.string(), (str) => + Effect.gen(function*() { + const result = yield* Primitive.validate( + Primitive.text, + Option.some(str), + CliConfig.defaultConfig + ) + expect(result).toEqual(str) + }).pipe(runEffect)))) + }) +}) + +const simplePrimitiveTestSuite = ( + primitive: Primitive.Primitive, + arb: fc.Arbitrary, + primitiveTypeName: string +) => { + describe(`${primitiveTypeName}`, () => { + it(`validates that valid values are accepted`, () => + fc.assert(fc.asyncProperty(arb, (value) => + Effect.gen(function*() { + const str = value instanceof Date ? value.toISOString() : `${value}` + const result = yield* Primitive.validate(primitive, Option.some(str), CliConfig.defaultConfig) + expect(result).toEqual(value) + }).pipe(runEffect)))) + + it(`validates that invalid values are rejected`, () => + Effect.gen(function*() { + const result = yield* Effect.flip(Primitive.validate(primitive, Option.some("bad"), CliConfig.defaultConfig)) + expect(result).toBe(`'bad' is not a ${Primitive.getTypeName(primitive)}`) + }).pipe(runEffect)) + }) +} + +const randomizeCharacterCases = (str: string): string => { + let result = "" + for (let i = 0; i < str.length; i++) { + const char = str[i] + result += Math.random() < 0.5 ? char.toLowerCase() : char.toUpperCase() + } + return result +} + +const trueValuesArb = fc.constantFrom("true", "1", "y", "yes", "on").map(randomizeCharacterCases) +const falseValuesArb = fc.constantFrom("false", "0", "n", "no", "off").map(randomizeCharacterCases) + +const pairsArb = fc.array(fc.tuple(fc.string(), fc.float()), { minLength: 2, maxLength: 100 }) + .map((pairs) => Array.dedupeWith(pairs, ([str1], [str2]) => str1 === str2)) + .chain((pairs) => fc.tuple(fc.constantFrom(...pairs), fc.constant(pairs))) diff --git a/repos/effect/packages/cli/test/Prompt.test.ts b/repos/effect/packages/cli/test/Prompt.test.ts new file mode 100644 index 0000000..2fa57da --- /dev/null +++ b/repos/effect/packages/cli/test/Prompt.test.ts @@ -0,0 +1,484 @@ +import type * as CliApp from "@effect/cli/CliApp" +import * as Prompt from "@effect/cli/Prompt" +import { NodeFileSystem, NodePath } from "@effect/platform-node" +import * as Ansi from "@effect/printer-ansi/Ansi" +import * as Doc from "@effect/printer-ansi/AnsiDoc" +import { describe, expect, it } from "@effect/vitest" +import * as Console from "effect/Console" +import * as Effect from "effect/Effect" +import * as Fiber from "effect/Fiber" +import * as Layer from "effect/Layer" +import * as Redacted from "effect/Redacted" +import * as MockConsole from "./services/MockConsole.js" +import * as MockTerminal from "./services/MockTerminal.js" + +const MainLive = Effect.gen(function*() { + const console = yield* MockConsole.make + return Layer.mergeAll( + Console.setConsole(console), + NodeFileSystem.layer, + MockTerminal.layer, + NodePath.layer + ) +}).pipe(Layer.unwrapEffect) + +const runEffect = ( + self: Effect.Effect +): Promise => Effect.provide(self, MainLive).pipe(Effect.runPromise) + +describe("Prompt", () => { + describe("text", () => { + it("should use the prompt value when no default is provided", () => + Effect.gen(function*() { + const prompt = Prompt.text({ + message: "This does not have a default" + }) + + const fiber = yield* Effect.fork(prompt) + yield* MockTerminal.inputKey("enter") + const result = yield* Fiber.join(fiber) + + expect(result).toBe("") + }).pipe(runEffect)) + + it("should use the default value when the default is provided", () => + Effect.gen(function*() { + const prompt = Prompt.text({ + message: "This should have a default", + default: "default-value" + }) + + const fiber = yield* Effect.fork(prompt) + yield* MockTerminal.inputKey("enter") + const result = yield* Fiber.join(fiber) + + expect(result).toBe("default-value") + }).pipe(runEffect)) + + it("should render the default value when the default is provided", () => + Effect.gen(function*() { + const prompt = Prompt.text({ + message: "Test Prompt", + default: "default-value" + }) + + const fiber = yield* Effect.fork(prompt) + + yield* MockTerminal.inputKey("enter") + yield* Fiber.join(fiber) + + const lines = yield* MockConsole.getLines() + + const unsubmittedValue = Doc.annotate(Doc.text("default-value"), Ansi.blackBright).pipe(Doc.render({ + style: "pretty" + })) + + const submittedValue = Doc.annotate(Doc.text("default-value"), Ansi.white).pipe(Doc.render({ + style: "pretty" + })) + + expect(lines).toEqual( + expect.arrayContaining([ + expect.stringContaining( + unsubmittedValue + ), + expect.stringContaining( + submittedValue + ) + ]) + ) + + expect(lines.findIndex((line) => line.includes(unsubmittedValue))).toBeLessThan( + lines.findIndex((line) => line.includes(submittedValue)) + ) + }).pipe(runEffect)) + + it("should accept the default value when the tab is pressed", () => + Effect.gen(function*() { + const prompt = Prompt.text({ + message: "Test Prompt", + default: "default-value" + }) + + const fiber = yield* Effect.fork(prompt) + + yield* MockTerminal.inputKey("tab") + yield* MockTerminal.inputKey("enter") + yield* Fiber.join(fiber) + + const lines = yield* MockConsole.getLines() + + const unsubmittedValue = Doc.annotate(Doc.text("default-value"), Ansi.blackBright).pipe(Doc.render({ + style: "pretty" + })) + + const enteredValue = Doc.annotate(Doc.text("default-value"), Ansi.combine(Ansi.underlined, Ansi.cyanBright)) + .pipe(Doc.render({ + style: "pretty" + })) + + expect(lines).toEqual( + expect.arrayContaining([ + expect.stringContaining( + unsubmittedValue + ), + expect.stringContaining( + enteredValue + ) + ]) + ) + + expect(lines.findIndex((line) => line.includes(unsubmittedValue))).toBeLessThan( + lines.findIndex((line) => line.includes(enteredValue)) + ) + }).pipe(runEffect)) + }) + + describe("hidden", () => { + it("should use the prompt value when no default is provided", () => + Effect.gen(function*() { + const prompt = Prompt.hidden({ + message: "This does not have a default" + }) + + const fiber = yield* Effect.fork(prompt) + yield* MockTerminal.inputKey("enter") + const result = yield* Fiber.join(fiber) + + expect(result).toEqual(Redacted.make("")) + }).pipe(runEffect)) + + it("should use the default value when the default is provided", () => + Effect.gen(function*() { + const prompt = Prompt.hidden({ + message: "This should have a default", + default: "default-value" + }) + + const fiber = yield* Effect.fork(prompt) + yield* MockTerminal.inputKey("enter") + const result = yield* Fiber.join(fiber) + + expect(result).toEqual(Redacted.make("default-value")) + }).pipe(runEffect)) + + it("should not render the default value when the default is provided", () => + Effect.gen(function*() { + const prompt = Prompt.hidden({ + message: "Test Prompt", + default: "default-value" + }) + + const fiber = yield* Effect.fork(prompt) + + yield* MockTerminal.inputKey("enter") + yield* Fiber.join(fiber) + + const lines = yield* MockConsole.getLines({ stripAnsi: true }) + + expect(lines).not.toEqual( + expect.arrayContaining([ + expect.stringContaining( + "default-value" + ) + ]) + ) + }).pipe(runEffect)) + }) + + describe("list", () => { + it("should use the prompt value when no default is provided", () => + Effect.gen(function*() { + const prompt = Prompt.list({ + message: "This does not have a default" + }) + + const fiber = yield* Effect.fork(prompt) + yield* MockTerminal.inputKey("enter") + const result = yield* Fiber.join(fiber) + + expect(result).toEqual([""]) + }).pipe(runEffect)) + + it("should use the default value when the default is provided", () => + Effect.gen(function*() { + const prompt = Prompt.list({ + message: "This should have a default", + default: "default-value" + }) + + const fiber = yield* Effect.fork(prompt) + yield* MockTerminal.inputKey("enter") + const result = yield* Fiber.join(fiber) + + expect(result).toEqual(["default-value"]) + }).pipe(runEffect)) + + it("should render the default value when the default is provided", () => + Effect.gen(function*() { + const prompt = Prompt.list({ + message: "Test Prompt", + default: "default-value" + }) + + const fiber = yield* Effect.fork(prompt) + + yield* MockTerminal.inputKey("enter") + yield* Fiber.join(fiber) + + const lines = yield* MockConsole.getLines() + + const unsubmittedValue = Doc.annotate(Doc.text("default-value"), Ansi.blackBright).pipe(Doc.render({ + style: "pretty" + })) + + const submittedValue = Doc.annotate(Doc.text("default-value"), Ansi.white).pipe(Doc.render({ + style: "pretty" + })) + + expect(lines).toEqual( + expect.arrayContaining([ + expect.stringContaining( + unsubmittedValue + ), + expect.stringContaining( + submittedValue + ) + ]) + ) + + expect(lines.findIndex((line) => line.includes(unsubmittedValue))).toBeLessThan( + lines.findIndex((line) => line.includes(submittedValue)) + ) + }).pipe(runEffect)) + }) + + describe("password", () => { + it("should use the prompt value when no default is provided", () => + Effect.gen(function*() { + const prompt = Prompt.password({ + message: "This does not have a default" + }) + + const fiber = yield* Effect.fork(prompt) + yield* MockTerminal.inputKey("enter") + const result = yield* Fiber.join(fiber) + + expect(result).toEqual(Redacted.make("")) + }).pipe(runEffect)) + + it("should use the default value when the default is provided", () => + Effect.gen(function*() { + const prompt = Prompt.password({ + message: "This should have a default", + default: "default-value" + }) + + const fiber = yield* Effect.fork(prompt) + yield* MockTerminal.inputKey("enter") + const result = yield* Fiber.join(fiber) + + expect(result).toEqual(Redacted.make("default-value")) + }).pipe(runEffect)) + + it("should render the redacted default value when the default is provided", () => + Effect.gen(function*() { + const prompt = Prompt.password({ + message: "Test Prompt", + default: "default-value" + }) + + const fiber = yield* Effect.fork(prompt) + + yield* MockTerminal.inputKey("enter") + yield* Fiber.join(fiber) + + const lines = yield* MockConsole.getLines() + + const redactedValue = "*".repeat("default-value".length) + const unsubmittedValue = Doc.annotate(Doc.text(redactedValue), Ansi.blackBright).pipe(Doc.render({ + style: "pretty" + })) + + const submittedValue = Doc.annotate(Doc.text(redactedValue), Ansi.white).pipe(Doc.render({ + style: "pretty" + })) + + expect(lines).toEqual( + expect.arrayContaining([ + expect.stringContaining( + unsubmittedValue + ), + expect.stringContaining( + submittedValue + ) + ]) + ) + + expect(lines.findIndex((line) => line.includes(unsubmittedValue))).toBeLessThan( + lines.findIndex((line) => line.includes(submittedValue)) + ) + }).pipe(runEffect)) + }) + + describe("Prompt.select", () => { + it("should return the selected value when an option is chosen", () => + Effect.gen(function*() { + const prompt = Prompt.select({ + message: "Select an option", + choices: [ + { title: "Option 1", value: 1 }, + { title: "Option 2", value: 2 }, + { title: "Option 3", value: 3 } + ] + }) + + const fiber = yield* Effect.fork(prompt) + yield* MockTerminal.inputKey("down") + yield* MockTerminal.inputKey("enter") + const result = yield* Fiber.join(fiber) + + expect(result).toEqual(2) + }).pipe(runEffect)) + + it("should honor a single default selected choice", () => + Effect.gen(function*() { + const prompt = Prompt.select({ + message: "Select an option", + choices: [ + { title: "Option 1", value: 1 }, + { title: "Option 2", value: 2, selected: true }, + { title: "Option 3", value: 3 } + ] + }) + + const fiber = yield* Effect.fork(prompt) + // Immediately submit without navigation + yield* MockTerminal.inputKey("enter") + const result = yield* Fiber.join(fiber) + + expect(result).toEqual(2) + }).pipe(runEffect)) + + it("should throw if multiple default selected choices are provided", () => + Effect.gen(function*() { + expect(() => + Prompt.select({ + message: "Select an option", + choices: [ + { title: "Option 1", value: 1, selected: true }, + { title: "Option 2", value: 2, selected: true }, + { title: "Option 3", value: 3 } + ] + }) + ).toThrow() + }).pipe(runEffect)) + }) + + describe("Prompt.selectMulti", () => { + it("should return the selected values when multiple options are chosen", () => + Effect.gen(function*() { + const prompt = Prompt.multiSelect({ + message: "Select multiple options", + choices: [ + { title: "Option A", value: "A" }, + { title: "Option B", value: "B" }, + { title: "Option C", value: "C" } + ] + }) + + const fiber = yield* Effect.fork(prompt) + yield* MockTerminal.inputKey("down") + yield* MockTerminal.inputKey("down") + yield* MockTerminal.inputKey("space") + yield* MockTerminal.inputKey("down") + yield* MockTerminal.inputKey("space") + yield* MockTerminal.inputKey("enter") + const result = yield* Fiber.join(fiber) + + expect(result).toEqual(["A", "B"]) + }).pipe(runEffect)) + + it("should select all options when 'Select All' is triggered", () => + Effect.gen(function*() { + const prompt = Prompt.multiSelect({ + message: "Select multiple options", + choices: [ + { title: "Option A", value: "A" }, + { title: "Option B", value: "B" }, + { title: "Option C", value: "C" } + ] + }) + + const fiber = yield* Effect.fork(prompt) + yield* MockTerminal.inputKey("space") // Select All + yield* MockTerminal.inputKey("enter") + const result = yield* Fiber.join(fiber) + + expect(result).toEqual(["A", "B", "C"]) + }).pipe(runEffect)) + + it("should deselect all options when 'Select None' is triggered", () => + Effect.gen(function*() { + const prompt = Prompt.multiSelect({ + message: "Select multiple options", + choices: [ + { title: "Option A", value: "A" }, + { title: "Option B", value: "B" }, + { title: "Option C", value: "C" } + ] + }) + + const fiber = yield* Effect.fork(prompt) + yield* MockTerminal.inputKey("space") // Select All + yield* MockTerminal.inputKey("space") // Select None + yield* MockTerminal.inputKey("enter") + const result = yield* Fiber.join(fiber) + + expect(result).toEqual([]) + }).pipe(runEffect)) + + it("should inverse the selection when 'Inverse Selection' is triggered", () => + Effect.gen(function*() { + const prompt = Prompt.multiSelect({ + message: "Select multiple options", + choices: [ + { title: "Option A", value: "A" }, + { title: "Option B", value: "B" }, + { title: "Option C", value: "C" } + ] + }) + + const fiber = yield* Effect.fork(prompt) + yield* MockTerminal.inputKey("space") + yield* MockTerminal.inputKey("tab") + yield* MockTerminal.inputKey("tab") + yield* MockTerminal.inputKey("space") + yield* MockTerminal.inputKey("up") + yield* MockTerminal.inputKey("space") + yield* MockTerminal.inputKey("enter") + const result = yield* Fiber.join(fiber) + + expect(result).toEqual(["A"]) + }).pipe(runEffect)) + + it("should preselect choices marked with selected: true", () => + Effect.gen(function*() { + const prompt = Prompt.multiSelect({ + message: "Select multiple options", + choices: [ + { title: "Option A", value: "A", selected: true }, + { title: "Option B", value: "B", selected: true }, + { title: "Option C", value: "C" } + ] + }) + + const fiber = yield* Effect.fork(prompt) + // Immediately submit without any navigation or toggling + yield* MockTerminal.inputKey("enter") + const result = yield* Fiber.join(fiber) + + expect(result).toEqual(["A", "B"]) + }).pipe(runEffect)) + }) +}) diff --git a/repos/effect/packages/cli/test/Wizard.test.ts b/repos/effect/packages/cli/test/Wizard.test.ts new file mode 100644 index 0000000..f7e791e --- /dev/null +++ b/repos/effect/packages/cli/test/Wizard.test.ts @@ -0,0 +1,44 @@ +import type * as CliApp from "@effect/cli/CliApp" +import * as Command from "@effect/cli/Command" +import * as Options from "@effect/cli/Options" +import { NodeFileSystem, NodePath } from "@effect/platform-node" +import { describe, expect, it } from "@effect/vitest" +import { Array, Effect } from "effect" +import * as Console from "effect/Console" +import * as Fiber from "effect/Fiber" +import * as Layer from "effect/Layer" +import * as MockConsole from "./services/MockConsole.js" +import * as MockTerminal from "./services/MockTerminal.js" + +const MainLive = Effect.gen(function*() { + const console = yield* MockConsole.make + return Layer.mergeAll( + Console.setConsole(console), + NodeFileSystem.layer, + MockTerminal.layer, + NodePath.layer + ) +}).pipe(Layer.unwrapEffect) + +const runEffect = ( + self: Effect.Effect +): Promise => Effect.provide(self, MainLive).pipe(Effect.runPromise) + +describe("Wizard", () => { + it("should quit the wizard when CTRL+C is entered", () => + Effect.gen(function*() { + const cli = Command.make("foo", { message: Options.text("message") }).pipe( + Command.run({ + name: "Test", + version: "1.0.0" + }) + ) + const args = Array.make("node", "test", "--wizard") + const fiber = yield* Effect.fork(cli(args)) + yield* MockTerminal.inputKey("c", { ctrl: true }) + yield* Fiber.join(fiber) + const lines = yield* MockConsole.getLines({ stripAnsi: true }) + const result = Array.some(lines, (line) => line.includes("Quitting wizard mode...")) + expect(result).toBe(true) + }).pipe(runEffect)) +}) diff --git a/repos/effect/packages/cli/test/fixtures/config-file.toml b/repos/effect/packages/cli/test/fixtures/config-file.toml new file mode 100644 index 0000000..eba385f --- /dev/null +++ b/repos/effect/packages/cli/test/fixtures/config-file.toml @@ -0,0 +1 @@ +foo = 123 diff --git a/repos/effect/packages/cli/test/fixtures/config.ini b/repos/effect/packages/cli/test/fixtures/config.ini new file mode 100644 index 0000000..0756f81 --- /dev/null +++ b/repos/effect/packages/cli/test/fixtures/config.ini @@ -0,0 +1,2 @@ +foo = true +bar = baz diff --git a/repos/effect/packages/cli/test/fixtures/config.json b/repos/effect/packages/cli/test/fixtures/config.json new file mode 100644 index 0000000..9980fff --- /dev/null +++ b/repos/effect/packages/cli/test/fixtures/config.json @@ -0,0 +1,4 @@ +{ + "foo": true, + "bar": "baz" +} diff --git a/repos/effect/packages/cli/test/fixtures/config.toml b/repos/effect/packages/cli/test/fixtures/config.toml new file mode 100644 index 0000000..61537f7 --- /dev/null +++ b/repos/effect/packages/cli/test/fixtures/config.toml @@ -0,0 +1,2 @@ +foo = true +bar = "baz" diff --git a/repos/effect/packages/cli/test/fixtures/config.yaml b/repos/effect/packages/cli/test/fixtures/config.yaml new file mode 100644 index 0000000..70d6b77 --- /dev/null +++ b/repos/effect/packages/cli/test/fixtures/config.yaml @@ -0,0 +1,2 @@ +foo: true +bar: baz diff --git a/repos/effect/packages/cli/test/services/MockConsole.ts b/repos/effect/packages/cli/test/services/MockConsole.ts new file mode 100644 index 0000000..9a972cd --- /dev/null +++ b/repos/effect/packages/cli/test/services/MockConsole.ts @@ -0,0 +1,69 @@ +import * as Array from "effect/Array" +import * as Console from "effect/Console" +import * as Context from "effect/Context" +import * as Effect from "effect/Effect" +import * as Ref from "effect/Ref" + +export interface MockConsole extends Console.Console { + readonly getLines: ( + params?: Partial<{ + readonly stripAnsi: boolean + }> + ) => Effect.Effect> +} + +export const MockConsole = Context.GenericTag( + "effect/Console" +) +const pattern = new RegExp( + [ + "[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]+)*|[a-zA-Z\\d]+(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)", + "(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PRZcf-ntqry=><~]))" + ].join("|"), + "g" +) + +const stripAnsi = (str: string) => str.replace(pattern, "") + +export const make = Effect.gen(function*() { + const lines = yield* Ref.make(Array.empty()) + + const getLines: MockConsole["getLines"] = (params = {}) => + Ref.get(lines).pipe(Effect.map((lines) => + params.stripAnsi || false + ? Array.map(lines, stripAnsi) + : lines + )) + + const log: MockConsole["log"] = (...args) => Ref.update(lines, Array.appendAll(args)) + + return MockConsole.of({ + [Console.TypeId]: Console.TypeId, + getLines, + log, + unsafe: globalThis.console, + assert: () => Effect.void, + clear: Effect.void, + count: () => Effect.void, + countReset: () => Effect.void, + debug: () => Effect.void, + dir: () => Effect.void, + dirxml: () => Effect.void, + error: () => Effect.void, + group: () => Effect.void, + groupEnd: Effect.void, + info: () => Effect.void, + table: () => Effect.void, + time: () => Effect.void, + timeEnd: () => Effect.void, + timeLog: () => Effect.void, + trace: () => Effect.void, + warn: () => Effect.void + }) +}) + +export const getLines = ( + params?: Partial<{ + readonly stripAnsi?: boolean + }> +): Effect.Effect> => Effect.consoleWith((console) => (console as MockConsole).getLines(params)) diff --git a/repos/effect/packages/cli/test/services/MockTerminal.ts b/repos/effect/packages/cli/test/services/MockTerminal.ts new file mode 100644 index 0000000..d34e47f --- /dev/null +++ b/repos/effect/packages/cli/test/services/MockTerminal.ts @@ -0,0 +1,106 @@ +import type * as Terminal from "@effect/platform/Terminal" +import * as Array from "effect/Array" +import * as Console from "effect/Console" +import * as Context from "effect/Context" +import * as Effect from "effect/Effect" +import * as Layer from "effect/Layer" +import * as Mailbox from "effect/Mailbox" +import * as Option from "effect/Option" + +// ============================================================================= +// Models +// ============================================================================= + +export interface MockTerminal extends Terminal.Terminal { + readonly inputText: (text: string) => Effect.Effect + readonly inputKey: ( + key: string, + modifiers?: Partial + ) => Effect.Effect +} + +export declare namespace MockTerminal { + export interface Modifiers { + readonly ctrl: boolean + readonly meta: boolean + readonly shift: boolean + } +} + +// ============================================================================= +// Context +// ============================================================================= + +export const MockTerminal = Context.GenericTag( + "@effect/platform/Terminal" +) + +// ============================================================================= +// Constructors +// ============================================================================= + +export const make = Effect.gen(function*() { + const queue = yield* Effect.acquireRelease( + Mailbox.make(), + (_) => _.shutdown + ) + + const inputText: MockTerminal["inputText"] = (text: string) => { + const inputs = Array.map(text.split(""), (key) => toUserInput(key)) + return queue.offerAll(inputs).pipe(Effect.asVoid) + } + + const inputKey: MockTerminal["inputKey"] = ( + key: string, + modifiers?: Partial + ) => { + const input = toUserInput(key, modifiers) + return shouldQuit(input) ? queue.end : queue.offer(input).pipe(Effect.asVoid) + } + + const display: MockTerminal["display"] = (input) => Console.log(input) + + const readInput: MockTerminal["readInput"] = Effect.succeed(queue) + + return MockTerminal.of({ + columns: Effect.succeed(80), + rows: Effect.succeed(24), + isTTY: Effect.succeed(true), + display, + readInput, + readLine: Effect.succeed(""), + inputKey, + inputText + }) +}) + +// ============================================================================= +// Layer +// ============================================================================= + +export const layer = Layer.scoped(MockTerminal, make) + +// ============================================================================= +// Accessors +// ============================================================================= + +export const { columns, readInput, readLine } = Effect.serviceConstants(MockTerminal) +export const { inputKey, inputText } = Effect.serviceFunctions(MockTerminal) + +// ============================================================================= +// Utilities +// ============================================================================= + +const shouldQuit = (input: Terminal.UserInput): boolean => + input.key.ctrl && (input.key.name === "c" || input.key.name === "d") + +const toUserInput = ( + key: string, + modifiers: Partial = {} +): Terminal.UserInput => { + const { ctrl = false, meta = false, shift = false } = modifiers + return { + input: Option.some(key), + key: { name: key, ctrl, meta, shift } + } +} diff --git a/repos/effect/packages/cli/test/snapshots/bash-completions b/repos/effect/packages/cli/test/snapshots/bash-completions new file mode 100644 index 0000000..fdb68cd --- /dev/null +++ b/repos/effect/packages/cli/test/snapshots/bash-completions @@ -0,0 +1,91 @@ +[ + "function _forge_bash_completions() {", + " local i cur prev opts cmd", + " COMPREPLY=()", + " cur="${COMP_WORDS[COMP_CWORD]}"", + " prev="${COMP_WORDS[COMP_CWORD-1]}"", + " cmd=""", + " opts=""", + " for i in "${COMP_WORDS[@]}"; do", + " case "${cmd},${i}" in", + " ",$1")", + " cmd="forge"", + " ;;", + " forge,cache)", + " cmd="forge__cache"", + " ;;", + " forge,cache,clean)", + " cmd="forge__cache__clean"", + " ;;", + " forge,cache,ls)", + " cmd="forge__cache__ls"", + " ;;", + " *)", + " ;;", + " esac", + " done", + " case "${cmd}" in", + " forge)", + " opts="-h --completions --log-level --help --wizard --version cache"", + " if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then", + " COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )", + " return 0", + " fi", + " case "${prev}" in", + " *)", + " COMPREPLY=()", + " ;;", + " esac", + " COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )", + " return 0", + " ;;", + " forge__cache)", + " opts="-h --verbose --completions --log-level --help --wizard --version clean ls"", + " if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then", + " COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )", + " return 0", + " fi", + " case "${prev}" in", + " --verbose)", + " COMPREPLY=( "${cur}" )", + " return 0", + " ;;", + " *)", + " COMPREPLY=()", + " ;;", + " esac", + " COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )", + " return 0", + " ;;", + " forge__cache__clean)", + " opts="-h --completions --log-level --help --wizard --version"", + " if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then", + " COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )", + " return 0", + " fi", + " case "${prev}" in", + " *)", + " COMPREPLY=()", + " ;;", + " esac", + " COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )", + " return 0", + " ;;", + " forge__cache__ls)", + " opts="-h --completions --log-level --help --wizard --version"", + " if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then", + " COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )", + " return 0", + " fi", + " case "${prev}" in", + " *)", + " COMPREPLY=()", + " ;;", + " esac", + " COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )", + " return 0", + " ;;", + " esac", + "}", + "complete -F _forge_bash_completions -o nosort -o bashdefault -o default forge", +] \ No newline at end of file diff --git a/repos/effect/packages/cli/test/snapshots/fish-completions b/repos/effect/packages/cli/test/snapshots/fish-completions new file mode 100644 index 0000000..348e465 --- /dev/null +++ b/repos/effect/packages/cli/test/snapshots/fish-completions @@ -0,0 +1,26 @@ +[ + "complete -c forge -n "__fish_use_subcommand" -l completions -r -f -a "{sh'',bash'',fish'',zsh''}" -d 'Generate a completion script for a specific shell.'", + "complete -c forge -n "__fish_use_subcommand" -l log-level -r -f -a "{all'',trace'',debug'',info'',warning'',error'',fatal'',none''}" -d 'Sets the minimum log level for a command.'", + "complete -c forge -n "__fish_use_subcommand" -s h -l help -d 'Show the help documentation for a command.'", + "complete -c forge -n "__fish_use_subcommand" -l wizard -d 'Start wizard mode for a command.'", + "complete -c forge -n "__fish_use_subcommand" -l version -d 'Show the version of the application.'", + "complete -c forge -n "__fish_use_subcommand" -f -a "cache" -d 'The cache command does cache things'", + "complete -c forge -n "__fish_seen_subcommand_from cache; and not __fish_seen_subcommand_from clean; and not __fish_seen_subcommand_from ls" -l completions -r -f -a "{sh'',bash'',fish'',zsh''}" -d 'Generate a completion script for a specific shell.'", + "complete -c forge -n "__fish_seen_subcommand_from cache; and not __fish_seen_subcommand_from clean; and not __fish_seen_subcommand_from ls" -l log-level -r -f -a "{all'',trace'',debug'',info'',warning'',error'',fatal'',none''}" -d 'Sets the minimum log level for a command.'", + "complete -c forge -n "__fish_seen_subcommand_from cache; and not __fish_seen_subcommand_from clean; and not __fish_seen_subcommand_from ls" -s h -l help -d 'Show the help documentation for a command.'", + "complete -c forge -n "__fish_seen_subcommand_from cache; and not __fish_seen_subcommand_from clean; and not __fish_seen_subcommand_from ls" -l wizard -d 'Start wizard mode for a command.'", + "complete -c forge -n "__fish_seen_subcommand_from cache; and not __fish_seen_subcommand_from clean; and not __fish_seen_subcommand_from ls" -l version -d 'Show the version of the application.'", + "complete -c forge -n "__fish_seen_subcommand_from cache; and not __fish_seen_subcommand_from clean; and not __fish_seen_subcommand_from ls" -l verbose -d 'Output in verbose mode'", + "complete -c forge -n "__fish_seen_subcommand_from cache; and not __fish_seen_subcommand_from clean; and not __fish_seen_subcommand_from ls" -f -a "clean"", + "complete -c forge -n "__fish_seen_subcommand_from cache; and not __fish_seen_subcommand_from clean; and not __fish_seen_subcommand_from ls" -f -a "ls"", + "complete -c forge -n "__fish_seen_subcommand_from cache; and __fish_seen_subcommand_from clean" -l completions -r -f -a "{sh'',bash'',fish'',zsh''}" -d 'Generate a completion script for a specific shell.'", + "complete -c forge -n "__fish_seen_subcommand_from cache; and __fish_seen_subcommand_from clean" -l log-level -r -f -a "{all'',trace'',debug'',info'',warning'',error'',fatal'',none''}" -d 'Sets the minimum log level for a command.'", + "complete -c forge -n "__fish_seen_subcommand_from cache; and __fish_seen_subcommand_from clean" -s h -l help -d 'Show the help documentation for a command.'", + "complete -c forge -n "__fish_seen_subcommand_from cache; and __fish_seen_subcommand_from clean" -l wizard -d 'Start wizard mode for a command.'", + "complete -c forge -n "__fish_seen_subcommand_from cache; and __fish_seen_subcommand_from clean" -l version -d 'Show the version of the application.'", + "complete -c forge -n "__fish_seen_subcommand_from cache; and __fish_seen_subcommand_from ls" -l completions -r -f -a "{sh'',bash'',fish'',zsh''}" -d 'Generate a completion script for a specific shell.'", + "complete -c forge -n "__fish_seen_subcommand_from cache; and __fish_seen_subcommand_from ls" -l log-level -r -f -a "{all'',trace'',debug'',info'',warning'',error'',fatal'',none''}" -d 'Sets the minimum log level for a command.'", + "complete -c forge -n "__fish_seen_subcommand_from cache; and __fish_seen_subcommand_from ls" -s h -l help -d 'Show the help documentation for a command.'", + "complete -c forge -n "__fish_seen_subcommand_from cache; and __fish_seen_subcommand_from ls" -l wizard -d 'Start wizard mode for a command.'", + "complete -c forge -n "__fish_seen_subcommand_from cache; and __fish_seen_subcommand_from ls" -l version -d 'Show the version of the application.'", +] \ No newline at end of file diff --git a/repos/effect/packages/cli/test/snapshots/help-output/args-default-object b/repos/effect/packages/cli/test/snapshots/help-output/args-default-object new file mode 100644 index 0000000..c60e9c8 --- /dev/null +++ b/repos/effect/packages/cli/test/snapshots/help-output/args-default-object @@ -0,0 +1,31 @@ +{ + "_tag": "DescriptionList", + "definitions": [ + [ + { + "_tag": "Weak", + "value": { + "_tag": "Text", + "value": "", + }, + }, + { + "_tag": "Sequence", + "left": { + "_tag": "Paragraph", + "value": { + "_tag": "Text", + "value": "A user-defined piece of text.", + }, + }, + "right": { + "_tag": "Paragraph", + "value": { + "_tag": "Text", + "value": "This setting is optional. Defaults to: {"key":"value","number":456}", + }, + }, + }, + ], + ], +} \ No newline at end of file diff --git a/repos/effect/packages/cli/test/snapshots/help-output/args-default-primitive b/repos/effect/packages/cli/test/snapshots/help-output/args-default-primitive new file mode 100644 index 0000000..7cbefb3 --- /dev/null +++ b/repos/effect/packages/cli/test/snapshots/help-output/args-default-primitive @@ -0,0 +1,31 @@ +{ + "_tag": "DescriptionList", + "definitions": [ + [ + { + "_tag": "Weak", + "value": { + "_tag": "Text", + "value": "", + }, + }, + { + "_tag": "Sequence", + "left": { + "_tag": "Paragraph", + "value": { + "_tag": "Text", + "value": "An integer.", + }, + }, + "right": { + "_tag": "Paragraph", + "value": { + "_tag": "Text", + "value": "This setting is optional. Defaults to: 123", + }, + }, + }, + ], + ], +} \ No newline at end of file diff --git a/repos/effect/packages/cli/test/snapshots/help-output/args-no-default b/repos/effect/packages/cli/test/snapshots/help-output/args-no-default new file mode 100644 index 0000000..e656204 --- /dev/null +++ b/repos/effect/packages/cli/test/snapshots/help-output/args-no-default @@ -0,0 +1,31 @@ +{ + "_tag": "DescriptionList", + "definitions": [ + [ + { + "_tag": "Weak", + "value": { + "_tag": "Text", + "value": "", + }, + }, + { + "_tag": "Sequence", + "left": { + "_tag": "Paragraph", + "value": { + "_tag": "Text", + "value": "A user-defined piece of text.", + }, + }, + "right": { + "_tag": "Paragraph", + "value": { + "_tag": "Text", + "value": "This setting is optional.", + }, + }, + }, + ], + ], +} \ No newline at end of file diff --git a/repos/effect/packages/cli/test/snapshots/help-output/options-default-object b/repos/effect/packages/cli/test/snapshots/help-output/options-default-object new file mode 100644 index 0000000..ced106c --- /dev/null +++ b/repos/effect/packages/cli/test/snapshots/help-output/options-default-object @@ -0,0 +1,42 @@ +{ + "_tag": "DescriptionList", + "definitions": [ + [ + { + "_tag": "Sequence", + "left": { + "_tag": "Text", + "value": "--config", + }, + "right": { + "_tag": "Sequence", + "left": { + "_tag": "Text", + "value": " ", + }, + "right": { + "_tag": "Text", + "value": "text", + }, + }, + }, + { + "_tag": "Sequence", + "left": { + "_tag": "Paragraph", + "value": { + "_tag": "Text", + "value": "A user-defined piece of text.", + }, + }, + "right": { + "_tag": "Paragraph", + "value": { + "_tag": "Text", + "value": "This setting is optional. Defaults to: {"key":"value","number":456}", + }, + }, + }, + ], + ], +} \ No newline at end of file diff --git a/repos/effect/packages/cli/test/snapshots/help-output/options-default-primitive b/repos/effect/packages/cli/test/snapshots/help-output/options-default-primitive new file mode 100644 index 0000000..ecf133c --- /dev/null +++ b/repos/effect/packages/cli/test/snapshots/help-output/options-default-primitive @@ -0,0 +1,42 @@ +{ + "_tag": "DescriptionList", + "definitions": [ + [ + { + "_tag": "Sequence", + "left": { + "_tag": "Text", + "value": "--value", + }, + "right": { + "_tag": "Sequence", + "left": { + "_tag": "Text", + "value": " ", + }, + "right": { + "_tag": "Text", + "value": "integer", + }, + }, + }, + { + "_tag": "Sequence", + "left": { + "_tag": "Paragraph", + "value": { + "_tag": "Text", + "value": "An integer.", + }, + }, + "right": { + "_tag": "Paragraph", + "value": { + "_tag": "Text", + "value": "This setting is optional. Defaults to: 123", + }, + }, + }, + ], + ], +} \ No newline at end of file diff --git a/repos/effect/packages/cli/test/snapshots/help-output/options-no-default b/repos/effect/packages/cli/test/snapshots/help-output/options-no-default new file mode 100644 index 0000000..f96dffb --- /dev/null +++ b/repos/effect/packages/cli/test/snapshots/help-output/options-no-default @@ -0,0 +1,42 @@ +{ + "_tag": "DescriptionList", + "definitions": [ + [ + { + "_tag": "Sequence", + "left": { + "_tag": "Text", + "value": "--name", + }, + "right": { + "_tag": "Sequence", + "left": { + "_tag": "Text", + "value": " ", + }, + "right": { + "_tag": "Text", + "value": "text", + }, + }, + }, + { + "_tag": "Sequence", + "left": { + "_tag": "Paragraph", + "value": { + "_tag": "Text", + "value": "A user-defined piece of text.", + }, + }, + "right": { + "_tag": "Paragraph", + "value": { + "_tag": "Text", + "value": "This setting is optional.", + }, + }, + }, + ], + ], +} \ No newline at end of file diff --git a/repos/effect/packages/cli/test/snapshots/zsh-completions b/repos/effect/packages/cli/test/snapshots/zsh-completions new file mode 100644 index 0000000..4361b12 --- /dev/null +++ b/repos/effect/packages/cli/test/snapshots/zsh-completions @@ -0,0 +1,112 @@ +[ + "#compdef forge", + "", + "autoload -U is-at-least", + "", + "function _forge_zsh_completions() {", + " typeset -A opt_args", + " typeset -a _arguments_options", + " local ret=1", + "", + " if is-at-least 5.2; then", + " _arguments_options=(-s -S -C)", + " else", + " _arguments_options=(-s -C)", + " fi", + "", + " local context curcontext="$curcontext" state line", + " _arguments "${_arguments_options[@]}" \", + " '--completions[Generate a completion script for a specific shell.]:CHOICE:(sh bash fish zsh)' \", + " '--log-level[Sets the minimum log level for a command.]:CHOICE:(all trace debug info warning error fatal none)' \", + " '-h[Show the help documentation for a command.]' \", + " '--help[Show the help documentation for a command.]' \", + " '--wizard[Start wizard mode for a command.]' \", + " '--version[Show the version of the application.]' \", + " ":: :_forge_commands" \", + " "*::: :->forge" \", + " && ret=0", + " case $state in", + " (forge)", + " words=($line[1] "${words[@]}")", + " (( CURRENT += 1 ))", + " curcontext="${curcontext%:*:*}:forge-command-$line[1]:"", + " case $line[1] in", + " (cache)", + " _arguments "${_arguments_options[@]}" \", + " '--completions[Generate a completion script for a specific shell.]:CHOICE:(sh bash fish zsh)' \", + " '--log-level[Sets the minimum log level for a command.]:CHOICE:(all trace debug info warning error fatal none)' \", + " '-h[Show the help documentation for a command.]' \", + " '--help[Show the help documentation for a command.]' \", + " '--wizard[Start wizard mode for a command.]' \", + " '--version[Show the version of the application.]' \", + " '--verbose[Output in verbose mode]' \", + " ":: :_forge__cache_commands" \", + " "*::: :->cache" \", + " && ret=0", + " case $state in", + " (cache)", + " words=($line[1] "${words[@]}")", + " (( CURRENT += 1 ))", + " curcontext="${curcontext%:*:*}:forge-cache-command-$line[1]:"", + " case $line[1] in", + " (clean)", + " _arguments "${_arguments_options[@]}" \", + " '--completions[Generate a completion script for a specific shell.]:CHOICE:(sh bash fish zsh)' \", + " '--log-level[Sets the minimum log level for a command.]:CHOICE:(all trace debug info warning error fatal none)' \", + " '-h[Show the help documentation for a command.]' \", + " '--help[Show the help documentation for a command.]' \", + " '--wizard[Start wizard mode for a command.]' \", + " '--version[Show the version of the application.]' \", + " && ret=0", + " ;;", + " (ls)", + " _arguments "${_arguments_options[@]}" \", + " '--completions[Generate a completion script for a specific shell.]:CHOICE:(sh bash fish zsh)' \", + " '--log-level[Sets the minimum log level for a command.]:CHOICE:(all trace debug info warning error fatal none)' \", + " '-h[Show the help documentation for a command.]' \", + " '--help[Show the help documentation for a command.]' \", + " '--wizard[Start wizard mode for a command.]' \", + " '--version[Show the version of the application.]' \", + " && ret=0", + " ;;", + " esac", + " ;;", + " esac", + " ;;", + " esac", + " ;;", + " esac", + "}", + "", + "(( $+functions[_forge_commands] )) ||", + "_forge_commands() {", + " local commands; commands=( + 'cache:The cache command does cache things' \ + )", + " _describe -t commands 'forge commands' commands "$@"", + "}", + "(( $+functions[_forge__cache_commands] )) ||", + "_forge__cache_commands() {", + " local commands; commands=( + 'clean:' \ + 'ls:' \ + )", + " _describe -t commands 'forge cache commands' commands "$@"", + "}", + "(( $+functions[_forge__cache__clean_commands] )) ||", + "_forge__cache__clean_commands() {", + " local commands; commands=()", + " _describe -t commands 'forge cache clean commands' commands "$@"", + "}", + "(( $+functions[_forge__cache__ls_commands] )) ||", + "_forge__cache__ls_commands() {", + " local commands; commands=()", + " _describe -t commands 'forge cache ls commands' commands "$@"", + "}", + "", + "if [ "$funcstack[1]" = "_forge_zsh_completions" ]; then", + " _forge_zsh_completions "$@"", + "else", + " compdef _forge_zsh_completions forge", + "fi", +] \ No newline at end of file diff --git a/repos/effect/packages/cli/test/utils/grep.ts b/repos/effect/packages/cli/test/utils/grep.ts new file mode 100644 index 0000000..5188c87 --- /dev/null +++ b/repos/effect/packages/cli/test/utils/grep.ts @@ -0,0 +1,18 @@ +import * as Args from "@effect/cli/Args" +import * as Descriptor from "@effect/cli/CommandDescriptor" +import * as Options from "@effect/cli/Options" + +const afterFlag = Options.integer("after").pipe(Options.withAlias("A")) +const beforeFlag = Options.integer("before").pipe(Options.withAlias("B")) +export const options: Options.Options<[number, number]> = Options.all([ + afterFlag, + beforeFlag +]) + +export const args: Args.Args = Args.text() + +export const command: Descriptor.Command<{ + readonly name: "grep" + readonly options: [number, number] + readonly args: string +}> = Descriptor.make("grep", options, args) diff --git a/repos/effect/packages/cli/test/utils/tail.ts b/repos/effect/packages/cli/test/utils/tail.ts new file mode 100644 index 0000000..6f04e43 --- /dev/null +++ b/repos/effect/packages/cli/test/utils/tail.ts @@ -0,0 +1,15 @@ +import * as Args from "@effect/cli/Args" +import * as Descriptor from "@effect/cli/CommandDescriptor" +import * as Options from "@effect/cli/Options" + +export const options: Options.Options = Options.integer("n").pipe( + Options.withDefault(10) +) + +export const args: Args.Args = Args.file({ name: "file" }) + +export const command: Descriptor.Command<{ + readonly name: "tail" + readonly options: number + readonly args: string +}> = Descriptor.make("tail", options, args) diff --git a/repos/effect/packages/cli/test/utils/wc.ts b/repos/effect/packages/cli/test/utils/wc.ts new file mode 100644 index 0000000..86a3417 --- /dev/null +++ b/repos/effect/packages/cli/test/utils/wc.ts @@ -0,0 +1,22 @@ +import * as Args from "@effect/cli/Args" +import * as Descriptor from "@effect/cli/CommandDescriptor" +import * as Options from "@effect/cli/Options" + +const bytesFlag = Options.boolean("c") +const linesFlag = Options.boolean("l") +const wordsFlag = Options.boolean("w") +const charFlag = Options.boolean("m", { ifPresent: false }) +export const options: Options.Options<[boolean, boolean, boolean, boolean]> = Options.all([ + bytesFlag, + linesFlag, + wordsFlag, + charFlag +]) + +export const args: Args.Args> = Args.repeated(Args.file({ name: "files" })) + +export const command: Descriptor.Command<{ + readonly name: "wc" + readonly options: [boolean, boolean, boolean, boolean] + readonly args: ReadonlyArray +}> = Descriptor.make("wc", options, args) diff --git a/repos/effect/packages/cli/tsconfig.build.json b/repos/effect/packages/cli/tsconfig.build.json new file mode 100644 index 0000000..1a36eb8 --- /dev/null +++ b/repos/effect/packages/cli/tsconfig.build.json @@ -0,0 +1,17 @@ +{ + "extends": "./tsconfig.src.json", + "references": [ + { "path": "../effect/tsconfig.build.json" }, + { "path": "../printer/tsconfig.build.json" }, + { "path": "../printer-ansi/tsconfig.build.json" }, + { "path": "../platform/tsconfig.build.json" }, + { "path": "../platform-node/tsconfig.build.json" } + ], + "compilerOptions": { + "types": ["node"], + "tsBuildInfoFile": ".tsbuildinfo/build.tsbuildinfo", + "outDir": "build/esm", + "declarationDir": "build/dts", + "stripInternal": true + } +} diff --git a/repos/effect/packages/cli/tsconfig.examples.json b/repos/effect/packages/cli/tsconfig.examples.json new file mode 100644 index 0000000..c98fcea --- /dev/null +++ b/repos/effect/packages/cli/tsconfig.examples.json @@ -0,0 +1,14 @@ +{ + "extends": "../../tsconfig.base.json", + "include": ["examples"], + "references": [ + { "path": "tsconfig.build.json" }, + { "path": "../platform-node/tsconfig.build.json" } + ], + "compilerOptions": { + "types": ["node"], + "tsBuildInfoFile": ".tsbuildinfo/examples.tsbuildinfo", + "rootDir": "examples", + "noEmit": true + } +} diff --git a/repos/effect/packages/cli/tsconfig.json b/repos/effect/packages/cli/tsconfig.json new file mode 100644 index 0000000..3edbf6b --- /dev/null +++ b/repos/effect/packages/cli/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.base.json", + "include": [], + "references": [ + { "path": "tsconfig.src.json" }, + { "path": "tsconfig.test.json" }, + { "path": "tsconfig.examples.json" } + ] +} diff --git a/repos/effect/packages/cli/tsconfig.src.json b/repos/effect/packages/cli/tsconfig.src.json new file mode 100644 index 0000000..74de508 --- /dev/null +++ b/repos/effect/packages/cli/tsconfig.src.json @@ -0,0 +1,16 @@ +{ + "extends": "../../tsconfig.base.json", + "include": ["src"], + "references": [ + { "path": "../effect/tsconfig.src.json" }, + { "path": "../printer/tsconfig.src.json" }, + { "path": "../printer-ansi/tsconfig.src.json" }, + { "path": "../platform/tsconfig.src.json" } + ], + "compilerOptions": { + "types": ["node"], + "outDir": "build/src", + "tsBuildInfoFile": ".tsbuildinfo/src.tsbuildinfo", + "rootDir": "src" + } +} diff --git a/repos/effect/packages/cli/tsconfig.test.json b/repos/effect/packages/cli/tsconfig.test.json new file mode 100644 index 0000000..0fc1f0e --- /dev/null +++ b/repos/effect/packages/cli/tsconfig.test.json @@ -0,0 +1,15 @@ +{ + "extends": "../../tsconfig.base.json", + "include": ["test"], + "references": [ + { "path": "tsconfig.src.json" }, + { "path": "../vitest/tsconfig.src.json" }, + { "path": "../platform-node/tsconfig.src.json" } + ], + "compilerOptions": { + "types": ["node"], + "tsBuildInfoFile": ".tsbuildinfo/test.tsbuildinfo", + "rootDir": "test", + "noEmit": true + } +} diff --git a/repos/effect/packages/cli/vitest.config.ts b/repos/effect/packages/cli/vitest.config.ts new file mode 100644 index 0000000..578d066 --- /dev/null +++ b/repos/effect/packages/cli/vitest.config.ts @@ -0,0 +1,6 @@ +import { mergeConfig, type ViteUserConfig } from "vitest/config" +import shared from "../../vitest.shared.js" + +const config: ViteUserConfig = {} + +export default mergeConfig(shared, config) diff --git a/repos/effect/packages/cluster/CHANGELOG.md b/repos/effect/packages/cluster/CHANGELOG.md new file mode 100644 index 0000000..8310d71 --- /dev/null +++ b/repos/effect/packages/cluster/CHANGELOG.md @@ -0,0 +1,3052 @@ +# @effect/cluster + +## 0.58.3 + +### Patch Changes + +- [#6217](https://github.com/Effect-TS/effect/pull/6217) [`54e61b3`](https://github.com/Effect-TS/effect/commit/54e61b3e08ab30a52fb20eba3104a83b99f443fa) Thanks @tim-smart! - Allow Kubernetes pod condition `lastTransitionTime` values to be null. + +## 0.58.2 + +### Patch Changes + +- [#6110](https://github.com/Effect-TS/effect/pull/6110) [`0fac630`](https://github.com/Effect-TS/effect/commit/0fac630b27095ffbfa6c48851087950ddc29cda0) Thanks @mitre88! - fix: correct typos in source code (receive, separate) + +- Updated dependencies [[`74f3267`](https://github.com/Effect-TS/effect/commit/74f3267a6cc7ed7818c4c34cc1232f7cfc7d3339), [`518d0e3`](https://github.com/Effect-TS/effect/commit/518d0e3f4879be6d9d9a7fa137a1820604bb3ea7), [`c016642`](https://github.com/Effect-TS/effect/commit/c0166426f80b7eb8e7f7d3aecc95dcd4fdb5cb55), [`0fac630`](https://github.com/Effect-TS/effect/commit/0fac630b27095ffbfa6c48851087950ddc29cda0), [`e2374c2`](https://github.com/Effect-TS/effect/commit/e2374c20ce699d9f5340baf744cf1bd67bb220a0)]: + - effect@3.21.2 + - @effect/platform@0.96.1 + - @effect/rpc@0.75.1 + - @effect/sql@0.51.1 + +## 0.58.1 + +### Patch Changes + +- [#6183](https://github.com/Effect-TS/effect/pull/6183) [`4708bb8`](https://github.com/Effect-TS/effect/commit/4708bb8e327f24651ab9072221289d8214c4e2df) Thanks @tim-smart! - backport cluster serialization fix for notify path + +- Updated dependencies [[`f99048e`](https://github.com/Effect-TS/effect/commit/f99048e9f4b89ce1afe31e1827dee5d751ddaa5b)]: + - effect@3.21.1 + +## 0.58.0 + +### Patch Changes + +- Updated dependencies [[`f7bb09b`](https://github.com/Effect-TS/effect/commit/f7bb09b022f195d1f2b3c23d49e74b011ec5d109), [`bd7552a`](https://github.com/Effect-TS/effect/commit/bd7552a19cc0ed575507ac6cc0879a57e24ebd31), [`ad1a7eb`](https://github.com/Effect-TS/effect/commit/ad1a7eb7f6bebaf91c80be2443ac0439226d0098), [`0d32048`](https://github.com/Effect-TS/effect/commit/0d32048f9836e2b23a6ba3ec5f43f0a000bb92fb), [`0d32048`](https://github.com/Effect-TS/effect/commit/0d32048f9836e2b23a6ba3ec5f43f0a000bb92fb)]: + - effect@3.21.0 + - @effect/platform@0.96.0 + - @effect/rpc@0.75.0 + - @effect/sql@0.51.0 + - @effect/workflow@0.18.0 + +## 0.57.0 + +### Patch Changes + +- Updated dependencies [[`fc82e81`](https://github.com/Effect-TS/effect/commit/fc82e81448bd9136a37580139ce46a2c61b11b54), [`82996bc`](https://github.com/Effect-TS/effect/commit/82996bce8debffcb44feb98bb862cf2662bd56b7), [`4d97a61`](https://github.com/Effect-TS/effect/commit/4d97a61a15b9dd6a0eece65b8f0c035e16d42ada), [`f6b0960`](https://github.com/Effect-TS/effect/commit/f6b0960bf3184109920dfed16ee7dfd7d67bc0f2), [`8798a84`](https://github.com/Effect-TS/effect/commit/8798a843218e6c0c0d3a8eee83360880e370b4da)]: + - effect@3.20.0 + - @effect/platform@0.95.0 + - @effect/rpc@0.74.0 + - @effect/sql@0.50.0 + - @effect/workflow@0.17.0 + +## 0.56.4 + +### Patch Changes + +- [#6056](https://github.com/Effect-TS/effect/pull/6056) [`52cd531`](https://github.com/Effect-TS/effect/commit/52cd53115fe14e2b9c36dd0d51eb490aead74ee6) Thanks @0xh3x! - use HttpLayerRouter variants for route registration in HttpRunner + +- Updated dependencies [[`d67c708`](https://github.com/Effect-TS/effect/commit/d67c7089ba8616b2d48ef7324312267a2a6f310a), [`a8c436f`](https://github.com/Effect-TS/effect/commit/a8c436f7004cc2a8ce2daec589ea7256b91c324f), [`598ff76`](https://github.com/Effect-TS/effect/commit/598ff7642fdee7f3379bca49e378a0e9647bbe75)]: + - @effect/platform@0.94.5 + - effect@3.19.17 + - @effect/rpc@0.73.1 + +## 0.56.3 + +### Patch Changes + +- [#6033](https://github.com/Effect-TS/effect/pull/6033) [`740a912`](https://github.com/Effect-TS/effect/commit/740a912142c2578defcf3e1e7d449535b074bd61) Thanks @tim-smart! - Add `PgClient.fromPool` and `PgClient.layerFromPool`. + +- Updated dependencies [[`22d9d27`](https://github.com/Effect-TS/effect/commit/22d9d27bc007db86d9e4748c17324fab5f950c7d)]: + - @effect/platform@0.94.4 + +## 0.56.2 + +### Patch Changes + +- [#6031](https://github.com/Effect-TS/effect/pull/6031) [`1781244`](https://github.com/Effect-TS/effect/commit/17812444c6c0d8f19f9fbc85d82f911dff5523ab) Thanks @tim-smart! - backport effect v4 MessageStorage improvements + +## 0.56.1 + +### Patch Changes + +- [#5946](https://github.com/Effect-TS/effect/pull/5946) [`e88f289`](https://github.com/Effect-TS/effect/commit/e88f289c1d9fd4ec584ee793c264597aa84c9352) Thanks @vic-mo! - Fix HttpRunner double-slash routing + +## 0.56.0 + +### Patch Changes + +- Updated dependencies [[`77eeb86`](https://github.com/Effect-TS/effect/commit/77eeb86ddf208e51ec25932af83d52d3b4700371), [`ff7053f`](https://github.com/Effect-TS/effect/commit/ff7053f6d8508567b6145239f97aacc5773b0c53), [`287c32c`](https://github.com/Effect-TS/effect/commit/287c32c9f10da8e96f2b9ef8424316189d9ad4b3)]: + - effect@3.19.13 + - @effect/platform@0.94.0 + - @effect/rpc@0.73.0 + - @effect/sql@0.49.0 + - @effect/workflow@0.16.0 + +## 0.55.0 + +### Patch Changes + +- Updated dependencies [[`811852a`](https://github.com/Effect-TS/effect/commit/811852a61868136bb7b3367450f02e5a8fb8a3f9), [`811852a`](https://github.com/Effect-TS/effect/commit/811852a61868136bb7b3367450f02e5a8fb8a3f9)]: + - @effect/sql@0.48.6 + - @effect/workflow@0.15.0 + +## 0.54.0 + +### Patch Changes + +- [#5827](https://github.com/Effect-TS/effect/pull/5827) [`7bd4e82`](https://github.com/Effect-TS/effect/commit/7bd4e827bc246a39d71b48a105e0853352efdc3b) Thanks @tim-smart! - add TestRunner & SingleRunner modules + - `TestRunner` allows you to run a in-memory cluster for testing purposes. + - `SingleRunner` allows you to run a single node cluster simple deployment + scenarios. + - Message storage is backed by a SQL database + - Multiple nodes are not supported + +- Updated dependencies [[`7bd4e82`](https://github.com/Effect-TS/effect/commit/7bd4e827bc246a39d71b48a105e0853352efdc3b)]: + - @effect/workflow@0.14.0 + +## 0.53.6 + +### Patch Changes + +- [#5793](https://github.com/Effect-TS/effect/pull/5793) [`a2d965d`](https://github.com/Effect-TS/effect/commit/a2d965d2a22dcc018f81dbbcd55bfe33088d9411) Thanks @tim-smart! - allow advisory locks to be disabled + +- Updated dependencies [[`8ebd29e`](https://github.com/Effect-TS/effect/commit/8ebd29ec10976222c200901d9b72779af743e6d5)]: + - @effect/platform@0.93.4 + +## 0.53.5 + +### Patch Changes + +- [#5785](https://github.com/Effect-TS/effect/pull/5785) [`47bf3d2`](https://github.com/Effect-TS/effect/commit/47bf3d2324ab914f6a33e95cda7ff2aac01519a6) Thanks @tim-smart! - immediately set entity keep alive in EntityResource + +## 0.53.4 + +### Patch Changes + +- [#5783](https://github.com/Effect-TS/effect/pull/5783) [`8b879fb`](https://github.com/Effect-TS/effect/commit/8b879fb3b886a7262c9c8d9b2050cc128c5eb6f8) Thanks @tim-smart! - add EntityResource.makeK8sPod + +## 0.53.3 + +### Patch Changes + +- [#5781](https://github.com/Effect-TS/effect/pull/5781) [`df69cb5`](https://github.com/Effect-TS/effect/commit/df69cb5ed590045b18d1527162518ea0197ddd2e) Thanks @tim-smart! - provide seperate close scope to EntityResource via context + +## 0.53.2 + +### Patch Changes + +- [#5778](https://github.com/Effect-TS/effect/pull/5778) [`af7916a`](https://github.com/Effect-TS/effect/commit/af7916a3f00acdfc8ce451eabd3f5fb02914d0bb) Thanks @tim-smart! - fix postgres unprocessed message ordering + +- [#5778](https://github.com/Effect-TS/effect/pull/5778) [`af7916a`](https://github.com/Effect-TS/effect/commit/af7916a3f00acdfc8ce451eabd3f5fb02914d0bb) Thanks @tim-smart! - add @effect/cluster EntityResource module + + A `EntityResource` is a resource that can be acquired inside a cluster + entity, which will keep the entity alive even across restarts. + + The resource will only be fully released when the idle time to live is + reached, or when the `close` effect is called. + + By default, the `idleTimeToLive` is infinite, meaning the resource will only + be released when `close` is called. + + ```ts + import { Entity, EntityResource } from "@effect/cluster" + import { Rpc } from "@effect/rpc" + import { Effect } from "effect" + + const EntityA = Entity.make("EntityA", [Rpc.make("method")]) + + export const EntityALayer = EntityA.toLayer( + Effect.gen(function* () { + // When the entity receives a message, it will first acquire the resource + // + // If the entity restarts, the resource will be re-acquired in the new + // instance. + // + // It will only be released when the idle TTL is reached, or when the + // `close` effect is called. + const resource = yield* EntityResource.make({ + acquire: Effect.acquireRelease( + Effect.logInfo("Acquiring Entity resource"), + () => Effect.logInfo("Releasing Entity resource") + ), + // If the resource is not used for 10 minutes, it will be released and the + // entity will be allowed to shut down. + idleTimeToLive: "10 minutes" + }) + + return EntityA.of({ + method: Effect.fnUntraced(function* () { + yield* Effect.logInfo("EntityA.method called") + // To access the resource, use `resource.get` inside an Effect.scoped + yield* resource.get + }, Effect.scoped) + }) + }), + { + // After the resource is released, if the entity is not used for 1 minute, + // the entity will be shut down. + maxIdleTime: "1 minute" + } + ) + ``` + +- Updated dependencies [[`af7916a`](https://github.com/Effect-TS/effect/commit/af7916a3f00acdfc8ce451eabd3f5fb02914d0bb)]: + - effect@3.19.6 + +## 0.53.1 + +### Patch Changes + +- [#5775](https://github.com/Effect-TS/effect/pull/5775) [`b92632d`](https://github.com/Effect-TS/effect/commit/b92632ded7a976c9279985013c295260c6603b14) Thanks @tim-smart! - ensure ClusterCron's can be resumed if re-added + +## 0.53.0 + +### Patch Changes + +- [#5771](https://github.com/Effect-TS/effect/pull/5771) [`794c790`](https://github.com/Effect-TS/effect/commit/794c790d736f62784bff800fda5a656026d93749) Thanks @tim-smart! - backport Entity keep alive from effect 4.0 + +- [#5771](https://github.com/Effect-TS/effect/pull/5771) [`794c790`](https://github.com/Effect-TS/effect/commit/794c790d736f62784bff800fda5a656026d93749) Thanks @tim-smart! - add WorkflowEngine.makeUnsafe, which abstracts the serialization boundary + +- Updated dependencies [[`794c790`](https://github.com/Effect-TS/effect/commit/794c790d736f62784bff800fda5a656026d93749), [`794c790`](https://github.com/Effect-TS/effect/commit/794c790d736f62784bff800fda5a656026d93749), [`079975c`](https://github.com/Effect-TS/effect/commit/079975c69d80c62461da5c51fe89e02c44dfa2ea), [`794c790`](https://github.com/Effect-TS/effect/commit/794c790d736f62784bff800fda5a656026d93749), [`62f7636`](https://github.com/Effect-TS/effect/commit/62f76361ee01ed816687774c5302e7f8c5ff6a42)]: + - @effect/rpc@0.72.2 + - @effect/workflow@0.13.0 + - effect@3.19.5 + +## 0.52.11 + +### Patch Changes + +- [#5765](https://github.com/Effect-TS/effect/pull/5765) [`f38c215`](https://github.com/Effect-TS/effect/commit/f38c215f99920c9ce8809a01ddb38b1c96d18d44) Thanks @tim-smart! - add entityRegistrationTimeout to ShardingConfig + +## 0.52.10 + +### Patch Changes + +- [#5748](https://github.com/Effect-TS/effect/pull/5748) [`b93fc63`](https://github.com/Effect-TS/effect/commit/b93fc638da9a4e840a02417e422c34c8a735413c) Thanks @tim-smart! - don't resume parent workflow when suspending on failure + +## 0.52.9 + +### Patch Changes + +- [#5726](https://github.com/Effect-TS/effect/pull/5726) [`d34476e`](https://github.com/Effect-TS/effect/commit/d34476eebf1b61ce80b2127689d95d3b0c703aab) Thanks @tim-smart! - support and test against vitess for SqlRunnerStorage + +## 0.52.8 + +### Patch Changes + +- [#5719](https://github.com/Effect-TS/effect/pull/5719) [`3689a18`](https://github.com/Effect-TS/effect/commit/3689a183c5ace2fc31faec1910347a12b0e70364) Thanks @tim-smart! - re-use resume parent messages for child workflows + +## 0.52.7 + +### Patch Changes + +- [#5716](https://github.com/Effect-TS/effect/pull/5716) [`629b98d`](https://github.com/Effect-TS/effect/commit/629b98d3b696df6c928f0930b867ca3f27829af0) Thanks @tim-smart! - optimize Sharding shard release + +- Updated dependencies [[`7d28a90`](https://github.com/Effect-TS/effect/commit/7d28a908f965854cff386a19515141aea5b39eb7)]: + - effect@3.19.3 + +## 0.52.6 + +### Patch Changes + +- [#5705](https://github.com/Effect-TS/effect/pull/5705) [`84d22eb`](https://github.com/Effect-TS/effect/commit/84d22eb75985da4f3c626a59bcfa2b5a19a17fa2) Thanks @tim-smart! - use FiberMap to release shards concurrently + +- [#5705](https://github.com/Effect-TS/effect/pull/5705) [`84d22eb`](https://github.com/Effect-TS/effect/commit/84d22eb75985da4f3c626a59bcfa2b5a19a17fa2) Thanks @tim-smart! - if a shard lock fails to release, release all locks + +## 0.52.5 + +### Patch Changes + +- [#5703](https://github.com/Effect-TS/effect/pull/5703) [`374f58c`](https://github.com/Effect-TS/effect/commit/374f58c10799109b61d8a131a025f3d03ce5aab5) Thanks @tim-smart! - close release FiberHandle after acquisition fiber + +- Updated dependencies [[`374f58c`](https://github.com/Effect-TS/effect/commit/374f58c10799109b61d8a131a025f3d03ce5aab5), [`374f58c`](https://github.com/Effect-TS/effect/commit/374f58c10799109b61d8a131a025f3d03ce5aab5)]: + - effect@3.19.2 + +## 0.52.4 + +### Patch Changes + +- [#5701](https://github.com/Effect-TS/effect/pull/5701) [`c00268d`](https://github.com/Effect-TS/effect/commit/c00268d6ae82fcceda6d71a99e1ec4c5072c1deb) Thanks @tim-smart! - add timeout to pg lock queries + +## 0.52.3 + +### Patch Changes + +- [#5698](https://github.com/Effect-TS/effect/pull/5698) [`19e14ec`](https://github.com/Effect-TS/effect/commit/19e14ec89065ed938f0b3d27a9e8838dac81f3c8) Thanks @tim-smart! - add premptiveShutdown option to ShardingConfig + +## 0.52.2 + +### Patch Changes + +- [#5695](https://github.com/Effect-TS/effect/pull/5695) [`63f2bf3`](https://github.com/Effect-TS/effect/commit/63f2bf393ef4bb3e46db59abdf1b2160e8ee71d4) Thanks @tim-smart! - tie cluster Entity lifetimes to Layer scope + +- Updated dependencies [[`63f2bf3`](https://github.com/Effect-TS/effect/commit/63f2bf393ef4bb3e46db59abdf1b2160e8ee71d4), [`63f2bf3`](https://github.com/Effect-TS/effect/commit/63f2bf393ef4bb3e46db59abdf1b2160e8ee71d4)]: + - effect@3.19.1 + - @effect/workflow@0.12.2 + +## 0.52.1 + +### Patch Changes + +- [#5693](https://github.com/Effect-TS/effect/pull/5693) [`c7e572c`](https://github.com/Effect-TS/effect/commit/c7e572c3504ea9115df5fa1256d4791b03f33133) Thanks @tim-smart! - change workflow re-registration defect to a warning + +## 0.52.0 + +### Patch Changes + +- [#5684](https://github.com/Effect-TS/effect/pull/5684) [`15100f6`](https://github.com/Effect-TS/effect/commit/15100f6ed1ae554c295fb8034623e942dcdc6a72) Thanks @tim-smart! - retry interrupted workflow activities + +- [#5689](https://github.com/Effect-TS/effect/pull/5689) [`0d77928`](https://github.com/Effect-TS/effect/commit/0d779286eec84e679b55e0dbcd7d0dd981c28f18) Thanks @tim-smart! - only store client interrupts as suspends + +- [#5691](https://github.com/Effect-TS/effect/pull/5691) [`c2133fa`](https://github.com/Effect-TS/effect/commit/c2133fad3a5c3f73c0e0920be779390d24aa253f) Thanks @tim-smart! - prevent interrupt suspends from propagating + +- Updated dependencies [[`15100f6`](https://github.com/Effect-TS/effect/commit/15100f6ed1ae554c295fb8034623e942dcdc6a72), [`571025c`](https://github.com/Effect-TS/effect/commit/571025ceaff6ef432a61bf65735a5a0f45118313), [`d43577b`](https://github.com/Effect-TS/effect/commit/d43577be59ae510812287b1cbffe6da15c040452)]: + - @effect/workflow@0.12.1 + - @effect/sql@0.48.0 + - @effect/rpc@0.72.1 + +## 0.51.0 + +### Minor Changes + +- [#5606](https://github.com/Effect-TS/effect/pull/5606) [`24a1685`](https://github.com/Effect-TS/effect/commit/24a1685c70a9ed157468650f95a5c3da3f2c2433) Thanks @tim-smart! - backport @effect/cluster from effect v4 + + @effect/cluster no longer requires a Shard Manager, and instead relies on the + `RunnerStorage` service to track runner state. + + To migrate, remove any Shard Manager deployments and use the updated layers in + `@effect/platform-node` or `@effect/platform-bun`. + + # Breaking Changes + - `ShardManager` module has been removed + - `EntityNotManagedByRunner` error has been removed + - Shard locks now use database advisory locks, which requires stable sessions + for database connections. This means load balancers or proxies that rotate + connections may cause issues. + - `@effect/platform-node/NodeClusterSocketRunner` is now + `@effect/cluster/NodeClusterSocket` + - `@effect/platform-node/NodeClusterHttpRunner` is now + `@effect/cluster/NodeClusterHttp` + - `@effect/platform-bun/BunClusterSocketRunner` is now + `@effect/cluster/BunClusterSocket` + - `@effect/platform-bun/BunClusterHttpRunner` is now + `@effect/cluster/BunClusterHttp` + + # New Features + - `RunnerHealth.layerK8s` has been added, which uses the Kubernetes API to track + runner health and liveness. To use it, you will need a service account with + permissions to read pod information. + +### Patch Changes + +- Updated dependencies [[`27863ab`](https://github.com/Effect-TS/effect/commit/27863abed9047a3cb5d47b4136ff69d5456e2c74), [`3c15d5f`](https://github.com/Effect-TS/effect/commit/3c15d5f99fb8d8470a00c5a33d9ba3cac89dfe4c), [`3863fa8`](https://github.com/Effect-TS/effect/commit/3863fa89f61e63e5529fd961e37333bddf7db64a), [`2a03c76`](https://github.com/Effect-TS/effect/commit/2a03c76c2781ca7e9e228e838eab2eb0d0795b1d), [`24a1685`](https://github.com/Effect-TS/effect/commit/24a1685c70a9ed157468650f95a5c3da3f2c2433), [`24a1685`](https://github.com/Effect-TS/effect/commit/24a1685c70a9ed157468650f95a5c3da3f2c2433)]: + - @effect/workflow@0.12.0 + - effect@3.19.0 + - @effect/rpc@0.72.0 + - @effect/platform@0.93.0 + - @effect/sql@0.47.0 + +## 0.50.6 + +### Patch Changes + +- [#5642](https://github.com/Effect-TS/effect/pull/5642) [`b8e3c6d`](https://github.com/Effect-TS/effect/commit/b8e3c6d510aec858ac34bfe5eb2b8fc5506fd669) Thanks @tim-smart! - fix ReferenceError in NodeSocket.fromNet + +- Updated dependencies [[`b8e3c6d`](https://github.com/Effect-TS/effect/commit/b8e3c6d510aec858ac34bfe5eb2b8fc5506fd669)]: + - @effect/rpc@0.71.1 + - @effect/workflow@0.11.5 + +## 0.50.5 + +### Patch Changes + +- [#5640](https://github.com/Effect-TS/effect/pull/5640) [`85ea731`](https://github.com/Effect-TS/effect/commit/85ea731c8305c040fc50b82c204f3e20371c50a4) Thanks @tim-smart! - use external interruption for workflow suspend + +- Updated dependencies [[`85ea731`](https://github.com/Effect-TS/effect/commit/85ea731c8305c040fc50b82c204f3e20371c50a4)]: + - @effect/workflow@0.11.4 + +## 0.50.4 + +### Patch Changes + +- [#5618](https://github.com/Effect-TS/effect/pull/5618) [`d2140c1`](https://github.com/Effect-TS/effect/commit/d2140c1604a575186075e0907ecc05d9ab197c23) Thanks @tim-smart! - don't restart an entity during shutdown + +## 0.50.3 + +### Patch Changes + +- [#5602](https://github.com/Effect-TS/effect/pull/5602) [`64b764b`](https://github.com/Effect-TS/effect/commit/64b764b3207eb13cacb13da31343aaf425e966bf) Thanks @tim-smart! - guard against race conditions in NodeSocketServer + +- Updated dependencies [[`64b764b`](https://github.com/Effect-TS/effect/commit/64b764b3207eb13cacb13da31343aaf425e966bf)]: + - @effect/workflow@0.11.3 + +## 0.50.2 + +### Patch Changes + +- [#5590](https://github.com/Effect-TS/effect/pull/5590) [`f4c4702`](https://github.com/Effect-TS/effect/commit/f4c4702ab01900c42c0af4662dfb7a5973619646) Thanks @tim-smart! - add openTimeout options to NodeSocket.makeNet + +- Updated dependencies [[`f4c4702`](https://github.com/Effect-TS/effect/commit/f4c4702ab01900c42c0af4662dfb7a5973619646), [`f6987c0`](https://github.com/Effect-TS/effect/commit/f6987c04ebf1386dc37729dfea1631ce364a5a96)]: + - @effect/workflow@0.11.2 + - @effect/platform@0.92.1 + +## 0.50.1 + +### Patch Changes + +- [#5585](https://github.com/Effect-TS/effect/pull/5585) [`cf17f2f`](https://github.com/Effect-TS/effect/commit/cf17f2f0319a57a886558b01549fea675cd78b69) Thanks @tim-smart! - keep socket error listener attached in NodeSocket + +- Updated dependencies [[`07802f7`](https://github.com/Effect-TS/effect/commit/07802f78fd410d800f0231129ee0866977399152), [`cf17f2f`](https://github.com/Effect-TS/effect/commit/cf17f2f0319a57a886558b01549fea675cd78b69)]: + - effect@3.18.1 + - @effect/workflow@0.11.1 + +## 0.50.0 + +### Patch Changes + +- Updated dependencies [[`1c6ab74`](https://github.com/Effect-TS/effect/commit/1c6ab74b314b2b6df8bb1b1a0cb9527ceda0e3fa), [`70fe803`](https://github.com/Effect-TS/effect/commit/70fe803469db3355ffbf8359b52c351f1c2dc137), [`c296e32`](https://github.com/Effect-TS/effect/commit/c296e32554143b84ae8987046984e1cf1852417c), [`a098ddf`](https://github.com/Effect-TS/effect/commit/a098ddfc551f5aa0a7c36f9b4928372a64d4d9f2)]: + - effect@3.18.0 + - @effect/platform@0.92.0 + - @effect/rpc@0.71.0 + - @effect/sql@0.46.0 + - @effect/workflow@0.11.0 + +## 0.49.6 + +### Patch Changes + +- [#5581](https://github.com/Effect-TS/effect/pull/5581) [`dd7b459`](https://github.com/Effect-TS/effect/commit/dd7b4591b79ed88f3c0fcc607f9e42f22883f9bd) Thanks @tim-smart! - persist activity interrupts as "Suspended" + +- Updated dependencies [[`dd7b459`](https://github.com/Effect-TS/effect/commit/dd7b4591b79ed88f3c0fcc607f9e42f22883f9bd)]: + - @effect/rpc@0.70.2 + - @effect/workflow@0.10.3 + +## 0.49.5 + +### Patch Changes + +- [#5577](https://github.com/Effect-TS/effect/pull/5577) [`c9e1e40`](https://github.com/Effect-TS/effect/commit/c9e1e4064cef4c4324318ec76b35bbbdc026dace) Thanks @tim-smart! - ignore non-client interrupts in workflow activities + +- Updated dependencies [[`c9e1e40`](https://github.com/Effect-TS/effect/commit/c9e1e4064cef4c4324318ec76b35bbbdc026dace)]: + - @effect/rpc@0.70.1 + - @effect/workflow@0.10.2 + +## 0.49.4 + +### Patch Changes + +- [#5575](https://github.com/Effect-TS/effect/pull/5575) [`d0e97d7`](https://github.com/Effect-TS/effect/commit/d0e97d74a9e4d4a436ac8d148db81144cf872cc6) Thanks @tim-smart! - fix SqlMessageStorage last reply for sqlite + +- Updated dependencies [[`d0e97d7`](https://github.com/Effect-TS/effect/commit/d0e97d74a9e4d4a436ac8d148db81144cf872cc6)]: + - @effect/workflow@0.10.1 + +## 0.49.3 + +### Patch Changes + +- [#5563](https://github.com/Effect-TS/effect/pull/5563) [`fc7e32a`](https://github.com/Effect-TS/effect/commit/fc7e32af793214b542262f912a30c89f85068bb4) Thanks @tim-smart! - prevent EntityNotAssigned when sending to local runner + +## 0.49.2 + +### Patch Changes + +- [#5561](https://github.com/Effect-TS/effect/pull/5561) [`1a9e601`](https://github.com/Effect-TS/effect/commit/1a9e60163d25da51294e7aaba33bbf0bde7fe231) Thanks @tim-smart! - optimize SqlShardStorage queries + +## 0.49.1 + +### Patch Changes + +- [#5557](https://github.com/Effect-TS/effect/pull/5557) [`978b6ff`](https://github.com/Effect-TS/effect/commit/978b6ffc0b124d67d62a797211eff795f22cd1e6) Thanks @tim-smart! - allow NodeSocket.makeNet open to be interrupted + +## 0.49.0 + +### Patch Changes + +- Updated dependencies [[`d4d86a8`](https://github.com/Effect-TS/effect/commit/d4d86a81f02b94e09fce8004ce2c5369c505ca5a)]: + - @effect/platform@0.91.0 + - @effect/rpc@0.70.0 + - @effect/sql@0.45.0 + - @effect/workflow@0.10.0 + +## 0.48.16 + +### Patch Changes + +- [#5543](https://github.com/Effect-TS/effect/pull/5543) [`2b9db9e`](https://github.com/Effect-TS/effect/commit/2b9db9e7f930d3ae269b123286c881ea34d3afca) Thanks @tim-smart! - propagate workflow interruption to unfinished children + +- Updated dependencies [[`2b9db9e`](https://github.com/Effect-TS/effect/commit/2b9db9e7f930d3ae269b123286c881ea34d3afca)]: + - @effect/workflow@0.9.6 + +## 0.48.15 + +### Patch Changes + +- [#5541](https://github.com/Effect-TS/effect/pull/5541) [`4f07ee0`](https://github.com/Effect-TS/effect/commit/4f07ee06e59fa81762761b12938224f5c931b119) Thanks @tim-smart! - backport MessageStorage.unregisterReplyHandler + +- Updated dependencies [[`ea95998`](https://github.com/Effect-TS/effect/commit/ea95998de2a7613d844c42e67e7f5b16652c5000)]: + - effect@3.17.14 + +## 0.48.14 + +### Patch Changes + +- [#5529](https://github.com/Effect-TS/effect/pull/5529) [`d9c8bce`](https://github.com/Effect-TS/effect/commit/d9c8bce027a45da82190303cc6eb6838074f4fb9) Thanks @tim-smart! - catch interruptions in Sharding storage loop + +## 0.48.13 + +### Patch Changes + +- [#5527](https://github.com/Effect-TS/effect/pull/5527) [`e3dac95`](https://github.com/Effect-TS/effect/commit/e3dac9527ab8ff645c82b30ca2bba5a975b103df) Thanks @tim-smart! - attempt to prevent InterruptedException leaks into entity replies + +- Updated dependencies [[`e3dac95`](https://github.com/Effect-TS/effect/commit/e3dac9527ab8ff645c82b30ca2bba5a975b103df)]: + - @effect/rpc@0.69.4 + +## 0.48.12 + +### Patch Changes + +- [#5525](https://github.com/Effect-TS/effect/pull/5525) [`7edc074`](https://github.com/Effect-TS/effect/commit/7edc074f67d300957b749c9e20ea9706036dc0e0) Thanks @tim-smart! - backport ClusterWorkflowEngine partial entity clients + +## 0.48.11 + +### Patch Changes + +- [#5519](https://github.com/Effect-TS/effect/pull/5519) [`92c533f`](https://github.com/Effect-TS/effect/commit/92c533fb52d6d3869728071e92596f80b4c7aa36) Thanks @tim-smart! - improve SqlMessageStorage insert queries + +## 0.48.10 + +### Patch Changes + +- [#5517](https://github.com/Effect-TS/effect/pull/5517) [`de07e58`](https://github.com/Effect-TS/effect/commit/de07e5805496b80226ba6a5efc2b4c05e1aba4b8) Thanks @tim-smart! - backport cluster improvements from effect 4 + +- Updated dependencies [[`de07e58`](https://github.com/Effect-TS/effect/commit/de07e5805496b80226ba6a5efc2b4c05e1aba4b8)]: + - @effect/platform@0.90.10 + - @effect/rpc@0.69.3 + +## 0.48.9 + +### Patch Changes + +- [#5512](https://github.com/Effect-TS/effect/pull/5512) [`934c1d9`](https://github.com/Effect-TS/effect/commit/934c1d9517a7c8bcac1b362908fa474371f7f0fa) Thanks @tim-smart! - backport active shard latch changes from effect 4 + +## 0.48.8 + +### Patch Changes + +- [#5510](https://github.com/Effect-TS/effect/pull/5510) [`c2fed22`](https://github.com/Effect-TS/effect/commit/c2fed22a131fbd9ba54482af1886cc1e869a72cd) Thanks @tim-smart! - re-add two phase runner health check + +## 0.48.7 + +### Patch Changes + +- [#5505](https://github.com/Effect-TS/effect/pull/5505) [`1c4b6ae`](https://github.com/Effect-TS/effect/commit/1c4b6ae81daf17bce1f67c0a0bc298dc4ce8f461) Thanks @tim-smart! - ensure ShardManager and Runner get fresh versions of the rpc protocol + +- [#5508](https://github.com/Effect-TS/effect/pull/5508) [`26d1267`](https://github.com/Effect-TS/effect/commit/26d12678b08ee3021f5529bed97b63377155a7fe) Thanks @tim-smart! - ensure runner detects when it has been externally unregistered + +## 0.48.6 + +### Patch Changes + +- [#5501](https://github.com/Effect-TS/effect/pull/5501) [`a874567`](https://github.com/Effect-TS/effect/commit/a874567c59fa1b135268d8e86222a4afd693ae12) Thanks @tim-smart! - don't remove entities from map on shutdown + +## 0.48.5 + +### Patch Changes + +- [#5486](https://github.com/Effect-TS/effect/pull/5486) [`85a60a0`](https://github.com/Effect-TS/effect/commit/85a60a0f06e596991dec1d64bdfccac8df880f84) Thanks @tim-smart! - add Workflow.poll api + +- Updated dependencies [[`85a60a0`](https://github.com/Effect-TS/effect/commit/85a60a0f06e596991dec1d64bdfccac8df880f84)]: + - @effect/workflow@0.9.4 + +## 0.48.4 + +### Patch Changes + +- [#5484](https://github.com/Effect-TS/effect/pull/5484) [`0a9ec23`](https://github.com/Effect-TS/effect/commit/0a9ec23dca104ac6fd7ea5841e98f5fa7796be40) Thanks @tim-smart! - fix multiple persisted requests subscribing to the same id + +- Updated dependencies [[`333be04`](https://github.com/Effect-TS/effect/commit/333be046b50e8300f5cb70b871448e0628b7b37c)]: + - @effect/platform@0.90.8 + +## 0.48.3 + +### Patch Changes + +- [#5472](https://github.com/Effect-TS/effect/pull/5472) [`fb00cd6`](https://github.com/Effect-TS/effect/commit/fb00cd6cf20da70592125c0c682225de51a977d5) Thanks @tim-smart! - ensure defects don't cause loops in ClusterCron + +## 0.48.2 + +### Patch Changes + +- [#5415](https://github.com/Effect-TS/effect/pull/5415) [`4cb3af5`](https://github.com/Effect-TS/effect/commit/4cb3af5aeae8535a04f84fb0f64c3f2be19e2aed) Thanks @tim-smart! - fix rpc msgpack serialization when chunk contains partial frames + +- Updated dependencies [[`4cb3af5`](https://github.com/Effect-TS/effect/commit/4cb3af5aeae8535a04f84fb0f64c3f2be19e2aed)]: + - @effect/rpc@0.69.1 + - @effect/workflow@0.9.2 + +## 0.48.1 + +### Patch Changes + +- [#5394](https://github.com/Effect-TS/effect/pull/5394) [`59547d9`](https://github.com/Effect-TS/effect/commit/59547d94dc625b19da62b5d1f3ddffa59efb0ff2) Thanks @tim-smart! - add DurableDeferred.withActivityAttempt, for scoping it to the current activity run + +- Updated dependencies [[`59547d9`](https://github.com/Effect-TS/effect/commit/59547d94dc625b19da62b5d1f3ddffa59efb0ff2)]: + - @effect/workflow@0.9.1 + +## 0.48.0 + +### Patch Changes + +- Updated dependencies [[`3e163b2`](https://github.com/Effect-TS/effect/commit/3e163b24cc2b647e25566ba29ef25c3f57609042)]: + - @effect/rpc@0.69.0 + - @effect/workflow@0.9.0 + +## 0.47.0 + +### Minor Changes + +- [#5358](https://github.com/Effect-TS/effect/pull/5358) [`a949539`](https://github.com/Effect-TS/effect/commit/a94953971c2e908890dfda00f8560d317306c328) Thanks @tim-smart! - improve RunnerHealth by detecting if Runner is connected to the host + +### Patch Changes + +- Updated dependencies [[`a949539`](https://github.com/Effect-TS/effect/commit/a94953971c2e908890dfda00f8560d317306c328)]: + - effect@3.17.7 + +## 0.46.4 + +### Patch Changes + +- [#5350](https://github.com/Effect-TS/effect/pull/5350) [`d0b5fd1`](https://github.com/Effect-TS/effect/commit/d0b5fd1f7a292a47b9eeb058e5df57ace9a5ab14) Thanks @tim-smart! - add Migrator.fromRecord api + +- Updated dependencies [[`d0b5fd1`](https://github.com/Effect-TS/effect/commit/d0b5fd1f7a292a47b9eeb058e5df57ace9a5ab14)]: + - @effect/sql@0.44.1 + - @effect/workflow@0.8.3 + +## 0.46.3 + +### Patch Changes + +- [#5345](https://github.com/Effect-TS/effect/pull/5345) [`58d56d5`](https://github.com/Effect-TS/effect/commit/58d56d549fa49d6d06cfedb975a046872ac44f85) Thanks @tim-smart! - increase size of entity_type on cluster_messages table + +- [#5345](https://github.com/Effect-TS/effect/pull/5345) [`58d56d5`](https://github.com/Effect-TS/effect/commit/58d56d549fa49d6d06cfedb975a046872ac44f85) Thanks @tim-smart! - log defects in Entity & Workflow proxy for HttpApi endpoints + +- Updated dependencies [[`58d56d5`](https://github.com/Effect-TS/effect/commit/58d56d549fa49d6d06cfedb975a046872ac44f85)]: + - @effect/workflow@0.8.2 + +## 0.46.2 + +### Patch Changes + +- [#5297](https://github.com/Effect-TS/effect/pull/5297) [`eedbab5`](https://github.com/Effect-TS/effect/commit/eedbab5a02f6752acbd835bfeff338b1fd7c0deb) Thanks @tim-smart! - Remove retry from ClusterCron in favour of user supplied retry + +## 0.46.1 + +### Patch Changes + +- [#5283](https://github.com/Effect-TS/effect/pull/5283) [`de92b1c`](https://github.com/Effect-TS/effect/commit/de92b1c46a57761df9dd46e59e42d5a54beb6e34) Thanks @tim-smart! - disable tracing for ClusterCron client to disconnect parent span + +## 0.46.0 + +### Patch Changes + +- Updated dependencies [[`5a0f4f1`](https://github.com/Effect-TS/effect/commit/5a0f4f176687a39d9fa46bb894bb7ac3175b0e87), [`e9cbd26`](https://github.com/Effect-TS/effect/commit/e9cbd2673401723aa811b0535202e4f57baf6d2c)]: + - effect@3.17.1 + - @effect/rpc@0.68.0 + - @effect/workflow@0.8.0 + +## 0.45.0 + +### Patch Changes + +- Updated dependencies [[`7813640`](https://github.com/Effect-TS/effect/commit/7813640279d9e3a3e7fc0a29bfb5c6d5fb3c270f)]: + - @effect/platform@0.90.0 + - @effect/rpc@0.67.0 + - @effect/sql@0.44.0 + - @effect/workflow@0.7.0 + +## 0.44.0 + +### Patch Changes + +- Updated dependencies [[`40c3c87`](https://github.com/Effect-TS/effect/commit/40c3c875f724264312b43002859c82bed9ad0df9), [`ed2c74a`](https://github.com/Effect-TS/effect/commit/ed2c74ae8fa4ea0dd06ea84a3e58cd32e6916104), [`073a1b8`](https://github.com/Effect-TS/effect/commit/073a1b8be5dbfa87454393ee7346f5bc36a4fd63), [`f382e99`](https://github.com/Effect-TS/effect/commit/f382e99e409838a879246250fc3994b9bf5b3c2c), [`e8c7ba5`](https://github.com/Effect-TS/effect/commit/e8c7ba5fd3eb0c3ae3039fc24c09d69391987989), [`7e10415`](https://github.com/Effect-TS/effect/commit/7e1041599ade25103428703f5d2dfd7378a09636), [`e9bdece`](https://github.com/Effect-TS/effect/commit/e9bdececdc24f60a246be5055eca71a0d49ea7f2), [`8d95eb0`](https://github.com/Effect-TS/effect/commit/8d95eb0356b1d1736204836c275d201a547d208d)]: + - effect@3.17.0 + - @effect/platform@0.89.0 + - @effect/rpc@0.66.0 + - @effect/sql@0.43.0 + - @effect/workflow@0.6.0 + +## 0.43.1 + +### Patch Changes + +- [#5232](https://github.com/Effect-TS/effect/pull/5232) [`4ffbd67`](https://github.com/Effect-TS/effect/commit/4ffbd674af526059f983e20f2f00ee6699c7b956) Thanks @tim-smart! - retry RunnerHealth ping to relax down conditions + +## 0.43.0 + +### Patch Changes + +- [#5211](https://github.com/Effect-TS/effect/pull/5211) [`d25f22b`](https://github.com/Effect-TS/effect/commit/d25f22be7598abe977caf6cdac3b0dd78b438c48) Thanks @mattiamanzati! - Removed some unnecessary single-arg pipe calls + +- Updated dependencies [[`f5dfabf`](https://github.com/Effect-TS/effect/commit/f5dfabf51ba481a4468c1509c537314978ef6cec), [`17a5ea8`](https://github.com/Effect-TS/effect/commit/17a5ea8fa29785fe6e4c9480f2a2e9c8c59f3f38), [`d25f22b`](https://github.com/Effect-TS/effect/commit/d25f22be7598abe977caf6cdac3b0dd78b438c48)]: + - effect@3.16.14 + - @effect/sql@0.42.0 + - @effect/platform@0.88.1 + - @effect/rpc@0.65.1 + - @effect/workflow@0.5.1 + +## 0.42.0 + +### Patch Changes + +- Updated dependencies [[`27206d7`](https://github.com/Effect-TS/effect/commit/27206d7f0558d7fe28de57bf54f1d0cc83acc92e), [`dbabf5e`](https://github.com/Effect-TS/effect/commit/dbabf5e76fa63b050d2b6c466713c7dc59f07d3c)]: + - @effect/platform@0.88.0 + - @effect/rpc@0.65.0 + - @effect/sql@0.41.0 + - @effect/workflow@0.5.0 + +## 0.41.18 + +### Patch Changes + +- Updated dependencies [[`c1c05a8`](https://github.com/Effect-TS/effect/commit/c1c05a8242fb5df7445b4a12387a60eac7726eb7), [`5b7cd92`](https://github.com/Effect-TS/effect/commit/5b7cd923e786c38a0802faf0fe75498ab3cccf28), [`81fe4a2`](https://github.com/Effect-TS/effect/commit/81fe4a2c81d5e30e180a60e68c52016a27b350db)]: + - effect@3.16.13 + - @effect/rpc@0.64.14 + - @effect/platform@0.87.13 + - @effect/sql@0.40.14 + - @effect/workflow@0.4.14 + +## 0.41.17 + +### Patch Changes + +- [#5196](https://github.com/Effect-TS/effect/pull/5196) [`56b33c3`](https://github.com/Effect-TS/effect/commit/56b33c357cfc5f8976486f48e93032058c02d876) Thanks @tim-smart! - add discard methods to cluster EntityProxy's + +## 0.41.16 + +### Patch Changes + +- Updated dependencies [[`ad6e968`](https://github.com/Effect-TS/effect/commit/ad6e9688d78db27a80396ad79d376bb7eaf668bf), [`32ba77a`](https://github.com/Effect-TS/effect/commit/32ba77ae304d2161362a73e8b61965332626cf2d), [`d5e25b2`](https://github.com/Effect-TS/effect/commit/d5e25b237f05670ee42b386cb40b2cb448fc11d7), [`a28efb8`](https://github.com/Effect-TS/effect/commit/a28efb8913d9a7ac65c1cb783b17f382b185f8be)]: + - @effect/workflow@0.4.13 + - @effect/platform@0.87.12 + - @effect/rpc@0.64.13 + - @effect/sql@0.40.13 + +## 0.41.15 + +### Patch Changes + +- Updated dependencies [[`79a1947`](https://github.com/Effect-TS/effect/commit/79a1947359cbd89a47ea315cdd86a3d250f28f43), [`001392b`](https://github.com/Effect-TS/effect/commit/001392ba8bfcad101bb034348a7415012fb12f72), [`7bfb099`](https://github.com/Effect-TS/effect/commit/7bfb099cb5528511b8d63045c4fbb4dc9cb18528)]: + - @effect/rpc@0.64.12 + - @effect/platform@0.87.11 + - @effect/workflow@0.4.12 + - @effect/sql@0.40.12 + +## 0.41.14 + +### Patch Changes + +- Updated dependencies [[`678318d`](https://github.com/Effect-TS/effect/commit/678318d2e88233156b006acda56c2d138ee3ffa0), [`678318d`](https://github.com/Effect-TS/effect/commit/678318d2e88233156b006acda56c2d138ee3ffa0)]: + - @effect/platform@0.87.10 + - @effect/rpc@0.64.11 + - @effect/sql@0.40.11 + - @effect/workflow@0.4.11 + +## 0.41.13 + +### Patch Changes + +- Updated dependencies [[`54514a2`](https://github.com/Effect-TS/effect/commit/54514a2f53166de27ad7e756dbf12194691fd4af)]: + - @effect/platform@0.87.9 + - @effect/rpc@0.64.10 + - @effect/sql@0.40.10 + - @effect/workflow@0.4.10 + +## 0.41.12 + +### Patch Changes + +- Updated dependencies [[`4ce4f82`](https://github.com/Effect-TS/effect/commit/4ce4f824f6fdef492be1d35c05a490ffce518c89)]: + - @effect/platform@0.87.8 + - @effect/rpc@0.64.9 + - @effect/sql@0.40.9 + - @effect/workflow@0.4.9 + +## 0.41.11 + +### Patch Changes + +- Updated dependencies [[`a9b617f`](https://github.com/Effect-TS/effect/commit/a9b617f125171ed76cd79ab46d7a924daf3b0e70), [`7e26e86`](https://github.com/Effect-TS/effect/commit/7e26e86524abcc93713d6ad7eee486638c98f7c2)]: + - @effect/platform@0.87.7 + - @effect/rpc@0.64.8 + - @effect/sql@0.40.8 + - @effect/workflow@0.4.8 + +## 0.41.10 + +### Patch Changes + +- Updated dependencies [[`905da99`](https://github.com/Effect-TS/effect/commit/905da996aad665057b4ca6dba1a4af44fb8835bd)]: + - effect@3.16.12 + - @effect/platform@0.87.6 + - @effect/rpc@0.64.7 + - @effect/sql@0.40.7 + - @effect/workflow@0.4.7 + +## 0.41.9 + +### Patch Changes + +- Updated dependencies []: + - @effect/sql@0.40.6 + +## 0.41.8 + +### Patch Changes + +- Updated dependencies [[`2fd8676`](https://github.com/Effect-TS/effect/commit/2fd8676c803cd40000dfc3231f5daecaa0e0ebd2)]: + - @effect/platform@0.87.5 + - @effect/rpc@0.64.6 + - @effect/sql@0.40.5 + - @effect/workflow@0.4.6 + +## 0.41.7 + +### Patch Changes + +- [#5140](https://github.com/Effect-TS/effect/pull/5140) [`b01d2e0`](https://github.com/Effect-TS/effect/commit/b01d2e0d591418e10e9e362698205d848e97a9b7) Thanks @tim-smart! - remove dynamic cluster metrics that could cause memory leaks + +## 0.41.6 + +### Patch Changes + +- [#5139](https://github.com/Effect-TS/effect/pull/5139) [`7fdc16b`](https://github.com/Effect-TS/effect/commit/7fdc16bd88b872f5918384e4acda3731aab018da) Thanks @tim-smart! - only register new runners if they are not present in the ShardManager state + +- Updated dependencies [[`e82a4fd`](https://github.com/Effect-TS/effect/commit/e82a4fd60f6528d08cef1a4aba0abe0d3ba741ad)]: + - @effect/platform@0.87.4 + - @effect/rpc@0.64.5 + - @effect/sql@0.40.4 + - @effect/workflow@0.4.5 + +## 0.41.5 + +### Patch Changes + +- Updated dependencies [[`1b6e396`](https://github.com/Effect-TS/effect/commit/1b6e396d699f3cbbc56b68f99055cf746529bb9e), [`46c3216`](https://github.com/Effect-TS/effect/commit/46c321657d93393506278327418e36f8e7a77f86)]: + - @effect/platform@0.87.3 + - @effect/sql@0.40.3 + - @effect/rpc@0.64.4 + - @effect/workflow@0.4.4 + +## 0.41.4 + +### Patch Changes + +- Updated dependencies [[`4fea68c`](https://github.com/Effect-TS/effect/commit/4fea68ca7a25a3c39a1ab68b3885534513ab0c81), [`b927954`](https://github.com/Effect-TS/effect/commit/b9279543cf5688dd8a577af80456959c615217d0), [`99590a6`](https://github.com/Effect-TS/effect/commit/99590a6ca9128eb1ede265b6670b655311995614), [`6c3e24c`](https://github.com/Effect-TS/effect/commit/6c3e24c2308f7d4a29b8f4270ab81bca22ac6bb4)]: + - @effect/platform@0.87.2 + - effect@3.16.11 + - @effect/rpc@0.64.3 + - @effect/sql@0.40.2 + - @effect/workflow@0.4.3 + +## 0.41.3 + +### Patch Changes + +- Updated dependencies [[`faad30e`](https://github.com/Effect-TS/effect/commit/faad30ec8742916be59f9db642d0fc98225b636c)]: + - effect@3.16.10 + - @effect/platform@0.87.1 + - @effect/rpc@0.64.2 + - @effect/sql@0.40.1 + - @effect/workflow@0.4.2 + +## 0.41.2 + +### Patch Changes + +- Updated dependencies [[`112a93a`](https://github.com/Effect-TS/effect/commit/112a93a9bab73e95e79f7b3502d1a7b1acd668fc)]: + - @effect/rpc@0.64.1 + - @effect/workflow@0.4.1 + +## 0.41.1 + +### Patch Changes + +- [#5091](https://github.com/Effect-TS/effect/pull/5091) [`d5fd2c1`](https://github.com/Effect-TS/effect/commit/d5fd2c1526f06228853ed8317d9688c4af5f285a) Thanks @tim-smart! - fix unwrapping of DurableDeferred results + +- [#5089](https://github.com/Effect-TS/effect/pull/5089) [`9d189d7`](https://github.com/Effect-TS/effect/commit/9d189d744aa3307e055094c66f580453d95ff99d) Thanks @tim-smart! - propagate fiber refs to workflow activities + +## 0.41.0 + +### Minor Changes + +- [#5086](https://github.com/Effect-TS/effect/pull/5086) [`867919c`](https://github.com/Effect-TS/effect/commit/867919c8be9a2f770699c0db852a3f566017ffd6) Thanks @tim-smart! - add Type generic to cluster entities + +### Patch Changes + +- Updated dependencies [[`b5bac9a`](https://github.com/Effect-TS/effect/commit/b5bac9ac2913fcd11b02322624f03b544eef53ba)]: + - @effect/rpc@0.64.0 + - @effect/platform@0.87.0 + - @effect/workflow@0.4.0 + - @effect/sql@0.40.0 + +## 0.40.0 + +### Patch Changes + +- Updated dependencies [[`5137c70`](https://github.com/Effect-TS/effect/commit/5137c703461d8d3b363c112140a6e7f798241d07), [`c23d25c`](https://github.com/Effect-TS/effect/commit/c23d25c3e7c541f1f63b28484d8c461d86c67e99), [`5137c70`](https://github.com/Effect-TS/effect/commit/5137c703461d8d3b363c112140a6e7f798241d07), [`5137c70`](https://github.com/Effect-TS/effect/commit/5137c703461d8d3b363c112140a6e7f798241d07)]: + - effect@3.16.9 + - @effect/platform@0.86.0 + - @effect/rpc@0.63.0 + - @effect/sql@0.39.0 + - @effect/workflow@0.3.0 + +## 0.39.5 + +### Patch Changes + +- [#5070](https://github.com/Effect-TS/effect/pull/5070) [`ff90206`](https://github.com/Effect-TS/effect/commit/ff90206fc56f5c1eb1675603652462a83a27421d) Thanks @tim-smart! - use FIFO queue for shard assignment, to reduce churn + +## 0.39.4 + +### Patch Changes + +- Updated dependencies [[`a8d99b2`](https://github.com/Effect-TS/effect/commit/a8d99b2ec2f55d9aa6e7d00a5138e80380716877)]: + - @effect/rpc@0.62.4 + - @effect/workflow@0.2.4 + +## 0.39.3 + +### Patch Changes + +- Updated dependencies [[`914a191`](https://github.com/Effect-TS/effect/commit/914a191e7cb6341a3d0e965bccd27c336cf22e44)]: + - @effect/platform@0.85.2 + - @effect/rpc@0.62.3 + - @effect/sql@0.38.2 + - @effect/workflow@0.2.3 + +## 0.39.2 + +### Patch Changes + +- Updated dependencies [[`ddfd1e4`](https://github.com/Effect-TS/effect/commit/ddfd1e43db60e3b779d18a221344423c5f3c7416)]: + - @effect/rpc@0.62.2 + - @effect/workflow@0.2.2 + +## 0.39.1 + +### Patch Changes + +- Updated dependencies [[`8cb98d5`](https://github.com/Effect-TS/effect/commit/8cb98d53e68330228287ce2a2e0d8a4c86bcab3b), [`db2dd3c`](https://github.com/Effect-TS/effect/commit/db2dd3c3a8a77d791eae19e66153527e1cde4e6e)]: + - effect@3.16.8 + - @effect/platform@0.85.1 + - @effect/rpc@0.62.1 + - @effect/sql@0.38.1 + - @effect/workflow@0.2.1 + +## 0.39.0 + +### Patch Changes + +- [#5044](https://github.com/Effect-TS/effect/pull/5044) [`b7cc5c7`](https://github.com/Effect-TS/effect/commit/b7cc5c7b6b012f1c90458ce8d83ae287ea58a3d1) Thanks @tim-smart! - simplify and increase rebalance rate for ShardManager + +- Updated dependencies [[`93687dd`](https://github.com/Effect-TS/effect/commit/93687ddbb25ce3b324cd2b83d2ccff225e97307e), [`93687dd`](https://github.com/Effect-TS/effect/commit/93687ddbb25ce3b324cd2b83d2ccff225e97307e), [`93687dd`](https://github.com/Effect-TS/effect/commit/93687ddbb25ce3b324cd2b83d2ccff225e97307e)]: + - @effect/platform@0.85.0 + - @effect/rpc@0.62.0 + - @effect/sql@0.38.0 + - @effect/workflow@0.2.0 + +## 0.38.16 + +### Patch Changes + +- [#5036](https://github.com/Effect-TS/effect/pull/5036) [`cbac1ac`](https://github.com/Effect-TS/effect/commit/cbac1ac61a4e15ad15828563b39eef412bcee66e) Thanks @tim-smart! - add .of helpers to RpcGroup, Entity and AiToolkit + +- Updated dependencies [[`1bb0d8a`](https://github.com/Effect-TS/effect/commit/1bb0d8ab96782e99434356266b38251554ea0294), [`cbac1ac`](https://github.com/Effect-TS/effect/commit/cbac1ac61a4e15ad15828563b39eef412bcee66e)]: + - effect@3.16.7 + - @effect/rpc@0.61.15 + - @effect/platform@0.84.11 + - @effect/sql@0.37.12 + - @effect/workflow@0.1.14 + +## 0.38.15 + +### Patch Changes + +- Updated dependencies [[`a5f7595`](https://github.com/Effect-TS/effect/commit/a5f75956ef9a15a83c416517ef493f0ee2f5ee8a), [`a02470c`](https://github.com/Effect-TS/effect/commit/a02470c75579e91525a25adb3f21b3650d042fdd), [`bf369b2`](https://github.com/Effect-TS/effect/commit/bf369b2902a0e0b195d957c18b9efd180942cf8b), [`f891d45`](https://github.com/Effect-TS/effect/commit/f891d45adffdafd3f94a2eca23faa354e3a409a8)]: + - effect@3.16.6 + - @effect/platform@0.84.10 + - @effect/rpc@0.61.14 + - @effect/sql@0.37.11 + - @effect/workflow@0.1.13 + +## 0.38.14 + +### Patch Changes + +- Updated dependencies [[`ee3a197`](https://github.com/Effect-TS/effect/commit/ee3a1973f54d7611ae99979edfed3020e94e1126), [`ee3a197`](https://github.com/Effect-TS/effect/commit/ee3a1973f54d7611ae99979edfed3020e94e1126)]: + - @effect/rpc@0.61.13 + - @effect/workflow@0.1.12 + +## 0.38.13 + +### Patch Changes + +- Updated dependencies [[`e0d3d42`](https://github.com/Effect-TS/effect/commit/e0d3d424d8f4e6a8ada017160406991f02b3c068)]: + - @effect/rpc@0.61.12 + - @effect/workflow@0.1.11 + +## 0.38.12 + +### Patch Changes + +- [#4750](https://github.com/Effect-TS/effect/pull/4750) [`dca92fd`](https://github.com/Effect-TS/effect/commit/dca92fd8cf41f07561f55d863def5a9f62275f53) Thanks @tim-smart! - add disableFatalDefects option to cluster entities + +- Updated dependencies [[`dca92fd`](https://github.com/Effect-TS/effect/commit/dca92fd8cf41f07561f55d863def5a9f62275f53), [`dca92fd`](https://github.com/Effect-TS/effect/commit/dca92fd8cf41f07561f55d863def5a9f62275f53)]: + - @effect/rpc@0.61.11 + - @effect/workflow@0.1.10 + +## 0.38.11 + +### Patch Changes + +- [#5017](https://github.com/Effect-TS/effect/pull/5017) [`cc283b9`](https://github.com/Effect-TS/effect/commit/cc283b968235da3caf6c3e3a09b525fe09618fee) Thanks @tim-smart! - add more spans to ClusterWorkflowEngine + +- Updated dependencies [[`d350176`](https://github.com/Effect-TS/effect/commit/d3501768d42d7ff3ebc2d414c95cc1fcce15894a)]: + - @effect/workflow@0.1.9 + +## 0.38.10 + +### Patch Changes + +- [#5016](https://github.com/Effect-TS/effect/pull/5016) [`6e2e886`](https://github.com/Effect-TS/effect/commit/6e2e886f060c4ac057926b68d2e441c279480c30) Thanks @tim-smart! - fix ShardManager metrics + +- Updated dependencies [[`bf418ef`](https://github.com/Effect-TS/effect/commit/bf418ef14a0f2ec965535793d5cea8fa8ba177ac)]: + - effect@3.16.5 + - @effect/platform@0.84.9 + - @effect/rpc@0.61.10 + - @effect/sql@0.37.10 + - @effect/workflow@0.1.8 + +## 0.38.9 + +### Patch Changes + +- Updated dependencies [[`7bf6cb9`](https://github.com/Effect-TS/effect/commit/7bf6cb943810e403f472a901ed29ccbbf76a46b2), [`7bf6cb9`](https://github.com/Effect-TS/effect/commit/7bf6cb943810e403f472a901ed29ccbbf76a46b2)]: + - @effect/rpc@0.61.9 + - @effect/workflow@0.1.7 + +## 0.38.8 + +### Patch Changes + +- Updated dependencies [[`2a9a0ef`](https://github.com/Effect-TS/effect/commit/2a9a0ef1181a4419e239edb2abfd95f359a4b7f7)]: + - @effect/workflow@0.1.6 + +## 0.38.7 + +### Patch Changes + +- [#4999](https://github.com/Effect-TS/effect/pull/4999) [`22166f8`](https://github.com/Effect-TS/effect/commit/22166f80c677cad6b4719e0e0253a9d06f964626) Thanks @tim-smart! - make registerEntity a no-op on clientOnly cluster + +## 0.38.6 + +### Patch Changes + +- Updated dependencies [[`8b9db77`](https://github.com/Effect-TS/effect/commit/8b9db7742846af0f58fd8e8b7acb7f4f5ff487ec)]: + - @effect/platform@0.84.8 + - @effect/rpc@0.61.8 + - @effect/sql@0.37.9 + - @effect/workflow@0.1.5 + +## 0.38.5 + +### Patch Changes + +- Updated dependencies [[`34333ab`](https://github.com/Effect-TS/effect/commit/34333ab08de42a5269ddb13f66de1536ad6f249f), [`74ab9a0`](https://github.com/Effect-TS/effect/commit/74ab9a0a9e16d6e019369d256e1e24175c8bc3f3), [`770008e`](https://github.com/Effect-TS/effect/commit/770008eca3aad2899a2ed951236e575793294b28), [`34333ab`](https://github.com/Effect-TS/effect/commit/34333ab08de42a5269ddb13f66de1536ad6f249f)]: + - @effect/workflow@0.1.4 + - effect@3.16.4 + - @effect/platform@0.84.7 + - @effect/rpc@0.61.7 + - @effect/sql@0.37.8 + +## 0.38.4 + +### Patch Changes + +- [#4984](https://github.com/Effect-TS/effect/pull/4984) [`7e59d0e`](https://github.com/Effect-TS/effect/commit/7e59d0e2e004d86b8d0778e99c6fcd173fcb682a) Thanks @tim-smart! - expose Sharding.pollStorage api + +## 0.38.3 + +### Patch Changes + +- [#4983](https://github.com/Effect-TS/effect/pull/4983) [`59575c5`](https://github.com/Effect-TS/effect/commit/59575c5bf17a32c8b76c42e3794222b20e766581) Thanks @tim-smart! - do not resume already running workflows + +- Updated dependencies []: + - @effect/sql@0.37.7 + +## 0.38.2 + +### Patch Changes + +- [#4977](https://github.com/Effect-TS/effect/pull/4977) [`d244b63`](https://github.com/Effect-TS/effect/commit/d244b6345ea1d2ac88812562b0c170683913d502) Thanks @tim-smart! - add EntityProxy & EntityProxyServer, for deriving Rpc & HttpApi servers from cluster entities + +- Updated dependencies [[`d244b63`](https://github.com/Effect-TS/effect/commit/d244b6345ea1d2ac88812562b0c170683913d502), [`ceea77a`](https://github.com/Effect-TS/effect/commit/ceea77a13055f145520f763e3fce5b8ff15d728f)]: + - @effect/workflow@0.1.3 + - @effect/platform@0.84.6 + - @effect/rpc@0.61.6 + - @effect/sql@0.37.6 + +## 0.38.1 + +### Patch Changes + +- [#4973](https://github.com/Effect-TS/effect/pull/4973) [`612c739`](https://github.com/Effect-TS/effect/commit/612c73979abc44825feae573c8902b6484923aaa) Thanks @tim-smart! - ignore runners that cannot be parsed + +## 0.38.0 + +### Minor Changes + +- [#4969](https://github.com/Effect-TS/effect/pull/4969) [`3086405`](https://github.com/Effect-TS/effect/commit/308640563041004d790f08d2ba75cc3a85fdf752) Thanks @tim-smart! - add shard groups, to allow entities to target specific runners + + If you need to migrate an existing cluster, you can run the following PostgreSQL queries: + + ```sql + ALTER TABLE cluster_messages + ALTER COLUMN shard_id TYPE VARCHAR(50) USING format('default:%s', shard_id); + + ALTER TABLE cluster_shards + ALTER COLUMN shard_id TYPE VARCHAR(50) USING format('default:%s', shard_id); + + ALTER TABLE cluster_locks + ALTER COLUMN shard_id TYPE VARCHAR(50) USING format('default:%s', shard_id); + ``` + + To use shard groups, you can add a `ShardGroup` annotation when creating an + entity. You can then assign shard groups to specific runners in your layer + setup: + + ```typescript + import { ClusterSchema, Entity } from "@effect/cluster" + import { + NodeClusterRunnerSocket, + NodeClusterShardManagerSocket + } from "@effect/platform-node" + import { Rpc } from "@effect/rpc" + import { Schema } from "effect" + + const Counter = Entity.make("Counter", [ + Rpc.make("Increment", { + payload: { id: Schema.String, amount: Schema.Number }, + primaryKey: ({ id }) => id, + success: Schema.Number + }) + ]) + .annotate(ClusterSchema.ShardGroup, (_entityId) => "someGroupName") + .annotateRpcs(ClusterSchema.Persisted, true) + + // Assign the shard group to a specific runner in your layer setup. + // + // Make sure to include the "default" shard group, if you want to run entities + // without a specific group. + // + NodeClusterRunnerSocket.layer({ + shardingConfig: { + shardGroups: ["default", "someGroupName"] + } + }) + ``` + +### Patch Changes + +- [#4972](https://github.com/Effect-TS/effect/pull/4972) [`d0067ca`](https://github.com/Effect-TS/effect/commit/d0067caef053b2855d93dcef59ea585d0fad9d8c) Thanks @tim-smart! - optimize for requests with a WithExit reply + +- [#4966](https://github.com/Effect-TS/effect/pull/4966) [`8c79abe`](https://github.com/Effect-TS/effect/commit/8c79abeb47d070d8880b652d31626497d3005a4e) Thanks @tim-smart! - add ClusterCron module + +- Updated dependencies [[`ec52c6a`](https://github.com/Effect-TS/effect/commit/ec52c6a2211e76972462b15b9d5a9d6d56761b7a), [`71e1e6c`](https://github.com/Effect-TS/effect/commit/71e1e6c535c11a3ec498540a3af3c1a313a5319b)]: + - @effect/platform@0.84.5 + - @effect/rpc@0.61.5 + - @effect/sql@0.37.5 + +## 0.37.2 + +### Patch Changes + +- [#4958](https://github.com/Effect-TS/effect/pull/4958) [`6dfbae9`](https://github.com/Effect-TS/effect/commit/6dfbae946ea12ecee7234f5785335f3e7f8335b4) Thanks @tim-smart! - ensure Workflow.interrupt triggers compensation + +- Updated dependencies [[`b8aec45`](https://github.com/Effect-TS/effect/commit/b8aec45288834c499caeb3478a634ea5043fd611), [`b8aec45`](https://github.com/Effect-TS/effect/commit/b8aec45288834c499caeb3478a634ea5043fd611)]: + - @effect/workflow@0.1.2 + +## 0.37.1 + +### Patch Changes + +- Updated dependencies [[`fd60c73`](https://github.com/Effect-TS/effect/commit/fd60c73ea6d51c9b83279da60e7b6d605698b1d8)]: + - @effect/workflow@0.1.1 + +## 0.37.0 + +### Patch Changes + +- [#4945](https://github.com/Effect-TS/effect/pull/4945) [`a116aea`](https://github.com/Effect-TS/effect/commit/a116aeade97c83d8c96f17cdc5cf3b5a0bd9be74) Thanks @tim-smart! - add @effect/workflow package + +- Updated dependencies [[`87722fc`](https://github.com/Effect-TS/effect/commit/87722fce693a9b49284bbddbf82d30714c688261), [`36217ee`](https://github.com/Effect-TS/effect/commit/36217eeb1337edd9ac3f9a635b80a6385d22ae8f), [`a116aea`](https://github.com/Effect-TS/effect/commit/a116aeade97c83d8c96f17cdc5cf3b5a0bd9be74), [`a116aea`](https://github.com/Effect-TS/effect/commit/a116aeade97c83d8c96f17cdc5cf3b5a0bd9be74)]: + - effect@3.16.3 + - @effect/rpc@0.61.4 + - @effect/workflow@0.1.0 + - @effect/platform@0.84.4 + - @effect/sql@0.37.4 + +## 0.36.3 + +### Patch Changes + +- Updated dependencies [[`ab7684f`](https://github.com/Effect-TS/effect/commit/ab7684f1c2a0671bf091f255d220e3a4cc7f528e)]: + - @effect/platform@0.84.3 + - @effect/rpc@0.61.3 + - @effect/sql@0.37.3 + +## 0.36.2 + +### Patch Changes + +- Updated dependencies [[`0ddf148`](https://github.com/Effect-TS/effect/commit/0ddf148a247aa87af043d276b8453a714a400897), [`a77afb1`](https://github.com/Effect-TS/effect/commit/a77afb1f7191a57a68b09fcdee5e9f27a0682b0a)]: + - effect@3.16.2 + - @effect/rpc@0.61.2 + - @effect/platform@0.84.2 + - @effect/sql@0.37.2 + +## 0.36.1 + +### Patch Changes + +- Updated dependencies [[`71174d0`](https://github.com/Effect-TS/effect/commit/71174d09691314a9b6b66189e456fd21e3eb6543), [`d615e6e`](https://github.com/Effect-TS/effect/commit/d615e6e5b944f6fd5e627e31752c7ca7e4e1c17d)]: + - @effect/platform@0.84.1 + - effect@3.16.1 + - @effect/rpc@0.61.1 + - @effect/sql@0.37.1 + +## 0.36.0 + +### Patch Changes + +- Updated dependencies [[`ee0bd5d`](https://github.com/Effect-TS/effect/commit/ee0bd5d24864752c54cb359f67a67dd903971ec4), [`5189800`](https://github.com/Effect-TS/effect/commit/51898004e11766b8cf6d95e960b636f6d5db79ec), [`58bfeaa`](https://github.com/Effect-TS/effect/commit/58bfeaa64ded8c88f772b184311c0c0dbac10960), [`194d748`](https://github.com/Effect-TS/effect/commit/194d7486943f56f3267ef415395ac220a4b3e634), [`918c9ea`](https://github.com/Effect-TS/effect/commit/918c9ea1a57facb154f0fb26792021f337054dee), [`9198e6f`](https://github.com/Effect-TS/effect/commit/9198e6fcc1a3ff4fefb3363004de558d8de01f40), [`2a370bf`](https://github.com/Effect-TS/effect/commit/2a370bf625fdeede5659721468eb0d527e403279), [`58ccb91`](https://github.com/Effect-TS/effect/commit/58ccb91328c8df5d49808b673738bc09df355201), [`fd47834`](https://github.com/Effect-TS/effect/commit/fd478348203fa89462b0a1d067ce4de034353df4)]: + - effect@3.16.0 + - @effect/platform@0.84.0 + - @effect/rpc@0.61.0 + - @effect/sql@0.37.0 + +## 0.35.0 + +### Patch Changes + +- Updated dependencies [[`5522520`](https://github.com/Effect-TS/effect/commit/55225206ab9af0ad60b1c0654690a8a096d625cd), [`cc5bb2b`](https://github.com/Effect-TS/effect/commit/cc5bb2b918a9450a975f702dabcea891bda382cb)]: + - @effect/platform@0.83.0 + - effect@3.15.5 + - @effect/rpc@0.60.0 + - @effect/sql@0.36.0 + +## 0.34.5 + +### Patch Changes + +- [#4929](https://github.com/Effect-TS/effect/pull/4929) [`58c5fd3`](https://github.com/Effect-TS/effect/commit/58c5fd3dd30eceb6c8afea90406768b0e348f48f) Thanks @tim-smart! - disable tracing for internal sql queries + +- Updated dependencies [[`0617b9d`](https://github.com/Effect-TS/effect/commit/0617b9dc365f1963b36949ad7f9023ab6eb94524)]: + - @effect/platform@0.82.8 + - @effect/rpc@0.59.9 + - @effect/sql@0.35.8 + +## 0.34.4 + +### Patch Changes + +- Updated dependencies [[`f570554`](https://github.com/Effect-TS/effect/commit/f57055459524587b041340577dad85476bb35f81), [`78047e8`](https://github.com/Effect-TS/effect/commit/78047e8dfc8005b66f87afe50bb95981fea51561), [`c20b95a`](https://github.com/Effect-TS/effect/commit/c20b95a99ffe452b4774c844d397a905f713b6d6), [`94ada43`](https://github.com/Effect-TS/effect/commit/94ada430928d5685bdbef513e87562c20774a3a2)]: + - effect@3.15.4 + - @effect/platform@0.82.7 + - @effect/rpc@0.59.8 + - @effect/sql@0.35.7 + +## 0.34.3 + +### Patch Changes + +- Updated dependencies [[`618903b`](https://github.com/Effect-TS/effect/commit/618903ba9ae96e2bfe6ee31f61c4359b915f2a36)]: + - @effect/platform@0.82.6 + - @effect/rpc@0.59.7 + - @effect/sql@0.35.6 + +## 0.34.2 + +### Patch Changes + +- Updated dependencies [[`7764a07`](https://github.com/Effect-TS/effect/commit/7764a07d960c60df81f14e1dc949518f4bbe494a), [`4577f54`](https://github.com/Effect-TS/effect/commit/4577f548d67273e576cdde423bdd34a4b910766a), [`30a0d9c`](https://github.com/Effect-TS/effect/commit/30a0d9cb51c84290d51b1361d72ff5cee33c13c7)]: + - @effect/platform@0.82.5 + - effect@3.15.3 + - @effect/rpc@0.59.6 + - @effect/sql@0.35.5 + +## 0.34.1 + +### Patch Changes + +- [#4906](https://github.com/Effect-TS/effect/pull/4906) [`1627a02`](https://github.com/Effect-TS/effect/commit/1627a0299a07c3538ca15293f1ac3ffa7eeb45f3) Thanks @tim-smart! - add Entity.makeTestClient, for testing entity handlers + +- Updated dependencies [[`d45e8a8`](https://github.com/Effect-TS/effect/commit/d45e8a8ac8227192f504e39e6d04fdcf4fb1d225), [`89657ac`](https://github.com/Effect-TS/effect/commit/89657ac2fbda9ba38ac2962ce96949e536a464f9), [`d13b68e`](https://github.com/Effect-TS/effect/commit/d13b68e3a9456d0bfee9bca8273a7b44a9c69087)]: + - @effect/platform@0.82.4 + - @effect/sql@0.35.4 + - @effect/rpc@0.59.5 + +## 0.34.0 + +### Minor Changes + +- [#4883](https://github.com/Effect-TS/effect/pull/4883) [`eaf8405`](https://github.com/Effect-TS/effect/commit/eaf8405ab9bb52423050eb0d23dd7d3c21c18141) Thanks @tim-smart! - add EntityNotAssignedToRunner error + +### Patch Changes + +- Updated dependencies [[`b8722b8`](https://github.com/Effect-TS/effect/commit/b8722b817e2306fe8c8245f3f9e32d85b824b961), [`a328f4b`](https://github.com/Effect-TS/effect/commit/a328f4b4fe717dd53e5b04a30f387433c32f7328)]: + - effect@3.15.2 + - @effect/platform@0.82.3 + - @effect/rpc@0.59.4 + - @effect/sql@0.35.3 + +## 0.33.3 + +### Patch Changes + +- Updated dependencies [[`739a3d4`](https://github.com/Effect-TS/effect/commit/739a3d4a4565915fe2e690003f4f9085cb4422fc)]: + - @effect/platform@0.82.2 + - @effect/rpc@0.59.3 + - @effect/sql@0.35.2 + +## 0.33.2 + +### Patch Changes + +- Updated dependencies [[`787ce70`](https://github.com/Effect-TS/effect/commit/787ce7042e35b657963473c6efe47752868cd811), [`1269641`](https://github.com/Effect-TS/effect/commit/1269641a99ae43069f7648ff79ffe8729b54b348), [`1269641`](https://github.com/Effect-TS/effect/commit/1269641a99ae43069f7648ff79ffe8729b54b348)]: + - effect@3.15.1 + - @effect/platform@0.82.1 + - @effect/rpc@0.59.2 + - @effect/sql@0.35.1 + +## 0.33.1 + +### Patch Changes + +- Updated dependencies [[`6495440`](https://github.com/Effect-TS/effect/commit/64954405eb57313722023b87c0d92761980e2713)]: + - @effect/rpc@0.59.1 + +## 0.33.0 + +### Patch Changes + +- Updated dependencies [[`c654595`](https://github.com/Effect-TS/effect/commit/c65459587b51da140b78098e81fdbfece65d53e2), [`d9f5dea`](https://github.com/Effect-TS/effect/commit/d9f5deae0f02f5de2b9fcb1cca8b142ba4bc2bba), [`49aa723`](https://github.com/Effect-TS/effect/commit/49aa7236a15e13f818c86edbca08c4af67c8dfaf), [`74c14d0`](https://github.com/Effect-TS/effect/commit/74c14d01d0cb48cf517a1b6e29a373a96ed0ff5b), [`e4f49b6`](https://github.com/Effect-TS/effect/commit/e4f49b66857e01b74ab6a9a0bc7132f44cd04cbb), [`6f02224`](https://github.com/Effect-TS/effect/commit/6f02224b3fc46a682ad2defb1a260841956c6780), [`1dcfd41`](https://github.com/Effect-TS/effect/commit/1dcfd41ff96abd706901293a00c1893cb29dd8fd), [`b21ab16`](https://github.com/Effect-TS/effect/commit/b21ab16b6f773e7ec4369db4e752c35e719f7870), [`fcf1822`](https://github.com/Effect-TS/effect/commit/fcf1822f98fcda60351d64e9d2c2c13563d7e6db), [`0061dd1`](https://github.com/Effect-TS/effect/commit/0061dd140740165e91569a684cce27a77b23229e), [`8421e6e`](https://github.com/Effect-TS/effect/commit/8421e6e49332bca8f96f482dfd48680e238b3a89), [`a9b3fb7`](https://github.com/Effect-TS/effect/commit/a9b3fb78abcfdb525318a956fd02fcadeb56143e), [`fa10f56`](https://github.com/Effect-TS/effect/commit/fa10f56b96bd9af070ba99ebc3279aa93954261e)]: + - effect@3.15.0 + - @effect/platform@0.82.0 + - @effect/rpc@0.59.0 + - @effect/sql@0.35.0 + +## 0.32.0 + +### Patch Changes + +- Updated dependencies [[`cd6cd0e`](https://github.com/Effect-TS/effect/commit/cd6cd0eacd6b09d6dd48b30b32edeb4a3c3075f9)]: + - @effect/rpc@0.58.0 + +## 0.31.1 + +### Patch Changes + +- Updated dependencies [[`24a9ebb`](https://github.com/Effect-TS/effect/commit/24a9ebbb5af598f0bfd6ecc45307e528043fe011)]: + - effect@3.14.22 + - @effect/platform@0.81.1 + - @effect/rpc@0.57.1 + - @effect/sql@0.34.1 + +## 0.31.0 + +### Patch Changes + +- Updated dependencies [[`672920f`](https://github.com/Effect-TS/effect/commit/672920f85da8abd5f9d4ad85e29248a2aca57ed8)]: + - @effect/platform@0.81.0 + - @effect/rpc@0.57.0 + - @effect/sql@0.34.0 + +## 0.30.11 + +### Patch Changes + +- Updated dependencies [[`2f3b7d4`](https://github.com/Effect-TS/effect/commit/2f3b7d4e1fa1ef8790b0ca4da22eb88872ee31df)]: + - effect@3.14.21 + - @effect/platform@0.80.21 + - @effect/rpc@0.56.9 + - @effect/sql@0.33.21 + +## 0.30.10 + +### Patch Changes + +- Updated dependencies [[`17e2f30`](https://github.com/Effect-TS/effect/commit/17e2f3091408cf0fca9414d4af3bdf7b2765b378)]: + - effect@3.14.20 + - @effect/platform@0.80.20 + - @effect/rpc@0.56.8 + - @effect/sql@0.33.20 + +## 0.30.9 + +### Patch Changes + +- [#4829](https://github.com/Effect-TS/effect/pull/4829) [`2d55bc5`](https://github.com/Effect-TS/effect/commit/2d55bc52c596afd8381f8ad1badc69efa0be8a78) Thanks @tim-smart! - sending persisted messages without MessageStorage is now a defect + +## 0.30.8 + +### Patch Changes + +- [#4825](https://github.com/Effect-TS/effect/pull/4825) [`1b30f61`](https://github.com/Effect-TS/effect/commit/1b30f616e75580933284657cb2cefab5a7903323) Thanks @tim-smart! - ensure clientOnly mode without storage falls back to network messaging + +## 0.30.7 + +### Patch Changes + +- [#4823](https://github.com/Effect-TS/effect/pull/4823) [`146af39`](https://github.com/Effect-TS/effect/commit/146af39d8d3b4e82aceb13de9749e6c4120c580b) Thanks @tim-smart! - fix sqlite SqlMessagetStorage.repliesFor + +- Updated dependencies [[`056a910`](https://github.com/Effect-TS/effect/commit/056a910d0a0b8b00b0dc9df4a070466b2b5c2f6c), [`e25e7bb`](https://github.com/Effect-TS/effect/commit/e25e7bbc1797733916f48f501425d9f2ef310d9f), [`3273d57`](https://github.com/Effect-TS/effect/commit/3273d572c2b3175a842677f19efeea4cd65ab016)]: + - effect@3.14.19 + - @effect/platform@0.80.19 + - @effect/rpc@0.56.7 + - @effect/sql@0.33.19 + +## 0.30.6 + +### Patch Changes + +- Updated dependencies [[`b1164d4`](https://github.com/Effect-TS/effect/commit/b1164d49a1dfdf299e9971367b6fc6be4df0ddff)]: + - effect@3.14.18 + - @effect/platform@0.80.18 + - @effect/rpc@0.56.6 + - @effect/sql@0.33.18 + +## 0.30.5 + +### Patch Changes + +- Updated dependencies [[`0b54681`](https://github.com/Effect-TS/effect/commit/0b54681cd89245e211d8f49272be0f1bf2f81813), [`41a59d5`](https://github.com/Effect-TS/effect/commit/41a59d5916a296b12b0d5ead9e859e05f40b4cce)]: + - effect@3.14.17 + - @effect/platform@0.80.17 + - @effect/rpc@0.56.5 + - @effect/sql@0.33.17 + +## 0.30.4 + +### Patch Changes + +- Updated dependencies [[`ee14444`](https://github.com/Effect-TS/effect/commit/ee144441021ec77039e43396eaf90714687bb495), [`f1c8583`](https://github.com/Effect-TS/effect/commit/f1c8583f8c3ea9415f813795ca2940a897c9ba9a)]: + - effect@3.14.16 + - @effect/platform@0.80.16 + - @effect/rpc@0.56.4 + - @effect/sql@0.33.16 + +## 0.30.3 + +### Patch Changes + +- Updated dependencies [[`239cc99`](https://github.com/Effect-TS/effect/commit/239cc995ce645946210a3c3d2cb52bd3547c0687), [`8b6c947`](https://github.com/Effect-TS/effect/commit/8b6c947eaa8e45a67ecb3c37d45cd27f3e41d165), [`c50a63b`](https://github.com/Effect-TS/effect/commit/c50a63bbecb9f560b9cae349c447eed877d1b9b6)]: + - effect@3.14.15 + - @effect/platform@0.80.15 + - @effect/rpc@0.56.3 + - @effect/sql@0.33.15 + +## 0.30.2 + +### Patch Changes + +- [#4779](https://github.com/Effect-TS/effect/pull/4779) [`664293f`](https://github.com/Effect-TS/effect/commit/664293f975a282920a7208e966adaf4634c42ef4) Thanks @fubhy! - Fix `FiberRef` module import + +- Updated dependencies [[`6ed8d15`](https://github.com/Effect-TS/effect/commit/6ed8d1589beb181d30abc79afebdaabc1d101538)]: + - effect@3.14.14 + - @effect/platform@0.80.14 + - @effect/rpc@0.56.2 + - @effect/sql@0.33.14 + +## 0.30.1 + +### Patch Changes + +- Updated dependencies [[`ee77788`](https://github.com/Effect-TS/effect/commit/ee77788747e7ebbde6bfa88256cde49dbbad3608), [`5fce6ba`](https://github.com/Effect-TS/effect/commit/5fce6ba19c3cc63cc0104e737e581ad989dedbf0), [`570e45f`](https://github.com/Effect-TS/effect/commit/570e45f8cb936e42ec48f67f21bb2b7252f36c0c)]: + - effect@3.14.13 + - @effect/platform@0.80.13 + - @effect/rpc@0.56.1 + - @effect/sql@0.33.13 + +## 0.30.0 + +### Patch Changes + +- Updated dependencies [[`d6e1156`](https://github.com/Effect-TS/effect/commit/d6e115617fc1a26a846b55f407965a330145dbee), [`2c66c16`](https://github.com/Effect-TS/effect/commit/2c66c16375dc2fe128f7b4e78c5f5c27c25c0d19)]: + - @effect/rpc@0.56.0 + +## 0.29.22 + +### Patch Changes + +- Updated dependencies [[`c2ad9ee`](https://github.com/Effect-TS/effect/commit/c2ad9ee9f3c4c743390edf35ed9e85a20be33811), [`9c68654`](https://github.com/Effect-TS/effect/commit/9c686542b6eb3ea188cb70673ef2e41223633e89)]: + - effect@3.14.12 + - @effect/platform@0.80.12 + - @effect/rpc@0.55.17 + - @effect/sql@0.33.12 + +## 0.29.21 + +### Patch Changes + +- Updated dependencies [[`e536127`](https://github.com/Effect-TS/effect/commit/e536127c1e6f2fb3a542c73ae919435a629a346b), [`b5ad11e`](https://github.com/Effect-TS/effect/commit/b5ad11e511424c6d5c32e34e7ee9d04f0110617d)]: + - effect@3.14.11 + - @effect/rpc@0.55.16 + - @effect/platform@0.80.11 + - @effect/sql@0.33.11 + +## 0.29.20 + +### Patch Changes + +- Updated dependencies [[`d3df84e`](https://github.com/Effect-TS/effect/commit/d3df84e8af8e00a297e2329faeae625de0a95a71)]: + - @effect/rpc@0.55.15 + +## 0.29.19 + +### Patch Changes + +- Updated dependencies [[`bc7efa3`](https://github.com/Effect-TS/effect/commit/bc7efa3b031bb25e1ed3c8f2d3fb5e8da166cadc)]: + - effect@3.14.10 + - @effect/platform@0.80.10 + - @effect/rpc@0.55.14 + - @effect/sql@0.33.10 + +## 0.29.18 + +### Patch Changes + +- Updated dependencies [[`d78249f`](https://github.com/Effect-TS/effect/commit/d78249f0b67f63cf4baf806ff090cba33293daf0)]: + - effect@3.14.9 + - @effect/platform@0.80.9 + - @effect/rpc@0.55.13 + - @effect/sql@0.33.9 + +## 0.29.17 + +### Patch Changes + +- Updated dependencies [[`58eaca9`](https://github.com/Effect-TS/effect/commit/58eaca9ef14032fc310f4a0e3c09513bac1cb50a)]: + - @effect/rpc@0.55.12 + +## 0.29.16 + +### Patch Changes + +- [#4719](https://github.com/Effect-TS/effect/pull/4719) [`a79b732`](https://github.com/Effect-TS/effect/commit/a79b732bddea8bfca091c4fed0dd87aa0b1ab1f0) Thanks @tim-smart! - expire shard locks after 15 seconds + +- [#4719](https://github.com/Effect-TS/effect/pull/4719) [`a79b732`](https://github.com/Effect-TS/effect/commit/a79b732bddea8bfca091c4fed0dd87aa0b1ab1f0) Thanks @tim-smart! - ensure more persisted messages are eligible for the fast path + +## 0.29.15 + +### Patch Changes + +- [#4714](https://github.com/Effect-TS/effect/pull/4714) [`6966708`](https://github.com/Effect-TS/effect/commit/6966708a3061a3eb4bcfcb4d5877657fb41a019a) Thanks @tim-smart! - reset shard last_read on acquistion + +## 0.29.14 + +### Patch Changes + +- [#4712](https://github.com/Effect-TS/effect/pull/4712) [`da21953`](https://github.com/Effect-TS/effect/commit/da21953a3831bf5974ab6add8fcc7fad1c0ba472) Thanks @tim-smart! - attempt to use network for persisted messages where possible + +## 0.29.13 + +### Patch Changes + +- [#4710](https://github.com/Effect-TS/effect/pull/4710) [`896fbbf`](https://github.com/Effect-TS/effect/commit/896fbbf6ed6c11e099747e8aafb67b28edc4e466) Thanks @tim-smart! - tighten cluster health checks to improve worst case recovery time + +- Updated dependencies [[`b3a2d32`](https://github.com/Effect-TS/effect/commit/b3a2d32772e6f7f20eacf2e18128e99324c4d378)]: + - effect@3.14.8 + - @effect/platform@0.80.8 + - @effect/rpc@0.55.11 + - @effect/sql@0.33.8 + +## 0.29.12 + +### Patch Changes + +- Updated dependencies [[`b542a4b`](https://github.com/Effect-TS/effect/commit/b542a4bf195be0c9af1523e1ba96c953decc4d25)]: + - effect@3.14.7 + - @effect/platform@0.80.7 + - @effect/rpc@0.55.10 + - @effect/sql@0.33.7 + +## 0.29.11 + +### Patch Changes + +- Updated dependencies [[`a1d4673`](https://github.com/Effect-TS/effect/commit/a1d4673a423dfed050c0a762664d9d64002cfa90)]: + - @effect/rpc@0.55.9 + +## 0.29.10 + +### Patch Changes + +- Updated dependencies [[`47618c1`](https://github.com/Effect-TS/effect/commit/47618c1ad84ebcc5a51133a3fff5aa5012d49d45), [`6077882`](https://github.com/Effect-TS/effect/commit/60778824a4794336c33807801f813f8751d1c7e4)]: + - effect@3.14.6 + - @effect/platform@0.80.6 + - @effect/rpc@0.55.8 + - @effect/sql@0.33.6 + +## 0.29.9 + +### Patch Changes + +- Updated dependencies [[`4414042`](https://github.com/Effect-TS/effect/commit/44140423a2fb185f92f7db4d5b383f9b62a97bf9)]: + - @effect/rpc@0.55.7 + +## 0.29.8 + +### Patch Changes + +- Updated dependencies [[`40dbfef`](https://github.com/Effect-TS/effect/commit/40dbfeff239b6e567706752114f31b2fce7de4e3), [`85fba81`](https://github.com/Effect-TS/effect/commit/85fba815ac07eb13d4227a69ac76a18e4b94df18), [`5a5ebdd`](https://github.com/Effect-TS/effect/commit/5a5ebdddfaddd259538b4599a6676281faca778e)]: + - effect@3.14.5 + - @effect/platform@0.80.5 + - @effect/rpc@0.55.6 + - @effect/sql@0.33.5 + +## 0.29.7 + +### Patch Changes + +- Updated dependencies [[`e4ba2c6`](https://github.com/Effect-TS/effect/commit/e4ba2c66a878e81b5e295d6d49aaf724b80a28ef), [`e3e5873`](https://github.com/Effect-TS/effect/commit/e3e5873f30080bb0e5eed8a876170acaa6ed47ff), [`26c060c`](https://github.com/Effect-TS/effect/commit/26c060c65914a623220a20356991784f974bfe18)]: + - effect@3.14.4 + - @effect/rpc@0.55.5 + - @effect/platform@0.80.4 + - @effect/sql@0.33.4 + +## 0.29.6 + +### Patch Changes + +- [#4670](https://github.com/Effect-TS/effect/pull/4670) [`34f03d6`](https://github.com/Effect-TS/effect/commit/34f03d66875f21f266f102223a03cd14c2ed6ea6) Thanks @tim-smart! - fix Data.TaggedEnum with generics regression + +- Updated dependencies [[`0ec5e03`](https://github.com/Effect-TS/effect/commit/0ec5e0353a1db5d27c3500deba0df61001258e76), [`05c4d77`](https://github.com/Effect-TS/effect/commit/05c4d772acc42b7425add7b22f914c5ee3ff84bd), [`37aa8e1`](https://github.com/Effect-TS/effect/commit/37aa8e137725a902e70cd1e468ea98b873aa5056), [`34f03d6`](https://github.com/Effect-TS/effect/commit/34f03d66875f21f266f102223a03cd14c2ed6ea6)]: + - @effect/rpc@0.55.4 + - effect@3.14.3 + - @effect/platform@0.80.3 + - @effect/sql@0.33.3 + +## 0.29.5 + +### Patch Changes + +- Updated dependencies [[`f87991b`](https://github.com/Effect-TS/effect/commit/f87991b6d8a2edfaf90b01cebda4b466992ae865), [`f87991b`](https://github.com/Effect-TS/effect/commit/f87991b6d8a2edfaf90b01cebda4b466992ae865), [`0a3e3e1`](https://github.com/Effect-TS/effect/commit/0a3e3e18eea5e0d1882f1a6c906198e6ef226a41)]: + - effect@3.14.2 + - @effect/platform@0.80.2 + - @effect/rpc@0.55.3 + - @effect/sql@0.33.2 + +## 0.29.4 + +### Patch Changes + +- Updated dependencies [[`d2f11e5`](https://github.com/Effect-TS/effect/commit/d2f11e557de4639762124951252170fbf4d7c906)]: + - @effect/rpc@0.55.2 + +## 0.29.3 + +### Patch Changes + +- [#4637](https://github.com/Effect-TS/effect/pull/4637) [`18a7936`](https://github.com/Effect-TS/effect/commit/18a7936832158daa69e3c09a6caae55e3d6c0b86) Thanks @tim-smart! - fix clientOnly mode for SocketRunner + +## 0.29.2 + +### Patch Changes + +- [#4626](https://github.com/Effect-TS/effect/pull/4626) [`3a99a2d`](https://github.com/Effect-TS/effect/commit/3a99a2dbaa38348c1f6e210a531fcfb99b5e73c5) Thanks @tim-smart! - add Sharding.activeEntityCount + +## 0.29.1 + +### Patch Changes + +- [#4621](https://github.com/Effect-TS/effect/pull/4621) [`814733f`](https://github.com/Effect-TS/effect/commit/814733fe62bb3dc91c6cd632d16a8d2076b3755b) Thanks @tim-smart! - remove Sharding.make export + +- Updated dependencies [[`4a274fe`](https://github.com/Effect-TS/effect/commit/4a274fe9f623182b6b902827e0e83bd89ca3b05c)]: + - effect@3.14.1 + - @effect/platform@0.80.1 + - @effect/rpc@0.55.1 + - @effect/sql@0.33.1 + +## 0.29.0 + +### Minor Changes + +- [#4469](https://github.com/Effect-TS/effect/pull/4469) [`3131f8f`](https://github.com/Effect-TS/effect/commit/3131f8fd12ba9eb31b90fa2f42bf88b12309133c) Thanks @tim-smart! - refactor of @effect/cluster packages + +### Patch Changes + +- Updated dependencies [[`1f47e4e`](https://github.com/Effect-TS/effect/commit/1f47e4e12546ab691b29bfb7b5128bb17b93baa5), [`26dd75f`](https://github.com/Effect-TS/effect/commit/26dd75f276a0d8a63eab313bd5a167d5072c9780), [`aba2d1d`](https://github.com/Effect-TS/effect/commit/aba2d1d831ea149481bd4dd755528c0afa8239ce), [`3131f8f`](https://github.com/Effect-TS/effect/commit/3131f8fd12ba9eb31b90fa2f42bf88b12309133c), [`aba2d1d`](https://github.com/Effect-TS/effect/commit/aba2d1d831ea149481bd4dd755528c0afa8239ce), [`aba2d1d`](https://github.com/Effect-TS/effect/commit/aba2d1d831ea149481bd4dd755528c0afa8239ce), [`04dff2d`](https://github.com/Effect-TS/effect/commit/04dff2d01ac68c260f29a6d4743381825c353c86), [`c7fac0c`](https://github.com/Effect-TS/effect/commit/c7fac0cd7eadcd5cc0c3a987051c5b57ad271638), [`aba2d1d`](https://github.com/Effect-TS/effect/commit/aba2d1d831ea149481bd4dd755528c0afa8239ce), [`ffaa3f3`](https://github.com/Effect-TS/effect/commit/ffaa3f3969df26610fcc02ad537340641d44e803), [`ab957c1`](https://github.com/Effect-TS/effect/commit/ab957c1fee714868f56c7ab4e802b9d449e9b666), [`35db9ce`](https://github.com/Effect-TS/effect/commit/35db9ce228f1416c8abacc6dc9c36fbd0f33ef0f), [`aba2d1d`](https://github.com/Effect-TS/effect/commit/aba2d1d831ea149481bd4dd755528c0afa8239ce), [`cf77ea9`](https://github.com/Effect-TS/effect/commit/cf77ea9ab4fc89e66a43f682a9926ccdee6c57ed), [`26dd75f`](https://github.com/Effect-TS/effect/commit/26dd75f276a0d8a63eab313bd5a167d5072c9780), [`baaab60`](https://github.com/Effect-TS/effect/commit/baaab60b737f35dfab8e4a21bce28a195d19e899)]: + - effect@3.14.0 + - @effect/platform@0.80.0 + - @effect/rpc@0.55.0 + - @effect/sql@0.33.0 + +## 0.28.4 + +### Patch Changes + +- Updated dependencies []: + - @effect/sql@0.32.4 + +## 0.28.3 + +### Patch Changes + +- Updated dependencies [[`0c4803f`](https://github.com/Effect-TS/effect/commit/0c4803fcc69262d11a97ce49d0e9b4288df0651f), [`6f65ac4`](https://github.com/Effect-TS/effect/commit/6f65ac4eac1489cd6ea390e18b0908670722adad)]: + - effect@3.13.12 + - @effect/sql@0.32.3 + +## 0.28.2 + +### Patch Changes + +- Updated dependencies [[`fad8cca`](https://github.com/Effect-TS/effect/commit/fad8cca9bbfcc2eaeb44b97c15dbe0a1eda75315), [`4296293`](https://github.com/Effect-TS/effect/commit/4296293049414d0cf2d915a26c552b09f946b9a0), [`9c241ab`](https://github.com/Effect-TS/effect/commit/9c241abe47ccf7a5257b98a4a64a63054a12741d), [`082b0c1`](https://github.com/Effect-TS/effect/commit/082b0c1b9f4252bcdd69608f2e4a9226f953ac3f), [`be12983`](https://github.com/Effect-TS/effect/commit/be12983bc7e7537b41cd8910fc4eb7d1da56ab07), [`de88127`](https://github.com/Effect-TS/effect/commit/de88127a5a5906ccece98af74787b5ae0e65e431)]: + - effect@3.13.11 + - @effect/sql@0.32.2 + +## 0.28.1 + +### Patch Changes + +- Updated dependencies [[`527c964`](https://github.com/Effect-TS/effect/commit/527c9645229f5be9714a7e60a38a9e753c4bbfb1)]: + - effect@3.13.10 + - @effect/sql@0.32.1 + +## 0.28.0 + +### Patch Changes + +- Updated dependencies [[`2976e52`](https://github.com/Effect-TS/effect/commit/2976e52538d9dc9ffdcbc84d4ac748cff9305971)]: + - effect@3.13.9 + - @effect/sql@0.32.0 + +## 0.27.1 + +### Patch Changes + +- Updated dependencies [[`c65d336`](https://github.com/Effect-TS/effect/commit/c65d3362d07ec815ff3b46278314e8a31706ddc2), [`22d2ebb`](https://github.com/Effect-TS/effect/commit/22d2ebb4b11f5a44351a4736e65da391a3b647d0)]: + - effect@3.13.8 + - @effect/sql@0.31.1 + +## 0.27.0 + +### Patch Changes + +- Updated dependencies []: + - @effect/sql@0.31.0 + +## 0.26.7 + +### Patch Changes + +- Updated dependencies [[`840cc73`](https://github.com/Effect-TS/effect/commit/840cc7329908db7ca693ef47b07d4f845c29cadd), [`9bf8a74`](https://github.com/Effect-TS/effect/commit/9bf8a74b967f18d931743dd5196af326c9118e9c), [`87ba23c`](https://github.com/Effect-TS/effect/commit/87ba23c41c193503ed0c612b0d32d0b253794c64)]: + - effect@3.13.7 + - @effect/sql@0.30.7 + +## 0.26.6 + +### Patch Changes + +- Updated dependencies [[`3154ce4`](https://github.com/Effect-TS/effect/commit/3154ce4692fa18b804982158d3c4c8a8a5fae386)]: + - effect@3.13.6 + - @effect/sql@0.30.6 + +## 0.26.5 + +### Patch Changes + +- Updated dependencies [[`367bb35`](https://github.com/Effect-TS/effect/commit/367bb35f4c2a254e1fb211d96db2474a7aed9020), [`6cf11c3`](https://github.com/Effect-TS/effect/commit/6cf11c3a75773ceec2877c85ddc760f381f0866d), [`a0acec8`](https://github.com/Effect-TS/effect/commit/a0acec851f72e19466363d24b9cc218acd00006a)]: + - effect@3.13.5 + - @effect/sql@0.30.5 + +## 0.26.4 + +### Patch Changes + +- Updated dependencies [[`17d9e89`](https://github.com/Effect-TS/effect/commit/17d9e89f9851663bdbb6c1e685601d97806114a4)]: + - effect@3.13.4 + - @effect/sql@0.30.4 + +## 0.26.3 + +### Patch Changes + +- Updated dependencies [[`cc5588d`](https://github.com/Effect-TS/effect/commit/cc5588df07f9103513547cb429ce041b9436a8bd), [`623c8cd`](https://github.com/Effect-TS/effect/commit/623c8cd053ed6ee3d353aaa8778d484670fca2bb), [`00b4eb1`](https://github.com/Effect-TS/effect/commit/00b4eb1ece12a16e222e6220965bb4024d6752ac), [`f2aee98`](https://github.com/Effect-TS/effect/commit/f2aee989b0a600900ce83e7f460d02908620c80f), [`fb798eb`](https://github.com/Effect-TS/effect/commit/fb798eb9061f1191badc017d1aa649360254da20), [`2251b15`](https://github.com/Effect-TS/effect/commit/2251b1528810bb695b37ce388b653cec0c5bf80c), [`2e15c1e`](https://github.com/Effect-TS/effect/commit/2e15c1e33648add0b29fe274fbcb7294b7515085), [`a4979db`](https://github.com/Effect-TS/effect/commit/a4979db021aef16e731be64df196b72088fc4376), [`b74255a`](https://github.com/Effect-TS/effect/commit/b74255a304ad49d60bedb1a260fd697f370af27a), [`d7f6a5c`](https://github.com/Effect-TS/effect/commit/d7f6a5c7d26c1963dcd864ca62360d20d08c7b49), [`9dd8979`](https://github.com/Effect-TS/effect/commit/9dd8979e940915b1cc1b1f264f3d019c77a65a02), [`477b488`](https://github.com/Effect-TS/effect/commit/477b488284f47c5469d7fba3e4065fb7e3b6556e), [`10932cb`](https://github.com/Effect-TS/effect/commit/10932cbf58fc721ada631cebec42f773ce96d3cc), [`9f6c784`](https://github.com/Effect-TS/effect/commit/9f6c78468b3b5e9ebfc38ffdfb70702901ee977b), [`2c639ec`](https://github.com/Effect-TS/effect/commit/2c639ecee332de4266e36022c989c35ae4e02105), [`886aaa8`](https://github.com/Effect-TS/effect/commit/886aaa81e06dfd3cd9391e8ea987d8cd5ada1124)]: + - effect@3.13.3 + - @effect/sql@0.30.3 + +## 0.26.2 + +### Patch Changes + +- Updated dependencies [[`31be72a`](https://github.com/Effect-TS/effect/commit/31be72ada118cb84a942e67b1663263f8db74a9f)]: + - effect@3.13.2 + - @effect/sql@0.30.2 + +## 0.26.1 + +### Patch Changes + +- Updated dependencies [[`b56a211`](https://github.com/Effect-TS/effect/commit/b56a2110569fd0ec0b57ac137743e926d49f51cc)]: + - effect@3.13.1 + - @effect/sql@0.30.1 + +## 0.26.0 + +### Patch Changes + +- Updated dependencies [[`8baef83`](https://github.com/Effect-TS/effect/commit/8baef83e7ff0b7bc0738b680e1ef013065386cff), [`655bfe2`](https://github.com/Effect-TS/effect/commit/655bfe29e44cc3f0fb9b4e53038f50b891c188df), [`d90cbc2`](https://github.com/Effect-TS/effect/commit/d90cbc274e2742d18671fe65aa4764c057eb6cba), [`75632bd`](https://github.com/Effect-TS/effect/commit/75632bd44b8025101d652ccbaeef898c7086c91c), [`c874a2e`](https://github.com/Effect-TS/effect/commit/c874a2e4b17e9d71904ca8375bb77b020975cb1d), [`bf865e5`](https://github.com/Effect-TS/effect/commit/bf865e5833f77fd8f6c06944ca9d507b54488301), [`f98b2b7`](https://github.com/Effect-TS/effect/commit/f98b2b7592cf20f9d85313e7f1e964cb65878138), [`de8ce92`](https://github.com/Effect-TS/effect/commit/de8ce924923eaa4e1b761a97eb45ec967389f3d5), [`cf8b2dd`](https://github.com/Effect-TS/effect/commit/cf8b2dd112f8e092ed99d78fd728db0f91c29050), [`db426a5`](https://github.com/Effect-TS/effect/commit/db426a5fb41ab84d18e3c8753a7329b4de544245), [`6862444`](https://github.com/Effect-TS/effect/commit/6862444094906ad4f2cb077ff3b9cc0b73880c8c), [`5fc8a90`](https://github.com/Effect-TS/effect/commit/5fc8a90ba46a5fd9f3b643f0b5aeadc69d717339), [`546a492`](https://github.com/Effect-TS/effect/commit/546a492e60eb2b8b048a489a474b934ea0877005), [`65c4796`](https://github.com/Effect-TS/effect/commit/65c47966ce39055f02cf5c808daabb3ea6442b0b), [`9760fdc`](https://github.com/Effect-TS/effect/commit/9760fdc37bdaef9da8b150e46b86ddfbe2ad9221), [`5b471e7`](https://github.com/Effect-TS/effect/commit/5b471e7d4317e8ee5d72bbbd3e0c9775160949ab), [`4f810cc`](https://github.com/Effect-TS/effect/commit/4f810cc2770e9f1f266851d2cb6257112c12af49)]: + - effect@3.13.0 + - @effect/sql@0.30.0 + +## 0.25.1 + +### Patch Changes + +- Updated dependencies [[`4018eae`](https://github.com/Effect-TS/effect/commit/4018eaed2733241676ddb8c52416f463a8c32e35), [`543d36d`](https://github.com/Effect-TS/effect/commit/543d36d1a11452560b01ab966a82529ad5fee8c9), [`f70a65a`](https://github.com/Effect-TS/effect/commit/f70a65ac80c6635d80b12beaf4d32a9cc59fa143), [`ba409f6`](https://github.com/Effect-TS/effect/commit/ba409f69c41aeaa29e475c0630735726eaf4dbac), [`3d2e356`](https://github.com/Effect-TS/effect/commit/3d2e3565e8a43d1bdb5daee8db3b90f56d71d859)]: + - effect@3.12.12 + - @effect/sql@0.29.1 + +## 0.25.0 + +### Patch Changes + +- Updated dependencies [[`b6a032f`](https://github.com/Effect-TS/effect/commit/b6a032f07bffa020a848c813881879395134fa20), [`42ddd5f`](https://github.com/Effect-TS/effect/commit/42ddd5f144ce9f9d94a036679ebbd626446d37f5), [`2fe447c`](https://github.com/Effect-TS/effect/commit/2fe447c6354d334f9c591b8a8481818f5f0e797e)]: + - effect@3.12.11 + - @effect/sql@0.29.0 + +## 0.24.4 + +### Patch Changes + +- Updated dependencies [[`e30f132`](https://github.com/Effect-TS/effect/commit/e30f132c336c9d0760bad39f82a55c7ce5159eb7), [`33fa667`](https://github.com/Effect-TS/effect/commit/33fa667c2623be1026e1ccee91bd44f73b09020a), [`87f5f28`](https://github.com/Effect-TS/effect/commit/87f5f2842e4196cb88d13f10f443ff0567e82832), [`4dbd170`](https://github.com/Effect-TS/effect/commit/4dbd170538e8fb7a36aa7c469c6f93b6c7000091)]: + - effect@3.12.10 + - @effect/sql@0.28.4 + +## 0.24.3 + +### Patch Changes + +- Updated dependencies [[`1b4a4e9`](https://github.com/Effect-TS/effect/commit/1b4a4e904ef5227ec7d9114d4e417eca19eed940)]: + - effect@3.12.9 + - @effect/sql@0.28.3 + +## 0.24.2 + +### Patch Changes + +- Updated dependencies [[`766113c`](https://github.com/Effect-TS/effect/commit/766113c0ea3512cdb887650ead8ba314236e22ee), [`712277f`](https://github.com/Effect-TS/effect/commit/712277f949052a24b46e4aa234063a6abf395c90), [`f269122`](https://github.com/Effect-TS/effect/commit/f269122508693b111142994dd48698ddc75f3d69), [`430c846`](https://github.com/Effect-TS/effect/commit/430c846cbac05b187e3d24ac8dfee0cf22506f7c), [`7b03057`](https://github.com/Effect-TS/effect/commit/7b03057507d2dab5e6793beb9c578dedaaeb15fe), [`a9c94c8`](https://github.com/Effect-TS/effect/commit/a9c94c807755610831211a686d2fad849ab38eb4), [`107e6f0`](https://github.com/Effect-TS/effect/commit/107e6f0557a1e2d3b0dce25d62fa1e2601521752), [`65c11b9`](https://github.com/Effect-TS/effect/commit/65c11b9266ec9447c31c26fe3ed35c73bd3b81fd), [`e386d2f`](https://github.com/Effect-TS/effect/commit/e386d2f1b3ab3ac2c14ee76de11f5963d32a3df4), [`9172efb`](https://github.com/Effect-TS/effect/commit/9172efba98bc6a82353e6ec2af61ac08f038ba64)]: + - effect@3.12.8 + - @effect/sql@0.28.2 + +## 0.24.1 + +### Patch Changes + +- Updated dependencies [[`8dff1d1`](https://github.com/Effect-TS/effect/commit/8dff1d1bff76cdba643cad7f0bf864300f08bc61)]: + - effect@3.12.7 + - @effect/sql@0.28.1 + +## 0.24.0 + +### Patch Changes + +- Updated dependencies [[`289c13b`](https://github.com/Effect-TS/effect/commit/289c13b38e8e35b214d46d385d05dead176c87cd), [`8b4e75d`](https://github.com/Effect-TS/effect/commit/8b4e75d35daea807c447ca760948a717aa66bb52), [`fc5e0f0`](https://github.com/Effect-TS/effect/commit/fc5e0f0d357a0051cfa01c1ede83ffdd3cb41ab1), [`004fd2b`](https://github.com/Effect-TS/effect/commit/004fd2bbd1459e64fb1b57f02eeb791ca5ea1ea5), [`b2a31be`](https://github.com/Effect-TS/effect/commit/b2a31be85c35d891351ce4f9a2cc93ece0c257f6), [`5514d05`](https://github.com/Effect-TS/effect/commit/5514d05b5cd586ff5868b8bd41c959e95e6c33cd), [`bf5f0ae`](https://github.com/Effect-TS/effect/commit/bf5f0ae9daa0170471678e22585e8ec14ce667bb), [`3b19bcf`](https://github.com/Effect-TS/effect/commit/3b19bcfd3aaadb6c9253428622df524537c8e626), [`b064b3b`](https://github.com/Effect-TS/effect/commit/b064b3b293615fd268cc5a5647d0981eb67750b8), [`289c13b`](https://github.com/Effect-TS/effect/commit/289c13b38e8e35b214d46d385d05dead176c87cd), [`f474678`](https://github.com/Effect-TS/effect/commit/f474678bf10b8f1c80e3dc096ddc7ecf20b2b23e), [`ee187d0`](https://github.com/Effect-TS/effect/commit/ee187d098007a402844c94d04f0cd8f07695377a)]: + - effect@3.12.6 + - @effect/sql@0.28.0 + +## 0.23.0 + +### Patch Changes + +- Updated dependencies [[`a8b0ddb`](https://github.com/Effect-TS/effect/commit/a8b0ddb84710054799fc8f57485b95d00093ada1), [`507d546`](https://github.com/Effect-TS/effect/commit/507d546bd49db31000425fb5da88c434e4291bea), [`a8b0ddb`](https://github.com/Effect-TS/effect/commit/a8b0ddb84710054799fc8f57485b95d00093ada1), [`8db239b`](https://github.com/Effect-TS/effect/commit/8db239b9c869a3707f6566b9d9dbdf53c4df03fc), [`84a0911`](https://github.com/Effect-TS/effect/commit/84a091181634c3a022c94234cec7764a3aeef1be), [`84a0911`](https://github.com/Effect-TS/effect/commit/84a091181634c3a022c94234cec7764a3aeef1be), [`3179a9f`](https://github.com/Effect-TS/effect/commit/3179a9f65d23369a6a9a1f80f7750566dd28df22), [`6cb9b76`](https://github.com/Effect-TS/effect/commit/6cb9b766396d0b2ed995cf26957359713efd202e), [`1fcbe55`](https://github.com/Effect-TS/effect/commit/1fcbe55345042d8468f6a98c84081bd00b6bcf5a), [`d9a63d9`](https://github.com/Effect-TS/effect/commit/d9a63d9d385653865954cac895065360d54cc56b)]: + - @effect/sql@0.27.0 + - effect@3.12.5 + +## 0.22.1 + +### Patch Changes + +- Updated dependencies [[`5b50ea4`](https://github.com/Effect-TS/effect/commit/5b50ea4a10cf9acd51f9624b2474d9d5ded74019), [`c170a68`](https://github.com/Effect-TS/effect/commit/c170a68b6266100774461fcd6c0e0fabb60112f2), [`a66c2eb`](https://github.com/Effect-TS/effect/commit/a66c2eb473245092cd41f04c2eb2b7b02cf53718)]: + - effect@3.12.4 + - @effect/sql@0.26.1 + +## 0.22.0 + +### Patch Changes + +- Updated dependencies [[`d7dac48`](https://github.com/Effect-TS/effect/commit/d7dac48a477cdfeec509dbe9f33fce6a1b02b63d), [`1d7fd2b`](https://github.com/Effect-TS/effect/commit/1d7fd2b7ee8eeecc912d27adf76ed897db236dc5), [`1d7fd2b`](https://github.com/Effect-TS/effect/commit/1d7fd2b7ee8eeecc912d27adf76ed897db236dc5)]: + - effect@3.12.3 + - @effect/sql@0.26.0 + +## 0.21.2 + +### Patch Changes + +- Updated dependencies [[`734af82`](https://github.com/Effect-TS/effect/commit/734af82138e78b9c57a8355b1c6b80e80d38b222), [`b63c780`](https://github.com/Effect-TS/effect/commit/b63c78010893101520448ddda7019c487cf7eedd), [`c640d77`](https://github.com/Effect-TS/effect/commit/c640d77b33ad417876f4e8ffe8574ee6cbe5607f), [`0def088`](https://github.com/Effect-TS/effect/commit/0def0887cfdb6755729a64dfd52b3b9f46b0576c)]: + - effect@3.12.2 + - @effect/sql@0.25.2 + +## 0.21.1 + +### Patch Changes + +- Updated dependencies [[`302b57d`](https://github.com/Effect-TS/effect/commit/302b57d2cbf9b9ccc17450945aeebfb33cfe8d43), [`0988083`](https://github.com/Effect-TS/effect/commit/0988083d4594938590df5a287e5b27d38526dd07), [`8b46be6`](https://github.com/Effect-TS/effect/commit/8b46be6a3b8160362ab5ea9171c5e6932505125c), [`bfe8027`](https://github.com/Effect-TS/effect/commit/bfe802734b450a4b4ee069d1125dd37995db2bff), [`16dd657`](https://github.com/Effect-TS/effect/commit/16dd657033d8afac2ffea567b3c8bb27c9b249b6), [`39db211`](https://github.com/Effect-TS/effect/commit/39db211414e90c8db8fdad7dc8ce5b4661bcfaef)]: + - effect@3.12.1 + - @effect/sql@0.25.1 + +## 0.21.0 + +### Patch Changes + +- Updated dependencies [[`abb22a4`](https://github.com/Effect-TS/effect/commit/abb22a429b9c52c31e84856294f175d2064a9b4d), [`f369a89`](https://github.com/Effect-TS/effect/commit/f369a89e98bc682969803b9304adaf4557bb36c2), [`642376c`](https://github.com/Effect-TS/effect/commit/642376c63fd7d78754db991631a4d50a5dc79aa3), [`3d2b7a7`](https://github.com/Effect-TS/effect/commit/3d2b7a7e942a7157afae5b1cdbc6f3fef116428e), [`73f9c6f`](https://github.com/Effect-TS/effect/commit/73f9c6f2ff091512cf904cc54ab59965b86e87c8), [`17cb451`](https://github.com/Effect-TS/effect/commit/17cb4514590e8a86263f7aed009f24da8a237342), [`d801820`](https://github.com/Effect-TS/effect/commit/d80182060c2ee945d7e0e4728812abf9465a0d6a), [`e1eeb2d`](https://github.com/Effect-TS/effect/commit/e1eeb2d7064b3870041dab142f3057970699bbf1), [`c11f3a6`](https://github.com/Effect-TS/effect/commit/c11f3a60a05c3b5fc8e7ce90136728154dc505b0), [`618f7e0`](https://github.com/Effect-TS/effect/commit/618f7e092a1011e5090dca1e69b5e9285689654b), [`c0ba834`](https://github.com/Effect-TS/effect/commit/c0ba834d1995cf5a8b250e4780fd43f3e3881151), [`e1eeb2d`](https://github.com/Effect-TS/effect/commit/e1eeb2d7064b3870041dab142f3057970699bbf1)]: + - effect@3.12.0 + - @effect/sql@0.25.0 + +## 0.20.3 + +### Patch Changes + +- Updated dependencies [[`39457d4`](https://github.com/Effect-TS/effect/commit/39457d4897d9bc7df8af5c05d352866bbeae82eb), [`a475cc2`](https://github.com/Effect-TS/effect/commit/a475cc25fd7c9f26b27a8e98f8fbe43cc9e6ee3e), [`199214e`](https://github.com/Effect-TS/effect/commit/199214e21c616d8a0ccd7ed5f92e944e6c580193), [`b3c160d`](https://github.com/Effect-TS/effect/commit/b3c160d7a1fdfc2d3fb2440530f1ab80efc65133)]: + - effect@3.11.10 + - @effect/sql@0.24.3 + +## 0.20.2 + +### Patch Changes + +- Updated dependencies [[`1c08a0b`](https://github.com/Effect-TS/effect/commit/1c08a0b8505badcffb4d9cade5a746ea90c9557e), [`1ce703b`](https://github.com/Effect-TS/effect/commit/1ce703b041bbd7560c5c437c9b9be48f027937fd), [`1ce703b`](https://github.com/Effect-TS/effect/commit/1ce703b041bbd7560c5c437c9b9be48f027937fd)]: + - effect@3.11.9 + - @effect/sql@0.24.2 + +## 0.20.1 + +### Patch Changes + +- Updated dependencies []: + - @effect/sql@0.24.1 + +## 0.20.0 + +### Patch Changes + +- Updated dependencies [[`1a6b52d`](https://github.com/Effect-TS/effect/commit/1a6b52dcf020d36e38a7bc90b648152cf5a8ccba)]: + - effect@3.11.8 + - @effect/sql@0.24.0 + +## 0.19.3 + +### Patch Changes + +- Updated dependencies []: + - @effect/sql@0.23.3 + +## 0.19.2 + +### Patch Changes + +- Updated dependencies [[`2408616`](https://github.com/Effect-TS/effect/commit/24086163b60b09cc6d0885bd565ef080dcbe866b), [`cec0b4d`](https://github.com/Effect-TS/effect/commit/cec0b4d152ef660be2ccdb0927255f2471436e6e), [`cec0b4d`](https://github.com/Effect-TS/effect/commit/cec0b4d152ef660be2ccdb0927255f2471436e6e), [`8d978c5`](https://github.com/Effect-TS/effect/commit/8d978c53f6fcc98d9d645ecba3e4b55d4297dd36), [`cec0b4d`](https://github.com/Effect-TS/effect/commit/cec0b4d152ef660be2ccdb0927255f2471436e6e), [`cec0b4d`](https://github.com/Effect-TS/effect/commit/cec0b4d152ef660be2ccdb0927255f2471436e6e)]: + - effect@3.11.7 + - @effect/sql@0.23.2 + +## 0.19.1 + +### Patch Changes + +- Updated dependencies []: + - @effect/sql@0.23.1 + +## 0.19.0 + +### Patch Changes + +- Updated dependencies [[`662d1ce`](https://github.com/Effect-TS/effect/commit/662d1ce6fb7da384a95888d5b2bb5605bdf3208d), [`31c62d8`](https://github.com/Effect-TS/effect/commit/31c62d83cbdcf9850a8b5331faa239601c60f78a)]: + - effect@3.11.6 + - @effect/sql@0.23.0 + +## 0.18.7 + +### Patch Changes + +- Updated dependencies [[`9f5a6f7`](https://github.com/Effect-TS/effect/commit/9f5a6f701bf7ba31adccd1f1bcfa8ab5614c9be8), [`22905cf`](https://github.com/Effect-TS/effect/commit/22905cf5addfb1ff3d2a6135c52036be958ae911), [`9f5a6f7`](https://github.com/Effect-TS/effect/commit/9f5a6f701bf7ba31adccd1f1bcfa8ab5614c9be8), [`1e59e4f`](https://github.com/Effect-TS/effect/commit/1e59e4fd778da18296812a2a32f36ca8ae50f60d), [`8d914e5`](https://github.com/Effect-TS/effect/commit/8d914e504e7a22d0ea628e8af265ee450ff9530f), [`03bb00f`](https://github.com/Effect-TS/effect/commit/03bb00faa74f9e168a54a8cc0828a664fbb1ab05), [`9f5a6f7`](https://github.com/Effect-TS/effect/commit/9f5a6f701bf7ba31adccd1f1bcfa8ab5614c9be8), [`14e1149`](https://github.com/Effect-TS/effect/commit/14e1149f1af5a022f06eb8c2e4ba9fec17fe7426), [`9f5a6f7`](https://github.com/Effect-TS/effect/commit/9f5a6f701bf7ba31adccd1f1bcfa8ab5614c9be8), [`9f5a6f7`](https://github.com/Effect-TS/effect/commit/9f5a6f701bf7ba31adccd1f1bcfa8ab5614c9be8)]: + - effect@3.11.5 + - @effect/sql@0.22.7 + +## 0.18.6 + +### Patch Changes + +- Updated dependencies []: + - @effect/sql@0.22.6 + +## 0.18.5 + +### Patch Changes + +- Updated dependencies [[`518b258`](https://github.com/Effect-TS/effect/commit/518b258a8a67ecd332a9252c35cc060f8368dee2), [`6e323a3`](https://github.com/Effect-TS/effect/commit/6e323a36faaee46b328c8e3cf60a76b3aff9907f), [`6e323a3`](https://github.com/Effect-TS/effect/commit/6e323a36faaee46b328c8e3cf60a76b3aff9907f)]: + - effect@3.11.4 + - @effect/sql@0.22.5 + +## 0.18.4 + +### Patch Changes + +- Updated dependencies [[`90906f7`](https://github.com/Effect-TS/effect/commit/90906f7f154b12c7182e8f39e3c55ef3937db857), [`3862cd3`](https://github.com/Effect-TS/effect/commit/3862cd3c7f6a542ed65fb81255b3bd696ce2f567), [`3862cd3`](https://github.com/Effect-TS/effect/commit/3862cd3c7f6a542ed65fb81255b3bd696ce2f567), [`343b6aa`](https://github.com/Effect-TS/effect/commit/343b6aa6ac4a74276bfc7c63ccbf4a1d72bc1bed), [`afba339`](https://github.com/Effect-TS/effect/commit/afba339adc11dad56b5a3b7ca94487e58f34d613)]: + - effect@3.11.3 + - @effect/sql@0.22.4 + +## 0.18.3 + +### Patch Changes + +- Updated dependencies []: + - @effect/sql@0.22.3 + +## 0.18.2 + +### Patch Changes + +- Updated dependencies [[`01cee56`](https://github.com/Effect-TS/effect/commit/01cee560b58d94b24cc20e98083251b73e658b41)]: + - effect@3.11.2 + - @effect/sql@0.22.2 + +## 0.18.1 + +### Patch Changes + +- Updated dependencies [[`dd8a2d8`](https://github.com/Effect-TS/effect/commit/dd8a2d8e80d33b16719fc69361eaedf0b59d4620), [`a71bfef`](https://github.com/Effect-TS/effect/commit/a71bfef46f5061bb2502a61a333638a987b62273)]: + - effect@3.11.1 + - @effect/sql@0.22.1 + +## 0.18.0 + +### Patch Changes + +- Updated dependencies [[`147434b`](https://github.com/Effect-TS/effect/commit/147434b03d5e1fd692dd9f126e5ab0910f3b76d3), [`6e69493`](https://github.com/Effect-TS/effect/commit/6e694930048bbaf98110f35f41566aeb9752d471), [`147434b`](https://github.com/Effect-TS/effect/commit/147434b03d5e1fd692dd9f126e5ab0910f3b76d3), [`5eff3f6`](https://github.com/Effect-TS/effect/commit/5eff3f6fa3aae7e86948a62cbfd63b8d6c3bdf92), [`d9fe79b`](https://github.com/Effect-TS/effect/commit/d9fe79bb5a3fe105d8e7a3bc2922a8ad936a5d10), [`251d189`](https://github.com/Effect-TS/effect/commit/251d189420bbba71990574e91098c499065f9a9b), [`5a259f3`](https://github.com/Effect-TS/effect/commit/5a259f3711b4369f55d885b568bdb21136155261), [`b4ce4ea`](https://github.com/Effect-TS/effect/commit/b4ce4ea7fd514a7e572f2dcd879c98f334981b0e), [`15fcc5a`](https://github.com/Effect-TS/effect/commit/15fcc5a0ea4bbf40ab48fa6a04fdda74f76f4c07), [`9bc9a47`](https://github.com/Effect-TS/effect/commit/9bc9a476800dc645903c888a68bb1d3baa3383c6), [`aadb8a4`](https://github.com/Effect-TS/effect/commit/aadb8a48d2cba197c06ec9996505510e48e4e5cb), [`1e2747c`](https://github.com/Effect-TS/effect/commit/1e2747c63a4820d1459cbbc88c71212983bd68bd), [`9264162`](https://github.com/Effect-TS/effect/commit/9264162a82783a651776fb7b87604564a63e7070), [`e0b9b09`](https://github.com/Effect-TS/effect/commit/e0b9b09e70c386b2da17d1f0a15b0511861c89e8), [`c36f3b9`](https://github.com/Effect-TS/effect/commit/c36f3b95df5ce9d71b66f22f26ce12eda8d3e848), [`aadb8a4`](https://github.com/Effect-TS/effect/commit/aadb8a48d2cba197c06ec9996505510e48e4e5cb)]: + - effect@3.11.0 + - @effect/sql@0.22.0 + +## 0.17.4 + +### Patch Changes + +- Updated dependencies [[`3069614`](https://github.com/Effect-TS/effect/commit/30696149271129fc618f6f2ccd1d8f2f6c0f9cd7), [`09a5e52`](https://github.com/Effect-TS/effect/commit/09a5e522fd9b221f05d85b1d1c8a740d4973c302)]: + - effect@3.10.20 + - @effect/sql@0.21.4 + +## 0.17.3 + +### Patch Changes + +- Updated dependencies []: + - @effect/sql@0.21.3 + +## 0.17.2 + +### Patch Changes + +- Updated dependencies []: + - @effect/sql@0.21.2 + +## 0.17.1 + +### Patch Changes + +- Updated dependencies []: + - @effect/sql@0.21.1 + +## 0.17.0 + +### Patch Changes + +- Updated dependencies [[`e9dfea3`](https://github.com/Effect-TS/effect/commit/e9dfea3f394444ebd8929e5cfe05ce740cf84d6e), [`1b1ba09`](https://github.com/Effect-TS/effect/commit/1b1ba099bca49ff48ffe931cc1b607314a5eaafa)]: + - @effect/sql@0.21.0 + +## 0.16.12 + +### Patch Changes + +- Updated dependencies [[`944025b`](https://github.com/Effect-TS/effect/commit/944025bc5ce139f4a85846aa689bf30ec06a8ec1), [`54addee`](https://github.com/Effect-TS/effect/commit/54addee438a644bf010646c52042c7b89c5fc0a7)]: + - effect@3.10.19 + - @effect/sql@0.20.12 + +## 0.16.11 + +### Patch Changes + +- Updated dependencies [[`af409cf`](https://github.com/Effect-TS/effect/commit/af409cf1d2ff973be11cc079ea373eaeedca25de)]: + - effect@3.10.18 + - @effect/sql@0.20.11 + +## 0.16.10 + +### Patch Changes + +- Updated dependencies [[`42c4ce6`](https://github.com/Effect-TS/effect/commit/42c4ce6f8d8c7d847e97757650a8ad9419a829d7)]: + - effect@3.10.17 + - @effect/sql@0.20.10 + +## 0.16.9 + +### Patch Changes + +- Updated dependencies [[`4dca30c`](https://github.com/Effect-TS/effect/commit/4dca30cfcdafe4542e236489f71d6f171a5b4e38), [`1d99867`](https://github.com/Effect-TS/effect/commit/1d998671be3cd11043f232822e91dd8c98fccfa9), [`6dae414`](https://github.com/Effect-TS/effect/commit/6dae4147991a97ec14a99289bd25fadae7541e8d), [`6b0d737`](https://github.com/Effect-TS/effect/commit/6b0d737078bf63b97891e6bc47affc04b28f9cf7), [`d8356aa`](https://github.com/Effect-TS/effect/commit/d8356aad428a0c2290db52380220f81d9ec94232)]: + - effect@3.10.16 + - @effect/sql@0.20.9 + +## 0.16.8 + +### Patch Changes + +- Updated dependencies []: + - @effect/sql@0.20.8 + +## 0.16.7 + +### Patch Changes + +- Updated dependencies []: + - @effect/sql@0.20.7 + +## 0.16.6 + +### Patch Changes + +- Updated dependencies [[`8398b32`](https://github.com/Effect-TS/effect/commit/8398b3208242a88239d4449910b7baf923cfe3b6), [`72e55b7`](https://github.com/Effect-TS/effect/commit/72e55b7c610784fcebdbadc592c876e23e76a986)]: + - effect@3.10.15 + - @effect/sql@0.20.6 + +## 0.16.5 + +### Patch Changes + +- Updated dependencies [[`f983946`](https://github.com/Effect-TS/effect/commit/f9839467b4cad6e788297764ef9f9f0b9fd203f9), [`2d8a750`](https://github.com/Effect-TS/effect/commit/2d8a75081eb83a0a81f817fdf6f428369c5064ab)]: + - effect@3.10.14 + - @effect/sql@0.20.5 + +## 0.16.4 + +### Patch Changes + +- Updated dependencies [[`995bbdf`](https://github.com/Effect-TS/effect/commit/995bbdffea2e332f203cd5b474cd6a1c77dfa6ae)]: + - effect@3.10.13 + - @effect/sql@0.20.4 + +## 0.16.3 + +### Patch Changes + +- Updated dependencies []: + - @effect/sql@0.20.3 + +## 0.16.2 + +### Patch Changes + +- Updated dependencies [[`dd14efe`](https://github.com/Effect-TS/effect/commit/dd14efe0ace255f571273aae876adea96267d7e6)]: + - effect@3.10.12 + - @effect/sql@0.20.2 + +## 0.16.1 + +### Patch Changes + +- Updated dependencies [[`5eef499`](https://github.com/Effect-TS/effect/commit/5eef4998b6ccb7a5404d9e4fef85e57fa35fbb8a)]: + - effect@3.10.11 + - @effect/sql@0.20.1 + +## 0.16.0 + +### Patch Changes + +- Updated dependencies []: + - @effect/sql@0.20.0 + +## 0.15.0 + +### Patch Changes + +- Updated dependencies [[`cd720ae`](https://github.com/Effect-TS/effect/commit/cd720aedf7f2571edec0843d6a633e84e4832b28), [`cd720ae`](https://github.com/Effect-TS/effect/commit/cd720aedf7f2571edec0843d6a633e84e4832b28), [`b631f40`](https://github.com/Effect-TS/effect/commit/b631f40abbe649b2a089764585b5c39f6a695ac6)]: + - effect@3.10.10 + - @effect/sql@0.19.0 + +## 0.14.16 + +### Patch Changes + +- Updated dependencies []: + - @effect/sql@0.18.16 + +## 0.14.15 + +### Patch Changes + +- Updated dependencies [[`a123e80`](https://github.com/Effect-TS/effect/commit/a123e80f111a625428a5b5622b7f55ee1073566b), [`bd5fcd3`](https://github.com/Effect-TS/effect/commit/bd5fcd3e6b603b1e505af90d6a00627c8eca6d41), [`0289d3b`](https://github.com/Effect-TS/effect/commit/0289d3b6391031d00329365bab9791b355031fe3), [`7386b71`](https://github.com/Effect-TS/effect/commit/7386b710e5be570e17f468928a6ed19d549a3e12), [`4211a23`](https://github.com/Effect-TS/effect/commit/4211a2355bb3af3f0e756e2aae9d293379f25662)]: + - effect@3.10.9 + - @effect/sql@0.18.15 + +## 0.14.14 + +### Patch Changes + +- Updated dependencies [[`68b5c9e`](https://github.com/Effect-TS/effect/commit/68b5c9e44f34192cef26e1cadda5e661a027df41), [`9c9928d`](https://github.com/Effect-TS/effect/commit/9c9928dfeacd9ac33dc37eb0ca3d7d8c39175ada), [`6306e66`](https://github.com/Effect-TS/effect/commit/6306e6656092b350d4ede5746da6f245ec9f7e07), [`361c7f3`](https://github.com/Effect-TS/effect/commit/361c7f39a2c10ede9324847c3d3ba192a6f9b20a)]: + - effect@3.10.8 + - @effect/sql@0.18.14 + +## 0.14.13 + +### Patch Changes + +- Updated dependencies [[`33f5b9f`](https://github.com/Effect-TS/effect/commit/33f5b9ffaebea4f1bd0e391b44c41fb6230e743a), [`50f0281`](https://github.com/Effect-TS/effect/commit/50f0281b0d2116726b8927a6217622d5f394f3e4)]: + - effect@3.10.7 + - @effect/sql@0.18.13 + +## 0.14.12 + +### Patch Changes + +- Updated dependencies [[`ce1c21f`](https://github.com/Effect-TS/effect/commit/ce1c21ffc11902ac9ab453a51904207859d38552)]: + - effect@3.10.6 + - @effect/sql@0.18.12 + +## 0.14.11 + +### Patch Changes + +- Updated dependencies [[`3a6d757`](https://github.com/Effect-TS/effect/commit/3a6d757badeebe00d8ef4d67530d073c8264dcfa), [`59d813a`](https://github.com/Effect-TS/effect/commit/59d813aa4973d1115cfc70cc3667508335f49693)]: + - effect@3.10.5 + - @effect/sql@0.18.11 + +## 0.14.10 + +### Patch Changes + +- Updated dependencies [[`2367708`](https://github.com/Effect-TS/effect/commit/2367708be449f9526a2047e321302d7bfb16f18e)]: + - effect@3.10.4 + - @effect/sql@0.18.10 + +## 0.14.9 + +### Patch Changes + +- Updated dependencies []: + - @effect/sql@0.18.9 + +## 0.14.8 + +### Patch Changes + +- Updated dependencies [[`b9423d8`](https://github.com/Effect-TS/effect/commit/b9423d8bf8181a2389fdbce1e3c14ac6fe8d54f5)]: + - effect@3.10.3 + - @effect/sql@0.18.8 + +## 0.14.7 + +### Patch Changes + +- Updated dependencies [[`714e119`](https://github.com/Effect-TS/effect/commit/714e11945e45e5a2554ee058e6c43f82a8e309cf), [`c1afd55`](https://github.com/Effect-TS/effect/commit/c1afd55c54e61f9c432823d21b3d016f79160a37)]: + - effect@3.10.2 + - @effect/sql@0.18.7 + +## 0.14.6 + +### Patch Changes + +- Updated dependencies [[`9604d6b`](https://github.com/Effect-TS/effect/commit/9604d6b616435103dafea8b53637a9d1450b4750)]: + - effect@3.10.1 + - @effect/sql@0.18.6 + +## 0.14.5 + +### Patch Changes + +- Updated dependencies []: + - @effect/sql@0.18.5 + +## 0.14.4 + +### Patch Changes + +- Updated dependencies []: + - @effect/sql@0.18.4 + +## 0.14.3 + +### Patch Changes + +- Updated dependencies []: + - @effect/sql@0.18.3 + +## 0.14.2 + +### Patch Changes + +- Updated dependencies []: + - @effect/sql@0.18.2 + +## 0.14.1 + +### Patch Changes + +- Updated dependencies []: + - @effect/sql@0.18.1 + +## 0.14.0 + +### Patch Changes + +- Updated dependencies [[`4a01828`](https://github.com/Effect-TS/effect/commit/4a01828b66d6213e9bbe18979c893b13f7bb29bf), [`4a01828`](https://github.com/Effect-TS/effect/commit/4a01828b66d6213e9bbe18979c893b13f7bb29bf), [`c79c4c1`](https://github.com/Effect-TS/effect/commit/c79c4c178390fe61ff6dda88c9e058862349343a), [`38d30f0`](https://github.com/Effect-TS/effect/commit/38d30f08b8da62f9c3e308b9250738cb8d17bdb5), [`5821ce3`](https://github.com/Effect-TS/effect/commit/5821ce3455b47d25e0a40cae6ce22af9db5fa556)]: + - effect@3.10.0 + - @effect/sql@0.18.0 + +## 0.13.0 + +### Patch Changes + +- [#3760](https://github.com/Effect-TS/effect/pull/3760) [`dacbf7d`](https://github.com/Effect-TS/effect/commit/dacbf7db59899065aee4e5dd95a6459880e09ceb) Thanks @tim-smart! - add sql-clickhouse & clickhouse dialect + +- Updated dependencies [[`dacbf7d`](https://github.com/Effect-TS/effect/commit/dacbf7db59899065aee4e5dd95a6459880e09ceb)]: + - @effect/sql@0.17.0 + +## 0.12.6 + +### Patch Changes + +- Updated dependencies [[`382556f`](https://github.com/Effect-TS/effect/commit/382556f8930780c0634de681077706113a8c8239), [`97cb014`](https://github.com/Effect-TS/effect/commit/97cb0145114b2cd2f378e98f6c4ff5bf2c1865f5)]: + - @effect/schema@0.75.5 + - @effect/sql@0.16.6 + +## 0.12.5 + +### Patch Changes + +- Updated dependencies []: + - @effect/sql@0.16.5 + +## 0.12.4 + +### Patch Changes + +- Updated dependencies []: + - @effect/sql@0.16.4 + +## 0.12.3 + +### Patch Changes + +- Updated dependencies [[`61a99b2`](https://github.com/Effect-TS/effect/commit/61a99b2bf9d757870ef0c2ec9d4c877cdd364a3d)]: + - effect@3.9.2 + - @effect/schema@0.75.4 + - @effect/sql@0.16.3 + +## 0.12.2 + +### Patch Changes + +- Updated dependencies [[`360ec14`](https://github.com/Effect-TS/effect/commit/360ec14dd4102c526aef7433a8881ad4d9beab75)]: + - @effect/schema@0.75.3 + - @effect/sql@0.16.2 + +## 0.12.1 + +### Patch Changes + +- Updated dependencies []: + - @effect/sql@0.16.1 + +## 0.12.0 + +### Patch Changes + +- Updated dependencies [[`f02b354`](https://github.com/Effect-TS/effect/commit/f02b354ab5b0451143b82bb73dc866be29adec85)]: + - @effect/schema@0.75.2 + - @effect/sql@0.16.0 + +## 0.11.1 + +### Patch Changes + +- Updated dependencies [[`3b2ad1d`](https://github.com/Effect-TS/effect/commit/3b2ad1d58a2e33dc1a72b7037396bd25ca1702a9)]: + - effect@3.9.1 + - @effect/schema@0.75.1 + - @effect/sql@0.15.1 + +## 0.11.0 + +### Patch Changes + +- Updated dependencies [[`ff3d1aa`](https://github.com/Effect-TS/effect/commit/ff3d1aab290b4d1173b2dfc7e4c76abb4babdc16), [`0ba66f2`](https://github.com/Effect-TS/effect/commit/0ba66f2451641fd6990e02ec1ed01c014db9dab0), [`bf77f51`](https://github.com/Effect-TS/effect/commit/bf77f51b323c383224ebf08adf77a7a6e8c9b3cd), [`016f9ad`](https://github.com/Effect-TS/effect/commit/016f9ad931a4b3d09a34e5caf13d87c5b8e9c984), [`0779681`](https://github.com/Effect-TS/effect/commit/07796813f07de035719728733096ba64ce333469), [`534129f`](https://github.com/Effect-TS/effect/commit/534129f8113ce1a8ec50828083e16da9c86326c6), [`d75140c`](https://github.com/Effect-TS/effect/commit/d75140c7a664ceda43142d999f4ff8dcd36d6dda), [`be0451c`](https://github.com/Effect-TS/effect/commit/be0451c149b6618af79cb839cdf04af2db1efb03), [`9237ac6`](https://github.com/Effect-TS/effect/commit/9237ac69bc07de5b3b60076a0ad2921c21de7457), [`be0451c`](https://github.com/Effect-TS/effect/commit/be0451c149b6618af79cb839cdf04af2db1efb03), [`5b36494`](https://github.com/Effect-TS/effect/commit/5b364942e9a9003fdb8217324f8a2d8369c969da), [`c716adb`](https://github.com/Effect-TS/effect/commit/c716adb250ebbea1d1048d818ef7fed4f621d186), [`4986391`](https://github.com/Effect-TS/effect/commit/49863919cd8628c962a712fb1df30d2983820933), [`d75140c`](https://github.com/Effect-TS/effect/commit/d75140c7a664ceda43142d999f4ff8dcd36d6dda), [`d1387ae`](https://github.com/Effect-TS/effect/commit/d1387aebd1ff01bbebde26be46d488956e4daef6)]: + - effect@3.9.0 + - @effect/schema@0.75.0 + - @effect/sql@0.15.0 + +## 0.10.1 + +### Patch Changes + +- Updated dependencies [[`88e85db`](https://github.com/Effect-TS/effect/commit/88e85db34bd402526e27a323e950d053fa34d232), [`83887ca`](https://github.com/Effect-TS/effect/commit/83887ca1b1793916913d8550a4db4450cd14a044), [`5266b6c`](https://github.com/Effect-TS/effect/commit/5266b6cd86d76c3886da041c8829bca04b1a3110), [`cdead5c`](https://github.com/Effect-TS/effect/commit/cdead5c9cfd54dc6c4f215d9732f654c4a12e991), [`766a8af`](https://github.com/Effect-TS/effect/commit/766a8af307b414aca3648d91c4eab7493a5ec862)]: + - effect@3.8.5 + - @effect/schema@0.74.2 + - @effect/sql@0.14.1 + +## 0.10.0 + +### Patch Changes + +- Updated dependencies [[`f100e20`](https://github.com/Effect-TS/effect/commit/f100e2087172d7e4ab8c0d1ee9a5780b9712382a)]: + - @effect/sql@0.14.0 + +## 0.9.4 + +### Patch Changes + +- Updated dependencies []: + - @effect/sql@0.13.4 + +## 0.9.3 + +### Patch Changes + +- Updated dependencies [[`0a68746`](https://github.com/Effect-TS/effect/commit/0a68746c89651c364db2ee8c72dcfe552e1782ea), [`734eae6`](https://github.com/Effect-TS/effect/commit/734eae654f215e4adca457d04d2a1728b1a55c83), [`fd83d0e`](https://github.com/Effect-TS/effect/commit/fd83d0e548feff9ea2d53d370a0b626c4a1d940e), [`4509656`](https://github.com/Effect-TS/effect/commit/45096569d50262275ee984f44c456f5c83b62683), [`ad7e1de`](https://github.com/Effect-TS/effect/commit/ad7e1de948745c0751bfdac96671028ff4b7a727), [`090e41c`](https://github.com/Effect-TS/effect/commit/090e41c636d720b1c7d89684a739855765ed4382), [`090e41c`](https://github.com/Effect-TS/effect/commit/090e41c636d720b1c7d89684a739855765ed4382)]: + - @effect/sql@0.13.3 + - @effect/schema@0.74.1 + - effect@3.8.4 + +## 0.9.2 + +### Patch Changes + +- Updated dependencies []: + - @effect/sql@0.13.2 + +## 0.9.1 + +### Patch Changes + +- Updated dependencies [[`d5c8e7e`](https://github.com/Effect-TS/effect/commit/d5c8e7e47373f9fd78637f24e36d6fb61ee35eb4)]: + - @effect/sql@0.13.1 + +## 0.9.0 + +### Patch Changes + +- Updated dependencies [[`de48aa5`](https://github.com/Effect-TS/effect/commit/de48aa54e98d97722a8a4c2c8f9e1fe1d4560ea2)]: + - @effect/schema@0.74.0 + - @effect/sql@0.13.0 + +## 0.8.6 + +### Patch Changes + +- Updated dependencies [[`bb5ec6b`](https://github.com/Effect-TS/effect/commit/bb5ec6b4b6a6f537394596c5a596faf52cb2aef4)]: + - effect@3.8.3 + - @effect/sql@0.12.6 + - @effect/schema@0.73.4 + +## 0.8.5 + +### Patch Changes + +- Updated dependencies [[`e6440a7`](https://github.com/Effect-TS/effect/commit/e6440a74fb3f12f6422ed794c07cb44af91cbacc)]: + - @effect/schema@0.73.3 + - @effect/sql@0.12.5 + +## 0.8.4 + +### Patch Changes + +- Updated dependencies []: + - @effect/sql@0.12.4 + +## 0.8.3 + +### Patch Changes + +- Updated dependencies [[`f0d8ef1`](https://github.com/Effect-TS/effect/commit/f0d8ef1ce97ec2a87b09b3e24150cfeab85d6e2f)]: + - effect@3.8.2 + - @effect/schema@0.73.2 + - @effect/sql@0.12.3 + +## 0.8.2 + +### Patch Changes + +- Updated dependencies [[`10bf621`](https://github.com/Effect-TS/effect/commit/10bf6213f36d8ddb00f058a4609b85220f3d8334), [`f56ab78`](https://github.com/Effect-TS/effect/commit/f56ab785cbee0c1c43bd2c182c35602f486f61f0), [`ae36fa6`](https://github.com/Effect-TS/effect/commit/ae36fa68f754eeab9a54b6dc0f8b44db513aa2b6)]: + - effect@3.8.1 + - @effect/schema@0.73.1 + - @effect/sql@0.12.2 + +## 0.8.1 + +### Patch Changes + +- Updated dependencies []: + - @effect/sql@0.12.1 + +## 0.8.0 + +### Patch Changes + +- Updated dependencies [[`fcfa6ee`](https://github.com/Effect-TS/effect/commit/fcfa6ee30ffd07d998bf22799357bf58580a116f), [`bb9931b`](https://github.com/Effect-TS/effect/commit/bb9931b62e249a3b801f2cb9d097aec0c8511af7), [`5798f76`](https://github.com/Effect-TS/effect/commit/5798f7619529de33e5ba06f551806f68fedc19db), [`5f0bfa1`](https://github.com/Effect-TS/effect/commit/5f0bfa17205398d4e4818bfbcf9e1b505b3b1fc5), [`7fdf9d9`](https://github.com/Effect-TS/effect/commit/7fdf9d9aa1e2c1c125cbf87991e6efbf4abb7b07), [`812a4e8`](https://github.com/Effect-TS/effect/commit/812a4e86e2d1aa23b477ef5829aa0e5c07784936), [`273565e`](https://github.com/Effect-TS/effect/commit/273565e7901639e8d0541930ab715aea9c80fbaa), [`569a801`](https://github.com/Effect-TS/effect/commit/569a8017ef0a0bc203e4312867cbdd37b0effbd7), [`aa1fa53`](https://github.com/Effect-TS/effect/commit/aa1fa5301e886b9657c8eb0d38cb87cef92a8305), [`02f6b06`](https://github.com/Effect-TS/effect/commit/02f6b0660e12bee1069532a9cc18d3ab855257be), [`12b893e`](https://github.com/Effect-TS/effect/commit/12b893e63cc6dfada4aca7773b4783940e2edf25), [`bbad27e`](https://github.com/Effect-TS/effect/commit/bbad27ec0a90860593f759405caa877e7f4a655f), [`adf7d7a`](https://github.com/Effect-TS/effect/commit/adf7d7a7dfce3a7021e9f3b0d847dc85be89d754), [`007289a`](https://github.com/Effect-TS/effect/commit/007289a52d5877f8e90e2dacf38171ff9bf603fd), [`42a8f99`](https://github.com/Effect-TS/effect/commit/42a8f99740eefdaf2c4544d2c345313f97547a36), [`eebfd29`](https://github.com/Effect-TS/effect/commit/eebfd29633fd5d38b505c5c0842036f61f05e913), [`040703d`](https://github.com/Effect-TS/effect/commit/040703d0e100cd5511e52d812c15492414262b5e)]: + - effect@3.8.0 + - @effect/schema@0.73.0 + - @effect/sql@0.12.0 + +## 0.7.3 + +### Patch Changes + +- Updated dependencies [[`ccd67df`](https://github.com/Effect-TS/effect/commit/ccd67df6b44ae7075a03759bc7866f6bc9ebb03e)]: + - @effect/sql@0.11.3 + +## 0.7.2 + +### Patch Changes + +- Updated dependencies [[`d8aff79`](https://github.com/Effect-TS/effect/commit/d8aff79d4cbedee33d0552ec43fdced007cae358), [`35a0f81`](https://github.com/Effect-TS/effect/commit/35a0f813141652d696461cd5d19fd146adaf85be), [`d8aff79`](https://github.com/Effect-TS/effect/commit/d8aff79d4cbedee33d0552ec43fdced007cae358)]: + - @effect/sql@0.11.2 + - effect@3.7.3 + - @effect/schema@0.72.4 + +## 0.7.1 + +### Patch Changes + +- Updated dependencies []: + - @effect/sql@0.11.1 + +## 0.7.0 + +### Patch Changes + +- Updated dependencies [[`f6acb71`](https://github.com/Effect-TS/effect/commit/f6acb71b17a0e6b0d449e7f661c9e2c3d335fcac)]: + - @effect/schema@0.72.3 + - @effect/sql@0.11.0 + +## 0.6.4 + +### Patch Changes + +- Updated dependencies []: + - @effect/sql@0.10.4 + +## 0.6.3 + +### Patch Changes + +- Updated dependencies []: + - @effect/sql@0.10.3 + +## 0.6.2 + +### Patch Changes + +- Updated dependencies [[`8a601d7`](https://github.com/Effect-TS/effect/commit/8a601d7a1f8ffe52ac9e6d67e9282a1495fe59c9), [`353ba19`](https://github.com/Effect-TS/effect/commit/353ba19f9b2b9e959f0a00d058c6d40a4bc02db7)]: + - effect@3.7.2 + - @effect/schema@0.72.2 + - @effect/sql@0.10.2 + +## 0.6.1 + +### Patch Changes + +- Updated dependencies [[`79859e7`](https://github.com/Effect-TS/effect/commit/79859e71040d8edf1868b8530b90c650f4321eff), [`f6a469c`](https://github.com/Effect-TS/effect/commit/f6a469c190b9f00eee5ea0cd4d5912a0ef8b46f5), [`dcb9ec0`](https://github.com/Effect-TS/effect/commit/dcb9ec0db443894dd204d87450f779c44b9ad7f1), [`79aa6b1`](https://github.com/Effect-TS/effect/commit/79aa6b136e1f29b36f34e88cb2ff162bff2bb4ed)]: + - effect@3.7.1 + - @effect/schema@0.72.1 + - @effect/sql@0.10.1 + +## 0.6.0 + +### Patch Changes + +- Updated dependencies [[`db89601`](https://github.com/Effect-TS/effect/commit/db89601ee9c1050c4e762b7bd7ec65a6a2799dfe), [`2f456cc`](https://github.com/Effect-TS/effect/commit/2f456cce5012b9fcb6b4e039190d527813b75b92), [`8745e41`](https://github.com/Effect-TS/effect/commit/8745e41ed96e3765dc6048efc2a9afbe05c8a1e9), [`e557838`](https://github.com/Effect-TS/effect/commit/e55783886b046d3c5f33447f455f9ccf2fa75922), [`d6e7e40`](https://github.com/Effect-TS/effect/commit/d6e7e40b1e2ad0c59aa02f07344d28601b14ebdc), [`8356321`](https://github.com/Effect-TS/effect/commit/8356321598da04bd77c1001f45a4e447bec5591d), [`192f2eb`](https://github.com/Effect-TS/effect/commit/192f2ebb2c4ddbf4bfd8baedd32140b2376868f4), [`718cb70`](https://github.com/Effect-TS/effect/commit/718cb70038629a6d58d02e407760e341f7c94474), [`e9d0310`](https://github.com/Effect-TS/effect/commit/e9d03107acbf204d9304f3e8aea0816b7d3c7dfb), [`6bf28f7`](https://github.com/Effect-TS/effect/commit/6bf28f7e3b1e5e0608ff567205fea0581d11666f)]: + - effect@3.7.0 + - @effect/schema@0.72.0 + - @effect/sql@0.10.0 + +## 0.5.7 + +### Patch Changes + +- Updated dependencies [[`e809286`](https://github.com/Effect-TS/effect/commit/e8092865900608c4df7a6b7991b1c13cc1e4ca2d)]: + - effect@3.6.8 + - @effect/schema@0.71.4 + - @effect/sql@0.9.7 + +## 0.5.6 + +### Patch Changes + +- Updated dependencies [[`50ec889`](https://github.com/Effect-TS/effect/commit/50ec8897a49b7d1fe84f63107f89d543c52f3dfc)]: + - effect@3.6.7 + - @effect/sql@0.9.6 + - @effect/schema@0.71.3 + +## 0.5.5 + +### Patch Changes + +- Updated dependencies [[`f960bf4`](https://github.com/Effect-TS/effect/commit/f960bf45239e9badac6e0ad3a602f4174cd7bbdf), [`46a575f`](https://github.com/Effect-TS/effect/commit/46a575f48a05457b782fb21f7827d338c9b59320)]: + - effect@3.6.6 + - @effect/schema@0.71.2 + - @effect/sql@0.9.5 + +## 0.5.4 + +### Patch Changes + +- Updated dependencies [[`14a47a8`](https://github.com/Effect-TS/effect/commit/14a47a8c1f3cff2186b8fe7a919a1d773888fb5b), [`35be739`](https://github.com/Effect-TS/effect/commit/35be739a413e32ed251f775714af2f87355e8664), [`f8326cc`](https://github.com/Effect-TS/effect/commit/f8326cc1095630a3fbee3f25d6b4e74edb905903), [`0c09841`](https://github.com/Effect-TS/effect/commit/0c0984173be3d58f050b300a1a8aa89d76ba49ae), [`8dd3959`](https://github.com/Effect-TS/effect/commit/8dd3959e967ca2b38ba601d94a80f1c50e9445e0), [`2cb6ebb`](https://github.com/Effect-TS/effect/commit/2cb6ebbf782b79643befa061c6adcf0366a7b8b3), [`5e9f51e`](https://github.com/Effect-TS/effect/commit/5e9f51e4a1169018d7f59a0db444c783cc1d5794), [`83a108a`](https://github.com/Effect-TS/effect/commit/83a108a254341721d20a82633b1e1d406d2368a3), [`f2c8dbb`](https://github.com/Effect-TS/effect/commit/f2c8dbb77e196c9a36cb3bf2ae3b82ce68e9874d), [`5e9f51e`](https://github.com/Effect-TS/effect/commit/5e9f51e4a1169018d7f59a0db444c783cc1d5794)]: + - effect@3.6.5 + - @effect/sql@0.9.4 + - @effect/schema@0.71.1 + +## 0.5.3 + +### Patch Changes + +- Updated dependencies [[`c3446d3`](https://github.com/Effect-TS/effect/commit/c3446d3e57b0cbfe9341d6f2aebf5f5d6fefefe3)]: + - @effect/sql@0.9.3 + +## 0.5.2 + +### Patch Changes + +- Updated dependencies [[`cfcfbdf`](https://github.com/Effect-TS/effect/commit/cfcfbdfe586b011a5edc28083fd5391edeee0023)]: + - @effect/sql@0.9.2 + +## 0.5.1 + +### Patch Changes + +- Updated dependencies [[`e9da539`](https://github.com/Effect-TS/effect/commit/e9da5396bba99b2ddc20c97c7955154e6da4cab5), [`4fabf75`](https://github.com/Effect-TS/effect/commit/4fabf75b44ea98b1773059bd589167d5d8f64f06)]: + - @effect/sql@0.9.1 + +## 0.5.0 + +### Patch Changes + +- Updated dependencies [[`c1987e2`](https://github.com/Effect-TS/effect/commit/c1987e25c8f5c48bdc9ad223d7a6f2c32f93f5a1), [`8295281`](https://github.com/Effect-TS/effect/commit/8295281ae9bd7441e680402540bf3c8682ec417b), [`c940df6`](https://github.com/Effect-TS/effect/commit/c940df63800bf3c4396d91cf28ec34938642fd2c), [`00b6c6d`](https://github.com/Effect-TS/effect/commit/00b6c6d4001f5de728b7d990a1b14560b4961a63), [`1ceed14`](https://github.com/Effect-TS/effect/commit/1ceed149dc64f4874e64b5cf2f954eba0a5a1f12), [`a07990d`](https://github.com/Effect-TS/effect/commit/a07990de977fb60ab4af1e8f3a2250454dedbb34), [`f8d95a6`](https://github.com/Effect-TS/effect/commit/f8d95a61ad0762147933c5c32bb6d7237e18eef4), [`0e42a8f`](https://github.com/Effect-TS/effect/commit/0e42a8f045ecb1fd3d080edf3d49fef16a9b0ca1)]: + - @effect/schema@0.71.0 + - effect@3.6.4 + - @effect/sql@0.9.0 + +## 0.4.7 + +### Patch Changes + +- Updated dependencies [[`04adcac`](https://github.com/Effect-TS/effect/commit/04adcace913e6fc483df266874a68005e9e04ccf)]: + - effect@3.6.3 + - @effect/schema@0.70.4 + - @effect/sql@0.8.7 + +## 0.4.6 + +### Patch Changes + +- Updated dependencies []: + - @effect/sql@0.8.6 + +## 0.4.5 + +### Patch Changes + +- Updated dependencies [[`99ad841`](https://github.com/Effect-TS/effect/commit/99ad8415293a82d08bd7043c563b29e2b468ca74), [`fd4b2f6`](https://github.com/Effect-TS/effect/commit/fd4b2f6516b325740dde615f1cf0229edf13ca0c)]: + - @effect/schema@0.70.3 + - effect@3.6.2 + - @effect/sql@0.8.5 + +## 0.4.4 + +### Patch Changes + +- Updated dependencies []: + - @effect/sql@0.8.4 + +## 0.4.3 + +### Patch Changes + +- Updated dependencies []: + - @effect/sql@0.8.3 + +## 0.4.2 + +### Patch Changes + +- Updated dependencies [[`510a34d`](https://github.com/Effect-TS/effect/commit/510a34d4cc5d2f51347a53847f6c7db84d2b17c6), [`45dbb9f`](https://github.com/Effect-TS/effect/commit/45dbb9ffeaf93d9e4df99d0cd4920e41ba9a3978)]: + - effect@3.6.1 + - @effect/schema@0.70.2 + - @effect/sql@0.8.2 + +## 0.4.1 + +### Patch Changes + +- Updated dependencies [[`3dce357`](https://github.com/Effect-TS/effect/commit/3dce357efe4a4451d7d29859d08ac11713999b1a), [`657fc48`](https://github.com/Effect-TS/effect/commit/657fc48bb32daf2dc09c9335b3cbc3152bcbdd3b)]: + - @effect/schema@0.70.1 + - @effect/sql@0.8.1 + +## 0.4.0 + +### Patch Changes + +- Updated dependencies [[`42d0706`](https://github.com/Effect-TS/effect/commit/42d07067e9823ceb8977eff9672d9a290941dad5)]: + - @effect/sql@0.8.0 + +## 0.3.0 + +### Patch Changes + +- Updated dependencies [[`1e0fe80`](https://github.com/Effect-TS/effect/commit/1e0fe802b36c257971296617473ce0abe730e8dc), [`8135294`](https://github.com/Effect-TS/effect/commit/8135294b591ea94fde7e6f94a504608f0e630520), [`cd255a4`](https://github.com/Effect-TS/effect/commit/cd255a48872d8fb924cf713ef73f0883a9cc6987), [`3845646`](https://github.com/Effect-TS/effect/commit/3845646828e98f3c7cda1217f6cfe5f642ac0603), [`2d09078`](https://github.com/Effect-TS/effect/commit/2d09078c5948b37fc2f79ef858fe4ca3e4814085), [`4bce5a0`](https://github.com/Effect-TS/effect/commit/4bce5a0274203550ccf117d830721891b0a3d182), [`4ddbff0`](https://github.com/Effect-TS/effect/commit/4ddbff0bb4e3ffddfeb509c59835b83245fb975e), [`e74cc38`](https://github.com/Effect-TS/effect/commit/e74cc38cb420a320c4d7ef98180f19d452a8b316), [`bb069b4`](https://github.com/Effect-TS/effect/commit/bb069b49ef291c532a02c1e8e74271f6d1bb32ec), [`cd255a4`](https://github.com/Effect-TS/effect/commit/cd255a48872d8fb924cf713ef73f0883a9cc6987), [`7d02174`](https://github.com/Effect-TS/effect/commit/7d02174af3bcbf054e5cdddb821c91d0f47e8285)]: + - effect@3.6.0 + - @effect/schema@0.70.0 + - @effect/sql@0.7.0 + +## 0.2.3 + +### Patch Changes + +- Updated dependencies [[`7c0da50`](https://github.com/Effect-TS/effect/commit/7c0da5050d30cb804f4eacb15995d0fb7f3a28d2), [`2fc0ff4`](https://github.com/Effect-TS/effect/commit/2fc0ff4c59c25977018f6ac70ced99b04a8c7b2b), [`6359644`](https://github.com/Effect-TS/effect/commit/635964446323cf55d4060559337e710e4a24496e), [`f262665`](https://github.com/Effect-TS/effect/commit/f262665c2773492c01e5dd0e8d6db235aafaaad8), [`7f41e42`](https://github.com/Effect-TS/effect/commit/7f41e428830bf3043b8be0d28dcd235d5747c942), [`9bbe7a6`](https://github.com/Effect-TS/effect/commit/9bbe7a681430ebf5c10167bb7140ba3742e46bb7), [`f566fd1`](https://github.com/Effect-TS/effect/commit/f566fd1d7eea531a0d981dd24037f14a603a1273)]: + - @effect/schema@0.69.3 + - effect@3.5.9 + - @effect/sql@0.6.3 + +## 0.2.2 + +### Patch Changes + +- Updated dependencies [[`1ba640c`](https://github.com/Effect-TS/effect/commit/1ba640c702f187a866023bf043c26e25cce941ef), [`c8c71bd`](https://github.com/Effect-TS/effect/commit/c8c71bd20eb87d23133dac6156b83bb08941597c), [`a26ce58`](https://github.com/Effect-TS/effect/commit/a26ce581ca7d407e1e81439b58c8045b3fa65231)]: + - effect@3.5.8 + - @effect/sql@0.6.2 + - @effect/schema@0.69.2 + +## 0.2.1 + +### Patch Changes + +- Updated dependencies [[`f241154`](https://github.com/Effect-TS/effect/commit/f241154added5d91e95866c39481f09cdb13bd4d)]: + - @effect/schema@0.69.1 + - @effect/sql@0.6.1 + +## 0.2.0 + +### Patch Changes + +- Updated dependencies [[`20807a4`](https://github.com/Effect-TS/effect/commit/20807a45edeb4334e903dca5d708cd62a71702d8)]: + - @effect/schema@0.69.0 + - @effect/sql@0.6.0 + +## 0.1.3 + +### Patch Changes + +- [#3310](https://github.com/Effect-TS/effect/pull/3310) [`99bddcf`](https://github.com/Effect-TS/effect/commit/99bddcfb3d6eab4d489d055404e26ad81afe52fc) Thanks @fubhy! - Added additional pure annotations to improve tree-shakeability + +- Updated dependencies [[`3afcc93`](https://github.com/Effect-TS/effect/commit/3afcc93413a3d910beb69e4ce9ae120e4adaffd5), [`99bddcf`](https://github.com/Effect-TS/effect/commit/99bddcfb3d6eab4d489d055404e26ad81afe52fc), [`6921c4f`](https://github.com/Effect-TS/effect/commit/6921c4fb8c45badff09b493043b85ca71302b560)]: + - effect@3.5.7 + - @effect/schema@0.68.27 + - @effect/sql@0.5.3 + +## 0.1.2 + +### Patch Changes + +- Updated dependencies [[`f0285d3`](https://github.com/Effect-TS/effect/commit/f0285d3af6a18829123bc1818331c67206becbc4), [`8ec4955`](https://github.com/Effect-TS/effect/commit/8ec49555ed3b3c98093fa4d135a4c57a3f16ebd1), [`3ac2d76`](https://github.com/Effect-TS/effect/commit/3ac2d76048da09e876cf6c3aee3397febd843fe9), [`cc327a1`](https://github.com/Effect-TS/effect/commit/cc327a1bccd22a4ee27ec7e58b53205e93b23e2c), [`4bfe4fb`](https://github.com/Effect-TS/effect/commit/4bfe4fb5c82f597c9beea9baa92e772593598b60), [`2b14d18`](https://github.com/Effect-TS/effect/commit/2b14d181462cad8359da4fa6bc6dfda0f742c398)]: + - @effect/schema@0.68.26 + - effect@3.5.6 + - @effect/sql@0.5.2 + +## 0.1.1 + +### Patch Changes + +- Updated dependencies [[`a9d7800`](https://github.com/Effect-TS/effect/commit/a9d7800f6a253192b653d77778b0674f39b1ca39)]: + - effect@3.5.5 + - @effect/schema@0.68.25 + - @effect/sql@0.5.1 + +## 0.1.0 + +### Patch Changes + +- Updated dependencies [[`53c0db0`](https://github.com/Effect-TS/effect/commit/53c0db06872d5b5edea2a706e83249908385325c), [`ed0dde4`](https://github.com/Effect-TS/effect/commit/ed0dde4888e6f1a97ad5bba06b755d26a6a1c52e), [`ca775ce`](https://github.com/Effect-TS/effect/commit/ca775cec53baebc1a43d9b8852a3ac6726178498), [`5be9cc0`](https://github.com/Effect-TS/effect/commit/5be9cc044025a9541b9b7acefa2d3fc05fa1301b), [`203658f`](https://github.com/Effect-TS/effect/commit/203658f8001c132b25764ab70344b171683b554c), [`eb1c4d4`](https://github.com/Effect-TS/effect/commit/eb1c4d44e54b9d8d201a366d1ff94face2a6dcd3)]: + - @effect/sql@0.5.0 + - effect@3.5.4 + - @effect/schema@0.68.24 + +## 0.0.32 + +### Patch Changes + +- Updated dependencies [[`edb0da3`](https://github.com/Effect-TS/effect/commit/edb0da383746d760f35d8582f5fb0cc0eeca9217), [`edb0da3`](https://github.com/Effect-TS/effect/commit/edb0da383746d760f35d8582f5fb0cc0eeca9217), [`c8d3fb0`](https://github.com/Effect-TS/effect/commit/c8d3fb0fe23585f6efb724af51fbab3ba1ad6e83), [`dabd028`](https://github.com/Effect-TS/effect/commit/dabd028decf9b7983ca16ebe0f48c05c11a84b68), [`786b2ab`](https://github.com/Effect-TS/effect/commit/786b2ab29d525c877bb84035dac9e2d6499339d1), [`fc57354`](https://github.com/Effect-TS/effect/commit/fc573547d41667016fce05eaee75960fcc6dce4d)]: + - effect@3.5.3 + - @effect/schema@0.68.23 + - @effect/sql@0.4.27 + +## 0.0.31 + +### Patch Changes + +- Updated dependencies [[`639208e`](https://github.com/Effect-TS/effect/commit/639208eeb8a44622994f832bc2d45d06ab636bc8), [`6684b4c`](https://github.com/Effect-TS/effect/commit/6684b4c27d77a7fcc7af2e261a450edf971b62b5), [`6684b4c`](https://github.com/Effect-TS/effect/commit/6684b4c27d77a7fcc7af2e261a450edf971b62b5), [`6684b4c`](https://github.com/Effect-TS/effect/commit/6684b4c27d77a7fcc7af2e261a450edf971b62b5)]: + - effect@3.5.2 + - @effect/schema@0.68.22 + - @effect/sql@0.4.26 + +## 0.0.30 + +### Patch Changes + +- Updated dependencies []: + - @effect/sql@0.4.25 + +## 0.0.29 + +### Patch Changes + +- Updated dependencies [[`55fdd76`](https://github.com/Effect-TS/effect/commit/55fdd761ee95afd73b6a892c13fee92b36c02837)]: + - effect@3.5.1 + - @effect/schema@0.68.21 + - @effect/sql@0.4.24 + +## 0.0.28 + +### Patch Changes + +- Updated dependencies [[`a1f5b83`](https://github.com/Effect-TS/effect/commit/a1f5b831a1bc7535988b370d68d0b3eb1123e0ce), [`a1f5b83`](https://github.com/Effect-TS/effect/commit/a1f5b831a1bc7535988b370d68d0b3eb1123e0ce), [`a1f5b83`](https://github.com/Effect-TS/effect/commit/a1f5b831a1bc7535988b370d68d0b3eb1123e0ce), [`60bc3d0`](https://github.com/Effect-TS/effect/commit/60bc3d0867b13e48b24dc22604b4dd2e7b2c1ca4), [`5ab348f`](https://github.com/Effect-TS/effect/commit/5ab348f265db3d283aa091ddca6d2d49137c16f2), [`60bc3d0`](https://github.com/Effect-TS/effect/commit/60bc3d0867b13e48b24dc22604b4dd2e7b2c1ca4), [`3e04bf8`](https://github.com/Effect-TS/effect/commit/3e04bf8a7127e956cadb7684a8f4c661df57663b), [`e7fc45f`](https://github.com/Effect-TS/effect/commit/e7fc45f0c7002aafdaec7878149ac064cd104ea3), [`a1f5b83`](https://github.com/Effect-TS/effect/commit/a1f5b831a1bc7535988b370d68d0b3eb1123e0ce), [`4626de5`](https://github.com/Effect-TS/effect/commit/4626de59c25b384216faa0be87bf0b8cd36357d0), [`f01e7db`](https://github.com/Effect-TS/effect/commit/f01e7db317827255d7901f523f2e28b43298e8df), [`60bc3d0`](https://github.com/Effect-TS/effect/commit/60bc3d0867b13e48b24dc22604b4dd2e7b2c1ca4), [`79d2d91`](https://github.com/Effect-TS/effect/commit/79d2d91464d95dde0e9444d43e7a7f309f05d6e6), [`ac71f37`](https://github.com/Effect-TS/effect/commit/ac71f378f2413e5aa91c95f649ffe898d6a26114), [`8432360`](https://github.com/Effect-TS/effect/commit/8432360ce68614a419bb328083a4109d0fc8aa93), [`e4bf1bf`](https://github.com/Effect-TS/effect/commit/e4bf1bf2b4a970eacd77c9b77b5ea8c68bc84498), [`13cb861`](https://github.com/Effect-TS/effect/commit/13cb861a5eded15c55c6cdcf6a8acde8320367a6), [`79d2d91`](https://github.com/Effect-TS/effect/commit/79d2d91464d95dde0e9444d43e7a7f309f05d6e6), [`e7fc45f`](https://github.com/Effect-TS/effect/commit/e7fc45f0c7002aafdaec7878149ac064cd104ea3), [`9f66825`](https://github.com/Effect-TS/effect/commit/9f66825f1fce0fe8d10420c285f7dc4c71e8af8d)]: + - effect@3.5.0 + - @effect/schema@0.68.20 + - @effect/sql@0.4.23 + +## 0.0.27 + +### Patch Changes + +- Updated dependencies [[`7af137c`](https://github.com/Effect-TS/effect/commit/7af137c9433f6e74959b3887561ec1e6f12e10ee), [`ee4b3dc`](https://github.com/Effect-TS/effect/commit/ee4b3dc5f68d19dc3ae1c2d12901c5b8ffbebabb), [`097d25c`](https://github.com/Effect-TS/effect/commit/097d25cb5d13c049e01789651be56b09620186ef)]: + - effect@3.4.9 + - @effect/schema@0.68.19 + - @effect/sql@0.4.22 + +## 0.0.26 + +### Patch Changes + +- Updated dependencies [[`5d5cc6c`](https://github.com/Effect-TS/effect/commit/5d5cc6cfd7d63b07081290fb189b364999201fc5), [`a435e0f`](https://github.com/Effect-TS/effect/commit/a435e0fc5378b33a49bcec92ee235df6f16a2419), [`b5554db`](https://github.com/Effect-TS/effect/commit/b5554db36c4dd6f64fa5e6a62a29b2759c54217a), [`359ff8a`](https://github.com/Effect-TS/effect/commit/359ff8aa2e4e6389bf56d759baa804e2a7674a16), [`a9c4fb3`](https://github.com/Effect-TS/effect/commit/a9c4fb3bf3c6e92cd1c142b0605fddf7eb3c697c), [`f7534b9`](https://github.com/Effect-TS/effect/commit/f7534b94cba06b143a3d4f29275d92874a939559)]: + - @effect/schema@0.68.18 + - effect@3.4.8 + - @effect/sql@0.4.21 + +## 0.0.25 + +### Patch Changes + +- Updated dependencies [[`15967cf`](https://github.com/Effect-TS/effect/commit/15967cf18931fb6ede3083eb687a8dfff371cc56), [`2328e17`](https://github.com/Effect-TS/effect/commit/2328e17577112db17c29b7756942a0ff64a70ee0), [`a5737d6`](https://github.com/Effect-TS/effect/commit/a5737d6db2b921605c332eabbc5402ee3d17357b)]: + - @effect/schema@0.68.17 + - effect@3.4.7 + - @effect/sql@0.4.20 + +## 0.0.24 + +### Patch Changes + +- Updated dependencies [[`d006cec`](https://github.com/Effect-TS/effect/commit/d006cec022e8524dbfd6dc6df751fe4c86b10042), [`cb22726`](https://github.com/Effect-TS/effect/commit/cb2272656881aa5878a1c3fc0b12d8fbc66eb63c), [`e911cfd`](https://github.com/Effect-TS/effect/commit/e911cfdc79418462d7e9000976fded15ea6b738d)]: + - @effect/schema@0.68.16 + - @effect/sql@0.4.19 + +## 0.0.23 + +### Patch Changes + +- Updated dependencies []: + - @effect/sql@0.4.18 + +## 0.0.22 + +### Patch Changes + +- Updated dependencies [[`5c0ceb0`](https://github.com/Effect-TS/effect/commit/5c0ceb00826cce9e50bf9d41d83e191d5352c030), [`5c0ceb0`](https://github.com/Effect-TS/effect/commit/5c0ceb00826cce9e50bf9d41d83e191d5352c030), [`34faeb6`](https://github.com/Effect-TS/effect/commit/34faeb6305ba52af4d6f8bdd2e633bb6a5a7a35b), [`33735b1`](https://github.com/Effect-TS/effect/commit/33735b16b41bd26929d8f4754c190925db6323b7), [`5c0ceb0`](https://github.com/Effect-TS/effect/commit/5c0ceb00826cce9e50bf9d41d83e191d5352c030), [`139d4b3`](https://github.com/Effect-TS/effect/commit/139d4b39fb3bff2eeaa7c0c809c581da42425a83)]: + - effect@3.4.6 + - @effect/schema@0.68.15 + - @effect/sql@0.4.17 + +## 0.0.21 + +### Patch Changes + +- Updated dependencies [[`61e5964`](https://github.com/Effect-TS/effect/commit/61e59640fd993216cca8ace0ac8abd9104e213ce)]: + - @effect/schema@0.68.14 + - @effect/sql@0.4.16 + +## 0.0.20 + +### Patch Changes + +- Updated dependencies [[`cb76bcb`](https://github.com/Effect-TS/effect/commit/cb76bcb2f8858a90db4f785efee262cea1b9844e)]: + - @effect/schema@0.68.13 + - @effect/sql@0.4.15 + +## 0.0.19 + +### Patch Changes + +- Updated dependencies []: + - @effect/sql@0.4.14 + +## 0.0.18 + +### Patch Changes + +- Updated dependencies [[`a047af9`](https://github.com/Effect-TS/effect/commit/a047af99447dfffc729e9c8ef0ca143537927e91), [`d990544`](https://github.com/Effect-TS/effect/commit/d9905444b9e800850cb65899114ca0e502e68fe8)]: + - effect@3.4.5 + - @effect/schema@0.68.12 + - @effect/sql@0.4.13 + +## 0.0.17 + +### Patch Changes + +- Updated dependencies [[`72638e3`](https://github.com/Effect-TS/effect/commit/72638e3d99f0e93a24febf6c225256ce92d4a20b), [`d7dde2b`](https://github.com/Effect-TS/effect/commit/d7dde2b4af08b37af859d4c327c1f5c6f00cf9d9), [`9b2fc3b`](https://github.com/Effect-TS/effect/commit/9b2fc3b9dfd304a2bd0508ef2313cfc54357be0c), [`d71c192`](https://github.com/Effect-TS/effect/commit/d71c192b89fd1162423acddc5fd3d6270fbf2ef6)]: + - effect@3.4.4 + - @effect/schema@0.68.11 + - @effect/sql@0.4.12 + +## 0.0.16 + +### Patch Changes + +- Updated dependencies []: + - @effect/sql@0.4.11 + +## 0.0.15 + +### Patch Changes + +- [#3079](https://github.com/Effect-TS/effect/pull/3079) [`bbdd365`](https://github.com/Effect-TS/effect/commit/bbdd36567706c94cdec45bacea825941c347b6cd) Thanks @tim-smart! - update to typescript 5.5 + +- Updated dependencies [[`c342739`](https://github.com/Effect-TS/effect/commit/c3427396226e1ad7b95b40595a23f9bdff3e3365), [`8898e5e`](https://github.com/Effect-TS/effect/commit/8898e5e238622f6337583d91ee23609c1f5ccdf7), [`ff78636`](https://github.com/Effect-TS/effect/commit/ff786367c522975f40f0f179a0ecdfcfab7ecbdb), [`c86bd4e`](https://github.com/Effect-TS/effect/commit/c86bd4e134c23146c216f9ff97e03781d55991b6), [`bbdd365`](https://github.com/Effect-TS/effect/commit/bbdd36567706c94cdec45bacea825941c347b6cd), [`bbdd365`](https://github.com/Effect-TS/effect/commit/bbdd36567706c94cdec45bacea825941c347b6cd)]: + - effect@3.4.3 + - @effect/schema@0.68.10 + - @effect/sql@0.4.10 + +## 0.0.14 + +### Patch Changes + +- Updated dependencies [[`0b47fdf`](https://github.com/Effect-TS/effect/commit/0b47fdfe449f42de89e0e88b61ae5140f629e5c4)]: + - @effect/schema@0.68.9 + - @effect/sql@0.4.9 + +## 0.0.13 + +### Patch Changes + +- Updated dependencies [[`192261b`](https://github.com/Effect-TS/effect/commit/192261b2aec94e9913ceed83683fdcfbc9fca66f), [`3da1497`](https://github.com/Effect-TS/effect/commit/3da1497b5c9cc886d300258bc928fd68a4fefe6f)]: + - @effect/schema@0.68.8 + - effect@3.4.2 + - @effect/sql@0.4.8 + +## 0.0.12 + +### Patch Changes + +- Updated dependencies []: + - @effect/sql@0.4.7 + +## 0.0.11 + +### Patch Changes + +- Updated dependencies [[`66a1910`](https://github.com/Effect-TS/effect/commit/66a19109ff90c4252123b8809b8c8a74681dba6a)]: + - effect@3.4.1 + - @effect/schema@0.68.7 + - @effect/sql@0.4.6 + +## 0.0.10 + +### Patch Changes + +- Updated dependencies []: + - @effect/sql@0.4.5 + +## 0.0.9 + +### Patch Changes + +- Updated dependencies []: + - @effect/sql@0.4.4 + +## 0.0.8 + +### Patch Changes + +- Updated dependencies [[`530fa9e`](https://github.com/Effect-TS/effect/commit/530fa9e36b8532589b948fc4faa37593f36b7f42)]: + - @effect/schema@0.68.6 + - @effect/sql@0.4.3 + +## 0.0.7 + +### Patch Changes + +- Updated dependencies [[`1d62815`](https://github.com/Effect-TS/effect/commit/1d62815a50f34115606940ffa397442d75a20c81)]: + - @effect/schema@0.68.5 + - @effect/sql@0.4.2 + +## 0.0.6 + +### Patch Changes + +- Updated dependencies []: + - @effect/sql@0.4.1 + +## 0.0.5 + +### Patch Changes + +- [#3035](https://github.com/Effect-TS/effect/pull/3035) [`d33d8b0`](https://github.com/Effect-TS/effect/commit/d33d8b050b8e3c87dcde9587083e6c1cf733f72b) Thanks @tim-smart! - restructure sql modules to have flat imports + +- Updated dependencies [[`c0ce180`](https://github.com/Effect-TS/effect/commit/c0ce180861ad0938053c0e6145e813fa6404df3b), [`d33d8b0`](https://github.com/Effect-TS/effect/commit/d33d8b050b8e3c87dcde9587083e6c1cf733f72b), [`61707b6`](https://github.com/Effect-TS/effect/commit/61707b6ffc7397c2ba0dce22512b44955724f60f), [`9c1b5b3`](https://github.com/Effect-TS/effect/commit/9c1b5b39e6c19604ce834f072a114ad392c50a06), [`a35faf8`](https://github.com/Effect-TS/effect/commit/a35faf8d116f94899bfc03feab33b004c8ddfdf7), [`ff73c0c`](https://github.com/Effect-TS/effect/commit/ff73c0cacd66132bfad2e5211b3eae347729c667), [`984d516`](https://github.com/Effect-TS/effect/commit/984d516ccd9412dc41188f6a46b748dd20dd5848), [`8c3b8a2`](https://github.com/Effect-TS/effect/commit/8c3b8a2ce208eab753b6206a51605a424f104e98), [`017e2f9`](https://github.com/Effect-TS/effect/commit/017e2f9b371ce24ea4945e5d7390c934ad3c39cf), [`91bf8a2`](https://github.com/Effect-TS/effect/commit/91bf8a2e9d1959393b3cf7366cc1d584d3e666b7), [`c6a4a26`](https://github.com/Effect-TS/effect/commit/c6a4a266606575fd2c7165940c4072ad4c57d01f)]: + - effect@3.4.0 + - @effect/sql@0.4.0 + - @effect/schema@0.68.4 + +## 0.0.4 + +### Patch Changes + +- Updated dependencies []: + - @effect/sql@0.3.18 + +## 0.0.3 + +### Patch Changes + +- Updated dependencies [[`d473800`](https://github.com/Effect-TS/effect/commit/d47380012c3241d7287b66968d33a2414275ce7b)]: + - @effect/schema@0.68.3 + - @effect/sql@0.3.17 + +## 0.0.2 + +### Patch Changes + +- Updated dependencies [[`eb341b3`](https://github.com/Effect-TS/effect/commit/eb341b3eb34ad64499371bc08b7f59e429979d8a)]: + - @effect/schema@0.68.2 + - @effect/sql@0.3.16 diff --git a/repos/effect/packages/cluster/LICENSE b/repos/effect/packages/cluster/LICENSE new file mode 100644 index 0000000..7f6fe48 --- /dev/null +++ b/repos/effect/packages/cluster/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023-present The Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/repos/effect/packages/cluster/README.md b/repos/effect/packages/cluster/README.md new file mode 100644 index 0000000..e0edb8b --- /dev/null +++ b/repos/effect/packages/cluster/README.md @@ -0,0 +1,5 @@ +## Documentation + +- **API Reference**: [View the full documentation](https://effect-ts.github.io/effect/docs/cluster). + +. diff --git a/repos/effect/packages/cluster/docgen.json b/repos/effect/packages/cluster/docgen.json new file mode 100644 index 0000000..573861b --- /dev/null +++ b/repos/effect/packages/cluster/docgen.json @@ -0,0 +1,38 @@ +{ + "$schema": "../../node_modules/@effect/docgen/schema.json", + "srcLink": "https://github.com/Effect-TS/effect/tree/main/packages/cluster/src/", + "exclude": ["src/internal/**/*.ts"], + "examplesCompilerOptions": { + "noEmit": true, + "strict": true, + "skipLibCheck": true, + "moduleResolution": "Bundler", + "module": "ES2022", + "target": "ES2022", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "paths": { + "effect": ["../../../effect/src/index.js"], + "effect/*": ["../../../effect/src/*.js"], + "@effect/cluster": ["../../../cluster/src/index.js"], + "@effect/cluster/*": ["../../../cluster/src/*.js"], + "@effect/experimental": ["../../../experimental/src/index.js"], + "@effect/experimental/*": ["../../../experimental/src/*.js"], + "@effect/platform": ["../../../platform/src/index.js"], + "@effect/platform/*": ["../../../platform/src/*.js"], + "@effect/platform-node": ["../../../platform-node/src/index.js"], + "@effect/platform-node/*": ["../../../platform-node/src/*.js"], + "@effect/platform-node-shared": [ + "../../../platform-node-shared/src/index.js" + ], + "@effect/platform-node-shared/*": [ + "../../../platform-node-shared/src/*.js" + ], + "@effect/rpc": ["../../../rpc/src/index.js"], + "@effect/rpc/*": ["../../../rpc/src/*.js"], + "@effect/sql": ["../../../sql/src/index.js"], + "@effect/sql/*": ["../../../sql/src/*.js"], + "@effect/workflow": ["../../../workflow/src/index.js"], + "@effect/workflow/*": ["../../../workflow/src/*.js"] + } + } +} diff --git a/repos/effect/packages/cluster/package.json b/repos/effect/packages/cluster/package.json new file mode 100644 index 0000000..dc8e7c8 --- /dev/null +++ b/repos/effect/packages/cluster/package.json @@ -0,0 +1,60 @@ +{ + "name": "@effect/cluster", + "type": "module", + "version": "0.58.3", + "description": "Unified interfaces for common cluster-specific services", + "publishConfig": { + "access": "public", + "provenance": true, + "directory": "dist", + "linkDirectory": false + }, + "repository": { + "type": "git", + "url": "https://github.com/Effect-TS/effect.git", + "directory": "packages/cluster" + }, + "homepage": "https://effect.website", + "license": "MIT", + "exports": { + "./package.json": "./package.json", + ".": "./src/index.ts", + "./*": "./src/*.ts", + "./internal/*": null + }, + "scripts": { + "codegen": "build-utils prepare-v3", + "build": "pnpm build-esm && pnpm build-annotate && pnpm build-cjs && build-utils pack-v3", + "build-esm": "tsc -b tsconfig.build.json", + "build-cjs": "babel build/esm --plugins @babel/transform-export-namespace-from --plugins @babel/transform-modules-commonjs --out-dir build/cjs --source-maps", + "build-annotate": "babel build/esm --plugins annotate-pure-calls --out-dir build/esm --source-maps", + "check": "tsc -b tsconfig.json", + "test": "vitest", + "coverage": "vitest --coverage" + }, + "peerDependencies": { + "@effect/platform": "workspace:^", + "@effect/rpc": "workspace:^", + "@effect/sql": "workspace:^", + "@effect/workflow": "workspace:^", + "effect": "workspace:^" + }, + "devDependencies": { + "@effect/platform": "workspace:^", + "@effect/platform-node": "workspace:^", + "@effect/rpc": "workspace:^", + "@effect/sql": "workspace:^", + "@effect/sql-mysql2": "workspace:^", + "@effect/sql-pg": "workspace:^", + "@effect/sql-sqlite-node": "workspace:^", + "@effect/workflow": "workspace:^", + "@testcontainers/mysql": "^10.25.0", + "@testcontainers/postgresql": "^10.25.0", + "@types/pg": "^8.15.6", + "effect": "workspace:^", + "pg": "^8.16.3" + }, + "dependencies": { + "kubernetes-types": "^1.30.0" + } +} diff --git a/repos/effect/packages/cluster/src/ClusterCron.ts b/repos/effect/packages/cluster/src/ClusterCron.ts new file mode 100644 index 0000000..237d8a7 --- /dev/null +++ b/repos/effect/packages/cluster/src/ClusterCron.ts @@ -0,0 +1,139 @@ +/** + * @since 1.0.0 + */ +import * as Rpc from "@effect/rpc/Rpc" +import * as Cron from "effect/Cron" +import * as DateTime from "effect/DateTime" +import * as Duration from "effect/Duration" +import * as Effect from "effect/Effect" +import * as Exit from "effect/Exit" +import * as Layer from "effect/Layer" +import * as Option from "effect/Option" +import * as PrimaryKey from "effect/PrimaryKey" +import * as Schedule from "effect/Schedule" +import * as Schema from "effect/Schema" +import type { Scope } from "effect/Scope" +import * as ClusterSchema from "./ClusterSchema.js" +import { Persisted, Uninterruptible } from "./ClusterSchema.js" +import * as DeliverAt from "./DeliverAt.js" +import * as Entity from "./Entity.js" +import type { Sharding } from "./Sharding.js" +import * as Singleton from "./Singleton.js" + +/** + * @since 1.0.0 + * @category Constructors + */ +export const make = (options: { + readonly name: string + readonly cron: Cron.Cron + readonly execute: Effect.Effect + + /** + * Choose a shard group to run this cron job on. + */ + readonly shardGroup?: string | undefined + + /** + * Whether to run the next cron job based from the time of the previous run. + * + * Defaults to `false`, meaning the next run will be calculated from the + * current time. + */ + readonly calculateNextRunFromPrevious?: boolean | undefined + + /** + * If set, the cron job will skip execution if the scheduled time is older + * than this duration. + * + * This is useful to prevent running jobs that were scheduled too far in the + * past. + * + * Defaults to "1 day". + */ + readonly skipIfOlderThan?: Duration.DurationInput | undefined +}): Layer.Layer> => { + const CronEntity = Entity.make(`ClusterCron/${options.name}`, [ + Rpc.make("run", { + payload: CronPayload + }) + .annotate(Persisted, true) + .annotate(Uninterruptible, true) + ]) + .annotate(ClusterSchema.ShardGroup, () => options.shardGroup ?? "default") + .annotate(ClusterSchema.ClientTracingEnabled, false) + + const InitialRun = Singleton.make( + `ClusterCron/${options.name}`, + Effect.gen(function*() { + const now = yield* DateTime.now + const next = DateTime.unsafeFromDate(Cron.next(options.cron, now)) + const entityId = options.calculateNextRunFromPrevious ? "initial" : DateTime.formatIso(next) + const client = (yield* CronEntity.client)(entityId) + yield* client.run({ dateTime: next }, { discard: true }) + }), + { shardGroup: options.shardGroup } + ) + + const skipIfOlderThan = Option.fromNullable(options.skipIfOlderThan).pipe( + Option.map(Duration.decode), + Option.getOrElse(() => Duration.days(1)) + ) + + const effect = Effect.fnUntraced(function*(dateTime: DateTime.Utc) { + const now = yield* DateTime.now + if (DateTime.lessThan(dateTime, DateTime.subtractDuration(now, skipIfOlderThan))) { + return + } + return yield* options.execute + }, Effect.orDie) + + const EntityLayer = CronEntity.toLayer(Effect.gen(function*() { + const makeClient = yield* CronEntity.client + return { + run: (request) => + effect(request.payload.dateTime).pipe( + Effect.exit, + Effect.flatMap(Effect.fnUntraced(function*(exit) { + if (Exit.isFailure(exit)) { + yield* Effect.logWarning(exit.cause) + } + const now = yield* DateTime.now + const next = DateTime.unsafeFromDate(Cron.next( + options.cron, + options.calculateNextRunFromPrevious ? request.payload.dateTime : now + )) + const client = makeClient(DateTime.formatIso(next)) + return yield* client.run({ dateTime: next }, { discard: true }).pipe( + Effect.tapErrorCause((cause) => Effect.logWarning("Failed to schedule next run, retrying", cause)), + Effect.sandbox, + Effect.retry(retryPolicy), + Effect.orDie + ) + })), + Effect.annotateLogs({ + module: "ClusterCron", + name: options.name, + dateTime: request.payload.dateTime + }) + ) + } + })) + + return Layer.merge(InitialRun, EntityLayer) +} + +const retryPolicy = Schedule.exponential(200, 1.5).pipe( + Schedule.union(Schedule.spaced("1 minute")) +) + +class CronPayload extends Schema.Class("@effect/cluster/ClusterCron/CronPayload")({ + dateTime: Schema.DateTimeUtc +}) { + [PrimaryKey.symbol]() { + return "" + } + [DeliverAt.symbol]() { + return this.dateTime + } +} diff --git a/repos/effect/packages/cluster/src/ClusterError.ts b/repos/effect/packages/cluster/src/ClusterError.ts new file mode 100644 index 0000000..423e91a --- /dev/null +++ b/repos/effect/packages/cluster/src/ClusterError.ts @@ -0,0 +1,193 @@ +/** + * @since 1.0.0 + */ +import * as Cause from "effect/Cause" +import * as Effect from "effect/Effect" +import { hasProperty, isTagged } from "effect/Predicate" +import * as Schema from "effect/Schema" +import { EntityAddress } from "./EntityAddress.js" +import { RunnerAddress } from "./RunnerAddress.js" +import { SnowflakeFromString } from "./Snowflake.js" + +/** + * @since 1.0.0 + * @category Symbols + */ +export const TypeId: unique symbol = Symbol.for("@effect/cluster/ClusterError") + +/** + * @since 1.0.0 + * @category Symbols + */ +export type TypeId = typeof TypeId + +/** + * Represents an error that occurs when a Runner receives a message for an entity + * that it is not assigned to it. + * + * @since 1.0.0 + * @category errors + */ +export class EntityNotAssignedToRunner extends Schema.TaggedError()( + "EntityNotAssignedToRunner", + { address: EntityAddress } +) { + /** + * @since 1.0.0 + */ + readonly [TypeId] = TypeId + + /** + * @since 1.0.0 + */ + static is(u: unknown): u is EntityNotAssignedToRunner { + return hasProperty(u, TypeId) && isTagged(u, "EntityNotAssignedToRunner") + } +} + +/** + * Represents an error that occurs when a message fails to be properly + * deserialized by an entity. + * + * @since 1.0.0 + * @category errors + */ +export class MalformedMessage extends Schema.TaggedError()( + "MalformedMessage", + { cause: Schema.Defect } +) { + /** + * @since 1.0.0 + */ + readonly [TypeId] = TypeId + + /** + * @since 1.0.0 + */ + static is(u: unknown): u is MalformedMessage { + return hasProperty(u, TypeId) && isTagged(u, "MalformedMessage") + } + + /** + * @since 1.0.0 + */ + static refail: (effect: Effect.Effect) => Effect.Effect< + A, + MalformedMessage, + R + > = Effect.mapError((cause) => new MalformedMessage({ cause })) +} + +/** + * Represents an error that occurs when a message fails to be persisted into + * cluster's mailbox storage. + * + * @since 1.0.0 + * @category errors + */ +export class PersistenceError extends Schema.TaggedError()( + "PersistenceError", + { cause: Schema.Defect } +) { + /** + * @since 1.0.0 + */ + readonly [TypeId] = TypeId + + /** + * @since 1.0.0 + */ + static refail(effect: Effect.Effect): Effect.Effect { + return Effect.catchAllCause(effect, (cause) => Effect.fail(new PersistenceError({ cause: Cause.squash(cause) }))) + } +} + +/** + * Represents an error that occurs when a Runner is not registered with the shard + * manager. + * + * @since 1.0.0 + * @category errors + */ +export class RunnerNotRegistered extends Schema.TaggedError()( + "RunnerNotRegistered", + { address: RunnerAddress } +) { + /** + * @since 1.0.0 + */ + readonly [TypeId] = TypeId +} + +/** + * Represents an error that occurs when a Runner is unresponsive. + * + * @since 1.0.0 + * @category errors + */ +export class RunnerUnavailable extends Schema.TaggedError()( + "RunnerUnavailable", + { address: RunnerAddress } +) { + /** + * @since 1.0.0 + */ + readonly [TypeId] = TypeId + + /** + * @since 1.0.0 + */ + static is(u: unknown): u is RunnerUnavailable { + return hasProperty(u, TypeId) && isTagged(u, "RunnerUnavailable") + } +} + +/** + * Represents an error that occurs when the entities mailbox is full. + * + * @since 1.0.0 + * @category errors + */ +export class MailboxFull extends Schema.TaggedError()( + "MailboxFull", + { address: EntityAddress } +) { + /** + * @since 1.0.0 + */ + readonly [TypeId] = TypeId + + /** + * @since 1.0.0 + */ + static is(u: unknown): u is MailboxFull { + return hasProperty(u, TypeId) && isTagged(u, "MailboxFull") + } +} + +/** + * Represents an error that occurs when the entity is already processing a + * request. + * + * @since 1.0.0 + * @category errors + */ +export class AlreadyProcessingMessage extends Schema.TaggedError()( + "AlreadyProcessingMessage", + { + envelopeId: SnowflakeFromString, + address: EntityAddress + } +) { + /** + * @since 1.0.0 + */ + readonly [TypeId] = TypeId + + /** + * @since 1.0.0 + */ + static is(u: unknown): u is AlreadyProcessingMessage { + return hasProperty(u, TypeId) && isTagged(u, "AlreadyProcessingMessage") + } +} diff --git a/repos/effect/packages/cluster/src/ClusterMetrics.ts b/repos/effect/packages/cluster/src/ClusterMetrics.ts new file mode 100644 index 0000000..e8ee5c7 --- /dev/null +++ b/repos/effect/packages/cluster/src/ClusterMetrics.ts @@ -0,0 +1,44 @@ +/** + * @since 1.0.0 + */ +import * as Metric from "effect/Metric" + +/** + * @since 1.0.0 + * @category metrics + */ +export const entities = Metric.gauge("effect_cluster_entities", { + bigint: true +}) + +/** + * @since 1.0.0 + * @category metrics + */ +export const singletons = Metric.gauge("effect_cluster_singletons", { + bigint: true +}) + +/** + * @since 1.0.0 + * @category metrics + */ +export const runners = Metric.gauge("effect_cluster_runners", { + bigint: true +}) + +/** + * @since 1.0.0 + * @category metrics + */ +export const runnersHealthy = Metric.gauge("effect_cluster_runners_healthy", { + bigint: true +}) + +/** + * @since 1.0.0 + * @category metrics + */ +export const shards = Metric.gauge("effect_cluster_shards", { + bigint: true +}) diff --git a/repos/effect/packages/cluster/src/ClusterSchema.ts b/repos/effect/packages/cluster/src/ClusterSchema.ts new file mode 100644 index 0000000..08fa78c --- /dev/null +++ b/repos/effect/packages/cluster/src/ClusterSchema.ts @@ -0,0 +1,57 @@ +/** + * @since 1.0.0 + */ +import * as Context from "effect/Context" +import { constFalse, constTrue } from "effect/Function" +import type { EntityId } from "./EntityId.js" + +/** + * @since 1.0.0 + * @category Annotations + */ +export class Persisted extends Context.Reference()("@effect/cluster/ClusterSchema/Persisted", { + defaultValue: constFalse +}) {} + +/** + * @since 1.0.0 + * @category Annotations + */ +export class Uninterruptible + extends Context.Reference()("@effect/cluster/ClusterSchema/Uninterruptible", { + defaultValue: (): boolean | "client" | "server" => false + }) +{ + /** + * @since 1.0.0 + */ + static forServer(context: Context.Context): boolean { + const value = Context.get(context, Uninterruptible) + return value === true || value === "server" + } + /** + * @since 1.0.0 + */ + static forClient(context: Context.Context): boolean { + const value = Context.get(context, Uninterruptible) + return value === true || value === "client" + } +} + +/** + * @since 1.0.0 + * @category Annotations + */ +export class ShardGroup extends Context.Reference()("@effect/cluster/ClusterSchema/ShardGroup", { + defaultValue: (): (entityId: EntityId) => string => (_) => "default" +}) {} + +/** + * @since 1.0.0 + * @category Annotations + */ +export class ClientTracingEnabled + extends Context.Reference()("@effect/cluster/ClusterSchema/ClientTracingEnabled", { + defaultValue: constTrue + }) +{} diff --git a/repos/effect/packages/cluster/src/ClusterWorkflowEngine.ts b/repos/effect/packages/cluster/src/ClusterWorkflowEngine.ts new file mode 100644 index 0000000..afdb1f5 --- /dev/null +++ b/repos/effect/packages/cluster/src/ClusterWorkflowEngine.ts @@ -0,0 +1,659 @@ +/** + * @since 1.0.0 + */ +import * as Rpc from "@effect/rpc/Rpc" +import * as RpcServer from "@effect/rpc/RpcServer" +import { DurableDeferred } from "@effect/workflow" +import * as Activity from "@effect/workflow/Activity" +import * as DurableClock from "@effect/workflow/DurableClock" +import * as Workflow from "@effect/workflow/Workflow" +import { makeUnsafe, WorkflowEngine, WorkflowInstance } from "@effect/workflow/WorkflowEngine" +import * as Arr from "effect/Array" +import * as Cause from "effect/Cause" +import * as Context from "effect/Context" +import * as DateTime from "effect/DateTime" +import * as Duration from "effect/Duration" +import * as Effect from "effect/Effect" +import * as Exit from "effect/Exit" +import * as Fiber from "effect/Fiber" +import * as FiberId from "effect/FiberId" +import * as Layer from "effect/Layer" +import * as Option from "effect/Option" +import type * as ParseResult from "effect/ParseResult" +import * as PrimaryKey from "effect/PrimaryKey" +import * as RcMap from "effect/RcMap" +import type * as Record from "effect/Record" +import * as Runtime from "effect/Runtime" +import * as Schedule from "effect/Schedule" +import * as Schema from "effect/Schema" +import type * as Scope from "effect/Scope" +import * as ClusterSchema from "./ClusterSchema.js" +import * as DeliverAt from "./DeliverAt.js" +import * as Entity from "./Entity.js" +import { EntityAddress } from "./EntityAddress.js" +import { EntityId } from "./EntityId.js" +import { EntityType } from "./EntityType.js" +import { MessageStorage } from "./MessageStorage.js" +import type { WithExitEncoded } from "./Reply.js" +import * as Reply from "./Reply.js" +import * as Sharding from "./Sharding.js" +import * as Snowflake from "./Snowflake.js" + +/** + * @since 1.0.0 + * @category Constructors + */ +export const make = Effect.gen(function*() { + const sharding = yield* Sharding.Sharding + const storage = yield* MessageStorage + + const workflows = new Map() + const entities = new Map< + string, + Entity.Entity< + string, + | Rpc.Rpc< + "run", + Schema.Struct< + Record< + typeof payloadParentKey, + Schema.optional< + Schema.Struct<{ + workflowName: typeof Schema.String + executionId: typeof Schema.String + }> + > + > + >, + Schema.Schema> + > + | Rpc.Rpc<"deferred", Schema.Struct<{ name: typeof Schema.String; exit: typeof ExitUnknown }>, typeof ExitUnknown> + | Rpc.Rpc< + "activity", + Schema.Struct<{ name: typeof Schema.String; attempt: typeof Schema.Number }>, + Schema.Schema> + > + | Rpc.Rpc<"resume", Schema.Struct<{}>> + > + >() + const partialEntities = new Map< + string, + Entity.Entity< + string, + | Rpc.Rpc<"deferred", Schema.Struct<{ name: typeof Schema.String; exit: typeof ExitUnknown }>, typeof ExitUnknown> + | Rpc.Rpc< + "activity", + Schema.Struct<{ name: typeof Schema.String; attempt: typeof Schema.Number }>, + Schema.Schema> + > + | Rpc.Rpc<"resume"> + > + >() + const ensureEntity = (workflow: Workflow.Any) => { + let entity = entities.get(workflow.name) + if (!entity) { + entity = makeWorkflowEntity(workflow) as any + workflows.set(workflow.name, workflow) + entities.set(workflow.name, entity as any) + } + return entity! + } + const ensurePartialEntity = (workflowName: string) => { + let entity = partialEntities.get(workflowName) + if (!entity) { + entity = makePartialWorkflowEntity(workflowName) as any + partialEntities.set(workflowName, entity as any) + } + return entity! + } + + const activities = new Map + }>() + const interruptedActivities = new Set() + const activityLatches = new Map() + const clients = yield* RcMap.make({ + lookup: Effect.fnUntraced(function*(workflowName: string) { + const entity = entities.get(workflowName) + if (!entity) { + return yield* Effect.dieMessage(`Workflow ${workflowName} not registered`) + } + return yield* entity.client + }), + idleTimeToLive: "5 minutes" + }) + const clientsPartial = yield* RcMap.make({ + lookup: Effect.fnUntraced(function*(workflowName: string) { + const entity = entities.get(workflowName) ?? ensurePartialEntity(workflowName) + return yield* entity.client + }), + idleTimeToLive: "5 minutes" + }) + const clockClient = yield* ClockEntity.client + + const requestIdFor = Effect.fnUntraced(function*(options: { + readonly workflow: Workflow.Any + readonly entityType: string + readonly executionId: string + readonly tag: string + readonly id: string + }) { + const shardGroup = Context.get(options.workflow.annotations, ClusterSchema.ShardGroup)( + options.executionId as EntityId + ) + const entityId = EntityId.make(options.executionId) + const address = new EntityAddress({ + entityType: EntityType.make(options.entityType), + entityId, + shardId: sharding.getShardId(entityId, shardGroup) + }) + return yield* storage.requestIdForPrimaryKey({ address, tag: options.tag, id: options.id }) + }) + + const replyForRequestId = Effect.fnUntraced(function*(requestId: Snowflake.Snowflake) { + const replies = yield* storage.repliesForUnfiltered([requestId]) + return Arr.last(replies).pipe( + Option.filter((reply) => reply._tag === "WithExit"), + Option.map((reply) => + reply as WithExitEncoded, Schema.Schema>>> + ) + ) + }) + + const requestReply = Effect.fnUntraced(function*(options: { + readonly workflow: Workflow.Any + readonly entityType: string + readonly executionId: string + readonly tag: string + readonly id: string + }) { + const requestId = yield* requestIdFor(options) + if (Option.isNone(requestId)) { + return Option.none() + } + return yield* replyForRequestId(requestId.value) + }) + + const resetActivityAttempt = Effect.fnUntraced( + function*(options: { + readonly workflow: Workflow.Any + readonly executionId: string + readonly activity: Activity.Any + readonly attempt: number + }) { + const requestId = yield* requestIdFor({ + workflow: options.workflow, + entityType: `Workflow/${options.workflow.name}`, + executionId: options.executionId, + tag: "activity", + id: activityPrimaryKey(options.activity.name, options.attempt) + }) + if (Option.isNone(requestId)) return + yield* sharding.reset(requestId.value) + }, + Effect.retry({ + times: 3, + schedule: Schedule.exponential(250) + }), + Effect.orDie + ) + + const clearClock = Effect.fnUntraced(function*(options: { + readonly workflow: Workflow.Any + readonly executionId: string + }) { + const shardGroup = Context.get(options.workflow.annotations, ClusterSchema.ShardGroup)( + options.executionId as EntityId + ) + const entityId = EntityId.make(options.executionId) + const shardId = sharding.getShardId(entityId, shardGroup) + const clockAddress = new EntityAddress({ + entityType: ClockEntity.type, + entityId, + shardId + }) + yield* storage.clearAddress(clockAddress) + }) + + const resume = Effect.fnUntraced(function*(workflow: Workflow.Any, executionId: string) { + const maybeReply = yield* requestReply({ + workflow, + entityType: `Workflow/${workflow.name}`, + executionId, + tag: "run", + id: "" + }) + const maybeSuspended = Option.filter( + maybeReply, + (reply) => reply.exit._tag === "Success" && reply.exit.value._tag === "Suspended" + ) + if (Option.isNone(maybeSuspended)) return + yield* sharding.reset(Snowflake.Snowflake(maybeSuspended.value.requestId)) + yield* sharding.pollStorage + }) + + const sendResumeParent = Effect.fnUntraced(function*(options: { + readonly workflowName: string + readonly executionId: string + }) { + const requestId = yield* requestIdFor({ + workflow: workflows.get(options.workflowName)!, + entityType: `Workflow/${options.workflowName}`, + executionId: options.executionId, + tag: "resume", + id: "" + }) + if (Option.isNone(requestId)) { + const client = (yield* RcMap.get(clientsPartial, options.workflowName))(options.executionId) + return yield* client.resume({} as any, { discard: true }) + } + const reply = yield* replyForRequestId(requestId.value) + if (Option.isNone(reply)) return + yield* sharding.reset(requestId.value) + }, Effect.scoped) + + const engine = makeUnsafe({ + register: (workflow, execute) => + Effect.suspend(() => + sharding.registerEntity( + ensureEntity(workflow), + Effect.gen(function*() { + const address = yield* Entity.CurrentAddress + const executionId = address.entityId + return { + run: (request: Entity.Request) => { + const instance = WorkflowInstance.initial(workflow, executionId) + const payload = request.payload + let parent: { workflowName: string; executionId: string } | undefined + if (payload[payloadParentKey]) { + parent = payload[payloadParentKey] + } + return execute(workflow.payloadSchema.make(payload), executionId).pipe( + Effect.onExit((exit) => { + const suspendOnFailure = Context.get(workflow.annotations, Workflow.SuspendOnFailure) + if (!instance.suspended && !(suspendOnFailure && exit._tag === "Failure")) { + return parent ? ensureSuccess(sendResumeParent(parent)) : Effect.void + } + return engine.deferredResult(InterruptSignal).pipe( + Effect.flatMap((maybeExit) => { + if (maybeExit === undefined) { + return Effect.void + } + instance.suspended = false + instance.interrupted = true + return Effect.zipRight( + Effect.ignore(clearClock({ workflow, executionId })), + Effect.withFiberRuntime((fiber) => Effect.interruptible(Fiber.interrupt(fiber))) + ) + }), + Effect.orDie + ) + }), + Workflow.intoResult, + Effect.provideService(WorkflowInstance, instance) + ) as any + }, + + activity(request: Entity.Request) { + const activityId = `${executionId}/${request.payload.name}` + const instance = WorkflowInstance.initial(workflow, executionId) + interruptedActivities.delete(activityId) + return Effect.gen(function*() { + let entry = activities.get(activityId) + while (!entry) { + const latch = Effect.unsafeMakeLatch() + activityLatches.set(activityId, latch) + yield* latch.await + entry = activities.get(activityId) + } + const contextMap = new Map(entry.runtime.context.unsafeMap) + contextMap.set(Activity.CurrentAttempt.key, request.payload.attempt) + contextMap.set(WorkflowInstance.key, instance) + const runtime = Runtime.make({ + context: Context.unsafeMake(contextMap), + fiberRefs: entry.runtime.fiberRefs, + runtimeFlags: Runtime.defaultRuntimeFlags + }) + return yield* entry.activity.executeEncoded.pipe( + Effect.provide(runtime) + ) + }).pipe( + Workflow.intoResult, + Effect.catchAllCause((cause) => { + const interruptors = Cause.interruptors(cause) + // we only want to store interrupts as suspends when the + // client requested it + const ids = Array.from(interruptors, (id) => Array.from(FiberId.ids(id))).flat() + const suspend = ids.includes(RpcServer.fiberIdClientInterrupt.id) + if (suspend) { + interruptedActivities.add(activityId) + return Effect.succeed(new Workflow.Suspended()) + } + return Effect.failCause(cause) + }), + Effect.provideService(WorkflowInstance, instance), + Effect.provideService(Activity.CurrentAttempt, request.payload.attempt), + Effect.ensuring(Effect.sync(() => { + activities.delete(activityId) + })), + Rpc.wrap({ + fork: true, + uninterruptible: true + }) + ) + }, + + deferred: Effect.fnUntraced(function*(request: Entity.Request) { + yield* ensureSuccess(resume(workflow, executionId)) + return request.payload.exit + }), + + resume: () => ensureSuccess(resume(workflow, executionId)) + } + }) + ) as Effect.Effect + ), + + execute: (workflow, { discard, executionId, parent, payload }) => { + ensureEntity(workflow) + return RcMap.get(clients, workflow.name).pipe( + Effect.flatMap((make) => + make(executionId).run( + parent ? + { + ...payload, + [payloadParentKey]: { workflowName: parent.workflow.name, executionId: parent.executionId } + } : + payload, + { discard } + ) + ), + Effect.orDie, + Effect.scoped + ) + }, + + poll: Effect.fnUntraced(function*(workflow, executionId) { + const entity = ensureEntity(workflow) + const exitSchema = Rpc.exitSchema(entity.protocol.requests.get("run")!) + const oreply = yield* requestReply({ + workflow, + entityType: `Workflow/${workflow.name}`, + executionId, + tag: "run", + id: "" + }) + if (Option.isNone(oreply)) return undefined + const exit = yield* (Schema.decode(exitSchema)(oreply.value.exit) as Effect.Effect< + Exit.Exit, + ParseResult.ParseError + >) + return yield* exit + }, Effect.orDie), + + interrupt: Effect.fnUntraced( + function*(workflow, executionId) { + ensureEntity(workflow) + const oreply = yield* requestReply({ + workflow, + entityType: `Workflow/${workflow.name}`, + executionId, + tag: "run", + id: "" + }) + const nonSuspendedReply = oreply.pipe( + Option.filter((reply) => reply.exit._tag !== "Success" || reply.exit.value._tag !== "Suspended") + ) + if (Option.isSome(nonSuspendedReply)) { + return + } + + yield* engine.deferredDone(InterruptSignal, { + workflowName: workflow.name, + executionId, + deferredName: InterruptSignal.name, + exit: Exit.void + }) + }, + Effect.retry({ + while: (e) => e._tag === "PersistenceError", + times: 3, + schedule: Schedule.exponential(250) + }), + Effect.orDie + ), + + resume: (workflow, executionId) => ensureSuccess(resume(workflow, executionId)), + + activityExecute: Effect.fnUntraced( + function*(activity, attempt) { + const runtime = yield* Effect.runtime() + const context = runtime.context + const instance = Context.get(context, WorkflowInstance) + yield* Effect.annotateCurrentSpan("executionId", instance.executionId) + const activityId = `${instance.executionId}/${activity.name}` + const client = (yield* RcMap.get(clientsPartial, instance.workflow.name))(instance.executionId) + while (true) { + if (!activities.has(activityId)) { + activities.set(activityId, { activity, runtime }) + const latch = activityLatches.get(activityId) + if (latch) { + yield* latch.release + activityLatches.delete(activityId) + } + } + const result = yield* Effect.orDie(client.activity({ name: activity.name, attempt })) + // If the activity has suspended and did not execute, we need to resume + // it by resetting the attempt and re-executing. + if (result._tag === "Suspended" && (activities.has(activityId) || interruptedActivities.has(activityId))) { + yield* resetActivityAttempt({ + workflow: instance.workflow, + executionId: instance.executionId, + activity, + attempt + }) + continue + } + activities.delete(activityId) + return result + } + }, + Effect.scoped + ), + + deferredResult: (deferred) => + WorkflowInstance.pipe( + Effect.flatMap((instance) => + requestReply({ + workflow: instance.workflow, + entityType: `Workflow/${instance.workflow.name}`, + executionId: instance.executionId, + tag: "deferred", + id: deferred.name + }) + ), + Effect.map((oreply) => { + if (Option.isNone(oreply)) { + return undefined + } + const reply = oreply.value + const decoded = decodeDeferredWithExit(reply as any) + return decoded.exit._tag === "Success" + ? decoded.exit.value + : decoded.exit + }), + Effect.retry({ + while: (e) => e._tag === "PersistenceError", + times: 3, + schedule: Schedule.exponential(250) + }), + Effect.orDie + ), + + deferredDone: Effect.fnUntraced( + function*({ deferredName, executionId, exit, workflowName }) { + const client = yield* RcMap.get(clientsPartial, workflowName) + return yield* Effect.orDie( + client(executionId).deferred({ + name: deferredName, + exit + }, { discard: true }) + ) + }, + Effect.scoped + ), + + scheduleClock(workflow, options) { + const client = clockClient(options.executionId) + return DateTime.now.pipe( + Effect.flatMap((now) => + client.run({ + name: options.clock.name, + workflowName: workflow.name, + wakeUp: DateTime.addDuration(now, options.clock.duration) + }, { discard: true }) + ), + Effect.orDie + ) + } + }) + + return engine +}) + +const retryPolicy = Schedule.exponential(200, 1.5).pipe( + Schedule.union(Schedule.spaced("1 minute")) +) + +const ensureSuccess = (effect: Effect.Effect) => + effect.pipe( + Effect.sandbox, + Effect.retry(retryPolicy), + Effect.orDie + ) + +const ActivityRpc = Rpc.make("activity", { + payload: { + name: Schema.String, + attempt: Schema.Number + }, + primaryKey: ({ attempt, name }) => activityPrimaryKey(name, attempt), + success: Workflow.Result({ + success: Schema.Unknown, + error: Schema.Unknown + }) +}) + .annotate(ClusterSchema.Persisted, true) + .annotate(ClusterSchema.Uninterruptible, "server") + +const payloadParentKey = "~@effect/workflow/parent" as const + +const makeWorkflowEntity = (workflow: Workflow.Any) => + Entity.make(`Workflow/${workflow.name}`, [ + Rpc.make("run", { + payload: { + ...workflow.payloadSchema.fields, + [payloadParentKey]: Schema.optional(Schema.Struct({ + workflowName: Schema.String, + executionId: Schema.String + })) + }, + primaryKey: () => "", + success: Workflow.Result({ + success: workflow.successSchema, + error: workflow.errorSchema + }) + }) + .annotate(ClusterSchema.Persisted, true) + .annotate(ClusterSchema.Uninterruptible, true), + + DeferredRpc, + ResumeRpc, + ActivityRpc + ]).annotateContext(workflow.annotations) + +const ExitUnknown = Schema.Exit({ + success: Schema.Unknown, + failure: Schema.Unknown, + defect: Schema.Defect +}) + +const DeferredRpc = Rpc.make("deferred", { + payload: { + name: Schema.String, + exit: ExitUnknown + }, + primaryKey: ({ name }) => name, + success: ExitUnknown +}) + .annotate(ClusterSchema.Persisted, true) + .annotate(ClusterSchema.Uninterruptible, true) + +const decodeDeferredWithExit = Schema.decodeSync(Reply.WithExit.schema(DeferredRpc)) + +const ResumeRpc = Rpc.make("resume", { + payload: {}, + primaryKey: () => "" +}) + .annotate(ClusterSchema.Persisted, true) + .annotate(ClusterSchema.Uninterruptible, true) + +const makePartialWorkflowEntity = (workflowName: string) => + Entity.make(`Workflow/${workflowName}`, [ + DeferredRpc, + ResumeRpc, + ActivityRpc + ]) + +const activityPrimaryKey = (activity: string, attempt: number) => `${activity}/${attempt}` + +class ClockPayload extends Schema.Class(`Workflow/DurableClock/Run`)({ + name: Schema.String, + workflowName: Schema.String, + wakeUp: Schema.DateTimeUtcFromNumber +}) { + [PrimaryKey.symbol]() { + return this.name + } + [DeliverAt.symbol]() { + return this.wakeUp + } +} + +const ClockEntity = Entity.make("Workflow/-/DurableClock", [ + Rpc.make("run", { payload: ClockPayload }) + .annotate(ClusterSchema.Persisted, true) + .annotate(ClusterSchema.Uninterruptible, true) +]) + +const ClockEntityLayer = ClockEntity.toLayer(Effect.gen(function*() { + const engine = yield* WorkflowEngine + const address = yield* Entity.CurrentAddress + const executionId = address.entityId + return { + run(request) { + const deferred = DurableClock.make({ name: request.payload.name, duration: Duration.zero }).deferred + return ensureSuccess(engine.deferredDone(deferred, { + workflowName: request.payload.workflowName, + executionId, + deferredName: deferred.name, + exit: Exit.void + })) + } + } +})) + +const InterruptSignal = DurableDeferred.make("Workflow/InterruptSignal") + +/** + * @since 1.0.0 + * @category Layers + */ +export const layer: Layer.Layer< + WorkflowEngine, + never, + Sharding.Sharding | MessageStorage +> = ClockEntityLayer.pipe( + Layer.provideMerge(Layer.scoped(WorkflowEngine, make)) +) diff --git a/repos/effect/packages/cluster/src/DeliverAt.ts b/repos/effect/packages/cluster/src/DeliverAt.ts new file mode 100644 index 0000000..c0e5128 --- /dev/null +++ b/repos/effect/packages/cluster/src/DeliverAt.ts @@ -0,0 +1,36 @@ +/** + * @since 1.0.0 + */ +import type { DateTime } from "effect/DateTime" +import { hasProperty } from "effect/Predicate" + +/** + * @since 1.0.0 + * @category symbols + */ +export const symbol: unique symbol = Symbol.for("@effect/cluster/DeliverAt") + +/** + * @since 1.0.0 + * @category models + */ +export interface DeliverAt { + [symbol](): DateTime +} + +/** + * @since 1.0.0 + * @category guards + */ +export const isDeliverAt = (self: unknown): self is DeliverAt => hasProperty(self, symbol) + +/** + * @since 1.0.0 + * @category accessors + */ +export const toMillis = (self: unknown): number | null => { + if (isDeliverAt(self)) { + return self[symbol]().epochMillis + } + return null +} diff --git a/repos/effect/packages/cluster/src/Entity.ts b/repos/effect/packages/cluster/src/Entity.ts new file mode 100644 index 0000000..792e206 --- /dev/null +++ b/repos/effect/packages/cluster/src/Entity.ts @@ -0,0 +1,653 @@ +/** + * @since 1.0.0 + */ +import * as Headers from "@effect/platform/Headers" +import * as Rpc from "@effect/rpc/Rpc" +import * as RpcClient from "@effect/rpc/RpcClient" +import * as RpcGroup from "@effect/rpc/RpcGroup" +import * as RpcServer from "@effect/rpc/RpcServer" +import * as Arr from "effect/Array" +import type { Brand } from "effect/Brand" +import type * as Cause from "effect/Cause" +import * as Context from "effect/Context" +import * as Data from "effect/Data" +import type { DurationInput } from "effect/Duration" +import * as Effect from "effect/Effect" +import * as Equal from "effect/Equal" +import * as Exit from "effect/Exit" +import { identity } from "effect/Function" +import * as Hash from "effect/Hash" +import * as Layer from "effect/Layer" +import * as Mailbox from "effect/Mailbox" +import * as Option from "effect/Option" +import * as Predicate from "effect/Predicate" +import type * as Schedule from "effect/Schedule" +import { Scope } from "effect/Scope" +import type * as Stream from "effect/Stream" +import type { AlreadyProcessingMessage, MailboxFull, PersistenceError } from "./ClusterError.js" +import { ShardGroup } from "./ClusterSchema.js" +import * as ClusterSchema from "./ClusterSchema.js" +import { EntityAddress } from "./EntityAddress.js" +import type { EntityId } from "./EntityId.js" +import { EntityType } from "./EntityType.js" +import * as Envelope from "./Envelope.js" +import { hashString } from "./internal/hash.js" +import { ResourceMap } from "./internal/resourceMap.js" +import * as Message from "./Message.js" +import type * as Reply from "./Reply.js" +import { RunnerAddress } from "./RunnerAddress.js" +import * as ShardId from "./ShardId.js" +import type { Sharding } from "./Sharding.js" +import { ShardingConfig } from "./ShardingConfig.js" +import * as Snowflake from "./Snowflake.js" + +/** + * @since 1.0.0 + * @category type ids + */ +export const TypeId: unique symbol = Symbol.for("@effect/cluster/Entity") + +/** + * @since 1.0.0 + * @category type ids + */ +export type TypeId = typeof TypeId + +/** + * @since 1.0.0 + * @category models + */ +export interface Entity< + in out Type extends string, + in out Rpcs extends Rpc.Any +> extends Equal.Equal { + readonly [TypeId]: TypeId + /** + * The name of the entity type. + */ + readonly type: Type & Brand<"EntityType"> + + /** + * A RpcGroup definition for messages which represents the messaging protocol + * that the entity is capable of processing. + */ + readonly protocol: RpcGroup.RpcGroup + + /** + * Get the shard group for the given EntityId. + */ + getShardGroup(entityId: EntityId): string + + /** + * Get the ShardId for the given EntityId. + */ + getShardId(entityId: EntityId): Effect.Effect + + /** + * Annotate the entity with a value. + */ + annotate(tag: Context.Tag, value: S): Entity + + /** + * Annotate the Rpc's above this point with a value. + */ + annotateRpcs(tag: Context.Tag, value: S): Entity + + /** + * Annotate the entity with a context object. + */ + annotateContext(context: Context.Context): Entity + + /** + * Annotate the Rpc's above this point with a context object. + */ + annotateRpcsContext(context: Context.Context): Entity + + /** + * Create a client for this entity. + */ + readonly client: Effect.Effect< + ( + entityId: string + ) => RpcClient.RpcClient.From< + Rpcs, + MailboxFull | AlreadyProcessingMessage | PersistenceError + >, + never, + Sharding + > + + /** + * Create a Layer from an Entity. + * + * It will register the entity with the Sharding service. + */ + toLayer< + Handlers extends HandlersFrom, + RX = never + >( + build: Handlers | Effect.Effect, + options?: { + readonly maxIdleTime?: DurationInput | undefined + readonly concurrency?: number | "unbounded" | undefined + readonly mailboxCapacity?: number | "unbounded" | undefined + readonly disableFatalDefects?: boolean | undefined + readonly defectRetryPolicy?: Schedule.Schedule | undefined + readonly spanAttributes?: Record | undefined + } + ): Layer.Layer< + never, + never, + | Exclude + | RpcGroup.HandlersContext + | Rpc.Context + | Rpc.Middleware + | Sharding + > + + of>(handlers: Handlers): Handlers + + /** + * Create a Layer from an Entity. + * + * It will register the entity with the Sharding service. + */ + toLayerMailbox< + R, + RX = never + >( + build: + | (( + mailbox: Mailbox.ReadonlyMailbox>, + replier: Replier + ) => Effect.Effect) + | Effect.Effect< + ( + mailbox: Mailbox.ReadonlyMailbox>, + replier: Replier + ) => Effect.Effect, + never, + RX + >, + options?: { + readonly maxIdleTime?: DurationInput | undefined + readonly mailboxCapacity?: number | "unbounded" | undefined + readonly disableFatalDefects?: boolean | undefined + readonly defectRetryPolicy?: Schedule.Schedule | undefined + readonly spanAttributes?: Record | undefined + } + ): Layer.Layer< + never, + never, + | Exclude + | R + | Rpc.Context + | Rpc.Middleware + | Sharding + > +} +/** + * @since 1.0.0 + * @category models + */ +export type Any = Entity + +/** + * @since 1.0.0 + * @category models + */ +export type HandlersFrom = { + readonly [Current in Rpc as Current["_tag"]]: ( + envelope: Request + ) => Rpc.ResultFrom | Rpc.Wrapper> +} + +/** + * @since 1.0.0 + * @category refinements + */ +export const isEntity = (u: unknown): u is Any => Predicate.hasProperty(u, TypeId) + +const Proto = { + [TypeId]: TypeId, + [Hash.symbol](this: Entity): number { + return Hash.structure({ type: this.type }) + }, + [Equal.symbol](this: Entity, that: Equal.Equal): boolean { + return isEntity(that) && this.type === that.type + }, + annotate(this: Entity, tag: Context.Tag, value: S) { + return fromRpcGroup(this.type, this.protocol.annotate(tag, value)) + }, + annotateRpcs(this: Entity, tag: Context.Tag, value: S) { + return fromRpcGroup(this.type, this.protocol.annotateRpcs(tag, value)) + }, + annotateContext(this: Entity, context: Context.Context) { + return fromRpcGroup(this.type, this.protocol.annotateContext(context)) + }, + annotateRpcsContext(this: Entity, context: Context.Context) { + return fromRpcGroup(this.type, this.protocol.annotateRpcsContext(context)) + }, + getShardId(this: Entity, entityId: EntityId) { + return Effect.map(shardingTag, (sharding) => sharding.getShardId(entityId, this.getShardGroup(entityId))) + }, + get client() { + return shardingTag.pipe( + Effect.flatMap((sharding) => sharding.makeClient(this as any)) + ) + }, + toLayer< + Rpcs extends Rpc.Any, + Handlers extends HandlersFrom, + RX = never + >( + this: Entity, + build: Handlers | Effect.Effect, + options?: { + readonly maxIdleTime?: DurationInput | undefined + readonly concurrency?: number | "unbounded" | undefined + readonly mailboxCapacity?: number | "unbounded" | undefined + readonly disableFatalDefects?: boolean | undefined + readonly defectRetryPolicy?: Schedule.Schedule | undefined + readonly spanAttributes?: Record | undefined + } + ): Layer.Layer< + never, + never, + | Exclude + | RpcGroup.HandlersContext + | Rpc.Context + | Rpc.Middleware + | Sharding + > { + return shardingTag.pipe( + Effect.flatMap((sharding) => + sharding.registerEntity( + this, + Effect.isEffect(build) ? build : Effect.succeed(build), + options + ) + ), + Layer.scopedDiscard + ) + }, + of: identity, + toLayerMailbox< + Rpcs extends Rpc.Any, + R, + RX = never + >( + this: Entity, + build: + | (( + mailbox: Mailbox.ReadonlyMailbox>, + replier: Replier + ) => Effect.Effect) + | Effect.Effect< + ( + mailbox: Mailbox.ReadonlyMailbox>, + replier: Replier + ) => Effect.Effect, + never, + RX + >, + options?: { + readonly maxIdleTime?: DurationInput | undefined + readonly mailboxCapacity?: number | "unbounded" | undefined + readonly disableFatalDefects?: boolean | undefined + readonly defectRetryPolicy?: Schedule.Schedule | undefined + readonly spanAttributes?: Record | undefined + } + ) { + const buildHandlers = Effect.gen(this, function*() { + const behaviour = Effect.isEffect(build) ? yield* build : build + const mailbox = yield* Mailbox.make>() + + // create the rpc handlers for the entity + const handler = (envelope: any) => { + return Effect.async((resume) => { + mailbox.unsafeOffer(envelope) + resumes.set(envelope, resume) + }) + } + const handlers: Record = {} + for (const rpc of this.protocol.requests.keys()) { + handlers[rpc] = handler + } + + // make the Replier for the behaviour + const resumes = new Map, (exit: Exit.Exit) => void>() + const complete = (request: Envelope.Request, exit: Exit.Exit) => + Effect.sync(() => { + const resume = resumes.get(request) + if (resume) { + resumes.delete(request) + resume(exit) + } + }) + const replier: Replier = { + succeed: (request, value) => complete(request, Exit.succeed(value)), + fail: (request, error) => complete(request, Exit.fail(error)), + failCause: (request, cause) => complete(request, Exit.failCause(cause)), + complete + } + + // fork the behaviour into the layer scope + yield* behaviour(mailbox, replier).pipe( + Effect.catchAllCause((cause) => { + const exit = Exit.failCause(cause) + for (const resume of resumes.values()) { + resume(exit) + } + return Effect.void + }), + Effect.interruptible, + Effect.forkScoped + ) + + return handlers as any + }) + + return this.toLayer(buildHandlers, { + ...options, + concurrency: "unbounded" + }) + } +} + +/** + * Creates a new `Entity` of the specified `type` which will accept messages + * that adhere to the provided `RpcGroup`. + * + * @since 1.0.0 + * @category constructors + */ +export const fromRpcGroup = ( + /** + * The entity type name. + */ + type: Type, + /** + * The schema definition for messages that the entity is capable of + * processing. + */ + protocol: RpcGroup.RpcGroup +): Entity => { + const self = Object.create(Proto) + self.type = EntityType.make(type) + self.protocol = protocol + self.getShardGroup = Context.get(protocol.annotations, ShardGroup) + return self +} + +/** + * Creates a new `Entity` of the specified `type` which will accept messages + * that adhere to the provided schemas. + * + * @since 1.0.0 + * @category constructors + */ +export const make = >( + /** + * The entity type name. + */ + type: Type, + /** + * The schema definition for messages that the entity is capable of + * processing. + */ + protocol: Rpcs +): Entity => fromRpcGroup(type, RpcGroup.make(...protocol)) + +/** + * A Context.Tag to access the current entity address. + * + * @since 1.0.0 + * @category context + */ +export class CurrentAddress extends Context.Tag("@effect/cluster/Entity/EntityAddress")< + CurrentAddress, + EntityAddress +>() {} + +/** + * A Context.Tag to access the current Runner address. + * + * @since 1.0.0 + * @category context + */ +export class CurrentRunnerAddress extends Context.Tag("@effect/cluster/Entity/RunnerAddress")< + CurrentRunnerAddress, + RunnerAddress +>() {} + +/** + * @since 1.0.0 + * @category Replier + */ +export interface Replier { + readonly succeed: ( + request: Envelope.Request, + value: Replier.Success + ) => Effect.Effect + + readonly fail: ( + request: Envelope.Request, + error: Rpc.Error + ) => Effect.Effect + + readonly failCause: ( + request: Envelope.Request, + cause: Cause.Cause> + ) => Effect.Effect + + readonly complete: ( + request: Envelope.Request, + exit: Exit.Exit, Rpc.Error> + ) => Effect.Effect +} + +/** + * @since 1.0.0 + * @category Replier + */ +export declare namespace Replier { + /** + * @since 1.0.0 + * @category Replier + */ + export type Success = Rpc.Success extends Stream.Stream ? + Stream.Stream<_A, _E | Rpc.Error, _R> | Mailbox.ReadonlyMailbox<_A, _E | Rpc.Error> + : Rpc.Success +} + +/** + * @since 1.0.0 + * @category Request + */ +export class Request extends Data.Class< + Envelope.Request & { + readonly lastSentChunk: Option.Option> + } +> { + /** + * @since 1.0.0 + */ + get lastSentChunkValue(): Option.Option> { + return this.lastSentChunk.pipe(Option.map((chunk) => Arr.lastNonEmpty(chunk.values))) + } + + /** + * @since 1.0.0 + */ + get nextSequence(): number { + if (Option.isNone(this.lastSentChunk)) { + return 0 + } + return this.lastSentChunk.value.sequence + 1 + } +} + +const shardingTag = Context.GenericTag("@effect/cluster/Sharding") + +/** + * @since 1.0.0 + * @category Testing + */ +export const makeTestClient: ( + entity: Entity, + layer: Layer.Layer +) => Effect.Effect< + (entityId: string) => Effect.Effect>, + LE, + Scope | ShardingConfig | Exclude | Rpc.MiddlewareClient +> = Effect.fnUntraced(function*( + entity: Entity, + layer: Layer.Layer +) { + const config = yield* ShardingConfig + const makeShardId = (entityId: string) => + ShardId.make( + entity.getShardGroup(entityId as EntityId), + (Math.abs(hashString(entityId) % config.shardsPerGroup)) + 1 + ) + const snowflakeGen = yield* Snowflake.makeGenerator + const runnerAddress = new RunnerAddress({ host: "localhost", port: 3000 }) + const entityMap = new Map | Rpc.Middleware | LR> + readonly concurrency: number | "unbounded" + readonly build: Effect.Effect< + Context.Context>, + never, + Scope | CurrentAddress + > + }>() + const sharding = shardingTag.of({ + ...({} as Sharding["Type"]), + registerEntity: (entity, handlers, options) => + Effect.contextWith((context) => { + entityMap.set(entity.type, { + context: context as any, + concurrency: options?.concurrency ?? 1, + build: entity.protocol.toHandlersContext(handlers).pipe( + Effect.provide(context.pipe( + Context.add(CurrentRunnerAddress, runnerAddress), + Context.omit(Scope) + )) + ) as any + }) + }) + }) + yield* Layer.build(Layer.provide(layer, Layer.succeed(shardingTag, sharding))) + const entityEntry = entityMap.get(entity.type) + if (!entityEntry) { + return yield* Effect.dieMessage(`Entity.makeTestClient: ${entity.type} was not registered by layer`) + } + + const map = yield* ResourceMap.make(Effect.fnUntraced(function*(entityId: string) { + const address = new EntityAddress({ + entityType: entity.type, + entityId: entityId as EntityId, + shardId: makeShardId(entityId) + }) + const handlers = yield* entityEntry.build.pipe( + Effect.provideService(CurrentAddress, address) + ) + + // eslint-disable-next-line prefer-const + let client!: Effect.Effect.Success>> + const server = yield* RpcServer.makeNoSerialization(entity.protocol, { + concurrency: entityEntry.concurrency, + onFromServer(response) { + return client.write(response) + } + }).pipe(Effect.provide(handlers)) + + client = yield* RpcClient.makeNoSerialization(entity.protocol, { + supportsAck: true, + generateRequestId: () => snowflakeGen.unsafeNext() as any, + onFromClient({ message }) { + if (message._tag === "Request") { + return server.write(0, { + ...message, + payload: new Request({ + ...message, + [Envelope.TypeId]: Envelope.TypeId, + address, + requestId: Snowflake.Snowflake(message.id), + lastSentChunk: Option.none() + }) as any + }) + } + return server.write(0, message) + } + }) + return client.client + })) + + return (entityId: string) => map.get(entityId) +}) + +/** + * @since 1.0.0 + * @category Keep alive + */ +export const keepAlive: ( + enabled: boolean +) => Effect.Effect< + void, + never, + Sharding | CurrentAddress +> = Effect.fnUntraced(function*(enabled: boolean) { + const olatch = yield* Effect.serviceOption(KeepAliveLatch) + if (olatch._tag === "None") return + if (!enabled) { + yield* olatch.value.open + return + } + const sharding = yield* shardingTag + const address = yield* CurrentAddress + const requestId = yield* sharding.getSnowflake + const span = yield* Effect.orDie(Effect.currentSpan) + olatch.value.unsafeClose() + yield* Effect.orDie(sharding.sendOutgoing( + new Message.OutgoingRequest({ + rpc: KeepAliveRpc, + context: Context.empty() as any, + envelope: Envelope.makeRequest({ + requestId, + address, + tag: KeepAliveRpc._tag, + payload: void 0, + headers: Headers.empty, + traceId: span.traceId, + spanId: span.spanId, + sampled: span.sampled + }), + lastReceivedReply: Option.none(), + respond: () => Effect.void + }), + true + )) +}, (effect, enabled) => + Effect.withSpan( + effect, + "Entity/keepAlive", + { attributes: { enabled }, captureStackTrace: false } + )) + +/** + * @since 1.0.0 + * @category Keep alive + */ +export const KeepAliveRpc = Rpc.make("Cluster/Entity/keepAlive") + .annotate(ClusterSchema.Persisted, true) + .annotate(ClusterSchema.Uninterruptible, true) + +/** + * @since 1.0.0 + * @category Keep alive + */ +export class KeepAliveLatch extends Context.Tag( + "effect/cluster/Entity/KeepAliveLatch" +)() {} diff --git a/repos/effect/packages/cluster/src/EntityAddress.ts b/repos/effect/packages/cluster/src/EntityAddress.ts new file mode 100644 index 0000000..e1f8843 --- /dev/null +++ b/repos/effect/packages/cluster/src/EntityAddress.ts @@ -0,0 +1,75 @@ +/** + * @since 1.0.0 + */ +import * as Equal from "effect/Equal" +import * as Hash from "effect/Hash" +import * as Schema from "effect/Schema" +import { EntityId } from "./EntityId.js" +import { EntityType } from "./EntityType.js" +import { ShardId } from "./ShardId.js" + +const SymbolKey = "@effect/cluster/EntityAddress" + +/** + * @since 1.0.0 + * @category type ids + */ +export const TypeId: unique symbol = Symbol.for(SymbolKey) + +/** + * @since 1.0.0 + * @category type ids + */ +export type TypeId = typeof TypeId + +/** + * Represents the unique address of an entity within the cluster. + * + * @since 1.0.0 + * @category models + */ +export class EntityAddress extends Schema.Class(SymbolKey)({ + shardId: ShardId, + entityType: EntityType, + entityId: EntityId +}) { + /** + * @since 1.0.0 + */ + readonly [TypeId] = TypeId; + + /** + * @since 1.0.0 + */ + [Equal.symbol](that: EntityAddress): boolean { + return this.entityType === that.entityType && this.entityId === that.entityId && + this.shardId[Equal.symbol](that.shardId) + } + + /** + * @since 1.0.0 + */ + [Hash.symbol]() { + return Hash.cached(this, Hash.string(`${this.entityType}:${this.entityId}:${this.shardId.toString()}`)) + } +} + +/** + * Represents the unique address of an entity within the cluster. + * + * @since 1.0.0 + * @category schemas + */ +export const EntityAddressFromSelf: Schema.Schema = Schema.typeSchema( + EntityAddress +) + +/** + * @since 4.0.0 + * @category constructors + */ +export const make = (options: { + readonly shardId: ShardId + readonly entityType: EntityType + readonly entityId: EntityId +}): EntityAddress => new EntityAddress(options, { disableValidation: true }) diff --git a/repos/effect/packages/cluster/src/EntityId.ts b/repos/effect/packages/cluster/src/EntityId.ts new file mode 100644 index 0000000..8f226ae --- /dev/null +++ b/repos/effect/packages/cluster/src/EntityId.ts @@ -0,0 +1,22 @@ +/** + * @since 1.0.0 + */ +import * as Schema from "effect/Schema" + +/** + * @since 1.0.0 + * @category constructors + */ +export const EntityId = Schema.NonEmptyTrimmedString.pipe(Schema.brand("EntityId")) + +/** + * @since 1.0.0 + * @category models + */ +export type EntityId = typeof EntityId.Type + +/** + * @since 1.0.0 + * @category constructors + */ +export const make = (id: string): EntityId => id as EntityId diff --git a/repos/effect/packages/cluster/src/EntityProxy.ts b/repos/effect/packages/cluster/src/EntityProxy.ts new file mode 100644 index 0000000..6eb5290 --- /dev/null +++ b/repos/effect/packages/cluster/src/EntityProxy.ts @@ -0,0 +1,227 @@ +/** + * @since 1.0.0 + */ +import * as HttpApiEndpoint from "@effect/platform/HttpApiEndpoint" +import * as HttpApiGroup from "@effect/platform/HttpApiGroup" +import * as Rpc from "@effect/rpc/Rpc" +import * as RpcGroup from "@effect/rpc/RpcGroup" +import * as Schema from "effect/Schema" +import { AlreadyProcessingMessage, MailboxFull, PersistenceError } from "./ClusterError.js" +import type * as Entity from "./Entity.js" + +const clientErrors = [ + MailboxFull, + AlreadyProcessingMessage, + PersistenceError +] as const + +/** + * Derives an `RpcGroup` from an `Entity`. + * + * ```ts + * import { ClusterSchema, Entity, EntityProxy, EntityProxyServer } from "@effect/cluster" + * import { Rpc, RpcServer } from "@effect/rpc" + * import { Layer, Schema } from "effect" + * + * export const Counter = Entity.make("Counter", [ + * Rpc.make("Increment", { + * payload: { id: Schema.String, amount: Schema.Number }, + * primaryKey: ({ id }) => id, + * success: Schema.Number + * }) + * ]).annotateRpcs(ClusterSchema.Persisted, true) + * + * // Use EntityProxy.toRpcGroup to create a `RpcGroup` from the Counter entity + * export class MyRpcs extends EntityProxy.toRpcGroup(Counter) {} + * + * // Use EntityProxyServer.layerRpcHandlers to create a layer that implements + * // the rpc handlers + * const RpcServerLayer = RpcServer.layer(MyRpcs).pipe( + * Layer.provide(EntityProxyServer.layerRpcHandlers(Counter)) + * ) + * ``` + * + * @since 1.0.0 + * @category Constructors + */ +export const toRpcGroup = ( + entity: Entity.Entity +): RpcGroup.RpcGroup> => { + const rpcs: Array = [] + for (const parentRpc_ of entity.protocol.requests.values()) { + const parentRpc = parentRpc_ as any as Rpc.AnyWithProps + const payloadSchema = Schema.Struct({ + entityId: Schema.String, + payload: parentRpc.payloadSchema + }) + const oldMake = payloadSchema.make + payloadSchema.make = (input: any, options?: Schema.MakeOptions) => { + return oldMake({ + entityId: input.entityId, + payload: parentRpc.payloadSchema.make ? parentRpc.payloadSchema.make(input.payload, options) : input.payload + }, options) + } + const rpc = Rpc.make(`${entity.type}.${parentRpc._tag}`, { + payload: payloadSchema, + error: Schema.Union(parentRpc.errorSchema, ...clientErrors), + success: parentRpc.successSchema + }).annotateContext(parentRpc.annotations) + const rpcDiscard = Rpc.make(`${entity.type}.${parentRpc._tag}Discard`, { + payload: payloadSchema, + error: Schema.Union(...clientErrors) + }).annotateContext(parentRpc.annotations) + rpcs.push(rpc, rpcDiscard) + } + return RpcGroup.make(...rpcs) as any as RpcGroup.RpcGroup> +} + +/** + * @since 1.0.0 + */ +export type ConvertRpcs = Rpcs extends Rpc.Rpc< + infer _Tag, + infer _Payload, + infer _Success, + infer _Error, + infer _Middleware +> ? + | Rpc.Rpc< + `${Prefix}.${_Tag}`, + Schema.Struct<{ + entityId: typeof Schema.String + payload: _Payload + }>, + _Success, + Schema.Schema< + | _Error["Type"] + | MailboxFull + | AlreadyProcessingMessage + | PersistenceError + | _Error["Encoded"] + | typeof MailboxFull["Encoded"] + | typeof AlreadyProcessingMessage["Encoded"] + | typeof PersistenceError["Encoded"], + _Error["Context"] + > + > + | Rpc.Rpc< + `${Prefix}.${_Tag}Discard`, + Schema.Struct<{ + entityId: typeof Schema.String + payload: _Payload + }>, + typeof Schema.Void, + Schema.Union<[ + typeof MailboxFull, + typeof AlreadyProcessingMessage, + typeof PersistenceError + ]> + > + : never + +const entityIdPath = Schema.Struct({ + entityId: Schema.String +}) + +/** + * Derives an `HttpApiGroup` from an `Entity`. + * + * ```ts + * import { ClusterSchema, Entity, EntityProxy, EntityProxyServer } from "@effect/cluster" + * import { HttpApi, HttpApiBuilder } from "@effect/platform" + * import { Rpc } from "@effect/rpc" + * import { Layer, Schema } from "effect" + * + * export const Counter = Entity.make("Counter", [ + * Rpc.make("Increment", { + * payload: { id: Schema.String, amount: Schema.Number }, + * primaryKey: ({ id }) => id, + * success: Schema.Number + * }) + * ]).annotateRpcs(ClusterSchema.Persisted, true) + * + * // Use EntityProxy.toHttpApiGroup to create a `HttpApiGroup` from the + * // Counter entity + * export class MyApi extends HttpApi.make("api") + * .add( + * EntityProxy.toHttpApiGroup("counter", Counter) + * .prefix("/counter") + * ) + * {} + * + * // Use EntityProxyServer.layerHttpApi to create a layer that implements + * // the handlers for the HttpApiGroup + * const ApiLayer = HttpApiBuilder.api(MyApi).pipe( + * Layer.provide(EntityProxyServer.layerHttpApi(MyApi, "counter", Counter)) + * ) + * ``` + * + * @since 1.0.0 + * @category Constructors + */ +export const toHttpApiGroup = ( + name: Name, + entity: Entity.Entity +): HttpApiGroup.HttpApiGroup> => { + let group = HttpApiGroup.make(name) + for (const parentRpc_ of entity.protocol.requests.values()) { + const parentRpc = parentRpc_ as any as Rpc.AnyWithProps + const endpoint = HttpApiEndpoint.post(parentRpc._tag, `/${tagToPath(parentRpc._tag)}/:entityId`) + .setPath(entityIdPath) + .setPayload(parentRpc.payloadSchema) + .addSuccess(parentRpc.successSchema) + .addError(Schema.Union(parentRpc.errorSchema, ...clientErrors)) + .annotateContext(parentRpc.annotations) + const endpointDiscard = HttpApiEndpoint.post( + `${parentRpc._tag}Discard`, + `/${tagToPath(parentRpc._tag)}/:entityId/discard` + ) + .setPath(entityIdPath) + .setPayload(parentRpc.payloadSchema) + .addError(Schema.Union(...clientErrors)) + .annotateContext(parentRpc.annotations) + + group = group.add(endpoint).add(endpointDiscard) as any + } + return group as any as HttpApiGroup.HttpApiGroup> +} + +const tagToPath = (tag: string): string => + tag + .replace(/[^a-zA-Z0-9]+/g, "-") // Replace non-alphanumeric characters with hyphen + .replace(/([a-z])([A-Z])/g, "$1-$2") // Insert hyphen before uppercase letters + .toLowerCase() + +/** + * @since 1.0.0 + */ +export type ConvertHttpApi = Rpcs extends Rpc.Rpc< + infer _Tag, + infer _Payload, + infer _Success, + infer _Error, + infer _Middleware +> ? + | HttpApiEndpoint.HttpApiEndpoint< + _Tag, + "POST", + { readonly entityId: string }, + never, + _Payload["Type"], + never, + _Success["Type"], + _Error["Type"] | MailboxFull | AlreadyProcessingMessage | PersistenceError, + _Payload["Context"] | _Success["Context"], + _Error["Context"] + > + | HttpApiEndpoint.HttpApiEndpoint< + `${_Tag}Discard`, + "POST", + { readonly entityId: string }, + never, + _Payload["Type"], + never, + void, + MailboxFull | AlreadyProcessingMessage | PersistenceError + > + : never diff --git a/repos/effect/packages/cluster/src/EntityProxyServer.ts b/repos/effect/packages/cluster/src/EntityProxyServer.ts new file mode 100644 index 0000000..cf0b1ea --- /dev/null +++ b/repos/effect/packages/cluster/src/EntityProxyServer.ts @@ -0,0 +1,117 @@ +/** + * @since 1.0.0 + */ +import type * as HttpApi from "@effect/platform/HttpApi" +import * as HttpApiBuilder from "@effect/platform/HttpApiBuilder" +import type { ApiGroup, HttpApiGroup } from "@effect/platform/HttpApiGroup" +import type * as Rpc from "@effect/rpc/Rpc" +import * as Context from "effect/Context" +import * as Effect from "effect/Effect" +import * as Layer from "effect/Layer" +import type * as Entity from "./Entity.js" +import type { Sharding } from "./Sharding.js" + +/** + * @since 1.0.0 + * @category Layers + */ +export const layerHttpApi = < + ApiId extends string, + Groups extends HttpApiGroup.Any, + ApiE, + ApiR, + Name extends HttpApiGroup.Name, + Type extends string, + Rpcs extends Rpc.Any +>( + api: HttpApi.HttpApi, + name: Name, + entity: Entity.Entity +): Layer.Layer, never, Sharding | Rpc.Context> => + HttpApiBuilder.group( + api, + name, + Effect.fnUntraced(function*(handlers_) { + const client = yield* entity.client + let handlers = handlers_ + for (const parentRpc_ of entity.protocol.requests.values()) { + const parentRpc = parentRpc_ as any as Rpc.AnyWithProps + handlers = handlers + .handle( + parentRpc._tag as any, + (({ path, payload }: { path: { entityId: string }; payload: any }) => + (client(path.entityId) as any as Record Effect.Effect>)[parentRpc._tag]( + payload + ).pipe( + Effect.tapDefect(Effect.logError), + Effect.annotateLogs({ + module: "EntityProxyServer", + entity: entity.type, + entityId: path.entityId, + method: parentRpc._tag + }) + )) as any + ) + .handle( + `${parentRpc._tag}Discard` as any, + (({ path, payload }: { path: { entityId: string }; payload: any }) => + (client(path.entityId) as any as Record Effect.Effect>)[parentRpc._tag]( + payload, + { discard: true } + ).pipe( + Effect.tapDefect(Effect.logError), + Effect.annotateLogs({ + module: "EntityProxyServer", + entity: entity.type, + entityId: path.entityId, + method: `${parentRpc._tag}Discard` + }) + )) as any + ) as any + } + return handlers as HttpApiBuilder.Handlers + }) + ) + +/** + * @since 1.0.0 + * @category Layers + */ +export const layerRpcHandlers = < + const Type extends string, + Rpcs extends Rpc.Any +>(entity: Entity.Entity): Layer.Layer, never, Sharding | Rpc.Context> => + Layer.effectContext(Effect.gen(function*() { + const context = yield* Effect.context() + const client = yield* entity.client + const handlers = new Map>() + for (const parentRpc_ of entity.protocol.requests.values()) { + const parentRpc = parentRpc_ as any as Rpc.AnyWithProps + const tag = `${entity.type}.${parentRpc._tag}` as const + const key = `@effect/rpc/Rpc/${tag}` + handlers.set(key, { + context, + tag, + handler: ({ entityId, payload }: any) => (client(entityId) as any)[parentRpc._tag](payload) as any + } as any) + handlers.set(`${key}Discard`, { + context, + tag, + handler: ({ entityId, payload }: any) => + (client(entityId) as any)[parentRpc._tag](payload, { discard: true }) as any + } as any) + } + return Context.unsafeMake(handlers) + })) + +/** + * @since 1.0.0 + */ +export type RpcHandlers = Rpcs extends Rpc.Rpc< + infer _Tag, + infer _Payload, + infer _Success, + infer _Error, + infer _Middleware +> ? Rpc.Handler<`${Prefix}.${_Tag}`> | Rpc.Handler<`${Prefix}.${_Tag}Discard`> + : never diff --git a/repos/effect/packages/cluster/src/EntityResource.ts b/repos/effect/packages/cluster/src/EntityResource.ts new file mode 100644 index 0000000..5dc2741 --- /dev/null +++ b/repos/effect/packages/cluster/src/EntityResource.ts @@ -0,0 +1,138 @@ +/** + * @since 1.0.0 + */ +import * as Context from "effect/Context" +import * as Duration from "effect/Duration" +import * as Effect from "effect/Effect" +import { identity } from "effect/Function" +import * as RcRef from "effect/RcRef" +import * as Scope from "effect/Scope" +import type * as v1 from "kubernetes-types/core/v1.d.ts" +import * as Entity from "./Entity.js" +import * as K8sHttpClient from "./K8sHttpClient.js" +import type { Sharding } from "./Sharding.js" + +/** + * @since 1.0.0 + * @category Type ids + */ +export const TypeId: TypeId = "~@effect/cluster/EntityResource" + +/** + * @since 1.0.0 + * @category Type ids + */ +export type TypeId = "~@effect/cluster/EntityResource" + +/** + * @since 1.0.0 + * @category Models + */ +export interface EntityResource { + readonly [TypeId]: TypeId + readonly get: Effect.Effect + readonly close: Effect.Effect +} + +/** + * A `Scope` that is only closed when the resource is explicitly closed. + * + * It is not closed during restarts, due to shard movement or node shutdowns. + * + * @since 1.0.0 + * @category Scope + */ +export class CloseScope extends Context.Tag("@effect/cluster/EntityResource/CloseScope")< + CloseScope, + Scope.Scope +>() {} + +/** + * A `EntityResource` is a resource that can be acquired inside a cluster + * entity, which will keep the entity alive even across restarts. + * + * The resource will only be fully released when the idle time to live is + * reached, or when the `close` effect is called. + * + * By default, the `idleTimeToLive` is infinite, meaning the resource will only + * be released when `close` is called. + * + * @since 1.0.0 + * @category Constructors + */ +export const make: (options: { + readonly acquire: Effect.Effect + readonly idleTimeToLive?: Duration.DurationInput | undefined +}) => Effect.Effect< + EntityResource, + E, + Scope.Scope | Exclude | Sharding | Entity.CurrentAddress +> = Effect.fnUntraced(function*(options: { + readonly acquire: Effect.Effect + readonly idleTimeToLive?: Duration.DurationInput | undefined +}) { + let shuttingDown = false + + yield* Entity.keepAlive(true) + + const ref = yield* RcRef.make({ + acquire: Effect.gen(function*() { + const closeable = yield* Scope.make() + + yield* Effect.addFinalizer( + Effect.fnUntraced(function*(exit) { + if (shuttingDown) return + yield* Scope.close(closeable, exit) + yield* Entity.keepAlive(false) + }) + ) + + return yield* options.acquire.pipe( + Effect.provideService(CloseScope, closeable) + ) + }), + idleTimeToLive: options.idleTimeToLive ?? Duration.infinity + }) + + yield* Effect.addFinalizer(() => { + shuttingDown = true + return Effect.void + }) + + // Initialize the resource + yield* Effect.scoped(RcRef.get(ref)) + + return identity>({ + [TypeId]: TypeId, + get: RcRef.get(ref), + close: RcRef.invalidate(ref) + }) +}) + +/** + * @since 1.0.0 + * @category Kubernetes + */ +export const makeK8sPod: ( + spec: v1.Pod, + options?: { + readonly idleTimeToLive?: Duration.DurationInput | undefined + } | undefined +) => Effect.Effect< + EntityResource, + never, + Scope.Scope | Sharding | Entity.CurrentAddress | K8sHttpClient.K8sHttpClient +> = Effect.fnUntraced(function*(spec: v1.Pod, options?: { + readonly idleTimeToLive?: Duration.DurationInput | undefined +}) { + const createPod = yield* K8sHttpClient.makeCreatePod + return yield* make({ + ...options, + acquire: Effect.gen(function*() { + const scope = yield* CloseScope + return yield* createPod(spec).pipe( + Scope.extend(scope) + ) + }) + }) +}) diff --git a/repos/effect/packages/cluster/src/EntityType.ts b/repos/effect/packages/cluster/src/EntityType.ts new file mode 100644 index 0000000..9f97d29 --- /dev/null +++ b/repos/effect/packages/cluster/src/EntityType.ts @@ -0,0 +1,16 @@ +/** + * @since 1.0.0 + */ +import * as Schema from "effect/Schema" + +/** + * @since 1.0.0 + * @category constructors + */ +export const EntityType = Schema.NonEmptyTrimmedString.pipe(Schema.brand("EntityType")) + +/** + * @since 1.0.0 + * @category models + */ +export type EntityType = typeof EntityType.Type diff --git a/repos/effect/packages/cluster/src/Envelope.ts b/repos/effect/packages/cluster/src/Envelope.ts new file mode 100644 index 0000000..fc7f18f --- /dev/null +++ b/repos/effect/packages/cluster/src/Envelope.ts @@ -0,0 +1,368 @@ +/** + * @since 1.0.0 + */ +import * as Headers from "@effect/platform/Headers" +import type * as Rpc from "@effect/rpc/Rpc" +import * as Predicate from "effect/Predicate" +import * as PrimaryKey from "effect/PrimaryKey" +import type { ReadonlyRecord } from "effect/Record" +import * as Schema from "effect/Schema" +import { EntityAddress, EntityAddressFromSelf } from "./EntityAddress.js" +import { type Snowflake, SnowflakeFromString } from "./Snowflake.js" + +/** + * @since 1.0.0 + * @category type ids + */ +export const TypeId: unique symbol = Symbol.for("@effect/cluster/Envelope") + +/** + * @since 1.0.0 + * @category type ids + */ +export type TypeId = typeof TypeId + +/** + * @since 1.0.0 + * @category models + */ +export type Envelope = Request | AckChunk | Interrupt + +/** + * @since 1.0.0 + */ +export declare namespace Envelope { + /** + * @since 1.0.0 + * @category models + */ + export type Any = Envelope + + /** + * @since 1.0.0 + * @category models + */ + export type Encoded = Request.Encoded | typeof AckChunk.Encoded | typeof Interrupt.Encoded + + /** + * @since 1.0.0 + * @category models + */ + export type PartialEncoded = Request.PartialEncoded | AckChunk | Interrupt +} + +/** + * @since 1.0.0 + * @category models + */ +export interface Request { + readonly [TypeId]: TypeId + readonly _tag: "Request" + readonly requestId: Snowflake + readonly address: EntityAddress + readonly tag: Rpc.Tag + readonly payload: Rpc.Payload + readonly headers: Headers.Headers + readonly traceId?: string | undefined + readonly spanId?: string | undefined + readonly sampled?: boolean | undefined +} + +/** + * @since 1.0.0 + * @category models + */ +export class AckChunk extends Schema.TaggedClass("@effect/cluster/Envelope/AckChunk")("AckChunk", { + id: SnowflakeFromString, + address: EntityAddress, + requestId: SnowflakeFromString, + replyId: SnowflakeFromString +}) { + /** + * @since 1.0.0 + */ + readonly [TypeId]: TypeId = TypeId + + /** + * @since 1.0.0 + */ + withRequestId(requestId: Snowflake): AckChunk { + return new AckChunk({ + ...this, + requestId + }) + } +} + +/** + * @since 1.0.0 + * @category models + */ +export class Interrupt extends Schema.TaggedClass("@effect/cluster/Envelope/Interrupt")("Interrupt", { + id: SnowflakeFromString, + address: EntityAddress, + requestId: SnowflakeFromString +}) { + /** + * @since 1.0.0 + */ + readonly [TypeId]: TypeId = TypeId + + /** + * @since 1.0.0 + */ + withRequestId(requestId: Snowflake): Interrupt { + return new Interrupt({ + ...this, + requestId + }) + } +} + +/** + * @since 1.0.0 + */ +export declare namespace Request { + /** + * @since 1.0.0 + * @category models + */ + export type Any = Request + + /** + * @since 1.0.0 + * @category models + */ + export interface Encoded { + readonly _tag: "Request" + readonly requestId: string + readonly address: typeof EntityAddress.Encoded + readonly tag: string + readonly payload: unknown + readonly headers: ReadonlyRecord + readonly traceId?: string | undefined + readonly spanId?: string | undefined + readonly sampled?: boolean | undefined + } + + /** + * @since 1.0.0 + * @category models + */ + export interface PartialEncoded { + readonly _tag: "Request" + readonly requestId: Snowflake + readonly address: EntityAddress + readonly tag: string + readonly payload: unknown + readonly headers: Headers.Headers + readonly traceId?: string | undefined + readonly spanId?: string | undefined + readonly sampled?: boolean | undefined + } +} + +/** + * @since 1.0.0 + * @category refinements + */ +export const isEnvelope = (u: unknown): u is Envelope => Predicate.hasProperty(u, TypeId) + +/** + * @since 1.0.0 + * @category constructors + */ +export const makeRequest = ( + options: { + readonly requestId: Snowflake + readonly address: EntityAddress + readonly tag: Rpc.Tag + readonly payload: Rpc.Payload + readonly headers: Headers.Headers + readonly traceId?: string | undefined + readonly spanId?: string | undefined + readonly sampled?: boolean | undefined + } +): Request => ({ + [TypeId]: TypeId, + _tag: "Request", + requestId: options.requestId, + tag: options.tag, + address: options.address, + payload: options.payload, + headers: options.headers, + traceId: options.traceId, + spanId: options.spanId, + sampled: options.sampled +}) + +/** + * @since 1.0.0 + * @category serialization / deserialization + */ +export const EnvelopeFromSelf: Schema.Schema< + Envelope.Any, + Envelope.Any +> = Schema.declare(isEnvelope, { + typeConstructor: { _tag: "effect/cluster/Envelope" }, + identifier: "Envelope" +}) + +/** + * @since 1.0.0 + * @category serialization / deserialization + */ +export const RequestFromSelf: Schema.Schema< + Request.Any, + Request.Any +> = Schema.declare((u): u is Request.Any => isEnvelope(u) && u._tag === "Request", { + typeConstructor: { _tag: "effect/cluster/Envelope.Request" }, + identifier: "Envelope" +}) + +/** + * @since 1.0.0 + * @category serialization / deserialization + */ +export const PartialEncodedRequest: Schema.Struct< + { + _tag: Schema.Literal<["Request"]> + requestId: Schema.Schema + address: typeof EntityAddress + tag: typeof Schema.String + payload: typeof Schema.Unknown + headers: Schema.Schema> + traceId: Schema.optional + spanId: Schema.optional + sampled: Schema.optional + } +> = Schema.Struct({ + _tag: Schema.Literal("Request"), + requestId: SnowflakeFromString, + address: EntityAddress, + tag: Schema.String, + payload: Schema.Unknown, + headers: Headers.schema, + traceId: Schema.optional(Schema.String), + spanId: Schema.optional(Schema.String), + sampled: Schema.optional(Schema.Boolean) +}) satisfies Schema.Schema + +/** + * @since 1.0.0 + * @category serialization / deserialization + */ +export const PartialEncoded: Schema.Union< + [ + Schema.Struct< + { + _tag: Schema.Literal<["Request"]> + requestId: Schema.Schema + address: typeof EntityAddress + tag: typeof Schema.String + payload: typeof Schema.Unknown + headers: Schema.Schema> + traceId: Schema.optional + spanId: Schema.optional + sampled: Schema.optional + } + >, + typeof AckChunk, + typeof Interrupt + ] +> = Schema.Union(PartialEncodedRequest, AckChunk, Interrupt) satisfies Schema.Schema< + Envelope.PartialEncoded, + Envelope.Encoded +> + +/** + * @since 1.0.0 + * @category serialization / deserialization + */ +export const PartialEncodedArray: Schema.Schema< + Array, + Array +> = Schema.mutable(Schema.Array(PartialEncoded)) + +/** + * @since 1.0.0 + * @category serialization / deserialization + */ +export const PartialEncodedRequestFromSelf: Schema.Struct< + { + _tag: Schema.Literal<["Request"]> + requestId: Schema.Schema + address: Schema.Schema + tag: typeof Schema.String + payload: typeof Schema.Unknown + headers: Schema.Schema + traceId: Schema.optional + spanId: Schema.optional + sampled: Schema.optional + } +> = Schema.Struct({ + _tag: Schema.Literal("Request"), + requestId: Schema.typeSchema(SnowflakeFromString), + address: EntityAddressFromSelf, + tag: Schema.String, + payload: Schema.Unknown, + headers: Headers.schemaFromSelf, + traceId: Schema.optional(Schema.String), + spanId: Schema.optional(Schema.String), + sampled: Schema.optional(Schema.Boolean) +}) satisfies Schema.Schema + +/** + * @since 1.0.0 + * @category serialization / deserialization + */ +export const PartialEncodedFromSelf: Schema.Union< + [ + Schema.Struct< + { + _tag: Schema.Literal<["Request"]> + requestId: Schema.Schema + address: Schema.Schema + tag: typeof Schema.String + payload: typeof Schema.Unknown + headers: Schema.Schema + traceId: Schema.optional + spanId: Schema.optional + sampled: Schema.optional + } + >, + Schema.Schema, + Schema.Schema + ] +> = Schema.Union( + PartialEncodedRequestFromSelf, + Schema.typeSchema(AckChunk), + Schema.typeSchema(Interrupt) +) satisfies Schema.Schema + +/** + * @since 1.0.0 + * @category primary key + */ +export const primaryKey = (envelope: Envelope): string | null => { + if (envelope._tag !== "Request" || !(Predicate.hasProperty(envelope.payload, PrimaryKey.symbol))) { + return null + } + return primaryKeyByAddress({ + address: envelope.address, + tag: envelope.tag, + id: PrimaryKey.value(envelope.payload) + }) +} + +/** + * @since 1.0.0 + * @category primary key + */ +export const primaryKeyByAddress = (options: { + readonly address: EntityAddress + readonly tag: string + readonly id: string +}): string => + // hash the entity address to save space? + `${options.address.entityType}/${options.address.entityId}/${options.tag}/${options.id}` diff --git a/repos/effect/packages/cluster/src/HttpRunner.ts b/repos/effect/packages/cluster/src/HttpRunner.ts new file mode 100644 index 0000000..08130df --- /dev/null +++ b/repos/effect/packages/cluster/src/HttpRunner.ts @@ -0,0 +1,265 @@ +/** + * @since 1.0.0 + */ +import * as HttpClient from "@effect/platform/HttpClient" +import * as HttpClientRequest from "@effect/platform/HttpClientRequest" +import * as HttpRouter from "@effect/platform/HttpLayerRouter" +import type * as HttpServer from "@effect/platform/HttpServer" +import type { HttpServerRequest } from "@effect/platform/HttpServerRequest" +import type { HttpServerResponse } from "@effect/platform/HttpServerResponse" +import * as Socket from "@effect/platform/Socket" +import * as RpcClient from "@effect/rpc/RpcClient" +import * as RpcSerialization from "@effect/rpc/RpcSerialization" +import * as RpcServer from "@effect/rpc/RpcServer" +import * as Effect from "effect/Effect" +import * as Layer from "effect/Layer" +import type { Scope } from "effect/Scope" +import type { MessageStorage } from "./MessageStorage.js" +import type { RunnerHealth } from "./RunnerHealth.js" +import * as Runners from "./Runners.js" +import { RpcClientProtocol } from "./Runners.js" +import * as RunnerServer from "./RunnerServer.js" +import type { RunnerStorage } from "./RunnerStorage.js" +import * as Sharding from "./Sharding.js" +import type * as ShardingConfig from "./ShardingConfig.js" + +const normalizePath = (path: string): string => path.startsWith("/") ? path : `/${path}` + +/** + * @since 1.0.0 + * @category Layers + */ +export const layerClientProtocolHttp = (options: { + readonly path: string + readonly https?: boolean | undefined +}): Layer.Layer< + RpcClientProtocol, + never, + RpcSerialization.RpcSerialization | HttpClient.HttpClient +> => + Layer.effect(RpcClientProtocol)( + Effect.gen(function*() { + const serialization = yield* RpcSerialization.RpcSerialization + const client = yield* HttpClient.HttpClient + const https = options.https ?? false + return (address) => { + const clientWithUrl = HttpClient.mapRequest( + client, + HttpClientRequest.prependUrl( + `http${https ? "s" : ""}://${address.host}:${address.port}${normalizePath(options.path)}` + ) + ) + return RpcClient.makeProtocolHttp(clientWithUrl).pipe( + Effect.provideService(RpcSerialization.RpcSerialization, serialization) + ) + } + }) + ) + +/** + * @since 1.0.0 + * @category Layers + */ +export const layerClientProtocolHttpDefault: Layer.Layer< + Runners.RpcClientProtocol, + never, + RpcSerialization.RpcSerialization | HttpClient.HttpClient +> = layerClientProtocolHttp({ path: "/" }) + +/** + * @since 1.0.0 + * @category Layers + */ +export const layerClientProtocolWebsocket = (options: { + readonly path: string + readonly https?: boolean | undefined +}): Layer.Layer< + RpcClientProtocol, + never, + RpcSerialization.RpcSerialization | Socket.WebSocketConstructor +> => + Layer.effect(RpcClientProtocol)( + Effect.gen(function*() { + const serialization = yield* RpcSerialization.RpcSerialization + const https = options.https ?? false + const constructor = yield* Socket.WebSocketConstructor + return Effect.fnUntraced(function*(address) { + const socket = yield* Socket.makeWebSocket( + `ws${https ? "s" : ""}://${address.host}:${address.port}${normalizePath(options.path)}` + ).pipe( + Effect.provideService(Socket.WebSocketConstructor, constructor) + ) + return yield* RpcClient.makeProtocolSocket().pipe( + Effect.provideService(Socket.Socket, socket), + Effect.provideService(RpcSerialization.RpcSerialization, serialization) + ) + }) + }) + ) + +/** + * @since 1.0.0 + * @category Layers + */ +export const layerClientProtocolWebsocketDefault: Layer.Layer< + Runners.RpcClientProtocol, + never, + RpcSerialization.RpcSerialization | Socket.WebSocketConstructor +> = layerClientProtocolWebsocket({ path: "/" }) + +/** + * @since 1.0.0 + * @category Http App + */ +export const toHttpEffect: Effect.Effect< + Effect.Effect, + never, + Scope | RpcSerialization.RpcSerialization | Sharding.Sharding | MessageStorage +> = Effect.gen(function*() { + const handlers = yield* Layer.build(RunnerServer.layerHandlers) + return yield* RpcServer.toHttpApp(Runners.Rpcs, { + spanPrefix: "RunnerServer", + disableTracing: true + }).pipe(Effect.provide(handlers)) +}) + +/** + * @since 1.0.0 + * @category Http App + */ +export const toHttpEffectWebsocket: Effect.Effect< + Effect.Effect, + never, + Scope | RpcSerialization.RpcSerialization | Sharding.Sharding | MessageStorage +> = Effect.gen(function*() { + const handlers = yield* Layer.build(RunnerServer.layerHandlers) + return yield* RpcServer.toHttpAppWebsocket(Runners.Rpcs, { + spanPrefix: "RunnerServer", + disableTracing: true + }).pipe(Effect.provide(handlers)) +}) + +/** + * @since 1.0.0 + * @category Layers + */ +export const layerClient: Layer.Layer< + Sharding.Sharding | Runners.Runners, + never, + ShardingConfig.ShardingConfig | Runners.RpcClientProtocol | MessageStorage | RunnerStorage | RunnerHealth +> = Sharding.layer.pipe( + Layer.provideMerge(Runners.layerRpc) +) + +/** + * A HTTP layer for the `Runners` services, that adds a route to the provided + * `HttpRouter`. + * + * @since 1.0.0 + * @category Layers + */ +export const layerHttpOptions = (options: { + readonly path: HttpRouter.PathInput +}): Layer.Layer< + Sharding.Sharding | Runners.Runners, + never, + | RunnerStorage + | RunnerHealth + | RpcSerialization.RpcSerialization + | MessageStorage + | ShardingConfig.ShardingConfig + | Runners.RpcClientProtocol + | HttpRouter.HttpRouter +> => + RunnerServer.layerWithClients.pipe( + Layer.provide(RpcServer.layerProtocolHttpRouter(options)) + ) + +/** + * @since 1.0.0 + * @category Layers + */ +export const layerWebsocketOptions = (options: { + readonly path: HttpRouter.PathInput +}): Layer.Layer< + Sharding.Sharding | Runners.Runners, + never, + | ShardingConfig.ShardingConfig + | Runners.RpcClientProtocol + | MessageStorage + | RunnerStorage + | RunnerHealth + | RpcSerialization.RpcSerialization + | HttpRouter.HttpRouter +> => + RunnerServer.layerWithClients.pipe( + Layer.provide(RpcServer.layerProtocolWebsocketRouter(options)) + ) + +/** + * @since 1.0.0 + * @category Layers + */ +export const layerHttp: Layer.Layer< + Sharding.Sharding | Runners.Runners, + never, + | RpcSerialization.RpcSerialization + | ShardingConfig.ShardingConfig + | HttpClient.HttpClient + | HttpServer.HttpServer + | MessageStorage + | RunnerStorage + | RunnerHealth +> = HttpRouter.serve(layerHttpOptions({ path: "/" })).pipe( + Layer.provide(layerClientProtocolHttpDefault) +) + +/** + * @since 1.0.0 + * @category Layers + */ +export const layerHttpClientOnly: Layer.Layer< + Sharding.Sharding | Runners.Runners, + never, + | RpcSerialization.RpcSerialization + | ShardingConfig.ShardingConfig + | HttpClient.HttpClient + | MessageStorage + | RunnerStorage +> = RunnerServer.layerClientOnly.pipe( + Layer.provide(layerClientProtocolHttpDefault) +) + +/** + * @since 1.0.0 + * @category Layers + */ +export const layerWebsocket: Layer.Layer< + Sharding.Sharding | Runners.Runners, + never, + | RpcSerialization.RpcSerialization + | ShardingConfig.ShardingConfig + | Socket.WebSocketConstructor + | HttpServer.HttpServer + | MessageStorage + | RunnerStorage + | RunnerHealth +> = HttpRouter.serve(layerWebsocketOptions({ path: "/" })).pipe( + Layer.provide(layerClientProtocolWebsocketDefault) +) + +/** + * @since 1.0.0 + * @category Layers + */ +export const layerWebsocketClientOnly: Layer.Layer< + Sharding.Sharding | Runners.Runners, + never, + | ShardingConfig.ShardingConfig + | MessageStorage + | RunnerStorage + | RpcSerialization.RpcSerialization + | Socket.WebSocketConstructor +> = RunnerServer.layerClientOnly.pipe( + Layer.provide(layerClientProtocolWebsocketDefault) +) diff --git a/repos/effect/packages/cluster/src/K8sHttpClient.ts b/repos/effect/packages/cluster/src/K8sHttpClient.ts new file mode 100644 index 0000000..339db9a --- /dev/null +++ b/repos/effect/packages/cluster/src/K8sHttpClient.ts @@ -0,0 +1,240 @@ +/** + * @since 1.0.0 + */ +import * as FileSystem from "@effect/platform/FileSystem" +import * as HttpClient from "@effect/platform/HttpClient" +import type * as HttpClientError from "@effect/platform/HttpClientError" +import * as HttpClientRequest from "@effect/platform/HttpClientRequest" +import * as HttpClientResponse from "@effect/platform/HttpClientResponse" +import * as Context from "effect/Context" +import * as Effect from "effect/Effect" +import { identity } from "effect/Function" +import * as Layer from "effect/Layer" +import * as Option from "effect/Option" +import type * as ParseResult from "effect/ParseResult" +import * as Schedule from "effect/Schedule" +import * as Schema from "effect/Schema" +import type * as v1 from "kubernetes-types/core/v1.d.ts" + +/** + * @since 1.0.0 + * @category Tags + */ +export class K8sHttpClient extends Context.Tag("@effect/cluster/K8sHttpClient")< + K8sHttpClient, + HttpClient.HttpClient +>() {} + +/** + * @since 1.0.0 + * @category Layers + */ +export const layer: Layer.Layer< + K8sHttpClient, + never, + HttpClient.HttpClient | FileSystem.FileSystem +> = Layer.effect( + K8sHttpClient, + Effect.gen(function*() { + const fs = yield* FileSystem.FileSystem + const token = yield* fs.readFileString("/var/run/secrets/kubernetes.io/serviceaccount/token").pipe( + Effect.option + ) + return (yield* HttpClient.HttpClient).pipe( + HttpClient.mapRequest(HttpClientRequest.prependUrl("https://kubernetes.default.svc/api")), + token._tag === "Some" ? HttpClient.mapRequest(HttpClientRequest.bearerToken(token.value.trim())) : identity, + HttpClient.filterStatusOk, + HttpClient.retryTransient({ + schedule: Schedule.spaced(5000) + }) + ) + }) +) + +/** + * @since 1.0.0 + * @category Constructors + */ +export const makeGetPods: ( + options?: { + readonly namespace?: string | undefined + readonly labelSelector?: string | undefined + } | undefined +) => Effect.Effect< + Effect.Effect, HttpClientError.HttpClientError | ParseResult.ParseError, never>, + never, + K8sHttpClient +> = Effect.fnUntraced(function*(options?: { + readonly namespace?: string | undefined + readonly labelSelector?: string | undefined +}) { + const client = yield* K8sHttpClient + + const getPods = HttpClientRequest.get( + options?.namespace ? `/v1/namespaces/${options.namespace}/pods` : "/v1/pods" + ).pipe( + HttpClientRequest.setUrlParam("fieldSelector", "status.phase=Running"), + options?.labelSelector ? HttpClientRequest.setUrlParam("labelSelector", options.labelSelector) : identity + ) + + return yield* client.execute(getPods).pipe( + Effect.flatMap(HttpClientResponse.schemaBodyJson(PodList)), + Effect.map((list) => { + const pods = new Map() + for (let i = 0; i < list.items.length; i++) { + const pod = list.items[i] + pods.set(pod.status.podIP, pod) + } + return pods + }), + Effect.tapErrorCause((cause) => Effect.logWarning("Failed to fetch pods from Kubernetes API", cause)), + Effect.cachedWithTTL("10 seconds") + ) +}) + +/** + * @since 1.0.0 + * @category Constructors + */ +export const makeCreatePod = Effect.gen(function*() { + const client = yield* K8sHttpClient + + return Effect.fnUntraced(function*(spec: v1.Pod) { + spec = { + apiVersion: "v1", + kind: "Pod", + metadata: { + namespace: "default", + ...spec.metadata + }, + ...spec + } + const namespace = spec.metadata?.namespace ?? "default" + const name = spec.metadata!.name! + const readPodRaw = HttpClientRequest.get(`/v1/namespaces/${namespace}/pods/${name}`).pipe( + client.execute + ) + const readPod = readPodRaw.pipe( + Effect.flatMap(HttpClientResponse.schemaBodyJson(Pod)), + Effect.asSome, + Effect.retry({ + while: (e) => e._tag === "ParseError", + schedule: Schedule.spaced("1 seconds") + }), + Effect.catchIf((err) => err._tag === "ResponseError" && err.response.status === 404, () => Effect.succeedNone), + Effect.orDie + ) + const isPodFound = readPodRaw.pipe( + Effect.as(true), + Effect.catchIf( + (err) => err._tag === "ResponseError" && err.response.status === 404, + () => Effect.succeed(false) + ) + ) + const createPod = HttpClientRequest.post(`/v1/namespaces/${namespace}/pods`).pipe( + HttpClientRequest.bodyUnsafeJson(spec), + client.execute, + Effect.catchIf( + (err) => err._tag === "ResponseError" && err.response.status === 409, + () => readPod + ), + Effect.tapErrorCause(Effect.logInfo), + Effect.orDie + ) + const deletePod = HttpClientRequest.del(`/v1/namespaces/${namespace}/pods/${name}`).pipe( + client.execute, + Effect.flatMap((res) => res.json), + Effect.catchIf( + (err) => err._tag === "ResponseError" && err.response.status === 404, + () => Effect.void + ), + Effect.tapErrorCause(Effect.logInfo), + Effect.orDie, + Effect.asVoid + ) + yield* Effect.addFinalizer(Effect.fnUntraced(function*() { + yield* deletePod + yield* isPodFound.pipe( + Effect.repeat({ + until: (found) => !found, + schedule: Schedule.spaced("3 seconds") + }), + Effect.orDie + ) + })) + + let opod = Option.none() + while (Option.isNone(opod) || !opod.value.isReady) { + if (Option.isNone(opod)) { + yield* createPod + } + yield* Effect.sleep("3 seconds") + opod = yield* readPod + } + return opod.value.status + }, Effect.withSpan("K8sHttpClient.createPod")) +}) + +/** + * @since 1.0.0 + * @category Schemas + */ +export class PodStatus extends Schema.Class("@effect/cluster/K8sHttpClient/PodStatus")({ + phase: Schema.String, + conditions: Schema.Array(Schema.Struct({ + type: Schema.String, + status: Schema.String, + lastTransitionTime: Schema.NullOr(Schema.String) + })), + podIP: Schema.String, + hostIP: Schema.String +}) {} + +/** + * @since 1.0.0 + * @category Schemas + */ +export class Pod extends Schema.Class("@effect/cluster/K8sHttpClient/Pod")({ + status: PodStatus +}) { + get isReady(): boolean { + for (let i = 0; i < this.status.conditions.length; i++) { + const condition = this.status.conditions[i] + if (condition.type === "Ready") { + return condition.status === "True" + } + } + return false + } + + get isReadyOrInitializing(): boolean { + let initializedAt: string | null | undefined + let readyAt: string | null | undefined + for (let i = 0; i < this.status.conditions.length; i++) { + const condition = this.status.conditions[i] + switch (condition.type) { + case "Initialized": { + if (condition.status !== "True") { + return true + } + initializedAt = condition.lastTransitionTime + break + } + case "Ready": { + if (condition.status === "True") { + return true + } + readyAt = condition.lastTransitionTime + break + } + } + } + // if the pod is still booting up, consider it ready as it would have + // already registered itself with RunnerStorage by now + return initializedAt === readyAt + } +} + +const PodList = Schema.Struct({ + items: Schema.Array(Pod) +}) diff --git a/repos/effect/packages/cluster/src/MachineId.ts b/repos/effect/packages/cluster/src/MachineId.ts new file mode 100644 index 0000000..92056e2 --- /dev/null +++ b/repos/effect/packages/cluster/src/MachineId.ts @@ -0,0 +1,27 @@ +/** + * @since 1.0.0 + */ +import * as Schema from "effect/Schema" + +/** + * @since 1.0.0 + * @category constructors + */ +export const MachineId = Schema.Int.pipe( + Schema.brand("MachineId"), + Schema.annotations({ + pretty: () => (machineId) => `MachineId(${machineId})` + }) +) + +/** + * @since 1.0.0 + * @category models + */ +export type MachineId = typeof MachineId.Type + +/** + * @since 1.0.0 + * @category Constructors + */ +export const make = (shardId: number): MachineId => MachineId.make(shardId) diff --git a/repos/effect/packages/cluster/src/Message.ts b/repos/effect/packages/cluster/src/Message.ts new file mode 100644 index 0000000..ffc58d4 --- /dev/null +++ b/repos/effect/packages/cluster/src/Message.ts @@ -0,0 +1,206 @@ +/** + * @since 1.0.0 + */ +import * as Rpc from "@effect/rpc/Rpc" +import type { Context } from "effect/Context" +import * as Data from "effect/Data" +import * as Effect from "effect/Effect" +import * as FiberRef from "effect/FiberRef" +import * as Option from "effect/Option" +import * as Schema from "effect/Schema" +import type { PersistenceError } from "./ClusterError.js" +import { MalformedMessage } from "./ClusterError.js" +import type { EntityAddress } from "./EntityAddress.js" +import * as Envelope from "./Envelope.js" +import type * as Reply from "./Reply.js" +import type { Snowflake } from "./Snowflake.js" + +/** + * @since 1.0.0 + * @category incoming + */ +export type Incoming = IncomingRequest | IncomingEnvelope + +/** + * @since 1.0.0 + * @category incoming + */ +export type IncomingLocal = IncomingRequestLocal | IncomingEnvelope + +/** + * @since 1.0.0 + * @category incoming + */ +export const incomingLocalFromOutgoing = (self: Outgoing): IncomingLocal => { + if (self._tag === "OutgoingEnvelope") { + return new IncomingEnvelope({ envelope: self.envelope }) + } + return new IncomingRequestLocal({ + envelope: self.envelope, + respond: self.respond, + lastSentReply: Option.none() + }) +} + +/** + * @since 1.0.0 + * @category incoming + */ +export class IncomingRequest extends Data.TaggedClass("IncomingRequest")<{ + readonly envelope: Envelope.Request.PartialEncoded + readonly lastSentReply: Option.Option> + readonly respond: (reply: Reply.ReplyWithContext) => Effect.Effect +}> {} + +/** + * @since 1.0.0 + * @category outgoing + */ +export class IncomingRequestLocal extends Data.TaggedClass("IncomingRequestLocal")<{ + readonly envelope: Envelope.Request + readonly lastSentReply: Option.Option> + readonly respond: (reply: Reply.Reply) => Effect.Effect +}> {} + +/** + * @since 1.0.0 + * @category incoming + */ +export class IncomingEnvelope extends Data.TaggedClass("IncomingEnvelope")<{ + readonly _tag: "IncomingEnvelope" + readonly envelope: Envelope.AckChunk | Envelope.Interrupt +}> {} + +/** + * @since 1.0.0 + * @category outgoing + */ +export type Outgoing = OutgoingRequest | OutgoingEnvelope + +/** + * @since 1.0.0 + * @category outgoing + */ +export class OutgoingRequest extends Data.TaggedClass("OutgoingRequest")<{ + readonly envelope: Envelope.Request + readonly context: Context> + readonly lastReceivedReply: Option.Option> + readonly rpc: R + readonly respond: (reply: Reply.Reply) => Effect.Effect +}> { + /** + * @since 1.0.0 + */ + public encodedCache?: Envelope.Request.PartialEncoded +} + +/** + * @since 1.0.0 + * @category outgoing + */ +export class OutgoingEnvelope extends Data.TaggedClass("OutgoingEnvelope")<{ + readonly envelope: Envelope.AckChunk | Envelope.Interrupt + readonly rpc: Rpc.AnyWithProps +}> { + /** + * @since 1.0.0 + */ + static interrupt(options: { + readonly address: EntityAddress + readonly id: Snowflake + readonly requestId: Snowflake + }): OutgoingEnvelope { + return new OutgoingEnvelope({ + envelope: new Envelope.Interrupt(options), + rpc: neverRpc + }) + } +} + +const neverRpc = Rpc.make("Never", { + success: Schema.Never as any, + error: Schema.Never, + payload: {} +}) + +/** + * @since 1.0.0 + * @category serialization / deserialization + */ +export const serialize = ( + message: Outgoing +): Effect.Effect => { + if (message._tag !== "OutgoingRequest") { + return Effect.succeed(message.envelope) + } + return Effect.suspend(() => + message.encodedCache + ? Effect.succeed(message.encodedCache) + : serializeRequest(message) + ) +} + +/** + * @since 1.0.0 + * @category serialization / deserialization + */ +export const serializeEnvelope = ( + message: Outgoing +): Effect.Effect => + Effect.flatMap( + serialize(message), + (envelope) => MalformedMessage.refail(Schema.encode(Envelope.PartialEncoded)(envelope)) + ) + +/** + * @since 1.0.0 + * @category serialization / deserialization + */ +export const serializeRequest = ( + self: OutgoingRequest +): Effect.Effect => { + const rpc = self.rpc as any as Rpc.AnyWithProps + return Schema.encode(rpc.payloadSchema)(self.envelope.payload).pipe( + Effect.locally(FiberRef.currentContext, self.context), + MalformedMessage.refail, + Effect.map((payload) => ({ + ...self.envelope, + payload + })) + ) as any as Effect.Effect +} + +/** + * @since 1.0.0 + * @category serialization / deserialization + */ +export const deserializeLocal = ( + self: Outgoing, + encoded: Envelope.Envelope.PartialEncoded +): Effect.Effect< + IncomingLocal, + MalformedMessage +> => { + if (encoded._tag !== "Request") { + return Effect.succeed(new IncomingEnvelope({ envelope: encoded })) + } else if (self._tag !== "OutgoingRequest") { + return Effect.fail( + new MalformedMessage({ cause: new Error("Can only deserialize a Request with an OutgoingRequest message") }) + ) + } + const rpc = self.rpc as any as Rpc.AnyWithProps + return Schema.decode(rpc.payloadSchema)(encoded.payload).pipe( + Effect.locally(FiberRef.currentContext, self.context), + MalformedMessage.refail, + Effect.map((payload) => + new IncomingRequestLocal({ + envelope: Envelope.makeRequest({ + ...encoded, + payload + } as any), + lastSentReply: Option.none(), + respond: self.respond + }) + ) + ) as Effect.Effect, MalformedMessage> +} diff --git a/repos/effect/packages/cluster/src/MessageStorage.ts b/repos/effect/packages/cluster/src/MessageStorage.ts new file mode 100644 index 0000000..492c3ac --- /dev/null +++ b/repos/effect/packages/cluster/src/MessageStorage.ts @@ -0,0 +1,919 @@ +/** + * @since 1.0.0 + */ +import type * as Rpc from "@effect/rpc/Rpc" +import * as Arr from "effect/Array" +import * as Context from "effect/Context" +import * as Data from "effect/Data" +import * as Effect from "effect/Effect" +import * as Exit from "effect/Exit" +import * as FiberRef from "effect/FiberRef" +import { globalValue } from "effect/GlobalValue" +import * as Layer from "effect/Layer" +import * as Option from "effect/Option" +import type { ParseError } from "effect/ParseResult" +import type { Predicate } from "effect/Predicate" +import * as Schema from "effect/Schema" +import type { PersistenceError } from "./ClusterError.js" +import { EntityNotAssignedToRunner, MalformedMessage } from "./ClusterError.js" +import * as DeliverAt from "./DeliverAt.js" +import type { EntityAddress } from "./EntityAddress.js" +import * as Envelope from "./Envelope.js" +import * as Message from "./Message.js" +import * as Reply from "./Reply.js" +import type { ShardId } from "./ShardId.js" +import type { ShardingConfig } from "./ShardingConfig.js" +import * as Snowflake from "./Snowflake.js" + +/** + * @since 1.0.0 + * @category context + */ +export class MessageStorage extends Context.Tag("@effect/cluster/MessageStorage")( + envelope: Message.OutgoingRequest + ) => Effect.Effect, PersistenceError | MalformedMessage> + + /** + * Save the provided message and its associated metadata. + */ + readonly saveEnvelope: ( + envelope: Message.OutgoingEnvelope + ) => Effect.Effect + + /** + * Save the provided `Reply` and its associated metadata. + */ + readonly saveReply: ( + reply: Reply.ReplyWithContext + ) => Effect.Effect + + /** + * Clear the `Reply`s for the given request id. + */ + readonly clearReplies: (requestId: Snowflake.Snowflake) => Effect.Effect + + /** + * Retrieves the replies for the specified requests. + * + * - Un-acknowledged chunk replies + * - WithExit replies + */ + readonly repliesFor: ( + requests: Iterable> + ) => Effect.Effect>, PersistenceError | MalformedMessage> + + /** + * Retrieves the encoded replies for the specified request ids. + */ + readonly repliesForUnfiltered: ( + requestIds: Iterable + ) => Effect.Effect>, PersistenceError | MalformedMessage> + + /** + * Retrieves the request id for the specified primary key. + */ + readonly requestIdForPrimaryKey: ( + options: { + readonly address: EntityAddress + readonly tag: string + readonly id: string + } + ) => Effect.Effect, PersistenceError> + + /** + * For locally sent messages, register a handler to process the replies. + */ + readonly registerReplyHandler: ( + message: Message.OutgoingRequest | Message.IncomingRequest + ) => Effect.Effect + + /** + * Unregister the reply handler for the specified message. + */ + readonly unregisterReplyHandler: (requestId: Snowflake.Snowflake) => Effect.Effect + + /** + * Unregister the reply handlers for the specified ShardId. + */ + readonly unregisterShardReplyHandlers: (shardId: ShardId) => Effect.Effect + + /** + * Retrieves the unprocessed messages for the specified shards. + * + * A message is unprocessed when: + * + * - Requests that have no WithExit replies + * - Or they have no unacknowledged chunk replies + * - The latest AckChunk envelope + * - All Interrupt's for unprocessed requests + */ + readonly unprocessedMessages: ( + shardIds: Iterable + ) => Effect.Effect>, PersistenceError> + + /** + * Retrieves the unprocessed messages by id. + */ + readonly unprocessedMessagesById: ( + messageIds: Iterable + ) => Effect.Effect>, PersistenceError> + + /** + * Reset the mailbox state for the provided shards. + */ + readonly resetShards: ( + shardIds: Iterable + ) => Effect.Effect + + /** + * Reset the mailbox state for the provided address. + */ + readonly resetAddress: ( + address: EntityAddress + ) => Effect.Effect + + /** + * Clear all messages and replies for the provided address. + */ + readonly clearAddress: ( + address: EntityAddress + ) => Effect.Effect +}>() {} + +/** + * @since 1.0.0 + * @category SaveResult + */ +export type SaveResult = SaveResult.Success | SaveResult.Duplicate + +/** + * @since 1.0.0 + * @category SaveResult + */ +export const SaveResult = Data.taggedEnum() + +/** + * @since 1.0.0 + * @category SaveResult + */ +export const SaveResultEncoded = Data.taggedEnum() + +/** + * @since 1.0.0 + * @category SaveResult + */ +export declare namespace SaveResult { + /** + * @since 1.0.0 + * @category SaveResult + */ + export type Encoded = SaveResult.Success | SaveResult.DuplicateEncoded + + /** + * @since 1.0.0 + * @category SaveResult + */ + export interface Success { + readonly _tag: "Success" + } + + /** + * @since 1.0.0 + * @category SaveResult + */ + export interface Duplicate { + readonly _tag: "Duplicate" + readonly originalId: Snowflake.Snowflake + readonly lastReceivedReply: Option.Option> + } + + /** + * @since 1.0.0 + * @category SaveResult + */ + export interface DuplicateEncoded { + readonly _tag: "Duplicate" + readonly originalId: Snowflake.Snowflake + readonly lastReceivedReply: Option.Option> + } + + /** + * @since 1.0.0 + * @category SaveResult + */ + export interface Constructor extends Data.TaggedEnum.WithGenerics<1> { + readonly taggedEnum: SaveResult + } +} + +/** + * @since 1.0.0 + * @category Encoded + */ +export type Encoded = { + /** + * Save the provided message and its associated metadata. + */ + readonly saveEnvelope: ( + options: { + readonly envelope: Envelope.Envelope.Encoded + readonly primaryKey: string | null + readonly deliverAt: number | null + } + ) => Effect.Effect + + /** + * Save the provided `Reply` and its associated metadata. + */ + readonly saveReply: ( + reply: Reply.ReplyEncoded + ) => Effect.Effect + + /** + * Remove the replies for the specified request. + */ + readonly clearReplies: (requestId: Snowflake.Snowflake) => Effect.Effect + + /** + * Retrieves the request id for the specified primary key. + */ + readonly requestIdForPrimaryKey: ( + primaryKey: string + ) => Effect.Effect, PersistenceError> + + /** + * Retrieves the replies for the specified requests. + * + * - Un-acknowledged chunk replies + * - WithExit replies + */ + readonly repliesFor: (requestIds: Arr.NonEmptyArray) => Effect.Effect< + Array>, + PersistenceError + > + + /** + * Retrieves the replies for the specified request ids. + */ + readonly repliesForUnfiltered: (requestIds: Arr.NonEmptyArray) => Effect.Effect< + Array>, + PersistenceError + > + + /** + * Retrieves the unprocessed messages for the given shards. + * + * A message is unprocessed when: + * + * - Requests that have no WithExit replies + * - Or they have no unacknowledged chunk replies + * - The latest AckChunk envelope + * - All Interrupt's for unprocessed requests + */ + readonly unprocessedMessages: ( + shardIds: Arr.NonEmptyArray, + now: number + ) => Effect.Effect< + Array<{ + readonly envelope: Envelope.Envelope.Encoded + readonly lastSentReply: Option.Option> + }>, + PersistenceError + > + + /** + * Retrieves the unprocessed messages by id. + */ + readonly unprocessedMessagesById: ( + messageIds: Arr.NonEmptyArray, + now: number + ) => Effect.Effect< + Array<{ + readonly envelope: Envelope.Envelope.Encoded + readonly lastSentReply: Option.Option> + }>, + PersistenceError + > + + /** + * Reset the mailbox state for the provided address. + */ + readonly resetAddress: ( + address: EntityAddress + ) => Effect.Effect + + /** + * Clear all messages and replies for the provided address. + */ + readonly clearAddress: ( + address: EntityAddress + ) => Effect.Effect + + /** + * Reset the mailbox state for the provided shards. + */ + readonly resetShards: ( + shardIds: Arr.NonEmptyArray + ) => Effect.Effect +} + +/** + * @since 1.0.0 + * @category Encoded + */ +export type EncodedUnprocessedOptions = { + readonly existingShards: Array + readonly newShards: Array + readonly cursor: Option.Option +} + +/** + * @since 1.0.0 + * @category Encoded + */ +export type EncodedRepliesOptions = { + readonly existingRequests: Array + readonly newRequests: Array + readonly cursor: Option.Option +} + +/** + * @since 1.0.0 + * @category constructors + */ +export const make = ( + storage: Omit< + MessageStorage["Type"], + "registerReplyHandler" | "unregisterReplyHandler" | "unregisterShardReplyHandlers" + > +): Effect.Effect => + Effect.sync(() => { + type ReplyHandler = { + readonly message: Message.OutgoingRequest | Message.IncomingRequest + readonly shardSet: Set + readonly respond: (reply: Reply.ReplyWithContext) => Effect.Effect + readonly resume: (effect: Effect.Effect) => void + } + const replyHandlers = new Map>() + const replyHandlersShard = new Map>() + return MessageStorage.of({ + ...storage, + registerReplyHandler: (message) => { + const requestId = message.envelope.requestId + return Effect.async((resume) => { + const shardId = message.envelope.address.shardId.toString() + let handlers = replyHandlers.get(requestId) + if (handlers === undefined) { + handlers = [] + replyHandlers.set(requestId, handlers) + } + let shardSet = replyHandlersShard.get(shardId) + if (!shardSet) { + shardSet = new Set() + replyHandlersShard.set(shardId, shardSet) + } + const entry: ReplyHandler = { + message, + shardSet, + respond: message._tag === "IncomingRequest" ? message.respond : (reply) => message.respond(reply.reply), + resume + } + handlers.push(entry) + shardSet.add(entry) + return Effect.sync(() => { + const index = handlers.indexOf(entry) + handlers.splice(index, 1) + shardSet.delete(entry) + }) + }) + }, + unregisterReplyHandler: (requestId) => + Effect.sync(() => { + const handlers = replyHandlers.get(requestId) + if (!handlers) return Effect.void + replyHandlers.delete(requestId) + for (let i = 0; i < handlers.length; i++) { + const handler = handlers[i] + handler.shardSet.delete(handler) + handler.resume(Effect.fail( + new EntityNotAssignedToRunner({ + address: handler.message.envelope.address + }) + )) + } + }), + unregisterShardReplyHandlers: (shardId) => + Effect.sync(() => { + const id = shardId.toString() + const shardSet = replyHandlersShard.get(id) + if (!shardSet) return + replyHandlersShard.delete(id) + shardSet.forEach((handler) => { + replyHandlers.delete(handler.message.envelope.requestId) + handler.resume(Effect.fail( + new EntityNotAssignedToRunner({ + address: handler.message.envelope.address + }) + )) + }) + }), + saveReply(reply) { + const requestId = reply.reply.requestId + return Effect.flatMap(storage.saveReply(reply), () => { + const handlers = replyHandlers.get(requestId) + if (!handlers) { + return Effect.void + } else if (reply.reply._tag === "WithExit") { + replyHandlers.delete(requestId) + for (let i = 0; i < handlers.length; i++) { + const handler = handlers[i] + handler.shardSet.delete(handler) + handler.resume(Effect.void) + } + } + return handlers.length === 1 + ? handlers[0].respond(reply) + : Effect.forEach(handlers, (handler) => handler.respond(reply)) + }) + } + }) + }) + +/** + * @since 1.0.0 + * @category constructors + */ +export const makeEncoded: (encoded: Encoded) => Effect.Effect< + MessageStorage["Type"], + never, + Snowflake.Generator +> = Effect.fnUntraced(function*(encoded: Encoded) { + const snowflakeGen = yield* Snowflake.Generator + const clock = yield* Effect.clock + + const storage: MessageStorage["Type"] = yield* make({ + saveRequest: (message) => + Message.serializeEnvelope(message).pipe( + Effect.flatMap((envelope) => + encoded.saveEnvelope({ + envelope, + primaryKey: Envelope.primaryKey(message.envelope), + deliverAt: DeliverAt.toMillis(message.envelope.payload) + }) + ), + Effect.flatMap((result) => { + if (result._tag === "Success" || result.lastReceivedReply._tag === "None") { + return Effect.succeed(result as SaveResult) + } + const duplicate = result + const schema = Reply.Reply(message.rpc) + return Schema.decode(schema)(result.lastReceivedReply.value).pipe( + Effect.locally(FiberRef.currentContext, message.context), + MalformedMessage.refail, + Effect.map((reply) => + SaveResult.Duplicate({ + originalId: duplicate.originalId, + lastReceivedReply: Option.some(reply) + }) + ) + ) + }) + ), + saveEnvelope: (message) => + Message.serializeEnvelope(message).pipe( + Effect.flatMap((envelope) => + encoded.saveEnvelope({ + envelope, + primaryKey: null, + deliverAt: null + }) + ), + Effect.asVoid + ), + saveReply: (reply) => Effect.flatMap(Reply.serialize(reply), encoded.saveReply), + clearReplies: encoded.clearReplies, + repliesFor: Effect.fnUntraced(function*(messages) { + const requestIds = Arr.empty() + const map = new Map>() + for (const message of messages) { + const id = String(message.envelope.requestId) + requestIds.push(id) + map.set(id, message) + } + if (!Arr.isNonEmptyArray(requestIds)) return [] + const encodedReplies = yield* encoded.repliesFor(requestIds) + return yield* decodeReplies(map, encodedReplies) + }), + repliesForUnfiltered: (ids) => { + const requestIds = Array.from(ids, String) + return Arr.isNonEmptyArray(requestIds) + ? encoded.repliesForUnfiltered(requestIds) + : Effect.succeed([]) + }, + requestIdForPrimaryKey(options) { + const primaryKey = Envelope.primaryKeyByAddress(options) + return encoded.requestIdForPrimaryKey(primaryKey) + }, + unprocessedMessages: (shardIds) => { + const shards = Array.from(shardIds, (id) => id.toString()) + if (!Arr.isNonEmptyArray(shards)) return Effect.succeed([]) + return Effect.flatMap( + Effect.suspend(() => encoded.unprocessedMessages(shards, clock.unsafeCurrentTimeMillis())), + decodeMessages + ) + }, + unprocessedMessagesById(messageIds) { + const ids = Array.from(messageIds) + if (!Arr.isNonEmptyArray(ids)) return Effect.succeed([]) + return Effect.flatMap( + Effect.suspend(() => encoded.unprocessedMessagesById(ids, clock.unsafeCurrentTimeMillis())), + decodeMessages + ) + }, + resetAddress: encoded.resetAddress, + clearAddress: encoded.clearAddress, + resetShards: (shardIds) => { + const shards = Array.from(shardIds, (id) => id.toString()) + return Arr.isNonEmptyArray(shards) + ? encoded.resetShards(shards) + : Effect.void + } + }) + + const decodeMessages = ( + envelopes: Array<{ + readonly envelope: Envelope.Envelope.Encoded + readonly lastSentReply: Option.Option> + }> + ) => { + const messages: Array> = [] + let index = 0 + + // if we have a malformed message, we should not return it and update + // the storage with a defect + const decodeMessage = Effect.catchAll( + Effect.suspend(() => { + const envelope = envelopes[index] + if (!envelope) return Effect.succeed(undefined) + return decodeEnvelopeWithReply(envelope) + }), + (error) => { + const envelope = envelopes[index] + return storage.saveReply(Reply.ReplyWithContext.fromDefect({ + id: snowflakeGen.unsafeNext(), + requestId: Snowflake.Snowflake(envelope.envelope.requestId), + defect: error.toString() + })).pipe( + Effect.forkDaemon, + Effect.asVoid + ) + } + ) + return Effect.as( + Effect.whileLoop({ + while: () => index < envelopes.length, + body: () => decodeMessage, + step: (message) => { + const envelope = envelopes[index++] + if (!message) return + messages.push( + message.envelope._tag === "Request" + ? new Message.IncomingRequest({ + envelope: message.envelope, + lastSentReply: envelope.lastSentReply, + respond: storage.saveReply + }) + : new Message.IncomingEnvelope({ + envelope: message.envelope + }) + ) + } + }), + messages + ) + } + + const decodeReplies = ( + messages: Map>, + encodedReplies: Array> + ) => { + const replies: Array> = [] + const ignoredRequests = new Set() + let index = 0 + + const decodeReply: Effect.Effect> = Effect.catchAll( + Effect.suspend(() => { + const reply = encodedReplies[index] + if (ignoredRequests.has(reply.requestId)) return Effect.void + const message = messages.get(reply.requestId) + if (!message) return Effect.void + const schema = Reply.Reply(message.rpc) + return Schema.decode(schema)(reply).pipe( + Effect.locally(FiberRef.currentContext, message.context) + ) as Effect.Effect, ParseError> + }), + (error) => { + const reply = encodedReplies[index] + ignoredRequests.add(reply.requestId) + return Effect.succeed( + new Reply.WithExit({ + id: snowflakeGen.unsafeNext(), + requestId: Snowflake.Snowflake(reply.requestId), + exit: Exit.die(error) + }) + ) + } + ) + + return Effect.as( + Effect.whileLoop({ + while: () => index < encodedReplies.length, + body: () => decodeReply, + step: (reply) => { + index++ + if (reply) replies.push(reply) + } + }), + replies + ) + } + + return storage +}) + +/** + * @since 1.0.0 + * @category Constructors + */ +export const noop: MessageStorage["Type"] = globalValue( + "@effect/cluster/MessageStorage/noop", + () => + Effect.runSync(make({ + saveRequest: () => Effect.succeed(SaveResult.Success()), + saveEnvelope: () => Effect.void, + saveReply: () => Effect.void, + clearReplies: () => Effect.void, + repliesFor: () => Effect.succeed([]), + repliesForUnfiltered: () => Effect.succeed([]), + requestIdForPrimaryKey: () => Effect.succeedNone, + unprocessedMessages: () => Effect.succeed([]), + unprocessedMessagesById: () => Effect.succeed([]), + resetAddress: () => Effect.void, + clearAddress: () => Effect.void, + resetShards: () => Effect.void + })) +) + +/** + * @since 1.0.0 + * @category Memory + */ +export type MemoryEntry = { + readonly envelope: Envelope.Request.Encoded + lastReceivedChunk: Option.Option> + replies: Array> + deliverAt: number | null +} + +/** + * @since 1.0.0 + * @category Memory + */ +export class MemoryDriver extends Effect.Service()("@effect/cluster/MessageStorage/MemoryDriver", { + dependencies: [Snowflake.layerGenerator], + effect: Effect.gen(function*() { + const clock = yield* Effect.clock + const requests = new Map() + const requestsByPrimaryKey = new Map() + const unprocessed = new Set() + const replyIds = new Set() + + const journal: Array = [] + + const cursors = new WeakMap<{}, number>() + + const unprocessedWith = (predicate: Predicate) => { + const messages: Array<{ + readonly envelope: Envelope.Envelope.Encoded + readonly lastSentReply: Option.Option> + }> = [] + const now = clock.unsafeCurrentTimeMillis() + for (const envelope of unprocessed) { + if (!predicate(envelope)) { + continue + } + if (envelope._tag === "Request") { + const entry = requests.get(envelope.requestId) + if (entry?.deliverAt && entry.deliverAt > now) { + continue + } + messages.push({ + envelope, + lastSentReply: Option.fromNullable(entry?.replies[entry.replies.length - 1]) + }) + } else { + messages.push({ + envelope, + lastSentReply: Option.none() + }) + } + } + return messages + } + + const replyLatch = yield* Effect.makeLatch() + + function repliesFor(requestIds: Array) { + const replies = Arr.empty>() + for (const requestId of requestIds) { + const request = requests.get(requestId) + if (!request) continue + else if (Option.isNone(request.lastReceivedChunk)) { + // eslint-disable-next-line no-restricted-syntax + replies.push(...request.replies) + continue + } + const sequence = request.lastReceivedChunk.value.sequence + for (const reply of request.replies) { + if (reply._tag === "Chunk" && reply.sequence <= sequence) { + continue + } + replies.push(reply) + } + } + return replies + } + + const encoded: Encoded = { + saveEnvelope: ({ deliverAt, envelope: envelope_, primaryKey }) => + Effect.sync(() => { + const envelope = JSON.parse(JSON.stringify(envelope_)) as Envelope.Envelope.Encoded + const existing = primaryKey + ? requestsByPrimaryKey.get(primaryKey) + : envelope._tag === "Request" && requests.get(envelope.requestId) + if (existing) { + return SaveResultEncoded.Duplicate({ + originalId: Snowflake.Snowflake(existing.envelope.requestId), + lastReceivedReply: existing.replies.length === 1 && existing.replies[0]._tag === "WithExit" + ? Option.some(existing.replies[0]) + : existing.lastReceivedChunk + }) + } + if (envelope._tag === "Request") { + const entry: MemoryEntry = { envelope, replies: [], lastReceivedChunk: Option.none(), deliverAt } + requests.set(envelope.requestId, entry) + if (primaryKey) { + requestsByPrimaryKey.set(primaryKey, entry) + } + } else if (envelope._tag === "AckChunk") { + const entry = requests.get(envelope.requestId) + if (entry) { + entry.lastReceivedChunk = Arr.findFirst( + entry.replies, + (r): r is Reply.ChunkEncoded => r._tag === "Chunk" && r.id === envelope.replyId + ).pipe(Option.orElse(() => entry.lastReceivedChunk)) + } + } + unprocessed.add(envelope) + journal.push(envelope) + return SaveResultEncoded.Success() + }), + saveReply: (reply_) => + Effect.sync(() => { + const reply = JSON.parse(JSON.stringify(reply_)) as Reply.ReplyEncoded + const entry = requests.get(reply.requestId) + if (!entry || replyIds.has(reply.id)) return + if (reply._tag === "WithExit") { + unprocessed.delete(entry.envelope) + } + entry.replies.push(reply) + replyIds.add(reply.id) + replyLatch.unsafeOpen() + }), + clearReplies: (id) => + Effect.sync(() => { + const entry = requests.get(String(id)) + if (!entry) return + entry.replies = [] + entry.lastReceivedChunk = Option.none() + unprocessed.add(entry.envelope) + }), + requestIdForPrimaryKey: (primaryKey) => + Effect.sync(() => { + const entry = requestsByPrimaryKey.get(primaryKey) + return Option.fromNullable(entry?.envelope.requestId).pipe(Option.map(Snowflake.Snowflake)) + }), + repliesFor: (requestIds) => Effect.sync(() => repliesFor(requestIds)), + repliesForUnfiltered: (requestIds) => + Effect.sync(() => requestIds.flatMap((id) => requests.get(String(id))?.replies ?? [])), + unprocessedMessages: (shardIds) => + Effect.sync(() => { + if (unprocessed.size === 0) return [] + const now = clock.unsafeCurrentTimeMillis() + const messages = Arr.empty<{ + envelope: Envelope.Envelope.Encoded + lastSentReply: Option.Option> + }>() + for (let index = 0; index < journal.length; index++) { + const envelope = journal[index] + const shardId = envelope.address.shardId + const shardIdStr = `${shardId.group}:${shardId.id}` + if (!unprocessed.has(envelope as any) || !shardIds.includes(shardIdStr)) { + continue + } + if (envelope._tag === "Request") { + const entry = requests.get(envelope.requestId)! + if (entry.deliverAt && entry.deliverAt > now) { + continue + } + messages.push({ + envelope, + lastSentReply: Arr.last(entry.replies) + }) + } else { + messages.push({ + envelope, + lastSentReply: Option.none() + }) + unprocessed.delete(envelope) + } + } + return messages + }), + unprocessedMessagesById: (ids) => + Effect.sync(() => { + const envelopeIds = new Set() + for (const id of ids) { + envelopeIds.add(String(id)) + } + return unprocessedWith((envelope) => envelopeIds.has(envelope.requestId)) + }), + resetAddress: () => Effect.void, + clearAddress: (address) => + Effect.sync(() => { + for (let i = journal.length - 1; i >= 0; i--) { + const envelope = journal[i] + const sameAddress = address.entityType === envelope.address.entityType && + address.entityId === envelope.address.entityId + if (!sameAddress || envelope._tag !== "Request") { + continue + } + unprocessed.delete(envelope) + requests.delete(envelope.requestId) + journal.splice(i, 1) + } + }), + resetShards: () => Effect.void + } + + const storage = yield* makeEncoded(encoded) + + return { + storage, + encoded, + requests, + requestsByPrimaryKey, + unprocessed, + replyIds, + journal, + cursors + } as const + }) +}) {} + +/** + * @since 1.0.0 + * @category layers + */ +export const layerNoop: Layer.Layer = Layer.succeed(MessageStorage, noop) + +/** + * @since 1.0.0 + * @category layers + */ +export const layerMemory: Layer.Layer< + MessageStorage | MemoryDriver, + never, + ShardingConfig +> = Layer.effect(MessageStorage, Effect.map(MemoryDriver, (_) => _.storage)).pipe( + Layer.provideMerge(MemoryDriver.Default) +) + +// --- internal --- + +const EnvelopeWithReply: Schema.Schema<{ + readonly envelope: Envelope.Envelope.PartialEncoded + readonly lastSentReply: Option.Option> +}, { + readonly envelope: Envelope.Envelope.Encoded + readonly lastSentReply: Schema.OptionEncoded> +}> = Schema.Struct({ + envelope: Envelope.PartialEncoded, + lastSentReply: Schema.OptionFromSelf(Reply.Encoded) +}) as any + +const decodeEnvelopeWithReply = Schema.decode(EnvelopeWithReply) diff --git a/repos/effect/packages/cluster/src/Reply.ts b/repos/effect/packages/cluster/src/Reply.ts new file mode 100644 index 0000000..5d56bde --- /dev/null +++ b/repos/effect/packages/cluster/src/Reply.ts @@ -0,0 +1,316 @@ +/** + * @since 1.0.0 + */ +import * as Rpc from "@effect/rpc/Rpc" +import type * as RpcSchema from "@effect/rpc/RpcSchema" +import type { NonEmptyReadonlyArray } from "effect/Array" +import * as Context from "effect/Context" +import * as Data from "effect/Data" +import * as Effect from "effect/Effect" +import * as Exit from "effect/Exit" +import * as FiberId from "effect/FiberId" +import * as FiberRef from "effect/FiberRef" +import { identity } from "effect/Function" +import type * as Option from "effect/Option" +import { hasProperty } from "effect/Predicate" +import * as Schema from "effect/Schema" +import { MalformedMessage } from "./ClusterError.js" +import type { OutgoingRequest } from "./Message.js" +import { Snowflake, SnowflakeFromString } from "./Snowflake.js" + +/** + * @since 1.0.0 + * @category type ids + */ +export const TypeId: unique symbol = Symbol.for("@effect/cluster/Reply") + +/** + * @since 1.0.0 + * @category type ids + */ +export type TypeId = typeof TypeId + +/** + * @since 1.0.0 + * @category guards + */ +export const isReply = (u: unknown): u is Reply => hasProperty(u, TypeId) + +/** + * @since 1.0.0 + * @category models + */ +export type Reply = WithExit | Chunk + +/** + * @since 1.0.0 + * @category models + */ +export class ReplyWithContext extends Data.TaggedClass("ReplyWithContext")<{ + readonly reply: Reply + readonly context: Context.Context> + readonly rpc: R +}> { + /** + * @since 1.0.0 + */ + static fromDefect(options: { + readonly id: Snowflake + readonly requestId: Snowflake + readonly defect: unknown + }): ReplyWithContext { + return new ReplyWithContext({ + reply: new WithExit({ + requestId: options.requestId, + id: options.id, + exit: Exit.die(Schema.encodeSync(Schema.Defect)(options.defect)) + }), + context: Context.empty() as any, + rpc: neverRpc + }) + } + /** + * @since 1.0.0 + */ + static interrupt(options: { + readonly id: Snowflake + readonly requestId: Snowflake + }): ReplyWithContext { + return new ReplyWithContext({ + reply: new WithExit({ + requestId: options.requestId, + id: options.id, + exit: Exit.interrupt(FiberId.none) + }), + context: Context.empty() as any, + rpc: neverRpc + }) + } +} + +const neverRpc = Rpc.make("Never", { + success: Schema.Never as any, + error: Schema.Never, + payload: {} +}) + +/** + * @since 1.0.0 + * @category models + */ +export type ReplyEncoded = WithExitEncoded | ChunkEncoded + +/** + * @since 1.0.0 + * @category models + */ +export interface WithExitEncoded { + readonly _tag: "WithExit" + readonly requestId: string + readonly id: string + readonly exit: Rpc.ExitEncoded +} + +/** + * @since 1.0.0 + * @category models + */ +export interface ChunkEncoded { + readonly _tag: "Chunk" + readonly requestId: string + readonly id: string + readonly sequence: number + readonly values: NonEmptyReadonlyArray> +} + +const schemaCache = new WeakMap, ReplyEncoded, Rpc.Context>>() + +/** + * @since 1.0.0 + * @category schemas + */ +export const Reply = (rpc: R): Schema.Schema< + Reply, + ReplyEncoded, + Rpc.Context +> => { + if (schemaCache.has(rpc)) { + return schemaCache.get(rpc) as any + } + const schema = Schema.Union(WithExit.schema(rpc), Chunk.schema(rpc)) + schemaCache.set(rpc, schema) + return schema +} + +/** + * @since 1.0.0 + * @category schemas + */ +export const Encoded = Schema.Union( + Schema.Struct({ + _tag: Schema.Literal("WithExit"), + requestId: Schema.String, + id: Schema.String, + exit: Schema.Unknown + }), + Schema.Struct({ + _tag: Schema.Literal("Chunk"), + requestId: Schema.String, + id: Schema.String, + sequence: Schema.Number, + values: Schema.Array(Schema.Unknown) + }) +) + +/** + * @since 1.0.0 + * @category models + */ +export class Chunk extends Data.TaggedClass("Chunk")<{ + readonly requestId: Snowflake + readonly id: Snowflake + readonly sequence: number + readonly values: NonEmptyReadonlyArray> +}> { + /** + * @since 1.0.0 + */ + readonly [TypeId] = TypeId + + /** + * @since 1.0.0 + */ + static emptyFrom(requestId: Snowflake) { + return new Chunk({ + requestId, + id: Snowflake(BigInt(0)), + sequence: 0, + values: [undefined] + }) + } + + /** + * @since 1.0.0 + */ + static readonly schemaFromSelf: Schema.Schema> = Schema.declare( + (u): u is Chunk => isReply(u) && u._tag === "Chunk", + { + typeConstructor: { _tag: "effect/cluster/Reply.Chunk" } + } + ) + + /** + * @since 1.0.0 + */ + static schema(rpc: R): Schema.Schema< + Chunk, + ChunkEncoded, + Rpc.Context + > { + const successSchema = ((rpc as any as Rpc.AnyWithProps).successSchema as RpcSchema.Stream).success + if (!successSchema) { + return Schema.Never as any + } + return Schema.transform( + Schema.Struct({ + _tag: Schema.Literal("Chunk"), + requestId: SnowflakeFromString, + id: SnowflakeFromString, + sequence: Schema.Number, + values: Schema.NonEmptyArray(successSchema) + }), + Chunk.schemaFromSelf, + { + decode: (encoded) => new Chunk(encoded as any), + encode: identity + } + ) as any + } + + /** + * @since 1.0.0 + */ + withRequestId(requestId: Snowflake): Chunk { + return new Chunk({ + ...this, + requestId + }) + } +} + +/** + * @since 1.0.0 + * @category models + */ +export class WithExit extends Data.TaggedClass("WithExit")<{ + readonly requestId: Snowflake + readonly id: Snowflake + readonly exit: Rpc.Exit +}> { + /** + * @since 1.0.0 + */ + readonly [TypeId] = TypeId + + /** + * @since 1.0.0 + */ + static schema(rpc: R): Schema.Schema< + WithExit, + WithExitEncoded, + Rpc.Context + > { + return Schema.transform( + Schema.Struct({ + _tag: Schema.Literal("WithExit"), + requestId: SnowflakeFromString, + id: SnowflakeFromString, + exit: Rpc.exitSchema(rpc) + }), + Schema.declare((u): u is WithExit => isReply(u) && u._tag === "WithExit"), + { + decode: (encoded) => new WithExit(encoded), + encode: identity + } + ) as any + } + + /** + * @since 1.0.0 + */ + withRequestId(requestId: Snowflake): WithExit { + return new WithExit({ + ...this, + requestId + }) + } +} + +/** + * @since 1.0.0 + * @category serialization / deserialization + */ +export const serialize = ( + self: ReplyWithContext +): Effect.Effect, MalformedMessage> => { + const schema = Reply(self.rpc) + return MalformedMessage.refail( + Effect.locally(Schema.encode(schema)(self.reply), FiberRef.currentContext, self.context) + ) +} + +/** + * @since 1.0.0 + * @category serialization / deserialization + */ +export const serializeLastReceived = ( + self: OutgoingRequest +): Effect.Effect>, MalformedMessage> => { + if (self.lastReceivedReply._tag === "None") { + return Effect.succeedNone + } + const schema = Reply(self.rpc) + return Effect.asSome(MalformedMessage.refail( + Effect.locally(Schema.encode(schema)(self.lastReceivedReply.value), FiberRef.currentContext, self.context) + )) +} diff --git a/repos/effect/packages/cluster/src/Runner.ts b/repos/effect/packages/cluster/src/Runner.ts new file mode 100644 index 0000000..74d239d --- /dev/null +++ b/repos/effect/packages/cluster/src/Runner.ts @@ -0,0 +1,102 @@ +/** + * @since 1.0.0 + */ +import * as Equal from "effect/Equal" +import * as Hash from "effect/Hash" +import { NodeInspectSymbol } from "effect/Inspectable" +import * as Pretty from "effect/Pretty" +import * as Schema from "effect/Schema" +import { RunnerAddress } from "./RunnerAddress.js" + +const SymbolKey = "@effect/cluster/Runner" + +/** + * @since 1.0.0 + * @category type ids + */ +export const TypeId: unique symbol = Symbol.for(SymbolKey) + +/** + * @since 1.0.0 + * @category type ids + */ +export type TypeId = typeof TypeId + +/** + * A `Runner` represents a physical application server that is capable of running + * entities. + * + * Because a Runner represents a physical application server, a Runner must have a + * unique `address` which can be used to communicate with the server. + * + * The version of a Runner is used during rebalancing to give priority to newer + * application servers and slowly decommission older ones. + * + * @since 1.0.0 + * @category models + */ +export class Runner extends Schema.Class(SymbolKey)({ + address: RunnerAddress, + groups: Schema.Array(Schema.String), + weight: Schema.Number +}) { + /** + * @since 1.0.0 + */ + static pretty = Pretty.make(this) + + /** + * @since 1.0.0 + */ + readonly [TypeId] = TypeId + + /** + * @since 1.0.0 + */ + static readonly decodeSync = Schema.decodeSync(Schema.parseJson(Runner)) + + /** + * @since 1.0.0 + */ + static readonly encodeSync = Schema.encodeSync(Schema.parseJson(Runner)); + + /** + * @since 1.0.0 + */ + [NodeInspectSymbol](): string { + return this.toString() + } + + /** + * @since 1.0.0 + */ + [Equal.symbol](that: Runner): boolean { + return this.address[Equal.symbol](that.address) && this.weight === that.weight + } + + /** + * @since 1.0.0 + */ + [Hash.symbol](): number { + return Hash.cached(this, Hash.string(`${this.address.toString()}:${this.weight}`)) + } +} + +/** + * A `Runner` represents a physical application server that is capable of running + * entities. + * + * Because a Runner represents a physical application server, a Runner must have a + * unique `address` which can be used to communicate with the server. + * + * The version of a Runner is used during rebalancing to give priority to newer + * application servers and slowly decommission older ones. + * + * @since 1.0.0 + * @category Constructors + */ +export const make = (props: { + readonly address: RunnerAddress + readonly groups: ReadonlyArray + readonly weight: number +}): Runner => new Runner(props) diff --git a/repos/effect/packages/cluster/src/RunnerAddress.ts b/repos/effect/packages/cluster/src/RunnerAddress.ts new file mode 100644 index 0000000..a8e0e13 --- /dev/null +++ b/repos/effect/packages/cluster/src/RunnerAddress.ts @@ -0,0 +1,77 @@ +/** + * @since 1.0.0 + */ +import * as Equal from "effect/Equal" +import * as Hash from "effect/Hash" +import { NodeInspectSymbol } from "effect/Inspectable" +import * as PrimaryKey from "effect/PrimaryKey" +import * as Schema from "effect/Schema" + +const SymbolKey = "@effect/cluster/RunnerAddress" + +/** + * @since 1.0.0 + * @category type ids + */ +export const TypeId: unique symbol = Symbol.for(SymbolKey) + +/** + * @since 1.0.0 + * @category type ids + */ +export type TypeId = typeof TypeId + +/** + * @since 1.0.0 + * @category models + */ +export class RunnerAddress extends Schema.Class(SymbolKey)({ + host: Schema.NonEmptyString, + port: Schema.Int +}) { + /** + * @since 1.0.0 + */ + readonly [TypeId] = TypeId; + + /** + * @since 1.0.0 + */ + [PrimaryKey.symbol](): string { + return `${this.host}:${this.port}` + } + + /** + * @since 1.0.0 + */ + [Equal.symbol](that: RunnerAddress): boolean { + return this.host === that.host && this.port === that.port + } + + /** + * @since 1.0.0 + */ + [Hash.symbol]() { + return Hash.cached(this, Hash.string(this.toString())) + } + + /** + * @since 1.0.0 + */ + toString(): string { + return `RunnerAddress(${this.host}:${this.port})` + } + + /** + * @since 1.0.0 + */ + [NodeInspectSymbol](): string { + return this.toString() + } +} + +/** + * @since 1.0.0 + * @category constructors + */ +export const make = (host: string, port: number): RunnerAddress => new RunnerAddress({ host, port }) diff --git a/repos/effect/packages/cluster/src/RunnerHealth.ts b/repos/effect/packages/cluster/src/RunnerHealth.ts new file mode 100644 index 0000000..9d32da9 --- /dev/null +++ b/repos/effect/packages/cluster/src/RunnerHealth.ts @@ -0,0 +1,117 @@ +/** + * @since 1.0.0 + */ +import * as Context from "effect/Context" +import * as Effect from "effect/Effect" +import * as Layer from "effect/Layer" +import * as Schedule from "effect/Schedule" +import type * as Scope from "effect/Scope" +import * as K8s from "./K8sHttpClient.js" +import type { RunnerAddress } from "./RunnerAddress.js" +import * as Runners from "./Runners.js" + +/** + * Represents the service used to check if a Runner is healthy. + * + * If a Runner is responsive, shards will not be re-assigned because the Runner may + * still be processing messages. If a Runner is not responsive, then its + * associated shards can and will be re-assigned to a different Runner. + * + * @since 1.0.0 + * @category models + */ +export class RunnerHealth extends Context.Tag("@effect/cluster/RunnerHealth")< + RunnerHealth, + { + readonly isAlive: (address: RunnerAddress) => Effect.Effect + } +>() {} + +/** + * A layer which will **always** consider a Runner healthy. + * + * This is useful for testing. + * + * @since 1.0.0 + * @category layers + */ +export const layerNoop = Layer.succeed(RunnerHealth, { + isAlive: () => Effect.succeed(true) +}) + +/** + * @since 1.0.0 + * @category Constructors + */ +export const makePing: Effect.Effect< + RunnerHealth["Type"], + never, + Runners.Runners | Scope.Scope +> = Effect.gen(function*() { + const runners = yield* Runners.Runners + const schedule = Schedule.spaced(500) + + function isAlive(address: RunnerAddress): Effect.Effect { + return runners.ping(address).pipe( + Effect.timeout(10_000), + Effect.retry({ times: 5, schedule }), + Effect.isSuccess + ) + } + + return RunnerHealth.of({ isAlive }) +}) + +/** + * A layer which will ping a Runner directly to check if it is healthy. + * + * @since 1.0.0 + * @category layers + */ +export const layerPing: Layer.Layer< + RunnerHealth, + never, + Runners.Runners +> = Layer.scoped(RunnerHealth, makePing) + +/** + * @since 1.0.0 + * @category Constructors + */ +export const makeK8s = Effect.fnUntraced(function*(options?: { + readonly namespace?: string | undefined + readonly labelSelector?: string | undefined +}) { + const allPods = yield* K8s.makeGetPods(options) + + return RunnerHealth.of({ + isAlive: (address) => + allPods.pipe( + Effect.map((pods) => pods.get(address.host)?.isReadyOrInitializing ?? false), + Effect.catchAllCause(() => Effect.succeed(true)) + ) + }) +}) + +/** + * A layer which will check the Kubernetes API to see if a Runner is healthy. + * + * The provided HttpClient will need to add the pod's CA certificate to its + * trusted root certificates in order to communicate with the Kubernetes API. + * + * The pod service account will also need to have permissions to list pods in + * order to use this layer. + * + * @since 1.0.0 + * @category layers + */ +export const layerK8s = ( + options?: { + readonly namespace?: string | undefined + readonly labelSelector?: string | undefined + } | undefined +): Layer.Layer< + RunnerHealth, + never, + K8s.K8sHttpClient +> => Layer.effect(RunnerHealth, makeK8s(options)) diff --git a/repos/effect/packages/cluster/src/RunnerServer.ts b/repos/effect/packages/cluster/src/RunnerServer.ts new file mode 100644 index 0000000..3d65185 --- /dev/null +++ b/repos/effect/packages/cluster/src/RunnerServer.ts @@ -0,0 +1,203 @@ +/** + * @since 1.0.0 + */ +import * as RpcServer from "@effect/rpc/RpcServer" +import * as Effect from "effect/Effect" +import type * as Exit from "effect/Exit" +import * as Fiber from "effect/Fiber" +import { constant } from "effect/Function" +import * as Layer from "effect/Layer" +import * as Mailbox from "effect/Mailbox" +import * as Option from "effect/Option" +import * as Runtime from "effect/Runtime" +import type * as ClusterError from "./ClusterError.js" +import * as Message from "./Message.js" +import * as MessageStorage from "./MessageStorage.js" +import * as Reply from "./Reply.js" +import * as RunnerHealth from "./RunnerHealth.js" +import * as Runners from "./Runners.js" +import type * as RunnerStorage from "./RunnerStorage.js" +import * as Sharding from "./Sharding.js" +import { ShardingConfig } from "./ShardingConfig.js" + +const constVoid = constant(Effect.void) + +/** + * @since 1.0.0 + * @category Layers + */ +export const layerHandlers = Runners.Rpcs.toLayer(Effect.gen(function*() { + const sharding = yield* Sharding.Sharding + const storage = yield* MessageStorage.MessageStorage + + return { + Ping: () => Effect.void, + Notify: ({ envelope }) => + sharding.notify( + envelope._tag === "Request" + ? new Message.IncomingRequest({ + envelope, + respond: constVoid, + lastSentReply: Option.none() + }) + : new Message.IncomingEnvelope({ envelope }) + ), + Effect: ({ persisted, request }) => { + let replyEncoded: + | Effect.Effect< + Reply.ReplyEncoded, + ClusterError.EntityNotAssignedToRunner + > + | undefined = undefined + let resume = (reply: Effect.Effect, ClusterError.EntityNotAssignedToRunner>) => { + replyEncoded = reply + } + const message = new Message.IncomingRequest({ + envelope: request, + lastSentReply: Option.none(), + respond(reply) { + resume(Effect.orDie(Reply.serialize(reply))) + return Effect.void + } + }) + if (persisted) { + return Effect.async< + Reply.ReplyEncoded, + ClusterError.EntityNotAssignedToRunner + >((resume_) => { + resume = resume_ + const parent = Option.getOrThrow(Fiber.getCurrentFiber()) + const runtime = Runtime.make({ + context: parent.currentContext, + runtimeFlags: Runtime.defaultRuntimeFlags, + fiberRefs: parent.getFiberRefs() + }) + const onExit = ( + exit: Exit.Exit< + any, + ClusterError.EntityNotAssignedToRunner + > + ) => { + if (exit._tag === "Failure") { + resume(exit as any) + } + } + const fiber = Runtime.runFork(runtime)(storage.registerReplyHandler(message)) + fiber.addObserver(onExit) + Runtime.runFork(runtime)(Effect.catchTag( + sharding.notify(message, constWaitUntilRead), + "AlreadyProcessingMessage", + () => Effect.void + )).addObserver(onExit) + return Fiber.interrupt(fiber) + }) + } + return Effect.zipRight( + sharding.send(message), + Effect.async, ClusterError.EntityNotAssignedToRunner>((resume_) => { + if (replyEncoded) { + resume_(replyEncoded) + } else { + resume = resume_ + } + }) + ) + }, + Stream: ({ persisted, request }) => + Effect.flatMap( + Mailbox.make, ClusterError.EntityNotAssignedToRunner>(), + (mailbox) => { + const message = new Message.IncomingRequest({ + envelope: request, + lastSentReply: Option.none(), + respond(reply) { + return Effect.flatMap(Reply.serialize(reply), (reply) => { + mailbox.unsafeOffer(reply) + return Effect.void + }) + } + }) + return Effect.as( + persisted ? + Effect.zipRight( + storage.registerReplyHandler(message).pipe( + Effect.onError((cause) => mailbox.failCause(cause)), + Effect.forkScoped, + Effect.interruptible + ), + sharding.notify(message, constWaitUntilRead) + ) : + sharding.send(message), + mailbox + ) + } + ), + Envelope: ({ envelope }) => sharding.send(new Message.IncomingEnvelope({ envelope })) + } +})) + +const constWaitUntilRead = { waitUntilRead: true } as const + +/** + * The `RunnerServer` receives messages from other Runners and forwards them to the + * `Sharding` layer. + * + * It also responds to `Ping` requests. + * + * @since 1.0.0 + * @category Layers + */ +export const layer: Layer.Layer< + never, + never, + RpcServer.Protocol | Sharding.Sharding | MessageStorage.MessageStorage +> = RpcServer.layer(Runners.Rpcs, { + spanPrefix: "RunnerServer", + disableTracing: true +}).pipe(Layer.provide(layerHandlers)) + +/** + * A `RunnerServer` layer that includes the `Runners` & `Sharding` clients. + * + * @since 1.0.0 + * @category Layers + */ +export const layerWithClients: Layer.Layer< + Sharding.Sharding | Runners.Runners, + never, + | RpcServer.Protocol + | ShardingConfig + | Runners.RpcClientProtocol + | MessageStorage.MessageStorage + | RunnerStorage.RunnerStorage + | RunnerHealth.RunnerHealth +> = layer.pipe( + Layer.provideMerge(Sharding.layer), + Layer.provideMerge(Runners.layerRpc) +) + +/** + * A `Runners` layer that is client only. + * + * It will not register with RunnerStorage and receive shard assignments, + * so this layer can be used to embed a cluster client inside another effect + * application. + * + * @since 1.0.0 + * @category Layers + */ +export const layerClientOnly: Layer.Layer< + Sharding.Sharding | Runners.Runners, + never, + | ShardingConfig + | Runners.RpcClientProtocol + | MessageStorage.MessageStorage + | RunnerStorage.RunnerStorage +> = Sharding.layer.pipe( + Layer.provideMerge(Runners.layerRpc), + Layer.provide(RunnerHealth.layerNoop), + Layer.updateService(ShardingConfig, (config) => ({ + ...config, + runnerAddress: Option.none() + })) +) diff --git a/repos/effect/packages/cluster/src/RunnerStorage.ts b/repos/effect/packages/cluster/src/RunnerStorage.ts new file mode 100644 index 0000000..f8955ba --- /dev/null +++ b/repos/effect/packages/cluster/src/RunnerStorage.ts @@ -0,0 +1,218 @@ +/** + * @since 1.0.0 + */ +import { isNonEmptyArray, type NonEmptyArray } from "effect/Array" +import * as Context from "effect/Context" +import * as Effect from "effect/Effect" +import * as Layer from "effect/Layer" +import * as MutableHashMap from "effect/MutableHashMap" +import type { PersistenceError } from "./ClusterError.js" +import * as MachineId from "./MachineId.js" +import { Runner } from "./Runner.js" +import type { RunnerAddress } from "./RunnerAddress.js" +import { ShardId } from "./ShardId.js" + +/** + * Represents a generic interface to the persistent storage required by the + * cluster. + * + * @since 1.0.0 + * @category models + */ +export class RunnerStorage extends Context.Tag("@effect/cluster/RunnerStorage") Effect.Effect + + /** + * Unregister the runner with the given address. + */ + readonly unregister: (address: RunnerAddress) => Effect.Effect + + /** + * Get all runners registered with the cluster. + */ + readonly getRunners: Effect.Effect, PersistenceError> + + /** + * Set the health status of the given runner. + */ + readonly setRunnerHealth: (address: RunnerAddress, healthy: boolean) => Effect.Effect + + /** + * Try to acquire the given shard ids for processing. + * + * It returns an array of shards it was able to acquire. + */ + readonly acquire: ( + address: RunnerAddress, + shardIds: Iterable + ) => Effect.Effect, PersistenceError> + + /** + * Refresh the locks owned by the given runner. + */ + readonly refresh: ( + address: RunnerAddress, + shardIds: Iterable + ) => Effect.Effect, PersistenceError> + + /** + * Release the given shard ids. + */ + readonly release: ( + address: RunnerAddress, + shardId: ShardId + ) => Effect.Effect + + /** + * Release all the shards assigned to the given runner. + */ + readonly releaseAll: (address: RunnerAddress) => Effect.Effect +}>() {} + +/** + * @since 1.0.0 + * @category Encoded + */ +export interface Encoded { + /** + * Get all runners registered with the cluster. + */ + readonly getRunners: Effect.Effect, PersistenceError> + + /** + * Register a new runner with the cluster. + */ + readonly register: (address: string, runner: string, healthy: boolean) => Effect.Effect + + /** + * Unregister the runner with the given address. + */ + readonly unregister: (address: string) => Effect.Effect + + /** + * Set the health status of the given runner. + */ + readonly setRunnerHealth: (address: string, healthy: boolean) => Effect.Effect + + /** + * Acquire the lock on the given shards, returning the shards that were + * successfully locked. + */ + readonly acquire: ( + address: string, + shardIds: NonEmptyArray + ) => Effect.Effect, PersistenceError> + + /** + * Refresh the lock on the given shards, returning the shards that were + * successfully locked. + */ + readonly refresh: ( + address: string, + shardIds: Array + ) => Effect.Effect, PersistenceError> + + /** + * Release the lock on the given shard. + */ + readonly release: ( + address: string, + shardId: string + ) => Effect.Effect + + /** + * Release the lock on all shards for the given runner. + */ + readonly releaseAll: (address: string) => Effect.Effect +} + +/** + * @since 1.0.0 + * @category layers + */ +export const makeEncoded = (encoded: Encoded) => + RunnerStorage.of({ + getRunners: Effect.gen(function*() { + const runners = yield* encoded.getRunners + const results: Array<[Runner, boolean]> = [] + for (let i = 0; i < runners.length; i++) { + const [runner, healthy] = runners[i] + try { + results.push([Runner.decodeSync(runner), healthy]) + } catch { + // + } + } + return results + }), + register: (runner, healthy) => + Effect.map( + encoded.register(encodeRunnerAddress(runner.address), Runner.encodeSync(runner), healthy), + MachineId.make + ), + unregister: (address) => encoded.unregister(encodeRunnerAddress(address)), + setRunnerHealth: (address, healthy) => encoded.setRunnerHealth(encodeRunnerAddress(address), healthy), + acquire: (address, shardIds) => { + const arr = Array.from(shardIds, (id) => id.toString()) + if (!isNonEmptyArray(arr)) return Effect.succeed([]) + return encoded.acquire(encodeRunnerAddress(address), arr).pipe( + Effect.map((shards) => shards.map(ShardId.fromString)) + ) + }, + refresh: (address, shardIds) => + encoded.refresh(encodeRunnerAddress(address), Array.from(shardIds, (id) => id.toString())).pipe( + Effect.map((shards) => shards.map(ShardId.fromString)) + ), + release(address, shardId) { + return encoded.release(encodeRunnerAddress(address), shardId.toString()) + }, + releaseAll(address) { + return encoded.releaseAll(encodeRunnerAddress(address)) + } + }) + +/** + * @since 1.0.0 + * @category constructors + */ +export const makeMemory = Effect.gen(function*() { + const runners = MutableHashMap.empty() + let acquired: Array = [] + let id = 0 + + return RunnerStorage.of({ + getRunners: Effect.sync(() => Array.from(MutableHashMap.values(runners), (runner) => [runner, true])), + register: (runner) => + Effect.sync(() => { + MutableHashMap.set(runners, runner.address, runner) + return MachineId.make(id++) + }), + unregister: (address) => + Effect.sync(() => { + MutableHashMap.remove(runners, address) + }), + setRunnerHealth: () => Effect.void, + acquire: (_address, shardIds) => { + acquired = Array.from(shardIds) + return Effect.succeed(Array.from(shardIds)) + }, + refresh: () => Effect.sync(() => acquired), + release: () => Effect.void, + releaseAll: () => Effect.void + }) +}) + +/** + * @since 1.0.0 + * @category layers + */ +export const layerMemory: Layer.Layer = Layer.effect(RunnerStorage)(makeMemory) + +// ------------------------------------------------------------------------------------- +// internal +// ------------------------------------------------------------------------------------- + +const encodeRunnerAddress = (runnerAddress: RunnerAddress) => `${runnerAddress.host}:${runnerAddress.port}` diff --git a/repos/effect/packages/cluster/src/Runners.ts b/repos/effect/packages/cluster/src/Runners.ts new file mode 100644 index 0000000..c69b9a7 --- /dev/null +++ b/repos/effect/packages/cluster/src/Runners.ts @@ -0,0 +1,627 @@ +/** + * @since 1.0.0 + */ +import * as Rpc from "@effect/rpc/Rpc" +import * as RpcClient_ from "@effect/rpc/RpcClient" +import type { RpcClientError } from "@effect/rpc/RpcClientError" +import * as RpcGroup from "@effect/rpc/RpcGroup" +import * as RpcSchema from "@effect/rpc/RpcSchema" +import * as Cause from "effect/Cause" +import * as Context from "effect/Context" +import * as Effect from "effect/Effect" +import * as Exit from "effect/Exit" +import * as FiberRef from "effect/FiberRef" +import * as Layer from "effect/Layer" +import * as Option from "effect/Option" +import * as RcMap from "effect/RcMap" +import * as Schema from "effect/Schema" +import type { Scope } from "effect/Scope" +import type { PersistenceError } from "./ClusterError.js" +import { AlreadyProcessingMessage, EntityNotAssignedToRunner, MailboxFull, RunnerUnavailable } from "./ClusterError.js" +import { Persisted } from "./ClusterSchema.js" +import * as Envelope from "./Envelope.js" +import * as Message from "./Message.js" +import * as MessageStorage from "./MessageStorage.js" +import * as Reply from "./Reply.js" +import type { RunnerAddress } from "./RunnerAddress.js" +import { ShardingConfig } from "./ShardingConfig.js" +import * as Snowflake from "./Snowflake.js" + +/** + * @since 1.0.0 + * @category context + */ +export class Runners extends Context.Tag("@effect/cluster/Runners") Effect.Effect + + /** + * Send a message locally. + * + * This ensures that the message hits storage before being sent to the local + * entity. + */ + readonly sendLocal: ( + options: { + readonly message: Message.Outgoing + readonly send: ( + message: Message.IncomingLocal + ) => Effect.Effect< + void, + EntityNotAssignedToRunner | MailboxFull | AlreadyProcessingMessage + > + readonly simulateRemoteSerialization: boolean + } + ) => Effect.Effect< + void, + EntityNotAssignedToRunner | MailboxFull | AlreadyProcessingMessage | PersistenceError + > + + /** + * Send a message to a Runner. + */ + readonly send: ( + options: { + readonly address: RunnerAddress + readonly message: Message.Outgoing + } + ) => Effect.Effect< + void, + | EntityNotAssignedToRunner + | RunnerUnavailable + | MailboxFull + | AlreadyProcessingMessage + | PersistenceError + > + + /** + * Notify a Runner that a message is available, then read replies from storage. + */ + readonly notify: ( + options: { + readonly address: Option.Option + readonly message: Message.Outgoing + readonly discard: boolean + } + ) => Effect.Effect + + /** + * Notify the current Runner that a message is available, then read replies from + * storage. + * + * This ensures that the message hits storage before being sent to the local + * entity. + */ + readonly notifyLocal: ( + options: { + readonly message: Message.Outgoing + readonly notify: ( + options: Message.IncomingLocal + ) => Effect.Effect + readonly discard: boolean + readonly storageOnly?: boolean | undefined + } + ) => Effect.Effect + + /** + * Mark a Runner as unavailable. + */ + readonly onRunnerUnavailable: (address: RunnerAddress) => Effect.Effect +}>() {} + +/** + * @since 1.0.0 + * @category Constructors + */ +export const make: (options: Omit) => Effect.Effect< + Runners["Type"], + never, + MessageStorage.MessageStorage | Snowflake.Generator | ShardingConfig | Scope +> = Effect.fnUntraced(function*(options: Omit) { + const storage = yield* MessageStorage.MessageStorage + const runnersScope = yield* Effect.scope + const snowflakeGen = yield* Snowflake.Generator + const config = yield* ShardingConfig + + const requestIdRewrites = new Map() + + function notifyWith( + message: Message.Outgoing, + afterPersist: (message: Message.Outgoing, isDuplicate: boolean) => Effect.Effect + ): Effect.Effect { + const rpc = message.rpc as any as Rpc.AnyWithProps + const persisted = Context.get(rpc.annotations, Persisted) + if (!persisted) { + return Effect.dieMessage("Runners.notify only supports persisted messages") + } + + if (message._tag === "OutgoingEnvelope") { + const rewriteId = requestIdRewrites.get(message.envelope.requestId) + const requestId = rewriteId ?? message.envelope.requestId + const entry = storageRequests.get(requestId) + if (rewriteId) { + message = new Message.OutgoingEnvelope({ + ...message, + envelope: message.envelope.withRequestId(rewriteId) + }) + } + return storage.saveEnvelope(message).pipe( + Effect.catchTag("MalformedMessage", Effect.die), + Effect.zipRight( + entry ? Effect.zipRight(entry.latch.open, afterPersist(message, false)) : afterPersist(message, false) + ) + ) + } + + // For requests, after persisting the request, we need to check if the + // request is a duplicate. If it is, we need to resume from the last + // received reply. + // + // Otherwise, we notify the remote entity and then reply from storage. + return Effect.flatMap( + Effect.catchTag(storage.saveRequest(message), "MalformedMessage", Effect.die), + MessageStorage.SaveResult.$match({ + Success: () => afterPersist(message, false), + Duplicate: ({ lastReceivedReply, originalId }) => { + // If the last received reply is an exit, we can just return it + // as the response. + if (Option.isSome(lastReceivedReply) && lastReceivedReply.value._tag === "WithExit") { + return message.respond(lastReceivedReply.value.withRequestId(message.envelope.requestId)) + } + requestIdRewrites.set(message.envelope.requestId, originalId) + return afterPersist( + new Message.OutgoingRequest({ + ...message, + lastReceivedReply, + envelope: Envelope.makeRequest({ + ...message.envelope, + requestId: originalId + }), + respond(reply) { + if (reply._tag === "WithExit") { + requestIdRewrites.delete(message.envelope.requestId) + } + return message.respond(reply.withRequestId(message.envelope.requestId)) + } + }), + true + ) + } + }) + ) + } + + type StorageRequestEntry = { + readonly latch: Effect.Latch + doneLatch: Effect.Latch | undefined + readonly messages: Set> + replies: Array> + } + const storageRequests = new Map() + const waitingStorageRequests = new Map>() + const replyFromStorage = Effect.fnUntraced( + function*(message: Message.OutgoingRequest) { + let entry = storageRequests.get(message.envelope.requestId) + if (entry) { + entry.messages.add(message) + entry.doneLatch ??= Effect.unsafeMakeLatch(false) + return yield* entry.doneLatch.await + } else { + entry = { + latch: Effect.unsafeMakeLatch(false), + doneLatch: undefined, + replies: [], + messages: new Set([message]) + } + storageRequests.set(message.envelope.requestId, entry) + } + + while (true) { + // wait for the storage loop to notify us + entry.latch.unsafeClose() + waitingStorageRequests.set(message.envelope.requestId, message) + storageLatch.unsafeOpen() + yield* entry.latch.await + + // send the replies back + for (let i = 0; i < entry.replies.length; i++) { + const reply = entry.replies[i] + // we have reached the end + if (reply._tag === "WithExit") { + for (const message of entry.messages) { + yield* message.respond(reply) + } + entry.doneLatch?.unsafeOpen() + return + } + + entry.latch.unsafeClose() + for (const message of entry.messages) { + yield* message.respond(reply) + } + // wait for ack + yield* entry.latch.await + } + entry.replies = [] + } + }, + (effect, message) => + Effect.ensuring( + effect, + Effect.sync(() => { + const entry = storageRequests.get(message.envelope.requestId) + if (!entry || entry.messages.size > 1) { + entry?.messages.delete(message) + return + } + storageRequests.delete(message.envelope.requestId) + waitingStorageRequests.delete(message.envelope.requestId) + }) + ) + ) + + const storageLatch = Effect.unsafeMakeLatch(false) + if (storage !== MessageStorage.noop) { + yield* Effect.gen(function*() { + const foundRequests = new Set() + + while (true) { + yield* storageLatch.await + storageLatch.unsafeClose() + + const replies = yield* storage.repliesFor(waitingStorageRequests.values()).pipe( + Effect.catchAllCause((cause) => + Effect.as( + Effect.annotateLogs(Effect.logDebug(cause), { + package: "@effect/cluster", + module: "Runners", + fiber: "Read replies loop" + }), + [] + ) + ) + ) + + // put the replies into the storage requests and then open the latches + for (let i = 0; i < replies.length; i++) { + const reply = replies[i] + const entry = storageRequests.get(reply.requestId) + if (!entry) continue + entry.replies.push(reply) + waitingStorageRequests.delete(reply.requestId) + foundRequests.add(entry) + } + + foundRequests.forEach((entry) => entry.latch.unsafeOpen()) + foundRequests.clear() + } + }).pipe( + Effect.interruptible, + Effect.forkIn(runnersScope) + ) + + yield* Effect.suspend(() => { + if (waitingStorageRequests.size === 0) { + return storageLatch.await + } + return storageLatch.open + }).pipe( + Effect.delay(config.entityReplyPollInterval), + Effect.forever, + Effect.interruptible, + Effect.forkIn(runnersScope) + ) + } + + return Runners.of({ + ...options, + sendLocal(options) { + const message = options.message + if (!options.simulateRemoteSerialization) { + return options.send(Message.incomingLocalFromOutgoing(message)) + } + return Message.serialize(message).pipe( + Effect.flatMap((encoded) => Message.deserializeLocal(message, encoded)), + Effect.flatMap(options.send), + Effect.catchTag("MalformedMessage", (error) => { + if (message._tag === "OutgoingEnvelope") { + return Effect.die(error) + } + return message.respond( + new Reply.WithExit({ + id: snowflakeGen.unsafeNext(), + requestId: message.envelope.requestId, + exit: Exit.die(error) + }) + ) + }) + ) + }, + notify(options_) { + const { discard, message } = options_ + return notifyWith(message, (message, duplicate) => { + if (discard || message._tag === "OutgoingEnvelope") { + return options.notify(options_) + } else if (!duplicate && options_.address._tag === "Some") { + return Effect.catchAll( + options.send({ + address: options_.address.value, + message + }), + (_) => replyFromStorage(message) + ) + } + return options.notify(options_).pipe( + Effect.andThen(replyFromStorage(message)) + ) + }) + }, + notifyLocal(options) { + return notifyWith(options.message, (message, duplicate) => { + if (options.discard || message._tag === "OutgoingEnvelope") { + return Effect.catchTag( + options.notify(Message.incomingLocalFromOutgoing(message)), + "EntityNotAssignedToRunner", + () => Effect.void + ) + } else if (!duplicate && options.storageOnly !== true) { + return options.notify(Message.incomingLocalFromOutgoing(message)).pipe( + Effect.andThen(storage.registerReplyHandler(message)), + Effect.catchTag("EntityNotAssignedToRunner", () => replyFromStorage(message)) + ) + } + return options.notify(Message.incomingLocalFromOutgoing(message)).pipe( + Effect.catchTag("EntityNotAssignedToRunner", () => Effect.void), + Effect.andThen(replyFromStorage(message)) + ) + }) + } + }) +}) + +/** + * @since 1.0.0 + * @category No-op + */ +export const makeNoop: Effect.Effect< + Runners["Type"], + never, + MessageStorage.MessageStorage | Snowflake.Generator | ShardingConfig | Scope +> = make({ + send: ({ message }) => Effect.fail(new EntityNotAssignedToRunner({ address: message.envelope.address })), + notify: () => Effect.void, + ping: () => Effect.void, + onRunnerUnavailable: () => Effect.void +}) + +/** + * @since 1.0.0 + * @category Layers + */ +export const layerNoop: Layer.Layer< + Runners, + never, + ShardingConfig | MessageStorage.MessageStorage +> = Layer.scoped(Runners, makeNoop).pipe(Layer.provide([Snowflake.layerGenerator])) + +const rpcErrors: Schema.Union<[ + typeof EntityNotAssignedToRunner, + typeof MailboxFull, + typeof AlreadyProcessingMessage +]> = Schema.Union( + EntityNotAssignedToRunner, + MailboxFull, + AlreadyProcessingMessage +) + +/** + * @since 1.0.0 + * @category Rpcs + */ +export class Rpcs extends RpcGroup.make( + Rpc.make("Ping"), + Rpc.make("Notify", { + payload: { + envelope: Envelope.PartialEncoded + }, + success: Schema.Void, + error: Schema.Union(EntityNotAssignedToRunner, AlreadyProcessingMessage) + }), + Rpc.make("Effect", { + payload: { + request: Envelope.PartialEncodedRequest, + persisted: Schema.Boolean + }, + success: Schema.Object as Schema.Schema>, + error: rpcErrors + }), + Rpc.make("Stream", { + payload: { + request: Envelope.PartialEncodedRequest, + persisted: Schema.Boolean + }, + error: rpcErrors, + success: Schema.Object as Schema.Schema>, + stream: true + }), + Rpc.make("Envelope", { + payload: { + envelope: Schema.Union(Envelope.AckChunk, Envelope.Interrupt), + persisted: Schema.Boolean + }, + error: rpcErrors + }) +) {} + +/** + * @since 1.0.0 + * @category Rpcs + */ +export interface RpcClient extends RpcClient_.FromGroup {} + +/** + * @since 1.0.0 + * @category Rpcs + */ +export const makeRpcClient: Effect.Effect< + RpcClient, + never, + RpcClient_.Protocol | Scope +> = RpcClient_.make(Rpcs, { spanPrefix: "Runners", disableTracing: true }) + +/** + * @since 1.0.0 + * @category constructors + */ +export const makeRpc: Effect.Effect< + Runners["Type"], + never, + Scope | RpcClientProtocol | MessageStorage.MessageStorage | Snowflake.Generator | ShardingConfig +> = Effect.gen(function*() { + const makeClientProtocol = yield* RpcClientProtocol + const snowflakeGen = yield* Snowflake.Generator + + const clients = yield* RcMap.make({ + lookup: (address: RunnerAddress) => + Effect.flatMap( + makeClientProtocol(address), + (protocol) => Effect.provideService(makeRpcClient, RpcClient_.Protocol, protocol) + ), + idleTimeToLive: "3 minutes" + }) + + return yield* make({ + ping(address) { + return RcMap.get(clients, address).pipe( + Effect.flatMap((client) => client.Ping()), + Effect.catchAllCause(() => { + return Effect.zipRight( + RcMap.invalidate(clients, address), + Effect.fail(new RunnerUnavailable({ address })) + ) + }), + Effect.scoped + ) + }, + send({ address, message }) { + const rpc = message.rpc as any as Rpc.AnyWithProps + const isPersisted = Context.get(rpc.annotations, Persisted) + if (message._tag === "OutgoingEnvelope") { + return RcMap.get(clients, address).pipe( + Effect.flatMap((client) => + client.Envelope({ + envelope: message.envelope, + persisted: isPersisted + }) + ), + Effect.catchTag("RpcClientError", Effect.die), + Effect.scoped, + Effect.catchAllDefect(() => Effect.fail(new RunnerUnavailable({ address }))) + ) + } + const isStream = RpcSchema.isStreamSchema(rpc.successSchema) + if (!isStream) { + return Effect.matchEffect(Message.serializeRequest(message), { + onSuccess: (request) => + RcMap.get(clients, address).pipe( + Effect.flatMap((client) => + client.Effect({ + request, + persisted: isPersisted + }) + ), + Effect.catchTag("RpcClientError", Effect.die), + Effect.flatMap((reply) => + Schema.decode(Reply.Reply(message.rpc))(reply).pipe( + Effect.locally(FiberRef.currentContext, message.context), + Effect.orDie + ) + ), + Effect.flatMap(message.respond), + Effect.scoped, + Effect.catchAllDefect(() => Effect.fail(new RunnerUnavailable({ address }))) + ), + onFailure: (error) => + message.respond( + new Reply.WithExit({ + id: snowflakeGen.unsafeNext(), + requestId: message.envelope.requestId, + exit: Exit.die(error) + }) + ) + }) + } + return Effect.matchEffect(Message.serializeRequest(message), { + onSuccess: (request) => + RcMap.get(clients, address).pipe( + Effect.flatMap((client) => + client.Stream({ + request, + persisted: isPersisted + }, { asMailbox: true }) + ), + Effect.flatMap((mailbox) => { + const decode = Schema.decode(Reply.Reply(message.rpc)) + return mailbox.take.pipe( + Effect.flatMap((reply) => Effect.orDie(decode(reply))), + Effect.flatMap(message.respond), + Effect.forever, + Effect.catchTag("RpcClientError", Effect.die), + Effect.locally(FiberRef.currentContext, message.context), + Effect.catchIf(Cause.isNoSuchElementException, () => Effect.void), + Effect.catchAllDefect(() => Effect.fail(new RunnerUnavailable({ address }))) + ) + }), + Effect.scoped + ), + onFailure: (error) => + message.respond( + new Reply.WithExit({ + id: snowflakeGen.unsafeNext(), + requestId: message.envelope.requestId, + exit: Exit.die(error) + }) + ) + }) + }, + notify({ address, message }) { + if (Option.isNone(address)) { + return Effect.void + } + const encode: Effect.Effect = + message._tag === "OutgoingRequest" + ? Effect.orDie(Message.serializeRequest(message)) + : Effect.succeed(message.envelope) + return Effect.flatMap(encode, (envelope) => + RcMap.get(clients, address.value).pipe( + Effect.flatMap((client) => client.Notify({ envelope })), + Effect.scoped, + Effect.ignore + )) + }, + onRunnerUnavailable: (address) => RcMap.invalidate(clients, address) + }) +}) + +/** + * @since 1.0.0 + * @category Layers + */ +export const layerRpc: Layer.Layer< + Runners, + never, + MessageStorage.MessageStorage | RpcClientProtocol | ShardingConfig +> = Layer.scoped(Runners, makeRpc).pipe( + Layer.provide(Snowflake.layerGenerator) +) + +/** + * @since 1.0.0 + * @category Client + */ +export class RpcClientProtocol extends Context.Tag("@effect/cluster/Runners/RpcClientProtocol")< + RpcClientProtocol, + (address: RunnerAddress) => Effect.Effect +>() {} diff --git a/repos/effect/packages/cluster/src/ShardId.ts b/repos/effect/packages/cluster/src/ShardId.ts new file mode 100644 index 0000000..b53f825 --- /dev/null +++ b/repos/effect/packages/cluster/src/ShardId.ts @@ -0,0 +1,108 @@ +/** + * @since 1.0.0 + */ +import * as Equal from "effect/Equal" +import * as Hash from "effect/Hash" +import * as S from "effect/Schema" + +/** + * @since 1.0.0 + * @category Symbols + */ +export const TypeId: unique symbol = Symbol.for("@effect/cluster/ShardId") + +/** + * @since 1.0.0 + * @category Symbols + */ +export type TypeId = typeof TypeId + +const constDisableValidation = { disableValidation: true } + +/** + * @since 1.0.0 + * @category Constructors + */ +export const make = (group: string, id: number): ShardId => { + const key = `${group}:${id}` + let shardId = shardIdCache.get(key) + if (!shardId) { + shardId = new ShardId({ group, id }, constDisableValidation) + shardIdCache.set(key, shardId) + } + return shardId +} + +const shardIdCache = new Map() + +/** + * @since 1.0.0 + * @category Models + */ +export class ShardId extends S.Class("@effect/cluster/ShardId")({ + group: S.String, + id: S.Int +}) { + /** + * @since 1.0.0 + */ + readonly [TypeId]: TypeId = TypeId; + + /** + * @since 1.0.0 + */ + [Equal.symbol](that: ShardId): boolean { + return this.group === that.group && this.id === that.id + } + + /** + * @since 1.0.0 + */ + [Hash.symbol](): number { + return Hash.cached(this, Hash.string(this.toString())) + } + + /** + * @since 1.0.0 + */ + toString(): string { + return `${this.group}:${this.id}` + } + + /** + * @since 1.0.0 + */ + static toString(shardId: { + readonly group: string + readonly id: number + }): string { + return `${shardId.group}:${shardId.id}` + } + + /** + * @since 1.0.0 + */ + static fromStringEncoded(s: string): { + readonly group: string + readonly id: number + } { + const index = s.lastIndexOf(":") + if (index === -1) { + throw new Error(`Invalid ShardId format`) + } + const group = s.substring(0, index) + const id = Number(s.substring(index + 1)) + if (isNaN(id)) { + throw new Error(`ShardId id must be a number`) + } + return { group, id } + } + + /** + * @since 4.0.0 + */ + static fromString(s: string): ShardId { + const encoded = ShardId.fromStringEncoded(s) + return make(encoded.group, encoded.id) + } +} diff --git a/repos/effect/packages/cluster/src/Sharding.ts b/repos/effect/packages/cluster/src/Sharding.ts new file mode 100644 index 0000000..129637f --- /dev/null +++ b/repos/effect/packages/cluster/src/Sharding.ts @@ -0,0 +1,1444 @@ +/** + * @since 1.0.0 + */ +import type * as Rpc from "@effect/rpc/Rpc" +import * as RpcClient from "@effect/rpc/RpcClient" +import { type FromServer, RequestId } from "@effect/rpc/RpcMessage" +import * as Arr from "effect/Array" +import * as Cause from "effect/Cause" +import * as Context from "effect/Context" +import type { DurationInput } from "effect/Duration" +import * as Duration from "effect/Duration" +import * as Effect from "effect/Effect" +import * as Either from "effect/Either" +import * as Equal from "effect/Equal" +import type * as Exit from "effect/Exit" +import * as Fiber from "effect/Fiber" +import * as FiberMap from "effect/FiberMap" +import * as FiberRef from "effect/FiberRef" +import * as FiberSet from "effect/FiberSet" +import { constant } from "effect/Function" +import * as HashMap from "effect/HashMap" +import * as HashRing from "effect/HashRing" +import * as Layer from "effect/Layer" +import * as MutableHashMap from "effect/MutableHashMap" +import * as MutableHashSet from "effect/MutableHashSet" +import * as MutableRef from "effect/MutableRef" +import * as Option from "effect/Option" +import * as PubSub from "effect/PubSub" +import * as Schedule from "effect/Schedule" +import * as Scope from "effect/Scope" +import * as Stream from "effect/Stream" +import type { MailboxFull, PersistenceError } from "./ClusterError.js" +import { AlreadyProcessingMessage, EntityNotAssignedToRunner } from "./ClusterError.js" +import * as ClusterMetrics from "./ClusterMetrics.js" +import { Persisted, Uninterruptible } from "./ClusterSchema.js" +import * as ClusterSchema from "./ClusterSchema.js" +import type { CurrentAddress, CurrentRunnerAddress, Entity, HandlersFrom } from "./Entity.js" +import type { EntityAddress } from "./EntityAddress.js" +import { make as makeEntityAddress } from "./EntityAddress.js" +import type { EntityId } from "./EntityId.js" +import { make as makeEntityId } from "./EntityId.js" +import * as Envelope from "./Envelope.js" +import * as EntityManager from "./internal/entityManager.js" +import { EntityReaper } from "./internal/entityReaper.js" +import { joinAllDiscard } from "./internal/fiber.js" +import { hashString } from "./internal/hash.js" +import { internalInterruptors } from "./internal/interruptors.js" +import { ResourceMap } from "./internal/resourceMap.js" +import * as Message from "./Message.js" +import * as MessageStorage from "./MessageStorage.js" +import * as Reply from "./Reply.js" +import { Runner } from "./Runner.js" +import type { RunnerAddress } from "./RunnerAddress.js" +import * as RunnerHealth from "./RunnerHealth.js" +import { Runners } from "./Runners.js" +import { RunnerStorage } from "./RunnerStorage.js" +import type { ShardId } from "./ShardId.js" +import { make as makeShardId } from "./ShardId.js" +import { ShardingConfig } from "./ShardingConfig.js" +import { EntityRegistered, type ShardingRegistrationEvent, SingletonRegistered } from "./ShardingRegistrationEvent.js" +import { SingletonAddress } from "./SingletonAddress.js" +import * as Snowflake from "./Snowflake.js" + +/** + * @since 1.0.0 + * @category models + */ +export class Sharding extends Context.Tag("@effect/cluster/Sharding") + + /** + * Returns the `ShardId` of the shard to which the entity at the specified + * `address` is assigned. + */ + readonly getShardId: (entityId: EntityId, group: string) => ShardId + + /** + * Returns `true` if the specified `shardId` is assigned to this runner. + */ + readonly hasShardId: (shardId: ShardId) => boolean + + /** + * Generate a Snowflake ID that is unique to this runner. + */ + readonly getSnowflake: Effect.Effect + + /** + * Returns `true` if sharding is shutting down, `false` otherwise. + */ + readonly isShutdown: Effect.Effect + + /** + * Constructs a `RpcClient` which can be used to send messages to the + * specified `Entity`. + */ + readonly makeClient: ( + entity: Entity + ) => Effect.Effect< + ( + entityId: string + ) => RpcClient.RpcClient.From< + Rpcs, + MailboxFull | AlreadyProcessingMessage | PersistenceError + > + > + + /** + * Registers a new entity with the runner. + */ + readonly registerEntity: , RX>( + entity: Entity, + handlers: Effect.Effect, + options?: { + readonly maxIdleTime?: DurationInput | undefined + readonly concurrency?: number | "unbounded" | undefined + readonly mailboxCapacity?: number | "unbounded" | undefined + readonly disableFatalDefects?: boolean | undefined + readonly defectRetryPolicy?: Schedule.Schedule | undefined + readonly spanAttributes?: Record | undefined + } + ) => Effect.Effect< + void, + never, + | Scope.Scope + | Rpc.Context + | Rpc.Middleware + | Exclude + > + + /** + * Registers a new singleton with the runner. + */ + readonly registerSingleton: ( + name: string, + run: Effect.Effect, + options?: { + readonly shardGroup?: string | undefined + } + ) => Effect.Effect + + /** + * Sends a message to the specified entity. + */ + readonly send: (message: Message.Incoming) => Effect.Effect< + void, + EntityNotAssignedToRunner | MailboxFull | AlreadyProcessingMessage + > + + /** + * Sends an outgoing message + */ + readonly sendOutgoing: ( + message: Message.Outgoing, + discard: boolean + ) => Effect.Effect< + void, + MailboxFull | AlreadyProcessingMessage | PersistenceError + > + + /** + * Notify sharding that a message has been persisted to storage. + */ + readonly notify: (message: Message.Incoming, options?: { + readonly waitUntilRead?: boolean | undefined + }) => Effect.Effect< + void, + EntityNotAssignedToRunner | AlreadyProcessingMessage + > + + /** + * Reset the state of a message + */ + readonly reset: (requestId: Snowflake.Snowflake) => Effect.Effect + + /** + * Trigger a storage read, which will read all unprocessed messages. + */ + readonly pollStorage: Effect.Effect + + /** + * Retrieves the active entity count for the current runner. + */ + readonly activeEntityCount: Effect.Effect +}>() {} + +// ----------------------------------------------------------------------------- +// Implementation +// ----------------------------------------------------------------------------- + +interface EntityManagerState { + readonly entity: Entity + readonly manager: EntityManager.EntityManager + status: "alive" | "closing" | "closed" +} + +const make = Effect.gen(function*() { + const config = yield* ShardingConfig + const clock = yield* Effect.clock + + const runnersService = yield* Runners + const runnerHealth = yield* RunnerHealth.RunnerHealth + const snowflakeGen = yield* Snowflake.Generator + const shardingScope = yield* Effect.scope + const isShutdown = MutableRef.make(false) + const fiberSet = yield* FiberSet.make() + const runFork = yield* FiberSet.runtime(fiberSet)().pipe( + Effect.mapInputContext((context: Context.Context) => Context.omit(Scope.Scope)(context)) + ) + + const storage = yield* MessageStorage.MessageStorage + const storageEnabled = storage !== MessageStorage.noop + const runnerStorage = yield* RunnerStorage + + const entityManagers = new Map() + + const shardAssignments = MutableHashMap.empty() + const selfShards = MutableHashSet.empty() + + // the active shards are the ones that we have acquired the lock for + const acquiredShards = MutableHashSet.empty() + const activeShardsLatch = yield* Effect.makeLatch(false) + + const events = yield* PubSub.unbounded() + const getRegistrationEvents: Stream.Stream = Stream.fromPubSub(events) + + const isLocalRunner = (address: RunnerAddress) => + Option.isSome(config.runnerAddress) && Equal.equals(address, config.runnerAddress.value) + + function getShardId(entityId: EntityId, group: string): ShardId { + const id = Math.abs(hashString(entityId) % config.shardsPerGroup) + 1 + return makeShardId(group, id) + } + + function isEntityOnLocalShards(address: EntityAddress): boolean { + return MutableHashSet.has(acquiredShards, address.shardId) + } + + yield* Scope.addFinalizer( + shardingScope, + Effect.logDebug("Shutdown complete").pipe(Effect.annotateLogs({ + package: "@effect/cluster", + module: "Sharding" + })) + ) + + // --- Shard acquisition --- + // + // Responsible for acquiring and releasing shards from RunnerStorage. + // + // This should be shutdown last, when all entities have been shutdown, to + // allow them to move to another runner. + + const releasingShards = MutableHashSet.empty() + if (Option.isSome(config.runnerAddress)) { + const selfAddress = config.runnerAddress.value + yield* Scope.addFinalizerExit(shardingScope, () => { + // the locks expire over time, so if this fails we ignore it + return Effect.ignore(runnerStorage.releaseAll(selfAddress)) + }) + + const releaseShardsMap = yield* FiberMap.make() + const releaseShard = Effect.fnUntraced( + function*(shardId: ShardId) { + const fibers = Arr.empty>() + for (const state of entityManagers.values()) { + if (state.status === "closed") continue + fibers.push(yield* Effect.fork(state.manager.interruptShard(shardId))) + } + yield* joinAllDiscard(fibers) + yield* runnerStorage.release(selfAddress, shardId) + MutableHashSet.remove(releasingShards, shardId) + yield* storage.unregisterShardReplyHandlers(shardId) + }, + Effect.sandbox, + (effect, shardId) => + effect.pipe( + Effect.tapError((cause) => + Effect.logDebug(`Could not release shard, retrying`, cause).pipe( + Effect.annotateLogs({ + package: "@effect/cluster", + module: "Sharding", + fiber: "releaseShard", + runner: selfAddress, + shardId + }) + ) + ), + Effect.eventually, + FiberMap.run(releaseShardsMap, shardId, { onlyIfMissing: true }) + ) + ) + const releaseShards = Effect.gen(function*() { + for (const shardId of releasingShards) { + if (FiberMap.unsafeHas(releaseShardsMap, shardId)) continue + yield* releaseShard(shardId) + } + }) + + yield* Effect.gen(function*() { + activeShardsLatch.unsafeOpen() + + while (true) { + yield* activeShardsLatch.await + activeShardsLatch.unsafeClose() + + // if a shard is no longer assigned to this runner, we release it + for (const shardId of acquiredShards) { + if (MutableHashSet.has(selfShards, shardId)) continue + MutableHashSet.remove(acquiredShards, shardId) + MutableHashSet.add(releasingShards, shardId) + } + + if (MutableHashSet.size(releasingShards) > 0) { + yield* Effect.forkIn(syncSingletons, shardingScope) + yield* releaseShards + } + + // if a shard has been assigned to this runner, we acquire it + const unacquiredShards = MutableHashSet.empty() + for (const shardId of selfShards) { + if (MutableHashSet.has(acquiredShards, shardId) || MutableHashSet.has(releasingShards, shardId)) continue + MutableHashSet.add(unacquiredShards, shardId) + } + + if (MutableHashSet.size(unacquiredShards) === 0) { + continue + } + + const oacquired = yield* runnerStorage.acquire(selfAddress, unacquiredShards).pipe( + Effect.timeoutOption(config.shardLockRefreshInterval) + ) + if (Option.isNone(oacquired)) { + activeShardsLatch.unsafeOpen() + continue + } + + const acquired = oacquired.value + yield* storage.resetShards(acquired).pipe( + Effect.ignore, + Effect.timeoutOption(config.shardLockRefreshInterval) + ) + for (const shardId of acquired) { + if (MutableHashSet.has(releasingShards, shardId) || !MutableHashSet.has(selfShards, shardId)) { + continue + } + MutableHashSet.add(acquiredShards, shardId) + } + if (acquired.length > 0) { + yield* storageReadLatch.open + yield* Effect.forkIn(syncSingletons, shardingScope) + + // update metrics + ClusterMetrics.shards.unsafeUpdate(BigInt(MutableHashSet.size(acquiredShards)), []) + } + yield* Effect.sleep(1000) + activeShardsLatch.unsafeOpen() + } + }).pipe( + Effect.catchAllCause((cause) => Effect.logWarning("Could not acquire/release shards", cause)), + Effect.repeat(Schedule.spaced(config.entityMessagePollInterval)), + Effect.annotateLogs({ + package: "@effect/cluster", + module: "Sharding", + fiber: "Shard acquisition loop", + runner: selfAddress + }), + Effect.forkIn(shardingScope), + Effect.interruptible + ) + + // refresh the shard locks every `shardLockRefreshInterval` + yield* Effect.suspend(() => + runnerStorage.refresh(selfAddress, [ + ...acquiredShards, + ...releasingShards + ]) + ).pipe( + Effect.flatMap((acquired) => { + for (const shardId of acquiredShards) { + if (!acquired.includes(shardId)) { + MutableHashSet.remove(acquiredShards, shardId) + MutableHashSet.add(releasingShards, shardId) + } + } + for (let i = 0; i < acquired.length; i++) { + const shardId = acquired[i] + if (!MutableHashSet.has(selfShards, shardId)) { + MutableHashSet.remove(acquiredShards, shardId) + MutableHashSet.add(releasingShards, shardId) + } + } + return MutableHashSet.size(releasingShards) > 0 + ? activeShardsLatch.open + : Effect.void + }), + Effect.retry({ + times: 5, + schedule: Schedule.spaced(50) + }), + Effect.catchAllCause((cause) => + Effect.logError("Could not refresh shard locks", cause).pipe( + Effect.andThen(clearSelfShards) + ) + ), + Effect.repeat(Schedule.fixed(config.shardLockRefreshInterval)), + Effect.forever, + Effect.forkIn(shardingScope), + Effect.interruptible + ) + + // open the shard latch every poll interval + yield* activeShardsLatch.open.pipe( + Effect.delay(config.entityMessagePollInterval), + Effect.forever, + Effect.forkIn(shardingScope), + Effect.interruptible + ) + } + + const clearSelfShards = Effect.sync(() => { + MutableHashSet.clear(selfShards) + activeShardsLatch.unsafeOpen() + }) + + // --- Storage inbox --- + // + // Responsible for reading unprocessed messages from storage and sending them + // to the appropriate entity manager. + // + // This should be shutdown before shard acquisition, to ensure no messages are + // being processed before the shards are released. + // + // It should also be shutdown after the entity managers, to ensure interrupt + // & ack envelopes can still be processed. + + const storageReadLatch = yield* Effect.makeLatch(true) + const openStorageReadLatch = constant(storageReadLatch.open) + + const storageReadLock = Effect.unsafeMakeSemaphore(1) + const withStorageReadLock = storageReadLock.withPermits(1) + + if (storageEnabled && Option.isSome(config.runnerAddress)) { + const selfAddress = config.runnerAddress.value + const entityRegistrationTimeoutMillis = Duration.toMillis(config.entityRegistrationTimeout) + const storageStartMillis = clock.unsafeCurrentTimeMillis() + + yield* Effect.gen(function*() { + yield* Effect.logDebug("Starting") + yield* Effect.addFinalizer(() => Effect.logDebug("Shutting down")) + + let index = 0 + let messages: Array> = [] + const removableNotifications = new Set() + const resetAddresses = MutableHashSet.empty() + + const processMessages = Effect.whileLoop({ + while: () => index < messages.length, + step: () => index++, + body: () => send + }) + + const send = Effect.catchAllCause( + Effect.suspend(() => { + const message = messages[index] + const address = message.envelope.address + if (!MutableHashSet.has(acquiredShards, address.shardId)) { + return Effect.void + } + const state = entityManagers.get(address.entityType) + if (!state) { + const sinceStart = clock.unsafeCurrentTimeMillis() - storageStartMillis + if (sinceStart < entityRegistrationTimeoutMillis) { + // reset address in the case that the entity is slow to register + MutableHashSet.add(resetAddresses, address) + return Effect.void + } + // if the entity did not register in time, we save a defect reply + return Effect.die(new Error(`Entity type '${address.entityType}' not registered`)) + } else if (state.status === "closed") { + return Effect.void + } + + const isProcessing = state.manager.isProcessingFor(message) + + if (message._tag === "IncomingEnvelope" && isProcessing) { + // If the message might affect a currently processing request, we + // send it to the entity manager to be processed. + return state.manager.send(message) + } else if (isProcessing || state.status === "closing") { + // If the request is already processing, we skip it. + // Or if the entity is closing, we skip all incoming messages. + return Effect.void + } else if (message._tag === "IncomingRequest" && pendingNotifications.has(message.envelope.requestId)) { + const entry = pendingNotifications.get(message.envelope.requestId)! + pendingNotifications.delete(message.envelope.requestId) + removableNotifications.delete(entry) + entry.resume(Effect.void) + } + + // If the entity was resuming in another fiber, we add the message + // id to the unprocessed set. + const resumptionState = MutableHashMap.get(entityResumptionState, address) + if (Option.isSome(resumptionState)) { + resumptionState.value.unprocessed.add(message.envelope.requestId) + if (message.envelope._tag === "Interrupt") { + resumptionState.value.interrupts.set(message.envelope.requestId, message as Message.IncomingEnvelope) + } + return Effect.void + } + return state.manager.send(message) + }), + (cause) => { + const message = messages[index] + const error = Cause.failureOrCause(cause) + // if we get a defect, then update storage + if (Either.isRight(error)) { + if (Cause.isInterrupted(cause)) { + return Effect.void + } + return Effect.ignore(storage.saveReply(Reply.ReplyWithContext.fromDefect({ + id: snowflakeGen.unsafeNext(), + requestId: message.envelope.requestId, + defect: Cause.squash(cause) + }))) + } + if (error.left._tag === "MailboxFull") { + // MailboxFull can only happen for requests, so this cast is safe + return resumeEntityFromStorage(message as Message.IncomingRequest) + } + return Effect.void + } + ) + + while (true) { + // wait for the next poll interval, or if we get notified of a change + yield* storageReadLatch.await + + // if we get notified of a change, ensure we start a read immediately + // next iteration + storageReadLatch.unsafeClose() + + // the lock is used to ensure resuming entities have a garantee that no + // more items are added to the unprocessed set while the semaphore is + // acquired. + yield* storageReadLock.take(1) + + entityManagers.forEach((state) => state.manager.clearProcessed()) + if (pendingNotifications.size > 0) { + pendingNotifications.forEach((entry) => removableNotifications.add(entry)) + } + + messages = yield* storage.unprocessedMessages(acquiredShards) + index = 0 + yield* processMessages + + if (removableNotifications.size > 0) { + removableNotifications.forEach(({ message, resume }) => { + pendingNotifications.delete(message.envelope.requestId) + resume(Effect.fail(new EntityNotAssignedToRunner({ address: message.envelope.address }))) + }) + removableNotifications.clear() + } + if (MutableHashSet.size(resetAddresses) > 0) { + for (const address of resetAddresses) { + yield* Effect.logWarning("Could not find entity manager for address, retrying").pipe( + Effect.annotateLogs({ address }) + ) + yield* Effect.forkIn(storage.resetAddress(address), shardingScope) + } + MutableHashSet.clear(resetAddresses) + } + + // let the resuming entities check if they are done + yield* storageReadLock.release(1) + } + }).pipe( + Effect.scoped, + Effect.ensuring(storageReadLock.releaseAll), + Effect.catchAllCause((cause) => Effect.logWarning("Could not read messages from storage", cause)), + Effect.forever, + Effect.annotateLogs({ + package: "@effect/cluster", + module: "Sharding", + fiber: "Storage read loop", + runner: selfAddress + }), + Effect.withUnhandledErrorLogLevel(Option.none()), + Effect.forkIn(shardingScope), + Effect.interruptible + ) + + // open the storage latch every poll interval + yield* storageReadLatch.open.pipe( + Effect.delay(config.entityMessagePollInterval), + Effect.forever, + Effect.forkIn(shardingScope), + Effect.interruptible + ) + + // Resume unprocessed messages for entities that reached a full mailbox. + const entityResumptionState = MutableHashMap.empty + interrupts: Map + }>() + const resumeEntityFromStorage = (lastReceivedMessage: Message.IncomingRequest) => { + const address = lastReceivedMessage.envelope.address + const resumptionState = MutableHashMap.get(entityResumptionState, address) + if (Option.isSome(resumptionState)) { + resumptionState.value.unprocessed.add(lastReceivedMessage.envelope.requestId) + return Effect.void + } + MutableHashMap.set(entityResumptionState, address, { + unprocessed: new Set([lastReceivedMessage.envelope.requestId]), + interrupts: new Map() + }) + return resumeEntityFromStorageImpl(address) + } + const resumeEntityFromStorageImpl = Effect.fnUntraced( + function*(address: EntityAddress) { + const state = entityManagers.get(address.entityType) + if (!state) { + MutableHashMap.remove(entityResumptionState, address) + return + } + + const resumptionState = Option.getOrThrow(MutableHashMap.get(entityResumptionState, address)) + let done = false + + while (!done) { + // if the shard is no longer assigned to this runner, we stop + if (!MutableHashSet.has(acquiredShards, address.shardId)) { + return + } + + // take a batch of unprocessed messages ids + const messageIds = Arr.empty() + for (const id of resumptionState.unprocessed) { + if (messageIds.length === 1024) break + messageIds.push(id) + } + + const messages = yield* storage.unprocessedMessagesById(messageIds) + + // this should not happen, but we handle it just in case + if (messages.length === 0) { + yield* Effect.sleep(config.entityMessagePollInterval) + continue + } + + let index = 0 + + const sendWithRetry: Effect.Effect< + void, + EntityNotAssignedToRunner + > = Effect.catchTags( + Effect.suspend(() => { + if (!MutableHashSet.has(acquiredShards, address.shardId)) { + return Effect.fail(new EntityNotAssignedToRunner({ address })) + } + + const message = messages[index] + // check if this is a request that was interrupted + const interrupt = message._tag === "IncomingRequest" && + resumptionState.interrupts.get(message.envelope.requestId) + return interrupt ? + Effect.flatMap(state.manager.send(message), () => { + resumptionState.interrupts.delete(message.envelope.requestId) + return state.manager.send(interrupt) + }) : + state.manager.send(message) + }), + { + MailboxFull: () => Effect.delay(sendWithRetry, config.sendRetryInterval), + AlreadyProcessingMessage: () => Effect.void + } + ) + + yield* Effect.whileLoop({ + while: () => index < messages.length, + body: constant(sendWithRetry), + step: () => index++ + }) + + for (const id of messageIds) { + resumptionState.unprocessed.delete(id) + } + if (resumptionState.unprocessed.size > 0) continue + + // if we have caught up to the main storage loop, we let it take over + yield* withStorageReadLock(Effect.sync(() => { + if (resumptionState.unprocessed.size === 0) { + MutableHashMap.remove(entityResumptionState, address) + done = true + } + })) + } + }, + Effect.retry({ + while: (e) => e._tag === "PersistenceError", + schedule: Schedule.spaced(config.entityMessagePollInterval) + }), + Effect.catchAllCause((cause) => Effect.logDebug("Could not resume unprocessed messages", cause)), + (effect, address) => + Effect.annotateLogs(effect, { + package: "@effect/cluster", + module: "Sharding", + fiber: "Resuming unprocessed messages", + runner: selfAddress, + entity: address + }), + (effect, address) => + Effect.ensuring( + effect, + Effect.sync(() => MutableHashMap.remove(entityResumptionState, address)) + ), + Effect.withUnhandledErrorLogLevel(Option.none()), + Effect.forkIn(shardingScope), + Effect.interruptible + ) + } + + // --- Sending messages --- + + const sendLocal = | Message.Incoming>(message: M) => + Effect.suspend(function loop(): Effect.Effect< + void, + | EntityNotAssignedToRunner + | MailboxFull + | AlreadyProcessingMessage + | (M extends Message.Incoming ? never : PersistenceError) + > { + const address = message.envelope.address + if (!isEntityOnLocalShards(address)) { + return Effect.fail(new EntityNotAssignedToRunner({ address })) + } + const state = entityManagers.get(address.entityType) + if (!state) { + return Effect.flatMap(waitForEntityManager(address.entityType), loop) + } else if (state.status === "closed" || (state.status === "closing" && message._tag === "IncomingRequest")) { + // if we are shutting down, we don't accept new requests + return Effect.fail(new EntityNotAssignedToRunner({ address })) + } + + return message._tag === "IncomingRequest" || message._tag === "IncomingEnvelope" ? + state.manager.send(message) : + runnersService.sendLocal({ + message, + send: state.manager.sendLocal, + simulateRemoteSerialization: config.simulateRemoteSerialization + }) as any + }) + + type PendingNotification = { + resume: (_: Effect.Effect) => void + readonly message: Message.IncomingRequest + } + const pendingNotifications = new Map() + const notifyLocal = | Message.Incoming>( + message: M, + discard: boolean, + options?: { + readonly waitUntilRead?: boolean | undefined + } + ) => + Effect.suspend(function loop(): Effect.Effect< + void, + | EntityNotAssignedToRunner + | AlreadyProcessingMessage + | (M extends Message.Incoming ? never : PersistenceError) + > { + const address = message.envelope.address + const state = entityManagers.get(address.entityType) + if (!state) { + return Effect.flatMap(waitForEntityManager(address.entityType), loop) + } else if (state.status === "closed" || !isEntityOnLocalShards(address)) { + return Effect.fail(new EntityNotAssignedToRunner({ address })) + } + + const isLocal = isEntityOnLocalShards(address) + const notify = storageEnabled + ? openStorageReadLatch + : () => Effect.die("Sharding.notifyLocal: storage is disabled") + + if (message._tag === "IncomingRequest" || message._tag === "IncomingEnvelope") { + if (!isLocal) { + return Effect.fail(new EntityNotAssignedToRunner({ address })) + } else if ( + message._tag === "IncomingRequest" && state.manager.isProcessingFor(message, { excludeReplies: true }) + ) { + return Effect.fail(new AlreadyProcessingMessage({ address, envelopeId: message.envelope.requestId })) + } else if (message._tag === "IncomingRequest" && options?.waitUntilRead) { + if (!storageEnabled) return notify() + return Effect.async((resume) => { + let entry = pendingNotifications.get(message.envelope.requestId) + if (entry) { + const prevResume = entry.resume + entry.resume = (effect) => { + prevResume(effect) + resume(effect) + } + return + } + entry = { resume, message } + pendingNotifications.set(message.envelope.requestId, entry) + storageReadLatch.unsafeOpen() + }) + } + return notify() + } + + return runnersService.notifyLocal({ message, notify, discard, storageOnly: !isLocal }) as any + }) + + function sendOutgoing( + message: Message.Outgoing, + discard: boolean, + retries?: number + ): Effect.Effect< + void, + MailboxFull | AlreadyProcessingMessage | PersistenceError + > { + return Effect.catchIf( + Effect.suspend(() => { + const address = message.envelope.address + const isPersisted = Context.get(message.rpc.annotations, Persisted) + if (isPersisted && !storageEnabled) { + return Effect.die("Sharding.sendOutgoing: Persisted messages require MessageStorage") + } + const maybeRunner = MutableHashMap.get(shardAssignments, address.shardId) + const runnerIsLocal = Option.isSome(maybeRunner) && isLocalRunner(maybeRunner.value) + if (isPersisted) { + return runnerIsLocal + ? notifyLocal(message, discard) + : runnersService.notify({ address: maybeRunner, message, discard }) + } else if (Option.isNone(maybeRunner)) { + return Effect.fail(new EntityNotAssignedToRunner({ address })) + } + return runnerIsLocal + ? sendLocal(message) + : runnersService.send({ address: maybeRunner.value, message }) + }), + (error) => error._tag === "EntityNotAssignedToRunner" || error._tag === "RunnerUnavailable", + (error) => { + if (retries === 0) { + return Effect.die(error) + } + return Effect.delay(sendOutgoing(message, discard, retries && retries - 1), config.sendRetryInterval) + } + ) + } + + const reset: Sharding["Type"]["reset"] = (requestId) => + Effect.matchCause(storage.clearReplies(requestId), { + onSuccess: () => true, + onFailure: () => false + }) + + // --- RunnerStorage sync --- + // + // This is responsible for syncing the local view of runners and shard + // assignments with RunnerStorage. + // + // It should be shutdown after the clients, so that they can still get correct + // shard assignments for outgoing messages (they could still be in use by + // entities that are shutting down). + + const selfRunner = Option.isSome(config.runnerAddress) ? + new Runner({ + address: config.runnerAddress.value, + groups: config.shardGroups, + weight: config.runnerShardWeight + }) : + undefined + + let allRunners = MutableHashMap.empty() + let healthyRunnerCount = 0 + + // update metrics + if (selfRunner) { + ClusterMetrics.runners.unsafeUpdate(BigInt(1), []) + ClusterMetrics.runnersHealthy.unsafeUpdate(BigInt(1), []) + } + + yield* Effect.gen(function*() { + const hashRings = new Map>() + let nextRunners = MutableHashMap.empty() + const healthyRunners = MutableHashSet.empty() + const withTimeout = Effect.timeout(Duration.seconds(5)) + + while (true) { + // Ensure the current runner is registered + if (selfRunner && !isShutdown.current && !MutableHashMap.has(allRunners, selfRunner)) { + yield* Effect.logDebug("Registering runner", selfRunner) + const machineId = yield* withTimeout(runnerStorage.register(selfRunner, true)) + yield* snowflakeGen.setMachineId(machineId) + } + + const runners = yield* withTimeout(runnerStorage.getRunners) + let changed = false + for (let i = 0; i < runners.length; i++) { + const [runner, healthy] = runners[i] + MutableHashMap.set(nextRunners, runner, healthy) + const wasHealthy = MutableHashSet.has(healthyRunners, runner) + if (!healthy || wasHealthy) { + if (healthy === wasHealthy || !wasHealthy) { + // no change + MutableHashMap.remove(allRunners, runner) + } + continue + } + changed = true + MutableHashSet.add(healthyRunners, runner) + MutableHashMap.remove(allRunners, runner) + for (let j = 0; j < runner.groups.length; j++) { + const group = runner.groups[j] + let ring = hashRings.get(group) + if (!ring) { + ring = HashRing.make() + hashRings.set(group, ring) + } + HashRing.add(ring, runner.address, { weight: runner.weight }) + } + } + + // Remove runners that are no longer present or healthy + MutableHashMap.forEach(allRunners, (_, runner) => { + changed = true + MutableHashMap.remove(allRunners, runner) + MutableHashSet.remove(healthyRunners, runner) + runFork(runnersService.onRunnerUnavailable(runner.address)) + for (let i = 0; i < runner.groups.length; i++) { + HashRing.remove(hashRings.get(runner.groups[i])!, runner.address) + } + }) + + // swap allRunners and nextRunners + const prevRunners = allRunners + allRunners = nextRunners + nextRunners = prevRunners + healthyRunnerCount = MutableHashSet.size(healthyRunners) + + // Ensure the current runner is registered + if (selfRunner && !isShutdown.current && !MutableHashMap.has(allRunners, selfRunner)) { + continue + } + + // Recompute shard assignments if the set of healthy runners has changed. + if (changed) { + MutableHashSet.clear(selfShards) + hashRings.forEach((ring, group) => { + const newAssignments = HashRing.getShards(ring, config.shardsPerGroup) + for (let i = 0; i < config.shardsPerGroup; i++) { + const shard = makeShardId(group, i + 1) + if (newAssignments) { + const runner = newAssignments[i] + MutableHashMap.set(shardAssignments, shard, runner) + if (isLocalRunner(runner)) { + MutableHashSet.add(selfShards, shard) + } + } else { + MutableHashMap.remove(shardAssignments, shard) + } + } + }) + yield* Effect.logDebug("New shard assignments", selfShards) + activeShardsLatch.unsafeOpen() + + // update metrics + if (selfRunner) { + ClusterMetrics.runnersHealthy.unsafeUpdate( + BigInt(MutableHashSet.has(healthyRunners, selfRunner) ? 1 : 0), + [] + ) + } + } + + if (selfRunner && MutableHashSet.size(healthyRunners) === 0) { + yield* Effect.logWarning("No healthy runners available") + // to prevent a deadlock, we will mark the current node as healthy to + // start the health check singleton again + yield* withTimeout(runnerStorage.setRunnerHealth(selfRunner.address, true)) + } + + yield* Effect.sleep(config.refreshAssignmentsInterval) + } + }).pipe( + Effect.catchAllCause((cause) => Effect.logDebug(cause)), + Effect.repeat(Schedule.spaced(1000)), + Effect.annotateLogs({ + package: "@effect/cluster", + module: "Sharding", + fiber: "RunnerStorage sync", + runner: config.runnerAddress + }), + Effect.forkIn(shardingScope), + Effect.interruptible + ) + + // --- Clients --- + + type ClientRequestEntry = { + readonly rpc: Rpc.AnyWithProps + readonly services: Context.Context + lastChunkId?: Snowflake.Snowflake + } + const clientRequests = new Map() + + const clients: ResourceMap< + Entity, + (entityId: string) => RpcClient.RpcClient< + any, + MailboxFull | AlreadyProcessingMessage + >, + never + > = yield* ResourceMap.make(Effect.fnUntraced(function*(entity: Entity) { + const client = yield* RpcClient.makeNoSerialization(entity.protocol, { + spanPrefix: `${entity.type}.client`, + disableTracing: !Context.get(entity.protocol.annotations, ClusterSchema.ClientTracingEnabled), + supportsAck: true, + generateRequestId: () => RequestId(snowflakeGen.unsafeNext()), + flatten: true, + onFromClient(options): Effect.Effect< + void, + MailboxFull | AlreadyProcessingMessage | PersistenceError + > { + const address = Context.unsafeGet(options.context, ClientAddressTag) + switch (options.message._tag) { + case "Request": { + const fiber = Option.getOrThrow(Fiber.getCurrentFiber()) + const id = Snowflake.Snowflake(options.message.id) + const rpc = entity.protocol.requests.get(options.message.tag)! + let respond: (reply: Reply.Reply) => Effect.Effect + if (!options.discard) { + const entry: ClientRequestEntry = { + rpc: rpc as any, + services: fiber.currentContext + } + clientRequests.set(id, entry) + respond = makeClientRespond(entry, client.write) + } else { + respond = clientRespondDiscard + } + return sendOutgoing( + new Message.OutgoingRequest({ + envelope: Envelope.makeRequest({ + requestId: id, + address, + tag: options.message.tag, + payload: options.message.payload, + headers: options.message.headers, + traceId: options.message.traceId, + spanId: options.message.spanId, + sampled: options.message.sampled + }), + lastReceivedReply: Option.none(), + rpc, + context: fiber.currentContext as Context.Context, + respond + }), + options.discard + ) + } + case "Ack": { + const requestId = Snowflake.Snowflake(options.message.requestId) + const entry = clientRequests.get(requestId) + if (!entry) return Effect.void + return sendOutgoing( + new Message.OutgoingEnvelope({ + envelope: new Envelope.AckChunk({ + id: snowflakeGen.unsafeNext(), + address, + requestId, + replyId: entry.lastChunkId! + }), + rpc: entry.rpc + }), + false + ) + } + case "Interrupt": { + const requestId = Snowflake.Snowflake(options.message.requestId) + const entry = clientRequests.get(requestId)! + if (!entry) return Effect.void + clientRequests.delete(requestId) + if (Uninterruptible.forClient(entry.rpc.annotations)) { + return Effect.void + } + // for durable messages, we ignore interrupts on shutdown or as a + // result of a shard being resassigned + const isTransientInterrupt = MutableRef.get(isShutdown) || + options.message.interruptors.some((id) => internalInterruptors.has(id)) + if (isTransientInterrupt && Context.get(entry.rpc.annotations, Persisted)) { + return Effect.void + } + return Effect.ignore(sendOutgoing( + new Message.OutgoingEnvelope({ + envelope: new Envelope.Interrupt({ + id: snowflakeGen.unsafeNext(), + address, + requestId + }), + rpc: entry.rpc + }), + false, + 3 + )) + } + } + return Effect.void + } + }) + + yield* Scope.addFinalizer( + yield* Effect.scope, + Effect.fiberIdWith((fiberId) => { + internalInterruptors.add(fiberId) + return Effect.void + }) + ) + + return (entityId: string) => { + const id = makeEntityId(entityId) + const address = ClientAddressTag.context(makeEntityAddress({ + shardId: getShardId(id, entity.getShardGroup(entityId as EntityId)), + entityId: id, + entityType: entity.type + })) + const clientFn = function(tag: string, payload: any, options?: { + readonly context?: Context.Context + }) { + const context = options?.context ? Context.merge(options.context, address) : address + return client.client(tag, payload, { + ...options, + context + }) + } + const proxyClient: any = {} + return new Proxy(proxyClient, { + has(_, p) { + return entity.protocol.requests.has(p as string) + }, + get(target, p) { + if (p in target) { + return target[p] + } else if (!entity.protocol.requests.has(p as string)) { + return undefined + } + return target[p] = (payload: any, options?: {}) => clientFn(p as string, payload, options) + } + }) + } + })) + + const makeClient = (entity: Entity): Effect.Effect< + ( + entityId: string + ) => RpcClient.RpcClient.From + > => clients.get(entity) as any + + const clientRespondDiscard = (_reply: Reply.Reply) => Effect.void + + const makeClientRespond = ( + entry: ClientRequestEntry, + write: (reply: FromServer) => Effect.Effect + ) => + (reply: Reply.Reply) => { + switch (reply._tag) { + case "Chunk": { + entry.lastChunkId = reply.id + return write({ + _tag: "Chunk", + clientId: 0, + requestId: RequestId(reply.requestId), + values: reply.values + }) + } + case "WithExit": { + clientRequests.delete(reply.requestId) + return write({ + _tag: "Exit", + clientId: 0, + requestId: RequestId(reply.requestId), + exit: reply.exit + }) + } + } + } + + // --- Singletons --- + + const singletons = new Map>>() + const singletonFibers = yield* FiberMap.make() + const withSingletonLock = Effect.unsafeMakeSemaphore(1).withPermits(1) + + const registerSingleton: Sharding["Type"]["registerSingleton"] = Effect.fnUntraced( + function*(name, run, options) { + const shardGroup = options?.shardGroup ?? "default" + const address = new SingletonAddress({ + shardId: getShardId(makeEntityId(name), shardGroup), + name + }) + + let map = singletons.get(address.shardId) + if (!map) { + map = MutableHashMap.empty() + singletons.set(address.shardId, map) + } + if (MutableHashMap.has(map, address)) { + return yield* Effect.die(`Singleton '${name}' is already registered`) + } + + const context = yield* Effect.context() + const wrappedRun = run.pipe( + Effect.locally(FiberRef.currentLogAnnotations, HashMap.empty()), + Effect.andThen(Effect.never), + Effect.scoped, + Effect.provide(context), + Effect.orDie, + Effect.interruptible + ) as Effect.Effect + MutableHashMap.set(map, address, wrappedRun) + + yield* PubSub.publish(events, SingletonRegistered({ address })) + + // start if we are on the right shard + if (MutableHashSet.has(acquiredShards, address.shardId)) { + yield* Effect.logDebug("Starting singleton", address) + yield* FiberMap.run(singletonFibers, address, wrappedRun) + } + + yield* Effect.addFinalizer(() => { + const map = singletons.get(address.shardId)! + MutableHashMap.remove(map, address) + return FiberMap.remove(singletonFibers, address) + }) + }, + withSingletonLock + ) + + const syncSingletons = withSingletonLock(Effect.gen(function*() { + for (const [shardId, map] of singletons) { + for (const [address, run] of map) { + const running = FiberMap.unsafeHas(singletonFibers, address) + const shouldBeRunning = MutableHashSet.has(acquiredShards, shardId) + if (running && !shouldBeRunning) { + yield* Effect.logDebug("Stopping singleton", address) + internalInterruptors.add(Option.getOrThrow(Fiber.getCurrentFiber()).id()) + yield* FiberMap.remove(singletonFibers, address) + } else if (!running && shouldBeRunning) { + yield* Effect.logDebug("Starting singleton", address) + yield* FiberMap.run(singletonFibers, address, run) + } + } + } + ClusterMetrics.singletons.unsafeUpdate( + BigInt(yield* FiberMap.size(singletonFibers)), + [] + ) + })) + + // --- Entities --- + + const context = yield* Effect.context() + const reaper = yield* EntityReaper + const entityManagerLatches = new Map() + + const registerEntity: Sharding["Type"]["registerEntity"] = Effect.fnUntraced( + function*(entity, build, options) { + if (Option.isNone(config.runnerAddress) || entityManagers.has(entity.type)) return + const scope = yield* Effect.scope + yield* Scope.addFinalizer( + scope, + Effect.sync(() => { + state.status = "closed" + }) + ) + const manager = yield* EntityManager.make(entity, build, { + ...options, + storage, + runnerAddress: config.runnerAddress.value, + sharding + }).pipe( + Effect.provide(context.pipe( + Context.add(EntityReaper, reaper), + Context.add(Scope.Scope, scope), + Context.add(Snowflake.Generator, snowflakeGen) + )) + ) as Effect.Effect + const state: EntityManagerState = { + entity, + status: "alive", + manager + } + yield* Scope.addFinalizer( + scope, + Effect.fiberIdWith((id) => { + state.status = "closing" + internalInterruptors.add(id) + // if preemptive shutdown is enabled, we start shutting down Sharding + // too + return config.preemptiveShutdown ? shutdown() : Effect.void + }) + ) + + // register entities while storage is idle + // this ensures message order is preserved + yield* withStorageReadLock(Effect.sync(() => { + entityManagers.set(entity.type, state) + if (entityManagerLatches.has(entity.type)) { + entityManagerLatches.get(entity.type)!.unsafeOpen() + entityManagerLatches.delete(entity.type) + } + })) + + yield* PubSub.publish(events, EntityRegistered({ entity })) + } + ) + + const waitForEntityManager = (entityType: string) => { + let latch = entityManagerLatches.get(entityType) + if (!latch) { + latch = Effect.unsafeMakeLatch() + entityManagerLatches.set(entityType, latch) + } + return latch.await + } + + // --- Runner health checks --- + + if (selfRunner) { + const checkRunner = ([runner, healthy]: [Runner, boolean]) => + Effect.flatMap(runnerHealth.isAlive(runner.address), (isAlive) => { + if (healthy === isAlive) return Effect.void + if (isAlive) { + healthyRunnerCount++ + return Effect.logDebug(`Runner is healthy`, runner).pipe( + Effect.andThen(runnerStorage.setRunnerHealth(runner.address, isAlive)) + ) + } + if (healthyRunnerCount <= 1) { + // never mark the last runner as unhealthy, to prevent a deadlock + return Effect.void + } + healthyRunnerCount-- + return Effect.logDebug(`Runner is unhealthy`, runner).pipe( + Effect.andThen(runnerStorage.setRunnerHealth(runner.address, isAlive)) + ) + }) + + yield* registerSingleton( + "effect/cluster/Sharding/RunnerHealth", + Effect.gen(function*() { + while (true) { + // Skip health checks if we are the only runner + if (MutableHashMap.size(allRunners) > 1) { + yield* Effect.forEach(allRunners, checkRunner, { discard: true, concurrency: 10 }) + } + yield* Effect.sleep(config.runnerHealthCheckInterval) + } + }).pipe( + Effect.catchAllCause((cause) => Effect.logDebug("Runner health check failed", cause)), + Effect.forever, + Effect.annotateLogs({ + package: "@effect/cluster", + module: "Sharding", + fiber: "Runner health check" + }) + ) + ) + } + + // --- Finalization --- + + const shutdown = Effect.fnUntraced(function*(exit?: Exit.Exit) { + if (exit) { + yield* Effect.logDebug("Shutting down", exit._tag === "Failure" ? exit.cause : {}).pipe( + Effect.annotateLogs({ + package: "@effect/cluster", + module: "Sharding" + }) + ) + } + + internalInterruptors.add(yield* Effect.fiberId) + if (isShutdown.current) return + + MutableRef.set(isShutdown, true) + if (selfRunner) { + yield* Effect.ignore(runnerStorage.unregister(selfRunner.address)) + } + }) + + yield* Scope.addFinalizerExit(shardingScope, shutdown) + + const activeEntityCount = Effect.gen(function*() { + let count = 0 + for (const state of entityManagers.values()) { + count += yield* state.manager.activeEntityCount + } + return count + }) + + const sharding = Sharding.of({ + getRegistrationEvents, + getShardId, + hasShardId(shardId: ShardId) { + if (isShutdown.current) return false + return MutableHashSet.has(acquiredShards, shardId) + }, + getSnowflake: Effect.sync(() => snowflakeGen.unsafeNext()), + isShutdown: Effect.sync(() => MutableRef.get(isShutdown)), + registerEntity, + registerSingleton, + makeClient, + send: sendLocal, + sendOutgoing: (message, discard) => sendOutgoing(message, discard), + notify: (message, options) => notifyLocal(message, false, options), + activeEntityCount, + pollStorage: storageReadLatch.open, + reset + }) + + return sharding +}) + +/** + * @since 1.0.0 + * @category layers + */ +export const layer: Layer.Layer< + Sharding, + never, + ShardingConfig | Runners | MessageStorage.MessageStorage | RunnerStorage | RunnerHealth.RunnerHealth +> = Layer.scoped(Sharding)(make).pipe( + Layer.provide([Snowflake.layerGenerator, EntityReaper.Default]) +) + +// Utilities + +const ClientAddressTag = Context.GenericTag("@effect/cluster/Sharding/ClientAddress") diff --git a/repos/effect/packages/cluster/src/ShardingConfig.ts b/repos/effect/packages/cluster/src/ShardingConfig.ts new file mode 100644 index 0000000..20f80b3 --- /dev/null +++ b/repos/effect/packages/cluster/src/ShardingConfig.ts @@ -0,0 +1,287 @@ +/** + * @since 1.0.0 + */ +import * as Config from "effect/Config" +import type { ConfigError } from "effect/ConfigError" +import * as ConfigProvider from "effect/ConfigProvider" +import * as Context from "effect/Context" +import type { DurationInput } from "effect/Duration" +import * as Duration from "effect/Duration" +import * as Effect from "effect/Effect" +import * as Layer from "effect/Layer" +import * as Option from "effect/Option" +import { RunnerAddress } from "./RunnerAddress.js" + +/** + * Represents the configuration for the `Sharding` service on a given runner. + * + * @since 1.0.0 + * @category models + */ +export class ShardingConfig extends Context.Tag("@effect/cluster/ShardingConfig") + /** + * The listen address for the current runner. + * + * Defaults to the `runnerAddress`. + */ + readonly runnerListenAddress: Option.Option + /** + * A number that determines how many shards this runner will be assigned + * relative to other runners. + * + * Defaults to `1`. + * + * A value of `2` means that this runner should be assigned twice as many + * shards as a runner with a weight of `1`. + */ + readonly runnerShardWeight: number + /** + * The shard groups that are assigned to this runner. + * + * Defaults to `["default"]`. + */ + readonly shardGroups: ReadonlyArray + /** + * The number of shards to allocate per shard group. + * + * **Note**: this value should be consistent across all runners. + */ + readonly shardsPerGroup: number + /** + * Shard lock refresh interval. + */ + readonly shardLockRefreshInterval: DurationInput + /** + * Shard lock expiration duration. + */ + readonly shardLockExpiration: DurationInput + /** + * Disable the use of advisory locks for shard locking. + */ + readonly shardLockDisableAdvisory: boolean + /** + * Start shutting down as soon as an Entity has started shutting down. + * + * Defaults to `true`. + */ + readonly preemptiveShutdown: boolean + /** + * The default capacity of the mailbox for entities. + */ + readonly entityMailboxCapacity: number | "unbounded" + /** + * The maximum duration of inactivity (i.e. without receiving a message) + * after which an entity will be interrupted. + */ + readonly entityMaxIdleTime: DurationInput + /** + * If an entity does not register itself within this time after a message is + * sent to it, the message will be marked as failed. + * + * Defaults to 1 minute. + */ + readonly entityRegistrationTimeout: DurationInput + /** + * The maximum duration of time to wait for an entity to terminate. + * + * By default this is set to 15 seconds to stay within kubernetes defaults. + */ + readonly entityTerminationTimeout: DurationInput + /** + * The interval at which to poll for unprocessed messages from storage. + */ + readonly entityMessagePollInterval: DurationInput + /** + * The interval at which to poll for client replies from storage. + */ + readonly entityReplyPollInterval: DurationInput + /** + * The interval at which to poll for new runners and refresh shard + * assignments. + */ + readonly refreshAssignmentsInterval: DurationInput + /** + * The interval to retry a send if EntityNotAssignedToRunner is returned. + */ + readonly sendRetryInterval: DurationInput + /** + * The interval at which to check for unhealthy runners and report them + */ + readonly runnerHealthCheckInterval: DurationInput + /** + * Simulate serialization and deserialization to remote runners for local + * entities. + */ + readonly simulateRemoteSerialization: boolean +}>() {} + +const defaultRunnerAddress = RunnerAddress.make({ host: "localhost", port: 34431 }) + +/** + * @since 1.0.0 + * @category defaults + */ +export const defaults: ShardingConfig["Type"] = { + runnerAddress: Option.some(defaultRunnerAddress), + runnerListenAddress: Option.none(), + runnerShardWeight: 1, + shardsPerGroup: 300, + shardGroups: ["default"], + preemptiveShutdown: true, + shardLockRefreshInterval: Duration.seconds(10), + shardLockExpiration: Duration.seconds(35), + shardLockDisableAdvisory: false, + entityMailboxCapacity: 4096, + entityMaxIdleTime: Duration.minutes(1), + entityRegistrationTimeout: Duration.minutes(1), + entityTerminationTimeout: Duration.seconds(15), + entityMessagePollInterval: Duration.seconds(10), + entityReplyPollInterval: Duration.millis(200), + sendRetryInterval: Duration.millis(100), + refreshAssignmentsInterval: Duration.seconds(3), + runnerHealthCheckInterval: Duration.minutes(1), + simulateRemoteSerialization: true +} + +/** + * @since 1.0.0 + * @category Layers + */ +export const layer = (options?: Partial): Layer.Layer => + Layer.succeed(ShardingConfig, { ...defaults, ...options }) + +/** + * @since 1.0.0 + * @category defaults + */ +export const layerDefaults: Layer.Layer = layer() + +/** + * @since 1.0.0 + * @category Config + */ +export const config: Config.Config = Config.all({ + runnerAddress: Config.all({ + host: Config.string("host").pipe( + Config.withDefault(defaultRunnerAddress.host), + Config.withDescription("The hostname or IP address of the runner.") + ), + port: Config.integer("port").pipe( + Config.withDefault(defaultRunnerAddress.port), + Config.withDescription("The port used for inter-runner communication.") + ) + }).pipe(Config.map((options) => RunnerAddress.make(options)), Config.option), + runnerListenAddress: Config.all({ + host: Config.string("listenHost").pipe( + Config.withDescription("The host to listen on.") + ), + port: Config.integer("listenPort").pipe( + Config.withDefault(defaultRunnerAddress.port), + Config.withDescription("The port to listen on.") + ) + }).pipe(Config.map((options) => RunnerAddress.make(options)), Config.option), + runnerShardWeight: Config.integer("runnerShardWeight").pipe( + Config.withDefault(defaults.runnerShardWeight) + ), + shardGroups: Config.array(Config.string("shardGroups")).pipe( + Config.withDefault(["default"]), + Config.withDescription("The shard groups that are assigned to this runner.") + ), + shardsPerGroup: Config.integer("shardsPerGroup").pipe( + Config.withDefault(defaults.shardsPerGroup), + Config.withDescription("The number of shards to allocate per shard group.") + ), + preemptiveShutdown: Config.boolean("preemptiveShutdown").pipe( + Config.withDefault(defaults.preemptiveShutdown), + Config.withDescription("Start shutting down as soon as an Entity has started shutting down.") + ), + shardLockRefreshInterval: Config.duration("shardLockRefreshInterval").pipe( + Config.withDefault(defaults.shardLockRefreshInterval), + Config.withDescription("Shard lock refresh interval.") + ), + shardLockExpiration: Config.duration("shardLockExpiration").pipe( + Config.withDefault(defaults.shardLockExpiration), + Config.withDescription("Shard lock expiration duration.") + ), + shardLockDisableAdvisory: Config.boolean("shardLockDisableAdvisory").pipe( + Config.withDefault(defaults.shardLockDisableAdvisory), + Config.withDescription("Disable the use of advisory locks for shard locking.") + ), + entityMailboxCapacity: Config.integer("entityMailboxCapacity").pipe( + Config.withDefault(defaults.entityMailboxCapacity), + Config.withDescription("The default capacity of the mailbox for entities.") + ), + entityMaxIdleTime: Config.duration("entityMaxIdleTime").pipe( + Config.withDefault(defaults.entityMaxIdleTime), + Config.withDescription( + "The maximum duration of inactivity (i.e. without receiving a message) after which an entity will be interrupted." + ) + ), + entityRegistrationTimeout: Config.duration("entityRegistrationTimeout").pipe( + Config.withDefault(defaults.entityRegistrationTimeout), + Config.withDescription( + "If an entity does not register itself within this time after a message is sent to it, the message will be marked as failed." + ) + ), + entityTerminationTimeout: Config.duration("entityTerminationTimeout").pipe( + Config.withDefault(defaults.entityTerminationTimeout), + Config.withDescription("The maximum duration of time to wait for an entity to terminate.") + ), + entityMessagePollInterval: Config.duration("entityMessagePollInterval").pipe( + Config.withDefault(defaults.entityMessagePollInterval), + Config.withDescription("The interval at which to poll for unprocessed messages from storage.") + ), + entityReplyPollInterval: Config.duration("entityReplyPollInterval").pipe( + Config.withDefault(defaults.entityReplyPollInterval), + Config.withDescription("The interval at which to poll for client replies from storage.") + ), + sendRetryInterval: Config.duration("sendRetryInterval").pipe( + Config.withDefault(defaults.sendRetryInterval), + Config.withDescription("The interval to retry a send if EntityNotAssignedToRunner is returned.") + ), + refreshAssignmentsInterval: Config.duration("refreshAssignmentsInterval").pipe( + Config.withDefault(defaults.refreshAssignmentsInterval), + Config.withDescription("The interval at which to refresh shard assignments.") + ), + runnerHealthCheckInterval: Config.duration("runnerHealthCheckInterval").pipe( + Config.withDefault(defaults.runnerHealthCheckInterval), + Config.withDescription("The interval at which to check for unhealthy runners and report them.") + ), + simulateRemoteSerialization: Config.boolean("simulateRemoteSerialization").pipe( + Config.withDefault(defaults.simulateRemoteSerialization), + Config.withDescription("Simulate serialization and deserialization to remote runners for local entities.") + ) +}) + +/** + * @since 1.0.0 + * @category Config + */ +export const configFromEnv = config.pipe( + Effect.withConfigProvider( + ConfigProvider.fromEnv().pipe( + ConfigProvider.constantCase + ) + ) +) + +/** + * @since 1.0.0 + * @category Layers + */ +export const layerFromEnv = (options?: Partial | undefined): Layer.Layer< + ShardingConfig, + ConfigError +> => + Layer.effect( + ShardingConfig, + options ? Effect.map(configFromEnv, (config) => ({ ...config, ...options })) : configFromEnv + ) diff --git a/repos/effect/packages/cluster/src/ShardingRegistrationEvent.ts b/repos/effect/packages/cluster/src/ShardingRegistrationEvent.ts new file mode 100644 index 0000000..b0ba1b2 --- /dev/null +++ b/repos/effect/packages/cluster/src/ShardingRegistrationEvent.ts @@ -0,0 +1,61 @@ +/** + * @since 1.0.0 + */ +import * as Data from "effect/Data" +import type { Entity } from "./Entity.js" +import type { SingletonAddress } from "./SingletonAddress.js" + +/** + * Represents events that can occur when a runner registers entities or singletons. + * + * @since 1.0.0 + * @category models + */ +export type ShardingRegistrationEvent = + | EntityRegistered + | SingletonRegistered + +/** + * Represents an event that occurs when a new entity is registered with a runner. + * + * @since 1.0.0 + * @category models + */ +export interface EntityRegistered { + readonly _tag: "EntityRegistered" + readonly entity: Entity +} + +/** + * Represents an event that occurs when a new singleton is registered with a + * runner. + * + * @since 1.0.0 + * @category models + */ +export interface SingletonRegistered { + readonly _tag: "SingletonRegistered" + readonly address: SingletonAddress +} + +/** + * @since 1.0.0 + * @category pattern matching + */ +export const { + /** + * @since 1.0.0 + * @category pattern matching + */ + $match: match, + /** + * @since 1.0.0 + * @category constructors + */ + EntityRegistered, + /** + * @since 1.0.0 + * @category constructors + */ + SingletonRegistered +} = Data.taggedEnum() diff --git a/repos/effect/packages/cluster/src/SingleRunner.ts b/repos/effect/packages/cluster/src/SingleRunner.ts new file mode 100644 index 0000000..48e0f73 --- /dev/null +++ b/repos/effect/packages/cluster/src/SingleRunner.ts @@ -0,0 +1,41 @@ +/** + * @since 1.0.0 + */ +import type * as SqlClient from "@effect/sql/SqlClient" +import type * as ConfigError from "effect/ConfigError" +import * as Layer from "effect/Layer" +import type * as MessageStorage from "./MessageStorage.js" +import * as RunnerHealth from "./RunnerHealth.js" +import * as Runners from "./Runners.js" +import * as RunnerStorage from "./RunnerStorage.js" +import * as Sharding from "./Sharding.js" +import * as ShardingConfig from "./ShardingConfig.js" +import * as SqlMessageStorage from "./SqlMessageStorage.js" +import * as SqlRunnerStorage from "./SqlRunnerStorage.js" + +/** + * A sql backed single-node cluster, that can be used for running durable + * entities and workflows. + * + * @since 1.0.0 + * @category Layers + */ +export const layer = (options?: { + readonly shardingConfig?: Partial | undefined + readonly runnerStorage?: "memory" | "sql" | undefined +}): Layer.Layer< + | Sharding.Sharding + | Runners.Runners + | MessageStorage.MessageStorage, + ConfigError.ConfigError, + SqlClient.SqlClient +> => + Sharding.layer.pipe( + Layer.provideMerge(Runners.layerNoop), + Layer.provideMerge(SqlMessageStorage.layer), + Layer.provide([ + options?.runnerStorage === "memory" ? RunnerStorage.layerMemory : Layer.orDie(SqlRunnerStorage.layer), + RunnerHealth.layerNoop + ]), + Layer.provide(ShardingConfig.layerFromEnv(options?.shardingConfig)) + ) diff --git a/repos/effect/packages/cluster/src/Singleton.ts b/repos/effect/packages/cluster/src/Singleton.ts new file mode 100644 index 0000000..6388c1e --- /dev/null +++ b/repos/effect/packages/cluster/src/Singleton.ts @@ -0,0 +1,23 @@ +/** + * @since 1.0.0 + */ +import * as Effect from "effect/Effect" +import * as Layer from "effect/Layer" +import type { Scope } from "effect/Scope" +import { Sharding } from "./Sharding.js" + +/** + * @since 1.0.0 + * @category constructors + */ +export const make = ( + name: string, + run: Effect.Effect, + options?: { + readonly shardGroup?: string | undefined + } +): Layer.Layer> => + Layer.scopedDiscard(Effect.gen(function*() { + const sharding = yield* Sharding + yield* sharding.registerSingleton(name, run, options) + })) diff --git a/repos/effect/packages/cluster/src/SingletonAddress.ts b/repos/effect/packages/cluster/src/SingletonAddress.ts new file mode 100644 index 0000000..d4cc96d --- /dev/null +++ b/repos/effect/packages/cluster/src/SingletonAddress.ts @@ -0,0 +1,47 @@ +/** + * @since 1.0.0 + */ +import * as Equal from "effect/Equal" +import * as Hash from "effect/Hash" +import * as Schema from "effect/Schema" +import { ShardId } from "./ShardId.js" + +/** + * @since 1.0.0 + * @category Address + */ +export const TypeId: unique symbol = Symbol.for("@effect/cluster/SingletonAddress") + +/** + * @since 1.0.0 + * @category Address + */ +export type TypeId = typeof TypeId + +/** + * Represents the unique address of an singleton within the cluster. + * + * @since 1.0.0 + * @category Address + */ +export class SingletonAddress extends Schema.Class("@effect/cluster/SingletonAddress")({ + shardId: ShardId, + name: Schema.NonEmptyTrimmedString +}) { + /** + * @since 1.0.0 + */ + readonly [TypeId] = TypeId; + /** + * @since 1.0.0 + */ + [Hash.symbol]() { + return Hash.cached(this)(Hash.string(`${this.name}:${this.shardId.toString()}`)) + } + /** + * @since 1.0.0 + */ + [Equal.symbol](that: SingletonAddress): boolean { + return this.name === that.name && this.shardId[Equal.symbol](that.shardId) + } +} diff --git a/repos/effect/packages/cluster/src/Snowflake.ts b/repos/effect/packages/cluster/src/Snowflake.ts new file mode 100644 index 0000000..8031ecc --- /dev/null +++ b/repos/effect/packages/cluster/src/Snowflake.ts @@ -0,0 +1,194 @@ +/** + * @since 1.0.0 + */ +import type * as Brand from "effect/Brand" +import * as Context from "effect/Context" +import * as DateTime from "effect/DateTime" +import * as Effect from "effect/Effect" +import { identity } from "effect/Function" +import * as Layer from "effect/Layer" +import * as Schema from "effect/Schema" +import type { MachineId } from "./MachineId.js" + +/** + * @since 1.0.0 + * @category Symbols + */ +export const TypeId: unique symbol = Symbol.for("@effect/cluster/Snowflake") + +/** + * @since 1.0.0 + * @category Symbols + */ +export type TypeId = typeof TypeId + +/** + * @since 1.0.0 + * @category Models + */ +export type Snowflake = Brand.Branded + +/** + * @since 1.0.0 + * @category Models + */ +export const Snowflake = (input: string | bigint): Snowflake => + typeof input === "string" ? BigInt(input) as Snowflake : input as Snowflake + +/** + * @since 1.0.0 + * @category Models + */ +export declare namespace Snowflake { + /** + * @since 1.0.0 + * @category Models + */ + export interface Parts { + readonly timestamp: number + readonly machineId: MachineId + readonly sequence: number + } + + /** + * @since 1.0.0 + * @category Models + */ + export interface Generator { + readonly unsafeNext: () => Snowflake + readonly setMachineId: (machineId: MachineId) => Effect.Effect + } +} + +/** + * @since 1.0.0 + * @category Schemas + */ +export const SnowflakeFromBigInt: Schema.Schema = Schema.BigIntFromSelf.pipe( + Schema.brand(TypeId) +) + +/** + * @since 1.0.0 + * @category Schemas + */ +export const SnowflakeFromString: Schema.Schema = Schema.BigInt.pipe( + Schema.brand(TypeId) +) + +/** + * @since 1.0.0 + * @category Epoch + */ +export const constEpochMillis: number = Date.UTC(2025, 0, 1) + +const sinceUnixEpoch = constEpochMillis - Date.UTC(1970, 0, 1) +const constBigInt12 = BigInt(12) +const constBigInt22 = BigInt(22) +const constBigInt1024 = BigInt(1024) +const constBigInt4096 = BigInt(4096) + +/** + * @since 1.0.0 + * @category constructors + */ +export const make = (options: { + readonly machineId: MachineId + readonly sequence: number + readonly timestamp: number +}): Snowflake => + (BigInt(options.timestamp - constEpochMillis) << constBigInt22 + | (BigInt(options.machineId % 1024) << constBigInt12) + | BigInt(options.sequence % 4096)) as Snowflake + +/** + * @since 1.0.0 + * @category Parts + */ +export const timestamp = (snowflake: Snowflake): number => Number(snowflake >> constBigInt22) + sinceUnixEpoch + +/** + * @since 1.0.0 + * @category Parts + */ +export const dateTime = (snowflake: Snowflake): DateTime.Utc => DateTime.unsafeMake(timestamp(snowflake)) + +/** + * @since 1.0.0 + * @category Parts + */ +export const machineId = (snowflake: Snowflake): MachineId => + Number((snowflake >> constBigInt12) % constBigInt1024) as MachineId + +/** + * @since 1.0.0 + * @category Parts + */ +export const sequence = (snowflake: Snowflake): number => Number(snowflake % constBigInt4096) + +/** + * @since 1.0.0 + * @category Parts + */ +export const toParts = (snowflake: Snowflake): Snowflake.Parts => ({ + timestamp: timestamp(snowflake), + machineId: machineId(snowflake), + sequence: sequence(snowflake) +}) + +/** + * @since 1.0.0 + * @category Generator + */ +export const makeGenerator: Effect.Effect = Effect.gen(function*() { + let machineId = Math.floor(Math.random() * 1024) as MachineId + const clock = yield* Effect.clock + + let sequence = 0 + let sequenceAt = Math.floor(clock.unsafeCurrentTimeMillis()) + + return identity({ + setMachineId: (newMachineId) => + Effect.sync(() => { + machineId = newMachineId + }), + unsafeNext() { + let now = Math.floor(clock.unsafeCurrentTimeMillis()) + + // account for clock drift, only allow time to move forward + if (now < sequenceAt) { + now = sequenceAt + } else if (now > sequenceAt) { + // reset sequence if we're in a new millisecond + sequence = 0 + sequenceAt = now + } else if (sequence >= 4096) { + // if we've hit the max sequence for this millisecond, go to the next + // millisecond + sequenceAt++ + sequence = 0 + } + + return make({ + machineId, + sequence: sequence++, + timestamp: sequenceAt + }) + } + }) +}) + +/** + * @since 1.0.0 + * @category Generator + */ +export class Generator extends Context.Tag("@effect/cluster/Snowflake/Generator")< + Generator, + Snowflake.Generator +>() {} + +/** + * @since 1.0.0 + * @category Generator + */ +export const layerGenerator: Layer.Layer = Layer.effect(Generator, makeGenerator) diff --git a/repos/effect/packages/cluster/src/SocketRunner.ts b/repos/effect/packages/cluster/src/SocketRunner.ts new file mode 100644 index 0000000..615ac24 --- /dev/null +++ b/repos/effect/packages/cluster/src/SocketRunner.ts @@ -0,0 +1,56 @@ +/** + * @since 1.0.0 + */ +import { SocketServer } from "@effect/platform/SocketServer" +import type * as RpcSerialization from "@effect/rpc/RpcSerialization" +import * as RpcServer from "@effect/rpc/RpcServer" +import * as Effect from "effect/Effect" +import * as Layer from "effect/Layer" +import type { MessageStorage } from "./MessageStorage.js" +import type { RunnerHealth } from "./RunnerHealth.js" +import type * as Runners from "./Runners.js" +import * as RunnerServer from "./RunnerServer.js" +import type * as RunnerStorage from "./RunnerStorage.js" +import type * as Sharding from "./Sharding.js" +import type { ShardingConfig } from "./ShardingConfig.js" + +const withLogAddress = (layer: Layer.Layer): Layer.Layer => + Layer.effectDiscard(Effect.gen(function*() { + const server = yield* SocketServer + const address = server.address._tag === "UnixAddress" + ? server.address.path + : `${server.address.hostname}:${server.address.port}` + yield* Effect.annotateLogs(Effect.logInfo(`Listening on: ${address}`), { + package: "@effect/cluster", + service: "Runner" + }) + })).pipe(Layer.provideMerge(layer)) + +/** + * @since 1.0.0 + * @category Layers + */ +export const layer: Layer.Layer< + Sharding.Sharding | Runners.Runners, + never, + | Runners.RpcClientProtocol + | ShardingConfig + | RpcSerialization.RpcSerialization + | SocketServer + | MessageStorage + | RunnerStorage.RunnerStorage + | RunnerHealth +> = RunnerServer.layerWithClients.pipe( + withLogAddress, + Layer.provide(RpcServer.layerProtocolSocketServer) +) + +/** + * @since 1.0.0 + * @category Layers + */ +export const layerClientOnly: Layer.Layer< + Sharding.Sharding | Runners.Runners, + never, + Runners.RpcClientProtocol | ShardingConfig | MessageStorage | RunnerStorage.RunnerStorage +> = RunnerServer.layerClientOnly diff --git a/repos/effect/packages/cluster/src/SqlMessageStorage.ts b/repos/effect/packages/cluster/src/SqlMessageStorage.ts new file mode 100644 index 0000000..4326c38 --- /dev/null +++ b/repos/effect/packages/cluster/src/SqlMessageStorage.ts @@ -0,0 +1,974 @@ +/** + * @since 1.0.0 + */ +import * as Migrator from "@effect/sql/Migrator" +import * as SqlClient from "@effect/sql/SqlClient" +import type { Row } from "@effect/sql/SqlConnection" +import type { SqlError } from "@effect/sql/SqlError" +import * as Arr from "effect/Array" +import * as Effect from "effect/Effect" +import * as Layer from "effect/Layer" +import * as Option from "effect/Option" +import * as Schedule from "effect/Schedule" +import { PersistenceError } from "./ClusterError.js" +import type * as Envelope from "./Envelope.js" +import * as MessageStorage from "./MessageStorage.js" +import { SaveResultEncoded } from "./MessageStorage.js" +import type * as Reply from "./Reply.js" +import { ShardId } from "./ShardId.js" +import type { ShardingConfig } from "./ShardingConfig.js" +import * as Snowflake from "./Snowflake.js" + +const withTracerDisabled = Effect.withTracerEnabled(false) + +/** + * @since 1.0.0 + * @category Constructors + */ +export const make = Effect.fnUntraced(function*(options?: { + readonly prefix?: string | undefined +}) { + const sql = (yield* SqlClient.SqlClient).withoutTransforms() + const prefix = options?.prefix ?? "cluster" + const table = (name: string) => `${prefix}_${name}` + + yield* Effect.orDie( + Migrator.make({})({ + loader: migrations(options), + table: table("migrations") + }) + ) + + const messageKindAckChunk = sql.literal(String(messageKind.AckChunk)) + const messageKindInterrupt = sql.literal(String(messageKind.Interrupt)) + const replyKindWithExit = sql.literal(String(replyKind.WithExit)) + + const messagesTable = table("messages") + const messagesTableSql = sql(messagesTable) + + const repliesTable = table("replies") + const repliesTableSql = sql(repliesTable) + + const envelopeToRow = ( + envelope: Envelope.Envelope.Encoded, + message_id: string | null, + deliver_at: number | null + ): MessageRow => { + switch (envelope._tag) { + case "Request": + return { + id: envelope.requestId, + message_id, + shard_id: ShardId.toString(envelope.address.shardId), + entity_type: envelope.address.entityType, + entity_id: envelope.address.entityId, + kind: messageKind.Request, + tag: envelope.tag, + payload: JSON.stringify(envelope.payload), + headers: JSON.stringify(envelope.headers), + trace_id: envelope.traceId ?? null, + span_id: envelope.spanId ?? null, + sampled: envelope.sampled === undefined + ? null + : supportsBooleans + ? envelope.sampled + : envelope.sampled + ? 1 + : 0, + request_id: envelope.requestId, + reply_id: null, + deliver_at + } + case "AckChunk": + return { + id: envelope.id, + message_id, + shard_id: ShardId.toString(envelope.address.shardId), + entity_type: envelope.address.entityType, + entity_id: envelope.address.entityId, + kind: messageKind.AckChunk, + tag: null, + payload: null, + headers: null, + trace_id: null, + span_id: null, + sampled: null, + request_id: envelope.requestId, + reply_id: envelope.replyId, + deliver_at + } + case "Interrupt": + return { + id: envelope.id, + message_id, + shard_id: ShardId.toString(envelope.address.shardId), + entity_type: envelope.address.entityType, + entity_id: envelope.address.entityId, + kind: messageKind.Interrupt, + payload: null, + tag: null, + headers: null, + trace_id: null, + span_id: null, + sampled: null, + request_id: envelope.requestId, + reply_id: null, + deliver_at + } + } + } + + const replyToRow = (reply: Reply.ReplyEncoded): ReplyRow => ({ + id: reply.id, + kind: replyKind[reply._tag], + request_id: reply.requestId, + payload: reply._tag === "WithExit" ? JSON.stringify(reply.exit) : JSON.stringify(reply.values), + sequence: reply._tag === "Chunk" ? reply.sequence : null + }) + + const supportsBooleans = sql.onDialectOrElse({ + mssql: () => false, + sqlite: () => false, + orElse: () => true + }) + + const messageFromRow = (row: MessageRow & ReplyJoinRow): { + readonly envelope: Envelope.Envelope.Encoded + readonly lastSentReply: Option.Option> + } => { + switch (Number(row.kind) as 0 | 1 | 2) { + case 0: + return { + envelope: { + _tag: "Request", + requestId: String(row.id), + address: { + shardId: ShardId.fromStringEncoded(row.shard_id), + entityType: row.entity_type, + entityId: row.entity_id + }, + tag: row.tag!, + payload: JSON.parse(row.payload!), + headers: JSON.parse(row.headers!), + traceId: row.trace_id ?? undefined, + spanId: row.span_id ?? undefined, + sampled: !!row.sampled + }, + lastSentReply: row.reply_reply_id ? + Option.some({ + _tag: "Chunk", + id: String(row.reply_reply_id), + requestId: String(row.request_id), + sequence: Number(row.reply_sequence!), + values: JSON.parse(row.reply_payload!) + } as any) : + Option.none() + } + case 1: + return { + envelope: { + _tag: "AckChunk", + id: String(row.id), + requestId: String(row.request_id!), + replyId: String(row.reply_id!), + address: { + shardId: ShardId.fromStringEncoded(row.shard_id), + entityType: row.entity_type, + entityId: row.entity_id + } + }, + lastSentReply: Option.none() + } + case 2: + return { + envelope: { + _tag: "Interrupt", + id: String(row.id), + requestId: String(row.request_id!), + address: { + shardId: ShardId.fromStringEncoded(row.shard_id), + entityType: row.entity_type, + entityId: row.entity_id + } + }, + lastSentReply: Option.none() + } + } + } + + const sqlFalse = sql.literal(supportsBooleans ? "FALSE" : "0") + const sqlTrue = sql.literal(supportsBooleans ? "TRUE" : "1") + + const insertEnvelope: ( + row: MessageRow, + message_id: string + ) => Effect.Effect, SqlError> = sql.onDialectOrElse({ + pg: () => (row, message_id) => + sql` + INSERT INTO ${messagesTableSql} ${sql.insert(row)} + ON CONFLICT (message_id) DO NOTHING + RETURNING id + `.pipe(Effect.flatMap((rows) => { + // inserted a new row + if (rows.length > 0) return Effect.succeed([]) + return sql` + SELECT m.id, r.id as reply_id, r.kind as reply_kind, r.payload as reply_payload, r.sequence as reply_sequence + FROM ${messagesTableSql} m + LEFT JOIN ${repliesTableSql} r ON r.id = m.last_reply_id + WHERE m.message_id = ${message_id} + ` + })), + mysql: () => (row, message_id) => + Effect.flatMap( + sql`INSERT IGNORE INTO ${messagesTableSql} ${sql.insert(row)}`.raw, + (row: any) => { + if (row.affectedRows > 0) { + return Effect.succeed([]) + } + return sql` + SELECT m.id, r.id as reply_id, r.kind as reply_kind, r.payload as reply_payload, r.sequence as reply_sequence + FROM ${messagesTableSql} m + LEFT JOIN ${repliesTableSql} r ON r.id = m.last_reply_id + WHERE m.message_id = ${message_id} + ` + } + ), + mssql: () => (row, message_id) => + sql` + MERGE ${messagesTableSql} WITH (HOLDLOCK) AS target + USING (SELECT ${message_id} as message_id) AS source + ON target.message_id = source.message_id + WHEN NOT MATCHED THEN + INSERT ${sql.insert(row)} + OUTPUT + inserted.id, + CASE + WHEN inserted.id IS NULL THEN ( + SELECT r.id, r.kind, r.payload + FROM ${repliesTableSql} r + WHERE r.id = target.last_reply_id + ) + END as reply_id, + CASE + WHEN inserted.id IS NULL THEN ( + SELECT r.kind + FROM ${repliesTableSql} r + WHERE r.id = target.last_reply_id + ) + END as reply_kind, + CASE + WHEN inserted.id IS NULL THEN ( + SELECT r.payload + FROM ${repliesTableSql} r + WHERE r.id = target.last_reply_id + ) + END as reply_payload, + CASE + WHEN inserted.id IS NULL THEN ( + SELECT r.sequence + FROM ${repliesTableSql} r + WHERE r.id = target.last_reply_id + ) + END as reply_sequence; + `, + orElse: () => (row, message_id) => + sql` + SELECT m.id, r.id as reply_id, r.kind as reply_kind, r.payload as reply_payload, r.sequence as reply_sequence + FROM ${messagesTableSql} m + LEFT JOIN ${repliesTableSql} r ON r.id = m.last_reply_id + WHERE m.message_id = ${message_id} + `.pipe( + Effect.tap(sql`INSERT OR IGNORE INTO ${messagesTableSql} ${sql.insert(row)}`), + sql.withTransaction, + Effect.retry({ times: 3 }) + ) + }) + + const tenMinutesAgo = sql.onDialectOrElse({ + mssql: () => sql.literal(`DATEADD(MINUTE, -10, GETDATE())`), + mysql: () => sql.literal(`NOW() - INTERVAL 10 MINUTE`), + pg: () => sql.literal(`NOW() - INTERVAL '10 minutes'`), + orElse: () => sql.literal(`DATETIME('now', '-10 minute')`) + }) + const sqlNowString = sql.onDialectOrElse({ + pg: () => "NOW()", + mysql: () => "NOW()", + mssql: () => "GETDATE()", + orElse: () => "CURRENT_TIMESTAMP" + }) + const sqlNow = sql.literal(sqlNowString) + + const wrapString = sql.onDialectOrElse({ + mssql: () => (s: string) => `N'${s}'`, + orElse: () => (s: string) => `'${s}'` + }) + const forUpdate = sql.onDialectOrElse({ + sqlite: () => sql.literal(""), + orElse: () => sql.literal("FOR UPDATE") + }) + + const getUnprocessedMessages = sql.onDialectOrElse({ + pg: () => (shardIds: Arr.NonEmptyArray, now: number) => + sql` + WITH messages AS ( + UPDATE ${messagesTableSql} m + SET last_read = ${sqlNow} + FROM ( + SELECT m.* + FROM ${messagesTableSql} m + WHERE m.shard_id IN (${sql.literal(shardIds.map(wrapString).join(","))}) + AND NOT EXISTS ( + SELECT 1 FROM ${repliesTableSql} + WHERE request_id = m.request_id + AND (kind = ${replyKindWithExit} OR acked = ${sqlFalse}) + ) + AND m.processed = ${sqlFalse} + AND (m.last_read IS NULL OR m.last_read < ${tenMinutesAgo}) + AND (m.deliver_at IS NULL OR m.deliver_at <= ${sql.literal(String(now))}) + FOR UPDATE + ) AS ids + LEFT JOIN ${repliesTableSql} r ON r.id = ids.last_reply_id + WHERE m.id = ids.id + RETURNING ids.*, r.id as reply_reply_id, r.kind as reply_kind, r.payload as reply_payload, r.sequence as reply_sequence + ) + SELECT * FROM messages ORDER BY rowid ASC + `, + orElse: () => (shardIds: Arr.NonEmptyArray, now: number) => + sql` + SELECT m.*, r.id as reply_reply_id, r.kind as reply_kind, r.payload as reply_payload, r.sequence as reply_sequence + FROM ${messagesTableSql} m + LEFT JOIN ${repliesTableSql} r ON r.id = m.last_reply_id + WHERE m.shard_id IN (${sql.literal(shardIds.map(wrapString).join(","))}) + AND NOT EXISTS ( + SELECT 1 FROM ${repliesTableSql} + WHERE request_id = m.request_id + AND (kind = ${replyKindWithExit} OR acked = ${sqlFalse}) + ) + AND processed = ${sqlFalse} + AND (m.last_read IS NULL OR m.last_read < ${tenMinutesAgo}) + AND (m.deliver_at IS NULL OR m.deliver_at <= ${sql.literal(String(now))}) + ORDER BY m.rowid ASC + ${forUpdate} + `.unprepared.pipe( + Effect.tap((rows) => { + if (rows.length === 0) { + return Effect.void + } + return sql` + UPDATE ${messagesTableSql} + SET last_read = ${sqlNow} + WHERE id IN (${sql.literal(rows.map((row) => row.id).join(","))}) + `.unprepared + }), + sql.withTransaction + ) + }) + + return yield* MessageStorage.makeEncoded({ + saveEnvelope: ({ deliverAt, envelope, primaryKey }) => + Effect.suspend(() => { + const row = envelopeToRow(envelope, primaryKey, deliverAt) + let insert = primaryKey + ? insertEnvelope(row, primaryKey) + : Effect.as(sql`INSERT INTO ${messagesTableSql} ${sql.insert(row)}`.unprepared, []) + if (envelope._tag === "AckChunk") { + insert = sql`UPDATE ${repliesTableSql} SET acked = ${sqlTrue} WHERE id = ${envelope.replyId}`.pipe( + Effect.andThen( + sql`UPDATE ${messagesTableSql} SET processed = ${sqlTrue} WHERE processed = ${sqlFalse} AND request_id = ${envelope.requestId} AND kind = ${messageKindAckChunk}` + ), + Effect.andThen(insert), + sql.withTransaction + ) + } + return insert.pipe( + Effect.map((rows) => { + if (rows.length === 0) { + return SaveResultEncoded.Success() + } + const row = rows[0] + const replyKindNum = typeof row.reply_kind === "bigint" ? Number(row.reply_kind) : row.reply_kind + return SaveResultEncoded.Duplicate({ + originalId: Snowflake.Snowflake(row.id as any), + lastReceivedReply: row.reply_id ? + Option.some({ + id: String(row.reply_id), + requestId: String(row.id), + _tag: replyKindNum === replyKind.WithExit ? "WithExit" : "Chunk", + ...(replyKindNum === replyKind.WithExit + ? { exit: JSON.parse(row.reply_payload as string) } + : { + sequence: Number(row.reply_sequence), + values: JSON.parse(row.reply_payload as string) + }) + } as any) : + Option.none() + }) + }) + ) + }).pipe( + Effect.provideService(SqlClient.SafeIntegers, true), + PersistenceError.refail, + withTracerDisabled + ), + + saveReply: (reply) => + Effect.suspend(() => { + const row = replyToRow(reply) + const update = reply._tag === "Chunk" ? + sql`UPDATE ${messagesTableSql} SET last_reply_id = ${reply.id} WHERE id = ${reply.requestId}` : + sql`UPDATE ${messagesTableSql} SET processed = ${sqlTrue}, last_reply_id = ${reply.id} WHERE request_id = ${reply.requestId}` + return update.unprepared.pipe( + Effect.andThen(sql`INSERT INTO ${repliesTableSql} ${sql.insert(row)}`), + sql.withTransaction + ) + }).pipe( + Effect.asVoid, + PersistenceError.refail, + withTracerDisabled + ), + + clearReplies: Effect.fnUntraced( + function*(requestId) { + yield* sql`DELETE FROM ${repliesTableSql} WHERE request_id = ${String(requestId)} AND kind = 0` + yield* sql`DELETE FROM ${messagesTableSql} WHERE request_id = ${ + String(requestId) + } AND kind = ${messageKindInterrupt}` + yield* sql`UPDATE ${messagesTableSql} SET processed = ${sqlFalse}, last_reply_id = NULL, last_read = NULL WHERE request_id = ${ + String(requestId) + }` + }, + sql.withTransaction, + PersistenceError.refail, + withTracerDisabled + ), + + requestIdForPrimaryKey: (primaryKey) => + sql<{ id: string | bigint }>`SELECT id FROM ${messagesTableSql} WHERE message_id = ${primaryKey}`.pipe( + Effect.map((rows) => + Option.fromNullable(rows[0]?.id).pipe( + Option.map(Snowflake.Snowflake) + ) + ), + Effect.provideService(SqlClient.SafeIntegers, true), + PersistenceError.refail, + withTracerDisabled + ), + + repliesFor: (requestIds) => + // replies where: + // - the request is in the list + // - the kind is WithExit + // - or the kind is Chunk and has not been acked yet + sql` + SELECT id, kind, request_id, payload, sequence + FROM ${repliesTableSql} + WHERE request_id IN (${sql.literal(requestIds.join(","))}) + AND ( + kind = ${replyKindWithExit} + OR ( + kind IS NULL + AND acked = ${sqlFalse} + ) + ) + ORDER BY rowid ASC + `.unprepared.pipe( + Effect.provideService(SqlClient.SafeIntegers, true), + Effect.map(Arr.map(replyFromRow)), + PersistenceError.refail, + withTracerDisabled + ), + + repliesForUnfiltered: (requestIds) => + sql` + SELECT id, kind, request_id, payload, sequence + FROM ${repliesTableSql} + WHERE request_id IN (${sql.literal(requestIds.join(","))}) + ORDER BY rowid ASC + `.unprepared.pipe( + Effect.provideService(SqlClient.SafeIntegers, true), + Effect.map(Arr.map(replyFromRow)), + PersistenceError.refail, + withTracerDisabled + ), + + unprocessedMessages: Effect.fnUntraced( + function*(shardIds, now) { + const rows = yield* getUnprocessedMessages(shardIds, now) + if (rows.length === 0) { + return [] + } + const messages: Array<{ + readonly envelope: Envelope.Envelope.Encoded + readonly lastSentReply: Option.Option> + }> = new Array(rows.length) + const ids = new Array(rows.length) + for (let i = 0; i < rows.length; i++) { + messages[i] = messageFromRow(rows[i]) + ids[i] = String(rows[i].id) + } + return messages + }, + Effect.provideService(SqlClient.SafeIntegers, true), + PersistenceError.refail, + withTracerDisabled + ), + + unprocessedMessagesById(ids, now) { + const idArr = ids.map((id) => String(id)) + return sql` + SELECT m.*, r.id as reply_id, r.kind as reply_kind, r.payload as reply_payload, r.sequence as reply_sequence + FROM ${messagesTableSql} m + LEFT JOIN ${repliesTableSql} r ON r.id = m.last_reply_id + WHERE m.id IN (${sql.literal(idArr.join(","))}) + AND NOT EXISTS ( + SELECT 1 FROM ${repliesTableSql} + WHERE request_id = m.request_id + AND (kind = ${replyKindWithExit} OR acked = ${sqlFalse}) + ) + AND m.processed = ${sqlFalse} + AND (m.deliver_at IS NULL OR m.deliver_at <= ${sql.literal(String(now))}) + ORDER BY m.rowid ASC + `.unprepared.pipe( + Effect.map(Arr.map(messageFromRow)), + Effect.provideService(SqlClient.SafeIntegers, true), + PersistenceError.refail, + withTracerDisabled + ) + }, + + resetAddress: (address) => + sql` + UPDATE ${messagesTableSql} + SET last_read = NULL + WHERE processed = ${sqlFalse} + AND shard_id = ${address.shardId.toString()} + AND entity_type = ${address.entityType} + AND entity_id = ${address.entityId} + `.pipe( + Effect.asVoid, + PersistenceError.refail, + withTracerDisabled + ), + + clearAddress: (address) => + sql` + DELETE FROM ${repliesTableSql} + WHERE request_id IN ( + SELECT id FROM ${messagesTableSql} + WHERE entity_type = ${address.entityType} + AND entity_id = ${address.entityId} + ) + `.pipe( + Effect.andThen( + sql` + DELETE FROM ${messagesTableSql} + WHERE entity_type = ${address.entityType} + AND entity_id = ${address.entityId} + ` + ), + sql.withTransaction, + Effect.asVoid, + PersistenceError.refail, + withTracerDisabled + ), + + resetShards: (shardIds) => + sql` + UPDATE ${messagesTableSql} + SET last_read = NULL + WHERE processed = ${sqlFalse} + AND shard_id IN (${sql.literal(shardIds.map(wrapString).join(","))}) + `.pipe( + Effect.asVoid, + PersistenceError.refail, + withTracerDisabled + ) + }) +}, withTracerDisabled) + +/** + * @since 1.0.0 + * @category Layers + */ +export const layer: Layer.Layer< + MessageStorage.MessageStorage, + never, + SqlClient.SqlClient | ShardingConfig +> = Layer.scoped(MessageStorage.MessageStorage, make()).pipe( + Layer.provide(Snowflake.layerGenerator) +) + +/** + * @since 1.0.0 + * @category Layers + */ +export const layerWith = (options: { + readonly prefix?: string | undefined +}): Layer.Layer => + Layer.scoped(MessageStorage.MessageStorage, make(options)).pipe( + Layer.provide(Snowflake.layerGenerator) + ) + +// ------------------------------------------------------------------------------------------------- +// internal +// ------------------------------------------------------------------------------------------------- + +const migrations = (options?: { + readonly prefix?: string | undefined +}) => { + const prefix = options?.prefix ?? "cluster" + const table = (name: string) => `${prefix}_${name}` + const messagesTable = table("messages") + const repliesTable = table("replies") + + return Migrator.fromRecord({ + "0001_create_tables": Effect.gen(function*() { + const sql = (yield* SqlClient.SqlClient).withoutTransforms() + const messagesTableSql = sql(messagesTable) + const repliesTableSql = sql(repliesTable) + + yield* sql.onDialectOrElse({ + mssql: () => + sql` + IF OBJECT_ID(N'${messagesTableSql}', N'U') IS NULL + CREATE TABLE ${messagesTableSql} ( + id BIGINT PRIMARY KEY, + rowid BIGINT IDENTITY(1,1), + message_id VARCHAR(255), + shard_id VARCHAR(50) NOT NULL, + entity_type VARCHAR(150) NOT NULL, + entity_id VARCHAR(255) NOT NULL, + kind INT NOT NULL, + tag VARCHAR(50), + payload TEXT, + headers TEXT, + trace_id VARCHAR(32), + span_id VARCHAR(16), + sampled BIT, + processed BIT NOT NULL DEFAULT 0, + request_id BIGINT NOT NULL, + reply_id BIGINT, + last_reply_id BIGINT, + last_read DATETIME, + deliver_at BIGINT, + UNIQUE (message_id) + ) + `, + mysql: () => + sql` + CREATE TABLE IF NOT EXISTS ${messagesTableSql} ( + id BIGINT NOT NULL, + rowid BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, + message_id VARCHAR(255), + shard_id VARCHAR(50) NOT NULL, + entity_type VARCHAR(150) NOT NULL, + entity_id VARCHAR(255) NOT NULL, + kind INT NOT NULL, + tag VARCHAR(50), + payload TEXT, + headers TEXT, + trace_id VARCHAR(32), + span_id VARCHAR(16), + sampled BOOLEAN, + processed BOOLEAN NOT NULL DEFAULT FALSE, + request_id BIGINT NOT NULL, + reply_id BIGINT, + last_reply_id BIGINT, + last_read DATETIME, + deliver_at BIGINT, + UNIQUE (id), + UNIQUE (message_id) + ) + `, + pg: () => + sql` + CREATE TABLE IF NOT EXISTS ${messagesTableSql} ( + id BIGINT PRIMARY KEY, + rowid BIGSERIAL, + message_id VARCHAR(255), + shard_id VARCHAR(50) NOT NULL, + entity_type VARCHAR(150) NOT NULL, + entity_id VARCHAR(255) NOT NULL, + kind INT NOT NULL, + tag VARCHAR(50), + payload TEXT, + headers TEXT, + trace_id VARCHAR(32), + span_id VARCHAR(16), + sampled BOOLEAN, + processed BOOLEAN NOT NULL DEFAULT FALSE, + request_id BIGINT NOT NULL, + reply_id BIGINT, + last_reply_id BIGINT, + last_read TIMESTAMP, + deliver_at BIGINT, + UNIQUE (message_id) + ) + `.pipe(Effect.ignore), + orElse: () => + // sqlite + sql` + CREATE TABLE IF NOT EXISTS ${messagesTableSql} ( + id INTEGER PRIMARY KEY, + message_id TEXT, + shard_id TEXT NOT NULL, + entity_type TEXT NOT NULL, + entity_id TEXT NOT NULL, + kind INTEGER NOT NULL, + tag TEXT, + payload TEXT, + headers TEXT, + trace_id TEXT, + span_id TEXT, + sampled BOOLEAN, + processed BOOLEAN NOT NULL DEFAULT FALSE, + request_id INTEGER NOT NULL, + reply_id INTEGER, + last_reply_id INTEGER, + last_read TEXT, + deliver_at INTEGER, + UNIQUE (message_id) + ) + ` + }) + + // Add message indexes optimized for the specific query patterns + const shardLookupIndex = `${messagesTable}_shard_idx` + const requestIdLookupIndex = `${messagesTable}_request_id_idx` + yield* sql.onDialectOrElse({ + mssql: () => + sql` + IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = ${shardLookupIndex}) + CREATE INDEX ${sql(shardLookupIndex)} + ON ${messagesTableSql} (shard_id, processed, last_read, deliver_at); + + IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = ${requestIdLookupIndex}) + CREATE INDEX ${sql(requestIdLookupIndex)} + ON ${messagesTableSql} (request_id); + `, + mysql: () => + sql` + CREATE INDEX ${sql(shardLookupIndex)} + ON ${messagesTableSql} (shard_id, processed, last_read, deliver_at); + + CREATE INDEX ${sql(requestIdLookupIndex)} + ON ${messagesTableSql} (request_id); + `.unprepared.pipe(Effect.ignore), + pg: () => + sql` + CREATE INDEX IF NOT EXISTS ${sql(shardLookupIndex)} + ON ${messagesTableSql} (shard_id, processed, last_read, deliver_at); + + CREATE INDEX IF NOT EXISTS ${sql(requestIdLookupIndex)} + ON ${messagesTableSql} (request_id); + `.pipe( + Effect.tapDefect((error) => + Effect.annotateLogs(Effect.logDebug("Failed to create indexes", error), { + package: "@effect/cluster", + module: "SqlMessageStorage" + }) + ), + Effect.retry({ + schedule: Schedule.spaced(1000) + }) + ), + orElse: () => + // sqlite + Effect.all([ + sql` + CREATE INDEX IF NOT EXISTS ${sql(shardLookupIndex)} + ON ${messagesTableSql} (shard_id, processed, last_read, deliver_at) + `, + sql` + CREATE INDEX IF NOT EXISTS ${sql(requestIdLookupIndex)} + ON ${messagesTableSql} (request_id) + ` + ]).pipe(sql.withTransaction) + }) + + yield* sql.onDialectOrElse({ + mssql: () => + sql` + IF OBJECT_ID(N'${repliesTableSql}', N'U') IS NULL + CREATE TABLE ${repliesTableSql} ( + id BIGINT PRIMARY KEY, + rowid BIGINT IDENTITY(1,1), + kind INT, + request_id BIGINT NOT NULL, + payload TEXT NOT NULL, + sequence INT, + acked BIT NOT NULL DEFAULT 0, + CONSTRAINT ${sql(repliesTable + "_one_exit")} UNIQUE (request_id, kind), + CONSTRAINT ${sql(repliesTable + "_sequence")} UNIQUE (request_id, sequence) + ) + `, + mysql: () => + sql` + CREATE TABLE IF NOT EXISTS ${repliesTableSql} ( + id BIGINT NOT NULL, + rowid BIGINT AUTO_INCREMENT PRIMARY KEY, + kind INT, + request_id BIGINT NOT NULL, + payload TEXT NOT NULL, + sequence INT, + acked BOOLEAN NOT NULL DEFAULT FALSE, + UNIQUE (id), + UNIQUE (request_id, kind), + UNIQUE (request_id, sequence) + ) + `, + pg: () => + sql` + CREATE TABLE IF NOT EXISTS ${repliesTableSql} ( + id BIGINT PRIMARY KEY, + rowid BIGSERIAL, + kind INT, + request_id BIGINT NOT NULL, + payload TEXT NOT NULL, + sequence INT, + acked BOOLEAN NOT NULL DEFAULT FALSE, + UNIQUE (request_id, kind), + UNIQUE (request_id, sequence) + ) + `, + orElse: () => + // sqlite + sql` + CREATE TABLE IF NOT EXISTS ${repliesTableSql} ( + id INTEGER PRIMARY KEY, + kind INTEGER, + request_id INTEGER NOT NULL, + payload TEXT NOT NULL, + sequence INTEGER, + acked BOOLEAN NOT NULL DEFAULT FALSE, + UNIQUE (request_id, kind), + UNIQUE (request_id, sequence) + ) + ` + }) + + // Add reply indexes optimized for request_id lookups + const replyLookupIndex = `${repliesTable}_request_lookup_idx` + yield* sql.onDialectOrElse({ + mssql: () => + sql` + IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = ${replyLookupIndex}) + CREATE INDEX ${sql(replyLookupIndex)} + ON ${repliesTableSql} (request_id, kind, acked); + `, + mysql: () => + sql` + CREATE INDEX ${sql(replyLookupIndex)} + ON ${repliesTableSql} (request_id, kind, acked); + `.unprepared.pipe(Effect.ignore), + pg: () => + sql` + CREATE INDEX IF NOT EXISTS ${sql(replyLookupIndex)} + ON ${repliesTableSql} (request_id, kind, acked); + `.pipe( + Effect.tapDefect((error) => + Effect.annotateLogs(Effect.logDebug("Failed to create indexes", error), { + package: "@effect/cluster", + module: "SqlMessageStorage" + }) + ), + Effect.retry({ + schedule: Schedule.spaced(1000) + }) + ), + orElse: () => + // sqlite + sql` + CREATE INDEX IF NOT EXISTS ${sql(replyLookupIndex)} + ON ${repliesTableSql} (request_id, kind, acked); + ` + }) + }), + "0002_entity_type_size": Effect.gen(function*() { + const sql = (yield* SqlClient.SqlClient).withoutTransforms() + const messagesTableSql = sql(messagesTable) + + // resize entity_type to 150 characters + yield* sql.onDialectOrElse({ + mssql: () => + sql` + ALTER TABLE ${messagesTableSql} ALTER COLUMN entity_type VARCHAR(150) NOT NULL; + `, + mysql: () => + sql` + ALTER TABLE ${messagesTableSql} MODIFY entity_type VARCHAR(150) NOT NULL; + `.unprepared.pipe(Effect.ignore), + pg: () => + sql` + ALTER TABLE ${messagesTableSql} ALTER COLUMN entity_type TYPE VARCHAR(150); + `, + orElse: () => + // sqlite + Effect.void + }) + }) + }) +} + +const messageKind = { + "Request": 0, + "AckChunk": 1, + "Interrupt": 2 +} as const satisfies Record + +const replyKind = { + "WithExit": 0, + "Chunk": null +} as const satisfies Record["_tag"], number | null> + +const replyFromRow = (row: ReplyRow): Reply.ReplyEncoded => + Number(row.kind) === replyKind.WithExit ? + { + _tag: "WithExit", + id: String(row.id), + requestId: String(row.request_id), + exit: JSON.parse(row.payload) + } : + { + _tag: "Chunk", + id: String(row.id), + requestId: String(row.request_id), + values: JSON.parse(row.payload), + sequence: Number(row.sequence!) + } + +type MessageRow = { + readonly id: string | bigint + readonly message_id: string | null + readonly shard_id: string + readonly entity_type: string + readonly entity_id: string + readonly kind: 0 | 1 | 2 | 0n | 1n | 2n + readonly tag: string | null + readonly payload: string | null + readonly headers: string | null + readonly trace_id: string | null + readonly span_id: string | null + readonly sampled: boolean | number | bigint | null + readonly request_id: string | bigint | null + readonly reply_id: string | bigint | null + readonly deliver_at: number | bigint | null +} + +type ReplyRow = { + readonly id: string | bigint + readonly kind: 0 | null | 0n + readonly request_id: string | bigint + readonly payload: string + readonly sequence: number | bigint | null +} + +type ReplyJoinRow = { + readonly reply_reply_id: string | bigint | null + readonly reply_payload: string | null + readonly reply_sequence: number | bigint | null +} + +type MessageJoinRow = MessageRow & ReplyJoinRow & { + readonly sequence: number | bigint +} diff --git a/repos/effect/packages/cluster/src/SqlRunnerStorage.ts b/repos/effect/packages/cluster/src/SqlRunnerStorage.ts new file mode 100644 index 0000000..99f2033 --- /dev/null +++ b/repos/effect/packages/cluster/src/SqlRunnerStorage.ts @@ -0,0 +1,662 @@ +/** + * @since 1.0.0 + */ +import * as SqlClient from "@effect/sql/SqlClient" +import type { SqlError } from "@effect/sql/SqlError" +import type * as Statement from "@effect/sql/Statement" +import * as Arr from "effect/Array" +import * as Duration from "effect/Duration" +import * as Effect from "effect/Effect" +import * as Layer from "effect/Layer" +import * as Scope from "effect/Scope" +import { PersistenceError } from "./ClusterError.js" +import { ResourceRef } from "./internal/resourceRef.js" +import * as RunnerStorage from "./RunnerStorage.js" +import * as ShardId from "./ShardId.js" +import * as ShardingConfig from "./ShardingConfig.js" + +const withTracerDisabled = Effect.withTracerEnabled(false) + +/** + * @since 1.0.0 + * @category Constructors + */ +export const make = Effect.fnUntraced(function*(options: { + readonly prefix?: string | undefined +}) { + const config = yield* ShardingConfig.ShardingConfig + const disableAdvisoryLocks = config.shardLockDisableAdvisory + const sql = (yield* SqlClient.SqlClient).withoutTransforms() + const prefix = options?.prefix ?? "cluster" + const table = (name: string) => `${prefix}_${name}` + + const acquireLockConn = sql.onDialectOrElse({ + pg: () => + Effect.fnUntraced(function*(scope: Scope.Scope) { + const conn = yield* Effect.orDie(sql.reserve).pipe( + Scope.extend(scope) + ) + const pid = (yield* conn.executeValues("SELECT pg_backend_pid()", []))[0][0] as number + yield* Scope.addFinalizerExit(scope, () => Effect.orDie(conn.executeRaw("SELECT pg_advisory_unlock_all()", []))) + return [conn, pid] as const + }, Effect.orDie), + mysql: () => + Effect.fnUntraced(function*(scope: Scope.Scope) { + const conn = yield* Effect.orDie(sql.reserve).pipe( + Scope.extend(scope) + ) + // we need to get the connection id using IS_USED_LOCK to properly + // support vitess + let pid: number | undefined = undefined + while (pid === undefined) { + const address = `cluster:pid:${(Math.random() * Number.MAX_SAFE_INTEGER) | 0}` + const taken = (yield* conn.executeValues( + `SELECT GET_LOCK('${address}', 10), IS_USED_LOCK('${address}')`, + [] + ))[0] as [1 | null, number] + if (taken[0] === null) continue + pid = taken[1] + } + yield* Scope.addFinalizerExit(scope, () => Effect.orDie(conn.executeRaw("SELECT RELEASE_ALL_LOCKS()", []))) + return [conn, pid] as const + }, Effect.orDie), + orElse: () => undefined + }) + const lockConn = acquireLockConn && (yield* ResourceRef.from(yield* Effect.scope, acquireLockConn)) + + const runnersTable = table("runners") + const runnersTableSql = sql(runnersTable) + + // Migrate old tables if they exist + // TODO: Remove in next major version + const hasOldTables = yield* sql`SELECT shard_id FROM ${sql(table("shards"))} LIMIT 1`.pipe( + Effect.isSuccess + ) + if (hasOldTables) { + yield* sql`DROP TABLE ${sql(table("shards"))}`.pipe(Effect.ignore) + yield* sql`DROP TABLE ${runnersTableSql}`.pipe(Effect.ignore) + } + + yield* sql.onDialectOrElse({ + mssql: () => + sql` + IF OBJECT_ID(N'${runnersTableSql}', N'U') IS NULL + CREATE TABLE ${runnersTableSql} ( + machine_id INT IDENTITY PRIMARY KEY, + address VARCHAR(255) NOT NULL, + runner TEXT NOT NULL, + healthy BIT NOT NULL DEFAULT 1, + last_heartbeat DATETIME NOT NULL DEFAULT GETDATE(), + UNIQUE(address) + ) + `, + mysql: () => + sql` + CREATE TABLE IF NOT EXISTS ${runnersTableSql} ( + machine_id INT AUTO_INCREMENT PRIMARY KEY, + address VARCHAR(255) NOT NULL, + runner TEXT NOT NULL, + healthy BOOLEAN NOT NULL DEFAULT TRUE, + last_heartbeat DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + UNIQUE(address) + ) + `, + pg: () => + sql` + CREATE TABLE IF NOT EXISTS ${runnersTableSql} ( + machine_id SERIAL PRIMARY KEY, + address VARCHAR(255) NOT NULL, + runner TEXT NOT NULL, + healthy BOOLEAN NOT NULL DEFAULT TRUE, + last_heartbeat TIMESTAMP NOT NULL DEFAULT NOW(), + UNIQUE(address) + ) + `, + orElse: () => + // sqlite + sql` + CREATE TABLE IF NOT EXISTS ${runnersTableSql} ( + machine_id INTEGER PRIMARY KEY AUTOINCREMENT, + address TEXT NOT NULL, + runner TEXT NOT NULL, + healthy INTEGER NOT NULL DEFAULT 1, + last_heartbeat DATETIME NOT NULL DEFAULT (CURRENT_TIMESTAMP), + UNIQUE(address) + ) + ` + }) + + const locksTable = table("locks") + const locksTableSql = sql(locksTable) + + yield* sql.onDialectOrElse({ + mssql: () => + sql` + IF OBJECT_ID(N'${locksTableSql}', N'U') IS NULL + CREATE TABLE ${locksTableSql} ( + shard_id VARCHAR(50) PRIMARY KEY, + address VARCHAR(255) NOT NULL, + acquired_at DATETIME NOT NULL + ) + `, + mysql: () => + sql` + CREATE TABLE IF NOT EXISTS ${locksTableSql} ( + shard_id VARCHAR(50) PRIMARY KEY, + address VARCHAR(255) NOT NULL, + acquired_at DATETIME NOT NULL + ) + `, + pg: () => + sql` + CREATE TABLE IF NOT EXISTS ${locksTableSql} ( + shard_id VARCHAR(50) PRIMARY KEY, + address VARCHAR(255) NOT NULL, + acquired_at TIMESTAMP NOT NULL + ) + `, + orElse: () => + // sqlite + sql` + CREATE TABLE IF NOT EXISTS ${locksTableSql} ( + shard_id TEXT PRIMARY KEY, + address TEXT NOT NULL, + acquired_at DATETIME NOT NULL + ) + ` + }) + + const sqlNowString = sql.onDialectOrElse({ + pg: () => "NOW()", + mysql: () => "NOW()", + mssql: () => "GETDATE()", + orElse: () => "CURRENT_TIMESTAMP" + }) + const sqlNow = sql.literal(sqlNowString) + + const expiresSeconds = sql.literal(Math.ceil(Duration.toSeconds(config.shardLockExpiration)).toString()) + const lockExpiresAt = sql.onDialectOrElse({ + pg: () => sql`${sqlNow} - INTERVAL '${expiresSeconds} seconds'`, + mysql: () => sql`DATE_SUB(${sqlNow}, INTERVAL ${expiresSeconds} SECOND)`, + mssql: () => sql`DATEADD(SECOND, -${expiresSeconds}, ${sqlNow})`, + orElse: () => sql`datetime(${sqlNow}, '-${expiresSeconds} seconds')` + }) + + const encodeBoolean = sql.onDialectOrElse({ + mssql: () => (b: boolean) => (b ? 1 : 0), + sqlite: () => (b: boolean) => (b ? 1 : 0), + orElse: () => (b: boolean) => b + }) + + // Upsert runner and return machine_id + const insertRunner = sql.onDialectOrElse({ + mssql: () => (address: string, runner: string, healthy: boolean) => + sql` + MERGE ${runnersTableSql} AS target + USING (SELECT ${address} AS address, ${runner} AS runner, ${sqlNow} AS last_heartbeat, ${ + encodeBoolean(healthy) + } AS healthy) AS source + ON target.address = source.address + WHEN MATCHED THEN + UPDATE SET runner = source.runner, last_heartbeat = source.last_heartbeat, healthy = source.healthy + WHEN NOT MATCHED THEN + INSERT (address, runner, last_heartbeat, healthy) + VALUES (source.address, source.runner, source.last_heartbeat, source.healthy) + OUTPUT INSERTED.machine_id; + `.values, + mysql: () => (address: string, runner: string, healthy: boolean) => + sql<{ machine_id: number }>` + INSERT INTO ${runnersTableSql} (address, runner, last_heartbeat, healthy) + VALUES (${address}, ${runner}, ${sqlNow}, ${healthy}) + ON DUPLICATE KEY UPDATE + runner = VALUES(runner), + last_heartbeat = VALUES(last_heartbeat), + healthy = VALUES(healthy); + SELECT machine_id FROM ${runnersTableSql} WHERE address = ${address}; + `.unprepared.pipe( + Effect.map((results: any) => [[results[1][0].machine_id]]) + ), + pg: () => (address: string, runner: string, healthy: boolean) => + sql` + INSERT INTO ${runnersTableSql} (address, runner, last_heartbeat, healthy) + VALUES (${address}, ${runner}, ${sqlNow}, ${healthy}) + ON CONFLICT (address) DO UPDATE + SET runner = EXCLUDED.runner, + last_heartbeat = EXCLUDED.last_heartbeat, + healthy = EXCLUDED.healthy + RETURNING machine_id + `.values, + orElse: () => (address: string, runner: string, healthy: boolean) => + // sqlite + sql` + INSERT INTO ${runnersTableSql} (address, runner, last_heartbeat, healthy) + VALUES (${address}, ${runner}, ${sqlNow}, ${encodeBoolean(healthy)}) + ON CONFLICT(address) DO UPDATE SET + runner = excluded.runner, + last_heartbeat = excluded.last_heartbeat, + healthy = excluded.healthy + RETURNING machine_id; + `.values + }) + + const execWithLockConn = (effect: Statement.Statement): Effect.Effect => { + if (!lockConn) return effect + const [query, params] = effect.compile() + return lockConn.await.pipe( + Effect.flatMap(([conn]) => conn.executeRaw(query, params)), + Effect.onError(() => lockConn.unsafeRebuild()) + ) + } + const execWithLockConnUnprepared = ( + effect: Statement.Statement + ): Effect.Effect>, SqlError> => { + if (!lockConn) return effect.values + const [query, params] = effect.compile() + return lockConn.await.pipe( + Effect.flatMap(([conn]) => conn.executeUnprepared(query, params, undefined)), + Effect.onError(() => lockConn.unsafeRebuild()) + ) + } + const execWithLockConnValues = ( + effect: Statement.Statement + ): Effect.Effect>, SqlError> => { + if (!lockConn) return effect.values + const [query, params] = effect.compile() + return lockConn.await.pipe( + Effect.flatMap(([conn]) => conn.executeValues(query, params)), + Effect.onError(() => lockConn.unsafeRebuild()) + ) + } + + const acquireLock = sql.onDialectOrElse({ + pg: () => { + if (disableAdvisoryLocks) { + return (address: string, shardIds: ReadonlyArray) => { + const values = shardIds.map((shardId) => + sql`(${stringLiteral(shardId)}, ${stringLiteral(address)}, ${sqlNow})` + ) + return sql` + INSERT INTO ${locksTableSql} (shard_id, address, acquired_at) VALUES ${sql.csv(values)} + ON CONFLICT (shard_id) DO UPDATE + SET address = ${address}, acquired_at = ${sqlNow} + WHERE ${locksTableSql}.address = ${address} + OR ${locksTableSql}.acquired_at < ${lockExpiresAt} +`.pipe( + Effect.andThen(acquiredLocks(address, shardIds)) + ) + } + } + return Effect.fnUntraced(function*(_address: string, shardIds: ReadonlyArray) { + const [conn, pid] = yield* lockConn!.await + const acquiredShardIds: Array = [] + const toAcquire = new Map(shardIds.map((shardId) => [lockNumbers.get(shardId)!, shardId])) + const takenLocks = yield* conn.executeValues( + `SELECT objid FROM pg_locks WHERE locktype = 'advisory' AND granted = true AND pid = ${pid} ORDER BY objid`, + [] + ) + for (let i = 0; i < takenLocks.length; i++) { + const lockNum = takenLocks[i][0] as number + acquiredShardIds.push(lockNumbersReverse.get(lockNum)!) + toAcquire.delete(lockNum) + } + if (toAcquire.size === 0) { + return acquiredShardIds + } + const rows = yield* conn.executeUnprepared(`SELECT ${pgLocks(toAcquire)}`, [], undefined) + const results = rows[0] as Record + for (const shardId in results) { + if (results[shardId]) { + acquiredShardIds.push(shardId) + } + } + return acquiredShardIds + }, Effect.onError(() => lockConn!.unsafeRebuild())) + }, + + mysql: () => { + if (disableAdvisoryLocks) { + return (address: string, shardIds: ReadonlyArray) => { + const values = shardIds.map((shardId) => + sql`(${stringLiteral(shardId)}, ${stringLiteral(address)}, ${sqlNow})` + ) + return sql` + INSERT INTO ${locksTableSql} (shard_id, address, acquired_at) VALUES ${sql.csv(values)} + ON DUPLICATE KEY UPDATE + address = IF(address = VALUES(address) OR acquired_at < ${lockExpiresAt}, VALUES(address), address), + acquired_at = IF(address = VALUES(address) OR acquired_at < ${lockExpiresAt}, VALUES(acquired_at), acquired_at) +`.unprepared.pipe( + Effect.andThen(acquiredLocks(address, shardIds)) + ) + } + } + return Effect.fnUntraced(function*(_address: string, shardIds: ReadonlyArray) { + const [conn, pid] = yield* lockConn!.await + const takenLocks = (yield* conn.executeValues(`SELECT ${allMySqlTakenLocks}`, []))[0] as Array + const acquiredShardIds: Array = [] + const toAcquire: Array = [] + for (let i = 0; i < shardIds.length; i++) { + const shardId = shardIds[i] + const lockTakenBy = takenLocks[shardIdsIndex.get(shardId)!] + if (lockTakenBy === pid) { + acquiredShardIds.push(shardId) + } else if (shardIds.includes(shardId)) { + toAcquire.push(shardId) + } + } + if (toAcquire.length === 0) { + return acquiredShardIds + } + const results = (yield* conn.executeValues(`SELECT ${mysqlLocks(toAcquire)}`, []))[0] as Array + for (let i = 0; i < results.length; i++) { + if (results[i] === 1) { + acquiredShardIds.push(toAcquire[i]) + } + } + return acquiredShardIds + }, Effect.onError(() => lockConn!.unsafeRebuild())) + }, + + mssql: () => (address: string, shardIds: ReadonlyArray) => { + const values = shardIds.map((shardId) => sql`(${stringLiteral(shardId)}, ${stringLiteral(address)}, ${sqlNow})`) + return sql` + MERGE ${locksTableSql} WITH (HOLDLOCK) AS target + USING (SELECT * FROM (VALUES ${sql.csv(values)})) AS source (shard_id, address, acquired_at) + ON target.shard_id = source.shard_id + WHEN MATCHED AND (target.address = source.address OR DATEDIFF(SECOND, target.acquired_at, ${sqlNow}) > ${expiresSeconds}) THEN + UPDATE SET address = source.address, acquired_at = source.acquired_at + WHEN NOT MATCHED THEN + INSERT (shard_id, address, acquired_at) + VALUES (source.shard_id, source.address, source.acquired_at); + `.pipe( + Effect.andThen(acquiredLocks(address, shardIds)), + sql.withTransaction + ) + }, + + orElse: () => (address: string, shardIds: ReadonlyArray) => { + const values = shardIds.map((shardId) => sql`(${stringLiteral(shardId)}, ${stringLiteral(address)}, ${sqlNow})`) + return sql` + WITH source(shard_id, address, acquired_at) AS (VALUES ${sql.csv(values)}) + INSERT INTO ${locksTableSql} (shard_id, address, acquired_at) + SELECT source.shard_id, source.address, source.acquired_at + FROM source + WHERE NOT EXISTS ( + SELECT 1 FROM ${locksTableSql} + WHERE shard_id = source.shard_id + AND address != ${address} + AND (strftime('%s', ${sqlNow}) - strftime('%s', acquired_at)) <= ${expiresSeconds} + ) + ON CONFLICT(shard_id) DO UPDATE + SET address = ${address}, acquired_at = ${sqlNow} + `.pipe( + Effect.andThen(acquiredLocks(address, shardIds)), + sql.withTransaction + ) + } + }) + + const lockNumbers = new Map() + const lockNumbersReverse = new Map() + for (let i = 0; i < config.shardGroups.length; i++) { + const group = config.shardGroups[i] + const base = (i + 1) * 1000000 + for (let shard = 1; shard <= config.shardsPerGroup; shard++) { + const shardId = ShardId.make(group, shard).toString() + const lockNum = base + shard + lockNumbers.set(shardId, lockNum) + lockNumbersReverse.set(lockNum, shardId) + } + } + + const shardIdsIndex = new Map() + const lockNames = new Map() + const lockNamesReverse = new Map() + { + let index = 0 + for (let i = 0; i < config.shardGroups.length; i++) { + const group = config.shardGroups[i] + for (let shard = 1; shard <= config.shardsPerGroup; shard++) { + const shardId = ShardId.make(group, shard).toString() + const lockName = `${prefix}.${shardId}` + shardIdsIndex.set(shardId, index++) + lockNames.set(shardId, lockName) + lockNamesReverse.set(lockName, shardId) + } + } + } + + const pgLocks = (shardIdsMap: Map) => + Array.from( + shardIdsMap.entries(), + ([lockNum, shardId]) => `pg_try_advisory_lock(${lockNum}) AS "${shardId}"` + ).join(", ") + + const mysqlLocks = (shardIds: ReadonlyArray) => + shardIds.map((shardId) => `GET_LOCK('${lockNames.get(shardId)!}', 0) AS "${shardId}"`).join(", ") + + const allMySqlTakenLocks = Array.from( + lockNames.entries(), + ([shardId, lockName]) => `IS_USED_LOCK('${lockName}') AS "${shardId}"` + ).join(", ") + + const acquiredLocks = (address: string, shardIds: ReadonlyArray) => + sql<{ shard_id: string }>` + SELECT shard_id FROM ${sql(locksTable)} + WHERE address = ${address} + AND acquired_at >= ${lockExpiresAt} + AND shard_id IN ${stringLiteralArr(shardIds)} + `.values.pipe( + Effect.map((rows) => rows.map((row) => row[0] as string)) + ) + + const wrapString = sql.onDialectOrElse({ + mssql: () => (s: string) => `N'${s}'`, + orElse: () => (s: string) => `'${s}'` + }) + const stringLiteral = (s: string) => sql.literal(wrapString(s)) + const stringLiteralArr = (arr: ReadonlyArray) => sql.literal(`(${arr.map(wrapString).join(",")})`) + + const refreshShards = sql.onDialectOrElse({ + pg: () => { + if (!disableAdvisoryLocks) return acquireLock + return (address: string, shardIds: ReadonlyArray) => + sql` + UPDATE ${locksTableSql} + SET acquired_at = ${sqlNow} + WHERE address = ${address} AND shard_id IN ${stringLiteralArr(shardIds)} + RETURNING shard_id + `.pipe( + execWithLockConnValues, + Effect.map((rows) => rows.map((row) => row[0] as string)) + ) + }, + mysql: () => { + if (!disableAdvisoryLocks) return acquireLock + return (address: string, shardIds: ReadonlyArray) => { + const shardIdsStr = stringLiteralArr(shardIds) + return sql>` + UPDATE ${locksTableSql} + SET acquired_at = ${sqlNow} + WHERE address = ${address} AND shard_id IN ${shardIdsStr}; + SELECT shard_id FROM ${locksTableSql} WHERE address = ${address} AND shard_id IN ${shardIdsStr} + `.pipe( + execWithLockConnUnprepared, + Effect.map((rows) => rows[1].map((row) => row.shard_id)) + ) + } + }, + mssql: () => (address: string, shardIds: ReadonlyArray) => + sql` + UPDATE ${locksTableSql} + SET acquired_at = ${sqlNow} + OUTPUT inserted.shard_id + WHERE address = ${address} AND shard_id IN ${stringLiteralArr(shardIds)} + `.pipe(execWithLockConnValues, Effect.map((rows) => rows.map((row) => row[0] as string))), + orElse: () => (address: string, shardIds: ReadonlyArray) => + sql` + UPDATE ${locksTableSql} + SET acquired_at = ${sqlNow} + WHERE address = ${address} AND shard_id IN ${stringLiteralArr(shardIds)} + RETURNING shard_id + `.pipe(execWithLockConnValues, Effect.map((rows) => rows.map((row) => row[0] as string))) + }) + + return RunnerStorage.makeEncoded({ + getRunners: sql`SELECT runner, healthy FROM ${runnersTableSql} WHERE last_heartbeat > ${lockExpiresAt}`.values.pipe( + PersistenceError.refail, + Effect.map(Arr.map(([runner, healthy]) => [String(runner), Boolean(healthy)] as const)), + withTracerDisabled + ), + + register: (address, runner, healthy) => + insertRunner(address, runner, healthy).pipe( + Effect.map((rows: any) => Number(rows[0][0])), + PersistenceError.refail, + withTracerDisabled + ), + + unregister: (address) => + sql`DELETE FROM ${runnersTableSql} WHERE address = ${address} OR last_heartbeat < ${lockExpiresAt}`.pipe( + Effect.asVoid, + PersistenceError.refail, + withTracerDisabled + ), + + setRunnerHealth: (address, healthy) => + sql`UPDATE ${runnersTableSql} SET healthy = ${encodeBoolean(healthy)} WHERE address = ${address}` + .pipe( + Effect.asVoid, + PersistenceError.refail, + withTracerDisabled + ), + + acquire: (address, shardIds) => + acquireLock(address, shardIds).pipe( + PersistenceError.refail, + withTracerDisabled + ), + + refresh: (address, shardIds) => + sql`UPDATE ${runnersTableSql} SET last_heartbeat = ${sqlNow} WHERE address = ${address}`.pipe( + execWithLockConn, + shardIds.length > 0 ? + Effect.andThen(refreshShards(address, shardIds)) : + Effect.as([]), + PersistenceError.refail + ), + + release: sql.onDialectOrElse({ + pg: () => { + if (disableAdvisoryLocks) { + return (address: string, shardId: string) => + sql`DELETE FROM ${locksTableSql} WHERE address = ${address} AND shard_id = ${shardId}`.pipe( + PersistenceError.refail + ) + } + return Effect.fnUntraced( + function*(_address, shardId) { + const lockNum = lockNumbers.get(shardId)! + for (let i = 0; i < 5; i++) { + const [conn] = yield* lockConn!.await + yield* conn.executeRaw(`SELECT pg_advisory_unlock(${lockNum})`, []) + const takenLocks = yield* conn.executeValues( + `SELECT 1 FROM pg_locks WHERE locktype = 'advisory' AND granted = true AND pid = pg_backend_pid() AND objid = ${lockNum}`, + [] + ) + if (takenLocks.length === 0) return + } + const [conn] = yield* lockConn!.await + yield* conn.executeRaw(`SELECT pg_advisory_unlock_all()`, []) + }, + Effect.onError(() => lockConn!.unsafeRebuild()), + Effect.asVoid, + PersistenceError.refail + ) + }, + mysql: () => { + if (disableAdvisoryLocks) { + return (address: string, shardId: string) => + sql`DELETE FROM ${locksTableSql} WHERE address = ${address} AND shard_id = ${shardId}`.pipe( + PersistenceError.refail + ) + } + return Effect.fnUntraced( + function*(_address, shardId) { + const lockName = lockNames.get(shardId)! + while (true) { + const [conn, pid] = yield* lockConn!.await + yield* conn.executeRaw(`SELECT RELEASE_LOCK('${lockName}')`, []) + const takenLocks = yield* conn.executeValues( + `SELECT IS_USED_LOCK('${lockName}')`, + [] + ) + if (takenLocks.length === 0 || takenLocks[0][0] !== pid) return + } + }, + Effect.onError(() => lockConn!.unsafeRebuild()), + Effect.asVoid, + PersistenceError.refail + ) + }, + orElse: () => (address, shardId) => + sql`DELETE FROM ${locksTableSql} WHERE address = ${address} AND shard_id = ${shardId}`.pipe( + PersistenceError.refail + ) + }), + + releaseAll: sql.onDialectOrElse({ + pg: () => (address) => { + if (disableAdvisoryLocks) { + return sql`DELETE FROM ${locksTableSql} WHERE address = ${address}`.pipe( + PersistenceError.refail, + withTracerDisabled + ) + } + return sql`SELECT pg_advisory_unlock_all()`.pipe( + execWithLockConn, + Effect.asVoid, + PersistenceError.refail, + withTracerDisabled + ) + }, + mysql: () => (address) => { + if (disableAdvisoryLocks) { + return sql`DELETE FROM ${locksTableSql} WHERE address = ${address}`.pipe( + PersistenceError.refail, + withTracerDisabled + ) + } + return sql`SELECT RELEASE_ALL_LOCKS()`.pipe( + execWithLockConn, + Effect.asVoid, + PersistenceError.refail, + withTracerDisabled + ) + }, + orElse: () => (address) => + sql`DELETE FROM ${locksTableSql} WHERE address = ${address}`.pipe( + PersistenceError.refail, + withTracerDisabled + ) + }) + }) +}, withTracerDisabled) + +/** + * @since 1.0.0 + * @category Layers + */ +export const layer: Layer.Layer< + RunnerStorage.RunnerStorage, + SqlError, + SqlClient.SqlClient | ShardingConfig.ShardingConfig +> = Layer.scoped(RunnerStorage.RunnerStorage)(make({})) + +/** + * @since 1.0.0 + * @category Layers + */ +export const layerWith = (options: { + readonly prefix?: string | undefined +}): Layer.Layer => + Layer.scoped(RunnerStorage.RunnerStorage)(make(options)) diff --git a/repos/effect/packages/cluster/src/TestRunner.ts b/repos/effect/packages/cluster/src/TestRunner.ts new file mode 100644 index 0000000..94c276b --- /dev/null +++ b/repos/effect/packages/cluster/src/TestRunner.ts @@ -0,0 +1,28 @@ +/** + * @since 1.0.0 + */ +import * as Layer from "effect/Layer" +import * as MessageStorage from "./MessageStorage.js" +import * as RunnerHealth from "./RunnerHealth.js" +import * as Runners from "./Runners.js" +import * as RunnerStorage from "./RunnerStorage.js" +import * as Sharding from "./Sharding.js" +import * as ShardingConfig from "./ShardingConfig.js" + +/** + * An in-memory cluster that can be used for testing purposes. + * + * MessageStorage is backed by an in-memory driver, and RunnerStorage is backed + * by an in-memory driver. + * + * @since 1.0.0 + * @category Layers + */ +export const layer: Layer.Layer< + Sharding.Sharding | Runners.Runners | MessageStorage.MessageStorage | MessageStorage.MemoryDriver +> = Sharding.layer.pipe( + Layer.provideMerge(Runners.layerNoop), + Layer.provideMerge(MessageStorage.layerMemory), + Layer.provide([RunnerStorage.layerMemory, RunnerHealth.layerNoop]), + Layer.provide(ShardingConfig.layer()) +) diff --git a/repos/effect/packages/cluster/src/index.ts b/repos/effect/packages/cluster/src/index.ts new file mode 100644 index 0000000..4915e63 --- /dev/null +++ b/repos/effect/packages/cluster/src/index.ts @@ -0,0 +1,189 @@ +/** + * @since 1.0.0 + */ +export * as ClusterCron from "./ClusterCron.js" + +/** + * @since 1.0.0 + */ +export * as ClusterError from "./ClusterError.js" + +/** + * @since 1.0.0 + */ +export * as ClusterMetrics from "./ClusterMetrics.js" + +/** + * @since 1.0.0 + */ +export * as ClusterSchema from "./ClusterSchema.js" + +/** + * @since 1.0.0 + */ +export * as ClusterWorkflowEngine from "./ClusterWorkflowEngine.js" + +/** + * @since 1.0.0 + */ +export * as DeliverAt from "./DeliverAt.js" + +/** + * @since 1.0.0 + */ +export * as Entity from "./Entity.js" + +/** + * @since 1.0.0 + */ +export * as EntityAddress from "./EntityAddress.js" + +/** + * @since 1.0.0 + */ +export * as EntityId from "./EntityId.js" + +/** + * @since 1.0.0 + */ +export * as EntityProxy from "./EntityProxy.js" + +/** + * @since 1.0.0 + */ +export * as EntityProxyServer from "./EntityProxyServer.js" + +/** + * @since 1.0.0 + */ +export * as EntityResource from "./EntityResource.js" + +/** + * @since 1.0.0 + */ +export * as EntityType from "./EntityType.js" + +/** + * @since 1.0.0 + */ +export * as Envelope from "./Envelope.js" + +/** + * @since 1.0.0 + */ +export * as HttpRunner from "./HttpRunner.js" + +/** + * @since 1.0.0 + */ +export * as K8sHttpClient from "./K8sHttpClient.js" + +/** + * @since 1.0.0 + */ +export * as MachineId from "./MachineId.js" + +/** + * @since 1.0.0 + */ +export * as Message from "./Message.js" + +/** + * @since 1.0.0 + */ +export * as MessageStorage from "./MessageStorage.js" + +/** + * @since 1.0.0 + */ +export * as Reply from "./Reply.js" + +/** + * @since 1.0.0 + */ +export * as Runner from "./Runner.js" + +/** + * @since 1.0.0 + */ +export * as RunnerAddress from "./RunnerAddress.js" + +/** + * @since 1.0.0 + */ +export * as RunnerHealth from "./RunnerHealth.js" + +/** + * @since 1.0.0 + */ +export * as RunnerServer from "./RunnerServer.js" + +/** + * @since 1.0.0 + */ +export * as RunnerStorage from "./RunnerStorage.js" + +/** + * @since 1.0.0 + */ +export * as Runners from "./Runners.js" + +/** + * @since 1.0.0 + */ +export * as ShardId from "./ShardId.js" + +/** + * @since 1.0.0 + */ +export * as Sharding from "./Sharding.js" + +/** + * @since 1.0.0 + */ +export * as ShardingConfig from "./ShardingConfig.js" + +/** + * @since 1.0.0 + */ +export * as ShardingRegistrationEvent from "./ShardingRegistrationEvent.js" + +/** + * @since 1.0.0 + */ +export * as SingleRunner from "./SingleRunner.js" + +/** + * @since 1.0.0 + */ +export * as Singleton from "./Singleton.js" + +/** + * @since 1.0.0 + */ +export * as SingletonAddress from "./SingletonAddress.js" + +/** + * @since 1.0.0 + */ +export * as Snowflake from "./Snowflake.js" + +/** + * @since 1.0.0 + */ +export * as SocketRunner from "./SocketRunner.js" + +/** + * @since 1.0.0 + */ +export * as SqlMessageStorage from "./SqlMessageStorage.js" + +/** + * @since 1.0.0 + */ +export * as SqlRunnerStorage from "./SqlRunnerStorage.js" + +/** + * @since 1.0.0 + */ +export * as TestRunner from "./TestRunner.js" diff --git a/repos/effect/packages/cluster/src/internal/entityManager.ts b/repos/effect/packages/cluster/src/internal/entityManager.ts new file mode 100644 index 0000000..5744d95 --- /dev/null +++ b/repos/effect/packages/cluster/src/internal/entityManager.ts @@ -0,0 +1,642 @@ +import type * as Rpc from "@effect/rpc/Rpc" +import { RequestId } from "@effect/rpc/RpcMessage" +import * as RpcServer from "@effect/rpc/RpcServer" +import * as Arr from "effect/Array" +import * as Cause from "effect/Cause" +import * as Context from "effect/Context" +import * as Duration from "effect/Duration" +import type { DurationInput } from "effect/Duration" +import * as Effect from "effect/Effect" +import * as Equal from "effect/Equal" +import * as Exit from "effect/Exit" +import type * as Fiber from "effect/Fiber" +import * as FiberRef from "effect/FiberRef" +import { identity } from "effect/Function" +import * as HashMap from "effect/HashMap" +import * as Metric from "effect/Metric" +import * as Option from "effect/Option" +import * as ParseResult from "effect/ParseResult" +import * as Runtime from "effect/Runtime" +import * as Schedule from "effect/Schedule" +import * as Schema from "effect/Schema" +import * as Scope from "effect/Scope" +import { AlreadyProcessingMessage, EntityNotAssignedToRunner, MailboxFull, MalformedMessage } from "../ClusterError.js" +import * as ClusterMetrics from "../ClusterMetrics.js" +import { Persisted, Uninterruptible } from "../ClusterSchema.js" +import type { Entity, HandlersFrom } from "../Entity.js" +import { CurrentAddress, CurrentRunnerAddress, KeepAliveLatch, KeepAliveRpc, Request } from "../Entity.js" +import type { EntityAddress } from "../EntityAddress.js" +import type { EntityId } from "../EntityId.js" +import type * as Envelope from "../Envelope.js" +import * as Message from "../Message.js" +import * as MessageStorage from "../MessageStorage.js" +import * as Reply from "../Reply.js" +import type { RunnerAddress } from "../RunnerAddress.js" +import type { ShardId } from "../ShardId.js" +import type { Sharding } from "../Sharding.js" +import { ShardingConfig } from "../ShardingConfig.js" +import * as Snowflake from "../Snowflake.js" +import { EntityReaper } from "./entityReaper.js" +import { joinAllDiscard } from "./fiber.js" +import { internalInterruptors } from "./interruptors.js" +import { ResourceMap } from "./resourceMap.js" +import { ResourceRef } from "./resourceRef.js" + +/** @internal */ +export interface EntityManager { + readonly sendLocal: ( + message: Message.IncomingLocal + ) => Effect.Effect + + readonly send: ( + message: Message.Incoming + ) => Effect.Effect + + readonly isProcessingFor: (message: Message.Incoming, options?: { + readonly excludeReplies?: boolean + }) => boolean + readonly clearProcessed: () => void + + readonly interruptShard: (shardId: ShardId) => Effect.Effect + + readonly activeEntityCount: Effect.Effect +} + +// Represents the entities managed by this entity manager +/** @internal */ +export type EntityState = { + readonly address: EntityAddress + readonly scope: Scope.Scope + readonly activeRequests: Map + sentReply: boolean + lastSentChunk: Option.Option> + sequence: number + }> + lastActiveCheck: number + write: RpcServer.RpcServer["write"] + readonly keepAliveLatch: Effect.Latch + keepAliveEnabled: boolean +} + +/** @internal */ +export const make = Effect.fnUntraced(function*< + Type extends string, + Rpcs extends Rpc.Any, + Handlers extends HandlersFrom, + RX +>( + entity: Entity, + buildHandlers: Effect.Effect, + options: { + readonly sharding: Sharding["Type"] + readonly storage: MessageStorage.MessageStorage["Type"] + readonly runnerAddress: RunnerAddress + readonly maxIdleTime?: DurationInput | undefined + readonly concurrency?: number | "unbounded" | undefined + readonly mailboxCapacity?: number | "unbounded" | undefined + readonly disableFatalDefects?: boolean | undefined + readonly defectRetryPolicy?: Schedule.Schedule | undefined + readonly spanAttributes?: Record | undefined + } +) { + const config = yield* ShardingConfig + const snowflakeGen = yield* Snowflake.Generator + const managerScope = yield* Effect.scope + const storageEnabled = options.storage !== MessageStorage.noop + const mailboxCapacity = options.mailboxCapacity ?? config.entityMailboxCapacity + const clock = yield* Effect.clock + const context = yield* Effect.context | Rpc.Middleware | RX>() + const retryDriver = yield* Schedule.driver( + options.defectRetryPolicy ? Schedule.andThen(options.defectRetryPolicy, defaultRetryPolicy) : defaultRetryPolicy + ) + const entityRpcs = new Map(entity.protocol.requests) + + // add internal rpcs + entityRpcs.set(KeepAliveRpc._tag, KeepAliveRpc as any) + + const activeServers = new Map() + const serverCloseLatches = new Map() + const processedRequestIds = new Set() + + const entities: ResourceMap< + EntityAddress, + EntityState, + EntityNotAssignedToRunner + > = yield* ResourceMap.make(Effect.fnUntraced(function*(address: EntityAddress) { + if (!options.sharding.hasShardId(address.shardId)) { + return yield* new EntityNotAssignedToRunner({ address }) + } + + const scope = yield* Effect.scope + const endLatch = Effect.unsafeMakeLatch() + const keepAliveLatch = Effect.unsafeMakeLatch(false) + + // on shutdown, reset the storage for the entity + yield* Scope.addFinalizerExit( + scope, + () => { + serverCloseLatches.get(address)?.unsafeOpen() + serverCloseLatches.delete(address) + return Effect.void + } + ) + + const activeRequests: EntityState["activeRequests"] = new Map() + let defectRequestIds: Array = [] + + // the server is stored in a ref, so if there is a defect, we can + // swap the server without losing the active requests + const writeRef = yield* ResourceRef.from( + scope, + Effect.fnUntraced(function*(scope) { + let isShuttingDown = false + + // Initiate the behavior for the entity + const handlers = yield* (entity.protocol.toHandlersContext(buildHandlers).pipe( + Effect.provide(context.pipe( + Context.add(CurrentAddress, address), + Context.add(CurrentRunnerAddress, options.runnerAddress), + Context.add(KeepAliveLatch, keepAliveLatch), + Context.add(Scope.Scope, scope) + )), + Effect.locally(FiberRef.currentLogAnnotations, HashMap.empty()) + ) as Effect.Effect>>) + + const server = yield* RpcServer.makeNoSerialization(entity.protocol, { + spanPrefix: `${entity.type}(${address.entityId})`, + spanAttributes: { + ...options.spanAttributes, + "entity.type": entity.type, + "entity.id": address.entityId + }, + concurrency: options.concurrency ?? 1, + disableFatalDefects: options.disableFatalDefects, + onFromServer(response): Effect.Effect { + switch (response._tag) { + case "Exit": { + const request = activeRequests.get(response.requestId) + if (!request) return Effect.void + + request.sentReply = true + + // For durable messages, ignore interrupts during shutdown. + // They will be retried when the entity is restarted. + // Also, if the request is uninterruptible, we ignore the + // interrupt. + if ( + storageEnabled && + Context.get(request.rpc.annotations, Persisted) && + Exit.isFailure(response.exit) && + Exit.isInterrupted(response.exit) && + (isShuttingDown || Uninterruptible.forServer(request.rpc.annotations)) + ) { + if (!isShuttingDown) { + return server.write(0, { + ...request.message.envelope, + id: RequestId(request.message.envelope.requestId), + tag: request.message.envelope.tag as any, + payload: new Request({ + ...request.message.envelope, + lastSentChunk: request.lastSentChunk + } as any) as any + }).pipe( + Effect.forkIn(scope) + ) + } + activeRequests.delete(response.requestId) + return options.storage.unregisterReplyHandler(request.message.envelope.requestId) + } + return retryRespond( + 4, + Effect.suspend(() => + request.message.respond( + new Reply.WithExit({ + requestId: Snowflake.Snowflake(response.requestId), + id: snowflakeGen.unsafeNext(), + exit: response.exit + }) + ) + ) + ).pipe( + Effect.flatMap(() => { + processedRequestIds.add(request.message.envelope.requestId) + activeRequests.delete(response.requestId) + + // ensure that the reaper does not remove the entity as we haven't + // been "idle" yet + if (activeRequests.size === 0) { + state.lastActiveCheck = clock.unsafeCurrentTimeMillis() + } + + return Effect.void + }), + Effect.orDie + ) + } + case "Chunk": { + const request = activeRequests.get(response.requestId) + if (!request) return Effect.void + const sequence = request.sequence + request.sequence++ + if (!request.sentReply) { + request.sentReply = true + } + return Effect.orDie(retryRespond( + 4, + Effect.suspend(() => { + const reply = new Reply.Chunk({ + requestId: Snowflake.Snowflake(response.requestId), + id: snowflakeGen.unsafeNext(), + sequence, + values: response.values + }) + request.lastSentChunk = Option.some(reply) + return request.message.respond(reply) + }) + )) + } + case "Defect": { + return Effect.forkIn(onDefect(Cause.die(response.defect)), managerScope) + } + case "ClientEnd": { + return endLatch.open + } + } + } + }).pipe( + Scope.extend(scope), + Effect.provide(handlers) + ) + + yield* Scope.addFinalizer( + scope, + Effect.sync(() => { + isShuttingDown = true + }) + ) + + if (defectRequestIds.length > 0) { + for (const id of defectRequestIds) { + const { lastSentChunk, message } = activeRequests.get(id)! + yield* server.write(0, { + ...message.envelope, + id: RequestId(message.envelope.requestId), + tag: message.envelope.tag as any, + payload: new Request({ + ...message.envelope, + lastSentChunk + } as any) as any + }) + } + defectRequestIds = [] + } + + return server.write + }) + ) + + function onDefect(cause: Cause.Cause): Effect.Effect { + if (!activeServers.has(address.entityId)) { + return endLatch.open + } + const effect = writeRef.unsafeRebuild() + defectRequestIds = Array.from(activeRequests.keys()) + return Effect.logError("Defect in entity, restarting", cause).pipe( + Effect.andThen(Effect.ignore(retryDriver.next(void 0))), + Effect.flatMap(() => activeServers.has(address.entityId) ? effect : endLatch.open), + Effect.annotateLogs({ + module: "EntityManager", + address, + runner: options.runnerAddress + }), + Effect.catchAllCause(onDefect) + ) + } + + const state: EntityState = { + scope, + address, + write(clientId, message) { + if (writeRef.state.current._tag !== "Acquired") { + return Effect.flatMap(writeRef.await, (write) => write(clientId, message)) + } + return writeRef.state.current.value(clientId, message) + }, + activeRequests, + lastActiveCheck: clock.unsafeCurrentTimeMillis(), + keepAliveLatch, + keepAliveEnabled: false + } + + // During shutdown, signal that no more messages will be processed + // and wait for the fiber to complete. + // + // If the termination timeout is reached, let the server clean itself up + yield* Scope.addFinalizer( + scope, + Effect.withFiberRuntime((fiber) => { + activeServers.delete(address.entityId) + serverCloseLatches.set(address, Effect.unsafeMakeLatch(false)) + internalInterruptors.add(fiber.id()) + return state.write(0, { _tag: "Eof" }).pipe( + Effect.andThen(Effect.interruptible(endLatch.await)), + Effect.timeoutOption(config.entityTerminationTimeout) + ) + }) + ) + activeServers.set(address.entityId, state) + + return state + }, Effect.locally(FiberRef.currentLogAnnotations, HashMap.empty()))) + + const reaper = yield* EntityReaper + const maxIdleTime = Duration.toMillis(options.maxIdleTime ?? config.entityMaxIdleTime) + if (Number.isFinite(maxIdleTime)) { + yield* reaper.register({ + maxIdleTime, + servers: activeServers, + entities + }) + } + + // update metrics for active servers + const gauge = ClusterMetrics.entities.pipe(Metric.tagged("type", entity.type)) + yield* Effect.sync(() => { + gauge.unsafeUpdate(BigInt(activeServers.size), []) + }).pipe( + Effect.andThen(Effect.sleep(1000)), + Effect.forever, + Effect.forkIn(managerScope) + ) + + function sendLocal( + message: Message.IncomingLocal + ): Effect.Effect { + return Effect.locally( + Effect.flatMap( + entities.get(message.envelope.address), + (server): Effect.Effect => { + switch (message._tag) { + case "IncomingRequestLocal": { + // If the request is already running, then we might have more than + // one sender for the same request. In this case, the other senders + // should resume from storage only. + let entry = server.activeRequests.get(message.envelope.requestId) + if (entry || processedRequestIds.has(message.envelope.requestId)) { + return Effect.fail( + new AlreadyProcessingMessage({ + envelopeId: message.envelope.requestId, + address: message.envelope.address + }) + ) + } + + const rpc = entityRpcs.get(message.envelope.tag)! as any as Rpc.AnyWithProps + if (!storageEnabled && Context.get(rpc.annotations, Persisted)) { + return Effect.dieMessage( + "EntityManager.sendLocal: Cannot process a persisted message without MessageStorage" + ) + } + + // Cluster internal RPCs + + // keep-alive RPC + if (rpc._tag === KeepAliveRpc._tag) { + const msg = message as unknown as Message.IncomingRequestLocal + const reply = Effect.suspend(() => + Effect.orDie(retryRespond( + 4, + msg.respond( + new Reply.WithExit({ + requestId: message.envelope.requestId, + id: snowflakeGen.unsafeNext(), + exit: Exit.void + }) + ) + )) + ) + + if (server.keepAliveEnabled) return reply + server.keepAliveEnabled = true + return server.keepAliveLatch.whenOpen(Effect.suspend(() => { + server.keepAliveEnabled = false + return reply + })).pipe( + Effect.forkIn(server.scope), + Effect.asVoid + ) + } + + if (mailboxCapacity !== "unbounded" && server.activeRequests.size >= mailboxCapacity) { + return Effect.fail(new MailboxFull({ address: message.envelope.address })) + } + + entry = { + rpc, + message, + sentReply: false, + lastSentChunk: message.lastSentReply as any, + sequence: Option.match(message.lastSentReply, { + onNone: () => 0, + onSome: (reply) => reply._tag === "Chunk" ? reply.sequence + 1 : 0 + }) + } + server.activeRequests.set(message.envelope.requestId, entry) + return server.write(0, { + ...message.envelope, + id: RequestId(message.envelope.requestId), + payload: new Request({ + ...message.envelope, + lastSentChunk: message.lastSentReply as any + }) + }) + } + case "IncomingEnvelope": { + const entry = server.activeRequests.get(message.envelope.requestId) + if (!entry) { + return Effect.void + } else if ( + message.envelope._tag === "AckChunk" && + Option.isSome(entry.lastSentChunk) && + message.envelope.replyId !== entry.lastSentChunk.value.id + ) { + return Effect.void + } + return server.write( + 0, + message.envelope._tag === "AckChunk" + ? { _tag: "Ack", requestId: RequestId(message.envelope.requestId) } + : { _tag: "Interrupt", requestId: RequestId(message.envelope.requestId), interruptors: [] } + ) + } + } + } + ), + FiberRef.currentLogAnnotations, + HashMap.empty() + ) + } + + const decodeMessage = makeMessageDecode(entity, entityRpcs) + + const runFork = Runtime.runFork( + yield* Effect.runtime().pipe( + Effect.interruptible + ) + ) + + return identity({ + interruptShard: (shardId: ShardId) => + Effect.suspend(function loop(): Effect.Effect { + const fibers = Arr.empty>() + activeServers.forEach((state) => { + if (shardId[Equal.symbol](state.address.shardId)) { + fibers.push(runFork(entities.removeIgnore(state.address))) + } + }) + serverCloseLatches.forEach((latch, address) => { + if (shardId[Equal.symbol](address.shardId)) { + fibers.push(runFork(latch.await)) + } + }) + if (fibers.length === 0) return Effect.void + return Effect.flatMap(joinAllDiscard(fibers), loop) + }), + isProcessingFor(message, options) { + if (options?.excludeReplies !== true && processedRequestIds.has(message.envelope.requestId)) { + return true + } + const state = activeServers.get(message.envelope.address.entityId) + if (!state) return false + const request = state.activeRequests.get(message.envelope.requestId) + if (request === undefined) { + return false + } else if (options?.excludeReplies && request.sentReply) { + return false + } + return true + }, + clearProcessed() { + processedRequestIds.clear() + }, + sendLocal, + send: (message) => + decodeMessage(message).pipe( + Effect.matchEffect({ + onFailure: (cause) => { + if (message._tag === "IncomingEnvelope") { + return Effect.die(new MalformedMessage({ cause })) + } + return Effect.orDie(message.respond( + new Reply.ReplyWithContext({ + reply: new Reply.WithExit({ + id: snowflakeGen.unsafeNext(), + requestId: message.envelope.requestId, + exit: Exit.die(new MalformedMessage({ cause })) + }), + rpc: entityRpcs.get(message.envelope.tag)!, + context + }) + )) + }, + onSuccess: (decoded) => { + if (decoded._tag === "IncomingEnvelope") { + return sendLocal( + new Message.IncomingEnvelope(decoded) + ) + } + const request = message as Message.IncomingRequest + const rpc = entityRpcs.get(decoded.envelope.tag)! + return sendLocal( + new Message.IncomingRequestLocal({ + envelope: decoded.envelope, + lastSentReply: decoded.lastSentReply, + respond: (reply) => + request.respond( + new Reply.ReplyWithContext({ + reply, + rpc, + context + }) + ) + }) + ) + } + }), + Effect.provide(context as Context.Context) + ), + activeEntityCount: Effect.sync(() => activeServers.size) + }) +}) + +const defaultRetryPolicy = Schedule.exponential(500, 1.5).pipe( + Schedule.union(Schedule.spaced("10 seconds")) +) + +const makeMessageDecode = ( + entity: Entity, + entityRpcs: Map +) => { + const decodeRequest = ( + message: Message.IncomingRequest, + rpc: Rpc.AnyWithProps + ) => { + const payload = Schema.decode(rpc.payloadSchema)(message.envelope.payload) + const lastSentReply = Option.isSome(message.lastSentReply) + ? Effect.asSome(Schema.decode(Reply.Reply(rpc as any))(message.lastSentReply.value)) + : Effect.succeedNone + return Effect.flatMap(payload, (payload) => + Effect.map(lastSentReply, (lastSentReply) => ({ + _tag: "IncomingRequest" as const, + envelope: { + ...message.envelope, + payload + } as Envelope.Request.Any, + lastSentReply + }))) + } + + return (message: Message.Incoming): Effect.Effect< + { + readonly _tag: "IncomingRequest" + readonly envelope: Envelope.Request.Any + readonly lastSentReply: Option.Option> + } | { + readonly _tag: "IncomingEnvelope" + readonly envelope: Envelope.AckChunk | Envelope.Interrupt + }, + ParseResult.ParseError, + Rpc.Context + > => { + if (message._tag === "IncomingEnvelope") { + return Effect.succeed(message) + } + const rpc = entityRpcs.get(message.envelope.tag) as any as Rpc.AnyWithProps + if (!rpc) { + return Effect.fail( + new ParseResult.ParseError({ + issue: new ParseResult.Unexpected( + message, + `Unknown tag ${message.envelope.tag} for entity type ${entity.type}` + ) + }) + ) + } + return decodeRequest(message, rpc) as Effect.Effect< + { + readonly _tag: "IncomingRequest" + readonly envelope: Envelope.Request.Any + readonly lastSentReply: Option.Option> + }, + ParseResult.ParseError, + Rpc.Context + > + } +} + +const retryRespond = (times: number, effect: Effect.Effect): Effect.Effect => + times === 0 ? + effect : + Effect.catchAll(effect, () => Effect.delay(retryRespond(times - 1, effect), 200)) diff --git a/repos/effect/packages/cluster/src/internal/entityReaper.ts b/repos/effect/packages/cluster/src/internal/entityReaper.ts new file mode 100644 index 0000000..9d9479d --- /dev/null +++ b/repos/effect/packages/cluster/src/internal/entityReaper.ts @@ -0,0 +1,53 @@ +import * as Effect from "effect/Effect" +import type { EntityNotAssignedToRunner } from "../ClusterError.js" +import type { EntityAddress } from "../EntityAddress.js" +import type { EntityId } from "../EntityId.js" +import type { EntityState } from "./entityManager.js" +import type { ResourceMap } from "./resourceMap.js" + +/** @internal */ +export class EntityReaper extends Effect.Service()("@effect/cluster/EntityReaper", { + scoped: Effect.gen(function*() { + let currentResolution = 30_000 + const registered: Array<{ + readonly maxIdleTime: number + readonly servers: Map + readonly entities: ResourceMap + }> = [] + const latch = yield* Effect.makeLatch() + + const register = (options: { + readonly maxIdleTime: number + readonly servers: Map + readonly entities: ResourceMap + }) => + Effect.suspend(() => { + currentResolution = Math.max(Math.min(currentResolution, options.maxIdleTime), 5000) + registered.push(options) + return latch.open + }) + + const clock = yield* Effect.clock + yield* Effect.gen(function*() { + while (true) { + yield* Effect.sleep(currentResolution) + const now = clock.unsafeCurrentTimeMillis() + for (const { entities, maxIdleTime, servers } of registered) { + for (const state of servers.values()) { + const duration = now - state.lastActiveCheck + if (state.keepAliveEnabled || state.activeRequests.size > 0 || duration < maxIdleTime) { + continue + } + yield* Effect.fork(entities.removeIgnore(state.address)) + } + } + } + }).pipe( + latch.whenOpen, + Effect.interruptible, + Effect.forkScoped + ) + + return { register } as const + }) +}) {} diff --git a/repos/effect/packages/cluster/src/internal/fiber.ts b/repos/effect/packages/cluster/src/internal/fiber.ts new file mode 100644 index 0000000..2be64d4 --- /dev/null +++ b/repos/effect/packages/cluster/src/internal/fiber.ts @@ -0,0 +1,35 @@ +import * as Cause from "effect/Cause" +import * as Effect from "effect/Effect" +import type * as Exit from "effect/Exit" +import type * as Fiber from "effect/Fiber" + +/** @internal */ +export const joinAllDiscard = (fibers: ReadonlyArray>) => + Effect.async((resume) => { + let cause: Cause.Cause | undefined = undefined + let i = 0 + function loop() { + while (i < fibers.length) { + const fiber = fibers[i] + const exit = fiber.unsafePoll() + if (exit) { + i++ + if (exit._tag === "Success") continue + cause = cause ? Cause.parallel(cause, exit.cause) : exit.cause + continue + } + fiber.addObserver(onExit) + return + } + resume(cause ? Effect.failCause(cause) : Effect.void) + } + function onExit(exit: Exit.Exit) { + i++ + if (exit._tag === "Failure") { + cause = cause ? Cause.parallel(cause, exit.cause) : exit.cause + } + loop() + } + loop() + return Effect.sync(() => fibers[i].removeObserver(onExit)) + }) diff --git a/repos/effect/packages/cluster/src/internal/hash.ts b/repos/effect/packages/cluster/src/internal/hash.ts new file mode 100644 index 0000000..cd30459 --- /dev/null +++ b/repos/effect/packages/cluster/src/internal/hash.ts @@ -0,0 +1,11 @@ +/** @internal */ +export const hashOptimize = (n: number): number => (n & 0xbfffffff) | ((n >>> 1) & 0x40000000) + +/** @internal */ +export const hashString = (str: string) => { + let h = 5381, i = str.length + while (i) { + h = (h * 33) ^ str.charCodeAt(--i) + } + return hashOptimize(h) +} diff --git a/repos/effect/packages/cluster/src/internal/interruptors.ts b/repos/effect/packages/cluster/src/internal/interruptors.ts new file mode 100644 index 0000000..3ef2be8 --- /dev/null +++ b/repos/effect/packages/cluster/src/internal/interruptors.ts @@ -0,0 +1,4 @@ +import type { FiberId } from "effect/FiberId" + +/** @internal */ +export const internalInterruptors = new WeakSet() diff --git a/repos/effect/packages/cluster/src/internal/resourceMap.ts b/repos/effect/packages/cluster/src/internal/resourceMap.ts new file mode 100644 index 0000000..f5bac55 --- /dev/null +++ b/repos/effect/packages/cluster/src/internal/resourceMap.ts @@ -0,0 +1,89 @@ +import * as Context from "effect/Context" +import * as Deferred from "effect/Deferred" +import * as Effect from "effect/Effect" +import * as Exit from "effect/Exit" +import * as MutableHashMap from "effect/MutableHashMap" +import * as MutableRef from "effect/MutableRef" +import * as Option from "effect/Option" +import * as Scope from "effect/Scope" + +export class ResourceMap { + constructor( + readonly lookup: (key: K, scope: Scope.Scope) => Effect.Effect, + readonly entries: MutableHashMap.MutableHashMap + }>, + readonly isClosed: MutableRef.MutableRef + ) {} + + static make = Effect.fnUntraced(function*(lookup: (key: K) => Effect.Effect) { + const scope = yield* Effect.scope + const context = yield* Effect.context() + const isClosed = MutableRef.make(false) + + const entries = MutableHashMap.empty + }>() + + yield* Scope.addFinalizerExit( + scope, + (exit) => { + MutableRef.set(isClosed, true) + return Effect.forEach(entries, ([key, { scope }]) => { + MutableHashMap.remove(entries, key) + return Effect.exit(Scope.close(scope, exit)) + }, { concurrency: "unbounded", discard: true }) + } + ) + + return new ResourceMap( + (key, scope) => Effect.provide(lookup(key), Context.add(context, Scope.Scope, scope)), + entries, + isClosed + ) + }) + + get(key: K): Effect.Effect { + return Effect.withFiberRuntime((fiber) => { + if (MutableRef.get(this.isClosed)) { + return Effect.interrupt + } + const existing = MutableHashMap.get(this.entries, key) + if (Option.isSome(existing)) { + return Deferred.await(existing.value.deferred) + } + const scope = Effect.runSync(Scope.make()) + const deferred = Deferred.unsafeMake(fiber.id()) + MutableHashMap.set(this.entries, key, { scope, deferred }) + return Effect.onExit(this.lookup(key, scope), (exit) => { + if (exit._tag === "Success") { + return Deferred.done(deferred, exit) + } + MutableHashMap.remove(this.entries, key) + return Deferred.done(deferred, exit) + }) + }) + } + + remove(key: K): Effect.Effect { + return Effect.suspend(() => { + const entry = MutableHashMap.get(this.entries, key) + if (Option.isNone(entry)) { + return Effect.void + } + MutableHashMap.remove(this.entries, key) + return Scope.close(entry.value.scope, Exit.void) + }) + } + + removeIgnore(key: K): Effect.Effect { + return Effect.catchAllCause(this.remove(key), (cause) => + Effect.annotateLogs(Effect.logDebug(cause), { + module: "ResourceMap", + method: "removeIgnore", + key + })) + } +} diff --git a/repos/effect/packages/cluster/src/internal/resourceRef.ts b/repos/effect/packages/cluster/src/internal/resourceRef.ts new file mode 100644 index 0000000..7299eac --- /dev/null +++ b/repos/effect/packages/cluster/src/internal/resourceRef.ts @@ -0,0 +1,91 @@ +import * as Effect from "effect/Effect" +import * as Exit from "effect/Exit" +import * as MutableRef from "effect/MutableRef" +import * as Option from "effect/Option" +import * as Scope from "effect/Scope" +import { internalInterruptors } from "./interruptors.js" + +export type State = { + readonly _tag: "Closed" +} | { + readonly _tag: "Acquiring" + readonly scope: Scope.CloseableScope +} | { + readonly _tag: "Acquired" + readonly scope: Scope.CloseableScope + readonly value: A +} + +export class ResourceRef { + static from = Effect.fnUntraced(function*( + parentScope: Scope.Scope, + acquire: (scope: Scope.Scope) => Effect.Effect + ) { + const state = MutableRef.make>({ _tag: "Closed" }) + + yield* Scope.addFinalizerExit(parentScope, (exit) => { + const s = MutableRef.get(state) + if (s._tag === "Closed") { + return Effect.void + } + const scope = s.scope + MutableRef.set(state, { _tag: "Closed" }) + return Scope.close(scope, exit) + }) + + const scope = yield* Scope.make() + MutableRef.set(state, { _tag: "Acquiring", scope }) + const value = yield* acquire(scope) + MutableRef.set(state, { _tag: "Acquired", scope, value }) + + return new ResourceRef(state, acquire) + }) + + constructor( + readonly state: MutableRef.MutableRef>, + readonly acquire: (scope: Scope.Scope) => Effect.Effect + ) {} + + latch = Effect.unsafeMakeLatch(true) + + unsafeGet(): Option.Option { + if (this.state.current._tag === "Acquired") { + return Option.some(this.state.current.value) + } + return Option.none() + } + + unsafeRebuild(): Effect.Effect { + const s = this.state.current + if (s._tag === "Closed") { + return Effect.interrupt + } + const prevScope = s.scope + const scope = Effect.runSync(Scope.make()) + this.latch.unsafeClose() + MutableRef.set(this.state, { _tag: "Acquiring", scope }) + return Effect.fiberIdWith((fiberId) => { + internalInterruptors.add(fiberId) + return Scope.close(prevScope, Exit.void) + }).pipe( + Effect.andThen(this.acquire(scope)), + Effect.flatMap((value) => { + if (this.state.current._tag === "Closed") { + return Effect.interrupt + } + MutableRef.set(this.state, { _tag: "Acquired", scope, value }) + return this.latch.open + }) + ) + } + + await: Effect.Effect = Effect.suspend(() => { + const s = this.state.current + if (s._tag === "Closed") { + return Effect.interrupt + } else if (s._tag === "Acquired") { + return Effect.succeed(s.value) + } + return Effect.zipRight(this.latch.await, this.await) + }) +} diff --git a/repos/effect/packages/cluster/test/ClusterWorkflowEngine.test.ts b/repos/effect/packages/cluster/test/ClusterWorkflowEngine.test.ts new file mode 100644 index 0000000..dc4a931 --- /dev/null +++ b/repos/effect/packages/cluster/test/ClusterWorkflowEngine.test.ts @@ -0,0 +1,563 @@ +import { ClusterWorkflowEngine, MessageStorage, Runners, Sharding, ShardingConfig } from "@effect/cluster" +import { assert, describe, expect, it } from "@effect/vitest" +import { Activity, DurableClock, DurableDeferred, Workflow } from "@effect/workflow" +import { WorkflowInstance } from "@effect/workflow/WorkflowEngine" +import { DateTime, Effect, Exit, Fiber, Layer, Schema, TestClock } from "effect" +import * as Cause from "effect/Cause" +import * as Duration from "effect/Duration" +import * as RunnerHealth from "../src/RunnerHealth.js" +import * as RunnerStorage from "../src/RunnerStorage.js" + +describe.concurrent("ClusterWorkflowEngine", () => { + it.effect("should run a workflow", () => + Effect.gen(function*() { + const sharding = yield* Sharding.Sharding + const driver = yield* MessageStorage.MemoryDriver + const flags = yield* Flags + + const fiber = yield* EmailWorkflow.execute({ + id: "test-email-1", + to: "bob@example.com" + }).pipe(Effect.fork) + + yield* TestClock.adjust(1) + // resume after the clock + yield* TestClock.adjust("10 seconds") + yield* sharding.pollStorage + yield* TestClock.adjust(5000) + + // --- the workflow is suspended at this point + + // - 1 initial request + // - 5 attempts to send email + // - 1 sleep activity + // - 1 durable clock run + // - 1 durable clock deferred set + expect(driver.requests.size).toEqual(9) + const executionId = driver.journal[0].address.entityId + + // normal finalizer should run even after suspension + expect(flags.get("finalizer")).toBeTruthy() + // but not compensation + expect(flags.get("compensation")).toBeFalsy() + // ensuring will run + expect(flags.get("ensuring")).toBeTruthy() + expect(flags.get("catchAllCause")).toBeFalsy() + + // --- resume the workflow using DurableDeferred.done + + const token = yield* DurableDeferred.token(EmailTrigger).pipe( + Effect.provideService(WorkflowInstance, WorkflowInstance.initial(EmailWorkflow, executionId)) + ) + yield* DurableDeferred.done(EmailTrigger, { + token, + exit: Exit.succeed("done") + }) + yield* sharding.pollStorage + + // - 1 DurableDeferred set + expect(driver.requests.size).toEqual(10) + + // allow suspend polling to complete + yield* TestClock.adjust(10000) + expect(yield* Fiber.join(fiber)).toBeUndefined() + + // --- the workflow is complete + + // ensuring will run + expect(flags.get("ensuring")).toBeTruthy() + expect(flags.get("catchAllCause")).toBeFalsy() + + // test deduplication + yield* EmailWorkflow.execute({ + id: "test-email-1", + to: "bob@example.com" + }) + expect(driver.requests.size).toEqual(10) + + // test poll + expect(yield* EmailWorkflow.poll(executionId)).toEqual(new Workflow.Complete({ exit: Exit.void })) + }).pipe(Effect.provide(TestWorkflowLayer))) + + it.effect("interrupt", () => + Effect.gen(function*() { + const sharding = yield* Sharding.Sharding + const driver = yield* MessageStorage.MemoryDriver + yield* TestClock.adjust(1) + + const fiber = yield* EmailWorkflow.execute({ + id: "test-email-2", + to: "bob@example.com" + }).pipe(Effect.fork) + + yield* TestClock.adjust(1) + yield* TestClock.adjust("10 seconds") + yield* sharding.pollStorage + yield* TestClock.adjust(1) + + const envelope = driver.journal[0] + const executionId = envelope.address.entityId + yield* EmailWorkflow.interrupt(executionId) + + // - 1 initial request + // - 5 attempts to send email + // - 1 sleep activity + // - 1 durable clock run + // - 1 durable clock deferred set + // - 1 interrupt signal set + expect(driver.requests.size).toEqual(10) + yield* TestClock.adjust(5000) + yield* sharding.pollStorage + yield* TestClock.adjust(5000) + // - clock cleared + expect(driver.requests.size).toEqual(9) + + const result = driver.requests.get(envelope.requestId)! + const reply = result.replies[0]! + assert( + reply._tag === "WithExit" && + reply.exit._tag === "Success" + ) + const value = reply.exit.value as Workflow.ResultEncoded + assert(value._tag === "Complete" && value.exit._tag === "Failure") + + const exit = yield* Fiber.await(fiber) + assert(Exit.isInterrupted(exit)) + + const flags = yield* Flags + assert.isTrue(flags.get("compensation")) + }).pipe( + Effect.provide(TestWorkflowLayer) + )) + + it.effect("Workflow.withCompensation", () => + Effect.gen(function*() { + yield* TestClock.adjust(1) + + const fiber = yield* EmailWorkflow.execute({ + id: "test-email-3", + to: "compensation" + }).pipe(Effect.fork) + + yield* TestClock.adjust(1) + + const flags = yield* Flags + assert.isTrue(flags.get("compensation")) + + const error = yield* Fiber.join(fiber).pipe( + Effect.flip + ) + expect(error).toBeInstanceOf(SendEmailError) + }).pipe( + Effect.provide(TestWorkflowLayer) + )) + + it.effect("Activity.raceAll", () => + Effect.gen(function*() { + const flags = yield* Flags + yield* TestClock.adjust(1) + + const fiber = yield* RaceWorkflow.execute({ + id: "race-1" + }).pipe(Effect.fork) + + yield* TestClock.adjust(1) + yield* TestClock.adjust(1000) + + const result = yield* Fiber.join(fiber) + expect(result).toEqual("Activity3") + + expect(flags.get("interrupt1")).toBeTruthy() + expect(flags.get("interrupt2")).toBeTruthy() + expect(flags.get("interrupt3")).toBeFalsy() + }).pipe(Effect.provide(TestWorkflowLayer))) + + it.effect("Activity.raceAll durable", () => + Effect.gen(function*() { + const sharding = yield* Sharding.Sharding + yield* TestClock.adjust(1) + + const fiber = yield* DurableRaceWorkflow.execute({ + id: "race-2" + }).pipe(Effect.fork) + + yield* TestClock.adjust(1) + yield* TestClock.adjust(1000) + yield* sharding.pollStorage + yield* TestClock.adjust(5000) + + const result = yield* Fiber.join(fiber) + expect(result).toEqual("Activity3") + }).pipe(Effect.provide(TestWorkflowLayer))) + + it.effect("nested workflows", () => + Effect.gen(function*() { + const flags = yield* Flags + const sharding = yield* Sharding.Sharding + yield* TestClock.adjust(1) + + yield* ParentWorkflow.execute({ + id: "123" + }).pipe(Effect.fork) + yield* TestClock.adjust(1) + yield* TestClock.adjust(5000) + + assert.isUndefined(flags.get("parent-end")) + assert.isUndefined(flags.get("child-end")) + assert.isTrue(flags.get("parent-suspended")) + const token = flags.get("child-token") + assert(typeof token === "string") + + yield* DurableDeferred.done(ChildDeferred, { + token: DurableDeferred.Token.make(token), + exit: Exit.void + }) + yield* sharding.pollStorage + yield* TestClock.adjust(5000) + assert.isTrue(flags.get("parent-end")) + assert.isTrue(flags.get("child-end")) + }).pipe(Effect.provide(TestWorkflowLayer))) + + it.effect("SuspendOnFailure", () => + Effect.gen(function*() { + const flags = yield* Flags + yield* TestClock.adjust(1) + + yield* SuspendOnFailureWorkflow.execute({ + id: "" + }).pipe(Effect.fork) + yield* TestClock.adjust(1) + + assert.isTrue(flags.get("suspended")) + assert.include(flags.get("cause"), "boom") + }).pipe(Effect.provide(TestWorkflowLayer))) + + it.effect("catchAllCause activity", () => + Effect.gen(function*() { + const flags = yield* Flags + yield* TestClock.adjust(1) + + const fiber = yield* CatchWorkflow.execute({ + id: "" + }).pipe(Effect.fork) + yield* TestClock.adjust(1) + yield* Fiber.join(fiber) + + assert.isTrue(flags.get("catch")) + }).pipe(Effect.provide(TestWorkflowLayer))) +}) + +const TestShardingConfig = ShardingConfig.layer({ + shardsPerGroup: 300, + entityMailboxCapacity: 10, + entityTerminationTimeout: 0, + entityMessagePollInterval: 5000, + sendRetryInterval: 100 +}) + +const TestWorkflowEngine = ClusterWorkflowEngine.layer.pipe( + Layer.provideMerge(Sharding.layer), + Layer.provide(Runners.layerNoop), + Layer.provideMerge(MessageStorage.layerMemory), + Layer.provide(RunnerStorage.layerMemory), + Layer.provide(RunnerHealth.layerNoop), + Layer.provide(TestShardingConfig) +) + +class SendEmailError extends Schema.TaggedError("SendEmailError")("SendEmailError", { + message: Schema.String +}) {} + +const EmailWorkflow = Workflow.make({ + name: "EmailWorkflow", + payload: { + to: Schema.String, + id: Schema.String + }, + error: SendEmailError, + idempotencyKey(payload) { + return payload.id + } +}) + +class Flags extends Effect.Service()("Flags", { + sync: () => new Map() +}) {} + +const EmailWorkflowLayer = EmailWorkflow.toLayer(Effect.fn(function*(payload) { + const flags = yield* Flags + + yield* Effect.addFinalizer(() => + Effect.sync(() => { + flags.set("finalizer", true) + }) + ) + + yield* Activity.make({ + name: "SendEmail", + error: SendEmailError, + execute: Effect.gen(function*() { + const attempt = yield* Activity.CurrentAttempt + + if (attempt !== 5) { + return yield* new SendEmailError({ + message: `Failed to send email for ${payload.id} on attempt ${attempt}` + }) + } + }) + }).pipe( + EmailWorkflow.withCompensation(Effect.fnUntraced(function*() { + flags.set("compensation", true) + })), + Activity.retry({ times: 5 }) + ) + + if (payload.to === "compensation") { + return yield* new SendEmailError({ message: `Compensation triggered` }) + } + + const result = yield* Activity.make({ + name: "Sleep", + success: Schema.DateTimeUtc, + execute: Effect.gen(function*() { + // suspended inside Activity + yield* DurableClock.sleep({ + name: "Some sleep", + duration: "10 seconds", + inMemoryThreshold: Duration.zero + }) + return yield* DateTime.now + }) + }) + // test serialization from Activity + assert(DateTime.isUtc(result)) + + yield* DurableDeferred.token(EmailTrigger) + // suspended outside Activity + yield* DurableDeferred.await(EmailTrigger).pipe( + Effect.catchAllCause(() => { + flags.set("catchAllCause", true) + return Effect.void + }), + Effect.ensuring(Effect.sync(() => { + flags.set("ensuring", true) + })) + ) +})).pipe( + Layer.provideMerge(Flags.Default) +) + +const EmailTrigger = DurableDeferred.make("EmailTrigger", { + success: Schema.String +}) + +const RaceWorkflow = Workflow.make({ + name: "RaceWorkflow", + payload: { + id: Schema.String + }, + success: Schema.String, + idempotencyKey: ({ id }) => id +}) + +const RaceWorkflowLayer = RaceWorkflow.toLayer(Effect.fnUntraced(function*() { + const flags = yield* Flags + + yield* Effect.addFinalizer(() => + Effect.sync(() => { + flags.set("finalizer", true) + }) + ) + + return yield* Activity.raceAll("race", [ + Activity.make({ + name: "Activity1", + success: Schema.String, + error: Schema.Never, + execute: Effect.onInterrupt(Effect.delay(Effect.succeed("Activity1"), 1000), () => + Effect.sync(() => { + flags.set("interrupt1", true) + })) + }), + Activity.make({ + name: "Activity2", + success: Schema.String, + error: Schema.Never, + execute: Effect.onInterrupt(Effect.delay(Effect.succeed("Activity2"), 500), () => + Effect.sync(() => { + flags.set("interrupt2", true) + })) + }), + Activity.make({ + name: "Activity3", + success: Schema.String, + error: Schema.Never, + execute: Effect.onInterrupt(Effect.delay(Effect.succeed("Activity3"), 100), () => + Effect.sync(() => { + flags.set("interrupt3", true) + })) + }) + ]) +})) + +const DurableRaceWorkflow = Workflow.make({ + name: "DurableRaceWorkflow", + payload: { + id: Schema.String + }, + success: Schema.String, + idempotencyKey: ({ id }) => id +}) + +const DurableRaceWorkflowLayer = DurableRaceWorkflow.toLayer(Effect.fnUntraced(function*() { + const flags = yield* Flags + + yield* Effect.addFinalizer(() => + Effect.sync(() => { + flags.set("finalizer", true) + }) + ) + + return yield* Activity.raceAll("race", [ + Activity.make({ + name: "Activity1", + success: Schema.String, + error: Schema.Never, + execute: DurableClock.sleep({ + name: "Activity1", + duration: 50000, + inMemoryThreshold: Duration.zero + }).pipe( + Effect.as("Activity1") + ) + }), + Activity.make({ + name: "Activity2", + success: Schema.String, + error: Schema.Never, + execute: DurableClock.sleep({ + name: "Activity2", + duration: 10000, + inMemoryThreshold: Duration.zero + }).pipe( + Effect.as("Activity2") + ) + }), + Activity.make({ + name: "Activity3", + success: Schema.String, + error: Schema.Never, + execute: DurableClock.sleep({ + name: "Activity3", + duration: 1000, + inMemoryThreshold: Duration.zero + }).pipe( + Effect.as("Activity3") + ) + }) + ]) +})) + +const ParentWorkflow = Workflow.make({ + name: "ParentWorkflow", + payload: { + id: Schema.String + }, + idempotencyKey(payload) { + return payload.id + } +}) + +const ChildWorkflow = Workflow.make({ + name: "ChildWorkflow", + payload: { + id: Schema.String + }, + idempotencyKey(payload) { + return payload.id + } +}) + +const ParentWorkflowLayer = ParentWorkflow.toLayer(Effect.fnUntraced(function*({ id }) { + const flags = yield* Flags + const instance = yield* WorkflowInstance + yield* Effect.addFinalizer(() => + Effect.sync(() => { + flags.set("parent-suspended", instance.suspended) + }) + ) + yield* ChildWorkflow.execute({ id }) + flags.set("parent-end", true) +})) + +const ChildDeferred = DurableDeferred.make("ChildDeferred") +const ChildWorkflowLayer = ChildWorkflow.toLayer(Effect.fnUntraced(function*() { + const flags = yield* Flags + flags.set("child-token", yield* DurableDeferred.token(ChildDeferred)) + yield* DurableDeferred.await(ChildDeferred) + flags.set("child-end", true) +})) + +const SuspendOnFailureWorkflow = Workflow.make({ + name: "SuspendOnFailureWorkflow", + payload: { + id: Schema.String + }, + idempotencyKey(payload) { + return payload.id + } +}).annotate(Workflow.SuspendOnFailure, true) + +const SuspendOnFailureWorkflowLayer = SuspendOnFailureWorkflow.toLayer(Effect.fnUntraced(function*() { + const flags = yield* Flags + const instance = yield* WorkflowInstance + yield* Effect.addFinalizer(() => + Effect.sync(() => { + flags.set("suspended", instance.suspended) + flags.set("cause", Cause.pretty(instance.cause!)) + }) + ) + yield* Activity.make({ + name: "fail", + execute: Effect.die("boom") + }) +})) + +const CatchWorkflow = Workflow.make({ + name: "CatchWorkflow", + payload: { + id: Schema.String + }, + idempotencyKey(payload) { + return payload.id + } +}) + +const CatchWorkflowLayer = CatchWorkflow.toLayer(Effect.fnUntraced(function*() { + const flags = yield* Flags + yield* Activity.make({ + name: "fail", + execute: Effect.die("boom") + }).pipe( + Effect.catchAllCause((cause) => + Activity.make({ + name: "log", + execute: Effect.suspend(() => { + flags.set("catch", true) + return Effect.log(cause) + }) + }) + ) + ) +})) + +const TestWorkflowLayer = EmailWorkflowLayer.pipe( + Layer.merge(RaceWorkflowLayer), + Layer.merge(DurableRaceWorkflowLayer), + Layer.merge(ParentWorkflowLayer), + Layer.merge(ChildWorkflowLayer), + Layer.merge(SuspendOnFailureWorkflowLayer), + Layer.merge(CatchWorkflowLayer), + Layer.provideMerge(Flags.Default), + Layer.provideMerge(TestWorkflowEngine) +) diff --git a/repos/effect/packages/cluster/test/Entity.test.ts b/repos/effect/packages/cluster/test/Entity.test.ts new file mode 100644 index 0000000..d0b1a69 --- /dev/null +++ b/repos/effect/packages/cluster/test/Entity.test.ts @@ -0,0 +1,24 @@ +import { Entity, ShardingConfig } from "@effect/cluster" +import { assert, describe, it } from "@effect/vitest" +import { Effect } from "effect" +import { TestEntity, TestEntityLayer, User } from "./TestEntity.js" + +describe.concurrent("Entity", () => { + describe("makeTestClient", () => { + it.scoped("round trip", () => + Effect.gen(function*() { + const makeClient = yield* Entity.makeTestClient(TestEntity, TestEntityLayer) + const client = yield* makeClient("123") + const user = yield* client.GetUser({ id: 1 }) + assert.deepEqual(user, new User({ id: 1, name: "User 1" })) + }).pipe(Effect.provide(TestShardingConfig))) + }) +}) + +const TestShardingConfig = ShardingConfig.layer({ + shardsPerGroup: 300, + entityMailboxCapacity: 10, + entityTerminationTimeout: 0, + entityMessagePollInterval: 5000, + sendRetryInterval: 100 +}) diff --git a/repos/effect/packages/cluster/test/HttpRunner.test.ts b/repos/effect/packages/cluster/test/HttpRunner.test.ts new file mode 100644 index 0000000..4ee282b --- /dev/null +++ b/repos/effect/packages/cluster/test/HttpRunner.test.ts @@ -0,0 +1,137 @@ +import * as HttpClient from "@effect/platform/HttpClient" +import * as HttpClientError from "@effect/platform/HttpClientError" +import * as HttpLayerRouter from "@effect/platform/HttpLayerRouter" +import { RpcSerialization } from "@effect/rpc" +import { describe, expect, it } from "@effect/vitest" +import { Effect, Layer, Ref } from "effect" +import * as HttpRunner from "../src/HttpRunner.js" +import { MessageStorage, RunnerAddress, RunnerHealth, Runners, RunnerStorage, ShardingConfig } from "../src/index.js" + +describe("HttpRunner", () => { + describe("layerClientProtocolHttp", () => { + const makeUrlCapturingClient = (urlRef: Ref.Ref>) => + HttpClient.make((request, url) => + Ref.update(urlRef, (urls) => [...urls, url.toString()]).pipe( + Effect.flatMap(() => + Effect.fail( + new HttpClientError.RequestError({ + request, + reason: "Transport", + cause: new Error("Mock - URL captured") + }) + ) + ) + ) + ) + + const testRequest = { + _tag: "Request" as const, + id: "1", + tag: "test", + payload: {}, + headers: [] as ReadonlyArray<[string, string]> + } + + it.scoped("path '/' produces http://host:port/", () => + Effect.gen(function*() { + const urlRef = yield* Ref.make>([]) + + const layer = HttpRunner.layerClientProtocolHttp({ path: "/" }).pipe( + Layer.provide(Layer.succeed(HttpClient.HttpClient, makeUrlCapturingClient(urlRef))), + Layer.provide(RpcSerialization.layerNdjson) + ) + + const makeProtocol = yield* Effect.provide(Runners.RpcClientProtocol, layer) + const protocol = yield* makeProtocol(RunnerAddress.make("localhost", 3000)) + + yield* protocol.send(testRequest).pipe(Effect.ignore) + + const urls = yield* Ref.get(urlRef) + expect(urls[0]).toBe("http://localhost:3000/") + })) + + it.scoped("path '' produces http://host:port/", () => + Effect.gen(function*() { + const urlRef = yield* Ref.make>([]) + + const layer = HttpRunner.layerClientProtocolHttp({ path: "" }).pipe( + Layer.provide(Layer.succeed(HttpClient.HttpClient, makeUrlCapturingClient(urlRef))), + Layer.provide(RpcSerialization.layerNdjson) + ) + + const makeProtocol = yield* Effect.provide(Runners.RpcClientProtocol, layer) + const protocol = yield* makeProtocol(RunnerAddress.make("localhost", 3000)) + + yield* protocol.send(testRequest).pipe(Effect.ignore) + + const urls = yield* Ref.get(urlRef) + expect(urls[0]).toBe("http://localhost:3000/") + })) + + it.scoped("path '/rpc' produces http://host:port/rpc", () => + Effect.gen(function*() { + const urlRef = yield* Ref.make>([]) + + const layer = HttpRunner.layerClientProtocolHttp({ path: "/rpc" }).pipe( + Layer.provide(Layer.succeed(HttpClient.HttpClient, makeUrlCapturingClient(urlRef))), + Layer.provide(RpcSerialization.layerNdjson) + ) + + const makeProtocol = yield* Effect.provide(Runners.RpcClientProtocol, layer) + const protocol = yield* makeProtocol(RunnerAddress.make("localhost", 3000)) + + yield* protocol.send(testRequest).pipe(Effect.ignore) + + const urls = yield* Ref.get(urlRef) + expect(urls[0]).toBe("http://localhost:3000/rpc") + })) + + it.scoped("path 'rpc' produces http://host:port/rpc", () => + Effect.gen(function*() { + const urlRef = yield* Ref.make>([]) + + const layer = HttpRunner.layerClientProtocolHttp({ path: "rpc" }).pipe( + Layer.provide(Layer.succeed(HttpClient.HttpClient, makeUrlCapturingClient(urlRef))), + Layer.provide(RpcSerialization.layerNdjson) + ) + + const makeProtocol = yield* Effect.provide(Runners.RpcClientProtocol, layer) + const protocol = yield* makeProtocol(RunnerAddress.make("localhost", 3000)) + + yield* protocol.send(testRequest).pipe(Effect.ignore) + + const urls = yield* Ref.get(urlRef) + expect(urls[0]).toBe("http://localhost:3000/rpc") + })) + }) + + describe("layerHttpOptions", () => { + const deps = Layer.mergeAll( + RunnerStorage.layerMemory, + RunnerHealth.layerNoop, + MessageStorage.layerNoop, + ShardingConfig.layerDefaults, + RpcSerialization.layerNdjson, + Layer.succeed(Runners.RpcClientProtocol, () => Effect.die("mock")) + ) + + it.scoped("registers route on HttpLayerRouter so POST / is not 404", () => + Effect.gen(function*() { + const { dispose, handler } = HttpLayerRouter.toWebHandler( + HttpRunner.layerHttpOptions({ path: "/" }).pipe(Layer.provide(deps)) + ) + yield* Effect.addFinalizer(() => Effect.promise(() => dispose())) + + const response = yield* Effect.promise(() => + handler( + new Request("http://localhost/", { + method: "POST", + headers: { "content-type": "application/octet-stream" }, + body: new Uint8Array([]) + }) + ) + ) + expect(response.status).not.toBe(404) + })) + }) +}) diff --git a/repos/effect/packages/cluster/test/K8sHttpClient.test.ts b/repos/effect/packages/cluster/test/K8sHttpClient.test.ts new file mode 100644 index 0000000..3c633bc --- /dev/null +++ b/repos/effect/packages/cluster/test/K8sHttpClient.test.ts @@ -0,0 +1,34 @@ +import { assert, describe, it } from "@effect/vitest" +import { Effect, Schema } from "effect" +import * as K8sHttpClient from "../src/K8sHttpClient.js" + +describe("K8sHttpClient", () => { + describe("Pod", () => { + it.effect("decodes null lastTransitionTime values", () => + Effect.sync(() => { + const pod = Schema.decodeSync(K8sHttpClient.Pod)({ + status: { + phase: "Running", + conditions: [ + { + type: "Initialized", + status: "True", + lastTransitionTime: null + }, + { + type: "Ready", + status: "False", + lastTransitionTime: null + } + ], + podIP: "10.0.0.1", + hostIP: "10.0.0.2" + } + }) + + assert.strictEqual(pod.status.conditions[0]?.lastTransitionTime, null) + assert.isFalse(pod.isReady) + assert.isTrue(pod.isReadyOrInitializing) + })) + }) +}) diff --git a/repos/effect/packages/cluster/test/MessageStorage.test.ts b/repos/effect/packages/cluster/test/MessageStorage.test.ts new file mode 100644 index 0000000..2070393 --- /dev/null +++ b/repos/effect/packages/cluster/test/MessageStorage.test.ts @@ -0,0 +1,252 @@ +import { + EntityAddress, + EntityId, + EntityType, + Envelope, + Message, + MessageStorage, + Reply, + ShardId, + ShardingConfig, + Snowflake +} from "@effect/cluster" +import { Headers } from "@effect/platform" +import { Rpc, RpcSchema } from "@effect/rpc" +import { describe, expect, it } from "@effect/vitest" +import { Context, Effect, Exit, Layer, Option, PrimaryKey, Schema } from "effect" +import * as TestClock from "effect/TestClock" + +const MemoryLive = MessageStorage.layerMemory.pipe( + Layer.provideMerge(Snowflake.layerGenerator), + Layer.provide(ShardingConfig.layerDefaults) +) + +describe("MessageStorage", () => { + describe("memory", () => { + it.effect("saves a request", () => + Effect.gen(function*() { + const storage = yield* MessageStorage.MessageStorage + const request = yield* makeRequest() + const result = yield* storage.saveRequest(request) + expect(result._tag).toEqual("Success") + const messages = yield* storage.unprocessedMessages([request.envelope.address.shardId]) + expect(messages).toHaveLength(1) + }).pipe(Effect.provide(MemoryLive))) + + it.effect("detects duplicates", () => + Effect.gen(function*() { + const storage = yield* MessageStorage.MessageStorage + yield* storage.saveRequest( + yield* makeRequest({ + rpc: Rpc.fromTaggedRequest(PrimaryKeyTest), + payload: new PrimaryKeyTest({ id: 123 }) + }) + ) + const result = yield* storage.saveRequest( + yield* makeRequest({ + rpc: Rpc.fromTaggedRequest(PrimaryKeyTest), + payload: new PrimaryKeyTest({ id: 123 }) + }) + ) + expect(result._tag).toEqual("Duplicate") + }).pipe(Effect.provide(MemoryLive))) + + it.effect("unprocessedMessages excludes complete requests", () => + Effect.gen(function*() { + const storage = yield* MessageStorage.MessageStorage + const request = yield* makeRequest() + yield* storage.saveRequest(request) + yield* storage.saveReply(yield* makeReply(request)) + const messages = yield* storage.unprocessedMessages([request.envelope.address.shardId]) + expect(messages).toHaveLength(0) + }).pipe(Effect.provide(MemoryLive))) + + it.effect("repliesFor", () => + Effect.gen(function*() { + const storage = yield* MessageStorage.MessageStorage + const request = yield* makeRequest() + yield* storage.saveRequest(request) + let replies = yield* storage.repliesFor([request]) + expect(replies).toHaveLength(0) + yield* storage.saveReply(yield* makeReply(request)) + replies = yield* storage.repliesFor([request]) + expect(replies).toHaveLength(1) + expect(replies[0].requestId).toEqual(request.envelope.requestId) + }).pipe(Effect.provide(MemoryLive))) + + it.effect("registerReplyHandler", () => + Effect.gen(function*() { + const storage = yield* MessageStorage.MessageStorage + const latch = yield* Effect.makeLatch() + const request = yield* makeRequest() + yield* storage.saveRequest(request) + const fiber = yield* storage.registerReplyHandler( + new Message.OutgoingRequest({ + ...request, + respond: () => latch.open + }) + ).pipe(Effect.fork) + yield* TestClock.adjust(1) + yield* storage.saveReply(yield* makeReply(request)) + yield* latch.await + yield* fiber.await + }).pipe(Effect.provide(MemoryLive))) + + it.effect("unregisterReplyHandler", () => + Effect.gen(function*() { + const storage = yield* MessageStorage.MessageStorage + const request = yield* makeRequest() + yield* storage.saveRequest(request) + const fiber = yield* storage.registerReplyHandler( + new Message.OutgoingRequest({ + ...request, + respond: () => Effect.void + }) + ).pipe(Effect.fork) + yield* TestClock.adjust(1) + yield* storage.unregisterReplyHandler(request.envelope.requestId) + yield* fiber.await + }).pipe(Effect.provide(MemoryLive))) + }) + + describe("makeEncoded", () => { + it.effect("guards empty id lists before delegating", () => + Effect.gen(function*() { + const encoded = { + saveEnvelope: () => Effect.succeed(MessageStorage.SaveResultEncoded.Success()), + saveReply: () => Effect.void, + clearReplies: () => Effect.void, + requestIdForPrimaryKey: () => Effect.succeed(Option.none()), + repliesFor: () => Effect.succeed([]), + repliesForUnfiltered: () => Effect.die("unexpected repliesForUnfiltered call"), + unprocessedMessages: () => Effect.succeed([]), + unprocessedMessagesById: () => Effect.succeed([]), + resetAddress: () => Effect.void, + clearAddress: () => Effect.void, + resetShards: () => Effect.die("unexpected resetShards call") + } + + const storage = yield* MessageStorage.makeEncoded(encoded).pipe( + Effect.provide(Snowflake.layerGenerator) + ) + + const replies = yield* storage.repliesForUnfiltered([]) + expect(replies).toEqual([]) + + yield* storage.resetShards([]) + })) + }) +}) + +export const GetUserRpc = Rpc.make("GetUser", { + payload: { id: Schema.Number } +}) + +export const makeRequest = Effect.fnUntraced(function*(options?: { + readonly rpc?: Rpc.AnyWithProps + readonly payload?: any +}) { + const snowflake = yield* Snowflake.Generator + const rpc = options?.rpc ?? GetUserRpc + return new Message.OutgoingRequest({ + envelope: Envelope.makeRequest({ + requestId: snowflake.unsafeNext(), + address: EntityAddress.EntityAddress.make({ + shardId: ShardId.make("default", 1), + entityType: EntityType.EntityType.make("test"), + entityId: EntityId.EntityId.make("1") + }), + tag: rpc._tag, + payload: options?.payload ?? { id: 123 }, + traceId: "noop", + spanId: "noop", + sampled: false, + headers: Headers.empty + }), + context: Context.empty() as any, + rpc, + lastReceivedReply: Option.none(), + respond() { + return Effect.void + } + }) +}) + +export class PrimaryKeyTest extends Schema.TaggedRequest()("PrimaryKeyTest", { + success: Schema.Void, + failure: Schema.Never, + payload: { + id: Schema.Number + } +}) { + [PrimaryKey.symbol]() { + return this.id.toString() + } +} + +export class StreamTest extends Schema.TaggedRequest()("StreamTest", { + success: RpcSchema.Stream({ + success: Schema.Void, + failure: Schema.Never + }), + failure: Schema.Never, + payload: { + id: Schema.Number + } +}) { + [PrimaryKey.symbol]() { + return this.id.toString() + } +} +export const StreamRpc = Rpc.fromTaggedRequest(StreamTest) + +export const makeReply = Effect.fnUntraced(function*(request: Message.OutgoingRequest) { + const snowflake = yield* Snowflake.Generator + return new Reply.ReplyWithContext({ + reply: new Reply.WithExit({ + id: snowflake.unsafeNext(), + requestId: request.envelope.requestId, + exit: Exit.void as any + }), + context: request.context, + rpc: request.rpc + }) +}) + +export const makeAckChunk = Effect.fnUntraced(function*( + request: Message.OutgoingRequest, + chunk: Reply.ReplyWithContext +) { + const snowflake = yield* Snowflake.Generator + return new Message.OutgoingEnvelope({ + envelope: new Envelope.AckChunk({ + id: snowflake.unsafeNext(), + address: request.envelope.address, + requestId: chunk.reply.requestId, + replyId: chunk.reply.id + }), + rpc: request.rpc + }) +}) + +export const makeChunkReply = Effect.fnUntraced(function*(request: Message.OutgoingRequest, sequence = 0) { + const snowflake = yield* Snowflake.Generator + return new Reply.ReplyWithContext({ + reply: new Reply.Chunk({ + id: snowflake.unsafeNext(), + requestId: request.envelope.requestId, + sequence, + values: [undefined] + }), + context: request.context, + rpc: request.rpc + }) +}) + +export const makeEmptyReply = (request: Message.OutgoingRequest) => { + return new Reply.ReplyWithContext({ + reply: Reply.Chunk.emptyFrom(request.envelope.requestId), + context: request.context, + rpc: request.rpc + }) +} diff --git a/repos/effect/packages/cluster/test/Sharding.test.ts b/repos/effect/packages/cluster/test/Sharding.test.ts new file mode 100644 index 0000000..bcde4d7 --- /dev/null +++ b/repos/effect/packages/cluster/test/Sharding.test.ts @@ -0,0 +1,543 @@ +import { + MessageStorage, + RunnerAddress, + Runners, + RunnerStorage, + Sharding, + ShardingConfig, + Snowflake +} from "@effect/cluster" +import { assert, describe, expect, it } from "@effect/vitest" +import { + Array, + Cause, + Chunk, + Effect, + Exit, + Fiber, + FiberId, + Layer, + Mailbox, + MutableRef, + Option, + Stream, + TestClock +} from "effect" +import * as RunnerHealth from "../src/RunnerHealth.js" +import { TestEntity, TestEntityNoState, TestEntityState, User } from "./TestEntity.js" + +describe.concurrent("Sharding", () => { + it.scoped("delivers a message", () => + Effect.gen(function*() { + yield* TestClock.adjust(1) + const makeClient = yield* TestEntity.client + const client = makeClient("1") + const user = yield* client.GetUserVolatile({ id: 1 }) + expect(user).toEqual(new User({ id: 1, name: "User 1" })) + }).pipe(Effect.provide(TestSharding))) + + it.scoped("delivers a message via storage", () => + Effect.gen(function*() { + yield* TestClock.adjust(1) + const driver = yield* MessageStorage.MemoryDriver + const makeClient = yield* TestEntity.client + const client = makeClient("1") + const user = yield* client.GetUser({ id: 1 }) + expect(user).toEqual(new User({ id: 1, name: "User 1" })) + expect(driver.journal.length).toEqual(1) + expect(driver.unprocessed.size).toEqual(0) + }).pipe(Effect.provide(TestSharding))) + + it.scoped("interrupts", () => + Effect.gen(function*() { + const driver = yield* MessageStorage.MemoryDriver + const state = yield* TestEntityState + const makeClient = yield* TestEntity.client + yield* TestClock.adjust(1) + const client = makeClient("1") + + const fiber = yield* client.Never().pipe(Effect.fork) + yield* TestClock.adjust(1) + yield* Fiber.interrupt(fiber) + + yield* TestClock.adjust(1) + expect(driver.journal.length).toEqual(2) + expect(driver.replyIds.size).toEqual(1) + expect(state.interrupts.unsafeSize()).toEqual(Option.some(1)) + }).pipe(Effect.provide(TestSharding))) + + it.scoped("interrupts aren't sent for durable messages on shutdown", () => + Effect.gen(function*() { + let driver!: MessageStorage.MemoryDriver + yield* Effect.gen(function*() { + driver = yield* MessageStorage.MemoryDriver + const makeClient = yield* TestEntity.client + yield* TestClock.adjust(1) + const client = makeClient("1") + yield* client.Never().pipe(Effect.fork) + yield* TestClock.adjust(1) + }).pipe(Effect.provide(TestSharding)) + + // request, client interrupt is dropped + expect(driver.journal.length).toEqual(1) + // server interrupt is not sent + expect(driver.replyIds.size).toEqual(0) + })) + + it.scoped("interrupts are sent for volatile messages on shutdown", () => + Effect.gen(function*() { + let interrupted = false + const testClock = (yield* Effect.clock) as TestClock.TestClock + + yield* Effect.gen(function*() { + const makeClient = yield* TestEntity.client + const client = makeClient("1") + const fiber = yield* client.NeverVolatile().pipe(Effect.fork) + yield* TestClock.adjust(1) + const config = yield* ShardingConfig.ShardingConfig + ;(config as any).runnerAddress = Option.some(RunnerAddress.make("localhost", 1234)) + fiber.currentScheduler.scheduleTask( + () => { + fiber.unsafeInterruptAsFork(FiberId.none) + Effect.runFork(testClock.adjust(30000)) + }, + 0, + fiber + ) + }).pipe( + Effect.provide(TestShardingWithoutRunners.pipe( + Layer.provide(Layer.scoped( + Runners.Runners, + Effect.gen(function*() { + const runners = yield* Runners.makeNoop + return { + ...runners, + send(options) { + if (options.message.envelope._tag === "Interrupt") { + interrupted = true + return Effect.void + } + return runners.send(options) + } + } + }) + )), + Layer.provide([MessageStorage.layerMemory, Snowflake.layerGenerator]), + Layer.provideMerge(ShardingConfig.layer({ + entityMailboxCapacity: 10, + entityTerminationTimeout: 30000, + entityMessagePollInterval: 5000, + sendRetryInterval: 100 + })) + )) + ) + + assert.isTrue(interrupted) + })) + + it.scoped("malformed message in storage", () => + Effect.gen(function*() { + const driver = yield* MessageStorage.MemoryDriver + const makeClient = yield* TestEntity.client + yield* TestClock.adjust(1) + const client = makeClient("1") + + const fiber = yield* client.Never().pipe(Effect.fork) + yield* TestClock.adjust(1) + + const request = driver.journal[0] + yield* driver.encoded.saveEnvelope({ + envelope: { + id: "boom", + _tag: "Interrupt", + requestId: request.requestId, + address: { + shardId: request.address.shardId + } as any + }, + primaryKey: null, + deliverAt: null + }) + + // wait for storage to poll + yield* TestClock.adjust(5000) + + const exit = fiber.unsafePoll() + assert(exit && Exit.isFailure(exit) && Cause.isDie(exit.cause)) + + // malformed message should be left in the database + expect(driver.journal.length).toEqual(2) + // defect reply should be sent + expect(driver.replyIds.size).toEqual(1) + + const reply = driver.requests.get(request.requestId)!.replies[0] + assert(reply._tag === "WithExit" && reply.exit._tag === "Failure" && reply.exit.cause._tag === "Die") + }).pipe(Effect.provide(TestSharding))) + + it.scoped("MailboxFull for volatile messages", () => + Effect.gen(function*() { + const makeClient = yield* TestEntity.client + yield* TestClock.adjust(1) + const client = makeClient("1") + + yield* client.NeverVolatile().pipe(Effect.fork, Effect.replicateEffect(10)) + yield* TestClock.adjust(1) + const error = yield* client.NeverVolatile().pipe(Effect.flip) + assert.strictEqual(error._tag, "MailboxFull") + }).pipe(Effect.provide(TestSharding))) + + it.scoped("durable messages are retried when mailbox is full", () => + Effect.gen(function*() { + const requestedIds = yield* Mailbox.make>() + yield* Effect.gen(function*() { + const state = yield* TestEntityState + const makeClient = yield* TestEntity.client + yield* TestClock.adjust(1) + const client = makeClient("1") + + const fibers = yield* client.NeverFork().pipe(Effect.fork, Effect.replicateEffect(11)) + yield* TestClock.adjust(1) + + // wait for entity to go into resume mode and request ids + const ids = yield* requestedIds.take + assert.strictEqual(ids.length, 1) + + // test entity should still only have 10 requests + assert.deepStrictEqual(state.envelopes.unsafeSize(), Option.some(10)) + + // interrupt first request + yield* Fiber.interrupt(fibers[0]) + yield* TestClock.adjust(100) // let retry happen + + // last request should come through + assert.deepStrictEqual(state.envelopes.unsafeSize(), Option.some(11)) + + // interrupt second request, now the entity should be back in the main storage loop + yield* Fiber.interrupt(fibers[1]) + + // send another request within mailbox capacity + yield* client.NeverFork().pipe(Effect.fork) + yield* TestClock.adjust(1) + yield* Fiber.interruptAll(fibers) + yield* TestClock.adjust(100) + + // no more ids should have been requested from entity catch up + assert.deepStrictEqual(requestedIds.unsafeSize(), Option.some(0)) + }).pipe(Effect.provide(TestShardingWithoutStorage.pipe( + Layer.updateService(MessageStorage.MessageStorage, (storage) => ({ + ...storage, + unprocessedMessagesById(messageIds) { + requestedIds.unsafeOffer(Array.fromIterable(messageIds)) + return storage.unprocessedMessagesById(messageIds) + } + })), + Layer.provide(MessageStorage.layerMemory), + Layer.provide(TestShardingConfig) + ))) + })) + + it.scoped("interrupt for future request works while mailbox is full", () => + Effect.gen(function*() { + const state = yield* TestEntityState + const makeClient = yield* TestEntity.client + yield* TestClock.adjust(1) + const client = makeClient("1") + + const fibers = yield* client.NeverFork().pipe( + Effect.fork, + Effect.replicateEffect(12) + ) + yield* TestClock.adjust(1) + + // interrupt 11th request + yield* Fiber.interrupt(fibers[10]) + yield* TestClock.adjust(100) // let retry happen + // interrupt first request, and let the 11th request come through + yield* Fiber.interrupt(fibers[0]) + yield* TestClock.adjust(100) // let retry happen + + assert.deepStrictEqual(state.envelopes.unsafeSize(), Option.some(11)) + // second interrupt should be sent + assert.deepStrictEqual(state.interrupts.unsafeSize(), Option.some(2)) + }).pipe(Effect.provide(TestSharding))) + + it.scoped("delivers a durable stream", () => + Effect.gen(function*() { + const driver = yield* MessageStorage.MemoryDriver + yield* TestClock.adjust(1) + const makeClient = yield* TestEntity.client + const client = makeClient("1") + const users = yield* client.GetAllUsers({ ids: [1, 2, 3] }).pipe( + Stream.runCollect + ) + expect(Chunk.toReadonlyArray(users)).toEqual([ + new User({ id: 1, name: "User 1" }), + new User({ id: 2, name: "User 2" }), + new User({ id: 3, name: "User 3" }) + ]) + + // 1 request, 3 acks, 4 replies + expect(driver.journal.length).toEqual(4) + expect(driver.replyIds.size).toEqual(4) + }).pipe(Effect.provide(TestSharding))) + + it.scoped("durable stream while mailbox is full", () => + Effect.gen(function*() { + const requestedIds = yield* Mailbox.make>() + yield* Effect.gen(function*() { + const state = yield* TestEntityState + const makeClient = yield* TestEntity.client + yield* TestClock.adjust(1) + const client = makeClient("1") + + const fibers = yield* client.NeverFork().pipe(Effect.fork, Effect.replicateEffect(10)) + yield* TestClock.adjust(1) + + const fiber = yield* client.GetAllUsers({ ids: [1, 2, 3] }).pipe( + Stream.runCollect, + Effect.fork + ) + + // wait for entity to go into resume mode and request ids + const ids = yield* requestedIds.take + assert.strictEqual(ids.length, 1) + assert.deepStrictEqual(state.envelopes.unsafeSize(), Option.some(10)) + + // make sure entity doesn't leave resume mode + yield* client.NeverFork().pipe(Effect.fork) + yield* TestClock.adjust(1) + + // interrupt first request + yield* Fiber.interrupt(fibers[0]) + yield* TestClock.adjust(100) // let retry happen + + // last request should come through + assert.deepStrictEqual(state.envelopes.unsafeSize(), Option.some(11)) + + // acks should be allowed to be sent + const users = yield* Fiber.join(fiber) + expect(Chunk.toReadonlyArray(users)).toEqual([ + new User({ id: 1, name: "User 1" }), + new User({ id: 2, name: "User 2" }), + new User({ id: 3, name: "User 3" }) + ]) + + const driver = yield* MessageStorage.MemoryDriver + // 12 requests, 3 acks, 1 interrupt, 5 replies + assert.strictEqual(driver.journal.length, 12 + 3 + 1) + assert.strictEqual(driver.replyIds.size, 1 + 4) + }).pipe(Effect.provide(TestShardingWithoutStorage.pipe( + Layer.provideMerge(Layer.service(MessageStorage.MemoryDriver)), + Layer.updateService(MessageStorage.MessageStorage, (storage) => ({ + ...storage, + unprocessedMessagesById(messageIds) { + requestedIds.unsafeOffer(Array.fromIterable(messageIds)) + return storage.unprocessedMessagesById(messageIds) + } + })), + Layer.provide(MessageStorage.layerMemory), + Layer.provide(TestShardingConfig) + ))) + })) + + it.scoped("durable messages are retried on restart", () => + Effect.gen(function*() { + const EnvLayer = TestShardingWithoutState.pipe( + Layer.provide(Runners.layerNoop), + Layer.provide(TestShardingConfig) + ) + const driver = yield* MessageStorage.MemoryDriver + const state = yield* TestEntityState + + yield* Effect.gen(function*() { + yield* TestClock.adjust(1) + const makeClient = yield* TestEntity.client + const client = makeClient("1") + yield* Effect.fork(client.RequestWithKey({ key: "abc" })) + yield* TestClock.adjust(1) + }).pipe( + Effect.provide(EnvLayer), + Effect.scoped + ) + + // only the request should be in the journal + expect(driver.journal.length).toEqual(1) + expect(driver.replyIds.size).toEqual(0) + expect(driver.unprocessed.size).toEqual(1) + + // add response + yield* state.messages.offer(void 0) + + yield* TestClock.adjust(5000).pipe( + Effect.provide(EnvLayer), + Effect.scoped + ) + + expect(driver.journal.length).toEqual(1) + expect(driver.replyIds.size).toEqual(1) + expect(driver.unprocessed.size).toEqual(0) + + // the client should read the result from storage + yield* Effect.gen(function*() { + yield* TestClock.adjust(1) + const makeClient = yield* TestEntity.client + const client = makeClient("1") + const result = yield* client.RequestWithKey({ key: "abc" }) + expect(result).toEqual(void 0) + }).pipe( + Effect.provide(EnvLayer), + Effect.scoped + ) + + // the request should not hit the entity + expect(driver.journal.length).toEqual(1) + expect(driver.replyIds.size).toEqual(1) + expect(driver.unprocessed.size).toEqual(0) + }).pipe(Effect.provide(MessageStorage.layerMemory.pipe( + Layer.provide(TestShardingConfig), + Layer.merge(TestEntityState.Default) + )))) + + it.scoped("durable streams are resumed on restart", () => + Effect.gen(function*() { + const EnvLayer = TestShardingWithoutState.pipe( + Layer.provide(Runners.layerNoop), + Layer.provide(TestShardingConfig) + ) + const driver = yield* MessageStorage.MemoryDriver + const state = yield* TestEntityState + + // first chunk + yield* state.streamMessages.offerAll([void 0, void 0]) + + yield* Effect.gen(function*() { + yield* TestClock.adjust(1) + const makeClient = yield* TestEntity.client + const client = makeClient("1") + yield* Effect.fork(Stream.runDrain(client.StreamWithKey({ key: "abc" }))) + yield* TestClock.adjust(1) + // second chunk + yield* state.streamMessages.offer(void 0) + yield* TestClock.adjust(1) + }).pipe( + Effect.provide(EnvLayer), + Effect.scoped + ) + + // 1 request, 2 acks, 2 replies + expect(driver.journal.length).toEqual(1 + 2) + expect(driver.replyIds.size).toEqual(2) + expect(driver.unprocessed.size).toEqual(1) + + // third chunk + yield* state.streamMessages.offerAll([void 0, void 0]) + yield* state.streamMessages.end + + // the client should resume + yield* Effect.gen(function*() { + yield* TestClock.adjust(5000) // let the shards get assigned and storage poll + const makeClient = yield* TestEntity.client + const client = makeClient("1") + + // let the reply loop run + yield* TestClock.adjust(500).pipe(Effect.fork) + + const results = Chunk.toReadonlyArray( + yield* Stream.runCollect(client.StreamWithKey({ key: "abc" })) + ) + expect(results).toEqual([3, 4]) + }).pipe( + Effect.provide(EnvLayer), + Effect.scoped + ) + + // 1 request, 3 acks, 4 replies (3 chunks + WithExit) + expect(driver.journal.length).toEqual(1 + 3) + expect(driver.replyIds.size).toEqual(4) + expect(driver.unprocessed.size).toEqual(0) + }).pipe(Effect.provide(MessageStorage.layerMemory.pipe( + Layer.provide(TestShardingConfig), + Layer.merge(TestEntityState.Default) + )))) + + it.scoped("client discard option", () => + Effect.gen(function*() { + yield* TestClock.adjust(1) + const driver = yield* MessageStorage.MemoryDriver + const makeClient = yield* TestEntity.client + const client = makeClient("1") + const result = yield* client.GetUser({ id: 123 }, { discard: true }) + expect(result).toEqual(void 0) + yield* TestClock.adjust(1) + expect(driver.journal.length).toEqual(1) + expect(driver.unprocessed.size).toEqual(0) + }).pipe(Effect.provide(TestSharding))) + + it.scoped("client discard with Never", () => + Effect.gen(function*() { + yield* TestClock.adjust(1) + const driver = yield* MessageStorage.MemoryDriver + const makeClient = yield* TestEntity.client + const client = makeClient("1") + const result = yield* client.Never(void 0, { discard: true }) + expect(result).toEqual(void 0) + yield* TestClock.adjust(1) + expect(driver.journal.length).toEqual(1) + // should still be processing + expect(driver.unprocessed.size).toEqual(1) + }).pipe(Effect.provide(TestSharding))) + + it.scoped("defect when no MessageStorage", () => + Effect.gen(function*() { + const makeClient = yield* TestEntity.client + const client = makeClient("1") + const cause = yield* client.Never().pipe( + Effect.sandbox, + Effect.flip + ) + assert(Cause.isDie(cause)) + }).pipe(Effect.provide(TestShardingWithoutStorage.pipe( + Layer.provide(MessageStorage.layerNoop) + )))) + + it.scoped("restart on defect", () => + Effect.gen(function*() { + yield* TestClock.adjust(1) + const state = yield* TestEntityState + const makeClient = yield* TestEntity.client + const client = makeClient("1") + MutableRef.set(state.defectTrigger, true) + const result = yield* client.GetUser({ id: 123 }) + expect(result).toEqual(new User({ id: 123, name: "User 123" })) + expect(state.layerBuilds.current).toEqual(2) + }).pipe(Effect.provide(TestSharding))) +}) + +const TestShardingConfig = ShardingConfig.layer({ + entityMailboxCapacity: 10, + entityTerminationTimeout: 0, + entityMessagePollInterval: 5000, + sendRetryInterval: 100 +}) + +const TestShardingWithoutState = TestEntityNoState.pipe( + Layer.provideMerge(Sharding.layer), + Layer.provide(RunnerStorage.layerMemory), + Layer.provide(RunnerHealth.layerNoop) + // Layer.provide(Logger.minimumLogLevel(LogLevel.All)), + // Layer.provideMerge(Logger.pretty) +) + +const TestShardingWithoutRunners = TestShardingWithoutState.pipe( + Layer.provideMerge(TestEntityState.Default) +) + +const TestShardingWithoutStorage = TestShardingWithoutRunners.pipe( + Layer.provide(Runners.layerNoop), + Layer.provide(TestShardingConfig) +) + +const TestSharding = TestShardingWithoutStorage.pipe( + Layer.provideMerge(MessageStorage.layerMemory), + Layer.provide(TestShardingConfig) +) diff --git a/repos/effect/packages/cluster/test/SqlMessageStorage.test.ts b/repos/effect/packages/cluster/test/SqlMessageStorage.test.ts new file mode 100644 index 0000000..067c81d --- /dev/null +++ b/repos/effect/packages/cluster/test/SqlMessageStorage.test.ts @@ -0,0 +1,226 @@ +import { Message, MessageStorage, ShardingConfig, Snowflake, SqlMessageStorage } from "@effect/cluster" +import { FileSystem } from "@effect/platform" +import { NodeFileSystem } from "@effect/platform-node" +import { Rpc } from "@effect/rpc" +import { SqliteClient } from "@effect/sql-sqlite-node" +import { SqlClient } from "@effect/sql/SqlClient" +import { assert, describe, expect, it } from "@effect/vitest" +import { Effect, Fiber, Layer, TestClock } from "effect" +import { MysqlContainer } from "./fixtures/utils-mysql.js" +import { PgContainer } from "./fixtures/utils-pg.js" +import { + makeAckChunk, + makeChunkReply, + makeReply, + makeRequest, + PrimaryKeyTest, + StreamRpc, + StreamTest +} from "./MessageStorage.test.js" + +const StorageLive = SqlMessageStorage.layer.pipe( + Layer.provideMerge(Snowflake.layerGenerator), + Layer.provide(ShardingConfig.layerDefaults) +) + +const truncate = Effect.gen(function*() { + const sql = yield* SqlClient + yield* sql`DELETE FROM cluster_replies` + yield* sql`DELETE FROM cluster_messages` +}) + +describe("SqlMessageStorage", () => { + ;([ + ["pg", Layer.orDie(PgContainer.ClientLive)], + ["mysql", Layer.orDie(MysqlContainer.ClientLive)], + ["sqlite", Layer.orDie(SqliteLayer)] + ] as const).forEach(([label, layer]) => { + it.layer(StorageLive.pipe(Layer.provideMerge(layer)), { + timeout: 120000 + })(label, (it) => { + it.effect("saveRequest", () => + Effect.gen(function*() { + const storage = yield* MessageStorage.MessageStorage + const request = yield* makeRequest({ payload: { id: 1 } }) + const result = yield* storage.saveRequest(request) + expect(result._tag).toEqual("Success") + + for (let i = 2; i <= 5; i++) { + yield* storage.saveRequest(yield* makeRequest({ payload: { id: i } })) + } + + yield* storage.saveReply(yield* makeReply(request)) + + let messages = yield* storage.unprocessedMessages([request.envelope.address.shardId]) + expect(messages).toHaveLength(4) + expect(messages.map((m: any) => m.envelope.payload.id)).toEqual([2, 3, 4, 5]) + + for (let i = 6; i <= 10; i++) { + yield* storage.saveRequest(yield* makeRequest({ payload: { id: i } })) + } + messages = yield* storage.unprocessedMessages([request.envelope.address.shardId]) + expect(messages).toHaveLength(5) + expect(messages.map((m: any) => m.envelope.payload.id)).toEqual([6, 7, 8, 9, 10]) + })) + + it.effect("saveReply + saveRequest duplicate", () => + Effect.gen(function*() { + const sql = yield* SqlClient + const storage = yield* MessageStorage.MessageStorage + const request = yield* makeRequest({ + rpc: StreamRpc, + payload: new StreamTest({ id: 123 }) + }) + let result = yield* storage.saveRequest(request) + expect(result._tag).toEqual("Success") + + let chunk = yield* makeChunkReply(request, 0) + yield* storage.saveReply(chunk) + const ackChunk = yield* makeAckChunk(request, chunk) + yield* storage.saveEnvelope(ackChunk) + + chunk = yield* makeChunkReply(request, 1) + yield* storage.saveReply(chunk) + + result = yield* storage.saveRequest( + yield* makeRequest({ + rpc: StreamRpc, + payload: new StreamTest({ id: 123 }) + }) + ) + assert(result._tag === "Duplicate") + assert(result.lastReceivedReply._tag === "Some") + expect(result.lastReceivedReply.value._tag).toEqual("Chunk") + + // get the un-acked chunk + const replies = yield* storage.repliesFor([request]) + expect(replies).toHaveLength(1) + + yield* storage.saveReply(yield* makeReply(request)) + + result = yield* storage.saveRequest( + yield* makeRequest({ + rpc: StreamRpc, + payload: new StreamTest({ id: 123 }) + }) + ) + assert(result._tag === "Duplicate") + assert(result.lastReceivedReply._tag === "Some") + expect(result.lastReceivedReply.value._tag).toEqual("WithExit") + + // duplicate WithExit + const fiber = yield* storage.saveReply(yield* makeReply(request)).pipe(Effect.fork) + yield* TestClock.adjust(1) + while (!fiber.unsafePoll()) { + yield* sql`SELECT 1` + yield* TestClock.adjust(1000) + } + const error = yield* Effect.flip(Fiber.join(fiber)) + expect(error._tag).toEqual("PersistenceError") + })) + + it.effect("detects duplicates", () => + Effect.gen(function*() { + yield* truncate + + const storage = yield* MessageStorage.MessageStorage + yield* storage.saveRequest( + yield* makeRequest({ + rpc: Rpc.fromTaggedRequest(PrimaryKeyTest), + payload: new PrimaryKeyTest({ id: 123 }) + }) + ) + const result = yield* storage.saveRequest( + yield* makeRequest({ + rpc: Rpc.fromTaggedRequest(PrimaryKeyTest), + payload: new PrimaryKeyTest({ id: 123 }) + }) + ) + expect(result._tag).toEqual("Duplicate") + })) + + it.effect("unprocessedMessages", () => + Effect.gen(function*() { + yield* truncate + + const storage = yield* MessageStorage.MessageStorage + const request = yield* makeRequest() + yield* storage.saveRequest(request) + let messages = yield* storage.unprocessedMessages([request.envelope.address.shardId]) + expect(messages).toHaveLength(1) + messages = yield* storage.unprocessedMessages([request.envelope.address.shardId]) + expect(messages).toHaveLength(0) + yield* storage.saveRequest(yield* makeRequest()) + messages = yield* storage.unprocessedMessages([request.envelope.address.shardId]) + expect(messages).toHaveLength(1) + })) + + it.effect("unprocessedMessages excludes complete requests", () => + Effect.gen(function*() { + yield* truncate + + const storage = yield* MessageStorage.MessageStorage + const request = yield* makeRequest() + yield* storage.saveRequest(request) + yield* storage.saveReply(yield* makeReply(request)) + const messages = yield* storage.unprocessedMessages([request.envelope.address.shardId]) + expect(messages).toHaveLength(0) + })) + + it.effect("repliesFor", () => + Effect.gen(function*() { + yield* truncate + + const storage = yield* MessageStorage.MessageStorage + const request = yield* makeRequest() + yield* storage.saveRequest(request) + let replies = yield* storage.repliesFor([request]) + expect(replies).toHaveLength(0) + yield* storage.saveReply(yield* makeReply(request)) + replies = yield* storage.repliesFor([request]) + expect(replies).toHaveLength(1) + expect(replies[0].requestId).toEqual(request.envelope.requestId) + })) + + it.effect("registerReplyHandler", () => + Effect.gen(function*() { + const storage = yield* MessageStorage.MessageStorage + const latch = yield* Effect.makeLatch() + const request = yield* makeRequest() + yield* storage.saveRequest(request) + const fiber = yield* storage.registerReplyHandler( + new Message.OutgoingRequest({ + ...request, + respond: () => latch.open + }) + ).pipe(Effect.fork) + yield* TestClock.adjust(1) + yield* storage.saveReply(yield* makeReply(request)) + yield* latch.await + yield* fiber.await + })) + + it.effect("unprocessedMessagesById", () => + Effect.gen(function*() { + yield* truncate + + const storage = yield* MessageStorage.MessageStorage + const request = yield* makeRequest() + yield* storage.saveRequest(request) + let messages = yield* storage.unprocessedMessagesById([request.envelope.requestId]) + expect(messages).toHaveLength(1) + yield* storage.saveReply(yield* makeReply(request)) + messages = yield* storage.unprocessedMessagesById([request.envelope.requestId]) + expect(messages).toHaveLength(0) + })) + }) + }) +}) + +const SqliteLayer = Effect.gen(function*() { + const fs = yield* FileSystem.FileSystem + const dir = yield* fs.makeTempDirectoryScoped() + return SqliteClient.layer({ + filename: dir + "/test.db" + }) +}).pipe(Layer.unwrapScoped, Layer.provide(NodeFileSystem.layer)) diff --git a/repos/effect/packages/cluster/test/SqlRunnerStorage.test.ts b/repos/effect/packages/cluster/test/SqlRunnerStorage.test.ts new file mode 100644 index 0000000..e894484 --- /dev/null +++ b/repos/effect/packages/cluster/test/SqlRunnerStorage.test.ts @@ -0,0 +1,96 @@ +import { Runner, RunnerAddress, RunnerStorage, ShardId, SqlRunnerStorage } from "@effect/cluster" +import { FileSystem } from "@effect/platform" +import { NodeFileSystem } from "@effect/platform-node" +import { SqliteClient } from "@effect/sql-sqlite-node" +import { describe, expect, it } from "@effect/vitest" +import { Effect, Layer } from "effect" +import * as ShardingConfig from "../src/ShardingConfig.js" +import { MysqlContainer } from "./fixtures/utils-mysql.js" +import { PgContainer } from "./fixtures/utils-pg.js" + +const StorageLive = SqlRunnerStorage.layer + +describe("SqlRunnerStorage", () => { + ;([ + ["pg", Layer.orDie(PgContainer.ClientLive)], + ["mysql", Layer.orDie(MysqlContainer.ClientLive)], + ["vitess", Layer.orDie(MysqlContainer.ClientLiveVitess)], + ["sqlite", Layer.orDie(SqliteLayer)] + ] as const).flatMap(([label, layer]) => + [ + [label, StorageLive.pipe(Layer.provideMerge(layer), Layer.provide(ShardingConfig.layer()))], + [ + label + " (no advisory)", + StorageLive.pipe( + Layer.provideMerge(layer), + Layer.provide(ShardingConfig.layer({ + shardLockDisableAdvisory: true + })) + ) + ] + ] as const + ).forEach(([label, layer]) => { + it.layer(layer, { + timeout: 60000 + })(label, (it) => { + it.effect("getRunners", () => + Effect.gen(function*() { + const storage = yield* RunnerStorage.RunnerStorage + + const runner = Runner.make({ + address: runnerAddress1, + groups: ["default"], + weight: 1 + }) + const machineId = yield* storage.register(runner, true) + yield* storage.register(runner, true) + expect(machineId).toEqual(1) + expect(yield* storage.getRunners).toEqual([[runner, true]]) + + yield* storage.setRunnerHealth(runnerAddress1, false) + expect(yield* storage.getRunners).toEqual([[runner, false]]) + + yield* storage.unregister(runnerAddress1) + expect(yield* storage.getRunners).toEqual([]) + }), 30_000) + + it.effect("acquireShards", () => + Effect.gen(function*() { + const storage = yield* RunnerStorage.RunnerStorage + + let acquired = yield* storage.acquire(runnerAddress1, [ + ShardId.make("default", 1), + ShardId.make("default", 2), + ShardId.make("default", 3) + ]) + expect(acquired.map((_) => _.id)).toEqual([1, 2, 3]) + acquired = yield* storage.acquire(runnerAddress1, [ + ShardId.make("default", 1), + ShardId.make("default", 2), + ShardId.make("default", 3) + ]) + expect(acquired.map((_) => _.id)).toEqual([1, 2, 3]) + + const refreshed = yield* storage.refresh(runnerAddress1, [ + ShardId.make("default", 1), + ShardId.make("default", 2), + ShardId.make("default", 3) + ]) + expect(refreshed.map((_) => _.id)).toEqual([1, 2, 3]) + + // smoke test release + yield* storage.release(runnerAddress1, ShardId.make("default", 2)) + })) + }) + }) +}) + +const runnerAddress1 = RunnerAddress.make("localhost", 1234) + +const SqliteLayer = Effect.gen(function*() { + const fs = yield* FileSystem.FileSystem + const dir = yield* fs.makeTempDirectoryScoped() + return SqliteClient.layer({ + filename: dir + "/test.db" + }) +}).pipe(Layer.unwrapScoped, Layer.provide(NodeFileSystem.layer)) diff --git a/repos/effect/packages/cluster/test/TestEntity.ts b/repos/effect/packages/cluster/test/TestEntity.ts new file mode 100644 index 0000000..3915977 --- /dev/null +++ b/repos/effect/packages/cluster/test/TestEntity.ts @@ -0,0 +1,131 @@ +import type { Envelope } from "@effect/cluster" +import { ClusterSchema, Entity } from "@effect/cluster" +import type { RpcGroup } from "@effect/rpc" +import { Rpc, RpcSchema } from "@effect/rpc" +import { Effect, Layer, Mailbox, MutableRef, Option, PrimaryKey, Schedule, Schema, Stream } from "effect" + +export class User extends Schema.Class("User")({ + id: Schema.Number, + name: Schema.String +}) {} + +export class StreamWithKey extends Schema.TaggedRequest()("StreamWithKey", { + success: RpcSchema.Stream({ + success: Schema.Number, + failure: Schema.Never + }), + failure: Schema.Never, + payload: { key: Schema.String } +}) { + [PrimaryKey.symbol]() { + return this.key + } +} + +export const TestEntity = Entity.make("TestEntity", [ + Rpc.make("GetUser", { + success: User, + payload: { id: Schema.Number } + }), + Rpc.make("GetUserVolatile", { + success: User, + payload: { id: Schema.Number } + }).annotate(ClusterSchema.Persisted, false), + Rpc.make("Never"), + Rpc.make("NeverFork"), + Rpc.make("NeverVolatile").annotate(ClusterSchema.Persisted, false), + Rpc.make("RequestWithKey", { + payload: { key: Schema.String }, + primaryKey: ({ key }) => key + }), + Rpc.fromTaggedRequest(StreamWithKey), + Rpc.make("GetAllUsers", { + success: User, + payload: { ids: Schema.Array(Schema.Number) }, + stream: true + }) +]).annotateRpcs(ClusterSchema.Persisted, true) + +export class TestEntityState extends Effect.Service()("TestEntityState", { + effect: Effect.gen(function*() { + const messages = yield* Mailbox.make() + const streamMessages = yield* Mailbox.make() + const envelopes = yield* Mailbox.make< + RpcGroup.Rpcs extends infer R ? R extends Rpc.Any ? Envelope.Request : never + : never + >() + const interrupts = yield* Mailbox.make< + RpcGroup.Rpcs extends infer R ? R extends Rpc.Any ? Envelope.Request : never + : never + >() + const defectTrigger = MutableRef.make(false) + const layerBuilds = MutableRef.make(0) + + return { + messages, + streamMessages, + envelopes, + interrupts, + defectTrigger, + layerBuilds + } as const + }) +}) {} + +export const TestEntityNoState = TestEntity.toLayer( + Effect.gen(function*() { + const state = yield* TestEntityState + + MutableRef.update(state.layerBuilds, (count) => count + 1) + + const never = (envelope: any) => + Effect.suspend(() => { + state.envelopes.unsafeOffer(envelope) + return Effect.never + }).pipe(Effect.onInterrupt(() => { + state.interrupts.unsafeOffer(envelope) + return Effect.void + })) + return { + GetUser: (envelope) => + Effect.sync(() => { + state.envelopes.unsafeOffer(envelope) + if (state.defectTrigger.current) { + MutableRef.set(state.defectTrigger, false) + throw new Error("User not found") + } + return new User({ id: envelope.payload.id, name: `User ${envelope.payload.id}` }) + }), + GetUserVolatile: (envelope) => + Effect.sync(() => { + state.envelopes.unsafeOffer(envelope) + return new User({ id: envelope.payload.id, name: `User ${envelope.payload.id}` }) + }), + Never: never, + NeverFork: (envelope) => Rpc.fork(never(envelope)), + NeverVolatile: never, + RequestWithKey: (envelope) => { + state.envelopes.unsafeOffer(envelope) + return Effect.orDie(state.messages.take) + }, + StreamWithKey: (envelope) => { + let sequence = envelope.lastSentChunkValue.pipe( + Option.map((value) => value + 1), + Option.getOrElse(() => 0) + ) + return Mailbox.toStream(state.streamMessages).pipe( + Stream.map(() => sequence++) + ) + }, + GetAllUsers: (envelope) => { + state.envelopes.unsafeOffer(envelope) + return Stream.fromIterable(envelope.payload.ids.map((id) => new User({ id, name: `User ${id}` }))).pipe( + Stream.rechunk(1) + ) + } + } + }), + { defectRetryPolicy: Schedule.forever } +) + +export const TestEntityLayer = TestEntityNoState.pipe(Layer.provideMerge(TestEntityState.Default)) diff --git a/repos/effect/packages/cluster/test/fixtures/utils-mysql.ts b/repos/effect/packages/cluster/test/fixtures/utils-mysql.ts new file mode 120000 index 0000000..e28433e --- /dev/null +++ b/repos/effect/packages/cluster/test/fixtures/utils-mysql.ts @@ -0,0 +1 @@ +../../../sql-mysql2/test/utils.ts \ No newline at end of file diff --git a/repos/effect/packages/cluster/test/fixtures/utils-pg.ts b/repos/effect/packages/cluster/test/fixtures/utils-pg.ts new file mode 120000 index 0000000..676460d --- /dev/null +++ b/repos/effect/packages/cluster/test/fixtures/utils-pg.ts @@ -0,0 +1 @@ +../../../sql-pg/test/utils.ts \ No newline at end of file diff --git a/repos/effect/packages/cluster/tsconfig.build.json b/repos/effect/packages/cluster/tsconfig.build.json new file mode 100644 index 0000000..4ed461e --- /dev/null +++ b/repos/effect/packages/cluster/tsconfig.build.json @@ -0,0 +1,15 @@ +{ + "extends": "./tsconfig.src.json", + "references": [ + { "path": "../effect/tsconfig.build.json" }, + { "path": "../platform/tsconfig.build.json" }, + { "path": "../rpc/tsconfig.build.json" }, + { "path": "../sql/tsconfig.build.json" } + ], + "compilerOptions": { + "tsBuildInfoFile": ".tsbuildinfo/build.tsbuildinfo", + "outDir": "build/esm", + "declarationDir": "build/dts", + "stripInternal": true + } +} diff --git a/repos/effect/packages/cluster/tsconfig.json b/repos/effect/packages/cluster/tsconfig.json new file mode 100644 index 0000000..2c291d2 --- /dev/null +++ b/repos/effect/packages/cluster/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.base.json", + "include": [], + "references": [ + { "path": "tsconfig.src.json" }, + { "path": "tsconfig.test.json" } + ] +} diff --git a/repos/effect/packages/cluster/tsconfig.src.json b/repos/effect/packages/cluster/tsconfig.src.json new file mode 100644 index 0000000..672bc33 --- /dev/null +++ b/repos/effect/packages/cluster/tsconfig.src.json @@ -0,0 +1,16 @@ +{ + "extends": "../../tsconfig.base.json", + "include": ["src"], + "references": [ + { "path": "../effect" }, + { "path": "../platform" }, + { "path": "../rpc" }, + { "path": "../sql" }, + { "path": "../workflow" } + ], + "compilerOptions": { + "tsBuildInfoFile": ".tsbuildinfo/src.tsbuildinfo", + "rootDir": "src", + "outDir": "build/src" + } +} diff --git a/repos/effect/packages/cluster/tsconfig.test.json b/repos/effect/packages/cluster/tsconfig.test.json new file mode 100644 index 0000000..9dc0d73 --- /dev/null +++ b/repos/effect/packages/cluster/tsconfig.test.json @@ -0,0 +1,16 @@ +{ + "extends": "../../tsconfig.base.json", + "include": ["test"], + "references": [ + { "path": "tsconfig.src.json" }, + { "path": "../platform-node/tsconfig.src.json" }, + { "path": "../sql-pg/tsconfig.src.json" }, + { "path": "../sql-mysql2/tsconfig.src.json" }, + { "path": "../sql-sqlite-node/tsconfig.src.json" } + ], + "compilerOptions": { + "tsBuildInfoFile": ".tsbuildinfo/test.tsbuildinfo", + "rootDir": "test", + "noEmit": true + } +} diff --git a/repos/effect/packages/cluster/vitest.config.ts b/repos/effect/packages/cluster/vitest.config.ts new file mode 100644 index 0000000..0411095 --- /dev/null +++ b/repos/effect/packages/cluster/vitest.config.ts @@ -0,0 +1,6 @@ +import { mergeConfig, type UserConfigExport } from "vitest/config" +import shared from "../../vitest.shared.js" + +const config: UserConfigExport = {} + +export default mergeConfig(shared, config) diff --git a/repos/effect/packages/effect/CHANGELOG.md b/repos/effect/packages/effect/CHANGELOG.md new file mode 100644 index 0000000..ef5d700 --- /dev/null +++ b/repos/effect/packages/effect/CHANGELOG.md @@ -0,0 +1,9902 @@ +# effect + +## 3.21.2 + +### Patch Changes + +- [#6194](https://github.com/Effect-TS/effect/pull/6194) [`74f3267`](https://github.com/Effect-TS/effect/commit/74f3267a6cc7ed7818c4c34cc1232f7cfc7d3339) Thanks @mikearnaldi! - Fix `TestClock.unsafeCurrentTimeNanos()` to floor fractional millisecond instants before converting them to `BigInt`. + +## 3.21.1 + +### Patch Changes + +- [#6139](https://github.com/Effect-TS/effect/pull/6139) [`f99048e`](https://github.com/Effect-TS/effect/commit/f99048e9f4b89ce1afe31e1827dee5d751ddaa5b) Thanks @marbemac! - Fix batched request resolver defects causing consumer fibers to hang forever. + + When a `RequestResolver.makeBatched` resolver died with a defect, the request `Deferred`s were never completed because the cleanup logic in `invokeWithInterrupt` used `flatMap` (which only runs on success). Changed to `ensuring` so uncompleted request entries are always resolved regardless of exit type. + +## 3.21.0 + +### Minor Changes + +- [#5780](https://github.com/Effect-TS/effect/pull/5780) [`f7bb09b`](https://github.com/Effect-TS/effect/commit/f7bb09b022f195d1f2b3c23d49e74b011ec5d109) Thanks @kitlangton! - Add `Cron.prev` and reverse iteration support, aligning next/prev lookup tables, fixing DST handling symmetry, and expanding cron backward/forward test coverage. + +- [#5780](https://github.com/Effect-TS/effect/pull/5780) [`bd7552a`](https://github.com/Effect-TS/effect/commit/bd7552a19cc0ed575507ac6cc0879a57e24ebd31) Thanks @mattiamanzati! - Add type-level utils to asserting layer types + +- [#5780](https://github.com/Effect-TS/effect/pull/5780) [`ad1a7eb`](https://github.com/Effect-TS/effect/commit/ad1a7eb7f6bebaf91c80be2443ac0439226d0098) Thanks @schickling! - RcMap: support dynamic `idleTimeToLive` values per key + + The `idleTimeToLive` option can now be a function that receives the key and returns a duration, allowing different TTL values for different resources. + + ```ts + const map = + yield * + RcMap.make({ + lookup: (key: string) => acquireResource(key), + idleTimeToLive: (key: string) => { + if (key.startsWith("premium:")) return Duration.minutes(10) + return Duration.minutes(1) + } + }) + ``` + +- [#5780](https://github.com/Effect-TS/effect/pull/5780) [`0d32048`](https://github.com/Effect-TS/effect/commit/0d32048f9836e2b23a6ba3ec5f43f0a000bb92fb) Thanks @mikearnaldi! - Fix annotateCurrentSpan, add Effect.currentPropagatedSpan + +### Patch Changes + +- [#5780](https://github.com/Effect-TS/effect/pull/5780) [`0d32048`](https://github.com/Effect-TS/effect/commit/0d32048f9836e2b23a6ba3ec5f43f0a000bb92fb) Thanks @mikearnaldi! - Add logs to first propagated span, in the following case before this fix the log would not be added to the `p` span because `Effect.fn` adds a fake span for the purpose of adding a stack frame. + + ```ts + import { Effect } from "effect" + + const f = Effect.fn(function* () { + yield* Effect.logWarning("FooBar") + return yield* Effect.fail("Oops") + }) + + const p = f().pipe(Effect.withSpan("p")) + ``` + +## 3.20.1 + +### Patch Changes + +- [#6133](https://github.com/Effect-TS/effect/pull/6133) [`add06f4`](https://github.com/Effect-TS/effect/commit/add06f4521403cbf4b9a692f9b59fb9d3d48293c) Thanks @aniravi24! - Fix `Equal.equals` crash when comparing `null` values inside `structuralRegion`. Added null guard before `Object.getPrototypeOf` calls to prevent `TypeError: Cannot convert undefined or null to object`. + +- [#6093](https://github.com/Effect-TS/effect/pull/6093) [`a03b6a2`](https://github.com/Effect-TS/effect/commit/a03b6a29ed0b983b0440b8ef4be47f47c57d73d7) Thanks @luchersou! - avoid class for PrettyError to preserve error.name + +## 3.20.0 + +### Minor Changes + +- [#6124](https://github.com/Effect-TS/effect/pull/6124) [`8798a84`](https://github.com/Effect-TS/effect/commit/8798a843218e6c0c0d3a8eee83360880e370b4da) Thanks @mikearnaldi! - Fix scheduler task draining to isolate `AsyncLocalStorage` across fibers. + +### Patch Changes + +- [#6107](https://github.com/Effect-TS/effect/pull/6107) [`fc82e81`](https://github.com/Effect-TS/effect/commit/fc82e81448bd9136a37580139ce46a2c61b11b54) Thanks @gcanti! - Backport `Types.VoidIfEmpty` to 3.x + +- [#6088](https://github.com/Effect-TS/effect/pull/6088) [`82996bc`](https://github.com/Effect-TS/effect/commit/82996bce8debffcb44feb98bb862cf2662bd56b7) Thanks @taylorOntologize! - Schema: fix `Schema.omit` producing wrong result on Struct with `optionalWith({ default })` and index signatures + + `getIndexSignatures` now handles `Transformation` AST nodes by delegating to `ast.to`, matching the existing behavior of `getPropertyKeys` and `getPropertyKeyIndexedAccess`. Previously, `Schema.omit` on a struct combining `Schema.optionalWith` (with `{ default }`, `{ as: "Option" }`, etc.) and `Schema.Record` would silently take the wrong code path, returning a Transformation with property signatures instead of a TypeLiteral with index signatures. + +- [#6086](https://github.com/Effect-TS/effect/pull/6086) [`4d97a61`](https://github.com/Effect-TS/effect/commit/4d97a61a15b9dd6a0eece65b8f0c035e16d42ada) Thanks @taylorOntologize! - Schema: fix `getPropertySignatures` crash on Struct with `optionalWith({ default })` and other Transformation-producing variants + + `SchemaAST.getPropertyKeyIndexedAccess` now handles `Transformation` AST nodes by delegating to `ast.to`, matching the existing behavior of `getPropertyKeys`. Previously, calling `getPropertySignatures` on a `Schema.Struct` containing `Schema.optionalWith` with `{ default }`, `{ as: "Option" }`, `{ nullable: true }`, or similar options would throw `"Unsupported schema (Transformation)"`. + +- [#6097](https://github.com/Effect-TS/effect/pull/6097) [`f6b0960`](https://github.com/Effect-TS/effect/commit/f6b0960bf3184109920dfed16ee7dfd7d67bc0f2) Thanks @gcanti! - Fix TupleWithRest post-rest validation to check each tail index sequentially. + +## 3.19.19 + +### Patch Changes + +- [#6079](https://github.com/Effect-TS/effect/pull/6079) [`4eb5c00`](https://github.com/Effect-TS/effect/commit/4eb5c008dfc7d2a97b191ca608948d994a2cce4c) Thanks @tim-smart! - add short circuit to fiber.await internals + +- [#6079](https://github.com/Effect-TS/effect/pull/6079) [`4eb5c00`](https://github.com/Effect-TS/effect/commit/4eb5c008dfc7d2a97b191ca608948d994a2cce4c) Thanks @tim-smart! - build ManagedRuntime synchronously if possible + +- [#6081](https://github.com/Effect-TS/effect/pull/6081) [`2d2bb13`](https://github.com/Effect-TS/effect/commit/2d2bb1364a906bd44800c6387b9575fddccdaf53) Thanks @tim-smart! - fix semaphore race condition where permits could be leaked + +## 3.19.18 + +### Patch Changes + +- [#6062](https://github.com/Effect-TS/effect/pull/6062) [`12b1f1e`](https://github.com/Effect-TS/effect/commit/12b1f1eadf649e30dec581b7351ba3abb12f8004) Thanks @tim-smart! - prevent Stream.changes from writing empty chunks + +## 3.19.17 + +### Patch Changes + +- [#6040](https://github.com/Effect-TS/effect/pull/6040) [`a8c436f`](https://github.com/Effect-TS/effect/commit/a8c436f7004cc2a8ce2daec589ea7256b91c324f) Thanks @jacobconley! - Fix `Stream.decodeText` to correctly handle multi-byte UTF-8 characters split across chunk boundaries. + +## 3.19.16 + +### Patch Changes + +- [#6018](https://github.com/Effect-TS/effect/pull/6018) [`e71889f`](https://github.com/Effect-TS/effect/commit/e71889f35b081d13b7da2c04d2f81d6933056b49) Thanks @codewithkenzo! - fix(Match): handle null/undefined in `Match.tag` and `Match.tagStartsWith` + + Added null checks to `discriminator` and `discriminatorStartsWith` predicates to prevent crashes when matching nullable union types. + + Fixes #6017 + +## 3.19.15 + +### Patch Changes + +- [#5981](https://github.com/Effect-TS/effect/pull/5981) [`7e925ea`](https://github.com/Effect-TS/effect/commit/7e925eae4a9db556bcbf7e8b6a762ccf8588aa3b) Thanks @bxff! - Fix type inference loss in `Array.flatten` for complex nested structures like unions of Effects with contravariant requirements. Uses distributive indexed access (`T[number][number]`) in the `Flatten` type utility and adds `const` to the `flatten` generic parameter. + +- [#5970](https://github.com/Effect-TS/effect/pull/5970) [`d7e75d6`](https://github.com/Effect-TS/effect/commit/d7e75d6d15294bbcd7ac49a0e9005848379ea86f) Thanks @KhraksMamtsov! - fix Config.orElseIf signature + +- [#5996](https://github.com/Effect-TS/effect/pull/5996) [`4860d1e`](https://github.com/Effect-TS/effect/commit/4860d1e09b436061ea4aeca07605a669793560fc) Thanks @parischap! - fix Equal.equals plain object comparisons in structural mode + +## 3.19.14 + +### Patch Changes + +- [#5924](https://github.com/Effect-TS/effect/pull/5924) [`488d6e8`](https://github.com/Effect-TS/effect/commit/488d6e870eda3dfc137f4940bb69416f61ed8fe3) Thanks @mikearnaldi! - Fix `Effect.retry` to respect `times: 0` option by using explicit undefined check instead of truthy check. + +## 3.19.13 + +### Patch Changes + +- [#5911](https://github.com/Effect-TS/effect/pull/5911) [`77eeb86`](https://github.com/Effect-TS/effect/commit/77eeb86ddf208e51ec25932af83d52d3b4700371) Thanks @mattiamanzati! - Add test for ensuring typeConstructor is attached + +- [#5910](https://github.com/Effect-TS/effect/pull/5910) [`287c32c`](https://github.com/Effect-TS/effect/commit/287c32c9f10da8e96f2b9ef8424316189d9ad4b3) Thanks @mattiamanzati! - Add typeConstructor annotation for Schema + +## 3.19.12 + +### Patch Changes + +- [#5897](https://github.com/Effect-TS/effect/pull/5897) [`a6dfca9`](https://github.com/Effect-TS/effect/commit/a6dfca93b676eeffe4db64945b01e2004b395cb8) Thanks @fubhy! - Ensure `performance.now` is only used if it's available + +## 3.19.11 + +### Patch Changes + +- [#5888](https://github.com/Effect-TS/effect/pull/5888) [`38abd67`](https://github.com/Effect-TS/effect/commit/38abd67998f676893866a72cb41bbd5edd07b169) Thanks @gcanti! - filter non-JSON values from schema examples and defaults, closes #5884 + + Introduce JsonValue type and update JsonSchemaAnnotations to use it for + type safety. Add validation to filter invalid values (BigInt, cyclic refs) + from examples and defaults, preventing infinite recursion on cycles. + +- [#5885](https://github.com/Effect-TS/effect/pull/5885) [`44e0b04`](https://github.com/Effect-TS/effect/commit/44e0b044480c5d8ab17fbdaf1c528f06796fa681) Thanks @gcanti! - feat(JSONSchema): add missing options for target JSON Schema version in make function, closes #5883 + +## 3.19.10 + +### Patch Changes + +- [#5874](https://github.com/Effect-TS/effect/pull/5874) [`bd08028`](https://github.com/Effect-TS/effect/commit/bd080284febb620e7e71f661bf9d850c402bb87f) Thanks @mattiamanzati! - Fix NoSuchElementException instantiation in fastPath and add corresponding test case + +- [#5878](https://github.com/Effect-TS/effect/pull/5878) [`6c5c2ba`](https://github.com/Effect-TS/effect/commit/6c5c2ba50ce49386e8d1e657230492ee900a6ec7) Thanks @Hoishin! - prevent crash from Hash and Equal with invalid Date object + +## 3.19.9 + +### Patch Changes + +- [#5875](https://github.com/Effect-TS/effect/pull/5875) [`3f9bbfe`](https://github.com/Effect-TS/effect/commit/3f9bbfe9ef78303ecc6817b68ec9671f4d42d249) Thanks @gcanti! - Fix the arbitrary generator for BigDecimal to allow negative scales. + +## 3.19.8 + +### Patch Changes + +- [#5815](https://github.com/Effect-TS/effect/pull/5815) [`f03b8e5`](https://github.com/Effect-TS/effect/commit/f03b8e55f12019cc855a1306e9cbfc7611a9e281) Thanks @lokhmakov! - Prevent multiple iterations over the same Iterable in Array.intersectionWith and Array.differenceWith + +## 3.19.7 + +### Patch Changes + +- [#5813](https://github.com/Effect-TS/effect/pull/5813) [`7ef13d3`](https://github.com/Effect-TS/effect/commit/7ef13d30147dd50eae1cdbb67a1978141751cad5) Thanks @tim-smart! - fix SqlPersistedQueue batch size + +## 3.19.6 + +### Patch Changes + +- [#5778](https://github.com/Effect-TS/effect/pull/5778) [`af7916a`](https://github.com/Effect-TS/effect/commit/af7916a3f00acdfc8ce451eabd3f5fb02914d0bb) Thanks @tim-smart! - add RcRef.invalidate api + +## 3.19.5 + +### Patch Changes + +- [#5772](https://github.com/Effect-TS/effect/pull/5772) [`079975c`](https://github.com/Effect-TS/effect/commit/079975c69d80c62461da5c51fe89e02c44dfa2ea) Thanks @tim-smart! - backport Effect.gen optimization + +## 3.19.4 + +### Patch Changes + +- [#5752](https://github.com/Effect-TS/effect/pull/5752) [`f445b87`](https://github.com/Effect-TS/effect/commit/f445b87bab342188a5c223cfc76c697d65594d1d) Thanks @janglad! - Fix Types.DeepMutable mapping over functions + +- [#5757](https://github.com/Effect-TS/effect/pull/5757) [`d2b68ac`](https://github.com/Effect-TS/effect/commit/d2b68ac9e1ac1d58d7387715843c448195f14675) Thanks @tim-smart! - add experimental PartitionedSemaphore module + + A `PartitionedSemaphore` is a concurrency primitive that can be used to + control concurrent access to a resource across multiple partitions identified + by keys. + + The total number of permits is shared across all partitions, with waiting + permits equally distributed among partitions using a round-robin strategy. + + This is useful when you want to limit the total number of concurrent accesses + to a resource, while still allowing for fair distribution of access across + different partitions. + + ```ts + import { Effect, PartitionedSemaphore } from "effect" + + Effect.gen(function* () { + const semaphore = yield* PartitionedSemaphore.make({ permits: 5 }) + + // Take the first 5 permits with key "A", then the following permits will be + // equally distributed between all the keys using a round-robin strategy + yield* Effect.log("A").pipe( + Effect.delay(1000), + semaphore.withPermits("A", 1), + Effect.replicateEffect(15, { concurrency: "unbounded" }), + Effect.fork + ) + yield* Effect.log("B").pipe( + Effect.delay(1000), + semaphore.withPermits("B", 1), + Effect.replicateEffect(10, { concurrency: "unbounded" }), + Effect.fork + ) + yield* Effect.log("C").pipe( + Effect.delay(1000), + semaphore.withPermits("C", 1), + Effect.replicateEffect(10, { concurrency: "unbounded" }), + Effect.fork + ) + + return yield* Effect.never + }).pipe(Effect.runFork) + ``` + +## 3.19.3 + +### Patch Changes + +- [#5712](https://github.com/Effect-TS/effect/pull/5712) [`7d28a90`](https://github.com/Effect-TS/effect/commit/7d28a908f965854cff386a19515141aea5b39eb7) Thanks @gcanti! - Use standard formatting function in Config error messages, closes #5709 + +## 3.19.2 + +### Patch Changes + +- [#5703](https://github.com/Effect-TS/effect/pull/5703) [`374f58c`](https://github.com/Effect-TS/effect/commit/374f58c10799109b61d8a131a025f3d03ce5aab5) Thanks @tim-smart! - preserve Layer.mergeAll context order + +- [#5703](https://github.com/Effect-TS/effect/pull/5703) [`374f58c`](https://github.com/Effect-TS/effect/commit/374f58c10799109b61d8a131a025f3d03ce5aab5) Thanks @tim-smart! - ensure FiberHandle.run state transition is atomic + +## 3.19.1 + +### Patch Changes + +- [#5695](https://github.com/Effect-TS/effect/pull/5695) [`63f2bf3`](https://github.com/Effect-TS/effect/commit/63f2bf393ef4bb3e46db59abdf1b2160e8ee71d4) Thanks @tim-smart! - allow parallel finalization of merged layers + +## 3.19.0 + +### Minor Changes + +- [#5606](https://github.com/Effect-TS/effect/pull/5606) [`3863fa8`](https://github.com/Effect-TS/effect/commit/3863fa89f61e63e5529fd961e37333bddf7db64a) Thanks @mikearnaldi! - Add Effect.fn.Return to allow typing returns on Effect.fn + +- [#5606](https://github.com/Effect-TS/effect/pull/5606) [`2a03c76`](https://github.com/Effect-TS/effect/commit/2a03c76c2781ca7e9e228e838eab2eb0d0795b1d) Thanks @fubhy! - Backport `Graph` module updates + +- [#5606](https://github.com/Effect-TS/effect/pull/5606) [`24a1685`](https://github.com/Effect-TS/effect/commit/24a1685c70a9ed157468650f95a5c3da3f2c2433) Thanks @tim-smart! - add experimental HashRing module + +### Patch Changes + +- [#5679](https://github.com/Effect-TS/effect/pull/5679) [`3c15d5f`](https://github.com/Effect-TS/effect/commit/3c15d5f99fb8d8470a00c5a33d9ba3cac89dfe4c) Thanks @KhraksMamtsov! - `Array.window` signature has been improved + +## 3.18.5 + +### Patch Changes + +- [#5669](https://github.com/Effect-TS/effect/pull/5669) [`a537469`](https://github.com/Effect-TS/effect/commit/a5374696bdabee005bf75d7b1b57f8bee7763cba) Thanks @fubhy! - Fix Graph.neighbors() returning self-loops in undirected graphs. + + Graph.neighbors() now correctly returns the other endpoint for undirected graphs instead of always returning edge.target, which caused nodes to appear as their own neighbors when queried from the target side of an edge. + +- [#5628](https://github.com/Effect-TS/effect/pull/5628) [`52d5963`](https://github.com/Effect-TS/effect/commit/52d59635f35406bd27874ca0090f8642432928f4) Thanks @mikearnaldi! - Make sure AsEffect is computed + +- [#5671](https://github.com/Effect-TS/effect/pull/5671) [`463345d`](https://github.com/Effect-TS/effect/commit/463345d734fb462dc284d590193b7843dc104d78) Thanks @gcanti! - JSON Schema generation: add `jsonSchema2020-12` target and fix tuple output for: + - JSON Schema 2019-09 + - OpenAPI 3.1 + +## 3.18.4 + +### Patch Changes + +- [#5617](https://github.com/Effect-TS/effect/pull/5617) [`6ae2f5d`](https://github.com/Effect-TS/effect/commit/6ae2f5da45a9ed9832605eca12b3e2bf2e2a1a67) Thanks @gcanti! - JSONSchema: Fix issue where invalid `default`s were included in the output. + + Now they are ignored, similar to invalid `examples`. + + Before + + ```ts + import { JSONSchema, Schema } from "effect" + + const schema = Schema.NonEmptyString.annotations({ + default: "" + }) + + const jsonSchema = JSONSchema.make(schema) + + console.log(JSON.stringify(jsonSchema, null, 2)) + /* + Output: + { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "string", + "description": "a non empty string", + "title": "nonEmptyString", + "default": "", + "minLength": 1 + } + */ + ``` + + After + + ```ts + import { JSONSchema, Schema } from "effect" + + const schema = Schema.NonEmptyString.annotations({ + default: "" + }) + + const jsonSchema = JSONSchema.make(schema) + + console.log(JSON.stringify(jsonSchema, null, 2)) + /* + Output: + { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "string", + "description": "a non empty string", + "title": "nonEmptyString", + "minLength": 1 + } + */ + ``` + +## 3.18.3 + +### Patch Changes + +- [#5612](https://github.com/Effect-TS/effect/pull/5612) [`25fab81`](https://github.com/Effect-TS/effect/commit/25fab8147c8c58e637332cfd9e690f777898c813) Thanks @gcanti! - Fix JSON Schema generation with `topLevelReferenceStrategy: "skip"`, closes #5611 + + This patch fixes a bug that occurred when generating JSON Schemas with nested schemas that had identifiers, while using `topLevelReferenceStrategy: "skip"`. + + Previously, the generator would still output `$ref` entries even though references were supposed to be skipped, leaving unresolved definitions. + + **Before** + + ```ts + import { JSONSchema, Schema } from "effect" + + const A = Schema.Struct({ value: Schema.String }).annotations({ + identifier: "A" + }) + const B = Schema.Struct({ a: A }).annotations({ identifier: "B" }) + + const definitions = {} + console.log( + JSON.stringify( + JSONSchema.fromAST(B.ast, { + definitions, + topLevelReferenceStrategy: "skip" + }), + null, + 2 + ) + ) + /* + { + "type": "object", + "required": ["a"], + "properties": { + "a": { + "$ref": "#/$defs/A" + } + }, + "additionalProperties": false + } + */ + console.log(definitions) + /* + { + A: { + type: "object", + required: ["value"], + properties: { value: [Object] }, + additionalProperties: false + } + } + */ + ``` + + **After** + + ```ts + import { JSONSchema, Schema } from "effect" + + const A = Schema.Struct({ value: Schema.String }).annotations({ + identifier: "A" + }) + const B = Schema.Struct({ a: A }).annotations({ identifier: "B" }) + + const definitions = {} + console.log( + JSON.stringify( + JSONSchema.fromAST(B.ast, { + definitions, + topLevelReferenceStrategy: "skip" + }), + null, + 2 + ) + ) + /* + { + "type": "object", + "required": ["a"], + "properties": { + "a": { + "type": "object", + "required": ["value"], + "properties": { + "value": { "type": "string" } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + */ + console.log(definitions) + /* + {} + */ + ``` + + Now schemas are correctly inlined, and no leftover `$ref` entries or unused definitions remain. + +## 3.18.2 + +### Patch Changes + +- [#5598](https://github.com/Effect-TS/effect/pull/5598) [`8ba4757`](https://github.com/Effect-TS/effect/commit/8ba47576c75b8b91be4bf9c1dae13995b37018af) Thanks @cyberixae! - Fix Array Do documentation + +## 3.18.1 + +### Patch Changes + +- [#5584](https://github.com/Effect-TS/effect/pull/5584) [`07802f7`](https://github.com/Effect-TS/effect/commit/07802f78fd410d800f0231129ee0866977399152) Thanks @indietyp! - Enable `console.group` use in `Logger.prettyFormat` when using Bun + +## 3.18.0 + +### Minor Changes + +- [#5302](https://github.com/Effect-TS/effect/pull/5302) [`1c6ab74`](https://github.com/Effect-TS/effect/commit/1c6ab74b314b2b6df8bb1b1a0cb9527ceda0e3fa) Thanks @schickling! - Add experimental Graph module with comprehensive graph data structure support + + This experimental module provides: + - Directed and undirected graph support + - Immutable and mutable graph variants + - Type-safe node and edge operations + - Graph algorithms: DFS, BFS, shortest paths, cycle detection, etc. + + Example usage: + + ```typescript + import { Graph } from "effect" + + // Create a graph with mutations + const graph = Graph.directed((mutable) => { + const nodeA = Graph.addNode(mutable, "Node A") + const nodeB = Graph.addNode(mutable, "Node B") + Graph.addEdge(mutable, nodeA, nodeB, 5) + }) + + console.log( + `Nodes: ${Graph.nodeCount(graph)}, Edges: ${Graph.edgeCount(graph)}` + ) + ``` + +- [#5302](https://github.com/Effect-TS/effect/pull/5302) [`70fe803`](https://github.com/Effect-TS/effect/commit/70fe803469db3355ffbf8359b52c351f1c2dc137) Thanks @mikearnaldi! - Automatically set otel parent when present as external span + +- [#5302](https://github.com/Effect-TS/effect/pull/5302) [`c296e32`](https://github.com/Effect-TS/effect/commit/c296e32554143b84ae8987046984e1cf1852417c) Thanks @tim-smart! - add Effect.Semaphore.resize + +- [#5302](https://github.com/Effect-TS/effect/pull/5302) [`a098ddf`](https://github.com/Effect-TS/effect/commit/a098ddfc551f5aa0a7c36f9b4928372a64d4d9f2) Thanks @mikearnaldi! - Introduce ReadonlyTag as the covariant side of a tag, enables: + + ```ts + import type { Context } from "effect" + import { Effect } from "effect" + + export class MyRequirement extends Effect.Service()( + "MyRequirement", + { succeed: () => 42 } + ) {} + + export class MyUseCase extends Effect.Service()("MyUseCase", { + dependencies: [MyRequirement.Default], + effect: Effect.gen(function* () { + const requirement = yield* MyRequirement + return Effect.fn("MyUseCase.execute")(function* () { + return requirement() + }) + }) + }) {} + + export function effectHandler, A, E, R>( + service: Context.ReadonlyTag Effect.Effect> + ) { + return Effect.fn("effectHandler")(function* (...args: Args) { + const execute = yield* service + yield* execute(...args) + }) + } + + export const program = effectHandler(MyUseCase) + ``` + +## 3.17.14 + +### Patch Changes + +- [#5533](https://github.com/Effect-TS/effect/pull/5533) [`ea95998`](https://github.com/Effect-TS/effect/commit/ea95998de2a7613d844c42e67e7f5b16652c5000) Thanks @IMax153! - Preserve the precision of histogram boundary values + +## 3.17.13 + +### Patch Changes + +- [#5462](https://github.com/Effect-TS/effect/pull/5462) [`51bfc78`](https://github.com/Effect-TS/effect/commit/51bfc78a7003e663f24941f7bc18485abf4caf15) Thanks @tim-smart! - ensure tracerLogger does not drop message items + +## 3.17.12 + +### Patch Changes + +- [#5456](https://github.com/Effect-TS/effect/pull/5456) [`b359bdc`](https://github.com/Effect-TS/effect/commit/b359bdca4fe25bf0485d0f744c54ec3fed48af70) Thanks @tim-smart! - add preload options to LayerMap + +## 3.17.11 + +### Patch Changes + +- [#5449](https://github.com/Effect-TS/effect/pull/5449) [`fb5e414`](https://github.com/Effect-TS/effect/commit/fb5e414943df05654db90952eb4f5339fc8cd9a1) Thanks @tim-smart! - Simplify Effect.raceAll implementation, ensure children fibers are awaited + +- [#5451](https://github.com/Effect-TS/effect/pull/5451) [`018363b`](https://github.com/Effect-TS/effect/commit/018363b9cbe3cdd553d59592cb24a0fc0fa47bdd) Thanks @mikearnaldi! - Fix Predicate.isIterable to allow strings + +## 3.17.10 + +### Patch Changes + +- [#5368](https://github.com/Effect-TS/effect/pull/5368) [`3b26094`](https://github.com/Effect-TS/effect/commit/3b2609409ac1e8c6939d699584f00b1b99c47e2e) Thanks @gcanti! - ## Annotation Behavior + + When you call `.annotations` on a schema, any identifier annotations that were previously set will now be removed. Identifiers are now always tied to the schema's `ast` reference (this was the intended behavior). + + **Example** + + ```ts + import { JSONSchema, Schema } from "effect" + + const schema = Schema.URL + + console.log(JSON.stringify(JSONSchema.make(schema), null, 2)) + /* + { + "$schema": "http://json-schema.org/draft-07/schema#", + "$defs": { + "URL": { + "type": "string", + "description": "a string to be decoded into a URL" + } + }, + "$ref": "#/$defs/URL" + } + */ + + const annotated = Schema.URL.annotations({ description: "description" }) + + console.log(JSON.stringify(JSONSchema.make(annotated), null, 2)) + /* + { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "string", + "description": "description" + } + */ + ``` + + ## OpenAPI 3.1 Compatibility + + OpenAPI 3.1 does not allow `nullable: true`. + Instead, the schema will now correctly use `{ "type": "null" }` inside a union. + + **Example** + + ```ts + import { JSONSchema, Schema } from "effect" + + const schema = Schema.NullOr(Schema.String) + + console.log( + JSON.stringify( + JSONSchema.fromAST(schema.ast, { + definitions: {}, + target: "openApi3.1" + }), + null, + 2 + ) + ) + /* + { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + */ + ``` + + ## Schema Description Deduplication + + Previously, when a schema was reused, only the first description was kept. + Now, every property keeps its own description, even if the schema is reused. + + **Example** + + ```ts + import { JSONSchema, Schema } from "effect" + + const schemaWithAnIdentifier = Schema.String.annotations({ + identifier: "my-id" + }) + + const schema = Schema.Struct({ + a: schemaWithAnIdentifier.annotations({ + description: "a-description" + }), + b: schemaWithAnIdentifier.annotations({ + description: "b-description" + }) + }) + + console.log(JSON.stringify(JSONSchema.make(schema), null, 2)) + /* + { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "required": [ + "a", + "b" + ], + "properties": { + "a": { + "type": "string", + "description": "a-description" + }, + "b": { + "type": "string", + "description": "b-description" + } + }, + "additionalProperties": false + } + */ + ``` + + ## Fragment Detection in Non-Refinement Schemas + + This patch fixes the issue where fragments (e.g. `jsonSchema.format`) were not detected on non-refinement schemas. + + **Example** + + ```ts + import { JSONSchema, Schema } from "effect" + + const schema = Schema.UUID.pipe( + Schema.compose(Schema.String), + Schema.annotations({ + identifier: "UUID", + title: "title", + description: "description", + jsonSchema: { + format: "uuid" // fragment + } + }) + ) + + console.log(JSON.stringify(JSONSchema.make(schema), null, 2)) + /* + { + "$schema": "http://json-schema.org/draft-07/schema#", + "$defs": { + "UUID": { + "type": "string", + "description": "description", + "format": "uuid", + "pattern": "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$", + "title": "title" + } + }, + "$ref": "#/$defs/UUID" + } + */ + ``` + + ## Nested Unions + + Nested unions are no longer flattened. Instead, they remain as nested `anyOf` arrays. + This is fine because JSON Schema allows nested `anyOf`. + + **Example** + + ```ts + import { JSONSchema, Schema } from "effect" + + const schema = Schema.Union( + Schema.NullOr(Schema.String), + Schema.Literal("a", null) + ) + + console.log(JSON.stringify(JSONSchema.make(schema), null, 2)) + /* + { + "$schema": "http://json-schema.org/draft-07/schema#", + "anyOf": [ + { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + { + "anyOf": [ + { + "type": "string", + "enum": [ + "a" + ] + }, + { + "type": "null" + } + ] + } + ] + } + */ + ``` + + ## Refinements without `jsonSchema` annotation + + Refinements that don't provide a `jsonSchema` annotation no longer cause errors. + They are simply ignored, so you can still generate a JSON Schema even when refinements can't easily be expressed. + +- [#5437](https://github.com/Effect-TS/effect/pull/5437) [`a33e491`](https://github.com/Effect-TS/effect/commit/a33e49153d944abd183fed93267fa7e52abae68b) Thanks @tim-smart! - ensure Effect.promise captures span on defect + +## 3.17.9 + +### Patch Changes + +- [#5422](https://github.com/Effect-TS/effect/pull/5422) [`0271f14`](https://github.com/Effect-TS/effect/commit/0271f1450c0c861f589e26ff534a73dea7ea97b7) Thanks @gcanti! - backport `formatUnknown` from v4 + +## 3.17.8 + +### Patch Changes + +- [#5407](https://github.com/Effect-TS/effect/pull/5407) [`84bc300`](https://github.com/Effect-TS/effect/commit/84bc3003b42ad51210e9e1248efd04c5d0e3dd1e) Thanks @thewilkybarkid! - Fix Schema.Defect when seeing a null-prototype object + +## 3.17.7 + +### Patch Changes + +- [#5358](https://github.com/Effect-TS/effect/pull/5358) [`a949539`](https://github.com/Effect-TS/effect/commit/a94953971c2e908890dfda00f8560d317306c328) Thanks @tim-smart! - expose RcMap.has api + +## 3.17.6 + +### Patch Changes + +- [#5322](https://github.com/Effect-TS/effect/pull/5322) [`f187941`](https://github.com/Effect-TS/effect/commit/f187941946c675713b3539fc4d5480123037563a) Thanks @beezee! - Use non-greedy matching for Schema.String in Schema.TemplateLiteralParser + +## 3.17.5 + +### Patch Changes + +- [#5315](https://github.com/Effect-TS/effect/pull/5315) [`5f98388`](https://github.com/Effect-TS/effect/commit/5f983881754fce7dc0e2d752145f3b865af27958) Thanks @patroza! - improve provide/merge apis to support readonly array inputs. + +## 3.17.4 + +### Patch Changes + +- [#5306](https://github.com/Effect-TS/effect/pull/5306) [`7d7c55d`](https://github.com/Effect-TS/effect/commit/7d7c55dadeea2f9de16e60abff124085733e1953) Thanks @leonitousconforti! - Align RcMap.keys return type with internal signature + +## 3.17.3 + +### Patch Changes + +- [#5275](https://github.com/Effect-TS/effect/pull/5275) [`3504555`](https://github.com/Effect-TS/effect/commit/35045558e7cac19c888fe677dda93c4741c7f8a8) Thanks @taylornz! - fix DateTime.makeZoned handling of DST transitions + +- [#5282](https://github.com/Effect-TS/effect/pull/5282) [`f6c7ca7`](https://github.com/Effect-TS/effect/commit/f6c7ca752fc9de5f7a2a6c439bbc6cca06566357) Thanks @beezee! - Improve inference on Metric.trackSuccessWith for use in Effect.pipe(...) + +- [#5275](https://github.com/Effect-TS/effect/pull/5275) [`3504555`](https://github.com/Effect-TS/effect/commit/35045558e7cac19c888fe677dda93c4741c7f8a8) Thanks @taylornz! - add DateTime.Disambiguation for handling DST edge cases + + Added four disambiguation strategies to `DateTime.Zoned` constructors for handling DST edge cases: + - `'compatible'` - Maintains backward compatibility + - `'earlier'` - Choose earlier time during ambiguous periods (default) + - `'later'` - Choose later time during ambiguous periods + - `'reject'` - Throw error for ambiguous times + +## 3.17.2 + +### Patch Changes + +- [#5277](https://github.com/Effect-TS/effect/pull/5277) [`6309e0a`](https://github.com/Effect-TS/effect/commit/6309e0abe16e82da8d0091fff1b9962fd9eeb585) Thanks @tim-smart! - Fix Layer.mock dual detection + +## 3.17.1 + +### Patch Changes + +- [#5262](https://github.com/Effect-TS/effect/pull/5262) [`5a0f4f1`](https://github.com/Effect-TS/effect/commit/5a0f4f176687a39d9fa46bb894bb7ac3175b0e87) Thanks @tim-smart! - remove recursion from Sink fold loop + +## 3.17.0 + +### Minor Changes + +- [#4949](https://github.com/Effect-TS/effect/pull/4949) [`40c3c87`](https://github.com/Effect-TS/effect/commit/40c3c875f724264312b43002859c82bed9ad0df9) Thanks @fubhy! - Added `Random.fixed` to create a version of the `Random` service with fixed + values for testing. + +- [#4949](https://github.com/Effect-TS/effect/pull/4949) [`ed2c74a`](https://github.com/Effect-TS/effect/commit/ed2c74ae8fa4ea0dd06ea84a3e58cd32e6916104) Thanks @dmaretskyi! - Add `Struct.entries` function + +- [#4949](https://github.com/Effect-TS/effect/pull/4949) [`073a1b8`](https://github.com/Effect-TS/effect/commit/073a1b8be5dbfa87454393ee7346f5bc36a4fd63) Thanks @f15u! - Add `Layer.mock` + + Creates a mock layer for testing purposes. You can provide a partial + implementation of the service, and any methods not provided will + throw an `UnimplementedError` defect when called. + + ```ts + import { Context, Effect, Layer } from "effect" + + class MyService extends Context.Tag("MyService")< + MyService, + { + one: Effect.Effect + two(): Effect.Effect + } + >() {} + + const MyServiceTest = Layer.mock(MyService, { + two: () => Effect.succeed(2) + }) + ``` + +- [#4949](https://github.com/Effect-TS/effect/pull/4949) [`f382e99`](https://github.com/Effect-TS/effect/commit/f382e99e409838a879246250fc3994b9bf5b3c2c) Thanks @KhraksMamtsov! - Schedule output has been added into `CurrentIterationMetadata` + +- [#4949](https://github.com/Effect-TS/effect/pull/4949) [`e8c7ba5`](https://github.com/Effect-TS/effect/commit/e8c7ba5fd3eb0c3ae3039fc24c09d69391987989) Thanks @mikearnaldi! - Remove global state index by version, make version mismatch a warning message + +- [#4949](https://github.com/Effect-TS/effect/pull/4949) [`7e10415`](https://github.com/Effect-TS/effect/commit/7e1041599ade25103428703f5d2dfd7378a09636) Thanks @devinjameson! - Array: add findFirstWithIndex function + +- [#4949](https://github.com/Effect-TS/effect/pull/4949) [`e9bdece`](https://github.com/Effect-TS/effect/commit/e9bdececdc24f60a246be5055eca71a0d49ea7f2) Thanks @vinassefranche! - Add HashMap.countBy + + ```ts + import { HashMap } from "effect" + + const map = HashMap.make([1, "a"], [2, "b"], [3, "c"]) + const result = HashMap.countBy(map, (_v, key) => key % 2 === 1) + console.log(result) // 2 + ``` + +- [#4949](https://github.com/Effect-TS/effect/pull/4949) [`8d95eb0`](https://github.com/Effect-TS/effect/commit/8d95eb0356b1d1736204836c275d201a547d208d) Thanks @tim-smart! - add Effect.ensure{Success,Error,Requirements}Type, for constraining Effect types + +## 3.16.17 + +### Patch Changes + +- [#5246](https://github.com/Effect-TS/effect/pull/5246) [`aaa6ad0`](https://github.com/Effect-TS/effect/commit/aaa6ad0673f843a27954fd92821961cce33941ad) Thanks @mikearnaldi! - Copy over apply, bind, call into service proxy + +- [#5158](https://github.com/Effect-TS/effect/pull/5158) [`5b74ea5`](https://github.com/Effect-TS/effect/commit/5b74ea5e5862742e2fb60feefb765bc8681171f4) Thanks @cyberixae! - Clarify Tuple length requirements + +## 3.16.16 + +### Patch Changes + +- [#5224](https://github.com/Effect-TS/effect/pull/5224) [`127e602`](https://github.com/Effect-TS/effect/commit/127e602ee647839198f44d19cff7d11f6e4b473b) Thanks @tim-smart! - prevent fiber leak when Stream.toAsyncIterable returns early + +## 3.16.15 + +### Patch Changes + +- [#5222](https://github.com/Effect-TS/effect/pull/5222) [`15df9bf`](https://github.com/Effect-TS/effect/commit/15df9bf0c7a11e775c04e69516e47c5094146d55) Thanks @gcanti! - Schema.attachPropertySignature: simplify signature and fix parameter type to use Schema instead of SchemaClass + +## 3.16.14 + +### Patch Changes + +- [#5213](https://github.com/Effect-TS/effect/pull/5213) [`f5dfabf`](https://github.com/Effect-TS/effect/commit/f5dfabf51ba481a4468c1509c537314978ef6cec) Thanks @gcanti! - Fix incorrect schema ID annotation in `Schema.lessThanOrEqualToDate`, closes #5212 + +- [#5192](https://github.com/Effect-TS/effect/pull/5192) [`17a5ea8`](https://github.com/Effect-TS/effect/commit/17a5ea8fa29785fe6e4c9480f2a2e9c8c59f3f38) Thanks @nikelborm! - Updated deprecated OTel Resource attributes names and values. + + Many of the attributes have undergone the process of deprecation not once, but twice. Most of the constants holding attribute names have been renamed. These are minor changes. + + Additionally, there were numerous changes to the attribute keys themselves. These changes can be considered major. + + In the `@opentelemetry/semantic-conventions` package, new attributes having ongoing discussion about them are going through a process called incubation, until a consensus about their necessity and form is reached. Otel team [recommends](https://github.com/open-telemetry/opentelemetry-js/blob/main/semantic-conventions/README.md#unstable-semconv) devs to copy them directly into their code. Luckily, it's not necessary because all of the new attribute names and values came out of this process (some of them were changed again) and are now considered stable. + + ## Reasoning for minor version bump + + | Package | Major attribute changes | Major value changes | + | -------------------------- | ----------------------------------------------------------------------------- | --------------------------------- | + | Clickhouse client | `db.system` -> `db.system.name`
`db.name` -> `db.namespace` | | + | MsSQL client | `db.system` -> `db.system.name`
`db.name` -> `db.namespace` | `mssql` -> `microsoft.sql_server` | + | MySQL client | `db.system` -> `db.system.name`
`db.name` -> `db.namespace` | | + | Pg client | `db.system` -> `db.system.name`
`db.name` -> `db.namespace` | | + | Bun SQLite client | `db.system` -> `db.system.name` | | + | Node SQLite client | `db.system` -> `db.system.name` | | + | React.Native SQLite client | `db.system` -> `db.system.name` | | + | Wasm SQLite client | `db.system` -> `db.system.name` | | + | SQLite Do client | `db.system` -> `db.system.name` | | + | LibSQL client | `db.system` -> `db.system.name` | | + | D1 client | `db.system` -> `db.system.name` | | + | Kysely client | `db.statement` -> `db.query.text` | | + | @effect/sql | `db.statement` -> `db.query.text`
`db.operation` -> `db.operation.name` | | + +- [#5211](https://github.com/Effect-TS/effect/pull/5211) [`d25f22b`](https://github.com/Effect-TS/effect/commit/d25f22be7598abe977caf6cdac3b0dd78b438c48) Thanks @mattiamanzati! - Removed some unnecessary single-arg pipe calls + +## 3.16.13 + +### Patch Changes + +- [#5097](https://github.com/Effect-TS/effect/pull/5097) [`c1c05a8`](https://github.com/Effect-TS/effect/commit/c1c05a8242fb5df7445b4a12387a60eac7726eb7) Thanks @tim-smart! - remove completion helper overload from Effect.catchTag, to fix Effect.fn inference + +- [#5157](https://github.com/Effect-TS/effect/pull/5157) [`81fe4a2`](https://github.com/Effect-TS/effect/commit/81fe4a2c81d5e30e180a60e68c52016a27b350db) Thanks @cyberixae! - Clarify Array rotate example + +## 3.16.12 + +### Patch Changes + +- [#5149](https://github.com/Effect-TS/effect/pull/5149) [`905da99`](https://github.com/Effect-TS/effect/commit/905da996aad665057b4ca6dba1a4af44fb8835bd) Thanks @milkyskies! - Fix `$match` to disallow invalid `_tag` keys in `TaggedEnum` handler objects. + +## 3.16.11 + +### Patch Changes + +- [#5127](https://github.com/Effect-TS/effect/pull/5127) [`99590a6`](https://github.com/Effect-TS/effect/commit/99590a6ca9128eb1ede265b6670b655311995614) Thanks @tim-smart! - fix DateTime zone check to includes zones without ":" + +- [#5123](https://github.com/Effect-TS/effect/pull/5123) [`6c3e24c`](https://github.com/Effect-TS/effect/commit/6c3e24c2308f7d4a29b8f4270ab81bca22ac6bb4) Thanks @gcanti! - Schema.equivalence: handle non-array and non-record inputs + +## 3.16.10 + +### Patch Changes + +- [#5100](https://github.com/Effect-TS/effect/pull/5100) [`faad30e`](https://github.com/Effect-TS/effect/commit/faad30ec8742916be59f9db642d0fc98225b636c) Thanks @tim-smart! - relax Predicate.compose constraint on second refinement + +## 3.16.9 + +### Patch Changes + +- [#5081](https://github.com/Effect-TS/effect/pull/5081) [`5137c70`](https://github.com/Effect-TS/effect/commit/5137c703461d8d3b363c112140a6e7f798241d07) Thanks @tim-smart! - expose Stream.provideSomeContext + +- [#5082](https://github.com/Effect-TS/effect/pull/5082) [`c23d25c`](https://github.com/Effect-TS/effect/commit/c23d25c3e7c541f1f63b28484d8c461d86c67e99) Thanks @tim-smart! - fix Effect.filterOrFail return type inference + +## 3.16.8 + +### Patch Changes + +- [#5047](https://github.com/Effect-TS/effect/pull/5047) [`8cb98d5`](https://github.com/Effect-TS/effect/commit/8cb98d53e68330228287ce2a2e0d8a4c86bcab3b) Thanks @tim-smart! - ensure Stream.toReadableStream ignores empty chunks + +- [#5046](https://github.com/Effect-TS/effect/pull/5046) [`db2dd3c`](https://github.com/Effect-TS/effect/commit/db2dd3c3a8a77d791eae19e66153527e1cde4e6e) Thanks @tim-smart! - ignore ReadableStream defect in bun due to controller bug + +## 3.16.7 + +### Patch Changes + +- [#5033](https://github.com/Effect-TS/effect/pull/5033) [`1bb0d8a`](https://github.com/Effect-TS/effect/commit/1bb0d8ab96782e99434356266b38251554ea0294) Thanks @tim-smart! - ensure DateTime.make interprets strings without zone as UTC + +## 3.16.6 + +### Patch Changes + +- [#5026](https://github.com/Effect-TS/effect/pull/5026) [`a5f7595`](https://github.com/Effect-TS/effect/commit/a5f75956ef9a15a83c416517ef493f0ee2f5ee8a) Thanks @KhraksMamtsov! - Add missing type variances + +- [#5031](https://github.com/Effect-TS/effect/pull/5031) [`a02470c`](https://github.com/Effect-TS/effect/commit/a02470c75579e91525a25adb3f21b3650d042fdd) Thanks @KhraksMamtsov! - Fix Context.add & Context.make signatures + +- [#5003](https://github.com/Effect-TS/effect/pull/5003) [`f891d45`](https://github.com/Effect-TS/effect/commit/f891d45adffdafd3f94a2eca23faa354e3a409a8) Thanks @beezee! - Ensure binding `__proto__` to lexical scope in do notation is preserved by `bind` and `let` + +## 3.16.5 + +### Patch Changes + +- [#5008](https://github.com/Effect-TS/effect/pull/5008) [`bf418ef`](https://github.com/Effect-TS/effect/commit/bf418ef14a0f2ec965535793d5cea8fa8ba177ac) Thanks @jdharrisnz! - Record.findFirst: Accept ReadonlyRecord type input and optimise the loop + +## 3.16.4 + +### Patch Changes + +- [#4994](https://github.com/Effect-TS/effect/pull/4994) [`74ab9a0`](https://github.com/Effect-TS/effect/commit/74ab9a0a9e16d6e019369d256e1e24175c8bc3f3) Thanks @tim-smart! - don't inherit interruption flag in Effect.addFinalizer + +- [#4986](https://github.com/Effect-TS/effect/pull/4986) [`770008e`](https://github.com/Effect-TS/effect/commit/770008eca3aad2899a2ed951236e575793294b28) Thanks @tim-smart! - ensure Cause.YieldableError extends Error + +## 3.16.3 + +### Patch Changes + +- [#4952](https://github.com/Effect-TS/effect/pull/4952) [`87722fc`](https://github.com/Effect-TS/effect/commit/87722fce693a9b49284bbddbf82d30714c688261) Thanks @tim-smart! - improve Effect.catchTag auto-completion + +- [#4950](https://github.com/Effect-TS/effect/pull/4950) [`36217ee`](https://github.com/Effect-TS/effect/commit/36217eeb1337edd9ac3f9a635b80a6385d22ae8f) Thanks @tim-smart! - remove `this` type propagation from Effect.fn + +## 3.16.2 + +### Patch Changes + +- [#4943](https://github.com/Effect-TS/effect/pull/4943) [`0ddf148`](https://github.com/Effect-TS/effect/commit/0ddf148a247aa87af043d276b8453a714a400897) Thanks @gcanti! - relax `Schema.brand` constraint, closes #4942 + +## 3.16.1 + +### Patch Changes + +- [#4936](https://github.com/Effect-TS/effect/pull/4936) [`71174d0`](https://github.com/Effect-TS/effect/commit/71174d09691314a9b6b66189e456fd21e3eb6543) Thanks @mattiamanzati! - Escape JSON Schema $id for empty struct + +- [#4937](https://github.com/Effect-TS/effect/pull/4937) [`d615e6e`](https://github.com/Effect-TS/effect/commit/d615e6e5b944f6fd5e627e31752c7ca7e4e1c17d) Thanks @tim-smart! - adjust ExecutionPlan `provides` & `requirements` types + +## 3.16.0 + +### Minor Changes + +- [#4891](https://github.com/Effect-TS/effect/pull/4891) [`ee0bd5d`](https://github.com/Effect-TS/effect/commit/ee0bd5d24864752c54cb359f67a67dd903971ec4) Thanks @KhraksMamtsov! - `Schedule.CurrentIterationMetadata` has been added + + ```ts + import { Effect, Schedule } from "effect" + + Effect.gen(function* () { + const currentIterationMetadata = yield* Schedule.CurrentIterationMetadata + // ^? Schedule.IterationMetadata + + console.log(currentIterationMetadata) + }).pipe(Effect.repeat(Schedule.recurs(2))) + // { + // elapsed: Duration.zero, + // elapsedSincePrevious: Duration.zero, + // input: undefined, + // now: 0, + // recurrence: 0, + // start: 0 + // } + // { + // elapsed: Duration.zero, + // elapsedSincePrevious: Duration.zero, + // input: undefined, + // now: 0, + // recurrence: 1, + // start: 0 + // } + // { + // elapsed: Duration.zero, + // elapsedSincePrevious: Duration.zero, + // input: undefined, + // now: 0, + // recurrence: 2, + // start: 0 + // } + + Effect.gen(function* () { + const currentIterationMetadata = yield* Schedule.CurrentIterationMetadata + + console.log(currentIterationMetadata) + }).pipe( + Effect.schedule( + Schedule.intersect(Schedule.fibonacci("1 second"), Schedule.recurs(3)) + ) + ) + // { + // elapsed: Duration.zero, + // elapsedSincePrevious: Duration.zero, + // recurrence: 1, + // input: undefined, + // now: 0, + // start: 0 + // }, + // { + // elapsed: Duration.seconds(1), + // elapsedSincePrevious: Duration.seconds(1), + // recurrence: 2, + // input: undefined, + // now: 1000, + // start: 0 + // }, + // { + // elapsed: Duration.seconds(2), + // elapsedSincePrevious: Duration.seconds(1), + // recurrence: 3, + // input: undefined, + // now: 2000, + // start: 0 + // } + ``` + +- [#4891](https://github.com/Effect-TS/effect/pull/4891) [`5189800`](https://github.com/Effect-TS/effect/commit/51898004e11766b8cf6d95e960b636f6d5db79ec) Thanks @vinassefranche! - Add HashMap.hasBy helper + + ```ts + import { HashMap } from "effect" + + const hm = HashMap.make([1, "a"]) + HashMap.hasBy(hm, (value, key) => value === "a" && key === 1) // -> true + HashMap.hasBy(hm, (value) => value === "b") // -> false + ``` + +- [#4891](https://github.com/Effect-TS/effect/pull/4891) [`58bfeaa`](https://github.com/Effect-TS/effect/commit/58bfeaa64ded8c88f772b184311c0c0dbac10960) Thanks @jrudder! - Add round and sumAll to BigDecimal + +- [#4891](https://github.com/Effect-TS/effect/pull/4891) [`194d748`](https://github.com/Effect-TS/effect/commit/194d7486943f56f3267ef415395ac220a4b3e634) Thanks @tim-smart! - add ExecutionPlan module + + A `ExecutionPlan` can be used with `Effect.withExecutionPlan` or `Stream.withExecutionPlan`, allowing you to provide different resources for each step of execution until the effect succeeds or the plan is exhausted. + + ```ts + import { type AiLanguageModel } from "@effect/ai" + import type { Layer } from "effect" + import { Effect, ExecutionPlan, Schedule } from "effect" + + declare const layerBad: Layer.Layer + declare const layerGood: Layer.Layer + + const ThePlan = ExecutionPlan.make( + { + // First try with the bad layer 2 times with a 3 second delay between attempts + provide: layerBad, + attempts: 2, + schedule: Schedule.spaced(3000) + }, + // Then try with the bad layer 3 times with a 1 second delay between attempts + { + provide: layerBad, + attempts: 3, + schedule: Schedule.spaced(1000) + }, + // Finally try with the good layer. + // + // If `attempts` is omitted, the plan will only attempt once, unless a schedule is provided. + { + provide: layerGood + } + ) + + declare const effect: Effect.Effect< + void, + never, + AiLanguageModel.AiLanguageModel + > + const withPlan: Effect.Effect = Effect.withExecutionPlan( + effect, + ThePlan + ) + ``` + +- [#4891](https://github.com/Effect-TS/effect/pull/4891) [`918c9ea`](https://github.com/Effect-TS/effect/commit/918c9ea1a57facb154f0fb26792021f337054dee) Thanks @thewilkybarkid! - Add Array.removeOption and Chunk.removeOption + +- [#4891](https://github.com/Effect-TS/effect/pull/4891) [`9198e6f`](https://github.com/Effect-TS/effect/commit/9198e6fcc1a3ff4fefb3363004de558d8de01f40) Thanks @TylorS! - Add parameter support for Effect.Service + + This allows you to pass parameters to the `effect` & `scoped` Effect.Service + constructors, which will also be reflected in the `.Default` layer. + + ```ts + import type { Layer } from "effect" + import { Effect } from "effect" + + class NumberService extends Effect.Service()("NumberService", { + // You can now pass a function to the `effect` and `scoped` constructors + effect: Effect.fn(function* (input: number) { + return { + get: Effect.succeed(`The number is: ${input}`) + } as const + }) + }) {} + + // Pass the arguments to the `Default` layer + const CoolNumberServiceLayer: Layer.Layer = + NumberService.Default(6942) + ``` + +- [#4891](https://github.com/Effect-TS/effect/pull/4891) [`2a370bf`](https://github.com/Effect-TS/effect/commit/2a370bf625fdeede5659721468eb0d527e403279) Thanks @vinassefranche! - Add `Iterable.countBy` and `Array.countBy` + + ```ts + import { Array, Iterable } from "effect" + + const resultArray = Array.countBy([1, 2, 3, 4, 5], (n) => n % 2 === 0) + console.log(resultArray) // 2 + + const resultIterable = resultIterable.countBy( + [1, 2, 3, 4, 5], + (n) => n % 2 === 0 + ) + console.log(resultIterable) // 2 + ``` + +- [#4891](https://github.com/Effect-TS/effect/pull/4891) [`58ccb91`](https://github.com/Effect-TS/effect/commit/58ccb91328c8df5d49808b673738bc09df355201) Thanks @KhraksMamtsov! - The `Config.port` and `Config.branded` functions have been added. + + ```ts + import { Brand, Config } from "effect" + + type DbPort = Brand.Branded + const DbPort = Brand.nominal() + + const dbPort: Config.Config = Config.branded( + Config.port("DB_PORT"), + DbPort + ) + ``` + + ```ts + import { Brand, Config } from "effect" + + type Port = Brand.Branded + const Port = Brand.refined( + (num) => + !Number.isNaN(num) && Number.isInteger(num) && num >= 1 && num <= 65535, + (n) => Brand.error(`Expected ${n} to be an TCP port`) + ) + + const dbPort: Config.Config = Config.number("DB_PORT").pipe( + Config.branded(Port) + ) + ``` + +- [#4891](https://github.com/Effect-TS/effect/pull/4891) [`fd47834`](https://github.com/Effect-TS/effect/commit/fd478348203fa89462b0a1d067ce4de034353df4) Thanks @tim-smart! - return a proxy Layer from LayerMap service + + The new usage is: + + ```ts + import { NodeRuntime } from "@effect/platform-node" + import { Context, Effect, FiberRef, Layer, LayerMap } from "effect" + + class Greeter extends Context.Tag("Greeter")< + Greeter, + { + greet: Effect.Effect + } + >() {} + + // create a service that wraps a LayerMap + class GreeterMap extends LayerMap.Service()("GreeterMap", { + // define the lookup function for the layer map + // + // The returned Layer will be used to provide the Greeter service for the + // given name. + lookup: (name: string) => + Layer.succeed(Greeter, { + greet: Effect.succeed(`Hello, ${name}!`) + }), + + // If a layer is not used for a certain amount of time, it can be removed + idleTimeToLive: "5 seconds", + + // Supply the dependencies for the layers in the LayerMap + dependencies: [] + }) {} + + // usage + const program: Effect.Effect = Effect.gen( + function* () { + // access and use the Greeter service + const greeter = yield* Greeter + yield* Effect.log(yield* greeter.greet) + } + ).pipe( + // use the GreeterMap service to provide a variant of the Greeter service + Effect.provide(GreeterMap.get("John")) + ) + + // run the program + program.pipe(Effect.provide(GreeterMap.Default), NodeRuntime.runMain) + ``` + +## 3.15.5 + +### Patch Changes + +- [#4924](https://github.com/Effect-TS/effect/pull/4924) [`cc5bb2b`](https://github.com/Effect-TS/effect/commit/cc5bb2b918a9450a975f702dabcea891bda382cb) Thanks @KhraksMamtsov! - Fix type inference for Effect suptypes in NonGen case + +## 3.15.4 + +### Patch Changes + +- [#4869](https://github.com/Effect-TS/effect/pull/4869) [`f570554`](https://github.com/Effect-TS/effect/commit/f57055459524587b041340577dad85476bb35f81) Thanks @IGassmann! - Fix summary metric’s min/max values when no samples + +- [#4917](https://github.com/Effect-TS/effect/pull/4917) [`78047e8`](https://github.com/Effect-TS/effect/commit/78047e8dfc8005b66f87afe50bb95981fea51561) Thanks @KhraksMamtsov! - Fix Effect.fn inference in case of use with pipe functions + +## 3.15.3 + +### Patch Changes + +- [#4907](https://github.com/Effect-TS/effect/pull/4907) [`4577f54`](https://github.com/Effect-TS/effect/commit/4577f548d67273e576cdde423bdd34a4b910766a) Thanks @mattiamanzati! - Escape JSON-pointers + +## 3.15.2 + +### Patch Changes + +- [#4659](https://github.com/Effect-TS/effect/pull/4659) [`b8722b8`](https://github.com/Effect-TS/effect/commit/b8722b817e2306fe8c8245f3f9e32d85b824b961) Thanks @KhraksMamtsov! - - The `HashMap.has/get` family has become more type-safe. + - Fix the related type errors in TestAnnotationsMap.ts. + +## 3.15.1 + +### Patch Changes + +- [#4870](https://github.com/Effect-TS/effect/pull/4870) [`787ce70`](https://github.com/Effect-TS/effect/commit/787ce7042e35b657963473c6efe47752868cd811) Thanks @tim-smart! - ensure generic refinements work with Effect.filterOr\* + +- [#4857](https://github.com/Effect-TS/effect/pull/4857) [`1269641`](https://github.com/Effect-TS/effect/commit/1269641a99ae43069f7648ff79ffe8729b54b348) Thanks @tim-smart! - preserve explicit `this` in Effect.fn apis + +- [#4857](https://github.com/Effect-TS/effect/pull/4857) [`1269641`](https://github.com/Effect-TS/effect/commit/1269641a99ae43069f7648ff79ffe8729b54b348) Thanks @tim-smart! - use span name as function name in Effect.fn + +## 3.15.0 + +### Minor Changes + +- [#4641](https://github.com/Effect-TS/effect/pull/4641) [`c654595`](https://github.com/Effect-TS/effect/commit/c65459587b51da140b78098e81fdbfece65d53e2) Thanks @tim-smart! - Add Layer.setRandom, for over-riding the default Random service + +- [#4641](https://github.com/Effect-TS/effect/pull/4641) [`d9f5dea`](https://github.com/Effect-TS/effect/commit/d9f5deae0f02f5de2b9fcb1cca8b142ba4bc2bba) Thanks @KhraksMamtsov! - `Brand.unbranded` getter has been added + +- [#4641](https://github.com/Effect-TS/effect/pull/4641) [`49aa723`](https://github.com/Effect-TS/effect/commit/49aa7236a15e13f818c86edbca08c4af67c8dfaf) Thanks @titouancreach! - Add Either.transposeMapOption + +- [#4641](https://github.com/Effect-TS/effect/pull/4641) [`74c14d0`](https://github.com/Effect-TS/effect/commit/74c14d01d0cb48cf517a1b6e29a373a96ed0ff5b) Thanks @vinassefranche! - Add Record.findFirst + +- [#4641](https://github.com/Effect-TS/effect/pull/4641) [`e4f49b6`](https://github.com/Effect-TS/effect/commit/e4f49b66857e01b74ab6a9a0bc7132f44cd04cbb) Thanks @KhraksMamtsov! - Default `never` type has been added to `MutableHasMap.empty` & `MutableList.empty` ctors + +- [#4641](https://github.com/Effect-TS/effect/pull/4641) [`6f02224`](https://github.com/Effect-TS/effect/commit/6f02224b3fc46a682ad2defb1a260841956c6780) Thanks @tim-smart! - add Stream.toAsyncIterable\* apis + + ```ts + import { Stream } from "effect" + + // Will print: + // 1 + // 2 + // 3 + const stream = Stream.make(1, 2, 3) + for await (const result of Stream.toAsyncIterable(stream)) { + console.log(result) + } + ``` + +- [#4641](https://github.com/Effect-TS/effect/pull/4641) [`1dcfd41`](https://github.com/Effect-TS/effect/commit/1dcfd41ff96abd706901293a00c1893cb29dd8fd) Thanks @tim-smart! - improve Effect.filter\* types to exclude candidates in fallback functions + +- [#4641](https://github.com/Effect-TS/effect/pull/4641) [`b21ab16`](https://github.com/Effect-TS/effect/commit/b21ab16b6f773e7ec4369db4e752c35e719f7870) Thanks @KhraksMamtsov! - Simplified the creation of pipeable classes. + + ```ts + class MyClass extends Pipeable.Class() { + constructor(public a: number) { + super() + } + methodA() { + return this.a + } + } + console.log(new MyClass(2).pipe((x) => x.methodA())) // 2 + ``` + + ```ts + class A { + constructor(public a: number) {} + methodA() { + return this.a + } + } + class B extends Pipeable.Class(A) { + constructor(private b: string) { + super(b.length) + } + methodB() { + return [this.b, this.methodA()] + } + } + console.log(new B("pipe").pipe((x) => x.methodB())) // ['pipe', 4] + ``` + +- [#4641](https://github.com/Effect-TS/effect/pull/4641) [`fcf1822`](https://github.com/Effect-TS/effect/commit/fcf1822f98fcda60351d64e9d2c2c13563d7e6db) Thanks @KhraksMamtsov! - property `message: string` has been added to `ConfigError.And` & `Or` members + +- [#4641](https://github.com/Effect-TS/effect/pull/4641) [`0061dd1`](https://github.com/Effect-TS/effect/commit/0061dd140740165e91569a684cce27a77b23229e) Thanks @tim-smart! - allow catching multiple different tags in Effect.catchTag + +- [#4641](https://github.com/Effect-TS/effect/pull/4641) [`8421e6e`](https://github.com/Effect-TS/effect/commit/8421e6e49332bca8f96f482dfd48680e238b3a89) Thanks @mlegenhausen! - Expose `Cause.isTimeoutException` + +- [#4641](https://github.com/Effect-TS/effect/pull/4641) [`fa10f56`](https://github.com/Effect-TS/effect/commit/fa10f56b96bd9af070ba99ebc3279aa93954261e) Thanks @thewilkybarkid! - Support multiple values in Function.apply + +## 3.14.22 + +### Patch Changes + +- [#4847](https://github.com/Effect-TS/effect/pull/4847) [`24a9ebb`](https://github.com/Effect-TS/effect/commit/24a9ebbb5af598f0bfd6ecc45307e528043fe011) Thanks @gcanti! - Schema: TaggedError no longer crashes when the `message` field is explicitly defined. + + If you define a `message` field in your schema, `TaggedError` will no longer add its own `message` getter. This avoids a stack overflow caused by infinite recursion. + + Before + + ```ts + import { Schema } from "effect" + + class Todo extends Schema.TaggedError()("Todo", { + message: Schema.optional(Schema.String) + }) {} + + // ❌ Throws "Maximum call stack size exceeded" + console.log(Todo.make({})) + ``` + + After + + ```ts + // ✅ Works correctly + console.log(Todo.make({})) + ``` + +## 3.14.21 + +### Patch Changes + +- [#4837](https://github.com/Effect-TS/effect/pull/4837) [`2f3b7d4`](https://github.com/Effect-TS/effect/commit/2f3b7d4e1fa1ef8790b0ca4da22eb88872ee31df) Thanks @tim-smart! - fix Mailbox.fromStream + +## 3.14.20 + +### Patch Changes + +- [#4832](https://github.com/Effect-TS/effect/pull/4832) [`17e2f30`](https://github.com/Effect-TS/effect/commit/17e2f3091408cf0fca9414d4af3bdf7b2765b378) Thanks @gcanti! - JSONSchema: respect annotations on declarations. + + Previously, annotations added with `.annotations(...)` on `Schema.declare(...)` were not included in the generated JSON Schema output. + + Before + + ```ts + import { JSONSchema, Schema } from "effect" + + class MyType {} + + const schema = Schema.declare((x) => x instanceof MyType, { + jsonSchema: { + type: "my-type" + } + }).annotations({ + title: "My Title", + description: "My Description" + }) + + console.log(JSON.stringify(JSONSchema.make(schema), null, 2)) + /* + { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "my-type" + } + */ + ``` + + After + + ```ts + import { JSONSchema, Schema } from "effect" + + class MyType {} + + const schema = Schema.declare((x) => x instanceof MyType, { + jsonSchema: { + type: "my-type" + } + }).annotations({ + title: "My Title", + description: "My Description" + }) + + console.log(JSON.stringify(JSONSchema.make(schema), null, 2)) + /* + { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "My Description", + "title": "My Title", + "type": "my-type" + } + */ + ``` + +## 3.14.19 + +### Patch Changes + +- [#4822](https://github.com/Effect-TS/effect/pull/4822) [`056a910`](https://github.com/Effect-TS/effect/commit/056a910d0a0b8b00b0dc9df4a070466b2b5c2f6c) Thanks @KhraksMamtsov! - fix `Layer.discard` jsdoc + +- [#4816](https://github.com/Effect-TS/effect/pull/4816) [`3273d57`](https://github.com/Effect-TS/effect/commit/3273d572c2b3175a842677f19efeea4cd65ab016) Thanks @mikearnaldi! - Fix captureStackTrace for bun + +## 3.14.18 + +### Patch Changes + +- [#4809](https://github.com/Effect-TS/effect/pull/4809) [`b1164d4`](https://github.com/Effect-TS/effect/commit/b1164d49a1dfdf299e9971367b6fc6be4df0ddff) Thanks @tim-smart! - fix refinement narrowing in Match + +## 3.14.17 + +### Patch Changes + +- [#4806](https://github.com/Effect-TS/effect/pull/4806) [`0b54681`](https://github.com/Effect-TS/effect/commit/0b54681cd89245e211d8f49272be0f1bf2f81813) Thanks @thewilkybarkid! - Match the JS API for locale arguments + +- [#4805](https://github.com/Effect-TS/effect/pull/4805) [`41a59d5`](https://github.com/Effect-TS/effect/commit/41a59d5916a296b12b0d5ead9e859e05f40b4cce) Thanks @mikearnaldi! - Implement stack cleaning for Bun + +## 3.14.16 + +### Patch Changes + +- [#4800](https://github.com/Effect-TS/effect/pull/4800) [`ee14444`](https://github.com/Effect-TS/effect/commit/ee144441021ec77039e43396eaf90714687bb495) Thanks @tim-smart! - improve Match refinement resolution + +## 3.14.15 + +### Patch Changes + +- [#4798](https://github.com/Effect-TS/effect/pull/4798) [`239cc99`](https://github.com/Effect-TS/effect/commit/239cc995ce645946210a3c3d2cb52bd3547c0687) Thanks @gcanti! - Schema: respect custom constructors in `make` for `Schema.Class`, closes #4797 + + Previously, the `make` method did not support custom constructors defined using `Schema.Class` or `Schema.TaggedError`, resulting in type errors when passing custom constructor arguments. + + This update ensures that `make` now correctly uses the class constructor, allowing custom parameters and initialization logic. + + Before + + ```ts + import { Schema } from "effect" + + class MyError extends Schema.TaggedError()("MyError", { + message: Schema.String + }) { + constructor({ a, b }: { a: string; b: string }) { + super({ message: `${a}:${b}` }) + } + } + + // @ts-expect-error: Object literal may only specify known properties, and 'a' does not exist in type '{ readonly message: string; }'.ts(2353) + MyError.make({ a: "1", b: "2" }) + ``` + + After + + ```ts + import { Schema } from "effect" + + class MyError extends Schema.TaggedError()("MyError", { + message: Schema.String + }) { + constructor({ a, b }: { a: string; b: string }) { + super({ message: `${a}:${b}` }) + } + } + + console.log(MyError.make({ a: "1", b: "2" }).message) + // Output: "1:2" + ``` + +- [#4687](https://github.com/Effect-TS/effect/pull/4687) [`8b6c947`](https://github.com/Effect-TS/effect/commit/8b6c947eaa8e45a67ecb3c37d45cd27f3e41d165) Thanks @KhraksMamtsov! - Modify the signatures of `Either.liftPredicate` and `Effect.predicate` to make them reusable. + +- [#4794](https://github.com/Effect-TS/effect/pull/4794) [`c50a63b`](https://github.com/Effect-TS/effect/commit/c50a63bbecb9f560b9cae349c447eed877d1b9b6) Thanks @IGassmann! - Fix summary metric’s quantile value calculation + +## 3.14.14 + +### Patch Changes + +- [#4786](https://github.com/Effect-TS/effect/pull/4786) [`6ed8d15`](https://github.com/Effect-TS/effect/commit/6ed8d1589beb181d30abc79afebdaabc1d101538) Thanks @tim-smart! - drop use of performance.timeOrigin in clock + +## 3.14.13 + +### Patch Changes + +- [#4777](https://github.com/Effect-TS/effect/pull/4777) [`ee77788`](https://github.com/Effect-TS/effect/commit/ee77788747e7ebbde6bfa88256cde49dbbad3608) Thanks @gcanti! - JSONSchema: apply `encodeOption` to each example and retain successful results. + + **Example** + + ```ts + import { JSONSchema, Schema } from "effect" + + const schema = Schema.Struct({ + a: Schema.propertySignature(Schema.BigInt).annotations({ + examples: [1n, 2n] + }) + }) + + console.log(JSON.stringify(JSONSchema.make(schema), null, 2)) + /* + { + "$schema": "http://json-schema.org/draft-07/schema#", + "$defs": { + "BigInt": { + "type": "string", + "description": "a string to be decoded into a bigint" + } + }, + "type": "object", + "required": [ + "a" + ], + "properties": { + "a": { + "$ref": "#/$defs/BigInt", + "examples": [ + "1", + "2" + ] + } + }, + "additionalProperties": false + } + */ + ``` + +- [#4701](https://github.com/Effect-TS/effect/pull/4701) [`5fce6ba`](https://github.com/Effect-TS/effect/commit/5fce6ba19c3cc63cc0104e737e581ad989dedbf0) Thanks @gcanti! - Fix `JSONSchema.make` for `Exit` schemas. + + Before + + ```ts + import { JSONSchema, Schema } from "effect" + + const schema = Schema.Exit({ + failure: Schema.String, + success: Schema.Number, + defect: Schema.Defect + }) + + console.log(JSON.stringify(JSONSchema.make(schema), null, 2)) + /* + throws + Error: Missing annotation + at path: ["cause"]["left"] + details: Generating a JSON Schema for this schema requires an "identifier" annotation + schema (Suspend): CauseEncoded + */ + ``` + + After + + ```ts + import { JSONSchema, Schema } from "effect" + + const schema = Schema.Exit({ + failure: Schema.String, + success: Schema.Number, + defect: Schema.Defect + }) + + console.log(JSON.stringify(JSONSchema.make(schema), null, 2)) + /* + Output: + { + "$schema": "http://json-schema.org/draft-07/schema#", + "$defs": { + "CauseEncoded0": { + "anyOf": [ + { + "type": "object", + "required": [ + "_tag" + ], + "properties": { + "_tag": { + "type": "string", + "enum": [ + "Empty" + ] + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "_tag", + "error" + ], + "properties": { + "_tag": { + "type": "string", + "enum": [ + "Fail" + ] + }, + "error": { + "type": "string" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "_tag", + "defect" + ], + "properties": { + "_tag": { + "type": "string", + "enum": [ + "Die" + ] + }, + "defect": { + "$ref": "#/$defs/Defect" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "_tag", + "fiberId" + ], + "properties": { + "_tag": { + "type": "string", + "enum": [ + "Interrupt" + ] + }, + "fiberId": { + "$ref": "#/$defs/FiberIdEncoded" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "_tag", + "left", + "right" + ], + "properties": { + "_tag": { + "type": "string", + "enum": [ + "Sequential" + ] + }, + "left": { + "$ref": "#/$defs/CauseEncoded0" + }, + "right": { + "$ref": "#/$defs/CauseEncoded0" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "_tag", + "left", + "right" + ], + "properties": { + "_tag": { + "type": "string", + "enum": [ + "Parallel" + ] + }, + "left": { + "$ref": "#/$defs/CauseEncoded0" + }, + "right": { + "$ref": "#/$defs/CauseEncoded0" + } + }, + "additionalProperties": false + } + ], + "title": "CauseEncoded" + }, + "Defect": { + "$id": "/schemas/unknown", + "title": "unknown" + }, + "FiberIdEncoded": { + "anyOf": [ + { + "$ref": "#/$defs/FiberIdNoneEncoded" + }, + { + "$ref": "#/$defs/FiberIdRuntimeEncoded" + }, + { + "$ref": "#/$defs/FiberIdCompositeEncoded" + } + ] + }, + "FiberIdNoneEncoded": { + "type": "object", + "required": [ + "_tag" + ], + "properties": { + "_tag": { + "type": "string", + "enum": [ + "None" + ] + } + }, + "additionalProperties": false + }, + "FiberIdRuntimeEncoded": { + "type": "object", + "required": [ + "_tag", + "id", + "startTimeMillis" + ], + "properties": { + "_tag": { + "type": "string", + "enum": [ + "Runtime" + ] + }, + "id": { + "$ref": "#/$defs/Int" + }, + "startTimeMillis": { + "$ref": "#/$defs/Int" + } + }, + "additionalProperties": false + }, + "Int": { + "type": "integer", + "description": "an integer", + "title": "int" + }, + "FiberIdCompositeEncoded": { + "type": "object", + "required": [ + "_tag", + "left", + "right" + ], + "properties": { + "_tag": { + "type": "string", + "enum": [ + "Composite" + ] + }, + "left": { + "$ref": "#/$defs/FiberIdEncoded" + }, + "right": { + "$ref": "#/$defs/FiberIdEncoded" + } + }, + "additionalProperties": false + } + }, + "anyOf": [ + { + "type": "object", + "required": [ + "_tag", + "cause" + ], + "properties": { + "_tag": { + "type": "string", + "enum": [ + "Failure" + ] + }, + "cause": { + "$ref": "#/$defs/CauseEncoded0" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "_tag", + "value" + ], + "properties": { + "_tag": { + "type": "string", + "enum": [ + "Success" + ] + }, + "value": { + "type": "number" + } + }, + "additionalProperties": false + } + ], + "title": "ExitEncoded" + } + */ + ``` + +- [#4775](https://github.com/Effect-TS/effect/pull/4775) [`570e45f`](https://github.com/Effect-TS/effect/commit/570e45f8cb936e42ec48f67f21bb2b7252f36c0c) Thanks @gcanti! - JSONSchema: preserve original key name when using `fromKey` followed by `annotations`, closes #4774. + + Before: + + ```ts + import { JSONSchema, Schema } from "effect" + + const schema = Schema.Struct({ + a: Schema.propertySignature(Schema.String) + .pipe(Schema.fromKey("b")) + .annotations({}) + }) + + console.log(JSON.stringify(JSONSchema.make(schema), null, 2)) + /* + { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "required": [ + "a" + ], + "properties": { + "a": { + "type": "string" + } + }, + "additionalProperties": false + } + */ + ``` + + After: + + ```ts + import { JSONSchema, Schema } from "effect" + + const schema = Schema.Struct({ + a: Schema.propertySignature(Schema.String) + .pipe(Schema.fromKey("b")) + .annotations({}) + }) + + console.log(JSON.stringify(JSONSchema.make(schema), null, 2)) + /* + { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "required": [ + "b" + ], + "properties": { + "b": { + "type": "string" + } + }, + "additionalProperties": false + } + */ + ``` + +## 3.14.12 + +### Patch Changes + +- [#4770](https://github.com/Effect-TS/effect/pull/4770) [`c2ad9ee`](https://github.com/Effect-TS/effect/commit/c2ad9ee9f3c4c743390edf35ed9e85a20be33811) Thanks @gcanti! - Fixes a bug where non existing properties were allowed in the `make` constructor of a `Schema.Class`, closes #4767. + + **Example** + + ```ts + import { Schema } from "effect" + + class A extends Schema.Class
("A")({ + a: Schema.String + }) {} + + A.make({ + a: "a", + // @ts-expect-error: Object literal may only specify known properties, and 'b' does not exist in type '{ readonly a: string; }'.ts(2353) + b: "b" + }) + ``` + +- [#4735](https://github.com/Effect-TS/effect/pull/4735) [`9c68654`](https://github.com/Effect-TS/effect/commit/9c686542b6eb3ea188cb70673ef2e41223633e89) Thanks @suddenlyGiovanni! - Improve `Number` module with comprehensive TsDocs and type-level tests + +## 3.14.11 + +### Patch Changes + +- [#4756](https://github.com/Effect-TS/effect/pull/4756) [`e536127`](https://github.com/Effect-TS/effect/commit/e536127c1e6f2fb3a542c73ae919435a629a346b) Thanks @tim-smart! - allow Pool to acquire multiple items at once + +## 3.14.10 + +### Patch Changes + +- [#4748](https://github.com/Effect-TS/effect/pull/4748) [`bc7efa3`](https://github.com/Effect-TS/effect/commit/bc7efa3b031bb25e1ed3c8f2d3fb5e8da166cadc) Thanks @tim-smart! - preserve refinement types in Match.when + +## 3.14.9 + +### Patch Changes + +- [#4734](https://github.com/Effect-TS/effect/pull/4734) [`d78249f`](https://github.com/Effect-TS/effect/commit/d78249f0b67f63cf4baf806ff090cba33293daf0) Thanks @thewilkybarkid! - Allow Match.typeTags to specify a return type + +## 3.14.8 + +### Patch Changes + +- [#4708](https://github.com/Effect-TS/effect/pull/4708) [`b3a2d32`](https://github.com/Effect-TS/effect/commit/b3a2d32772e6f7f20eacf2e18128e99324c4d378) Thanks @thewilkybarkid! - Make Match.valueTags dual + +## 3.14.7 + +### Patch Changes + +- [#4706](https://github.com/Effect-TS/effect/pull/4706) [`b542a4b`](https://github.com/Effect-TS/effect/commit/b542a4bf195be0c9af1523e1ba96c953decc4d25) Thanks @IGassmann! - Fix summary metric’s quantile values + +## 3.14.6 + +### Patch Changes + +- [#4674](https://github.com/Effect-TS/effect/pull/4674) [`47618c1`](https://github.com/Effect-TS/effect/commit/47618c1ad84ebcc5a51133a3fff5aa5012d49d45) Thanks @suddenlyGiovanni! - Improved TsDoc documentation for `MutableHashSet` module. + +- [#4699](https://github.com/Effect-TS/effect/pull/4699) [`6077882`](https://github.com/Effect-TS/effect/commit/60778824a4794336c33807801f813f8751d1c7e4) Thanks @gcanti! - Fix JSONSchema generation for record values that include `undefined`, closes #4697. + + Before + + ```ts + import { JSONSchema, Schema } from "effect" + + const schema = Schema.partial( + Schema.Struct( + { foo: Schema.Number }, + { + key: Schema.String, + value: Schema.Number + } + ) + ) + + console.log(JSON.stringify(JSONSchema.make(schema), null, 2)) + // throws + ``` + + After + + ```ts + import { JSONSchema, Schema } from "effect" + + const schema = Schema.partial( + Schema.Struct( + { foo: Schema.Number }, + { + key: Schema.String, + value: Schema.Number + } + ) + ) + + console.log(JSON.stringify(JSONSchema.make(schema), null, 2)) + /* + Output: + { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "required": [], + "properties": { + "foo": { + "type": "number" + } + }, + "additionalProperties": { + "type": "number" + } + } + */ + ``` + +## 3.14.5 + +### Patch Changes + +- [#4676](https://github.com/Effect-TS/effect/pull/4676) [`40dbfef`](https://github.com/Effect-TS/effect/commit/40dbfeff239b6e567706752114f31b2fce7de4e3) Thanks @tim-smart! - allow Effect.fnUntraced to return non-effects + +- [#4682](https://github.com/Effect-TS/effect/pull/4682) [`5a5ebdd`](https://github.com/Effect-TS/effect/commit/5a5ebdddfaddd259538b4599a6676281faca778e) Thanks @thewilkybarkid! - ensure Equal considers URL by value + +## 3.14.4 + +### Patch Changes + +- [#4667](https://github.com/Effect-TS/effect/pull/4667) [`e4ba2c6`](https://github.com/Effect-TS/effect/commit/e4ba2c66a878e81b5e295d6d49aaf724b80a28ef) Thanks @suddenlyGiovanni! - Fix: `HashSet.md` api docs; previously broken by issue with Docgen JsDoc parser. + +## 3.14.3 + +### Patch Changes + +- [#4664](https://github.com/Effect-TS/effect/pull/4664) [`37aa8e1`](https://github.com/Effect-TS/effect/commit/37aa8e137725a902e70cd1e468ea98b873aa5056) Thanks @suddenlyGiovanni! - Improved TsDoc documentation for `HashSet` module. + +- [#4670](https://github.com/Effect-TS/effect/pull/4670) [`34f03d6`](https://github.com/Effect-TS/effect/commit/34f03d66875f21f266f102223a03cd14c2ed6ea6) Thanks @tim-smart! - fix Data.TaggedEnum with generics regression + +## 3.14.2 + +### Patch Changes + +- [#4646](https://github.com/Effect-TS/effect/pull/4646) [`f87991b`](https://github.com/Effect-TS/effect/commit/f87991b6d8a2edfaf90b01cebda4b466992ae865) Thanks @gcanti! - SchemaAST: add missing `getSchemaIdAnnotation` API + +- [#4646](https://github.com/Effect-TS/effect/pull/4646) [`f87991b`](https://github.com/Effect-TS/effect/commit/f87991b6d8a2edfaf90b01cebda4b466992ae865) Thanks @gcanti! - Arbitrary: fix bug where annotations were ignored. + + Before + + ```ts + import { Arbitrary, Schema } from "effect" + + const schema = Schema.Int.annotations({ + arbitrary: (_, ctx) => (fc) => { + console.log("context: ", ctx) + return fc.integer() + } + }).pipe(Schema.greaterThan(0), Schema.lessThan(10)) + + Arbitrary.make(schema) + // No output ❌ + ``` + + After + + ```ts + import { Arbitrary, Schema } from "effect" + + const schema = Schema.Int.annotations({ + arbitrary: (_, ctx) => (fc) => { + console.log("context: ", ctx) + return fc.integer() + } + }).pipe(Schema.greaterThan(0), Schema.lessThan(10)) + + Arbitrary.make(schema) + /* + context: { + maxDepth: 2, + constraints: { + _tag: 'NumberConstraints', + constraints: { min: 0, minExcluded: true, max: 10, maxExcluded: true }, + isInteger: true + } + } + */ + ``` + +- [#4648](https://github.com/Effect-TS/effect/pull/4648) [`0a3e3e1`](https://github.com/Effect-TS/effect/commit/0a3e3e18eea5e0d1882f1a6c906198e6ef226a41) Thanks @gcanti! - Schema: `standardSchemaV1` now includes the schema, closes #4494. + + This update fixes an issue where passing `Schema.standardSchemaV1(...)` directly to `JSONSchema.make` would throw a `TypeError`. The schema was missing from the returned object, causing the JSON schema generation to fail. + + Now `standardSchemaV1` includes the schema itself, so it can be used with `JSONSchema.make` without issues. + + **Example** + + ```ts + import { JSONSchema, Schema } from "effect" + + const Person = Schema.Struct({ + name: Schema.optionalWith(Schema.NonEmptyString, { exact: true }) + }) + + const standardSchema = Schema.standardSchemaV1(Person) + + console.log(JSONSchema.make(standardSchema)) + /* + { + '$schema': 'http://json-schema.org/draft-07/schema#', + '$defs': { + NonEmptyString: { + type: 'string', + description: 'a non empty string', + title: 'nonEmptyString', + minLength: 1 + } + }, + type: 'object', + required: [], + properties: { name: { '$ref': '#/$defs/NonEmptyString' } }, + additionalProperties: false + } + */ + ``` + +## 3.14.1 + +### Patch Changes + +- [#4620](https://github.com/Effect-TS/effect/pull/4620) [`4a274fe`](https://github.com/Effect-TS/effect/commit/4a274fe9f623182b6b902827e0e83bd89ca3b05c) Thanks @tim-smart! - remove Context.ValidTagsById usage + +## 3.14.0 + +### Minor Changes + +- [#4469](https://github.com/Effect-TS/effect/pull/4469) [`1f47e4e`](https://github.com/Effect-TS/effect/commit/1f47e4e12546ab691b29bfb7b5128bb17b93baa5) Thanks @vinassefranche! - Add DateTime.nowAsDate creator + +- [#4469](https://github.com/Effect-TS/effect/pull/4469) [`26dd75f`](https://github.com/Effect-TS/effect/commit/26dd75f276a0d8a63eab313bd5a167d5072c9780) Thanks @tim-smart! - expose the Layer.MemoMap via Layer.CurrentMemoMap to the layers being built + +- [#4469](https://github.com/Effect-TS/effect/pull/4469) [`04dff2d`](https://github.com/Effect-TS/effect/commit/04dff2d01ac68c260f29a6d4743381825c353c86) Thanks @tim-smart! - add Tracer Span.addLinks, for dynamically linking spans + +- [#4469](https://github.com/Effect-TS/effect/pull/4469) [`c7fac0c`](https://github.com/Effect-TS/effect/commit/c7fac0cd7eadcd5cc0c3a987051c5b57ad271638) Thanks @LaureRC! - Add HashMap.every + +- [#4469](https://github.com/Effect-TS/effect/pull/4469) [`ffaa3f3`](https://github.com/Effect-TS/effect/commit/ffaa3f3969df26610fcc02ad537340641d44e803) Thanks @vinassefranche! - Add Either.transposeOption + +- [#4469](https://github.com/Effect-TS/effect/pull/4469) [`ab957c1`](https://github.com/Effect-TS/effect/commit/ab957c1fee714868f56c7ab4e802b9d449e9b666) Thanks @vinassefranche! - Make TestClock.setTime accept a DateTime.Input + +- [#4469](https://github.com/Effect-TS/effect/pull/4469) [`35db9ce`](https://github.com/Effect-TS/effect/commit/35db9ce228f1416c8abacc6dc9c36fbd0f33ef0f) Thanks @LaureRC! - Add Effect.transposeMapOption + +- [#4469](https://github.com/Effect-TS/effect/pull/4469) [`cf77ea9`](https://github.com/Effect-TS/effect/commit/cf77ea9ab4fc89e66a43f682a9926ccdee6c57ed) Thanks @f15u! - Add `Array.window` function + +- [#4469](https://github.com/Effect-TS/effect/pull/4469) [`26dd75f`](https://github.com/Effect-TS/effect/commit/26dd75f276a0d8a63eab313bd5a167d5072c9780) Thanks @tim-smart! - add LayerMap module + + A `LayerMap` allows you to create a map of Layer's that can be used to + dynamically access resources based on a key. + + Here is an example of how you can use a `LayerMap` to create a service that + provides access to multiple OpenAI completions services. + + ```ts + import { Completions } from "@effect/ai" + import { OpenAiClient, OpenAiCompletions } from "@effect/ai-openai" + import { FetchHttpClient } from "@effect/platform" + import { NodeRuntime } from "@effect/platform-node" + import { Config, Effect, Layer, LayerMap } from "effect" + + // create the openai client layer + const OpenAiLayer = OpenAiClient.layerConfig({ + apiKey: Config.redacted("OPENAI_API_KEY") + }).pipe(Layer.provide(FetchHttpClient.layer)) + + // create a service that wraps a LayerMap + class AiClients extends LayerMap.Service()("AiClients", { + // this LayerMap will provide the ai Completions service + provides: Completions.Completions, + + // define the lookup function for the layer map + // + // The returned Layer will be used to provide the Completions service for the + // given model. + lookup: (model: OpenAiCompletions.Model) => + OpenAiCompletions.layer({ model }), + + // If a layer is not used for a certain amount of time, it can be removed + idleTimeToLive: "5 seconds", + + // Supply the dependencies for the layers in the LayerMap + dependencies: [OpenAiLayer] + }) {} + + // usage + Effect.gen(function* () { + // access and use the generic Completions service + const ai = yield* Completions.Completions + const response = yield* ai.create("Hello, world!") + console.log(response.text) + }).pipe( + // use the AiClients service to provide a variant of the Completions service + AiClients.provide("gpt-4o"), + // provide the LayerMap service + Effect.provide(AiClients.Default), + NodeRuntime.runMain + ) + ``` + +- [#4469](https://github.com/Effect-TS/effect/pull/4469) [`baaab60`](https://github.com/Effect-TS/effect/commit/baaab60b737f35dfab8e4a21bce28a195d19e899) Thanks @vinassefranche! - Make Runtime.run\* apis dual + +### Patch Changes + +- [#4469](https://github.com/Effect-TS/effect/pull/4469) [`aba2d1d`](https://github.com/Effect-TS/effect/commit/aba2d1d831ea149481bd4dd755528c0afa8239ce) Thanks @tim-smart! - preserve interruptors in channel executor .runIn + +## 3.13.12 + +### Patch Changes + +- [#4610](https://github.com/Effect-TS/effect/pull/4610) [`0c4803f`](https://github.com/Effect-TS/effect/commit/0c4803fcc69262d11a97ce49d0e9b4288df0651f) Thanks @gcanti! - Preserve specific annotations (e.g., `arbitrary`) when using `Schema.typeSchema`, closes #4609. + + Previously, annotations such as `arbitrary` were lost when calling `Schema.typeSchema` on a transformation. This update ensures that certain annotations, which depend only on the "to" side of the transformation, are preserved. + + Annotations that are now retained: + - `examples` + - `default` + - `jsonSchema` + - `arbitrary` + - `pretty` + - `equivalence` + + **Example** + + Before + + ```ts + import { Arbitrary, FastCheck, Schema } from "effect" + + const schema = Schema.NumberFromString.annotations({ + arbitrary: () => (fc) => fc.constant(1) + }) + + const to = Schema.typeSchema(schema) // ❌ Annotation is lost + + console.log(FastCheck.sample(Arbitrary.make(to), 5)) + /* + [ + 2.5223372357846707e-44, + -2.145443957806771e+25, + -3.4028179901346956e+38, + 5.278086259208735e+29, + 1.8216880036222622e-44 + ] + */ + ``` + + After + + ```ts + import { Arbitrary, FastCheck, Schema } from "effect" + + const schema = Schema.NumberFromString.annotations({ + arbitrary: () => (fc) => fc.constant(1) + }) + + const to = Schema.typeSchema(schema) // ✅ Annotation is now preserved + + console.log(FastCheck.sample(Arbitrary.make(to), 5)) + /* + [ 1, 1, 1, 1, 1 ] + */ + ``` + +- [#4607](https://github.com/Effect-TS/effect/pull/4607) [`6f65ac4`](https://github.com/Effect-TS/effect/commit/6f65ac4eac1489cd6ea390e18b0908670722adad) Thanks @gcanti! - Add support for `jsonSchema` annotations on `SymbolFromSelf` index signatures. + + **Before** + + ```ts + import { JSONSchema, Schema } from "effect" + + const schema = Schema.Record({ + key: Schema.SymbolFromSelf.annotations({ jsonSchema: { type: "string" } }), + value: Schema.Number + }) + + JSONSchema.make(schema) + /* + throws: + Error: Unsupported index signature parameter + schema (SymbolKeyword): symbol + */ + ``` + + **After** + + ```ts + import { JSONSchema, Schema } from "effect" + + const schema = Schema.Record({ + key: Schema.SymbolFromSelf.annotations({ jsonSchema: { type: "string" } }), + value: Schema.Number + }) + + console.log(JSON.stringify(JSONSchema.make(schema), null, 2)) + /* + Output: + { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "required": [], + "properties": {}, + "additionalProperties": { + "type": "number" + }, + "propertyNames": { + "type": "string" + } + } + */ + ``` + +## 3.13.11 + +### Patch Changes + +- [#4601](https://github.com/Effect-TS/effect/pull/4601) [`fad8cca`](https://github.com/Effect-TS/effect/commit/fad8cca9bbfcc2eaeb44b97c15dbe0a1eda75315) Thanks @gcanti! - Schema: enhance the internal `formatUnknown` function to handle various types including iterables, classes, and additional edge cases. + + Before + + ```ts + import { Schema } from "effect" + + const schema = Schema.Array(Schema.Number) + + Schema.decodeUnknownSync(schema)(new Set([1, 2])) + // throws Expected ReadonlyArray, actual {} + + class A { + constructor(readonly a: number) {} + } + + Schema.decodeUnknownSync(schema)(new A(1)) + // throws Expected ReadonlyArray, actual {"a":1} + ``` + + After + + ```ts + import { Schema } from "effect" + + const schema = Schema.Array(Schema.Number) + + Schema.decodeUnknownSync(schema)(new Set([1, 2])) + // throws Expected ReadonlyArray, actual Set([1,2]) + + class A { + constructor(readonly a: number) {} + } + + Schema.decodeUnknownSync(schema)(new A(1)) + // throws Expected ReadonlyArray, actual A({"a":1}) + ``` + +- [#4606](https://github.com/Effect-TS/effect/pull/4606) [`4296293`](https://github.com/Effect-TS/effect/commit/4296293049414d0cf2d915a26c552b09f946b9a0) Thanks @gcanti! - Fix issue with generic filters when generating arbitraries, closes #4605. + + Previously, applying a `filter` to a schema when generating arbitraries could cause a `TypeError` due to missing properties. This fix ensures that arbitraries are generated correctly when filters are used. + + **Before** + + ```ts + import { Arbitrary, Schema } from "effect" + + const schema = Schema.BigIntFromSelf.pipe(Schema.filter(() => true)) + + Arbitrary.make(schema) + // TypeError: Cannot read properties of undefined (reading 'min') + ``` + + **After** + + ```ts + import { Arbitrary, Schema } from "effect" + + const schema = Schema.BigIntFromSelf.pipe(Schema.filter(() => true)) + + const result = Arbitrary.make(schema) // Works correctly + ``` + +- [#4587](https://github.com/Effect-TS/effect/pull/4587) [`9c241ab`](https://github.com/Effect-TS/effect/commit/9c241abe47ccf7a5257b98a4a64a63054a12741d) Thanks @gcanti! - Schema: simplify `Struct` and `Record` return types. + +- [#4591](https://github.com/Effect-TS/effect/pull/4591) [`082b0c1`](https://github.com/Effect-TS/effect/commit/082b0c1b9f4252bcdd69608f2e4a9226f953ac3f) Thanks @IMax153! - Improve clarity of the `TimeoutException` error message + +- [#4604](https://github.com/Effect-TS/effect/pull/4604) [`be12983`](https://github.com/Effect-TS/effect/commit/be12983bc7e7537b41cd8910fc4eb7d1da56ab07) Thanks @gcanti! - Add support for refinements to `Schema.omit`, closes #4603. + + Before + + ```ts + import { Schema } from "effect" + + const schema = Schema.Struct({ + a: Schema.String, + b: Schema.String + }) + + const omitted = schema.pipe( + Schema.filter(() => true), + Schema.omit("a") + ) + + console.log(String(omitted.ast)) + // {} ❌ + ``` + + After + + ```ts + import { Schema } from "effect" + + const schema = Schema.Struct({ + a: Schema.String, + b: Schema.String + }) + + const omitted = schema.pipe( + Schema.filter(() => true), + Schema.omit("a") + ) + + console.log(String(omitted.ast)) + // { readonly b: string } + ``` + +- [#4593](https://github.com/Effect-TS/effect/pull/4593) [`de88127`](https://github.com/Effect-TS/effect/commit/de88127a5a5906ccece98af74787b5ae0e65e431) Thanks @gcanti! - Schema: export `Field` type. + + Useful for creating a type that can be used to add custom constraints to the fields of a struct. + + ```ts + import { Schema } from "effect" + + const f = >( + schema: Schema.Struct + ) => { + return schema.omit("a") + } + + // ┌─── Schema.Struct<{ b: typeof Schema.Number; }> + // ▼ + const result = f(Schema.Struct({ a: Schema.String, b: Schema.Number })) + ``` + +## 3.13.10 + +### Patch Changes + +- [#4578](https://github.com/Effect-TS/effect/pull/4578) [`527c964`](https://github.com/Effect-TS/effect/commit/527c9645229f5be9714a7e60a38a9e753c4bbfb1) Thanks @gcanti! - Allow `toString` Method to Be Overridden in Schema Classes, closes #4577. + + Previously, attempting to override the `toString` method in schema classes caused a `TypeError` in the browser because the property was set as **read-only** (`writable: false`). This fix makes `toString` **writable**, allowing developers to override it when needed. + +## 3.13.9 + +### Patch Changes + +- [#4579](https://github.com/Effect-TS/effect/pull/4579) [`2976e52`](https://github.com/Effect-TS/effect/commit/2976e52538d9dc9ffdcbc84d4ac748cff9305971) Thanks @giuliobracci! - Fix `Match.tags` throwing exception on `undefined` input value + +## 3.13.8 + +### Patch Changes + +- [#4567](https://github.com/Effect-TS/effect/pull/4567) [`c65d336`](https://github.com/Effect-TS/effect/commit/c65d3362d07ec815ff3b46278314e8a31706ddc2) Thanks @rehos! - Schema: `standardSchemaV1` now returns all errors by default and supports custom options. + + The `standardSchemaV1` now returns **all validation errors** by default (`ParseOptions = { errors: "all" }`). Additionally, it now accepts an optional `overrideOptions` parameter, allowing you to customize the default parsing behavior as needed. + +- [#4565](https://github.com/Effect-TS/effect/pull/4565) [`22d2ebb`](https://github.com/Effect-TS/effect/commit/22d2ebb4b11f5a44351a4736e65da391a3b647d0) Thanks @gcanti! - ParseResult.ArrayFormatter: correct `_tag` fields for `Refinement` and `Transformation` issues, closes #4564. + + This update fixes an issue where `ParseResult.ArrayFormatter` incorrectly labeled **Refinement** and **Transformation** errors as `Type` in the output. + + **Before** + + ```ts + import { Effect, ParseResult, Schema } from "effect" + + const schema = Schema.Struct({ + a: Schema.NonEmptyString, + b: Schema.NumberFromString + }) + + const input = { a: "", b: "" } + + const program = Schema.decodeUnknown(schema, { errors: "all" })(input).pipe( + Effect.catchTag("ParseError", (err) => + ParseResult.ArrayFormatter.formatError(err).pipe( + Effect.map((err) => JSON.stringify(err, null, 2)) + ) + ) + ) + + program.pipe(Effect.runPromise).then(console.log) + /* + [ + { + "_tag": "Type", ❌ + "path": [ + "a" + ], + "message": "Expected a non empty string, actual \"\"" + }, + { + "_tag": "Type", ❌ + "path": [ + "b" + ], + "message": "Unable to decode \"\" into a number" + } + ] + */ + ``` + + **After** + + ```ts + import { Effect, ParseResult, Schema } from "effect" + + const schema = Schema.Struct({ + a: Schema.NonEmptyString, + b: Schema.NumberFromString + }) + + const input = { a: "", b: "" } + + const program = Schema.decodeUnknown(schema, { errors: "all" })(input).pipe( + Effect.catchTag("ParseError", (err) => + ParseResult.ArrayFormatter.formatError(err).pipe( + Effect.map((err) => JSON.stringify(err, null, 2)) + ) + ) + ) + + program.pipe(Effect.runPromise).then(console.log) + /* + [ + { + "_tag": "Refinement", ✅ + "path": [ + "a" + ], + "message": "Expected a non empty string, actual \"\"" + }, + { + "_tag": "Transformation", ✅ + "path": [ + "b" + ], + "message": "Unable to decode \"\" into a number" + } + ] + */ + ``` + +## 3.13.7 + +### Patch Changes + +- [#4540](https://github.com/Effect-TS/effect/pull/4540) [`840cc73`](https://github.com/Effect-TS/effect/commit/840cc7329908db7ca693ef47b07d4f845c29cadd) Thanks @gcanti! - Add `additionalPropertiesStrategy` option to `OpenApi.fromApi`, closes #4531. + + This update introduces the `additionalPropertiesStrategy` option in `OpenApi.fromApi`, allowing control over how additional properties are handled in the generated OpenAPI schema. + - When `"strict"` (default), additional properties are disallowed (`"additionalProperties": false`). + - When `"allow"`, additional properties are allowed (`"additionalProperties": true`), making APIs more flexible. + + The `additionalPropertiesStrategy` option has also been added to: + - `JSONSchema.fromAST` + - `OpenApiJsonSchema.makeWithDefs` + + **Example** + + ```ts + import { + HttpApi, + HttpApiEndpoint, + HttpApiGroup, + OpenApi + } from "@effect/platform" + import { Schema } from "effect" + + const api = HttpApi.make("api").add( + HttpApiGroup.make("group").add( + HttpApiEndpoint.get("get", "/").addSuccess( + Schema.Struct({ a: Schema.String }) + ) + ) + ) + + const schema = OpenApi.fromApi(api, { + additionalPropertiesStrategy: "allow" + }) + + console.log(JSON.stringify(schema, null, 2)) + /* + { + "openapi": "3.1.0", + "info": { + "title": "Api", + "version": "0.0.1" + }, + "paths": { + "/": { + "get": { + "tags": [ + "group" + ], + "operationId": "group.get", + "parameters": [], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "a" + ], + "properties": { + "a": { + "type": "string" + } + }, + "additionalProperties": true + } + } + } + }, + "400": { + "description": "The request did not match the expected schema", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HttpApiDecodeError" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "HttpApiDecodeError": { + "type": "object", + "required": [ + "issues", + "message", + "_tag" + ], + "properties": { + "issues": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Issue" + } + }, + "message": { + "type": "string" + }, + "_tag": { + "type": "string", + "enum": [ + "HttpApiDecodeError" + ] + } + }, + "additionalProperties": true, + "description": "The request did not match the expected schema" + }, + "Issue": { + "type": "object", + "required": [ + "_tag", + "path", + "message" + ], + "properties": { + "_tag": { + "type": "string", + "enum": [ + "Pointer", + "Unexpected", + "Missing", + "Composite", + "Refinement", + "Transformation", + "Type", + "Forbidden" + ], + "description": "The tag identifying the type of parse issue" + }, + "path": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PropertyKey" + }, + "description": "The path to the property where the issue occurred" + }, + "message": { + "type": "string", + "description": "A descriptive message explaining the issue" + } + }, + "additionalProperties": true, + "description": "Represents an error encountered while parsing a value to match the schema" + }, + "PropertyKey": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "object", + "required": [ + "_tag", + "key" + ], + "properties": { + "_tag": { + "type": "string", + "enum": [ + "symbol" + ] + }, + "key": { + "type": "string" + } + }, + "additionalProperties": true, + "description": "an object to be decoded into a globally shared symbol" + } + ] + } + }, + "securitySchemes": {} + }, + "security": [], + "tags": [ + { + "name": "group" + } + ] + } + */ + ``` + +- [#4541](https://github.com/Effect-TS/effect/pull/4541) [`9bf8a74`](https://github.com/Effect-TS/effect/commit/9bf8a74b967f18d931743dd5196af326c9118e9c) Thanks @fubhy! - Disallowed excess properties for various function options + +- [#4554](https://github.com/Effect-TS/effect/pull/4554) [`87ba23c`](https://github.com/Effect-TS/effect/commit/87ba23c41c193503ed0c612b0d32d0b253794c64) Thanks @gcanti! - ConfigProvider: `fromEnv`: add missing `Partial` modifier. + +## 3.13.6 + +### Patch Changes + +- [#4551](https://github.com/Effect-TS/effect/pull/4551) [`3154ce4`](https://github.com/Effect-TS/effect/commit/3154ce4692fa18b804982158d3c4c8a8a5fae386) Thanks @gcanti! - Arbitrary: `make` called on `Schema.Class` now respects property annotations, closes #4550. + + Previously, when calling `Arbitrary.make` on a `Schema.Class`, property-specific annotations (such as `arbitrary`) were ignored, leading to unexpected values in generated instances. + + Before + + Even though `a` had an `arbitrary` annotation, the generated values were random: + + ```ts + import { Arbitrary, FastCheck, Schema } from "effect" + + class Class extends Schema.Class("Class")({ + a: Schema.NumberFromString.annotations({ + arbitrary: () => (fc) => fc.constant(1) + }) + }) {} + + console.log(FastCheck.sample(Arbitrary.make(Class), 5)) + /* + Example Output: + [ + Class { a: 2.6624670822171524e-44 }, + Class { a: 3.4028177873105996e+38 }, + Class { a: 3.402820626847944e+38 }, + Class { a: 3.783505853677006e-44 }, + Class { a: 3243685 } + ] + */ + ``` + + After + + Now, the values respect the `arbitrary` annotation and return the expected constant: + + ```ts + import { Arbitrary, FastCheck, Schema } from "effect" + + class Class extends Schema.Class("Class")({ + a: Schema.NumberFromString.annotations({ + arbitrary: () => (fc) => fc.constant(1) + }) + }) {} + + console.log(FastCheck.sample(Arbitrary.make(Class), 5)) + /* + [ + Class { a: 1 }, + Class { a: 1 }, + Class { a: 1 }, + Class { a: 1 }, + Class { a: 1 } + ] + */ + ``` + +## 3.13.5 + +### Patch Changes + +- [#4530](https://github.com/Effect-TS/effect/pull/4530) [`367bb35`](https://github.com/Effect-TS/effect/commit/367bb35f4c2a254e1fb211d96db2474a7aed9020) Thanks @tim-smart! - Match.tag + Match.withReturnType can use literals without as const + +- [#4543](https://github.com/Effect-TS/effect/pull/4543) [`6cf11c3`](https://github.com/Effect-TS/effect/commit/6cf11c3a75773ceec2877c85ddc760f381f0866d) Thanks @gcanti! - Preserve branded primitive types in `DeepMutable` transformation, closes #4542. + + Previously, applying `DeepMutable` to branded primitive types (e.g., `string & Brand.Brand<"mybrand">`) caused unexpected behavior, where `String` prototype methods were incorrectly inherited. + + This fix ensures that branded types remain unchanged during transformation, preventing type inconsistencies. + + **Example** + + Before + + ```ts + import type { Brand, Types } from "effect" + + type T = string & Brand.Brand<"mybrand"> + + /* + type Result = { + [x: number]: string; + toString: () => string; + charAt: (pos: number) => string; + charCodeAt: (index: number) => number; + concat: (...strings: string[]) => string; + indexOf: (searchString: string, position?: number) => number; + ... 47 more ...; + [BrandTypeId]: { + ...; + }; + } + */ + type Result = Types.DeepMutable + ``` + + After + + ```ts + import type { Brand, Types } from "effect" + + type T = string & Brand.Brand<"mybrand"> + + // type Result = string & Brand.Brand<"mybrand"> + type Result = Types.DeepMutable + ``` + +- [#4546](https://github.com/Effect-TS/effect/pull/4546) [`a0acec8`](https://github.com/Effect-TS/effect/commit/a0acec851f72e19466363d24b9cc218acd00006a) Thanks @gcanti! - Schema.extend: add support for Transformation + Struct, closes #4536. + + **Example** + + Before + + ```ts + import { Schema } from "effect" + + const A = Schema.Struct({ + a: Schema.String + }) + + const B = Schema.Struct({ + b: Schema.String + }) + + const C = Schema.Struct({ + c: Schema.String + }) + + const AB = Schema.transform(A, B, { + strict: true, + decode: (a) => ({ b: a.a }), + encode: (b) => ({ a: b.b }) + }) + + // Transformation + Struct + const schema = Schema.extend(AB, C) + /* + throws: + Error: Unsupported schema or overlapping types + details: cannot extend ({ readonly a: string } <-> { readonly b: string }) with { readonly c: string } + */ + ``` + + After + + ```ts + import { Schema } from "effect" + + const A = Schema.Struct({ + a: Schema.String + }) + + const B = Schema.Struct({ + b: Schema.String + }) + + const C = Schema.Struct({ + c: Schema.String + }) + + const AB = Schema.transform(A, B, { + strict: true, + decode: (a) => ({ b: a.a }), + encode: (b) => ({ a: b.b }) + }) + + // Transformation + Struct + const schema = Schema.extend(AB, C) + + console.log(Schema.decodeUnknownSync(schema)({ a: "a", c: "c" })) + // Output: { b: 'a', c: 'c' } + + console.log(Schema.encodeSync(schema)({ b: "b", c: "c" })) + // Output: { a: 'b', c: 'c' } + ``` + +## 3.13.4 + +### Patch Changes + +- [#4533](https://github.com/Effect-TS/effect/pull/4533) [`17d9e89`](https://github.com/Effect-TS/effect/commit/17d9e89f9851663bdbb6c1e685601d97806114a4) Thanks @gcanti! - Schema: Export `MakeOptions` type, closes #4532. + +## 3.13.3 + +### Patch Changes + +- [#4502](https://github.com/Effect-TS/effect/pull/4502) [`cc5588d`](https://github.com/Effect-TS/effect/commit/cc5588df07f9103513547cb429ce041b9436a8bd) Thanks @gcanti! - Schema: More Accurate Return Types for `DataFromSelf` and `Data`. + + This update refines the return types of `DataFromSelf` and `Data`, making them clearer and more specific, especially when working with structured schemas. + + **Before** + + The return types were more generic, making it harder to see the underlying structure: + + ```ts + import { Schema } from "effect" + + const struct = Schema.Struct({ a: Schema.NumberFromString }) + + // ┌─── Schema.DataFromSelf> + // ▼ + const schema1 = Schema.DataFromSelf(struct) + + // ┌─── Schema.Data> + // ▼ + const schema2 = Schema.Data(struct) + ``` + + **After** + + Now, the return types clearly reflect the original schema structure: + + ```ts + import { Schema } from "effect" + + const struct = Schema.Struct({ a: Schema.NumberFromString }) + + // ┌─── Schema.DataFromSelf> + // ▼ + const schema1 = Schema.DataFromSelf(struct) + + // ┌─── Schema.Data> + // ▼ + const schema2 = Schema.Data(struct) + ``` + +- [#4510](https://github.com/Effect-TS/effect/pull/4510) [`623c8cd`](https://github.com/Effect-TS/effect/commit/623c8cd053ed6ee3d353aaa8778d484670fca2bb) Thanks @gcanti! - Schema: More Accurate Return Type for `compose`. + + **Before** + + ```ts + import { Schema } from "effect" + + // ┌─── SchemaClass + // ▼ + const schema = Schema.compose( + Schema.NumberFromString, + Schema.NullOr(Schema.Number) + ) + + // @ts-expect-error: Property 'from' does not exist + schema.from + + // @ts-expect-error: Property 'to' does not exist + schema.to + ``` + + **After** + + ```ts + import { Schema } from "effect" + + // ┌─── transform> + // ▼ + const schema = Schema.compose( + Schema.NumberFromString, + Schema.NullOr(Schema.Number) + ) + + // ┌─── typeof Schema.NumberFromString + // ▼ + schema.from + + // ┌─── Schema.NullOr + // ▼ + schema.to + ``` + +- [#4488](https://github.com/Effect-TS/effect/pull/4488) [`00b4eb1`](https://github.com/Effect-TS/effect/commit/00b4eb1ece12a16e222e6220965bb4024d6752ac) Thanks @gcanti! - Schema: more precise return types when filters are involved. + + **Example** (with `Schema.maxLength`) + + Before + + ```ts + import { Schema } from "effect" + + // ┌─── Schema.filter> + // ▼ + const schema = Schema.String.pipe(Schema.maxLength(10)) + + // Schema + schema.from + ``` + + After + + ```ts + import { Schema } from "effect" + + // ┌─── Schema.filter + // ▼ + const schema = Schema.String.pipe(Schema.maxLength(10)) + + // typeof Schema.String + schema.from + ``` + + String filters: + - `maxLength` + - `minLength` + - `length` + - `pattern` + - `startsWith` + - `endsWith` + - `includes` + - `lowercased` + - `capitalized` + - `uncapitalized` + - `uppercased` + - `nonEmptyString` + - `trimmed` + + Number filters: + - `finite` + - `greaterThan` + - `greaterThanOrEqualTo` + - `lessThan` + - `lessThanOrEqualTo` + - `int` + - `multipleOf` + - `between` + - `nonNaN` + - `positive` + - `negative` + - `nonPositive` + - `nonNegative` + + BigInt filters: + - `greaterThanBigInt` + - `greaterThanOrEqualToBigInt` + - `lessThanBigInt` + - `lessThanOrEqualToBigInt` + - `betweenBigInt` + - `positiveBigInt` + - `negativeBigInt` + - `nonNegativeBigInt` + - `nonPositiveBigInt` + + Duration filters: + - `lessThanDuration` + - `lessThanOrEqualToDuration` + - `greaterThanDuration` + - `greaterThanOrEqualToDuration` + - `betweenDuration` + + Array filters: + - `minItems` + - `maxItems` + - `itemsCount` + + Date filters: + - `validDate` + - `lessThanDate` + - `lessThanOrEqualToDate` + - `greaterThanDate` + - `greaterThanOrEqualToDate` + - `betweenDate` + + BigDecimal filters: + - `greaterThanBigDecimal` + - `greaterThanOrEqualToBigDecimal` + - `lessThanBigDecimal` + - `lessThanOrEqualToBigDecimal` + - `positiveBigDecimal` + - `nonNegativeBigDecimal` + - `negativeBigDecimal` + - `nonPositiveBigDecimal` + - `betweenBigDecimal` + +- [#4508](https://github.com/Effect-TS/effect/pull/4508) [`f2aee98`](https://github.com/Effect-TS/effect/commit/f2aee989b0a600900ce83e7f460d02908620c80f) Thanks @gcanti! - Schema: More Accurate Return Types for `ArrayEnsure` and `NonEmptyArrayEnsure`. + + **Before** + + ```ts + import { Schema } from "effect" + + const schema1 = Schema.ArrayEnsure(Schema.String) + + // @ts-expect-error: Property 'from' does not exist + schema1.from + + const schema2 = Schema.NonEmptyArrayEnsure(Schema.String) + + // @ts-expect-error: Property 'from' does not exist + schema2.from + ``` + + **After** + + ```ts + import { Schema } from "effect" + + const schema1 = Schema.ArrayEnsure(Schema.String) + + // ┌─── Schema.Union<[typeof Schema.String, Schema.Array$]> + // ▼ + schema1.from + + const schema2 = Schema.NonEmptyArrayEnsure(Schema.String) + + // ┌─── Schema.Union<[typeof Schema.String, Schema.NonEmptyArray]> + // ▼ + schema2.from + ``` + +- [#4509](https://github.com/Effect-TS/effect/pull/4509) [`fb798eb`](https://github.com/Effect-TS/effect/commit/fb798eb9061f1191badc017d1aa649360254da20) Thanks @gcanti! - Schema: More Accurate Return Types for: + - `transformLiteral` + - `clamp` + - `clampBigInt` + - `clampDuration` + - `clampBigDecimal` + - `head` + - `headNonEmpty` + - `headOrElse` + +- [#4524](https://github.com/Effect-TS/effect/pull/4524) [`2251b15`](https://github.com/Effect-TS/effect/commit/2251b1528810bb695b37ce388b653cec0c5bf80c) Thanks @gcanti! - Schema: More Accurate Return Type for `parseNumber`. + + **Before** + + ```ts + import { Schema } from "effect" + + const schema = Schema.parseNumber(Schema.String) + + // ┌─── Schema + // ▼ + schema.from + ``` + + **After** + + ```ts + import { Schema } from "effect" + + const schema = Schema.parseNumber(Schema.String) + + // ┌─── typeof Schema.String + // ▼ + schema.from + ``` + +- [#4483](https://github.com/Effect-TS/effect/pull/4483) [`2e15c1e`](https://github.com/Effect-TS/effect/commit/2e15c1e33648add0b29fe274fbcb7294b7515085) Thanks @mikearnaldi! - Fix nested batching + +- [#4514](https://github.com/Effect-TS/effect/pull/4514) [`a4979db`](https://github.com/Effect-TS/effect/commit/a4979db021aef16e731be64df196b72088fc4376) Thanks @gcanti! - Schema: add missing `from` property to `brand` interface. + + Before + + ```ts + import { Schema } from "effect" + + const schema = Schema.String.pipe(Schema.brand("my-brand")) + + // @ts-expect-error: Property 'from' does not exist + schema.from + ``` + + After + + ```ts + import { Schema } from "effect" + + const schema = Schema.String.pipe(Schema.brand("my-brand")) + + // ┌─── typeof Schema.String + // ▼ + schema.from + ``` + +- [#4496](https://github.com/Effect-TS/effect/pull/4496) [`b74255a`](https://github.com/Effect-TS/effect/commit/b74255a304ad49d60bedb1a260fd697f370af27a) Thanks @tim-smart! - ensure fibers can't be added to Fiber{Handle,Set,Map} during closing + +- [#4419](https://github.com/Effect-TS/effect/pull/4419) [`d7f6a5c`](https://github.com/Effect-TS/effect/commit/d7f6a5c7d26c1963dcd864ca62360d20d08c7b49) Thanks @KhraksMamtsov! - Fix Context.Tag unification + +- [#4495](https://github.com/Effect-TS/effect/pull/4495) [`9dd8979`](https://github.com/Effect-TS/effect/commit/9dd8979e940915b1cc1b1f264f3d019c77a65a02) Thanks @KhraksMamtsov! - Simplify `sortWith`, `sort`, `reverse`, `sortBy`, `unzip`, `dedupe` signatures in Array module + +- [#4507](https://github.com/Effect-TS/effect/pull/4507) [`477b488`](https://github.com/Effect-TS/effect/commit/477b488284f47c5469d7fba3e4065fb7e3b6556e) Thanks @gcanti! - Schema: More Accurate Return Type for `parseJson(schema)`. + + **Before** + + ```ts + import { Schema } from "effect" + + // ┌─── Schema.SchemaClass<{ readonly a: number; }, string> + // ▼ + const schema = Schema.parseJson( + Schema.Struct({ + a: Schema.NumberFromString + }) + ) + + // @ts-expect-error: Property 'to' does not exist + schema.to + ``` + + **After** + + ```ts + import { Schema } from "effect" + + // ┌─── Schema.transform, Schema.Struct<{ a: typeof Schema.NumberFromString; }>> + // ▼ + const schema = Schema.parseJson( + Schema.Struct({ + a: Schema.NumberFromString + }) + ) + + // ┌─── Schema.Struct<{ a: typeof Schema.NumberFromString; }> + // ▼ + schema.to + ``` + +- [#4519](https://github.com/Effect-TS/effect/pull/4519) [`10932cb`](https://github.com/Effect-TS/effect/commit/10932cbf58fc721ada631cebec42f773ce96d3cc) Thanks @gcanti! - Refactor `JSONSchema` to use `additionalProperties` instead of `patternProperties` for simple records, closes #4518. + + This update improves how records are represented in JSON Schema by replacing `patternProperties` with `additionalProperties`, resolving issues in OpenAPI schema generation. + + **Why the change?** + - **Fixes OpenAPI issues** – Previously, records were represented using `patternProperties`, which caused problems with OpenAPI tools. + - **Better schema compatibility** – Some tools, like `openapi-ts`, struggled with `patternProperties`, generating `Record` instead of the correct type. + - **Fixes missing example values** – When using `patternProperties`, OpenAPI failed to generate proper response examples, displaying only `{}`. + - **Simplifies schema modification** – Users previously had to manually fix schemas with `OpenApi.Transform`, which was messy and lacked type safety. + + **Before** + + ```ts + import { JSONSchema, Schema } from "effect" + + const schema = Schema.Record({ key: Schema.String, value: Schema.Number }) + + console.log(JSON.stringify(JSONSchema.make(schema), null, 2)) + /* + { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "required": [], + "properties": {}, + "patternProperties": { + "": { // ❌ Empty string pattern + "type": "number" + } + } + } + */ + ``` + + **After** + + Now, `additionalProperties` is used instead, which properly represents an open-ended record: + + ```ts + import { JSONSchema, Schema } from "effect" + + const schema = Schema.Record({ key: Schema.String, value: Schema.Number }) + + console.log(JSON.stringify(JSONSchema.make(schema), null, 2)) + /* + { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "required": [], + "properties": {}, + "additionalProperties": { // ✅ Represents unrestricted record keys + "type": "number" + } + } + */ + ``` + +- [#4501](https://github.com/Effect-TS/effect/pull/4501) [`9f6c784`](https://github.com/Effect-TS/effect/commit/9f6c78468b3b5e9ebfc38ffdfb70702901ee977b) Thanks @gcanti! - Schema: Add Missing `declare` API Interface to Expose Type Parameters. + + **Example** + + ```ts + import { Schema } from "effect" + + const schema = Schema.OptionFromSelf(Schema.String) + + // ┌─── readonly [typeof Schema.String] + // ▼ + schema.typeParameters + ``` + +- [#4487](https://github.com/Effect-TS/effect/pull/4487) [`2c639ec`](https://github.com/Effect-TS/effect/commit/2c639ecee332de4266e36022c989c35ae4e02105) Thanks @gcanti! - Schema: more precise return types when transformations are involved. + - `Chunk` + - `NonEmptyChunk` + - `Redacted` + - `Option` + - `OptionFromNullOr` + - `OptionFromUndefinedOr` + - `OptionFromNullishOr` + - `Either` + - `EitherFromUnion` + - `ReadonlyMap` + - `Map` + - `HashMap` + - `ReadonlySet` + - `Set` + - `HashSet` + - `List` + - `Cause` + - `Exit` + - `SortedSet` + - `head` + - `headNonEmpty` + - `headOrElse` + + **Example** (with `Schema.Chunk`) + + Before + + ```ts + import { Schema } from "effect" + + const schema = Schema.Chunk(Schema.Number) + + // Property 'from' does not exist on type 'Chunk' + schema.from + ``` + + After + + ```ts + import { Schema } from "effect" + + const schema = Schema.Chunk(Schema.Number) + + // Schema.Array$ + schema.from + ``` + +- [#4492](https://github.com/Effect-TS/effect/pull/4492) [`886aaa8`](https://github.com/Effect-TS/effect/commit/886aaa81e06dfd3cd9391e8ea987d8cd5ada1124) Thanks @gcanti! - Schema: Improve `Literal` return type — now returns `SchemaClass` instead of `Schema` + +## 3.13.2 + +### Patch Changes + +- [#4472](https://github.com/Effect-TS/effect/pull/4472) [`31be72a`](https://github.com/Effect-TS/effect/commit/31be72ada118cb84a942e67b1663263f8db74a9f) Thanks @gcanti! - Fix `Schema.Enums` `toString()` method to display correct enum values. + + Now, `toString()` correctly displays the actual enum values instead of internal numeric indices. + + **Before** + + ```ts + import { Schema } from "effect" + + enum Fruits { + Apple = "apple", + Banana = "banana", + Cantaloupe = 0 + } + + const schema = Schema.Enums(Fruits) + + console.log(String(schema)) + // Output: ❌ (incorrect) + ``` + + **After** + + ```ts + import { Schema } from "effect" + + enum Fruits { + Apple = "apple", + Banana = "banana", + Cantaloupe = 0 + } + + const schema = Schema.Enums(Fruits) + + console.log(String(schema)) + // Output: ✅ (correct) + ``` + +## 3.13.1 + +### Patch Changes + +- [#4454](https://github.com/Effect-TS/effect/pull/4454) [`b56a211`](https://github.com/Effect-TS/effect/commit/b56a2110569fd0ec0b57ac137743e926d49f51cc) Thanks @FizzyElt! - fix Option filterMap example + +## 3.13.0 + +### Minor Changes + +- [#4280](https://github.com/Effect-TS/effect/pull/4280) [`8baef83`](https://github.com/Effect-TS/effect/commit/8baef83e7ff0b7bc0738b680e1ef013065386cff) Thanks @tim-smart! - add Promise based apis to Fiber{Handle,Set,Map} modules + +- [#4280](https://github.com/Effect-TS/effect/pull/4280) [`655bfe2`](https://github.com/Effect-TS/effect/commit/655bfe29e44cc3f0fb9b4e53038f50b891c188df) Thanks @gcanti! - Add `Effect.transposeOption`, closes #3142. + + Converts an `Option` of an `Effect` into an `Effect` of an `Option`. + + **Details** + + This function transforms an `Option>` into an + `Effect, E, R>`. If the `Option` is `None`, the resulting `Effect` + will immediately succeed with a `None` value. If the `Option` is `Some`, the + inner `Effect` will be executed, and its result wrapped in a `Some`. + + **Example** + + ```ts + import { Effect, Option } from "effect" + + // ┌─── Option> + // ▼ + const maybe = Option.some(Effect.succeed(42)) + + // ┌─── Effect, never, never> + // ▼ + const result = Effect.transposeOption(maybe) + + console.log(Effect.runSync(result)) + // Output: { _id: 'Option', _tag: 'Some', value: 42 } + ``` + +- [#4280](https://github.com/Effect-TS/effect/pull/4280) [`d90cbc2`](https://github.com/Effect-TS/effect/commit/d90cbc274e2742d18671fe65aa4764c057eb6cba) Thanks @indietyp! - Add `Effect.whenLogLevel`, which conditionally executes an effect if the specified log level is enabled + +- [#4280](https://github.com/Effect-TS/effect/pull/4280) [`75632bd`](https://github.com/Effect-TS/effect/commit/75632bd44b8025101d652ccbaeef898c7086c91c) Thanks @tim-smart! - add RcMap.touch, for reseting the idle timeout for an item + +- [#4280](https://github.com/Effect-TS/effect/pull/4280) [`c874a2e`](https://github.com/Effect-TS/effect/commit/c874a2e4b17e9d71904ca8375bb77b020975cb1d) Thanks @LaureRC! - Add HashMap.some + +- [#4280](https://github.com/Effect-TS/effect/pull/4280) [`bf865e5`](https://github.com/Effect-TS/effect/commit/bf865e5833f77fd8f6c06944ca9d507b54488301) Thanks @tim-smart! - allow accessing args in Effect.fn pipe + +- [#4280](https://github.com/Effect-TS/effect/pull/4280) [`f98b2b7`](https://github.com/Effect-TS/effect/commit/f98b2b7592cf20f9d85313e7f1e964cb65878138) Thanks @tim-smart! - add RcMap.invalidate api, for removing a resource from an RcMap + +- [#4280](https://github.com/Effect-TS/effect/pull/4280) [`de8ce92`](https://github.com/Effect-TS/effect/commit/de8ce924923eaa4e1b761a97eb45ec967389f3d5) Thanks @mikearnaldi! - Add Layer.updateService mirroring Effect.updateService + +- [#4280](https://github.com/Effect-TS/effect/pull/4280) [`db426a5`](https://github.com/Effect-TS/effect/commit/db426a5fb41ab84d18e3c8753a7329b4de544245) Thanks @KhraksMamtsov! - `Differ` implements `Pipeable` + +- [#4280](https://github.com/Effect-TS/effect/pull/4280) [`6862444`](https://github.com/Effect-TS/effect/commit/6862444094906ad4f2cb077ff3b9cc0b73880c8c) Thanks @thewilkybarkid! - Make it easy to convert a DateTime.Zoned to a DateTime.Utc + +- [#4280](https://github.com/Effect-TS/effect/pull/4280) [`5fc8a90`](https://github.com/Effect-TS/effect/commit/5fc8a90ba46a5fd9f3b643f0b5aeadc69d717339) Thanks @gcanti! - Add missing `Either.void` constructor. + +- [#4280](https://github.com/Effect-TS/effect/pull/4280) [`546a492`](https://github.com/Effect-TS/effect/commit/546a492e60eb2b8b048a489a474b934ea0877005) Thanks @vinassefranche! - Add `HashMap.toValues` and `HashSet.toValues` getters + +- [#4280](https://github.com/Effect-TS/effect/pull/4280) [`65c4796`](https://github.com/Effect-TS/effect/commit/65c47966ce39055f02cf5c808daabb3ea6442b0b) Thanks @tim-smart! - add {FiberHandle,FiberSet,FiberMap}.awaitEmpty apis + +- [#4280](https://github.com/Effect-TS/effect/pull/4280) [`9760fdc`](https://github.com/Effect-TS/effect/commit/9760fdc37bdaef9da8b150e46b86ddfbe2ad9221) Thanks @gcanti! - Schema: Add `standardSchemaV1` API to Generate a [Standard Schema v1](https://standardschema.dev/). + + **Example** + + ```ts + import { Schema } from "effect" + + const schema = Schema.Struct({ + name: Schema.String + }) + + // ┌─── StandardSchemaV1<{ readonly name: string; }> + // ▼ + const standardSchema = Schema.standardSchemaV1(schema) + ``` + +- [#4280](https://github.com/Effect-TS/effect/pull/4280) [`5b471e7`](https://github.com/Effect-TS/effect/commit/5b471e7d4317e8ee5d72bbbd3e0c9775160949ab) Thanks @fubhy! - Added `Duration.formatIso` and `Duration.fromIso` for formatting and parsing ISO8601 durations. + +- [#4280](https://github.com/Effect-TS/effect/pull/4280) [`4f810cc`](https://github.com/Effect-TS/effect/commit/4f810cc2770e9f1f266851d2cb6257112c12af49) Thanks @tim-smart! - add Effect.filterEffect\* apis + + #### Effect.filterEffectOrElse + + Filters an effect with an effectful predicate, falling back to an alternative + effect if the predicate fails. + + ```ts + import { Effect, pipe } from "effect" + + // Define a user interface + interface User { + readonly name: string + } + + // Simulate an asynchronous authentication function + declare const auth: () => Promise + + const program = pipe( + Effect.promise(() => auth()), + // Use filterEffectOrElse with an effectful predicate + Effect.filterEffectOrElse({ + predicate: (user) => Effect.succeed(user !== null), + orElse: (user) => Effect.fail(new Error(`Unauthorized user: ${user}`)) + }) + ) + ``` + + #### Effect.filterEffectOrFail + + Filters an effect with an effectful predicate, failing with a custom error if the predicate fails. + + ```ts + import { Effect, pipe } from "effect" + + // Define a user interface + interface User { + readonly name: string + } + + // Simulate an asynchronous authentication function + declare const auth: () => Promise + + const program = pipe( + Effect.promise(() => auth()), + // Use filterEffectOrFail with an effectful predicate + Effect.filterEffectOrFail({ + predicate: (user) => Effect.succeed(user !== null), + orFailWith: (user) => Effect.fail(new Error(`Unauthorized user: ${user}`)) + }) + ) + ``` + +### Patch Changes + +- [#4280](https://github.com/Effect-TS/effect/pull/4280) [`cf8b2dd`](https://github.com/Effect-TS/effect/commit/cf8b2dd112f8e092ed99d78fd728db0f91c29050) Thanks @KhraksMamtsov! - `Trie` type annotations have been aligned. The type parameter was made covariant because the structure is immutable. + +## 3.12.12 + +### Patch Changes + +- [#4440](https://github.com/Effect-TS/effect/pull/4440) [`4018eae`](https://github.com/Effect-TS/effect/commit/4018eaed2733241676ddb8c52416f463a8c32e35) Thanks @gcanti! - Schema: add missing support for tuple annotations in `TaggedRequest`. + +- [#4439](https://github.com/Effect-TS/effect/pull/4439) [`543d36d`](https://github.com/Effect-TS/effect/commit/543d36d1a11452560b01ab966a82529ad5fee8c9) Thanks @gcanti! - Schedule: fix unsafe `tapOutput` signature. + + Previously, `tapOutput` allowed using an output type that wasn't properly inferred, leading to potential runtime errors. Now, TypeScript correctly detects mismatches at compile time, preventing unexpected crashes. + + **Before (Unsafe, Causes Runtime Error)** + + ```ts + import { Effect, Schedule, Console } from "effect" + + const schedule = Schedule.once.pipe( + Schedule.as(1), + Schedule.tapOutput((s: string) => Console.log(s.trim())) // ❌ Runtime error + ) + + Effect.runPromise(Effect.void.pipe(Effect.schedule(schedule))) + // throws: TypeError: s.trim is not a function + ``` + + **After (Safe, Catches Type Error at Compile Time)** + + ```ts + import { Console, Schedule } from "effect" + + const schedule = Schedule.once.pipe( + Schedule.as(1), + // ✅ Type Error: Type 'number' is not assignable to type 'string' + Schedule.tapOutput((s: string) => Console.log(s.trim())) + ) + ``` + +- [#4447](https://github.com/Effect-TS/effect/pull/4447) [`f70a65a`](https://github.com/Effect-TS/effect/commit/f70a65ac80c6635d80b12beaf4d32a9cc59fa143) Thanks @gcanti! - Preserve function `length` property in `Effect.fn` / `Effect.fnUntraced`, closes #4435 + + Previously, functions created with `Effect.fn` and `Effect.fnUntraced` always had a `.length` of `0`, regardless of their actual number of parameters. This has been fixed so that the `length` property correctly reflects the expected number of arguments. + + **Before** + + ```ts + import { Effect } from "effect" + + const fn1 = Effect.fn("fn1")(function* (n: number) { + return n + }) + + console.log(fn1.length) + // Output: 0 ❌ (incorrect) + + const fn2 = Effect.fnUntraced(function* (n: number) { + return n + }) + + console.log(fn2.length) + // Output: 0 ❌ (incorrect) + ``` + + **After** + + ```ts + import { Effect } from "effect" + + const fn1 = Effect.fn("fn1")(function* (n: number) { + return n + }) + + console.log(fn1.length) + // Output: 1 ✅ (correct) + + const fn2 = Effect.fnUntraced(function* (n: number) { + return n + }) + + console.log(fn2.length) + // Output: 1 ✅ (correct) + ``` + +- [#4422](https://github.com/Effect-TS/effect/pull/4422) [`ba409f6`](https://github.com/Effect-TS/effect/commit/ba409f69c41aeaa29e475c0630735726eaf4dbac) Thanks @mikearnaldi! - Fix Context.Tag inference using explicit generics + +- [#4432](https://github.com/Effect-TS/effect/pull/4432) [`3d2e356`](https://github.com/Effect-TS/effect/commit/3d2e3565e8a43d1bdb5daee8db3b90f56d71d859) Thanks @tim-smart! - use Map for Scope finalizers, to ensure they are always added + +## 3.12.11 + +### Patch Changes + +- [#4430](https://github.com/Effect-TS/effect/pull/4430) [`b6a032f`](https://github.com/Effect-TS/effect/commit/b6a032f07bffa020a848c813881879395134fa20) Thanks @tim-smart! - ensure Channel executor catches defects in doneHalt + +- [#4426](https://github.com/Effect-TS/effect/pull/4426) [`42ddd5f`](https://github.com/Effect-TS/effect/commit/42ddd5f144ce9f9d94a036679ebbd626446d37f5) Thanks @gcanti! - Schema: add missing `description` annotation to `BooleanFromString`. + +- [#4404](https://github.com/Effect-TS/effect/pull/4404) [`2fe447c`](https://github.com/Effect-TS/effect/commit/2fe447c6354d334f9c591b8a8481818f5f0e797e) Thanks @gcanti! - Update `forEach` function in `Chunk` to include missing index parameter. + +## 3.12.10 + +### Patch Changes + +- [#4412](https://github.com/Effect-TS/effect/pull/4412) [`e30f132`](https://github.com/Effect-TS/effect/commit/e30f132c336c9d0760bad39f82a55c7ce5159eb7) Thanks @KhraksMamtsov! - Fix STM unification + +- [#4403](https://github.com/Effect-TS/effect/pull/4403) [`33fa667`](https://github.com/Effect-TS/effect/commit/33fa667c2623be1026e1ccee91bd44f73b09020a) Thanks @gcanti! - Duration: fix `format` output when the input is zero. + + Before + + ```ts + import { Duration } from "effect" + + console.log(Duration.format(Duration.zero)) + // Output: "" + ``` + + After + + ```ts + import { Duration } from "effect" + + console.log(Duration.format(Duration.zero)) + // Output: "0" + ``` + +- [#4411](https://github.com/Effect-TS/effect/pull/4411) [`87f5f28`](https://github.com/Effect-TS/effect/commit/87f5f2842e4196cb88d13f10f443ff0567e82832) Thanks @gcanti! - Enhance `TagClass` and `ReferenceClass` to enforce `key` type narrowing, closes #4409. + + The `key` property in `TagClass` and `ReferenceClass` now correctly retains its specific string value, just like in `Effect.Service` + + ```ts + import { Context, Effect } from "effect" + + // ------------------------------------------------------------------------------------- + // `key` field + // ------------------------------------------------------------------------------------- + + class A extends Effect.Service()("A", { succeed: { a: "value" } }) {} + + // $ExpectType "A" + A.key + + class B extends Context.Tag("B")() {} + + // $ExpectType "B" + B.key + + class C extends Context.Reference()("C", { defaultValue: () => 0 }) {} + + // $ExpectType "C" + C.key + ``` + +- [#4397](https://github.com/Effect-TS/effect/pull/4397) [`4dbd170`](https://github.com/Effect-TS/effect/commit/4dbd170538e8fb7a36aa7c469c6f93b6c7000091) Thanks @thewilkybarkid! - Make Array.makeBy dual + +## 3.12.9 + +### Patch Changes + +- [#4392](https://github.com/Effect-TS/effect/pull/4392) [`1b4a4e9`](https://github.com/Effect-TS/effect/commit/1b4a4e904ef5227ec7d9114d4e417eca19eed940) Thanks @gcanti! - Fix internal import in Schema.ts, closes #4391 + +## 3.12.8 + +### Patch Changes + +- [#4341](https://github.com/Effect-TS/effect/pull/4341) [`766113c`](https://github.com/Effect-TS/effect/commit/766113c0ea3512cdb887650ead8ba314236e22ee) Thanks @fubhy! - Improve `Duration.decode` Handling of High-Resolution Time + - **Ensured Immutability**: Added the `readonly` modifier to `[seconds: number, nanos: number]` in `DurationInput` to prevent accidental modifications. + - **Better Edge Case Handling**: Now correctly processes special values like `-Infinity` and `NaN` when they appear in the tuple representation of duration. + +- [#4333](https://github.com/Effect-TS/effect/pull/4333) [`712277f`](https://github.com/Effect-TS/effect/commit/712277f949052a24b46e4aa234063a6abf395c90) Thanks @gcanti! - Cron: `unsafeParse` now throws a more informative error instead of a generic one + +- [#4387](https://github.com/Effect-TS/effect/pull/4387) [`f269122`](https://github.com/Effect-TS/effect/commit/f269122508693b111142994dd48698ddc75f3d69) Thanks @KhraksMamtsov! - A more precise signature has been applied for `Effect.schedule` + +- [#4351](https://github.com/Effect-TS/effect/pull/4351) [`430c846`](https://github.com/Effect-TS/effect/commit/430c846cbac05b187e3d24ac8dfee0cf22506f7c) Thanks @tim-smart! - fix Layer.scope types to correctly use the Scope tag identifier + +- [#4344](https://github.com/Effect-TS/effect/pull/4344) [`7b03057`](https://github.com/Effect-TS/effect/commit/7b03057507d2dab5e6793beb9c578dedaaeb15fe) Thanks @IMax153! - Expose `Schedule.isSchedule` + +- [#4313](https://github.com/Effect-TS/effect/pull/4313) [`a9c94c8`](https://github.com/Effect-TS/effect/commit/a9c94c807755610831211a686d2fad849ab38eb4) Thanks @gcanti! - Schema: Update `Duration` Encoding to a Tagged Union Format. + + This changeset fixes the `Duration` schema to support all possible duration types, including finite, infinite, and nanosecond durations. The encoding format has been updated from a tuple (`readonly [seconds: number, nanos: number]`) to a tagged union. + + This update introduces a change to the encoding format. The previous tuple representation is replaced with a more expressive tagged union, which accommodates all duration types: + + ```ts + type DurationEncoded = + | { + readonly _tag: "Millis" + readonly millis: number + } + | { + readonly _tag: "Nanos" + readonly nanos: string + } + | { + readonly _tag: "Infinity" + } + ``` + + **Rationale** + + The `Duration` schema is primarily used to encode durations for transmission. The new tagged union format ensures clear and precise encoding for: + - Finite durations, such as milliseconds. + - Infinite durations, such as `Duration.infinity`. + - Nanosecond durations. + + **Example** + + ```ts + import { Duration, Schema } from "effect" + + // Encoding a finite duration in milliseconds + console.log(Schema.encodeSync(Schema.Duration)(Duration.millis(1000))) + // Output: { _tag: 'Millis', millis: 1000 } + + // Encoding an infinite duration + console.log(Schema.encodeSync(Schema.Duration)(Duration.infinity)) + // Output: { _tag: 'Infinity' } + + // Encoding a duration in nanoseconds + console.log(Schema.encodeSync(Schema.Duration)(Duration.nanos(1000n))) + // Output: { _tag: 'Nanos', nanos: '1000' } + ``` + +- [#4331](https://github.com/Effect-TS/effect/pull/4331) [`107e6f0`](https://github.com/Effect-TS/effect/commit/107e6f0557a1e2d3b0dce25d62fa1e2601521752) Thanks @gcanti! - Schema: Improve encoding in `Defect` and add test for array-based defects. + +- [#4329](https://github.com/Effect-TS/effect/pull/4329) [`65c11b9`](https://github.com/Effect-TS/effect/commit/65c11b9266ec9447c31c26fe3ed35c73bd3b81fd) Thanks @gcanti! - Schema: Update `itemsCount` to allow `0` as a valid argument, closes #4328. + +- [#4330](https://github.com/Effect-TS/effect/pull/4330) [`e386d2f`](https://github.com/Effect-TS/effect/commit/e386d2f1b3ab3ac2c14ee76de11f5963d32a3df4) Thanks @gcanti! - Add missing overload for `Option.as`. + +- [#4352](https://github.com/Effect-TS/effect/pull/4352) [`9172efb`](https://github.com/Effect-TS/effect/commit/9172efba98bc6a82353e6ec2af61ac08f038ba64) Thanks @tim-smart! - optimize Stream.toReadableStream + +## 3.12.7 + +### Patch Changes + +- [#4320](https://github.com/Effect-TS/effect/pull/4320) [`8dff1d1`](https://github.com/Effect-TS/effect/commit/8dff1d1bff76cdba643cad7f0bf864300f08bc61) Thanks @KhraksMamtsov! - Fix: Cannot find name 'MissingSelfGeneric'. + +## 3.12.6 + +### Patch Changes + +- [#4307](https://github.com/Effect-TS/effect/pull/4307) [`289c13b`](https://github.com/Effect-TS/effect/commit/289c13b38e8e35b214d46d385d05dead176c87cd) Thanks @gcanti! - Schema: Enhance error messages for discriminated unions. + + **Before** + + ```ts + import { Schema } from "effect" + + const schema = Schema.Union( + Schema.Tuple(Schema.Literal(-1), Schema.Literal(0)).annotations({ + identifier: "A" + }), + Schema.Tuple(Schema.NonNegativeInt, Schema.NonNegativeInt).annotations({ + identifier: "B" + }) + ).annotations({ identifier: "AB" }) + + Schema.decodeUnknownSync(schema)([-500, 0]) + /* + throws: + ParseError: AB + ├─ { readonly 0: -1 } + │ └─ ["0"] + │ └─ Expected -1, actual -500 + └─ B + └─ [0] + └─ NonNegativeInt + └─ From side refinement failure + └─ NonNegative + └─ Predicate refinement failure + └─ Expected a non-negative number, actual -500 + */ + ``` + + **After** + + ```diff + import { Schema } from "effect" + + const schema = Schema.Union( + Schema.Tuple(Schema.Literal(-1), Schema.Literal(0)).annotations({ + identifier: "A" + }), + Schema.Tuple(Schema.NonNegativeInt, Schema.NonNegativeInt).annotations({ + identifier: "B" + }) + ).annotations({ identifier: "AB" }) + + Schema.decodeUnknownSync(schema)([-500, 0]) + /* + throws: + ParseError: AB + -├─ { readonly 0: -1 } + +├─ A + │ └─ ["0"] + │ └─ Expected -1, actual -500 + └─ B + └─ [0] + └─ NonNegativeInt + └─ From side refinement failure + └─ NonNegative + └─ Predicate refinement failure + └─ Expected a non-negative number, actual -500 + */ + ``` + +- [#4298](https://github.com/Effect-TS/effect/pull/4298) [`8b4e75d`](https://github.com/Effect-TS/effect/commit/8b4e75d35daea807c447ca760948a717aa66bb52) Thanks @KhraksMamtsov! - Added type-level validation for the `Effect.Service` function to ensure the `Self` generic parameter is provided. If the generic is missing, the `MissingSelfGeneric` type will be returned, indicating that the generic parameter must be specified. This improves type safety and prevents misuse of the `Effect.Service` function. + + ```ts + type MissingSelfGeneric = + `Missing \`Self\` generic - use \`class Self extends Service()...\`` + ``` + +- [#4292](https://github.com/Effect-TS/effect/pull/4292) [`fc5e0f0`](https://github.com/Effect-TS/effect/commit/fc5e0f0d357a0051cfa01c1ede83ffdd3cb41ab1) Thanks @gcanti! - Improve `UnknownException` error messages + + `UnknownException` error messages now include the name of the Effect api that + created the error. + + ```ts + import { Effect } from "effect" + + Effect.tryPromise(() => + Promise.reject(new Error("The operation failed")) + ).pipe(Effect.catchAllCause(Effect.logError), Effect.runFork) + + // timestamp=2025-01-21T00:41:03.403Z level=ERROR fiber=#0 cause="UnknownException: An unknown error occurred in Effect.tryPromise + // at fail (.../effect/packages/effect/src/internal/core-effect.ts:1654:19) + // at (.../effect/packages/effect/src/internal/core-effect.ts:1674:26) { + // [cause]: Error: The operation failed + // at (.../effect/scratchpad/error.ts:4:24) + // at .../effect/packages/effect/src/internal/core-effect.ts:1671:7 + // }" + ``` + +- [#4309](https://github.com/Effect-TS/effect/pull/4309) [`004fd2b`](https://github.com/Effect-TS/effect/commit/004fd2bbd1459e64fb1b57f02eeb791ca5ea1ea5) Thanks @gcanti! - Schema: Enforce Finite Durations in `DurationFromNanos`. + + This update ensures that `DurationFromNanos` only accepts finite durations. Previously, the schema did not explicitly enforce this constraint. + + A filter has been added to validate that the duration is finite. + + ```diff + DurationFromSelf + +.pipe( + + filter((duration) => duration_.isFinite(duration), { + + description: "a finite duration" + + }) + ) + ``` + +- [#4314](https://github.com/Effect-TS/effect/pull/4314) [`b2a31be`](https://github.com/Effect-TS/effect/commit/b2a31be85c35d891351ce4f9a2cc93ece0c257f6) Thanks @gcanti! - Duration: make `DurationValue` properties readonly. + +- [#4287](https://github.com/Effect-TS/effect/pull/4287) [`5514d05`](https://github.com/Effect-TS/effect/commit/5514d05b5cd586ff5868b8bd41c959e95e6c33cd) Thanks @gcanti! - Array: Fix `Either` import and correct `partition` example. + +- [#4301](https://github.com/Effect-TS/effect/pull/4301) [`bf5f0ae`](https://github.com/Effect-TS/effect/commit/bf5f0ae9daa0170471678e22585e8ec14ce667bb) Thanks @gcanti! - Schema: Fix `BigIntFromNumber` to enforce upper and lower bounds. + + This update ensures the `BigIntFromNumber` schema adheres to safe integer limits by applying the following bounds: + + ```diff + BigIntFromSelf + + .pipe( + + betweenBigInt( + + BigInt(Number.MIN_SAFE_INTEGER), + + BigInt(Number.MAX_SAFE_INTEGER) + + ) + + ) + ``` + +- [#4228](https://github.com/Effect-TS/effect/pull/4228) [`3b19bcf`](https://github.com/Effect-TS/effect/commit/3b19bcfd3aaadb6c9253428622df524537c8e626) Thanks @fubhy! - Fixed conflicting `ParseError` tags between `Cron` and `Schema` + +- [#4294](https://github.com/Effect-TS/effect/pull/4294) [`b064b3b`](https://github.com/Effect-TS/effect/commit/b064b3b293615fd268cc5a5647d0981eb67750b8) Thanks @tim-smart! - ensure cause is rendered in FiberFailure + +- [#4307](https://github.com/Effect-TS/effect/pull/4307) [`289c13b`](https://github.com/Effect-TS/effect/commit/289c13b38e8e35b214d46d385d05dead176c87cd) Thanks @gcanti! - Schema: Add Support for Infinity in `Duration`. + + This update adds support for encoding `Duration.infinity` in `Schema.Duration`. + + **Before** + + Attempting to encode `Duration.infinity` resulted in a `ParseError` due to the lack of support for `Infinity` in `Schema.Duration`: + + ```ts + import { Duration, Schema } from "effect" + + console.log(Schema.encodeUnknownSync(Schema.Duration)(Duration.infinity)) + /* + throws: + ParseError: Duration + └─ Encoded side transformation failure + └─ HRTime + └─ [0] + └─ NonNegativeInt + └─ Predicate refinement failure + └─ Expected an integer, actual Infinity + */ + ``` + + **After** + + The updated behavior successfully encodes `Duration.infinity` as `[ -1, 0 ]`: + + ```ts + import { Duration, Schema } from "effect" + + console.log(Schema.encodeUnknownSync(Schema.Duration)(Duration.infinity)) + // Output: [ -1, 0 ] + ``` + +- [#4300](https://github.com/Effect-TS/effect/pull/4300) [`f474678`](https://github.com/Effect-TS/effect/commit/f474678bf10b8f1c80e3dc096ddc7ecf20b2b23e) Thanks @gcanti! - Schema: update `pluck` type signature to respect optional fields. + + **Before** + + ```ts + import { Schema } from "effect" + + const schema1 = Schema.Struct({ a: Schema.optional(Schema.String) }) + + /* + const schema2: Schema.Schema + */ + const schema2 = Schema.pluck(schema1, "a") + ``` + + **After** + + ```ts + import { Schema } from "effect" + + const schema1 = Schema.Struct({ a: Schema.optional(Schema.String) }) + + /* + const schema2: Schema.Schema + */ + const schema2 = Schema.pluck(schema1, "a") + ``` + +- [#4296](https://github.com/Effect-TS/effect/pull/4296) [`ee187d0`](https://github.com/Effect-TS/effect/commit/ee187d098007a402844c94d04f0cd8f07695377a) Thanks @gcanti! - fix: update `Cause.isCause` type from 'never' to 'unknown' + +## 3.12.5 + +### Patch Changes + +- [#4273](https://github.com/Effect-TS/effect/pull/4273) [`a8b0ddb`](https://github.com/Effect-TS/effect/commit/a8b0ddb84710054799fc8f57485b95d00093ada1) Thanks @gcanti! - Arbitrary: Fix bug adjusting array constraints for schemas with fixed and rest elements + + This fix ensures that when a schema includes both fixed elements and a rest element, the constraints for the array are correctly adjusted. The adjustment now subtracts the number of values generated by the fixed elements from the overall constraints. + +- [#4259](https://github.com/Effect-TS/effect/pull/4259) [`507d546`](https://github.com/Effect-TS/effect/commit/507d546bd49db31000425fb5da88c434e4291bea) Thanks @gcanti! - Schema: improve error messages for invalid transformations + + **Before** + + ```ts + import { Schema } from "effect" + + Schema.decodeUnknownSync(Schema.NumberFromString)("a") + /* + throws: + ParseError: NumberFromString + └─ Transformation process failure + └─ Expected NumberFromString, actual "a" + */ + ``` + + **After** + + ```ts + import { Schema } from "effect" + + Schema.decodeUnknownSync(Schema.NumberFromString)("a") + /* + throws: + ParseError: NumberFromString + └─ Transformation process failure + └─ Unable to decode "a" into a number + */ + ``` + +- [#4273](https://github.com/Effect-TS/effect/pull/4273) [`a8b0ddb`](https://github.com/Effect-TS/effect/commit/a8b0ddb84710054799fc8f57485b95d00093ada1) Thanks @gcanti! - Schema: Extend Support for Array filters, closes #4269. + + Added support for `minItems`, `maxItems`, and `itemsCount` to all schemas where `A` extends `ReadonlyArray`, including `NonEmptyArray`. + + **Example** + + ```ts + import { Schema } from "effect" + + // Previously, this would have caused an error + const schema = Schema.NonEmptyArray(Schema.String).pipe(Schema.maxItems(2)) + ``` + +- [#4257](https://github.com/Effect-TS/effect/pull/4257) [`8db239b`](https://github.com/Effect-TS/effect/commit/8db239b9c869a3707f6566b9d9dbdf53c4df03fc) Thanks @gcanti! - Schema: Correct `BigInt` and `BigIntFromNumber` identifier annotations to follow naming conventions + +- [#4276](https://github.com/Effect-TS/effect/pull/4276) [`84a0911`](https://github.com/Effect-TS/effect/commit/84a091181634c3a022c94234cec7764a3aeef1be) Thanks @tim-smart! - fix formatting of time zone offsets that round to 60 minutes + +- [#4276](https://github.com/Effect-TS/effect/pull/4276) [`84a0911`](https://github.com/Effect-TS/effect/commit/84a091181634c3a022c94234cec7764a3aeef1be) Thanks @tim-smart! - ensure DateTimeZonedFromSelf arbitrary generates in the range supported by the time zone database + +- [#4267](https://github.com/Effect-TS/effect/pull/4267) [`3179a9f`](https://github.com/Effect-TS/effect/commit/3179a9f65d23369a6a9a1f80f7750566dd28df22) Thanks @tim-smart! - ensure DateTime.Zoned produces valid dates + +- [#4264](https://github.com/Effect-TS/effect/pull/4264) [`6cb9b76`](https://github.com/Effect-TS/effect/commit/6cb9b766396d0b2ed995cf26957359713efd202e) Thanks @gcanti! - Relocate the `Issue` definition from `platform/HttpApiError` to `Schema` (renamed as `ArrayFormatterIssue`). + +- [#4266](https://github.com/Effect-TS/effect/pull/4266) [`1fcbe55`](https://github.com/Effect-TS/effect/commit/1fcbe55345042d8468f6a98c84081bd00b6bcf5a) Thanks @gcanti! - Schema: Replace the `TimeZoneFromSelf` interface with a class definition and fix the arbitraries for `DateTimeUtcFromSelf` and `DateTimeZonedFromSelf` (`fc.date({ noInvalidDate: true })`). + +- [#4279](https://github.com/Effect-TS/effect/pull/4279) [`d9a63d9`](https://github.com/Effect-TS/effect/commit/d9a63d9d385653865954cac895065360d54cc56b) Thanks @tim-smart! - improve performance of Effect.forkIn + +## 3.12.4 + +### Patch Changes + +- [#4231](https://github.com/Effect-TS/effect/pull/4231) [`5b50ea4`](https://github.com/Effect-TS/effect/commit/5b50ea4a10cf9acd51f9624b2474d9d5ded74019) Thanks @KhraksMamtsov! - fix `Layer.retry` and `MetricPolling.retry` signatures + +- [#4253](https://github.com/Effect-TS/effect/pull/4253) [`c170a68`](https://github.com/Effect-TS/effect/commit/c170a68b6266100774461fcd6c0e0fabb60112f2) Thanks @sukovanej! - Use non-enumerable properties for mutable fields of `DateTime` objects. + +- [#4255](https://github.com/Effect-TS/effect/pull/4255) [`a66c2eb`](https://github.com/Effect-TS/effect/commit/a66c2eb473245092cd41f04c2eb2b7b02cf53718) Thanks @sukovanej! - Improve DateTime type preservation + +## 3.12.3 + +### Patch Changes + +- [#4244](https://github.com/Effect-TS/effect/pull/4244) [`d7dac48`](https://github.com/Effect-TS/effect/commit/d7dac48a477cdfeec509dbe9f33fce6a1b02b63d) Thanks @gcanti! - Improve pattern handling by merging multiple patterns into a union, closes #4243. + + Previously, the algorithm always prioritized the first pattern when multiple patterns were encountered. + + This fix introduces a merging strategy that combines patterns into a union (e.g., `(?:${pattern1})|(?:${pattern2})`). By doing so, all patterns have an equal chance to generate values when using `FastCheck.stringMatching`. + + **Example** + + ```ts + import { Arbitrary, FastCheck, Schema } from "effect" + + // /^[^A-Z]*$/ (given by Lowercase) + /^0x[0-9a-f]{40}$/ + const schema = Schema.Lowercase.pipe(Schema.pattern(/^0x[0-9a-f]{40}$/)) + + const arb = Arbitrary.make(schema) + + // Before this fix, the first pattern would always dominate, + // making it impossible to generate values + const sample = FastCheck.sample(arb, { numRuns: 100 }) + + console.log(sample) + ``` + +- [#4252](https://github.com/Effect-TS/effect/pull/4252) [`1d7fd2b`](https://github.com/Effect-TS/effect/commit/1d7fd2b7ee8eeecc912d27adf76ed897db236dc5) Thanks @gcanti! - Fix: Correct `Arbitrary.make` to support nested `TemplateLiteral`s. + + Previously, `Arbitrary.make` did not properly handle nested `TemplateLiteral` schemas, resulting in incorrect or empty outputs. This fix ensures that nested template literals are processed correctly, producing valid arbitrary values. + + **Before** + + ```ts + import { Arbitrary, FastCheck, Schema as S } from "effect" + + const schema = S.TemplateLiteral( + "<", + S.TemplateLiteral("h", S.Literal(1, 2)), + ">" + ) + + const arb = Arbitrary.make(schema) + + console.log(FastCheck.sample(arb, { numRuns: 10 })) + /* + Output: + [ + '<>', '<>', '<>', + '<>', '<>', '<>', + '<>', '<>', '<>', + '<>' + ] + */ + ``` + + **After** + + ```ts + import { Arbitrary, FastCheck, Schema as S } from "effect" + + const schema = S.TemplateLiteral( + "<", + S.TemplateLiteral("h", S.Literal(1, 2)), + ">" + ) + + const arb = Arbitrary.make(schema) + + console.log(FastCheck.sample(arb, { numRuns: 10 })) + /* + Output: + [ + '

', '

', + '

', '

', + '

', '

', + '

', '

', + '

', '

' + ] + */ + ``` + +- [#4252](https://github.com/Effect-TS/effect/pull/4252) [`1d7fd2b`](https://github.com/Effect-TS/effect/commit/1d7fd2b7ee8eeecc912d27adf76ed897db236dc5) Thanks @gcanti! - Fix: Allow `Schema.TemplateLiteral` to handle strings with linebreaks, closes #4251. + + **Before** + + ```ts + import { Schema } from "effect" + + const schema = Schema.TemplateLiteral("a: ", Schema.String) + + console.log(Schema.decodeSync(schema)("a: b \n c")) + // throws: ParseError: Expected `a: ${string}`, actual "a: b \n c" + ``` + + **After** + + ```ts + import { Schema } from "effect" + + const schema = Schema.TemplateLiteral("a: ", Schema.String) + + console.log(Schema.decodeSync(schema)("a: b \n c")) + /* + Output: + a: b + c + */ + ``` + +## 3.12.2 + +### Patch Changes + +- [#4220](https://github.com/Effect-TS/effect/pull/4220) [`734af82`](https://github.com/Effect-TS/effect/commit/734af82138e78b9c57a8355b1c6b80e80d38b222) Thanks @KhraksMamtsov! - fix inference for contravariant type-parameters + +- [#4212](https://github.com/Effect-TS/effect/pull/4212) [`b63c780`](https://github.com/Effect-TS/effect/commit/b63c78010893101520448ddda7019c487cf7eedd) Thanks @KhraksMamtsov! - Refine `Effect.validateAll` return type to use `NonEmptyArray` for errors. + + This refinement is possible because `Effect.validateAll` guarantees that when the input iterable is non-empty, any validation failure will produce at least one error. In such cases, the errors are inherently non-empty, making it safe and accurate to represent them using a `NonEmptyArray` type. This change aligns the return type with the function's actual behavior, improving type safety and making the API more predictable for developers. + +- [#4219](https://github.com/Effect-TS/effect/pull/4219) [`c640d77`](https://github.com/Effect-TS/effect/commit/c640d77b33ad417876f4e8ffe8574ee6cbe5607f) Thanks @whoisandy! - fix: ManagedRuntime.Context to work when Context is of type never + +- [#4236](https://github.com/Effect-TS/effect/pull/4236) [`0def088`](https://github.com/Effect-TS/effect/commit/0def0887cfdb6755729a64dfd52b3b9f46b0576c) Thanks @tim-smart! - fix color option for Logger.prettyLogger + +## 3.12.1 + +### Patch Changes + +- [#4194](https://github.com/Effect-TS/effect/pull/4194) [`302b57d`](https://github.com/Effect-TS/effect/commit/302b57d2cbf9b9ccc17450945aeebfb33cfe8d43) Thanks @KhraksMamtsov! - take concurrentFinalizers option in account in `Effect.all` combinator + +- [#4202](https://github.com/Effect-TS/effect/pull/4202) [`0988083`](https://github.com/Effect-TS/effect/commit/0988083d4594938590df5a287e5b27d38526dd07) Thanks @mikearnaldi! - Remove internal EffectError make sure errors are raised with Effect.fail in Effect.try + +- [#4185](https://github.com/Effect-TS/effect/pull/4185) [`8b46be6`](https://github.com/Effect-TS/effect/commit/8b46be6a3b8160362ab5ea9171c5e6932505125c) Thanks @jessekelly881! - fixed incorrect type declaration in LibsqlClient.layer + +- [#4189](https://github.com/Effect-TS/effect/pull/4189) [`bfe8027`](https://github.com/Effect-TS/effect/commit/bfe802734b450a4b4ee069d1125dd37995db2bff) Thanks @tim-smart! - ensure Effect.timeoutTo sleep is interrupted + +- [#4190](https://github.com/Effect-TS/effect/pull/4190) [`16dd657`](https://github.com/Effect-TS/effect/commit/16dd657033d8afac2ffea567b3c8bb27c9b249b6) Thanks @IMax153! - extend `IterableIterator` instead of `Generator` in `SingleShotGen` + +- [#4196](https://github.com/Effect-TS/effect/pull/4196) [`39db211`](https://github.com/Effect-TS/effect/commit/39db211414e90c8db8fdad7dc8ce5b4661bcfaef) Thanks @mikearnaldi! - Avoid putting symbols in global to fix incompatibility with Temporal Sandbox. + + After speaking with James Watkins-Harvey we realized current Effect escapes the Temporal Worker sandbox that doesn't look for symbols when restoring global context in the isolate they create leading to memory leaks. + +## 3.12.0 + +### Minor Changes + +- [#4068](https://github.com/Effect-TS/effect/pull/4068) [`abb22a4`](https://github.com/Effect-TS/effect/commit/abb22a429b9c52c31e84856294f175d2064a9b4d) Thanks @titouancreach! - Added encodeUriComponent/decodeUriComponent for both Encoding and Schema + +- [#4068](https://github.com/Effect-TS/effect/pull/4068) [`f369a89`](https://github.com/Effect-TS/effect/commit/f369a89e98bc682969803b9304adaf4557bb36c2) Thanks @vinassefranche! - Add Runtime.Runtime.Context type extractor + +- [#4068](https://github.com/Effect-TS/effect/pull/4068) [`642376c`](https://github.com/Effect-TS/effect/commit/642376c63fd7d78754db991631a4d50a5dc79aa3) Thanks @tim-smart! - add non-traced overload to Effect.fn + +- [#4068](https://github.com/Effect-TS/effect/pull/4068) [`3d2b7a7`](https://github.com/Effect-TS/effect/commit/3d2b7a7e942a7157afae5b1cdbc6f3fef116428e) Thanks @mikearnaldi! - Update fast-check to latest version + +- [#4068](https://github.com/Effect-TS/effect/pull/4068) [`73f9c6f`](https://github.com/Effect-TS/effect/commit/73f9c6f2ff091512cf904cc54ab59965b86e87c8) Thanks @wewelll! - add DateTimeUtcFromDate schema + +- [#4068](https://github.com/Effect-TS/effect/pull/4068) [`17cb451`](https://github.com/Effect-TS/effect/commit/17cb4514590e8a86263f7aed009f24da8a237342) Thanks @fubhy! - Added support for `second` granularity to `Cron`. + +- [#4068](https://github.com/Effect-TS/effect/pull/4068) [`d801820`](https://github.com/Effect-TS/effect/commit/d80182060c2ee945d7e0e4728812abf9465a0d6a) Thanks @fubhy! - Added `Cron.unsafeParse` and allow passing the `Cron.parse` time zone parameter as `string`. + +- [#4068](https://github.com/Effect-TS/effect/pull/4068) [`e1eeb2d`](https://github.com/Effect-TS/effect/commit/e1eeb2d7064b3870041dab142f3057970699bbf1) Thanks @mikearnaldi! - add Effect.fnUntraced - an untraced version of Effect.fn + +- [#4068](https://github.com/Effect-TS/effect/pull/4068) [`c11f3a6`](https://github.com/Effect-TS/effect/commit/c11f3a60a05c3b5fc8e7ce90136728154dc505b0) Thanks @QuentinJanuel! - Add Context.mergeAll to combine multiple Contexts into one. + +- [#4068](https://github.com/Effect-TS/effect/pull/4068) [`618f7e0`](https://github.com/Effect-TS/effect/commit/618f7e092a1011e5090dca1e69b5e9285689654b) Thanks @tim-smart! - add span annotation to disable propagation to the tracer + +- [#4068](https://github.com/Effect-TS/effect/pull/4068) [`c0ba834`](https://github.com/Effect-TS/effect/commit/c0ba834d1995cf5a8b250e4780fd43f3e3881151) Thanks @titouancreach! - Add Schema.headNonEmpty for Schema.NonEmptyArray + +### Patch Changes + +- [#4068](https://github.com/Effect-TS/effect/pull/4068) [`e1eeb2d`](https://github.com/Effect-TS/effect/commit/e1eeb2d7064b3870041dab142f3057970699bbf1) Thanks @mikearnaldi! - Carry both call-site and definition site in Effect.fn, auto-trace to anon + +## 3.11.10 + +### Patch Changes + +- [#4176](https://github.com/Effect-TS/effect/pull/4176) [`39457d4`](https://github.com/Effect-TS/effect/commit/39457d4897d9bc7df8af5c05d352866bbeae82eb) Thanks @mikearnaldi! - Fix Stream.scoped example + +- [#4181](https://github.com/Effect-TS/effect/pull/4181) [`a475cc2`](https://github.com/Effect-TS/effect/commit/a475cc25fd7c9f26b27a8e98f8fbe43cc9e6ee3e) Thanks @gcanti! - Schema: Fix `withDecodingDefault` implementation to align with its signature (now removes `undefined` from the AST). + + Additionally, a new constraint has been added to the signature to prevent calling `withDecodingDefault` after `withConstructorDefault`, which previously led to the following issue: + + ```ts + import { Schema } from "effect" + + const schema = Schema.Struct({ + a: Schema.optional(Schema.String).pipe( + Schema.withConstructorDefault(() => undefined), // this is invalidated by the following call to `withDecodingDefault` + Schema.withDecodingDefault(() => "") + ) + }) + ``` + +- [#4175](https://github.com/Effect-TS/effect/pull/4175) [`199214e`](https://github.com/Effect-TS/effect/commit/199214e21c616d8a0ccd7ed5f92e944e6c580193) Thanks @gcanti! - Schema: refactor annotations: + - Export internal `Uint8` schema + - Export internal `NonNegativeInt` schema + - Remove title annotations that are identical to identifiers + - Avoid setting a title annotation when applying branding + - Add more title annotations to refinements + - Improve `toString` output and provide more precise error messages for refinements: + + Before + + ```ts + import { Schema } from "effect" + + const schema = Schema.Number.pipe( + Schema.int({ identifier: "MyInt" }), + Schema.positive() + ) + + console.log(String(schema)) + // Output: a positive number + + Schema.decodeUnknownSync(schema)(1.1) + /* + throws: + ParseError: a positive number + └─ From side refinement failure + └─ MyInt + └─ Predicate refinement failure + └─ Expected MyInt, actual 1.1 + */ + ``` + + After + - `toString` now combines all refinements with `" & "` instead of showing only the last one. + - The last message (`"Expected ..."`) now uses the extended description to make the error message clearer. + + ```ts + import { Schema } from "effect" + + const schema = Schema.Number.pipe( + Schema.int({ identifier: "MyInt" }), + Schema.positive() + ) + + console.log(String(schema)) + // Output: MyInt & positive // <= all the refinements + + Schema.decodeUnknownSync(schema)(1.1) + /* + throws: + ParseError: MyInt & positive + └─ From side refinement failure + └─ MyInt + └─ Predicate refinement failure + └─ Expected an integer, actual 1.1 // <= extended description + */ + ``` + +- [#4182](https://github.com/Effect-TS/effect/pull/4182) [`b3c160d`](https://github.com/Effect-TS/effect/commit/b3c160d7a1fdfc2d3fb2440530f1ab80efc65133) Thanks @mikearnaldi! - Replace absolute imports with relative ones + +## 3.11.9 + +### Patch Changes + +- [#4113](https://github.com/Effect-TS/effect/pull/4113) [`1c08a0b`](https://github.com/Effect-TS/effect/commit/1c08a0b8505badcffb4d9cade5a746ea90c9557e) Thanks @thewilkybarkid! - Schema: Support template literals in `Schema.Config`. + + **Example** + + ```ts + import { Schema } from "effect" + + // const config: Config<`a${string}`> + const config = Schema.Config( + "A", + Schema.TemplateLiteral(Schema.Literal("a"), Schema.String) + ) + ``` + +- [#4174](https://github.com/Effect-TS/effect/pull/4174) [`1ce703b`](https://github.com/Effect-TS/effect/commit/1ce703b041bbd7560c5c437c9b9be48f027937fd) Thanks @gcanti! - Schema: Add support for `TemplateLiteral` parameters in `TemplateLiteral`, closes #4166. + + This update also adds support for `TemplateLiteral` and `TemplateLiteralParser` parameters in `TemplateLiteralParser`. + + Before + + ```ts + import { Schema } from "effect" + + const schema = Schema.TemplateLiteralParser( + "<", + Schema.TemplateLiteralParser("h", Schema.Literal(1, 2)), + ">" + ) + /* + throws: + Error: Unsupported template literal span + schema (TemplateLiteral): `h${"1" | "2"}` + */ + ``` + + After + + ```ts + import { Schema } from "effect" + + // Schema"], "

" | "

", never> + const schema = Schema.TemplateLiteralParser( + "<", + Schema.TemplateLiteralParser("h", Schema.Literal(1, 2)), + ">" + ) + + console.log(Schema.decodeUnknownSync(schema)("

")) + // Output: [ '<', [ 'h', 1 ], '>' ] + ``` + +- [#4174](https://github.com/Effect-TS/effect/pull/4174) [`1ce703b`](https://github.com/Effect-TS/effect/commit/1ce703b041bbd7560c5c437c9b9be48f027937fd) Thanks @gcanti! - Schema: Fix bug in `TemplateLiteralParser` where unions of numeric literals were not coerced correctly. + + Before + + ```ts + import { Schema } from "effect" + + const schema = Schema.TemplateLiteralParser("a", Schema.Literal(1, 2)) + + console.log(Schema.decodeUnknownSync(schema)("a1")) + /* + throws: + ParseError: (`a${"1" | "2"}` <-> readonly ["a", 1 | 2]) + └─ Type side transformation failure + └─ readonly ["a", 1 | 2] + └─ [1] + └─ 1 | 2 + ├─ Expected 1, actual "1" + └─ Expected 2, actual "1" + */ + ``` + + After + + ```ts + import { Schema } from "effect" + + const schema = Schema.TemplateLiteralParser("a", Schema.Literal(1, 2)) + + console.log(Schema.decodeUnknownSync(schema)("a1")) + // Output: [ 'a', 1 ] + + console.log(Schema.decodeUnknownSync(schema)("a2")) + // Output: [ 'a', 2 ] + + console.log(Schema.decodeUnknownSync(schema)("a3")) + /* + throws: + ParseError: (`a${"1" | "2"}` <-> readonly ["a", 1 | 2]) + └─ Encoded side transformation failure + └─ Expected `a${"1" | "2"}`, actual "a3" + */ + ``` + +## 3.11.8 + +### Patch Changes + +- [#4150](https://github.com/Effect-TS/effect/pull/4150) [`1a6b52d`](https://github.com/Effect-TS/effect/commit/1a6b52dcf020d36e38a7bc90b648152cf5a8ccba) Thanks @gcanti! - Arbitrary: optimize date-based refinements + +## 3.11.7 + +### Patch Changes + +- [#4137](https://github.com/Effect-TS/effect/pull/4137) [`2408616`](https://github.com/Effect-TS/effect/commit/24086163b60b09cc6d0885bd565ef080dcbe866b) Thanks @gcanti! - Arbitrary: fix bug where refinements in declarations raised an incorrect missing annotation error, closes #4136 + +- [#4138](https://github.com/Effect-TS/effect/pull/4138) [`cec0b4d`](https://github.com/Effect-TS/effect/commit/cec0b4d152ef660be2ccdb0927255f2471436e6e) Thanks @gcanti! - JSONSchema: ignore never members in unions. + + Before + + ```ts + import { JSONSchema, Schema } from "effect" + + const schema = Schema.Union(Schema.String, Schema.Never) + + console.log(JSON.stringify(JSONSchema.make(schema), null, 2)) + /* + { + "$schema": "http://json-schema.org/draft-07/schema#", + "anyOf": [ + { + "type": "string" + }, + { + "$id": "/schemas/never", + "not": {}, + "title": "never" + } + ] + } + */ + ``` + + After + + ```ts + import { JSONSchema, Schema } from "effect" + + const schema = Schema.Union(Schema.String, Schema.Never) + + console.log(JSON.stringify(JSONSchema.make(schema), null, 2)) + /* + { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "string" + } + */ + ``` + +- [#4138](https://github.com/Effect-TS/effect/pull/4138) [`cec0b4d`](https://github.com/Effect-TS/effect/commit/cec0b4d152ef660be2ccdb0927255f2471436e6e) Thanks @gcanti! - JSONSchema: handle the `nullable` keyword for OpenAPI target, closes #4075. + + Before + + ```ts + import { OpenApiJsonSchema } from "@effect/platform" + import { Schema } from "effect" + + const schema = Schema.NullOr(Schema.String) + + console.log(JSON.stringify(OpenApiJsonSchema.make(schema), null, 2)) + /* + { + "anyOf": [ + { + "type": "string" + }, + { + "enum": [ + null + ] + } + ] + } + */ + ``` + + After + + ```ts + import { OpenApiJsonSchema } from "@effect/platform" + import { Schema } from "effect" + + const schema = Schema.NullOr(Schema.String) + + console.log(JSON.stringify(OpenApiJsonSchema.make(schema), null, 2)) + /* + { + "type": "string", + "nullable": true + } + */ + ``` + +- [#4128](https://github.com/Effect-TS/effect/pull/4128) [`8d978c5`](https://github.com/Effect-TS/effect/commit/8d978c53f6fcc98d9d645ecba3e4b55d4297dd36) Thanks @gcanti! - JSONSchema: add `type` for homogeneous enum schemas, closes #4127 + + Before + + ```ts + import { JSONSchema, Schema } from "effect" + + const schema = Schema.Literal("a", "b") + + console.log(JSON.stringify(JSONSchema.make(schema), null, 2)) + /* + { + "$schema": "http://json-schema.org/draft-07/schema#", + "enum": [ + "a", + "b" + ] + } + */ + ``` + + After + + ```ts + import { JSONSchema, Schema } from "effect" + + const schema = Schema.Literal("a", "b") + + console.log(JSON.stringify(JSONSchema.make(schema), null, 2)) + /* + { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "string", + "enum": [ + "a", + "b" + ] + } + */ + ``` + +- [#4138](https://github.com/Effect-TS/effect/pull/4138) [`cec0b4d`](https://github.com/Effect-TS/effect/commit/cec0b4d152ef660be2ccdb0927255f2471436e6e) Thanks @gcanti! - JSONSchema: use `{ "type": "null" }` to represent the `null` literal + + Before + + ```ts + import { JSONSchema, Schema } from "effect" + + const schema = Schema.NullOr(Schema.String) + + console.log(JSON.stringify(JSONSchema.make(schema), null, 2)) + /* + { + "$schema": "http://json-schema.org/draft-07/schema#", + "anyOf": [ + { + "type": "string" + }, + { + "enum": [ + null + ] + } + ] + } + */ + ``` + + After + + ```ts + import { JSONSchema, Schema } from "effect" + + const schema = Schema.NullOr(Schema.String) + + console.log(JSON.stringify(JSONSchema.make(schema), null, 2)) + /* + { + "$schema": "http://json-schema.org/draft-07/schema#", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + */ + ``` + +- [#4138](https://github.com/Effect-TS/effect/pull/4138) [`cec0b4d`](https://github.com/Effect-TS/effect/commit/cec0b4d152ef660be2ccdb0927255f2471436e6e) Thanks @gcanti! - JSONSchema: handle empty native enums. + + Before + + ```ts + import { JSONSchema, Schema } from "effect" + + enum Empty {} + + const schema = Schema.Enums(Empty) + + console.log(JSON.stringify(JSONSchema.make(schema), null, 2)) + /* + { + "$schema": "http://json-schema.org/draft-07/schema#", + "$comment": "/schemas/enums", + "anyOf": [] // <= invalid schema! + } + */ + ``` + + After + + ```ts + import { JSONSchema, Schema } from "effect" + + enum Empty {} + + const schema = Schema.Enums(Empty) + + console.log(JSON.stringify(JSONSchema.make(schema), null, 2)) + /* + { + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "/schemas/never", + "not": {} + } + */ + ``` + +## 3.11.6 + +### Patch Changes + +- [#4118](https://github.com/Effect-TS/effect/pull/4118) [`662d1ce`](https://github.com/Effect-TS/effect/commit/662d1ce6fb7da384a95888d5b2bb5605bdf3208d) Thanks @gcanti! - Allow the transformation created by the Class API to be annotated on all its components: the type side, the transformation itself, and the encoded side. + + **Example** + + ```ts + import { Schema, SchemaAST } from "effect" + + class A extends Schema.Class("A")( + { + a: Schema.NonEmptyString + }, + [ + { identifier: "TypeID" }, // annotations for the type side + { identifier: "TransformationID" }, // annotations for the the transformation itself + { identifier: "EncodedID" } // annotations for the the encoded side + ] + ) {} + + console.log(SchemaAST.getIdentifierAnnotation(A.ast.to)) // Some("TypeID") + console.log(SchemaAST.getIdentifierAnnotation(A.ast)) // Some("TransformationID") + console.log(SchemaAST.getIdentifierAnnotation(A.ast.from)) // Some("EncodedID") + + A.make({ a: "" }) + /* + ParseError: TypeID + └─ ["a"] + └─ NonEmptyString + └─ Predicate refinement failure + └─ Expected NonEmptyString, actual "" + */ + + Schema.encodeSync(A)({ a: "" }) + /* + ParseError: TransformationID + └─ Type side transformation failure + └─ TypeID + └─ ["a"] + └─ NonEmptyString + └─ Predicate refinement failure + └─ Expected NonEmptyString, actual "" + */ + ``` + +- [#4126](https://github.com/Effect-TS/effect/pull/4126) [`31c62d8`](https://github.com/Effect-TS/effect/commit/31c62d83cbdcf9850a8b5331faa239601c60f78a) Thanks @gcanti! - Rewrite the Arbitrary compiler from scratch, closes #2312 + +## 3.11.5 + +### Patch Changes + +- [#4019](https://github.com/Effect-TS/effect/pull/4019) [`9f5a6f7`](https://github.com/Effect-TS/effect/commit/9f5a6f701bf7ba31adccd1f1bcfa8ab5614c9be8) Thanks @gcanti! - Add missing `jsonSchema` annotations to the following filters: + - `lowercased` + - `capitalized` + - `uncapitalized` + - `uppercased` + + Before + + ```ts + import { JSONSchema, Schema } from "effect" + + const schema = Schema.Struct({ + a: Schema.Uppercased + }) + + console.log(JSON.stringify(JSONSchema.make(schema), null, 2)) + /* + throws: + Error: Missing annotation + details: Generating a JSON Schema for this schema requires a "jsonSchema" annotation + schema (Refinement): Uppercased + */ + ``` + + After + + ```ts + import { JSONSchema, Schema } from "effect" + + const schema = Schema.Uppercased + + console.log(JSON.stringify(JSONSchema.make(schema), null, 2)) + /* + Output: + { + "$ref": "#/$defs/Uppercased", + "$schema": "http://json-schema.org/draft-07/schema#", + "$defs": { + "Uppercased": { + "type": "string", + "description": "an uppercase string", + "title": "Uppercased", + "pattern": "^[^a-z]*$" + } + } + } + */ + ``` + +- [#4111](https://github.com/Effect-TS/effect/pull/4111) [`22905cf`](https://github.com/Effect-TS/effect/commit/22905cf5addfb1ff3d2a6135c52036be958ae911) Thanks @gcanti! - JSONSchema: merge refinement fragments instead of just overwriting them. + + Before + + ```ts + import { JSONSchema, Schema } from "effect" + + export const schema = Schema.String.pipe( + Schema.startsWith("a"), // <= overwritten! + Schema.endsWith("c") + ) + + console.log(JSON.stringify(JSONSchema.make(schema), null, 2)) + /* + { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "string", + "description": "a string ending with \"c\"", + "pattern": "^.*c$" // <= overwritten! + } + */ + ``` + + After + + ```ts + import { JSONSchema, Schema } from "effect" + + export const schema = Schema.String.pipe( + Schema.startsWith("a"), // <= preserved! + Schema.endsWith("c") + ) + + console.log(JSON.stringify(JSONSchema.make(schema), null, 2)) + /* + { + "type": "string", + "description": "a string ending with \"c\"", + "pattern": "^.*c$", + "allOf": [ + { + "pattern": "^a" // <= preserved! + } + ], + "$schema": "http://json-schema.org/draft-07/schema#" + } + */ + ``` + +- [#4019](https://github.com/Effect-TS/effect/pull/4019) [`9f5a6f7`](https://github.com/Effect-TS/effect/commit/9f5a6f701bf7ba31adccd1f1bcfa8ab5614c9be8) Thanks @gcanti! - JSONSchema: Correct the output order when generating a JSON Schema from a Union that includes literals and primitive schemas. + + Before + + ```ts + import { JSONSchema, Schema } from "effect" + + const schema = Schema.Union(Schema.Literal(1, 2), Schema.String) + + console.log(JSON.stringify(JSONSchema.make(schema), null, 2)) + /* + { + "$schema": "http://json-schema.org/draft-07/schema#", + "anyOf": [ + { + "type": "string" + }, + { + "enum": [ + 1, + 2 + ] + } + ] + } + */ + ``` + + After + + ```ts + import { JSONSchema, Schema } from "effect" + + const schema = Schema.Union(Schema.Literal(1, 2), Schema.String) + + console.log(JSON.stringify(JSONSchema.make(schema), null, 2)) + /* + { + "$schema": "http://json-schema.org/draft-07/schema#", + "anyOf": [ + { + "enum": [ + 1, + 2 + ] + }, + { + "type": "string" + } + ] + } + */ + ``` + +- [#4107](https://github.com/Effect-TS/effect/pull/4107) [`1e59e4f`](https://github.com/Effect-TS/effect/commit/1e59e4fd778da18296812a2a32f36ca8ae50f60d) Thanks @tim-smart! - remove FnEffect type to improve return type of Effect.fn + +- [#4108](https://github.com/Effect-TS/effect/pull/4108) [`8d914e5`](https://github.com/Effect-TS/effect/commit/8d914e504e7a22d0ea628e8af265ee450ff9530f) Thanks @gcanti! - JSONSchema: represent `never` as `{"not":{}}` + + Before + + ```ts + import { JSONSchema, Schema } from "effect" + + const schema = Schema.Never + + console.log(JSON.stringify(JSONSchema.make(schema), null, 2)) + /* + throws: + Error: Missing annotation + details: Generating a JSON Schema for this schema requires a "jsonSchema" annotation + schema (NeverKeyword): never + */ + ``` + + After + + ```ts + import { JSONSchema, Schema } from "effect" + + const schema = Schema.Never + + console.log(JSON.stringify(JSONSchema.make(schema), null, 2)) + /* + { + "$id": "/schemas/never", + "not": {}, + "title": "never", + "$schema": "http://json-schema.org/draft-07/schema#" + } + */ + ``` + +- [#4115](https://github.com/Effect-TS/effect/pull/4115) [`03bb00f`](https://github.com/Effect-TS/effect/commit/03bb00faa74f9e168a54a8cc0828a664fbb1ab05) Thanks @tim-smart! - avoid using non-namespaced "async" internally + +- [#4019](https://github.com/Effect-TS/effect/pull/4019) [`9f5a6f7`](https://github.com/Effect-TS/effect/commit/9f5a6f701bf7ba31adccd1f1bcfa8ab5614c9be8) Thanks @gcanti! - JSONSchema: fix special case in `parseJson` handling to target the "to" side of the transformation only at the top level. + + Before + + ```ts + import { JSONSchema, Schema } from "effect" + + const schema = Schema.parseJson( + Schema.Struct({ + a: Schema.parseJson( + Schema.Struct({ + b: Schema.String + }) + ) + }) + ) + + console.log(JSON.stringify(JSONSchema.make(schema), null, 2)) + /* + { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "required": [ + "a" + ], + "properties": { + "a": { + "type": "object", + "required": [ + "b" + ], + "properties": { + "b": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + */ + ``` + + After + + ```ts + import { JSONSchema, Schema } from "effect" + + const schema = Schema.parseJson( + Schema.Struct({ + a: Schema.parseJson( + Schema.Struct({ + b: Schema.String + }) + ) + }) + ) + + console.log(JSON.stringify(JSONSchema.make(schema), null, 2)) + /* + { + "type": "object", + "required": [ + "a" + ], + "properties": { + "a": { + "type": "string", + "contentMediaType": "application/json" + } + }, + "additionalProperties": false, + "$schema": "http://json-schema.org/draft-07/schema#" + } + */ + ``` + +- [#4101](https://github.com/Effect-TS/effect/pull/4101) [`14e1149`](https://github.com/Effect-TS/effect/commit/14e1149f1af5a022f06eb8c2e4ba9fec17fe7426) Thanks @gcanti! - Schema: align the `make` constructor of structs with the behavior of the Class API constructors when all fields have a default. + + Before + + ```ts + import { Schema } from "effect" + + const schema = Schema.Struct({ + a: Schema.propertySignature(Schema.Number).pipe( + Schema.withConstructorDefault(() => 0) + ) + }) + + // TypeScript error: Expected 1-2 arguments, but got 0.ts(2554) + console.log(schema.make()) + ``` + + After + + ```ts + import { Schema } from "effect" + + const schema = Schema.Struct({ + a: Schema.propertySignature(Schema.Number).pipe( + Schema.withConstructorDefault(() => 0) + ) + }) + + console.log(schema.make()) + // Output: { a: 0 } + ``` + +- [#4019](https://github.com/Effect-TS/effect/pull/4019) [`9f5a6f7`](https://github.com/Effect-TS/effect/commit/9f5a6f701bf7ba31adccd1f1bcfa8ab5614c9be8) Thanks @gcanti! - JSONSchema: Fix issue where `identifier` is ignored when a refinement is applied to a schema, closes #4012 + + Before + + ```ts + import { JSONSchema, Schema } from "effect" + + const schema = Schema.NonEmptyString + + console.log(JSON.stringify(JSONSchema.make(schema), null, 2)) + /* + { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "string", + "description": "a non empty string", + "title": "NonEmptyString", + "minLength": 1 + } + */ + ``` + + After + + ```ts + import { JSONSchema, Schema } from "effect" + + const schema = Schema.NonEmptyString + + console.log(JSON.stringify(JSONSchema.make(schema), null, 2)) + /* + { + "$schema": "http://json-schema.org/draft-07/schema#", + "$ref": "#/$defs/NonEmptyString", + "$defs": { + "NonEmptyString": { + "type": "string", + "description": "a non empty string", + "title": "NonEmptyString", + "minLength": 1 + } + } + } + */ + ``` + +- [#4019](https://github.com/Effect-TS/effect/pull/4019) [`9f5a6f7`](https://github.com/Effect-TS/effect/commit/9f5a6f701bf7ba31adccd1f1bcfa8ab5614c9be8) Thanks @gcanti! - JSONSchema: Use identifier with Class APIs to create a `$ref` instead of inlining the schema. + + Before + + ```ts + import { JSONSchema, Schema } from "effect" + + class A extends Schema.Class("A")({ + a: Schema.String + }) {} + + console.log(JSON.stringify(JSONSchema.make(A), null, 2)) + /* + { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "required": [ + "a" + ], + "properties": { + "a": { + "type": "string" + } + }, + "additionalProperties": false + } + */ + ``` + + After + + ```ts + import { JSONSchema, Schema } from "effect" + + class A extends Schema.Class("A")({ + a: Schema.String + }) {} + + console.log(JSON.stringify(JSONSchema.make(A), null, 2)) + /* + { + "$ref": "#/$defs/A", + "$schema": "http://json-schema.org/draft-07/schema#", + "$defs": { + "A": { + "type": "object", + "required": [ + "a" + ], + "properties": { + "a": { + "type": "string" + } + }, + "additionalProperties": false + } + } + } + */ + ``` + +## 3.11.4 + +### Patch Changes + +- [#4087](https://github.com/Effect-TS/effect/pull/4087) [`518b258`](https://github.com/Effect-TS/effect/commit/518b258a8a67ecd332a9252c35cc060f8368dee2) Thanks @tim-smart! - remove use of .unsafeAsync in non-suspended contexts + +- [#4010](https://github.com/Effect-TS/effect/pull/4010) [`6e323a3`](https://github.com/Effect-TS/effect/commit/6e323a36faaee46b328c8e3cf60a76b3aff9907f) Thanks @fubhy! - Add support for daylight savings time transitions + +- [#4010](https://github.com/Effect-TS/effect/pull/4010) [`6e323a3`](https://github.com/Effect-TS/effect/commit/6e323a36faaee46b328c8e3cf60a76b3aff9907f) Thanks @fubhy! - Improved efficiency of `Cron.next` lookup + +## 3.11.3 + +### Patch Changes + +- [#4080](https://github.com/Effect-TS/effect/pull/4080) [`90906f7`](https://github.com/Effect-TS/effect/commit/90906f7f154b12c7182e8f39e3c55ef3937db857) Thanks @gcanti! - Fix the `Schema.TemplateLiteral` output type when the arguments include a branded type. + + Before + + ```ts + import { Schema } from "effect" + + const schema = Schema.TemplateLiteral( + "a ", + Schema.String.pipe(Schema.brand("MyBrand")) + ) + + // type Type = `a ${Schema.brand & string}` + // | `a ${Schema.brand & number}` + // | `a ${Schema.brand & bigint}` + // | `a ${Schema.brand<...> & false}` + // | `a ${Schema.brand<...> & true}` + type Type = typeof schema.Type + ``` + + After + + ```ts + import { Schema } from "effect" + + const schema = Schema.TemplateLiteral( + "a ", + Schema.String.pipe(Schema.brand("MyBrand")) + ) + + // type Type = `a ${string & Brand<"MyBrand">}` + type Type = typeof schema.Type + ``` + +- [#4076](https://github.com/Effect-TS/effect/pull/4076) [`3862cd3`](https://github.com/Effect-TS/effect/commit/3862cd3c7f6a542ed65fb81255b3bd696ce2f567) Thanks @gcanti! - Schema: fix bug in `Schema.TemplateLiteralParser` resulting in a runtime error. + + Before + + ```ts + import { Schema } from "effect" + + const schema = Schema.TemplateLiteralParser("a", "b") + // throws TypeError: Cannot read properties of undefined (reading 'replace') + ``` + + After + + ```ts + import { Schema } from "effect" + + const schema = Schema.TemplateLiteralParser("a", "b") + + console.log(Schema.decodeUnknownSync(schema)("ab")) + // Output: [ 'a', 'b' ] + ``` + +- [#4076](https://github.com/Effect-TS/effect/pull/4076) [`3862cd3`](https://github.com/Effect-TS/effect/commit/3862cd3c7f6a542ed65fb81255b3bd696ce2f567) Thanks @gcanti! - SchemaAST: fix `TemplateLiteral` model. + + Added `Literal` and `Union` as valid types. + +- [#4083](https://github.com/Effect-TS/effect/pull/4083) [`343b6aa`](https://github.com/Effect-TS/effect/commit/343b6aa6ac4a74276bfc7c63ccbf4a1d72bc1bed) Thanks @gcanti! - Preserve `MissingMessageAnnotation`s on property signature declarations when another field is a property signature transformation. + + Before + + ```ts + import { Console, Effect, ParseResult, Schema } from "effect" + + const schema = Schema.Struct({ + a: Schema.propertySignature(Schema.String).annotations({ + missingMessage: () => "message1" + }), + b: Schema.propertySignature(Schema.String) + .annotations({ missingMessage: () => "message2" }) + .pipe(Schema.fromKey("c")), // <= transformation + d: Schema.propertySignature(Schema.String).annotations({ + missingMessage: () => "message3" + }) + }) + + Effect.runPromiseExit( + Schema.decodeUnknown(schema, { errors: "all" })({}).pipe( + Effect.tapError((error) => + Console.log(ParseResult.ArrayFormatter.formatErrorSync(error)) + ) + ) + ) + /* + Output: + [ + { _tag: 'Missing', path: [ 'a' ], message: 'is missing' }, // <= wrong + { _tag: 'Missing', path: [ 'c' ], message: 'message2' }, + { _tag: 'Missing', path: [ 'd' ], message: 'is missing' } // <= wrong + ] + */ + ``` + + After + + ```ts + import { Console, Effect, ParseResult, Schema } from "effect" + + const schema = Schema.Struct({ + a: Schema.propertySignature(Schema.String).annotations({ + missingMessage: () => "message1" + }), + b: Schema.propertySignature(Schema.String) + .annotations({ missingMessage: () => "message2" }) + .pipe(Schema.fromKey("c")), // <= transformation + d: Schema.propertySignature(Schema.String).annotations({ + missingMessage: () => "message3" + }) + }) + + Effect.runPromiseExit( + Schema.decodeUnknown(schema, { errors: "all" })({}).pipe( + Effect.tapError((error) => + Console.log(ParseResult.ArrayFormatter.formatErrorSync(error)) + ) + ) + ) + /* + Output: + [ + { _tag: 'Missing', path: [ 'a' ], message: 'message1' }, + { _tag: 'Missing', path: [ 'c' ], message: 'message2' }, + { _tag: 'Missing', path: [ 'd' ], message: 'message3' } + ] + */ + ``` + +- [#4081](https://github.com/Effect-TS/effect/pull/4081) [`afba339`](https://github.com/Effect-TS/effect/commit/afba339adc11dad56b5a3b7ca94487e58f34d613) Thanks @gcanti! - Fix the behavior of `Schema.TemplateLiteralParser` when the arguments include literals other than string literals. + + Before + + ```ts + import { Schema } from "effect" + + const schema = Schema.TemplateLiteralParser(Schema.String, 1) + + console.log(Schema.decodeUnknownSync(schema)("a1")) + /* + throws + ParseError: (`${string}1` <-> readonly [string, 1]) + └─ Type side transformation failure + └─ readonly [string, 1] + └─ [1] + └─ Expected 1, actual "1" + */ + ``` + + After + + ```ts + import { Schema } from "effect" + + const schema = Schema.TemplateLiteralParser(Schema.String, 1) + + console.log(Schema.decodeUnknownSync(schema)("a1")) + // Output: [ 'a', 1 ] + ``` + +## 3.11.2 + +### Patch Changes + +- [#4063](https://github.com/Effect-TS/effect/pull/4063) [`01cee56`](https://github.com/Effect-TS/effect/commit/01cee560b58d94b24cc20e98083251b73e658b41) Thanks @tim-smart! - Micro adjustments + - rename Fiber to MicroFiber + - add Micro.fiberJoin api + - adjust output when inspecting Micro data types + +## 3.11.1 + +### Patch Changes + +- [#4052](https://github.com/Effect-TS/effect/pull/4052) [`dd8a2d8`](https://github.com/Effect-TS/effect/commit/dd8a2d8e80d33b16719fc69361eaedf0b59d4620) Thanks @tim-smart! - ensure pool.get is interrupted on shutdown + +- [#4059](https://github.com/Effect-TS/effect/pull/4059) [`a71bfef`](https://github.com/Effect-TS/effect/commit/a71bfef46f5061bb2502a61a333638a987b62273) Thanks @IMax153! - Ensure that the current time zone context tag type is properly exported + +## 3.11.0 + +### Minor Changes + +- [#3835](https://github.com/Effect-TS/effect/pull/3835) [`147434b`](https://github.com/Effect-TS/effect/commit/147434b03d5e1fd692dd9f126e5ab0910f3b76d3) Thanks @IMax153! - Ensure scopes are preserved by stream / sink / channel operations + + **NOTE**: This change does modify the public signature of several `Stream` / `Sink` / `Channel` methods. Namely, certain run methods that previously removed a `Scope` from the environment will no longer do so. This was a bug with the previous implementation of how scopes were propagated, and is why this change is being made in a minor release. + +- [#3835](https://github.com/Effect-TS/effect/pull/3835) [`6e69493`](https://github.com/Effect-TS/effect/commit/6e694930048bbaf98110f35f41566aeb9752d471) Thanks @tim-smart! - add Context.Reference - a Tag with a default value + +- [#3835](https://github.com/Effect-TS/effect/pull/3835) [`147434b`](https://github.com/Effect-TS/effect/commit/147434b03d5e1fd692dd9f126e5ab0910f3b76d3) Thanks @IMax153! - Add `Effect.scopedWith` to run an effect that depends on a `Scope`, and then closes the `Scope` after the effect has completed + + ```ts + import { Effect, Scope } from "effect" + + const program: Effect.Effect = Effect.scopedWith((scope) => + Effect.acquireRelease(Effect.log("Acquiring..."), () => + Effect.log("Releasing...") + ).pipe(Scope.extend(scope)) + ) + + Effect.runPromise(program) + // Output: + // timestamp=2024-11-26T16:44:54.158Z level=INFO fiber=#0 message=Acquiring... + // timestamp=2024-11-26T16:44:54.165Z level=INFO fiber=#0 message=Releasing... + ``` + +- [#3835](https://github.com/Effect-TS/effect/pull/3835) [`d9fe79b`](https://github.com/Effect-TS/effect/commit/d9fe79bb5a3fe105d8e7a3bc2922a8ad936a5d10) Thanks @tim-smart! - remove Env, EnvRef & FiberFlags from Micro + +- [#3835](https://github.com/Effect-TS/effect/pull/3835) [`251d189`](https://github.com/Effect-TS/effect/commit/251d189420bbba71990574e91098c499065f9a9b) Thanks @KhraksMamtsov! - `Config.url` constructor has been added, which parses a string using `new URL()` + +- [#3835](https://github.com/Effect-TS/effect/pull/3835) [`5a259f3`](https://github.com/Effect-TS/effect/commit/5a259f3711b4369f55d885b568bdb21136155261) Thanks @tim-smart! - use fiber based runtime for Micro module + - Improved performance + - Improved interruption model + - Consistency with the Effect data type + +- [#3835](https://github.com/Effect-TS/effect/pull/3835) [`b4ce4ea`](https://github.com/Effect-TS/effect/commit/b4ce4ea7fd514a7e572f2dcd879c98f334981b0e) Thanks @SandroMaglione! - New methods `extractAll` and `extractSchema` to `UrlParams` (added `Schema.BooleanFromString`). + +- [#3835](https://github.com/Effect-TS/effect/pull/3835) [`15fcc5a`](https://github.com/Effect-TS/effect/commit/15fcc5a0ea4bbf40ab48fa6a04fdda74f76f4c07) Thanks @fubhy! - Integrated `DateTime` with `Cron` to add timezone support for cron expressions. + +- [#3835](https://github.com/Effect-TS/effect/pull/3835) [`9bc9a47`](https://github.com/Effect-TS/effect/commit/9bc9a476800dc645903c888a68bb1d3baa3383c6) Thanks @KhraksMamtsov! - `URL` and `URLFromSelf` schemas have been added + +- [#3835](https://github.com/Effect-TS/effect/pull/3835) [`aadb8a4`](https://github.com/Effect-TS/effect/commit/aadb8a48d2cba197c06ec9996505510e48e4e5cb) Thanks @fubhy! - Added `BigDecimal.toExponential` for scientific notation formatting of `BigDecimal` values. + + The implementation of `BigDecimal.format` now uses scientific notation for values with + at least 16 decimal places or trailing zeroes. Previously, extremely large or small values + could cause `OutOfMemory` errors when formatting. + +- [#3835](https://github.com/Effect-TS/effect/pull/3835) [`1e2747c`](https://github.com/Effect-TS/effect/commit/1e2747c63a4820d1459cbbc88c71212983bd68bd) Thanks @KhraksMamtsov! - - JSONSchema module + - add `format?: string` optional field to `JsonSchema7String` interface + - Schema module + - add custom json schema annotation to `UUID` schema including `format: "uuid"` + - OpenApiJsonSchema module + - add `format?: string` optional field to `String` and ` Numeric` interfaces + +- [#3835](https://github.com/Effect-TS/effect/pull/3835) [`e0b9b09`](https://github.com/Effect-TS/effect/commit/e0b9b09e70c386b2da17d1f0a15b0511861c89e8) Thanks @mikearnaldi! - Implement Effect.fn to define traced functions. + + ```ts + import { Effect } from "effect" + + const logExample = Effect.fn("example")(function* (n: N) { + yield* Effect.annotateCurrentSpan("n", n) + yield* Effect.logInfo(`got: ${n}`) + yield* Effect.fail(new Error()) + }, Effect.delay("1 second")) + + Effect.runFork(logExample(100).pipe(Effect.catchAllCause(Effect.logError))) + ``` + +- [#3835](https://github.com/Effect-TS/effect/pull/3835) [`c36f3b9`](https://github.com/Effect-TS/effect/commit/c36f3b95df5ce9d71b66f22f26ce12eda8d3e848) Thanks @KhraksMamtsov! - `Config.redacted` has been made more flexible and can now wrap any other config. This allows to transform or validate config values before it’s hidden. + + ```ts + import { Config } from "effect" + + Effect.gen(function* () { + // can be any string including empty + const pass1 = yield* Config.redacted("PASSWORD") + // ^? Redacted + + // can't be empty string + const pass2 = yield* Config.redacted(Config.nonEmptyString("PASSWORD")) + // ^? Redacted + + const pass2 = yield* Config.redacted(Config.number("SECRET_NUMBER")) + // ^? Redacted + }) + ``` + +- [#3835](https://github.com/Effect-TS/effect/pull/3835) [`aadb8a4`](https://github.com/Effect-TS/effect/commit/aadb8a48d2cba197c06ec9996505510e48e4e5cb) Thanks @fubhy! - Added `BigDecimal.unsafeFromNumber` and `BigDecimal.safeFromNumber`. + + Deprecated `BigDecimal.fromNumber` in favour of `BigDecimal.unsafeFromNumber`. + + The current implementation of `BigDecimal.fromNumber` and `BigDecimal.unsafeFromNumber` now throws + a `RangeError` for numbers that are not finite such as `NaN`, `+Infinity` or `-Infinity`. + +### Patch Changes + +- [#3835](https://github.com/Effect-TS/effect/pull/3835) [`5eff3f6`](https://github.com/Effect-TS/effect/commit/5eff3f6fa3aae7e86948a62cbfd63b8d6c3bdf92) Thanks @tim-smart! - fix multipart support for bun http server + +- [#3835](https://github.com/Effect-TS/effect/pull/3835) [`9264162`](https://github.com/Effect-TS/effect/commit/9264162a82783a651776fb7b87604564a63e7070) Thanks @IMax153! - inherit child fibers created by merged streams + +## 3.10.20 + +### Patch Changes + +- [#4042](https://github.com/Effect-TS/effect/pull/4042) [`3069614`](https://github.com/Effect-TS/effect/commit/30696149271129fc618f6f2ccd1d8f2f6c0f9cd7) Thanks @tim-smart! - catch logger defects from calling .toJSON on data types + +- [#4041](https://github.com/Effect-TS/effect/pull/4041) [`09a5e52`](https://github.com/Effect-TS/effect/commit/09a5e522fd9b221f05d85b1d1c8a740d4973c302) Thanks @tim-smart! - fix docs for Stream.partition + +## 3.10.19 + +### Patch Changes + +- [#4007](https://github.com/Effect-TS/effect/pull/4007) [`944025b`](https://github.com/Effect-TS/effect/commit/944025bc5ce139f4a85846aa689bf30ec06a8ec1) Thanks @gcanti! - Wrap JSDoc @example tags with a TypeScript fence, closes #4002 + +- [#4013](https://github.com/Effect-TS/effect/pull/4013) [`54addee`](https://github.com/Effect-TS/effect/commit/54addee438a644bf010646c52042c7b89c5fc0a7) Thanks @thewilkybarkid! - Remove reference to non-existent function + +## 3.10.18 + +### Patch Changes + +- [#4004](https://github.com/Effect-TS/effect/pull/4004) [`af409cf`](https://github.com/Effect-TS/effect/commit/af409cf1d2ff973be11cc079ea373eaeedca25de) Thanks @tim-smart! - fix behavour of Stream.partition to match the types + +## 3.10.17 + +### Patch Changes + +- [#3998](https://github.com/Effect-TS/effect/pull/3998) [`42c4ce6`](https://github.com/Effect-TS/effect/commit/42c4ce6f8d8c7d847e97757650a8ad9419a829d7) Thanks @tim-smart! - ensure fiber observers are cleared after exit to prevent memory leaks + +## 3.10.16 + +### Patch Changes + +- [#3918](https://github.com/Effect-TS/effect/pull/3918) [`4dca30c`](https://github.com/Effect-TS/effect/commit/4dca30cfcdafe4542e236489f71d6f171a5b4e38) Thanks @gcanti! - Use a specific annotation (`AutoTitleAnnotationId`) to add automatic titles (added by `Struct` and `Class` APIs), instead of `TitleAnnotationId`, to avoid interfering with user-defined titles. + +- [#3981](https://github.com/Effect-TS/effect/pull/3981) [`1d99867`](https://github.com/Effect-TS/effect/commit/1d998671be3cd11043f232822e91dd8c98fccfa9) Thanks @gcanti! - Stable filters such as `minItems`, `maxItems`, and `itemsCount` should be applied only if the from part fails with a `Composite` issue, closes #3980 + + Before + + ```ts + import { Schema } from "effect" + + const schema = Schema.Struct({ + a: Schema.Array(Schema.String).pipe(Schema.minItems(1)) + }) + + Schema.decodeUnknownSync(schema)({}, { errors: "all" }) + // throws: TypeError: Cannot read properties of undefined (reading 'length') + ``` + + After + + ```ts + import { Schema } from "effect" + + const schema = Schema.Struct({ + a: Schema.Array(Schema.String).pipe(Schema.minItems(1)) + }) + + Schema.decodeUnknownSync(schema)({}, { errors: "all" }) + /* + throws: + ParseError: { readonly a: an array of at least 1 items } + └─ ["a"] + └─ is missing + */ + ``` + +- [#3972](https://github.com/Effect-TS/effect/pull/3972) [`6dae414`](https://github.com/Effect-TS/effect/commit/6dae4147991a97ec14a99289bd25fadae7541e8d) Thanks @tim-smart! - add support for 0 capacity to Mailbox + +- [#3959](https://github.com/Effect-TS/effect/pull/3959) [`6b0d737`](https://github.com/Effect-TS/effect/commit/6b0d737078bf63b97891e6bc47affc04b28f9cf7) Thanks @gcanti! - Remove `Omit` from the `Class` interface definition to align type signatures with runtime behavior. This fix addresses the issue of being unable to override base class methods in extended classes without encountering type errors, closes #3958 + + Before + + ```ts + import { Schema } from "effect" + + class Base extends Schema.Class("Base")({ + a: Schema.String + }) { + f() { + console.log("base") + } + } + + class Extended extends Base.extend("Extended")({}) { + // Class '{ readonly a: string; } & Omit' defines instance member property 'f', + // but extended class 'Extended' defines it as instance member function.ts(2425) + // @ts-expect-error + override f() { + console.log("extended") + } + } + ``` + + After + + ```ts + import { Schema } from "effect" + + class Base extends Schema.Class("Base")({ + a: Schema.String + }) { + f() { + console.log("base") + } + } + + class Extended extends Base.extend("Extended")({}) { + // ok + override f() { + console.log("extended") + } + } + ``` + +- [#3971](https://github.com/Effect-TS/effect/pull/3971) [`d8356aa`](https://github.com/Effect-TS/effect/commit/d8356aad428a0c2290db52380220f81d9ec94232) Thanks @gcanti! - Refactor JSON Schema Generation to Include Transformation Annotations, closes #3016 + + When generating a JSON Schema, treat `TypeLiteralTransformations` (such as when `Schema.optionalWith` is used) as a special case. Annotations from the transformation itself will now be applied, unless there are user-defined annotations on the form side. This change ensures that the user's intended annotations are properly included in the schema. + + **Before** + + Annotations set on the transformation are ignored. However while using `Schema.optionalWith` internally generates a transformation schema, this is considered a technical detail. The user's intention is to add annotations to the "struct" schema, not to the transformation. + + ```ts + import { JSONSchema, Schema } from "effect" + + const schema = Schema.Struct({ + a: Schema.optionalWith(Schema.String, { default: () => "" }) + }).annotations({ + identifier: "MyID", + description: "My description", + title: "My title" + }) + + console.log(JSONSchema.make(schema)) + /* + Output: + { + '$schema': 'http://json-schema.org/draft-07/schema#', + type: 'object', + required: [], + properties: { a: { type: 'string' } }, + additionalProperties: false + } + */ + ``` + + **After** + + Annotations set on the transformation are now considered during JSON Schema generation: + + ```ts + import { JSONSchema, Schema } from "effect" + + const schema = Schema.Struct({ + a: Schema.optionalWith(Schema.String, { default: () => "" }) + }).annotations({ + identifier: "MyID", + description: "My description", + title: "My title" + }) + + console.log(JSONSchema.make(schema)) + /* + Output: + { + '$schema': 'http://json-schema.org/draft-07/schema#', + '$ref': '#/$defs/MyID', + '$defs': { + MyID: { + type: 'object', + required: [], + properties: [Object], + additionalProperties: false, + description: 'My description', + title: 'My title' + } + } + } + */ + ``` + +## 3.10.15 + +### Patch Changes + +- [#3936](https://github.com/Effect-TS/effect/pull/3936) [`8398b32`](https://github.com/Effect-TS/effect/commit/8398b3208242a88239d4449910b7baf923cfe3b6) Thanks @tim-smart! - allow DateTime.makeZoned to default to the local time zone + +- [#3917](https://github.com/Effect-TS/effect/pull/3917) [`72e55b7`](https://github.com/Effect-TS/effect/commit/72e55b7c610784fcebdbadc592c876e23e76a986) Thanks @SuttonKyle! - Allow Stream.split to use refinement for better type inference + +## 3.10.14 + +### Patch Changes + +- [#3920](https://github.com/Effect-TS/effect/pull/3920) [`f983946`](https://github.com/Effect-TS/effect/commit/f9839467b4cad6e788297764ef9f9f0b9fd203f9) Thanks @gcanti! - remove redundant check in `JSONNumber` declaration + +- [#3924](https://github.com/Effect-TS/effect/pull/3924) [`2d8a750`](https://github.com/Effect-TS/effect/commit/2d8a75081eb83a0a81f817fdf6f428369c5064ab) Thanks @tim-smart! - ensure a ManagedRuntime can be built synchronously + +## 3.10.13 + +### Patch Changes + +- [#3907](https://github.com/Effect-TS/effect/pull/3907) [`995bbdf`](https://github.com/Effect-TS/effect/commit/995bbdffea2e332f203cd5b474cd6a1c77dfa6ae) Thanks @arijoon! - Schema.BigDecimal Arbitrary's scale limited to the range 0-18 + +## 3.10.12 + +### Patch Changes + +- [#3904](https://github.com/Effect-TS/effect/pull/3904) [`dd14efe`](https://github.com/Effect-TS/effect/commit/dd14efe0ace255f571273aae876adea96267d7e6) Thanks @tim-smart! - allow pool items te be used while being acquired + +## 3.10.11 + +### Patch Changes + +- [#3903](https://github.com/Effect-TS/effect/pull/3903) [`5eef499`](https://github.com/Effect-TS/effect/commit/5eef4998b6ccb7a5404d9e4fef85e57fa35fbb8a) Thanks @tim-smart! - cache Schema.Class AST once generated + +## 3.10.10 + +### Patch Changes + +- [#3893](https://github.com/Effect-TS/effect/pull/3893) [`cd720ae`](https://github.com/Effect-TS/effect/commit/cd720aedf7f2571edec0843d6a633e84e4832b28) Thanks @tim-smart! - support "dropping" & "sliding" strategies in Mailbox + +- [#3893](https://github.com/Effect-TS/effect/pull/3893) [`cd720ae`](https://github.com/Effect-TS/effect/commit/cd720aedf7f2571edec0843d6a633e84e4832b28) Thanks @tim-smart! - add Mailbox.fromStream api + +- [#3886](https://github.com/Effect-TS/effect/pull/3886) [`b631f40`](https://github.com/Effect-TS/effect/commit/b631f40abbe649b2a089764585b5c39f6a695ac6) Thanks @fubhy! - Optimized `Base64.decode` by not capturing the padding characters in the underlying array buffer. + + Previously, the implementation first captured the padding characters in the underlying array buffer and + then returned a new subarray view of the buffer with the padding characters removed. + + By not capturing the padding characters, we avoid the creation of another typed array instance for the + subarray view. + +## 3.10.9 + +### Patch Changes + +- [#3883](https://github.com/Effect-TS/effect/pull/3883) [`a123e80`](https://github.com/Effect-TS/effect/commit/a123e80f111a625428a5b5622b7f55ee1073566b) Thanks @tim-smart! - add FromIterator primitive to improve Effect.gen performance + +- [#3880](https://github.com/Effect-TS/effect/pull/3880) [`bd5fcd3`](https://github.com/Effect-TS/effect/commit/bd5fcd3e6b603b1e505af90d6a00627c8eca6d41) Thanks @tim-smart! - refactor Effect.gen to improve performance + +- [#3881](https://github.com/Effect-TS/effect/pull/3881) [`0289d3b`](https://github.com/Effect-TS/effect/commit/0289d3b6391031d00329365bab9791b355031fe3) Thanks @tim-smart! - implement Effect.suspend using OP_COMMIT + +- [#3862](https://github.com/Effect-TS/effect/pull/3862) [`7386b71`](https://github.com/Effect-TS/effect/commit/7386b710e5be570e17f468928a6ed19d549a3e12) Thanks @furrycatherder! - fix the type signature of `use` in Effect.Service + +- [#3879](https://github.com/Effect-TS/effect/pull/3879) [`4211a23`](https://github.com/Effect-TS/effect/commit/4211a2355bb3af3f0e756e2aae9d293379f25662) Thanks @IMax153! - Return a sequential cause when both the `use` and `release` fail in `Effect.acquireUseRelease` + +## 3.10.8 + +### Patch Changes + +- [#3868](https://github.com/Effect-TS/effect/pull/3868) [`68b5c9e`](https://github.com/Effect-TS/effect/commit/68b5c9e44f34192cef26e1cadda5e661a027df41) Thanks @tim-smart! - move \_op check out of the fiber hot path + +- [#3849](https://github.com/Effect-TS/effect/pull/3849) [`9c9928d`](https://github.com/Effect-TS/effect/commit/9c9928dfeacd9ac33dc37eb0ca3d7d8c39175ada) Thanks @patroza! - improve: use literal `key` on Service + +- [#3872](https://github.com/Effect-TS/effect/pull/3872) [`6306e66`](https://github.com/Effect-TS/effect/commit/6306e6656092b350d4ede5746da6f245ec9f7e07) Thanks @KhraksMamtsov! - Fix `Config.integer` & `Config.number` + +- [#3869](https://github.com/Effect-TS/effect/pull/3869) [`361c7f3`](https://github.com/Effect-TS/effect/commit/361c7f39a2c10ede9324847c3d3ba192a6f9b20a) Thanks @KhraksMamtsov! - jsdoc-examples for class-based APIs have been added, e.g. `Schema.TaggedError`, `Effect.Service` and others + +## 3.10.7 + +### Patch Changes + +- [#3867](https://github.com/Effect-TS/effect/pull/3867) [`33f5b9f`](https://github.com/Effect-TS/effect/commit/33f5b9ffaebea4f1bd0e391b44c41fb6230e743a) Thanks @tim-smart! - ensure Channel.mergeWith fibers can be interrupted + +- [#3865](https://github.com/Effect-TS/effect/pull/3865) [`50f0281`](https://github.com/Effect-TS/effect/commit/50f0281b0d2116726b8927a6217622d5f394f3e4) Thanks @tim-smart! - fix memory leak in Stream.retry + +## 3.10.6 + +### Patch Changes + +- [#3858](https://github.com/Effect-TS/effect/pull/3858) [`ce1c21f`](https://github.com/Effect-TS/effect/commit/ce1c21ffc11902ac9ab453a51904207859d38552) Thanks @KhraksMamtsov! - fix `Tag.Proxy` type + +## 3.10.5 + +### Patch Changes + +- [#3841](https://github.com/Effect-TS/effect/pull/3841) [`3a6d757`](https://github.com/Effect-TS/effect/commit/3a6d757badeebe00d8ef4d67530d073c8264dcfa) Thanks @KhraksMamtsov! - Support union of parameters in functions in `Effect.Tag.Proxy` type + +- [#3845](https://github.com/Effect-TS/effect/pull/3845) [`59d813a`](https://github.com/Effect-TS/effect/commit/59d813aa4973d1115cfc70cc3667508335f49693) Thanks @tim-smart! - ensure fiber refs are not inherited by ManagedRuntime + +## 3.10.4 + +### Patch Changes + +- [#3842](https://github.com/Effect-TS/effect/pull/3842) [`2367708`](https://github.com/Effect-TS/effect/commit/2367708be449f9526a2047e321302d7bfb16f18e) Thanks @gcanti! - add support for `Schema.OptionFromUndefinedOr` in JSON Schema generation, closes #3839 + + Before + + ```ts + import { JSONSchema, Schema } from "effect" + + const schema = Schema.Struct({ + a: Schema.OptionFromUndefinedOr(Schema.Number) + }) + + console.log(JSON.stringify(JSONSchema.make(schema), null, 2)) + /* + throws: + Error: Missing annotation + at path: ["a"] + details: Generating a JSON Schema for this schema requires a "jsonSchema" annotation + schema (UndefinedKeyword): undefined + */ + ``` + + After + + ```ts + import { JSONSchema, Schema } from "effect" + + const schema = Schema.Struct({ + a: Schema.OptionFromUndefinedOr(Schema.Number) + }) + + console.log(JSON.stringify(JSONSchema.make(schema), null, 2)) + /* + Output: + { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "required": [], + "properties": { + "a": { + "type": "number" + } + }, + "additionalProperties": false + } + */ + ``` + +## 3.10.3 + +### Patch Changes + +- [#3833](https://github.com/Effect-TS/effect/pull/3833) [`b9423d8`](https://github.com/Effect-TS/effect/commit/b9423d8bf8181a2389fdbce1e3c14ac6fe8d54f5) Thanks @IMax153! - Ensure undefined JSON values are not coerced to empty string + +## 3.10.2 + +### Patch Changes + +- [#3820](https://github.com/Effect-TS/effect/pull/3820) [`714e119`](https://github.com/Effect-TS/effect/commit/714e11945e45e5a2554ee058e6c43f82a8e309cf) Thanks @tim-smart! - simplify Match fail keys types + +- [#3825](https://github.com/Effect-TS/effect/pull/3825) [`c1afd55`](https://github.com/Effect-TS/effect/commit/c1afd55c54e61f9c432823d21b3d016f79160a37) Thanks @KhraksMamtsov! - - Make `MergeRight`, `MergeLeft` and `MergeRecord` in `Types` module homomorphic (preserve original `readonly` and optionality modifiers) + - `MergeRecord` now is alias for `MergeLeft` + +## 3.10.1 + +### Patch Changes + +- [#3818](https://github.com/Effect-TS/effect/pull/3818) [`9604d6b`](https://github.com/Effect-TS/effect/commit/9604d6b616435103dafea8b53637a9d1450b4750) Thanks @tim-smart! - fix Channel.embedInput halting in uninterruptible region + +## 3.10.0 + +### Minor Changes + +- [#3764](https://github.com/Effect-TS/effect/pull/3764) [`4a01828`](https://github.com/Effect-TS/effect/commit/4a01828b66d6213e9bbe18979c893b13f7bb29bf) Thanks @evelant! - add TSubscriptionRef + +- [#3764](https://github.com/Effect-TS/effect/pull/3764) [`4a01828`](https://github.com/Effect-TS/effect/commit/4a01828b66d6213e9bbe18979c893b13f7bb29bf) Thanks @evelant! - add Stream.fromTQueue & Stream.fromTPubSub + +- [#3764](https://github.com/Effect-TS/effect/pull/3764) [`c79c4c1`](https://github.com/Effect-TS/effect/commit/c79c4c178390fe61ff6dda88c9e058862349343a) Thanks @gcanti! - Merge Schema into Effect. + + ### Modules + + Before + + ```ts + import { + Arbitrary, + AST, + FastCheck, + JSONSchema, + ParseResult, + Pretty, + Schema + } from "@effect/schema" + ``` + + After + + ```ts + import { + Arbitrary, + SchemaAST, // changed + FastCheck, + JSONSchema, + ParseResult, + Pretty, + Schema + } from "effect" + ``` + + ### Formatters + + `ArrayFormatter` / `TreeFormatter` merged into `ParseResult` module. + + Before + + ```ts + import { ArrayFormatter, TreeFormatter } from "@effect/schema" + ``` + + After + + ```ts + import { ArrayFormatter, TreeFormatter } from "effect/ParseResult" + ``` + + ### Serializable + + Merged into `Schema` module. + + ### Equivalence + + Merged into `Schema` module. + + Before + + ```ts + import { Equivalence } from "@effect/schema" + + Equivalence.make(myschema) + ``` + + After + + ```ts + import { Schema } from "@effect/schema" + + Schema.equivalence(myschema) + ``` + +- [#3764](https://github.com/Effect-TS/effect/pull/3764) [`38d30f0`](https://github.com/Effect-TS/effect/commit/38d30f08b8da62f9c3e308b9250738cb8d17bdb5) Thanks @tim-smart! - add option to .releaseLock a ReadableStream on finalization + +- [#3764](https://github.com/Effect-TS/effect/pull/3764) [`5821ce3`](https://github.com/Effect-TS/effect/commit/5821ce3455b47d25e0a40cae6ce22af9db5fa556) Thanks @patroza! - feat: implement Redactable. Used by Headers to not log sensitive information + +## 3.9.2 + +### Patch Changes + +- [#3768](https://github.com/Effect-TS/effect/pull/3768) [`61a99b2`](https://github.com/Effect-TS/effect/commit/61a99b2bf9d757870ef0c2ec9d4c877cdd364a3d) Thanks @tim-smart! - allow tacit usage with do notation apis (.bind / .let) + +## 3.9.1 + +### Patch Changes + +- [#3740](https://github.com/Effect-TS/effect/pull/3740) [`3b2ad1d`](https://github.com/Effect-TS/effect/commit/3b2ad1d58a2e33dc1a72b7037396bd25ca1702a9) Thanks @tim-smart! - revert deno Inspectable changes + +## 3.9.0 + +### Minor Changes + +- [#3620](https://github.com/Effect-TS/effect/pull/3620) [`ff3d1aa`](https://github.com/Effect-TS/effect/commit/ff3d1aab290b4d1173b2dfc7e4c76abb4babdc16) Thanks @vinassefranche! - Adds HashMap.HashMap.Entry type helper + +- [#3620](https://github.com/Effect-TS/effect/pull/3620) [`0ba66f2`](https://github.com/Effect-TS/effect/commit/0ba66f2451641fd6990e02ec1ed01c014db9dab0) Thanks @tim-smart! - add deno support to Inspectable + +- [#3620](https://github.com/Effect-TS/effect/pull/3620) [`bf77f51`](https://github.com/Effect-TS/effect/commit/bf77f51b323c383224ebf08adf77a7a6e8c9b3cd) Thanks @KhraksMamtsov! - `Latch` implements `Effect` with `.await` semantic + +- [#3620](https://github.com/Effect-TS/effect/pull/3620) [`0779681`](https://github.com/Effect-TS/effect/commit/07796813f07de035719728733096ba64ce333469) Thanks @KhraksMamtsov! - Effect.mapAccum & Array.mapAccum preserve non-emptiness + +- [#3620](https://github.com/Effect-TS/effect/pull/3620) [`534129f`](https://github.com/Effect-TS/effect/commit/534129f8113ce1a8ec50828083e16da9c86326c6) Thanks @KhraksMamtsov! - `Pool` is now a subtype of `Effect`, equivalent to `Pool.get` + +- [#3620](https://github.com/Effect-TS/effect/pull/3620) [`d75140c`](https://github.com/Effect-TS/effect/commit/d75140c7a664ceda43142d999f4ff8dcd36d6dda) Thanks @mikearnaldi! - Support providing an array of layers via Effect.provide and Layer.provide + +- [#3620](https://github.com/Effect-TS/effect/pull/3620) [`be0451c`](https://github.com/Effect-TS/effect/commit/be0451c149b6618af79cb839cdf04af2db1efb03) Thanks @leonitousconforti! - support ManagedRuntime in Effect.provide + +- [#3620](https://github.com/Effect-TS/effect/pull/3620) [`be0451c`](https://github.com/Effect-TS/effect/commit/be0451c149b6618af79cb839cdf04af2db1efb03) Thanks @leonitousconforti! - `ManagedRuntime` is subtype of `Effect, E, never>` + +- [#3620](https://github.com/Effect-TS/effect/pull/3620) [`5b36494`](https://github.com/Effect-TS/effect/commit/5b364942e9a9003fdb8217324f8a2d8369c969da) Thanks @KhraksMamtsov! - `Tuple.map` transforms each element of tuple using the given function, treating tuple homomorphically + + ```ts + import { pipe, Tuple } from "effect" + + const result = pipe( + // ^? [string, string, string] + ["a", 1, false] as const, + T.map((el) => { + //^? "a" | 1 | false + return el.toString().toUppercase() + }) + ) + assert.deepStrictEqual(result, ["A", "1", "FALSE"]) + ``` + +- [#3620](https://github.com/Effect-TS/effect/pull/3620) [`c716adb`](https://github.com/Effect-TS/effect/commit/c716adb250ebbea1d1048d818ef7fed4f621d186) Thanks @AlexGeb! - Add Array.pad function + +- [#3620](https://github.com/Effect-TS/effect/pull/3620) [`4986391`](https://github.com/Effect-TS/effect/commit/49863919cd8628c962a712fb1df30d2983820933) Thanks @ianbollinger! - Add an `isRegExp` type guard + +- [#3620](https://github.com/Effect-TS/effect/pull/3620) [`d75140c`](https://github.com/Effect-TS/effect/commit/d75140c7a664ceda43142d999f4ff8dcd36d6dda) Thanks @mikearnaldi! - Implement Effect.Service as a Tag and Layer with Opaque Type. + + Namely the following is now possible: + + ```ts + class Prefix extends Effect.Service()("Prefix", { + sync: () => ({ + prefix: "PRE" + }) + }) {} + + class Postfix extends Effect.Service()("Postfix", { + sync: () => ({ + postfix: "POST" + }) + }) {} + + const messages: Array = [] + + class Logger extends Effect.Service()("Logger", { + accessors: true, + effect: Effect.gen(function* () { + const { prefix } = yield* Prefix + const { postfix } = yield* Postfix + return { + info: (message: string) => + Effect.sync(() => { + messages.push(`[${prefix}][${message}][${postfix}]`) + }) + } + }), + dependencies: [Prefix.Default, Postfix.Default] + }) {} + + describe("Effect", () => { + it.effect("Service correctly wires dependencies", () => + Effect.gen(function* () { + const { _tag } = yield* Logger + expect(_tag).toEqual("Logger") + yield* Logger.info("Ok") + expect(messages).toEqual(["[PRE][Ok][POST]"]) + const { prefix } = yield* Prefix + expect(prefix).toEqual("PRE") + const { postfix } = yield* Postfix + expect(postfix).toEqual("POST") + }).pipe(Effect.provide([Logger.Default, Prefix.Default, Postfix.Default])) + ) + }) + ``` + +- [#3620](https://github.com/Effect-TS/effect/pull/3620) [`d1387ae`](https://github.com/Effect-TS/effect/commit/d1387aebd1ff01bbebde26be46d488956e4daef6) Thanks @KhraksMamtsov! - `Resource` is subtype of `Effect`. + `ScopedRed` is subtype of `Effect`. + +### Patch Changes + +- [#3620](https://github.com/Effect-TS/effect/pull/3620) [`016f9ad`](https://github.com/Effect-TS/effect/commit/016f9ad931a4b3d09a34e5caf13d87c5b8e9c984) Thanks @tim-smart! - fix Unify for Deferred + +- [#3620](https://github.com/Effect-TS/effect/pull/3620) [`9237ac6`](https://github.com/Effect-TS/effect/commit/9237ac69bc07de5b3b60076a0ad2921c21de7457) Thanks @leonitousconforti! - move ManagedRuntime.TypeId to fix circular imports + +## 3.8.5 + +### Patch Changes + +- [#3734](https://github.com/Effect-TS/effect/pull/3734) [`88e85db`](https://github.com/Effect-TS/effect/commit/88e85db34bd402526e27a323e950d053fa34d232) Thanks @mikearnaldi! - Ensure random numbers are correctly distributed + +- [#3717](https://github.com/Effect-TS/effect/pull/3717) [`83887ca`](https://github.com/Effect-TS/effect/commit/83887ca1b1793916913d8550a4db4450cd14a044) Thanks @mikearnaldi! - Consider async operation in runSync as a defect, add span based stack + +- [#3731](https://github.com/Effect-TS/effect/pull/3731) [`5266b6c`](https://github.com/Effect-TS/effect/commit/5266b6cd86d76c3886da041c8829bca04b1a3110) Thanks @patroza! - Improve DX of type errors from inside `pipe` and `flow` + +- [#3699](https://github.com/Effect-TS/effect/pull/3699) [`cdead5c`](https://github.com/Effect-TS/effect/commit/cdead5c9cfd54dc6c4f215d9732f654c4a12e991) Thanks @jessekelly881! - added Stream.mergeWithTag + + Combines a struct of streams into a single stream of tagged values where the tag is the key of the struct. + + ```ts + import { Stream } from "effect" + + // Stream.Stream<{ _tag: "a"; value: number; } | { _tag: "b"; value: string; }> + const stream = Stream.mergeWithTag( + { + a: Stream.make(0), + b: Stream.make("") + }, + { concurrency: 1 } + ) + ``` + +- [#3706](https://github.com/Effect-TS/effect/pull/3706) [`766a8af`](https://github.com/Effect-TS/effect/commit/766a8af307b414aca3648d91c4eab7493a5ec862) Thanks @fubhy! - Made `BigDecimal.scale` dual. + +## 3.8.4 + +### Patch Changes + +- [#3661](https://github.com/Effect-TS/effect/pull/3661) [`4509656`](https://github.com/Effect-TS/effect/commit/45096569d50262275ee984f44c456f5c83b62683) Thanks @KhraksMamtsov! - `Micro.EnvRef` and `Micro.Handle` is subtype of `Micro` + +## 3.8.3 + +### Patch Changes + +- [#3644](https://github.com/Effect-TS/effect/pull/3644) [`bb5ec6b`](https://github.com/Effect-TS/effect/commit/bb5ec6b4b6a6f537394596c5a596faf52cb2aef4) Thanks @tim-smart! - fix encoding of logs to tracer span events + +## 3.8.2 + +### Patch Changes + +- [#3627](https://github.com/Effect-TS/effect/pull/3627) [`f0d8ef1`](https://github.com/Effect-TS/effect/commit/f0d8ef1ce97ec2a87b09b3e24150cfeab85d6e2f) Thanks @fubhy! - Revert cron schedule regression + +## 3.8.1 + +### Patch Changes + +- [#3624](https://github.com/Effect-TS/effect/pull/3624) [`10bf621`](https://github.com/Effect-TS/effect/commit/10bf6213f36d8ddb00f058a4609b85220f3d8334) Thanks @fubhy! - Fixed double firing of cron schedules in cases where the current time matched the initial interval. + +- [#3623](https://github.com/Effect-TS/effect/pull/3623) [`ae36fa6`](https://github.com/Effect-TS/effect/commit/ae36fa68f754eeab9a54b6dc0f8b44db513aa2b6) Thanks @fubhy! - Allow CRLF characters in base64 encoded strings. + +## 3.8.0 + +### Minor Changes + +- [#3541](https://github.com/Effect-TS/effect/pull/3541) [`fcfa6ee`](https://github.com/Effect-TS/effect/commit/fcfa6ee30ffd07d998bf22799357bf58580a116f) Thanks @Schniz! - add `Logger.withLeveledConsole` + + In browsers and different platforms, `console.error` renders differently than `console.info`. This helps to distinguish between different levels of logging. `Logger.withLeveledConsole` takes any logger and calls the respective `Console` method based on the log level. For instance, `Effect.logError` will call `Console.error` and `Effect.logInfo` will call `Console.info`. + + To use it, you can replace the default logger with a `Logger.withLeveledConsole` logger: + + ```ts + import { Logger, Effect } from "effect" + + const loggerLayer = Logger.withLeveledConsole(Logger.stringLogger) + + Effect.gen(function* () { + yield* Effect.logError("an error") + yield* Effect.logInfo("an info") + }).pipe(Effect.provide(loggerLayer)) + ``` + +- [#3541](https://github.com/Effect-TS/effect/pull/3541) [`bb9931b`](https://github.com/Effect-TS/effect/commit/bb9931b62e249a3b801f2cb9d097aec0c8511af7) Thanks @KhraksMamtsov! - Made `Ref`, `SynchronizedRed` and `SubscriptionRef` a subtype of `Effect` + +- [#3541](https://github.com/Effect-TS/effect/pull/3541) [`5798f76`](https://github.com/Effect-TS/effect/commit/5798f7619529de33e5ba06f551806f68fedc19db) Thanks @tim-smart! - add Semaphore.withPermitsIfAvailable + + You can now use `Semaphore.withPermitsIfAvailable` to run an Effect only if the + Semaphore has enough permits available. This is useful when you want to run an + Effect only if you can acquire a permit without blocking. + + It will return an `Option.Some` with the result of the Effect if the permits were + available, or `None` if they were not. + + ```ts + import { Effect } from "effect" + + Effect.gen(function* () { + const semaphore = yield* Effect.makeSemaphore(1) + semaphore.withPermitsIfAvailable(1)(Effect.void) + }) + ``` + +- [#3541](https://github.com/Effect-TS/effect/pull/3541) [`5f0bfa1`](https://github.com/Effect-TS/effect/commit/5f0bfa17205398d4e4818bfbcf9e1b505b3b1fc5) Thanks @KhraksMamtsov! - The `Deferred` is now a subtype of `Effect`. This change simplifies handling of deferred values, removing the need for explicit call `Deffer.await`. + + ```typescript + import { Effect, Deferred } from "effect" + + Effect.gen(function* () { + const deferred = yield* Deferred.make() + + const before = yield* Deferred.await(deferred) + const after = yield* deferred + }) + ``` + +- [#3541](https://github.com/Effect-TS/effect/pull/3541) [`812a4e8`](https://github.com/Effect-TS/effect/commit/812a4e86e2d1aa23b477ef5829aa0e5c07784936) Thanks @tim-smart! - add Logger.prettyLoggerDefault, to prevent duplicate pretty loggers + +- [#3541](https://github.com/Effect-TS/effect/pull/3541) [`273565e`](https://github.com/Effect-TS/effect/commit/273565e7901639e8d0541930ab715aea9c80fbaa) Thanks @tim-smart! - add Effect.makeLatch, for creating a simple async latch + + ```ts + import { Effect } from "effect" + + Effect.gen(function* () { + // Create a latch, starting in the closed state + const latch = yield* Effect.makeLatch(false) + + // Fork a fiber that logs "open sesame" when the latch is opened + const fiber = yield* Effect.log("open sesame").pipe( + latch.whenOpen, + Effect.fork + ) + + // Open the latch + yield* latch.open + yield* fiber.await + }) + ``` + +- [#3541](https://github.com/Effect-TS/effect/pull/3541) [`569a801`](https://github.com/Effect-TS/effect/commit/569a8017ef0a0bc203e4312867cbdd37b0effbd7) Thanks @KhraksMamtsov! - `Dequeue` and `Queue` is subtype of `Effect`. This means that now it can be used as an `Effect`, and when called, it will automatically extract and return an item from the queue, without having to explicitly use the `Queue.take` function. + + ```ts + Effect.gen(function* () { + const queue = yield* Queue.unbounded() + yield* Queue.offer(queue, 1) + yield* Queue.offer(queue, 2) + const oldWay = yield* Queue.take(queue) + const newWay = yield* queue + }) + ``` + +- [#3541](https://github.com/Effect-TS/effect/pull/3541) [`aa1fa53`](https://github.com/Effect-TS/effect/commit/aa1fa5301e886b9657c8eb0d38cb87cef92a8305) Thanks @vinassefranche! - Add Number.round + +- [#3541](https://github.com/Effect-TS/effect/pull/3541) [`02f6b06`](https://github.com/Effect-TS/effect/commit/02f6b0660e12bee1069532a9cc18d3ab855257be) Thanks @fubhy! - Add additional `Duration` conversion apis + - `Duration.toMinutes` + - `Duration.toHours` + - `Duration.toDays` + - `Duration.toWeeks` + +- [#3541](https://github.com/Effect-TS/effect/pull/3541) [`12b893e`](https://github.com/Effect-TS/effect/commit/12b893e63cc6dfada4aca7773b4783940e2edf25) Thanks @KhraksMamtsov! - The `Fiber` is now a subtype of `Effect`. This change removes the need for explicit call `Fiber.join`. + + ```typescript + import { Effect, Fiber } from "effect" + + Effect.gen(function*() { + const fiber = yield* Effect.fork(Effect.succeed(1)) + + const oldWay = yield* Fiber.join(fiber) + const now = yield* fiber + })) + ``` + +- [#3541](https://github.com/Effect-TS/effect/pull/3541) [`bbad27e`](https://github.com/Effect-TS/effect/commit/bbad27ec0a90860593f759405caa877e7f4a655f) Thanks @dilame! - add `Stream.share` api + + The `Stream.share` api is a ref counted variant of the broadcast apis. + + It allows you to share a stream between multiple consumers, and will close the + upstream when the last consumer ends. + +- [#3541](https://github.com/Effect-TS/effect/pull/3541) [`adf7d7a`](https://github.com/Effect-TS/effect/commit/adf7d7a7dfce3a7021e9f3b0d847dc85be89d754) Thanks @tim-smart! - add Mailbox module, a queue which can have done or failure signals + + ```ts + import { Chunk, Effect, Mailbox } from "effect" + import * as assert from "node:assert" + + Effect.gen(function* () { + const mailbox = yield* Mailbox.make() + + // add messages to the mailbox + yield* mailbox.offer(1) + yield* mailbox.offer(2) + yield* mailbox.offerAll([3, 4, 5]) + + // take messages from the mailbox + const [messages, done] = yield* mailbox.takeAll + assert.deepStrictEqual(Chunk.toReadonlyArray(messages), [1, 2, 3, 4, 5]) + assert.strictEqual(done, false) + + // signal that the mailbox is done + yield* mailbox.end + const [messages2, done2] = yield* mailbox.takeAll + assert.deepStrictEqual(messages2, Chunk.empty()) + assert.strictEqual(done2, true) + + // signal that the mailbox is failed + yield* mailbox.fail("boom") + }) + ``` + +- [#3541](https://github.com/Effect-TS/effect/pull/3541) [`007289a`](https://github.com/Effect-TS/effect/commit/007289a52d5877f8e90e2dacf38171ff9bf603fd) Thanks @mikearnaldi! - Cache some fiber references in the runtime to optimize reading in hot-paths + +- [#3541](https://github.com/Effect-TS/effect/pull/3541) [`42a8f99`](https://github.com/Effect-TS/effect/commit/42a8f99740eefdaf2c4544d2c345313f97547a36) Thanks @fubhy! - Added `RcMap.keys` and `MutableHashMap.keys`. + + These functions allow you to get a list of keys currently stored in the underlying hash map. + + ```ts + const map = MutableHashMap.make([ + ["a", "a"], + ["b", "b"], + ["c", "c"] + ]) + const keys = MutableHashMap.keys(map) // ["a", "b", "c"] + ``` + + ```ts + Effect.gen(function* () { + const map = yield* RcMap.make({ + lookup: (key) => Effect.succeed(key) + }) + + yield* RcMap.get(map, "a") + yield* RcMap.get(map, "b") + yield* RcMap.get(map, "c") + + const keys = yield* RcMap.keys(map) // ["a", "b", "c"] + }) + ``` + +- [#3541](https://github.com/Effect-TS/effect/pull/3541) [`eebfd29`](https://github.com/Effect-TS/effect/commit/eebfd29633fd5d38b505c5c0842036f61f05e913) Thanks @fubhy! - Add `Duration.parts` api + + ```ts + const parts = Duration.parts(Duration.sum("5 minutes", "20 seconds")) + assert.equal(parts.minutes, 5) + assert.equal(parts.seconds, 20) + ``` + +- [#3541](https://github.com/Effect-TS/effect/pull/3541) [`040703d`](https://github.com/Effect-TS/effect/commit/040703d0e100cd5511e52d812c15492414262b5e) Thanks @KhraksMamtsov! - The `FiberRef` is now a subtype of `Effect`. This change simplifies handling of deferred values, removing the need for explicit call `FiberRef.get`. + + ```typescript + import { Effect, FiberRef } from "effect" + + Effect.gen(function* () { + const fiberRef = yield* FiberRef.make("value") + + const before = yield* FiberRef.get(fiberRef) + const after = yield* fiberRef + }) + ``` + +## 3.7.3 + +### Patch Changes + +- [#3592](https://github.com/Effect-TS/effect/pull/3592) [`35a0f81`](https://github.com/Effect-TS/effect/commit/35a0f813141652d696461cd5d19fd146adaf85be) Thanks @mikearnaldi! - TestClock yield with setTimeout(0) + +## 3.7.2 + +### Patch Changes + +- [#3548](https://github.com/Effect-TS/effect/pull/3548) [`8a601d7`](https://github.com/Effect-TS/effect/commit/8a601d7a1f8ffe52ac9e6d67e9282a1495fe59c9) Thanks @tim-smart! - remove console.log statements from Micro + +- [#3546](https://github.com/Effect-TS/effect/pull/3546) [`353ba19`](https://github.com/Effect-TS/effect/commit/353ba19f9b2b9e959f0a00d058c6d40a4bc02db7) Thanks @tim-smart! - fix exported Stream types for `broadcast*` and `toPubSub` + +## 3.7.1 + +### Patch Changes + +- [#3536](https://github.com/Effect-TS/effect/pull/3536) [`79859e7`](https://github.com/Effect-TS/effect/commit/79859e71040d8edf1868b8530b90c650f4321eff) Thanks @mikearnaldi! - Optimize Array.sortWith to avoid calling the map function excesively + +- [#3516](https://github.com/Effect-TS/effect/pull/3516) [`f6a469c`](https://github.com/Effect-TS/effect/commit/f6a469c190b9f00eee5ea0cd4d5912a0ef8b46f5) Thanks @KhraksMamtsov! - support tacit usage for `Effect.tapErrorTag` and `Effect.catchTag` + +- [#3543](https://github.com/Effect-TS/effect/pull/3543) [`dcb9ec0`](https://github.com/Effect-TS/effect/commit/dcb9ec0db443894dd204d87450f779c44b9ad7f1) Thanks @datner! - Align behavior of `Stream.empty` to act like `Stream.make()` to fix behavior with `NodeStream.toReadable` + +- [#3545](https://github.com/Effect-TS/effect/pull/3545) [`79aa6b1`](https://github.com/Effect-TS/effect/commit/79aa6b136e1f29b36f34e88cb2ff162bff2bb4ed) Thanks @tim-smart! - fix Micro.forEach for empty iterables + +## 3.7.0 + +### Minor Changes + +- [#3410](https://github.com/Effect-TS/effect/pull/3410) [`2f456cc`](https://github.com/Effect-TS/effect/commit/2f456cce5012b9fcb6b4e039190d527813b75b92) Thanks @vinassefranche! - preserve `Array.modify` `Array.modifyOption` non emptiness + +- [#3410](https://github.com/Effect-TS/effect/pull/3410) [`8745e41`](https://github.com/Effect-TS/effect/commit/8745e41ed96e3765dc6048efc2a9afbe05c8a1e9) Thanks @patroza! - improve: type Fiber.awaitAll as Exit[]. + +- [#3410](https://github.com/Effect-TS/effect/pull/3410) [`e557838`](https://github.com/Effect-TS/effect/commit/e55783886b046d3c5f33447f455f9ccf2fa75922) Thanks @titouancreach! - New constructor Config.nonEmptyString + +- [#3410](https://github.com/Effect-TS/effect/pull/3410) [`d6e7e40`](https://github.com/Effect-TS/effect/commit/d6e7e40b1e2ad0c59aa02f07344d28601b14ebdc) Thanks @KhraksMamtsov! - preserve `Array.replace` `Array.replaceOption` non emptiness + +- [#3410](https://github.com/Effect-TS/effect/pull/3410) [`8356321`](https://github.com/Effect-TS/effect/commit/8356321598da04bd77c1001f45a4e447bec5591d) Thanks @KhraksMamtsov! - add `Effect.bindAll` api + + This api allows you to combine `Effect.all` with `Effect.bind`. It is useful + when you want to concurrently run multiple effects and then combine their + results in a Do notation pipeline. + + ```ts + import { Effect } from "effect" + + const result = Effect.Do.pipe( + Effect.bind("x", () => Effect.succeed(2)), + Effect.bindAll( + ({ x }) => ({ + a: Effect.succeed(x + 1), + b: Effect.succeed("foo") + }), + { concurrency: 2 } + ) + ) + assert.deepStrictEqual(Effect.runSync(result), { + x: 2, + a: 3, + b: "foo" + }) + ``` + +- [#3410](https://github.com/Effect-TS/effect/pull/3410) [`192f2eb`](https://github.com/Effect-TS/effect/commit/192f2ebb2c4ddbf4bfd8baedd32140b2376868f4) Thanks @tim-smart! - add `propagateInterruption` option to Fiber{Handle,Set,Map} + + This option will send any external interrupts to the .join result. + +- [#3410](https://github.com/Effect-TS/effect/pull/3410) [`718cb70`](https://github.com/Effect-TS/effect/commit/718cb70038629a6d58d02e407760e341f7c94474) Thanks @dilame! - feat(Stream): implement `race` operator, which accepts two upstreams and returns a stream that mirrors the first upstream to emit an item and interrupts the other upstream. + + ```ts + import { Stream, Schedule, Console, Effect } from "effect" + + const stream = Stream.fromSchedule(Schedule.spaced("2 millis")).pipe( + Stream.race(Stream.fromSchedule(Schedule.spaced("1 millis"))), + Stream.take(6), + Stream.tap((n) => Console.log(n)) + ) + + Effect.runPromise(Stream.runDrain(stream)) + // Output each millisecond from the first stream, the rest streams are interrupted + // 0 + // 1 + // 2 + // 3 + // 4 + // 5 + ``` + +- [#3410](https://github.com/Effect-TS/effect/pull/3410) [`e9d0310`](https://github.com/Effect-TS/effect/commit/e9d03107acbf204d9304f3e8aea0816b7d3c7dfb) Thanks @mikearnaldi! - Avoid automatic propagation of finalizer concurrency, closes #3440 + +- [#3410](https://github.com/Effect-TS/effect/pull/3410) [`6bf28f7`](https://github.com/Effect-TS/effect/commit/6bf28f7e3b1e5e0608ff567205fea0581d11666f) Thanks @tim-smart! - add Context.getOrElse api, for gettings a Tag's value with a fallback + +### Patch Changes + +- [#3410](https://github.com/Effect-TS/effect/pull/3410) [`db89601`](https://github.com/Effect-TS/effect/commit/db89601ee9c1050c4e762b7bd7ec65a6a2799dfe) Thanks @juliusmarminge! - add `Micro.isMicroCause` guard + +## 3.6.8 + +### Patch Changes + +- [#3510](https://github.com/Effect-TS/effect/pull/3510) [`e809286`](https://github.com/Effect-TS/effect/commit/e8092865900608c4df7a6b7991b1c13cc1e4ca2d) Thanks @fubhy! - Detect environment in Logger.pretty using process.stdout + +## 3.6.7 + +### Patch Changes + +- [#3504](https://github.com/Effect-TS/effect/pull/3504) [`50ec889`](https://github.com/Effect-TS/effect/commit/50ec8897a49b7d1fe84f63107f89d543c52f3dfc) Thanks @datner! - improve the performance of Effect.partitionMap + +## 3.6.6 + +### Patch Changes + +- [#3306](https://github.com/Effect-TS/effect/pull/3306) [`f960bf4`](https://github.com/Effect-TS/effect/commit/f960bf45239e9badac6e0ad3a602f4174cd7bbdf) Thanks @dilame! - Introduce left / right naming for Stream apis + +- [#3499](https://github.com/Effect-TS/effect/pull/3499) [`46a575f`](https://github.com/Effect-TS/effect/commit/46a575f48a05457b782fb21f7827d338c9b59320) Thanks @tim-smart! - fix nested Config.array, by ensuring path patches aren't applied twice in sequences + +## 3.6.5 + +### Patch Changes + +- [#3474](https://github.com/Effect-TS/effect/pull/3474) [`14a47a8`](https://github.com/Effect-TS/effect/commit/14a47a8c1f3cff2186b8fe7a919a1d773888fb5b) Thanks @IMax153! - Add support for incrementing and decrementing a gauge based on its prior value + +- [#3490](https://github.com/Effect-TS/effect/pull/3490) [`0c09841`](https://github.com/Effect-TS/effect/commit/0c0984173be3d58f050b300a1a8aa89d76ba49ae) Thanks @tim-smart! - fix type error when .pipe() has no arguments + +## 3.6.4 + +### Patch Changes + +- [#3404](https://github.com/Effect-TS/effect/pull/3404) [`8295281`](https://github.com/Effect-TS/effect/commit/8295281ae9bd7441e680402540bf3c8682ec417b) Thanks @KhraksMamtsov! - Fix `Cache<_, Value, _>` type parameter variance (covariant -> invariant) + +- [#3452](https://github.com/Effect-TS/effect/pull/3452) [`c940df6`](https://github.com/Effect-TS/effect/commit/c940df63800bf3c4396d91cf28ec34938642fd2c) Thanks @tim-smart! - ensure Scheduler tasks are added to a matching priority bucket + +- [#3459](https://github.com/Effect-TS/effect/pull/3459) [`00b6c6d`](https://github.com/Effect-TS/effect/commit/00b6c6d4001f5de728b7d990a1b14560b4961a63) Thanks @tim-smart! - ensure defects are caught in Effect.tryPromise + +- [#3458](https://github.com/Effect-TS/effect/pull/3458) [`f8d95a6`](https://github.com/Effect-TS/effect/commit/f8d95a61ad0762147933c5c32bb6d7237e18eef4) Thanks @thomasvargiu! - fix `DateTime.makeZonedFromString` for 0 offset + +## 3.6.3 + +### Patch Changes + +- [#3444](https://github.com/Effect-TS/effect/pull/3444) [`04adcac`](https://github.com/Effect-TS/effect/commit/04adcace913e6fc483df266874a68005e9e04ccf) Thanks @tim-smart! - ensure Stream.toReadableStream pulls always result in a enqueue + +## 3.6.2 + +### Patch Changes + +- [#3435](https://github.com/Effect-TS/effect/pull/3435) [`fd4b2f6`](https://github.com/Effect-TS/effect/commit/fd4b2f6516b325740dde615f1cf0229edf13ca0c) Thanks @Andarist! - ensure fiber is properly cleared in FiberHandle.unsafeSet + +## 3.6.1 + +### Patch Changes + +- [#3405](https://github.com/Effect-TS/effect/pull/3405) [`510a34d`](https://github.com/Effect-TS/effect/commit/510a34d4cc5d2f51347a53847f6c7db84d2b17c6) Thanks @KhraksMamtsov! - Fix `Effect.repeat` with times option returns wrong value + +- [#3398](https://github.com/Effect-TS/effect/pull/3398) [`45dbb9f`](https://github.com/Effect-TS/effect/commit/45dbb9ffeaf93d9e4df99d0cd4920e41ba9a3978) Thanks @sukovanej! - Fix `Stream.asyncPush` type signature - allow the `register` effect to fail. + +## 3.6.0 + +### Minor Changes + +- [#3380](https://github.com/Effect-TS/effect/pull/3380) [`1e0fe80`](https://github.com/Effect-TS/effect/commit/1e0fe802b36c257971296617473ce0abe730e8dc) Thanks @tim-smart! - make List.Cons extend NonEmptyIterable + +- [#3380](https://github.com/Effect-TS/effect/pull/3380) [`8135294`](https://github.com/Effect-TS/effect/commit/8135294b591ea94fde7e6f94a504608f0e630520) Thanks @tim-smart! - add DateTime module + + The `DateTime` module provides functionality for working with time, including + support for time zones and daylight saving time. + + It has two main data types: `DateTime.Utc` and `DateTime.Zoned`. + + A `DateTime.Utc` represents a time in Coordinated Universal Time (UTC), and + a `DateTime.Zoned` contains both a UTC timestamp and a time zone. + + There is also a `CurrentTimeZone` service, for setting a time zone contextually. + + ```ts + import { DateTime, Effect } from "effect" + + Effect.gen(function* () { + // Get the current time in the current time zone + const now = yield* DateTime.nowInCurrentZone + + // Math functions are included + const tomorrow = DateTime.add(now, 1, "day") + + // Convert to a different time zone + // The UTC portion of the `DateTime` is preserved and only the time zone is + // changed + const sydneyTime = tomorrow.pipe( + DateTime.unsafeSetZoneNamed("Australia/Sydney") + ) + }).pipe(DateTime.withCurrentZoneNamed("America/New_York")) + ``` + +- [#3380](https://github.com/Effect-TS/effect/pull/3380) [`cd255a4`](https://github.com/Effect-TS/effect/commit/cd255a48872d8fb924cf713ef73f0883a9cc6987) Thanks @tim-smart! - add Stream.asyncPush api + + This api creates a stream from an external push-based resource. + + You can use the `emit` helper to emit values to the stream. You can also use + the `emit` helper to signal the end of the stream by using apis such as + `emit.end` or `emit.fail`. + + By default it uses an "unbounded" buffer size. + You can customize the buffer size and strategy by passing an object as the + second argument with the `bufferSize` and `strategy` fields. + + ```ts + import { Effect, Stream } from "effect" + + Stream.asyncPush( + (emit) => + Effect.acquireRelease( + Effect.gen(function* () { + yield* Effect.log("subscribing") + return setInterval(() => emit.single("tick"), 1000) + }), + (handle) => + Effect.gen(function* () { + yield* Effect.log("unsubscribing") + clearInterval(handle) + }) + ), + { bufferSize: 16, strategy: "dropping" } + ) + ``` + +- [#3380](https://github.com/Effect-TS/effect/pull/3380) [`3845646`](https://github.com/Effect-TS/effect/commit/3845646828e98f3c7cda1217f6cfe5f642ac0603) Thanks @mikearnaldi! - Implement Struct.keys as a typed alternative to Object.keys + + ```ts + import { Struct } from "effect" + + const symbol: unique symbol = Symbol() + + const value = { + a: 1, + b: 2, + [symbol]: 3 + } + + const keys: Array<"a" | "b"> = Struct.keys(value) + ``` + +- [#3380](https://github.com/Effect-TS/effect/pull/3380) [`2d09078`](https://github.com/Effect-TS/effect/commit/2d09078c5948b37fc2f79ef858fe4ca3e4814085) Thanks @sukovanej! - Add `Random.choice`. + + ```ts + import { Random } from "effect" + + Effect.gen(function* () { + const randomItem = yield* Random.choice([1, 2, 3]) + console.log(randomItem) + }) + ``` + +- [#3380](https://github.com/Effect-TS/effect/pull/3380) [`4bce5a0`](https://github.com/Effect-TS/effect/commit/4bce5a0274203550ccf117d830721891b0a3d182) Thanks @vinassefranche! - Add onlyEffect option to Effect.tap + +- [#3380](https://github.com/Effect-TS/effect/pull/3380) [`4ddbff0`](https://github.com/Effect-TS/effect/commit/4ddbff0bb4e3ffddfeb509c59835b83245fb975e) Thanks @KhraksMamtsov! - Support `Refinement` in `Predicate.tuple` and `Predicate.struct` + +- [#3380](https://github.com/Effect-TS/effect/pull/3380) [`e74cc38`](https://github.com/Effect-TS/effect/commit/e74cc38cb420a320c4d7ef98180f19d452a8b316) Thanks @dilame! - Implement `Stream.onEnd` that adds an effect to be executed at the end of the stream. + + ```ts + import { Console, Effect, Stream } from "effect" + + const stream = Stream.make(1, 2, 3).pipe( + Stream.map((n) => n * 2), + Stream.tap((n) => Console.log(`after mapping: ${n}`)), + Stream.onEnd(Console.log("Stream ended")) + ) + + Effect.runPromise(Stream.runCollect(stream)).then(console.log) + // after mapping: 2 + // after mapping: 4 + // after mapping: 6 + // Stream ended + // { _id: 'Chunk', values: [ 2, 4, 6 ] } + ``` + +- [#3380](https://github.com/Effect-TS/effect/pull/3380) [`bb069b4`](https://github.com/Effect-TS/effect/commit/bb069b49ef291c532a02c1e8e74271f6d1bb32ec) Thanks @dilame! - Implement `Stream.onStart` that adds an effect to be executed at the start of the stream. + + ```ts + import { Console, Effect, Stream } from "effect" + + const stream = Stream.make(1, 2, 3).pipe( + Stream.onStart(Console.log("Stream started")), + Stream.map((n) => n * 2), + Stream.tap((n) => Console.log(`after mapping: ${n}`)) + ) + + Effect.runPromise(Stream.runCollect(stream)).then(console.log) + // Stream started + // after mapping: 2 + // after mapping: 4 + // after mapping: 6 + // { _id: 'Chunk', values: [ 2, 4, 6 ] } + ``` + +- [#3380](https://github.com/Effect-TS/effect/pull/3380) [`cd255a4`](https://github.com/Effect-TS/effect/commit/cd255a48872d8fb924cf713ef73f0883a9cc6987) Thanks @tim-smart! - add `bufferSize` option to Stream.fromEventListener + +- [#3380](https://github.com/Effect-TS/effect/pull/3380) [`7d02174`](https://github.com/Effect-TS/effect/commit/7d02174af3bcbf054e5cdddb821c91d0f47e8285) Thanks @fubhy! - Changed various function signatures to return `Array` instead of `ReadonlyArray` + +## 3.5.9 + +### Patch Changes + +- [#3377](https://github.com/Effect-TS/effect/pull/3377) [`6359644`](https://github.com/Effect-TS/effect/commit/635964446323cf55d4060559337e710e4a24496e) Thanks @tim-smart! - add MicroScheduler to Micro module + +- [#3362](https://github.com/Effect-TS/effect/pull/3362) [`7f41e42`](https://github.com/Effect-TS/effect/commit/7f41e428830bf3043b8be0d28dcd235d5747c942) Thanks @IMax153! - Add `Service` and `Identifier` to `Context.Tag`. + + These helpers can be used, for example, to extract the service shape from a tag: + + ```ts + import * as Context from "effect/Context" + + export class Foo extends Context.Tag("Foo")< + Foo, + { + readonly foo: Effect.Effect + } + >() {} + + type ServiceShape = typeof Foo.Service + ``` + +- [#3373](https://github.com/Effect-TS/effect/pull/3373) [`f566fd1`](https://github.com/Effect-TS/effect/commit/f566fd1d7eea531a0d981dd24037f14a603a1273) Thanks @KhraksMamtsov! - Add test for Hash.number(0.1) !== Has.number(0) + +## 3.5.8 + +### Patch Changes + +- [#3345](https://github.com/Effect-TS/effect/pull/3345) [`1ba640c`](https://github.com/Effect-TS/effect/commit/1ba640c702f187a866023bf043c26e25cce941ef) Thanks @mikearnaldi! - Fix typo propety to property + +- [#3349](https://github.com/Effect-TS/effect/pull/3349) [`c8c71bd`](https://github.com/Effect-TS/effect/commit/c8c71bd20eb87d23133dac6156b83bb08941597c) Thanks @tim-smart! - ensure all Data.Error arguments are preserved in .toJSON + +- [#3355](https://github.com/Effect-TS/effect/pull/3355) [`a26ce58`](https://github.com/Effect-TS/effect/commit/a26ce581ca7d407e1e81439b58c8045b3fa65231) Thanks @tim-smart! - fix Hash.number not returning unique values + +## 3.5.7 + +### Patch Changes + +- [#3288](https://github.com/Effect-TS/effect/pull/3288) [`3afcc93`](https://github.com/Effect-TS/effect/commit/3afcc93413a3d910beb69e4ce9ae120e4adaffd5) Thanks @mikearnaldi! - Forbid usage of property "name" in Effect.Tag + +- [#3310](https://github.com/Effect-TS/effect/pull/3310) [`99bddcf`](https://github.com/Effect-TS/effect/commit/99bddcfb3d6eab4d489d055404e26ad81afe52fc) Thanks @fubhy! - Added additional pure annotations to improve tree-shakeability + +## 3.5.6 + +### Patch Changes + +- [#3294](https://github.com/Effect-TS/effect/pull/3294) [`cc327a1`](https://github.com/Effect-TS/effect/commit/cc327a1bccd22a4ee27ec7e58b53205e93b23e2c) Thanks @tim-smart! - correctly exclude symbols from Record.keys + +- [#3289](https://github.com/Effect-TS/effect/pull/3289) [`4bfe4fb`](https://github.com/Effect-TS/effect/commit/4bfe4fb5c82f597c9beea9baa92e772593598b60) Thanks @dilame! - Changed `Stream.groupByKey`/`Stream.grouped`/`Stream.groupedWithin` JSDoc category from `utils` to `grouping` + +- [#3295](https://github.com/Effect-TS/effect/pull/3295) [`2b14d18`](https://github.com/Effect-TS/effect/commit/2b14d181462cad8359da4fa6bc6dfda0f742c398) Thanks @tim-smart! - fix YieldableError rendering on bun + +## 3.5.5 + +### Patch Changes + +- [#3266](https://github.com/Effect-TS/effect/pull/3266) [`a9d7800`](https://github.com/Effect-TS/effect/commit/a9d7800f6a253192b653d77778b0674f39b1ca39) Thanks @tim-smart! - use "unbounded" buffer for Stream.fromEventListener + +## 3.5.4 + +### Patch Changes + +- [#3253](https://github.com/Effect-TS/effect/pull/3253) [`ed0dde4`](https://github.com/Effect-TS/effect/commit/ed0dde4888e6f1a97ad5bba06b755d26a6a1c52e) Thanks @tim-smart! - update dependencies + +- [#3247](https://github.com/Effect-TS/effect/pull/3247) [`ca775ce`](https://github.com/Effect-TS/effect/commit/ca775cec53baebc1a43d9b8852a3ac6726178498) Thanks @tim-smart! - if performance.timeOrigin is 0, use performance.now() directly in Clock + + This is a workaround for cloudflare, where performance.now() cannot be used in + the global scope to calculate the origin. + +- [#3259](https://github.com/Effect-TS/effect/pull/3259) [`5be9cc0`](https://github.com/Effect-TS/effect/commit/5be9cc044025a9541b9b7acefa2d3fc05fa1301b) Thanks @IMax153! - expose `Channel.isChannel` + +- [#3250](https://github.com/Effect-TS/effect/pull/3250) [`203658f`](https://github.com/Effect-TS/effect/commit/203658f8001c132b25764ab70344b171683b554c) Thanks @gcanti! - add support for `Refinement`s to `Predicate.or`, closes #3243 + + ```ts + import { Predicate } from "effect" + + // Refinement + const isStringOrNumber = Predicate.or(Predicate.isString, Predicate.isNumber) + ``` + +- [#3246](https://github.com/Effect-TS/effect/pull/3246) [`eb1c4d4`](https://github.com/Effect-TS/effect/commit/eb1c4d44e54b9d8d201a366d1ff94face2a6dcd3) Thanks @tim-smart! - render nested causes in Cause.pretty + +## 3.5.3 + +### Patch Changes + +- [#3234](https://github.com/Effect-TS/effect/pull/3234) [`edb0da3`](https://github.com/Effect-TS/effect/commit/edb0da383746d760f35d8582f5fb0cc0eeca9217) Thanks @tim-smart! - do not add a error "cause" if the upstream error does not contain one + +- [#3236](https://github.com/Effect-TS/effect/pull/3236) [`c8d3fb0`](https://github.com/Effect-TS/effect/commit/c8d3fb0fe23585f6efb724af51fbab3ba1ad6e83) Thanks @tim-smart! - set Logger.pretty message color to deepskyblue on browsers + +- [#3240](https://github.com/Effect-TS/effect/pull/3240) [`dabd028`](https://github.com/Effect-TS/effect/commit/dabd028decf9b7983ca16ebe0f48c05c11a84b68) Thanks @tim-smart! - fix process .isTTY detection + +- [#3230](https://github.com/Effect-TS/effect/pull/3230) [`786b2ab`](https://github.com/Effect-TS/effect/commit/786b2ab29d525c877bb84035dac9e2d6499339d1) Thanks @KhraksMamtsov! - support heterogenous argument in `Option.firstSomeOf` + +- [#3238](https://github.com/Effect-TS/effect/pull/3238) [`fc57354`](https://github.com/Effect-TS/effect/commit/fc573547d41667016fce05eaee75960fcc6dce4d) Thanks @leonitousconforti! - Align Stream.run public function signatures + +## 3.5.2 + +### Patch Changes + +- [#3228](https://github.com/Effect-TS/effect/pull/3228) [`639208e`](https://github.com/Effect-TS/effect/commit/639208eeb8a44622994f832bc2d45d06ab636bc8) Thanks @IMax153! - Render a more helpful error message when timing out an effect + +- [#3235](https://github.com/Effect-TS/effect/pull/3235) [`6684b4c`](https://github.com/Effect-TS/effect/commit/6684b4c27d77a7fcc7af2e261a450edf971b62b5) Thanks @tim-smart! - improve safari support for Logger.pretty + +- [#3235](https://github.com/Effect-TS/effect/pull/3235) [`6684b4c`](https://github.com/Effect-TS/effect/commit/6684b4c27d77a7fcc7af2e261a450edf971b62b5) Thanks @tim-smart! - fix span stack rendering when stack function returns undefined + +- [#3235](https://github.com/Effect-TS/effect/pull/3235) [`6684b4c`](https://github.com/Effect-TS/effect/commit/6684b4c27d77a7fcc7af2e261a450edf971b62b5) Thanks @tim-smart! - align UnsafeConsole group types with web apis + +## 3.5.1 + +### Patch Changes + +- [#3220](https://github.com/Effect-TS/effect/pull/3220) [`55fdd76`](https://github.com/Effect-TS/effect/commit/55fdd761ee95afd73b6a892c13fee92b36c02837) Thanks @tim-smart! - fix Logger.pretty on bun + +## 3.5.0 + +### Minor Changes + +- [#3048](https://github.com/Effect-TS/effect/pull/3048) [`a1f5b83`](https://github.com/Effect-TS/effect/commit/a1f5b831a1bc7535988b370d68d0b3eb1123e0ce) Thanks @tim-smart! - add renderErrorCause option to Cause.pretty + +- [#3048](https://github.com/Effect-TS/effect/pull/3048) [`60bc3d0`](https://github.com/Effect-TS/effect/commit/60bc3d0867b13e48b24dc22604b4dd2e7b2c1ca4) Thanks @tim-smart! - add RcRef module + + An `RcRef` wraps a reference counted resource that can be acquired and released multiple times. + + The resource is lazily acquired on the first call to `get` and released when the last reference is released. + + ```ts + import { Effect, RcRef } from "effect" + + Effect.gen(function* () { + const ref = yield* RcRef.make({ + acquire: Effect.acquireRelease(Effect.succeed("foo"), () => + Effect.log("release foo") + ) + }) + + // will only acquire the resource once, and release it + // when the scope is closed + yield* RcRef.get(ref).pipe(Effect.andThen(RcRef.get(ref)), Effect.scoped) + }) + ``` + +- [#3048](https://github.com/Effect-TS/effect/pull/3048) [`5ab348f`](https://github.com/Effect-TS/effect/commit/5ab348f265db3d283aa091ddca6d2d49137c16f2) Thanks @tim-smart! - allowing customizing Stream pubsub strategy + + ```ts + import { Schedule, Stream } from "effect" + + // toPubSub + Stream.fromSchedule(Schedule.spaced(1000)).pipe( + Stream.toPubSub({ + capacity: 16, // or "unbounded" + strategy: "dropping" // or "sliding" / "suspend" + }) + ) + + // also for the broadcast apis + Stream.fromSchedule(Schedule.spaced(1000)).pipe( + Stream.broadcastDynamic({ + capacity: 16, + strategy: "dropping" + }) + ) + ``` + +- [#3048](https://github.com/Effect-TS/effect/pull/3048) [`60bc3d0`](https://github.com/Effect-TS/effect/commit/60bc3d0867b13e48b24dc22604b4dd2e7b2c1ca4) Thanks @tim-smart! - add Duration.isZero, for checking if a Duration is zero + +- [#3048](https://github.com/Effect-TS/effect/pull/3048) [`3e04bf8`](https://github.com/Effect-TS/effect/commit/3e04bf8a7127e956cadb7684a8f4c661df57663b) Thanks @sukovanej! - Add `Success` type util for `Config`. + +- [#3048](https://github.com/Effect-TS/effect/pull/3048) [`e7fc45f`](https://github.com/Effect-TS/effect/commit/e7fc45f0c7002aafdaec7878149ac064cd104ea3) Thanks @tim-smart! - add Logger.prettyLogger and Logger.pretty + + `Logger.pretty` is a new logger that leverages the features of the `console` APIs to provide a more visually appealing output. + + To try it out, provide it to your program: + + ```ts + import { Effect, Logger } from "effect" + + Effect.log("Hello, World!").pipe(Effect.provide(Logger.pretty)) + ``` + +- [#3048](https://github.com/Effect-TS/effect/pull/3048) [`a1f5b83`](https://github.com/Effect-TS/effect/commit/a1f5b831a1bc7535988b370d68d0b3eb1123e0ce) Thanks @tim-smart! - add .groupCollapsed to UnsafeConsole + +- [#3048](https://github.com/Effect-TS/effect/pull/3048) [`4626de5`](https://github.com/Effect-TS/effect/commit/4626de59c25b384216faa0be87bf0b8cd36357d0) Thanks @giacomoran! - export Random.make taking hashable values as seed + +- [#3048](https://github.com/Effect-TS/effect/pull/3048) [`f01e7db`](https://github.com/Effect-TS/effect/commit/f01e7db317827255d7901f523f2e28b43298e8df) Thanks @tim-smart! - add `replay` option to PubSub constructors + + This option adds a replay buffer in front of the given PubSub. The buffer will + replay the last `n` messages to any new subscriber. + + ```ts + Effect.gen(function*() { + const messages = [1, 2, 3, 4, 5] + const pubsub = yield* PubSub.bounded({ capacity: 16, replay: 3 }) + yield* PubSub.publishAll(pubsub, messages) + const sub = yield* PubSub.subscribe(pubsub) + assert.deepStrictEqual(Chunk.toReadonlyArray(yield* Queue.takeAll(sub)), [3, 4, 5]) + })) + ``` + +- [#3048](https://github.com/Effect-TS/effect/pull/3048) [`60bc3d0`](https://github.com/Effect-TS/effect/commit/60bc3d0867b13e48b24dc22604b4dd2e7b2c1ca4) Thanks @tim-smart! - add RcMap module + + An `RcMap` can contain multiple reference counted resources that can be indexed + by a key. The resources are lazily acquired on the first call to `get` and + released when the last reference is released. + + Complex keys can extend `Equal` and `Hash` to allow lookups by value. + + ```ts + import { Effect, RcMap } from "effect" + + Effect.gen(function* () { + const map = yield* RcMap.make({ + lookup: (key: string) => + Effect.acquireRelease(Effect.succeed(`acquired ${key}`), () => + Effect.log(`releasing ${key}`) + ) + }) + + // Get "foo" from the map twice, which will only acquire it once + // It will then be released once the scope closes. + yield* RcMap.get(map, "foo").pipe( + Effect.andThen(RcMap.get(map, "foo")), + Effect.scoped + ) + }) + ``` + +- [#3048](https://github.com/Effect-TS/effect/pull/3048) [`ac71f37`](https://github.com/Effect-TS/effect/commit/ac71f378f2413e5aa91c95f649ffe898d6a26114) Thanks @dilame! - Ensure `Scope` is excluded from `R` in the `Channel` / `Stream` `run*` functions. + + This fix ensures that `Scope` is now properly excluded from the resulting effect environment. + The affected functions include `run`, `runCollect`, `runCount`, `runDrain` and other non-scoped `run*` in both `Stream` and `Channel` modules. + This fix brings the type declaration in line with the runtime implementation. + +- [#3048](https://github.com/Effect-TS/effect/pull/3048) [`8432360`](https://github.com/Effect-TS/effect/commit/8432360ce68614a419bb328083a4109d0fc8aa93) Thanks @dilame! - refactor(Stream/mergeLeft): rename `self`/`that` argument names to `left`/`right` for clarity + + refactor(Stream/mergeRight): rename `self`/`that` argument names to `left`/`right` for clarity + +- [#3048](https://github.com/Effect-TS/effect/pull/3048) [`e4bf1bf`](https://github.com/Effect-TS/effect/commit/e4bf1bf2b4a970eacd77c9b77b5ea8c68bc84498) Thanks @dilame! - feat(Stream): implement "raceAll" operator, which returns a stream that mirrors the first source stream to emit an item. + + ```ts + import { Stream, Schedule, Console, Effect } from "effect" + + const stream = Stream.raceAll( + Stream.fromSchedule(Schedule.spaced("1 millis")), + Stream.fromSchedule(Schedule.spaced("2 millis")), + Stream.fromSchedule(Schedule.spaced("4 millis")) + ).pipe(Stream.take(6), Stream.tap(Console.log)) + + Effect.runPromise(Stream.runDrain(stream)) + // Output only from the first stream, the rest streams are interrupted + // 0 + // 1 + // 2 + // 3 + // 4 + // 5 + ``` + +- [#3048](https://github.com/Effect-TS/effect/pull/3048) [`13cb861`](https://github.com/Effect-TS/effect/commit/13cb861a5eded15c55c6cdcf6a8acde8320367a6) Thanks @dilame! - refactor(Stream): use new built-in `Types.TupleOf` instead of `Stream.DynamicTuple` and deprecate it + +- [#3048](https://github.com/Effect-TS/effect/pull/3048) [`79d2d91`](https://github.com/Effect-TS/effect/commit/79d2d91464d95dde0e9444d43e7a7f309f05d6e6) Thanks @tim-smart! - support ErrorOptions in YieldableError constructor + +- [#3048](https://github.com/Effect-TS/effect/pull/3048) [`9f66825`](https://github.com/Effect-TS/effect/commit/9f66825f1fce0fe8d10420c285f7dc4c71e8af8d) Thanks @tim-smart! - allow customizing the output buffer for the Stream.async\* apis + + ```ts + import { Stream } from "effect" + + Stream.async( + (emit) => { + // ... + }, + { + bufferSize: 16, + strategy: "dropping" // you can also use "sliding" or "suspend" + } + ) + ``` + +### Patch Changes + +- [#3048](https://github.com/Effect-TS/effect/pull/3048) [`a1f5b83`](https://github.com/Effect-TS/effect/commit/a1f5b831a1bc7535988b370d68d0b3eb1123e0ce) Thanks @tim-smart! - include Error.cause stack in log output + +- [#3048](https://github.com/Effect-TS/effect/pull/3048) [`a1f5b83`](https://github.com/Effect-TS/effect/commit/a1f5b831a1bc7535988b370d68d0b3eb1123e0ce) Thanks @tim-smart! - set stackTraceLimit to 1 in PrettyError to address performance issues + +- [#3048](https://github.com/Effect-TS/effect/pull/3048) [`79d2d91`](https://github.com/Effect-TS/effect/commit/79d2d91464d95dde0e9444d43e7a7f309f05d6e6) Thanks @tim-smart! - ensure "cause" is rendered in Data.Error output + +- [#3048](https://github.com/Effect-TS/effect/pull/3048) [`e7fc45f`](https://github.com/Effect-TS/effect/commit/e7fc45f0c7002aafdaec7878149ac064cd104ea3) Thanks @tim-smart! - fix types of UnsafeConsole.group + +## 3.4.9 + +### Patch Changes + +- [#3210](https://github.com/Effect-TS/effect/pull/3210) [`7af137c`](https://github.com/Effect-TS/effect/commit/7af137c9433f6e74959b3887561ec1e6f12e10ee) Thanks @tim-smart! - prevent reclaim of manually invalidated pool items + +- [#3204](https://github.com/Effect-TS/effect/pull/3204) [`ee4b3dc`](https://github.com/Effect-TS/effect/commit/ee4b3dc5f68d19dc3ae1c2d12901c5b8ffbebabb) Thanks @gcanti! - Updated the JSDocs for the `Stream` module by adding examples to key functions. + +- [#3202](https://github.com/Effect-TS/effect/pull/3202) [`097d25c`](https://github.com/Effect-TS/effect/commit/097d25cb5d13c049e01789651be56b09620186ef) Thanks @tim-smart! - allow invalidated Pool items to be reclaimed with usage strategy + +## 3.4.8 + +### Patch Changes + +- [#3181](https://github.com/Effect-TS/effect/pull/3181) [`a435e0f`](https://github.com/Effect-TS/effect/commit/a435e0fc5378b33a49bcec92ee235df6f16a2419) Thanks @KhraksMamtsov! - refactor `TrimEnd` & `TrimStart` + +- [#3176](https://github.com/Effect-TS/effect/pull/3176) [`b5554db`](https://github.com/Effect-TS/effect/commit/b5554db36c4dd6f64fa5e6a62a29b2759c54217a) Thanks @tim-smart! - allow Stream run fiber to close before trying to interrupt it + +- [#3175](https://github.com/Effect-TS/effect/pull/3175) [`a9c4fb3`](https://github.com/Effect-TS/effect/commit/a9c4fb3bf3c6e92cd1c142b0605fddf7eb3c697c) Thanks @tim-smart! - ensure fibers are interrupted in Stream.mergeWith + +## 3.4.7 + +### Patch Changes + +- [#3161](https://github.com/Effect-TS/effect/pull/3161) [`a5737d6`](https://github.com/Effect-TS/effect/commit/a5737d6db2b921605c332eabbc5402ee3d17357b) Thanks @tim-smart! - ensure PubSub.publishAll does not increase size while there are no subscribers + +## 3.4.6 + +### Patch Changes + +- [#3096](https://github.com/Effect-TS/effect/pull/3096) [`5c0ceb0`](https://github.com/Effect-TS/effect/commit/5c0ceb00826cce9e50bf9d41d83e191d5352c030) Thanks @gcanti! - Micro: align with `Effect` module (renamings and new combinators). + + General naming convention rule: ``. + - `Failure` -> `MicroCause` + - `Failure.Expected` -> `MicroCause.Fail` + - `Failure.Unexpected` -> `MicroCause.Die` + - `Failure.Aborted` -> `MicroCause.Interrupt` + - `FailureExpected` -> `causeFail` + - `FailureUnexpected` -> `causeDie` + - `FailureAborted` -> `causeInterrupt` + - `failureIsExpected` -> `causeIsFail` + - `failureIsExpected` -> `causeIsFail` + - `failureIsUnexpected` -> `causeIsDie` + - `failureIsAborted` -> `causeIsInterrupt` + - `failureSquash` -> `causeSquash` + - `failureWithTrace` -> `causeWithTrace` + - `Result` -> `MicroExit` + - `ResultAborted` -> `exitInterrupt` + - `ResultSuccess` -> `exitSucceed` + - `ResultFail` -> `exitFail` + - `ResultFailUnexpected` -> `exitDie` + - `ResultFailWith` -> `exitFailCause` + - `resultIsSuccess` -> `exitIsSuccess` + - `resultIsFailure` -> `exitIsFailure` + - `resultIsAborted` -> `exitIsInterrupt` + - `resultIsFailureExpected` -> `exitIsFail` + - `resultIsFailureUnexpected` -> `exitIsDie` + - `resultVoid` -> `exitVoid` + - `DelayFn` -> `MicroSchedule` + - `delayExponential` -> `scheduleExponential` + - `delaySpaced` -> `scheduleSpaced` + - `delayWithMax` -> `scheduleWithMaxDelay` + - `delayWithMaxElapsed` -> `scheduleWithMaxElapsed` + - `delayWithRecurs` -> `scheduleRecurs` and make it a constructor + - add `scheduleAddDelay` combinator + - add `scheduleUnion` combinator + - add `scheduleIntersect` combinator + - `Handle` + - `abort` -> `interrupt` + - `unsafeAbort` -> `unsafeInterrupt` + - `provideServiceMicro` -> `provideServiceEffect` + - `fromResult` -> `fromExit` + - `fromResultSync` -> `fromExitSync` + - `failWith` -> `failCause` + - `failWithSync` -> `failCauseSync` + - `asResult` -> `exit` + - `filterOrFailWith` -> `filterOrFailCause` + - `repeatResult` -> `repeatExit` + - `catchFailure` -> `catchAllCause` + - `catchFailureIf` -> `catchCauseIf` + - `catchExpected` -> `catchAll` + - `catchUnexpected` -> `catchAllDefect` + - `tapFailure` -> `tapErrorCause` + - `tapFailureIf` -> `tapErrorCauseIf` + - `tapExpected` -> `tapError` + - `tapUnexpected` -> `tapDefect` + - `mapFailure` -> `mapErrorCause` + - `matchFailureMicro` -> `matchCauseEffect` + - `matchFailure` -> `matchCause` + - `matchMicro` -> `matchEffect` + - `onResult` -> `onExit` + - `onResultIf` -> `onExitIf` + - `onFailure` -> `onError` + - `onAbort` -> `onInterrupt` + - `abort` -> `interrupt` + - `runPromiseResult` -> `runPromiseExit` + - `runSyncResult` -> `runSyncExit` + - rename `delay` option to `schedule` + +- [#3096](https://github.com/Effect-TS/effect/pull/3096) [`5c0ceb0`](https://github.com/Effect-TS/effect/commit/5c0ceb00826cce9e50bf9d41d83e191d5352c030) Thanks @gcanti! - Micro: rename `timeout` to `timeoutOption`, and add a `timeout` that fails with a `TimeoutException` + +- [#3121](https://github.com/Effect-TS/effect/pull/3121) [`33735b1`](https://github.com/Effect-TS/effect/commit/33735b16b41bd26929d8f4754c190925db6323b7) Thanks @KhraksMamtsov! - Support for the tacit usage of external handlers for `Match.tag` and `Match.tagStartsWith` functions + + ```ts + type Value = { _tag: "A"; a: string } | { _tag: "B"; b: number } + const handlerA = (_: { _tag: "A"; a: number }) => _.a + + // $ExpectType string | number + pipe( + M.type(), + M.tag("A", handlerA), // <-- no type issue + M.orElse((_) => _.b) + )(value) + ``` + +- [#3096](https://github.com/Effect-TS/effect/pull/3096) [`5c0ceb0`](https://github.com/Effect-TS/effect/commit/5c0ceb00826cce9e50bf9d41d83e191d5352c030) Thanks @gcanti! - Micro: move MicroExit types to a namespace + +- [#3134](https://github.com/Effect-TS/effect/pull/3134) [`139d4b3`](https://github.com/Effect-TS/effect/commit/139d4b39fb3bff2eeaa7c0c809c581da42425a83) Thanks @tim-smart! - use Channel.acquireUseRelease for Channel.withSpan + +## 3.4.5 + +### Patch Changes + +- [#3099](https://github.com/Effect-TS/effect/pull/3099) [`a047af9`](https://github.com/Effect-TS/effect/commit/a047af99447dfffc729e9c8ef0ca143537927e91) Thanks @tim-smart! - fix using unions with Match.withReturnType + +## 3.4.4 + +### Patch Changes + +- [#3083](https://github.com/Effect-TS/effect/pull/3083) [`72638e3`](https://github.com/Effect-TS/effect/commit/72638e3d99f0e93a24febf6c225256ce92d4a20b) Thanks @gcanti! - Micro: add `NoSuchElementException` error and update `fromOption` to change the failure type from `Option.None` to `NoSuchElementException` + +- [#3095](https://github.com/Effect-TS/effect/pull/3095) [`d7dde2b`](https://github.com/Effect-TS/effect/commit/d7dde2b4af08b37af859d4c327c1f5c6f00cf9d9) Thanks @tim-smart! - remove global AbortController from Micro + +- [#3085](https://github.com/Effect-TS/effect/pull/3085) [`9b2fc3b`](https://github.com/Effect-TS/effect/commit/9b2fc3b9dfd304a2bd0508ef2313cfc54357be0c) Thanks @gcanti! - Micro: add `zipWith` + +## 3.4.3 + +### Patch Changes + +- [#3065](https://github.com/Effect-TS/effect/pull/3065) [`c342739`](https://github.com/Effect-TS/effect/commit/c3427396226e1ad7b95b40595a23f9bdff3e3365) Thanks @KhraksMamtsov! - Support `this` argument for `Micro.gen` + +- [#3067](https://github.com/Effect-TS/effect/pull/3067) [`8898e5e`](https://github.com/Effect-TS/effect/commit/8898e5e238622f6337583d91ee23609c1f5ccdf7) Thanks @KhraksMamtsov! - Cleanup signal "abort" event handler in `Micro.runFork` + +- [#3082](https://github.com/Effect-TS/effect/pull/3082) [`ff78636`](https://github.com/Effect-TS/effect/commit/ff786367c522975f40f0f179a0ecdfcfab7ecbdb) Thanks @gcanti! - Align the `Micro.catchIf` signature with `Effect.catchIf` + +- [#3078](https://github.com/Effect-TS/effect/pull/3078) [`c86bd4e`](https://github.com/Effect-TS/effect/commit/c86bd4e134c23146c216f9ff97e03781d55991b6) Thanks @KhraksMamtsov! - Support unification for Micro module + +- [#3079](https://github.com/Effect-TS/effect/pull/3079) [`bbdd365`](https://github.com/Effect-TS/effect/commit/bbdd36567706c94cdec45bacea825941c347b6cd) Thanks @tim-smart! - update to typescript 5.5 + +## 3.4.2 + +### Patch Changes + +- [#3062](https://github.com/Effect-TS/effect/pull/3062) [`3da1497`](https://github.com/Effect-TS/effect/commit/3da1497b5c9cc886d300258bc928fd68a4fefe6f) Thanks @KhraksMamtsov! - Reuse centralized do-notation code + +## 3.4.1 + +### Patch Changes + +- [#3056](https://github.com/Effect-TS/effect/pull/3056) [`66a1910`](https://github.com/Effect-TS/effect/commit/66a19109ff90c4252123b8809b8c8a74681dba6a) Thanks @gcanti! - add missing `TypeLambda` to `Micro` module + +## 3.4.0 + +### Minor Changes + +- [#2938](https://github.com/Effect-TS/effect/pull/2938) [`c0ce180`](https://github.com/Effect-TS/effect/commit/c0ce180861ad0938053c0e6145e813fa6404df3b) Thanks @LaureRC! - Make Option.liftPredicate dual + +- [#2938](https://github.com/Effect-TS/effect/pull/2938) [`61707b6`](https://github.com/Effect-TS/effect/commit/61707b6ffc7397c2ba0dce22512b44955724f60f) Thanks @LaureRC! - Add Effect.liftPredicate + + `Effect.liftPredicate` transforms a `Predicate` function into an `Effect` returning the input value if the predicate returns `true` or failing with specified error if the predicate fails. + + ```ts + import { Effect } from "effect" + + const isPositive = (n: number): boolean => n > 0 + + // succeeds with `1` + Effect.liftPredicate(1, isPositive, (n) => `${n} is not positive`) + + // fails with `"0 is not positive"` + Effect.liftPredicate(0, isPositive, (n) => `${n} is not positive`) + ``` + +- [#2938](https://github.com/Effect-TS/effect/pull/2938) [`9c1b5b3`](https://github.com/Effect-TS/effect/commit/9c1b5b39e6c19604ce834f072a114ad392c50a06) Thanks @tim-smart! - add EventListener type to Stream to avoid use of dom lib + +- [#2938](https://github.com/Effect-TS/effect/pull/2938) [`a35faf8`](https://github.com/Effect-TS/effect/commit/a35faf8d116f94899bfc03feab33b004c8ddfdf7) Thanks @gcanti! - Add `lastNonEmpty` function to `Chunk` module, closes #2946 + +- [#2938](https://github.com/Effect-TS/effect/pull/2938) [`ff73c0c`](https://github.com/Effect-TS/effect/commit/ff73c0cacd66132bfad2e5211b3eae347729c667) Thanks @dilame! - feat(Stream): implement Success, Error, Context type accessors + +- [#2938](https://github.com/Effect-TS/effect/pull/2938) [`984d516`](https://github.com/Effect-TS/effect/commit/984d516ccd9412dc41188f6a46b748dd20dd5848) Thanks @tim-smart! - add Micro module + + A lightweight alternative to Effect, for when bundle size really matters. + + At a minimum, Micro adds 5kb gzipped to your bundle, and scales with the amount + of features you use. + +- [#2938](https://github.com/Effect-TS/effect/pull/2938) [`8c3b8a2`](https://github.com/Effect-TS/effect/commit/8c3b8a2ce208eab753b6206a51605a424f104e98) Thanks @gcanti! - add `ManagedRuntime` type utils (`Context`, and `Error`) + +- [#2938](https://github.com/Effect-TS/effect/pull/2938) [`017e2f9`](https://github.com/Effect-TS/effect/commit/017e2f9b371ce24ea4945e5d7390c934ad3c39cf) Thanks @LaureRC! - Add Either.liftPredicate + +- [#2938](https://github.com/Effect-TS/effect/pull/2938) [`91bf8a2`](https://github.com/Effect-TS/effect/commit/91bf8a2e9d1959393b3cf7366cc1d584d3e666b7) Thanks @msensys! - Add `Tuple.at` api, to retrieve an element at a specified index from a tuple. + + ```ts + import { Tuple } from "effect" + + assert.deepStrictEqual(Tuple.at([1, "hello", true], 1), "hello") + ``` + +- [#2938](https://github.com/Effect-TS/effect/pull/2938) [`c6a4a26`](https://github.com/Effect-TS/effect/commit/c6a4a266606575fd2c7165940c4072ad4c57d01f) Thanks @datner! - add `ensure` util for Array, used to normalize `A | ReadonlyArray` + + ```ts + import { ensure } from "effect/Array" + + // lets say you are not 100% sure if it's a member or a collection + declare const someValue: { foo: string } | Array<{ foo: string }> + + // $ExpectType ({ foo: string })[] + const normalized = ensure(someValue) + ``` + +## 3.3.5 + +### Patch Changes + +- [#3012](https://github.com/Effect-TS/effect/pull/3012) [`6c89408`](https://github.com/Effect-TS/effect/commit/6c89408cd7b9204ec4c5828a46cd5312d8afb5e7) Thanks @tim-smart! - ensure Config.Wrap only destructures plain objects + +## 3.3.4 + +### Patch Changes + +- [#3001](https://github.com/Effect-TS/effect/pull/3001) [`a67b8fe`](https://github.com/Effect-TS/effect/commit/a67b8fe2ace08419424811b5f0d9a5378eaea352) Thanks @tim-smart! - use Math.random for Hash.random + +## 3.3.3 + +### Patch Changes + +- [#2999](https://github.com/Effect-TS/effect/pull/2999) [`06ede85`](https://github.com/Effect-TS/effect/commit/06ede85d6e84710e6622463be95ff3927fb30dad) Thanks @KhraksMamtsov! - Added tests for `Chunk.toArray` and `Chunk.toReadonlyArray` with use cases in the `pipe` + +- [#3000](https://github.com/Effect-TS/effect/pull/3000) [`7204ca5`](https://github.com/Effect-TS/effect/commit/7204ca5761c2b1d27999a624db23aa10b6e0504d) Thanks @tim-smart! - fix support for Predicates in Predicate.compose + +## 3.3.2 + +### Patch Changes + +- [#2981](https://github.com/Effect-TS/effect/pull/2981) [`3572646`](https://github.com/Effect-TS/effect/commit/3572646d5e0804f85bc7f64633fb95722533f9dd) Thanks @tim-smart! - ensure multiline error messages are preserved in cause rendering + +- [#2970](https://github.com/Effect-TS/effect/pull/2970) [`1aed347`](https://github.com/Effect-TS/effect/commit/1aed347a125ed3847ec90863424810d6759cbc85) Thanks @gcanti! - Updated `Chunk.toArray` and `Chunk.toReadonlyArray`. Improved function signatures to preserve non-empty status of chunks during conversion. + +- [#2977](https://github.com/Effect-TS/effect/pull/2977) [`df4bf4b`](https://github.com/Effect-TS/effect/commit/df4bf4b62e7b316c6647da0271fc5544a84e7ba2) Thanks @tim-smart! - fix discard option in Effect.all + +- [#2917](https://github.com/Effect-TS/effect/pull/2917) [`f085f92`](https://github.com/Effect-TS/effect/commit/f085f92dfa204afb41823ffc27d437225137643d) Thanks @mikearnaldi! - Fix Unify for Stream + +## 3.3.1 + +### Patch Changes + +- [#2952](https://github.com/Effect-TS/effect/pull/2952) [`eb98c5b`](https://github.com/Effect-TS/effect/commit/eb98c5b79ab50aa0cde239bd4e660dd19dbab612) Thanks @KhraksMamtsov! - Change `Config.array` to return `Array` instead of `ReadonlyArray` + +- [#2950](https://github.com/Effect-TS/effect/pull/2950) [`184fed8`](https://github.com/Effect-TS/effect/commit/184fed83ac36cba05a75a5a8013f740f9f696e3b) Thanks @gcanti! - Ensure `Chunk.reverse` preserves `NonEmpty` status, closes #2947 + +- [#2954](https://github.com/Effect-TS/effect/pull/2954) [`6068e07`](https://github.com/Effect-TS/effect/commit/6068e073d4cc8b3c8583583fd5eb3efe43f7d5ba) Thanks @jessekelly881! - Fix runtime error in `Struct.evolve` by enhancing compile-time checks, closes #2953 + +- [#2948](https://github.com/Effect-TS/effect/pull/2948) [`3a77e20`](https://github.com/Effect-TS/effect/commit/3a77e209783933bac3aaddba1b05ff6a9ac72b36) Thanks @gcanti! - Remove unnecessary `===` comparison in `getEquivalence` functions + + In some `getEquivalence` functions that use `make`, there is an unnecessary `===` comparison. The `make` function already handles this comparison. + +## 3.3.0 + +### Minor Changes + +- [#2837](https://github.com/Effect-TS/effect/pull/2837) [`1f4ac00`](https://github.com/Effect-TS/effect/commit/1f4ac00a91c336c9c9c9b8c3ed9ceb9920ebc9bd) Thanks @dilame! - add `Stream.zipLatestAll` api + +- [#2837](https://github.com/Effect-TS/effect/pull/2837) [`9305b76`](https://github.com/Effect-TS/effect/commit/9305b764cceeae4f16564435ae7172f79c2bf822) Thanks @mattrossman! - Add queuing strategy option for Stream.toReadableStream + +- [#2837](https://github.com/Effect-TS/effect/pull/2837) [`0f40d98`](https://github.com/Effect-TS/effect/commit/0f40d989da10f68df3ecd72b36849401ad679bfb) Thanks @tim-smart! - add `timeToLiveStrategy` to `Pool` options + + The `timeToLiveStrategy` determines how items are invalidated. If set to + "creation", then items are invalidated based on their creation time. If set + to "usage", then items are invalidated based on pool usage. + + By default, the `timeToLiveStrategy` is set to "usage". + +- [#2837](https://github.com/Effect-TS/effect/pull/2837) [`b761ef0`](https://github.com/Effect-TS/effect/commit/b761ef00eaf6c67b7ffe34798b98aae5347ab376) Thanks @tim-smart! - add Layer.annotateLogs & Layer.annotateSpans + + This allows you to add log & span annotation to a Layer. + + ```ts + import { Effect, Layer } from "effect" + + Layer.effectDiscard(Effect.log("hello")).pipe( + Layer.annotateLogs({ + service: "my-service" + }) + ) + ``` + +- [#2837](https://github.com/Effect-TS/effect/pull/2837) [`b53f69b`](https://github.com/Effect-TS/effect/commit/b53f69bff1452a487b21198cd83961f844e02d36) Thanks @dilame! - Types: implement `TupleOf` and `TupleOfAtLeast` types + + Predicate: implement `isTupleOf` and `isTupleOfAtLeast` type guards + +- [#2837](https://github.com/Effect-TS/effect/pull/2837) [`0f40d98`](https://github.com/Effect-TS/effect/commit/0f40d989da10f68df3ecd72b36849401ad679bfb) Thanks @tim-smart! - add `concurrency` & `targetUtilization` option to `Pool.make` & `Pool.makeWithTTL` + + This option allows you to specify the level of concurrent access per pool item. + I.e. setting `concurrency: 2` will allow each pool item to be in use by 2 concurrent tasks. + + `targetUtilization` determines when to create new pool items. It is a value + between 0 and 1, where 1 means only create new pool items when all the existing + items are fully utilized. + + A `targetUtilization` of 0.5 will create new pool items when the existing items are + 50% utilized. + +- [#2837](https://github.com/Effect-TS/effect/pull/2837) [`5bd549e`](https://github.com/Effect-TS/effect/commit/5bd549e4bd7144727db438ecca6b8dc9b3ef7e22) Thanks @KhraksMamtsov! - Support `this` argument for `{STM, Either, Option}.gen` + +- [#2837](https://github.com/Effect-TS/effect/pull/2837) [`67f160a`](https://github.com/Effect-TS/effect/commit/67f160a213de0219a565d4bf653b3cbf24f58e8f) Thanks @KhraksMamtsov! - Introduced `Redacted` module - `Secret` generalization + `Secret extends Redacted` + The use of the `Redacted` has been replaced by the use of the `Redacted` in packages with version `0.*.*` + +## 3.2.9 + +### Patch Changes + +- [#2921](https://github.com/Effect-TS/effect/pull/2921) [`8c5d280`](https://github.com/Effect-TS/effect/commit/8c5d280c0402284a4e58372867a15a431cb99461) Thanks @tim-smart! - remove usage of performance.timeOrigin + +- [#2912](https://github.com/Effect-TS/effect/pull/2912) [`6ba6d26`](https://github.com/Effect-TS/effect/commit/6ba6d269f5891e6b11aa35c5281dde4bf3273004) Thanks @mikearnaldi! - Remove toJSON from PrettyError and fix message generation + +- [#2923](https://github.com/Effect-TS/effect/pull/2923) [`3f28bf2`](https://github.com/Effect-TS/effect/commit/3f28bf274333611906175446b772243f34f1b6d5) Thanks @tim-smart! - only wrap objects with string keys in Config.Wrap + +- [#2914](https://github.com/Effect-TS/effect/pull/2914) [`5817820`](https://github.com/Effect-TS/effect/commit/58178204a770d1a78c06945ef438f9fffbb50afa) Thanks @mikearnaldi! - Fix id extraction in Context.Tag.Identifier + +## 3.2.8 + +### Patch Changes + +- [#2894](https://github.com/Effect-TS/effect/pull/2894) [`fb91f17`](https://github.com/Effect-TS/effect/commit/fb91f17098b48497feca9ec976feb87e4a82451b) Thanks @mikearnaldi! - ensure Equal considers Date by value + +## 3.2.7 + +### Patch Changes + +- [#2887](https://github.com/Effect-TS/effect/pull/2887) [`6801fca`](https://github.com/Effect-TS/effect/commit/6801fca44366be3ee1b6b99f54bd4f38a1b5e4f4) Thanks @mikearnaldi! - Ensure provide of runtime is additive on context + +## 3.2.6 + +### Patch Changes + +- [#2879](https://github.com/Effect-TS/effect/pull/2879) [`cc8ac50`](https://github.com/Effect-TS/effect/commit/cc8ac5080daba8622ca2ff5dab5c37ddfab732ba) Thanks @TylorS! - Support tuples in Types.DeepMutable + +## 3.2.5 + +### Patch Changes + +- [#2823](https://github.com/Effect-TS/effect/pull/2823) [`608b01f`](https://github.com/Effect-TS/effect/commit/608b01fc342dbae2a642b308a67b84ead530ecea) Thanks @gcanti! - Array: simplify signatures (`ReadonlyArray | Iterable = Iterable`) + +- [#2834](https://github.com/Effect-TS/effect/pull/2834) [`031c712`](https://github.com/Effect-TS/effect/commit/031c7122a24ac42e48d6a434646b4f5d279d7442) Thanks @tim-smart! - attach Stream.toReadableStream fibers to scope + +- [#2744](https://github.com/Effect-TS/effect/pull/2744) [`a44e532`](https://github.com/Effect-TS/effect/commit/a44e532cf3a6a498b12a5aacf8124aa267e24ba0) Thanks @KhraksMamtsov! - make `Array.separate`, `Array.getRights`, `Array.getLefts`, `Array.getSomes` heterogeneous + +## 3.2.4 + +### Patch Changes + +- [#2801](https://github.com/Effect-TS/effect/pull/2801) [`1af94df`](https://github.com/Effect-TS/effect/commit/1af94df6b74aeb4f6ebcbe80e074b4cb252e62e3) Thanks @tim-smart! - ensure pool calls finalizer for failed acquisitions + +- [#2808](https://github.com/Effect-TS/effect/pull/2808) [`e313a01`](https://github.com/Effect-TS/effect/commit/e313a01b7e80f6cb7704055a190e5623c9d22c6d) Thanks @gcanti! - Array: fix `flatMapNullable` implementation and add descriptions / examples + +## 3.2.3 + +### Patch Changes + +- [#2805](https://github.com/Effect-TS/effect/pull/2805) [`45578e8`](https://github.com/Effect-TS/effect/commit/45578e8faa80ae33d23e08f6f19467f818b7788f) Thanks @tim-smart! - fix internal cutpoint name preservation + +## 3.2.2 + +### Patch Changes + +- [#2787](https://github.com/Effect-TS/effect/pull/2787) [`5d9266e`](https://github.com/Effect-TS/effect/commit/5d9266e8c740746ac9e186c3df6090a1b57fbe2a) Thanks @mikearnaldi! - Prohibit name clashes in Effect.Tag + + The following now correctly flags a type error given that the property `context` exists already in `Tag`: + + ```ts + import { Effect } from "effect" + + class LoaderArgs extends Effect.Tag("@services/LoaderContext")< + LoaderArgs, + { context: number } + >() {} + ``` + +- [#2797](https://github.com/Effect-TS/effect/pull/2797) [`9f8122e`](https://github.com/Effect-TS/effect/commit/9f8122e78884ab47c5e5f364d86eee1d1543cc61) Thanks @mikearnaldi! - Improve internalization of functions to clean stack traces + +- [#2798](https://github.com/Effect-TS/effect/pull/2798) [`6a6f670`](https://github.com/Effect-TS/effect/commit/6a6f6706b8613c8c7c10971b8d81a0f9e440a6f2) Thanks @mikearnaldi! - Avoid eager read of the stack when captured by a span + +## 3.2.1 + +### Patch Changes + +- [#2779](https://github.com/Effect-TS/effect/pull/2779) [`c1e991d`](https://github.com/Effect-TS/effect/commit/c1e991dd5ba87901cd0e05697a8b4a267e7e954a) Thanks [@tim-smart](https://github.com/tim-smart)! - fix Config.Wrap for optional properties + +## 3.2.0 + +### Minor Changes + +- [#2778](https://github.com/Effect-TS/effect/pull/2778) [`146cadd`](https://github.com/Effect-TS/effect/commit/146cadd9d004634a3ff85c480bf92cf975c853e2) Thanks [@tim-smart](https://github.com/tim-smart)! - Add Stream.toReadableStreamEffect / .toReadableStreamRuntime + +- [#2778](https://github.com/Effect-TS/effect/pull/2778) [`7135748`](https://github.com/Effect-TS/effect/commit/713574813a0f64085db0b5240ba39e7a0a7c137e) Thanks [@tim-smart](https://github.com/tim-smart)! - add Cause.prettyErrors api + + You can use this to extract `Error` instances from a `Cause`, that have clean stack traces and have had span information added to them. + +- [#2778](https://github.com/Effect-TS/effect/pull/2778) [`963b4e7`](https://github.com/Effect-TS/effect/commit/963b4e7ac87e2468feb6a344f7ab4ee4ad711198) Thanks [@tim-smart](https://github.com/tim-smart)! - add Chunk.difference & Chunk.differenceWith + +- [#2778](https://github.com/Effect-TS/effect/pull/2778) [`64c9414`](https://github.com/Effect-TS/effect/commit/64c9414e960e82058ca09bbb3976d6fbef303a8e) Thanks [@tim-smart](https://github.com/tim-smart)! - Improve causal rendering in vitest by rethrowing pretty errors + +- [#2778](https://github.com/Effect-TS/effect/pull/2778) [`7135748`](https://github.com/Effect-TS/effect/commit/713574813a0f64085db0b5240ba39e7a0a7c137e) Thanks [@tim-smart](https://github.com/tim-smart)! - add Effect.functionWithSpan + + Allows you to define an effectful function that is wrapped with a span. + + ```ts + import { Effect } from "effect" + + const getTodo = Effect.functionWithSpan({ + body: (id: number) => Effect.succeed(`Got todo ${id}!`), + options: (id) => ({ + name: `getTodo-${id}`, + attributes: { id } + }) + }) + ``` + +- [#2778](https://github.com/Effect-TS/effect/pull/2778) [`2cbb76b`](https://github.com/Effect-TS/effect/commit/2cbb76bb52500a3f4bf27d1c91482518cbea56d7) Thanks [@tim-smart](https://github.com/tim-smart)! - Add do notation for Array + +- [#2778](https://github.com/Effect-TS/effect/pull/2778) [`870c5fa`](https://github.com/Effect-TS/effect/commit/870c5fa52cd61e745e8e828d38c3f09f00737553) Thanks [@tim-smart](https://github.com/tim-smart)! - support $is & $match for Data.TaggedEnum with generics + +- [#2778](https://github.com/Effect-TS/effect/pull/2778) [`7135748`](https://github.com/Effect-TS/effect/commit/713574813a0f64085db0b5240ba39e7a0a7c137e) Thanks [@tim-smart](https://github.com/tim-smart)! - capture stack trace for tracing spans + +### Patch Changes + +- [#2778](https://github.com/Effect-TS/effect/pull/2778) [`7135748`](https://github.com/Effect-TS/effect/commit/713574813a0f64085db0b5240ba39e7a0a7c137e) Thanks [@tim-smart](https://github.com/tim-smart)! - add span stack trace to rendered causes + +- [#2778](https://github.com/Effect-TS/effect/pull/2778) [`64c9414`](https://github.com/Effect-TS/effect/commit/64c9414e960e82058ca09bbb3976d6fbef303a8e) Thanks [@tim-smart](https://github.com/tim-smart)! - Consider Generator.next a cutpoint + +## 3.1.6 + +### Patch Changes + +- [#2761](https://github.com/Effect-TS/effect/pull/2761) [`17fc22e`](https://github.com/Effect-TS/effect/commit/17fc22e132593c5caa563705a4748ba0f04a853c) Thanks [@KhraksMamtsov](https://github.com/KhraksMamtsov)! - Add `{ once: true }` to all `"abort"` event listeners for `AbortController` to automatically remove handlers after execution + +- [#2762](https://github.com/Effect-TS/effect/pull/2762) [`810f222`](https://github.com/Effect-TS/effect/commit/810f222268792b13067c7a7bf317b93a9bb8917b) Thanks [@tim-smart](https://github.com/tim-smart)! - fix Config.Wrap incorrectly wrapping functions & arrays + +- [#2773](https://github.com/Effect-TS/effect/pull/2773) [`596aaea`](https://github.com/Effect-TS/effect/commit/596aaea022648b2e06fb1ec22f1652043d6fe64e) Thanks [@tim-smart](https://github.com/tim-smart)! - fix for Infinity delays in Schedule + +## 3.1.5 + +### Patch Changes + +- [#2750](https://github.com/Effect-TS/effect/pull/2750) [`6ac4847`](https://github.com/Effect-TS/effect/commit/6ac48479447c01a4f35d655552af93e47e562610) Thanks [@tim-smart](https://github.com/tim-smart)! - ensure exponential schedules don't reach Infinity + +## 3.1.4 + +### Patch Changes + +- [#2732](https://github.com/Effect-TS/effect/pull/2732) [`e41e911`](https://github.com/Effect-TS/effect/commit/e41e91122fa6dd12fc81e50dcad0db891be67146) Thanks [@mikearnaldi](https://github.com/mikearnaldi)! - Call Equal.equals internally in order inputs were passed. + +## 3.1.3 + +### Patch Changes + +- [#2706](https://github.com/Effect-TS/effect/pull/2706) [`1f6dc96`](https://github.com/Effect-TS/effect/commit/1f6dc96f51c7bb9c8d11415358308604ba7c7c8e) Thanks [@sukovanej](https://github.com/sukovanej)! - rebuild packages + +## 3.1.2 + +### Patch Changes + +- [#2679](https://github.com/Effect-TS/effect/pull/2679) [`2e1cdf6`](https://github.com/Effect-TS/effect/commit/2e1cdf67d141281288fffe9a5c10d1379a800513) Thanks [@tim-smart](https://github.com/tim-smart)! - ensure all type ids are annotated with `unique symbol` + +## 3.1.1 + +### Patch Changes + +- [#2670](https://github.com/Effect-TS/effect/pull/2670) [`e5e56d1`](https://github.com/Effect-TS/effect/commit/e5e56d138dbed3204636f605229c6685f89659fc) Thanks [@tim-smart](https://github.com/tim-smart)! - Allow structural regions in equality for testing + +## 3.1.0 + +### Minor Changes + +- [#2543](https://github.com/Effect-TS/effect/pull/2543) [`c3c12c6`](https://github.com/Effect-TS/effect/commit/c3c12c6625633fe80e79f9db75a3b8cf8ca8b11d) Thanks [@github-actions](https://github.com/apps/github-actions)! - add SortedMap.lastOption & partition apis + +- [#2543](https://github.com/Effect-TS/effect/pull/2543) [`ba64ea6`](https://github.com/Effect-TS/effect/commit/ba64ea6757810c5e74cad3863a7d19d4d38af66b) Thanks [@github-actions](https://github.com/apps/github-actions)! - add `Types.DeepMutable`, an alternative to `Types.Mutable` that makes all properties recursively mutable + +- [#2543](https://github.com/Effect-TS/effect/pull/2543) [`b5de2d2`](https://github.com/Effect-TS/effect/commit/b5de2d2ce5b1afe8be90827bf898a95cec40eb2b) Thanks [@github-actions](https://github.com/apps/github-actions)! - add Effect.annotateLogsScoped + + This api allows you to annotate logs until the Scope has been closed. + + ```ts + import { Effect } from "effect" + + Effect.gen(function* () { + yield* Effect.log("no annotations") + yield* Effect.annotateLogsScoped({ foo: "bar" }) + yield* Effect.log("annotated with foo=bar") + }).pipe(Effect.scoped, Effect.andThen(Effect.log("no annotations again"))) + ``` + +- [#2543](https://github.com/Effect-TS/effect/pull/2543) [`a1c7ab8`](https://github.com/Effect-TS/effect/commit/a1c7ab8ffedacd18c1fc784f4ff5844f79498b83) Thanks [@github-actions](https://github.com/apps/github-actions)! - added Stream.fromEventListener, and BrowserStream.{fromEventListenerWindow, fromEventListenerDocument} for constructing a stream from addEventListener + +- [#2543](https://github.com/Effect-TS/effect/pull/2543) [`a023f28`](https://github.com/Effect-TS/effect/commit/a023f28336f3865687d9a30c1883e36909906d85) Thanks [@github-actions](https://github.com/apps/github-actions)! - add `kind` property to `Tracer.Span` + + This can be used to specify what kind of service created the span. + +- [#2543](https://github.com/Effect-TS/effect/pull/2543) [`1c9454d`](https://github.com/Effect-TS/effect/commit/1c9454d532eae79b9f759aea77f59332cc6d18ed) Thanks [@github-actions](https://github.com/apps/github-actions)! - add Effect.timeoutOption + + Returns an effect that will return `None` if the effect times out, otherwise it + will return `Some` of the produced value. + + ```ts + import { Effect } from "effect" + + // will return `None` after 500 millis + Effect.succeed("hello").pipe( + Effect.delay(1000), + Effect.timeoutOption("500 millis") + ) + ``` + +- [#2543](https://github.com/Effect-TS/effect/pull/2543) [`92d56db`](https://github.com/Effect-TS/effect/commit/92d56dbb3f33e36636c2a2f1030c56492e39cf4d) Thanks [@github-actions](https://github.com/apps/github-actions)! - add $is & $match helpers to Data.TaggedEnum constructors + + ```ts + import { Data } from "effect" + + type HttpError = Data.TaggedEnum<{ + NotFound: {} + InternalServerError: { reason: string } + }> + const { $is, $match, InternalServerError, NotFound } = + Data.taggedEnum() + + // create a matcher + const matcher = $match({ + NotFound: () => 0, + InternalServerError: () => 1 + }) + + // true + $is("NotFound")(NotFound()) + + // false + $is("NotFound")(InternalServerError({ reason: "fail" })) + ``` + +## 3.0.8 + +### Patch Changes + +- [#2656](https://github.com/Effect-TS/effect/pull/2656) [`557707b`](https://github.com/Effect-TS/effect/commit/557707bc9e5f230c8964d2757012075c34339b5c) Thanks [@tim-smart](https://github.com/tim-smart)! - update dependencies + +- [#2654](https://github.com/Effect-TS/effect/pull/2654) [`f4ed306`](https://github.com/Effect-TS/effect/commit/f4ed3068a70b50302d078a30d18ca3cfd2bc679c) Thanks [@mikearnaldi](https://github.com/mikearnaldi)! - Actually fix Cause equality + +- [#2640](https://github.com/Effect-TS/effect/pull/2640) [`661004f`](https://github.com/Effect-TS/effect/commit/661004f4bf5f8b25f5a0678c21a3a822188ce461) Thanks [@patroza](https://github.com/patroza)! - fix: forEach NonEmpty overload causing inference issues for Iterables + +- [#2653](https://github.com/Effect-TS/effect/pull/2653) [`e79cb83`](https://github.com/Effect-TS/effect/commit/e79cb83d3b19098bc40a3012e2a059b8426306c2) Thanks [@mikearnaldi](https://github.com/mikearnaldi)! - Consider type of failure in Cause equality + +## 3.0.7 + +### Patch Changes + +- [#2637](https://github.com/Effect-TS/effect/pull/2637) [`18de56b`](https://github.com/Effect-TS/effect/commit/18de56b4a6b6d1f99230dfabf9147d59ea4dd759) Thanks [@mikearnaldi](https://github.com/mikearnaldi)! - Avoid treating completed requests as interrupted when race conditions occur + +## 3.0.6 + +### Patch Changes + +- [#2625](https://github.com/Effect-TS/effect/pull/2625) [`ffe4f4e`](https://github.com/Effect-TS/effect/commit/ffe4f4e95db35fff6869e360b072e3837befa0a1) Thanks [@mikearnaldi](https://github.com/mikearnaldi)! - Avoid circularity on generators + +- [#2626](https://github.com/Effect-TS/effect/pull/2626) [`027418e`](https://github.com/Effect-TS/effect/commit/027418edaa6aa6c0ae4861b95832827b45adace4) Thanks [@fubhy](https://github.com/fubhy)! - Reintroduce custom `NoInfer` type + +- [#2609](https://github.com/Effect-TS/effect/pull/2609) [`ac1898e`](https://github.com/Effect-TS/effect/commit/ac1898eb7bc96880f911c276048e2ea3d6fe9c50) Thanks [@patroza](https://github.com/patroza)! - change: BatchedRequestResolver works with NonEmptyArray + +- [#2625](https://github.com/Effect-TS/effect/pull/2625) [`ffe4f4e`](https://github.com/Effect-TS/effect/commit/ffe4f4e95db35fff6869e360b072e3837befa0a1) Thanks [@mikearnaldi](https://github.com/mikearnaldi)! - Make sure GenKind utilities are backward compatible + +## 3.0.5 + +### Patch Changes + +- [#2611](https://github.com/Effect-TS/effect/pull/2611) [`6222404`](https://github.com/Effect-TS/effect/commit/62224044678751829ed2f128e05133a91c6b0569) Thanks [@tim-smart](https://github.com/tim-smart)! - simplify EffectGenerator type to improve inference + +- [#2608](https://github.com/Effect-TS/effect/pull/2608) [`868ed2a`](https://github.com/Effect-TS/effect/commit/868ed2a8fe94ee7f4206a6070f29dcf2a5ba1dc3) Thanks [@patroza](https://github.com/patroza)! - feat: foreach preserve non emptyness. + +## 3.0.4 + +### Patch Changes + +- [#2602](https://github.com/Effect-TS/effect/pull/2602) [`9a24667`](https://github.com/Effect-TS/effect/commit/9a246672008a2b668d43fbfd2fe5508c54b2b920) Thanks [@mikearnaldi](https://github.com/mikearnaldi)! - allow use of generators (Effect.gen) without the adapter + + Effect's data types now implement a Iterable that can be `yield*`'ed directly. + + ```ts + Effect.gen(function* () { + const a = yield* Effect.success(1) + const b = yield* Effect.success(2) + return a + b + }) + ``` + +## 3.0.3 + +### Patch Changes + +- [#2568](https://github.com/Effect-TS/effect/pull/2568) [`a7b4b84`](https://github.com/Effect-TS/effect/commit/a7b4b84bd5a25f51aba922f9259c3a58c98c6a4e) Thanks [@tim-smart](https://github.com/tim-smart)! - add Match.withReturnType api + + Which can be used to constrain the return type of a match expression. + + ```ts + import { Match } from "effect" + + Match.type().pipe( + Match.withReturnType(), + Match.when("foo", () => "foo"), // valid + Match.when("bar", () => 123), // type error + Match.else(() => "baz") + ) + ``` + +## 3.0.2 + +### Patch Changes + +- [#2562](https://github.com/Effect-TS/effect/pull/2562) [`2cecdbd`](https://github.com/Effect-TS/effect/commit/2cecdbd1cf30befce4e84796ccd953ea55ecfb86) Thanks [@fubhy](https://github.com/fubhy)! - Added provenance publishing + +## 3.0.1 + +### Patch Changes + +- [#2539](https://github.com/Effect-TS/effect/pull/2539) [`3da0cfa`](https://github.com/Effect-TS/effect/commit/3da0cfa12c407fd930dc480be1ecc9217a8058f8) Thanks [@tim-smart](https://github.com/tim-smart)! - skip running effects in FiberHandle/Map if not required + +- [#2552](https://github.com/Effect-TS/effect/pull/2552) [`570e8d8`](https://github.com/Effect-TS/effect/commit/570e8d87e7c0e9ad4cd2686462fdb9b4812f7716) Thanks [@TylorS](https://github.com/TylorS)! - Improve typings of Array.isArray + +- [#2555](https://github.com/Effect-TS/effect/pull/2555) [`8edacca`](https://github.com/Effect-TS/effect/commit/8edacca37f8e37c01a63fec332b06d9361efaa7b) Thanks [@tim-smart](https://github.com/tim-smart)! - prevent use of `Array` as import name to solve bundler issues + +## 3.0.0 + +### Major Changes + +- [#2207](https://github.com/Effect-TS/effect/pull/2207) [`2fb7d9c`](https://github.com/Effect-TS/effect/commit/2fb7d9ca15037ff62a578bb9fe5732da5f4f317d) Thanks [@github-actions](https://github.com/apps/github-actions)! - Release Effect 3.0 🎉 + +### Minor Changes + +- [#2207](https://github.com/Effect-TS/effect/pull/2207) [`1b5f0c7`](https://github.com/Effect-TS/effect/commit/1b5f0c77e7fd477a0026071e82129a948227f4b3) Thanks [@github-actions](https://github.com/apps/github-actions)! - close FiberHandle/FiberSet/FiberMap when it is released + + When they are closed, fibers can no longer be added to them. + +- [#2207](https://github.com/Effect-TS/effect/pull/2207) [`d50a652`](https://github.com/Effect-TS/effect/commit/d50a652479f4d1d64f48da05c79fa847e6e51548) Thanks [@github-actions](https://github.com/apps/github-actions)! - add preregisteredWords option to frequency metric key type + + You can use this to register a list of words to pre-populate the value of the + metric. + + ```ts + import { Metric } from "effect" + + const counts = Metric.frequency("counts", { + preregisteredWords: ["a", "b", "c"] + }).register() + ``` + +- [#2207](https://github.com/Effect-TS/effect/pull/2207) [`9a3bd47`](https://github.com/Effect-TS/effect/commit/9a3bd47ebd0750c7e498162734f6d21895de0cb2) Thanks [@github-actions](https://github.com/apps/github-actions)! - Bump TypeScript min requirement to version 5.4 + +- [#2207](https://github.com/Effect-TS/effect/pull/2207) [`be9d025`](https://github.com/Effect-TS/effect/commit/be9d025e42355260ace02dd135851a8935a4deba) Thanks [@github-actions](https://github.com/apps/github-actions)! - add unique identifier to Tracer.ParentSpan tag + +- [#2529](https://github.com/Effect-TS/effect/pull/2529) [`78b767c`](https://github.com/Effect-TS/effect/commit/78b767c2b1625186e17131761a0edbac25d21850) Thanks [@fubhy](https://github.com/fubhy)! - Renamed `ReadonlyArray` and `ReadonlyRecord` modules for better discoverability. + +- [#2207](https://github.com/Effect-TS/effect/pull/2207) [`5c2b561`](https://github.com/Effect-TS/effect/commit/5c2b5614f583b88784ed68126ae939832fb3c092) Thanks [@github-actions](https://github.com/apps/github-actions)! - The signatures of the `HaltStrategy.match` `StreamHaltStrategy.match` functions have been changed to the generally accepted ones + +- [#2207](https://github.com/Effect-TS/effect/pull/2207) [`a18f594`](https://github.com/Effect-TS/effect/commit/a18f5948f1439a147232448b2c443472fda0eceb) Thanks [@github-actions](https://github.com/apps/github-actions)! - support variadic arguments in Effect.log + + This makes Effect.log more similar to console.log: + + ```ts + Effect.log("hello", { foo: "bar" }, Cause.fail("error")) + ``` + +- [#2207](https://github.com/Effect-TS/effect/pull/2207) [`2f96d93`](https://github.com/Effect-TS/effect/commit/2f96d938b90f8c19377583279e3c7afd9b509c50) Thanks [@github-actions](https://github.com/apps/github-actions)! - Fix ConfigError `_tag`, with the previous implementation catching the `ConfigError` with `Effect.catchTag` would show `And`, `Or`, etc. + +- [#2207](https://github.com/Effect-TS/effect/pull/2207) [`5a2314b`](https://github.com/Effect-TS/effect/commit/5a2314b70ec79c2c02b51cef45a5ddec8327daa1) Thanks [@github-actions](https://github.com/apps/github-actions)! - replace use of `unit` terminology with `void` + + For all the data types. + + ```ts + Effect.unit // => Effect.void + Stream.unit // => Stream.void + + // etc + ``` + +- [#2207](https://github.com/Effect-TS/effect/pull/2207) [`271b79f`](https://github.com/Effect-TS/effect/commit/271b79fc0b66a6c11e07a8779ff8800493a7eac2) Thanks [@github-actions](https://github.com/apps/github-actions)! - Either: fix `getEquivalence` parameter order from `Either.getEquivalence(left, right)` to `Either.getEquivalence({ left, right })` + +- [#2207](https://github.com/Effect-TS/effect/pull/2207) [`53d1c2a`](https://github.com/Effect-TS/effect/commit/53d1c2a77559081fbb89667e343346375c6d6650) Thanks [@github-actions](https://github.com/apps/github-actions)! - use LazyArg for Effect.if branches + + Instead of: + + ```ts + Effect.if(true, { + onTrue: Effect.succeed("true"), + onFalse: Effect.succeed("false") + }) + ``` + + You should now write: + + ```ts + Effect.if(true, { + onTrue: () => Effect.succeed("true"), + onFalse: () => Effect.succeed("false") + }) + ``` + +- [#2207](https://github.com/Effect-TS/effect/pull/2207) [`e7e1bbe`](https://github.com/Effect-TS/effect/commit/e7e1bbe68486fdf31c8f84b0880522d39adcaad3) Thanks [@github-actions](https://github.com/apps/github-actions)! - Replaced custom `NoInfer` type with the native `NoInfer` type from TypeScript 5.4 + +- [#2207](https://github.com/Effect-TS/effect/pull/2207) [`10c169e`](https://github.com/Effect-TS/effect/commit/10c169eadc874e91b4defca3f467b4e6a50fd8f3) Thanks [@github-actions](https://github.com/apps/github-actions)! - `Cache` has been changed to `Cache`. + `ScopedCache` has been changed to `ScopedCache`. + `Lookup` has been changed to `Lookup` + +### Patch Changes + +- [#2104](https://github.com/Effect-TS/effect/pull/2104) [`1499974`](https://github.com/Effect-TS/effect/commit/14999741d2e19c1747f6a7e19d68977f6429cdb8) Thanks [@IMax153](https://github.com/IMax153)! - don't run resolver if there are no incomplete requests + +- [#2207](https://github.com/Effect-TS/effect/pull/2207) [`1b5f0c7`](https://github.com/Effect-TS/effect/commit/1b5f0c77e7fd477a0026071e82129a948227f4b3) Thanks [@github-actions](https://github.com/apps/github-actions)! - add FiberMap.has/unsafeHas api + +- [#2104](https://github.com/Effect-TS/effect/pull/2104) [`1499974`](https://github.com/Effect-TS/effect/commit/14999741d2e19c1747f6a7e19d68977f6429cdb8) Thanks [@IMax153](https://github.com/IMax153)! - add String casing transformation apis + - `snakeToCamel` + - `snakeToPascal` + - `snakeToKebab` + - `camelToSnake` + - `pascalToSnake` + - `kebabToSnake` + +- [#2207](https://github.com/Effect-TS/effect/pull/2207) [`1b5f0c7`](https://github.com/Effect-TS/effect/commit/1b5f0c77e7fd477a0026071e82129a948227f4b3) Thanks [@github-actions](https://github.com/apps/github-actions)! - add FiberHandle module, for holding a reference to a running fiber + + ```ts + import { Effect, FiberHandle } from "effect" + + Effect.gen(function* (_) { + const handle = yield* _(FiberHandle.make()) + + // run some effects + yield* _(FiberHandle.run(handle, Effect.never)) + // this will interrupt the previous fiber + yield* _(FiberHandle.run(handle, Effect.never)) + // this will not run, as a fiber is already running + yield* _(FiberHandle.run(handle, Effect.never, { onlyIfMissing: true })) + + yield* _(Effect.sleep(1000)) + }).pipe( + Effect.scoped // The fiber will be interrupted when the scope is closed + ) + ``` + +- [#2521](https://github.com/Effect-TS/effect/pull/2521) [`6424181`](https://github.com/Effect-TS/effect/commit/64241815fe6a939e91e6947253e7dceea1306aa8) Thanks [@patroza](https://github.com/patroza)! - change return type of Fiber.joinAll to return an array + +## 2.4.19 + +### Patch Changes + +- [#2503](https://github.com/Effect-TS/effect/pull/2503) [`41c8102`](https://github.com/Effect-TS/effect/commit/41c810228b1a50e4b41f19e735d7c62fe8d36871) Thanks [@gcanti](https://github.com/gcanti)! - Centralize error messages for bugs + +- [#2493](https://github.com/Effect-TS/effect/pull/2493) [`776ef2b`](https://github.com/Effect-TS/effect/commit/776ef2bb66db9aa9f68b7beab14f6986f9c1288b) Thanks [@gcanti](https://github.com/gcanti)! - add a `RegExp` module to `packages/effect`, closes #2488 + +- [#2499](https://github.com/Effect-TS/effect/pull/2499) [`217147e`](https://github.com/Effect-TS/effect/commit/217147ea67c5c42c96f024775c41e5b070f81e4c) Thanks [@tim-smart](https://github.com/tim-smart)! - ensure FIFO ordering when a Deferred is resolved + +- [#2502](https://github.com/Effect-TS/effect/pull/2502) [`90776ec`](https://github.com/Effect-TS/effect/commit/90776ec8e8671d835b65fc33ead1de6c864b81b9) Thanks [@tim-smart](https://github.com/tim-smart)! - make tracing spans cheaper to construct + +- [#2472](https://github.com/Effect-TS/effect/pull/2472) [`8709856`](https://github.com/Effect-TS/effect/commit/870985694ae985c3cb9360ad8a25c60e6f785f55) Thanks [@tim-smart](https://github.com/tim-smart)! - add Subscribable trait / module + + Subscribable represents a resource that has a current value and can be subscribed to for updates. + + The following data types are subscribable: + - A `SubscriptionRef` + - An `Actor` from the experimental `Machine` module + +- [#2500](https://github.com/Effect-TS/effect/pull/2500) [`232c353`](https://github.com/Effect-TS/effect/commit/232c353c2e6f743f38e57639ee30e324ffa9c2a9) Thanks [@tim-smart](https://github.com/tim-smart)! - simplify scope internals + +- [#2507](https://github.com/Effect-TS/effect/pull/2507) [`0ca835c`](https://github.com/Effect-TS/effect/commit/0ca835cbac8e69072a93ace83b534219faba24e8) Thanks [@gcanti](https://github.com/gcanti)! - ensure correct value is passed to mapping function in `mapAccum` loop, closes #2506 + +- [#2472](https://github.com/Effect-TS/effect/pull/2472) [`8709856`](https://github.com/Effect-TS/effect/commit/870985694ae985c3cb9360ad8a25c60e6f785f55) Thanks [@tim-smart](https://github.com/tim-smart)! - add Readable module / trait + + `Readable` is a common interface for objects that can be read from using a `get` + Effect. + + For example, `Ref`'s implement `Readable`: + + ```ts + import { Effect, Readable, Ref } from "effect" + import assert from "assert" + + Effect.gen(function* (_) { + const ref = yield* _(Ref.make(123)) + assert(Readable.isReadable(ref)) + + const result = yield* _(ref.get) + assert(result === 123) + }) + ``` + +- [#2498](https://github.com/Effect-TS/effect/pull/2498) [`e983740`](https://github.com/Effect-TS/effect/commit/e9837401145605aff5bc2ec7e73004f397c5d2d1) Thanks [@jessekelly881](https://github.com/jessekelly881)! - added {Readable, Subscribable}.unwrap + +- [#2494](https://github.com/Effect-TS/effect/pull/2494) [`e3e0924`](https://github.com/Effect-TS/effect/commit/e3e09247d46a35430fc60e4aa4032cc50814f212) Thanks [@thewilkybarkid](https://github.com/thewilkybarkid)! - Add `Duration.divide` and `Duration.unsafeDivide`. + + ```ts + import { Duration, Option } from "effect" + import assert from "assert" + + assert.deepStrictEqual( + Duration.divide("10 seconds", 2), + Option.some(Duration.decode("5 seconds")) + ) + assert.deepStrictEqual(Duration.divide("10 seconds", 0), Option.none()) + assert.deepStrictEqual(Duration.divide("1 nano", 1.5), Option.none()) + + assert.deepStrictEqual( + Duration.unsafeDivide("10 seconds", 2), + Duration.decode("5 seconds") + ) + assert.deepStrictEqual( + Duration.unsafeDivide("10 seconds", 0), + Duration.infinity + ) + assert.throws(() => Duration.unsafeDivide("1 nano", 1.5)) + ``` + +## 2.4.18 + +### Patch Changes + +- [#2473](https://github.com/Effect-TS/effect/pull/2473) [`dadc690`](https://github.com/Effect-TS/effect/commit/dadc6906121c512bc32be22b52adbd1ada834594) Thanks [@tim-smart](https://github.com/tim-smart)! - add Logger.withConsoleLog/withConsoleError apis + + These apis send a Logger's output to console.log/console.error respectively. + + ```ts + import { Logger } from "effect" + + // send output to stderr + const stderrLogger = Logger.withConsoleError(Logger.stringLogger) + ``` + +## 2.4.17 + +### Patch Changes + +- [#2461](https://github.com/Effect-TS/effect/pull/2461) [`8fdfda6`](https://github.com/Effect-TS/effect/commit/8fdfda6618be848c01b399d13bc05a9a3adfb613) Thanks [@tim-smart](https://github.com/tim-smart)! - add Inspectable.toStringUnknown/stringifyCircular + +- [#2462](https://github.com/Effect-TS/effect/pull/2462) [`607b2e7`](https://github.com/Effect-TS/effect/commit/607b2e7a7fd9318c57acf4e50ec61747eea74ad7) Thanks [@tim-smart](https://github.com/tim-smart)! - remove handled errors from Effect.retryOrElse + +- [#2461](https://github.com/Effect-TS/effect/pull/2461) [`8fdfda6`](https://github.com/Effect-TS/effect/commit/8fdfda6618be848c01b399d13bc05a9a3adfb613) Thanks [@tim-smart](https://github.com/tim-smart)! - improve formatting of Runtime failures + +- [#2415](https://github.com/Effect-TS/effect/pull/2415) [`8206caf`](https://github.com/Effect-TS/effect/commit/8206caf7c2d22c68be4313318b61cfdacf6222b6) Thanks [@tim-smart](https://github.com/tim-smart)! - add Iterable module + + This module shares many apis compared to "effect/ReadonlyArray", but is fully lazy. + + ```ts + import { Iterable, pipe } from "effect" + + // Only 5 items will be generated & transformed + pipe( + Iterable.range(1, 100), + Iterable.map((i) => `item ${i}`), + Iterable.take(5) + ) + ``` + +- [#2438](https://github.com/Effect-TS/effect/pull/2438) [`7ddd654`](https://github.com/Effect-TS/effect/commit/7ddd65415b65ccb654ad04f4dbefe39402f15117) Thanks [@mikearnaldi](https://github.com/mikearnaldi)! - Support Heterogeneous Effects in Effect Iterable apis + + Including: + - `Effect.allSuccesses` + - `Effect.firstSuccessOf` + - `Effect.mergeAll` + - `Effect.reduceEffect` + - `Effect.raceAll` + - `Effect.forkAll` + + For example: + + ```ts + import { Effect } from "effect" + + class Foo extends Effect.Tag("Foo")() {} + class Bar extends Effect.Tag("Bar")() {} + + // const program: Effect.Effect<(1 | 2 | 3 | 4)[], never, Foo | Bar> + export const program = Effect.allSuccesses([ + Effect.succeed(1 as const), + Effect.succeed(2 as const), + Foo, + Bar + ]) + ``` + + The above is now possible while before it was expecting all Effects to conform to the same type + +- [#2438](https://github.com/Effect-TS/effect/pull/2438) [`7ddd654`](https://github.com/Effect-TS/effect/commit/7ddd65415b65ccb654ad04f4dbefe39402f15117) Thanks [@mikearnaldi](https://github.com/mikearnaldi)! - add Effect.filterMap api + + Which allows you to filter and map an Iterable of Effects in one step. + + ```ts + import { Effect, Option } from "effect" + + // resolves with `["even: 2"] + Effect.filterMap( + [Effect.succeed(1), Effect.succeed(2), Effect.succeed(3)], + (i) => (i % 2 === 0 ? Option.some(`even: ${i}`) : Option.none()) + ) + ``` + +- [#2461](https://github.com/Effect-TS/effect/pull/2461) [`8fdfda6`](https://github.com/Effect-TS/effect/commit/8fdfda6618be848c01b399d13bc05a9a3adfb613) Thanks [@tim-smart](https://github.com/tim-smart)! - use Inspectable.toStringUnknown for absurd runtime errors + +- [#2460](https://github.com/Effect-TS/effect/pull/2460) [`f456ba2`](https://github.com/Effect-TS/effect/commit/f456ba273bae21a6dcf8c966c50c97b5f0897d9f) Thanks [@tim-smart](https://github.com/tim-smart)! - use const type parameter for Config.withDefault + + Which ensures that the fallback value type is not widened for literals. + +## 2.4.16 + +### Patch Changes + +- [#2445](https://github.com/Effect-TS/effect/pull/2445) [`5170ce7`](https://github.com/Effect-TS/effect/commit/5170ce708c606283e8a30d273950f1a21c7eddc2) Thanks [@vecerek](https://github.com/vecerek)! - generate proper trace ids in default effect Tracer + +## 2.4.15 + +### Patch Changes + +- [#2407](https://github.com/Effect-TS/effect/pull/2407) [`d7688c0`](https://github.com/Effect-TS/effect/commit/d7688c0c72717fe7876c871567f6946dabfc0546) Thanks [@thewilkybarkid](https://github.com/thewilkybarkid)! - Add Config.duration + + This can be used to parse Duration's from environment variables: + + ```ts + import { Config, Effect } from "effect" + + Config.duration("CACHE_TTL").pipe( + Effect.andThen((duration) => ...) + ) + ``` + +- [#2416](https://github.com/Effect-TS/effect/pull/2416) [`b3a4fac`](https://github.com/Effect-TS/effect/commit/b3a4face2acaca422f0b0530436e8f13129f3b3a) Thanks [@mikearnaldi](https://github.com/mikearnaldi)! - Collect exits on forEach interrupt of residual requests + +## 2.4.14 + +### Patch Changes + +- [#2404](https://github.com/Effect-TS/effect/pull/2404) [`6180c0c`](https://github.com/Effect-TS/effect/commit/6180c0cc51dee785cfce72220a52c9fc3b9bf9aa) Thanks [@patroza](https://github.com/patroza)! - fix interruption of parked Requests + +## 2.4.13 + +### Patch Changes + +- [#2402](https://github.com/Effect-TS/effect/pull/2402) [`3336287`](https://github.com/Effect-TS/effect/commit/3336287ff55a25e56d759b83847bfaa21c40f499) Thanks [@tim-smart](https://github.com/tim-smart)! - add Duration.subtract api + +- [#2399](https://github.com/Effect-TS/effect/pull/2399) [`54b7c00`](https://github.com/Effect-TS/effect/commit/54b7c0077fa784ad2646b812d6a44641f672edcd) Thanks [@coleea](https://github.com/coleea)! - add BigInt.fromString and BigInt.fromNumber + +- [#2402](https://github.com/Effect-TS/effect/pull/2402) [`3336287`](https://github.com/Effect-TS/effect/commit/3336287ff55a25e56d759b83847bfaa21c40f499) Thanks [@tim-smart](https://github.com/tim-smart)! - remove use of bigint literals in Duration + +## 2.4.12 + +### Patch Changes + +- [#2385](https://github.com/Effect-TS/effect/pull/2385) [`3307729`](https://github.com/Effect-TS/effect/commit/3307729de162a033fa9caa8e14c111013dcf0d87) Thanks [@tim-smart](https://github.com/tim-smart)! - update typescript to 5.4 + +## 2.4.11 + +### Patch Changes + +- [#2384](https://github.com/Effect-TS/effect/pull/2384) [`2f488c4`](https://github.com/Effect-TS/effect/commit/2f488c436de52576562803c57ebc132ef40ccdd8) Thanks [@tim-smart](https://github.com/tim-smart)! - update dependencies + +- [#2381](https://github.com/Effect-TS/effect/pull/2381) [`37ca592`](https://github.com/Effect-TS/effect/commit/37ca592a4101ad90adbf8c8b3f727faf3110cae5) Thanks [@tim-smart](https://github.com/tim-smart)! - add fiber ref for disabling the tracer + + You can use it with the Effect.withTracerEnabled api: + + ```ts + import { Effect } from "effect" + + Effect.succeed(42).pipe( + Effect.withSpan("my-span"), + // the span will not be registered with the tracer + Effect.withTracerEnabled(false) + ) + ``` + +- [#2383](https://github.com/Effect-TS/effect/pull/2383) [`317b5b8`](https://github.com/Effect-TS/effect/commit/317b5b8e8c8c2207469b3ebfcf72bf3a9f7cbc60) Thanks [@tim-smart](https://github.com/tim-smart)! - add Duration.isFinite api, to determine if a duration is not Infinity + +## 2.4.10 + +### Patch Changes + +- [#2375](https://github.com/Effect-TS/effect/pull/2375) [`9bab1f9`](https://github.com/Effect-TS/effect/commit/9bab1f9fa5b999740755e4e82485cb77c638643a) Thanks [@tim-smart](https://github.com/tim-smart)! - remove dangling variable in frequency metric hook + +- [#2373](https://github.com/Effect-TS/effect/pull/2373) [`9bbde5b`](https://github.com/Effect-TS/effect/commit/9bbde5be9a0168d1c2a0308bfc27167ed62f3968) Thanks [@patroza](https://github.com/patroza)! - Use incremental counters instead of up-down for runtime metrics + +## 2.4.9 + +### Patch Changes + +- [#2357](https://github.com/Effect-TS/effect/pull/2357) [`71fd528`](https://github.com/Effect-TS/effect/commit/71fd5287500f9ce155a7d9f0df6ee3e0ac3aeb99) Thanks [@tim-smart](https://github.com/tim-smart)! - make more data types in /platform implement Inspectable + +## 2.4.8 + +### Patch Changes + +- [#2354](https://github.com/Effect-TS/effect/pull/2354) [`bb0b69e`](https://github.com/Effect-TS/effect/commit/bb0b69e519698c7c76aa68217de423c78ad16566) Thanks [@tim-smart](https://github.com/tim-smart)! - add overload to Effect.filterOrFail that fails with NoSuchElementException + + This allows you to perform a filterOrFail without providing a fallback failure + function. + + Example: + + ```ts + import { Effect } from "effect" + + // fails with NoSuchElementException + Effect.succeed(1).pipe(Effect.filterOrFail((n) => n === 0)) + ``` + +- [#2336](https://github.com/Effect-TS/effect/pull/2336) [`6b20bad`](https://github.com/Effect-TS/effect/commit/6b20badebb3a7ca4d38857753e8ecaa09d02ccfb) Thanks [@jessekelly881](https://github.com/jessekelly881)! - added Predicate.isTruthy + +- [#2351](https://github.com/Effect-TS/effect/pull/2351) [`4e64e9b`](https://github.com/Effect-TS/effect/commit/4e64e9b9876de6bfcbabe39e18a91a08e5f3fbb0) Thanks [@tim-smart](https://github.com/tim-smart)! - fix metrics not using labels from fiber ref + +- [#2266](https://github.com/Effect-TS/effect/pull/2266) [`3851a02`](https://github.com/Effect-TS/effect/commit/3851a022c481006aec1db36651e4b4fd727aa742) Thanks [@patroza](https://github.com/patroza)! - fix Effect.Tag generated proxy functions to work with andThen/tap, or others that do function/isEffect checks + +- [#2353](https://github.com/Effect-TS/effect/pull/2353) [`5f5fcd9`](https://github.com/Effect-TS/effect/commit/5f5fcd969ae30ed6fe61d566a571498d9e895e16) Thanks [@tim-smart](https://github.com/tim-smart)! - Types: add `Has` helper + +- [#2299](https://github.com/Effect-TS/effect/pull/2299) [`814e5b8`](https://github.com/Effect-TS/effect/commit/814e5b828f68210b9e8f336fd6ac688646835dd9) Thanks [@alex-dixon](https://github.com/alex-dixon)! - Prevent Effect.if from crashing when first argument is not an Effect + +## 2.4.7 + +### Patch Changes + +- [#2328](https://github.com/Effect-TS/effect/pull/2328) [`eb93283`](https://github.com/Effect-TS/effect/commit/eb93283985913d7b04ca750e36ac8513e7b6cef6) Thanks [@tim-smart](https://github.com/tim-smart)! - set unhandled log level to none for fibers in FiberSet/Map + +## 2.4.6 + +### Patch Changes + +- [#2290](https://github.com/Effect-TS/effect/pull/2290) [`4f35a7e`](https://github.com/Effect-TS/effect/commit/4f35a7e7c4eba598924aff24d1158b9056bb24be) Thanks [@mikearnaldi](https://github.com/mikearnaldi)! - Remove function renaming from internals, introduce new cutpoint strategy + +- [#2311](https://github.com/Effect-TS/effect/pull/2311) [`9971186`](https://github.com/Effect-TS/effect/commit/99711862722188fbb5ed3ee75126ad5edf13f72f) Thanks [@tim-smart](https://github.com/tim-smart)! - add Channel.splitLines api + + It splits strings on newlines. Handles both Windows newlines (`\r\n`) and UNIX + newlines (`\n`). + +## 2.4.5 + +### Patch Changes + +- [#2300](https://github.com/Effect-TS/effect/pull/2300) [`bce21c5`](https://github.com/Effect-TS/effect/commit/bce21c5ded2177114666ba229bd5029fa000dee3) Thanks [@gcanti](https://github.com/gcanti)! - ReadonlyArray: fix `intersperse` signature + +- [#2303](https://github.com/Effect-TS/effect/pull/2303) [`c7d3036`](https://github.com/Effect-TS/effect/commit/c7d303630b7f0825cb2e584557c5767a67214d9f) Thanks [@gcanti](https://github.com/gcanti)! - ReadonlyArray: fix `sort` signature, closes #2301 + +## 2.4.4 + +### Patch Changes + +- [#2172](https://github.com/Effect-TS/effect/pull/2172) [`5d47ee0`](https://github.com/Effect-TS/effect/commit/5d47ee0855e492532085b6092879b1b952d84949) Thanks [@gcanti](https://github.com/gcanti)! - Brand: add `refined` overload + + ```ts + export function refined>( + f: (unbranded: Brand.Unbranded) => Option.Option + ): Brand.Constructor + ``` + +- [#2285](https://github.com/Effect-TS/effect/pull/2285) [`817a04c`](https://github.com/Effect-TS/effect/commit/817a04cb2df0f4140984dc97eb3e1bb14a6c4a38) Thanks [@tim-smart](https://github.com/tim-smart)! - add support for AbortSignal's to runPromise + + If the signal is aborted, the effect execution will be interrupted. + + ```ts + import { Effect } from "effect" + + const controller = new AbortController() + + Effect.runPromise(Effect.never, { signal: controller.signal }) + + // abort after 1 second + setTimeout(() => controller.abort(), 1000) + ``` + +- [#2293](https://github.com/Effect-TS/effect/pull/2293) [`d90a99d`](https://github.com/Effect-TS/effect/commit/d90a99d03d074adc7cd2533f15419138264da5a2) Thanks [@tim-smart](https://github.com/tim-smart)! - add AbortSignal support to ManagedRuntime + +- [#2288](https://github.com/Effect-TS/effect/pull/2288) [`dd05faa`](https://github.com/Effect-TS/effect/commit/dd05faa621555ef3585ecd914ac13ecd89b710f4) Thanks [@tim-smart](https://github.com/tim-smart)! - optimize addition of blocked requests to parallel collection + +- [#2288](https://github.com/Effect-TS/effect/pull/2288) [`dd05faa`](https://github.com/Effect-TS/effect/commit/dd05faa621555ef3585ecd914ac13ecd89b710f4) Thanks [@tim-smart](https://github.com/tim-smart)! - use Chunk for request block collections + +- [#2280](https://github.com/Effect-TS/effect/pull/2280) [`802674b`](https://github.com/Effect-TS/effect/commit/802674b379b7559ad3ff09b33388891445a9e48b) Thanks [@jessekelly881](https://github.com/jessekelly881)! - added support for PromiseLike + +## 2.4.3 + +### Patch Changes + +- [#2211](https://github.com/Effect-TS/effect/pull/2211) [`20e63fb`](https://github.com/Effect-TS/effect/commit/20e63fb9207210f3fe2d136ec40d0a2dbff3225e) Thanks [@tim-smart](https://github.com/tim-smart)! - add ManagedRuntime module, to make incremental adoption easier + + You can use a ManagedRuntime to run Effect's that can use the + dependencies from the given Layer. For example: + + ```ts + import { Console, Effect, Layer, ManagedRuntime } from "effect" + + class Notifications extends Effect.Tag("Notifications")< + Notifications, + { readonly notify: (message: string) => Effect.Effect } + >() { + static Live = Layer.succeed(this, { + notify: (message) => Console.log(message) + }) + } + + async function main() { + const runtime = ManagedRuntime.make(Notifications.Live) + await runtime.runPromise(Notifications.notify("Hello, world!")) + await runtime.dispose() + } + + main() + ``` + +- [#2211](https://github.com/Effect-TS/effect/pull/2211) [`20e63fb`](https://github.com/Effect-TS/effect/commit/20e63fb9207210f3fe2d136ec40d0a2dbff3225e) Thanks [@tim-smart](https://github.com/tim-smart)! - add Layer.toRuntimeWithMemoMap api + + Similar to Layer.toRuntime, but allows you to share a Layer.MemoMap between + layer builds. + + By sharing the MemoMap, layers are shared between each build - ensuring layers + are only built once between multiple calls to Layer.toRuntimeWithMemoMap. + +## 2.4.2 + +### Patch Changes + +- [#2264](https://github.com/Effect-TS/effect/pull/2264) [`e03811e`](https://github.com/Effect-TS/effect/commit/e03811e80c93e986e6348b3b67ac2ed6d5fefff0) Thanks [@patroza](https://github.com/patroza)! - fix: unmatched function fallthrough in `andThen` and `tap` + +- [#2225](https://github.com/Effect-TS/effect/pull/2225) [`ac41d84`](https://github.com/Effect-TS/effect/commit/ac41d84776484cdce8165b7ca2c9c9b6377eee2d) Thanks [@mikearnaldi](https://github.com/mikearnaldi)! - Add Effect.Tag to simplify access to service. + + This change allows to define tags in the following way: + + ```ts + class DemoTag extends Effect.Tag("DemoTag")< + DemoTag, + { + readonly getNumbers: () => Array + readonly strings: Array + } + >() {} + ``` + + And use them like: + + ```ts + DemoTag.getNumbers() + DemoTag.strings + ``` + + This fuses together `serviceFunctions` and `serviceConstants` in the static side of the tag. + + Additionally it allows using the service like: + + ```ts + DemoTag.use((_) => _.getNumbers()) + ``` + + This is especially useful when having functions that contain generics in the service given that those can't be reliably transformed at the type level and because of that we can't put them on the tag. + +- [#2238](https://github.com/Effect-TS/effect/pull/2238) [`6137533`](https://github.com/Effect-TS/effect/commit/613753300c7705518ab1fea2f370b032851c2750) Thanks [@JJayet](https://github.com/JJayet)! - Request: swap Success and Error params + +- [#2270](https://github.com/Effect-TS/effect/pull/2270) [`f373529`](https://github.com/Effect-TS/effect/commit/f373529999f4b8bc92b634f6ea14f19271388eed) Thanks [@tim-smart](https://github.com/tim-smart)! - add structured logging apis + - Logger.json / Logger.jsonLogger + - Logger.structured / Logger.structuredLogger + + `Logger.json` logs JSON serialized strings to the console. + + `Logger.structured` logs structured objects, which is useful in the browser + where you can inspect objects logged to the console. + +- [#2257](https://github.com/Effect-TS/effect/pull/2257) [`1bf9f31`](https://github.com/Effect-TS/effect/commit/1bf9f31f07667de677673f7c29a4e7a26ebad3c8) Thanks [@mikearnaldi](https://github.com/mikearnaldi)! - Make sure Effect.Tag works on primitives. + + This change allows the following to work just fine: + + ```ts + import { Effect, Layer } from "effect" + + class DateTag extends Effect.Tag("DateTag")() { + static date = new Date(1970, 1, 1) + static Live = Layer.succeed(this, this.date) + } + + class MapTag extends Effect.Tag("MapTag")>() { + static Live = Layer.effect( + this, + Effect.sync(() => new Map()) + ) + } + + class NumberTag extends Effect.Tag("NumberTag")() { + static Live = Layer.succeed(this, 100) + } + ``` + +- [#2244](https://github.com/Effect-TS/effect/pull/2244) [`e3ff789`](https://github.com/Effect-TS/effect/commit/e3ff789226f89e71eb28ca38ce79f90af6a03f1a) Thanks [@tim-smart](https://github.com/tim-smart)! - add FiberMap/FiberSet.join api + + This api can be used to propogate failures back to a parent fiber, in case any of the fibers added to the FiberMap/FiberSet fail with an error. + + Example: + + ```ts + import { Effect, FiberSet } from "effect" + + Effect.gen(function* (_) { + const set = yield* _(FiberSet.make()) + yield* _(FiberSet.add(set, Effect.runFork(Effect.fail("error")))) + + // parent fiber will fail with "error" + yield* _(FiberSet.join(set)) + }) + ``` + +- [#2238](https://github.com/Effect-TS/effect/pull/2238) [`6137533`](https://github.com/Effect-TS/effect/commit/613753300c7705518ab1fea2f370b032851c2750) Thanks [@JJayet](https://github.com/JJayet)! - make Effect.request dual + +- [#2263](https://github.com/Effect-TS/effect/pull/2263) [`507ba40`](https://github.com/Effect-TS/effect/commit/507ba4060ff043c1a8d541dae723fa6940633b00) Thanks [@thewilkybarkid](https://github.com/thewilkybarkid)! - Allow duration inputs to be singular + +- [#2255](https://github.com/Effect-TS/effect/pull/2255) [`e466afe`](https://github.com/Effect-TS/effect/commit/e466afe32f2de598ceafd8982bd0cfbd388e5671) Thanks [@jessekelly881](https://github.com/jessekelly881)! - added Either.Either.{Left,Right} and Option.Option.Value type utils + +- [#2270](https://github.com/Effect-TS/effect/pull/2270) [`f373529`](https://github.com/Effect-TS/effect/commit/f373529999f4b8bc92b634f6ea14f19271388eed) Thanks [@tim-smart](https://github.com/tim-smart)! - add Logger.batched, for batching logger output + + It takes a duration window and an effectful function that processes the batched output. + + Example: + + ```ts + import { Console, Effect, Logger } from "effect" + + const LoggerLive = Logger.replaceScoped( + Logger.defaultLogger, + Logger.logfmtLogger.pipe( + Logger.batched("500 millis", (messages) => + Console.log("BATCH", messages.join("\n")) + ) + ) + ) + + Effect.gen(function* (_) { + yield* _(Effect.log("one")) + yield* _(Effect.log("two")) + yield* _(Effect.log("three")) + }).pipe(Effect.provide(LoggerLive), Effect.runFork) + ``` + +- [#2233](https://github.com/Effect-TS/effect/pull/2233) [`de74eb8`](https://github.com/Effect-TS/effect/commit/de74eb80a79eebde5ff645033765e7a617e92f27) Thanks [@gcanti](https://github.com/gcanti)! - Struct: make `pick` / `omit` dual + +## 2.4.1 + +### Patch Changes + +- [#2219](https://github.com/Effect-TS/effect/pull/2219) [`a4a0006`](https://github.com/Effect-TS/effect/commit/a4a0006c7f19fc261df5cda16963d73457e4d6ac) Thanks [@KhraksMamtsov](https://github.com/KhraksMamtsov)! - fix documentation for `Predicate.isNull` `Predicate.isNotNull` + +- [#2223](https://github.com/Effect-TS/effect/pull/2223) [`0a37676`](https://github.com/Effect-TS/effect/commit/0a37676aa0eb2a21e17af2e6df9f81f52bbc8831) Thanks [@Schniz](https://github.com/Schniz)! - document Effect.zipLeft and Effect.zipRight + +- [#2224](https://github.com/Effect-TS/effect/pull/2224) [`6f503b7`](https://github.com/Effect-TS/effect/commit/6f503b774d893bf2af34f66202e270d8c45d5f31) Thanks [@jessekelly881](https://github.com/jessekelly881)! - added isSet and isMap to Predicate module + +## 2.4.0 + +### Minor Changes + +- [#2101](https://github.com/Effect-TS/effect/pull/2101) [`5de7be5`](https://github.com/Effect-TS/effect/commit/5de7be5beca2e963b503e6029dcc3217848187d2) Thanks [@github-actions](https://github.com/apps/github-actions)! - remove ReadonlyRecord.fromIterable (duplicate of fromEntries) + +- [#2101](https://github.com/Effect-TS/effect/pull/2101) [`489fcf3`](https://github.com/Effect-TS/effect/commit/489fcf363ff2b2a953166b740cb9a62d7fc2a101) Thanks [@github-actions](https://github.com/apps/github-actions)! - - swap `Schedule` type parameters from `Schedule` to `Schedule`, closes #2154 + - swap `ScheduleDriver` type parameters from `ScheduleDriver` to `ScheduleDriver` + +- [#2101](https://github.com/Effect-TS/effect/pull/2101) [`7d9c3bf`](https://github.com/Effect-TS/effect/commit/7d9c3bff6c18d451e0e4781042945ec5c7be1b9f) Thanks [@github-actions](https://github.com/apps/github-actions)! - Consolidate `Effect.asyncOption`, `Effect.asyncEither`, `Stream.asyncOption`, `Stream.asyncEither`, and `Stream.asyncInterrupt` + + This PR removes `Effect.asyncOption` and `Effect.asyncEither` as their behavior can be entirely implemented with the new signature of `Effect.async`, which optionally returns a cleanup `Effect` from the registration callback. + + ```ts + declare const async: ( + register: ( + callback: (_: Effect) => void, + signal: AbortSignal + ) => void | Effect, + blockingOn?: FiberId + ) => Effect + ``` + + Additionally, this PR removes `Stream.asyncOption`, `Stream.asyncEither`, and `Stream.asyncInterrupt` as their behavior can be entirely implemented with the new signature of `Stream.async`, which can optionally return a cleanup `Effect` from the registration callback. + + ```ts + declare const async: ( + register: (emit: Emit) => Effect | void, + outputBuffer?: number + ) => Stream + ``` + +- [#2101](https://github.com/Effect-TS/effect/pull/2101) [`d8d278b`](https://github.com/Effect-TS/effect/commit/d8d278b2efb2966947029885e01f7b68348a021f) Thanks [@github-actions](https://github.com/apps/github-actions)! - swap `GroupBy` type parameters from `GroupBy` to `GroupBy` + +- [#2101](https://github.com/Effect-TS/effect/pull/2101) [`14c5711`](https://github.com/Effect-TS/effect/commit/14c57110078f0862b8da5c7a2c5d980f54447484) Thanks [@github-actions](https://github.com/apps/github-actions)! - Remove Effect.unified and Effect.unifiedFn in favour of Unify.unify. + + The `Unify` module fully replaces the need for specific unify functions, when before you did: + + ```ts + import { Effect } from "effect" + + const effect = Effect.unified( + Math.random() > 0.5 ? Effect.succeed("OK") : Effect.fail("NO") + ) + const effectFn = Effect.unifiedFn((n: number) => + Math.random() > 0.5 ? Effect.succeed("OK") : Effect.fail("NO") + ) + ``` + + You can now do: + + ```ts + import { Effect, Unify } from "effect" + + const effect = Unify.unify( + Math.random() > 0.5 ? Effect.succeed("OK") : Effect.fail("NO") + ) + const effectFn = Unify.unify((n: number) => + Math.random() > 0.5 ? Effect.succeed("OK") : Effect.fail("NO") + ) + ``` + +- [#2101](https://github.com/Effect-TS/effect/pull/2101) [`5de7be5`](https://github.com/Effect-TS/effect/commit/5de7be5beca2e963b503e6029dcc3217848187d2) Thanks [@github-actions](https://github.com/apps/github-actions)! - add key type to ReadonlyRecord + +- [#2101](https://github.com/Effect-TS/effect/pull/2101) [`585fcce`](https://github.com/Effect-TS/effect/commit/585fcce162d0f07a48d7cd984a9b722966fbebbe) Thanks [@github-actions](https://github.com/apps/github-actions)! - add support for optional property keys to `pick`, `omit` and `get` + + Before: + + ```ts + import { pipe } from "effect/Function" + import * as S from "effect/Struct" + + const struct: { + a?: string + b: number + c: boolean + } = { b: 1, c: true } + + // error + const x = pipe(struct, S.pick("a", "b")) + + const record: Record = {} + + const y = pipe(record, S.pick("a", "b")) + console.log(y) // => { a: undefined, b: undefined } + + // error + console.log(pipe(struct, S.get("a"))) + ``` + + Now + + ```ts + import { pipe } from "effect/Function" + import * as S from "effect/Struct" + + const struct: { + a?: string + b: number + c: boolean + } = { b: 1, c: true } + + const x = pipe(struct, S.pick("a", "b")) + console.log(x) // => { b: 1 } + + const record: Record = {} + + const y = pipe(record, S.pick("a", "b")) + console.log(y) // => {} + + console.log(pipe(struct, S.get("a"))) // => undefined + ``` + +- [#2101](https://github.com/Effect-TS/effect/pull/2101) [`a025b12`](https://github.com/Effect-TS/effect/commit/a025b121235ba01cfce8d62a775491880c575561) Thanks [@github-actions](https://github.com/apps/github-actions)! - Swap type params of Either from `Either` to `Either`. + + Along the same line of the other changes this allows to shorten the most common types such as: + + ```ts + import { Either } from "effect" + + const right: Either.Either = Either.right("ok") + ``` + +### Patch Changes + +- [#2193](https://github.com/Effect-TS/effect/pull/2193) [`b9cb3a9`](https://github.com/Effect-TS/effect/commit/b9cb3a9c9bfdd75536bd70b4e8b557c12d4923ff) Thanks [@jessekelly881](https://github.com/jessekelly881)! - added Number.parse, BigInt.toNumber, ParseResult.fromOption + +- [#2101](https://github.com/Effect-TS/effect/pull/2101) [`93b412d`](https://github.com/Effect-TS/effect/commit/93b412d4a9ed762dc9fa5807e51fad0fc78a614a) Thanks [@github-actions](https://github.com/apps/github-actions)! - ReadonlyArray.groupBy: allow for grouping by symbols, closes #2180 + +- [#2101](https://github.com/Effect-TS/effect/pull/2101) [`55b26a6`](https://github.com/Effect-TS/effect/commit/55b26a6342b4826f1116e7a1eb660118c274458e) Thanks [@github-actions](https://github.com/apps/github-actions)! - Either: fix `fromOption` overloads order + +- [#2101](https://github.com/Effect-TS/effect/pull/2101) [`2097739`](https://github.com/Effect-TS/effect/commit/20977393d2383bff709304e81ec7d51cafd57108) Thanks [@github-actions](https://github.com/apps/github-actions)! - Add Do notation methods `Do`, `bindTo`, `bind` and `let` to Either + +## 2.3.8 + +### Patch Changes + +- [#2167](https://github.com/Effect-TS/effect/pull/2167) [`5ad2eec`](https://github.com/Effect-TS/effect/commit/5ad2eece0280b6db6a749d25cac1dcf6d33659a9) Thanks [@tim-smart](https://github.com/tim-smart)! - add Hash.cached + + This api assists with adding a layer of caching, when hashing immutable data structures. + + ```ts + import { Data, Hash } from "effect" + + class User extends Data.Class<{ + id: number + name: string + }> { + [Hash.symbol]() { + return Hash.cached(this, Hash.string(`${this.id}-${this.name}`)) + } + } + ``` + +- [#2187](https://github.com/Effect-TS/effect/pull/2187) [`e6d36c0`](https://github.com/Effect-TS/effect/commit/e6d36c0813d836f17eabb6a9c7849baffca12dbf) Thanks [@tim-smart](https://github.com/tim-smart)! - update development dependencies + +## 2.3.7 + +### Patch Changes + +- [#2142](https://github.com/Effect-TS/effect/pull/2142) [`bc8404d`](https://github.com/Effect-TS/effect/commit/bc8404d54fd42072d200c0399cb39672837afa9f) Thanks [@mikearnaldi](https://github.com/mikearnaldi)! - Expose version control via ModuleVersion. + + This enables low level framework authors to run their own effect version which won't conflict with any other effect versions running on the same process. + + Imagine cases where for example a function runtime is built on effect, we don't want lifecycle of the runtime to clash with lifecycle of user-land provided code. + + To manually control the module version one can use: + + ```ts + import * as ModuleVersion from "effect/ModuleVersion" + + ModuleVersion.setCurrentVersion( + `my-effect-runtime-${ModuleVersion.getCurrentVersion()}` + ) + ``` + + Note that this code performs side effects and should be executed before any module is imported ideally via an init script. + + The resulting order of execution has to be: + + ```ts + import * as ModuleVersion from "effect/ModuleVersion" + + ModuleVersion.setCurrentVersion( + `my-effect-runtime-${ModuleVersion.getCurrentVersion()}` + ) + + import { Effect } from "effect" + + // rest of code + ``` + +- [#2159](https://github.com/Effect-TS/effect/pull/2159) [`2c5cbcd`](https://github.com/Effect-TS/effect/commit/2c5cbcd1161b4f40dab184999291e817314107de) Thanks [@IMax153](https://github.com/IMax153)! - Avoid incrementing cache hits for expired entries + +- [#2165](https://github.com/Effect-TS/effect/pull/2165) [`6565916`](https://github.com/Effect-TS/effect/commit/6565916ef254bf910e47d25fd0ef55e7cb420241) Thanks [@tim-smart](https://github.com/tim-smart)! - fix Hash implemention for Option.none + +## 2.3.6 + +### Patch Changes + +- [#2145](https://github.com/Effect-TS/effect/pull/2145) [`b1163b2`](https://github.com/Effect-TS/effect/commit/b1163b2bd67b65bafbbb39fc4c67576e5cbaf444) Thanks [@tim-smart](https://github.com/tim-smart)! - add RequestResolver.aroundRequests api + + This can be used to run side effects that introspect the requests being + executed. + + Example: + + ```ts + import { Effect, Request, RequestResolver } from "effect" + + interface GetUserById extends Request.Request { + readonly id: number + } + + declare const resolver: RequestResolver.RequestResolver + + RequestResolver.aroundRequests( + resolver, + (requests) => Effect.log(`got ${requests.length} requests`), + (requests, _) => Effect.log(`finised running ${requests.length} requests`) + ) + ``` + +- [#2148](https://github.com/Effect-TS/effect/pull/2148) [`b46b869`](https://github.com/Effect-TS/effect/commit/b46b869e59a6da5aa235a9fcc25e1e0d24e9e8f8) Thanks [@riordanpawley](https://github.com/riordanpawley)! - Flipped scheduleForked types to match new signature + +- [#2139](https://github.com/Effect-TS/effect/pull/2139) [`de1b226`](https://github.com/Effect-TS/effect/commit/de1b226282b5ab6c2809dd93f3bdb066f24a1333) Thanks [@mikearnaldi](https://github.com/mikearnaldi)! - Introduce FiberId.Single, make FiberId.None behave like FiberId.Runtime, relax FiberRefs to use Single instead of Runtime. + + This change is a precursor to enable easier APIs to modify the Runtime when patching FiberRefs. + +- [#2137](https://github.com/Effect-TS/effect/pull/2137) [`a663390`](https://github.com/Effect-TS/effect/commit/a66339090ae7b960f8a8b90a0dcdc505de5aaf3e) Thanks [@mikearnaldi](https://github.com/mikearnaldi)! - Expose Random Tag and functions to use a specific random service implementation + +- [#2143](https://github.com/Effect-TS/effect/pull/2143) [`ff88f80`](https://github.com/Effect-TS/effect/commit/ff88f808c4ed9947a148045849e7410b00acad0a) Thanks [@mikearnaldi](https://github.com/mikearnaldi)! - Fix Cause.pretty when toString is invalid + + ```ts + import { Cause } from "effect" + + console.log(Cause.pretty(Cause.fail([{ toString: "" }]))) + ``` + + The code above used to throw now it prints: + + ```bash + Error: [{"toString":""}] + ``` + +- [#2080](https://github.com/Effect-TS/effect/pull/2080) [`11be07b`](https://github.com/Effect-TS/effect/commit/11be07bf65d82cfdf994cdb9d8ca937f995cb4f0) Thanks [@KhraksMamtsov](https://github.com/KhraksMamtsov)! - Add functional analogue of `satisfies` operator. + This is a convenient operator to use in the `pipe` chain to localize type errors closer to their source. + + ```ts + import { satisfies } from "effect/Function" + + const test1 = satisfies()(5 as const) + // ^? const test: 5 + + // @ts-expect-error + const test2 = satisfies()(5) + // ^? Argument of type 'number' is not assignable to parameter of type 'string' + ``` + +- [#2147](https://github.com/Effect-TS/effect/pull/2147) [`c568645`](https://github.com/Effect-TS/effect/commit/c5686451c87d26382135a1c63b00ef171bb24f62) Thanks [@tim-smart](https://github.com/tim-smart)! - generate a random span id for the built-in tracer + + This ensures the same span id isn't used between application runs. + +- [#2144](https://github.com/Effect-TS/effect/pull/2144) [`88835e5`](https://github.com/Effect-TS/effect/commit/88835e575a0bfbeff9a3696a332f32192c940e12) Thanks [@mikearnaldi](https://github.com/mikearnaldi)! - Fix withRandom and withClock types + +- [#2138](https://github.com/Effect-TS/effect/pull/2138) [`b415577`](https://github.com/Effect-TS/effect/commit/b415577f6c576073733929c858e5aac27b6d5880) Thanks [@mikearnaldi](https://github.com/mikearnaldi)! - Fix internals of TestAnnotationsMap making it respect equality + +- [#2149](https://github.com/Effect-TS/effect/pull/2149) [`ff8046f`](https://github.com/Effect-TS/effect/commit/ff8046f57dfd073eba60ce6d3144ab060fbf93ce) Thanks [@tim-smart](https://github.com/tim-smart)! - add Runtime.updateFiberRefs/setFiberRef/deleteFiberRef + + This change allows you to update fiber ref values inside a Runtime object. + + Example: + + ```ts + import { Effect, FiberRef, Runtime } from "effect" + + const ref = FiberRef.unsafeMake(0) + + const updatedRuntime = Runtime.defaultRuntime.pipe( + Runtime.setFiberRef(ref, 1) + ) + + // returns 1 + const result = Runtime.runSync(updatedRuntime)(FiberRef.get(ref)) + ``` + +## 2.3.5 + +### Patch Changes + +- [#2114](https://github.com/Effect-TS/effect/pull/2114) [`b881365`](https://github.com/Effect-TS/effect/commit/b8813650355322ea2fc1fbaa4f846bd87a7a05f3) Thanks [@IMax153](https://github.com/IMax153)! - Fix the ordering of results returned from batched requests + +## 2.3.4 + +### Patch Changes + +- [#2107](https://github.com/Effect-TS/effect/pull/2107) [`17bda66`](https://github.com/Effect-TS/effect/commit/17bda66431c999a546920c10adb205e6c8bea7d1) Thanks [@tim-smart](https://github.com/tim-smart)! - ensure large semaphore takes don't block smaller takes + +## 2.3.3 + +### Patch Changes + +- [#2090](https://github.com/Effect-TS/effect/pull/2090) [`efd41d8`](https://github.com/Effect-TS/effect/commit/efd41d8131c3d90867608969ef7c4eef490eb5e6) Thanks [@hsubra89](https://github.com/hsubra89)! - Update `RateLimiter` to support passing in a custom `cost` per effect. This is really useful for API(s) that have a "credit cost" per endpoint. + + Usage Example : + + ```ts + import { Effect, RateLimiter } from "effect" + import { compose } from "effect/Function" + + const program = Effect.scoped( + Effect.gen(function* ($) { + // Create a rate limiter that has an hourly limit of 1000 credits + const rateLimiter = yield* $(RateLimiter.make(1000, "1 hours")) + // Query API costs 1 credit per call ( 1 is the default cost ) + const queryAPIRL = compose(rateLimiter, RateLimiter.withCost(1)) + // Mutation API costs 5 credits per call + const mutationAPIRL = compose(rateLimiter, RateLimiter.withCost(5)) + // ... + // Use the pre-defined rate limiters + yield* $(queryAPIRL(Effect.log("Sample Query"))) + yield* $(mutationAPIRL(Effect.log("Sample Mutation"))) + + // Or set a cost on-the-fly + yield* $( + rateLimiter(Effect.log("Another query with a different cost")).pipe( + RateLimiter.withCost(3) + ) + ) + }) + ) + ``` + +- [#2097](https://github.com/Effect-TS/effect/pull/2097) [`0f83515`](https://github.com/Effect-TS/effect/commit/0f83515a9c01d13c7c15a3f026e02d22c3c6bb7f) Thanks [@IMax153](https://github.com/IMax153)! - Updates the `RateLimiter.make` constructor to take an object of `RateLimiter.Options`, which allows for specifying the rate-limiting algorithm to utilize: + + You can choose from either the `token-bucket` or the `fixed-window` algorithms for rate-limiting. + + ```ts + export declare namespace RateLimiter { + export interface Options { + /** + * The maximum number of requests that should be allowed. + */ + readonly limit: number + /** + * The interval to utilize for rate-limiting requests. The semantics of the + * specified `interval` vary depending on the chosen `algorithm`: + * + * `token-bucket`: The maximum number of requests will be spread out over + * the provided interval if no tokens are available. + * + * For example, for a `RateLimiter` using the `token-bucket` algorithm with + * a `limit` of `10` and an `interval` of `1 seconds`, `1` request can be + * made every `100 millis`. + * + * `fixed-window`: The maximum number of requests will be reset during each + * interval. For example, for a `RateLimiter` using the `fixed-window` + * algorithm with a `limit` of `10` and an `interval` of `1 seconds`, a + * maximum of `10` requests can be made each second. + */ + readonly interval: DurationInput + /** + * The algorithm to utilize for rate-limiting requests. + * + * Defaults to `token-bucket`. + */ + readonly algorithm?: "fixed-window" | "token-bucket" + } + } + ``` + +- [#2097](https://github.com/Effect-TS/effect/pull/2097) [`0f83515`](https://github.com/Effect-TS/effect/commit/0f83515a9c01d13c7c15a3f026e02d22c3c6bb7f) Thanks [@IMax153](https://github.com/IMax153)! - return the resulting available permits from Semaphore.release + +## 2.3.2 + +### Patch Changes + +- [#2096](https://github.com/Effect-TS/effect/pull/2096) [`6654f5f`](https://github.com/Effect-TS/effect/commit/6654f5f0f6b9d97165ede5e04ca16776e2599328) Thanks [@tim-smart](https://github.com/tim-smart)! - default to `never` for Runtime returning functions + + This includes: + - Effect.runtime + - FiberSet.makeRuntime + + It prevents `unknown` from creeping into types, as well as `never` being a + useful default type for propogating Fiber Refs and other context. + +- [#2094](https://github.com/Effect-TS/effect/pull/2094) [`2eb11b4`](https://github.com/Effect-TS/effect/commit/2eb11b47752cedf233ef4c4395d9c4efc9b9e180) Thanks [@tim-smart](https://github.com/tim-smart)! - revert some type param adjustments in FiberSet + + `makeRuntime` now has the R parameter first again. + + Default to `unknown` for the A and E parameters instead of never. + +- [#2103](https://github.com/Effect-TS/effect/pull/2103) [`56c09bd`](https://github.com/Effect-TS/effect/commit/56c09bd369279a6a7785209d172739935818cba6) Thanks [@patroza](https://github.com/patroza)! - Expand Either and Option `andThen` to support the `map` case like Effects' `andThen` + + For example: + + ```ts + expect(pipe(Either.right(1), Either.andThen(2))).toStrictEqual( + Either.right(2) + ) + expect( + pipe( + Either.right(1), + Either.andThen(() => 2) + ) + ).toStrictEqual(Either.right(2)) + + expect(pipe(Option.some(1), Option.andThen(2))).toStrictEqual(Option.some(2)) + expect( + pipe( + Option.some(1), + Option.andThen(() => 2) + ) + ).toStrictEqual(Option.some(2)) + ``` + +- [#2098](https://github.com/Effect-TS/effect/pull/2098) [`71aa5b1`](https://github.com/Effect-TS/effect/commit/71aa5b1c180dcb8b53aefe232d12a97bd06b5447) Thanks [@ethanniser](https://github.com/ethanniser)! - removed `./internal/timeout` and replaced all usages with `setTimeout` directly + + previously it was required to abstract away conditionally solving an bun had an issue with `setTimeout`, that caused incorrect behavior + that bug has since been fixed, and the `isBun` check is no longer needed + as such the timeout module is also no longer needed + +- [#2099](https://github.com/Effect-TS/effect/pull/2099) [`1700af8`](https://github.com/Effect-TS/effect/commit/1700af8af1131602887da721914c8562b6342393) Thanks [@tim-smart](https://github.com/tim-smart)! - optimize Effect.zip{Left,Right} + + for the sequential case, avoid using Effect.all internally + +## 2.3.1 + +### Patch Changes + +- [#2085](https://github.com/Effect-TS/effect/pull/2085) [`b5a8215`](https://github.com/Effect-TS/effect/commit/b5a8215ee2a97a8865d69ee55ce1b9835948c922) Thanks [@gcanti](https://github.com/gcanti)! - Fix Schedule typings (some APIs didn't have Effect parameters swapped). + +## 2.3.0 + +### Minor Changes + +- [#2006](https://github.com/Effect-TS/effect/pull/2006) [`96bcee2`](https://github.com/Effect-TS/effect/commit/96bcee21021aecd8ffd86440a2c9be353c4668e3) Thanks [@github-actions](https://github.com/apps/github-actions)! - change `Runtime.AsyncFiberException` type parameters order from `AsyncFiberException` to `AsyncFiberException` + +- [#2006](https://github.com/Effect-TS/effect/pull/2006) [`96bcee2`](https://github.com/Effect-TS/effect/commit/96bcee21021aecd8ffd86440a2c9be353c4668e3) Thanks [@github-actions](https://github.com/apps/github-actions)! - change `Runtime.Cancel` type parameters order from `Cancel` to `Cancel` + +- [#2006](https://github.com/Effect-TS/effect/pull/2006) [`c77f635`](https://github.com/Effect-TS/effect/commit/c77f635f8a26ca6d83cb569d911f8eee79033fd9) Thanks [@github-actions](https://github.com/apps/github-actions)! - change `Exit` type parameter order from `Exit` to `Exit` + +- [#2006](https://github.com/Effect-TS/effect/pull/2006) [`e343a74`](https://github.com/Effect-TS/effect/commit/e343a74843dd9edf879417fa94cb51de7ed5b402) Thanks [@github-actions](https://github.com/apps/github-actions)! - change `Resource` type parameters order from `Resource` to `Resource` + +- [#2006](https://github.com/Effect-TS/effect/pull/2006) [`acf1894`](https://github.com/Effect-TS/effect/commit/acf1894f45945dbe5c39451e36aabb4b5092f257) Thanks [@github-actions](https://github.com/apps/github-actions)! - change `FiberMap` type parameters order from `FiberMap` to `FiberMap` + +- [#2006](https://github.com/Effect-TS/effect/pull/2006) [`9a2d1c1`](https://github.com/Effect-TS/effect/commit/9a2d1c1468ea0789b34767ad683da074f061ea9c) Thanks [@github-actions](https://github.com/apps/github-actions)! - With this change we now require a string key to be provided for all tags and renames the dear old `Tag` to `GenericTag`, so when previously you could do: + + ```ts + import { Effect, Context } from "effect" + interface Service { + readonly _: unique symbol + } + const Service = Context.Tag< + Service, + { + number: Effect.Effect + } + >() + ``` + + you are now mandated to do: + + ```ts + import { Effect, Context } from "effect" + interface Service { + readonly _: unique symbol + } + const Service = Context.GenericTag< + Service, + { + number: Effect.Effect + } + >("Service") + ``` + + This makes by default all tags globals and ensures better debuggaility when unexpected errors arise. + + Furthermore we introduce a new way of constructing tags that should be considered the new default: + + ```ts + import { Effect, Context } from "effect" + class Service extends Context.Tag("Service")< + Service, + { + number: Effect.Effect + } + >() {} + + const program = Effect.flatMap(Service, ({ number }) => number).pipe( + Effect.flatMap((_) => Effect.log(`number: ${_}`)) + ) + ``` + + this will use "Service" as the key and will create automatically an opaque identifier (the class) to be used at the type level, it does something similar to the above in a single shot. + +- [#2006](https://github.com/Effect-TS/effect/pull/2006) [`1a77f72`](https://github.com/Effect-TS/effect/commit/1a77f72cdaf43d6cdc91b6060f82832edcdbbcb3) Thanks [@github-actions](https://github.com/apps/github-actions)! - change `Effect` type parameters order from `Effect` to `Effect` + +- [#2006](https://github.com/Effect-TS/effect/pull/2006) [`c986f0e`](https://github.com/Effect-TS/effect/commit/c986f0e0ce4d22ba08177ed351152718479ab63c) Thanks [@github-actions](https://github.com/apps/github-actions)! - change `FiberSet` type parameters order from `FiberSet` to `FiberSet` + +- [#2006](https://github.com/Effect-TS/effect/pull/2006) [`96bcee2`](https://github.com/Effect-TS/effect/commit/96bcee21021aecd8ffd86440a2c9be353c4668e3) Thanks [@github-actions](https://github.com/apps/github-actions)! - change `Runtime.RunCallbackOptions` type parameters order from `RunCallbackOptions` to `RunCallbackOptions` + +- [#2006](https://github.com/Effect-TS/effect/pull/2006) [`70dde23`](https://github.com/Effect-TS/effect/commit/70dde238f81125e353fd7bde5fc24ecd8969bf97) Thanks [@github-actions](https://github.com/apps/github-actions)! - change `TDeferred` type parameters order from `TDeferred` to `TDeferred` + +- [#2006](https://github.com/Effect-TS/effect/pull/2006) [`81b7425`](https://github.com/Effect-TS/effect/commit/81b7425320cbbe2a6cf547a3e3ab3549cdba14cf) Thanks [@github-actions](https://github.com/apps/github-actions)! - change `Streamable.Class` and `Effectable.Class` type parameters order from `Class` to `Class` + +- [#2006](https://github.com/Effect-TS/effect/pull/2006) [`02c3461`](https://github.com/Effect-TS/effect/commit/02c34615d02f91269ea04036d0306fccf4e39e18) Thanks [@github-actions](https://github.com/apps/github-actions)! - With this change we remove the `Data.Data` type and we make `Equal.Equal` & `Hash.Hash` implicit traits. + + The main reason is that `Data.Data` was structurally equivalent to `A & Equal.Equal` but extending `Equal.Equal` doesn't mean that the equality is implemented by-value, so the type was simply adding noise without gaining any level of safety. + + The module `Data` remains unchanged at the value level, all the functions previously available are supposed to work in exactly the same manner. + + At the type level instead the functions return `Readonly` variants, so for example we have: + + ```ts + import { Data } from "effect" + + const obj = Data.struct({ + a: 0, + b: 1 + }) + ``` + + will have the `obj` typed as: + + ```ts + declare const obj: { + readonly a: number + readonly b: number + } + ``` + +- [#2006](https://github.com/Effect-TS/effect/pull/2006) [`0e56e99`](https://github.com/Effect-TS/effect/commit/0e56e998ab9815c4d096c239a553cb86a0f99af9) Thanks [@github-actions](https://github.com/apps/github-actions)! - change `Deferred` type parameters order from `Deferred` to `Deferred` + +- [#2006](https://github.com/Effect-TS/effect/pull/2006) [`8b0ded9`](https://github.com/Effect-TS/effect/commit/8b0ded9f10ba0d96fcb9af24eff2dbd9341f85e3) Thanks [@github-actions](https://github.com/apps/github-actions)! - change `Fiber` type parameters order from `Fiber` to `Fiber` + +- [#2006](https://github.com/Effect-TS/effect/pull/2006) [`8dd83e8`](https://github.com/Effect-TS/effect/commit/8dd83e854bfcaa6dab876994c5f813dcfb486c28) Thanks [@github-actions](https://github.com/apps/github-actions)! - change `Channel` type parameters order from `Channel` to `Channel` + +- [#2006](https://github.com/Effect-TS/effect/pull/2006) [`d75f6fe`](https://github.com/Effect-TS/effect/commit/d75f6fe6499deb0a5ee9ec94af3b5fd4eb03a2d0) Thanks [@github-actions](https://github.com/apps/github-actions)! - change `Take` type parameters order from `Take` to `Take` + +- [#2006](https://github.com/Effect-TS/effect/pull/2006) [`7356e5c`](https://github.com/Effect-TS/effect/commit/7356e5cc16e9d70f18c02dee1dcb4ad539fd130a) Thanks [@github-actions](https://github.com/apps/github-actions)! - change `STM` type parameters order from `STM` to `STM` + +- [#2006](https://github.com/Effect-TS/effect/pull/2006) [`3077cde`](https://github.com/Effect-TS/effect/commit/3077cde08a60246821a940964a84dd7f7c8b9f54) Thanks [@github-actions](https://github.com/apps/github-actions)! - change `Stream` type parameters order from `Stream` to `Stream` + +- [#2006](https://github.com/Effect-TS/effect/pull/2006) [`78f47ab`](https://github.com/Effect-TS/effect/commit/78f47abfe3cb0a8bbde818b1c5fc603270538b47) Thanks [@github-actions](https://github.com/apps/github-actions)! - change `Pool` type parameters order from `Pool` to `Pool`, and `KeyedPool` from `KeyedPool` to `KeyedPool` + +- [#2006](https://github.com/Effect-TS/effect/pull/2006) [`52e5d20`](https://github.com/Effect-TS/effect/commit/52e5d2077582bf51f25861c7139fc920c2c24166) Thanks [@github-actions](https://github.com/apps/github-actions)! - change `Request` type parameters order from `Request` to `Request` + +- [#2006](https://github.com/Effect-TS/effect/pull/2006) [`c6137ec`](https://github.com/Effect-TS/effect/commit/c6137ec62c6b5542d5062ae1a3c936cb915dee22) Thanks [@github-actions](https://github.com/apps/github-actions)! - change `RuntimeFiber` type parameters order from `RuntimeFiber` to `RuntimeFiber` + +- [#2006](https://github.com/Effect-TS/effect/pull/2006) [`f5ae081`](https://github.com/Effect-TS/effect/commit/f5ae08195e68e76faeac258c565d79da4e01e7d6) Thanks [@github-actions](https://github.com/apps/github-actions)! - Use `TimeoutException` instead of `NoSuchElementException` for timeout. + +- [#2006](https://github.com/Effect-TS/effect/pull/2006) [`60686f5`](https://github.com/Effect-TS/effect/commit/60686f5c38bef1b93a3a0dda9b6596d46aceab03) Thanks [@github-actions](https://github.com/apps/github-actions)! - change `Layer` type parameters order from `Layer` to `Layer` + +- [#2006](https://github.com/Effect-TS/effect/pull/2006) [`9a2d1c1`](https://github.com/Effect-TS/effect/commit/9a2d1c1468ea0789b34767ad683da074f061ea9c) Thanks [@github-actions](https://github.com/apps/github-actions)! - This change enables `Effect.serviceConstants` and `Effect.serviceMembers` to access any constant in the service, not only the effects, namely it is now possible to do: + + ```ts + import { Effect, Context } from "effect" + + class NumberRepo extends Context.TagClass("NumberRepo")< + NumberRepo, + { + readonly numbers: Array + } + >() { + static numbers = Effect.serviceConstants(NumberRepo).numbers + } + ``` + +- [#2006](https://github.com/Effect-TS/effect/pull/2006) [`5127afe`](https://github.com/Effect-TS/effect/commit/5127afec1c519e0a3d7460844a9101a96272f29e) Thanks [@github-actions](https://github.com/apps/github-actions)! - Rename ReadonlyRecord.update to .replace + +- [#2006](https://github.com/Effect-TS/effect/pull/2006) [`8ee2931`](https://github.com/Effect-TS/effect/commit/8ee293159b4f7cb7af8558287a0a047f3a69743d) Thanks [@github-actions](https://github.com/apps/github-actions)! - enhance DX by swapping type parameters and adding defaults to: + - Effect + - async + - asyncOption + - asyncEither + - Stream + - asyncEffect + - asyncInterrupt + - asyncOption + - asyncScoped + - identity + +- [#2006](https://github.com/Effect-TS/effect/pull/2006) [`6727474`](https://github.com/Effect-TS/effect/commit/672747497490a30d36dd49c06db19aabf09dc7f0) Thanks [@github-actions](https://github.com/apps/github-actions)! - change `Sink` type parameters order from `Sink` to `Sink` + +- [#2006](https://github.com/Effect-TS/effect/pull/2006) [`5127afe`](https://github.com/Effect-TS/effect/commit/5127afec1c519e0a3d7460844a9101a96272f29e) Thanks [@github-actions](https://github.com/apps/github-actions)! - rename ReadonlyRecord.upsert to .set + +### Patch Changes + +- [#2006](https://github.com/Effect-TS/effect/pull/2006) [`5127afe`](https://github.com/Effect-TS/effect/commit/5127afec1c519e0a3d7460844a9101a96272f29e) Thanks [@github-actions](https://github.com/apps/github-actions)! - add ReadonlyRecord.modify + +- [#2083](https://github.com/Effect-TS/effect/pull/2083) [`be19ce0`](https://github.com/Effect-TS/effect/commit/be19ce0b8bdf1fac80bb8d7e0b06a86986b47409) Thanks [@mikearnaldi](https://github.com/mikearnaldi)! - Add `Ratelimiter` which limits the number of calls to a resource within a time window using the token bucket algorithm. + + Usage Example: + + ```ts + import { Effect, RateLimiter } from "effect" + + // we need a scope because the rate limiter needs to allocate a state and a background job + const program = Effect.scoped( + Effect.gen(function* ($) { + // create a rate limiter that executes up to 10 requests within 2 seconds + const rateLimit = yield* $(RateLimiter.make(10, "2 seconds")) + // simulate repeated calls + for (let n = 0; n < 100; n++) { + // wrap the effect we want to limit with rateLimit + yield* $(rateLimit(Effect.log("Calling RateLimited Effect"))) + } + }) + ) + + // will print 10 calls immediately and then throttle + program.pipe(Effect.runFork) + ``` + + Or, in a more real world scenario, with a dedicated Service + Layer: + + ```ts + import { Context, Effect, Layer, RateLimiter } from "effect" + + class ApiLimiter extends Context.Tag("@services/ApiLimiter")< + ApiLimiter, + RateLimiter.RateLimiter + >() { + static Live = RateLimiter.make(10, "2 seconds").pipe( + Layer.scoped(ApiLimiter) + ) + } + + const program = Effect.gen(function* ($) { + const rateLimit = yield* $(ApiLimiter) + for (let n = 0; n < 100; n++) { + yield* $(rateLimit(Effect.log("Calling RateLimited Effect"))) + } + }) + + program.pipe(Effect.provide(ApiLimiter.Live), Effect.runFork) + ``` + +- [#2084](https://github.com/Effect-TS/effect/pull/2084) [`4a5d01a`](https://github.com/Effect-TS/effect/commit/4a5d01a409e9b6dd53893e65f8e5c9247f568021) Thanks [@tim-smart](https://github.com/tim-smart)! - simplify RateLimiter implementation using semaphore + +- [#2084](https://github.com/Effect-TS/effect/pull/2084) [`4a5d01a`](https://github.com/Effect-TS/effect/commit/4a5d01a409e9b6dd53893e65f8e5c9247f568021) Thanks [@tim-smart](https://github.com/tim-smart)! - add Number.nextPow2 + + This function returns the next power of 2 from the given number. + + ```ts + import { nextPow2 } from "effect/Number" + + assert.deepStrictEqual(nextPow2(5), 8) + assert.deepStrictEqual(nextPow2(17), 32) + ``` + +## 2.2.5 + +### Patch Changes + +- [#2075](https://github.com/Effect-TS/effect/pull/2075) [`3ddfdbf`](https://github.com/Effect-TS/effect/commit/3ddfdbf914edea536aef207cec6695f33496258c) Thanks [@tim-smart](https://github.com/tim-smart)! - add apis for manipulating context to the Runtime module + + These include: + - `Runtime.updateContext` for modifying the `Context` directly + - `Runtime.provideService` for adding services to an existing Runtime + + Example: + + ```ts + import { Context, Runtime } from "effect" + + interface Name { + readonly _: unique symbol + } + const Name = Context.Tag("Name") + + const runtime: Runtime.Runtime = Runtime.defaultRuntime.pipe( + Runtime.provideService(Name, "John") + ) + ``` + +- [#2075](https://github.com/Effect-TS/effect/pull/2075) [`3ddfdbf`](https://github.com/Effect-TS/effect/commit/3ddfdbf914edea536aef207cec6695f33496258c) Thanks [@tim-smart](https://github.com/tim-smart)! - add apis for patching runtime flags to the Runtime module + + The apis include: + - `Runtime.updateRuntimeFlags` for updating all the flags at once + - `Runtime.enableRuntimeFlag` for enabling a single runtime flag + - `Runtime.disableRuntimeFlag` for disabling a single runtime flag + +## 2.2.4 + +### Patch Changes + +- [#2067](https://github.com/Effect-TS/effect/pull/2067) [`d0b911c`](https://github.com/Effect-TS/effect/commit/d0b911c75f284c7aa87f25aa96926e6bde7690d0) Thanks [@tim-smart](https://github.com/tim-smart)! - add releaseAll api to Semaphore + + You can use `semphore.releaseAll` to atomically release all the permits of a + Semaphore. + +- [#2071](https://github.com/Effect-TS/effect/pull/2071) [`330e1a4`](https://github.com/Effect-TS/effect/commit/330e1a4e2c1fc0af6c80c80c81dd38c3e50fab78) Thanks [@tim-smart](https://github.com/tim-smart)! - add Option.orElseSome + + Allows you to specify a default value for an Option, similar to + Option.getOrElse, except the return value is still an Option. + + ```ts + import * as O from "effect/Option" + import { pipe } from "effect/Function" + + assert.deepStrictEqual( + pipe( + O.none(), + O.orElseSome(() => "b") + ), + O.some("b") + ) + assert.deepStrictEqual( + pipe( + O.some("a"), + O.orElseSome(() => "b") + ), + O.some("a") + ) + ``` + +- [#2057](https://github.com/Effect-TS/effect/pull/2057) [`6928a2b`](https://github.com/Effect-TS/effect/commit/6928a2b0bae86a4bdfbece0aa32924207c2d5a70) Thanks [@joepjoosten](https://github.com/joepjoosten)! - Fix for possible stack overflow errors when using Array.push with spread operator arguments + +- [#2033](https://github.com/Effect-TS/effect/pull/2033) [`296bc1c`](https://github.com/Effect-TS/effect/commit/296bc1c9d24986d299d2669115d584cb27b73c60) Thanks [@rehos](https://github.com/rehos)! - Add toJSON for Secret + +## 2.2.3 + +### Patch Changes + +- [#2004](https://github.com/Effect-TS/effect/pull/2004) [`22794e0`](https://github.com/Effect-TS/effect/commit/22794e0ba00e40281f30a22fa84412003c24877d) Thanks [@IMax153](https://github.com/IMax153)! - add documentation to Effect.intoDeferred + +- [#2007](https://github.com/Effect-TS/effect/pull/2007) [`f73e6c0`](https://github.com/Effect-TS/effect/commit/f73e6c033fb0729a9cfa5eb4bc39f79d3126e247) Thanks [@tim-smart](https://github.com/tim-smart)! - optimize fiber id hashing + +## 2.2.2 + +### Patch Changes + +- [#1970](https://github.com/Effect-TS/effect/pull/1970) [`d404561`](https://github.com/Effect-TS/effect/commit/d404561e47ec2fa5f68709a308ee5d2ee959141d) Thanks [@IMax153](https://github.com/IMax153)! - execute acquire in `ScopedRef` uninterruptibly + +- [#1971](https://github.com/Effect-TS/effect/pull/1971) [`7b84a3c`](https://github.com/Effect-TS/effect/commit/7b84a3c7e4b9c8dc02294b0e3cc3ae3becea977b) Thanks [@IMax153](https://github.com/IMax153)! - race interruptibly in `Channel.mergeAllWith` + +## 2.2.1 + +### Patch Changes + +- [#1964](https://github.com/Effect-TS/effect/pull/1964) [`84da31f`](https://github.com/Effect-TS/effect/commit/84da31f0643e8651b9d311b30526b1e4edfbdfb8) Thanks [@gcanti](https://github.com/gcanti)! - ReadonlyArray: fix `sortWith` sig, closes #1961 + +- [#1958](https://github.com/Effect-TS/effect/pull/1958) [`645bea2`](https://github.com/Effect-TS/effect/commit/645bea2551129f94a5b0e38347e28067dee531bb) Thanks [@gcanti](https://github.com/gcanti)! - Fix signatures related to predicates, closes #1916 + +## 2.2.0 + +### Minor Changes + +- [#1951](https://github.com/Effect-TS/effect/pull/1951) [`ee4ff8a`](https://github.com/Effect-TS/effect/commit/ee4ff8a943141fcf2877af92c5877ee87a989fb9) Thanks [@github-actions](https://github.com/apps/github-actions)! - make data-last FiberSet.run accept an Effect + +- [#1951](https://github.com/Effect-TS/effect/pull/1951) [`ee4ff8a`](https://github.com/Effect-TS/effect/commit/ee4ff8a943141fcf2877af92c5877ee87a989fb9) Thanks [@github-actions](https://github.com/apps/github-actions)! - make data-last FiberMap.run accept an Effect + +### Patch Changes + +- [#1957](https://github.com/Effect-TS/effect/pull/1957) [`202befc`](https://github.com/Effect-TS/effect/commit/202befc2ecbeb117c4fa85ef9b12a3d3a48273d2) Thanks [@IMax153](https://github.com/IMax153)! - cache `FiberId` hash in the constructor + +- [#1951](https://github.com/Effect-TS/effect/pull/1951) [`ee4ff8a`](https://github.com/Effect-TS/effect/commit/ee4ff8a943141fcf2877af92c5877ee87a989fb9) Thanks [@github-actions](https://github.com/apps/github-actions)! - add Fiber{Map,Set}.makeRuntime + +- [#1951](https://github.com/Effect-TS/effect/pull/1951) [`ee4ff8a`](https://github.com/Effect-TS/effect/commit/ee4ff8a943141fcf2877af92c5877ee87a989fb9) Thanks [@github-actions](https://github.com/apps/github-actions)! - add Fiber{Set,Map}.runtime api + +- [#1952](https://github.com/Effect-TS/effect/pull/1952) [`10df798`](https://github.com/Effect-TS/effect/commit/10df798639e556f9d88265ef7fc3cf8a3bbe3874) Thanks [@tim-smart](https://github.com/tim-smart)! - avoid sleep for zero duration in schedule + +## 2.1.2 + +### Patch Changes + +- [#1949](https://github.com/Effect-TS/effect/pull/1949) [`21b9edd`](https://github.com/Effect-TS/effect/commit/21b9edde464f7c5624ef54ad1b5e264204a37625) Thanks [@TylorS](https://github.com/TylorS)! - Fix runFork with Scope + +## 2.1.1 + +### Patch Changes + +- [#1934](https://github.com/Effect-TS/effect/pull/1934) [`a222524`](https://github.com/Effect-TS/effect/commit/a2225247e9de2e013d287320790fde88c081dbbd) Thanks [@gcanti](https://github.com/gcanti)! - ReadonlyRecord: add `mapKeys` / `mapEntries` + +## 2.1.0 + +### Minor Changes + +- [#1919](https://github.com/Effect-TS/effect/pull/1919) [`05c44b3`](https://github.com/Effect-TS/effect/commit/05c44b30662554dde50b70bad79f13ae895fda02) Thanks [@github-actions](https://github.com/apps/github-actions)! - Add immediate:boolean flag to runFork/runCallback + +- [#1919](https://github.com/Effect-TS/effect/pull/1919) [`05c44b3`](https://github.com/Effect-TS/effect/commit/05c44b30662554dde50b70bad79f13ae895fda02) Thanks [@github-actions](https://github.com/apps/github-actions)! - Improve Effect.retry options + +- [#1919](https://github.com/Effect-TS/effect/pull/1919) [`05c44b3`](https://github.com/Effect-TS/effect/commit/05c44b30662554dde50b70bad79f13ae895fda02) Thanks [@github-actions](https://github.com/apps/github-actions)! - remove Effect.retry\* variants + +- [#1919](https://github.com/Effect-TS/effect/pull/1919) [`05c44b3`](https://github.com/Effect-TS/effect/commit/05c44b30662554dde50b70bad79f13ae895fda02) Thanks [@github-actions](https://github.com/apps/github-actions)! - Allow providing Scope to Runtime.runFork + +- [#1919](https://github.com/Effect-TS/effect/pull/1919) [`05c44b3`](https://github.com/Effect-TS/effect/commit/05c44b30662554dde50b70bad79f13ae895fda02) Thanks [@github-actions](https://github.com/apps/github-actions)! - Add RunForkOptions to Effect.runFork + +### Patch Changes + +- [#1919](https://github.com/Effect-TS/effect/pull/1919) [`05c44b3`](https://github.com/Effect-TS/effect/commit/05c44b30662554dde50b70bad79f13ae895fda02) Thanks [@github-actions](https://github.com/apps/github-actions)! - add Effect.repeat options overload + +## 2.0.5 + +### Patch Changes + +- [#1920](https://github.com/Effect-TS/effect/pull/1920) [`f7f19f6`](https://github.com/Effect-TS/effect/commit/f7f19f66a5fa349baa2412c1f9f15111c437df09) Thanks [@tim-smart](https://github.com/tim-smart)! - add FiberMap.remove + +## 2.0.4 + +### Patch Changes + +- [#1897](https://github.com/Effect-TS/effect/pull/1897) [`536c1df`](https://github.com/Effect-TS/effect/commit/536c1dfb7833961dfb2fbd6bcd2dbdfa2f208d51) Thanks [@tim-smart](https://github.com/tim-smart)! - add FiberSet module + +- [#1891](https://github.com/Effect-TS/effect/pull/1891) [`8eec87e`](https://github.com/Effect-TS/effect/commit/8eec87e311ce55281a98517e6df0ef103b43e8a8) Thanks [@gcanti](https://github.com/gcanti)! - Types: add `MatchRecord` + +- [#1871](https://github.com/Effect-TS/effect/pull/1871) [`540b294`](https://github.com/Effect-TS/effect/commit/540b2941dd0a81e9688311583ce7e2e140d6e7a5) Thanks [@SandroMaglione](https://github.com/SandroMaglione)! - added Trie module + +- [#1897](https://github.com/Effect-TS/effect/pull/1897) [`536c1df`](https://github.com/Effect-TS/effect/commit/536c1dfb7833961dfb2fbd6bcd2dbdfa2f208d51) Thanks [@tim-smart](https://github.com/tim-smart)! - add MutableHashMap.clear + +- [#1903](https://github.com/Effect-TS/effect/pull/1903) [`a3f96d6`](https://github.com/Effect-TS/effect/commit/a3f96d615b8b3e238dbfa01ef713c87e6f4532be) Thanks [@fubhy](https://github.com/fubhy)! - Converted value bag classes to object literals + +- [#1891](https://github.com/Effect-TS/effect/pull/1891) [`8eec87e`](https://github.com/Effect-TS/effect/commit/8eec87e311ce55281a98517e6df0ef103b43e8a8) Thanks [@gcanti](https://github.com/gcanti)! - Struct: fix `pick` signature + +- [#1897](https://github.com/Effect-TS/effect/pull/1897) [`536c1df`](https://github.com/Effect-TS/effect/commit/536c1dfb7833961dfb2fbd6bcd2dbdfa2f208d51) Thanks [@tim-smart](https://github.com/tim-smart)! - add FiberMap module + +- [#1891](https://github.com/Effect-TS/effect/pull/1891) [`8eec87e`](https://github.com/Effect-TS/effect/commit/8eec87e311ce55281a98517e6df0ef103b43e8a8) Thanks [@gcanti](https://github.com/gcanti)! - Struct: add `get` + +- [#1891](https://github.com/Effect-TS/effect/pull/1891) [`8eec87e`](https://github.com/Effect-TS/effect/commit/8eec87e311ce55281a98517e6df0ef103b43e8a8) Thanks [@gcanti](https://github.com/gcanti)! - Struct: fix `omit` signature + +- [#1894](https://github.com/Effect-TS/effect/pull/1894) [`25adce7`](https://github.com/Effect-TS/effect/commit/25adce7ae76ce834096dca1ed70a60ad1a349217) Thanks [@tim-smart](https://github.com/tim-smart)! - allow pre-validated cron expressions for Schedule.cron + +- [#1897](https://github.com/Effect-TS/effect/pull/1897) [`536c1df`](https://github.com/Effect-TS/effect/commit/536c1dfb7833961dfb2fbd6bcd2dbdfa2f208d51) Thanks [@tim-smart](https://github.com/tim-smart)! - add MutableHashSet.clear + +## 2.0.3 + +### Patch Changes + +- [#1884](https://github.com/Effect-TS/effect/pull/1884) [`87f7ef2`](https://github.com/Effect-TS/effect/commit/87f7ef28a3c27e2e4f2fcfa465f85bb2a45a3d6b) Thanks [@fubhy](https://github.com/fubhy)! - Added `Cron` module and `Schedule.cron` constructor + +- [#1885](https://github.com/Effect-TS/effect/pull/1885) [`1d3a06b`](https://github.com/Effect-TS/effect/commit/1d3a06bb58ad1ac123ae8f9d42b4345f9c9c53c0) Thanks [@mikearnaldi](https://github.com/mikearnaldi)! - Avoid killing all fibers on interrupt + +## 2.0.2 + +### Patch Changes + +- [#1850](https://github.com/Effect-TS/effect/pull/1850) [`d5a1949`](https://github.com/Effect-TS/effect/commit/d5a19499aac7c1d147674a35ac69992177c7536c) Thanks [@matheuspuel](https://github.com/matheuspuel)! - add index argument to many functions in ReadonlyArray + +## 2.0.1 + +### Patch Changes + +- [#1859](https://github.com/Effect-TS/effect/pull/1859) [`16bd87d`](https://github.com/Effect-TS/effect/commit/16bd87d32611b966dc42ea4fc979764f97a49071) Thanks [@sukovanej](https://github.com/sukovanej)! - Include Config.LiteralValue in dts. + +## 2.0.0 + +### Minor Changes + +- [`d0471ca`](https://github.com/Effect-TS/effect/commit/d0471ca7b544746674b9e1750202da72b0a21233) Thanks [@mikearnaldi](https://github.com/mikearnaldi)! - Switch to monorepo structure + +### Patch Changes + +- [`d987daa`](https://github.com/Effect-TS/effect/commit/d987daafaddd43b6ade74916a08236c19ea0a9fa) Thanks [@mikearnaldi](https://github.com/mikearnaldi)! - Switch effect dependency to caret + +- [#1797](https://github.com/Effect-TS/effect/pull/1797) [`7b5eaa3`](https://github.com/Effect-TS/effect/commit/7b5eaa3838c79bf4bdccf91b94d61bbc38a2ec95) Thanks [@matheuspuel](https://github.com/matheuspuel)! - make serviceFunctions and similar accept an Effect as the service + +- [#1854](https://github.com/Effect-TS/effect/pull/1854) [`0724211`](https://github.com/Effect-TS/effect/commit/072421149c36010748ff6b6ee19c15c6cffefe09) Thanks [@gcanti](https://github.com/gcanti)! - Add Option-returning overloads for findFirst and findLast in ReadonlyArray + +- [#1795](https://github.com/Effect-TS/effect/pull/1795) [`9f2bc5a`](https://github.com/Effect-TS/effect/commit/9f2bc5a19e0b678a0a85e84daac290922b0fd57d) Thanks [@matheuspuel](https://github.com/matheuspuel)! - add Config.literal + +- [#1848](https://github.com/Effect-TS/effect/pull/1848) [`04fb8b4`](https://github.com/Effect-TS/effect/commit/04fb8b428b19bba85a2c79910c5e363340d074e7) Thanks [@fubhy](https://github.com/fubhy)! - Avoid default parameter initilization + +- [#1847](https://github.com/Effect-TS/effect/pull/1847) [`bcf0900`](https://github.com/Effect-TS/effect/commit/bcf0900b58f449262556f80bff21e771a37272aa) Thanks [@fubhy](https://github.com/fubhy)! - Avoid inline creation & spreading of objects and arrays + +- [#1798](https://github.com/Effect-TS/effect/pull/1798) [`6299b84`](https://github.com/Effect-TS/effect/commit/6299b84c11e5d1fe79fa538df8935018c7613747) Thanks [@leonitousconforti](https://github.com/leonitousconforti)! - Uncommented linesIterator string function + +## 2.0.0-next.62 + +### Minor Changes + +- [#1780](https://github.com/Effect-TS/effect/pull/1780) [`d6dd74e`](https://github.com/Effect-TS/effect/commit/d6dd74e191d3c798b08718b1326abc94982358ec) Thanks [@tim-smart](https://github.com/tim-smart)! - use NoSuchElementException for more optional apis + +### Patch Changes + +- [#1785](https://github.com/Effect-TS/effect/pull/1785) [`11a6910`](https://github.com/Effect-TS/effect/commit/11a6910f562e838b379ebc5edac94abb49d3a8e0) Thanks [@tim-smart](https://github.com/tim-smart)! - simplify Match extraction types + +- [#1782](https://github.com/Effect-TS/effect/pull/1782) [`1f398cf`](https://github.com/Effect-TS/effect/commit/1f398cf35008ec59f820338adeb2f4e2b928b1fb) Thanks [@tim-smart](https://github.com/tim-smart)! - add Layer.empty + +- [#1786](https://github.com/Effect-TS/effect/pull/1786) [`d27b68b`](https://github.com/Effect-TS/effect/commit/d27b68b7e3a57f77039fde78bf4c9924dc9d8226) Thanks [@tim-smart](https://github.com/tim-smart)! - only add one predicate in Match.discriminators + +## 2.0.0-next.61 + +### Patch Changes + +- [#1768](https://github.com/Effect-TS/effect/pull/1768) [`7c6b90c`](https://github.com/Effect-TS/effect/commit/7c6b90c507835871bdefacdf0e0f84cb87febf16) Thanks [@gcanti](https://github.com/gcanti)! - Effect.mergeAll should work when Z is an iterable, closes #1765 + +- [#1772](https://github.com/Effect-TS/effect/pull/1772) [`a1ba0c4`](https://github.com/Effect-TS/effect/commit/a1ba0c4dbbc8ee0a8d3652feabbf3c0accdbe3de) Thanks [@gcanti](https://github.com/gcanti)! - ReadonlyRecord: add `fromIterableBy` + +- [#1778](https://github.com/Effect-TS/effect/pull/1778) [`2c5a401`](https://github.com/Effect-TS/effect/commit/2c5a401a0be13b709c83365acf6a49a52896711f) Thanks [@IMax153](https://github.com/IMax153)! - add ConfigProvider.fromJson to support loading configuration from a JSON object + +- [#1770](https://github.com/Effect-TS/effect/pull/1770) [`d4d403e`](https://github.com/Effect-TS/effect/commit/d4d403e60d9ae81a69aa1190f50e6f9cb11651f3) Thanks [@tim-smart](https://github.com/tim-smart)! - adjust metric boundaries for timer histograms + +- [#1776](https://github.com/Effect-TS/effect/pull/1776) [`4c22ed5`](https://github.com/Effect-TS/effect/commit/4c22ed51b6f6458166d1151b1eaef0fe4ac2f5e4) Thanks [@fubhy](https://github.com/fubhy)! - Self-assign normalized `BigDecimal` + +## 2.0.0-next.60 + +### Minor Changes + +- [#1755](https://github.com/Effect-TS/effect/pull/1755) [`0200f12`](https://github.com/Effect-TS/effect/commit/0200f1263dcfd769ed6b381036207a583b34964c) Thanks [@gcanti](https://github.com/gcanti)! - Effect: remove `config` API (since `Config` now implements `Effect`) + +- [#1747](https://github.com/Effect-TS/effect/pull/1747) [`83db34e`](https://github.com/Effect-TS/effect/commit/83db34eb4080909b3ae7536886d27870e77d8b7e) Thanks [@fubhy](https://github.com/fubhy)! - Generate proxy packages + +### Patch Changes + +- [#1756](https://github.com/Effect-TS/effect/pull/1756) [`7c1dcc7`](https://github.com/Effect-TS/effect/commit/7c1dcc732c735a6f3f64274be4b6daea6e9fdde6) Thanks [@mikearnaldi](https://github.com/mikearnaldi)! - Fix stack filtering for first throw point + +- [#1753](https://github.com/Effect-TS/effect/pull/1753) [`1727ca5`](https://github.com/Effect-TS/effect/commit/1727ca5011d62b5353ed7c53bf1867dc37a41954) Thanks [@IMax153](https://github.com/IMax153)! - expose Console service tag + +- [#1749](https://github.com/Effect-TS/effect/pull/1749) [`299e8b5`](https://github.com/Effect-TS/effect/commit/299e8b5e085a624d1141b5fdaf00fc50203c57fa) Thanks [@IMax153](https://github.com/IMax153)! - fix the jsdoc for Effect.withConsoleScoped + +- [#1758](https://github.com/Effect-TS/effect/pull/1758) [`88d957d`](https://github.com/Effect-TS/effect/commit/88d957d724b390e005fb245b9deadfcdbd4a55d1) Thanks [@mikearnaldi](https://github.com/mikearnaldi)! - Fix provideSomeRuntime internals, restore context and flags properly + +- [#1754](https://github.com/Effect-TS/effect/pull/1754) [`6a95cc0`](https://github.com/Effect-TS/effect/commit/6a95cc0f38914b63a3884697e410f79c75add185) Thanks [@tim-smart](https://github.com/tim-smart)! - make Config implement Effect + +## 2.0.0-next.59 + +### Minor Changes + +- [#1743](https://github.com/Effect-TS/effect/pull/1743) [`143ee1e`](https://github.com/Effect-TS/effect/commit/143ee1e58ff98c9b8813622d14ef67a0e7f76874) Thanks [@tim-smart](https://github.com/tim-smart)! - rename FiberRefs.updatedAs to FiberRef.updateAs + +- [#1738](https://github.com/Effect-TS/effect/pull/1738) [`d4abb06`](https://github.com/Effect-TS/effect/commit/d4abb06a411cc088d1eb20d853c3a9da97d4f847) Thanks [@gcanti](https://github.com/gcanti)! - ReaonlyRecord: rename `fromIterable` to `fromIterableWith` and add standard `fromIterable` API + +- [#1743](https://github.com/Effect-TS/effect/pull/1743) [`143ee1e`](https://github.com/Effect-TS/effect/commit/143ee1e58ff98c9b8813622d14ef67a0e7f76874) Thanks [@tim-smart](https://github.com/tim-smart)! - use native js data types for Metrics + +### Patch Changes + +- [#1733](https://github.com/Effect-TS/effect/pull/1733) [`8177e4c`](https://github.com/Effect-TS/effect/commit/8177e4cc50eba7534b794ddaabb7754641060e9b) Thanks [@IMax153](https://github.com/IMax153)! - add `withConsoleScoped` to `Console`/`Effect` modules + +- [#1726](https://github.com/Effect-TS/effect/pull/1726) [`1152a2c`](https://github.com/Effect-TS/effect/commit/1152a2c900c43687876e042d1fc78570e48aebe0) Thanks [@gcanti](https://github.com/gcanti)! - ReadonlyArray: fix the tacit use of unzip + +- [#1743](https://github.com/Effect-TS/effect/pull/1743) [`143ee1e`](https://github.com/Effect-TS/effect/commit/143ee1e58ff98c9b8813622d14ef67a0e7f76874) Thanks [@tim-smart](https://github.com/tim-smart)! - prefer Date.now() over new Date().getTime() + +- [#1735](https://github.com/Effect-TS/effect/pull/1735) [`cf4c044`](https://github.com/Effect-TS/effect/commit/cf4c044d799ae1249084abfd59d7f2ecd4a7c755) Thanks [@tim-smart](https://github.com/tim-smart)! - expose Layer MemoMap apis + +- [#1724](https://github.com/Effect-TS/effect/pull/1724) [`1884fa3`](https://github.com/Effect-TS/effect/commit/1884fa3f18c0ae85f62af338f1ac5863ad24f778) Thanks [@gcanti](https://github.com/gcanti)! - Chunk: fix the tacit use of flatten + +- [#1726](https://github.com/Effect-TS/effect/pull/1726) [`1152a2c`](https://github.com/Effect-TS/effect/commit/1152a2c900c43687876e042d1fc78570e48aebe0) Thanks [@gcanti](https://github.com/gcanti)! - ReadonlyArray: fix the tacit use of reverse + +- [#1726](https://github.com/Effect-TS/effect/pull/1726) [`1152a2c`](https://github.com/Effect-TS/effect/commit/1152a2c900c43687876e042d1fc78570e48aebe0) Thanks [@gcanti](https://github.com/gcanti)! - ReadonlyArray: fix the tacit use of dedupe + +- [#1743](https://github.com/Effect-TS/effect/pull/1743) [`143ee1e`](https://github.com/Effect-TS/effect/commit/143ee1e58ff98c9b8813622d14ef67a0e7f76874) Thanks [@tim-smart](https://github.com/tim-smart)! - add FiberRefs.updateManyAs + +- [#1737](https://github.com/Effect-TS/effect/pull/1737) [`9c26f58`](https://github.com/Effect-TS/effect/commit/9c26f58715c386885e25fa30662ad8c77576c22e) Thanks [@gcanti](https://github.com/gcanti)! - Chunk: add splitNonEmptyAt + +- [#1743](https://github.com/Effect-TS/effect/pull/1743) [`143ee1e`](https://github.com/Effect-TS/effect/commit/143ee1e58ff98c9b8813622d14ef67a0e7f76874) Thanks [@tim-smart](https://github.com/tim-smart)! - short circuit for empty patches + +- [#1736](https://github.com/Effect-TS/effect/pull/1736) [`8249277`](https://github.com/Effect-TS/effect/commit/82492774087746a1174353480465c439388f88f4) Thanks [@gcanti](https://github.com/gcanti)! - ReadonlyArray: add splitWhere + +- [#1729](https://github.com/Effect-TS/effect/pull/1729) [`3c77e12`](https://github.com/Effect-TS/effect/commit/3c77e12d92030413e25f8a32ab84a4feb15c5164) Thanks [@jessekelly881](https://github.com/jessekelly881)! - updated BigDecimal.toString + +- [#1726](https://github.com/Effect-TS/effect/pull/1726) [`1152a2c`](https://github.com/Effect-TS/effect/commit/1152a2c900c43687876e042d1fc78570e48aebe0) Thanks [@gcanti](https://github.com/gcanti)! - Chunk > flatMap: fix return type + +- [#1736](https://github.com/Effect-TS/effect/pull/1736) [`8249277`](https://github.com/Effect-TS/effect/commit/82492774087746a1174353480465c439388f88f4) Thanks [@gcanti](https://github.com/gcanti)! - ReadonlyArray: add split + +- [#1726](https://github.com/Effect-TS/effect/pull/1726) [`1152a2c`](https://github.com/Effect-TS/effect/commit/1152a2c900c43687876e042d1fc78570e48aebe0) Thanks [@gcanti](https://github.com/gcanti)! - ReadonlyArray: fix sortBy signature + +- [#1726](https://github.com/Effect-TS/effect/pull/1726) [`1152a2c`](https://github.com/Effect-TS/effect/commit/1152a2c900c43687876e042d1fc78570e48aebe0) Thanks [@gcanti](https://github.com/gcanti)! - ReadonlyArray: fix chop signature + +- [#1726](https://github.com/Effect-TS/effect/pull/1726) [`1152a2c`](https://github.com/Effect-TS/effect/commit/1152a2c900c43687876e042d1fc78570e48aebe0) Thanks [@gcanti](https://github.com/gcanti)! - List > flatMap: fix return type + +- [#1743](https://github.com/Effect-TS/effect/pull/1743) [`143ee1e`](https://github.com/Effect-TS/effect/commit/143ee1e58ff98c9b8813622d14ef67a0e7f76874) Thanks [@tim-smart](https://github.com/tim-smart)! - replace use of throw in fiber runtime + +- [#1743](https://github.com/Effect-TS/effect/pull/1743) [`143ee1e`](https://github.com/Effect-TS/effect/commit/143ee1e58ff98c9b8813622d14ef67a0e7f76874) Thanks [@tim-smart](https://github.com/tim-smart)! - optimize FiberRef.update/forkAs + +- [#1724](https://github.com/Effect-TS/effect/pull/1724) [`1884fa3`](https://github.com/Effect-TS/effect/commit/1884fa3f18c0ae85f62af338f1ac5863ad24f778) Thanks [@gcanti](https://github.com/gcanti)! - ReadonlyArray: fix the tacit use of flatten + +- [#1727](https://github.com/Effect-TS/effect/pull/1727) [`9b5f72d`](https://github.com/Effect-TS/effect/commit/9b5f72d6bb9efd22f52c64c727b79f29d94507d3) Thanks [@photomoose](https://github.com/photomoose)! - Fix number of retries in retryN + +- [#1735](https://github.com/Effect-TS/effect/pull/1735) [`cf4c044`](https://github.com/Effect-TS/effect/commit/cf4c044d799ae1249084abfd59d7f2ecd4a7c755) Thanks [@tim-smart](https://github.com/tim-smart)! - fix memoization of Layer.effect/scoped + +- [#1726](https://github.com/Effect-TS/effect/pull/1726) [`1152a2c`](https://github.com/Effect-TS/effect/commit/1152a2c900c43687876e042d1fc78570e48aebe0) Thanks [@gcanti](https://github.com/gcanti)! - ReadonlyArray: fix dedupeWith signature + +- [#1726](https://github.com/Effect-TS/effect/pull/1726) [`1152a2c`](https://github.com/Effect-TS/effect/commit/1152a2c900c43687876e042d1fc78570e48aebe0) Thanks [@gcanti](https://github.com/gcanti)! - ReadonlyArray > flatMap: fix return type + +- [#1743](https://github.com/Effect-TS/effect/pull/1743) [`143ee1e`](https://github.com/Effect-TS/effect/commit/143ee1e58ff98c9b8813622d14ef67a0e7f76874) Thanks [@tim-smart](https://github.com/tim-smart)! - optimize MutableHashMap + +- [#1745](https://github.com/Effect-TS/effect/pull/1745) [`c142caa`](https://github.com/Effect-TS/effect/commit/c142caa725646929d8086d8e63d7a406fd2415da) Thanks [@IMax153](https://github.com/IMax153)! - rename ConfigSecret to Secret + +- [#1733](https://github.com/Effect-TS/effect/pull/1733) [`8177e4c`](https://github.com/Effect-TS/effect/commit/8177e4cc50eba7534b794ddaabb7754641060e9b) Thanks [@IMax153](https://github.com/IMax153)! - export `Console` combinators from the `Effect` module to match other default services + +## 2.0.0-next.58 + +### Patch Changes + +- [#1722](https://github.com/Effect-TS/effect/pull/1722) [`b5569e3`](https://github.com/Effect-TS/effect/commit/b5569e358534da41047a687afbc85dbe8517ddca) Thanks [@tim-smart](https://github.com/tim-smart)! - update build setup to put cjs in root directory + +- [#1720](https://github.com/Effect-TS/effect/pull/1720) [`56a0334`](https://github.com/Effect-TS/effect/commit/56a033456c3285ff95fdbeeddff2bda6a1e39bec) Thanks [@tim-smart](https://github.com/tim-smart)! - fix jsdoc for Inspectable.format + +## 2.0.0-next.57 + +### Minor Changes + +- [#1701](https://github.com/Effect-TS/effect/pull/1701) [`739460b06`](https://github.com/Effect-TS/effect/commit/739460b0609cd490abbb0a8dfbe3dfe9f67a3680) Thanks [@fubhy](https://github.com/fubhy)! - Allow to set a custom description for timer metrics + +- [#1704](https://github.com/Effect-TS/effect/pull/1704) [`accf8a647`](https://github.com/Effect-TS/effect/commit/accf8a647b7a869d2de445e430dab07f08aac0cc) Thanks [@fubhy](https://github.com/fubhy)! - Renamed `ReadonlyArray.compact` and `ReadonlyRecord.compact` to `.getSomes` + +- [#1716](https://github.com/Effect-TS/effect/pull/1716) [`023b512bd`](https://github.com/Effect-TS/effect/commit/023b512bd0b3d5a91bbe85b262e8762e5ce3ac21) Thanks [@gcanti](https://github.com/gcanti)! - List: merge NonEmpty APIs into base ones + +- [#1717](https://github.com/Effect-TS/effect/pull/1717) [`869c9c31d`](https://github.com/Effect-TS/effect/commit/869c9c31de2d297bc2937ca6b0a417c10ed1a12f) Thanks [@gcanti](https://github.com/gcanti)! - ReadonlyArray: merge NonEmpty APIs into base ones + +- [#1713](https://github.com/Effect-TS/effect/pull/1713) [`906343263`](https://github.com/Effect-TS/effect/commit/906343263f6306965602422508ef3c7158dd7cc8) Thanks [@gcanti](https://github.com/gcanti)! - BigDecimal: rename `toString` to `format` + +- [#1688](https://github.com/Effect-TS/effect/pull/1688) [`9698427fe`](https://github.com/Effect-TS/effect/commit/9698427fea446a08b702d3f820db96753efad638) Thanks [@tim-smart](https://github.com/tim-smart)! - replace Layer.provide* with Layer.use* + +- [#1711](https://github.com/Effect-TS/effect/pull/1711) [`ff6fadb93`](https://github.com/Effect-TS/effect/commit/ff6fadb934fef37dec1f58eaeff40c0d027393bb) Thanks [@gcanti](https://github.com/gcanti)! - Chunk: merge NonEmpty APIs into base ones + +- [#1707](https://github.com/Effect-TS/effect/pull/1707) [`fb1a98fab`](https://github.com/Effect-TS/effect/commit/fb1a98fab613c7ec34abf35c348f3cadcc7d943d) Thanks [@gcanti](https://github.com/gcanti)! - Layer: rename `zipWithPar` to `zipWith` (standard) + +### Patch Changes + +- [#1690](https://github.com/Effect-TS/effect/pull/1690) [`eb6d7aada`](https://github.com/Effect-TS/effect/commit/eb6d7aada122b260b52e53ff2fd28bfe851b7f40) Thanks [@tim-smart](https://github.com/tim-smart)! - allow omission of Scope type in R of Stream.asyncScoped + +- [#1704](https://github.com/Effect-TS/effect/pull/1704) [`accf8a647`](https://github.com/Effect-TS/effect/commit/accf8a647b7a869d2de445e430dab07f08aac0cc) Thanks [@fubhy](https://github.com/fubhy)! - Added `.getLefts` and `.getRights` + +- [#1703](https://github.com/Effect-TS/effect/pull/1703) [`f8d27500d`](https://github.com/Effect-TS/effect/commit/f8d27500dae8eb23ff8b93e8b894a4ab4ec6ebad) Thanks [@jessekelly881](https://github.com/jessekelly881)! - improved Duration.toString + +- [#1689](https://github.com/Effect-TS/effect/pull/1689) [`a0bd532e8`](https://github.com/Effect-TS/effect/commit/a0bd532e85fa603b29e58c6a2670433b0346377a) Thanks [@FedericoBiccheddu](https://github.com/FedericoBiccheddu)! - improve `Pool`'s `makeWithTTL` JSDoc example + +- [#1715](https://github.com/Effect-TS/effect/pull/1715) [`8b1a7e8a1`](https://github.com/Effect-TS/effect/commit/8b1a7e8a1acddec126b4292fc24154f6df615f0a) Thanks [@tim-smart](https://github.com/tim-smart)! - only add onInterrupt in Effect.async if required + +- [#1694](https://github.com/Effect-TS/effect/pull/1694) [`33ffa62b4`](https://github.com/Effect-TS/effect/commit/33ffa62b444db82afc0e43154eb4b3576761c583) Thanks [@extremegf](https://github.com/extremegf)! - Add Either.filterOrLeft + +- [#1695](https://github.com/Effect-TS/effect/pull/1695) [`7ccd1eb0b`](https://github.com/Effect-TS/effect/commit/7ccd1eb0b71ec84033d2b106412ccfcac7753e4c) Thanks [@jessekelly881](https://github.com/jessekelly881)! - added Option.andThen + +- [#1706](https://github.com/Effect-TS/effect/pull/1706) [`8a1e98ce3`](https://github.com/Effect-TS/effect/commit/8a1e98ce33344347f6edec0fc89c4c22d8393e90) Thanks [@mikearnaldi](https://github.com/mikearnaldi)! - Improve consistency between request batching and fiber environment. + + In prior releases request batching required operators that act on fiber context such as `Effect.locally` to be aware of batching in order to avoid bugs where the effect executed post batching would lose the fiber environment (context, refs, and flags). + + This change restructure how batching internally works, inside the fiber we now slice up the stack and restore the exact context that was destroyed, by rewriting the internals of forEach batching is now transparent to any other function that deals with fiber state. + +- [#1687](https://github.com/Effect-TS/effect/pull/1687) [`e4d90ed38`](https://github.com/Effect-TS/effect/commit/e4d90ed3896f6d00e8470e73e0d9f597504fc888) Thanks [@matheuspuel](https://github.com/matheuspuel)! - fix YieldableError.toString crashing on react-native + +- [#1681](https://github.com/Effect-TS/effect/pull/1681) [`e5cd27c7d`](https://github.com/Effect-TS/effect/commit/e5cd27c7d5988902b238d568d6d13470babb8ee9) Thanks [@matheuspuel](https://github.com/matheuspuel)! - forbid excess properties when matching tags + +- [#1692](https://github.com/Effect-TS/effect/pull/1692) [`37a7cfe94`](https://github.com/Effect-TS/effect/commit/37a7cfe94f3baa51fb465c1f6591f20d2aab5c7f) Thanks [@tim-smart](https://github.com/tim-smart)! - add PrimaryKey.value + +- [#1679](https://github.com/Effect-TS/effect/pull/1679) [`c1146e473`](https://github.com/Effect-TS/effect/commit/c1146e47343a39e0d168f10547df43d0728df50d) Thanks [@k44](https://github.com/k44)! - fix error value of `Effect.tryPromise` + +- [#1719](https://github.com/Effect-TS/effect/pull/1719) [`30893ed48`](https://github.com/Effect-TS/effect/commit/30893ed48592460b8bbef6388802893ed9f0f23f) Thanks [@tim-smart](https://github.com/tim-smart)! - add Request.failCause + +- [#1712](https://github.com/Effect-TS/effect/pull/1712) [`e2ccf5120`](https://github.com/Effect-TS/effect/commit/e2ccf512088e0dfb0e3816ec259d4f6736f5cf28) Thanks [@thewilkybarkid](https://github.com/thewilkybarkid)! - fix ReadonlyArray.difference description + +- [#1715](https://github.com/Effect-TS/effect/pull/1715) [`8b1a7e8a1`](https://github.com/Effect-TS/effect/commit/8b1a7e8a1acddec126b4292fc24154f6df615f0a) Thanks [@tim-smart](https://github.com/tim-smart)! - simplify Effect.tryCatch implementation + +- [#1684](https://github.com/Effect-TS/effect/pull/1684) [`aeb33b158`](https://github.com/Effect-TS/effect/commit/aeb33b158b14ea1a28fb78954adb717019f913a1) Thanks [@KhraksMamtsov](https://github.com/KhraksMamtsov)! - change typo in Either documentation + +- [#1699](https://github.com/Effect-TS/effect/pull/1699) [`06eb1d380`](https://github.com/Effect-TS/effect/commit/06eb1d3801ae6ef93412529af3ef37b509cba7ef) Thanks [@gcanti](https://github.com/gcanti)! - Config: standardize error messages + +- [#1683](https://github.com/Effect-TS/effect/pull/1683) [`a6a78ccad`](https://github.com/Effect-TS/effect/commit/a6a78ccad976c510cf0d6a33eee5e10697b310da) Thanks [@tim-smart](https://github.com/tim-smart)! - add default type to data class props generic + +- [#1718](https://github.com/Effect-TS/effect/pull/1718) [`3b0768ce6`](https://github.com/Effect-TS/effect/commit/3b0768ce68035fe8a2b017b736f24ab3bcce350b) Thanks [@KhraksMamtsov](https://github.com/KhraksMamtsov)! - get rid `absorb` mention + +- [#1686](https://github.com/Effect-TS/effect/pull/1686) [`9f4d2874d`](https://github.com/Effect-TS/effect/commit/9f4d2874da7568eafd14c183d127132788f86668) Thanks [@gcanti](https://github.com/gcanti)! - Types: add variance helpers + +- [#1697](https://github.com/Effect-TS/effect/pull/1697) [`e1a4b6a63`](https://github.com/Effect-TS/effect/commit/e1a4b6a63d73aa111015652bfe4584b767a53e61) Thanks [@tim-smart](https://github.com/tim-smart)! - expose currentConcurrency fiber ref + +## 2.0.0-next.56 + +### Minor Changes + +- [#1671](https://github.com/Effect-TS/effect/pull/1671) [`c415248cd`](https://github.com/Effect-TS/effect/commit/c415248cd8e5a01144a0c9135da58cb0b0afc37d) Thanks [@tim-smart](https://github.com/tim-smart)! - support Promise in Effect.andThen and .tap + +- [#1671](https://github.com/Effect-TS/effect/pull/1671) [`c415248cd`](https://github.com/Effect-TS/effect/commit/c415248cd8e5a01144a0c9135da58cb0b0afc37d) Thanks [@tim-smart](https://github.com/tim-smart)! - add Cause.UnknownException and use it over `unknown` + +- [#1678](https://github.com/Effect-TS/effect/pull/1678) [`8ed7626a4`](https://github.com/Effect-TS/effect/commit/8ed7626a49f1a4fb1b9315d97008355f1b6962ef) Thanks [@tim-smart](https://github.com/tim-smart)! - use `new` for Cause error constructors + +### Patch Changes + +- [#1662](https://github.com/Effect-TS/effect/pull/1662) [`aa6787e16`](https://github.com/Effect-TS/effect/commit/aa6787e166ba51511de0ff96dbfd986f1c974f2d) Thanks [@gcanti](https://github.com/gcanti)! - TDeferred: fix E, A variance (from covariant to invariant) + +- [#1662](https://github.com/Effect-TS/effect/pull/1662) [`aa6787e16`](https://github.com/Effect-TS/effect/commit/aa6787e166ba51511de0ff96dbfd986f1c974f2d) Thanks [@gcanti](https://github.com/gcanti)! - SynchronizedRef: fix A variance (from covariant to invariant) + +- [#1661](https://github.com/Effect-TS/effect/pull/1661) [`6c32d12d7`](https://github.com/Effect-TS/effect/commit/6c32d12d7be5eb829dc5d8fe576f4fda8217ea6d) Thanks [@fubhy](https://github.com/fubhy)! - Use `sideEffects: []` in package.json + +- [#1663](https://github.com/Effect-TS/effect/pull/1663) [`69bcb5b7a`](https://github.com/Effect-TS/effect/commit/69bcb5b7ab4c4faa873cf8132172e68fc8eb9b6d) Thanks [@tim-smart](https://github.com/tim-smart)! - add TaggedClass to /request + +- [#1676](https://github.com/Effect-TS/effect/pull/1676) [`995318829`](https://github.com/Effect-TS/effect/commit/9953188299848a96adf637b5a90093b4cc8792f6) Thanks [@tim-smart](https://github.com/tim-smart)! - support undefined values in TPubSub + +- [#1658](https://github.com/Effect-TS/effect/pull/1658) [`396428a73`](https://github.com/Effect-TS/effect/commit/396428a73871715a6aed632c2c5b5affb2e509ac) Thanks [@wmaurer](https://github.com/wmaurer)! - ReadonlyArray: Improved refinement typings for partition + +- [#1672](https://github.com/Effect-TS/effect/pull/1672) [`80bf68da5`](https://github.com/Effect-TS/effect/commit/80bf68da546fecf91e3ebcd43c8d4798841227df) Thanks [@tim-smart](https://github.com/tim-smart)! - add metric .register() for forcing addition to registry + +- [#1669](https://github.com/Effect-TS/effect/pull/1669) [`541330b11`](https://github.com/Effect-TS/effect/commit/541330b110fc3d5f463f34cb48490e25b29036ae) Thanks [@tim-smart](https://github.com/tim-smart)! - add PrimaryKey module + +- [#1662](https://github.com/Effect-TS/effect/pull/1662) [`aa6787e16`](https://github.com/Effect-TS/effect/commit/aa6787e166ba51511de0ff96dbfd986f1c974f2d) Thanks [@gcanti](https://github.com/gcanti)! - RedBlackTree: make Key invariant + +- [#1664](https://github.com/Effect-TS/effect/pull/1664) [`54ce5e638`](https://github.com/Effect-TS/effect/commit/54ce5e63882136d77b50ebe6613db4f349bb0195) Thanks [@gcanti](https://github.com/gcanti)! - PollingMetric: renamed to MetricPolling (standard) + +- [#1662](https://github.com/Effect-TS/effect/pull/1662) [`aa6787e16`](https://github.com/Effect-TS/effect/commit/aa6787e166ba51511de0ff96dbfd986f1c974f2d) Thanks [@gcanti](https://github.com/gcanti)! - Deferred: fix E and A variance (from covariant to invariant) + +- [#1660](https://github.com/Effect-TS/effect/pull/1660) [`ecc334703`](https://github.com/Effect-TS/effect/commit/ecc3347037965df8f6e6e19423f4c0cfea7e04b7) Thanks [@gcanti](https://github.com/gcanti)! - HashMap: swap findFirst > predicate arguments (standard) + +- [#1662](https://github.com/Effect-TS/effect/pull/1662) [`aa6787e16`](https://github.com/Effect-TS/effect/commit/aa6787e166ba51511de0ff96dbfd986f1c974f2d) Thanks [@gcanti](https://github.com/gcanti)! - TSet: fix A variance (from covariant to invariant) + +- [#1603](https://github.com/Effect-TS/effect/pull/1603) [`4e7a6912c`](https://github.com/Effect-TS/effect/commit/4e7a6912c782571f07a055eccae8aa973b4b5c6f) Thanks [@mikearnaldi](https://github.com/mikearnaldi)! - Auto-flattening Effect.tap + +- [#1662](https://github.com/Effect-TS/effect/pull/1662) [`aa6787e16`](https://github.com/Effect-TS/effect/commit/aa6787e166ba51511de0ff96dbfd986f1c974f2d) Thanks [@gcanti](https://github.com/gcanti)! - RequestResolver: fix A variance (from covariant to contravariant) + +- [#1662](https://github.com/Effect-TS/effect/pull/1662) [`aa6787e16`](https://github.com/Effect-TS/effect/commit/aa6787e166ba51511de0ff96dbfd986f1c974f2d) Thanks [@gcanti](https://github.com/gcanti)! - ScopedRef: fix A variance (from covariant to invariant) + +- [#1662](https://github.com/Effect-TS/effect/pull/1662) [`aa6787e16`](https://github.com/Effect-TS/effect/commit/aa6787e166ba51511de0ff96dbfd986f1c974f2d) Thanks [@gcanti](https://github.com/gcanti)! - Reloadable: fix A variance (from covariant to invariant) + +- [#1660](https://github.com/Effect-TS/effect/pull/1660) [`ecc334703`](https://github.com/Effect-TS/effect/commit/ecc3347037965df8f6e6e19423f4c0cfea7e04b7) Thanks [@gcanti](https://github.com/gcanti)! - fix ReadonlyRecord.partition signature + +- [#1670](https://github.com/Effect-TS/effect/pull/1670) [`c3bfc90e4`](https://github.com/Effect-TS/effect/commit/c3bfc90e4af20c2f2e8e3c663690779d4332f86e) Thanks [@tim-smart](https://github.com/tim-smart)! - add Request.Class + +- [#1662](https://github.com/Effect-TS/effect/pull/1662) [`aa6787e16`](https://github.com/Effect-TS/effect/commit/aa6787e166ba51511de0ff96dbfd986f1c974f2d) Thanks [@gcanti](https://github.com/gcanti)! - Resource: fix E, A variance (from covariant to invariant) + +- [#1674](https://github.com/Effect-TS/effect/pull/1674) [`c687a8701`](https://github.com/Effect-TS/effect/commit/c687a870157e1c212f29ddb3264db0200d03466e) Thanks [@fubhy](https://github.com/fubhy)! - Allow hrtime as `Duration` input + +- [#1676](https://github.com/Effect-TS/effect/pull/1676) [`995318829`](https://github.com/Effect-TS/effect/commit/9953188299848a96adf637b5a90093b4cc8792f6) Thanks [@tim-smart](https://github.com/tim-smart)! - support undefined values in TQueue + +- [#1668](https://github.com/Effect-TS/effect/pull/1668) [`fc9bce6a2`](https://github.com/Effect-TS/effect/commit/fc9bce6a24b1fc46955d276ed0011a93378b3297) Thanks [@gcanti](https://github.com/gcanti)! - Config: propagate the path in validation, closes #1667 + +- [#1662](https://github.com/Effect-TS/effect/pull/1662) [`aa6787e16`](https://github.com/Effect-TS/effect/commit/aa6787e166ba51511de0ff96dbfd986f1c974f2d) Thanks [@gcanti](https://github.com/gcanti)! - PubSub: fix A variance (from contravariant to invariant) + +- [#1676](https://github.com/Effect-TS/effect/pull/1676) [`995318829`](https://github.com/Effect-TS/effect/commit/9953188299848a96adf637b5a90093b4cc8792f6) Thanks [@tim-smart](https://github.com/tim-smart)! - support null values in PubSub + +- [#1655](https://github.com/Effect-TS/effect/pull/1655) [`0c6330db0`](https://github.com/Effect-TS/effect/commit/0c6330db0dac8264d9a9e2ca8babea01a054317a) Thanks [@gcanti](https://github.com/gcanti)! - interfaces: revert changing methods to props (RE: #1644) + +- [#1662](https://github.com/Effect-TS/effect/pull/1662) [`aa6787e16`](https://github.com/Effect-TS/effect/commit/aa6787e166ba51511de0ff96dbfd986f1c974f2d) Thanks [@gcanti](https://github.com/gcanti)! - FiberRef: fix A variance (from covariant to invariant) + +- [#1662](https://github.com/Effect-TS/effect/pull/1662) [`aa6787e16`](https://github.com/Effect-TS/effect/commit/aa6787e166ba51511de0ff96dbfd986f1c974f2d) Thanks [@gcanti](https://github.com/gcanti)! - StrategyVariance: fix A variance (from covariant to invariant) + +- [#1678](https://github.com/Effect-TS/effect/pull/1678) [`8ed7626a4`](https://github.com/Effect-TS/effect/commit/8ed7626a49f1a4fb1b9315d97008355f1b6962ef) Thanks [@tim-smart](https://github.com/tim-smart)! - Cause.YieldableError extends Inspectable + +- [#1662](https://github.com/Effect-TS/effect/pull/1662) [`aa6787e16`](https://github.com/Effect-TS/effect/commit/aa6787e166ba51511de0ff96dbfd986f1c974f2d) Thanks [@gcanti](https://github.com/gcanti)! - TMap: fix K, V variance (from covariant to invariant) + +- [#1665](https://github.com/Effect-TS/effect/pull/1665) [`a00b920b8`](https://github.com/Effect-TS/effect/commit/a00b920b8910f975ff61be48c1538de527fa290b) Thanks [@gcanti](https://github.com/gcanti)! - Chunk: fix partition signature (expose the index of the element) + +- [#1662](https://github.com/Effect-TS/effect/pull/1662) [`aa6787e16`](https://github.com/Effect-TS/effect/commit/aa6787e166ba51511de0ff96dbfd986f1c974f2d) Thanks [@gcanti](https://github.com/gcanti)! - Pool: fix A variance (from covariant to invariant) + +- [#1662](https://github.com/Effect-TS/effect/pull/1662) [`aa6787e16`](https://github.com/Effect-TS/effect/commit/aa6787e166ba51511de0ff96dbfd986f1c974f2d) Thanks [@gcanti](https://github.com/gcanti)! - Cache / ConsumerCache: fix Key variance (from contravariant to invariant) + +- [#1662](https://github.com/Effect-TS/effect/pull/1662) [`aa6787e16`](https://github.com/Effect-TS/effect/commit/aa6787e166ba51511de0ff96dbfd986f1c974f2d) Thanks [@gcanti](https://github.com/gcanti)! - SubscriptionRef: fix A variance (from covariant to invariant) + +- [#1603](https://github.com/Effect-TS/effect/pull/1603) [`4e7a6912c`](https://github.com/Effect-TS/effect/commit/4e7a6912c782571f07a055eccae8aa973b4b5c6f) Thanks [@mikearnaldi](https://github.com/mikearnaldi)! - Introduce Types.NoInfer + +- [#1662](https://github.com/Effect-TS/effect/pull/1662) [`aa6787e16`](https://github.com/Effect-TS/effect/commit/aa6787e166ba51511de0ff96dbfd986f1c974f2d) Thanks [@gcanti](https://github.com/gcanti)! - TPriorityQueue: fix A variance (from covariant to invariant) + +- [#1662](https://github.com/Effect-TS/effect/pull/1662) [`aa6787e16`](https://github.com/Effect-TS/effect/commit/aa6787e166ba51511de0ff96dbfd986f1c974f2d) Thanks [@gcanti](https://github.com/gcanti)! - SortedSet: make A invariant + +- [#1654](https://github.com/Effect-TS/effect/pull/1654) [`d2b7e0ef0`](https://github.com/Effect-TS/effect/commit/d2b7e0ef022234ceba0c3b77afdc3285081ece97) Thanks [@wmaurer](https://github.com/wmaurer)! - Added refinement overloads to Sink.collectAllWhile, Stream.partition and Stream.takeWhile. Added dtslint tests for Sink and Stream functions with refinement overloads + +- [#1662](https://github.com/Effect-TS/effect/pull/1662) [`aa6787e16`](https://github.com/Effect-TS/effect/commit/aa6787e166ba51511de0ff96dbfd986f1c974f2d) Thanks [@gcanti](https://github.com/gcanti)! - SortedMap: make K invariant + +- [#1603](https://github.com/Effect-TS/effect/pull/1603) [`4e7a6912c`](https://github.com/Effect-TS/effect/commit/4e7a6912c782571f07a055eccae8aa973b4b5c6f) Thanks [@mikearnaldi](https://github.com/mikearnaldi)! - Introduce Effect.andThen + +- [#1662](https://github.com/Effect-TS/effect/pull/1662) [`aa6787e16`](https://github.com/Effect-TS/effect/commit/aa6787e166ba51511de0ff96dbfd986f1c974f2d) Thanks [@gcanti](https://github.com/gcanti)! - TArray: fix A variance (from covariant to invariant) + +- [#1662](https://github.com/Effect-TS/effect/pull/1662) [`aa6787e16`](https://github.com/Effect-TS/effect/commit/aa6787e166ba51511de0ff96dbfd986f1c974f2d) Thanks [@gcanti](https://github.com/gcanti)! - KeyedPool: fix A variance (from covariant to invariant) + +- [#1662](https://github.com/Effect-TS/effect/pull/1662) [`aa6787e16`](https://github.com/Effect-TS/effect/commit/aa6787e166ba51511de0ff96dbfd986f1c974f2d) Thanks [@gcanti](https://github.com/gcanti)! - TPubSub: make A invariant + +- [#1671](https://github.com/Effect-TS/effect/pull/1671) [`c415248cd`](https://github.com/Effect-TS/effect/commit/c415248cd8e5a01144a0c9135da58cb0b0afc37d) Thanks [@tim-smart](https://github.com/tim-smart)! - move internal exceptions into core + +## 2.0.0-next.55 + +### Patch Changes + +- [#1648](https://github.com/Effect-TS/effect/pull/1648) [`b2cbb6a79`](https://github.com/Effect-TS/effect/commit/b2cbb6a7946590411ce2d48df19c1b4795415945) Thanks [@gcanti](https://github.com/gcanti)! - Cause: fix exception constructors (should respect `exactOptionalPropertyTypes: true` when creating `message` prop) + +- [#1613](https://github.com/Effect-TS/effect/pull/1613) [`2dee48696`](https://github.com/Effect-TS/effect/commit/2dee48696b70abde7dffea2a52f98dd0306f3649) Thanks [@gcanti](https://github.com/gcanti)! - Types: add Mutable helper + +- [#1621](https://github.com/Effect-TS/effect/pull/1621) [`33c06822d`](https://github.com/Effect-TS/effect/commit/33c06822d7b415849b29c2cd04f4b96f7e001557) Thanks [@gcanti](https://github.com/gcanti)! - SortedSet: make fromIterable dual + +- [#1608](https://github.com/Effect-TS/effect/pull/1608) [`a9082c91c`](https://github.com/Effect-TS/effect/commit/a9082c91c2e64864b8e8f573362f62462490a5df) Thanks [@mikearnaldi](https://github.com/mikearnaldi)! - Fix off-by-one in Random.shuffle + +- [#1617](https://github.com/Effect-TS/effect/pull/1617) [`79719018b`](https://github.com/Effect-TS/effect/commit/79719018b327bc457a300a6b484eca58192633fb) Thanks [@gcanti](https://github.com/gcanti)! - HashMap: add entries + +- [#1628](https://github.com/Effect-TS/effect/pull/1628) [`ba1aa04a8`](https://github.com/Effect-TS/effect/commit/ba1aa04a8cf3ef98fb7444bcdff2196a22709736) Thanks [@gcanti](https://github.com/gcanti)! - TSet: replace toReadonlyArray with toArray + +- [#1625](https://github.com/Effect-TS/effect/pull/1625) [`cc9a03ac7`](https://github.com/Effect-TS/effect/commit/cc9a03ac7029b10b4fef838a3329962ad53c7936) Thanks [@gcanti](https://github.com/gcanti)! - TMap: rename reduceWithIndex / reduceWithIndexSTM to reduce / reduceSTM + +- [#1649](https://github.com/Effect-TS/effect/pull/1649) [`a3cda801a`](https://github.com/Effect-TS/effect/commit/a3cda801a8f8491ecc5286efa1364d9169a76aa5) Thanks [@gcanti](https://github.com/gcanti)! - interfaces: replace 0-arity functions with values + +- [#1625](https://github.com/Effect-TS/effect/pull/1625) [`cc9a03ac7`](https://github.com/Effect-TS/effect/commit/cc9a03ac7029b10b4fef838a3329962ad53c7936) Thanks [@gcanti](https://github.com/gcanti)! - TMap: removeIf returns `Array<[K, V]>` instead of `Array` + +- [#1642](https://github.com/Effect-TS/effect/pull/1642) [`b2fdff3b8`](https://github.com/Effect-TS/effect/commit/b2fdff3b83d566c37a499fa58f9f1492f8219e0f) Thanks [@gcanti](https://github.com/gcanti)! - TMap: merge removeIf / removeIfDiscard, retainIf / retainIf (`{ discard: boolean }` optional argument) + +- [#1631](https://github.com/Effect-TS/effect/pull/1631) [`af2854596`](https://github.com/Effect-TS/effect/commit/af2854596854ec6bf9e1d1dbe24535ed2a772430) Thanks [@gcanti](https://github.com/gcanti)! - Duration: refactor `between` with an `options` argument for `minimum` and `maximum` (standard) + +- [#1625](https://github.com/Effect-TS/effect/pull/1625) [`cc9a03ac7`](https://github.com/Effect-TS/effect/commit/cc9a03ac7029b10b4fef838a3329962ad53c7936) Thanks [@gcanti](https://github.com/gcanti)! - TMap: replace toReadonlyMap with toMap + +- [#1631](https://github.com/Effect-TS/effect/pull/1631) [`af2854596`](https://github.com/Effect-TS/effect/commit/af2854596854ec6bf9e1d1dbe24535ed2a772430) Thanks [@gcanti](https://github.com/gcanti)! - Duration: refactor `clamp` with an `options` argument for `minimum` and `maximum` (standard) + +- [#1638](https://github.com/Effect-TS/effect/pull/1638) [`4eedf057b`](https://github.com/Effect-TS/effect/commit/4eedf057b38c09ea1a6bc5b85c886edb02681d54) Thanks [@gcanti](https://github.com/gcanti)! - Predicate: exclude functions from `isRecord` + +- [#1645](https://github.com/Effect-TS/effect/pull/1645) [`d2e15f377`](https://github.com/Effect-TS/effect/commit/d2e15f377f55fb4a3f2114bd148f5e7eba52643a) Thanks [@tim-smart](https://github.com/tim-smart)! - add Logger.withSpanAnnotations + +- [#1611](https://github.com/Effect-TS/effect/pull/1611) [`8b22648aa`](https://github.com/Effect-TS/effect/commit/8b22648aa8153d31c2435c62e826e3211b2e2cd7) Thanks [@tim-smart](https://github.com/tim-smart)! - ensure pool acquire is interruptible when allocated dynamically + +- [#1642](https://github.com/Effect-TS/effect/pull/1642) [`b2fdff3b8`](https://github.com/Effect-TS/effect/commit/b2fdff3b83d566c37a499fa58f9f1492f8219e0f) Thanks [@gcanti](https://github.com/gcanti)! - TSet: merge removeIf / removeIfDiscard, retainIf / retainIf (`{ discard: boolean }` optional argument) + +- [#1647](https://github.com/Effect-TS/effect/pull/1647) [`82006f69b`](https://github.com/Effect-TS/effect/commit/82006f69b9af9bc70a06ee1abfdc9c24777025c6) Thanks [@gcanti](https://github.com/gcanti)! - turn on exactOptionalPropertyTypes + +- [#1626](https://github.com/Effect-TS/effect/pull/1626) [`2e99983ef`](https://github.com/Effect-TS/effect/commit/2e99983ef3e3cf5884c831e8b25d94f4846f739a) Thanks [@gcanti](https://github.com/gcanti)! - Fix Ref Variance + +- [#1628](https://github.com/Effect-TS/effect/pull/1628) [`ba1aa04a8`](https://github.com/Effect-TS/effect/commit/ba1aa04a8cf3ef98fb7444bcdff2196a22709736) Thanks [@gcanti](https://github.com/gcanti)! - Chunk: add toArray + +- [#1619](https://github.com/Effect-TS/effect/pull/1619) [`66e6939ea`](https://github.com/Effect-TS/effect/commit/66e6939ea0f124c0a9c672ab5d8db7dc9d4ccaa2) Thanks [@gcanti](https://github.com/gcanti)! - remove readonly tuples from return type when possible + +- [#1631](https://github.com/Effect-TS/effect/pull/1631) [`af2854596`](https://github.com/Effect-TS/effect/commit/af2854596854ec6bf9e1d1dbe24535ed2a772430) Thanks [@gcanti](https://github.com/gcanti)! - Order: refactor `clamp` with an `options` argument for `minimum` and `maximum` (standard) + +- [#1628](https://github.com/Effect-TS/effect/pull/1628) [`ba1aa04a8`](https://github.com/Effect-TS/effect/commit/ba1aa04a8cf3ef98fb7444bcdff2196a22709736) Thanks [@gcanti](https://github.com/gcanti)! - TPriorityQueue: replace toArray with toChunk + +- [#1617](https://github.com/Effect-TS/effect/pull/1617) [`79719018b`](https://github.com/Effect-TS/effect/commit/79719018b327bc457a300a6b484eca58192633fb) Thanks [@gcanti](https://github.com/gcanti)! - SortedMap: change entries to return IterableIterator<[K, V]> + +- [#1644](https://github.com/Effect-TS/effect/pull/1644) [`6e2c84d4c`](https://github.com/Effect-TS/effect/commit/6e2c84d4c3b618e355b2ef9141cef973da4768b9) Thanks [@gcanti](https://github.com/gcanti)! - interfaces: add readonly modifiers when missing and remove bivariance by changing methods to props + +- [#1607](https://github.com/Effect-TS/effect/pull/1607) [`e7101ef05`](https://github.com/Effect-TS/effect/commit/e7101ef05125f3fc60ee5e3717eda30b6fa05c4d) Thanks [@mikearnaldi](https://github.com/mikearnaldi)! - Remove potentially offenive language + +- [#1639](https://github.com/Effect-TS/effect/pull/1639) [`b27958bc5`](https://github.com/Effect-TS/effect/commit/b27958bc5206b4bdb5bcc0032041df6846f6ebbf) Thanks [@gcanti](https://github.com/gcanti)! - Match: fix record signature (remove any from the codomain) + +- [#1628](https://github.com/Effect-TS/effect/pull/1628) [`ba1aa04a8`](https://github.com/Effect-TS/effect/commit/ba1aa04a8cf3ef98fb7444bcdff2196a22709736) Thanks [@gcanti](https://github.com/gcanti)! - List: replace toReadonlyArray with toArray + +- [#1621](https://github.com/Effect-TS/effect/pull/1621) [`33c06822d`](https://github.com/Effect-TS/effect/commit/33c06822d7b415849b29c2cd04f4b96f7e001557) Thanks [@gcanti](https://github.com/gcanti)! - SortedMap: make fromIterable dual + +- [#1631](https://github.com/Effect-TS/effect/pull/1631) [`af2854596`](https://github.com/Effect-TS/effect/commit/af2854596854ec6bf9e1d1dbe24535ed2a772430) Thanks [@gcanti](https://github.com/gcanti)! - Number: refactor `between` with an `options` argument for `minimum` and `maximum` (standard) + +- [#1628](https://github.com/Effect-TS/effect/pull/1628) [`ba1aa04a8`](https://github.com/Effect-TS/effect/commit/ba1aa04a8cf3ef98fb7444bcdff2196a22709736) Thanks [@gcanti](https://github.com/gcanti)! - TSet: fix toChunk (was returning an array) + +- [#1631](https://github.com/Effect-TS/effect/pull/1631) [`af2854596`](https://github.com/Effect-TS/effect/commit/af2854596854ec6bf9e1d1dbe24535ed2a772430) Thanks [@gcanti](https://github.com/gcanti)! - Number: refactor `clamp` with an `options` argument for `minimum` and `maximum` (standard) + +- [#1631](https://github.com/Effect-TS/effect/pull/1631) [`af2854596`](https://github.com/Effect-TS/effect/commit/af2854596854ec6bf9e1d1dbe24535ed2a772430) Thanks [@gcanti](https://github.com/gcanti)! - BigDecimal: refactor `clamp` with an `options` argument for `minimum` and `maximum` (standard) + +- [#1631](https://github.com/Effect-TS/effect/pull/1631) [`af2854596`](https://github.com/Effect-TS/effect/commit/af2854596854ec6bf9e1d1dbe24535ed2a772430) Thanks [@gcanti](https://github.com/gcanti)! - BigInt: refactor `between` with an `options` argument for `minimum` and `maximum` (standard) + +- [#1631](https://github.com/Effect-TS/effect/pull/1631) [`af2854596`](https://github.com/Effect-TS/effect/commit/af2854596854ec6bf9e1d1dbe24535ed2a772430) Thanks [@gcanti](https://github.com/gcanti)! - BigInt: refactor `clamp` with an `options` argument for `minimum` and `maximum` (standard) + +- [#1617](https://github.com/Effect-TS/effect/pull/1617) [`79719018b`](https://github.com/Effect-TS/effect/commit/79719018b327bc457a300a6b484eca58192633fb) Thanks [@gcanti](https://github.com/gcanti)! - HashMap: add toEntries + +- [#1641](https://github.com/Effect-TS/effect/pull/1641) [`f0a4bf430`](https://github.com/Effect-TS/effect/commit/f0a4bf430c0d723e4d6e3f3fb48dcc7118338653) Thanks [@gcanti](https://github.com/gcanti)! - RedBlackTree: fix bug in Hash and Equal implementation + +- [#1625](https://github.com/Effect-TS/effect/pull/1625) [`cc9a03ac7`](https://github.com/Effect-TS/effect/commit/cc9a03ac7029b10b4fef838a3329962ad53c7936) Thanks [@gcanti](https://github.com/gcanti)! - TMap: fix toChunk (was returning an array) + +- [#1606](https://github.com/Effect-TS/effect/pull/1606) [`265f60842`](https://github.com/Effect-TS/effect/commit/265f608424c50d7bc9eac74e551db6d8db66cdb2) Thanks [@tim-smart](https://github.com/tim-smart)! - add Logger.mapInputOptions + +- [#1632](https://github.com/Effect-TS/effect/pull/1632) [`c86f87c1b`](https://github.com/Effect-TS/effect/commit/c86f87c1b7923ae8e66bb99d9282b35f38e16774) Thanks [@gcanti](https://github.com/gcanti)! - Either: rename `reverse` to `flip` (to align with `Effect.flip`) + +- [#1599](https://github.com/Effect-TS/effect/pull/1599) [`c3cb2dff7`](https://github.com/Effect-TS/effect/commit/c3cb2dff73f2e7293ab937bb6978995fb23d2547) Thanks [@gcanti](https://github.com/gcanti)! - add Refinement overloading to Effect.loop + +- [#1638](https://github.com/Effect-TS/effect/pull/1638) [`4eedf057b`](https://github.com/Effect-TS/effect/commit/4eedf057b38c09ea1a6bc5b85c886edb02681d54) Thanks [@gcanti](https://github.com/gcanti)! - Match: add `symbol` predicate + +- [#1640](https://github.com/Effect-TS/effect/pull/1640) [`9ea7edf77`](https://github.com/Effect-TS/effect/commit/9ea7edf775acc05b5a763310e6c4afecfda7a52c) Thanks [@gcanti](https://github.com/gcanti)! - fix link in "please report an issue..." message + +- [#1597](https://github.com/Effect-TS/effect/pull/1597) [`38643141d`](https://github.com/Effect-TS/effect/commit/38643141d55cdd8f47c96904a199f218bd890037) Thanks [@gcanti](https://github.com/gcanti)! - add Refinement overloading to Effect.iterate, closes #1596 + +- [#1621](https://github.com/Effect-TS/effect/pull/1621) [`33c06822d`](https://github.com/Effect-TS/effect/commit/33c06822d7b415849b29c2cd04f4b96f7e001557) Thanks [@gcanti](https://github.com/gcanti)! - RedBlackTree: make fromIterable dual + +- [#1631](https://github.com/Effect-TS/effect/pull/1631) [`af2854596`](https://github.com/Effect-TS/effect/commit/af2854596854ec6bf9e1d1dbe24535ed2a772430) Thanks [@gcanti](https://github.com/gcanti)! - Order: refactor `between` with an `options` argument for `minimum` and `maximum` (standard) + +- [#1625](https://github.com/Effect-TS/effect/pull/1625) [`cc9a03ac7`](https://github.com/Effect-TS/effect/commit/cc9a03ac7029b10b4fef838a3329962ad53c7936) Thanks [@gcanti](https://github.com/gcanti)! - TMap: retainIf returns `Array<[K, V]>` instead of `Array` + +- [#1630](https://github.com/Effect-TS/effect/pull/1630) [`67025357e`](https://github.com/Effect-TS/effect/commit/67025357e9c705cf68d9b7e8ffb942f567720e88) Thanks [@gcanti](https://github.com/gcanti)! - Tuple: rename `tuple` to `make` (standard) + +- [#1628](https://github.com/Effect-TS/effect/pull/1628) [`ba1aa04a8`](https://github.com/Effect-TS/effect/commit/ba1aa04a8cf3ef98fb7444bcdff2196a22709736) Thanks [@gcanti](https://github.com/gcanti)! - TPriorityQueue: replace toReadonlyArray with toArray + +- [#1625](https://github.com/Effect-TS/effect/pull/1625) [`cc9a03ac7`](https://github.com/Effect-TS/effect/commit/cc9a03ac7029b10b4fef838a3329962ad53c7936) Thanks [@gcanti](https://github.com/gcanti)! - TMap: replace toReadonlyArray with toArray + +- [#1631](https://github.com/Effect-TS/effect/pull/1631) [`af2854596`](https://github.com/Effect-TS/effect/commit/af2854596854ec6bf9e1d1dbe24535ed2a772430) Thanks [@gcanti](https://github.com/gcanti)! - BigDecimal: refactor `between` with an `options` argument for `minimum` and `maximum` (standard) + +## 2.0.0-next.54 + +### Patch Changes + +- [#1594](https://github.com/Effect-TS/effect/pull/1594) [`a3a31c722`](https://github.com/Effect-TS/effect/commit/a3a31c722dbf006f612f5909ff9b1a1f2d99c050) Thanks [@tim-smart](https://github.com/tim-smart)! - fix regression in process.hrtime detection + +## 2.0.0-next.53 + +### Minor Changes + +- [#1562](https://github.com/Effect-TS/effect/pull/1562) [`0effd559e`](https://github.com/Effect-TS/effect/commit/0effd559e510a435eae98b201226515dfbc5fc2e) Thanks [@tim-smart](https://github.com/tim-smart)! - rename RequestResolver.fromFunctionEffect to fromEffect + +- [#1564](https://github.com/Effect-TS/effect/pull/1564) [`0eb0605b8`](https://github.com/Effect-TS/effect/commit/0eb0605b89a095242093539e70cc91976e541f83) Thanks [@mikearnaldi](https://github.com/mikearnaldi)! - Isolate state by version and check for version correctness + +### Patch Changes + +- [#1586](https://github.com/Effect-TS/effect/pull/1586) [`3ed4997c6`](https://github.com/Effect-TS/effect/commit/3ed4997c667f462371f26f650ac51fe533e3c044) Thanks [@leonitousconforti](https://github.com/leonitousconforti)! - Fix List.map implementation of the index parameter and removed the index parameter from List.flatMapNonEmpty + +- [#1593](https://github.com/Effect-TS/effect/pull/1593) [`92f7316a2`](https://github.com/Effect-TS/effect/commit/92f7316a2f847582146c77b2b83bf65046bf0231) Thanks [@tim-smart](https://github.com/tim-smart)! - fix timeOrigin polyfill in clock + +- [#1568](https://github.com/Effect-TS/effect/pull/1568) [`a51fb6d80`](https://github.com/Effect-TS/effect/commit/a51fb6d80d22c912157b862432ee0ca5e0d14caa) Thanks [@tim-smart](https://github.com/tim-smart)! - add Stream.accumulate + +- [#1592](https://github.com/Effect-TS/effect/pull/1592) [`57d8f1792`](https://github.com/Effect-TS/effect/commit/57d8f17924e91e10617753382a91a3136043b421) Thanks [@gcanti](https://github.com/gcanti)! - Predicate: add hasProperty (+ internal refactoring to leverage it) + +- [#1562](https://github.com/Effect-TS/effect/pull/1562) [`0effd559e`](https://github.com/Effect-TS/effect/commit/0effd559e510a435eae98b201226515dfbc5fc2e) Thanks [@tim-smart](https://github.com/tim-smart)! - add RequestResolver.fromEffectTagged + +- [#1588](https://github.com/Effect-TS/effect/pull/1588) [`7c9d15c25`](https://github.com/Effect-TS/effect/commit/7c9d15c25f23f79ab4e7777a3b656119234586f9) Thanks [@mikearnaldi](https://github.com/mikearnaldi)! - Fix fiber failure stack + +- [#1568](https://github.com/Effect-TS/effect/pull/1568) [`a51fb6d80`](https://github.com/Effect-TS/effect/commit/a51fb6d80d22c912157b862432ee0ca5e0d14caa) Thanks [@tim-smart](https://github.com/tim-smart)! - add Stream.accumulateChunks + +- [#1585](https://github.com/Effect-TS/effect/pull/1585) [`e0ef64102`](https://github.com/Effect-TS/effect/commit/e0ef64102b05c874e844f680f53746821609e1b6) Thanks [@gcanti](https://github.com/gcanti)! - Chunk: getEquivalence, resolve index out-of-bounds error when comparing chunks of different lengths + +## 2.0.0-next.52 + +### Patch Changes + +- [#1565](https://github.com/Effect-TS/effect/pull/1565) [`98de6fe6e`](https://github.com/Effect-TS/effect/commit/98de6fe6e0cb89750cbc4ca795a880c56488a1e8) Thanks [@tim-smart](https://github.com/tim-smart)! - fix support for optional props in Data classes + +## 2.0.0-next.51 + +### Minor Changes + +- [#1560](https://github.com/Effect-TS/effect/pull/1560) [`1395dc58c`](https://github.com/Effect-TS/effect/commit/1395dc58c9d1b384d22411722eff7aeeec129d36) Thanks [@tim-smart](https://github.com/tim-smart)! - use Proxy for TaggedEnum constructors + +### Patch Changes + +- [#1555](https://github.com/Effect-TS/effect/pull/1555) [`62140675c`](https://github.com/Effect-TS/effect/commit/62140675cd0b36d203b1d8fa94ea9f1732881488) Thanks [@gcanti](https://github.com/gcanti)! - ReadonlyArray / List / Chunk: merge mapNonEmpty with map + +- [#1559](https://github.com/Effect-TS/effect/pull/1559) [`6114c3893`](https://github.com/Effect-TS/effect/commit/6114c38936d650238172f09358e82a4af21200cb) Thanks [@mikearnaldi](https://github.com/mikearnaldi)! - Avoid relying on captureStackTrace for Data.Error + +- [#1528](https://github.com/Effect-TS/effect/pull/1528) [`b45b7e452`](https://github.com/Effect-TS/effect/commit/b45b7e452681dfece0db4a85265c56cef149d721) Thanks [@fubhy](https://github.com/fubhy)! - Added BigDecimal module + +- [#1554](https://github.com/Effect-TS/effect/pull/1554) [`fe7d7c28b`](https://github.com/Effect-TS/effect/commit/fe7d7c28bb6cdbffe8af5b927e95eea8fec2d4d6) Thanks [@sukovanej](https://github.com/sukovanej)! - Fix `Struct.omit` and `Struct.pick` return types. + +- [#1547](https://github.com/Effect-TS/effect/pull/1547) [`c0569f8fe`](https://github.com/Effect-TS/effect/commit/c0569f8fe91707c2088adebd86562ec455a62bab) Thanks [@gcanti](https://github.com/gcanti)! - Data: improve DX (displayed types) + + Previously, the displayed types of data used the Omit type to exclude certain fields. + This commit removes the use of Omit from the displayed types of data. This makes the types simpler and easier to understand. + It also enforces all fields as readonly. + +- [#1549](https://github.com/Effect-TS/effect/pull/1549) [`f82208687`](https://github.com/Effect-TS/effect/commit/f82208687e04fe191f8c18a56ceb10eb61376152) Thanks [@mikearnaldi](https://github.com/mikearnaldi)! - Fix missing globalValue in Logger and Query + +- [#1557](https://github.com/Effect-TS/effect/pull/1557) [`15013f707`](https://github.com/Effect-TS/effect/commit/15013f7078358ccaf10f9a89b1d36df14b758a88) Thanks [@mikearnaldi](https://github.com/mikearnaldi)! - Allow optional parameters to be used in TaggedEnum + +## 2.0.0-next.50 + +### Minor Changes + +- [#1526](https://github.com/Effect-TS/effect/pull/1526) [`656955944`](https://github.com/Effect-TS/effect/commit/6569559440e8304c596edaaa21bcae4c8dba2568) Thanks [@gcanti](https://github.com/gcanti)! - ReadonlyRecord: remove useless alias toArray + +- [#1539](https://github.com/Effect-TS/effect/pull/1539) [`9c7dea219`](https://github.com/Effect-TS/effect/commit/9c7dea219ded2cb86a2a33d6ab98a629a891e365) Thanks [@tim-smart](https://github.com/tim-smart)! - remove sampled from span options + +- [#1530](https://github.com/Effect-TS/effect/pull/1530) [`7c3a6d59d`](https://github.com/Effect-TS/effect/commit/7c3a6d59de642a3691dff525bca981e5f6c05cd1) Thanks [@fubhy](https://github.com/fubhy)! - Change `divide` return type to `Option` and added a `unsafeDivide` operation that throws in case the divisor is `0` + +- [#1535](https://github.com/Effect-TS/effect/pull/1535) [`fd296a6d5`](https://github.com/Effect-TS/effect/commit/fd296a6d5206b1e4c072bad675f2f6a70b60a7f8) Thanks [@tim-smart](https://github.com/tim-smart)! - use context for tracer spans + +- [#1534](https://github.com/Effect-TS/effect/pull/1534) [`fb26bb770`](https://github.com/Effect-TS/effect/commit/fb26bb7707e7599a70892f06e485065e331b63e3) Thanks [@fubhy](https://github.com/fubhy)! - Removed optional math variants + +### Patch Changes + +- [#1537](https://github.com/Effect-TS/effect/pull/1537) [`9bd70154b`](https://github.com/Effect-TS/effect/commit/9bd70154b62c2f101b85a8d509e480d5281abe4b) Thanks [@patroza](https://github.com/patroza)! - fix: Either/Option gen when no yield executes, just a plain return + +- [#1526](https://github.com/Effect-TS/effect/pull/1526) [`656955944`](https://github.com/Effect-TS/effect/commit/6569559440e8304c596edaaa21bcae4c8dba2568) Thanks [@gcanti](https://github.com/gcanti)! - ReadonlyRecord: add missing APIs: + - keys + - values + - upsert + - update + - isSubrecord + - isSubrecordBy + - reduce + - every + - some + - union + - intersection + - difference + - getEquivalence + - singleton + +- [#1536](https://github.com/Effect-TS/effect/pull/1536) [`80800bfb0`](https://github.com/Effect-TS/effect/commit/80800bfb044585c836b8af585946881f2160ebb1) Thanks [@fubhy](https://github.com/fubhy)! - avoid use of bigint literals + +## 2.0.0-next.49 + +### Patch Changes + +- [#1517](https://github.com/Effect-TS/effect/pull/1517) [`685a645b9`](https://github.com/Effect-TS/effect/commit/685a645b940f7785d8c1020eeaff1591bbb19535) Thanks [@tim-smart](https://github.com/tim-smart)! - fix off-by-one bug in Stream.fromIterable + +- [#1489](https://github.com/Effect-TS/effect/pull/1489) [`c2a11978f`](https://github.com/Effect-TS/effect/commit/c2a11978f9e3e7ce9df89715770a0fee564e1422) Thanks [@FedericoBiccheddu](https://github.com/FedericoBiccheddu)! - Add `Chunk.mapNonEmpty` + +- [#1516](https://github.com/Effect-TS/effect/pull/1516) [`ccbb23ba3`](https://github.com/Effect-TS/effect/commit/ccbb23ba3b52d6920f77d69809f46cd172be98cb) Thanks [@tim-smart](https://github.com/tim-smart)! - export Channel.suspend + +- [#1511](https://github.com/Effect-TS/effect/pull/1511) [`35ecb915a`](https://github.com/Effect-TS/effect/commit/35ecb915a56ff46580747f66cf69fb1b7c0c0061) Thanks [@tim-smart](https://github.com/tim-smart)! - improve Cause toJSON output + +- [#1489](https://github.com/Effect-TS/effect/pull/1489) [`c2a11978f`](https://github.com/Effect-TS/effect/commit/c2a11978f9e3e7ce9df89715770a0fee564e1422) Thanks [@FedericoBiccheddu](https://github.com/FedericoBiccheddu)! - Add `List.mapNonEmpty` + +- [#1519](https://github.com/Effect-TS/effect/pull/1519) [`43fdc45bf`](https://github.com/Effect-TS/effect/commit/43fdc45bfd9e81797b64e62af98fc9adc629151f) Thanks [@gcanti](https://github.com/gcanti)! - HashMap: add Key, Value type-level helpers + +- [#1525](https://github.com/Effect-TS/effect/pull/1525) [`f710599df`](https://github.com/Effect-TS/effect/commit/f710599df73d10b9b73bb1890fadd300f52829de) Thanks [@ahrjarrett](https://github.com/ahrjarrett)! - removes unnecessary type parameter from TaggedEnum + +- [#1521](https://github.com/Effect-TS/effect/pull/1521) [`2db755525`](https://github.com/Effect-TS/effect/commit/2db7555256e6bfd4420cb71251c386c355ded40f) Thanks [@ahrjarrett](https://github.com/ahrjarrett)! - enforce that members passed to TaggedEnum do not have a `_tag` property themselves + +- [#1529](https://github.com/Effect-TS/effect/pull/1529) [`df512220e`](https://github.com/Effect-TS/effect/commit/df512220ee21876621d6c966f1732477b4eac796) Thanks [@tim-smart](https://github.com/tim-smart)! - fix Channel.mergeAllWith unbounded concurrency + +## 2.0.0-next.48 + +### Minor Changes + +- [#1484](https://github.com/Effect-TS/effect/pull/1484) [`4cdc1ebc6`](https://github.com/Effect-TS/effect/commit/4cdc1ebc6072db7e0038473b96c596759bff7601) Thanks [@fubhy](https://github.com/fubhy)! - Renamed `Bigint` to `BigInt` + +- [#1500](https://github.com/Effect-TS/effect/pull/1500) [`8c81e5830`](https://github.com/Effect-TS/effect/commit/8c81e58303efeb9fe408b889da6c74e3be672053) Thanks [@sukovanej](https://github.com/sukovanej)! - Allow log annotations to be any object. + +- [#1506](https://github.com/Effect-TS/effect/pull/1506) [`a4fbb7055`](https://github.com/Effect-TS/effect/commit/a4fbb705527aef50a27508825ceb31d69fc5f67d) Thanks [@tim-smart](https://github.com/tim-smart)! - move Effect.set\* Layer apis to the Layer module + +- [#1500](https://github.com/Effect-TS/effect/pull/1500) [`8c81e5830`](https://github.com/Effect-TS/effect/commit/8c81e58303efeb9fe408b889da6c74e3be672053) Thanks [@sukovanej](https://github.com/sukovanej)! - add sampled flag to spans + +- [#1506](https://github.com/Effect-TS/effect/pull/1506) [`a4fbb7055`](https://github.com/Effect-TS/effect/commit/a4fbb705527aef50a27508825ceb31d69fc5f67d) Thanks [@tim-smart](https://github.com/tim-smart)! - refactor Effect span apis + +### Patch Changes + +- [#1504](https://github.com/Effect-TS/effect/pull/1504) [`f186416b9`](https://github.com/Effect-TS/effect/commit/f186416b9108a409eae23870129b1261ef2cc41c) Thanks [@kutyel](https://github.com/kutyel)! - feat: add `ap` method to `Effect`, `ap` and `zipWith` to `Either` ⚡️ + +- [#1507](https://github.com/Effect-TS/effect/pull/1507) [`2397b5548`](https://github.com/Effect-TS/effect/commit/2397b5548b957b32acdb5baf091295babe5b36e9) Thanks [@tim-smart](https://github.com/tim-smart)! - allow message property on Data YieldableError + +- [#1501](https://github.com/Effect-TS/effect/pull/1501) [`4ca2abd06`](https://github.com/Effect-TS/effect/commit/4ca2abd06ee5e7c51abf77b094adab871693bdd5) Thanks [@tim-smart](https://github.com/tim-smart)! - add Match module + +- [#1500](https://github.com/Effect-TS/effect/pull/1500) [`8c81e5830`](https://github.com/Effect-TS/effect/commit/8c81e58303efeb9fe408b889da6c74e3be672053) Thanks [@sukovanej](https://github.com/sukovanej)! - allow tracing attributes to be unknown + +- [#1506](https://github.com/Effect-TS/effect/pull/1506) [`a4fbb7055`](https://github.com/Effect-TS/effect/commit/a4fbb705527aef50a27508825ceb31d69fc5f67d) Thanks [@tim-smart](https://github.com/tim-smart)! - add onEnd finalizer to Layer span apis + +- [#1503](https://github.com/Effect-TS/effect/pull/1503) [`6a928e49f`](https://github.com/Effect-TS/effect/commit/6a928e49f18355fdd6e82dc1b9f40f29c7aab639) Thanks [@VenomAV](https://github.com/VenomAV)! - Fix Stream.groupAdjacentBy when group spans multiple chunks + +- [#1500](https://github.com/Effect-TS/effect/pull/1500) [`8c81e5830`](https://github.com/Effect-TS/effect/commit/8c81e58303efeb9fe408b889da6c74e3be672053) Thanks [@sukovanej](https://github.com/sukovanej)! - add Tracer.externalSpan constructor + +- [#1506](https://github.com/Effect-TS/effect/pull/1506) [`a4fbb7055`](https://github.com/Effect-TS/effect/commit/a4fbb705527aef50a27508825ceb31d69fc5f67d) Thanks [@tim-smart](https://github.com/tim-smart)! - add Layer.withParentSpan api + +- [#1507](https://github.com/Effect-TS/effect/pull/1507) [`2397b5548`](https://github.com/Effect-TS/effect/commit/2397b5548b957b32acdb5baf091295babe5b36e9) Thanks [@tim-smart](https://github.com/tim-smart)! - add name getter to YieldableError + +## 2.0.0-next.47 + +### Minor Changes + +- [#1495](https://github.com/Effect-TS/effect/pull/1495) [`01c479f0c`](https://github.com/Effect-TS/effect/commit/01c479f0c86d99344c0a5625bdc2c5564915d512) Thanks [@mikearnaldi](https://github.com/mikearnaldi)! - Support rendezvous-like behaviour in Queue.bounded + +## 2.0.0-next.46 + +### Minor Changes + +- [#1483](https://github.com/Effect-TS/effect/pull/1483) [`e68453bf4`](https://github.com/Effect-TS/effect/commit/e68453bf457f32502e5cd47273c298fb24f2feb0) Thanks [@mikearnaldi](https://github.com/mikearnaldi)! - Include stack in Data.Error/Data.TaggedError + +- [#1483](https://github.com/Effect-TS/effect/pull/1483) [`e68453bf4`](https://github.com/Effect-TS/effect/commit/e68453bf457f32502e5cd47273c298fb24f2feb0) Thanks [@mikearnaldi](https://github.com/mikearnaldi)! - Include Error module in Data + +### Patch Changes + +- [#1487](https://github.com/Effect-TS/effect/pull/1487) [`bd1748406`](https://github.com/Effect-TS/effect/commit/bd17484068436d0b605c179e47ad63cdbdfb39b0) Thanks [@fubhy](https://github.com/fubhy)! - Added bigint math functions for `abs`, `sqrt`, `lcm` and `gcd` + +- [#1491](https://github.com/Effect-TS/effect/pull/1491) [`6ff77385c`](https://github.com/Effect-TS/effect/commit/6ff77385c43e049fc864719574ced3691969c3f8) Thanks [@tim-smart](https://github.com/tim-smart)! - fix Layer.withSpan optional args + +- [#1492](https://github.com/Effect-TS/effect/pull/1492) [`471b5172b`](https://github.com/Effect-TS/effect/commit/471b5172bda5d29c4104414b4017f36a45b431c7) Thanks [@tim-smart](https://github.com/tim-smart)! - ensure more failures are annotated with spans + +## 2.0.0-next.45 + +### Patch Changes + +- [#1465](https://github.com/Effect-TS/effect/pull/1465) [`10a8fb9fe`](https://github.com/Effect-TS/effect/commit/10a8fb9fe513b219e678da71ebe80ce7ce61dd68) Thanks [@tim-smart](https://github.com/tim-smart)! - add incremental only counters + +- [#1472](https://github.com/Effect-TS/effect/pull/1472) [`1c56aa9c1`](https://github.com/Effect-TS/effect/commit/1c56aa9c1b267faea5f48e0f126cac579c30ae24) Thanks [@tim-smart](https://github.com/tim-smart)! - switch to build-utils prepare-v1 + +- [#1480](https://github.com/Effect-TS/effect/pull/1480) [`c31de5410`](https://github.com/Effect-TS/effect/commit/c31de54105a42a7d27f5db797f1993b463fd7b66) Thanks [@tim-smart](https://github.com/tim-smart)! - refactor Effectable and Streamable public api + +- [#1463](https://github.com/Effect-TS/effect/pull/1463) [`8932e9b26`](https://github.com/Effect-TS/effect/commit/8932e9b264f85233069407e884b951bc87c159d4) Thanks [@gcanti](https://github.com/gcanti)! - Rename Hub to PubSub, closes #1462 + +- [#1455](https://github.com/Effect-TS/effect/pull/1455) [`c3e99ce56`](https://github.com/Effect-TS/effect/commit/c3e99ce5677ba0092e598624f7234d489f48e131) Thanks [@TylorS](https://github.com/TylorS)! - add Streamable for creating custom Streams + +- [#1465](https://github.com/Effect-TS/effect/pull/1465) [`10a8fb9fe`](https://github.com/Effect-TS/effect/commit/10a8fb9fe513b219e678da71ebe80ce7ce61dd68) Thanks [@tim-smart](https://github.com/tim-smart)! - add bigint counter & gauge metrics + +- [#1473](https://github.com/Effect-TS/effect/pull/1473) [`6c967c9bc`](https://github.com/Effect-TS/effect/commit/6c967c9bc7c6279af201ea205432d62b2a1764be) Thanks [@tim-smart](https://github.com/tim-smart)! - support records in Effect.tagMetrics + +- [#1480](https://github.com/Effect-TS/effect/pull/1480) [`c31de5410`](https://github.com/Effect-TS/effect/commit/c31de54105a42a7d27f5db797f1993b463fd7b66) Thanks [@tim-smart](https://github.com/tim-smart)! - expose Effect prototype objects in Effectable module + +## 2.0.0-next.44 + +### Patch Changes + +- [#1469](https://github.com/Effect-TS/effect/pull/1469) [`5a217ac18`](https://github.com/Effect-TS/effect/commit/5a217ac1842252636d4e529baa191ea0778e42ce) Thanks [@mikearnaldi](https://github.com/mikearnaldi)! - Fix yield loop + +## 2.0.0-next.43 + +### Patch Changes + +- [#1467](https://github.com/Effect-TS/effect/pull/1467) [`7e258a9c1`](https://github.com/Effect-TS/effect/commit/7e258a9c1fabeeb9319cb50655a0221bd1e38ac8) Thanks [@mikearnaldi](https://github.com/mikearnaldi)! - Attempt at resolving TS issue with module discovery + +## 2.0.0-next.42 + +### Patch Changes + +- [#1466](https://github.com/Effect-TS/effect/pull/1466) [`31c4068fe`](https://github.com/Effect-TS/effect/commit/31c4068fe830797162c554a57ec3e6cec8c4a834) Thanks [@tim-smart](https://github.com/tim-smart)! - ensure all fiber refs are wrapped with globalValue + +- [#1459](https://github.com/Effect-TS/effect/pull/1459) [`e8fb7f73b`](https://github.com/Effect-TS/effect/commit/e8fb7f73bd3cbdb663585de1d8d3b196a9cbec98) Thanks [@fubhy](https://github.com/fubhy)! - Fix binding issue in timeout module + +- [#1466](https://github.com/Effect-TS/effect/pull/1466) [`31c4068fe`](https://github.com/Effect-TS/effect/commit/31c4068fe830797162c554a57ec3e6cec8c4a834) Thanks [@tim-smart](https://github.com/tim-smart)! - fix comparisons by reference + +- [#1461](https://github.com/Effect-TS/effect/pull/1461) [`90210ba28`](https://github.com/Effect-TS/effect/commit/90210ba28a6b078087a4d4c7b26b1b578e920476) Thanks [@gcanti](https://github.com/gcanti)! - Error: rename Tagged to TaggedClass (to align with the naming convention in the Data module) + +## 2.0.0-next.41 + +### Patch Changes + +- [#1456](https://github.com/Effect-TS/effect/pull/1456) [`4bc30e5ff`](https://github.com/Effect-TS/effect/commit/4bc30e5ff0db46f7920cedeb9254bb09c50a5875) Thanks [@tim-smart](https://github.com/tim-smart)! - re-add types field to exports in package.json + +## 2.0.0-next.40 + +### Patch Changes + +- [#1454](https://github.com/Effect-TS/effect/pull/1454) [`0a9afd299`](https://github.com/Effect-TS/effect/commit/0a9afd299aeb265f09d46f203294db5d970cf903) Thanks [@tim-smart](https://github.com/tim-smart)! - add Layer.withSpan + +- [#1451](https://github.com/Effect-TS/effect/pull/1451) [`44ea13d9c`](https://github.com/Effect-TS/effect/commit/44ea13d9c7dc57b94b1fe73984b0a62a05994cfe) Thanks [@fubhy](https://github.com/fubhy)! - Move types export condition to the top + +## 2.0.0-next.39 + +### Patch Changes + +- [#1446](https://github.com/Effect-TS/effect/pull/1446) [`3f6f23149`](https://github.com/Effect-TS/effect/commit/3f6f23149d50ac63b94bbf452353156899750f7c) Thanks [@mikearnaldi](https://github.com/mikearnaldi)! - Add sideEffects to package json + +- [#1445](https://github.com/Effect-TS/effect/pull/1445) [`52626a538`](https://github.com/Effect-TS/effect/commit/52626a538693be17667d9f5d28b632215d0e714f) Thanks [@gcanti](https://github.com/gcanti)! - Duration: add toSeconds + +- [#1450](https://github.com/Effect-TS/effect/pull/1450) [`713337c7c`](https://github.com/Effect-TS/effect/commit/713337c7c8eb55e50dcfe538c40dace280aee3f8) Thanks [@fubhy](https://github.com/fubhy)! - Hotfix type condition in package.json exports + +- [#1449](https://github.com/Effect-TS/effect/pull/1449) [`8f74d671d`](https://github.com/Effect-TS/effect/commit/8f74d671db4018156831e8305876360ec7d1ee3f) Thanks [@tim-smart](https://github.com/tim-smart)! - add preserveModules patch for preconstruct + +## 2.0.0-next.38 + +### Patch Changes + +- [#1442](https://github.com/Effect-TS/effect/pull/1442) [`c5e4a2390`](https://github.com/Effect-TS/effect/commit/c5e4a2390168b335307004a6e5623e53bd22734e) Thanks [@tim-smart](https://github.com/tim-smart)! - add top level exports from Function + +## 2.0.0-next.37 + +### Minor Changes + +- [#1434](https://github.com/Effect-TS/effect/pull/1434) [`61b95aefe`](https://github.com/Effect-TS/effect/commit/61b95aefede7c01e00aa05e56b5f65c11736a4fd) Thanks [@mikearnaldi](https://github.com/mikearnaldi)! - Switch on \_op to allow for yieldable tagged errors + +- [#1434](https://github.com/Effect-TS/effect/pull/1434) [`61b95aefe`](https://github.com/Effect-TS/effect/commit/61b95aefede7c01e00aa05e56b5f65c11736a4fd) Thanks [@mikearnaldi](https://github.com/mikearnaldi)! - Unify ecosystem packages + +### Patch Changes + +- [#1434](https://github.com/Effect-TS/effect/pull/1434) [`61b95aefe`](https://github.com/Effect-TS/effect/commit/61b95aefede7c01e00aa05e56b5f65c11736a4fd) Thanks [@mikearnaldi](https://github.com/mikearnaldi)! - add Error module for creating error classes + +- [#1434](https://github.com/Effect-TS/effect/pull/1434) [`61b95aefe`](https://github.com/Effect-TS/effect/commit/61b95aefede7c01e00aa05e56b5f65c11736a4fd) Thanks [@mikearnaldi](https://github.com/mikearnaldi)! - add Effectable module for creating custom Effect's + +## 2.0.0-next.36 + +### Patch Changes + +- [#1436](https://github.com/Effect-TS/effect/pull/1436) [`f7cb1b8be`](https://github.com/Effect-TS/effect/commit/f7cb1b8be7dd961cbe7def7210bfac876c7f95db) Thanks [@fubhy](https://github.com/fubhy)! - update dependencies + +## 2.0.0-next.35 + +### Patch Changes + +- [#1435](https://github.com/Effect-TS/effect/pull/1435) [`f197821b7`](https://github.com/Effect-TS/effect/commit/f197821b7faa3796a861f4c2d14ce6605ba12234) Thanks [@tim-smart](https://github.com/tim-smart)! - update dependencies + +- [#1432](https://github.com/Effect-TS/effect/pull/1432) [`b8b11c5a5`](https://github.com/Effect-TS/effect/commit/b8b11c5a5aee53e880d6f205fc19027b771966f0) Thanks [@gcanti](https://github.com/gcanti)! - move FiberRefsPatch from FiberRefs module to its own module + +## 2.0.0-next.34 + +### Patch Changes + +- [#1428](https://github.com/Effect-TS/effect/pull/1428) [`d77e66834`](https://github.com/Effect-TS/effect/commit/d77e668346db402374e9a28a5f00840e75679387) Thanks [@gcanti](https://github.com/gcanti)! - expose /stm THub module + +## 2.0.0-next.33 + +### Patch Changes + +- [#1426](https://github.com/Effect-TS/effect/pull/1426) [`92af22066`](https://github.com/Effect-TS/effect/commit/92af220665261946a440b62e283e3772e4c5fa72) Thanks [@tim-smart](https://github.com/tim-smart)! - expose /data GlobalValue & Types modules + +## 2.0.0-next.32 + +### Patch Changes + +- [#1422](https://github.com/Effect-TS/effect/pull/1422) [`89759cc0c`](https://github.com/Effect-TS/effect/commit/89759cc0c934248ae3ecb0c394f5b1e0917b423f) Thanks [@gcanti](https://github.com/gcanti)! - update dependencies + +## 2.0.0-next.31 + +### Patch Changes + +- [#1419](https://github.com/Effect-TS/effect/pull/1419) [`543dfb495`](https://github.com/Effect-TS/effect/commit/543dfb495c7cfd4799b27e0623d547cf0341a838) Thanks [@tim-smart](https://github.com/tim-smart)! - update dependencies + +## 2.0.0-next.30 + +### Patch Changes + +- [#1416](https://github.com/Effect-TS/effect/pull/1416) [`f464fb494`](https://github.com/Effect-TS/effect/commit/f464fb4948aec38621ca20d824d542980bc250f5) Thanks [@tim-smart](https://github.com/tim-smart)! - update dependencies + +## 2.0.0-next.29 + +### Patch Changes + +- [#1412](https://github.com/Effect-TS/effect/pull/1412) [`93f4c9f9a`](https://github.com/Effect-TS/effect/commit/93f4c9f9ab1da2bfe37a439383bb14d861441ea4) Thanks [@tim-smart](https://github.com/tim-smart)! - update peer deps + +## 2.0.0-next.28 + +### Patch Changes + +- [#1410](https://github.com/Effect-TS/effect/pull/1410) [`a8ffb5fb9`](https://github.com/Effect-TS/effect/commit/a8ffb5fb9a4a4decc44dce811356136542813af0) Thanks [@tim-smart](https://github.com/tim-smart)! - update @effect/match + +## 2.0.0-next.27 + +### Patch Changes + +- [#1408](https://github.com/Effect-TS/effect/pull/1408) [`a6b9f4f01`](https://github.com/Effect-TS/effect/commit/a6b9f4f01892c3cdc6ce56fa3a47c051b0064629) Thanks [@tim-smart](https://github.com/tim-smart)! - update /match + +## 2.0.0-next.26 + +### Patch Changes + +- [#1404](https://github.com/Effect-TS/effect/pull/1404) [`6441df29e`](https://github.com/Effect-TS/effect/commit/6441df29ede8a8d33398fff4ae44d141741c64f9) Thanks [@tim-smart](https://github.com/tim-smart)! - expose Console module + +## 2.0.0-next.25 + +### Patch Changes + +- [#1402](https://github.com/Effect-TS/effect/pull/1402) [`0844367c5`](https://github.com/Effect-TS/effect/commit/0844367c546184dd9105a3394fc019ed9ad0199e) Thanks [@tim-smart](https://github.com/tim-smart)! - use dependencies + peerDependencies for packages + +## 2.0.0-next.24 + +### Minor Changes + +- [#1395](https://github.com/Effect-TS/effect/pull/1395) [`aecaeb88c`](https://github.com/Effect-TS/effect/commit/aecaeb88c5cc58da18e0291cdefdf3a30a14a759) Thanks [@tim-smart](https://github.com/tim-smart)! - update dependencies + +### Patch Changes + +- [#1395](https://github.com/Effect-TS/effect/pull/1395) [`aecaeb88c`](https://github.com/Effect-TS/effect/commit/aecaeb88c5cc58da18e0291cdefdf3a30a14a759) Thanks [@tim-smart](https://github.com/tim-smart)! - switch to using peerDependencies + +- [#1397](https://github.com/Effect-TS/effect/pull/1397) [`9ffd45ba7`](https://github.com/Effect-TS/effect/commit/9ffd45ba78b989d704b0d23691a7afdd758a8674) Thanks [@tim-smart](https://github.com/tim-smart)! - switch to @effect/build-utils and @effect/eslint-plugin + +## 2.0.0-next.23 + +### Patch Changes + +- [#1393](https://github.com/Effect-TS/effect/pull/1393) [`db1f1e677`](https://github.com/Effect-TS/effect/commit/db1f1e677570045126c15b0a5158866f2233363a) Thanks [@tim-smart](https://github.com/tim-smart)! - update packages + +## 2.0.0-next.22 + +### Patch Changes + +- [#1389](https://github.com/Effect-TS/effect/pull/1389) [`02703a5c7`](https://github.com/Effect-TS/effect/commit/02703a5c7959692d61dbea734bb84b4e4b48c10e) Thanks [@tim-smart](https://github.com/tim-smart)! - update packages + +## 2.0.0-next.21 + +### Patch Changes + +- [#1387](https://github.com/Effect-TS/effect/pull/1387) [`83401b13a`](https://github.com/Effect-TS/effect/commit/83401b13a98b4b961a3257d21feef0b5978cbf7e) Thanks [@tim-smart](https://github.com/tim-smart)! - update /stream + +## 2.0.0-next.20 + +### Patch Changes + +- [#1385](https://github.com/Effect-TS/effect/pull/1385) [`a53697e15`](https://github.com/Effect-TS/effect/commit/a53697e1532f330d1a653332ec3fd1d74188efbf) Thanks [@tim-smart](https://github.com/tim-smart)! - add /stm, /stream and /match + +## 2.0.0-next.19 + +### Minor Changes + +- [#1383](https://github.com/Effect-TS/effect/pull/1383) [`d9c229a87`](https://github.com/Effect-TS/effect/commit/d9c229a87133847b596f3ed2871904bb8ad90fb2) Thanks [@tim-smart](https://github.com/tim-smart)! - update /io and /data + +## 2.0.0-next.18 + +### Patch Changes + +- [#1381](https://github.com/Effect-TS/effect/pull/1381) [`bf5ebae41`](https://github.com/Effect-TS/effect/commit/bf5ebae41d4851bf2cd6228c6244ac268c20c92f) Thanks [@tim-smart](https://github.com/tim-smart)! - update /io + +## 2.0.0-next.17 + +### Patch Changes + +- [#1379](https://github.com/Effect-TS/effect/pull/1379) [`2e9b54d03`](https://github.com/Effect-TS/effect/commit/2e9b54d0393c3f3c7e63ba2d6507d36074be0b51) Thanks [@tim-smart](https://github.com/tim-smart)! - update /io and /data + +## 2.0.0-next.16 + +### Patch Changes + +- [#1376](https://github.com/Effect-TS/effect/pull/1376) [`f356fa23d`](https://github.com/Effect-TS/effect/commit/f356fa23d8dc075781432ce336ea0ed748cf8131) Thanks [@gcanti](https://github.com/gcanti)! - add Config/\* modules + +## 2.0.0-next.15 + +### Patch Changes + +- [#1374](https://github.com/Effect-TS/effect/pull/1374) [`37cb95bfd`](https://github.com/Effect-TS/effect/commit/37cb95bfd33bda273d30f62b3176bf410684ae96) Thanks [@gcanti](https://github.com/gcanti)! - remove fast-check from deps + +## 2.0.0-next.14 + +### Patch Changes + +- [#1372](https://github.com/Effect-TS/effect/pull/1372) [`1322363d5`](https://github.com/Effect-TS/effect/commit/1322363d59ddca50b72758da47a1ef8b48a53bcc) Thanks [@gcanti](https://github.com/gcanti)! - upgrade to latest versions + +## 2.0.0-next.13 + +### Patch Changes + +- [#1360](https://github.com/Effect-TS/effect/pull/1360) [`fef698b15`](https://github.com/Effect-TS/effect/commit/fef698b151dba7a4f9598a452cf6acbd1bee7567) Thanks [@mikearnaldi](https://github.com/mikearnaldi)! - Elect selected modules to main export + +## 2.0.0-next.12 + +### Patch Changes + +- [#1358](https://github.com/Effect-TS/effect/pull/1358) [`54152d7af`](https://github.com/Effect-TS/effect/commit/54152d7af3cf188b4e550d2e1879c0fe18ea2de7) Thanks [@mikearnaldi](https://github.com/mikearnaldi)! - Restructure package + +## 2.0.0-next.11 + +### Patch Changes + +- [#1356](https://github.com/Effect-TS/effect/pull/1356) [`9fcc559d2`](https://github.com/Effect-TS/effect/commit/9fcc559d2206fed5eeb44dd604d7cb3ed7c8465c) Thanks [@mikearnaldi](https://github.com/mikearnaldi)! - Update release + +## 2.0.0-next.10 + +### Patch Changes + +- [#1353](https://github.com/Effect-TS/effect/pull/1353) [`6285a7712`](https://github.com/Effect-TS/effect/commit/6285a7712b0fd630f5031fec360eb42a68d9b788) Thanks [@mikearnaldi](https://github.com/mikearnaldi)! - Update @effect/io + +## 2.0.0-next.9 + +### Patch Changes + +- [#1352](https://github.com/Effect-TS/effect/pull/1352) [`5220362c9`](https://github.com/Effect-TS/effect/commit/5220362c993fcd655229d90fdaf2a740c216b189) Thanks [@mikearnaldi](https://github.com/mikearnaldi)! - Update dependencies + +- [#1350](https://github.com/Effect-TS/effect/pull/1350) [`b18068ebe`](https://github.com/Effect-TS/effect/commit/b18068ebe8ba1c1ceebbd0ec088bd3587c318b29) Thanks [@mikearnaldi](https://github.com/mikearnaldi)! - Update @effect/io and remove LogLevel isolation + +## 2.0.0-next.8 + +### Patch Changes + +- [#1348](https://github.com/Effect-TS/effect/pull/1348) [`a789742bd`](https://github.com/Effect-TS/effect/commit/a789742bd5ef48e3023f3e47499ee11e9874501e) Thanks [@mikearnaldi](https://github.com/mikearnaldi)! - Isolate LogLevel export + +## 2.0.0-next.7 + +### Patch Changes + +- [#1346](https://github.com/Effect-TS/effect/pull/1346) [`2d6cdbc2a`](https://github.com/Effect-TS/effect/commit/2d6cdbc2a842b25e56136735953e32f851094d74) Thanks [@mikearnaldi](https://github.com/mikearnaldi)! - Add Logger extensions + +## 2.0.0-next.6 + +### Patch Changes + +- [#1344](https://github.com/Effect-TS/effect/pull/1344) [`aa550d9f9`](https://github.com/Effect-TS/effect/commit/aa550d9f9eb743ce4f6f1d7902374855b57cffe8) Thanks [@mikearnaldi](https://github.com/mikearnaldi)! - Update @effect/io + +## 2.0.0-next.5 + +### Patch Changes + +- [#1342](https://github.com/Effect-TS/effect/pull/1342) [`2c8c14f7c`](https://github.com/Effect-TS/effect/commit/2c8c14f7c9a6ca03ed38f52e9a78774403cbf8bd) Thanks [@mikearnaldi](https://github.com/mikearnaldi)! - Update @effect/io + +## 2.0.0-next.4 + +### Patch Changes + +- [#1339](https://github.com/Effect-TS/effect/pull/1339) [`aabfb1d0f`](https://github.com/Effect-TS/effect/commit/aabfb1d0fc348ad70c83706fae8c84d8cd81017f) Thanks [@mikearnaldi](https://github.com/mikearnaldi)! - Update @effect/io + +- [#1341](https://github.com/Effect-TS/effect/pull/1341) [`a2b0eca61`](https://github.com/Effect-TS/effect/commit/a2b0eca6118d895746bc178e83dfe8cba0ef5edb) Thanks [@mikearnaldi](https://github.com/mikearnaldi)! - Add Optic Re-Export + +## 2.0.0-next.3 + +### Patch Changes + +- [#1337](https://github.com/Effect-TS/effect/pull/1337) [`4f805f5c1`](https://github.com/Effect-TS/effect/commit/4f805f5c1f7306c8af144a9a5d888121c0a1488d) Thanks [@mikearnaldi](https://github.com/mikearnaldi)! - Update docs + +## 2.0.0-next.2 + +### Patch Changes + +- [#1333](https://github.com/Effect-TS/effect/pull/1333) [`b3dac7e1b`](https://github.com/Effect-TS/effect/commit/b3dac7e1be152b0882340bc57866bc8ce0a3eb47) Thanks [@mikearnaldi](https://github.com/mikearnaldi)! - Update repo in package.json + +- [#1335](https://github.com/Effect-TS/effect/pull/1335) [`3c7d4f2e4`](https://github.com/Effect-TS/effect/commit/3c7d4f2e440f2076b02f0dc985f598808b54b358) Thanks [@mikearnaldi](https://github.com/mikearnaldi)! - Update docs + +## 2.0.0-next.1 + +### Patch Changes + +- [#1330](https://github.com/Effect-TS/core/pull/1330) [`75780dea1`](https://github.com/Effect-TS/core/commit/75780dea16555c8eef8053d3cd167a60cdd2e1d9) Thanks [@mikearnaldi](https://github.com/mikearnaldi)! - Update dependencies + +## 2.0.0-next.0 + +### Major Changes + +- [#1321](https://github.com/Effect-TS/core/pull/1321) [`315a3ab42`](https://github.com/Effect-TS/core/commit/315a3ab42e626ef31fd0336214416cad86131654) Thanks [@mikearnaldi](https://github.com/mikearnaldi)! - Bootstrap Ecosystem Package + +### Patch Changes + +- [#1329](https://github.com/Effect-TS/core/pull/1329) [`b015fdac5`](https://github.com/Effect-TS/core/commit/b015fdac5d9db44bae189e216a8d68b6739d8015) Thanks [@mikearnaldi](https://github.com/mikearnaldi)! - Update to @effect/io@0.0.9 + +- [#1324](https://github.com/Effect-TS/core/pull/1324) [`74fa4086e`](https://github.com/Effect-TS/core/commit/74fa4086e5b26c5f20706a3209e6c8345e187bcc) Thanks [@mikearnaldi](https://github.com/mikearnaldi)! - Isolate Debug + +- [#1326](https://github.com/Effect-TS/core/pull/1326) [`edc131f65`](https://github.com/Effect-TS/core/commit/edc131f65d71b751f4f2dd4b46acd1fbef5f9804) Thanks [@mikearnaldi](https://github.com/mikearnaldi)! - Add Remaining Effect Re-Exports + +- [#1323](https://github.com/Effect-TS/core/pull/1323) [`7f57f59de`](https://github.com/Effect-TS/core/commit/7f57f59deabfe6d2c06afd56ffc79bb22758290b) Thanks [@mikearnaldi](https://github.com/mikearnaldi)! - Add fp-ts/data re-exports + +- [#1325](https://github.com/Effect-TS/core/pull/1325) [`52dacbf72`](https://github.com/Effect-TS/core/commit/52dacbf7252f0bfcbd9ed01b93bc0b26f0440da4) Thanks [@mikearnaldi](https://github.com/mikearnaldi)! - Add fp-ts/core Re-Exports diff --git a/repos/effect/packages/effect/LICENSE b/repos/effect/packages/effect/LICENSE new file mode 100644 index 0000000..be1f5c1 --- /dev/null +++ b/repos/effect/packages/effect/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Effectful Technologies Inc + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/repos/effect/packages/effect/README.md b/repos/effect/packages/effect/README.md new file mode 100644 index 0000000..513ae19 --- /dev/null +++ b/repos/effect/packages/effect/README.md @@ -0,0 +1,53 @@ +# `effect` Core Package + +The `effect` package is the heart of the Effect framework, providing robust primitives for managing side effects, ensuring type safety, and supporting concurrency in your TypeScript applications. + +## Requirements + +- **TypeScript 5.4 or Newer:** + Ensure you are using a compatible TypeScript version. + +- **Strict Type-Checking:** + The `strict` flag must be enabled in your `tsconfig.json`. For example: + + ```json + { + "compilerOptions": { + "strict": true + // ...other options + } + } + ``` + +## Installation + +Install the core package using your preferred package manager. For example, with npm: + +```bash +npm install effect +``` + +## Documentation + +- **Website:** + For detailed information and usage examples, visit the [Effect website](https://www.effect.website/). + +- **API Reference:** + For a complete API reference of the core package `effect`, see the [Effect API documentation](https://effect-ts.github.io/effect/). + +## Overview of Effect Modules + +The `effect` package provides a collection of modules designed for functional programming in TypeScript. Below is a brief overview of the core modules: + +| Module | Description | +| -------- | -------------------------------------------------------------------------------------------------------------------------- | +| Effect | The core abstraction for managing side effects, concurrency, and error handling in a structured way. | +| Context | A lightweight dependency injection mechanism that enables passing services through computations without direct references. | +| Layer | A system for managing dependencies, allowing for modular and composable resource allocation. | +| Fiber | Lightweight virtual threads with resource-safe cancellation capabilities, enabling many features in Effect. | +| Stream | A powerful abstraction for handling asynchronous, event-driven data processing. | +| Schedule | A module for defining retry and repeat policies with composable schedules. | +| Scope | Manages the lifecycle of resources, ensuring proper acquisition and release. | +| Schema | A powerful library for defining, validating, and transforming structured data with type-safe encoding and decoding. | + +For a comparison between `effect/Schema` and `zod`, see [Schema vs Zod](https://github.com/Effect-TS/effect/tree/main/packages/effect/schema-vs-zod.md). diff --git a/repos/effect/packages/effect/benchmark/SchemaArray.ts b/repos/effect/packages/effect/benchmark/SchemaArray.ts new file mode 100644 index 0000000..e832fc8 --- /dev/null +++ b/repos/effect/packages/effect/benchmark/SchemaArray.ts @@ -0,0 +1,45 @@ +import * as ParseResult from "effect/ParseResult" +import * as S from "effect/Schema" +import type { ParseOptions } from "effect/SchemaAST" +import { Bench } from "tinybench" + +/* +┌─────────┬──────────────────────────────────────────┬─────────────┬───────────────────┬──────────┬─────────┐ +│ (index) │ Task Name │ ops/sec │ Average Time (ns) │ Margin │ Samples │ +├─────────┼──────────────────────────────────────────┼─────────────┼───────────────────┼──────────┼─────────┤ +│ 0 │ 'Schema.decodeUnknownEither (good)' │ '3,390,518' │ 294.9401324693781 │ '±0.32%' │ 3390519 │ +│ 1 │ 'ParseResult.decodeUnknownEither (good)' │ '3,388,065' │ 295.1536946952488 │ '±0.27%' │ 3388087 │ +│ 2 │ 'Schema.decodeUnknownEither (bad)' │ '228,525' │ 4375.873939945003 │ '±0.13%' │ 228526 │ +│ 3 │ 'ParseResult.decodeUnknownEither (bad)' │ '3,236,794' │ 308.9476420349623 │ '±0.30%' │ 3236795 │ +└─────────┴──────────────────────────────────────────┴─────────────┴───────────────────┴──────────┴─────────┘ +*/ + +const bench = new Bench({ time: 1000 }) + +const schema = S.Array(S.String) + +const good = ["a", "b", "c"] + +const bad = ["a", 2, "c"] + +const schemaDecodeUnknownEither = S.decodeUnknownEither(schema) +const parseResultDecodeUnknownEither = ParseResult.decodeUnknownEither(schema) +const options: ParseOptions = { errors: "all" } + +bench + .add("Schema.decodeUnknownEither (good)", function() { + schemaDecodeUnknownEither(good, options) + }) + .add("ParseResult.decodeUnknownEither (good)", function() { + parseResultDecodeUnknownEither(good, options) + }) + .add("Schema.decodeUnknownEither (bad)", function() { + schemaDecodeUnknownEither(bad, options) + }) + .add("ParseResult.decodeUnknownEither (bad)", function() { + parseResultDecodeUnknownEither(bad, options) + }) + +await bench.run() + +console.table(bench.table()) diff --git a/repos/effect/packages/effect/benchmark/SchemaFilters.ts b/repos/effect/packages/effect/benchmark/SchemaFilters.ts new file mode 100644 index 0000000..42bc256 --- /dev/null +++ b/repos/effect/packages/effect/benchmark/SchemaFilters.ts @@ -0,0 +1,96 @@ +import * as ParseResult from "effect/ParseResult" +import * as S from "effect/Schema" +import type { ParseOptions } from "effect/SchemaAST" +import { Bench } from "tinybench" +import { z } from "zod" + +/* +┌─────────┬──────────────────────────────────────────┬─────────────┬────────────────────┬──────────┬─────────┐ +│ (index) │ Task Name │ ops/sec │ Average Time (ns) │ Margin │ Samples │ +├─────────┼──────────────────────────────────────────┼─────────────┼────────────────────┼──────────┼─────────┤ +│ 0 │ 'Schema.decodeUnknownEither (good)' │ '178,340' │ 5607.258022552317 │ '±0.39%' │ 178341 │ +│ 1 │ 'ParseResult.decodeUnknownEither (good)' │ '178,705' │ 5595.811024811139 │ '±0.15%' │ 178706 │ +│ 2 │ 'zod (good)' │ '1,745,923' │ 572.7628212909881 │ '±0.41%' │ 1745924 │ +│ 3 │ 'Schema.decodeUnknownEither (bad)' │ '103,361' │ 9674.739442547396 │ '±0.13%' │ 103363 │ +│ 4 │ 'ParseResult.decodeUnknownEither (bad)' │ '187,992' │ 5319.374051161494 │ '±0.23%' │ 187993 │ +│ 5 │ 'zod (bad)' │ '471,637' │ 2120.2705459049216 │ '±2.25%' │ 471639 │ +└─────────┴──────────────────────────────────────────┴─────────────┴────────────────────┴──────────┴─────────┘ +*/ + +const bench = new Bench({ time: 1000 }) + +const UserZod = z.object({ + name: z.string().min(3).max(20), + age: z.number().min(0).max(120), + address: z.object({ + street: z.string().min(3).max(200), + number: z.number().min(0).max(120), + city: z.string().min(3).max(200), + country: z.string().min(3).max(200), + zip: z.string().min(3).max(200) + }) +}) + +const schema = S.Struct({ + name: S.String.pipe(S.minLength(3), S.maxLength(20)), + age: S.Number.pipe(S.greaterThanOrEqualTo(0), S.lessThanOrEqualTo(120)), + address: S.Struct({ + street: S.String.pipe(S.minLength(3), S.maxLength(200)), + number: S.Number.pipe(S.greaterThanOrEqualTo(0), S.lessThanOrEqualTo(120)), + city: S.String.pipe(S.minLength(3), S.maxLength(200)), + country: S.String.pipe(S.minLength(3), S.maxLength(200)), + zip: S.String.pipe(S.minLength(3), S.maxLength(200)) + }) +}) + +const good = { + name: "Joe", + age: 13, + address: { + street: "Main Street", + number: 12, + city: "New York", + country: "USA", + zip: "12345" + } +} + +const bad = { + name: "Jo", + age: 13, + address: { + street: "Main Street", + number: 12, + city: "New York", + country: "USA", + zip: "12345" + } +} + +const schemaDecodeUnknownEither = S.decodeUnknownEither(schema) +const parseResultDecodeUnknownEither = ParseResult.decodeUnknownEither(schema) +const options: ParseOptions = { errors: "all" } + +bench + .add("Schema.decodeUnknownEither (good)", function() { + schemaDecodeUnknownEither(good, options) + }) + .add("ParseResult.decodeUnknownEither (good)", function() { + parseResultDecodeUnknownEither(good, options) + }) + .add("zod (good)", function() { + UserZod.safeParse(good) + }) + .add("Schema.decodeUnknownEither (bad)", function() { + schemaDecodeUnknownEither(bad, options) + }) + .add("ParseResult.decodeUnknownEither (bad)", function() { + parseResultDecodeUnknownEither(bad, options) + }) + .add("zod (bad)", function() { + UserZod.safeParse(bad) + }) + +await bench.run() + +console.table(bench.table()) diff --git a/repos/effect/packages/effect/benchmark/SchemaIndex.ts b/repos/effect/packages/effect/benchmark/SchemaIndex.ts new file mode 100644 index 0000000..001fd26 --- /dev/null +++ b/repos/effect/packages/effect/benchmark/SchemaIndex.ts @@ -0,0 +1,163 @@ +import * as ParseResult from "effect/ParseResult" +import * as S from "effect/Schema" +import type { ParseOptions } from "effect/SchemaAST" +import { Bench } from "tinybench" +import { z } from "zod" + +/* +┌─────────┬──────────────────────────────────────────┬───────────┬────────────────────┬──────────┬─────────┐ +│ (index) │ Task Name │ ops/sec │ Average Time (ns) │ Margin │ Samples │ +├─────────┼──────────────────────────────────────────┼───────────┼────────────────────┼──────────┼─────────┤ +│ 0 │ 'Schema.decodeUnknownEither (good)' │ '509,149' │ 1964.061260925078 │ '±1.79%' │ 509150 │ +│ 1 │ 'ParseResult.decodeUnknownEither (good)' │ '533,211' │ 1875.429060111383 │ '±0.29%' │ 533212 │ +│ 2 │ 'zod (good)' │ '678,945' │ 1472.8725318364138 │ '±0.25%' │ 678946 │ +│ 3 │ 'Schema.decodeUnknownEither (bad)' │ '150,067' │ 6663.685855746499 │ '±0.15%' │ 150068 │ +│ 4 │ 'ParseResult.decodeUnknownEither (bad)' │ '435,462' │ 2296.4078417675796 │ '±0.32%' │ 435463 │ +│ 5 │ 'zod (bad)' │ '252,755' │ 3956.3951281064533 │ '±2.17%' │ 252756 │ +└─────────┴──────────────────────────────────────────┴───────────┴────────────────────┴──────────┴─────────┘ +*/ + +const bench = new Bench({ time: 1000 }) + +const Vector = S.Tuple(S.Number, S.Number, S.Number) +const VectorZod = z.tuple([z.number(), z.number(), z.number()]) + +const Asteroid = S.Struct({ + type: S.Literal("asteroid"), + location: Vector, + mass: S.Number +}) +const AsteroidZod = z.object({ + type: z.literal("asteroid"), + location: VectorZod, + mass: z.number() +}) + +const Planet = S.Struct({ + type: S.Literal("planet"), + location: Vector, + mass: S.Number, + population: S.Number, + habitable: S.Boolean +}) +const PlanetZod = z.object({ + type: z.literal("planet"), + location: VectorZod, + mass: z.number(), + population: z.number(), + habitable: z.boolean() +}) + +const Rank = S.Union( + S.Literal("captain"), + S.Literal("first mate"), + S.Literal("officer"), + S.Literal("ensign") +) +const RankZod = z.union([ + z.literal("captain"), + z.literal("first mate"), + z.literal("officer"), + z.literal("ensign") +]) + +const CrewMember = S.Struct({ + name: S.String, + age: S.Number, + rank: Rank, + home: Planet +}) +const CrewMemberZod = z.object({ + name: z.string(), + age: z.number(), + rank: RankZod, + home: PlanetZod +}) + +const Ship = S.Struct({ + type: S.Literal("ship"), + location: Vector, + mass: S.Number, + name: S.String, + crew: S.Array(CrewMember) +}) +const ShipZod = z.object({ + type: z.literal("ship"), + location: VectorZod, + mass: z.number(), + name: z.string(), + crew: z.array(CrewMemberZod) +}) + +export const schema = S.Union(Asteroid, Planet, Ship) +export const schemaZod = z.discriminatedUnion("type", [AsteroidZod, PlanetZod, ShipZod]) + +const good = { + type: "ship", + location: [1, 2, 3], + mass: 4, + name: "foo", + crew: [ + { + name: "bar", + age: 44, + rank: "captain", + home: { + type: "planet", + location: [5, 6, 7], + mass: 8, + population: 1000, + habitable: true + } + } + ] +} + +const bad = { + type: "ship", + location: [1, 2, "a"], + mass: 4, + name: "foo", + crew: [ + { + name: "bar", + age: 44, + rank: "captain", + home: { + type: "planet", + location: [5, 6, 7], + mass: 8, + population: "a", + habitable: true + } + } + ] +} + +export const schemaDecodeUnknownEither = S.decodeUnknownEither(schema) +export const parseResultDecodeUnknownEither = ParseResult.decodeUnknownEither(schema) +const options: ParseOptions = { errors: "all" } + +bench + .add("Schema.decodeUnknownEither (good)", function() { + schemaDecodeUnknownEither(good, options) + }) + .add("ParseResult.decodeUnknownEither (good)", function() { + parseResultDecodeUnknownEither(good, options) + }) + .add("zod (good)", function() { + schemaZod.safeParse(good) + }) + .add("Schema.decodeUnknownEither (bad)", function() { + schemaDecodeUnknownEither(bad, options) + }) + .add("ParseResult.decodeUnknownEither (bad)", function() { + parseResultDecodeUnknownEither(bad, options) + }) + .add("zod (bad)", function() { + schemaZod.safeParse(bad) + }) + +await bench.run() + +console.table(bench.table()) diff --git a/repos/effect/packages/effect/benchmark/SchemaPropertyOrder.ts b/repos/effect/packages/effect/benchmark/SchemaPropertyOrder.ts new file mode 100644 index 0000000..59d250c --- /dev/null +++ b/repos/effect/packages/effect/benchmark/SchemaPropertyOrder.ts @@ -0,0 +1,49 @@ +import * as ParseResult from "effect/ParseResult" +import * as S from "effect/Schema" +import { Bench } from "tinybench" + +/* +┌─────────┬────────────────────────────────────────────────────────────┬──────────────┬────────────────────┬──────────┬──────────┐ +│ (index) │ Task Name │ ops/sec │ Average Time (ns) │ Margin │ Samples │ +├─────────┼────────────────────────────────────────────────────────────┼──────────────┼────────────────────┼──────────┼──────────┤ +│ 0 │ 'decodeUnknownEither (valid input)' │ '1,286,800' │ 777.1212922589341 │ '±0.29%' │ 1286801 │ +│ 1 │ 'decodeUnknownEitherPreserveInputKeyOrder (valid input)' │ '843,338' │ 1185.7631901288776 │ '±0.18%' │ 843339 │ +│ 2 │ 'decodeUnknownEither (invalid input)' │ '13,934,307' │ 71.76531493346903 │ '±0.31%' │ 13934308 │ +│ 3 │ 'decodeUnknownEitherPreserveInputKeyOrder (invalid input)' │ '13,450,784' │ 74.3451049138201 │ '±0.56%' │ 13450785 │ +└─────────┴────────────────────────────────────────────────────────────┴──────────────┴────────────────────┴──────────┴──────────┘ +*/ + +const bench = new Bench({ time: 1000 }) + +const schema = S.Struct({ + a: S.Literal("a"), + b: S.Array(S.String), + c: S.Record({ key: S.String, value: S.Number }), + d: S.NumberFromString, + e: S.Boolean +}) + +const validInput = { a: "a", b: ["b"], c: { c: 1 }, d: "1", e: true } + +const invalidInput = { b: ["b"], c: { c: 1 }, d: "1", e: true, a: null } + +const decodeUnknownEither = ParseResult.decodeUnknownEither(schema) +const decodeUnknownEitherPreserveInputKeyOrder = ParseResult.decodeUnknownEither(schema, { propertyOrder: "original" }) + +bench + .add("ParseResult.decodeUnknownEither (valid input)", function() { + decodeUnknownEither(validInput) + }) + .add("ParseResult.decodeUnknownEitherPreserveInputKeyOrder (valid input)", function() { + decodeUnknownEitherPreserveInputKeyOrder(validInput) + }) + .add("ParseResult.decodeUnknownEither (invalid input)", function() { + decodeUnknownEither(invalidInput) + }) + .add("ParseResult.decodeUnknownEitherPreserveInputKeyOrder (invalid input)", function() { + decodeUnknownEitherPreserveInputKeyOrder(invalidInput) + }) + +await bench.run() + +console.table(bench.table()) diff --git a/repos/effect/packages/effect/benchmark/SchemaStruct.ts b/repos/effect/packages/effect/benchmark/SchemaStruct.ts new file mode 100644 index 0000000..61d2905 --- /dev/null +++ b/repos/effect/packages/effect/benchmark/SchemaStruct.ts @@ -0,0 +1,43 @@ +import * as ParseResult from "effect/ParseResult" +import * as S from "effect/Schema" +import { Bench } from "tinybench" + +/* +┌─────────┬──────────────────────────────────────────┬──────────────┬───────────────────┬──────────┬──────────┐ +│ (index) │ Task Name │ ops/sec │ Average Time (ns) │ Margin │ Samples │ +├─────────┼──────────────────────────────────────────┼──────────────┼───────────────────┼──────────┼──────────┤ +│ 0 │ 'ParseResult.decodeUnknownEither (good)' │ '1,253,290' │ 797.8996777284824 │ '±0.23%' │ 1253291 │ +│ 1 │ 'ParseResult.decodeUnknownEither (bad)' │ '13,713,888' │ 72.91877607298059 │ '±0.36%' │ 13713890 │ +└─────────┴──────────────────────────────────────────┴──────────────┴───────────────────┴──────────┴──────────┘ +*/ + +const bench = new Bench({ time: 1000 }) + +const schema = S.Struct({ + a: S.Literal("a"), + b: S.Array(S.String), + c: S.Record({ key: S.String, value: S.Number }), + d: S.NumberFromString, + e: S.Boolean +}) + +const good = { a: "a", b: ["b"], c: { c: 1 }, d: "1", e: true } + +const bad = { b: ["b"], c: { c: 1 }, d: "1", e: true, a: null } + +const decodeUnknownEither = ParseResult.decodeUnknownEither(schema) + +// console.log(decodeUnknownEither(good)) +// console.log(decodeUnknownEither(bad)) + +bench + .add("ParseResult.decodeUnknownEither (good)", function() { + decodeUnknownEither(good) + }) + .add("ParseResult.decodeUnknownEither (bad)", function() { + decodeUnknownEither(bad) + }) + +await bench.run() + +console.table(bench.table()) diff --git a/repos/effect/packages/effect/benchmark/SchemaToString.ts b/repos/effect/packages/effect/benchmark/SchemaToString.ts new file mode 100644 index 0000000..701e1b4 --- /dev/null +++ b/repos/effect/packages/effect/benchmark/SchemaToString.ts @@ -0,0 +1,42 @@ +import * as ParseResult from "effect/ParseResult" +import * as S from "effect/Schema" +import { Bench } from "tinybench" + +/* +┌─────────┬─────────────────────────────────┬───────────┬────────────────────┬──────────┬─────────┐ +│ (index) │ Task Name │ ops/sec │ Average Time (ns) │ Margin │ Samples │ +├─────────┼─────────────────────────────────┼───────────┼────────────────────┼──────────┼─────────┤ +│ 0 │ 'toString' │ '282,142' │ 3544.30530263047 │ '±1.79%' │ 282143 │ +│ 1 │ 'toJSON' │ '319,008' │ 3134.714130322425 │ '±0.45%' │ 319009 │ +│ 2 │ 'TreeFormatter.formatIssueSync' │ '35,271' │ 28351.291506011636 │ '±0.18%' │ 35272 │ +└─────────┴─────────────────────────────────┴───────────┴────────────────────┴──────────┴─────────┘ +*/ + +const bench = new Bench({ time: 1000 }) + +const schema = S.Struct({ + a: S.Literal("a"), + b: S.Array(S.String), + c: S.Record({ key: S.String, value: S.Number }), + d: S.NumberFromString, + e: S.Boolean +}) + +const result: any = ParseResult.decodeUnknownEither(schema)({ a: "a", b: ["b"], c: { c: "c" }, d: "1", e: true }) + +// console.log(String(schema.ast)) + +bench + .add("toString", function() { + String(schema.ast) + }) + .add("toJSON", function() { + schema.ast.toJSON() + }) + .add("TreeFormatter.formatIssueSync", function() { + ParseResult.TreeFormatter.formatIssueSync(result.left) + }) + +await bench.run() + +console.table(bench.table()) diff --git a/repos/effect/packages/effect/benchmark/SchemaTreeFormatter.ts b/repos/effect/packages/effect/benchmark/SchemaTreeFormatter.ts new file mode 100644 index 0000000..66376ec --- /dev/null +++ b/repos/effect/packages/effect/benchmark/SchemaTreeFormatter.ts @@ -0,0 +1,38 @@ +import type * as Either from "effect/Either" +import * as ParseResult from "effect/ParseResult" +import * as S from "effect/Schema" +import { Bench } from "tinybench" + +/* +┌─────────┬────────────────────────────────────────┬──────────┬───────────────────┬──────────┬─────────┐ +│ (index) │ Task Name │ ops/sec │ Average Time (ns) │ Margin │ Samples │ +├─────────┼────────────────────────────────────────┼──────────┼───────────────────┼──────────┼─────────┤ +│ 0 │ 'TreeFormatter.formatIssueSync(issue)' │ '27,902' │ 35839.27072357856 │ '±0.29%' │ 27903 │ +└─────────┴────────────────────────────────────────┴──────────┴───────────────────┴──────────┴─────────┘ +*/ + +const bench = new Bench({ time: 1000 }) + +const schema = S.Struct({ + a: S.Struct({ + b: S.Struct({ + c: S.NonEmptyString + }) + }) +}) + +const decodeUnknownEither = S.decodeUnknownEither(schema) +const input = { a: { b: { c: "" } } } +const result = decodeUnknownEither(input) +const issue = (result as any as Either.Left).left.issue + +// console.log(issue) + +bench + .add("TreeFormatter.formatIssueSync(issue)", function() { + ParseResult.TreeFormatter.formatIssueSync(issue) + }) + +await bench.run() + +console.table(bench.table()) diff --git a/repos/effect/packages/effect/benchmark/SchemaTuple.ts b/repos/effect/packages/effect/benchmark/SchemaTuple.ts new file mode 100644 index 0000000..9b58f65 --- /dev/null +++ b/repos/effect/packages/effect/benchmark/SchemaTuple.ts @@ -0,0 +1,45 @@ +import * as ParseResult from "effect/ParseResult" +import * as S from "effect/Schema" +import type { ParseOptions } from "effect/SchemaAST" +import { Bench } from "tinybench" + +/* +┌─────────┬──────────────────────────────────────────┬─────────────┬────────────────────┬──────────┬─────────┐ +│ (index) │ Task Name │ ops/sec │ Average Time (ns) │ Margin │ Samples │ +├─────────┼──────────────────────────────────────────┼─────────────┼────────────────────┼──────────┼─────────┤ +│ 0 │ 'Schema.decodeUnknownEither (good)' │ '3,587,107' │ 278.7761790277582 │ '±0.37%' │ 3587108 │ +│ 1 │ 'ParseResult.decodeUnknownEither (good)' │ '3,586,893' │ 278.79274046012614 │ '±0.26%' │ 3586894 │ +│ 2 │ 'Schema.decodeUnknownEither (bad)' │ '232,689' │ 4297.571077399399 │ '±0.10%' │ 232690 │ +│ 3 │ 'ParseResult.decodeUnknownEither (bad)' │ '3,927,039' │ 254.64472936358712 │ '±0.06%' │ 3927040 │ +└─────────┴──────────────────────────────────────────┴─────────────┴────────────────────┴──────────┴─────────┘ +*/ + +const bench = new Bench({ time: 1000 }) + +const schema = S.Tuple(S.String, S.Number) + +const good = ["a", 1] + +const bad = ["a", "b"] + +const schemadecodeUnknownEither = S.decodeUnknownEither(schema) +const parseResultdecodeUnknownEither = ParseResult.decodeUnknownEither(schema) +const options: ParseOptions = { errors: "all" } + +bench + .add("Schema.decodeUnknownEither (good)", function() { + schemadecodeUnknownEither(good, options) + }) + .add("ParseResult.decodeUnknownEither (good)", function() { + parseResultdecodeUnknownEither(good, options) + }) + .add("Schema.decodeUnknownEither (bad)", function() { + schemadecodeUnknownEither(bad, options) + }) + .add("ParseResult.decodeUnknownEither (bad)", function() { + parseResultdecodeUnknownEither(bad, options) + }) + +await bench.run() + +console.table(bench.table()) diff --git a/repos/effect/packages/effect/benchmark/SchemaUnion.ts b/repos/effect/packages/effect/benchmark/SchemaUnion.ts new file mode 100644 index 0000000..91175a1 --- /dev/null +++ b/repos/effect/packages/effect/benchmark/SchemaUnion.ts @@ -0,0 +1,83 @@ +import * as RA from "effect/Array" +import * as ParseResult from "effect/ParseResult" +import * as S from "effect/Schema" +import type { ParseOptions } from "effect/SchemaAST" +import { Bench } from "tinybench" +import { z } from "zod" + +/* +┌─────────┬──────────────────────────────────────────┬─────────────┬────────────────────┬──────────┬─────────┐ +│ (index) │ Task Name │ ops/sec │ Average Time (ns) │ Margin │ Samples │ +├─────────┼──────────────────────────────────────────┼─────────────┼────────────────────┼──────────┼─────────┤ +│ 0 │ 'Schema.decodeUnknownEither (good)' │ '2,777,583' │ 360.025132633242 │ '±0.31%' │ 2777584 │ +│ 1 │ 'ParseResult.decodeUnknownEither (good)' │ '2,763,947' │ 361.80132983691675 │ '±0.03%' │ 2763948 │ +│ 2 │ 'zod (good)' │ '3,335,028' │ 299.8475137697173 │ '±0.31%' │ 3335029 │ +│ 3 │ 'Schema.decodeUnknownEither (bad)' │ '207,579' │ 4817.437354273092 │ '±0.08%' │ 207580 │ +│ 4 │ 'ParseResult.decodeUnknownEither (bad)' │ '1,707,747' │ 585.5667206168476 │ '±0.31%' │ 1707748 │ +│ 5 │ 'zod (bad)' │ '3,305,101' │ 302.5625463294264 │ '±0.24%' │ 3305102 │ +└─────────┴──────────────────────────────────────────┴─────────────┴────────────────────┴──────────┴─────────┘ +*/ + +const bench = new Bench({ time: 1000 }) + +const n = 100 +const members = RA.makeBy(n, (i) => + S.Struct({ + kind: S.Literal(i), + a: S.String, + b: S.Number, + c: S.Boolean + })) +const schema = S.Union(...members) + +const x = RA.makeBy(n, (i) => + z.object({ + kind: z.literal(i), + a: z.string(), + b: z.number(), + c: z.boolean() + })) + +const schemaZod = z.discriminatedUnion("kind", x) + +const good = { + kind: n - 1, + a: "a", + b: 1, + c: true +} + +const bad = { + kind: n - 1, + a: "a", + b: 1, + c: "c" +} + +const schemaDecodeUnknownEither = S.decodeUnknownEither(schema) +const parseResultDecodeUnknownEither = ParseResult.decodeUnknownEither(schema) +const options: ParseOptions = { errors: "all" } + +bench + .add("Schema.decodeUnknownEither (good)", function() { + schemaDecodeUnknownEither(good, options) + }) + .add("ParseResult.decodeUnknownEither (good)", function() { + parseResultDecodeUnknownEither(good, options) + }) + .add("zod (good)", function() { + schemaZod.safeParse(good) + }) + .add("Schema.decodeUnknownEither (bad)", function() { + schemaDecodeUnknownEither(bad, options) + }) + .add("ParseResult.decodeUnknownEither (bad)", function() { + parseResultDecodeUnknownEither(bad, options) + }) + .add("zod (bad)", function() { + schemaZod.safeParse(good) + }) + +await bench.run() + +console.table(bench.table()) diff --git a/repos/effect/packages/effect/benchmark/tsconfig.json b/repos/effect/packages/effect/benchmark/tsconfig.json new file mode 100644 index 0000000..754e674 --- /dev/null +++ b/repos/effect/packages/effect/benchmark/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "strict": true, + "module": "NodeNext", + "moduleResolution": "NodeNext", + "target": "ES2022", + "paths": { + "effect/*": ["../../effect/src/*.js"] + } + } +} diff --git a/repos/effect/packages/effect/docgen.json b/repos/effect/packages/effect/docgen.json new file mode 100644 index 0000000..2f0b254 --- /dev/null +++ b/repos/effect/packages/effect/docgen.json @@ -0,0 +1,27 @@ +{ + "$schema": "../../node_modules/@effect/docgen/schema.json", + "exclude": ["src/internal/**/*.ts"], + "srcLink": "https://github.com/Effect-TS/effect/tree/main/packages/effect/src/", + "examplesCompilerOptions": { + "noEmit": true, + "strict": true, + "skipLibCheck": true, + "moduleResolution": "Bundler", + "module": "ES2022", + "target": "ES2022", + "lib": ["ES2022", "DOM"], + "paths": { + "effect": ["../../../effect/src/index.js"], + "effect/*": ["../../../effect/src/*.js"], + "@effect/ai": ["../../../ai/ai/src/index.js"], + "@effect/platform": ["../../../platform/src/index.js"], + "@effect/platform/*": ["../../../platform/src/*.js"], + "@effect/printer": ["../../../printer/src/index.js"], + "@effect/printer/*": ["../../../printer/src/*.js"], + "@effect/printer-ansi": ["../../../printer-ansi/src/index.js"], + "@effect/printer-ansi/*": ["../../../printer-ansi/src/*.js"], + "@effect/typeclass": ["../../../typeclass/src/index.js"], + "@effect/typeclass/*": ["../../../typeclass/src/*.js"] + } + } +} diff --git a/repos/effect/packages/effect/dtslint/Array.tst.ts b/repos/effect/packages/effect/dtslint/Array.tst.ts new file mode 100644 index 0000000..265c168 --- /dev/null +++ b/repos/effect/packages/effect/dtslint/Array.tst.ts @@ -0,0 +1,1271 @@ +import { Array, Effect, Either, Option, Order, Predicate } from "effect" +import { hole, identity, pipe } from "effect/Function" +import { describe, expect, it, when } from "tstyche" + +declare const nonEmptyReadonlyStrings: Array.NonEmptyReadonlyArray +declare const nonEmptyNumbers: Array.NonEmptyArray +declare const nonEmptyStrings: Array.NonEmptyArray +declare const readonlyNumbers: ReadonlyArray +declare const readonlyStrings: ReadonlyArray +declare const numbers: Array +declare const strings: Array +declare const iterNumbers: Iterable +declare const iterStrings: Iterable +declare const numbersOrStrings: Array + +declare const primitiveNumber: number +declare const primitiveNumberOrString: string | number +declare const predicateNumbersOrStrings: Predicate.Predicate + +declare const unknownValue: unknown +declare const stringOrStringArrayOrNull: string | Array | null + +const symA = Symbol.for("a") +const symB = Symbol.for("b") +const symC = Symbol.for("c") + +interface A { + readonly a: string +} +interface AB extends A { + readonly b: number +} +declare const ABs: ReadonlyArray +declare const nonEmptyABs: Array.NonEmptyReadonlyArray +declare const orderA: Order.Order + +interface Eff { + readonly _R: (_: R) => void +} +interface R1 { + readonly _r1: unique symbol +} +interface R2 { + readonly _r2: unique symbol +} +interface R3 { + readonly _r3: unique symbol +} +declare const arg1: Eff +declare const arg2: Eff + +describe("Array", () => { + it("isArray", () => { + if (Array.isArray(unknownValue)) { + expect(unknownValue).type.toBe>() + } + if (Array.isArray(stringOrStringArrayOrNull)) { + expect(stringOrStringArrayOrNull).type.toBe>() + } + }) + + it("isEmptyReadonlyArray", () => { + if (Array.isEmptyReadonlyArray(readonlyNumbers)) { + expect(readonlyNumbers).type.toBe() + } + // should play well with `Option.liftPredicate` + expect(Option.liftPredicate(Array.isEmptyReadonlyArray)).type.toBe< + (a: ReadonlyArray) => Option.Option + >() + }) + + it("isEmptyArray", () => { + if (Array.isEmptyArray(numbers)) { + expect(numbers).type.toBe<[]>() + } + // should play well with `Option.liftPredicate` + expect(Option.liftPredicate(Array.isEmptyArray)).type.toBe< + (a: Array) => Option.Option<[]> + >() + }) + + it("isNonEmptyReadonlyArray", () => { + if (Array.isNonEmptyReadonlyArray(readonlyNumbers)) { + expect(readonlyNumbers).type.toBe]>() + } + // should play well with `Option.liftPredicate` + expect(Option.liftPredicate(Array.isNonEmptyReadonlyArray)).type.toBe< + (a: ReadonlyArray) => Option.Option]> + >() + }) + + it("isNonEmptyArray", () => { + if (Array.isNonEmptyArray(numbers)) { + expect(numbers).type.toBe<[number, ...Array]>() + } + // should play well with `Option.liftPredicate` + expect(Option.liftPredicate(Array.isNonEmptyArray)).type.toBe< + (a: Array) => Option.Option<[A, ...Array]> + >() + }) + + it("map", () => { + expect(Array.map(readonlyStrings, (s, i) => { + expect(s).type.toBe() + expect(i).type.toBe() + return s + "a" + })).type.toBe>() + expect(pipe( + readonlyStrings, + Array.map((s, i) => { + expect(s).type.toBe() + expect(i).type.toBe() + return s + "a" + }) + )).type.toBe>() + + expect(Array.map(strings, (s, i) => { + expect(s).type.toBe() + expect(i).type.toBe() + return s + "a" + })).type.toBe>() + expect(pipe( + strings, + Array.map((s, i) => { + expect(s).type.toBe() + expect(i).type.toBe() + return s + "a" + }) + )).type.toBe>() + + expect(Array.map(nonEmptyReadonlyStrings, (s, i) => { + expect(s).type.toBe() + expect(i).type.toBe() + return s + "a" + })).type.toBe<[string, ...Array]>() + expect(pipe( + nonEmptyReadonlyStrings, + Array.map((s, i) => { + expect(s).type.toBe() + expect(i).type.toBe() + return s + "a" + }) + )).type.toBe<[string, ...Array]>() + + expect(Array.map(nonEmptyStrings, (s, i) => { + expect(s).type.toBe() + expect(i).type.toBe() + return s + "a" + })).type.toBe<[string, ...Array]>() + expect(pipe( + nonEmptyStrings, + Array.map((s, i) => { + expect(s).type.toBe() + expect(i).type.toBe() + return s + "a" + }) + )).type.toBe<[string, ...Array]>() + }) + + it("groupBy", () => { + expect(Array.groupBy([1, 2, 3], (n) => { + expect(n).type.toBe() + return String(n) + })).type.toBe]>>() + expect(pipe( + [1, 2, 3], + Array.groupBy((n) => { + expect(n).type.toBe() + return String(n) + }) + )).type.toBe]>>() + expect( + Array.groupBy([1, 2, 3], (n) => n > 0 ? "positive" as const : "negative" as const) + ).type.toBe]>>() + expect(Array.groupBy(["a", "b"], Symbol.for)).type.toBe]>>() + expect(Array.groupBy(["a", "b"], (s) => s === "a" ? symA : s === "b" ? symB : symC)).type.toBe< + Record]> + >() + }) + + it("some", () => { + expect(Array.some(numbersOrStrings, (item, i) => { + expect(item).type.toBe() + expect(i).type.toBe() + return true + })).type.toBe() + expect(pipe( + numbersOrStrings, + Array.some((item, i) => { + expect(item).type.toBe() + expect(i).type.toBe() + return true + }) + )).type.toBe() + if (Array.some(numbersOrStrings, Predicate.isString)) { + expect(numbersOrStrings).type.toBe< + Array & readonly [string | number, ...Array] + >() + } + }) + + it("every", () => { + expect(Array.every(numbersOrStrings, (item, i) => { + expect(item).type.toBe() + expect(i).type.toBe() + return true + })).type.toBe() + expect(pipe( + numbersOrStrings, + Array.every((item, i) => { + expect(item).type.toBe() + expect(i).type.toBe() + return true + }) + )).type.toBe() + if (Array.every(numbersOrStrings, Predicate.isString)) { + expect(numbersOrStrings).type.toBe & ReadonlyArray>() + } + if (Array.every(Predicate.isString)(numbersOrStrings)) { + expect(numbersOrStrings).type.toBe & ReadonlyArray>() + } + }) + + it("append", () => { + expect(Array.append(numbersOrStrings, true)) + .type.toBe<[string | number | boolean, ...Array]>() + expect(pipe(numbersOrStrings, Array.append(true))) + .type.toBe<[string | number | boolean, ...Array]>() + expect(Array.append(true)(numbersOrStrings)) + .type.toBe<[string | number | boolean, ...Array]>() + }) + + it("prepend", () => { + expect(Array.prepend(numbersOrStrings, true)) + .type.toBe<[string | number | boolean, ...Array]>() + expect(pipe(numbersOrStrings, Array.prepend(true))) + .type.toBe<[string | number | boolean, ...Array]>() + expect(Array.prepend(true)(numbersOrStrings)) + .type.toBe<[string | number | boolean, ...Array]>() + }) + + it("sort", () => { + expect(Array.sort(ABs, orderA)).type.toBe>() + expect(pipe(ABs, Array.sort(orderA))).type.toBe>() + expect(Array.sort(orderA)(ABs)).type.toBe>() + expect(Array.sort(nonEmptyABs, orderA)).type.toBe<[AB, ...Array]>() + expect(pipe(nonEmptyABs, Array.sort(orderA))).type.toBe<[AB, ...Array]>() + expect(Array.sort(orderA)(nonEmptyABs)).type.toBe<[AB, ...Array]>() + + when(pipe).isCalledWith([1], expect(Array.sort).type.not.toBeCallableWith(Order.string)) + expect(Array.sort).type.not.toBeCallableWith([1], Order.string) + expect(Array.sort(Order.string)).type.not.toBeCallableWith([1]) + }) + + it("sortWith", () => { + expect(pipe( + ABs, + Array.sortWith(identity, (a, b) => { + expect(a).type.toBe() + expect(b).type.toBe() + return 0 + }) + )).type.toBe>() + expect(Array.sortWith(ABs, identity, (a, b) => { + expect(a).type.toBe() + expect(b).type.toBe() + return 0 + })).type.toBe>() + expect(pipe( + nonEmptyABs, + Array.sortWith(identity, (a, b) => { + expect(a).type.toBe() + expect(b).type.toBe() + return 0 + }) + )).type.toBe< + [AB, ...Array] + >() + expect(Array.sortWith(nonEmptyABs, identity, (a, b) => { + expect(a).type.toBe() + expect(b).type.toBe() + return 0 + })).type.toBe< + [AB, ...Array] + >() + }) + + it("sortBy", () => { + // Array + expect(pipe( + ABs, + Array.sortBy((a, b) => { + expect(a).type.toBe() + expect(b).type.toBe() + return 0 + }) + )).type.toBe>() + expect( + pipe( + ABs, + Array.sortBy((a, b) => { + expect(a).type.toBe() + expect(b).type.toBe() + return 0 + }) + ) + ).type.toBe>() + + // NonEmptyArray + expect(pipe( + nonEmptyABs, + Array.sortBy((a, b) => { + expect(a).type.toBe() + expect(b).type.toBe() + return 0 + }) + )).type.toBe<[AB, ...Array]>() + expect( + pipe( + nonEmptyABs, + Array.sortBy((a, b) => { + expect(a).type.toBe() + expect(b).type.toBe() + return 0 + }) + ) + ).type.toBe<[AB, ...Array]>() + }) + + it("partition", () => { + expect(Array.partition(numbersOrStrings, (item, i) => { + expect(item).type.toBe() + expect(i).type.toBe() + return true + })).type.toBe<[Array, Array]>() + expect(pipe( + numbersOrStrings, + Array.partition((item, i) => { + expect(item).type.toBe() + expect(i).type.toBe() + return true + }) + )).type.toBe<[Array, Array]>() + expect(Array.partition(numbersOrStrings, predicateNumbersOrStrings)) + .type.toBe<[excluded: Array, satisfying: Array]>() + expect(pipe(numbersOrStrings, Array.partition(predicateNumbersOrStrings))) + .type.toBe<[excluded: Array, satisfying: Array]>() + expect(Array.partition(numbersOrStrings, Predicate.isNumber)) + .type.toBe<[excluded: Array, satisfying: Array]>() + expect(pipe(numbersOrStrings, Array.partition(Predicate.isNumber))) + .type.toBe<[excluded: Array, satisfying: Array]>() + }) + + it("filter", () => { + expect(Array.filter(numbersOrStrings, (item, i) => { + expect(item).type.toBe() + expect(i).type.toBe() + return true + })).type.toBe>() + expect(pipe( + numbersOrStrings, + Array.filter((item, i) => { + expect(item).type.toBe() + expect(i).type.toBe() + return true + }) + )).type.toBe>() + + expect(Array.filter).type.not.toBeCallableWith(numbersOrStrings, (_item: string) => true) + when(pipe).isCalledWith( + numbersOrStrings, + expect(Array.filter).type.not.toBeCallableWith((_item: string) => true) + ) + + expect(Array.filter(numbers, predicateNumbersOrStrings)).type.toBe>() + expect(pipe(numbers, Array.filter(predicateNumbersOrStrings))).type.toBe>() + + expect(Array.filter(numbersOrStrings, predicateNumbersOrStrings)).type.toBe>() + expect(pipe(numbersOrStrings, Array.filter(predicateNumbersOrStrings))).type.toBe>() + + expect(Array.filter(numbersOrStrings, Predicate.isNumber)).type.toBe>() + expect(pipe(numbersOrStrings, Array.filter(Predicate.isNumber))).type.toBe>() + }) + + it("takeWhile", () => { + expect(Array.takeWhile(numbersOrStrings, (item, i) => { + expect(item).type.toBe() + expect(i).type.toBe() + return true + })).type.toBe>() + expect(pipe( + numbersOrStrings, + Array.takeWhile((item, i) => { + expect(item).type.toBe() + expect(i).type.toBe() + return true + }) + )).type.toBe>() + + expect(Array.takeWhile(numbers, predicateNumbersOrStrings)).type.toBe>() + expect(pipe(numbers, Array.takeWhile(predicateNumbersOrStrings))).type.toBe>() + + expect(Array.takeWhile(numbersOrStrings, predicateNumbersOrStrings)).type.toBe>() + expect(pipe(numbersOrStrings, Array.takeWhile(predicateNumbersOrStrings))).type.toBe>() + + expect(Array.takeWhile(numbersOrStrings, Predicate.isNumber)).type.toBe>() + expect(pipe(numbersOrStrings, Array.takeWhile(Predicate.isNumber))).type.toBe>() + }) + + it("findFirst", () => { + expect(Array.findFirst(numbersOrStrings, (item, i) => { + expect(item).type.toBe() + expect(i).type.toBe() + return true + })).type.toBe>() + expect(pipe( + numbersOrStrings, + Array.findFirst((item, i) => { + expect(item).type.toBe() + expect(i).type.toBe() + return true + }) + )).type.toBe>() + + expect(Array.findFirst(numbersOrStrings, Predicate.isNumber)).type.toBe>() + expect(pipe(numbersOrStrings, Array.findFirst(Predicate.isNumber))).type.toBe>() + + expect(Array.findFirst(numbersOrStrings, (item, i) => { + expect(item).type.toBe() + expect(i).type.toBe() + return Option.some(true) + })).type.toBe>() + expect(pipe( + numbersOrStrings, + Array.findFirst((item, i) => { + expect(item).type.toBe() + expect(i).type.toBe() + return Option.some(true) + }) + )).type.toBe>() + + expect(Array.findFirst(numbersOrStrings, predicateNumbersOrStrings)).type.toBe>() + expect(pipe(numbersOrStrings, Array.findFirst(predicateNumbersOrStrings))) + .type.toBe>() + }) + + it("findLast", () => { + expect(Array.findLast(numbersOrStrings, (item, i) => { + expect(item).type.toBe() + expect(i).type.toBe() + return true + })).type.toBe>() + expect(pipe( + numbersOrStrings, + Array.findLast((item, i) => { + expect(item).type.toBe() + expect(i).type.toBe() + return true + }) + )).type.toBe>() + + expect(Array.findLast(numbersOrStrings, Predicate.isNumber)).type.toBe>() + expect(pipe(numbersOrStrings, Array.findLast(Predicate.isNumber))).type.toBe>() + + expect(Array.findLast(numbersOrStrings, (item, i) => { + expect(item).type.toBe() + expect(i).type.toBe() + return Option.some(true) + })).type.toBe>() + expect(pipe( + numbersOrStrings, + Array.findLast((item, i) => { + expect(item).type.toBe() + expect(i).type.toBe() + return Option.some(true) + }) + )).type.toBe>() + + expect(Array.findLast(numbersOrStrings, predicateNumbersOrStrings)).type.toBe>() + expect(pipe(numbersOrStrings, Array.findLast(predicateNumbersOrStrings))) + .type.toBe>() + }) + + it("liftPredicate", () => { + expect( + pipe( + primitiveNumber, + Array.liftPredicate((n) => { + expect(n).type.toBe() + return true + }) + ) + ).type.toBe>() + expect(pipe( + primitiveNumberOrString, + Array.liftPredicate((n): n is number => { + expect(n).type.toBe() + return typeof n === "number" + }) + )).type.toBe>() + + expect(pipe(primitiveNumberOrString, Array.liftPredicate(Predicate.isString))).type.toBe>() + expect(pipe(primitiveNumberOrString, Array.liftPredicate(predicateNumbersOrStrings))) + .type.toBe>() + expect(pipe(primitiveNumber, Array.liftPredicate(predicateNumbersOrStrings))).type.toBe>() + }) + + it("span", () => { + Array.span(numbersOrStrings, (item, i) => { + expect(item).type.toBe() + expect(i).type.toBe() + return true + }) + pipe( + numbersOrStrings, + Array.span((item, i) => { + expect(item).type.toBe() + expect(i).type.toBe() + return true + }) + ) + expect(Array.span(numbers, predicateNumbersOrStrings)).type.toBe<[init: Array, rest: Array]>() + expect(pipe(numbers, Array.span(predicateNumbersOrStrings))).type.toBe<[init: Array, rest: Array]>() + + expect(Array.span(numbersOrStrings, predicateNumbersOrStrings)) + .type.toBe<[init: Array, rest: Array]>() + expect(pipe(numbersOrStrings, Array.span(predicateNumbersOrStrings))) + .type.toBe<[init: Array, rest: Array]>() + + expect(Array.span(numbersOrStrings, Predicate.isNumber)).type.toBe<[init: Array, rest: Array]>() + expect(pipe(numbersOrStrings, Array.span(Predicate.isNumber))) + .type.toBe<[init: Array, rest: Array]>() + }) + + it("dropWhile", () => { + expect(Array.dropWhile(numbersOrStrings, (item, i) => { + expect(item).type.toBe() + expect(i).type.toBe() + return true + })).type.toBe>() + expect(pipe( + numbersOrStrings, + Array.dropWhile((item, i) => { + expect(item).type.toBe() + expect(i).type.toBe() + return true + }) + )).type.toBe>() + + expect(Array.dropWhile(numbers, predicateNumbersOrStrings)).type.toBe>() + expect(pipe(numbers, Array.dropWhile(predicateNumbersOrStrings))).type.toBe>() + + expect(Array.dropWhile(numbersOrStrings, predicateNumbersOrStrings)).type.toBe>() + expect(pipe(numbersOrStrings, Array.dropWhile(predicateNumbersOrStrings))).type.toBe>() + + expect(Array.dropWhile(numbersOrStrings, Predicate.isNumber)).type.toBe>() + expect(pipe(numbersOrStrings, Array.dropWhile(Predicate.isNumber))).type.toBe>() + }) + + it("flatMap", () => { + expect( + Array.flatMap(strings, (item, i) => { + expect(item).type.toBe() + expect(i).type.toBe() + return Array.empty() + }) + ).type.toBe>() + expect( + pipe( + strings, + Array.flatMap((item, i) => { + expect(item).type.toBe() + expect(i).type.toBe() + return Array.empty() + }) + ) + ).type.toBe>() + + expect( + Array.flatMap(nonEmptyReadonlyStrings, (item, i) => { + expect(item).type.toBe() + expect(i).type.toBe() + return Array.empty() + }) + ).type.toBe>() + expect( + pipe( + nonEmptyReadonlyStrings, + Array.flatMap((item, i) => { + expect(item).type.toBe() + expect(i).type.toBe() + return Array.empty() + }) + ) + ).type.toBe>() + + expect( + Array.flatMap(nonEmptyReadonlyStrings, (item, i) => { + expect(item).type.toBe() + expect(i).type.toBe() + return Array.of(item.length) + }) + ).type.toBe<[number, ...Array]>() + expect( + pipe( + nonEmptyReadonlyStrings, + Array.flatMap((item, i) => { + expect(item).type.toBe() + expect(i).type.toBe() + return Array.of(item.length) + }) + ) + ).type.toBe<[number, ...Array]>() + }) + + it("flatten", () => { + // Mutable arrays + expect(Array.flatten(hole>>())).type.toBe>() + expect(Array.flatten(hole>>())).type.toBe>() + expect(Array.flatten(hole>>())).type.toBe>() + expect(Array.flatten(hole>>())) + .type.toBe<[number, ...Array]>() + + // Readonly arrays + expect( + hole>>>().pipe(Effect.map((x) => { + expect(x).type.toBe>>() + return Array.flatten(x) + })) + ).type.toBe, never, never>>() + expect( + hole>>>().pipe(Effect.map((x) => { + expect(x).type.toBe>>() + return Array.flatten(x) + })) + ).type.toBe], never, never>>() + + expect(Array.flatten([[arg1], [arg2]])).type.toBe | Eff>>() + expect(Array.flatten([[arg2], [arg1]])).type.toBe | Eff>>() + }) + + it("prependAll", () => { + // Array + Array + expect(Array.prependAll(strings, numbers)).type.toBe>() + expect(pipe(strings, Array.prependAll(numbers))).type.toBe>() + + // NonEmptyArray + Array + expect(Array.prependAll(nonEmptyStrings, numbers)).type.toBe<[string | number, ...Array]>() + expect(pipe(nonEmptyStrings, Array.prependAll(numbers))).type.toBe<[string | number, ...Array]>() + + // Array + NonEmptyArray + expect(Array.prependAll(strings, nonEmptyNumbers)).type.toBe<[string | number, ...Array]>() + expect(pipe(strings, Array.prependAll(nonEmptyNumbers))).type.toBe<[string | number, ...Array]>() + + // NonEmptyArray + NonEmptyArray + expect(Array.prependAll(nonEmptyStrings, nonEmptyNumbers)).type.toBe<[string | number, ...Array]>() + expect(pipe(nonEmptyStrings, Array.prependAll(nonEmptyNumbers))).type.toBe< + [string | number, ...Array] + >() + + // Iterable + Array + expect(Array.prependAll(iterStrings, numbers)).type.toBe>() + expect(pipe(iterStrings, Array.prependAll(numbers))).type.toBe>() + + // Iterable + NonEmptyArray + expect(Array.prependAll(iterStrings, nonEmptyNumbers)).type.toBe<[string | number, ...Array]>() + expect(pipe(iterStrings, Array.prependAll(nonEmptyNumbers))).type.toBe< + [string | number, ...Array] + >() + + // Array + Iterable + expect(Array.prependAll(numbers, iterStrings)).type.toBe>() + expect(pipe(numbers, Array.prependAll(iterStrings))).type.toBe>() + + // NonEmptyArray + Iterable + expect(Array.prependAll(nonEmptyStrings, iterNumbers)).type.toBe<[string | number, ...Array]>() + expect(pipe(nonEmptyStrings, Array.prependAll(iterNumbers))).type.toBe< + [string | number, ...Array] + >() + }) + + it("appendAll", () => { + // Array + Array + expect(Array.appendAll(strings, numbers)).type.toBe>() + expect(pipe(strings, Array.appendAll(numbers))).type.toBe>() + + // NonEmptyArray + Array + expect(Array.appendAll(nonEmptyStrings, numbers)).type.toBe<[string | number, ...Array]>() + expect(pipe(nonEmptyStrings, Array.appendAll(numbers))).type.toBe<[string | number, ...Array]>() + + // Array + NonEmptyArray + expect(Array.appendAll(strings, nonEmptyNumbers)).type.toBe<[string | number, ...Array]>() + expect(pipe(strings, Array.appendAll(nonEmptyNumbers))).type.toBe<[string | number, ...Array]>() + + // NonEmptyArray + NonEmptyArray + expect(Array.appendAll(nonEmptyStrings, nonEmptyNumbers)).type.toBe<[string | number, ...Array]>() + expect(pipe(nonEmptyStrings, Array.appendAll(nonEmptyNumbers))) + .type.toBe<[string | number, ...Array]>() + + // Iterable + Array + expect(Array.appendAll(iterStrings, numbers)).type.toBe>() + expect(pipe(iterStrings, Array.appendAll(numbers))).type.toBe>() + + // Iterable + NonEmptyArray + expect(Array.appendAll(iterStrings, nonEmptyNumbers)).type.toBe<[string | number, ...Array]>() + expect(pipe(iterStrings, Array.appendAll(nonEmptyNumbers))).type.toBe< + [string | number, ...Array] + >() + + // Array + Iterable + expect(Array.appendAll(numbers, iterStrings)).type.toBe>() + expect(pipe(numbers, Array.appendAll(iterStrings))).type.toBe>() + + // NonEmptyArray + Iterable + expect(Array.appendAll(nonEmptyStrings, iterNumbers)).type.toBe<[string | number, ...Array]>() + expect(pipe(nonEmptyStrings, Array.appendAll(iterNumbers))) + .type.toBe<[string | number, ...Array]>() + }) + + it("zip", () => { + expect(Array.zip(strings, numbers)).type.toBe>() + expect(pipe(strings, Array.zip(numbers))).type.toBe>() + expect(Array.zip(numbers)(strings)).type.toBe>() + + expect(Array.zip(nonEmptyStrings, nonEmptyNumbers)).type.toBe<[[string, number], ...Array<[string, number]>]>() + expect(pipe(nonEmptyStrings, Array.zip(nonEmptyNumbers))) + .type.toBe<[[string, number], ...Array<[string, number]>]>() + expect(Array.zip(nonEmptyNumbers)(nonEmptyStrings)).type.toBe<[[string, number], ...Array<[string, number]>]>() + }) + + it("intersperse", () => { + expect(Array.intersperse(strings, "a")).type.toBe>() + expect(pipe(strings, Array.intersperse("a"))).type.toBe>() + expect(Array.intersperse("a")(strings)).type.toBe>() + + expect(Array.intersperse(strings, 1)).type.toBe>() + expect(pipe(strings, Array.intersperse(1))).type.toBe>() + expect(Array.intersperse(1)(strings)).type.toBe>() + + expect(Array.intersperse(nonEmptyStrings, "a")).type.toBe<[string, ...Array]>() + expect(pipe(nonEmptyStrings, Array.intersperse("a"))).type.toBe<[string, ...Array]>() + expect(Array.intersperse("a")(nonEmptyStrings)).type.toBe<[string, ...Array]>() + + expect(Array.intersperse(nonEmptyStrings, 1)).type.toBe<[string | number, ...Array]>() + expect(pipe(nonEmptyStrings, Array.intersperse(1))).type.toBe<[string | number, ...Array]>() + expect(Array.intersperse(1)(nonEmptyStrings)).type.toBe<[string | number, ...Array]>() + }) + + it("rotate", () => { + expect(Array.rotate(strings, 10)).type.toBe>() + expect(pipe(strings, Array.rotate(10))).type.toBe>() + expect(Array.rotate(10)(strings)).type.toBe>() + + expect(Array.rotate(nonEmptyStrings, 10)).type.toBe<[string, ...Array]>() + expect(pipe(nonEmptyStrings, Array.rotate(10))).type.toBe<[string, ...Array]>() + expect(Array.rotate(10)(nonEmptyStrings)).type.toBe<[string, ...Array]>() + }) + + it("union", () => { + expect(Array.union(strings, numbers)).type.toBe>() + expect(pipe(strings, Array.union(numbers))).type.toBe>() + expect(Array.union(numbers)(strings)).type.toBe>() + + expect(Array.union(nonEmptyStrings, numbers)).type.toBe<[string | number, ...Array]>() + expect(pipe(nonEmptyStrings, Array.union(numbers))).type.toBe<[string | number, ...Array]>() + expect(Array.union(numbers)(nonEmptyStrings)).type.toBe<[string | number, ...Array]>() + + expect(Array.union(strings, nonEmptyNumbers)).type.toBe<[string | number, ...Array]>() + expect(pipe(strings, Array.union(nonEmptyNumbers))).type.toBe<[string | number, ...Array]>() + expect(Array.union(nonEmptyNumbers)(strings)).type.toBe<[string | number, ...Array]>() + + expect(Array.union(nonEmptyStrings, nonEmptyNumbers)).type.toBe<[string | number, ...Array]>() + expect(pipe(nonEmptyStrings, Array.union(nonEmptyNumbers))) + .type.toBe<[string | number, ...Array]>() + + expect(Array.union(nonEmptyNumbers)(nonEmptyStrings)).type.toBe<[string | number, ...Array]>() + }) + + it("unionWith", () => { + // Array + Array + expect( + Array.unionWith(strings, numbers, (a, b) => { + expect(a).type.toBe() + expect(b).type.toBe() + return true + }) + ).type.toBe>() + expect( + pipe( + strings, + Array.unionWith(numbers, (a, b) => { + expect(a).type.toBe() + expect(b).type.toBe() + return true + }) + ) + ).type.toBe>() + + // NonEmptyArray + Array + expect( + Array.unionWith(nonEmptyStrings, numbers, (a, b) => { + expect(a).type.toBe() + expect(b).type.toBe() + return true + }) + ).type.toBe<[string | number, ...Array]>() + expect( + pipe( + nonEmptyStrings, + Array.unionWith(numbers, (a, b) => { + expect(a).type.toBe() + expect(b).type.toBe() + return true + }) + ) + ).type.toBe<[string | number, ...Array]>() + + // Array + NonEmptyArray + expect( + Array.unionWith(strings, nonEmptyNumbers, (a, b) => { + expect(a).type.toBe() + expect(b).type.toBe() + return true + }) + ).type.toBe<[string | number, ...Array]>() + expect( + pipe( + strings, + Array.unionWith(nonEmptyNumbers, (a, b) => { + expect(a).type.toBe() + expect(b).type.toBe() + return true + }) + ) + ).type.toBe<[string | number, ...Array]>() + + // NonEmptyArray + NonEmptyArray + expect( + Array.unionWith(nonEmptyStrings, nonEmptyNumbers, (a, b) => { + expect(a).type.toBe() + expect(b).type.toBe() + return true + }) + ).type.toBe<[string | number, ...Array]>() + expect( + pipe( + nonEmptyStrings, + Array.unionWith(nonEmptyNumbers, (a, b) => { + expect(a).type.toBe() + expect(b).type.toBe() + return true + }) + ) + ).type.toBe<[string | number, ...Array]>() + }) + + it("dedupe", () => { + // Array + expect(Array.dedupe(strings)).type.toBe>() + expect(pipe(strings, Array.dedupe)).type.toBe>() + + // NonEmptyArray + expect(Array.dedupe(nonEmptyStrings)).type.toBe<[string, ...Array]>() + expect(pipe(nonEmptyStrings, Array.dedupe)).type.toBe<[string, ...Array]>() + }) + + it("dedupeWith", () => { + // Array + expect( + Array.dedupeWith(strings, (a, b) => { + expect(a).type.toBe() + expect(b).type.toBe() + return true + }) + ).type.toBe>() + expect(pipe( + strings, + Array.dedupeWith((a, b) => { + expect(a).type.toBe() + expect(b).type.toBe() + return true + }) + )).type.toBe>() + + // NonEmptyArray + expect( + Array.dedupeWith(nonEmptyStrings, (a, b) => { + expect(a).type.toBe() + expect(b).type.toBe() + return true + }) + ).type.toBe<[string, ...Array]>() + expect( + pipe( + nonEmptyStrings, + Array.dedupeWith((a, b) => { + expect(a).type.toBe() + expect(b).type.toBe() + return true + }) + ) + ).type.toBe<[string, ...Array]>() + }) + + it("chop", () => { + // Array + expect( + Array.chop(strings, ([head, ...tail]) => { + expect(head).type.toBe() + expect(tail).type.toBe>() + return [head, tail] + }) + ).type.toBe>() + expect( + pipe( + strings, + Array.chop(([head, ...tail]) => { + expect(head).type.toBe() + expect(tail).type.toBe>() + return [head, tail] + }) + ) + ).type.toBe>() + + // NonEmptyArray + expect( + Array.chop(nonEmptyStrings, ([head, ...tail]) => { + expect(head).type.toBe() + expect(tail).type.toBe>() + return [head, tail] + }) + ).type.toBe<[string, ...Array]>() + expect( + pipe( + nonEmptyStrings, + Array.chop(([head, ...tail]) => { + expect(head).type.toBe() + expect(tail).type.toBe>() + return [head, tail] + }) + ) + ).type.toBe<[string, ...Array]>() + }) + + it("chunksOf", () => { + // Array + expect(Array.chunksOf(strings, 10)).type.toBe]>>() + expect(pipe(strings, Array.chunksOf(10))).type.toBe]>>() + expect(Array.chunksOf(10)(strings)).type.toBe]>>() + + // NonEmptyArray + expect(Array.chunksOf(nonEmptyStrings, 10)) + .type.toBe<[[string, ...Array], ...Array<[string, ...Array]>]>() + expect(pipe(nonEmptyStrings, Array.chunksOf(10))) + .type.toBe<[[string, ...Array], ...Array<[string, ...Array]>]>() + expect(Array.chunksOf(10)(nonEmptyStrings)) + .type.toBe<[[string, ...Array], ...Array<[string, ...Array]>]>() + }) + + it("window", () => { + const two: number = 2 + // Array + expect(Array.window(strings, two)).type.toBe>>() + expect(pipe(strings, Array.window(two))).type.toBe>>() + expect(Array.window(two)(strings)).type.toBe>>() + + // NonEmptyArray + expect(Array.window(nonEmptyStrings, two)).type.toBe>>() + expect(pipe(nonEmptyStrings, Array.window(two))).type.toBe>>() + expect(Array.window(two)(nonEmptyStrings)).type.toBe>>() + + // literal + Array + expect(Array.window(strings, 2)).type.toBe>() + expect(pipe(strings, Array.window(2))).type.toBe>() + expect(Array.window(2)(strings)).type.toBe>() + + // literal + NonEmptyArray + expect(Array.window(nonEmptyStrings, 2)).type.toBe>() + expect(pipe(nonEmptyStrings, Array.window(2))).type.toBe>() + expect(Array.window(2)(nonEmptyStrings)).type.toBe>() + }) + + it("reverse", () => { + // Array + expect(Array.reverse(strings)).type.toBe>() + expect(pipe(strings, Array.reverse)).type.toBe>() + + // NonEmptyArray + expect(Array.reverse(nonEmptyStrings)).type.toBe<[string, ...Array]>() + expect(pipe(nonEmptyStrings, Array.reverse)).type.toBe<[string, ...Array]>() + }) + + it("unzip", () => { + // Array + expect(Array.unzip(hole>())).type.toBe<[Array, Array]>() + expect(pipe(hole>(), Array.unzip)).type.toBe<[Array, Array]>() + + // NonEmptyArray + expect(Array.unzip(hole>())) + .type.toBe<[[string, ...Array], [number, ...Array]]>() + expect(pipe(hole>(), Array.unzip)) + .type.toBe<[[string, ...Array], [number, ...Array]]>() + }) + + it("zipWith", () => { + // Array + Array + expect( + Array.zipWith(strings, numbers, (a, b) => { + expect(a).type.toBe() + expect(b).type.toBe() + return [a, b] as [string, number] + }) + ).type.toBe>() + expect( + pipe( + strings, + Array.zipWith(numbers, (a, b) => { + expect(a).type.toBe() + expect(b).type.toBe() + return [a, b] as [string, number] + }) + ) + ).type.toBe>() + + // NonEmptyArray + NonEmptyArray + expect( + Array.zipWith(nonEmptyStrings, nonEmptyNumbers, (a, b) => { + expect(a).type.toBe() + expect(b).type.toBe() + return [a, b] as [string, number] + }) + ).type.toBe<[[string, number], ...Array<[string, number]>]>() + expect( + pipe( + nonEmptyStrings, + Array.zipWith(nonEmptyNumbers, (a, b) => { + expect(a).type.toBe() + expect(b).type.toBe() + return [a, b] as [string, number] + }) + ) + ).type.toBe<[[string, number], ...Array<[string, number]>]>() + }) + + it("separate", () => { + expect(Array.separate([])).type.toBe<[Array, Array]>() + expect(Array.separate([Either.right(1)])).type.toBe<[Array, Array]>() + expect(Array.separate([Either.left("a")])).type.toBe<[Array, Array]>() + expect(Array.separate([Either.left("a"), Either.right(1)])).type.toBe<[Array, Array]>() + expect(Array.separate(hole>>())).type.toBe<[Array, Array]>() + expect(Array.separate(hole>>())).type.toBe<[Array, Array]>() + expect(Array.separate( + hole | Either.Either>>() + )).type.toBe<[Array, Array]>() + expect(Array.separate( + hole> | Iterable>>() + )).type.toBe<[Array, Array]>() + }) + + it("getRights", () => { + expect(Array.getRights([])).type.toBe>() + expect(Array.getRights([Either.left("a")])).type.toBe>() + expect(Array.getRights([Either.right(1)])).type.toBe>() + expect(Array.getRights([Either.left("a"), Either.right(1)])).type.toBe>() + expect(Array.getRights(hole>>())).type.toBe>() + expect(Array.getRights(hole>>())).type.toBe>() + expect(Array.getRights( + hole | Either.Either>>() + )).type.toBe>() + expect(Array.getRights( + hole> | Iterable>>() + )).type.toBe>() + }) + + it("getLefts", () => { + expect(Array.getLefts([])).type.toBe>() + expect(Array.getLefts([Either.left("a")])).type.toBe>() + expect(Array.getLefts([Either.right(1)])).type.toBe>() + expect(Array.getLefts([Either.left("a"), Either.right(1)])).type.toBe>() + expect(Array.getLefts(hole>>())).type.toBe>() + expect(Array.getLefts(hole>>())).type.toBe>() + expect(Array.getLefts(hole | Either.Either>>())) + .type.toBe>() + expect(Array.getLefts(hole> | Iterable>>())) + .type.toBe>() + }) + + it("getSomes", () => { + expect(Array.getSomes([])).type.toBe>() + expect(Array.getSomes([Option.none()])).type.toBe>() + expect(Array.getSomes([Option.some(1)])).type.toBe>() + expect(Array.getSomes([Option.none(), Option.some(1)])).type.toBe>() + expect(Array.getSomes(hole>>())).type.toBe>() + expect(Array.getSomes(hole>>())).type.toBe>() + expect(Array.getSomes(hole | Option.Option>>())) + .type.toBe>() + expect(Array.getSomes(hole> | Iterable>>())) + .type.toBe>() + }) + + it("replace", () => { + expect(Array.replace([], 0, "a")).type.toBe>() + expect(Array.replace(numbers, 0, "a")).type.toBe>() + expect(Array.replace(nonEmptyNumbers, 0, "a" as const)).type.toBe<[number | "a", ...Array]>() + expect(Array.replace(new Set([1, 2] as const), 0, "a" as const)).type.toBe>() + expect(pipe([], Array.replace(0, "a"))).type.toBe>() + expect(pipe(numbers, Array.replace(0, "a"))).type.toBe>() + expect(pipe(nonEmptyNumbers, Array.replace(0, "a" as const))).type.toBe<[number | "a", ...Array]>() + expect(pipe(new Set([1, 2] as const), Array.replace(0, "a" as const))).type.toBe>() + expect(pipe(Array.of(1), Array.replace(0, "a" as const))).type.toBe<[number | "a", ...Array]>() + }) + + it("replaceOption", () => { + expect(Array.replaceOption([], 0, "a")).type.toBe>>() + expect(Array.replaceOption(numbers, 0, "a")).type.toBe>>() + expect(Array.replaceOption(nonEmptyNumbers, 0, "a" as const)) + .type.toBe]>>() + expect(Array.replaceOption(new Set([1, 2] as const), 0, "a" as const)) + .type.toBe>>() + expect(pipe([], Array.replaceOption(0, "a"))).type.toBe>>() + expect(pipe(numbers, Array.replaceOption(0, "a"))).type.toBe>>() + expect(pipe(nonEmptyNumbers, Array.replaceOption(0, "a" as const))) + .type.toBe]>>() + expect(pipe(new Set([1, 2] as const), Array.replaceOption(0, "a" as const))) + .type.toBe>>() + }) + + it("modify", () => { + // Empty Array + expect(Array.modify([], 0, (n) => { + expect(n).type.toBe() + return "a" + })).type.toBe>() + expect(pipe( + [], + Array.modify(0, (n) => { + expect(n).type.toBe() + return "a" + }) + )).type.toBe>() + + // Array + expect(Array.modify(numbers, 0, (n) => { + expect(n).type.toBe() + return "a" + })).type.toBe>() + expect(pipe( + numbers, + Array.modify(0, (n) => { + expect(n).type.toBe() + return "a" + }) + )).type.toBe>() + + // NonEmptyArray + expect(Array.modify(nonEmptyNumbers, 0, (n) => { + expect(n).type.toBe() + return "a" as const + })).type.toBe<[number | "a", ...Array]>() + expect(pipe( + nonEmptyNumbers, + Array.modify(0, (n) => { + expect(n).type.toBe() + return "a" as const + }) + )).type.toBe<[number | "a", ...Array]>() + + // Iterable + expect(Array.modify(new Set([1, 2] as const), 0, (n) => { + expect(n).type.toBe<1 | 2>() + return "a" as const + })).type.toBe>() + expect(pipe( + new Set([1, 2] as const), + Array.modify(0, (n) => { + expect(n).type.toBe<1 | 2>() + return "a" as const + }) + )).type.toBe>() + }) + + it("modifyOption", () => { + // Empty Array + expect(Array.modifyOption([], 0, (n) => { + expect(n).type.toBe() + return "a" + })).type.toBe>>() + expect(pipe( + [], + Array.modifyOption(0, (n) => { + expect(n).type.toBe() + return "a" + }) + )).type.toBe>>() + + // Array + expect(Array.modifyOption(numbers, 0, (n) => { + expect(n).type.toBe() + return "a" + })).type.toBe>>() + expect(pipe( + numbers, + Array.modifyOption(0, (n) => { + expect(n).type.toBe() + return "a" + }) + )).type.toBe>>() + + // NonEmptyArray + expect(Array.modifyOption(nonEmptyNumbers, 0, (n) => { + expect(n).type.toBe() + return "a" as const + })).type.toBe]>>() + expect(pipe( + nonEmptyNumbers, + Array.modifyOption(0, (n) => { + expect(n).type.toBe() + return "a" as const + }) + )).type.toBe]>>() + + // Iterable + expect(Array.modifyOption(new Set([1, 2] as const), 0, (n) => { + expect(n).type.toBe<1 | 2>() + return "a" as const + })).type.toBe>>() + expect(pipe( + new Set([1, 2] as const), + Array.modifyOption(0, (n) => { + expect(n).type.toBe<1 | 2>() + return "a" as const + }) + )).type.toBe>>() + }) + + it("mapAccum", () => { + // Array + expect(Array.mapAccum(strings, 0, (s, a, i) => { + expect(s).type.toBe() + expect(a).type.toBe() + expect(i).type.toBe() + return [s + i, a] + })).type.toBe<[state: number, mappedArray: Array]>() + expect(pipe( + strings, + Array.mapAccum(0, (s, a, i) => { + expect(s).type.toBe() + expect(a).type.toBe() + expect(i).type.toBe() + return [s + i, a] + }) + )).type.toBe<[state: number, mappedArray: Array]>() + + // NonEmptyArray + expect(Array.mapAccum(nonEmptyReadonlyStrings, 0, (s, a, i) => { + expect(s).type.toBe() + expect(a).type.toBe() + expect(i).type.toBe() + return [s + i, a] + })).type.toBe<[state: number, mappedArray: [string, ...Array]]>() + expect(pipe( + nonEmptyReadonlyStrings, + Array.mapAccum(0, (s, a, i) => { + expect(s).type.toBe() + expect(a).type.toBe() + expect(i).type.toBe() + return [s + i, a] + }) + )).type.toBe<[state: number, mappedArray: [string, ...Array]]>() + }) +}) diff --git a/repos/effect/packages/effect/dtslint/Cause.tst.ts b/repos/effect/packages/effect/dtslint/Cause.tst.ts new file mode 100644 index 0000000..d7274c4 --- /dev/null +++ b/repos/effect/packages/effect/dtslint/Cause.tst.ts @@ -0,0 +1,22 @@ +import type { Predicate } from "effect" +import { Cause, hole, pipe } from "effect" +import { describe, expect, it } from "tstyche" + +declare const cause1: Cause.Cause<"err-1"> +declare const cause2: Cause.Cause<"err-2"> + +describe("Cause", () => { + it("andThen", () => { + expect(Cause.andThen(cause1, cause2)).type.toBe>() + expect(Cause.andThen(cause1, () => cause2)).type.toBe>() + + expect(cause1.pipe(Cause.andThen(cause2))).type.toBe>() + expect(cause1.pipe(Cause.andThen(() => cause2))).type.toBe>() + }) + + it("filter", () => { + const predicate = hole>>() + expect(Cause.filter(cause1, predicate)).type.toBe>() + expect(pipe(cause1, Cause.filter(predicate))).type.toBe>() + }) +}) diff --git a/repos/effect/packages/effect/dtslint/Chunk.tst.ts b/repos/effect/packages/effect/dtslint/Chunk.tst.ts new file mode 100644 index 0000000..ab92af4 --- /dev/null +++ b/repos/effect/packages/effect/dtslint/Chunk.tst.ts @@ -0,0 +1,470 @@ +import type { Option } from "effect" +import { Chunk, Effect, hole, pipe, Predicate } from "effect" +import { describe, expect, it } from "tstyche" + +declare const numbers: Chunk.Chunk +declare const strings: Chunk.Chunk +declare const nonEmptyNumbers: Chunk.NonEmptyChunk +declare const nonEmptyStrings: Chunk.NonEmptyChunk +declare const numbersOrStrings: Chunk.Chunk +declare const predicateNumbersOrStrings: Predicate.Predicate + +describe("Chunk", () => { + it("every", () => { + if (Chunk.every(numbersOrStrings, Predicate.isString)) { + expect(numbersOrStrings).type.toBe>() + } + if (Chunk.every(Predicate.isString)(numbersOrStrings)) { + expect(numbersOrStrings).type.toBe>() + } + + expect(Chunk.every(numbersOrStrings, (item) => { + expect(item).type.toBe() + return true + })).type.toBe() + expect(pipe( + numbersOrStrings, + Chunk.every((item) => { + expect(item).type.toBe() + return true + }) + )).type.toBe() + }) + + it("some", () => { + if (Chunk.some(numbersOrStrings, Predicate.isString)) { + expect(numbersOrStrings).type.toBe>() + } + + expect( + Chunk.some(numbersOrStrings, (item) => { + expect(item).type.toBe() + return true + }) + ).type.toBe() + expect(pipe( + numbersOrStrings, + Chunk.some((item) => { + expect(item).type.toBe() + return true + }) + )).type.toBe() + }) + + it("partition", () => { + expect(Chunk.partition(numbersOrStrings, (item) => { + expect(item).type.toBe() + return true + })).type.toBe<[excluded: Chunk.Chunk, satisfying: Chunk.Chunk]>() + expect(pipe( + numbersOrStrings, + Chunk.partition((item) => { + expect(item).type.toBe() + return true + }) + )).type.toBe<[excluded: Chunk.Chunk, satisfying: Chunk.Chunk]>() + + expect(Chunk.partition(numbersOrStrings, predicateNumbersOrStrings)).type.toBe< + [excluded: Chunk.Chunk, satisfying: Chunk.Chunk] + >() + expect(pipe(numbersOrStrings, Chunk.partition(predicateNumbersOrStrings))).type.toBe< + [excluded: Chunk.Chunk, satisfying: Chunk.Chunk] + >() + + expect(Chunk.partition(numbers, predicateNumbersOrStrings)).type.toBe< + [excluded: Chunk.Chunk, satisfying: Chunk.Chunk] + >() + expect(pipe(numbers, Chunk.partition(predicateNumbersOrStrings))).type.toBe< + [excluded: Chunk.Chunk, satisfying: Chunk.Chunk] + >() + + expect(Chunk.partition(numbersOrStrings, Predicate.isNumber)).type.toBe< + [excluded: Chunk.Chunk, satisfying: Chunk.Chunk] + >() + expect(pipe(numbersOrStrings, Chunk.partition(Predicate.isNumber))).type.toBe< + [excluded: Chunk.Chunk, satisfying: Chunk.Chunk] + >() + }) + + it("append", () => { + expect(Chunk.append(numbersOrStrings, true)).type.toBe>() + expect(pipe(numbersOrStrings, Chunk.append(true))).type.toBe>() + expect(Chunk.append(true)(numbersOrStrings)).type.toBe>() + }) + + it("prepend", () => { + expect(Chunk.prepend(numbersOrStrings, true)).type.toBe>() + expect(pipe(numbersOrStrings, Chunk.prepend(true))).type.toBe>() + expect(Chunk.prepend(true)(numbersOrStrings)).type.toBe>() + }) + + it("map", () => { + // Chunk + expect(Chunk.map(strings, (s, i) => { + expect(s).type.toBe() + expect(i).type.toBe() + return s + "a" + })).type.toBe>() + expect(pipe( + strings, + Chunk.map((s, i) => { + expect(s).type.toBe() + expect(i).type.toBe() + return s + "a" + }) + )).type.toBe>() + + // NonEmptyChunk + expect(Chunk.map(nonEmptyStrings, (s, i) => { + expect(s).type.toBe() + expect(i).type.toBe() + return s + "a" + })).type.toBe>() + expect(pipe( + nonEmptyStrings, + Chunk.map((s, i) => { + expect(s).type.toBe() + expect(i).type.toBe() + return s + "a" + }) + )).type.toBe>() + }) + + it("filter", () => { + Chunk.filter(numbersOrStrings, (item) => { + expect(item).type.toBe() + return true + }) + expect( + pipe( + numbersOrStrings, + Chunk.filter((item) => { + expect(item).type.toBe() + return true + }) + ) + ).type.toBe>() + expect( + Chunk.filter(numbersOrStrings, predicateNumbersOrStrings) + ).type.toBe>() + expect( + pipe(numbersOrStrings, Chunk.filter(predicateNumbersOrStrings)) + ).type.toBe>() + + expect( + Chunk.filter(numbers, predicateNumbersOrStrings) + ).type.toBe>() + expect( + pipe(numbers, Chunk.filter(predicateNumbersOrStrings)) + ).type.toBe>() + + expect( + Chunk.filter(numbersOrStrings, Predicate.isNumber) + ).type.toBe>() + expect( + pipe(numbersOrStrings, Chunk.filter(Predicate.isNumber)) + ).type.toBe>() + }) + + it("takeWhile", () => { + Chunk.takeWhile(numbersOrStrings, (item) => { + expect(item).type.toBe() + return true + }) + expect( + pipe( + numbersOrStrings, + Chunk.takeWhile((item) => { + expect(item).type.toBe() + return true + }) + ) + ).type.toBe>() + + expect( + Chunk.takeWhile(numbersOrStrings, predicateNumbersOrStrings) + ).type.toBe>() + expect( + pipe(numbersOrStrings, Chunk.takeWhile(predicateNumbersOrStrings)) + ).type.toBe>() + + expect(Chunk.takeWhile(numbers, predicateNumbersOrStrings)).type.toBe< + Chunk.Chunk + >() + expect( + pipe(numbers, Chunk.takeWhile(predicateNumbersOrStrings)) + ).type.toBe>() + + expect( + Chunk.takeWhile(numbersOrStrings, Predicate.isNumber) + ).type.toBe>() + expect( + pipe(numbersOrStrings, Chunk.takeWhile(Predicate.isNumber)) + ).type.toBe>() + }) + + it("findFirst", () => { + Chunk.findFirst(numbersOrStrings, (item) => { + expect(item).type.toBe() + return true + }) + expect( + pipe( + numbersOrStrings, + Chunk.findFirst((_item) => { + expect(_item).type.toBe() + return true + }) + ) + ).type.toBe>() + + expect(Chunk.findFirst(numbersOrStrings, predicateNumbersOrStrings)).type.toBe>() + expect(pipe(numbersOrStrings, Chunk.findFirst(predicateNumbersOrStrings))).type.toBe< + Option.Option + >() + + expect(Chunk.findFirst(numbersOrStrings, Predicate.isNumber)).type.toBe>() + expect(pipe(numbersOrStrings, Chunk.findFirst(Predicate.isNumber))).type.toBe>() + }) + + it("findLast", () => { + Chunk.findLast(numbersOrStrings, (_item) => { + expect(_item).type.toBe() + return true + }) + expect( + pipe( + numbersOrStrings, + Chunk.findLast((_item) => { + expect(_item).type.toBe() + return true + }) + ) + ).type.toBe>() + + expect(Chunk.findLast(numbersOrStrings, predicateNumbersOrStrings)).type.toBe>() + expect(pipe(numbersOrStrings, Chunk.findLast(predicateNumbersOrStrings))).type.toBe< + Option.Option + >() + + expect(Chunk.findLast(numbersOrStrings, Predicate.isNumber)).type.toBe>() + expect(pipe(numbersOrStrings, Chunk.findLast(Predicate.isNumber))).type.toBe>() + }) + + it("dropWhile", () => { + Chunk.dropWhile(numbersOrStrings, (item) => { + expect(item).type.toBe() + return true + }) + expect( + pipe( + numbersOrStrings, + Chunk.dropWhile((item) => { + expect(item).type.toBe() + return true + }) + ) + ).type.toBe>() + + expect(Chunk.dropWhile(numbers, predicateNumbersOrStrings)).type.toBe>() + expect(pipe(numbers, Chunk.dropWhile(predicateNumbersOrStrings))).type.toBe>() + + expect(Chunk.dropWhile(numbersOrStrings, Predicate.isNumber)).type.toBe>() + expect(pipe(numbersOrStrings, Chunk.dropWhile(Predicate.isNumber))).type.toBe>() + }) + + it("splitWhere", () => { + Chunk.splitWhere(numbersOrStrings, (item) => { + expect(item).type.toBe() + return true + }) + expect( + pipe( + numbersOrStrings, + Chunk.splitWhere((item) => { + expect(item).type.toBe() + return true + }) + ) + ).type.toBe< + [beforeMatch: Chunk.Chunk, fromMatch: Chunk.Chunk] + >() + + expect(Chunk.splitWhere(numbers, predicateNumbersOrStrings)) + .type.toBe<[beforeMatch: Chunk.Chunk, fromMatch: Chunk.Chunk]>() + expect(pipe(numbers, Chunk.splitWhere(predicateNumbersOrStrings))) + .type.toBe<[beforeMatch: Chunk.Chunk, fromMatch: Chunk.Chunk]>() + + expect(Chunk.splitWhere(numbersOrStrings, Predicate.isNumber)) + .type.toBe<[beforeMatch: Chunk.Chunk, fromMatch: Chunk.Chunk]>() + expect(pipe(numbersOrStrings, Chunk.splitWhere(Predicate.isNumber))) + .type.toBe<[beforeMatch: Chunk.Chunk, fromMatch: Chunk.Chunk]>() + }) + + it("prependAll", () => { + // Chunk + Chunk + expect(Chunk.prependAll(strings, numbers)).type.toBe>() + expect(pipe(strings, Chunk.prependAll(numbers))).type.toBe>() + + // NonEmptyChunk + Chunk + expect(Chunk.prependAll(nonEmptyStrings, numbers)).type.toBe>() + expect(pipe(nonEmptyStrings, Chunk.prependAll(numbers))).type.toBe>() + + // Chunk + NonEmptyChunk + expect(Chunk.prependAll(strings, nonEmptyNumbers)).type.toBe>() + expect(pipe(strings, Chunk.prependAll(nonEmptyNumbers))).type.toBe>() + + // NonEmptyChunk + NonEmptyChunk + expect(Chunk.prependAll(nonEmptyStrings, nonEmptyNumbers)).type.toBe>() + expect(pipe(nonEmptyStrings, Chunk.prependAll(nonEmptyNumbers))).type.toBe>() + }) + + it("appendAll", () => { + // Chunk + Chunk + expect(Chunk.appendAll(strings, numbers)).type.toBe>() + expect(pipe(strings, Chunk.appendAll(numbers))).type.toBe>() + + // NonEmptyChunk + Chunk + expect(Chunk.appendAll(nonEmptyStrings, numbers)).type.toBe>() + expect(pipe(nonEmptyStrings, Chunk.appendAll(numbers))).type.toBe>() + + // Chunk + NonEmptyChunk + expect(Chunk.appendAll(strings, nonEmptyNumbers)).type.toBe>() + expect(pipe(strings, Chunk.appendAll(nonEmptyNumbers))).type.toBe>() + + // NonEmptyChunk + NonEmptyChunk + expect(Chunk.appendAll(nonEmptyStrings, nonEmptyNumbers)).type.toBe>() + expect(pipe(nonEmptyStrings, Chunk.appendAll(nonEmptyNumbers))).type.toBe>() + }) + + it("flatMap", () => { + // Chunk + Chunk + expect( + Chunk.flatMap(strings, (s, i) => { + expect(s).type.toBe() + expect(i).type.toBe() + return Chunk.empty() + }) + ).type.toBe>() + expect( + pipe( + strings, + Chunk.flatMap((s, i) => { + expect(s).type.toBe() + expect(i).type.toBe() + return Chunk.empty() + }) + ) + ).type.toBe>() + + // NonEmptyChunk + Chunk + expect( + Chunk.flatMap(nonEmptyStrings, (s, i) => { + expect(s).type.toBe() + expect(i).type.toBe() + return Chunk.empty() + }) + ).type.toBe>() + expect( + pipe( + nonEmptyStrings, + Chunk.flatMap((s, i) => { + expect(s).type.toBe() + expect(i).type.toBe() + return Chunk.empty() + }) + ) + ).type.toBe>() + + // NonEmptyChunk + NonEmptyChunk + expect( + Chunk.flatMap(nonEmptyStrings, (s, i) => { + expect(s).type.toBe() + expect(i).type.toBe() + return Chunk.of(s.length) + }) + ).type.toBe>() + expect( + pipe( + nonEmptyStrings, + Chunk.flatMap((s, i) => { + expect(s).type.toBe() + expect(i).type.toBe() + return Chunk.of(s.length) + }) + ) + ).type.toBe>() + }) + + it("flatten", () => { + expect(Chunk.flatten(hole>>())).type.toBe>() + expect(pipe(hole>>(), Chunk.flatten)).type.toBe>() + + expect(Chunk.flatten(hole>>())).type.toBe>() + expect(pipe(hole>>(), Chunk.flatten)).type.toBe>() + + expect(Chunk.flatten(hole>>())).type.toBe>() + expect(pipe(hole>>(), Chunk.flatten)).type.toBe>() + + expect(Chunk.flatten(hole>>())) + .type.toBe>() + expect(pipe(hole>>(), Chunk.flatten)) + .type.toBe>() + + const nestedChunk = hole>, never, never>>() + const nestedNonEmptyChunk = hole>, never, never>>() + + expect(nestedChunk.pipe(Effect.map((x) => { + expect(x).type.toBe>>() + return Chunk.flatten(x) + }))).type.toBe< + Effect.Effect, never, never> + >() + expect(nestedChunk.pipe(Effect.map(Chunk.flatten))).type.toBe< + Effect.Effect, never, never> + >() + expect(nestedNonEmptyChunk.pipe(Effect.map((x) => { + expect(x).type.toBe>>() + return Chunk.flatten(x) + }))).type.toBe< + Effect.Effect, never, never> + >() + expect(nestedNonEmptyChunk.pipe(Effect.map(Chunk.flatten))).type.toBe< + Effect.Effect, never, never> + >() + }) + + it("reverse", () => { + // Chunk + expect(Chunk.reverse(numbers)).type.toBe>() + expect(pipe(numbers, Chunk.reverse)).type.toBe>() + + // NonEmptyChunk + expect(Chunk.reverse(nonEmptyNumbers)).type.toBe>() + expect(pipe(nonEmptyNumbers, Chunk.reverse)).type.toBe>() + }) + + it("toArray", () => { + // Chunk + expect(Chunk.toArray(hole>())).type.toBe>() + expect(pipe(hole>(), Chunk.toArray)).type.toBe>() + + // NonEmptyChunk + expect(Chunk.toArray(hole>())).type.toBe<[string, ...Array]>() + expect(pipe(hole>(), Chunk.toArray)).type.toBe<[string, ...Array]>() + }) + + it("toReadonlyArray", () => { + // Chunk + expect(Chunk.toReadonlyArray(hole>())).type.toBe>() + expect(pipe(hole>(), Chunk.toReadonlyArray)).type.toBe>() + + // NonEmptyChunk + expect(Chunk.toReadonlyArray(hole>())).type.toBe< + readonly [string, ...Array] + >() + expect(pipe(hole>(), Chunk.toReadonlyArray)).type.toBe< + readonly [string, ...Array] + >() + }) +}) diff --git a/repos/effect/packages/effect/dtslint/Config.tst.ts b/repos/effect/packages/effect/dtslint/Config.tst.ts new file mode 100644 index 0000000..53aba1d --- /dev/null +++ b/repos/effect/packages/effect/dtslint/Config.tst.ts @@ -0,0 +1,68 @@ +import { Brand, Config, hole, pipe } from "effect" +import { describe, expect, it, when } from "tstyche" + +declare const string: Config.Config +declare const number: Config.Config +declare const array: Array> +declare const record: Record> + +type Int = Brand.Branded +const Int = Brand.refined( + (n) => Number.isInteger(n), + (n) => Brand.error(`Expected ${n} to be an integer`) +) + +type Str = Brand.Branded +const Str = Brand.refined( + (n) => n.length > 2, + (n) => Brand.error(`Expected "${n}" to be longer than 2`) +) + +describe("Config", () => { + describe("all", () => { + it("tuple", () => { + expect(Config.all([string, number])).type.toBe>() + expect(pipe([string, number] as const, Config.all)).type.toBe>() + }) + + it("struct", () => { + expect(Config.all({ a: string, b: number })).type.toBe>() + expect(pipe({ a: string, b: number }, Config.all)).type.toBe>() + }) + + it("array", () => { + expect(Config.all(array)).type.toBe>>() + expect(pipe(array, Config.all)).type.toBe>>() + }) + + it("record", () => { + expect(Config.all(record)).type.toBe>>() + expect(pipe(record, Config.all)).type.toBe>>() + }) + }) + + it("branded", () => { + expect(Config.branded).type.not.toBeCallableWith("NAME", Int) + expect(Config.branded).type.not.toBeCallableWith(number, Str) + when(number.pipe).isCalledWith(expect(Config.branded).type.not.toBeCallableWith(Str)) + + expect(Config.branded(number, Int)).type.toBe>() + expect(Config.branded("NAME", Str)).type.toBe>() + expect(number.pipe(Config.branded(Int))).type.toBe>() + expect(pipe([string, number] as const, Config.all)).type.toBe>() + }) + + it("orElseIf", () => { + expect(Config.orElseIf(number, { + if: hole(), + orElse: () => string + })).type.toBe>() + }) + + it("Config.Success helper type", () => { + expect(hole>()).type.toBe() + expect(hole>()).type.toBe() + const _config = Config.all({ a: string, b: number }) + expect(hole>()).type.toBe<{ a: string; b: number }>() + }) +}) diff --git a/repos/effect/packages/effect/dtslint/ConfigProvider.tst.ts b/repos/effect/packages/effect/dtslint/ConfigProvider.tst.ts new file mode 100644 index 0000000..c9f4c86 --- /dev/null +++ b/repos/effect/packages/effect/dtslint/ConfigProvider.tst.ts @@ -0,0 +1,11 @@ +import { ConfigProvider } from "effect" +import { describe, expect, it } from "tstyche" + +describe("ConfigProvider", () => { + describe("fromEnv", () => { + it("should accept a partial configuration", () => { + expect(ConfigProvider.fromEnv({ pathDelim: "." })).type.toBe() + expect(ConfigProvider.fromEnv({ seqDelim: "." })).type.toBe() + }) + }) +}) diff --git a/repos/effect/packages/effect/dtslint/Context.tst.ts b/repos/effect/packages/effect/dtslint/Context.tst.ts new file mode 100644 index 0000000..bfbe82a --- /dev/null +++ b/repos/effect/packages/effect/dtslint/Context.tst.ts @@ -0,0 +1,25 @@ +import { Context, Effect } from "effect" +import { describe, expect, it, when } from "tstyche" + +describe("Context", () => { + it("`key` field", () => { + class A extends Effect.Service()("A", { succeed: { a: "value" } }) {} + expect(A.key).type.toBe<"A">() + + class B extends Context.Tag("B")() {} + expect(B.key).type.toBe<"B">() + + class C extends Context.Reference()("C", { defaultValue: () => 0 }) {} + expect(C.key).type.toBe<"C">() + }) + it("Tag with static fields", () => { + class Foo extends Context.Tag("Foo")() { + static readonly StaticField = "StaticField" + } + + when(Context.empty().pipe).isCalledWith(expect(Context.add).type.not.toBeCallableWith(Foo, 123)) + + const ctx = Context.empty().pipe(Context.add(Foo, { bar: "2" })) + expect(ctx).type.toBe>() + }) +}) diff --git a/repos/effect/packages/effect/dtslint/Data.tst.ts b/repos/effect/packages/effect/dtslint/Data.tst.ts new file mode 100644 index 0000000..f3db883 --- /dev/null +++ b/repos/effect/packages/effect/dtslint/Data.tst.ts @@ -0,0 +1,193 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import * as Data from "effect/Data" +import { describe, expect, it, pick } from "tstyche" + +declare const readonlyStruct: { readonly a: string } +declare const struct: { a: string } + +declare const readonlyArray: ReadonlyArray +declare const array: Array + +describe("Data", () => { + it("struct", () => { + // Create a readonly struct from a mutable one + const struct1 = Data.struct(struct) + expect(struct1).type.toBe<{ readonly a: string }>() + + // Create a readonly struct from a readonly one + const struct2 = Data.struct(readonlyStruct) + expect(struct2).type.toBe<{ readonly a: string }>() + }) + + it("unsafeStruct", () => { + const struct3 = Data.unsafeStruct(struct) + expect(struct3).type.toBe<{ readonly a: string }>() + + const struct4 = Data.unsafeStruct(readonlyStruct) + expect(struct4).type.toBe<{ readonly a: string }>() + }) + + it("tuple", () => { + const tuple1 = Data.tuple("a", 1) + expect(tuple1).type.toBe() + }) + + it("array", () => { + const array1 = Data.array(array) + expect(array1).type.toBe>() + + const array2 = Data.array(readonlyArray) + expect(array2).type.toBe>() + }) + + it("unsafeArray", () => { + const array3 = Data.unsafeArray(array) + expect(array3).type.toBe>() + + const array4 = Data.unsafeArray(readonlyArray) + expect(array4).type.toBe>() + }) + + it("case", () => { + interface Person { + readonly name: string + } + const makePerson = Data.case() + + expect(makePerson).type.toBe<(args: { readonly name: string }) => Person>() + + const person = makePerson({ name: "Mike" }) + + // fields should be readonly + expect(person).type.toBe<{ readonly name: string }>() + }) + + it("tagged", () => { + interface TaggedPerson { + readonly _tag: "Person" + readonly name: string + readonly optional?: string + } + const taggedPerson = Data.tagged("Person") + + expect(taggedPerson).type.toBe<(args: { readonly name: string; readonly optional?: string }) => TaggedPerson>() + }) + + it("Class", () => { + class Person extends Data.Class<{ name: string; age?: number }> {} + const person = new Person({ name: "Mike" }) + // fields should be readonly + expect(person).type.toBe<{ readonly name: string; readonly age?: number }>() + + class Void extends Data.Class {} + // void constructor + expect>().type.toBe<[args?: void]>() + }) + + it("TaggedClass", () => { + class Person extends Data.TaggedClass("Person")<{ name: string; age?: number }> {} + const person = new Person({ name: "Mike" }) + // fields should be readonly + expect(person).type.toBe<{ readonly name: string; readonly age?: number; readonly _tag: "Person" }>() + + class Void extends Data.TaggedClass("Void") {} + // void constructor + expect>().type.toBe<[args?: void]>() + }) + + it("Error", () => { + class Err extends Data.Error<{ message: string; a: number; optional?: string }> {} + const err = new Err({ message: "Oh no!", a: 1 }) + + // assignable to Error + expect().type.toBeAssignableTo() + + // non-Error fields should be readonly + expect(pick(err, "message", "a", "optional")).type.toBe< + { message: string; readonly a: number; readonly optional?: string } + >() + + class Void extends Data.Error {} + // void constructor + expect>().type.toBe<[args?: void]>() + }) + + it("TaggedError", () => { + class Err extends Data.TaggedError("Foo")<{ message?: string; a: number }> {} + // Test optional props are allowed + new Err({ a: 1 }) + + // assignable to Error + expect().type.toBeAssignableTo() + + const err = new Err({ message: "Oh no!", a: 1 }) + + // non-Error fields should be readonly + expect(pick(err, "message", "a")).type.toBe<{ message: string; readonly a: number }>() + + class Void extends Data.TaggedError("Foo") {} + // void constructor + expect>().type.toBe<[args?: void]>() + }) + + describe("TaggedEnum", () => { + it("should be able to create a tagged enum", () => { + type TE = Data.TaggedEnum<{ + A: { readonly required: string } + B: { readonly optional?: number } + }> + expect>().type.toBe< + { readonly _tag: "A"; readonly required: string } + >() + expect>().type.toBe< + { readonly _tag: "B"; readonly optional?: number } + >() + }) + + it("should raise an error if one of the variants has a _tag property", () => { + // @ts-expect-error: It looks like you're trying to create a tagged enum, but one or more of its members already has a `_tag` property. + type TE = Data.TaggedEnum<{ + A: { readonly _tag: "A" } + B: { readonly b: number } + }> + }) + }) + + describe("taggedEnum", () => { + it("should be able to create a concrete tagged enum", () => { + type TE = Data.TaggedEnum<{ + A: { readonly required: string } + B: { readonly optional?: number } + }> + + const { $is, A, B } = Data.taggedEnum() + expect>().type.toBe<[{ readonly required: string }]>() + expect>().type.toBe<{ readonly _tag: "A"; readonly required: string }>() + expect>().type.toBe<[{ readonly optional?: number }]>() + expect>().type.toBe<{ readonly _tag: "B"; readonly optional?: number }>() + const isA = $is("A") + expect(isA).type.toBe< + (u: unknown) => u is { readonly _tag: "A"; readonly required: string } + >() + const isB = $is("B") + expect(isB).type.toBe< + (u: unknown) => u is { readonly _tag: "B"; readonly optional?: number } + >() + }) + + it("should be able to create a generic tagged enum", () => { + type TE = Data.TaggedEnum<{ + A: { a: T } + B: { b?: T } + }> + + interface TEDefinition extends Data.TaggedEnum.WithGenerics<1> { + readonly taggedEnum: TE + } + + const { A, B } = Data.taggedEnum() + expect().type.toBe<((args: { readonly a: A }) => { readonly _tag: "A"; readonly a: A })>() + expect().type.toBe<((args: { readonly b?: B }) => { readonly _tag: "B"; readonly b?: B })>() + }) + }) +}) diff --git a/repos/effect/packages/effect/dtslint/DateTime.tst.ts b/repos/effect/packages/effect/dtslint/DateTime.tst.ts new file mode 100644 index 0000000..da1cbed --- /dev/null +++ b/repos/effect/packages/effect/dtslint/DateTime.tst.ts @@ -0,0 +1,42 @@ +import * as DateTime from "effect/DateTime" +import { describe, expect, it } from "tstyche" + +declare const utc: DateTime.Utc +declare const zoned: DateTime.Zoned +declare const dateTime: DateTime.DateTime + +describe("DateTime", () => { + it("min", () => { + expect(DateTime.min(utc, zoned)).type.toBe() + expect(DateTime.min(utc, utc)).type.toBe() + expect(DateTime.min(zoned, zoned)).type.toBe() + expect(DateTime.min(dateTime, zoned)).type.toBe() + expect(DateTime.min(dateTime, utc)).type.toBe() + expect(DateTime.min(dateTime, dateTime)).type.toBe() + + expect(utc.pipe(DateTime.min(zoned))).type.toBe() + expect(zoned.pipe(DateTime.min(utc))).type.toBe() + expect(utc.pipe(DateTime.min(utc))).type.toBe() + expect(zoned.pipe(DateTime.min(zoned))).type.toBe() + expect(dateTime.pipe(DateTime.min(zoned))).type.toBe() + expect(dateTime.pipe(DateTime.min(utc))).type.toBe() + expect(dateTime.pipe(DateTime.min(dateTime))).type.toBe() + }) + + it("max", () => { + expect(DateTime.max(utc, zoned)).type.toBe() + expect(DateTime.max(utc, utc)).type.toBe() + expect(DateTime.max(zoned, zoned)).type.toBe() + expect(DateTime.max(dateTime, zoned)).type.toBe() + expect(DateTime.max(dateTime, utc)).type.toBe() + expect(DateTime.max(dateTime, dateTime)).type.toBe() + + expect(utc.pipe(DateTime.max(zoned))).type.toBe() + expect(zoned.pipe(DateTime.max(utc))).type.toBe() + expect(utc.pipe(DateTime.max(utc))).type.toBe() + expect(zoned.pipe(DateTime.max(zoned))).type.toBe() + expect(dateTime.pipe(DateTime.max(zoned))).type.toBe() + expect(dateTime.pipe(DateTime.max(utc))).type.toBe() + expect(dateTime.pipe(DateTime.max(dateTime))).type.toBe() + }) +}) diff --git a/repos/effect/packages/effect/dtslint/Duration.tst.ts b/repos/effect/packages/effect/dtslint/Duration.tst.ts new file mode 100644 index 0000000..c455b70 --- /dev/null +++ b/repos/effect/packages/effect/dtslint/Duration.tst.ts @@ -0,0 +1,107 @@ +import type { Option } from "effect" +import { Duration } from "effect" +import { describe, expect, it } from "tstyche" + +describe("Duration", () => { + it("decode", () => { + expect(Duration.decode(100)).type.toBe() + expect(Duration.decode(10n)).type.toBe() + expect(Duration.decode("1 nano")).type.toBe() + expect(Duration.decode("10 nanos")).type.toBe() + expect(Duration.decode("1 micro")).type.toBe() + expect(Duration.decode("10 micros")).type.toBe() + expect(Duration.decode("1 milli")).type.toBe() + expect(Duration.decode("10 millis")).type.toBe() + expect(Duration.decode("1 second")).type.toBe() + expect(Duration.decode("10 seconds")).type.toBe() + expect(Duration.decode("1 minute")).type.toBe() + expect(Duration.decode("10 minutes")).type.toBe() + expect(Duration.decode("1 hour")).type.toBe() + expect(Duration.decode("10 hours")).type.toBe() + expect(Duration.decode("1 day")).type.toBe() + expect(Duration.decode("10 days")).type.toBe() + + expect(Duration.decode).type.not.toBeCallableWith("10 unknown") + }) + + it("toMillis", () => { + expect(Duration.toMillis("1 millis")).type.toBe() + }) + + it("toNanos", () => { + expect(Duration.toNanos("1 millis")).type.toBe>() + }) + + it("unsafeToNanos", () => { + expect(Duration.unsafeToNanos("1 millis")).type.toBe() + }) + + it("toHrTime", () => { + expect(Duration.toHrTime("1 millis")).type.toBe<[seconds: number, nanos: number]>() + }) + + it("match", () => { + expect(Duration.match("100 millis", { + onMillis: (n) => { + expect(n).type.toBe() + return "millis" + }, + onNanos: (bi) => { + expect(bi).type.toBe() + return "nanos" + } + })).type.toBe() + }) + + it("between", () => { + expect(Duration.between("1 minutes", { minimum: "59 seconds", maximum: "61 seconds" })).type.toBe() + }) + + it("min", () => { + expect(Duration.min("1 minutes", "2 millis")).type.toBe() + }) + + it("max", () => { + expect(Duration.max("1 minutes", "2 millis")).type.toBe() + }) + + it("clamp", () => { + expect(Duration.clamp("1 millis", { minimum: "2 millis", maximum: "3 millis" })).type.toBe() + }) + + it("divide", () => { + expect(Duration.divide("1 seconds", 2)).type.toBe>() + }) + + it("unsafeDivide", () => { + expect(Duration.unsafeDivide("1 seconds", 2)).type.toBe() + }) + + it("times", () => { + expect(Duration.times("1 seconds", 60)).type.toBe() + }) + + it("sum", () => { + expect(Duration.sum("30 seconds", "30 seconds")).type.toBe() + }) + + it("greaterThanOrEqualTo", () => { + expect(Duration.greaterThanOrEqualTo("2 seconds", "2 seconds")).type.toBe() + }) + + it("greaterThan", () => { + expect(Duration.greaterThan("2 seconds", "2 seconds")).type.toBe() + }) + + it("lessThanOrEqualTo", () => { + expect(Duration.lessThanOrEqualTo("2 seconds", "2 seconds")).type.toBe() + }) + + it("lessThan", () => { + expect(Duration.lessThan("2 seconds", "2 seconds")).type.toBe() + }) + + it("equals", () => { + expect(Duration.equals("2 seconds", "2 seconds")).type.toBe() + }) +}) diff --git a/repos/effect/packages/effect/dtslint/Effect.tst.ts b/repos/effect/packages/effect/dtslint/Effect.tst.ts new file mode 100644 index 0000000..4c9f794 --- /dev/null +++ b/repos/effect/packages/effect/dtslint/Effect.tst.ts @@ -0,0 +1,1551 @@ +import type { Either, Types } from "effect" +import { Array as Arr, Context, Effect, hole, Option, pipe, Predicate, Schedule } from "effect" +import type { NonEmptyArray, NonEmptyReadonlyArray } from "effect/Array" +import type { Cause, NoSuchElementException, UnknownException } from "effect/Cause" +import type { Exit } from "effect/Exit" +import { describe, expect, it, when } from "tstyche" + +class TestError1 { + readonly _tag = "TestError1" +} +class TestError2 { + readonly _tag = "TestError2" +} + +class TestService extends Context.Tag("TestService")() {} + +declare const string: Effect.Effect +declare const number: Effect.Effect +declare const boolean: Effect.Effect +declare const stringArray: Array> +declare const numberRecord: Record> + +declare const numberArray: Array +declare const numberEffectIterable: Array> + +declare const readonlyNonEmptyStrings: NonEmptyReadonlyArray +declare const strings: Array +declare const numbersArray: Array +declare const predicateNumbersOrStringsEffect: (input: number | string) => Effect.Effect +declare const primitiveNumber: number +declare const primitiveNumberOrString: string | number +declare const predicateNumbersOrStrings: Predicate.Predicate + +// Tacit helpers +const tacitString = (s: string): Effect.Effect => Effect.succeed(`string ${s}`) +const tacitStringCause = (s: Cause): Effect.Effect => Effect.succeed(`string ${s}`) +const tacitStringPredicate = (_s: string): boolean => true +const tacitStringError = (_s: string): "a" => "a" +const tacitStringErrorEffect = (_s: string): Effect.Effect => Effect.fail("a") + +describe("Effect", () => { + describe("forEach", () => { + it("array", () => { + expect(Effect.forEach(strings, (a, i) => { + expect(a).type.toBe() + expect(i).type.toBe() + return string + })).type.toBe, "err-1", "dep-1">>() + expect(pipe( + strings, + Effect.forEach((a, i) => { + expect(a).type.toBe() + expect(i).type.toBe() + return string + }) + )).type.toBe, "err-1", "dep-1">>() + + expect(Effect.forEach(strings, (a, i) => { + expect(a).type.toBe() + expect(i).type.toBe() + return string + }, { discard: true })).type.toBe>() + expect(pipe( + strings, + Effect.forEach((a, i) => { + expect(a).type.toBe() + expect(i).type.toBe() + return string + }, { discard: true }) + )).type.toBe>() + }) + + it("non empty array", () => { + expect(Effect.forEach(readonlyNonEmptyStrings, (a, i) => { + expect(a).type.toBe() + expect(i).type.toBe() + return string + })).type.toBe], "err-1", "dep-1">>() + expect(pipe( + readonlyNonEmptyStrings, + Effect.forEach((a, i) => { + expect(a).type.toBe() + expect(i).type.toBe() + return string + }) + )).type.toBe], "err-1", "dep-1">>() + + expect(Effect.forEach(readonlyNonEmptyStrings, (a, i) => { + expect(a).type.toBe() + expect(i).type.toBe() + return string + }, { discard: true })).type.toBe>() + expect(pipe( + readonlyNonEmptyStrings, + Effect.forEach((a, i) => { + expect(a).type.toBe() + expect(i).type.toBe() + return string + }, { discard: true }) + )).type.toBe>() + }) + + it("tuple as non empty array", () => { + const tuple = ["a", "b"] as const + expect(Effect.forEach(tuple, (a, i) => { + expect(a).type.toBe<"a" | "b">() + expect(i).type.toBe() + return string + })).type.toBe], "err-1", "dep-1">>() + expect(pipe( + tuple, + Effect.forEach((a, i) => { + expect(a).type.toBe<"a" | "b">() + expect(i).type.toBe() + return string + }) + )).type.toBe], "err-1", "dep-1">>() + + expect(Effect.forEach(tuple, (a, i) => { + expect(a).type.toBe<"a" | "b">() + expect(i).type.toBe() + return string + }, { discard: true })).type.toBe>() + expect(pipe( + tuple, + Effect.forEach((a, i) => { + expect(a).type.toBe<"a" | "b">() + expect(i).type.toBe() + return string + }, { discard: true }) + )).type.toBe>() + }) + }) + + describe("all", () => { + it("tuple", () => { + expect(Effect.all([string, number])).type.toBe< + Effect.Effect<[string, number], "err-1" | "err-2", "dep-1" | "dep-2"> + >() + expect(Effect.all([string, number], undefined)).type.toBe< + Effect.Effect<[string, number], "err-1" | "err-2", "dep-1" | "dep-2"> + >() + expect(Effect.all([string, number], {})).type.toBe< + Effect.Effect<[string, number], "err-1" | "err-2", "dep-1" | "dep-2"> + >() + expect(Effect.all([string, number], { concurrency: "unbounded" })).type.toBe< + Effect.Effect<[string, number], "err-1" | "err-2", "dep-1" | "dep-2"> + >() + expect(Effect.all([string, number], { discard: true })).type.toBe< + Effect.Effect + >() + expect(Effect.all([string, number], { discard: true, concurrency: "unbounded" })).type.toBe< + Effect.Effect + >() + expect(Effect.all([string, number], { mode: "validate" })).type.toBe< + Effect.Effect<[string, number], [Option.Option<"err-1">, Option.Option<"err-2">], "dep-1" | "dep-2"> + >() + expect(Effect.all([string, number], { mode: "validate", discard: true })).type.toBe< + Effect.Effect, Option.Option<"err-2">], "dep-1" | "dep-2"> + >() + expect(Effect.all([string, number], { mode: "either" })).type.toBe< + Effect.Effect<[Either.Either, Either.Either], never, "dep-1" | "dep-2"> + >() + expect(Effect.all([string, number], { mode: "either", discard: true })).type.toBe< + Effect.Effect + >() + }) + + it("struct", () => { + expect(Effect.all({ a: string, b: number })).type.toBe< + Effect.Effect<{ a: string; b: number }, "err-1" | "err-2", "dep-1" | "dep-2"> + >() + expect(Effect.all({ a: string, b: number }, undefined)).type.toBe< + Effect.Effect<{ a: string; b: number }, "err-1" | "err-2", "dep-1" | "dep-2"> + >() + expect(Effect.all({ a: string, b: number }, {})).type.toBe< + Effect.Effect<{ a: string; b: number }, "err-1" | "err-2", "dep-1" | "dep-2"> + >() + expect(Effect.all({ a: string, b: number }, { concurrency: "unbounded" })).type.toBe< + Effect.Effect<{ a: string; b: number }, "err-1" | "err-2", "dep-1" | "dep-2"> + >() + expect(Effect.all({ a: string, b: number }, { discard: true })).type.toBe< + Effect.Effect + >() + expect(Effect.all({ a: string, b: number }, { discard: true, concurrency: "unbounded" })).type.toBe< + Effect.Effect + >() + expect(Effect.all({ a: string, b: number }, { mode: "validate" })).type.toBe< + Effect.Effect< + { a: string; b: number }, + { a: Option.Option<"err-1">; b: Option.Option<"err-2"> }, + "dep-1" | "dep-2" + > + >() + expect(Effect.all({ a: string, b: number }, { mode: "validate", discard: true })).type.toBe< + Effect.Effect; b: Option.Option<"err-2"> }, "dep-1" | "dep-2"> + >() + expect(Effect.all({ a: string, b: number }, { mode: "either" })).type.toBe< + Effect.Effect< + { a: Either.Either; b: Either.Either }, + never, + "dep-1" | "dep-2" + > + >() + expect(Effect.all({ a: string, b: number }, { mode: "either", discard: true })).type.toBe< + Effect.Effect + >() + }) + + it("array", () => { + expect(Effect.all(stringArray)).type.toBe< + Effect.Effect, "err-3", "dep-3"> + >() + expect(Effect.all(stringArray, undefined)).type.toBe< + Effect.Effect, "err-3", "dep-3"> + >() + expect(Effect.all(stringArray, {})).type.toBe< + Effect.Effect, "err-3", "dep-3"> + >() + expect(Effect.all(stringArray, { concurrency: "unbounded" })).type.toBe< + Effect.Effect, "err-3", "dep-3"> + >() + expect(Effect.all(stringArray, { discard: true })).type.toBe< + Effect.Effect + >() + expect(Effect.all(stringArray, { discard: true, concurrency: "unbounded" })).type.toBe< + Effect.Effect + >() + expect(Effect.all(stringArray, { mode: "validate" })).type.toBe< + Effect.Effect, Array>, "dep-3"> + >() + expect(Effect.all(stringArray, { mode: "validate", discard: true })).type.toBe< + Effect.Effect>, "dep-3"> + >() + expect(Effect.all(stringArray, { mode: "either" })).type.toBe< + Effect.Effect>, never, "dep-3"> + >() + expect(Effect.all(stringArray, { mode: "either", discard: true })).type.toBe< + Effect.Effect + >() + }) + + it("record", () => { + expect(Effect.all(numberRecord)).type.toBe< + Effect.Effect<{ [x: string]: number }, "err-4", "dep-4"> + >() + expect(Effect.all(numberRecord, undefined)).type.toBe< + Effect.Effect<{ [x: string]: number }, "err-4", "dep-4"> + >() + expect(Effect.all(numberRecord, {})).type.toBe< + Effect.Effect<{ [x: string]: number }, "err-4", "dep-4"> + >() + expect(Effect.all(numberRecord, { concurrency: "unbounded" })).type.toBe< + Effect.Effect<{ [x: string]: number }, "err-4", "dep-4"> + >() + expect(Effect.all(numberRecord, { discard: true })).type.toBe< + Effect.Effect + >() + expect(Effect.all(numberRecord, { discard: true, concurrency: "unbounded" })).type.toBe< + Effect.Effect + >() + expect(Effect.all(numberRecord, { mode: "validate" })).type.toBe< + Effect.Effect<{ [x: string]: number }, { [x: string]: Option.Option<"err-4"> }, "dep-4"> + >() + expect(Effect.all(numberRecord, { mode: "validate", discard: true })).type.toBe< + Effect.Effect }, "dep-4"> + >() + expect(Effect.all(numberRecord, { mode: "either" })).type.toBe< + Effect.Effect<{ [x: string]: Either.Either }, never, "dep-4"> + >() + expect(Effect.all(numberRecord, { mode: "either", discard: true })).type.toBe< + Effect.Effect + >() + }) + }) + + describe("allWith", () => { + it("tuple", () => { + expect(pipe([string, number] as const, Effect.allWith())).type.toBe< + Effect.Effect<[string, number], "err-1" | "err-2", "dep-1" | "dep-2"> + >() + expect(pipe([string, number] as const, Effect.allWith(undefined))).type.toBe< + Effect.Effect<[string, number], "err-1" | "err-2", "dep-1" | "dep-2"> + >() + expect(pipe([string, number] as const, Effect.allWith({}))).type.toBe< + Effect.Effect<[string, number], "err-1" | "err-2", "dep-1" | "dep-2"> + >() + expect(pipe([string, number] as const, Effect.allWith({ concurrency: "unbounded" }))).type.toBe< + Effect.Effect<[string, number], "err-1" | "err-2", "dep-1" | "dep-2"> + >() + expect(pipe([string, number] as const, Effect.allWith({ discard: true }))).type.toBe< + Effect.Effect + >() + expect(pipe([string, number] as const, Effect.allWith({ discard: true, concurrency: "unbounded" }))).type.toBe< + Effect.Effect + >() + expect(pipe([string, number] as const, Effect.allWith({ mode: "validate" }))).type.toBe< + Effect.Effect<[string, number], [Option.Option<"err-1">, Option.Option<"err-2">], "dep-1" | "dep-2"> + >() + expect(pipe([string, number] as const, Effect.allWith({ mode: "validate", discard: true }))).type.toBe< + Effect.Effect, Option.Option<"err-2">], "dep-1" | "dep-2"> + >() + expect(pipe([string, number] as const, Effect.allWith({ mode: "either" }))).type.toBe< + Effect.Effect<[Either.Either, Either.Either], never, "dep-1" | "dep-2"> + >() + expect(pipe([string, number] as const, Effect.allWith({ mode: "either", discard: true }))).type.toBe< + Effect.Effect + >() + }) + + it("struct", () => { + expect(pipe({ a: string, b: number }, Effect.allWith())).type.toBe< + Effect.Effect<{ a: string; b: number }, "err-1" | "err-2", "dep-1" | "dep-2"> + >() + expect(pipe({ a: string, b: number }, Effect.allWith(undefined))).type.toBe< + Effect.Effect<{ a: string; b: number }, "err-1" | "err-2", "dep-1" | "dep-2"> + >() + expect(pipe({ a: string, b: number }, Effect.allWith({}))).type.toBe< + Effect.Effect<{ a: string; b: number }, "err-1" | "err-2", "dep-1" | "dep-2"> + >() + expect(pipe({ a: string, b: number }, Effect.allWith({ concurrency: "unbounded" }))).type.toBe< + Effect.Effect<{ a: string; b: number }, "err-1" | "err-2", "dep-1" | "dep-2"> + >() + expect(pipe({ a: string, b: number }, Effect.allWith({ discard: true }))).type.toBe< + Effect.Effect + >() + expect(pipe({ a: string, b: number }, Effect.allWith({ discard: true, concurrency: "unbounded" }))).type.toBe< + Effect.Effect + >() + expect(pipe({ a: string, b: number }, Effect.allWith({ mode: "validate" }))).type.toBe< + Effect.Effect< + { a: string; b: number }, + { a: Option.Option<"err-1">; b: Option.Option<"err-2"> }, + "dep-1" | "dep-2" + > + >() + expect(pipe({ a: string, b: number }, Effect.allWith({ mode: "validate", discard: true }))).type.toBe< + Effect.Effect; b: Option.Option<"err-2"> }, "dep-1" | "dep-2"> + >() + expect(pipe({ a: string, b: number }, Effect.allWith({ mode: "either" }))).type.toBe< + Effect.Effect< + { a: Either.Either; b: Either.Either }, + never, + "dep-1" | "dep-2" + > + >() + expect(pipe({ a: string, b: number }, Effect.allWith({ mode: "either", discard: true }))).type.toBe< + Effect.Effect + >() + }) + + it("array", () => { + expect(pipe(stringArray, Effect.allWith())).type.toBe< + Effect.Effect, "err-3", "dep-3"> + >() + expect(pipe(stringArray, Effect.allWith(undefined))).type.toBe< + Effect.Effect, "err-3", "dep-3"> + >() + expect(pipe(stringArray, Effect.allWith({}))).type.toBe< + Effect.Effect, "err-3", "dep-3"> + >() + expect(pipe(stringArray, Effect.allWith({ concurrency: "unbounded" }))).type.toBe< + Effect.Effect, "err-3", "dep-3"> + >() + expect(pipe(stringArray, Effect.allWith({ discard: true }))).type.toBe< + Effect.Effect + >() + expect(pipe(stringArray, Effect.allWith({ discard: true, concurrency: "unbounded" }))).type.toBe< + Effect.Effect + >() + expect(pipe(stringArray, Effect.allWith({ mode: "validate" }))).type.toBe< + Effect.Effect, Array>, "dep-3"> + >() + expect(pipe(stringArray, Effect.allWith({ mode: "validate", discard: true }))).type.toBe< + Effect.Effect>, "dep-3"> + >() + expect(pipe(stringArray, Effect.allWith({ mode: "either" }))).type.toBe< + Effect.Effect>, never, "dep-3"> + >() + expect(pipe(stringArray, Effect.allWith({ mode: "either", discard: true }))).type.toBe< + Effect.Effect + >() + }) + + it("record", () => { + expect(pipe(numberRecord, Effect.allWith())).type.toBe< + Effect.Effect<{ [x: string]: number }, "err-4", "dep-4"> + >() + expect(pipe(numberRecord, Effect.allWith(undefined))).type.toBe< + Effect.Effect<{ [x: string]: number }, "err-4", "dep-4"> + >() + expect(pipe(numberRecord, Effect.allWith({}))).type.toBe< + Effect.Effect<{ [x: string]: number }, "err-4", "dep-4"> + >() + expect(pipe(numberRecord, Effect.allWith({ concurrency: "unbounded" }))).type.toBe< + Effect.Effect<{ [x: string]: number }, "err-4", "dep-4"> + >() + expect(pipe(numberRecord, Effect.allWith({ discard: true }))).type.toBe< + Effect.Effect + >() + expect(pipe(numberRecord, Effect.allWith({ discard: true, concurrency: "unbounded" }))).type.toBe< + Effect.Effect + >() + expect(pipe(numberRecord, Effect.allWith({ mode: "validate" }))).type.toBe< + Effect.Effect<{ [x: string]: number }, { [x: string]: Option.Option<"err-4"> }, "dep-4"> + >() + expect(pipe(numberRecord, Effect.allWith({ mode: "validate", discard: true }))).type.toBe< + Effect.Effect }, "dep-4"> + >() + expect(pipe(numberRecord, Effect.allWith({ mode: "either" }))).type.toBe< + Effect.Effect<{ [x: string]: Either.Either }, never, "dep-4"> + >() + expect(pipe(numberRecord, Effect.allWith({ mode: "either", discard: true }))).type.toBe< + Effect.Effect + >() + }) + }) + + it("filterOrFail", () => { + expect( + Effect.succeed("a" as const).pipe( + Effect.filterOrFail( + tacitStringPredicate, + (x) => { + expect(x).type.toBe<"a">() + return "a" as const + } + ) + ) + ).type.toBe>() + expect( + Effect.succeed("a" as const).pipe( + Effect.filterOrFail( + (x) => { + expect(x).type.toBe<"a">() + return true + }, + tacitStringError + ) + ) + ).type.toBe>() + expect( + Effect.succeed<"a" | "b">("a").pipe( + Effect.filterOrFail( + (s): s is "a" => s === "a", + (x) => { + expect(x).type.toBe<"b">() + return "a" as const + } + ) + ) + ).type.toBe>() + }) + + it("filterOrDie", () => { + expect( + Effect.succeed("a" as const).pipe( + Effect.filterOrDie( + tacitStringPredicate, + (x) => { + expect(x).type.toBe<"a">() + return "fail" + } + ) + ) + ).type.toBe>() + + expect( + Effect.succeed<"a" | "b">("a").pipe( + Effect.filterOrDie( + (s): s is "a" => s === "a", + (x) => { + expect(x).type.toBe<"b">() + return "fail" + } + ) + ) + ).type.toBe>() + }) + + it("filterOrDieMessage", () => { + expect( + Effect.succeed("a" as const).pipe( + Effect.filterOrDieMessage( + tacitStringPredicate, + "fail" + ) + ) + ).type.toBe>() + }) + + it("filterOrElse", () => { + expect( + Effect.succeed("a" as const).pipe( + Effect.filterOrElse( + tacitStringPredicate, + (x) => { + expect(x).type.toBe<"a">() + return Effect.fail("a" as const) + } + ) + ) + ).type.toBe>() + expect( + Effect.succeed("a" as const).pipe( + Effect.filterOrElse( + (x) => { + expect(x).type.toBe<"a">() + return true + }, + tacitStringErrorEffect + ) + ) + ).type.toBe>() + + expect( + Effect.succeed(numberArray).pipe( + Effect.filterOrElse( + Arr.isNonEmptyArray, + () => Effect.fail("a" as const) + ) + ) + ).type.toBe, "a">>() + }) + + it("tap", () => { + when(Effect.succeed("a" as const).pipe).isCalledWith( + expect(Effect.tap).type.not.toBeCallableWith(tacitStringError, { onlyEffect: true }) + ) + when(Effect.succeed("a" as const).pipe).isCalledWith( + expect(Effect.tap).type.not.toBeCallableWith("a", { onlyEffect: true }) + ) + + expect(Effect.succeed("a" as const).pipe(Effect.tap(tacitString))).type.toBe>() + + expect(Effect.succeed("a" as const).pipe(Effect.tap(tacitString, { onlyEffect: true }))) + .type.toBe>() + + expect(Effect.succeed("a" as const).pipe(Effect.tap(tacitString("a"), { onlyEffect: true }))) + .type.toBe>() + }) + + it("tapError", () => { + expect( + Effect.fail("a" as const).pipe(Effect.tapError(tacitString)) + ).type.toBe>() + }) + + it("tapErrorCause", () => { + expect( + Effect.fail("a" as const).pipe(Effect.tapErrorCause(tacitStringCause)) + ).type.toBe>() + }) + + it("tapDefect", () => { + expect( + Effect.fail("a" as const).pipe(Effect.tapDefect(tacitStringCause)) + ).type.toBe>() + }) + + it("tapBoth", () => { + expect(pipe( + Effect.succeed("a" as const) as Effect.Effect<"a", "a">, + Effect.tapBoth({ + onFailure: tacitString, + onSuccess: tacitString + }) + )).type.toBe>() + }) + + it("zip", () => { + expect(Effect.zip(Effect.succeed(1), Effect.succeed("a"))).type.toBe< + Effect.Effect<[number, string]> + >() + }) + + it("validate", () => { + expect(Effect.validate(Effect.succeed(1), Effect.succeed("a"))).type.toBe< + Effect.Effect<[number, string]> + >() + }) + + it("promise", () => { + expect(Effect.promise( + () => + new Promise((resolve) => { + setTimeout(() => { + resolve("Async operation completed successfully!") + }, 2000) + }) + )).type.toBe>() + }) + + it("tapErrorTag", () => { + class TestError1 { + readonly _tag = "TestError1" + } + class TestError2 { + readonly _tag = "TestError2" + } + + expect(pipe( + Effect.fail(new TestError1()), + Effect.tapErrorTag("TestError1", (x) => { + expect(x).type.toBe() + return Effect.succeed(1) + }) + )).type.toBe>() + + expect(pipe( + Effect.fail(new TestError1()), + Effect.tapErrorTag("TestError1", (x) => { + expect(x).type.toBe() + return Effect.fail(new Error("")) + }) + )).type.toBe>() + + expect(pipe( + Effect.fail(new TestError1()), + Effect.tapErrorTag("TestError1", (x) => { + expect(x).type.toBe() + return Effect.succeed(1) + }) + )).type.toBe>() + + expect( + hole>().pipe( + Effect.tapErrorTag("TestError1", Effect.log) + ) + ).type.toBe>() + }) + + it("catchIf", () => { + expect(pipe( + Effect.fail(new TestError1()), + Effect.catchIf( + (error) => { + expect(error).type.toBe() + return true + }, + Effect.succeed + ), + Effect.exit + )).type.toBe, never, never>>() + }) + + it("catchTag", () => { + expect(Effect.catchTag).type.not.toBeCallableWith( + hole>(), + "wrong", + () => Effect.succeed(1) + ) + when(pipe).isCalledWith( + hole>(), + expect(Effect.catchTag).type.not.toBeCallableWith("wrong", () => Effect.succeed(1)) + ) + + expect(Effect.catchTag).type.not.toBeCallableWith( + hole>(), + "wrong", + () => Effect.succeed(1) + ) + when(pipe).isCalledWith( + hole>(), + expect(Effect.catchTag).type.not.toBeCallableWith("wrong", () => Effect.succeed(1)) + ) + + expect(Effect.catchTag( + hole>(), + "TestError1", + (e) => { + expect(e).type.toBe() + return Effect.succeed(1) + } + )).type.toBe>() + + expect( + Effect.catchTag(hole>(), "TestError1", (e) => { + expect(e).type.toBe() + return Effect.succeed(1) + }) + ).type.toBe< + Effect.Effect + >() + + expect( + hole>().pipe( + Effect.catchTag("TestError1", Effect.log) + ) + ).type.toBe>() + + expect(pipe( + hole>(), + Effect.catchTag("TestError1", (e) => { + expect(e).type.toBe() + return Effect.succeed(1) + }) + )).type.toBe>() + + expect(Effect.catchTag(hole>(), "TestError1", Effect.succeed)).type + .toBe>() + + expect(pipe( + hole>(), + Effect.catchTag("TestError1", (e) => { + expect(e).type.toBe() + return Effect.succeed(1) + }) + )).type.toBe>() + + expect( + Effect.catchTag( + hole>(), + "TestError1", + (e) => { + expect(e).type.toBe() + return Effect.succeed(1) + } + ) + ).type.toBe>() + + expect(pipe( + hole>(), + Effect.catchTag("TestError1", (e) => { + expect(e).type.toBe() + return Effect.succeed(1) + }) + )).type.toBe>() + + expect(pipe( + hole>(), + Effect.catchTag( + "TestError1", + Effect.fn(function*(e) { + expect(e).type.toBe() + return 1 + }) + ) + )).type.toBe>() + }) + + it("catchTags", () => { + expect(pipe( + Effect.fail(new Error()), + Effect.catchTags({ + TestError1: (e) => { + expect(e).type.toBe() + return Effect.succeed(1) + } + }) + )).type.toBe>() + + when(pipe).isCalledWith( + Effect.fail(new TestError1()), + expect(Effect.catchTags).type.not.toBeCallableWith({ + TestError1: () => Effect.succeed(1), + Other: () => Effect.succeed(1) + }) + ) + + expect(Effect.catchTags).type.not.toBeCallableWith(Effect.fail(new TestError1()), { + TestError1: () => Effect.succeed(1), + Other: () => Effect.succeed(1) + }) + + when(pipe).isCalledWith( + Effect.fail(new TestError1() as TestError1 | string), + expect(Effect.catchTags).type.not.toBeCallableWith({ + TestError1: () => Effect.succeed(1), + Other: () => Effect.succeed(1) + }) + ) + + expect(Effect.catchTags).type.not.toBeCallableWith(Effect.fail(new TestError1() as TestError1 | string), { + TestError1: () => Effect.succeed(1), + Other: () => Effect.succeed(1) + }) + + expect(pipe( + Effect.fail(new TestError1() as unknown), + Effect.catchTags({ + TestError1: () => Effect.succeed(1) + }) + )).type.toBe>() + + expect(Effect.catchTags(Effect.fail(new TestError1() as unknown), { + TestError1: () => Effect.succeed(1) + })).type.toBe>() + }) + + it("iterate", () => { + // predicate + expect(Effect.iterate(100, { + while: (n) => { + expect(n).type.toBe() + return n > 0 + }, + body: (n) => { + expect(n).type.toBe() + return Effect.succeed(n - 1) + } + })).type.toBe>() + + // refinement + expect(Effect.iterate(100 as null | number, { + while: (n): n is number => { + expect(n).type.toBe() + return Predicate.isNotNull(n) && n > 0 + }, + body: (n) => { + expect(n).type.toBe() + return Effect.succeed(n - 1) + } + })).type.toBe>() + }) + + it("loop", () => { + // predicate + expect(Effect.loop(0, { + while: (n) => { + expect(n).type.toBe() + return n < 5 + }, + step: (n) => { + expect(n).type.toBe() + return n + 1 + }, + body: (n) => { + expect(n).type.toBe() + return Effect.succeed(n * 2) + } + })).type.toBe>>() + + expect(Effect.loop(0, { + while: (n) => { + expect(n).type.toBe() + return n < 5 + }, + step: (n) => { + expect(n).type.toBe() + return n + 1 + }, + body: (n) => { + expect(n).type.toBe() + return Effect.succeed(n * 2) + }, + discard: true + })).type.toBe>() + + // refinement + expect(Effect.loop(0 as null | number, { + while: (n): n is number => { + expect(n).type.toBe() + return Predicate.isNotNull(n) && n < 5 + }, + step: (n) => { + expect(n).type.toBe() + return n + 1 + }, + body: (n) => { + expect(n).type.toBe() + return Effect.succeed(n * 2) + } + })).type.toBe>>() + + expect(Effect.loop(0 as null | number, { + while: (n): n is number => { + expect(n).type.toBe() + return Predicate.isNotNull(n) && n < 5 + }, + step: (n) => { + expect(n).type.toBe() + return n + 1 + }, + body: (n) => { + expect(n).type.toBe() + return Effect.succeed(n * 2) + }, + discard: true + })).type.toBe>() + }) + + it("dropWhile", () => { + expect(Effect.dropWhile(numbersArray, (n) => { + expect(n).type.toBe() + return Effect.succeed(true) + })).type.toBe>>() + + expect(pipe( + numbersArray, + Effect.dropWhile((n) => { + expect(n).type.toBe() + return Effect.succeed(true) + }) + )).type.toBe>>() + + expect(pipe( + numbersArray, + Effect.dropWhile((n) => { + expect(n).type.toBe() + return Effect.succeed(true) + }) + )).type.toBe>>() + + expect(Effect.dropWhile(numbersArray, predicateNumbersOrStringsEffect)).type.toBe< + Effect.Effect> + >() + expect(pipe(numbersArray, Effect.dropWhile(predicateNumbersOrStringsEffect))).type.toBe< + Effect.Effect> + >() + }) + + it("dropUntil", () => { + expect(Effect.dropUntil(numbersArray, (n) => { + expect(n).type.toBe() + return Effect.succeed(true) + })).type.toBe< + Effect.Effect> + >() + + expect(pipe( + numbersArray, + Effect.dropUntil((n) => { + expect(n).type.toBe() + return Effect.succeed(true) + }) + )).type.toBe>>() + + expect(pipe( + numbersArray, + Effect.dropUntil((n) => { + expect(n).type.toBe() + return Effect.succeed(true) + }) + )).type.toBe>>() + + expect(Effect.dropUntil(numbersArray, predicateNumbersOrStringsEffect)).type.toBe< + Effect.Effect> + >() + expect(pipe(numbersArray, Effect.dropUntil(predicateNumbersOrStringsEffect))).type.toBe< + Effect.Effect> + >() + }) + + it("andThen", () => { + expect(Effect.andThen(string, number)).type.toBe< + Effect.Effect + >() + expect(string.pipe(Effect.andThen(number))).type.toBe< + Effect.Effect + >() + + expect(Effect.andThen(string, () => number)).type.toBe< + Effect.Effect + >() + expect(string.pipe(Effect.andThen(() => number))).type.toBe< + Effect.Effect + >() + + expect(Effect.andThen(string, Promise.resolve(123))).type.toBe< + Effect.Effect + >() + expect(string.pipe(Effect.andThen(Promise.resolve(123)))).type.toBe< + Effect.Effect + >() + + expect(Effect.andThen(string, () => Promise.resolve(123))).type.toBe< + Effect.Effect + >() + expect(string.pipe(Effect.andThen(() => Promise.resolve(123)))).type.toBe< + Effect.Effect + >() + + expect(Effect.andThen(string, 1)).type.toBe< + Effect.Effect + >() + expect(string.pipe(Effect.andThen(1))).type.toBe< + Effect.Effect + >() + + expect(Effect.andThen(string, () => 1)).type.toBe< + Effect.Effect + >() + expect(string.pipe(Effect.andThen(() => 1))).type.toBe< + Effect.Effect + >() + }) + + it("retry", () => { + expect(Effect.retry(string, Schedule.forever)).type.toBe< + Effect.Effect + >() + expect(string.pipe(Effect.retry(Schedule.forever))).type.toBe< + Effect.Effect + >() + + expect(Effect.retry(string, { schedule: Schedule.forever })).type.toBe< + Effect.Effect + >() + expect(string.pipe(Effect.retry({ schedule: Schedule.forever }))).type.toBe< + Effect.Effect + >() + + expect(Effect.retry(string, { + schedule: Schedule.forever, + until: (e) => { + expect(e).type.toBe<"err-1">() + return true + } + })).type.toBe>() + expect(string.pipe(Effect.retry({ + schedule: Schedule.forever, + until: (e) => { + expect(e).type.toBe<"err-1">() + return true + } + }))).type.toBe>() + + expect(Effect.retry(string, { + schedule: Schedule.forever, + until: (e) => { + expect(e).type.toBe<"err-1">() + return boolean + } + })).type.toBe>() + expect(string.pipe(Effect.retry({ + schedule: Schedule.forever, + until: (e) => { + expect(e).type.toBe<"err-1">() + return boolean + } + }))).type.toBe>() + + expect(Effect.retry(Effect.fail(""), { + until: (e): e is "err" => { + expect(e).type.toBe() + return true + } + })).type.toBe>() + expect( + Effect.fail("").pipe(Effect.retry({ + until: (e): e is "err" => { + expect(e).type.toBe() + return true + } + })) + ).type.toBe>() + + expect(Effect.retry(Effect.fail(""), { + schedule: Schedule.forever, + until: (e): e is "err" => { + expect(e).type.toBe() + return true + } + })).type.toBe>() + expect( + Effect.fail("").pipe(Effect.retry({ + schedule: Schedule.forever, + until: (e): e is "err" => { + expect(e).type.toBe() + return true + } + })) + ).type.toBe>() + }) + + it("repeat", () => { + expect(Effect.repeat(string, Schedule.forever)).type.toBe< + Effect.Effect + >() + expect(string.pipe(Effect.repeat(Schedule.forever))).type.toBe< + Effect.Effect + >() + + expect(Effect.repeat(string, { schedule: Schedule.forever })).type.toBe< + Effect.Effect + >() + expect(string.pipe(Effect.repeat({ schedule: Schedule.forever }))).type.toBe< + Effect.Effect + >() + + expect(Effect.repeat(string, { + schedule: Schedule.forever, + until: (e) => { + expect(e).type.toBe() + return true + } + })).type.toBe>() + expect(string.pipe(Effect.repeat({ + schedule: Schedule.forever, + until: (e) => { + expect(e).type.toBe() + return true + } + }))).type.toBe< + Effect.Effect + >() + expect(Effect.repeat(string, { + schedule: Schedule.forever, + until: (e) => { + expect(e).type.toBe() + return boolean + } + })).type.toBe>() + expect(string.pipe(Effect.repeat({ + schedule: Schedule.forever, + until: (e) => { + expect(e).type.toBe() + return boolean + } + }))).type.toBe>() + + expect(Effect.repeat(Effect.succeed(123), { + until: (e): e is 123 => { + expect(e).type.toBe() + return true + } + })).type.toBe>() + expect( + Effect.succeed(123).pipe(Effect.repeat({ + until: (e): e is 123 => { + expect(e).type.toBe() + return true + } + })) + ).type.toBe>() + + expect(Effect.repeat(Effect.succeed(""), { + schedule: Schedule.forever, + until: (e): e is "hello" => { + expect(e).type.toBe() + return true + } + })).type.toBe>() + expect( + Effect.succeed("").pipe(Effect.repeat({ + schedule: Schedule.forever, + until: (e): e is "hello" => { + expect(e).type.toBe() + return true + } + })) + ).type.toBe>() + }) + + it("filter", () => { + expect(Effect.filter(numberArray, (n) => { + expect(n).type.toBe() + return Effect.succeed(true) + })).type.toBe>>() + expect(pipe( + numberArray, + Effect.filter((n) => { + expect(n).type.toBe() + return Effect.succeed(true) + }) + )).type.toBe>>() + + expect(Effect.filter(numberArray, (_n: unknown) => Effect.succeed(true))).type.toBe< + Effect.Effect> + >() + expect(pipe(numberArray, Effect.filter((_n: unknown) => Effect.succeed(true)))).type.toBe< + Effect.Effect> + >() + }) + + it("findFirst", () => { + expect(Effect.findFirst(numberArray, (n) => { + expect(n).type.toBe() + return Effect.succeed(true) + })).type.toBe>>() + expect(pipe( + numberArray, + Effect.findFirst((n) => { + expect(n).type.toBe() + return Effect.succeed(true) + }) + )).type.toBe>>() + + expect(Effect.findFirst(numberArray, (_n: unknown) => Effect.succeed(true))).type.toBe< + Effect.Effect> + >() + expect(pipe(numberArray, Effect.findFirst((_n: unknown) => Effect.succeed(true)))).type.toBe< + Effect.Effect> + >() + }) + + it("reduceEffect", () => { + expect(Effect.reduceEffect(numberEffectIterable, Effect.succeed(0), (n) => { + expect(n).type.toBe() + return 0 + })).type.toBe>() + expect(pipe( + numberEffectIterable, + Effect.reduceEffect(Effect.succeed(0), (n) => { + expect(n).type.toBe() + return 0 + }) + )).type.toBe>() + + expect(Effect.reduceEffect(numberEffectIterable, Effect.succeed(0), (_n: unknown): number | string => 0)).type.toBe< + Effect.Effect + >() + expect(pipe(numberEffectIterable, Effect.reduceEffect(Effect.succeed(0), (_n: unknown): number | string => 0))).type + .toBe>() + }) + + it("takeUntil", () => { + expect(Effect.takeUntil(numberArray, (n) => { + expect(n).type.toBe() + return Effect.succeed(true) + })).type.toBe>>() + expect(pipe( + numberArray, + Effect.takeUntil((n) => { + expect(n).type.toBe() + return Effect.succeed(true) + }) + )).type.toBe>>() + + expect(Effect.takeUntil(numberArray, (_n: unknown) => Effect.succeed(true))).type.toBe< + Effect.Effect> + >() + expect(pipe(numberArray, Effect.takeUntil((_n: unknown) => Effect.succeed(true)))).type.toBe< + Effect.Effect> + >() + }) + + it("takeWhile", () => { + expect(Effect.takeWhile(numberArray, (n) => { + expect(n).type.toBe() + return Effect.succeed(true) + })).type.toBe>>() + expect(pipe( + numberArray, + Effect.takeWhile((n) => { + expect(n).type.toBe() + return Effect.succeed(true) + }) + )).type.toBe>>() + + expect(Effect.takeWhile(numberArray, (_n: unknown) => Effect.succeed(true))).type.toBe< + Effect.Effect> + >() + expect(pipe(numberArray, Effect.takeWhile((_n: unknown) => Effect.succeed(true)))).type.toBe< + Effect.Effect> + >() + }) + + it("catchSome", () => { + expect(pipe( + string, + Effect.catchSome((e) => { + expect(e).type.toBe<"err-1">() + return Option.some(Effect.succeed(1)) + }) + )).type.toBe>() + + expect(Effect.catchSome(string, (e) => { + expect(e).type.toBe<"err-1">() + return Option.some(Effect.succeed(1)) + })).type.toBe< + Effect.Effect + >() + + expect(Effect.catchSome(string, (_e: string) => Option.some(Effect.succeed(1)))).type.toBe< + Effect.Effect + >() + expect(pipe(string, Effect.catchSome((_e: string) => Option.some(Effect.succeed(1))))).type.toBe< + Effect.Effect + >() + }) + + it("retryOrElse", () => { + expect(Effect.retryOrElse(string, Schedule.forever, (e) => { + expect(e).type.toBe<"err-1">() + return Effect.succeed(0) + })).type.toBe>() + expect(string.pipe(Effect.retryOrElse(Schedule.forever, (e) => { + expect(e).type.toBe<"err-1">() + return Effect.succeed(0) + }))).type.toBe>() + + expect(Effect.retryOrElse(string, Schedule.forever, (_e: string) => Effect.succeed(0))).type.toBe< + Effect.Effect + >() + expect(string.pipe(Effect.retryOrElse(Schedule.forever, (_e: string) => Effect.succeed(0)))).type.toBe< + Effect.Effect + >() + }) + + it("do notation", () => { + expect(pipe( + Effect.Do, + Effect.bind("a", (scope) => { + expect(scope).type.toBe<{}>() + return Effect.succeed(1) + }), + Effect.bind("b", (scope) => { + expect(scope).type.toBe<{ a: number }>() + return Effect.succeed("b") + }), + Effect.let("c", (scope) => { + expect(scope).type.toBe<{ a: number; b: string }>() + return true + }) + )).type.toBe>() + + expect(pipe( + Effect.succeed(1), + Effect.bindTo("a"), + Effect.bind("b", (scope) => { + expect(scope).type.toBe<{ a: number }>() + return Effect.succeed("b") + }), + Effect.let("c", (scope) => { + expect(scope).type.toBe<{ a: number; b: string }>() + return true + }) + )).type.toBe>() + }) + + it("liftPredicate", () => { + expect(pipe( + primitiveNumberOrString, + Effect.liftPredicate(Predicate.isString, (sn) => { + expect(sn).type.toBe() + return "b" as const + }) + )).type.toBe>() + expect(Effect.liftPredicate(primitiveNumberOrString, Predicate.isString, (sn) => { + expect(sn).type.toBe() + return "b" as const + })).type.toBe>() + + expect(Effect.liftPredicate(hole>(), (sn) => { + expect(sn).type.toBe() + return "b" as const + })).type.toBe<(a: string | number) => Effect.Effect>() + expect(Effect.liftPredicate(Predicate.isString, (sn) => { + expect(sn).type.toBe() + return "b" as const + })).type.toBe<(a: unknown) => Effect.Effect>() + + expect(pipe( + primitiveNumberOrString, + Effect.liftPredicate( + (sn): sn is number => { + expect(sn).type.toBe() + return typeof sn === "number" + }, + (sn) => { + expect(sn).type.toBe() + return "b" as const + } + ) + )).type.toBe>() + expect(Effect.liftPredicate(primitiveNumberOrString, (sn): sn is number => { + expect(sn).type.toBe() + return typeof sn === "number" + }, (sn) => { + expect(sn).type.toBe() + return "b" as const + })).type.toBe>() + + expect(pipe( + primitiveNumberOrString, + Effect.liftPredicate(predicateNumbersOrStrings, (sn) => { + expect(sn).type.toBe() + return "b" as const + }) + )).type.toBe>() + expect(Effect.liftPredicate(primitiveNumberOrString, predicateNumbersOrStrings, (sn) => { + expect(sn).type.toBe() + return "b" as const + })).type.toBe>() + + expect(pipe( + primitiveNumber, + Effect.liftPredicate(predicateNumbersOrStrings, (n) => { + expect(n).type.toBe() + return "b" as const + }) + )).type.toBe>() + expect(Effect.liftPredicate(primitiveNumber, predicateNumbersOrStrings, (n) => { + expect(n).type.toBe() + return "b" as const + })).type.toBe>() + + expect(pipe( + primitiveNumber, + Effect.liftPredicate( + (n) => { + expect(n).type.toBe() + return true + }, + (n) => { + expect(n).type.toBe() + return "b" as const + } + ) + )).type.toBe>() + expect(Effect.liftPredicate( + primitiveNumber, + (n) => { + expect(n).type.toBe() + return true + }, + (n) => { + expect(n).type.toBe() + return "b" as const + } + )).type.toBe>() + }) + + it("mapAccum", () => { + expect(Effect.mapAccum(strings, 0, (s, a, i) => { + expect(s).type.toBe() + expect(a).type.toBe() + expect(i).type.toBe() + return Effect.succeed([s + i, a]) + })).type.toBe]>>() + expect(pipe( + strings, + Effect.mapAccum(0, (s, a, i) => { + expect(s).type.toBe() + expect(a).type.toBe() + expect(i).type.toBe() + return Effect.succeed([s + i, a]) + }) + )).type.toBe]>>() + + expect(Effect.mapAccum(readonlyNonEmptyStrings, 0, (s, a, i) => { + expect(s).type.toBe() + expect(a).type.toBe() + expect(i).type.toBe() + return Effect.succeed([s + i, a]) + })).type.toBe]]>>() + expect(pipe( + readonlyNonEmptyStrings, + Effect.mapAccum(0, (s, a, i) => { + expect(s).type.toBe() + expect(a).type.toBe() + expect(i).type.toBe() + return Effect.succeed([s + i, a]) + }) + )).type.toBe]]>>() + }) + + it("Tag.Proxy", () => { + expect(hole>>()).type.toBe<{}>() + expect(hole 1 }>>>()) + .type.toBe<{ a: () => Effect.Effect<1, never, "R"> }>() + expect(hole) => void }>>>()) + .type.toBe<{ a: (...args: ReadonlyArray) => Effect.Effect }>() + expect(hole void }>>>()) + .type.toBe<{ a: (...args: Readonly<[1] | [2, 3]>) => Effect.Effect }>() + expect(hole Effect.Effect<1, 2, 3> }>>>()) + .type.toBe<{ a: (...args: Readonly<[1] | [2, 3]>) => Effect.Effect<1, 2, 3 | "R"> }>() + expect(hole>>()) + .type.toBe<{ a: Effect.Effect<1, never, "R"> }>() + expect(hole Promise<1> }>>>()) + .type.toBe<{ a: () => Effect.Effect<1, UnknownException, "R"> }>() + }) + + it("transposeOption", () => { + expect(Effect.transposeOption(Option.none())).type.toBe>>() + expect(Effect.transposeOption(Option.some(string))).type.toBe< + Effect.Effect, "err-1", "dep-1"> + >() + }) + + it("transposeMapOption", () => { + expect(Effect.transposeMapOption(Option.none(), (value) => { + expect(value).type.toBe() + return string + })).type.toBe< + Effect.Effect, "err-1", "dep-1"> + >() + expect(pipe( + Option.none(), + Effect.transposeMapOption((value) => { + expect(value).type.toBe() + return string + }) + )).type.toBe< + Effect.Effect, "err-1", "dep-1"> + >() + expect(Effect.transposeMapOption(Option.some(42), (value) => { + expect(value).type.toBe() + return string + })).type.toBe< + Effect.Effect, "err-1", "dep-1"> + >() + expect(pipe( + Option.some(42), + Effect.transposeMapOption((value) => { + expect(value).type.toBe() + return string + }) + )).type.toBe< + Effect.Effect, "err-1", "dep-1"> + >() + }) + + it("fn", () => { + const fn = Effect.fn((a?: string) => Effect.succeed(a), Effect.asVoid) + expect(fn).type.toBe<(a?: string | undefined) => Effect.Effect>() + }) + + it("fn returns Effect subtype", () => { + const fnNonGen = Effect.fn((a?: string) => Effect.succeed(a), () => Option.some("test")) + const fnGen = Effect.fn(function*(a?: string) { + return Effect.succeed(a) + }, () => Option.some("test")) + + expect(fnNonGen).type.toBe< + (a?: string | undefined) => Effect.Effect + >() + + expect(fnGen).type.toBe< + (a?: string | undefined) => Effect.Effect + >() + }) + + it("ensureSuccessType", () => { + expect(Effect.succeed(123).pipe(Effect.ensureSuccessType())).type.toBe< + Effect.Effect + >() + }) + + it("ensureErrorType", () => { + const withoutError = Effect.succeed("no error") + expect(withoutError.pipe(Effect.ensureErrorType())).type.toBe>() + + const withError = Effect.fail(new TestError1()) + expect(withError.pipe(Effect.ensureErrorType())).type.toBe>() + }) + + it("ensureRequirementsType", () => { + const withoutRequirements = Effect.never + expect(withoutRequirements.pipe(Effect.ensureRequirementsType())).type.toBe< + Effect.Effect + >() + + const withRequirement = Effect.flatMap(TestService, () => Effect.never) + expect(withRequirement.pipe(Effect.ensureRequirementsType())).type.toBe< + Effect.Effect + >() + }) +}) diff --git a/repos/effect/packages/effect/dtslint/Either.tst.ts b/repos/effect/packages/effect/dtslint/Either.tst.ts new file mode 100644 index 0000000..23df737 --- /dev/null +++ b/repos/effect/packages/effect/dtslint/Either.tst.ts @@ -0,0 +1,353 @@ +import { Array, Either, hole, Option, pipe, Predicate } from "effect" +import { describe, expect, it } from "tstyche" + +declare const string$string: Either.Either +declare const number$string: Either.Either +declare const boolean$string: Either.Either +declare const boolean$Error: Either.Either +declare const literal$Error: Either.Either<"a", Error> + +describe("Either", () => { + it("flip", () => { + expect(Either.flip(number$string)).type.toBe>() + expect(pipe(number$string, Either.flip)).type.toBe>() + }) + + it("try", () => { + expect(Either.try(() => 1)).type.toBe>() + expect(Either.try({ try: () => 1, catch: () => new Error() })).type.toBe>() + }) + + describe("all", () => { + it("tuple", () => { + expect(Either.all([])).type.toBe>() + expect(Either.all([number$string])).type.toBe>() + expect(Either.all([number$string, boolean$string])).type.toBe>() + expect(Either.all([number$string, boolean$Error])).type.toBe>() + expect(pipe([number$string, boolean$string] as const, Either.all)).type.toBe< + Either.Either<[number, boolean], string> + >() + expect(pipe([number$string, boolean$Error] as const, Either.all)).type.toBe< + Either.Either<[number, boolean], string | Error> + >() + }) + + it("struct", () => { + expect(Either.all({})).type.toBe>() + expect(Either.all({ a: number$string })).type.toBe>() + expect(Either.all({ a: number$string, b: boolean$string })).type.toBe< + Either.Either<{ a: number; b: boolean }, string> + >() + expect(Either.all({ a: number$string, b: boolean$Error })).type.toBe< + Either.Either<{ a: number; b: boolean }, string | Error> + >() + expect(pipe({ a: number$string, b: boolean$string }, Either.all)).type.toBe< + Either.Either<{ a: number; b: boolean }, string> + >() + expect(pipe({ a: number$string, b: boolean$Error }, Either.all)).type.toBe< + Either.Either<{ a: number; b: boolean }, string | Error> + >() + }) + + it("array", () => { + const eitherArray = hole>>() + expect(Either.all(eitherArray)).type.toBe, string>>() + expect(pipe(eitherArray, Either.all)).type.toBe, string>>() + }) + + it("record", () => { + const eitherRecord = hole>>() + expect(Either.all(eitherRecord)).type.toBe>() + expect(pipe(eitherRecord, Either.all)).type.toBe>() + }) + }) + + it("andThen", () => { + expect(Either.andThen(string$string, number$string)).type.toBe>() + expect(string$string.pipe(Either.andThen(number$string))).type.toBe>() + + expect(Either.andThen(string$string, () => number$string)).type.toBe>() + expect(string$string.pipe(Either.andThen(() => number$string))).type.toBe>() + }) + + it("liftPredicate", () => { + const primitiveNumber = hole() + const stringOrNumber = hole() + const predicateNumberOrString = hole>() + const refinementNumberOrStringToNumber = hole>() + + expect( + Either.liftPredicate(predicateNumberOrString, (sn) => { + expect(sn).type.toBe() + return "b" as const + }) + ).type.toBe<(a: string | number) => Either.Either>() + expect( + Either.liftPredicate(refinementNumberOrStringToNumber, (sn) => { + expect(sn).type.toBe() + return "b" as const + }) + ).type.toBe<(a: string | number) => Either.Either>() + + expect( + Either.liftPredicate( + stringOrNumber, + (sn): sn is number => { + expect(sn).type.toBe() + return typeof sn === "number" + }, + (sn) => { + expect(sn).type.toBe() + return "b" as const + } + ) + ).type.toBe>() + expect( + pipe( + stringOrNumber, + Either.liftPredicate( + (sn): sn is number => { + expect(sn).type.toBe() + return typeof sn === "number" + }, + (sn) => { + expect(sn).type.toBe() + return "b" as const + } + ) + ) + ).type.toBe>() + + expect( + Either.liftPredicate(stringOrNumber, predicateNumberOrString, (sn) => { + expect(sn).type.toBe() + return "b" as const + }) + ).type.toBe>() + expect( + pipe( + stringOrNumber, + Either.liftPredicate(predicateNumberOrString, (sn) => { + expect(sn).type.toBe() + return "b" as const + }) + ) + ).type.toBe>() + + expect( + Either.liftPredicate(primitiveNumber, predicateNumberOrString, (sn) => { + expect(sn).type.toBe() + return "b" as const + }) + ).type.toBe>() + expect( + pipe( + primitiveNumber, + Either.liftPredicate(predicateNumberOrString, (sn) => { + expect(sn).type.toBe() + return "b" as const + }) + ) + ).type.toBe>() + }) + + it("fromNullable", () => { + const nullableString = hole() + const nullableObject = hole<{ a: string } | undefined>() + + expect( + Either.fromNullable( + nullableString, + () => new Error() + ) + ).type.toBe>() + + expect( + pipe( + nullableString, + Either.fromNullable(() => new Error()) + ) + ).type.toBe>() + + expect( + Either.fromNullable(nullableObject, () => new Error()) + ).type.toBe>() + + expect( + pipe( + nullableObject, + Either.fromNullable(() => new Error()) + ) + ).type.toBe< + Either.Either<{ a: string }, Error> + >() + }) + + it("filterOrLeft", () => { + const predicateUnknown = hole>() + + const arrayOfStrings$Error = hole, Error>>() + expect( + Either.filterOrLeft(arrayOfStrings$Error, Array.isNonEmptyArray, (ss) => { + expect(ss).type.toBe>() + return "b" as const + }) + ).type.toBe], "b" | Error>>() + expect( + pipe( + arrayOfStrings$Error, + Either.filterOrLeft(Array.isNonEmptyArray, (ss) => { + expect(ss).type.toBe>() + return "b" as const + }) + ) + ).type.toBe], "b" | Error>>() + + const readonlyArrayOfStrings$Error = hole, Error>>() + expect( + Either.filterOrLeft(readonlyArrayOfStrings$Error, Array.isNonEmptyReadonlyArray, (ss) => { + expect(ss).type.toBe>() + return "b" as const + }) + ).type.toBe], "b" | Error>>() + expect( + pipe( + readonlyArrayOfStrings$Error, + Either.filterOrLeft(Array.isNonEmptyReadonlyArray, (ss) => { + expect(ss).type.toBe>() + return "b" as const + }) + ) + ).type.toBe], "b" | Error>>() + + // @tstyche fixme -- This doesn't work but it should + expect( + Either.filterOrLeft(literal$Error, Predicate.isString, (a) => { + // @tstyche fixme -- This doesn't work but it should + expect(a).type.toBe<"a">() + return "b" as const + }) + ).type.toBe>() + expect( + pipe( + literal$Error, + Either.filterOrLeft(Predicate.isString, (a) => { + expect(a).type.toBe<"a">() + return "b" as const + }) + ) + ).type.toBe>() + + // @tstyche fixme -- This doesn't work but it should + expect( + Either.filterOrLeft(literal$Error, Predicate.isString, (_s: string) => "b" as const) + ).type.toBe>() + expect( + pipe( + literal$Error, + Either.filterOrLeft(Predicate.isString, (_s: string) => "b" as const) + ) + ).type.toBe>() + + expect( + Either.filterOrLeft(literal$Error, predicateUnknown, (a) => { + expect(a).type.toBe<"a">() + return "b" as const + }) + ).type.toBe>() + expect( + pipe( + literal$Error, + Either.filterOrLeft(predicateUnknown, (a) => { + expect(a).type.toBe<"a">() + return "b" as const + }) + ) + ).type.toBe>() + + expect( + Either.filterOrLeft(literal$Error, predicateUnknown, (_s: string) => "b" as const) + ).type.toBe>() + expect( + pipe( + literal$Error, + Either.filterOrLeft(predicateUnknown, (_s: string) => "b" as const) + ) + ).type.toBe>() + }) + + it("type level helpers", () => { + type R = Either.Either.Right + type L = Either.Either.Left + expect().type.toBe() + expect().type.toBe() + }) + + it("do notation", () => { + expect( + pipe( + Either.Do, + Either.bind("a", (scope) => { + expect(scope).type.toBe<{}>() + return Either.right(1) + }), + Either.bind("b", (scope) => { + expect(scope).type.toBe<{ a: number }>() + return Either.right("b") + }), + Either.let("c", (scope) => { + expect(scope).type.toBe<{ a: number; b: string }>() + return true + }) + ) + ).type.toBe>() + + expect( + pipe( + Either.right(1), + Either.bindTo("a"), + Either.bind("b", (scope) => { + expect(scope).type.toBe<{ a: number }>() + return Either.right("b") + }), + Either.let("c", (scope) => { + expect(scope).type.toBe<{ a: number; b: string }>() + return true + }) + ) + ).type.toBe>() + }) +}) + +it("transposeMapOption", () => { + expect(Either.transposeMapOption(Option.none(), (value) => { + expect(value).type.toBe() + return string$string + })).type.toBe< + Either.Either, string> + >() + expect(pipe( + Option.none(), + Either.transposeMapOption((value) => { + expect(value).type.toBe() + return string$string + }) + )).type.toBe< + Either.Either, string> + >() + expect(Either.transposeMapOption(Option.some(42), (value) => { + expect(value).type.toBe() + return string$string + })).type.toBe< + Either.Either, string> + >() + expect(pipe( + Option.some(42), + Either.transposeMapOption((value) => { + expect(value).type.toBe() + return string$string + }) + )).type.toBe< + Either.Either, string> + >() +}) diff --git a/repos/effect/packages/effect/dtslint/Equal.tst.ts b/repos/effect/packages/effect/dtslint/Equal.tst.ts new file mode 100644 index 0000000..1440fbf --- /dev/null +++ b/repos/effect/packages/effect/dtslint/Equal.tst.ts @@ -0,0 +1,12 @@ +import { Data, Equal, Hash } from "effect" + +export class MyClass extends Data.TaggedClass("mytag")<{}> { + // should support `Hash.symbol` as method + [Hash.symbol]() { + return 0 + } + // should support `Equal.symbol` as method + [Equal.symbol]() { + return false + } +} diff --git a/repos/effect/packages/effect/dtslint/Exit.tst.ts b/repos/effect/packages/effect/dtslint/Exit.tst.ts new file mode 100644 index 0000000..141dbd2 --- /dev/null +++ b/repos/effect/packages/effect/dtslint/Exit.tst.ts @@ -0,0 +1,43 @@ +import * as Exit from "effect/Exit" +import { pipe } from "effect/Function" +import * as Predicate from "effect/Predicate" +import { describe, expect, it } from "tstyche" + +declare const number$string: Exit.Exit +declare const stringOrNumber$string: Exit.Exit + +describe("Exit", () => { + it("exists", () => { + if (Exit.exists(stringOrNumber$string, Predicate.isString)) { + expect(stringOrNumber$string).type.toBe>() + } + if (pipe(stringOrNumber$string, Exit.exists(Predicate.isString))) { + // @tstyche fixme -- This doesn't work but it should + expect(stringOrNumber$string).type.toBe>() + } + if (Exit.exists(Predicate.isString)(stringOrNumber$string)) { + expect(stringOrNumber$string).type.toBe>() + } + + if ( + pipe( + number$string, + Exit.exists((n) => { + expect(n).type.toBe() + return true + }) + ) + ) { + expect(number$string).type.toBe>() + } + + if ( + pipe( + number$string, + Exit.exists((_sn: string | number) => true) + ) + ) { + expect(number$string).type.toBe>() + } + }) +}) diff --git a/repos/effect/packages/effect/dtslint/Fiber.tst.ts b/repos/effect/packages/effect/dtslint/Fiber.tst.ts new file mode 100644 index 0000000..b42422c --- /dev/null +++ b/repos/effect/packages/effect/dtslint/Fiber.tst.ts @@ -0,0 +1,21 @@ +import type { Effect, Exit } from "effect" +import { Fiber } from "effect" +import { describe, expect, it } from "tstyche" + +declare const string: Fiber.Fiber +declare const number: Fiber.Fiber +declare const arrayOfStringOrNumber: Array | Fiber.Fiber> + +describe("Fiber", () => { + it("awaitAll", () => { + expect(Fiber.awaitAll([string, number])).type.toBe< + Effect.Effect<[Exit.Exit, Exit.Exit]> + >() + expect(Fiber.awaitAll(new Set([string, number]))).type.toBe< + Effect.Effect | Exit.Exit>> + >() + expect(Fiber.awaitAll(arrayOfStringOrNumber)).type.toBe< + Effect.Effect | Exit.Exit>> + >() + }) +}) diff --git a/repos/effect/packages/effect/dtslint/Function.tst.ts b/repos/effect/packages/effect/dtslint/Function.tst.ts new file mode 100644 index 0000000..5ecb60d --- /dev/null +++ b/repos/effect/packages/effect/dtslint/Function.tst.ts @@ -0,0 +1,58 @@ +import { flow, Function, identity, Option, pipe } from "effect" +import { describe, expect, it } from "tstyche" + +describe("Function", () => { + describe("pipe", () => { + it("We should only have one error for the missing definition", () => { + const _x = (): number => + pipe( + 1, + // @ts-expect-error: Cannot find name 'add' + add(1), + identity + ) + + const _y = (): (n: number) => number => + flow( + // @ts-expect-error: Cannot find name 'add' + add(1), + identity + ) + + const _z = (): number => + Option.some(1).pipe( + // @ts-expect-error: Cannot find name 'add' + add(1), + identity + ) + }) + + it("should preserve literal types (issue #5963)", () => { + const result = pipe( + [1, 2, 3] as const, + (x) => x + ) + expect(result).type.toBe() + }) + }) + + it("apply", () => { + const apply1 = Function.apply("a") + const apply2 = Function.apply("a", 1) + + const countArgs = (...args: Array) => args.length + const arg1 = (a: string) => a + const arg2 = (a: string, b: number) => `${a}${b}` + const arg3 = (a: number) => a + + expect(apply1(countArgs)).type.toBe() + expect(apply1(arg1)).type.toBe() + expect(apply1).type.not.toBeCallableWith(arg2) + expect(apply1).type.not.toBeCallableWith(arg3) + + expect(apply2(countArgs)).type.toBe() + expect(apply2(arg1)).type.toBe() + expect(apply2(arg2)).type.toBe() + expect(apply1).type.not.toBeCallableWith(arg3) + }) +}) diff --git a/repos/effect/packages/effect/dtslint/HKT.tst.ts b/repos/effect/packages/effect/dtslint/HKT.tst.ts new file mode 100644 index 0000000..32f9a7d --- /dev/null +++ b/repos/effect/packages/effect/dtslint/HKT.tst.ts @@ -0,0 +1,8 @@ +import type { HKT } from "effect" + +export function testIssue536( + x: HKT.Kind +): HKT.Kind { + // @ts-expect-error: Type 'Kind' is not assignable to type 'Kind' + return x +} diff --git a/repos/effect/packages/effect/dtslint/HashMap.tst.ts b/repos/effect/packages/effect/dtslint/HashMap.tst.ts new file mode 100644 index 0000000..68c6d3c --- /dev/null +++ b/repos/effect/packages/effect/dtslint/HashMap.tst.ts @@ -0,0 +1,74 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import type { Option } from "effect" +import { HashMap, hole, pipe, Predicate } from "effect" +import { describe, expect, it } from "tstyche" + +declare const literals: HashMap.HashMap<"k", "v"> +declare const string$numberOrString: HashMap.HashMap + +describe("HashMap", () => { + it("HashMap.Key type helper", () => { + type K = HashMap.HashMap.Key + expect(hole()).type.toBe<"k">() + }) + + it("HashMap.Value type helper", () => { + type V = HashMap.HashMap.Value + expect().type.toBe<"v">() + }) + + it("HashMap.Entry type helper", () => { + expect>().type.toBe<["k", "v"]>() + }) + + it("filter", () => { + // Predicate + expect( + HashMap.filter(string$numberOrString, (value, key) => { + expect(value).type.toBe() + expect(key).type.toBe() + return true + }) + ).type.toBe>() + expect( + pipe( + string$numberOrString, + HashMap.filter((value, key) => { + expect(value).type.toBe() + expect(key).type.toBe() + return true + }) + ) + ).type.toBe>() + + // Refinement + expect(HashMap.filter(string$numberOrString, Predicate.isNumber)).type.toBe>() + expect( + pipe(string$numberOrString, HashMap.filter(Predicate.isNumber)) + ).type.toBe>() + }) + + it("findFirst", () => { + // Predicate + expect(HashMap.findFirst(string$numberOrString, (value, key) => { + expect(value).type.toBe() + expect(key).type.toBe() + return true + })).type.toBe>() + expect(pipe( + string$numberOrString, + HashMap.findFirst((value, key) => { + expect(value).type.toBe() + expect(key).type.toBe() + return true + }) + )).type.toBe>() + + // Refinement + expect(HashMap.findFirst(string$numberOrString, Predicate.isNumber)) + .type.toBe>() + expect( + pipe(string$numberOrString, HashMap.findFirst(Predicate.isNumber)) + ).type.toBe>() + }) +}) diff --git a/repos/effect/packages/effect/dtslint/HashSet.tst.ts b/repos/effect/packages/effect/dtslint/HashSet.tst.ts new file mode 100644 index 0000000..218ff50 --- /dev/null +++ b/repos/effect/packages/effect/dtslint/HashSet.tst.ts @@ -0,0 +1,82 @@ +import { HashSet, pipe, Predicate } from "effect" +import { describe, expect, it } from "tstyche" + +declare const number: HashSet.HashSet +declare const numberOrString: HashSet.HashSet + +declare const predicateNumberOrString: Predicate.Predicate + +describe("HashSet", () => { + it("every", () => { + if (HashSet.every(numberOrString, Predicate.isString)) { + expect(numberOrString).type.toBe>() + } + if (pipe(numberOrString, HashSet.every(Predicate.isString))) { + // @tstyche fixme -- This doesn't work but it should + expect(numberOrString).type.toBe>() + } + if (HashSet.every(Predicate.isString)(numberOrString)) { + expect(numberOrString).type.toBe>() + } + + expect(HashSet.every(numberOrString, (value) => { + expect(value).type.toBe() + return true + })).type.toBe() + expect(pipe( + numberOrString, + HashSet.every((value) => { + expect(value).type.toBe() + return true + }) + )).type.toBe() + }) + + it("partition", () => { + // Predicate + expect(HashSet.partition(numberOrString, (value) => { + expect(value).type.toBe() + return true + })).type.toBe<[excluded: HashSet.HashSet, satisfying: HashSet.HashSet]>() + expect(pipe( + numberOrString, + HashSet.partition((value) => { + expect(value).type.toBe() + return true + }) + )).type.toBe<[excluded: HashSet.HashSet, satisfying: HashSet.HashSet]>() + + // Refinement + expect(HashSet.partition(numberOrString, Predicate.isNumber)) + .type.toBe<[excluded: HashSet.HashSet, satisfying: HashSet.HashSet]>() + + expect(pipe(numberOrString, HashSet.partition(Predicate.isNumber))) + .type.toBe<[excluded: HashSet.HashSet, satisfying: HashSet.HashSet]>() + }) + + it("filter", () => { + // Predicate + expect(HashSet.filter(numberOrString, (value) => { + expect(value).type.toBe() + return true + })).type.toBe>() + expect(pipe( + numberOrString, + HashSet.filter((value) => { + expect(value).type.toBe() + return true + }) + )).type.toBe>() + + expect(pipe(numberOrString, HashSet.filter(predicateNumberOrString))) + .type.toBe>() + expect(pipe(number, HashSet.filter(predicateNumberOrString))) + .type.toBe>() + + // Refinement + expect(HashSet.filter(numberOrString, Predicate.isNumber)) + .type.toBe>() + expect(pipe(numberOrString, HashSet.filter(Predicate.isNumber))) + .type.toBe>() + }) +}) diff --git a/repos/effect/packages/effect/dtslint/Layer.tst.ts b/repos/effect/packages/effect/dtslint/Layer.tst.ts new file mode 100644 index 0000000..909927b --- /dev/null +++ b/repos/effect/packages/effect/dtslint/Layer.tst.ts @@ -0,0 +1,69 @@ +import { Context, Layer, Schedule } from "effect" +import { describe, expect, it } from "tstyche" + +interface In1 {} +interface Err1 {} +interface Out1 {} + +declare const layer1: Layer.Layer + +interface In2 {} +interface Err2 {} +interface Out2 {} + +declare const layer2: Layer.Layer + +interface In3 {} +interface Err3 {} +interface Out3 {} + +declare const layer3: Layer.Layer + +class TestService1 extends Context.Tag("TestService1")() {} + +describe("Layer", () => { + it("merge", () => { + expect(Layer.merge).type.not.toBeCallableWith() + + expect(Layer.merge(layer1, layer2)).type.toBe>() + expect(layer1.pipe(Layer.merge(layer2))).type.toBe>() + }) + + it("mergeAll", () => { + expect(Layer.mergeAll).type.not.toBeCallableWith() + + expect(Layer.mergeAll(layer1)).type.toBe>() + expect(Layer.mergeAll(layer1, layer2)).type.toBe>() + expect(Layer.mergeAll(layer1, layer2, layer3)) + .type.toBe>() + }) + + it("retry", () => { + expect(Layer.retry(layer1, Schedule.recurs(1))).type.toBe>() + expect(layer1.pipe(Layer.retry(Schedule.recurs(1)))).type.toBe>() + }) + + it("ensureSuccessType", () => { + expect(layer1.pipe(Layer.ensureSuccessType())).type.toBe>() + }) + + it("ensureErrorType", () => { + const withoutError = Layer.succeed(TestService1, {}) + expect(withoutError.pipe(Layer.ensureErrorType())).type.toBe>() + + const withError = layer1 + expect(withError.pipe(Layer.ensureErrorType())).type.toBe>() + }) + + it("ensureRequirementsType", () => { + const withoutRequirements = Layer.succeed(TestService1, {}) + expect(withoutRequirements.pipe(Layer.ensureRequirementsType())).type.toBe< + Layer.Layer + >() + + const withRequirement = layer1 + expect(withRequirement.pipe(Layer.ensureRequirementsType())).type.toBe< + Layer.Layer + >() + }) +}) diff --git a/repos/effect/packages/effect/dtslint/List.tst.ts b/repos/effect/packages/effect/dtslint/List.tst.ts new file mode 100644 index 0000000..dddf937 --- /dev/null +++ b/repos/effect/packages/effect/dtslint/List.tst.ts @@ -0,0 +1,235 @@ +import type { Option } from "effect" +import { List, pipe, Predicate } from "effect" +import { describe, expect, it } from "tstyche" + +declare const number: List.List +declare const string: List.List +declare const nonEmptyNumber: List.Cons +declare const nonEmptyString: List.Cons +declare const numberOrString: List.List +declare const predicateNumberOrString: Predicate.Predicate + +describe("List", () => { + it("every", () => { + if (List.every(numberOrString, Predicate.isString)) { + expect(numberOrString).type.toBe>() + } + if (pipe(numberOrString, List.every(Predicate.isString))) { + // @tstyche fixme -- This doesn't work but it should + expect(numberOrString).type.toBe>() + } + if (List.every(Predicate.isString)(numberOrString)) { + expect(numberOrString).type.toBe>() + } + + expect( + List.every(numberOrString, (value) => { + expect(value).type.toBe() + return true + }) + ).type.toBe() + expect(pipe( + numberOrString, + List.every((value) => { + expect(value).type.toBe() + return true + }) + )).type.toBe() + }) + + it("some", () => { + if (List.some(numberOrString, Predicate.isString)) { + expect(numberOrString).type.toBe>() + } + if (pipe(numberOrString, List.some(Predicate.isString))) { + // @tstyche fixme -- This doesn't work but it should + expect(numberOrString).type.toBe>() + } + if (List.some(Predicate.isString)(numberOrString)) { + expect(numberOrString).type.toBe>() + } + + expect( + List.some(numberOrString, (value) => { + expect(value).type.toBe() + return true + }) + ).type.toBe() + expect(pipe( + numberOrString, + List.some((value) => { + expect(value).type.toBe() + return true + }) + )).type.toBe() + }) + + it("partition", () => { + // Predicate + expect(List.partition(numberOrString, (value) => { + expect(value).type.toBe() + return true + })).type.toBe< + [excluded: List.List, satisfying: List.List] + >() + expect(pipe( + numberOrString, + List.partition((value) => { + expect(value).type.toBe() + return true + }) + )).type.toBe< + [excluded: List.List, satisfying: List.List] + >() + expect(List.partition(number, predicateNumberOrString)).type.toBe< + [excluded: List.List, satisfying: List.List] + >() + expect(pipe(number, List.partition(predicateNumberOrString))).type.toBe< + [excluded: List.List, satisfying: List.List] + >() + + // Refinement + expect(List.partition(numberOrString, Predicate.isNumber)).type.toBe< + [excluded: List.List, satisfying: List.List] + >() + + expect(pipe(numberOrString, List.partition(Predicate.isNumber))).type.toBe< + [excluded: List.List, satisfying: List.List] + >() + }) + + it("append", () => { + expect(List.append(numberOrString, true)).type.toBe>() + expect(pipe(numberOrString, List.append(true))).type.toBe>() + expect(List.append(true)(numberOrString)).type.toBe>() + }) + + it("prepend", () => { + expect(List.prepend(numberOrString, true)).type.toBe>() + expect(pipe(numberOrString, List.prepend(true))).type.toBe>() + expect(List.prepend(true)(numberOrString)).type.toBe>() + }) + + it("map", () => { + expect(List.map(number, (n) => n + 1)).type.toBe>() + expect(pipe(number, List.map((n) => n + 1))).type.toBe>() + expect(List.map(nonEmptyNumber, (n) => n + 1)).type.toBe>() + expect(pipe(nonEmptyNumber, List.map((n) => n + 1))).type.toBe>() + }) + + it("filter", () => { + // Predicate + expect(List.filter(numberOrString, (value) => { + expect(value).type.toBe() + return true + })).type.toBe>() + expect(pipe( + numberOrString, + List.filter((value) => { + expect(value).type.toBe() + return true + }) + )).type.toBe>() + + expect(List.filter(number, predicateNumberOrString)).type.toBe>() + expect(pipe(number, List.filter(predicateNumberOrString))).type.toBe>() + + // Refinement + expect(List.filter(numberOrString, Predicate.isNumber)).type.toBe>() + expect(pipe(numberOrString, List.filter(Predicate.isNumber))).type.toBe>() + }) + + it("findFirst", () => { + // Predicate + expect(List.findFirst(numberOrString, (value) => { + expect(value).type.toBe() + return true + })).type.toBe>() + expect(pipe( + numberOrString, + List.findFirst((value) => { + expect(value).type.toBe() + return true + }) + )).type.toBe>() + + expect(List.findFirst(number, predicateNumberOrString)).type.toBe>() + expect(pipe(number, List.findFirst(predicateNumberOrString))).type.toBe>() + + // Refinement + expect(List.findFirst(numberOrString, Predicate.isNumber)).type.toBe>() + expect(pipe(numberOrString, List.findFirst(Predicate.isNumber))).type.toBe>() + }) + + it("appendAll", () => { + expect(List.appendAll(string, number)).type.toBe>() + expect(pipe(string, List.appendAll(number))).type.toBe>() + expect(List.appendAll(nonEmptyString, number)).type.toBe>() + expect(pipe(nonEmptyString, List.appendAll(number))).type.toBe>() + expect(List.appendAll(string, nonEmptyNumber)).type.toBe>() + expect(pipe(string, List.appendAll(nonEmptyNumber))).type.toBe>() + expect(List.appendAll(nonEmptyString, nonEmptyNumber)).type.toBe>() + expect(pipe(nonEmptyString, List.appendAll(nonEmptyNumber))).type.toBe>() + }) + + it("prependAll", () => { + expect(List.prependAll(string, number)).type.toBe>() + expect(pipe(string, List.prependAll(number))).type.toBe>() + expect(List.prependAll(nonEmptyString, number)).type.toBe>() + expect(pipe(nonEmptyString, List.prependAll(number))).type.toBe>() + expect(List.prependAll(string, nonEmptyNumber)).type.toBe>() + expect(pipe(string, List.prependAll(nonEmptyNumber))).type.toBe>() + expect(List.prependAll(nonEmptyString, nonEmptyNumber)).type.toBe>() + expect(pipe(nonEmptyString, List.prependAll(nonEmptyNumber))).type.toBe>() + }) + + it("flatMap", () => { + expect( + List.flatMap(string, (value) => { + expect(value).type.toBe() + return List.empty() + }) + ).type.toBe>() + expect( + pipe( + string, + List.flatMap((value) => { + expect(value).type.toBe() + return List.empty() + }) + ) + ).type.toBe>() + + expect( + List.flatMap(nonEmptyString, (value) => { + expect(value).type.toBe() + return List.empty() + }) + ).type.toBe>() + expect( + pipe( + nonEmptyString, + List.flatMap((value) => { + expect(value).type.toBe() + return List.empty() + }) + ) + ).type.toBe>() + + expect( + List.flatMap(nonEmptyString, (value) => { + expect(value).type.toBe() + return List.of(value.length) + }) + ).type.toBe>() + expect( + pipe( + nonEmptyString, + List.flatMap((value) => { + expect(value).type.toBe() + return List.of(value.length) + }) + ) + ).type.toBe>() + }) +}) diff --git a/repos/effect/packages/effect/dtslint/ManagedRuntime.tst.ts b/repos/effect/packages/effect/dtslint/ManagedRuntime.tst.ts new file mode 100644 index 0000000..d149635 --- /dev/null +++ b/repos/effect/packages/effect/dtslint/ManagedRuntime.tst.ts @@ -0,0 +1,15 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import type { ManagedRuntime } from "effect" +import { describe, expect, it } from "tstyche" + +declare const runtime: ManagedRuntime.ManagedRuntime<"context", "error"> + +describe("ManagedRuntime", () => { + it("ManagedRuntime.Context type helper", () => { + expect>().type.toBe<"context">() + }) + + it("ManagedRuntime.Error type helper", () => { + expect>().type.toBe<"error">() + }) +}) diff --git a/repos/effect/packages/effect/dtslint/Match.tst.ts b/repos/effect/packages/effect/dtslint/Match.tst.ts new file mode 100644 index 0000000..1bb5ccd --- /dev/null +++ b/repos/effect/packages/effect/dtslint/Match.tst.ts @@ -0,0 +1,665 @@ +import { Either, hole, Match, Option, pipe, Predicate } from "effect" +import { describe, expect, it } from "tstyche" + +type Value = { _tag: "A"; a: number } | { _tag: "B"; b: number } +declare const value: Value +declare const handlerA: (_: { _tag: "A"; a: number }) => string +const isArray = (_: unknown): _ is ReadonlyArray => Array.isArray(_) + +describe("Match", () => { + it("type", () => { + expect( + Match.type().pipe( + Match.when(Match.any, (v) => { + expect(v).type.toBe() + return "a" + }), + Match.exhaustive + ) + ).type.toBe<(u: Value) => string>() + }) + + it("value", () => { + expect( + Match.value(hole()).pipe( + Match.when(Match.any, (v) => { + expect(v).type.toBe() + return "a" + }), + Match.exhaustive + ) + ).type.toBe() + }) + + it("withReturnType", () => { + expect( + Match.type<{ a: number } | { b: string }>().pipe( + Match.withReturnType(), + Match.when({ a: Match.number }, (_) => + // @ts-expect-error: Type 'number' is not assignable to type 'string' + _.a), + Match.when({ b: Match.string }, (_) => _.b), + Match.exhaustive + ) + ).type.toBe<(u: { a: number } | { b: string }) => string>() + }) + + it("orElse", () => { + expect( + Match.value(hole()).pipe( + Match.when(Match.string, (s) => { + expect(s).type.toBe() + return Symbol.for(s) + }), + Match.orElse((n) => { + expect(n).type.toBe() + return true + }) + ) + ).type.toBe() + }) + + it("option", () => { + expect( + Match.value(hole()).pipe( + Match.when(Match.string, (s) => { + expect(s).type.toBe() + return Symbol.for(s) + }), + Match.option + ) + ).type.toBe>() + }) + + it("either", () => { + expect( + Match.value(hole()).pipe( + Match.when(Match.string, (s) => { + expect(s).type.toBe() + return Symbol.for(s) + }), + Match.either + ) + ).type.toBe>() + }) + + describe("when", () => { + it("schema exhaustive-literal", () => { + expect( + pipe( + Match.value(hole<{ _tag: "A"; a: number | string } | { _tag: "B"; b: number }>()), + Match.when({ _tag: Match.is("A", "B"), a: Match.number }, (v) => { + expect(v).type.toBe<{ _tag: "A"; a: number }>() + return Either.right(v._tag) + }), + Match.when({ _tag: Match.string, a: Match.string }, (v) => { + expect(v).type.toBe<{ _tag: "A"; a: string }>() + return Either.right(v._tag) + }), + Match.when({ b: Match.number }, (v) => { + expect(v).type.toBe<{ _tag: "B"; b: number }>() + return Either.left(v._tag) + }), + Match.orElse((v) => { + expect(v).type.toBe<{ _tag: "A"; a: number | string }>() + throw "absurd" + }) + ) + ).type.toBe>() + }) + + it("tuples", () => { + expect( + pipe( + Match.value(hole<[string, string]>()), + Match.when(["yeah"], (v) => { + expect(v).type.toBe() + return true + }), + Match.option + ) + ).type.toBe>() + }) + + it("not literal", () => { + expect( + pipe( + Match.value(hole()), + Match.not("hi", (v) => { + expect(v).type.toBe() + return "a" + }), + Match.orElse((v) => { + expect(v).type.toBe<"hi">() + return "b" + }) + ) + ).type.toBe() + }) + + it("literals", () => { + expect( + pipe( + Match.value(hole()), + Match.when("yeah", (v) => { + expect(v).type.toBe<"yeah">() + return v === "yeah" + }), + Match.orElse((v) => { + expect(v).type.toBe() + return "nah" + }) + ) + ).type.toBe() + }) + + it("nested", () => { + expect( + pipe( + Match.value( + hole< + | { foo: { bar: { baz: { qux: string } } } } + | { foo: { bar: { baz: { qux: number } } } } + | { foo: { bar: null } } + >() + ), + Match.when({ foo: { bar: { baz: { qux: 2 } } } }, (v) => { + expect(v).type.toBe<{ foo: { bar: { baz: { qux: 2 } } } }>() + return `literal ${v.foo.bar.baz.qux}` + }), + Match.when({ foo: { bar: { baz: { qux: "b" } } } }, (v) => { + expect(v).type.toBe<{ foo: { bar: { baz: { qux: "b" } } } }>() + return `literal ${v.foo.bar.baz.qux}` + }), + Match.when({ foo: { bar: { baz: { qux: Match.number } } } }, (v) => v.foo.bar.baz.qux), + Match.when({ foo: { bar: { baz: { qux: Match.string } } } }, (v) => v.foo.bar.baz.qux), + Match.when({ foo: { bar: null } }, (v) => v.foo.bar), + Match.exhaustive + ) + ).type.toBe() + }) + + it("deep recursive", () => { + type A = null | string | number | { [K in string]: A } + expect( + pipe( + Match.value(hole()), + Match.when(Predicate.isNull, (v) => { + expect(v).type.toBe() + return "null" + }), + Match.when(Predicate.isBoolean, (v) => { + expect(v).type.toBe() + return "boolean" + }), + Match.when(Predicate.isNumber, (v) => { + expect(v).type.toBe() + return "number" + }), + Match.when(Predicate.isString, (v) => { + expect(v).type.toBe() + return "string" + }), + Match.when(Match.record, (v) => { + expect(v).type.toBe<{ [x: string]: A }>() + return "record" + }), + Match.when(Predicate.isSymbol, (v) => { + expect(v).type.toBe() + return "symbol" + }), + Match.when(Predicate.isReadonlyRecord, (v) => { + expect(v).type.toBe<{ readonly [x: string]: unknown; readonly [x: symbol]: unknown }>() + return "readonlyrecord" + }), + Match.exhaustive + ) + ).type.toBe() + }) + + it("instanceOf", () => { + class Test {} + class Test2 {} + expect( + pipe( + Match.value(new Test()), + Match.when(Match.instanceOf(Test), (v) => { + expect(v).type.toBe() + return 1 + }), + Match.orElse((v) => { + expect(v).type.toBe() + return 0 + }) + ) + ).type.toBe() + + const match = pipe( + Match.type(), + Match.when(Match.instanceOf(Uint8Array), (v) => { + // @tstyche if { target: ">=5.7" } -- Before TypeScript 5.7, 'Uint8Array' was not generic + expect(v).type.toBe>() + // @tstyche if { target: "<5.7" } + expect(v).type.toBe() + return "uint8" + }), + Match.when(Match.instanceOf(Uint16Array), (v) => { + // @tstyche if { target: ">=5.7" } -- Before TypeScript 5.7, 'Uint16Array' was not generic + expect(v).type.toBe>() + // @tstyche if { target: "<5.7" } + expect(v).type.toBe() + return "uint16" + }), + Match.orElse((v) => { + // @tstyche if { target: ">=5.7" } -- Before TypeScript 5.7, 'Uint8Array' and 'Uint16Array' were not generic + expect(v).type.toBe | Uint16Array>() + // @tstyche if { target: "<5.7" } + expect(v).type.toBe() + return "a" + }) + ) + + expect(match(new Uint8Array())).type.toBe() + expect(match(new Uint16Array())).type.toBe() + }) + + it("instanceOf prop", () => { + class Test {} + expect( + pipe( + Match.value<{ test: Test | null }>({ test: new Test() }), + Match.when({ test: Match.instanceOf(Test) }, ({ test }) => { + expect(test).type.toBe() + return 1 + }), + Match.orElse(({ test }) => { + expect(test).type.toBe() + return 0 + }) + ) + ).type.toBe() + }) + + it("refinement with unknown", () => { + const isArray = (_: unknown): _ is ReadonlyArray => Array.isArray(_) + expect( + pipe( + Match.value(hole>()), + Match.when(isArray, (v) => { + expect(v).type.toBe>() + return "array" + }), + Match.when(Predicate.isString, (v) => { + expect(v).type.toBe() + return "string" + }), + Match.exhaustive + ) + ).type.toBe() + }) + + it("refinement nested with unknown", () => { + expect( + pipe( + Match.value(hole<{ readonly a: string | Array }>()), + Match.when({ a: isArray }, (v) => { + expect(v).type.toBe<{ a: Array }>() + return "array" + }), + Match.orElse((v) => { + expect(v).type.toBe<{ readonly a: string | Array }>() + return "fail" + }) + ) + ).type.toBe() + }) + + it("unknown refinement", () => { + expect( + pipe( + Match.value(hole()), + Match.when(Predicate.isReadonlyRecord, (v) => { + expect(v).type.toBe<{ readonly [x: string]: unknown; readonly [x: symbol]: unknown }>() + return "record" + }), + Match.orElse(() => "unknown") + ) + ).type.toBe() + }) + + it("any refinement", () => { + expect( + pipe( + Match.value(hole()), + Match.when(Predicate.isReadonlyRecord, (v) => { + expect(v).type.toBe<{ readonly [x: string]: unknown; readonly [x: symbol]: unknown }>() + return "record" + }), + Match.orElse(() => "unknown") + ) + ).type.toBe() + }) + + it("pattern type is not fixed by the function argument type", () => { + type T = + | { resolveType: "A"; value: number } + | { resolveType: "B"; value: number } + | { resolveType: "C"; value: number } + const doStuff = (x: { value: number }) => x + expect( + pipe( + Match.value(hole()), + Match.when({ resolveType: Match.is("A", "B") }, doStuff), + Match.not({ resolveType: Match.is("A", "B") }, doStuff), + Match.exhaustive + ) + ).type.toBe<{ value: number }>() + }) + + it("non literal refinement", () => { + const a: number = 1 + const b: string = "b" + expect( + Match.value(hole<{ a: number; b: string }>()).pipe( + Match.when({ a, b }, (v) => { + expect(v).type.toBe<{ a: number; b: string }>() + return "ok" + }), + Match.either + ) + ).type.toBe>() + }) + }) + + it("valueTags", () => { + expect( + pipe( + value, + Match.valueTags({ + A: (A) => { + expect(A).type.toBe<{ _tag: "A"; a: number }>() + return A.a + }, + B: (B) => { + expect(B).type.toBe<{ _tag: "B"; b: number }>() + return "B" + } + }) + ) + ).type.toBe() + + expect( + Match.valueTags(value, { + A: (A) => { + expect(A).type.toBe<{ _tag: "A"; a: number }>() + return A.a + }, + B: (B) => { + expect(B).type.toBe<{ _tag: "B"; b: number }>() + return "B" + } + }) + ).type.toBe() + + pipe( + value, + Match.valueTags({ + A: (_A) => _A.a, + B: () => "B", + // @ts-expect-error: Type '() => boolean' is not assignable to type 'never' + C: () => false + }) + ) + + Match.valueTags(value, { + A: (_A) => _A.a, + B: () => "B", + // @ts-expect-error: Type '() => boolean' is not assignable to type 'never' + C: () => false + }) + }) + + it("typeTags", () => { + expect( + Match.typeTags()({ + A: (A) => { + expect(A).type.toBe<{ _tag: "A"; a: number }>() + return A.a + }, + B: (B) => { + expect(B).type.toBe<{ _tag: "B"; b: number }>() + return "B" + } + })(value) + ).type.toBe() + + expect( + Match.typeTags()({ + A: (A) => { + expect(A).type.toBe<{ _tag: "A"; a: number }>() + return A.a + }, + B: (B) => { + expect(B).type.toBe<{ _tag: "B"; b: number }>() + return "B" + } + })(value) + ).type.toBe() + + Match.typeTags()({ + A: (_) => _.a, + B: () => "B", + // @ts-expect-error: Type '() => boolean' is not assignable to type 'never' + C: () => false + })(value) + + Match.typeTags()({ + // @ts-expect-error: Type 'number' is not assignable to type 'string' + A: (_) => _.a, + B: () => "B", + // @ts-expect-error: Type '() => boolean' is not assignable to type 'never' + C: () => false + })(value) + }) + + it("discriminators", () => { + expect( + pipe( + Match.type(), + Match.discriminators("_tag")({ + A: (A) => { + expect(A).type.toBe<{ _tag: "A"; a: number }>() + return A.a + }, + B: (B) => { + expect(B).type.toBe<{ _tag: "B"; b: number }>() + return "B" + } + }), + Match.exhaustive + )(value) + ).type.toBe() + + pipe( + Match.type(), + Match.discriminators("_tag")({ + A: (_) => _.a, + B: () => "B", + // @ts-expect-error: Type '() => boolean' is not assignable to type 'never' + C: () => false + }), + Match.exhaustive + )(value) + }) + + it("discriminatorsExhaustive", () => { + expect( + pipe( + Match.type(), + Match.discriminatorsExhaustive("_tag")({ + A: (A) => { + expect(A).type.toBe<{ _tag: "A"; a: number }>() + return A.a + }, + B: (B) => { + expect(B).type.toBe<{ _tag: "B"; b: number }>() + return "B" + } + }) + )(value) + ).type.toBe() + + pipe( + Match.type(), + Match.discriminatorsExhaustive("_tag")({ + A: (_) => _.a, + B: () => "B", + // @ts-expect-error: Type '() => boolean' is not assignable to type 'never' + C: () => false + }) + )(value) + }) + + it("tags", () => { + expect( + pipe( + Match.type(), + Match.tags({ + A: (A) => { + expect(A).type.toBe<{ _tag: "A"; a: number }>() + return A.a + }, + B: (B) => { + expect(B).type.toBe<{ _tag: "B"; b: number }>() + return "B" + } + }), + Match.exhaustive + )(value) + ).type.toBe() + + pipe( + Match.type(), + Match.tags({ + A: (_) => _.a, + B: () => "B", + // @ts-expect-error: Type '() => boolean' is not assignable to type 'never' + C: () => false + }), + Match.exhaustive + )(value) + }) + + it("tagsExhaustive", () => { + expect( + pipe( + Match.type(), + Match.tagsExhaustive({ + A: (A) => { + expect(A).type.toBe<{ _tag: "A"; a: number }>() + return A.a + }, + B: (B) => { + expect(B).type.toBe<{ _tag: "B"; b: number }>() + return "B" + } + }) + )(value) + ).type.toBe() + + pipe( + Match.type(), + Match.tagsExhaustive({ + A: (_) => _.a, + B: () => "B", + // @ts-expect-error: Type '() => boolean' is not assignable to type 'never' + C: () => false + }) + )(value) + }) + + it("tag", () => { + expect( + pipe( + Match.type(), + Match.tag("A", handlerA), + Match.orElse((B) => { + expect(B).type.toBe<{ _tag: "B"; b: number }>() + return B.b + }) + )(value) + ).type.toBe() + }) + + it("tagStartsWith", () => { + expect( + pipe( + Match.type(), + Match.tagStartsWith("A", handlerA), + Match.orElse((B) => { + expect(B).type.toBe<{ _tag: "B"; b: number }>() + return B.b + }) + )(value) + ).type.toBe() + }) + + it("Option.isSome", () => { + expect( + pipe( + Match.type<{ maybeNumber: Option.Option }>(), + Match.when({ maybeNumber: Option.isSome }, (v) => { + expect(v).type.toBe<{ maybeNumber: Option.Some }>() + return v.maybeNumber.value + }), + Match.orElse((B) => { + expect(B).type.toBe<{ maybeNumber: Option.Option }>() + return undefined + }) + )({ maybeNumber: Option.some(1) }) + ).type.toBe() + }) + + it("whenOr refinement with pattern", () => { + class Person { + get contactable() { + return true + } + } + expect( + pipe( + Match.type<{ maybeNumber: Option.Option; person: Person }>(), + Match.whenOr({ + maybeNumber: { + _tag: Match.is("Some", "None") + }, + person: { contactable: true } + }, ({ person }) => { + expect(person.contactable).type.toBe() + return person.contactable + }), + Match.orElse(({ person }) => { + expect(person).type.toBe() + return false + }) + )({ maybeNumber: Option.some(1), person: new Person() }) + ).type.toBe() + }) + + it(".is prop", () => { + Match.value<{ foo: string }>({ foo: "bar" }).pipe( + Match.when({ foo: Match.is("baz") }, (_) => { + expect(_).type.toBe<{ foo: "baz" }>() + return true + }), + Match.when({ foo: (s): s is "baz" => s === "baz" }, (_) => { + expect(_).type.toBe<{ foo: "baz" }>() + return true + }), + Match.orElse((_) => { + expect(_).type.toBe<{ foo: string }>() + return true + }) + ) + }) +}) diff --git a/repos/effect/packages/effect/dtslint/Micro.tst.ts b/repos/effect/packages/effect/dtslint/Micro.tst.ts new file mode 100644 index 0000000..be44db8 --- /dev/null +++ b/repos/effect/packages/effect/dtslint/Micro.tst.ts @@ -0,0 +1,25 @@ +import { hole, Micro } from "effect" +import { describe, expect, it } from "tstyche" + +describe("Micro", () => { + it("catchCauseIf", () => { + expect( + hole>().pipe(Micro.catchCauseIf( + (cause): cause is Micro.MicroCause => true, + (cause) => { + expect(cause).type.toBe>() + return hole>() + } + )) + ).type.toBe>() + + expect(Micro.catchCauseIf( + hole>(), + (cause): cause is Micro.MicroCause => true, + (cause) => { + expect(cause).type.toBe>() + return hole>() + } + )).type.toBe>() + }) +}) diff --git a/repos/effect/packages/effect/dtslint/Number.tst.ts b/repos/effect/packages/effect/dtslint/Number.tst.ts new file mode 100644 index 0000000..2d8994a --- /dev/null +++ b/repos/effect/packages/effect/dtslint/Number.tst.ts @@ -0,0 +1,355 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import type { Option } from "effect" +import { HashSet, Number, pipe } from "effect" +import { describe, expect, it } from "tstyche" + +describe("Number", () => { + const a = 10 + const b = -5 + + it("isNumber", () => { + const value: unknown = 42 + expect(value).type.not.toBeAssignableTo() + + if (Number.isNumber(value)) { + expect(value).type.toBe() + } + + // Type guard should properly narrow union types + const numOrString: number | string = 123 + if (Number.isNumber(numOrString)) { + expect(numOrString).type.toBe() + expect(numOrString).type.not.toBeAssignableFrom() + } + }) + + it("sum", () => { + const dataLast = Number.sum(a) + type DataLast = typeof dataLast + type DataFirst = typeof Number.sum + + // test the input type + expect>().type.toBeAssignableFrom<[number, number]>() + expect>().type.toBeAssignableFrom<[number]>() + + // test the output type + expect(Number.sum(a, b)).type.toBe() + expect(pipe(a, Number.sum(b))).type.toBe() + }) + + it("subtract", () => { + const dataLast = Number.subtract(a) + type DataLast = typeof dataLast + type DataFirst = typeof Number.subtract + + // test the input type + expect>().type.toBeAssignableFrom<[number, number]>() + expect>().type.toBeAssignableFrom<[number]>() + + // test the output type + expect(Number.subtract(a, b)).type.toBe() + expect(pipe(a, Number.subtract(b))).type.toBe() + }) + + it("multiply", () => { + const dataLast = Number.multiply(a) + type DataLast = typeof dataLast + type DataFirst = typeof Number.multiply + + // test the input type + expect>().type.toBeAssignableFrom<[number, number]>() + + expect>().type.toBeAssignableFrom<[number]>() + + // test the output type + expect(Number.multiply(a, b)).type.toBe() + expect(pipe(a, Number.multiply(b))).type.toBe() + }) + + it("divide", () => { + const dataLast = Number.divide(a) + type DataLast = typeof dataLast + type DataFirst = typeof Number.divide + + // test the input type + expect>().type.toBeAssignableFrom<[number, number]>() + + expect>().type.toBeAssignableFrom<[number]>() + + // test the output type + expect(Number.divide(a, b)).type.toBe>() + expect(pipe(a, Number.divide(b))).type.toBe>() + }) + + it("unsafeDivide", () => { + const dataLast = Number.unsafeDivide(a) + type DataLast = typeof dataLast + type DataFirst = typeof Number.unsafeDivide + + // test the input type + expect>().type.toBeAssignableFrom<[number, number]>() + + expect>().type.toBeAssignableFrom<[number]>() + + // test the output type + expect(Number.unsafeDivide(a, b)).type.toBe() + expect(pipe(a, Number.unsafeDivide(b))).type.toBe() + }) + + it("increment", () => { + type DataFirst = typeof Number.increment + + // test the input type + expect>().type.toBeAssignableFrom<[number]>() + + // test the output type + expect(Number.increment(a)).type.toBe() + expect(pipe(a, Number.increment)).type.toBe() + }) + + it("decrement", () => { + type DataFirst = typeof Number.decrement + + // test the input type + expect>().type.toBeAssignableFrom<[number]>() + + // test the output type + expect(Number.decrement(a)).type.toBe() + expect(pipe(a, Number.decrement)).type.toBe() + }) + + it("Equivalence", () => { + type DataFirst = typeof Number.Equivalence + + // test the input type + expect>().type.toBeAssignableFrom<[number, number]>() + + // test the output type + expect(Number.Equivalence(a, b)).type.toBe() + }) + + it("Order", () => { + type DataFirst = typeof Number.Order + + // test the input type + expect>().type.toBeAssignableFrom<[number, number]>() + + // test the output type + expect(Number.Order(a, b)).type.toBe<-1 | 0 | 1>() + }) + + it("lessThan", () => { + const dataLast = Number.lessThan(a) + type DataLast = typeof dataLast + type DataFirst = typeof Number.lessThan + + // test the input type + expect>().type.toBeAssignableFrom<[number, number]>() + expect>().type.toBeAssignableFrom<[number]>() + + // test the output type + expect(Number.lessThan(a, b)).type.toBe() + expect(pipe(a, Number.lessThan(b))).type.toBe() + }) + + it("lessThanOrEqualTo", () => { + const dataLast = Number.lessThanOrEqualTo(a) + type DataLast = typeof dataLast + type DataFirst = typeof Number.lessThanOrEqualTo + + // test the input type + expect>().type.toBeAssignableFrom<[number, number]>() + + expect>().type.toBeAssignableFrom<[number]>() + + // test the output type + expect(Number.lessThanOrEqualTo(a, b)).type.toBe() + expect(pipe(a, Number.lessThanOrEqualTo(b))).type.toBe() + }) + + it("greaterThan", () => { + const dataLast = Number.greaterThan(a) + type DataLast = typeof dataLast + type DataFirst = typeof Number.greaterThan + + // test the input type + expect>().type.toBeAssignableFrom<[number, number]>() + + expect>().type.toBeAssignableFrom<[number]>() + + // test the output type + expect(Number.greaterThan(a, b)).type.toBe() + expect(pipe(a, Number.greaterThan(b))).type.toBe() + }) + + it("greaterThanOrEqualTo", () => { + const dataLast = Number.greaterThanOrEqualTo(a) + type DataLast = typeof dataLast + type DataFirst = typeof Number.greaterThanOrEqualTo + + // test the input type + expect>().type.toBeAssignableFrom<[number, number]>() + + expect>().type.toBeAssignableFrom<[number]>() + + // test the output type + expect(Number.greaterThanOrEqualTo(a, b)).type.toBe() + expect(pipe(a, Number.greaterThanOrEqualTo(b))).type.toBe() + }) + + it("between", () => { + const options = { minimum: a, maximum: b } + + const dataLast = Number.between(options) + type DataLast = typeof dataLast + type DataFirst = typeof Number.between + + // test the input type + expect>().type.toBeAssignableFrom< + [number, { minimum: number; maximum: number }] + >() + + expect>().type.toBeAssignableFrom<[number]>() + + // test the output type + expect(Number.between(a, options)).type.toBe() + expect(pipe(a, Number.between(options))).type.toBe() + }) + + it("clamp", () => { + const options = { minimum: a, maximum: b } + + const dataLast = Number.clamp(options) + type DataLast = typeof dataLast + type DataFirst = typeof Number.clamp + + // test the input type + expect>().type.toBeAssignableFrom< + [number, { minimum: number; maximum: number }] + >() + + expect>().type.toBeAssignableFrom<[number]>() + + // test the output type + expect(Number.clamp(a, options)).type.toBe() + expect(pipe(a, Number.clamp(options))).type.toBe() + }) + + it("min", () => { + const dataLast = Number.min(a) + type DataLast = typeof dataLast + type DataFirst = typeof Number.min + + // test the input type + expect>().type.toBeAssignableFrom<[number, number]>() + + expect>().type.toBeAssignableFrom<[number]>() + + // test the output type + expect(Number.min(a, b)).type.toBe() + expect(pipe(a, Number.min(b))).type.toBe() + }) + + it("max", () => { + const dataLast = Number.max(a) + type DataLast = typeof dataLast + type DataFirst = typeof Number.max + + // test the input type + expect>().type.toBeAssignableFrom<[number, number]>() + + expect>().type.toBeAssignableFrom<[number]>() + + // test the output type + expect(Number.max(a, b)).type.toBe() + expect(pipe(a, Number.max(b))).type.toBe() + }) + + it("sign", () => { + type DataFirst = typeof Number.sign + + // test the input type + expect>().type.toBeAssignableFrom<[number]>() + + // test the output type + expect(Number.sign(a)).type.toBe<-1 | 0 | 1>() + }) + + it("sumAll", () => { + type DataFirst = typeof Number.sumAll + + // test the input type + expect>().type.toBeAssignableFrom< + [Iterable] + >() + + // test the output type + expect(Number.sumAll([a, b, a, b, a, b])).type.toBe() + expect(Number.sumAll(HashSet.make(a, b, a, b, a, b))).type.toBe() + }) + + it("multiplyAll", () => { + type DataFirst = typeof Number.multiplyAll + + // test the input type + expect>().type.toBeAssignableFrom< + [Iterable] + >() + + // test the output type + expect(Number.multiplyAll([a, b, a, b, a, b])).type.toBe() + expect( + Number.multiplyAll(HashSet.make(a, b, a, b, a, b)) + ).type.toBe() + }) + + it("remainder", () => { + const dataLast = Number.remainder(a) + type DataLast = typeof dataLast + type DataFirst = typeof Number.remainder + + // test the input type + expect>().type.toBeAssignableFrom<[number, number]>() + + expect>().type.toBeAssignableFrom<[number]>() + + // test the output type + expect(Number.remainder(a, b)).type.toBe() + expect(pipe(a, Number.remainder(b))).type.toBe() + }) + + it("nextPow2", () => { + type DataFirst = typeof Number.nextPow2 + + // test the input type + expect>().type.toBeAssignableFrom<[number]>() + + // test the output type + expect(Number.nextPow2(a)).type.toBe() + }) + + it("parse", () => { + type DataFirst = typeof Number.parse + + // test the input type + expect>().type.toBeAssignableFrom<[string]>() + + // test the output type + expect(Number.parse("123")).type.toBe>() + }) + + it("round", () => { + const dataLast = Number.round(2) + type DataLast = typeof dataLast + type DataFirst = typeof Number.round + + // test the input type + expect>().type.toBeAssignableFrom<[number, number]>() + + expect>().type.toBeAssignableFrom<[number]>() + + // test the output type + expect(Number.round(a, 2)).type.toBe() + expect(pipe(a, Number.round(2))).type.toBe() + }) +}) diff --git a/repos/effect/packages/effect/dtslint/Option.tst.ts b/repos/effect/packages/effect/dtslint/Option.tst.ts new file mode 100644 index 0000000..675de92 --- /dev/null +++ b/repos/effect/packages/effect/dtslint/Option.tst.ts @@ -0,0 +1,221 @@ +import { hole, Option, pipe, Predicate } from "effect" +import { describe, expect, it } from "tstyche" + +declare const number: Option.Option +declare const string: Option.Option +declare const numberOrString: Option.Option + +declare const primitiveNumber: number +declare const primitiveNumberOrString: string | number +declare const predicateNumbersOrStrings: Predicate.Predicate + +describe("Option", () => { + it("liftPredicate", () => { + expect( + Option.liftPredicate(primitiveNumberOrString, Predicate.isString) + ).type.toBe>() + expect( + pipe(primitiveNumberOrString, Option.liftPredicate(Predicate.isString)) + ).type.toBe>() + + expect( + Option.liftPredicate( + primitiveNumberOrString, + (n): n is number => { + expect(n).type.toBe() + return typeof n === "number" + } + ) + ).type.toBe>() + expect( + pipe( + primitiveNumberOrString, + Option.liftPredicate( + (n): n is number => { + expect(n).type.toBe() + return typeof n === "number" + } + ) + ) + ).type.toBe>() + + expect( + Option.liftPredicate(primitiveNumber, predicateNumbersOrStrings) + ).type.toBe>() + expect( + pipe(primitiveNumber, Option.liftPredicate(predicateNumbersOrStrings)) + ).type.toBe>() + }) + + it("getOrElse", () => { + expect(Option.getOrElse(Option.some("a"), () => null)).type.toBe() + expect(pipe(Option.some("a"), Option.getOrElse(() => null))).type.toBe() + }) + + it("filter", () => { + expect(Option.filter(number, predicateNumbersOrStrings)).type.toBe>() + expect(pipe(number, Option.filter(predicateNumbersOrStrings))).type.toBe>() + + expect(pipe(numberOrString, Option.filter(Predicate.isString))).type.toBe>() + expect(Option.filter(numberOrString, Predicate.isString)).type.toBe>() + + expect( + Option.filter(number, (value) => { + expect(value).type.toBe() + return true + }) + ).type.toBe>() + expect( + pipe( + number, + Option.filter((value) => { + expect(value).type.toBe() + return true + }) + ) + ).type.toBe>() + }) + + describe("all", () => { + it("tuple", () => { + expect(Option.all([])).type.toBe>() + expect(Option.all([Option.some(1)])).type.toBe>() + expect(Option.all([Option.some(1), Option.some("b")])).type.toBe>() + expect(pipe([Option.some(1), Option.some("b")] as const, Option.all)).type.toBe>() + }) + + it("struct", () => { + expect(Option.all({})).type.toBe>() + expect(Option.all({ a: Option.some(1) })).type.toBe>() + expect(Option.all({ a: Option.some(1), b: Option.some("b") })) + .type.toBe>() + expect(pipe({ a: Option.some(1), b: Option.some("b") }, Option.all)) + .type.toBe>() + }) + + it("array", () => { + const optionArray = hole>>() + expect(Option.all(optionArray)).type.toBe>>() + expect(pipe(optionArray, Option.all)).type.toBe>>() + }) + + it("record", () => { + const optionRecord = hole>>() + expect(Option.all(optionRecord)).type.toBe>() + expect(pipe(optionRecord, Option.all)).type.toBe>() + }) + }) + + it("exists", () => { + if (Option.exists(Predicate.isString)(numberOrString)) { + expect(numberOrString).type.toBe>() + } + if (Option.exists(numberOrString, Predicate.isString)) { + expect(numberOrString).type.toBe>() + } + + expect( + Option.exists(number, (value) => { + expect(value).type.toBe() + return true + }) + ).type.toBe() + expect( + pipe( + number, + Option.exists((value) => { + expect(value).type.toBe() + return true + }) + ) + ).type.toBe() + }) + + it("andThen", () => { + expect(Option.andThen(numberOrString, numberOrString)) + .type.toBe>() + expect(Option.andThen(numberOrString, () => numberOrString)) + .type.toBe>() + expect(numberOrString.pipe(Option.andThen(numberOrString))) + .type.toBe>() + expect(numberOrString.pipe(Option.andThen(() => numberOrString))) + .type.toBe>() + }) + + it("Option.Value type helper", () => { + type V = Option.Option.Value + expect().type.toBe() + }) + + it("do notation", () => { + expect( + pipe( + Option.Do, + Option.bind("a", (scope) => { + expect(scope).type.toBe<{}>() + return Option.some(1) + }), + Option.bind("b", (scope) => { + expect(scope).type.toBe<{ a: number }>() + return Option.some("b") + }), + Option.let("c", (scope) => { + expect(scope).type.toBe<{ a: number; b: string }>() + return true + }) + ) + ).type.toBe>() + + expect( + pipe( + Option.some(1), + Option.bindTo("a"), + Option.bind("b", (scope) => { + expect(scope).type.toBe<{ a: number }>() + return Option.some("b") + }), + Option.let("c", (scope) => { + expect(scope).type.toBe<{ a: number; b: string }>() + return true + }) + ) + ).type.toBe>() + }) + + describe("firstSomeOf", () => { + it("should error for invalid type parameter", () => { + expect(Option.firstSomeOf).type.not.toBeCallableWith( + [number, string] + ) + expect(pipe).type.not.toBeCallableWith( + [number, string], + Option.firstSomeOf + ) + }) + + it("should work for heterogeneous usage", () => { + expect(Option.firstSomeOf([number, string])).type.toBe>() + expect(pipe([number, string], Option.firstSomeOf)).type.toBe>() + }) + + it("should work for heterogeneous usage of iterable union", () => { + expect( + Option.firstSomeOf( + hole< + | Iterable> + | [Option.Option] + >() + ) + ).type.toBe>() + expect( + pipe( + hole< + | Iterable> + | [Option.Option] + >(), + Option.firstSomeOf + ) + ).type.toBe>() + }) + }) +}) diff --git a/repos/effect/packages/effect/dtslint/ParseResult.tst.ts b/repos/effect/packages/effect/dtslint/ParseResult.tst.ts new file mode 100644 index 0000000..8363a01 --- /dev/null +++ b/repos/effect/packages/effect/dtslint/ParseResult.tst.ts @@ -0,0 +1,11 @@ +import type * as ParseResult from "effect/ParseResult" +import { describe, expect, it } from "tstyche" + +declare const issue: ParseResult.ParseIssue + +describe("ParseResult", () => { + it("should always have an `actual` field", () => { + expect(issue.actual) + .type.toBe() + }) +}) diff --git a/repos/effect/packages/effect/dtslint/Predicate.tst.ts b/repos/effect/packages/effect/dtslint/Predicate.tst.ts new file mode 100644 index 0000000..2cba23f --- /dev/null +++ b/repos/effect/packages/effect/dtslint/Predicate.tst.ts @@ -0,0 +1,260 @@ +import { hole, pipe, Predicate } from "effect" +import { describe, expect, it } from "tstyche" + +declare const u: unknown +declare const anys: ReadonlyArray +declare const unknowns: ReadonlyArray +declare const numberOrNull: ReadonlyArray +declare const numberOrUndefined: ReadonlyArray +declare const numberOrNullOrUndefined: ReadonlyArray + +describe("Predicate", () => { + it("isString", () => { + expect(unknowns.filter(Predicate.isString)).type.toBe>() + }) + + it("isNumber", () => { + expect(unknowns.filter(Predicate.isNumber)).type.toBe>() + }) + + it("isBoolean", () => { + expect(unknowns.filter(Predicate.isBoolean)).type.toBe>() + }) + + it("isBigInt", () => { + expect(unknowns.filter(Predicate.isBigInt)).type.toBe>() + }) + + it("isSymbol", () => { + expect(unknowns.filter(Predicate.isSymbol)).type.toBe>() + }) + + it("isUndefined", () => { + expect(unknowns.filter(Predicate.isUndefined)).type.toBe>() + }) + + it("isNotUndefined", () => { + expect(numberOrUndefined.filter(Predicate.isNotUndefined)).type.toBe>() + expect(numberOrNullOrUndefined.filter(Predicate.isNotUndefined)).type.toBe>() + }) + + it("isNull", () => { + expect(unknowns.filter(Predicate.isNull)).type.toBe>() + }) + + it("isNotNull", () => { + expect(numberOrNull.filter(Predicate.isNotNull)).type.toBe>() + expect(numberOrNullOrUndefined.filter(Predicate.isNotNull)).type.toBe>() + }) + + it("isNever", () => { + expect(unknowns.filter(Predicate.isNever)).type.toBe>() + }) + + it("isUnknown", () => { + expect(anys.filter(Predicate.isUnknown)).type.toBe>() + }) + + it("isObject", () => { + expect(anys.filter(Predicate.isObject)).type.toBe>() + }) + + it("isTagged", () => { + expect(anys.filter(Predicate.isTagged("a"))).type.toBe>() + }) + + it("isNullable", () => { + expect(numberOrNull.filter(Predicate.isNullable)).type.toBe>() + expect(numberOrUndefined.filter(Predicate.isNullable)).type.toBe>() + expect(numberOrNullOrUndefined.filter(Predicate.isNullable)).type.toBe>() + + if (Predicate.isNullable(u)) { + expect(u).type.toBe() + } + }) + + it("isNotNullable", () => { + expect(numberOrNull.filter(Predicate.isNotNullable)).type.toBe>() + expect(numberOrUndefined.filter(Predicate.isNotNullable)).type.toBe>() + expect(numberOrNullOrUndefined.filter(Predicate.isNotNullable)).type.toBe>() + + if (Predicate.isNotNullable(u)) { + expect(u).type.toBe<{}>() + } + }) + + it("isError", () => { + expect(unknowns.filter(Predicate.isError)).type.toBe>() + }) + + it("isDate", () => { + expect(unknowns.filter(Predicate.isDate)).type.toBe>() + }) + + it("isRecord", () => { + expect(unknowns.filter(Predicate.isRecord)).type.toBe>() + }) + + it("isReadonlyRecord", () => { + expect(unknowns.filter(Predicate.isReadonlyRecord)).type.toBe< + Array<{ readonly [x: string]: unknown; readonly [x: symbol]: unknown }> + >() + }) + + it("isTupleOf", () => { + if (Predicate.isTupleOf(unknowns, 3)) { + expect(unknowns).type.toBe<[unknown, unknown, unknown]>() + } + }) + + it("isTupleOfAtLeast", () => { + if (Predicate.isTupleOfAtLeast(unknowns, 3)) { + expect(unknowns).type.toBe<[unknown, unknown, unknown, ...Array]>() + } + }) + + it("isRegExp", () => { + expect(unknowns.filter(Predicate.isRegExp)).type.toBe>() + }) + + it("compose", () => { + interface NonEmptyStringBrand { + readonly NonEmptyString: unique symbol + } + type NonEmptyString = string & NonEmptyStringBrand + const isNonEmptyString = hole>() + + expect(pipe(Predicate.isString, Predicate.compose(isNonEmptyString))) + .type.toBe>() + + expect(Predicate.compose(Predicate.isString, isNonEmptyString)) + .type.toBe>() + + expect( + pipe( + Predicate.isString, + Predicate.compose((s): s is NonEmptyString => { + expect(s).type.toBe() + return s.length > 0 + }) + ) + ).type.toBe>() + + expect(Predicate.compose(Predicate.isString, (s): s is NonEmptyString => { + expect(s).type.toBe() + return s.length > 0 + })) + .type.toBe>() + + expect(pipe(Predicate.isString, Predicate.compose((s) => /^a/.test(s)))) + .type.toBe>() + }) + + it("and", () => { + const isPositive = hole>() + const isLessThan2 = hole>() + + expect(pipe(isPositive, Predicate.and(isLessThan2))) + .type.toBe>() + + expect(Predicate.and(isPositive, isLessThan2)) + .type.toBe>() + + expect(pipe(Predicate.isNumber, Predicate.and(isPositive))) + .type.toBe>() + + expect(Predicate.and(Predicate.isNumber, isPositive)) + .type.toBe>() + + const hasa = hole>() + const hasb = hole>() + + expect(pipe(hasa, Predicate.and(hasb))) + .type.toBe>() + + expect(Predicate.and(hasa, hasb)) + .type.toBe>() + }) + + it("or", () => { + expect( + pipe( + hole>(), + Predicate.or(hole>()) + ) + ).type.toBe>() + + expect(Predicate.or(hole>(), hole>())) + .type.toBe>() + + expect(pipe(Predicate.isString, Predicate.or(Predicate.isNumber))) + .type.toBe>() + + expect(Predicate.or(Predicate.isString, Predicate.isNumber)) + .type.toBe>() + }) + + it("tuple", () => { + const isA = hole>() + const isTrue = hole>() + const isOdd = hole>() + + expect(Predicate.tuple(isTrue, isA)) + .type.toBe>() + + expect(Predicate.tuple(isTrue, isOdd)) + .type.toBe>() + + expect(Predicate.tuple(isOdd, isOdd)) + .type.toBe>() + + expect(Predicate.tuple(...hole>>())) + .type.toBe>>() + + expect(Predicate.tuple(...hole | Predicate.Refinement>>())) + .type.toBe, ReadonlyArray>>() + + expect(Predicate.tuple(...hole>>())) + .type.toBe, ReadonlyArray>>() + }) + + it("struct", () => { + expect( + Predicate.struct({ + a: hole>(), + true: hole>() + }) + ).type.toBe< + Predicate.Refinement< + { readonly a: string; readonly true: boolean }, + { readonly a: "a"; readonly true: true } + > + >() + + expect( + Predicate.struct({ + odd: hole>(), + true: hole>() + }) + ).type.toBe< + Predicate.Refinement< + { readonly odd: number; readonly true: boolean }, + { readonly odd: number; readonly true: true } + > + >() + + expect( + Predicate.struct({ + odd: hole>(), + odd1: hole>() + }) + ).type.toBe>() + }) + + it("isUint8Array", () => { + // @tstyche if { target: ">=5.7" } -- Before TypeScript 5.7, 'Uint8Array' was not generic + expect(unknowns.filter(Predicate.isUint8Array)).type.toBe>>() + // @tstyche if { target: "<5.7" } + expect(unknowns.filter(Predicate.isUint8Array)).type.toBe>() + }) +}) diff --git a/repos/effect/packages/effect/dtslint/Random.tst.ts b/repos/effect/packages/effect/dtslint/Random.tst.ts new file mode 100644 index 0000000..fb0fc51 --- /dev/null +++ b/repos/effect/packages/effect/dtslint/Random.tst.ts @@ -0,0 +1,23 @@ +import type { Array, Cause, Chunk, Effect } from "effect" +import { Random } from "effect" +import { describe, expect, it } from "tstyche" + +declare const array: Array +declare const nonEmptyArray: Array.NonEmptyArray + +declare const readonlyArray: Array +declare const nonEmptyReadonlyArray: Array.NonEmptyArray + +declare const chunk: Chunk.Chunk +declare const nonEmptyChunk: Chunk.NonEmptyChunk + +describe("Random", () => { + it("choice", () => { + expect(Random.choice(array)).type.toBe>() + expect(Random.choice(nonEmptyArray)).type.toBe>() + expect(Random.choice(readonlyArray)).type.toBe>() + expect(Random.choice(nonEmptyReadonlyArray)).type.toBe>() + expect(Random.choice(chunk)).type.toBe>() + expect(Random.choice(nonEmptyChunk)).type.toBe>() + }) +}) diff --git a/repos/effect/packages/effect/dtslint/Record.tst.ts b/repos/effect/packages/effect/dtslint/Record.tst.ts new file mode 100644 index 0000000..cfda39f --- /dev/null +++ b/repos/effect/packages/effect/dtslint/Record.tst.ts @@ -0,0 +1,529 @@ +import type { Brand } from "effect" +import { Either, hole, Option, pipe, Predicate, Record } from "effect" +import { describe, expect, it, when } from "tstyche" + +declare const string$numbers: Record +declare const string$numbersOrStrings: Record +declare const string$structAB: Record<"a" | "b", number> +declare const string$structCD: Record<"c" | "d", string> + +declare const predicateNumbersOrStrings: Predicate.Predicate + +const symA = Symbol.for("a") +const symB = Symbol.for("b") + +declare const symbol$numbers: Record +declare const template$numbers: Record<`a${string}`, number> + +describe("Record", () => { + it("NonLiteralKey", () => { + expect(hole>()).type.toBe() + expect(hole>()).type.toBe() + expect(hole>()).type.toBe() + expect(hole>()).type.toBe() + expect(hole>()).type.toBe() + expect(hole>()).type.toBe() + expect(hole>()).type.toBe() + expect(hole>()).type.toBe() + expect(hole>()).type.toBe() + expect(hole>()).type.toBe<`a${string}`>() + expect(hole>()).type.toBe<`${string}a`>() + expect(hole>()).type.toBe<`a${string}b${string}`>() + expect(hole>()).type.toBe<`a${number}`>() + expect(hole>()).type.toBe< + `a${number}b${string}c${number}` + >() + }) + + it("empty", () => { + expect(Record.empty()).type.not.toBeAssignableTo>() + + const empty1: Record = Record.empty() + expect(empty1).type.toBe>() + const empty2: Record = Record.empty() + expect(empty2).type.toBe>() + expect(Record.empty()).type.toBe>() + expect(Record.empty<"a">()).type.toBe>() + expect(Record.empty<`a${string}bc`>()).type.toBe>() + expect(Record.empty()).type.toBe>() + }) + + it("fromIterableWith and fromIterableBy", () => { + expect(Record.fromIterableWith([1, 2], (n) => { + expect(n).type.toBe() + return [String(n), n] + })).type.toBe>() + expect(Record.fromIterableWith([symA, symB], (s) => { + expect(s).type.toBe() + return [String(s), s] + })).type.toBe>() + expect(Record.fromIterableWith([1, symA], (ns) => { + expect(ns).type.toBe() + return [Predicate.isNumber(ns) ? String(ns) : ns, ns] + })).type.toBe>() + }) + + it("fromIterableBy", () => { + expect(Record.fromIterableBy([1, 2], (n) => { + expect(n).type.toBe() + return String(n) + })).type.toBe>() + expect(Record.fromIterableBy([symA, symB], (s) => { + expect(s).type.toBe() + return String(s) + })).type.toBe>() + expect(Record.fromIterableBy([1, symA], (ns) => { + expect(ns).type.toBe() + return Predicate.isNumber(ns) ? String(ns) : ns + })).type.toBe>() + }) + + it("fromEntries", () => { + expect(Record.fromEntries([["a", 1], ["b", 2]])).type.toBe>() + expect(Record.fromEntries([[symA, 1], [symB, 2]])).type.toBe>() + expect(Record.fromEntries([["a", 1], [symB, 2]])).type.toBe>() + }) + + it("collect", () => { + expect(Record.collect(string$numbers, (k, a) => { + expect(k).type.toBe() + expect(a).type.toBe() + return a + })).type.toBe>() + expect(pipe( + string$numbers, + Record.collect((k, a) => { + expect(k).type.toBe() + expect(a).type.toBe() + return a + }) + )).type.toBe>() + + expect(Record.collect(template$numbers, (k, a) => { + expect(k).type.toBe<`a${string}`>() + expect(a).type.toBe() + return a + })).type.toBe>() + expect(pipe( + template$numbers, + Record.collect((k, a) => { + expect(k).type.toBe<`a${string}`>() + expect(a).type.toBe() + return a + }) + )).type.toBe>() + + expect(Record.collect(string$structAB, (k, a) => { + expect(k).type.toBe<"a" | "b">() + expect(a).type.toBe() + return a + })).type.toBe>() + expect(pipe( + string$structAB, + Record.collect((k, a) => { + expect(k).type.toBe<"a" | "b">() + expect(a).type.toBe() + return a + }) + )).type.toBe>() + }) + + it("toEntries", () => { + expect(Record.toEntries(string$numbers)).type.toBe>() + expect(Record.toEntries(template$numbers)).type.toBe>() + expect(Record.toEntries(symbol$numbers)).type.toBe>() + // Testing with branded records + const brandedRecord = hole, number>>() + expect(Record.toEntries(brandedRecord)).type.toBe, number]>>() + expect(Record.toEntries(string$structAB)).type.toBe>() + }) + + it("has", () => { + expect(Record.has(string$numbers, "a")).type.toBe() + expect(pipe(string$numbers, Record.has("a"))).type.toBe() + expect(Record.has).type.not.toBeCallableWith(string$numbers, symA) + expect(Record.has(template$numbers, "a")).type.toBe() + expect(Record.has).type.not.toBeCallableWith(template$numbers, "b") + expect(Record.has(symbol$numbers, symA)).type.toBe() + expect(Record.has).type.not.toBeCallableWith(symbol$numbers, "a") + expect(Record.has).type.not.toBeCallableWith(string$structAB, "c") + expect(Record.has).type.not.toBeCallableWith(string$structAB, symA) + }) + + it("get", () => { + expect(Record.get(string$numbers, "a")).type.toBe>() + expect(pipe(string$numbers, Record.get("a"))).type.toBe>() + when(pipe).isCalledWith(string$numbers, expect(Record.get).type.not.toBeCallableWith(symA)) + expect(pipe(template$numbers, Record.get("a"))).type.toBe>() + when(pipe).isCalledWith(template$numbers, expect(Record.get).type.not.toBeCallableWith("b")) + expect(pipe(symbol$numbers, Record.get(symA))).type.toBe>() + when(pipe).isCalledWith(symbol$numbers, expect(Record.get).type.not.toBeCallableWith("a")) + expect(pipe(string$structAB, Record.get("a"))).type.toBe>() + when(pipe).isCalledWith(string$structAB, expect(Record.get).type.not.toBeCallableWith("c")) + }) + + it("modify, modifyOption, and replaceOption", () => { + expect(pipe(string$numbers, Record.modify("a", () => 2))).type.toBe>() + expect(pipe(string$numbers, Record.modify("a", () => true))).type.toBe>() + expect(pipe(template$numbers, Record.modify("a", () => 2))).type.toBe>() + expect(pipe(template$numbers, Record.modify("a", () => true))).type.toBe< + Record<`a${string}`, number | boolean> + >() + when(pipe).isCalledWith(template$numbers, expect(Record.modify).type.not.toBeCallableWith("b", () => true)) + expect(pipe(symbol$numbers, Record.modify(symA, () => 2))).type.toBe>() + expect(pipe(symbol$numbers, Record.modify(symA, () => true))).type.toBe>() + expect(pipe(string$structAB, Record.modify("a", () => 2))).type.toBe>() + expect(pipe(string$structAB, Record.modify("a", () => true))).type.toBe< + Record<"a" | "b", number | boolean> + >() + }) + + it("modifyOption", () => { + expect(pipe(string$numbers, Record.modifyOption("a", () => 2))) + .type.toBe>>() + expect(pipe(string$numbers, Record.modifyOption("a", () => true))) + .type.toBe>>() + expect(pipe(template$numbers, Record.modifyOption("a", () => 2))) + .type.toBe>>() + expect(pipe(template$numbers, Record.modifyOption("a", () => true))) + .type.toBe>>() + when(pipe).isCalledWith( + template$numbers, + expect(Record.modifyOption).type.not.toBeCallableWith("b", () => true) + ) + expect(pipe(symbol$numbers, Record.modifyOption(symA, () => 2))) + .type.toBe>>() + expect(pipe(symbol$numbers, Record.modifyOption(symA, () => true))) + .type.toBe>>() + expect(pipe(string$structAB, Record.modifyOption("a", () => 2))) + .type.toBe>>() + expect(pipe(string$structAB, Record.modifyOption("a", () => true))) + .type.toBe>>() + }) + + it("replaceOption", () => { + expect(pipe(string$numbers, Record.replaceOption("a", 2))) + .type.toBe>>() + expect(pipe(string$numbers, Record.replaceOption("a", true))) + .type.toBe>>() + expect(pipe(template$numbers, Record.replaceOption("a", 2))) + .type.toBe>>() + expect(pipe(template$numbers, Record.replaceOption("a", true))) + .type.toBe>>() + when(pipe).isCalledWith(template$numbers, expect(Record.replaceOption).type.not.toBeCallableWith("b", true)) + expect(pipe(symbol$numbers, Record.replaceOption(symA, 2))) + .type.toBe>>() + expect(pipe(symbol$numbers, Record.replaceOption(symA, true))) + .type.toBe>>() + expect(pipe(string$structAB, Record.replaceOption("a", 2))) + .type.toBe>>() + expect(pipe(string$structAB, Record.replaceOption("a", true))) + .type.toBe>>() + }) + + it("remove", () => { + expect(pipe(string$numbers, Record.remove("a"))).type.toBe>() + expect(pipe(template$numbers, Record.remove("a"))).type.toBe>() + when(pipe).isCalledWith(template$numbers, expect(Record.remove).type.not.toBeCallableWith("b")) + expect(pipe(symbol$numbers, Record.remove(symA))).type.toBe>() + expect(pipe(string$structAB, Record.remove("a"))).type.toBe>() + }) + + it("pop", () => { + expect(pipe(string$numbers, Record.pop("a"))).type.toBe]>>() + expect(pipe(template$numbers, Record.pop("a"))).type.toBe< + Option.Option<[number, Record<`a${string}`, number>]> + >() + when(pipe).isCalledWith(template$numbers, expect(Record.pop).type.not.toBeCallableWith("b")) + expect(pipe(symbol$numbers, Record.pop(symA))).type.toBe]>>() + expect(pipe(string$structAB, Record.pop("a"))).type.toBe]>>() + }) + + it("map", () => { + expect(Record.map(string$numbers, (v, k) => { + expect(v).type.toBe() + expect(k).type.toBe() + return v > 0 + })).type.toBe>() + expect(pipe( + string$numbers, + Record.map((v, k) => { + expect(v).type.toBe() + expect(k).type.toBe() + return v > 0 + }) + )).type.toBe>() + expect(Record.map(template$numbers, (v, k) => { + expect(v).type.toBe() + expect(k).type.toBe<`a${string}`>() + return v > 0 + })).type.toBe>() + expect(Record.map(symbol$numbers, (v, k) => { + expect(v).type.toBe() + expect(k).type.toBe() + return v + 1 + })).type.toBe>() + expect(Record.map(string$structAB, (v, k) => { + expect(v).type.toBe() + expect(k).type.toBe<"a" | "b">() + return v > 0 + })).type.toBe>() + expect(pipe( + string$structAB, + Record.map((v, k) => { + expect(v).type.toBe() + expect(k).type.toBe<"a" | "b">() + return v > 0 + }) + )).type.toBe>() + }) + + it("filterMap", () => { + expect(Record.filterMap(string$numbers, (v, k) => { + expect(v).type.toBe() + expect(k).type.toBe() + return v > 0 ? Option.some("positive") : Option.none() + })) + .type.toBe>() + expect(Record.filterMap(template$numbers, (v, k) => { + expect(v).type.toBe() + expect(k).type.toBe<`a${string}`>() + return v > 0 ? Option.some("positive") : Option.none() + })) + .type.toBe>() + expect(Record.filterMap(symbol$numbers, (v, k) => { + expect(v).type.toBe() + expect(k).type.toBe() + return v > 0 ? Option.some("positive") : Option.none() + })) + .type.toBe>() + expect(pipe( + string$structAB, + Record.filterMap((v, k) => { + expect(v).type.toBe() + expect(k).type.toBe<"a" | "b">() + return k === "a" ? Option.some(v) : Option.none() + }) + )) + .type.toBe>() + expect(Record.filterMap(string$structAB, (v, k) => { + expect(v).type.toBe() + expect(k).type.toBe<"a" | "b">() + return k === "a" ? Option.some(v) : Option.none() + })) + .type.toBe>() + }) + + it("filter", () => { + expect(Record.filter(string$numbers, (v, k) => { + expect(v).type.toBe() + expect(k).type.toBe() + return v > 0 + })).type.toBe>() + expect(Record.filter(template$numbers, (v, k) => { + expect(v).type.toBe() + expect(k).type.toBe<`a${string}`>() + return v > 0 + })).type.toBe>() + expect(pipe( + string$structAB, + Record.filter((v, k) => { + expect(v).type.toBe() + expect(k).type.toBe<"a" | "b">() + return k === "a" + }) + )) + .type.toBe>() + expect(Record.filter(string$structAB, (v, k) => { + expect(v).type.toBe() + expect(k).type.toBe<"a" | "b">() + return k === "a" + })) + .type.toBe>() + expect(Record.filter(string$numbersOrStrings, predicateNumbersOrStrings)) + .type.toBe>() + expect(Record.filter(string$numbers, predicateNumbersOrStrings)) + .type.toBe>() + expect(pipe(string$numbersOrStrings, Record.filter(predicateNumbersOrStrings))) + .type.toBe>() + expect(pipe(string$numbers, Record.filter(predicateNumbersOrStrings))) + .type.toBe>() + expect(Record.filter(string$numbersOrStrings, Predicate.isNumber)) + .type.toBe>() + expect(pipe(string$numbersOrStrings, Record.filter(Predicate.isNumber))) + .type.toBe>() + }) + + it("partitionMap", () => { + expect( + Record.partitionMap(string$numbers, (v, k) => { + expect(v).type.toBe() + expect(k).type.toBe() + return v > 0 ? Either.right("positive") : Either.left(false) + }) + ).type.toBe<[left: Record, right: Record]>() + expect( + pipe( + string$structAB, + Record.partitionMap((v, k) => { + expect(v).type.toBe() + expect(k).type.toBe<"a" | "b">() + return k === "a" ? Either.right("positive") : Either.left(false) + }) + ) + ).type.toBe<[left: Record, right: Record]>() + expect( + Record.partitionMap(string$structAB, (v, k) => { + expect(v).type.toBe() + expect(k).type.toBe<"a" | "b">() + return k === "a" ? Either.right("positive") : Either.left(false) + }) + ).type.toBe<[left: Record, right: Record]>() + }) + + it("partition", () => { + expect(Record.partition(string$numbers, (v, k) => { + expect(v).type.toBe() + expect(k).type.toBe() + return v > 0 + })) + .type.toBe<[excluded: Record, satisfying: Record]>() + expect(pipe( + string$structAB, + Record.partition((v, k) => { + expect(v).type.toBe() + expect(k).type.toBe<"a" | "b">() + return k === "a" + }) + )) + .type.toBe<[excluded: Record, satisfying: Record]>() + expect(Record.partition(string$structAB, (v, k) => { + expect(v).type.toBe() + expect(k).type.toBe<"a" | "b">() + return k === "a" + })) + .type.toBe<[excluded: Record, satisfying: Record]>() + expect(Record.partition(string$numbersOrStrings, predicateNumbersOrStrings)) + .type.toBe< + [excluded: Record, satisfying: Record] + >() + expect(pipe(string$numbersOrStrings, Record.partition(predicateNumbersOrStrings))) + .type.toBe< + [excluded: Record, satisfying: Record] + >() + expect(Record.partition(string$numbersOrStrings, Predicate.isNumber)) + .type.toBe<[excluded: Record, satisfying: Record]>() + expect(pipe(string$numbersOrStrings, Record.partition(Predicate.isNumber))) + .type.toBe<[excluded: Record, satisfying: Record]>() + }) + + it("keys", () => { + expect(Record.keys(string$structAB)).type.toBe>() + }) + + it("values", () => { + expect(Record.values(string$structAB)).type.toBe>() + }) + + it("set", () => { + expect(Record.set(string$numbers, "a", 2)).type.toBe>() + expect(Record.set(string$numbers, "a", true)).type.toBe>() + expect(Record.set(template$numbers, "a", 2)).type.toBe>() + expect(Record.set(template$numbers, "a", true)).type.toBe>() + expect(Record.set(template$numbers, "b", true)).type.toBe>() + expect(Record.set(string$structAB, "a", 2)).type.toBe>() + expect(Record.set(string$structAB, "a", true)).type.toBe>() + expect(Record.set(string$structAB, "c", true)).type.toBe>() + }) + + it("reduce", () => { + const result = Record.reduce(string$structAB, "", (acc, v, k) => { + expect(acc).type.toBe() + expect(v).type.toBe() + expect(k).type.toBe<"a" | "b">() + return typeof k === "string" ? k : acc + }) + expect(result).type.toBe() + }) + + it("some", () => { + expect(Record.some(string$structAB, (v, k) => { + expect(v).type.toBe() + expect(k).type.toBe<"a" | "b">() + return false + })).type.toBe() + pipe( + string$numbersOrStrings, + Record.some((v, k) => { + expect(v).type.toBe() + expect(k).type.toBe() + return true + }) + ) + }) + + it("union", () => { + expect(Record.union(string$numbers, string$numbers, (_, b) => b)) + .type.toBe>() + expect(Record.union(string$numbers, string$numbersOrStrings, (_, b) => b)) + .type.toBe>() + expect(Record.union(string$structAB, string$structCD, (_, b) => b)) + .type.toBe>() + }) + + it("singleton", () => { + expect(Record.singleton("a", 1)).type.toBe>() + }) + + it("every", () => { + pipe( + string$numbersOrStrings, + Record.every((v, k) => { + expect(v).type.toBe() + expect(k).type.toBe() + return true + }) + ) + Record.every(string$structAB, (v, k) => { + expect(v).type.toBe() + expect(k).type.toBe<"a" | "b">() + return false + }) + if (Record.every(string$numbersOrStrings, Predicate.isString)) { + expect(string$numbersOrStrings).type.toBe>() + } + if (Record.every(string$numbersOrStrings, Predicate.isString)) { + expect(string$numbersOrStrings).type.toBe>() + } + }) + + it("intersection", () => { + expect(Record.intersection(string$numbers, string$numbers, (a, _) => a)) + .type.toBe>() + expect(Record.intersection(string$numbers, string$numbersOrStrings, (_, b) => b)) + .type.toBe>() + expect(Record.intersection(string$structAB, string$structCD, (_, b) => b)) + .type.toBe>() + expect(Record.intersection(string$structAB, string$structCD, (a, _) => a)) + .type.toBe>() + expect(Record.intersection(string$numbers, string$numbers, (a, _) => a)) + .type.toBe>() + expect(Record.intersection(string$numbers, string$structCD, (a, _) => a)) + .type.toBe>() + expect(Record.intersection(string$structAB, { c: 2 }, (a, _) => a)) + .type.toBe>() + expect(Record.intersection(string$structAB, { b: 2 }, (a, _) => a)) + .type.toBe>() + }) + + it("findFirst", () => { + expect(Record.findFirst(string$numbersOrStrings, (a, _) => predicateNumbersOrStrings(a))) + .type.toBe>() + expect(pipe(string$numbersOrStrings, Record.findFirst((a, _) => predicateNumbersOrStrings(a)))) + .type.toBe>() + expect(Record.findFirst(string$numbersOrStrings, Predicate.isString)) + .type.toBe>() + expect(pipe(string$numbersOrStrings, Record.findFirst(Predicate.isString))) + .type.toBe>() + }) +}) diff --git a/repos/effect/packages/effect/dtslint/RedBlackTree.tst.ts b/repos/effect/packages/effect/dtslint/RedBlackTree.tst.ts new file mode 100644 index 0000000..60cfa4b --- /dev/null +++ b/repos/effect/packages/effect/dtslint/RedBlackTree.tst.ts @@ -0,0 +1,17 @@ +import type { Order } from "effect" +import { pipe, RedBlackTree } from "effect" +import { describe, expect, it } from "tstyche" + +declare const stringAndNumberIterable: Iterable<[string, number]> +declare const stringOrUndefinedOrder: Order.Order + +describe("RedBlackTree", () => { + it("fromIterable", () => { + expect(RedBlackTree.fromIterable(stringAndNumberIterable, stringOrUndefinedOrder)).type.toBe< + RedBlackTree.RedBlackTree + >() + expect(pipe(stringAndNumberIterable, RedBlackTree.fromIterable(stringOrUndefinedOrder))).type.toBe< + RedBlackTree.RedBlackTree + >() + }) +}) diff --git a/repos/effect/packages/effect/dtslint/Runtime.tst.ts b/repos/effect/packages/effect/dtslint/Runtime.tst.ts new file mode 100644 index 0000000..db2d959 --- /dev/null +++ b/repos/effect/packages/effect/dtslint/Runtime.tst.ts @@ -0,0 +1,9 @@ +import type { Runtime } from "effect" +import { describe, expect, it } from "tstyche" + +describe("Runtime", () => { + it("Runtime.Context type helper", () => { + type ContextOfRuntime = Runtime.Runtime.Context> + expect().type.toBe<{ foo: string }>() + }) +}) diff --git a/repos/effect/packages/effect/dtslint/Schedule.tst.ts b/repos/effect/packages/effect/dtslint/Schedule.tst.ts new file mode 100644 index 0000000..a51e748 --- /dev/null +++ b/repos/effect/packages/effect/dtslint/Schedule.tst.ts @@ -0,0 +1,34 @@ +import { Console, Schedule } from "effect" +import { describe, expect, it, when } from "tstyche" + +describe("Schedule", () => { + it("tapOutput", () => { + expect(Schedule.once.pipe( + Schedule.as(1), + Schedule.tapOutput((x) => { + expect(x).type.toBe() + return Console.log(x) + }) + )).type.toBe>() + + // The callback should not affect the type of the output (`number`) + expect(Schedule.once.pipe( + Schedule.as(1), + Schedule.tapOutput((x: string | number) => Console.log(x)) + )).type.toBe>() + + expect(Schedule.tapOutput( + Schedule.once.pipe( + Schedule.as(1) + ), + (x: string | number) => Console.log(x) + )).type.toBe>() + + when(Schedule.once.pipe).isCalledWith( + Schedule.as(1), + expect(Schedule.tapOutput).type.not.toBeCallableWith( + (s: string) => Console.log(s.trim()) + ) + ) + }) +}) diff --git a/repos/effect/packages/effect/dtslint/Schema/Brand.tst.ts b/repos/effect/packages/effect/dtslint/Schema/Brand.tst.ts new file mode 100644 index 0000000..67d630d --- /dev/null +++ b/repos/effect/packages/effect/dtslint/Schema/Brand.tst.ts @@ -0,0 +1,28 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import { pipe, Schema } from "effect" +import { describe, expect, it, when } from "tstyche" + +const Int1 = Symbol.for("Int") +const Int2 = Symbol.for("Int") + +const schema1 = pipe(Schema.Number, Schema.int(), Schema.brand(Int1)) +const schema2 = pipe(Schema.Number, Schema.int(), Schema.brand(Int2)) + +type A1 = Schema.Schema.Type +type A2 = Schema.Schema.Type + +describe("SchemaBrand", () => { + it("should differentiate between branded schema types", () => { + expect().type.not.toBeAssignableTo() + expect().type.not.toBeAssignableTo() + }) + + it("should raise an error when the brand is not assignable to the schema", () => { + when(pipe).isCalledWith( + Schema.Number, + expect(Schema.brand).type.not.toBeCallableWith("UserId", { + examples: ["a"] + }) + ) + }) +}) diff --git a/repos/effect/packages/effect/dtslint/Schema/Context.tst.ts b/repos/effect/packages/effect/dtslint/Schema/Context.tst.ts new file mode 100644 index 0000000..eecd8b8 --- /dev/null +++ b/repos/effect/packages/effect/dtslint/Schema/Context.tst.ts @@ -0,0 +1,407 @@ +import type { Exit } from "effect" +import { Context, Effect, Option } from "effect" +import { hole } from "effect/Function" +import * as ParseResult from "effect/ParseResult" +import * as S from "effect/Schema" +import { describe, expect, it } from "tstyche" + +interface aContext extends S.Schema {} +interface bContext extends S.Schema {} +interface cContext extends S.Schema {} + +declare const aContext: aContext +declare const bContext: bContext +declare const cContext: cContext + +const Taga = Context.GenericTag<"Taga", string>("Taga") +const Tagb = Context.GenericTag<"Tagb", number>("Tagb") +const Tag1 = Context.GenericTag<"Tag1", string>("Tag1") +const Tag2 = Context.GenericTag<"Tag2", number>("Tag2") + +declare const myRequest: MyRequest + +describe("Schema Context", () => { + it("declare: simple predicate", () => { + const schema = S.declare((u): u is string => typeof u === "string") + expect(S.asSchema(schema)).type.toBe>() + expect(schema).type.toBe>() + }) + + it("declare: with contexts and options", () => { + const schema = S.declare( + [aContext, bContext], + { + decode: (_a, _b) => () => ParseResult.succeed("a"), + encode: (_a, _b) => () => ParseResult.succeed(1) + }, + { + arbitrary: (_a, _b) => (fc) => fc.string(), + pretty: (_a, _b) => (s) => s, + equivalence: () => (_a, _b) => true + } + ) + expect(S.asSchema(schema)).type.toBe>() + expect(schema).type.toBe>() + }) + + it("declare errors", () => { + expect(S.declare).type.not.toBeCallableWith( + [aContext, bContext], + { + decode: (_a: S.Schema, _b: S.Schema) => () => + Taga.pipe(Effect.flatMap(ParseResult.succeed)), + encode: (_a: S.Schema, _b: S.Schema) => () => ParseResult.succeed(1) + } + ) + + expect(S.declare).type.not.toBeCallableWith( + [aContext, bContext], + { + decode: (_a: S.Schema, _b: S.Schema) => () => ParseResult.succeed("a"), + encode: (_a: S.Schema, _b: S.Schema) => () => + Tagb.pipe(Effect.flatMap(ParseResult.succeed)) + } + ) + + expect(S.declare).type.not.toBeCallableWith( + [aContext, bContext], + { + decode: (_a: S.Schema, _b: S.Schema) => () => + Taga.pipe(Effect.flatMap(ParseResult.succeed)), + encode: (_a: S.Schema, _b: S.Schema) => () => + Tagb.pipe(Effect.flatMap(ParseResult.succeed)) + } + ) + + expect(S.declare).type.not.toBeCallableWith( + [], + { + decode: () => () => Tag1.pipe(Effect.flatMap(ParseResult.succeed)), + encode: () => () => ParseResult.succeed(1) + } + ) + + expect(S.declare).type.not.toBeCallableWith( + [aContext, bContext], + { + decode: (_a: S.Schema, _b: S.Schema) => () => + Tag1.pipe(Effect.flatMap(ParseResult.succeed)), + encode: (_a: S.Schema, _b: S.Schema) => () => ParseResult.succeed(1) + } + ) + + expect(S.declare).type.not.toBeCallableWith( + [aContext, bContext], + { + decode: (_a: S.Schema, _b: S.Schema) => () => ParseResult.succeed("a"), + encode: (_a: S.Schema, _b: S.Schema) => () => + Tag2.pipe(Effect.flatMap(ParseResult.succeed)) + } + ) + }) + + it("Union", () => { + expect(S.asSchema(S.Union(aContext, bContext))) + .type.toBe>() + expect(S.Union(aContext, bContext)) + .type.toBe>() + }) + + it("Tuple2", () => { + const schema = S.Tuple(aContext, bContext) + expect(S.asSchema(schema)) + .type.toBe>() + expect(schema).type.toBe>() + }) + + it("TupleType", () => { + expect(S.asSchema(S.Tuple([aContext], bContext))) + .type.toBe< + S.Schema], readonly [string, ...Array], "aContext" | "bContext"> + >() + expect(S.Tuple([aContext], bContext)) + .type.toBe>() + }) + + it("OptionalElement", () => { + expect(S.asSchema(S.Tuple(aContext, S.optionalElement(bContext)))) + .type.toBe>() + expect(S.Tuple(aContext, S.optionalElement(bContext))) + .type.toBe]>>() + }) + + it("Array", () => { + expect(S.asSchema(S.Array(aContext))) + .type.toBe, ReadonlyArray, "aContext">>() + expect(S.Array(aContext)) + .type.toBe>() + }) + + it("NonEmptyArray", () => { + expect(S.asSchema(S.NonEmptyArray(aContext))) + .type.toBe], readonly [string, ...Array], "aContext">>() + expect(S.NonEmptyArray(aContext)) + .type.toBe>() + }) + + it("propertySignatureDeclaration", () => { + expect(S.propertySignature(aContext)) + .type.toBe>() + expect(S.propertySignature(aContext).annotations({})) + .type.toBe>() + }) + + it("optionalToOptional", () => { + expect(S.optionalToOptional(aContext, S.String, { decode: (o) => o, encode: (o) => o })) + .type.toBe>() + }) + + it("optionalToRequired", () => { + expect( + S.optionalToRequired(aContext, S.String, { decode: Option.getOrElse(() => ""), encode: Option.some }) + ).type.toBe>() + }) + + it("requiredToOptional", () => { + expect( + S.requiredToOptional(aContext, S.String, { decode: Option.some, encode: Option.getOrElse(() => "") }) + ).type.toBe>() + }) + + it("optional", () => { + expect(S.optional(aContext)) + .type.toBe>() + }) + + it("Struct", () => { + expect(S.asSchema(S.Struct({ a: aContext, b: bContext }))) + .type.toBe< + S.Schema< + { readonly a: string; readonly b: number }, + { readonly a: string; readonly b: number }, + "aContext" | "bContext" + > + >() + expect(S.Struct({ a: aContext, b: bContext })) + .type.toBe>() + }) + + it("pick", () => { + expect(S.Struct({ a: aContext, b: bContext }).pipe(S.pick("a"))) + .type.toBe>() + }) + + it("omit", () => { + expect(S.Struct({ a: aContext, b: bContext }).pipe(S.omit("b"))) + .type.toBe>() + }) + + it("partialWith", () => { + expect(S.partialWith(S.Struct({ a: aContext, b: bContext }), { exact: true })) + .type.toBe< + S.SchemaClass< + { readonly a?: string; readonly b?: number }, + { readonly a?: string; readonly b?: number }, + "aContext" | "bContext" + > + >() + }) + + it("required", () => { + expect(S.required(S.partialWith(S.Struct({ a: aContext, b: bContext }), { exact: true }))) + .type.toBe< + S.SchemaClass< + { readonly a: string; readonly b: number }, + { readonly a: string; readonly b: number }, + "aContext" | "bContext" + > + >() + }) + + it("mutable", () => { + expect(S.asSchema(S.mutable(S.Struct({ a: aContext, b: bContext })))) + .type.toBe>() + expect(S.mutable(S.Struct({ a: aContext, b: bContext }))) + .type.toBe>>() + }) + + it("Record", () => { + expect(S.asSchema(S.Record({ key: aContext, value: bContext }))) + .type.toBe< + S.Schema<{ readonly [x: string]: number }, { readonly [x: string]: number }, "aContext" | "bContext"> + >() + expect(S.Record({ key: aContext, value: bContext })) + .type.toBe>() + }) + + it("extend", () => { + expect( + S.asSchema(S.Struct({ a: aContext, b: bContext }).pipe(S.extend(S.Struct({ c: cContext })))) + ) + .type.toBe< + S.Schema< + { readonly a: string; readonly b: number } & { readonly c: boolean }, + { readonly a: string; readonly b: number } & { readonly c: boolean }, + "aContext" | "bContext" | "cContext" + > + >() + expect(S.Struct({ a: aContext, b: bContext }).pipe(S.extend(S.Struct({ c: cContext })))) + .type.toBe, S.Struct<{ c: cContext }>>>() + }) + + it("compose", () => { + expect(S.asSchema(aContext.pipe(S.compose(bContext, { strict: false })))) + .type.toBe>() + }) + + it("suspend", () => { + expect(S.suspend(() => aContext)) + .type.toBe>() + }) + + it("filter", () => { + expect(aContext.pipe(S.filter(() => false))) + .type.toBe>() + }) + + it("transformOrFail", () => { + expect( + S.asSchema( + S.transformOrFail(aContext, bContext, { + decode: () => ParseResult.succeed(1), + encode: () => ParseResult.succeed("") + }) + ) + ) + .type.toBe>() + expect( + S.transformOrFail(aContext, bContext, { + decode: () => ParseResult.succeed(1), + encode: () => ParseResult.succeed("") + }) + ) + .type.toBe>() + }) + + it("transform", () => { + expect(S.asSchema(S.transform(aContext, bContext, { decode: () => 1, encode: () => "" }))) + .type.toBe>() + expect(S.transform(aContext, bContext, { decode: () => 1, encode: () => "" })) + .type.toBe>() + }) + + it("attachPropertySignature", () => { + expect(S.Struct({ a: aContext }).pipe(S.attachPropertySignature("_tag", "A"))) + .type.toBe>() + expect(S.attachPropertySignature(S.Struct({ a: aContext }), "_tag", "A")) + .type.toBe>() + }) + + it("annotations", () => { + expect(aContext.annotations({})) + .type.toBe>() + }) + + it("rename", () => { + expect(S.rename(S.Struct({ a: aContext, b: bContext }), { a: "c", b: "d" })) + .type.toBe< + S.SchemaClass< + { readonly c: string; readonly d: number }, + { readonly a: string; readonly b: number }, + "aContext" | "bContext" + > + >() + }) +}) + +class MyClass extends S.Class("MyClass")({ + a: aContext +}) {} + +describe("Class", () => { + it("Class", () => { + expect>() + .type.toBe<"aContext">() + }) + + it("Class.transform", () => { + class MyClassWithTransform extends MyClass.transformOrFail( + "MyClassWithTransform" + )( + { b: bContext }, + { + decode: (i) => + Tag1.pipe( + Effect.flatMap((a) => ParseResult.succeed(i.a === a ? { ...i, b: 1 } : { ...i, b: 2 })) + ), + encode: (a) => + Tag2.pipe( + Effect.flatMap((b) => ParseResult.succeed(a.b === b ? { a: "a1" } : { a: "a2" })) + ) + } + ) {} + expect>() + .type.toBe<"aContext" | "bContext" | "Tag1" | "Tag2">() + expect(MyClassWithTransform.fields) + .type.toBe<{ readonly a: aContext; readonly b: bContext }>() + }) + + it("Class.transformFrom", () => { + class MyClassWithTransformFrom extends MyClass.transformOrFailFrom( + "MyClassWithTransformFrom" + )( + { b: bContext }, + { + decode: (i) => + Tag1.pipe( + Effect.flatMap((a) => ParseResult.succeed(i.a === a ? { ...i, b: 1 } : { ...i, b: 2 })) + ), + encode: (a) => + Tag2.pipe( + Effect.flatMap((b) => ParseResult.succeed(a.b === b ? { a: "a1" } : { a: "a2" })) + ) + } + ) {} + expect>() + .type.toBe<"aContext" | "bContext" | "Tag1" | "Tag2">() + expect(MyClassWithTransformFrom.fields) + .type.toBe<{ readonly a: aContext; readonly b: bContext }>() + }) +}) + +class MyRequest extends S.TaggedRequest()("MyRequest", { + failure: bContext, + success: cContext, + payload: { a: aContext } +}) {} + +describe("TaggedRequest", () => { + it("TaggedRequest", () => { + expect>() + .type.toBe<"aContext">() + expect(MyRequest.fields) + .type.toBe<{ readonly _tag: S.tag<"MyRequest">; readonly a: aContext }>() + }) + + it("exitSchema", () => { + expect(S.exitSchema(myRequest)) + .type.toBe< + S.Schema, S.ExitEncoded, "bContext" | "cContext"> + >() + }) +}) + +describe("TemplateLiteralParser", () => { + it("TemplateLiteralParser", () => { + expect( + S.asSchema( + S.TemplateLiteralParser( + hole>(), + "a", + hole>() + ) + ) + ) + .type.toBe>() + }) +}) diff --git a/repos/effect/packages/effect/dtslint/Schema/Generic.tst.ts b/repos/effect/packages/effect/dtslint/Schema/Generic.tst.ts new file mode 100644 index 0000000..d2e593c --- /dev/null +++ b/repos/effect/packages/effect/dtslint/Schema/Generic.tst.ts @@ -0,0 +1,36 @@ +import { Either, Schema } from "effect" + +export const f1 = ( + resultSchema: Schema.Schema +) => { + const left = Schema.Struct({ + ok: Schema.Literal(false), + error: Schema.String + }) + const right = ( + resultSchema: Schema.Schema + ) => Schema.extend(Schema.Struct({ ok: Schema.Literal(true) }), resultSchema) + const union = Schema.Union(left, right(resultSchema)) + const out = Schema.transform( + union, + Schema.EitherFromSelf({ left: Schema.String, right: Schema.typeSchema(resultSchema) }), + { + decode: (u) => u.ok ? Either.right(u) : Either.left(u.error), + encode: (a) => ({ ok: true as const, ...a }) + } + ) + return out +} + +type Model = { id: string } & Record + +export const f2 = (schema: Schema.Schema) => { + type Patch = Pick & Partial> + + const patch: Schema.Schema = schema.pipe( + Schema.pick("id"), + Schema.extend(schema.pipe(Schema.omit("id"), Schema.partial)) + ) + + return patch +} diff --git a/repos/effect/packages/effect/dtslint/Schema/PropertySignature.tst.ts b/repos/effect/packages/effect/dtslint/Schema/PropertySignature.tst.ts new file mode 100644 index 0000000..62057e3 --- /dev/null +++ b/repos/effect/packages/effect/dtslint/Schema/PropertySignature.tst.ts @@ -0,0 +1,34 @@ +import { Schema } from "effect" +import { describe, expect, it } from "tstyche" + +describe("Schema Property Signature", () => { + it("should have the correct field types", () => { + const A = Schema.propertySignature(Schema.String) + expect(A).type.toBe>() + + const AA = A.annotations({}) + expect(AA).type.toBe>() + + const B = Schema.optional(Schema.Number) + expect(B).type.toBe>() + + const BB = B.annotations({}) + expect(BB).type.toBe>() + + const C = Schema.optionalWith(Schema.Boolean, { exact: true }) + expect(C).type.toBe>() + + const CC = C.annotations({}) + expect(CC).type.toBe>() + + const schema = Schema.Struct({ + a: AA, + b: BB, + c: CC + }) + + expect(schema.fields.a.from).type.toBe() + expect(schema.fields.b.from).type.toBe() + expect(schema.fields.c.from).type.toBe() + }) +}) diff --git a/repos/effect/packages/effect/dtslint/Schema/Schema.tst.ts b/repos/effect/packages/effect/dtslint/Schema/Schema.tst.ts new file mode 100644 index 0000000..173c741 --- /dev/null +++ b/repos/effect/packages/effect/dtslint/Schema/Schema.tst.ts @@ -0,0 +1,4191 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import type { StandardSchemaV1 } from "@standard-schema/spec" +import type { + Arbitrary, + BigDecimal, + Cause, + Chunk, + Config, + DateTime, + Duration, + Either, + Equivalence, + Exit, + FiberId, + HashMap, + HashSet, + List, + Pretty, + Redacted, + SchemaAST, + SortedSet, + Types +} from "effect" +import { + Brand, + Context, + Effect, + hole, + identity, + Number as N, + Option, + ParseResult, + pipe, + Schema as S, + String as Str +} from "effect" +import { describe, expect, it, when } from "tstyche" + +class A extends S.Class("A")({ a: S.NonEmptyString }) {} +declare const anyNever: S.Schema +declare const neverAny: S.Schema +declare const anyNeverPropertySignature: S.PropertySignature<"?:", any, never, "?:", never, false> +declare const neverAnyPropertySignature: S.PropertySignature<"?:", never, never, "?:", any, false> +const ServiceA = Context.GenericTag<"ServiceA", string>("ServiceA") +declare const aContext: S.Schema +declare const bContext: S.Schema +declare const cContext: S.Schema + +describe("Schema", () => { + describe("SchemaClass", () => { + it("the constructor should not be callable", () => { + // @ts-expect-error! TODO use '.toHaveConstructSignatures()' when it will be implemented + new S.String() + }) + }) + + describe("Type Level Helpers", () => { + it("Schema.Encoded", () => { + expect>().type.toBe() + expect>().type.toBe() + }) + + it("Schema.Type", () => { + expect>().type.toBe() + expect>().type.toBe() + }) + + it("Schema.Context", () => { + expect>().type.toBe() + expect>>().type.toBe<"ctx">() + }) + + it("Struct.Type", () => { + expect(hole>>()).type.toBe<{}>() + expect(hole }>>>()).type.toBe<{ readonly a: number }>() + expect(hole< + Types.Simplify< + S.Struct.Type<{ + a: S.Schema + b: S.PropertySignature<":", number, never, ":", string, false, "context"> + }> + > + >()).type.toBe<{ readonly a: number; readonly b: number }>() + expect(hole< + Types.Simplify< + S.Struct.Type<{ + a: S.Schema + b: S.PropertySignature<":", number, never, "?:", string, false, "context"> + }> + > + >()).type.toBe<{ readonly a: number; readonly b: number }>() + expect(hole< + Types.Simplify< + S.Struct.Type<{ + a: S.Schema + b: S.PropertySignature<":", number, "c", ":", string, false, "context"> + }> + > + >()).type.toBe<{ readonly a: number; readonly b: number }>() + expect(hole< + Types.Simplify< + S.Struct.Type<{ + a: S.Schema + b: S.PropertySignature<":", number, "c", "?:", string, false, "context"> + }> + > + >()).type.toBe<{ readonly a: number; readonly b: number }>() + expect(hole< + Types.Simplify< + S.Struct.Type<{ + a: S.Schema + b: S.PropertySignature<"?:", number, never, ":", string, false, "context"> + }> + > + >()).type.toBe<{ readonly a: number; readonly b?: number }>() + expect(hole< + Types.Simplify< + S.Struct.Type<{ + a: S.Schema + b: S.PropertySignature<"?:", number, never, "?:", string, false, "context"> + }> + > + >()).type.toBe<{ readonly a: number; readonly b?: number }>() + expect(hole< + Types.Simplify< + S.Struct.Type<{ + a: S.Schema + b: S.PropertySignature<"?:", number, "c", ":", string, false, "context"> + }> + > + >()).type.toBe<{ readonly a: number; readonly b?: number }>() + expect(hole< + Types.Simplify< + S.Struct.Type<{ + a: S.Schema + b: S.PropertySignature<"?:", number, "c", "?:", string, false, "context"> + }> + > + >()).type.toBe<{ readonly a: number; readonly b?: number }>() + }) + + it("Struct.Encoded", () => { + expect(hole>>()).type.toBe<{}>() + expect(hole }>>>()).type.toBe< + { readonly a: string } + >() + expect(hole< + Types.Simplify< + S.Struct.Encoded<{ + a: S.Schema + b: S.PropertySignature<":", number, never, ":", string, false, "context"> + }> + > + >()).type.toBe<{ readonly a: string; readonly b: string }>() + expect(hole< + Types.Simplify< + S.Struct.Encoded<{ + a: S.Schema + b: S.PropertySignature<":", number, never, "?:", string, false, "context"> + }> + > + >()).type.toBe<{ readonly a: string; readonly b?: string }>() + expect(hole< + Types.Simplify< + S.Struct.Encoded<{ + a: S.Schema + b: S.PropertySignature<":", number, "c", ":", string, false, "context"> + }> + > + >()).type.toBe<{ readonly a: string; readonly c: string }>() + expect(hole< + Types.Simplify< + S.Struct.Encoded<{ + a: S.Schema + b: S.PropertySignature<":", number, "c", "?:", string, false, "context"> + }> + > + >()).type.toBe<{ readonly a: string; readonly c?: string }>() + expect(hole< + Types.Simplify< + S.Struct.Encoded<{ + a: S.Schema + b: S.PropertySignature<"?:", number, never, ":", string, false, "context"> + }> + > + >()).type.toBe<{ readonly a: string; readonly b: string }>() + expect(hole< + Types.Simplify< + S.Struct.Encoded<{ + a: S.Schema + b: S.PropertySignature<"?:", number, never, "?:", string, false, "context"> + }> + > + >()).type.toBe<{ readonly a: string; readonly b?: string }>() + expect(hole< + Types.Simplify< + S.Struct.Encoded<{ + a: S.Schema + b: S.PropertySignature<"?:", number, "c", ":", string, false, "context"> + }> + > + >()).type.toBe<{ readonly a: string; readonly c: string }>() + expect(hole< + Types.Simplify< + S.Struct.Encoded<{ + a: S.Schema + b: S.PropertySignature<"?:", number, "c", "?:", string, false, "context"> + }> + > + >()).type.toBe<{ readonly a: string; readonly c?: string }>() + }) + + it("Struct.Constructor", () => { + expect( + hole< + S.Struct.Constructor<{ + a: S.PropertySignature<":", string, never, ":", string, true> + b: typeof S.Number + c: S.PropertySignature<":", boolean, never, ":", boolean, true> + }> + >() + ).type.toBe<{ readonly a?: string } & { readonly b: number } & { readonly c?: boolean }>() + }) + + it("TupleType.Type", () => { + expect(hole>()).type.toBe() + expect(hole>()).type.toBe() + expect(hole>()).type.toBe>() + expect(hole>()).type.toBe< + readonly [number, ...Array] + >() + expect( + hole>() + ).type.toBe, number]>() + expect( + hole], []>>() + ).type.toBe() + expect( + hole< + S.TupleType.Type< + [typeof S.NumberFromString, S.Element], + [typeof S.NumberFromString] + > + >() + ).type.toBe]>() + }) + + it("TupleType.Encoded", () => { + expect(hole>()).type.toBe() + expect(hole>()).type.toBe() + expect(hole>()).type.toBe>() + expect(hole>()).type.toBe< + readonly [string, ...Array] + >() + expect( + hole>() + ).type.toBe, string]>() + expect( + hole], []>>() + ).type.toBe() + expect( + hole< + S.TupleType.Encoded< + [typeof S.NumberFromString, S.Element], + [typeof S.NumberFromString] + > + >() + ).type.toBe]>() + }) + + it("TupleType.Context", () => { + expect( + hole>>() + ).type.toBe<"a" | "b" | "c">() + }) + }) + + it("annotations", () => { + // should allow to add custom string annotations to a schema + expect(S.String.annotations({ a: 1 })).type.toBe>() + // should allow to add custom symbol annotations to a schema + expect(S.String.annotations({ [Symbol.for("a")]: 1 })).type.toBe>() + + interface AnnotatedString extends S.Annotable {} + const AnnotatedString = hole() + + expect(hole>().pipe(S.annotations({}))).type.toBe>() + expect(AnnotatedString.pipe(S.annotations({}))).type.toBe() + + expect(S.Number.pipe(S.int(), S.brand("Int"), S.annotations({}))) + .type.toBe, "Int">>() + expect(S.Struct({ a: AnnotatedString }).pipe(S.annotations({}))).type.toBe>() + expect(A.pipe(S.annotations({}))).type.toBe>() + expect(S.Number.pipe(S.int(), S.brand("Int")).make(1)).type.toBe>() + }) + + it("Never", () => { + expect(S.Never).type.toBeAssignableTo>() + expect(S.Never).type.toBeAssignableTo>() + }) + + it("Primitives", () => { + expect(S.asSchema(S.Void)).type.toBe>() + expect(S.Void).type.toBe() + + expect(S.asSchema(S.Undefined)).type.toBe>() + expect(S.Undefined).type.toBe() + + expect(S.asSchema(S.String)).type.toBe>() + expect(S.String).type.toBe() + + expect(S.asSchema(S.Number)).type.toBe>() + expect(S.Number).type.toBe() + + expect(S.asSchema(S.Boolean)).type.toBe>() + expect(S.Boolean).type.toBe() + + expect(S.asSchema(S.BigIntFromSelf)).type.toBe>() + expect(S.BigIntFromSelf).type.toBe() + + expect(S.asSchema(S.BigInt)).type.toBe>() + expect(S.BigInt).type.toBe() + + expect(S.asSchema(S.SymbolFromSelf)).type.toBe>() + expect(S.SymbolFromSelf).type.toBe() + + expect(S.asSchema(S.Symbol)).type.toBe>() + expect(S.Symbol).type.toBe() + + expect(S.asSchema(S.Unknown)).type.toBe>() + expect(S.Unknown).type.toBe() + + expect(S.asSchema(S.Any)).type.toBe>() + expect(S.Any).type.toBe() + + expect(S.asSchema(S.Object)).type.toBe>() + expect(S.Object).type.toBe() + }) + + it("Literal", () => { + expect(S.asSchema(S.Null)).type.toBe>() + expect(S.Null).type.toBe() + + expect(S.Literal()).type.toBe() + expect(S.Literal(...[])).type.toBe() + expect(S.Literal(...([] as Array<"a" | "b">))).type.toBe>() + expect(S.Literal(...([] as Array))).type.toBe>() + expect(S.asSchema(S.Literal("a"))).type.toBe>() + expect(S.Literal("a")).type.toBe>() + + expect(S.asSchema(S.Literal("a", "b", "c"))).type.toBe>() + expect(S.Literal("a", "b", "c")).type.toBe>() + + expect(S.Literal(1)).type.toBe>() + expect(S.Literal(2n)).type.toBe>() + expect(S.Literal(true)).type.toBe>() + expect(S.Literal("A", "B")).type.toBe>() + expect(S.Literal("A", "B").literals).type.toBe() + expect(S.Literal("A", "B").annotations({})).type.toBe>() + }) + + describe("Enums", () => { + enum Fruits { + Apple, + Banana + } + + const schema = S.Enums(Fruits) + + it("Schema Type", () => { + expect(S.asSchema(schema)).type.toBeAssignableTo>() + expect(schema).type.toBe>() + }) + + it("should expose the enums field", () => { + const enums = schema.enums + expect(enums).type.toBe() + expect(enums.Apple).type.toBe() + expect(enums.Banana).type.toBe() + }) + }) + + it("UndefinedOr", () => { + expect(S.UndefinedOr(S.Never)).type.toBe>() + }) + + it("NullishOr", () => { + expect(S.NullishOr(S.Never)).type.toBe>() + }) + + it("NullOr", () => { + expect(S.NullOr(S.Never)).type.toBe>() + expect(S.asSchema(S.NullOr(S.String))).type.toBe>() + expect(S.NullOr(S.String)).type.toBe>() + expect(S.asSchema(S.NullOr(S.NumberFromString))).type.toBe>() + expect(S.NullOr(S.NumberFromString)).type.toBe>() + }) + + describe("Union", () => { + it("should allow Never members", () => { + const schema = S.Union(S.String, S.Never) + expect(S.asSchema(schema)).type.toBe>() + expect(schema).type.toBe>() + }) + + it("union of primitives", () => { + const schema = S.Union(S.String, S.Number) + expect(S.asSchema(schema)).type.toBe>() + expect(schema).type.toBe>() + expect(schema.annotations({})).type.toBe>() + // should expose the members + expect(schema.members).type.toBe() + }) + + it("primitive + transformation", () => { + const schema = S.Union(S.Boolean, S.NumberFromString) + expect(S.asSchema(schema)).type.toBe>() + expect(schema).type.toBe>() + expect(schema.annotations({ + pretty: () => (a) => { + expect(a).type.toBe() + return "-" + } + })).type.toBe>() + }) + }) + + it("keyof", () => { + expect(S.keyof(S.Struct({ a: S.String, b: S.NumberFromString }))).type.toBe>() + }) + + describe("Tuple", () => { + it("required elements", () => { + expect(S.asSchema(S.Tuple(S.String, S.Number))) + .type.toBe>() + expect(S.Tuple(S.String, S.Number)).type.toBe>() + expect(S.asSchema(S.Tuple(S.String, S.NumberFromString))) + .type.toBe>() + expect(S.Tuple(S.String, S.NumberFromString)).type.toBe>() + expect(S.Tuple(S.String, S.Number).elements).type.toBe() + expect(S.Tuple(S.String, S.Number).rest).type.toBe() + }) + + it("required elements + rest", () => { + expect(S.asSchema(S.Tuple([S.String], S.Number, S.Boolean))).type.toBe< + S.Schema, boolean], readonly [string, ...Array, boolean]> + >() + expect(S.Tuple([S.String], S.Number, S.Boolean)).type.toBe< + S.TupleType + >() + expect(S.Tuple([S.String], S.Number).elements).type.toBe() + expect(S.Tuple([S.String], S.Number).rest).type.toBe() + expect(S.Tuple([S.String], S.Number, S.Boolean).rest).type.toBe() + }) + + it("optional elements", () => { + expect(S.asSchema(S.Tuple(S.String, S.Number, S.optionalElement(S.Boolean)))) + .type.toBe>() + expect(S.Tuple(S.String, S.Number, S.optionalElement(S.Boolean))) + .type.toBe]>>() + expect( + S.asSchema(S.Tuple(S.String, S.NumberFromString, S.optionalElement(S.NumberFromString))) + ).type.toBe>() + expect( + S.Tuple(S.String, S.NumberFromString, S.optionalElement(S.NumberFromString)) + ).type.toBe]>>() + }) + + it("Array", () => { + expect(S.asSchema(S.Array(S.Number))).type.toBe, ReadonlyArray>>() + expect(S.Array(S.Number)).type.toBe>() + expect(pipe(S.Number, S.Array)).type.toBe>() + expect(S.asSchema(S.Array(S.NumberFromString))) + .type.toBe, ReadonlyArray>>() + expect(S.Array(S.NumberFromString)).type.toBe>() + expect(S.Array(S.String).value).type.toBe() + expect(S.Array(S.String).elements).type.toBe() + expect(S.Array(S.String).rest).type.toBe() + }) + }) + + it("NonEmptyArray", () => { + const schema = S.NonEmptyArray(S.NumberFromString) + expect(S.asSchema(schema)) + .type.toBe], readonly [string, ...Array]>>() + expect(schema).type.toBe>() + expect(schema.annotations({ + pretty: () => (nea) => { + expect(nea).type.toBe]>() + return "-" + } + })).type.toBe>() + + // should support pipe + expect(pipe(S.NumberFromString, S.NonEmptyArray)).type.toBe>() + + // should expose the value, elements and rest fields + expect(schema.value).type.toBe() + expect(schema.elements).type.toBe() + expect(schema.rest).type.toBe() + }) + + describe("Struct", () => { + it("baseline", () => { + const schema = S.Struct({ a: S.String, b: S.NumberFromString }) + expect(S.asSchema(schema)) + .type.toBe>() + expect(schema).type.toBe>() + expect(schema.annotations({ + pretty: () => (a) => { + expect(a).type.toBe<{ + readonly a: string + readonly b: number + }>() + return "-" + } + })).type.toBe>() + + // exposed fields + expect(schema.fields).type.toBe<{ readonly a: typeof S.String; readonly b: typeof S.NumberFromString }>() + expect(schema.records).type.toBe() + }) + + it("should accept Never as a field", () => { + const schema = S.Struct({ a: S.Never, b: S.NumberFromString }) + expect(S.asSchema(schema)) + .type.toBe>() + expect(schema).type.toBe>() + expect(schema.annotations({ + pretty: () => (a) => { + expect(a).type.toBe<{ + readonly a: never + readonly b: number + }>() + return "-" + } + })).type.toBe>() + + expect(S.asSchema(S.Struct({ a: anyNever }))).type.toBe>() + expect(S.asSchema(S.Struct({ a: neverAny }))).type.toBe>() + expect(S.asSchema(S.Struct({ a: anyNeverPropertySignature }))) + .type.toBe>() + expect(S.asSchema(S.Struct({ a: neverAnyPropertySignature }))) + .type.toBe>() + }) + + describe("make", () => { + it("baseline", () => { + const schema = S.Struct({ a: S.String, b: S.NumberFromString }) + expect(schema.make).type.toBe< + ( + props: { readonly a: string; readonly b: number }, + options?: S.MakeOptions | undefined + ) => { readonly a: string; readonly b: number } + >() + expect(schema.annotations({}).make).type.toBe< + ( + props: { readonly a: string; readonly b: number }, + options?: S.MakeOptions | undefined + ) => { readonly a: string; readonly b: number } + >() + }) + + it("withConstructorDefault", () => { + const schema = S.Struct({ + a: S.propertySignature(S.String).pipe(S.withConstructorDefault(() => "")), + b: S.Number, + c: S.propertySignature(S.Boolean).pipe(S.withConstructorDefault(() => true)) + }) + expect(schema.make).type.toBe< + ( + props: { readonly a?: string; readonly b: number; readonly c?: boolean }, + options?: S.MakeOptions | undefined + ) => { readonly a: string; readonly b: number; readonly c: boolean } + >() + expect(schema.annotations({}).make).type.toBe< + ( + props: { readonly a?: string; readonly b: number; readonly c?: boolean }, + options?: S.MakeOptions | undefined + ) => { readonly a: string; readonly b: number; readonly c: boolean } + >() + }) + }) + + it("pick", () => { + expect(S.Struct({ a: S.String }).pick).type.not.toBeCallableWith("c") + expect(S.Struct({ a: S.propertySignature(S.String).pipe(S.fromKey("c")) }).pick) + .type.not.toBeCallableWith("c") + + expect(S.Struct({ a: S.String, b: S.Number, c: S.Boolean }).pick("a", "b")) + .type.toBe>() + + const f = ( + schema: S.Struct & S.Schema, S.Struct.Context> + ) => { + expect(schema.fields).type.toBe>() + const picked = schema.pick("a") // existing field + expect(picked).type.toBe>() + + const e = S.encodeUnknown(schema)(null) + expect(e).type.toBe< + Effect.Effect, ParseResult.ParseError, S.Schema.Context> + >() + + return picked + } + + when(f).isCalledWith(expect(S.Struct).type.not.toBeCallableWith({ b: S.String })) + + when(f).isCalledWith(expect(S.Struct).type.not.toBeCallableWith({ a: S.Number })) + }) + + it("omit", () => { + expect(S.Struct({ a: S.String }).omit).type.not.toBeCallableWith("c") + expect(S.Struct({ a: S.propertySignature(S.String).pipe(S.fromKey("c")) }).omit).type.not.toBeCallableWith("c") + + expect(S.Struct({ a: S.String, b: S.Number, c: S.Boolean }).omit("c")) + .type.toBe>() + expect(S.Struct({ a: S.Number, b: S.Number.pipe(S.propertySignature, S.fromKey("c")) }).omit("b")) + .type.toBe>() + + const f = ( + schema: S.Struct & S.Schema, S.Struct.Context> + ) => { + expect(schema.fields).type.toBe>() + const omitted = schema.omit("a") // existing field + expect(omitted).type.toBe]: Omit[K] }>>() + + const e = S.encodeUnknown(schema)(null) + expect(e).type.toBe< + Effect.Effect, ParseResult.ParseError, S.Schema.Context> + >() + + return omitted + } + + when(f).isCalledWith(expect(S.Struct).type.not.toBeCallableWith({ b: S.String })) + + when(f).isCalledWith(expect(S.Struct).type.not.toBeCallableWith({ a: S.Number })) + }) + }) + + describe("Record", () => { + it("baseline", () => { + const schema = S.Record({ key: S.String, value: S.NumberFromString }) + expect(S.asSchema(schema)).type.toBe< + S.Schema<{ readonly [x: string]: number }, { readonly [x: string]: string }> + >() + expect(schema).type.toBe>() + expect(schema.annotations({ + pretty: () => (s) => { + expect(s).type.toBe<{ readonly [x: string]: number }>() + return "-" + } + })).type.toBe>() + + // exposed fields + expect(schema.fields).type.toBe<{}>() + expect(schema.records).type.toBe< + readonly [{ readonly key: typeof S.String; readonly value: typeof S.NumberFromString }] + >() + expect(schema.key).type.toBe() + expect(schema.value).type.toBe() + }) + + it("make", () => { + const schema = S.Record({ key: S.String, value: S.NumberFromString }) + const make = schema.make + expect(make).type.toBe< + ( + props: void | { readonly [x: string]: number }, + options?: S.MakeOptions | undefined + ) => { readonly [x: string]: number } + >() + }) + + it("keys as union of literals", () => { + const schema = S.Record({ key: S.Union(S.Literal("a"), S.Literal("b")), value: S.NumberFromString }) + expect(S.asSchema(schema)).type.toBe< + S.Schema< + { readonly a: number; readonly b: number }, + { readonly a: string; readonly b: string } + > + >() + expect(schema).type.toBe, S.Literal<["b"]>]>, typeof S.NumberFromString>>() + expect(schema.annotations({ + pretty: () => (s) => { + expect(s).type.toBe<{ + readonly a: number + readonly b: number + }>() + return "-" + } + })).type.toBe, S.Literal<["b"]>]>, typeof S.NumberFromString>>() + }) + + it("keys as symbols", () => { + const schema = S.Record({ key: S.SymbolFromSelf, value: S.NumberFromString }) + expect(S.asSchema(schema)).type.toBe< + S.Schema< + { readonly [x: symbol]: number }, + { readonly [x: symbol]: string } + > + >() + expect(schema).type.toBe>() + expect(schema.annotations({ + pretty: () => (s) => { + expect(s).type.toBe<{ readonly [x: symbol]: number }>() + return "-" + } + })).type.toBe>() + }) + + it("keys as template literals", () => { + const schema = S.Record({ key: S.TemplateLiteral(S.Literal("a"), S.String), value: S.NumberFromString }) + expect(S.asSchema(schema)).type.toBe< + S.Schema<{ readonly [x: `a${string}`]: number }, { readonly [x: `a${string}`]: string }> + >() + expect(schema).type.toBe, typeof S.NumberFromString>>() + expect(schema.annotations({ + pretty: () => (s) => { + expect(s).type.toBe<{ readonly [x: `a${string}`]: number }>() + return "-" + } + })).type.toBe, typeof S.NumberFromString>>() + }) + + it("keys as branded types (string)", () => { + const schema = S.Record({ key: S.String.pipe(S.brand("UserId")), value: S.NumberFromString }) + expect(S.asSchema(schema)).type.toBe< + S.Schema<{ readonly [x: string & Brand.Brand<"UserId">]: number }, { readonly [x: string]: string }> + >() + expect(schema).type.toBe, typeof S.NumberFromString>>() + expect(schema.annotations({ + pretty: () => (s) => { + expect(s).type.toBe<{ readonly [x: string & Brand.Brand<"UserId">]: number }>() + return "-" + } + })).type.toBe, typeof S.NumberFromString>>() + }) + + it("keys as branded types (symbol)", () => { + const schema = S.Record({ key: S.String.pipe(S.brand(Symbol.for("UserId"))), value: S.NumberFromString }) + expect(S.asSchema(schema)).type.toBe< + S.Schema<{ readonly [x: string & Brand.Brand]: number }, { readonly [x: string]: string }> + >() + expect(schema).type.toBe, typeof S.NumberFromString>>() + expect(schema.annotations({ + pretty: () => (s) => { + expect(s).type.toBe<{ readonly [x: string & Brand.Brand]: number }>() + return "-" + } + })).type.toBe, typeof S.NumberFromString>>() + }) + }) + + describe("TypeLiteral", () => { + it("1 index signature", () => { + const schema = S.Struct({ a: S.NumberFromString }, { key: S.String, value: S.NumberFromString }) + expect(S.asSchema(schema)).type.toBe< + S.Schema< + { readonly [x: string]: number; readonly a: number }, + { readonly [x: string]: string; readonly a: string } + > + >() + expect(schema).type.toBe< + S.TypeLiteral< + { a: typeof S.NumberFromString }, + readonly [{ readonly key: typeof S.String; readonly value: typeof S.NumberFromString }] + > + >() + expect(schema.annotations({ + pretty: () => (s) => { + expect(s).type.toBe<{ readonly [x: string]: number; readonly a: number }>() + return "-" + } + })).type.toBe< + S.TypeLiteral< + { a: typeof S.NumberFromString }, + readonly [{ readonly key: typeof S.String; readonly value: typeof S.NumberFromString }] + > + >() + + // exposed fields + expect(schema.fields).type.toBe<{ readonly a: typeof S.NumberFromString }>() + expect(schema.records).type.toBe< + readonly [{ readonly key: typeof S.String; readonly value: typeof S.NumberFromString }] + >() + }) + + it("make", () => { + const schema = S.Struct({ a: S.NumberFromString }, { key: S.String, value: S.NumberFromString }) + expect(schema.make).type.toBe< + ( + props: void | { readonly [x: string]: number; readonly a: number }, + options?: S.MakeOptions | undefined + ) => { readonly [x: string]: number; readonly a: number } + >() + expect(schema.annotations({}).make).type.toBe< + ( + props: void | { readonly [x: string]: number; readonly a: number }, + options?: S.MakeOptions | undefined + ) => { readonly [x: string]: number; readonly a: number } + >() + }) + + it("2 index signatures", () => { + const schema = S.Struct( + { a: S.NumberFromString }, + { key: S.String, value: S.NumberFromString }, + { key: S.Symbol, value: S.NumberFromString } + ) + expect(S.asSchema(schema)).type.toBe< + S.Schema< + { readonly [x: string]: number; readonly [x: symbol]: number; readonly a: number }, + { readonly [x: string]: string; readonly a: string } + > + >() + expect(schema).type.toBe< + S.TypeLiteral< + { a: typeof S.NumberFromString }, + readonly [ + { readonly key: typeof S.String; readonly value: typeof S.NumberFromString }, + { readonly key: typeof S.Symbol; readonly value: typeof S.NumberFromString } + ] + > + >() + expect(schema.annotations({ + pretty: () => (s) => { + expect(s).type.toBe<{ readonly [x: string]: number; readonly [x: symbol]: number; readonly a: number }>() + return "-" + } + })).type.toBe< + S.TypeLiteral< + { a: typeof S.NumberFromString }, + readonly [ + { readonly key: typeof S.String; readonly value: typeof S.NumberFromString }, + { readonly key: typeof S.Symbol; readonly value: typeof S.NumberFromString } + ] + > + >() + + // exposed fields + expect(schema.fields).type.toBe<{ readonly a: typeof S.NumberFromString }>() + expect(schema.records).type.toBe< + readonly [ + { readonly key: typeof S.String; readonly value: typeof S.NumberFromString }, + { readonly key: typeof S.Symbol; readonly value: typeof S.NumberFromString } + ] + >() + + expect(schema.make).type.toBe< + ( + props: void | { readonly [x: string]: number; readonly [x: symbol]: number; readonly a: number }, + options?: S.MakeOptions | undefined + ) => { readonly [x: string]: number; readonly [x: symbol]: number; readonly a: number } + >() + expect(schema.annotations({}).make).type.toBe< + ( + props: void | { readonly [x: string]: number; readonly [x: symbol]: number; readonly a: number }, + options?: S.MakeOptions | undefined + ) => { readonly [x: string]: number; readonly [x: symbol]: number; readonly a: number } + >() + }) + }) + + it("optional", () => { + expect(S.optional(S.Never)).type.toBe>() + expect( + S.asSchema(S.Struct({ a: S.String, b: S.Number, c: S.optional(S.Boolean) })) + ).type.toBe< + S.Schema< + { readonly a: string; readonly b: number; readonly c?: boolean | undefined }, + { readonly a: string; readonly b: number; readonly c?: boolean | undefined }, + never + > + >() + expect(S.Struct({ a: S.String, b: S.Number, c: S.optional(S.Boolean) })) + .type.toBe }>>() + expect( + S.asSchema(S.Struct({ a: S.String, b: S.Number, c: S.optional(S.NumberFromString) })) + ).type.toBe< + S.Schema< + { readonly a: string; readonly b: number; readonly c?: number | undefined }, + { readonly a: string; readonly b: number; readonly c?: string | undefined }, + never + > + >() + expect(S.Struct({ a: S.String, b: S.Number, c: S.optional(S.NumberFromString) })) + .type.toBe }>>() + expect(S.asSchema(S.Struct({ a: S.String.pipe(S.optional) }))) + .type.toBe>() + expect(S.Struct({ a: S.String.pipe(S.optional) })).type.toBe }>>() + }) + + describe("optionalWith", () => { + it("{ exact: true }", () => { + expect( + S.asSchema(S.Struct({ a: S.optionalWith(S.Never, { exact: true }) })) + ).type.toBe>() + expect(S.Struct({ a: S.optionalWith(S.Never, { exact: true }) })) + .type.toBe }>>() + expect( + S.asSchema(S.Struct({ a: S.String, b: S.Number, c: S.optionalWith(S.Boolean, { exact: true }) })) + ).type.toBe< + S.Schema< + { readonly a: string; readonly b: number; readonly c?: boolean }, + { readonly a: string; readonly b: number; readonly c?: boolean }, + never + > + >() + expect( + S.asSchema(S.Struct({ a: S.String, b: S.Number, c: S.optionalWith(S.NumberFromString, { exact: true }) })) + ).type.toBe< + S.Schema< + { readonly a: string; readonly b: number; readonly c?: number }, + { readonly a: string; readonly b: number; readonly c?: string }, + never + > + >() + expect(S.Struct({ a: S.Literal("a", "b").pipe(S.optionalWith({ exact: true })) })) + .type.toBe, { exact: true }> }>>() + }) + + it("Type Level Errors", () => { + expect(S.optionalWith).type.not.toBeCallableWith(S.String, { as: "Option", default: () => "" }) + expect(S.optionalWith).type.not.toBeCallableWith(S.String, { + as: "Option", + exact: true, + onNoneEncoding: () => Option.some(null) + }) + expect(S.optionalWith).type.not.toBeCallableWith( + { as: "Option", exact: true, onNoneEncoding: () => Option.some(null) } + ) + expect(S.optionalWith).type.not.toBeCallableWith(S.String, { + as: "Option", + exact: true, + nullable: true, + onNoneEncoding: () => Option.some(1) + }) + expect(S.optionalWith).type.not.toBeCallableWith(S.String, { + as: "Option", + onNoneEncoding: () => Option.some(null) + }) + expect(S.optionalWith).type.not.toBeCallableWith({ as: "Option", onNoneEncoding: () => Option.some(null) }) + expect(S.optionalWith).type.not.toBeCallableWith( + { as: "Option", exact: true, nullable: true, onNoneEncoding: () => Option.some(1) } + ) + expect(S.optionalWith).type.not.toBeCallableWith(S.String, { + as: "Option", + nullable: true, + onNoneEncoding: () => Option.some(1) + }) + expect(S.optionalWith).type.not.toBeCallableWith( + { as: "Option", nullable: true, onNoneEncoding: () => Option.some(1) } + ) + expect(S.optionalWith).type.not.toBeCallableWith(S.String, { as: null }) + expect(S.optionalWith).type.not.toBeCallableWith(S.String, { default: null }) + }) + + it("used in a generic context", () => { + type TypeWithValue = { value: S.optionalWith } + const makeTypeWithValue = (value: Value): TypeWithValue => ({ + value: S.optionalWith(value, { nullable: true }) + }) + expect(makeTypeWithValue(S.String)).type.toBe>() + }) + + it("{ exact: true, default: () => A }", () => { + expect( + S.asSchema( + S.Struct({ + a: S.String, + b: S.Number, + c: S.optionalWith(S.Boolean, { exact: true, default: () => false }) + }) + ) + ).type.toBe< + S.Schema< + { readonly a: string; readonly b: number; readonly c: boolean }, + { readonly a: string; readonly b: number; readonly c?: boolean }, + never + > + >() + + expect( + S.Struct({ + a: S.String, + b: S.Number, + c: S.optionalWith(S.Boolean, { exact: true, default: () => false }) + }) + ).type.toBe< + S.Struct<{ + a: typeof S.String + b: typeof S.Number + c: S.optionalWith false }> + }> + >() + + expect( + S.asSchema( + S.Struct({ + a: S.String, + b: S.Number, + c: S.optionalWith(S.NumberFromString, { exact: true, default: () => 0 }) + }) + ) + ).type.toBe< + S.Schema< + { readonly a: string; readonly b: number; readonly c: number }, + { readonly a: string; readonly b: number; readonly c?: string }, + never + > + >() + + expect( + S.Struct({ + a: S.String, + b: S.Number, + c: S.optionalWith(S.NumberFromString, { exact: true, default: () => 0 }) + }) + ).type.toBe< + S.Struct<{ + a: typeof S.String + b: typeof S.Number + c: S.optionalWith number }> + }> + >() + + expect( + S.Struct({ a: S.optionalWith(S.Literal("a", "b"), { default: () => "a", exact: true }) }) + ).type.toBe< + S.Struct<{ + a: S.optionalWith, { default: () => "a"; exact: true }> + }> + >() + + expect( + S.asSchema( + S.Struct({ a: S.Literal("a", "b").pipe(S.optionalWith({ default: () => "a", exact: true })) }) + ) + ).type.toBe< + S.Schema< + { readonly a: "a" | "b" }, + { readonly a?: "a" | "b" }, + never + > + >() + + expect( + S.Struct({ a: S.Literal("a", "b").pipe(S.optionalWith({ default: () => "a", exact: true })) }) + ).type.toBe< + S.Struct<{ + a: S.optionalWith, { default: () => "a"; exact: true }> + }> + >() + }) + + it("{ default: () => A }", () => { + expect( + S.asSchema( + S.Struct({ a: S.String, b: S.Number, c: S.optionalWith(S.Boolean, { default: () => false }) }) + ) + ).type.toBe< + S.Schema< + { readonly a: string; readonly b: number; readonly c: boolean }, + { readonly a: string; readonly b: number; readonly c?: boolean | undefined }, + never + > + >() + + expect( + S.Struct({ a: S.String, b: S.Number, c: S.optionalWith(S.Boolean, { default: () => false }) }) + ).type.toBe< + S.Struct<{ + a: typeof S.String + b: typeof S.Number + c: S.optionalWith false }> + }> + >() + + expect( + S.asSchema( + S.Struct({ a: S.String, b: S.Number, c: S.optionalWith(S.NumberFromString, { default: () => 0 }) }) + ) + ).type.toBe< + S.Schema< + { readonly a: string; readonly b: number; readonly c: number }, + { readonly a: string; readonly b: number; readonly c?: string | undefined }, + never + > + >() + + expect( + S.Struct({ a: S.String, b: S.Number, c: S.optionalWith(S.NumberFromString, { default: () => 0 }) }) + ).type.toBe< + S.Struct<{ + a: typeof S.String + b: typeof S.Number + c: S.optionalWith number }> + }> + >() + + expect( + S.Struct({ a: S.optionalWith(S.Literal("a", "b"), { default: () => "a" }) }) + ).type.toBe< + S.Struct<{ + a: S.optionalWith, { default: () => "a" }> + }> + >() + + expect( + S.asSchema( + S.Struct({ a: S.Literal("a", "b").pipe(S.optionalWith({ default: () => "a" })) }) + ) + ).type.toBe< + S.Schema< + { readonly a: "a" | "b" }, + { readonly a?: "a" | "b" | undefined }, + never + > + >() + + expect( + S.Struct({ a: S.Literal("a", "b").pipe(S.optionalWith({ default: () => "a" })) }) + ).type.toBe< + S.Struct<{ + a: S.optionalWith, { default: () => "a" }> + }> + >() + }) + + it("{ exact: true, nullable: true, default: () => A }", () => { + expect( + S.asSchema( + S.Struct({ a: S.optionalWith(S.NumberFromString, { exact: true, nullable: true, default: () => 0 }) }) + ) + ).type.toBe< + S.Schema< + { readonly a: number }, + { readonly a?: string | null }, + never + > + >() + + expect( + S.Struct({ a: S.optionalWith(S.NumberFromString, { exact: true, nullable: true, default: () => 0 }) }) + ).type.toBe< + S.Struct<{ + a: S.optionalWith number }> + }> + >() + }) + + it("{ nullable: true, default: () => A }", () => { + expect( + S.asSchema( + S.Struct({ a: S.optionalWith(S.NumberFromString, { nullable: true, default: () => 0 }) }) + ) + ).type.toBe< + S.Schema< + { readonly a: number }, + { readonly a?: string | null | undefined }, + never + > + >() + + expect( + S.Struct({ a: S.optionalWith(S.NumberFromString, { nullable: true, default: () => 0 }) }) + ).type.toBe< + S.Struct<{ + a: S.optionalWith number }> + }> + >() + + expect( + S.asSchema( + S.Struct({ a: S.optionalWith(S.NumberFromString, { exact: true, nullable: true, default: () => 0 }) }) + ) + ).type.toBe< + S.Schema< + { readonly a: number }, + { readonly a?: string | null }, + never + > + >() + + expect( + S.Struct({ a: S.optionalWith(S.NumberFromString, { exact: true, nullable: true, default: () => 0 }) }) + ).type.toBe< + S.Struct<{ + a: S.optionalWith number }> + }> + >() + + expect( + S.Struct({ a: S.optionalWith(S.Literal("a", "b"), { default: () => "a", nullable: true }) }) + ).type.toBe< + S.Struct<{ + a: S.optionalWith, { default: () => "a"; nullable: true }> + }> + >() + + expect( + S.asSchema( + S.Struct({ a: S.Literal("a", "b").pipe(S.optionalWith({ default: () => "a", nullable: true })) }) + ) + ).type.toBe< + S.Schema< + { readonly a: "a" | "b" }, + { readonly a?: "a" | "b" | null | undefined }, + never + > + >() + + expect( + S.Struct({ a: S.Literal("a", "b").pipe(S.optionalWith({ default: () => "a", nullable: true })) }) + ).type.toBe< + S.Struct<{ + a: S.optionalWith, { default: () => "a"; nullable: true }> + }> + >() + }) + + it("{ exact: true, as: 'Option' }", () => { + expect( + S.asSchema( + S.Struct({ a: S.String, b: S.Number, c: S.optionalWith(S.Boolean, { exact: true, as: "Option" }) }) + ) + ).type.toBe< + S.Schema< + { readonly a: string; readonly b: number; readonly c: Option.Option }, + { readonly a: string; readonly b: number; readonly c?: boolean }, + never + > + >() + + expect( + S.Struct({ a: S.String, b: S.Number, c: S.optionalWith(S.Boolean, { exact: true, as: "Option" }) }) + ).type.toBe< + S.Struct<{ + a: typeof S.String + b: typeof S.Number + c: S.optionalWith + }> + >() + + expect( + S.asSchema( + S.Struct({ a: S.String, b: S.Number, c: S.optionalWith(S.NumberFromString, { exact: true, as: "Option" }) }) + ) + ).type.toBe< + S.Schema< + { readonly a: string; readonly b: number; readonly c: Option.Option }, + { readonly a: string; readonly b: number; readonly c?: string }, + never + > + >() + + expect( + S.Struct({ a: S.String, b: S.Number, c: S.optionalWith(S.NumberFromString, { exact: true, as: "Option" }) }) + ).type.toBe< + S.Struct<{ + a: typeof S.String + b: typeof S.Number + c: S.optionalWith + }> + >() + + expect( + S.asSchema(S.Struct({ a: S.String.pipe(S.optionalWith({ exact: true, as: "Option" })) })) + ).type.toBe< + S.Schema< + { readonly a: Option.Option }, + { readonly a?: string }, + never + > + >() + + expect( + S.Struct({ a: S.String.pipe(S.optionalWith({ exact: true, as: "Option" })) }) + ).type.toBe< + S.Struct<{ + a: S.optionalWith + }> + >() + }) + + it("{ as: 'Option' }", () => { + expect( + S.asSchema( + S.Struct({ a: S.String, b: S.Number, c: S.optionalWith(S.Boolean, { as: "Option" }) }) + ) + ).type.toBe< + S.Schema< + { readonly a: string; readonly b: number; readonly c: Option.Option }, + { readonly a: string; readonly b: number; readonly c?: boolean | undefined }, + never + > + >() + expect( + S.Struct({ a: S.String, b: S.Number, c: S.optionalWith(S.Boolean, { as: "Option" }) }) + ).type.toBe< + S.Struct<{ + a: typeof S.String + b: typeof S.Number + c: S.optionalWith + }> + >() + expect( + S.asSchema( + S.Struct({ a: S.String, b: S.Number, c: S.optionalWith(S.NumberFromString, { as: "Option" }) }) + ) + ).type.toBe< + S.Schema< + { readonly a: string; readonly b: number; readonly c: Option.Option }, + { readonly a: string; readonly b: number; readonly c?: string | undefined }, + never + > + >() + expect( + S.Struct({ a: S.String, b: S.Number, c: S.optionalWith(S.NumberFromString, { as: "Option" }) }) + ).type.toBe< + S.Struct<{ + a: typeof S.String + b: typeof S.Number + c: S.optionalWith + }> + >() + expect( + S.asSchema(S.Struct({ a: S.String.pipe(S.optionalWith({ as: "Option" })) })) + ).type.toBe< + S.Schema< + { readonly a: Option.Option }, + { readonly a?: string | undefined }, + never + > + >() + expect( + S.Struct({ a: S.String.pipe(S.optionalWith({ as: "Option" })) }) + ).type.toBe< + S.Struct<{ + a: S.optionalWith + }> + >() + }) + + it("{ nullable: true, as: 'Option' }", () => { + expect( + S.asSchema( + S.Struct({ a: S.optionalWith(S.NumberFromString, { nullable: true, as: "Option" }) }) + ) + ).type.toBe< + S.Schema< + { readonly a: Option.Option }, + { readonly a?: string | null | undefined }, + never + > + >() + expect( + S.Struct({ a: S.optionalWith(S.NumberFromString, { nullable: true, as: "Option" }) }) + ).type.toBe< + S.Struct<{ + a: S.optionalWith + }> + >() + expect( + S.asSchema(S.Struct({ a: S.String.pipe(S.optionalWith({ nullable: true, as: "Option" })) })) + ).type.toBe< + S.Schema< + { readonly a: Option.Option }, + { readonly a?: string | null | undefined }, + never + > + >() + expect( + S.Struct({ a: S.String.pipe(S.optionalWith({ nullable: true, as: "Option" })) }) + ).type.toBe< + S.Struct<{ + a: S.optionalWith + }> + >() + }) + + it("{ exact: true, nullable: true, as: 'Option' }", () => { + expect( + S.asSchema( + S.Struct({ a: S.optionalWith(S.NumberFromString, { exact: true, nullable: true, as: "Option" }) }) + ) + ).type.toBe< + S.Schema< + { readonly a: Option.Option }, + { readonly a?: string | null }, + never + > + >() + expect( + S.Struct({ a: S.optionalWith(S.NumberFromString, { exact: true, nullable: true, as: "Option" }) }) + ).type.toBe< + S.Struct<{ + a: S.optionalWith + }> + >() + expect( + S.asSchema(S.Struct({ a: S.String.pipe(S.optionalWith({ exact: true, nullable: true, as: "Option" })) })) + ).type.toBe< + S.Schema< + { readonly a: Option.Option }, + { readonly a?: string | null }, + never + > + >() + expect( + S.Struct({ a: S.String.pipe(S.optionalWith({ exact: true, nullable: true, as: "Option" })) }) + ).type.toBe< + S.Struct<{ + a: S.optionalWith + }> + >() + }) + }) + + describe("pick", () => { + it("required fields", () => { + when(pipe).isCalledWith( + S.Struct({ a: S.propertySignature(S.Number).pipe(S.fromKey("c")) }), + expect(S.pick).type.not.toBeCallableWith("a") + ) + expect( + pipe(S.Struct({ a: S.String, b: S.Number, c: S.Boolean }), S.pick("a", "b")) + ).type.toBe< + S.SchemaClass<{ readonly a: string; readonly b: number }, { readonly a: string; readonly b: number }> + >() + expect( + pipe(S.Struct({ a: S.String, b: S.NumberFromString, c: S.Boolean }), S.pick("a", "b")) + ).type.toBe< + S.SchemaClass<{ readonly a: string; readonly b: number }, { readonly a: string; readonly b: string }> + >() + }) + + it("optional fields", () => { + expect( + pipe( + S.Struct({ a: S.optionalWith(S.String, { exact: true }), b: S.Number, c: S.Boolean }), + S.pick("a", "b") + ) + ).type.toBe< + S.SchemaClass<{ readonly a?: string; readonly b: number }, { readonly a?: string; readonly b: number }> + >() + expect( + pipe( + S.Struct({ a: S.optionalWith(S.String, { exact: true }), b: S.NumberFromString, c: S.Boolean }), + S.pick("a", "b") + ) + ).type.toBe< + S.SchemaClass<{ readonly a?: string; readonly b: number }, { readonly a?: string; readonly b: string }> + >() + expect( + pipe( + S.Struct({ + a: S.optionalWith(S.String, { exact: true, default: () => "" }), + b: S.NumberFromString, + c: S.Boolean + }), + S.pick("a", "b") + ) + ).type.toBe< + S.SchemaClass<{ readonly a: string; readonly b: number }, { readonly a?: string; readonly b: string }> + >() + }) + }) + + describe("omit", () => { + it("required fields", () => { + when(pipe).isCalledWith( + S.Struct({ a: S.propertySignature(S.Number).pipe(S.fromKey("c")) }), + expect(S.omit).type.not.toBeCallableWith("a") + ) + expect( + pipe(S.Struct({ a: S.String, b: S.Number, c: S.Boolean }), S.omit("c")) + ).type.toBe< + S.SchemaClass<{ readonly a: string; readonly b: number }, { readonly a: string; readonly b: number }> + >() + expect( + pipe(S.Struct({ a: S.String, b: S.NumberFromString, c: S.Boolean }), S.omit("c")) + ).type.toBe< + S.SchemaClass<{ readonly a: string; readonly b: number }, { readonly a: string; readonly b: string }> + >() + }) + + it("optional fields", () => { + expect( + pipe( + S.Struct({ a: S.optionalWith(S.String, { exact: true }), b: S.Number, c: S.Boolean }), + S.omit("c") + ) + ).type.toBe< + S.SchemaClass<{ readonly a?: string; readonly b: number }, { readonly a?: string; readonly b: number }> + >() + expect( + pipe( + S.Struct({ a: S.optionalWith(S.String, { exact: true }), b: S.NumberFromString, c: S.Boolean }), + S.omit("c") + ) + ).type.toBe< + S.SchemaClass<{ readonly a?: string; readonly b: number }, { readonly a?: string; readonly b: string }> + >() + expect( + pipe( + S.Struct({ + a: S.optionalWith(S.String, { exact: true, default: () => "" }), + b: S.NumberFromString, + c: S.Boolean + }), + S.omit("c") + ) + ).type.toBe< + S.SchemaClass<{ readonly a: string; readonly b: number }, { readonly a?: string; readonly b: string }> + >() + }) + }) + + it("brand", () => { + const schema = pipe(S.Number, S.int(), S.brand("Int")) + expect(S.asSchema(schema)).type.toBe, number>>() + expect(schema).type.toBe, "Int">>() + expect(schema.annotations({})).type.toBe, "Int">>() + expect(schema.from).type.toBe>() + + const schema2 = pipe(S.NumberFromString, S.int(), S.brand("Int")) + expect(S.asSchema(schema2)).type.toBe, string>>() + expect(schema2).type.toBe, "Int">>() + }) + + it("partial", () => { + expect(S.partial(S.Struct({ a: S.String, b: S.Number }))) + .type.toBe< + S.SchemaClass< + { readonly a?: string | undefined; readonly b?: number | undefined }, + { readonly a?: string | undefined; readonly b?: number | undefined }, + never + > + >() + expect(S.partial(S.Struct({ a: S.String, b: S.NumberFromString }))) + .type.toBe< + S.SchemaClass< + { readonly a?: string | undefined; readonly b?: number | undefined }, + { readonly a?: string | undefined; readonly b?: string | undefined }, + never + > + >() + expect(S.Struct({ a: S.String, b: S.Number }).pipe(S.partial)) + .type.toBe< + S.SchemaClass< + { readonly a?: string | undefined; readonly b?: number | undefined }, + { readonly a?: string | undefined; readonly b?: number | undefined }, + never + > + >() + }) + + it("partialWith", () => { + expect(S.partialWith(S.Struct({ a: S.String, b: S.Number }), { exact: true })) + .type.toBe< + S.SchemaClass< + { readonly a?: string; readonly b?: number }, + { readonly a?: string; readonly b?: number }, + never + > + >() + expect(S.partialWith(S.Struct({ a: S.String, b: S.NumberFromString }), { exact: true })) + .type.toBe< + S.SchemaClass< + { readonly a?: string; readonly b?: number }, + { readonly a?: string; readonly b?: string }, + never + > + >() + expect(S.Struct({ a: S.String, b: S.Number }).pipe(S.partialWith({ exact: true }))) + .type.toBe< + S.SchemaClass< + { readonly a?: string; readonly b?: number }, + { readonly a?: string; readonly b?: number }, + never + > + >() + }) + + it("required with optionalWith", () => { + expect( + S.required( + S.Struct({ a: S.optionalWith(S.String, { exact: true }), b: S.optionalWith(S.Number, { exact: true }) }) + ) + ).type.toBe< + S.SchemaClass< + { readonly a: string; readonly b: number }, + { readonly a: string; readonly b: number }, + never + > + >() + expect( + S.required( + S.Struct({ + a: S.optionalWith(S.String, { exact: true }), + b: S.NumberFromString, + c: S.optionalWith(S.NumberFromString, { exact: true }) + }) + ) + ).type.toBe< + S.SchemaClass< + { readonly a: string; readonly b: number; readonly c: number }, + { readonly a: string; readonly b: string; readonly c: string }, + never + > + >() + }) + + it("extend", () => { + expect( + S.asSchema( + pipe( + S.Struct({ a: S.String, b: S.String }), + S.extend(S.Struct({ c: S.String })) + ) + ) + ).type.toBe< + S.Schema< + { readonly a: string; readonly b: string } & { readonly c: string }, + { readonly a: string; readonly b: string } & { readonly c: string }, + never + > + >() + expect( + pipe( + S.Struct({ a: S.String, b: S.String }), + S.extend(S.Struct({ c: S.String })) + ) + ).type.toBe, S.Struct<{ c: typeof S.String }>>>() + expect( + S.asSchema(S.extend(S.Struct({ a: S.String, b: S.String }), S.Struct({ c: S.String }))) + ).type.toBe< + S.Schema< + { readonly a: string; readonly b: string } & { readonly c: string }, + { readonly a: string; readonly b: string } & { readonly c: string }, + never + > + >() + expect( + S.extend(S.Struct({ a: S.String, b: S.String }), S.Struct({ c: S.String })) + ).type.toBe, S.Struct<{ c: typeof S.String }>>>() + expect( + S.asSchema( + S.extend(S.Struct({ a: S.String }), S.Union(S.Struct({ b: S.Number }), S.Struct({ c: S.Boolean }))) + ) + ).type.toBe< + S.Schema< + { readonly a: string } & ({ readonly b: number } | { readonly c: boolean }), + { readonly a: string } & ({ readonly b: number } | { readonly c: boolean }), + never + > + >() + expect( + S.extend(S.Struct({ a: S.String }), S.Union(S.Struct({ b: S.Number }), S.Struct({ c: S.Boolean }))) + ).type.toBe< + S.extend< + S.Struct<{ a: typeof S.String }>, + S.Union<[S.Struct<{ b: typeof S.Number }>, S.Struct<{ c: typeof S.Boolean }>]> + > + >() + expect( + S.asSchema( + pipe( + S.Struct({ a: S.String, b: S.String }), + S.extend(S.Struct({ c: S.String })), + S.extend(S.Record({ key: S.String, value: S.String })) + ) + ) + ).type.toBe< + S.Schema< + { readonly a: string; readonly b: string } & { readonly c: string } & { readonly [x: string]: string } + > + >() + expect( + pipe( + S.Struct({ a: S.String, b: S.String }), + S.extend(S.Struct({ c: S.String })), + S.extend(S.Record({ key: S.String, value: S.String })) + ) + ).type.toBe< + S.extend< + S.extend, S.Struct<{ c: typeof S.String }>>, + S.Record$ + > + >() + }) + + it("suspend", () => { + interface SuspendIEqualA { + readonly a: number + readonly as: ReadonlyArray + } + const SuspendIEqualA = S.Struct({ + a: S.Number, + as: S.Array(S.suspend((): S.Schema => SuspendIEqualA)) + }) + expect(SuspendIEqualA.fields) + .type.toBe< + { readonly a: typeof S.Number; readonly as: S.Array$> } + >() + + interface SuspendINotEqualA_A { + readonly a: string + readonly as: ReadonlyArray + } + interface SuspendINotEqualA_I { + readonly a: number + readonly as: ReadonlyArray + } + const SuspendINotEqualA = S.Struct({ + a: S.NumberFromString, + as: S.Array(S.suspend((): S.Schema => SuspendINotEqualA)) + }) + expect(SuspendINotEqualA.fields).type.toBe< + { + readonly a: typeof S.NumberFromString + readonly as: S.Array$> + } + >() + }) + + it("rename", () => { + expect(S.rename(S.Struct({ a: S.String, b: S.Number }), {})) + .type.toBe< + S.SchemaClass< + { readonly a: string; readonly b: number }, + { readonly a: string; readonly b: number }, + never + > + >() + expect(S.rename(S.Struct({ a: S.String, b: S.Number }), { a: "c" })) + .type.toBe< + S.SchemaClass< + { readonly c: string; readonly b: number }, + { readonly a: string; readonly b: number }, + never + > + >() + expect(S.rename(S.Struct({ a: S.String, b: S.Number }), { a: "c", b: "d" })) + .type.toBe< + S.SchemaClass< + { readonly c: string; readonly d: number }, + { readonly a: string; readonly b: number }, + never + > + >() + const a = Symbol.for("effect/Schema/dtslint/a") + expect(S.rename(S.Struct({ a: S.String, b: S.Number }), { a })) + .type.toBe< + S.SchemaClass< + { readonly [a]: string; readonly b: number }, + { readonly a: string; readonly b: number }, + never + > + >() + expect(S.rename).type.not.toBeCallableWith(S.Struct({ a: S.String, b: S.Number }), { c: "d" }) + expect(S.rename).type.not.toBeCallableWith(S.Struct({ a: S.String, b: S.Number }), { a: "c", d: "e" }) + expect(S.Struct({ a: S.String, b: S.Number }).pipe(S.rename({}))) + .type.toBe< + S.SchemaClass< + { readonly a: string; readonly b: number }, + { readonly a: string; readonly b: number }, + never + > + >() + expect(S.Struct({ a: S.String, b: S.Number }).pipe(S.rename({ a: "c" }))) + .type.toBe< + S.SchemaClass< + { readonly c: string; readonly b: number }, + { readonly a: string; readonly b: number }, + never + > + >() + when(S.Struct({ a: S.String, b: S.Number }).pipe).isCalledWith( + expect(S.rename).type.not.toBeCallableWith({ c: "d" }) + ) + when(S.Struct({ a: S.String, b: S.Number }).pipe).isCalledWith( + expect(S.rename).type.not.toBeCallableWith({ a: "c", d: "e" }) + ) + }) + + describe("declare", () => { + it("instanceOf", () => { + class Test { + constructor(readonly name: string) {} + } + const schema = S.instanceOf(Test) + expect(S.asSchema(schema)).type.toBe>() + expect(schema).type.toBe>() + expect(schema.annotations({})).type.toBe>() + // should expose the type parameters + expect(schema.typeParameters).type.toBe() + }) + }) + + it("TemplateLiteral", () => { + expect(S.TemplateLiteral("a")) + .type.toBe>() + expect(S.TemplateLiteral(S.Literal("a"))) + .type.toBe>() + expect(S.TemplateLiteral(1)) + .type.toBe>() + expect(S.TemplateLiteral(S.Literal(1))) + .type.toBe>() + expect(S.TemplateLiteral(S.String)) + .type.toBe>() + expect(S.TemplateLiteral(S.Number)) + .type.toBe>() + expect(S.TemplateLiteral("a", "b")) + .type.toBe>() + expect(S.TemplateLiteral(S.Literal("a"), S.Literal("b"))) + .type.toBe>() + expect(S.TemplateLiteral("a", S.String)) + .type.toBe>() + expect(S.TemplateLiteral(S.Literal("a"), S.String)) + .type.toBe>() + expect(S.TemplateLiteral("a", S.Number)) + .type.toBe>() + expect(S.TemplateLiteral(S.Literal("a"), S.Number)) + .type.toBe>() + expect(S.TemplateLiteral(S.String, "a")) + .type.toBe>() + expect(S.TemplateLiteral(S.String, S.Literal("a"))) + .type.toBe>() + expect(S.TemplateLiteral(S.Number, "a")) + .type.toBe>() + expect(S.TemplateLiteral(S.Number, S.Literal("a"))) + .type.toBe>() + expect(S.TemplateLiteral(S.String, 0)) + .type.toBe>() + expect(S.TemplateLiteral(S.String, true)) + .type.toBe>() + expect(S.TemplateLiteral(S.String, null)) + .type.toBe>() + expect(S.TemplateLiteral(S.String, 1n)) + .type.toBe>() + expect(S.TemplateLiteral(S.String, S.Literal("a", 0))) + .type.toBe>() + expect(S.TemplateLiteral(S.String, S.Literal("/"), S.Number)) + .type.toBe>() + expect(S.TemplateLiteral(S.String, "/", S.Number)) + .type.toBe>() + const EmailLocaleIDs = S.Literal("welcome_email", "email_heading") + const FooterLocaleIDs = S.Literal("footer_title", "footer_sendoff") + expect(S.asSchema(S.TemplateLiteral(S.Union(EmailLocaleIDs, FooterLocaleIDs), S.Literal("_id")))) + .type.toBe< + S.Schema< + "welcome_email_id" | "email_heading_id" | "footer_title_id" | "footer_sendoff_id", + "welcome_email_id" | "email_heading_id" | "footer_title_id" | "footer_sendoff_id", + never + > + >() + expect(S.TemplateLiteral(S.Union(EmailLocaleIDs, FooterLocaleIDs), "_id")) + .type.toBe< + S.TemplateLiteral< + "welcome_email_id" | "email_heading_id" | "footer_title_id" | "footer_sendoff_id" + > + >() + expect(S.TemplateLiteral(S.String.pipe(S.brand("MyBrand")))) + .type.toBe}`>>() + expect(S.TemplateLiteral(S.Number.pipe(S.brand("MyBrand")))) + .type.toBe}`>>() + expect(S.TemplateLiteral("a", S.String.pipe(S.brand("MyBrand")))) + .type.toBe}`>>() + expect(S.TemplateLiteral(S.Literal("a"), S.String.pipe(S.brand("MyBrand")))) + .type.toBe}`>>() + expect(S.TemplateLiteral(S.Literal("a").pipe(S.brand("L")), S.String.pipe(S.brand("MyBrand")))) + .type.toBe}${string & Brand.Brand<"MyBrand">}`>>() + expect(S.TemplateLiteral("a", S.Number.pipe(S.brand("MyBrand")))) + .type.toBe}`>>() + expect(S.TemplateLiteral(S.Literal("a"), S.Number.pipe(S.brand("MyBrand")))) + .type.toBe}`>>() + expect(S.TemplateLiteral("a", S.Union(S.Number, S.String))) + .type.toBe>() + }) + + it("attachPropertySignature", () => { + expect( + pipe(S.Struct({ radius: S.Number }), S.attachPropertySignature("kind", "circle")) + ).type.toBe< + S.SchemaClass< + { readonly radius: number } & { readonly kind: "circle" }, + { readonly radius: number }, + never + > + >() + expect( + pipe(S.Struct({ radius: S.NumberFromString }), S.attachPropertySignature("kind", "circle")) + ).type.toBe< + S.SchemaClass< + { readonly radius: number } & { readonly kind: "circle" }, + { readonly radius: string }, + never + > + >() + expect(S.attachPropertySignature(S.Struct({ radius: S.Number }), "kind", "circle")) + .type.toBe< + S.SchemaClass< + { readonly radius: number } & { readonly kind: "circle" }, + { readonly radius: number }, + never + > + >() + expect(S.attachPropertySignature(S.Struct({ radius: S.NumberFromString }), "kind", "circle")) + .type.toBe< + S.SchemaClass< + { readonly radius: number } & { readonly kind: "circle" }, + { readonly radius: string }, + never + > + >() + const taggedStruct = ( + name: Name, + fields: Fields + ) => S.Struct(fields).pipe(S.attachPropertySignature("_tag", name)) + expect(taggedStruct("A", { a: S.String })) + .type.toBe< + S.SchemaClass< + { readonly a: string } & { readonly _tag: "A" }, + { readonly a: string }, + never + > + >() + // should work with generic code + const _f = (B: S.Schema, input: unknown) => { + const union = S.Union( + S.Struct({ code: S.Number }).pipe(S.attachPropertySignature("ok", false)), + B.pipe(S.attachPropertySignature("ok", true)) + ) + S.decodeUnknown(union)(input).pipe(Effect.flatMap((data) => { + if (!data.ok) { + expect(data.code).type.toBe() + } + return Effect.succeed(data) + })) + } + }) + + it("filterEffect", () => { + expect( + S.String.pipe(S.filterEffect((s) => { + expect(s).type.toBe() + return Effect.succeed(undefined) + })) + ).type.toBe>() + expect( + S.filterEffect(S.String, (s) => { + expect(s).type.toBe() + return Effect.succeed(undefined) + }) + ).type.toBe>() + + expect( + S.String.pipe( + S.filterEffect((s) => + Effect.gen(function*() { + const str = yield* ServiceA + return str === s + }) + ) + ) + ).type.toBe>() + expect( + S.filterEffect(S.String, (s) => + Effect.gen(function*() { + const str = yield* ServiceA + return str === s + })) + ).type.toBe>() + }) + + describe("compose", () => { + it("{ strict: true } should not allow incompatible types", () => { + expect(S.compose).type.not.toBeCallableWith( + S.String, + S.Number + ) + expect(S.compose).type.not.toBeCallableWith( + S.String, + S.Number, + { strict: true } + ) + when(S.String.pipe).isCalledWith( + expect(S.compose).type.not.toBeCallableWith(S.Number, { strict: true }) + ) + }) + + it("{ strict: false } should allow incompatible types", () => { + S.compose(S.String, S.Number, { strict: false }) + S.String.pipe(S.compose(S.Number, { strict: false })) + }) + + it("data last", () => { + // first overload + const schema1_1 = S.split(",").pipe(S.compose(S.Array(S.NumberFromString))) + expect(S.asSchema(schema1_1)).type.toBe, string>>() + expect(schema1_1) + .type.toBe< + S.transform< + S.transform, S.Array$>, + S.Array$ + > + >() + expect(schema1_1.annotations({})) + .type.toBe< + S.transform< + S.transform, S.Array$>, + S.Array$ + > + >() + const schema1_2 = S.Union(S.Null, S.String).pipe(S.compose(S.NumberFromString)) + expect(S.asSchema(schema1_2)).type.toBe>() + expect(schema1_2) + .type.toBe, typeof S.NumberFromString>>() + expect(schema1_2.annotations({})) + .type.toBe, typeof S.NumberFromString>>() + expect(schema1_2.to).type.toBe() + + // second overload + const schema2 = S.NumberFromString.pipe(S.compose(S.Union(S.Null, S.Number))) + expect(schema2) + .type.toBe>>() + expect(schema2.annotations({})) + .type.toBe>>() + + // third overload + const schema3 = S.split(",").pipe(S.compose(S.Array(S.NumberFromString), { strict: true })) + expect(S.asSchema(schema3)).type.toBe, string>>() + expect(schema3).type.toBe< + S.transform< + S.transform, S.Array$>, + S.Array$ + > + >() + expect(schema3.annotations({})).type.toBe< + S.transform< + S.transform, S.Array$>, + S.Array$ + > + >() + + // fourth overload + const schema4 = S.String.pipe(S.compose(S.Number, { strict: false })) + expect(S.asSchema(schema4)).type.toBe>() + expect(schema4).type.toBe>() + expect(schema4.annotations({})).type.toBe>() + expect(schema4.from).type.toBe() + expect(schema4.to).type.toBe() + }) + + it("data first", () => { + // first overload + const schema1_1 = S.compose(S.split(","), S.Array(S.NumberFromString)) + expect(S.asSchema(schema1_1)).type.toBe, string>>() + expect(schema1_1) + .type.toBe< + S.transform< + S.transform, S.Array$>, + S.Array$ + > + >() + expect(schema1_1.annotations({})) + .type.toBe< + S.transform< + S.transform, S.Array$>, + S.Array$ + > + >() + const schema1_2 = S.compose(S.Union(S.Null, S.String), S.NumberFromString) + expect(S.asSchema(schema1_2)).type.toBe>() + expect(schema1_2) + .type.toBe, typeof S.NumberFromString>>() + expect(schema1_2.annotations({})) + .type.toBe, typeof S.NumberFromString>>() + expect(schema1_2.to).type.toBe() + + // second overload + const schema2 = S.compose(S.NumberFromString, S.Union(S.Null, S.Number)) + expect(schema2) + .type.toBe>>() + expect(schema2.annotations({})) + .type.toBe>>() + + // third overload + const schema3 = S.compose(S.split(","), S.Array(S.NumberFromString), { strict: true }) + expect(S.asSchema(schema3)).type.toBe, string>>() + expect(schema3).type.toBe< + S.transform< + S.transform, S.Array$>, + S.Array$ + > + >() + expect(schema3.annotations({})).type.toBe< + S.transform< + S.transform, S.Array$>, + S.Array$ + > + >() + + // fourth overload + const schema4 = S.compose(S.String, S.Number, { strict: false }) + expect(S.asSchema(schema4)).type.toBe>() + expect(schema4).type.toBe>() + expect(schema4.annotations({})).type.toBe>() + expect(schema4.from).type.toBe() + expect(schema4.to).type.toBe() + }) + }) + + it("fromBrand", () => { + type Eur = number & Brand.Brand<"Eur"> + const Eur = Brand.nominal() + expect(S.Number.pipe(S.fromBrand(Eur))) + .type.toBe, number>>() + }) + + it("mutable", () => { + expect(S.asSchema(S.mutable(S.String))) + .type.toBe>() + S.mutable(S.String) + expect(S.asSchema(S.mutable(S.Struct({ a: S.Number })))) + .type.toBe>() + S.mutable(S.Struct({ a: S.Number })) + expect(S.asSchema(S.mutable(S.Record({ key: S.String, value: S.Number })))) + .type.toBe>() + S.mutable(S.Record({ key: S.String, value: S.Number })) + expect(S.asSchema(S.mutable(S.Array(S.String)))) + .type.toBe>>() + S.mutable(S.Array(S.String)) + expect(S.asSchema(S.mutable(S.Union(S.Struct({ a: S.Number }), S.Array(S.String))))) + .type.toBe | { a: number }, Array | { a: number }>>() + S.mutable(S.Union(S.Struct({ a: S.Number }), S.Array(S.String))) + expect(S.asSchema(S.mutable(S.Array(S.String).pipe(S.maxItems(2))))) + .type.toBe>>() + expect(S.asSchema(S.mutable(S.NonEmptyArray(S.String).pipe(S.maxItems(2))))) + .type.toBe], [string, ...Array]>>() + expect(S.asSchema(S.mutable(S.suspend(() => S.Array(S.String))))) + .type.toBe>>() + S.mutable(S.suspend(() => S.Array(S.String))) + expect( + S.asSchema(S.mutable(S.transform(S.Array(S.String), S.Array(S.String), { decode: identity, encode: identity }))) + ) + .type.toBe>>() + S.mutable(S.transform(S.Array(S.String), S.Array(S.String), { decode: identity, encode: identity })) + expect(S.asSchema(S.extend(S.mutable(S.Struct({ a: S.String })), S.mutable(S.Struct({ b: S.Number }))))) + .type.toBe>() + expect(S.asSchema(S.extend(S.mutable(S.Struct({ a: S.String })), S.Struct({ b: S.Number })))) + .type.toBe>() + expect( + S.asSchema( + S.extend(S.mutable(S.Struct({ a: S.String })), S.mutable(S.Record({ key: S.String, value: S.String }))) + ) + ) + .type.toBe>() + expect(S.asSchema(S.extend(S.mutable(S.Struct({ a: S.String })), S.Record({ key: S.String, value: S.String })))) + .type.toBe< + S.Schema< + { a: string } & { readonly [x: string]: string }, + { a: string } & { readonly [x: string]: string }, + never + > + >() + }) + + it("transform", () => { + const transform1 = S.String.pipe( + S.transform(S.Number, { decode: (s) => s.length, encode: (n) => String(n) }) + ) + expect(transform1.from).type.toBe() + expect(transform1.to).type.toBe() + transform1.annotations({}) + expect(S.asSchema(transform1)) + .type.toBe>() + expect( + S.asSchema( + S.String.pipe(S.transform(S.Number, { strict: false, decode: (s) => s, encode: (n) => n })) + ) + ).type.toBe>() + S.String.pipe(S.transform(S.Number, { strict: false, decode: (s) => s, encode: (n) => n })) + when(S.String.pipe).isCalledWith( + expect(S.transform).type.not.toBeCallableWith(S.Number, (s: any) => s, (n: any) => String(n)) + ) + when(S.String.pipe).isCalledWith( + expect(S.transform).type.not.toBeCallableWith(S.Number, (s: any) => s.length, (n: any) => n) + ) + + // should receive the fromI value other than the fromA value + S.transform( + S.Struct({ + a: S.String, + b: S.NumberFromString + }), + S.Struct({ + a: S.NumberFromString + }), + { + strict: true, + decode: ({ a, b }, i) => { + expect(a).type.toBe() + expect(b).type.toBe() + expect(i).type.toBe<{ readonly a: string; readonly b: string }>() + return { a: a + i.b } + }, + encode: (i, a) => { + expect(i).type.toBe<{ readonly a: string }>() + expect(a).type.toBe<{ readonly a: number }>() + return { ...i, b: a.a * 2 } + } + } + ) + }) + + it("transformOrFail", () => { + const transformOrFail1 = S.String.pipe( + S.transformOrFail( + S.Number, + { decode: (s) => ParseResult.succeed(s.length), encode: (n) => ParseResult.succeed(String(n)) } + ) + ) + expect(transformOrFail1.from).type.toBe() + expect(transformOrFail1.to).type.toBe() + transformOrFail1.annotations({}) + expect(S.asSchema(transformOrFail1)) + .type.toBe>() + expect( + S.asSchema( + S.String.pipe( + S.transformOrFail( + S.Number, + { strict: false, decode: (s) => ParseResult.succeed(s), encode: (n) => ParseResult.succeed(String(n)) } + ) + ) + ) + ).type.toBe>() + S.String.pipe( + S.transformOrFail( + S.Number, + { strict: false, decode: (s) => ParseResult.succeed(s), encode: (n) => ParseResult.succeed(String(n)) } + ) + ) + when(S.String.pipe).isCalledWith( + expect(S.transformOrFail).type.not.toBeCallableWith( + S.Number, + (s: any) => ParseResult.succeed(s), + (n: any) => ParseResult.succeed(String(n)) + ) + ) + when(S.String.pipe).isCalledWith( + expect(S.transformOrFail).type.not.toBeCallableWith( + S.Number, + (s: any) => ParseResult.succeed(s.length), + (n: any) => ParseResult.succeed(n) + ) + ) + + // should receive the fromI value other than the fromA value + S.transformOrFail( + S.Struct({ + a: S.String, + b: S.NumberFromString + }), + S.Struct({ + a: S.NumberFromString + }), + { + strict: true, + decode: ({ a, b }, _options, _ast, i) => { + expect(a).type.toBe() + expect(b).type.toBe() + expect(i).type.toBe<{ readonly a: string; readonly b: string }>() + return ParseResult.succeed({ a: a + i.b }) + }, + encode: (i, _options, _ast, a) => { + expect(i).type.toBe<{ readonly a: string }>() + expect(a).type.toBe<{ readonly a: number }>() + return ParseResult.succeed({ ...i, b: a.a * 2 }) + } + } + ) + }) + + it("transformLiteral", () => { + const schema = S.transformLiteral(0, "a") + expect(S.asSchema(schema)).type.toBe>() + expect(schema).type.toBe>() + expect(schema.annotations({})).type.toBe>() + expect(schema.from).type.toBe>() + expect(schema.to).type.toBe>() + }) + + it("transformLiterals", () => { + const schema = S.transformLiterals([0, "a"], [1, "b"]) + expect(S.asSchema(schema)).type.toBe>() + expect(schema).type.toBe, S.transformLiteral<"b", 1>]>>() + expect(schema.annotations({})).type.toBe, S.transformLiteral<"b", 1>]>>() + + expect(S.transformLiterals([0, "a"])).type.toBe>() + const pairs = hole>() + expect(S.transformLiterals(...pairs)).type.toBe>() + }) + + it("propertySignature", () => { + expect(S.propertySignature(S.String)).type.toBe>() + expect(S.propertySignature(S.String).annotations({})).type.toBe>() + }) + + it("PropertySignature.annotations", () => { + expect(S.optional(S.String).annotations({})).type.toBe>() + }) + + it("TaggedClass", () => { + class MyTaggedClass extends S.TaggedClass()("MyTaggedClass", { + a: S.String + }) {} + expect(hole>()).type.toBe< + [props: { readonly a: string }, options?: S.MakeOptions | undefined] + >() + expect(hole>()).type.toBe< + { readonly a: string; readonly _tag: "MyTaggedClass" } + >() + expect(hole>()).type.toBe() + class VoidTaggedClass extends S.TaggedClass()("VoidTaggedClass", {}) {} + expect(hole>()).type.toBe< + [props?: void | {}, options?: S.MakeOptions | undefined] + >() + expect(S.asSchema(S.Struct(MyTaggedClass.fields))) + .type.toBe< + S.Schema< + { readonly a: string; readonly _tag: "MyTaggedClass" }, + { readonly a: string; readonly _tag: "MyTaggedClass" }, + never + > + >() + expect(hole["make"]>>()).type.toBe< + [props: { readonly a: string; readonly _tag?: "MyTaggedClass" }, options?: S.MakeOptions | undefined] + >() + }) + + it("TaggedError", () => { + class MyTaggedError extends S.TaggedError()("MyTaggedError", { + a: S.String + }) {} + expect(S.asSchema(S.Struct(MyTaggedError.fields))) + .type.toBe< + S.Schema< + { readonly a: string; readonly _tag: "MyTaggedError" }, + { readonly a: string; readonly _tag: "MyTaggedError" }, + never + > + >() + expect(hole["make"]>>()).type.toBe< + [props: { readonly a: string; readonly _tag?: "MyTaggedError" }, options?: S.MakeOptions | undefined] + >() + }) + + it("TaggedRequest", () => { + class MyTaggedRequest extends S.TaggedRequest()("MyTaggedRequest", { + failure: S.String, + success: S.Number, + payload: { a: S.String } + }) {} + expect(S.asSchema(S.Struct(MyTaggedRequest.fields))) + .type.toBe< + S.Schema< + { readonly a: string; readonly _tag: "MyTaggedRequest" }, + { readonly a: string; readonly _tag: "MyTaggedRequest" }, + never + > + >() + expect(hole["make"]>>()).type.toBe< + [props: { readonly a: string; readonly _tag?: "MyTaggedRequest" }, options?: S.MakeOptions | undefined] + >() + }) + + it("TypeLiteral", () => { + expect(S.asSchema(hole>())) + .type.toBe>() + expect(S.asSchema(hole>())) + .type.toBe>() + expect( + S.asSchema( + hole< + S.TypeLiteral< + {}, + [{ key: typeof S.String; value: typeof S.String }, { key: typeof S.Symbol; value: typeof S.Number }] + > + >() + ) + ).type.toBe< + S.Schema< + { readonly [x: string]: string; readonly [x: symbol]: number }, + { readonly [x: string]: never }, + never + > + >() + expect( + S.asSchema(hole>()) + ) + .type.toBe< + S.Schema< + { readonly [x: string]: unknown; readonly a: string }, + { readonly [x: string]: unknown; readonly a: string }, + never + > + >() + }) + + it("withConstructorDefault", () => { + when(S.propertySignature(S.String).pipe).isCalledWith( + expect(S.withConstructorDefault).type.not.toBeCallableWith(() => 1) + ) + expect(S.propertySignature(S.String).pipe(S.withConstructorDefault(() => "a"))) + .type.toBe>() + expect(S.withConstructorDefault(S.propertySignature(S.String), () => "a")) + .type.toBe>() + }) + + it("withDecodingDefault", () => { + when(S.Struct).isCalledWith({ + a: when(S.optional(S.String).pipe).isCalledWith( + S.withConstructorDefault(() => undefined), + expect(S.withDecodingDefault).type.not.toBeCallableWith(() => "") + ) + }) + when(S.Struct).isCalledWith({ + a: when(S.optional(S.String).pipe).isCalledWith( + expect(S.withDecodingDefault).type.not.toBeCallableWith(() => undefined) + ) + }) + expect( + S.asSchema(S.Struct({ a: S.optional(S.String).pipe(S.withDecodingDefault(() => "")) })) + ).type.toBe< + S.Schema<{ readonly a: string }, { readonly a?: string | undefined }> + >() + expect(S.Struct({ a: S.optional(S.String).pipe(S.withDecodingDefault(() => "")) })) + .type.toBe }>>() + }) + + it("withDefaults", () => { + when(S.Struct).isCalledWith({ + a: when(S.optional(S.String).pipe).isCalledWith( + expect(S.withDefaults).type.not.toBeCallableWith({ + decoding: () => undefined, + constructor: () => undefined + }) + ) + }) + expect( + S.asSchema( + S.Struct({ a: S.optional(S.String).pipe(S.withDefaults({ decoding: () => "", constructor: () => "" })) }) + ) + ).type.toBe< + S.Schema<{ readonly a: string }, { readonly a?: string | undefined }> + >() + expect( + S.Struct({ a: S.optional(S.String).pipe(S.withDefaults({ decoding: () => "", constructor: () => "" })) }) + ).type.toBe }>>() + const make4 = + S.Struct({ a: S.optional(S.String).pipe(S.withDefaults({ decoding: () => "", constructor: () => "" })) }).make + expect(hole[0]>()).type.toBe() + }) + + it("Schema.AsSchema", () => { + const MyStruct = (x: X) => S.Struct({ x }) + type MyStructReturnType = S.Schema.Type>> + function _AsSchemaTest1(obj: MyStructReturnType>) { + expect(obj.x).type.toBe>() + } + type XStruct = S.Schema< + S.Struct.Type<{ + expectedVersion: typeof S.Number + props: X + }>, + S.Struct.Encoded<{ + expectedVersion: typeof S.Number + props: X + }> + > + const _AsSchemaTest2 = ( + domainEvent: S.Schema.Type>> + ) => { + expect(domainEvent.expectedVersion).type.toBe() + expect(domainEvent.props).type.toBe>() + } + }) + + it("Schema.is", () => { + expect(hole>().filter(S.is(S.String))) + .type.toBe>() + expect(hole>().find(S.is(S.String))) + .type.toBe() + }) + + it("TaggedStruct", () => { + expect(S.tag("A")).type.toBe>() + const MyTaggedStruct = S.TaggedStruct("Product", { + category: S.tag("Electronics"), + name: S.String, + price: S.Number + }) + expect(S.asSchema(MyTaggedStruct)) + .type.toBe< + S.Schema< + { readonly _tag: "Product"; readonly name: string; readonly category: "Electronics"; readonly price: number }, + { readonly _tag: "Product"; readonly name: string; readonly category: "Electronics"; readonly price: number }, + never + > + >() + expect(hole>()).type.toBe< + [ + props: { + readonly _tag?: "Product" + readonly name: string + readonly category?: "Electronics" + readonly price: number + }, + options?: S.MakeOptions | undefined + ] + >() + }) + + describe("Optional Primitives", () => { + it("optionalToOptional", () => { + expect( + S.asSchema(S.Struct({ a: S.optionalToOptional(aContext, S.String, { decode: (o) => o, encode: (o) => o }) })) + ).type.toBe>() + expect( + S.Struct({ a: S.optionalToOptional(aContext, S.String, { decode: (o) => o, encode: (o) => o }) }) + ).type.toBe }>>() + }) + + it("optionalToRequired", () => { + expect( + S.asSchema( + S.Struct({ + a: S.optionalToRequired(aContext, S.String, { decode: Option.getOrElse(() => ""), encode: Option.some }) + }) + ) + ).type.toBe>() + expect( + S.Struct({ + a: S.optionalToRequired(aContext, S.String, { decode: Option.getOrElse(() => ""), encode: Option.some }) + }) + ).type.toBe }>>() + }) + + it("requiredToOptional", () => { + expect( + S.asSchema( + S.Struct({ + a: S.requiredToOptional(aContext, S.String, { decode: Option.some, encode: Option.getOrElse(() => "") }) + }) + ) + ).type.toBe>() + expect( + S.Struct({ + a: S.requiredToOptional(aContext, S.String, { decode: Option.some, encode: Option.getOrElse(() => "") }) + }) + ).type.toBe }>>() + }) + }) + + it("TemplateLiteralParser", () => { + expect(S.asSchema(S.TemplateLiteralParser("a"))) + .type.toBe>() + expect(S.asSchema(S.TemplateLiteralParser(S.Literal("a")))) + .type.toBe>() + expect(S.asSchema(S.TemplateLiteralParser(1))) + .type.toBe>() + expect(S.asSchema(S.TemplateLiteralParser(S.Literal(1)))) + .type.toBe>() + expect(S.asSchema(S.TemplateLiteralParser(S.String))) + .type.toBe>() + expect(S.asSchema(S.TemplateLiteralParser(S.Number))) + .type.toBe>() + expect(S.asSchema(S.TemplateLiteralParser("a", "b"))) + .type.toBe>() + expect(S.asSchema(S.TemplateLiteralParser(S.Literal("a"), S.Literal("b")))) + .type.toBe>() + expect(S.asSchema(S.TemplateLiteralParser("a", S.String))) + .type.toBe>() + expect(S.asSchema(S.TemplateLiteralParser(S.Literal("a"), S.String))) + .type.toBe>() + expect(S.asSchema(S.TemplateLiteralParser("a", S.Number))) + .type.toBe>() + expect(S.asSchema(S.TemplateLiteralParser(S.Literal("a"), S.Number))) + .type.toBe>() + expect(S.asSchema(S.TemplateLiteralParser(S.String, "a"))) + .type.toBe>() + expect(S.asSchema(S.TemplateLiteralParser(S.String, S.Literal("a")))) + .type.toBe>() + expect(S.asSchema(S.TemplateLiteralParser(S.Number, "a"))) + .type.toBe>() + expect(S.asSchema(S.TemplateLiteralParser(S.Number, S.Literal("a")))) + .type.toBe>() + expect(S.asSchema(S.TemplateLiteralParser(S.String, 0))) + .type.toBe>() + expect(S.asSchema(S.TemplateLiteralParser(S.String, true))) + .type.toBe>() + expect(S.asSchema(S.TemplateLiteralParser(S.String, null))) + .type.toBe>() + expect(S.asSchema(S.TemplateLiteralParser(S.String, 1n))) + .type.toBe>() + expect(S.asSchema(S.TemplateLiteralParser(S.String, S.Literal("a", 0)))) + .type.toBe>() + expect(S.asSchema(S.TemplateLiteralParser(S.String, S.Literal("/"), S.Number))) + .type.toBe>() + expect(S.asSchema(S.TemplateLiteralParser(S.String, "/", S.Number))) + .type.toBe>() + const EmailLocaleIDs = S.Literal("welcome_email", "email_heading") + const FooterLocaleIDs = S.Literal("footer_title", "footer_sendoff") + expect(S.asSchema(S.TemplateLiteralParser(S.Union(EmailLocaleIDs, FooterLocaleIDs), S.Literal("_id")))) + .type.toBe< + S.Schema< + readonly ["welcome_email" | "email_heading" | "footer_title" | "footer_sendoff", "_id"], + "welcome_email_id" | "email_heading_id" | "footer_title_id" | "footer_sendoff_id", + never + > + >() + expect(S.asSchema(S.TemplateLiteralParser(S.Union(EmailLocaleIDs, FooterLocaleIDs), "_id"))) + .type.toBe< + S.Schema< + readonly ["welcome_email" | "email_heading" | "footer_title" | "footer_sendoff", "_id"], + "welcome_email_id" | "email_heading_id" | "footer_title_id" | "footer_sendoff_id", + never + > + >() + expect(S.asSchema(S.TemplateLiteralParser(S.String.pipe(S.brand("MyBrand"))))) + .type.toBe], string>>() + expect(S.asSchema(S.TemplateLiteralParser(S.Number.pipe(S.brand("MyBrand"))))) + .type.toBe], `${number}`>>() + expect(S.asSchema(S.TemplateLiteralParser("a", S.String.pipe(S.brand("MyBrand"))))) + .type.toBe], `a${string}`>>() + expect(S.asSchema(S.TemplateLiteralParser(S.Literal("a"), S.String.pipe(S.brand("MyBrand"))))) + .type.toBe], `a${string}`>>() + expect( + S.asSchema( + S.TemplateLiteralParser(S.Literal("a").pipe(S.brand("L")), S.String.pipe(S.brand("MyBrand"))) + ) + ).type.toBe< + S.Schema), string & Brand.Brand<"MyBrand">], `a${string}`> + >() + expect(S.asSchema(S.TemplateLiteralParser("a", S.Number.pipe(S.brand("MyBrand"))))) + .type.toBe], `a${number}`>>() + expect(S.asSchema(S.TemplateLiteralParser(S.Literal("a"), S.Number.pipe(S.brand("MyBrand"))))) + .type.toBe], `a${number}`>>() + expect(S.asSchema(S.TemplateLiteralParser("a", S.Union(S.Number, S.String)))) + .type.toBe>() + }) + + describe("Filters", () => { + it("filter", () => { + S.String.pipe(S.filter((s, options, ast) => { + expect(s).type.toBe() + expect(options).type.toBe() + expect(ast).type.toBe() + return undefined + })) + const predicateFilter1 = (u: unknown): boolean => typeof u === "string" + const FromFilter = S.Union(S.String, S.Number) + expect(pipe(FromFilter, S.filter(predicateFilter1))) + .type.toBe>>() + const FromRefinement = S.Struct({ + a: S.optionalWith(S.String, { exact: true }), + b: S.optionalWith(S.Number, { exact: true }) + }) + expect(pipe(FromRefinement, S.filter(S.is(S.Struct({ b: S.Number }))))) + .type.toBe< + S.refine< + { readonly a?: string; readonly b?: number } & { readonly b: number }, + S.Schema + > + >() + const LiteralFilter = S.Literal("a", "b") + const predicateFilter2 = (u: unknown): u is "a" => typeof u === "string" && u === "a" + expect(pipe(LiteralFilter, S.filter(predicateFilter2))) + .type.toBe>>() + expect(pipe(LiteralFilter, S.filter(S.is(S.Literal("a"))))) + .type.toBe>>() + expect(pipe(LiteralFilter, S.filter(S.is(S.Literal("c"))))) + .type.toBe>>() + const UnionFilter = hole< + S.Schema< + { readonly a: string } | { readonly b: string }, + { readonly a: string } | { readonly b: string }, + never + > + >() + expect(pipe(UnionFilter, S.filter(S.is(S.Struct({ b: S.String }))))) + .type.toBe< + S.refine< + ({ readonly a: string } | { readonly b: string }) & { readonly b: string }, + S.Schema + > + >() + expect(pipe(S.Number, S.filter((n): n is number & Brand.Brand<"MyNumber"> => n > 0))) + .type.toBe, S.Schema>>() + // annotations + pipe( + S.String, + S.filter( + (s) => { + expect(s).type.toBe() + return true + }, + { + arbitrary: (from, ctx) => (fc) => { + expect(from).type.toBe>() + expect(ctx).type.toBe() + return fc.string() + }, + pretty: (from) => (s) => { + expect(from).type.toBe>() + expect(s).type.toBe() + return s + }, + equivalence: (from) => (a, b) => { + expect(from).type.toBe>() + expect(a).type.toBe() + expect(b).type.toBe() + return true + } + } + ) + ) + pipe( + S.String, + S.filter((s) => { + expect(s).type.toBe() + return true + }) + ).annotations({ + arbitrary: (...x) => (fc) => { + expect(x).type.toBe>() + return fc.string() + }, + pretty: (...x) => (s) => { + expect(x).type.toBe>() + return s + }, + equivalence: (...x) => (a, b) => { + expect(x).type.toBe>() + expect(a).type.toBe() + expect(b).type.toBe() + return true + } + }) + }) + + describe("String Filters", () => { + it("maxLength", () => { + when(pipe).isCalledWith(S.Null, expect(S.maxLength).type.not.toBeCallableWith(5)) + // should allow generic context + const _f1 = (schema: S.Schema) => schema.pipe(S.maxLength(5)) + const _f2 = (schema: S.Schema) => + when(schema.pipe).isCalledWith( + expect(S.greaterThan).type.not.toBeCallableWith(5) + ) + // should allow string subtypes + pipe( + S.TemplateLiteral("a", S.String), + S.maxLength(5, { + pretty: () => (s) => { + expect(s).type.toBe<`a${string}`>() + return "-" + } + }) + ) + + const schema = pipe( + S.String, + S.maxLength(5, { + pretty: () => (s) => { + expect(s).type.toBe() + return "-" + } + }) + ) + expect(S.asSchema(schema)).type.toBe>() + expect(schema).type.toBe>() + expect(schema.annotations({})).type.toBe>() + expect(schema.from).type.toBe() + }) + + it("minLength", () => { + when(pipe).isCalledWith(S.Null, expect(S.minLength).type.not.toBeCallableWith(5)) + + const schema = pipe(S.String, S.minLength(5)) + expect(S.asSchema(schema)).type.toBe>() + expect(schema).type.toBe>() + expect(schema.annotations({})).type.toBe>() + expect(schema.from).type.toBe() + }) + + it("length", () => { + when(pipe).isCalledWith(S.Null, expect(S.length).type.not.toBeCallableWith(5)) + + const schema = pipe(S.String, S.length(5)) + expect(S.asSchema(schema)).type.toBe>() + expect(schema).type.toBe>() + expect(schema.annotations({})).type.toBe>() + expect(schema.from).type.toBe() + }) + + it("pattern", () => { + when(pipe).isCalledWith(S.Null, expect(S.pattern).type.not.toBeCallableWith(/a/)) + + const schema = pipe(S.String, S.pattern(/a/)) + expect(S.asSchema(schema)).type.toBe>() + expect(schema).type.toBe>() + expect(schema.annotations({})).type.toBe>() + expect(schema.from).type.toBe() + }) + + it("startsWith", () => { + when(pipe).isCalledWith(S.Null, expect(S.startsWith).type.not.toBeCallableWith("a")) + + const schema = pipe(S.String, S.startsWith("a")) + expect(S.asSchema(schema)).type.toBe>() + expect(schema).type.toBe>() + expect(schema.annotations({})).type.toBe>() + expect(schema.from).type.toBe() + }) + + it("endsWith", () => { + when(pipe).isCalledWith(S.Null, expect(S.endsWith).type.not.toBeCallableWith("a")) + + const schema = pipe(S.String, S.endsWith("a")) + expect(S.asSchema(schema)).type.toBe>() + expect(schema).type.toBe>() + expect(schema.annotations({})).type.toBe>() + expect(schema.from).type.toBe() + }) + + it("includes", () => { + when(pipe).isCalledWith(S.Null, expect(S.includes).type.not.toBeCallableWith("a")) + + const schema = pipe(S.String, S.includes("a")) + expect(S.asSchema(schema)).type.toBe>() + expect(schema).type.toBe>() + expect(schema.annotations({})).type.toBe>() + expect(schema.from).type.toBe() + }) + + it("lowercased", () => { + when(pipe).isCalledWith(S.Null, expect(S.lowercased).type.not.toBeCallableWith()) + + const schema = pipe(S.String, S.lowercased()) + expect(S.asSchema(schema)).type.toBe>() + expect(schema).type.toBe>() + expect(schema.annotations({})).type.toBe>() + expect(schema.from).type.toBe() + }) + + it("uppercased", () => { + when(pipe).isCalledWith(S.Null, expect(S.uppercased).type.not.toBeCallableWith()) + + const schema = pipe(S.String, S.uppercased()) + expect(S.asSchema(schema)).type.toBe>() + expect(schema).type.toBe>() + expect(schema.annotations({})).type.toBe>() + expect(schema.from).type.toBe() + }) + + it("capitalized", () => { + when(pipe).isCalledWith(S.Null, expect(S.capitalized).type.not.toBeCallableWith()) + + const schema = pipe(S.String, S.capitalized()) + expect(S.asSchema(schema)).type.toBe>() + expect(schema).type.toBe>() + expect(schema.annotations({})).type.toBe>() + expect(schema.from).type.toBe() + }) + + it("uncapitalized", () => { + when(pipe).isCalledWith(S.Null, expect(S.uncapitalized).type.not.toBeCallableWith()) + + const schema = pipe(S.String, S.uncapitalized()) + expect(S.asSchema(schema)).type.toBe>() + expect(schema).type.toBe>() + expect(schema.annotations({})).type.toBe>() + expect(schema.from).type.toBe() + }) + + it("nonEmptyString", () => { + when(pipe).isCalledWith(S.Null, expect(S.nonEmptyString).type.not.toBeCallableWith()) + + const schema = pipe(S.String, S.nonEmptyString()) + expect(S.asSchema(schema)).type.toBe>() + expect(schema).type.toBe>() + expect(schema.annotations({})).type.toBe>() + expect(schema.from).type.toBe() + }) + + it("trimmed", () => { + when(pipe).isCalledWith(S.Null, expect(S.trimmed).type.not.toBeCallableWith()) + + const schema = pipe(S.String, S.trimmed()) + expect(S.asSchema(schema)).type.toBe>() + expect(schema).type.toBe>() + expect(schema.annotations({})).type.toBe>() + expect(schema.from).type.toBe() + }) + }) + + describe("Number Filters", () => { + it("finite", () => { + when(pipe).isCalledWith(S.Null, expect(S.finite).type.not.toBeCallableWith()) + + const schema = pipe(S.Number, S.finite()) + expect(S.asSchema(schema)).type.toBe>() + expect(schema).type.toBe>() + expect(schema.annotations({})).type.toBe>() + expect(schema.from).type.toBe() + }) + + it("greaterThan", () => { + when(pipe).isCalledWith(S.Null, expect(S.greaterThan).type.not.toBeCallableWith(5)) + + const schema = pipe(S.Number, S.greaterThan(5)) + expect(S.asSchema(schema)).type.toBe>() + expect(schema).type.toBe>() + expect(schema.annotations({})).type.toBe>() + expect(schema.from).type.toBe() + }) + + it("greaterThanOrEqualTo", () => { + when(pipe).isCalledWith(S.Null, expect(S.greaterThanOrEqualTo).type.not.toBeCallableWith(5)) + + const schema = pipe(S.Number, S.greaterThanOrEqualTo(5)) + expect(S.asSchema(schema)).type.toBe>() + expect(schema).type.toBe>() + expect(schema.annotations({})).type.toBe>() + expect(schema.from).type.toBe() + }) + + it("lessThan", () => { + when(pipe).isCalledWith(S.Null, expect(S.lessThan).type.not.toBeCallableWith(5)) + + const schema = pipe(S.Number, S.lessThan(5)) + expect(S.asSchema(schema)).type.toBe>() + expect(schema).type.toBe>() + expect(schema.annotations({})).type.toBe>() + expect(schema.from).type.toBe() + }) + + it("lessThanOrEqualTo", () => { + when(pipe).isCalledWith(S.Null, expect(S.lessThanOrEqualTo).type.not.toBeCallableWith(5)) + + const schema = pipe(S.Number, S.lessThanOrEqualTo(5)) + expect(S.asSchema(schema)).type.toBe>() + expect(schema).type.toBe>() + expect(schema.annotations({})).type.toBe>() + expect(schema.from).type.toBe() + }) + + it("int", () => { + when(pipe).isCalledWith(S.Null, expect(S.int).type.not.toBeCallableWith()) + + const schema = pipe(S.Number, S.int()) + expect(S.asSchema(schema)).type.toBe>() + expect(schema).type.toBe>() + expect(schema.annotations({})).type.toBe>() + expect(schema.from).type.toBe() + }) + + it("multipleOf", () => { + when(pipe).isCalledWith(S.Null, expect(S.multipleOf).type.not.toBeCallableWith(5)) + + const schema = pipe(S.Number, S.multipleOf(5)) + expect(S.asSchema(schema)).type.toBe>() + expect(schema).type.toBe>() + expect(schema.annotations({})).type.toBe>() + expect(schema.from).type.toBe() + }) + + it("between", () => { + when(pipe).isCalledWith(S.Null, expect(S.between).type.not.toBeCallableWith(1, 5)) + + const schema = pipe(S.Number, S.between(1, 5)) + expect(S.asSchema(schema)).type.toBe>() + expect(schema).type.toBe>() + expect(schema.annotations({})).type.toBe>() + expect(schema.from).type.toBe() + }) + + it("nonNaN", () => { + when(pipe).isCalledWith(S.Null, expect(S.nonNaN).type.not.toBeCallableWith()) + + const schema = pipe(S.Number, S.nonNaN()) + expect(S.asSchema(schema)).type.toBe>() + expect(schema).type.toBe>() + expect(schema.annotations({})).type.toBe>() + expect(schema.from).type.toBe() + }) + + it("positive", () => { + when(pipe).isCalledWith(S.Null, expect(S.positive).type.not.toBeCallableWith()) + + const schema = pipe(S.Number, S.positive()) + expect(S.asSchema(schema)).type.toBe>() + expect(schema).type.toBe>() + expect(schema.annotations({})).type.toBe>() + expect(schema.from).type.toBe() + }) + + it("negative", () => { + when(pipe).isCalledWith(S.Null, expect(S.negative).type.not.toBeCallableWith()) + + const schema = pipe(S.Number, S.negative()) + expect(S.asSchema(schema)).type.toBe>() + expect(schema).type.toBe>() + expect(schema.annotations({})).type.toBe>() + expect(schema.from).type.toBe() + }) + + it("nonPositive", () => { + when(pipe).isCalledWith(S.Null, expect(S.nonPositive).type.not.toBeCallableWith()) + + const schema = pipe(S.Number, S.nonPositive()) + expect(S.asSchema(schema)).type.toBe>() + expect(schema).type.toBe>() + expect(schema.annotations({})).type.toBe>() + expect(schema.from).type.toBe() + }) + + it("nonNegative", () => { + when(pipe).isCalledWith(S.Null, expect(S.nonNegative).type.not.toBeCallableWith()) + + const schema = pipe(S.Number, S.nonNegative()) + expect(S.asSchema(schema)).type.toBe>() + expect(schema).type.toBe>() + expect(schema.annotations({})).type.toBe>() + expect(schema.from).type.toBe() + }) + }) + + describe("BigInt Filters", () => { + it("greaterThanBigInt", () => { + when(pipe).isCalledWith(S.Null, expect(S.greaterThanBigInt).type.not.toBeCallableWith(5n)) + + const schema = pipe(S.BigIntFromSelf, S.greaterThanBigInt(5n)) + expect(S.asSchema(schema)).type.toBe>() + expect(schema).type.toBe>() + expect(schema.annotations({})).type.toBe>() + expect(schema.from).type.toBe() + }) + + it("greaterThanOrEqualToBigInt", () => { + when(pipe).isCalledWith(S.Null, expect(S.greaterThanOrEqualToBigInt).type.not.toBeCallableWith(5n)) + + const schema = pipe(S.BigIntFromSelf, S.greaterThanOrEqualToBigInt(5n)) + expect(S.asSchema(schema)).type.toBe>() + expect(schema).type.toBe>() + expect(schema.annotations({})).type.toBe>() + expect(schema.from).type.toBe() + }) + + it("lessThanBigInt", () => { + when(pipe).isCalledWith(S.Null, expect(S.lessThanBigInt).type.not.toBeCallableWith(5n)) + + const schema = pipe(S.BigIntFromSelf, S.lessThanBigInt(5n)) + expect(S.asSchema(schema)).type.toBe>() + expect(schema).type.toBe>() + expect(schema.annotations({})).type.toBe>() + expect(schema.from).type.toBe() + }) + + it("lessThanOrEqualToBigInt", () => { + when(pipe).isCalledWith(S.Null, expect(S.lessThanOrEqualToBigInt).type.not.toBeCallableWith(5n)) + + const schema = pipe(S.BigIntFromSelf, S.lessThanOrEqualToBigInt(5n)) + expect(S.asSchema(schema)).type.toBe>() + expect(schema).type.toBe>() + expect(schema.annotations({})).type.toBe>() + expect(schema.from).type.toBe() + }) + + it("betweenBigInt", () => { + when(pipe).isCalledWith(S.Null, expect(S.betweenBigInt).type.not.toBeCallableWith(1n, 5n)) + + const schema = pipe(S.BigIntFromSelf, S.betweenBigInt(1n, 5n)) + expect(S.asSchema(schema)).type.toBe>() + expect(schema).type.toBe>() + expect(schema.annotations({})).type.toBe>() + expect(schema.from).type.toBe() + }) + + it("positiveBigInt", () => { + when(pipe).isCalledWith(S.Null, expect(S.positiveBigInt).type.not.toBeCallableWith()) + + const schema = pipe(S.BigIntFromSelf, S.positiveBigInt()) + expect(S.asSchema(schema)).type.toBe>() + expect(schema).type.toBe>() + expect(schema.annotations({})).type.toBe>() + expect(schema.from).type.toBe() + }) + + it("negativeBigInt", () => { + when(pipe).isCalledWith(S.Null, expect(S.negativeBigInt).type.not.toBeCallableWith()) + + const schema = pipe(S.BigIntFromSelf, S.negativeBigInt()) + expect(S.asSchema(schema)).type.toBe>() + expect(schema).type.toBe>() + expect(schema.annotations({})).type.toBe>() + expect(schema.from).type.toBe() + }) + + it("nonNegativeBigInt", () => { + expect(pipe).type.not.toBeCallableWith(S.Null, S.nonNegativeBigInt()) + + const schema = pipe(S.BigIntFromSelf, S.nonNegativeBigInt()) + expect(S.asSchema(schema)).type.toBe>() + expect(schema).type.toBe>() + expect(schema.annotations({})).type.toBe>() + expect(schema.from).type.toBe() + }) + + it("nonPositiveBigInt", () => { + expect(pipe).type.not.toBeCallableWith(S.Null, S.negativeBigInt()) + + const schema = pipe(S.BigIntFromSelf, S.nonPositiveBigInt()) + expect(S.asSchema(schema)).type.toBe>() + expect(schema).type.toBe>() + expect(schema.annotations({})).type.toBe>() + expect(schema.from).type.toBe() + }) + }) + + describe("Duration filters", () => { + it("lessThanDuration", () => { + when(pipe).isCalledWith(S.Null, expect(S.lessThanDuration).type.not.toBeCallableWith("10 millis")) + + const schema = pipe(S.DurationFromSelf, S.lessThanDuration("10 millis")) + expect(S.asSchema(schema)).type.toBe>() + expect(schema).type.toBe>() + expect(schema.annotations({})).type.toBe>() + expect(schema.from).type.toBe() + }) + + it("lessThanOrEqualToDuration", () => { + when(pipe).isCalledWith(S.Null, expect(S.lessThanOrEqualToDuration).type.not.toBeCallableWith("10 millis")) + + const schema = pipe(S.DurationFromSelf, S.lessThanOrEqualToDuration("10 millis")) + expect(S.asSchema(schema)).type.toBe>() + expect(schema).type.toBe>() + expect(schema.annotations({})).type.toBe>() + expect(schema.from).type.toBe() + }) + + it("greaterThanDuration", () => { + when(pipe).isCalledWith(S.Null, expect(S.greaterThanDuration).type.not.toBeCallableWith("10 millis")) + + const schema = pipe(S.DurationFromSelf, S.greaterThanDuration("10 millis")) + expect(S.asSchema(schema)).type.toBe>() + expect(schema).type.toBe>() + expect(schema.annotations({})).type.toBe>() + expect(schema.from).type.toBe() + }) + + it("greaterThanOrEqualToDuration", () => { + when(pipe).isCalledWith(S.Null, expect(S.greaterThanOrEqualToDuration).type.not.toBeCallableWith("10 millis")) + + const schema = pipe(S.DurationFromSelf, S.greaterThanOrEqualToDuration("10 millis")) + expect(S.asSchema(schema)).type.toBe>() + expect(schema).type.toBe>() + expect(schema.annotations({})).type.toBe>() + expect(schema.from).type.toBe() + }) + + it("betweenDuration", () => { + when(pipe).isCalledWith(S.Null, expect(S.betweenDuration).type.not.toBeCallableWith("10 millis", "50 millis")) + + const schema = pipe(S.DurationFromSelf, S.betweenDuration("10 millis", "50 millis")) + expect(S.asSchema(schema)).type.toBe>() + expect(schema).type.toBe>() + expect(schema.annotations({})).type.toBe>() + expect(schema.from).type.toBe() + }) + }) + + describe("Array Filters", () => { + describe("Array", () => { + it("minItems", () => { + when(pipe).isCalledWith(S.Null, expect(S.minItems).type.not.toBeCallableWith(2)) + + const schema = S.Array(S.String).pipe(S.minItems(2)) + expect(S.asSchema(schema)).type.toBe>>() + expect(schema).type.toBe>>() + expect(schema.annotations({})).type.toBe>>() + expect(schema.from).type.toBe>() + }) + + it("maxItems", () => { + when(pipe).isCalledWith(S.Null, expect(S.maxItems).type.not.toBeCallableWith(2)) + + const schema = S.Array(S.String).pipe(S.maxItems(2)) + expect(S.asSchema(schema)).type.toBe>>() + expect(schema).type.toBe>>() + expect(schema.annotations({})).type.toBe>>() + expect(schema.from).type.toBe>() + }) + + it("itemsCount", () => { + when(pipe).isCalledWith(S.Null, expect(S.itemsCount).type.not.toBeCallableWith(2)) + + const schema = S.Array(S.String).pipe(S.itemsCount(2)) + expect(S.asSchema(schema)).type.toBe>>() + expect(schema).type.toBe>>() + expect(schema.annotations({})).type.toBe>>() + expect(schema.from).type.toBe>() + }) + }) + + describe("NonEmptyArray", () => { + it("minItems", () => { + const schema = S.NonEmptyArray(S.String).pipe(S.minItems(2)) + expect(S.asSchema(schema)).type.toBe]>>() + expect(schema).type.toBe>>() + expect(schema.annotations({})).type.toBe>>() + expect(schema.from).type.toBe>() + }) + + it("maxItems", () => { + const schema = S.NonEmptyArray(S.String).pipe(S.maxItems(2)) + expect(S.asSchema(schema)).type.toBe]>>() + expect(schema).type.toBe>>() + expect(schema.annotations({})).type.toBe>>() + expect(schema.from).type.toBe>() + }) + + it("itemsCount", () => { + const schema = S.NonEmptyArray(S.String).pipe(S.itemsCount(2)) + expect(S.asSchema(schema)).type.toBe]>>() + expect(schema).type.toBe>>() + expect(schema.annotations({})).type.toBe>>() + expect(schema.from).type.toBe>() + }) + }) + }) + + describe("Date Filters", () => { + it("validDate", () => { + when(pipe).isCalledWith(S.Null, expect(S.validDate).type.not.toBeCallableWith()) + + const schema = pipe(S.DateFromSelf, S.validDate()) + expect(S.asSchema(schema)).type.toBe>() + expect(schema).type.toBe>() + expect(schema.annotations({})).type.toBe>() + expect(schema.from).type.toBe() + }) + + it("lessThanDate", () => { + when(pipe).isCalledWith(S.Null, expect(S.lessThanDate).type.not.toBeCallableWith(new Date())) + + const schema = pipe(S.DateFromSelf, S.lessThanDate(new Date())) + expect(S.asSchema(schema)).type.toBe>() + expect(schema).type.toBe>() + expect(schema.annotations({})).type.toBe>() + expect(schema.from).type.toBe() + }) + + it("lessThanOrEqualToDate", () => { + when(pipe).isCalledWith(S.Null, expect(S.lessThanOrEqualToDate).type.not.toBeCallableWith(new Date())) + + const schema = pipe(S.DateFromSelf, S.lessThanOrEqualToDate(new Date())) + expect(S.asSchema(schema)).type.toBe>() + expect(schema).type.toBe>() + expect(schema.annotations({})).type.toBe>() + expect(schema.from).type.toBe() + }) + + it("greaterThanDate", () => { + when(pipe).isCalledWith(S.Null, expect(S.greaterThanDate).type.not.toBeCallableWith(new Date())) + + const schema = pipe(S.DateFromSelf, S.greaterThanDate(new Date())) + expect(S.asSchema(schema)).type.toBe>() + expect(schema).type.toBe>() + expect(schema.annotations({})).type.toBe>() + expect(schema.from).type.toBe() + }) + + it("greaterThanOrEqualToDate", () => { + when(pipe).isCalledWith(S.Null, expect(S.greaterThanOrEqualToDate).type.not.toBeCallableWith(new Date())) + + const schema = pipe(S.DateFromSelf, S.greaterThanOrEqualToDate(new Date())) + expect(S.asSchema(schema)).type.toBe>() + expect(schema).type.toBe>() + expect(schema.annotations({})).type.toBe>() + expect(schema.from).type.toBe() + }) + + it("betweenDate", () => { + when(pipe).isCalledWith(S.Null, expect(S.betweenDate).type.not.toBeCallableWith(new Date(0), new Date(100))) + + const schema = pipe(S.DateFromSelf, S.betweenDate(new Date(0), new Date(100))) + expect(S.asSchema(schema)).type.toBe>() + expect(schema).type.toBe>() + expect(schema.annotations({})).type.toBe>() + expect(schema.from).type.toBe() + }) + }) + + describe("BigDecimal Filters", () => { + const bd = hole() + + it("greaterThanBigDecimal", () => { + when(pipe).isCalledWith(S.Null, expect(S.greaterThanBigDecimal).type.not.toBeCallableWith(bd)) + + const schema = pipe(S.BigDecimalFromSelf, S.greaterThanBigDecimal(bd)) + expect(S.asSchema(schema)).type.toBe>() + expect(schema).type.toBe>() + expect(schema.annotations({})).type.toBe>() + expect(schema.from).type.toBe() + }) + + it("greaterThanOrEqualToBigDecimal", () => { + when(pipe).isCalledWith(S.Null, expect(S.greaterThanOrEqualToBigDecimal).type.not.toBeCallableWith(bd)) + + const schema = pipe(S.BigDecimalFromSelf, S.greaterThanOrEqualToBigDecimal(bd)) + expect(S.asSchema(schema)).type.toBe>() + expect(schema).type.toBe>() + expect(schema.annotations({})).type.toBe>() + expect(schema.from).type.toBe() + }) + + it("lessThanBigDecimal", () => { + when(pipe).isCalledWith(S.Null, expect(S.lessThanBigDecimal).type.not.toBeCallableWith(bd)) + + const schema = pipe(S.BigDecimalFromSelf, S.lessThanBigDecimal(bd)) + expect(S.asSchema(schema)).type.toBe>() + expect(schema).type.toBe>() + expect(schema.annotations({})).type.toBe>() + expect(schema.from).type.toBe() + }) + + it("lessThanOrEqualToBigDecimal", () => { + when(pipe).isCalledWith(S.Null, expect(S.lessThanOrEqualToBigDecimal).type.not.toBeCallableWith(bd)) + + const schema = pipe(S.BigDecimalFromSelf, S.lessThanOrEqualToBigDecimal(bd)) + expect(S.asSchema(schema)).type.toBe>() + expect(schema).type.toBe>() + expect(schema.annotations({})).type.toBe>() + expect(schema.from).type.toBe() + }) + + it("positiveBigDecimal", () => { + when(pipe).isCalledWith(S.Null, expect(S.positiveBigDecimal).type.not.toBeCallableWith()) + + const schema = pipe(S.BigDecimalFromSelf, S.positiveBigDecimal()) + expect(S.asSchema(schema)).type.toBe>() + expect(schema).type.toBe>() + expect(schema.annotations({})).type.toBe>() + expect(schema.from).type.toBe() + }) + + it("nonNegativeBigDecimal", () => { + when(pipe).isCalledWith(S.Null, expect(S.nonNegativeBigDecimal).type.not.toBeCallableWith()) + + const schema = pipe(S.BigDecimalFromSelf, S.nonNegativeBigDecimal()) + expect(S.asSchema(schema)).type.toBe>() + expect(schema).type.toBe>() + expect(schema.annotations({})).type.toBe>() + expect(schema.from).type.toBe() + }) + + it("negativeBigDecimal", () => { + when(pipe).isCalledWith(S.Null, expect(S.negativeBigDecimal).type.not.toBeCallableWith()) + + const schema = pipe(S.BigDecimalFromSelf, S.negativeBigDecimal()) + expect(S.asSchema(schema)).type.toBe>() + expect(schema).type.toBe>() + expect(schema.annotations({})).type.toBe>() + expect(schema.from).type.toBe() + }) + + it("nonPositiveBigDecimal", () => { + when(pipe).isCalledWith(S.Null, expect(S.nonPositiveBigDecimal).type.not.toBeCallableWith()) + + const schema = pipe(S.BigDecimalFromSelf, S.nonPositiveBigDecimal()) + expect(S.asSchema(schema)).type.toBe>() + expect(schema).type.toBe>() + expect(schema.annotations({})).type.toBe>() + expect(schema.from).type.toBe() + }) + + it("betweenBigDecimal", () => { + when(pipe).isCalledWith(S.Null, expect(S.betweenBigDecimal).type.not.toBeCallableWith(bd, bd)) + + const schema = pipe(S.BigDecimalFromSelf, S.betweenBigDecimal(bd, bd)) + expect(S.asSchema(schema)).type.toBe>() + expect(schema).type.toBe>() + expect(schema.annotations({})).type.toBe>() + expect(schema.from).type.toBe() + }) + }) + }) + + describe("Data Types", () => { + it("Uint8ArrayFromSelf", () => { + const schema = S.Uint8ArrayFromSelf + expect(S.asSchema(schema)).type.toBe>() + expect(schema).type.toBe() + // TODO: should be typeof S.Uint8ArrayFromSelf + expect(schema.annotations({})).type.toBe>() + }) + + it("DateFromSelf", () => { + const schema = S.DateFromSelf + expect(S.asSchema(schema)).type.toBe>() + expect(schema).type.toBe() + // TODO: should be typeof S.DateFromSelf + expect(schema.annotations({})).type.toBe>() + }) + + it("DateTimeUtcFromSelf", () => { + const schema = S.DateTimeUtcFromSelf + expect(S.asSchema(schema)).type.toBe>() + expect(schema).type.toBe() + // TODO: should be typeof S.DateTimeUtcFromSelf + expect(schema.annotations({})).type.toBe>() + }) + + it("TimeZoneOffsetFromSelf", () => { + const schema = S.TimeZoneOffsetFromSelf + expect(S.asSchema(schema)).type.toBe>() + expect(schema).type.toBe() + // TODO: should be typeof S.TimeZoneOffsetFromSelf + expect(schema.annotations({})).type.toBe>() + }) + + it("TimeZoneNamedFromSelf", () => { + const schema = S.TimeZoneNamedFromSelf + expect(S.asSchema(schema)).type.toBe>() + expect(schema).type.toBe() + // TODO: should be typeof S.TimeZoneNamedFromSelf + expect(schema.annotations({})).type.toBe>() + }) + + it("DateTimeZonedFromSelf", () => { + const schema = S.DateTimeZonedFromSelf + expect(S.asSchema(schema)).type.toBe>() + expect(schema).type.toBe() + // TODO: should be typeof S.DateTimeZonedFromSelf + expect(schema.annotations({})).type.toBe>() + }) + + it("DurationFromSelf", () => { + const schema = S.DurationFromSelf + expect(S.asSchema(schema)).type.toBe>() + expect(schema).type.toBe() + // TODO: should be typeof S.DurationFromSelf + expect(schema.annotations({})).type.toBe>() + }) + + it("Duration", () => { + expect(S.asSchema(S.Duration)) + .type.toBe>() + }) + + it("DurationFromMillis", () => { + expect(S.asSchema(S.DurationFromMillis)).type.toBe>() + }) + + it("DurationFromNanos", () => { + expect(S.asSchema(S.DurationFromNanos)).type.toBe>() + }) + + it("BigDecimalFromSelf", () => { + const schema = S.BigDecimalFromSelf + expect(S.asSchema(schema)).type.toBe>() + expect(schema).type.toBe() + // TODO: should be typeof S.BigDecimalFromSelf + expect(schema.annotations({})).type.toBe>() + }) + + it("FiberIdFromSelf", () => { + const schema = S.FiberIdFromSelf + expect(S.asSchema(schema)).type.toBe>() + expect(schema).type.toBe() + // TODO: should be typeof S.FiberIdFromSelf + expect(schema.annotations({})).type.toBe>() + }) + + it("BigDecimal", () => { + expect(S.asSchema(S.BigDecimal)).type.toBe>() + }) + + it("BigDecimalFromNumber", () => { + expect(S.asSchema(S.BigDecimalFromNumber)).type.toBe>() + }) + + it("ChunkFromSelf", () => { + const schema = S.ChunkFromSelf(S.NumberFromString) + expect(S.asSchema(schema)).type.toBe, Chunk.Chunk>>() + expect(schema).type.toBe>() + expect(schema.annotations({})).type.toBe>() + // should expose the type parameters + expect(schema.typeParameters).type.toBe() + }) + + it("Chunk", () => { + const schema = S.Chunk(S.NumberFromString) + expect(S.asSchema(schema)).type.toBe, ReadonlyArray>>() + expect(schema).type.toBe>() + expect(schema.annotations({})).type.toBe>() + expect(schema.from).type.toBe>() + expect(schema.to).type.toBe>>() + }) + + it("NonEmptyChunkFromSelf", () => { + const schema = S.NonEmptyChunkFromSelf(S.NumberFromString) + expect(S.asSchema(schema)).type.toBe, Chunk.NonEmptyChunk>>() + expect(schema).type.toBe>() + expect(schema.annotations({})).type.toBe>() + // should expose the type parameters + expect(schema.typeParameters).type.toBe() + }) + + it("NonEmptyChunk", () => { + const schema = S.NonEmptyChunk(S.NumberFromString) + expect(S.asSchema(schema)).type.toBe, readonly [string, ...Array]>>() + expect(schema).type.toBe>() + expect(schema.annotations({})).type.toBe>() + expect(schema.from).type.toBe>() + expect(schema.to).type.toBe>>() + }) + + it("DataFromSelf", () => { + expect(S.DataFromSelf).type.not.toBeCallableWith(hole>()) + expect(S.DataFromSelf).type.not.toBeCallableWith(hole>()) + + // should allow generic context + const _f1 = > | ReadonlyArray>(schema: S.Schema) => + S.DataFromSelf(schema) + const _f2 = < + A extends Readonly> | ReadonlyArray, + I extends Readonly> | ReadonlyArray + >(schema: S.Schema) => S.DataFromSelf(schema) + const _f3 = > | ReadonlyArray, I extends number>( + schema: S.Schema + ) => expect(S.DataFromSelf).type.not.toBeCallableWith(schema) + const _f4 = (schema: S.Schema) => expect(S.DataFromSelf).type.not.toBeCallableWith(schema) + + // should allow mutable arguments + S.DataFromSelf(S.mutable(S.Struct({ a: S.NumberFromString }))) + + const schema = S.DataFromSelf(S.Struct({ a: S.NumberFromString })) + expect(schema) + .type.toBe< + S.DataFromSelf< + S.Struct<{ + a: typeof S.NumberFromString + }> + > + >() + expect(schema.annotations({})) + .type.toBe< + S.DataFromSelf< + S.Struct<{ + a: typeof S.NumberFromString + }> + > + >() + // should expose the type parameters + expect(schema.typeParameters).type.toBe< + readonly [ + S.Struct<{ + a: typeof S.NumberFromString + }> + ] + >() + }) + + it("Data", () => { + expect(S.Data).type.not.toBeCallableWith(hole>()) + expect(S.Data).type.not.toBeCallableWith(hole>()) + + const schema = S.Data(S.Struct({ a: S.NumberFromString })) + expect(schema) + .type.toBe< + S.Data< + S.Struct<{ + a: typeof S.NumberFromString + }> + > + >() + expect(schema.annotations({})) + .type.toBe< + S.Data< + S.Struct<{ + a: typeof S.NumberFromString + }> + > + >() + expect(schema.from).type.toBe< + S.Struct<{ + a: typeof S.NumberFromString + }> + >() + expect(schema.to).type.toBe>>() + }) + + it("RedactedFromSelf", () => { + const schema = S.RedactedFromSelf(S.NumberFromString) + expect(S.asSchema(schema)).type.toBe, Redacted.Redacted>>() + expect(schema).type.toBe>() + expect(schema.annotations({})).type.toBe>() + // should expose the type parameters + expect(schema.typeParameters).type.toBe() + }) + + it("Redacted", () => { + const schema = S.Redacted(S.NumberFromString) + expect(S.asSchema(schema)).type.toBe, string>>() + expect(schema).type.toBe>() + expect(schema.annotations({})).type.toBe>() + expect(schema.from).type.toBe() + expect(schema.to).type.toBe>>() + }) + + it("OptionFromSelf", () => { + const schema = S.OptionFromSelf(S.NumberFromString) + expect(S.asSchema(schema)).type.toBe, Option.Option>>() + expect(schema).type.toBe>() + expect(schema.annotations({})).type.toBe>() + // should expose the type parameters + expect(schema.typeParameters).type.toBe() + }) + + it("Option", () => { + const schema = S.Option(S.NumberFromString) + expect(S.asSchema(schema)).type.toBe, S.OptionEncoded>>() + expect(schema).type.toBe>() + expect(schema.annotations({})).type.toBe>() + expect(schema.from).type.toBe< + S.Union< + [ + S.Struct<{ _tag: S.Literal<["None"]> }>, + S.Struct<{ _tag: S.Literal<["Some"]>; value: typeof S.NumberFromString }> + ] + > + >() + expect(schema.to).type.toBe>>() + }) + + it("OptionFromNullOr", () => { + const schema = S.OptionFromNullOr(S.NumberFromString) + expect(S.asSchema(schema)).type.toBe, string | null>>() + expect(schema).type.toBe>() + expect(schema.annotations({})).type.toBe>() + expect(schema.from).type.toBe>() + expect(schema.to).type.toBe>>() + }) + + it("OptionFromUndefinedOr", () => { + const schema = S.OptionFromUndefinedOr(S.NumberFromString) + expect(S.asSchema(schema)).type.toBe, string | undefined>>() + expect(schema).type.toBe>() + expect(schema.annotations({})).type.toBe>() + expect(schema.from).type.toBe>() + expect(schema.to).type.toBe>>() + }) + + it("OptionFromNullishOr", () => { + const schema = S.OptionFromNullishOr(S.NumberFromString, null) + expect(S.asSchema(schema)).type.toBe, string | null | undefined>>() + expect(schema).type.toBe>() + expect(schema.annotations({})).type.toBe>() + expect(schema.from).type.toBe>() + expect(schema.to).type.toBe>>() + }) + + it("EitherFromSelf", () => { + const schema = S.EitherFromSelf({ right: S.NumberFromString, left: S.String }) + expect(S.asSchema(schema)).type.toBe, Either.Either>>() + expect(schema).type.toBe>() + expect(schema.annotations({})).type.toBe>() + // should expose the type parameters + expect(schema.typeParameters).type.toBe() + + // should allow never as right + expect(S.EitherFromSelf({ right: S.Never, left: S.String })) + .type.toBe>() + // should allow never as left + expect(S.EitherFromSelf({ right: S.String, left: S.Never })) + .type.toBe>() + }) + + it("Either", () => { + const schema = S.Either({ right: S.NumberFromString, left: S.String }) + expect(S.asSchema(schema)).type.toBe, S.EitherEncoded>>() + expect(schema).type.toBe>() + expect(schema.annotations({})).type.toBe>() + expect(schema.from).type.toBe< + S.Union<[ + S.Struct<{ + _tag: S.Literal<["Right"]> + right: typeof S.NumberFromString + }>, + S.Struct<{ + _tag: S.Literal<["Left"]> + left: typeof S.String + }> + ]> + >() + expect(schema.to).type.toBe, S.SchemaClass>>() + + // should allow never as right + expect(S.Either({ right: S.Never, left: S.String })).type.toBe>() + // should allow never as left + expect(S.Either({ right: S.String, left: S.Never })).type.toBe>() + }) + + it("EitherFromUnion", () => { + const schema = S.EitherFromUnion({ right: S.NumberFromString, left: S.Boolean }) + expect(S.asSchema(schema)).type.toBe, string | boolean>>() + expect(schema).type.toBe>() + expect(schema.annotations({})).type.toBe>() + expect(schema.from).type.toBe< + S.Union<[ + S.transform< + typeof S.NumberFromString, + S.Struct<{ + _tag: S.Literal<["Right"]> + right: S.SchemaClass + }> + >, + S.transform< + typeof S.Boolean, + S.Struct<{ + _tag: S.Literal<["Left"]> + right: S.SchemaClass + }> + > + ]> + >() + expect(schema.from.members[0].to.fields.right).type.toBe>() + expect(schema.to).type.toBe, S.SchemaClass>>() + + // should allow never as right + expect(S.EitherFromUnion({ right: S.Never, left: S.String })) + .type.toBe>() + // should allow never as left + expect(S.EitherFromUnion({ right: S.String, left: S.Never })) + .type.toBe>() + }) + + it("ReadonlyMapFromSelf", () => { + const schema = S.ReadonlyMapFromSelf({ key: S.NumberFromString, value: S.String }) + expect(S.asSchema(schema)).type.toBe, ReadonlyMap>>() + expect(schema).type.toBe>() + expect(schema.annotations({})).type.toBe>() + // should expose the type parameters + expect(schema.typeParameters).type.toBe() + }) + + it("ReadonlyMap", () => { + const schema = S.ReadonlyMap({ key: S.NumberFromString, value: S.String }) + expect(S.asSchema(schema)) + .type.toBe, ReadonlyArray>>() + expect(schema).type.toBe>() + expect(schema.annotations({})).type.toBe>() + expect(schema.from).type.toBe>>() + expect(schema.to).type.toBe, S.SchemaClass>>() + }) + + it("MapFromSelf", () => { + const schema = S.MapFromSelf({ key: S.NumberFromString, value: S.String }) + expect(S.asSchema(schema)).type.toBe, ReadonlyMap>>() + expect(schema).type.toBe>() + expect(schema.annotations({})).type.toBe>() + // should expose the type parameters + expect(schema.typeParameters).type.toBe() + }) + + it("Map", () => { + const schema = S.Map({ key: S.NumberFromString, value: S.String }) + expect(S.asSchema(schema)).type.toBe, ReadonlyArray>>() + expect(schema).type.toBe>() + expect(schema.annotations({})).type.toBe>() + expect(schema.from).type.toBe>>() + expect(schema.to).type.toBe, S.SchemaClass>>() + }) + + it("HashMapFromSelf", () => { + const schema = S.HashMapFromSelf({ key: S.NumberFromString, value: S.String }) + expect(S.asSchema(schema)).type.toBe, HashMap.HashMap>>() + expect(schema).type.toBe>() + expect(schema.annotations({})).type.toBe>() + // should expose the type parameters + expect(schema.typeParameters).type.toBe() + }) + + it("HashMap", () => { + const schema = S.HashMap({ key: S.NumberFromString, value: S.String }) + expect(S.asSchema(schema)) + .type.toBe, ReadonlyArray>>() + expect(schema).type.toBe>() + expect(schema.annotations({})).type.toBe>() + expect(schema.from).type.toBe>>() + expect(schema.to).type.toBe, S.SchemaClass>>() + }) + + it("ReadonlySetFromSelf", () => { + const schema = S.ReadonlySetFromSelf(S.NumberFromString) + expect(S.asSchema(schema)).type.toBe, ReadonlySet>>() + expect(schema).type.toBe>() + expect(schema.annotations({})).type.toBe>() + // should expose the type parameters + expect(schema.typeParameters).type.toBe() + }) + + it("ReadonlySet", () => { + const schema = S.ReadonlySet(S.NumberFromString) + expect(S.asSchema(schema)).type.toBe, ReadonlyArray>>() + expect(schema).type.toBe>() + expect(schema.annotations({})).type.toBe>() + expect(schema.from).type.toBe>() + expect(schema.to).type.toBe>>() + }) + + it("SetFromSelf", () => { + const schema = S.SetFromSelf(S.NumberFromString) + expect(S.asSchema(schema)).type.toBe, ReadonlySet>>() + expect(schema).type.toBe>() + expect(schema.annotations({})).type.toBe>() + // should expose the type parameters + expect(schema.typeParameters).type.toBe() + }) + + it("Set", () => { + const schema = S.Set(S.NumberFromString) + expect(S.asSchema(schema)).type.toBe, ReadonlyArray>>() + expect(schema).type.toBe>() + expect(schema.annotations({})).type.toBe>() + expect(schema.from).type.toBe>() + expect(schema.to).type.toBe>>() + }) + + it("HashSetFromSelf", () => { + const schema = S.HashSetFromSelf(S.NumberFromString) + expect(S.asSchema(schema)).type.toBe, HashSet.HashSet>>() + expect(schema).type.toBe>() + expect(schema.annotations({})).type.toBe>() + // should expose the type parameters + expect(schema.typeParameters).type.toBe() + }) + + it("HashSet", () => { + const schema = S.HashSet(S.NumberFromString) + expect(S.asSchema(schema)).type.toBe, ReadonlyArray>>() + expect(schema).type.toBe>() + expect(schema.annotations({})).type.toBe>() + expect(schema.from).type.toBe>() + expect(schema.to).type.toBe>>() + }) + + it("ListFromSelf", () => { + const schema = S.ListFromSelf(S.NumberFromString) + expect(S.asSchema(schema)).type.toBe, List.List>>() + expect(schema).type.toBe>() + expect(schema.annotations({})).type.toBe>() + // should expose the type parameters + expect(schema.typeParameters).type.toBe() + }) + + it("List", () => { + const schema = S.List(S.NumberFromString) + expect(S.asSchema(schema)).type.toBe, ReadonlyArray>>() + expect(schema).type.toBe>() + expect(schema.annotations({})).type.toBe>() + expect(schema.from).type.toBe>() + expect(schema.to).type.toBe>>() + }) + + it("CauseFromSelf", () => { + const schema = S.CauseFromSelf({ error: S.String, defect: S.Unknown }) + expect(S.asSchema(schema)).type.toBe, Cause.Cause>>() + expect(schema).type.toBe>() + expect(schema.annotations({})).type.toBe>() + // should expose the type parameters + expect(schema.typeParameters).type.toBe() + + const defectWithR = S.CauseFromSelf({ error: S.String, defect: hole>() }) + expect(S.asSchema(defectWithR)).type.toBe, Cause.Cause, "a">>() + expect(defectWithR).type.toBe>>() + }) + + it("Cause", () => { + const schema = S.Cause({ error: S.String, defect: S.Defect }) + expect(S.asSchema(schema)).type.toBe, S.CauseEncoded>>() + expect(schema).type.toBe>() + expect(schema.annotations({})).type.toBe>() + expect(schema.from).type.toBe< + S.SchemaClass, S.CauseEncoded, never> + >() + expect(schema.to).type.toBe, S.SchemaClass>>() + + const defectWithR = S.Cause({ error: S.String, defect: hole>() }) + expect(S.asSchema(defectWithR)).type.toBe, S.CauseEncoded, "a">>() + expect(defectWithR).type.toBe>>() + }) + + it("ExitFromSelf", () => { + const schema = S.ExitFromSelf({ success: S.Number, failure: S.String, defect: S.Unknown }) + expect(S.asSchema(schema)).type.toBe, Exit.Exit>>() + expect(schema).type.toBe>() + expect(schema.annotations({})).type.toBe>() + // should expose the type parameters + expect(schema.typeParameters).type.toBe() + + const defectWithR = S.ExitFromSelf({ + success: S.Number, + failure: S.String, + defect: hole>() + }) + expect(S.asSchema(defectWithR)).type.toBe, Exit.Exit, "a">>() + expect(defectWithR).type.toBe>>() + }) + + it("Exit", () => { + const schema = S.Exit({ success: S.NumberFromString, failure: S.String, defect: S.Defect }) + expect(S.asSchema(schema)) + .type.toBe, S.ExitEncoded>>() + expect(schema).type.toBe>() + expect(schema.annotations({})).type.toBe>() + expect(schema.from).type.toBe< + S.Union<[ + S.Struct<{ + _tag: S.Literal<["Failure"]> + cause: S.SchemaClass, S.CauseEncoded, never> + }>, + S.Struct<{ + _tag: S.Literal<["Success"]> + value: typeof S.NumberFromString + }> + ]> + >() + expect(schema.to).type.toBe< + S.ExitFromSelf, S.SchemaClass, S.SchemaClass> + >() + + const defectWithR = S.Exit({ + success: S.Number, + failure: S.String, + defect: hole>() + }) + expect(S.asSchema(defectWithR)) + .type.toBe, S.ExitEncoded, "a">>() + expect(defectWithR).type.toBe>>() + }) + + it("SortedSetFromSelf", () => { + const schema = S.SortedSetFromSelf(S.NumberFromString, N.Order, Str.Order) + expect(S.asSchema(schema)).type.toBe, SortedSet.SortedSet>>() + expect(schema).type.toBe>() + expect(schema.annotations({})).type.toBe>() + // should expose the type parameters + expect(schema.typeParameters).type.toBe() + }) + + it("SortedSet", () => { + const schema = S.SortedSet(S.NumberFromString, N.Order) + expect(S.asSchema(schema)).type.toBe, ReadonlyArray>>() + expect(schema).type.toBe>() + expect(schema.annotations({})).type.toBe>() + expect(schema.from).type.toBe>() + expect(schema.to).type.toBe>>() + }) + + it("Config", () => { + expect(S.Config("A", S.String)) + .type.toBe>() + expect(S.Config("A", S.BooleanFromString)) + .type.toBe>() + expect(S.Config("A", S.TemplateLiteral(S.Literal("a"), S.String))) + .type.toBe>() + + // passed schemas must be encodable to string + expect(S.Config).type.not.toBeCallableWith("A", S.Boolean) + }) + + it("Defect", () => { + const schema = S.Defect + expect(S.asSchema(schema)).type.toBe>() + expect(schema).type.toBe() + }) + }) + + describe("parseJson", () => { + it("no arguments", () => { + const schema = S.parseJson() + expect(S.asSchema(schema)).type.toBe>() + expect(schema).type.toBe>() + expect(schema.annotations({})).type.toBe>() + }) + + it("single argument", () => { + const schema = S.parseJson(S.Struct({ a: S.Number })) + expect(S.asSchema(schema)).type.toBe>() + expect(schema).type.toBe, S.Struct<{ a: typeof S.Number }>>>() + expect(schema.annotations({})).type.toBe< + S.transform, S.Struct<{ a: typeof S.Number }>> + >() + expect(schema.from).type.toBe>() + expect(schema.to).type.toBe>() + }) + }) + + it("ArrayEnsure", () => { + const schema = S.ArrayEnsure(S.NumberFromString) + expect(S.asSchema(schema)).type.toBe, string | ReadonlyArray>>() + expect(schema).type.toBe>() + expect(schema.annotations({})).type.toBe>() + expect(schema.from).type.toBe]>>() + expect(schema.to).type.toBe>>() + }) + + it("NonEmptyArrayEnsure", () => { + const schema = S.NonEmptyArrayEnsure(S.NumberFromString) + expect(S.asSchema(schema)).type.toBe< + S.Schema], string | readonly [string, ...Array]> + >() + expect(schema).type.toBe>() + expect(schema.annotations({})).type.toBe>() + expect(schema.from).type.toBe]>>() + expect(schema.to).type.toBe>>() + }) + + it("ReadonlyMapFromRecord", () => { + const schema = S.ReadonlyMapFromRecord({ key: S.NumberFromString, value: S.NumberFromString }) + expect(S.asSchema(schema)).type.toBe, { readonly [x: string]: string }>>() + expect(schema).type.toBe, { readonly [x: string]: string }>>() + expect(schema.annotations({})).type.toBe< + S.SchemaClass, { readonly [x: string]: string }> + >() + }) + + it("MapFromRecord", () => { + const schema = S.MapFromRecord({ key: S.NumberFromString, value: S.NumberFromString }) + expect(S.asSchema(schema)).type.toBe, { readonly [x: string]: string }>>() + expect(schema).type.toBe, { readonly [x: string]: string }>>() + expect(schema.annotations({})).type.toBe, { readonly [x: string]: string }>>() + }) + + describe("Transformations", () => { + it("clamp", () => { + when(S.String.pipe).isCalledWith(expect(S.clamp).type.not.toBeCallableWith(-1, 1)) + + const schema = S.Number.pipe(S.clamp(-1, 1)) + expect(S.asSchema(schema)).type.toBe>() + expect(schema).type.toBe>>>() + expect(schema.annotations({})).type.toBe>>>() + expect(schema.from).type.toBe() + expect(schema.to).type.toBe>>() + }) + + it("clampBigInt", () => { + when(S.String.pipe).isCalledWith(expect(S.clampBigInt).type.not.toBeCallableWith(-1, 1)) + + const schema = S.BigIntFromSelf.pipe(S.clampBigInt(-1n, 1n)) + expect(S.asSchema(schema)).type.toBe>() + expect(schema).type.toBe>>>() + expect(schema.annotations({})).type.toBe< + S.transform>> + >() + expect(schema.from).type.toBe() + expect(schema.to).type.toBe>>() + }) + + it("clampDuration", () => { + when(S.String.pipe).isCalledWith(expect(S.clampDuration).type.not.toBeCallableWith(-1, 1)) + + const schema = S.DurationFromSelf.pipe(S.clampDuration(-1, 1)) + expect(S.asSchema(schema)).type.toBe>() + expect(schema).type.toBe>>>() + expect(schema.annotations({})).type.toBe< + S.transform>> + >() + expect(schema.from).type.toBe() + expect(schema.to).type.toBe>>() + }) + + it("clampBigDecimal", () => { + when(S.String.pipe).isCalledWith(expect(S.clampBigDecimal).type.not.toBeCallableWith(-1, 1)) + + const schema = S.BigDecimalFromSelf.pipe( + S.clampBigDecimal(hole(), hole()) + ) + expect(S.asSchema(schema)).type.toBe>() + expect(schema).type.toBe< + S.transform>> + >() + expect(schema.annotations({})).type.toBe< + S.transform>> + >() + expect(schema.from).type.toBe() + expect(schema.to).type.toBe>>() + }) + + it("head", () => { + expect(S.String.pipe).type.not.toBeCallableWith(S.head) + + const schema = S.head(S.Array(S.NumberFromString)) + + expect(S.asSchema(schema)).type.toBe, ReadonlyArray, never>>() + expect(schema) + .type.toBe, S.OptionFromSelf>>>() + expect(schema.annotations({})) + .type.toBe, S.OptionFromSelf>>>() + expect(schema.from).type.toBe>() + expect(schema.to).type.toBe>>() + }) + + it("headNonEmpty", () => { + expect(S.String.pipe).type.not.toBeCallableWith(S.headNonEmpty) + + const schema = S.headNonEmpty(S.NonEmptyArray(S.Number)) + expect(S.asSchema(schema)).type.toBe]>>() + expect(schema) + .type.toBe, S.SchemaClass>>() + expect(schema.annotations({})) + .type.toBe, S.SchemaClass>>() + expect(schema.from).type.toBe>() + expect(schema.to).type.toBe>() + }) + + it("headOrElse", () => { + expect(S.String.pipe).type.not.toBeCallableWith(S.headOrElse()) + expect(S.headOrElse).type.not.toBeCallableWith(S.Array(S.Number), () => "a") + when(S.Array(S.Number).pipe).isCalledWith(expect(S.headOrElse).type.not.toBeCallableWith(() => "a")) + + const schema = S.headOrElse(S.Array(S.Number)) + expect(S.asSchema(schema)).type.toBe>>() + expect(schema).type.toBe, S.SchemaClass>>() + expect(schema.annotations({})).type.toBe, S.SchemaClass>>() + expect(schema.from).type.toBe>() + expect(schema.to).type.toBe>() + }) + + it("pluck", () => { + expect(S.pluck).type.not.toBeCallableWith( + S.Struct({ a: S.propertySignature(S.Number).pipe(S.fromKey("c")) }), + "a" + ) + + expect(pipe(S.Struct({ a: S.String, b: S.Number }), S.pluck("a"))) + .type.toBe>() + const schema = S.pluck(S.Struct({ a: S.String, b: S.Number }), "a") + expect(schema).type.toBe>() + expect(schema.annotations({})).type.toBe>() + + // should support optional fields + expect(S.pluck(S.Struct({ a: S.optional(S.String), b: S.Number }), "a")) + .type.toBe>() + expect(pipe(S.Struct({ a: S.optional(S.String), b: S.Number }), S.pluck("a"))) + .type.toBe>() + expect(S.pluck(S.Struct({ a: S.optionalWith(S.String, { exact: true }), b: S.Number }), "a")) + .type.toBe>() + expect(pipe(S.Struct({ a: S.optionalWith(S.String, { exact: true }), b: S.Number }), S.pluck("a"))) + .type.toBe>() + }) + + it("parseNumber", () => { + expect(S.parseNumber).type.not.toBeCallableWith(S.Null) + + const schema = S.parseNumber(S.String) + expect(S.asSchema(schema)).type.toBe>() + expect(schema).type.toBe>() + expect(schema.annotations({})).type.toBe>() + expect(schema.from).type.toBe() + expect(schema.to).type.toBe() + }) + }) + + it("standardSchemaV1", () => { + const standardSchema = S.standardSchemaV1(S.NumberFromString) + expect(S.asSchema(standardSchema)).type.toBe>() + expect(standardSchema).type.toBe & S.SchemaClass>() + }) +}) diff --git a/repos/effect/packages/effect/dtslint/Schema/SchemaClass.tst.ts b/repos/effect/packages/effect/dtslint/Schema/SchemaClass.tst.ts new file mode 100644 index 0000000..881f881 --- /dev/null +++ b/repos/effect/packages/effect/dtslint/Schema/SchemaClass.tst.ts @@ -0,0 +1,278 @@ +import type { SchemaAST as AST } from "effect" +import { Schema as S } from "effect" +import { describe, expect, it } from "tstyche" + +type HasFields = S.Struct | { + readonly [S.RefineSchemaId]: HasFields +} + +declare const checkForConflicts: ( + fieldsOr: Fields | HasFields +) => S.Struct + +declare const aContext: S.Schema +declare const bContext: S.Schema +declare const cContext: S.Schema + +class WithContext extends S.Class("WithContext")({ a: aContext, b: bContext }) {} + +describe("Schema.Class", () => { + it("should check conflicts with fields/from keys", () => { + expect(checkForConflicts({ fields: S.String })).type.toBe< + S.Struct<{ fields: typeof S.String }> + >() + expect(checkForConflicts({ from: S.String })).type.toBe< + S.Struct<{ from: typeof S.String }> + >() + expect(checkForConflicts(S.Struct({ fields: S.String }))).type.toBe< + S.Struct<{ fields: typeof S.String }> + >() + expect(checkForConflicts(S.Struct({ from: S.String }))).type.toBe< + S.Struct<{ from: typeof S.String }> + >() + expect(checkForConflicts(S.Struct({ fields: S.String }).pipe(S.filter(() => true)))).type.toBe< + S.Struct<{ fields: typeof S.String }> + >() + expect(checkForConflicts(S.Struct({ from: S.String }).pipe(S.filter(() => true)))).type.toBe< + S.Struct<{ from: typeof S.String }> + >() + expect( + checkForConflicts(S.Struct({ fields: S.String }).pipe(S.filter(() => true), S.filter(() => true))) + ).type.toBe>() + expect( + checkForConflicts(S.Struct({ from: S.String }).pipe(S.filter(() => true), S.filter(() => true))) + ).type.toBe>() + expect(checkForConflicts({ fields: S.Struct({ a: S.String }) })).type.toBe< + S.Struct<{ fields: S.Struct<{ a: typeof S.String }> }> + >() + expect(checkForConflicts({ fields: S.Struct({ a: S.String }).pipe(S.filter(() => true)) })).type.toBe< + S.Struct<{ fields: S.filter> }> + >() + expect( + checkForConflicts({ fields: S.Struct({ a: S.String }).pipe(S.filter(() => true), S.filter(() => true)) }) + ).type.toBe>> }>>() + expect(checkForConflicts({ from: S.Struct({ a: S.String }) })).type.toBe< + S.Struct<{ from: S.Struct<{ a: typeof S.String }> }> + >() + expect(checkForConflicts({ from: S.Struct({ a: S.String }).pipe(S.filter(() => true)) })).type.toBe< + S.Struct<{ from: S.filter> }> + >() + expect( + checkForConflicts({ from: S.Struct({ a: S.String }).pipe(S.filter(() => true), S.filter(() => true)) }) + ).type.toBe>> }>>() + }) + + it("A class with no fields should permit an empty argument in the constructor.", () => { + class NoFields extends S.Class("NoFields")({}) {} + + expect(NoFields.ast).type.toBe() + + expect>().type.toBe< + [props?: void | {}, options?: S.MakeOptions | undefined] + >() + + new NoFields() + NoFields.make() + new NoFields({}) + NoFields.make({}) + }) + + it("should reject non existing props", () => { + class A extends S.Class("A")({ + a: S.String + }) {} + + expect(A).type.not.toBeConstructableWith({ a: "a", b: "b" }) + expect(A.make).type.not.toBeCallableWith({ a: "a", b: "b" }) + }) + + it("A class with all fields with a default should permit an empty argument in the constructor.", () => { + class AllDefaultedFields extends S.Class("AllDefaultedFields")({ + a: S.String.pipe(S.propertySignature, S.withConstructorDefault(() => "")) + }) {} + + expect>().type.toBe< + [props?: void | { readonly a?: string }, options?: S.MakeOptions | undefined] + >() + + new AllDefaultedFields() + AllDefaultedFields.make() + new AllDefaultedFields({}) + AllDefaultedFields.make({}) + }) + + it("test Context", () => { + expect>() + .type.toBe() + expect>() + .type.toBe<{ readonly a: string; readonly b: number }>() + expect>() + .type.toBe<"a" | "b">() + }) + + it("should be a constructor", () => { + expect>() + .type.toBe<[props: { readonly a: string; readonly b: number }, options?: S.MakeOptions | undefined]>() + }) + + it("should expose a `fields` field", () => { + expect(WithContext.fields).type.toBe<{ readonly a: typeof aContext; readonly b: typeof bContext }>() + }) + + it("can be extended with Class.extend", () => { + class Extended extends WithContext.extend("Extended")({ + c: cContext + }) {} + + expect>() + .type.toBe() + + expect>() + .type.toBe<{ readonly a: string; readonly b: number; readonly c: boolean }>() + + expect>().type.toBe<"a" | "b" | "c">() + + expect(Extended.fields) + .type.toBe< + { + readonly a: S.Schema + readonly b: S.Schema + readonly c: S.Schema + } + >() + + expect>() + .type.toBe< + [props: { readonly a: string; readonly b: number; readonly c: boolean }, options?: S.MakeOptions | undefined] + >() + }) + + it("can be extended with another Class `fields` field", () => { + class ExtendedFromClassFields extends S.Class("ExtendedFromClassFields")({ + ...WithContext.fields, + b: S.String, + c: cContext + }) {} + + expect>() + .type.toBe() + + expect>() + .type.toBe<{ readonly a: string; readonly b: string; readonly c: boolean }>() + + expect>() + .type.toBe<"a" | "c">() + + expect(ExtendedFromClassFields.fields) + .type.toBe< + { + readonly b: typeof S.String + readonly c: S.Schema + readonly a: S.Schema + } + >() + + expect>() + .type.toBe< + [props: { readonly a: string; readonly b: string; readonly c: boolean }, options?: S.MakeOptions | undefined] + >() + }) + + it("can be extended with another TaggedClass `fields` field", () => { + class ExtendedFromTaggedClassFields + extends S.TaggedClass()("ExtendedFromTaggedClassFields", { + ...WithContext.fields, + b: S.String, + c: cContext + }) + {} + + expect>() + .type.toBe() + + expect>() + .type.toBe< + { readonly a: string; readonly b: string; readonly c: boolean; readonly _tag: "ExtendedFromTaggedClassFields" } + >() + + expect>() + .type.toBe<"a" | "c">() + + expect(ExtendedFromTaggedClassFields.fields) + .type.toBe< + { + readonly _tag: S.tag<"ExtendedFromTaggedClassFields"> + readonly b: typeof S.String + readonly c: S.Schema + readonly a: S.Schema + } + >() + + expect>() + .type.toBe< + [props: { readonly a: string; readonly b: string; readonly c: boolean }, options?: S.MakeOptions | undefined] + >() + }) + + it("should accept a HasFields as argument", () => { + class _A extends S.Class<_A>("A")(S.Struct({ a: S.String })) {} + class _B extends S.Class<_B>("B")(S.Struct({ a: S.String }).pipe(S.filter(() => true))) {} + class _C extends S.Class<_C>("C")( + S.Struct({ a: S.String }).pipe(S.filter(() => true), S.filter(() => true)) + ) {} + }) + + it("users can override an instance member property", () => { + class A extends S.Class("A")(S.Struct({ a: S.String })) { + readonly b: number = 1 + } + + class B extends A.extend("B")({ c: S.String }) { + override readonly b = 2 + } + + expect(new B({ a: "a", c: "c" }).b) + .type.toBe<2>() + }) + + it("users can override an instance member function", () => { + class A extends S.Class("A")(S.Struct({ a: S.String })) { + b(): number { + return 1 + } + } + + class B extends A.extend("B")({ c: S.String }) { + override b(): 2 { + return 2 + } + } + + expect(new B({ a: "a", c: "c" }).b()) + .type.toBe<2>() + }) + + it("users can override a field with an instance member property", () => { + class A extends S.Class("A")(S.Struct({ a: S.String })) {} + + class B extends A.extend("B")({ + c: S.String + }) { + override readonly a = "default" + } + + expect(new B({ a: "a", c: "c" }).a) + .type.toBe<"default">() + }) + + it(`users can't override an instance member property with a field`, () => { + class A extends S.Class("A")(S.Struct({ a: S.String })) { + readonly b = 1 + } + + class B extends A.extend("B")({ b: S.Number }) {} + + expect(new B({ a: "a", b: 2 }).b) + .type.toBe<1>() + }) +}) diff --git a/repos/effect/packages/effect/dtslint/Schema/Serializable.tst.ts b/repos/effect/packages/effect/dtslint/Schema/Serializable.tst.ts new file mode 100644 index 0000000..47976dc --- /dev/null +++ b/repos/effect/packages/effect/dtslint/Schema/Serializable.tst.ts @@ -0,0 +1,78 @@ +import { Schema } from "effect" +import { hole } from "effect/Function" +import { describe, expect, it } from "tstyche" + +class TR extends Schema.TaggedRequest()("TR", { + failure: Schema.String, + success: Schema.NumberFromString, + payload: { + id: Schema.NumberFromString + } +}) {} + +const successSchema = (req: Req) => Schema.successSchema(Schema.asWithResult(req)) + +const failureSchema = (req: Req) => Schema.failureSchema(Schema.asWithResult(req)) + +const selfSchema = (req: Req) => + Schema.serializableSchema(Schema.asSerializable(req)) + +declare const F: Schema.Schema<"failure", "failure-encoded", "failure-context"> +declare const S_: Schema.Schema<"success", "success-encoded", "success-context"> +declare const P: { + a: Schema.Schema<"payload", "payload-encoded", "payload-context"> +} + +class Foo extends Schema.TaggedRequest()("A", { + failure: F, + success: S_, + payload: P +}) {} + +describe("Schema Serializable", () => { + it("Serializable type-level helpers", () => { + expect(hole>>()).type.toBe() + + expect(hole>>()).type.toBe< + Schema.Struct.Encoded< + { readonly _tag: Schema.tag<"A"> } & { a: Schema.Schema<"payload", "payload-encoded", "payload-context"> } + > + >() + + expect(hole>>()).type.toBe<"payload-context">() + }) + + it("successSchema", () => { + expect(successSchema(new TR({ id: 1 }))).type.toBe>() + }) + + it("failureSchema", () => { + expect(failureSchema(new TR({ id: 1 }))).type.toBe>() + }) + + it("selfSchema", () => { + expect(selfSchema(new TR({ id: 1 }))).type.toBe< + Schema.Schema< + TR, + Schema.Struct.Encoded<{ readonly _tag: Schema.tag<"TR"> } & { id: typeof Schema.NumberFromString }>, + never + > + >() + }) + + it("WithResult type-level helpers", () => { + expect(hole>>()).type.toBe<"success">() + expect(hole>>()).type.toBe<"success-encoded">() + expect(hole>>()).type.toBe<"failure">() + expect(hole>>()).type.toBe<"failure-encoded">() + expect(hole>>()).type.toBe< + "failure-context" | "success-context" + >() + }) + + it("SerializableWithResult type-level helpers", () => { + expect(hole>>()).type.toBe< + "failure-context" | "success-context" | "payload-context" + >() + }) +}) diff --git a/repos/effect/packages/effect/dtslint/Schema/TaggedClass.tst.ts b/repos/effect/packages/effect/dtslint/Schema/TaggedClass.tst.ts new file mode 100644 index 0000000..4cc82f6 --- /dev/null +++ b/repos/effect/packages/effect/dtslint/Schema/TaggedClass.tst.ts @@ -0,0 +1,18 @@ +import { Schema } from "effect" +import { describe, expect, it } from "tstyche" + +describe("Schema.TaggedClass", () => { + it("Annotations as tuple", () => { + // @ts-expect-error! + class _A extends Schema.TaggedClass<_A>()("A", { id: Schema.Number }, [ + undefined, + undefined, + { + pretty: () => (x) => { + expect(x).type.toBe<{ readonly _tag: "A"; readonly id: number }>() + return "" + } + } + ]) {} + }) +}) diff --git a/repos/effect/packages/effect/dtslint/Schema/TaggedError.tst.ts b/repos/effect/packages/effect/dtslint/Schema/TaggedError.tst.ts new file mode 100644 index 0000000..8c93fe9 --- /dev/null +++ b/repos/effect/packages/effect/dtslint/Schema/TaggedError.tst.ts @@ -0,0 +1,43 @@ +import type { Unify } from "effect" +import { Effect, Schema } from "effect" +import { describe, expect, it } from "tstyche" + +describe("Schema.TaggedError", () => { + it("should be yieldable", () => { + class Err extends Schema.TaggedError()("Err", {}) {} + + expect>().type.toBe() + + expect(Effect.gen(function*($) { + return yield* $(new Err()) + })).type.toBe>() + }) + + it("make should respect custom constructors", () => { + class MyError extends Schema.TaggedError()( + "MyError", + { message: Schema.String } + ) { + constructor({ a, b }: { a: string; b: string }) { + super({ message: `${a}:${b}` }) + } + } + + expect(MyError.make({ a: "a", b: "b" }).message).type.toBe() + expect(new MyError({ a: "a", b: "b" }).message).type.toBe() + }) + + it("Annotations as tuple", () => { + // @ts-expect-error! + class _A extends Schema.TaggedError<_A>()("A", { id: Schema.Number }, [ + undefined, + undefined, + { + pretty: () => (x) => { + expect(x).type.toBe<{ readonly _tag: "A"; readonly id: number }>() + return "" + } + } + ]) {} + }) +}) diff --git a/repos/effect/packages/effect/dtslint/Schema/TaggedRequest.tst.ts b/repos/effect/packages/effect/dtslint/Schema/TaggedRequest.tst.ts new file mode 100644 index 0000000..7ca6c25 --- /dev/null +++ b/repos/effect/packages/effect/dtslint/Schema/TaggedRequest.tst.ts @@ -0,0 +1,46 @@ +import { Schema } from "effect" +import { describe, expect, it } from "tstyche" + +describe("Schema.TaggedRequest", () => { + it("should expose fields, _tag, success and failure", () => { + class A extends Schema.TaggedRequest()("A", { + failure: Schema.String, + success: Schema.Number, + payload: { + id: Schema.Number + } + }) {} + + expect(A.fields) + .type.toBe<{ readonly _tag: Schema.tag<"A">; readonly id: typeof Schema.Number }>() + + expect(A._tag) + .type.toBe<"A">() + + expect(A.success) + .type.toBe() + + expect(A.failure) + .type.toBe() + }) + + it("Annotations as tuple", () => { + // @ts-expect-error! + class _A extends Schema.TaggedRequest<_A>()("A", { + failure: Schema.String, + success: Schema.Number, + payload: { + id: Schema.Number + } + }, [ + undefined, + undefined, + { + pretty: () => (x) => { + expect(x).type.toBe<{ readonly _tag: "A"; readonly id: number }>() + return "" + } + } + ]) {} + }) +}) diff --git a/repos/effect/packages/effect/dtslint/Schema/Userland.tst.ts b/repos/effect/packages/effect/dtslint/Schema/Userland.tst.ts new file mode 100644 index 0000000..1ef1b3a --- /dev/null +++ b/repos/effect/packages/effect/dtslint/Schema/Userland.tst.ts @@ -0,0 +1,43 @@ +import { Schema as S } from "effect" +import { describe, expect, it } from "tstyche" + +describe("Schema Userland", () => { + it("goal: pass a Schema Class as a parameter to a function", () => { + // Discord: https://discordapp.com/channels/795981131316985866/847382157861060618/1268580485412556883 + + class Person extends S.Class("Person")({ + id: S.Number, + name: S.String.pipe(S.nonEmptyString()) + }) { + static create(id: number): Person { + return new Person({ id, name: "" }) + } + update(id: number): Person { + return new Person({ id, name: this.name }) + } + } + + type ModelProto = { update(id: number): Person } + type ModelStatics = { create(id: number): Person } + type Model< + Self, + Fields extends S.Struct.Fields + > = + & S.Class< + Self, + Fields, + S.Struct.Encoded, + S.Struct.Context, + S.Struct.Constructor, + ModelProto, + {} + > + & ModelStatics + + function f1(clazz: Model) { + return clazz.create(2).update(3) + } + + expect(f1(Person)).type.toBe() + }) +}) diff --git a/repos/effect/packages/effect/dtslint/SchemaAST.tst.ts b/repos/effect/packages/effect/dtslint/SchemaAST.tst.ts new file mode 100644 index 0000000..8e9b4fd --- /dev/null +++ b/repos/effect/packages/effect/dtslint/SchemaAST.tst.ts @@ -0,0 +1,11 @@ +import * as AST from "effect/SchemaAST" +import { describe, expect, it } from "tstyche" + +describe("SchemaAST", () => { + it("annotations", () => { + // should allow to add custom string annotations to a schema + expect(AST.annotations(AST.stringKeyword, { a: 1 })).type.toBe() + // should allow to add custom symbol annotations to a schema + expect(AST.annotations(AST.stringKeyword, { [Symbol.for("a")]: 1 })).type.toBe() + }) +}) diff --git a/repos/effect/packages/effect/dtslint/Sink.tst.ts b/repos/effect/packages/effect/dtslint/Sink.tst.ts new file mode 100644 index 0000000..e3aad3f --- /dev/null +++ b/repos/effect/packages/effect/dtslint/Sink.tst.ts @@ -0,0 +1,16 @@ +import type { Chunk } from "effect" +import { Predicate, Sink } from "effect" +import { describe, expect, it } from "tstyche" + +declare const predicate: Predicate.Predicate + +describe("Sink", () => { + it("collectAllWhile", () => { + expect(Sink.collectAllWhile(predicate)) + .type.toBe, string | number, string | number, never, never>>() + expect(Sink.collectAllWhile(Predicate.isNumber)) + .type.toBe, unknown, unknown, never, never>>() + expect(Sink.collectAllWhile(Predicate.isString)) + .type.toBe, unknown, unknown, never, never>>() + }) +}) diff --git a/repos/effect/packages/effect/dtslint/SortedMap.tst.ts b/repos/effect/packages/effect/dtslint/SortedMap.tst.ts new file mode 100644 index 0000000..79065bb --- /dev/null +++ b/repos/effect/packages/effect/dtslint/SortedMap.tst.ts @@ -0,0 +1,15 @@ +import type { Order } from "effect" +import { pipe, SortedMap } from "effect" +import { describe, expect, it } from "tstyche" + +declare const stringAndNumberIterable: Iterable<[string, number]> +declare const stringOrUndefinedOrder: Order.Order + +describe("SortedMap", () => { + it("fromIterable", () => { + expect(SortedMap.fromIterable(stringAndNumberIterable, stringOrUndefinedOrder)) + .type.toBe>() + expect(pipe(stringAndNumberIterable, SortedMap.fromIterable(stringOrUndefinedOrder))) + .type.toBe>() + }) +}) diff --git a/repos/effect/packages/effect/dtslint/SortedSet.tst.ts b/repos/effect/packages/effect/dtslint/SortedSet.tst.ts new file mode 100644 index 0000000..8dc2feb --- /dev/null +++ b/repos/effect/packages/effect/dtslint/SortedSet.tst.ts @@ -0,0 +1,96 @@ +import { pipe } from "effect/Function" +import type { Order } from "effect/Order" +import type * as Predicate from "effect/Predicate" +import * as SortedSet from "effect/SortedSet" +import { describe, expect, it } from "tstyche" + +declare const numbers: SortedSet.SortedSet +declare const numbersOrStrings: SortedSet.SortedSet +declare const stringIterable: Iterable +declare const stringOrUndefinedOrder: Order +declare const predicateNumbersOrStrings: Predicate.Predicate + +describe("SortedSet", () => { + it("every", () => { + pipe( + numbersOrStrings, + SortedSet.every((_item) => { + expect(_item).type.toBe() + return true + }) + ) + }) + + it("some", () => { + pipe( + numbersOrStrings, + SortedSet.some((_item) => { + expect(_item).type.toBe() + return true + }) + ) + }) + + it("partition", () => { + SortedSet.partition(numbersOrStrings, (_item) => { + expect(_item).type.toBe() + return true + }) + + pipe( + numbersOrStrings, + SortedSet.partition((_item) => { + expect(_item).type.toBe() + return true + }) + ) + expect(SortedSet.partition(numbersOrStrings, predicateNumbersOrStrings)).type.toBe< + [excluded: SortedSet.SortedSet, satisfying: SortedSet.SortedSet] + >() + + expect(pipe(numbersOrStrings, SortedSet.partition(predicateNumbersOrStrings))).type.toBe< + [excluded: SortedSet.SortedSet, satisfying: SortedSet.SortedSet] + >() + }) + + it("fromIterable", () => { + expect(SortedSet.fromIterable(stringIterable, stringOrUndefinedOrder)).type.toBe< + SortedSet.SortedSet + >() + + expect(pipe(stringIterable, SortedSet.fromIterable(stringOrUndefinedOrder))).type.toBe< + SortedSet.SortedSet + >() + }) + + it("filter", () => { + SortedSet.filter(numbersOrStrings, (_item) => { + expect(_item).type.toBe() + return true + }) + + pipe( + numbersOrStrings, + SortedSet.filter((_item) => { + expect(_item).type.toBe() + return true + }) + ) + + expect(SortedSet.filter(numbersOrStrings, predicateNumbersOrStrings)).type.toBe< + SortedSet.SortedSet + >() + + expect(SortedSet.filter(numbers, predicateNumbersOrStrings)).type.toBe< + SortedSet.SortedSet + >() + + expect(pipe(numbersOrStrings, SortedSet.filter(predicateNumbersOrStrings))).type.toBe< + SortedSet.SortedSet + >() + + expect(pipe(numbers, SortedSet.filter(predicateNumbersOrStrings))).type.toBe< + SortedSet.SortedSet + >() + }) +}) diff --git a/repos/effect/packages/effect/dtslint/Stream.tst.ts b/repos/effect/packages/effect/dtslint/Stream.tst.ts new file mode 100644 index 0000000..c2a04f9 --- /dev/null +++ b/repos/effect/packages/effect/dtslint/Stream.tst.ts @@ -0,0 +1,327 @@ +import type { Chunk, Effect, Scope } from "effect" +import { Cause, pipe, Predicate, Stream } from "effect" +import { describe, expect, it } from "tstyche" + +declare const numbers: Stream.Stream +declare const numbersOrStrings: Stream.Stream +declare const predicateNumbersOrStrings: Predicate.Predicate + +describe("Stream", () => { + it("filter", () => { + Stream.filter(numbersOrStrings, (item) => { + expect(item).type.toBe() + return true + }) + + pipe( + numbersOrStrings, + Stream.filter((item) => { + expect(item).type.toBe() + return true + }) + ) + + expect(Stream.filter(numbersOrStrings, predicateNumbersOrStrings)).type.toBe< + Stream.Stream + >() + + expect(Stream.filter(numbers, predicateNumbersOrStrings)).type.toBe< + Stream.Stream + >() + + expect(pipe(numbersOrStrings, Stream.filter(predicateNumbersOrStrings))).type.toBe< + Stream.Stream + >() + + expect(pipe(numbers, Stream.filter(predicateNumbersOrStrings))).type.toBe< + Stream.Stream + >() + + expect(Stream.filter(numbersOrStrings, Predicate.isNumber)).type.toBe< + Stream.Stream + >() + + expect(pipe(numbersOrStrings, Stream.filter(Predicate.isNumber))).type.toBe< + Stream.Stream + >() + }) + + it("find", () => { + Stream.find(numbersOrStrings, (item) => { + expect(item).type.toBe() + return true + }) + + pipe( + numbersOrStrings, + Stream.find((item) => { + expect(item).type.toBe() + return true + }) + ) + + expect(Stream.find(numbersOrStrings, predicateNumbersOrStrings)).type.toBe< + Stream.Stream + >() + + expect(pipe(numbersOrStrings, Stream.find(predicateNumbersOrStrings))).type.toBe< + Stream.Stream + >() + + expect(Stream.find(numbersOrStrings, Predicate.isNumber)).type.toBe< + Stream.Stream + >() + + expect(pipe(numbersOrStrings, Stream.find(Predicate.isNumber))).type.toBe< + Stream.Stream + >() + }) + + it("partition", () => { + Stream.partition(numbersOrStrings, (item) => { + expect(item).type.toBe() + return true + }) + + pipe( + numbersOrStrings, + Stream.partition((item) => { + expect(item).type.toBe() + return true + }) + ) + + // The expected type is an Effect that yields a tuple of two Streams. + expect(Stream.partition(numbersOrStrings, predicateNumbersOrStrings)).type.toBe< + Effect.Effect< + [ + excluded: Stream.Stream, + satisfying: Stream.Stream + ], + never, + Scope.Scope + > + >() + + expect(pipe(numbersOrStrings, Stream.partition(predicateNumbersOrStrings))).type.toBe< + Effect.Effect< + [ + excluded: Stream.Stream, + satisfying: Stream.Stream + ], + never, + Scope.Scope + > + >() + + expect(Stream.partition(numbersOrStrings, Predicate.isNumber)).type.toBe< + Effect.Effect< + [excluded: Stream.Stream, satisfying: Stream.Stream], + never, + Scope.Scope + > + >() + + expect(pipe(numbersOrStrings, Stream.partition(Predicate.isNumber))) + .type.toBe< + Effect.Effect< + [excluded: Stream.Stream, satisfying: Stream.Stream], + never, + Scope.Scope + > + >() + }) + + it("takeWhile", () => { + Stream.takeWhile(numbersOrStrings, (item) => { + expect(item).type.toBe() + return true + }) + + pipe( + numbersOrStrings, + Stream.takeWhile((item) => { + expect(item).type.toBe() + return true + }) + ) + + expect(Stream.takeWhile(numbersOrStrings, predicateNumbersOrStrings)).type.toBe< + Stream.Stream + >() + + expect(pipe(numbersOrStrings, Stream.takeWhile(predicateNumbersOrStrings))).type.toBe< + Stream.Stream + >() + + expect(Stream.takeWhile(numbersOrStrings, Predicate.isNumber)).type.toBe< + Stream.Stream + >() + + expect(pipe(numbersOrStrings, Stream.takeWhile(Predicate.isNumber))).type.toBe< + Stream.Stream + >() + + // Additional variations: + expect(Stream.takeWhile(numbersOrStrings, predicateNumbersOrStrings)).type.toBe< + Stream.Stream + >() + + expect(Stream.takeWhile(numbers, predicateNumbersOrStrings)).type.toBe< + Stream.Stream + >() + + expect(pipe(numbersOrStrings, Stream.takeWhile(predicateNumbersOrStrings))).type.toBe< + Stream.Stream + >() + + expect(pipe(numbers, Stream.takeWhile(predicateNumbersOrStrings))).type.toBe< + Stream.Stream + >() + + expect(Stream.takeWhile(numbersOrStrings, Predicate.isNumber)).type.toBe< + Stream.Stream + >() + + expect(pipe(numbersOrStrings, Stream.takeWhile(Predicate.isNumber))).type.toBe< + Stream.Stream + >() + }) + + it("dropWhile", () => { + expect(Stream.dropWhile(numbers, predicateNumbersOrStrings)).type.toBe< + Stream.Stream + >() + + expect(pipe(numbers, Stream.dropWhile(predicateNumbersOrStrings))).type.toBe< + Stream.Stream + >() + + expect(Stream.dropWhile(numbersOrStrings, Predicate.isNumber)).type.toBe< + Stream.Stream + >() + + expect(pipe(numbersOrStrings, Stream.dropWhile(Predicate.isNumber))).type.toBe< + Stream.Stream + >() + }) + + it("dropUntil", () => { + expect(Stream.dropUntil(numbers, predicateNumbersOrStrings)).type.toBe< + Stream.Stream + >() + + expect(pipe(numbers, Stream.dropUntil(predicateNumbersOrStrings))).type.toBe< + Stream.Stream + >() + + expect(Stream.dropUntil(numbersOrStrings, Predicate.isNumber)).type.toBe< + Stream.Stream + >() + + expect(pipe(numbersOrStrings, Stream.dropUntil(Predicate.isNumber))).type.toBe< + Stream.Stream + >() + }) + + it("split", () => { + expect(Stream.split(numbers, predicateNumbersOrStrings)).type.toBe< + Stream.Stream, never, never> + >() + + expect(pipe(numbers, Stream.split(predicateNumbersOrStrings))).type.toBe< + Stream.Stream, never, never> + >() + + expect(Stream.split(numbersOrStrings, Predicate.isNumber)).type.toBe< + Stream.Stream, never, never> + >() + + expect(pipe(numbersOrStrings, Stream.split(Predicate.isNumber))).type.toBe< + Stream.Stream, never, never> + >() + }) + + it("takeUntil", () => { + expect(Stream.takeUntil(numbers, predicateNumbersOrStrings)).type.toBe< + Stream.Stream + >() + + expect(pipe(numbers, Stream.takeUntil(predicateNumbersOrStrings))).type.toBe< + Stream.Stream + >() + + expect(Stream.takeUntil(numbersOrStrings, Predicate.isNumber)).type.toBe< + Stream.Stream + >() + + expect(pipe(numbersOrStrings, Stream.takeUntil(Predicate.isNumber))).type.toBe< + Stream.Stream + >() + }) + + it("Do Notation", () => { + expect( + pipe( + Stream.Do, + Stream.bind("a", (scope) => { + expect(scope).type.toBe<{}>() + return Stream.succeed(1) + }), + Stream.bind("b", (scope) => { + expect(scope).type.toBe<{ a: number }>() + return Stream.succeed("b") + }), + Stream.let("c", (scope) => { + expect(scope).type.toBe<{ a: number; b: string }>() + return true + }) + ) + ).type.toBe>() + + expect( + pipe( + Stream.succeed(1), + Stream.bindTo("a"), + Stream.bind("b", (scope) => { + expect(scope).type.toBe<{ a: number }>() + return Stream.succeed("b") + }), + Stream.let("c", (scope) => { + expect(scope).type.toBe<{ a: number; b: string }>() + return true + }) + ) + ).type.toBe>() + }) + + it("zipLatestAll", () => { + expect(Stream.zipLatestAll()).type.toBe>() + + expect(Stream.zipLatestAll(numbers, numbersOrStrings)).type.toBe< + Stream.Stream<[number, string | number], never, never> + >() + + expect(Stream.zipLatestAll(numbers, numbersOrStrings, Stream.fail(new Error("")))).type.toBe< + Stream.Stream<[number, string | number, never], Error, never> + >() + }) + + it("mergeWithTag", () => { + expect( + Stream.mergeWithTag( + { + a: pipe(Stream.make(0), Stream.tap(() => new Cause.NoSuchElementException())), + b: Stream.make("") + }, + { concurrency: 1 } + ) + ).type.toBe< + Stream.Stream< + { _tag: "a"; value: number } | { _tag: "b"; value: string }, + Cause.NoSuchElementException, + never + > + >() + }) +}) diff --git a/repos/effect/packages/effect/dtslint/String.tst.ts b/repos/effect/packages/effect/dtslint/String.tst.ts new file mode 100644 index 0000000..b6adb8e --- /dev/null +++ b/repos/effect/packages/effect/dtslint/String.tst.ts @@ -0,0 +1,100 @@ +import { hole, String as Str } from "effect" +import { describe, expect, it } from "tstyche" + +describe("String", () => { + it("concat", () => { + expect(Str.concat(Str.capitalize("foo"), Str.capitalize("bar"))) + .type.toBe("FooBar") + }) + + it("toUpperCase", () => { + expect(Str.toUpperCase("foo")) + .type.toBe("FOO") + }) + + it("toLowerCase", () => { + expect(Str.toLowerCase("BAR")) + .type.toBe("bar") + }) + + it("capitalize", () => { + expect(Str.capitalize("foo")) + .type.toBe("Foo") + }) + + it("uncapitalize", () => { + expect(Str.uncapitalize("BAR")) + .type.toBe("bAR") + }) + + it("trim", () => { + expect(Str.trim(" foo ")).type.toBe("foo") + expect( + Str.trim(` + \t foo + \r\n +`) + ).type.toBe("foo") + }) + + it("trimEnd", () => { + expect( + Str.trimEnd(` foo + \r\n +`) + ).type.toBe(" foo") + }) + + it("trimStart", () => { + expect( + Str.trimStart(` + \r\n\t foo `) + ).type.toBe("foo ") + }) + + describe("String type helpers", () => { + type FooCapitalCase = "Foo" + type BarCapitalCase = "Bar" + + it("Str.Concat", () => { + type Test = Str.Concat + expect(hole()) + .type.toBe<"FooBar">() + }) + + type LeadingSpaces = " foo" + type TrailingSpaces = "bar " + type LeadingAndTrailingSpaces = " baz " + + type NewLines = ` + foo + ` + type NewLinesAndTabs = ` + \t\t foo + ` + type CarriageReturns = ` + \r\n foo + ` + + it("Str.TrimStart", () => { + expect>() + .type.toBe<"foo">() + }) + + it("Str.TrimEnd", () => { + expect>() + .type.toBe<"bar">() + }) + + it("Str.Trim", () => { + expect>() + .type.toBe<"baz">() + expect>() + .type.toBe<"foo">() + expect>() + .type.toBe<"foo">() + expect>() + .type.toBe<"foo">() + }) + }) +}) diff --git a/repos/effect/packages/effect/dtslint/Struct.tst.ts b/repos/effect/packages/effect/dtslint/Struct.tst.ts new file mode 100644 index 0000000..7a00e24 --- /dev/null +++ b/repos/effect/packages/effect/dtslint/Struct.tst.ts @@ -0,0 +1,342 @@ +import { hole, pipe, Struct } from "effect" +import { describe, expect, it, when } from "tstyche" + +const asym = Symbol.for("effect/dtslint/a") +const bsym = Symbol.for("effect/dtslint/b") +const csym = Symbol.for("effect/dtslint/c") +const dsym = Symbol.for("effect/dtslint/d") + +declare const string$numberRecord: Record +declare const symbol$numberRecord: Record +declare const number$numberRecord: Record +declare const templateLiteral$numberRecord: Record<`a${string}`, number> + +const stringStruct = { a: "a", b: 1, c: true } +const symbolStruct = { [asym]: "a", [bsym]: 1, [csym]: true } +const numberStruct = { 1: "a", 2: 1, 3: true } +declare const optionalStringStruct: { a?: string; b: number; c: boolean } + +describe("Struct", () => { + describe("evolve", () => { + it("evolves a single field", () => { + expect(Struct.evolve({ a: 1 }, { + a: (n) => { + expect(n).type.toBe() + return n > 0 + } + })) + .type.toBe<{ a: boolean }>() + expect(pipe( + { a: 1 }, + Struct.evolve({ + a: (n) => { + expect(n).type.toBe() + return n > 0 + } + }) + )) + .type.toBe<{ a: boolean }>() + }) + + it("evolves multiple fields", () => { + expect(Struct.evolve({ a: "a", b: 1 }, { + a: (s) => { + expect(s).type.toBe() + return s.length + } + })) + .type.toBe<{ a: number; b: number }>() + expect(pipe( + { a: "a", b: 1 }, + Struct.evolve({ + a: (s) => { + expect(s).type.toBe() + return s.length + } + }) + )) + .type.toBe<{ a: number; b: number }>() + }) + + it("errors", () => { + expect(Struct.evolve).type.not.toBeCallableWith( + { a: "a", b: 1 }, + { a: (n: number) => n } + ) + expect(Struct.evolve).type.not.toBeCallableWith( + hole<{ a: "a"; b: 1 }>(), + hole>() + ) + expect(Struct.evolve).type.not.toBeCallableWith( + hole<{ a: "a"; b: 1 }>(), + hole null>>() + ) + when(pipe).isCalledWith( + { a: "a", b: 1 }, + expect(Struct.evolve).type.not.toBeCallableWith({ a: (n: number) => n }) + ) + + when(pipe).isCalledWith( + hole<{ a: "a"; b: 1 }>(), + expect(Struct.evolve).type.not.toBeCallableWith(hole>()) + ) + + when(pipe).isCalledWith( + hole<{ a: "a"; b: 1 }>(), + expect(Struct.evolve).type.not.toBeCallableWith(hole null>>()) + ) + }) + }) + + describe("get", () => { + it("returns unknown when getting a key from an empty object", () => { + expect(pipe({}, Struct.get("a"))) + .type.toBe() + }) + + it("gets a required property", () => { + expect(pipe(stringStruct, Struct.get("a"))) + .type.toBe() + expect(Struct.get("a")(stringStruct)) + .type.toBe() + + expect(pipe(symbolStruct, Struct.get(asym))) + .type.toBe() + expect(Struct.get(asym)(symbolStruct)) + .type.toBe() + + expect(pipe(numberStruct, Struct.get(1))) + .type.toBe() + expect(Struct.get(1)(numberStruct)) + .type.toBe() + }) + + it("gets an optional property", () => { + expect(pipe(optionalStringStruct, Struct.get("a"))) + .type.toBe() + expect(Struct.get("a")(optionalStringStruct)) + .type.toBe() + }) + + it("record", () => { + expect(pipe(string$numberRecord, Struct.get("a"))) + .type.toBe() + expect(Struct.get("a")(string$numberRecord)) + .type.toBe() + + expect(pipe(symbol$numberRecord, Struct.get(asym))) + .type.toBe() + expect(Struct.get(asym)(symbol$numberRecord)) + .type.toBe() + + expect(pipe(number$numberRecord, Struct.get(1))) + .type.toBe() + expect(Struct.get(1)(number$numberRecord)) + .type.toBe() + + expect(pipe(templateLiteral$numberRecord, Struct.get("ab"))) + .type.toBe() + expect(Struct.get("ab")(templateLiteral$numberRecord)) + .type.toBe() + }) + + it("struct + record", () => { + expect(pipe(hole & { a: boolean }>(), Struct.get("a"))) + .type.toBe() + when(pipe).isCalledWith( + hole & { a: boolean }>(), + expect(Struct.get).type.not.toBeCallableWith("b") + ) + }) + }) + + describe("pick", () => { + it("errors when picking a non-existent key", () => { + when(pipe).isCalledWith( + stringStruct, + expect(Struct.pick).type.not.toBeCallableWith("d") + ) + expect(Struct.pick("d")).type.not.toBeCallableWith( + stringStruct + ) + + when(pipe).isCalledWith( + symbolStruct, + expect(Struct.pick).type.not.toBeCallableWith(dsym) + ) + expect(Struct.pick(dsym)).type.not.toBeCallableWith( + symbolStruct + ) + + when(pipe).isCalledWith( + numberStruct, + expect(Struct.pick).type.not.toBeCallableWith(4) + ) + expect(Struct.pick(4)).type.not.toBeCallableWith( + numberStruct + ) + }) + + it("returns a string record with unknown values when picking a dynamic string key", () => { + expect(pipe(stringStruct, Struct.pick("d" as string))) + .type.toBe<{ [x: string]: unknown }>() + expect(Struct.pick("d" as string)(stringStruct)) + .type.toBe<{ [x: string]: unknown }>() + }) + + it("returns a symbol record with unknown values when picking a dynamic symbol key", () => { + expect(pipe(symbolStruct, Struct.pick(dsym as symbol))) + .type.toBe<{ [x: symbol]: unknown }>() + expect(Struct.pick(dsym as symbol)(symbolStruct)) + .type.toBe<{ [x: symbol]: unknown }>() + }) + + it("returns a number record with unknown values when picking a dynamic numeric key", () => { + expect(pipe(numberStruct, Struct.pick(4 as number))) + .type.toBe<{ [x: number]: unknown }>() + expect(Struct.pick(4 as number)(numberStruct)) + .type.toBe<{ [x: number]: unknown }>() + }) + + it("struct with required properties", () => { + expect(pipe(stringStruct, Struct.pick("a", "b"))) + .type.toBe<{ a: string; b: number }>() + expect(Struct.pick(stringStruct, "a", "b")) + .type.toBe<{ a: string; b: number }>() + + expect(Struct.pick(symbolStruct, asym, bsym)) + .type.toBe<{ [asym]: string; [bsym]: number }>() + expect(pipe(symbolStruct, Struct.pick(asym, bsym))) + .type.toBe<{ [asym]: string; [bsym]: number }>() + + expect(Struct.pick(numberStruct, 1, 2)) + .type.toBe<{ 1: string; 2: number }>() + expect(pipe(numberStruct, Struct.pick(1, 2))) + .type.toBe<{ 1: string; 2: number }>() + }) + + it("record", () => { + expect(Struct.pick(string$numberRecord, "a", "b")) + .type.toBe<{ a?: number; b?: number }>() + expect(pipe(string$numberRecord, Struct.pick("a", "b"))) + .type.toBe<{ a?: number; b?: number }>() + + expect(Struct.pick(symbol$numberRecord, asym, bsym)) + .type.toBe<{ [asym]?: number; [bsym]?: number }>() + expect(pipe(symbol$numberRecord, Struct.pick(asym, bsym))) + .type.toBe<{ [asym]?: number; [bsym]?: number }>() + + expect(Struct.pick(number$numberRecord, 1, 2)) + .type.toBe<{ 1?: number; 2?: number }>() + expect(pipe(number$numberRecord, Struct.pick(1, 2))) + .type.toBe<{ 1?: number; 2?: number }>() + + expect(Struct.pick(templateLiteral$numberRecord, "aa", "ab")) + .type.toBe<{ aa?: number; ab?: number }>() + expect(pipe(templateLiteral$numberRecord, Struct.pick("aa", "ab"))) + .type.toBe<{ aa?: number; ab?: number }>() + }) + + it("struct + record", () => { + const sr = hole & { a: boolean }>() + + expect(Struct.pick(sr, "a")) + .type.toBe<{ a: boolean }>() + expect(pipe(sr, Struct.pick("a"))) + .type.toBe<{ a: boolean }>() + + // @tstyche fixme -- This doesn't work but it should + expect(Struct.pick).type.not.toBeCallableWith(sr, "b") + + when(pipe).isCalledWith( + sr, + expect(Struct.pick).type.not.toBeCallableWith("b") + ) + }) + + it("struct with optional properties", () => { + expect(Struct.pick(optionalStringStruct, "a", "b")) + .type.toBe<{ a?: string; b: number }>() + expect(pipe(optionalStringStruct, Struct.pick("a", "b"))) + .type.toBe<{ a?: string; b: number }>() + }) + }) + + describe("omit", () => { + it("errors when omitting a non-existent key", () => { + when(pipe).isCalledWith( + stringStruct, + expect(Struct.omit).type.not.toBeCallableWith("d") + ) + expect(Struct.omit("d")).type.not.toBeCallableWith( + stringStruct + ) + + when(pipe).isCalledWith( + symbolStruct, + expect(Struct.omit).type.not.toBeCallableWith(dsym) + ) + expect(Struct.omit(dsym)).type.not.toBeCallableWith( + symbolStruct + ) + + when(pipe).isCalledWith( + numberStruct, + expect(Struct.omit).type.not.toBeCallableWith(4) + ) + expect(Struct.omit(4)).type.not.toBeCallableWith( + numberStruct + ) + }) + + it("struct", () => { + expect(Struct.omit(stringStruct, "a")) + .type.toBe<{ b: number; c: boolean }>() + expect(pipe(stringStruct, Struct.omit("a"))) + .type.toBe<{ b: number; c: boolean }>() + + expect(Struct.omit(symbolStruct, asym)) + .type.toBe<{ [bsym]: number; [csym]: boolean }>() + expect(pipe(symbolStruct, Struct.omit(asym))) + .type.toBe<{ [bsym]: number; [csym]: boolean }>() + + expect(Struct.omit(numberStruct, 1)) + .type.toBe<{ 2: number; 3: boolean }>() + expect(pipe(numberStruct, Struct.omit(1))) + .type.toBe<{ 2: number; 3: boolean }>() + }) + + it("record", () => { + expect(Struct.omit(string$numberRecord, "a")) + .type.toBe<{ [x: string]: number }>() + expect(pipe(string$numberRecord, Struct.omit("a"))) + .type.toBe<{ [x: string]: number }>() + + expect(Struct.omit(symbol$numberRecord, asym)) + .type.toBe<{ [x: symbol]: number }>() + expect(pipe(symbol$numberRecord, Struct.omit(asym))) + .type.toBe<{ [x: symbol]: number }>() + + expect(Struct.omit(number$numberRecord, 1)) + .type.toBe<{ [x: number]: number }>() + expect(pipe(number$numberRecord, Struct.omit(1))) + .type.toBe<{ [x: number]: number }>() + + expect(Struct.omit(templateLiteral$numberRecord, "aa")) + .type.toBe<{ [x: `a${string}`]: number }>() + expect(pipe(templateLiteral$numberRecord, Struct.omit("aa"))) + .type.toBe<{ [x: `a${string}`]: number }>() + }) + }) + + describe("entries", () => { + it("excludes symbol keys", () => { + const c = Symbol("c") + const value = { a: "a", b: 1, [c]: 2 } + // should not include symbol keys + expect(Struct.entries(value)).type.toBe>() + // when the object is passed as a parameter, the values should be narrowed + expect(Struct.entries({ a: "a", b: 1, [c]: 2 })).type.toBe>() + }) + }) +}) diff --git a/repos/effect/packages/effect/dtslint/TMap.tst.ts b/repos/effect/packages/effect/dtslint/TMap.tst.ts new file mode 100644 index 0000000..cbf6f44 --- /dev/null +++ b/repos/effect/packages/effect/dtslint/TMap.tst.ts @@ -0,0 +1,44 @@ +import type { STM } from "effect" +import { pipe, TMap } from "effect" +import { describe, expect, it } from "tstyche" + +declare const string$number: TMap.TMap + +describe("TMap", () => { + it("removeIf", () => { + expect(TMap.removeIf(string$number, (key) => key === "aa")) + .type.toBe>>() + expect(pipe(string$number, TMap.removeIf((key) => key === "aa"))) + .type.toBe>>() + + expect(TMap.removeIf(string$number, (key) => key === "aa", { discard: false })) + .type.toBe>>() + expect(pipe(string$number, TMap.removeIf((key) => key === "aa", { discard: false }))) + .type.toBe>>() + + expect(TMap.removeIf(string$number, (key) => key === "aa", { discard: true })) + .type.toBe>() + expect(pipe(string$number, TMap.removeIf((key) => key === "aa", { discard: true }))) + .type.toBe>() + }) + + it("retainIf", () => { + expect(TMap.retainIf(string$number, (key) => key === "aa")) + .type.toBe>>() + + expect(TMap.retainIf(string$number, (key) => key === "aa", { discard: false })) + .type.toBe>>() + + expect(pipe(string$number, TMap.retainIf((key) => key === "aa"))) + .type.toBe>>() + + expect(pipe(string$number, TMap.retainIf((key) => key === "aa", { discard: false }))) + .type.toBe>>() + + expect(TMap.retainIf(string$number, (key) => key === "aa", { discard: true })) + .type.toBe>() + + expect(pipe(string$number, TMap.retainIf((key) => key === "aa", { discard: true }))) + .type.toBe>() + }) +}) diff --git a/repos/effect/packages/effect/dtslint/TSet.tst.ts b/repos/effect/packages/effect/dtslint/TSet.tst.ts new file mode 100644 index 0000000..9177cf6 --- /dev/null +++ b/repos/effect/packages/effect/dtslint/TSet.tst.ts @@ -0,0 +1,41 @@ +import type { STM } from "effect" +import { pipe, TSet } from "effect" +import { describe, expect, it } from "tstyche" + +declare const string: TSet.TSet + +describe("TSet", () => { + it("removeIf", () => { + expect(TSet.removeIf(string, (key) => key === "aa")) + .type.toBe>>() + expect(pipe(string, TSet.removeIf((key) => key === "aa"))) + .type.toBe>>() + + expect(TSet.removeIf(string, (key) => key === "aa", { discard: false })) + .type.toBe>>() + expect(pipe(string, TSet.removeIf((key) => key === "aa", { discard: false }))) + .type.toBe>>() + + expect(TSet.removeIf(string, (key) => key === "aa", { discard: true })) + .type.toBe>() + expect(pipe(string, TSet.removeIf((key) => key === "aa", { discard: true }))) + .type.toBe>() + }) + + it("retainIf", () => { + expect(TSet.retainIf(string, (key) => key === "aa")) + .type.toBe>>() + expect(pipe(string, TSet.retainIf((key) => key === "aa"))) + .type.toBe>>() + + expect(TSet.retainIf(string, (key) => key === "aa", { discard: false })) + .type.toBe>>() + expect(pipe(string, TSet.retainIf((key) => key === "aa", { discard: false }))) + .type.toBe>>() + + expect(TSet.retainIf(string, (key) => key === "aa", { discard: true })) + .type.toBe>() + expect(pipe(string, TSet.retainIf((key) => key === "aa", { discard: true }))) + .type.toBe>() + }) +}) diff --git a/repos/effect/packages/effect/dtslint/Tuple.tst.ts b/repos/effect/packages/effect/dtslint/Tuple.tst.ts new file mode 100644 index 0000000..8044cd4 --- /dev/null +++ b/repos/effect/packages/effect/dtslint/Tuple.tst.ts @@ -0,0 +1,113 @@ +import { hole, pipe } from "effect/Function" +import * as T from "effect/Tuple" +import { describe, expect, it } from "tstyche" + +declare const string$number: [string, number] +declare const readonlyString$number: readonly [string, number] +declare const arrayOfNumbers: Array + +describe("Tuple", () => { + it("make", () => { + expect(T.make("a", 1, true)) + .type.toBe<[string, number, boolean]>() + }) + + it("appendElement", () => { + expect(T.appendElement(T.make("a", 1), true)) + .type.toBe<[string, number, boolean]>() + expect(pipe(T.make("a", 1), T.appendElement(true))) + .type.toBe<[string, number, boolean]>() + }) + + describe("at", () => { + it("should return undefined for an empty tuple", () => { + expect(T.at(hole<[]>(), 0)) + .type.toBe() + expect(pipe(hole<[]>(), T.at(0))) + .type.toBe() + + expect(T.at(hole(), 0)) + .type.toBe() + expect(pipe(hole(), T.at(0))) + .type.toBe() + }) + + it("should return the first element for [string, number]", () => { + expect(T.at(string$number, 0)) + .type.toBe() + expect(pipe(string$number, T.at(0))) + .type.toBe() + + expect(T.at(readonlyString$number, 0)) + .type.toBe() + expect(pipe(readonlyString$number, T.at(0))) + .type.toBe() + }) + + it("should return the second element for [string, number]", () => { + expect(T.at(string$number, 1)) + .type.toBe() + expect(pipe(string$number, T.at(1))) + .type.toBe() + + expect(T.at(readonlyString$number, 1)) + .type.toBe() + expect(pipe(readonlyString$number, T.at(1))) + .type.toBe() + }) + + it("should return undefined for an out-of-bound index", () => { + expect(T.at(string$number, 2)) + .type.toBe() + expect(pipe(string$number, T.at(2))) + .type.toBe() + + expect(T.at(readonlyString$number, 2)) + .type.toBe() + expect(pipe(readonlyString$number, T.at(2))) + .type.toBe() + }) + + it("should return string | number for a negative index", () => { + expect(T.at(string$number, -1)) + .type.toBe() + expect(pipe(string$number, T.at(-1))) + .type.toBe() + + expect(T.at(readonlyString$number, -1)) + .type.toBe() + expect(pipe(readonlyString$number, T.at(-1))) + .type.toBe() + }) + + it("should work with arrays", () => { + expect(T.at(arrayOfNumbers, 1)) + .type.toBe() + expect(pipe(arrayOfNumbers, T.at(1))) + .type.toBe() + + expect(T.at(arrayOfNumbers, -1)) + .type.toBe() + expect(pipe(arrayOfNumbers, T.at(-1))) + .type.toBe() + }) + }) + + it("map", () => { + expect(pipe( + T.make("a", 1), + T.appendElement(true), + T.map((x) => { + expect(x).type.toBe() + return false as const + }) + )) + .type.toBe<[false, false, false]>() + + expect(T.map(["a", 1, false], (x) => { + expect(x).type.toBe() + return false as const + })) + .type.toBe<[false, false, false]>() + }) +}) diff --git a/repos/effect/packages/effect/dtslint/Types.tst.ts b/repos/effect/packages/effect/dtslint/Types.tst.ts new file mode 100644 index 0000000..2b277b9 --- /dev/null +++ b/repos/effect/packages/effect/dtslint/Types.tst.ts @@ -0,0 +1,320 @@ +import type { Brand, Types } from "effect" +import { describe, expect, it } from "tstyche" + +describe("Types", () => { + it("TupleOf", () => { + expect>() + .type.toBe() + expect>() + .type.toBe<[]>() + expect>() + .type.toBe<[number, number, number]>() + }) + + it("TupleOfAtLeast", () => { + expect>() + .type.toBe<[number, number, number, ...Array]>() + }) + + it("UnionToIntersection", () => { + expect>() + .type.toBe<{ a: string } & { b: number }>() + }) + + it("Tags", () => { + expect & unknown>() + .type.toBe<"a" | "b">() + }) + + it("ExcludeTag", () => { + expect & unknown>() + .type.toBe() + }) + + it("ExtractTag", () => { + expect & unknown>() + .type.toBe<{ _tag: "b"; b: number }>() + }) + + it("Simplify", () => { + expect>() + .type.toBe<{ a: number; b: number }>() + }) + + describe("Equals", () => { + it("should return true for identical types", () => { + expect>() + .type.toBe() + }) + it("should return false for different types", () => { + expect>() + .type.toBe() + }) + }) + + describe("MergeRight", () => { + it("mutable overwrites mutable", () => { + expect>() + .type.toBe<{ a: string; c: boolean; b: number }>() + }) + + it("mutable overwrites readonly", () => { + expect>() + .type.toBe<{ a: string; c: boolean; b: number }>() + }) + + it("readonly overwrites mutable", () => { + expect>() + .type.toBe<{ readonly a: string; c: boolean; b: number }>() + }) + + it("required overwrites optional", () => { + expect>() + .type.toBe<{ a: string; c: boolean; b: number }>() + }) + + it("optional overwrites optional", () => { + expect>() + .type.toBe<{ a?: string; c: boolean; b: number }>() + }) + + it("optional overwrites required", () => { + expect>() + .type.toBe<{ a?: string; c: boolean; b: number }>() + }) + + it("readonly optional overwrites mutable required", () => { + expect>() + .type.toBe<{ readonly a?: string; c: boolean; b: number }>() + }) + + it("mutable required overwrites readonly optional", () => { + expect>() + .type.toBe<{ a: string; c: boolean; b: number }>() + }) + + it("optionality of non involved keys must be preserved", () => { + expect>() + .type.toBe<{ readonly c?: string; readonly a?: number; b: number }>() + }) + }) + + describe("Mutable", () => { + it("should convert a readonly object to mutable", () => { + expect>>() + .type.toBe<{ a: string; b: number }>() + }) + + it("should convert a ReadonlyArray to a mutable array", () => { + expect>>() + .type.toBe>() + }) + + it("should convert a readonly tuple to a mutable tuple", () => { + expect>() + .type.toBe<[string, number]>() + }) + + it("should convert a readonly record to a mutable record", () => { + expect>>() + .type.toBe<{ [x: string]: number }>() + }) + }) + + describe("DeepMutable", () => { + type TaggedValues = { + readonly _tag: string + readonly value: ReadonlyArray + } + + it("primitives and literals", () => { + expect< + [ + Types.DeepMutable, + Types.DeepMutable, + Types.DeepMutable, + Types.DeepMutable, + Types.DeepMutable, + Types.DeepMutable, + Types.DeepMutable, + Types.DeepMutable<"a">, + Types.DeepMutable<1>, + Types.DeepMutable + ] + >().type.toBe<[string, number, boolean, bigint, symbol, never, null, "a", 1, true]>() + }) + + it("functions", () => { + expect 2>>().type.toBe<(arg: 1) => 2>() + }) + + it("built in objects", () => { + expect< + [ + Types.DeepMutable, + Types.DeepMutable, + Types.DeepMutable, + Types.DeepMutable, + Types.DeepMutable, + Types.DeepMutable, + Types.DeepMutable, + Types.DeepMutable + ] + >().type.toBeAssignableTo<[String, Number, Boolean, BigInt, Symbol, Date, RegExp, Generator]>() + }) + + describe("Branded", () => { + it("should leave a string brand unchanged", () => { + type T = string & Brand.Brand<"mybrand"> + expect>().type.toBe() + }) + + it("should leave a number brand unchanged", () => { + type T = number & Brand.Brand<"mybrand"> + expect>().type.toBe() + }) + + it("should leave a boolean brand unchanged", () => { + type T = boolean & Brand.Brand<"mybrand"> + expect>().type.toBe() + }) + + it("should leave a bigint brand unchanged", () => { + type T = bigint & Brand.Brand<"mybrand"> + expect>().type.toBe() + }) + + it("should leave a symbol brand unchanged", () => { + type T = symbol & Brand.Brand<"mybrand"> + expect>().type.toBe() + }) + }) + + describe("Index Signature", () => { + it("should convert an readonly Index Signature to a mutable Index Signature", () => { + expect>() + .type.toBe<{ [x: string]: number }>() + }) + + it("should leave an Index Signature unchanged", () => { + expect>() + .type.toBe<{ [x: string]: number }>() + }) + }) + + describe("Struct", () => { + it("should support an empty object", () => { + expect>() + .type.toBe<{}>() + }) + + it("should deeply mutate nested structs", () => { + expect>>>>>() + .type.toBe< + Array<{ + _tag: string + value: Array<{ + _tag: string + value: Array<{ + _tag: string + value: Array + }> + }> + }> + >() + }) + }) + + describe("Array", () => { + it("should convert a readonly empty array to a mutable empty array", () => { + expect>() + .type.toBe<[]>() + }) + + it("should leave a mutable empty array unchanged", () => { + expect>() + .type.toBe<[]>() + }) + + it("should convert a readonly array to a mutable array", () => { + expect>>() + .type.toBe>() + }) + + it("should leave a mutable array unchanged", () => { + expect>>() + .type.toBe>() + }) + }) + + describe("Tuple", () => { + it("should convert a readonly tuple", () => { + expect>() + .type.toBe<[string, number, boolean]>() + }) + + it("should leave a mutable tuple unchanged", () => { + expect>() + .type.toBe<[string, number, boolean]>() + }) + }) + + describe("ReadonlySet", () => { + it("should convert a ReadonlySet to a mutable Set", () => { + expect }>>>() + .type.toBe } }>>() + }) + + it("should leave a mutable Set unchanged", () => { + expect }>>>() + .type.toBe } }>>() + }) + }) + + describe("ReadonlyMap", () => { + it("should convert a ReadonlyMap to a mutable Map", () => { + expect, ReadonlySet>>>>() + .type.toBe }, Set<{ _tag: string; value: Array }>>>() + }) + + it("should leave a mutable Map unchanged", () => { + expect, ReadonlySet>>>>() + .type.toBe }, Set<{ _tag: string; value: Array }>>>() + }) + }) + + describe("Union", () => { + it("should convert a readonly union to a mutable union", () => { + type T = + | ReadonlySet<{ readonly value: TaggedValues }> + | ReadonlyMap, ReadonlySet>> + expect>() + .type.toBe< + | Set<{ value: { _tag: string; value: Array } }> + | Map<{ _tag: string; value: Array }, Set<{ _tag: string; value: Array }>> + >() + }) + + it("should leave a mutable union unchanged", () => { + type T = + | ReadonlySet<{ readonly value: TaggedValues }> + | ReadonlyMap, ReadonlySet>> + expect>() + .type.toBe< + | Set<{ value: { _tag: string; value: Array } }> + | Map<{ _tag: string; value: Array }, Set<{ _tag: string; value: Array }>> + >() + }) + }) + }) + + describe("MatchRecord", () => { + it("should yield 1 when matching a record type", () => { + expect>().type.toBe<1>() + }) + + it("should yield 0 when not matching a record type", () => { + expect>().type.toBe<0>() + }) + }) +}) diff --git a/repos/effect/packages/effect/dtslint/Unify.tst.ts b/repos/effect/packages/effect/dtslint/Unify.tst.ts new file mode 100644 index 0000000..da027cf --- /dev/null +++ b/repos/effect/packages/effect/dtslint/Unify.tst.ts @@ -0,0 +1,295 @@ +import type { + Context, + Deferred, + Effect, + Exit, + Fiber, + FiberRef, + ManagedRuntime, + Micro, + Option, + Pool, + Queue, + RcRef, + Ref, + Resource, + ScopedRef, + STM, + Stream, + SubscriptionRef, + SynchronizedRef +} from "effect" +import { Either, Unify } from "effect" +import { describe, expect, it } from "tstyche" + +describe("Unify", () => { + describe("Unify", () => { + it("should unify Context types", () => { + expect | Context.Tag<"a", "b">>>() + .type.toBe | Context.Tag<"a", "b">>() + }) + + it("should unify Option types", () => { + expect | Option.Option>>() + .type.toBe>() + }) + + it("should unify Either types", () => { + expect | Either.Either<"RB", "LB">>>() + .type.toBe>() + }) + + it("should unify a mixed union of Either, Option, and primitive value", () => { + expect< + Unify.Unify< + | Either.Either<"RA", "LA"> + | Either.Either<"RB", "LB"> + | Option.Option + | Option.Option + | 0 + > + >().type.toBe<0 | Option.Option | Either.Either<"RA" | "RB", "LA" | "LB">>() + }) + + it("should unify a record type", () => { + expect>() + .type.toBe<{ [k: string]: string }>() + }) + + it("should unify Stream types", () => { + expect | Stream.Stream<"a", "b", "c">>>() + .type.toBe>() + }) + + it("should unify Micro types", () => { + expect | Micro.Micro<"a", "b", "c">>>() + .type.toBe>() + }) + + it("should unify Effect types", () => { + expect< + Unify.Unify< + | Effect.Effect<0, 1, 2> + | Effect.Effect<"a", "b", "c"> + > + >().type.toBe>() + }) + + it("should unify STM types", () => { + expect< + Unify.Unify< + | STM.STM<0, 1, 2> + | STM.STM<"a", "b", "c"> + > + >().type.toBe>() + }) + + it("should unify Exit types", () => { + expect | Exit.Exit<"a", "b">>>() + .type.toBe>() + }) + + it("should unify Ref types", () => { + expect | Ref.Ref<"a">>>() + .type.toBe | Ref.Ref<"a">>() + }) + + it("should unify SynchronizedRef types", () => { + expect< + Unify.Unify< + | SynchronizedRef.SynchronizedRef<1> + | SynchronizedRef.SynchronizedRef<"a"> + > + >() + .type.toBe | SynchronizedRef.SynchronizedRef<"a">>() + }) + + it("should unify SubscriptionRef types", () => { + expect< + Unify.Unify< + | SubscriptionRef.SubscriptionRef<1> + | SubscriptionRef.SubscriptionRef<"a"> + > + >() + .type.toBe | SubscriptionRef.SubscriptionRef<"a">>() + }) + + it("should unify RcRef types", () => { + expect | RcRef.RcRef<"a", "b">>>() + .type.toBe>() + }) + + it("should unify Deferred types", () => { + expect | Deferred.Deferred<"a", "b">>>() + .type.toBe | Deferred.Deferred<"a", "b">>() + }) + + it("should unify FiberRef types", () => { + expect | FiberRef.FiberRef<"a">>>() + .type.toBe | FiberRef.FiberRef<"a">>() + }) + + it("should unify Fiber types", () => { + expect | Fiber.Fiber<"a", "b">>>() + .type.toBe>() + }) + + it("should unify RuntimeFiber types", () => { + expect | Fiber.RuntimeFiber<"a", "b">>>() + .type.toBe>() + }) + + it("should unify ManagedRuntime types", () => { + expect< + Unify.Unify< + | ManagedRuntime.ManagedRuntime<1, 2> + | ManagedRuntime.ManagedRuntime<"a", "b"> + > + >().type.toBe | ManagedRuntime.ManagedRuntime<"a", "b">>() + }) + + it("should unify Queue types", () => { + expect | Queue.Queue<"a">>>() + .type.toBe | Queue.Queue<"a">>() + }) + + it("should unify Dequeue types", () => { + expect | Queue.Dequeue<"a">>>() + .type.toBe>() + }) + + it("should unify Pool types", () => { + expect< + Unify.Unify< + | Pool.Pool<1, 2> + | Pool.Pool<"a", "b"> + | Pool.Pool<"a", "c"> + > + >() + .type.toBe | Pool.Pool<"a", "b" | "c">>() + }) + + it("should unify ScopedRef types", () => { + expect | ScopedRef.ScopedRef<"a">>>() + .type.toBe | ScopedRef.ScopedRef<"a">>() + }) + + it("should unify Resource types", () => { + expect< + Unify.Unify< + | Resource.Resource<1> + | Resource.Resource + | Resource.Resource<1, 2> + | Resource.Resource<"a", "b"> + | Resource.Resource + > + >() + .type.toBe< + | Resource.Resource<1, never> + | Resource.Resource + | Resource.Resource<1, 2> + | Resource.Resource<"a", "b"> + | Resource.Resource + >() + }) + + it("should unify a huge union", () => { + expect< + Unify.Unify< + | Context.Tag<0, 1> + | Context.Tag<"a", "b"> + | Either.Either<1, 0> + | Either.Either<"a", "b"> + | Option.Option + | Option.Option + | Effect.Effect<"a", "b", "R"> + | Effect.Effect<1, 0, "R1"> + | STM.STM<0, 1, 2> + | STM.STM<"a", "b", "c"> + | Ref.Ref<1> + | Ref.Ref<"a"> + | SynchronizedRef.SynchronizedRef<1> + | SynchronizedRef.SynchronizedRef<"a"> + | SubscriptionRef.SubscriptionRef<1> + | SubscriptionRef.SubscriptionRef<"a"> + | RcRef.RcRef<1, 0> + | RcRef.RcRef<"a", "b"> + | Deferred.Deferred<1, 0> + | Deferred.Deferred<"a", "b"> + | FiberRef.FiberRef<1> + | FiberRef.FiberRef<"a"> + | Fiber.Fiber<1, 0> + | Fiber.Fiber<"a", "b"> + | Fiber.RuntimeFiber<1, 0> + | Fiber.RuntimeFiber<"a", "b"> + | Queue.Queue<1> + | Queue.Queue<"a"> + | Queue.Dequeue<1> + | Queue.Dequeue<"a"> + | Pool.Pool<1, 2> + | Pool.Pool<"a", "b"> + | Pool.Pool<"a", "c"> + | ScopedRef.ScopedRef<1> + | ScopedRef.ScopedRef<"a"> + | Resource.Resource<1, 0> + | Resource.Resource<"a", "b"> + | Effect.Latch + | ManagedRuntime.ManagedRuntime<1, 0> + | ManagedRuntime.ManagedRuntime<"a", "b"> + | 0 + > + >() + .type.toBe< + | Context.Tag<0, 1> + | Context.Tag<"a", "b"> + | 0 + | Option.Option + | STM.STM<0 | "a", "b" | 1, "c" | 2> + | Ref.Ref<1> + | Ref.Ref<"a"> + | SynchronizedRef.SynchronizedRef<1> + | SynchronizedRef.SynchronizedRef<"a"> + | SubscriptionRef.SubscriptionRef<1> + | SubscriptionRef.SubscriptionRef<"a"> + | Deferred.Deferred<"a", "b"> + | FiberRef.FiberRef<1> + | FiberRef.FiberRef<"a"> + | ManagedRuntime.ManagedRuntime<"a", "b"> + | Queue.Queue<1> + | Queue.Queue<"a"> + | Queue.Dequeue<"a" | 1> + | Pool.Pool<1, 2> + | Pool.Pool<"a", "b" | "c"> + | ScopedRef.ScopedRef<1> + | ScopedRef.ScopedRef<"a"> + | Resource.Resource<"a", "b"> + | Deferred.Deferred<1, 0> + | Resource.Resource<1, 0> + | Effect.Latch + | ManagedRuntime.ManagedRuntime<1, 0> + | RcRef.RcRef<"a" | 1, 0 | "b"> + | Fiber.Fiber<"a" | 1, 0 | "b"> + | Fiber.RuntimeFiber<"a" | 1, 0 | "b"> + | Either.Either<"a" | 1, 0 | "b"> + | Effect.Effect<"a" | 1, 0 | "b", "R" | "R1"> + >() + }) + }) + + describe("unify", () => { + it("should infer the type of Unify.unify for a function", () => { + function f(n: N) { + return Math.random() > 0 ? Either.right(n) : Either.left("ok") + } + type Expected = (n: N) => Either.Either + expect(Unify.unify(f)) + .type.toBe() + }) + + it("should unify a value using Unify.unify", () => { + expect( + Unify.unify(Math.random() > 0 ? Either.right(10) : Either.left("ok")) + ).type.toBe>() + }) + }) +}) diff --git a/repos/effect/packages/effect/dtslint/tsconfig.json b/repos/effect/packages/effect/dtslint/tsconfig.json new file mode 100644 index 0000000..4d1caf1 --- /dev/null +++ b/repos/effect/packages/effect/dtslint/tsconfig.json @@ -0,0 +1,18 @@ +{ + "extends": "../../../tsconfig.base.json", + "include": ["."], + "references": [ + { "path": "../tsconfig.src.json" }, + ], + "compilerOptions": { + "incremental": false, + "composite": false, + "noUnusedLocals": false, + "plugins": [ + { + "name": "@effect/language-server", + "diagnostics": false + } + ] + } +} diff --git a/repos/effect/packages/effect/package.json b/repos/effect/packages/effect/package.json new file mode 100644 index 0000000..4591514 --- /dev/null +++ b/repos/effect/packages/effect/package.json @@ -0,0 +1,61 @@ +{ + "name": "effect", + "version": "3.21.2", + "type": "module", + "license": "MIT", + "description": "The missing standard library for TypeScript, for writing production-grade software.", + "homepage": "https://effect.website", + "repository": { + "type": "git", + "url": "https://github.com/Effect-TS/effect.git", + "directory": "packages/effect" + }, + "bugs": { + "url": "https://github.com/Effect-TS/effect/issues" + }, + "tags": [ + "typescript", + "error-handling", + "concurrency", + "observability" + ], + "keywords": [ + "typescript", + "error-handling", + "concurrency", + "observability" + ], + "publishConfig": { + "access": "public", + "provenance": true, + "directory": "dist", + "linkDirectory": false + }, + "exports": { + "./package.json": "./package.json", + ".": "./src/index.ts", + "./*": "./src/*.ts", + "./internal/*": null + }, + "scripts": { + "codegen": "build-utils prepare-v3", + "build": "pnpm build-esm && pnpm build-annotate && pnpm build-cjs && build-utils pack-v3", + "build-esm": "tsc -b tsconfig.build.json", + "build-cjs": "babel build/esm --plugins @babel/transform-export-namespace-from --plugins @babel/transform-modules-commonjs --out-dir build/cjs --source-maps", + "build-annotate": "babel build/esm --plugins annotate-pure-calls --out-dir build/esm --source-maps", + "test-types": "tstyche", + "check": "tsc -b tsconfig.json", + "test": "vitest", + "coverage": "vitest --coverage" + }, + "devDependencies": { + "ajv": "^8.17.1", + "ast-types": "^0.14.2", + "tinybench": "^4.0.1", + "zod": "^3.24.4" + }, + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "fast-check": "^3.23.1" + } +} diff --git a/repos/effect/packages/effect/schema-vs-zod.md b/repos/effect/packages/effect/schema-vs-zod.md new file mode 100644 index 0000000..21dee5c --- /dev/null +++ b/repos/effect/packages/effect/schema-vs-zod.md @@ -0,0 +1,1462 @@ +# Schema vs. Zod: Key Differences and Features + +`effect/Schema` provides similar functionality to `zod` (v3), with additional features and key differences that may suit specific use cases. Below is a summary of the main distinctions: + +1. **Bidirectional Transformations** + `effect/Schema` supports both decoding (transforming raw data into validated data) and encoding (transforming validated data back into a format for external use). This makes it suitable for scenarios where data needs to be sent or received over a network. In contrast, `zod` focuses primarily on decoding. + +2. **Integration with `effect`** + `effect/Schema` is designed to integrate with the `effect` library, leveraging features such as dependency tracking during transformations. This integration allows developers to incorporate schemas directly into `effect` workflows. + +3. **Customizable Through Annotations** + Annotations in `effect/Schema` provide a way to attach metadata to schemas. This can include custom error messages, fallback values, or any other additional information to enhance schema behavior. Annotations offer a structured approach to schema customization that goes beyond basic validations. + +4. **Functional Programming Style** + `effect/Schema` uses a style based on combinators and transformations. This approach provides greater flexibility when composing schemas and enables better tree shaking for optimized bundle sizes. On the other hand, `zod` uses a chainable API for defining schemas. + +## Parse, don't validate + +`effect/Schema` adheres to the principle of [parse, don't validate](https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-validate/). This means that schemas are designed to parse input data into a validated and usable form, rather than simply checking if the data matches a set of rules. For example, instead of validating that a string conforms to a URL format (like `zod`'s `z.string().url()`), `effect/Schema` provides a schema to parse a string directly into a `URL` object. + +The distinction between parsing and validating lies in the outcome: + +- Validation checks if data satisfies a set of rules but does not modify or transform it. +- Parsing not only checks validity but also converts the data into a desired format or type. + +For instance, `Schema.URL` transforms a string into a `URL` object, enabling direct use in your code without additional processing. + +**Example** (Parsing URL strings into `URL` objects) + +```ts +import { Schema } from "effect" + +// ┌─── The output type +// │ ┌─── The input type +// ▼ ▼ +// ┌─── Schema +// ▼ +const schema = Schema.URL + +// Parse a valid URL string +console.log(Schema.decodeUnknownSync(Schema.URL)("https://example.com")) +// Output: URL { href: 'https://example.com/', ... } (instance of URL) + +// Attempt to parse an invalid URL +console.log(Schema.decodeUnknownSync(Schema.URL)("example.com")) +/* +throws: +ParseError: URL +└─ Transformation process failure + └─ Unable to decode "example.com" into a URL. Invalid URL +*/ +``` + +## Basic usage + +Here are a couple of examples to introduce the basic usage of `zod` and `effect/Schema`. + +While both libraries provide similar parsing features, `effect/Schema` uses [Either](https://effect.website/docs/data-types/either/) for safe parsing. The result is either `Either.right` (on success) or `Either.left` (on failure). In contrast, `zod` returns an object with `success` and `error` fields for safe parsing. + +**Example** (Creating and using a schema for strings) + +Zod + +```ts +import { z } from "zod" + +// creating a schema for strings +const mySchema = z.string() + +// parsing +mySchema.parse("tuna") // => "tuna" +mySchema.parse(12) // => throws ZodError + +// "safe" parsing (doesn't throw error if validation fails) +mySchema.safeParse("tuna") // => { success: true; data: "tuna" } +mySchema.safeParse(12) // => { success: false; error: ZodError } +``` + +Schema + +```ts +import { Schema } from "effect" + +// creating a schema for strings +const mySchema = Schema.String + +// parsing +Schema.decodeUnknownSync(mySchema)("tuna") // => "tuna" +Schema.decodeUnknownSync(mySchema)(12) // => throws ParseError + +// "safe" parsing (doesn't throw error if validation fails) +Schema.decodeUnknownEither(mySchema)("tuna") // => Either.right("tuna") +Schema.decodeUnknownEither(mySchema)(12) // => Either.left(ParseError) +``` + +**Example** (Creating and using a schema for objects) + +Zod + +```ts +import { z } from "zod" + +const User = z.object({ + username: z.string() +}) + +User.parse({ username: "Ludwig" }) + +// extract the inferred type +type User = z.infer +// { username: string } +``` + +Schema + +```ts +import { Schema } from "effect" + +const User = Schema.Struct({ + username: Schema.String +}) + +Schema.decodeUnknownSync(User)({ username: "Ludwig" }) + +// extract the inferred type +type User = typeof User.Type +// { readonly username: string } +``` + +## Naming Conventions + +The naming conventions in `effect/Schema` are designed to be straightforward and logical, **focusing primarily on compatibility with JSON serialization**. This approach simplifies the understanding and use of schemas, especially for developers who are integrating web technologies where JSON is a standard data interchange format. + +### Overview of Naming Strategies + +**JSON-Compatible Types** + +Schemas that naturally serialize to JSON-compatible formats are named directly after their data types. + +For instance: + +- `Schema.Date`: serializes JavaScript Date objects to ISO-formatted strings, a typical method for representing dates in JSON. +- `Schema.Number`: used directly as it maps precisely to the JSON number type, requiring no special transformation to remain JSON-compatible. + +**Non-JSON-Compatible Types** + +When dealing with types that do not have a direct representation in JSON, the naming strategy incorporates additional details to indicate the necessary transformation. This helps in setting clear expectations about the schema's behavior: + +For instance: + +- `Schema.DateFromSelf`: indicates that the schema handles `Date` objects, which are not natively JSON-serializable. +- `Schema.NumberFromString`: this naming suggests that the schema processes numbers that are initially represented as strings, emphasizing the transformation from string to number when decoding. + +The primary goal of these schemas is to ensure that domain objects can be easily serialized ("encoded") and deserialized ("decoded") for transmission over network connections, thus facilitating their transfer between different parts of an application or across different applications. + +### Rationale + +While JSON's ubiquity justifies its primary consideration in naming, the conventions also accommodate serialization for other types of transport. For instance, converting a `Date` to a string is a universally useful method for various communication protocols, not just JSON. Thus, the selected naming conventions serve as sensible defaults that prioritize clarity and ease of use, facilitating the serialization and deserialization processes across diverse technological environments. + +## Primitives + +| Feature | Zod | Schema | +| -------- | ------------- | ----------------------- | +| Strings | `z.string()` | `Schema.String` | +| Numbers | `z.number()` | `Schema.Number` | +| BigInts | `z.bigint()` | `Schema.BigIntFromSelf` | +| Booleans | `z.boolean()` | `Schema.Boolean` | +| Dates | `z.date()` | `Schema.DateFromSelf` | +| Symbols | `z.symbol()` | `Schema.SymbolFromSelf` | + +**Empty Types** + +| Feature | Zod | Schema | +| --------- | --------------- | ------------------ | +| Undefined | `z.undefined()` | `Schema.Undefined` | +| Null | `z.null()` | `Schema.Null` | +| Void | `z.void()` | `Schema.Void` | + +**Catch-All Types** + +| Feature | Zod | Schema | +| ------- | ------------- | ---------------- | +| Any | `z.any()` | `Schema.Any` | +| Unknown | `z.unknown()` | `Schema.Unknown` | + +**Never Type** + +| Feature | Zod | Schema | +| ------- | ----------- | -------------- | +| Never | `z.never()` | `Schema.Never` | + +## Coercion for primitives + +No direct equivalent in `effect/Schema`. + +## Literals + +| Feature | Zod | Schema | Differences | +| ---------------- | -------------------------- | -------------------------------------------- | --------------------------------------------------------------------------------------------------- | +| String Literal | `z.literal("tuna")` | `Schema.Literal("tuna")` | | +| Number Literal | `z.literal(12)` | `Schema.Literal(12)` | | +| BigInt Literal | `z.literal(2n)` | `Schema.Literal(2n)` | | +| Boolean Literal | `z.literal(true)` | `Schema.Literal(true)` | | +| Unique Symbol | `z.literal(Symbol("foo"))` | `Schema.UniqueSymbolFromSelf(Symbol("foo"))` | Zod uses `z.literal`, while `effect/Schema` has a specific function for unique symbols. | +| Retrieving Value | `tuna.value // "tuna"` | `tuna.literals // ["tuna"]` | Zod uses `.value` for a single literal, while Schema returns an array of literals with `.literals`. | + +## Strings + +The following tables compare the string handling features in `zod` and `effect/Schema`. + +**String Validations** + +| Feature | zod | effect/Schema | Differences | +| ----------------------- | ------------------------------- | ----------------------------------------------- | ------------------------------------------------------------------------- | +| Max Length | `z.string().max(5)` | `Schema.String.pipe(Schema.maxLength(5))` | None | +| Min Length | `z.string().min(5)` | `Schema.String.pipe(Schema.minLength(5))` | None | +| Exact Length | `z.string().length(5)` | `Schema.String.pipe(Schema.length(5))` | None | +| Pattern Matching | `z.string().regex(regex)` | `Schema.String.pipe(Schema.pattern(regex))` | None | +| Includes Substring | `z.string().includes(string)` | `Schema.String.pipe(Schema.includes(string))` | None | +| Starts With | `z.string().startsWith(string)` | `Schema.String.pipe(Schema.startsWith(string))` | None | +| Ends With | `z.string().endsWith(string)` | `Schema.String.pipe(Schema.endsWith(string))` | None | +| UUID Validation | `z.string().uuid()` | `Schema.UUID` | None | +| ULID Validation | `z.string().ulid()` | `Schema.ULID` | None | +| Email Validation | `z.string().email()` | Not available | `zod` provides built-in email validation, while `effect/Schema` does not. | +| URL Validation | `z.string().url()` | Not available (see [URLs](#urls)) | `zod` supports URL validation, while `effect/Schema` does not. | +| Emoji Validation | `z.string().emoji()` | Not available | `zod` provides emoji validation, while `effect/Schema` does not. | +| Nano ID Validation | `z.string().nanoid()` | Not available | `zod` supports Nano ID validation, while `effect/Schema` does not. | +| CUID Validation | `z.string().cuid()` | Not available | `zod` supports CUID validation, while `effect/Schema` does not. | +| CUID2 Validation | `z.string().cuid2()` | Not available | `zod` supports CUID2 validation, while `effect/Schema` does not. | +| ISO Datetime Validation | `z.string().datetime()` | Not available (see [Datetimes](#datetimes)) | `zod` supports ISO datetime validation, while `effect/Schema` does not. | +| `YYYY-MM-DD` format | `z.string().date()` | Not available (see [Datetimes](#datetimes)) | `zod` supports ISO date validation, while `effect/Schema` does not. | +| ISO Time Validation | `z.string().time()` | Not available | `zod` supports ISO time validation, while `effect/Schema` does not. | +| ISO Duration Validation | `z.string().duration()` | Not available | `zod` supports ISO duration validation, while `effect/Schema` does not. | +| IP Address Validation | `z.string().ip()` | Not available | `zod` supports IP address validation, while `effect/Schema` does not. | +| Base64 Validation | `z.string().base64()` | Not available | `zod` supports base64 validation, while `effect/Schema` does not. | + +**String Transformations** + +| Feature | zod | effect/Schema | Differences | +| --------------- | -------------------------- | ------------------ | ---------------------------------------------- | +| Trim Whitespace | `z.string().trim()` | `Schema.Trim` | Syntax differs, but functionality is the same. | +| Lowercase | `z.string().toLowerCase()` | `Schema.Lowercase` | Syntax differs, but functionality is the same. | +| Uppercase | `z.string().toUpperCase()` | `Schema.Uppercase` | Syntax differs, but functionality is the same. | + +## Custom Error Messages + +| Feature | zod | effect/Schema | Differences | +| ---------------------------- | --------------------------------------------------------- | ------------------------------------------------------------------------------------- | ---------------------------------------------------- | +| Schema-Level Messages | `z.string({ required_error, invalid_type_error })` | `Schema.String.annotations({ message: () => "Custom error" })` | `effect/Schema` uses annotations for error messages. | +| Validation-Specific Messages | `z.string().min(5, { message: "Must be 5+ characters" })` | `Schema.String.pipe(Schema.minLength(5, { message: () => "Must be 5+ characters" }))` | | + +**Example** (Custom error messages for strings) + +You can customize some common error messages when creating a string schema. + +Zod + +```ts +const name = z.string({ + required_error: "Name is required", + invalid_type_error: "Name must be a string" +}) +``` + +Schema + +```ts +import { Schema } from "effect" + +const name = Schema.String.annotations({ + // No direct equivalent for required error + message: () => "Name must be a string" +}) +``` + +**Example** (Custom error messages for string length) + +When using validation methods, you can pass in an additional argument to provide a custom error message. + +Zod + +```ts +z.string().min(5, { message: "Must be 5 or more characters long" }) +``` + +Schema + +```ts +Schema.String.pipe( + Schema.minLength(5, { message: () => "Must be 5 or more characters long" }) +) +``` + +## URLs + +In `zod`, the `z.string().url()` method validates string URLs. In `effect/Schema`, there is no direct equivalent. However, you can use `Schema.URL` to parse string URLs into `URL` objects. + +**Example** (Parsing URL strings into `URL` objects) + +```ts +import { Schema } from "effect" + +// Parse a valid URL string +console.log(Schema.decodeUnknownSync(Schema.URL)("https://example.com")) +// Output: URL { href: 'https://example.com/', ... } + +// Attempt to parse an invalid URL +console.log(Schema.decodeUnknownSync(Schema.URL)("example.com")) +/* +throws: +ParseError: URL +└─ Transformation process failure + └─ Unable to decode "example.com" into a URL. Invalid URL +*/ +``` + +## Datetimes + +In `zod`, the `z.string().datetime()` method validates ISO 8601 datetime strings. In `effect/Schema`, there is no direct equivalent. However, you can use `Schema.Date`, which parses a string into a `Date` object using the `new Date()` constructor. + +**Example** (Parsing date strings into `Date` objects) + +```ts +import { Schema } from "effect" + +// Parse a valid ISO 8601 date string +console.log(Schema.decodeUnknownSync(Schema.Date)("2020-01-01")) +// Output: 2020-01-01T00:00:00.000Z (as Date object) + +// Parse a less strict date format +console.log(Schema.decodeUnknownSync(Schema.Date)("2020-1-1")) +// Output: 2019-12-31T23:00:00.000Z (as Date object) + +// Attempt to parse an invalid date +console.log(Schema.decodeUnknownSync(Schema.Date)("2020-01-32")) +/* +throws: +ParseError: Date +└─ Predicate refinement failure + └─ Expected a valid Date, actual Invalid Date +*/ +``` + +## Numbers + +The following tables provide a detailed comparison of number validations and custom error handling in `zod` and `effect/Schema`. + +**Number Validations** + +| Feature | zod | effect/Schema | Differences | +| ------------------------ | -------------------------- | ---------------------------------------------------- | ---------------------------------------------------------------------------- | +| Greater Than | `z.number().gt(5)` | `Schema.Number.pipe(Schema.greaterThan(5))` | None | +| Greater Than or Equal To | `z.number().gte(5)` | `Schema.Number.pipe(Schema.greaterThanOrEqualTo(5))` | None | +| Less Than | `z.number().lt(5)` | `Schema.Number.pipe(Schema.lessThan(5))` | None | +| Less Than or Equal To | `z.number().lte(5)` | `Schema.Number.pipe(Schema.lessThanOrEqualTo(5))` | None | +| Integer Validation | `z.number().int()` | `Schema.Number.pipe(Schema.int())` | None | +| Positive Numbers | `z.number().positive()` | `Schema.Number.pipe(Schema.positive())` | None | +| Non-Negative Numbers | `z.number().nonnegative()` | `Schema.Number.pipe(Schema.nonNegative())` | None | +| Negative Numbers | `z.number().negative()` | `Schema.Number.pipe(Schema.negative())` | None | +| Non-Positive Numbers | `z.number().nonpositive()` | `Schema.Number.pipe(Schema.nonPositive())` | None | +| Divisible by a Number | `z.number().multipleOf(5)` | `Schema.Number.pipe(Schema.multipleOf(5))` | None | +| Finite Numbers | `z.number().finite()` | `Schema.Number.pipe(Schema.finite())` | None | +| Safe Numbers | `z.number().safe()` | Not available | `zod` includes validation for safe integers, while `effect/Schema` does not. | + +**Custom Error Messages** + +| Feature | zod | effect/Schema | Differences | +| --------------------------- | ---------------------------------------------- | ---------------------------------------------------------------------------------- | ------------------------------------------------------- | +| Custom Error for Validation | `z.number().lte(5, { message: "my message" })` | `Schema.Number.pipe(Schema.lessThanOrEqualTo(5, { message: () => "my message" }))` | Syntax differs between chainable and functional styles. | + +**Example** (Custom error messages for numbers) + +Zod + +```ts +z.number().lte(5, { message: "my message" }) +``` + +Schema + +```ts +import { Schema } from "effect" + +Schema.Number.pipe(Schema.lessThanOrEqualTo(5, { message: () => "my message" })) +``` + +## BigInts + +| Feature | zod | effect/Schema | Differences | +| ------------------------ | --------------------------- | ----------------------------------------------------------- | ------------------------------------------------------------------------ | +| Greater Than | `z.bigint().gt(5n)` | `Schema.BigInt.pipe(Schema.greaterThanBigInt(5n))` | | +| Greater Than or Equal To | `z.bigint().gte(5n)` | `Schema.BigInt.pipe(Schema.greaterThanOrEqualToBigInt(5n))` | | +| Less Than | `z.bigint().lt(5n)` | `Schema.BigInt.pipe(Schema.lessThanBigInt(5n))` | | +| Less Than or Equal To | `z.bigint().lte(5n)` | `Schema.BigInt.pipe(Schema.lessThanOrEqualToBigInt(5n))` | | +| Positive | `z.bigint().positive()` | `Schema.BigInt.pipe(Schema.positiveBigInt())` | | +| Non-Negative | `z.bigint().nonnegative()` | `Schema.BigInt.pipe(Schema.nonNegativeBigInt())` | | +| Negative | `z.bigint().negative()` | `Schema.BigInt.pipe(Schema.negativeBigInt())` | | +| Non-Positive | `z.bigint().nonpositive()` | `Schema.BigInt.pipe(Schema.nonPositiveBigInt())` | | +| Multiple Of | `z.bigint().multipleOf(5n)` | Not available | `zod` supports `.multipleOf`, which is not available in `effect/Schema`. | + +## Zod enums + +The table below summarizes the differences between `zod` and `effect/Schema` for enums. + +| Feature | zod | effect/Schema | Differences | +| ----------------- | ------------------------------------------------- | ----------------------------------------------------------- | ---------------------------------------------------------------------- | +| Defining an Enum | `z.enum(["Salmon", "Tuna", "Trout"])` | `Schema.Literal("Salmon", "Tuna", "Trout")` | `zod` accepts an array, while `effect/Schema` uses variadic arguments. | +| Retrieving Values | `FishEnum.options // ["Salmon", "Tuna", "Trout"]` | `FishEnum.literals // readonly ["Salmon", "Tuna", "Trout"]` | `.options` vs `.literals`, with similar behavior. | + +**Example** (Creating an enum schema) + +```ts +const FishEnum = z.enum(["Salmon", "Tuna", "Trout"]) + +FishEnum.options // ["Salmon", "Tuna", "Trout"]; +``` + +Schema + +```ts +import { Schema } from "effect" + +const FishEnum = Schema.Literal("Salmon", "Tuna", "Trout") + +FishEnum.literals // readonly ["Salmon", "Tuna", "Trout"] +``` + +## Native enums + +Both `zod` and `effect/Schema` support working with native TypeScript `enum`s, enabling validation of enum values. + +| Feature | zod | effect/Schema | Differences | +| --------------------- | ---------------------- | ---------------------- | ----------- | +| Defining Native Enums | `z.nativeEnum(Fruits)` | `Schema.Enums(Fruits)` | | + +**Example** (Creating a schema for a native enum) + +Zod + +```ts +enum Fruits { + Apple, + Banana +} + +const FruitEnum = z.nativeEnum(Fruits) + +type FruitEnum = z.infer // Fruits + +FruitEnum.parse(Fruits.Apple) // passes +FruitEnum.parse(Fruits.Banana) // passes +FruitEnum.parse(0) // passes +FruitEnum.parse(1) // passes +FruitEnum.parse(3) // fails +``` + +Schema + +```ts +import { Schema } from "effect" + +enum Fruits { + Apple, + Banana +} + +const FruitEnum = Schema.Enums(Fruits) + +type FruitEnum = typeof FruitEnum.Type // Fruits + +Schema.decodeUnknownSync(FruitEnum)(Fruits.Apple) // passes +Schema.decodeUnknownSync(FruitEnum)(Fruits.Banana) // passes +Schema.decodeUnknownSync(FruitEnum)(0) // passes +Schema.decodeUnknownSync(FruitEnum)(1) // passes +Schema.decodeUnknownSync(FruitEnum)(3) // fails +``` + +## Optionals + +In both `zod` and `effect/Schema`, you can mark a field as optional, indicating that the property may or may not be present in the object. + +**Example** (Defining an optional field) + +Zod + +```ts +const user = z.object({ + username: z.string().optional() +}) + +type Type = z.infer // { username?: string | undefined }; +``` + +Schema + +```ts +import { Schema } from "effect" + +const user = Schema.Struct({ + username: Schema.optional(Schema.String) +}) + +type Type = typeof user.Type // { readonly username?: string | undefined }; +``` + +## Nullables + +Both `zod` and `effect/Schema` allow you to define nullable fields, meaning a value can either have the specified type or be `null`. + +**Example** (Defining a nullable string) + +Zod + +```ts +const nullableString = z.nullable(z.string()) + +nullableString.parse("asdf") // => "asdf" +nullableString.parse(null) // => null +``` + +Schema + +```ts +import { Schema } from "effect" + +const nullableString = Schema.NullOr(Schema.String) + +Schema.decodeUnknownSync(nullableString)("asdf") // => "asdf" +Schema.decodeUnknownSync(nullableString)(null) // => null +``` + +## Objects + +Both `zod` and `effect/Schema` support object schemas where all properties are required by default. + +**Example** (Defining and inferring types of an object schema) + +Zod + +```ts +// all properties are required by default +const Dog = z.object({ + name: z.string(), + age: z.number() +}) + +// extract the inferred type like this +type Dog = z.infer + +// equivalent to: +type Dog = { + name: string + age: number +} +``` + +Schema + +```ts +import { Schema } from "effect" + +// all properties are required by default +const Dog = Schema.Struct({ + name: Schema.String, + age: Schema.Number +}) + +// extract the inferred type like this +type Dog = typeof Dog.Type + +// equivalent to: +type Dog = { + readonly name: string + readonly age: number +} +``` + +### shape + +Both libraries allow access to the individual schemas of object fields. + +**Example** (Accessing object field schemas) + +Zod + +```ts +Dog.shape.name // => string schema +Dog.shape.age // => number schema +``` + +Schema + +```ts +Dog.fields.name // => String schema +Dog.fields.age // => Number schema +``` + +### keyof + +Both libraries allow extracting the keys of an object schema as a new schema. + +**Example** (Creating a schema of object keys) + +Zod + +```ts +const keySchema = Dog.keyof() +keySchema // ZodEnum<["name", "age"]> +``` + +Schema + +```ts +// ┌─── Schema<"name" | "age", "name" | "age", never> +// ▼ +const keySchema = Schema.keyof(Dog) +``` + +### extend + +Objects can be extended to include additional properties. + +**Example** (Extending an object schema) + +Zod + +```ts +const DogWithBreed = Dog.extend({ + breed: z.string() +}) +``` + +Schema + +```ts +const DogWithBreed = Dog.pipe( + Schema.extend( + Schema.Struct({ + breed: Schema.String + }) + ) +) + +// Recommended alternative when working with structs +const DogWithBreed = Schema.Struct({ + ...Dog.fields, + breed: Schema.String +}) +``` + +### pick / omit + +Fields can be selected or removed from an object schema. + +**Example** (Selecting or omitting fields) + +Zod + +```ts +const Recipe = z.object({ + id: z.string(), + name: z.string(), + ingredients: z.array(z.string()) +}) + +const JustTheName = Recipe.pick({ name: true }) + +const NoIDRecipe = Recipe.omit({ id: true }) +``` + +Schema + +```ts +import { Schema } from "effect" + +const Recipe = Schema.Struct({ + id: Schema.String, + name: Schema.String, + ingredients: Schema.Array(Schema.String) +}) + +const JustTheName = Recipe.pick("name") + +const NoIDRecipe = Recipe.omit("id") +``` + +### partial + +All fields in an object schema can be made optional. + +**Example** (Making all fields optional) + +Zod + +```ts +const user = z.object({ + email: z.string(), + username: z.string() +}) + +const partialUser = user.partial() +``` + +Schema + +```ts +import { Schema } from "effect" + +const user = Schema.Struct({ + email: Schema.String, + username: Schema.String +}) + +const partialUser = Schema.partial(user) +``` + +### deepPartial + +There is no direct equivalent for deeply making all fields optional in `effect/Schema`. + +### required + +Both `zod` and `effect/Schema` allow you to make all fields in an object schema required. + +**Example** (Converting optional fields to required) + +Zod + +```ts +const user = z + .object({ + email: z.string(), + username: z.string() + }) + .partial() // Makes all fields optional + +const requiredUser = user.required() // Converts all fields back to required +``` + +Schema + +```ts +import { Schema } from "effect" + +const user = Schema.Struct({ + email: Schema.String, + username: Schema.String +}).pipe(Schema.partial) // Makes all fields optional + +const requiredUser = Schema.required(user) // Converts all fields back to required +``` + +### passthrough + +Both `zod` and `effect/Schema` provide mechanisms to handle additional properties that are not explicitly defined in an object schema. By default, both libraries ignore or strip these extra properties, but they also allow configurations to preserve them. + +- In `zod`, passthrough behavior is enabled using the `.passthrough()` method. +- In `effect/Schema`, passthrough is achieved by setting the `onExcessProperty` option to `"preserve"` during decoding. + +**Example** (Handling additional properties) + +Zod + +```ts +const person = z.object({ + name: z.string() +}) + +person.parse({ + name: "bob dylan", + extraKey: 61 +}) +// => { name: "bob dylan" } +// extraKey has been stripped + +person.passthrough().parse({ + name: "bob dylan", + extraKey: 61 +}) +// => { name: "bob dylan", extraKey: 61 } +``` + +Schema + +```ts +import { Schema } from "effect" + +const person = Schema.Struct({ + name: Schema.String +}) + +Schema.decodeUnknownSync(person)( + { + name: "bob dylan", + extraKey: 61 + }, + { onExcessProperty: "preserve" } +) +// => { name: "bob dylan", extraKey: 61 } +``` + +### strict + +Both `zod` and `effect/Schema` offer a way to enforce strict object schemas, meaning that any additional properties not defined in the schema will result in an error. + +- In `zod`, strict mode is enabled using the `.strict()` method when defining an object schema. +- In `effect/Schema`, strict behavior is configured during decoding by setting the `onExcessProperty` option to `"error"`. + +**Example** (Enforcing strict object validation) + +Zod + +```ts +const person = z + .object({ + name: z.string() + }) + .strict() + +person.parse({ + name: "bob dylan", + extraKey: 61 +}) +// => throws ZodError +``` + +Schema + +```ts +import { Schema } from "effect" + +const person = Schema.Struct({ + name: Schema.String +}) + +Schema.decodeUnknownSync(person)( + { + name: "bob dylan", + extraKey: 61 + }, + { onExcessProperty: "error" } +) +// => throws ParseError +``` + +### catch + +Both `zod` and `effect/Schema` allow you to define fallback values when parsing fails. + +- In `zod`, fallback values are set using the `.catch()` method when defining the schema. +- In `effect/Schema`, fallback values are specified using the `decodingFallback` annotation. + +**Example** (Defining a fallback value for parsing failures) + +Zod + +```ts +import { z } from "zod" + +const schema = z.number().catch(42) + +console.log(schema.parse(5)) // => 5 +console.log(schema.parse("tuna")) // => 42 +``` + +Schema + +```ts +import { Either, Schema } from "effect" + +const schema = Schema.Number.annotations({ + decodingFallback: () => Either.right(42) +}) + +console.log(Schema.decodeUnknownSync(schema)(5)) // => 5 +console.log(Schema.decodeUnknownSync(schema)("tuna")) // => 42 +``` + +### catchall + +Both `zod` and `effect/Schema` allow you to handle additional properties that are not explicitly defined in an object schema by applying a "catchall" schema to validate those properties. This is useful when dealing with objects that may include dynamic keys with uniform value types. + +- In `zod`, catchall behavior is enabled using the `.catchall()` method, which applies a specified schema to all additional properties. +- In `effect/Schema`, this is achieved by combining a `Schema.Record` schema with the main object schema. The `Schema.Record` defines the type for dynamic keys and their values. + +**Example** (Defining a catchall schema for additional properties) + +Zod + +````ts +const person = z + .object({ + name: z.string() + }) + .catchall(z.string()) + +person.parse({ + name: "bob dylan", + validExtraKey: "foo" // works fine +}) + +person.parse({ + name: "bob dylan", + validExtraKey: false // fails +}) +// => throws ZodError``` +```` + +Schema + +```ts +import { Schema } from "effect" + +const person = Schema.Struct( + { + name: Schema.String + }, + Schema.Record({ key: Schema.String, value: Schema.String }) +) + +Schema.decodeUnknownSync(person)({ + name: "bob dylan", + validExtraKey: "foo" // works fine +}) + +Schema.decodeUnknownSync(person)({ + name: "bob dylan", + validExtraKey: true // fails +}) +// => throws ParseError +``` + +## Arrays + +Both `zod` and `effect/Schema` provide tools for defining schemas for arrays. These schemas validate that the input is an array and that each element in the array conforms to the specified schema. + +- In `zod`, array schemas are created using the `z.array()` method, which takes a schema for the array elements as an argument. +- In `effect/Schema`, array schemas are created using `Schema.Array()`, which also takes the schema for the elements as an argument. + +**Example** (Defining an array schema for strings) + +Zod + +```ts +const stringArray = z.array(z.string()) +``` + +Schema + +```ts +import { Schema } from "effect" + +const stringArray = Schema.Array(Schema.String) +``` + +### Accessing the Element Schema + +- In `zod`, the `.element` property is used to access the schema for the elements in the array. +- In `effect/Schema`, the `.value` property serves the same purpose. + +**Example** (Accessing the schema for array elements) + +Zod + +```ts +stringArray.element // => string schema +``` + +Schema + +```ts +stringArray.value // => String schema +``` + +### Defining Non-Empty Arrays + +- `zod` provides the `.nonempty()` method for array schemas to enforce that the array has at least one element. +- `effect/Schema` uses `Schema.NonEmptyArray()` for the same functionality. + +**Example** (Enforcing arrays to have at least one element) + +Zod + +```ts +const nonEmptyStrings = z.string().array().nonempty() +// the inferred type is now +// [string, ...string[]] + +nonEmptyStrings.parse([]) // throws: "Array cannot be empty" +nonEmptyStrings.parse(["Ariana Grande"]) // passes +``` + +Schema + +```ts +import { Schema } from "effect" + +const nonEmptyStrings = Schema.NonEmptyArray(Schema.String) +// the inferred type is now +// [string, ...string[]] + +Schema.decodeUnknownSync(nonEmptyStrings)([]) +/* throws: +Error: readonly [string, ...string[]] +└─ [0] + └─ is missing +*/ +Schema.decodeUnknownSync(nonEmptyStrings)(["Ariana Grande"]) // passes +``` + +### Array Length Validations + +- In `zod`, methods like `.min()`, `.max()`, and `.length()` are chained to set array length constraints. +- In `effect/Schema`, length validations are applied using `pipe()` with combinators like `Schema.minItems()`, `Schema.maxItems()`, and `Schema.itemsCount()`. + +**Example** (Validating array length) + +Zod + +```ts +z.string().array().min(5) // must contain 5 or more items +z.string().array().max(5) // must contain 5 or fewer items +z.string().array().length(5) // must contain 5 items exactly +``` + +Schema + +```ts +import { Schema } from "effect" + +Schema.Array(Schema.String).pipe(Schema.minItems(5)) // must contain 5 or more items +Schema.Array(Schema.String).pipe(Schema.maxItems(5)) // must contain 5 or fewer items +Schema.Array(Schema.String).pipe(Schema.itemsCount(5)) // must contain 5 items exactly +``` + +## Tuples + +Both `zod` and `effect/Schema` support tuples, allowing you to define fixed-length arrays where each element has a specific type. + +- In `zod`, tuples are created using the `z.tuple()` method, where the schema for each element is defined in an array. +- In `effect/Schema`, tuples are defined using `Schema.Tuple()` and accept the schemas for the elements as arguments. + +Tuples in `effect/Schema` are readonly by default, whereas tuples in `zod` are mutable unless explicitly marked as readonly in TypeScript. + +**Example** (Defining a tuple schema) + +Zod + +```ts +const athleteSchema = z.tuple([ + z.string(), // name + z.number(), // jersey number + z.object({ + pointsScored: z.number() + }) // statistics +]) + +type Athlete = z.infer +// type Athlete = [string, number, { pointsScored: number }] +``` + +Schema + +```ts +import { Schema } from "effect" + +const athleteSchema = Schema.Tuple( + Schema.String, // name + Schema.Number, // jersey number + Schema.Struct({ + pointsScored: Schema.Number + }) // statistics +) + +// type Athlete = readonly [string, number, { readonly pointsScored: number }] +type Athlete = typeof athleteSchema.Type +``` + +### Variadic Tuples + +- `zod` supports variadic tuples with the `.rest()` method, allowing the tuple to include additional elements of a specific type. +- `effect/Schema` handles this by combining a fixed tuple schema with a rest schema for additional elements. + +**Example** (Defining a variadic tuple schema) + +Zod + +```ts +const variadicTuple = z.tuple([z.string()]).rest(z.number()) +const result = variadicTuple.parse(["hello", 1, 2, 3]) +// => [string, ...number[]]; +``` + +Schema + +```ts +import { Schema } from "effect" + +const variadicTuple = Schema.Tuple([Schema.String], Schema.Number) + +const result = Schema.decodeUnknownSync(variadicTuple)(["hello", 1, 2, 3]) +// => readonly [string, ...number[]]; +``` + +## Unions + +Both `zod` and `effect/Schema` support unions, which allow you to define a schema that accepts multiple types. + +- In `zod`, unions are created using the `z.union()` method, where the possible schemas are passed as an array. +- In `effect/Schema`, unions are defined using `Schema.Union()`, where the schemas are passed as arguments. + +**Discriminated Unions** + +- In `zod`, discriminated unions require explicitly using the `z.discriminatedUnion()` method for better performance and error messages. +- In `effect/Schema`, discriminated unions are automatically detected, so no additional configuration is needed. + +**Example** (Defining a union schema) + +Zod + +```ts +const stringOrNumber = z.union([z.string(), z.number()]) + +stringOrNumber.parse("foo") // passes +stringOrNumber.parse(14) // passes +``` + +Schema + +```ts +import { Schema } from "effect" + +const stringOrNumber = Schema.Union(Schema.String, Schema.Number) + +Schema.decodeUnknownSync(stringOrNumber)("foo") // passes +Schema.decodeUnknownSync(stringOrNumber)(14) // passes +``` + +## Discriminated unions + +In `zod`, discriminated unions must be explicitly declared using the `z.discriminatedUnion()` method. +In `effect/Schema`, discriminated unions are automatically detected based on shared properties. No special method is needed to handle them. + +## Records + +Both `zod` and `effect/Schema` support record schemas, which are used to validate objects with dynamic keys. A record schema ensures that all keys in the object match a specified schema and that their corresponding values also conform to a schema. + +`effect/Schema` generates readonly types by default. + +**Example** (Defining a record schema) + +Zod + +```ts +const User = z.object({ name: z.string() }) + +const UserStore = z.record(z.string(), User) + +// type UserStore = Record +type UserStore = z.infer +``` + +Schema + +```ts +import { Schema } from "effect" + +const User = Schema.Struct({ name: Schema.String }) + +const UserStore = Schema.Record({ key: Schema.String, value: User }) + +// type UserStore = { readonly [x: string]: { readonly name: string; }; } +type UserStore = typeof UserStore.Type +``` + +## Maps + +Both `zod` and `effect/Schema` support schemas for `Map` objects, where keys and values can be validated using specified schemas. + +- In `zod`, maps are defined using the `z.map()` method, where the first argument is the key schema and the second is the value schema. +- In `effect/Schema`, maps are defined using `Schema.Map()` or `Schema.ReadonlyMap()` for mutable or readonly maps, respectively. Both require an object specifying the `key` and `value` schemas. + +`effect/Schema` provides an explicit `Schema.ReadonlyMap()` schema to generate readonly maps. + +**Example** (Defining a schema for maps) + +Zod + +```ts +const stringNumberMap = z.map(z.string(), z.number()) + +type StringNumberMap = z.infer +// type StringNumberMap = Map +``` + +Schema + +```ts +import { Schema } from "effect" + +const map1 = Schema.Map({ key: Schema.String, value: Schema.Number }) + +// type Map1 = Map +type Map1 = typeof map1.Type + +const map2 = Schema.ReadonlyMap({ key: Schema.String, value: Schema.Number }) + +// type Map2 = ReadonlyMap +type Map2 = typeof map2.Type +``` + +## Sets + +Both `zod` and `effect/Schema` support schemas for `Set` objects, allowing you to validate sets where all elements conform to a specified schema. + +- In `zod`, sets are created using the `z.set()` method, where you pass the schema for the elements of the set. +- In `effect/Schema`, sets are defined using `Schema.Set()` or `Schema.ReadonlySet()` for mutable or readonly sets, respectively. Both require the schema for the elements. + +`effect/Schema` includes `Schema.ReadonlySet()` to explicitly define readonly sets. + +**Example** (Defining a schema for sets) + +Zod + +```ts +const numberSet = z.set(z.number()) +type NumberSet = z.infer +// type NumberSet = Set +``` + +Schema + +```ts +import { Schema } from "effect" + +const set1 = Schema.Set(Schema.Number) + +// type Set1 = Set +type Set1 = typeof set1.Type + +const set2 = Schema.ReadonlySet(Schema.Number) + +// type Set2 = ReadonlySet +type Set2 = typeof set2.Type +``` + +## Intersections + +In `zod`, intersections are used to combine multiple schemas into one, requiring the input to satisfy all the combined schemas. + +`effect/Schema` does not have a direct equivalent for intersections. However, similar behavior can be achieved using `Schema.extend()` to merge two or more struct schemas, or by spreading fields from multiple schemas into a new `Schema.Struct()`. + +## Recursive types + +Both `zod` and `effect/Schema` support defining recursive types, which are types that reference themselves. Recursive types are commonly used for hierarchical data structures such as trees, graphs, or nested categories. + +- In `zod`, recursion is achieved using the `z.lazy()` method, which defers the evaluation of the schema until it is referenced. +- In `effect/Schema`, recursion is handled using the `Schema.suspend()` function, which delays the resolution of the schema. + +**Example** (Defining a recursive schema for categories) + +Zod + +```ts +const baseCategorySchema = z.object({ + name: z.string() +}) + +type Category = z.infer & { + subcategories: Category[] +} + +const categorySchema: z.ZodType = baseCategorySchema.extend({ + subcategories: z.lazy(() => categorySchema.array()) +}) +``` + +Schema + +```ts +import { Schema } from "effect" + +const baseCategorySchema = Schema.Struct({ + name: Schema.String +}) + +type Category = Schema.Schema.Type & { + readonly subcategories: ReadonlyArray +} + +const categorySchema: Schema.Schema = Schema.Struct({ + ...baseCategorySchema.fields, + subcategories: Schema.suspend(() => Schema.Array(categorySchema)) +}) +``` + +## Promises + +No direct equivalent in `effect/Schema`. + +## Instanceof + +Both `zod` and `effect/Schema` support validating instances of classes or constructors using their `instanceof` functionality. + +- In `zod`, the `z.instanceof()` method is used to create a schema that validates if an input is an instance of a specified class or constructor. +- In `effect/Schema`, the `Schema.instanceOf()` method provides the same functionality, taking the target class as an argument. + +**Example** (Validating instances of a class) + +Zod + +```ts +class Test { + name: string = "name" +} + +const TestSchema = z.instanceof(Test) + +const blob: any = "whatever" +TestSchema.parse(new Test()) // passes +TestSchema.parse(blob) // throws +``` + +Schema + +```ts +import { Schema } from "effect" + +class Test { + name: string = "name" +} + +const TestSchema = Schema.instanceOf(Test) + +const blob: any = "whatever" + +Schema.decodeUnknownSync(TestSchema)(new Test()) // passes +Schema.decodeUnknownSync(TestSchema)(blob) // throws +``` + +## Functions + +No direct equivalent in `effect/Schema`. + +## Preprocess + +No direct equivalent in `effect/Schema`. + +## Custom schemas + +Both `zod` and `effect/Schema` allow you to define custom schemas for validation scenarios that fall outside the scope of built-in schema types. + +- In `zod`, custom schemas are created using the `z.custom()` method. This method allows you to define a validation function that returns a boolean indicating whether the input is valid. +- In `effect/Schema`, custom schemas are created using the `Schema.declare()` function. This approach provides more flexibility by allowing you to define the input and output types, parsing logic, and error handling. + +See the [Schema.declare](https://effect.website/docs/schema/advanced-usage/#declaring-new-data-types) documentation for more details. + +## refine / superRefine + +Both `zod` and `effect/Schema` allow you to add custom validation rules to existing schemas. These rules are useful for applying constraints that go beyond the basic validation logic provided by the libraries' built-in schema types. + +- In `zod`, you can use `.refine()` to apply a single validation rule or `.superRefine()` for more complex validations that require access to the validation context (e.g., adding multiple errors). +- In `effect/Schema`, you can use `Schema.filter()` for simple validations or `Schema.filterEffect()` to include asynchronous or effectful validation logic. + +See the [Schema.filter](https://effect.website/docs/schema/filters/) and [Schema.filterEffect](https://effect.website/docs/schema/transformations/#effectful-filters) documentation for more details. + +## transform + +Both `zod` and `effect/Schema` provide functionality to transform input data into a desired format during parsing. Transformations are useful when you need to derive new values, normalize input, or map raw data into a structure that is more convenient for further processing. While the capabilities of the two libraries overlap, there are differences in how transformations are defined and applied. + +- In `zod`, the `.transform()` method is used to apply a transformation function directly to the schema. +- In `effect/Schema`, transformations are applied using `Schema.transform()` or `Schema.transformOrFail()` for additional error handling during the transformation process. + +See the [transform](https://effect.website/docs/schema/transformations/#transform) and [transformOrFail](https://effect.website/docs/schema/transformations/#transformorfail) documentation for more details. + +## describe + +Both `zod` and `effect/Schema` allow you to attach descriptive metadata to schemas. This feature is useful for documentation, error reporting, or providing additional context about a schema's purpose. The description does not affect validation or parsing; it serves purely as a human-readable explanation. + +- In `zod`, descriptions are added using the `.describe()` method, which accepts a string describing the schema. +- In `effect/Schema`, descriptions are added using the `annotations()` method, where the `description` is included as a metadata property. + +**Example** (Adding a description to a schema) + +Zod + +```ts +const documentedString = z + .string() + .describe("A useful bit of text, if you know what to do with it.") +documentedString.description // A useful bit of text… +``` + +Schema + +```ts +import { Schema, SchemaAST } from "effect" + +const documentedString = Schema.String.annotations({ + description: "A useful bit of text, if you know what to do with it." +}) + +console.log(SchemaAST.getDescriptionAnnotation(documentedString.ast)) +/* +Output: +{ + _id: 'Option', + _tag: 'Some', + value: 'A useful bit of text, if you know what to do with it.' +} +*/ +``` + +## nullish + +Both `zod` and `effect/Schema` provide support for schemas that allow values to be `null` or `undefined` in addition to a specified type. + +- In `zod`, you use the `.nullish()` method to extend a schema to allow `null` or `undefined` values in addition to the specified type. +- In `effect/Schema`, the equivalent is achieved with `Schema.NullishOr()`, where you pass the desired type. + +**Example** (Defining a schema that allows `null` or `undefined` values) + +Zod + +```ts +const nullishString = z.string().nullish() // string | null | undefined +``` + +Schema + +```ts +import { Schema } from "effect" + +const nullishString = Schema.NullishOr(Schema.String) // string | null | undefined +``` + +## brand + +Both `zod` and `effect/Schema` support branding, a feature that allows you to tag types with a unique identifier without changing their runtime behavior. Branding is useful when you need stronger type distinctions for otherwise identical structures, preventing accidental misuse or mixing of similar types. + +- In `zod`, branding is applied using the `.brand<>()` method on a schema, where you specify the brand name as a generic type argument. +- In `effect/Schema`, branding is achieved by using the `Schema.brand()` function in combination with the `pipe()` method. + +**Example** (Defining a branded schema) + +Zod + +```ts +const Cat = z.object({ name: z.string() }).brand<"Cat">() +``` + +Schema + +```ts +import { Schema } from "effect" + +const Cat = Schema.Struct({ name: Schema.String }).pipe(Schema.brand("Cat")) +``` + +## readonly + +No equivalent as it's the default behavior. diff --git a/repos/effect/packages/effect/src/.index.ts b/repos/effect/packages/effect/src/.index.ts new file mode 100644 index 0000000..c12c6cf --- /dev/null +++ b/repos/effect/packages/effect/src/.index.ts @@ -0,0 +1,30 @@ +/** + * @since 2.0.0 + */ + +export { + /** + * @since 2.0.0 + */ + absurd, + /** + * @since 2.0.0 + */ + flow, + /** + * @since 2.0.0 + */ + hole, + /** + * @since 2.0.0 + */ + identity, + /** + * @since 2.0.0 + */ + pipe, + /** + * @since 2.0.0 + */ + unsafeCoerce +} from "./Function.js" diff --git a/repos/effect/packages/effect/src/Arbitrary.ts b/repos/effect/packages/effect/src/Arbitrary.ts new file mode 100644 index 0000000..a46e96b --- /dev/null +++ b/repos/effect/packages/effect/src/Arbitrary.ts @@ -0,0 +1,1101 @@ +/** + * @since 3.10.0 + */ + +import * as Arr from "./Array.js" +import * as FastCheck from "./FastCheck.js" +import { globalValue } from "./GlobalValue.js" +import * as errors_ from "./internal/schema/errors.js" +import * as schemaId_ from "./internal/schema/schemaId.js" +import * as util_ from "./internal/schema/util.js" +import * as Option from "./Option.js" +import * as Predicate from "./Predicate.js" +import type * as Schema from "./Schema.js" +import * as SchemaAST from "./SchemaAST.js" +import type * as Types from "./Types.js" + +/** + * @category model + * @since 3.10.0 + */ +export interface LazyArbitrary { + (fc: typeof FastCheck): FastCheck.Arbitrary +} + +/** + * @category annotations + * @since 3.10.0 + */ +export interface ArbitraryGenerationContext { + readonly maxDepth: number + readonly depthIdentifier?: string + readonly constraints?: StringConstraints | NumberConstraints | BigIntConstraints | DateConstraints | ArrayConstraints +} + +/** + * @category annotations + * @since 3.10.0 + */ +export type ArbitraryAnnotation = readonly []> = ( + ...arbitraries: [ + ...{ readonly [K in keyof TypeParameters]: LazyArbitrary }, + ctx: ArbitraryGenerationContext + ] +) => LazyArbitrary + +/** + * Returns a LazyArbitrary for the `A` type of the provided schema. + * + * @category arbitrary + * @since 3.10.0 + */ +export const makeLazy = (schema: Schema.Schema): LazyArbitrary => { + const description = getDescription(schema.ast, []) + return go(description, { maxDepth: 2 }) +} + +/** + * Returns a fast-check Arbitrary for the `A` type of the provided schema. + * + * @category arbitrary + * @since 3.10.0 + */ +export const make = (schema: Schema.Schema): FastCheck.Arbitrary => makeLazy(schema)(FastCheck) + +interface StringConstraints { + readonly _tag: "StringConstraints" + readonly constraints: FastCheck.StringSharedConstraints + readonly pattern?: string +} + +/** @internal */ +export const makeStringConstraints = (options: { + readonly minLength?: number | undefined + readonly maxLength?: number | undefined + readonly pattern?: string | undefined +}): StringConstraints => { + const out: Types.Mutable = { + _tag: "StringConstraints", + constraints: {} + } + if (Predicate.isNumber(options.minLength)) { + out.constraints.minLength = options.minLength + } + if (Predicate.isNumber(options.maxLength)) { + out.constraints.maxLength = options.maxLength + } + if (Predicate.isString(options.pattern)) { + out.pattern = options.pattern + } + return out +} + +interface NumberConstraints { + readonly _tag: "NumberConstraints" + readonly constraints: FastCheck.FloatConstraints + readonly isInteger: boolean +} + +/** @internal */ +export const makeNumberConstraints = (options: { + readonly isInteger?: boolean | undefined + readonly min?: unknown + readonly minExcluded?: boolean | undefined + readonly max?: unknown + readonly maxExcluded?: boolean | undefined + readonly noNaN?: boolean | undefined + readonly noDefaultInfinity?: boolean | undefined +}): NumberConstraints => { + const out: Types.Mutable = { + _tag: "NumberConstraints", + constraints: {}, + isInteger: options.isInteger ?? false + } + if (Predicate.isNumber(options.min)) { + out.constraints.min = Math.fround(options.min) + } + if (Predicate.isBoolean(options.minExcluded)) { + out.constraints.minExcluded = options.minExcluded + } + if (Predicate.isNumber(options.max)) { + out.constraints.max = Math.fround(options.max) + } + if (Predicate.isBoolean(options.maxExcluded)) { + out.constraints.maxExcluded = options.maxExcluded + } + if (Predicate.isBoolean(options.noNaN)) { + out.constraints.noNaN = options.noNaN + } + if (Predicate.isBoolean(options.noDefaultInfinity)) { + out.constraints.noDefaultInfinity = options.noDefaultInfinity + } + return out +} + +interface BigIntConstraints { + readonly _tag: "BigIntConstraints" + readonly constraints: FastCheck.BigIntConstraints +} + +/** @internal */ +export const makeBigIntConstraints = (options: { + readonly min?: bigint | undefined + readonly max?: bigint | undefined +}): BigIntConstraints => { + const out: Types.Mutable = { + _tag: "BigIntConstraints", + constraints: {} + } + if (Predicate.isBigInt(options.min)) { + out.constraints.min = options.min + } + if (Predicate.isBigInt(options.max)) { + out.constraints.max = options.max + } + return out +} + +interface ArrayConstraints { + readonly _tag: "ArrayConstraints" + readonly constraints: FastCheck.ArrayConstraints +} + +/** @internal */ +export const makeArrayConstraints = (options: { + readonly minLength?: unknown + readonly maxLength?: unknown +}): ArrayConstraints => { + const out: Types.Mutable = { + _tag: "ArrayConstraints", + constraints: {} + } + if (Predicate.isNumber(options.minLength)) { + out.constraints.minLength = options.minLength + } + if (Predicate.isNumber(options.maxLength)) { + out.constraints.maxLength = options.maxLength + } + return out +} + +interface DateConstraints { + readonly _tag: "DateConstraints" + readonly constraints: FastCheck.DateConstraints +} + +/** @internal */ +export const makeDateConstraints = (options: { + readonly min?: Date | undefined + readonly max?: Date | undefined + readonly noInvalidDate?: boolean | undefined +}): DateConstraints => { + const out: Types.Mutable = { + _tag: "DateConstraints", + constraints: {} + } + if (Predicate.isDate(options.min)) { + out.constraints.min = options.min + } + if (Predicate.isDate(options.max)) { + out.constraints.max = options.max + } + if (Predicate.isBoolean(options.noInvalidDate)) { + out.constraints.noInvalidDate = options.noInvalidDate + } + return out +} + +type Refinements = ReadonlyArray + +interface Base { + readonly path: ReadonlyArray + readonly refinements: Refinements + readonly annotations: ReadonlyArray> +} + +interface StringKeyword extends Base { + readonly _tag: "StringKeyword" + readonly constraints: ReadonlyArray +} + +interface NumberKeyword extends Base { + readonly _tag: "NumberKeyword" + readonly constraints: ReadonlyArray +} + +interface BigIntKeyword extends Base { + readonly _tag: "BigIntKeyword" + readonly constraints: ReadonlyArray +} + +interface DateFromSelf extends Base { + readonly _tag: "DateFromSelf" + readonly constraints: ReadonlyArray +} + +interface Declaration extends Base { + readonly _tag: "Declaration" + readonly typeParameters: ReadonlyArray + readonly ast: SchemaAST.AST +} + +interface TupleType extends Base { + readonly _tag: "TupleType" + readonly constraints: ReadonlyArray + readonly elements: ReadonlyArray<{ + readonly isOptional: boolean + readonly description: Description + }> + readonly rest: ReadonlyArray +} + +interface TypeLiteral extends Base { + readonly _tag: "TypeLiteral" + readonly propertySignatures: ReadonlyArray<{ + readonly isOptional: boolean + readonly name: PropertyKey + readonly value: Description + }> + readonly indexSignatures: ReadonlyArray<{ + readonly parameter: Description + readonly value: Description + }> +} + +interface Union extends Base { + readonly _tag: "Union" + readonly members: ReadonlyArray +} + +interface Suspend extends Base { + readonly _tag: "Suspend" + readonly id: string + readonly ast: SchemaAST.AST + readonly description: () => Description +} + +interface Ref extends Base { + readonly _tag: "Ref" + readonly id: string + readonly ast: SchemaAST.AST +} + +interface NeverKeyword extends Base { + readonly _tag: "NeverKeyword" + readonly ast: SchemaAST.AST +} + +interface Keyword extends Base { + readonly _tag: "Keyword" + readonly value: + | "UndefinedKeyword" + | "VoidKeyword" + | "UnknownKeyword" + | "AnyKeyword" + | "BooleanKeyword" + | "SymbolKeyword" + | "ObjectKeyword" +} + +interface Literal extends Base { + readonly _tag: "Literal" + readonly literal: SchemaAST.LiteralValue +} + +interface UniqueSymbol extends Base { + readonly _tag: "UniqueSymbol" + readonly symbol: symbol +} + +interface Enums extends Base { + readonly _tag: "Enums" + readonly enums: ReadonlyArray + readonly ast: SchemaAST.AST +} + +interface TemplateLiteral extends Base { + readonly _tag: "TemplateLiteral" + readonly head: string + readonly spans: ReadonlyArray<{ + readonly description: Description + readonly literal: string + }> +} + +type Description = + | Declaration + | NeverKeyword + | Keyword + | Literal + | UniqueSymbol + | Enums + | TemplateLiteral + | StringKeyword + | NumberKeyword + | BigIntKeyword + | DateFromSelf + | TupleType + | TypeLiteral + | Union + | Suspend + | Ref + +const getArbitraryAnnotation = SchemaAST.getAnnotation>(SchemaAST.ArbitraryAnnotationId) + +const getASTConstraints = (ast: SchemaAST.AST) => { + const TypeAnnotationId = ast.annotations[SchemaAST.SchemaIdAnnotationId] + if (Predicate.isPropertyKey(TypeAnnotationId)) { + const out = ast.annotations[TypeAnnotationId] + if (Predicate.isReadonlyRecord(out)) { + return out + } + } +} + +const idMemoMap = globalValue( + Symbol.for("effect/Arbitrary/IdMemoMap"), + () => new Map() +) + +let counter = 0 + +function wrapGetDescription( + f: (ast: SchemaAST.AST, description: Description) => Description, + g: (ast: SchemaAST.AST, path: ReadonlyArray) => Description +): (ast: SchemaAST.AST, path: ReadonlyArray) => Description { + return (ast, path) => f(ast, g(ast, path)) +} + +function parseMeta(ast: SchemaAST.AST): [SchemaAST.SchemaIdAnnotation | undefined, Record] { + const jsonSchema = SchemaAST.getJSONSchemaAnnotation(ast).pipe( + Option.filter(Predicate.isReadonlyRecord), + Option.getOrUndefined + ) + const schemaId = Option.getOrElse(SchemaAST.getSchemaIdAnnotation(ast), () => undefined) + const schemaParams = Option.fromNullable(schemaId).pipe( + Option.map((id) => ast.annotations[id]), + Option.filter(Predicate.isReadonlyRecord), + Option.getOrUndefined + ) + return [schemaId, { ...schemaParams, ...jsonSchema }] +} + +/** @internal */ +export const getDescription = wrapGetDescription( + (ast, description) => { + const annotation = getArbitraryAnnotation(ast) + if (Option.isSome(annotation)) { + return { + ...description, + annotations: [...description.annotations, annotation.value] + } + } + return description + }, + (ast, path) => { + const [schemaId, meta] = parseMeta(ast) + switch (ast._tag) { + case "Refinement": { + const from = getDescription(ast.from, path) + switch (from._tag) { + case "StringKeyword": + return { + ...from, + constraints: [...from.constraints, makeStringConstraints(meta)], + refinements: [...from.refinements, ast] + } + case "NumberKeyword": { + const c = schemaId === schemaId_.NonNaNSchemaId ? + makeNumberConstraints({ noNaN: true }) : + schemaId === schemaId_.FiniteSchemaId || schemaId === schemaId_.JsonNumberSchemaId ? + makeNumberConstraints({ noDefaultInfinity: true, noNaN: true }) : + makeNumberConstraints({ + isInteger: "type" in meta && meta.type === "integer", + noNaN: undefined, + noDefaultInfinity: undefined, + min: meta.exclusiveMinimum ?? meta.minimum, + minExcluded: "exclusiveMinimum" in meta ? true : undefined, + max: meta.exclusiveMaximum ?? meta.maximum, + maxExcluded: "exclusiveMaximum" in meta ? true : undefined + }) + return { + ...from, + constraints: [...from.constraints, c], + refinements: [...from.refinements, ast] + } + } + case "BigIntKeyword": { + const c = getASTConstraints(ast) + return { + ...from, + constraints: c !== undefined ? [...from.constraints, makeBigIntConstraints(c)] : from.constraints, + refinements: [...from.refinements, ast] + } + } + case "TupleType": + return { + ...from, + constraints: [ + ...from.constraints, + makeArrayConstraints({ + minLength: meta.minItems, + maxLength: meta.maxItems + }) + ], + refinements: [...from.refinements, ast] + } + case "DateFromSelf": + return { + ...from, + constraints: [...from.constraints, makeDateConstraints(meta)], + refinements: [...from.refinements, ast] + } + default: + return { + ...from, + refinements: [...from.refinements, ast] + } + } + } + case "Declaration": { + if (schemaId === schemaId_.DateFromSelfSchemaId) { + return { + _tag: "DateFromSelf", + constraints: [makeDateConstraints(meta)], + path, + refinements: [], + annotations: [] + } + } + return { + _tag: "Declaration", + typeParameters: ast.typeParameters.map((ast) => getDescription(ast, path)), + path, + refinements: [], + annotations: [], + ast + } + } + case "Literal": { + return { + _tag: "Literal", + literal: ast.literal, + path, + refinements: [], + annotations: [] + } + } + case "UniqueSymbol": { + return { + _tag: "UniqueSymbol", + symbol: ast.symbol, + path, + refinements: [], + annotations: [] + } + } + case "Enums": { + return { + _tag: "Enums", + enums: ast.enums, + path, + refinements: [], + annotations: [], + ast + } + } + case "TemplateLiteral": { + return { + _tag: "TemplateLiteral", + head: ast.head, + spans: ast.spans.map((span) => ({ + description: getDescription(span.type, path), + literal: span.literal + })), + path, + refinements: [], + annotations: [] + } + } + case "StringKeyword": + return { + _tag: "StringKeyword", + constraints: [], + path, + refinements: [], + annotations: [] + } + case "NumberKeyword": + return { + _tag: "NumberKeyword", + constraints: [], + path, + refinements: [], + annotations: [] + } + case "BigIntKeyword": + return { + _tag: "BigIntKeyword", + constraints: [], + path, + refinements: [], + annotations: [] + } + case "TupleType": + return { + _tag: "TupleType", + constraints: [], + elements: ast.elements.map((element, i) => ({ + isOptional: element.isOptional, + description: getDescription(element.type, [...path, i]) + })), + rest: ast.rest.map((element, i) => getDescription(element.type, [...path, i])), + path, + refinements: [], + annotations: [] + } + case "TypeLiteral": + return { + _tag: "TypeLiteral", + propertySignatures: ast.propertySignatures.map((ps) => ({ + isOptional: ps.isOptional, + name: ps.name, + value: getDescription(ps.type, [...path, ps.name]) + })), + indexSignatures: ast.indexSignatures.map((is) => ({ + parameter: getDescription(is.parameter, path), + value: getDescription(is.type, path) + })), + path, + refinements: [], + annotations: [] + } + case "Union": + return { + _tag: "Union", + members: ast.types.map((member, i) => getDescription(member, [...path, i])), + path, + refinements: [], + annotations: [] + } + case "Suspend": { + const memoId = idMemoMap.get(ast) + if (memoId !== undefined) { + return { + _tag: "Ref", + id: memoId, + ast, + path, + refinements: [], + annotations: [] + } + } + counter++ + const id = `__id-${counter}__` + idMemoMap.set(ast, id) + return { + _tag: "Suspend", + id, + ast, + description: () => getDescription(ast.f(), path), + path, + refinements: [], + annotations: [] + } + } + case "Transformation": + return getDescription(ast.to, path) + case "NeverKeyword": + return { + _tag: "NeverKeyword", + path, + refinements: [], + annotations: [], + ast + } + default: { + return { + _tag: "Keyword", + value: ast._tag, + path, + refinements: [], + annotations: [] + } + } + } + } +) + +function getMax(n1: Date | undefined, n2: Date | undefined): Date | undefined +function getMax(n1: bigint | undefined, n2: bigint | undefined): bigint | undefined +function getMax(n1: number | undefined, n2: number | undefined): number | undefined +function getMax( + n1: bigint | number | Date | undefined, + n2: bigint | number | Date | undefined +): bigint | number | Date | undefined { + return n1 === undefined ? n2 : n2 === undefined ? n1 : n1 <= n2 ? n2 : n1 +} + +function getMin(n1: Date | undefined, n2: Date | undefined): Date | undefined +function getMin(n1: bigint | undefined, n2: bigint | undefined): bigint | undefined +function getMin(n1: number | undefined, n2: number | undefined): number | undefined +function getMin( + n1: bigint | number | Date | undefined, + n2: bigint | number | Date | undefined +): bigint | number | Date | undefined { + return n1 === undefined ? n2 : n2 === undefined ? n1 : n1 <= n2 ? n1 : n2 +} + +const getOr = (a: boolean | undefined, b: boolean | undefined): boolean | undefined => { + return a === undefined ? b : b === undefined ? a : a || b +} + +function mergePattern(pattern1: string | undefined, pattern2: string | undefined): string | undefined { + if (pattern1 === undefined) { + return pattern2 + } + if (pattern2 === undefined) { + return pattern1 + } + return `(?:${pattern1})|(?:${pattern2})` +} + +function mergeStringConstraints(c1: StringConstraints, c2: StringConstraints): StringConstraints { + return makeStringConstraints({ + minLength: getMax(c1.constraints.minLength, c2.constraints.minLength), + maxLength: getMin(c1.constraints.maxLength, c2.constraints.maxLength), + pattern: mergePattern(c1.pattern, c2.pattern) + }) +} + +function buildStringConstraints(description: StringKeyword): StringConstraints | undefined { + return description.constraints.length === 0 + ? undefined + : description.constraints.reduce(mergeStringConstraints) +} + +function mergeNumberConstraints(c1: NumberConstraints, c2: NumberConstraints): NumberConstraints { + return makeNumberConstraints({ + isInteger: c1.isInteger || c2.isInteger, + min: getMax(c1.constraints.min, c2.constraints.min), + minExcluded: getOr(c1.constraints.minExcluded, c2.constraints.minExcluded), + max: getMin(c1.constraints.max, c2.constraints.max), + maxExcluded: getOr(c1.constraints.maxExcluded, c2.constraints.maxExcluded), + noNaN: getOr(c1.constraints.noNaN, c2.constraints.noNaN), + noDefaultInfinity: getOr(c1.constraints.noDefaultInfinity, c2.constraints.noDefaultInfinity) + }) +} + +function buildNumberConstraints(description: NumberKeyword): NumberConstraints | undefined { + return description.constraints.length === 0 + ? undefined + : description.constraints.reduce(mergeNumberConstraints) +} + +function mergeBigIntConstraints(c1: BigIntConstraints, c2: BigIntConstraints): BigIntConstraints { + return makeBigIntConstraints({ + min: getMax(c1.constraints.min, c2.constraints.min), + max: getMin(c1.constraints.max, c2.constraints.max) + }) +} + +function buildBigIntConstraints(description: BigIntKeyword): BigIntConstraints | undefined { + return description.constraints.length === 0 + ? undefined + : description.constraints.reduce(mergeBigIntConstraints) +} + +function mergeDateConstraints(c1: DateConstraints, c2: DateConstraints): DateConstraints { + return makeDateConstraints({ + min: getMax(c1.constraints.min, c2.constraints.min), + max: getMin(c1.constraints.max, c2.constraints.max), + noInvalidDate: getOr(c1.constraints.noInvalidDate, c2.constraints.noInvalidDate) + }) +} + +function buildDateConstraints(description: DateFromSelf): DateConstraints | undefined { + return description.constraints.length === 0 + ? undefined + : description.constraints.reduce(mergeDateConstraints) +} + +const constArrayConstraints = makeArrayConstraints({}) + +function mergeArrayConstraints(c1: ArrayConstraints, c2: ArrayConstraints): ArrayConstraints { + return makeArrayConstraints({ + minLength: getMax(c1.constraints.minLength, c2.constraints.minLength), + maxLength: getMin(c1.constraints.maxLength, c2.constraints.maxLength) + }) +} + +function buildArrayConstraints(description: TupleType): ArrayConstraints | undefined { + return description.constraints.length === 0 + ? undefined + : description.constraints.reduce(mergeArrayConstraints) +} + +const arbitraryMemoMap = globalValue( + Symbol.for("effect/Arbitrary/arbitraryMemoMap"), + () => new WeakMap>() +) + +function applyFilters(filters: ReadonlyArray>, arb: LazyArbitrary): LazyArbitrary { + return (fc) => filters.reduce((arb, filter) => arb.filter(filter), arb(fc)) +} + +function absurd(message: string): LazyArbitrary { + return () => { + throw new Error(message) + } +} + +function getContextConstraints(description: Description): ArbitraryGenerationContext["constraints"] { + switch (description._tag) { + case "StringKeyword": + return buildStringConstraints(description) + case "NumberKeyword": + return buildNumberConstraints(description) + case "BigIntKeyword": + return buildBigIntConstraints(description) + case "DateFromSelf": + return buildDateConstraints(description) + case "TupleType": + return buildArrayConstraints(description) + } +} + +function wrapGo( + f: (description: Description, ctx: ArbitraryGenerationContext, lazyArb: LazyArbitrary) => LazyArbitrary, + g: (description: Description, ctx: ArbitraryGenerationContext) => LazyArbitrary +): (description: Description, ctx: ArbitraryGenerationContext) => LazyArbitrary { + return (description, ctx) => f(description, ctx, g(description, ctx)) +} + +const go = wrapGo( + (description, ctx, lazyArb) => { + const annotation: ArbitraryAnnotation | undefined = + description.annotations[description.annotations.length - 1] + + // error handling + if (annotation === undefined) { + switch (description._tag) { + case "Declaration": + case "NeverKeyword": + throw new Error(errors_.getArbitraryMissingAnnotationErrorMessage(description.path, description.ast)) + case "Enums": + if (description.enums.length === 0) { + throw new Error(errors_.getArbitraryEmptyEnumErrorMessage(description.path)) + } + } + } + + const filters = description.refinements.map((ast) => (a: any) => + Option.isNone(ast.filter(a, SchemaAST.defaultParseOption, ast)) + ) + if (annotation === undefined) { + return applyFilters(filters, lazyArb) + } + + const constraints = getContextConstraints(description) + if (constraints !== undefined) { + ctx = { ...ctx, constraints } + } + + if (description._tag === "Declaration") { + return applyFilters(filters, annotation(...description.typeParameters.map((p) => go(p, ctx)), ctx)) + } + if (description.refinements.length > 0) { + // TODO(4.0): remove the `lazyArb` parameter + return applyFilters(filters, annotation(lazyArb, ctx)) + } + return annotation(ctx) + }, + (description, ctx) => { + switch (description._tag) { + case "DateFromSelf": { + const constraints = buildDateConstraints(description) + return (fc) => fc.date(constraints?.constraints) + } + case "Declaration": + case "NeverKeyword": + return absurd(`BUG: cannot generate an arbitrary for ${description._tag}`) + case "Literal": + return (fc) => fc.constant(description.literal) + case "UniqueSymbol": + return (fc) => fc.constant(description.symbol) + case "Keyword": { + switch (description.value) { + case "UndefinedKeyword": + return (fc) => fc.constant(undefined) + case "VoidKeyword": + case "UnknownKeyword": + case "AnyKeyword": + return (fc) => fc.anything() + case "BooleanKeyword": + return (fc) => fc.boolean() + case "SymbolKeyword": + return (fc) => fc.string().map((s) => Symbol.for(s)) + case "ObjectKeyword": + return (fc) => fc.oneof(fc.object(), fc.array(fc.anything())) + } + } + case "Enums": + return (fc) => fc.oneof(...description.enums.map(([_, value]) => fc.constant(value))) + case "TemplateLiteral": { + return (fc) => { + const string = fc.string({ maxLength: 5 }) + const number = fc.float({ noDefaultInfinity: true, noNaN: true }) + + const getTemplateLiteralArb = (description: TemplateLiteral) => { + const components: Array> = description.head !== "" + ? [fc.constant(description.head)] + : [] + + const getTemplateLiteralSpanTypeArb = ( + description: Description + ): FastCheck.Arbitrary => { + switch (description._tag) { + case "StringKeyword": + return string + case "NumberKeyword": + return number + case "Literal": + return fc.constant(String(description.literal)) + case "Union": + return fc.oneof(...description.members.map(getTemplateLiteralSpanTypeArb)) + case "TemplateLiteral": + return getTemplateLiteralArb(description) + default: + return fc.constant("") + } + } + + description.spans.forEach((span) => { + components.push(getTemplateLiteralSpanTypeArb(span.description)) + if (span.literal !== "") { + components.push(fc.constant(span.literal)) + } + }) + + return fc.tuple(...components).map((spans) => spans.join("")) + } + + return getTemplateLiteralArb(description) + } + } + case "StringKeyword": { + const constraints = buildStringConstraints(description) + const pattern = constraints?.pattern + return pattern !== undefined ? + (fc) => fc.stringMatching(new RegExp(pattern)) : + (fc) => fc.string(constraints?.constraints) + } + case "NumberKeyword": { + const constraints = buildNumberConstraints(description) + return constraints?.isInteger ? + (fc) => fc.integer(constraints.constraints) : + (fc) => fc.float(constraints?.constraints) + } + case "BigIntKeyword": { + const constraints = buildBigIntConstraints(description) + return (fc) => fc.bigInt(constraints?.constraints ?? {}) + } + case "TupleType": { + const elements: Array> = [] + let hasOptionals = false + for (const element of description.elements) { + elements.push(go(element.description, ctx)) + if (element.isOptional) { + hasOptionals = true + } + } + const rest = description.rest.map((d) => go(d, ctx)) + return (fc) => { + // --------------------------------------------- + // handle elements + // --------------------------------------------- + let output = fc.tuple(...elements.map((arb) => arb(fc))) + if (hasOptionals) { + const indexes = fc.tuple( + ...description.elements.map((element) => element.isOptional ? fc.boolean() : fc.constant(true)) + ) + output = output.chain((tuple) => + indexes.map((booleans) => { + for (const [i, b] of booleans.reverse().entries()) { + if (!b) { + tuple.splice(booleans.length - i, 1) + } + } + return tuple + }) + ) + } + + // --------------------------------------------- + // handle rest element + // --------------------------------------------- + if (Arr.isNonEmptyReadonlyArray(rest)) { + const constraints = buildArrayConstraints(description) ?? constArrayConstraints + const [head, ...tail] = rest + const item = head(fc) + output = output.chain((as) => { + const len = as.length + // We must adjust the constraints for the rest element + // because the elements might have generated some values + const restArrayConstraints = subtractElementsLength(constraints.constraints, len) + if (restArrayConstraints.maxLength === 0) { + return fc.constant(as) + } + /* + + `getSuspendedArray` is used to generate less values in + the context of a recursive schema. Without it, the following schema + would generate an big amount of values possibly leading to a stack + overflow: + + ```ts + type A = ReadonlyArray + + const schema = S.Array( + S.NullOr(S.suspend((): S.Schema => schema)) + ) + ``` + + */ + const arr = ctx.depthIdentifier !== undefined + ? getSuspendedArray(fc, ctx.depthIdentifier, ctx.maxDepth, item, restArrayConstraints) + : fc.array(item, restArrayConstraints) + if (len === 0) { + return arr + } + return arr.map((rest) => [...as, ...rest]) + }) + // --------------------------------------------- + // handle post rest elements + // --------------------------------------------- + for (let j = 0; j < tail.length; j++) { + output = output.chain((as) => tail[j](fc).map((a) => [...as, a])) + } + } + + return output + } + } + case "TypeLiteral": { + const propertySignatures: Array> = [] + const requiredKeys: Array = [] + for (const ps of description.propertySignatures) { + if (!ps.isOptional) { + requiredKeys.push(ps.name) + } + propertySignatures.push(go(ps.value, ctx)) + } + const indexSignatures = description.indexSignatures.map((is) => + [go(is.parameter, ctx), go(is.value, ctx)] as const + ) + return (fc) => { + const pps: any = {} + for (let i = 0; i < propertySignatures.length; i++) { + const ps = description.propertySignatures[i] + pps[ps.name] = propertySignatures[i](fc) + } + let output = fc.record(pps, { requiredKeys }) + // --------------------------------------------- + // handle index signatures + // --------------------------------------------- + for (let i = 0; i < indexSignatures.length; i++) { + const key = indexSignatures[i][0](fc) + const value = indexSignatures[i][1](fc) + output = output.chain((o) => { + const item = fc.tuple(key, value) + /* + + `getSuspendedArray` is used to generate less key/value pairs in + the context of a recursive schema. Without it, the following schema + would generate an big amount of values possibly leading to a stack + overflow: + + ```ts + type A = { [_: string]: A } + + const schema = S.Record({ key: S.String, value: S.suspend((): S.Schema => schema) }) + ``` + + */ + const arr = ctx.depthIdentifier !== undefined ? + getSuspendedArray(fc, ctx.depthIdentifier, ctx.maxDepth, item, { maxLength: 2 }) : + fc.array(item) + return arr.map((tuples) => ({ ...Object.fromEntries(tuples), ...o })) + }) + } + + return output + } + } + case "Union": { + const members = description.members.map((member) => go(member, ctx)) + return (fc) => fc.oneof(...members.map((arb) => arb(fc))) + } + case "Suspend": { + const memo = arbitraryMemoMap.get(description.ast) + if (memo) { + return memo + } + if (ctx.depthIdentifier === undefined) { + ctx = { ...ctx, depthIdentifier: description.id } + } + const get = util_.memoizeThunk(() => { + return go(description.description(), ctx) + }) + const out: LazyArbitrary = (fc) => fc.constant(null).chain(() => get()(fc)) + arbitraryMemoMap.set(description.ast, out) + return out + } + case "Ref": { + const memo = arbitraryMemoMap.get(description.ast) + if (memo) { + return memo + } + throw new Error(`BUG: Ref ${JSON.stringify(description.id)} not found`) + } + } + } +) + +function subtractElementsLength( + constraints: FastCheck.ArrayConstraints, + len: number +): FastCheck.ArrayConstraints { + if (len === 0 || (constraints.minLength === undefined && constraints.maxLength === undefined)) { + return constraints + } + const out = { ...constraints } + if (out.minLength !== undefined) { + out.minLength = Math.max(out.minLength - len, 0) + } + if (out.maxLength !== undefined) { + out.maxLength = Math.max(out.maxLength - len, 0) + } + return out +} + +const getSuspendedArray = ( + fc: typeof FastCheck, + depthIdentifier: string, + maxDepth: number, + item: FastCheck.Arbitrary, + constraints: FastCheck.ArrayConstraints +) => { + // In the context of a recursive schema, we don't want a `maxLength` greater than 2. + // The only exception is when `minLength` is also set, in which case we set + // `maxLength` to the minimum value, which is `minLength`. + const maxLengthLimit = Math.max(2, constraints.minLength ?? 0) + if (constraints.maxLength !== undefined && constraints.maxLength > maxLengthLimit) { + constraints = { ...constraints, maxLength: maxLengthLimit } + } + return fc.oneof( + { maxDepth, depthIdentifier }, + fc.constant([]), + fc.array(item, constraints) + ) +} diff --git a/repos/effect/packages/effect/src/Array.ts b/repos/effect/packages/effect/src/Array.ts new file mode 100644 index 0000000..c2a56e6 --- /dev/null +++ b/repos/effect/packages/effect/src/Array.ts @@ -0,0 +1,3590 @@ +/** + * This module provides utility functions for working with arrays in TypeScript. + * + * @since 2.0.0 + */ + +import * as Either from "./Either.js" +import * as Equal from "./Equal.js" +import * as Equivalence from "./Equivalence.js" +import type { LazyArg } from "./Function.js" +import { dual, identity } from "./Function.js" +import type { TypeLambda } from "./HKT.js" +import * as internalArray from "./internal/array.js" +import * as internalDoNotation from "./internal/doNotation.js" +import * as moduleIterable from "./Iterable.js" +import * as Option from "./Option.js" +import * as Order from "./Order.js" +import * as Predicate from "./Predicate.js" +import * as Record from "./Record.js" +import * as Tuple from "./Tuple.js" +import type { NoInfer, TupleOf } from "./Types.js" + +/** + * @category type lambdas + * @since 2.0.0 + */ +export interface ReadonlyArrayTypeLambda extends TypeLambda { + readonly type: ReadonlyArray +} + +/** + * @category models + * @since 2.0.0 + */ +export type NonEmptyReadonlyArray = readonly [A, ...Array] + +/** + * @category models + * @since 2.0.0 + */ +export type NonEmptyArray = [A, ...Array] + +/** + * Builds a `NonEmptyArray` from an non-empty collection of elements. + * + * **Example** + * + * ```ts + * import { Array } from "effect" + * + * const result = Array.make(1, 2, 3) + * console.log(result) // [1, 2, 3] + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const make = >( + ...elements: Elements +): NonEmptyArray => elements + +/** + * Creates a new `Array` of the specified length. + * + * **Example** + * + * ```ts + * import { Array } from "effect" + * + * const result = Array.allocate(3) + * console.log(result) // [ <3 empty items> ] + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const allocate = (n: number): Array => new Array(n) + +/** + * Return a `NonEmptyArray` of length `n` with element `i` initialized with `f(i)`. + * + * **Note**. `n` is normalized to an integer >= 1. + * + * **Example** + * + * ```ts + * import { makeBy } from "effect/Array" + * + * const result = makeBy(5, n => n * 2) + * console.log(result) // [0, 2, 4, 6, 8] + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const makeBy: { + (f: (i: number) => A): (n: number) => NonEmptyArray + (n: number, f: (i: number) => A): NonEmptyArray +} = dual(2, (n: number, f: (i: number) => A) => { + const max = Math.max(1, Math.floor(n)) + const out = new Array(max) + for (let i = 0; i < max; i++) { + out[i] = f(i) + } + return out as NonEmptyArray +}) + +/** + * Return a `NonEmptyArray` containing a range of integers, including both endpoints. + * + * **Example** + * + * ```ts + * import { range } from "effect/Array" + * + * const result = range(1, 3) + * console.log(result) // [1, 2, 3] + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const range = (start: number, end: number): NonEmptyArray => + start <= end ? makeBy(end - start + 1, (i) => start + i) : [start] + +/** + * Return a `NonEmptyArray` containing a value repeated the specified number of times. + * + * **Note**. `n` is normalized to an integer >= 1. + * + * **Example** + * + * ```ts + * import { Array } from "effect" + * + * const result = Array.replicate("a", 3) + * console.log(result) // ["a", "a", "a"] + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const replicate: { + (n: number): (a: A) => NonEmptyArray + (a: A, n: number): NonEmptyArray +} = dual(2, (a: A, n: number): NonEmptyArray => makeBy(n, () => a)) + +/** + * Creates a new `Array` from an iterable collection of values. + * If the input is already an array, it returns the input as-is. + * Otherwise, it converts the iterable collection to an array. + * + * **Example** + * + * ```ts + * import { Array } from "effect" + * + * const result = Array.fromIterable(new Set([1, 2, 3])) + * console.log(result) // [1, 2, 3] + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const fromIterable = (collection: Iterable): Array => + Array.isArray(collection) ? collection : Array.from(collection) + +/** + * Creates a new `Array` from a value that might not be an iterable. + * + * **Example** + * + * ```ts + * import { Array } from "effect" + * + * console.log(Array.ensure("a")) // ["a"] + * console.log(Array.ensure(["a"])) // ["a"] + * console.log(Array.ensure(["a", "b", "c"])) // ["a", "b", "c"] + * ``` + * + * @category constructors + * @since 3.3.0 + */ +export const ensure = (self: ReadonlyArray | A): Array => Array.isArray(self) ? self : [self as A] + +/** + * Takes a record and returns an array of tuples containing its keys and values. + * + * **Example** + * + * ```ts + * import { Array } from "effect" + * + * const result = Array.fromRecord({ a: 1, b: 2, c: 3 }) + * console.log(result) // [["a", 1], ["b", 2], ["c", 3]] + * ``` + * + * @category conversions + * @since 2.0.0 + */ +export const fromRecord: (self: Readonly>) => Array<[K, A]> = Record.toEntries + +/** + * Converts an `Option` to an array. + * + * **Example** + * + * ```ts + * import { Array, Option } from "effect" + * + * console.log(Array.fromOption(Option.some(1))) // [1] + * console.log(Array.fromOption(Option.none())) // [] + * ``` + * + * @category conversions + * @since 2.0.0 + */ +export const fromOption: (self: Option.Option) => Array = Option.toArray + +/** + * Matches the elements of an array, applying functions to cases of empty and non-empty arrays. + * + * **Example** + * + * ```ts + * import { Array } from "effect" + * + * const match = Array.match({ + * onEmpty: () => "empty", + * onNonEmpty: ([head, ...tail]) => `head: ${head}, tail: ${tail.length}` + * }) + * console.log(match([])) // "empty" + * console.log(match([1, 2, 3])) // "head: 1, tail: 2" + * ``` + * + * @category pattern matching + * @since 2.0.0 + */ +export const match: { + ( + options: { + readonly onEmpty: LazyArg + readonly onNonEmpty: (self: NonEmptyReadonlyArray) => C + } + ): (self: ReadonlyArray) => B | C + ( + self: ReadonlyArray, + options: { + readonly onEmpty: LazyArg + readonly onNonEmpty: (self: NonEmptyReadonlyArray) => C + } + ): B | C +} = dual(2, ( + self: ReadonlyArray, + { onEmpty, onNonEmpty }: { + readonly onEmpty: LazyArg + readonly onNonEmpty: (self: NonEmptyReadonlyArray) => C + } +): B | C => isNonEmptyReadonlyArray(self) ? onNonEmpty(self) : onEmpty()) + +/** + * Matches the elements of an array from the left, applying functions to cases of empty and non-empty arrays. + * + * **Example** + * + * ```ts + * import { Array } from "effect" + * + * const matchLeft = Array.matchLeft({ + * onEmpty: () => "empty", + * onNonEmpty: (head, tail) => `head: ${head}, tail: ${tail.length}` + * }) + * console.log(matchLeft([])) // "empty" + * console.log(matchLeft([1, 2, 3])) // "head: 1, tail: 2" + * ``` + * + * @category pattern matching + * @since 2.0.0 + */ +export const matchLeft: { + ( + options: { + readonly onEmpty: LazyArg + readonly onNonEmpty: (head: A, tail: Array) => C + } + ): (self: ReadonlyArray) => B | C + ( + self: ReadonlyArray, + options: { + readonly onEmpty: LazyArg + readonly onNonEmpty: (head: A, tail: Array) => C + } + ): B | C +} = dual(2, ( + self: ReadonlyArray, + { onEmpty, onNonEmpty }: { + readonly onEmpty: LazyArg + readonly onNonEmpty: (head: A, tail: Array) => C + } +): B | C => isNonEmptyReadonlyArray(self) ? onNonEmpty(headNonEmpty(self), tailNonEmpty(self)) : onEmpty()) + +/** + * Matches the elements of an array from the right, applying functions to cases of empty and non-empty arrays. + * + * **Example** + * + * ```ts + * import { Array } from "effect" + * + * const matchRight = Array.matchRight({ + * onEmpty: () => "empty", + * onNonEmpty: (init, last) => `init: ${init.length}, last: ${last}` + * }) + * console.log(matchRight([])) // "empty" + * console.log(matchRight([1, 2, 3])) // "init: 2, last: 3" + * ``` + * + * @category pattern matching + * @since 2.0.0 + */ +export const matchRight: { + ( + options: { + readonly onEmpty: LazyArg + readonly onNonEmpty: (init: Array, last: A) => C + } + ): (self: ReadonlyArray) => B | C + ( + self: ReadonlyArray, + options: { + readonly onEmpty: LazyArg + readonly onNonEmpty: (init: Array, last: A) => C + } + ): B | C +} = dual(2, ( + self: ReadonlyArray, + { onEmpty, onNonEmpty }: { + readonly onEmpty: LazyArg + readonly onNonEmpty: (init: Array, last: A) => C + } +): B | C => + isNonEmptyReadonlyArray(self) ? + onNonEmpty(initNonEmpty(self), lastNonEmpty(self)) : + onEmpty()) + +/** + * Prepend an element to the front of an `Iterable`, creating a new `NonEmptyArray`. + * + * **Example** + * + * ```ts + * import { Array } from "effect" + * + * const result = Array.prepend([2, 3, 4], 1) + * console.log(result) // [1, 2, 3, 4] + * ``` + * + * @category concatenating + * @since 2.0.0 + */ +export const prepend: { + (head: B): (self: Iterable) => NonEmptyArray + (self: Iterable, head: B): NonEmptyArray +} = dual(2, (self: Iterable, head: B): NonEmptyArray => [head, ...self]) + +/** + * Prepends the specified prefix array (or iterable) to the beginning of the specified array (or iterable). + * If either array is non-empty, the result is also a non-empty array. + * + * **Example** + * + * ```ts + * import { Array } from "effect" + * + * const result = Array.prependAll([2, 3], [0, 1]) + * console.log(result) // [0, 1, 2, 3] + * ``` + * + * @category concatenating + * @since 2.0.0 + */ +export const prependAll: { + , T extends Iterable>( + that: T + ): (self: S) => ReadonlyArray.OrNonEmpty | ReadonlyArray.Infer> + (self: Iterable, that: NonEmptyReadonlyArray): NonEmptyArray + (self: NonEmptyReadonlyArray, that: Iterable): NonEmptyArray + (self: Iterable, that: Iterable): Array +} = dual( + 2, + (self: Iterable, that: Iterable): Array => fromIterable(that).concat(fromIterable(self)) +) + +/** + * Append an element to the end of an `Iterable`, creating a new `NonEmptyArray`. + * + * **Example** + * + * ```ts + * import { Array } from "effect" + * + * const result = Array.append([1, 2, 3], 4); + * console.log(result) // [1, 2, 3, 4] + * ``` + * + * @category concatenating + * @since 2.0.0 + */ +export const append: { + (last: B): (self: Iterable) => NonEmptyArray + (self: Iterable, last: B): NonEmptyArray +} = dual(2, (self: Iterable, last: B): Array => [...self, last]) + +/** + * Concatenates two arrays (or iterables), combining their elements. + * If either array is non-empty, the result is also a non-empty array. + * + * @category concatenating + * @since 2.0.0 + */ +export const appendAll: { + , T extends Iterable>( + that: T + ): (self: S) => ReadonlyArray.OrNonEmpty | ReadonlyArray.Infer> + (self: Iterable, that: NonEmptyReadonlyArray): NonEmptyArray + (self: NonEmptyReadonlyArray, that: Iterable): NonEmptyArray + (self: Iterable, that: Iterable): Array +} = dual( + 2, + (self: Iterable, that: Iterable): Array => fromIterable(self).concat(fromIterable(that)) +) + +/** + * Accumulates values from an `Iterable` starting from the left, storing + * each intermediate result in an array. Useful for tracking the progression of + * a value through a series of transformations. + * + * **Example** + * + * ```ts + * import { Array } from "effect"; + * + * const result = Array.scan([1, 2, 3, 4], 0, (acc, value) => acc + value) + * console.log(result) // [0, 1, 3, 6, 10] + * + * // Explanation: + * // This function starts with the initial value (0 in this case) + * // and adds each element of the array to this accumulator one by one, + * // keeping track of the cumulative sum after each addition. + * // Each of these sums is captured in the resulting array. + * ``` + * + * @category folding + * @since 2.0.0 + */ +export const scan: { + (b: B, f: (b: B, a: A) => B): (self: Iterable) => NonEmptyArray + (self: Iterable, b: B, f: (b: B, a: A) => B): NonEmptyArray +} = dual(3, (self: Iterable, b: B, f: (b: B, a: A) => B): NonEmptyArray => { + const out: NonEmptyArray = [b] + let i = 0 + for (const a of self) { + out[i + 1] = f(out[i], a) + i++ + } + return out +}) + +/** + * Accumulates values from an `Iterable` starting from the right, storing + * each intermediate result in an array. Useful for tracking the progression of + * a value through a series of transformations. + * + * **Example** + * + * ```ts + * import { Array } from "effect"; + * + * const result = Array.scanRight([1, 2, 3, 4], 0, (acc, value) => acc + value) + * console.log(result) // [10, 9, 7, 4, 0] + * ``` + * + * @category folding + * @since 2.0.0 + */ +export const scanRight: { + (b: B, f: (b: B, a: A) => B): (self: Iterable) => NonEmptyArray + (self: Iterable, b: B, f: (b: B, a: A) => B): NonEmptyArray +} = dual(3, (self: Iterable, b: B, f: (b: B, a: A) => B): NonEmptyArray => { + const input = fromIterable(self) + const out: NonEmptyArray = new Array(input.length + 1) as any + out[input.length] = b + for (let i = input.length - 1; i >= 0; i--) { + out[i] = f(out[i + 1], input[i]) + } + return out +}) + +/** + * Determine if `unknown` is an Array. + * + * **Example** + * + * ```ts + * import { Array } from "effect" + * + * console.log(Array.isArray(null)) // false + * console.log(Array.isArray([1, 2, 3])) // true + * ``` + * + * @category guards + * @since 2.0.0 + */ +export const isArray: { + (self: unknown): self is Array + (self: T): self is Extract> +} = Array.isArray + +/** + * Determine if an `Array` is empty narrowing down the type to `[]`. + * + * **Example** + * + * ```ts + * import { Array } from "effect" + * + * console.log(Array.isEmptyArray([])) // true + * console.log(Array.isEmptyArray([1, 2, 3])) // false + * ``` + * + * @category guards + * @since 2.0.0 + */ +export const isEmptyArray = (self: Array): self is [] => self.length === 0 + +/** + * Determine if a `ReadonlyArray` is empty narrowing down the type to `readonly []`. + * + * **Example** + * + * ```ts + * import { Array } from "effect" + * + * console.log(Array.isEmptyReadonlyArray([])) // true + * console.log(Array.isEmptyReadonlyArray([1, 2, 3])) // false + * ``` + * + * @category guards + * @since 2.0.0 + */ +export const isEmptyReadonlyArray: (self: ReadonlyArray) => self is readonly [] = isEmptyArray as any + +/** + * Determine if an `Array` is non empty narrowing down the type to `NonEmptyArray`. + * + * An `Array` is considered to be a `NonEmptyArray` if it contains at least one element. + * + * **Example** + * + * ```ts + * import { Array } from "effect" + * + * console.log(Array.isNonEmptyArray([])) // false + * console.log(Array.isNonEmptyArray([1, 2, 3])) // true + * ``` + * + * @category guards + * @since 2.0.0 + */ +export const isNonEmptyArray: (self: Array) => self is NonEmptyArray = internalArray.isNonEmptyArray + +/** + * Determine if a `ReadonlyArray` is non empty narrowing down the type to `NonEmptyReadonlyArray`. + * + * A `ReadonlyArray` is considered to be a `NonEmptyReadonlyArray` if it contains at least one element. + * + * **Example** + * + * ```ts + * import { Array } from "effect" + * + * console.log(Array.isNonEmptyReadonlyArray([])) // false + * console.log(Array.isNonEmptyReadonlyArray([1, 2, 3])) // true + * ``` + * + * @category guards + * @since 2.0.0 + */ +export const isNonEmptyReadonlyArray: (self: ReadonlyArray) => self is NonEmptyReadonlyArray = + internalArray.isNonEmptyArray + +/** + * Return the number of elements in a `ReadonlyArray`. + * + * @category getters + * @since 2.0.0 + */ +export const length = (self: ReadonlyArray): number => self.length + +const isOutOfBounds = (i: number, as: ReadonlyArray): boolean => i < 0 || i >= as.length + +const clamp = (i: number, as: ReadonlyArray): number => Math.floor(Math.min(Math.max(0, i), as.length)) + +/** + * This function provides a safe way to read a value at a particular index from a `ReadonlyArray`. + * + * @category getters + * @since 2.0.0 + */ +export const get: { + (index: number): (self: ReadonlyArray) => Option.Option + (self: ReadonlyArray, index: number): Option.Option +} = dual(2, (self: ReadonlyArray, index: number): Option.Option => { + const i = Math.floor(index) + return isOutOfBounds(i, self) ? Option.none() : Option.some(self[i]) +}) + +/** + * Gets an element unsafely, will throw on out of bounds. + * + * @since 2.0.0 + * @category unsafe + */ +export const unsafeGet: { + (index: number): (self: ReadonlyArray) => A + (self: ReadonlyArray, index: number): A +} = dual(2, (self: ReadonlyArray, index: number): A => { + const i = Math.floor(index) + if (isOutOfBounds(i, self)) { + throw new Error(`Index ${i} out of bounds`) + } + return self[i] +}) + +/** + * Return a tuple containing the first element, and a new `Array` of the remaining elements, if any. + * + * **Example** + * + * ```ts + * import { Array } from "effect"; + * + * const result = Array.unprepend([1, 2, 3, 4]) + * console.log(result) // [1, [2, 3, 4]] + * ``` + * + * @category splitting + * @since 2.0.0 + */ +export const unprepend = ( + self: NonEmptyReadonlyArray +): [firstElement: A, remainingElements: Array] => [headNonEmpty(self), tailNonEmpty(self)] + +/** + * Return a tuple containing a copy of the `NonEmptyReadonlyArray` without its last element, and that last element. + * + * **Example** + * + * ```ts + * import { Array } from "effect"; + * + * const result = Array.unappend([1, 2, 3, 4]) + * console.log(result) // [[1, 2, 3], 4] + * ``` + * + * @category splitting + * @since 2.0.0 + */ +export const unappend = ( + self: NonEmptyReadonlyArray +): [arrayWithoutLastElement: Array, lastElement: A] => [initNonEmpty(self), lastNonEmpty(self)] + +/** + * Get the first element of a `ReadonlyArray`, or `None` if the `ReadonlyArray` is empty. + * + * @category getters + * @since 2.0.0 + */ +export const head: (self: ReadonlyArray) => Option.Option = get(0) + +/** + * Get the first element of a non empty array. + * + * **Example** + * + * ```ts + * import { Array } from "effect" + * + * const result = Array.headNonEmpty([1, 2, 3, 4]) + * console.log(result) // 1 + * ``` + * + * @category getters + * @since 2.0.0 + */ +export const headNonEmpty: (self: NonEmptyReadonlyArray) => A = unsafeGet(0) + +/** + * Get the last element in a `ReadonlyArray`, or `None` if the `ReadonlyArray` is empty. + * + * @category getters + * @since 2.0.0 + */ +export const last = (self: ReadonlyArray): Option.Option => + isNonEmptyReadonlyArray(self) ? Option.some(lastNonEmpty(self)) : Option.none() + +/** + * Get the last element of a non empty array. + * + * **Example** + * + * ```ts + * import { Array } from "effect" + * + * const result = Array.lastNonEmpty([1, 2, 3, 4]) + * console.log(result) // 4 + * ``` + * + * @category getters + * @since 2.0.0 + */ +export const lastNonEmpty = (self: NonEmptyReadonlyArray): A => self[self.length - 1] + +/** + * Get all but the first element of an `Iterable`, creating a new `Array`, or `None` if the `Iterable` is empty. + * + * @category getters + * @since 2.0.0 + */ +export const tail = (self: Iterable): Option.Option> => { + const input = fromIterable(self) + return isNonEmptyReadonlyArray(input) ? Option.some(tailNonEmpty(input)) : Option.none() +} + +/** + * Get all but the first element of a `NonEmptyReadonlyArray`. + * + * **Example** + * + * ```ts + * import { Array } from "effect" + * + * const result = Array.tailNonEmpty([1, 2, 3, 4]) + * console.log(result) // [2, 3, 4] + * ``` + * + * @category getters + * @since 2.0.0 + */ +export const tailNonEmpty = (self: NonEmptyReadonlyArray): Array => self.slice(1) + +/** + * Get all but the last element of an `Iterable`, creating a new `Array`, or `None` if the `Iterable` is empty. + * + * @category getters + * @since 2.0.0 + */ +export const init = (self: Iterable): Option.Option> => { + const input = fromIterable(self) + return isNonEmptyReadonlyArray(input) ? Option.some(initNonEmpty(input)) : Option.none() +} + +/** + * Get all but the last element of a non empty array, creating a new array. + * + * **Example** + * + * ```ts + * import { Array } from "effect" + * + * const result = Array.initNonEmpty([1, 2, 3, 4]) + * console.log(result) // [1, 2, 3] + * ``` + * + * @category getters + * @since 2.0.0 + */ +export const initNonEmpty = (self: NonEmptyReadonlyArray): Array => self.slice(0, -1) + +/** + * Keep only a max number of elements from the start of an `Iterable`, creating a new `Array`. + * + * **Note**. `n` is normalized to a non negative integer. + * + * **Example** + * + * ```ts + * import { Array } from "effect" + * + * const result = Array.take([1, 2, 3, 4, 5], 3) + * console.log(result) // [1, 2, 3] + * ``` + * + * @category getters + * @since 2.0.0 + */ +export const take: { + (n: number): (self: Iterable) => Array + (self: Iterable, n: number): Array +} = dual(2, (self: Iterable, n: number): Array => { + const input = fromIterable(self) + return input.slice(0, clamp(n, input)) +}) + +/** + * Keep only a max number of elements from the end of an `Iterable`, creating a new `Array`. + * + * **Note**. `n` is normalized to a non negative integer. + * + * **Example** + * + * ```ts + * import { Array } from "effect" + * + * const result = Array.takeRight([1, 2, 3, 4, 5], 3) + * console.log(result) // [3, 4, 5] + * ``` + * + * @category getters + * @since 2.0.0 + */ +export const takeRight: { + (n: number): (self: Iterable) => Array + (self: Iterable, n: number): Array +} = dual(2, (self: Iterable, n: number): Array => { + const input = fromIterable(self) + const i = clamp(n, input) + return i === 0 ? [] : input.slice(-i) +}) + +/** + * Calculate the longest initial subarray for which all element satisfy the specified predicate, creating a new `Array`. + * + * **Example** + * + * ```ts + * import { Array } from "effect" + * + * const result = Array.takeWhile([1, 3, 2, 4, 1, 2], x => x < 4) + * console.log(result) // [1, 3, 2] + * + * // Explanation: + * // - The function starts with the first element (`1`), which is less than `4`, so it adds `1` to the result. + * // - The next element (`3`) is also less than `4`, so it adds `3`. + * // - The next element (`2`) is again less than `4`, so it adds `2`. + * // - The function then encounters `4`, which is not less than `4`. At this point, it stops checking further elements and finalizes the result. + * ``` + * + * @category getters + * @since 2.0.0 + */ +export const takeWhile: { + (refinement: (a: NoInfer, i: number) => a is B): (self: Iterable) => Array + (predicate: (a: NoInfer, i: number) => boolean): (self: Iterable) => Array + (self: Iterable, refinement: (a: A, i: number) => a is B): Array + (self: Iterable, predicate: (a: A, i: number) => boolean): Array +} = dual(2, (self: Iterable, predicate: (a: A, i: number) => boolean): Array => { + let i = 0 + const out: Array = [] + for (const a of self) { + if (!predicate(a, i)) { + break + } + out.push(a) + i++ + } + return out +}) + +const spanIndex = (self: Iterable, predicate: (a: A, i: number) => boolean): number => { + let i = 0 + for (const a of self) { + if (!predicate(a, i)) { + break + } + i++ + } + return i +} + +/** + * Split an `Iterable` into two parts: + * + * 1. the longest initial subarray for which all elements satisfy the specified predicate + * 2. the remaining elements + * + * @category splitting + * @since 2.0.0 + */ +export const span: { + ( + refinement: (a: NoInfer, i: number) => a is B + ): (self: Iterable) => [init: Array, rest: Array>] + (predicate: (a: NoInfer, i: number) => boolean): (self: Iterable) => [init: Array, rest: Array] + ( + self: Iterable, + refinement: (a: A, i: number) => a is B + ): [init: Array, rest: Array>] + (self: Iterable, predicate: (a: A, i: number) => boolean): [init: Array, rest: Array] +} = dual( + 2, + (self: Iterable, predicate: (a: A, i: number) => boolean): [init: Array, rest: Array] => + splitAt(self, spanIndex(self, predicate)) +) + +/** + * Drop a max number of elements from the start of an `Iterable`, creating a new `Array`. + * + * **Note**. `n` is normalized to a non negative integer. + * + * **Example** + * + * ```ts + * import { Array } from "effect" + * + * const result = Array.drop([1, 2, 3, 4, 5], 2) + * console.log(result) // [3, 4, 5] + * ``` + * + * @category getters + * @since 2.0.0 + */ +export const drop: { + (n: number): (self: Iterable) => Array + (self: Iterable, n: number): Array +} = dual(2, (self: Iterable, n: number): Array => { + const input = fromIterable(self) + return input.slice(clamp(n, input), input.length) +}) + +/** + * Drop a max number of elements from the end of an `Iterable`, creating a new `Array`. + * + * **Note**. `n` is normalized to a non negative integer. + * + * **Example** + * + * ```ts + * import { Array } from "effect" + * + * const result = Array.dropRight([1, 2, 3, 4, 5], 2) + * console.log(result) // [1, 2, 3] + * ``` + * + * @category getters + * @since 2.0.0 + */ +export const dropRight: { + (n: number): (self: Iterable) => Array + (self: Iterable, n: number): Array +} = dual(2, (self: Iterable, n: number): Array => { + const input = fromIterable(self) + return input.slice(0, input.length - clamp(n, input)) +}) + +/** + * Remove the longest initial subarray for which all element satisfy the specified predicate, creating a new `Array`. + * + * **Example** + * + * ```ts + * import { Array } from "effect" + * + * const result = Array.dropWhile([1, 2, 3, 4, 5], x => x < 4) + * console.log(result) // [4, 5] + * ``` + * + * @category getters + * @since 2.0.0 + */ +export const dropWhile: { + (predicate: (a: NoInfer, i: number) => boolean): (self: Iterable) => Array + (self: Iterable, predicate: (a: A, i: number) => boolean): Array +} = dual( + 2, + (self: Iterable, predicate: (a: A, i: number) => boolean): Array => + fromIterable(self).slice(spanIndex(self, predicate)) +) + +/** + * Return the first index for which a predicate holds. + * + * **Example** + * + * ```ts + * import { Array } from "effect" + * + * const result = Array.findFirstIndex([5, 3, 8, 9], x => x > 5) + * console.log(result) // Option.some(2) + * ``` + * + * @category elements + * @since 2.0.0 + */ +export const findFirstIndex: { + (predicate: (a: NoInfer, i: number) => boolean): (self: Iterable) => Option.Option + (self: Iterable, predicate: (a: A, i: number) => boolean): Option.Option +} = dual(2, (self: Iterable, predicate: (a: A, i: number) => boolean): Option.Option => { + let i = 0 + for (const a of self) { + if (predicate(a, i)) { + return Option.some(i) + } + i++ + } + return Option.none() +}) + +/** + * Return the last index for which a predicate holds. + * + * **Example** + * + * ```ts + * import { Array } from "effect" + * + * const result = Array.findLastIndex([1, 3, 8, 9], x => x < 5) + * console.log(result) // Option.some(1) + * ``` + * + * @category elements + * @since 2.0.0 + */ +export const findLastIndex: { + (predicate: (a: NoInfer, i: number) => boolean): (self: Iterable) => Option.Option + (self: Iterable, predicate: (a: A, i: number) => boolean): Option.Option +} = dual(2, (self: Iterable, predicate: (a: A, i: number) => boolean): Option.Option => { + const input = fromIterable(self) + for (let i = input.length - 1; i >= 0; i--) { + if (predicate(input[i], i)) { + return Option.some(i) + } + } + return Option.none() +}) + +/** + * Returns the first element that satisfies the specified + * predicate, or `None` if no such element exists. + * + * **Example** + * + * ```ts + * import { Array } from "effect" + * + * const result = Array.findFirst([1, 2, 3, 4, 5], x => x > 3) + * console.log(result) // Option.some(4) + * ``` + * + * @category elements + * @since 2.0.0 + */ +export const findFirst: { + (f: (a: NoInfer, i: number) => Option.Option): (self: Iterable) => Option.Option + (refinement: (a: NoInfer, i: number) => a is B): (self: Iterable) => Option.Option + (predicate: (a: NoInfer, i: number) => boolean): (self: Iterable) => Option.Option + (self: Iterable, f: (a: A, i: number) => Option.Option): Option.Option + (self: Iterable, refinement: (a: A, i: number) => a is B): Option.Option + (self: Iterable, predicate: (a: A, i: number) => boolean): Option.Option +} = moduleIterable.findFirst + +/** + * Finds the last element in an iterable collection that satisfies the given predicate or refinement. + * Returns an `Option` containing the found element, or `Option.none` if no element matches. + * + * **Example** + * + * ```ts + * import { Array } from "effect" + * + * const result = Array.findLast([1, 2, 3, 4, 5], n => n % 2 === 0) + * console.log(result) // Option.some(4) + * ``` + * + * @category elements + * @since 2.0.0 + */ +export const findLast: { + (f: (a: NoInfer, i: number) => Option.Option): (self: Iterable) => Option.Option + (refinement: (a: NoInfer, i: number) => a is B): (self: Iterable) => Option.Option + (predicate: (a: NoInfer, i: number) => boolean): (self: Iterable) => Option.Option + (self: Iterable, f: (a: A, i: number) => Option.Option): Option.Option + (self: Iterable, refinement: (a: A, i: number) => a is B): Option.Option + (self: Iterable, predicate: (a: A, i: number) => boolean): Option.Option +} = dual( + 2, + ( + self: Iterable, + f: ((a: A, i: number) => boolean) | ((a: A, i: number) => Option.Option) + ): Option.Option => { + const input = fromIterable(self) + for (let i = input.length - 1; i >= 0; i--) { + const a = input[i] + const o = f(a, i) + if (Predicate.isBoolean(o)) { + if (o) { + return Option.some(a) + } + } else { + if (Option.isSome(o)) { + return o + } + } + } + return Option.none() + } +) + +/** + * Returns a tuple of the first element that satisfies the specified + * predicate and its index, or `None` if no such element exists. + * + * **Example** + * + * ```ts + * import { Array } from "effect" + * + * const result = Array.findFirstWithIndex([1, 2, 3, 4, 5], x => x > 3) + * console.log(result) // Option.some([4, 3]) + * ``` + * + * @category elements + * @since 3.17.0 + */ +export const findFirstWithIndex: { + (f: (a: NoInfer, i: number) => Option.Option): (self: Iterable) => Option.Option<[B, number]> + (refinement: (a: NoInfer, i: number) => a is B): (self: Iterable) => Option.Option<[B, number]> + (predicate: (a: NoInfer, i: number) => boolean): (self: Iterable) => Option.Option<[A, number]> + (self: Iterable, f: (a: A, i: number) => Option.Option): Option.Option<[B, number]> + (self: Iterable, refinement: (a: A, i: number) => a is B): Option.Option<[B, number]> + (self: Iterable, predicate: (a: A, i: number) => boolean): Option.Option<[A, number]> +} = dual( + 2, + ( + self: Iterable, + f: ((a: A, i: number) => boolean) | ((a: A, i: number) => Option.Option) + ): Option.Option<[A, number]> => { + let i = 0 + for (const a of self) { + const o = f(a, i) + if (Predicate.isBoolean(o)) { + if (o) { + return Option.some([a, i]) + } + } else { + if (Option.isSome(o)) { + return Option.some([o.value, i]) + } + } + i++ + } + return Option.none() + } +) + +/** + * Counts all the element of the given array that pass the given predicate + * + * **Example** + * + * ```ts + * import { Array } from "effect" + * + * const result = Array.countBy([1, 2, 3, 4, 5], n => n % 2 === 0) + * console.log(result) // 2 + * ``` + * + * @category folding + * @since 3.16.0 + */ +export const countBy: { + (predicate: (a: NoInfer, i: number) => boolean): (self: Iterable) => number + (self: Iterable, predicate: (a: A, i: number) => boolean): number +} = dual( + 2, + ( + self: Iterable, + f: (a: A, i: number) => boolean + ): number => { + let count = 0 + const as = fromIterable(self) + for (let i = 0; i < as.length; i++) { + const a = as[i] + if (f(a, i)) { + count++ + } + } + return count + } +) + +/** + * Insert an element at the specified index, creating a new `NonEmptyArray`, + * or return `None` if the index is out of bounds. + * + * **Example** + * + * ```ts + * import { Array } from "effect" + * + * const result = Array.insertAt(['a', 'b', 'c', 'e'], 3, 'd') + * console.log(result) // Option.some(['a', 'b', 'c', 'd', 'e']) + * ``` + * + * @since 2.0.0 + */ +export const insertAt: { + (i: number, b: B): (self: Iterable) => Option.Option> + (self: Iterable, i: number, b: B): Option.Option> +} = dual(3, (self: Iterable, i: number, b: B): Option.Option> => { + const out: Array = Array.from(self) + // v--- `= self.length` is ok, it means inserting in last position + if (i < 0 || i > out.length) { + return Option.none() + } + out.splice(i, 0, b) + return Option.some(out) as any +}) + +/** + * Change the element at the specified index, creating a new `Array`, + * or return a copy of the input if the index is out of bounds. + * + * **Example** + * + * ```ts + * import { Array } from "effect" + * + * const result = Array.replace(['a', 'b', 'c', 'd'], 1, 'z') + * console.log(result) // ['a', 'z', 'c', 'd'] + * ``` + * + * @since 2.0.0 + */ +export const replace: { + ( + i: number, + b: B + ): = Iterable>( + self: S + ) => ReadonlyArray.With | B> + = Iterable>( + self: S, + i: number, + b: B + ): ReadonlyArray.With | B> +} = dual(3, (self: Iterable, i: number, b: B): Array => modify(self, i, () => b)) + +/** + * Replaces an element in an array with the given value, returning an option of the updated array. + * + * **Example** + * + * ```ts + * import { Array } from "effect" + * + * const result = Array.replaceOption([1, 2, 3], 1, 4) + * console.log(result) // Option.some([1, 4, 3]) + * ``` + * + * @since 2.0.0 + */ +export const replaceOption: { + ( + i: number, + b: B + ): = Iterable>( + self: S + ) => Option.Option | B>> + = Iterable>( + self: S, + i: number, + b: B + ): Option.Option | B>> +} = dual( + 3, + (self: Iterable, i: number, b: B): Option.Option> => modifyOption(self, i, () => b) +) + +/** + * Apply a function to the element at the specified index, creating a new `Array`, + * or return a copy of the input if the index is out of bounds. + * + * **Example** + * + * ```ts + * import { Array } from "effect" + * + * const result = Array.modify([1, 2, 3, 4], 2, (n) => n * 2) + * console.log(result) // [1, 2, 6, 4] + * ``` + * + * @since 2.0.0 + */ +export const modify: { + = Iterable>( + i: number, + f: (a: ReadonlyArray.Infer) => B + ): (self: S) => ReadonlyArray.With | B> + = Iterable>( + self: S, + i: number, + f: (a: ReadonlyArray.Infer) => B + ): ReadonlyArray.With | B> +} = dual( + 3, + (self: Iterable, i: number, f: (a: A) => B): Array => { + const out: Array = Array.from(self) + if (isOutOfBounds(i, out)) { + return out + } + const b = f(out[i] as A) + out[i] = b + return out + } +) + +/** + * Apply a function to the element at the specified index, creating a new `Array`, + * or return `None` if the index is out of bounds. + * + * **Example** + * + * ```ts + * import { Array } from "effect" + * + * const input = [1, 2, 3, 4] + * const result = Array.modifyOption(input, 2, (n) => n * 2) + * console.log(result) // Option.some([1, 2, 6, 4]) + * + * const outOfBoundsResult = Array.modifyOption(input, 5, (n) => n * 2) + * console.log(outOfBoundsResult) // Option.none() + * ``` + * + * @since 2.0.0 + */ +export const modifyOption: { + = Iterable>( + i: number, + f: (a: ReadonlyArray.Infer) => B + ): (self: S) => Option.Option | B>> + = Iterable>( + self: S, + i: number, + f: (a: ReadonlyArray.Infer) => B + ): Option.Option | B>> +} = dual(3, (self: Iterable, i: number, f: (a: A) => B): Option.Option> => { + const arr = fromIterable(self) + if (isOutOfBounds(i, arr)) { + return Option.none() + } + const out: Array = Array.isArray(self) ? self.slice() : arr + const b = f(arr[i]) + out[i] = b + return Option.some(out) +}) + +/** + * Delete the element at the specified index, creating a new `Array`, + * or return a copy of the input if the index is out of bounds. + * + * **Example** + * + * ```ts + * import { Array } from "effect" + * + * const input = [1, 2, 3, 4] + * const result = Array.remove(input, 2) + * console.log(result) // [1, 2, 4] + * + * const outOfBoundsResult = Array.remove(input, 5) + * console.log(outOfBoundsResult) // [1, 2, 3, 4] + * ``` + * + * @since 2.0.0 + */ +export const remove: { + (i: number): (self: Iterable) => Array + (self: Iterable, i: number): Array +} = dual(2, (self: Iterable, i: number): Array => { + const out = Array.from(self) + if (isOutOfBounds(i, out)) { + return out + } + out.splice(i, 1) + return out +}) + +/** + * Delete the element at the specified index, creating a new `Array`, + * or return `None` if the index is out of bounds. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { Array, Option } from "effect" + * + * const numbers = [1, 2, 3, 4] + * const result = Array.removeOption(numbers, 2) + * assert.deepStrictEqual(result, Option.some([1, 2, 4])) + * + * const outOfBoundsResult = Array.removeOption(numbers, 5) + * assert.deepStrictEqual(outOfBoundsResult, Option.none()) + * ``` + * + * @since 3.16.0 + */ +export const removeOption: { + (i: number): (self: Iterable) => Option.Option> + (self: Iterable, i: number): Option.Option> +} = dual(2, (self: Iterable, i: number): Option.Option> => { + const arr = fromIterable(self) + if (isOutOfBounds(i, arr)) { + return Option.none() + } + const out = Array.isArray(self) ? self.slice() : arr + out.splice(i, 1) + return Option.some(out) +}) + +/** + * Reverse an `Iterable`, creating a new `Array`. + * + * **Example** + * + * ```ts + * import { Array } from "effect" + * + * const result = Array.reverse([1, 2, 3, 4]) + * console.log(result) // [4, 3, 2, 1] + * ``` + * + * @category elements + * @since 2.0.0 + */ +export const reverse = >( + self: S +): S extends NonEmptyReadonlyArray ? NonEmptyArray : S extends Iterable ? Array : never => + Array.from(self).reverse() as any + +/** + * Create a new array with elements sorted in increasing order based on the specified comparator. + * If the input is a `NonEmptyReadonlyArray`, the output will also be a `NonEmptyReadonlyArray`. + * + * @category sorting + * @since 2.0.0 + */ +export const sort: { + ( + O: Order.Order + ): >(self: S) => ReadonlyArray.With> + (self: NonEmptyReadonlyArray, O: Order.Order): NonEmptyArray + (self: Iterable, O: Order.Order): Array +} = dual(2, (self: Iterable, O: Order.Order): Array => { + const out = Array.from(self) + out.sort(O) + return out +}) + +/** + * Sorts an array based on a provided mapping function and order. The mapping + * function transforms the elements into a value that can be compared, and the + * order defines how those values should be sorted. + * + * **Example** + * + * ```ts + * import { Array, Order } from "effect" + * + * const result = Array.sortWith(["aaa", "b", "cc"], (s) => s.length, Order.number) + * console.log(result) // ["b", "cc", "aaa"] + * + * // Explanation: + * // The array of strings is sorted based on their lengths. The mapping function `(s) => s.length` + * // converts each string into its length, and the `Order.number` specifies that the lengths should + * // be sorted in ascending order. + * ``` + * + * @since 2.0.0 + * @category elements + */ +export const sortWith: { + , B>( + f: (a: ReadonlyArray.Infer) => B, + order: Order.Order + ): (self: S) => ReadonlyArray.With> + (self: NonEmptyReadonlyArray, f: (a: A) => B, O: Order.Order): NonEmptyArray + (self: Iterable, f: (a: A) => B, order: Order.Order): Array +} = dual( + 3, + (self: Iterable, f: (a: A) => B, order: Order.Order): Array => + Array.from(self).map((a) => [a, f(a)] as const).sort(([, a], [, b]) => order(a, b)).map(([_]) => _) +) + +/** + * Sorts the elements of an `Iterable` in increasing order based on the provided + * orders. The elements are compared using the first order in `orders`, then the + * second order if the first comparison is equal, and so on. + * + * **Example** + * + * ```ts + * import { Array, Order, pipe } from "effect" + * + * const users = [ + * { name: "Alice", age: 30 }, + * { name: "Bob", age: 25 }, + * { name: "Charlie", age: 30 } + * ] + * + * const result = pipe( + * users, + * Array.sortBy( + * Order.mapInput(Order.number, (user: (typeof users)[number]) => user.age), + * Order.mapInput(Order.string, (user: (typeof users)[number]) => user.name) + * ) + * ) + * + * console.log(result) + * // [ + * // { name: "Bob", age: 25 }, + * // { name: "Alice", age: 30 }, + * // { name: "Charlie", age: 30 } + * // ] + * + * // Explanation: + * // The array of users is sorted first by age in ascending order. When ages are equal, + * // the users are further sorted by name in ascending order. + * ``` + * + * @category sorting + * @since 2.0.0 + */ +export const sortBy = >( + ...orders: ReadonlyArray>> +) => { + const sortByAll = sort(Order.combineAll(orders)) + return ( + self: S + ): S extends NonEmptyReadonlyArray ? NonEmptyArray : S extends Iterable ? Array : never => { + const input = fromIterable(self) + if (isNonEmptyReadonlyArray(input)) { + return sortByAll(input) as any + } + return [] as any + } +} + +/** + * Takes two `Iterable`s and returns an `Array` of corresponding pairs. + * If one input `Iterable` is short, excess elements of the + * longer `Iterable` are discarded. + * + * **Example** + * + * ```ts + * import { Array } from "effect" + * + * const result = Array.zip([1, 2, 3], ['a', 'b']) + * console.log(result) // [[1, 'a'], [2, 'b']] + * ``` + * + * @category zipping + * @since 2.0.0 + */ +export const zip: { + (that: NonEmptyReadonlyArray): (self: NonEmptyReadonlyArray) => NonEmptyArray<[A, B]> + (that: Iterable): (self: Iterable) => Array<[A, B]> + (self: NonEmptyReadonlyArray, that: NonEmptyReadonlyArray): NonEmptyArray<[A, B]> + (self: Iterable, that: Iterable): Array<[A, B]> +} = dual( + 2, + (self: Iterable, that: Iterable): Array<[A, B]> => zipWith(self, that, Tuple.make) +) + +/** + * Apply a function to pairs of elements at the same index in two `Iterable`s, collecting the results in a new `Array`. If one + * input `Iterable` is short, excess elements of the longer `Iterable` are discarded. + * + * **Example** + * + * ```ts + * import { Array } from "effect" + * + * const result = Array.zipWith([1, 2, 3], [4, 5, 6], (a, b) => a + b) + * console.log(result) // [5, 7, 9] + * ``` + * + * @category zipping + * @since 2.0.0 + */ +export const zipWith: { + (that: NonEmptyReadonlyArray, f: (a: A, b: B) => C): (self: NonEmptyReadonlyArray) => NonEmptyArray + (that: Iterable, f: (a: A, b: B) => C): (self: Iterable) => Array + (self: NonEmptyReadonlyArray, that: NonEmptyReadonlyArray, f: (a: A, b: B) => C): NonEmptyArray + (self: Iterable, that: Iterable, f: (a: A, b: B) => C): Array +} = dual(3, (self: Iterable, that: Iterable, f: (a: A, b: B) => C): Array => { + const as = fromIterable(self) + const bs = fromIterable(that) + if (isNonEmptyReadonlyArray(as) && isNonEmptyReadonlyArray(bs)) { + const out: NonEmptyArray = [f(headNonEmpty(as), headNonEmpty(bs))] + const len = Math.min(as.length, bs.length) + for (let i = 1; i < len; i++) { + out[i] = f(as[i], bs[i]) + } + return out + } + return [] +}) + +/** + * This function is the inverse of `zip`. Takes an `Iterable` of pairs and return two corresponding `Array`s. + * + * **Example** + * + * ```ts + * import { Array } from "effect" + * + * const result = Array.unzip([[1, "a"], [2, "b"], [3, "c"]]) + * console.log(result) // [[1, 2, 3], ['a', 'b', 'c']] + * ``` + * + * @since 2.0.0 + */ +export const unzip: >( + self: S +) => S extends NonEmptyReadonlyArray ? [NonEmptyArray, NonEmptyArray] + : S extends Iterable ? [Array, Array] + : never = ((self: Iterable): [Array, Array] => { + const input = fromIterable(self) + if (isNonEmptyReadonlyArray(input)) { + const fa: NonEmptyArray = [input[0][0]] + const fb: NonEmptyArray = [input[0][1]] + for (let i = 1; i < input.length; i++) { + fa[i] = input[i][0] + fb[i] = input[i][1] + } + return [fa, fb] + } + return [[], []] + }) as any + +/** + * Places an element in between members of an `Iterable`. + * If the input is a non-empty array, the result is also a non-empty array. + * + * **Example** + * + * ```ts + * import { Array } from "effect" + * + * const result = Array.intersperse([1, 2, 3], 0) + * console.log(result) // [1, 0, 2, 0, 3] + * ``` + * + * @since 2.0.0 + */ +export const intersperse: { + ( + middle: B + ): >(self: S) => ReadonlyArray.With | B> + (self: NonEmptyReadonlyArray, middle: B): NonEmptyArray + (self: Iterable, middle: B): Array +} = dual(2, (self: Iterable, middle: B): Array => { + const input = fromIterable(self) + if (isNonEmptyReadonlyArray(input)) { + const out: NonEmptyArray = [headNonEmpty(input)] + const tail = tailNonEmpty(input) + for (let i = 0; i < tail.length; i++) { + if (i < tail.length) { + out.push(middle) + } + out.push(tail[i]) + } + return out + } + return [] +}) + +/** + * Apply a function to the head, creating a new `NonEmptyReadonlyArray`. + * + * **Example** + * + * ```ts + * import { Array } from "effect" + * + * const result = Array.modifyNonEmptyHead([1, 2, 3], n => n * 10) + * console.log(result) // [10, 2, 3] + * ``` + * + * @since 2.0.0 + */ +export const modifyNonEmptyHead: { + (f: (a: A) => B): (self: NonEmptyReadonlyArray) => NonEmptyArray + (self: NonEmptyReadonlyArray, f: (a: A) => B): NonEmptyArray +} = dual( + 2, + ( + self: NonEmptyReadonlyArray, + f: (a: A) => B + ): NonEmptyArray => [f(headNonEmpty(self)), ...tailNonEmpty(self)] +) + +/** + * Change the head, creating a new `NonEmptyReadonlyArray`. + * + * **Example** + * + * ```ts + * import { Array } from "effect" + * + * const result = Array.setNonEmptyHead([1, 2, 3], 10) + * console.log(result) // [10, 2, 3] + * ``` + * + * @since 2.0.0 + */ +export const setNonEmptyHead: { + (b: B): (self: NonEmptyReadonlyArray) => NonEmptyArray + (self: NonEmptyReadonlyArray, b: B): NonEmptyArray +} = dual( + 2, + (self: NonEmptyReadonlyArray, b: B): NonEmptyArray => modifyNonEmptyHead(self, () => b) +) + +/** + * Apply a function to the last element, creating a new `NonEmptyReadonlyArray`. + * + * **Example** + * + * ```ts + * import { Array } from "effect" + * + * const result = Array.modifyNonEmptyLast([1, 2, 3], n => n * 2) + * console.log(result) // [1, 2, 6] + * ``` + * + * @since 2.0.0 + */ +export const modifyNonEmptyLast: { + (f: (a: A) => B): (self: NonEmptyReadonlyArray) => NonEmptyArray + (self: NonEmptyReadonlyArray, f: (a: A) => B): NonEmptyArray +} = dual( + 2, + (self: NonEmptyReadonlyArray, f: (a: A) => B): NonEmptyArray => + append(initNonEmpty(self), f(lastNonEmpty(self))) +) + +/** + * Change the last element, creating a new `NonEmptyReadonlyArray`. + * + * **Example** + * + * ```ts + * import { Array } from "effect" + * + * const result = Array.setNonEmptyLast([1, 2, 3], 4) + * console.log(result) // [1, 2, 4] + * ``` + * + * @since 2.0.0 + */ +export const setNonEmptyLast: { + (b: B): (self: NonEmptyReadonlyArray) => NonEmptyArray + (self: NonEmptyReadonlyArray, b: B): NonEmptyArray +} = dual( + 2, + (self: NonEmptyReadonlyArray, b: B): NonEmptyArray => modifyNonEmptyLast(self, () => b) +) + +/** + * Rotate an `Iterable` by `n` steps. + * If the input is a non-empty array, the result is also a non-empty array. + * + * **Example** + * + * ```ts + * import { Array } from "effect" + * + * const result = Array.rotate(['a', 'b', 'c', 'd', 'e'], 2) + * console.log(result) // [ 'd', 'e', 'a', 'b', 'c' ] + * ``` + * + * @since 2.0.0 + */ +export const rotate: { + (n: number): >(self: S) => ReadonlyArray.With> + (self: NonEmptyReadonlyArray, n: number): NonEmptyArray + (self: Iterable, n: number): Array +} = dual(2, (self: Iterable, n: number): Array => { + const input = fromIterable(self) + if (isNonEmptyReadonlyArray(input)) { + const len = input.length + const m = Math.round(n) % len + if (isOutOfBounds(Math.abs(m), input) || m === 0) { + return copy(input) + } + if (m < 0) { + const [f, s] = splitNonEmptyAt(input, -m) + return appendAll(s, f) + } else { + return rotate(self, m - len) + } + } + return [] +}) + +/** + * Returns a function that checks if a `ReadonlyArray` contains a given value using a provided `isEquivalent` function. + * + * **Example** + * + * ```ts + * import { Array, pipe } from "effect" + * + * const isEquivalent = (a: number, b: number) => a === b + * const containsNumber = Array.containsWith(isEquivalent) + * const result = pipe([1, 2, 3, 4], containsNumber(3)) + * console.log(result) // true + * ``` + * + * @category elements + * @since 2.0.0 + */ +export const containsWith = (isEquivalent: (self: A, that: A) => boolean): { + (a: A): (self: Iterable) => boolean + (self: Iterable, a: A): boolean +} => + dual(2, (self: Iterable, a: A): boolean => { + for (const i of self) { + if (isEquivalent(a, i)) { + return true + } + } + return false + }) + +const _equivalence = Equal.equivalence() + +/** + * Returns a function that checks if a `ReadonlyArray` contains a given value using the default `Equivalence`. + * + * **Example** + * + * ```ts + * import { Array, pipe } from "effect" + * + * const result = pipe(['a', 'b', 'c', 'd'], Array.contains('c')) + * console.log(result) // true + * ``` + * + * @category elements + * @since 2.0.0 + */ +export const contains: { + (a: A): (self: Iterable) => boolean + (self: Iterable, a: A): boolean +} = containsWith(_equivalence) + +/** + * A useful recursion pattern for processing an `Iterable` to produce a new `Array`, often used for "chopping" up the input + * `Iterable`. Typically chop is called with some function that will consume an initial prefix of the `Iterable` and produce a + * value and the rest of the `Array`. + * + * **Example** + * + * ```ts + * import { Array } from "effect" + * + * const result = Array.chop([1, 2, 3, 4, 5], (as): [number, Array] => [as[0] * 2, as.slice(1)]) + * console.log(result) // [2, 4, 6, 8, 10] + * + * // Explanation: + * // The `chopFunction` takes the first element of the array, doubles it, and then returns it along with the rest of the array. + * // The `chop` function applies this `chopFunction` recursively to the input array `[1, 2, 3, 4, 5]`, + * // resulting in a new array `[2, 4, 6, 8, 10]`. + * ``` + * + * @since 2.0.0 + */ +export const chop: { + , B>( + f: (as: NonEmptyReadonlyArray>) => readonly [B, ReadonlyArray>] + ): (self: S) => ReadonlyArray.With> + ( + self: NonEmptyReadonlyArray, + f: (as: NonEmptyReadonlyArray) => readonly [B, ReadonlyArray] + ): NonEmptyArray + ( + self: Iterable, + f: (as: NonEmptyReadonlyArray) => readonly [B, ReadonlyArray] + ): Array +} = dual(2, ( + self: Iterable, + f: (as: NonEmptyReadonlyArray) => readonly [B, ReadonlyArray] +): Array => { + const input = fromIterable(self) + if (isNonEmptyReadonlyArray(input)) { + const [b, rest] = f(input) + const out: NonEmptyArray = [b] + let next: ReadonlyArray = rest + while (internalArray.isNonEmptyArray(next)) { + const [b, rest] = f(next) + out.push(b) + next = rest + } + return out + } + return [] +}) + +/** + * Splits an `Iterable` into two segments, with the first segment containing a maximum of `n` elements. + * The value of `n` can be `0`. + * + * **Example** + * + * ```ts + * import { Array } from "effect" + * + * const result = Array.splitAt([1, 2, 3, 4, 5], 3) + * console.log(result) // [[1, 2, 3], [4, 5]] + * ``` + * + * @category splitting + * @since 2.0.0 + */ +export const splitAt: { + (n: number): (self: Iterable) => [beforeIndex: Array, fromIndex: Array] + (self: Iterable, n: number): [beforeIndex: Array, fromIndex: Array] +} = dual(2, (self: Iterable, n: number): [Array, Array] => { + const input = Array.from(self) + const _n = Math.floor(n) + if (isNonEmptyReadonlyArray(input)) { + if (_n >= 1) { + return splitNonEmptyAt(input, _n) + } + return [[], input] + } + return [input, []] +}) + +/** + * Splits a `NonEmptyReadonlyArray` into two segments, with the first segment containing a maximum of `n` elements. + * The value of `n` must be `>= 1`. + * + * **Example** + * + * ```ts + * import { Array } from "effect" + * + * const result = Array.splitNonEmptyAt(["a", "b", "c", "d", "e"], 3) + * console.log(result) // [["a", "b", "c"], ["d", "e"]] + * ``` + * + * @category splitting + * @since 2.0.0 + */ +export const splitNonEmptyAt: { + (n: number): (self: NonEmptyReadonlyArray) => [beforeIndex: NonEmptyArray, fromIndex: Array] + (self: NonEmptyReadonlyArray, n: number): [beforeIndex: NonEmptyArray, fromIndex: Array] +} = dual(2, (self: NonEmptyReadonlyArray, n: number): [NonEmptyArray, Array] => { + const _n = Math.max(1, Math.floor(n)) + return _n >= self.length ? + [copy(self), []] : + [prepend(self.slice(1, _n), headNonEmpty(self)), self.slice(_n)] +}) + +/** + * Splits this iterable into `n` equally sized arrays. + * + * **Example** + * + * ```ts + * import { Array } from "effect" + * + * const result = Array.split([1, 2, 3, 4, 5, 6, 7, 8], 3) + * console.log(result) // [[1, 2, 3], [4, 5, 6], [7, 8]] + * ``` + * + * @since 2.0.0 + * @category splitting + */ +export const split: { + (n: number): (self: Iterable) => Array> + (self: Iterable, n: number): Array> +} = dual(2, (self: Iterable, n: number) => { + const input = fromIterable(self) + return chunksOf(input, Math.ceil(input.length / Math.floor(n))) +}) + +/** + * Splits this iterable on the first element that matches this predicate. + * Returns a tuple containing two arrays: the first one is before the match, and the second one is from the match onward. + * + * **Example** + * + * ```ts + * import { Array } from "effect" + * + * const result = Array.splitWhere([1, 2, 3, 4, 5], n => n > 3) + * console.log(result) // [[1, 2, 3], [4, 5]] + * ``` + * + * @category splitting + * @since 2.0.0 + */ +export const splitWhere: { + ( + predicate: (a: NoInfer, i: number) => boolean + ): (self: Iterable) => [beforeMatch: Array, fromMatch: Array] + (self: Iterable, predicate: (a: A, i: number) => boolean): [beforeMatch: Array, fromMatch: Array] +} = dual( + 2, + (self: Iterable, predicate: (a: A, i: number) => boolean): [beforeMatch: Array, fromMatch: Array] => + span(self, (a: A, i: number) => !predicate(a, i)) +) + +/** + * Copies an array. + * + * **Example** + * + * ```ts + * import { Array } from "effect" + * + * const result = Array.copy([1, 2, 3]) + * console.log(result) // [1, 2, 3] + * ``` + * + * @since 2.0.0 + */ +export const copy: { + (self: NonEmptyReadonlyArray): NonEmptyArray + (self: ReadonlyArray): Array +} = ((self: ReadonlyArray): Array => self.slice()) as any + +/** + * Pads an array. + * Returns a new array of length `n` with the elements of `array` followed by `fill` elements if `array` is shorter than `n`. + * If `array` is longer than `n`, the returned array will be a slice of `array` containing the `n` first elements of `array`. + * If `n` is less than or equal to 0, the returned array will be an empty array. + * + * **Example** + * + * ```ts + * import { Array } from "effect" + * + * const result = Array.pad([1, 2, 3], 6, 0) + * console.log(result) // [1, 2, 3, 0, 0, 0] + * ``` + * + * @since 3.8.4 + */ +export const pad: { + ( + n: number, + fill: T + ): ( + self: Array + ) => Array + (self: Array, n: number, fill: T): Array +} = dual(3, (self: Array, n: number, fill: T): Array => { + if (self.length >= n) { + return take(self, n) + } + return appendAll( + self, + makeBy(n - self.length, () => fill) + ) +}) + +/** + * Splits an `Iterable` into length-`n` pieces. The last piece will be shorter if `n` does not evenly divide the length of + * the `Iterable`. Note that `chunksOf(n)([])` is `[]`, not `[[]]`. This is intentional, and is consistent with a recursive + * definition of `chunksOf`; it satisfies the property that + * + * ```ts skip-type-checking + * chunksOf(n)(xs).concat(chunksOf(n)(ys)) == chunksOf(n)(xs.concat(ys))) + * ``` + * + * whenever `n` evenly divides the length of `self`. + * + * **Example** + * + * ```ts + * import { Array } from "effect" + * + * const result = Array.chunksOf([1, 2, 3, 4, 5], 2) + * console.log(result) // [[1, 2], [3, 4], [5]] + * + * // Explanation: + * // The `chunksOf` function takes an array of numbers `[1, 2, 3, 4, 5]` and a number `2`. + * // It splits the array into chunks of length 2. Since the array length is not evenly divisible by 2, + * // the last chunk contains the remaining elements. + * // The result is `[[1, 2], [3, 4], [5]]`. + * ``` + * + * @category splitting + * @since 2.0.0 + */ +export const chunksOf: { + ( + n: number + ): >( + self: S + ) => ReadonlyArray.With>> + (self: NonEmptyReadonlyArray, n: number): NonEmptyArray> + (self: Iterable, n: number): Array> +} = dual(2, (self: Iterable, n: number): Array> => { + const input = fromIterable(self) + if (isNonEmptyReadonlyArray(input)) { + return chop(input, splitNonEmptyAt(n)) + } + return [] +}) + +/** + * Creates sliding windows of size `n` from an `Iterable`. + * If the number of elements is less than `n` or if `n` is not greater than zero, + * an empty array is returned. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { Array } from "effect" + * + * const numbers = [1, 2, 3, 4, 5] + * assert.deepStrictEqual(Array.window(numbers, 3), [[1, 2, 3], [2, 3, 4], [3, 4, 5]]) + * assert.deepStrictEqual(Array.window(numbers, 6), []) + * ``` + * + * @category splitting + * @since 3.13.2 + */ +export const window: { + ( + n: N + ): (self: Iterable) => Array> + ( + self: Iterable, + n: N + ): Array> +} = dual(2, (self: Iterable, n: N): Array> => { + const input = fromIterable(self) + if (n > 0 && isNonEmptyReadonlyArray(input)) { + return Array.from( + { length: input.length - (n - 1) }, + (_, index) => input.slice(index, index + n) + ) + } + return [] +}) + +/** + * Group equal, consecutive elements of a `NonEmptyReadonlyArray` into `NonEmptyArray`s using the provided `isEquivalent` function. + * + * **Example** + * + * ```ts + * import { Array } from "effect" + * + * const result = Array.groupWith(["a", "a", "b", "b", "b", "c", "a"], (x, y) => x === y) + * console.log(result) // [["a", "a"], ["b", "b", "b"], ["c"], ["a"]] + * ``` + * + * @category grouping + * @since 2.0.0 + */ +export const groupWith: { + (isEquivalent: (self: A, that: A) => boolean): (self: NonEmptyReadonlyArray) => NonEmptyArray> + (self: NonEmptyReadonlyArray, isEquivalent: (self: A, that: A) => boolean): NonEmptyArray> +} = dual( + 2, + (self: NonEmptyReadonlyArray, isEquivalent: (self: A, that: A) => boolean): NonEmptyArray> => + chop(self, (as) => { + const h = headNonEmpty(as) + const out: NonEmptyArray = [h] + let i = 1 + for (; i < as.length; i++) { + const a = as[i] + if (isEquivalent(a, h)) { + out.push(a) + } else { + break + } + } + return [out, as.slice(i)] + }) +) + +/** + * Group equal, consecutive elements of a `NonEmptyReadonlyArray` into `NonEmptyArray`s. + * + * **Example** + * + * ```ts + * import { Array } from "effect" + * + * const result = Array.group([1, 1, 2, 2, 2, 3, 1]) + * console.log(result) // [[1, 1], [2, 2, 2], [3], [1]] + * ``` + * + * @category grouping + * @since 2.0.0 + */ +export const group: (self: NonEmptyReadonlyArray) => NonEmptyArray> = groupWith( + Equal.equivalence() +) + +/** + * Splits an `Iterable` into sub-non-empty-arrays stored in an object, based on the result of calling a `string`-returning + * function on each element, and grouping the results according to values returned + * + * **Example** + * + * ```ts + * import { Array } from "effect" + * + * const people = [ + * { name: "Alice", group: "A" }, + * { name: "Bob", group: "B" }, + * { name: "Charlie", group: "A" } + * ] + * + * const result = Array.groupBy(people, person => person.group) + * console.log(result) + * // { + * // A: [{ name: "Alice", group: "A" }, { name: "Charlie", group: "A" }], + * // B: [{ name: "Bob", group: "B" }] + * // } + * ``` + * + * @category grouping + * @since 2.0.0 + */ +export const groupBy: { + ( + f: (a: A) => K + ): (self: Iterable) => Record, NonEmptyArray> + ( + self: Iterable, + f: (a: A) => K + ): Record, NonEmptyArray> +} = dual(2, ( + self: Iterable, + f: (a: A) => K +): Record, NonEmptyArray> => { + const out: Record> = {} + for (const a of self) { + const k = f(a) + if (Object.prototype.hasOwnProperty.call(out, k)) { + out[k].push(a) + } else { + out[k] = [a] + } + } + return out +}) + +/** + * Calculates the union of two arrays using the provided equivalence relation. + * + * **Example** + * + * ```ts + * import { Array } from "effect" + * + * const union = Array.unionWith([1, 2], [2, 3], (a, b) => a === b) + * console.log(union) // [1, 2, 3] + * ``` + * + * @since 2.0.0 + */ +export const unionWith: { + , T extends Iterable>( + that: T, + isEquivalent: (self: ReadonlyArray.Infer, that: ReadonlyArray.Infer) => boolean + ): (self: S) => ReadonlyArray.OrNonEmpty | ReadonlyArray.Infer> + ( + self: NonEmptyReadonlyArray, + that: Iterable, + isEquivalent: (self: A, that: B) => boolean + ): NonEmptyArray + ( + self: Iterable, + that: NonEmptyReadonlyArray, + isEquivalent: (self: A, that: B) => boolean + ): NonEmptyArray + (self: Iterable, that: Iterable, isEquivalent: (self: A, that: B) => boolean): Array +} = dual(3, (self: Iterable, that: Iterable, isEquivalent: (self: A, that: A) => boolean): Array => { + const a = fromIterable(self) + const b = fromIterable(that) + if (isNonEmptyReadonlyArray(a)) { + if (isNonEmptyReadonlyArray(b)) { + const dedupe = dedupeWith(isEquivalent) + return dedupe(appendAll(a, b)) + } + return a + } + return b +}) + +/** + * Creates a union of two arrays, removing duplicates. + * + * **Example** + * + * ```ts + * import { Array } from "effect" + * + * const result = Array.union([1, 2], [2, 3]) + * console.log(result) // [1, 2, 3] + * ``` + * + * @since 2.0.0 + */ +export const union: { + >( + that: T + ): >( + self: S + ) => ReadonlyArray.OrNonEmpty | ReadonlyArray.Infer> + (self: NonEmptyReadonlyArray, that: ReadonlyArray): NonEmptyArray + (self: ReadonlyArray, that: NonEmptyReadonlyArray): NonEmptyArray + (self: Iterable, that: Iterable): Array +} = dual(2, (self: Iterable, that: Iterable): Array => unionWith(self, that, _equivalence)) + +/** + * Creates an `Array` of unique values that are included in all given `Iterable`s using the provided `isEquivalent` function. + * The order and references of result values are determined by the first `Iterable`. + * + * **Example** + * + * ```ts + * import { Array } from "effect" + * + * const array1 = [{ id: 1 }, { id: 2 }, { id: 3 }] + * const array2 = [{ id: 3 }, { id: 4 }, { id: 1 }] + * const isEquivalent = (a: { id: number }, b: { id: number }) => a.id === b.id + * const result = Array.intersectionWith(isEquivalent)(array2)(array1) + * console.log(result) // [{ id: 1 }, { id: 3 }] + * ``` + * + * @since 2.0.0 + */ +export const intersectionWith = (isEquivalent: (self: A, that: A) => boolean): { + (that: Iterable): (self: Iterable) => Array + (self: Iterable, that: Iterable): Array +} => { + const has = containsWith(isEquivalent) + return dual( + 2, + (self: Iterable, that: Iterable): Array => { + const bs = fromIterable(that) + return fromIterable(self).filter((a) => has(bs, a)) + } + ) +} + +/** + * Creates an `Array` of unique values that are included in all given `Iterable`s. + * The order and references of result values are determined by the first `Iterable`. + * + * **Example** + * + * ```ts + * import { Array } from "effect" + * + * const result = Array.intersection([1, 2, 3], [3, 4, 1]) + * console.log(result) // [1, 3] + * ``` + * + * @since 2.0.0 + */ +export const intersection: { + (that: Iterable): (self: Iterable) => Array + (self: Iterable, that: Iterable): Array +} = intersectionWith(_equivalence) + +/** + * Creates a `Array` of values not included in the other given `Iterable` using the provided `isEquivalent` function. + * The order and references of result values are determined by the first `Iterable`. + * + * **Example** + * + * ```ts + * import { Array } from "effect" + * + * const array1 = [1, 2, 3] + * const array2 = [2, 3, 4] + * const difference = Array.differenceWith((a, b) => a === b)(array1, array2) + * console.log(difference) // [1] + * ``` + * + * @since 2.0.0 + */ +export const differenceWith = (isEquivalent: (self: A, that: A) => boolean): { + (that: Iterable): (self: Iterable) => Array + (self: Iterable, that: Iterable): Array +} => { + const has = containsWith(isEquivalent) + return dual( + 2, + (self: Iterable, that: Iterable): Array => { + const bs = fromIterable(that) + return fromIterable(self).filter((a) => !has(bs, a)) + } + ) +} + +/** + * Creates a `Array` of values not included in the other given `Iterable`. + * The order and references of result values are determined by the first `Iterable`. + * + * **Example** + * + * ```ts + * import { Array } from "effect" + * + * const difference = Array.difference([1, 2, 3], [2, 3, 4]) + * console.log(difference) // [1] + * ``` + * + * @since 2.0.0 + */ +export const difference: { + (that: Iterable): (self: Iterable) => Array + (self: Iterable, that: Iterable): Array +} = differenceWith(_equivalence) + +/** + * @category constructors + * @since 2.0.0 + */ +export const empty: () => Array = () => [] + +/** + * Constructs a new `NonEmptyArray` from the specified value. + * + * @category constructors + * @since 2.0.0 + */ +export const of = (a: A): NonEmptyArray => [a] + +/** + * @since 2.0.0 + */ +export declare namespace ReadonlyArray { + /** + * @since 2.0.0 + */ + export type Infer> = S extends ReadonlyArray ? A + : S extends Iterable ? A + : never + + /** + * @since 2.0.0 + */ + export type With, A> = S extends NonEmptyReadonlyArray ? NonEmptyArray + : Array + + /** + * @since 2.0.0 + */ + export type OrNonEmpty< + S extends Iterable, + T extends Iterable, + A + > = S extends NonEmptyReadonlyArray ? NonEmptyArray + : T extends NonEmptyReadonlyArray ? NonEmptyArray + : Array + + /** + * @since 2.0.0 + */ + export type AndNonEmpty< + S extends Iterable, + T extends Iterable, + A + > = S extends NonEmptyReadonlyArray ? T extends NonEmptyReadonlyArray ? NonEmptyArray + : Array + : Array + + /** + * @since 2.0.0 + */ + export type Flatten>> = T extends + NonEmptyReadonlyArray> ? NonEmptyArray + : Array +} + +/** + * @category mapping + * @since 2.0.0 + */ +export const map: { + , B>( + f: (a: ReadonlyArray.Infer, i: number) => B + ): (self: S) => ReadonlyArray.With + , B>(self: S, f: (a: ReadonlyArray.Infer, i: number) => B): ReadonlyArray.With +} = dual(2, (self: ReadonlyArray, f: (a: A, i: number) => B): Array => self.map(f)) + +/** + * Applies a function to each element in an array and returns a new array containing the concatenated mapped elements. + * + * @category sequencing + * @since 2.0.0 + */ +export const flatMap: { + , T extends ReadonlyArray>( + f: (a: ReadonlyArray.Infer, i: number) => T + ): (self: S) => ReadonlyArray.AndNonEmpty> + (self: NonEmptyReadonlyArray, f: (a: A, i: number) => NonEmptyReadonlyArray): NonEmptyArray + (self: ReadonlyArray, f: (a: A, i: number) => ReadonlyArray): Array +} = dual( + 2, + (self: ReadonlyArray, f: (a: A, i: number) => ReadonlyArray): Array => { + if (isEmptyReadonlyArray(self)) { + return [] + } + const out: Array = [] + for (let i = 0; i < self.length; i++) { + const inner = f(self[i], i) + for (let j = 0; j < inner.length; j++) { + out.push(inner[j]) + } + } + return out + } +) + +/** + * Combines multiple arrays into a single array by concatenating all elements + * from each nested array. This function ensures that the structure of nested + * arrays is collapsed into a single, flat array. + * + * **Example** + * + * ```ts + * import { Array } from "effect" + * + * const result = Array.flatten([[1, 2], [], [3, 4], [], [5, 6]]) + * console.log(result) // [1, 2, 3, 4, 5, 6] + * ``` + * + * @category sequencing + * @since 2.0.0 + */ +export const flatten: >>( + self: S +) => ReadonlyArray.Flatten = flatMap( + identity +) as any + +/** + * Applies a function to each element of the `Iterable` and filters based on the result, keeping the transformed values where the function returns `Some`. + * This method combines filtering and mapping functionalities, allowing transformations and filtering of elements based on a single function pass. + * + * **Example** + * + * ```ts + * import { Array, Option } from "effect" + * + * const evenSquares = (x: number) => x % 2 === 0 ? Option.some(x * x) : Option.none() + * + * const result = Array.filterMap([1, 2, 3, 4, 5], evenSquares); + * console.log(result) // [4, 16] + * ``` + * + * @category filtering + * @since 2.0.0 + */ +export const filterMap: { + (f: (a: A, i: number) => Option.Option): (self: Iterable) => Array + (self: Iterable, f: (a: A, i: number) => Option.Option): Array +} = dual( + 2, + (self: Iterable, f: (a: A, i: number) => Option.Option): Array => { + const as = fromIterable(self) + const out: Array = [] + for (let i = 0; i < as.length; i++) { + const o = f(as[i], i) + if (Option.isSome(o)) { + out.push(o.value) + } + } + return out + } +) + +/** + * Applies a function to each element of the array and filters based on the result, stopping when a condition is not met. + * This method combines filtering and mapping in a single pass, and short-circuits, i.e., stops processing, as soon as the function returns `None`. + * This is useful when you need to transform an array but only up to the point where a certain condition holds true. + * + * **Example** + * + * ```ts + * import { Array, Option } from "effect" + * + * const toSquareTillOdd = (x: number) => x % 2 === 0 ? Option.some(x * x) : Option.none() + * + * const result = Array.filterMapWhile([2, 4, 5], toSquareTillOdd) + * console.log(result) // [4, 16] + * ``` + * + * @category filtering + * @since 2.0.0 + */ +export const filterMapWhile: { + (f: (a: A, i: number) => Option.Option): (self: Iterable) => Array + (self: Iterable, f: (a: A, i: number) => Option.Option): Array +} = dual(2, (self: Iterable, f: (a: A, i: number) => Option.Option) => { + let i = 0 + const out: Array = [] + for (const a of self) { + const b = f(a, i) + if (Option.isSome(b)) { + out.push(b.value) + } else { + break + } + i++ + } + return out +}) + +/** + * Applies a function to each element of the `Iterable`, categorizing the results into two separate arrays. + * This function is particularly useful for operations where each element can result in two possible types, + * and you want to separate these types into different collections. For instance, separating validation results + * into successes and failures. + * + * **Example** + * + * ```ts + * import { Array, Either } from "effect"; + * + * const isEven = (x: number) => x % 2 === 0 + * + * const result = Array.partitionMap([1, 2, 3, 4, 5], x => + * isEven(x) ? Either.right(x) : Either.left(x) + * ) + * console.log(result) + * // [ + * // [1, 3, 5], + * // [2, 4] + * // ] + * ``` + * + * @category filtering + * @since 2.0.0 + */ +export const partitionMap: { + (f: (a: A, i: number) => Either.Either): (self: Iterable) => [left: Array, right: Array] + (self: Iterable, f: (a: A, i: number) => Either.Either): [left: Array, right: Array] +} = dual( + 2, + (self: Iterable, f: (a: A, i: number) => Either.Either): [left: Array, right: Array] => { + const left: Array = [] + const right: Array = [] + const as = fromIterable(self) + for (let i = 0; i < as.length; i++) { + const e = f(as[i], i) + if (Either.isLeft(e)) { + left.push(e.left) + } else { + right.push(e.right) + } + } + return [left, right] + } +) + +/** + * Retrieves the `Some` values from an `Iterable` of `Option`s, collecting them into an array. + * + * **Example** + * + * ```ts + * import { Array, Option } from "effect" + * + * const result = Array.getSomes([Option.some(1), Option.none(), Option.some(2)]) + * console.log(result) // [1, 2] + * ``` + * + * @category filtering + * @since 2.0.0 + */ + +export const getSomes: >, X = any>( + self: T +) => Array>> = filterMap(identity as any) + +/** + * Retrieves the `Left` values from an `Iterable` of `Either`s, collecting them into an array. + * + * **Example** + * + * ```ts + * import { Array, Either } from "effect" + * + * const result = Array.getLefts([Either.right(1), Either.left("err"), Either.right(2)]) + * console.log(result) // ["err"] + * ``` + * + * @category filtering + * @since 2.0.0 + */ +export const getLefts = >>( + self: T +): Array>> => { + const out: Array = [] + for (const a of self) { + if (Either.isLeft(a)) { + out.push(a.left) + } + } + + return out +} + +/** + * Retrieves the `Right` values from an `Iterable` of `Either`s, collecting them into an array. + * + * **Example** + * + * ```ts + * import { Array, Either } from "effect" + * + * const result = Array.getRights([Either.right(1), Either.left("err"), Either.right(2)]) + * console.log(result) // [1, 2] + * ``` + * + * @category filtering + * @since 2.0.0 + */ +export const getRights = >>( + self: T +): Array>> => { + const out: Array = [] + for (const a of self) { + if (Either.isRight(a)) { + out.push(a.right) + } + } + + return out +} + +/** + * @category filtering + * @since 2.0.0 + */ +export const filter: { + (refinement: (a: NoInfer, i: number) => a is B): (self: Iterable) => Array + (predicate: (a: NoInfer, i: number) => boolean): (self: Iterable) => Array + (self: Iterable, refinement: (a: A, i: number) => a is B): Array + (self: Iterable, predicate: (a: A, i: number) => boolean): Array +} = dual( + 2, + (self: Iterable, predicate: (a: A, i: number) => boolean): Array => { + const as = fromIterable(self) + const out: Array = [] + for (let i = 0; i < as.length; i++) { + if (predicate(as[i], i)) { + out.push(as[i]) + } + } + return out + } +) + +/** + * Separate elements based on a predicate that also exposes the index of the element. + * + * **Example** + * + * ```ts + * import { Array } from "effect" + * + * const result = Array.partition([1, 2, 3, 4], n => n % 2 === 0) + * console.log(result) // [[1, 3], [2, 4]] + * ``` + * + * @category filtering + * @since 2.0.0 + */ +export const partition: { + (refinement: (a: NoInfer, i: number) => a is B): ( + self: Iterable + ) => [excluded: Array>, satisfying: Array] + ( + predicate: (a: NoInfer, i: number) => boolean + ): (self: Iterable) => [excluded: Array, satisfying: Array] + ( + self: Iterable, + refinement: (a: A, i: number) => a is B + ): [excluded: Array>, satisfying: Array] + (self: Iterable, predicate: (a: A, i: number) => boolean): [excluded: Array, satisfying: Array] +} = dual( + 2, + (self: Iterable, predicate: (a: A, i: number) => boolean): [excluded: Array, satisfying: Array] => { + const left: Array = [] + const right: Array = [] + const as = fromIterable(self) + for (let i = 0; i < as.length; i++) { + if (predicate(as[i], i)) { + right.push(as[i]) + } else { + left.push(as[i]) + } + } + return [left, right] + } +) + +/** + * Separates an `Iterable` into two arrays based on a predicate. + * + * @category filtering + * @since 2.0.0 + */ +export const separate: >>( + self: T +) => [Array>>, Array>>] = + partitionMap(identity) + +/** + * Reduces an array from the left. + * + * **Example** + * + * ```ts + * import { Array } from "effect" + * + * const result = Array.reduce([1, 2, 3], 0, (acc, n) => acc + n) + * console.log(result) // 6 + * ``` + * + * @category folding + * @since 2.0.0 + */ +export const reduce: { + (b: B, f: (b: B, a: A, i: number) => B): (self: Iterable) => B + (self: Iterable, b: B, f: (b: B, a: A, i: number) => B): B +} = dual( + 3, + (self: Iterable, b: B, f: (b: B, a: A, i: number) => B): B => + fromIterable(self).reduce((b, a, i) => f(b, a, i), b) +) + +/** + * Reduces an array from the right. + * + * **Example** + * + * ```ts + * import { Array } from "effect" + * + * const result = Array.reduceRight([1, 2, 3], 0, (acc, n) => acc + n) + * console.log(result) // 6 + * ``` + * + * @category folding + * @since 2.0.0 + */ +export const reduceRight: { + (b: B, f: (b: B, a: A, i: number) => B): (self: Iterable) => B + (self: Iterable, b: B, f: (b: B, a: A, i: number) => B): B +} = dual( + 3, + (self: Iterable, b: B, f: (b: B, a: A, i: number) => B): B => + fromIterable(self).reduceRight((b, a, i) => f(b, a, i), b) +) + +/** + * Lifts a predicate into an array. + * + * **Example** + * + * ```ts + * import { Array } from "effect" + * + * const isEven = (n: number) => n % 2 === 0 + * const to = Array.liftPredicate(isEven) + * console.log(to(1)) // [] + * console.log(to(2)) // [2] + * ``` + * + * @category lifting + * @since 2.0.0 + */ +export const liftPredicate: { // Note: I intentionally avoid using the NoInfer pattern here. + (refinement: Predicate.Refinement): (a: A) => Array + (predicate: Predicate.Predicate): (b: B) => Array +} = (predicate: Predicate.Predicate) => (b: B): Array => predicate(b) ? [b] : [] + +/** + * @category lifting + * @since 2.0.0 + */ +export const liftOption = , B>( + f: (...a: A) => Option.Option +) => +(...a: A): Array => fromOption(f(...a)) + +/** + * @category conversions + * @since 2.0.0 + */ +export const fromNullable = (a: A): Array> => a == null ? empty() : [a as NonNullable] + +/** + * @category lifting + * @since 2.0.0 + */ +export const liftNullable = , B>( + f: (...a: A) => B | null | undefined +): (...a: A) => Array> => +(...a) => fromNullable(f(...a)) + +/** + * Maps over an array and flattens the result, removing null and undefined values. + * + * **Example** + * + * ```ts + * import { Array } from "effect" + * + * const result = Array.flatMapNullable([1, 2, 3], n => (n % 2 === 0 ? null : n)) + * console.log(result) // [1, 3] + * + * // Explanation: + * // The array of numbers [1, 2, 3] is mapped with a function that returns null for even numbers + * // and the number itself for odd numbers. The resulting array [1, null, 3] is then flattened + * // to remove null values, resulting in [1, 3]. + * ``` + * + * @category sequencing + * @since 2.0.0 + */ +export const flatMapNullable: { + (f: (a: A) => B | null | undefined): (self: ReadonlyArray) => Array> + (self: ReadonlyArray, f: (a: A) => B | null | undefined): Array> +} = dual( + 2, + (self: ReadonlyArray, f: (a: A) => B | null | undefined): Array> => + flatMap(self, (a) => fromNullable(f(a))) +) + +/** + * Lifts a function that returns an `Either` into a function that returns an array. + * If the `Either` is a left, it returns an empty array. + * If the `Either` is a right, it returns an array with the right value. + * + * **Example** + * + * ```ts + * import { Array, Either } from "effect" + * + * const parseNumber = (s: string): Either.Either => + * isNaN(Number(s)) ? Either.left(new Error("Not a number")) : Either.right(Number(s)) + * + * const liftedParseNumber = Array.liftEither(parseNumber) + * + * const result1 = liftedParseNumber("42") + * console.log(result1) // [42] + * + * const result2 = liftedParseNumber("not a number") + * console.log(result2) // [] + * + * // Explanation: + * // The function parseNumber is lifted to return an array. + * // When parsing "42", it returns an Either.left with the number 42, resulting in [42]. + * // When parsing "not a number", it returns an Either.right with an error, resulting in an empty array []. + * ``` + * + * @category lifting + * @since 2.0.0 + */ +export const liftEither = , E, B>( + f: (...a: A) => Either.Either +) => +(...a: A): Array => { + const e = f(...a) + return Either.isLeft(e) ? [] : [e.right] +} + +/** + * Check if a predicate holds true for every `ReadonlyArray` element. + * + * @category elements + * @since 2.0.0 + */ +export const every: { + ( + refinement: (a: NoInfer, i: number) => a is B + ): (self: ReadonlyArray) => self is ReadonlyArray + (predicate: (a: NoInfer, i: number) => boolean): (self: ReadonlyArray) => boolean + (self: ReadonlyArray, refinement: (a: A, i: number) => a is B): self is ReadonlyArray + (self: ReadonlyArray, predicate: (a: A, i: number) => boolean): boolean +} = dual( + 2, + (self: ReadonlyArray, refinement: (a: A, i: number) => a is B): self is ReadonlyArray => + self.every(refinement) +) + +/** + * Check if a predicate holds true for some `ReadonlyArray` element. + * + * @category elements + * @since 2.0.0 + */ +export const some: { + ( + predicate: (a: NoInfer, i: number) => boolean + ): (self: ReadonlyArray) => self is NonEmptyReadonlyArray + (self: ReadonlyArray, predicate: (a: A, i: number) => boolean): self is NonEmptyReadonlyArray +} = dual( + 2, + (self: ReadonlyArray, predicate: (a: A, i: number) => boolean): self is NonEmptyReadonlyArray => + self.some(predicate) +) + +/** + * Extends an array with a function that maps each subarray to a value. + * + * **Example** + * + * ```ts + * import { Array } from "effect" + * + * const result = Array.extend([1, 2, 3], as => as.length) + * console.log(result) // [3, 2, 1] + * + * // Explanation: + * // The function maps each subarray starting from each element to its length. + * // The subarrays are: [1, 2, 3], [2, 3], [3]. + * // The lengths are: 3, 2, 1. + * // Therefore, the result is [3, 2, 1]. + * ``` + * + * @since 2.0.0 + */ +export const extend: { + (f: (as: ReadonlyArray) => B): (self: ReadonlyArray) => Array + (self: ReadonlyArray, f: (as: ReadonlyArray) => B): Array +} = dual( + 2, + (self: ReadonlyArray, f: (as: ReadonlyArray) => B): Array => self.map((_, i, as) => f(as.slice(i))) +) + +/** + * Finds the minimum element in an array based on a comparator. + * + * **Example** + * + * ```ts + * import { Array, Order } from "effect" + * + * const result = Array.min([3, 1, 2], Order.number) + * console.log(result) // 1 + * ``` + * + * @since 2.0.0 + */ +export const min: { + (O: Order.Order): (self: NonEmptyReadonlyArray) => A + (self: NonEmptyReadonlyArray, O: Order.Order): A +} = dual(2, (self: NonEmptyReadonlyArray, O: Order.Order): A => self.reduce(Order.min(O))) + +/** + * Finds the maximum element in an array based on a comparator. + * + * **Example** + * + * ```ts + * import { Array, Order } from "effect" + * + * const result = Array.max([3, 1, 2], Order.number) + * console.log(result) // 3 + * ``` + * + * @since 2.0.0 + */ +export const max: { + (O: Order.Order): (self: NonEmptyReadonlyArray) => A + (self: NonEmptyReadonlyArray, O: Order.Order): A +} = dual(2, (self: NonEmptyReadonlyArray, O: Order.Order): A => self.reduce(Order.max(O))) + +/** + * @category constructors + * @since 2.0.0 + */ +export const unfold = (b: B, f: (b: B) => Option.Option): Array => { + const out: Array = [] + let next: B = b + let o: Option.Option + while (Option.isSome(o = f(next))) { + const [a, b] = o.value + out.push(a) + next = b + } + return out +} + +/** + * This function creates and returns a new `Order` for an array of values based on a given `Order` for the elements of the array. + * The returned `Order` compares two arrays by applying the given `Order` to each element in the arrays. + * If all elements are equal, the arrays are then compared based on their length. + * It is useful when you need to compare two arrays of the same type and you have a specific way of comparing each element of the array. + * + * @category instances + * @since 2.0.0 + */ +export const getOrder: (O: Order.Order) => Order.Order> = Order.array + +/** + * Creates an equivalence relation for arrays. + * + * **Example** + * + * ```ts + * import { Array } from "effect" + * + * const eq = Array.getEquivalence((a, b) => a === b) + * console.log(eq([1, 2, 3], [1, 2, 3])) // true + * ``` + * + * @category instances + * @since 2.0.0 + */ +export const getEquivalence: ( + isEquivalent: Equivalence.Equivalence +) => Equivalence.Equivalence> = Equivalence.array + +/** + * Performs a side-effect for each element of the `Iterable`. + * + * **Example** + * + * ```ts + * import { Array } from "effect" + * + * Array.forEach([1, 2, 3], n => console.log(n)) // 1, 2, 3 + * ``` + * + * @since 2.0.0 + */ +export const forEach: { + (f: (a: A, i: number) => void): (self: Iterable) => void + (self: Iterable, f: (a: A, i: number) => void): void +} = dual(2, (self: Iterable, f: (a: A, i: number) => void): void => fromIterable(self).forEach((a, i) => f(a, i))) + +/** + * Remove duplicates from an `Iterable` using the provided `isEquivalent` function, + * preserving the order of the first occurrence of each element. + * + * **Example** + * + * ```ts + * import { Array } from "effect" + * + * const result = Array.dedupeWith([1, 2, 2, 3, 3, 3], (a, b) => a === b) + * console.log(result) // [1, 2, 3] + * ``` + * + * @since 2.0.0 + */ +export const dedupeWith: { + >( + isEquivalent: (self: ReadonlyArray.Infer, that: ReadonlyArray.Infer) => boolean + ): (self: S) => ReadonlyArray.With> + (self: NonEmptyReadonlyArray, isEquivalent: (self: A, that: A) => boolean): NonEmptyArray + (self: Iterable, isEquivalent: (self: A, that: A) => boolean): Array +} = dual( + 2, + (self: Iterable, isEquivalent: (self: A, that: A) => boolean): Array => { + const input = fromIterable(self) + if (isNonEmptyReadonlyArray(input)) { + const out: NonEmptyArray = [headNonEmpty(input)] + const rest = tailNonEmpty(input) + for (const r of rest) { + if (out.every((a) => !isEquivalent(r, a))) { + out.push(r) + } + } + return out + } + return [] + } +) + +/** + * Remove duplicates from an `Iterable`, preserving the order of the first occurrence of each element. + * The equivalence used to compare elements is provided by `Equal.equivalence()` from the `Equal` module. + * + * @since 2.0.0 + */ +export const dedupe = >( + self: S +): S extends NonEmptyReadonlyArray ? NonEmptyArray : S extends Iterable ? Array : never => + dedupeWith(self, Equal.equivalence()) as any + +/** + * Deduplicates adjacent elements that are identical using the provided `isEquivalent` function. + * + * **Example** + * + * ```ts + * import { Array } from "effect" + * + * const result = Array.dedupeAdjacentWith([1, 1, 2, 2, 3, 3], (a, b) => a === b) + * console.log(result) // [1, 2, 3] + * ``` + * + * @since 2.0.0 + */ +export const dedupeAdjacentWith: { + (isEquivalent: (self: A, that: A) => boolean): (self: Iterable) => Array + (self: Iterable, isEquivalent: (self: A, that: A) => boolean): Array +} = dual(2, (self: Iterable, isEquivalent: (self: A, that: A) => boolean): Array => { + const out: Array = [] + let lastA: Option.Option = Option.none() + for (const a of self) { + if (Option.isNone(lastA) || !isEquivalent(a, lastA.value)) { + out.push(a) + lastA = Option.some(a) + } + } + return out +}) + +/** + * Deduplicates adjacent elements that are identical. + * + * **Example** + * + * ```ts + * import { Array } from "effect" + * + * const result = Array.dedupeAdjacent([1, 1, 2, 2, 3, 3]) + * console.log(result) // [1, 2, 3] + * ``` + * + * @since 2.0.0 + */ +export const dedupeAdjacent: (self: Iterable) => Array = dedupeAdjacentWith(Equal.equivalence()) + +/** + * Joins the elements together with "sep" in the middle. + * + * **Example** + * + * ```ts + * import { Array } from "effect" + * + * const strings = ["a", "b", "c"] + * const joined = Array.join(strings, "-") + * console.log(joined) // "a-b-c" + * ``` + * + * @since 2.0.0 + * @category folding + */ +export const join: { + (sep: string): (self: Iterable) => string + (self: Iterable, sep: string): string +} = dual(2, (self: Iterable, sep: string): string => fromIterable(self).join(sep)) + +/** + * Statefully maps over the chunk, producing new elements of type `B`. + * + * **Example** + * + * ```ts + * import { Array } from "effect" + * + * const result = Array.mapAccum([1, 2, 3], 0, (acc, n) => [acc + n, acc + n]) + * console.log(result) // [6, [1, 3, 6]] + * ``` + * + * @since 2.0.0 + * @category folding + */ +export const mapAccum: { + = Iterable>( + s: S, + f: (s: S, a: ReadonlyArray.Infer, i: number) => readonly [S, B] + ): (self: I) => [state: S, mappedArray: ReadonlyArray.With] + = Iterable>( + self: I, + s: S, + f: (s: S, a: ReadonlyArray.Infer, i: number) => readonly [S, B] + ): [state: S, mappedArray: ReadonlyArray.With] +} = dual( + 3, + (self: Iterable, s: S, f: (s: S, a: A, i: number) => [S, B]): [state: S, mappedArray: Array] => { + let i = 0 + let s1 = s + const out: Array = [] + for (const a of self) { + const r = f(s1, a, i) + s1 = r[0] + out.push(r[1]) + i++ + } + return [s1, out] + } +) + +/** + * Zips this chunk crosswise with the specified chunk using the specified combiner. + * + * **Example** + * + * ```ts + * import { Array } from "effect" + * + * const result = Array.cartesianWith([1, 2], ["a", "b"], (a, b) => `${a}-${b}`) + * console.log(result) // ["1-a", "1-b", "2-a", "2-b"] + * ``` + * + * @since 2.0.0 + * @category elements + */ +export const cartesianWith: { + (that: ReadonlyArray, f: (a: A, b: B) => C): (self: ReadonlyArray) => Array + (self: ReadonlyArray, that: ReadonlyArray, f: (a: A, b: B) => C): Array +} = dual( + 3, + (self: ReadonlyArray, that: ReadonlyArray, f: (a: A, b: B) => C): Array => + flatMap(self, (a) => map(that, (b) => f(a, b))) +) + +/** + * Zips this chunk crosswise with the specified chunk. + * + * **Example** + * + * ```ts + * import { Array } from "effect" + * + * const result = Array.cartesian([1, 2], ["a", "b"]) + * console.log(result) // [[1, "a"], [1, "b"], [2, "a"], [2, "b"]] + * ``` + * + * @since 2.0.0 + * @category elements + */ +export const cartesian: { + (that: ReadonlyArray): (self: ReadonlyArray) => Array<[A, B]> + (self: ReadonlyArray, that: ReadonlyArray): Array<[A, B]> +} = dual( + 2, + (self: ReadonlyArray, that: ReadonlyArray): Array<[A, B]> => cartesianWith(self, that, (a, b) => [a, b]) +) + +// ------------------------------------------------------------------------------------- +// do notation +// ------------------------------------------------------------------------------------- + +/** + * The "do simulation" for array allows you to sequentially apply operations to the elements of arrays, just as nested loops allow you to go through all combinations of elements in an arrays. + * + * It can be used to simulate "array comprehension". + * It's a technique that allows you to create new arrays by iterating over existing ones and applying specific **conditions** or **transformations** to the elements. It's like assembling a new collection from pieces of other collections based on certain rules. + * + * Here's how the do simulation works: + * + * 1. Start the do simulation using the `Do` value + * 2. Within the do simulation scope, you can use the `bind` function to define variables and bind them to `Array` values + * 3. You can accumulate multiple `bind` statements to define multiple variables within the scope + * 4. Inside the do simulation scope, you can also use the `let` function to define variables and bind them to simple values + * 5. Regular `Array` functions like `map` and `filter` can still be used within the do simulation. These functions will receive the accumulated variables as arguments within the scope + * + * **Example** + * + * ```ts + * import { Array, pipe } from "effect" + * + * const doResult = pipe( + * Array.Do, + * Array.bind("x", () => [1, 3, 5]), + * Array.bind("y", () => [2, 4, 6]), + * Array.filter(({ x, y }) => x < y), // condition + * Array.map(({ x, y }) => [x, y] as const) // transformation + * ) + * console.log(doResult) // [[1, 2], [1, 4], [1, 6], [3, 4], [3, 6], [5, 6]] + * + * // equivalent + * const x = [1, 3, 5], + * y = [2, 4, 6], + * result = []; + * for(let i = 0; i < x.length; i++) { + * for(let j = 0; j < y.length; j++) { + * const _x = x[i], _y = y[j]; + * if(_x < _y) result.push([_x, _y] as const) + * } + * } + * ``` + * + * @see {@link bindTo} + * @see {@link bind} + * @see {@link let_ let} + * + * @category do notation + * @since 3.2.0 + */ +export const Do: ReadonlyArray<{}> = of({}) + +/** + * The "do simulation" for array allows you to sequentially apply operations to the elements of arrays, just as nested loops allow you to go through all combinations of elements in an arrays. + * + * It can be used to simulate "array comprehension". + * It's a technique that allows you to create new arrays by iterating over existing ones and applying specific **conditions** or **transformations** to the elements. It's like assembling a new collection from pieces of other collections based on certain rules. + * + * Here's how the do simulation works: + * + * 1. Start the do simulation using the `Do` value + * 2. Within the do simulation scope, you can use the `bind` function to define variables and bind them to `Array` values + * 3. You can accumulate multiple `bind` statements to define multiple variables within the scope + * 4. Inside the do simulation scope, you can also use the `let` function to define variables and bind them to simple values + * 5. Regular `Array` functions like `map` and `filter` can still be used within the do simulation. These functions will receive the accumulated variables as arguments within the scope + * + * **Example** + * + * ```ts + * import { Array, pipe } from "effect" + * + * const doResult = pipe( + * Array.Do, + * Array.bind("x", () => [1, 3, 5]), + * Array.bind("y", () => [2, 4, 6]), + * Array.filter(({ x, y }) => x < y), // condition + * Array.map(({ x, y }) => [x, y] as const) // transformation + * ) + * console.log(doResult) // [[1, 2], [1, 4], [1, 6], [3, 4], [3, 6], [5, 6]] + * + * // equivalent + * const x = [1, 3, 5], + * y = [2, 4, 6], + * result = []; + * for(let i = 0; i < x.length; i++) { + * for(let j = 0; j < y.length; j++) { + * const _x = x[i], _y = y[j]; + * if(_x < _y) result.push([_x, _y] as const) + * } + * } + * ``` + * + * @see {@link bindTo} + * @see {@link Do} + * @see {@link let_ let} + * + * @category do notation + * @since 3.2.0 + */ +export const bind: { + ( + tag: Exclude, + f: (a: NoInfer) => ReadonlyArray + ): ( + self: ReadonlyArray + ) => Array<{ [K in N | keyof A]: K extends keyof A ? A[K] : B }> + ( + self: ReadonlyArray, + tag: Exclude, + f: (a: NoInfer) => ReadonlyArray + ): Array<{ [K in N | keyof A]: K extends keyof A ? A[K] : B }> +} = internalDoNotation.bind(map, flatMap) as any + +/** + * The "do simulation" for array allows you to sequentially apply operations to the elements of arrays, just as nested loops allow you to go through all combinations of elements in an arrays. + * + * It can be used to simulate "array comprehension". + * It's a technique that allows you to create new arrays by iterating over existing ones and applying specific **conditions** or **transformations** to the elements. It's like assembling a new collection from pieces of other collections based on certain rules. + * + * Here's how the do simulation works: + * + * 1. Start the do simulation using the `Do` value + * 2. Within the do simulation scope, you can use the `bind` function to define variables and bind them to `Array` values + * 3. You can accumulate multiple `bind` statements to define multiple variables within the scope + * 4. Inside the do simulation scope, you can also use the `let` function to define variables and bind them to simple values + * 5. Regular `Array` functions like `map` and `filter` can still be used within the do simulation. These functions will receive the accumulated variables as arguments within the scope + * + * **Example** + * + * ```ts + * import { Array, pipe } from "effect" + * + * const doResult = pipe( + * Array.Do, + * Array.bind("x", () => [1, 3, 5]), + * Array.bind("y", () => [2, 4, 6]), + * Array.filter(({ x, y }) => x < y), // condition + * Array.map(({ x, y }) => [x, y] as const) // transformation + * ) + * console.log(doResult) // [[1, 2], [1, 4], [1, 6], [3, 4], [3, 6], [5, 6]] + * + * // equivalent + * const x = [1, 3, 5], + * y = [2, 4, 6], + * result = []; + * for(let i = 0; i < x.length; i++) { + * for(let j = 0; j < y.length; j++) { + * const _x = x[i], _y = y[j]; + * if(_x < _y) result.push([_x, _y] as const) + * } + * } + * ``` + * + * @see {@link bindTo} + * @see {@link Do} + * @see {@link let_ let} + * + * @category do notation + * @since 3.2.0 + */ +export const bindTo: { + (tag: N): (self: ReadonlyArray) => Array<{ [K in N]: A }> + (self: ReadonlyArray, tag: N): Array<{ [K in N]: A }> +} = internalDoNotation.bindTo(map) as any + +const let_: { + ( + tag: Exclude, + f: (a: NoInfer) => B + ): (self: ReadonlyArray) => Array<{ [K in N | keyof A]: K extends keyof A ? A[K] : B }> + ( + self: ReadonlyArray, + tag: Exclude, + f: (a: NoInfer) => B + ): Array<{ [K in N | keyof A]: K extends keyof A ? A[K] : B }> +} = internalDoNotation.let_(map) as any + +export { + /** + * The "do simulation" for array allows you to sequentially apply operations to the elements of arrays, just as nested loops allow you to go through all combinations of elements in an arrays. + * + * It can be used to simulate "array comprehension". + * It's a technique that allows you to create new arrays by iterating over existing ones and applying specific **conditions** or **transformations** to the elements. It's like assembling a new collection from pieces of other collections based on certain rules. + * + * Here's how the do simulation works: + * + * 1. Start the do simulation using the `Do` value + * 2. Within the do simulation scope, you can use the `bind` function to define variables and bind them to `Array` values + * 3. You can accumulate multiple `bind` statements to define multiple variables within the scope + * 4. Inside the do simulation scope, you can also use the `let` function to define variables and bind them to simple values + * 5. Regular `Array` functions like `map` and `filter` can still be used within the do simulation. These functions will receive the accumulated variables as arguments within the scope + * + * **Example** + * + * ```ts + * import { Array, pipe } from "effect" + * + * const doResult = pipe( + * Array.Do, + * Array.bind("x", () => [1, 3, 5]), + * Array.bind("y", () => [2, 4, 6]), + * Array.filter(({ x, y }) => x < y), // condition + * Array.map(({ x, y }) => [x, y] as const) // transformation + * ) + * console.log(doResult) // [[1, 2], [1, 4], [1, 6], [3, 4], [3, 6], [5, 6]] + * + * // equivalent + * const x = [1, 3, 5], + * y = [2, 4, 6], + * result = []; + * for(let i = 0; i < x.length; i++) { + * for(let j = 0; j < y.length; j++) { + * const _x = x[i], _y = y[j]; + * if(_x < _y) result.push([_x, _y] as const) + * } + * } + * + * ``` + * + * @see {@link bindTo} + * @see {@link bind} + * @see {@link Do} + * + * @category do notation + * @since 3.2.0 + */ + let_ as let +} diff --git a/repos/effect/packages/effect/src/BigDecimal.ts b/repos/effect/packages/effect/src/BigDecimal.ts new file mode 100644 index 0000000..1a52ce9 --- /dev/null +++ b/repos/effect/packages/effect/src/BigDecimal.ts @@ -0,0 +1,1349 @@ +/** + * This module provides utility functions and type class instances for working with the `BigDecimal` type in TypeScript. + * It includes functions for basic arithmetic operations, as well as type class instances for `Equivalence` and `Order`. + * + * A `BigDecimal` allows storing any real number to arbitrary precision; which avoids common floating point errors + * (such as 0.1 + 0.2 ≠ 0.3) at the cost of complexity. + * + * Internally, `BigDecimal` uses a `BigInt` object, paired with a 64-bit integer which determines the position of the + * decimal point. Therefore, the precision *is not* actually arbitrary, but limited to 263 decimal places. + * + * It is not recommended to convert a floating point number to a decimal directly, as the floating point representation + * may be unexpected. + * + * @module BigDecimal + * @since 2.0.0 + * @see {@link module:BigInt} for more similar operations on `bigint` types + * @see {@link module:Number} for more similar operations on `number` types + */ + +import * as Equal from "./Equal.js" +import * as equivalence from "./Equivalence.js" +import { dual, pipe } from "./Function.js" +import * as Hash from "./Hash.js" +import { type Inspectable, NodeInspectSymbol } from "./Inspectable.js" +import * as Option from "./Option.js" +import * as order from "./Order.js" +import type { Ordering } from "./Ordering.js" +import { type Pipeable, pipeArguments } from "./Pipeable.js" +import { hasProperty } from "./Predicate.js" + +const DEFAULT_PRECISION = 100 +const FINITE_INT_REGEX = /^[+-]?\d+$/ + +/** + * @since 2.0.0 + * @category symbols + */ +export const TypeId: unique symbol = Symbol.for("effect/BigDecimal") + +/** + * @since 2.0.0 + * @category symbol + */ +export type TypeId = typeof TypeId + +/** + * @since 2.0.0 + * @category models + */ +export interface BigDecimal extends Equal.Equal, Pipeable, Inspectable { + readonly [TypeId]: TypeId + readonly value: bigint + readonly scale: number + /** @internal */ + normalized?: BigDecimal +} + +const BigDecimalProto: Omit = { + [TypeId]: TypeId, + [Hash.symbol](this: BigDecimal): number { + const normalized = normalize(this) + return pipe( + Hash.hash(normalized.value), + Hash.combine(Hash.number(normalized.scale)), + Hash.cached(this) + ) + }, + [Equal.symbol](this: BigDecimal, that: unknown): boolean { + return isBigDecimal(that) && equals(this, that) + }, + toString(this: BigDecimal) { + return `BigDecimal(${format(this)})` + }, + toJSON(this: BigDecimal) { + return { + _id: "BigDecimal", + value: String(this.value), + scale: this.scale + } + }, + [NodeInspectSymbol](this: BigDecimal) { + return this.toJSON() + }, + pipe() { + return pipeArguments(this, arguments) + } +} as const + +/** + * Checks if a given value is a `BigDecimal`. + * + * @since 2.0.0 + * @category guards + */ +export const isBigDecimal = (u: unknown): u is BigDecimal => hasProperty(u, TypeId) + +/** + * Creates a `BigDecimal` from a `bigint` value and a scale. + * + * @since 2.0.0 + * @category constructors + */ +export const make = (value: bigint, scale: number): BigDecimal => { + const o = Object.create(BigDecimalProto) + o.value = value + o.scale = scale + return o +} + +/** + * Internal function used to create pre-normalized `BigDecimal`s. + * + * @internal + */ +export const unsafeMakeNormalized = (value: bigint, scale: number): BigDecimal => { + if (value !== bigint0 && value % bigint10 === bigint0) { + throw new RangeError("Value must be normalized") + } + + const o = make(value, scale) + o.normalized = o + return o +} + +const bigint0 = BigInt(0) +const bigint1 = BigInt(1) +const bigint10 = BigInt(10) +const zero = unsafeMakeNormalized(bigint0, 0) + +/** + * Normalizes a given `BigDecimal` by removing trailing zeros. + * + * **Example** + * + * ```ts + * import * as assert from "node:assert" + * import { normalize, make, unsafeFromString } from "effect/BigDecimal" + * + * assert.deepStrictEqual(normalize(unsafeFromString("123.00000")), normalize(make(123n, 0))) + * assert.deepStrictEqual(normalize(unsafeFromString("12300000")), normalize(make(123n, -5))) + * ``` + * + * @since 2.0.0 + * @category scaling + */ +export const normalize = (self: BigDecimal): BigDecimal => { + if (self.normalized === undefined) { + if (self.value === bigint0) { + self.normalized = zero + } else { + const digits = `${self.value}` + + let trail = 0 + for (let i = digits.length - 1; i >= 0; i--) { + if (digits[i] === "0") { + trail++ + } else { + break + } + } + + if (trail === 0) { + self.normalized = self + } + + const value = BigInt(digits.substring(0, digits.length - trail)) + const scale = self.scale - trail + self.normalized = unsafeMakeNormalized(value, scale) + } + } + + return self.normalized +} + +/** + * Scales a given `BigDecimal` to the specified scale. + * + * If the given scale is smaller than the current scale, the value will be rounded down to + * the nearest integer. + * + * @since 2.0.0 + * @category scaling + */ +export const scale: { + (scale: number): (self: BigDecimal) => BigDecimal + (self: BigDecimal, scale: number): BigDecimal +} = dual(2, (self: BigDecimal, scale: number): BigDecimal => { + if (scale > self.scale) { + return make(self.value * bigint10 ** BigInt(scale - self.scale), scale) + } + + if (scale < self.scale) { + return make(self.value / bigint10 ** BigInt(self.scale - scale), scale) + } + + return self +}) + +/** + * Provides an addition operation on `BigDecimal`s. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { sum, unsafeFromString } from "effect/BigDecimal" + * + * assert.deepStrictEqual(sum(unsafeFromString("2"), unsafeFromString("3")), unsafeFromString("5")) + * ``` + * + * @since 2.0.0 + * @category math + */ +export const sum: { + (that: BigDecimal): (self: BigDecimal) => BigDecimal + (self: BigDecimal, that: BigDecimal): BigDecimal +} = dual(2, (self: BigDecimal, that: BigDecimal): BigDecimal => { + if (that.value === bigint0) { + return self + } + + if (self.value === bigint0) { + return that + } + + if (self.scale > that.scale) { + return make(scale(that, self.scale).value + self.value, self.scale) + } + + if (self.scale < that.scale) { + return make(scale(self, that.scale).value + that.value, that.scale) + } + + return make(self.value + that.value, self.scale) +}) + +/** + * Provides a multiplication operation on `BigDecimal`s. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { multiply, unsafeFromString } from "effect/BigDecimal" + * + * assert.deepStrictEqual(multiply(unsafeFromString("2"), unsafeFromString("3")), unsafeFromString("6")) + * ``` + * + * @since 2.0.0 + * @category math + */ +export const multiply: { + (that: BigDecimal): (self: BigDecimal) => BigDecimal + (self: BigDecimal, that: BigDecimal): BigDecimal +} = dual(2, (self: BigDecimal, that: BigDecimal): BigDecimal => { + if (that.value === bigint0 || self.value === bigint0) { + return zero + } + + return make(self.value * that.value, self.scale + that.scale) +}) + +/** + * Provides a subtraction operation on `BigDecimal`s. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { subtract, unsafeFromString } from "effect/BigDecimal" + * + * assert.deepStrictEqual(subtract(unsafeFromString("2"), unsafeFromString("3")), unsafeFromString("-1")) + * ``` + * + * @since 2.0.0 + * @category math + */ +export const subtract: { + (that: BigDecimal): (self: BigDecimal) => BigDecimal + (self: BigDecimal, that: BigDecimal): BigDecimal +} = dual(2, (self: BigDecimal, that: BigDecimal): BigDecimal => { + if (that.value === bigint0) { + return self + } + + if (self.value === bigint0) { + return make(-that.value, that.scale) + } + + if (self.scale > that.scale) { + return make(self.value - scale(that, self.scale).value, self.scale) + } + + if (self.scale < that.scale) { + return make(scale(self, that.scale).value - that.value, that.scale) + } + + return make(self.value - that.value, self.scale) +}) + +/** + * Internal function used for arbitrary precision division. + */ +const divideWithPrecision = ( + num: bigint, + den: bigint, + scale: number, + precision: number +): BigDecimal => { + const numNegative = num < bigint0 + const denNegative = den < bigint0 + const negateResult = numNegative !== denNegative + + num = numNegative ? -num : num + den = denNegative ? -den : den + + // Shift digits until numerator is larger than denominator (set scale appropriately). + while (num < den) { + num *= bigint10 + scale++ + } + + // First division. + let quotient = num / den + let remainder = num % den + + if (remainder === bigint0) { + // No remainder, return immediately. + return make(negateResult ? -quotient : quotient, scale) + } + + // The quotient is guaranteed to be non-negative at this point. No need to consider sign. + let count = `${quotient}`.length + + // Shift the remainder by 1 decimal; The quotient will be 1 digit upon next division. + remainder *= bigint10 + while (remainder !== bigint0 && count < precision) { + const q = remainder / den + const r = remainder % den + quotient = quotient * bigint10 + q + remainder = r * bigint10 + + count++ + scale++ + } + + if (remainder !== bigint0) { + // Round final number with remainder. + quotient += roundTerminal(remainder / den) + } + + return make(negateResult ? -quotient : quotient, scale) +} + +/** + * Internal function used for rounding. + * + * Returns 1 if the most significant digit is >= 5, otherwise 0. + * + * This is used after dividing a number by a power of ten and rounding the last digit. + * + * @internal + */ +export const roundTerminal = (n: bigint): bigint => { + const pos = n >= bigint0 ? 0 : 1 + return Number(`${n}`[pos]) < 5 ? bigint0 : bigint1 +} + +/** + * Provides a division operation on `BigDecimal`s. + * + * If the dividend is not a multiple of the divisor the result will be a `BigDecimal` value + * which represents the integer division rounded down to the nearest integer. + * + * If the divisor is `0`, the result will be `None`. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { BigDecimal, Option } from "effect" + * + * assert.deepStrictEqual(BigDecimal.divide(BigDecimal.unsafeFromString("6"), BigDecimal.unsafeFromString("3")), Option.some(BigDecimal.unsafeFromString("2"))) + * assert.deepStrictEqual(BigDecimal.divide(BigDecimal.unsafeFromString("6"), BigDecimal.unsafeFromString("4")), Option.some(BigDecimal.unsafeFromString("1.5"))) + * assert.deepStrictEqual(BigDecimal.divide(BigDecimal.unsafeFromString("6"), BigDecimal.unsafeFromString("0")), Option.none()) + * ``` + * + * @since 2.0.0 + * @category math + */ +export const divide: { + (that: BigDecimal): (self: BigDecimal) => Option.Option + (self: BigDecimal, that: BigDecimal): Option.Option +} = dual(2, (self: BigDecimal, that: BigDecimal): Option.Option => { + if (that.value === bigint0) { + return Option.none() + } + + if (self.value === bigint0) { + return Option.some(zero) + } + + const scale = self.scale - that.scale + if (self.value === that.value) { + return Option.some(make(bigint1, scale)) + } + + return Option.some(divideWithPrecision(self.value, that.value, scale, DEFAULT_PRECISION)) +}) + +/** + * Provides an unsafe division operation on `BigDecimal`s. + * + * If the dividend is not a multiple of the divisor the result will be a `BigDecimal` value + * which represents the integer division rounded down to the nearest integer. + * + * Throws a `RangeError` if the divisor is `0`. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { unsafeDivide, unsafeFromString } from "effect/BigDecimal" + * + * assert.deepStrictEqual(unsafeDivide(unsafeFromString("6"), unsafeFromString("3")), unsafeFromString("2")) + * assert.deepStrictEqual(unsafeDivide(unsafeFromString("6"), unsafeFromString("4")), unsafeFromString("1.5")) + * ``` + * + * @since 2.0.0 + * @category math + */ +export const unsafeDivide: { + (that: BigDecimal): (self: BigDecimal) => BigDecimal + (self: BigDecimal, that: BigDecimal): BigDecimal +} = dual(2, (self: BigDecimal, that: BigDecimal): BigDecimal => { + if (that.value === bigint0) { + throw new RangeError("Division by zero") + } + + if (self.value === bigint0) { + return zero + } + + const scale = self.scale - that.scale + if (self.value === that.value) { + return make(bigint1, scale) + } + return divideWithPrecision(self.value, that.value, scale, DEFAULT_PRECISION) +}) + +/** + * @since 2.0.0 + * @category instances + */ +export const Order: order.Order = order.make((self, that) => { + const scmp = order.number(sign(self), sign(that)) + if (scmp !== 0) { + return scmp + } + + if (self.scale > that.scale) { + return order.bigint(self.value, scale(that, self.scale).value) + } + + if (self.scale < that.scale) { + return order.bigint(scale(self, that.scale).value, that.value) + } + + return order.bigint(self.value, that.value) +}) + +/** + * Returns `true` if the first argument is less than the second, otherwise `false`. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { lessThan, unsafeFromString } from "effect/BigDecimal" + * + * assert.deepStrictEqual(lessThan(unsafeFromString("2"), unsafeFromString("3")), true) + * assert.deepStrictEqual(lessThan(unsafeFromString("3"), unsafeFromString("3")), false) + * assert.deepStrictEqual(lessThan(unsafeFromString("4"), unsafeFromString("3")), false) + * ``` + * + * @since 2.0.0 + * @category predicates + */ +export const lessThan: { + (that: BigDecimal): (self: BigDecimal) => boolean + (self: BigDecimal, that: BigDecimal): boolean +} = order.lessThan(Order) + +/** + * Checks if a given `BigDecimal` is less than or equal to the provided one. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { lessThanOrEqualTo, unsafeFromString } from "effect/BigDecimal" + * + * assert.deepStrictEqual(lessThanOrEqualTo(unsafeFromString("2"), unsafeFromString("3")), true) + * assert.deepStrictEqual(lessThanOrEqualTo(unsafeFromString("3"), unsafeFromString("3")), true) + * assert.deepStrictEqual(lessThanOrEqualTo(unsafeFromString("4"), unsafeFromString("3")), false) + * ``` + * + * @since 2.0.0 + * @category predicates + */ +export const lessThanOrEqualTo: { + (that: BigDecimal): (self: BigDecimal) => boolean + (self: BigDecimal, that: BigDecimal): boolean +} = order.lessThanOrEqualTo(Order) + +/** + * Returns `true` if the first argument is greater than the second, otherwise `false`. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { greaterThan, unsafeFromString } from "effect/BigDecimal" + * + * assert.deepStrictEqual(greaterThan(unsafeFromString("2"), unsafeFromString("3")), false) + * assert.deepStrictEqual(greaterThan(unsafeFromString("3"), unsafeFromString("3")), false) + * assert.deepStrictEqual(greaterThan(unsafeFromString("4"), unsafeFromString("3")), true) + * ``` + * + * @since 2.0.0 + * @category predicates + */ +export const greaterThan: { + (that: BigDecimal): (self: BigDecimal) => boolean + (self: BigDecimal, that: BigDecimal): boolean +} = order.greaterThan(Order) + +/** + * Checks if a given `BigDecimal` is greater than or equal to the provided one. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { greaterThanOrEqualTo, unsafeFromString } from "effect/BigDecimal" + * + * assert.deepStrictEqual(greaterThanOrEqualTo(unsafeFromString("2"), unsafeFromString("3")), false) + * assert.deepStrictEqual(greaterThanOrEqualTo(unsafeFromString("3"), unsafeFromString("3")), true) + * assert.deepStrictEqual(greaterThanOrEqualTo(unsafeFromString("4"), unsafeFromString("3")), true) + * ``` + * + * @since 2.0.0 + * @category predicates + */ +export const greaterThanOrEqualTo: { + (that: BigDecimal): (self: BigDecimal) => boolean + (self: BigDecimal, that: BigDecimal): boolean +} = order.greaterThanOrEqualTo(Order) + +/** + * Checks if a `BigDecimal` is between a `minimum` and `maximum` value (inclusive). + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { BigDecimal } from "effect" + * + * const between = BigDecimal.between({ + * minimum: BigDecimal.unsafeFromString("1"), + * maximum: BigDecimal.unsafeFromString("5") } + * ) + * + * assert.deepStrictEqual(between(BigDecimal.unsafeFromString("3")), true) + * assert.deepStrictEqual(between(BigDecimal.unsafeFromString("0")), false) + * assert.deepStrictEqual(between(BigDecimal.unsafeFromString("6")), false) + * ``` + * + * @since 2.0.0 + * @category predicates + */ +export const between: { + (options: { + minimum: BigDecimal + maximum: BigDecimal + }): (self: BigDecimal) => boolean + (self: BigDecimal, options: { + minimum: BigDecimal + maximum: BigDecimal + }): boolean +} = order.between(Order) + +/** + * Restricts the given `BigDecimal` to be within the range specified by the `minimum` and `maximum` values. + * + * - If the `BigDecimal` is less than the `minimum` value, the function returns the `minimum` value. + * - If the `BigDecimal` is greater than the `maximum` value, the function returns the `maximum` value. + * - Otherwise, it returns the original `BigDecimal`. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { BigDecimal } from "effect" + * + * const clamp = BigDecimal.clamp({ + * minimum: BigDecimal.unsafeFromString("1"), + * maximum: BigDecimal.unsafeFromString("5") } + * ) + * + * assert.deepStrictEqual(clamp(BigDecimal.unsafeFromString("3")), BigDecimal.unsafeFromString("3")) + * assert.deepStrictEqual(clamp(BigDecimal.unsafeFromString("0")), BigDecimal.unsafeFromString("1")) + * assert.deepStrictEqual(clamp(BigDecimal.unsafeFromString("6")), BigDecimal.unsafeFromString("5")) + * ``` + * + * @since 2.0.0 + * @category math + */ +export const clamp: { + (options: { + minimum: BigDecimal + maximum: BigDecimal + }): (self: BigDecimal) => BigDecimal + (self: BigDecimal, options: { + minimum: BigDecimal + maximum: BigDecimal + }): BigDecimal +} = order.clamp(Order) + +/** + * Returns the minimum between two `BigDecimal`s. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { min, unsafeFromString } from "effect/BigDecimal" + * + * assert.deepStrictEqual(min(unsafeFromString("2"), unsafeFromString("3")), unsafeFromString("2")) + * ``` + * + * @since 2.0.0 + * @category math + */ +export const min: { + (that: BigDecimal): (self: BigDecimal) => BigDecimal + (self: BigDecimal, that: BigDecimal): BigDecimal +} = order.min(Order) + +/** + * Returns the maximum between two `BigDecimal`s. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { max, unsafeFromString } from "effect/BigDecimal" + * + * assert.deepStrictEqual(max(unsafeFromString("2"), unsafeFromString("3")), unsafeFromString("3")) + * ``` + * + * @since 2.0.0 + * @category math + */ +export const max: { + (that: BigDecimal): (self: BigDecimal) => BigDecimal + (self: BigDecimal, that: BigDecimal): BigDecimal +} = order.max(Order) + +/** + * Determines the sign of a given `BigDecimal`. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { sign, unsafeFromString } from "effect/BigDecimal" + * + * assert.deepStrictEqual(sign(unsafeFromString("-5")), -1) + * assert.deepStrictEqual(sign(unsafeFromString("0")), 0) + * assert.deepStrictEqual(sign(unsafeFromString("5")), 1) + * ``` + * + * @since 2.0.0 + * @category math + */ +export const sign = (n: BigDecimal): Ordering => n.value === bigint0 ? 0 : n.value < bigint0 ? -1 : 1 + +/** + * Determines the absolute value of a given `BigDecimal`. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { abs, unsafeFromString } from "effect/BigDecimal" + * + * assert.deepStrictEqual(abs(unsafeFromString("-5")), unsafeFromString("5")) + * assert.deepStrictEqual(abs(unsafeFromString("0")), unsafeFromString("0")) + * assert.deepStrictEqual(abs(unsafeFromString("5")), unsafeFromString("5")) + * ``` + * + * @since 2.0.0 + * @category math + */ +export const abs = (n: BigDecimal): BigDecimal => n.value < bigint0 ? make(-n.value, n.scale) : n + +/** + * Provides a negate operation on `BigDecimal`s. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { negate, unsafeFromString } from "effect/BigDecimal" + * + * assert.deepStrictEqual(negate(unsafeFromString("3")), unsafeFromString("-3")) + * assert.deepStrictEqual(negate(unsafeFromString("-6")), unsafeFromString("6")) + * ``` + * + * @since 2.0.0 + * @category math + */ +export const negate = (n: BigDecimal): BigDecimal => make(-n.value, n.scale) + +/** + * Returns the remainder left over when one operand is divided by a second operand. + * + * If the divisor is `0`, the result will be `None`. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { BigDecimal, Option } from "effect" + * + * assert.deepStrictEqual(BigDecimal.remainder(BigDecimal.unsafeFromString("2"), BigDecimal.unsafeFromString("2")), Option.some(BigDecimal.unsafeFromString("0"))) + * assert.deepStrictEqual(BigDecimal.remainder(BigDecimal.unsafeFromString("3"), BigDecimal.unsafeFromString("2")), Option.some(BigDecimal.unsafeFromString("1"))) + * assert.deepStrictEqual(BigDecimal.remainder(BigDecimal.unsafeFromString("-4"), BigDecimal.unsafeFromString("2")), Option.some(BigDecimal.unsafeFromString("0"))) + * ``` + * + * @since 2.0.0 + * @category math + */ +export const remainder: { + (divisor: BigDecimal): (self: BigDecimal) => Option.Option + (self: BigDecimal, divisor: BigDecimal): Option.Option +} = dual(2, (self: BigDecimal, divisor: BigDecimal): Option.Option => { + if (divisor.value === bigint0) { + return Option.none() + } + + const max = Math.max(self.scale, divisor.scale) + return Option.some(make(scale(self, max).value % scale(divisor, max).value, max)) +}) + +/** + * Returns the remainder left over when one operand is divided by a second operand. + * + * Throws a `RangeError` if the divisor is `0`. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { unsafeRemainder, unsafeFromString } from "effect/BigDecimal" + * + * assert.deepStrictEqual(unsafeRemainder(unsafeFromString("2"), unsafeFromString("2")), unsafeFromString("0")) + * assert.deepStrictEqual(unsafeRemainder(unsafeFromString("3"), unsafeFromString("2")), unsafeFromString("1")) + * assert.deepStrictEqual(unsafeRemainder(unsafeFromString("-4"), unsafeFromString("2")), unsafeFromString("0")) + * ``` + * + * @since 2.0.0 + * @category math + */ +export const unsafeRemainder: { + (divisor: BigDecimal): (self: BigDecimal) => BigDecimal + (self: BigDecimal, divisor: BigDecimal): BigDecimal +} = dual(2, (self: BigDecimal, divisor: BigDecimal): BigDecimal => { + if (divisor.value === bigint0) { + throw new RangeError("Division by zero") + } + + const max = Math.max(self.scale, divisor.scale) + return make(scale(self, max).value % scale(divisor, max).value, max) +}) + +/** + * @category instances + * @since 2.0.0 + */ +export const Equivalence: equivalence.Equivalence = equivalence.make((self, that) => { + if (self.scale > that.scale) { + return scale(that, self.scale).value === self.value + } + + if (self.scale < that.scale) { + return scale(self, that.scale).value === that.value + } + + return self.value === that.value +}) + +/** + * Checks if two `BigDecimal`s are equal. + * + * @since 2.0.0 + * @category predicates + */ +export const equals: { + (that: BigDecimal): (self: BigDecimal) => boolean + (self: BigDecimal, that: BigDecimal): boolean +} = dual(2, (self: BigDecimal, that: BigDecimal): boolean => Equivalence(self, that)) + +/** + * Creates a `BigDecimal` from a `bigint` value. + * + * @since 2.0.0 + * @category constructors + */ +export const fromBigInt = (n: bigint): BigDecimal => make(n, 0) + +/** + * Creates a `BigDecimal` from a `number` value. + * + * It is not recommended to convert a floating point number to a decimal directly, + * as the floating point representation may be unexpected. + * + * Throws a `RangeError` if the number is not finite (`NaN`, `+Infinity` or `-Infinity`). + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { unsafeFromNumber, make } from "effect/BigDecimal" + * + * assert.deepStrictEqual(unsafeFromNumber(123), make(123n, 0)) + * assert.deepStrictEqual(unsafeFromNumber(123.456), make(123456n, 3)) + * ``` + * + * @since 3.11.0 + * @category constructors + */ +export const unsafeFromNumber = (n: number): BigDecimal => + Option.getOrThrowWith(safeFromNumber(n), () => new RangeError(`Number must be finite, got ${n}`)) + +/** + * Creates a `BigDecimal` from a `number` value. + * + * It is not recommended to convert a floating point number to a decimal directly, + * as the floating point representation may be unexpected. + * + * Throws a `RangeError` if the number is not finite (`NaN`, `+Infinity` or `-Infinity`). + * + * @since 2.0.0 + * @category constructors + * @deprecated Use {@link unsafeFromNumber} instead. + */ +export const fromNumber: (n: number) => BigDecimal = unsafeFromNumber + +// TODO(4.0): Rename this to `fromNumber` after removing the current, unsafe implementation of `fromNumber`. +/** + * Creates a `BigDecimal` from a `number` value. + * + * It is not recommended to convert a floating point number to a decimal directly, + * as the floating point representation may be unexpected. + * + * Returns `None` if the number is not finite (`NaN`, `+Infinity` or `-Infinity`). + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { BigDecimal, Option } from "effect" + * + * assert.deepStrictEqual(BigDecimal.safeFromNumber(123), Option.some(BigDecimal.make(123n, 0))) + * assert.deepStrictEqual(BigDecimal.safeFromNumber(123.456), Option.some(BigDecimal.make(123456n, 3))) + * assert.deepStrictEqual(BigDecimal.safeFromNumber(Infinity), Option.none()) + * ``` + * + * @since 3.11.0 + * @category constructors + */ +export const safeFromNumber = (n: number): Option.Option => { + if (!Number.isFinite(n)) { + return Option.none() + } + + const string = `${n}` + if (string.includes("e")) { + return fromString(string) + } + + const [lead, trail = ""] = string.split(".") + return Option.some(make(BigInt(`${lead}${trail}`), trail.length)) +} + +/** + * Parses a numerical `string` into a `BigDecimal`. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { BigDecimal, Option } from "effect" + * + * assert.deepStrictEqual(BigDecimal.fromString("123"), Option.some(BigDecimal.make(123n, 0))) + * assert.deepStrictEqual(BigDecimal.fromString("123.456"), Option.some(BigDecimal.make(123456n, 3))) + * assert.deepStrictEqual(BigDecimal.fromString("123.abc"), Option.none()) + * ``` + * + * @since 2.0.0 + * @category constructors + */ +export const fromString = (s: string): Option.Option => { + if (s === "") { + return Option.some(zero) + } + + let base: string + let exp: number + const seperator = s.search(/[eE]/) + if (seperator !== -1) { + const trail = s.slice(seperator + 1) + base = s.slice(0, seperator) + exp = Number(trail) + if (base === "" || !Number.isSafeInteger(exp) || !FINITE_INT_REGEX.test(trail)) { + return Option.none() + } + } else { + base = s + exp = 0 + } + + let digits: string + let offset: number + const dot = base.search(/\./) + if (dot !== -1) { + const lead = base.slice(0, dot) + const trail = base.slice(dot + 1) + digits = `${lead}${trail}` + offset = trail.length + } else { + digits = base + offset = 0 + } + + if (!FINITE_INT_REGEX.test(digits)) { + return Option.none() + } + + const scale = offset - exp + if (!Number.isSafeInteger(scale)) { + return Option.none() + } + + return Option.some(make(BigInt(digits), scale)) +} + +/** + * Parses a numerical `string` into a `BigDecimal`. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { unsafeFromString, make } from "effect/BigDecimal" + * + * assert.deepStrictEqual(unsafeFromString("123"), make(123n, 0)) + * assert.deepStrictEqual(unsafeFromString("123.456"), make(123456n, 3)) + * assert.throws(() => unsafeFromString("123.abc")) + * ``` + * + * @since 2.0.0 + * @category constructors + */ +export const unsafeFromString = (s: string): BigDecimal => + Option.getOrThrowWith(fromString(s), () => new Error("Invalid numerical string")) + +/** + * Formats a given `BigDecimal` as a `string`. + * + * If the scale of the `BigDecimal` is greater than or equal to 16, the `BigDecimal` will + * be formatted in scientific notation. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { format, unsafeFromString } from "effect/BigDecimal" + * + * assert.deepStrictEqual(format(unsafeFromString("-5")), "-5") + * assert.deepStrictEqual(format(unsafeFromString("123.456")), "123.456") + * assert.deepStrictEqual(format(unsafeFromString("-0.00000123")), "-0.00000123") + * ``` + * + * @since 2.0.0 + * @category conversions + */ +export const format = (n: BigDecimal): string => { + const normalized = normalize(n) + if (Math.abs(normalized.scale) >= 16) { + return toExponential(normalized) + } + + const negative = normalized.value < bigint0 + const absolute = negative ? `${normalized.value}`.substring(1) : `${normalized.value}` + + let before: string + let after: string + + if (normalized.scale >= absolute.length) { + before = "0" + after = "0".repeat(normalized.scale - absolute.length) + absolute + } else { + const location = absolute.length - normalized.scale + if (location > absolute.length) { + const zeros = location - absolute.length + before = `${absolute}${"0".repeat(zeros)}` + after = "" + } else { + after = absolute.slice(location) + before = absolute.slice(0, location) + } + } + + const complete = after === "" ? before : `${before}.${after}` + return negative ? `-${complete}` : complete +} + +/** + * Formats a given `BigDecimal` as a `string` in scientific notation. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { toExponential, make } from "effect/BigDecimal" + * + * assert.deepStrictEqual(toExponential(make(123456n, -5)), "1.23456e+10") + * ``` + * + * @since 3.11.0 + * @category conversions + */ +export const toExponential = (n: BigDecimal): string => { + if (isZero(n)) { + return "0e+0" + } + + const normalized = normalize(n) + const digits = `${abs(normalized).value}` + const head = digits.slice(0, 1) + const tail = digits.slice(1) + + let output = `${isNegative(normalized) ? "-" : ""}${head}` + if (tail !== "") { + output += `.${tail}` + } + + const exp = tail.length - normalized.scale + return `${output}e${exp >= 0 ? "+" : ""}${exp}` +} + +/** + * Converts a `BigDecimal` to a `number`. + * + * This function will produce incorrect results if the `BigDecimal` exceeds the 64-bit range of a `number`. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { unsafeToNumber, unsafeFromString } from "effect/BigDecimal" + * + * assert.deepStrictEqual(unsafeToNumber(unsafeFromString("123.456")), 123.456) + * ``` + * + * @since 2.0.0 + * @category conversions + */ +export const unsafeToNumber = (n: BigDecimal): number => Number(format(n)) + +/** + * Checks if a given `BigDecimal` is an integer. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { isInteger, unsafeFromString } from "effect/BigDecimal" + * + * assert.deepStrictEqual(isInteger(unsafeFromString("0")), true) + * assert.deepStrictEqual(isInteger(unsafeFromString("1")), true) + * assert.deepStrictEqual(isInteger(unsafeFromString("1.1")), false) + * ``` + * + * @since 2.0.0 + * @category predicates + */ +export const isInteger = (n: BigDecimal): boolean => normalize(n).scale <= 0 + +/** + * Checks if a given `BigDecimal` is `0`. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { isZero, unsafeFromString } from "effect/BigDecimal" + * + * assert.deepStrictEqual(isZero(unsafeFromString("0")), true) + * assert.deepStrictEqual(isZero(unsafeFromString("1")), false) + * ``` + * + * @since 2.0.0 + * @category predicates + */ +export const isZero = (n: BigDecimal): boolean => n.value === bigint0 + +/** + * Checks if a given `BigDecimal` is negative. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { isNegative, unsafeFromString } from "effect/BigDecimal" + * + * assert.deepStrictEqual(isNegative(unsafeFromString("-1")), true) + * assert.deepStrictEqual(isNegative(unsafeFromString("0")), false) + * assert.deepStrictEqual(isNegative(unsafeFromString("1")), false) + * ``` + * + * @since 2.0.0 + * @category predicates + */ +export const isNegative = (n: BigDecimal): boolean => n.value < bigint0 + +/** + * Checks if a given `BigDecimal` is positive. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { isPositive, unsafeFromString } from "effect/BigDecimal" + * + * assert.deepStrictEqual(isPositive(unsafeFromString("-1")), false) + * assert.deepStrictEqual(isPositive(unsafeFromString("0")), false) + * assert.deepStrictEqual(isPositive(unsafeFromString("1")), true) + * ``` + * + * @since 2.0.0 + * @category predicates + */ +export const isPositive = (n: BigDecimal): boolean => n.value > bigint0 + +const isBigDecimalArgs = (args: IArguments) => isBigDecimal(args[0]) + +/** + * Calculate the ceiling of a `BigDecimal` at the given scale. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { ceil, unsafeFromString } from "effect/BigDecimal" + * + * assert.deepStrictEqual(ceil(unsafeFromString("145"), -1), unsafeFromString("150")) + * assert.deepStrictEqual(ceil(unsafeFromString("-14.5")), unsafeFromString("-14")) + * ``` + * + * @since 3.16.0 + * @category math + */ +export const ceil: { + (scale: number): (self: BigDecimal) => BigDecimal + (self: BigDecimal, scale?: number): BigDecimal +} = dual(isBigDecimalArgs, (self: BigDecimal, scale: number = 0): BigDecimal => { + const truncated = truncate(self, scale) + + if (isPositive(self) && lessThan(truncated, self)) { + return sum(truncated, make(1n, scale)) + } + + return truncated +}) + +/** + * Calculate the floor of a `BigDecimal` at the given scale. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { floor, unsafeFromString } from "effect/BigDecimal" + * + * assert.deepStrictEqual(floor(unsafeFromString("145"), -1), unsafeFromString("140")) + * assert.deepStrictEqual(floor(unsafeFromString("-14.5")), unsafeFromString("-15")) + * ``` + * + * @since 3.16.0 + * @category math + */ +export const floor: { + (scale: number): (self: BigDecimal) => BigDecimal + (self: BigDecimal, scale?: number): BigDecimal +} = dual(isBigDecimalArgs, (self: BigDecimal, scale: number = 0): BigDecimal => { + const truncated = truncate(self, scale) + + if (isNegative(self) && greaterThan(truncated, self)) { + return sum(truncated, make(-1n, scale)) + } + + return truncated +}) + +/** + * Truncate a `BigDecimal` at the given scale. This is the same operation as rounding away from zero. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { truncate, unsafeFromString } from "effect/BigDecimal" + * + * assert.deepStrictEqual(truncate(unsafeFromString("145"), -1), unsafeFromString("140")) + * assert.deepStrictEqual(truncate(unsafeFromString("-14.5")), unsafeFromString("-14")) + * ``` + * + * @since 3.16.0 + * @category math + */ +export const truncate: { + (scale: number): (self: BigDecimal) => BigDecimal + (self: BigDecimal, scale?: number): BigDecimal +} = dual(isBigDecimalArgs, (self: BigDecimal, scale: number = 0): BigDecimal => { + if (self.scale <= scale) { + return self + } + + // BigInt division truncates towards zero + return make(self.value / (10n ** BigInt(self.scale - scale)), scale) +}) + +/** + * Internal function used by `round` for `half-even` and `half-odd` rounding modes. + * + * Returns the digit at the position of the given `scale` within the `BigDecimal`. + * + * @internal + */ +export const digitAt: { + (scale: number): (self: BigDecimal) => bigint + (self: BigDecimal, scale: number): bigint +} = dual(2, (self: BigDecimal, scale: number): bigint => { + if (self.scale < scale) { + return 0n + } + + const scaled = self.value / (10n ** BigInt(self.scale - scale)) + return scaled % 10n +}) + +/** + * Rounding modes for `BigDecimal`. + * + * `ceil`: round towards positive infinity + * `floor`: round towards negative infinity + * `to-zero`: round towards zero + * `from-zero`: round away from zero + * `half-ceil`: round to the nearest neighbor; if equidistant round towards positive infinity + * `half-floor`: round to the nearest neighbor; if equidistant round towards negative infinity + * `half-to-zero`: round to the nearest neighbor; if equidistant round towards zero + * `half-from-zero`: round to the nearest neighbor; if equidistant round away from zero + * `half-even`: round to the nearest neighbor; if equidistant round to the neighbor with an even digit + * `half-odd`: round to the nearest neighbor; if equidistant round to the neighbor with an odd digit + * + * @since 3.16.0 + * @category math + */ +export type RoundingMode = + | "ceil" + | "floor" + | "to-zero" + | "from-zero" + | "half-ceil" + | "half-floor" + | "half-to-zero" + | "half-from-zero" + | "half-even" + | "half-odd" + +/** + * Rounds a `BigDecimal` at the given scale with the specified rounding mode. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { round, unsafeFromString } from "effect/BigDecimal" + * + * assert.deepStrictEqual(round(unsafeFromString("145"), { mode: "from-zero", scale: -1 }), unsafeFromString("150")) + * assert.deepStrictEqual(round(unsafeFromString("-14.5")), unsafeFromString("-15")) + * ``` + * + * @since 3.16.0 + * @category math + */ +export const round: { + (options: { scale?: number; mode?: RoundingMode }): (self: BigDecimal) => BigDecimal + (n: BigDecimal, options?: { scale?: number; mode?: RoundingMode }): BigDecimal +} = dual(isBigDecimalArgs, (self: BigDecimal, options?: { scale?: number; mode?: RoundingMode }): BigDecimal => { + const mode = options?.mode ?? "half-from-zero" + const scale = options?.scale ?? 0 + + switch (mode) { + case "ceil": + return ceil(self, scale) + + case "floor": + return floor(self, scale) + + case "to-zero": + return truncate(self, scale) + + case "from-zero": + return (isPositive(self) ? ceil(self, scale) : floor(self, scale)) + + case "half-ceil": + return floor(sum(self, make(5n, scale + 1)), scale) + + case "half-floor": + return ceil(sum(self, make(-5n, scale + 1)), scale) + + case "half-to-zero": + return isNegative(self) + ? floor(sum(self, make(5n, scale + 1)), scale) + : ceil(sum(self, make(-5n, scale + 1)), scale) + + case "half-from-zero": + return isNegative(self) + ? ceil(sum(self, make(-5n, scale + 1)), scale) + : floor(sum(self, make(5n, scale + 1)), scale) + } + + const halfCeil = floor(sum(self, make(5n, scale + 1)), scale) + const halfFloor = ceil(sum(self, make(-5n, scale + 1)), scale) + const digit = digitAt(halfCeil, scale) + + switch (mode) { + case "half-even": + return equals(halfCeil, halfFloor) ? halfCeil : (digit % 2n === 0n) ? halfCeil : halfFloor + + case "half-odd": + return equals(halfCeil, halfFloor) ? halfCeil : (digit % 2n === 0n) ? halfFloor : halfCeil + } +}) + +/** + * Takes an `Iterable` of `BigDecimal`s and returns their sum as a single `BigDecimal` + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { unsafeFromString, sumAll } from "effect/BigDecimal" + * + * assert.deepStrictEqual(sumAll([unsafeFromString("2"), unsafeFromString("3"), unsafeFromString("4")]), unsafeFromString("9")) + * ``` + * + * @category math + * @since 3.16.0 + */ +export const sumAll = (collection: Iterable): BigDecimal => { + let out = zero + for (const n of collection) { + out = sum(out, n) + } + + return out +} diff --git a/repos/effect/packages/effect/src/BigInt.ts b/repos/effect/packages/effect/src/BigInt.ts new file mode 100644 index 0000000..8a87f64 --- /dev/null +++ b/repos/effect/packages/effect/src/BigInt.ts @@ -0,0 +1,643 @@ +/** + * This module provides utility functions and type class instances for working with the `bigint` type in TypeScript. + * It includes functions for basic arithmetic operations, as well as type class instances for + * `Equivalence` and `Order`. + * + * @module BigInt + * @since 2.0.0 + * @see {@link module:BigDecimal} for more similar operations on `BigDecimal` types + * @see {@link module:Number} for more similar operations on `number` types + */ + +import * as equivalence from "./Equivalence.js" +import { dual } from "./Function.js" +import * as Option from "./Option.js" +import * as order from "./Order.js" +import type { Ordering } from "./Ordering.js" +import * as predicate from "./Predicate.js" + +const bigint0 = BigInt(0) +const bigint1 = BigInt(1) +const bigint2 = BigInt(2) + +/** + * Tests if a value is a `bigint`. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { isBigInt } from "effect/BigInt" + * + * assert.deepStrictEqual(isBigInt(1n), true) + * assert.deepStrictEqual(isBigInt(1), false) + * ``` + * + * @category guards + * @since 2.0.0 + */ +export const isBigInt: (u: unknown) => u is bigint = predicate.isBigInt + +/** + * Provides an addition operation on `bigint`s. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { sum } from "effect/BigInt" + * + * assert.deepStrictEqual(sum(2n, 3n), 5n) + * ``` + * + * @category math + * @since 2.0.0 + */ +export const sum: { + (that: bigint): (self: bigint) => bigint + (self: bigint, that: bigint): bigint +} = dual(2, (self: bigint, that: bigint): bigint => self + that) + +/** + * Provides a multiplication operation on `bigint`s. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { multiply } from "effect/BigInt" + * + * assert.deepStrictEqual(multiply(2n, 3n), 6n) + * ``` + * + * @category math + * @since 2.0.0 + */ +export const multiply: { + (that: bigint): (self: bigint) => bigint + (self: bigint, that: bigint): bigint +} = dual(2, (self: bigint, that: bigint): bigint => self * that) + +/** + * Provides a subtraction operation on `bigint`s. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { subtract } from "effect/BigInt" + * + * assert.deepStrictEqual(subtract(2n, 3n), -1n) + * ``` + * + * @category math + * @since 2.0.0 + */ +export const subtract: { + (that: bigint): (self: bigint) => bigint + (self: bigint, that: bigint): bigint +} = dual(2, (self: bigint, that: bigint): bigint => self - that) + +/** + * Provides a division operation on `bigint`s. + * + * If the dividend is not a multiple of the divisor the result will be a `bigint` value + * which represents the integer division rounded down to the nearest integer. + * + * Returns `None` if the divisor is `0n`. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { BigInt, Option } from "effect" + * + * assert.deepStrictEqual(BigInt.divide(6n, 3n), Option.some(2n)) + * assert.deepStrictEqual(BigInt.divide(6n, 0n), Option.none()) + * ``` + * + * @category math + * @since 2.0.0 + */ +export const divide: { + (that: bigint): (self: bigint) => Option.Option + (self: bigint, that: bigint): Option.Option +} = dual( + 2, + (self: bigint, that: bigint): Option.Option => that === bigint0 ? Option.none() : Option.some(self / that) +) + +/** + * Provides a division operation on `bigint`s. + * + * If the dividend is not a multiple of the divisor the result will be a `bigint` value + * which represents the integer division rounded down to the nearest integer. + * + * Throws a `RangeError` if the divisor is `0n`. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { unsafeDivide } from "effect/BigInt" + * + * assert.deepStrictEqual(unsafeDivide(6n, 3n), 2n) + * assert.deepStrictEqual(unsafeDivide(6n, 4n), 1n) + * ``` + * + * @category math + * @since 2.0.0 + */ +export const unsafeDivide: { + (that: bigint): (self: bigint) => bigint + (self: bigint, that: bigint): bigint +} = dual(2, (self: bigint, that: bigint): bigint => self / that) + +/** + * Returns the result of adding `1n` to a given number. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { increment } from "effect/BigInt" + * + * assert.deepStrictEqual(increment(2n), 3n) + * ``` + * + * @category math + * @since 2.0.0 + */ +export const increment = (n: bigint): bigint => n + bigint1 + +/** + * Decrements a number by `1n`. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { decrement } from "effect/BigInt" + * + * assert.deepStrictEqual(decrement(3n), 2n) + * ``` + * + * @category math + * @since 2.0.0 + */ +export const decrement = (n: bigint): bigint => n - bigint1 + +/** + * @category instances + * @since 2.0.0 + */ +export const Equivalence: equivalence.Equivalence = equivalence.bigint + +/** + * @category instances + * @since 2.0.0 + */ +export const Order: order.Order = order.bigint + +/** + * Returns `true` if the first argument is less than the second, otherwise `false`. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { lessThan } from "effect/BigInt" + * + * assert.deepStrictEqual(lessThan(2n, 3n), true) + * assert.deepStrictEqual(lessThan(3n, 3n), false) + * assert.deepStrictEqual(lessThan(4n, 3n), false) + * ``` + * + * @category predicates + * @since 2.0.0 + */ +export const lessThan: { + (that: bigint): (self: bigint) => boolean + (self: bigint, that: bigint): boolean +} = order.lessThan(Order) + +/** + * Returns a function that checks if a given `bigint` is less than or equal to the provided one. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { lessThanOrEqualTo } from "effect/BigInt" + * + * assert.deepStrictEqual(lessThanOrEqualTo(2n, 3n), true) + * assert.deepStrictEqual(lessThanOrEqualTo(3n, 3n), true) + * assert.deepStrictEqual(lessThanOrEqualTo(4n, 3n), false) + * ``` + * + * @category predicates + * @since 2.0.0 + */ +export const lessThanOrEqualTo: { + (that: bigint): (self: bigint) => boolean + (self: bigint, that: bigint): boolean +} = order.lessThanOrEqualTo(Order) + +/** + * Returns `true` if the first argument is greater than the second, otherwise `false`. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { greaterThan } from "effect/BigInt" + * + * assert.deepStrictEqual(greaterThan(2n, 3n), false) + * assert.deepStrictEqual(greaterThan(3n, 3n), false) + * assert.deepStrictEqual(greaterThan(4n, 3n), true) + * ``` + * + * @category predicates + * @since 2.0.0 + */ +export const greaterThan: { + (that: bigint): (self: bigint) => boolean + (self: bigint, that: bigint): boolean +} = order.greaterThan(Order) + +/** + * Returns a function that checks if a given `bigint` is greater than or equal to the provided one. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { greaterThanOrEqualTo } from "effect/BigInt" + * + * assert.deepStrictEqual(greaterThanOrEqualTo(2n, 3n), false) + * assert.deepStrictEqual(greaterThanOrEqualTo(3n, 3n), true) + * assert.deepStrictEqual(greaterThanOrEqualTo(4n, 3n), true) + * ``` + * + * @category predicates + * @since 2.0.0 + */ +export const greaterThanOrEqualTo: { + (that: bigint): (self: bigint) => boolean + (self: bigint, that: bigint): boolean +} = order.greaterThanOrEqualTo(Order) + +/** + * Checks if a `bigint` is between a `minimum` and `maximum` value (inclusive). + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { BigInt } from "effect" + * + * const between = BigInt.between({ minimum: 0n, maximum: 5n }) + * + * assert.deepStrictEqual(between(3n), true) + * assert.deepStrictEqual(between(-1n), false) + * assert.deepStrictEqual(between(6n), false) + * ``` + * + * @category predicates + * @since 2.0.0 + */ +export const between: { + (options: { + minimum: bigint + maximum: bigint + }): (self: bigint) => boolean + (self: bigint, options: { + minimum: bigint + maximum: bigint + }): boolean +} = order.between(Order) + +/** + * Restricts the given `bigint` to be within the range specified by the `minimum` and `maximum` values. + * + * - If the `bigint` is less than the `minimum` value, the function returns the `minimum` value. + * - If the `bigint` is greater than the `maximum` value, the function returns the `maximum` value. + * - Otherwise, it returns the original `bigint`. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { BigInt } from "effect" + * + * const clamp = BigInt.clamp({ minimum: 1n, maximum: 5n }) + * + * assert.equal(clamp(3n), 3n) + * assert.equal(clamp(0n), 1n) + * assert.equal(clamp(6n), 5n) + * ``` + * + * @since 2.0.0 + */ +export const clamp: { + (options: { + minimum: bigint + maximum: bigint + }): (self: bigint) => bigint + (self: bigint, options: { + minimum: bigint + maximum: bigint + }): bigint +} = order.clamp(Order) + +/** + * Returns the minimum between two `bigint`s. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { min } from "effect/BigInt" + * + * assert.deepStrictEqual(min(2n, 3n), 2n) + * ``` + * + * @since 2.0.0 + */ +export const min: { + (that: bigint): (self: bigint) => bigint + (self: bigint, that: bigint): bigint +} = order.min(Order) + +/** + * Returns the maximum between two `bigint`s. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { max } from "effect/BigInt" + * + * assert.deepStrictEqual(max(2n, 3n), 3n) + * ``` + * + * @since 2.0.0 + */ +export const max: { + (that: bigint): (self: bigint) => bigint + (self: bigint, that: bigint): bigint +} = order.max(Order) + +/** + * Determines the sign of a given `bigint`. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { sign } from "effect/BigInt" + * + * assert.deepStrictEqual(sign(-5n), -1) + * assert.deepStrictEqual(sign(0n), 0) + * assert.deepStrictEqual(sign(5n), 1) + * ``` + * + * @category math + * @since 2.0.0 + */ +export const sign = (n: bigint): Ordering => Order(n, bigint0) + +/** + * Determines the absolute value of a given `bigint`. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { abs } from "effect/BigInt" + * + * assert.deepStrictEqual(abs(-5n), 5n) + * assert.deepStrictEqual(abs(0n), 0n) + * assert.deepStrictEqual(abs(5n), 5n) + * ``` + * + * @category math + * @since 2.0.0 + */ +export const abs = (n: bigint): bigint => (n < bigint0 ? -n : n) + +/** + * Determines the greatest common divisor of two `bigint`s. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { gcd } from "effect/BigInt" + * + * assert.deepStrictEqual(gcd(2n, 3n), 1n) + * assert.deepStrictEqual(gcd(2n, 4n), 2n) + * assert.deepStrictEqual(gcd(16n, 24n), 8n) + * ``` + * + * @category math + * @since 2.0.0 + */ +export const gcd: { + (that: bigint): (self: bigint) => bigint + (self: bigint, that: bigint): bigint +} = dual(2, (self: bigint, that: bigint): bigint => { + while (that !== bigint0) { + const t = that + that = self % that + self = t + } + return self +}) + +/** + * Determines the least common multiple of two `bigint`s. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { lcm } from "effect/BigInt" + * + * assert.deepStrictEqual(lcm(2n, 3n), 6n) + * assert.deepStrictEqual(lcm(2n, 4n), 4n) + * assert.deepStrictEqual(lcm(16n, 24n), 48n) + * ``` + * + * @category math + * @since 2.0.0 + */ +export const lcm: { + (that: bigint): (self: bigint) => bigint + (self: bigint, that: bigint): bigint +} = dual(2, (self: bigint, that: bigint): bigint => (self * that) / gcd(self, that)) + +/** + * Determines the square root of a given `bigint` unsafely. Throws if the given `bigint` is negative. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { unsafeSqrt } from "effect/BigInt" + * + * assert.deepStrictEqual(unsafeSqrt(4n), 2n) + * assert.deepStrictEqual(unsafeSqrt(9n), 3n) + * assert.deepStrictEqual(unsafeSqrt(16n), 4n) + * ``` + * + * @category math + * @since 2.0.0 + */ +export const unsafeSqrt = (n: bigint): bigint => { + if (n < bigint0) { + throw new RangeError("Cannot take the square root of a negative number") + } + if (n < bigint2) { + return n + } + let x = n / bigint2 + while (x * x > n) { + x = ((n / x) + x) / bigint2 + } + return x +} + +/** + * Determines the square root of a given `bigint` safely. Returns `none` if the given `bigint` is negative. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { BigInt, Option } from "effect" + * + * assert.deepStrictEqual(BigInt.sqrt(4n), Option.some(2n)) + * assert.deepStrictEqual(BigInt.sqrt(9n), Option.some(3n)) + * assert.deepStrictEqual(BigInt.sqrt(16n), Option.some(4n)) + * assert.deepStrictEqual(BigInt.sqrt(-1n), Option.none()) + * ``` + * + * @category math + * @since 2.0.0 + */ +export const sqrt = (n: bigint): Option.Option => + greaterThanOrEqualTo(n, bigint0) ? Option.some(unsafeSqrt(n)) : Option.none() + +/** + * Takes an `Iterable` of `bigint`s and returns their sum as a single `bigint + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { sumAll } from "effect/BigInt" + * + * assert.deepStrictEqual(sumAll([2n, 3n, 4n]), 9n) + * ``` + * + * @category math + * @since 2.0.0 + */ +export const sumAll = (collection: Iterable): bigint => { + let out = bigint0 + for (const n of collection) { + out += n + } + return out +} + +/** + * Takes an `Iterable` of `bigint`s and returns their multiplication as a single `number`. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { multiplyAll } from "effect/BigInt" + * + * assert.deepStrictEqual(multiplyAll([2n, 3n, 4n]), 24n) + * ``` + * + * @category math + * @since 2.0.0 + */ +export const multiplyAll = (collection: Iterable): bigint => { + let out = bigint1 + for (const n of collection) { + if (n === bigint0) { + return bigint0 + } + out *= n + } + return out +} + +/** + * Takes a `bigint` and returns an `Option` of `number`. + * + * If the `bigint` is outside the safe integer range for JavaScript (`Number.MAX_SAFE_INTEGER` + * and `Number.MIN_SAFE_INTEGER`), it returns `Option.none()`. Otherwise, it converts the `bigint` + * to a number and returns `Option.some(number)`. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { BigInt as BI, Option } from "effect" + * + * assert.deepStrictEqual(BI.toNumber(BigInt(42)), Option.some(42)) + * assert.deepStrictEqual(BI.toNumber(BigInt(Number.MAX_SAFE_INTEGER) + BigInt(1)), Option.none()) + * assert.deepStrictEqual(BI.toNumber(BigInt(Number.MIN_SAFE_INTEGER) - BigInt(1)), Option.none()) + * ``` + * + * @category conversions + * @since 2.0.0 + */ +export const toNumber = (b: bigint): Option.Option => { + if (b > BigInt(Number.MAX_SAFE_INTEGER) || b < BigInt(Number.MIN_SAFE_INTEGER)) { + return Option.none() + } + return Option.some(Number(b)) +} + +/** + * Takes a string and returns an `Option` of `bigint`. + * + * If the string is empty or contains characters that cannot be converted into a `bigint`, + * it returns `Option.none()`, otherwise, it returns `Option.some(bigint)`. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { BigInt as BI, Option } from "effect" + * + * assert.deepStrictEqual(BI.fromString("42"), Option.some(BigInt(42))) + * assert.deepStrictEqual(BI.fromString(" "), Option.none()) + * assert.deepStrictEqual(BI.fromString("a"), Option.none()) + * ``` + * + * @category conversions + * @since 2.4.12 + */ +export const fromString = (s: string): Option.Option => { + try { + return s.trim() === "" + ? Option.none() + : Option.some(BigInt(s)) + } catch { + return Option.none() + } +} + +/** + * Takes a number and returns an `Option` of `bigint`. + * + * If the number is outside the safe integer range for JavaScript (`Number.MAX_SAFE_INTEGER` + * and `Number.MIN_SAFE_INTEGER`), it returns `Option.none()`. Otherwise, it attempts to + * convert the number to a `bigint` and returns `Option.some(bigint)`. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { BigInt as BI, Option } from "effect" + * + * assert.deepStrictEqual(BI.fromNumber(42), Option.some(BigInt(42))) + * assert.deepStrictEqual(BI.fromNumber(Number.MAX_SAFE_INTEGER + 1), Option.none()) + * assert.deepStrictEqual(BI.fromNumber(Number.MIN_SAFE_INTEGER - 1), Option.none()) + * ``` + * + * @category conversions + * @since 2.4.12 + */ +export const fromNumber = (n: number): Option.Option => { + if (n > Number.MAX_SAFE_INTEGER || n < Number.MIN_SAFE_INTEGER) { + return Option.none() + } + + try { + return Option.some(BigInt(n)) + } catch { + return Option.none() + } +} diff --git a/repos/effect/packages/effect/src/Boolean.ts b/repos/effect/packages/effect/src/Boolean.ts new file mode 100644 index 0000000..5296433 --- /dev/null +++ b/repos/effect/packages/effect/src/Boolean.ts @@ -0,0 +1,287 @@ +/** + * This module provides utility functions and type class instances for working with the `boolean` type in TypeScript. + * It includes functions for basic boolean operations, as well as type class instances for + * `Equivalence` and `Order`. + * + * @since 2.0.0 + */ +import * as equivalence from "./Equivalence.js" +import type { LazyArg } from "./Function.js" +import { dual } from "./Function.js" +import * as order from "./Order.js" +import * as predicate from "./Predicate.js" + +/** + * Tests if a value is a `boolean`. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { isBoolean } from "effect/Boolean" + * + * assert.deepStrictEqual(isBoolean(true), true) + * assert.deepStrictEqual(isBoolean("true"), false) + * ``` + * + * @category guards + * @since 2.0.0 + */ +export const isBoolean: (input: unknown) => input is boolean = predicate.isBoolean + +/** + * This function returns the result of either of the given functions depending on the value of the boolean parameter. + * It is useful when you have to run one of two functions depending on the boolean value. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { Boolean } from "effect" + * + * assert.deepStrictEqual(Boolean.match(true, { onFalse: () => "It's false!", onTrue: () => "It's true!" }), "It's true!") + * ``` + * + * @category pattern matching + * @since 2.0.0 + */ +export const match: { + (options: { + readonly onFalse: LazyArg + readonly onTrue: LazyArg + }): (value: boolean) => A | B + (value: boolean, options: { + readonly onFalse: LazyArg + readonly onTrue: LazyArg + }): A | B +} = dual(2, (value: boolean, options: { + readonly onFalse: LazyArg + readonly onTrue: LazyArg +}): A | B => value ? options.onTrue() : options.onFalse()) + +/** + * @category instances + * @since 2.0.0 + */ +export const Equivalence: equivalence.Equivalence = equivalence.boolean + +/** + * @category instances + * @since 2.0.0 + */ +export const Order: order.Order = order.boolean + +/** + * Negates the given boolean: `!self` + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { not } from "effect/Boolean" + * + * assert.deepStrictEqual(not(true), false) + * assert.deepStrictEqual(not(false), true) + * ``` + * + * @category combinators + * @since 2.0.0 + */ +export const not = (self: boolean): boolean => !self + +/** + * Combines two boolean using AND: `self && that`. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { and } from "effect/Boolean" + * + * assert.deepStrictEqual(and(true, true), true) + * assert.deepStrictEqual(and(true, false), false) + * assert.deepStrictEqual(and(false, true), false) + * assert.deepStrictEqual(and(false, false), false) + * ``` + * + * @category combinators + * @since 2.0.0 + */ +export const and: { + (that: boolean): (self: boolean) => boolean + (self: boolean, that: boolean): boolean +} = dual(2, (self: boolean, that: boolean): boolean => self && that) + +/** + * Combines two boolean using NAND: `!(self && that)`. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { nand } from "effect/Boolean" + * + * assert.deepStrictEqual(nand(true, true), false) + * assert.deepStrictEqual(nand(true, false), true) + * assert.deepStrictEqual(nand(false, true), true) + * assert.deepStrictEqual(nand(false, false), true) + * ``` + * + * @category combinators + * @since 2.0.0 + */ +export const nand: { + (that: boolean): (self: boolean) => boolean + (self: boolean, that: boolean): boolean +} = dual(2, (self: boolean, that: boolean): boolean => !(self && that)) + +/** + * Combines two boolean using OR: `self || that`. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { or } from "effect/Boolean" + * + * assert.deepStrictEqual(or(true, true), true) + * assert.deepStrictEqual(or(true, false), true) + * assert.deepStrictEqual(or(false, true), true) + * assert.deepStrictEqual(or(false, false), false) + * ``` + * + * @category combinators + * @since 2.0.0 + */ +export const or: { + (that: boolean): (self: boolean) => boolean + (self: boolean, that: boolean): boolean +} = dual(2, (self: boolean, that: boolean): boolean => self || that) + +/** + * Combines two booleans using NOR: `!(self || that)`. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { nor } from "effect/Boolean" + * + * assert.deepStrictEqual(nor(true, true), false) + * assert.deepStrictEqual(nor(true, false), false) + * assert.deepStrictEqual(nor(false, true), false) + * assert.deepStrictEqual(nor(false, false), true) + * ``` + * + * @category combinators + * @since 2.0.0 + */ +export const nor: { + (that: boolean): (self: boolean) => boolean + (self: boolean, that: boolean): boolean +} = dual(2, (self: boolean, that: boolean): boolean => !(self || that)) + +/** + * Combines two booleans using XOR: `(!self && that) || (self && !that)`. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { xor } from "effect/Boolean" + * + * assert.deepStrictEqual(xor(true, true), false) + * assert.deepStrictEqual(xor(true, false), true) + * assert.deepStrictEqual(xor(false, true), true) + * assert.deepStrictEqual(xor(false, false), false) + * ``` + * + * @category combinators + * @since 2.0.0 + */ +export const xor: { + (that: boolean): (self: boolean) => boolean + (self: boolean, that: boolean): boolean +} = dual(2, (self: boolean, that: boolean): boolean => (!self && that) || (self && !that)) + +/** + * Combines two booleans using EQV (aka XNOR): `!xor(self, that)`. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { eqv } from "effect/Boolean" + * + * assert.deepStrictEqual(eqv(true, true), true) + * assert.deepStrictEqual(eqv(true, false), false) + * assert.deepStrictEqual(eqv(false, true), false) + * assert.deepStrictEqual(eqv(false, false), true) + * ``` + * + * @category combinators + * @since 2.0.0 + */ +export const eqv: { + (that: boolean): (self: boolean) => boolean + (self: boolean, that: boolean): boolean +} = dual(2, (self: boolean, that: boolean): boolean => !xor(self, that)) + +/** + * Combines two booleans using an implication: `(!self || that)`. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { implies } from "effect/Boolean" + * + * assert.deepStrictEqual(implies(true, true), true) + * assert.deepStrictEqual(implies(true, false), false) + * assert.deepStrictEqual(implies(false, true), true) + * assert.deepStrictEqual(implies(false, false), true) + * ``` + * + * @category combinators + * @since 2.0.0 + */ +export const implies: { + (that: boolean): (self: boolean) => boolean + (self: boolean, that: boolean): boolean +} = dual(2, (self, that) => self ? that : true) + +/** + * This utility function is used to check if all the elements in a collection of boolean values are `true`. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { every } from "effect/Boolean" + * + * assert.deepStrictEqual(every([true, true, true]), true) + * assert.deepStrictEqual(every([true, false, true]), false) + * ``` + * + * @since 2.0.0 + */ +export const every = (collection: Iterable): boolean => { + for (const b of collection) { + if (!b) { + return false + } + } + return true +} + +/** + * This utility function is used to check if at least one of the elements in a collection of boolean values is `true`. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { some } from "effect/Boolean" + * + * assert.deepStrictEqual(some([true, false, true]), true) + * assert.deepStrictEqual(some([false, false, false]), false) + * ``` + * + * @since 2.0.0 + */ +export const some = (collection: Iterable): boolean => { + for (const b of collection) { + if (b) { + return true + } + } + return false +} diff --git a/repos/effect/packages/effect/src/Brand.ts b/repos/effect/packages/effect/src/Brand.ts new file mode 100644 index 0000000..e6576de --- /dev/null +++ b/repos/effect/packages/effect/src/Brand.ts @@ -0,0 +1,360 @@ +/** + * This module provides types and utility functions to create and work with branded types, + * which are TypeScript types with an added type tag to prevent accidental usage of a value in the wrong context. + * + * The `refined` and `nominal` functions are both used to create branded types in TypeScript. + * The main difference between them is that `refined` allows for validation of the data, while `nominal` does not. + * + * The `nominal` function is used to create a new branded type that has the same underlying type as the input, but with a different name. + * This is useful when you want to distinguish between two values of the same type that have different meanings. + * The `nominal` function does not perform any validation of the input data. + * + * On the other hand, the `refined` function is used to create a new branded type that has the same underlying type as the input, + * but with a different name, and it also allows for validation of the input data. + * The `refined` function takes a predicate that is used to validate the input data. + * If the input data fails the validation, a `BrandErrors` is returned, which provides information about the specific validation failure. + * + * @since 2.0.0 + */ +import * as Arr from "./Array.js" +import * as Either from "./Either.js" +import { identity, unsafeCoerce } from "./Function.js" +import * as Option from "./Option.js" +import type { Predicate } from "./Predicate.js" +import type * as Types from "./Types.js" + +/** + * @since 2.0.0 + * @category symbols + */ +export const BrandTypeId: unique symbol = Symbol.for("effect/Brand") + +/** + * @since 2.0.0 + * @category symbols + */ +export type BrandTypeId = typeof BrandTypeId + +/** + * @since 2.0.0 + * @category symbols + */ +export const RefinedConstructorsTypeId: unique symbol = Symbol.for("effect/Brand/Refined") + +/** + * @since 2.0.0 + * @category symbols + */ +export type RefinedConstructorsTypeId = typeof RefinedConstructorsTypeId + +/** + * A generic interface that defines a branded type. + * + * @since 2.0.0 + * @category models + */ +export interface Brand { + readonly [BrandTypeId]: { + readonly [k in K]: K + } +} + +/** + * @since 2.0.0 + */ +export declare namespace Brand { + /** + * Represents a list of refinement errors. + * + * @since 2.0.0 + * @category models + */ + export interface BrandErrors extends Array {} + + /** + * Represents an error that occurs when the provided value of the branded type does not pass the refinement predicate. + * + * @since 2.0.0 + * @category models + */ + export interface RefinementError { + readonly meta: unknown + readonly message: string + } + + /** + * @since 2.0.0 + * @category models + */ + export interface Constructor> { + readonly [RefinedConstructorsTypeId]: RefinedConstructorsTypeId + /** + * Constructs a branded type from a value of type `A`, throwing an error if + * the provided `A` is not valid. + */ + (args: Brand.Unbranded): A + /** + * Constructs a branded type from a value of type `A`, returning `Some` + * if the provided `A` is valid, `None` otherwise. + */ + option(args: Brand.Unbranded): Option.Option + /** + * Constructs a branded type from a value of type `A`, returning `Right` + * if the provided `A` is valid, `Left` otherwise. + */ + either(args: Brand.Unbranded): Either.Either + /** + * Attempts to refine the provided value of type `A`, returning `true` if + * the provided `A` is valid, `false` otherwise. + */ + is(a: Brand.Unbranded): a is Brand.Unbranded & A + } + + /** + * A utility type to extract a branded type from a `Brand.Constructor`. + * + * @since 2.0.0 + * @category models + */ + export type FromConstructor = A extends Brand.Constructor ? B : never + + /** + * A utility type to extract the value type from a brand. + * + * @since 2.0.0 + * @category models + */ + export type Unbranded

= P extends infer Q & Brands

? Q : P + + /** + * A utility type to extract the brands from a branded type. + * + * @since 2.0.0 + * @category models + */ + export type Brands

= P extends Brand ? Types.UnionToIntersection< + { + [k in keyof P[BrandTypeId]]: k extends string | symbol ? Brand + : never + }[keyof P[BrandTypeId]] + > + : never + + /** + * A utility type that checks that all brands have the same base type. + * + * @since 2.0.0 + * @category models + */ + export type EnsureCommonBase< + Brands extends readonly [Brand.Constructor, ...Array>] + > = { + [B in keyof Brands]: Brand.Unbranded> extends + Brand.Unbranded> + ? Brand.Unbranded> extends Brand.Unbranded> + ? Brands[B] + : Brands[B] + : "ERROR: All brands should have the same base type" + } +} + +/** + * @category alias + * @since 2.0.0 + */ +export type Branded = A & Brand + +/** + * Returns a `BrandErrors` that contains a single `RefinementError`. + * + * @since 2.0.0 + * @category constructors + */ +export const error = (message: string, meta?: unknown): Brand.BrandErrors => [{ + message, + meta +}] + +/** + * Takes a variable number of `BrandErrors` and returns a single `BrandErrors` that contains all refinement errors. + * + * @since 2.0.0 + * @category constructors + */ +export const errors: (...errors: Array) => Brand.BrandErrors = ( + ...errors: Array +): Brand.BrandErrors => Arr.flatten(errors) + +/** + * Returns a `Brand.Constructor` that can construct a branded type from an unbranded value using the provided `refinement` + * predicate as validation of the input data. + * + * If you don't want to perform any validation but only distinguish between two values of the same type but with different meanings, + * see {@link nominal}. + * + * **Example** + * + * ```ts + * import * as assert from "node:assert" + * import { Brand } from "effect" + * + * type Int = number & Brand.Brand<"Int"> + * + * const Int = Brand.refined( + * (n) => Number.isInteger(n), + * (n) => Brand.error(`Expected ${n} to be an integer`) + * ) + * + * console.log(Int(1)) + * // 1 + * + * assert.throws(() => Int(1.1)) + * ``` + * + * @since 2.0.0 + * @category constructors + */ +export function refined>( + f: (unbranded: Brand.Unbranded) => Option.Option +): Brand.Constructor +export function refined>( + refinement: Predicate>, + onFailure: (unbranded: Brand.Unbranded) => Brand.BrandErrors +): Brand.Constructor +export function refined>( + ...args: [(unbranded: Brand.Unbranded) => Option.Option] | [ + Predicate>, + (unbranded: Brand.Unbranded) => Brand.BrandErrors + ] +): Brand.Constructor { + const either: (unbranded: Brand.Unbranded) => Either.Either = args.length === 2 ? + (unbranded) => args[0](unbranded) ? Either.right(unbranded as A) : Either.left(args[1](unbranded)) : + (unbranded) => { + return Option.match(args[0](unbranded), { + onNone: () => Either.right(unbranded as A), + onSome: Either.left + }) + } + return Object.assign((unbranded: Brand.Unbranded) => Either.getOrThrowWith(either(unbranded), identity), { + [RefinedConstructorsTypeId]: RefinedConstructorsTypeId, + option: (args: any) => Option.getRight(either(args)), + either, + is: (args: any): args is Brand.Unbranded & A => Either.isRight(either(args)) + }) as any +} + +/** + * This function returns a `Brand.Constructor` that **does not apply any runtime checks**, it just returns the provided value. + * It can be used to create nominal types that allow distinguishing between two values of the same type but with different meanings. + * + * If you also want to perform some validation, see {@link refined}. + * + * **Example** + * + * ```ts + * import * as assert from "node:assert" + * import { Brand } from "effect" + * + * type UserId = number & Brand.Brand<"UserId"> + * + * const UserId = Brand.nominal() + * + * console.log(UserId(1)) + * // 1 + * ``` + * + * @since 2.0.0 + * @category constructors + */ +export const nominal = >(): Brand.Constructor< + A +> => { + // @ts-expect-error + return Object.assign((args) => args, { + [RefinedConstructorsTypeId]: RefinedConstructorsTypeId, + option: (args: any) => Option.some(args), + either: (args: any) => Either.right(args), + is: (_args: any): _args is Brand.Unbranded & A => true + }) +} + +/** + * Combines two or more brands together to form a single branded type. + * This API is useful when you want to validate that the input data passes multiple brand validators. + * + * **Example** + * + * ```ts + * import * as assert from "node:assert" + * import { Brand } from "effect" + * + * type Int = number & Brand.Brand<"Int"> + * const Int = Brand.refined( + * (n) => Number.isInteger(n), + * (n) => Brand.error(`Expected ${n} to be an integer`) + * ) + * type Positive = number & Brand.Brand<"Positive"> + * const Positive = Brand.refined( + * (n) => n > 0, + * (n) => Brand.error(`Expected ${n} to be positive`) + * ) + * + * const PositiveInt = Brand.all(Int, Positive) + * + * console.log(PositiveInt(1)) + * // 1 + * + * assert.throws(() => PositiveInt(1.1)) + * ``` + * + * @since 2.0.0 + * @category combining + */ +export const all: , ...Array>]>( + ...brands: Brand.EnsureCommonBase +) => Brand.Constructor< + Types.UnionToIntersection<{ [B in keyof Brands]: Brand.FromConstructor }[number]> extends + infer X extends Brand ? X : Brand +> = < + Brands extends readonly [Brand.Constructor, ...Array>] +>(...brands: Brand.EnsureCommonBase): Brand.Constructor< + Types.UnionToIntersection< + { + [B in keyof Brands]: Brand.FromConstructor + }[number] + > extends infer X extends Brand ? X : Brand +> => { + const either = (args: any): Either.Either => { + let result: Either.Either = Either.right(args) + for (const brand of brands) { + const nextResult = brand.either(args) + if (Either.isLeft(result) && Either.isLeft(nextResult)) { + result = Either.left([...result.left, ...nextResult.left]) + } else { + result = Either.isLeft(result) ? result : nextResult + } + } + return result + } + // @ts-expect-error + return Object.assign((args) => + Either.match(either(args), { + onLeft: (e) => { + throw e + }, + onRight: identity + }), { + [RefinedConstructorsTypeId]: RefinedConstructorsTypeId, + option: (args: any) => Option.getRight(either(args)), + either, + is: (args: any): args is any => Either.isRight(either(args)) + }) +} + +/** + * Retrieves the unbranded value from a `Brand` instance. + * + * @since 3.15.0 + * @category getters + */ +export const unbranded: >(branded: A) => Brand.Unbranded = unsafeCoerce diff --git a/repos/effect/packages/effect/src/Cache.ts b/repos/effect/packages/effect/src/Cache.ts new file mode 100644 index 0000000..876a92d --- /dev/null +++ b/repos/effect/packages/effect/src/Cache.ts @@ -0,0 +1,281 @@ +/** + * @since 2.0.0 + */ +import type * as Duration from "./Duration.js" +import type * as Effect from "./Effect.js" +import type { Either } from "./Either.js" +import type * as Exit from "./Exit.js" +import * as internal from "./internal/cache.js" +import type * as Option from "./Option.js" +import type * as Predicate from "./Predicate.js" +import type * as Types from "./Types.js" + +/** + * @since 2.0.0 + * @category symbols + */ +export const CacheTypeId: unique symbol = internal.CacheTypeId + +/** + * @since 2.0.0 + * @category symbols + */ +export type CacheTypeId = typeof CacheTypeId + +/** + * @since 3.6.4 + * @category symbols + */ +export const ConsumerCacheTypeId: unique symbol = internal.ConsumerCacheTypeId + +/** + * @since 3.6.4 + * @category symbols + */ +export type ConsumerCacheTypeId = typeof ConsumerCacheTypeId + +/** + * A `Cache` is defined in terms of a lookup function that, given a key of + * type `Key`, can either fail with an error of type `Error` or succeed with a + * value of type `Value`. Getting a value from the cache will either return + * the previous result of the lookup function if it is available or else + * compute a new result with the lookup function, put it in the cache, and + * return it. + * + * A cache also has a specified capacity and time to live. When the cache is + * at capacity the least recently accessed values in the cache will be + * removed to make room for new values. Getting a value with a life older than + * the specified time to live will result in a new value being computed with + * the lookup function and returned when available. + * + * The cache is safe for concurrent access. If multiple fibers attempt to get + * the same key the lookup function will only be computed once and the result + * will be returned to all fibers. + * + * @since 2.0.0 + * @category models + */ +export interface Cache + extends ConsumerCache, Cache.Variance +{ + /** + * Retrieves the value associated with the specified key if it exists. + * Otherwise computes the value with the lookup function, puts it in the + * cache, and returns it. + */ + get(key: Key): Effect.Effect + + /** + * Retrieves the value associated with the specified key if it exists as a left. + * Otherwise computes the value with the lookup function, puts it in the + * cache, and returns it as a right. + */ + getEither(key: Key): Effect.Effect, Error> + + /** + * Computes the value associated with the specified key, with the lookup + * function, and puts it in the cache. The difference between this and + * `get` method is that `refresh` triggers (re)computation of the value + * without invalidating it in the cache, so any request to the associated + * key can still be served while the value is being re-computed/retrieved + * by the lookup function. Additionally, `refresh` always triggers the + * lookup function, disregarding the last `Error`. + */ + refresh(key: Key): Effect.Effect + + /** + * Associates the specified value with the specified key in the cache. + */ + set(key: Key, value: Value): Effect.Effect +} + +/** + * A ConsumerCache models a portion of a cache which is safe to share without allowing to create new values or access existing ones. + * + * It can be used safely to give over control for request management without leaking writer side details. + * + * @since 2.0.0 + * @category models + */ +export interface ConsumerCache + extends Cache.ConsumerVariance +{ + /** + * Retrieves the value associated with the specified key if it exists. + * Otherwise returns `Option.none`. + */ + getOption(key: Key): Effect.Effect, Error> + + /** + * Retrieves the value associated with the specified key if it exists and the + * lookup function has completed. Otherwise returns `Option.none`. + */ + getOptionComplete(key: Key): Effect.Effect> + + /** + * Returns statistics for this cache. + */ + readonly cacheStats: Effect.Effect + + /** + * Returns whether a value associated with the specified key exists in the + * cache. + */ + contains(key: Key): Effect.Effect + + /** + * Returns statistics for the specified entry. + */ + entryStats(key: Key): Effect.Effect> + + /** + * Invalidates the value associated with the specified key. + */ + invalidate(key: Key): Effect.Effect + + /** + * Invalidates the value associated with the specified key if the predicate holds. + */ + invalidateWhen(key: Key, predicate: Predicate.Predicate): Effect.Effect + + /** + * Invalidates all values in the cache. + */ + readonly invalidateAll: Effect.Effect + + /** + * Returns the approximate number of values in the cache. + */ + readonly size: Effect.Effect + + /** + * Returns an approximation of the values in the cache. + */ + readonly keys: Effect.Effect> + + /** + * Returns an approximation of the values in the cache. + */ + readonly values: Effect.Effect> + + /** + * Returns an approximation of the values in the cache. + */ + readonly entries: Effect.Effect> +} + +/** + * @since 2.0.0 + */ +export declare namespace Cache { + /** + * @since 2.0.0 + * @category models + */ + export interface Variance { + readonly [CacheTypeId]: { + readonly _Key: Types.Invariant + readonly _Error: Types.Covariant + readonly _Value: Types.Invariant + } + } + /** + * @since 3.6.4 + * @category models + */ + export interface ConsumerVariance { + readonly [ConsumerCacheTypeId]: { + readonly _Key: Types.Invariant + readonly _Error: Types.Covariant + readonly _Value: Types.Covariant + } + } +} + +/** + * Constructs a new cache with the specified capacity, time to live, and + * lookup function. + * + * @since 2.0.0 + * @category constructors + */ +export const make: ( + options: { + readonly capacity: number + readonly timeToLive: Duration.DurationInput + readonly lookup: Lookup + } +) => Effect.Effect, never, Environment> = internal.make + +/** + * Constructs a new cache with the specified capacity, time to live, and + * lookup function, where the time to live can depend on the `Exit` value + * returned by the lookup function. + * + * @since 2.0.0 + * @category constructors + */ +export const makeWith: ( + options: { + readonly capacity: number + readonly lookup: Lookup + readonly timeToLive: (exit: Exit.Exit) => Duration.DurationInput + } +) => Effect.Effect, never, Environment> = internal.makeWith + +/** + * `CacheStats` represents a snapshot of statistics for the cache as of a + * point in time. + * + * @since 2.0.0 + * @category models + */ +export interface CacheStats { + readonly hits: number + readonly misses: number + readonly size: number +} + +/** + * Constructs a new `CacheStats` from the specified values. + * + * @since 2.0.0 + * @category constructors + */ +export const makeCacheStats: ( + options: { + readonly hits: number + readonly misses: number + readonly size: number + } +) => CacheStats = internal.makeCacheStats + +/** + * Represents a snapshot of statistics for an entry in the cache. + * + * @since 2.0.0 + * @category models + */ +export interface EntryStats { + readonly loadedMillis: number +} + +/** + * Constructs a new `EntryStats` from the specified values. + * + * @since 2.0.0 + * @category constructors + */ +export const makeEntryStats: (loadedMillis: number) => EntryStats = internal.makeEntryStats + +/** + * A `Lookup` represents a lookup function that, given a key of type `Key`, can + * return an effect that will either produce a value of type `Value` or fail + * with an error of type `Error` using an environment of type `Environment`. + * + * @since 2.0.0 + * @category models + */ +export type Lookup = ( + key: Key +) => Effect.Effect diff --git a/repos/effect/packages/effect/src/Cause.ts b/repos/effect/packages/effect/src/Cause.ts new file mode 100644 index 0000000..f53c2b3 --- /dev/null +++ b/repos/effect/packages/effect/src/Cause.ts @@ -0,0 +1,1555 @@ +/** + * The `Effect` type is polymorphic in values of type `E` and we can + * work with any error type that we want. However, there is a lot of information + * that is not inside an arbitrary `E` value. So as a result, an `Effect` needs + * somewhere to store things like unexpected errors or defects, stack and + * execution traces, causes of fiber interruptions, and so forth. + * + * Effect-TS is very strict about preserving the full information related to a + * failure. It captures all type of errors into the `Cause` data type. `Effect` + * uses the `Cause` data type to store the full story of failure. So its + * error model is lossless. It doesn't throw information related to the failure + * result. So we can figure out exactly what happened during the operation of + * our effects. + * + * It is important to note that `Cause` is an underlying data type representing + * errors occuring within an `Effect` workflow. Thus, we don't usually deal with + * `Cause`s directly. Even though it is not a data type that we deal with very + * often, the `Cause` of a failing `Effect` workflow can be accessed at any + * time, which gives us total access to all parallel and sequential errors in + * occurring within our codebase. + * + * @since 2.0.0 + */ +import type * as Channel from "./Channel.js" +import type * as Chunk from "./Chunk.js" +import type * as Effect from "./Effect.js" +import type * as Either from "./Either.js" +import type * as Equal from "./Equal.js" +import type * as FiberId from "./FiberId.js" +import type * as HashSet from "./HashSet.js" +import type { Inspectable } from "./Inspectable.js" +import * as internal from "./internal/cause.js" +import * as core from "./internal/core.js" +import type * as Option from "./Option.js" +import type { Pipeable } from "./Pipeable.js" +import type { Predicate, Refinement } from "./Predicate.js" +import type * as Sink from "./Sink.js" +import type * as Stream from "./Stream.js" +import type { Span } from "./Tracer.js" +import type { Covariant, NoInfer } from "./Types.js" + +/** + * A unique symbol identifying the `Cause` type. + * + * **Details** + * + * This provides a symbol that helps identify instances of the `Cause` data + * type. This can be used for advanced operations such as refining types or + * building internal utilities that check whether an unknown value is a `Cause`. + * + * @see {@link isCause} Check if a value is a `Cause` + * + * @since 2.0.0 + * @category Symbols + */ +export const CauseTypeId: unique symbol = internal.CauseTypeId + +/** + * @since 2.0.0 + * @category Symbols + */ +export type CauseTypeId = typeof CauseTypeId + +/** + * A unique symbol identifying the `RuntimeException` type. + * + * **Details** + * + * This provides a symbol that identifies a `RuntimeException`. This is + * typically used internally by the library to recognize checked exceptions that + * occur during runtime. + * + * @see {@link RuntimeException} Create or work with a `RuntimeException` + * + * @since 2.0.0 + * @category Symbols + */ +export const RuntimeExceptionTypeId: unique symbol = core.RuntimeExceptionTypeId + +/** + * @since 2.0.0 + * @category Symbols + */ +export type RuntimeExceptionTypeId = typeof RuntimeExceptionTypeId + +/** + * A unique symbol identifying the `InterruptedException` type. + * + * **Details** + * + * This provides a symbol that identifies an `InterruptedException`. This is + * typically used internally to recognize when a fiber has been interrupted, + * helping the framework handle interruption logic correctly. + * + * @see {@link InterruptedException} Create or work with an `InterruptedException` + * + * @since 2.0.0 + * @category Symbols + */ +export const InterruptedExceptionTypeId: unique symbol = core.InterruptedExceptionTypeId + +/** + * @since 2.0.0 + * @category Symbols + */ +export type InterruptedExceptionTypeId = typeof InterruptedExceptionTypeId + +/** + * A unique symbol identifying the `IllegalArgumentException` type. + * + * **Details** + * + * This provides a symbol that identifies an `IllegalArgumentException`. This is + * often used in scenarios where invalid arguments are supplied to methods that + * expect specific input. + * + * @see {@link IllegalArgumentException} Create or work with an `IllegalArgumentException` + * + * @since 2.0.0 + * @category Symbols + */ +export const IllegalArgumentExceptionTypeId: unique symbol = core.IllegalArgumentExceptionTypeId + +/** + * @since 2.0.0 + * @category Symbols + */ +export type IllegalArgumentExceptionTypeId = typeof IllegalArgumentExceptionTypeId + +/** + * A unique symbol identifying the `NoSuchElementException` type. + * + * **Details** + * + * This provides a symbol that identifies a `NoSuchElementException`. It helps + * differentiate cases where a required element is missing within a data + * structure. + * + * @see {@link NoSuchElementException} Create or work with a `NoSuchElementException` + * + * @since 2.0.0 + * @category Symbols + */ +export const NoSuchElementExceptionTypeId: unique symbol = core.NoSuchElementExceptionTypeId + +/** + * @since 2.0.0 + * @category Symbols + */ +export type NoSuchElementExceptionTypeId = typeof NoSuchElementExceptionTypeId + +/** + * A unique symbol identifying the `InvalidPubSubCapacityException` type. + * + * **Details** + * + * This provides a symbol that identifies an `InvalidPubSubCapacityException`. + * It indicates an error related to an invalid capacity passed to a `PubSub` + * structure. + * + * @see {@link InvalidPubSubCapacityException} Create or work with an `InvalidPubSubCapacityException` + * + * @since 2.0.0 + * @category Symbols + */ +export const InvalidPubSubCapacityExceptionTypeId: unique symbol = core.InvalidPubSubCapacityExceptionTypeId + +/** + * @since 2.0.0 + * @category Symbols + */ +export type InvalidPubSubCapacityExceptionTypeId = typeof InvalidPubSubCapacityExceptionTypeId + +/** + * A unique symbol identifying the `ExceededCapacityException` type. + * + * **Details** + * + * This provides a symbol that identifies an `ExceededCapacityException`. It + * denotes situations where a resource has exceeded its configured capacity + * limit. + * + * @see {@link ExceededCapacityException} Create or work with an `ExceededCapacityException` + * + * @since 3.5.0 + * @category Symbols + */ +export const ExceededCapacityExceptionTypeId: unique symbol = core.ExceededCapacityExceptionTypeId + +/** + * @since 3.5.0 + * @category Symbols + */ +export type ExceededCapacityExceptionTypeId = typeof ExceededCapacityExceptionTypeId + +/** + * A unique symbol identifying the `TimeoutException` type. + * + * **Details** + * + * This provides a symbol that identifies a `TimeoutException`. It helps the + * framework recognize errors related to operations that fail to complete within + * a given timeframe. + * + * @see {@link TimeoutException} Create or work with a `TimeoutException` + * + * @since 2.0.0 + * @category Symbols + */ +export const TimeoutExceptionTypeId: unique symbol = core.TimeoutExceptionTypeId + +/** + * @since 2.0.0 + * @category Symbols + */ +export type TimeoutExceptionTypeId = typeof TimeoutExceptionTypeId + +/** + * A unique symbol identifying the `UnknownException` type. + * + * **Details** + * + * This provides a symbol that identifies an `UnknownException`. It is typically + * used for generic or unexpected errors that do not fit other specific + * exception categories. + * + * @see {@link UnknownException} Create or work with an `UnknownException` + * + * @since 2.0.0 + * @category Symbols + */ +export const UnknownExceptionTypeId: unique symbol = core.UnknownExceptionTypeId + +/** + * @since 2.0.0 + * @category Symbols + */ +export type UnknownExceptionTypeId = typeof UnknownExceptionTypeId + +/** + * Represents the full history of a failure within an `Effect`. + * + * **Details** + * + * This type is a data structure that captures all information about why and how + * an effect has failed, including parallel errors, sequential errors, defects, + * and interruptions. It enables a "lossless" error model: no error-related + * information is discarded, which helps in debugging and understanding the root + * cause of failures. + * + * @since 2.0.0 + * @category Models + */ +export type Cause = + | Empty + | Fail + | Die + | Interrupt + | Sequential + | Parallel + +/** + * @since 2.0.0 + */ +export declare namespace Cause { + /** + * This interface is used internally to manage the type variance of `Cause`. + * + * @since 2.0.0 + * @category Models + */ + export interface Variance { + readonly [CauseTypeId]: { + readonly _E: Covariant + } + } +} + +/** + * Describes methods for reducing a `Cause` into a value of type `Z` with + * access to contextual information. + * + * **Details** + * + * This interface is meant for advanced transformations of `Cause`. By + * implementing each method, you can define how different parts of the `Cause` + * structure (like `Fail`, `Die`, or `Interrupt`) should be transformed into a + * final type `Z`. The `context` parameter carries additional data needed during + * this reduction. + * + * @see {@link reduceWithContext} Apply a `CauseReducer` to transform a `Cause` + * + * @since 2.0.0 + * @category Models + */ +export interface CauseReducer { + emptyCase(context: C): Z + failCase(context: C, error: E): Z + dieCase(context: C, defect: unknown): Z + interruptCase(context: C, fiberId: FiberId.FiberId): Z + sequentialCase(context: C, left: Z, right: Z): Z + parallelCase(context: C, left: Z, right: Z): Z +} + +/** + * Represents an error object that can be yielded in `Effect.gen`. + * + * @since 2.0.0 + * @category Models + */ +export interface YieldableError extends Pipeable, Inspectable, Error { + readonly [Effect.EffectTypeId]: Effect.Effect.VarianceStruct + readonly [Stream.StreamTypeId]: Stream.Stream.VarianceStruct + readonly [Sink.SinkTypeId]: Sink.Sink.VarianceStruct + readonly [Channel.ChannelTypeId]: Channel.Channel.VarianceStruct + [Symbol.iterator](): Effect.EffectGenerator> +} + +/** + * Creates an error that occurs at runtime, extendable for other exception + * types. + * + * @since 2.0.0 + * @category Errors + */ +export const YieldableError: new(message?: string | undefined) => YieldableError = core.YieldableError + +/** + * An error representing a runtime error. + * + * **Details** + * + * This interface is used for errors that occur at runtime but are still + * considered recoverable or typed. + * + * @since 2.0.0 + * @category Models + */ +export interface RuntimeException extends YieldableError { + readonly _tag: "RuntimeException" + readonly [RuntimeExceptionTypeId]: RuntimeExceptionTypeId +} + +/** + * An error representing fiber interruption. + * + * **Details** + * + * This interface represents errors that occur when a fiber is forcefully + * interrupted. Interruption can happen for various reasons, including + * cancellations or system directives to halt operations. Code that deals with + * concurrency might need to catch or handle these to ensure proper cleanup. + * + * @since 2.0.0 + * @category Models + */ +export interface InterruptedException extends YieldableError { + readonly _tag: "InterruptedException" + readonly [InterruptedExceptionTypeId]: InterruptedExceptionTypeId +} + +/** + * An error representing an invalid argument passed to a method. + * + * **Details** + * + * This interface is used for signaling that a function or method received an + * argument that does not meet its preconditions. + * + * @since 2.0.0 + * @category Models + */ +export interface IllegalArgumentException extends YieldableError { + readonly _tag: "IllegalArgumentException" + readonly [IllegalArgumentExceptionTypeId]: IllegalArgumentExceptionTypeId +} + +/** + * An error that occurs when an expected element is missing. + * + * **Details** + * + * This interface indicates scenarios like looking up an item in a collection + * or searching for data that should be present but isn't. It helps your code + * signal a more specific issue rather than a general error. + * + * @since 2.0.0 + * @category Models + */ +export interface NoSuchElementException extends YieldableError { + readonly _tag: "NoSuchElementException" + readonly [NoSuchElementExceptionTypeId]: NoSuchElementExceptionTypeId +} + +/** + * An error indicating invalid capacity for a `PubSub`. + * + * @since 2.0.0 + * @category Models + */ +export interface InvalidPubSubCapacityException extends YieldableError { + readonly _tag: "InvalidPubSubCapacityException" + readonly [InvalidPubSubCapacityExceptionTypeId]: InvalidPubSubCapacityExceptionTypeId +} + +/** + * An error that occurs when resource capacity is exceeded. + * + * @since 3.5.0 + * @category Models + */ +export interface ExceededCapacityException extends YieldableError { + readonly _tag: "ExceededCapacityException" + readonly [ExceededCapacityExceptionTypeId]: ExceededCapacityExceptionTypeId +} + +/** + * An error representing a computation that timed out. + * + * @since 2.0.0 + * @category Models + */ +export interface TimeoutException extends YieldableError { + readonly _tag: "TimeoutException" + readonly [TimeoutExceptionTypeId]: TimeoutExceptionTypeId +} + +/** + * A checked exception for handling unknown or unexpected errors. + * + * **Details** + * + * This interface captures errors that don't fall under known categories. It is + * especially helpful for wrapping low-level or third-party library errors that + * might provide little or no context, such as from a rejected promise. + * + * @since 2.0.0 + * @category Models + */ +export interface UnknownException extends YieldableError { + readonly _tag: "UnknownException" + readonly [UnknownExceptionTypeId]: UnknownExceptionTypeId + readonly error: unknown +} + +/** + * Represents a lack of errors within a `Cause`. + * + * @see {@link empty} Construct a new `Empty` cause + * @see {@link isEmptyType} Check if a `Cause` is an `Empty` type + * + * @since 2.0.0 + * @category Models + */ +export interface Empty extends Cause.Variance, Equal.Equal, Pipeable, Inspectable { + readonly _tag: "Empty" +} + +/** + * Represents an expected error within a `Cause`. + * + * **Details** + * + * This interface models a `Cause` that carries an expected or known error of + * type `E`. For example, if you validate user input and find it invalid, you + * might store that error within a `Fail`. + * + * @see {@link fail} Construct a `Fail` cause + * @see {@link isFailType} Check if a `Cause` is a `Fail` + * + * @since 2.0.0 + * @category Models + */ +export interface Fail extends Cause.Variance, Equal.Equal, Pipeable, Inspectable { + readonly _tag: "Fail" + readonly error: E +} + +/** + * Represents an unexpected defect within a `Cause`. + * + * **Details** + * + * This interface models a `Cause` for errors that are typically unrecoverable or + * unanticipated—like runtime exceptions or bugs. When code "dies," it indicates a + * severe failure that wasn't accounted for. + * + * @see {@link die} Construct a `Die` cause + * @see {@link isDieType} Check if a `Cause` is a `Die` + * + * @since 2.0.0 + * @category Models + */ +export interface Die extends Cause.Variance, Equal.Equal, Pipeable, Inspectable { + readonly _tag: "Die" + readonly defect: unknown +} + +/** + * Represents fiber interruption within a `Cause`. + * + * **Details** + * + * This interface models a scenario where an effect was halted by an external + * signal, carrying a `FiberId` that identifies which fiber was interrupted. + * Interruption is a normal part of concurrency, used for cancellation or + * resource cleanup. + * + * @see {@link interrupt} Construct an `Interrupt` cause + * @see {@link isInterruptType} Check if a `Cause` is an `Interrupt` + * + * @since 2.0.0 + * @category Models + */ +export interface Interrupt extends Cause.Variance, Equal.Equal, Pipeable, Inspectable { + readonly _tag: "Interrupt" + readonly fiberId: FiberId.FiberId +} + +/** + * Represents parallel composition of two `Cause`s. + * + * **Details** + * + * This interface captures failures that happen simultaneously. In scenarios + * with concurrency, more than one operation can fail in parallel. Instead of + * losing information, this structure stores both errors together. + * + * @see {@link parallel} Combine two `Cause`s in parallel + * @see {@link isParallelType} Check if a `Cause` is a `Parallel` + * + * @since 2.0.0 + * @category Models + */ +export interface Parallel extends Cause.Variance, Equal.Equal, Pipeable, Inspectable { + readonly _tag: "Parallel" + readonly left: Cause + readonly right: Cause +} + +/** + * Represents sequential composition of two `Cause`s. + * + * **Details** + * + * This interface models the scenario where one error follows another in + * sequence, such as when a main effect fails and then a finalizer also fails. + * It ensures both errors are retained in the final `Cause`. + * + * @see {@link sequential} Combine two `Cause`s sequentially + * @see {@link isSequentialType} Check if a `Cause` is a `Sequential` + * + * @since 2.0.0 + * @category Models + */ +export interface Sequential extends Cause.Variance, Equal.Equal, Pipeable, Inspectable { + readonly _tag: "Sequential" + readonly left: Cause + readonly right: Cause +} + +/** + * Creates an `Empty` cause. + * + * **Details** + * + * This function returns a cause that signifies "no error." It's commonly used + * to represent an absence of failure conditions. + * + * @see {@link isEmpty} Check if a `Cause` is empty + * + * @since 2.0.0 + * @category Constructors + */ +export const empty: Cause = internal.empty + +/** + * Creates a `Fail` cause from an expected error. + * + * **Details** + * + * This function constructs a `Cause` carrying an error of type `E`. It's used + * when you want to represent a known or anticipated failure in your effectful + * computations. + * + * @see {@link isFailure} Check if a `Cause` contains a failure + * + * @since 2.0.0 + * @category Constructors + */ +export const fail: (error: E) => Cause = internal.fail + +/** + * Creates a `Die` cause from an unexpected error. + * + * **Details** + * + * This function wraps an unhandled or unknown defect (like a runtime crash) + * into a `Cause`. It's useful for capturing unforeseen issues in a structured + * way. + * + * @see {@link isDie} Check if a `Cause` contains a defect + * + * @since 2.0.0 + * @category Constructors + */ +export const die: (defect: unknown) => Cause = internal.die + +/** + * Creates an `Interrupt` cause from a `FiberId`. + * + * **Details** + * + * This function represents a fiber that has been interrupted. It stores the + * identifier of the interrupted fiber, enabling precise tracking of concurrent + * cancellations. + * + * @see {@link isInterrupted} Check if a `Cause` contains an interruption + * + * @since 2.0.0 + * @category Constructors + */ +export const interrupt: (fiberId: FiberId.FiberId) => Cause = internal.interrupt + +/** + * Combines two `Cause`s in parallel. + * + * **Details** + * + * This function merges two errors that occurred simultaneously. Instead of + * discarding one error, both are retained, allowing for richer error reporting + * and debugging. + * + * @see {@link isParallelType} Check if a `Cause` is a `Parallel` + * + * @since 2.0.0 + * @category Constructors + */ +export const parallel: (left: Cause, right: Cause) => Cause = internal.parallel + +/** + * Combines two `Cause`s sequentially. + * + * **Details** + * + * This function merges two errors that occurred in sequence, such as a main + * error followed by a finalization error. It preserves both errors for complete + * failure information. + * + * @see {@link isSequentialType} Check if a `Cause` is a `Sequential` + * + * @since 2.0.0 + * @category Constructors + */ +export const sequential: (left: Cause, right: Cause) => Cause = internal.sequential + +/** + * Checks if a value is a `Cause`. + * + * @since 2.0.0 + * @category Guards + */ +export const isCause: (u: unknown) => u is Cause = internal.isCause + +/** + * Checks if a `Cause` is an `Empty` type. + * + * @see {@link empty} Create a new `Empty` cause + * + * @since 2.0.0 + * @category Guards + */ +export const isEmptyType: (self: Cause) => self is Empty = internal.isEmptyType + +/** + * Checks if a `Cause` is a `Fail` type. + * + * @see {@link fail} Create a new `Fail` cause + * + * @since 2.0.0 + * @category Guards + */ +export const isFailType: (self: Cause) => self is Fail = internal.isFailType + +/** + * Checks if a `Cause` is a `Die` type. + * + * @see {@link die} Create a new `Die` cause + * + * @since 2.0.0 + * @category Guards + */ +export const isDieType: (self: Cause) => self is Die = internal.isDieType + +/** + * Checks if a `Cause` is an `Interrupt` type. + * + * @see {@link interrupt} Create an `Interrupt` cause + * + * @since 2.0.0 + * @category Guards + */ +export const isInterruptType: (self: Cause) => self is Interrupt = internal.isInterruptType + +/** + * Checks if a `Cause` is a `Sequential` type. + * + * @see {@link sequential} Combine two `Cause`s sequentially + * + * @since 2.0.0 + * @category Guards + */ +export const isSequentialType: (self: Cause) => self is Sequential = internal.isSequentialType + +/** + * Checks if a `Cause` is a `Parallel` type. + * + * @see {@link parallel} Combine two `Cause`s in parallel + * + * @since 2.0.0 + * @category Guards + */ +export const isParallelType: (self: Cause) => self is Parallel = internal.isParallelType + +/** + * Calculates the size of a `Cause`. + * + * **Details** + * + * This function returns the total number of `Cause` nodes in the semiring + * structure, reflecting how many individual error elements are recorded. + * + * @since 2.0.0 + * @category Getters + */ +export const size: (self: Cause) => number = internal.size + +/** + * Checks if a `Cause` is entirely empty. + * + * **Details** + * + * This function returns `true` if the `Cause` contains no errors, defects, or + * interruptions. It's helpful for verifying if a computation truly had no + * failures. + * + * @since 2.0.0 + * @category Getters + */ +export const isEmpty: (self: Cause) => boolean = internal.isEmpty + +/** + * Checks if a `Cause` contains a failure. + * + * **Details** + * + * This function returns `true` if the `Cause` includes any `Fail` error. It's + * commonly used to confirm whether a workflow encountered an anticipated error + * versus just defects or interruptions. + * + * @since 2.0.0 + * @category Getters + */ +export const isFailure: (self: Cause) => boolean = internal.isFailure + +/** + * Checks if a `Cause` contains a defect. + * + * **Details** + * + * This function returns `true` if the `Cause` includes any unexpected or + * unhandled errors (`Die`). It's useful for differentiating known failures from + * unexpected ones. + * + * @since 2.0.0 + * @category Getters + */ +export const isDie: (self: Cause) => boolean = internal.isDie + +/** + * Checks if a `Cause` contains an interruption. + * + * **Details** + * + * This function returns `true` if the `Cause` includes any fiber interruptions. + * + * @since 2.0.0 + * @category Getters + */ +export const isInterrupted: (self: Cause) => boolean = internal.isInterrupted + +/** + * Checks if a `Cause` contains only interruptions. + * + * **Details** + * + * This function returns `true` if the `Cause` has been interrupted but does not + * contain any other failures, such as `Fail` or `Die`. It's helpful for + * verifying purely "cancellation" scenarios. + * + * @since 2.0.0 + * @category Getters + */ +export const isInterruptedOnly: (self: Cause) => boolean = internal.isInterruptedOnly + +/** + * Extracts all recoverable errors of type `E` from a `Cause`. + * + * **Details** + * + * This function returns a chunk of errors, providing a list of all `Fail` + * values found in the cause. It's useful for collecting all known failures for + * logging or combined error handling. + * + * @since 2.0.0 + * @category Getters + */ +export const failures: (self: Cause) => Chunk.Chunk = internal.failures + +/** + * Extracts all unrecoverable defects from a `Cause`. + * + * **Details** + * + * This function returns a chunk of values representing unexpected errors + * (`Die`). It's handy for capturing or logging unanticipated failures that + * might need special handling, such as bug reports. + * + * @since 2.0.0 + * @category Getters + */ +export const defects: (self: Cause) => Chunk.Chunk = internal.defects + +/** + * Collects all `FiberId`s responsible for interrupting a fiber. + * + * **Details** + * + * This function returns a set of IDs indicating which fibers caused + * interruptions within this `Cause`. It's useful for debugging concurrency + * issues or tracing cancellations. + * + * @since 2.0.0 + * @category Getters + */ +export const interruptors: (self: Cause) => HashSet.HashSet = internal.interruptors + +/** + * Retrieves the first `Fail` error in a `Cause`, if present. + * + * **Details** + * + * This function returns an `Option` containing the first recoverable error + * (`E`) from the cause. It's often used to quickly check if there's a primary + * error to handle or display. + * + * @since 2.0.0 + * @category Getters + */ +export const failureOption: (self: Cause) => Option.Option = internal.failureOption + +/** + * Splits a `Cause` into either its first `Fail` error or the rest of the cause + * (which might only contain `Die` or `Interrupt`). + * + * **Details** + * + * This function either returns the checked error (`E`) or the remaining + * `Cause` with defects/interruptions. It helps you decide if there's a + * recoverable path or if only unhandled issues remain. + * + * @since 2.0.0 + * @category Getters + */ +export const failureOrCause: (self: Cause) => Either.Either, E> = internal.failureOrCause + +/** + * Strips out failures with an error of `None` from a `Cause>`. + * + * **Details** + * + * This function turns a `Cause>` into an `Option>`. If the + * cause only contains failures of `None`, it becomes `None`; otherwise, it + * returns a `Cause` of the remaining errors. It's helpful when working with + * optional errors and filtering out certain error paths. + * + * @since 2.0.0 + * @category Getters + */ +export const flipCauseOption: (self: Cause>) => Option.Option> = internal.flipCauseOption + +/** + * Retrieves the first `Die` defect in a `Cause`, if present. + * + * **Details** + * + * This function returns an `Option` containing the first unexpected failure + * (`Die`) discovered. It's helpful for diagnosing the primary defect in a chain + * of errors. + * + * @since 2.0.0 + * @category Getters + */ +export const dieOption: (self: Cause) => Option.Option = internal.dieOption + +/** + * Retrieves the first `Interrupt` in a `Cause`, if present. + * + * **Details** + * + * This function returns an `Option` with the first fiber interruption + * discovered. This is particularly useful for concurrency analysis or debugging + * cancellations. + * + * @since 2.0.0 + * @category Getters + */ +export const interruptOption: (self: Cause) => Option.Option = internal.interruptOption + +/** + * Removes all `Fail` and `Interrupt` nodes, keeping only defects (`Die`) in a + * `Cause`. + * + * **Details** + * + * This function strips a cause of recoverable errors and interruptions, leaving + * only unexpected failures. If no defects remain, it returns `None`. It's + * valuable for focusing only on unanticipated problems when both known errors + * and defects could occur. + * + * @since 2.0.0 + * @category Getters + */ +export const keepDefects: (self: Cause) => Option.Option> = internal.keepDefects + +// TODO(4.0): remove? what's the point of this API? +/** + * Linearizes a `Cause` into a set of parallel causes, each containing a + * sequential chain of failures. + * + * **Details** + * + * This function reorganizes the cause structure so that you can analyze each + * parallel branch separately, even if they have multiple sequential errors. + * + * @since 2.0.0 + * @category Getters + */ +export const linearize: (self: Cause) => HashSet.HashSet> = internal.linearize + +/** + * Removes `Fail` and `Interrupt` nodes from a `Cause`, keeping only defects + * (`Die`). + * + * **Details** + * + * This function is similar to `keepDefects` but returns a `Cause` + * directly, which can still store `Die` or finalizer-related defects. It's + * helpful for analyzing only the irrecoverable portion of the error. + * + * @since 2.0.0 + * @category Getters + */ +export const stripFailures: (self: Cause) => Cause = internal.stripFailures + +/** + * Removes matching defects from a `Cause` using a partial function, returning + * the remainder. + * + * **Details** + * + * This function applies a user-defined extraction function to each defect + * (`Die`). If the function matches the defect, that defect is removed. If all + * defects match, the result is `None`. Otherwise, you get a `Cause` with the + * unmatched defects. + * + * @since 2.0.0 + * @category Getters + */ +export const stripSomeDefects: { + (pf: (defect: unknown) => Option.Option): (self: Cause) => Option.Option> + (self: Cause, pf: (defect: unknown) => Option.Option): Option.Option> +} = internal.stripSomeDefects + +/** + * Replaces any errors in a `Cause` with a provided constant error. + * + * **Details** + * + * This function transforms all `Fail` errors into the specified error value, + * preserving the structure of the `Cause`. It's useful when you no longer need + * the original error details but still want to keep the cause shape. + * + * @see {@link map} Apply a custom transformation to `Fail` errors + * + * @since 2.0.0 + * @category Mapping + */ +export const as: { + (error: E2): (self: Cause) => Cause + (self: Cause, error: E2): Cause +} = internal.as + +/** + * Transforms the errors in a `Cause` using a user-provided function. + * + * **Details** + * + * This function applies `f` to each `Fail` error while leaving defects (`Die`) + * and interruptions untouched. It's useful for changing or simplifying error + * types in your effectful workflows. + * + * @see {@link as} Replace errors with a single constant + * + * @since 2.0.0 + * @category Mapping + */ +export const map: { + (f: (e: E) => E2): (self: Cause) => Cause + (self: Cause, f: (e: E) => E2): Cause +} = internal.map + +/** + * Transforms errors in a `Cause` into new causes. + * + * **Details** + * + * This function applies a function `f` to each `Fail` error, converting it into + * a new `Cause`. This is especially powerful for merging or restructuring error + * types while preserving or combining cause information. + * + * @see {@link map} Apply a simpler transformation to errors + * + * @since 2.0.0 + * @category Sequencing + */ +export const flatMap: { + (f: (e: E) => Cause): (self: Cause) => Cause + (self: Cause, f: (e: E) => Cause): Cause +} = internal.flatMap + +/** + * Sequences two `Cause`s. The second `Cause` can be dependent on the result of + * the first `Cause`. + * + * @since 2.0.0 + * @category Sequencing + */ +export const andThen: { + (f: (e: E) => Cause): (self: Cause) => Cause + (f: Cause): (self: Cause) => Cause + (self: Cause, f: (e: E) => Cause): Cause + (self: Cause, f: Cause): Cause +} = internal.andThen + +/** + * Flattens a nested `Cause` structure. + * + * **Details** + * + * This function takes a `Cause>` and merges the layers into a single + * `Cause`. It's useful for eliminating additional nesting created by + * repeated transformations or compositions. + * + * @see {@link flatMap} Compose nested causes + * + * @since 2.0.0 + * @category Sequencing + */ +export const flatten: (self: Cause>) => Cause = internal.flatten + +/** + * Checks if the current `Cause` contains or is equal to another `Cause`. + * + * **Details** + * + * This function returns `true` if `that` cause is part of or the same as + * the current `Cause`. It's useful when you need to check for specific + * error patterns or deduplicate repeated failures. + * + * @since 2.0.0 + * @category Elements + */ +export const contains: { + (that: Cause): (self: Cause) => boolean + (self: Cause, that: Cause): boolean +} = internal.contains + +/** + * Extracts the most "important" defect from a `Cause`. + * + * **Details** + * + * This function reduces a `Cause` to a single, prioritized defect. It evaluates + * the `Cause` in the following order of priority: + * + * 1. If the `Cause` contains a failure (e.g., from `Effect.fail`), it returns + * the raw error value. + * 2. If there is no failure, it looks for the first defect (e.g., from + * `Effect.die`). + * 3. If neither of the above is present, and the `Cause` stems from an + * interruption, it creates and returns an `InterruptedException`. + * + * This function ensures you can always extract a meaningful representation of + * the primary issue from a potentially complex `Cause` structure. + * + * **When to Use** + * + * Use this function when you need to extract the most relevant error or defect + * from a `Cause`, especially in scenarios where multiple errors or defects may + * be present. It's particularly useful for simplifying error reporting or + * logging. + * + * @see {@link squashWith} Allows transforming failures into defects when squashing. + * + * @since 2.0.0 + * @category Destructors + */ +export const squash: (self: Cause) => unknown = core.causeSquash + +/** + * Extracts the most "important" defect from a `Cause`, transforming failures + * into defects using a provided function. + * + * **Details** + * + * This function reduces a `Cause` to a single, prioritized defect, while + * allowing you to transform recoverable failures into defects through a custom + * function. It processes the `Cause` in the following order: + * + * 1. If the `Cause` contains a failure (e.g., from `Effect.fail`), it applies + * the provided function `f` to the error to transform it into a defect. + * 2. If there is no failure, it looks for the first defect (e.g., from + * `Effect.die`) and returns it. + * 3. If neither is present and the `Cause` stems from an interruption, it + * returns an `InterruptedException`. + * + * This function is particularly useful when you need custom handling or + * transformation of errors while processing a `Cause`. + * + * @see {@link squash} Extracts the most "important" defect without transforming failures. + * + * @since 2.0.0 + * @category Destructors + */ +export const squashWith: { + (f: (error: E) => unknown): (self: Cause) => unknown + (self: Cause, f: (error: E) => unknown): unknown +} = core.causeSquashWith + +/** + * Searches a `Cause` using a partial function to extract information. + * + * **Details** + * + * This function allows you to search through a `Cause` using a custom partial + * function. The partial function is applied to the `Cause`, and if it matches, + * the result is returned wrapped in a `Some`. If no match is found, the result + * is `None`. + * + * This is particularly useful when you are only interested in specific types of + * errors, defects, or interruption causes within a potentially complex `Cause` + * structure. By leveraging a partial function, you can focus on extracting only + * the relevant information you care about. + * + * The partial function should return an `Option` indicating whether it matched + * and the value it extracted. + * + * @since 2.0.0 + * @category Elements + */ +export const find: { + (pf: (cause: Cause) => Option.Option): (self: Cause) => Option.Option + (self: Cause, pf: (cause: Cause) => Option.Option): Option.Option +} = internal.find + +/** + * Preserves parts of a `Cause` that match a given predicate. + * + * **Details** + * + * This function allows you to retain only the parts of a `Cause` structure that + * match a specified predicate or refinement. Any parts of the `Cause` that do + * not match the provided condition are excluded from the result. + * + * You can use this function in two ways: + * - With a `Predicate`: A function that evaluates whether a `Cause` should be + * retained based on its value. + * - With a `Refinement`: A more specific predicate that can refine the type of + * the `Cause`. + * + * This is useful when you need to extract specific types of errors, defects, or + * interruptions from a `Cause` while discarding unrelated parts. + * + * @since 2.0.0 + * @category Filtering + */ +export const filter: { + (refinement: Refinement>, Cause>): (self: Cause) => Cause + (predicate: Predicate>>): (self: Cause) => Cause + (self: Cause, refinement: Refinement, Cause>): Cause + (self: Cause, predicate: Predicate>): Cause +} = internal.filter + +/** + * Transforms a `Cause` into a single value using custom handlers for each + * possible case. + * + * **Details** + * + * This function processes a `Cause` by applying a set of custom handlers to + * each possible type of cause: `Empty`, `Fail`, `Die`, `Interrupt`, + * `Sequential`, and `Parallel`. The result of this function is a single value + * of type `Z`. This function allows you to define exactly how to handle each + * part of a `Cause`, whether it's a failure, defect, interruption, or a + * combination of these. + * + * The options parameter provides handlers for: + * - `onEmpty`: Handles the case where the cause is `Empty`, meaning no errors + * occurred. + * - `onFail`: Processes a failure with an error of type `E`. + * - `onDie`: Processes a defect (unexpected error). + * - `onInterrupt`: Handles a fiber interruption, providing the `FiberId` of the + * interruption. + * - `onSequential`: Combines two sequential causes into a single value of type + * `Z`. + * - `onParallel`: Combines two parallel causes into a single value of type `Z`. + * + * @since 2.0.0 + * @category Matching + */ +export const match: { + ( + options: { + readonly onEmpty: Z + readonly onFail: (error: E) => Z + readonly onDie: (defect: unknown) => Z + readonly onInterrupt: (fiberId: FiberId.FiberId) => Z + readonly onSequential: (left: Z, right: Z) => Z + readonly onParallel: (left: Z, right: Z) => Z + } + ): (self: Cause) => Z + ( + self: Cause, + options: { + readonly onEmpty: Z + readonly onFail: (error: E) => Z + readonly onDie: (defect: unknown) => Z + readonly onInterrupt: (fiberId: FiberId.FiberId) => Z + readonly onSequential: (left: Z, right: Z) => Z + readonly onParallel: (left: Z, right: Z) => Z + } + ): Z +} = internal.match + +/** + * Combines all parts of a `Cause` into a single value by starting with an + * initial value. + * + * **Details** + * + * This function processes a `Cause` by starting with an initial value (`zero`) + * and applying a custom function (`pf`) to combine all elements of the `Cause` + * into a single result of type `Z`. The custom function determines how each + * part of the `Cause` contributes to the final result. The function can return + * an `Option` to either continue combining values or skip specific parts of the + * `Cause`. + * + * This function is useful for tasks such as: + * - Aggregating error messages from a `Cause` into a single string. + * - Summarizing the structure of a `Cause` into a simplified result. + * - Filtering or processing only specific parts of a `Cause`. + * + * The reduction proceeds in a top-down manner, visiting all nodes in the + * `Cause` structure. This gives you complete control over how each part of the + * `Cause` contributes to the final result. + * + * @since 2.0.0 + * @category Reducing + */ +export const reduce: { + (zero: Z, pf: (accumulator: Z, cause: Cause) => Option.Option): (self: Cause) => Z + (self: Cause, zero: Z, pf: (accumulator: Z, cause: Cause) => Option.Option): Z +} = internal.reduce + +/** + * Combines all parts of a `Cause` into a single value using a custom reducer + * and a context. + * + * **Details** + * + * This function allows you to reduce a `Cause` into a single value of type `Z` + * using a custom `CauseReducer`. A `CauseReducer` provides methods to handle + * specific parts of the `Cause`, such as failures, defects, or interruptions. + * Additionally, this function provides access to a `context` value, which can + * be used to carry information or maintain state during the reduction process. + * + * This is particularly useful when the reduction process needs additional + * context or configuration, such as: + * - Aggregating error details with dynamic formatting. + * - Collecting logs or statistics about the `Cause`. + * - Performing stateful transformations based on the `context`. + * + * @see {@link reduce} To reduce a `Cause` without additional context. + * + * @since 2.0.0 + * @category Reducing + */ +export const reduceWithContext: { + (context: C, reducer: CauseReducer): (self: Cause) => Z + (self: Cause, context: C, reducer: CauseReducer): Z +} = internal.reduceWithContext + +/** + * Creates an error that indicates a `Fiber` was interrupted. + * + * **Details** + * + * This function constructs an `InterruptedException` recognized by the Effect + * runtime. It is usually thrown or returned when a fiber's execution is + * interrupted by external events or by another fiber. This is particularly + * helpful in concurrent programs where fibers may halt each other before + * completion. + * + * @since 2.0.0 + * @category Errors + */ +export const InterruptedException: new(message?: string | undefined) => InterruptedException = core.InterruptedException + +/** + * Checks if a given unknown value is an `InterruptedException`. + * + * @since 2.0.0 + * @category Guards + */ +export const isInterruptedException: (u: unknown) => u is InterruptedException = core.isInterruptedException + +/** + * Creates an error indicating an invalid method argument. + * + * **Details** + * + * This function constructs an `IllegalArgumentException`. It is typically + * thrown or returned when an operation receives improper inputs, such as + * out-of-range values or invalid object states. + * + * @since 2.0.0 + * @category Errors + */ +export const IllegalArgumentException: new(message?: string | undefined) => IllegalArgumentException = + core.IllegalArgumentException + +/** + * Checks if a given unknown value is an `IllegalArgumentException`. + * + * @since 2.0.0 + * @category Guards + */ +export const isIllegalArgumentException: (u: unknown) => u is IllegalArgumentException = core.isIllegalArgumentException + +/** + * Creates an error indicating a missing element. + * + * **Details** + * + * This function constructs a `NoSuchElementException`. It helps you clearly + * communicate that a required element is unavailable. + * + * @since 2.0.0 + * @category Errors + */ +export const NoSuchElementException: new(message?: string | undefined) => NoSuchElementException = + core.NoSuchElementException + +/** + * Checks if a given unknown value is a `NoSuchElementException`. + * + * @since 2.0.0 + * @category Guards + */ +export const isNoSuchElementException: (u: unknown) => u is NoSuchElementException = core.isNoSuchElementException + +/** + * Creates an error for general runtime errors. + * + * **Details** + * + * This function constructs a `RuntimeException`, for errors that occur at + * runtime but are not specifically typed or categorized as interruptions, + * missing elements, or invalid arguments. It helps unify a wide range of + * unexpected conditions under a single, recognizable error type. + * + * @since 2.0.0 + * @category Errors + */ +export const RuntimeException: new(message?: string | undefined) => RuntimeException = core.RuntimeException + +/** + * Checks if a given unknown value is a `RuntimeException`. + * + * @since 2.0.0 + * @category Guards + */ +export const isRuntimeException: (u: unknown) => u is RuntimeException = core.isRuntimeException + +/** + * Creates an error for operations that exceed their expected time. + * + * **Details** + * + * This function constructs a `TimeoutException`. It is typically used to signal + * that an operation or fiber did not complete within a designated time limit, + * allowing you to handle slow or hanging processes. + * + * @since 2.0.0 + * @category Errors + */ +export const TimeoutException: new(message?: string | undefined) => TimeoutException = core.TimeoutException + +/** + * Checks if a given unknown value is a `TimeoutException`. + * + * @since 3.15.0 + * @category Guards + */ +export const isTimeoutException: (u: unknown) => u is TimeoutException = core.isTimeoutException + +/** + * Creates an instance of `UnknownException`, an error object used to handle + * unknown errors such as those from rejected promises. + * + * **Details** + * + * This function constructs an `UnknownException` with flexible behavior for + * managing the error message and cause. + * + * The required `error` argument is passed as the `cause` to the global `Error` + * constructor, ensuring that the original cause is preserved in the error chain + * for debugging purposes. This ensures that the origin stack trace is + * preserved. + * + * The `error` argument is always stored in the `error` property of the + * `UnknownException` instance for reference, regardless of its type. + * + * Additionally, if you provide a `message` argument, it is used as the error + * message. If no `message` is provided, the error message defaults to `"An + * unknown error occurred"`. + * + * **When to Use** + * + * Use this function when you need to handle unexpected or unknown errors in + * your application, particularly when the source of the error might not provide + * a clear message. This is useful for wrapping generic errors thrown from + * promises or external APIs. + * + * @since 2.0.0 + * @category Errors + */ +export const UnknownException: new(error: unknown, message?: string | undefined) => UnknownException = + core.UnknownException + +/** + * Checks if a given unknown value is an `UnknownException`. + * + * @since 2.0.0 + * @category Guards + */ +export const isUnknownException: (u: unknown) => u is UnknownException = core.isUnknownException + +/** + * Creates an error indicating resource capacity has been exceeded. + * + * **Details** + * + * This function constructs an `ExceededCapacityException`, signifying that an + * operation or resource usage surpassed established limits. This can be + * essential for concurrency or resource management situations, ensuring your + * application doesn't go beyond acceptable thresholds. + * + * @since 3.5.0 + * @category Errors + */ +export const ExceededCapacityException: new(message?: string | undefined) => ExceededCapacityException = + core.ExceededCapacityException + +/** + * Checks if a given unknown value is an `ExceededCapacityException`. + * + * @since 3.5.0 + * @category Guards + */ +export const isExceededCapacityException: (u: unknown) => u is ExceededCapacityException = + core.isExceededCapacityException + +/** + * Converts a `Cause` into a human-readable string. + * + * **Details** + * + * This function pretty-prints the entire `Cause`, including any failures, + * defects, and interruptions. It can be especially helpful for logging, + * debugging, or displaying structured errors to users. + * + * You can optionally pass `options` to configure how the error cause is + * rendered. By default, it includes essential details of all errors in the + * `Cause`. + * + * @see {@link prettyErrors} Get a list of `PrettyError` objects instead of a single string. + * + * @since 2.0.0 + * @category Formatting + */ +export const pretty: (cause: Cause, options?: { + readonly renderErrorCause?: boolean | undefined +}) => string = internal.pretty + +/** + * A shape for prettified errors, optionally including a source span. + * + * @since 3.2.0 + * @category Models + */ +export interface PrettyError extends Error { + readonly span: Span | undefined +} + +/** + * Returns a list of prettified errors (`PrettyError`) from a `Cause`. + * + * **Details** + * + * This function inspects the entire `Cause` and produces an array of + * `PrettyError` objects. Each object may include additional metadata, such as a + * `Span`, to provide deeper insights into where and how the error occurred. + * + * @since 3.2.0 + * @category Formatting + */ +export const prettyErrors: (cause: Cause) => Array = internal.prettyErrors + +/** + * Retrieves the original, unproxied error instance from an error object. + * + * **Details** + * + * This function returns the underlying error object without any + * library-specific wrapping or proxying that might occur during error handling. + * This can be essential if you need direct access to the error's native + * properties, such as stack traces or custom data fields, for detailed + * debugging or integration with external systems. + * + * @since 2.0.0 + * @category Errors + */ +export const originalError: (obj: E) => E = core.originalInstance diff --git a/repos/effect/packages/effect/src/Channel.ts b/repos/effect/packages/effect/src/Channel.ts new file mode 100644 index 0000000..f3449c2 --- /dev/null +++ b/repos/effect/packages/effect/src/Channel.ts @@ -0,0 +1,2355 @@ +/** + * @since 2.0.0 + */ +import type * as Cause from "./Cause.js" +import type * as ChildExecutorDecision from "./ChildExecutorDecision.js" +import type * as Chunk from "./Chunk.js" +import type * as Context from "./Context.js" +import type * as Deferred from "./Deferred.js" +import type * as Effect from "./Effect.js" +import type * as Either from "./Either.js" +import type * as Exit from "./Exit.js" +import type { LazyArg } from "./Function.js" +import * as channel from "./internal/channel.js" +import * as core from "./internal/core-stream.js" +import * as sink from "./internal/sink.js" +import * as stream from "./internal/stream.js" +import type * as Layer from "./Layer.js" +import type * as MergeDecision from "./MergeDecision.js" +import type * as MergeStrategy from "./MergeStrategy.js" +import type * as Option from "./Option.js" +import type { Pipeable } from "./Pipeable.js" +import type { Predicate } from "./Predicate.js" +import type * as PubSub from "./PubSub.js" +import type * as Queue from "./Queue.js" +import type * as Ref from "./Ref.js" +import type * as Scope from "./Scope.js" +import type * as SingleProducerAsyncInput from "./SingleProducerAsyncInput.js" +import type * as Sink from "./Sink.js" +import type * as Stream from "./Stream.js" +import type * as Tracer from "./Tracer.js" +import type * as Types from "./Types.js" +import type * as Unify from "./Unify.js" +import type * as UpstreamPullRequest from "./UpstreamPullRequest.js" +import type * as UpstreamPullStrategy from "./UpstreamPullStrategy.js" + +/** + * @since 2.0.0 + * @category symbols + */ +export const ChannelTypeId: unique symbol = core.ChannelTypeId + +/** + * @since 2.0.0 + * @category symbols + */ +export type ChannelTypeId = typeof ChannelTypeId + +/** + * A `Channel` is a nexus of I/O operations, which supports both reading and + * writing. A channel may read values of type `InElem` and write values of type + * `OutElem`. When the channel finishes, it yields a value of type `OutDone`. A + * channel may fail with a value of type `OutErr`. + * + * Channels are the foundation of Streams: both streams and sinks are built on + * channels. Most users shouldn't have to use channels directly, as streams and + * sinks are much more convenient and cover all common use cases. However, when + * adding new stream and sink operators, or doing something highly specialized, + * it may be useful to use channels directly. + * + * Channels compose in a variety of ways: + * + * - **Piping**: One channel can be piped to another channel, assuming the + * input type of the second is the same as the output type of the first. + * - **Sequencing**: The terminal value of one channel can be used to create + * another channel, and both the first channel and the function that makes + * the second channel can be composed into a channel. + * - **Concatenating**: The output of one channel can be used to create other + * channels, which are all concatenated together. The first channel and the + * function that makes the other channels can be composed into a channel. + * + * @since 2.0.0 + * @category models + */ +// export interface Channel +export interface Channel< + out OutElem, + in InElem = unknown, + out OutErr = never, + in InErr = unknown, + out OutDone = void, + in InDone = unknown, + out Env = never +> extends + Channel.Variance< + OutElem, + InElem, + OutErr, + InErr, + OutDone, + InDone, + Env + >, + Pipeable +{ + [Unify.typeSymbol]?: unknown + [Unify.unifySymbol]?: ChannelUnify + [Unify.ignoreSymbol]?: ChannelUnifyIgnore +} + +/** + * @since 2.0.0 + * @category models + */ +export interface ChannelUnify extends Effect.EffectUnify { + Channel?: () => A[Unify.typeSymbol] extends + | Channel< + infer OutElem, + infer InElem, + infer OutErr, + infer InErr, + infer OutDone, + infer InDone, + infer Env + > + | infer _ ? Channel + : never +} + +/** + * @category models + * @since 2.0.0 + */ +export interface ChannelUnifyIgnore extends Effect.EffectUnifyIgnore { + Channel?: true +} + +/** + * @since 2.0.0 + * @category models + */ +declare module "./Effect.js" { + interface Effect extends Channel {} + interface EffectUnifyIgnore { + Channel?: true + } +} + +/** + * @since 2.0.0 + */ +export declare namespace Channel { + /** + * @since 2.0.0 + * @category models + */ + export interface Variance { + readonly [ChannelTypeId]: VarianceStruct + } + /** + * @since 2.0.0 + * @category models + */ + export interface VarianceStruct { + _Env: Types.Covariant + _InErr: Types.Contravariant + _InElem: Types.Contravariant + _InDone: Types.Contravariant + _OutErr: Types.Covariant + _OutElem: Types.Covariant + _OutDone: Types.Covariant + } +} + +/** + * @since 2.0.0 + * @category symbols + */ +export const ChannelExceptionTypeId: unique symbol = channel.ChannelExceptionTypeId + +/** + * @since 2.0.0 + * @category symbols + */ +export type ChannelExceptionTypeId = typeof ChannelExceptionTypeId + +/** + * Represents a generic checked exception which occurs when a `Channel` is + * executed. + * + * @since 2.0.0 + * @category models + */ +export interface ChannelException { + readonly _tag: "ChannelException" + readonly [ChannelExceptionTypeId]: ChannelExceptionTypeId + readonly error: E +} + +/** + * @since 3.5.4 + * @category refinements + */ +export const isChannel: (u: unknown) => u is Channel< + unknown, + unknown, + unknown, + unknown, + unknown, + unknown, + unknown +> = core.isChannel + +/** + * @since 2.0.0 + * @category constructors + */ +export const acquireUseRelease: ( + acquire: Effect.Effect, + use: (a: Acquired) => Channel, + release: (a: Acquired, exit: Exit.Exit) => Effect.Effect +) => Channel = channel.acquireUseRelease + +/** + * @since 2.0.0 + * @category constructors + */ +export const acquireReleaseOut: { + ( + release: (z: Z, e: Exit.Exit) => Effect.Effect + ): (self: Effect.Effect) => Channel + ( + self: Effect.Effect, + release: (z: Z, e: Exit.Exit) => Effect.Effect + ): Channel +} = core.acquireReleaseOut + +/** + * Returns a new channel that is the same as this one, except the terminal + * value of the channel is the specified constant value. + * + * This method produces the same result as mapping this channel to the + * specified constant value. + * + * @since 2.0.0 + * @category mapping + */ +export const as: { + ( + value: OutDone2 + ): ( + self: Channel + ) => Channel + ( + self: Channel, + value: OutDone2 + ): Channel +} = channel.as + +/** + * @since 2.0.0 + * @category mapping + */ +export const asVoid: ( + self: Channel +) => Channel = channel.asVoid + +/** + * Creates a channel backed by a buffer. When the buffer is empty, the channel + * will simply passthrough its input as output. However, when the buffer is + * non-empty, the value inside the buffer will be passed along as output. + * + * @since 2.0.0 + * @category constructors + */ +export const buffer: ( + options: { readonly empty: InElem; readonly isEmpty: Predicate; readonly ref: Ref.Ref } +) => Channel = channel.buffer + +/** + * @since 2.0.0 + * @category constructors + */ +export const bufferChunk: ( + ref: Ref.Ref> +) => Channel, Chunk.Chunk, InErr, InErr, InDone, InDone> = channel.bufferChunk + +/** + * Returns a new channel that is the same as this one, except if this channel + * errors for any typed error, then the returned channel will switch over to + * using the fallback channel returned by the specified error handler. + * + * @since 2.0.0 + * @category error handling + */ +export const catchAll: { + ( + f: (error: OutErr) => Channel + ): ( + self: Channel + ) => Channel< + OutElem1 | OutElem, + InElem & InElem1, + OutErr1, + InErr & InErr1, + OutDone1 | OutDone, + InDone & InDone1, + Env1 | Env + > + ( + self: Channel, + f: (error: OutErr) => Channel + ): Channel< + OutElem | OutElem1, + InElem & InElem1, + OutErr1, + InErr & InErr1, + OutDone | OutDone1, + InDone & InDone1, + Env | Env1 + > +} = channel.catchAll + +/** + * Returns a new channel that is the same as this one, except if this channel + * errors for any typed error, then the returned channel will switch over to + * using the fallback channel returned by the specified error handler. + * + * @since 2.0.0 + * @category error handling + */ +export const catchAllCause: { + ( + f: (cause: Cause.Cause) => Channel + ): ( + self: Channel + ) => Channel< + OutElem1 | OutElem, + InElem & InElem1, + OutErr1, + InErr & InErr1, + OutDone1 | OutDone, + InDone & InDone1, + Env1 | Env + > + ( + self: Channel, + f: (cause: Cause.Cause) => Channel + ): Channel< + OutElem | OutElem1, + InElem & InElem1, + OutErr1, + InErr & InErr1, + OutDone | OutDone1, + InDone & InDone1, + Env | Env1 + > +} = core.catchAllCause + +/** + * Concat sequentially a channel of channels. + * + * @since 2.0.0 + * @category constructors + */ +export const concatAll: ( + channels: Channel, InElem, OutErr, InErr, any, InDone, Env> +) => Channel = core.concatAll + +/** + * Concat sequentially a channel of channels. + * + * @since 2.0.0 + * @category constructors + */ +export const concatAllWith: < + OutElem, + InElem2, + OutErr2, + InErr2, + OutDone, + InDone2, + Env2, + InElem, + OutErr, + InErr, + OutDone2, + InDone, + Env, + OutDone3 +>( + channels: Channel< + Channel, + InElem, + OutErr, + InErr, + OutDone2, + InDone, + Env + >, + f: (o: OutDone, o1: OutDone) => OutDone, + g: (o: OutDone, o2: OutDone2) => OutDone3 +) => Channel = + core.concatAllWith + +/** + * Returns a new channel whose outputs are fed to the specified factory + * function, which creates new channels in response. These new channels are + * sequentially concatenated together, and all their outputs appear as outputs + * of the newly returned channel. + * + * @since 2.0.0 + * @category utils + */ +export const concatMap: { + ( + f: (o: OutElem) => Channel + ): ( + self: Channel + ) => Channel + ( + self: Channel, + f: (o: OutElem) => Channel + ): Channel +} = channel.concatMap + +/** + * Returns a new channel whose outputs are fed to the specified factory + * function, which creates new channels in response. These new channels are + * sequentially concatenated together, and all their outputs appear as outputs + * of the newly returned channel. The provided merging function is used to + * merge the terminal values of all channels into the single terminal value of + * the returned channel. + * + * @since 2.0.0 + * @category utils + */ +export const concatMapWith: { + ( + f: (o: OutElem) => Channel, + g: (o: OutDone, o1: OutDone) => OutDone, + h: (o: OutDone, o2: OutDone2) => OutDone3 + ): ( + self: Channel + ) => Channel + < + OutElem, + InElem, + OutErr, + InErr, + OutDone2, + InDone, + Env, + OutElem2, + InElem2, + OutErr2, + InErr2, + OutDone, + InDone2, + Env2, + OutDone3 + >( + self: Channel, + f: (o: OutElem) => Channel, + g: (o: OutDone, o1: OutDone) => OutDone, + h: (o: OutDone, o2: OutDone2) => OutDone3 + ): Channel +} = core.concatMapWith + +/** + * Returns a new channel whose outputs are fed to the specified factory + * function, which creates new channels in response. These new channels are + * sequentially concatenated together, and all their outputs appear as outputs + * of the newly returned channel. The provided merging function is used to + * merge the terminal values of all channels into the single terminal value of + * the returned channel. + * + * @since 2.0.0 + * @category utils + */ +export const concatMapWithCustom: { + ( + f: (o: OutElem) => Channel, + g: (o: OutDone, o1: OutDone) => OutDone, + h: (o: OutDone, o2: OutDone2) => OutDone3, + onPull: ( + upstreamPullRequest: UpstreamPullRequest.UpstreamPullRequest + ) => UpstreamPullStrategy.UpstreamPullStrategy, + onEmit: (elem: OutElem2) => ChildExecutorDecision.ChildExecutorDecision + ): ( + self: Channel + ) => Channel + < + OutElem, + InElem, + OutErr, + InErr, + OutDone2, + InDone, + Env, + OutElem2, + InElem2, + OutErr2, + InErr2, + OutDone, + InDone2, + Env2, + OutDone3 + >( + self: Channel, + f: (o: OutElem) => Channel, + g: (o: OutDone, o1: OutDone) => OutDone, + h: (o: OutDone, o2: OutDone2) => OutDone3, + onPull: ( + upstreamPullRequest: UpstreamPullRequest.UpstreamPullRequest + ) => UpstreamPullStrategy.UpstreamPullStrategy, + onEmit: (elem: OutElem2) => ChildExecutorDecision.ChildExecutorDecision + ): Channel +} = core.concatMapWithCustom + +/** + * Returns a new channel, which is the same as this one, except its outputs + * are filtered and transformed by the specified partial function. + * + * @since 2.0.0 + * @category utils + */ +export const collect: { + ( + pf: (o: OutElem) => Option.Option + ): ( + self: Channel + ) => Channel + ( + self: Channel, + pf: (o: OutElem) => Option.Option + ): Channel +} = channel.collect + +/** + * Returns a new channel, which is the concatenation of all the channels that + * are written out by this channel. This method may only be called on channels + * that output other channels. + * + * @since 2.0.0 + * @category utils + */ +export const concatOut: ( + self: Channel< + Channel, + InElem, + OutErr, + InErr, + OutDone, + InDone, + Env + > +) => Channel = channel.concatOut + +/** + * Returns a new channel which is the same as this one but applies the given + * function to the input channel's done value. + * + * @since 2.0.0 + * @category utils + */ +export const mapInput: { + ( + f: (a: InDone0) => InDone + ): ( + self: Channel + ) => Channel + ( + self: Channel, + f: (a: InDone0) => InDone + ): Channel +} = channel.mapInput + +/** + * Returns a new channel which is the same as this one but applies the given + * effectual function to the input channel's done value. + * + * @since 2.0.0 + * @category utils + */ +export const mapInputEffect: { + ( + f: (i: InDone0) => Effect.Effect + ): ( + self: Channel + ) => Channel + ( + self: Channel, + f: (i: InDone0) => Effect.Effect + ): Channel +} = channel.mapInputEffect + +/** + * Returns a new channel which is the same as this one but applies the given + * function to the input channel's error value. + * + * @since 2.0.0 + * @category utils + */ +export const mapInputError: { + ( + f: (a: InErr0) => InErr + ): ( + self: Channel + ) => Channel + ( + self: Channel, + f: (a: InErr0) => InErr + ): Channel +} = channel.mapInputError + +/** + * Returns a new channel which is the same as this one but applies the given + * effectual function to the input channel's error value. + * + * @since 2.0.0 + * @category utils + */ +export const mapInputErrorEffect: { + ( + f: (error: InErr0) => Effect.Effect + ): ( + self: Channel + ) => Channel + ( + self: Channel, + f: (error: InErr0) => Effect.Effect + ): Channel +} = channel.mapInputErrorEffect + +/** + * Returns a new channel which is the same as this one but applies the given + * function to the input channel's output elements. + * + * @since 2.0.0 + * @category utils + */ +export const mapInputIn: { + ( + f: (a: InElem0) => InElem + ): ( + self: Channel + ) => Channel + ( + self: Channel, + f: (a: InElem0) => InElem + ): Channel +} = channel.mapInputIn + +/** + * Returns a new channel which is the same as this one but applies the given + * effectual function to the input channel's output elements. + * + * @since 2.0.0 + * @category utils + */ +export const mapInputInEffect: { + ( + f: (a: InElem0) => Effect.Effect + ): ( + self: Channel + ) => Channel + ( + self: Channel, + f: (a: InElem0) => Effect.Effect + ): Channel +} = channel.mapInputInEffect + +/** + * Returns a new channel, which is the same as this one, except that all the + * outputs are collected and bundled into a tuple together with the terminal + * value of this channel. + * + * As the channel returned from this channel collects all of this channel's + * output into an in- memory chunk, it is not safe to call this method on + * channels that output a large or unbounded number of values. + * + * @since 2.0.0 + * @category utils + */ +export const doneCollect: ( + self: Channel +) => Channel, OutDone], InDone, Env> = channel.doneCollect + +/** + * Returns a new channel which reads all the elements from upstream's output + * channel and ignores them, then terminates with the upstream result value. + * + * @since 2.0.0 + * @category utils + */ +export const drain: ( + self: Channel +) => Channel = channel.drain + +/** + * Returns a new channel which connects the given `AsyncInputProducer` as + * this channel's input. + * + * @since 2.0.0 + * @category utils + */ +export const embedInput: { + ( + input: SingleProducerAsyncInput.AsyncInputProducer + ): ( + self: Channel + ) => Channel + ( + self: Channel, + input: SingleProducerAsyncInput.AsyncInputProducer + ): Channel +} = core.embedInput + +/** + * Returns a new channel that collects the output and terminal value of this + * channel, which it then writes as output of the returned channel. + * + * @since 2.0.0 + * @category utils + */ +export const emitCollect: ( + self: Channel +) => Channel<[Chunk.Chunk, OutDone], InElem, OutErr, InErr, void, InDone, Env> = channel.emitCollect + +/** + * Returns a new channel with an attached finalizer. The finalizer is + * guaranteed to be executed so long as the channel begins execution (and + * regardless of whether or not it completes). + * + * @since 2.0.0 + * @category utils + */ +export const ensuring: { + ( + finalizer: Effect.Effect + ): ( + self: Channel + ) => Channel + ( + self: Channel, + finalizer: Effect.Effect + ): Channel +} = channel.ensuring + +/** + * Returns a new channel with an attached finalizer. The finalizer is + * guaranteed to be executed so long as the channel begins execution (and + * regardless of whether or not it completes). + * + * @since 2.0.0 + * @category utils + */ +export const ensuringWith: { + ( + finalizer: (e: Exit.Exit) => Effect.Effect + ): ( + self: Channel + ) => Channel + ( + self: Channel, + finalizer: (e: Exit.Exit) => Effect.Effect + ): Channel +} = core.ensuringWith + +/** + * Accesses the whole context of the channel. + * + * @since 2.0.0 + * @category context + */ +export const context: () => Channel, unknown, Env> = + channel.context + +/** + * Accesses the context of the channel with the specified function. + * + * @since 2.0.0 + * @category context + */ +export const contextWith: ( + f: (env: Context.Context) => OutDone +) => Channel = channel.contextWith + +/** + * Accesses the context of the channel in the context of a channel. + * + * @since 2.0.0 + * @category context + */ +export const contextWithChannel: ( + f: (env: Context.Context) => Channel +) => Channel = channel.contextWithChannel + +/** + * Accesses the context of the channel in the context of an effect. + * + * @since 2.0.0 + * @category context + */ +export const contextWithEffect: ( + f: (env: Context.Context) => Effect.Effect +) => Channel = channel.contextWithEffect + +/** + * Constructs a channel that fails immediately with the specified error. + * + * @since 2.0.0 + * @category constructors + */ +export const fail: (error: E) => Channel = core.fail + +/** + * Constructs a channel that succeeds immediately with the specified lazily + * evaluated value. + * + * @since 2.0.0 + * @category constructors + */ +export const failSync: (evaluate: LazyArg) => Channel = core.failSync + +/** + * Constructs a channel that fails immediately with the specified `Cause`. + * + * @since 2.0.0 + * @category constructors + */ +export const failCause: (cause: Cause.Cause) => Channel = + core.failCause + +/** + * Constructs a channel that succeeds immediately with the specified lazily + * evaluated `Cause`. + * + * @since 2.0.0 + * @category constructors + */ +export const failCauseSync: ( + evaluate: LazyArg> +) => Channel = core.failCauseSync + +/** + * Returns a new channel, which sequentially combines this channel, together + * with the provided factory function, which creates a second channel based on + * the terminal value of this channel. The result is a channel that will first + * perform the functions of this channel, before performing the functions of + * the created channel (including yielding its terminal value). + * + * @since 2.0.0 + * @category sequencing + */ +export const flatMap: { + ( + f: (d: OutDone) => Channel + ): ( + self: Channel + ) => Channel< + OutElem1 | OutElem, + InElem & InElem1, + OutErr1 | OutErr, + InErr & InErr1, + OutDone2, + InDone & InDone1, + Env1 | Env + > + ( + self: Channel, + f: (d: OutDone) => Channel + ): Channel< + OutElem | OutElem1, + InElem & InElem1, + OutErr | OutErr1, + InErr & InErr1, + OutDone2, + InDone & InDone1, + Env | Env1 + > +} = core.flatMap + +/** + * Returns a new channel, which flattens the terminal value of this channel. + * This function may only be called if the terminal value of this channel is + * another channel of compatible types. + * + * @since 2.0.0 + * @category sequencing + */ +export const flatten: < + OutElem, + InElem, + OutErr, + InErr, + OutElem1, + InElem1, + OutErr1, + InErr1, + OutDone2, + InDone1, + Env1, + InDone, + Env +>( + self: Channel< + OutElem, + InElem, + OutErr, + InErr, + Channel, + InDone, + Env + > +) => Channel< + OutElem | OutElem1, + InElem & InElem1, + OutErr | OutErr1, + InErr & InErr1, + OutDone2, + InDone & InDone1, + Env1 | Env +> = channel.flatten + +/** + * Folds over the result of this channel. + * + * @since 2.0.0 + * @category utils + */ +export const foldChannel: { + < + OutErr, + OutElem1, + InElem1, + OutErr1, + InErr1, + OutDone1, + InDone1, + Env1, + OutDone, + OutElem2, + InElem2, + OutErr2, + InErr2, + OutDone2, + InDone2, + Env2 + >( + options: { + readonly onFailure: (error: OutErr) => Channel + readonly onSuccess: (done: OutDone) => Channel + } + ): ( + self: Channel + ) => Channel< + OutElem1 | OutElem2 | OutElem, + InElem & InElem1 & InElem2, + OutErr1 | OutErr2, + InErr & InErr1 & InErr2, + OutDone1 | OutDone2, + InDone & InDone1 & InDone2, + Env1 | Env2 | Env + > + < + OutElem, + InElem, + OutErr, + InErr, + OutDone, + InDone, + Env, + OutElem1, + InElem1, + OutErr1, + InErr1, + OutDone1, + InDone1, + Env1, + OutElem2, + InElem2, + OutErr2, + InErr2, + OutDone2, + InDone2, + Env2 + >( + self: Channel, + options: { + readonly onFailure: (error: OutErr) => Channel + readonly onSuccess: (done: OutDone) => Channel + } + ): Channel< + OutElem | OutElem1 | OutElem2, + InElem & InElem1 & InElem2, + OutErr1 | OutErr2, + InErr & InErr1 & InErr2, + OutDone1 | OutDone2, + InDone & InDone1 & InDone2, + Env | Env1 | Env2 + > +} = channel.foldChannel + +/** + * Folds over the result of this channel including any cause of termination. + * + * @since 2.0.0 + * @category utils + */ +export const foldCauseChannel: { + < + OutErr, + OutElem1, + InElem1, + OutErr2, + InErr1, + OutDone2, + InDone1, + Env1, + OutDone, + OutElem2, + InElem2, + OutErr3, + InErr2, + OutDone3, + InDone2, + Env2 + >( + options: { + readonly onFailure: ( + c: Cause.Cause + ) => Channel + readonly onSuccess: (o: OutDone) => Channel + } + ): ( + self: Channel + ) => Channel< + OutElem1 | OutElem2 | OutElem, + InElem & InElem1 & InElem2, + OutErr2 | OutErr3, + InErr & InErr1 & InErr2, + OutDone2 | OutDone3, + InDone & InDone1 & InDone2, + Env1 | Env2 | Env + > + < + OutElem, + InElem, + OutErr, + InErr, + OutDone, + InDone, + Env, + OutElem1, + InElem1, + OutErr2, + InErr1, + OutDone2, + InDone1, + Env1, + OutElem2, + InElem2, + OutErr3, + InErr2, + OutDone3, + InDone2, + Env2 + >( + self: Channel, + options: { + readonly onFailure: ( + c: Cause.Cause + ) => Channel + readonly onSuccess: (o: OutDone) => Channel + } + ): Channel< + OutElem | OutElem1 | OutElem2, + InElem & InElem1 & InElem2, + OutErr2 | OutErr3, + InErr & InErr1 & InErr2, + OutDone2 | OutDone3, + InDone & InDone1 & InDone2, + Env | Env1 | Env2 + > +} = core.foldCauseChannel + +/** + * Use an effect to end a channel. + * + * @since 2.0.0 + * @category constructors + */ +export const fromEffect: ( + effect: Effect.Effect +) => Channel = core.fromEffect + +/** + * Constructs a channel from an `Either`. + * + * @since 2.0.0 + * @category constructors + */ +export const fromEither: (either: Either.Either) => Channel = + channel.fromEither + +/** + * Construct a `Channel` from an `AsyncInputConsumer`. + * + * @since 2.0.0 + * @category constructors + */ +export const fromInput: ( + input: SingleProducerAsyncInput.AsyncInputConsumer +) => Channel = channel.fromInput + +/** + * Construct a `Channel` from a `PubSub`. + * + * @since 2.0.0 + * @category constructors + */ +export const fromPubSub: ( + pubsub: PubSub.PubSub>> +) => Channel = channel.fromPubSub + +/** + * Construct a `Channel` from a `PubSub` within a scoped effect. + * + * @since 2.0.0 + * @category constructors + */ +export const fromPubSubScoped: ( + pubsub: PubSub.PubSub>> +) => Effect.Effect, never, Scope.Scope> = channel.fromPubSubScoped + +/** + * Construct a `Channel` from an `Option`. + * + * @since 2.0.0 + * @category constructors + */ +export const fromOption: ( + option: Option.Option +) => Channel, unknown, A, unknown> = channel.fromOption + +/** + * Construct a `Channel` from a `Queue`. + * + * @since 2.0.0 + * @category constructors + */ +export const fromQueue: ( + queue: Queue.Dequeue>> +) => Channel = channel.fromQueue + +/** + * @since 2.0.0 + * @category constructors + */ +export const identity: () => Channel = channel.identityChannel + +/** + * Returns a new channel, which is the same as this one, except it will be + * interrupted when the specified effect completes. If the effect completes + * successfully before the underlying channel is done, then the returned + * channel will yield the success value of the effect as its terminal value. + * On the other hand, if the underlying channel finishes first, then the + * returned channel will yield the success value of the underlying channel as + * its terminal value. + * + * @since 2.0.0 + * @category utils + */ +export const interruptWhen: { + ( + effect: Effect.Effect + ): ( + self: Channel + ) => Channel + ( + self: Channel, + effect: Effect.Effect + ): Channel +} = channel.interruptWhen + +/** + * Returns a new channel, which is the same as this one, except it will be + * interrupted when the specified deferred is completed. If the deferred is + * completed before the underlying channel is done, then the returned channel + * will yield the value of the deferred. Otherwise, if the underlying channel + * finishes first, then the returned channel will yield the value of the + * underlying channel. + * + * @since 2.0.0 + * @category utils + */ +export const interruptWhenDeferred: { + ( + deferred: Deferred.Deferred + ): ( + self: Channel + ) => Channel + ( + self: Channel, + deferred: Deferred.Deferred + ): Channel +} = channel.interruptWhenDeferred + +/** + * Returns a new channel, which is the same as this one, except the terminal + * value of the returned channel is created by applying the specified function + * to the terminal value of this channel. + * + * @since 2.0.0 + * @category mapping + */ +export const map: { + ( + f: (out: OutDone) => OutDone2 + ): ( + self: Channel + ) => Channel + ( + self: Channel, + f: (out: OutDone) => OutDone2 + ): Channel +} = channel.map + +/** + * Returns a new channel, which is the same as this one, except the terminal + * value of the returned channel is created by applying the specified + * effectful function to the terminal value of this channel. + * + * @since 2.0.0 + * @category mapping + */ +export const mapEffect: { + ( + f: (o: OutDone) => Effect.Effect + ): ( + self: Channel + ) => Channel + ( + self: Channel, + f: (o: OutDone) => Effect.Effect + ): Channel +} = channel.mapEffect + +/** + * Returns a new channel, which is the same as this one, except the failure + * value of the returned channel is created by applying the specified function + * to the failure value of this channel. + * + * @since 2.0.0 + * @category mapping + */ +export const mapError: { + ( + f: (err: OutErr) => OutErr2 + ): ( + self: Channel + ) => Channel + ( + self: Channel, + f: (err: OutErr) => OutErr2 + ): Channel +} = channel.mapError + +/** + * A more powerful version of `mapError` which also surfaces the `Cause` + * of the channel failure. + * + * @since 2.0.0 + * @category mapping + */ +export const mapErrorCause: { + ( + f: (cause: Cause.Cause) => Cause.Cause + ): ( + self: Channel + ) => Channel + ( + self: Channel, + f: (cause: Cause.Cause) => Cause.Cause + ): Channel +} = channel.mapErrorCause + +/** + * Maps the output of this channel using the specified function. + * + * @since 2.0.0 + * @category mapping + */ +export const mapOut: { + ( + f: (o: OutElem) => OutElem2 + ): ( + self: Channel + ) => Channel + ( + self: Channel, + f: (o: OutElem) => OutElem2 + ): Channel +} = channel.mapOut + +/** + * Creates a channel that is like this channel but the given effectful function + * gets applied to each emitted output element. + * + * @since 2.0.0 + * @category mapping + */ +export const mapOutEffect: { + ( + f: (o: OutElem) => Effect.Effect + ): ( + self: Channel + ) => Channel + ( + self: Channel, + f: (o: OutElem) => Effect.Effect + ): Channel +} = channel.mapOutEffect + +/** + * Creates a channel that is like this channel but the given Effect function gets + * applied to each emitted output element, taking `n` elements at once and + * mapping them in parallel. + * + * @since 2.0.0 + * @category mapping + */ +export const mapOutEffectPar: { + ( + f: (o: OutElem) => Effect.Effect, + n: number + ): ( + self: Channel + ) => Channel + ( + self: Channel, + f: (o: OutElem) => Effect.Effect, + n: number + ): Channel +} = channel.mapOutEffectPar + +/** + * @since 2.0.0 + * @category utils + */ +export const mergeAll: ( + options: { + readonly concurrency: number | "unbounded" + readonly bufferSize?: number | undefined + readonly mergeStrategy?: MergeStrategy.MergeStrategy | undefined + } +) => ( + channels: Channel< + Channel, + InElem, + OutErr, + InErr, + unknown, + InDone, + Env + > +) => Channel = + channel.mergeAll + +/** + * @since 2.0.0 + * @category utils + */ +export const mergeAllUnbounded: ( + channels: Channel< + Channel, + InElem, + OutErr, + InErr, + unknown, + InDone, + Env + > +) => Channel = + channel.mergeAllUnbounded + +/** + * @since 2.0.0 + * @category utils + */ +export const mergeAllUnboundedWith: < + OutElem, + InElem1, + OutErr1, + InErr1, + OutDone, + InDone1, + Env1, + InElem, + OutErr, + InErr, + InDone, + Env +>( + channels: Channel< + Channel, + InElem, + OutErr, + InErr, + OutDone, + InDone, + Env + >, + f: (o1: OutDone, o2: OutDone) => OutDone +) => Channel = + channel.mergeAllUnboundedWith + +/** + * @since 2.0.0 + * @category utils + */ +export const mergeAllWith: ( + { bufferSize, concurrency, mergeStrategy }: { + readonly concurrency: number | "unbounded" + readonly bufferSize?: number | undefined + readonly mergeStrategy?: MergeStrategy.MergeStrategy | undefined + } +) => ( + channels: Channel< + Channel, + InElem, + OutErr, + InErr, + OutDone, + InDone, + Env + >, + f: (o1: OutDone, o2: OutDone) => OutDone +) => Channel = + channel.mergeAllWith + +/** + * Returns a new channel which creates a new channel for each emitted element + * and merges some of them together. Different merge strategies control what + * happens if there are more than the given maximum number of channels gets + * created. See `Channel.mergeAll`. + * + * @since 2.0.0 + * @category mapping + */ +export const mergeMap: { + ( + f: (outElem: OutElem) => Channel, + options: { + readonly concurrency: number | "unbounded" + readonly bufferSize?: number | undefined + readonly mergeStrategy?: MergeStrategy.MergeStrategy | undefined + } + ): ( + self: Channel + ) => Channel + ( + self: Channel, + f: (outElem: OutElem) => Channel, + options: { + readonly concurrency: number | "unbounded" + readonly bufferSize?: number | undefined + readonly mergeStrategy?: MergeStrategy.MergeStrategy | undefined + } + ): Channel +} = channel.mergeMap + +/** + * Returns a new channel which merges a number of channels emitted by this + * channel using the back pressuring merge strategy. See `Channel.mergeAll`. + * + * @since 2.0.0 + * @category utils + */ +export const mergeOut: { + ( + n: number + ): ( + self: Channel< + Channel, + InElem, + OutErr, + InErr, + OutDone, + InDone, + Env + > + ) => Channel + ( + self: Channel< + Channel, + InElem, + OutErr, + InErr, + OutDone, + InDone, + Env + >, + n: number + ): Channel +} = channel.mergeOut + +/** + * Returns a new channel which merges a number of channels emitted by this + * channel using the back pressuring merge strategy and uses a given function + * to merge each completed subchannel's result value. See + * `Channel.mergeAll`. + * + * @since 2.0.0 + * @category utils + */ +export const mergeOutWith: { + ( + n: number, + f: (o1: OutDone1, o2: OutDone1) => OutDone1 + ): ( + self: Channel< + Channel, + InElem, + OutErr, + InErr, + OutDone1, + InDone, + Env + > + ) => Channel + ( + self: Channel< + Channel, + InElem, + OutErr, + InErr, + OutDone1, + InDone, + Env + >, + n: number, + f: (o1: OutDone1, o2: OutDone1) => OutDone1 + ): Channel +} = channel.mergeOutWith + +/** + * Returns a new channel, which is the merge of this channel and the specified + * channel, where the behavior of the returned channel on left or right early + * termination is decided by the specified `leftDone` and `rightDone` merge + * decisions. + * + * @since 2.0.0 + * @category utils + */ +export const mergeWith: { + ( + options: { + readonly other: Channel + readonly onSelfDone: ( + exit: Exit.Exit + ) => MergeDecision.MergeDecision + readonly onOtherDone: ( + ex: Exit.Exit + ) => MergeDecision.MergeDecision + } + ): ( + self: Channel + ) => Channel< + OutElem1 | OutElem, + InElem & InElem1, + OutErr2 | OutErr3, + InErr & InErr1, + OutDone2 | OutDone3, + InDone & InDone1, + Env1 | Env + > + < + OutElem, + InElem, + OutErr, + InErr, + OutDone, + InDone, + Env, + OutElem1, + InElem1, + OutErr1, + InErr1, + OutDone1, + InDone1, + Env1, + OutErr2, + OutDone2, + OutErr3, + OutDone3 + >( + self: Channel, + options: { + readonly other: Channel + readonly onSelfDone: ( + exit: Exit.Exit + ) => MergeDecision.MergeDecision + readonly onOtherDone: ( + ex: Exit.Exit + ) => MergeDecision.MergeDecision + } + ): Channel< + OutElem | OutElem1, + InElem & InElem1, + OutErr2 | OutErr3, + InErr & InErr1, + OutDone2 | OutDone3, + InDone & InDone1, + Env | Env1 + > +} = channel.mergeWith + +/** + * Returns a channel that never completes + * + * @since 2.0.0 + * @category constructors + */ +export const never: Channel = channel.never + +/** + * Translates channel failure into death of the fiber, making all failures + * unchecked and not a part of the type of the channel. + * + * @since 2.0.0 + * @category error handling + */ +export const orDie: { + ( + error: LazyArg + ): ( + self: Channel + ) => Channel + ( + self: Channel, + error: LazyArg + ): Channel +} = channel.orDie + +/** + * Keeps none of the errors, and terminates the fiber with them, using the + * specified function to convert the `OutErr` into a defect. + * + * @since 2.0.0 + * @category error handling + */ +export const orDieWith: { + ( + f: (e: OutErr) => unknown + ): ( + self: Channel + ) => Channel + ( + self: Channel, + f: (e: OutErr) => unknown + ): Channel +} = channel.orDieWith + +/** + * Returns a new channel that will perform the operations of this one, until + * failure, and then it will switch over to the operations of the specified + * fallback channel. + * + * @since 2.0.0 + * @category error handling + */ +export const orElse: { + ( + that: LazyArg> + ): ( + self: Channel + ) => Channel< + OutElem1 | OutElem, + InElem & InElem1, + OutErr1, + InErr & InErr1, + OutDone1 | OutDone, + InDone & InDone1, + Env1 | Env + > + ( + self: Channel, + that: LazyArg> + ): Channel< + OutElem | OutElem1, + InElem & InElem1, + OutErr1, + InErr & InErr1, + OutDone | OutDone1, + InDone & InDone1, + Env | Env1 + > +} = channel.orElse + +/** + * Returns a new channel that pipes the output of this channel into the + * specified channel. The returned channel has the input type of this channel, + * and the output type of the specified channel, terminating with the value of + * the specified channel. + * + * @since 2.0.0 + * @category utils + */ +export const pipeTo: { + ( + that: Channel + ): ( + self: Channel + ) => Channel + ( + self: Channel, + that: Channel + ): Channel +} = core.pipeTo + +/** + * Returns a new channel that pipes the output of this channel into the + * specified channel and preserves this channel's failures without providing + * them to the other channel for observation. + * + * @since 2.0.0 + * @category utils + */ +export const pipeToOrFail: { + ( + that: Channel + ): ( + self: Channel + ) => Channel + ( + self: Channel, + that: Channel + ): Channel +} = channel.pipeToOrFail + +/** + * Provides the channel with its required context, which eliminates its + * dependency on `Env`. + * + * @since 2.0.0 + * @category context + */ +export const provideContext: { + ( + env: Context.Context + ): ( + self: Channel + ) => Channel + ( + self: Channel, + env: Context.Context + ): Channel +} = core.provideContext + +/** + * Provides a layer to the channel, which translates it to another level. + * + * @since 2.0.0 + * @category context + */ +export const provideLayer: { + ( + layer: Layer.Layer + ): ( + self: Channel + ) => Channel + ( + self: Channel, + layer: Layer.Layer + ): Channel +} = channel.provideLayer + +/** + * Transforms the context being provided to the channel with the specified + * function. + * + * @since 2.0.0 + * @category context + */ +export const mapInputContext: { + ( + f: (env: Context.Context) => Context.Context + ): ( + self: Channel + ) => Channel + ( + self: Channel, + f: (env: Context.Context) => Context.Context + ): Channel +} = channel.mapInputContext + +/** + * Splits the context into two parts, providing one part using the + * specified layer and leaving the remainder `Env0`. + * + * @since 2.0.0 + * @category context + */ +export const provideSomeLayer: { + ( + layer: Layer.Layer + ): ( + self: Channel + ) => Channel> + ( + self: Channel, + layer: Layer.Layer + ): Channel> +} = channel.provideSomeLayer + +/** + * Provides the effect with the single service it requires. If the effect + * requires more than one service use `provideContext` instead. + * + * @since 2.0.0 + * @category context + */ +export const provideService: { + ( + tag: Context.Tag, + service: Types.NoInfer + ): ( + self: Channel + ) => Channel> + ( + self: Channel, + tag: Context.Tag, + service: Types.NoInfer + ): Channel> +} = channel.provideService + +/** + * @since 2.0.0 + * @category constructors + */ +export const read: () => Channel, unknown, In, unknown> = channel.read + +/** + * @since 2.0.0 + * @category constructors + */ +export const readOrFail: (error: E) => Channel = core.readOrFail + +/** + * @since 2.0.0 + * @category constructors + */ +export const readWith: < + InElem, + OutElem, + OutErr, + InErr, + OutDone, + InDone, + Env, + OutElem2, + OutErr2, + OutDone2, + Env2, + OutElem3, + OutErr3, + OutDone3, + Env3 +>( + options: { + readonly onInput: (input: InElem) => Channel + readonly onFailure: (error: InErr) => Channel + readonly onDone: (done: InDone) => Channel + } +) => Channel< + OutElem | OutElem2 | OutElem3, + InElem, + OutErr | OutErr2 | OutErr3, + InErr, + OutDone | OutDone2 | OutDone3, + InDone, + Env | Env2 | Env3 +> = core.readWith + +/** + * @since 2.0.0 + * @category constructors + */ +export const readWithCause: < + InElem, + OutElem, + OutErr, + InErr, + OutDone, + InDone, + Env, + OutElem2, + OutErr2, + OutDone2, + Env2, + OutElem3, + OutErr3, + OutDone3, + Env3 +>( + options: { + readonly onInput: (input: InElem) => Channel + readonly onFailure: (cause: Cause.Cause) => Channel + readonly onDone: (done: InDone) => Channel + } +) => Channel< + OutElem | OutElem2 | OutElem3, + InElem, + OutErr | OutErr2 | OutErr3, + InErr, + OutDone | OutDone2 | OutDone3, + InDone, + Env | Env2 | Env3 +> = core.readWithCause + +/** + * Creates a channel which repeatedly runs this channel. + * + * @since 2.0.0 + * @category utils + */ +export const repeated: ( + self: Channel +) => Channel = channel.repeated + +/** + * Runs a channel until the end is received. + * + * @since 2.0.0 + * @category destructors + */ +export const run: ( + self: Channel +) => Effect.Effect = channel.run + +/** + * Run the channel until it finishes with a done value or fails with an error + * and collects its emitted output elements. + * + * The channel must not read any input. + * + * @since 2.0.0 + * @category destructors + */ +export const runCollect: ( + self: Channel +) => Effect.Effect<[Chunk.Chunk, OutDone], OutErr, Env> = channel.runCollect + +/** + * Runs a channel until the end is received. + * + * @since 2.0.0 + * @category destructors + */ +export const runDrain: ( + self: Channel +) => Effect.Effect = channel.runDrain + +/** + * Run the channel until it finishes with a done value or fails with an error. + * The channel must not read any input or write any output. + * + * Closing the channel, which includes execution of all the finalizers + * attached to the channel will be added to the current scope as a finalizer. + * + * @since 3.11.0 + * @category destructors + */ +export const runScoped: ( + self: Channel +) => Effect.Effect = channel.runScoped + +/** + * Use a scoped effect to emit an output element. + * + * @since 2.0.0 + * @category constructors + */ +export const scoped: ( + effect: Effect.Effect +) => Channel> = channel.scoped + +/** + * Use a function that receives a scope and returns an effect to emit an output + * element. The output element will be the result of the returned effect, if + * successful. + * + * @since 3.11.0 + * @category constructors + */ +export const scopedWith: ( + f: (scope: Scope.Scope) => Effect.Effect +) => Channel = channel.scopedWith + +/** + * Splits strings on newlines. Handles both Windows newlines (`\r\n`) and UNIX + * newlines (`\n`). + * + * @since 2.0.0 + * @category combinators + */ +export const splitLines: () => Channel< + Chunk.Chunk, + Chunk.Chunk, + Err, + Err, + Done, + Done, + never +> = channel.splitLines + +/** + * Constructs a channel that succeeds immediately with the specified value. + * + * @since 2.0.0 + * @category constructors + */ +export const succeed: (value: A) => Channel = core.succeed + +/** + * Lazily constructs a channel from the given side effect. + * + * @since 2.0.0 + * @category constructors + */ +export const suspend: ( + evaluate: LazyArg> +) => Channel = core.suspend + +/** + * Constructs a channel that succeeds immediately with the specified lazy value. + * + * @since 2.0.0 + * @category constructors + */ +export const sync: ( + evaluate: LazyArg +) => Channel = core.sync + +/** + * Converts a `Channel` to a `PubSub`. + * + * @since 2.0.0 + * @category destructors + */ +export const toPubSub: ( + pubsub: PubSub.PubSub>> +) => Channel = channel.toPubSub + +/** + * Returns a scoped `Effect` that can be used to repeatedly pull elements from + * the constructed `Channel`. The pull effect fails with the channel's failure + * in case the channel fails, or returns either the channel's done value or an + * emitted element. + * + * @since 2.0.0 + * @category destructors + */ +export const toPull: ( + self: Channel +) => Effect.Effect, OutErr, Env>, never, Scope.Scope | Env> = + channel.toPull + +/** + * Returns an `Effect` that can be used to repeatedly pull elements from the + * constructed `Channel` within the provided `Scope`. The pull effect fails + * with the channel's failure in case the channel fails, or returns either the + * channel's done value or an emitted element. + * + * @since 3.11.0 + * @category destructors + */ +export const toPullIn: { + ( + scope: Scope.Scope + ): ( + self: Channel + ) => Effect.Effect, OutErr, Env>, never, Env> + ( + self: Channel, + scope: Scope.Scope + ): Effect.Effect, OutErr, Env>, never, Env> +} = channel.toPullIn + +/** + * Converts a `Channel` to a `Queue`. + * + * @since 2.0.0 + * @category destructors + */ +export const toQueue: ( + queue: Queue.Enqueue>> +) => Channel = channel.toQueue + +/** Converts this channel to a `Sink`. + * + * @since 2.0.0 + * @category destructors + */ +export const toSink: ( + self: Channel, Chunk.Chunk, OutErr, InErr, OutDone, unknown, Env> +) => Sink.Sink = sink.channelToSink + +/** + * Converts this channel to a `Stream`. + * + * @since 2.0.0 + * @category destructors + */ +export const toStream: ( + self: Channel, unknown, OutErr, unknown, OutDone, unknown, Env> +) => Stream.Stream = stream.channelToStream + +const void_: Channel = core.void +export { + /** + * @since 2.0.0 + * @category constructors + */ + void_ as void +} + +/** + * Constructs a `Channel` from an effect that will result in a `Channel` if + * successful. + * + * @since 2.0.0 + * @category constructors + */ +export const unwrap: ( + channel: Effect.Effect, E, R> +) => Channel = channel.unwrap + +/** + * Constructs a `Channel` from a scoped effect that will result in a + * `Channel` if successful. + * + * @since 2.0.0 + * @category constructors + */ +export const unwrapScoped: ( + self: Effect.Effect, E, R> +) => Channel> = channel.unwrapScoped + +/** + * Constructs a `Channel` from a function which receives a `Scope` and returns + * an effect that will result in a `Channel` if successful. + * + * @since 3.11.0 + * @category constructors + */ +export const unwrapScopedWith: ( + f: (scope: Scope.Scope) => Effect.Effect, E, R> +) => Channel = channel.unwrapScopedWith + +/** + * Updates a service in the context of this channel. + * + * @since 2.0.0 + * @category context + */ +export const updateService: { + ( + tag: Context.Tag, + f: (resource: Types.NoInfer) => Types.NoInfer + ): ( + self: Channel + ) => Channel + ( + self: Channel, + tag: Context.Tag, + f: (resource: Types.NoInfer) => Types.NoInfer + ): Channel +} = channel.updateService + +/** + * Wraps the channel with a new span for tracing. + * + * @since 2.0.0 + * @category tracing + */ +export const withSpan: { + ( + name: string, + options?: Tracer.SpanOptions | undefined + ): ( + self: Channel + ) => Channel> + ( + self: Channel, + name: string, + options?: Tracer.SpanOptions | undefined + ): Channel> +} = channel.withSpan + +/** + * Writes a single value to the channel. + * + * @since 2.0.0 + * @category constructors + */ +export const write: (out: OutElem) => Channel = core.write + +/** + * Writes a sequence of values to the channel. + * + * @since 2.0.0 + * @category constructors + */ +export const writeAll: >( + ...outs: OutElems +) => Channel = channel.writeAll + +/** + * Writes a `Chunk` of values to the channel. + * + * @since 2.0.0 + * @category constructors + */ +export const writeChunk: ( + outs: Chunk.Chunk +) => Channel = channel.writeChunk + +/** + * Returns a new channel that is the sequential composition of this channel + * and the specified channel. The returned channel terminates with a tuple of + * the terminal values of both channels. + * + * @since 2.0.0 + * @category zipping + */ +export const zip: { + ( + that: Channel, + options?: { readonly concurrent?: boolean | undefined } | undefined + ): ( + self: Channel + ) => Channel< + OutElem1 | OutElem, + InElem & InElem1, + OutErr1 | OutErr, + InErr & InErr1, + readonly [OutDone, OutDone1], + InDone & InDone1, + Env1 | Env + > + ( + self: Channel, + that: Channel, + options?: { readonly concurrent?: boolean | undefined } | undefined + ): Channel< + OutElem | OutElem1, + InElem & InElem1, + OutErr | OutErr1, + InErr & InErr1, + readonly [OutDone, OutDone1], + InDone & InDone1, + Env | Env1 + > +} = channel.zip + +/** + * Returns a new channel that is the sequential composition of this channel + * and the specified channel. The returned channel terminates with the + * terminal value of this channel. + * + * @since 2.0.0 + * @category zipping + */ +export const zipLeft: { + ( + that: Channel, + options?: { readonly concurrent?: boolean | undefined } | undefined + ): ( + self: Channel + ) => Channel< + OutElem1 | OutElem, + InElem & InElem1, + OutErr1 | OutErr, + InErr & InErr1, + OutDone, + InDone & InDone1, + Env1 | Env + > + ( + self: Channel, + that: Channel, + options?: { readonly concurrent?: boolean | undefined } | undefined + ): Channel< + OutElem | OutElem1, + InElem & InElem1, + OutErr | OutErr1, + InErr & InErr1, + OutDone, + InDone & InDone1, + Env | Env1 + > +} = channel.zipLeft + +/** + * Returns a new channel that is the sequential composition of this channel + * and the specified channel. The returned channel terminates with the + * terminal value of that channel. + * + * @since 2.0.0 + * @category zipping + */ +export const zipRight: { + ( + that: Channel, + options?: { + readonly concurrent?: boolean | undefined + } + ): ( + self: Channel + ) => Channel< + OutElem1 | OutElem, + InElem & InElem1, + OutErr1 | OutErr, + InErr & InErr1, + OutDone1, + InDone & InDone1, + Env1 | Env + > + ( + self: Channel, + that: Channel, + options?: { + readonly concurrent?: boolean | undefined + } + ): Channel< + OutElem | OutElem1, + InElem & InElem1, + OutErr | OutErr1, + InErr & InErr1, + OutDone1, + InDone & InDone1, + Env | Env1 + > +} = channel.zipRight + +/** + * Represents a generic checked exception which occurs when a `Channel` is + * executed. + * + * @since 2.0.0 + * @category errors + */ +export const ChannelException: (error: E) => ChannelException = channel.ChannelException + +/** + * Returns `true` if the specified value is an `ChannelException`, `false` + * otherwise. + * + * @since 2.0.0 + * @category refinements + */ +export const isChannelException: (u: unknown) => u is ChannelException = channel.isChannelException diff --git a/repos/effect/packages/effect/src/ChildExecutorDecision.ts b/repos/effect/packages/effect/src/ChildExecutorDecision.ts new file mode 100644 index 0000000..05dc13c --- /dev/null +++ b/repos/effect/packages/effect/src/ChildExecutorDecision.ts @@ -0,0 +1,146 @@ +/** + * @since 2.0.0 + */ +import * as internal from "./internal/channel/childExecutorDecision.js" + +/** + * @since 2.0.0 + * @category symbols + */ +export const ChildExecutorDecisionTypeId: unique symbol = internal.ChildExecutorDecisionTypeId + +/** + * @since 2.0.0 + * @category symbols + */ +export type ChildExecutorDecisionTypeId = typeof ChildExecutorDecisionTypeId + +/** + * @since 2.0.0 + * @category models + */ +export type ChildExecutorDecision = Continue | Close | Yield + +/** + * @since 2.0.0 + */ +export declare namespace ChildExecutorDecision { + /** + * @since 2.0.0 + * @category models + */ + export interface Proto { + readonly [ChildExecutorDecisionTypeId]: ChildExecutorDecisionTypeId + } +} + +/** + * Continue executing the current substream + * + * @since 2.0.0 + * @category models + */ +export interface Continue extends ChildExecutorDecision.Proto { + readonly _tag: "Continue" +} + +/** + * Close the current substream with a given value and pass execution to the + * next substream + * + * @since 2.0.0 + * @category models + */ +export interface Close extends ChildExecutorDecision.Proto { + readonly _tag: "Close" + readonly value: unknown +} + +/** + * Pass execution to the next substream. This either pulls a new element + * from upstream, or yields to an already created active substream. + * + * @since 2.0.0 + * @category models + */ +export interface Yield extends ChildExecutorDecision.Proto { + readonly _tag: "Yield" +} + +/** + * @since 2.0.0 + * @category constructors + */ +export const Continue: (_: void) => ChildExecutorDecision = internal.Continue + +/** + * @since 2.0.0 + * @category constructors + */ +export const Close: (value: unknown) => ChildExecutorDecision = internal.Close + +/** + * @since 2.0.0 + * @category constructors + */ +export const Yield: (_: void) => ChildExecutorDecision = internal.Yield + +/** + * Returns `true` if the specified value is a `ChildExecutorDecision`, `false` + * otherwise. + * + * @since 2.0.0 + * @category refinements + */ +export const isChildExecutorDecision: (u: unknown) => u is ChildExecutorDecision = internal.isChildExecutorDecision + +/** + * Returns `true` if the specified `ChildExecutorDecision` is a `Continue`, + * `false` otherwise. + * + * @since 2.0.0 + * @category refinements + */ +export const isContinue: (self: ChildExecutorDecision) => self is Continue = internal.isContinue + +/** + * Returns `true` if the specified `ChildExecutorDecision` is a `Close`, `false` + * otherwise. + * + * @since 2.0.0 + * @category refinements + */ +export const isClose: (self: ChildExecutorDecision) => self is Close = internal.isClose + +/** + * Returns `true` if the specified `ChildExecutorDecision` is a `Yield`, `false` + * otherwise. + * + * @since 2.0.0 + * @category refinements + */ +export const isYield: (self: ChildExecutorDecision) => self is Yield = internal.isYield + +/** + * Folds over a `ChildExecutorDecision` to produce a value of type `A`. + * + * @since 2.0.0 + * @category folding + */ +export const match: { + ( + options: { + readonly onContinue: () => A + readonly onClose: (value: unknown) => A + readonly onYield: () => A + } + ): (self: ChildExecutorDecision) => A + ( + self: ChildExecutorDecision, + options: { + readonly onContinue: () => A + readonly onClose: (value: unknown) => A + readonly onYield: () => A + } + ): A +} = internal.match diff --git a/repos/effect/packages/effect/src/Chunk.ts b/repos/effect/packages/effect/src/Chunk.ts new file mode 100644 index 0000000..8afd6da --- /dev/null +++ b/repos/effect/packages/effect/src/Chunk.ts @@ -0,0 +1,1495 @@ +/** + * @since 2.0.0 + */ +import * as RA from "./Array.js" +import type { NonEmptyReadonlyArray } from "./Array.js" +import type { Either } from "./Either.js" +import * as Equal from "./Equal.js" +import * as Equivalence from "./Equivalence.js" +import { dual, identity, pipe } from "./Function.js" +import * as Hash from "./Hash.js" +import type { TypeLambda } from "./HKT.js" +import { format, type Inspectable, NodeInspectSymbol, toJSON } from "./Inspectable.js" +import type { NonEmptyIterable } from "./NonEmptyIterable.js" +import type { Option } from "./Option.js" +import * as O from "./Option.js" +import * as Order from "./Order.js" +import type { Pipeable } from "./Pipeable.js" +import { pipeArguments } from "./Pipeable.js" +import { hasProperty, type Predicate, type Refinement } from "./Predicate.js" +import type { Covariant, NoInfer } from "./Types.js" + +const TypeId: unique symbol = Symbol.for("effect/Chunk") as TypeId + +/** + * @category symbol + * @since 2.0.0 + */ +export type TypeId = typeof TypeId + +/** + * @category models + * @since 2.0.0 + */ +export interface Chunk extends Iterable, Equal.Equal, Pipeable, Inspectable { + readonly [TypeId]: { + readonly _A: Covariant + } + readonly length: number + /** @internal */ + right: Chunk + /** @internal */ + left: Chunk + /** @internal */ + backing: Backing + /** @internal */ + depth: number +} + +/** + * @category model + * @since 2.0.0 + */ +export interface NonEmptyChunk extends Chunk, NonEmptyIterable {} + +/** + * @category type lambdas + * @since 2.0.0 + */ +export interface ChunkTypeLambda extends TypeLambda { + readonly type: Chunk +} + +type Backing = + | IArray + | IConcat + | ISingleton + | IEmpty + | ISlice + +interface IArray { + readonly _tag: "IArray" + readonly array: ReadonlyArray +} + +interface IConcat { + readonly _tag: "IConcat" + readonly left: Chunk + readonly right: Chunk +} + +interface ISingleton { + readonly _tag: "ISingleton" + readonly a: A +} + +interface IEmpty { + readonly _tag: "IEmpty" +} + +interface ISlice { + readonly _tag: "ISlice" + readonly chunk: Chunk + readonly offset: number + readonly length: number +} + +function copy( + src: ReadonlyArray, + srcPos: number, + dest: Array, + destPos: number, + len: number +) { + for (let i = srcPos; i < Math.min(src.length, srcPos + len); i++) { + dest[destPos + i - srcPos] = src[i]! + } + return dest +} + +const emptyArray: ReadonlyArray = [] + +/** + * Compares the two chunks of equal length using the specified function + * + * @category equivalence + * @since 2.0.0 + */ +export const getEquivalence = (isEquivalent: Equivalence.Equivalence): Equivalence.Equivalence> => + Equivalence.make((self, that) => + self.length === that.length && toReadonlyArray(self).every((value, i) => isEquivalent(value, unsafeGet(that, i))) + ) + +const _equivalence = getEquivalence(Equal.equals) + +const ChunkProto: Omit, "backing" | "depth" | "left" | "length" | "right"> = { + [TypeId]: { + _A: (_: never) => _ + }, + toString(this: Chunk) { + return format(this.toJSON()) + }, + toJSON(this: Chunk) { + return { + _id: "Chunk", + values: toReadonlyArray(this).map(toJSON) + } + }, + [NodeInspectSymbol](this: Chunk) { + return this.toJSON() + }, + [Equal.symbol](this: Chunk, that: unknown): boolean { + return isChunk(that) && _equivalence(this, that) + }, + [Hash.symbol](this: Chunk): number { + return Hash.cached(this, Hash.array(toReadonlyArray(this))) + }, + [Symbol.iterator](this: Chunk): Iterator { + switch (this.backing._tag) { + case "IArray": { + return this.backing.array[Symbol.iterator]() + } + case "IEmpty": { + return emptyArray[Symbol.iterator]() + } + default: { + return toReadonlyArray(this)[Symbol.iterator]() + } + } + }, + pipe(this: Chunk) { + return pipeArguments(this, arguments) + } +} + +const makeChunk = (backing: Backing): Chunk => { + const chunk = Object.create(ChunkProto) + chunk.backing = backing + switch (backing._tag) { + case "IEmpty": { + chunk.length = 0 + chunk.depth = 0 + chunk.left = chunk + chunk.right = chunk + break + } + case "IConcat": { + chunk.length = backing.left.length + backing.right.length + chunk.depth = 1 + Math.max(backing.left.depth, backing.right.depth) + chunk.left = backing.left + chunk.right = backing.right + break + } + case "IArray": { + chunk.length = backing.array.length + chunk.depth = 0 + chunk.left = _empty + chunk.right = _empty + break + } + case "ISingleton": { + chunk.length = 1 + chunk.depth = 0 + chunk.left = _empty + chunk.right = _empty + break + } + case "ISlice": { + chunk.length = backing.length + chunk.depth = backing.chunk.depth + 1 + chunk.left = _empty + chunk.right = _empty + break + } + } + return chunk +} + +/** + * Checks if `u` is a `Chunk` + * + * @category constructors + * @since 2.0.0 + */ +export const isChunk: { + (u: Iterable): u is Chunk + (u: unknown): u is Chunk +} = (u: unknown): u is Chunk => hasProperty(u, TypeId) + +const _empty = makeChunk({ _tag: "IEmpty" }) + +/** + * @category constructors + * @since 2.0.0 + */ +export const empty: () => Chunk = () => _empty + +/** + * Builds a `NonEmptyChunk` from an non-empty collection of elements. + * + * @category constructors + * @since 2.0.0 + */ +export const make = ]>(...as: As): NonEmptyChunk => + unsafeFromNonEmptyArray(as) + +/** + * Builds a `NonEmptyChunk` from a single element. + * + * @category constructors + * @since 2.0.0 + */ +export const of = (a: A): NonEmptyChunk => makeChunk({ _tag: "ISingleton", a }) as any + +/** + * Creates a new `Chunk` from an iterable collection of values. + * + * @category constructors + * @since 2.0.0 + */ +export const fromIterable = (self: Iterable): Chunk => + isChunk(self) ? self : unsafeFromArray(RA.fromIterable(self)) + +const copyToArray = (self: Chunk, array: Array, initial: number): void => { + switch (self.backing._tag) { + case "IArray": { + copy(self.backing.array, 0, array, initial, self.length) + break + } + case "IConcat": { + copyToArray(self.left, array, initial) + copyToArray(self.right, array, initial + self.left.length) + break + } + case "ISingleton": { + array[initial] = self.backing.a + break + } + case "ISlice": { + let i = 0 + let j = initial + while (i < self.length) { + array[j] = unsafeGet(self, i) + i += 1 + j += 1 + } + break + } + } +} + +const toArray_ = (self: Chunk): Array => toReadonlyArray(self).slice() + +/** + * Converts a `Chunk` into an `Array`. If the provided `Chunk` is non-empty + * (`NonEmptyChunk`), the function will return a `NonEmptyArray`, ensuring the + * non-empty property is preserved. + * + * @category conversions + * @since 2.0.0 + */ +export const toArray: >( + self: S +) => S extends NonEmptyChunk ? RA.NonEmptyArray> : Array> = toArray_ as any + +const toReadonlyArray_ = (self: Chunk): ReadonlyArray => { + switch (self.backing._tag) { + case "IEmpty": { + return emptyArray + } + case "IArray": { + return self.backing.array + } + default: { + const arr = new Array(self.length) + copyToArray(self, arr, 0) + self.backing = { + _tag: "IArray", + array: arr + } + self.left = _empty + self.right = _empty + self.depth = 0 + return arr + } + } +} + +/** + * Converts a `Chunk` into a `ReadonlyArray`. If the provided `Chunk` is + * non-empty (`NonEmptyChunk`), the function will return a + * `NonEmptyReadonlyArray`, ensuring the non-empty property is preserved. + * + * @category conversions + * @since 2.0.0 + */ +export const toReadonlyArray: >( + self: S +) => S extends NonEmptyChunk ? RA.NonEmptyReadonlyArray> : ReadonlyArray> = + toReadonlyArray_ as any + +const reverseChunk = (self: Chunk): Chunk => { + switch (self.backing._tag) { + case "IEmpty": + case "ISingleton": + return self + case "IArray": { + return makeChunk({ _tag: "IArray", array: RA.reverse(self.backing.array) }) + } + case "IConcat": { + return makeChunk({ _tag: "IConcat", left: reverse(self.backing.right), right: reverse(self.backing.left) }) + } + case "ISlice": + return unsafeFromArray(RA.reverse(toReadonlyArray(self))) + } +} + +/** + * Reverses the order of elements in a `Chunk`. + * Importantly, if the input chunk is a `NonEmptyChunk`, the reversed chunk will also be a `NonEmptyChunk`. + * + * **Example** + * + * ```ts + * import { Chunk } from "effect" + * + * const chunk = Chunk.make(1, 2, 3) + * const result = Chunk.reverse(chunk) + * + * console.log(result) + * // { _id: 'Chunk', values: [ 3, 2, 1 ] } + * ``` + * + * @since 2.0.0 + * @category elements + */ +export const reverse: >(self: S) => Chunk.With> = reverseChunk as any + +/** + * This function provides a safe way to read a value at a particular index from a `Chunk`. + * + * @category elements + * @since 2.0.0 + */ +export const get: { + (index: number): (self: Chunk) => Option + (self: Chunk, index: number): Option +} = dual( + 2, + (self: Chunk, index: number): Option => + index < 0 || index >= self.length ? O.none() : O.some(unsafeGet(self, index)) +) + +/** + * Wraps an array into a chunk without copying, unsafe on mutable arrays + * + * @since 2.0.0 + * @category unsafe + */ +export const unsafeFromArray = (self: ReadonlyArray): Chunk => + self.length === 0 ? empty() : self.length === 1 ? of(self[0]) : makeChunk({ _tag: "IArray", array: self }) + +/** + * Wraps an array into a chunk without copying, unsafe on mutable arrays + * + * @since 2.0.0 + * @category unsafe + */ +export const unsafeFromNonEmptyArray = (self: NonEmptyReadonlyArray): NonEmptyChunk => + unsafeFromArray(self) as any + +/** + * Gets an element unsafely, will throw on out of bounds + * + * @since 2.0.0 + * @category unsafe + */ +export const unsafeGet: { + (index: number): (self: Chunk) => A + (self: Chunk, index: number): A +} = dual(2, (self: Chunk, index: number): A => { + switch (self.backing._tag) { + case "IEmpty": { + throw new Error(`Index out of bounds`) + } + case "ISingleton": { + if (index !== 0) { + throw new Error(`Index out of bounds`) + } + return self.backing.a + } + case "IArray": { + if (index >= self.length || index < 0) { + throw new Error(`Index out of bounds`) + } + return self.backing.array[index]! + } + case "IConcat": { + return index < self.left.length + ? unsafeGet(self.left, index) + : unsafeGet(self.right, index - self.left.length) + } + case "ISlice": { + return unsafeGet(self.backing.chunk, index + self.backing.offset) + } + } +}) + +/** + * Appends the specified element to the end of the `Chunk`. + * + * @category concatenating + * @since 2.0.0 + */ +export const append: { + (a: A2): (self: Chunk) => NonEmptyChunk + (self: Chunk, a: A2): NonEmptyChunk +} = dual(2, (self: Chunk, a: A2): NonEmptyChunk => appendAll(self, of(a))) + +/** + * Prepend an element to the front of a `Chunk`, creating a new `NonEmptyChunk`. + * + * @category concatenating + * @since 2.0.0 + */ +export const prepend: { + (elem: B): (self: Chunk) => NonEmptyChunk + (self: Chunk, elem: B): NonEmptyChunk +} = dual(2, (self: Chunk, elem: B): NonEmptyChunk => appendAll(of(elem), self)) + +/** + * Takes the first up to `n` elements from the chunk + * + * @since 2.0.0 + */ +export const take: { + (n: number): (self: Chunk) => Chunk + (self: Chunk, n: number): Chunk +} = dual(2, (self: Chunk, n: number): Chunk => { + if (n <= 0) { + return _empty + } else if (n >= self.length) { + return self + } else { + switch (self.backing._tag) { + case "ISlice": { + return makeChunk({ + _tag: "ISlice", + chunk: self.backing.chunk, + length: n, + offset: self.backing.offset + }) + } + case "IConcat": { + if (n > self.left.length) { + return makeChunk({ + _tag: "IConcat", + left: self.left, + right: take(self.right, n - self.left.length) + }) + } + + return take(self.left, n) + } + default: { + return makeChunk({ + _tag: "ISlice", + chunk: self, + offset: 0, + length: n + }) + } + } + } +}) + +/** + * Drops the first up to `n` elements from the chunk + * + * @since 2.0.0 + */ +export const drop: { + (n: number): (self: Chunk) => Chunk + (self: Chunk, n: number): Chunk +} = dual(2, (self: Chunk, n: number): Chunk => { + if (n <= 0) { + return self + } else if (n >= self.length) { + return _empty + } else { + switch (self.backing._tag) { + case "ISlice": { + return makeChunk({ + _tag: "ISlice", + chunk: self.backing.chunk, + offset: self.backing.offset + n, + length: self.backing.length - n + }) + } + case "IConcat": { + if (n > self.left.length) { + return drop(self.right, n - self.left.length) + } + return makeChunk({ + _tag: "IConcat", + left: drop(self.left, n), + right: self.right + }) + } + default: { + return makeChunk({ + _tag: "ISlice", + chunk: self, + offset: n, + length: self.length - n + }) + } + } + } +}) + +/** + * Drops the last `n` elements. + * + * @since 2.0.0 + */ +export const dropRight: { + (n: number): (self: Chunk) => Chunk + (self: Chunk, n: number): Chunk +} = dual(2, (self: Chunk, n: number): Chunk => take(self, Math.max(0, self.length - n))) + +/** + * Drops all elements so long as the predicate returns true. + * + * @since 2.0.0 + */ +export const dropWhile: { + (predicate: Predicate>): (self: Chunk) => Chunk + (self: Chunk, predicate: Predicate): Chunk +} = dual(2, (self: Chunk, predicate: Predicate): Chunk => { + const arr = toReadonlyArray(self) + const len = arr.length + let i = 0 + while (i < len && predicate(arr[i]!)) { + i++ + } + return drop(self, i) +}) + +/** + * Prepends the specified prefix chunk to the beginning of the specified chunk. + * If either chunk is non-empty, the result is also a non-empty chunk. + * + * **Example** + * + * ```ts + * import { Chunk } from "effect" + * + * const result = Chunk.make(1, 2).pipe(Chunk.prependAll(Chunk.make("a", "b")), Chunk.toArray) + * + * console.log(result) + * // [ "a", "b", 1, 2 ] + * ``` + * + * @category concatenating + * @since 2.0.0 + */ +export const prependAll: { + , T extends Chunk>( + that: T + ): (self: S) => Chunk.OrNonEmpty | Chunk.Infer> + (self: Chunk, that: NonEmptyChunk): NonEmptyChunk + (self: NonEmptyChunk, that: Chunk): NonEmptyChunk + (self: Chunk, that: Chunk): Chunk +} = dual(2, (self: NonEmptyChunk, that: Chunk): Chunk => appendAll(that, self)) + +/** + * Concatenates two chunks, combining their elements. + * If either chunk is non-empty, the result is also a non-empty chunk. + * + * **Example** + * + * ```ts + * import { Chunk } from "effect" + * + * const result = Chunk.make(1, 2).pipe(Chunk.appendAll(Chunk.make("a", "b")), Chunk.toArray) + * + * console.log(result) + * // [ 1, 2, "a", "b" ] + * ``` + * + * @category concatenating + * @since 2.0.0 + */ +export const appendAll: { + , T extends Chunk>( + that: T + ): (self: S) => Chunk.OrNonEmpty | Chunk.Infer> + (self: Chunk, that: NonEmptyChunk): NonEmptyChunk + (self: NonEmptyChunk, that: Chunk): NonEmptyChunk + (self: Chunk, that: Chunk): Chunk +} = dual(2, (self: Chunk, that: Chunk): Chunk => { + if (self.backing._tag === "IEmpty") { + return that + } + if (that.backing._tag === "IEmpty") { + return self + } + const diff = that.depth - self.depth + if (Math.abs(diff) <= 1) { + return makeChunk({ _tag: "IConcat", left: self, right: that }) + } else if (diff < -1) { + if (self.left.depth >= self.right.depth) { + const nr = appendAll(self.right, that) + return makeChunk({ _tag: "IConcat", left: self.left, right: nr }) + } else { + const nrr = appendAll(self.right.right, that) + if (nrr.depth === self.depth - 3) { + const nr = makeChunk({ _tag: "IConcat", left: self.right.left, right: nrr }) + return makeChunk({ _tag: "IConcat", left: self.left, right: nr }) + } else { + const nl = makeChunk({ _tag: "IConcat", left: self.left, right: self.right.left }) + return makeChunk({ _tag: "IConcat", left: nl, right: nrr }) + } + } + } else { + if (that.right.depth >= that.left.depth) { + const nl = appendAll(self, that.left) + return makeChunk({ _tag: "IConcat", left: nl, right: that.right }) + } else { + const nll = appendAll(self, that.left.left) + if (nll.depth === that.depth - 3) { + const nl = makeChunk({ _tag: "IConcat", left: nll, right: that.left.right }) + return makeChunk({ _tag: "IConcat", left: nl, right: that.right }) + } else { + const nr = makeChunk({ _tag: "IConcat", left: that.left.right, right: that.right }) + return makeChunk({ _tag: "IConcat", left: nll, right: nr }) + } + } + } +}) + +/** + * Returns a filtered and mapped subset of the elements. + * + * @since 2.0.0 + * @category filtering + */ +export const filterMap: { + (f: (a: A, i: number) => Option): (self: Chunk) => Chunk + (self: Chunk, f: (a: A, i: number) => Option): Chunk +} = dual( + 2, + (self: Chunk, f: (a: A, i: number) => Option): Chunk => unsafeFromArray(RA.filterMap(self, f)) +) + +/** + * Returns a filtered and mapped subset of the elements. + * + * @since 2.0.0 + * @category filtering + */ +export const filter: { + (refinement: Refinement, B>): (self: Chunk) => Chunk + (predicate: Predicate>): (self: Chunk) => Chunk + (self: Chunk, refinement: Refinement): Chunk + (self: Chunk, predicate: Predicate): Chunk +} = dual( + 2, + (self: Chunk, predicate: Predicate): Chunk => unsafeFromArray(RA.filter(self, predicate)) +) + +/** + * Transforms all elements of the chunk for as long as the specified function returns some value + * + * @since 2.0.0 + * @category filtering + */ +export const filterMapWhile: { + (f: (a: A) => Option): (self: Chunk) => Chunk + (self: Chunk, f: (a: A) => Option): Chunk +} = dual(2, (self: Chunk, f: (a: A) => Option) => unsafeFromArray(RA.filterMapWhile(self, f))) + +/** + * Filter out optional values + * + * @since 2.0.0 + * @category filtering + */ +export const compact = (self: Chunk>): Chunk => filterMap(self, identity) + +/** + * Applies a function to each element in a chunk and returns a new chunk containing the concatenated mapped elements. + * + * @since 2.0.0 + * @category sequencing + */ +export const flatMap: { + , T extends Chunk>( + f: (a: Chunk.Infer, i: number) => T + ): (self: S) => Chunk.AndNonEmpty> + (self: NonEmptyChunk, f: (a: A, i: number) => NonEmptyChunk): NonEmptyChunk + (self: Chunk, f: (a: A, i: number) => Chunk): Chunk +} = dual(2, (self: Chunk, f: (a: A, i: number) => Chunk) => { + if (self.backing._tag === "ISingleton") { + return f(self.backing.a, 0) + } + let out: Chunk = _empty + let i = 0 + for (const k of self) { + out = appendAll(out, f(k, i++)) + } + return out +}) + +/** + * Iterates over each element of a `Chunk` and applies a function to it. + * + * **Details** + * + * This function processes every element of the given `Chunk`, calling the + * provided function `f` on each element. It does not return a new value; + * instead, it is primarily used for side effects, such as logging or + * accumulating data in an external variable. + * + * @since 2.0.0 + * @category combinators + */ +export const forEach: { + (f: (a: A, index: number) => B): (self: Chunk) => void + (self: Chunk, f: (a: A, index: number) => B): void +} = dual(2, (self: Chunk, f: (a: A) => B): void => toReadonlyArray(self).forEach(f)) + +/** + * Flattens a chunk of chunks into a single chunk by concatenating all chunks. + * + * @since 2.0.0 + * @category sequencing + */ +export const flatten: >>(self: S) => Chunk.Flatten = flatMap(identity) as any + +/** + * Groups elements in chunks of up to `n` elements. + * + * @since 2.0.0 + * @category elements + */ +export const chunksOf: { + (n: number): (self: Chunk) => Chunk> + (self: Chunk, n: number): Chunk> +} = dual(2, (self: Chunk, n: number) => { + const gr: Array> = [] + let current: Array = [] + toReadonlyArray(self).forEach((a) => { + current.push(a) + if (current.length >= n) { + gr.push(unsafeFromArray(current)) + current = [] + } + }) + if (current.length > 0) { + gr.push(unsafeFromArray(current)) + } + return unsafeFromArray(gr) +}) + +/** + * Creates a Chunk of unique values that are included in all given Chunks. + * + * The order and references of result values are determined by the Chunk. + * + * @since 2.0.0 + * @category elements + */ +export const intersection: { + (that: Chunk): (self: Chunk) => Chunk + (self: Chunk, that: Chunk): Chunk +} = dual( + 2, + (self: Chunk, that: Chunk): Chunk => + unsafeFromArray(RA.intersection(toReadonlyArray(self), toReadonlyArray(that))) +) + +/** + * Determines if the chunk is empty. + * + * @since 2.0.0 + * @category elements + */ +export const isEmpty = (self: Chunk): boolean => self.length === 0 + +/** + * Determines if the chunk is not empty. + * + * @since 2.0.0 + * @category elements + */ +export const isNonEmpty = (self: Chunk): self is NonEmptyChunk => self.length > 0 + +/** + * Returns the first element of this chunk if it exists. + * + * @since 2.0.0 + * @category elements + */ +export const head: (self: Chunk) => Option = get(0) + +/** + * Returns the first element of this chunk. + * + * It will throw an error if the chunk is empty. + * + * @since 2.0.0 + * @category unsafe + */ +export const unsafeHead = (self: Chunk): A => unsafeGet(self, 0) + +/** + * Returns the first element of this non empty chunk. + * + * @since 2.0.0 + * @category elements + */ +export const headNonEmpty: (self: NonEmptyChunk) => A = unsafeHead + +/** + * Returns the last element of this chunk if it exists. + * + * @since 2.0.0 + * @category elements + */ +export const last = (self: Chunk): Option => get(self, self.length - 1) + +/** + * Returns the last element of this chunk. + * + * It will throw an error if the chunk is empty. + * + * @since 2.0.0 + * @category unsafe + */ +export const unsafeLast = (self: Chunk): A => unsafeGet(self, self.length - 1) + +/** + * Returns the last element of this non empty chunk. + * + * @since 3.4.0 + * @category elements + */ +export const lastNonEmpty: (self: NonEmptyChunk) => A = unsafeLast + +/** + * @since 2.0.0 + */ +export declare namespace Chunk { + /** + * @since 2.0.0 + */ + export type Infer> = S extends Chunk ? A : never + + /** + * @since 2.0.0 + */ + export type With, A> = S extends NonEmptyChunk ? NonEmptyChunk : Chunk + + /** + * @since 2.0.0 + */ + export type OrNonEmpty, T extends Chunk, A> = S extends NonEmptyChunk ? + NonEmptyChunk + : T extends NonEmptyChunk ? NonEmptyChunk + : Chunk + + /** + * @since 2.0.0 + */ + export type AndNonEmpty, T extends Chunk, A> = S extends NonEmptyChunk ? + T extends NonEmptyChunk ? NonEmptyChunk + : Chunk : + Chunk + + /** + * @since 2.0.0 + */ + export type Flatten>> = T extends NonEmptyChunk> ? NonEmptyChunk + : T extends Chunk> ? Chunk + : never +} + +/** + * Transforms the elements of a chunk using the specified mapping function. + * If the input chunk is non-empty, the resulting chunk will also be non-empty. + * + * **Example** + * + * ```ts + * import { Chunk } from "effect" + * + * const result = Chunk.map(Chunk.make(1, 2), (n) => n + 1) + * + * console.log(result) + * // { _id: 'Chunk', values: [ 2, 3 ] } + * ``` + * + * @since 2.0.0 + * @category mapping + */ +export const map: { + , B>(f: (a: Chunk.Infer, i: number) => B): (self: S) => Chunk.With + (self: NonEmptyChunk, f: (a: A, i: number) => B): NonEmptyChunk + (self: Chunk, f: (a: A, i: number) => B): Chunk +} = dual(2, (self: Chunk, f: (a: A, i: number) => B): Chunk => + self.backing._tag === "ISingleton" ? + of(f(self.backing.a, 0)) : + unsafeFromArray(pipe(toReadonlyArray(self), RA.map((a, i) => f(a, i))))) + +/** + * Statefully maps over the chunk, producing new elements of type `B`. + * + * @since 2.0.0 + * @category folding + */ +export const mapAccum: { + (s: S, f: (s: S, a: A) => readonly [S, B]): (self: Chunk) => [S, Chunk] + (self: Chunk, s: S, f: (s: S, a: A) => readonly [S, B]): [S, Chunk] +} = dual(3, (self: Chunk, s: S, f: (s: S, a: A) => readonly [S, B]): [S, Chunk] => { + const [s1, as] = RA.mapAccum(self, s, f) + return [s1, unsafeFromArray(as)] +}) + +/** + * Separate elements based on a predicate that also exposes the index of the element. + * + * @category filtering + * @since 2.0.0 + */ +export const partition: { + ( + refinement: (a: NoInfer, i: number) => a is B + ): (self: Chunk) => [excluded: Chunk>, satisfying: Chunk] + ( + predicate: (a: NoInfer, i: number) => boolean + ): (self: Chunk) => [excluded: Chunk, satisfying: Chunk] + ( + self: Chunk, + refinement: (a: A, i: number) => a is B + ): [excluded: Chunk>, satisfying: Chunk] + (self: Chunk, predicate: (a: A, i: number) => boolean): [excluded: Chunk, satisfying: Chunk] +} = dual( + 2, + (self: Chunk, predicate: (a: A, i: number) => boolean): [excluded: Chunk, satisfying: Chunk] => + pipe( + RA.partition(toReadonlyArray(self), predicate), + ([l, r]) => [unsafeFromArray(l), unsafeFromArray(r)] + ) +) + +/** + * Partitions the elements of this chunk into two chunks using f. + * + * @category filtering + * @since 2.0.0 + */ +export const partitionMap: { + (f: (a: A) => Either): (self: Chunk) => [left: Chunk, right: Chunk] + (self: Chunk, f: (a: A) => Either): [left: Chunk, right: Chunk] +} = dual(2, (self: Chunk, f: (a: A) => Either): [left: Chunk, right: Chunk] => + pipe( + RA.partitionMap(toReadonlyArray(self), f), + ([l, r]) => [unsafeFromArray(l), unsafeFromArray(r)] + )) + +/** + * Partitions the elements of this chunk into two chunks. + * + * @category filtering + * @since 2.0.0 + */ +export const separate = (self: Chunk>): [Chunk, Chunk] => + pipe( + RA.separate(toReadonlyArray(self)), + ([l, r]) => [unsafeFromArray(l), unsafeFromArray(r)] + ) + +/** + * Retireves the size of the chunk + * + * @since 2.0.0 + * @category elements + */ +export const size = (self: Chunk): number => self.length + +/** + * Sort the elements of a Chunk in increasing order, creating a new Chunk. + * + * @since 2.0.0 + * @category sorting + */ +export const sort: { + (O: Order.Order): (self: Chunk) => Chunk + (self: Chunk, O: Order.Order): Chunk +} = dual( + 2, + (self: Chunk, O: Order.Order): Chunk => unsafeFromArray(RA.sort(toReadonlyArray(self), O)) +) + +/** + * @since 2.0.0 + * @category sorting + */ +export const sortWith: { + (f: (a: A) => B, order: Order.Order): (self: Chunk) => Chunk + (self: Chunk, f: (a: A) => B, order: Order.Order): Chunk +} = dual( + 3, + (self: Chunk, f: (a: A) => B, order: Order.Order): Chunk => sort(self, Order.mapInput(order, f)) +) + +/** + * Returns two splits of this chunk at the specified index. + * + * @since 2.0.0 + * @category splitting + */ +export const splitAt: { + (n: number): (self: Chunk) => [beforeIndex: Chunk, fromIndex: Chunk] + (self: Chunk, n: number): [beforeIndex: Chunk, fromIndex: Chunk] +} = dual(2, (self: Chunk, n: number): [Chunk, Chunk] => [take(self, n), drop(self, n)]) + +/** + * Splits a `NonEmptyChunk` into two segments, with the first segment containing a maximum of `n` elements. + * The value of `n` must be `>= 1`. + * + * @category splitting + * @since 2.0.0 + */ +export const splitNonEmptyAt: { + (n: number): (self: NonEmptyChunk) => [beforeIndex: NonEmptyChunk, fromIndex: Chunk] + (self: NonEmptyChunk, n: number): [beforeIndex: NonEmptyChunk, fromIndex: Chunk] +} = dual(2, (self: NonEmptyChunk, n: number): [Chunk, Chunk] => { + const _n = Math.max(1, Math.floor(n)) + return _n >= self.length ? + [self, empty()] : + [take(self, _n), drop(self, _n)] +}) + +/** + * Splits this chunk into `n` equally sized chunks. + * + * @since 2.0.0 + * @category splitting + */ +export const split: { + (n: number): (self: Chunk) => Chunk> + (self: Chunk, n: number): Chunk> +} = dual(2, (self: Chunk, n: number) => chunksOf(self, Math.ceil(self.length / Math.floor(n)))) + +/** + * Splits this chunk on the first element that matches this predicate. + * Returns a tuple containing two chunks: the first one is before the match, and the second one is from the match onward. + * + * @category splitting + * @since 2.0.0 + */ +export const splitWhere: { + (predicate: Predicate>): (self: Chunk) => [beforeMatch: Chunk, fromMatch: Chunk] + (self: Chunk, predicate: Predicate): [beforeMatch: Chunk, fromMatch: Chunk] +} = dual(2, (self: Chunk, predicate: Predicate): [beforeMatch: Chunk, fromMatch: Chunk] => { + let i = 0 + for (const a of toReadonlyArray(self)) { + if (predicate(a)) { + break + } else { + i++ + } + } + return splitAt(self, i) +}) + +/** + * Returns every elements after the first. + * + * @since 2.0.0 + * @category elements + */ +export const tail = (self: Chunk): Option> => self.length > 0 ? O.some(drop(self, 1)) : O.none() + +/** + * Returns every elements after the first. + * + * @since 2.0.0 + * @category elements + */ +export const tailNonEmpty = (self: NonEmptyChunk): Chunk => drop(self, 1) + +/** + * Takes the last `n` elements. + * + * @since 2.0.0 + * @category elements + */ +export const takeRight: { + (n: number): (self: Chunk) => Chunk + (self: Chunk, n: number): Chunk +} = dual(2, (self: Chunk, n: number): Chunk => drop(self, self.length - n)) + +/** + * Takes all elements so long as the predicate returns true. + * + * @since 2.0.0 + * @category elements + */ +export const takeWhile: { + (refinement: Refinement, B>): (self: Chunk) => Chunk + (predicate: Predicate>): (self: Chunk) => Chunk + (self: Chunk, refinement: Refinement): Chunk + (self: Chunk, predicate: Predicate): Chunk +} = dual(2, (self: Chunk, predicate: Predicate): Chunk => { + const out: Array = [] + for (const a of toReadonlyArray(self)) { + if (predicate(a)) { + out.push(a) + } else { + break + } + } + return unsafeFromArray(out) +}) + +/** + * Creates a Chunks of unique values, in order, from all given Chunks. + * + * @since 2.0.0 + * @category elements + */ +export const union: { + (that: Chunk): (self: Chunk) => Chunk + (self: Chunk, that: Chunk): Chunk +} = dual( + 2, + (self: Chunk, that: Chunk) => unsafeFromArray(RA.union(toReadonlyArray(self), toReadonlyArray(that))) +) + +/** + * Remove duplicates from an array, keeping the first occurrence of an element. + * + * @since 2.0.0 + * @category elements + */ +export const dedupe = (self: Chunk): Chunk => unsafeFromArray(RA.dedupe(toReadonlyArray(self))) + +/** + * Deduplicates adjacent elements that are identical. + * + * @since 2.0.0 + * @category filtering + */ +export const dedupeAdjacent = (self: Chunk): Chunk => unsafeFromArray(RA.dedupeAdjacent(self)) + +/** + * Takes a `Chunk` of pairs and return two corresponding `Chunk`s. + * + * Note: The function is reverse of `zip`. + * + * @since 2.0.0 + * @category elements + */ +export const unzip = (self: Chunk): [Chunk, Chunk] => { + const [left, right] = RA.unzip(self) + return [unsafeFromArray(left), unsafeFromArray(right)] +} + +/** + * Zips this chunk pointwise with the specified chunk using the specified combiner. + * + * @since 2.0.0 + * @category zipping + */ +export const zipWith: { + (that: Chunk, f: (a: A, b: B) => C): (self: Chunk) => Chunk + (self: Chunk, that: Chunk, f: (a: A, b: B) => C): Chunk +} = dual( + 3, + (self: Chunk, that: Chunk, f: (a: A, b: B) => C): Chunk => + unsafeFromArray(RA.zipWith(self, that, f)) +) + +/** + * Zips this chunk pointwise with the specified chunk. + * + * @since 2.0.0 + * @category zipping + */ +export const zip: { + (that: Chunk): (self: Chunk) => Chunk<[A, B]> + (self: Chunk, that: Chunk): Chunk<[A, B]> +} = dual( + 2, + (self: Chunk, that: Chunk): Chunk<[A, B]> => zipWith(self, that, (a, b) => [a, b]) +) + +/** + * Delete the element at the specified index, creating a new `Chunk`. + * + * @since 2.0.0 + */ +export const remove: { + (i: number): (self: Chunk) => Chunk + (self: Chunk, i: number): Chunk +} = dual( + 2, + (self: Chunk, i: number): Chunk => { + if (i < 0 || i >= self.length) return self + return unsafeFromArray(RA.remove(toReadonlyArray(self), i)) + } +) + +/** + * @since 3.16.0 + */ +export const removeOption: { + (i: number): (self: Chunk) => Option> + (self: Chunk, i: number): Option> +} = dual( + 2, + (self: Chunk, i: number): Option> => { + if (i < 0 || i >= self.length) return O.none() + return O.some(unsafeFromArray(RA.remove(toReadonlyArray(self), i))) + } +) + +/** + * @since 2.0.0 + */ +export const modifyOption: { + (i: number, f: (a: A) => B): (self: Chunk) => Option> + (self: Chunk, i: number, f: (a: A) => B): Option> +} = dual( + 3, + (self: Chunk, i: number, f: (a: A) => B): Option> => { + if (i < 0 || i >= self.length) return O.none() + return O.some(unsafeFromArray(RA.modify(toReadonlyArray(self), i, f))) + } +) + +/** + * Apply a function to the element at the specified index, creating a new `Chunk`, + * or returning the input if the index is out of bounds. + * + * @since 2.0.0 + */ +export const modify: { + (i: number, f: (a: A) => B): (self: Chunk) => Chunk + (self: Chunk, i: number, f: (a: A) => B): Chunk +} = dual( + 3, + (self: Chunk, i: number, f: (a: A) => B): Chunk => O.getOrElse(modifyOption(self, i, f), () => self) +) + +/** + * Change the element at the specified index, creating a new `Chunk`, + * or returning the input if the index is out of bounds. + * + * @since 2.0.0 + */ +export const replace: { + (i: number, b: B): (self: Chunk) => Chunk + (self: Chunk, i: number, b: B): Chunk +} = dual(3, (self: Chunk, i: number, b: B): Chunk => modify(self, i, () => b)) + +/** + * @since 2.0.0 + */ +export const replaceOption: { + (i: number, b: B): (self: Chunk) => Option> + (self: Chunk, i: number, b: B): Option> +} = dual(3, (self: Chunk, i: number, b: B): Option> => modifyOption(self, i, () => b)) + +/** + * Return a Chunk of length n with element i initialized with f(i). + * + * **Note**. `n` is normalized to an integer >= 1. + * + * @category constructors + * @since 2.0.0 + */ +export const makeBy: { + (f: (i: number) => A): (n: number) => NonEmptyChunk + (n: number, f: (i: number) => A): NonEmptyChunk +} = dual(2, (n, f) => fromIterable(RA.makeBy(n, f))) + +/** + * Create a non empty `Chunk` containing a range of integers, including both endpoints. + * + * @category constructors + * @since 2.0.0 + */ +export const range = (start: number, end: number): NonEmptyChunk => + start <= end ? makeBy(end - start + 1, (i) => start + i) : of(start) + +// ------------------------------------------------------------------------------------- +// re-exports from ReadonlyArray +// ------------------------------------------------------------------------------------- + +/** + * Returns a function that checks if a `Chunk` contains a given value using the default `Equivalence`. + * + * @category elements + * @since 2.0.0 + */ +export const contains: { + (a: A): (self: Chunk) => boolean + (self: Chunk, a: A): boolean +} = RA.contains + +/** + * Returns a function that checks if a `Chunk` contains a given value using a provided `isEquivalent` function. + * + * @category elements + * @since 2.0.0 + */ +export const containsWith: ( + isEquivalent: (self: A, that: A) => boolean +) => { + (a: A): (self: Chunk) => boolean + (self: Chunk, a: A): boolean +} = RA.containsWith + +/** + * Returns the first element that satisfies the specified + * predicate, or `None` if no such element exists. + * + * @category elements + * @since 2.0.0 + */ +export const findFirst: { + (refinement: Refinement, B>): (self: Chunk) => Option + (predicate: Predicate>): (self: Chunk) => Option + (self: Chunk, refinement: Refinement): Option + (self: Chunk, predicate: Predicate): Option +} = RA.findFirst + +/** + * Return the first index for which a predicate holds. + * + * @category elements + * @since 2.0.0 + */ +export const findFirstIndex: { + (predicate: Predicate): (self: Chunk) => Option + (self: Chunk, predicate: Predicate): Option +} = RA.findFirstIndex + +/** + * Find the last element for which a predicate holds. + * + * @category elements + * @since 2.0.0 + */ +export const findLast: { + (refinement: Refinement, B>): (self: Chunk) => Option + (predicate: Predicate>): (self: Chunk) => Option + (self: Chunk, refinement: Refinement): Option + (self: Chunk, predicate: Predicate): Option +} = RA.findLast + +/** + * Return the last index for which a predicate holds. + * + * @category elements + * @since 2.0.0 + */ +export const findLastIndex: { + (predicate: Predicate): (self: Chunk) => Option + (self: Chunk, predicate: Predicate): Option +} = RA.findLastIndex + +/** + * Check if a predicate holds true for every `Chunk` element. + * + * @category elements + * @since 2.0.0 + */ +export const every: { + (refinement: Refinement, B>): (self: Chunk) => self is Chunk + (predicate: Predicate): (self: Chunk) => boolean + (self: Chunk, refinement: Refinement): self is Chunk + (self: Chunk, predicate: Predicate): boolean +} = dual( + 2, + (self: Chunk, refinement: Refinement): self is Chunk => + RA.fromIterable(self).every(refinement) +) + +/** + * Check if a predicate holds true for some `Chunk` element. + * + * @category elements + * @since 2.0.0 + */ +export const some: { + (predicate: Predicate>): (self: Chunk) => self is NonEmptyChunk + (self: Chunk, predicate: Predicate): self is NonEmptyChunk +} = dual( + 2, + (self: Chunk, predicate: Predicate): self is NonEmptyChunk => RA.fromIterable(self).some(predicate) +) + +/** + * Joins the elements together with "sep" in the middle. + * + * @category folding + * @since 2.0.0 + */ +export const join: { + (sep: string): (self: Chunk) => string + (self: Chunk, sep: string): string +} = RA.join + +/** + * @category folding + * @since 2.0.0 + */ +export const reduce: { + (b: B, f: (b: B, a: A, i: number) => B): (self: Chunk) => B + (self: Chunk, b: B, f: (b: B, a: A, i: number) => B): B +} = RA.reduce + +/** + * @category folding + * @since 2.0.0 + */ +export const reduceRight: { + (b: B, f: (b: B, a: A, i: number) => B): (self: Chunk) => B + (self: Chunk, b: B, f: (b: B, a: A, i: number) => B): B +} = RA.reduceRight + +/** + * Creates a `Chunk` of values not included in the other given `Chunk` using the provided `isEquivalent` function. + * The order and references of result values are determined by the first `Chunk`. + * + * @since 3.2.0 + */ +export const differenceWith = (isEquivalent: (self: A, that: A) => boolean): { + (that: Chunk): (self: Chunk) => Chunk + (self: Chunk, that: Chunk): Chunk +} => { + return dual( + 2, + (self: Chunk, that: Chunk): Chunk => unsafeFromArray(RA.differenceWith(isEquivalent)(that, self)) + ) +} + +/** + * Creates a `Chunk` of values not included in the other given `Chunk`. + * The order and references of result values are determined by the first `Chunk`. + * + * @since 3.2.0 + */ +export const difference: { + (that: Chunk): (self: Chunk) => Chunk + (self: Chunk, that: Chunk): Chunk +} = dual( + 2, + (self: Chunk, that: Chunk): Chunk => unsafeFromArray(RA.difference(that, self)) +) diff --git a/repos/effect/packages/effect/src/Clock.ts b/repos/effect/packages/effect/src/Clock.ts new file mode 100644 index 0000000..ae42987 --- /dev/null +++ b/repos/effect/packages/effect/src/Clock.ts @@ -0,0 +1,111 @@ +/** + * @since 2.0.0 + */ +import type * as Context from "./Context.js" +import type * as Duration from "./Duration.js" +import type * as Effect from "./Effect.js" +import * as internal from "./internal/clock.js" +import * as defaultServices from "./internal/defaultServices.js" + +/** + * @since 2.0.0 + * @category symbols + */ +export const ClockTypeId: unique symbol = internal.ClockTypeId + +/** + * @since 2.0.0 + * @category symbols + */ +export type ClockTypeId = typeof ClockTypeId + +/** + * Represents a time-based clock which provides functionality related to time + * and scheduling. + * + * @since 2.0.0 + * @category models + */ +export interface Clock { + readonly [ClockTypeId]: ClockTypeId + /** + * Unsafely returns the current time in milliseconds. + */ + unsafeCurrentTimeMillis(): number + /** + * Returns the current time in milliseconds. + */ + readonly currentTimeMillis: Effect.Effect + /** + * Unsafely returns the current time in nanoseconds. + */ + unsafeCurrentTimeNanos(): bigint + /** + * Returns the current time in nanoseconds. + */ + readonly currentTimeNanos: Effect.Effect + /** + * Asynchronously sleeps for the specified duration. + */ + sleep(duration: Duration.Duration): Effect.Effect +} + +/** + * @since 2.0.0 + * @category models + */ +export type CancelToken = () => boolean + +/** + * @since 2.0.0 + * @category models + */ +export type Task = () => void + +/** + * @since 2.0.0 + * @category models + */ +export interface ClockScheduler { + /** + * Unsafely schedules the specified task for the specified duration. + */ + unsafeSchedule(task: Task, duration: Duration.Duration): CancelToken +} + +/** + * @since 2.0.0 + * @category constructors + */ +export const make: (_: void) => Clock = internal.make + +/** + * @since 2.0.0 + * @category constructors + */ +export const sleep: (duration: Duration.DurationInput) => Effect.Effect = defaultServices.sleep + +/** + * @since 2.0.0 + * @category constructors + */ +export const currentTimeMillis: Effect.Effect = defaultServices.currentTimeMillis + +/** + * @since 2.0.0 + * @category constructors + */ +export const currentTimeNanos: Effect.Effect = defaultServices.currentTimeNanos + +/** + * @since 2.0.0 + * @category constructors + */ +export const clockWith: (f: (clock: Clock) => Effect.Effect) => Effect.Effect = + defaultServices.clockWith + +/** + * @since 2.0.0 + * @category context + */ +export const Clock: Context.Tag = internal.clockTag diff --git a/repos/effect/packages/effect/src/Config.ts b/repos/effect/packages/effect/src/Config.ts new file mode 100644 index 0000000..89d6a7f --- /dev/null +++ b/repos/effect/packages/effect/src/Config.ts @@ -0,0 +1,542 @@ +/** + * @since 2.0.0 + */ +import type * as Brand from "./Brand.js" +import type * as Chunk from "./Chunk.js" +import type * as ConfigError from "./ConfigError.js" +import type * as Duration from "./Duration.js" +import type * as Effect from "./Effect.js" +import type * as Either from "./Either.js" +import type { LazyArg } from "./Function.js" +import type * as HashMap from "./HashMap.js" +import type * as HashSet from "./HashSet.js" +import * as internal from "./internal/config.js" +import type * as LogLevel from "./LogLevel.js" +import type * as Option from "./Option.js" +import type { Predicate, Refinement } from "./Predicate.js" +import type * as Redacted from "./Redacted.js" +import type * as Secret from "./Secret.js" +import type * as Types from "./Types.js" + +/** + * @since 2.0.0 + * @category symbols + */ +export const ConfigTypeId: unique symbol = internal.ConfigTypeId + +/** + * @since 2.0.0 + * @category symbols + */ +export type ConfigTypeId = typeof ConfigTypeId + +/** + * A `Config` describes the structure of some configuration data. + * + * @since 2.0.0 + * @category models + */ +export interface Config extends Config.Variance, Effect.Effect {} + +/** + * @since 2.0.0 + */ +export declare namespace Config { + /** + * @since 2.0.0 + * @category models + */ + export interface Variance { + readonly [ConfigTypeId]: { + readonly _A: Types.Covariant + } + } + + /** + * @since 2.5.0 + * @category models + */ + export type Success> = [T] extends [Config] ? _A : never + + /** + * @since 2.0.0 + * @category models + */ + export interface Primitive extends Config { + readonly description: string + parse(text: string): Either.Either + } + + /** + * Wraps a nested structure, converting all primitives to a `Config`. + * + * `Config.Wrap<{ key: string }>` becomes `{ key: Config }` + * + * To create the resulting config, use the `unwrap` constructor. + * + * @since 2.0.0 + * @category models + */ + export type Wrap = [NonNullable] extends [infer T] ? [IsPlainObject] extends [true] ? + | { readonly [K in keyof A]: Wrap } + | Config + : Config + : Config + + type IsPlainObject = [A] extends [Record] + ? [keyof A] extends [never] ? false : [keyof A] extends [string] ? true : false + : false +} + +/** + * @since 2.0.0 + * @category models + */ +export type LiteralValue = string | number | boolean | null | bigint + +/** + * Constructs a config from a tuple / struct / arguments of configs. + * + * @since 2.0.0 + * @category constructors + */ +export const all: > | Record>>( + arg: Arg +) => Config< + [Arg] extends [ReadonlyArray>] ? { + -readonly [K in keyof Arg]: [Arg[K]] extends [Config] ? A : never + } + : [Arg] extends [Iterable>] ? Array + : [Arg] extends [Record>] ? { + -readonly [K in keyof Arg]: [Arg[K]] extends [Config] ? A : never + } + : never +> = internal.all + +/** + * Constructs a config for an array of values. + * + * @since 2.0.0 + * @category constructors + */ +export const array: (config: Config, name?: string) => Config> = internal.array + +/** + * Constructs a config for a boolean value. + * + * @since 2.0.0 + * @category constructors + */ +export const boolean: (name?: string) => Config = internal.boolean + +/** + * Constructs a config for a network port [1, 65535]. + * + * @since 3.16.0 + * @category constructors + */ +export const port: (name?: string) => Config = internal.port + +/** + * Constructs a config for an URL value. + * + * @since 3.11.0 + * @category constructors + */ +export const url: (name?: string) => Config = internal.url + +/** + * Constructs a config for a sequence of values. + * + * @since 2.0.0 + * @category constructors + */ +export const chunk: (config: Config, name?: string) => Config> = internal.chunk + +/** + * Constructs a config for a date value. + * + * @since 2.0.0 + * @category constructors + */ +export const date: (name?: string) => Config = internal.date + +/** + * Constructs a config that fails with the specified message. + * + * @since 2.0.0 + * @category constructors + */ +export const fail: (message: string) => Config = internal.fail + +/** + * Constructs a config for a float value. + * + * @since 2.0.0 + * @category constructors + */ +export const number: (name?: string) => Config = internal.number + +/** + * Constructs a config for a integer value. + * + * @since 2.0.0 + * @category constructors + */ +export const integer: (name?: string) => Config = internal.integer + +/** + * Constructs a config for a literal value. + * + * **Example** + * + * ```ts + * import { Config } from "effect" + * + * const config = Config.literal("http", "https")("PROTOCOL") + * ``` + * + * @since 2.0.0 + * @category constructors + */ +export const literal: >(...literals: Literals) => ( + name?: string +) => Config = internal.literal + +/** + * Constructs a config for a `LogLevel` value. + * + * @since 2.0.0 + * @category constructors + */ +export const logLevel: (name?: string) => Config = internal.logLevel + +/** + * Constructs a config for a duration value. + * + * @since 2.5.0 + * @category constructors + */ +export const duration: (name?: string) => Config = internal.duration + +/** + * This function returns `true` if the specified value is an `Config` value, + * `false` otherwise. + * + * This function can be useful for checking the type of a value before + * attempting to operate on it as an `Config` value. For example, you could + * use `isConfig` to check the type of a value before using it as an + * argument to a function that expects an `Config` value. + * + * @since 2.0.0 + * @category refinements + */ +export const isConfig: (u: unknown) => u is Config = internal.isConfig + +/** + * Returns a config whose structure is the same as this one, but which produces + * a different value, constructed using the specified function. + * + * @since 2.0.0 + * @category mapping + */ +export const map: { + (f: (a: A) => B): (self: Config) => Config + (self: Config, f: (a: A) => B): Config +} = internal.map + +/** + * Returns a config whose structure is the same as this one, but which may + * produce a different value, constructed using the specified function, which + * may throw exceptions that will be translated into validation errors. + * + * @since 2.0.0 + * @category utils + */ +export const mapAttempt: { + (f: (a: A) => B): (self: Config) => Config + (self: Config, f: (a: A) => B): Config +} = internal.mapAttempt + +/** + * Returns a new config whose structure is the samea as this one, but which + * may produce a different value, constructed using the specified fallible + * function. + * + * @since 2.0.0 + * @category utils + */ +export const mapOrFail: { + (f: (a: A) => Either.Either): (self: Config) => Config + (self: Config, f: (a: A) => Either.Either): Config +} = internal.mapOrFail + +/** + * Returns a config that has this configuration nested as a property of the + * specified name. + * + * @since 2.0.0 + * @category utils + */ +export const nested: { + (name: string): (self: Config) => Config + (self: Config, name: string): Config +} = internal.nested + +/** + * Returns a config whose structure is preferentially described by this + * config, but which falls back to the specified config if there is an issue + * reading from this config. + * + * @since 2.0.0 + * @category utils + */ +export const orElse: { + (that: LazyArg>): (self: Config) => Config + (self: Config, that: LazyArg>): Config +} = internal.orElse + +/** + * Returns configuration which reads from this configuration, but which falls + * back to the specified configuration if reading from this configuration + * fails with an error satisfying the specified predicate. + * + * @since 2.0.0 + * @category utils + */ +export const orElseIf: { + ( + options: { + readonly if: Predicate + readonly orElse: LazyArg> + } + ): (self: Config) => Config + ( + self: Config, + options: { + readonly if: Predicate + readonly orElse: LazyArg> + } + ): Config +} = internal.orElseIf + +/** + * Returns an optional version of this config, which will be `None` if the + * data is missing from configuration, and `Some` otherwise. + * + * @since 2.0.0 + * @category utils + */ +export const option: (self: Config) => Config> = internal.option + +/** + * Constructs a new primitive config. + * + * @since 2.0.0 + * @category constructors + */ +export const primitive: ( + description: string, + parse: (text: string) => Either.Either +) => Config = internal.primitive + +/** + * Returns a config that describes a sequence of values, each of which has the + * structure of this config. + * + * @since 2.0.0 + * @category utils + */ +export const repeat: (self: Config) => Config> = internal.repeat + +/** + * Constructs a config for a secret value. + * + * @since 2.0.0 + * @category constructors + * @deprecated + */ +export const secret: (name?: string) => Config = internal.secret + +/** + * Constructs a config for a redacted value. + * + * @since 2.0.0 + * @category constructors + */ +export const redacted: { + (name?: string): Config + (config: Config): Config> +} = internal.redacted + +/** + * Constructs a config for a branded value. + * + * @since 3.16.0 + * @category constructors + */ +export const branded: { + >( + constructor: Brand.Brand.Constructor + ): (config: Config) => Config + >( + name: string | undefined, + constructor: Brand.Brand.Constructor + ): Config + >( + config: Config, + constructor: Brand.Brand.Constructor + ): Config +} = internal.branded + +/** + * Constructs a config for a sequence of values. + * + * @since 2.0.0 + * @category constructors + */ +export const hashSet: (config: Config, name?: string) => Config> = internal.hashSet + +/** + * Constructs a config for a string value. + * + * @since 2.0.0 + * @category constructors + */ +export const string: (name?: string) => Config = internal.string + +/** + * Constructs a config for a non-empty string value. + * + * @since 3.7.0 + * @category constructors + */ +export const nonEmptyString: (name?: string) => Config = internal.nonEmptyString + +/** + * Constructs a config which contains the specified value. + * + * @since 2.0.0 + * @category constructors + */ +export const succeed: (value: A) => Config = internal.succeed + +/** + * Lazily constructs a config. + * + * @since 2.0.0 + * @category constructors + */ +export const suspend: (config: LazyArg>) => Config = internal.suspend + +/** + * Constructs a config which contains the specified lazy value. + * + * @since 2.0.0 + * @category constructors + */ +export const sync: (value: LazyArg) => Config = internal.sync + +/** + * Constructs a config for a sequence of values. + * + * @since 2.0.0 + * @category constructors + */ +export const hashMap: (config: Config, name?: string) => Config> = internal.hashMap + +/** + * Constructs a config from some configuration wrapped with the `Wrap` utility type. + * + * For example: + * + * ``` + * import { Config, unwrap } from "./Config" + * + * interface Options { key: string } + * + * const makeConfig = (config: Config.Wrap): Config => unwrap(config) + * ``` + * + * @since 2.0.0 + * @category constructors + */ +export const unwrap: (wrapped: Config.Wrap) => Config = internal.unwrap + +/** + * Returns a config that describes the same structure as this one, but which + * performs validation during loading. + * + * @since 2.0.0 + * @category utils + */ +export const validate: { + ( + options: { + readonly message: string + readonly validation: Refinement + } + ): (self: Config) => Config + (options: { + readonly message: string + readonly validation: Predicate + }): (self: Config) => Config + ( + self: Config, + options: { + readonly message: string + readonly validation: Refinement + } + ): Config + (self: Config, options: { + readonly message: string + readonly validation: Predicate + }): Config +} = internal.validate + +/** + * Returns a config that describes the same structure as this one, but has the + * specified default value in case the information cannot be found. + * + * @since 2.0.0 + * @category utils + */ +export const withDefault: { + (def: A2): (self: Config) => Config + (self: Config, def: A2): Config +} = internal.withDefault + +/** + * Adds a description to this configuration, which is intended for humans. + * + * @since 2.0.0 + * @category utils + */ +export const withDescription: { + (description: string): (self: Config) => Config + (self: Config, description: string): Config +} = internal.withDescription + +/** + * Returns a config that is the composition of this config and the specified + * config. + * + * @since 2.0.0 + * @category zipping + */ +export const zip: { + (that: Config): (self: Config) => Config<[A, B]> + (self: Config, that: Config): Config<[A, B]> +} = internal.zip + +/** + * Returns a config that is the composes this config and the specified config + * using the provided function. + * + * @since 2.0.0 + * @category zipping + */ +export const zipWith: { + (that: Config, f: (a: A, b: B) => C): (self: Config) => Config + (self: Config, that: Config, f: (a: A, b: B) => C): Config +} = internal.zipWith diff --git a/repos/effect/packages/effect/src/ConfigError.ts b/repos/effect/packages/effect/src/ConfigError.ts new file mode 100644 index 0000000..df6819a --- /dev/null +++ b/repos/effect/packages/effect/src/ConfigError.ts @@ -0,0 +1,270 @@ +/** + * @since 2.0.0 + */ +import type * as Cause from "./Cause.js" +import * as internal from "./internal/configError.js" + +/** + * @since 2.0.0 + * @category symbols + */ +export const ConfigErrorTypeId: unique symbol = internal.ConfigErrorTypeId + +/** + * @since 2.0.0 + * @category symbols + */ +export type ConfigErrorTypeId = typeof ConfigErrorTypeId + +/** + * The possible ways that loading configuration data may fail. + * + * @since 2.0.0 + * @category models + */ +export type ConfigError = + | And + | Or + | InvalidData + | MissingData + | SourceUnavailable + | Unsupported + +/** + * @since 2.0.0 + */ +export declare namespace ConfigError { + /** + * @since 2.0.0 + * @category models + */ + export interface Proto { + readonly _tag: "ConfigError" + readonly [ConfigErrorTypeId]: ConfigErrorTypeId + } + + /** + * @since 2.0.0 + * @category models + */ + export type Reducer = ConfigErrorReducer +} + +/** + * @since 2.0.0 + * @category models + */ +export interface ConfigErrorReducer { + andCase(context: C, left: Z, right: Z): Z + orCase(context: C, left: Z, right: Z): Z + invalidDataCase(context: C, path: Array, message: string): Z + missingDataCase(context: C, path: Array, message: string): Z + sourceUnavailableCase( + context: C, + path: Array, + message: string, + cause: Cause.Cause + ): Z + unsupportedCase(context: C, path: Array, message: string): Z +} + +/** + * @since 2.0.0 + * @category models + */ +export interface And extends ConfigError.Proto { + readonly _op: "And" + readonly left: ConfigError + readonly right: ConfigError + readonly message: string +} + +/** + * @since 2.0.0 + * @category models + */ +export interface Or extends ConfigError.Proto { + readonly _op: "Or" + readonly left: ConfigError + readonly right: ConfigError + readonly message: string +} + +/** + * @since 2.0.0 + * @category models + */ +export interface InvalidData extends ConfigError.Proto { + readonly _op: "InvalidData" + readonly path: Array + readonly message: string +} + +/** + * @since 2.0.0 + * @category models + */ +export interface MissingData extends ConfigError.Proto { + readonly _op: "MissingData" + readonly path: Array + readonly message: string +} + +/** + * @since 2.0.0 + * @category models + */ +export interface SourceUnavailable extends ConfigError.Proto { + readonly _op: "SourceUnavailable" + readonly path: Array + readonly message: string + readonly cause: Cause.Cause +} + +/** + * @since 2.0.0 + * @category models + */ +export interface Unsupported extends ConfigError.Proto { + readonly _op: "Unsupported" + readonly path: Array + readonly message: string +} + +/** + * @since 2.0.0 + * @category models + */ +export interface Options { + readonly pathDelim: string +} + +/** + * @since 2.0.0 + * @category constructors + */ +export const And: (self: ConfigError, that: ConfigError) => ConfigError = internal.And + +/** + * @since 2.0.0 + * @category constructors + */ +export const Or: (self: ConfigError, that: ConfigError) => ConfigError = internal.Or + +/** + * @since 2.0.0 + * @category constructors + */ +export const MissingData: (path: Array, message: string, options?: Options) => ConfigError = + internal.MissingData + +/** + * @since 2.0.0 + * @category constructors + */ +export const InvalidData: (path: Array, message: string, options?: Options) => ConfigError = + internal.InvalidData + +/** + * @since 2.0.0 + * @category constructors + */ +export const SourceUnavailable: ( + path: Array, + message: string, + cause: Cause.Cause, + options?: Options +) => ConfigError = internal.SourceUnavailable + +/** + * @since 2.0.0 + * @category constructors + */ +export const Unsupported: (path: Array, message: string, options?: Options) => ConfigError = + internal.Unsupported + +/** + * Returns `true` if the specified value is a `ConfigError`, `false` otherwise. + * + * @since 2.0.0 + * @category refinements + */ +export const isConfigError: (u: unknown) => u is ConfigError = internal.isConfigError + +/** + * Returns `true` if the specified `ConfigError` is an `And`, `false` otherwise. + * + * @since 2.0.0 + * @category refinements + */ +export const isAnd: (self: ConfigError) => self is And = internal.isAnd + +/** + * Returns `true` if the specified `ConfigError` is an `Or`, `false` otherwise. + * + * @since 2.0.0 + * @category refinements + */ +export const isOr: (self: ConfigError) => self is Or = internal.isOr + +/** + * Returns `true` if the specified `ConfigError` is an `InvalidData`, `false` + * otherwise. + * + * @since 2.0.0 + * @category refinements + */ +export const isInvalidData: (self: ConfigError) => self is InvalidData = internal.isInvalidData + +/** + * Returns `true` if the specified `ConfigError` is an `MissingData`, `false` + * otherwise. + * + * @since 2.0.0 + * @category refinements + */ +export const isMissingData: (self: ConfigError) => self is MissingData = internal.isMissingData + +/** + * Returns `true` if the specified `ConfigError` contains only `MissingData` errors, `false` otherwise. + * + * @since 2.0.0 + * @categer getters + */ +export const isMissingDataOnly: (self: ConfigError) => boolean = internal.isMissingDataOnly + +/** + * Returns `true` if the specified `ConfigError` is a `SourceUnavailable`, + * `false` otherwise. + * + * @since 2.0.0 + * @category refinements + */ +export const isSourceUnavailable: (self: ConfigError) => self is SourceUnavailable = internal.isSourceUnavailable + +/** + * Returns `true` if the specified `ConfigError` is an `Unsupported`, `false` + * otherwise. + * + * @since 2.0.0 + * @category refinements + */ +export const isUnsupported: (self: ConfigError) => self is Unsupported = internal.isUnsupported + +/** + * @since 2.0.0 + * @category utils + */ +export const prefixed: { + (prefix: Array): (self: ConfigError) => ConfigError + (self: ConfigError, prefix: Array): ConfigError +} = internal.prefixed + +/** + * @since 2.0.0 + * @category folding + */ +export const reduceWithContext: { + (context: C, reducer: ConfigErrorReducer): (self: ConfigError) => Z + (self: ConfigError, context: C, reducer: ConfigErrorReducer): Z +} = internal.reduceWithContext diff --git a/repos/effect/packages/effect/src/ConfigProvider.ts b/repos/effect/packages/effect/src/ConfigProvider.ts new file mode 100644 index 0000000..ccab14b --- /dev/null +++ b/repos/effect/packages/effect/src/ConfigProvider.ts @@ -0,0 +1,333 @@ +/** + * @since 2.0.0 + */ +import type * as Config from "./Config.js" +import type * as ConfigError from "./ConfigError.js" +import type * as PathPatch from "./ConfigProviderPathPatch.js" +import type * as Context from "./Context.js" +import type * as Effect from "./Effect.js" +import type { LazyArg } from "./Function.js" +import type * as HashSet from "./HashSet.js" +import * as internal from "./internal/configProvider.js" +import type { Pipeable } from "./Pipeable.js" + +/** + * @since 2.0.0 + * @category symbols + */ +export const ConfigProviderTypeId: unique symbol = internal.ConfigProviderTypeId + +/** + * @since 2.0.0 + * @category symbols + */ +export type ConfigProviderTypeId = typeof ConfigProviderTypeId + +/** + * @since 2.0.0 + * @category symbols + */ +export const FlatConfigProviderTypeId: unique symbol = internal.FlatConfigProviderTypeId + +/** + * @since 2.0.0 + * @category symbols + */ +export type FlatConfigProviderTypeId = typeof FlatConfigProviderTypeId + +/** + * A ConfigProvider is a service that provides configuration given a description + * of the structure of that configuration. + * + * @since 2.0.0 + * @category models + */ +export interface ConfigProvider extends ConfigProvider.Proto, Pipeable { + /** + * Loads the specified configuration, or fails with a config error. + */ + load(config: Config.Config): Effect.Effect + /** + * Flattens this config provider into a simplified config provider that knows + * only how to deal with flat (key/value) properties. + */ + readonly flattened: ConfigProvider.Flat +} + +/** + * @since 2.0.0 + */ +export declare namespace ConfigProvider { + /** + * @since 2.0.0 + * @category models + */ + export interface Proto { + readonly [ConfigProviderTypeId]: ConfigProviderTypeId + } + + /** + * A simplified config provider that knows only how to deal with flat + * (key/value) properties. Because these providers are common, there is + * special support for implementing them. + * + * @since 2.0.0 + * @category models + */ + export interface Flat { + readonly [FlatConfigProviderTypeId]: FlatConfigProviderTypeId + readonly patch: PathPatch.PathPatch + load( + path: ReadonlyArray, + config: Config.Config.Primitive, + split?: boolean + ): Effect.Effect, ConfigError.ConfigError> + enumerateChildren( + path: ReadonlyArray + ): Effect.Effect, ConfigError.ConfigError> + } + + /** + * @since 2.0.0 + * @category models + */ + export interface FromMapConfig { + readonly pathDelim: string + readonly seqDelim: string + } + + /** + * @since 2.0.0 + * @category models + */ + export interface FromEnvConfig { + readonly pathDelim: string + readonly seqDelim: string + } + + /** + * @since 1.0.0 + * @category models + */ + export type KeyComponent = KeyName | KeyIndex + + /** + * @since 1.0.0 + * @category models + */ + export interface KeyName { + readonly _tag: "KeyName" + readonly name: string + } + + /** + * @since 1.0.0 + * @category models + */ + export interface KeyIndex { + readonly _tag: "KeyIndex" + readonly index: number + } +} + +/** + * The service tag for `ConfigProvider`. + * + * @since 2.0.0 + * @category context + */ +export const ConfigProvider: Context.Tag = internal.configProviderTag + +/** + * Creates a new config provider. + * + * @since 2.0.0 + * @category constructors + */ +export const make: ( + options: { + readonly load: (config: Config.Config) => Effect.Effect + readonly flattened: ConfigProvider.Flat + } +) => ConfigProvider = internal.make + +/** + * Creates a new flat config provider. + * + * @since 2.0.0 + * @category constructors + */ +export const makeFlat: (options: { + readonly load: ( + path: ReadonlyArray, + config: Config.Config.Primitive, + split: boolean + ) => Effect.Effect, ConfigError.ConfigError> + readonly enumerateChildren: ( + path: ReadonlyArray + ) => Effect.Effect, ConfigError.ConfigError> + readonly patch: PathPatch.PathPatch +}) => ConfigProvider.Flat = internal.makeFlat + +/** + * A config provider that loads configuration from context variables + * + * **Options**: + * + * - `pathDelim`: The delimiter for the path segments (default: `"_"`). + * - `seqDelim`: The delimiter for the sequence of values (default: `","`). + * + * @since 2.0.0 + * @category constructors + */ +export const fromEnv: (options?: Partial) => ConfigProvider = internal.fromEnv + +/** + * Constructs a new `ConfigProvider` from a key/value (flat) provider, where + * nesting is embedded into the string keys. + * + * @since 2.0.0 + * @category constructors + */ +export const fromFlat: (flat: ConfigProvider.Flat) => ConfigProvider = internal.fromFlat + +/** + * Constructs a new `ConfigProvider` from a JSON object. + * + * @since 2.0.0 + * @category constructors + */ +export const fromJson: (json: unknown) => ConfigProvider = internal.fromJson + +// TODO(4.0): use `_` for nested configs instead of `.` in next major +/** + * Constructs a ConfigProvider using a map and the specified delimiter string, + * which determines how to split the keys in the map into path segments. + * + * @since 2.0.0 + * @category constructors + */ +export const fromMap: (map: Map, config?: Partial) => ConfigProvider = + internal.fromMap + +/** + * Returns a new config provider that will automatically convert all property + * names to constant case. This can be utilized to adapt the names of + * configuration properties from the default naming convention of camel case + * to the naming convention of a config provider. + * + * @since 2.0.0 + * @category combinators + */ +export const constantCase: (self: ConfigProvider) => ConfigProvider = internal.constantCase + +/** + * Returns a new config provider that will automatically tranform all path + * configuration names with the specified function. This can be utilized to + * adapt the names of configuration properties from one naming convention to + * another. + * + * @since 2.0.0 + * @category utils + */ +export const mapInputPath: { + (f: (path: string) => string): (self: ConfigProvider) => ConfigProvider + (self: ConfigProvider, f: (path: string) => string): ConfigProvider +} = internal.mapInputPath + +/** + * Returns a new config provider that will automatically convert all property + * names to kebab case. This can be utilized to adapt the names of + * configuration properties from the default naming convention of camel case + * to the naming convention of a config provider. + * + * @since 2.0.0 + * @category combinators + */ +export const kebabCase: (self: ConfigProvider) => ConfigProvider = internal.kebabCase + +/** + * Returns a new config provider that will automatically convert all property + * names to lower case. This can be utilized to adapt the names of + * configuration properties from the default naming convention of camel case + * to the naming convention of a config provider. + * + * @since 2.0.0 + * @category combinators + */ +export const lowerCase: (self: ConfigProvider) => ConfigProvider = internal.lowerCase + +/** + * Returns a new config provider that will automatically nest all + * configuration under the specified property name. This can be utilized to + * aggregate separate configuration sources that are all required to load a + * single configuration value. + * + * @since 2.0.0 + * @category utils + */ +export const nested: { + (name: string): (self: ConfigProvider) => ConfigProvider + (self: ConfigProvider, name: string): ConfigProvider +} = internal.nested + +/** + * Returns a new config provider that preferentially loads configuration data + * from this one, but which will fall back to the specified alternate provider + * if there are any issues loading the configuration from this provider. + * + * @since 2.0.0 + * @category utils + */ +export const orElse: { + (that: LazyArg): (self: ConfigProvider) => ConfigProvider + (self: ConfigProvider, that: LazyArg): ConfigProvider +} = internal.orElse + +/** + * Returns a new config provider that will automatically un-nest all + * configuration under the specified property name. This can be utilized to + * de-aggregate separate configuration sources that are all required to load a + * single configuration value. + * + * @since 2.0.0 + * @category utils + */ +export const unnested: { + (name: string): (self: ConfigProvider) => ConfigProvider + (self: ConfigProvider, name: string): ConfigProvider +} = internal.unnested + +/** + * Returns a new config provider that will automatically convert all property + * names to upper case. This can be utilized to adapt the names of + * configuration properties from the default naming convention of camel case + * to the naming convention of a config provider. + * + * @since 2.0.0 + * @category combinators + */ +export const snakeCase: (self: ConfigProvider) => ConfigProvider = internal.snakeCase + +/** + * Returns a new config provider that will automatically convert all property + * names to upper case. This can be utilized to adapt the names of + * configuration properties from the default naming convention of camel case + * to the naming convention of a config provider. + * + * @since 2.0.0 + * @category combinators + */ +export const upperCase: (self: ConfigProvider) => ConfigProvider = internal.upperCase + +/** + * Returns a new config provider that transforms the config provider with the + * specified function within the specified path. + * + * @since 2.0.0 + * @category combinators + */ +export const within: { + (path: ReadonlyArray, f: (self: ConfigProvider) => ConfigProvider): (self: ConfigProvider) => ConfigProvider + (self: ConfigProvider, path: ReadonlyArray, f: (self: ConfigProvider) => ConfigProvider): ConfigProvider +} = internal.within diff --git a/repos/effect/packages/effect/src/ConfigProviderPathPatch.ts b/repos/effect/packages/effect/src/ConfigProviderPathPatch.ts new file mode 100644 index 0000000..f0b60f3 --- /dev/null +++ b/repos/effect/packages/effect/src/ConfigProviderPathPatch.ts @@ -0,0 +1,100 @@ +/** + * @since 2.0.0 + */ +import * as internal from "./internal/configProvider/pathPatch.js" + +/** + * Represents a description of how to modify the path to a configuration + * value. + * + * @since 2.0.0 + * @category models + */ +export type PathPatch = Empty | AndThen | MapName | Nested | Unnested + +/** + * @since 2.0.0 + * @category models + */ +export interface Empty { + readonly _tag: "Empty" +} + +/** + * @since 2.0.0 + * @category models + */ +export interface AndThen { + readonly _tag: "AndThen" + readonly first: PathPatch + readonly second: PathPatch +} + +/** + * @since 2.0.0 + * @category models + */ +export interface MapName { + readonly _tag: "MapName" + f(string: string): string +} + +/** + * @since 2.0.0 + * @category models + */ +export interface Nested { + readonly _tag: "Nested" + readonly name: string +} + +/** + * @since 2.0.0 + * @category models + */ +export interface Unnested { + readonly _tag: "Unnested" + readonly name: string +} + +/** + * @since 2.0.0 + * @category constructors + */ +export const empty: PathPatch = internal.empty + +/** + * @since 2.0.0 + * @category constructors + */ +export const andThen: { + (that: PathPatch): (self: PathPatch) => PathPatch + (self: PathPatch, that: PathPatch): PathPatch +} = internal.andThen + +/** + * @since 2.0.0 + * @category constructors + */ +export const mapName: { + (f: (string: string) => string): (self: PathPatch) => PathPatch + (self: PathPatch, f: (string: string) => string): PathPatch +} = internal.mapName + +/** + * @since 2.0.0 + * @category constructors + */ +export const nested: { + (name: string): (self: PathPatch) => PathPatch + (self: PathPatch, name: string): PathPatch +} = internal.nested + +/** + * @since 2.0.0 + * @category constructors + */ +export const unnested: { + (name: string): (self: PathPatch) => PathPatch + (self: PathPatch, name: string): PathPatch +} = internal.unnested diff --git a/repos/effect/packages/effect/src/Console.ts b/repos/effect/packages/effect/src/Console.ts new file mode 100644 index 0000000..82ac78b --- /dev/null +++ b/repos/effect/packages/effect/src/Console.ts @@ -0,0 +1,226 @@ +/** + * @since 2.0.0 + */ +import type * as Context from "./Context.js" +import type { Effect } from "./Effect.js" +import * as internal from "./internal/console.js" +import * as defaultConsole from "./internal/defaultServices/console.js" +import type * as Layer from "./Layer.js" +import type { Scope } from "./Scope.js" + +/** + * @since 2.0.0 + * @category type ids + */ +export const TypeId: unique symbol = defaultConsole.TypeId + +/** + * @since 2.0.0 + * @category type ids + */ +export type TypeId = typeof TypeId + +/** + * @since 2.0.0 + * @category model + */ +export interface Console { + readonly [TypeId]: TypeId + assert(condition: boolean, ...args: ReadonlyArray): Effect + readonly clear: Effect + count(label?: string): Effect + countReset(label?: string): Effect + debug(...args: ReadonlyArray): Effect + dir(item: any, options?: any): Effect + dirxml(...args: ReadonlyArray): Effect + error(...args: ReadonlyArray): Effect + group(options?: { + readonly label?: string | undefined + readonly collapsed?: boolean | undefined + }): Effect + readonly groupEnd: Effect + info(...args: ReadonlyArray): Effect + log(...args: ReadonlyArray): Effect + table(tabularData: any, properties?: ReadonlyArray): Effect + time(label?: string): Effect + timeEnd(label?: string): Effect + timeLog(label?: string, ...args: ReadonlyArray): Effect + trace(...args: ReadonlyArray): Effect + warn(...args: ReadonlyArray): Effect + readonly unsafe: UnsafeConsole +} + +/** + * @since 2.0.0 + * @category model + */ +export interface UnsafeConsole { + assert(condition: boolean, ...args: ReadonlyArray): void + clear(): void + count(label?: string): void + countReset(label?: string): void + debug(...args: ReadonlyArray): void + dir(item: any, options?: any): void + dirxml(...args: ReadonlyArray): void + error(...args: ReadonlyArray): void + group(...args: ReadonlyArray): void + groupCollapsed(...args: ReadonlyArray): void + groupEnd(): void + info(...args: ReadonlyArray): void + log(...args: ReadonlyArray): void + table(tabularData: any, properties?: ReadonlyArray): void + time(label?: string): void + timeEnd(label?: string): void + timeLog(label?: string, ...args: ReadonlyArray): void + trace(...args: ReadonlyArray): void + warn(...args: ReadonlyArray): void +} + +/** + * @since 2.0.0 + * @category context + */ +export const Console: Context.Tag = defaultConsole.consoleTag + +/** + * @since 2.0.0 + * @category default services + */ +export const withConsole: { + (console: C): (effect: Effect) => Effect + (effect: Effect, console: C): Effect +} = internal.withConsole + +/** + * @since 2.0.0 + * @category default services + */ +export const setConsole: (console: A) => Layer.Layer = internal.setConsole + +/** + * @since 2.0.0 + * @category accessor + */ +export const consoleWith: (f: (console: Console) => Effect) => Effect = internal.consoleWith + +/** + * @since 2.0.0 + * @category accessor + */ +export const assert: (condition: boolean, ...args: ReadonlyArray) => Effect = internal.assert + +/** + * @since 2.0.0 + * @category accessor + */ +export const clear: Effect = internal.clear + +/** + * @since 2.0.0 + * @category accessor + */ +export const count: (label?: string) => Effect = internal.count + +/** + * @since 2.0.0 + * @category accessor + */ +export const countReset: (label?: string) => Effect = internal.countReset + +/** + * @since 2.0.0 + * @category accessor + */ +export const debug: (...args: ReadonlyArray) => Effect = internal.debug + +/** + * @since 2.0.0 + * @category accessor + */ +export const dir: (item: any, options?: any) => Effect = internal.dir + +/** + * @since 2.0.0 + * @category accessor + */ +export const dirxml: (...args: ReadonlyArray) => Effect = internal.dirxml + +/** + * @since 2.0.0 + * @category accessor + */ +export const error: (...args: ReadonlyArray) => Effect = internal.error + +/** + * @since 2.0.0 + * @category accessor + */ +export const group: ( + options?: { label?: string | undefined; collapsed?: boolean | undefined } | undefined +) => Effect = internal.group + +/** + * @since 2.0.0 + * @category accessor + */ +export const info: (...args: ReadonlyArray) => Effect = internal.info + +/** + * @since 2.0.0 + * @category accessor + */ +export const log: (...args: ReadonlyArray) => Effect = internal.log + +/** + * @since 2.0.0 + * @category accessor + */ +export const table: (tabularData: any, properties?: ReadonlyArray) => Effect = internal.table + +/** + * @since 2.0.0 + * @category accessor + */ +export const time: (label?: string | undefined) => Effect = internal.time + +/** + * @since 2.0.0 + * @category accessor + */ +export const timeLog: (label?: string, ...args: ReadonlyArray) => Effect = internal.timeLog + +/** + * @since 2.0.0 + * @category accessor + */ +export const trace: (...args: ReadonlyArray) => Effect = internal.trace + +/** + * @since 2.0.0 + * @category accessor + */ +export const warn: (...args: ReadonlyArray) => Effect = internal.warn + +/** + * @since 2.0.0 + * @category accessor + */ +export const withGroup: { + (options?: { + readonly label?: string | undefined + readonly collapsed?: boolean | undefined + }): (self: Effect) => Effect + (self: Effect, options?: { + readonly label?: string | undefined + readonly collapsed?: boolean | undefined + }): Effect +} = internal.withGroup + +/** + * @since 2.0.0 + * @category accessor + */ +export const withTime: { + (label?: string): (self: Effect) => Effect + (self: Effect, label?: string): Effect +} = internal.withTime diff --git a/repos/effect/packages/effect/src/Context.ts b/repos/effect/packages/effect/src/Context.ts new file mode 100644 index 0000000..4c2bf95 --- /dev/null +++ b/repos/effect/packages/effect/src/Context.ts @@ -0,0 +1,585 @@ +/** + * This module provides a data structure called `Context` that can be used for dependency injection in effectful + * programs. It is essentially a table mapping `Tag`s to their implementations (called `Service`s), and can be used to + * manage dependencies in a type-safe way. The `Context` data structure is essentially a way of providing access to a set + * of related services that can be passed around as a single unit. This module provides functions to create, modify, and + * query the contents of a `Context`, as well as a number of utility types for working with tags and services. + * + * @since 2.0.0 + */ +import type * as Effect from "./Effect.js" +import type { Equal } from "./Equal.js" +import type { LazyArg } from "./Function.js" +import type { Inspectable } from "./Inspectable.js" +import * as internal from "./internal/context.js" +import type { Option } from "./Option.js" +import type { Pipeable } from "./Pipeable.js" +import type * as Types from "./Types.js" +import type * as Unify from "./Unify.js" + +/** + * @since 2.0.0 + * @category symbol + */ +export const TagTypeId: unique symbol = internal.TagTypeId + +/** + * @since 2.0.0 + * @category symbol + */ +export type TagTypeId = typeof TagTypeId + +/** + * @since 3.5.9 + * @category models + */ +export interface Tag extends Pipeable, Inspectable, ReadonlyTag { + readonly _op: "Tag" + readonly Service: Value + readonly Identifier: Id + readonly [TagTypeId]: { + readonly _Service: Types.Invariant + readonly _Identifier: Types.Invariant + } + of(self: Value): Value + context(self: Value): Context + readonly stack?: string | undefined + readonly key: string + [Unify.typeSymbol]?: unknown + [Unify.unifySymbol]?: TagUnify + [Unify.ignoreSymbol]?: TagUnifyIgnore +} + +/** + * @since 3.5.9 + * @category models + */ +export interface ReadonlyTag extends Pipeable, Inspectable, Effect.Effect { + readonly _op: "Tag" + readonly Service: Value + readonly Identifier: Id + readonly [TagTypeId]: { + readonly _Service: Types.Covariant + readonly _Identifier: Types.Invariant + } + readonly stack?: string | undefined + readonly key: string +} + +/** + * @since 3.11.0 + * @category symbol + */ +export const ReferenceTypeId: unique symbol = internal.ReferenceTypeId + +/** + * @since 3.11.0 + * @category symbol + */ +export type ReferenceTypeId = typeof ReferenceTypeId + +/** + * @since 3.11.0 + * @category models + */ +export interface Reference extends Pipeable, Inspectable { + readonly [ReferenceTypeId]: ReferenceTypeId + readonly defaultValue: () => Value + + readonly _op: "Tag" + readonly Service: Value + readonly Identifier: Id + readonly [TagTypeId]: { + readonly _Service: Types.Invariant + readonly _Identifier: Types.Invariant + } + of(self: Value): Value + context(self: Value): Context + readonly stack?: string | undefined + readonly key: string + [Unify.typeSymbol]?: unknown + [Unify.unifySymbol]?: TagUnify + [Unify.ignoreSymbol]?: TagUnifyIgnore +} + +/** + * @since 2.0.0 + * @category models + */ +export interface TagClassShape { + readonly [TagTypeId]: TagTypeId + readonly Type: Shape + readonly Id: Id +} + +// TODO(4.0): move key narrowing to the Tag interface +/** + * @since 2.0.0 + * @category models + */ +export interface TagClass extends Tag { + new(_: never): TagClassShape + readonly key: Id +} + +// TODO(4.0): move key narrowing to the Reference interface +/** + * @since 3.11.0 + * @category models + */ +export interface ReferenceClass extends Reference { + new(_: never): TagClassShape + readonly key: Id +} + +/** + * @category models + * @since 2.0.0 + */ +export interface TagUnify { + Tag?: () => Extract> +} + +/** + * @category models + * @since 2.0.0 + */ +export interface TagUnifyIgnore {} + +/** + * @since 2.0.0 + */ +export declare namespace Tag { + /** + * @since 2.0.0 + */ + export type Service | TagClassShape> = T extends Tag ? T["Service"] + : T extends TagClassShape ? A + : never + /** + * @since 2.0.0 + */ + export type Identifier | TagClassShape> = T extends Tag ? T["Identifier"] + : T extends TagClassShape ? T + : never +} + +/** + * Creates a new `Tag` instance with an optional key parameter. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { Context } from "effect" + * + * assert.strictEqual(Context.GenericTag("PORT").key === Context.GenericTag("PORT").key, true) + * ``` + * + * @since 2.0.0 + * @category constructors + */ +export const GenericTag: (key: string) => Tag = + internal.makeGenericTag + +const TypeId: unique symbol = internal.TypeId as TypeId + +/** + * @since 2.0.0 + * @category symbol + */ +export type TypeId = typeof TypeId + +/** + * @since 2.0.0 + * @category models + */ +export type ValidTagsById = R extends infer S ? Tag : never + +/** + * @since 2.0.0 + * @category models + */ +export interface Context extends Equal, Pipeable, Inspectable { + readonly [TypeId]: { + readonly _Services: Types.Contravariant + } + readonly unsafeMap: Map +} + +/** + * @since 2.0.0 + * @category constructors + */ +export const unsafeMake: (unsafeMap: Map) => Context = internal.makeContext + +/** + * Checks if the provided argument is a `Context`. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { Context } from "effect" + * + * assert.strictEqual(Context.isContext(Context.empty()), true) + * ``` + * + * @since 2.0.0 + * @category guards + */ +export const isContext: (input: unknown) => input is Context = internal.isContext + +/** + * Checks if the provided argument is a `Tag`. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { Context } from "effect" + * + * assert.strictEqual(Context.isTag(Context.GenericTag("Tag")), true) + * ``` + * + * @since 2.0.0 + * @category guards + */ +export const isTag: (input: unknown) => input is Tag = internal.isTag + +/** + * Checks if the provided argument is a `Reference`. + * + * @since 3.11.0 + * @category guards + * @experimental + */ +export const isReference: (u: unknown) => u is Reference = internal.isReference + +/** + * Returns an empty `Context`. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { Context } from "effect" + * + * assert.strictEqual(Context.isContext(Context.empty()), true) + * ``` + * + * @since 2.0.0 + * @category constructors + */ +export const empty: () => Context = internal.empty + +/** + * Creates a new `Context` with a single service associated to the tag. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { Context } from "effect" + * + * const Port = Context.GenericTag<{ PORT: number }>("Port") + * + * const Services = Context.make(Port, { PORT: 8080 }) + * + * assert.deepStrictEqual(Context.get(Services, Port), { PORT: 8080 }) + * ``` + * + * @since 2.0.0 + * @category constructors + */ +export const make: (tag: Tag, service: Types.NoInfer) => Context = internal.make + +/** + * Adds a service to a given `Context`. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { Context, pipe } from "effect" + * + * const Port = Context.GenericTag<{ PORT: number }>("Port") + * const Timeout = Context.GenericTag<{ TIMEOUT: number }>("Timeout") + * + * const someContext = Context.make(Port, { PORT: 8080 }) + * + * const Services = pipe( + * someContext, + * Context.add(Timeout, { TIMEOUT: 5000 }) + * ) + * + * assert.deepStrictEqual(Context.get(Services, Port), { PORT: 8080 }) + * assert.deepStrictEqual(Context.get(Services, Timeout), { TIMEOUT: 5000 }) + * ``` + * + * @since 2.0.0 + */ +export const add: { + (tag: Tag, service: Types.NoInfer): (self: Context) => Context + (self: Context, tag: Tag, service: Types.NoInfer): Context +} = internal.add + +/** + * Get a service from the context that corresponds to the given tag. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { pipe, Context } from "effect" + * + * const Port = Context.GenericTag<{ PORT: number }>("Port") + * const Timeout = Context.GenericTag<{ TIMEOUT: number }>("Timeout") + * + * const Services = pipe( + * Context.make(Port, { PORT: 8080 }), + * Context.add(Timeout, { TIMEOUT: 5000 }) + * ) + * + * assert.deepStrictEqual(Context.get(Services, Timeout), { TIMEOUT: 5000 }) + * ``` + * + * @since 2.0.0 + * @category getters + */ +export const get: { + (tag: Reference): (self: Context) => S + (tag: Tag): (self: Context) => S + (self: Context, tag: Reference): S + (self: Context, tag: Tag): S +} = internal.get + +/** + * Get a service from the context that corresponds to the given tag, or + * use the fallback value. + * + * @since 3.7.0 + * @category getters + */ +export const getOrElse: { + (tag: Tag, orElse: LazyArg): (self: Context) => S | B + (self: Context, tag: Tag, orElse: LazyArg): S | B +} = internal.getOrElse + +/** + * Get a service from the context that corresponds to the given tag. + * This function is unsafe because if the tag is not present in the context, a runtime error will be thrown. + * + * For a safer version see {@link getOption}. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { Context } from "effect" + * + * const Port = Context.GenericTag<{ PORT: number }>("Port") + * const Timeout = Context.GenericTag<{ TIMEOUT: number }>("Timeout") + * + * const Services = Context.make(Port, { PORT: 8080 }) + * + * assert.deepStrictEqual(Context.unsafeGet(Services, Port), { PORT: 8080 }) + * assert.throws(() => Context.unsafeGet(Services, Timeout)) + * ``` + * + * @since 2.0.0 + * @category unsafe + */ +export const unsafeGet: { + (tag: Tag): (self: Context) => S + (self: Context, tag: Tag): S +} = internal.unsafeGet + +/** + * Get the value associated with the specified tag from the context wrapped in an `Option` object. If the tag is not + * found, the `Option` object will be `None`. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { Context, Option } from "effect" + * + * const Port = Context.GenericTag<{ PORT: number }>("Port") + * const Timeout = Context.GenericTag<{ TIMEOUT: number }>("Timeout") + * + * const Services = Context.make(Port, { PORT: 8080 }) + * + * assert.deepStrictEqual(Context.getOption(Services, Port), Option.some({ PORT: 8080 })) + * assert.deepStrictEqual(Context.getOption(Services, Timeout), Option.none()) + * ``` + * + * @since 2.0.0 + * @category getters + */ +export const getOption: { + (tag: Tag): (self: Context) => Option + (self: Context, tag: Tag): Option +} = internal.getOption + +/** + * Merges two `Context`s, returning a new `Context` containing the services of both. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { Context } from "effect" + * + * const Port = Context.GenericTag<{ PORT: number }>("Port") + * const Timeout = Context.GenericTag<{ TIMEOUT: number }>("Timeout") + * + * const firstContext = Context.make(Port, { PORT: 8080 }) + * const secondContext = Context.make(Timeout, { TIMEOUT: 5000 }) + * + * const Services = Context.merge(firstContext, secondContext) + * + * assert.deepStrictEqual(Context.get(Services, Port), { PORT: 8080 }) + * assert.deepStrictEqual(Context.get(Services, Timeout), { TIMEOUT: 5000 }) + * ``` + * + * @since 2.0.0 + */ +export const merge: { + (that: Context): (self: Context) => Context + (self: Context, that: Context): Context +} = internal.merge + +/** + * Merges any number of `Context`s, returning a new `Context` containing the services of all. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { Context } from "effect" + * + * const Port = Context.GenericTag<{ PORT: number }>("Port") + * const Timeout = Context.GenericTag<{ TIMEOUT: number }>("Timeout") + * const Host = Context.GenericTag<{ HOST: string }>("Host") + * + * const firstContext = Context.make(Port, { PORT: 8080 }) + * const secondContext = Context.make(Timeout, { TIMEOUT: 5000 }) + * const thirdContext = Context.make(Host, { HOST: "localhost" }) + * + * const Services = Context.mergeAll(firstContext, secondContext, thirdContext) + * + * assert.deepStrictEqual(Context.get(Services, Port), { PORT: 8080 }) + * assert.deepStrictEqual(Context.get(Services, Timeout), { TIMEOUT: 5000 }) + * assert.deepStrictEqual(Context.get(Services, Host), { HOST: "localhost" }) + * ``` + * + * @since 3.12.0 + */ +export const mergeAll: >( + ...ctxs: [...{ [K in keyof T]: Context }] +) => Context = internal.mergeAll + +/** + * Returns a new `Context` that contains only the specified services. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { pipe, Context, Option } from "effect" + * + * const Port = Context.GenericTag<{ PORT: number }>("Port") + * const Timeout = Context.GenericTag<{ TIMEOUT: number }>("Timeout") + * + * const someContext = pipe( + * Context.make(Port, { PORT: 8080 }), + * Context.add(Timeout, { TIMEOUT: 5000 }) + * ) + * + * const Services = pipe(someContext, Context.pick(Port)) + * + * assert.deepStrictEqual(Context.getOption(Services, Port), Option.some({ PORT: 8080 })) + * assert.deepStrictEqual(Context.getOption(Services, Timeout), Option.none()) + * ``` + * + * @since 2.0.0 + */ +export const pick: >>( + ...tags: Tags +) => (self: Context) => Context> = internal.pick + +/** + * @since 2.0.0 + */ +export const omit: >>( + ...tags: Tags +) => (self: Context) => Context>> = internal.omit + +/** + * @example + * ```ts + * import * as assert from "node:assert" + * import { Context, Layer } from "effect" + * + * class MyTag extends Context.Tag("MyTag")< + * MyTag, + * { readonly myNum: number } + * >() { + * static Live = Layer.succeed(this, { myNum: 108 }) + * } + * ``` + * + * @since 2.0.0 + * @category constructors + */ +export const Tag: (id: Id) => () => TagClass = internal.Tag + +/** + * Creates a context tag with a default value. + * + * **Details** + * + * `Context.Reference` allows you to create a tag that can hold a value. You can + * provide a default value for the service, which will automatically be used + * when the context is accessed, or override it with a custom implementation + * when needed. + * + * **Example** (Declaring a Tag with a default value) + * + * ```ts + * import * as assert from "node:assert" + * import { Context, Effect } from "effect" + * + * class SpecialNumber extends Context.Reference()( + * "SpecialNumber", + * { defaultValue: () => 2048 } + * ) {} + * + * // ┌─── Effect + * // ▼ + * const program = Effect.gen(function* () { + * const specialNumber = yield* SpecialNumber + * console.log(`The special number is ${specialNumber}`) + * }) + * + * // No need to provide the SpecialNumber implementation + * Effect.runPromise(program) + * // Output: The special number is 2048 + * ``` + * + * **Example** (Overriding the default value) + * + * ```ts + * import { Context, Effect } from "effect" + * + * class SpecialNumber extends Context.Reference()( + * "SpecialNumber", + * { defaultValue: () => 2048 } + * ) {} + * + * const program = Effect.gen(function* () { + * const specialNumber = yield* SpecialNumber + * console.log(`The special number is ${specialNumber}`) + * }) + * + * Effect.runPromise(program.pipe(Effect.provideService(SpecialNumber, -1))) + * // Output: The special number is -1 + * ``` + * + * @since 3.11.0 + * @category constructors + * @experimental + */ +export const Reference: () => ( + id: Id, + options: { readonly defaultValue: () => Service } +) => ReferenceClass = internal.Reference diff --git a/repos/effect/packages/effect/src/Cron.ts b/repos/effect/packages/effect/src/Cron.ts new file mode 100644 index 0000000..43b16e9 --- /dev/null +++ b/repos/effect/packages/effect/src/Cron.ts @@ -0,0 +1,836 @@ +/** + * @since 2.0.0 + */ +import * as Arr from "./Array.js" +import * as Data from "./Data.js" +import type * as DateTime from "./DateTime.js" +import * as Either from "./Either.js" +import * as Equal from "./Equal.js" +import * as equivalence from "./Equivalence.js" +import { constVoid, dual, identity, pipe } from "./Function.js" +import * as Hash from "./Hash.js" +import { format, type Inspectable, NodeInspectSymbol } from "./Inspectable.js" +import * as dateTime from "./internal/dateTime.js" +import * as N from "./Number.js" +import * as Option from "./Option.js" +import { type Pipeable, pipeArguments } from "./Pipeable.js" +import { hasProperty } from "./Predicate.js" +import * as String from "./String.js" +import type { Mutable } from "./Types.js" + +/** + * @since 2.0.0 + * @category symbols + */ +export const TypeId: unique symbol = Symbol.for("effect/Cron") + +/** + * @since 2.0.0 + * @category symbol + */ +export type TypeId = typeof TypeId + +/** + * @since 2.0.0 + * @category models + */ +export interface Cron extends Pipeable, Equal.Equal, Inspectable { + readonly [TypeId]: TypeId + readonly tz: Option.Option + readonly seconds: ReadonlySet + readonly minutes: ReadonlySet + readonly hours: ReadonlySet + readonly days: ReadonlySet + readonly months: ReadonlySet + readonly weekdays: ReadonlySet + /** @internal */ + readonly first: { + readonly second: number + readonly minute: number + readonly hour: number + readonly day: number + readonly month: number + readonly weekday: number + } + /** @internal */ + readonly last: { + readonly second: number + readonly minute: number + readonly hour: number + readonly day: number + readonly month: number + readonly weekday: number + } + /** @internal */ + readonly next: { + readonly second: ReadonlyArray + readonly minute: ReadonlyArray + readonly hour: ReadonlyArray + readonly day: ReadonlyArray + readonly month: ReadonlyArray + readonly weekday: ReadonlyArray + } + /** @internal */ + readonly prev: { + readonly second: ReadonlyArray + readonly minute: ReadonlyArray + readonly hour: ReadonlyArray + readonly day: ReadonlyArray + readonly month: ReadonlyArray + readonly weekday: ReadonlyArray + } +} + +const CronProto = { + [TypeId]: TypeId, + [Equal.symbol](this: Cron, that: unknown) { + return isCron(that) && equals(this, that) + }, + [Hash.symbol](this: Cron): number { + return pipe( + Hash.hash(this.tz), + Hash.combine(Hash.array(Arr.fromIterable(this.seconds))), + Hash.combine(Hash.array(Arr.fromIterable(this.minutes))), + Hash.combine(Hash.array(Arr.fromIterable(this.hours))), + Hash.combine(Hash.array(Arr.fromIterable(this.days))), + Hash.combine(Hash.array(Arr.fromIterable(this.months))), + Hash.combine(Hash.array(Arr.fromIterable(this.weekdays))), + Hash.cached(this) + ) + }, + toString(this: Cron) { + return format(this.toJSON()) + }, + toJSON(this: Cron) { + return { + _id: "Cron", + tz: this.tz, + seconds: Arr.fromIterable(this.seconds), + minutes: Arr.fromIterable(this.minutes), + hours: Arr.fromIterable(this.hours), + days: Arr.fromIterable(this.days), + months: Arr.fromIterable(this.months), + weekdays: Arr.fromIterable(this.weekdays) + } + }, + [NodeInspectSymbol](this: Cron) { + return this.toJSON() + }, + pipe() { + return pipeArguments(this, arguments) + } +} + +/** + * Checks if a given value is a `Cron` instance. + * + * @since 2.0.0 + * @category guards + */ +export const isCron = (u: unknown): u is Cron => hasProperty(u, TypeId) + +/** + * Creates a `Cron` instance. + * + * @since 2.0.0 + * @category constructors + */ +export const make = (values: { + readonly seconds?: Iterable | undefined + readonly minutes: Iterable + readonly hours: Iterable + readonly days: Iterable + readonly months: Iterable + readonly weekdays: Iterable + readonly tz?: DateTime.TimeZone | undefined +}): Cron => { + const o: Mutable = Object.create(CronProto) + o.seconds = new Set(Arr.sort(values.seconds ?? [0], N.Order)) + o.minutes = new Set(Arr.sort(values.minutes, N.Order)) + o.hours = new Set(Arr.sort(values.hours, N.Order)) + o.days = new Set(Arr.sort(values.days, N.Order)) + o.months = new Set(Arr.sort(values.months, N.Order)) + o.weekdays = new Set(Arr.sort(values.weekdays, N.Order)) + o.tz = Option.fromNullable(values.tz) + + const seconds = Array.from(o.seconds) + const minutes = Array.from(o.minutes) + const hours = Array.from(o.hours) + const days = Array.from(o.days) + const months = Array.from(o.months) + const weekdays = Array.from(o.weekdays) + + o.first = { + second: seconds[0] ?? 0, + minute: minutes[0] ?? 0, + hour: hours[0] ?? 0, + day: days[0] ?? 1, + month: (months[0] ?? 1) - 1, + weekday: weekdays[0] ?? 0 + } + + o.last = { + second: seconds[seconds.length - 1] ?? 59, + minute: minutes[minutes.length - 1] ?? 59, + hour: hours[hours.length - 1] ?? 23, + day: days[days.length - 1] ?? 31, + month: (months[months.length - 1] ?? 12) - 1, + weekday: weekdays[weekdays.length - 1] ?? 6 + } + + o.next = { + second: lookupTable(seconds, 60, "next"), + minute: lookupTable(minutes, 60, "next"), + hour: lookupTable(hours, 24, "next"), + day: lookupTable(days, 32, "next"), + month: lookupTable(months, 13, "next"), + weekday: lookupTable(weekdays, 7, "next") + } + + o.prev = { + second: lookupTable(seconds, 60, "prev"), + minute: lookupTable(minutes, 60, "prev"), + hour: lookupTable(hours, 24, "prev"), + day: lookupTable(days, 32, "prev"), + month: lookupTable(months, 13, "prev"), + weekday: lookupTable(weekdays, 7, "prev") + } + + return o +} + +const lookupTable = ( + values: ReadonlyArray, + size: number, + dir: "next" | "prev" +): Array => { + const result = new Array(size).fill(undefined) + if (values.length === 0) { + return result + } + + let current: number | undefined = undefined + + if (dir === "next") { + let index = values.length - 1 + for (let i = size - 1; i >= 0; i--) { + while (index >= 0 && values[index] >= i) { + current = values[index--] + } + result[i] = current + } + } else { + let index = 0 + for (let i = 0; i < size; i++) { + while (index < values.length && values[index] <= i) { + current = values[index++] + } + result[i] = current + } + } + + return result +} + +/** + * @since 2.0.0 + * @category symbol + */ +export const ParseErrorTypeId: unique symbol = Symbol.for("effect/Cron/errors/ParseError") + +/** + * @since 2.0.0 + * @category symbols + */ +export type ParseErrorTypeId = typeof ParseErrorTypeId + +/** + * Represents a checked exception which occurs when decoding fails. + * + * @since 2.0.0 + * @category models + */ +export class ParseError extends Data.TaggedError("CronParseError")<{ + readonly message: string + readonly input?: string +}> { + /** + * @since 2.0.0 + */ + readonly [ParseErrorTypeId] = ParseErrorTypeId +} + +/** + * Returns `true` if the specified value is an `ParseError`, `false` otherwise. + * + * @since 2.0.0 + * @category guards + */ +export const isParseError = (u: unknown): u is ParseError => hasProperty(u, ParseErrorTypeId) + +/** + * Parses a cron expression into a `Cron` instance. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { Cron, Either } from "effect" + * + * // At 04:00 on every day-of-month from 8 through 14. + * assert.deepStrictEqual(Cron.parse("0 0 4 8-14 * *"), Either.right(Cron.make({ + * seconds: [0], + * minutes: [0], + * hours: [4], + * days: [8, 9, 10, 11, 12, 13, 14], + * months: [], + * weekdays: [] + * }))) + * ``` + * + * @since 2.0.0 + * @category constructors + */ +export const parse = (cron: string, tz?: DateTime.TimeZone | string): Either.Either => { + const segments = cron.split(" ").filter(String.isNonEmpty) + if (segments.length !== 5 && segments.length !== 6) { + return Either.left( + new ParseError({ + message: `Invalid number of segments in cron expression`, + input: cron + }) + ) + } + + if (segments.length === 5) { + segments.unshift("0") + } + + const [seconds, minutes, hours, days, months, weekdays] = segments + const zone = tz === undefined || dateTime.isTimeZone(tz) ? + Either.right(tz) : + Either.fromOption(dateTime.zoneFromString(tz), () => + new ParseError({ + message: `Invalid time zone in cron expression`, + input: tz + })) + + return Either.all({ + tz: zone, + seconds: parseSegment(seconds, secondOptions), + minutes: parseSegment(minutes, minuteOptions), + hours: parseSegment(hours, hourOptions), + days: parseSegment(days, dayOptions), + months: parseSegment(months, monthOptions), + weekdays: parseSegment(weekdays, weekdayOptions) + }).pipe(Either.map(make)) +} + +/** + * Parses a cron expression into a `Cron` instance. + * + * **Details** + * + * This function takes a cron expression as a string and attempts to parse it + * into a `Cron` instance. If the expression is valid, the resulting `Cron` + * instance will represent the schedule defined by the cron expression. + * + * If the expression is invalid, the function throws a `ParseError`. + * + * You can optionally provide a time zone (`tz`) to interpret the cron + * expression in a specific time zone. If no time zone is provided, the cron + * expression will use the default time zone. + * + * @example + * ```ts + * import { Cron } from "effect" + * + * // At 04:00 on every day-of-month from 8 through 14. + * console.log(Cron.unsafeParse("0 4 8-14 * *")) + * // Output: + * // { + * // _id: 'Cron', + * // tz: { _id: 'Option', _tag: 'None' }, + * // seconds: [ 0 ], + * // minutes: [ 0 ], + * // hours: [ 4 ], + * // days: [ + * // 8, 9, 10, 11, + * // 12, 13, 14 + * // ], + * // months: [], + * // weekdays: [] + * // } + * ``` + * + * @since 2.0.0 + * @category constructors + */ +export const unsafeParse = (cron: string, tz?: DateTime.TimeZone | string): Cron => + Either.getOrThrowWith(parse(cron, tz), identity) + +/** + * Checks if a given `Date` falls within an active `Cron` time window. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { Cron, Either } from "effect" + * + * const cron = Either.getOrThrow(Cron.parse("0 4 8-14 * *")) + * assert.deepStrictEqual(Cron.match(cron, new Date("2021-01-08 04:00:00")), true) + * assert.deepStrictEqual(Cron.match(cron, new Date("2021-01-08 05:00:00")), false) + * ``` + * + * @throws `IllegalArgumentException` if the given `DateTime.Input` is invalid. + * + * @since 2.0.0 + */ +export const match = (cron: Cron, date: DateTime.DateTime.Input): boolean => { + const parts = dateTime.unsafeMakeZoned(date, { + timeZone: Option.getOrUndefined(cron.tz) + }).pipe(dateTime.toParts) + + if (cron.seconds.size !== 0 && !cron.seconds.has(parts.seconds)) { + return false + } + + if (cron.minutes.size !== 0 && !cron.minutes.has(parts.minutes)) { + return false + } + + if (cron.hours.size !== 0 && !cron.hours.has(parts.hours)) { + return false + } + + if (cron.months.size !== 0 && !cron.months.has(parts.month)) { + return false + } + + if (cron.days.size === 0 && cron.weekdays.size === 0) { + return true + } + + if (cron.weekdays.size === 0) { + return cron.days.has(parts.day) + } + + if (cron.days.size === 0) { + return cron.weekdays.has(parts.weekDay) + } + + return cron.days.has(parts.day) || cron.weekdays.has(parts.weekDay) +} + +const daysInMonth = (date: Date): number => + new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth() + 1, 0)).getUTCDate() + +/** + * Returns the next run `Date` for the given `Cron` instance. + * + * Uses the current time as a starting point if no value is provided for `startFrom`. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { Cron, Either } from "effect" + * + * const after = new Date("2021-01-01 00:00:00") + * const cron = Either.getOrThrow(Cron.parse("0 4 8-14 * *")) + * assert.deepStrictEqual(Cron.next(cron, after), new Date("2021-01-08 04:00:00")) + * ``` + * + * @throws `IllegalArgumentException` if the given `DateTime.Input` is invalid. + * @throws `Error` if the next run date cannot be found within 10,000 iterations. + * + * @since 2.0.0 + */ +export const next = (cron: Cron, startFrom?: DateTime.DateTime.Input): Date => { + return stepCron(cron, startFrom, "next") +} + +/** + * Returns the previous run `Date` for the given `Cron` instance. + * + * Uses the current time as a starting point if no value is provided for `startFrom`. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { Cron, Either } from "effect" + * + * const before = new Date("2021-01-15 00:00:00") + * const cron = Either.getOrThrow(Cron.parse("0 4 8-14 * *")) + * assert.deepStrictEqual(Cron.prev(cron, before), new Date("2021-01-14 04:00:00")) + * ``` + * + * @throws `IllegalArgumentException` if the given `DateTime.Input` is invalid. + * @throws `Error` if the previous run date cannot be found within 10,000 iterations. + * + * @since 3.20.0 + */ +export const prev = (cron: Cron, startFrom?: DateTime.DateTime.Input): Date => { + return stepCron(cron, startFrom, "prev") +} + +/** @internal */ +const stepCron = (cron: Cron, startFrom: DateTime.DateTime.Input | undefined, direction: "next" | "prev"): Date => { + const tz = Option.getOrUndefined(cron.tz) + const zoned = dateTime.unsafeMakeZoned(startFrom ?? new Date(), { + timeZone: tz + }) + + const prev = direction === "prev" + const tick = prev ? -1 : 1 + const table = cron[direction] + const boundary = prev ? cron.last : cron.first + + const needsStep = prev + ? (next: number, current: number) => next < current + : (next: number, current: number) => next > current + + const utc = tz !== undefined && dateTime.isTimeZoneNamed(tz) && tz.id === "UTC" + const adjustDst = utc ? constVoid : (current: Date) => { + const adjusted = dateTime.unsafeMakeZoned(current, { + timeZone: zoned.zone, + adjustForTimeZone: true, + disambiguation: prev ? "later" : undefined + }).pipe(dateTime.toDate) + + const drift = current.getTime() - adjusted.getTime() + if (prev ? drift !== 0 : drift > 0) { + current.setTime(adjusted.getTime()) + } + } + + const result = dateTime.mutate(zoned, (current) => { + current.setUTCSeconds(current.getUTCSeconds() + tick, 0) + + for (let i = 0; i < 10_000; i++) { + if (cron.seconds.size !== 0) { + const currentSecond = current.getUTCSeconds() + const nextSecond = table.second[currentSecond] + if (nextSecond === undefined) { + current.setUTCMinutes(current.getUTCMinutes() + tick, boundary.second) + adjustDst(current) + continue + } + if (needsStep(nextSecond, currentSecond)) { + current.setUTCSeconds(nextSecond) + adjustDst(current) + continue + } + } + + if (cron.minutes.size !== 0) { + const currentMinute = current.getUTCMinutes() + const nextMinute = table.minute[currentMinute] + if (nextMinute === undefined) { + current.setUTCHours(current.getUTCHours() + tick, boundary.minute, boundary.second) + adjustDst(current) + continue + } + if (needsStep(nextMinute, currentMinute)) { + current.setUTCMinutes(nextMinute, boundary.second) + adjustDst(current) + continue + } + } + + if (cron.hours.size !== 0) { + const currentHour = current.getUTCHours() + const nextHour = table.hour[currentHour] + if (nextHour === undefined) { + current.setUTCDate(current.getUTCDate() + tick) + current.setUTCHours(boundary.hour, boundary.minute, boundary.second) + adjustDst(current) + continue + } + if (needsStep(nextHour, currentHour)) { + current.setUTCHours(nextHour, boundary.minute, boundary.second) + adjustDst(current) + continue + } + } + + if (cron.weekdays.size !== 0 || cron.days.size !== 0) { + let a: number = prev ? -Infinity : Infinity + let b: number = prev ? -Infinity : Infinity + + if (cron.weekdays.size !== 0) { + const currentWeekday = current.getUTCDay() + const nextWeekday = table.weekday[currentWeekday] + if (nextWeekday === undefined) { + a = prev + ? currentWeekday - 7 + boundary.weekday + : 7 - currentWeekday + boundary.weekday + } else { + a = nextWeekday - currentWeekday + } + } + + // Only check day-of-month if weekday constraint not already satisfied (they're OR'd) + if (cron.days.size !== 0 && a !== 0) { + const currentDay = current.getUTCDate() + const nextDay = table.day[currentDay] + if (nextDay === undefined) { + if (prev) { + // When wrapping to previous month, calculate days back: + // Current day offset + gap from end of prev month to target day + // Example: June 3 → May 20 with boundary.day=20: -(3 + (31 - 20)) = -14 + const prevMonthDays = daysInMonth( + new Date(Date.UTC(current.getUTCFullYear(), current.getUTCMonth(), 0)) + ) + b = -(currentDay + (prevMonthDays - boundary.day)) + } else { + b = daysInMonth(current) - currentDay + boundary.day + } + } else { + b = nextDay - currentDay + } + } + + const addDays = prev ? Math.max(a, b) : Math.min(a, b) + if (addDays !== 0) { + current.setUTCDate(current.getUTCDate() + addDays) + current.setUTCHours(boundary.hour, boundary.minute, boundary.second) + adjustDst(current) + continue + } + } + + if (cron.months.size !== 0) { + const currentMonth = current.getUTCMonth() + 1 + const nextMonth = table.month[currentMonth] + const clampBoundaryDay = (targetMonthIndex: number): number => { + if (cron.days.size !== 0) { + return boundary.day + } + const maxDayInMonth = daysInMonth(new Date(Date.UTC(current.getUTCFullYear(), targetMonthIndex, 1))) + return Math.min(boundary.day, maxDayInMonth) + } + if (nextMonth === undefined) { + current.setUTCFullYear(current.getUTCFullYear() + tick) + current.setUTCMonth(boundary.month, clampBoundaryDay(boundary.month)) + current.setUTCHours(boundary.hour, boundary.minute, boundary.second) + adjustDst(current) + continue + } + if (needsStep(nextMonth, currentMonth)) { + const targetMonthIndex = nextMonth - 1 + current.setUTCMonth(targetMonthIndex, clampBoundaryDay(targetMonthIndex)) + current.setUTCHours(boundary.hour, boundary.minute, boundary.second) + adjustDst(current) + continue + } + } + + return + } + + throw new Error("Unable to find next cron date") + }) + + return dateTime.toDateUtc(result) +} + +/** + * Returns an `IterableIterator` which yields the sequence of `Date`s that match the `Cron` instance. + * + * @since 2.0.0 + */ +export const sequence = function*(cron: Cron, startFrom?: DateTime.DateTime.Input): IterableIterator { + while (true) { + yield startFrom = next(cron, startFrom) + } +} + +/** + * Returns an `IterableIterator` which yields the sequence of `Date`s that match the `Cron` instance, + * in reverse direction. + * + * @since 3.20.0 + */ +export const sequenceReverse = function*(cron: Cron, startFrom?: DateTime.DateTime.Input): IterableIterator { + while (true) { + yield startFrom = prev(cron, startFrom) + } +} + +/** + * @category instances + * @since 2.0.0 + */ +export const Equivalence: equivalence.Equivalence = equivalence.make((self, that) => + restrictionsEquals(self.seconds, that.seconds) && + restrictionsEquals(self.minutes, that.minutes) && + restrictionsEquals(self.hours, that.hours) && + restrictionsEquals(self.days, that.days) && + restrictionsEquals(self.months, that.months) && + restrictionsEquals(self.weekdays, that.weekdays) +) + +const restrictionsArrayEquals = equivalence.array(equivalence.number) +const restrictionsEquals = (self: ReadonlySet, that: ReadonlySet): boolean => + restrictionsArrayEquals(Arr.fromIterable(self), Arr.fromIterable(that)) + +/** + * Checks if two `Cron`s are equal. + * + * @since 2.0.0 + * @category predicates + */ +export const equals: { + (that: Cron): (self: Cron) => boolean + (self: Cron, that: Cron): boolean +} = dual(2, (self: Cron, that: Cron): boolean => Equivalence(self, that)) + +interface SegmentOptions { + min: number + max: number + aliases?: Record | undefined +} + +const secondOptions: SegmentOptions = { + min: 0, + max: 59 +} + +const minuteOptions: SegmentOptions = { + min: 0, + max: 59 +} + +const hourOptions: SegmentOptions = { + min: 0, + max: 23 +} + +const dayOptions: SegmentOptions = { + min: 1, + max: 31 +} + +const monthOptions: SegmentOptions = { + min: 1, + max: 12, + aliases: { + jan: 1, + feb: 2, + mar: 3, + apr: 4, + may: 5, + jun: 6, + jul: 7, + aug: 8, + sep: 9, + oct: 10, + nov: 11, + dec: 12 + } +} + +const weekdayOptions: SegmentOptions = { + min: 0, + max: 6, + aliases: { + sun: 0, + mon: 1, + tue: 2, + wed: 3, + thu: 4, + fri: 5, + sat: 6 + } +} + +const parseSegment = ( + input: string, + options: SegmentOptions +): Either.Either, ParseError> => { + const capacity = options.max - options.min + 1 + const values = new Set() + const fields = input.split(",") + + for (const field of fields) { + const [raw, step] = splitStep(field) + if (raw === "*" && step === undefined) { + return Either.right(new Set()) + } + + if (step !== undefined) { + if (!Number.isInteger(step)) { + return Either.left(new ParseError({ message: `Expected step value to be a positive integer`, input })) + } + if (step < 1) { + return Either.left(new ParseError({ message: `Expected step value to be greater than 0`, input })) + } + if (step > options.max) { + return Either.left(new ParseError({ message: `Expected step value to be less than ${options.max}`, input })) + } + } + + if (raw === "*") { + for (let i = options.min; i <= options.max; i += step ?? 1) { + values.add(i) + } + } else { + const [left, right] = splitRange(raw, options.aliases) + if (!Number.isInteger(left)) { + return Either.left(new ParseError({ message: `Expected a positive integer`, input })) + } + if (left < options.min || left > options.max) { + return Either.left( + new ParseError({ message: `Expected a value between ${options.min} and ${options.max}`, input }) + ) + } + + if (right === undefined) { + values.add(left) + } else { + if (!Number.isInteger(right)) { + return Either.left(new ParseError({ message: `Expected a positive integer`, input })) + } + if (right < options.min || right > options.max) { + return Either.left( + new ParseError({ message: `Expected a value between ${options.min} and ${options.max}`, input }) + ) + } + if (left > right) { + return Either.left(new ParseError({ message: `Invalid value range`, input })) + } + + for (let i = left; i <= right; i += step ?? 1) { + values.add(i) + } + } + } + + if (values.size >= capacity) { + return Either.right(new Set()) + } + } + + return Either.right(values) +} + +const splitStep = (input: string): [string, number | undefined] => { + const seperator = input.indexOf("/") + if (seperator !== -1) { + return [input.slice(0, seperator), Number(input.slice(seperator + 1))] + } + + return [input, undefined] +} + +const splitRange = (input: string, aliases?: Record): [number, number | undefined] => { + const seperator = input.indexOf("-") + if (seperator !== -1) { + return [aliasOrValue(input.slice(0, seperator), aliases), aliasOrValue(input.slice(seperator + 1), aliases)] + } + + return [aliasOrValue(input, aliases), undefined] +} + +function aliasOrValue(field: string, aliases?: Record): number { + return aliases?.[field.toLocaleLowerCase()] ?? Number(field) +} diff --git a/repos/effect/packages/effect/src/Data.ts b/repos/effect/packages/effect/src/Data.ts new file mode 100644 index 0000000..f4910d4 --- /dev/null +++ b/repos/effect/packages/effect/src/Data.ts @@ -0,0 +1,590 @@ +/** + * @since 2.0.0 + */ +import type * as Cause from "./Cause.js" +import * as core from "./internal/core.js" +import * as internal from "./internal/data.js" +import { StructuralPrototype } from "./internal/effectable.js" +import * as Predicate from "./Predicate.js" +import type * as Types from "./Types.js" +import type { Unify } from "./Unify.js" + +/** + * @since 2.0.0 + */ +export declare namespace Case { + /** + * @since 2.0.0 + * @category models + */ + export interface Constructor { + ( + args: Types.VoidIfEmpty<{ readonly [P in keyof A as P extends Tag ? never : P]: A[P] }> + ): A + } +} + +/** + * @example + * ```ts + * import * as assert from "node:assert" + * import { Data, Equal } from "effect" + * + * const alice = Data.struct({ name: "Alice", age: 30 }) + * + * const bob = Data.struct({ name: "Bob", age: 40 }) + * + * assert.deepStrictEqual(Equal.equals(alice, alice), true) + * assert.deepStrictEqual(Equal.equals(alice, Data.struct({ name: "Alice", age: 30 })), true) + * + * assert.deepStrictEqual(Equal.equals(alice, { name: "Alice", age: 30 }), false) + * assert.deepStrictEqual(Equal.equals(alice, bob), false) + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const struct: >(a: A) => { readonly [P in keyof A]: A[P] } = internal.struct + +/** + * @category constructors + * @since 2.0.0 + */ +export const unsafeStruct = >(as: A): { readonly [P in keyof A]: A[P] } => + Object.setPrototypeOf(as, StructuralPrototype) + +/** + * @example + * ```ts + * import * as assert from "node:assert" + * import { Data, Equal } from "effect" + * + * const alice = Data.tuple("Alice", 30) + * + * const bob = Data.tuple("Bob", 40) + * + * assert.deepStrictEqual(Equal.equals(alice, alice), true) + * assert.deepStrictEqual(Equal.equals(alice, Data.tuple("Alice", 30)), true) + * + * assert.deepStrictEqual(Equal.equals(alice, ["Alice", 30]), false) + * assert.deepStrictEqual(Equal.equals(alice, bob), false) + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const tuple = >(...as: As): Readonly => unsafeArray(as) + +/** + * @example + * ```ts + * import * as assert from "node:assert" + * import { Data, Equal } from "effect" + * + * const alice = Data.struct({ name: "Alice", age: 30 }) + * const bob = Data.struct({ name: "Bob", age: 40 }) + * + * const persons = Data.array([alice, bob]) + * + * assert.deepStrictEqual( + * Equal.equals( + * persons, + * Data.array([ + * Data.struct({ name: "Alice", age: 30 }), + * Data.struct({ name: "Bob", age: 40 }) + * ]) + * ), + * true + * ) + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const array = >(as: As): Readonly => unsafeArray(as.slice(0) as unknown as As) + +/** + * @category constructors + * @since 2.0.0 + */ +export const unsafeArray = >(as: As): Readonly => + Object.setPrototypeOf(as, internal.ArrayProto) + +const _case = (): Case.Constructor => (args) => + (args === undefined ? Object.create(StructuralPrototype) : struct(args)) as any + +export { + /** + * Provides a constructor for the specified `Case`. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { Data, Equal } from "effect" + * + * interface Person { + * readonly name: string + * } + * + * // Creating a constructor for the specified Case + * const Person = Data.case() + * + * // Creating instances of Person + * const mike1 = Person({ name: "Mike" }) + * const mike2 = Person({ name: "Mike" }) + * const john = Person({ name: "John" }) + * + * // Checking equality + * assert.deepStrictEqual(Equal.equals(mike1, mike2), true) + * assert.deepStrictEqual(Equal.equals(mike1, john), false) + * + * ``` + * @since 2.0.0 + * @category constructors + */ + _case as case +} + +/** + * Provides a tagged constructor for the specified `Case`. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { Data } from "effect" + * + * interface Person { + * readonly _tag: "Person" // the tag + * readonly name: string + * } + * + * const Person = Data.tagged("Person") + * + * const mike = Person({ name: "Mike" }) + * + * assert.deepEqual(mike, { _tag: "Person", name: "Mike" }) + * ``` + * + * @since 2.0.0 + * @category constructors + */ +export const tagged = ( + tag: A["_tag"] +): Case.Constructor => +(args) => { + const value = args === undefined ? Object.create(StructuralPrototype) : struct(args) + value._tag = tag + return value +} + +/** + * Provides a constructor for a Case Class. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { Data, Equal } from "effect" + * + * class Person extends Data.Class<{ readonly name: string }> {} + * + * // Creating instances of Person + * const mike1 = new Person({ name: "Mike" }) + * const mike2 = new Person({ name: "Mike" }) + * const john = new Person({ name: "John" }) + * + * // Checking equality + * assert.deepStrictEqual(Equal.equals(mike1, mike2), true) + * assert.deepStrictEqual(Equal.equals(mike1, john), false) + * ``` + * + * @since 2.0.0 + * @category constructors + */ +export const Class: new = {}>( + args: Types.VoidIfEmpty<{ readonly [P in keyof A]: A[P] }> +) => Readonly = internal.Structural as any + +/** + * Provides a Tagged constructor for a Case Class. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { Data, Equal } from "effect" + * + * class Person extends Data.TaggedClass("Person")<{ readonly name: string }> {} + * + * // Creating instances of Person + * const mike1 = new Person({ name: "Mike" }) + * const mike2 = new Person({ name: "Mike" }) + * const john = new Person({ name: "John" }) + * + * // Checking equality + * assert.deepStrictEqual(Equal.equals(mike1, mike2), true) + * assert.deepStrictEqual(Equal.equals(mike1, john), false) + * + * assert.deepStrictEqual(mike1._tag, "Person") + * ``` + * + * @since 2.0.0 + * @category constructors + */ +export const TaggedClass = ( + tag: Tag +): new = {}>( + args: Types.VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P] }> +) => Readonly & { readonly _tag: Tag } => { + class Base extends Class { + readonly _tag = tag + } + return Base as any +} + +/** + * @since 2.0.0 + * @category constructors + */ +export const Structural: new( + args: Types.VoidIfEmpty<{ readonly [P in keyof A]: A[P] }> +) => {} = internal.Structural as any + +/** + * Create a tagged enum data type, which is a union of `Data` structs. + * + * ```ts + * import * as assert from "node:assert" + * import { Data } from "effect" + * + * type HttpError = Data.TaggedEnum<{ + * BadRequest: { readonly status: 400, readonly message: string } + * NotFound: { readonly status: 404, readonly message: string } + * }> + * + * // Equivalent to: + * type HttpErrorPlain = + * | { + * readonly _tag: "BadRequest" + * readonly status: 400 + * readonly message: string + * } + * | { + * readonly _tag: "NotFound" + * readonly status: 404 + * readonly message: string + * } + * ``` + * + * @since 2.0.0 + * @category models + */ +export type TaggedEnum< + A extends Record> & UntaggedChildren +> = keyof A extends infer Tag ? + Tag extends keyof A ? Types.Simplify<{ readonly _tag: Tag } & { readonly [K in keyof A[Tag]]: A[Tag][K] }> + : never + : never + +type ChildrenAreTagged = keyof A extends infer K ? K extends keyof A ? "_tag" extends keyof A[K] ? true + : false + : never + : never + +type UntaggedChildren = true extends ChildrenAreTagged + ? "It looks like you're trying to create a tagged enum, but one or more of its members already has a `_tag` property." + : unknown + +/** + * @since 2.0.0 + */ +export declare namespace TaggedEnum { + /** + * @since 2.0.0 + * @category models + */ + export interface WithGenerics { + readonly taggedEnum: { readonly _tag: string } + readonly numberOfGenerics: Count + + readonly A: unknown + readonly B: unknown + readonly C: unknown + readonly D: unknown + } + + /** + * @since 2.0.0 + * @category models + */ + export type Kind< + Z extends WithGenerics, + A = unknown, + B = unknown, + C = unknown, + D = unknown + > = (Z & { + readonly A: A + readonly B: B + readonly C: C + readonly D: D + })["taggedEnum"] + + /** + * @since 2.0.0 + */ + export type Args< + A extends { readonly _tag: string }, + K extends A["_tag"], + E = Extract + > = { readonly [K in keyof E as K extends "_tag" ? never : K]: E[K] } extends infer T ? Types.VoidIfEmpty + : never + + /** + * @since 2.0.0 + */ + export type Value< + A extends { readonly _tag: string }, + K extends A["_tag"] + > = Extract + + /** + * @since 3.1.0 + */ + export type Constructor = Types.Simplify< + & { + readonly [Tag in A["_tag"]]: Case.Constructor, "_tag"> + } + & { + readonly $is: (tag: Tag) => (u: unknown) => u is Extract + readonly $match: { + < + const Cases extends { + readonly [Tag in A["_tag"]]: (args: Extract) => any + } + >( + cases: Cases & { [K in Exclude]: never } + ): (value: A) => Unify> + < + const Cases extends { + readonly [Tag in A["_tag"]]: (args: Extract) => any + } + >( + value: A, + cases: Cases & { [K in Exclude]: never } + ): Unify> + } + } + > + + /** + * @since 3.2.0 + */ + export interface GenericMatchers> { + readonly $is: ( + tag: Tag + ) => { + >( + u: T + ): u is T & { readonly _tag: Tag } + (u: unknown): u is Extract, { readonly _tag: Tag }> + } + readonly $match: { + < + A, + B, + C, + D, + Cases extends { + readonly [Tag in Z["taggedEnum"]["_tag"]]: ( + args: Extract, { readonly _tag: Tag }> + ) => any + } + >( + cases: Cases & { [K in Exclude]: never } + ): (self: TaggedEnum.Kind) => Unify> + < + A, + B, + C, + D, + Cases extends { + readonly [Tag in Z["taggedEnum"]["_tag"]]: ( + args: Extract, { readonly _tag: Tag }> + ) => any + } + >( + self: TaggedEnum.Kind, + cases: Cases & { [K in Exclude]: never } + ): Unify> + } + } +} + +/** + * Create a constructor for a tagged union of `Data` structs. + * + * You can also pass a `TaggedEnum.WithGenerics` if you want to add generics to + * the constructor. + * + * @example + * ```ts + * import { Data } from "effect" + * + * const { BadRequest, NotFound } = Data.taggedEnum< + * | { readonly _tag: "BadRequest"; readonly status: 400; readonly message: string } + * | { readonly _tag: "NotFound"; readonly status: 404; readonly message: string } + * >() + * + * const notFound = NotFound({ status: 404, message: "Not Found" }) + * ``` + * + * @example + * import { Data } from "effect" + * + * type MyResult = Data.TaggedEnum<{ + * Failure: { readonly error: E } + * Success: { readonly value: A } + * }> + * interface MyResultDefinition extends Data.TaggedEnum.WithGenerics<2> { + * readonly taggedEnum: MyResult + * } + * const { Failure, Success } = Data.taggedEnum() + * + * const success = Success({ value: 1 }) + * + * @category constructors + * @since 2.0.0 + */ +export const taggedEnum: { + >(): Types.Simplify< + { + readonly [Tag in Z["taggedEnum"]["_tag"]]: ( + args: TaggedEnum.Args< + TaggedEnum.Kind, + Tag, + Extract, { readonly _tag: Tag }> + > + ) => TaggedEnum.Value, Tag> + } & TaggedEnum.GenericMatchers + > + + >(): Types.Simplify< + { + readonly [Tag in Z["taggedEnum"]["_tag"]]: ( + args: TaggedEnum.Args< + TaggedEnum.Kind, + Tag, + Extract, { readonly _tag: Tag }> + > + ) => TaggedEnum.Value, Tag> + } & TaggedEnum.GenericMatchers + > + + >(): Types.Simplify< + { + readonly [Tag in Z["taggedEnum"]["_tag"]]: ( + args: TaggedEnum.Args< + TaggedEnum.Kind, + Tag, + Extract, { readonly _tag: Tag }> + > + ) => TaggedEnum.Value, Tag> + } & TaggedEnum.GenericMatchers + > + + >(): Types.Simplify< + { + readonly [Tag in Z["taggedEnum"]["_tag"]]: ( + args: TaggedEnum.Args< + TaggedEnum.Kind, + Tag, + Extract, { readonly _tag: Tag }> + > + ) => TaggedEnum.Value, Tag> + } & TaggedEnum.GenericMatchers + > + + (): TaggedEnum.Constructor +} = () => + new Proxy({}, { + get(_target, tag, _receiver) { + if (tag === "$is") { + return Predicate.isTagged + } else if (tag === "$match") { + return taggedMatch + } + return tagged(tag as string) + } + }) as any + +function taggedMatch< + A extends { readonly _tag: string }, + Cases extends { + readonly [K in A["_tag"]]: (args: Extract) => any + } +>(self: A, cases: Cases): ReturnType +function taggedMatch< + A extends { readonly _tag: string }, + Cases extends { + readonly [K in A["_tag"]]: (args: Extract) => any + } +>(cases: Cases): (value: A) => ReturnType +function taggedMatch< + A extends { readonly _tag: string }, + Cases extends { + readonly [K in A["_tag"]]: (args: Extract) => any + } +>(): any { + if (arguments.length === 1) { + const cases = arguments[0] as Cases + return function(value: A): ReturnType { + return cases[value._tag as A["_tag"]](value as any) + } + } + const value = arguments[0] as A + const cases = arguments[1] as Cases + return cases[value._tag as A["_tag"]](value as any) +} + +/** + * Provides a constructor for a Case Class. + * + * @since 2.0.0 + * @category constructors + */ +export const Error: new = {}>( + args: Types.VoidIfEmpty<{ readonly [P in keyof A]: A[P] }> +) => Cause.YieldableError & Readonly = (function() { + const plainArgsSymbol = Symbol.for("effect/Data/Error/plainArgs") + const O = { + BaseEffectError: class extends core.YieldableError { + constructor(args: any) { + super(args?.message, args?.cause ? { cause: args.cause } : undefined) + if (args) { + Object.assign(this, args) + // @effect-diagnostics-next-line floatingEffect:off + Object.defineProperty(this, plainArgsSymbol, { value: args, enumerable: false }) + } + } + toJSON() { + return { ...(this as any)[plainArgsSymbol], ...this } + } + } as any + } + return O.BaseEffectError +})() + +/** + * @since 2.0.0 + * @category constructors + */ +export const TaggedError = (tag: Tag): new = {}>( + args: Types.VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P] }> +) => Cause.YieldableError & { readonly _tag: Tag } & Readonly => { + const O = { + BaseEffectError: class extends Error<{}> { + readonly _tag = tag + } + } + ;(O.BaseEffectError.prototype as any).name = tag + return O.BaseEffectError as any +} diff --git a/repos/effect/packages/effect/src/DateTime.ts b/repos/effect/packages/effect/src/DateTime.ts new file mode 100644 index 0000000..5f11606 --- /dev/null +++ b/repos/effect/packages/effect/src/DateTime.ts @@ -0,0 +1,1686 @@ +/** + * @since 3.6.0 + */ +import type { IllegalArgumentException } from "./Cause.js" +import * as Context from "./Context.js" +import type * as Duration from "./Duration.js" +import * as Effect from "./Effect.js" +import type * as Either from "./Either.js" +import type * as equivalence from "./Equivalence.js" +import { dual, type LazyArg } from "./Function.js" +import type { Inspectable } from "./Inspectable.js" +import * as Internal from "./internal/dateTime.js" +import * as Layer from "./Layer.js" +import type * as Option from "./Option.js" +import type * as order from "./Order.js" +import type { Pipeable } from "./Pipeable.js" + +/** + * @since 3.6.0 + * @category type ids + */ +export const TypeId: unique symbol = Internal.TypeId + +/** + * @since 3.6.0 + * @category type ids + */ +export type TypeId = typeof TypeId + +/** + * A `DateTime` represents a point in time. It can optionally have a time zone + * associated with it. + * + * @since 3.6.0 + * @category models + */ +export type DateTime = Utc | Zoned + +/** + * @since 3.6.0 + * @category models + */ +export interface Utc extends DateTime.Proto { + readonly _tag: "Utc" + readonly epochMillis: number + partsUtc: DateTime.PartsWithWeekday | undefined +} + +/** + * @since 3.6.0 + * @category models + */ +export interface Zoned extends DateTime.Proto { + readonly _tag: "Zoned" + readonly epochMillis: number + readonly zone: TimeZone + adjustedEpochMillis: number | undefined + partsAdjusted: DateTime.PartsWithWeekday | undefined + partsUtc: DateTime.PartsWithWeekday | undefined +} + +/** + * @since 3.6.0 + * @category models + */ +export declare namespace DateTime { + /** + * @since 3.6.0 + * @category models + */ + export type Input = DateTime | Partial | Date | number | string + + /** + * @since 3.6.0 + * @category models + */ + export type PreserveZone = A extends Zoned ? Zoned : Utc + + /** + * @since 3.6.0 + * @category models + */ + export type Unit = UnitSingular | UnitPlural + + /** + * @since 3.6.0 + * @category models + */ + export type UnitSingular = + | "milli" + | "second" + | "minute" + | "hour" + | "day" + | "week" + | "month" + | "year" + + /** + * @since 3.6.0 + * @category models + */ + export type UnitPlural = + | "millis" + | "seconds" + | "minutes" + | "hours" + | "days" + | "weeks" + | "months" + | "years" + + /** + * @since 3.6.0 + * @category models + */ + export interface PartsWithWeekday { + readonly millis: number + readonly seconds: number + readonly minutes: number + readonly hours: number + readonly day: number + readonly weekDay: number + readonly month: number + readonly year: number + } + + /** + * @since 3.6.0 + * @category models + */ + export interface Parts { + readonly millis: number + readonly seconds: number + readonly minutes: number + readonly hours: number + readonly day: number + readonly month: number + readonly year: number + } + + /** + * @since 3.6.0 + * @category models + */ + export interface PartsForMath { + readonly millis: number + readonly seconds: number + readonly minutes: number + readonly hours: number + readonly days: number + readonly weeks: number + readonly months: number + readonly years: number + } + + /** + * @since 3.6.0 + * @category models + */ + export interface Proto extends Pipeable, Inspectable { + readonly [TypeId]: TypeId + } +} + +/** + * @since 3.6.0 + * @category type ids + */ +export const TimeZoneTypeId: unique symbol = Internal.TimeZoneTypeId + +/** + * @since 3.6.0 + * @category type ids + */ +export type TimeZoneTypeId = typeof TimeZoneTypeId + +/** + * @since 3.6.0 + * @category models + */ +export type TimeZone = TimeZone.Offset | TimeZone.Named + +/** + * @since 3.6.0 + * @category models + */ +export declare namespace TimeZone { + /** + * @since 3.6.0 + * @category models + */ + export interface Proto extends Inspectable { + readonly [TimeZoneTypeId]: TimeZoneTypeId + } + + /** + * @since 3.6.0 + * @category models + */ + export interface Offset extends Proto { + readonly _tag: "Offset" + readonly offset: number + } + + /** + * @since 3.6.0 + * @category models + */ + export interface Named extends Proto { + readonly _tag: "Named" + readonly id: string + /** @internal */ + readonly format: Intl.DateTimeFormat + } +} + +/** + * A `Disambiguation` is used to resolve ambiguities when a `DateTime` is + * ambiguous, such as during a daylight saving time transition. + * + * For more information, see the [Temporal documentation](https://tc39.es/proposal-temporal/docs/timezone.html#ambiguity-due-to-dst-or-other-time-zone-offset-changes) + * + * - `"compatible"`: (default) Behavior matching Temporal API and legacy JavaScript Date and moment.js. + * For repeated times, chooses the earlier occurrence. For gap times, chooses the later interpretation. + * + * - `"earlier"`: For repeated times, always choose the earlier occurrence. + * For gap times, choose the time before the gap. + * + * - `"later"`: For repeated times, always choose the later occurrence. + * For gap times, choose the time after the gap. + * + * - `"reject"`: Throw an `RangeError` when encountering ambiguous or non-existent times. + * + * @example + * ```ts + * import { DateTime } from "effect" + * + * // Fall-back example: 01:30 on Nov 2, 2025 in New York happens twice + * const ambiguousTime = { year: 2025, month: 11, day: 2, hours: 1, minutes: 30 } + * const timeZone = DateTime.zoneUnsafeMakeNamed("America/New_York") + * + * DateTime.makeZoned(ambiguousTime, { timeZone, adjustForTimeZone: true, disambiguation: "earlier" }) + * // Earlier occurrence (DST time): 2025-11-02T05:30:00.000Z + * + * DateTime.makeZoned(ambiguousTime, { timeZone, adjustForTimeZone: true, disambiguation: "later" }) + * // Later occurrence (standard time): 2025-11-02T06:30:00.000Z + * + * // Gap example: 02:30 on Mar 9, 2025 in New York doesn't exist + * const gapTime = { year: 2025, month: 3, day: 9, hours: 2, minutes: 30 } + * + * DateTime.makeZoned(gapTime, { timeZone, adjustForTimeZone: true, disambiguation: "earlier" }) + * // Time before gap: 2025-03-09T06:30:00.000Z (01:30 EST) + * + * DateTime.makeZoned(gapTime, { timeZone, adjustForTimeZone: true, disambiguation: "later" }) + * // Time after gap: 2025-03-09T07:30:00.000Z (03:30 EDT) + * ``` + * + * @since 3.18.0 + * @category models + */ +export type Disambiguation = "compatible" | "earlier" | "later" | "reject" + +// ============================================================================= +// guards +// ============================================================================= + +/** + * @since 3.6.0 + * @category guards + */ +export const isDateTime: (u: unknown) => u is DateTime = Internal.isDateTime + +/** + * @since 3.6.0 + * @category guards + */ +export const isTimeZone: (u: unknown) => u is TimeZone = Internal.isTimeZone + +/** + * @since 3.6.0 + * @category guards + */ +export const isTimeZoneOffset: (u: unknown) => u is TimeZone.Offset = Internal.isTimeZoneOffset + +/** + * @since 3.6.0 + * @category guards + */ +export const isTimeZoneNamed: (u: unknown) => u is TimeZone.Named = Internal.isTimeZoneNamed + +/** + * @since 3.6.0 + * @category guards + */ +export const isUtc: (self: DateTime) => self is Utc = Internal.isUtc + +/** + * @since 3.6.0 + * @category guards + */ +export const isZoned: (self: DateTime) => self is Zoned = Internal.isZoned + +// ============================================================================= +// instances +// ============================================================================= + +/** + * @since 3.6.0 + * @category instances + */ +export const Equivalence: equivalence.Equivalence = Internal.Equivalence + +/** + * @since 3.6.0 + * @category instances + */ +export const Order: order.Order = Internal.Order + +/** + * @since 3.6.0 + */ +export const clamp: { + ( + options: { readonly minimum: Min; readonly maximum: Max } + ): (self: A) => A | Min | Max + ( + self: A, + options: { readonly minimum: Min; readonly maximum: Max } + ): A | Min | Max +} = Internal.clamp + +// ============================================================================= +// constructors +// ============================================================================= + +/** + * Create a `DateTime` from a `Date`. + * + * If the `Date` is invalid, an `IllegalArgumentException` will be thrown. + * + * @since 3.6.0 + * @category constructors + */ +export const unsafeFromDate: (date: Date) => Utc = Internal.unsafeFromDate + +/** + * Create a `DateTime` from one of the following: + * + * - A `DateTime` + * - A `Date` instance (invalid dates will throw an `IllegalArgumentException`) + * - The `number` of milliseconds since the Unix epoch + * - An object with the parts of a date + * - A `string` that can be parsed by `Date.parse` + * + * @since 3.6.0 + * @category constructors + * @example + * ```ts + * import { DateTime } from "effect" + * + * // from Date + * DateTime.unsafeMake(new Date()) + * + * // from parts + * DateTime.unsafeMake({ year: 2024 }) + * + * // from string + * DateTime.unsafeMake("2024-01-01") + * ``` + */ +export const unsafeMake: (input: A) => DateTime.PreserveZone = Internal.unsafeMake + +/** + * Create a `DateTime.Zoned` using `DateTime.unsafeMake` and a time zone. + * + * The input is treated as UTC and then the time zone is attached, unless + * `adjustForTimeZone` is set to `true`. In that case, the input is treated as + * already in the time zone. + * + * When `adjustForTimeZone` is true and ambiguous times occur during DST transitions, + * the `disambiguation` option controls how to resolve the ambiguity: + * - `compatible` (default): Choose earlier time for repeated times, later for gaps + * - `earlier`: Always choose the earlier of two possible times + * - `later`: Always choose the later of two possible times + * - `reject`: Throw an error when ambiguous times are encountered + * + * @since 3.6.0 + * @category constructors + * @example + * ```ts + * import { DateTime } from "effect" + * + * DateTime.unsafeMakeZoned(new Date(), { timeZone: "Europe/London" }) + * ``` + */ +export const unsafeMakeZoned: (input: DateTime.Input, options?: { + readonly timeZone?: number | string | TimeZone | undefined + readonly adjustForTimeZone?: boolean | undefined + readonly disambiguation?: Disambiguation | undefined +}) => Zoned = Internal.unsafeMakeZoned + +/** + * Create a `DateTime.Zoned` using `DateTime.make` and a time zone. + * + * The input is treated as UTC and then the time zone is attached, unless + * `adjustForTimeZone` is set to `true`. In that case, the input is treated as + * already in the time zone. + * + * When `adjustForTimeZone` is true and ambiguous times occur during DST transitions, + * the `disambiguation` option controls how to resolve the ambiguity: + * - `compatible` (default): Choose earlier time for repeated times, later for gaps + * - `earlier`: Always choose the earlier of two possible times + * - `later`: Always choose the later of two possible times + * - `reject`: Throw an error when ambiguous times are encountered + * + * If the date time input or time zone is invalid, `None` will be returned. + * + * @since 3.6.0 + * @category constructors + * @example + * ```ts + * import { DateTime } from "effect" + * + * DateTime.makeZoned(new Date(), { timeZone: "Europe/London" }) + * ``` + */ +export const makeZoned: ( + input: DateTime.Input, + options?: { + readonly timeZone?: number | string | TimeZone | undefined + readonly adjustForTimeZone?: boolean | undefined + readonly disambiguation?: Disambiguation | undefined + } +) => Option.Option = Internal.makeZoned + +/** + * Create a `DateTime` from one of the following: + * + * - A `DateTime` + * - A `Date` instance (invalid dates will throw an `IllegalArgumentException`) + * - The `number` of milliseconds since the Unix epoch + * - An object with the parts of a date + * - A `string` that can be parsed by `Date.parse` + * + * If the input is invalid, `None` will be returned. + * + * @since 3.6.0 + * @category constructors + * @example + * ```ts + * import { DateTime } from "effect" + * + * // from Date + * DateTime.make(new Date()) + * + * // from parts + * DateTime.make({ year: 2024 }) + * + * // from string + * DateTime.make("2024-01-01") + * ``` + */ +export const make: (input: A) => Option.Option> = Internal.make + +/** + * Create a `DateTime.Zoned` from a string. + * + * It uses the format: `YYYY-MM-DDTHH:mm:ss.sss+HH:MM[Time/Zone]`. + * + * @since 3.6.0 + * @category constructors + */ +export const makeZonedFromString: (input: string) => Option.Option = Internal.makeZonedFromString + +/** + * Get the current time using the `Clock` service and convert it to a `DateTime`. + * + * @since 3.6.0 + * @category constructors + * @example + * ```ts + * import { DateTime, Effect } from "effect" + * + * Effect.gen(function* () { + * const now = yield* DateTime.now + * }) + * ``` + */ +export const now: Effect.Effect = Internal.now + +/** + * Get the current time using the `Clock` service. + * + * @since 3.14.0 + * @category constructors + * @example + * ```ts + * import { DateTime, Effect } from "effect" + * + * Effect.gen(function* () { + * const now = yield* DateTime.nowAsDate + * }) + * ``` + */ +export const nowAsDate: Effect.Effect = Internal.nowAsDate + +/** + * Get the current time using `Date.now`. + * + * @since 3.6.0 + * @category constructors + */ +export const unsafeNow: LazyArg = Internal.unsafeNow + +// ============================================================================= +// time zones +// ============================================================================= + +/** + * For a `DateTime` returns a new `DateTime.Utc`. + * + * @since 3.13.0 + * @category time zones + * @example + * ```ts + * import { DateTime } from "effect" + * + * const now = DateTime.unsafeMakeZoned({ year: 2024 }, { timeZone: "Europe/London" }) + * + * // set as UTC + * const utc: DateTime.Utc = DateTime.toUtc(now) + * ``` + */ +export const toUtc: (self: DateTime) => Utc = Internal.toUtc + +/** + * Set the time zone of a `DateTime`, returning a new `DateTime.Zoned`. + * + * @since 3.6.0 + * @category time zones + * @example + * ```ts + * import { DateTime, Effect } from "effect" + * + * Effect.gen(function* () { + * const now = yield* DateTime.now + * const zone = DateTime.zoneUnsafeMakeNamed("Europe/London") + * + * // set the time zone + * const zoned: DateTime.Zoned = DateTime.setZone(now, zone) + * }) + * ``` + */ +export const setZone: { + (zone: TimeZone, options?: { + readonly adjustForTimeZone?: boolean | undefined + readonly disambiguation?: Disambiguation | undefined + }): (self: DateTime) => Zoned + (self: DateTime, zone: TimeZone, options?: { + readonly adjustForTimeZone?: boolean | undefined + readonly disambiguation?: Disambiguation | undefined + }): Zoned +} = Internal.setZone + +/** + * Add a fixed offset time zone to a `DateTime`. + * + * The offset is in milliseconds. + * + * @since 3.6.0 + * @category time zones + * @example + * ```ts + * import { DateTime, Effect } from "effect" + * + * Effect.gen(function* () { + * const now = yield* DateTime.now + * + * // set the offset time zone in milliseconds + * const zoned: DateTime.Zoned = DateTime.setZoneOffset(now, 3 * 60 * 60 * 1000) + * }) + * ``` + */ +export const setZoneOffset: { + (offset: number, options?: { + readonly adjustForTimeZone?: boolean | undefined + readonly disambiguation?: Disambiguation | undefined + }): (self: DateTime) => Zoned + (self: DateTime, offset: number, options?: { + readonly adjustForTimeZone?: boolean | undefined + readonly disambiguation?: Disambiguation | undefined + }): Zoned +} = Internal.setZoneOffset + +/** + * Attempt to create a named time zone from a IANA time zone identifier. + * + * If the time zone is invalid, an `IllegalArgumentException` will be thrown. + * + * @since 3.6.0 + * @category time zones + */ +export const zoneUnsafeMakeNamed: (zoneId: string) => TimeZone.Named = Internal.zoneUnsafeMakeNamed + +/** + * Create a fixed offset time zone. + * + * @since 3.6.0 + * @category time zones + */ +export const zoneMakeOffset: (offset: number) => TimeZone.Offset = Internal.zoneMakeOffset + +/** + * Create a named time zone from a IANA time zone identifier. If the time zone + * is invalid, `None` will be returned. + * + * @since 3.6.0 + * @category time zones + */ +export const zoneMakeNamed: (zoneId: string) => Option.Option = Internal.zoneMakeNamed + +/** + * Create a named time zone from a IANA time zone identifier. If the time zone + * is invalid, it will fail with an `IllegalArgumentException`. + * + * @since 3.6.0 + * @category time zones + */ +export const zoneMakeNamedEffect: (zoneId: string) => Effect.Effect = + Internal.zoneMakeNamedEffect + +/** + * Create a named time zone from the system's local time zone. + * + * @since 3.6.0 + * @category time zones + */ +export const zoneMakeLocal: () => TimeZone.Named = Internal.zoneMakeLocal + +/** + * Try parse a TimeZone from a string + * + * @since 3.6.0 + * @category time zones + */ +export const zoneFromString: (zone: string) => Option.Option = Internal.zoneFromString + +/** + * Format a `TimeZone` as a string. + * + * @since 3.6.0 + * @category time zones + * @example + * ```ts + * import { DateTime, Effect } from "effect" + * + * // Outputs "+03:00" + * DateTime.zoneToString(DateTime.zoneMakeOffset(3 * 60 * 60 * 1000)) + * + * // Outputs "Europe/London" + * DateTime.zoneToString(DateTime.zoneUnsafeMakeNamed("Europe/London")) + * ``` + */ +export const zoneToString: (self: TimeZone) => string = Internal.zoneToString + +/** + * Set the time zone of a `DateTime` from an IANA time zone identifier. If the + * time zone is invalid, `None` will be returned. + * + * @since 3.6.0 + * @category time zones + * @example + * ```ts + * import { DateTime, Effect } from "effect" + * + * Effect.gen(function* () { + * const now = yield* DateTime.now + * // set the time zone, returns an Option + * DateTime.setZoneNamed(now, "Europe/London") + * }) + * ``` + */ +export const setZoneNamed: { + (zoneId: string, options?: { + readonly adjustForTimeZone?: boolean | undefined + readonly disambiguation?: Disambiguation | undefined + }): (self: DateTime) => Option.Option + (self: DateTime, zoneId: string, options?: { + readonly adjustForTimeZone?: boolean | undefined + readonly disambiguation?: Disambiguation | undefined + }): Option.Option +} = Internal.setZoneNamed + +/** + * Set the time zone of a `DateTime` from an IANA time zone identifier. If the + * time zone is invalid, an `IllegalArgumentException` will be thrown. + * + * @since 3.6.0 + * @category time zones + * @example + * ```ts + * import { DateTime, Effect } from "effect" + * + * Effect.gen(function* () { + * const now = yield* DateTime.now + * // set the time zone + * DateTime.unsafeSetZoneNamed(now, "Europe/London") + * }) + * ``` + */ +export const unsafeSetZoneNamed: { + (zoneId: string, options?: { + readonly adjustForTimeZone?: boolean | undefined + readonly disambiguation?: Disambiguation | undefined + }): (self: DateTime) => Zoned + (self: DateTime, zoneId: string, options?: { + readonly adjustForTimeZone?: boolean | undefined + readonly disambiguation?: Disambiguation | undefined + }): Zoned +} = Internal.unsafeSetZoneNamed + +// ============================================================================= +// comparisons +// ============================================================================= + +/** + * Calulate the difference between two `DateTime` values, returning the number + * of milliseconds the `other` DateTime is from `self`. + * + * If `other` is *after* `self`, the result will be a positive number. + * + * @since 3.6.0 + * @category comparisons + * @example + * ```ts + * import { DateTime, Effect } from "effect" + * + * Effect.gen(function* () { + * const now = yield* DateTime.now + * const other = DateTime.add(now, { minutes: 1 }) + * + * // returns 60000 + * DateTime.distance(now, other) + * }) + * ``` + */ +export const distance: { + (other: DateTime): (self: DateTime) => number + (self: DateTime, other: DateTime): number +} = Internal.distance + +/** + * Calulate the difference between two `DateTime` values. + * + * If the `other` DateTime is before `self`, the result will be a negative + * `Duration`, returned as a `Left`. + * + * If the `other` DateTime is after `self`, the result will be a positive + * `Duration`, returned as a `Right`. + * + * @since 3.6.0 + * @category comparisons + * @example + * ```ts + * import { DateTime, Effect } from "effect" + * + * Effect.gen(function* () { + * const now = yield* DateTime.now + * const other = DateTime.add(now, { minutes: 1 }) + * + * // returns Either.right(Duration.minutes(1)) + * DateTime.distanceDurationEither(now, other) + * + * // returns Either.left(Duration.minutes(1)) + * DateTime.distanceDurationEither(other, now) + * }) + * ``` + */ +export const distanceDurationEither: { + (other: DateTime): (self: DateTime) => Either.Either + (self: DateTime, other: DateTime): Either.Either +} = Internal.distanceDurationEither + +/** + * Calulate the distance between two `DateTime` values. + * + * @since 3.6.0 + * @category comparisons + * @example + * ```ts + * import { DateTime, Effect } from "effect" + * + * Effect.gen(function* () { + * const now = yield* DateTime.now + * const other = DateTime.add(now, { minutes: 1 }) + * + * // returns Duration.minutes(1) + * DateTime.distanceDuration(now, other) + * }) + * ``` + */ +export const distanceDuration: { + (other: DateTime): (self: DateTime) => Duration.Duration + (self: DateTime, other: DateTime): Duration.Duration +} = Internal.distanceDuration + +/** + * @since 3.6.0 + * @category comparisons + */ +export const min: { + (that: That): (self: Self) => Self | That + (self: Self, that: That): Self | That +} = Internal.min + +/** + * @since 3.6.0 + * @category comparisons + */ +export const max: { + (that: That): (self: Self) => Self | That + (self: Self, that: That): Self | That +} = Internal.max + +/** + * @since 3.6.0 + * @category comparisons + */ +export const greaterThan: { + (that: DateTime): (self: DateTime) => boolean + (self: DateTime, that: DateTime): boolean +} = Internal.greaterThan + +/** + * @since 3.6.0 + * @category comparisons + */ +export const greaterThanOrEqualTo: { + (that: DateTime): (self: DateTime) => boolean + (self: DateTime, that: DateTime): boolean +} = Internal.greaterThanOrEqualTo + +/** + * @since 3.6.0 + * @category comparisons + */ +export const lessThan: { + (that: DateTime): (self: DateTime) => boolean + (self: DateTime, that: DateTime): boolean +} = Internal.lessThan + +/** + * @since 3.6.0 + * @category comparisons + */ +export const lessThanOrEqualTo: { + (that: DateTime): (self: DateTime) => boolean + (self: DateTime, that: DateTime): boolean +} = Internal.lessThanOrEqualTo + +/** + * @since 3.6.0 + * @category comparisons + */ +export const between: { + (options: { minimum: DateTime; maximum: DateTime }): (self: DateTime) => boolean + (self: DateTime, options: { minimum: DateTime; maximum: DateTime }): boolean +} = Internal.between + +/** + * @since 3.6.0 + * @category comparisons + */ +export const isFuture: (self: DateTime) => Effect.Effect = Internal.isFuture + +/** + * @since 3.6.0 + * @category comparisons + */ +export const unsafeIsFuture: (self: DateTime) => boolean = Internal.unsafeIsFuture + +/** + * @since 3.6.0 + * @category comparisons + */ +export const isPast: (self: DateTime) => Effect.Effect = Internal.isPast + +/** + * @since 3.6.0 + * @category comparisons + */ +export const unsafeIsPast: (self: DateTime) => boolean = Internal.unsafeIsPast + +// ============================================================================= +// conversions +// ============================================================================= + +/** + * Get the UTC `Date` of a `DateTime`. + * + * @since 3.6.0 + * @category conversions + */ +export const toDateUtc: (self: DateTime) => Date = Internal.toDateUtc + +/** + * Convert a `DateTime` to a `Date`, applying the time zone first. + * + * @since 3.6.0 + * @category conversions + */ +export const toDate: (self: DateTime) => Date = Internal.toDate + +/** + * Calculate the time zone offset of a `DateTime.Zoned` in milliseconds. + * + * @since 3.6.0 + * @category conversions + */ +export const zonedOffset: (self: Zoned) => number = Internal.zonedOffset + +/** + * Calculate the time zone offset of a `DateTime` in milliseconds. + * + * The offset is formatted as "±HH:MM". + * + * @since 3.6.0 + * @category conversions + */ +export const zonedOffsetIso: (self: Zoned) => string = Internal.zonedOffsetIso + +/** + * Get the milliseconds since the Unix epoch of a `DateTime`. + * + * @since 3.6.0 + * @category conversions + */ +export const toEpochMillis: (self: DateTime) => number = Internal.toEpochMillis + +/** + * Remove the time aspect of a `DateTime`, first adjusting for the time + * zone. It will return a `DateTime.Utc` only containing the date. + * + * @since 3.6.0 + * @category conversions + * @example + * ```ts + * import { DateTime } from "effect" + * + * // returns "2024-01-01T00:00:00Z" + * DateTime.unsafeMakeZoned("2024-01-01T05:00:00Z", { + * timeZone: "Pacific/Auckland", + * adjustForTimeZone: true + * }).pipe( + * DateTime.removeTime, + * DateTime.formatIso + * ) + * ``` + */ +export const removeTime: (self: DateTime) => Utc = Internal.removeTime + +// ============================================================================= +// parts +// ============================================================================= + +/** + * Get the different parts of a `DateTime` as an object. + * + * The parts will be time zone adjusted. + * + * @since 3.6.0 + * @category parts + */ +export const toParts: (self: DateTime) => DateTime.PartsWithWeekday = Internal.toParts + +/** + * Get the different parts of a `DateTime` as an object. + * + * The parts will be in UTC. + * + * @since 3.6.0 + * @category parts + */ +export const toPartsUtc: (self: DateTime) => DateTime.PartsWithWeekday = Internal.toPartsUtc + +/** + * Get a part of a `DateTime` as a number. + * + * The part will be in the UTC time zone. + * + * @since 3.6.0 + * @category parts + * @example + * ```ts + * import * as assert from "node:assert" + * import { DateTime } from "effect" + * + * const now = DateTime.unsafeMake({ year: 2024 }) + * const year = DateTime.getPartUtc(now, "year") + * assert.strictEqual(year, 2024) + * ``` + */ +export const getPartUtc: { + (part: keyof DateTime.PartsWithWeekday): (self: DateTime) => number + (self: DateTime, part: keyof DateTime.PartsWithWeekday): number +} = Internal.getPartUtc + +/** + * Get a part of a `DateTime` as a number. + * + * The part will be time zone adjusted. + * + * @since 3.6.0 + * @category parts + * @example + * ```ts + * import * as assert from "node:assert" + * import { DateTime } from "effect" + * + * const now = DateTime.unsafeMakeZoned({ year: 2024 }, { timeZone: "Europe/London" }) + * const year = DateTime.getPart(now, "year") + * assert.strictEqual(year, 2024) + * ``` + */ +export const getPart: { + (part: keyof DateTime.PartsWithWeekday): (self: DateTime) => number + (self: DateTime, part: keyof DateTime.PartsWithWeekday): number +} = Internal.getPart + +/** + * Set the different parts of a `DateTime` as an object. + * + * The Date will be time zone adjusted. + * + * @since 3.6.0 + * @category parts + */ +export const setParts: { + (parts: Partial): (self: A) => A + (self: A, parts: Partial): A +} = Internal.setParts + +/** + * Set the different parts of a `DateTime` as an object. + * + * @since 3.6.0 + * @category parts + */ +export const setPartsUtc: { + (parts: Partial): (self: A) => A + (self: A, parts: Partial): A +} = Internal.setPartsUtc + +// ============================================================================= +// current time zone +// ============================================================================= + +/** + * @since 3.11.0 + * @category current time zone + */ +export class CurrentTimeZone extends Context.Tag("effect/DateTime/CurrentTimeZone")() {} + +/** + * Set the time zone of a `DateTime` to the current time zone, which is + * determined by the `CurrentTimeZone` service. + * + * @since 3.6.0 + * @category current time zone + * @example + * ```ts + * import { DateTime, Effect } from "effect" + * + * Effect.gen(function* () { + * const now = yield* DateTime.now + * + * // set the time zone to "Europe/London" + * const zoned = yield* DateTime.setZoneCurrent(now) + * }).pipe(DateTime.withCurrentZoneNamed("Europe/London")) + * ``` + */ +export const setZoneCurrent = (self: DateTime): Effect.Effect => + Effect.map(CurrentTimeZone, (zone) => setZone(self, zone)) + +/** + * Provide the `CurrentTimeZone` to an effect. + * + * @since 3.6.0 + * @category current time zone + * @example + * ```ts + * import { DateTime, Effect } from "effect" + * + * const zone = DateTime.zoneUnsafeMakeNamed("Europe/London") + * + * Effect.gen(function* () { + * const now = yield* DateTime.nowInCurrentZone + * }).pipe(DateTime.withCurrentZone(zone)) + * ``` + */ +export const withCurrentZone: { + ( + zone: TimeZone + ): (effect: Effect.Effect) => Effect.Effect> + (effect: Effect.Effect, zone: TimeZone): Effect.Effect> +} = dual( + 2, + ( + effect: Effect.Effect, + zone: TimeZone + ): Effect.Effect> => Effect.provideService(effect, CurrentTimeZone, zone) +) + +/** + * Provide the `CurrentTimeZone` to an effect, using the system's local time + * zone. + * + * @since 3.6.0 + * @category current time zone + * @example + * ```ts + * import { DateTime, Effect } from "effect" + * + * Effect.gen(function* () { + * // will use the system's local time zone + * const now = yield* DateTime.nowInCurrentZone + * }).pipe(DateTime.withCurrentZoneLocal) + * ``` + */ +export const withCurrentZoneLocal = ( + effect: Effect.Effect +): Effect.Effect> => + Effect.provideServiceEffect(effect, CurrentTimeZone, Effect.sync(zoneMakeLocal)) + +/** + * Provide the `CurrentTimeZone` to an effect, using a offset. + * + * @since 3.6.0 + * @category current time zone + * @example + * ```ts + * import { DateTime, Effect } from "effect" + * + * Effect.gen(function* () { + * // will use the system's local time zone + * const now = yield* DateTime.nowInCurrentZone + * }).pipe(DateTime.withCurrentZoneOffset(3 * 60 * 60 * 1000)) + * ``` + */ +export const withCurrentZoneOffset: { + (offset: number): ( + effect: Effect.Effect + ) => Effect.Effect> + (effect: Effect.Effect, offset: number): Effect.Effect> +} = dual( + 2, + (effect: Effect.Effect, offset: number): Effect.Effect> => + Effect.provideService(effect, CurrentTimeZone, zoneMakeOffset(offset)) +) + +/** + * Provide the `CurrentTimeZone` to an effect using an IANA time zone + * identifier. + * + * If the time zone is invalid, it will fail with an `IllegalArgumentException`. + * + * @since 3.6.0 + * @category current time zone + * @example + * ```ts + * import { DateTime, Effect } from "effect" + * + * Effect.gen(function* () { + * // will use the "Europe/London" time zone + * const now = yield* DateTime.nowInCurrentZone + * }).pipe(DateTime.withCurrentZoneNamed("Europe/London")) + * ``` + */ +export const withCurrentZoneNamed: { + (zone: string): ( + effect: Effect.Effect + ) => Effect.Effect> + ( + effect: Effect.Effect, + zone: string + ): Effect.Effect> +} = dual( + 2, + ( + effect: Effect.Effect, + zone: string + ): Effect.Effect> => + Effect.provideServiceEffect(effect, CurrentTimeZone, zoneMakeNamedEffect(zone)) +) + +/** + * Get the current time as a `DateTime.Zoned`, using the `CurrentTimeZone`. + * + * @since 3.6.0 + * @category current time zone + * @example + * ```ts + * import { DateTime, Effect } from "effect" + * + * Effect.gen(function* () { + * // will use the "Europe/London" time zone + * const now = yield* DateTime.nowInCurrentZone + * }).pipe(DateTime.withCurrentZoneNamed("Europe/London")) + * ``` + */ +export const nowInCurrentZone: Effect.Effect = Effect.flatMap(now, setZoneCurrent) + +// ============================================================================= +// mapping +// ============================================================================= + +/** + * Modify a `DateTime` by applying a function to a cloned `Date` instance. + * + * The `Date` will first have the time zone applied if possible, and then be + * converted back to a `DateTime` within the same time zone. + * + * Supports `disambiguation` when the new wall clock time is ambiguous. + * + * @since 3.6.0 + * @category mapping + */ +export const mutate: { + ( + f: (date: Date) => void, + options?: { + readonly disambiguation?: Disambiguation | undefined + } + ): (self: A) => A + ( + self: A, + f: (date: Date) => void, + options?: { + readonly disambiguation?: Disambiguation | undefined + } + ): A +} = Internal.mutate + +/** + * Modify a `DateTime` by applying a function to a cloned UTC `Date` instance. + * + * @since 3.6.0 + * @category mapping + */ +export const mutateUtc: { + (f: (date: Date) => void): (self: A) => A + (self: A, f: (date: Date) => void): A +} = Internal.mutateUtc + +/** + * Transform a `DateTime` by applying a function to the number of milliseconds + * since the Unix epoch. + * + * @since 3.6.0 + * @category mapping + * @example + * ```ts + * import { DateTime } from "effect" + * + * // add 10 milliseconds + * DateTime.unsafeMake(0).pipe( + * DateTime.mapEpochMillis((millis) => millis + 10) + * ) + * ``` + */ +export const mapEpochMillis: { + (f: (millis: number) => number): (self: A) => A + (self: A, f: (millis: number) => number): A +} = Internal.mapEpochMillis + +/** + * Using the time zone adjusted `Date`, apply a function to the `Date` and + * return the result. + * + * @since 3.6.0 + * @category mapping + * @example + * ```ts + * import { DateTime } from "effect" + * + * // get the time zone adjusted date in milliseconds + * DateTime.unsafeMakeZoned(0, { timeZone: "Europe/London" }).pipe( + * DateTime.withDate((date) => date.getTime()) + * ) + * ``` + */ +export const withDate: { + (f: (date: Date) => A): (self: DateTime) => A + (self: DateTime, f: (date: Date) => A): A +} = Internal.withDate + +/** + * Using the time zone adjusted `Date`, apply a function to the `Date` and + * return the result. + * + * @since 3.6.0 + * @category mapping + * @example + * ```ts + * import { DateTime } from "effect" + * + * // get the date in milliseconds + * DateTime.unsafeMake(0).pipe( + * DateTime.withDateUtc((date) => date.getTime()) + * ) + * ``` + */ +export const withDateUtc: { + (f: (date: Date) => A): (self: DateTime) => A + (self: DateTime, f: (date: Date) => A): A +} = Internal.withDateUtc + +/** + * @since 3.6.0 + * @category mapping + */ +export const match: { + (options: { + readonly onUtc: (_: Utc) => A + readonly onZoned: (_: Zoned) => B + }): (self: DateTime) => A | B + (self: DateTime, options: { + readonly onUtc: (_: Utc) => A + readonly onZoned: (_: Zoned) => B + }): A | B +} = Internal.match + +// ============================================================================= +// math +// ============================================================================= + +/** + * Add the given `Duration` to a `DateTime`. + * + * @since 3.6.0 + * @category math + * @example + * ```ts + * import { DateTime } from "effect" + * + * // add 5 minutes + * DateTime.unsafeMake(0).pipe( + * DateTime.addDuration("5 minutes") + * ) + * ``` + */ +export const addDuration: { + (duration: Duration.DurationInput): (self: A) => A + (self: A, duration: Duration.DurationInput): A +} = Internal.addDuration + +/** + * Subtract the given `Duration` from a `DateTime`. + * + * @since 3.6.0 + * @category math + * @example + * ```ts + * import { DateTime } from "effect" + * + * // subtract 5 minutes + * DateTime.unsafeMake(0).pipe( + * DateTime.subtractDuration("5 minutes") + * ) + * ``` + */ +export const subtractDuration: { + (duration: Duration.DurationInput): (self: A) => A + (self: A, duration: Duration.DurationInput): A +} = Internal.subtractDuration + +/** + * Add the given `amount` of `unit`'s to a `DateTime`. + * + * The time zone is taken into account when adding days, weeks, months, and + * years. + * + * @since 3.6.0 + * @category math + * @example + * ```ts + * import { DateTime } from "effect" + * + * // add 5 minutes + * DateTime.unsafeMake(0).pipe( + * DateTime.add({ minutes: 5 }) + * ) + * ``` + */ +export const add: { + (parts: Partial): (self: A) => A + (self: A, parts: Partial): A +} = Internal.add + +/** + * Subtract the given `amount` of `unit`'s from a `DateTime`. + * + * @since 3.6.0 + * @category math + * @example + * ```ts + * import { DateTime } from "effect" + * + * // subtract 5 minutes + * DateTime.unsafeMake(0).pipe( + * DateTime.subtract({ minutes: 5 }) + * ) + * ``` + */ +export const subtract: { + (parts: Partial): (self: A) => A + (self: A, parts: Partial): A +} = Internal.subtract + +/** + * Converts a `DateTime` to the start of the given `part`. + * + * If the part is `week`, the `weekStartsOn` option can be used to specify the + * day of the week that the week starts on. The default is 0 (Sunday). + * + * @since 3.6.0 + * @category math + * @example + * ```ts + * import { DateTime } from "effect" + * + * // returns "2024-01-01T00:00:00Z" + * DateTime.unsafeMake("2024-01-01T12:00:00Z").pipe( + * DateTime.startOf("day"), + * DateTime.formatIso + * ) + * ``` + */ +export const startOf: { + ( + part: DateTime.UnitSingular, + options?: { readonly weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | undefined } + ): (self: A) => A + ( + self: A, + part: DateTime.UnitSingular, + options?: { readonly weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | undefined } + ): A +} = Internal.startOf + +/** + * Converts a `DateTime` to the end of the given `part`. + * + * If the part is `week`, the `weekStartsOn` option can be used to specify the + * day of the week that the week starts on. The default is 0 (Sunday). + * + * @since 3.6.0 + * @category math + * @example + * ```ts + * import { DateTime } from "effect" + * + * // returns "2024-01-01T23:59:59.999Z" + * DateTime.unsafeMake("2024-01-01T12:00:00Z").pipe( + * DateTime.endOf("day"), + * DateTime.formatIso + * ) + * ``` + */ +export const endOf: { + ( + part: DateTime.UnitSingular, + options?: { readonly weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | undefined } + ): (self: A) => A + ( + self: A, + part: DateTime.UnitSingular, + options?: { readonly weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | undefined } + ): A +} = Internal.endOf + +/** + * Converts a `DateTime` to the nearest given `part`. + * + * If the part is `week`, the `weekStartsOn` option can be used to specify the + * day of the week that the week starts on. The default is 0 (Sunday). + * + * @since 3.6.0 + * @category math + * @example + * ```ts + * import { DateTime } from "effect" + * + * // returns "2024-01-02T00:00:00Z" + * DateTime.unsafeMake("2024-01-01T12:01:00Z").pipe( + * DateTime.nearest("day"), + * DateTime.formatIso + * ) + * ``` + */ +export const nearest: { + ( + part: DateTime.UnitSingular, + options?: { readonly weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | undefined } + ): (self: A) => A + ( + self: A, + part: DateTime.UnitSingular, + options?: { readonly weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | undefined } + ): A +} = Internal.nearest + +// ============================================================================= +// formatting +// ============================================================================= + +/** + * Format a `DateTime` as a string using the `DateTimeFormat` API. + * + * The `timeZone` option is set to the offset of the time zone. + * + * Note: On Node versions < 22, fixed "Offset" zones will set the time zone to + * "UTC" and use the adjusted `Date`. + * + * @since 3.6.0 + * @category formatting + */ +export const format: { + ( + options?: + | Intl.DateTimeFormatOptions & { + readonly locale?: Intl.LocalesArgument + } + | undefined + ): (self: DateTime) => string + ( + self: DateTime, + options?: + | Intl.DateTimeFormatOptions & { + readonly locale?: Intl.LocalesArgument + } + | undefined + ): string +} = Internal.format + +/** + * Format a `DateTime` as a string using the `DateTimeFormat` API. + * + * It will use the system's local time zone & locale. + * + * @since 3.6.0 + * @category formatting + */ +export const formatLocal: { + ( + options?: + | Intl.DateTimeFormatOptions & { + readonly locale?: Intl.LocalesArgument + } + | undefined + ): (self: DateTime) => string + ( + self: DateTime, + options?: + | Intl.DateTimeFormatOptions & { + readonly locale?: Intl.LocalesArgument + } + | undefined + ): string +} = Internal.formatLocal + +/** + * Format a `DateTime` as a string using the `DateTimeFormat` API. + * + * This forces the time zone to be UTC. + * + * @since 3.6.0 + * @category formatting + */ +export const formatUtc: { + ( + options?: + | Intl.DateTimeFormatOptions & { + readonly locale?: Intl.LocalesArgument + } + | undefined + ): (self: DateTime) => string + ( + self: DateTime, + options?: + | Intl.DateTimeFormatOptions & { + readonly locale?: Intl.LocalesArgument + } + | undefined + ): string +} = Internal.formatUtc + +/** + * Format a `DateTime` as a string using the `DateTimeFormat` API. + * + * @since 3.6.0 + * @category formatting + */ +export const formatIntl: { + (format: Intl.DateTimeFormat): (self: DateTime) => string + (self: DateTime, format: Intl.DateTimeFormat): string +} = Internal.formatIntl + +/** + * Format a `DateTime` as a UTC ISO string. + * + * @since 3.6.0 + * @category formatting + */ +export const formatIso: (self: DateTime) => string = Internal.formatIso + +/** + * Format a `DateTime` as a time zone adjusted ISO date string. + * + * @since 3.6.0 + * @category formatting + */ +export const formatIsoDate: (self: DateTime) => string = Internal.formatIsoDate + +/** + * Format a `DateTime` as a UTC ISO date string. + * + * @since 3.6.0 + * @category formatting + */ +export const formatIsoDateUtc: (self: DateTime) => string = Internal.formatIsoDateUtc + +/** + * Format a `DateTime.Zoned` as a ISO string with an offset. + * + * @since 3.6.0 + * @category formatting + */ +export const formatIsoOffset: (self: DateTime) => string = Internal.formatIsoOffset + +/** + * Format a `DateTime.Zoned` as a string. + * + * It uses the format: `YYYY-MM-DDTHH:mm:ss.sss+HH:MM[Time/Zone]`. + * + * @since 3.6.0 + * @category formatting + */ +export const formatIsoZoned: (self: Zoned) => string = Internal.formatIsoZoned + +/** + * Create a Layer from the given time zone. + * + * @since 3.6.0 + * @category current time zone + */ +export const layerCurrentZone = (zone: TimeZone): Layer.Layer => Layer.succeed(CurrentTimeZone, zone) + +/** + * Create a Layer from the given time zone offset. + * + * @since 3.6.0 + * @category current time zone + */ +export const layerCurrentZoneOffset = (offset: number): Layer.Layer => + Layer.succeed(CurrentTimeZone, Internal.zoneMakeOffset(offset)) + +/** + * Create a Layer from the given IANA time zone identifier. + * + * @since 3.6.0 + * @category current time zone + */ +export const layerCurrentZoneNamed = ( + zoneId: string +): Layer.Layer => + Layer.effect(CurrentTimeZone, Internal.zoneMakeNamedEffect(zoneId)) + +/** + * Create a Layer from the systems local time zone. + * + * @since 3.6.0 + * @category current time zone + */ +export const layerCurrentZoneLocal: Layer.Layer = Layer.sync(CurrentTimeZone, zoneMakeLocal) diff --git a/repos/effect/packages/effect/src/DefaultServices.ts b/repos/effect/packages/effect/src/DefaultServices.ts new file mode 100644 index 0000000..f353285 --- /dev/null +++ b/repos/effect/packages/effect/src/DefaultServices.ts @@ -0,0 +1,34 @@ +/** + * @since 2.0.0 + */ +import type * as Clock from "./Clock.js" +import type * as ConfigProvider from "./ConfigProvider.js" +import type * as Console from "./Console.js" +import type * as Context from "./Context.js" +import type * as FiberRef from "./FiberRef.js" +import * as internal from "./internal/defaultServices.js" +import type * as Random from "./Random.js" +import type * as Tracer from "./Tracer.js" + +/** + * @since 2.0.0 + * @category models + */ +export type DefaultServices = + | Clock.Clock + | Console.Console + | Random.Random + | ConfigProvider.ConfigProvider + | Tracer.Tracer + +/** + * @since 2.0.0 + * @category constructors + */ +export const liveServices: Context.Context = internal.liveServices + +/** + * @since 2.0.0 + * @category fiberRefs + */ +export const currentServices: FiberRef.FiberRef> = internal.currentServices diff --git a/repos/effect/packages/effect/src/Deferred.ts b/repos/effect/packages/effect/src/Deferred.ts new file mode 100644 index 0000000..e4feb97 --- /dev/null +++ b/repos/effect/packages/effect/src/Deferred.ts @@ -0,0 +1,301 @@ +/** + * @since 2.0.0 + */ +import type * as Cause from "./Cause.js" +import type * as Effect from "./Effect.js" +import type * as Exit from "./Exit.js" +import type * as FiberId from "./FiberId.js" +import type { LazyArg } from "./Function.js" +import * as core from "./internal/core.js" +import * as internal from "./internal/deferred.js" +import type * as MutableRef from "./MutableRef.js" +import type * as Option from "./Option.js" +import type * as Types from "./Types.js" +import type * as Unify from "./Unify.js" + +/** + * @since 2.0.0 + * @category symbols + */ +export const DeferredTypeId: unique symbol = internal.DeferredTypeId + +/** + * @since 2.0.0 + * @category symbols + */ +export type DeferredTypeId = typeof DeferredTypeId + +/** + * A `Deferred` represents an asynchronous variable that can be set exactly + * once, with the ability for an arbitrary number of fibers to suspend (by + * calling `Deferred.await`) and automatically resume when the variable is set. + * + * `Deferred` can be used for building primitive actions whose completions + * require the coordinated action of multiple fibers, and for building + * higher-level concurrent or asynchronous structures. + * + * @since 2.0.0 + * @category models + */ +export interface Deferred extends Effect.Effect, Deferred.Variance { + /** @internal */ + readonly state: MutableRef.MutableRef> + /** @internal */ + readonly blockingOn: FiberId.FiberId + readonly [Unify.typeSymbol]?: unknown + readonly [Unify.unifySymbol]?: DeferredUnify + readonly [Unify.ignoreSymbol]?: DeferredUnifyIgnore +} + +/** + * @category models + * @since 3.8.0 + */ +export interface DeferredUnify extends Effect.EffectUnify { + Deferred?: () => Extract> +} + +/** + * @category models + * @since 3.8.0 + */ +export interface DeferredUnifyIgnore extends Effect.EffectUnifyIgnore { + Effect?: true +} + +/** + * @since 2.0.0 + */ +export declare namespace Deferred { + /** + * @since 2.0.0 + * @category models + */ + export interface Variance { + readonly [DeferredTypeId]: { + readonly _A: Types.Invariant + readonly _E: Types.Invariant + } + } +} + +/** + * Creates a new `Deferred`. + * + * @since 2.0.0 + * @category constructors + */ +export const make: () => Effect.Effect> = core.deferredMake + +/** + * Creates a new `Deferred` from the specified `FiberId`. + * + * @since 2.0.0 + * @category constructors + */ +export const makeAs: (fiberId: FiberId.FiberId) => Effect.Effect> = core.deferredMakeAs + +const _await: (self: Deferred) => Effect.Effect = core.deferredAwait + +export { + /** + * Retrieves the value of the `Deferred`, suspending the fiber running the + * workflow until the result is available. + * + * @since 2.0.0 + * @category getters + */ + _await as await +} + +/** + * Completes the deferred with the result of the specified effect. If the + * deferred has already been completed, the method will produce false. + * + * Note that `Deferred.completeWith` will be much faster, so consider using + * that if you do not need to memoize the result of the specified effect. + * + * @since 2.0.0 + * @category utils + */ +export const complete: { + (effect: Effect.Effect): (self: Deferred) => Effect.Effect + (self: Deferred, effect: Effect.Effect): Effect.Effect +} = core.deferredComplete + +/** + * Completes the deferred with the result of the specified effect. If the + * deferred has already been completed, the method will produce false. + * + * @since 2.0.0 + * @category utils + */ +export const completeWith: { + (effect: Effect.Effect): (self: Deferred) => Effect.Effect + (self: Deferred, effect: Effect.Effect): Effect.Effect +} = core.deferredCompleteWith + +/** + * Exits the `Deferred` with the specified `Exit` value, which will be + * propagated to all fibers waiting on the value of the `Deferred`. + * + * @since 2.0.0 + * @category utils + */ +export const done: { + (exit: Exit.Exit): (self: Deferred) => Effect.Effect + (self: Deferred, exit: Exit.Exit): Effect.Effect +} = core.deferredDone + +/** + * Fails the `Deferred` with the specified error, which will be propagated to + * all fibers waiting on the value of the `Deferred`. + * + * @since 2.0.0 + * @category utils + */ +export const fail: { + (error: E): (self: Deferred) => Effect.Effect + (self: Deferred, error: E): Effect.Effect +} = core.deferredFail + +/** + * Fails the `Deferred` with the specified error, which will be propagated to + * all fibers waiting on the value of the `Deferred`. + * + * @since 2.0.0 + * @category utils + */ +export const failSync: { + (evaluate: LazyArg): (self: Deferred) => Effect.Effect + (self: Deferred, evaluate: LazyArg): Effect.Effect +} = core.deferredFailSync + +/** + * Fails the `Deferred` with the specified `Cause`, which will be propagated to + * all fibers waiting on the value of the `Deferred`. + * + * @since 2.0.0 + * @category utils + */ +export const failCause: { + (cause: Cause.Cause): (self: Deferred) => Effect.Effect + (self: Deferred, cause: Cause.Cause): Effect.Effect +} = core.deferredFailCause + +/** + * Fails the `Deferred` with the specified `Cause`, which will be propagated to + * all fibers waiting on the value of the `Deferred`. + * + * @since 2.0.0 + * @category utils + */ +export const failCauseSync: { + (evaluate: LazyArg>): (self: Deferred) => Effect.Effect + (self: Deferred, evaluate: LazyArg>): Effect.Effect +} = core.deferredFailCauseSync + +/** + * Kills the `Deferred` with the specified defect, which will be propagated to + * all fibers waiting on the value of the `Deferred`. + * + * @since 2.0.0 + * @category utils + */ +export const die: { + (defect: unknown): (self: Deferred) => Effect.Effect + (self: Deferred, defect: unknown): Effect.Effect +} = core.deferredDie + +/** + * Kills the `Deferred` with the specified defect, which will be propagated to + * all fibers waiting on the value of the `Deferred`. + * + * @since 2.0.0 + * @category utils + */ +export const dieSync: { + (evaluate: LazyArg): (self: Deferred) => Effect.Effect + (self: Deferred, evaluate: LazyArg): Effect.Effect +} = core.deferredDieSync + +/** + * Completes the `Deferred` with interruption. This will interrupt all fibers + * waiting on the value of the `Deferred` with the `FiberId` of the fiber + * calling this method. + * + * @since 2.0.0 + * @category utils + */ +export const interrupt: (self: Deferred) => Effect.Effect = core.deferredInterrupt + +/** + * Completes the `Deferred` with interruption. This will interrupt all fibers + * waiting on the value of the `Deferred` with the specified `FiberId`. + * + * @since 2.0.0 + * @category utils + */ +export const interruptWith: { + (fiberId: FiberId.FiberId): (self: Deferred) => Effect.Effect + (self: Deferred, fiberId: FiberId.FiberId): Effect.Effect +} = core.deferredInterruptWith + +/** + * Returns `true` if this `Deferred` has already been completed with a value or + * an error, `false` otherwise. + * + * @since 2.0.0 + * @category getters + */ +export const isDone: (self: Deferred) => Effect.Effect = core.deferredIsDone + +/** + * Returns a `Some>` from the `Deferred` if this `Deferred` has + * already been completed, `None` otherwise. + * + * @since 2.0.0 + * @category getters + */ +export const poll: ( + self: Deferred +) => Effect.Effect>> = core.deferredPoll + +/** + * Completes the `Deferred` with the specified value. + * + * @since 2.0.0 + * @category utils + */ +export const succeed: { + (value: A): (self: Deferred) => Effect.Effect + (self: Deferred, value: A): Effect.Effect +} = core.deferredSucceed + +/** + * Completes the `Deferred` with the specified lazily evaluated value. + * + * @since 2.0.0 + * @category utils + */ +export const sync: { + (evaluate: LazyArg): (self: Deferred) => Effect.Effect + (self: Deferred, evaluate: LazyArg): Effect.Effect +} = core.deferredSync + +/** + * Unsafely creates a new `Deferred` from the specified `FiberId`. + * + * @since 2.0.0 + * @category unsafe + */ +export const unsafeMake: (fiberId: FiberId.FiberId) => Deferred = core.deferredUnsafeMake + +/** + * Unsafely exits the `Deferred` with the specified `Exit` value, which will be + * propagated to all fibers waiting on the value of the `Deferred`. + * + * @since 2.0.0 + * @category unsafe + */ +export const unsafeDone: (self: Deferred, effect: Effect.Effect) => void = core.deferredUnsafeDone diff --git a/repos/effect/packages/effect/src/Differ.ts b/repos/effect/packages/effect/src/Differ.ts new file mode 100644 index 0000000..8471ce0 --- /dev/null +++ b/repos/effect/packages/effect/src/Differ.ts @@ -0,0 +1,450 @@ +/** + * @since 2.0.0 + */ +import type { Chunk } from "./Chunk.js" +import type { Context } from "./Context.js" +import type { Either } from "./Either.js" +import type { Equal } from "./Equal.js" +import * as Dual from "./Function.js" +import type { HashMap } from "./HashMap.js" +import type { HashSet } from "./HashSet.js" +import * as internal from "./internal/differ.js" +import * as ChunkPatch from "./internal/differ/chunkPatch.js" +import * as ContextPatch from "./internal/differ/contextPatch.js" +import * as HashMapPatch from "./internal/differ/hashMapPatch.js" +import * as HashSetPatch from "./internal/differ/hashSetPatch.js" +import * as OrPatch from "./internal/differ/orPatch.js" +import * as ReadonlyArrayPatch from "./internal/differ/readonlyArrayPatch.js" +import type { Pipeable } from "./Pipeable.js" +import type * as Types from "./Types.js" + +/** + * @since 2.0.0 + * @category symbol + */ +export const TypeId: unique symbol = internal.DifferTypeId as TypeId + +/** + * @since 2.0.0 + * @category symbol + */ +export type TypeId = typeof TypeId + +/** + * A `Differ` knows how to compare an old value and new value of + * type `Value` to produce a patch of type `Patch` that describes the + * differences between those values. A `Differ` also knows how to apply a patch + * to an old value to produce a new value that represents the old value updated + * with the changes described by the patch. + * + * A `Differ` can be used to construct a `FiberRef` supporting compositional + * updates using the `FiberRef.makePatch` constructor. + * + * The `Differ` companion object contains constructors for `Differ` values for + * common data types such as `Chunk`, `HashMap`, and `HashSet``. In addition, + * `Differ`values can be transformed using the `transform` operator and combined + * using the `orElseEither` and `zip` operators. This allows creating `Differ` + * values for arbitrarily complex data types compositionally. + * + * @since 2.0.0 + * @category models + */ +export interface Differ extends Pipeable { + readonly [TypeId]: { + readonly _V: Types.Invariant + readonly _P: Types.Invariant + } + readonly empty: Patch + diff(oldValue: Value, newValue: Value): Patch + combine(first: Patch, second: Patch): Patch + patch(patch: Patch, oldValue: Value): Value +} + +const ChunkPatchTypeId: unique symbol = ChunkPatch.ChunkPatchTypeId as Differ.Chunk.TypeId +const ContextPatchTypeId: unique symbol = ContextPatch.ContextPatchTypeId as Differ.Context.TypeId +const HashMapPatchTypeId: unique symbol = HashMapPatch.HashMapPatchTypeId as Differ.HashMap.TypeId +const HashSetPatchTypeId: unique symbol = HashSetPatch.HashSetPatchTypeId as Differ.HashSet.TypeId +const OrPatchTypeId: unique symbol = OrPatch.OrPatchTypeId as Differ.Or.TypeId +const ReadonlyArrayPatchTypeId: unique symbol = ReadonlyArrayPatch + .ReadonlyArrayPatchTypeId as Differ.ReadonlyArray.TypeId + +/** + * @since 2.0.0 + */ +export declare namespace Differ { + /** + * @since 2.0.0 + */ + export namespace Context { + /** + * @since 2.0.0 + * @category symbol + */ + export type TypeId = typeof ContextPatchTypeId + /** + * A `Patch` describes an update that transforms a `Env` + * to a `Env` as a data structure. This allows combining updates to + * different services in the environment in a compositional way. + * + * @since 2.0.0 + * @category models + */ + export interface Patch extends Equal { + readonly [ContextPatchTypeId]: { + readonly _Input: Types.Contravariant + readonly _Output: Types.Covariant + } + } + } + + /** + * @since 2.0.0 + */ + export namespace Chunk { + /** + * @since 2.0.0 + * @category symbol + */ + export type TypeId = typeof ChunkPatchTypeId + /** + * A patch which describes updates to a chunk of values. + * + * @since 2.0.0 + * @category models + */ + export interface Patch extends Equal { + readonly [ChunkPatchTypeId]: { + readonly _Value: Types.Invariant + readonly _Patch: Types.Invariant + } + } + } + + /** + * @since 2.0.0 + */ + export namespace HashMap { + /** + * @since 2.0.0 + * @category symbol + */ + export type TypeId = typeof HashMapPatchTypeId + /** + * A patch which describes updates to a map of keys and values. + * + * @since 2.0.0 + * @category models + */ + export interface Patch extends Equal { + readonly [HashMapPatchTypeId]: { + readonly _Key: Types.Invariant + readonly _Value: Types.Invariant + readonly _Patch: Types.Invariant + } + } + } + + /** + * @since 2.0.0 + */ + export namespace HashSet { + /** + * @since 2.0.0 + * @category symbol + */ + export type TypeId = typeof HashSetPatchTypeId + /** + * A patch which describes updates to a set of values. + * + * @since 2.0.0 + * @category models + */ + export interface Patch extends Equal { + readonly [HashSetPatchTypeId]: { + readonly _Value: Types.Invariant + } + } + } + + /** + * @since 2.0.0 + */ + export namespace Or { + /** + * @since 2.0.0 + * @category symbol + */ + export type TypeId = typeof OrPatchTypeId + /** + * A patch which describes updates to either one value or another. + * + * @since 2.0.0 + * @category models + */ + export interface Patch extends Equal { + readonly [OrPatchTypeId]: { + readonly _Value: Types.Invariant + readonly _Value2: Types.Invariant + readonly _Patch: Types.Invariant + readonly _Patch2: Types.Invariant + } + } + } + + /** + * @since 2.0.0 + */ + export namespace ReadonlyArray { + /** + * @since 2.0.0 + * @category symbol + */ + export type TypeId = typeof ReadonlyArrayPatchTypeId + /** + * A patch which describes updates to a ReadonlyArray of values. + * + * @since 2.0.0 + * @category models + */ + export interface Patch extends Equal { + readonly [ReadonlyArrayPatchTypeId]: { + readonly _Value: Types.Invariant + readonly _Patch: Types.Invariant + } + } + } +} + +/** + * An empty patch that describes no changes. + * + * @since 2.0.0 + * @category patch + */ +export const empty: (self: Differ) => Patch = ( + self +) => self.empty + +/** + * @since 2.0.0 + * @category patch + */ +export const diff: { + (oldValue: Value, newValue: Value): ( + self: Differ + ) => Patch + ( + self: Differ, + oldValue: Value, + newValue: Value + ): Patch +} = Dual.dual( + 3, + ( + self: Differ, + oldValue: Value, + newValue: Value + ): Patch => self.diff(oldValue, newValue) +) + +/** + * Combines two patches to produce a new patch that describes the updates of + * the first patch and then the updates of the second patch. The combine + * operation should be associative. In addition, if the combine operation is + * commutative then joining multiple fibers concurrently will result in + * deterministic `FiberRef` values. + * + * @since 2.0.0 + * @category patch + */ +export const combine: { + (first: Patch, second: Patch): ( + self: Differ + ) => Patch + ( + self: Differ, + first: Patch, + second: Patch + ): Patch +} = Dual.dual( + 3, + ( + self: Differ, + first: Patch, + second: Patch + ): Patch => self.combine(first, second) +) + +/** + * Applies a patch to an old value to produce a new value that is equal to the + * old value with the updates described by the patch. + * + * @since 2.0.0 + * @category patch + */ +export const patch: { + (patch: Patch, oldValue: Value): ( + self: Differ + ) => Value + ( + self: Differ, + patch: Patch, + oldValue: Value + ): Value +} = Dual.dual( + 3, + ( + self: Differ, + patch: Patch, + oldValue: Value + ): Value => self.patch(patch, oldValue) +) + +/** + * Constructs a new `Differ`. + * + * @since 2.0.0 + * @category constructors + */ +export const make: (params: { + readonly empty: Patch + readonly diff: (oldValue: Value, newValue: Value) => Patch + readonly combine: (first: Patch, second: Patch) => Patch + readonly patch: (patch: Patch, oldValue: Value) => Value +}) => Differ = internal.make + +/** + * Constructs a differ that knows how to diff `Env` values. + * + * @since 2.0.0 + * @category constructors + */ +export const environment: () => Differ< + Context, + Differ.Context.Patch +> = internal.environment + +/** + * Constructs a differ that knows how to diff a `Chunk` of values given a + * differ that knows how to diff the values. + * + * @since 2.0.0 + * @category constructors + */ +export const chunk: ( + differ: Differ +) => Differ, Differ.Chunk.Patch> = internal.chunk + +/** + * Constructs a differ that knows how to diff a `HashMap` of keys and values given + * a differ that knows how to diff the values. + * + * @since 2.0.0 + * @category constructors + */ +export const hashMap: ( + differ: Differ +) => Differ, Differ.HashMap.Patch> = internal.hashMap + +/** + * Constructs a differ that knows how to diff a `HashSet` of values. + * + * @since 2.0.0 + * @category constructors + */ +export const hashSet: () => Differ< + HashSet, + Differ.HashSet.Patch +> = internal.hashSet + +/** + * Combines this differ and the specified differ to produce a differ that + * knows how to diff the sum of their values. + * + * @since 2.0.0 + */ +export const orElseEither: { + (that: Differ): ( + self: Differ + ) => Differ< + Either, + Differ.Or.Patch + > + ( + self: Differ, + that: Differ + ): Differ< + Either, + Differ.Or.Patch + > +} = internal.orElseEither + +/** + * Constructs a differ that knows how to diff a `ReadonlyArray` of values. + * + * @since 2.0.0 + * @category constructors + */ +export const readonlyArray: ( + differ: Differ +) => Differ, Differ.ReadonlyArray.Patch> = internal.readonlyArray + +/** + * Transforms the type of values that this differ knows how to differ using + * the specified functions that map the new and old value types to each other. + * + * @since 2.0.0 + */ +export const transform: { + (options: { + readonly toNew: (value: Value) => Value2 + readonly toOld: (value: Value2) => Value + }): (self: Differ) => Differ + ( + self: Differ, + options: { + readonly toNew: (value: Value) => Value2 + readonly toOld: (value: Value2) => Value + } + ): Differ +} = internal.transform + +/** + * Constructs a differ that just diffs two values by returning a function that + * sets the value to the new value. This differ does not support combining + * multiple updates to the value compositionally and should only be used when + * there is no compositional way to update them. + * + * @since 2.0.0 + */ +export const update: () => Differ A> = internal.update + +/** + * A variant of `update` that allows specifying the function that will be used + * to combine old values with new values. + * + * @since 2.0.0 + */ +export const updateWith: (f: (x: A, y: A) => A) => Differ A> = internal.updateWith + +/** + * Combines this differ and the specified differ to produce a new differ that + * knows how to diff the product of their values. + * + * @since 2.0.0 + */ +export const zip: { + (that: Differ): ( + self: Differ + ) => Differ< + readonly [Value, Value2], // readonly because invariant + readonly [Patch, Patch2] // readonly because invariant + > + ( + self: Differ, + that: Differ + ): Differ< + readonly [Value, Value2], // readonly because invariant + readonly [Patch, Patch2] // readonly because invariant + > +} = internal.zip diff --git a/repos/effect/packages/effect/src/Duration.ts b/repos/effect/packages/effect/src/Duration.ts new file mode 100644 index 0000000..677138b --- /dev/null +++ b/repos/effect/packages/effect/src/Duration.ts @@ -0,0 +1,1000 @@ +/** + * @since 2.0.0 + */ +import * as Equal from "./Equal.js" +import type * as equivalence from "./Equivalence.js" +import { dual } from "./Function.js" +import * as Hash from "./Hash.js" +import type { Inspectable } from "./Inspectable.js" +import { NodeInspectSymbol } from "./Inspectable.js" +import * as Option from "./Option.js" +import * as order from "./Order.js" +import type { Pipeable } from "./Pipeable.js" +import { pipeArguments } from "./Pipeable.js" +import { hasProperty, isBigInt, isNumber, isString } from "./Predicate.js" + +const TypeId: unique symbol = Symbol.for("effect/Duration") + +const bigint0 = BigInt(0) +const bigint24 = BigInt(24) +const bigint60 = BigInt(60) +const bigint1e3 = BigInt(1_000) +const bigint1e6 = BigInt(1_000_000) +const bigint1e9 = BigInt(1_000_000_000) + +/** + * @since 2.0.0 + * @category symbol + */ +export type TypeId = typeof TypeId + +/** + * @since 2.0.0 + * @category models + */ +export interface Duration extends Equal.Equal, Pipeable, Inspectable { + readonly [TypeId]: TypeId + readonly value: DurationValue +} +/** + * @since 2.0.0 + * @category models + */ +export type DurationValue = + | { + readonly _tag: "Millis" + readonly millis: number + } + | { + readonly _tag: "Nanos" + readonly nanos: bigint + } + | { + readonly _tag: "Infinity" + } + +/** + * @since 2.0.0 + * @category models + */ +export type Unit = + | "nano" + | "nanos" + | "micro" + | "micros" + | "milli" + | "millis" + | "second" + | "seconds" + | "minute" + | "minutes" + | "hour" + | "hours" + | "day" + | "days" + | "week" + | "weeks" + +/** + * @since 2.0.0 + * @category models + */ +export type DurationInput = + | Duration + | number // millis + | bigint // nanos + | readonly [seconds: number, nanos: number] + | `${number} ${Unit}` + +const DURATION_REGEX = /^(-?\d+(?:\.\d+)?)\s+(nanos?|micros?|millis?|seconds?|minutes?|hours?|days?|weeks?)$/ + +/** + * @since 2.0.0 + */ +export const decode = (input: DurationInput): Duration => { + if (isDuration(input)) { + return input + } else if (isNumber(input)) { + return millis(input) + } else if (isBigInt(input)) { + return nanos(input) + } else if (Array.isArray(input) && input.length === 2 && input.every(isNumber)) { + if (input[0] === -Infinity || input[1] === -Infinity || Number.isNaN(input[0]) || Number.isNaN(input[1])) { + return zero + } + + if (input[0] === Infinity || input[1] === Infinity) { + return infinity + } + + return nanos(BigInt(Math.round(input[0] * 1_000_000_000)) + BigInt(Math.round(input[1]))) + } else if (isString(input)) { + const match = DURATION_REGEX.exec(input) + if (match) { + const [_, valueStr, unit] = match + const value = Number(valueStr) + switch (unit) { + case "nano": + case "nanos": + return nanos(BigInt(valueStr)) + case "micro": + case "micros": + return micros(BigInt(valueStr)) + case "milli": + case "millis": + return millis(value) + case "second": + case "seconds": + return seconds(value) + case "minute": + case "minutes": + return minutes(value) + case "hour": + case "hours": + return hours(value) + case "day": + case "days": + return days(value) + case "week": + case "weeks": + return weeks(value) + } + } + } + throw new Error("Invalid DurationInput") +} + +/** + * @since 2.5.0 + */ +export const decodeUnknown: (u: unknown) => Option.Option = Option.liftThrowable(decode) as any + +const zeroValue: DurationValue = { _tag: "Millis", millis: 0 } +const infinityValue: DurationValue = { _tag: "Infinity" } + +const DurationProto: Omit = { + [TypeId]: TypeId, + [Hash.symbol](this: Duration) { + return Hash.cached(this, Hash.structure(this.value)) + }, + [Equal.symbol](this: Duration, that: unknown): boolean { + return isDuration(that) && equals(this, that) + }, + toString(this: Duration) { + return `Duration(${format(this)})` + }, + toJSON(this: Duration) { + switch (this.value._tag) { + case "Millis": + return { _id: "Duration", _tag: "Millis", millis: this.value.millis } + case "Nanos": + return { _id: "Duration", _tag: "Nanos", hrtime: toHrTime(this) } + case "Infinity": + return { _id: "Duration", _tag: "Infinity" } + } + }, + [NodeInspectSymbol]() { + return this.toJSON() + }, + pipe() { + return pipeArguments(this, arguments) + } +} as const + +const make = (input: number | bigint): Duration => { + const duration = Object.create(DurationProto) + if (isNumber(input)) { + if (isNaN(input) || input <= 0) { + duration.value = zeroValue + } else if (!Number.isFinite(input)) { + duration.value = infinityValue + } else if (!Number.isInteger(input)) { + duration.value = { _tag: "Nanos", nanos: BigInt(Math.round(input * 1_000_000)) } + } else { + duration.value = { _tag: "Millis", millis: input } + } + } else if (input <= bigint0) { + duration.value = zeroValue + } else { + duration.value = { _tag: "Nanos", nanos: input } + } + return duration +} + +/** + * @since 2.0.0 + * @category guards + */ +export const isDuration = (u: unknown): u is Duration => hasProperty(u, TypeId) + +/** + * @since 2.0.0 + * @category guards + */ +export const isFinite = (self: Duration): boolean => self.value._tag !== "Infinity" + +/** + * @since 3.5.0 + * @category guards + */ +export const isZero = (self: Duration): boolean => { + switch (self.value._tag) { + case "Millis": { + return self.value.millis === 0 + } + case "Nanos": { + return self.value.nanos === bigint0 + } + case "Infinity": { + return false + } + } +} + +/** + * @since 2.0.0 + * @category constructors + */ +export const zero: Duration = make(0) + +/** + * @since 2.0.0 + * @category constructors + */ +export const infinity: Duration = make(Infinity) + +/** + * @since 2.0.0 + * @category constructors + */ +export const nanos = (nanos: bigint): Duration => make(nanos) + +/** + * @since 2.0.0 + * @category constructors + */ +export const micros = (micros: bigint): Duration => make(micros * bigint1e3) + +/** + * @since 2.0.0 + * @category constructors + */ +export const millis = (millis: number): Duration => make(millis) + +/** + * @since 2.0.0 + * @category constructors + */ +export const seconds = (seconds: number): Duration => make(seconds * 1000) + +/** + * @since 2.0.0 + * @category constructors + */ +export const minutes = (minutes: number): Duration => make(minutes * 60_000) + +/** + * @since 2.0.0 + * @category constructors + */ +export const hours = (hours: number): Duration => make(hours * 3_600_000) + +/** + * @since 2.0.0 + * @category constructors + */ +export const days = (days: number): Duration => make(days * 86_400_000) + +/** + * @since 2.0.0 + * @category constructors + */ +export const weeks = (weeks: number): Duration => make(weeks * 604_800_000) + +/** + * @since 2.0.0 + * @category getters + */ +export const toMillis = (self: DurationInput): number => + match(self, { + onMillis: (millis) => millis, + onNanos: (nanos) => Number(nanos) / 1_000_000 + }) + +/** + * @since 2.0.0 + * @category getters + */ +export const toSeconds = (self: DurationInput): number => + match(self, { + onMillis: (millis) => millis / 1_000, + onNanos: (nanos) => Number(nanos) / 1_000_000_000 + }) + +/** + * @since 3.8.0 + * @category getters + */ +export const toMinutes = (self: DurationInput): number => + match(self, { + onMillis: (millis) => millis / 60_000, + onNanos: (nanos) => Number(nanos) / 60_000_000_000 + }) + +/** + * @since 3.8.0 + * @category getters + */ +export const toHours = (self: DurationInput): number => + match(self, { + onMillis: (millis) => millis / 3_600_000, + onNanos: (nanos) => Number(nanos) / 3_600_000_000_000 + }) + +/** + * @since 3.8.0 + * @category getters + */ +export const toDays = (self: DurationInput): number => + match(self, { + onMillis: (millis) => millis / 86_400_000, + onNanos: (nanos) => Number(nanos) / 86_400_000_000_000 + }) + +/** + * @since 3.8.0 + * @category getters + */ +export const toWeeks = (self: DurationInput): number => + match(self, { + onMillis: (millis) => millis / 604_800_000, + onNanos: (nanos) => Number(nanos) / 604_800_000_000_000 + }) + +/** + * Get the duration in nanoseconds as a bigint. + * + * If the duration is infinite, returns `Option.none()` + * + * @since 2.0.0 + * @category getters + */ +export const toNanos = (self: DurationInput): Option.Option => { + const _self = decode(self) + switch (_self.value._tag) { + case "Infinity": + return Option.none() + case "Nanos": + return Option.some(_self.value.nanos) + case "Millis": + return Option.some(BigInt(Math.round(_self.value.millis * 1_000_000))) + } +} + +/** + * Get the duration in nanoseconds as a bigint. + * + * If the duration is infinite, it throws an error. + * + * @since 2.0.0 + * @category getters + */ +export const unsafeToNanos = (self: DurationInput): bigint => { + const _self = decode(self) + switch (_self.value._tag) { + case "Infinity": + throw new Error("Cannot convert infinite duration to nanos") + case "Nanos": + return _self.value.nanos + case "Millis": + return BigInt(Math.round(_self.value.millis * 1_000_000)) + } +} + +/** + * @since 2.0.0 + * @category getters + */ +export const toHrTime = (self: DurationInput): [seconds: number, nanos: number] => { + const _self = decode(self) + switch (_self.value._tag) { + case "Infinity": + return [Infinity, 0] + case "Nanos": + return [ + Number(_self.value.nanos / bigint1e9), + Number(_self.value.nanos % bigint1e9) + ] + case "Millis": + return [ + Math.floor(_self.value.millis / 1000), + Math.round((_self.value.millis % 1000) * 1_000_000) + ] + } +} + +/** + * @since 2.0.0 + * @category pattern matching + */ +export const match: { + ( + options: { + readonly onMillis: (millis: number) => A + readonly onNanos: (nanos: bigint) => B + } + ): (self: DurationInput) => A | B + ( + self: DurationInput, + options: { + readonly onMillis: (millis: number) => A + readonly onNanos: (nanos: bigint) => B + } + ): A | B +} = dual(2, ( + self: DurationInput, + options: { + readonly onMillis: (millis: number) => A + readonly onNanos: (nanos: bigint) => B + } +): A | B => { + const _self = decode(self) + switch (_self.value._tag) { + case "Nanos": + return options.onNanos(_self.value.nanos) + case "Infinity": + return options.onMillis(Infinity) + case "Millis": + return options.onMillis(_self.value.millis) + } +}) + +/** + * @since 2.0.0 + * @category pattern matching + */ +export const matchWith: { + ( + that: DurationInput, + options: { + readonly onMillis: (self: number, that: number) => A + readonly onNanos: (self: bigint, that: bigint) => B + } + ): (self: DurationInput) => A | B + ( + self: DurationInput, + that: DurationInput, + options: { + readonly onMillis: (self: number, that: number) => A + readonly onNanos: (self: bigint, that: bigint) => B + } + ): A | B +} = dual(3, ( + self: DurationInput, + that: DurationInput, + options: { + readonly onMillis: (self: number, that: number) => A + readonly onNanos: (self: bigint, that: bigint) => B + } +): A | B => { + const _self = decode(self) + const _that = decode(that) + if (_self.value._tag === "Infinity" || _that.value._tag === "Infinity") { + return options.onMillis( + toMillis(_self), + toMillis(_that) + ) + } else if (_self.value._tag === "Nanos" || _that.value._tag === "Nanos") { + const selfNanos = _self.value._tag === "Nanos" ? + _self.value.nanos : + BigInt(Math.round(_self.value.millis * 1_000_000)) + const thatNanos = _that.value._tag === "Nanos" ? + _that.value.nanos : + BigInt(Math.round(_that.value.millis * 1_000_000)) + return options.onNanos(selfNanos, thatNanos) + } + + return options.onMillis( + _self.value.millis, + _that.value.millis + ) +}) + +/** + * @category instances + * @since 2.0.0 + */ +export const Order: order.Order = order.make((self, that) => + matchWith(self, that, { + onMillis: (self, that) => (self < that ? -1 : self > that ? 1 : 0), + onNanos: (self, that) => (self < that ? -1 : self > that ? 1 : 0) + }) +) + +/** + * Checks if a `Duration` is between a `minimum` and `maximum` value. + * + * @category predicates + * @since 2.0.0 + */ +export const between: { + (options: { + minimum: DurationInput + maximum: DurationInput + }): (self: DurationInput) => boolean + (self: DurationInput, options: { + minimum: DurationInput + maximum: DurationInput + }): boolean +} = order.between(order.mapInput(Order, decode)) + +/** + * @category instances + * @since 2.0.0 + */ +export const Equivalence: equivalence.Equivalence = (self, that) => + matchWith(self, that, { + onMillis: (self, that) => self === that, + onNanos: (self, that) => self === that + }) + +const _min = order.min(Order) + +/** + * @since 2.0.0 + */ +export const min: { + (that: DurationInput): (self: DurationInput) => Duration + (self: DurationInput, that: DurationInput): Duration +} = dual(2, (self: DurationInput, that: DurationInput): Duration => _min(decode(self), decode(that))) + +const _max = order.max(Order) + +/** + * @since 2.0.0 + * @category order + */ +export const max: { + (that: DurationInput): (self: DurationInput) => Duration + (self: DurationInput, that: DurationInput): Duration +} = dual(2, (self: DurationInput, that: DurationInput): Duration => _max(decode(self), decode(that))) + +const _clamp = order.clamp(Order) + +/** + * @since 2.0.0 + * @category order + */ +export const clamp: { + (options: { + minimum: DurationInput + maximum: DurationInput + }): (self: DurationInput) => Duration + (self: DurationInput, options: { + minimum: DurationInput + maximum: DurationInput + }): Duration +} = dual( + 2, + (self: DurationInput, options: { + minimum: DurationInput + maximum: DurationInput + }): Duration => + _clamp(decode(self), { + minimum: decode(options.minimum), + maximum: decode(options.maximum) + }) +) + +/** + * @since 2.4.19 + * @category math + */ +export const divide: { + (by: number): (self: DurationInput) => Option.Option + (self: DurationInput, by: number): Option.Option +} = dual( + 2, + (self: DurationInput, by: number): Option.Option => + match(self, { + onMillis: (millis) => { + if (by === 0 || isNaN(by) || !Number.isFinite(by)) { + return Option.none() + } + return Option.some(make(millis / by)) + }, + onNanos: (nanos) => { + if (isNaN(by) || by <= 0 || !Number.isFinite(by)) { + return Option.none() + } + try { + return Option.some(make(nanos / BigInt(by))) + } catch { + return Option.none() + } + } + }) +) + +/** + * @since 2.4.19 + * @category math + */ +export const unsafeDivide: { + (by: number): (self: DurationInput) => Duration + (self: DurationInput, by: number): Duration +} = dual( + 2, + (self: DurationInput, by: number): Duration => + match(self, { + onMillis: (millis) => make(millis / by), + onNanos: (nanos) => { + if (isNaN(by) || by < 0 || Object.is(by, -0)) { + return zero + } else if (Object.is(by, 0) || !Number.isFinite(by)) { + return infinity + } + return make(nanos / BigInt(by)) + } + }) +) + +/** + * @since 2.0.0 + * @category math + */ +export const times: { + (times: number): (self: DurationInput) => Duration + (self: DurationInput, times: number): Duration +} = dual( + 2, + (self: DurationInput, times: number): Duration => + match(self, { + onMillis: (millis) => make(millis * times), + onNanos: (nanos) => make(nanos * BigInt(times)) + }) +) + +/** + * @since 2.0.0 + * @category math + */ +export const subtract: { + (that: DurationInput): (self: DurationInput) => Duration + (self: DurationInput, that: DurationInput): Duration +} = dual( + 2, + (self: DurationInput, that: DurationInput): Duration => + matchWith(self, that, { + onMillis: (self, that) => make(self - that), + onNanos: (self, that) => make(self - that) + }) +) + +/** + * @since 2.0.0 + * @category math + */ +export const sum: { + (that: DurationInput): (self: DurationInput) => Duration + (self: DurationInput, that: DurationInput): Duration +} = dual( + 2, + (self: DurationInput, that: DurationInput): Duration => + matchWith(self, that, { + onMillis: (self, that) => make(self + that), + onNanos: (self, that) => make(self + that) + }) +) + +/** + * @since 2.0.0 + * @category predicates + */ +export const lessThan: { + (that: DurationInput): (self: DurationInput) => boolean + (self: DurationInput, that: DurationInput): boolean +} = dual( + 2, + (self: DurationInput, that: DurationInput): boolean => + matchWith(self, that, { + onMillis: (self, that) => self < that, + onNanos: (self, that) => self < that + }) +) + +/** + * @since 2.0.0 + * @category predicates + */ +export const lessThanOrEqualTo: { + (that: DurationInput): (self: DurationInput) => boolean + (self: DurationInput, that: DurationInput): boolean +} = dual( + 2, + (self: DurationInput, that: DurationInput): boolean => + matchWith(self, that, { + onMillis: (self, that) => self <= that, + onNanos: (self, that) => self <= that + }) +) + +/** + * @since 2.0.0 + * @category predicates + */ +export const greaterThan: { + (that: DurationInput): (self: DurationInput) => boolean + (self: DurationInput, that: DurationInput): boolean +} = dual( + 2, + (self: DurationInput, that: DurationInput): boolean => + matchWith(self, that, { + onMillis: (self, that) => self > that, + onNanos: (self, that) => self > that + }) +) + +/** + * @since 2.0.0 + * @category predicates + */ +export const greaterThanOrEqualTo: { + (that: DurationInput): (self: DurationInput) => boolean + (self: DurationInput, that: DurationInput): boolean +} = dual( + 2, + (self: DurationInput, that: DurationInput): boolean => + matchWith(self, that, { + onMillis: (self, that) => self >= that, + onNanos: (self, that) => self >= that + }) +) + +/** + * @since 2.0.0 + * @category predicates + */ +export const equals: { + (that: DurationInput): (self: DurationInput) => boolean + (self: DurationInput, that: DurationInput): boolean +} = dual(2, (self: DurationInput, that: DurationInput): boolean => Equivalence(decode(self), decode(that))) + +/** + * Converts a `Duration` to its parts. + * + * @since 3.8.0 + * @category conversions + */ +export const parts = (self: DurationInput): { + days: number + hours: number + minutes: number + seconds: number + millis: number + nanos: number +} => { + const duration = decode(self) + if (duration.value._tag === "Infinity") { + return { + days: Infinity, + hours: Infinity, + minutes: Infinity, + seconds: Infinity, + millis: Infinity, + nanos: Infinity + } + } + + const nanos = unsafeToNanos(duration) + const ms = nanos / bigint1e6 + const sec = ms / bigint1e3 + const min = sec / bigint60 + const hr = min / bigint60 + const days = hr / bigint24 + + return { + days: Number(days), + hours: Number(hr % bigint24), + minutes: Number(min % bigint60), + seconds: Number(sec % bigint60), + millis: Number(ms % bigint1e3), + nanos: Number(nanos % bigint1e6) + } +} + +/** + * Converts a `Duration` to a human readable string. + * + * @since 2.0.0 + * @category conversions + * @example + * ```ts + * import { Duration } from "effect" + * + * Duration.format(Duration.millis(1000)) // "1s" + * Duration.format(Duration.millis(1001)) // "1s 1ms" + * ``` + */ +export const format = (self: DurationInput): string => { + const duration = decode(self) + if (duration.value._tag === "Infinity") { + return "Infinity" + } + if (isZero(duration)) { + return "0" + } + + const fragments = parts(duration) + const pieces = [] + if (fragments.days !== 0) { + pieces.push(`${fragments.days}d`) + } + + if (fragments.hours !== 0) { + pieces.push(`${fragments.hours}h`) + } + + if (fragments.minutes !== 0) { + pieces.push(`${fragments.minutes}m`) + } + + if (fragments.seconds !== 0) { + pieces.push(`${fragments.seconds}s`) + } + + if (fragments.millis !== 0) { + pieces.push(`${fragments.millis}ms`) + } + + if (fragments.nanos !== 0) { + pieces.push(`${fragments.nanos}ns`) + } + + return pieces.join(" ") +} + +/** + * Formats a Duration into an ISO8601 duration string. + * + * Months are assumed to be 30 days and years are assumed to be 365 days. + * + * Milliseconds and nanoseconds are expressed as fractional seconds. + * + * @example + * ```ts + * import { Duration } from "effect" + * + * Duration.unsafeFormatIso(Duration.days(1)) // => "P1D" + * Duration.unsafeFormatIso(Duration.minutes(90)) // => "PT1H30M" + * Duration.unsafeFormatIso(Duration.millis(1500)) // => "PT1.5S" + * ``` + * + * @throws `RangeError` If the duration is not finite. + * + * @since 3.13.0 + * @category conversions + */ +export const unsafeFormatIso = (self: DurationInput): string => { + const duration = decode(self) + if (!isFinite(duration)) { + throw new RangeError("Cannot format infinite duration") + } + + const fragments = [] + const { + days, + hours, + millis, + minutes, + nanos, + seconds + } = parts(duration) + + let rest = days + if (rest >= 365) { + const years = Math.floor(rest / 365) + rest %= 365 + fragments.push(`${years}Y`) + } + + if (rest >= 30) { + const months = Math.floor(rest / 30) + rest %= 30 + fragments.push(`${months}M`) + } + + if (rest >= 7) { + const weeks = Math.floor(rest / 7) + rest %= 7 + fragments.push(`${weeks}W`) + } + + if (rest > 0) { + fragments.push(`${rest}D`) + } + + if (hours !== 0 || minutes !== 0 || seconds !== 0 || millis !== 0 || nanos !== 0) { + fragments.push("T") + + if (hours !== 0) { + fragments.push(`${hours}H`) + } + + if (minutes !== 0) { + fragments.push(`${minutes}M`) + } + + if (seconds !== 0 || millis !== 0 || nanos !== 0) { + const total = BigInt(seconds) * bigint1e9 + BigInt(millis) * bigint1e6 + BigInt(nanos) + const str = (Number(total) / 1e9).toFixed(9).replace(/\.?0+$/, "") + fragments.push(`${str}S`) + } + } + + return `P${fragments.join("") || "T0S"}` +} + +/** + * Formats a Duration into an ISO8601 duration string. + * + * Months are assumed to be 30 days and years are assumed to be 365 days. + * + * Returns `Option.none()` if the duration is infinite. + * + * @example + * ```ts + * import { Duration, Option } from "effect" + * + * Duration.formatIso(Duration.days(1)) // => Option.some("P1D") + * Duration.formatIso(Duration.minutes(90)) // => Option.some("PT1H30M") + * Duration.formatIso(Duration.millis(1500)) // => Option.some("PT1.5S") + * Duration.formatIso(Duration.infinity) // => Option.none() + * ``` + * + * @since 3.13.0 + * @category conversions + */ +export const formatIso = (self: DurationInput): Option.Option => { + const duration = decode(self) + return isFinite(duration) ? Option.some(unsafeFormatIso(duration)) : Option.none() +} + +/** + * Parses an ISO8601 duration string into a `Duration`. + * + * Months are assumed to be 30 days and years are assumed to be 365 days. + * + * @example + * ```ts + * import { Duration, Option } from "effect" + * + * Duration.fromIso("P1D") // => Option.some(Duration.days(1)) + * Duration.fromIso("PT1H") // => Option.some(Duration.hours(1)) + * Duration.fromIso("PT1M") // => Option.some(Duration.minutes(1)) + * Duration.fromIso("PT1.5S") // => Option.some(Duration.seconds(1.5)) + * ``` + * + * @since 3.13.0 + * @category conversions + */ +export const fromIso = (iso: string): Option.Option => { + const result = DURATION_ISO_REGEX.exec(iso) + if (result == null) { + return Option.none() + } + + const [years, months, weeks, days, hours, mins, secs] = result.slice(1, 8).map((_) => _ ? Number(_) : 0) + const value = years * 365 * 24 * 60 * 60 + + months * 30 * 24 * 60 * 60 + + weeks * 7 * 24 * 60 * 60 + + days * 24 * 60 * 60 + + hours * 60 * 60 + + mins * 60 + + secs + + return Option.some(seconds(value)) +} + +const DURATION_ISO_REGEX = + /^P(?!$)(?:(\d+)Y)?(?:(\d+)M)?(?:(\d+)W)?(?:(\d+)D)?(?:T(?!$)(?:(\d+)H)?(?:(\d+)M)?(?:(\d+(?:\.\d+)?)S)?)?$/ diff --git a/repos/effect/packages/effect/src/Effect.ts b/repos/effect/packages/effect/src/Effect.ts new file mode 100644 index 0000000..d9143c2 --- /dev/null +++ b/repos/effect/packages/effect/src/Effect.ts @@ -0,0 +1,14815 @@ +/** + * @since 2.0.0 + */ +import type * as RA from "./Array.js" +import type * as Cause from "./Cause.js" +import type * as Chunk from "./Chunk.js" +import type * as Clock from "./Clock.js" +import type { ConfigProvider } from "./ConfigProvider.js" +import type { Console } from "./Console.js" +import type * as Context from "./Context.js" +import type * as Deferred from "./Deferred.js" +import type * as Duration from "./Duration.js" +import type * as Either from "./Either.js" +import type { Equivalence } from "./Equivalence.js" +import type { ExecutionPlan } from "./ExecutionPlan.js" +import type { ExecutionStrategy } from "./ExecutionStrategy.js" +import type * as Exit from "./Exit.js" +import type * as Fiber from "./Fiber.js" +import type * as FiberId from "./FiberId.js" +import type * as FiberRef from "./FiberRef.js" +import type * as FiberRefs from "./FiberRefs.js" +import type * as FiberRefsPatch from "./FiberRefsPatch.js" +import type * as FiberStatus from "./FiberStatus.js" +import type { LazyArg } from "./Function.js" +import { dual } from "./Function.js" +import type * as HashMap from "./HashMap.js" +import type * as HashSet from "./HashSet.js" +import type { TypeLambda } from "./HKT.js" +import * as internalCause from "./internal/cause.js" +import * as console_ from "./internal/console.js" +import { TagProto } from "./internal/context.js" +import * as effect from "./internal/core-effect.js" +import * as core from "./internal/core.js" +import * as defaultServices from "./internal/defaultServices.js" +import * as circular from "./internal/effect/circular.js" +import * as internalExecutionPlan from "./internal/executionPlan.js" +import * as fiberRuntime from "./internal/fiberRuntime.js" +import * as layer from "./internal/layer.js" +import * as option_ from "./internal/option.js" +import * as query from "./internal/query.js" +import * as runtime_ from "./internal/runtime.js" +import * as schedule_ from "./internal/schedule.js" +import * as internalTracer from "./internal/tracer.js" +import type * as Layer from "./Layer.js" +import type * as LogLevel from "./LogLevel.js" +import type * as ManagedRuntime from "./ManagedRuntime.js" +import type * as Metric from "./Metric.js" +import type * as MetricLabel from "./MetricLabel.js" +import type * as Option from "./Option.js" +import type { Pipeable } from "./Pipeable.js" +import type { Predicate, Refinement } from "./Predicate.js" +import * as Random from "./Random.js" +import type * as Ref from "./Ref.js" +import * as Request from "./Request.js" +import type { RequestBlock } from "./RequestBlock.js" +import type { RequestResolver } from "./RequestResolver.js" +import type * as Runtime from "./Runtime.js" +import type * as RuntimeFlags from "./RuntimeFlags.js" +import type * as RuntimeFlagsPatch from "./RuntimeFlagsPatch.js" +import type * as Schedule from "./Schedule.js" +import * as Scheduler from "./Scheduler.js" +import type * as Scope from "./Scope.js" +import type * as Supervisor from "./Supervisor.js" +import type * as Tracer from "./Tracer.js" +import type { + Concurrency, + Contravariant, + Covariant, + EqualsWith, + NoExcessProperties, + NoInfer, + NotFunction +} from "./Types.js" +import type * as Unify from "./Unify.js" +import { isGeneratorFunction, type YieldWrap } from "./Utils.js" + +/** + * @since 2.0.0 + * @category Symbols + */ +export const EffectTypeId: unique symbol = core.EffectTypeId + +/** + * @since 2.0.0 + * @category Symbols + */ +export type EffectTypeId = typeof EffectTypeId + +/** + * The `Effect` interface defines a value that describes a workflow or job, + * which can succeed or fail. + * + * **Details** + * + * The `Effect` interface represents a computation that can model a workflow + * involving various types of operations, such as synchronous, asynchronous, + * concurrent, and parallel interactions. It operates within a context of type + * `R`, and the result can either be a success with a value of type `A` or a + * failure with an error of type `E`. The `Effect` is designed to handle complex + * interactions with external resources, offering advanced features such as + * fiber-based concurrency, scheduling, interruption handling, and scalability. + * This makes it suitable for tasks that require fine-grained control over + * concurrency and error management. + * + * To execute an `Effect` value, you need a `Runtime`, which provides the + * environment necessary to run and manage the computation. + * + * @since 2.0.0 + * @category Models + */ +export interface Effect extends Effect.Variance, Pipeable { + readonly [Unify.typeSymbol]?: unknown + readonly [Unify.unifySymbol]?: EffectUnify + readonly [Unify.ignoreSymbol]?: EffectUnifyIgnore + [Symbol.iterator](): EffectGenerator> +} + +/** + * @since 3.0.0 + * @category Models + */ +export interface EffectGenerator> { + next(...args: ReadonlyArray): IteratorResult, Effect.Success> +} + +/** + * @since 2.0.0 + * @category Models + */ +export interface EffectUnify + extends Either.EitherUnify, Option.OptionUnify, Context.TagUnify +{ + Effect?: () => A[Unify.typeSymbol] extends Effect | infer _ ? Effect : never +} + +/** + * @category Models + * @since 2.0.0 + */ +export interface EffectUnifyIgnore { + Tag?: true + Option?: true + Either?: true +} + +/** + * @category Type lambdas + * @since 2.0.0 + */ +export interface EffectTypeLambda extends TypeLambda { + readonly type: Effect +} + +/** + * @since 2.0.0 + * @category Models + */ +export interface Blocked extends Effect { + readonly _op: "Blocked" + readonly effect_instruction_i0: RequestBlock + readonly effect_instruction_i1: Effect +} + +/** + * @since 2.0.0 + * @category Models + */ +declare module "./Context.js" { + interface Tag extends Effect { + [Symbol.iterator](): EffectGenerator> + } + interface Reference extends Effect { + [Symbol.iterator](): EffectGenerator> + } + interface TagUnifyIgnore { + Effect?: true + Either?: true + Option?: true + } +} + +/** + * @since 2.0.0 + * @category Models + */ +declare module "./Either.js" { + interface Left extends Effect { + readonly _tag: "Left" + [Symbol.iterator](): EffectGenerator> + } + interface Right extends Effect { + readonly _tag: "Right" + [Symbol.iterator](): EffectGenerator> + } + interface EitherUnifyIgnore { + Effect?: true + Tag?: true + Option?: true + } +} + +/** + * @since 2.0.0 + * @category Models + */ +declare module "./Option.js" { + interface None extends Effect { + readonly _tag: "None" + [Symbol.iterator](): EffectGenerator> + } + interface Some extends Effect { + readonly _tag: "Some" + [Symbol.iterator](): EffectGenerator> + } + interface OptionUnifyIgnore { + Effect?: true + Tag?: true + Either?: true + } +} + +/** + * @since 2.0.0 + */ +export declare namespace Effect { + /** + * @since 2.0.0 + * @category Models + */ + export interface Variance { + readonly [EffectTypeId]: VarianceStruct + } + /** + * @since 2.0.0 + * @category Models + */ + export interface VarianceStruct { + readonly _V: string + readonly _A: Covariant + readonly _E: Covariant + readonly _R: Covariant + } + /** + * @since 2.0.0 + * @category Effect Type Extractors + */ + export type Context> = [T] extends [Effect] ? _R : never + /** + * @since 2.0.0 + * @category Effect Type Extractors + */ + export type Error> = [T] extends [Effect] ? _E : never + /** + * @since 2.0.0 + * @category Effect Type Extractors + */ + export type Success> = [T] extends [Effect] ? _A : never + /** + * @since 3.15.5 + * @category Effect Type Extractors + */ + export type AsEffect> = Effect< + T extends Effect ? _A : never, + T extends Effect ? _E : never, + T extends Effect ? _R : never + > extends infer Q ? Q : never +} + +/** + * Checks if a given value is an `Effect` value. + * + * **When to Use** + * + * This function can be useful for checking the type of a value before + * attempting to operate on it as an `Effect` value. For example, you could use + * `Effect.isEffect` to check the type of a value before using it as an argument + * to a function that expects an `Effect` value. + * + * @since 2.0.0 + * @category Guards + */ +export const isEffect: (u: unknown) => u is Effect = core.isEffect + +/** + * Returns an effect that caches its result for a specified `Duration`, + * known as "timeToLive" (TTL). + * + * **Details** + * + * This function is used to cache the result of an effect for a specified amount + * of time. This means that the first time the effect is evaluated, its result + * is computed and stored. + * + * If the effect is evaluated again within the specified `timeToLive`, the + * cached result will be used, avoiding recomputation. + * + * After the specified duration has passed, the cache expires, and the effect + * will be recomputed upon the next evaluation. + * + * **When to Use** + * + * Use this function when you have an effect that involves costly operations or + * computations, and you want to avoid repeating them within a short time frame. + * + * It's ideal for scenarios where the result of an effect doesn't change + * frequently and can be reused for a specified duration. + * + * By caching the result, you can improve efficiency and reduce unnecessary + * computations, especially in performance-critical applications. + * + * **Example** + * + * ```ts + * import { Effect, Console } from "effect" + * + * let i = 1 + * const expensiveTask = Effect.promise(() => { + * console.log("expensive task...") + * return new Promise((resolve) => { + * setTimeout(() => { + * resolve(`result ${i++}`) + * }, 100) + * }) + * }) + * + * const program = Effect.gen(function* () { + * const cached = yield* Effect.cachedWithTTL(expensiveTask, "150 millis") + * yield* cached.pipe(Effect.andThen(Console.log)) + * yield* cached.pipe(Effect.andThen(Console.log)) + * yield* Effect.sleep("100 millis") + * yield* cached.pipe(Effect.andThen(Console.log)) + * }) + * + * Effect.runFork(program) + * // Output: + * // expensive task... + * // result 1 + * // result 1 + * // expensive task... + * // result 2 + * ``` + * + * @see {@link cached} for a similar function that caches the result + * indefinitely. + * @see {@link cachedInvalidateWithTTL} for a similar function that includes an + * additional effect for manually invalidating the cached value. + * + * @since 2.0.0 + * @category Caching + */ +export const cachedWithTTL: { + (timeToLive: Duration.DurationInput): (self: Effect) => Effect, never, R> + (self: Effect, timeToLive: Duration.DurationInput): Effect, never, R> +} = circular.cached + +/** + * Caches an effect's result for a specified duration and allows manual + * invalidation before expiration. + * + * **Details** + * + * This function behaves similarly to {@link cachedWithTTL} by caching the + * result of an effect for a specified period of time. However, it introduces an + * additional feature: it provides an effect that allows you to manually + * invalidate the cached result before it naturally expires. + * + * This gives you more control over the cache, allowing you to refresh the + * result when needed, even if the original cache has not yet expired. + * + * Once the cache is invalidated, the next time the effect is evaluated, the + * result will be recomputed, and the cache will be refreshed. + * + * **When to Use** + * + * Use this function when you have an effect whose result needs to be cached for + * a certain period, but you also want the option to refresh the cache manually + * before the expiration time. + * + * This is useful when you need to ensure that the cached data remains valid for + * a certain period but still want to invalidate it if the underlying data + * changes or if you want to force a recomputation. + * + * **Example** + * + * ```ts + * import { Effect, Console } from "effect" + * + * let i = 1 + * const expensiveTask = Effect.promise(() => { + * console.log("expensive task...") + * return new Promise((resolve) => { + * setTimeout(() => { + * resolve(`result ${i++}`) + * }, 100) + * }) + * }) + * + * const program = Effect.gen(function* () { + * const [cached, invalidate] = yield* Effect.cachedInvalidateWithTTL( + * expensiveTask, + * "1 hour" + * ) + * yield* cached.pipe(Effect.andThen(Console.log)) + * yield* cached.pipe(Effect.andThen(Console.log)) + * yield* invalidate + * yield* cached.pipe(Effect.andThen(Console.log)) + * }) + * + * Effect.runFork(program) + * // Output: + * // expensive task... + * // result 1 + * // result 1 + * // expensive task... + * // result 2 + * ``` + * + * @see {@link cached} for a similar function that caches the result + * indefinitely. + * @see {@link cachedWithTTL} for a similar function that caches the result for + * a specified duration but does not include an effect for manual invalidation. + * + * @since 2.0.0 + * @category Caching + */ +export const cachedInvalidateWithTTL: { + (timeToLive: Duration.DurationInput): ( + self: Effect + ) => Effect<[Effect, Effect], never, R> + ( + self: Effect, + timeToLive: Duration.DurationInput + ): Effect<[Effect, Effect], never, R> +} = circular.cachedInvalidateWithTTL + +/** + * Returns an effect that lazily computes a result and caches it for subsequent + * evaluations. + * + * **Details** + * + * This function wraps an effect and ensures that its result is computed only + * once. Once the result is computed, it is cached, meaning that subsequent + * evaluations of the same effect will return the cached result without + * re-executing the logic. + * + * **When to Use** + * + * Use this function when you have an expensive or time-consuming operation that + * you want to avoid repeating. The first evaluation will compute the result, + * and all following evaluations will immediately return the cached value, + * improving performance and reducing unnecessary work. + * + * **Example** + * + * ```ts + * import { Effect, Console } from "effect" + * + * let i = 1 + * const expensiveTask = Effect.promise(() => { + * console.log("expensive task...") + * return new Promise((resolve) => { + * setTimeout(() => { + * resolve(`result ${i++}`) + * }, 100) + * }) + * }) + * + * const program = Effect.gen(function* () { + * console.log("non-cached version:") + * yield* expensiveTask.pipe(Effect.andThen(Console.log)) + * yield* expensiveTask.pipe(Effect.andThen(Console.log)) + * console.log("cached version:") + * const cached = yield* Effect.cached(expensiveTask) + * yield* cached.pipe(Effect.andThen(Console.log)) + * yield* cached.pipe(Effect.andThen(Console.log)) + * }) + * + * Effect.runFork(program) + * // Output: + * // non-cached version: + * // expensive task... + * // result 1 + * // expensive task... + * // result 2 + * // cached version: + * // expensive task... + * // result 3 + * // result 3 + * ``` + * + * @see {@link cachedWithTTL} for a similar function that includes a + * time-to-live duration for the cached value. + * @see {@link cachedInvalidateWithTTL} for a similar function that includes an + * additional effect for manually invalidating the cached value. + * + * @since 2.0.0 + * @category Caching + */ +export const cached: (self: Effect) => Effect> = effect.memoize + +/** + * Returns a memoized version of a function with effects, reusing results for + * the same inputs. + * + * **Details** + * + * This function creates a memoized version of a given function that performs an + * effect. Memoization ensures that once a result is computed for a specific + * input, it is stored and reused for subsequent calls with the same input, + * reducing the need to recompute the result. + * + * The function can optionally take an `Equivalence` parameter to + * determine how inputs are compared for caching purposes. + * + * **When to Use** + * + * Use this function when you have a function that performs an effect and you + * want to avoid recomputing the result for the same input multiple times. + * + * It's ideal for functions that produce deterministic results based on their + * inputs, and you want to improve performance by caching the output. + * + * This is particularly useful in scenarios where the function involves + * expensive calculations or operations that should be avoided after the first + * execution with the same parameters. + * + * **Example** + * + * ```ts + * import { Effect, Random } from "effect" + * + * const program = Effect.gen(function* () { + * const randomNumber = (n: number) => Random.nextIntBetween(1, n) + * console.log("non-memoized version:") + * console.log(yield* randomNumber(10)) + * console.log(yield* randomNumber(10)) + * + * console.log("memoized version:") + * const memoized = yield* Effect.cachedFunction(randomNumber) + * console.log(yield* memoized(10)) + * console.log(yield* memoized(10)) + * }) + * + * Effect.runFork(program) + * // Example Output: + * // non-memoized version: + * // 2 + * // 8 + * // memoized version: + * // 5 + * // 5 + * ``` + * + * @since 2.0.0 + * @category Caching + */ +export const cachedFunction: ( + f: (a: A) => Effect, + eq?: Equivalence +) => Effect<(a: A) => Effect> = circular.cachedFunction + +/** + * Returns an effect that executes only once, regardless of how many times it's + * called. + * + * **Details** + * + * This function ensures that a specific effect is executed only a single time, + * no matter how many times it is invoked. The result of the effect will be + * cached, and subsequent calls to the effect will immediately return the cached + * result without re-executing the original logic. + * + * **When to Use** + * + * Use this function when you need to perform a task only once, regardless of + * how many times the effect is triggered. It's particularly useful when you + * have initialization tasks, logging, or other one-time actions that should not + * be repeated. This can help optimize performance and avoid redundant actions. + * + * **Example** + * + * ```ts + * import { Effect, Console } from "effect" + * + * const program = Effect.gen(function* () { + * const task1 = Console.log("task1") + * yield* Effect.repeatN(task1, 2) + * const task2 = yield* Effect.once(Console.log("task2")) + * yield* Effect.repeatN(task2, 2) + * }) + * + * Effect.runFork(program) + * // Output: + * // task1 + * // task1 + * // task1 + * // task2 + * ``` + * + * @since 2.0.0 + * @category Caching + */ +export const once: (self: Effect) => Effect> = effect.once + +/** + * Combines multiple effects into one, returning results based on the input + * structure. + * + * **Details** + * + * Use this function when you need to run multiple effects and combine their + * results into a single output. It supports tuples, iterables, structs, and + * records, making it flexible for different input types. + * + * For instance, if the input is a tuple: + * + * ```ts skip-type-checking + * // ┌─── a tuple of effects + * // ▼ + * Effect.all([effect1, effect2, ...]) + * ``` + * + * the effects are executed sequentially, and the result is a new effect + * containing the results as a tuple. The results in the tuple match the order + * of the effects passed to `Effect.all`. + * + * **Concurrency** + * + * You can control the execution order (e.g., sequential vs. concurrent) using + * the `concurrency` option. + * + * **Short-Circuiting Behavior** + * + * This function stops execution on the first error it encounters, this is + * called "short-circuiting". If any effect in the collection fails, the + * remaining effects will not run, and the error will be propagated. To change + * this behavior, you can use the `mode` option, which allows all effects to run + * and collect results as `Either` or `Option`. + * + * **The `mode` option** + * + * The `{ mode: "either" }` option changes the behavior of `Effect.all` to + * ensure all effects run, even if some fail. Instead of stopping on the first + * failure, this mode collects both successes and failures, returning an array + * of `Either` instances where each result is either a `Right` (success) or a + * `Left` (failure). + * + * Similarly, the `{ mode: "validate" }` option uses `Option` to indicate + * success or failure. Each effect returns `None` for success and `Some` with + * the error for failure. + * + * **Example** (Combining Effects in Tuples) + * + * ```ts + * import { Effect, Console } from "effect" + * + * const tupleOfEffects = [ + * Effect.succeed(42).pipe(Effect.tap(Console.log)), + * Effect.succeed("Hello").pipe(Effect.tap(Console.log)) + * ] as const + * + * // ┌─── Effect<[number, string], never, never> + * // ▼ + * const resultsAsTuple = Effect.all(tupleOfEffects) + * + * Effect.runPromise(resultsAsTuple).then(console.log) + * // Output: + * // 42 + * // Hello + * // [ 42, 'Hello' ] + * ``` + * + * **Example** (Combining Effects in Iterables) + * + * ```ts + * import { Effect, Console } from "effect" + * + * const iterableOfEffects: Iterable> = [1, 2, 3].map( + * (n) => Effect.succeed(n).pipe(Effect.tap(Console.log)) + * ) + * + * // ┌─── Effect + * // ▼ + * const resultsAsArray = Effect.all(iterableOfEffects) + * + * Effect.runPromise(resultsAsArray).then(console.log) + * // Output: + * // 1 + * // 2 + * // 3 + * // [ 1, 2, 3 ] + * ``` + * + * **Example** (Combining Effects in Structs) + * + * ```ts + * import { Effect, Console } from "effect" + * + * const structOfEffects = { + * a: Effect.succeed(42).pipe(Effect.tap(Console.log)), + * b: Effect.succeed("Hello").pipe(Effect.tap(Console.log)) + * } + * + * // ┌─── Effect<{ a: number; b: string; }, never, never> + * // ▼ + * const resultsAsStruct = Effect.all(structOfEffects) + * + * Effect.runPromise(resultsAsStruct).then(console.log) + * // Output: + * // 42 + * // Hello + * // { a: 42, b: 'Hello' } + * ``` + * + * **Example** (Combining Effects in Records) + * + * ```ts + * import { Effect, Console } from "effect" + * + * const recordOfEffects: Record> = { + * key1: Effect.succeed(1).pipe(Effect.tap(Console.log)), + * key2: Effect.succeed(2).pipe(Effect.tap(Console.log)) + * } + * + * // ┌─── Effect<{ [x: string]: number; }, never, never> + * // ▼ + * const resultsAsRecord = Effect.all(recordOfEffects) + * + * Effect.runPromise(resultsAsRecord).then(console.log) + * // Output: + * // 1 + * // 2 + * // { key1: 1, key2: 2 } + * ``` + * + * **Example** (Short-Circuiting Behavior) + * + * ```ts + * import { Effect, Console } from "effect" + * + * const program = Effect.all([ + * Effect.succeed("Task1").pipe(Effect.tap(Console.log)), + * Effect.fail("Task2: Oh no!").pipe(Effect.tap(Console.log)), + * // Won't execute due to earlier failure + * Effect.succeed("Task3").pipe(Effect.tap(Console.log)) + * ]) + * + * Effect.runPromiseExit(program).then(console.log) + * // Output: + * // Task1 + * // { + * // _id: 'Exit', + * // _tag: 'Failure', + * // cause: { _id: 'Cause', _tag: 'Fail', failure: 'Task2: Oh no!' } + * // } + * ``` + * + * **Example** (Collecting Results with `mode: "either"`) + * + * ```ts + * import { Effect, Console } from "effect" + * + * const effects = [ + * Effect.succeed("Task1").pipe(Effect.tap(Console.log)), + * Effect.fail("Task2: Oh no!").pipe(Effect.tap(Console.log)), + * Effect.succeed("Task3").pipe(Effect.tap(Console.log)) + * ] + * + * const program = Effect.all(effects, { mode: "either" }) + * + * Effect.runPromiseExit(program).then(console.log) + * // Output: + * // Task1 + * // Task3 + * // { + * // _id: 'Exit', + * // _tag: 'Success', + * // value: [ + * // { _id: 'Either', _tag: 'Right', right: 'Task1' }, + * // { _id: 'Either', _tag: 'Left', left: 'Task2: Oh no!' }, + * // { _id: 'Either', _tag: 'Right', right: 'Task3' } + * // ] + * // } + * ``` + * + * **Example** (Collecting Results with `mode: "validate"`) + * + * ```ts + * import { Effect, Console } from "effect" + * + * const effects = [ + * Effect.succeed("Task1").pipe(Effect.tap(Console.log)), + * Effect.fail("Task2: Oh no!").pipe(Effect.tap(Console.log)), + * Effect.succeed("Task3").pipe(Effect.tap(Console.log)) + * ] + * + * const program = Effect.all(effects, { mode: "validate" }) + * + * Effect.runPromiseExit(program).then((result) => console.log("%o", result)) + * // Output: + * // Task1 + * // Task3 + * // { + * // _id: 'Exit', + * // _tag: 'Failure', + * // cause: { + * // _id: 'Cause', + * // _tag: 'Fail', + * // failure: [ + * // { _id: 'Option', _tag: 'None' }, + * // { _id: 'Option', _tag: 'Some', value: 'Task2: Oh no!' }, + * // { _id: 'Option', _tag: 'None' } + * // ] + * // } + * // } + * ``` + * + * @see {@link forEach} for iterating over elements and applying an effect. + * @see {@link allWith} for a data-last version of this function. + * + * @since 2.0.0 + * @category Collecting + */ +export const all: < + const Arg extends Iterable> | Record>, + O extends NoExcessProperties<{ + readonly concurrency?: Concurrency | undefined + readonly batching?: boolean | "inherit" | undefined + readonly discard?: boolean | undefined + readonly mode?: "default" | "validate" | "either" | undefined + readonly concurrentFinalizers?: boolean | undefined + }, O> +>(arg: Arg, options?: O) => All.Return = fiberRuntime.all + +/** + * A data-last version of {@link all}, designed for use in pipelines. + * + * **When to Use** + * + * This function enables you to combine multiple effects and customize execution + * options such as concurrency levels. This version is useful in functional + * pipelines where you first define your data and then apply operations to it. + * + * **Example** + * + * ```ts + * import { Effect, pipe } from "effect" + * + * const task1 = Effect.succeed(1).pipe( + * Effect.delay("200 millis"), + * Effect.tap(Effect.log("task1 done")) + * ) + * + * const task2 = Effect.succeed("hello").pipe( + * Effect.delay("100 millis"), + * Effect.tap(Effect.log("task2 done")) + * ) + * + * const program = pipe( + * [task1, task2], + * // Run both effects concurrently using the concurrent option + * Effect.allWith({ concurrency: 2 }) + * ) + * + * Effect.runPromise(program).then(console.log) + * // Output: + * // timestamp=... level=INFO fiber=#3 message="task2 done" + * // timestamp=... level=INFO fiber=#2 message="task1 done" + * // [ 1, 'hello' ] + * ``` + * + * @since 2.0.0 + * @category Collecting + */ +export const allWith: < + O extends NoExcessProperties<{ + readonly concurrency?: Concurrency | undefined + readonly batching?: boolean | "inherit" | undefined + readonly discard?: boolean | undefined + readonly mode?: "default" | "validate" | "either" | undefined + readonly concurrentFinalizers?: boolean | undefined + }, O> +>( + options?: O +) => > | Record>>( + arg: Arg +) => All.Return = fiberRuntime.allWith + +/** + * @since 2.0.0 + */ +export declare namespace All { + /** + * @since 2.0.0 + */ + export type EffectAny = Effect + + /** + * @since 2.0.0 + */ + export type ReturnIterable, Discard extends boolean, Mode> = [T] extends + [Iterable>] ? Effect< + Discard extends true ? void : Mode extends "either" ? Array> : Array, + Mode extends "either" ? never + : Mode extends "validate" ? Array> + : L0, + R + > + : never + + /** + * @since 2.0.0 + */ + export type ReturnTuple, Discard extends boolean, Mode> = Effect< + Discard extends true ? void + : T[number] extends never ? [] + : Mode extends "either" ? { + -readonly [K in keyof T]: [T[K]] extends [Effect.Variance] ? + Either.Either<_A, _E> + : never + } + : { -readonly [K in keyof T]: [T[K]] extends [Effect.Variance] ? _A : never }, + Mode extends "either" ? never + : T[number] extends never ? never + : Mode extends "validate" ? { + -readonly [K in keyof T]: [T[K]] extends [Effect.Variance] ? Option.Option<_E> + : never + } + : [T[number]] extends [{ [EffectTypeId]: { _E: (_: never) => infer E } }] ? E + : never, + T[number] extends never ? never + : [T[number]] extends [{ [EffectTypeId]: { _R: (_: never) => infer R } }] ? R + : never + > extends infer X ? X : never + + /** + * @since 2.0.0 + */ + export type ReturnObject = [T] extends [{ [K: string]: EffectAny }] ? Effect< + Discard extends true ? void + : Mode extends "either" ? { + -readonly [K in keyof T]: [T[K]] extends [Effect.Variance] ? + Either.Either<_A, _E> + : never + } + : { -readonly [K in keyof T]: [T[K]] extends [Effect.Variance] ? _A : never }, + Mode extends "either" ? never + : keyof T extends never ? never + : Mode extends "validate" ? { + -readonly [K in keyof T]: [T[K]] extends [Effect.Variance] ? Option.Option<_E> + : never + } + : [T[keyof T]] extends [{ [EffectTypeId]: { _E: (_: never) => infer E } }] ? E + : never, + keyof T extends never ? never + : [T[keyof T]] extends [{ [EffectTypeId]: { _R: (_: never) => infer R } }] ? R + : never + > + : never + + /** + * @since 2.0.0 + */ + export type IsDiscard = [Extract] extends [never] ? false : true + + /** + * @since 2.0.0 + */ + export type ExtractMode = [A] extends [{ mode: infer M }] ? M : "default" + + /** + * @since 2.0.0 + */ + export type Return< + Arg extends Iterable | Record, + O extends NoExcessProperties<{ + readonly concurrency?: Concurrency | undefined + readonly batching?: boolean | "inherit" | undefined + readonly discard?: boolean | undefined + readonly mode?: "default" | "validate" | "either" | undefined + readonly concurrentFinalizers?: boolean | undefined + }, O> + > = [Arg] extends [ReadonlyArray] ? ReturnTuple, ExtractMode> + : [Arg] extends [Iterable] ? ReturnIterable, ExtractMode> + : [Arg] extends [Record] ? ReturnObject, ExtractMode> + : never +} + +/** + * Evaluates and runs each effect in the iterable, collecting only the + * successful results while discarding failures. + * + * **Details** + * + * This function function processes an iterable of effects and runs each one. If + * an effect is successful, its result is collected; if it fails, the result is + * discarded. This ensures that only successful outcomes are kept. + * + * **Options** + * + * The function also allows you to customize how the effects are handled by + * specifying options such as concurrency, batching, and how finalizers behave. + * These options provide flexibility in running the effects concurrently or + * adjusting other execution details. + * + * **Example** + * + * ```ts + * import { Effect } from "effect" + * + * const tasks = [ + * Effect.succeed(1), + * Effect.fail("Error 1"), + * Effect.succeed(2), + * Effect.fail("Error 2") + * ] + * + * const program = Effect.gen(function*() { + * const successfulResults = yield* Effect.allSuccesses(tasks) + * console.log(successfulResults) + * }) + * + * Effect.runFork(program) + * // Output: [1, 2] + * + * ``` + * + * @since 2.0.0 + * @category Collecting + */ +export const allSuccesses: >( + elements: Iterable, + options?: + | { + readonly concurrency?: Concurrency | undefined + readonly batching?: boolean | "inherit" | undefined + readonly concurrentFinalizers?: boolean | undefined + } + | undefined +) => Effect>, never, Effect.Context> = fiberRuntime.allSuccesses + +/** + * Drops elements until the effectful predicate returns `true`. + * + * **Details** + * + * This function processes a collection of elements and uses an effectful + * predicate to determine when to stop dropping elements. It drops elements from + * the beginning of the collection until the predicate returns `true`. + * + * The predicate is a function that takes an element and its index in the + * collection and returns an effect that evaluates to a boolean. + * + * Once the predicate returns `true`, the remaining elements of the collection + * are returned. + * + * **Note**: The first element for which the predicate returns `true` is also + * dropped. + * + * **When to Use** + * + * This function allows you to conditionally skip over a part of the collection + * based on some criteria defined in the predicate. + * + * **Example** + * + * ```ts + * import { Effect } from "effect" + * + * const numbers = [1, 2, 3, 4, 5, 6] + * const predicate = (n: number, i: number) => Effect.succeed(n > 3) + * + * const program = Effect.gen(function*() { + * const result = yield* Effect.dropUntil(numbers, predicate) + * console.log(result) + * }) + * + * Effect.runFork(program) + * // Output: [5, 6] + * ``` + * + * @see {@link dropWhile} for a similar function that drops elements while the + * predicate returns `true`. + * + * @since 2.0.0 + * @category Collecting + */ +export const dropUntil: { + ( + predicate: (a: NoInfer, i: number) => Effect + ): (elements: Iterable) => Effect, E, R> + (elements: Iterable, predicate: (a: A, i: number) => Effect): Effect, E, R> +} = effect.dropUntil + +/** + * Drops elements as long as the predicate returns `true`. + * + * **Details** + * + * This function processes a collection of elements and uses a predicate to + * decide whether to drop an element. + * + * The predicate is a function that takes an element and its index, and it + * returns an effect that evaluates to a boolean. + * + * As long as the predicate returns `true`, elements will continue to be dropped + * from the collection. + * + * Once the predicate returns `false`, the remaining elements are kept. + * + * **When to Use** + * + * This function allows you to discard elements from the start of a collection + * based on a condition, and only keep the rest when the condition no longer + * holds. + * + * **Example** + * + * ```ts + * import { Effect } from "effect" + * + * const numbers = [1, 2, 3, 4, 5, 6] + * const predicate = (n: number, i: number) => Effect.succeed(n <= 3) + * + * const program = Effect.gen(function*() { + * const result = yield* Effect.dropWhile(numbers, predicate) + * console.log(result) + * }) + * + * Effect.runFork(program) + * // Output: [4, 5, 6] + * ``` + * + * @see {@link dropUntil} for a similar function that drops elements until the + * predicate returns `true`. + * + * @since 2.0.0 + * @category Collecting + */ +export const dropWhile: { + ( + predicate: (a: NoInfer, i: number) => Effect + ): (elements: Iterable) => Effect, E, R> + (elements: Iterable, predicate: (a: A, i: number) => Effect): Effect, E, R> +} = effect.dropWhile + +/** + * Takes elements from a collection until the effectful predicate returns + * `true`. + * + * **Details** + * + * This function processes a collection of elements and uses an effectful + * predicate to decide when to stop taking elements. The elements are taken from + * the beginning of the collection until the predicate returns `true`. + * + * The predicate is a function that takes an element and its index in the + * collection, and returns an effect that resolves to a boolean. + * + * Once the predicate returns `true`, the remaining elements of the collection + * are discarded, and the function stops taking more elements. + * + * **Note**: The first element for which the predicate returns `true` is also + * included in the result. + * + * **When to Use** + * + * Use this function when you want to conditionally take elements from a + * collection based on a dynamic condition. For example, you may want to collect + * numbers from a list until a certain threshold is reached, or gather items + * until a specific condition is met. + * + * **Example** + * + * ```ts + * import { Effect } from "effect" + * + * const numbers = [1, 2, 3, 4, 5, 6] + * const predicate = (n: number, i: number) => Effect.succeed(n > 3) + * + * const program = Effect.gen(function*() { + * const result = yield* Effect.takeUntil(numbers, predicate) + * console.log(result) + * }) + * + * Effect.runFork(program) + * // Output: [ 1, 2, 3, 4 ] + * ``` + * + * @see {@link takeWhile} for a similar function that takes elements while the + * predicate returns `true`. + * + * @since 2.0.0 + * @category Collecting + */ +export const takeUntil: { + ( + predicate: (a: NoInfer, i: number) => Effect + ): (elements: Iterable) => Effect, E, R> + ( + elements: Iterable, + predicate: (a: NoInfer, i: number) => Effect + ): Effect, E, R> +} = effect.takeUntil + +/** + * Takes elements as long as the predicate returns `true`. + * + * **Details** + * + * This function processes a collection of elements and uses a predicate to + * decide whether to take an element. + * + * The predicate is a function that takes an element and its index, and it + * returns an effect that evaluates to a boolean. + * + * As long as the predicate returns `true`, elements will continue to be taken + * from the collection. + * + * Once the predicate returns `false`, the remaining elements are discarded. + * + * **Example** + * + * ```ts + * import { Effect } from "effect" + * + * const numbers = [1, 2, 3, 4, 5, 6] + * const predicate = (n: number, i: number) => Effect.succeed(n <= 3) + * + * const program = Effect.gen(function*() { + * const result = yield* Effect.takeWhile(numbers, predicate) + * console.log(result) + * }) + * + * Effect.runFork(program) + * // Output: [1, 2, 3] + * ``` + * + * @see {@link takeUntil} for a similar function that takes elements until the predicate returns `true`. + * + * @since 2.0.0 + * @category Collecting + */ +export const takeWhile: { + ( + predicate: (a: NoInfer, i: number) => Effect + ): (elements: Iterable) => Effect, E, R> + ( + elements: Iterable, + predicate: (a: NoInfer, i: number) => Effect + ): Effect, E, R> +} = effect.takeWhile + +/** + * Determines whether all elements of the iterable satisfy the effectful + * predicate. + * + * **Details** + * + * This function checks whether every element in a given collection (an + * iterable) satisfies a condition defined by an effectful predicate. + * + * The predicate is a function that takes an element and its index, and it + * returns an effect that evaluates to a boolean. + * + * The function will process each element and return `true` if all elements + * satisfy the predicate; otherwise, it returns `false`. + * + * **When to Use** + * + * This function is useful when you need to verify that all items in a + * collection meet certain criteria, even when the evaluation of each item + * involves effects, such as asynchronous checks or complex computations. + * + * **Example** + * + * ```ts + * import { Effect } from "effect" + * + * const numbers = [2, 4, 6, 8] + * const predicate = (n: number, i: number) => Effect.succeed(n % 2 === 0) + * + * const program = Effect.gen(function*() { + * const allEven = yield* Effect.every(numbers, predicate) + * console.log(allEven) + * }) + * + * Effect.runFork(program) + * // Output: true + * ``` + * + * @see {@link exists} for a similar function that returns a boolean indicating + * whether **any** element satisfies the predicate. + * + * @since 2.0.0 + * @category Condition Checking + */ +export const every: { + (predicate: (a: A, i: number) => Effect): (elements: Iterable) => Effect + (elements: Iterable, predicate: (a: A, i: number) => Effect): Effect +} = effect.every + +/** + * Determines whether any element of the iterable satisfies the effectual + * predicate. + * + * **Details** + * + * This function checks whether any element in a given collection (an iterable) + * satisfies a condition defined by an effectful predicate. + * + * The predicate is a function that takes an element and its index, and it + * returns an effect that evaluates to a boolean. + * + * The function will process each element, and if any element satisfies the + * predicate (returns `true`), the function will immediately return `true`. + * + * If none of the elements satisfy the condition, it will return `false`. + * + * **When to Use** + * + * This function allows you to quickly check for a condition in a collection + * without having to manually iterate over it. + * + * **Example** + * + * ```ts + * import { Effect } from "effect" + * + * const numbers = [1, 2, 3, 4] + * const predicate = (n: number, i: number) => Effect.succeed(n > 2) + * + * const program = Effect.gen(function*() { + * const hasLargeNumber = yield* Effect.exists(numbers, predicate) + * console.log(hasLargeNumber) + * }) + * + * Effect.runFork(program) + * // Output: true + * ``` + * + * @see {@link every} for a similar function that checks if **all** elements + * satisfy the predicate. + * + * @since 2.0.0 + * @category Condition Checking + */ +export const exists: { + ( + predicate: (a: A, i: number) => Effect, + options?: + | { + readonly concurrency?: Concurrency | undefined + readonly batching?: boolean | "inherit" | undefined + readonly concurrentFinalizers?: boolean | undefined + } + | undefined + ): (elements: Iterable) => Effect + ( + elements: Iterable, + predicate: (a: A, i: number) => Effect, + options?: + | { + readonly concurrency?: Concurrency | undefined + readonly batching?: boolean | "inherit" | undefined + readonly concurrentFinalizers?: boolean | undefined + } + | undefined + ): Effect +} = fiberRuntime.exists + +/** + * Filters an iterable using the specified effectful predicate. + * + * **Details** + * + * This function filters a collection (an iterable) by applying an effectful + * predicate. + * + * The predicate is a function that takes an element and its index, and it + * returns an effect that evaluates to a boolean. + * + * The function processes each element in the collection and keeps only those + * that satisfy the condition defined by the predicate. + * + * **Options** + * + * You can also adjust the behavior with options such as concurrency, batching, + * or whether to negate the condition. + * + * **When to Use** + * + * This function allows you to selectively keep or remove elements based on a + * condition that may involve asynchronous or side-effect-causing operations. + * + * **Example** + * + * ```ts + * import { Effect } from "effect" + * + * const numbers = [1, 2, 3, 4, 5] + * const predicate = (n: number, i: number) => Effect.succeed(n % 2 === 0) + * + * const program = Effect.gen(function*() { + * const result = yield* Effect.filter(numbers, predicate) + * console.log(result) + * }) + * + * Effect.runFork(program) + * // Output: [2, 4] + * ``` + * + * @since 2.0.0 + * @category Filtering + */ +export const filter: { + ( + predicate: (a: NoInfer, i: number) => Effect, + options?: { + readonly concurrency?: Concurrency | undefined + readonly batching?: boolean | "inherit" | undefined + readonly negate?: boolean | undefined + readonly concurrentFinalizers?: boolean | undefined + } | undefined + ): (elements: Iterable) => Effect, E, R> + ( + elements: Iterable, + predicate: (a: NoInfer, i: number) => Effect, + options?: { + readonly concurrency?: Concurrency | undefined + readonly batching?: boolean | "inherit" | undefined + readonly negate?: boolean | undefined + readonly concurrentFinalizers?: boolean | undefined + } | undefined + ): Effect, E, R> +} = fiberRuntime.filter + +/** + * Filters and maps elements sequentially in one operation. + * + * This function processes each element one by one. It applies a function that + * returns an `Option` to each element. If the function returns `Some`, the + * element is kept; if it returns `None`, the element is removed. The operation + * is done sequentially for each element. + * + * **Example** + * + * ```ts + * import { Console, Effect, Option } from "effect" + * + * const task = (n: number) => + * Effect.succeed(n).pipe( + * Effect.delay(1000 - (n * 100)), + * Effect.tap(Console.log(`task${n} done`)) + * ) + * + * const program = Effect.filterMap( + * [task(1), task(2), task(3), task(4)], + * (n) => n % 2 === 0 ? Option.some(n) : Option.none() + * ) + * + * Effect.runPromise(program).then(console.log) + * // Output: + * // task1 done + * // task2 done + * // task3 done + * // task4 done + * // [ 2, 4 ] + * ``` + * + * @since 2.0.0 + * @category Filtering + */ +export const filterMap: { + , B>( + pf: (a: Effect.Success) => Option.Option + ): (elements: Iterable) => Effect, Effect.Error, Effect.Context> + , B>( + elements: Iterable, + pf: (a: Effect.Success) => Option.Option + ): Effect, Effect.Error, Effect.Context> +} = effect.filterMap + +/** + * Returns the first element that satisfies the effectful predicate. + * + * **Details** + * + * This function processes a collection of elements and applies an effectful + * predicate to each element. + * + * The predicate is a function that takes an element and its index in the + * collection, and it returns an effect that evaluates to a boolean. + * + * The function stops as soon as it finds the first element for which the + * predicate returns `true` and returns that element wrapped in an `Option`. + * + * If no element satisfies the predicate, the result will be `None`. + * + * **When to Use** + * + * This function allows you to efficiently find an element that meets a specific + * condition, even when the evaluation involves effects like asynchronous + * operations or side effects. + * + * **Example** + * + * ```ts + * import { Effect } from "effect" + * + * const numbers = [1, 2, 3, 4, 5] + * const predicate = (n: number, i: number) => Effect.succeed(n > 3) + * + * const program = Effect.gen(function*() { + * const result = yield* Effect.findFirst(numbers, predicate) + * console.log(result) + * }) + * + * Effect.runFork(program) + * // Output: { _id: 'Option', _tag: 'Some', value: 4 } + * ``` + * + * @since 2.0.0 + * @category Collecting + */ +export const findFirst: { + ( + predicate: (a: NoInfer, i: number) => Effect + ): (elements: Iterable) => Effect, E, R> + ( + elements: Iterable, + predicate: (a: NoInfer, i: number) => Effect + ): Effect, E, R> +} = effect.findFirst + +/** + * Executes an effectful operation for each element in an `Iterable`. + * + * **Details** + * + * This function applies a provided operation to each element in the iterable, + * producing a new effect that returns an array of results. + * + * If any effect fails, the iteration stops immediately (short-circuiting), and + * the error is propagated. + * + * **Concurrency** + * + * The `concurrency` option controls how many operations are performed + * concurrently. By default, the operations are performed sequentially. + * + * **Discarding Results** + * + * If the `discard` option is set to `true`, the intermediate results are not + * collected, and the final result of the operation is `void`. + * + * **Example** (Applying Effects to Iterable Elements) + * + * ```ts + * import { Effect, Console } from "effect" + * + * const result = Effect.forEach([1, 2, 3, 4, 5], (n, index) => + * Console.log(`Currently at index ${index}`).pipe(Effect.as(n * 2)) + * ) + * + * Effect.runPromise(result).then(console.log) + * // Output: + * // Currently at index 0 + * // Currently at index 1 + * // Currently at index 2 + * // Currently at index 3 + * // Currently at index 4 + * // [ 2, 4, 6, 8, 10 ] + * ``` + * + * **Example** (Discarding Results) + * + * ```ts + * import { Effect, Console } from "effect" + * + * // Apply effects but discard the results + * const result = Effect.forEach( + * [1, 2, 3, 4, 5], + * (n, index) => + * Console.log(`Currently at index ${index}`).pipe(Effect.as(n * 2)), + * { discard: true } + * ) + * + * Effect.runPromise(result).then(console.log) + * // Output: + * // Currently at index 0 + * // Currently at index 1 + * // Currently at index 2 + * // Currently at index 3 + * // Currently at index 4 + * // undefined + * ``` + * + * @see {@link all} for combining multiple effects into one. + * + * @since 2.0.0 + * @category Looping + */ +export const forEach: { + >( + f: (a: RA.ReadonlyArray.Infer, i: number) => Effect, + options?: { + readonly concurrency?: Concurrency | undefined + readonly batching?: boolean | "inherit" | undefined + readonly discard?: false | undefined + readonly concurrentFinalizers?: boolean | undefined + } | undefined + ): ( + self: S + ) => Effect, E, R> + ( + f: (a: A, i: number) => Effect, + options: { + readonly concurrency?: Concurrency | undefined + readonly batching?: boolean | "inherit" | undefined + readonly discard: true + readonly concurrentFinalizers?: boolean | undefined + } + ): (self: Iterable) => Effect + >( + self: S, + f: (a: RA.ReadonlyArray.Infer, i: number) => Effect, + options?: { + readonly concurrency?: Concurrency | undefined + readonly batching?: boolean | "inherit" | undefined + readonly discard?: false | undefined + readonly concurrentFinalizers?: boolean | undefined + } | undefined + ): Effect, E, R> + ( + self: Iterable, + f: (a: A, i: number) => Effect, + options: { + readonly concurrency?: Concurrency | undefined + readonly batching?: boolean | "inherit" | undefined + readonly discard: true + readonly concurrentFinalizers?: boolean | undefined + } + ): Effect +} = fiberRuntime.forEach + +/** + * Returns the first element of the iterable if the collection is non-empty, or + * fails with the error `NoSuchElementException` if the collection is empty. + * + * **When to Use** + * + * This function is useful when you need to retrieve the first item from a + * collection and want to handle the case where the collection might be empty + * without causing an unhandled exception. + * + * **Example** + * + * ```ts + * import { Effect } from "effect" + * + * // Simulate an async operation + * const fetchNumbers = Effect.succeed([1, 2, 3]).pipe(Effect.delay("100 millis")) + * + * const program = Effect.gen(function*() { + * const firstElement = yield* Effect.head(fetchNumbers) + * console.log(firstElement) + * }) + * + * Effect.runFork(program) + * // Output: 1 + * ``` + * + * @since 2.0.0 + * @category Collecting + */ +export const head: (self: Effect, E, R>) => Effect = + effect.head + +/** + * Merges an `Iterable>` to a single effect. + * + * **Details** + * + * This function takes an iterable of effects and combines them into a single + * effect. It does this by iterating over each effect in the collection and + * applying a function that accumulates results into a "zero" value, which + * starts with an initial value and is updated with each effect's success. + * + * The provided function `f` is called for each element in the iterable, + * allowing you to specify how to combine the results. + * + * **Options** + * + * The function also allows you to customize how the effects are handled by + * specifying options such as concurrency, batching, and how finalizers behave. + * These options provide flexibility in running the effects concurrently or + * adjusting other execution details. + * + * **Example** + * + * ```ts + * import { Effect } from "effect" + * + * const numbers = [Effect.succeed(1), Effect.succeed(2), Effect.succeed(3)] + * const add = (sum: number, value: number, i: number) => sum + value + * const zero = 0 + * + * const program = Effect.gen(function*() { + * const total = yield* Effect.mergeAll(numbers, zero, add) + * console.log(total) + * }) + * + * Effect.runFork(program) + * // Output: 6 + * ``` + * + * @since 2.0.0 + * @category Collecting + */ +export const mergeAll: { + >( + zero: Z, + f: (z: Z, a: Effect.Success, i: number) => Z, + options?: + | { + readonly concurrency?: Concurrency | undefined + readonly batching?: boolean | "inherit" | undefined + readonly concurrentFinalizers?: boolean | undefined + } + | undefined + ): (elements: Iterable) => Effect, Effect.Context> + , Z>( + elements: Iterable, + zero: Z, + f: (z: Z, a: Effect.Success, i: number) => Z, + options?: + | { + readonly concurrency?: Concurrency | undefined + readonly batching?: boolean | "inherit" | undefined + readonly concurrentFinalizers?: boolean | undefined + } + | undefined + ): Effect, Effect.Context> +} = fiberRuntime.mergeAll + +/** + * Processes an iterable and applies an effectful function to each element, + * categorizing the results into successes and failures. + * + * **Details** + * + * This function processes each element in the provided iterable by applying an + * effectful function to it. The results are then categorized into two separate + * lists: one for failures and another for successes. This separation allows you + * to handle the two categories differently. Failures are collected in a list + * without interrupting the processing of the remaining elements, so the + * operation continues even if some elements fail. This is particularly useful + * when you need to handle both successful and failed results separately, + * without stopping the entire process on encountering a failure. + * + * **When to Use** + * + * Use this function when you want to process a collection of items and handle + * errors or failures without interrupting the processing of other items. It's + * useful when you need to distinguish between successful and failed results and + * process them separately, for example, when logging errors while continuing to + * work with valid data. The function ensures that failures are captured, while + * successes are processed normally. + * + * **Example** + * + * ```ts + * import { Effect } from "effect" + * + * // ┌─── Effect<[string[], number[]], never, never> + * // ▼ + * const program = Effect.partition([0, 1, 2, 3, 4], (n) => { + * if (n % 2 === 0) { + * return Effect.succeed(n) + * } else { + * return Effect.fail(`${n} is not even`) + * } + * }) + * + * Effect.runPromise(program).then(console.log, console.error) + * // Output: + * // [ [ '1 is not even', '3 is not even' ], [ 0, 2, 4 ] ] + * ``` + * + * @see {@link validateAll} for a function that either collects all failures or all successes. + * @see {@link validateFirst} for a function that stops at the first success. + * + * @since 2.0.0 + * @category Error Accumulation + */ +export const partition: { + ( + f: (a: A, i: number) => Effect, + options?: + | { + readonly concurrency?: Concurrency | undefined + readonly batching?: boolean | "inherit" | undefined + readonly concurrentFinalizers?: boolean | undefined + } + | undefined + ): (elements: Iterable) => Effect<[excluded: Array, satisfying: Array], never, R> + ( + elements: Iterable, + f: (a: A, i: number) => Effect, + options?: + | { + readonly concurrency?: Concurrency | undefined + readonly batching?: boolean | "inherit" | undefined + readonly concurrentFinalizers?: boolean | undefined + } + | undefined + ): Effect<[excluded: Array, satisfying: Array], never, R> +} = fiberRuntime.partition + +/** + * Reduces an `Iterable` using an effectual function `f`, working + * sequentially from left to right. + * + * **Details** + * + * This function takes an iterable and applies a function `f` to each element in + * the iterable. The function works sequentially, starting with an initial value + * `zero` and then combining it with each element in the collection. The + * provided function `f` is called for each element in the iterable, allowing + * you to accumulate a result based on the current value and the element being + * processed. + * + * **When to Use** + * + * The function is often used for operations like summing a collection of + * numbers or combining results from multiple tasks. It ensures that operations + * are performed one after the other, maintaining the order of the elements. + * + * **Example** + * + * ```ts + * import { Console, Effect } from "effect" + * + * const processOrder = (id: number) => + * Effect.succeed({ id, price: 100 * id }) + * .pipe(Effect.tap(() => Console.log(`Order ${id} processed`)), Effect.delay(500 - (id * 100))) + * + * const program = Effect.reduce( + * [1, 2, 3, 4], + * 0, + * (acc, id, i) => + * processOrder(id) + * .pipe(Effect.map((order) => acc + order.price)) + * ) + * + * Effect.runPromise(program).then(console.log) + * // Output: + * // Order 1 processed + * // Order 2 processed + * // Order 3 processed + * // Order 4 processed + * // 1000 + * ``` + * + * @see {@link reduceWhile} for a similar function that stops the process based on a predicate. + * @see {@link reduceRight} for a similar function that works from right to left. + * + * @since 2.0.0 + * @category Collecting + */ +export const reduce: { + (zero: Z, f: (z: Z, a: A, i: number) => Effect): (elements: Iterable) => Effect + (elements: Iterable, zero: Z, f: (z: Z, a: A, i: number) => Effect): Effect +} = effect.reduce + +/** + * Reduces an `Iterable` using an effectual function `body`, working + * sequentially from left to right, stopping the process early when the + * predicate `while` is not satisfied. + * + * **Details** + * + * This function processes a collection of elements, applying a function `body` + * to reduce them to a single value, starting from the first element. It checks + * the value of the accumulator against a predicate (`while`). If at any point + * the predicate returns `false`, the reduction stops, and the accumulated + * result is returned. + * + * **When to Use** + * + * Use this function when you need to reduce a collection of elements, but only + * continue the process as long as a certain condition holds true. For example, + * if you want to sum values in a list but stop as soon as the sum exceeds a + * certain threshold, you can use this function. + * + * **Example** + * + * ```ts + * import { Console, Effect } from "effect" + * + * const processOrder = (id: number) => + * Effect.succeed({ id, price: 100 * id }) + * .pipe(Effect.tap(() => Console.log(`Order ${id} processed`)), Effect.delay(500 - (id * 100))) + * + * const program = Effect.reduceWhile( + * [1, 2, 3, 4], + * 0, + * { + * body: (acc, id, i) => + * processOrder(id) + * .pipe(Effect.map((order) => acc + order.price)), + * while: (acc) => acc < 500 + * } + * ) + * + * Effect.runPromise(program).then(console.log) + * // Output: + * // Order 1 processed + * // Order 2 processed + * // Order 3 processed + * // 600 + * ``` + * + * @since 2.0.0 + * @category Collecting + */ +export const reduceWhile: { + ( + zero: Z, + options: { readonly while: Predicate; readonly body: (s: Z, a: A, i: number) => Effect } + ): (elements: Iterable) => Effect + ( + elements: Iterable, + zero: Z, + options: { readonly while: Predicate; readonly body: (s: Z, a: A, i: number) => Effect } + ): Effect +} = effect.reduceWhile + +/** + * Reduces an `Iterable` using an effectual function `f`, working + * sequentially from right to left. + * + * **Details** + * + * This function takes an iterable and applies a function `f` to each element in + * the iterable. The function works sequentially, starting with an initial value + * `zero` and then combining it with each element in the collection. The + * provided function `f` is called for each element in the iterable, allowing + * you to accumulate a result based on the current value and the element being + * processed. + * + * **When to Use** + * + * The function is often used for operations like summing a collection of + * numbers or combining results from multiple tasks. It ensures that operations + * are performed one after the other, maintaining the order of the elements. + * + * **Example** + * + * ```ts + * import { Console, Effect } from "effect" + * + * const processOrder = (id: number) => + * Effect.succeed({ id, price: 100 * id }) + * .pipe(Effect.tap(() => Console.log(`Order ${id} processed`)), Effect.delay(500 - (id * 100))) + * + * const program = Effect.reduceRight( + * [1, 2, 3, 4], + * 0, + * (id, acc, i) => + * processOrder(id) + * .pipe(Effect.map((order) => acc + order.price)) + * ) + * + * Effect.runPromise(program).then(console.log) + * // Output: + * // Order 4 processed + * // Order 3 processed + * // Order 2 processed + * // Order 1 processed + * // 1000 + * ``` + * + * @see {@link reduce} for a similar function that works from left to right. + * + * @since 2.0.0 + * @category Collecting + */ +export const reduceRight: { + (zero: Z, f: (a: A, z: Z, i: number) => Effect): (elements: Iterable) => Effect + (elements: Iterable, zero: Z, f: (a: A, z: Z, i: number) => Effect): Effect +} = effect.reduceRight + +/** + * Reduces an `Iterable>` to a single effect. + * + * **Details** + * + * This function processes a collection of effects and combines them into one + * single effect. It starts with an initial effect (`zero`) and applies a + * function `f` to each element in the collection. + * + * **Options** + * + * The function also allows you to customize how the effects are handled by + * specifying options such as concurrency, batching, and how finalizers behave. + * These options provide flexibility in running the effects concurrently or + * adjusting other execution details. + * + * **Example** + * + * ```ts + * import { Console, Effect } from "effect" + * + * const processOrder = (id: number) => + * Effect.succeed({ id, price: 100 * id }) + * .pipe(Effect.tap(() => Console.log(`Order ${id} processed`)), Effect.delay(500 - (id * 100))) + * + * const program = Effect.reduceEffect( + * [processOrder(1), processOrder(2), processOrder(3), processOrder(4)], + * Effect.succeed(0), + * (acc, order, i) => acc + order.price + * ) + * + * Effect.runPromise(program).then(console.log) + * // Output: + * // Order 1 processed + * // Order 2 processed + * // Order 3 processed + * // Order 4 processed + * // 1000 + * ``` + * + * @since 2.0.0 + * @category Collecting + */ +export const reduceEffect: { + >( + zero: Effect, + f: (z: NoInfer, a: Effect.Success, i: number) => Z, + options?: + | { + readonly concurrency?: Concurrency | undefined + readonly batching?: boolean | "inherit" | undefined + readonly concurrentFinalizers?: boolean | undefined + } + | undefined + ): (elements: Iterable) => Effect, R | Effect.Context> + , Z, E, R>( + elements: Iterable, + zero: Effect, + f: (z: NoInfer, a: Effect.Success, i: number) => Z, + options?: + | { + readonly concurrency?: Concurrency | undefined + readonly batching?: boolean | "inherit" | undefined + readonly concurrentFinalizers?: boolean | undefined + } + | undefined + ): Effect, R | Effect.Context> +} = fiberRuntime.reduceEffect + +/** + * Replicates the given effect `n` times. + * + * **Details** + * + * This function takes an effect and replicates it a specified number of times + * (`n`). The result is an array of `n` effects, each of which is identical to + * the original effect. + * + * **Example** + * + * ```ts + * import { Console, Effect } from "effect" + * + * const task = Effect.succeed("Hello, World!").pipe( + * Effect.tap(Console.log) + * ) + * + * const program = Effect.gen(function*() { + * // Replicate the task 3 times + * const tasks = Effect.replicate(task, 3) + * for (const t of tasks) { + * // Run each task + * yield* t + * } + * }) + * + * Effect.runFork(program) + * // Output: + * // Hello, World! + * // Hello, World! + * // Hello, World! + * ``` + * + * @since 2.0.0 + */ +export const replicate: { + (n: number): (self: Effect) => Array> + (self: Effect, n: number): Array> +} = fiberRuntime.replicate + +/** + * Performs this effect the specified number of times and collects the results. + * + * **Details** + * + * This function repeats an effect multiple times and collects the results into + * an array. You specify how many times to execute the effect, and it runs that + * many times, either in sequence or concurrently depending on the provided + * options. + * + * **Options** + * + * If the `discard` option is set to `true`, the intermediate results are not + * collected, and the final result of the operation is `void`. + * + * The function also allows you to customize how the effects are handled by + * specifying options such as concurrency, batching, and how finalizers behave. + * These options provide flexibility in running the effects concurrently or + * adjusting other execution details. + * + * **Example** + * + * ```ts + * import { Console, Effect } from "effect" + * + * let counter = 0 + * + * const task = Effect.sync(() => ++counter).pipe( + * Effect.tap(() => Console.log(`Task completed`)) + * ) + * + * const program = Effect.gen(function*() { + * // Replicate the task 3 times and collect the results + * const results = yield* Effect.replicateEffect(task, 3) + * yield* Console.log(`Results: ${results.join(", ")}`) + * }) + * + * Effect.runFork(program) + * // Output: + * // Task completed + * // Task completed + * // Task completed + * // Results: 1, 2, 3 + * ``` + * + * @since 2.0.0 + * @category Collecting + */ +export const replicateEffect: { + ( + n: number, + options?: { + readonly concurrency?: Concurrency | undefined + readonly batching?: boolean | "inherit" | undefined + readonly discard?: false | undefined + readonly concurrentFinalizers?: boolean | undefined + } + ): (self: Effect) => Effect, E, R> + ( + n: number, + options: { + readonly concurrency?: Concurrency | undefined + readonly batching?: boolean | "inherit" | undefined + readonly discard: true + readonly concurrentFinalizers?: boolean | undefined + } + ): (self: Effect) => Effect + ( + self: Effect, + n: number, + options?: { + readonly concurrency?: Concurrency | undefined + readonly batching?: boolean | "inherit" | undefined + readonly discard?: false | undefined + readonly concurrentFinalizers?: boolean | undefined + } + ): Effect, E, R> + ( + self: Effect, + n: number, + options: { + readonly concurrency?: Concurrency | undefined + readonly batching?: boolean | "inherit" | undefined + readonly discard: true + readonly concurrentFinalizers?: boolean | undefined + } + ): Effect +} = fiberRuntime.replicateEffect + +/** + * Applies an effectful operation to each element in a collection while + * collecting both successes and failures. + * + * **Details** + * + * This function allows you to apply an effectful operation to every item in a + * collection. + * + * Unlike {@link forEach}, which would stop at the first error, this function + * continues processing all elements, accumulating both successes and failures. + * + * **When to Use** + * + * Use this function when you want to process every item in a collection, even + * if some items fail. This is particularly useful when you need to perform + * operations on all elements without halting due to an error. + * + * Keep in mind that if there are any failures, **all successes will be lost**, + * so this function is not suitable when you need to keep the successful results + * in case of errors. + * + * **Example** + * + * ```ts + * import { Effect, Console } from "effect" + * + * // ┌─── Effect + * // ▼ + * const program = Effect.validateAll([1, 2, 3, 4, 5], (n) => { + * if (n < 4) { + * return Console.log(`item ${n}`).pipe(Effect.as(n)) + * } else { + * return Effect.fail(`${n} is not less that 4`) + * } + * }) + * + * Effect.runPromiseExit(program).then(console.log) + * // Output: + * // item 1 + * // item 2 + * // item 3 + * // { + * // _id: 'Exit', + * // _tag: 'Failure', + * // cause: { + * // _id: 'Cause', + * // _tag: 'Fail', + * // failure: [ '4 is not less that 4', '5 is not less that 4' ] + * // } + * // } + * ``` + * + * @see {@link forEach} for a similar function that stops at the first error. + * @see {@link partition} when you need to separate successes and failures + * instead of losing successes with errors. + * + * @since 2.0.0 + * @category Error Accumulation + */ +export const validateAll: { + ( + f: (a: A, i: number) => Effect, + options?: { + readonly concurrency?: Concurrency | undefined + readonly batching?: boolean | "inherit" | undefined + readonly discard?: false | undefined + readonly concurrentFinalizers?: boolean | undefined + } | undefined + ): (elements: Iterable) => Effect, RA.NonEmptyArray, R> + ( + f: (a: A, i: number) => Effect, + options: { + readonly concurrency?: Concurrency | undefined + readonly batching?: boolean | "inherit" | undefined + readonly discard: true + readonly concurrentFinalizers?: boolean | undefined + } + ): (elements: Iterable) => Effect, R> + ( + elements: Iterable, + f: (a: A, i: number) => Effect, + options?: { + readonly concurrency?: Concurrency | undefined + readonly batching?: boolean | "inherit" | undefined + readonly discard?: false | undefined + readonly concurrentFinalizers?: boolean | undefined + } | undefined + ): Effect, RA.NonEmptyArray, R> + ( + elements: Iterable, + f: (a: A, i: number) => Effect, + options: { + readonly concurrency?: Concurrency | undefined + readonly batching?: boolean | "inherit" | undefined + readonly discard: true + readonly concurrentFinalizers?: boolean | undefined + } + ): Effect, R> +} = fiberRuntime.validateAll + +/** + * This function is similar to {@link validateAll} but with a key difference: it + * returns the first successful result or all errors if none of the operations + * succeed. + * + * **Details** + * + * This function processes a collection of elements and applies an effectful + * operation to each. Unlike {@link validateAll}, which accumulates both + * successes and failures, `Effect.validateFirst` stops and returns the first + * success it encounters. If no success occurs, it returns all accumulated + * errors. This can be useful when you are interested in the first successful + * result and want to avoid processing further once a valid result is found. + * + * **Example** + * + * ```ts + * import { Effect, Console } from "effect" + * + * // ┌─── Effect + * // ▼ + * const program = Effect.validateFirst([1, 2, 3, 4, 5], (n) => { + * if (n < 4) { + * return Effect.fail(`${n} is not less that 4`) + * } else { + * return Console.log(`item ${n}`).pipe(Effect.as(n)) + * } + * }) + * + * Effect.runPromise(program).then(console.log, console.error) + * // Output: + * // item 4 + * // 4 + * ``` + * + * @see {@link validateAll} for a similar function that accumulates all results. + * @see {@link firstSuccessOf} for a similar function that processes multiple + * effects and returns the first successful one or the last error. + * + * @since 2.0.0 + * @category Error Accumulation + */ +export const validateFirst: { + ( + f: (a: A, i: number) => Effect, + options?: + | { + readonly concurrency?: Concurrency | undefined + readonly batching?: boolean | "inherit" | undefined + readonly concurrentFinalizers?: boolean | undefined + } + | undefined + ): (elements: Iterable) => Effect, R> + ( + elements: Iterable, + f: (a: A, i: number) => Effect, + options?: + | { + readonly concurrency?: Concurrency | undefined + readonly batching?: boolean | "inherit" | undefined + readonly concurrentFinalizers?: boolean | undefined + } + | undefined + ): Effect, R> +} = fiberRuntime.validateFirst + +/** + * Creates an `Effect` from a callback-based asynchronous function. + * + * **Details** + * + * The `resume` function: + * - Must be called exactly once. Any additional calls will be ignored. + * - Can return an optional `Effect` that will be run if the `Fiber` executing + * this `Effect` is interrupted. This can be useful in scenarios where you + * need to handle resource cleanup if the operation is interrupted. + * - Can receive an `AbortSignal` to handle interruption if needed. + * + * The `FiberId` of the fiber that may complete the async callback may also be + * specified using the `blockingOn` argument. This is called the "blocking + * fiber" because it suspends the fiber executing the `async` effect (i.e. + * semantically blocks the fiber from making progress). Specifying this fiber id + * in cases where it is known will improve diagnostics, but not affect the + * behavior of the returned effect. + * + * **When to Use** + * + * Use `Effect.async` when dealing with APIs that use callback-style instead of + * `async/await` or `Promise`. + * + * **Example** (Wrapping a Callback API) + * + * ```ts + * import { Effect } from "effect" + * import * as NodeFS from "node:fs" + * + * const readFile = (filename: string) => + * Effect.async((resume) => { + * NodeFS.readFile(filename, (error, data) => { + * if (error) { + * // Resume with a failed Effect if an error occurs + * resume(Effect.fail(error)) + * } else { + * // Resume with a succeeded Effect if successful + * resume(Effect.succeed(data)) + * } + * }) + * }) + * + * // ┌─── Effect + * // ▼ + * const program = readFile("example.txt") + * ``` + * + * **Example** (Handling Interruption with Cleanup) + * + * ```ts + * import { Effect, Fiber } from "effect" + * import * as NodeFS from "node:fs" + * + * // Simulates a long-running operation to write to a file + * const writeFileWithCleanup = (filename: string, data: string) => + * Effect.async((resume) => { + * const writeStream = NodeFS.createWriteStream(filename) + * + * // Start writing data to the file + * writeStream.write(data) + * + * // When the stream is finished, resume with success + * writeStream.on("finish", () => resume(Effect.void)) + * + * // In case of an error during writing, resume with failure + * writeStream.on("error", (err) => resume(Effect.fail(err))) + * + * // Handle interruption by returning a cleanup effect + * return Effect.sync(() => { + * console.log(`Cleaning up ${filename}`) + * NodeFS.unlinkSync(filename) + * }) + * }) + * + * const program = Effect.gen(function* () { + * const fiber = yield* Effect.fork( + * writeFileWithCleanup("example.txt", "Some long data...") + * ) + * // Simulate interrupting the fiber after 1 second + * yield* Effect.sleep("1 second") + * yield* Fiber.interrupt(fiber) // This will trigger the cleanup + * }) + * + * // Run the program + * Effect.runPromise(program) + * // Output: + * // Cleaning up example.txt + * ``` + * + * **Example** (Handling Interruption with AbortSignal) + * + * ```ts + * import { Effect, Fiber } from "effect" + * + * // A task that supports interruption using AbortSignal + * const interruptibleTask = Effect.async((resume, signal) => { + * // Handle interruption + * signal.addEventListener("abort", () => { + * console.log("Abort signal received") + * clearTimeout(timeoutId) + * }) + * + * // Simulate a long-running task + * const timeoutId = setTimeout(() => { + * console.log("Operation completed") + * resume(Effect.void) + * }, 2000) + * }) + * + * const program = Effect.gen(function* () { + * const fiber = yield* Effect.fork(interruptibleTask) + * // Simulate interrupting the fiber after 1 second + * yield* Effect.sleep("1 second") + * yield* Fiber.interrupt(fiber) + * }) + * + * // Run the program + * Effect.runPromise(program) + * // Output: + * // Abort signal received + * ``` + * + * @since 2.0.0 + * @category Creating Effects + */ +export const async: ( + resume: (callback: (_: Effect) => void, signal: AbortSignal) => void | Effect, + blockingOn?: FiberId.FiberId +) => Effect = core.async + +/** + * A variant of {@link async} where the registration function may return an `Effect`. + * + * @since 2.0.0 + * @category Creating Effects + */ +export const asyncEffect: ( + register: (callback: (_: Effect) => void) => Effect | void, E2, R2> +) => Effect = runtime_.asyncEffect + +/** + * Low level constructor that enables for custom stack tracing cutpoints. + * + * It is meant to be called with a bag of instructions that become available in + * the "this" of the effect. + * + * **Example** + * + * ```ts + * import { Effect } from "effect" + * + * const throwingFunction = () => { throw new Error() } + * const blowUp = Effect.custom(throwingFunction, function() { + * return Effect.succeed(this.effect_instruction_i0()) + * }) + * ``` + * + * @since 2.0.0 + * @category Creating Effects + */ +export const custom: { + (i0: X, body: (this: { effect_instruction_i0: X }) => Effect): Effect + ( + i0: X, + i1: Y, + body: (this: { effect_instruction_i0: X; effect_instruction_i1: Y }) => Effect + ): Effect + ( + i0: X, + i1: Y, + i2: Z, + body: (this: { effect_instruction_i0: X; effect_instruction_i1: Y; effect_instruction_i2: Z }) => Effect + ): Effect +} = core.custom + +/** + * @since 2.0.0 + * @category Creating Effects + */ +export const withFiberRuntime: ( + withRuntime: ( + fiber: Fiber.RuntimeFiber, + status: FiberStatus.Running + ) => Effect +) => Effect = core.withFiberRuntime + +/** + * Creates an `Effect` that represents a recoverable error. + * + * **When to Use** + * + * Use this function to explicitly signal an error in an `Effect`. The error + * will keep propagating unless it is handled. You can handle the error with + * functions like {@link catchAll} or {@link catchTag}. + * + * **Example** (Creating a Failed Effect) + * + * ```ts + * import { Effect } from "effect" + * + * // ┌─── Effect + * // ▼ + * const failure = Effect.fail( + * new Error("Operation failed due to network error") + * ) + * ``` + * + * @see {@link succeed} to create an effect that represents a successful value. + * + * @since 2.0.0 + * @category Creating Effects + */ +export const fail: (error: E) => Effect = core.fail + +/** + * Creates an `Effect` that fails with the specified error, evaluated lazily. + * + * @since 2.0.0 + * @category Creating Effects + */ +export const failSync: (evaluate: LazyArg) => Effect = core.failSync + +/** + * Creates an `Effect` that fails with the specified `Cause`. + * + * @since 2.0.0 + * @category Creating Effects + */ +export const failCause: (cause: Cause.Cause) => Effect = core.failCause + +/** + * Creates an `Effect` that fails with the specified `Cause`, evaluated lazily. + * + * @since 2.0.0 + * @category Creating Effects + */ +export const failCauseSync: (evaluate: LazyArg>) => Effect = core.failCauseSync + +/** + * Creates an effect that terminates a fiber with a specified error. + * + * **Details** + * + * This function is used to signal a defect, which represents a critical and + * unexpected error in the code. When invoked, it produces an effect that does + * not handle the error and instead terminates the fiber. + * + * The error channel of the resulting effect is of type `never`, indicating that + * it cannot recover from this failure. + * + * **When to Use** + * + * Use this function when encountering unexpected conditions in your code that + * should not be handled as regular errors but instead represent unrecoverable + * defects. + * + * **Example** (Terminating on Division by Zero with a Specified Error) + * + * ```ts + * import { Effect } from "effect" + * + * const divide = (a: number, b: number) => + * b === 0 + * ? Effect.die(new Error("Cannot divide by zero")) + * : Effect.succeed(a / b) + * + * // ┌─── Effect + * // ▼ + * const program = divide(1, 0) + * + * Effect.runPromise(program).catch(console.error) + * // Output: + * // (FiberFailure) Error: Cannot divide by zero + * // ...stack trace... + * ``` + * + * @see {@link dieSync} for a variant that throws a specified error, evaluated + * lazily. + * @see {@link dieMessage} for a variant that throws a `RuntimeException` with a + * message. + * + * @since 2.0.0 + * @category Creating Effects + */ +export const die: (defect: unknown) => Effect = core.die + +/** + * Creates an effect that terminates a fiber with a `RuntimeException` + * containing the specified message. + * + * **Details** + * + * This function is used to signal a defect, representing a critical and + * unexpected error in the code. When invoked, it produces an effect that + * terminates the fiber with a `RuntimeException` carrying the given message. + * + * The resulting effect has an error channel of type `never`, indicating it does + * not handle or recover from the error. + * + * **When to Use** + * + * Use this function when you want to terminate a fiber due to an unrecoverable + * defect and include a clear explanation in the message. + * + * **Example** (Terminating on Division by Zero with a Specified Message) + * + * ```ts + * import { Effect } from "effect" + * + * const divide = (a: number, b: number) => + * b === 0 + * ? Effect.dieMessage("Cannot divide by zero") + * : Effect.succeed(a / b) + * + * // ┌─── Effect + * // ▼ + * const program = divide(1, 0) + * + * Effect.runPromise(program).catch(console.error) + * // Output: + * // (FiberFailure) RuntimeException: Cannot divide by zero + * // ...stack trace... + * ``` + * + * @see {@link die} for a variant that throws a specified error. + * @see {@link dieSync} for a variant that throws a specified error, evaluated + * lazily. + * + * @since 2.0.0 + * @category Creating Effects + */ +export const dieMessage: (message: string) => Effect = core.dieMessage + +/** + * Creates an effect that dies with the specified error, evaluated lazily. + * + * **Details** + * + * This function allows you to create an effect that will terminate with a fatal error. + * The error is provided as a lazy argument, meaning it will only be evaluated when the effect runs. + * + * @see {@link die} if you don't need to evaluate the error lazily. + * + * @since 2.0.0 + * @category Creating Effects + */ +export const dieSync: (evaluate: LazyArg) => Effect = core.dieSync + +/** + * Provides a way to write effectful code using generator functions, simplifying + * control flow and error handling. + * + * **When to Use** + * + * `Effect.gen` allows you to write code that looks and behaves like synchronous + * code, but it can handle asynchronous tasks, errors, and complex control flow + * (like loops and conditions). It helps make asynchronous code more readable + * and easier to manage. + * + * The generator functions work similarly to `async/await` but with more + * explicit control over the execution of effects. You can `yield*` values from + * effects and return the final result at the end. + * + * **Example** + * + * ```ts + * import { Effect } from "effect" + * + * const addServiceCharge = (amount: number) => amount + 1 + * + * const applyDiscount = ( + * total: number, + * discountRate: number + * ): Effect.Effect => + * discountRate === 0 + * ? Effect.fail(new Error("Discount rate cannot be zero")) + * : Effect.succeed(total - (total * discountRate) / 100) + * + * const fetchTransactionAmount = Effect.promise(() => Promise.resolve(100)) + * + * const fetchDiscountRate = Effect.promise(() => Promise.resolve(5)) + * + * export const program = Effect.gen(function* () { + * const transactionAmount = yield* fetchTransactionAmount + * const discountRate = yield* fetchDiscountRate + * const discountedAmount = yield* applyDiscount( + * transactionAmount, + * discountRate + * ) + * const finalAmount = addServiceCharge(discountedAmount) + * return `Final amount to charge: ${finalAmount}` + * }) + * ``` + * + * @since 2.0.0 + * @category Creating Effects + */ +export const gen: { + >, AEff>( + f: (resume: Adapter) => Generator + ): Effect< + AEff, + [Eff] extends [never] ? never : [Eff] extends [YieldWrap>] ? E : never, + [Eff] extends [never] ? never : [Eff] extends [YieldWrap>] ? R : never + > + >, AEff>( + self: Self, + f: (this: Self, resume: Adapter) => Generator + ): Effect< + AEff, + [Eff] extends [never] ? never : [Eff] extends [YieldWrap>] ? E : never, + [Eff] extends [never] ? never : [Eff] extends [YieldWrap>] ? R : never + > +} = core.gen + +/** + * @since 2.0.0 + * @category Models + */ +export interface Adapter { + (self: Effect): Effect + (a: A, ab: (a: A) => Effect<_A, _E, _R>): Effect<_A, _E, _R> + (a: A, ab: (a: A) => B, bc: (b: B) => Effect<_A, _E, _R>): Effect<_A, _E, _R> + (a: A, ab: (a: A) => B, bc: (b: B) => C, cd: (c: C) => Effect<_A, _E, _R>): Effect<_A, _E, _R> + ( + a: A, + ab: (a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D, + de: (d: D) => Effect<_A, _E, _R> + ): Effect<_A, _E, _R> + ( + a: A, + ab: (a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D, + de: (d: D) => E, + ef: (e: E) => Effect<_A, _E, _R> + ): Effect<_A, _E, _R> + ( + a: A, + ab: (a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D, + de: (d: D) => E, + ef: (e: E) => F, + fg: (f: F) => Effect<_A, _E, _R> + ): Effect<_A, _E, _R> + ( + a: A, + ab: (a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D, + de: (d: D) => E, + ef: (e: E) => F, + fg: (f: F) => G, + gh: (g: G) => Effect<_A, _E, _R> + ): Effect<_A, _E, _R> + ( + a: A, + ab: (a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D, + de: (d: D) => E, + ef: (e: E) => F, + fg: (f: F) => G, + gh: (g: G) => H, + hi: (g: H) => Effect<_A, _E, _R> + ): Effect<_A, _E, _R> + ( + a: A, + ab: (a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D, + de: (d: D) => E, + ef: (e: E) => F, + fg: (f: F) => G, + gh: (g: G) => H, + hi: (h: H) => I, + ij: (i: I) => Effect<_A, _E, _R> + ): Effect<_A, _E, _R> + ( + a: A, + ab: (a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D, + de: (d: D) => E, + ef: (e: E) => F, + fg: (f: F) => G, + gh: (g: G) => H, + hi: (h: H) => I, + ij: (i: I) => J, + jk: (j: J) => Effect<_A, _E, _R> + ): Effect<_A, _E, _R> + ( + a: A, + ab: (a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D, + de: (d: D) => E, + ef: (e: E) => F, + fg: (f: F) => G, + gh: (g: G) => H, + hi: (h: H) => I, + ij: (i: I) => J, + jk: (j: J) => K, + kl: (k: K) => Effect<_A, _E, _R> + ): Effect<_A, _E, _R> + ( + a: A, + ab: (a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D, + de: (d: D) => E, + ef: (e: E) => F, + fg: (f: F) => G, + gh: (g: G) => H, + hi: (h: H) => I, + ij: (i: I) => J, + jk: (j: J) => K, + kl: (k: K) => L, + lm: (l: L) => Effect<_A, _E, _R> + ): Effect<_A, _E, _R> + ( + a: A, + ab: (a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D, + de: (d: D) => E, + ef: (e: E) => F, + fg: (f: F) => G, + gh: (g: G) => H, + hi: (h: H) => I, + ij: (i: I) => J, + jk: (j: J) => K, + kl: (k: K) => L, + lm: (l: L) => M, + mn: (m: M) => Effect<_A, _E, _R> + ): Effect<_A, _E, _R> + ( + a: A, + ab: (a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D, + de: (d: D) => E, + ef: (e: E) => F, + fg: (f: F) => G, + gh: (g: G) => H, + hi: (h: H) => I, + ij: (i: I) => J, + jk: (j: J) => K, + kl: (k: K) => L, + lm: (l: L) => M, + mn: (m: M) => N, + no: (n: N) => Effect<_A, _E, _R> + ): Effect<_A, _E, _R> + ( + a: A, + ab: (a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D, + de: (d: D) => E, + ef: (e: E) => F, + fg: (f: F) => G, + gh: (g: G) => H, + hi: (h: H) => I, + ij: (i: I) => J, + jk: (j: J) => K, + kl: (k: K) => L, + lm: (l: L) => M, + mn: (m: M) => N, + no: (n: N) => O, + op: (o: O) => Effect<_A, _E, _R> + ): Effect<_A, _E, _R> + ( + a: A, + ab: (a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D, + de: (d: D) => E, + ef: (e: E) => F, + fg: (f: F) => G, + gh: (g: G) => H, + hi: (h: H) => I, + ij: (i: I) => J, + jk: (j: J) => K, + kl: (k: K) => L, + lm: (l: L) => M, + mn: (m: M) => N, + no: (n: N) => O, + op: (o: O) => P, + pq: (p: P) => Effect<_A, _E, _R> + ): Effect<_A, _E, _R> + ( + a: A, + ab: (a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D, + de: (d: D) => E, + ef: (e: E) => F, + fg: (f: F) => G, + gh: (g: G) => H, + hi: (h: H) => I, + ij: (i: I) => J, + jk: (j: J) => K, + kl: (k: K) => L, + lm: (l: L) => M, + mn: (m: M) => N, + no: (n: N) => O, + op: (o: O) => P, + pq: (p: P) => Q, + qr: (q: Q) => Effect<_A, _E, _R> + ): Effect<_A, _E, _R> + ( + a: A, + ab: (a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D, + de: (d: D) => E, + ef: (e: E) => F, + fg: (f: F) => G, + gh: (g: G) => H, + hi: (h: H) => I, + ij: (i: I) => J, + jk: (j: J) => K, + kl: (k: K) => L, + lm: (l: L) => M, + mn: (m: M) => N, + no: (n: N) => O, + op: (o: O) => P, + pq: (p: P) => Q, + qr: (q: Q) => R, + rs: (r: R) => Effect<_A, _E, _R> + ): Effect<_A, _E, _R> + ( + a: A, + ab: (a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D, + de: (d: D) => E, + ef: (e: E) => F, + fg: (f: F) => G, + gh: (g: G) => H, + hi: (h: H) => I, + ij: (i: I) => J, + jk: (j: J) => K, + kl: (k: K) => L, + lm: (l: L) => M, + mn: (m: M) => N, + no: (n: N) => O, + op: (o: O) => P, + pq: (p: P) => Q, + qr: (q: Q) => R, + rs: (r: R) => S, + st: (s: S) => Effect<_A, _E, _R> + ): Effect<_A, _E, _R> + ( + a: A, + ab: (a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D, + de: (d: D) => E, + ef: (e: E) => F, + fg: (f: F) => G, + gh: (g: G) => H, + hi: (h: H) => I, + ij: (i: I) => J, + jk: (j: J) => K, + kl: (k: K) => L, + lm: (l: L) => M, + mn: (m: M) => N, + no: (n: N) => O, + op: (o: O) => P, + pq: (p: P) => Q, + qr: (q: Q) => R, + rs: (r: R) => S, + st: (s: S) => T, + tu: (s: T) => Effect<_A, _E, _R> + ): Effect<_A, _E, _R> +} + +/** + * An effect that that runs indefinitely and never produces any result. The + * moral equivalent of `while(true) {}`, only without the wasted CPU cycles. + * + * **When to Use** + * + * It could be useful for long-running background tasks or to simulate waiting + * behavior without actually consuming resources. This effect is ideal for cases + * where you want to keep the program alive or in a certain state without + * performing any active work. + * + * @since 2.0.0 + * @category Creating Effects + */ +export const never: Effect = core.never + +/** + * Ensures the `Option` is `None`, returning `void`. Otherwise, raises a + * `NoSuchElementException`. + * + * **Details** + * + * This function checks if the provided `Option` is `None`. If it is, it returns + * an effect that produces no result (i.e., `void`). If the `Option` is not + * `None` (i.e., it contains a value), the function will raise a + * `NoSuchElementException` error. + * + * **When to Use** + * + * This is useful when you want to ensure that a certain value is absent (i.e., + * `None`) before continuing execution, and to handle cases where the value is + * unexpectedly present. + * + * @since 2.0.0 + */ +export const none: ( + self: Effect, E, R> +) => Effect = effect.none + +/** + * Creates an `Effect` that represents an asynchronous computation guaranteed to + * succeed. + * + * **Details** + * + * The provided function (`thunk`) returns a `Promise` that should never reject; if it does, the error + * will be treated as a "defect". + * + * This defect is not a standard error but indicates a flaw in the logic that + * was expected to be error-free. You can think of it similar to an unexpected + * crash in the program, which can be further managed or logged using tools like + * {@link catchAllDefect}. + * + * **Interruptions** + * + * An optional `AbortSignal` can be provided to allow for interruption of the + * wrapped `Promise` API. + * + * **When to Use** + * + * Use this function when you are sure the operation will not reject. + * + * **Example** (Delayed Message) + * + * ```ts + * import { Effect } from "effect" + * + * const delay = (message: string) => + * Effect.promise( + * () => + * new Promise((resolve) => { + * setTimeout(() => { + * resolve(message) + * }, 2000) + * }) + * ) + * + * // ┌─── Effect + * // ▼ + * const program = delay("Async operation completed successfully!") + * ``` + * + * @see {@link tryPromise} for a version that can handle failures. + * + * @since 2.0.0 + * @category Creating Effects + */ +export const promise: ( + evaluate: (signal: AbortSignal) => PromiseLike +) => Effect = effect.promise + +/** + * Creates an `Effect` that always succeeds with a given value. + * + * **When to Use** + * + * Use this function when you need an effect that completes successfully with a + * specific value without any errors or external dependencies. + * + * **Example** (Creating a Successful Effect) + * + * ```ts + * import { Effect } from "effect" + * + * // Creating an effect that represents a successful scenario + * // + * // ┌─── Effect + * // ▼ + * const success = Effect.succeed(42) + * ``` + * + * @see {@link fail} to create an effect that represents a failure. + * + * @since 2.0.0 + * @category Creating Effects + */ +export const succeed: (value: A) => Effect = core.succeed + +/** + * Returns an effect which succeeds with `None`. + * + * **When to Use** + * + * Use this function when you need to represent the absence of a value in your + * code, especially when working with optional data. This can be helpful when + * you want to indicate that no result is available without throwing an error or + * performing additional logic. + * + * @see {@link succeedSome} to create an effect that succeeds with a `Some` value. + * + * @since 2.0.0 + * @category Creating Effects + */ +export const succeedNone: Effect> = effect.succeedNone + +/** + * Returns an effect which succeeds with the value wrapped in a `Some`. + * + * @see {@link succeedNone} for a similar function that returns `None` when the value is absent. + * + * @since 2.0.0 + * @category Creating Effects + */ +export const succeedSome: (value: A) => Effect> = effect.succeedSome + +/** + * Delays the creation of an `Effect` until it is actually needed. + * + * **Details** + * + * The `Effect.suspend` function takes a thunk that represents the effect and + * wraps it in a suspended effect. This means the effect will not be created + * until it is explicitly needed, which is helpful in various scenarios: + * - **Lazy Evaluation**: Helps optimize performance by deferring computations, + * especially when the effect might not be needed, or when its computation is + * expensive. This also ensures that any side effects or scoped captures are + * re-executed on each invocation. + * - **Handling Circular Dependencies**: Useful in managing circular + * dependencies, such as recursive functions that need to avoid eager + * evaluation to prevent stack overflow. + * - **Unifying Return Types**: Can help TypeScript unify return types in + * situations where multiple branches of logic return different effects, + * simplifying type inference. + * + * **When to Use** + * + * Use this function when you need to defer the evaluation of an effect until it + * is required. This is particularly useful for optimizing expensive + * computations, managing circular dependencies, or resolving type inference + * issues. + * + * **Example** (Lazy Evaluation with Side Effects) + * + * ```ts + * import { Effect } from "effect" + * + * let i = 0 + * + * const bad = Effect.succeed(i++) + * + * const good = Effect.suspend(() => Effect.succeed(i++)) + * + * console.log(Effect.runSync(bad)) // Output: 0 + * console.log(Effect.runSync(bad)) // Output: 0 + * + * console.log(Effect.runSync(good)) // Output: 1 + * console.log(Effect.runSync(good)) // Output: 2 + * ``` + * + * **Example** (Recursive Fibonacci) + * + * ```ts + * import { Effect } from "effect" + * + * const blowsUp = (n: number): Effect.Effect => + * n < 2 + * ? Effect.succeed(1) + * : Effect.zipWith(blowsUp(n - 1), blowsUp(n - 2), (a, b) => a + b) + * + * console.log(Effect.runSync(blowsUp(32))) + * // crash: JavaScript heap out of memory + * + * const allGood = (n: number): Effect.Effect => + * n < 2 + * ? Effect.succeed(1) + * : Effect.zipWith( + * Effect.suspend(() => allGood(n - 1)), + * Effect.suspend(() => allGood(n - 2)), + * (a, b) => a + b + * ) + * + * console.log(Effect.runSync(allGood(32))) + * // Output: 3524578 + * ``` + * + * **Example** (Using Effect.suspend to Help TypeScript Infer Types) + * + * ```ts + * import { Effect } from "effect" + * + * // Without suspend, TypeScript may struggle with type inference. + * // Inferred type: + * // (a: number, b: number) => + * // Effect | Effect + * const withoutSuspend = (a: number, b: number) => + * b === 0 + * ? Effect.fail(new Error("Cannot divide by zero")) + * : Effect.succeed(a / b) + * + * // Using suspend to unify return types. + * // Inferred type: + * // (a: number, b: number) => Effect + * const withSuspend = (a: number, b: number) => + * Effect.suspend(() => + * b === 0 + * ? Effect.fail(new Error("Cannot divide by zero")) + * : Effect.succeed(a / b) + * ) + * ``` + * + * @since 2.0.0 + * @category Creating Effects + */ +export const suspend: (effect: LazyArg>) => Effect = core.suspend + +/** + * Creates an `Effect` that represents a synchronous side-effectful computation. + * + * **Details** + * + * The provided function (`thunk`) must not throw errors; if it does, the error + * will be treated as a "defect". + * + * This defect is not a standard error but indicates a flaw in the logic that + * was expected to be error-free. You can think of it similar to an unexpected + * crash in the program, which can be further managed or logged using tools like + * {@link catchAllDefect}. + * + * **When to Use** + * + * Use this function when you are sure the operation will not fail. + * + * **Example** (Logging a Message) + * + * ```ts + * import { Effect } from "effect" + * + * const log = (message: string) => + * Effect.sync(() => { + * console.log(message) // side effect + * }) + * + * // ┌─── Effect + * // ▼ + * const program = log("Hello, World!") + * ``` + * + * @see {@link try_ | try} for a version that can handle failures. + * + * @since 2.0.0 + * @category Creating Effects + */ +export const sync: (thunk: LazyArg) => Effect = core.sync + +const _void: Effect = core.void + +export { + /** + * Represents an effect that does nothing and produces no value. + * + * **When to Use** + * + * Use this effect when you need to represent an effect that does nothing. + * This is useful in scenarios where you need to satisfy an effect-based + * interface or control program flow without performing any operations. For + * example, it can be used in situations where you want to return an effect + * from a function but do not need to compute or return any result. + * + * @since 2.0.0 + * @category Creating Effects + */ + _void as void +} + +/** + * @since 2.0.0 + * @category Creating Effects + */ +export const yieldNow: (options?: { + readonly priority?: number | undefined +}) => Effect = core.yieldNow + +const _catch: { + ( + discriminator: N, + options: { readonly failure: K; readonly onFailure: (error: Extract) => Effect } + ): (self: Effect) => Effect, R1 | R> + ( + self: Effect, + discriminator: N, + options: { readonly failure: K; readonly onFailure: (error: Extract) => Effect } + ): Effect, R | R1> +} = effect._catch + +export { + /** + * Recovers from a specified error by catching it and handling it with a provided function. + * + * **Details** + * + * This function allows you to recover from specific errors that occur during + * the execution of an effect. It works by catching a specific type of error + * (identified by a discriminator) and then handling it using a provided + * handler function. The handler can return a new effect that helps recover + * from the error, allowing the program to continue. If the error doesn't + * match the specified type, the function allows the original effect to + * continue as it was. + * + * **Example** + * + * ```ts + * import { Console, Effect } from "effect" + * + * class NetworkError { + * readonly _tag = "NetworkError" + * } + * class ValidationError { + * readonly _tag = "ValidationError" + * } + * + * // Simulate an effect that may fail + * const task: Effect.Effect = Effect.fail(new NetworkError()) + * + * const program = Effect.gen(function*() { + * const result = yield* Effect.catch(task, "_tag", { + * failure: "NetworkError", + * onFailure: (error) => Effect.succeed(`recovered from error: ${error._tag}`) + * }) + * console.log(`Result: ${result}`) + * }) + * + * Effect.runFork(program) + * // Output: Result: recovered from error: NetworkError + * ``` + * + * @see {@link catchTag} for a version that can recover from errors based on a `_tag` discriminator. + * + * @since 2.0.0 + * @category Error handling + */ + _catch as catch +} + +/** + * Handles all errors in an effect by providing a fallback effect. + * + * **Details** + * + * This function catches any errors that may occur during the execution of an + * effect and allows you to handle them by specifying a fallback effect. This + * ensures that the program continues without failing by recovering from errors + * using the provided fallback logic. + * + * **Note**: This function only handles recoverable errors. It will not recover + * from unrecoverable defects. + * + * **Example** (Providing Recovery Logic for Recoverable Errors) + * + * ```ts + * import { Effect, Random } from "effect" + * + * class HttpError { + * readonly _tag = "HttpError" + * } + * + * class ValidationError { + * readonly _tag = "ValidationError" + * } + * + * // ┌─── Effect + * // ▼ + * const program = Effect.gen(function* () { + * const n1 = yield* Random.next + * const n2 = yield* Random.next + * if (n1 < 0.5) { + * yield* Effect.fail(new HttpError()) + * } + * if (n2 < 0.5) { + * yield* Effect.fail(new ValidationError()) + * } + * return "some result" + * }) + * + * // ┌─── Effect + * // ▼ + * const recovered = program.pipe( + * Effect.catchAll((error) => + * Effect.succeed(`Recovering from ${error._tag}`) + * ) + * ) + * ``` + * + * @see {@link catchAllCause} for a version that can recover from both + * recoverable and unrecoverable errors. + * + * @since 2.0.0 + * @category Error handling + */ +export const catchAll: { + (f: (e: E) => Effect): (self: Effect) => Effect + (self: Effect, f: (e: E) => Effect): Effect +} = core.catchAll + +/** + * Handles both recoverable and unrecoverable errors by providing a recovery + * effect. + * + * **When to Use** + * + * The `catchAllCause` function allows you to handle all errors, including + * unrecoverable defects, by providing a recovery effect. The recovery logic is + * based on the `Cause` of the error, which provides detailed information about + * the failure. + * + * **When to Recover from Defects** + * + * Defects are unexpected errors that typically shouldn't be recovered from, as + * they often indicate serious issues. However, in some cases, such as + * dynamically loaded plugins, controlled recovery might be needed. + * + * **Example** (Recovering from All Errors) + * + * ```ts + * import { Cause, Effect } from "effect" + * + * // Define an effect that may fail with a recoverable or unrecoverable error + * const program = Effect.fail("Something went wrong!") + * + * // Recover from all errors by examining the cause + * const recovered = program.pipe( + * Effect.catchAllCause((cause) => + * Cause.isFailure(cause) + * ? Effect.succeed("Recovered from a regular error") + * : Effect.succeed("Recovered from a defect") + * ) + * ) + * + * Effect.runPromise(recovered).then(console.log) + * // Output: "Recovered from a regular error" + * ``` + * + * @since 2.0.0 + * @category Error handling + */ +export const catchAllCause: { + ( + f: (cause: Cause.Cause) => Effect + ): (self: Effect) => Effect + ( + self: Effect, + f: (cause: Cause.Cause) => Effect + ): Effect +} = core.catchAllCause + +/** + * Recovers from all defects using a provided recovery function. + * + * **When to Use** + * + * There is no sensible way to recover from defects. This method should be used + * only at the boundary between Effect and an external system, to transmit + * information on a defect for diagnostic or explanatory purposes. + * + * **Details** + * + * `catchAllDefect` allows you to handle defects, which are unexpected errors + * that usually cause the program to terminate. This function lets you recover + * from these defects by providing a function that handles the error. However, + * it does not handle expected errors (like those from {@link fail}) or + * execution interruptions (like those from {@link interrupt}). + * + * **When to Recover from Defects** + * + * Defects are unexpected errors that typically shouldn't be recovered from, as + * they often indicate serious issues. However, in some cases, such as + * dynamically loaded plugins, controlled recovery might be needed. + * + * **Example** (Handling All Defects) + * + * ```ts + * import { Effect, Cause, Console } from "effect" + * + * // Simulating a runtime error + * const task = Effect.dieMessage("Boom!") + * + * const program = Effect.catchAllDefect(task, (defect) => { + * if (Cause.isRuntimeException(defect)) { + * return Console.log( + * `RuntimeException defect caught: ${defect.message}` + * ) + * } + * return Console.log("Unknown defect caught.") + * }) + * + * // We get an Exit.Success because we caught all defects + * Effect.runPromiseExit(program).then(console.log) + * // Output: + * // RuntimeException defect caught: Boom! + * // { + * // _id: "Exit", + * // _tag: "Success", + * // value: undefined + * // } + * ``` + * + * @since 2.0.0 + * @category Error handling + */ +export const catchAllDefect: { + ( + f: (defect: unknown) => Effect + ): (self: Effect) => Effect + ( + self: Effect, + f: (defect: unknown) => Effect + ): Effect +} = effect.catchAllDefect + +/** + * Recovers from specific errors based on a predicate. + * + * **When to Use** + * + * `catchIf` works similarly to {@link catchSome}, but it allows you to + * recover from errors by providing a predicate function. If the predicate + * matches the error, the recovery effect is applied. This function doesn't + * alter the error type, so the resulting effect still carries the original + * error type unless a user-defined type guard is used to narrow the type. + * + * **Example** (Catching Specific Errors with a Predicate) + * + * ```ts + * import { Effect, Random } from "effect" + * + * class HttpError { + * readonly _tag = "HttpError" + * } + * + * class ValidationError { + * readonly _tag = "ValidationError" + * } + * + * // ┌─── Effect + * // ▼ + * const program = Effect.gen(function* () { + * const n1 = yield* Random.next + * const n2 = yield* Random.next + * if (n1 < 0.5) { + * yield* Effect.fail(new HttpError()) + * } + * if (n2 < 0.5) { + * yield* Effect.fail(new ValidationError()) + * } + * return "some result" + * }) + * + * // ┌─── Effect + * // ▼ + * const recovered = program.pipe( + * Effect.catchIf( + * // Only handle HttpError errors + * (error) => error._tag === "HttpError", + * () => Effect.succeed("Recovering from HttpError") + * ) + * ) + * ``` + * + * @since 2.0.0 + * @category Error handling + */ +export const catchIf: { + ( + refinement: Refinement, EB>, + f: (e: EB) => Effect + ): (self: Effect) => Effect, R2 | R> + ( + predicate: Predicate>, + f: (e: NoInfer) => Effect + ): (self: Effect) => Effect + ( + self: Effect, + refinement: Refinement, + f: (e: EB) => Effect + ): Effect, R | R2> + ( + self: Effect, + predicate: Predicate, + f: (e: E) => Effect + ): Effect +} = core.catchIf + +/** + * Catches and recovers from specific types of errors, allowing you to attempt + * recovery only for certain errors. + * + * **Details** + * + * `catchSome` lets you selectively catch and handle errors of certain + * types by providing a recovery effect for specific errors. If the error + * matches a condition, recovery is attempted; if not, it doesn't affect the + * program. This function doesn't alter the error type, meaning the error type + * remains the same as in the original effect. + * + * **Example** (Handling Specific Errors with Effect.catchSome) + * + * ```ts + * import { Effect, Random, Option } from "effect" + * + * class HttpError { + * readonly _tag = "HttpError" + * } + * + * class ValidationError { + * readonly _tag = "ValidationError" + * } + * + * // ┌─── Effect + * // ▼ + * const program = Effect.gen(function* () { + * const n1 = yield* Random.next + * const n2 = yield* Random.next + * if (n1 < 0.5) { + * yield* Effect.fail(new HttpError()) + * } + * if (n2 < 0.5) { + * yield* Effect.fail(new ValidationError()) + * } + * return "some result" + * }) + * + * // ┌─── Effect + * // ▼ + * const recovered = program.pipe( + * Effect.catchSome((error) => { + * // Only handle HttpError errors + * if (error._tag === "HttpError") { + * return Option.some(Effect.succeed("Recovering from HttpError")) + * } else { + * return Option.none() + * } + * }) + * ) + * ``` + * + * @see {@link catchIf} for a version that allows you to recover from errors based on a predicate. + * + * @since 2.0.0 + * @category Error handling + */ +export const catchSome: { + ( + pf: (e: NoInfer) => Option.Option> + ): (self: Effect) => Effect + ( + self: Effect, + pf: (e: NoInfer) => Option.Option> + ): Effect +} = core.catchSome + +/** + * Recovers from specific causes using a provided partial function. + * + * @see {@link catchSome} for a version that allows you to recover from errors. + * @see {@link catchSomeDefect} for a version that allows you to recover from defects. + * + * @since 2.0.0 + * @category Error handling + */ +export const catchSomeCause: { + ( + f: (cause: Cause.Cause>) => Option.Option> + ): (self: Effect) => Effect + ( + self: Effect, + f: (cause: Cause.Cause>) => Option.Option> + ): Effect +} = effect.catchSomeCause + +/** + * Recovers from specific defects using a provided partial function. + * + * **Details** + * + * `catchSomeDefect` allows you to handle specific defects, which are + * unexpected errors that can cause the program to stop. It uses a partial + * function to catch only certain defects and ignores others. The function does + * not handle expected errors (such as those caused by {@link fail}) or + * interruptions in execution (like those caused by {@link interrupt}). + * + * This function provides a way to handle certain types of defects while + * allowing others to propagate and cause failure in the program. + * + * **Note**: There is no sensible way to recover from defects. This method + * should be used only at the boundary between Effect and an external system, to + * transmit information on a defect for diagnostic or explanatory purposes. + * + * **How the Partial Function Works** + * + * The function provided to `catchSomeDefect` acts as a filter and a handler for defects: + * - It receives the defect as an input. + * - If the defect matches a specific condition (e.g., a certain error type), the function returns + * an `Option.some` containing the recovery logic. + * - If the defect does not match, the function returns `Option.none`, allowing the defect to propagate. + * + * **Example** (Handling Specific Defects) + * + * ```ts + * import { Effect, Cause, Option, Console } from "effect" + * + * // Simulating a runtime error + * const task = Effect.dieMessage("Boom!") + * + * const program = Effect.catchSomeDefect(task, (defect) => { + * if (Cause.isIllegalArgumentException(defect)) { + * return Option.some( + * Console.log( + * `Caught an IllegalArgumentException defect: ${defect.message}` + * ) + * ) + * } + * return Option.none() + * }) + * + * // Since we are only catching IllegalArgumentException + * // we will get an Exit.Failure because we simulated a runtime error. + * Effect.runPromiseExit(program).then(console.log) + * // Output: + * // { + * // _id: 'Exit', + * // _tag: 'Failure', + * // cause: { + * // _id: 'Cause', + * // _tag: 'Die', + * // defect: { _tag: 'RuntimeException' } + * // } + * // } + * ``` + * + * @since 2.0.0 + * @category Error handling + */ +export const catchSomeDefect: { + ( + pf: (defect: unknown) => Option.Option> + ): (self: Effect) => Effect + ( + self: Effect, + pf: (defect: unknown) => Option.Option> + ): Effect +} = effect.catchSomeDefect + +/** + * Catches and handles specific errors by their `_tag` field, which is used as a + * discriminator. + * + * **When to Use** + * + * `catchTag` is useful when your errors are tagged with a readonly `_tag` field + * that identifies the error type. You can use this function to handle specific + * error types by matching the `_tag` value. This allows for precise error + * handling, ensuring that only specific errors are caught and handled. + * + * The error type must have a readonly `_tag` field to use `catchTag`. This + * field is used to identify and match errors. + * + * **Example** (Handling Errors by Tag) + * + * ```ts + * import { Effect, Random } from "effect" + * + * class HttpError { + * readonly _tag = "HttpError" + * } + * + * class ValidationError { + * readonly _tag = "ValidationError" + * } + * + * // ┌─── Effect + * // ▼ + * const program = Effect.gen(function* () { + * const n1 = yield* Random.next + * const n2 = yield* Random.next + * if (n1 < 0.5) { + * yield* Effect.fail(new HttpError()) + * } + * if (n2 < 0.5) { + * yield* Effect.fail(new ValidationError()) + * } + * return "some result" + * }) + * + * // ┌─── Effect + * // ▼ + * const recovered = program.pipe( + * // Only handle HttpError errors + * Effect.catchTag("HttpError", (_HttpError) => + * Effect.succeed("Recovering from HttpError") + * ) + * ) + * ``` + * + * @see {@link catchTags} for a version that allows you to handle multiple error + * types at once. + * + * @since 2.0.0 + * @category Error handling + */ +export const catchTag: { + , A1, E1, R1>( + ...args: [...tags: K, f: (e: Extract, { _tag: K[number] }>) => Effect] + ): (self: Effect) => Effect | E1, R | R1> + , A1, E1, R1>( + self: Effect, + ...args: [...tags: K, f: (e: Extract, { _tag: K[number] }>) => Effect] + ): Effect | E1, R | R1> +} = effect.catchTag + +/** + * Handles multiple errors in a single block of code using their `_tag` field. + * + * **When to Use** + * + * `catchTags` is a convenient way to handle multiple error types at + * once. Instead of using {@link catchTag} multiple times, you can pass an + * object where each key is an error type's `_tag`, and the value is the handler + * for that specific error. This allows you to catch and recover from multiple + * error types in a single call. + * + * The error type must have a readonly `_tag` field to use `catchTag`. This + * field is used to identify and match errors. + * + * **Example** (Handling Multiple Tagged Error Types at Once) + * + * ```ts + * import { Effect, Random } from "effect" + * + * class HttpError { + * readonly _tag = "HttpError" + * } + * + * class ValidationError { + * readonly _tag = "ValidationError" + * } + * + * // ┌─── Effect + * // ▼ + * const program = Effect.gen(function* () { + * const n1 = yield* Random.next + * const n2 = yield* Random.next + * if (n1 < 0.5) { + * yield* Effect.fail(new HttpError()) + * } + * if (n2 < 0.5) { + * yield* Effect.fail(new ValidationError()) + * } + * return "some result" + * }) + * + * // ┌─── Effect + * // ▼ + * const recovered = program.pipe( + * Effect.catchTags({ + * HttpError: (_HttpError) => + * Effect.succeed(`Recovering from HttpError`), + * ValidationError: (_ValidationError) => + * Effect.succeed(`Recovering from ValidationError`) + * }) + * ) + * ``` + * + * @since 2.0.0 + * @category Error handling + */ +export const catchTags: { + < + E, + Cases extends + & { [K in Extract["_tag"]]+?: ((error: Extract) => Effect) } + & (unknown extends E ? {} : { [K in Exclude["_tag"]>]: never }) + >( + cases: Cases + ): ( + self: Effect + ) => Effect< + | A + | { + [K in keyof Cases]: Cases[K] extends (...args: Array) => Effect ? A : never + }[keyof Cases], + | Exclude + | { + [K in keyof Cases]: Cases[K] extends (...args: Array) => Effect ? E : never + }[keyof Cases], + | R + | { + [K in keyof Cases]: Cases[K] extends (...args: Array) => Effect ? R : never + }[keyof Cases] + > + < + R, + E, + A, + Cases extends + & { [K in Extract["_tag"]]+?: ((error: Extract) => Effect) } + & (unknown extends E ? {} : { [K in Exclude["_tag"]>]: never }) + >( + self: Effect, + cases: Cases + ): Effect< + | A + | { + [K in keyof Cases]: Cases[K] extends (...args: Array) => Effect ? A : never + }[keyof Cases], + | Exclude + | { + [K in keyof Cases]: Cases[K] extends (...args: Array) => Effect ? E : never + }[keyof Cases], + | R + | { + [K in keyof Cases]: Cases[K] extends (...args: Array) => Effect ? R : never + }[keyof Cases] + > +} = effect.catchTags + +/** + * Retrieves the cause of a failure in an effect. + * + * **Details** + * + * This function allows you to expose the detailed cause of an effect, which + * includes a more precise representation of failures, such as error messages + * and defects. + * + * **When to Use** + * + * This function is helpful when you need to inspect the cause of a failure in + * an effect, giving you more information than just the error message. It can be + * used to log, handle, or analyze failures in more detail, including + * distinguishing between different types of defects (e.g., runtime exceptions, + * interruptions, etc.). + * + * **Example** + * + * ```ts + * import { Effect, Console } from "effect" + * + * // ┌─── Effect + * // ▼ + * const program = Effect.fail("Oh uh!").pipe(Effect.as(2)) + * + * // ┌─── Effect + * // ▼ + * const recovered = Effect.gen(function* () { + * const cause = yield* Effect.cause(program) + * yield* Console.log(cause) + * }) + * ``` + * + * @since 2.0.0 + * @category Error handling + */ +export const cause: (self: Effect) => Effect, never, R> = effect.cause + +/** + * Runs an effect repeatedly until it succeeds, ignoring errors. + * + * **Details** + * + * This function takes an effect and runs it repeatedly until the effect + * successfully completes. If the effect fails, it will ignore the error and + * retry the operation. This is useful when you need to perform a task that may + * fail occasionally, but you want to keep trying until it eventually succeeds. + * It works by repeatedly executing the effect until it no longer throws an + * error. + * + * **When to Use** + * + * Use this function when you want to retry an operation multiple times until it + * succeeds. It is helpful in cases where the operation may fail temporarily + * (e.g., a network request), and you want to keep trying without handling or + * worrying about the errors. + * + * **Example** + * + * ```ts + * import { Effect } from "effect" + * + * let counter = 0 + * + * const effect = Effect.try(() => { + * counter++ + * if (counter < 3) { + * console.log("running effect") + * throw new Error("error") + * } else { + * console.log("effect done") + * return "some result" + * } + * }) + * + * const program = Effect.eventually(effect) + * + * Effect.runPromise(program).then(console.log) + * // Output: + * // running effect + * // running effect + * // effect done + * // some result + * ``` + * + * @since 2.0.0 + * @category Error handling + */ +export const eventually: (self: Effect) => Effect = effect.eventually + +/** + * Discards both the success and failure values of an effect. + * + * **When to Use** + * + * `ignore` allows you to run an effect without caring about its result, whether + * it succeeds or fails. This is useful when you only care about the side + * effects of the effect and do not need to handle or process its outcome. + * + * **Example** (Using Effect.ignore to Discard Values) + * + * ```ts + * import { Effect } from "effect" + * + * // ┌─── Effect + * // ▼ + * const task = Effect.fail("Uh oh!").pipe(Effect.as(5)) + * + * // ┌─── Effect + * // ▼ + * const program = Effect.ignore(task) + * ``` + * + * @see {@link ignoreLogged} to log failures while ignoring them. + * + * @since 2.0.0 + * @category Error handling + */ +export const ignore: (self: Effect) => Effect = effect.ignore + +/** + * Ignores the result of an effect but logs any failures. + * + * **Details** + * + * This function takes an effect and returns a new effect that ignores whether + * the original effect succeeds or fails. However, if the effect fails, it will + * log the failure at the Debug level, so you can keep track of any issues that + * arise. + * + * **When to Use** + * + * This is useful in scenarios where you want to continue with your program + * regardless of the result of the effect, but you still want to be aware of + * potential failures that may need attention later. + * + * @since 2.0.0 + * @category Error handling + */ +export const ignoreLogged: (self: Effect) => Effect = effect.ignoreLogged + +/** + * Combines all errors from concurrent operations into a single error. + * + * **Details** + * + * This function is used when you have multiple operations running at the same + * time, and you want to capture all the errors that occur across those + * operations. Instead of handling each error separately, it combines all the + * errors into one unified error. + * + * **When to Use** + * + * When using this function, any errors that occur in the concurrently running + * operations will be grouped together into a single error. This helps simplify + * error handling in cases where you don't need to differentiate between each + * failure, but simply want to know that multiple failures occurred. + * + * **Example** + * + * ```ts + * import { Effect } from "effect" + * + * const fail1 = Effect.fail("Oh uh!") + * const fail2 = Effect.fail("Oh no!") + * const die = Effect.dieMessage("Boom!") + * + * // Run all effects concurrently and capture all errors + * const program = Effect.all([fail1, fail2, die], { + * concurrency: "unbounded" + * }).pipe(Effect.asVoid, Effect.parallelErrors) + * + * Effect.runPromiseExit(program).then(console.log) + * // Output: + * // { + * // _id: 'Exit', + * // _tag: 'Failure', + * // cause: { _id: 'Cause', _tag: 'Fail', failure: [ 'Oh uh!', 'Oh no!' ] } + * // } + * ``` + * + * @since 2.0.0 + * @category Error handling + */ +export const parallelErrors: (self: Effect) => Effect, R> = effect.parallelErrors + +/** + * Transforms an effect to expose detailed error causes. + * + * **Details** + * + * This function enhances an effect by providing detailed information about any + * error, defect, or interruption that may occur during its execution. It + * modifies the error channel of the effect so that it includes a full cause of + * the failure, wrapped in a `Cause` type. + * + * After applying this function, you can use operators like {@link catchAll} and + * {@link catchTags} to handle specific types of errors. + * + * If you no longer need the detailed cause information, you can revert the + * changes using {@link unsandbox} to return to the original error-handling + * behavior. + * + * **Example** + * + * ```ts + * import { Effect, Console } from "effect" + * + * // ┌─── Effect + * // ▼ + * const task = Effect.fail(new Error("Oh uh!")).pipe( + * Effect.as("primary result") + * ) + * + * // ┌─── Effect, never> + * // ▼ + * const sandboxed = Effect.sandbox(task) + * + * const program = Effect.catchTags(sandboxed, { + * Die: (cause) => + * Console.log(`Caught a defect: ${cause.defect}`).pipe( + * Effect.as("fallback result on defect") + * ), + * Interrupt: (cause) => + * Console.log(`Caught a defect: ${cause.fiberId}`).pipe( + * Effect.as("fallback result on fiber interruption") + * ), + * Fail: (cause) => + * Console.log(`Caught a defect: ${cause.error}`).pipe( + * Effect.as("fallback result on failure") + * ) + * }) + * + * // Restore the original error handling with unsandbox + * const main = Effect.unsandbox(program) + * + * Effect.runPromise(main).then(console.log) + * // Output: + * // Caught a defect: Oh uh! + * // fallback result on failure + * ``` + * + * @see {@link unsandbox} to restore the original error handling. + * + * @since 2.0.0 + * @category Error handling + */ +export const sandbox: (self: Effect) => Effect, R> = effect.sandbox + +/** + * @since 2.0.0 + * @category Error handling + */ +export declare namespace Retry { + /** + * @since 2.0.0 + * @category Error handling + */ + export type Return, O>> = Effect< + A, + | (O extends { schedule: Schedule.Schedule } ? E + : O extends { until: Refinement } ? E2 + : E) + | (O extends { while: (...args: Array) => Effect } ? E : never) + | (O extends { until: (...args: Array) => Effect } ? E : never), + | R + | (O extends { schedule: Schedule.Schedule } ? R : never) + | (O extends { while: (...args: Array) => Effect } ? R : never) + | (O extends { until: (...args: Array) => Effect } ? R : never) + > extends infer Z ? Z : never + + /** + * @since 2.0.0 + * @category Error handling + */ + export interface Options { + while?: ((error: E) => boolean | Effect) | undefined + until?: ((error: E) => boolean | Effect) | undefined + times?: number | undefined + schedule?: Schedule.Schedule | undefined + } +} + +/** + * Retries a failing effect based on a defined retry policy. + * + * **Details** + * + * The `Effect.retry` function takes an effect and a {@link Schedule} policy, + * and will automatically retry the effect if it fails, following the rules of + * the policy. + * + * If the effect ultimately succeeds, the result will be returned. + * + * If the maximum retries are exhausted and the effect still fails, the failure + * is propagated. + * + * **When to Use** + * + * This can be useful when dealing with intermittent failures, such as network + * issues or temporary resource unavailability. By defining a retry policy, you + * can control the number of retries, the delay between them, and when to stop + * retrying. + * + * **Example** (Retrying with a Fixed Delay) + * + * ```ts + * import { Effect, Schedule } from "effect" + * + * let count = 0 + * + * // Simulates an effect with possible failures + * const task = Effect.async((resume) => { + * if (count <= 2) { + * count++ + * console.log("failure") + * resume(Effect.fail(new Error())) + * } else { + * console.log("success") + * resume(Effect.succeed("yay!")) + * } + * }) + * + * // Define a repetition policy using a fixed delay between retries + * const policy = Schedule.fixed("100 millis") + * + * const repeated = Effect.retry(task, policy) + * + * Effect.runPromise(repeated).then(console.log) + * // Output: + * // failure + * // failure + * // failure + * // success + * // yay! + * ``` + * + * **Example** (Retrying a Task up to 5 times) + * + * ```ts + * import { Effect } from "effect" + * + * let count = 0 + * + * // Simulates an effect with possible failures + * const task = Effect.async((resume) => { + * if (count <= 2) { + * count++ + * console.log("failure") + * resume(Effect.fail(new Error())) + * } else { + * console.log("success") + * resume(Effect.succeed("yay!")) + * } + * }) + * + * // Retry the task up to 5 times + * Effect.runPromise(Effect.retry(task, { times: 5 })).then(console.log) + * // Output: + * // failure + * // failure + * // failure + * // success + * ``` + * + * **Example** (Retrying Until a Specific Condition is Met) + * + * ```ts + * import { Effect } from "effect" + * + * let count = 0 + * + * // Define an effect that simulates varying error on each invocation + * const action = Effect.failSync(() => { + * console.log(`Action called ${++count} time(s)`) + * return `Error ${count}` + * }) + * + * // Retry the action until a specific condition is met + * const program = Effect.retry(action, { + * until: (err) => err === "Error 3" + * }) + * + * Effect.runPromiseExit(program).then(console.log) + * // Output: + * // Action called 1 time(s) + * // Action called 2 time(s) + * // Action called 3 time(s) + * // { + * // _id: 'Exit', + * // _tag: 'Failure', + * // cause: { _id: 'Cause', _tag: 'Fail', failure: 'Error 3' } + * // } + * ``` + * + * @see {@link retryOrElse} for a version that allows you to run a fallback. + * @see {@link repeat} if your retry condition is based on successful outcomes rather than errors. + * + * @since 2.0.0 + * @category Error handling + */ +export const retry: { + , O>>( + options: O + ): (self: Effect) => Retry.Return + (policy: Schedule.Schedule, R1>): (self: Effect) => Effect + , O>>( + self: Effect, + options: O + ): Retry.Return + (self: Effect, policy: Schedule.Schedule, R1>): Effect +} = schedule_.retry_combined + +/** + * Apply an `ExecutionPlan` to the effect, which allows you to fallback to + * different resources in case of failure. + * + * @since 3.16.0 + * @category Error handling + * @experimental + */ +export const withExecutionPlan: { + ( + plan: ExecutionPlan<{ provides: Provides; input: Input; error: PlanE; requirements: PlanR }> + ): (effect: Effect) => Effect | PlanR> + ( + effect: Effect, + plan: ExecutionPlan<{ provides: Provides; input: Input; error: PlanE; requirements: PlanR }> + ): Effect | PlanR> +} = internalExecutionPlan.withExecutionPlan + +/** + * Retries a failing effect and runs a fallback effect if retries are exhausted. + * + * **Details** + * + * The `Effect.retryOrElse` function attempts to retry a failing effect multiple + * times according to a defined {@link Schedule} policy. + * + * If the retries are exhausted and the effect still fails, it runs a fallback + * effect instead. + * + * **When to Use** + * + * This function is useful when you want to handle failures gracefully by + * specifying an alternative action after repeated failures. + * + * **Example** (Retrying with Fallback) + * + * ```ts + * import { Effect, Schedule, Console } from "effect" + * + * let count = 0 + * + * // Simulates an effect with possible failures + * const task = Effect.async((resume) => { + * if (count <= 2) { + * count++ + * console.log("failure") + * resume(Effect.fail(new Error())) + * } else { + * console.log("success") + * resume(Effect.succeed("yay!")) + * } + * }) + * + * // Retry the task with a delay between retries and a maximum of 2 retries + * const policy = Schedule.addDelay(Schedule.recurs(2), () => "100 millis") + * + * // If all retries fail, run the fallback effect + * const repeated = Effect.retryOrElse( + * task, + * policy, + * // fallback + * () => Console.log("orElse").pipe(Effect.as("default value")) + * ) + * + * Effect.runPromise(repeated).then(console.log) + * // Output: + * // failure + * // failure + * // failure + * // orElse + * // default value + * ``` + * + * @see {@link retry} for a version that does not run a fallback effect. + * + * @since 2.0.0 + * @category Error handling + */ +export const retryOrElse: { + ( + policy: Schedule.Schedule, R1>, + orElse: (e: NoInfer, out: A1) => Effect + ): (self: Effect) => Effect + ( + self: Effect, + policy: Schedule.Schedule, R1>, + orElse: (e: NoInfer, out: A1) => Effect + ): Effect +} = schedule_.retryOrElse_Effect + +const try_: { + (options: { readonly try: LazyArg; readonly catch: (error: unknown) => E }): Effect + (thunk: LazyArg): Effect +} = effect.try_ + +export { + /** + * Creates an `Effect` that represents a synchronous computation that might + * fail. + * + * **When to Use** + * + * In situations where you need to perform synchronous operations that might + * fail, such as parsing JSON, you can use the `try` constructor. This + * constructor is designed to handle operations that could throw exceptions by + * capturing those exceptions and transforming them into manageable errors. + * + * **Error Handling** + * + * There are two ways to handle errors with `try`: + * + * 1. If you don't provide a `catch` function, the error is caught and the + * effect fails with an `UnknownException`. + * 2. If you provide a `catch` function, the error is caught and the `catch` + * function maps it to an error of type `E`. + * + * **Example** (Safe JSON Parsing) + * + * ```ts + * import { Effect } from "effect" + * + * const parse = (input: string) => + * // This might throw an error if input is not valid JSON + * Effect.try(() => JSON.parse(input)) + * + * // ┌─── Effect + * // ▼ + * const program = parse("") + * + * ``` + * + * **Example** (Custom Error Handling) + * + * ```ts + * import { Effect } from "effect" + * + * const parse = (input: string) => + * Effect.try({ + * // JSON.parse may throw for bad input + * try: () => JSON.parse(input), + * // remap the error + * catch: (unknown) => new Error(`something went wrong ${unknown}`) + * }) + * + * // ┌─── Effect + * // ▼ + * const program = parse("") + * ``` + * + * @see {@link sync} if the effectful computation is synchronous and does not + * throw errors. + * + * @since 2.0.0 + * @category Creating Effects + */ + try_ as try +} + +/** + * Returns an effect that maps its success using the specified side-effecting + * `try` function, converting any errors into typed failed effects using the + * `catch` function. + * + * @see {@link tryPromise} for a version that works with asynchronous computations. + * + * @since 2.0.0 + * @category Error handling + */ +export const tryMap: { + ( + options: { readonly try: (a: A) => B; readonly catch: (error: unknown) => E1 } + ): (self: Effect) => Effect + (self: Effect, options: { + readonly try: (a: A) => B + readonly catch: (error: unknown) => E1 + }): Effect +} = effect.tryMap + +/** + * Returns an effect that maps its success using the specified side-effecting + * `try` function, converting any promise rejections into typed failed effects + * using the `catch` function. + * + * An optional `AbortSignal` can be provided to allow for interruption of the + * wrapped `Promise` API. + * + * @see {@link tryMap} for a version that works with synchronous computations. + * + * @since 2.0.0 + * @category Error handling + */ +export const tryMapPromise: { + ( + options: { readonly try: (a: A, signal: AbortSignal) => PromiseLike; readonly catch: (error: unknown) => E1 } + ): (self: Effect) => Effect + ( + self: Effect, + options: { readonly try: (a: A, signal: AbortSignal) => PromiseLike; readonly catch: (error: unknown) => E1 } + ): Effect +} = effect.tryMapPromise + +/** + * Creates an `Effect` that represents an asynchronous computation that might + * fail. + * + * **When to Use** + * + * In situations where you need to perform asynchronous operations that might + * fail, such as fetching data from an API, you can use the `tryPromise` + * constructor. This constructor is designed to handle operations that could + * throw exceptions by capturing those exceptions and transforming them into + * manageable errors. + * + * **Error Handling** + * + * There are two ways to handle errors with `tryPromise`: + * + * 1. If you don't provide a `catch` function, the error is caught and the + * effect fails with an `UnknownException`. + * 2. If you provide a `catch` function, the error is caught and the `catch` + * function maps it to an error of type `E`. + * + * **Interruptions** + * + * An optional `AbortSignal` can be provided to allow for interruption of the + * wrapped `Promise` API. + * + * **Example** (Fetching a TODO Item) + * + * ```ts + * import { Effect } from "effect" + * + * const getTodo = (id: number) => + * // Will catch any errors and propagate them as UnknownException + * Effect.tryPromise(() => + * fetch(`https://jsonplaceholder.typicode.com/todos/${id}`) + * ) + * + * // ┌─── Effect + * // ▼ + * const program = getTodo(1) + * ``` + * + * **Example** (Custom Error Handling) + * + * ```ts + * import { Effect } from "effect" + * + * const getTodo = (id: number) => + * Effect.tryPromise({ + * try: () => fetch(`https://jsonplaceholder.typicode.com/todos/${id}`), + * // remap the error + * catch: (unknown) => new Error(`something went wrong ${unknown}`) + * }) + * + * // ┌─── Effect + * // ▼ + * const program = getTodo(1) + * ``` + * + * @see {@link promise} if the effectful computation is asynchronous and does not throw errors. + * + * @since 2.0.0 + * @category Creating Effects + */ +export const tryPromise: { + ( + options: { + readonly try: (signal: AbortSignal) => PromiseLike + readonly catch: (error: unknown) => E + } + ): Effect + (evaluate: (signal: AbortSignal) => PromiseLike): Effect +} = effect.tryPromise + +/** + * The `unsandbox` function is used to revert an effect that has been + * sandboxed by {@link sandbox}. When you apply `unsandbox`, the + * effect's error channel is restored to its original state, without the + * detailed `Cause` information. This means that any underlying causes of + * errors, defects, or fiber interruptions are no longer exposed in the error + * channel. + * + * This function is useful when you want to remove the detailed error tracking + * provided by `sandbox` and return to the standard error handling for + * your effect. Once unsandboxed, the effect behaves as if `sandbox` was + * never applied. + * + * @see {@link sandbox} to expose the full cause of failures, defects, or interruptions. + * + * @since 2.0.0 + * @category Error handling + */ +export const unsandbox: (self: Effect, R>) => Effect = effect.unsandbox + +/** + * Allows interruption of the current fiber, even in uninterruptible regions. + * + * **Details** + * + * This effect checks whether any other fibers are attempting to interrupt the + * current fiber. If so, it allows the current fiber to perform a + * self-interruption. + * + * **When to Use** + * + * This is useful in situations where you want to allow interruption to happen + * even in regions of the code that are normally uninterruptible. + * + * @since 2.0.0 + * @category Interruption + */ +export const allowInterrupt: Effect = effect.allowInterrupt + +/** + * Checks if interruption is allowed and executes a callback accordingly. + * + * **Details** + * + * This function checks the current interrupt status of the running fiber. It + * then calls the provided callback, passing a boolean indicating whether + * interruption is allowed. + * + * **When to Use** + * + * This is useful for handling specific logic based on whether the current + * operation can be interrupted, such as when performing asynchronous operations + * or handling cancellation. + * + * **Example** + * + * ```ts + * import { Console, Effect } from "effect" + * + * const program = Effect.gen(function*() { + * yield* Effect.checkInterruptible((isInterruptible) => { + * if (isInterruptible) { + * return Console.log("You can interrupt this operation.") + * } else { + * return Console.log("This operation cannot be interrupted.") + * } + * }) + * }) + * + * Effect.runPromise(program) + * // Output: You can interrupt this operation. + * + * Effect.runPromise(program.pipe(Effect.uninterruptible)) + * // Output: This operation cannot be interrupted. + * + * ``` + * + * @since 2.0.0 + * @category Interruption + */ +export const checkInterruptible: (f: (isInterruptible: boolean) => Effect) => Effect = + core.checkInterruptible + +/** + * Provides a way to handle timeouts in uninterruptible effects, allowing them + * to continue in the background while the main control flow proceeds with the + * timeout error. + * + * **Details** + * + * The `disconnect` function allows an uninterruptible effect to continue + * running in the background, while enabling the main control flow to + * immediately recognize a timeout condition. This is useful when you want to + * avoid blocking the program due to long-running tasks, especially when those + * tasks do not need to affect the flow of the rest of the program. + * + * Without `disconnect`, an uninterruptible effect will ignore the + * timeout and continue executing until it completes. The timeout error will + * only be assessed after the effect finishes, which can cause delays in + * recognizing a timeout. + * + * With `disconnect`, the uninterruptible effect proceeds in the + * background while the main program flow can immediately handle the timeout + * error or trigger alternative logic. This enables faster timeout handling + * without waiting for the completion of the long-running task. + * + * **Example** + * + * ```ts + * import { Effect } from "effect" + * + * const longRunningTask = Effect.gen(function* () { + * console.log("Start heavy processing...") + * yield* Effect.sleep("5 seconds") // Simulate a long process + * console.log("Heavy processing done.") + * return "Data processed" + * }) + * + * const timedEffect = longRunningTask.pipe( + * Effect.uninterruptible, + * // Allows the task to finish in the background if it times out + * Effect.disconnect, + * Effect.timeout("1 second") + * ) + * + * Effect.runPromiseExit(timedEffect).then(console.log) + * // Output: + * // Start heavy processing... + * // { + * // _id: 'Exit', + * // _tag: 'Failure', + * // cause: { + * // _id: 'Cause', + * // _tag: 'Fail', + * // failure: { _tag: 'TimeoutException' } + * // } + * // } + * // Heavy processing done. + * ``` + * + * @see {@link timeout} for a version that interrupts the effect. + * @see {@link uninterruptible} for creating an uninterruptible effect. + * + * @since 2.0.0 + * @category Interruption + */ +export const disconnect: (self: Effect) => Effect = fiberRuntime.disconnect + +/** + * Represents an effect that interrupts the current fiber. + * + * **Details** + * + * This effect models the explicit interruption of the fiber in which it runs. + * When executed, it causes the fiber to stop its operation immediately, + * capturing the interruption details such as the fiber's ID and its start time. + * The resulting interruption can be observed in the `Exit` type if the effect + * is run with functions like {@link runPromiseExit}. + * + * **Example** + * + * ```ts + * import { Effect } from "effect" + * + * const program = Effect.gen(function* () { + * console.log("start") + * yield* Effect.sleep("2 seconds") + * yield* Effect.interrupt + * console.log("done") + * return "some result" + * }) + * + * Effect.runPromiseExit(program).then(console.log) + * // Output: + * // start + * // { + * // _id: 'Exit', + * // _tag: 'Failure', + * // cause: { + * // _id: 'Cause', + * // _tag: 'Interrupt', + * // fiberId: { + * // _id: 'FiberId', + * // _tag: 'Runtime', + * // id: 0, + * // startTimeMillis: ... + * // } + * // } + * // } + * ``` + * + * @since 2.0.0 + * @category Interruption + */ +export const interrupt: Effect = core.interrupt + +/** + * @since 2.0.0 + * @category Interruption + */ +export const interruptWith: (fiberId: FiberId.FiberId) => Effect = core.interruptWith + +/** + * Marks an effect as interruptible. + * + * @since 2.0.0 + * @category Interruption + */ +export const interruptible: (self: Effect) => Effect = core.interruptible + +/** + * This function behaves like {@link interruptible}, but it also provides a + * `restore` function. This function can be used to restore the interruptibility + * of any specific region of code. + * + * @since 2.0.0 + * @category Interruption + */ +export const interruptibleMask: ( + f: (restore: (effect: Effect) => Effect) => Effect +) => Effect = core.interruptibleMask + +/** + * Registers a cleanup effect to run when an effect is interrupted. + * + * **Details** + * + * This function allows you to specify an effect to run when the fiber is + * interrupted. This effect will be executed when the fiber is interrupted, + * allowing you to perform cleanup or other actions. + * + * **Example** (Running a Cleanup Action on Interruption) + * + * ```ts + * import { Console, Effect } from "effect" + * + * // This handler is executed when the fiber is interrupted + * const handler = Effect.onInterrupt((_fibers) => Console.log("Cleanup completed")) + * + * const success = Console.log("Task completed").pipe(Effect.as("some result"), handler) + * + * Effect.runFork(success) + * // Output: + * // Task completed + * + * const failure = Console.log("Task failed").pipe(Effect.andThen(Effect.fail("some error")), handler) + * + * Effect.runFork(failure) + * // Output: + * // Task failed + * + * const interruption = Console.log("Task interrupted").pipe(Effect.andThen(Effect.interrupt), handler) + * + * Effect.runFork(interruption) + * // Output: + * // Task interrupted + * // Cleanup completed + * ``` + * + * @since 2.0.0 + * @category Interruption + */ +export const onInterrupt: { + ( + cleanup: (interruptors: HashSet.HashSet) => Effect + ): (self: Effect) => Effect + ( + self: Effect, + cleanup: (interruptors: HashSet.HashSet) => Effect + ): Effect +} = core.onInterrupt + +/** + * Marks an effect as uninterruptible. + * + * @since 2.0.0 + * @category Interruption + */ +export const uninterruptible: (self: Effect) => Effect = core.uninterruptible + +/** + * This function behaves like {@link uninterruptible}, but it also provides a + * `restore` function. This function can be used to restore the interruptibility + * of any specific region of code. + * + * @since 2.0.0 + * @category Interruption + */ +export const uninterruptibleMask: ( + f: (restore: (effect: Effect) => Effect) => Effect +) => Effect = core.uninterruptibleMask + +/** + * Transforms a `Predicate` function into an `Effect` returning the input value if the predicate returns `true` + * or failing with specified error if the predicate fails + * + * **Example** + * + * ```ts + * import { Effect } from "effect" + * + * const isPositive = (n: number): boolean => n > 0 + * + * // succeeds with `1` + * Effect.liftPredicate(1, isPositive, n => `${n} is not positive`) + * + * // fails with `"0 is not positive"` + * Effect.liftPredicate(0, isPositive, n => `${n} is not positive`) + * ``` + * + * @category Condition Checking + * @since 3.4.0 + */ +export const liftPredicate: { + ( + predicate: Refinement | Predicate, + orFailWith: (a: EqualsWith>) => E + ): (a: A) => Effect, E> + ( + self: A, + predicate: Refinement | Predicate, + orFailWith: (a: EqualsWith>) => E + ): Effect +} = effect.liftPredicate + +/** + * Replaces the value inside an effect with a constant value. + * + * **Details** + * + * This function allows you to ignore the original value inside an effect and + * replace it with a constant value. + * + * **When to Use** + * + * It is useful when you no longer need the value produced by an effect but want + * to ensure that the effect completes successfully with a specific constant + * result instead. For instance, you can replace the value produced by a + * computation with a predefined value, ignoring what was calculated before. + * + * **Example** (Replacing a Value) + * + * ```ts + * import { pipe, Effect } from "effect" + * + * // Replaces the value 5 with the constant "new value" + * const program = pipe(Effect.succeed(5), Effect.as("new value")) + * + * Effect.runPromise(program).then(console.log) + * // Output: "new value" + * ``` + * + * @since 2.0.0 + * @category Mapping + */ +export const as: { + (value: B): (self: Effect) => Effect + (self: Effect, value: B): Effect +} = core.as + +/** + * This function maps the success value of an `Effect` value to a `Some` value + * in an `Option` value. If the original `Effect` value fails, the returned + * `Effect` value will also fail. + * + * @category Mapping + * @since 2.0.0 + */ +export const asSome: (self: Effect) => Effect, E, R> = effect.asSome + +/** + * This function maps the error value of an `Effect` value to a `Some` value + * in an `Option` value. If the original `Effect` value succeeds, the returned + * `Effect` value will also succeed. + * + * @category Mapping + * @since 2.0.0 + */ +export const asSomeError: (self: Effect) => Effect, R> = effect.asSomeError + +/** + * This function maps the success value of an `Effect` value to `void`. If the + * original `Effect` value succeeds, the returned `Effect` value will also + * succeed. If the original `Effect` value fails, the returned `Effect` value + * will fail with the same error. + * + * @since 2.0.0 + * @category Mapping + */ +export const asVoid: (self: Effect) => Effect = core.asVoid + +/** + * Swaps the success and error channels of an effect. + * + * **Details** + * + * This function reverses the flow of an effect by swapping its success and + * error channels. The success value becomes an error, and the error value + * becomes a success. + * + * **Example** + * + * ```ts + * import { Effect } from "effect" + * + * // ┌─── Effect + * // ▼ + * const program = Effect.fail("Oh uh!").pipe(Effect.as(2)) + * + * // ┌─── Effect + * // ▼ + * const flipped = Effect.flip(program) + * ``` + * + * @since 2.0.0 + * @category Mapping + */ +export const flip: (self: Effect) => Effect = core.flip + +/** + * Swaps the error/value parameters, applies the function `f` and flips the + * parameters back + * + * @since 2.0.0 + * @category Mapping + */ +export const flipWith: { + ( + f: (effect: Effect) => Effect + ): (self: Effect) => Effect + ( + self: Effect, + f: (effect: Effect) => Effect + ): Effect +} = effect.flipWith + +/** + * Transforms the value inside an effect by applying a function to it. + * + * **Syntax** + * + * ```ts skip-type-checking + * const mappedEffect = pipe(myEffect, Effect.map(transformation)) + * // or + * const mappedEffect = Effect.map(myEffect, transformation) + * // or + * const mappedEffect = myEffect.pipe(Effect.map(transformation)) + * ``` + * + * **Details** + * + * `map` takes a function and applies it to the value contained within an + * effect, creating a new effect with the transformed value. + * + * It's important to note that effects are immutable, meaning that the original + * effect is not modified. Instead, a new effect is returned with the updated + * value. + * + * **Example** (Adding a Service Charge) + * + * ```ts + * import { pipe, Effect } from "effect" + * + * const addServiceCharge = (amount: number) => amount + 1 + * + * const fetchTransactionAmount = Effect.promise(() => Promise.resolve(100)) + * + * const finalAmount = pipe( + * fetchTransactionAmount, + * Effect.map(addServiceCharge) + * ) + * + * Effect.runPromise(finalAmount).then(console.log) + * // Output: 101 + * ``` + * + * @see {@link mapError} for a version that operates on the error channel. + * @see {@link mapBoth} for a version that operates on both channels. + * @see {@link flatMap} or {@link andThen} for a version that can return a new effect. + * + * @since 2.0.0 + * @category Mapping + */ +export const map: { + (f: (a: A) => B): (self: Effect) => Effect + (self: Effect, f: (a: A) => B): Effect +} = core.map + +/** + * Applies a stateful transformation to each element of a collection, producing + * new elements along with an updated state. + * + * **When to Use** + * + * Use `mapAccum` when you need to process each element of a collection while + * keeping track of some state across iterations. + * + * **Details** + * + * `mapAccum` takes an initial state (`initial`) and a function (`f`) that is + * applied to each element. This function returns a new state and a transformed + * element. The final effect produces both the accumulated state and the + * transformed collection. + * + * If the input collection is a non-empty array, the return type will match the + * input collection type. + * + * **Example** + * + * ```ts + * import { Effect } from "effect" + * + * // Define an initial state and a transformation function + * const initialState = 0 + * + * const transformation = (state: number, element: string) => + * Effect.succeed<[number, string]>([state + element.length, element.toUpperCase()]) + * + * // Apply mapAccum to transform an array of strings + * const program = Effect.mapAccum(["a", "bb", "ccc"], initialState, transformation) + * + * Effect.runPromise(program).then(([finalState, transformedCollection]) => { + * console.log(finalState) + * console.log(transformedCollection) + * }) + * // Output: + * // 6 + * // [ 'A', 'BB', 'CCC' ] + * ``` + * + * @since 2.0.0 + * @category Mapping + */ +export const mapAccum: { + = Iterable>( + initial: S, + f: (state: S, a: RA.ReadonlyArray.Infer, i: number) => Effect + ): (elements: I) => Effect<[S, RA.ReadonlyArray.With], E, R> + = Iterable>( + elements: I, + initial: S, + f: (state: S, a: RA.ReadonlyArray.Infer, i: number) => Effect + ): Effect<[S, RA.ReadonlyArray.With], E, R> +} = effect.mapAccum + +/** + * Applies transformations to both the success and error channels of an effect. + * + * **Details** + * + * This function takes two map functions as arguments: one for the error channel + * and one for the success channel. You can use it when you want to modify both + * the error and the success values without altering the overall success or + * failure status of the effect. + * + * **Example** + * + * ```ts + * import { Effect } from "effect" + * + * // ┌─── Effect + * // ▼ + * const simulatedTask = Effect.fail("Oh no!").pipe(Effect.as(1)) + * + * // ┌─── Effect + * // ▼ + * const modified = Effect.mapBoth(simulatedTask, { + * onFailure: (message) => new Error(message), + * onSuccess: (n) => n > 0 + * }) + * ``` + * + * @see {@link map} for a version that operates on the success channel. + * @see {@link mapError} for a version that operates on the error channel. + * + * @since 2.0.0 + * @category Mapping + */ +export const mapBoth: { + ( + options: { readonly onFailure: (e: E) => E2; readonly onSuccess: (a: A) => A2 } + ): (self: Effect) => Effect + ( + self: Effect, + options: { readonly onFailure: (e: E) => E2; readonly onSuccess: (a: A) => A2 } + ): Effect +} = core.mapBoth + +/** + * Transforms or modifies the error produced by an effect without affecting its + * success value. + * + * **When to Use** + * + * This function is helpful when you want to enhance the error with additional + * information, change the error type, or apply custom error handling while + * keeping the original behavior of the effect's success values intact. It only + * operates on the error channel and leaves the success channel unchanged. + * + * **Example** + * + * ```ts + * import { Effect } from "effect" + * + * // ┌─── Effect + * // ▼ + * const simulatedTask = Effect.fail("Oh no!").pipe(Effect.as(1)) + * + * // ┌─── Effect + * // ▼ + * const mapped = Effect.mapError( + * simulatedTask, + * (message) => new Error(message) + * ) + * ``` + * + * @see {@link map} for a version that operates on the success channel. + * @see {@link mapBoth} for a version that operates on both channels. + * @see {@link orElseFail} if you want to replace the error with a new one. + * + * @since 2.0.0 + * @category Mapping + */ +export const mapError: { + (f: (e: E) => E2): (self: Effect) => Effect + (self: Effect, f: (e: E) => E2): Effect +} = core.mapError + +/** + * Maps the cause of failure of an effect using a specified function. + * + * @see {@link sandbox} for a version that exposes the full cause of failures, defects, or interruptions. + * @see {@link catchAllCause} for a version that can recover from all types of defects. + * + * @since 2.0.0 + * @category Mapping + */ +export const mapErrorCause: { + (f: (cause: Cause.Cause) => Cause.Cause): (self: Effect) => Effect + (self: Effect, f: (cause: Cause.Cause) => Cause.Cause): Effect +} = effect.mapErrorCause + +/** + * Combines both success and error channels of an effect into a single outcome. + * + * **Details** + * + * This function transforms an effect that may fail into one that always returns + * a value, where both success and failure outcomes are handled as values in the + * success channel. + * + * **When to Use** + * + * This can be useful when you want to continue execution regardless of the + * error type and still capture both successful results and errors as part of + * the outcome. + * + * **Example** + * + * ```ts + * import { Effect } from "effect" + * + * // ┌─── Effect + * // ▼ + * const program = Effect.fail("Oh uh!").pipe(Effect.as(2)) + * + * // ┌─── Effect + * // ▼ + * const recovered = Effect.merge(program) + * ``` + * + * @since 2.0.0 + * @category Mapping + */ +export const merge: (self: Effect) => Effect = effect.merge + +/** + * Returns a new effect with the boolean value of this effect negated. + * + * @since 2.0.0 + * @category Mapping + */ +export const negate: (self: Effect) => Effect = effect.negate + +/** + * Creates a scoped resource using an `acquire` and `release` effect. + * + * **Details** + * + * This function helps manage resources by combining two `Effect` values: one + * for acquiring the resource and one for releasing it. + * + * `acquireRelease` does the following: + * + * 1. Ensures that the effect that acquires the resource will not be + * interrupted. Note that acquisition may still fail due to internal + * reasons (such as an uncaught exception). + * 2. Ensures that the `release` effect will not be interrupted, and will be + * executed as long as the acquisition effect successfully acquires the + * resource. + * + * If the `acquire` function succeeds, the `release` function is added to the + * list of finalizers for the scope. This ensures that the release will happen + * automatically when the scope is closed. + * + * Both `acquire` and `release` run uninterruptibly, meaning they cannot be + * interrupted while they are executing. + * + * Additionally, the `release` function can be influenced by the exit value when + * the scope closes, allowing for custom handling of how the resource is + * released based on the execution outcome. + * + * **When to Use** + * + * This function is used to ensure that an effect that represents the + * acquisition of a resource (for example, opening a file, launching a thread, + * etc.) will not be interrupted, and that the resource will always be released + * when the `Effect` completes execution. + * + * **Example** (Defining a Simple Resource) + * + * ```ts + * import { Effect } from "effect" + * + * // Define an interface for a resource + * interface MyResource { + * readonly contents: string + * readonly close: () => Promise + * } + * + * // Simulate resource acquisition + * const getMyResource = (): Promise => + * Promise.resolve({ + * contents: "lorem ipsum", + * close: () => + * new Promise((resolve) => { + * console.log("Resource released") + * resolve() + * }) + * }) + * + * // Define how the resource is acquired + * const acquire = Effect.tryPromise({ + * try: () => + * getMyResource().then((res) => { + * console.log("Resource acquired") + * return res + * }), + * catch: () => new Error("getMyResourceError") + * }) + * + * // Define how the resource is released + * const release = (res: MyResource) => Effect.promise(() => res.close()) + * + * // Create the resource management workflow + * // + * // ┌─── Effect + * // ▼ + * const resource = Effect.acquireRelease(acquire, release) + * ``` + * + * @see {@link acquireUseRelease} for a version that automatically handles the scoping of resources. + * + * @since 2.0.0 + * @category Scoping, Resources & Finalization + */ +export const acquireRelease: { + ( + release: (a: A, exit: Exit.Exit) => Effect + ): (acquire: Effect) => Effect + ( + acquire: Effect, + release: (a: A, exit: Exit.Exit) => Effect + ): Effect +} = fiberRuntime.acquireRelease + +/** + * Creates a scoped resource with an interruptible acquire action. + * + * **Details** + * + * This function is similar to {@link acquireRelease}, but it allows the + * acquisition of the resource to be interrupted. The `acquire` effect, which + * represents the process of obtaining the resource, can be interrupted if + * necessary. + * + * @since 2.0.0 + * @category Scoping, Resources & Finalization + */ +export const acquireReleaseInterruptible: { + ( + release: (exit: Exit.Exit) => Effect + ): (acquire: Effect) => Effect + ( + acquire: Effect, + release: (exit: Exit.Exit) => Effect + ): Effect +} = fiberRuntime.acquireReleaseInterruptible + +/** + * Many real-world operations involve working with resources that must be released when no longer needed, such as: + * + * - Database connections + * - File handles + * - Network requests + * + * This function ensures that a resource is: + * + * 1. **Acquired** properly. + * 2. **Used** for its intended purpose. + * 3. **Released** even if an error occurs. + * + * **Example** (Automatically Managing Resource Lifetime) + * + * ```ts + * import { Effect, Console } from "effect" + * + * // Define an interface for a resource + * interface MyResource { + * readonly contents: string + * readonly close: () => Promise + * } + * + * // Simulate resource acquisition + * const getMyResource = (): Promise => + * Promise.resolve({ + * contents: "lorem ipsum", + * close: () => + * new Promise((resolve) => { + * console.log("Resource released") + * resolve() + * }) + * }) + * + * // Define how the resource is acquired + * const acquire = Effect.tryPromise({ + * try: () => + * getMyResource().then((res) => { + * console.log("Resource acquired") + * return res + * }), + * catch: () => new Error("getMyResourceError") + * }) + * + * // Define how the resource is released + * const release = (res: MyResource) => Effect.promise(() => res.close()) + * + * const use = (res: MyResource) => Console.log(`content is ${res.contents}`) + * + * // ┌─── Effect + * // ▼ + * const program = Effect.acquireUseRelease(acquire, use, release) + * + * Effect.runPromise(program) + * // Output: + * // Resource acquired + * // content is lorem ipsum + * // Resource released + * ``` + * + * @since 2.0.0 + * @category Scoping, Resources & Finalization + */ +export const acquireUseRelease: { + ( + use: (a: A) => Effect, + release: (a: A, exit: Exit.Exit) => Effect + ): (acquire: Effect) => Effect + ( + acquire: Effect, + use: (a: A) => Effect, + release: (a: A, exit: Exit.Exit) => Effect + ): Effect +} = core.acquireUseRelease + +/** + * Ensures a finalizer is added to the scope of the calling effect, guaranteeing + * it runs when the scope is closed. + * + * **Details** + * + * This function adds a finalizer that will execute whenever the scope of the + * effect is closed, regardless of whether the effect succeeds, fails, or is + * interrupted. The finalizer receives the `Exit` value of the effect's scope, + * allowing it to react differently depending on how the effect concludes. + * + * Finalizers are a reliable way to manage resource cleanup, ensuring that + * resources such as file handles, network connections, or database transactions + * are properly closed even in the event of an unexpected interruption or error. + * + * Finalizers operate in conjunction with Effect's scoped resources. If an + * effect with a finalizer is wrapped in a scope, the finalizer will execute + * automatically when the scope ends. + * + * **Example** (Adding a Finalizer on Success) + * + * ```ts + * import { Effect, Console } from "effect" + * + * // ┌─── Effect + * // ▼ + * const program = Effect.gen(function* () { + * yield* Effect.addFinalizer((exit) => + * Console.log(`Finalizer executed. Exit status: ${exit._tag}`) + * ) + * return "some result" + * }) + * + * // Wrapping the effect in a scope + * // + * // ┌─── Effect + * // ▼ + * const runnable = Effect.scoped(program) + * + * Effect.runPromiseExit(runnable).then(console.log) + * // Output: + * // Finalizer executed. Exit status: Success + * // { _id: 'Exit', _tag: 'Success', value: 'some result' } + * ``` + * + * **Example** (Adding a Finalizer on Failure) + * + * ```ts + * import { Effect, Console } from "effect" + * + * // ┌─── Effect + * // ▼ + * const program = Effect.gen(function* () { + * yield* Effect.addFinalizer((exit) => + * Console.log(`Finalizer executed. Exit status: ${exit._tag}`) + * ) + * return yield* Effect.fail("Uh oh!") + * }) + * + * // Wrapping the effect in a scope + * // + * // ┌─── Effect + * // ▼ + * const runnable = Effect.scoped(program) + * + * Effect.runPromiseExit(runnable).then(console.log) + * // Output: + * // Finalizer executed. Exit status: Failure + * // { + * // _id: 'Exit', + * // _tag: 'Failure', + * // cause: { _id: 'Cause', _tag: 'Fail', failure: 'Uh oh!' } + * // } + * ``` + * + * **Example** (Adding a Finalizer on Interruption) + * + * ```ts + * import { Effect, Console } from "effect" + * + * // ┌─── Effect + * // ▼ + * const program = Effect.gen(function* () { + * yield* Effect.addFinalizer((exit) => + * Console.log(`Finalizer executed. Exit status: ${exit._tag}`) + * ) + * return yield* Effect.interrupt + * }) + * + * // Wrapping the effect in a scope + * // + * // ┌─── Effect + * // ▼ + * const runnable = Effect.scoped(program) + * + * Effect.runPromiseExit(runnable).then(console.log) + * // Output: + * // Finalizer executed. Exit status: Failure + * // { + * // _id: 'Exit', + * // _tag: 'Failure', + * // cause: { + * // _id: 'Cause', + * // _tag: 'Interrupt', + * // fiberId: { + * // _id: 'FiberId', + * // _tag: 'Runtime', + * // id: 0, + * // startTimeMillis: ... + * // } + * // } + * // } + * ``` + * + * @see {@link onExit} for attaching a finalizer directly to an effect. + * + * @since 2.0.0 + * @category Scoping, Resources & Finalization + */ +export const addFinalizer: ( + finalizer: (exit: Exit.Exit) => Effect +) => Effect = fiberRuntime.addFinalizer + +/** + * Guarantees the execution of a finalizer when an effect starts execution. + * + * **Details** + * + * This function allows you to specify a `finalizer` effect that will always be + * run once the effect starts execution, regardless of whether the effect + * succeeds, fails, or is interrupted. + * + * **When to Use** + * + * This is useful when you need to ensure that certain cleanup or final steps + * are executed in all cases, such as releasing resources or performing + * necessary logging. + * + * While this function provides strong guarantees about executing the finalizer, + * it is considered a low-level tool, which may not be ideal for more complex + * resource management. For higher-level resource management with automatic + * acquisition and release, see the {@link acquireRelease} family of functions. + * For use cases where you need access to the result of an effect, consider + * using {@link onExit}. + * + * **Example** (Running a Finalizer in All Outcomes) + * + * ```ts + * import { Console, Effect } from "effect" + * + * // Define a cleanup effect + * const handler = Effect.ensuring(Console.log("Cleanup completed")) + * + * // Define a successful effect + * const success = Console.log("Task completed").pipe( + * Effect.as("some result"), + * handler + * ) + * + * Effect.runFork(success) + * // Output: + * // Task completed + * // Cleanup completed + * + * // Define a failing effect + * const failure = Console.log("Task failed").pipe( + * Effect.andThen(Effect.fail("some error")), + * handler + * ) + * + * Effect.runFork(failure) + * // Output: + * // Task failed + * // Cleanup completed + * + * // Define an interrupted effect + * const interruption = Console.log("Task interrupted").pipe( + * Effect.andThen(Effect.interrupt), + * handler + * ) + * + * Effect.runFork(interruption) + * // Output: + * // Task interrupted + * // Cleanup completed + * ``` + * + * @see {@link onExit} for a version that provides access to the result of an + * effect. + * + * @since 2.0.0 + * @category Scoping, Resources & Finalization + */ +export const ensuring: { + (finalizer: Effect): (self: Effect) => Effect + (self: Effect, finalizer: Effect): Effect +} = fiberRuntime.ensuring + +/** + * Ensures a cleanup effect runs whenever the calling effect fails, providing + * the failure cause to the cleanup effect. + * + * **Details** + * + * This function allows you to attach a cleanup effect that runs whenever the + * calling effect fails. The cleanup effect receives the cause of the failure, + * allowing you to perform actions such as logging, releasing resources, or + * executing additional recovery logic based on the error. The cleanup effect + * will execute even if the failure is due to interruption. + * + * Importantly, the cleanup effect itself is uninterruptible, ensuring that it + * completes regardless of external interruptions. + * + * **Example** (Running Cleanup Only on Failure) + * + * ```ts + * import { Console, Effect } from "effect" + * + * // This handler logs the failure cause when the effect fails + * const handler = Effect.onError((cause) => + * Console.log(`Cleanup completed: ${cause}`) + * ) + * + * // Define a successful effect + * const success = Console.log("Task completed").pipe( + * Effect.as("some result"), + * handler + * ) + * + * Effect.runFork(success) + * // Output: + * // Task completed + * + * // Define a failing effect + * const failure = Console.log("Task failed").pipe( + * Effect.andThen(Effect.fail("some error")), + * handler + * ) + * + * Effect.runFork(failure) + * // Output: + * // Task failed + * // Cleanup completed: Error: some error + * + * // Define a failing effect + * const defect = Console.log("Task failed with defect").pipe( + * Effect.andThen(Effect.die("Boom!")), + * handler + * ) + * + * Effect.runFork(defect) + * // Output: + * // Task failed with defect + * // Cleanup completed: Error: Boom! + * + * // Define an interrupted effect + * const interruption = Console.log("Task interrupted").pipe( + * Effect.andThen(Effect.interrupt), + * handler + * ) + * + * Effect.runFork(interruption) + * // Output: + * // Task interrupted + * // Cleanup completed: All fibers interrupted without errors. + * ``` + * + * @see {@link ensuring} for attaching a cleanup effect that runs on both success and failure. + * @see {@link onExit} for attaching a cleanup effect that runs on all possible exits. + * + * @since 2.0.0 + * @category Scoping, Resources & Finalization + */ +export const onError: { + ( + cleanup: (cause: Cause.Cause) => Effect + ): (self: Effect) => Effect + ( + self: Effect, + cleanup: (cause: Cause.Cause) => Effect + ): Effect +} = core.onError + +/** + * Guarantees that a cleanup function runs regardless of whether the effect + * succeeds, fails, or is interrupted. + * + * **Details** + * + * This function ensures that a provided cleanup function is executed after the + * effect completes, regardless of the outcome. The cleanup function is given + * the `Exit` value of the effect, which provides detailed information about the + * result: + * - If the effect succeeds, the `Exit` contains the success value. + * - If the effect fails, the `Exit` contains the error or failure cause. + * - If the effect is interrupted, the `Exit` reflects the interruption. + * + * The cleanup function is guaranteed to run uninterruptibly, ensuring reliable + * resource management even in complex or high-concurrency scenarios. + * + * **Example** (Running a Cleanup Function with the Effect’s Result) + * + * ```ts + * import { Console, Effect, Exit } from "effect" + * + * // Define a cleanup effect that logs the result + * const handler = Effect.onExit((exit) => + * Console.log(`Cleanup completed: ${Exit.getOrElse(exit, String)}`) + * ) + * + * // Define a successful effect + * const success = Console.log("Task completed").pipe( + * Effect.as("some result"), + * handler + * ) + * + * Effect.runFork(success) + * // Output: + * // Task completed + * // Cleanup completed: some result + * + * // Define a failing effect + * const failure = Console.log("Task failed").pipe( + * Effect.andThen(Effect.fail("some error")), + * handler + * ) + * + * Effect.runFork(failure) + * // Output: + * // Task failed + * // Cleanup completed: Error: some error + * + * // Define an interrupted effect + * const interruption = Console.log("Task interrupted").pipe( + * Effect.andThen(Effect.interrupt), + * handler + * ) + * + * Effect.runFork(interruption) + * // Output: + * // Task interrupted + * // Cleanup completed: All fibers interrupted without errors. + * ``` + * + * @since 2.0.0 + * @category Scoping, Resources & Finalization + */ +export const onExit: { + ( + cleanup: (exit: Exit.Exit) => Effect + ): (self: Effect) => Effect + ( + self: Effect, + cleanup: (exit: Exit.Exit) => Effect + ): Effect +} = core.onExit + +/** + * Ensures that finalizers are run concurrently when the scope of an effect is + * closed. + * + * **Details** + * + * This function modifies the behavior of finalizers within a scoped workflow to + * allow them to run concurrently when the scope is closed. + * + * By default, finalizers are executed sequentially in reverse order of their + * addition, but this function changes that behavior to execute all finalizers + * concurrently. + * + * **When to Use** + * + * Running finalizers concurrently can improve performance when multiple + * independent cleanup tasks need to be performed. However, it requires that + * these tasks do not depend on the order of execution or introduce race + * conditions. + * + * **Example** + * + * ```ts + * import { Console, Effect } from "effect" + * + * // Define a program that adds multiple finalizers + * const program = Effect.gen(function*() { + * yield* Effect.addFinalizer(() => Console.log("Finalizer 1 executed").pipe(Effect.delay("300 millis"))) + * yield* Effect.addFinalizer(() => Console.log("Finalizer 2 executed").pipe(Effect.delay("100 millis"))) + * yield* Effect.addFinalizer(() => Console.log("Finalizer 3 executed").pipe(Effect.delay("200 millis"))) + * return "some result" + * }) + * + * // Modify the program to ensure finalizers run in parallel + * const modified = program.pipe(Effect.parallelFinalizers) + * + * const runnable = Effect.scoped(modified) + * + * Effect.runFork(runnable) + * // Output: + * // Finalizer 2 executed + * // Finalizer 3 executed + * // Finalizer 1 executed + * ``` + * + * @see {@link sequentialFinalizers} for a version that ensures finalizers are run sequentially. + * + * @since 2.0.0 + * @category Scoping, Resources & Finalization + */ +export const parallelFinalizers: (self: Effect) => Effect = fiberRuntime.parallelFinalizers + +/** + * Ensures that finalizers are run sequentially in reverse order of their + * addition. + * + * **Details** + * + * This function modifies the behavior of finalizers within a scoped workflow to + * ensure they are run sequentially in reverse order when the scope is closed. + * + * By default, finalizers are executed sequentially, so this only changes the + * behavior if the scope is configured to run finalizers concurrently. + * + * @see {@link parallelFinalizers} for a version that ensures finalizers are run concurrently. + * + * @since 2.0.0 + * @category Scoping, Resources & Finalization + */ +export const sequentialFinalizers: (self: Effect) => Effect = + fiberRuntime.sequentialFinalizers + +/** + * Applies a custom execution strategy to finalizers within a scoped workflow. + * + * **Details** + * + * This function allows you to control how finalizers are executed in a scope by + * applying a specified `ExecutionStrategy`. The `strategy` can dictate whether + * finalizers run (e.g., sequentially or in parallel). + * + * Additionally, the function provides a `restore` operation, which ensures that + * the effect passed to it is executed under the default execution strategy. + * + * @since 2.0.0 + * @category Scoping, Resources & Finalization + */ +export const finalizersMask: ( + strategy: ExecutionStrategy +) => ( + self: (restore: (self: Effect) => Effect) => Effect +) => Effect = fiberRuntime.finalizersMask + +/** + * Provides access to the current scope in a scoped workflow. + * + * @since 2.0.0 + * @category Scoping, Resources & Finalization + */ +export const scope: Effect = fiberRuntime.scope + +/** + * Accesses the current scope and uses it to perform the specified effect. + * + * @since 2.0.0 + * @category Scoping, Resources & Finalization + */ +export const scopeWith: (f: (scope: Scope.Scope) => Effect) => Effect = + fiberRuntime.scopeWith + +/** + * Creates a `Scope`, passes it to the specified effectful function, and closes + * the scope when the effect completes (whether through success, failure, or + * interruption). + * + * @since 3.11.0 + * @category Scoping, Resources & Finalization + */ +export const scopedWith: (f: (scope: Scope.Scope) => Effect) => Effect = + fiberRuntime.scopedWith + +/** + * Scopes all resources used in an effect to the lifetime of the effect. + * + * **Details** + * + * This function ensures that all resources used within an effect are tied to + * its lifetime. Finalizers for these resources are executed automatically when + * the effect completes, whether through success, failure, or interruption. This + * guarantees proper resource cleanup without requiring explicit management. + * + * @since 2.0.0 + * @category Scoping, Resources & Finalization + */ +export const scoped: (effect: Effect) => Effect> = + fiberRuntime.scopedEffect + +/** + * Scopes all resources acquired by one effect to the lifetime of another + * effect. + * + * **Details** + * + * This function allows you to scope the resources acquired by one effect + * (`self`) to the lifetime of another effect (`use`). This ensures that the + * resources are cleaned up as soon as the `use` effect completes, regardless of + * how the `use` effect ends (success, failure, or interruption). + * + * **Example** + * + * ```ts + * import { Console, Effect } from "effect" + * + * const acquire = Console.log("Acquiring resource").pipe( + * Effect.as(1), + * Effect.tap(Effect.addFinalizer(() => Console.log("Releasing resource"))) + * ) + * const use = (resource: number) => Console.log(`Using resource: ${resource}`) + * + * const program = acquire.pipe(Effect.using(use)) + * + * Effect.runFork(program) + * // Output: + * // Acquiring resource + * // Using resource: 1 + * // Releasing resource + * ``` + * + * @see {@link scopedWith} Manage scoped operations with a temporary scope. + * + * @since 2.0.0 + * @category Scoping, Resources & Finalization + */ +export const using: { + ( + use: (a: A) => Effect + ): (self: Effect) => Effect> + ( + self: Effect, + use: (a: A) => Effect + ): Effect> +} = fiberRuntime.using + +/** + * Returns the result of the effect and a finalizer to close its scope. + * + * **Details** + * + * This function allows you to retrieve both the result of an effect and a + * finalizer that can be used to manually close its scope. This is useful for + * workflows where you need early access to the result while retaining control + * over the resource cleanup process. + * + * **Example** + * + * ```ts + * import { Console, Effect } from "effect" + * + * const acquire = Console.log("Acquiring resource").pipe( + * Effect.as(1), + * Effect.tap(Effect.addFinalizer(() => Console.log("Releasing resource"))) + * ) + * const program = Effect.gen(function*() { + * const [finalizer, resource] = yield* Effect.withEarlyRelease(acquire) + * console.log(`Using resource: ${resource}`) + * yield* Effect.sleep("1 second") + * yield* finalizer + * }) + * + * Effect.runFork(program.pipe(Effect.scoped)) + * // Output: + * // Acquiring resource + * // Using resource: 1 + * // Releasing resource + * ``` + * + * @since 2.0.0 + * @category Scoping, Resources & Finalization + */ +export const withEarlyRelease: ( + self: Effect +) => Effect<[finalizer: Effect, result: A], E, R | Scope.Scope> = fiberRuntime.withEarlyRelease + +/** + * Returns a new effect that will not succeed with its value before first + * waiting for the end of all child fibers forked by the effect. + * + * @since 2.0.0 + * @category Supervision & Fibers + */ +export const awaitAllChildren: (self: Effect) => Effect = circular.awaitAllChildren + +/** + * Returns a new workflow that will not supervise any fibers forked by this + * workflow. + * + * @since 2.0.0 + * @category Supervision & Fibers + */ +export const daemonChildren: (self: Effect) => Effect = fiberRuntime.daemonChildren + +/** + * Constructs an effect with information about the current `Fiber`. + * + * @since 2.0.0 + * @category Supervision & Fibers + */ +export const descriptor: Effect = effect.descriptor + +/** + * Constructs an effect based on information about the current `Fiber`. + * + * @since 2.0.0 + * @category Supervision & Fibers + */ +export const descriptorWith: (f: (descriptor: Fiber.Fiber.Descriptor) => Effect) => Effect = + effect.descriptorWith + +/** + * Returns a new workflow that executes this one and captures the changes in + * `FiberRef` values. + * + * @since 2.0.0 + * @category Supervision & Fibers + */ +export const diffFiberRefs: ( + self: Effect +) => Effect<[FiberRefsPatch.FiberRefsPatch, A], E, R> = effect.diffFiberRefs + +/** + * Acts on the children of this fiber (collected into a single fiber), + * guaranteeing the specified callback will be invoked, whether or not this + * effect succeeds. + * + * @since 2.0.0 + * @category Supervision & Fibers + */ +export const ensuringChild: { + ( + f: (fiber: Fiber.Fiber, any>) => Effect + ): (self: Effect) => Effect + ( + self: Effect, + f: (fiber: Fiber.Fiber, any>) => Effect + ): Effect +} = circular.ensuringChild + +/** + * Acts on the children of this fiber, guaranteeing the specified callback + * will be invoked, whether or not this effect succeeds. + * + * @since 2.0.0 + * @category Supervision & Fibers + */ +export const ensuringChildren: { + ( + children: (fibers: ReadonlyArray>) => Effect + ): (self: Effect) => Effect + ( + self: Effect, + children: (fibers: ReadonlyArray>) => Effect + ): Effect +} = circular.ensuringChildren + +/** + * @since 2.0.0 + * @category Supervision & Fibers + */ +export const fiberId: Effect = core.fiberId + +/** + * @since 2.0.0 + * @category Supervision & Fibers + */ +export const fiberIdWith: (f: (descriptor: FiberId.Runtime) => Effect) => Effect = + core.fiberIdWith + +/** + * Creates a new fiber to run an effect concurrently. + * + * **Details** + * + * This function takes an effect and forks it into a separate fiber, allowing it + * to run concurrently without blocking the original effect. The new fiber + * starts execution immediately after being created, and the fiber object is + * returned immediately without waiting for the effect to begin. This is useful + * when you want to run tasks concurrently while continuing other tasks in the + * parent fiber. + * + * The forked fiber is attached to the parent fiber's scope. This means that + * when the parent fiber terminates, the child fiber will also be terminated + * automatically. This feature, known as "auto supervision," ensures that no + * fibers are left running unintentionally. If you prefer not to have this auto + * supervision behavior, you can use {@link forkDaemon} or {@link forkIn}. + * + * **When to Use** + * + * Use this function when you need to run an effect concurrently without + * blocking the current execution flow. For example, you might use it to launch + * background tasks or concurrent computations. However, working with fibers can + * be complex, so before using this function directly, you might want to explore + * higher-level functions like {@link raceWith}, {@link zip}, or others that can + * manage concurrency for you. + * + * **Example** + * + * ```ts + * import { Effect } from "effect" + * + * const fib = (n: number): Effect.Effect => + * n < 2 + * ? Effect.succeed(n) + * : Effect.zipWith(fib(n - 1), fib(n - 2), (a, b) => a + b) + * + * // ┌─── Effect, never, never> + * // ▼ + * const fib10Fiber = Effect.fork(fib(10)) + * ``` + * + * @see {@link forkWithErrorHandler} for a version that allows you to handle errors. + * + * @since 2.0.0 + * @category Supervision & Fibers + */ +export const fork: (self: Effect) => Effect, never, R> = fiberRuntime.fork + +/** + * Creates a long-running background fiber that is independent of its parent. + * + * **Details** + * + * This function creates a "daemon" fiber that runs in the background and is not + * tied to the lifecycle of its parent fiber. Unlike normal fibers that stop + * when the parent fiber terminates, a daemon fiber will continue running until + * the global scope closes or the fiber completes naturally. This makes it + * useful for tasks that need to run in the background independently, such as + * periodic logging, monitoring, or background data processing. + * + * **Example** (Creating a Daemon Fiber) + * + * ```ts + * import { Effect, Console, Schedule } from "effect" + * + * // Daemon fiber that logs a message repeatedly every second + * const daemon = Effect.repeat( + * Console.log("daemon: still running!"), + * Schedule.fixed("1 second") + * ) + * + * const parent = Effect.gen(function* () { + * console.log("parent: started!") + * // Daemon fiber running independently + * yield* Effect.forkDaemon(daemon) + * yield* Effect.sleep("3 seconds") + * console.log("parent: finished!") + * }) + * + * Effect.runFork(parent) + * // Output: + * // parent: started! + * // daemon: still running! + * // daemon: still running! + * // daemon: still running! + * // parent: finished! + * // daemon: still running! + * // daemon: still running! + * // daemon: still running! + * // daemon: still running! + * // daemon: still running! + * // ...etc... + * ``` + * + * @since 2.0.0 + * @category Supervision & Fibers + */ +export const forkDaemon: (self: Effect) => Effect, never, R> = + fiberRuntime.forkDaemon + +/** + * Returns an effect that forks all of the specified values, and returns a + * composite fiber that produces a list of their results, in order. + * + * @since 2.0.0 + * @category Supervision & Fibers + */ +export const forkAll: { + ( + options?: { readonly discard?: false | undefined } | undefined + ): >( + effects: Iterable + ) => Effect>, Effect.Error>, never, Effect.Context> + ( + options: { readonly discard: true } + ): >(effects: Iterable) => Effect> + >( + effects: Iterable, + options?: { readonly discard?: false | undefined } | undefined + ): Effect>, Effect.Error>, never, Effect.Context> + >( + effects: Iterable, + options: { readonly discard: true } + ): Effect> +} = circular.forkAll + +/** + * Forks an effect in a specific scope, allowing finer control over its + * execution. + * + * **Details** + * + * There are some cases where we need more fine-grained control, so we want to + * fork a fiber in a specific scope. We can use the `Effect.forkIn` operator + * which takes the target scope as an argument. + * + * The fiber will be interrupted when the scope is closed. + * + * **Example** (Forking a Fiber in a Specific Scope) + * + * In this example, the child fiber is forked into the outerScope, + * allowing it to outlive the inner scope but still be terminated + * when the outerScope is closed. + * + * ```ts + * import { Console, Effect, Schedule } from "effect" + * + * // Child fiber that logs a message repeatedly every second + * const child = Effect.repeat( + * Console.log("child: still running!"), + * Schedule.fixed("1 second") + * ) + * + * const program = Effect.scoped( + * Effect.gen(function* () { + * yield* Effect.addFinalizer(() => + * Console.log("The outer scope is about to be closed!") + * ) + * + * // Capture the outer scope + * const outerScope = yield* Effect.scope + * + * // Create an inner scope + * yield* Effect.scoped( + * Effect.gen(function* () { + * yield* Effect.addFinalizer(() => + * Console.log("The inner scope is about to be closed!") + * ) + * // Fork the child fiber in the outer scope + * yield* Effect.forkIn(child, outerScope) + * yield* Effect.sleep("3 seconds") + * }) + * ) + * + * yield* Effect.sleep("5 seconds") + * }) + * ) + * + * Effect.runFork(program) + * // Output: + * // child: still running! + * // child: still running! + * // child: still running! + * // The inner scope is about to be closed! + * // child: still running! + * // child: still running! + * // child: still running! + * // child: still running! + * // child: still running! + * // child: still running! + * // The outer scope is about to be closed! + * ``` + * + * @since 2.0.0 + * @category Supervision & Fibers + */ +export const forkIn: { + (scope: Scope.Scope): (self: Effect) => Effect, never, R> + (self: Effect, scope: Scope.Scope): Effect, never, R> +} = circular.forkIn + +/** + * Forks a fiber in a local scope, ensuring it outlives its parent. + * + * **Details** + * + * This function is used to create fibers that are tied to a local scope, + * meaning they are not dependent on their parent fiber's lifecycle. Instead, + * they will continue running until the scope they were created in is closed. + * This is particularly useful when you need a fiber to run independently of the + * parent fiber, but still want it to be terminated when the scope ends. + * + * Fibers created with this function are isolated from the parent fiber’s + * termination, so they can run for a longer period. This behavior is different + * from fibers created with {@link fork}, which are terminated when the parent fiber + * terminates. With `forkScoped`, the child fiber will keep running until the + * local scope ends, regardless of the state of the parent fiber. + * + * **Example** (Forking a Fiber in a Local Scope) + * + * In this example, the child fiber continues to run beyond the lifetime of the parent fiber. + * The child fiber is tied to the local scope and will be terminated only when the scope ends. + * + * ```ts + * import { Effect, Console, Schedule } from "effect" + * + * // Child fiber that logs a message repeatedly every second + * const child = Effect.repeat( + * Console.log("child: still running!"), + * Schedule.fixed("1 second") + * ) + * + * // ┌─── Effect + * // ▼ + * const parent = Effect.gen(function* () { + * console.log("parent: started!") + * // Child fiber attached to local scope + * yield* Effect.forkScoped(child) + * yield* Effect.sleep("3 seconds") + * console.log("parent: finished!") + * }) + * + * // Program runs within a local scope + * const program = Effect.scoped( + * Effect.gen(function* () { + * console.log("Local scope started!") + * yield* Effect.fork(parent) + * // Scope lasts for 5 seconds + * yield* Effect.sleep("5 seconds") + * console.log("Leaving the local scope!") + * }) + * ) + * + * Effect.runFork(program) + * // Output: + * // Local scope started! + * // parent: started! + * // child: still running! + * // child: still running! + * // child: still running! + * // parent: finished! + * // child: still running! + * // child: still running! + * // Leaving the local scope! + * ``` + * + * @since 2.0.0 + * @category Supervision & Fibers + */ +export const forkScoped: (self: Effect) => Effect, never, Scope.Scope | R> = + circular.forkScoped + +/** + * Like {@link fork} but handles an error with the provided handler. + * + * @since 2.0.0 + * @category Supervision & Fibers + */ +export const forkWithErrorHandler: { + ( + handler: (e: E) => Effect + ): (self: Effect) => Effect, never, R> + ( + self: Effect, + handler: (e: E) => Effect + ): Effect, never, R> +} = fiberRuntime.forkWithErrorHandler + +/** + * Creates an `Effect` value that represents the exit value of the specified + * fiber. + * + * @see {@link fromFiberEffect} for creating an effect from a fiber obtained from an effect. + * + * @since 2.0.0 + * @category Supervision & Fibers + */ +export const fromFiber: (fiber: Fiber.Fiber) => Effect = circular.fromFiber + +/** + * Creates an `Effect` value that represents the exit value of a fiber obtained + * from an effect. + * + * @see {@link fromFiber} for creating an effect from a fiber. + * + * @since 2.0.0 + * @category Supervision & Fibers + */ +export const fromFiberEffect: (fiber: Effect, E, R>) => Effect = + circular.fromFiberEffect + +/** + * Supervises child fibers by reporting them to a specified supervisor. + * + * **Details** + * + * This function takes a supervisor as an argument and returns an effect where + * all child fibers forked within it are supervised by the provided supervisor. + * This enables you to capture detailed information about these child fibers, + * such as their status, through the supervisor. + * + * **Example** (Monitoring Fiber Count) + * + * ```ts + * import { Effect, Supervisor, Schedule, Fiber, FiberStatus } from "effect" + * + * // Main program that monitors fibers while calculating a Fibonacci number + * const program = Effect.gen(function* () { + * // Create a supervisor to track child fibers + * const supervisor = yield* Supervisor.track + * + * // Start a Fibonacci calculation, supervised by the supervisor + * const fibFiber = yield* fib(20).pipe( + * Effect.supervised(supervisor), + * // Fork the Fibonacci effect into a fiber + * Effect.fork + * ) + * + * // Define a schedule to periodically monitor the fiber count every 500ms + * const policy = Schedule.spaced("500 millis").pipe( + * Schedule.whileInputEffect((_) => + * Fiber.status(fibFiber).pipe( + * // Continue while the Fibonacci fiber is not done + * Effect.andThen((status) => status !== FiberStatus.done) + * ) + * ) + * ) + * + * // Start monitoring the fibers, using the supervisor to track the count + * const monitorFiber = yield* monitorFibers(supervisor).pipe( + * // Repeat the monitoring according to the schedule + * Effect.repeat(policy), + * // Fork the monitoring into its own fiber + * Effect.fork + * ) + * + * // Join the monitor and Fibonacci fibers to ensure they complete + * yield* Fiber.join(monitorFiber) + * const result = yield* Fiber.join(fibFiber) + * + * console.log(`fibonacci result: ${result}`) + * }) + * + * // Function to monitor and log the number of active fibers + * const monitorFibers = ( + * supervisor: Supervisor.Supervisor>> + * ): Effect.Effect => + * Effect.gen(function* () { + * const fibers = yield* supervisor.value // Get the current set of fibers + * console.log(`number of fibers: ${fibers.length}`) + * }) + * + * // Recursive Fibonacci calculation, spawning fibers for each recursive step + * const fib = (n: number): Effect.Effect => + * Effect.gen(function* () { + * if (n <= 1) { + * return 1 + * } + * yield* Effect.sleep("500 millis") // Simulate work by delaying + * + * // Fork two fibers for the recursive Fibonacci calls + * const fiber1 = yield* Effect.fork(fib(n - 2)) + * const fiber2 = yield* Effect.fork(fib(n - 1)) + * + * // Join the fibers to retrieve their results + * const v1 = yield* Fiber.join(fiber1) + * const v2 = yield* Fiber.join(fiber2) + * + * return v1 + v2 // Combine the results + * }) + * + * Effect.runPromise(program) + * // Output: + * // number of fibers: 0 + * // number of fibers: 2 + * // number of fibers: 6 + * // number of fibers: 14 + * // number of fibers: 30 + * // number of fibers: 62 + * // number of fibers: 126 + * // number of fibers: 254 + * // number of fibers: 510 + * // number of fibers: 1022 + * // number of fibers: 2034 + * // number of fibers: 3795 + * // number of fibers: 5810 + * // number of fibers: 6474 + * // number of fibers: 4942 + * // number of fibers: 2515 + * // number of fibers: 832 + * // number of fibers: 170 + * // number of fibers: 18 + * // number of fibers: 0 + * // fibonacci result: 10946 + * ``` + * + * @since 2.0.0 + * @category Supervision & Fibers + */ +export const supervised: { + (supervisor: Supervisor.Supervisor): (self: Effect) => Effect + (self: Effect, supervisor: Supervisor.Supervisor): Effect +} = circular.supervised + +/** + * Transplants specified effects so that when those effects fork other + * effects, the forked effects will be governed by the scope of the fiber that + * executes this effect. + * + * This can be used to "graft" deep grandchildren onto a higher-level scope, + * effectively extending their lifespans into the parent scope. + * + * @since 2.0.0 + * @category Supervision & Fibers + */ +export const transplant: ( + f: (grafter: (effect: Effect) => Effect) => Effect +) => Effect = core.transplant + +/** + * @since 2.0.0 + * @category Supervision & Fibers + */ +export const withConcurrency: { + (concurrency: number | "unbounded"): (self: Effect) => Effect + (self: Effect, concurrency: number | "unbounded"): Effect +} = core.withConcurrency + +/** + * Sets the provided scheduler for usage in the wrapped effect + * + * @since 2.0.0 + * @category Scheduler + */ +export const withScheduler: { + (scheduler: Scheduler.Scheduler): (self: Effect) => Effect + (self: Effect, scheduler: Scheduler.Scheduler): Effect +} = Scheduler.withScheduler + +/** + * Sets the scheduling priority used when yielding + * + * @since 2.0.0 + * @category Scheduler + */ +export const withSchedulingPriority: { + (priority: number): (self: Effect) => Effect + (self: Effect, priority: number): Effect +} = core.withSchedulingPriority + +/** + * Sets the maximum number of operations before yield by the default schedulers + * + * @since 2.0.0 + * @category Scheduler + */ +export const withMaxOpsBeforeYield: { + (priority: number): (self: Effect) => Effect + (self: Effect, priority: number): Effect +} = core.withMaxOpsBeforeYield + +/** + * Retrieves the `Clock` service from the context. + * + * **Example** + * + * ```ts + * import { Effect } from "effect" + * + * const program = Effect.gen(function*() { + * const clock = yield* Effect.clock + * const currentTime = yield* clock.currentTimeMillis + * console.log(`Current time in milliseconds: ${currentTime}`) + * }) + * + * Effect.runFork(program) + * // Example Output: + * // Current time in milliseconds: 1735484796134 + * ``` + * + * @since 2.0.0 + * @category Clock + */ +export const clock: Effect = effect.clock + +/** + * Retrieves the `Clock` service from the context and provides it to the + * specified effectful function. + * + * **Example** + * + * ```ts + * import { Console, Effect } from "effect" + * + * const program = Effect.clockWith((clock) => + * clock.currentTimeMillis.pipe( + * Effect.map((currentTime) => `Current time is: ${currentTime}`), + * Effect.tap(Console.log) + * ) + * ) + * + * Effect.runFork(program) + * // Example Output: + * // Current time is: 1735484929744 + * ``` + * + * @since 2.0.0 + * @category Clock + */ +export const clockWith: (f: (clock: Clock.Clock) => Effect) => Effect = effect.clockWith + +/** + * Sets the implementation of the `Clock` service to the specified value and + * restores it to its original value when the scope is closed. + * + * @since 2.0.0 + * @category Clock + */ +export const withClockScoped: (clock: C) => Effect = + fiberRuntime.withClockScoped + +/** + * Executes the specified workflow with the specified implementation of the + * `Clock` service. + * + * @since 2.0.0 + * @category Clock + */ +export const withClock: { + (clock: C): (effect: Effect) => Effect + (effect: Effect, clock: C): Effect +} = defaultServices.withClock + +/** + * Retreives the `Console` service from the context + * + * @since 2.0.0 + * @category Console + */ +export const console: Effect = console_.console + +/** + * Retreives the `Console` service from the context and provides it to the + * specified effectful function. + * + * @since 2.0.0 + * @category Console + */ +export const consoleWith: (f: (console: Console) => Effect) => Effect = console_.consoleWith + +/** + * Sets the implementation of the console service to the specified value and + * restores it to its original value when the scope is closed. + * + * @since 2.0.0 + * @category Creating Effects + */ +export const withConsoleScoped: (console: A) => Effect = + console_.withConsoleScoped + +/** + * Executes the specified workflow with the specified implementation of the + * console service. + * + * @since 2.0.0 + * @category Console + */ +export const withConsole: { + (console: C): (effect: Effect) => Effect + (effect: Effect, console: C): Effect +} = console_.withConsole + +/** + * Delays the execution of an effect by a specified `Duration`. + * + * **Details + * + * This function postpones the execution of the provided effect by the specified + * duration. The duration can be provided in various formats supported by the + * `Duration` module. + * + * Internally, this function does not block the thread; instead, it uses an + * efficient, non-blocking mechanism to introduce the delay. + * + * **Example** + * + * ```ts + * import { Console, Effect } from "effect" + * + * const task = Console.log("Task executed") + * + * const program = Console.log("start").pipe( + * Effect.andThen( + * // Delays the log message by 2 seconds + * task.pipe(Effect.delay("2 seconds")) + * ) + * ) + * + * Effect.runFork(program) + * // Output: + * // start + * // Task executed + * ``` + * + * @since 2.0.0 + * @category Delays & Timeouts + */ +export const delay: { + (duration: Duration.DurationInput): (self: Effect) => Effect + (self: Effect, duration: Duration.DurationInput): Effect +} = effect.delay + +/** + * Suspends the execution of an effect for a specified `Duration`. + * + * **Details** + * + * This function pauses the execution of an effect for a given duration. It is + * asynchronous, meaning that it does not block the fiber executing the effect. + * Instead, the fiber is suspended during the delay period and can resume once + * the specified time has passed. + * + * The duration can be specified using various formats supported by the + * `Duration` module, such as a string (`"2 seconds"`) or numeric value + * representing milliseconds. + * + * **Example** + * + * ```ts + * import { Effect } from "effect" + * + * const program = Effect.gen(function*() { + * console.log("Starting task...") + * yield* Effect.sleep("3 seconds") // Waits for 3 seconds + * console.log("Task completed!") + * }) + * + * Effect.runFork(program) + * // Output: + * // Starting task... + * // Task completed! + * ``` + * + * @since 2.0.0 + * @category Delays & Timeouts + */ +export const sleep: (duration: Duration.DurationInput) => Effect = effect.sleep + +/** + * Executes an effect and measures the time it takes to complete. + * + * **Details** + * + * This function wraps the provided effect and returns a new effect that, when + * executed, performs the original effect and calculates its execution duration. + * + * The result of the new effect includes both the execution time (as a + * `Duration`) and the original effect's result. This is useful for monitoring + * performance or gaining insights into the time taken by specific operations. + * + * The original effect's behavior (success, failure, or interruption) remains + * unchanged, and the timing information is provided alongside the result in a + * tuple. + * + * **Example** + * + * ```ts + * import { Duration, Effect } from "effect" + * + * const task = Effect.gen(function*() { + * yield* Effect.sleep("2 seconds") // Simulates some work + * return "some result" + * }) + * + * const timedTask = task.pipe(Effect.timed) + * + * const program = Effect.gen(function*() { + * const [duration, result] = yield* timedTask + * console.log(`Task completed in ${Duration.toMillis(duration)} ms with result: ${result}`) + * }) + * + * Effect.runFork(program) + * // Output: Task completed in 2003.749125 ms with result: some result + * ``` + * + * @since 2.0.0 + * @category Delays & Timeouts + */ +export const timed: (self: Effect) => Effect<[duration: Duration.Duration, result: A], E, R> = + effect.timed + +/** + * Executes an effect and measures its execution time using a custom clock. + * + * **Details** + * + * This function extends the functionality of {@link timed} by allowing you to + * specify a custom clock for measuring the execution duration. The provided + * effect (`nanoseconds`) represents the clock and should return the current + * time in nanoseconds. The timing information is computed using this custom + * clock instead of the default system clock. + * + * @since 2.0.0 + * @category Delays & Timeouts + */ +export const timedWith: { + ( + nanoseconds: Effect + ): (self: Effect) => Effect<[Duration.Duration, A], E1 | E, R1 | R> + ( + self: Effect, + nanoseconds: Effect + ): Effect<[Duration.Duration, A], E | E1, R | R1> +} = effect.timedWith + +/** + * Adds a time limit to an effect, triggering a timeout if the effect exceeds + * the duration. + * + * **Details** + * + * This function allows you to enforce a time limit on the execution of an + * effect. If the effect does not complete within the given duration, it fails + * with a `TimeoutException`. This is useful for preventing tasks from hanging + * indefinitely, especially in scenarios where responsiveness or resource limits + * are critical. + * + * The returned effect will either: + * - Succeed with the original effect's result if it completes within the + * specified duration. + * - Fail with a `TimeoutException` if the time limit is exceeded. + * + * **Example** + * + * ```ts + * import { Effect } from "effect" + * + * const task = Effect.gen(function* () { + * console.log("Start processing...") + * yield* Effect.sleep("2 seconds") // Simulates a delay in processing + * console.log("Processing complete.") + * return "Result" + * }) + * + * // Output will show a TimeoutException as the task takes longer + * // than the specified timeout duration + * const timedEffect = task.pipe(Effect.timeout("1 second")) + * + * Effect.runPromiseExit(timedEffect).then(console.log) + * // Output: + * // Start processing... + * // { + * // _id: 'Exit', + * // _tag: 'Failure', + * // cause: { + * // _id: 'Cause', + * // _tag: 'Fail', + * // failure: { _tag: 'TimeoutException' } + * // } + * // } + * ``` + * + * @see {@link timeoutFail} for a version that raises a custom error. + * @see {@link timeoutFailCause} for a version that raises a custom defect. + * @see {@link timeoutTo} for a version that allows specifying both success and + * timeout handlers. + * + * @since 2.0.0 + * @category Delays & Timeouts + */ +export const timeout: { + (duration: Duration.DurationInput): (self: Effect) => Effect + (self: Effect, duration: Duration.DurationInput): Effect +} = circular.timeout + +/** + * Gracefully handles timeouts by returning an `Option` that represents either + * the result or a timeout. + * + * **Details** + * + * This function wraps the outcome of an effect in an `Option` type. If the + * effect completes within the specified duration, it returns a `Some` + * containing the result. If the effect times out, it returns a `None`. Unlike + * other timeout methods, this approach does not raise errors or exceptions; + * instead, it allows you to treat timeouts as a regular outcome, simplifying + * the logic for handling delays. + * + * **When to Use** + * + * This is useful when you want to handle timeouts without causing the program + * to fail, making it easier to manage situations where you expect tasks might + * take too long but want to continue executing other tasks. + * + * **Example** + * + * ```ts + * import { Effect } from "effect" + * + * const task = Effect.gen(function* () { + * console.log("Start processing...") + * yield* Effect.sleep("2 seconds") // Simulates a delay in processing + * console.log("Processing complete.") + * return "Result" + * }) + * + * const timedOutEffect = Effect.all([ + * task.pipe(Effect.timeoutOption("3 seconds")), + * task.pipe(Effect.timeoutOption("1 second")) + * ]) + * + * Effect.runPromise(timedOutEffect).then(console.log) + * // Output: + * // Start processing... + * // Processing complete. + * // Start processing... + * // [ + * // { _id: 'Option', _tag: 'Some', value: 'Result' }, + * // { _id: 'Option', _tag: 'None' } + * // ] + * ``` + * + * @see {@link timeout} for a version that raises a `TimeoutException`. + * @see {@link timeoutFail} for a version that raises a custom error. + * @see {@link timeoutFailCause} for a version that raises a custom defect. + * @see {@link timeoutTo} for a version that allows specifying both success and + * timeout handlers. + * + * @since 3.1.0 + * @category Delays & Timeouts + */ +export const timeoutOption: { + (duration: Duration.DurationInput): (self: Effect) => Effect, E, R> + (self: Effect, duration: Duration.DurationInput): Effect, E, R> +} = circular.timeoutOption + +/** + * Specifies a custom error to be produced when a timeout occurs. + * + * **Details** + * + * This function allows you to handle timeouts in a customized way by defining a + * specific error to be raised when an effect exceeds the given duration. Unlike + * default timeout behaviors that use generic exceptions, this function gives + * you the flexibility to specify a meaningful error type that aligns with your + * application's needs. + * + * When you apply this function, you provide: + * - A `duration`: The time limit for the effect. + * - An `onTimeout` function: A lazy evaluation function that generates the + * custom error if the timeout occurs. + * + * If the effect completes within the time limit, its result is returned + * normally. Otherwise, the `onTimeout` function is triggered, and its output is + * used as the error for the effect. + * + * **Example** + * + * ```ts + * import { Effect } from "effect" + * + * const task = Effect.gen(function* () { + * console.log("Start processing...") + * yield* Effect.sleep("2 seconds") // Simulates a delay in processing + * console.log("Processing complete.") + * return "Result" + * }) + * + * class MyTimeoutError { + * readonly _tag = "MyTimeoutError" + * } + * + * const program = task.pipe( + * Effect.timeoutFail({ + * duration: "1 second", + * onTimeout: () => new MyTimeoutError() // Custom timeout error + * }) + * ) + * + * Effect.runPromiseExit(program).then(console.log) + * // Output: + * // Start processing... + * // { + * // _id: 'Exit', + * // _tag: 'Failure', + * // cause: { + * // _id: 'Cause', + * // _tag: 'Fail', + * // failure: MyTimeoutError { _tag: 'MyTimeoutError' } + * // } + * // } + * ``` + * + * @see {@link timeout} for a version that raises a `TimeoutException`. + * @see {@link timeoutFailCause} for a version that raises a custom defect. + * @see {@link timeoutTo} for a version that allows specifying both success and + * timeout handlers. + * + * @since 2.0.0 + * @category Delays & Timeouts + */ +export const timeoutFail: { + ( + options: { readonly onTimeout: LazyArg; readonly duration: Duration.DurationInput } + ): (self: Effect) => Effect + ( + self: Effect, + options: { readonly onTimeout: LazyArg; readonly duration: Duration.DurationInput } + ): Effect +} = circular.timeoutFail + +/** + * Specifies a custom defect to be thrown when a timeout occurs. + * + * **Details** + * + * This function allows you to handle timeouts as exceptional cases by + * generating a custom defect when an effect exceeds the specified duration. You + * provide: + * - A `duration`: The time limit for the effect. + * - An `onTimeout` function: A lazy evaluation function that generates the + * custom defect (typically created using `Cause.die`). + * + * If the effect completes within the time limit, its result is returned + * normally. Otherwise, the custom defect is triggered, and the effect fails + * with that defect. + * + * **When to Use** + * + * This is especially useful when you need to treat timeouts as critical + * failures in your application and wish to include meaningful information in + * the defect. + * + * **Example** + * + * ```ts + * import { Effect, Cause } from "effect" + * + * const task = Effect.gen(function* () { + * console.log("Start processing...") + * yield* Effect.sleep("2 seconds") // Simulates a delay in processing + * console.log("Processing complete.") + * return "Result" + * }) + * + * const program = task.pipe( + * Effect.timeoutFailCause({ + * duration: "1 second", + * onTimeout: () => Cause.die("Timed out!") // Custom defect for timeout + * }) + * ) + * + * Effect.runPromiseExit(program).then(console.log) + * // Output: + * // Start processing... + * // { + * // _id: 'Exit', + * // _tag: 'Failure', + * // cause: { _id: 'Cause', _tag: 'Die', defect: 'Timed out!' } + * // } + * ``` + * + * @see {@link timeout} for a version that raises a `TimeoutException`. + * @see {@link timeoutFail} for a version that raises a custom error. + * @see {@link timeoutTo} for a version that allows specifying both success and + * timeout handlers. + * + * @since 2.0.0 + * @category Delays & Timeouts + */ +export const timeoutFailCause: { + ( + options: { readonly onTimeout: LazyArg>; readonly duration: Duration.DurationInput } + ): (self: Effect) => Effect + ( + self: Effect, + options: { readonly onTimeout: LazyArg>; readonly duration: Duration.DurationInput } + ): Effect +} = circular.timeoutFailCause + +/** + * Provides custom behavior for successful and timed-out operations. + * + * **Details** + * + * This function allows you to define distinct outcomes for an effect depending + * on whether it completes within a specified time frame or exceeds the timeout + * duration. You can provide: + * - `onSuccess`: A handler for processing the result of the effect if it + * completes successfully within the time limit. + * - `onTimeout`: A handler for generating a result when the effect times out. + * - `duration`: The maximum allowed time for the effect to complete. + * + * **When to Use** + * + * Unlike {@link timeout}, which raises an exception for timeouts, this function + * gives you full control over the behavior for both success and timeout + * scenarios. It is particularly useful when you want to encapsulate timeouts + * and successes into a specific data structure, like an `Either` type, to + * represent these outcomes in a meaningful way. + * + * **Example** + * + * ```ts + * import { Effect, Either } from "effect" + * + * const task = Effect.gen(function* () { + * console.log("Start processing...") + * yield* Effect.sleep("2 seconds") // Simulates a delay in processing + * console.log("Processing complete.") + * return "Result" + * }) + * + * const program = task.pipe( + * Effect.timeoutTo({ + * duration: "1 second", + * onSuccess: (result): Either.Either => + * Either.right(result), + * onTimeout: (): Either.Either => + * Either.left("Timed out!") + * }) + * ) + * + * Effect.runPromise(program).then(console.log) + * // Output: + * // Start processing... + * // { + * // _id: "Either", + * // _tag: "Left", + * // left: "Timed out!" + * // } + * ``` + * + * @see {@link timeout} for a version that raises a `TimeoutException`. + * @see {@link timeoutFail} for a version that raises a custom error. + * @see {@link timeoutFailCause} for a version that raises a custom defect. + * + * @since 2.0.0 + * @category Delays & Timeouts + */ +export const timeoutTo: { + ( + options: { + readonly onTimeout: LazyArg + readonly onSuccess: (a: A) => B + readonly duration: Duration.DurationInput + } + ): (self: Effect) => Effect + ( + self: Effect, + options: { + readonly onTimeout: LazyArg + readonly onSuccess: (a: A) => B + readonly duration: Duration.DurationInput + } + ): Effect +} = circular.timeoutTo + +/** + * Allows working with the default configuration provider. + * + * **Details** + * + * This function retrieves the default configuration provider and passes it to + * the provided function, which can use it to perform computations or retrieve + * configuration values. The function can return an effect that leverages the + * configuration provider for its operations. + * + * @since 2.0.0 + * @category Config + */ +export const configProviderWith: (f: (provider: ConfigProvider) => Effect) => Effect = + defaultServices.configProviderWith + +/** + * Executes an effect using a specific configuration provider. + * + * **Details** + * + * This function lets you run an effect with a specified configuration provider. + * The custom provider will override the default configuration provider for the + * duration of the effect's execution. + * + * **When to Use** + * + * This is particularly useful when you need to use a different set of + * configuration values or sources for specific parts of your application. + * + * **Example** + * + * ```ts + * import { Config, ConfigProvider, Effect } from "effect" + * + * const customProvider: ConfigProvider.ConfigProvider = ConfigProvider.fromMap( + * new Map([["custom-key", "custom-value"]]) + * ) + * + * const program = Effect.withConfigProvider(customProvider)( + * Effect.gen(function*() { + * const value = yield* Config.string("custom-key") + * console.log(`Config value: ${value}`) + * }) + * ) + * + * Effect.runPromise(program) + * // Output: + * // Config value: custom-value + * ``` + * + * @since 2.0.0 + * @category Config + */ +export const withConfigProvider: { + (provider: ConfigProvider): (self: Effect) => Effect + (self: Effect, provider: ConfigProvider): Effect +} = defaultServices.withConfigProvider + +/** + * Sets a configuration provider within a scope. + * + * **Details** + * + * This function sets the configuration provider to a specified value and + * ensures that it is restored to its original value when the scope is closed. + * + * @since 2.0.0 + * @category Config + */ +export const withConfigProviderScoped: (provider: ConfigProvider) => Effect = + fiberRuntime.withConfigProviderScoped + +/** + * Accesses the full context of the effect. + * + * **Details** + * + * This function provides the ability to access the entire context required by + * an effect. The context is a container that holds dependencies or environment + * values needed by an effect to run. By using this function, you can retrieve + * and work with the context directly within an effect. + * + * @since 2.0.0 + * @category Context + */ +export const context: () => Effect, never, R> = core.context + +/** + * Accesses the context and applies a transformation function. + * + * **Details** + * + * This function retrieves the context of the effect and applies a pure + * transformation function to it. The result of the transformation is then + * returned within the effect. + * + * @see {@link contextWithEffect} for a version that allows effectful transformations. + * + * @since 2.0.0 + * @category Context + */ +export const contextWith: (f: (context: Context.Context) => A) => Effect = effect.contextWith + +/** + * Accesses the context and performs an effectful transformation. + * + * **Details** + * + * This function retrieves the context and allows you to transform it + * effectually using another effect. It is useful when the transformation + * involves asynchronous or effectful operations. + * + * @see {@link contextWith} for a version that allows pure transformations. + * + * @since 2.0.0 + * @category Context + */ +export const contextWithEffect: ( + f: (context: Context.Context) => Effect +) => Effect = core.contextWithEffect + +/** + * Provides part of the required context while leaving the rest unchanged. + * + * **Details** + * + * This function allows you to transform the context required by an effect, + * providing part of the context and leaving the rest to be fulfilled later. + * + * **Example** + * + * ```ts + * import { Context, Effect } from "effect" + * + * class Service1 extends Context.Tag("Service1")() {} + * class Service2 extends Context.Tag("Service2")() {} + * + * const program = Effect.gen(function*() { + * const service1 = yield* Service1 + * console.log(service1.port) + * const service2 = yield* Service2 + * console.log(service2.connection) + * return "some result" + * }) + * + * // ┌─── Effect + * // ▼ + * const programWithService1 = Effect.mapInputContext( + * program, + * (ctx: Context.Context) => Context.add(ctx, Service1, { port: 3000 }) + * ) + * + * const runnable = programWithService1.pipe( + * Effect.provideService(Service2, { connection: "localhost" }), + * Effect.provideService(Service1, { port: 3001 }) + * ) + * + * Effect.runPromise(runnable) + * // Output: + * // 3000 + * // localhost + * ``` + * + * @since 2.0.0 + * @category Context + */ +export const mapInputContext: { + (f: (context: Context.Context) => Context.Context): (self: Effect) => Effect + (self: Effect, f: (context: Context.Context) => Context.Context): Effect +} = core.mapInputContext + +/** + * Provides necessary dependencies to an effect, removing its environmental + * requirements. + * + * **Details** + * + * This function allows you to supply the required environment for an effect. + * The environment can be provided in the form of one or more `Layer`s, a + * `Context`, a `Runtime`, or a `ManagedRuntime`. Once the environment is + * provided, the effect can run without requiring external dependencies. + * + * You can compose layers to create a modular and reusable way of setting up the + * environment for effects. For example, layers can be used to configure + * databases, logging services, or any other required dependencies. + * + * **Example** + * + * ```ts + * import { Context, Effect, Layer } from "effect" + * + * class Database extends Context.Tag("Database")< + * Database, + * { readonly query: (sql: string) => Effect.Effect> } + * >() {} + * + * const DatabaseLive = Layer.succeed( + * Database, + * { + * // Simulate a database query + * query: (sql: string) => Effect.log(`Executing query: ${sql}`).pipe(Effect.as([])) + * } + * ) + * + * // ┌─── Effect + * // ▼ + * const program = Effect.gen(function*() { + * const database = yield* Database + * const result = yield* database.query("SELECT * FROM users") + * return result + * }) + * + * // ┌─── Effect + * // ▼ + * const runnable = Effect.provide(program, DatabaseLive) + * + * Effect.runPromise(runnable).then(console.log) + * // Output: + * // timestamp=... level=INFO fiber=#0 message="Executing query: SELECT * FROM users" + * // [] + * ``` + * + * @see {@link provideService} for providing a service to an effect. + * + * @since 2.0.0 + * @category Context + */ +export const provide: { + ]>( + layers: Layers + ): ( + self: Effect + ) => Effect< + A, + E | { [k in keyof Layers]: Layer.Layer.Error }[number], + | { [k in keyof Layers]: Layer.Layer.Context }[number] + | Exclude }[number]> + > + ( + layer: Layer.Layer + ): (self: Effect) => Effect> + (context: Context.Context): (self: Effect) => Effect> + (runtime: Runtime.Runtime): (self: Effect) => Effect> + ( + managedRuntime: ManagedRuntime.ManagedRuntime + ): (self: Effect) => Effect> + ]>( + self: Effect, + layers: Layers + ): Effect< + A, + E | { [k in keyof Layers]: Layer.Layer.Error }[number], + | { [k in keyof Layers]: Layer.Layer.Context }[number] + | Exclude }[number]> + > + ( + self: Effect, + layer: Layer.Layer + ): Effect> + (self: Effect, context: Context.Context): Effect> + (self: Effect, runtime: Runtime.Runtime): Effect> + ( + self: Effect, + runtime: ManagedRuntime.ManagedRuntime + ): Effect> +} = layer.effect_provide + +/** + * Provides an implementation for a service in the context of an effect. + * + * **Details** + * + * This function allows you to supply a specific implementation for a service + * required by an effect. Services are typically defined using `Context.Tag`, + * which acts as a unique identifier for the service. By using this function, + * you link the service to its concrete implementation, enabling the effect to + * execute successfully without additional requirements. + * + * For example, you can use this function to provide a random number generator, + * a logger, or any other service your effect depends on. Once the service is + * provided, all parts of the effect that rely on the service will automatically + * use the implementation you supplied. + * + * **Example** + * + * ```ts + * import { Effect, Context } from "effect" + * + * // Declaring a tag for a service that generates random numbers + * class Random extends Context.Tag("MyRandomService")< + * Random, + * { readonly next: Effect.Effect } + * >() {} + * + * // Using the service + * const program = Effect.gen(function* () { + * const random = yield* Random + * const randomNumber = yield* random.next + * console.log(`random number: ${randomNumber}`) + * }) + * + * // Providing the implementation + * // + * // ┌─── Effect + * // ▼ + * const runnable = Effect.provideService(program, Random, { + * next: Effect.sync(() => Math.random()) + * }) + * + * // Run successfully + * Effect.runPromise(runnable) + * // Example Output: + * // random number: 0.8241872233134417 + * ``` + * + * @see {@link provide} for providing multiple layers to an effect. + * + * @since 2.0.0 + * @category Context + */ +export const provideService: { + (tag: Context.Tag, service: NoInfer): (self: Effect) => Effect> + (self: Effect, tag: Context.Tag, service: NoInfer): Effect> +} = effect.provideService + +/** + * Dynamically provides an implementation for a service using an effect. + * + * **Details** + * + * This function allows you to provide an implementation for a service + * dynamically by using another effect. The provided effect is executed to + * produce the service implementation, which is then made available to the + * consuming effect. This is particularly useful when the service implementation + * itself requires asynchronous or resource-intensive initialization. + * + * For example, you can use this function to lazily initialize a database + * connection or fetch configuration values from an external source before + * making the service available to your effect. + * + * @since 2.0.0 + * @category Context + */ +export const provideServiceEffect: { + ( + tag: Context.Tag, + effect: Effect, E1, R1> + ): (self: Effect) => Effect> + ( + self: Effect, + tag: Context.Tag, + effect: Effect, E1, R1> + ): Effect> +} = effect.provideServiceEffect + +/** + * Creates a function that uses a service from the context to produce a value. + * + * @see {@link serviceFunctionEffect} for a version that returns an effect. + * + * @since 2.0.0 + * @category Context + */ +export const serviceFunction: , Args extends Array, A>( + getService: T, + f: (_: Effect.Success) => (...args: Args) => A +) => (...args: Args) => Effect, Effect.Context> = effect.serviceFunction + +/** + * Creates a function that uses a service from the context to produce an effect. + * + * @see {@link serviceFunction} for a version that returns a value. + * + * @since 2.0.0 + * @category Context + */ +export const serviceFunctionEffect: , Args extends Array, A, E, R>( + getService: T, + f: (_: Effect.Success) => (...args: Args) => Effect +) => (...args: Args) => Effect, R | Effect.Context> = effect.serviceFunctionEffect + +/** + * @since 2.0.0 + * @category Context + */ +export const serviceFunctions: ( + getService: Effect +) => { + [k in keyof S as S[k] extends (...args: Array) => Effect ? k : never]: S[k] extends + (...args: infer Args) => Effect ? (...args: Args) => Effect + : never +} = effect.serviceFunctions as any + +/** + * @since 2.0.0 + * @category Context + */ +export const serviceConstants: ( + getService: Effect +) => { + [k in { [k in keyof S]: k }[keyof S]]: S[k] extends Effect ? Effect + : Effect +} = effect.serviceConstants + +/** + * @since 2.0.0 + * @category Context + */ +export const serviceMembers: ( + getService: Effect +) => { + functions: { + [k in keyof S as S[k] extends (...args: Array) => Effect ? k : never]: S[k] extends + (...args: infer Args) => Effect ? (...args: Args) => Effect + : never + } + constants: { + [k in { [k in keyof S]: k }[keyof S]]: S[k] extends Effect ? Effect + : Effect + } +} = effect.serviceMembers as any + +/** + * Retrieves an optional service from the context as an `Option`. + * + * **Details** + * + * This function retrieves a service from the context and wraps it in an + * `Option`. If the service is available, it returns a `Some` containing the + * service. If the service is not found, it returns a `None`. This approach is + * useful when you want to handle the absence of a service gracefully without + * causing an error. + * + * **When to Use** + * + * Use this function when: + * - You need to access a service that may or may not be present in the context. + * - You want to handle the absence of a service using the `Option` type instead + * of throwing an error. + * + * @see {@link serviceOptional} for a version that throws an error if the service is missing. + * + * @since 2.0.0 + * @category Context + */ +export const serviceOption: (tag: Context.Tag) => Effect> = effect.serviceOption + +/** + * Retrieves a service from the context, throwing an error if it is missing. + * + * **Details** + * + * This function retrieves a required service from the context. If the service + * is available, it returns the service. If the service is missing, it throws a + * `NoSuchElementException`, which can be handled using Effect's error-handling + * mechanisms. This is useful for services that are critical to the execution of + * your effect. + * + * @see {@link serviceOption} for a version that returns an `Option` instead of throwing an error. + * + * @since 2.0.0 + * @category Context + */ +export const serviceOptional: (tag: Context.Tag) => Effect = + effect.serviceOptional + +/** + * Updates a service in the context with a new implementation. + * + * **Details** + * + * This function modifies the existing implementation of a service in the + * context. It retrieves the current service, applies the provided + * transformation function `f`, and replaces the old service with the + * transformed one. + * + * **When to Use** + * + * This is useful for adapting or extending a service's behavior during the + * execution of an effect. + * + * @since 2.0.0 + * @category Context + */ +export const updateService: { + ( + tag: Context.Tag, + f: (service: NoInfer) => NoInfer + ): (self: Effect) => Effect + ( + self: Effect, + tag: Context.Tag, + f: (service: NoInfer) => NoInfer + ): Effect +} = effect.updateService + +/** + * The "do simulation" in Effect allows you to write code in a more declarative style, similar to the "do notation" in other programming languages. It provides a way to define variables and perform operations on them using functions like `bind` and `let`. + * + * Here's how the do simulation works: + * + * 1. Start the do simulation using the `Do` value + * 2. Within the do simulation scope, you can use the `bind` function to define variables and bind them to `Effect` values + * 3. You can accumulate multiple `bind` statements to define multiple variables within the scope + * 4. Inside the do simulation scope, you can also use the `let` function to define variables and bind them to simple values + * + * **Example** + * + * ```ts + * import * as assert from "node:assert" + * import { Effect, pipe } from "effect" + * + * const result = pipe( + * Effect.Do, + * Effect.bind("x", () => Effect.succeed(2)), + * Effect.bind("y", () => Effect.succeed(3)), + * Effect.let("sum", ({ x, y }) => x + y) + * ) + * assert.deepStrictEqual(Effect.runSync(result), { x: 2, y: 3, sum: 5 }) + * ``` + * + * @see {@link bind} + * @see {@link bindTo} + * @see {@link let_ let} + * + * @category Do notation + * @since 2.0.0 + */ +export const Do: Effect<{}> = effect.Do + +/** + * The "do simulation" in Effect allows you to write code in a more declarative style, similar to the "do notation" in other programming languages. It provides a way to define variables and perform operations on them using functions like `bind` and `let`. + * + * Here's how the do simulation works: + * + * 1. Start the do simulation using the `Do` value + * 2. Within the do simulation scope, you can use the `bind` function to define variables and bind them to `Effect` values + * 3. You can accumulate multiple `bind` statements to define multiple variables within the scope + * 4. Inside the do simulation scope, you can also use the `let` function to define variables and bind them to simple values + * + * **Example** + * + * ```ts + * import * as assert from "node:assert" + * import { Effect, pipe } from "effect" + * + * const result = pipe( + * Effect.Do, + * Effect.bind("x", () => Effect.succeed(2)), + * Effect.bind("y", () => Effect.succeed(3)), + * Effect.let("sum", ({ x, y }) => x + y) + * ) + * assert.deepStrictEqual(Effect.runSync(result), { x: 2, y: 3, sum: 5 }) + * ``` + * + * @see {@link Do} + * @see {@link bindTo} + * @see {@link let_ let} + * + * @category Do notation + * @since 2.0.0 + */ +export const bind: { + ( + name: Exclude, + f: (a: NoInfer) => Effect + ): (self: Effect) => Effect<{ [K in N | keyof A]: K extends keyof A ? A[K] : B }, E2 | E1, R2 | R1> + ( + self: Effect, + name: Exclude, + f: (a: NoInfer) => Effect + ): Effect<{ [K in N | keyof A]: K extends keyof A ? A[K] : B }, E1 | E2, R1 | R2> +} = effect.bind + +/** + * `bindAll` combines `all` with `bind`. It is useful + * when you want to concurrently run multiple effects and then combine their + * results in a Do notation pipeline. + * + * **Example** + * + * ```ts + * import * as assert from "node:assert" + * import { Effect, Either, pipe } from "effect" + * + * const result = pipe( + * Effect.Do, + * Effect.bind("x", () => Effect.succeed(2)), + * Effect.bindAll(({ x }) => ({ + * a: Effect.succeed(x), + * b: Effect.fail("oops"), + * }), { concurrency: 2, mode: "either" }) + * ) + * assert.deepStrictEqual(Effect.runSync(result), { x: 2, a: Either.right(2), b: Either.left("oops") }) + * ``` + * + * @category Do notation + * @since 3.7.0 + */ +export const bindAll: { + < + A extends object, + X extends Record>, + O extends NoExcessProperties<{ + readonly concurrency?: Concurrency | undefined + readonly batching?: boolean | "inherit" | undefined + readonly mode?: "default" | "validate" | "either" | undefined + readonly concurrentFinalizers?: boolean | undefined + }, O> + >( + f: (a: NoInfer) => [Extract] extends [never] ? X : `Duplicate keys`, + options?: undefined | O + ): ( + self: Effect + ) => [All.ReturnObject>] extends [Effect] + ? Effect< + { [K in keyof A | keyof Success]: K extends keyof A ? A[K] : K extends keyof Success ? Success[K] : never }, + E1 | Error, + R1 | Context + > + : never + < + A extends object, + X extends Record>, + O extends NoExcessProperties<{ + readonly concurrency?: Concurrency | undefined + readonly batching?: boolean | "inherit" | undefined + readonly mode?: "default" | "validate" | "either" | undefined + readonly concurrentFinalizers?: boolean | undefined + }, O>, + E1, + R1 + >( + self: Effect, + f: (a: NoInfer) => [Extract] extends [never] ? X : `Duplicate keys`, + options?: undefined | { + readonly concurrency?: Concurrency | undefined + readonly batching?: boolean | "inherit" | undefined + readonly mode?: "default" | "validate" | "either" | undefined + readonly concurrentFinalizers?: boolean | undefined + } + ): [All.ReturnObject>] extends [Effect] + ? Effect< + { [K in keyof A | keyof Success]: K extends keyof A ? A[K] : K extends keyof Success ? Success[K] : never }, + E1 | Error, + R1 | Context + > + : never +} = circular.bindAll + +/** + * The "do simulation" in Effect allows you to write code in a more declarative style, similar to the "do notation" in other programming languages. It provides a way to define variables and perform operations on them using functions like `bind` and `let`. + * + * Here's how the do simulation works: + * + * 1. Start the do simulation using the `Do` value + * 2. Within the do simulation scope, you can use the `bind` function to define variables and bind them to `Effect` values + * 3. You can accumulate multiple `bind` statements to define multiple variables within the scope + * 4. Inside the do simulation scope, you can also use the `let` function to define variables and bind them to simple values + * + * **Example** + * + * ```ts + * import * as assert from "node:assert" + * import { Effect, pipe } from "effect" + * + * const result = pipe( + * Effect.Do, + * Effect.bind("x", () => Effect.succeed(2)), + * Effect.bind("y", () => Effect.succeed(3)), + * Effect.let("sum", ({ x, y }) => x + y) + * ) + * assert.deepStrictEqual(Effect.runSync(result), { x: 2, y: 3, sum: 5 }) + * ``` + * + * @see {@link Do} + * @see {@link bind} + * @see {@link let_ let} + * + * @category Do notation + * @since 2.0.0 + */ +export const bindTo: { + (name: N): (self: Effect) => Effect<{ [K in N]: A }, E, R> + (self: Effect, name: N): Effect<{ [K in N]: A }, E, R> +} = effect.bindTo + +const let_: { + ( + name: Exclude, + f: (a: NoInfer) => B + ): (self: Effect) => Effect<{ [K in N | keyof A]: K extends keyof A ? A[K] : B }, E, R> + ( + self: Effect, + name: Exclude, + f: (a: NoInfer) => B + ): Effect<{ [K in N | keyof A]: K extends keyof A ? A[K] : B }, E, R> +} = effect.let_ + +export { + /** + * The "do simulation" in Effect allows you to write code in a more declarative style, similar to the "do notation" in other programming languages. It provides a way to define variables and perform operations on them using functions like `bind` and `let`. + * + * Here's how the do simulation works: + * + * 1. Start the do simulation using the `Do` value + * 2. Within the do simulation scope, you can use the `bind` function to define variables and bind them to `Effect` values + * 3. You can accumulate multiple `bind` statements to define multiple variables within the scope + * 4. Inside the do simulation scope, you can also use the `let` function to define variables and bind them to simple values + * + * **Example** + * + * ```ts + * import * as assert from "node:assert" + * import { Effect, pipe } from "effect" + * + * const result = pipe( + * Effect.Do, + * Effect.bind("x", () => Effect.succeed(2)), + * Effect.bind("y", () => Effect.succeed(3)), + * Effect.let("sum", ({ x, y }) => x + y) + * ) + * assert.deepStrictEqual(Effect.runSync(result), { x: 2, y: 3, sum: 5 }) + * + * ``` + * + * @see {@link Do} + * @see {@link bind} + * @see {@link bindTo} + * + * @category Do notation + * @since 2.0.0 + */ + let_ as let +} + +/** + * Encapsulates the result of an effect in an `Option`. + * + * **Details** + * + * This function wraps the outcome of an effect in an `Option` type. If the + * original effect succeeds, the success value is wrapped in `Option.some`. If + * the effect fails, the failure is converted to `Option.none`. + * + * This is particularly useful for scenarios where you want to represent the + * absence of a value explicitly, without causing the resulting effect to fail. + * The resulting effect has an error type of `never`, meaning it cannot fail + * directly. However, unrecoverable errors, also referred to as defects, are + * not captured and will still result in failure. + * + * **Example** (Using Effect.option to Handle Errors) + * + * ```ts + * import { Effect } from "effect" + * + * const maybe1 = Effect.option(Effect.succeed(1)) + * + * Effect.runPromiseExit(maybe1).then(console.log) + * // Output: + * // { + * // _id: 'Exit', + * // _tag: 'Success', + * // value: { _id: 'Option', _tag: 'Some', value: 1 } + * // } + * + * const maybe2 = Effect.option(Effect.fail("Uh oh!")) + * + * Effect.runPromiseExit(maybe2).then(console.log) + * // Output: + * // { + * // _id: 'Exit', + * // _tag: 'Success', + * // value: { _id: 'Option', _tag: 'None' } + * // } + * + * const maybe3 = Effect.option(Effect.die("Boom!")) + * + * Effect.runPromiseExit(maybe3).then(console.log) + * // Output: + * // { + * // _id: 'Exit', + * // _tag: 'Failure', + * // cause: { _id: 'Cause', _tag: 'Die', defect: 'Boom!' } + * // } + * ``` + * + * @see {@link either} for a version that uses `Either` instead. + * @see {@link exit} for a version that encapsulates both recoverable errors and defects in an `Exit`. + * + * @since 2.0.0 + * @category Outcome Encapsulation + */ +export const option: (self: Effect) => Effect, never, R> = effect.option + +/** + * Encapsulates both success and failure of an `Effect` into an `Either` type. + * + * **Details** + * + * This function converts an effect that may fail into an effect that always + * succeeds, wrapping the outcome in an `Either` type. The result will be + * `Either.Left` if the effect fails, containing the recoverable error, or + * `Either.Right` if it succeeds, containing the result. + * + * Using this function, you can handle recoverable errors explicitly without + * causing the effect to fail. This is particularly useful in scenarios where + * you want to chain effects and manage both success and failure in the same + * logical flow. + * + * It's important to note that unrecoverable errors, often referred to as + * "defects," are still thrown and not captured within the `Either` type. Only + * failures that are explicitly represented as recoverable errors in the effect + * are encapsulated. + * + * The resulting effect cannot fail directly because all recoverable failures + * are represented inside the `Either` type. + * + * **Example** + * + * ```ts + * import { Effect, Either, Random } from "effect" + * + * class HttpError { + * readonly _tag = "HttpError" + * } + * + * class ValidationError { + * readonly _tag = "ValidationError" + * } + * + * // ┌─── Effect + * // ▼ + * const program = Effect.gen(function* () { + * const n1 = yield* Random.next + * const n2 = yield* Random.next + * if (n1 < 0.5) { + * yield* Effect.fail(new HttpError()) + * } + * if (n2 < 0.5) { + * yield* Effect.fail(new ValidationError()) + * } + * return "some result" + * }) + * + * // ┌─── Effect + * // ▼ + * const recovered = Effect.gen(function* () { + * // ┌─── Either + * // ▼ + * const failureOrSuccess = yield* Effect.either(program) + * return Either.match(failureOrSuccess, { + * onLeft: (error) => `Recovering from ${error._tag}`, + * onRight: (value) => value // Do nothing in case of success + * }) + * }) + * ``` + * + * @see {@link option} for a version that uses `Option` instead. + * @see {@link exit} for a version that encapsulates both recoverable errors and defects in an `Exit`. + * + * @since 2.0.0 + * @category Outcome Encapsulation + */ +export const either: (self: Effect) => Effect, never, R> = core.either + +/** + * Encapsulates both success and failure of an `Effect` using the `Exit` type. + * + * **Details** + * + * This function converts an effect into one that always succeeds, wrapping its + * outcome in the `Exit` type. The `Exit` type provides explicit handling of + * both success (`Exit.Success`) and failure (`Exit.Failure`) cases, including + * defects (unrecoverable errors). + * + * Unlike {@link either} or {@link option}, this function also encapsulates + * defects, which are typically unrecoverable and would otherwise terminate the + * effect. With the `Exit` type, defects are represented in `Exit.Failure`, + * allowing for detailed introspection and structured error handling. + * + * This makes the resulting effect robust and incapable of direct failure (its + * error type is `never`). It is particularly useful for workflows where all + * outcomes, including unexpected defects, must be managed and analyzed. + * + * **Example** + * + * ```ts + * import { Effect, Cause, Console, Exit } from "effect" + * + * // Simulating a runtime error + * const task = Effect.dieMessage("Boom!") + * + * const program = Effect.gen(function* () { + * const exit = yield* Effect.exit(task) + * if (Exit.isFailure(exit)) { + * const cause = exit.cause + * if ( + * Cause.isDieType(cause) && + * Cause.isRuntimeException(cause.defect) + * ) { + * yield* Console.log( + * `RuntimeException defect caught: ${cause.defect.message}` + * ) + * } else { + * yield* Console.log("Unknown failure caught.") + * } + * } + * }) + * + * // We get an Exit.Success because we caught all failures + * Effect.runPromiseExit(program).then(console.log) + * // Output: + * // RuntimeException defect caught: Boom! + * // { + * // _id: "Exit", + * // _tag: "Success", + * // value: undefined + * // } + * ``` + * + * @see {@link option} for a version that uses `Option` instead. + * @see {@link either} for a version that uses `Either` instead. + * + * @since 2.0.0 + * @category Outcome Encapsulation + */ +export const exit: (self: Effect) => Effect, never, R> = core.exit + +/** + * Converts an `Effect` into an operation that completes a `Deferred` with its result. + * + * **Details** + * + * The `intoDeferred` function takes an effect and a `Deferred` and ensures that the `Deferred` + * is completed based on the outcome of the effect. If the effect succeeds, the `Deferred` is + * completed with the success value. If the effect fails, the `Deferred` is completed with the + * failure. Additionally, if the effect is interrupted, the `Deferred` will also be interrupted. + * + * **Example** + * + * ```ts + * import { Deferred, Effect } from "effect" + * + * // Define an effect that succeeds + * const successEffect = Effect.succeed(42) + * + * const program = Effect.gen(function*() { + * // Create a deferred + * const deferred = yield* Deferred.make() + * + * // Complete the deferred using the successEffect + * const isCompleted = yield* Effect.intoDeferred(successEffect, deferred) + * + * // Access the value of the deferred + * const value = yield* Deferred.await(deferred) + * console.log(value) + * + * return isCompleted + * }) + * + * Effect.runPromise(program).then(console.log) + * // Output: + * // 42 + * // true + * ``` + * + * @since 2.0.0 + * @category Synchronization Utilities + */ +export const intoDeferred: { + (deferred: Deferred.Deferred): (self: Effect) => Effect + (self: Effect, deferred: Deferred.Deferred): Effect +} = core.intoDeferred + +const if_: { + ( + options: { readonly onTrue: LazyArg>; readonly onFalse: LazyArg> } + ): (self: boolean | Effect) => Effect + ( + self: boolean | Effect, + options: { readonly onTrue: LazyArg>; readonly onFalse: LazyArg> } + ): Effect +} = core.if_ + +export { + /** + * Executes one of two effects based on a condition evaluated by an effectful predicate. + * + * Use `if` to run one of two effects depending on whether the predicate effect + * evaluates to `true` or `false`. If the predicate is `true`, the `onTrue` effect + * is executed. If it is `false`, the `onFalse` effect is executed instead. + * + * **Example** (Simulating a Coin Flip) + * + * ```ts + * import { Effect, Random, Console } from "effect" + * + * const flipTheCoin = Effect.if(Random.nextBoolean, { + * onTrue: () => Console.log("Head"), // Runs if the predicate is true + * onFalse: () => Console.log("Tail") // Runs if the predicate is false + * }) + * + * Effect.runFork(flipTheCoin) + * ``` + * + * @since 2.0.0 + * @category Conditional Operators + */ + if_ as if +} + +/** + * Filters an effect, dying with a custom defect if the predicate fails. + * + * **Details** + * + * This function applies a predicate to the result of an effect. If the + * predicate evaluates to `false`, the effect dies with a custom defect + * generated by the `orDieWith` function. + * + * **When to Use** + * + * This is useful for enforcing constraints on values and treating violations as + * fatal program errors. + * + * @since 2.0.0 + * @category Filtering + */ +export const filterOrDie: { + ( + refinement: Refinement, B>, + orDieWith: (a: EqualsWith>) => unknown + ): (self: Effect) => Effect + ( + predicate: Predicate>, + orDieWith: (a: NoInfer) => unknown + ): (self: Effect) => Effect + ( + self: Effect, + refinement: Refinement, + orDieWith: (a: EqualsWith>) => unknown + ): Effect + (self: Effect, predicate: Predicate, orDieWith: (a: A) => unknown): Effect +} = effect.filterOrDie + +/** + * Filters an effect, dying with a custom message if the predicate fails. + * + * **Details** + * + * This function works like {@link filterOrDie} but allows you to specify a + * custom error message to describe the reason for the failure. The message is + * included in the defect when the predicate evaluates to `false`. + * + * @since 2.0.0 + * @category Filtering + */ +export const filterOrDieMessage: { + ( + refinement: Refinement, B>, + message: string + ): (self: Effect) => Effect + (predicate: Predicate>, message: string): (self: Effect) => Effect + (self: Effect, refinement: Refinement, message: string): Effect + (self: Effect, predicate: Predicate, message: string): Effect +} = effect.filterOrDieMessage + +/** + * Filters an effect, providing an alternative effect if the predicate fails. + * + * **Details** + * + * This function applies a predicate to the result of an effect. If the + * predicate evaluates to `false`, it executes the `orElse` effect instead. The + * `orElse` effect can produce an alternative value or perform additional + * computations. + * + * @since 2.0.0 + * @category Filtering + */ +export const filterOrElse: { + ( + refinement: Refinement, B>, + orElse: (a: EqualsWith, Exclude, B>>) => Effect + ): (self: Effect) => Effect + ( + predicate: Predicate>, + orElse: (a: NoInfer) => Effect + ): (self: Effect) => Effect + ( + self: Effect, + refinement: Refinement, + orElse: (a: EqualsWith>) => Effect + ): Effect + ( + self: Effect, + predicate: Predicate, + orElse: (a: A) => Effect + ): Effect +} = effect.filterOrElse + +/** + * Filters an effect, failing with a custom error if the predicate fails. + * + * **Details** + * + * This function applies a predicate to the result of an effect. If the + * predicate evaluates to `false`, the effect fails with a custom error + * generated by the `orFailWith` function. + * + * **When to Use** + * + * This is useful for enforcing constraints and treating violations as + * recoverable errors. + * + * **Providing a Guard** + * + * In addition to the filtering capabilities discussed earlier, you have the + * option to further refine and narrow down the type of the success channel by + * providing a [user-defined type + * guard](https://www.typescriptlang.org/docs/handbook/2/narrowing.html#using-type-predicates). + * Let's explore this concept through an example: + * + * **Example** + * + * ```ts + * import { Effect, pipe } from "effect" + * + * // Define a user interface + * interface User { + * readonly name: string + * } + * + * // Simulate an asynchronous authentication function + * declare const auth: () => Promise + * + * const program = pipe( + * Effect.promise(() => auth()), + * // Use filterOrFail with a custom type guard to ensure user is not null + * Effect.filterOrFail( + * (user): user is User => user !== null, // Type guard + * () => new Error("Unauthorized") + * ), + * // 'user' now has the type `User` (not `User | null`) + * Effect.andThen((user) => user.name) + * ) + * ``` + * + * @since 2.0.0 + * @category Filtering + */ +export const filterOrFail: { + ( + refinement: Refinement, B>, + orFailWith: (a: EqualsWith, Exclude, B>>) => E2 + ): (self: Effect) => Effect, E2 | E, R> + ( + predicate: Predicate>, + orFailWith: (a: NoInfer) => E2 + ): (self: Effect) => Effect + ( + self: Effect, + refinement: Refinement, + orFailWith: (a: EqualsWith>) => E2 + ): Effect, E2 | E, R> + (self: Effect, predicate: Predicate, orFailWith: (a: A) => E2): Effect + ( + refinement: Refinement, B> + ): (self: Effect) => Effect, Cause.NoSuchElementException | E, R> + (predicate: Predicate>): (self: Effect) => Effect + ( + self: Effect, + refinement: Refinement + ): Effect, E | Cause.NoSuchElementException, R> + (self: Effect, predicate: Predicate): Effect +} = effect.filterOrFail + +/** + * Filters an effect with an effectful predicate, falling back to an alternative + * effect if the predicate fails. + * + * **Details** + * + * This function applies a predicate to the result of an effect. If the + * predicate evaluates to `false`, the effect falls back to the `orElse` + * effect. The `orElse` effect can produce an alternative value or perform + * additional computations. + * + * **Example** + * + * ```ts + * import { Effect, pipe } from "effect" + * + * // Define a user interface + * interface User { + * readonly name: string + * } + * + * // Simulate an asynchronous authentication function + * declare const auth: () => Promise + * + * const program = pipe( + * Effect.promise(() => auth()), + * // Use filterEffectOrElse with an effectful predicate + * Effect.filterEffectOrElse({ + * predicate: (user) => Effect.succeed(user !== null), + * orElse: (user) => Effect.fail(new Error(`Unauthorized user: ${user}`)) + * }), + * ) + * ``` + * + * @since 3.13.0 + * @category Filtering + */ +export const filterEffectOrElse: { + ( + options: { + readonly predicate: (a: NoInfer) => Effect + readonly orElse: (a: NoInfer) => Effect + } + ): (self: Effect) => Effect + ( + self: Effect, + options: { + readonly predicate: (a: A) => Effect + readonly orElse: (a: A) => Effect + } + ): Effect +} = core.filterEffectOrElse + +/** + * Filters an effect with an effectful predicate, failing with a custom error if the predicate fails. + * + * **Details** + * + * This function applies a predicate to the result of an effect. If the + * predicate evaluates to `false`, the effect fails with a custom error + * generated by the `orFailWith` function. + * + * **When to Use** + * + * This is useful for enforcing constraints and treating violations as + * recoverable errors. + * + * **Example** + * + * ```ts + * import { Effect, pipe } from "effect" + * + * // Define a user interface + * interface User { + * readonly name: string + * } + * + * // Simulate an asynchronous authentication function + * declare const auth: () => Promise + * + * const program = pipe( + * Effect.promise(() => auth()), + * // Use filterEffectOrFail with an effectful predicate + * Effect.filterEffectOrFail({ + * predicate: (user) => Effect.succeed(user !== null), + * orFailWith: () => new Error("Unauthorized") + * }), + * ) + * ``` + * + * @since 3.13.0 + * @category Filtering + */ +export const filterEffectOrFail: { + ( + options: { + readonly predicate: (a: NoInfer) => Effect + readonly orFailWith: (a: NoInfer) => E3 + } + ): (self: Effect) => Effect + ( + self: Effect, + options: { + readonly predicate: (a: A) => Effect + readonly orFailWith: (a: A) => E3 + } + ): Effect +} = core.filterEffectOrFail + +/** + * Executes an effect only if the condition is `false`. + * + * @see {@link unlessEffect} for a version that allows the condition to be an effect. + * @see {@link when} for a version that executes the effect when the condition is `true`. + * + * @since 2.0.0 + * @category Conditional Operators + */ +export const unless: { + (condition: LazyArg): (self: Effect) => Effect, E, R> + (self: Effect, condition: LazyArg): Effect, E, R> +} = effect.unless + +/** + * Conditionally execute an effect based on the result of another effect. + * + * @see {@link unless} for a version that allows the condition to be a boolean. + * @see {@link whenEffect} for a version that executes the effect when the condition is `true`. + * + * @since 2.0.0 + * @category Conditional Operators + */ +export const unlessEffect: { + ( + condition: Effect + ): (self: Effect) => Effect, E2 | E, R2 | R> + (self: Effect, condition: Effect): Effect, E | E2, R | R2> +} = effect.unlessEffect + +/** + * Conditionally executes an effect based on a boolean condition. + * + * **Details** + * + * This function allows you to run an effect only if a given condition evaluates + * to `true`. If the condition is `true`, the effect is executed, and its result + * is wrapped in an `Option.some`. If the condition is `false`, the effect is + * skipped, and the result is `Option.none`. + * + * **When to Use** + * + * This function is useful for scenarios where you need to dynamically decide + * whether to execute an effect based on runtime logic, while also representing + * the skipped case explicitly. + * + * **Example** (Conditional Effect Execution) + * + * ```ts + * import { Effect, Option } from "effect" + * + * const validateWeightOption = ( + * weight: number + * ): Effect.Effect> => + * // Conditionally execute the effect if the weight is non-negative + * Effect.succeed(weight).pipe(Effect.when(() => weight >= 0)) + * + * // Run with a valid weight + * Effect.runPromise(validateWeightOption(100)).then(console.log) + * // Output: + * // { + * // _id: "Option", + * // _tag: "Some", + * // value: 100 + * // } + * + * // Run with an invalid weight + * Effect.runPromise(validateWeightOption(-5)).then(console.log) + * // Output: + * // { + * // _id: "Option", + * // _tag: "None" + * // } + * ``` + * + * @see {@link whenEffect} for a version that allows the condition to be an effect. + * @see {@link unless} for a version that executes the effect when the condition is `false`. + * + * @since 2.0.0 + * @category Conditional Operators + */ +export const when: { + (condition: LazyArg): (self: Effect) => Effect, E, R> + (self: Effect, condition: LazyArg): Effect, E, R> +} = effect.when + +/** + * Conditionally executes an effect based on the result of another effect. + * + * **Details** + * + * This function allows you to run an effect only if a conditional effect + * evaluating to a boolean resolves to `true`. If the conditional effect + * evaluates to `true`, the specified effect is executed, and its result is + * wrapped in `Option.some`. If the conditional effect evaluates to `false`, the + * effect is skipped, and the result is `Option.none`. + * + * **When to Use** + * + * This function is particularly useful when the decision to execute an effect + * depends on the result of another effect, such as a random value, a + * user-provided input, or a network request result. + * + * **Example** (Using an Effect as a Condition) + * + * ```ts + * import { Effect, Random } from "effect" + * + * const randomIntOption = Random.nextInt.pipe( + * Effect.whenEffect(Random.nextBoolean) + * ) + * + * console.log(Effect.runSync(randomIntOption)) + * // Example Output: + * // { _id: 'Option', _tag: 'Some', value: 8609104974198840 } + * ``` + * + * @see {@link when} for a version that allows the condition to be a boolean. + * @see {@link unlessEffect} for a version that executes the effect when the condition is `false`. + * + * @since 2.0.0 + * @category Conditional Operators + */ +export const whenEffect: { + ( + condition: Effect + ): (effect: Effect) => Effect, E | E2, R | R2> + (self: Effect, condition: Effect): Effect, E2 | E, R2 | R> +} = core.whenEffect + +/** + * Executes an effect conditionally based on the value of a `FiberRef` that + * satisfies a predicate. + * + * **Details** + * + * This function enables you to execute an effect only when the value of a + * specified `FiberRef` meets a certain condition defined by a predicate. If the + * value satisfies the predicate, the effect is executed, and the result is + * wrapped in an `Option.some`. If the predicate is not satisfied, the effect is + * skipped, and the result is `Option.none`. In both cases, the current value of + * the `FiberRef` is included in the result. + * + * @since 2.0.0 + * @category Conditional Operators + */ +export const whenFiberRef: { + ( + fiberRef: FiberRef.FiberRef, + predicate: Predicate + ): (self: Effect) => Effect<[S, Option.Option], E, R> + ( + self: Effect, + fiberRef: FiberRef.FiberRef, + predicate: Predicate + ): Effect<[S, Option.Option], E, R> +} = effect.whenFiberRef + +/** + * Executes an effect conditionally based on the value of a `Ref` that satisfies + * a predicate. + * + * **Details** + * + * This function allows you to execute an effect only when the value of a + * specified `Ref` meets a condition defined by a predicate. If the value + * satisfies the predicate, the effect is executed, and the result is wrapped in + * an `Option.some`. If the predicate is not satisfied, the effect is skipped, + * and the result is `Option.none`. In both cases, the current value of the + * `Ref` is included in the result. + * + * @since 2.0.0 + * @category Conditional Operators + */ +export const whenRef: { + (ref: Ref.Ref, predicate: Predicate): (self: Effect) => Effect<[S, Option.Option], E, R> + (self: Effect, ref: Ref.Ref, predicate: Predicate): Effect<[S, Option.Option], E, R> +} = effect.whenRef + +/** + * Chains effects to produce new `Effect` instances, useful for combining + * operations that depend on previous results. + * + * **Syntax** + * + * ```ts skip-type-checking + * const flatMappedEffect = pipe(myEffect, Effect.flatMap(transformation)) + * // or + * const flatMappedEffect = Effect.flatMap(myEffect, transformation) + * // or + * const flatMappedEffect = myEffect.pipe(Effect.flatMap(transformation)) + * ``` + * + * **Details** + * + * `flatMap` lets you sequence effects so that the result of one effect can be + * used in the next step. It is similar to `flatMap` used with arrays but works + * specifically with `Effect` instances, allowing you to avoid deeply nested + * effect structures. + * + * Since effects are immutable, `flatMap` always returns a new effect instead of + * changing the original one. + * + * **When to Use** + * + * Use `flatMap` when you need to chain multiple effects, ensuring that each + * step produces a new `Effect` while flattening any nested effects that may + * occur. + * + * **Example** + * + * ```ts + * import { pipe, Effect } from "effect" + * + * // Function to apply a discount safely to a transaction amount + * const applyDiscount = ( + * total: number, + * discountRate: number + * ): Effect.Effect => + * discountRate === 0 + * ? Effect.fail(new Error("Discount rate cannot be zero")) + * : Effect.succeed(total - (total * discountRate) / 100) + * + * // Simulated asynchronous task to fetch a transaction amount from database + * const fetchTransactionAmount = Effect.promise(() => Promise.resolve(100)) + * + * // Chaining the fetch and discount application using `flatMap` + * const finalAmount = pipe( + * fetchTransactionAmount, + * Effect.flatMap((amount) => applyDiscount(amount, 5)) + * ) + * + * Effect.runPromise(finalAmount).then(console.log) + * // Output: 95 + * ``` + * + * @see {@link tap} for a version that ignores the result of the effect. + * + * @since 2.0.0 + * @category Sequencing + */ +export const flatMap: { + (f: (a: A) => Effect): (self: Effect) => Effect + (self: Effect, f: (a: A) => Effect): Effect +} = core.flatMap + +/** + * Chains two actions, where the second action can depend on the result of the + * first. + * + * **Syntax** + * + * ```ts skip-type-checking + * const transformedEffect = pipe(myEffect, Effect.andThen(anotherEffect)) + * // or + * const transformedEffect = Effect.andThen(myEffect, anotherEffect) + * // or + * const transformedEffect = myEffect.pipe(Effect.andThen(anotherEffect)) + * ``` + * + * **When to Use** + * + * Use `andThen` when you need to run multiple actions in sequence, with the + * second action depending on the result of the first. This is useful for + * combining effects or handling computations that must happen in order. + * + * **Details** + * + * The second action can be: + * + * - A constant value (similar to {@link as}) + * - A function returning a value (similar to {@link map}) + * - A `Promise` + * - A function returning a `Promise` + * - An `Effect` + * - A function returning an `Effect` (similar to {@link flatMap}) + * + * **Note:** `andThen` works well with both `Option` and `Either` types, + * treating them as effects. + * + * **Example** (Applying a Discount Based on Fetched Amount) + * + * ```ts + * import { pipe, Effect } from "effect" + * + * // Function to apply a discount safely to a transaction amount + * const applyDiscount = ( + * total: number, + * discountRate: number + * ): Effect.Effect => + * discountRate === 0 + * ? Effect.fail(new Error("Discount rate cannot be zero")) + * : Effect.succeed(total - (total * discountRate) / 100) + * + * // Simulated asynchronous task to fetch a transaction amount from database + * const fetchTransactionAmount = Effect.promise(() => Promise.resolve(100)) + * + * // Using Effect.map and Effect.flatMap + * const result1 = pipe( + * fetchTransactionAmount, + * Effect.map((amount) => amount * 2), + * Effect.flatMap((amount) => applyDiscount(amount, 5)) + * ) + * + * Effect.runPromise(result1).then(console.log) + * // Output: 190 + * + * // Using Effect.andThen + * const result2 = pipe( + * fetchTransactionAmount, + * Effect.andThen((amount) => amount * 2), + * Effect.andThen((amount) => applyDiscount(amount, 5)) + * ) + * + * Effect.runPromise(result2).then(console.log) + * // Output: 190 + * ``` + * + * @since 2.0.0 + * @category Sequencing + */ +export const andThen: { + ( + f: (a: NoInfer) => X + ): ( + self: Effect + ) => [X] extends [Effect] ? Effect + : [X] extends [PromiseLike] ? Effect + : Effect + ( + f: NotFunction + ): ( + self: Effect + ) => [X] extends [Effect] ? Effect + : [X] extends [PromiseLike] ? Effect + : Effect + ( + self: Effect, + f: (a: NoInfer) => X + ): [X] extends [Effect] ? Effect + : [X] extends [PromiseLike] ? Effect + : Effect + ( + self: Effect, + f: NotFunction + ): [X] extends [Effect] ? Effect + : [X] extends [PromiseLike] ? Effect + : Effect +} = core.andThen + +/** + * @since 2.0.0 + * @category Sequencing + */ +export const flatten: (self: Effect, E, R>) => Effect = + core.flatten + +/** + * Races two effects and returns the result of the first successful one. + * + * **Details** + * + * This function takes two effects and runs them concurrently. The first effect + * that successfully completes will determine the result of the race, and the + * other effect will be interrupted. + * + * If neither effect succeeds, the function will fail with a `Cause` + * containing all the errors. + * + * **When to Use** + * + * This is useful when you want to run two effects concurrently, but only care + * about the first one to succeed. It is commonly used in cases like timeouts, + * retries, or when you want to optimize for the faster response without + * worrying about the other effect. + * + * **Handling Success or Failure with Either** + * + * If you want to handle the result of whichever task completes first, whether + * it succeeds or fails, you can use the `Effect.either` function. This function + * wraps the result in an `Either` type, allowing you to see if the result + * was a success (`Right`) or a failure (`Left`). + * + * **Example** (Both Tasks Succeed) + * + * ```ts + * import { Effect, Console } from "effect" + * + * const task1 = Effect.succeed("task1").pipe( + * Effect.delay("200 millis"), + * Effect.tap(Console.log("task1 done")), + * Effect.onInterrupt(() => Console.log("task1 interrupted")) + * ) + * const task2 = Effect.succeed("task2").pipe( + * Effect.delay("100 millis"), + * Effect.tap(Console.log("task2 done")), + * Effect.onInterrupt(() => Console.log("task2 interrupted")) + * ) + * + * const program = Effect.race(task1, task2) + * + * Effect.runFork(program) + * // Output: + * // task1 done + * // task2 interrupted + * ``` + * + * **Example** (One Task Fails, One Succeeds) + * + * ```ts + * import { Effect, Console } from "effect" + * + * const task1 = Effect.fail("task1").pipe( + * Effect.delay("100 millis"), + * Effect.tap(Console.log("task1 done")), + * Effect.onInterrupt(() => Console.log("task1 interrupted")) + * ) + * const task2 = Effect.succeed("task2").pipe( + * Effect.delay("200 millis"), + * Effect.tap(Console.log("task2 done")), + * Effect.onInterrupt(() => Console.log("task2 interrupted")) + * ) + * + * const program = Effect.race(task1, task2) + * + * Effect.runFork(program) + * // Output: + * // task2 done + * ``` + * + * **Example** (Both Tasks Fail) + * + * ```ts + * import { Effect, Console } from "effect" + * + * const task1 = Effect.fail("task1").pipe( + * Effect.delay("100 millis"), + * Effect.tap(Console.log("task1 done")), + * Effect.onInterrupt(() => Console.log("task1 interrupted")) + * ) + * const task2 = Effect.fail("task2").pipe( + * Effect.delay("200 millis"), + * Effect.tap(Console.log("task2 done")), + * Effect.onInterrupt(() => Console.log("task2 interrupted")) + * ) + * + * const program = Effect.race(task1, task2) + * + * Effect.runPromiseExit(program).then(console.log) + * // Output: + * // { + * // _id: 'Exit', + * // _tag: 'Failure', + * // cause: { + * // _id: 'Cause', + * // _tag: 'Parallel', + * // left: { _id: 'Cause', _tag: 'Fail', failure: 'task1' }, + * // right: { _id: 'Cause', _tag: 'Fail', failure: 'task2' } + * // } + * // } + * ``` + * + * **Example** (Handling Success or Failure with Either) + * + * ```ts + * import { Effect, Console } from "effect" + * + * const task1 = Effect.fail("task1").pipe( + * Effect.delay("100 millis"), + * Effect.tap(Console.log("task1 done")), + * Effect.onInterrupt(() => Console.log("task1 interrupted")) + * ) + * const task2 = Effect.succeed("task2").pipe( + * Effect.delay("200 millis"), + * Effect.tap(Console.log("task2 done")), + * Effect.onInterrupt(() => Console.log("task2 interrupted")) + * ) + * + * // Run both tasks concurrently, wrapping the result + * // in Either to capture success or failure + * const program = Effect.race(Effect.either(task1), Effect.either(task2)) + * + * Effect.runPromise(program).then(console.log) + * // Output: + * // task2 interrupted + * // { _id: 'Either', _tag: 'Left', left: 'task1' } + * ``` + * + * @see {@link raceAll} for a version that handles multiple effects. + * @see {@link raceFirst} for a version that returns the result of the first effect to complete. + * + * @since 2.0.0 + * @category Racing + */ +export const race: { + (that: Effect): (self: Effect) => Effect + (self: Effect, that: Effect): Effect +} = fiberRuntime.race + +/** + * Races multiple effects and returns the first successful result. + * + * **Details** + * + * This function runs multiple effects concurrently and returns the result of + * the first one to succeed. If one effect succeeds, the others will be + * interrupted. + * + * If none of the effects succeed, the function will fail with the last error + * encountered. + * + * **When to Use** + * + * This is useful when you want to race multiple effects, but only care about + * the first one to succeed. It is commonly used in cases like timeouts, + * retries, or when you want to optimize for the faster response without + * worrying about the other effects. + * + * **Example** (All Tasks Succeed) + * + * ```ts + * import { Effect, Console } from "effect" + * + * const task1 = Effect.succeed("task1").pipe( + * Effect.delay("100 millis"), + * Effect.tap(Console.log("task1 done")), + * Effect.onInterrupt(() => Console.log("task1 interrupted")) + * ) + * const task2 = Effect.succeed("task2").pipe( + * Effect.delay("200 millis"), + * Effect.tap(Console.log("task2 done")), + * Effect.onInterrupt(() => Console.log("task2 interrupted")) + * ) + * + * const task3 = Effect.succeed("task3").pipe( + * Effect.delay("150 millis"), + * Effect.tap(Console.log("task3 done")), + * Effect.onInterrupt(() => Console.log("task3 interrupted")) + * ) + * + * const program = Effect.raceAll([task1, task2, task3]) + * + * Effect.runFork(program) + * // Output: + * // task1 done + * // task2 interrupted + * // task3 interrupted + * ``` + * + * **Example** (One Task Fails, Two Tasks Succeed) + * + * ```ts + * import { Effect, Console } from "effect" + * + * const task1 = Effect.fail("task1").pipe( + * Effect.delay("100 millis"), + * Effect.tap(Console.log("task1 done")), + * Effect.onInterrupt(() => Console.log("task1 interrupted")) + * ) + * const task2 = Effect.succeed("task2").pipe( + * Effect.delay("200 millis"), + * Effect.tap(Console.log("task2 done")), + * Effect.onInterrupt(() => Console.log("task2 interrupted")) + * ) + * + * const task3 = Effect.succeed("task3").pipe( + * Effect.delay("150 millis"), + * Effect.tap(Console.log("task3 done")), + * Effect.onInterrupt(() => Console.log("task3 interrupted")) + * ) + * + * const program = Effect.raceAll([task1, task2, task3]) + * + * Effect.runFork(program) + * // Output: + * // task3 done + * // task2 interrupted + * ``` + * + * **Example** (All Tasks Fail) + * + * ```ts + * import { Effect, Console } from "effect" + * + * const task1 = Effect.fail("task1").pipe( + * Effect.delay("100 millis"), + * Effect.tap(Console.log("task1 done")), + * Effect.onInterrupt(() => Console.log("task1 interrupted")) + * ) + * const task2 = Effect.fail("task2").pipe( + * Effect.delay("200 millis"), + * Effect.tap(Console.log("task2 done")), + * Effect.onInterrupt(() => Console.log("task2 interrupted")) + * ) + * + * const task3 = Effect.fail("task3").pipe( + * Effect.delay("150 millis"), + * Effect.tap(Console.log("task3 done")), + * Effect.onInterrupt(() => Console.log("task3 interrupted")) + * ) + * + * const program = Effect.raceAll([task1, task2, task3]) + * + * Effect.runPromiseExit(program).then(console.log) + * // Output: + * // { + * // _id: 'Exit', + * // _tag: 'Failure', + * // cause: { _id: 'Cause', _tag: 'Fail', failure: 'task2' } + * // } + * ``` + * + * @see {@link race} for a version that handles only two effects. + * + * @since 2.0.0 + * @category Racing + */ +export const raceAll: >( + all: Iterable +) => Effect, Effect.Error, Effect.Context> = fiberRuntime.raceAll + +/** + * Races two effects and returns the result of the first one to complete. + * + * **Details** + * + * This function takes two effects and runs them concurrently, returning the + * result of the first one that completes, regardless of whether it succeeds or + * fails. + * + * **When to Use** + * + * This function is useful when you want to race two operations, and you want to + * proceed with whichever one finishes first, regardless of whether it succeeds + * or fails. + * + * **Disconnecting Effects** + * + * The `Effect.raceFirst` function safely interrupts the “loser” effect once the other completes, but it will not resume until the loser is cleanly terminated. + * + * If you want a quicker return, you can disconnect the interrupt signal for both effects. Instead of calling: + * + * ```ts skip-type-checking + * Effect.raceFirst(task1, task2) + * ``` + * + * You can use: + * + * ```ts skip-type-checking + * Effect.raceFirst(Effect.disconnect(task1), Effect.disconnect(task2)) + * ``` + * + * This allows both effects to complete independently while still terminating the losing effect in the background. + * + * **Example** (Both Tasks Succeed) + * + * ```ts + * import { Effect, Console } from "effect" + * + * const task1 = Effect.succeed("task1").pipe( + * Effect.delay("100 millis"), + * Effect.tap(Console.log("task1 done")), + * Effect.onInterrupt(() => + * Console.log("task1 interrupted").pipe(Effect.delay("100 millis")) + * ) + * ) + * const task2 = Effect.succeed("task2").pipe( + * Effect.delay("200 millis"), + * Effect.tap(Console.log("task2 done")), + * Effect.onInterrupt(() => + * Console.log("task2 interrupted").pipe(Effect.delay("100 millis")) + * ) + * ) + * + * const program = Effect.raceFirst(task1, task2).pipe( + * Effect.tap(Console.log("more work...")) + * ) + * + * Effect.runPromiseExit(program).then(console.log) + * // Output: + * // task1 done + * // task2 interrupted + * // more work... + * // { _id: 'Exit', _tag: 'Success', value: 'task1' } + * ``` + * + * **Example** (One Task Fails, One Succeeds) + * + * ```ts + * import { Effect, Console } from "effect" + * + * const task1 = Effect.fail("task1").pipe( + * Effect.delay("100 millis"), + * Effect.tap(Console.log("task1 done")), + * Effect.onInterrupt(() => + * Console.log("task1 interrupted").pipe(Effect.delay("100 millis")) + * ) + * ) + * const task2 = Effect.succeed("task2").pipe( + * Effect.delay("200 millis"), + * Effect.tap(Console.log("task2 done")), + * Effect.onInterrupt(() => + * Console.log("task2 interrupted").pipe(Effect.delay("100 millis")) + * ) + * ) + * + * const program = Effect.raceFirst(task1, task2).pipe( + * Effect.tap(Console.log("more work...")) + * ) + * + * Effect.runPromiseExit(program).then(console.log) + * // Output: + * // task2 interrupted + * // { + * // _id: 'Exit', + * // _tag: 'Failure', + * // cause: { _id: 'Cause', _tag: 'Fail', failure: 'task1' } + * // } + * ``` + * + * **Example** (Using Effect.disconnect for Quicker Return) + * + * ```ts + * import { Effect, Console } from "effect" + * + * const task1 = Effect.succeed("task1").pipe( + * Effect.delay("100 millis"), + * Effect.tap(Console.log("task1 done")), + * Effect.onInterrupt(() => + * Console.log("task1 interrupted").pipe(Effect.delay("100 millis")) + * ) + * ) + * const task2 = Effect.succeed("task2").pipe( + * Effect.delay("200 millis"), + * Effect.tap(Console.log("task2 done")), + * Effect.onInterrupt(() => + * Console.log("task2 interrupted").pipe(Effect.delay("100 millis")) + * ) + * ) + * + * // Race the two tasks with disconnect to allow quicker return + * const program = Effect.raceFirst( + * Effect.disconnect(task1), + * Effect.disconnect(task2) + * ).pipe(Effect.tap(Console.log("more work..."))) + * + * Effect.runPromiseExit(program).then(console.log) + * // Output: + * // task1 done + * // more work... + * // { _id: 'Exit', _tag: 'Success', value: 'task1' } + * // task2 interrupted + * ``` + * + * @since 2.0.0 + * @category Racing + */ +export const raceFirst: { + (that: Effect): (self: Effect) => Effect + (self: Effect, that: Effect): Effect +} = circular.raceFirst + +/** + * Races two effects and calls a finisher when the first one completes. + * + * **Details** + * + * This function runs two effects concurrently and calls a specified “finisher” + * function once one of the effects completes, regardless of whether it succeeds + * or fails. + * + * The finisher functions for each effect allow you to handle the results of + * each effect as soon as they complete. + * + * The function takes two finisher callbacks, one for each effect, and allows + * you to specify how to handle the result of the race. + * + * **When to Use** + * + * This function is useful when you need to react to the completion of either + * effect without waiting for both to finish. It can be used whenever you want + * to take action based on the first available result. + * + * **Example** (Handling Results of Concurrent Tasks) + * + * ```ts + * import { Effect, Console } from "effect" + * + * const task1 = Effect.succeed("task1").pipe( + * Effect.delay("100 millis"), + * Effect.tap(Console.log("task1 done")), + * Effect.onInterrupt(() => + * Console.log("task1 interrupted").pipe(Effect.delay("100 millis")) + * ) + * ) + * const task2 = Effect.succeed("task2").pipe( + * Effect.delay("200 millis"), + * Effect.tap(Console.log("task2 done")), + * Effect.onInterrupt(() => + * Console.log("task2 interrupted").pipe(Effect.delay("100 millis")) + * ) + * ) + * + * const program = Effect.raceWith(task1, task2, { + * onSelfDone: (exit) => Console.log(`task1 exited with ${exit}`), + * onOtherDone: (exit) => Console.log(`task2 exited with ${exit}`) + * }) + * + * Effect.runFork(program) + * // Output: + * // task1 done + * // task1 exited with { + * // "_id": "Exit", + * // "_tag": "Success", + * // "value": "task1" + * // } + * // task2 interrupted + * ``` + * + * @since 2.0.0 + * @category Racing + */ +export const raceWith: { + ( + other: Effect, + options: { + readonly onSelfDone: (exit: Exit.Exit, fiber: Fiber.Fiber) => Effect + readonly onOtherDone: (exit: Exit.Exit, fiber: Fiber.Fiber) => Effect + } + ): (self: Effect) => Effect + ( + self: Effect, + other: Effect, + options: { + readonly onSelfDone: (exit: Exit.Exit, fiber: Fiber.Fiber) => Effect + readonly onOtherDone: (exit: Exit.Exit, fiber: Fiber.Fiber) => Effect + } + ): Effect +} = fiberRuntime.raceWith + +/** + * Summarizes a effect by computing some value before and after execution, and + * then combining the values to produce a summary, together with the result of + * execution. + * + * @since 2.0.0 + * @category Sequencing + */ +export const summarized: { + ( + summary: Effect, + f: (start: B, end: B) => C + ): (self: Effect) => Effect<[C, A], E2 | E, R2 | R> + ( + self: Effect, + summary: Effect, + f: (start: B, end: B) => C + ): Effect<[C, A], E2 | E, R2 | R> +} = effect.summarized + +/** + * Runs a side effect with the result of an effect without changing the original + * value. + * + * **Details** + * + * This function works similarly to `flatMap`, but it ignores the result of the + * function passed to it. The value from the previous effect remains available + * for the next part of the chain. Note that if the side effect fails, the + * entire chain will fail too. + * + * **When to Use** + * + * Use this function when you want to perform a side effect, like logging or + * tracking, without modifying the main value. This is useful when you need to + * observe or record an action but want the original value to be passed to the + * next step. + * + * **Example** (Logging a step in a pipeline) + * + * ```ts + * import { Console, Effect, pipe } from "effect" + * + * // Function to apply a discount safely to a transaction amount + * const applyDiscount = ( + * total: number, + * discountRate: number + * ): Effect.Effect => + * discountRate === 0 + * ? Effect.fail(new Error("Discount rate cannot be zero")) + * : Effect.succeed(total - (total * discountRate) / 100) + * + * // Simulated asynchronous task to fetch a transaction amount from database + * const fetchTransactionAmount = Effect.promise(() => Promise.resolve(100)) + * + * const finalAmount = pipe( + * fetchTransactionAmount, + * // Log the fetched transaction amount + * Effect.tap((amount) => Console.log(`Apply a discount to: ${amount}`)), + * // `amount` is still available! + * Effect.flatMap((amount) => applyDiscount(amount, 5)) + * ) + * + * Effect.runPromise(finalAmount).then(console.log) + * // Output: + * // Apply a discount to: 100 + * // 95 + * ``` + * + * @see {@link flatMap} for a version that allows you to change the value. + * + * @since 2.0.0 + * @category Sequencing + */ +export const tap: { + ( + f: (a: NoInfer) => X + ): ( + self: Effect + ) => [X] extends [Effect] ? Effect + : [X] extends [PromiseLike] ? Effect + : Effect + ( + f: (a: NoInfer) => Effect, + options: { onlyEffect: true } + ): ( + self: Effect + ) => Effect + ( + f: NotFunction + ): ( + self: Effect + ) => [X] extends [Effect] ? Effect + : [X] extends [PromiseLike] ? Effect + : Effect + ( + f: Effect, + options: { onlyEffect: true } + ): ( + self: Effect + ) => Effect + ( + self: Effect, + f: (a: NoInfer) => X + ): [X] extends [Effect] ? Effect + : [X] extends [PromiseLike] ? Effect + : Effect + ( + self: Effect, + f: (a: NoInfer) => Effect, + options: { onlyEffect: true } + ): Effect + ( + self: Effect, + f: NotFunction + ): [X] extends [Effect] ? Effect + : [X] extends [PromiseLike] ? Effect + : Effect + ( + self: Effect, + f: Effect, + options: { onlyEffect: true } + ): Effect +} = core.tap + +/** + * Allows you to inspect both success and failure outcomes of an effect and + * perform side effects for each. + * + * **Details** + * + * This function enables you to handle both success and failure cases + * separately, without modifying the main effect's result. It is particularly + * useful for scenarios where you need to log, monitor, or perform additional + * actions depending on whether the effect succeeded or failed. + * + * When the effect succeeds, the `onSuccess` handler is executed with the + * success value. When the effect fails, the `onFailure` handler is executed + * with the failure value. Both handlers can include side effects such as + * logging or analytics, and neither modifies the original effect's output. + * + * If either the success or failure handler fails, the overall effect will also + * fail. + * + * **Example** + * + * ```ts + * import { Effect, Random, Console } from "effect" + * + * // Simulate a task that might fail + * const task = Effect.filterOrFail( + * Random.nextRange(-1, 1), + * (n) => n >= 0, + * () => "random number is negative" + * ) + * + * // Use tapBoth to log both success and failure outcomes + * const tapping = Effect.tapBoth(task, { + * onFailure: (error) => Console.log(`failure: ${error}`), + * onSuccess: (randomNumber) => + * Console.log(`random number: ${randomNumber}`) + * }) + * + * Effect.runFork(tapping) + * // Example Output: + * // failure: random number is negative + * ``` + * + * @since 2.0.0 + * @category Sequencing + */ +export const tapBoth: { + ( + options: { + readonly onFailure: (e: NoInfer) => Effect + readonly onSuccess: (a: NoInfer) => Effect + } + ): (self: Effect) => Effect + ( + self: Effect, + options: { + readonly onFailure: (e: E) => Effect + readonly onSuccess: (a: A) => Effect + } + ): Effect +} = effect.tapBoth + +/** + * Inspect severe errors or defects (non-recoverable failures) in an effect. + * + * **Details** + * + * This function is specifically designed to handle and inspect defects, which + * are critical failures in your program, such as unexpected runtime exceptions + * or system-level errors. Unlike normal recoverable errors, defects typically + * indicate serious issues that cannot be addressed through standard error + * handling. + * + * When a defect occurs in an effect, the function you provide to this function + * will be executed, allowing you to log, monitor, or handle the defect in some + * way. Importantly, this does not alter the main result of the effect. If no + * defect occurs, the effect behaves as if this function was not used. + * + * **Example** + * + * ```ts + * import { Effect, Console } from "effect" + * + * // Simulate a task that fails with a recoverable error + * const task1: Effect.Effect = Effect.fail("NetworkError") + * + * // tapDefect won't log anything because NetworkError is not a defect + * const tapping1 = Effect.tapDefect(task1, (cause) => + * Console.log(`defect: ${cause}`) + * ) + * + * Effect.runFork(tapping1) + * // No Output + * + * // Simulate a severe failure in the system + * const task2: Effect.Effect = Effect.dieMessage( + * "Something went wrong" + * ) + * + * // Log the defect using tapDefect + * const tapping2 = Effect.tapDefect(task2, (cause) => + * Console.log(`defect: ${cause}`) + * ) + * + * Effect.runFork(tapping2) + * // Output: + * // defect: RuntimeException: Something went wrong + * // ... stack trace ... + * ``` + * + * @since 2.0.0 + * @category Sequencing + */ +export const tapDefect: { + ( + f: (cause: Cause.Cause) => Effect + ): (self: Effect) => Effect + ( + self: Effect, + f: (cause: Cause.Cause) => Effect + ): Effect +} = effect.tapDefect + +/** + * Execute a side effect on failure without modifying the original effect. + * + * **Details** + * + * This function allows you to inspect and react to the failure of an effect by + * executing an additional effect. The failure value is passed to the provided + * function, enabling you to log it, track it, or perform any other operation. + * Importantly, the original failure remains intact and is re-propagated, so the + * effect's behavior is unchanged. + * + * The side effect you provide is only executed when the effect fails. If the + * effect succeeds, the function is ignored, and the success value is propagated + * as usual. + * + * **Example** + * + * ```ts + * import { Effect, Console } from "effect" + * + * // Simulate a task that fails with an error + * const task: Effect.Effect = Effect.fail("NetworkError") + * + * // Use tapError to log the error message when the task fails + * const tapping = Effect.tapError(task, (error) => + * Console.log(`expected error: ${error}`) + * ) + * + * Effect.runFork(tapping) + * // Output: + * // expected error: NetworkError + * ``` + * + * @since 2.0.0 + * @category Sequencing + */ +export const tapError: { + ( + f: (e: NoInfer) => Effect + ): (self: Effect) => Effect + (self: Effect, f: (e: E) => Effect): Effect +} = effect.tapError + +/** + * Inspect errors matching a specific tag without altering the original effect. + * + * **Details** + * + * This function allows you to inspect and handle specific error types based on + * their `_tag` property. It is particularly useful in applications where errors + * are modeled with tagged types (e.g., union types with discriminating tags). + * By targeting errors with a specific `_tag`, you can log or perform actions on + * them while leaving the error channel and overall effect unchanged. + * + * If the error doesn't match the specified tag, this function does nothing, and + * the effect proceeds as usual. + * + * **Example** + * + * ```ts + * import { Effect, Console } from "effect" + * + * class NetworkError { + * readonly _tag = "NetworkError" + * constructor(readonly statusCode: number) {} + * } + * + * class ValidationError { + * readonly _tag = "ValidationError" + * constructor(readonly field: string) {} + * } + * + * // Create a task that fails with a NetworkError + * const task: Effect.Effect = + * Effect.fail(new NetworkError(504)) + * + * // Use tapErrorTag to inspect only NetworkError types and log the status code + * const tapping = Effect.tapErrorTag(task, "NetworkError", (error) => + * Console.log(`expected error: ${error.statusCode}`) + * ) + * + * Effect.runFork(tapping) + * // Output: + * // expected error: 504 + * ``` + * + * @since 2.0.0 + * @category Sequencing + */ +export const tapErrorTag: { + ( + k: K, + f: (e: NoInfer>) => Effect + ): (self: Effect) => Effect + ( + self: Effect, + k: K, + f: (e: Extract) => Effect + ): Effect +} = effect.tapErrorTag + +/** + * Inspect the complete cause of an error, including failures and defects. + * + * **Details** + * + * This function provides access to the full cause of an error, including both + * recoverable failures and irrecoverable defects. It allows you to handle, log, + * or monitor specific error causes without modifying the result of the effect. + * The full `Cause` object encapsulates the error and its contextual + * information, making it useful for debugging and understanding failure + * scenarios in complex workflows. + * + * The effect itself is not modified, and any errors or defects remain in the + * error channel of the original effect. + * + * **Example** + * + * ```ts + * import { Effect, Console } from "effect" + * + * // Create a task that fails with a NetworkError + * const task1: Effect.Effect = Effect.fail("NetworkError") + * + * const tapping1 = Effect.tapErrorCause(task1, (cause) => + * Console.log(`error cause: ${cause}`) + * ) + * + * Effect.runFork(tapping1) + * // Output: + * // error cause: Error: NetworkError + * + * // Simulate a severe failure in the system + * const task2: Effect.Effect = Effect.dieMessage( + * "Something went wrong" + * ) + * + * const tapping2 = Effect.tapErrorCause(task2, (cause) => + * Console.log(`error cause: ${cause}`) + * ) + * + * Effect.runFork(tapping2) + * // Output: + * // error cause: RuntimeException: Something went wrong + * // ... stack trace ... + * ``` + * + * @since 2.0.0 + * @category Sequencing + */ +export const tapErrorCause: { + ( + f: (cause: Cause.Cause>) => Effect + ): (self: Effect) => Effect + ( + self: Effect, + f: (cause: Cause.Cause) => Effect + ): Effect +} = effect.tapErrorCause + +/** + * Repeats an effect indefinitely until an error occurs. + * + * **Details** + * + * This function executes an effect repeatedly in an infinite loop. Each + * iteration is executed sequentially, and the loop continues until the first + * error occurs. If the effect succeeds, it starts over from the beginning. If + * the effect fails, the error is propagated, and the loop stops. + * + * Be cautious when using this function, as it will run indefinitely unless an + * error interrupts it. This makes it suitable for long-running processes or + * continuous polling tasks, but you should ensure proper error handling or + * combine it with other operators like `timeout` or `schedule` to prevent + * unintentional infinite loops. + * + * @since 2.0.0 + * @category Repetition / Recursion + */ +export const forever: (self: Effect) => Effect = effect.forever + +/** + * Repeatedly updates a state through an effectful operation until a condition + * is no longer met. + * + * **Details** + * + * This function provides a way to implement effectful loops, similar to a + * `while` loop in JavaScript. + * + * ```ts skip-type-checking + * let result = initial + * + * while (options.while(result)) { + * result = options.body(result) + * } + * + * return result + * ``` + * + * It starts with an initial state, checks a + * condition (`while`), and executes a body operation to update the state if the + * condition evaluates to `true`. The process repeats until the condition + * returns `false`. + * + * The state is passed between iterations, allowing the body operation to modify + * it dynamically. The final state after the loop ends is returned as the result + * of the effect. + * + * **When to Use** + * + * This is particularly useful for scenarios where looping logic involves + * asynchronous or side-effectful operations, such as polling or iterative + * computations that depend on external factors. + * + * **Example** (Effectful Iteration) + * + * ```ts + * import { Effect } from "effect" + * + * const result = Effect.iterate( + * // Initial result + * 1, + * { + * // Condition to continue iterating + * while: (result) => result <= 5, + * // Operation to change the result + * body: (result) => Effect.succeed(result + 1) + * } + * ) + * + * Effect.runPromise(result).then(console.log) + * // Output: 6 + * ``` + * + * @since 2.0.0 + * @category Looping + */ +export const iterate: { + ( + initial: A, + options: { + readonly while: Refinement + readonly body: (b: B) => Effect + } + ): Effect + ( + initial: A, + options: { + readonly while: Predicate + readonly body: (a: A) => Effect + } + ): Effect +} = effect.iterate + +/** + * Repeatedly executes a loop with a state, collecting results or discarding + * them based on configuration. + * + * **Details** + * + * This function performs an effectful loop, starting with an initial state and + * iterating as long as the `while` condition evaluates to `true`, similar to a + * `while` loop in JavaScript. + * + * ```ts skip-type-checking + * let state = initial + * const result = [] + * + * while (options.while(state)) { + * result.push(options.body(state)) // Perform the effectful operation + * state = options.step(state) // Update the state + * } + * + * return result + * ``` + * + * During each iteration, the `step` function updates the state, and the `body` + * effect is executed. + * + * The results of the body effect can be collected in an array or discarded + * based on the `discard` option. + * + * **Discarding Intermediate Results** + * + * - If `discard` is `false` or not provided, the intermediate results are + * collected into an array and returned as the final result. + * - If `discard` is `true`, the intermediate results are ignored, and the + * effect returns `void`. + * + * **When to Use** + * + * This is useful for implementing loops where you need to perform effectful + * computations repeatedly, such as processing items in a list, generating + * values, or performing iterative updates. + * + * **Example** (Looping with Collected Results) + * + * ```ts + * import { Effect } from "effect" + * + * // A loop that runs 5 times, collecting each iteration's result + * const result = Effect.loop( + * // Initial state + * 1, + * { + * // Condition to continue looping + * while: (state) => state <= 5, + * // State update function + * step: (state) => state + 1, + * // Effect to be performed on each iteration + * body: (state) => Effect.succeed(state) + * } + * ) + * + * Effect.runPromise(result).then(console.log) + * // Output: [1, 2, 3, 4, 5] + * ``` + * + * **Example** (Loop with Discarded Results) + * + * ```ts + * import { Effect, Console } from "effect" + * + * const result = Effect.loop( + * // Initial state + * 1, + * { + * // Condition to continue looping + * while: (state) => state <= 5, + * // State update function + * step: (state) => state + 1, + * // Effect to be performed on each iteration + * body: (state) => Console.log(`Currently at state ${state}`), + * // Discard intermediate results + * discard: true + * } + * ) + * + * Effect.runPromise(result).then(console.log) + * // Output: + * // Currently at state 1 + * // Currently at state 2 + * // Currently at state 3 + * // Currently at state 4 + * // Currently at state 5 + * // undefined + * ``` + * + * @since 2.0.0 + * @category Looping + */ +export const loop: { + ( + initial: A, + options: { + readonly while: Refinement + readonly step: (b: B) => A + readonly body: (b: B) => Effect + readonly discard?: false | undefined + } + ): Effect, E, R> + ( + initial: A, + options: { + readonly while: (a: A) => boolean + readonly step: (a: A) => A + readonly body: (a: A) => Effect + readonly discard?: false | undefined + } + ): Effect, E, R> + ( + initial: A, + options: { + readonly while: Refinement + readonly step: (b: B) => A + readonly body: (b: B) => Effect + readonly discard: true + } + ): Effect + ( + initial: A, + options: { + readonly while: (a: A) => boolean + readonly step: (a: A) => A + readonly body: (a: A) => Effect + readonly discard: true + } + ): Effect +} = effect.loop + +/** + * @since 2.0.0 + * @category Repetition / Recursion + */ +export declare namespace Repeat { + /** + * @since 2.0.0 + * @category Repetition / Recursion + */ + export type Return, O>> = Effect< + (O extends { schedule: Schedule.Schedule } ? Out + : O extends { until: Refinement } ? B + : A), + | E + | (O extends { while: (...args: Array) => Effect } ? E : never) + | (O extends { until: (...args: Array) => Effect } ? E : never), + | R + | (O extends { schedule: Schedule.Schedule } ? R : never) + | (O extends { while: (...args: Array) => Effect } ? R : never) + | (O extends { until: (...args: Array) => Effect } ? R : never) + > extends infer Z ? Z : never + + /** + * @since 2.0.0 + * @category Repetition / Recursion + */ + export interface Options { + while?: ((_: A) => boolean | Effect) | undefined + until?: ((_: A) => boolean | Effect) | undefined + times?: number | undefined + schedule?: Schedule.Schedule | undefined + } +} + +/** + * Repeats an effect based on a specified schedule or until the first failure. + * + * **Details** + * + * This function executes an effect repeatedly according to the given schedule. + * Each repetition occurs after the initial execution of the effect, meaning + * that the schedule determines the number of additional repetitions. For + * example, using `Schedule.once` will result in the effect being executed twice + * (once initially and once as part of the repetition). + * + * If the effect succeeds, it is repeated according to the schedule. If it + * fails, the repetition stops immediately, and the failure is returned. + * + * The schedule can also specify delays between repetitions, making it useful + * for tasks like retrying operations with backoff, periodic execution, or + * performing a series of dependent actions. + * + * You can combine schedules for more advanced repetition logic, such as adding + * delays, limiting recursions, or dynamically adjusting based on the outcome of + * each execution. + * + * **Example** (Success Example) + * + * ```ts + * import { Effect, Schedule, Console } from "effect" + * + * const action = Console.log("success") + * const policy = Schedule.addDelay(Schedule.recurs(2), () => "100 millis") + * const program = Effect.repeat(action, policy) + * + * Effect.runPromise(program).then((n) => console.log(`repetitions: ${n}`)) + * ``` + * + * **Example** (Failure Example) + * + * ```ts + * import { Effect, Schedule } from "effect" + * + * let count = 0 + * + * // Define an async effect that simulates an action with possible failures + * const action = Effect.async((resume) => { + * if (count > 1) { + * console.log("failure") + * resume(Effect.fail("Uh oh!")) + * } else { + * count++ + * console.log("success") + * resume(Effect.succeed("yay!")) + * } + * }) + * + * const policy = Schedule.addDelay(Schedule.recurs(2), () => "100 millis") + * const program = Effect.repeat(action, policy) + * + * Effect.runPromiseExit(program).then(console.log) + * ``` + * + * @since 2.0.0 + * @category Repetition / Recursion + */ +export const repeat: { + , O>, A>( + options: O + ): ( + self: Effect + ) => Repeat.Return + ( + schedule: Schedule.Schedule + ): (self: Effect) => Effect + , O>>( + self: Effect, + options: O + ): Repeat.Return + (self: Effect, schedule: Schedule.Schedule): Effect +} = schedule_.repeat_combined + +/** + * Repeats an effect a specified number of times or until the first failure. + * + * **Details** + * + * This function executes an effect initially and then repeats it the specified + * number of times, as long as it succeeds. For example, calling + * `repeatN(action, 2)` will execute `action` once initially and then repeat it + * two additional times if there are no failures. + * + * If the effect fails during any repetition, the failure is returned, and no + * further repetitions are attempted. + * + * **When to Use** + * + * This function is useful for tasks that need to be retried a fixed number of + * times or for performing repeated actions without requiring a schedule. + * + * **Example** + * + * ```ts + * import { Effect, Console } from "effect" + * + * const action = Console.log("success") + * const program = Effect.repeatN(action, 2) + * + * Effect.runPromise(program) + * ``` + * + * @since 2.0.0 + * @category Repetition / Recursion + */ +export const repeatN: { + (n: number): (self: Effect) => Effect + (self: Effect, n: number): Effect +} = effect.repeatN + +/** + * Repeats an effect with a schedule, handling failures using a custom handler. + * + * **Details** + * + * This function allows you to execute an effect repeatedly based on a specified + * schedule. If the effect fails at any point, a custom failure handler is + * invoked. The handler is provided with both the failure value and the output + * of the schedule at the time of failure. This enables advanced error recovery + * or alternative fallback logic while maintaining flexibility in how + * repetitions are handled. + * + * For example, using a schedule with `recurs(2)` will allow for two additional + * repetitions after the initial execution, provided the effect succeeds. If a + * failure occurs during any iteration, the failure handler is invoked to handle + * the situation. + * + * **Example** + * + * ```ts + * import { Effect, Schedule } from "effect" + * + * let count = 0 + * + * // Define an async effect that simulates an action with possible failures + * const action = Effect.async((resume) => { + * if (count > 1) { + * console.log("failure") + * resume(Effect.fail("Uh oh!")) + * } else { + * count++ + * console.log("success") + * resume(Effect.succeed("yay!")) + * } + * }) + * + * const policy = Schedule.addDelay( + * Schedule.recurs(2), // Repeat for a maximum of 2 times + * () => "100 millis" // Add a delay of 100 milliseconds between repetitions + * ) + * + * const program = Effect.repeatOrElse(action, policy, () => + * Effect.sync(() => { + * console.log("orElse") + * return count - 1 + * }) + * ) + * + * Effect.runPromise(program).then((n) => console.log(`repetitions: ${n}`)) + * ``` + * + * @since 2.0.0 + * @category Repetition / Recursion + */ +export const repeatOrElse: { + ( + schedule: Schedule.Schedule, + orElse: (error: E, option: Option.Option) => Effect + ): (self: Effect) => Effect + ( + self: Effect, + schedule: Schedule.Schedule, + orElse: (error: E, option: Option.Option) => Effect + ): Effect +} = schedule_.repeatOrElse_Effect + +/** + * Repeats an effect based on a specified schedule. + * + * **Details** + * + * This function allows you to execute an effect repeatedly according to a given + * schedule. The schedule determines the timing and number of repetitions. Each + * repetition can also depend on the decision of the schedule, providing + * flexibility for complex workflows. This function does not modify the effect's + * success or failure; it only controls its repetition. + * + * For example, you can use a schedule that recurs a specific number of times, + * adds delays between repetitions, or customizes repetition behavior based on + * external inputs. The effect runs initially and is repeated according to the + * schedule. + * + * @see {@link scheduleFrom} for a variant that allows the schedule's decision + * to depend on the result of this effect. + * + * @since 2.0.0 + * @category Repetition / Recursion + */ +export const schedule: { + ( + schedule: Schedule.Schedule | undefined, R2> + ): (self: Effect) => Effect + ( + self: Effect, + schedule: Schedule.Schedule + ): Effect +} = schedule_.schedule_Effect + +/** + * Runs an effect repeatedly on a new fiber according to a given schedule. + * + * **Details** + * + * This function starts the provided effect on a new fiber and runs it + * repeatedly based on the specified schedule. The repetitions are managed by + * the schedule's rules, which define the timing and number of iterations. The + * fiber is attached to the current scope, meaning it is automatically managed + * and cleaned up when the scope is closed. + * + * The function returns a `RuntimeFiber` that allows you to monitor or interact + * with the running fiber. + * + * **When to Use** + * + * This is particularly useful for concurrent execution of scheduled tasks or + * when you want to continue processing without waiting for the repetitions to + * complete. + * + * @since 2.0.0 + * @category Repetition / Recursion + */ +export const scheduleForked: { + ( + schedule: Schedule.Schedule + ): (self: Effect) => Effect, never, Scope.Scope | R2 | R> + ( + self: Effect, + schedule: Schedule.Schedule + ): Effect, never, Scope.Scope | R | R2> +} = schedule_.scheduleForked + +/** + * Runs an effect repeatedly according to a schedule, starting from a specified + * input value. + * + * **Details** + * + * This function allows you to repeatedly execute an effect based on a schedule. + * The schedule starts with the given `initial` input value, which is passed to + * the first execution. Subsequent executions of the effect are controlled by + * the schedule's rules, using the output of the previous iteration as the input + * for the next one. + * + * The returned effect will complete when the schedule ends or the effect fails, + * propagating the error. + * + * @since 2.0.0 + * @category Repetition / Recursion + */ +export const scheduleFrom: { + ( + initial: In, + schedule: Schedule.Schedule + ): (self: Effect) => Effect + ( + self: Effect, + initial: In, + schedule: Schedule.Schedule + ): Effect +} = schedule_.scheduleFrom_Effect + +/** + * @since 2.0.0 + * @category Repetition / Recursion + */ +export const whileLoop: ( + options: { + readonly while: LazyArg + readonly body: LazyArg> + readonly step: (a: A) => void + } +) => Effect = core.whileLoop + +/** + * Returns a collection of all `FiberRef` values for the fiber running this + * effect. + * + * @since 2.0.0 + * @category Fiber Refs + */ +export const getFiberRefs: Effect = effect.fiberRefs + +/** + * Inherits values from all `FiberRef` instances into current fiber. + * + * @since 2.0.0 + * @category Fiber Refs + */ +export const inheritFiberRefs: (childFiberRefs: FiberRefs.FiberRefs) => Effect = effect.inheritFiberRefs + +/** + * @since 2.0.0 + * @category Fiber Refs + */ +export const locally: { + (self: FiberRef.FiberRef, value: A): (use: Effect) => Effect + (use: Effect, self: FiberRef.FiberRef, value: A): Effect +} = core.fiberRefLocally + +/** + * @since 2.0.0 + * @category Fiber Refs + */ +export const locallyWith: { + (self: FiberRef.FiberRef, f: (a: A) => A): (use: Effect) => Effect + (use: Effect, self: FiberRef.FiberRef, f: (a: A) => A): Effect +} = core.fiberRefLocallyWith + +/** + * @since 2.0.0 + * @category Fiber Refs + */ +export const locallyScoped: { + (value: A): (self: FiberRef.FiberRef) => Effect + (self: FiberRef.FiberRef, value: A): Effect +} = fiberRuntime.fiberRefLocallyScoped + +/** + * @since 2.0.0 + * @category Fiber Refs + */ +export const locallyScopedWith: { + (f: (a: A) => A): (self: FiberRef.FiberRef) => Effect + (self: FiberRef.FiberRef, f: (a: A) => A): Effect +} = fiberRuntime.fiberRefLocallyScopedWith + +/** + * Applies the specified changes to the `FiberRef` values for the fiber + * running this workflow. + * + * @since 2.0.0 + * @category Fiber Refs + */ +export const patchFiberRefs: (patch: FiberRefsPatch.FiberRefsPatch) => Effect = effect.patchFiberRefs + +/** + * Sets the `FiberRef` values for the fiber running this effect to the values + * in the specified collection of `FiberRef` values. + * + * @since 2.0.0 + * @category Fiber Refs + */ +export const setFiberRefs: (fiberRefs: FiberRefs.FiberRefs) => Effect = effect.setFiberRefs + +/** + * Updates the `FiberRef` values for the fiber running this effect using the + * specified function. + * + * @since 2.0.0 + * @category Fiber Refs + */ +export const updateFiberRefs: ( + f: (fiberId: FiberId.Runtime, fiberRefs: FiberRefs.FiberRefs) => FiberRefs.FiberRefs +) => Effect = effect.updateFiberRefs + +/** + * Checks if an effect has failed. + * + * **Details** + * + * This function evaluates whether an effect has resulted in a failure. It + * returns a boolean value wrapped in an effect, with `true` indicating the + * effect failed and `false` otherwise. + * + * The resulting effect cannot fail (`never` in the error channel) but retains + * the context of the original effect. + * + * **Example** + * + * ```ts + * import { Effect } from "effect" + * + * const failure = Effect.fail("Uh oh!") + * + * console.log(Effect.runSync(Effect.isFailure(failure))) + * // Output: true + * + * const defect = Effect.dieMessage("BOOM!") + * + * Effect.runSync(Effect.isFailure(defect)) + * // throws: BOOM! + * ``` + * + * @since 2.0.0 + * @category Condition Checking + */ +export const isFailure: (self: Effect) => Effect = effect.isFailure + +/** + * Checks if an effect has succeeded. + * + * **Details** + * + * This function evaluates whether an effect has resulted in a success. It + * returns a boolean value wrapped in an effect, with `true` indicating the + * effect succeeded and `false` otherwise. + * + * The resulting effect cannot fail (`never` in the error channel) but retains + * the context of the original effect. + * + * @since 2.0.0 + * @category Condition Checking + */ +export const isSuccess: (self: Effect) => Effect = effect.isSuccess + +/** + * Handles both success and failure cases of an effect without performing side + * effects. + * + * **Details** + * + * `match` lets you define custom handlers for both success and failure + * scenarios. You provide separate functions to handle each case, allowing you + * to process the result if the effect succeeds, or handle the error if the + * effect fails. + * + * **When to Use** + * + * This is useful for structuring your code to respond differently to success or + * failure without triggering side effects. + * + * **Example** (Handling Both Success and Failure Cases) + * + * ```ts + * import { Effect } from "effect" + * + * const success: Effect.Effect = Effect.succeed(42) + * + * const program1 = Effect.match(success, { + * onFailure: (error) => `failure: ${error.message}`, + * onSuccess: (value) => `success: ${value}` + * }) + * + * // Run and log the result of the successful effect + * Effect.runPromise(program1).then(console.log) + * // Output: "success: 42" + * + * const failure: Effect.Effect = Effect.fail( + * new Error("Uh oh!") + * ) + * + * const program2 = Effect.match(failure, { + * onFailure: (error) => `failure: ${error.message}`, + * onSuccess: (value) => `success: ${value}` + * }) + * + * // Run and log the result of the failed effect + * Effect.runPromise(program2).then(console.log) + * // Output: "failure: Uh oh!" + * ``` + * + * @see {@link matchEffect} if you need to perform side effects in the handlers. + * + * @since 2.0.0 + * @category Matching + */ +export const match: { + ( + options: { + readonly onFailure: (error: E) => A2 + readonly onSuccess: (value: A) => A3 + } + ): (self: Effect) => Effect + ( + self: Effect, + options: { + readonly onFailure: (error: E) => A2 + readonly onSuccess: (value: A) => A3 + } + ): Effect +} = effect.match + +/** + * Handles failures by matching the cause of failure. + * + * **Details** + * + * The `matchCause` function allows you to handle failures with access to the + * full cause of the failure within a fiber. + * + * **When to Use** + * + * This is useful for differentiating between different types of errors, such as + * regular failures, defects, or interruptions. You can provide specific + * handling logic for each failure type based on the cause. + * + * **Example** (Handling Different Failure Causes) + * + * ```ts + * import { Effect } from "effect" + * + * const task: Effect.Effect = Effect.die("Uh oh!") + * + * const program = Effect.matchCause(task, { + * onFailure: (cause) => { + * switch (cause._tag) { + * case "Fail": + * // Handle standard failure + * return `Fail: ${cause.error.message}` + * case "Die": + * // Handle defects (unexpected errors) + * return `Die: ${cause.defect}` + * case "Interrupt": + * // Handle interruption + * return `${cause.fiberId} interrupted!` + * } + * // Fallback for other causes + * return "failed due to other causes" + * }, + * onSuccess: (value) => + * // task completes successfully + * `succeeded with ${value} value` + * }) + * + * Effect.runPromise(program).then(console.log) + * // Output: "Die: Uh oh!" + * ``` + * + * @see {@link matchCauseEffect} if you need to perform side effects in the + * handlers. + * @see {@link match} if you don't need to handle the cause of the failure. + * + * @since 2.0.0 + * @category Matching + */ +export const matchCause: { + ( + options: { + readonly onFailure: (cause: Cause.Cause) => A2 + readonly onSuccess: (a: A) => A3 + } + ): (self: Effect) => Effect + ( + self: Effect, + options: { + readonly onFailure: (cause: Cause.Cause) => A2 + readonly onSuccess: (a: A) => A3 + } + ): Effect +} = core.matchCause + +/** + * Handles failures with access to the cause and allows performing side effects. + * + * **Details** + * + * The `matchCauseEffect` function works similarly to {@link matchCause}, but it + * also allows you to perform additional side effects based on the failure + * cause. This function provides access to the complete cause of the failure, + * making it possible to differentiate between various failure types, and allows + * you to respond accordingly while performing side effects (like logging or + * other operations). + * + * **Example** (Handling Different Failure Causes with Side Effects) + * + * ```ts + * import { Effect, Console } from "effect" + * + * const task: Effect.Effect = Effect.die("Uh oh!") + * + * const program = Effect.matchCauseEffect(task, { + * onFailure: (cause) => { + * switch (cause._tag) { + * case "Fail": + * // Handle standard failure with a logged message + * return Console.log(`Fail: ${cause.error.message}`) + * case "Die": + * // Handle defects (unexpected errors) by logging the defect + * return Console.log(`Die: ${cause.defect}`) + * case "Interrupt": + * // Handle interruption and log the fiberId that was interrupted + * return Console.log(`${cause.fiberId} interrupted!`) + * } + * // Fallback for other causes + * return Console.log("failed due to other causes") + * }, + * onSuccess: (value) => + * // Log success if the task completes successfully + * Console.log(`succeeded with ${value} value`) + * }) + * + * Effect.runPromise(program) + * // Output: "Die: Uh oh!" + * ``` + * + * @see {@link matchCause} if you don't need side effects and only want to handle the result or failure. + * @see {@link matchEffect} if you don't need to handle the cause of the failure. + * + * @since 2.0.0 + * @category Matching + */ +export const matchCauseEffect: { + ( + options: { + readonly onFailure: (cause: Cause.Cause) => Effect + readonly onSuccess: (a: A) => Effect + } + ): (self: Effect) => Effect + ( + self: Effect, + options: { + readonly onFailure: (cause: Cause.Cause) => Effect + readonly onSuccess: (a: A) => Effect + } + ): Effect +} = core.matchCauseEffect + +/** + * Handles both success and failure cases of an effect, allowing for additional + * side effects. + * + * **Details** + * + * The `matchEffect` function is similar to {@link match}, but it enables you to + * perform side effects in the handlers for both success and failure outcomes. + * + * **When to Use** + * + * This is useful when you need to execute additional actions, like logging or + * notifying users, based on whether an effect succeeds or fails. + * + * **Example** (Handling Both Success and Failure Cases with Side Effects) + * + * ```ts + * import { Effect } from "effect" + * + * const success: Effect.Effect = Effect.succeed(42) + * const failure: Effect.Effect = Effect.fail( + * new Error("Uh oh!") + * ) + * + * const program1 = Effect.matchEffect(success, { + * onFailure: (error) => + * Effect.succeed(`failure: ${error.message}`).pipe( + * Effect.tap(Effect.log) + * ), + * onSuccess: (value) => + * Effect.succeed(`success: ${value}`).pipe(Effect.tap(Effect.log)) + * }) + * + * console.log(Effect.runSync(program1)) + * // Output: + * // timestamp=... level=INFO fiber=#0 message="success: 42" + * // success: 42 + * + * const program2 = Effect.matchEffect(failure, { + * onFailure: (error) => + * Effect.succeed(`failure: ${error.message}`).pipe( + * Effect.tap(Effect.log) + * ), + * onSuccess: (value) => + * Effect.succeed(`success: ${value}`).pipe(Effect.tap(Effect.log)) + * }) + * + * console.log(Effect.runSync(program2)) + * // Output: + * // timestamp=... level=INFO fiber=#1 message="failure: Uh oh!" + * // failure: Uh oh! + * ``` + * + * @see {@link match} if you don't need side effects and only want to handle the + * result or failure. + * + * @since 2.0.0 + * @category Matching + */ +export const matchEffect: { + ( + options: { + readonly onFailure: (e: E) => Effect + readonly onSuccess: (a: A) => Effect + } + ): (self: Effect) => Effect + ( + self: Effect, + options: { + readonly onFailure: (e: E) => Effect + readonly onSuccess: (a: A) => Effect + } + ): Effect +} = core.matchEffect + +/** + * Logs one or more messages or error causes at the current log level. + * + * **Details** + * + * This function provides a simple way to log messages or error causes during + * the execution of your effects. By default, logs are recorded at the `INFO` + * level, but this can be adjusted using other logging utilities + * (`Logger.withMinimumLogLevel`). Multiple items, including `Cause` instances, + * can be logged in a single call. When logging `Cause` instances, detailed + * error information is included in the log output. + * + * The log output includes useful metadata like the current timestamp, log + * level, and fiber ID, making it suitable for debugging and tracking purposes. + * This function does not interrupt or alter the effect's execution flow. + * + * **Example** + * + * ```ts + * import { Cause, Effect } from "effect" + * + * const program = Effect.log( + * "message1", + * "message2", + * Cause.die("Oh no!"), + * Cause.die("Oh uh!") + * ) + * + * Effect.runFork(program) + * // Output: + * // timestamp=... level=INFO fiber=#0 message=message1 message=message2 cause="Error: Oh no! + * // Error: Oh uh!" + * ``` + * + * @since 2.0.0 + * @category Logging + */ +export const log: (...message: ReadonlyArray) => Effect = effect.log + +/** + * Logs messages or error causes at a specified log level. + * + * **Details** + * + * This function allows you to log one or more messages or error causes while + * specifying the desired log level (e.g., DEBUG, INFO, ERROR). It provides + * flexibility in categorizing logs based on their importance or severity, + * making it easier to filter logs during debugging or production monitoring. + * + * **Example** + * + * ```ts + * import { Cause, Effect, LogLevel } from "effect" + * + * const program = Effect.logWithLevel( + * LogLevel.Error, + * "Critical error encountered", + * Cause.die("System failure!") + * ) + * + * Effect.runFork(program) + * // Output: + * // timestamp=... level=ERROR fiber=#0 message=Critical error encountered cause="Error: System failure!" + * ``` + * + * @since 2.0.0 + * @category Logging + */ +export const logWithLevel = ( + level: LogLevel.LogLevel, + ...message: ReadonlyArray +): Effect => effect.logWithLevel(level)(...message) + +/** + * Logs messages at the TRACE log level. + * + * **Details** + * + * This function logs the specified messages at the TRACE level. TRACE logs are + * typically used for very detailed diagnostic information. These messages are + * not displayed by default. To view them, you must adjust the logging + * configuration by setting the minimum log level to `LogLevel.Trace` using + * `Logger.withMinimumLogLevel`. + * + * **Example** + * + * ```ts + * import { Effect, Logger, LogLevel } from "effect" + * + * const program = Effect.logTrace("message1").pipe(Logger.withMinimumLogLevel(LogLevel.Trace)) + * + * Effect.runFork(program) + * // timestamp=... level=TRACE fiber=#0 message=message1 + * ``` + * + * @since 2.0.0 + * @category Logging + */ +export const logTrace: (...message: ReadonlyArray) => Effect = effect.logTrace + +/** + * Logs messages at the DEBUG log level. + * + * **Details** + * + * This function logs messages at the DEBUG level, which is typically used for + * diagnosing application behavior during development. DEBUG messages provide + * less detailed information than TRACE logs but are still not shown by default. + * To view these logs, adjust the log level using `Logger.withMinimumLogLevel`. + * + * **Example** + * + * ```ts + * import { Effect, Logger, LogLevel } from "effect" + * + * const program = Effect.logDebug("message1").pipe(Logger.withMinimumLogLevel(LogLevel.Debug)) + * + * Effect.runFork(program) + * // timestamp=... level=DEBUG fiber=#0 message=message1 + * ``` + * + * @since 2.0.0 + * @category Logging + */ +export const logDebug: (...message: ReadonlyArray) => Effect = effect.logDebug + +/** + * Logs messages at the INFO log level. + * + * **Details** + * + * This function logs messages at the INFO level, suitable for general + * application events or operational messages. INFO logs are shown by default + * and are commonly used for highlighting normal, non-error operations. + * + * @since 2.0.0 + * @category Logging + */ +export const logInfo: (...message: ReadonlyArray) => Effect = effect.logInfo + +/** + * Logs messages at the WARNING log level. + * + * **Details** + * + * This function logs messages at the WARNING level, suitable for highlighting + * potential issues that are not errors but may require attention. These + * messages indicate that something unexpected occurred or might lead to errors + * in the future. + * + * @since 2.0.0 + * @category Logging + */ +export const logWarning: (...message: ReadonlyArray) => Effect = effect.logWarning + +/** + * Logs messages at the ERROR log level. + * + * **Details** + * + * This function logs messages at the ERROR level, suitable for reporting + * application errors or failures. These logs are typically used for unexpected + * issues that need immediate attention. + * + * @since 2.0.0 + * @category Logging + */ +export const logError: (...message: ReadonlyArray) => Effect = effect.logError + +/** + * Logs messages at the FATAL log level. + * + * **Details** + * + * This function logs messages at the FATAL level, suitable for reporting + * critical errors that cause the application to terminate or stop functioning. + * These logs are typically used for unrecoverable errors that require immediate + * attention. + * + * @since 2.0.0 + * @category Logging + */ +export const logFatal: (...message: ReadonlyArray) => Effect = effect.logFatal + +/** + * Adds a log span to an effect for tracking and logging its execution duration. + * + * **Details** + * + * This function wraps an effect with a log span, providing performance + * monitoring and debugging capabilities. The log span tracks the duration of + * the wrapped effect and logs it with the specified label. This is particularly + * useful when analyzing time-sensitive operations or understanding the + * execution time of specific tasks in your application. + * + * The logged output will include the label and the total time taken for the + * operation. The span information is included in the log metadata, making it + * easy to trace performance metrics in logs. + * + * **Example** + * + * ```ts + * import { Effect } from "effect" + * + * const program = Effect.gen(function*() { + * yield* Effect.sleep("1 second") + * yield* Effect.log("The job is finished!") + * }).pipe(Effect.withLogSpan("myspan")) + * + * Effect.runFork(program) + * // timestamp=... level=INFO fiber=#0 message="The job is finished!" myspan=1011ms + * ``` + * + * @since 2.0.0 + * @category Logging + */ +export const withLogSpan: { + (label: string): (effect: Effect) => Effect + (effect: Effect, label: string): Effect +} = effect.withLogSpan + +/** + * Adds custom annotations to log entries generated within an effect. + * + * **Details** + * + * This function allows you to enhance log messages by appending additional + * context in the form of key-value pairs. These annotations are included in + * every log message created during the execution of the effect, making the logs + * more informative and easier to trace. + * + * The annotations can be specified as a single key-value pair or as a record of + * multiple key-value pairs. This is particularly useful for tracking + * operations, debugging, or associating specific metadata with logs for better + * observability. + * + * The annotated key-value pairs will appear alongside the log message in the + * output. + * + * **Example** + * + * ```ts + * import { Effect } from "effect" + * + * const program = Effect.gen(function*() { + * yield* Effect.log("message1") + * yield* Effect.log("message2") + * }).pipe(Effect.annotateLogs("taskId", "1234")) // Annotation as key/value pair + * + * Effect.runFork(program) + * // timestamp=... level=INFO fiber=#0 message=message1 taskId=1234 + * // timestamp=... level=INFO fiber=#0 message=message2 taskId=1234 + * ``` + * + * @see {@link annotateLogsScoped} to add log annotations with a limited scope. + * + * @since 2.0.0 + * @category Logging + */ +export const annotateLogs: { + (key: string, value: unknown): (effect: Effect) => Effect + (values: Record): (effect: Effect) => Effect + (effect: Effect, key: string, value: unknown): Effect + (effect: Effect, values: Record): Effect +} = effect.annotateLogs + +/** + * Adds log annotations with a limited scope to enhance contextual logging. + * + * **Details** + * + * This function allows you to apply key-value annotations to log entries + * generated within a specific scope of your effect computations. The + * annotations are restricted to the defined `Scope`, ensuring that they are + * only applied to logs produced during that scope. Once the scope ends, the + * annotations are automatically removed, making it easier to manage + * context-specific logging without affecting other parts of your application. + * + * The annotations can be provided as a single key-value pair or as a record of + * multiple key-value pairs. This flexibility enables fine-grained control over + * the additional metadata included in logs for specific tasks or operations. + * + * **Example** + * + * ```ts + * import { Effect } from "effect" + * + * const program = Effect.gen(function*() { + * yield* Effect.log("no annotations") + * yield* Effect.annotateLogsScoped({ key: "value" }) + * yield* Effect.log("message1") // Annotation is applied to this log + * yield* Effect.log("message2") // Annotation is applied to this log + * }).pipe(Effect.scoped, Effect.andThen(Effect.log("no annotations again"))) + * + * Effect.runFork(program) + * // timestamp=... level=INFO fiber=#0 message="no annotations" + * // timestamp=... level=INFO fiber=#0 message=message1 key=value + * // timestamp=... level=INFO fiber=#0 message=message2 key=value + * // timestamp=... level=INFO fiber=#0 message="no annotations again" + * ``` + * + * @see {@link annotateLogs} to add custom annotations to log entries generated within an effect. + * + * @since 3.1.0 + * @category Logging + */ +export const annotateLogsScoped: { + (key: string, value: unknown): Effect + (values: Record): Effect +} = fiberRuntime.annotateLogsScoped + +/** + * Retrieves the current log annotations for the current scope. + * + * **Details** + * + * This function provides access to the log annotations associated with the + * current scope. Log annotations are key-value pairs that provide additional + * context to log entries. They are often used to add metadata such as tags, + * identifiers, or extra debugging information to logs. + * + * By using this function, you can inspect or utilize the annotations applied to + * the current scope, making it easier to trace and debug specific sections of + * your application. + * + * @see {@link annotateLogs} to add custom annotations to log entries generated within an effect. + * @see {@link annotateLogsScoped} to add log annotations with a limited scope. + * + * @since 2.0.0 + * @category Logging + */ +export const logAnnotations: Effect> = effect.logAnnotations + +/** + * Configures whether child fibers will log unhandled errors and at what log + * level. + * + * **Details** + * + * This function allows you to control whether unhandled errors from child + * fibers are logged and to specify the log level for these errors. By default, + * unhandled errors are reported via the logger. However, using this function, + * you can choose to suppress these logs by passing `Option.none` or adjust the + * log level to a specific severity, such as `Error`, `Warning`, or `Info`. + * + * This configuration is scoped to the effect it is applied to, meaning the + * changes only apply to the child fibers created within that effect's context. + * It is especially useful when you want to reduce noise in logs or prioritize + * certain types of errors. + * + * **Example** + * + * ```ts + * import { Effect, Fiber, LogLevel, Option } from "effect" + * + * const program = Effect.gen(function*() { + * const fiber = yield* Effect.fork(Effect.fail("Unhandled error!")) + * yield* Fiber.join(fiber) + * }) + * + * Effect.runFork(program.pipe(Effect.withUnhandledErrorLogLevel(Option.some(LogLevel.Error)))) + * // Output: + * // timestamp=... level=ERROR fiber=#1 message="Fiber terminated with an unhandled error" cause="Error: Unhandled error!" + * ``` + * + * @since 2.0.0 + * @category Logging + */ +export const withUnhandledErrorLogLevel: { + (level: Option.Option): (self: Effect) => Effect + (self: Effect, level: Option.Option): Effect +} = core.withUnhandledErrorLogLevel + +/** + * Conditionally executes an effect based on the specified log level and currently enabled log level. + * + * **Details** + * + * This function runs the provided effect only if the specified log level is + * enabled. If the log level is enabled, the effect is executed and its result + * is wrapped in `Some`. If the log level is not enabled, the effect is not + * executed and `None` is returned. + * + * This function is useful for conditionally executing logging-related effects + * or other operations that depend on the current log level configuration. + * + * **Example** + * + * ```ts + * import { Effect, Logger, LogLevel } from "effect" + * + * const program = Effect.gen(function* () { + * yield* Effect.whenLogLevel(Effect.logTrace("message1"), LogLevel.Trace); // returns `None` + * yield* Effect.whenLogLevel(Effect.logDebug("message2"), LogLevel.Debug); // returns `Some` + * }).pipe(Logger.withMinimumLogLevel(LogLevel.Debug)); + * + * Effect.runFork(program) + * // timestamp=... level=DEBUG fiber=#0 message=message2 + * ``` + * + * @see {@link FiberRef.currentMinimumLogLevel} to retrieve the current minimum log level. + * + * @since 3.13.0 + * @category Logging + */ +export const whenLogLevel: { + (level: LogLevel.LogLevel | LogLevel.Literal): (self: Effect) => Effect, E, R> + (self: Effect, level: LogLevel.LogLevel | LogLevel.Literal): Effect, E, R> +} = fiberRuntime.whenLogLevel + +/** + * Converts an effect's failure into a fiber termination, removing the error + * from the effect's type. + * + * **Details** + * + * The `orDie` function is used when you encounter errors that you do not want + * to handle or recover from. It removes the error type from the effect and + * ensures that any failure will terminate the fiber. This is useful for + * propagating failures as defects, signaling that they should not be handled + * within the effect. + * + * **When to Use* + * + * Use `orDie` when failures should be treated as unrecoverable defects and no + * error handling is required. + * + * **Example** (Propagating an Error as a Defect) + * + * ```ts + * import { Effect } from "effect" + * + * const divide = (a: number, b: number) => + * b === 0 + * ? Effect.fail(new Error("Cannot divide by zero")) + * : Effect.succeed(a / b) + * + * // ┌─── Effect + * // ▼ + * const program = Effect.orDie(divide(1, 0)) + * + * Effect.runPromise(program).catch(console.error) + * // Output: + * // (FiberFailure) Error: Cannot divide by zero + * // ...stack trace... + * ``` + * + * @see {@link orDieWith} if you need to customize the error. + * + * @since 2.0.0 + * @category Converting Failures to Defects + */ +export const orDie: (self: Effect) => Effect = core.orDie + +/** + * Converts an effect's failure into a fiber termination with a custom error. + * + * **Details** + * + * The `orDieWith` function behaves like {@link orDie}, but it allows you to provide a mapping + * function to transform the error before terminating the fiber. This is useful for cases where + * you want to include a more detailed or user-friendly error when the failure is propagated + * as a defect. + * + * **When to Use** + * + * Use `orDieWith` when failures should terminate the fiber as defects, and you want to customize + * the error for clarity or debugging purposes. + * + * **Example** (Customizing Defect) + * + * ```ts + * import { Effect } from "effect" + * + * const divide = (a: number, b: number) => + * b === 0 + * ? Effect.fail(new Error("Cannot divide by zero")) + * : Effect.succeed(a / b) + * + * // ┌─── Effect + * // ▼ + * const program = Effect.orDieWith( + * divide(1, 0), + * (error) => new Error(`defect: ${error.message}`) + * ) + * + * Effect.runPromise(program).catch(console.error) + * // Output: + * // (FiberFailure) Error: defect: Cannot divide by zero + * // ...stack trace... + * ``` + * + * @see {@link orDie} if you don't need to customize the error. + * + * @since 2.0.0 + * @category Converting Failures to Defects + */ +export const orDieWith: { + (f: (error: E) => unknown): (self: Effect) => Effect + (self: Effect, f: (error: E) => unknown): Effect +} = core.orDieWith + +/** + * Attempts one effect, and if it fails, falls back to another effect. + * + * **Details** + * + * This function allows you to try executing an effect, and if it fails + * (produces an error), a fallback effect is executed instead. The fallback + * effect is defined as a lazy argument, meaning it will only be evaluated if + * the first effect fails. This provides a way to recover from errors by + * specifying an alternative path of execution. + * + * The error type of the resulting effect will be that of the fallback effect, + * as the first effect's error is replaced when the fallback is executed. + * + * **Example** + * + * ```ts + * import { Effect } from "effect" + * + * const success = Effect.succeed("success") + * const failure = Effect.fail("failure") + * const fallback = Effect.succeed("fallback") + * + * // Try the success effect first, fallback is not used + * const program1 = Effect.orElse(success, () => fallback) + * console.log(Effect.runSync(program1)) + * // Output: "success" + * + * // Try the failure effect first, fallback is used + * const program2 = Effect.orElse(failure, () => fallback) + * console.log(Effect.runSync(program2)) + * // Output: "fallback" + * ``` + * + * @see {@link catchAll} if you need to access the error in the fallback effect. + * + * @since 2.0.0 + * @category Fallback + */ +export const orElse: { + (that: LazyArg>): (self: Effect) => Effect + (self: Effect, that: LazyArg>): Effect +} = core.orElse + +/** + * Replaces the failure of an effect with a custom failure value. + * + * **Details** + * + * This function allows you to handle the failure of an effect by replacing it + * with a predefined failure value. If the effect fails, the new failure value + * provided by the `evaluate` function will be returned instead of the original + * failure. If the effect succeeds, the original success value is returned + * unchanged. + * + * **When to Use** + * + * This is particularly useful when you want to standardize error handling or + * provide a consistent failure value for specific operations. It simplifies + * error management by ensuring that all failures are replaced with a controlled + * alternative. + * + * **Example** + * + * ```ts + * import { Effect } from "effect" + * + * const validate = (age: number): Effect.Effect => { + * if (age < 0) { + * return Effect.fail("NegativeAgeError") + * } else if (age < 18) { + * return Effect.fail("IllegalAgeError") + * } else { + * return Effect.succeed(age) + * } + * } + * + * const program = Effect.orElseFail(validate(-1), () => "invalid age") + * + * console.log(Effect.runSyncExit(program)) + * // Output: + * // { + * // _id: 'Exit', + * // _tag: 'Failure', + * // cause: { _id: 'Cause', _tag: 'Fail', failure: 'invalid age' } + * // } + * ``` + * + * @see {@link mapError} if you need to access the error to transform it. + * + * @since 2.0.0 + * @category Fallback + */ +export const orElseFail: { + (evaluate: LazyArg): (self: Effect) => Effect + (self: Effect, evaluate: LazyArg): Effect +} = effect.orElseFail + +/** + * Ensures the effect always succeeds by replacing failures with a default + * success value. + * + * **Details** + * + * This function transforms an effect that may fail into one that cannot fail by + * replacing any failure with a provided success value. If the original effect + * fails, the failure is "swallowed," and the specified success value is + * returned instead. If the original effect succeeds, its value remains + * unchanged. + * + * **When to Use** + * + * This is especially useful for providing default values in case of failure, + * ensuring that an effect always completes successfully. By using this + * function, you can avoid the need for complex error handling and guarantee a + * fallback result. + * + * **Example** + * + * ```ts + * import { Effect } from "effect" + * + * const validate = (age: number): Effect.Effect => { + * if (age < 0) { + * return Effect.fail("NegativeAgeError") + * } else if (age < 18) { + * return Effect.fail("IllegalAgeError") + * } else { + * return Effect.succeed(age) + * } + * } + * + * const program = Effect.orElseSucceed(validate(-1), () => 18) + * + * console.log(Effect.runSyncExit(program)) + * // Output: + * // { _id: 'Exit', _tag: 'Success', value: 18 } + * ``` + * + * @since 2.0.0 + * @category Fallback + */ +export const orElseSucceed: { + (evaluate: LazyArg): (self: Effect) => Effect + (self: Effect, evaluate: LazyArg): Effect +} = effect.orElseSucceed + +/** + * Runs a sequence of effects and returns the result of the first successful + * one. + * + * **Details** + * + * This function allows you to execute a collection of effects in sequence, + * stopping at the first success. If an effect succeeds, its result is + * immediately returned, and no further effects in the sequence are executed. + * However, if all the effects fail, the function will return the error of the + * last effect. + * + * The execution is sequential, meaning that effects are evaluated one at a time + * in the order they are provided. This ensures predictable behavior and avoids + * unnecessary computations. + * + * If the collection of effects is empty, an `IllegalArgumentException` is + * thrown, indicating that the operation is invalid without any effects to try. + * + * **When to Use** + * + * This is particularly useful when you have multiple fallback strategies or + * alternative sources to obtain a result, such as attempting multiple APIs, + * retrieving configurations, or accessing resources in a prioritized manner. + * + * **Example** + * + * ```ts + * import { Effect, Console } from "effect" + * + * interface Config { + * host: string + * port: number + * apiKey: string + * } + * + * // Create a configuration object with sample values + * const makeConfig = (name: string): Config => ({ + * host: `${name}.example.com`, + * port: 8080, + * apiKey: "12345-abcde" + * }) + * + * // Simulate retrieving configuration from a remote node + * const remoteConfig = (name: string): Effect.Effect => + * Effect.gen(function* () { + * // Simulate node3 being the only one with available config + * if (name === "node3") { + * yield* Console.log(`Config for ${name} found`) + * return makeConfig(name) + * } else { + * yield* Console.log(`Unavailable config for ${name}`) + * return yield* Effect.fail(new Error(`Config not found for ${name}`)) + * } + * }) + * + * // Define the master configuration and potential fallback nodes + * const masterConfig = remoteConfig("master") + * const nodeConfigs = ["node1", "node2", "node3", "node4"].map(remoteConfig) + * + * // Attempt to find a working configuration, + * // starting with the master and then falling back to other nodes + * const config = Effect.firstSuccessOf([masterConfig, ...nodeConfigs]) + * + * // Run the effect to retrieve the configuration + * const result = Effect.runSync(config) + * + * console.log(result) + * // Output: + * // Unavailable config for master + * // Unavailable config for node1 + * // Unavailable config for node2 + * // Config for node3 found + * // { host: 'node3.example.com', port: 8080, apiKey: '12345-abcde' } + * ``` + * + * @since 2.0.0 + * @category Fallback + */ +export const firstSuccessOf: >( + effects: Iterable +) => Effect, Effect.Error, Effect.Context> = effect.firstSuccessOf + +/** + * Retrieves the `Random` service from the context. + * + * @since 2.0.0 + * @category Random + */ +export const random: Effect = effect.random + +/** + * Retrieves the `Random` service from the context and uses it to run the + * specified effect. + * + * @since 2.0.0 + * @category Random + */ +export const randomWith: (f: (random: Random.Random) => Effect) => Effect = + defaultServices.randomWith + +/** + * Executes the specified effect with the specified implementation of the + * `Random` service. + * + * @since 2.0.0 + * @category Random + */ +export const withRandom: { + (value: X): (effect: Effect) => Effect + (effect: Effect, value: X): Effect +} = defaultServices.withRandom + +/** + * Executes the specified effect with a `Random` service that cycles through + * a provided array of values. + * + * @example + * ```ts + * import { Effect, Random } from "effect" + * + * Effect.gen(function*() { + * console.log(yield* Random.next) // 0.2 + * console.log(yield* Random.next) // 0.5 + * console.log(yield* Random.next) // 0.8 + * }).pipe(Effect.withRandomFixed([0.2, 0.5, 0.8])) + * ``` + * + * @since 3.11.0 + * @category Random + */ +export const withRandomFixed: { + >(values: T): (effect: Effect) => Effect + , A, E, R>(effect: Effect, values: T): Effect +} = dual( + 2, + , A, E, R>(effect: Effect, values: T): Effect => + withRandom(effect, Random.fixed(values)) +) + +/** + * Sets the implementation of the `Random` service to the specified value and + * restores it to its original value when the scope is closed. + * + * @since 2.0.0 + * @category Random + */ +export const withRandomScoped: (value: A) => Effect = + fiberRuntime.withRandomScoped + +/** + * Returns an effect that accesses the runtime, which can be used to (unsafely) + * execute tasks. + * + * **When to Use** + * + * This is useful for integration with legacy code that must call back into + * Effect code. + * + * @since 2.0.0 + * @category Runtime + */ +export const runtime: () => Effect, never, R> = runtime_.runtime + +/** + * Retrieves an effect that succeeds with the current runtime flags, which + * govern behavior and features of the runtime system. + * + * @since 2.0.0 + * @category Runtime + */ +export const getRuntimeFlags: Effect = core.runtimeFlags + +/** + * @since 2.0.0 + * @category Runtime + */ +export const patchRuntimeFlags: (patch: RuntimeFlagsPatch.RuntimeFlagsPatch) => Effect = core.updateRuntimeFlags + +/** + * @since 2.0.0 + * @category Runtime + */ +export const withRuntimeFlagsPatch: { + (update: RuntimeFlagsPatch.RuntimeFlagsPatch): (self: Effect) => Effect + (self: Effect, update: RuntimeFlagsPatch.RuntimeFlagsPatch): Effect +} = core.withRuntimeFlags + +/** + * @since 2.0.0 + * @category Runtime + */ +export const withRuntimeFlagsPatchScoped: ( + update: RuntimeFlagsPatch.RuntimeFlagsPatch +) => Effect = fiberRuntime.withRuntimeFlagsScoped + +/** + * Tags each metric in an effect with specific key-value pairs. + * + * **Details** + * + * This function allows you to tag all metrics in an effect with a set of + * key-value pairs or a single key-value pair. Tags help you add metadata to + * metrics, making it easier to filter and categorize them in monitoring + * systems. The provided tags will apply to all metrics generated within the + * effect's scope. + * + * @since 2.0.0 + * @category Metrics + */ +export const tagMetrics: { + (key: string, value: string): (effect: Effect) => Effect + (values: Record): (effect: Effect) => Effect + (effect: Effect, key: string, value: string): Effect + (effect: Effect, values: Record): Effect +} = effect.tagMetrics + +/** + * Adds labels to metrics within an effect using `MetricLabel` objects. + * + * **Details** + * + * This function allows you to label metrics using `MetricLabel` objects. Labels + * help add structured metadata to metrics for categorization and filtering in + * monitoring systems. The provided labels will apply to all metrics within the + * effect's execution. + * + * @since 2.0.0 + * @category Metrics + */ +export const labelMetrics: { + (labels: Iterable): (self: Effect) => Effect + (self: Effect, labels: Iterable): Effect +} = effect.labelMetrics + +/** + * Tags metrics within a scope with a specific key-value pair. + * + * **Details** + * + * This function tags all metrics within a scope with the provided key-value + * pair. Once the scope is closed, the tag is automatically removed. This is + * useful for applying temporary context-specific tags to metrics during scoped + * operations. + * + * @since 2.0.0 + * @category Metrics + */ +export const tagMetricsScoped: (key: string, value: string) => Effect = + fiberRuntime.tagMetricsScoped + +/** + * Adds labels to metrics within a scope using `MetricLabel` objects. + * + * **Details** + * + * This function allows you to apply labels to all metrics generated within a + * specific scope using an array of `MetricLabel` objects. These labels provide + * additional metadata to metrics, which can be used for categorization, + * filtering, or monitoring purposes. The labels are scoped and will be removed + * automatically once the scope is closed, ensuring they are only applied + * temporarily within the defined context. + * + * @since 2.0.0 + * @category Metrics + */ +export const labelMetricsScoped: ( + labels: ReadonlyArray +) => Effect = fiberRuntime.labelMetricsScoped + +/** + * Retrieves the metric labels associated with the current scope. + * + * @since 2.0.0 + * @category Metrics + */ +export const metricLabels: Effect> = core.metricLabels + +/** + * Associates a metric with the current effect, updating it as the effect progresses. + * + * @since 2.0.0 + * @category Metrics + */ +export const withMetric: { + (metric: Metric.Metric): (self: Effect) => Effect + (self: Effect, metric: Metric.Metric): Effect +} = effect.withMetric + +/** + * @category Semaphore + * @since 2.0.0 + */ +export interface Permit { + readonly index: number +} + +/** + * A semaphore is a synchronization mechanism used to manage access to a shared + * resource. In Effect, semaphores help control resource access or coordinate + * tasks within asynchronous, concurrent operations. + * + * A semaphore acts as a generalized mutex, allowing a set number of permits to + * be held and released concurrently. Permits act like tickets, giving tasks or + * fibers controlled access to a shared resource. When no permits are available, + * tasks trying to acquire one will wait until a permit is released. + * + * @category Semaphore + * @since 2.0.0 + */ +export interface Semaphore { + /** + * Adjusts the number of permits available in the semaphore. + */ + resize(permits: number): Effect + + /** + * Runs an effect with the given number of permits and releases the permits + * when the effect completes. + * + * **Details** + * + * This function acquires the specified number of permits before executing + * the provided effect. Once the effect finishes, the permits are released. + * If insufficient permits are available, the function will wait until they + * are released by other tasks. + */ + withPermits(permits: number): (self: Effect) => Effect + + /** + * Runs an effect only if the specified number of permits are immediately + * available. + * + * **Details** + * + * This function attempts to acquire the specified number of permits. If they + * are available, it runs the effect and releases the permits after the effect + * completes. If permits are not available, the effect does not execute, and + * the result is `Option.none`. + */ + withPermitsIfAvailable(permits: number): (self: Effect) => Effect, E, R> + + /** + * Acquires the specified number of permits and returns the resulting + * available permits, suspending the task if they are not yet available. + * Concurrent pending `take` calls are processed in a first-in, first-out manner. + */ + take(permits: number): Effect + + /** + * Releases the specified number of permits and returns the resulting + * available permits. + */ + release(permits: number): Effect + + /** + * Releases all permits held by this semaphore and returns the resulting available permits. + */ + releaseAll: Effect +} + +/** + * Unsafely creates a new Semaphore. + * + * @since 2.0.0 + * @category Semaphore + */ +export const unsafeMakeSemaphore: (permits: number) => Semaphore = circular.unsafeMakeSemaphore + +/** + * Creates a new semaphore with the specified number of permits. + * + * **Details** + * + * This function initializes a semaphore that controls concurrent access to a + * shared resource. The number of permits determines how many tasks can access + * the resource concurrently. + * + * **Example** + * + * ```ts + * import { Effect } from "effect" + * + * // Create a semaphore with 3 permits + * const mutex = Effect.makeSemaphore(3) + * ``` + * + * @since 2.0.0 + * @category Semaphore + */ +export const makeSemaphore: (permits: number) => Effect = circular.makeSemaphore + +/** + * A `Latch` is a synchronization primitive that allows you to control the + * execution of fibers based on an open or closed state. It acts as a gate, + * where fibers can wait for the latch to open before proceeding. + * + * **Details** + * + * A `Latch` can be in one of two states: open or closed. Fibers can: + * - Wait for the latch to open using `await`. + * - Proceed only when the latch is open using `whenOpen`. + * - Open the latch to release all waiting fibers using `open`. + * - Close the latch to block fibers using `close`. + * + * Additionally, fibers can be released without changing the state of the latch + * using `release`. + * + * @category Latch + * @since 3.8.0 + */ +export interface Latch extends Effect { + /** + * Opens the latch, releasing all fibers waiting on it. + * + * **Details** + * + * Once the latch is opened, it remains open. Any fibers waiting on `await` + * will be released and can continue execution. + */ + readonly open: Effect + + /** + * Opens the latch, releasing all fibers waiting on it. + * + * **Details** + * + * Once the latch is opened, it remains open. Any fibers waiting on `await` + * will be released and can continue execution. + */ + readonly unsafeOpen: () => void + + /** + * Releases all fibers waiting on the latch without opening it. + * + * **Details** + * + * This function lets waiting fibers proceed without permanently changing the + * state of the latch. + */ + readonly release: Effect + + /** + * Waits for the latch to be opened. + * + * **Details** + * + * If the latch is already open, this effect completes immediately. Otherwise, + * it suspends the fiber until the latch is opened. + */ + readonly await: Effect + + /** + * Closes the latch, blocking fibers from proceeding. + * + * **Details** + * + * This operation puts the latch into a closed state, requiring it to be + * reopened before waiting fibers can proceed. + */ + readonly close: Effect + + /** + * Unsafely closes the latch, blocking fibers without effect guarantees. + * + * **Details** + * + * Use this operation cautiously, as it does not run within an effect context + * and bypasses runtime guarantees. + */ + readonly unsafeClose: () => void + + /** + * Runs the given effect only when the latch is open. + * + * **Details** + * + * This function ensures that the provided effect executes only if the latch + * is open. If the latch is closed, the fiber will wait until it opens. + */ + readonly whenOpen: (self: Effect) => Effect + + readonly [Unify.typeSymbol]?: unknown + readonly [Unify.unifySymbol]?: LatchUnify + readonly [Unify.ignoreSymbol]?: LatchUnifyIgnore +} + +/** + * @category Models + * @since 3.8.0 + */ +export interface LatchUnify extends EffectUnify { + Latch?: () => Latch +} + +/** + * @category Models + * @since 3.8.0 + */ +export interface LatchUnifyIgnore extends EffectUnifyIgnore { + Effect?: true +} + +/** + * @category Latch + * @since 3.8.0 + */ +export const unsafeMakeLatch: (open?: boolean | undefined) => Latch = circular.unsafeMakeLatch + +/** + * Creates a new `Latch`, starting in the specified state. + * + * **Details** + * + * This function initializes a `Latch` safely, ensuring proper runtime + * guarantees. By default, the latch starts in the closed state. + * + * **Example** + * + * ```ts + * import { Console, Effect } from "effect" + * + * const program = Effect.gen(function*() { + * // Create a latch, starting in the closed state + * const latch = yield* Effect.makeLatch(false) + * + * // Fork a fiber that logs "open sesame" when the latch is opened + * const fiber = yield* Console.log("open sesame").pipe( + * latch.whenOpen, + * Effect.fork + * ) + * + * yield* Effect.sleep("1 second") + * + * // Open the latch + * yield* latch.open + * yield* fiber.await + * }) + * + * Effect.runFork(program) + * // Output: open sesame (after 1 second) + * ``` + * + * @category Latch + * @since 3.8.0 + */ +export const makeLatch: (open?: boolean | undefined) => Effect = circular.makeLatch + +/** + * Runs an effect in the background, returning a fiber that can be observed or + * interrupted. + * + * Unless you specifically need a `Promise` or synchronous operation, `runFork` + * is a good default choice. + * + * **Details** + * + * This function is the foundational way to execute an effect in the background. + * It creates a "fiber," a lightweight, cooperative thread of execution that can + * be observed (to access its result), interrupted, or joined. Fibers are useful + * for concurrent programming and allow effects to run independently of the main + * program flow. + * + * Once the effect is running in a fiber, you can monitor its progress, cancel + * it if necessary, or retrieve its result when it completes. If the effect + * fails, the fiber will propagate the failure, which you can observe and + * handle. + * + * **When to Use** + * + * Use this function when you need to run an effect in the background, + * especially if the effect is long-running or performs periodic tasks. It's + * suitable for tasks that need to run independently but might still need + * observation or management, like logging, monitoring, or scheduled tasks. + * + * This function is ideal if you don't need the result immediately or if the + * effect is part of a larger concurrent workflow. + * + * **Example** (Running an Effect in the Background) + * + * ```ts + * import { Effect, Console, Schedule, Fiber } from "effect" + * + * // ┌─── Effect + * // ▼ + * const program = Effect.repeat( + * Console.log("running..."), + * Schedule.spaced("200 millis") + * ) + * + * // ┌─── RuntimeFiber + * // ▼ + * const fiber = Effect.runFork(program) + * + * setTimeout(() => { + * Effect.runFork(Fiber.interrupt(fiber)) + * }, 500) + * ``` + * + * @since 2.0.0 + * @category Running Effects + */ +export const runFork: ( + effect: Effect, + options?: Runtime.RunForkOptions +) => Fiber.RuntimeFiber = runtime_.unsafeForkEffect + +/** + * Executes an effect asynchronously and handles the result using a callback. + * + * **Details** + * + * This function runs an effect asynchronously and passes the result (`Exit`) to + * a specified callback. The callback is invoked with the outcome of the effect: + * - On success, the callback receives the successful result. + * - On failure, the callback receives the failure information. + * + * **When to Use** + * + * This function is effectful and should only be invoked at the edges of your + * program. + * + * @since 2.0.0 + * @category Running Effects + */ +export const runCallback: ( + effect: Effect, + options?: Runtime.RunCallbackOptions | undefined +) => Runtime.Cancel = runtime_.unsafeRunEffect + +/** + * Executes an effect and returns the result as a `Promise`. + * + * **Details** + * + * This function runs an effect and converts its result into a `Promise`. If the + * effect succeeds, the `Promise` will resolve with the successful result. If + * the effect fails, the `Promise` will reject with an error, which includes the + * failure details of the effect. + * + * The optional `options` parameter allows you to pass an `AbortSignal` for + * cancellation, enabling more fine-grained control over asynchronous tasks. + * + * **When to Use** + * + * Use this function when you need to execute an effect and work with its result + * in a promise-based system, such as when integrating with third-party + * libraries that expect `Promise` results. + * + * **Example** (Running a Successful Effect as a Promise) + * + * ```ts + * import { Effect } from "effect" + * + * Effect.runPromise(Effect.succeed(1)).then(console.log) + * // Output: 1 + * ``` + * + * **Example** (Handling a Failing Effect as a Rejected Promise) + * + * ```ts + * import { Effect } from "effect" + * + * Effect.runPromise(Effect.fail("my error")).catch(console.error) + * // Output: + * // (FiberFailure) Error: my error + * ``` + * + * @see {@link runPromiseExit} for a version that returns an `Exit` type instead + * of rejecting. + * + * @since 2.0.0 + * @category Running Effects + */ +export const runPromise: ( + effect: Effect, + options?: { readonly signal?: AbortSignal | undefined } | undefined +) => Promise = runtime_.unsafeRunPromiseEffect + +/** + * Runs an effect and returns a `Promise` that resolves to an `Exit`, + * representing the outcome. + * + * **Details** + * + * This function executes an effect and resolves to an `Exit` object. The `Exit` + * type provides detailed information about the result of the effect: + * - If the effect succeeds, the `Exit` will be of type `Success` and include + * the value produced by the effect. + * - If the effect fails, the `Exit` will be of type `Failure` and contain a + * `Cause` object, detailing the failure. + * + * Using this function allows you to examine both successful results and failure + * cases in a unified way, while still leveraging `Promise` for handling the + * asynchronous behavior of the effect. + * + * **When to Use** + * + * Use this function when you need to understand the outcome of an effect, + * whether it succeeded or failed, and want to work with this result using + * `Promise` syntax. This is particularly useful when integrating with systems + * that rely on promises but need more detailed error handling than a simple + * rejection. + * + * **Example** (Handling Results as Exit) + * + * ```ts + * import { Effect } from "effect" + * + * // Execute a successful effect and get the Exit result as a Promise + * Effect.runPromiseExit(Effect.succeed(1)).then(console.log) + * // Output: + * // { + * // _id: "Exit", + * // _tag: "Success", + * // value: 1 + * // } + * + * // Execute a failing effect and get the Exit result as a Promise + * Effect.runPromiseExit(Effect.fail("my error")).then(console.log) + * // Output: + * // { + * // _id: "Exit", + * // _tag: "Failure", + * // cause: { + * // _id: "Cause", + * // _tag: "Fail", + * // failure: "my error" + * // } + * // } + * ``` + * + * @since 2.0.0 + * @category Running Effects + */ +export const runPromiseExit: ( + effect: Effect, + options?: { readonly signal?: AbortSignal } | undefined +) => Promise> = runtime_.unsafeRunPromiseExitEffect + +/** + * Executes an effect synchronously, running it immediately and returning the + * result. + * + * **Details** + * + * This function evaluates the provided effect synchronously, returning its + * result directly. It is ideal for effects that do not fail or include + * asynchronous operations. If the effect does fail or involves async tasks, it + * will throw an error. Execution stops at the point of failure or asynchronous + * operation, making it unsuitable for effects that require asynchronous + * handling. + * + * **Important**: Attempting to run effects that involve asynchronous operations + * or failures will result in exceptions being thrown, so use this function with + * care for purely synchronous and error-free effects. + * + * **When to Use** + * + * Use this function when: + * - You are sure that the effect will not fail or involve asynchronous + * operations. + * - You need a direct, synchronous result from the effect. + * - You are working within a context where asynchronous effects are not + * allowed. + * + * Avoid using this function for effects that can fail or require asynchronous + * handling. For such cases, consider using {@link runPromise} or + * {@link runSyncExit}. + * + * **Example** (Synchronous Logging) + * + * ```ts + * import { Effect } from "effect" + * + * const program = Effect.sync(() => { + * console.log("Hello, World!") + * return 1 + * }) + * + * const result = Effect.runSync(program) + * // Output: Hello, World! + * + * console.log(result) + * // Output: 1 + * ``` + * + * **Example** (Incorrect Usage with Failing or Async Effects) + * + * ```ts + * import { Effect } from "effect" + * + * try { + * // Attempt to run an effect that fails + * Effect.runSync(Effect.fail("my error")) + * } catch (e) { + * console.error(e) + * } + * // Output: + * // (FiberFailure) Error: my error + * + * try { + * // Attempt to run an effect that involves async work + * Effect.runSync(Effect.promise(() => Promise.resolve(1))) + * } catch (e) { + * console.error(e) + * } + * // Output: + * // (FiberFailure) AsyncFiberException: Fiber #0 cannot be resolved synchronously. This is caused by using runSync on an effect that performs async work + * ``` + * + * @see {@link runSyncExit} for a version that returns an `Exit` type instead of + * throwing an error. + * + * @since 2.0.0 + * @category Running Effects + */ +export const runSync: (effect: Effect) => A = runtime_.unsafeRunSyncEffect + +/** + * Runs an effect synchronously and returns the result as an `Exit` type. + * + * **Details** + * + * This function executes the provided effect synchronously and returns an `Exit` + * type that encapsulates the outcome of the effect: + * - If the effect succeeds, the result is wrapped in a `Success`. + * - If the effect fails, it returns a `Failure` containing a `Cause` that explains + * the failure. + * + * If the effect involves asynchronous operations, this function will return a `Failure` + * with a `Die` cause, indicating that it cannot resolve the effect synchronously. + * This makes the function suitable for use only with effects that are synchronous + * in nature. + * + * **When to Use** + * + * Use this function when: + * - You want to handle both success and failure outcomes in a structured way using the `Exit` type. + * - You are working with effects that are purely synchronous and do not involve asynchronous operations. + * - You need to debug or inspect failures, including their causes, in a detailed manner. + * + * Avoid using this function for effects that involve asynchronous operations, as it will fail with a `Die` cause. + * + * **Example** (Handling Results as Exit) + * + * ```ts + * import { Effect } from "effect" + * + * console.log(Effect.runSyncExit(Effect.succeed(1))) + * // Output: + * // { + * // _id: "Exit", + * // _tag: "Success", + * // value: 1 + * // } + * + * console.log(Effect.runSyncExit(Effect.fail("my error"))) + * // Output: + * // { + * // _id: "Exit", + * // _tag: "Failure", + * // cause: { + * // _id: "Cause", + * // _tag: "Fail", + * // failure: "my error" + * // } + * // } + * ``` + * + * **Example** (Asynchronous Operation Resulting in Die) + * + * ```ts + * import { Effect } from "effect" + * + * console.log(Effect.runSyncExit(Effect.promise(() => Promise.resolve(1)))) + * // Output: + * // { + * // _id: 'Exit', + * // _tag: 'Failure', + * // cause: { + * // _id: 'Cause', + * // _tag: 'Die', + * // defect: [Fiber #0 cannot be resolved synchronously. This is caused by using runSync on an effect that performs async work] { + * // fiber: [FiberRuntime], + * // _tag: 'AsyncFiberException', + * // name: 'AsyncFiberException' + * // } + * // } + * // } + * ``` + * + * @since 2.0.0 + * @category Running Effects + */ +export const runSyncExit: (effect: Effect) => Exit.Exit = runtime_.unsafeRunSyncExitEffect + +/** + * Combines multiple effects and accumulates both successes and failures. + * + * **Details** + * + * This function allows you to combine multiple effects, continuing through all + * effects even if some of them fail. Unlike other functions that stop execution + * upon encountering an error, this function collects all errors into a `Cause`. + * The final result includes all successes and the accumulated failures. + * + * By default, effects are executed sequentially, but you can control + * concurrency and batching behavior using the `options` parameter. This + * provides flexibility in scenarios where you want to maximize performance or + * ensure specific ordering. + * + * **Example** + * + * ```ts + * import { Effect, Console } from "effect" + * + * const task1 = Console.log("task1").pipe(Effect.as(1)) + * const task2 = Effect.fail("Oh uh!").pipe(Effect.as(2)) + * const task3 = Console.log("task2").pipe(Effect.as(3)) + * const task4 = Effect.fail("Oh no!").pipe(Effect.as(4)) + * + * const program = task1.pipe( + * Effect.validate(task2), + * Effect.validate(task3), + * Effect.validate(task4) + * ) + * + * Effect.runPromiseExit(program).then(console.log) + * // Output: + * // task1 + * // task2 + * // { + * // _id: 'Exit', + * // _tag: 'Failure', + * // cause: { + * // _id: 'Cause', + * // _tag: 'Sequential', + * // left: { _id: 'Cause', _tag: 'Fail', failure: 'Oh uh!' }, + * // right: { _id: 'Cause', _tag: 'Fail', failure: 'Oh no!' } + * // } + * // } + * ``` + * + * @see {@link zip} for a version that stops at the first error. + * + * @since 2.0.0 + * @category Error Accumulation + */ +export const validate: { + ( + that: Effect, + options?: { + readonly concurrent?: boolean | undefined + readonly batching?: boolean | "inherit" | undefined + readonly concurrentFinalizers?: boolean | undefined + } | undefined + ): (self: Effect) => Effect<[A, B], E1 | E, R1 | R> + ( + self: Effect, + that: Effect, + options?: + | { + readonly concurrent?: boolean | undefined + readonly batching?: boolean | "inherit" | undefined + readonly concurrentFinalizers?: boolean | undefined + } + | undefined + ): Effect<[A, B], E | E1, R | R1> +} = fiberRuntime.validate + +/** + * Sequentially combines two effects using a specified combiner function while + * accumulating errors. + * + * **Details** + * + * This function combines two effects, `self` and `that`, into a single effect + * by applying the provided combiner function to their results. If both effects + * succeed, the combiner function is applied to their results to produce the + * final value. If either effect fails, the failures are accumulated into a + * combined `Cause`. + * + * By default, effects are executed sequentially. However, the execution mode + * can be controlled using the `options` parameter to enable concurrency, + * batching, or customized finalizer behavior. + * + * @since 2.0.0 + * @category Error Accumulation + */ +export const validateWith: { + ( + that: Effect, + f: (a: A, b: B) => C, + options?: + | { + readonly concurrent?: boolean | undefined + readonly batching?: boolean | "inherit" | undefined + readonly concurrentFinalizers?: boolean | undefined + } + | undefined + ): (self: Effect) => Effect + ( + self: Effect, + that: Effect, + f: (a: A, b: B) => C, + options?: + | { + readonly concurrent?: boolean | undefined + readonly batching?: boolean | "inherit" | undefined + readonly concurrentFinalizers?: boolean | undefined + } + | undefined + ): Effect +} = fiberRuntime.validateWith + +/** + * Combines two effects into a single effect, producing a tuple of their + * results. + * + * **Details** + * + * This function combines two effects, `self` and `that`, into one. It executes + * the first effect (`self`) and then the second effect (`that`), collecting + * their results into a tuple. Both effects must succeed for the resulting + * effect to succeed. If either effect fails, the entire operation fails. + * + * By default, the effects are executed sequentially. If the `concurrent` option + * is set to `true`, the effects will run concurrently, potentially improving + * performance for independent operations. + * + * **Example** (Combining Two Effects Sequentially) + * + * ```ts + * import { Effect } from "effect" + * + * const task1 = Effect.succeed(1).pipe( + * Effect.delay("200 millis"), + * Effect.tap(Effect.log("task1 done")) + * ) + * const task2 = Effect.succeed("hello").pipe( + * Effect.delay("100 millis"), + * Effect.tap(Effect.log("task2 done")) + * ) + * + * // Combine the two effects together + * // + * // ┌─── Effect<[number, string], never, never> + * // ▼ + * const program = Effect.zip(task1, task2) + * + * Effect.runPromise(program).then(console.log) + * // Output: + * // timestamp=... level=INFO fiber=#0 message="task1 done" + * // timestamp=... level=INFO fiber=#0 message="task2 done" + * // [ 1, 'hello' ] + * ``` + * + * **Example** (Combining Two Effects Concurrently) + * + * ```ts + * import { Effect } from "effect" + * + * const task1 = Effect.succeed(1).pipe( + * Effect.delay("200 millis"), + * Effect.tap(Effect.log("task1 done")) + * ) + * const task2 = Effect.succeed("hello").pipe( + * Effect.delay("100 millis"), + * Effect.tap(Effect.log("task2 done")) + * ) + * + * // Run both effects concurrently using the concurrent option + * const program = Effect.zip(task1, task2, { concurrent: true }) + * + * Effect.runPromise(program).then(console.log) + * // Output: + * // timestamp=... level=INFO fiber=#0 message="task2 done" + * // timestamp=... level=INFO fiber=#0 message="task1 done" + * // [ 1, 'hello' ] + * ``` + * + * @see {@link zipWith} for a version that combines the results with a custom + * function. + * @see {@link validate} for a version that accumulates errors. + * + * @since 2.0.0 + * @category Zipping + */ +export const zip: { + ( + that: Effect, + options?: + | { + readonly concurrent?: boolean | undefined + readonly batching?: boolean | "inherit" | undefined + readonly concurrentFinalizers?: boolean | undefined + } + | undefined + ): (self: Effect) => Effect<[A, A2], E2 | E, R2 | R> + ( + self: Effect, + that: Effect, + options?: + | { + readonly concurrent?: boolean | undefined + readonly batching?: boolean | "inherit" | undefined + readonly concurrentFinalizers?: boolean | undefined + } + | undefined + ): Effect<[A, A2], E | E2, R | R2> +} = fiberRuntime.zipOptions + +/** + * Executes two effects sequentially, returning the result of the first effect + * and ignoring the result of the second. + * + * **Details** + * + * This function allows you to run two effects in sequence, where the result of + * the first effect is preserved, and the result of the second effect is + * discarded. By default, the two effects are executed sequentially. If you need + * them to run concurrently, you can pass the `{ concurrent: true }` option. + * + * The second effect will always be executed, even though its result is ignored. + * This makes it useful for cases where you want to execute an effect for its + * side effects while keeping the result of another effect. + * + * **When to Use** + * + * Use this function when you are only interested in the result of the first + * effect but still need to run the second effect for its side effects, such as + * logging or performing a cleanup action. + * + * **Example** + * + * ```ts + * import { Effect } from "effect" + * + * const task1 = Effect.succeed(1).pipe( + * Effect.delay("200 millis"), + * Effect.tap(Effect.log("task1 done")) + * ) + * const task2 = Effect.succeed("hello").pipe( + * Effect.delay("100 millis"), + * Effect.tap(Effect.log("task2 done")) + * ) + * + * const program = Effect.zipLeft(task1, task2) + * + * Effect.runPromise(program).then(console.log) + * // Output: + * // timestamp=... level=INFO fiber=#0 message="task1 done" + * // timestamp=... level=INFO fiber=#0 message="task2 done" + * // 1 + * ``` + * + * @see {@link zipRight} for a version that returns the result of the second + * effect. + * + * @since 2.0.0 + * @category Zipping + */ +export const zipLeft: { + ( + that: Effect, + options?: + | { + readonly concurrent?: boolean | undefined + readonly batching?: boolean | "inherit" | undefined + readonly concurrentFinalizers?: boolean | undefined + } + | undefined + ): (self: Effect) => Effect + ( + self: Effect, + that: Effect, + options?: + | { + readonly concurrent?: boolean | undefined + readonly batching?: boolean | "inherit" | undefined + readonly concurrentFinalizers?: boolean | undefined + } + | undefined + ): Effect +} = fiberRuntime.zipLeftOptions + +/** + * Executes two effects sequentially, returning the result of the second effect + * while ignoring the result of the first. + * + * **Details** + * + * This function allows you to run two effects in sequence, keeping the result + * of the second effect and discarding the result of the first. By default, the + * two effects are executed sequentially. If you need them to run concurrently, + * you can pass the `{ concurrent: true }` option. + * + * The first effect will always be executed, even though its result is ignored. + * This makes it useful for scenarios where the first effect is needed for its + * side effects, but only the result of the second effect is important. + * + * **When to Use** + * + * Use this function when you are only interested in the result of the second + * effect but still need to run the first effect for its side effects, such as + * initialization or setup tasks. + * + * **Example** + * + * ```ts + * import { Effect } from "effect" + * + * const task1 = Effect.succeed(1).pipe( + * Effect.delay("200 millis"), + * Effect.tap(Effect.log("task1 done")) + * ) + * const task2 = Effect.succeed("hello").pipe( + * Effect.delay("100 millis"), + * Effect.tap(Effect.log("task2 done")) + * ) + * + * const program = Effect.zipRight(task1, task2) + * + * Effect.runPromise(program).then(console.log) + * // Output: + * // timestamp=... level=INFO fiber=#0 message="task1 done" + * // timestamp=... level=INFO fiber=#0 message="task2 done" + * // hello + * ``` + * + * @see {@link zipLeft} for a version that returns the result of the first + * effect. + * + * @since 2.0.0 + * @category Zipping + */ +export const zipRight: { + ( + that: Effect, + options?: { + readonly concurrent?: boolean | undefined + readonly batching?: boolean | "inherit" | undefined + readonly concurrentFinalizers?: boolean | undefined + } + ): (self: Effect) => Effect + ( + self: Effect, + that: Effect, + options?: { + readonly concurrent?: boolean | undefined + readonly batching?: boolean | "inherit" | undefined + readonly concurrentFinalizers?: boolean | undefined + } + ): Effect +} = fiberRuntime.zipRightOptions + +/** + * Combines two effects sequentially and applies a function to their results to + * produce a single value. + * + * **Details** + * + * This function runs two effects in sequence (or concurrently, if the `{ + * concurrent: true }` option is provided) and combines their results using a + * provided function. Unlike {@link zip}, which returns a tuple of the results, + * this function processes the results with a custom function to produce a + * single output. + * + * **Example** (Combining Effects with a Custom Function) + * + * ```ts + * import { Effect } from "effect" + * + * const task1 = Effect.succeed(1).pipe( + * Effect.delay("200 millis"), + * Effect.tap(Effect.log("task1 done")) + * ) + * const task2 = Effect.succeed("hello").pipe( + * Effect.delay("100 millis"), + * Effect.tap(Effect.log("task2 done")) + * ) + * + * const task3 = Effect.zipWith( + * task1, + * task2, + * // Combines results into a single value + * (number, string) => number + string.length + * ) + * + * Effect.runPromise(task3).then(console.log) + * // Output: + * // timestamp=... level=INFO fiber=#3 message="task1 done" + * // timestamp=... level=INFO fiber=#2 message="task2 done" + * // 6 + * ``` + * + * @since 2.0.0 + * @category Zipping + */ +export const zipWith: { + ( + that: Effect, + f: (a: A, b: A2) => B, + options?: { + readonly concurrent?: boolean | undefined + readonly batching?: boolean | "inherit" | undefined + readonly concurrentFinalizers?: boolean | undefined + } + ): (self: Effect) => Effect + ( + self: Effect, + that: Effect, + f: (a: A, b: A2) => B, + options?: { + readonly concurrent?: boolean | undefined + readonly batching?: boolean | "inherit" | undefined + readonly concurrentFinalizers?: boolean | undefined + } + ): Effect +} = fiberRuntime.zipWithOptions + +/** + * Applies the function produced by one effect to the value produced by another effect. + * + * **Details** + * + * This function combines two effects: + * - The first effect produces a function of type `(a: A) => B`. + * - The second effect produces a value of type `A`. + * + * Once both effects complete successfully, the function is applied to the value, resulting in an effect that produces a value of type `B`. + * + * @since 2.0.0 + */ +export const ap: { + (that: Effect): (self: Effect<(a: A) => B, E, R>) => Effect + (self: Effect<(a: A) => B, E, R>, that: Effect): Effect +} = dual( + 2, + (self: Effect<(a: A) => B, E, R>, that: Effect): Effect => + zipWith(self, that, (f, a) => f(a)) +) + +/** + * @category Requests & Batching + * @since 2.0.0 + */ +export const blocked: (blockedRequests: RequestBlock, _continue: Effect) => Blocked = core.blocked + +/** + * @category Requests & Batching + * @since 2.0.0 + */ +export const runRequestBlock: (blockedRequests: RequestBlock) => Effect = core.runRequestBlock + +/** + * @category Requests & Batching + * @since 2.0.0 + */ +export const step: (self: Effect) => Effect | Blocked, never, R> = core.step + +/** + * @since 2.0.0 + * @category Requests & Batching + */ +export const request: { + , Ds extends RequestResolver | Effect, any, any>>( + dataSource: Ds + ): ( + self: A + ) => Effect< + Request.Request.Success, + Request.Request.Error, + [Ds] extends [Effect] ? Effect.Context : never + > + < + Ds extends RequestResolver | Effect, any, any>, + A extends Request.Request + >( + self: A, + dataSource: Ds + ): Effect< + Request.Request.Success, + Request.Request.Error, + [Ds] extends [Effect] ? Effect.Context : never + > +} = dual((args) => Request.isRequest(args[0]), query.fromRequest) + +/** + * @since 2.0.0 + * @category Requests & Batching + */ +export const cacheRequestResult: >( + request: A, + result: Request.Request.Result +) => Effect = query.cacheRequest + +/** + * @since 2.0.0 + * @category Requests & Batching + */ +export const withRequestBatching: { + (requestBatching: boolean): (self: Effect) => Effect + (self: Effect, requestBatching: boolean): Effect +} = core.withRequestBatching + +/** + * @since 2.0.0 + * @category Requests & Batching + */ +export const withRequestCaching: { + (strategy: boolean): (self: Effect) => Effect + (self: Effect, strategy: boolean): Effect +} = query.withRequestCaching + +/** + * @since 2.0.0 + * @category Requests & Batching + */ +export const withRequestCache: { + (cache: Request.Cache): (self: Effect) => Effect + (self: Effect, cache: Request.Cache): Effect +} = query.withRequestCache + +/** + * @since 2.0.0 + * @category Tracing + */ +export const tracer: Effect = effect.tracer + +/** + * @since 2.0.0 + * @category Tracing + */ +export const tracerWith: (f: (tracer: Tracer.Tracer) => Effect) => Effect = + defaultServices.tracerWith + +/** + * @since 2.0.0 + * @category Tracing + */ +export const withTracer: { + (value: Tracer.Tracer): (effect: Effect) => Effect + (effect: Effect, value: Tracer.Tracer): Effect +} = defaultServices.withTracer + +/** + * @since 2.0.0 + * @category Tracing + */ +export const withTracerScoped: (value: Tracer.Tracer) => Effect = + fiberRuntime.withTracerScoped + +/** + * Disable the tracer for the given Effect. + * + * **Example** + * + * ```ts + * import { Effect } from "effect" + * + * Effect.succeed(42).pipe( + * Effect.withSpan("my-span"), + * // the span will not be registered with the tracer + * Effect.withTracerEnabled(false) + * ) + * ``` + * + * @since 2.0.0 + * @category Tracing + */ +export const withTracerEnabled: { + (enabled: boolean): (effect: Effect) => Effect + (effect: Effect, enabled: boolean): Effect +} = core.withTracerEnabled + +/** + * @since 2.0.0 + * @category Tracing + */ +export const withTracerTiming: { + (enabled: boolean): (effect: Effect) => Effect + (effect: Effect, enabled: boolean): Effect +} = core.withTracerTiming + +/** + * Adds annotations to each span in the effect for enhanced traceability. + * + * **Details** + * + * This function lets you attach key-value annotations to all spans generated + * during the execution of an effect. Annotations provide additional context, + * such as metadata or labels, which can help you understand and debug + * asynchronous workflows more effectively. + * + * You can either pass a single key-value pair or a record of key-value pairs to + * annotate the spans. These annotations can then be visualized in tracing tools + * that support span annotations. + * + * @since 2.0.0 + * @category Tracing + */ +export const annotateSpans: { + (key: string, value: unknown): (effect: Effect) => Effect + (values: Record): (effect: Effect) => Effect + (effect: Effect, key: string, value: unknown): Effect + (effect: Effect, values: Record): Effect +} = effect.annotateSpans + +/** + * Adds annotations to the currently active span for traceability. + * + * **Details** + * + * This function adds key-value annotations to the currently active span in the + * effect's trace. These annotations help provide more context about the + * operation being executed at a specific point in time. Unlike + * {@link annotateSpans}, which applies to all spans in an effect, this function + * focuses solely on the active span. + * + * You can either pass a single key-value pair or a record of key-value pairs to + * annotate the span. These annotations are useful for adding metadata to + * operations, especially in systems with detailed observability requirements. + * + * @since 2.0.0 + * @category Tracing + */ +export const annotateCurrentSpan: { + (key: string, value: unknown): Effect + (values: Record): Effect +} = effect.annotateCurrentSpan + +/** + * @since 2.0.0 + * @category Tracing + */ +export const currentSpan: Effect = effect.currentSpan + +/** + * @since 3.20.0 + * @category Tracing + */ +export const currentPropagatedSpan: Effect = effect.currentPropagatedSpan + +/** + * @since 2.0.0 + * @category Tracing + */ +export const currentParentSpan: Effect = effect.currentParentSpan + +/** + * @since 2.0.0 + * @category Tracing + */ +export const spanAnnotations: Effect> = effect.spanAnnotations + +/** + * @since 2.0.0 + * @category Tracing + */ +export const spanLinks: Effect> = effect.spanLinks + +/** + * For all spans in this effect, add a link with the provided span. + * + * @since 2.0.0 + * @category Tracing + */ +export const linkSpans: { + ( + span: Tracer.AnySpan, + attributes?: Record + ): (self: Effect) => Effect + ( + self: Effect, + span: Tracer.AnySpan, + attributes?: Record + ): Effect +} = effect.linkSpans + +/** + * Add span links to the current span. + * + * @since 3.14.0 + * @category Tracing + */ +export const linkSpanCurrent: { + (span: Tracer.AnySpan, attributes?: Readonly> | undefined): Effect + (links: ReadonlyArray): Effect +} = effect.linkSpanCurrent + +/** + * Create a new span for tracing. + * + * @since 2.0.0 + * @category Tracing + */ +export const makeSpan: ( + name: string, + options?: Tracer.SpanOptions +) => Effect = effect.makeSpan + +/** + * Create a new span for tracing, and automatically close it when the Scope + * finalizes. + * + * The span is not added to the current span stack, so no child spans will be + * created for it. + * + * @since 2.0.0 + * @category Tracing + */ +export const makeSpanScoped: ( + name: string, + options?: Tracer.SpanOptions | undefined +) => Effect = fiberRuntime.makeSpanScoped + +/** + * Create a new span for tracing, and automatically close it when the effect + * completes. + * + * The span is not added to the current span stack, so no child spans will be + * created for it. + * + * @since 2.0.0 + * @category Tracing + */ +export const useSpan: { + (name: string, evaluate: (span: Tracer.Span) => Effect): Effect + ( + name: string, + options: Tracer.SpanOptions, + evaluate: (span: Tracer.Span) => Effect + ): Effect +} = effect.useSpan + +/** + * Wraps the effect with a new span for tracing. + * + * @since 2.0.0 + * @category Tracing + */ +export const withSpan: { + ( + name: string, + options?: Tracer.SpanOptions | undefined + ): (self: Effect) => Effect> + ( + self: Effect, + name: string, + options?: Tracer.SpanOptions | undefined + ): Effect> +} = effect.withSpan + +/** + * Wraps a function that returns an effect with a new span for tracing. + * + * @since 3.2.0 + * @category Models + */ +export interface FunctionWithSpanOptions { + readonly name: string + readonly attributes?: Record | undefined + readonly links?: ReadonlyArray | undefined + readonly parent?: Tracer.AnySpan | undefined + readonly root?: boolean | undefined + readonly context?: Context.Context | undefined + readonly kind?: Tracer.SpanKind | undefined +} + +/** + * Wraps a function that returns an effect with a new span for tracing. + * + * **Example** + * + * ```ts + * import { Effect } from "effect" + * + * const getTodo = Effect.functionWithSpan({ + * body: (id: number) => Effect.succeed(`Got todo ${id}!`), + * options: (id) => ({ + * name: `getTodo-${id}`, + * attributes: { id } + * }) + * }) + * ``` + * + * @since 3.2.0 + * @category Tracing + */ +export const functionWithSpan: , Ret extends Effect>( + options: { + readonly body: (...args: Args) => Ret + readonly options: FunctionWithSpanOptions | ((...args: Args) => FunctionWithSpanOptions) + readonly captureStackTrace?: boolean | undefined + } +) => (...args: Args) => Unify.Unify = effect.functionWithSpan + +/** + * Wraps the effect with a new span for tracing. + * + * The span is ended when the Scope is finalized. + * + * @since 2.0.0 + * @category Tracing + */ +export const withSpanScoped: { + ( + name: string, + options?: Tracer.SpanOptions + ): (self: Effect) => Effect | Scope.Scope> + ( + self: Effect, + name: string, + options?: Tracer.SpanOptions + ): Effect | Scope.Scope> +} = fiberRuntime.withSpanScoped + +/** + * Adds the provided span to the current span stack. + * + * @since 2.0.0 + * @category Tracing + */ +export const withParentSpan: { + (span: Tracer.AnySpan): (self: Effect) => Effect> + (self: Effect, span: Tracer.AnySpan): Effect> +} = effect.withParentSpan + +/** + * Safely handles nullable values by creating an effect that fails for `null` or + * `undefined`. + * + * **Details** + * + * This function ensures that an input value is non-null and non-undefined + * before processing it. If the value is valid, the effect succeeds with the + * value. If the value is `null` or `undefined`, the effect fails with a + * `NoSuchElementException`. This is particularly useful for avoiding + * null-related errors by clearly separating valid values from invalid ones in + * effectful computations. + * + * The failure with `NoSuchElementException` allows you to explicitly handle + * cases where a value is expected but not provided, leading to safer and more + * predictable code. + * + * **When to Use** + * + * Use this function when working with values that may be `null` or `undefined` + * and you want to ensure that only non-null values are processed. It helps + * enforce null-safety and makes error handling more explicit. + * + * **Example** + * + * ```ts + * import { Effect } from "effect" + * + * // ┌─── Effect + * // ▼ + * const maybe1 = Effect.fromNullable(1) + * + * Effect.runPromiseExit(maybe1).then(console.log) + * // Output: + * // { _id: 'Exit', _tag: 'Success', value: 1 } + * + * // ┌─── Effect + * // ▼ + * const maybe2 = Effect.fromNullable(null as number | null) + * + * Effect.runPromiseExit(maybe2).then(console.log) + * // Output: + * // { + * // _id: 'Exit', + * // _tag: 'Failure', + * // cause: { + * // _id: 'Cause', + * // _tag: 'Fail', + * // failure: { _tag: 'NoSuchElementException' } + * // } + * // } + * ``` + * + * @since 2.0.0 + * @category Optional Wrapping & Unwrapping + */ +export const fromNullable: (value: A) => Effect, Cause.NoSuchElementException> = effect.fromNullable + +/** + * Converts an effect that may fail with a `NoSuchElementException` into an + * effect that succeeds with an `Option`. + * + * **Details** + * + * This function transforms an effect that might fail with + * `Cause.NoSuchElementException` into an effect that succeeds with an `Option` + * type. If the original effect succeeds, its value is wrapped in `Option.some`. + * If it fails specifically due to a `NoSuchElementException`, the failure is + * mapped to `Option.none`. Other types of failures remain unchanged and are + * passed through as they are. + * + * This is useful when working with effects where you want to gracefully handle + * the absence of a value while preserving other potential failures. + * + * **When to Use** + * + * Use this function when you need to handle missing values as `Option.none` + * rather than throwing or propagating errors like `NoSuchElementException`. + * It’s ideal for scenarios where you want to explicitly represent optionality + * in a type-safe way while retaining other failure information. + * + * **Example** + * + * ```ts + * import { Effect } from "effect" + * + * // ┌─── Effect + * // ▼ + * const maybe1 = Effect.fromNullable(1) + * + * // ┌─── Effect, never, never> + * // ▼ + * const option1 = Effect.optionFromOptional(maybe1) + * + * Effect.runPromise(option1).then(console.log) + * // Output: { _id: 'Option', _tag: 'Some', value: 1 } + * + * // ┌─── Effect + * // ▼ + * const maybe2 = Effect.fromNullable(null as number | null) + * + * // ┌─── Effect, never, never> + * // ▼ + * const option2 = Effect.optionFromOptional(maybe2) + * + * Effect.runPromise(option2).then(console.log) + * // Output: { _tag: 'None' } + * ``` + * + * @since 2.0.0 + * @category Optional Wrapping & Unwrapping + */ +export const optionFromOptional: ( + self: Effect +) => Effect, Exclude, R> = effect.optionFromOptional + +/** + * Converts an `Option` of an `Effect` into an `Effect` of an `Option`. + * + * **Details** + * + * This function transforms an `Option>` into an + * `Effect, E, R>`. If the `Option` is `None`, the resulting `Effect` + * will immediately succeed with a `None` value. If the `Option` is `Some`, the + * inner `Effect` will be executed, and its result wrapped in a `Some`. + * + * **Example** + * + * ```ts + * import { Effect, Option } from "effect" + * + * // ┌─── Option> + * // ▼ + * const maybe = Option.some(Effect.succeed(42)) + * + * // ┌─── Effect, never, never> + * // ▼ + * const result = Effect.transposeOption(maybe) + * + * console.log(Effect.runSync(result)) + * // Output: { _id: 'Option', _tag: 'Some', value: 42 } + * ``` + * + * @since 3.13.0 + * @category Optional Wrapping & Unwrapping + */ +export const transposeOption = ( + self: Option.Option> +): Effect, E, R> => { + return option_.isNone(self) ? succeedNone : map(self.value, option_.some) +} + +/** + * Applies an `Effect` on an `Option` and transposes the result. + * + * **Details** + * + * If the `Option` is `None`, the resulting `Effect` will immediately succeed with a `None` value. + * If the `Option` is `Some`, the effectful operation will be executed on the inner value, and its result wrapped in a `Some`. + * + * @example + * ```ts + * import { Effect, Option, pipe } from "effect" + * + * // ┌─── Effect, never, never>> + * // ▼ + * const noneResult = pipe( + * Option.none(), + * Effect.transposeMapOption(() => Effect.succeed(42)) // will not be executed + * ) + * console.log(Effect.runSync(noneResult)) + * // Output: { _id: 'Option', _tag: 'None' } + * + * // ┌─── Effect, never, never>> + * // ▼ + * const someSuccessResult = pipe( + * Option.some(42), + * Effect.transposeMapOption((value) => Effect.succeed(value * 2)) + * ) + * console.log(Effect.runSync(someSuccessResult)) + * // Output: { _id: 'Option', _tag: 'Some', value: 84 } + * ``` + * + * @since 3.14.0 + * @category Optional Wrapping & Unwrapping + */ +export const transposeMapOption = dual< + ( + f: (self: A) => Effect + ) => (self: Option.Option) => Effect, E, R>, + ( + self: Option.Option, + f: (self: A) => Effect + ) => Effect, E, R> +>(2, (self, f) => option_.isNone(self) ? succeedNone : map(f(self.value), option_.some)) + +/** + * @since 2.0.0 + * @category Models + */ +export declare namespace Tag { + /** + * @since 2.0.0 + * @category Models + */ + export interface ProhibitedType { + Service?: `property "Service" is forbidden` + Identifier?: `property "Identifier" is forbidden` + _op?: `property "_op" is forbidden` + of?: `property "of" is forbidden` + context?: `property "context" is forbidden` + key?: `property "key" is forbidden` + stack?: `property "stack" is forbidden` + name?: `property "name" is forbidden` + pipe?: `property "pipe" is forbidden` + use?: `property "use" is forbidden` + } + + /** + * @since 2.0.0 + * @category Models + */ + export type AllowedType = (Record & ProhibitedType) | string | number | symbol + + /** + * @since 3.9.0 + * @category Models + */ + export type Proxy = { + [ + k in keyof Type as Type[k] extends ((...args: infer Args extends ReadonlyArray) => infer Ret) ? + ((...args: Readonly) => Ret) extends Type[k] ? k : never + : k + ]: Type[k] extends (...args: infer Args extends ReadonlyArray) => Effect ? + (...args: Readonly) => Effect + : Type[k] extends (...args: infer Args extends ReadonlyArray) => Promise ? + (...args: Readonly) => Effect + : Type[k] extends (...args: infer Args extends ReadonlyArray) => infer A ? + (...args: Readonly) => Effect + : Type[k] extends Effect ? Effect + : Effect + } +} + +const makeTagProxy = (TagClass: Context.Tag & Record) => { + const cache = new Map() + return new Proxy(TagClass, { + get(target: any, prop: any, receiver) { + if (prop in target) { + return Reflect.get(target, prop, receiver) + } + if (cache.has(prop)) { + return cache.get(prop) + } + const fn = (...args: Array) => + core.andThen(target, (s: any) => { + if (typeof s[prop] === "function") { + cache.set(prop, (...args: Array) => core.andThen(target, (s: any) => s[prop](...args))) + return s[prop](...args) + } + cache.set(prop, core.andThen(target, (s: any) => s[prop])) + return s[prop] + }) + const cn = core.andThen(target, (s: any) => s[prop]) + // @effect-diagnostics-next-line floatingEffect:off + Object.assign(fn, cn) + const apply = fn.apply + const bind = fn.bind + const call = fn.call + const proto = Object.setPrototypeOf({}, Object.getPrototypeOf(cn)) + proto.apply = apply + proto.bind = bind + proto.call = call + Object.setPrototypeOf(fn, proto) + cache.set(prop, fn) + return fn + } + }) +} + +/** + * Creates a unique tag for a dependency, embedding the service's methods as + * static properties. + * + * **Details** + * + * This function allows you to define a `Tag` for a service or dependency in + * your application. The `Tag` not only acts as an identifier but also provides + * direct access to the service's methods via static properties. This makes it + * easier to access and use the service in your code without manually managing + * contexts. + * + * In the example below, the fields of the service (in this case, the `notify` + * method) are turned into static properties of the Notifications class, making + * it easier to access them. + * + * **Example** + * + * ```ts + * import { Effect } from "effect" + * + * class Notifications extends Effect.Tag("Notifications")< + * Notifications, + * { readonly notify: (message: string) => Effect.Effect } + * >() {} + * + * // Create an effect that depends on the Notifications service + * const action = Notifications.notify("Hello, world!") + * ``` + * + * @since 2.0.0 + * @category Context + */ +export const Tag: (id: Id) => < + Self, + Type extends Tag.AllowedType +>() => + & Context.TagClass + & (Type extends Record ? Tag.Proxy : {}) + & { + use: ( + body: (_: Type) => X + ) => [X] extends [Effect] ? Effect + : [X] extends [PromiseLike] ? Effect + : Effect + } = (id) => () => { + const limit = Error.stackTraceLimit + Error.stackTraceLimit = 2 + const creationError = new Error() + Error.stackTraceLimit = limit + function TagClass() {} + Object.setPrototypeOf(TagClass, TagProto) + TagClass.key = id + Object.defineProperty(TagClass, "use", { + get() { + return (body: (_: any) => any) => core.andThen(this, body) + } + }) + Object.defineProperty(TagClass, "stack", { + get() { + return creationError.stack + } + }) + return makeTagProxy(TagClass as any) + } + +type MissingSelfGeneric = `Missing \`Self\` generic - use \`class Self extends Effect.Service()...\`` + +/** + * Simplifies the creation and management of services in Effect by defining both + * a `Tag` and a `Layer`. + * + * **Details** + * + * This function allows you to streamline the creation of services by combining + * the definition of a `Context.Tag` and a `Layer` in a single step. It supports + * various ways of providing the service implementation: + * - Using an `effect` to define the service dynamically. + * - Using `sync` or `succeed` to define the service statically. + * - Using `scoped` to create services with lifecycle management. + * + * It also allows you to specify dependencies for the service, which will be + * provided automatically when the service is used. Accessors can be optionally + * generated for the service, making it more convenient to use. + * + * **Example** + * + * ```ts + * import { Effect } from 'effect'; + * + * class Prefix extends Effect.Service()("Prefix", { + * sync: () => ({ prefix: "PRE" }) + * }) {} + * + * class Logger extends Effect.Service()("Logger", { + * accessors: true, + * effect: Effect.gen(function* () { + * const { prefix } = yield* Prefix + * return { + * info: (message: string) => + * Effect.sync(() => { + * console.log(`[${prefix}][${message}]`) + * }) + * } + * }), + * dependencies: [Prefix.Default] + * }) {} + * ``` + * + * @since 3.9.0 + * @category Context + * @experimental might be up for breaking changes + */ +export const Service: () => [Self] extends [never] ? MissingSelfGeneric : { + < + const Key extends string, + const Make extends + | { + readonly scoped: + | Effect, any, any> + | ((...args: any) => Effect, any, any>) + readonly dependencies?: ReadonlyArray + readonly accessors?: boolean + /** @deprecated */ + readonly ಠ_ಠ: never + } + | { + readonly effect: + | Effect, any, any> + | ((...args: any) => Effect, any, any>) + readonly dependencies?: ReadonlyArray + readonly accessors?: boolean + /** @deprecated */ + readonly ಠ_ಠ: never + } + | { + readonly sync: LazyArg> + readonly dependencies?: ReadonlyArray + readonly accessors?: boolean + /** @deprecated */ + readonly ಠ_ಠ: never + } + | { + readonly succeed: Service.AllowedType + readonly dependencies?: ReadonlyArray + readonly accessors?: boolean + /** @deprecated */ + readonly ಠ_ಠ: never + } + >( + key: Key, + make: Make + ): Service.Class + < + const Key extends string, + const Make extends NoExcessProperties<{ + readonly scoped: + | Effect, any, any> + | ((...args: any) => Effect, any, any>) + readonly dependencies?: ReadonlyArray + readonly accessors?: boolean + }, Make> + >( + key: Key, + make: Make + ): Service.Class + < + const Key extends string, + const Make extends NoExcessProperties<{ + readonly effect: + | Effect, any, any> + | ((...args: any) => Effect, any, any>) + readonly dependencies?: ReadonlyArray + readonly accessors?: boolean + }, Make> + >( + key: Key, + make: Make + ): Service.Class + < + const Key extends string, + const Make extends NoExcessProperties<{ + readonly sync: LazyArg> + readonly dependencies?: ReadonlyArray + readonly accessors?: boolean + }, Make> + >( + key: Key, + make: Make + ): Service.Class + < + const Key extends string, + const Make extends NoExcessProperties<{ + readonly succeed: Service.AllowedType + readonly dependencies?: ReadonlyArray + readonly accessors?: boolean + }, Make> + >( + key: Key, + make: Make + ): Service.Class +} = function() { + return function() { + const [id, maker] = arguments + const proxy = "accessors" in maker ? maker["accessors"] : false + const limit = Error.stackTraceLimit + Error.stackTraceLimit = 2 + const creationError = new Error() + Error.stackTraceLimit = limit + + let patchState: "unchecked" | "plain" | "patched" = "unchecked" + const TagClass: any = function(this: any, service: any) { + if (patchState === "unchecked") { + const proto = Object.getPrototypeOf(service) + if (proto === Object.prototype || proto === null) { + patchState = "plain" + } else { + const selfProto = Object.getPrototypeOf(this) + Object.setPrototypeOf(selfProto, proto) + patchState = "patched" + } + } + if (patchState === "plain") { + Object.assign(this, service) + } else if (patchState === "patched") { + Object.setPrototypeOf(service, Object.getPrototypeOf(this)) + return service + } + } + + TagClass.prototype._tag = id + Object.defineProperty(TagClass, "make", { + get() { + return (service: any) => new this(service) + } + }) + Object.defineProperty(TagClass, "use", { + get() { + return (body: any) => core.andThen(this, body) + } + }) + TagClass.key = id + + Object.assign(TagClass, TagProto) + + Object.defineProperty(TagClass, "stack", { + get() { + return creationError.stack + } + }) + + const hasDeps = "dependencies" in maker && maker.dependencies.length > 0 + const layerName = hasDeps ? "DefaultWithoutDependencies" : "Default" + let layerCache: Layer.Layer.Any | undefined + let isFunction = false + if ("effect" in maker) { + isFunction = typeof maker.effect === "function" + Object.defineProperty(TagClass, layerName, { + get(this: any) { + if (isFunction) { + return function(this: typeof TagClass) { + return layer.fromEffect(TagClass, map(maker.effect.apply(null, arguments), (_) => new this(_))) + }.bind(this) + } + return layerCache ??= layer.fromEffect(TagClass, map(maker.effect, (_) => new this(_))) + } + }) + } else if ("scoped" in maker) { + isFunction = typeof maker.scoped === "function" + Object.defineProperty(TagClass, layerName, { + get(this: any) { + if (isFunction) { + return function(this: typeof TagClass) { + return layer.scoped(TagClass, map(maker.scoped.apply(null, arguments), (_) => new this(_))) + }.bind(this) + } + return layerCache ??= layer.scoped(TagClass, map(maker.scoped, (_) => new this(_))) + } + }) + } else if ("sync" in maker) { + Object.defineProperty(TagClass, layerName, { + get(this: any) { + return layerCache ??= layer.sync(TagClass, () => new this(maker.sync())) + } + }) + } else { + Object.defineProperty(TagClass, layerName, { + get(this: any) { + return layerCache ??= layer.succeed(TagClass, new this(maker.succeed)) + } + }) + } + + if (hasDeps) { + let layerWithDepsCache: Layer.Layer.Any | undefined + Object.defineProperty(TagClass, "Default", { + get(this: any) { + if (isFunction) { + return function(this: typeof TagClass) { + return layer.provide( + this.DefaultWithoutDependencies.apply(null, arguments), + maker.dependencies + ) + } + } + return layerWithDepsCache ??= layer.provide( + this.DefaultWithoutDependencies, + maker.dependencies + ) + } + }) + } + + return proxy === true ? makeTagProxy(TagClass) : TagClass + } +} as any + +/** + * @since 3.9.0 + * @category Context + */ +export declare namespace Service { + /** + * @since 3.9.0 + */ + export interface ProhibitedType { + Service?: `property "Service" is forbidden` + Identifier?: `property "Identifier" is forbidden` + Default?: `property "Default" is forbidden` + DefaultWithoutDependencies?: `property "DefaultWithoutDependencies" is forbidden` + _op_layer?: `property "_op_layer" is forbidden` + _op?: `property "_op" is forbidden` + of?: `property "of" is forbidden` + make?: `property "make" is forbidden` + context?: `property "context" is forbidden` + key?: `property "key" is forbidden` + stack?: `property "stack" is forbidden` + name?: `property "name" is forbidden` + pipe?: `property "pipe" is forbidden` + use?: `property "use" is forbidden` + _tag?: `property "_tag" is forbidden` + } + + /** + * @since 3.9.0 + */ + export type AllowedType = MakeAccessors extends true ? + & Record + & { + readonly [K in Extract, keyof ProhibitedType>]: K extends "_tag" ? Key + : ProhibitedType[K] + } + : Record & { readonly _tag?: Key } + + /** + * @since 3.9.0 + */ + export type Class< + Self, + Key extends string, + Make + > = + & { + new(_: MakeService): MakeService & { + readonly _tag: Key + } + readonly use: ( + body: (_: Self) => X + ) => [X] extends [Effect] ? Effect + : [X] extends [PromiseLike] ? Effect + : Effect + readonly make: (_: MakeService) => Self + } + & Context.Tag + & { key: Key } + & (MakeAccessors extends true ? Tag.Proxy> : {}) + & (MakeDeps extends never ? { + readonly Default: HasArguments extends true ? + (...args: MakeArguments) => Layer.Layer, MakeContext> + : Layer.Layer, MakeContext> + } : + { + readonly DefaultWithoutDependencies: HasArguments extends true + ? (...args: MakeArguments) => Layer.Layer, MakeContext> + : Layer.Layer, MakeContext> + + readonly Default: HasArguments extends true ? (...args: MakeArguments) => Layer.Layer< + Self, + MakeError | MakeDepsE, + | Exclude, MakeDepsOut> + | MakeDepsIn + > : + Layer.Layer< + Self, + MakeError | MakeDepsE, + | Exclude, MakeDepsOut> + | MakeDepsIn + > + }) + + /** + * @since 3.9.0 + */ + export type MakeService = Make extends { readonly effect: Effect } ? _A + : Make extends { readonly scoped: Effect } ? _A + : Make extends { readonly effect: (...args: infer _Args) => Effect } ? _A + : Make extends { readonly scoped: (...args: infer _Args) => Effect } ? _A + : Make extends { readonly sync: LazyArg } ? A + : Make extends { readonly succeed: infer A } ? A + : never + + /** + * @since 3.9.0 + */ + export type MakeError = Make extends { readonly effect: Effect } ? _E + : Make extends { readonly scoped: Effect } ? _E + : Make extends { readonly effect: (...args: infer _Args) => Effect } ? _E + : Make extends { readonly scoped: (...args: infer _Args) => Effect } ? _E + : never + + /** + * @since 3.9.0 + */ + export type MakeContext = Make extends { readonly effect: Effect } ? _R + : Make extends { readonly scoped: Effect } ? Exclude<_R, Scope.Scope> + : Make extends { readonly effect: (...args: infer _Args) => Effect } ? _R + : Make extends { readonly scoped: (...args: infer _Args) => Effect } ? + Exclude<_R, Scope.Scope> + : never + + /** + * @since 3.9.0 + */ + export type MakeDeps = Make extends { readonly dependencies: ReadonlyArray } + ? Make["dependencies"][number] + : never + + /** + * @since 3.9.0 + */ + export type MakeDepsOut = Contravariant.Type[Layer.LayerTypeId]["_ROut"]> + + /** + * @since 3.9.0 + */ + export type MakeDepsE = Covariant.Type[Layer.LayerTypeId]["_E"]> + + /** + * @since 3.9.0 + */ + export type MakeDepsIn = Covariant.Type[Layer.LayerTypeId]["_RIn"]> + + /** + * @since 3.9.0 + */ + export type MakeAccessors = Make extends { readonly accessors: true } ? true + : false + + /** + * @since 3.16.0 + */ + export type MakeArguments = Make extends + { readonly effect: (...args: infer Args) => Effect } ? Args + : Make extends { readonly scoped: (...args: infer Args) => Effect } ? Args + : never + + /** + * @since 3.16.0 + */ + export type HasArguments = Make extends { + readonly scoped: (...args: ReadonlyArray) => Effect + } ? true : + Make extends { + readonly effect: (...args: ReadonlyArray) => Effect + } ? true : + false +} + +/** + * @since 3.11.0 + * @category Models + */ +export namespace fn { + /** + * @since 3.19.0 + * @category Models + */ + export type Return = Generator>, A, any> + /** + * @since 3.11.0 + * @category Models + */ + export type Gen = { + >, AEff, Args extends Array>( + body: (...args: Args) => Generator + ): (...args: Args) => Effect< + AEff, + [Eff] extends [never] ? never : [Eff] extends [YieldWrap>] ? E : never, + [Eff] extends [never] ? never : [Eff] extends [YieldWrap>] ? R : never + > + < + Eff extends YieldWrap>, + AEff, + Args extends Array, + A extends Effect + >( + body: (...args: Args) => Generator, + a: ( + _: Effect< + AEff, + [Eff] extends [never] ? never : [Eff] extends [YieldWrap>] ? E : never, + [Eff] extends [never] ? never : [Eff] extends [YieldWrap>] ? R : never + >, + ...args: NoInfer + ) => A + ): (...args: Args) => Effect.AsEffect + < + Eff extends YieldWrap>, + AEff, + Args extends Array, + A, + B extends Effect + >( + body: (...args: Args) => Generator, + a: ( + _: Effect< + AEff, + [Eff] extends [never] ? never : [Eff] extends [YieldWrap>] ? E : never, + [Eff] extends [never] ? never : [Eff] extends [YieldWrap>] ? R : never + >, + ...args: NoInfer + ) => A, + b: (_: A, ...args: NoInfer) => B + ): (...args: Args) => Effect.AsEffect + < + Eff extends YieldWrap>, + AEff, + Args extends Array, + A, + B, + C extends Effect + >( + body: (...args: Args) => Generator, + a: ( + _: Effect< + AEff, + [Eff] extends [never] ? never : [Eff] extends [YieldWrap>] ? E : never, + [Eff] extends [never] ? never : [Eff] extends [YieldWrap>] ? R : never + >, + ...args: NoInfer + ) => A, + b: (_: A, ...args: NoInfer) => B, + c: (_: B, ...args: NoInfer) => C + ): (...args: Args) => Effect.AsEffect + < + Eff extends YieldWrap>, + AEff, + Args extends Array, + A, + B, + C, + D extends Effect + >( + body: (...args: Args) => Generator, + a: ( + _: Effect< + AEff, + [Eff] extends [never] ? never : [Eff] extends [YieldWrap>] ? E : never, + [Eff] extends [never] ? never : [Eff] extends [YieldWrap>] ? R : never + >, + ...args: NoInfer + ) => A, + b: (_: A, ...args: NoInfer) => B, + c: (_: B, ...args: NoInfer) => C, + d: (_: C, ...args: NoInfer) => D + ): (...args: Args) => Effect.AsEffect + < + Eff extends YieldWrap>, + AEff, + Args extends Array, + A, + B, + C, + D, + E extends Effect + >( + body: (...args: Args) => Generator, + a: ( + _: Effect< + AEff, + [Eff] extends [never] ? never : [Eff] extends [YieldWrap>] ? E : never, + [Eff] extends [never] ? never : [Eff] extends [YieldWrap>] ? R : never + >, + ...args: NoInfer + ) => A, + b: (_: A, ...args: NoInfer) => B, + c: (_: B, ...args: NoInfer) => C, + d: (_: C, ...args: NoInfer) => D, + e: (_: D, ...args: NoInfer) => E + ): (...args: Args) => Effect.AsEffect + < + Eff extends YieldWrap>, + AEff, + Args extends Array, + A, + B, + C, + D, + E, + F extends Effect + >( + body: (...args: Args) => Generator, + a: ( + _: Effect< + AEff, + [Eff] extends [never] ? never : [Eff] extends [YieldWrap>] ? E : never, + [Eff] extends [never] ? never : [Eff] extends [YieldWrap>] ? R : never + >, + ...args: NoInfer + ) => A, + b: (_: A, ...args: NoInfer) => B, + c: (_: B, ...args: NoInfer) => C, + d: (_: C, ...args: NoInfer) => D, + e: (_: D, ...args: NoInfer) => E, + f: (_: E, ...args: NoInfer) => F + ): (...args: Args) => Effect.AsEffect + < + Eff extends YieldWrap>, + AEff, + Args extends Array, + A, + B, + C, + D, + E, + F, + G extends Effect + >( + body: (...args: Args) => Generator, + a: ( + _: Effect< + AEff, + [Eff] extends [never] ? never : [Eff] extends [YieldWrap>] ? E : never, + [Eff] extends [never] ? never : [Eff] extends [YieldWrap>] ? R : never + >, + ...args: NoInfer + ) => A, + b: (_: A, ...args: NoInfer) => B, + c: (_: B, ...args: NoInfer) => C, + d: (_: C, ...args: NoInfer) => D, + e: (_: D, ...args: NoInfer) => E, + f: (_: E, ...args: NoInfer) => F, + g: (_: F, ...args: NoInfer) => G + ): (...args: Args) => Effect.AsEffect + < + Eff extends YieldWrap>, + AEff, + Args extends Array, + A, + B, + C, + D, + E, + F, + G, + H extends Effect + >( + body: (...args: Args) => Generator, + a: ( + _: Effect< + AEff, + [Eff] extends [never] ? never : [Eff] extends [YieldWrap>] ? E : never, + [Eff] extends [never] ? never : [Eff] extends [YieldWrap>] ? R : never + >, + ...args: NoInfer + ) => A, + b: (_: A, ...args: NoInfer) => B, + c: (_: B, ...args: NoInfer) => C, + d: (_: C, ...args: NoInfer) => D, + e: (_: D, ...args: NoInfer) => E, + f: (_: E, ...args: NoInfer) => F, + g: (_: F, ...args: NoInfer) => G, + h: (_: G, ...args: NoInfer) => H + ): (...args: Args) => Effect.AsEffect + < + Eff extends YieldWrap>, + AEff, + Args extends Array, + A, + B, + C, + D, + E, + F, + G, + H, + I extends Effect + >( + body: (...args: Args) => Generator, + a: ( + _: Effect< + AEff, + [Eff] extends [never] ? never : [Eff] extends [YieldWrap>] ? E : never, + [Eff] extends [never] ? never : [Eff] extends [YieldWrap>] ? R : never + >, + ...args: NoInfer + ) => A, + b: (_: A, ...args: NoInfer) => B, + c: (_: B, ...args: NoInfer) => C, + d: (_: C, ...args: NoInfer) => D, + e: (_: D, ...args: NoInfer) => E, + f: (_: E, ...args: NoInfer) => F, + g: (_: F, ...args: NoInfer) => G, + h: (_: G, ...args: NoInfer) => H, + i: (_: H, ...args: NoInfer) => I + ): (...args: Args) => Effect.AsEffect + } + + /** + * @since 3.11.0 + * @category Models + */ + export type NonGen = { + , Args extends Array>( + body: (...args: Args) => Eff + ): (...args: Args) => Effect.AsEffect + , A, Args extends Array>( + body: (...args: Args) => A, + a: (_: A, ...args: NoInfer) => Eff + ): (...args: Args) => Effect.AsEffect + , A, B, Args extends Array>( + body: (...args: Args) => A, + a: (_: A, ...args: NoInfer) => B, + b: (_: B, ...args: NoInfer) => Eff + ): (...args: Args) => Effect.AsEffect + , A, B, C, Args extends Array>( + body: (...args: Args) => A, + a: (_: A, ...args: NoInfer) => B, + b: (_: B, ...args: NoInfer) => C, + c: (_: C, ...args: NoInfer) => Eff + ): (...args: Args) => Effect.AsEffect + , A, B, C, D, Args extends Array>( + body: (...args: Args) => A, + a: (_: A, ...args: NoInfer) => B, + b: (_: B, ...args: NoInfer) => C, + c: (_: C, ...args: NoInfer) => D, + d: (_: D, ...args: NoInfer) => Eff + ): (...args: Args) => Effect.AsEffect + , A, B, C, D, E, Args extends Array>( + body: (...args: Args) => A, + a: (_: A, ...args: NoInfer) => B, + b: (_: B, ...args: NoInfer) => C, + c: (_: C, ...args: NoInfer) => D, + d: (_: D, ...args: NoInfer) => E, + e: (_: E, ...args: NoInfer) => Eff + ): (...args: Args) => Effect.AsEffect + , A, B, C, D, E, F, Args extends Array>( + body: (...args: Args) => A, + a: (_: A, ...args: NoInfer) => B, + b: (_: B, ...args: NoInfer) => C, + c: (_: C, ...args: NoInfer) => D, + d: (_: D, ...args: NoInfer) => E, + e: (_: E, ...args: NoInfer) => F, + f: (_: F, ...args: NoInfer) => Eff + ): (...args: Args) => Effect.AsEffect + , A, B, C, D, E, F, G, Args extends Array>( + body: (...args: Args) => A, + a: (_: A, ...args: NoInfer) => B, + b: (_: B, ...args: NoInfer) => C, + c: (_: C, ...args: NoInfer) => D, + d: (_: D, ...args: NoInfer) => E, + e: (_: E, ...args: NoInfer) => F, + f: (_: F, ...args: NoInfer) => G, + g: (_: G, ...args: NoInfer) => Eff + ): (...args: Args) => Effect.AsEffect + , A, B, C, D, E, F, G, H, Args extends Array>( + body: (...args: Args) => A, + a: (_: A, ...args: NoInfer) => B, + b: (_: B, ...args: NoInfer) => C, + c: (_: C, ...args: NoInfer) => D, + d: (_: D, ...args: NoInfer) => E, + e: (_: E, ...args: NoInfer) => F, + f: (_: F, ...args: NoInfer) => G, + g: (_: G, ...args: NoInfer) => H, + h: (_: H, ...args: NoInfer) => Eff + ): (...args: Args) => Effect.AsEffect + , A, B, C, D, E, F, G, H, I, Args extends Array>( + body: (...args: Args) => A, + a: (_: A, ...args: NoInfer) => B, + b: (_: B, ...args: NoInfer) => C, + c: (_: C, ...args: NoInfer) => D, + d: (_: D, ...args: NoInfer) => E, + e: (_: E, ...args: NoInfer) => F, + f: (_: F, ...args: NoInfer) => G, + g: (_: G, ...args: NoInfer) => H, + h: (_: H, ...args: NoInfer) => I, + i: (_: H, ...args: NoInfer) => Eff + ): (...args: Args) => Effect.AsEffect + } + + /** + * @since 3.11.0 + * @category Models + */ + export type Untraced = { + >, AEff, Args extends Array>( + body: (...args: Args) => Generator + ): (...args: Args) => Effect< + AEff, + [Eff] extends [never] ? never : [Eff] extends [YieldWrap>] ? E : never, + [Eff] extends [never] ? never : [Eff] extends [YieldWrap>] ? R : never + > + >, AEff, Args extends Array, A>( + body: (...args: Args) => Generator, + a: ( + _: Effect< + AEff, + [Eff] extends [never] ? never : [Eff] extends [YieldWrap>] ? E : never, + [Eff] extends [never] ? never : [Eff] extends [YieldWrap>] ? R : never + >, + ...args: NoInfer + ) => A + ): (...args: Args) => A + >, AEff, Args extends Array, A, B>( + body: (...args: Args) => Generator, + a: ( + _: Effect< + AEff, + [Eff] extends [never] ? never : [Eff] extends [YieldWrap>] ? E : never, + [Eff] extends [never] ? never : [Eff] extends [YieldWrap>] ? R : never + >, + ...args: NoInfer + ) => A, + b: (_: A, ...args: NoInfer) => B + ): (...args: Args) => B + < + Eff extends YieldWrap>, + AEff, + Args extends Array, + A, + B, + C + >( + body: (...args: Args) => Generator, + a: ( + _: Effect< + AEff, + [Eff] extends [never] ? never : [Eff] extends [YieldWrap>] ? E : never, + [Eff] extends [never] ? never : [Eff] extends [YieldWrap>] ? R : never + >, + ...args: NoInfer + ) => A, + b: (_: A, ...args: NoInfer) => B, + c: (_: B, ...args: NoInfer) => C + ): (...args: Args) => C + < + Eff extends YieldWrap>, + AEff, + Args extends Array, + A, + B, + C, + D + >( + body: (...args: Args) => Generator, + a: ( + _: Effect< + AEff, + [Eff] extends [never] ? never : [Eff] extends [YieldWrap>] ? E : never, + [Eff] extends [never] ? never : [Eff] extends [YieldWrap>] ? R : never + >, + ...args: NoInfer + ) => A, + b: (_: A, ...args: NoInfer) => B, + c: (_: B, ...args: NoInfer) => C, + d: (_: C, ...args: NoInfer) => D + ): (...args: Args) => D + < + Eff extends YieldWrap>, + AEff, + Args extends Array, + A, + B, + C, + D, + E + >( + body: (...args: Args) => Generator, + a: ( + _: Effect< + AEff, + [Eff] extends [never] ? never : [Eff] extends [YieldWrap>] ? E : never, + [Eff] extends [never] ? never : [Eff] extends [YieldWrap>] ? R : never + >, + ...args: NoInfer + ) => A, + b: (_: A, ...args: NoInfer) => B, + c: (_: B, ...args: NoInfer) => C, + d: (_: C, ...args: NoInfer) => D, + e: (_: D, ...args: NoInfer) => E + ): (...args: Args) => E + < + Eff extends YieldWrap>, + AEff, + Args extends Array, + A, + B, + C, + D, + E, + F + >( + body: (...args: Args) => Generator, + a: ( + _: Effect< + AEff, + [Eff] extends [never] ? never : [Eff] extends [YieldWrap>] ? E : never, + [Eff] extends [never] ? never : [Eff] extends [YieldWrap>] ? R : never + >, + ...args: NoInfer + ) => A, + b: (_: A, ...args: NoInfer) => B, + c: (_: B, ...args: NoInfer) => C, + d: (_: C, ...args: NoInfer) => D, + e: (_: D, ...args: NoInfer) => E, + f: (_: E, ...args: NoInfer) => F + ): (...args: Args) => F + < + Eff extends YieldWrap>, + AEff, + Args extends Array, + A, + B, + C, + D, + E, + F, + G + >( + body: (...args: Args) => Generator, + a: ( + _: Effect< + AEff, + [Eff] extends [never] ? never : [Eff] extends [YieldWrap>] ? E : never, + [Eff] extends [never] ? never : [Eff] extends [YieldWrap>] ? R : never + >, + ...args: NoInfer + ) => A, + b: (_: A, ...args: NoInfer) => B, + c: (_: B, ...args: NoInfer) => C, + d: (_: C, ...args: NoInfer) => D, + e: (_: D, ...args: NoInfer) => E, + f: (_: E, ...args: NoInfer) => F, + g: (_: F, ...args: NoInfer) => G + ): (...args: Args) => G + < + Eff extends YieldWrap>, + AEff, + Args extends Array, + A, + B, + C, + D, + E, + F, + G, + H + >( + body: (...args: Args) => Generator, + a: ( + _: Effect< + AEff, + [Eff] extends [never] ? never : [Eff] extends [YieldWrap>] ? E : never, + [Eff] extends [never] ? never : [Eff] extends [YieldWrap>] ? R : never + >, + ...args: NoInfer + ) => A, + b: (_: A, ...args: NoInfer) => B, + c: (_: B, ...args: NoInfer) => C, + d: (_: C, ...args: NoInfer) => D, + e: (_: D, ...args: NoInfer) => E, + f: (_: E, ...args: NoInfer) => F, + g: (_: F, ...args: NoInfer) => G, + h: (_: G, ...args: NoInfer) => H + ): (...args: Args) => H + < + Eff extends YieldWrap>, + AEff, + Args extends Array, + A, + B, + C, + D, + E, + F, + G, + H, + I + >( + body: (...args: Args) => Generator, + a: ( + _: Effect< + AEff, + [Eff] extends [never] ? never : [Eff] extends [YieldWrap>] ? E : never, + [Eff] extends [never] ? never : [Eff] extends [YieldWrap>] ? R : never + >, + ...args: NoInfer + ) => A, + b: (_: A, ...args: NoInfer) => B, + c: (_: B, ...args: NoInfer) => C, + d: (_: C, ...args: NoInfer) => D, + e: (_: D, ...args: NoInfer) => E, + f: (_: E, ...args: NoInfer) => F, + g: (_: F, ...args: NoInfer) => G, + h: (_: G, ...args: NoInfer) => H, + i: (_: H, ...args: NoInfer) => I + ): (...args: Args) => I + } +} + +/** + * The `Effect.fn` function allows you to create traced functions that return an + * effect. It provides two key features: + * + * - **Stack traces with location details** if an error occurs. + * - **Automatic span creation** for tracing when a span name is provided. + * + * If a span name is passed as the first argument, the function's execution is + * tracked using that name. If no name is provided, stack tracing still works, + * but spans are not created. + * + * A function can be defined using either: + * + * - A generator function, allowing the use of `yield*` for effect composition. + * - A regular function that returns an `Effect`. + * + * **Example** (Creating a Traced Function with a Span Name) + * + * ```ts + * import { Effect } from "effect" + * + * const myfunc = Effect.fn("myspan")(function* (n: N) { + * yield* Effect.annotateCurrentSpan("n", n) // Attach metadata to the span + * console.log(`got: ${n}`) + * yield* Effect.fail(new Error("Boom!")) // Simulate failure + * }) + * + * Effect.runFork(myfunc(100).pipe(Effect.catchAllCause(Effect.logError))) + * // Output: + * // got: 100 + * // timestamp=... level=ERROR fiber=#0 cause="Error: Boom! + * // at (/.../index.ts:6:22) <= Raise location + * // at myspan (/.../index.ts:3:23) <= Definition location + * // at myspan (/.../index.ts:9:16)" <= Call location + * ``` + * + * `Effect.fn` automatically creates spans. The spans capture information about + * the function execution, including metadata and error details. + * + * **Example** (Exporting Spans to the Console) + * + * ```ts skip-type-checking + * import { Effect } from "effect" + * import { NodeSdk } from "@effect/opentelemetry" + * import { + * ConsoleSpanExporter, + * BatchSpanProcessor + * } from "@opentelemetry/sdk-trace-base" + * + * const myfunc = Effect.fn("myspan")(function* (n: N) { + * yield* Effect.annotateCurrentSpan("n", n) + * console.log(`got: ${n}`) + * yield* Effect.fail(new Error("Boom!")) + * }) + * + * const program = myfunc(100) + * + * const NodeSdkLive = NodeSdk.layer(() => ({ + * resource: { serviceName: "example" }, + * // Export span data to the console + * spanProcessor: new BatchSpanProcessor(new ConsoleSpanExporter()) + * })) + * + * Effect.runFork(program.pipe(Effect.provide(NodeSdkLive))) + * // Output: + * // got: 100 + * // { + * // resource: { + * // attributes: { + * // 'service.name': 'example', + * // 'telemetry.sdk.language': 'nodejs', + * // 'telemetry.sdk.name': '@effect/opentelemetry', + * // 'telemetry.sdk.version': '1.30.1' + * // } + * // }, + * // instrumentationScope: { name: 'example', version: undefined, schemaUrl: undefined }, + * // traceId: '22801570119e57a6e2aacda3dec9665b', + * // parentId: undefined, + * // traceState: undefined, + * // name: 'myspan', + * // id: '7af530c1e01bc0cb', + * // kind: 0, + * // timestamp: 1741182277518402.2, + * // duration: 4300.416, + * // attributes: { + * // n: 100, + * // 'code.stacktrace': 'at (/.../index.ts:8:23)\n' + + * // 'at (/.../index.ts:14:17)' + * // }, + * // status: { code: 2, message: 'Boom!' }, + * // events: [ + * // { + * // name: 'exception', + * // attributes: { + * // 'exception.type': 'Error', + * // 'exception.message': 'Boom!', + * // 'exception.stacktrace': 'Error: Boom!\n' + + * // ' at (/.../index.ts:11:22)\n' + + * // ' at myspan (/.../index.ts:8:23)\n' + + * // ' at myspan (/.../index.ts:14:17)' + * // }, + * // time: [ 1741182277, 522702583 ], + * // droppedAttributesCount: 0 + * // } + * // ], + * // links: [] + * // } + * ``` + * + * `Effect.fn` also acts as a pipe function, allowing you to create a pipeline + * after the function definition using the effect returned by the generator + * function as the starting value of the pipeline. + * + * **Example** (Creating a Traced Function with a Delay) + * + * ```ts + * import { Effect } from "effect" + * + * const myfunc = Effect.fn( + * function* (n: number) { + * console.log(`got: ${n}`) + * yield* Effect.fail(new Error("Boom!")) + * }, + * // You can access both the created effect and the original arguments + * (effect, n) => Effect.delay(effect, `${n / 100} seconds`) + * ) + * + * Effect.runFork(myfunc(100).pipe(Effect.catchAllCause(Effect.logError))) + * // Output: + * // got: 100 + * // timestamp=... level=ERROR fiber=#0 cause="Error: Boom! (<= after 1 second) + * ``` + * + * @see {@link fnUntraced} for a version of this function that doesn't add a span. + * + * @since 3.11.0 + * @category Tracing + */ +export const fn: + & fn.Gen + & fn.NonGen + & (( + name: string, + options?: Tracer.SpanOptions + ) => fn.Gen & fn.NonGen) = function(nameOrBody: Function | string, ...pipeables: Array) { + const limit = Error.stackTraceLimit + Error.stackTraceLimit = 2 + const errorDef = new Error() + Error.stackTraceLimit = limit + if (typeof nameOrBody !== "string") { + return defineLength(nameOrBody.length, function(this: any, ...args: Array) { + const limit = Error.stackTraceLimit + Error.stackTraceLimit = 2 + const errorCall = new Error() + Error.stackTraceLimit = limit + return fnApply({ + self: this, + body: nameOrBody, + args, + pipeables, + spanName: "", + spanOptions: { + context: internalTracer.DisablePropagation.context(true) + }, + errorDef, + errorCall + }) + }) as any + } + const name = nameOrBody + const options = pipeables[0] + return (body: Function, ...pipeables: Array) => + defineLength( + body.length, + ({ + [name](this: any, ...args: Array) { + const limit = Error.stackTraceLimit + Error.stackTraceLimit = 2 + const errorCall = new Error() + Error.stackTraceLimit = limit + return fnApply({ + self: this, + body, + args, + pipeables, + spanName: name, + spanOptions: options, + errorDef, + errorCall + }) + } + })[name] + ) + } + +function defineLength(length: number, fn: F) { + return Object.defineProperty(fn, "length", { + value: length, + configurable: true + }) +} + +function fnApply(options: { + readonly self: any + readonly body: Function + readonly args: Array + readonly pipeables: Array + readonly spanName: string + readonly spanOptions: Tracer.SpanOptions + readonly errorDef: Error + readonly errorCall: Error +}) { + let effect: Effect + let fnError: any = undefined + if (isGeneratorFunction(options.body)) { + effect = core.fromIterator(() => options.body.apply(options.self, options.args)) + } else { + try { + effect = options.body.apply(options.self, options.args) + } catch (error) { + fnError = error + effect = die(error) + } + } + if (options.pipeables.length > 0) { + try { + for (const x of options.pipeables) { + effect = x(effect, ...options.args) + } + } catch (error) { + effect = fnError + ? failCause(internalCause.sequential( + internalCause.die(fnError), + internalCause.die(error) + )) + : die(error) + } + } + + let cache: false | string = false + const captureStackTrace = () => { + if (cache !== false) { + return cache + } + if (options.errorCall.stack) { + const stackDef = options.errorDef.stack!.trim().split("\n") + const stackCall = options.errorCall.stack.trim().split("\n") + let endStackDef = stackDef.slice(2).join("\n").trim() + if (!endStackDef.includes(`(`)) { + endStackDef = endStackDef.replace(/at (.*)/, "at ($1)") + } + let endStackCall = stackCall.slice(2).join("\n").trim() + if (!endStackCall.includes(`(`)) { + endStackCall = endStackCall.replace(/at (.*)/, "at ($1)") + } + cache = `${endStackDef}\n${endStackCall}` + return cache + } + } + const opts: any = (options.spanOptions && "captureStackTrace" in options.spanOptions) + ? options.spanOptions + : { captureStackTrace, ...options.spanOptions } + return withSpan(effect, options.spanName, opts) +} + +/** + * Same as {@link fn}, but allows you to create a function that is not traced, for when performance is critical. + * + * @see {@link fn} for a version that includes tracing. + * + * @since 3.12.0 + * @category Tracing + */ +export const fnUntraced: fn.Untraced = core.fnUntraced + +// ----------------------------------------------------------------------------- +// Type constraints +// ----------------------------------------------------------------------------- + +/** + * A no-op type constraint that enforces the success channel of an Effect conforms to + * the specified success type `A`. + * + * @example + * import { Effect } from "effect" + * + * // Ensure that the program does not expose any unhandled errors. + * const program = Effect.succeed(42).pipe(Effect.ensureSuccessType()) + * + * @since 3.17.0 + * @category Type constraints + */ +export const ensureSuccessType = () => (effect: Effect): Effect => effect + +/** + * A no-op type constraint that enforces the error channel of an Effect conforms to + * the specified error type `E`. + * + * @example + * import { Effect } from "effect" + * + * // Ensure that the program does not expose any unhandled errors. + * const program = Effect.succeed(42).pipe(Effect.ensureErrorType()) + * + * @since 3.17.0 + * @category Type constraints + */ +export const ensureErrorType = () => (effect: Effect): Effect => effect + +/** + * A no-op type constraint that enforces the requirements channel of an Effect conforms to + * the specified requirements type `R`. + * + * @example + * import { Effect } from "effect" + * + * // Ensure that the program does not have any requirements. + * const program = Effect.succeed(42).pipe(Effect.ensureRequirementsType()) + * + * @since 3.17.0 + * @category Type constraints + */ +export const ensureRequirementsType = () => (effect: Effect): Effect => + effect diff --git a/repos/effect/packages/effect/src/Effectable.ts b/repos/effect/packages/effect/src/Effectable.ts new file mode 100644 index 0000000..f082113 --- /dev/null +++ b/repos/effect/packages/effect/src/Effectable.ts @@ -0,0 +1,107 @@ +/** + * @since 2.0.0 + */ +import type * as Channel from "./Channel.js" +import type * as Effect from "./Effect.js" +import * as internal from "./internal/effectable.js" +import type * as Sink from "./Sink.js" +import type * as Stream from "./Stream.js" + +/** + * @since 2.0.0 + * @category type ids + */ +export const EffectTypeId: Effect.EffectTypeId = internal.EffectTypeId + +/** + * @since 2.0.0 + * @category type ids + */ +export type EffectTypeId = Effect.EffectTypeId + +/** + * @since 2.0.0 + * @category type ids + */ +export const StreamTypeId: Stream.StreamTypeId = internal.StreamTypeId + +/** + * @since 2.0.0 + * @category type ids + */ +export type StreamTypeId = Stream.StreamTypeId + +/** + * @since 2.0.0 + * @category type ids + */ +export const SinkTypeId: Sink.SinkTypeId = internal.SinkTypeId + +/** + * @since 2.0.0 + * @category type ids + */ +export type SinkTypeId = Sink.SinkTypeId + +/** + * @since 2.0.0 + * @category type ids + */ +export const ChannelTypeId: Channel.ChannelTypeId = internal.ChannelTypeId + +/** + * @since 2.0.0 + * @category type ids + */ +export type ChannelTypeId = Channel.ChannelTypeId + +/** + * @since 2.0.0 + * @category models + */ +export interface CommitPrimitive { + new(): Effect.Effect +} + +/** + * @since 2.0.0 + * @category prototypes + */ +export const EffectPrototype: Effect.Effect = internal.EffectPrototype + +/** + * @since 2.0.0 + * @category prototypes + */ +export const CommitPrototype: Effect.Effect = internal.CommitPrototype + +/** + * @since 2.0.0 + * @category prototypes + */ +export const StructuralCommitPrototype: Effect.Effect = internal.StructuralCommitPrototype + +const Base: CommitPrimitive = internal.Base +const StructuralBase: CommitPrimitive = internal.StructuralBase + +/** + * @since 2.0.0 + * @category constructors + */ +export abstract class Class extends Base { + /** + * @since 2.0.0 + */ + abstract commit(): Effect.Effect +} + +/** + * @since 2.0.0 + * @category constructors + */ +export abstract class StructuralClass extends StructuralBase { + /** + * @since 2.0.0 + */ + abstract commit(): Effect.Effect +} diff --git a/repos/effect/packages/effect/src/Either.ts b/repos/effect/packages/effect/src/Either.ts new file mode 100644 index 0000000..65767ae --- /dev/null +++ b/repos/effect/packages/effect/src/Either.ts @@ -0,0 +1,1040 @@ +/** + * @since 2.0.0 + */ + +import * as Equivalence from "./Equivalence.js" +import type { LazyArg } from "./Function.js" +import { constNull, constUndefined, dual, identity } from "./Function.js" +import type { TypeLambda } from "./HKT.js" +import type { Inspectable } from "./Inspectable.js" +import * as doNotation from "./internal/doNotation.js" +import * as either from "./internal/either.js" +import * as option_ from "./internal/option.js" +import type { Option } from "./Option.js" +import type { Pipeable } from "./Pipeable.js" +import type { Predicate, Refinement } from "./Predicate.js" +import { isFunction } from "./Predicate.js" +import type { Covariant, NoInfer, NotFunction } from "./Types.js" +import type * as Unify from "./Unify.js" +import * as Gen from "./Utils.js" + +/** + * @category models + * @since 2.0.0 + */ +export type Either = Left | Right + +/** + * @category symbols + * @since 2.0.0 + */ +export const TypeId: unique symbol = either.TypeId + +/** + * @category symbols + * @since 2.0.0 + */ +export type TypeId = typeof TypeId + +// TODO(4.0): flip the order of the type parameters +/** + * @category models + * @since 2.0.0 + */ +export interface Left extends Pipeable, Inspectable { + readonly _tag: "Left" + readonly _op: "Left" + readonly left: E + readonly [TypeId]: { + readonly _R: Covariant + readonly _L: Covariant + } + [Unify.typeSymbol]?: unknown + [Unify.unifySymbol]?: EitherUnify + [Unify.ignoreSymbol]?: EitherUnifyIgnore +} + +// TODO(4.0): flip the order of the type parameters +/** + * @category models + * @since 2.0.0 + */ +export interface Right extends Pipeable, Inspectable { + readonly _tag: "Right" + readonly _op: "Right" + readonly right: A + readonly [TypeId]: { + readonly _R: Covariant + readonly _L: Covariant + } + [Unify.typeSymbol]?: unknown + [Unify.unifySymbol]?: EitherUnify + [Unify.ignoreSymbol]?: EitherUnifyIgnore +} + +/** + * @category models + * @since 2.0.0 + */ +export interface EitherUnify { + Either?: () => A[Unify.typeSymbol] extends Either | infer _ ? Either : never +} + +/** + * @category models + * @since 2.0.0 + */ +export interface EitherUnifyIgnore {} + +/** + * @category type lambdas + * @since 2.0.0 + */ +export interface EitherTypeLambda extends TypeLambda { + readonly type: Either +} + +/** + * @since 2.0.0 + */ +export declare namespace Either { + /** + * @since 2.0.0 + * @category type-level + */ + export type Left> = [T] extends [Either] ? _E : never + /** + * @since 2.0.0 + * @category type-level + */ + export type Right> = [T] extends [Either] ? _A : never +} + +/** + * Constructs a new `Either` holding a `Right` value. This usually represents a successful value due to the right bias + * of this structure. + * + * @category constructors + * @since 2.0.0 + */ +export const right: (a: A) => Either = either.right + +const void_: Either = right(void 0) +export { + /** + * @category constructors + * @since 3.13.0 + */ + void_ as void +} + +/** + * Constructs a new `Either` holding a `Left` value. This usually represents a failure, due to the right-bias of this + * structure. + * + * @category constructors + * @since 2.0.0 + */ +export const left: (e: E) => Either = either.left + +/** + * Takes a lazy default and a nullable value, if the value is not nully (`null` or `undefined`), turn it into a `Right`, if the value is nully use + * the provided default as a `Left`. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { Either } from "effect" + * + * assert.deepStrictEqual(Either.fromNullable(1, () => 'fallback'), Either.right(1)) + * assert.deepStrictEqual(Either.fromNullable(null, () => 'fallback'), Either.left('fallback')) + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const fromNullable: { + (onNullable: (right: A) => E): (self: A) => Either, E> + (self: A, onNullable: (right: A) => E): Either, E> +} = dual( + 2, + (self: A, onNullable: (right: A) => E): Either, E> => + self == null ? left(onNullable(self)) : right(self) +) + +/** + * @example + * ```ts + * import * as assert from "node:assert" + * import { Either, Option } from "effect" + * + * assert.deepStrictEqual(Either.fromOption(Option.some(1), () => 'error'), Either.right(1)) + * assert.deepStrictEqual(Either.fromOption(Option.none(), () => 'error'), Either.left('error')) + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const fromOption: { + (onNone: () => E): (self: Option) => Either + (self: Option, onNone: () => E): Either +} = either.fromOption + +const try_: { + ( + options: { + readonly try: LazyArg + readonly catch: (error: unknown) => E + } + ): Either + (evaluate: LazyArg): Either +} = (( + evaluate: LazyArg | { + readonly try: LazyArg + readonly catch: (error: unknown) => E + } +) => { + if (isFunction(evaluate)) { + try { + return right(evaluate()) + } catch (e) { + return left(e) + } + } else { + try { + return right(evaluate.try()) + } catch (e) { + return left(evaluate.catch(e)) + } + } +}) as any + +export { + /** + * Imports a synchronous side-effect into a pure `Either` value, translating any + * thrown exceptions into typed failed eithers creating with `Either.left`. + * + * @category constructors + * @since 2.0.0 + */ + try_ as try +} + +/** + * Tests if a value is a `Either`. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { Either } from "effect" + * + * assert.deepStrictEqual(Either.isEither(Either.right(1)), true) + * assert.deepStrictEqual(Either.isEither(Either.left("a")), true) + * assert.deepStrictEqual(Either.isEither({ right: 1 }), false) + * ``` + * + * @category guards + * @since 2.0.0 + */ +export const isEither: (input: unknown) => input is Either = either.isEither + +/** + * Determine if a `Either` is a `Left`. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { Either } from "effect" + * + * assert.deepStrictEqual(Either.isLeft(Either.right(1)), false) + * assert.deepStrictEqual(Either.isLeft(Either.left("a")), true) + * ``` + * + * @category guards + * @since 2.0.0 + */ +export const isLeft: (self: Either) => self is Left = either.isLeft + +/** + * Determine if a `Either` is a `Right`. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { Either } from "effect" + * + * assert.deepStrictEqual(Either.isRight(Either.right(1)), true) + * assert.deepStrictEqual(Either.isRight(Either.left("a")), false) + * ``` + * + * @category guards + * @since 2.0.0 + */ +export const isRight: (self: Either) => self is Right = either.isRight + +/** + * Converts a `Either` to an `Option` discarding the `Left`. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { Either, Option } from "effect" + * + * assert.deepStrictEqual(Either.getRight(Either.right('ok')), Option.some('ok')) + * assert.deepStrictEqual(Either.getRight(Either.left('err')), Option.none()) + * ``` + * + * @category getters + * @since 2.0.0 + */ +export const getRight: (self: Either) => Option = either.getRight + +/** + * Converts a `Either` to an `Option` discarding the value. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { Either, Option } from "effect" + * + * assert.deepStrictEqual(Either.getLeft(Either.right('ok')), Option.none()) + * assert.deepStrictEqual(Either.getLeft(Either.left('err')), Option.some('err')) + * ``` + * + * @category getters + * @since 2.0.0 + */ +export const getLeft: (self: Either) => Option = either.getLeft + +/** + * @category equivalence + * @since 2.0.0 + */ +export const getEquivalence = ({ left, right }: { + right: Equivalence.Equivalence + left: Equivalence.Equivalence +}): Equivalence.Equivalence> => + Equivalence.make((x, y) => + isLeft(x) ? + isLeft(y) && left(x.left, y.left) : + isRight(y) && right(x.right, y.right) + ) + +/** + * @category mapping + * @since 2.0.0 + */ +export const mapBoth: { + (options: { + readonly onLeft: (left: E) => E2 + readonly onRight: (right: A) => A2 + }): (self: Either) => Either + (self: Either, options: { + readonly onLeft: (left: E) => E2 + readonly onRight: (right: A) => A2 + }): Either +} = dual( + 2, + (self: Either, { onLeft, onRight }: { + readonly onLeft: (left: E) => E2 + readonly onRight: (right: A) => A2 + }): Either => isLeft(self) ? left(onLeft(self.left)) : right(onRight(self.right)) +) + +/** + * Maps the `Left` side of an `Either` value to a new `Either` value. + * + * @category mapping + * @since 2.0.0 + */ +export const mapLeft: { + (f: (left: E) => E2): (self: Either) => Either + (self: Either, f: (left: E) => E2): Either +} = dual( + 2, + (self: Either, f: (left: E) => E2): Either => + isLeft(self) ? left(f(self.left)) : right(self.right) +) + +/** + * Maps the `Right` side of an `Either` value to a new `Either` value. + * + * @category mapping + * @since 2.0.0 + */ +export const map: { + (f: (right: A) => A2): (self: Either) => Either + (self: Either, f: (right: A) => A2): Either +} = dual( + 2, + (self: Either, f: (right: A) => A2): Either => + isRight(self) ? right(f(self.right)) : left(self.left) +) + +/** + * Takes two functions and an `Either` value, if the value is a `Left` the inner value is applied to the `onLeft function, + * if the value is a `Right` the inner value is applied to the `onRight` function. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { pipe, Either } from "effect" + * + * const onLeft = (strings: ReadonlyArray): string => `strings: ${strings.join(', ')}` + * + * const onRight = (value: number): string => `Ok: ${value}` + * + * assert.deepStrictEqual(pipe(Either.right(1), Either.match({ onLeft, onRight })), 'Ok: 1') + * assert.deepStrictEqual( + * pipe(Either.left(['string 1', 'string 2']), Either.match({ onLeft, onRight })), + * 'strings: string 1, string 2' + * ) + * ``` + * + * @category pattern matching + * @since 2.0.0 + */ +export const match: { + (options: { + readonly onLeft: (left: E) => B + readonly onRight: (right: A) => C + }): (self: Either) => B | C + (self: Either, options: { + readonly onLeft: (left: E) => B + readonly onRight: (right: A) => C + }): B | C +} = dual( + 2, + (self: Either, { onLeft, onRight }: { + readonly onLeft: (left: E) => B + readonly onRight: (right: A) => C + }): B | C => isLeft(self) ? onLeft(self.left) : onRight(self.right) +) + +/** + * Transforms a `Predicate` function into a `Right` of the input value if the predicate returns `true` + * or `Left` of the result of the provided function if the predicate returns false + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { pipe, Either } from "effect" + * + * const isPositive = (n: number): boolean => n > 0 + * const isPositiveEither = Either.liftPredicate(isPositive, n => `${n} is not positive`) + * + * assert.deepStrictEqual( + * isPositiveEither(1), + * Either.right(1) + * ) + * assert.deepStrictEqual( + * isPositiveEither(0), + * Either.left("0 is not positive") + * ) + * ``` + * + * @category lifting + * @since 3.4.0 + */ +export const liftPredicate: { + (refinement: Refinement, orLeftWith: (a: A) => E): (a: A) => Either + ( + predicate: Predicate, + orLeftWith: (a: A) => E + ): (a: B) => Either + ( + self: A, + refinement: Refinement, + orLeftWith: (a: A) => E + ): Either + ( + self: B, + predicate: Predicate, + orLeftWith: (a: A) => E + ): Either +} = dual( + 3, + (a: A, predicate: Predicate, orLeftWith: (a: A) => E): Either => + predicate(a) ? right(a) : left(orLeftWith(a)) +) + +/** + * Filter the right value with the provided function. + * If the predicate fails, set the left value with the result of the provided function. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { pipe, Either } from "effect" + * + * const isPositive = (n: number): boolean => n > 0 + * + * assert.deepStrictEqual( + * pipe( + * Either.right(1), + * Either.filterOrLeft(isPositive, n => `${n} is not positive`) + * ), + * Either.right(1) + * ) + * assert.deepStrictEqual( + * pipe( + * Either.right(0), + * Either.filterOrLeft(isPositive, n => `${n} is not positive`) + * ), + * Either.left("0 is not positive") + * ) + * ``` + * + * @since 2.0.0 + * @category filtering & conditionals + */ +export const filterOrLeft: { + ( + refinement: Refinement, B>, + orLeftWith: (right: NoInfer) => E2 + ): (self: Either) => Either + ( + predicate: Predicate>, + orLeftWith: (right: NoInfer) => E2 + ): (self: Either) => Either + ( + self: Either, + refinement: Refinement, + orLeftWith: (right: A) => E2 + ): Either + (self: Either, predicate: Predicate, orLeftWith: (right: A) => E2): Either +} = dual(3, ( + self: Either, + predicate: Predicate, + orLeftWith: (right: A) => E2 +): Either => flatMap(self, (r) => predicate(r) ? right(r) : left(orLeftWith(r)))) + +/** + * @category getters + * @since 2.0.0 + */ +export const merge: (self: Either) => E | A = match({ + onLeft: identity, + onRight: identity +}) + +/** + * Returns the wrapped value if it's a `Right` or a default value if is a `Left`. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { Either } from "effect" + * + * assert.deepStrictEqual(Either.getOrElse(Either.right(1), (error) => error + "!"), 1) + * assert.deepStrictEqual(Either.getOrElse(Either.left("not a number"), (error) => error + "!"), "not a number!") + * ``` + * + * @category getters + * @since 2.0.0 + */ +export const getOrElse: { + (onLeft: (left: E) => A2): (self: Either) => A2 | A + (self: Either, onLeft: (left: E) => A2): A | A2 +} = dual( + 2, + (self: Either, onLeft: (left: E) => A2): A | A2 => isLeft(self) ? onLeft(self.left) : self.right +) + +/** + * @example + * ```ts + * import * as assert from "node:assert" + * import { Either } from "effect" + * + * assert.deepStrictEqual(Either.getOrNull(Either.right(1)), 1) + * assert.deepStrictEqual(Either.getOrNull(Either.left("a")), null) + * ``` + * + * @category getters + * @since 2.0.0 + */ +export const getOrNull: (self: Either) => A | null = getOrElse(constNull) + +/** + * @example + * ```ts + * import * as assert from "node:assert" + * import { Either } from "effect" + * + * assert.deepStrictEqual(Either.getOrUndefined(Either.right(1)), 1) + * assert.deepStrictEqual(Either.getOrUndefined(Either.left("a")), undefined) + * ``` + * + * @category getters + * @since 2.0.0 + */ +export const getOrUndefined: (self: Either) => A | undefined = getOrElse(constUndefined) + +/** + * Extracts the value of an `Either` or throws if the `Either` is `Left`. + * + * If a default error is sufficient for your use case and you don't need to configure the thrown error, see {@link getOrThrow}. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { Either } from "effect" + * + * assert.deepStrictEqual( + * Either.getOrThrowWith(Either.right(1), () => new Error('Unexpected Left')), + * 1 + * ) + * assert.throws(() => Either.getOrThrowWith(Either.left("error"), () => new Error('Unexpected Left'))) + * ``` + * + * @category getters + * @since 2.0.0 + */ +export const getOrThrowWith: { + (onLeft: (left: E) => unknown): (self: Either) => A + (self: Either, onLeft: (left: E) => unknown): A +} = dual(2, (self: Either, onLeft: (left: E) => unknown): A => { + if (isRight(self)) { + return self.right + } + throw onLeft(self.left) +}) + +// TODO(4.0): by default should throw `L` (i.e getOrThrowWith with the identity function) +/** + * Extracts the value of an `Either` or throws if the `Either` is `Left`. + * + * The thrown error is a default error. To configure the error thrown, see {@link getOrThrowWith}. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { Either } from "effect" + * + * assert.deepStrictEqual(Either.getOrThrow(Either.right(1)), 1) + * assert.throws(() => Either.getOrThrow(Either.left("error"))) + * ``` + * + * @throws `Error("getOrThrow called on a Left")` + * + * @category getters + * @since 2.0.0 + */ +export const getOrThrow: (self: Either) => A = getOrThrowWith(() => + new Error("getOrThrow called on a Left") +) + +/** + * Returns `self` if it is a `Right` or `that` otherwise. + * + * @category error handling + * @since 2.0.0 + */ +export const orElse: { + (that: (left: E) => Either): (self: Either) => Either + (self: Either, that: (left: E) => Either): Either +} = dual( + 2, + (self: Either, that: (left: E) => Either): Either => + isLeft(self) ? that(self.left) : right(self.right) +) + +/** + * @category sequencing + * @since 2.0.0 + */ +export const flatMap: { + (f: (right: A) => Either): (self: Either) => Either + (self: Either, f: (right: A) => Either): Either +} = dual( + 2, + (self: Either, f: (right: A) => Either): Either => + isLeft(self) ? left(self.left) : f(self.right) +) + +/** + * Executes a sequence of two `Either`s. The second `Either` can be dependent on the result of the first `Either`. + * + * @category sequencing + * @since 2.0.0 + */ +export const andThen: { + (f: (right: A) => Either): (self: Either) => Either + (f: Either): (self: Either) => Either + (f: (right: A) => A2): (self: Either) => Either + (right: NotFunction): (self: Either) => Either + (self: Either, f: (right: A) => Either): Either + (self: Either, f: Either): Either + (self: Either, f: (right: A) => A2): Either + (self: Either, f: NotFunction): Either +} = dual( + 2, + (self: Either, f: (right: A) => Either | Either): Either => + flatMap(self, (a) => { + const b = isFunction(f) ? f(a) : f + return isEither(b) ? b : right(b) + }) +) + +/** + * @category zipping + * @since 2.0.0 + */ +export const zipWith: { + ( + that: Either, + f: (right: A, right2: A2) => B + ): (self: Either) => Either + ( + self: Either, + that: Either, + f: (right: A, right2: A2) => B + ): Either +} = dual( + 3, + (self: Either, that: Either, f: (right: A, right2: A2) => B): Either => + flatMap(self, (r) => map(that, (r2) => f(r, r2))) +) + +/** + * @category combining + * @since 2.0.0 + */ +export const ap: { + (that: Either): (self: Either<(right: A) => A2, E>) => Either + (self: Either<(right: A) => A2, E>, that: Either): Either +} = dual( + 2, + (self: Either<(right: A) => A2, E>, that: Either): Either => + zipWith(self, that, (f, a) => f(a)) +) + +/** + * Takes a structure of `Either`s and returns an `Either` of values with the same structure. + * + * - If a tuple is supplied, then the returned `Either` will contain a tuple with the same length. + * - If a struct is supplied, then the returned `Either` will contain a struct with the same keys. + * - If an iterable is supplied, then the returned `Either` will contain an array. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { Either } from "effect" + * + * assert.deepStrictEqual(Either.all([Either.right(1), Either.right(2)]), Either.right([1, 2])) + * assert.deepStrictEqual(Either.all({ right: Either.right(1), b: Either.right("hello") }), Either.right({ right: 1, b: "hello" })) + * assert.deepStrictEqual(Either.all({ right: Either.right(1), b: Either.left("error") }), Either.left("error")) + * ``` + * + * @category combining + * @since 2.0.0 + */ +// @ts-expect-error +export const all: > | Record>>( + input: I +) => [I] extends [ReadonlyArray>] ? Either< + { -readonly [K in keyof I]: [I[K]] extends [Either] ? A : never }, + I[number] extends never ? never : [I[number]] extends [Either] ? E : never + > + : [I] extends [Iterable>] ? Either, E> + : Either< + { -readonly [K in keyof I]: [I[K]] extends [Either] ? A : never }, + I[keyof I] extends never ? never : [I[keyof I]] extends [Either] ? E : never + > = ( + input: Iterable> | Record> + ): Either => { + if (Symbol.iterator in input) { + const out: Array> = [] + for (const e of input) { + if (isLeft(e)) { + return e + } + out.push(e.right) + } + return right(out) + } + + const out: Record = {} + for (const key of Object.keys(input)) { + const e = input[key] + if (isLeft(e)) { + return e + } + out[key] = e.right + } + return right(out) + } + +/** + * Returns an `Either` that swaps the error/success cases. This allows you to + * use all methods on the error channel, possibly before flipping back. + * + * @since 2.0.0 + * @category mapping + */ +export const flip = (self: Either): Either => isLeft(self) ? right(self.left) : left(self.right) + +const adapter = Gen.adapter() + +/** + * @category generators + * @since 2.0.0 + */ +export const gen: Gen.Gen> = (...args) => { + const f = args.length === 1 ? args[0] : args[1].bind(args[0]) + const iterator = f(adapter) + let state: IteratorResult = iterator.next() + while (!state.done) { + const current = Gen.isGenKind(state.value) + ? state.value.value + : Gen.yieldWrapGet(state.value) + if (isLeft(current)) { + return current + } + state = iterator.next(current.right as never) + } + return right(state.value) as any +} + +// ------------------------------------------------------------------------------------- +// do notation +// ------------------------------------------------------------------------------------- + +/** + * The "do simulation" in Effect allows you to write code in a more declarative style, similar to the "do notation" in other programming languages. It provides a way to define variables and perform operations on them using functions like `bind` and `let`. + * + * Here's how the do simulation works: + * + * 1. Start the do simulation using the `Do` value + * 2. Within the do simulation scope, you can use the `bind` function to define variables and bind them to `Either` values + * 3. You can accumulate multiple `bind` statements to define multiple variables within the scope + * 4. Inside the do simulation scope, you can also use the `let` function to define variables and bind them to simple values + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { Either, pipe } from "effect" + * + * const result = pipe( + * Either.Do, + * Either.bind("x", () => Either.right(2)), + * Either.bind("y", () => Either.right(3)), + * Either.let("sum", ({ x, y }) => x + y) + * ) + * assert.deepStrictEqual(result, Either.right({ x: 2, y: 3, sum: 5 })) + * ``` + * + * @see {@link bind} + * @see {@link bindTo} + * @see {@link let_ let} + * + * @category do notation + * @since 2.0.0 + */ +export const Do: Either<{}> = right({}) + +/** + * The "do simulation" in Effect allows you to write code in a more declarative style, similar to the "do notation" in other programming languages. It provides a way to define variables and perform operations on them using functions like `bind` and `let`. + * + * Here's how the do simulation works: + * + * 1. Start the do simulation using the `Do` value + * 2. Within the do simulation scope, you can use the `bind` function to define variables and bind them to `Either` values + * 3. You can accumulate multiple `bind` statements to define multiple variables within the scope + * 4. Inside the do simulation scope, you can also use the `let` function to define variables and bind them to simple values + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { Either, pipe } from "effect" + * + * const result = pipe( + * Either.Do, + * Either.bind("x", () => Either.right(2)), + * Either.bind("y", () => Either.right(3)), + * Either.let("sum", ({ x, y }) => x + y) + * ) + * assert.deepStrictEqual(result, Either.right({ x: 2, y: 3, sum: 5 })) + * ``` + * + * @see {@link Do} + * @see {@link bindTo} + * @see {@link let_ let} + * + * @category do notation + * @since 2.0.0 + */ +export const bind: { + ( + name: Exclude, + f: (a: NoInfer) => Either + ): (self: Either) => Either<{ [K in N | keyof A]: K extends keyof A ? A[K] : B }, E | E2> + ( + self: Either, + name: Exclude, + f: (a: NoInfer) => Either + ): Either<{ [K in N | keyof A]: K extends keyof A ? A[K] : B }, E | E2> +} = doNotation.bind(map, flatMap) + +/** + * The "do simulation" in Effect allows you to write code in a more declarative style, similar to the "do notation" in other programming languages. It provides a way to define variables and perform operations on them using functions like `bind` and `let`. + * + * Here's how the do simulation works: + * + * 1. Start the do simulation using the `Do` value + * 2. Within the do simulation scope, you can use the `bind` function to define variables and bind them to `Either` values + * 3. You can accumulate multiple `bind` statements to define multiple variables within the scope + * 4. Inside the do simulation scope, you can also use the `let` function to define variables and bind them to simple values + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { Either, pipe } from "effect" + * + * const result = pipe( + * Either.Do, + * Either.bind("x", () => Either.right(2)), + * Either.bind("y", () => Either.right(3)), + * Either.let("sum", ({ x, y }) => x + y) + * ) + * assert.deepStrictEqual(result, Either.right({ x: 2, y: 3, sum: 5 })) + * ``` + * + * @see {@link Do} + * @see {@link bind} + * @see {@link let_ let} + * + * @category do notation + * @since 2.0.0 + */ +export const bindTo: { + (name: N): (self: Either) => Either<{ [K in N]: A }, E> + (self: Either, name: N): Either<{ [K in N]: A }, E> +} = doNotation.bindTo(map) + +const let_: { + ( + name: Exclude, + f: (r: NoInfer) => B + ): (self: Either) => Either<{ [K in N | keyof A]: K extends keyof A ? A[K] : B }, E> + ( + self: Either, + name: Exclude, + f: (r: NoInfer) => B + ): Either<{ [K in N | keyof A]: K extends keyof A ? A[K] : B }, E> +} = doNotation.let_(map) + +export { + /** + * The "do simulation" in Effect allows you to write code in a more declarative style, similar to the "do notation" in other programming languages. It provides a way to define variables and perform operations on them using functions like `bind` and `let`. + * + * Here's how the do simulation works: + * + * 1. Start the do simulation using the `Do` value + * 2. Within the do simulation scope, you can use the `bind` function to define variables and bind them to `Either` values + * 3. You can accumulate multiple `bind` statements to define multiple variables within the scope + * 4. Inside the do simulation scope, you can also use the `let` function to define variables and bind them to simple values + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { Either, pipe } from "effect" + * + * const result = pipe( + * Either.Do, + * Either.bind("x", () => Either.right(2)), + * Either.bind("y", () => Either.right(3)), + * Either.let("sum", ({ x, y }) => x + y) + * ) + * assert.deepStrictEqual(result, Either.right({ x: 2, y: 3, sum: 5 })) + * ``` + * + * @see {@link Do} + * @see {@link bindTo} + * @see {@link bind} + * + * @category do notation + * @since 2.0.0 + */ + let_ as let +} + +/** + * Converts an `Option` of an `Either` into an `Either` of an `Option`. + * + * **Details** + * + * This function transforms an `Option>` into an + * `Either, E>`. If the `Option` is `None`, the resulting `Either` + * will be a `Right` with a `None` value. If the `Option` is `Some`, the + * inner `Either` will be executed, and its result wrapped in a `Some`. + * + * @example + * ```ts + * import { Effect, Either, Option } from "effect" + * + * // ┌─── Option> + * // ▼ + * const maybe = Option.some(Either.right(42)) + * + * // ┌─── Either, never, never> + * // ▼ + * const result = Either.transposeOption(maybe) + * + * console.log(Effect.runSync(result)) + * // Output: { _id: 'Option', _tag: 'Some', value: 42 } + * ``` + * + * @since 3.14.0 + * @category Optional Wrapping & Unwrapping + */ +export const transposeOption = ( + self: Option> +): Either, E> => { + return option_.isNone(self) ? right(option_.none) : map(self.value, option_.some) +} + +/** + * Applies an `Either` on an `Option` and transposes the result. + * + * **Details** + * + * If the `Option` is `None`, the resulting `Either` will immediately succeed with a `Right` value of `None`. + * If the `Option` is `Some`, the transformation function will be applied to the inner value, and its result wrapped in a `Some`. + * + * @example + * ```ts + * import { Either, Option, pipe } from "effect" + * + * // ┌─── Either, never>> + * // ▼ + * const noneResult = pipe( + * Option.none(), + * Either.transposeMapOption(() => Either.right(42)) // will not be executed + * ) + * console.log(noneResult) + * // Output: { _id: 'Either', _tag: 'Right', right: { _id: 'Option', _tag: 'None' } } + * + * // ┌─── Either, never>> + * // ▼ + * const someRightResult = pipe( + * Option.some(42), + * Either.transposeMapOption((value) => Either.right(value * 2)) + * ) + * console.log(someRightResult) + * // Output: { _id: 'Either', _tag: 'Right', right: { _id: 'Option', _tag: 'Some', value: 84 } } + * ``` + * + * @since 3.15.0 + * @category Optional Wrapping & Unwrapping + */ +export const transposeMapOption = dual< + ( + f: (self: A) => Either + ) => (self: Option) => Either, E>, + ( + self: Option, + f: (self: A) => Either + ) => Either, E> +>(2, (self, f) => option_.isNone(self) ? right(option_.none) : map(f(self.value), option_.some)) diff --git a/repos/effect/packages/effect/src/Encoding.ts b/repos/effect/packages/effect/src/Encoding.ts new file mode 100644 index 0000000..ec2d7e1 --- /dev/null +++ b/repos/effect/packages/effect/src/Encoding.ts @@ -0,0 +1,195 @@ +/** + * This module provides encoding & decoding functionality for: + * + * - base64 (RFC4648) + * - base64 (URL) + * - hex + * + * @since 2.0.0 + */ +import * as Either from "./Either.js" +import * as Base64 from "./internal/encoding/base64.js" +import * as Base64Url from "./internal/encoding/base64Url.js" +import * as Common from "./internal/encoding/common.js" +import * as Hex from "./internal/encoding/hex.js" + +/** + * Encodes the given value into a base64 (RFC4648) `string`. + * + * @category encoding + * @since 2.0.0 + */ +export const encodeBase64: (input: Uint8Array | string) => string = (input) => + typeof input === "string" ? Base64.encode(Common.encoder.encode(input)) : Base64.encode(input) + +/** + * Decodes a base64 (RFC4648) encoded `string` into a `Uint8Array`. + * + * @category decoding + * @since 2.0.0 + */ +export const decodeBase64 = (str: string): Either.Either => Base64.decode(str) + +/** + * Decodes a base64 (RFC4648) encoded `string` into a UTF-8 `string`. + * + * @category decoding + * @since 2.0.0 + */ +export const decodeBase64String = (str: string) => Either.map(decodeBase64(str), (_) => Common.decoder.decode(_)) + +/** + * Encodes the given value into a base64 (URL) `string`. + * + * @category encoding + * @since 2.0.0 + */ +export const encodeBase64Url: (input: Uint8Array | string) => string = (input) => + typeof input === "string" ? Base64Url.encode(Common.encoder.encode(input)) : Base64Url.encode(input) + +/** + * Decodes a base64 (URL) encoded `string` into a `Uint8Array`. + * + * @category decoding + * @since 2.0.0 + */ +export const decodeBase64Url = (str: string): Either.Either => Base64Url.decode(str) + +/** + * Decodes a base64 (URL) encoded `string` into a UTF-8 `string`. + * + * @category decoding + * @since 2.0.0 + */ +export const decodeBase64UrlString = (str: string) => Either.map(decodeBase64Url(str), (_) => Common.decoder.decode(_)) + +/** + * Encodes the given value into a hex `string`. + * + * @category encoding + * @since 2.0.0 + */ +export const encodeHex: (input: Uint8Array | string) => string = (input) => + typeof input === "string" ? Hex.encode(Common.encoder.encode(input)) : Hex.encode(input) + +/** + * Decodes a hex encoded `string` into a `Uint8Array`. + * + * @category decoding + * @since 2.0.0 + */ +export const decodeHex = (str: string): Either.Either => Hex.decode(str) + +/** + * Decodes a hex encoded `string` into a UTF-8 `string`. + * + * @category decoding + * @since 2.0.0 + */ +export const decodeHexString = (str: string) => Either.map(decodeHex(str), (_) => Common.decoder.decode(_)) + +/** + * Encodes a UTF-8 `string` into a URI component `string`. + * + * @category encoding + * @since 3.12.0 + */ +export const encodeUriComponent = (str: string): Either.Either => + Either.try({ + try: () => encodeURIComponent(str), + catch: (e) => EncodeException(str, e instanceof Error ? e.message : "Invalid input") + }) + +/** + * Decodes a URI component `string` into a UTF-8 `string`. + * + * @category decoding + * @since 3.12.0 + */ +export const decodeUriComponent = (str: string): Either.Either => + Either.try({ + try: () => decodeURIComponent(str), + catch: (e) => DecodeException(str, e instanceof Error ? e.message : "Invalid input") + }) + +/** + * @since 2.0.0 + * @category symbols + */ +export const DecodeExceptionTypeId: unique symbol = Common.DecodeExceptionTypeId + +/** + * @since 2.0.0 + * @category symbols + */ +export type DecodeExceptionTypeId = typeof DecodeExceptionTypeId + +/** + * Represents a checked exception which occurs when decoding fails. + * + * @since 2.0.0 + * @category models + */ +export interface DecodeException { + readonly _tag: "DecodeException" + readonly [DecodeExceptionTypeId]: DecodeExceptionTypeId + readonly input: string + readonly message?: string +} + +/** + * Creates a checked exception which occurs when decoding fails. + * + * @since 2.0.0 + * @category errors + */ +export const DecodeException: (input: string, message?: string) => DecodeException = Common.DecodeException + +/** + * Returns `true` if the specified value is an `DecodeException`, `false` otherwise. + * + * @since 2.0.0 + * @category refinements + */ +export const isDecodeException: (u: unknown) => u is DecodeException = Common.isDecodeException + +/** + * @since 3.12.0 + * @category symbols + */ +export const EncodeExceptionTypeId: unique symbol = Common.EncodeExceptionTypeId + +/** + * @since 3.12.0 + * @category symbols + */ +export type EncodeExceptionTypeId = typeof EncodeExceptionTypeId + +/** + * Represents a checked exception which occurs when encoding fails. + * + * @since 3.12.0 + * @category models + */ +export interface EncodeException { + readonly _tag: "EncodeException" + readonly [EncodeExceptionTypeId]: EncodeExceptionTypeId + readonly input: string + readonly message?: string +} + +/** + * Creates a checked exception which occurs when encoding fails. + * + * @since 3.12.0 + * @category errors + */ +export const EncodeException: (input: string, message?: string) => EncodeException = Common.EncodeException + +/** + * Returns `true` if the specified value is an `EncodeException`, `false` otherwise. + * + * @since 3.12.0 + * @category refinements + */ +export const isEncodeException: (u: unknown) => u is EncodeException = Common.isEncodeException diff --git a/repos/effect/packages/effect/src/Equal.ts b/repos/effect/packages/effect/src/Equal.ts new file mode 100644 index 0000000..e6ca6d8 --- /dev/null +++ b/repos/effect/packages/effect/src/Equal.ts @@ -0,0 +1,101 @@ +/** + * @since 2.0.0 + */ +import type { Equivalence } from "./Equivalence.js" +import * as Hash from "./Hash.js" +import { hasProperty } from "./Predicate.js" +import { structuralRegionState } from "./Utils.js" + +/** + * @since 2.0.0 + * @category symbols + */ +export const symbol: unique symbol = Symbol.for("effect/Equal") + +/** + * @since 2.0.0 + * @category models + */ +export interface Equal extends Hash.Hash { + [symbol](that: Equal): boolean +} + +/** + * @since 2.0.0 + * @category equality + */ +export function equals(that: B): (self: A) => boolean +export function equals(self: A, that: B): boolean +export function equals(): any { + if (arguments.length === 1) { + return (self: unknown) => compareBoth(self, arguments[0]) + } + return compareBoth(arguments[0], arguments[1]) +} + +function compareBoth(self: unknown, that: unknown): boolean { + if (self === that) { + return true + } + const selfType = typeof self + if (selfType !== typeof that) { + return false + } + if (selfType === "object" || selfType === "function") { + if (self !== null && that !== null) { + if (isEqual(self) && isEqual(that)) { + if (Hash.hash(self) === Hash.hash(that) && self[symbol](that)) { + return true + } else { + return structuralRegionState.enabled && structuralRegionState.tester + ? structuralRegionState.tester(self, that) + : false + } + } else if (self instanceof Date && that instanceof Date) { + const t1 = self.getTime() + const t2 = that.getTime() + return t1 === t2 || (Number.isNaN(t1) && Number.isNaN(t2)) + } else if (self instanceof URL && that instanceof URL) { + return self.href === that.href + } + } + if (structuralRegionState.enabled) { + if (self === null || that === null) { + return false + } + if (Array.isArray(self) && Array.isArray(that)) { + return self.length === that.length && self.every((v, i) => compareBoth(v, that[i])) + } + if (Object.getPrototypeOf(self) === Object.prototype && Object.getPrototypeOf(that) === Object.prototype) { + const keysSelf = Object.keys(self as any) + const keysThat = Object.keys(that as any) + if (keysSelf.length === keysThat.length) { + for (const key of keysSelf) { + // @ts-expect-error + if (!(key in that && compareBoth(self[key], that[key]))) { + return structuralRegionState.tester ? structuralRegionState.tester(self, that) : false + } + } + return true + } + } + return structuralRegionState.tester ? structuralRegionState.tester(self, that) : false + } + } + + return structuralRegionState.enabled && structuralRegionState.tester + ? structuralRegionState.tester(self, that) + : false +} + +/** + * @since 2.0.0 + * @category guards + */ +export const isEqual = (u: unknown): u is Equal => hasProperty(u, symbol) + +/** + * @since 2.0.0 + * @category instances + */ +export const equivalence: () => Equivalence = () => equals diff --git a/repos/effect/packages/effect/src/Equivalence.ts b/repos/effect/packages/effect/src/Equivalence.ts new file mode 100644 index 0000000..dfb6a11 --- /dev/null +++ b/repos/effect/packages/effect/src/Equivalence.ts @@ -0,0 +1,235 @@ +/** + * This module provides an implementation of the `Equivalence` type class, which defines a binary relation + * that is reflexive, symmetric, and transitive. In other words, it defines a notion of equivalence between values of a certain type. + * These properties are also known in mathematics as an "equivalence relation". + * + * @since 2.0.0 + */ +import { dual } from "./Function.js" +import type { TypeLambda } from "./HKT.js" + +/** + * @category type class + * @since 2.0.0 + */ +export interface Equivalence { + (self: A, that: A): boolean +} + +/** + * @category type lambdas + * @since 2.0.0 + */ +export interface EquivalenceTypeLambda extends TypeLambda { + readonly type: Equivalence +} + +/** + * @category constructors + * @since 2.0.0 + */ +export const make = (isEquivalent: (self: A, that: A) => boolean): Equivalence => (self: A, that: A): boolean => + self === that || isEquivalent(self, that) + +const isStrictEquivalent = (x: unknown, y: unknown) => x === y + +/** + * Return an `Equivalence` that uses strict equality (===) to compare values. + * + * @since 2.0.0 + * @category constructors + */ +export const strict: () => Equivalence = () => isStrictEquivalent + +/** + * @category instances + * @since 2.0.0 + */ +export const string: Equivalence = strict() + +/** + * @category instances + * @since 2.0.0 + */ +export const number: Equivalence = strict() + +/** + * @category instances + * @since 2.0.0 + */ +export const boolean: Equivalence = strict() + +/** + * @category instances + * @since 2.0.0 + */ +export const bigint: Equivalence = strict() + +/** + * @category instances + * @since 2.0.0 + */ +export const symbol: Equivalence = strict() + +/** + * @category combining + * @since 2.0.0 + */ +export const combine: { + (that: Equivalence): (self: Equivalence) => Equivalence + (self: Equivalence, that: Equivalence): Equivalence +} = dual(2, (self: Equivalence, that: Equivalence): Equivalence => make((x, y) => self(x, y) && that(x, y))) + +/** + * @category combining + * @since 2.0.0 + */ +export const combineMany: { + (collection: Iterable>): (self: Equivalence) => Equivalence + (self: Equivalence, collection: Iterable>): Equivalence +} = dual(2, (self: Equivalence, collection: Iterable>): Equivalence => + make((x, y) => { + if (!self(x, y)) { + return false + } + for (const equivalence of collection) { + if (!equivalence(x, y)) { + return false + } + } + return true + })) + +const isAlwaysEquivalent: Equivalence = (_x, _y) => true + +/** + * @category combining + * @since 2.0.0 + */ +export const combineAll = (collection: Iterable>): Equivalence => + combineMany(isAlwaysEquivalent, collection) + +/** + * @category mapping + * @since 2.0.0 + */ +export const mapInput: { + (f: (b: B) => A): (self: Equivalence) => Equivalence + (self: Equivalence, f: (b: B) => A): Equivalence +} = dual( + 2, + (self: Equivalence, f: (b: B) => A): Equivalence => make((x, y) => self(f(x), f(y))) +) + +/** + * @category instances + * @since 2.0.0 + */ +export const Date: Equivalence = mapInput(number, (date) => date.getTime()) + +/** + * @category combining + * @since 2.0.0 + */ +export const product: { + (that: Equivalence): (self: Equivalence) => Equivalence // readonly because invariant + (self: Equivalence, that: Equivalence): Equivalence // readonly because invariant +} = dual( + 2, + (self: Equivalence, that: Equivalence): Equivalence => + make(([xa, xb], [ya, yb]) => self(xa, ya) && that(xb, yb)) +) + +/** + * @category combining + * @since 2.0.0 + */ +export const all = (collection: Iterable>): Equivalence> => { + return make((x, y) => { + const len = Math.min(x.length, y.length) + + let collectionLength = 0 + for (const equivalence of collection) { + if (collectionLength >= len) { + break + } + if (!equivalence(x[collectionLength], y[collectionLength])) { + return false + } + collectionLength++ + } + return true + }) +} + +/** + * @category combining + * @since 2.0.0 + */ +export const productMany = ( + self: Equivalence, + collection: Iterable> +): Equivalence]> /* readonly because invariant */ => { + const equivalence = all(collection) + return make((x, y) => !self(x[0], y[0]) ? false : equivalence(x.slice(1), y.slice(1))) +} + +/** + * Similar to `Promise.all` but operates on `Equivalence`s. + * + * ```ts skip-type-checking + * [Equivalence, Equivalence, ...] -> Equivalence<[A, B, ...]> + * ``` + * + * Given a tuple of `Equivalence`s returns a new `Equivalence` that compares values of a tuple + * by applying each `Equivalence` to the corresponding element of the tuple. + * + * @category combinators + * @since 2.0.0 + */ +export const tuple = >>( + ...elements: T +): Equivalence] ? A : never }>> => all(elements) as any + +/** + * Creates a new `Equivalence` for an array of values based on a given `Equivalence` for the elements of the array. + * + * @category combinators + * @since 2.0.0 + */ +export const array = (item: Equivalence): Equivalence> => + make((self, that) => { + if (self.length !== that.length) { + return false + } + + for (let i = 0; i < self.length; i++) { + const isEq = item(self[i], that[i]) + if (!isEq) { + return false + } + } + + return true + }) + +/** + * Given a struct of `Equivalence`s returns a new `Equivalence` that compares values of a struct + * by applying each `Equivalence` to the corresponding property of the struct. + * + * @category combinators + * @since 2.0.0 + */ +export const struct = >>( + fields: R +): Equivalence<{ readonly [K in keyof R]: [R[K]] extends [Equivalence] ? A : never }> => { + const keys = Object.keys(fields) + return make((self, that) => { + for (const key of keys) { + if (!fields[key](self[key], that[key])) { + return false + } + } + return true + }) +} diff --git a/repos/effect/packages/effect/src/ExecutionPlan.ts b/repos/effect/packages/effect/src/ExecutionPlan.ts new file mode 100644 index 0000000..d928389 --- /dev/null +++ b/repos/effect/packages/effect/src/ExecutionPlan.ts @@ -0,0 +1,308 @@ +/** + * @since 3.16.0 + * @experimental + */ +import type { NonEmptyReadonlyArray } from "./Array.js" +import type * as Context from "./Context.js" +import * as Effect from "./Effect.js" +import * as internal from "./internal/executionPlan.js" +import * as Layer from "./Layer.js" +import type { Pipeable } from "./Pipeable.js" +import { pipeArguments } from "./Pipeable.js" +import type * as Schedule from "./Schedule.js" + +/** + * @since 3.16.0 + * @category Symbols + * @experimental + */ +export const TypeId: unique symbol = internal.TypeId + +/** + * @since 3.16.0 + * @category Symbols + * @experimental + */ +export type TypeId = typeof TypeId + +/** + * @since 3.16.0 + * @category Guards + * @experimental + */ +export const isExecutionPlan: (u: unknown) => u is ExecutionPlan = internal.isExecutionPlan + +/** + * A `ExecutionPlan` can be used with `Effect.withExecutionPlan` or `Stream.withExecutionPlan`, allowing you to provide different resources for each step of execution until the effect succeeds or the plan is exhausted. + * + * ```ts + * import type { LanguageModel } from "@effect/ai" + * import type { Layer } from "effect" + * import { Effect, ExecutionPlan, Schedule } from "effect" + * + * declare const layerBad: Layer.Layer + * declare const layerGood: Layer.Layer + * + * const ThePlan = ExecutionPlan.make( + * { + * // First try with the bad layer 2 times with a 3 second delay between attempts + * provide: layerBad, + * attempts: 2, + * schedule: Schedule.spaced(3000) + * }, + * // Then try with the bad layer 3 times with a 1 second delay between attempts + * { + * provide: layerBad, + * attempts: 3, + * schedule: Schedule.spaced(1000) + * }, + * // Finally try with the good layer. + * // + * // If `attempts` is omitted, the plan will only attempt once, unless a schedule is provided. + * { + * provide: layerGood + * } + * ) + * + * declare const effect: Effect.Effect< + * void, + * never, + * LanguageModel.LanguageModel + * > + * const withPlan: Effect.Effect = Effect.withExecutionPlan(effect, ThePlan) + * ``` + * + * @since 3.16.0 + * @category Models + * @experimental + */ +export interface ExecutionPlan< + Types extends { + provides: any + input: any + error: any + requirements: any + } +> extends Pipeable { + readonly [TypeId]: TypeId + readonly steps: NonEmptyReadonlyArray<{ + readonly provide: + | Context.Context + | Layer.Layer + readonly attempts?: number | undefined + readonly while?: + | ((input: Types["input"]) => Effect.Effect) + | undefined + readonly schedule?: Schedule.Schedule | undefined + }> + + /** + * Returns an equivalent `ExecutionPlan` with the requirements satisfied, + * using the current context. + */ + readonly withRequirements: Effect.Effect< + ExecutionPlan<{ + provides: Types["provides"] + input: Types["input"] + error: Types["error"] + requirements: never + }>, + never, + Types["requirements"] + > +} + +/** + * @since 3.16.0 + * @experimental + */ +export type TypesBase = { + provides: any + input: any + error: any + requirements: any +} + +/** + * Create an `ExecutionPlan`, which can be used with `Effect.withExecutionPlan` or `Stream.withExecutionPlan`, allowing you to provide different resources for each step of execution until the effect succeeds or the plan is exhausted. + * + * ```ts + * import type { LanguageModel } from "@effect/ai" + * import type { Layer } from "effect" + * import { Effect, ExecutionPlan, Schedule } from "effect" + * + * declare const layerBad: Layer.Layer + * declare const layerGood: Layer.Layer + * + * const ThePlan = ExecutionPlan.make( + * { + * // First try with the bad layer 2 times with a 3 second delay between attempts + * provide: layerBad, + * attempts: 2, + * schedule: Schedule.spaced(3000) + * }, + * // Then try with the bad layer 3 times with a 1 second delay between attempts + * { + * provide: layerBad, + * attempts: 3, + * schedule: Schedule.spaced(1000) + * }, + * // Finally try with the good layer. + * // + * // If `attempts` is omitted, the plan will only attempt once, unless a schedule is provided. + * { + * provide: layerGood + * } + * ) + * + * declare const effect: Effect.Effect< + * void, + * never, + * LanguageModel.LanguageModel + * > + * const withPlan: Effect.Effect = Effect.withExecutionPlan(effect, ThePlan) + * ``` + * + * @since 3.16.0 + * @category Constructors + * @experimental + */ +export const make = >( + ...steps: Steps & { [K in keyof Steps]: make.Step } +): ExecutionPlan<{ + provides: make.StepProvides + input: make.StepInput + error: + | (Steps[number]["provide"] extends Context.Context | Layer.Layer ? E + : never) + | (Steps[number]["while"] extends (input: infer _I) => Effect.Effect ? _E : never) + requirements: + | (Steps[number]["provide"] extends Layer.Layer ? R : never) + | (Steps[number]["while"] extends (input: infer _I) => Effect.Effect ? R : never) + | (Steps[number]["schedule"] extends Schedule.Schedule ? R : never) +}> => + makeProto(steps.map((options, i) => { + if (options.attempts && options.attempts < 1) { + throw new Error(`ExecutionPlan.make: step[${i}].attempts must be greater than 0`) + } + return { + schedule: options.schedule, + attempts: options.attempts, + while: options.while + ? (input: any) => + Effect.suspend(() => { + const result = options.while!(input) + return typeof result === "boolean" ? Effect.succeed(result) : result + }) + : undefined, + provide: options.provide + } + }) as any) + +/** + * @since 3.16.0 + * @experimental + */ +export declare namespace make { + /** + * @since 3.16.0 + * @experimental + */ + export type Step = { + readonly provide: Context.Context | Context.Context | Layer.Layer.Any + readonly attempts?: number | undefined + readonly while?: ((input: any) => boolean | Effect.Effect) | undefined + readonly schedule?: Schedule.Schedule | undefined + } + + /** + * @since 3.16.1 + * @experimental + */ + export type StepProvides, Out = unknown> = Steps extends + readonly [infer Step, ...infer Rest] ? StepProvides< + Rest, + & Out + & ( + (Step extends { readonly provide: Context.Context | Layer.Layer } ? P + : unknown) + ) + > : + Out + + /** + * @since 3.16.1 + * @experimental + */ + export type PlanProvides, Out = unknown> = Plans extends + readonly [infer Plan, ...infer Rest] ? + PlanProvides ? T["provides"] : unknown)> : + Out + + /** + * @since 3.16.0 + * @experimental + */ + export type StepInput, Out = unknown> = Steps extends + readonly [infer Step, ...infer Rest] ? StepInput< + Rest, + & Out + & ( + & (Step extends { readonly while: (input: infer I) => infer _ } ? I : unknown) + & (Step extends { readonly schedule: Schedule.Schedule } ? I : unknown) + ) + > : + Out + + /** + * @since 3.16.0 + * @experimental + */ + export type PlanInput, Out = unknown> = Plans extends + readonly [infer Plan, ...infer Rest] ? + PlanInput ? T["input"] : unknown)> : + Out +} + +const Proto: Omit, "steps"> = { + [TypeId]: TypeId, + get withRequirements() { + const self = this as any as ExecutionPlan + return Effect.contextWith((context: Context.Context) => + makeProto(self.steps.map((step) => ({ + ...step, + provide: Layer.isLayer(step.provide) ? Layer.provide(step.provide, Layer.succeedContext(context)) : step.provide + })) as any) + ) + }, + pipe() { + return pipeArguments(this, arguments) + } +} + +const makeProto = ( + steps: ExecutionPlan<{ + provides: Provides + input: In + error: PlanE + requirements: PlanR + }>["steps"] +) => { + const self = Object.create(Proto) + self.steps = steps + return self +} + +/** + * @since 3.16.0 + * @category Combining + * @experimental + */ +export const merge = >>( + ...plans: Plans +): ExecutionPlan<{ + provides: make.PlanProvides + input: make.PlanInput + error: Plans[number] extends ExecutionPlan ? T["error"] : never + requirements: Plans[number] extends ExecutionPlan ? T["requirements"] : never +}> => makeProto(plans.flatMap((plan) => plan.steps) as any) diff --git a/repos/effect/packages/effect/src/ExecutionStrategy.ts b/repos/effect/packages/effect/src/ExecutionStrategy.ts new file mode 100644 index 0000000..5dba91e --- /dev/null +++ b/repos/effect/packages/effect/src/ExecutionStrategy.ts @@ -0,0 +1,119 @@ +/** + * @since 2.0.0 + */ +import type { LazyArg } from "./Function.js" +import * as internal from "./internal/executionStrategy.js" + +/** + * Describes a strategy for evaluating multiple effects, potentially in + * parallel. + * + * There are 3 possible execution strategies: `Sequential`, `Parallel`, + * `ParallelN`. + * + * @since 2.0.0 + * @category models + */ +export type ExecutionStrategy = Sequential | Parallel | ParallelN + +/** + * Execute effects sequentially. + * + * @since 2.0.0 + * @category models + */ +export interface Sequential { + readonly _tag: "Sequential" +} + +/** + * Execute effects in parallel. + * + * @since 2.0.0 + * @category models + */ +export interface Parallel { + readonly _tag: "Parallel" +} + +/** + * Execute effects in parallel, up to the specified number of concurrent fibers. + * + * @since 2.0.0 + * @category models + */ +export interface ParallelN { + readonly _tag: "ParallelN" + readonly parallelism: number +} + +/** + * Execute effects sequentially. + * + * @since 2.0.0 + * @category constructors + */ +export const sequential: ExecutionStrategy = internal.sequential + +/** + * Execute effects in parallel. + * + * @since 2.0.0 + * @category constructors + */ +export const parallel: ExecutionStrategy = internal.parallel + +/** + * Execute effects in parallel, up to the specified number of concurrent fibers. + * + * @since 2.0.0 + * @category constructors + */ +export const parallelN: (parallelism: number) => ExecutionStrategy = internal.parallelN + +/** + * Returns `true` if the specified `ExecutionStrategy` is an instance of + * `Sequential`, `false` otherwise. + * + * @since 2.0.0 + * @category refinements + */ +export const isSequential: (self: ExecutionStrategy) => self is Sequential = internal.isSequential + +/** + * Returns `true` if the specified `ExecutionStrategy` is an instance of + * `Sequential`, `false` otherwise. + * + * @since 2.0.0 + * @category refinements + */ +export const isParallel: (self: ExecutionStrategy) => self is Parallel = internal.isParallel + +/** + * Returns `true` if the specified `ExecutionStrategy` is an instance of + * `Sequential`, `false` otherwise. + * + * @since 2.0.0 + * @category refinements + */ +export const isParallelN: (self: ExecutionStrategy) => self is ParallelN = internal.isParallelN + +/** + * Folds over the specified `ExecutionStrategy` using the provided case + * functions. + * + * @since 2.0.0 + * @category folding + */ +export const match: { + (options: { + readonly onSequential: LazyArg + readonly onParallel: LazyArg + readonly onParallelN: (n: number) => A + }): (self: ExecutionStrategy) => A + (self: ExecutionStrategy, options: { + readonly onSequential: LazyArg + readonly onParallel: LazyArg + readonly onParallelN: (n: number) => A + }): A +} = internal.match diff --git a/repos/effect/packages/effect/src/Exit.ts b/repos/effect/packages/effect/src/Exit.ts new file mode 100644 index 0000000..780682a --- /dev/null +++ b/repos/effect/packages/effect/src/Exit.ts @@ -0,0 +1,467 @@ +/** + * @since 2.0.0 + */ +import type * as Cause from "./Cause.js" +import type * as Effect from "./Effect.js" +import type * as Either from "./Either.js" +import type * as FiberId from "./FiberId.js" +import type { Inspectable } from "./Inspectable.js" +import * as core from "./internal/core.js" +import type * as Option from "./Option.js" +import type { Pipeable } from "./Pipeable.js" +import type { Predicate, Refinement } from "./Predicate.js" +import type { NoInfer } from "./Types.js" +import type * as Unify from "./Unify.js" + +/** + * An `Exit` describes the result of a executing an `Effect` workflow. + * + * There are two possible values for an `Exit`: + * - `Exit.Success` contain a success value of type `A` + * - `Exit.Failure` contains a failure `Cause` of type `E` + * + * @since 2.0.0 + * @category models + */ +export type Exit = Success | Failure + +/** + * Represents a failed `Effect` workflow containing the `Cause` of the failure + * of type `E`. + * + * @since 2.0.0 + * @category models + */ +export interface Failure extends Effect.Effect, Pipeable, Inspectable { + readonly _tag: "Failure" + readonly _op: "Failure" + readonly cause: Cause.Cause + [Unify.typeSymbol]?: unknown + [Unify.unifySymbol]?: ExitUnify + [Unify.ignoreSymbol]?: ExitUnifyIgnore + /** @internal */ + readonly effect_instruction_i0: Cause.Cause +} + +/** + * @category models + * @since 2.0.0 + */ +export interface ExitUnify extends Effect.EffectUnify { + Exit?: () => A[Unify.typeSymbol] extends Exit | infer _ ? Exit : never +} + +/** + * @category models + * @since 2.0.0 + */ +export interface ExitUnifyIgnore extends Effect.EffectUnifyIgnore { + Effect?: true +} + +/** + * Represents a successful `Effect` workflow and containing the returned value + * of type `A`. + * + * @since 2.0.0 + * @category models + */ +export interface Success extends Effect.Effect, Pipeable, Inspectable { + readonly _tag: "Success" + readonly _op: "Success" + readonly value: A + [Unify.typeSymbol]?: unknown + [Unify.unifySymbol]?: ExitUnify + [Unify.ignoreSymbol]?: ExitUnifyIgnore + /** @internal */ + readonly effect_instruction_i0: A +} + +/** + * Returns `true` if the specified value is an `Exit`, `false` otherwise. + * + * @since 2.0.0 + * @category refinements + */ +export const isExit: (u: unknown) => u is Exit = core.exitIsExit + +/** + * Returns `true` if the specified `Exit` is a `Failure`, `false` otherwise. + * + * @since 2.0.0 + * @category refinements + */ +export const isFailure: (self: Exit) => self is Failure = core.exitIsFailure + +/** + * Returns `true` if the specified `Exit` is a `Success`, `false` otherwise. + * + * @since 2.0.0 + * @category refinements + */ +export const isSuccess: (self: Exit) => self is Success = core.exitIsSuccess + +/** + * Returns `true` if the specified exit is a `Failure` **and** the `Cause` of + * the failure was due to interruption, `false` otherwise. + * + * @since 2.0.0 + * @category getters + */ +export const isInterrupted: (self: Exit) => boolean = core.exitIsInterrupted + +/** + * Maps the `Success` value of the specified exit to the provided constant + * value. + * + * @since 2.0.0 + * @category mapping + */ +export const as: { + (value: A2): (self: Exit) => Exit + (self: Exit, value: A2): Exit +} = core.exitAs + +/** + * Maps the `Success` value of the specified exit to a void. + * + * @since 2.0.0 + * @category mapping + */ +export const asVoid: (self: Exit) => Exit = core.exitAsVoid + +/** + * Returns a `Some>` if the specified exit is a `Failure`, `None` + * otherwise. + * + * @since 2.0.0 + * @category getters + */ +export const causeOption: (self: Exit) => Option.Option> = core.exitCauseOption + +/** + * Collects all of the specified exit values into a `Some, E>>`. If + * the provided iterable contains no elements, `None` will be returned. + * + * @since 2.0.0 + * @category constructors + */ +export const all: ( + exits: Iterable>, + options?: { readonly parallel?: boolean | undefined } | undefined +) => Option.Option, E>> = core.exitCollectAll + +/** + * Constructs a new `Exit.Failure` from the specified unrecoverable defect. + * + * @since 2.0.0 + * @category constructors + */ +export const die: (defect: unknown) => Exit = core.exitDie + +/** + * Executes the predicate on the value of the specified exit if it is a + * `Success`, otherwise returns `false`. + * + * @since 2.0.0 + * @category elements + */ +export const exists: { + (refinement: Refinement, B>): (self: Exit) => self is Exit + (predicate: Predicate>): (self: Exit) => boolean + (self: Exit, refinement: Refinement): self is Exit + (self: Exit, predicate: Predicate): boolean +} = core.exitExists + +/** + * Constructs a new `Exit.Failure` from the specified recoverable error of type + * `E`. + * + * @since 2.0.0 + * @category constructors + */ +export const fail: (error: E) => Exit = core.exitFail + +/** + * Constructs a new `Exit.Failure` from the specified `Cause` of type `E`. + * + * @since 2.0.0 + * @category constructors + */ +export const failCause: (cause: Cause.Cause) => Exit = core.exitFailCause + +/** + * @since 2.0.0 + * @category sequencing + */ +export const flatMap: { + (f: (a: A) => Exit): (self: Exit) => Exit + (self: Exit, f: (a: A) => Exit): Exit +} = core.exitFlatMap + +/** + * @since 2.0.0 + * @category sequencing + */ +export const flatMapEffect: { + ( + f: (a: A) => Effect.Effect, E2, R> + ): (self: Exit) => Effect.Effect, E2, R> + (self: Exit, f: (a: A) => Effect.Effect, E2, R>): Effect.Effect, E2, R> +} = core.exitFlatMapEffect + +/** + * @since 2.0.0 + * @category sequencing + */ +export const flatten: (self: Exit, E2>) => Exit = core.exitFlatten + +/** + * @since 2.0.0 + * @category traversing + */ +export const forEachEffect: { + (f: (a: A) => Effect.Effect): (self: Exit) => Effect.Effect, never, R> + (self: Exit, f: (a: A) => Effect.Effect): Effect.Effect, never, R> +} = core.exitForEachEffect + +/** + * Converts an `Either` into an `Exit`. + * + * @since 2.0.0 + * @category conversions + */ +export const fromEither: (either: Either.Either) => Exit = core.exitFromEither + +/** + * Converts an `Option` into an `Exit`. + * + * @since 2.0.0 + * @category conversions + */ +export const fromOption: (option: Option.Option) => Exit = core.exitFromOption + +/** + * Returns the `A` if specified exit is a `Success`, otherwise returns the + * alternate `A` value computed from the specified function which receives the + * `Cause` of the exit `Failure`. + * + * @since 2.0.0 + * @category getters + */ +export const getOrElse: { + (orElse: (cause: Cause.Cause) => A2): (self: Exit) => A2 | A + (self: Exit, orElse: (cause: Cause.Cause) => A2): A | A2 +} = core.exitGetOrElse + +/** + * Constructs a new `Exit.Failure` from the specified `FiberId` indicating that + * the `Fiber` running an `Effect` workflow was terminated due to interruption. + * + * @since 2.0.0 + * @category constructors + */ +export const interrupt: (fiberId: FiberId.FiberId) => Exit = core.exitInterrupt + +/** + * Maps over the `Success` value of the specified exit using the provided + * function. + * + * @since 2.0.0 + * @category mapping + */ +export const map: { + (f: (a: A) => B): (self: Exit) => Exit + (self: Exit, f: (a: A) => B): Exit +} = core.exitMap + +/** + * Maps over the `Success` and `Failure` cases of the specified exit using the + * provided functions. + * + * @since 2.0.0 + * @category mapping + */ +export const mapBoth: { + ( + options: { readonly onFailure: (e: E) => E2; readonly onSuccess: (a: A) => A2 } + ): (self: Exit) => Exit + ( + self: Exit, + options: { readonly onFailure: (e: E) => E2; readonly onSuccess: (a: A) => A2 } + ): Exit +} = core.exitMapBoth + +/** + * Maps over the error contained in the `Failure` of the specified exit using + * the provided function. + * + * @since 2.0.0 + * @category mapping + */ +export const mapError: { + (f: (e: E) => E2): (self: Exit) => Exit + (self: Exit, f: (e: E) => E2): Exit +} = core.exitMapError + +/** + * Maps over the `Cause` contained in the `Failure` of the specified exit using + * the provided function. + * + * @since 2.0.0 + * @category mapping + */ +export const mapErrorCause: { + (f: (cause: Cause.Cause) => Cause.Cause): (self: Exit) => Exit + (self: Exit, f: (cause: Cause.Cause) => Cause.Cause): Exit +} = core.exitMapErrorCause + +/** + * @since 2.0.0 + * @category folding + */ +export const match: { + ( + options: { readonly onFailure: (cause: Cause.Cause) => Z1; readonly onSuccess: (a: A) => Z2 } + ): (self: Exit) => Z1 | Z2 + ( + self: Exit, + options: { readonly onFailure: (cause: Cause.Cause) => Z1; readonly onSuccess: (a: A) => Z2 } + ): Z1 | Z2 +} = core.exitMatch + +/** + * @since 2.0.0 + * @category folding + */ +export const matchEffect: { + ( + options: { + readonly onFailure: (cause: Cause.Cause) => Effect.Effect + readonly onSuccess: (a: A) => Effect.Effect + } + ): (self: Exit) => Effect.Effect + ( + self: Exit, + options: { + readonly onFailure: (cause: Cause.Cause) => Effect.Effect + readonly onSuccess: (a: A) => Effect.Effect + } + ): Effect.Effect +} = core.exitMatchEffect + +/** + * Constructs a new `Exit.Success` containing the specified value of type `A`. + * + * @since 2.0.0 + * @category constructors + */ +export const succeed: (value: A) => Exit = core.exitSucceed + +const void_: Exit = core.exitVoid +export { + /** + * Represents an `Exit` which succeeds with `undefined`. + * + * @since 2.0.0 + * @category constructors + */ + void_ as void +} + +/** + * Sequentially zips the this result with the specified result or else returns + * the failed `Cause`. + * + * @since 2.0.0 + * @category zipping + */ +export const zip: { + (that: Exit): (self: Exit) => Exit<[A, A2], E2 | E> + (self: Exit, that: Exit): Exit<[A, A2], E | E2> +} = core.exitZip + +/** + * Sequentially zips the this result with the specified result discarding the + * second element of the tuple or else returns the failed `Cause`. + * + * @since 2.0.0 + * @category zipping + */ +export const zipLeft: { + (that: Exit): (self: Exit) => Exit + (self: Exit, that: Exit): Exit +} = core.exitZipLeft + +/** + * Sequentially zips the this result with the specified result discarding the + * first element of the tuple or else returns the failed `Cause`. + * + * @since 2.0.0 + * @category zipping + */ +export const zipRight: { + (that: Exit): (self: Exit) => Exit + (self: Exit, that: Exit): Exit +} = core.exitZipRight + +/** + * Parallelly zips the this result with the specified result or else returns + * the failed `Cause`. + * + * @since 2.0.0 + * @category zipping + */ +export const zipPar: { + (that: Exit): (self: Exit) => Exit<[A, A2], E2 | E> + (self: Exit, that: Exit): Exit<[A, A2], E | E2> +} = core.exitZipPar + +/** + * Parallelly zips the this result with the specified result discarding the + * second element of the tuple or else returns the failed `Cause`. + * + * @since 2.0.0 + * @category zipping + */ +export const zipParLeft: { + (that: Exit): (self: Exit) => Exit + (self: Exit, that: Exit): Exit +} = core.exitZipParLeft + +/** + * Parallelly zips the this result with the specified result discarding the + * first element of the tuple or else returns the failed `Cause`. + * + * @since 2.0.0 + * @category zipping + */ +export const zipParRight: { + (that: Exit): (self: Exit) => Exit + (self: Exit, that: Exit): Exit +} = core.exitZipParRight + +/** + * Zips this exit together with that exit using the specified combination + * functions. + * + * @since 2.0.0 + * @category zipping + */ +export const zipWith: { + ( + that: Exit, + options: { + readonly onSuccess: (a: A, b: B) => C + readonly onFailure: (cause: Cause.Cause, cause2: Cause.Cause) => Cause.Cause + } + ): (self: Exit) => Exit + ( + self: Exit, + that: Exit, + options: { + readonly onSuccess: (a: A, b: B) => C + readonly onFailure: (cause: Cause.Cause, cause2: Cause.Cause) => Cause.Cause + } + ): Exit +} = core.exitZipWith diff --git a/repos/effect/packages/effect/src/FastCheck.ts b/repos/effect/packages/effect/src/FastCheck.ts new file mode 100644 index 0000000..b5a3803 --- /dev/null +++ b/repos/effect/packages/effect/src/FastCheck.ts @@ -0,0 +1,9 @@ +/** + * @since 3.10.0 + */ + +/** + * @category re-exports + * @since 3.10.0 + */ +export * from "fast-check" diff --git a/repos/effect/packages/effect/src/Fiber.ts b/repos/effect/packages/effect/src/Fiber.ts new file mode 100644 index 0000000..3c3e760 --- /dev/null +++ b/repos/effect/packages/effect/src/Fiber.ts @@ -0,0 +1,744 @@ +/** + * @since 2.0.0 + */ +import type * as Cause from "./Cause.js" +import type { Context } from "./Context.js" +import type { DefaultServices } from "./DefaultServices.js" +import type * as Effect from "./Effect.js" +import type * as Either from "./Either.js" +import type * as Exit from "./Exit.js" +import type * as FiberId from "./FiberId.js" +import type { FiberRef } from "./FiberRef.js" +import type * as FiberRefs from "./FiberRefs.js" +import type * as FiberStatus from "./FiberStatus.js" +import type * as HashSet from "./HashSet.js" +import * as core from "./internal/core.js" +import * as circular from "./internal/effect/circular.js" +import * as internal from "./internal/fiber.js" +import * as fiberRuntime from "./internal/fiberRuntime.js" +import type * as Option from "./Option.js" +import type * as order from "./Order.js" +import type * as RuntimeFlags from "./RuntimeFlags.js" +import type { Scheduler } from "./Scheduler.js" +import type * as Scope from "./Scope.js" +import type { Supervisor } from "./Supervisor.js" +import type { AnySpan, Tracer } from "./Tracer.js" +import type * as Types from "./Types.js" +import type * as Unify from "./Unify.js" + +/** + * @since 2.0.0 + * @category symbols + */ +export const FiberTypeId: unique symbol = internal.FiberTypeId + +/** + * @since 2.0.0 + * @category symbols + */ +export type FiberTypeId = typeof FiberTypeId + +/** + * @since 2.0.0 + * @category symbols + */ +export const RuntimeFiberTypeId: unique symbol = internal.RuntimeFiberTypeId + +/** + * @since 2.0.0 + * @category symbols + */ +export type RuntimeFiberTypeId = typeof RuntimeFiberTypeId + +/** + * A fiber is a lightweight thread of execution that never consumes more than a + * whole thread (but may consume much less, depending on contention and + * asynchronicity). Fibers are spawned by forking effects, which run + * concurrently with the parent effect. + * + * Fibers can be joined, yielding their result to other fibers, or interrupted, + * which terminates the fiber, safely releasing all resources. + * + * @since 2.0.0 + * @category models + */ +export interface Fiber extends Effect.Effect, Fiber.Variance { + /** + * The identity of the fiber. + */ + id(): FiberId.FiberId + + /** + * Awaits the fiber, which suspends the awaiting fiber until the result of the + * fiber has been determined. + */ + readonly await: Effect.Effect> + + /** + * Retrieves the immediate children of the fiber. + */ + readonly children: Effect.Effect>> + + /** + * Inherits values from all `FiberRef` instances into current fiber. This + * will resume immediately. + */ + readonly inheritAll: Effect.Effect + + /** + * Tentatively observes the fiber, but returns immediately if it is not + * already done. + */ + readonly poll: Effect.Effect>> + + /** + * In the background, interrupts the fiber as if interrupted from the + * specified fiber. If the fiber has already exited, the returned effect will + * resume immediately. Otherwise, the effect will resume when the fiber exits. + */ + interruptAsFork(fiberId: FiberId.FiberId): Effect.Effect + + readonly [Unify.typeSymbol]?: unknown + readonly [Unify.unifySymbol]?: FiberUnify + readonly [Unify.ignoreSymbol]?: FiberUnifyIgnore +} + +/** + * @category models + * @since 3.8.0 + */ +export interface FiberUnify extends Effect.EffectUnify { + Fiber?: () => A[Unify.typeSymbol] extends Fiber | infer _ ? Fiber : never +} + +/** + * @category models + * @since 3.8.0 + */ +export interface FiberUnifyIgnore extends Effect.EffectUnifyIgnore { + Effect?: true +} + +/** + * A runtime fiber that is executing an effect. Runtime fibers have an + * identity and a trace. + * + * @since 2.0.0 + * @category models + */ +export interface RuntimeFiber extends Fiber, Fiber.RuntimeVariance { + /** + * Reads the current number of ops that have occurred since the last yield + */ + get currentOpCount(): number + + /** + * Reads the current value of a fiber ref + */ + getFiberRef(fiberRef: FiberRef): X + + /** + * The identity of the fiber. + */ + id(): FiberId.Runtime + + /** + * The status of the fiber. + */ + readonly status: Effect.Effect + + /** + * Returns the current `RuntimeFlags` the fiber is running with. + */ + readonly runtimeFlags: Effect.Effect + + /** + * Adds an observer to the list of observers. + */ + addObserver(observer: (exit: Exit.Exit) => void): void + + /** + * Removes the specified observer from the list of observers that will be + * notified when the fiber exits. + */ + removeObserver(observer: (exit: Exit.Exit) => void): void + + /** + * Retrieves all fiber refs of the fiber. + */ + getFiberRefs(): FiberRefs.FiberRefs + + /** + * Unsafely observes the fiber, but returns immediately if it is not + * already done. + */ + unsafePoll(): Exit.Exit | null + + /** + * In the background, interrupts the fiber as if interrupted from the + * specified fiber. If the fiber has already exited, the returned effect will + * resume immediately. Otherwise, the effect will resume when the fiber exits. + */ + unsafeInterruptAsFork(fiberId: FiberId.FiberId): void + + /** + * Gets the current context + */ + get currentContext(): Context + + /** + * Gets the current context + */ + get currentDefaultServices(): Context + + /** + * Gets the current scheduler + */ + get currentScheduler(): Scheduler + + /** + * Gets the current tracer + */ + get currentTracer(): Tracer + + /** + * Gets the current span + */ + get currentSpan(): AnySpan | undefined + + /** + * Gets the current supervisor + */ + get currentSupervisor(): Supervisor + + readonly [Unify.typeSymbol]?: unknown + readonly [Unify.unifySymbol]?: RuntimeFiberUnify + readonly [Unify.ignoreSymbol]?: RuntimeFiberUnifyIgnore +} + +/** + * @category models + * @since 3.8.0 + */ +export interface RuntimeFiberUnify extends FiberUnify { + RuntimeFiber?: () => A[Unify.typeSymbol] extends RuntimeFiber | infer _ ? RuntimeFiber + : never +} + +/** + * @category models + * @since 3.8.0 + */ +export interface RuntimeFiberUnifyIgnore extends FiberUnifyIgnore { + Fiber?: true +} + +/** + * @since 2.0.0 + */ +export declare namespace Fiber { + /** + * @since 2.0.0 + * @category models + */ + export type Runtime = RuntimeFiber + + /** + * @since 2.0.0 + * @category models + */ + export interface Variance { + readonly [FiberTypeId]: { + readonly _A: Types.Covariant + readonly _E: Types.Covariant + } + } + + /** + * @since 2.0.0 + */ + export interface RuntimeVariance { + readonly [RuntimeFiberTypeId]: { + readonly _A: Types.Covariant + readonly _E: Types.Covariant + } + } + + /** + * @since 2.0.0 + * @category models + */ + export interface Dump { + /** + * The fiber's unique identifier. + */ + readonly id: FiberId.Runtime + /** + * The status of the fiber. + */ + readonly status: FiberStatus.FiberStatus + } + + /** + * A record containing information about a `Fiber`. + * + * @since 2.0.0 + * @category models + */ + export interface Descriptor { + /** + * The fiber's unique identifier. + */ + readonly id: FiberId.FiberId + /** + * The status of the fiber. + */ + readonly status: FiberStatus.FiberStatus + /** + * The set of fibers attempting to interrupt the fiber or its ancestors. + */ + readonly interruptors: HashSet.HashSet + } +} + +/** + * @since 2.0.0 + * @category instances + */ +export const Order: order.Order> = internal.Order + +/** + * Returns `true` if the specified value is a `Fiber`, `false` otherwise. + * + * @since 2.0.0 + * @category refinements + */ +export const isFiber: (u: unknown) => u is Fiber = internal.isFiber + +/** + * Returns `true` if the specified `Fiber` is a `RuntimeFiber`, `false` + * otherwise. + * + * @since 2.0.0 + * @category refinements + */ +export const isRuntimeFiber: (self: Fiber) => self is RuntimeFiber = internal.isRuntimeFiber + +/** + * The identity of the fiber. + * + * @since 2.0.0 + * @category getters + */ +export const id: (self: Fiber) => FiberId.FiberId = internal.id + +const _await: (self: Fiber) => Effect.Effect> = internal._await + +export { + /** + * Awaits the fiber, which suspends the awaiting fiber until the result of the + * fiber has been determined. + * + * @since 2.0.0 + * @category getters + */ + _await as await +} + +/** + * Awaits on all fibers to be completed, successfully or not. + * + * @since 2.0.0 + * @category destructors + */ +export const awaitAll: >>( + fibers: T +) => Effect.Effect< + [T] extends [ReadonlyArray] + ? number extends T["length"] ? Array ? Exit.Exit : never> + : { -readonly [K in keyof T]: T[K] extends Fiber ? Exit.Exit : never } + : Array ? U extends Fiber ? Exit.Exit : never : never> +> = fiberRuntime.fiberAwaitAll + +/** + * Retrieves the immediate children of the fiber. + * + * @since 2.0.0 + * @category getters + */ +export const children: (self: Fiber) => Effect.Effect>> = internal.children + +/** + * Collects all fibers into a single fiber producing an in-order list of the + * results. + * + * @since 2.0.0 + * @category constructors + */ +export const all: (fibers: Iterable>) => Fiber, E> = fiberRuntime.fiberAll + +/** + * A fiber that is done with the specified `Exit` value. + * + * @since 2.0.0 + * @category constructors + */ +export const done: (exit: Exit.Exit) => Fiber = internal.done + +/** + * @since 2.0.0 + * @category destructors + */ +export const dump: (self: RuntimeFiber) => Effect.Effect = internal.dump + +/** + * @since 2.0.0 + * @category destructors + */ +export const dumpAll: ( + fibers: Iterable> +) => Effect.Effect> = internal.dumpAll + +/** + * A fiber that has already failed with the specified value. + * + * @since 2.0.0 + * @category constructors + */ +export const fail: (error: E) => Fiber = internal.fail + +/** + * Creates a `Fiber` that has already failed with the specified cause. + * + * @since 2.0.0 + * @category constructors + */ +export const failCause: (cause: Cause.Cause) => Fiber = internal.failCause + +/** + * Lifts an `Effect` into a `Fiber`. + * + * @since 2.0.0 + * @category conversions + */ +export const fromEffect: (effect: Effect.Effect) => Effect.Effect> = internal.fromEffect + +/** + * Gets the current fiber if one is running. + * + * @since 2.0.0 + * @category utilities + */ +export const getCurrentFiber: () => Option.Option> = internal.getCurrentFiber + +/** + * Inherits values from all `FiberRef` instances into current fiber. This + * will resume immediately. + * + * @since 2.0.0 + * @category destructors + */ +export const inheritAll: (self: Fiber) => Effect.Effect = internal.inheritAll + +/** + * Interrupts the fiber from whichever fiber is calling this method. If the + * fiber has already exited, the returned effect will resume immediately. + * Otherwise, the effect will resume when the fiber exits. + * + * @since 2.0.0 + * @category interruption + */ +export const interrupt: (self: Fiber) => Effect.Effect> = core.interruptFiber + +/** + * Constructrs a `Fiber` that is already interrupted. + * + * @since 2.0.0 + * @category constructors + */ +export const interrupted: (fiberId: FiberId.FiberId) => Fiber = internal.interrupted + +/** + * Interrupts the fiber as if interrupted from the specified fiber. If the + * fiber has already exited, the returned effect will resume immediately. + * Otherwise, the effect will resume when the fiber exits. + * + * @since 2.0.0 + * @category interruption + */ +export const interruptAs: { + (fiberId: FiberId.FiberId): (self: Fiber) => Effect.Effect> + (self: Fiber, fiberId: FiberId.FiberId): Effect.Effect> +} = core.interruptAsFiber + +/** + * Interrupts the fiber as if interrupted from the specified fiber. If the + * fiber has already exited, the returned effect will resume immediately. + * Otherwise, the effect will resume when the fiber exits. + * + * @since 2.0.0 + * @category interruption + */ +export const interruptAsFork: { + (fiberId: FiberId.FiberId): (self: Fiber) => Effect.Effect + (self: Fiber, fiberId: FiberId.FiberId): Effect.Effect +} = internal.interruptAsFork + +/** + * Interrupts all fibers, awaiting their interruption. + * + * @since 2.0.0 + * @category interruption + */ +export const interruptAll: (fibers: Iterable>) => Effect.Effect = internal.interruptAll + +/** + * Interrupts all fibers as by the specified fiber, awaiting their + * interruption. + * + * @since 2.0.0 + * @category interruption + */ +export const interruptAllAs: { + (fiberId: FiberId.FiberId): (fibers: Iterable>) => Effect.Effect + (fibers: Iterable>, fiberId: FiberId.FiberId): Effect.Effect +} = internal.interruptAllAs + +/** + * Interrupts the fiber from whichever fiber is calling this method. The + * interruption will happen in a separate daemon fiber, and the returned + * effect will always resume immediately without waiting. + * + * @since 2.0.0 + * @category interruption + */ +export const interruptFork: (self: Fiber) => Effect.Effect = fiberRuntime.fiberInterruptFork + +/** + * Joins the fiber, which suspends the joining fiber until the result of the + * fiber has been determined. Attempting to join a fiber that has erred will + * result in a catchable error. Joining an interrupted fiber will result in an + * "inner interruption" of this fiber, unlike interruption triggered by + * another fiber, "inner interruption" can be caught and recovered. + * + * @since 2.0.0 + * @category destructors + */ +export const join: (self: Fiber) => Effect.Effect = internal.join + +/** + * Joins all fibers, awaiting their _successful_ completion. Attempting to + * join a fiber that has erred will result in a catchable error, _if_ that + * error does not result from interruption. + * + * @since 2.0.0 + * @category destructors + */ +export const joinAll: (fibers: Iterable>) => Effect.Effect, E> = fiberRuntime.fiberJoinAll + +/** + * Maps over the value the Fiber computes. + * + * @since 2.0.0 + * @category mapping + */ +export const map: { + (f: (a: A) => B): (self: Fiber) => Fiber + (self: Fiber, f: (a: A) => B): Fiber +} = internal.map + +/** + * Effectually maps over the value the fiber computes. + * + * @since 2.0.0 + * @category mapping + */ +export const mapEffect: { + (f: (a: A) => Effect.Effect): (self: Fiber) => Fiber + (self: Fiber, f: (a: A) => Effect.Effect): Fiber +} = internal.mapEffect + +/** + * Passes the success of this fiber to the specified callback, and continues + * with the fiber that it returns. + * + * @since 2.0.0 + * @category mapping + */ +export const mapFiber: { + (f: (a: A) => Fiber): (self: Fiber) => Effect.Effect> + (self: Fiber, f: (a: A) => Fiber): Effect.Effect> +} = internal.mapFiber + +/** + * Folds over the `Fiber` or `RuntimeFiber`. + * + * @since 2.0.0 + * @category folding + */ +export const match: { + ( + options: { + readonly onFiber: (fiber: Fiber) => Z + readonly onRuntimeFiber: (fiber: RuntimeFiber) => Z + } + ): (self: Fiber) => Z + ( + self: Fiber, + options: { + readonly onFiber: (fiber: Fiber) => Z + readonly onRuntimeFiber: (fiber: RuntimeFiber) => Z + } + ): Z +} = internal.match + +/** + * A fiber that never fails or succeeds. + * + * @since 2.0.0 + * @category constructors + */ +export const never: Fiber = internal.never + +/** + * Returns a fiber that prefers `this` fiber, but falls back to the `that` one + * when `this` one fails. Interrupting the returned fiber will interrupt both + * fibers, sequentially, from left to right. + * + * @since 2.0.0 + * @category alternatives + */ +export const orElse: { + (that: Fiber): (self: Fiber) => Fiber + (self: Fiber, that: Fiber): Fiber +} = internal.orElse + +/** + * Returns a fiber that prefers `this` fiber, but falls back to the `that` one + * when `this` one fails. Interrupting the returned fiber will interrupt both + * fibers, sequentially, from left to right. + * + * @since 2.0.0 + * @category alternatives + */ +export const orElseEither: { + (that: Fiber): (self: Fiber) => Fiber, E2 | E> + (self: Fiber, that: Fiber): Fiber, E | E2> +} = internal.orElseEither + +/** + * Tentatively observes the fiber, but returns immediately if it is not + * already done. + * + * @since 2.0.0 + * @category getters + */ +export const poll: (self: Fiber) => Effect.Effect>> = internal.poll + +/** + * Pretty-prints a `RuntimeFiber`. + * + * @since 2.0.0 + * @category destructors + */ +export const pretty: (self: RuntimeFiber) => Effect.Effect = internal.pretty + +/** + * Returns a chunk containing all root fibers. + * + * @since 2.0.0 + * @category constructors + */ +export const roots: Effect.Effect>> = internal.roots + +/** + * Returns a chunk containing all root fibers. + * + * @since 2.0.0 + * @category constructors + */ +export const unsafeRoots: (_: void) => Array> = internal.unsafeRoots + +/** + * Converts this fiber into a scoped effect. The fiber is interrupted when the + * scope is closed. + * + * @since 2.0.0 + * @category destructors + */ +export const scoped: (self: Fiber) => Effect.Effect, never, Scope.Scope> = + fiberRuntime.fiberScoped + +/** + * Returns the `FiberStatus` of a `RuntimeFiber`. + * + * @since 2.0.0 + * @category getters + */ +export const status: (self: RuntimeFiber) => Effect.Effect = internal.status + +/** + * Returns a fiber that has already succeeded with the specified value. + * + * @since 2.0.0 + * @category constructors + */ +export const succeed: (value: A) => Fiber = internal.succeed + +const void_: Fiber = internal.void +export { + /** + * A fiber that has already succeeded with unit. + * + * @since 2.0.0 + * @category constructors + */ + void_ as void +} + +/** + * Zips this fiber and the specified fiber together, producing a tuple of + * their output. + * + * @since 2.0.0 + * @category zipping + */ +export const zip: { + (that: Fiber): (self: Fiber) => Fiber<[A, A2], E2 | E> + (self: Fiber, that: Fiber): Fiber<[A, A2], E | E2> +} = circular.zipFiber + +/** + * Same as `zip` but discards the output of that `Fiber`. + * + * @since 2.0.0 + * @category zipping + */ +export const zipLeft: { + (that: Fiber): (self: Fiber) => Fiber + (self: Fiber, that: Fiber): Fiber +} = circular.zipLeftFiber + +/** + * Same as `zip` but discards the output of this `Fiber`. + * + * @since 2.0.0 + * @category zipping + */ +export const zipRight: { + (that: Fiber): (self: Fiber) => Fiber + (self: Fiber, that: Fiber): Fiber +} = circular.zipRightFiber + +/** + * Zips this fiber with the specified fiber, combining their results using the + * specified combiner function. Both joins and interruptions are performed in + * sequential order from left to right. + * + * @since 2.0.0 + * @category zipping + */ +export const zipWith: { + (that: Fiber, f: (a: A, b: B) => C): (self: Fiber) => Fiber + (self: Fiber, that: Fiber, f: (a: A, b: B) => C): Fiber +} = circular.zipWithFiber diff --git a/repos/effect/packages/effect/src/FiberHandle.ts b/repos/effect/packages/effect/src/FiberHandle.ts new file mode 100644 index 0000000..d911267 --- /dev/null +++ b/repos/effect/packages/effect/src/FiberHandle.ts @@ -0,0 +1,540 @@ +/** + * @since 2.0.0 + */ +import type { NoSuchElementException } from "./Cause.js" +import * as Cause from "./Cause.js" +import * as Deferred from "./Deferred.js" +import * as Effect from "./Effect.js" +import * as Exit from "./Exit.js" +import * as Fiber from "./Fiber.js" +import * as FiberId from "./FiberId.js" +import { constFalse, dual } from "./Function.js" +import * as HashSet from "./HashSet.js" +import * as Inspectable from "./Inspectable.js" +import * as Option from "./Option.js" +import { type Pipeable, pipeArguments } from "./Pipeable.js" +import * as Predicate from "./Predicate.js" +import * as Runtime from "./Runtime.js" +import type * as Scope from "./Scope.js" + +/** + * @since 2.0.0 + * @categories type ids + */ +export const TypeId: unique symbol = Symbol.for("effect/FiberHandle") + +/** + * @since 2.0.0 + * @categories type ids + */ +export type TypeId = typeof TypeId + +/** + * @since 2.0.0 + * @categories models + */ +export interface FiberHandle extends Pipeable, Inspectable.Inspectable { + readonly [TypeId]: TypeId + readonly deferred: Deferred.Deferred + /** @internal */ + state: { + readonly _tag: "Open" + fiber: Fiber.RuntimeFiber | undefined + } | { + readonly _tag: "Closed" + } +} + +/** + * @since 2.0.0 + * @categories refinements + */ +export const isFiberHandle = (u: unknown): u is FiberHandle => Predicate.hasProperty(u, TypeId) + +const Proto = { + [TypeId]: TypeId, + toString(this: FiberHandle) { + return Inspectable.format(this.toJSON()) + }, + toJSON(this: FiberHandle) { + return { + _id: "FiberHandle", + state: this.state + } + }, + [Inspectable.NodeInspectSymbol](this: FiberHandle) { + return this.toJSON() + }, + pipe() { + return pipeArguments(this, arguments) + } +} + +const unsafeMake = ( + deferred: Deferred.Deferred +): FiberHandle => { + const self = Object.create(Proto) + self.state = { _tag: "Open", fiber: undefined } + self.deferred = deferred + return self +} + +/** + * A FiberHandle can be used to store a single fiber. + * When the associated Scope is closed, the contained fiber will be interrupted. + * + * You can add a fiber to the handle using `FiberHandle.run`, and the fiber will + * be automatically removed from the FiberHandle when it completes. + * + * @example + * ```ts + * import { Effect, FiberHandle } from "effect" + * + * Effect.gen(function*() { + * const handle = yield* FiberHandle.make() + * + * // run some effects + * yield* FiberHandle.run(handle, Effect.never) + * // this will interrupt the previous fiber + * yield* FiberHandle.run(handle, Effect.never) + * + * yield* Effect.sleep(1000) + * }).pipe( + * Effect.scoped // The fiber will be interrupted when the scope is closed + * ) + * ``` + * + * @since 2.0.0 + * @categories constructors + */ +export const make = (): Effect.Effect, never, Scope.Scope> => + Effect.acquireRelease( + Effect.map(Deferred.make(), (deferred) => unsafeMake(deferred)), + (handle) => + Effect.withFiberRuntime((parent) => { + const state = handle.state + if (state._tag === "Closed") return Effect.void + handle.state = { _tag: "Closed" } + return state.fiber ? + Effect.intoDeferred( + Effect.asVoid(Fiber.interruptAs(state.fiber, FiberId.combine(parent.id(), internalFiberId))), + handle.deferred + ) : + Deferred.done(handle.deferred, Exit.void) + }) + ) + +/** + * Create an Effect run function that is backed by a FiberHandle. + * + * @since 2.0.0 + * @categories constructors + */ +export const makeRuntime = (): Effect.Effect< + ( + effect: Effect.Effect, + options?: + | Runtime.RunForkOptions & { + readonly onlyIfMissing?: boolean | undefined + } + | undefined + ) => Fiber.RuntimeFiber, + never, + Scope.Scope | R +> => + Effect.flatMap( + make(), + (self) => runtime(self)() + ) + +/** + * Create an Effect run function that is backed by a FiberHandle. + * + * @since 3.13.0 + * @categories constructors + */ +export const makeRuntimePromise = (): Effect.Effect< + ( + effect: Effect.Effect, + options?: Runtime.RunForkOptions | undefined + ) => Promise, + never, + Scope.Scope | R +> => + Effect.flatMap( + make(), + (self) => runtimePromise(self)() + ) + +const internalFiberIdId = -1 +const internalFiberId = FiberId.make(internalFiberIdId, 0) +const isInternalInterruption = Cause.reduceWithContext(undefined, { + emptyCase: constFalse, + failCase: constFalse, + dieCase: constFalse, + interruptCase: (_, fiberId) => HashSet.has(FiberId.ids(fiberId), internalFiberIdId), + sequentialCase: (_, left, right) => left || right, + parallelCase: (_, left, right) => left || right +}) + +/** + * Set the fiber in a FiberHandle. When the fiber completes, it will be removed from the FiberHandle. + * If a fiber is already running, it will be interrupted unless `options.onlyIfMissing` is set. + * + * @since 2.0.0 + * @categories combinators + */ +export const unsafeSet: { + ( + fiber: Fiber.RuntimeFiber, + options?: { + readonly interruptAs?: FiberId.FiberId | undefined + readonly onlyIfMissing?: boolean | undefined + readonly propagateInterruption?: boolean | undefined + } + ): (self: FiberHandle) => void + ( + self: FiberHandle, + fiber: Fiber.RuntimeFiber, + options?: { + readonly interruptAs?: FiberId.FiberId | undefined + readonly onlyIfMissing?: boolean | undefined + readonly propagateInterruption?: boolean | undefined + } + ): void +} = dual((args) => isFiberHandle(args[0]), ( + self: FiberHandle, + fiber: Fiber.RuntimeFiber, + options?: { + readonly interruptAs?: FiberId.FiberId | undefined + readonly onlyIfMissing?: boolean | undefined + readonly propagateInterruption?: boolean | undefined + } +): void => { + if (self.state._tag === "Closed") { + fiber.unsafeInterruptAsFork(FiberId.combine(options?.interruptAs ?? FiberId.none, internalFiberId)) + return + } else if (self.state.fiber !== undefined) { + if (options?.onlyIfMissing === true) { + fiber.unsafeInterruptAsFork(FiberId.combine(options?.interruptAs ?? FiberId.none, internalFiberId)) + return + } else if (self.state.fiber === fiber) { + return + } + self.state.fiber.unsafeInterruptAsFork(FiberId.combine(options?.interruptAs ?? FiberId.none, internalFiberId)) + self.state.fiber = undefined + } + + self.state.fiber = fiber + fiber.addObserver((exit) => { + if (self.state._tag === "Open" && fiber === self.state.fiber) { + self.state.fiber = undefined + } + if ( + Exit.isFailure(exit) && + ( + options?.propagateInterruption === true ? + !isInternalInterruption(exit.cause) : + !Cause.isInterruptedOnly(exit.cause) + ) + ) { + Deferred.unsafeDone(self.deferred, exit as any) + } + }) +}) + +/** + * Set the fiber in the FiberHandle. When the fiber completes, it will be removed from the FiberHandle. + * If a fiber already exists in the FiberHandle, it will be interrupted unless `options.onlyIfMissing` is set. + * + * @since 2.0.0 + * @categories combinators + */ +export const set: { + ( + fiber: Fiber.RuntimeFiber, + options?: { + readonly onlyIfMissing?: boolean + readonly propagateInterruption?: boolean | undefined + } + ): (self: FiberHandle) => Effect.Effect + ( + self: FiberHandle, + fiber: Fiber.RuntimeFiber, + options?: { + readonly onlyIfMissing?: boolean + readonly propagateInterruption?: boolean | undefined + } + ): Effect.Effect +} = dual((args) => isFiberHandle(args[0]), ( + self: FiberHandle, + fiber: Fiber.RuntimeFiber, + options?: { + readonly onlyIfMissing?: boolean + readonly propagateInterruption?: boolean | undefined + } +): Effect.Effect => + Effect.fiberIdWith( + (fiberId) => + Effect.sync(() => + unsafeSet(self, fiber, { + interruptAs: fiberId, + onlyIfMissing: options?.onlyIfMissing, + propagateInterruption: options?.propagateInterruption + }) + ) + )) + +/** + * Retrieve the fiber from the FiberHandle. + * + * @since 2.0.0 + * @categories combinators + */ +export const unsafeGet = (self: FiberHandle): Option.Option> => + self.state._tag === "Closed" ? Option.none() : Option.fromNullable(self.state.fiber) + +/** + * Retrieve the fiber from the FiberHandle. + * + * @since 2.0.0 + * @categories combinators + */ +export const get = (self: FiberHandle): Effect.Effect, NoSuchElementException> => + Effect.suspend(() => unsafeGet(self)) + +/** + * @since 2.0.0 + * @categories combinators + */ +export const clear = (self: FiberHandle): Effect.Effect => + Effect.uninterruptibleMask((restore) => + Effect.withFiberRuntime((fiber) => { + if (self.state._tag === "Closed" || self.state.fiber === undefined) { + return Effect.void + } + return Effect.zipRight( + restore(Fiber.interruptAs(self.state.fiber, FiberId.combine(fiber.id(), internalFiberId))), + Effect.sync(() => { + if (self.state._tag === "Open") { + self.state.fiber = undefined + } + }) + ) + }) + ) + +const constInterruptedFiber = (function() { + let fiber: Fiber.RuntimeFiber | undefined = undefined + return () => { + if (fiber === undefined) { + fiber = Effect.runFork(Effect.interrupt) + } + return fiber + } +})() + +/** + * Run an Effect and add the forked fiber to the FiberHandle. + * When the fiber completes, it will be removed from the FiberHandle. + * + * @since 2.0.0 + * @categories combinators + */ +export const run: { + ( + self: FiberHandle, + options?: { + readonly onlyIfMissing?: boolean + readonly propagateInterruption?: boolean | undefined + } + ): ( + effect: Effect.Effect + ) => Effect.Effect, never, R> + ( + self: FiberHandle, + effect: Effect.Effect, + options?: { + readonly onlyIfMissing?: boolean + readonly propagateInterruption?: boolean | undefined + } + ): Effect.Effect, never, R> +} = function() { + const self = arguments[0] as FiberHandle + if (Effect.isEffect(arguments[1])) { + return runImpl(self, arguments[1], arguments[2]) as any + } + const options = arguments[1] + return (effect: Effect.Effect) => runImpl(self, effect, options) +} + +const runImpl = ( + self: FiberHandle, + effect: Effect.Effect, + options?: { + readonly onlyIfMissing?: boolean + readonly propagateInterruption?: boolean | undefined + } +): Effect.Effect, never, R> => + Effect.withFiberRuntime((parent) => { + if (self.state._tag === "Closed") { + return Effect.interrupt + } else if (self.state.fiber !== undefined && options?.onlyIfMissing === true) { + return Effect.sync(constInterruptedFiber) + } + const runtime = Runtime.make({ + context: parent.currentContext as any, + fiberRefs: parent.getFiberRefs(), + runtimeFlags: Runtime.defaultRuntime.runtimeFlags + }) + const fiber = Runtime.runFork(runtime)(effect) + unsafeSet(self, fiber, { ...options, interruptAs: parent.id() }) + return Effect.succeed(fiber) + }) + +/** + * Capture a Runtime and use it to fork Effect's, adding the forked fibers to the FiberHandle. + * + * @example + * ```ts + * import { Context, Effect, FiberHandle } from "effect" + * + * interface Users { + * readonly _: unique symbol + * } + * const Users = Context.GenericTag> + * }>("Users") + * + * Effect.gen(function*() { + * const handle = yield* FiberHandle.make() + * const run = yield* FiberHandle.runtime(handle)() + * + * // run an effect and set the fiber in the handle + * run(Effect.andThen(Users, _ => _.getAll)) + * + * // this will interrupt the previous fiber + * run(Effect.andThen(Users, _ => _.getAll)) + * }).pipe( + * Effect.scoped // The fiber will be interrupted when the scope is closed + * ) + * ``` + * + * @since 2.0.0 + * @categories combinators + */ +export const runtime: ( + self: FiberHandle +) => () => Effect.Effect< + ( + effect: Effect.Effect, + options?: + | Runtime.RunForkOptions & { + readonly onlyIfMissing?: boolean | undefined + readonly propagateInterruption?: boolean | undefined + } + | undefined + ) => Fiber.RuntimeFiber, + never, + R +> = (self: FiberHandle) => () => + Effect.map( + Effect.runtime(), + (runtime) => { + const runFork = Runtime.runFork(runtime) + return ( + effect: Effect.Effect, + options?: + | Runtime.RunForkOptions & { + readonly onlyIfMissing?: boolean | undefined + readonly propagateInterruption?: boolean | undefined + } + | undefined + ) => { + if (self.state._tag === "Closed") { + return constInterruptedFiber() + } else if (self.state.fiber !== undefined && options?.onlyIfMissing === true) { + return constInterruptedFiber() + } + const fiber = runFork(effect, options) + unsafeSet(self, fiber, options) + return fiber + } + } + ) + +/** + * Capture a Runtime and use it to fork Effect's, adding the forked fibers to the FiberHandle. + * + * The returned run function will return Promise's that will resolve when the + * fiber completes. + * + * @since 3.13.0 + * @categories combinators + */ +export const runtimePromise = (self: FiberHandle): () => Effect.Effect< + ( + effect: Effect.Effect, + options?: + | Runtime.RunForkOptions & { readonly propagateInterruption?: boolean | undefined } + | undefined + ) => Promise, + never, + R +> => +() => + Effect.map( + runtime(self)(), + (runFork) => + ( + effect: Effect.Effect, + options?: + | Runtime.RunForkOptions & { readonly propagateInterruption?: boolean | undefined } + | undefined + ): Promise => + new Promise((resolve, reject) => + runFork(effect, options).addObserver((exit) => { + if (Exit.isSuccess(exit)) { + resolve(exit.value) + } else { + reject(Cause.squash(exit.cause)) + } + }) + ) + ) + +/** + * If any of the Fiber's in the handle terminate with a failure, + * the returned Effect will terminate with the first failure that occurred. + * + * @since 2.0.0 + * @categories combinators + * @example + * ```ts + * import { Effect, FiberHandle } from "effect"; + * + * Effect.gen(function* (_) { + * const handle = yield* _(FiberHandle.make()); + * yield* _(FiberHandle.set(handle, Effect.runFork(Effect.fail("error")))); + * + * // parent fiber will fail with "error" + * yield* _(FiberHandle.join(handle)); + * }); + * ``` + */ +export const join = (self: FiberHandle): Effect.Effect => + Deferred.await(self.deferred as Deferred.Deferred) + +/** + * Wait for the fiber in the FiberHandle to complete. + * + * @since 3.13.0 + * @categories combinators + */ +export const awaitEmpty = (self: FiberHandle): Effect.Effect => + Effect.suspend(() => { + if (self.state._tag === "Closed" || self.state.fiber === undefined) { + return Effect.void + } + return Fiber.await(self.state.fiber) + }) diff --git a/repos/effect/packages/effect/src/FiberId.ts b/repos/effect/packages/effect/src/FiberId.ts new file mode 100644 index 0000000..91c3e12 --- /dev/null +++ b/repos/effect/packages/effect/src/FiberId.ts @@ -0,0 +1,195 @@ +/** + * @since 2.0.0 + */ +import type * as Equal from "./Equal.js" +import type * as HashSet from "./HashSet.js" +import type { Inspectable } from "./Inspectable.js" +import * as internal from "./internal/fiberId.js" +import type * as Option from "./Option.js" + +/** + * @since 2.0.0 + * @category symbols + */ +export const FiberIdTypeId: unique symbol = internal.FiberIdTypeId + +/** + * @since 2.0.0 + * @category symbols + */ +export type FiberIdTypeId = typeof FiberIdTypeId + +/** + * @since 2.0.0 + * @category models + */ +export type Single = None | Runtime + +/** + * @since 2.0.0 + * @category models + */ +export type FiberId = Single | Composite + +/** + * @since 2.0.0 + * @category models + */ +export interface None extends Equal.Equal, Inspectable { + readonly [FiberIdTypeId]: FiberIdTypeId + readonly _tag: "None" + readonly id: -1 + readonly startTimeMillis: -1 +} + +/** + * @since 2.0.0 + * @category models + */ +export interface Runtime extends Equal.Equal, Inspectable { + readonly [FiberIdTypeId]: FiberIdTypeId + readonly _tag: "Runtime" + readonly id: number + readonly startTimeMillis: number +} + +/** + * @since 2.0.0 + * @category models + */ +export interface Composite extends Equal.Equal, Inspectable { + readonly [FiberIdTypeId]: FiberIdTypeId + readonly _tag: "Composite" + readonly left: FiberId + readonly right: FiberId +} + +/** + * @since 2.0.0 + * @category constructors + */ +export const none: None = internal.none + +/** + * @since 2.0.0 + * @category constructors + */ +export const runtime: (id: number, startTimeMillis: number) => Runtime = internal.runtime + +/** + * @since 2.0.0 + * @category constructors + */ +export const composite: (left: FiberId, right: FiberId) => Composite = internal.composite + +/** + * Returns `true` if the specified unknown value is a `FiberId`, `false` + * otherwise. + * + * @since 2.0.0 + * @category refinements + */ +export const isFiberId: (self: unknown) => self is FiberId = internal.isFiberId + +/** + * Returns `true` if the `FiberId` is a `None`, `false` otherwise. + * + * @since 2.0.0 + * @category refinements + */ +export const isNone: (self: FiberId) => self is None = internal.isNone + +/** + * Returns `true` if the `FiberId` is a `Runtime`, `false` otherwise. + * + * @since 2.0.0 + * @category refinements + */ +export const isRuntime: (self: FiberId) => self is Runtime = internal.isRuntime + +/** + * Returns `true` if the `FiberId` is a `Composite`, `false` otherwise. + * + * @since 2.0.0 + * @category refinements + */ +export const isComposite: (self: FiberId) => self is Composite = internal.isComposite + +/** + * Combine two `FiberId`s. + * + * @since 2.0.0 + * @category constructors + */ +export const combine: { + (that: FiberId): (self: FiberId) => FiberId + (self: FiberId, that: FiberId): FiberId +} = internal.combine + +/** + * Combines a set of `FiberId`s into a single `FiberId`. + * + * @since 2.0.0 + * @category constructors + */ +export const combineAll: (fiberIds: HashSet.HashSet) => FiberId = internal.combineAll + +/** + * Returns this `FiberId` if it is not `None`, otherwise returns that `FiberId`. + * + * @since 2.0.0 + * @category utils + */ +export const getOrElse: { + (that: FiberId): (self: FiberId) => FiberId + (self: FiberId, that: FiberId): FiberId +} = internal.getOrElse + +/** + * Get the set of identifiers for this `FiberId`. + * + * @since 2.0.0 + * @category destructors + */ +export const ids: (self: FiberId) => HashSet.HashSet = internal.ids + +/** + * Creates a new `FiberId`. + * + * @since 2.0.0 + * @category constructors + */ +export const make: (id: number, startTimeSeconds: number) => FiberId = internal.make + +/** + * Creates a string representing the name of the current thread of execution + * represented by the specified `FiberId`. + * + * @since 2.0.0 + * @category destructors + */ +export const threadName: (self: FiberId) => string = internal.threadName + +/** + * Convert a `FiberId` into an `Option`. + * + * @since 2.0.0 + * @category destructors + */ +export const toOption: (self: FiberId) => Option.Option = internal.toOption + +/** + * Convert a `FiberId` into a `HashSet`. + * + * @since 2.0.0 + * @category destructors + */ +export const toSet: (self: FiberId) => HashSet.HashSet = internal.toSet + +/** + * Unsafely creates a new `FiberId`. + * + * @since 2.0.0 + * @category unsafe + */ +export const unsafeMake: (_: void) => Runtime = internal.unsafeMake diff --git a/repos/effect/packages/effect/src/FiberMap.ts b/repos/effect/packages/effect/src/FiberMap.ts new file mode 100644 index 0000000..3e0ecd7 --- /dev/null +++ b/repos/effect/packages/effect/src/FiberMap.ts @@ -0,0 +1,656 @@ +/** + * @since 2.0.0 + */ +import type { NoSuchElementException } from "./Cause.js" +import * as Cause from "./Cause.js" +import * as Deferred from "./Deferred.js" +import * as Effect from "./Effect.js" +import * as Exit from "./Exit.js" +import * as Fiber from "./Fiber.js" +import * as FiberId from "./FiberId.js" +import { constFalse, constVoid, dual } from "./Function.js" +import * as HashSet from "./HashSet.js" +import * as Inspectable from "./Inspectable.js" +import * as Iterable from "./Iterable.js" +import * as MutableHashMap from "./MutableHashMap.js" +import * as Option from "./Option.js" +import { type Pipeable, pipeArguments } from "./Pipeable.js" +import * as Predicate from "./Predicate.js" +import * as Runtime from "./Runtime.js" +import type * as Scope from "./Scope.js" + +/** + * @since 2.0.0 + * @categories type ids + */ +export const TypeId: unique symbol = Symbol.for("effect/FiberMap") + +/** + * @since 2.0.0 + * @categories type ids + */ +export type TypeId = typeof TypeId + +/** + * @since 2.0.0 + * @categories models + */ +export interface FiberMap + extends Pipeable, Inspectable.Inspectable, Iterable<[K, Fiber.RuntimeFiber]> +{ + readonly [TypeId]: TypeId + readonly deferred: Deferred.Deferred + /** @internal */ + state: { + readonly _tag: "Open" + readonly backing: MutableHashMap.MutableHashMap> + } | { + readonly _tag: "Closed" + } +} + +/** + * @since 2.0.0 + * @categories refinements + */ +export const isFiberMap = (u: unknown): u is FiberMap => Predicate.hasProperty(u, TypeId) + +const Proto = { + [TypeId]: TypeId, + [Symbol.iterator](this: FiberMap) { + if (this.state._tag === "Closed") { + return Iterable.empty() + } + return this.state.backing[Symbol.iterator]() + }, + toString(this: FiberMap) { + return Inspectable.format(this.toJSON()) + }, + toJSON(this: FiberMap) { + return { + _id: "FiberMap", + state: this.state + } + }, + [Inspectable.NodeInspectSymbol](this: FiberMap) { + return this.toJSON() + }, + pipe() { + return pipeArguments(this, arguments) + } +} + +const unsafeMake = ( + backing: MutableHashMap.MutableHashMap>, + deferred: Deferred.Deferred +): FiberMap => { + const self = Object.create(Proto) + self.state = { _tag: "Open", backing } + self.deferred = deferred + return self +} + +/** + * A FiberMap can be used to store a collection of fibers, indexed by some key. + * When the associated Scope is closed, all fibers in the map will be interrupted. + * + * You can add fibers to the map using `FiberMap.set` or `FiberMap.run`, and the fibers will + * be automatically removed from the FiberMap when they complete. + * + * @example + * ```ts + * import { Effect, FiberMap } from "effect" + * + * Effect.gen(function*() { + * const map = yield* FiberMap.make() + * + * // run some effects and add the fibers to the map + * yield* FiberMap.run(map, "fiber a", Effect.never) + * yield* FiberMap.run(map, "fiber b", Effect.never) + * + * yield* Effect.sleep(1000) + * }).pipe( + * Effect.scoped // The fibers will be interrupted when the scope is closed + * ) + * ``` + * + * @since 2.0.0 + * @categories constructors + */ +export const make = (): Effect.Effect, never, Scope.Scope> => + Effect.acquireRelease( + Effect.map(Deferred.make(), (deferred) => + unsafeMake( + MutableHashMap.empty(), + deferred + )), + (map) => + Effect.withFiberRuntime((parent) => { + const state = map.state + if (state._tag === "Closed") return Effect.void + map.state = { _tag: "Closed" } + return Fiber.interruptAllAs( + Iterable.map(state.backing, ([, fiber]) => fiber), + FiberId.combine(parent.id(), internalFiberId) + ).pipe( + Effect.intoDeferred(map.deferred) + ) + }) + ) + +/** + * Create an Effect run function that is backed by a FiberMap. + * + * @since 2.0.0 + * @categories constructors + */ +export const makeRuntime = (): Effect.Effect< + ( + key: K, + effect: Effect.Effect, + options?: + | Runtime.RunForkOptions & { + readonly onlyIfMissing?: boolean | undefined + } + | undefined + ) => Fiber.RuntimeFiber, + never, + Scope.Scope | R +> => + Effect.flatMap( + make(), + (self) => runtime(self)() + ) + +/** + * Create an Effect run function that is backed by a FiberMap. + * + * @since 3.13.0 + * @categories constructors + */ +export const makeRuntimePromise = (): Effect.Effect< + ( + key: K, + effect: Effect.Effect, + options?: + | Runtime.RunForkOptions & { + readonly onlyIfMissing?: boolean | undefined + } + | undefined + ) => Promise, + never, + Scope.Scope | R +> => + Effect.flatMap( + make(), + (self) => runtimePromise(self)() + ) + +const internalFiberIdId = -1 +const internalFiberId = FiberId.make(internalFiberIdId, 0) +const isInternalInterruption = Cause.reduceWithContext(undefined, { + emptyCase: constFalse, + failCase: constFalse, + dieCase: constFalse, + interruptCase: (_, fiberId) => HashSet.has(FiberId.ids(fiberId), internalFiberIdId), + sequentialCase: (_, left, right) => left || right, + parallelCase: (_, left, right) => left || right +}) + +/** + * Add a fiber to the FiberMap. When the fiber completes, it will be removed from the FiberMap. + * If the key already exists in the FiberMap, the previous fiber will be interrupted. + * + * @since 2.0.0 + * @categories combinators + */ +export const unsafeSet: { + ( + key: K, + fiber: Fiber.RuntimeFiber, + options?: { + readonly interruptAs?: FiberId.FiberId | undefined + readonly onlyIfMissing?: boolean | undefined + readonly propagateInterruption?: boolean | undefined + } | undefined + ): (self: FiberMap) => void + ( + self: FiberMap, + key: K, + fiber: Fiber.RuntimeFiber, + options?: { + readonly interruptAs?: FiberId.FiberId | undefined + readonly onlyIfMissing?: boolean | undefined + readonly propagateInterruption?: boolean | undefined + } | undefined + ): void +} = dual((args) => isFiberMap(args[0]), ( + self: FiberMap, + key: K, + fiber: Fiber.RuntimeFiber, + options?: { + readonly interruptAs?: FiberId.FiberId | undefined + readonly onlyIfMissing?: boolean | undefined + readonly propagateInterruption?: boolean | undefined + } | undefined +): void => { + if (self.state._tag === "Closed") { + fiber.unsafeInterruptAsFork(FiberId.combine(options?.interruptAs ?? FiberId.none, internalFiberId)) + return + } + + const previous = MutableHashMap.get(self.state.backing, key) + if (previous._tag === "Some") { + if (options?.onlyIfMissing === true) { + fiber.unsafeInterruptAsFork(FiberId.combine(options?.interruptAs ?? FiberId.none, internalFiberId)) + return + } else if (previous.value === fiber) { + return + } + previous.value.unsafeInterruptAsFork(FiberId.combine(options?.interruptAs ?? FiberId.none, internalFiberId)) + } + + MutableHashMap.set(self.state.backing, key, fiber) + fiber.addObserver((exit) => { + if (self.state._tag === "Closed") { + return + } + const current = MutableHashMap.get(self.state.backing, key) + if (Option.isSome(current) && fiber === current.value) { + MutableHashMap.remove(self.state.backing, key) + } + if ( + Exit.isFailure(exit) && + ( + options?.propagateInterruption === true ? + !isInternalInterruption(exit.cause) : + !Cause.isInterruptedOnly(exit.cause) + ) + ) { + Deferred.unsafeDone(self.deferred, exit as any) + } + }) +}) + +/** + * Add a fiber to the FiberMap. When the fiber completes, it will be removed from the FiberMap. + * If the key already exists in the FiberMap, the previous fiber will be interrupted. + * + * @since 2.0.0 + * @categories combinators + */ +export const set: { + ( + key: K, + fiber: Fiber.RuntimeFiber, + options?: { + readonly onlyIfMissing?: boolean | undefined + readonly propagateInterruption?: boolean | undefined + } | undefined + ): (self: FiberMap) => Effect.Effect + ( + self: FiberMap, + key: K, + fiber: Fiber.RuntimeFiber, + options?: { + readonly onlyIfMissing?: boolean | undefined + readonly propagateInterruption?: boolean | undefined + } | undefined + ): Effect.Effect +} = dual((args) => isFiberMap(args[0]), ( + self: FiberMap, + key: K, + fiber: Fiber.RuntimeFiber, + options?: { + readonly onlyIfMissing?: boolean | undefined + readonly propagateInterruption?: boolean | undefined + } | undefined +): Effect.Effect => + Effect.fiberIdWith( + (fiberId) => + Effect.sync(() => + unsafeSet(self, key, fiber, { + ...options, + interruptAs: fiberId + }) + ) + )) + +/** + * Retrieve a fiber from the FiberMap. + * + * @since 2.0.0 + * @categories combinators + */ +export const unsafeGet: { + (key: K): (self: FiberMap) => Option.Option> + (self: FiberMap, key: K): Option.Option> +} = dual< + ( + key: K + ) => (self: FiberMap) => Option.Option>, + ( + self: FiberMap, + key: K + ) => Option.Option> +>(2, (self, key) => self.state._tag === "Closed" ? Option.none() : MutableHashMap.get(self.state.backing, key)) + +/** + * Retrieve a fiber from the FiberMap. + * + * @since 2.0.0 + * @categories combinators + */ +export const get: { + (key: K): (self: FiberMap) => Effect.Effect, NoSuchElementException> + (self: FiberMap, key: K): Effect.Effect, NoSuchElementException> +} = dual< + ( + key: K + ) => (self: FiberMap) => Effect.Effect, NoSuchElementException>, + ( + self: FiberMap, + key: K + ) => Effect.Effect, NoSuchElementException> +>(2, (self, key) => Effect.suspend(() => unsafeGet(self, key))) + +/** + * Check if a key exists in the FiberMap. + * + * @since 2.0.0 + * @categories combinators + */ +export const unsafeHas: { + (key: K): (self: FiberMap) => boolean + (self: FiberMap, key: K): boolean +} = dual( + 2, + (self: FiberMap, key: K): boolean => + self.state._tag === "Closed" ? false : MutableHashMap.has(self.state.backing, key) +) + +/** + * Check if a key exists in the FiberMap. + * + * @since 2.0.0 + * @categories combinators + */ +export const has: { + (key: K): (self: FiberMap) => Effect.Effect + (self: FiberMap, key: K): Effect.Effect +} = dual( + 2, + (self: FiberMap, key: K): Effect.Effect => Effect.sync(() => unsafeHas(self, key)) +) + +/** + * Remove a fiber from the FiberMap, interrupting it if it exists. + * + * @since 2.0.0 + * @categories combinators + */ +export const remove: { + (key: K): (self: FiberMap) => Effect.Effect + (self: FiberMap, key: K): Effect.Effect +} = dual< + ( + key: K + ) => (self: FiberMap) => Effect.Effect, + ( + self: FiberMap, + key: K + ) => Effect.Effect +>(2, (self, key) => + Effect.withFiberRuntime((removeFiber) => { + if (self.state._tag === "Closed") { + return Effect.void + } + const fiber = MutableHashMap.get(self.state.backing, key) + if (fiber._tag === "None") { + return Effect.void + } + // will be removed by the observer + return Fiber.interruptAs(fiber.value, FiberId.combine(removeFiber.id(), internalFiberId)) + })) + +/** + * @since 2.0.0 + * @categories combinators + */ +export const clear = (self: FiberMap): Effect.Effect => + Effect.withFiberRuntime((clearFiber) => { + if (self.state._tag === "Closed") { + return Effect.void + } + + return Effect.forEach(self.state.backing, ([, fiber]) => + // will be removed by the observer + Fiber.interruptAs(fiber, FiberId.combine(clearFiber.id(), internalFiberId))) + }) + +const constInterruptedFiber = (function() { + let fiber: Fiber.RuntimeFiber | undefined = undefined + return () => { + if (fiber === undefined) { + fiber = Effect.runFork(Effect.interrupt) + } + return fiber + } +})() + +/** + * Run an Effect and add the forked fiber to the FiberMap. + * When the fiber completes, it will be removed from the FiberMap. + * + * @since 2.0.0 + * @categories combinators + */ +export const run: { + ( + self: FiberMap, + key: K, + options?: { + readonly onlyIfMissing?: boolean | undefined + readonly propagateInterruption?: boolean | undefined + } | undefined + ): ( + effect: Effect.Effect + ) => Effect.Effect, never, R> + ( + self: FiberMap, + key: K, + effect: Effect.Effect, + options?: { + readonly onlyIfMissing?: boolean | undefined + readonly propagateInterruption?: boolean | undefined + } | undefined + ): Effect.Effect, never, R> +} = function() { + const self = arguments[0] + if (Effect.isEffect(arguments[2])) { + return runImpl(self, arguments[1], arguments[2], arguments[3]) as any + } + const key = arguments[1] + const options = arguments[2] + return (effect: Effect.Effect) => runImpl(self, key, effect, options) +} + +const runImpl = ( + self: FiberMap, + key: K, + effect: Effect.Effect, + options?: { + readonly onlyIfMissing?: boolean + readonly propagateInterruption?: boolean | undefined + } +) => + Effect.withFiberRuntime((parent) => { + if (self.state._tag === "Closed") { + return Effect.interrupt + } else if (options?.onlyIfMissing === true && unsafeHas(self, key)) { + return Effect.sync(constInterruptedFiber) + } + const runtime = Runtime.make({ + context: parent.currentContext as any, + fiberRefs: parent.getFiberRefs(), + runtimeFlags: Runtime.defaultRuntime.runtimeFlags + }) + const fiber = Runtime.runFork(runtime)(effect) + unsafeSet(self, key, fiber, { ...options, interruptAs: parent.id() }) + return Effect.succeed(fiber) + }) + +/** + * Capture a Runtime and use it to fork Effect's, adding the forked fibers to the FiberMap. + * + * @example + * ```ts + * import { Context, Effect, FiberMap } from "effect" + * + * interface Users { + * readonly _: unique symbol + * } + * const Users = Context.GenericTag> + * }>("Users") + * + * Effect.gen(function*() { + * const map = yield* FiberMap.make() + * const run = yield* FiberMap.runtime(map)() + * + * // run some effects and add the fibers to the map + * run("effect-a", Effect.andThen(Users, _ => _.getAll)) + * run("effect-b", Effect.andThen(Users, _ => _.getAll)) + * }).pipe( + * Effect.scoped // The fibers will be interrupted when the scope is closed + * ) + * ``` + * + * @since 2.0.0 + * @categories combinators + */ +export const runtime: ( + self: FiberMap +) => () => Effect.Effect< + ( + key: K, + effect: Effect.Effect, + options?: + | Runtime.RunForkOptions & { + readonly onlyIfMissing?: boolean | undefined + readonly propagateInterruption?: boolean | undefined + } + | undefined + ) => Fiber.RuntimeFiber, + never, + R +> = (self: FiberMap) => () => + Effect.map( + Effect.runtime(), + (runtime) => { + const runFork = Runtime.runFork(runtime) + return ( + key: K, + effect: Effect.Effect, + options?: + | Runtime.RunForkOptions & { + readonly onlyIfMissing?: boolean | undefined + readonly propagateInterruption?: boolean | undefined + } + | undefined + ) => { + if (self.state._tag === "Closed") { + return constInterruptedFiber() + } else if (options?.onlyIfMissing === true && unsafeHas(self, key)) { + return constInterruptedFiber() + } + const fiber = runFork(effect, options) + unsafeSet(self, key, fiber, options) + return fiber + } + } + ) + +/** + * Capture a Runtime and use it to fork Effect's, adding the forked fibers to the FiberMap. + * + * @since 3.13.0 + * @categories combinators + */ +export const runtimePromise = (self: FiberMap): () => Effect.Effect< + ( + key: K, + effect: Effect.Effect, + options?: + | Runtime.RunForkOptions & { + readonly onlyIfMissing?: boolean | undefined + readonly propagateInterruption?: boolean | undefined + } + | undefined + ) => Promise, + never, + R +> => +() => + Effect.map( + runtime(self)(), + (runFork) => + ( + key: K, + effect: Effect.Effect, + options?: + | Runtime.RunForkOptions & { readonly propagateInterruption?: boolean | undefined } + | undefined + ): Promise => + new Promise((resolve, reject) => + runFork(key, effect, options).addObserver((exit) => { + if (Exit.isSuccess(exit)) { + resolve(exit.value) + } else { + reject(Cause.squash(exit.cause)) + } + }) + ) + ) + +/** + * @since 2.0.0 + * @categories combinators + */ +export const size = (self: FiberMap): Effect.Effect => + Effect.sync(() => self.state._tag === "Closed" ? 0 : MutableHashMap.size(self.state.backing)) + +/** + * Join all fibers in the FiberMap. If any of the Fiber's in the map terminate with a failure, + * the returned Effect will terminate with the first failure that occurred. + * + * @since 2.0.0 + * @categories combinators + * @example + * ```ts + * import { Effect, FiberMap } from "effect"; + * + * Effect.gen(function* (_) { + * const map = yield* _(FiberMap.make()); + * yield* _(FiberMap.set(map, "a", Effect.runFork(Effect.fail("error")))); + * + * // parent fiber will fail with "error" + * yield* _(FiberMap.join(map)); + * }); + * ``` + */ +export const join = (self: FiberMap): Effect.Effect => + Deferred.await(self.deferred as Deferred.Deferred) + +/** + * Wait for the FiberMap to be empty. + * + * @since 3.13.0 + * @categories combinators + */ +export const awaitEmpty = (self: FiberMap): Effect.Effect => + Effect.whileLoop({ + while: () => self.state._tag === "Open" && MutableHashMap.size(self.state.backing) > 0, + body: () => Fiber.await(Iterable.unsafeHead(self)[1]), + step: constVoid + }) diff --git a/repos/effect/packages/effect/src/FiberRef.ts b/repos/effect/packages/effect/src/FiberRef.ts new file mode 100644 index 0000000..37f7402 --- /dev/null +++ b/repos/effect/packages/effect/src/FiberRef.ts @@ -0,0 +1,431 @@ +/** + * @since 2.0.0 + */ +import type * as Cause from "./Cause.js" +import type * as Chunk from "./Chunk.js" +import type * as Context from "./Context.js" +import type * as Differ from "./Differ.js" +import type * as Effect from "./Effect.js" +import type { LazyArg } from "./Function.js" +import type * as HashMap from "./HashMap.js" +import type * as HashSet from "./HashSet.js" +import * as core from "./internal/core.js" +import * as fiberRuntime from "./internal/fiberRuntime.js" +import * as query from "./internal/query.js" +import type * as List from "./List.js" +import type * as Logger from "./Logger.js" +import type * as LogLevel from "./LogLevel.js" +import type * as LogSpan from "./LogSpan.js" +import type * as MetricLabel from "./MetricLabel.js" +import type * as Option from "./Option.js" +import type * as Request from "./Request.js" +import type * as RuntimeFlags from "./RuntimeFlags.js" +import * as Scheduler from "./Scheduler.js" +import type * as Scope from "./Scope.js" +import type * as Supervisor from "./Supervisor.js" +import type * as Tracer from "./Tracer.js" +import type * as Types from "./Types.js" +import type * as Unify from "./Unify.js" + +/** + * @since 2.0.0 + * @category symbols + */ +export const FiberRefTypeId: unique symbol = core.FiberRefTypeId + +/** + * @since 2.0.0 + * @category symbols + */ +export type FiberRefTypeId = typeof FiberRefTypeId + +/** + * @since 2.0.0 + * @category model + */ +export interface FiberRef extends Effect.Effect, Variance { + /** @internal */ + readonly initial: A + /** @internal */ + diff(oldValue: A, newValue: A): unknown + /** @internal */ + combine(first: unknown, second: unknown): unknown + /** @internal */ + patch(patch: unknown): (oldValue: A) => A + /** @internal */ + readonly fork: unknown + /** @internal */ + join(oldValue: A, newValue: A): A + readonly [Unify.typeSymbol]?: unknown + readonly [Unify.unifySymbol]?: FiberRefUnify + readonly [Unify.ignoreSymbol]?: FiberRefUnifyIgnore +} + +/** + * @category models + * @since 3.8.0 + */ +export interface FiberRefUnify extends Effect.EffectUnify { + FiberRef?: () => Extract> +} + +/** + * @category models + * @since 3.8.0 + */ +export interface FiberRefUnifyIgnore extends Effect.EffectUnifyIgnore { + Effect?: true +} + +/** + * @since 2.0.0 + * @category models + */ +export interface Variance { + readonly [FiberRefTypeId]: { + readonly _A: Types.Invariant + } +} + +/** + * @since 2.0.0 + * @category constructors + */ +export const make: ( + initial: A, + options?: { + readonly fork?: ((a: A) => A) | undefined + readonly join?: ((left: A, right: A) => A) | undefined + } +) => Effect.Effect, never, Scope.Scope> = fiberRuntime.fiberRefMake + +/** + * @since 2.0.0 + * @category constructors + */ +export const makeWith: (ref: LazyArg>) => Effect.Effect, never, Scope.Scope> = + fiberRuntime.fiberRefMakeWith + +/** + * @since 2.0.0 + * @category constructors + */ +export const makeContext: ( + initial: Context.Context +) => Effect.Effect>, never, Scope.Scope> = fiberRuntime.fiberRefMakeContext + +/** + * @since 2.0.0 + * @category constructors + */ +export const makeRuntimeFlags: ( + initial: RuntimeFlags.RuntimeFlags +) => Effect.Effect, never, Scope.Scope> = fiberRuntime.fiberRefMakeRuntimeFlags + +/** + * @since 2.0.0 + * @category constructors + */ +export const unsafeMake: ( + initial: Value, + options?: { + readonly fork?: ((a: Value) => Value) | undefined + readonly join?: ((left: Value, right: Value) => Value) | undefined + } +) => FiberRef = core.fiberRefUnsafeMake + +/** + * @since 2.0.0 + * @category constructors + */ +export const unsafeMakeHashSet: (initial: HashSet.HashSet) => FiberRef> = + core.fiberRefUnsafeMakeHashSet + +/** + * @since 2.0.0 + * @category constructors + */ +export const unsafeMakeContext: (initial: Context.Context) => FiberRef> = + core.fiberRefUnsafeMakeContext + +/** + * @since 2.0.0 + * @category constructors + */ +export const unsafeMakeSupervisor: (initial: Supervisor.Supervisor) => FiberRef> = + fiberRuntime.fiberRefUnsafeMakeSupervisor + +/** + * @since 2.0.0 + * @category constructors + */ +export const unsafeMakePatch: ( + initial: Value, + options: { + readonly differ: Differ.Differ + readonly fork: Patch + readonly join?: ((oldV: Value, newV: Value) => Value) | undefined + } +) => FiberRef = core.fiberRefUnsafeMakePatch + +/** + * @since 2.0.0 + * @category getters + */ +export const get: (self: FiberRef) => Effect.Effect = core.fiberRefGet + +/** + * @since 2.0.0 + * @category utils + */ +export const getAndSet: { + (value: A): (self: FiberRef) => Effect.Effect + (self: FiberRef, value: A): Effect.Effect +} = core.fiberRefGetAndSet + +/** + * @since 2.0.0 + * @category utils + */ +export const getAndUpdate: { + (f: (a: A) => A): (self: FiberRef) => Effect.Effect + (self: FiberRef, f: (a: A) => A): Effect.Effect +} = core.fiberRefGetAndUpdate + +/** + * @since 2.0.0 + * @category utils + */ +export const getAndUpdateSome: { + (pf: (a: A) => Option.Option): (self: FiberRef) => Effect.Effect + (self: FiberRef, pf: (a: A) => Option.Option): Effect.Effect +} = core.fiberRefGetAndUpdateSome + +/** + * @since 2.0.0 + * @category utils + */ +export const getWith: { + (f: (a: A) => Effect.Effect): (self: FiberRef) => Effect.Effect + (self: FiberRef, f: (a: A) => Effect.Effect): Effect.Effect +} = core.fiberRefGetWith + +/** + * @since 2.0.0 + * @category utils + */ +export const set: { + (value: A): (self: FiberRef) => Effect.Effect + (self: FiberRef, value: A): Effect.Effect +} = core.fiberRefSet + +const _delete: (self: FiberRef) => Effect.Effect = core.fiberRefDelete + +export { + /** + * @since 2.0.0 + * @category utils + */ + _delete as delete +} + +/** + * @since 2.0.0 + * @category utils + */ +export const reset: (self: FiberRef) => Effect.Effect = core.fiberRefReset + +/** + * @since 2.0.0 + * @category utils + */ +export const modify: { + (f: (a: A) => readonly [B, A]): (self: FiberRef) => Effect.Effect + (self: FiberRef, f: (a: A) => readonly [B, A]): Effect.Effect +} = core.fiberRefModify + +/** + * @since 2.0.0 + * @category utils + */ +export const modifySome: ( + self: FiberRef, + def: B, + f: (a: A) => Option.Option +) => Effect.Effect = core.fiberRefModifySome + +/** + * @since 2.0.0 + * @category utils + */ +export const update: { + (f: (a: A) => A): (self: FiberRef) => Effect.Effect + (self: FiberRef, f: (a: A) => A): Effect.Effect +} = core.fiberRefUpdate + +/** + * @since 2.0.0 + * @category utils + */ +export const updateSome: { + (pf: (a: A) => Option.Option): (self: FiberRef) => Effect.Effect + (self: FiberRef, pf: (a: A) => Option.Option): Effect.Effect +} = core.fiberRefUpdateSome + +/** + * @since 2.0.0 + * @category utils + */ +export const updateAndGet: { + (f: (a: A) => A): (self: FiberRef) => Effect.Effect + (self: FiberRef, f: (a: A) => A): Effect.Effect +} = core.fiberRefUpdateAndGet + +/** + * @since 2.0.0 + * @category utils + */ +export const updateSomeAndGet: { + (pf: (a: A) => Option.Option): (self: FiberRef) => Effect.Effect + (self: FiberRef, pf: (a: A) => Option.Option): Effect.Effect +} = core.fiberRefUpdateSomeAndGet + +/** + * @since 2.0.0 + * @category fiberRefs + */ +export const currentConcurrency: FiberRef = core.currentConcurrency + +/** + * @since 2.0.0 + * @category fiberRefs + */ +export const currentRequestBatchingEnabled: FiberRef = core.currentRequestBatching + +/** + * @since 2.0.0 + * @category fiberRefs + */ +export const currentRequestCache: FiberRef = query.currentCache as any + +/** + * @since 2.0.0 + * @category fiberRefs + */ +export const currentRequestCacheEnabled: FiberRef = query.currentCacheEnabled + +/** + * @since 2.0.0 + * @category fiberRefs + */ +export const currentContext: FiberRef> = core.currentContext + +/** + * @since 2.0.0 + * @category fiberRefs + */ +export const currentSchedulingPriority: FiberRef = core.currentSchedulingPriority + +/** + * @since 2.0.0 + * @category fiberRefs + */ +export const currentMaxOpsBeforeYield: FiberRef = core.currentMaxOpsBeforeYield + +/** + * @since 2.0.0 + * @category fiberRefs + */ +export const unhandledErrorLogLevel: FiberRef> = core.currentUnhandledErrorLogLevel + +/** + * @since 3.17.0 + * @category fiberRefs + */ +export const versionMismatchErrorLogLevel: FiberRef> = + core.currentVersionMismatchErrorLogLevel + +/** + * @since 2.0.0 + * @category fiberRefs + */ +export const currentLogAnnotations: FiberRef> = core.currentLogAnnotations + +/** + * @since 2.0.0 + * @category fiberRefs + */ +export const currentLoggers: FiberRef>> = fiberRuntime.currentLoggers + +/** + * @since 2.0.0 + * @category fiberRefs + */ +export const currentLogLevel: FiberRef = core.currentLogLevel + +/** + * @since 2.0.0 + * @category fiberRefs + */ +export const currentMinimumLogLevel: FiberRef = fiberRuntime.currentMinimumLogLevel + +/** + * @since 2.0.0 + * @category fiberRefs + */ +export const currentLogSpan: FiberRef> = core.currentLogSpan + +/** + * @since 2.0.0 + * @category fiberRefs + */ +export const currentRuntimeFlags: FiberRef = fiberRuntime.currentRuntimeFlags + +/** + * @since 2.0.0 + * @category fiberRefs + */ +export const currentScheduler: FiberRef = Scheduler.currentScheduler + +/** + * @since 2.0.0 + * @category fiberRefs + */ +export const currentSupervisor: FiberRef> = fiberRuntime.currentSupervisor + +/** + * @since 2.0.0 + * @category fiberRefs + */ +export const currentMetricLabels: FiberRef> = core.currentMetricLabels + +/** + * @since 2.0.0 + * @category fiberRefs + */ +export const currentTracerEnabled: FiberRef = core.currentTracerEnabled + +/** + * @since 2.0.0 + * @category fiberRefs + */ +export const currentTracerTimingEnabled: FiberRef = core.currentTracerTimingEnabled + +/** + * @since 2.0.0 + * @category fiberRefs + */ +export const currentTracerSpanAnnotations: FiberRef> = + core.currentTracerSpanAnnotations + +/** + * @since 2.0.0 + * @category fiberRefs + */ +export const currentTracerSpanLinks: FiberRef> = core.currentTracerSpanLinks + +/** + * @since 2.0.0 + * @category fiberRefs + */ +export const interruptedCause: FiberRef> = core.currentInterruptedCause diff --git a/repos/effect/packages/effect/src/FiberRefs.ts b/repos/effect/packages/effect/src/FiberRefs.ts new file mode 100644 index 0000000..f3f0029 --- /dev/null +++ b/repos/effect/packages/effect/src/FiberRefs.ts @@ -0,0 +1,204 @@ +/** + * @since 2.0.0 + */ +import type * as Arr from "./Array.js" +import type * as Effect from "./Effect.js" +import type * as FiberId from "./FiberId.js" +import type * as FiberRef from "./FiberRef.js" +import type * as HashSet from "./HashSet.js" +import * as internal from "./internal/fiberRefs.js" +import type * as Option from "./Option.js" +import type { Pipeable } from "./Pipeable.js" + +/** + * @since 2.0.0 + * @category symbols + */ +export const FiberRefsSym: unique symbol = internal.FiberRefsSym + +/** + * @since 2.0.0 + * @category symbols + */ +export type FiberRefsSym = typeof FiberRefsSym + +/** + * `FiberRefs` is a data type that represents a collection of `FiberRef` values. + * + * This allows safely propagating `FiberRef` values across fiber boundaries, for + * example between an asynchronous producer and consumer. + * + * @since 2.0.0 + * @category models + */ +export interface FiberRefs extends Pipeable { + readonly [FiberRefsSym]: FiberRefsSym + readonly locals: Map, Arr.NonEmptyReadonlyArray> +} + +const delete_: { + (fiberRef: FiberRef.FiberRef): (self: FiberRefs) => FiberRefs + (self: FiberRefs, fiberRef: FiberRef.FiberRef): FiberRefs +} = internal.delete_ + +export { + /** + * Deletes the specified `FiberRef` from the `FibterRefs`. + * + * @since 2.0.0 + * @category utils + */ + delete_ as delete +} + +/** + * Returns a set of each `FiberRef` in this collection. + * + * @since 2.0.0 + * @category getters + */ +export const fiberRefs: (self: FiberRefs) => HashSet.HashSet> = internal.fiberRefs + +/** + * Forks this collection of fiber refs as the specified child fiber id. This + * will potentially modify the value of the fiber refs, as determined by the + * individual fiber refs that make up the collection. + * + * @since 2.0.0 + * @category utils + */ +export const forkAs: { + (childId: FiberId.Single): (self: FiberRefs) => FiberRefs + (self: FiberRefs, childId: FiberId.Single): FiberRefs +} = internal.forkAs + +/** + * Gets the value of the specified `FiberRef` in this collection of `FiberRef` + * values if it exists or `None` otherwise. + * + * @since 2.0.0 + * @category getters + */ +export const get: { + (fiberRef: FiberRef.FiberRef): (self: FiberRefs) => Option.Option + (self: FiberRefs, fiberRef: FiberRef.FiberRef): Option.Option +} = internal.get + +/** + * Gets the value of the specified `FiberRef` in this collection of `FiberRef` + * values if it exists or the `initial` value of the `FiberRef` otherwise. + * + * @since 2.0.0 + * @category getters + */ +export const getOrDefault: { + (fiberRef: FiberRef.FiberRef): (self: FiberRefs) => A + (self: FiberRefs, fiberRef: FiberRef.FiberRef): A +} = internal.getOrDefault + +/** + * Joins this collection of fiber refs to the specified collection, as the + * specified fiber id. This will perform diffing and merging to ensure + * preservation of maximum information from both child and parent refs. + * + * @since 2.0.0 + * @category utils + */ +export const joinAs: { + (fiberId: FiberId.Single, that: FiberRefs): (self: FiberRefs) => FiberRefs + (self: FiberRefs, fiberId: FiberId.Single, that: FiberRefs): FiberRefs +} = internal.joinAs + +/** + * Set each ref to either its value or its default. + * + * @since 2.0.0 + * @category utils + */ +export const setAll: (self: FiberRefs) => Effect.Effect = internal.setAll + +/** + * Updates the value of the specified `FiberRef` using the provided `FiberId` + * + * @since 2.0.0 + * @category utils + */ +export const updateAs: { + ( + options: { + readonly fiberId: FiberId.Single + readonly fiberRef: FiberRef.FiberRef + readonly value: A + } + ): (self: FiberRefs) => FiberRefs + ( + self: FiberRefs, + options: { + readonly fiberId: FiberId.Single + readonly fiberRef: FiberRef.FiberRef + readonly value: A + } + ): FiberRefs +} = internal.updateAs + +/** + * Updates the values of the specified `FiberRef` & value pairs using the provided `FiberId` + * + * @since 2.0.0 + * @category utils + */ +export const updateManyAs: { + ( + options: { + readonly forkAs?: FiberId.Single | undefined + readonly entries: readonly [ + readonly [ + FiberRef.FiberRef, + readonly [readonly [FiberId.Single, any], ...Array] + ], + ...Array< + readonly [ + FiberRef.FiberRef, + readonly [readonly [FiberId.Single, any], ...Array] + ] + > + ] + } + ): (self: FiberRefs) => FiberRefs + ( + self: FiberRefs, + options: { + readonly forkAs?: FiberId.Single | undefined + readonly entries: readonly [ + readonly [ + FiberRef.FiberRef, + readonly [readonly [FiberId.Single, any], ...Array] + ], + ...Array< + readonly [ + FiberRef.FiberRef, + readonly [readonly [FiberId.Single, any], ...Array] + ] + > + ] + } + ): FiberRefs +} = internal.updateManyAs + +/** + * Note: it will not copy the provided Map, make sure to provide a fresh one. + * + * @since 2.0.0 + * @category unsafe + */ +export const unsafeMake: ( + fiberRefLocals: Map, Arr.NonEmptyReadonlyArray> +) => FiberRefs = internal.unsafeMake + +/** + * The empty collection of `FiberRef` values. + * + * @category constructors + * @since 2.0.0 + */ +export const empty: () => FiberRefs = internal.empty diff --git a/repos/effect/packages/effect/src/FiberRefsPatch.ts b/repos/effect/packages/effect/src/FiberRefsPatch.ts new file mode 100644 index 0000000..d57e433 --- /dev/null +++ b/repos/effect/packages/effect/src/FiberRefsPatch.ts @@ -0,0 +1,105 @@ +/** + * @since 2.0.0 + */ +import type * as FiberId from "./FiberId.js" +import type * as FiberRef from "./FiberRef.js" +import type * as FiberRefs from "./FiberRefs.js" +import * as internal from "./internal/fiberRefs/patch.js" + +/** + * A `FiberRefsPatch` captures the changes in `FiberRef` values made by a single + * fiber as a value. This allows fibers to apply the changes made by a workflow + * without inheriting all the `FiberRef` values of the fiber that executed the + * workflow. + * + * @since 2.0.0 + * @category models + */ +export type FiberRefsPatch = Empty | Add | Remove | Update | AndThen + +/** + * @since 2.0.0 + * @category models + */ +export interface Empty { + readonly _tag: "Empty" +} + +/** + * @since 2.0.0 + * @category models + */ +export interface Add { + readonly _tag: "Add" + readonly fiberRef: FiberRef.FiberRef + readonly value: unknown +} + +/** + * @since 2.0.0 + * @category models + */ +export interface Remove { + readonly _tag: "Remove" + readonly fiberRef: FiberRef.FiberRef +} + +/** + * @since 2.0.0 + * @category models + */ +export interface Update { + readonly _tag: "Update" + readonly fiberRef: FiberRef.FiberRef + readonly patch: unknown +} + +/** + * @since 2.0.0 + * @category models + */ +export interface AndThen { + readonly _tag: "AndThen" + readonly first: FiberRefsPatch + readonly second: FiberRefsPatch +} + +/** + * @since 2.0.0 + * @category constructors + */ +export const empty: FiberRefsPatch = internal.empty + +/** + * Constructs a patch that describes the changes between the specified + * collections of `FiberRef` + * + * @since 2.0.0 + * @category constructors + */ +export const diff: (oldValue: FiberRefs.FiberRefs, newValue: FiberRefs.FiberRefs) => FiberRefsPatch = internal.diff + +/** + * Combines this patch and the specified patch to create a new patch that + * describes applying the changes from this patch and the specified patch + * sequentially. + * + * @since 2.0.0 + * @category constructors + */ +export const combine: { + (that: FiberRefsPatch): (self: FiberRefsPatch) => FiberRefsPatch + (self: FiberRefsPatch, that: FiberRefsPatch): FiberRefsPatch +} = internal.combine + +/** + * Applies the changes described by this patch to the specified collection + * of `FiberRef` values. + * + * @since 2.0.0 + * @category destructors + */ +export const patch: { + (fiberId: FiberId.Runtime, oldValue: FiberRefs.FiberRefs): (self: FiberRefsPatch) => FiberRefs.FiberRefs + (self: FiberRefsPatch, fiberId: FiberId.Runtime, oldValue: FiberRefs.FiberRefs): FiberRefs.FiberRefs +} = internal.patch diff --git a/repos/effect/packages/effect/src/FiberSet.ts b/repos/effect/packages/effect/src/FiberSet.ts new file mode 100644 index 0000000..08cb942 --- /dev/null +++ b/repos/effect/packages/effect/src/FiberSet.ts @@ -0,0 +1,491 @@ +/** + * @since 2.0.0 + */ +import * as Cause from "./Cause.js" +import * as Deferred from "./Deferred.js" +import * as Effect from "./Effect.js" +import * as Exit from "./Exit.js" +import * as Fiber from "./Fiber.js" +import * as FiberId from "./FiberId.js" +import { constFalse, constVoid, dual } from "./Function.js" +import * as HashSet from "./HashSet.js" +import * as Inspectable from "./Inspectable.js" +import * as Iterable from "./Iterable.js" +import { type Pipeable, pipeArguments } from "./Pipeable.js" +import * as Predicate from "./Predicate.js" +import * as Runtime from "./Runtime.js" +import type * as Scope from "./Scope.js" + +/** + * @since 2.0.0 + * @categories type ids + */ +export const TypeId: unique symbol = Symbol.for("effect/FiberSet") + +/** + * @since 2.0.0 + * @categories type ids + */ +export type TypeId = typeof TypeId + +/** + * @since 2.0.0 + * @categories models + */ +export interface FiberSet + extends Pipeable, Inspectable.Inspectable, Iterable> +{ + readonly [TypeId]: TypeId + readonly deferred: Deferred.Deferred + /** @internal */ + state: { + readonly _tag: "Open" + readonly backing: Set> + } | { + readonly _tag: "Closed" + } +} + +/** + * @since 2.0.0 + * @categories refinements + */ +export const isFiberSet = (u: unknown): u is FiberSet => Predicate.hasProperty(u, TypeId) + +const Proto = { + [TypeId]: TypeId, + [Symbol.iterator](this: FiberSet) { + if (this.state._tag === "Closed") { + return Iterable.empty() + } + return this.state.backing[Symbol.iterator]() + }, + toString(this: FiberSet) { + return Inspectable.format(this.toJSON()) + }, + toJSON(this: FiberSet) { + return { + _id: "FiberMap", + state: this.state + } + }, + [Inspectable.NodeInspectSymbol](this: FiberSet) { + return this.toJSON() + }, + pipe() { + return pipeArguments(this, arguments) + } +} + +const unsafeMake = ( + backing: Set>, + deferred: Deferred.Deferred +): FiberSet => { + const self = Object.create(Proto) + self.state = { _tag: "Open", backing } + self.deferred = deferred + return self +} + +/** + * A FiberSet can be used to store a collection of fibers. + * When the associated Scope is closed, all fibers in the set will be interrupted. + * + * You can add fibers to the set using `FiberSet.add` or `FiberSet.run`, and the fibers will + * be automatically removed from the FiberSet when they complete. + * + * @example + * ```ts + * import { Effect, FiberSet } from "effect" + * + * Effect.gen(function*() { + * const set = yield* FiberSet.make() + * + * // run some effects and add the fibers to the set + * yield* FiberSet.run(set, Effect.never) + * yield* FiberSet.run(set, Effect.never) + * + * yield* Effect.sleep(1000) + * }).pipe( + * Effect.scoped // The fibers will be interrupted when the scope is closed + * ) + * ``` + * + * @since 2.0.0 + * @categories constructors + */ +export const make = (): Effect.Effect, never, Scope.Scope> => + Effect.acquireRelease( + Effect.map(Deferred.make(), (deferred) => unsafeMake(new Set(), deferred)), + (set) => + Effect.withFiberRuntime((parent) => { + const state = set.state + if (state._tag === "Closed") return Effect.void + set.state = { _tag: "Closed" } + const fibers = state.backing + return Fiber.interruptAllAs(fibers, FiberId.combine(parent.id(), internalFiberId)).pipe( + Effect.intoDeferred(set.deferred) + ) + }) + ) + +/** + * Create an Effect run function that is backed by a FiberSet. + * + * @since 2.0.0 + * @categories constructors + */ +export const makeRuntime = (): Effect.Effect< + ( + effect: Effect.Effect, + options?: Runtime.RunForkOptions | undefined + ) => Fiber.RuntimeFiber, + never, + Scope.Scope | R +> => + Effect.flatMap( + make(), + (self) => runtime(self)() + ) + +/** + * Create an Effect run function that is backed by a FiberSet. + * + * @since 3.13.0 + * @categories constructors + */ +export const makeRuntimePromise = (): Effect.Effect< + ( + effect: Effect.Effect, + options?: Runtime.RunForkOptions | undefined + ) => Promise, + never, + Scope.Scope | R +> => + Effect.flatMap( + make(), + (self) => runtimePromise(self)() + ) + +const internalFiberIdId = -1 +const internalFiberId = FiberId.make(internalFiberIdId, 0) +const isInternalInterruption = Cause.reduceWithContext(undefined, { + emptyCase: constFalse, + failCase: constFalse, + dieCase: constFalse, + interruptCase: (_, fiberId) => HashSet.has(FiberId.ids(fiberId), internalFiberIdId), + sequentialCase: (_, left, right) => left || right, + parallelCase: (_, left, right) => left || right +}) + +/** + * Add a fiber to the FiberSet. When the fiber completes, it will be removed. + * + * @since 2.0.0 + * @categories combinators + */ +export const unsafeAdd: { + ( + fiber: Fiber.RuntimeFiber, + options?: { + readonly interruptAs?: FiberId.FiberId | undefined + readonly propagateInterruption?: boolean | undefined + } | undefined + ): (self: FiberSet) => void + ( + self: FiberSet, + fiber: Fiber.RuntimeFiber, + options?: { + readonly interruptAs?: FiberId.FiberId | undefined + readonly propagateInterruption?: boolean | undefined + } | undefined + ): void +} = dual((args) => isFiberSet(args[0]), ( + self: FiberSet, + fiber: Fiber.RuntimeFiber, + options?: { + readonly interruptAs?: FiberId.FiberId | undefined + readonly propagateInterruption?: boolean | undefined + } | undefined +): void => { + if (self.state._tag === "Closed") { + fiber.unsafeInterruptAsFork(FiberId.combine(options?.interruptAs ?? FiberId.none, internalFiberId)) + return + } else if (self.state.backing.has(fiber)) { + return + } + self.state.backing.add(fiber) + fiber.addObserver((exit) => { + if (self.state._tag === "Closed") { + return + } + self.state.backing.delete(fiber) + if ( + Exit.isFailure(exit) && + ( + options?.propagateInterruption === true ? + !isInternalInterruption(exit.cause) : + !Cause.isInterruptedOnly(exit.cause) + ) + ) { + Deferred.unsafeDone(self.deferred, exit as any) + } + }) +}) + +/** + * Add a fiber to the FiberSet. When the fiber completes, it will be removed. + * + * @since 2.0.0 + * @categories combinators + */ +export const add: { + ( + fiber: Fiber.RuntimeFiber, + options?: { + readonly propagateInterruption?: boolean | undefined + } | undefined + ): (self: FiberSet) => Effect.Effect + ( + self: FiberSet, + fiber: Fiber.RuntimeFiber, + options?: { + readonly propagateInterruption?: boolean | undefined + } | undefined + ): Effect.Effect +} = dual( + (args) => isFiberSet(args[0]), + ( + self: FiberSet, + fiber: Fiber.RuntimeFiber, + options?: { + readonly propagateInterruption?: boolean | undefined + } | undefined + ): Effect.Effect => + Effect.fiberIdWith((fiberId) => + Effect.sync(() => + unsafeAdd(self, fiber, { + ...options, + interruptAs: fiberId + }) + ) + ) +) + +/** + * @since 2.0.0 + * @categories combinators + */ +export const clear = (self: FiberSet): Effect.Effect => + Effect.withFiberRuntime((clearFiber) => { + if (self.state._tag === "Closed") { + return Effect.void + } + return Effect.forEach(self.state.backing, (fiber) => + // will be removed by the observer + Fiber.interruptAs(fiber, FiberId.combine(clearFiber.id(), internalFiberId))) + }) + +const constInterruptedFiber = (function() { + let fiber: Fiber.RuntimeFiber | undefined = undefined + return () => { + if (fiber === undefined) { + fiber = Effect.runFork(Effect.interrupt) + } + return fiber + } +})() + +/** + * Fork an Effect and add the forked fiber to the FiberSet. + * When the fiber completes, it will be removed from the FiberSet. + * + * @since 2.0.0 + * @categories combinators + */ +export const run: { + ( + self: FiberSet, + options?: { + readonly propagateInterruption?: boolean | undefined + } | undefined + ): ( + effect: Effect.Effect + ) => Effect.Effect, never, R> + ( + self: FiberSet, + effect: Effect.Effect, + options?: { + readonly propagateInterruption?: boolean | undefined + } | undefined + ): Effect.Effect, never, R> +} = function() { + const self = arguments[0] as FiberSet + if (!Effect.isEffect(arguments[1])) { + const options = arguments[1] + return (effect: Effect.Effect) => runImpl(self, effect, options) + } + return runImpl(self, arguments[1], arguments[2]) as any +} + +const runImpl = ( + self: FiberSet, + effect: Effect.Effect, + options?: { + readonly propagateInterruption?: boolean | undefined + } +): Effect.Effect, never, R> => + Effect.fiberIdWith((fiberId) => { + if (self.state._tag === "Closed") { + return Effect.sync(constInterruptedFiber) + } + return Effect.tap( + Effect.forkDaemon(effect), + (fiber) => + unsafeAdd(self, fiber, { + ...options, + interruptAs: fiberId + }) + ) + }) + +/** + * Capture a Runtime and use it to fork Effect's, adding the forked fibers to the FiberSet. + * + * @example + * ```ts + * import { Context, Effect, FiberSet } from "effect" + * + * interface Users { + * readonly _: unique symbol + * } + * const Users = Context.GenericTag> + * }>("Users") + * + * Effect.gen(function*() { + * const set = yield* FiberSet.make() + * const run = yield* FiberSet.runtime(set)() + * + * // run some effects and add the fibers to the set + * run(Effect.andThen(Users, _ => _.getAll)) + * }).pipe( + * Effect.scoped // The fibers will be interrupted when the scope is closed + * ) + * ``` + * + * @since 2.0.0 + * @categories combinators + */ +export const runtime: ( + self: FiberSet +) => () => Effect.Effect< + ( + effect: Effect.Effect, + options?: + | Runtime.RunForkOptions & { readonly propagateInterruption?: boolean | undefined } + | undefined + ) => Fiber.RuntimeFiber, + never, + R +> = (self: FiberSet) => () => + Effect.map( + Effect.runtime(), + (runtime) => { + const runFork = Runtime.runFork(runtime) + return ( + effect: Effect.Effect, + options?: + | Runtime.RunForkOptions & { readonly propagateInterruption?: boolean | undefined } + | undefined + ) => { + if (self.state._tag === "Closed") { + return constInterruptedFiber() + } + const fiber = runFork(effect, options) + unsafeAdd(self, fiber) + return fiber + } + } + ) + +/** + * Capture a Runtime and use it to fork Effect's, adding the forked fibers to the FiberSet. + * + * The returned run function will return Promise's. + * + * @since 3.13.0 + * @categories combinators + */ +export const runtimePromise = (self: FiberSet): () => Effect.Effect< + ( + effect: Effect.Effect, + options?: + | Runtime.RunForkOptions & { readonly propagateInterruption?: boolean | undefined } + | undefined + ) => Promise, + never, + R +> => +() => + Effect.map( + runtime(self)(), + (runFork) => + ( + effect: Effect.Effect, + options?: + | Runtime.RunForkOptions & { readonly propagateInterruption?: boolean | undefined } + | undefined + ): Promise => + new Promise((resolve, reject) => + runFork(effect, options).addObserver((exit) => { + if (Exit.isSuccess(exit)) { + resolve(exit.value) + } else { + reject(Cause.squash(exit.cause)) + } + }) + ) + ) + +/** + * @since 2.0.0 + * @categories combinators + */ +export const size = (self: FiberSet): Effect.Effect => + Effect.sync(() => self.state._tag === "Closed" ? 0 : self.state.backing.size) + +/** + * Join all fibers in the FiberSet. If any of the Fiber's in the set terminate with a failure, + * the returned Effect will terminate with the first failure that occurred. + * + * @since 2.0.0 + * @categories combinators + * @example + * ```ts + * import { Effect, FiberSet } from "effect"; + * + * Effect.gen(function* (_) { + * const set = yield* _(FiberSet.make()); + * yield* _(FiberSet.add(set, Effect.runFork(Effect.fail("error")))); + * + * // parent fiber will fail with "error" + * yield* _(FiberSet.join(set)); + * }); + * ``` + */ +export const join = (self: FiberSet): Effect.Effect => + Deferred.await(self.deferred as Deferred.Deferred) + +/** + * Wait until the fiber set is empty. + * + * @since 3.13.0 + * @categories combinators + */ +export const awaitEmpty = (self: FiberSet): Effect.Effect => + Effect.whileLoop({ + while: () => self.state._tag === "Open" && self.state.backing.size > 0, + body: () => Fiber.await(Iterable.unsafeHead(self)), + step: constVoid + }) diff --git a/repos/effect/packages/effect/src/FiberStatus.ts b/repos/effect/packages/effect/src/FiberStatus.ts new file mode 100644 index 0000000..4c08a7f --- /dev/null +++ b/repos/effect/packages/effect/src/FiberStatus.ts @@ -0,0 +1,108 @@ +/** + * @since 2.0.0 + */ +import type * as Equal from "./Equal.js" +import type * as FiberId from "./FiberId.js" +import * as internal from "./internal/fiberStatus.js" +import type * as RuntimeFlags from "./RuntimeFlags.js" + +/** + * @since 2.0.0 + * @category symbols + */ +export const FiberStatusTypeId: unique symbol = internal.FiberStatusTypeId + +/** + * @since 2.0.0 + * @category symbols + */ +export type FiberStatusTypeId = typeof FiberStatusTypeId + +/** + * @since 2.0.0 + * @category models + */ +export type FiberStatus = Done | Running | Suspended + +/** + * @since 2.0.0 + * @category models + */ +export interface Done extends Equal.Equal { + readonly _tag: "Done" + readonly [FiberStatusTypeId]: FiberStatusTypeId +} + +/** + * @since 2.0.0 + * @category models + */ +export interface Running extends Equal.Equal { + readonly _tag: "Running" + readonly [FiberStatusTypeId]: FiberStatusTypeId + readonly runtimeFlags: RuntimeFlags.RuntimeFlags +} + +/** + * @since 2.0.0 + * @category models + */ +export interface Suspended extends Equal.Equal { + readonly _tag: "Suspended" + readonly [FiberStatusTypeId]: FiberStatusTypeId + readonly runtimeFlags: RuntimeFlags.RuntimeFlags + readonly blockingOn: FiberId.FiberId +} + +/** + * @since 2.0.0 + * @category constructors + */ +export const done: FiberStatus = internal.done + +/** + * @since 2.0.0 + * @category constructors + */ +export const running: (runtimeFlags: RuntimeFlags.RuntimeFlags) => FiberStatus = internal.running + +/** + * @since 2.0.0 + * @category constructors + */ +export const suspended: (runtimeFlags: RuntimeFlags.RuntimeFlags, blockingOn: FiberId.FiberId) => FiberStatus = + internal.suspended + +/** + * Returns `true` if the specified value is a `FiberStatus`, `false` otherwise. + * + * @since 2.0.0 + * @category refinements + */ +export const isFiberStatus: (u: unknown) => u is FiberStatus = internal.isFiberStatus + +/** + * Returns `true` if the specified `FiberStatus` is `Done`, `false` otherwise. + * + * @since 2.0.0 + * @category refinements + */ +export const isDone: (self: FiberStatus) => self is Done = internal.isDone + +/** + * Returns `true` if the specified `FiberStatus` is `Running`, `false` + * otherwise. + * + * @since 2.0.0 + * @category refinements + */ +export const isRunning: (self: FiberStatus) => self is Running = internal.isRunning + +/** + * Returns `true` if the specified `FiberStatus` is `Suspended`, `false` + * otherwise. + * + * @since 2.0.0 + * @category refinements + */ +export const isSuspended: (self: FiberStatus) => self is Suspended = internal.isSuspended diff --git a/repos/effect/packages/effect/src/Function.ts b/repos/effect/packages/effect/src/Function.ts new file mode 100644 index 0000000..69d53fd --- /dev/null +++ b/repos/effect/packages/effect/src/Function.ts @@ -0,0 +1,1222 @@ +/** + * @since 2.0.0 + */ +import type { TypeLambda } from "./HKT.js" + +/** + * @category type lambdas + * @since 2.0.0 + */ +export interface FunctionTypeLambda extends TypeLambda { + readonly type: (a: this["In"]) => this["Target"] +} + +/** + * Tests if a value is a `function`. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { isFunction } from "effect/Predicate" + * + * assert.deepStrictEqual(isFunction(isFunction), true) + * assert.deepStrictEqual(isFunction("function"), false) + * ``` + * + * @category guards + * @since 2.0.0 + */ +export const isFunction = (input: unknown): input is Function => typeof input === "function" + +/** + * Creates a function that can be used in a data-last (aka `pipe`able) or + * data-first style. + * + * The first parameter to `dual` is either the arity of the uncurried function + * or a predicate that determines if the function is being used in a data-first + * or data-last style. + * + * Using the arity is the most common use case, but there are some cases where + * you may want to use a predicate. For example, if you have a function that + * takes an optional argument, you can use a predicate to determine if the + * function is being used in a data-first or data-last style. + * + * You can pass either the arity of the uncurried function or a predicate + * which determines if the function is being used in a data-first or + * data-last style. + * + * **Example** (Using arity to determine data-first or data-last style) + * + * ```ts + * import { dual, pipe } from "effect/Function" + * + * const sum = dual< + * (that: number) => (self: number) => number, + * (self: number, that: number) => number + * >(2, (self, that) => self + that) + * + * console.log(sum(2, 3)) // 5 + * console.log(pipe(2, sum(3))) // 5 + * ``` + * + * **Example** (Using call signatures to define the overloads) + * + * ```ts + * import { dual, pipe } from "effect/Function" + * + * const sum: { + * (that: number): (self: number) => number + * (self: number, that: number): number + * } = dual(2, (self: number, that: number): number => self + that) + * + * console.log(sum(2, 3)) // 5 + * console.log(pipe(2, sum(3))) // 5 + * ``` + * + * **Example** (Using a predicate to determine data-first or data-last style) + * + * ```ts + * import { dual, pipe } from "effect/Function" + * + * const sum = dual< + * (that: number) => (self: number) => number, + * (self: number, that: number) => number + * >( + * (args) => args.length === 2, + * (self, that) => self + that + * ) + * + * console.log(sum(2, 3)) // 5 + * console.log(pipe(2, sum(3))) // 5 + * ``` + * + * @since 2.0.0 + */ +export const dual: { + ) => any, DataFirst extends (...args: Array) => any>( + arity: Parameters["length"], + body: DataFirst + ): DataLast & DataFirst + ) => any, DataFirst extends (...args: Array) => any>( + isDataFirst: (args: IArguments) => boolean, + body: DataFirst + ): DataLast & DataFirst +} = function(arity, body) { + if (typeof arity === "function") { + return function() { + if (arity(arguments)) { + // @ts-expect-error + return body.apply(this, arguments) + } + return ((self: any) => body(self, ...arguments)) as any + } + } + + switch (arity) { + case 0: + case 1: + throw new RangeError(`Invalid arity ${arity}`) + + case 2: + return function(a, b) { + if (arguments.length >= 2) { + return body(a, b) + } + return function(self: any) { + return body(self, a) + } + } + + case 3: + return function(a, b, c) { + if (arguments.length >= 3) { + return body(a, b, c) + } + return function(self: any) { + return body(self, a, b) + } + } + + case 4: + return function(a, b, c, d) { + if (arguments.length >= 4) { + return body(a, b, c, d) + } + return function(self: any) { + return body(self, a, b, c) + } + } + + case 5: + return function(a, b, c, d, e) { + if (arguments.length >= 5) { + return body(a, b, c, d, e) + } + return function(self: any) { + return body(self, a, b, c, d) + } + } + + default: + return function() { + if (arguments.length >= arity) { + // @ts-expect-error + return body.apply(this, arguments) + } + const args = arguments + return function(self: any) { + return body(self, ...args) + } + } + } +} +/** + * Apply a function to given values. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { pipe, apply } from "effect/Function" + * import { length } from "effect/String" + * + * assert.deepStrictEqual(pipe(length, apply("hello")), 5) + * ``` + * + * @since 2.0.0 + */ +export const apply = >(...a: A) => (self: (...a: A) => B): B => self(...a) + +/** + * A lazy argument. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { LazyArg, constant } from "effect/Function" + * + * const constNull: LazyArg = constant(null) + * ``` + * + * @since 2.0.0 + */ +export interface LazyArg { + (): A +} + +/** + * @example + * ```ts + * import * as assert from "node:assert" + * import { FunctionN } from "effect/Function" + * + * const sum: FunctionN<[number, number], number> = (a, b) => a + b + * ``` + * + * @since 2.0.0 + */ +export interface FunctionN, B> { + (...args: A): B +} + +/** + * The identity function, i.e. A function that returns its input argument. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { identity } from "effect/Function" + * + * assert.deepStrictEqual(identity(5), 5) + * ``` + * + * @since 2.0.0 + */ +export const identity = (a: A): A => a + +/** + * A function that ensures that the type of an expression matches some type, + * without changing the resulting type of that expression. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { satisfies } from "effect/Function" + * + * const test1 = satisfies()(5 as const) + * //^? const test: 5 + * // @ts-expect-error + * const test2 = satisfies()(5) + * //^? Argument of type 'number' is not assignable to parameter of type 'string' + * + * assert.deepStrictEqual(satisfies()(5), 5) + * ``` + * + * @since 2.0.0 + */ +export const satisfies = () => (b: B) => b + +/** + * Casts the result to the specified type. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { unsafeCoerce, identity } from "effect/Function" + * + * assert.deepStrictEqual(unsafeCoerce, identity) + * ``` + * + * @since 2.0.0 + */ +export const unsafeCoerce: (a: A) => B = identity as any + +/** + * Creates a constant value that never changes. + * + * This is useful when you want to pass a value to a higher-order function (a function that takes another function as its argument) + * and want that inner function to always use the same value, no matter how many times it is called. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { constant } from "effect/Function" + * + * const constNull = constant(null) + * + * assert.deepStrictEqual(constNull(), null) + * assert.deepStrictEqual(constNull(), null) + * ``` + * + * @since 2.0.0 + */ +export const constant = (value: A): LazyArg => () => value + +/** + * A thunk that returns always `true`. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { constTrue } from "effect/Function" + * + * assert.deepStrictEqual(constTrue(), true) + * ``` + * + * @since 2.0.0 + */ +export const constTrue: LazyArg = constant(true) + +/** + * A thunk that returns always `false`. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { constFalse } from "effect/Function" + * + * assert.deepStrictEqual(constFalse(), false) + * ``` + * + * @since 2.0.0 + */ +export const constFalse: LazyArg = constant(false) + +/** + * A thunk that returns always `null`. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { constNull } from "effect/Function" + * + * assert.deepStrictEqual(constNull(), null) + * ``` + * + * @since 2.0.0 + */ +export const constNull: LazyArg = constant(null) + +/** + * A thunk that returns always `undefined`. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { constUndefined } from "effect/Function" + * + * assert.deepStrictEqual(constUndefined(), undefined) + * ``` + * + * @since 2.0.0 + */ +export const constUndefined: LazyArg = constant(undefined) + +/** + * A thunk that returns always `void`. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { constVoid } from "effect/Function" + * + * assert.deepStrictEqual(constVoid(), undefined) + * ``` + * + * @since 2.0.0 + */ +export const constVoid: LazyArg = constUndefined + +/** + * Reverses the order of arguments for a curried function. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { flip } from "effect/Function" + * + * const f = (a: number) => (b: string) => a - b.length + * + * assert.deepStrictEqual(flip(f)('aaa')(2), -1) + * ``` + * + * @since 2.0.0 + */ +export const flip = , B extends Array, C>( + f: (...a: A) => (...b: B) => C +): (...b: B) => (...a: A) => C => +(...b) => +(...a) => f(...a)(...b) + +/** + * Composes two functions, `ab` and `bc` into a single function that takes in an argument `a` of type `A` and returns a result of type `C`. + * The result is obtained by first applying the `ab` function to `a` and then applying the `bc` function to the result of `ab`. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { compose } from "effect/Function" + * + * const increment = (n: number) => n + 1; + * const square = (n: number) => n * n; + * + * assert.strictEqual(compose(increment, square)(2), 9); + * ``` + * + * @since 2.0.0 + */ +export const compose: { + (bc: (b: B) => C): (self: (a: A) => B) => (a: A) => C + (self: (a: A) => B, bc: (b: B) => C): (a: A) => C +} = dual(2, (ab: (a: A) => B, bc: (b: B) => C): (a: A) => C => (a) => bc(ab(a))) + +/** + * The `absurd` function is a stub for cases where a value of type `never` is encountered in your code, + * meaning that it should be impossible for this code to be executed. + * + * This function is particularly useful when it's necessary to specify that certain cases are impossible. + * + * @since 2.0.0 + */ +export const absurd = (_: never): A => { + throw new Error("Called `absurd` function which should be uncallable") +} + +/** + * Creates a version of this function: instead of `n` arguments, it accepts a single tuple argument. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { tupled } from "effect/Function" + * + * const sumTupled = tupled((x: number, y: number): number => x + y) + * + * assert.deepStrictEqual(sumTupled([1, 2]), 3) + * ``` + * + * @since 2.0.0 + */ +export const tupled = , B>(f: (...a: A) => B): (a: A) => B => (a) => f(...a) + +/** + * Inverse function of `tupled` + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { untupled } from "effect/Function" + * + * const getFirst = untupled((tuple: [A, B]): A => tuple[0]) + * + * assert.deepStrictEqual(getFirst(1, 2), 1) + * ``` + * + * @since 2.0.0 + */ +export const untupled = , B>(f: (a: A) => B): (...a: A) => B => (...a) => f(a) + +/** + * Pipes the value of an expression into a pipeline of functions. + * + * **Details** + * + * The `pipe` function is a utility that allows us to compose functions in a + * readable and sequential manner. It takes the output of one function and + * passes it as the input to the next function in the pipeline. This enables us + * to build complex transformations by chaining multiple functions together. + * + * ```ts skip-type-checking + * import { pipe } from "effect" + * + * const result = pipe(input, func1, func2, ..., funcN) + * ``` + * + * In this syntax, `input` is the initial value, and `func1`, `func2`, ..., + * `funcN` are the functions to be applied in sequence. The result of each + * function becomes the input for the next function, and the final result is + * returned. + * + * Here's an illustration of how `pipe` works: + * + * ``` + * ┌───────┐ ┌───────┐ ┌───────┐ ┌───────┐ ┌───────┐ ┌────────┐ + * │ input │───►│ func1 │───►│ func2 │───►│ ... │───►│ funcN │───►│ result │ + * └───────┘ └───────┘ └───────┘ └───────┘ └───────┘ └────────┘ + * ``` + * + * It's important to note that functions passed to `pipe` must have a **single + * argument** because they are only called with a single argument. + * + * **When to Use** + * + * This is useful in combination with data-last functions as a simulation of + * methods: + * + * ```ts skip-type-checking + * as.map(f).filter(g) + * ``` + * + * becomes: + * + * ```ts skip-type-checking + * import { pipe, Array } from "effect" + * + * pipe(as, Array.map(f), Array.filter(g)) + * ``` + * + * **Example** (Chaining Arithmetic Operations) + * + * ```ts + * import { pipe } from "effect" + * + * // Define simple arithmetic operations + * const increment = (x: number) => x + 1 + * const double = (x: number) => x * 2 + * const subtractTen = (x: number) => x - 10 + * + * // Sequentially apply these operations using `pipe` + * const result = pipe(5, increment, double, subtractTen) + * + * console.log(result) + * // Output: 2 + * ``` + * + * @since 2.0.0 + */ +export function pipe(a: A): A +export function pipe(a: A, ab: (a: A) => B): B +export function pipe( + a: A, + ab: (a: A) => B, + bc: (b: B) => C +): C +export function pipe( + a: A, + ab: (a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D +): D +export function pipe( + a: A, + ab: (a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D, + de: (d: D) => E +): E +export function pipe( + a: A, + ab: (a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D, + de: (d: D) => E, + ef: (e: E) => F +): F +export function pipe< + A, + B = never, + C = never, + D = never, + E = never, + F = never, + G = never +>( + a: A, + ab: (a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D, + de: (d: D) => E, + ef: (e: E) => F, + fg: (f: F) => G +): G +export function pipe< + A, + B = never, + C = never, + D = never, + E = never, + F = never, + G = never, + H = never +>( + a: A, + ab: (a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D, + de: (d: D) => E, + ef: (e: E) => F, + fg: (f: F) => G, + gh: (g: G) => H +): H +export function pipe< + A, + B = never, + C = never, + D = never, + E = never, + F = never, + G = never, + H = never, + I = never +>( + a: A, + ab: (a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D, + de: (d: D) => E, + ef: (e: E) => F, + fg: (f: F) => G, + gh: (g: G) => H, + hi: (h: H) => I +): I +export function pipe< + A, + B = never, + C = never, + D = never, + E = never, + F = never, + G = never, + H = never, + I = never, + J = never +>( + a: A, + ab: (a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D, + de: (d: D) => E, + ef: (e: E) => F, + fg: (f: F) => G, + gh: (g: G) => H, + hi: (h: H) => I, + ij: (i: I) => J +): J +export function pipe< + A, + B = never, + C = never, + D = never, + E = never, + F = never, + G = never, + H = never, + I = never, + J = never, + K = never +>( + a: A, + ab: (a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D, + de: (d: D) => E, + ef: (e: E) => F, + fg: (f: F) => G, + gh: (g: G) => H, + hi: (h: H) => I, + ij: (i: I) => J, + jk: (j: J) => K +): K +export function pipe< + A, + B = never, + C = never, + D = never, + E = never, + F = never, + G = never, + H = never, + I = never, + J = never, + K = never, + L = never +>( + a: A, + ab: (a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D, + de: (d: D) => E, + ef: (e: E) => F, + fg: (f: F) => G, + gh: (g: G) => H, + hi: (h: H) => I, + ij: (i: I) => J, + jk: (j: J) => K, + kl: (k: K) => L +): L +export function pipe< + A, + B = never, + C = never, + D = never, + E = never, + F = never, + G = never, + H = never, + I = never, + J = never, + K = never, + L = never, + M = never +>( + a: A, + ab: (a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D, + de: (d: D) => E, + ef: (e: E) => F, + fg: (f: F) => G, + gh: (g: G) => H, + hi: (h: H) => I, + ij: (i: I) => J, + jk: (j: J) => K, + kl: (k: K) => L, + lm: (l: L) => M +): M +export function pipe< + A, + B = never, + C = never, + D = never, + E = never, + F = never, + G = never, + H = never, + I = never, + J = never, + K = never, + L = never, + M = never, + N = never +>( + a: A, + ab: (a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D, + de: (d: D) => E, + ef: (e: E) => F, + fg: (f: F) => G, + gh: (g: G) => H, + hi: (h: H) => I, + ij: (i: I) => J, + jk: (j: J) => K, + kl: (k: K) => L, + lm: (l: L) => M, + mn: (m: M) => N +): N +export function pipe< + A, + B = never, + C = never, + D = never, + E = never, + F = never, + G = never, + H = never, + I = never, + J = never, + K = never, + L = never, + M = never, + N = never, + O = never +>( + a: A, + ab: (a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D, + de: (d: D) => E, + ef: (e: E) => F, + fg: (f: F) => G, + gh: (g: G) => H, + hi: (h: H) => I, + ij: (i: I) => J, + jk: (j: J) => K, + kl: (k: K) => L, + lm: (l: L) => M, + mn: (m: M) => N, + no: (n: N) => O +): O +export function pipe< + A, + B = never, + C = never, + D = never, + E = never, + F = never, + G = never, + H = never, + I = never, + J = never, + K = never, + L = never, + M = never, + N = never, + O = never, + P = never +>( + a: A, + ab: (a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D, + de: (d: D) => E, + ef: (e: E) => F, + fg: (f: F) => G, + gh: (g: G) => H, + hi: (h: H) => I, + ij: (i: I) => J, + jk: (j: J) => K, + kl: (k: K) => L, + lm: (l: L) => M, + mn: (m: M) => N, + no: (n: N) => O, + op: (o: O) => P +): P +export function pipe< + A, + B = never, + C = never, + D = never, + E = never, + F = never, + G = never, + H = never, + I = never, + J = never, + K = never, + L = never, + M = never, + N = never, + O = never, + P = never, + Q = never +>( + a: A, + ab: (a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D, + de: (d: D) => E, + ef: (e: E) => F, + fg: (f: F) => G, + gh: (g: G) => H, + hi: (h: H) => I, + ij: (i: I) => J, + jk: (j: J) => K, + kl: (k: K) => L, + lm: (l: L) => M, + mn: (m: M) => N, + no: (n: N) => O, + op: (o: O) => P, + pq: (p: P) => Q +): Q +export function pipe< + A, + B = never, + C = never, + D = never, + E = never, + F = never, + G = never, + H = never, + I = never, + J = never, + K = never, + L = never, + M = never, + N = never, + O = never, + P = never, + Q = never, + R = never +>( + a: A, + ab: (a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D, + de: (d: D) => E, + ef: (e: E) => F, + fg: (f: F) => G, + gh: (g: G) => H, + hi: (h: H) => I, + ij: (i: I) => J, + jk: (j: J) => K, + kl: (k: K) => L, + lm: (l: L) => M, + mn: (m: M) => N, + no: (n: N) => O, + op: (o: O) => P, + pq: (p: P) => Q, + qr: (q: Q) => R +): R +export function pipe< + A, + B = never, + C = never, + D = never, + E = never, + F = never, + G = never, + H = never, + I = never, + J = never, + K = never, + L = never, + M = never, + N = never, + O = never, + P = never, + Q = never, + R = never, + S = never +>( + a: A, + ab: (a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D, + de: (d: D) => E, + ef: (e: E) => F, + fg: (f: F) => G, + gh: (g: G) => H, + hi: (h: H) => I, + ij: (i: I) => J, + jk: (j: J) => K, + kl: (k: K) => L, + lm: (l: L) => M, + mn: (m: M) => N, + no: (n: N) => O, + op: (o: O) => P, + pq: (p: P) => Q, + qr: (q: Q) => R, + rs: (r: R) => S +): S +export function pipe< + A, + B = never, + C = never, + D = never, + E = never, + F = never, + G = never, + H = never, + I = never, + J = never, + K = never, + L = never, + M = never, + N = never, + O = never, + P = never, + Q = never, + R = never, + S = never, + T = never +>( + a: A, + ab: (a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D, + de: (d: D) => E, + ef: (e: E) => F, + fg: (f: F) => G, + gh: (g: G) => H, + hi: (h: H) => I, + ij: (i: I) => J, + jk: (j: J) => K, + kl: (k: K) => L, + lm: (l: L) => M, + mn: (m: M) => N, + no: (n: N) => O, + op: (o: O) => P, + pq: (p: P) => Q, + qr: (q: Q) => R, + rs: (r: R) => S, + st: (s: S) => T +): T +export function pipe( + a: unknown, + ab?: Function, + bc?: Function, + cd?: Function, + de?: Function, + ef?: Function, + fg?: Function, + gh?: Function, + hi?: Function +): unknown { + switch (arguments.length) { + case 1: + return a + case 2: + return ab!(a) + case 3: + return bc!(ab!(a)) + case 4: + return cd!(bc!(ab!(a))) + case 5: + return de!(cd!(bc!(ab!(a)))) + case 6: + return ef!(de!(cd!(bc!(ab!(a))))) + case 7: + return fg!(ef!(de!(cd!(bc!(ab!(a)))))) + case 8: + return gh!(fg!(ef!(de!(cd!(bc!(ab!(a))))))) + case 9: + return hi!(gh!(fg!(ef!(de!(cd!(bc!(ab!(a)))))))) + default: { + let ret = arguments[0] + for (let i = 1; i < arguments.length; i++) { + ret = arguments[i](ret) + } + return ret + } + } +} + +/** + * Performs left-to-right function composition. The first argument may have any arity, the remaining arguments must be unary. + * + * See also [`pipe`](#pipe). + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { flow } from "effect/Function" + * + * const len = (s: string): number => s.length + * const double = (n: number): number => n * 2 + * + * const f = flow(len, double) + * + * assert.strictEqual(f('aaa'), 6) + * ``` + * + * @since 2.0.0 + */ +export function flow, B = never>( + ab: (...a: A) => B +): (...a: A) => B +export function flow, B = never, C = never>( + ab: (...a: A) => B, + bc: (b: B) => C +): (...a: A) => C +export function flow< + A extends ReadonlyArray, + B = never, + C = never, + D = never +>(ab: (...a: A) => B, bc: (b: B) => C, cd: (c: C) => D): (...a: A) => D +export function flow< + A extends ReadonlyArray, + B = never, + C = never, + D = never, + E = never +>( + ab: (...a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D, + de: (d: D) => E +): (...a: A) => E +export function flow< + A extends ReadonlyArray, + B = never, + C = never, + D = never, + E = never, + F = never +>( + ab: (...a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D, + de: (d: D) => E, + ef: (e: E) => F +): (...a: A) => F +export function flow< + A extends ReadonlyArray, + B = never, + C = never, + D = never, + E = never, + F = never, + G = never +>( + ab: (...a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D, + de: (d: D) => E, + ef: (e: E) => F, + fg: (f: F) => G +): (...a: A) => G +export function flow< + A extends ReadonlyArray, + B = never, + C = never, + D = never, + E = never, + F = never, + G = never, + H = never +>( + ab: (...a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D, + de: (d: D) => E, + ef: (e: E) => F, + fg: (f: F) => G, + gh: (g: G) => H +): (...a: A) => H +export function flow< + A extends ReadonlyArray, + B = never, + C = never, + D = never, + E = never, + F = never, + G = never, + H = never, + I = never +>( + ab: (...a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D, + de: (d: D) => E, + ef: (e: E) => F, + fg: (f: F) => G, + gh: (g: G) => H, + hi: (h: H) => I +): (...a: A) => I +export function flow< + A extends ReadonlyArray, + B = never, + C = never, + D = never, + E = never, + F = never, + G = never, + H = never, + I = never, + J = never +>( + ab: (...a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D, + de: (d: D) => E, + ef: (e: E) => F, + fg: (f: F) => G, + gh: (g: G) => H, + hi: (h: H) => I, + ij: (i: I) => J +): (...a: A) => J +export function flow( + ab: Function, + bc?: Function, + cd?: Function, + de?: Function, + ef?: Function, + fg?: Function, + gh?: Function, + hi?: Function, + ij?: Function +): unknown { + switch (arguments.length) { + case 1: + return ab + case 2: + return function(this: unknown) { + return bc!(ab.apply(this, arguments)) + } + case 3: + return function(this: unknown) { + return cd!(bc!(ab.apply(this, arguments))) + } + case 4: + return function(this: unknown) { + return de!(cd!(bc!(ab.apply(this, arguments)))) + } + case 5: + return function(this: unknown) { + return ef!(de!(cd!(bc!(ab.apply(this, arguments))))) + } + case 6: + return function(this: unknown) { + return fg!(ef!(de!(cd!(bc!(ab.apply(this, arguments)))))) + } + case 7: + return function(this: unknown) { + return gh!(fg!(ef!(de!(cd!(bc!(ab.apply(this, arguments))))))) + } + case 8: + return function(this: unknown) { + return hi!(gh!(fg!(ef!(de!(cd!(bc!(ab.apply(this, arguments)))))))) + } + case 9: + return function(this: unknown) { + return ij!(hi!(gh!(fg!(ef!(de!(cd!(bc!(ab.apply(this, arguments))))))))) + } + } + return +} + +/** + * Type hole simulation. + * + * @since 2.0.0 + */ +export const hole: () => T = unsafeCoerce(absurd) + +/** + * The SK combinator, also known as the "S-K combinator" or "S-combinator", is a fundamental combinator in the + * lambda calculus and the SKI combinator calculus. + * + * This function is useful for discarding the first argument passed to it and returning the second argument. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { SK } from "effect/Function"; + * + * assert.deepStrictEqual(SK(0, "hello"), "hello") + * ``` + * + * @since 2.0.0 + */ +export const SK = (_: A, b: B): B => b diff --git a/repos/effect/packages/effect/src/GlobalValue.ts b/repos/effect/packages/effect/src/GlobalValue.ts new file mode 100644 index 0000000..5b5fe0c --- /dev/null +++ b/repos/effect/packages/effect/src/GlobalValue.ts @@ -0,0 +1,53 @@ +/** + * The `GlobalValue` module ensures that a single instance of a value is created globally, + * even when modules are imported multiple times (e.g., due to mixing CommonJS and ESM builds) + * or during hot-reloading in development environments like Next.js or Remix. + * + * It achieves this by using a versioned global store, identified by a unique `Symbol` tied to + * the current version of the `effect` library. The store holds values that are keyed by an identifier, + * allowing the reuse of previously computed instances across imports or reloads. + * + * This pattern is particularly useful in scenarios where frequent reloading can cause services or + * single-instance objects to be recreated unnecessarily, such as in development environments with hot-reloading. + * + * @since 2.0.0 + */ +const globalStoreId = `effect/GlobalValue` + +let globalStore: Map + +/** + * Retrieves or computes a global value associated with the given `id`. If the value for this `id` + * has already been computed, it will be returned from the global store. If it does not exist yet, + * the provided `compute` function will be executed to compute the value, store it, and then return it. + * + * This ensures that even in cases where the module is imported multiple times (e.g., in mixed environments + * like CommonJS and ESM, or during hot-reloading in development), the value is computed only once and reused + * thereafter. + * + * @example + * ```ts + * import { globalValue } from "effect/GlobalValue" + * + * // This cache will persist as long as the module is running, + * // even if reloaded or imported elsewhere + * const myCache = globalValue( + * Symbol.for("myCache"), + * () => new WeakMap() + * ) + * ``` + * + * @since 2.0.0 + */ +export const globalValue = (id: unknown, compute: () => A): A => { + if (!globalStore) { + // @ts-expect-error + globalThis[globalStoreId] ??= new Map() + // @ts-expect-error + globalStore = globalThis[globalStoreId] as Map + } + if (!globalStore.has(id)) { + globalStore.set(id, compute()) + } + return globalStore.get(id)! +} diff --git a/repos/effect/packages/effect/src/Graph.ts b/repos/effect/packages/effect/src/Graph.ts new file mode 100644 index 0000000..77f6ec5 --- /dev/null +++ b/repos/effect/packages/effect/src/Graph.ts @@ -0,0 +1,3735 @@ +/** + * @experimental + * @since 3.18.0 + */ + +import * as Data from "./Data.js" +import * as Equal from "./Equal.js" +import { dual } from "./Function.js" +import * as Hash from "./Hash.js" +import type { Inspectable } from "./Inspectable.js" +import { format, NodeInspectSymbol } from "./Inspectable.js" +import * as Option from "./Option.js" +import type { Pipeable } from "./Pipeable.js" +import { pipeArguments } from "./Pipeable.js" +import type { Mutable } from "./Types.js" + +/** + * Unique identifier for Graph instances. + * + * @since 3.18.0 + * @category symbol + */ +export const TypeId: "~effect/Graph" = "~effect/Graph" as const + +/** + * Type identifier for Graph instances. + * + * @since 3.18.0 + * @category symbol + */ +export type TypeId = typeof TypeId + +/** + * Node index for node identification using plain numbers. + * + * @since 3.18.0 + * @category models + */ +export type NodeIndex = number + +/** + * Edge index for edge identification using plain numbers. + * + * @since 3.18.0 + * @category models + */ +export type EdgeIndex = number + +/** + * Edge data containing source, target, and user data. + * + * @since 3.18.0 + * @category models + */ +export class Edge extends Data.Class<{ + readonly source: NodeIndex + readonly target: NodeIndex + readonly data: E +}> {} + +/** + * Graph type for distinguishing directed and undirected graphs. + * + * @since 3.18.0 + * @category models + */ +export type Kind = "directed" | "undirected" + +/** + * Graph prototype interface. + * + * @since 3.18.0 + * @category models + */ +export interface Proto extends Iterable, Equal.Equal, Pipeable, Inspectable { + readonly [TypeId]: TypeId + readonly nodes: Map + readonly edges: Map> + readonly adjacency: Map> + readonly reverseAdjacency: Map> + nextNodeIndex: NodeIndex + nextEdgeIndex: EdgeIndex + isAcyclic: Option.Option +} + +/** + * Immutable graph interface. + * + * @since 3.18.0 + * @category models + */ +export interface Graph extends Proto { + readonly type: T + readonly mutable: false +} + +/** + * Mutable graph interface. + * + * @since 3.18.0 + * @category models + */ +export interface MutableGraph extends Proto { + readonly type: T + readonly mutable: true +} + +/** + * Directed graph type alias. + * + * @since 3.18.0 + * @category models + */ +export type DirectedGraph = Graph + +/** + * Undirected graph type alias. + * + * @since 3.18.0 + * @category models + */ +export type UndirectedGraph = Graph + +/** + * Mutable directed graph type alias. + * + * @since 3.18.0 + * @category models + */ +export type MutableDirectedGraph = MutableGraph + +/** + * Mutable undirected graph type alias. + * + * @since 3.18.0 + * @category models + */ +export type MutableUndirectedGraph = MutableGraph + +// ============================================================================= +// Proto Objects +// ============================================================================= + +/** @internal */ +const ProtoGraph = { + [TypeId]: TypeId, + [Symbol.iterator](this: Graph) { + return this.nodes[Symbol.iterator]() + }, + [NodeInspectSymbol](this: Graph) { + return this.toJSON() + }, + [Equal.symbol](this: Graph, that: Equal.Equal): boolean { + if (isGraph(that)) { + if ( + this.nodes.size !== that.nodes.size || + this.edges.size !== that.edges.size || + this.type !== that.type + ) { + return false + } + // Compare nodes + for (const [nodeIndex, nodeData] of this.nodes) { + if (!that.nodes.has(nodeIndex)) { + return false + } + const otherNodeData = that.nodes.get(nodeIndex)! + if (!Equal.equals(nodeData, otherNodeData)) { + return false + } + } + // Compare edges + for (const [edgeIndex, edgeData] of this.edges) { + if (!that.edges.has(edgeIndex)) { + return false + } + const otherEdge = that.edges.get(edgeIndex)! + if (!Equal.equals(edgeData, otherEdge)) { + return false + } + } + return true + } + return false + }, + [Hash.symbol](this: Graph): number { + let hash = Hash.string("Graph") + hash = hash ^ Hash.string(this.type) + hash = hash ^ Hash.number(this.nodes.size) + hash = hash ^ Hash.number(this.edges.size) + for (const [nodeIndex, nodeData] of this.nodes) { + hash = hash ^ (Hash.hash(nodeIndex) + Hash.hash(nodeData)) + } + for (const [edgeIndex, edgeData] of this.edges) { + hash = hash ^ (Hash.hash(edgeIndex) + Hash.hash(edgeData)) + } + return hash + }, + toJSON(this: Graph) { + return { + _id: "Graph", + nodeCount: this.nodes.size, + edgeCount: this.edges.size, + type: this.type + } + }, + toString(this: Graph) { + return format(this) + }, + pipe() { + return pipeArguments(this, arguments) + } +} + +// ============================================================================= +// Errors +// ============================================================================= + +/** + * Error thrown when a graph operation fails. + * + * @since 3.18.0 + * @category errors + */ +export class GraphError extends Data.TaggedError("GraphError")<{ + readonly message: string +}> {} + +/** @internal */ +const missingNode = (node: number) => new GraphError({ message: `Node ${node} does not exist` }) + +// ============================================================================= +// Constructors +// ============================================================================= + +/** @internal */ +export const isGraph = (u: unknown): u is Graph => typeof u === "object" && u !== null && TypeId in u + +/** + * Creates a directed graph, optionally with initial mutations. + * + * @example + * ```ts + * import { Graph } from "effect" + * + * // Directed graph with initial nodes and edges + * const graph = Graph.directed((mutable) => { + * const a = Graph.addNode(mutable, "A") + * const b = Graph.addNode(mutable, "B") + * const c = Graph.addNode(mutable, "C") + * Graph.addEdge(mutable, a, b, "A->B") + * Graph.addEdge(mutable, b, c, "B->C") + * }) + * ``` + * + * @since 3.18.0 + * @category constructors + */ +export const directed = (mutate?: (mutable: MutableDirectedGraph) => void): DirectedGraph => { + const graph: Mutable> = Object.create(ProtoGraph) + graph.type = "directed" + graph.nodes = new Map() + graph.edges = new Map() + graph.adjacency = new Map() + graph.reverseAdjacency = new Map() + graph.nextNodeIndex = 0 + graph.nextEdgeIndex = 0 + graph.isAcyclic = Option.some(true) + graph.mutable = false + + if (mutate) { + const mutable = beginMutation(graph as DirectedGraph) + mutate(mutable as MutableDirectedGraph) + return endMutation(mutable) + } + + return graph +} + +/** + * Creates an undirected graph, optionally with initial mutations. + * + * @example + * ```ts + * import { Graph } from "effect" + * + * // Undirected graph with initial nodes and edges + * const graph = Graph.undirected((mutable) => { + * const a = Graph.addNode(mutable, "A") + * const b = Graph.addNode(mutable, "B") + * const c = Graph.addNode(mutable, "C") + * Graph.addEdge(mutable, a, b, "A-B") + * Graph.addEdge(mutable, b, c, "B-C") + * }) + * ``` + * + * @since 3.18.0 + * @category constructors + */ +export const undirected = (mutate?: (mutable: MutableUndirectedGraph) => void): UndirectedGraph => { + const graph: Mutable> = Object.create(ProtoGraph) + graph.type = "undirected" + graph.nodes = new Map() + graph.edges = new Map() + graph.adjacency = new Map() + graph.reverseAdjacency = new Map() + graph.nextNodeIndex = 0 + graph.nextEdgeIndex = 0 + graph.isAcyclic = Option.some(true) + graph.mutable = false + + if (mutate) { + const mutable = beginMutation(graph) + mutate(mutable as MutableUndirectedGraph) + return endMutation(mutable) + } + + return graph +} + +// ============================================================================= +// Scoped Mutable API +// ============================================================================= + +/** + * Creates a mutable scope for safe graph mutations by copying the data structure. + * + * @example + * ```ts + * import { Graph } from "effect" + * + * const graph = Graph.directed() + * const mutable = Graph.beginMutation(graph) + * // Now mutable can be safely modified without affecting original graph + * ``` + * + * @since 3.18.0 + * @category mutations + */ +export const beginMutation = ( + graph: Graph +): MutableGraph => { + // Copy adjacency maps with deep cloned arrays + const adjacency = new Map>() + const reverseAdjacency = new Map>() + + for (const [nodeIndex, edges] of graph.adjacency) { + adjacency.set(nodeIndex, [...edges]) + } + + for (const [nodeIndex, edges] of graph.reverseAdjacency) { + reverseAdjacency.set(nodeIndex, [...edges]) + } + + const mutable: Mutable> = Object.create(ProtoGraph) + mutable.type = graph.type + mutable.nodes = new Map(graph.nodes) + mutable.edges = new Map(graph.edges) + mutable.adjacency = adjacency + mutable.reverseAdjacency = reverseAdjacency + mutable.nextNodeIndex = graph.nextNodeIndex + mutable.nextEdgeIndex = graph.nextEdgeIndex + mutable.isAcyclic = graph.isAcyclic + mutable.mutable = true + + return mutable +} + +/** + * Converts a mutable graph back to an immutable graph, ending the mutation scope. + * + * @example + * ```ts + * import { Graph } from "effect" + * + * const graph = Graph.directed() + * const mutable = Graph.beginMutation(graph) + * // ... perform mutations on mutable ... + * const newGraph = Graph.endMutation(mutable) + * ``` + * + * @since 3.18.0 + * @category mutations + */ +export const endMutation = ( + mutable: MutableGraph +): Graph => { + const graph: Mutable> = Object.create(ProtoGraph) + graph.type = mutable.type + graph.nodes = new Map(mutable.nodes) + graph.edges = new Map(mutable.edges) + graph.adjacency = mutable.adjacency + graph.reverseAdjacency = mutable.reverseAdjacency + graph.nextNodeIndex = mutable.nextNodeIndex + graph.nextEdgeIndex = mutable.nextEdgeIndex + graph.isAcyclic = mutable.isAcyclic + graph.mutable = false + + return graph +} + +/** + * Performs scoped mutations on a graph, automatically managing the mutation lifecycle. + * + * @example + * ```ts + * import { Graph } from "effect" + * + * const graph = Graph.directed() + * const newGraph = Graph.mutate(graph, (mutable) => { + * // Safe mutations go here + * // mutable gets automatically converted back to immutable + * }) + * ``` + * + * @since 3.18.0 + * @category mutations + */ +export const mutate: { + ( + f: (mutable: MutableGraph) => void + ): (graph: Graph) => Graph + ( + graph: Graph, + f: (mutable: MutableGraph) => void + ): Graph +} = dual(2, ( + graph: Graph, + f: (mutable: MutableGraph) => void +): Graph => { + const mutable = beginMutation(graph) + f(mutable) + return endMutation(mutable) +}) + +// ============================================================================= +// Basic Node Operations +// ============================================================================= + +/** + * Adds a new node to a mutable graph and returns its index. + * + * @example + * ```ts + * import { Graph } from "effect" + * + * const result = Graph.mutate(Graph.directed(), (mutable) => { + * const nodeA = Graph.addNode(mutable, "Node A") + * const nodeB = Graph.addNode(mutable, "Node B") + * console.log(nodeA) // NodeIndex with value 0 + * console.log(nodeB) // NodeIndex with value 1 + * }) + * ``` + * + * @since 3.18.0 + * @category mutations + */ +export const addNode = ( + mutable: MutableGraph, + data: N +): NodeIndex => { + const nodeIndex = mutable.nextNodeIndex + + // Add node data + mutable.nodes.set(nodeIndex, data) + + // Initialize empty adjacency lists + mutable.adjacency.set(nodeIndex, []) + mutable.reverseAdjacency.set(nodeIndex, []) + + // Update graph allocators + mutable.nextNodeIndex = mutable.nextNodeIndex + 1 + + return nodeIndex +} + +/** + * Gets the data associated with a node index, if it exists. + * + * @example + * ```ts + * import { Graph, Option } from "effect" + * + * const graph = Graph.mutate(Graph.directed(), (mutable) => { + * Graph.addNode(mutable, "Node A") + * }) + * + * const nodeIndex = 0 + * const nodeData = Graph.getNode(graph, nodeIndex) + * + * if (Option.isSome(nodeData)) { + * console.log(nodeData.value) // "Node A" + * } + * ``` + * + * @since 3.18.0 + * @category getters + */ +export const getNode = ( + graph: Graph | MutableGraph, + nodeIndex: NodeIndex +): Option.Option => graph.nodes.has(nodeIndex) ? Option.some(graph.nodes.get(nodeIndex)!) : Option.none() + +/** + * Checks if a node with the given index exists in the graph. + * + * @example + * ```ts + * import { Graph } from "effect" + * + * const graph = Graph.mutate(Graph.directed(), (mutable) => { + * Graph.addNode(mutable, "Node A") + * }) + * + * const nodeIndex = 0 + * const exists = Graph.hasNode(graph, nodeIndex) + * console.log(exists) // true + * + * const nonExistentIndex = 999 + * const notExists = Graph.hasNode(graph, nonExistentIndex) + * console.log(notExists) // false + * ``` + * + * @since 3.18.0 + * @category getters + */ +export const hasNode = ( + graph: Graph | MutableGraph, + nodeIndex: NodeIndex +): boolean => graph.nodes.has(nodeIndex) + +/** + * Returns the number of nodes in the graph. + * + * @example + * ```ts + * import { Graph } from "effect" + * + * const emptyGraph = Graph.directed() + * console.log(Graph.nodeCount(emptyGraph)) // 0 + * + * const graphWithNodes = Graph.mutate(emptyGraph, (mutable) => { + * Graph.addNode(mutable, "Node A") + * Graph.addNode(mutable, "Node B") + * Graph.addNode(mutable, "Node C") + * }) + * + * console.log(Graph.nodeCount(graphWithNodes)) // 3 + * ``` + * + * @since 3.18.0 + * @category getters + */ +export const nodeCount = ( + graph: Graph | MutableGraph +): number => graph.nodes.size + +/** + * Finds the first node that matches the given predicate. + * + * @example + * ```ts + * import { Graph, Option } from "effect" + * + * const graph = Graph.mutate(Graph.directed(), (mutable) => { + * Graph.addNode(mutable, "Node A") + * Graph.addNode(mutable, "Node B") + * Graph.addNode(mutable, "Node C") + * }) + * + * const result = Graph.findNode(graph, (data) => data.startsWith("Node B")) + * console.log(result) // Option.some(1) + * + * const notFound = Graph.findNode(graph, (data) => data === "Node D") + * console.log(notFound) // Option.none() + * ``` + * + * @since 3.18.0 + * @category getters + */ +export const findNode = ( + graph: Graph | MutableGraph, + predicate: (data: N) => boolean +): Option.Option => { + for (const [index, data] of graph.nodes) { + if (predicate(data)) { + return Option.some(index) + } + } + return Option.none() +} + +/** + * Finds all nodes that match the given predicate. + * + * @example + * ```ts + * import { Graph } from "effect" + * + * const graph = Graph.mutate(Graph.directed(), (mutable) => { + * Graph.addNode(mutable, "Start A") + * Graph.addNode(mutable, "Node B") + * Graph.addNode(mutable, "Start C") + * }) + * + * const result = Graph.findNodes(graph, (data) => data.startsWith("Start")) + * console.log(result) // [0, 2] + * + * const empty = Graph.findNodes(graph, (data) => data === "Not Found") + * console.log(empty) // [] + * ``` + * + * @since 3.18.0 + * @category getters + */ +export const findNodes = ( + graph: Graph | MutableGraph, + predicate: (data: N) => boolean +): Array => { + const results: Array = [] + for (const [index, data] of graph.nodes) { + if (predicate(data)) { + results.push(index) + } + } + return results +} + +/** + * Finds the first edge that matches the given predicate. + * + * @example + * ```ts + * import { Graph, Option } from "effect" + * + * const graph = Graph.mutate(Graph.directed(), (mutable) => { + * const nodeA = Graph.addNode(mutable, "Node A") + * const nodeB = Graph.addNode(mutable, "Node B") + * const nodeC = Graph.addNode(mutable, "Node C") + * Graph.addEdge(mutable, nodeA, nodeB, 10) + * Graph.addEdge(mutable, nodeB, nodeC, 20) + * }) + * + * const result = Graph.findEdge(graph, (data) => data > 15) + * console.log(result) // Option.some(1) + * + * const notFound = Graph.findEdge(graph, (data) => data > 100) + * console.log(notFound) // Option.none() + * ``` + * + * @since 3.18.0 + * @category getters + */ +export const findEdge = ( + graph: Graph | MutableGraph, + predicate: (data: E, source: NodeIndex, target: NodeIndex) => boolean +): Option.Option => { + for (const [edgeIndex, edgeData] of graph.edges) { + if (predicate(edgeData.data, edgeData.source, edgeData.target)) { + return Option.some(edgeIndex) + } + } + return Option.none() +} + +/** + * Finds all edges that match the given predicate. + * + * @example + * ```ts + * import { Graph } from "effect" + * + * const graph = Graph.mutate(Graph.directed(), (mutable) => { + * const nodeA = Graph.addNode(mutable, "Node A") + * const nodeB = Graph.addNode(mutable, "Node B") + * const nodeC = Graph.addNode(mutable, "Node C") + * Graph.addEdge(mutable, nodeA, nodeB, 10) + * Graph.addEdge(mutable, nodeB, nodeC, 20) + * Graph.addEdge(mutable, nodeC, nodeA, 30) + * }) + * + * const result = Graph.findEdges(graph, (data) => data >= 20) + * console.log(result) // [1, 2] + * + * const empty = Graph.findEdges(graph, (data) => data > 100) + * console.log(empty) // [] + * ``` + * + * @since 3.18.0 + * @category getters + */ +export const findEdges = ( + graph: Graph | MutableGraph, + predicate: (data: E, source: NodeIndex, target: NodeIndex) => boolean +): Array => { + const results: Array = [] + for (const [edgeIndex, edgeData] of graph.edges) { + if (predicate(edgeData.data, edgeData.source, edgeData.target)) { + results.push(edgeIndex) + } + } + return results +} + +/** + * Updates a single node's data by applying a transformation function. + * + * @example + * ```ts + * import { Graph } from "effect" + * + * const graph = Graph.directed((mutable) => { + * Graph.addNode(mutable, "Node A") + * Graph.addNode(mutable, "Node B") + * Graph.updateNode(mutable, 0, (data) => data.toUpperCase()) + * }) + * + * const nodeData = Graph.getNode(graph, 0) + * console.log(nodeData) // Option.some("NODE A") + * ``` + * + * @since 3.18.0 + * @category transformations + */ +export const updateNode = ( + mutable: MutableGraph, + index: NodeIndex, + f: (data: N) => N +): void => { + if (!mutable.nodes.has(index)) { + return + } + + const currentData = mutable.nodes.get(index)! + const newData = f(currentData) + mutable.nodes.set(index, newData) +} + +/** + * Updates a single edge's data by applying a transformation function. + * + * @example + * ```ts + * import { Graph } from "effect" + * + * const result = Graph.mutate(Graph.directed(), (mutable) => { + * const nodeA = Graph.addNode(mutable, "Node A") + * const nodeB = Graph.addNode(mutable, "Node B") + * const edgeIndex = Graph.addEdge(mutable, nodeA, nodeB, 10) + * Graph.updateEdge(mutable, edgeIndex, (data) => data * 2) + * }) + * + * const edgeData = Graph.getEdge(result, 0) + * console.log(edgeData) // Option.some({ source: 0, target: 1, data: 20 }) + * ``` + * + * @since 3.18.0 + * @category mutations + */ +export const updateEdge = ( + mutable: MutableGraph, + edgeIndex: EdgeIndex, + f: (data: E) => E +): void => { + if (!mutable.edges.has(edgeIndex)) { + return + } + + const currentEdge = mutable.edges.get(edgeIndex)! + const newData = f(currentEdge.data) + mutable.edges.set(edgeIndex, { + ...currentEdge, + data: newData + }) +} + +/** + * Creates a new graph with transformed node data using the provided mapping function. + * + * @example + * ```ts + * import { Graph } from "effect" + * + * const graph = Graph.directed((mutable) => { + * Graph.addNode(mutable, "node a") + * Graph.addNode(mutable, "node b") + * Graph.addNode(mutable, "node c") + * Graph.mapNodes(mutable, (data) => data.toUpperCase()) + * }) + * + * const nodeData = Graph.getNode(graph, 0) + * console.log(nodeData) // Option.some("NODE A") + * ``` + * + * @since 3.18.0 + * @category transformations + */ +export const mapNodes = ( + mutable: MutableGraph, + f: (data: N) => N +): void => { + // Transform existing node data in place + for (const [index, data] of mutable.nodes) { + const newData = f(data) + mutable.nodes.set(index, newData) + } +} + +/** + * Transforms all edge data in a mutable graph using the provided mapping function. + * + * @example + * ```ts + * import { Graph } from "effect" + * + * const graph = Graph.directed((mutable) => { + * const a = Graph.addNode(mutable, "A") + * const b = Graph.addNode(mutable, "B") + * const c = Graph.addNode(mutable, "C") + * Graph.addEdge(mutable, a, b, 10) + * Graph.addEdge(mutable, b, c, 20) + * Graph.mapEdges(mutable, (data) => data * 2) + * }) + * + * const edgeData = Graph.getEdge(graph, 0) + * console.log(edgeData) // Option.some({ source: 0, target: 1, data: 20 }) + * ``` + * + * @since 3.18.0 + * @category transformations + */ +export const mapEdges = ( + mutable: MutableGraph, + f: (data: E) => E +): void => { + // Transform existing edge data in place + for (const [index, edgeData] of mutable.edges) { + const newData = f(edgeData.data) + mutable.edges.set(index, { + ...edgeData, + data: newData + }) + } +} + +/** + * Reverses all edge directions in a mutable graph by swapping source and target nodes. + * + * @example + * ```ts + * import { Graph } from "effect" + * + * const graph = Graph.directed((mutable) => { + * const a = Graph.addNode(mutable, "A") + * const b = Graph.addNode(mutable, "B") + * const c = Graph.addNode(mutable, "C") + * Graph.addEdge(mutable, a, b, 1) // A -> B + * Graph.addEdge(mutable, b, c, 2) // B -> C + * Graph.reverse(mutable) // Now B -> A, C -> B + * }) + * + * const edge0 = Graph.getEdge(graph, 0) + * console.log(edge0) // Option.some({ source: 1, target: 0, data: 1 }) - B -> A + * ``` + * + * @since 3.18.0 + * @category transformations + */ +export const reverse = ( + mutable: MutableGraph +): void => { + // Reverse all edges by swapping source and target + for (const [index, edgeData] of mutable.edges) { + mutable.edges.set(index, { + source: edgeData.target, + target: edgeData.source, + data: edgeData.data + }) + } + + // Clear and rebuild adjacency lists with reversed directions + mutable.adjacency.clear() + mutable.reverseAdjacency.clear() + + // Rebuild adjacency lists with reversed directions + for (const [edgeIndex, edgeData] of mutable.edges) { + // Add to forward adjacency (source -> target) + const sourceEdges = mutable.adjacency.get(edgeData.source) || [] + sourceEdges.push(edgeIndex) + mutable.adjacency.set(edgeData.source, sourceEdges) + + // Add to reverse adjacency (target <- source) + const targetEdges = mutable.reverseAdjacency.get(edgeData.target) || [] + targetEdges.push(edgeIndex) + mutable.reverseAdjacency.set(edgeData.target, targetEdges) + } + + // Invalidate cycle flag since edge directions changed + mutable.isAcyclic = Option.none() +} + +/** + * Filters and optionally transforms nodes in a mutable graph using a predicate function. + * Nodes that return Option.none are removed along with all their connected edges. + * + * @example + * ```ts + * import { Graph, Option } from "effect" + * + * const graph = Graph.directed((mutable) => { + * const a = Graph.addNode(mutable, "active") + * const b = Graph.addNode(mutable, "inactive") + * const c = Graph.addNode(mutable, "active") + * Graph.addEdge(mutable, a, b, 1) + * Graph.addEdge(mutable, b, c, 2) + * + * // Keep only "active" nodes and transform to uppercase + * Graph.filterMapNodes(mutable, (data) => + * data === "active" ? Option.some(data.toUpperCase()) : Option.none() + * ) + * }) + * + * console.log(Graph.nodeCount(graph)) // 2 (only "active" nodes remain) + * ``` + * + * @since 3.18.0 + * @category transformations + */ +export const filterMapNodes = ( + mutable: MutableGraph, + f: (data: N) => Option.Option +): void => { + const nodesToRemove: Array = [] + + // First pass: identify nodes to remove and transform data for nodes to keep + for (const [index, data] of mutable.nodes) { + const result = f(data) + if (Option.isSome(result)) { + // Transform node data + mutable.nodes.set(index, result.value) + } else { + // Mark for removal + nodesToRemove.push(index) + } + } + + // Second pass: remove filtered out nodes and their edges + for (const nodeIndex of nodesToRemove) { + removeNode(mutable, nodeIndex) + } +} + +/** + * Filters and optionally transforms edges in a mutable graph using a predicate function. + * Edges that return Option.none are removed from the graph. + * + * @example + * ```ts + * import { Graph, Option } from "effect" + * + * const graph = Graph.directed((mutable) => { + * const a = Graph.addNode(mutable, "A") + * const b = Graph.addNode(mutable, "B") + * const c = Graph.addNode(mutable, "C") + * Graph.addEdge(mutable, a, b, 5) + * Graph.addEdge(mutable, b, c, 15) + * Graph.addEdge(mutable, c, a, 25) + * + * // Keep only edges with weight >= 10 and double their weight + * Graph.filterMapEdges(mutable, (data) => + * data >= 10 ? Option.some(data * 2) : Option.none() + * ) + * }) + * + * console.log(Graph.edgeCount(graph)) // 2 (edges with weight 5 removed) + * ``` + * + * @since 3.18.0 + * @category transformations + */ +export const filterMapEdges = ( + mutable: MutableGraph, + f: (data: E) => Option.Option +): void => { + const edgesToRemove: Array = [] + + // First pass: identify edges to remove and transform data for edges to keep + for (const [index, edgeData] of mutable.edges) { + const result = f(edgeData.data) + if (Option.isSome(result)) { + // Transform edge data + mutable.edges.set(index, { + ...edgeData, + data: result.value + }) + } else { + // Mark for removal + edgesToRemove.push(index) + } + } + + // Second pass: remove filtered out edges + for (const edgeIndex of edgesToRemove) { + removeEdge(mutable, edgeIndex) + } +} + +/** + * Filters nodes by removing those that don't match the predicate. + * This function modifies the mutable graph in place. + * + * @example + * ```ts + * import { Graph } from "effect" + * + * const graph = Graph.directed((mutable) => { + * Graph.addNode(mutable, "active") + * Graph.addNode(mutable, "inactive") + * Graph.addNode(mutable, "pending") + * Graph.addNode(mutable, "active") + * + * // Keep only "active" nodes + * Graph.filterNodes(mutable, (data) => data === "active") + * }) + * + * console.log(Graph.nodeCount(graph)) // 2 (only "active" nodes remain) + * ``` + * + * @since 3.18.0 + * @category transformations + */ +export const filterNodes = ( + mutable: MutableGraph, + predicate: (data: N) => boolean +): void => { + const nodesToRemove: Array = [] + + // Identify nodes to remove + for (const [index, data] of mutable.nodes) { + if (!predicate(data)) { + nodesToRemove.push(index) + } + } + + // Remove filtered out nodes (this also removes connected edges) + for (const nodeIndex of nodesToRemove) { + removeNode(mutable, nodeIndex) + } +} + +/** + * Filters edges by removing those that don't match the predicate. + * This function modifies the mutable graph in place. + * + * @example + * ```ts + * import { Graph } from "effect" + * + * const graph = Graph.directed((mutable) => { + * const a = Graph.addNode(mutable, "A") + * const b = Graph.addNode(mutable, "B") + * const c = Graph.addNode(mutable, "C") + * + * Graph.addEdge(mutable, a, b, 5) + * Graph.addEdge(mutable, b, c, 15) + * Graph.addEdge(mutable, c, a, 25) + * + * // Keep only edges with weight >= 10 + * Graph.filterEdges(mutable, (data) => data >= 10) + * }) + * + * console.log(Graph.edgeCount(graph)) // 2 (edge with weight 5 removed) + * ``` + * + * @since 3.18.0 + * @category transformations + */ +export const filterEdges = ( + mutable: MutableGraph, + predicate: (data: E) => boolean +): void => { + const edgesToRemove: Array = [] + + // Identify edges to remove + for (const [index, edgeData] of mutable.edges) { + if (!predicate(edgeData.data)) { + edgesToRemove.push(index) + } + } + + // Remove filtered out edges + for (const edgeIndex of edgesToRemove) { + removeEdge(mutable, edgeIndex) + } +} + +// ============================================================================= +// Cycle Flag Management (Internal) +// ============================================================================= + +/** @internal */ +const invalidateCycleFlagOnRemoval = ( + mutable: MutableGraph +): void => { + // Only invalidate if the graph had cycles (removing edges/nodes cannot introduce cycles in acyclic graphs) + // If already unknown (null) or acyclic (true), no need to change + if (Option.isSome(mutable.isAcyclic) && mutable.isAcyclic.value === false) { + mutable.isAcyclic = Option.none() + } +} + +/** @internal */ +const invalidateCycleFlagOnAddition = ( + mutable: MutableGraph +): void => { + // Only invalidate if the graph was acyclic (adding edges cannot remove cycles from cyclic graphs) + // If already unknown (null) or cyclic (false), no need to change + if (Option.isSome(mutable.isAcyclic) && mutable.isAcyclic.value === true) { + mutable.isAcyclic = Option.none() + } +} + +// ============================================================================= +// Edge Operations +// ============================================================================= + +/** + * Adds a new edge to a mutable graph and returns its index. + * + * @example + * ```ts + * import { Graph } from "effect" + * + * const result = Graph.mutate(Graph.directed(), (mutable) => { + * const nodeA = Graph.addNode(mutable, "Node A") + * const nodeB = Graph.addNode(mutable, "Node B") + * const edge = Graph.addEdge(mutable, nodeA, nodeB, 42) + * console.log(edge) // EdgeIndex with value 0 + * }) + * ``` + * + * @since 3.18.0 + * @category mutations + */ +export const addEdge = ( + mutable: MutableGraph, + source: NodeIndex, + target: NodeIndex, + data: E +): EdgeIndex => { + // Validate that both nodes exist + if (!mutable.nodes.has(source)) { + throw missingNode(source) + } + if (!mutable.nodes.has(target)) { + throw missingNode(target) + } + + const edgeIndex = mutable.nextEdgeIndex + + // Create edge data + const edgeData = new Edge({ source, target, data }) + mutable.edges.set(edgeIndex, edgeData) + + // Update adjacency lists + const sourceAdjacency = mutable.adjacency.get(source) + if (sourceAdjacency !== undefined) { + sourceAdjacency.push(edgeIndex) + } + + const targetReverseAdjacency = mutable.reverseAdjacency.get(target) + if (targetReverseAdjacency !== undefined) { + targetReverseAdjacency.push(edgeIndex) + } + + // For undirected graphs, add reverse connections + if (mutable.type === "undirected") { + const targetAdjacency = mutable.adjacency.get(target) + if (targetAdjacency !== undefined) { + targetAdjacency.push(edgeIndex) + } + + const sourceReverseAdjacency = mutable.reverseAdjacency.get(source) + if (sourceReverseAdjacency !== undefined) { + sourceReverseAdjacency.push(edgeIndex) + } + } + + // Update allocators + mutable.nextEdgeIndex = mutable.nextEdgeIndex + 1 + + // Only invalidate cycle flag if the graph was acyclic + // Adding edges cannot remove cycles from cyclic graphs + invalidateCycleFlagOnAddition(mutable) + + return edgeIndex +} + +/** + * Removes a node and all its incident edges from a mutable graph. + * + * @example + * ```ts + * import { Graph } from "effect" + * + * const result = Graph.mutate(Graph.directed(), (mutable) => { + * const nodeA = Graph.addNode(mutable, "Node A") + * const nodeB = Graph.addNode(mutable, "Node B") + * Graph.addEdge(mutable, nodeA, nodeB, 42) + * + * // Remove nodeA and all edges connected to it + * Graph.removeNode(mutable, nodeA) + * }) + * ``` + * + * @since 3.18.0 + * @category mutations + */ +export const removeNode = ( + mutable: MutableGraph, + nodeIndex: NodeIndex +): void => { + // Check if node exists + if (!mutable.nodes.has(nodeIndex)) { + return // Node doesn't exist, nothing to remove + } + + // Collect all incident edges for removal + const edgesToRemove: Array = [] + + // Get outgoing edges + const outgoingEdges = mutable.adjacency.get(nodeIndex) + if (outgoingEdges !== undefined) { + for (const edge of outgoingEdges) { + edgesToRemove.push(edge) + } + } + + // Get incoming edges + const incomingEdges = mutable.reverseAdjacency.get(nodeIndex) + if (incomingEdges !== undefined) { + for (const edge of incomingEdges) { + edgesToRemove.push(edge) + } + } + + // Remove all incident edges + for (const edgeIndex of edgesToRemove) { + removeEdgeInternal(mutable, edgeIndex) + } + + // Remove the node itself + mutable.nodes.delete(nodeIndex) + mutable.adjacency.delete(nodeIndex) + mutable.reverseAdjacency.delete(nodeIndex) + + // Only invalidate cycle flag if the graph wasn't already known to be acyclic + // Removing nodes cannot introduce cycles in an acyclic graph + invalidateCycleFlagOnRemoval(mutable) +} + +/** + * Removes an edge from a mutable graph. + * + * @example + * ```ts + * import { Graph } from "effect" + * + * const result = Graph.mutate(Graph.directed(), (mutable) => { + * const nodeA = Graph.addNode(mutable, "Node A") + * const nodeB = Graph.addNode(mutable, "Node B") + * const edge = Graph.addEdge(mutable, nodeA, nodeB, 42) + * + * // Remove the edge + * Graph.removeEdge(mutable, edge) + * }) + * ``` + * + * @since 3.18.0 + * @category mutations + */ +export const removeEdge = ( + mutable: MutableGraph, + edgeIndex: EdgeIndex +): void => { + const wasRemoved = removeEdgeInternal(mutable, edgeIndex) + + // Only invalidate cycle flag if an edge was actually removed + // and only if the graph wasn't already known to be acyclic + if (wasRemoved) { + invalidateCycleFlagOnRemoval(mutable) + } +} + +/** @internal */ +const removeEdgeInternal = ( + mutable: MutableGraph, + edgeIndex: EdgeIndex +): boolean => { + // Get edge data + const edge = mutable.edges.get(edgeIndex) + if (edge === undefined) { + return false // Edge doesn't exist, no mutation occurred + } + + const { source, target } = edge + + // Remove from adjacency lists + const sourceAdjacency = mutable.adjacency.get(source) + if (sourceAdjacency !== undefined) { + const index = sourceAdjacency.indexOf(edgeIndex) + if (index !== -1) { + sourceAdjacency.splice(index, 1) + } + } + + const targetReverseAdjacency = mutable.reverseAdjacency.get(target) + if (targetReverseAdjacency !== undefined) { + const index = targetReverseAdjacency.indexOf(edgeIndex) + if (index !== -1) { + targetReverseAdjacency.splice(index, 1) + } + } + + // For undirected graphs, remove reverse connections + if (mutable.type === "undirected") { + const targetAdjacency = mutable.adjacency.get(target) + if (targetAdjacency !== undefined) { + const index = targetAdjacency.indexOf(edgeIndex) + if (index !== -1) { + targetAdjacency.splice(index, 1) + } + } + + const sourceReverseAdjacency = mutable.reverseAdjacency.get(source) + if (sourceReverseAdjacency !== undefined) { + const index = sourceReverseAdjacency.indexOf(edgeIndex) + if (index !== -1) { + sourceReverseAdjacency.splice(index, 1) + } + } + } + + // Remove edge data + mutable.edges.delete(edgeIndex) + + return true // Edge was successfully removed +} + +// ============================================================================= +// Edge Query Operations +// ============================================================================= + +/** + * Gets the edge data associated with an edge index, if it exists. + * + * @example + * ```ts + * import { Graph, Option } from "effect" + * + * const graph = Graph.mutate(Graph.directed(), (mutable) => { + * const nodeA = Graph.addNode(mutable, "Node A") + * const nodeB = Graph.addNode(mutable, "Node B") + * Graph.addEdge(mutable, nodeA, nodeB, 42) + * }) + * + * const edgeIndex = 0 + * const edgeData = Graph.getEdge(graph, edgeIndex) + * + * if (Option.isSome(edgeData)) { + * console.log(edgeData.value.data) // 42 + * console.log(edgeData.value.source) // NodeIndex(0) + * console.log(edgeData.value.target) // NodeIndex(1) + * } + * ``` + * + * @since 3.18.0 + * @category getters + */ +export const getEdge = ( + graph: Graph | MutableGraph, + edgeIndex: EdgeIndex +): Option.Option> => graph.edges.has(edgeIndex) ? Option.some(graph.edges.get(edgeIndex)!) : Option.none() + +/** + * Checks if an edge exists between two nodes in the graph. + * + * @example + * ```ts + * import { Graph } from "effect" + * + * const graph = Graph.mutate(Graph.directed(), (mutable) => { + * const nodeA = Graph.addNode(mutable, "Node A") + * const nodeB = Graph.addNode(mutable, "Node B") + * const nodeC = Graph.addNode(mutable, "Node C") + * Graph.addEdge(mutable, nodeA, nodeB, 42) + * }) + * + * const nodeA = 0 + * const nodeB = 1 + * const nodeC = 2 + * + * const hasAB = Graph.hasEdge(graph, nodeA, nodeB) + * console.log(hasAB) // true + * + * const hasAC = Graph.hasEdge(graph, nodeA, nodeC) + * console.log(hasAC) // false + * ``` + * + * @since 3.18.0 + * @category getters + */ +export const hasEdge = ( + graph: Graph | MutableGraph, + source: NodeIndex, + target: NodeIndex +): boolean => { + const adjacencyList = graph.adjacency.get(source) + if (adjacencyList === undefined) { + return false + } + + // Check if any edge in the adjacency list connects to the target + for (const edgeIndex of adjacencyList) { + const edge = graph.edges.get(edgeIndex) + if (edge !== undefined && edge.target === target) { + return true + } + } + + return false +} + +/** + * Returns the number of edges in the graph. + * + * @example + * ```ts + * import { Graph } from "effect" + * + * const emptyGraph = Graph.directed() + * console.log(Graph.edgeCount(emptyGraph)) // 0 + * + * const graphWithEdges = Graph.mutate(emptyGraph, (mutable) => { + * const nodeA = Graph.addNode(mutable, "Node A") + * const nodeB = Graph.addNode(mutable, "Node B") + * const nodeC = Graph.addNode(mutable, "Node C") + * Graph.addEdge(mutable, nodeA, nodeB, 1) + * Graph.addEdge(mutable, nodeB, nodeC, 2) + * Graph.addEdge(mutable, nodeC, nodeA, 3) + * }) + * + * console.log(Graph.edgeCount(graphWithEdges)) // 3 + * ``` + * + * @since 3.18.0 + * @category getters + */ +export const edgeCount = ( + graph: Graph | MutableGraph +): number => graph.edges.size + +/** + * Returns the neighboring nodes (targets of outgoing edges) for a given node. + * + * @example + * ```ts + * import { Graph } from "effect" + * + * const graph = Graph.mutate(Graph.directed(), (mutable) => { + * const nodeA = Graph.addNode(mutable, "Node A") + * const nodeB = Graph.addNode(mutable, "Node B") + * const nodeC = Graph.addNode(mutable, "Node C") + * Graph.addEdge(mutable, nodeA, nodeB, 1) + * Graph.addEdge(mutable, nodeA, nodeC, 2) + * }) + * + * const nodeA = 0 + * const nodeB = 1 + * const nodeC = 2 + * + * const neighborsA = Graph.neighbors(graph, nodeA) + * console.log(neighborsA) // [NodeIndex(1), NodeIndex(2)] + * + * const neighborsB = Graph.neighbors(graph, nodeB) + * console.log(neighborsB) // [] + * ``` + * + * @since 3.18.0 + * @category getters + */ +export const neighbors = ( + graph: Graph | MutableGraph, + nodeIndex: NodeIndex +): Array => { + // For undirected graphs, use the specialized helper that returns the other endpoint + if (graph.type === "undirected") { + return getUndirectedNeighbors(graph as any, nodeIndex) + } + + const adjacencyList = graph.adjacency.get(nodeIndex) + if (adjacencyList === undefined) { + return [] + } + + const result: Array = [] + for (const edgeIndex of adjacencyList) { + const edge = graph.edges.get(edgeIndex) + if (edge !== undefined) { + result.push(edge.target) + } + } + + return result +} + +/** + * Get neighbors of a node in a specific direction for bidirectional traversal. + * + * @example + * ```ts + * import { Graph } from "effect" + * + * const graph = Graph.directed((mutable) => { + * const a = Graph.addNode(mutable, "A") + * const b = Graph.addNode(mutable, "B") + * Graph.addEdge(mutable, a, b, "A->B") + * }) + * + * const nodeA = 0 + * const nodeB = 1 + * + * // Get outgoing neighbors (nodes that nodeA points to) + * const outgoing = Graph.neighborsDirected(graph, nodeA, "outgoing") + * + * // Get incoming neighbors (nodes that point to nodeB) + * const incoming = Graph.neighborsDirected(graph, nodeB, "incoming") + * ``` + * + * @since 3.18.0 + * @category queries + */ +export const neighborsDirected = ( + graph: Graph | MutableGraph, + nodeIndex: NodeIndex, + direction: Direction +): Array => { + const adjacencyMap = direction === "incoming" + ? graph.reverseAdjacency + : graph.adjacency + + const adjacencyList = adjacencyMap.get(nodeIndex) + if (adjacencyList === undefined) { + return [] + } + + const result: Array = [] + for (const edgeIndex of adjacencyList) { + const edge = graph.edges.get(edgeIndex) + if (edge !== undefined) { + // For incoming direction, we want the source node instead of target + const neighborNode = direction === "incoming" + ? edge.source + : edge.target + result.push(neighborNode) + } + } + + return result +} + +// ============================================================================= +// GraphViz Export +// ============================================================================= + +/** + * Configuration options for GraphViz DOT format generation from graphs. + * + * @since 3.18.0 + * @category models + */ +export interface GraphVizOptions { + readonly nodeLabel?: (data: N) => string + readonly edgeLabel?: (data: E) => string + readonly graphName?: string +} + +/** + * Exports a graph to GraphViz DOT format for visualization. + * + * @example + * ```ts + * import { Graph } from "effect" + * + * const graph = Graph.mutate(Graph.directed(), (mutable) => { + * const nodeA = Graph.addNode(mutable, "Node A") + * const nodeB = Graph.addNode(mutable, "Node B") + * const nodeC = Graph.addNode(mutable, "Node C") + * Graph.addEdge(mutable, nodeA, nodeB, 1) + * Graph.addEdge(mutable, nodeB, nodeC, 2) + * Graph.addEdge(mutable, nodeC, nodeA, 3) + * }) + * + * const dot = Graph.toGraphViz(graph) + * console.log(dot) + * // digraph G { + * // "0" [label="Node A"]; + * // "1" [label="Node B"]; + * // "2" [label="Node C"]; + * // "0" -> "1" [label="1"]; + * // "1" -> "2" [label="2"]; + * // "2" -> "0" [label="3"]; + * // } + * ``` + * + * @since 3.18.0 + * @category utils + */ +export const toGraphViz = ( + graph: Graph | MutableGraph, + options?: GraphVizOptions +): string => { + const { + edgeLabel = (data: E) => String(data), + graphName = "G", + nodeLabel = (data: N) => String(data) + } = options ?? {} + + const isDirected = graph.type === "directed" + const graphType = isDirected ? "digraph" : "graph" + const edgeOperator = isDirected ? "->" : "--" + + const lines: Array = [] + lines.push(`${graphType} ${graphName} {`) + + // Add nodes + for (const [nodeIndex, nodeData] of graph.nodes) { + const label = nodeLabel(nodeData).replace(/"/g, "\\\"") + lines.push(` "${nodeIndex}" [label="${label}"];`) + } + + // Add edges + for (const [, edgeData] of graph.edges) { + const label = edgeLabel(edgeData.data).replace(/"/g, "\\\"") + lines.push(` "${edgeData.source}" ${edgeOperator} "${edgeData.target}" [label="${label}"];`) + } + + lines.push("}") + return lines.join("\n") +} + +// ============================================================================= +// Mermaid Export +// ============================================================================= + +/** + * Mermaid node shape types. + * + * @since 3.18.0 + * @category models + */ +export type MermaidNodeShape = + | "rectangle" + | "rounded" + | "circle" + | "diamond" + | "hexagon" + | "stadium" + | "subroutine" + | "cylindrical" + +/** + * Mermaid diagram direction types. + * + * @since 3.18.0 + * @category models + */ +export type MermaidDirection = "TB" | "TD" | "BT" | "LR" | "RL" + +/** + * Mermaid diagram type. + * + * @since 3.18.0 + * @category models + */ +export type MermaidDiagramType = "flowchart" | "graph" + +/** + * Configuration options for Mermaid diagram generation. + * + * @since 3.18.0 + * @category models + */ +export interface MermaidOptions { + readonly nodeLabel?: (data: N) => string + readonly edgeLabel?: (data: E) => string + readonly diagramType?: MermaidDiagramType + readonly direction?: MermaidDirection + readonly nodeShape?: (data: N) => MermaidNodeShape +} + +/** @internal */ +const escapeMermaidLabel = (label: string): string => { + // Escape special characters for Mermaid using HTML entity codes + // According to: https://mermaid.js.org/syntax/flowchart.html#special-characters-that-break-syntax + return label + .replace(/#/g, "#35;") + .replace(/"/g, "#quot;") + .replace(//g, "#gt;") + .replace(/&/g, "#amp;") + .replace(/\[/g, "#91;") + .replace(/\]/g, "#93;") + .replace(/\{/g, "#123;") + .replace(/\}/g, "#125;") + .replace(/\(/g, "#40;") + .replace(/\)/g, "#41;") + .replace(/\|/g, "#124;") + .replace(/\\/g, "#92;") + .replace(/\n/g, "
") +} + +/** @internal */ +const formatMermaidNode = (nodeId: string, label: string, shape: MermaidNodeShape): string => { + switch (shape) { + case "rectangle": + return `${nodeId}["${label}"]` + case "rounded": + return `${nodeId}("${label}")` + case "circle": + return `${nodeId}(("${label}"))` + case "diamond": + return `${nodeId}{"${label}"}` + case "hexagon": + return `${nodeId}{{"${label}"}}` + case "stadium": + return `${nodeId}(["${label}"])` + case "subroutine": + return `${nodeId}[["${label}"]]` + case "cylindrical": + return `${nodeId}[("${label}")]` + } +} + +/** + * Exports a graph to Mermaid diagram format for visualization. + * + * @example + * ```ts + * import { Graph } from "effect" + * + * const graph = Graph.mutate(Graph.directed(), (mutable) => { + * const app = Graph.addNode(mutable, "App") + * const db = Graph.addNode(mutable, "Database") + * const cache = Graph.addNode(mutable, "Cache") + * Graph.addEdge(mutable, app, db, 1) + * Graph.addEdge(mutable, app, cache, 2) + * }) + * + * const mermaid = Graph.toMermaid(graph) + * console.log(mermaid) + * // flowchart TD + * // 0["App"] + * // 1["Database"] + * // 2["Cache"] + * // 0 -->|"1"| 1 + * // 0 -->|"2"| 2 + * ``` + * + * @since 3.18.0 + * @category utils + */ +export const toMermaid = ( + graph: Graph | MutableGraph, + options?: MermaidOptions +): string => { + // Extract and validate options with defaults + const { + diagramType, + direction = "TD", + edgeLabel = (data: E) => String(data), + nodeLabel = (data: N) => String(data), + nodeShape = () => "rectangle" as const + } = options ?? {} + + // Auto-detect diagram type if not specified + const finalDiagramType = diagramType ?? + (graph.type === "directed" ? "flowchart" : "graph") + + // Generate diagram header + const lines: Array = [] + lines.push(`${finalDiagramType} ${direction}`) + + // Add nodes + for (const [nodeIndex, nodeData] of graph.nodes) { + const nodeId = String(nodeIndex) + const label = escapeMermaidLabel(nodeLabel(nodeData)) + const shape = nodeShape(nodeData) + const formattedNode = formatMermaidNode(nodeId, label, shape) + lines.push(` ${formattedNode}`) + } + + // Add edges + const edgeOperator = finalDiagramType === "flowchart" ? "-->" : "---" + for (const [, edgeData] of graph.edges) { + const sourceId = String(edgeData.source) + const targetId = String(edgeData.target) + const label = escapeMermaidLabel(edgeLabel(edgeData.data)) + + if (label) { + lines.push(` ${sourceId} ${edgeOperator}|"${label}"| ${targetId}`) + } else { + lines.push(` ${sourceId} ${edgeOperator} ${targetId}`) + } + } + + return lines.join("\n") +} + +// ============================================================================= +// Direction Types for Bidirectional Traversal +// ============================================================================= + +/** + * Direction for graph traversal, indicating which edges to follow. + * + * @example + * ```ts + * import { Graph } from "effect" + * + * const graph = Graph.directed((mutable) => { + * const a = Graph.addNode(mutable, "A") + * const b = Graph.addNode(mutable, "B") + * Graph.addEdge(mutable, a, b, "A->B") + * }) + * + * // Follow outgoing edges (normal direction) + * const outgoingNodes = Array.from(Graph.indices(Graph.dfs(graph, { start: [0], direction: "outgoing" }))) + * + * // Follow incoming edges (reverse direction) + * const incomingNodes = Array.from(Graph.indices(Graph.dfs(graph, { start: [1], direction: "incoming" }))) + * ``` + * + * @since 3.18.0 + * @category models + */ +export type Direction = "outgoing" | "incoming" + +// ============================================================================= + +// ============================================================================= +// Graph Structure Analysis Algorithms (Phase 5A) +// ============================================================================= + +/** + * Checks if the graph is acyclic (contains no cycles). + * + * Uses depth-first search to detect back edges, which indicate cycles. + * For directed graphs, any back edge creates a cycle. For undirected graphs, + * a back edge that doesn't go to the immediate parent creates a cycle. + * + * @example + * ```ts + * import { Graph } from "effect" + * + * // Acyclic directed graph (DAG) + * const dag = Graph.directed((mutable) => { + * const a = Graph.addNode(mutable, "A") + * const b = Graph.addNode(mutable, "B") + * const c = Graph.addNode(mutable, "C") + * Graph.addEdge(mutable, a, b, "A->B") + * Graph.addEdge(mutable, b, c, "B->C") + * }) + * console.log(Graph.isAcyclic(dag)) // true + * + * // Cyclic directed graph + * const cyclic = Graph.directed((mutable) => { + * const a = Graph.addNode(mutable, "A") + * const b = Graph.addNode(mutable, "B") + * Graph.addEdge(mutable, a, b, "A->B") + * Graph.addEdge(mutable, b, a, "B->A") // Creates cycle + * }) + * console.log(Graph.isAcyclic(cyclic)) // false + * ``` + * + * @since 3.18.0 + * @category algorithms + */ +export const isAcyclic = ( + graph: Graph | MutableGraph +): boolean => { + // Use existing cycle flag if available + if (Option.isSome(graph.isAcyclic)) { + return graph.isAcyclic.value + } + + // Stack-safe DFS cycle detection using iterative approach + const visited = new Set() + const recursionStack = new Set() + + // Stack entry: [node, neighbors, neighborIndex, isFirstVisit] + type DfsStackEntry = [NodeIndex, Array, number, boolean] + + // Get all nodes to handle disconnected components + for (const startNode of graph.nodes.keys()) { + if (visited.has(startNode)) { + continue // Already processed this component + } + + // Iterative DFS with explicit stack + const stack: Array = [[startNode, [], 0, true]] + + while (stack.length > 0) { + const [node, neighbors, neighborIndex, isFirstVisit] = stack[stack.length - 1] + + // First visit to this node + if (isFirstVisit) { + if (recursionStack.has(node)) { + // Back edge found - cycle detected + graph.isAcyclic = Option.some(false) + return false + } + + if (visited.has(node)) { + stack.pop() + continue + } + + visited.add(node) + recursionStack.add(node) + + // Get neighbors for this node + const nodeNeighbors = Array.from(neighborsDirected(graph, node, "outgoing")) + stack[stack.length - 1] = [node, nodeNeighbors, 0, false] + continue + } + + // Process next neighbor + if (neighborIndex < neighbors.length) { + const neighbor = neighbors[neighborIndex] + stack[stack.length - 1] = [node, neighbors, neighborIndex + 1, false] + + if (recursionStack.has(neighbor)) { + // Back edge found - cycle detected + graph.isAcyclic = Option.some(false) + return false + } + + if (!visited.has(neighbor)) { + stack.push([neighbor, [], 0, true]) + } + } else { + // Done with this node - backtrack + recursionStack.delete(node) + stack.pop() + } + } + } + + // Cache the result + graph.isAcyclic = Option.some(true) + return true +} + +/** + * Checks if an undirected graph is bipartite. + * + * A bipartite graph is one whose vertices can be divided into two disjoint sets + * such that no two vertices within the same set are adjacent. Uses BFS coloring + * to determine bipartiteness. + * + * @example + * ```ts + * import { Graph } from "effect" + * + * // Bipartite graph (alternating coloring possible) + * const bipartite = Graph.undirected((mutable) => { + * const a = Graph.addNode(mutable, "A") + * const b = Graph.addNode(mutable, "B") + * const c = Graph.addNode(mutable, "C") + * const d = Graph.addNode(mutable, "D") + * Graph.addEdge(mutable, a, b, "edge") // Set 1: {A, C}, Set 2: {B, D} + * Graph.addEdge(mutable, b, c, "edge") + * Graph.addEdge(mutable, c, d, "edge") + * }) + * console.log(Graph.isBipartite(bipartite)) // true + * + * // Non-bipartite graph (odd cycle) + * const triangle = Graph.undirected((mutable) => { + * const a = Graph.addNode(mutable, "A") + * const b = Graph.addNode(mutable, "B") + * const c = Graph.addNode(mutable, "C") + * Graph.addEdge(mutable, a, b, "edge") + * Graph.addEdge(mutable, b, c, "edge") + * Graph.addEdge(mutable, c, a, "edge") // Triangle (3-cycle) + * }) + * console.log(Graph.isBipartite(triangle)) // false + * ``` + * + * @since 3.18.0 + * @category algorithms + */ +export const isBipartite = ( + graph: Graph | MutableGraph +): boolean => { + const coloring = new Map() + const discovered = new Set() + let isBipartiteGraph = true + + // Get all nodes to handle disconnected components + for (const startNode of graph.nodes.keys()) { + if (!discovered.has(startNode)) { + // Start BFS coloring from this component + const queue: Array = [startNode] + coloring.set(startNode, 0) // Color start node with 0 + discovered.add(startNode) + + while (queue.length > 0 && isBipartiteGraph) { + const current = queue.shift()! + const currentColor = coloring.get(current)! + const neighborColor: 0 | 1 = currentColor === 0 ? 1 : 0 + + // Get all neighbors for undirected graph + const nodeNeighbors = getUndirectedNeighbors(graph, current) + for (const neighbor of nodeNeighbors) { + if (!discovered.has(neighbor)) { + // Color unvisited neighbor with opposite color + coloring.set(neighbor, neighborColor) + discovered.add(neighbor) + queue.push(neighbor) + } else { + // Check if neighbor has the same color (conflict) + if (coloring.get(neighbor) === currentColor) { + isBipartiteGraph = false + break + } + } + } + } + + // Early exit if not bipartite + if (!isBipartiteGraph) { + break + } + } + } + + return isBipartiteGraph +} + +/** + * Get neighbors for undirected graphs by checking both adjacency and reverse adjacency. + * For undirected graphs, we need to find the other endpoint of each edge incident to the node. + */ +const getUndirectedNeighbors = ( + graph: Graph | MutableGraph, + nodeIndex: NodeIndex +): Array => { + const neighbors = new Set() + + // Check edges where this node is the source + const adjacencyList = graph.adjacency.get(nodeIndex) + if (adjacencyList !== undefined) { + for (const edgeIndex of adjacencyList) { + const edge = graph.edges.get(edgeIndex) + if (edge !== undefined) { + // For undirected graphs, the neighbor is the other endpoint + const otherNode = edge.source === nodeIndex ? edge.target : edge.source + neighbors.add(otherNode) + } + } + } + + return Array.from(neighbors) +} + +/** + * Find connected components in an undirected graph. + * Each component is represented as an array of node indices. + * + * @example + * ```ts + * import { Graph } from "effect" + * + * const graph = Graph.undirected((mutable) => { + * const a = Graph.addNode(mutable, "A") + * const b = Graph.addNode(mutable, "B") + * const c = Graph.addNode(mutable, "C") + * const d = Graph.addNode(mutable, "D") + * Graph.addEdge(mutable, a, b, "edge") // Component 1: A-B + * Graph.addEdge(mutable, c, d, "edge") // Component 2: C-D + * }) + * + * const components = Graph.connectedComponents(graph) + * console.log(components) // [[0, 1], [2, 3]] + * ``` + * + * @since 3.18.0 + * @category algorithms + */ +export const connectedComponents = ( + graph: Graph | MutableGraph +): Array> => { + const visited = new Set() + const components: Array> = [] + for (const startNode of graph.nodes.keys()) { + if (!visited.has(startNode)) { + // DFS to find all nodes in this component + const component: Array = [] + const stack: Array = [startNode] + + while (stack.length > 0) { + const current = stack.pop()! + if (!visited.has(current)) { + visited.add(current) + component.push(current) + + // Add all unvisited neighbors to stack + const nodeNeighbors = getUndirectedNeighbors(graph, current) + for (const neighbor of nodeNeighbors) { + if (!visited.has(neighbor)) { + stack.push(neighbor) + } + } + } + } + + components.push(component) + } + } + + return components +} + +/** + * Find strongly connected components in a directed graph using Kosaraju's algorithm. + * Each SCC is represented as an array of node indices. + * + * @example + * ```ts + * import { Graph } from "effect" + * + * const graph = Graph.directed((mutable) => { + * const a = Graph.addNode(mutable, "A") + * const b = Graph.addNode(mutable, "B") + * const c = Graph.addNode(mutable, "C") + * Graph.addEdge(mutable, a, b, "A->B") + * Graph.addEdge(mutable, b, c, "B->C") + * Graph.addEdge(mutable, c, a, "C->A") // Creates SCC: A-B-C + * }) + * + * const sccs = Graph.stronglyConnectedComponents(graph) + * console.log(sccs) // [[0, 1, 2]] + * ``` + * + * @since 3.18.0 + * @category algorithms + */ +export const stronglyConnectedComponents = ( + graph: Graph | MutableGraph +): Array> => { + const visited = new Set() + const finishOrder: Array = [] + // Iterate directly over node keys + + // Step 1: Stack-safe DFS on original graph to get finish times + // Stack entry: [node, neighbors, neighborIndex, isFirstVisit] + type DfsStackEntry = [NodeIndex, Array, number, boolean] + + for (const startNode of graph.nodes.keys()) { + if (visited.has(startNode)) { + continue + } + + const stack: Array = [[startNode, [], 0, true]] + + while (stack.length > 0) { + const [node, nodeNeighbors, neighborIndex, isFirstVisit] = stack[stack.length - 1] + + if (isFirstVisit) { + if (visited.has(node)) { + stack.pop() + continue + } + + visited.add(node) + const nodeNeighborsList = neighbors(graph, node) + stack[stack.length - 1] = [node, nodeNeighborsList, 0, false] + continue + } + + // Process next neighbor + if (neighborIndex < nodeNeighbors.length) { + const neighbor = nodeNeighbors[neighborIndex] + stack[stack.length - 1] = [node, nodeNeighbors, neighborIndex + 1, false] + + if (!visited.has(neighbor)) { + stack.push([neighbor, [], 0, true]) + } + } else { + // Done with this node - add to finish order (post-order) + finishOrder.push(node) + stack.pop() + } + } + } + + // Step 2: Stack-safe DFS on transpose graph in reverse finish order + visited.clear() + const sccs: Array> = [] + + for (let i = finishOrder.length - 1; i >= 0; i--) { + const startNode = finishOrder[i] + if (visited.has(startNode)) { + continue + } + + const scc: Array = [] + const stack: Array = [startNode] + + while (stack.length > 0) { + const node = stack.pop()! + + if (visited.has(node)) { + continue + } + + visited.add(node) + scc.push(node) + + // Use reverse adjacency (transpose graph) + const reverseAdjacency = graph.reverseAdjacency.get(node) + if (reverseAdjacency !== undefined) { + for (const edgeIndex of reverseAdjacency) { + const edge = graph.edges.get(edgeIndex) + if (edge !== undefined) { + const predecessor = edge.source + if (!visited.has(predecessor)) { + stack.push(predecessor) + } + } + } + } + } + + sccs.push(scc) + } + + return sccs +} + +// ============================================================================= +// Path Finding Algorithms (Phase 5B) +// ============================================================================= + +/** + * Result of a shortest path computation containing the path and total distance. + * + * @since 3.18.0 + * @category models + */ +export interface PathResult { + readonly path: Array + readonly distance: number + readonly costs: Array +} + +/** + * Configuration for Dijkstra's algorithm. + * + * @since 3.18.0 + * @category models + */ +export interface DijkstraConfig { + source: NodeIndex + target: NodeIndex + cost: (edgeData: E) => number +} + +/** + * Configuration for A* algorithm. + * + * @since 3.18.0 + * @category models + */ +export interface AstarConfig { + source: NodeIndex + target: NodeIndex + cost: (edgeData: E) => number + heuristic: (sourceNodeData: N, targetNodeData: N) => number +} + +/** + * Configuration for Bellman-Ford algorithm. + * + * @since 3.18.0 + * @category models + */ +export interface BellmanFordConfig { + source: NodeIndex + target: NodeIndex + cost: (edgeData: E) => number +} + +/** + * Find the shortest path between two nodes using Dijkstra's algorithm. + * + * Dijkstra's algorithm works with non-negative edge weights and finds the shortest + * path from a source node to a target node in O((V + E) log V) time complexity. + * + * @example + * ```ts + * import { Graph, Option } from "effect" + * + * const graph = Graph.directed((mutable) => { + * const a = Graph.addNode(mutable, "A") + * const b = Graph.addNode(mutable, "B") + * const c = Graph.addNode(mutable, "C") + * Graph.addEdge(mutable, a, b, 5) + * Graph.addEdge(mutable, a, c, 10) + * Graph.addEdge(mutable, b, c, 2) + * }) + * + * const result = Graph.dijkstra(graph, { source: 0, target: 2, cost: (edgeData) => edgeData }) + * if (Option.isSome(result)) { + * console.log(result.value.path) // [0, 1, 2] - shortest path A->B->C + * console.log(result.value.distance) // 7 - total distance + * } + * ``` + * + * @since 3.18.0 + * @category algorithms + */ +export const dijkstra = ( + graph: Graph | MutableGraph, + config: DijkstraConfig +): Option.Option> => { + const { cost, source, target } = config + // Validate that source and target nodes exist + if (!graph.nodes.has(source)) { + throw missingNode(source) + } + if (!graph.nodes.has(target)) { + throw missingNode(target) + } + + // Early return if source equals target + if (source === target) { + return Option.some({ + path: [source], + distance: 0, + costs: [] + }) + } + + // Distance tracking and priority queue simulation + const distances = new Map() + const previous = new Map() + const visited = new Set() + + // Initialize distances + // Iterate directly over node keys + for (const node of graph.nodes.keys()) { + distances.set(node, node === source ? 0 : Infinity) + previous.set(node, null) + } + + // Simple priority queue using array (can be optimized with proper heap) + const priorityQueue: Array<{ node: NodeIndex; distance: number }> = [ + { node: source, distance: 0 } + ] + + while (priorityQueue.length > 0) { + // Find minimum distance node (priority queue extract-min) + let minIndex = 0 + for (let i = 1; i < priorityQueue.length; i++) { + if (priorityQueue[i].distance < priorityQueue[minIndex].distance) { + minIndex = i + } + } + + const current = priorityQueue.splice(minIndex, 1)[0] + const currentNode = current.node + + // Skip if already visited (can happen with duplicate entries) + if (visited.has(currentNode)) { + continue + } + + visited.add(currentNode) + + // Early termination if we reached the target + if (currentNode === target) { + break + } + + // Get current distance + const currentDistance = distances.get(currentNode)! + + // Examine all outgoing edges + const adjacencyList = graph.adjacency.get(currentNode) + if (adjacencyList !== undefined) { + for (const edgeIndex of adjacencyList) { + const edge = graph.edges.get(edgeIndex) + if (edge !== undefined) { + const neighbor = edge.target + const weight = cost(edge.data) + + // Validate non-negative weights + if (weight < 0) { + throw new Error(`Dijkstra's algorithm requires non-negative edge weights, found ${weight}`) + } + + const newDistance = currentDistance + weight + const neighborDistance = distances.get(neighbor)! + + // Relaxation step + if (newDistance < neighborDistance) { + distances.set(neighbor, newDistance) + previous.set(neighbor, { node: currentNode, edgeData: edge.data }) + + // Add to priority queue if not visited + if (!visited.has(neighbor)) { + priorityQueue.push({ node: neighbor, distance: newDistance }) + } + } + } + } + } + } + + // Check if target is reachable + const targetDistance = distances.get(target)! + if (targetDistance === Infinity) { + return Option.none() // No path exists + } + + // Reconstruct path + const path: Array = [] + const costs: Array = [] + let currentNode: NodeIndex | null = target + + while (currentNode !== null) { + path.unshift(currentNode) + const prev: { node: NodeIndex; edgeData: E } | null = previous.get(currentNode)! + if (prev !== null) { + costs.unshift(prev.edgeData) + currentNode = prev.node + } else { + currentNode = null + } + } + + return Option.some({ + path, + distance: targetDistance, + costs + }) +} + +/** + * Result of all-pairs shortest path computation. + * + * @since 3.18.0 + * @category models + */ +export interface AllPairsResult { + readonly distances: Map> + readonly paths: Map | null>> + readonly costs: Map>> +} + +/** + * Find shortest paths between all pairs of nodes using Floyd-Warshall algorithm. + * + * Floyd-Warshall algorithm computes shortest paths between all pairs of nodes in O(V³) time. + * It can handle negative edge weights and detect negative cycles. + * + * @example + * ```ts + * import { Graph } from "effect" + * + * const graph = Graph.directed((mutable) => { + * const a = Graph.addNode(mutable, "A") + * const b = Graph.addNode(mutable, "B") + * const c = Graph.addNode(mutable, "C") + * Graph.addEdge(mutable, a, b, 3) + * Graph.addEdge(mutable, b, c, 2) + * Graph.addEdge(mutable, a, c, 7) + * }) + * + * const result = Graph.floydWarshall(graph, (edgeData) => edgeData) + * const distanceAToC = result.distances.get(0)?.get(2) // 5 (A->B->C) + * const pathAToC = result.paths.get(0)?.get(2) // [0, 1, 2] + * ``` + * + * @since 3.18.0 + * @category algorithms + */ +export const floydWarshall = ( + graph: Graph | MutableGraph, + cost: (edgeData: E) => number +): AllPairsResult => { + // Get all nodes for Floyd-Warshall algorithm (needs array for nested iteration) + const allNodes = Array.from(graph.nodes.keys()) + + // Initialize distance matrix + const dist = new Map>() + const next = new Map>() + const edgeMatrix = new Map>() + + // Initialize with infinity for all pairs + for (const i of allNodes) { + dist.set(i, new Map()) + next.set(i, new Map()) + edgeMatrix.set(i, new Map()) + + for (const j of allNodes) { + dist.get(i)!.set(j, i === j ? 0 : Infinity) + next.get(i)!.set(j, null) + edgeMatrix.get(i)!.set(j, null) + } + } + + // Set edge weights + for (const [, edgeData] of graph.edges) { + const weight = cost(edgeData.data) + const i = edgeData.source + const j = edgeData.target + + // Use minimum weight if multiple edges exist + const currentWeight = dist.get(i)!.get(j)! + if (weight < currentWeight) { + dist.get(i)!.set(j, weight) + next.get(i)!.set(j, j) + edgeMatrix.get(i)!.set(j, edgeData.data) + } + } + + // Floyd-Warshall main loop + for (const k of allNodes) { + for (const i of allNodes) { + for (const j of allNodes) { + const distIK = dist.get(i)!.get(k)! + const distKJ = dist.get(k)!.get(j)! + const distIJ = dist.get(i)!.get(j)! + + if (distIK !== Infinity && distKJ !== Infinity && distIK + distKJ < distIJ) { + dist.get(i)!.set(j, distIK + distKJ) + next.get(i)!.set(j, next.get(i)!.get(k)!) + } + } + } + } + + // Check for negative cycles + for (const i of allNodes) { + if (dist.get(i)!.get(i)! < 0) { + throw new Error(`Negative cycle detected involving node ${i}`) + } + } + + // Build result paths and edge weights + const paths = new Map | null>>() + const resultCosts = new Map>>() + + for (const i of allNodes) { + paths.set(i, new Map()) + resultCosts.set(i, new Map()) + + for (const j of allNodes) { + if (i === j) { + paths.get(i)!.set(j, [i]) + resultCosts.get(i)!.set(j, []) + } else if (dist.get(i)!.get(j)! === Infinity) { + paths.get(i)!.set(j, null) + resultCosts.get(i)!.set(j, []) + } else { + // Reconstruct path iteratively + const path: Array = [] + const weights: Array = [] + let current = i + + path.push(current) + while (current !== j) { + const nextNode = next.get(current)!.get(j)! + if (nextNode === null) break + + const edgeData = edgeMatrix.get(current)!.get(nextNode)! + if (edgeData !== null) { + weights.push(edgeData) + } + + current = nextNode + path.push(current) + } + + paths.get(i)!.set(j, path) + resultCosts.get(i)!.set(j, weights) + } + } + } + + return { + distances: dist, + paths, + costs: resultCosts + } +} + +/** + * Find the shortest path between two nodes using A* pathfinding algorithm. + * + * A* is an extension of Dijkstra's algorithm that uses a heuristic function to guide + * the search towards the target, potentially finding paths faster than Dijkstra's. + * The heuristic must be admissible (never overestimate the actual cost). + * + * @example + * ```ts + * import { Graph, Option } from "effect" + * + * const graph = Graph.directed<{x: number, y: number}, number>((mutable) => { + * const a = Graph.addNode(mutable, {x: 0, y: 0}) + * const b = Graph.addNode(mutable, {x: 1, y: 0}) + * const c = Graph.addNode(mutable, {x: 2, y: 0}) + * Graph.addEdge(mutable, a, b, 1) + * Graph.addEdge(mutable, b, c, 1) + * }) + * + * // Manhattan distance heuristic + * const heuristic = (nodeData: {x: number, y: number}, targetData: {x: number, y: number}) => + * Math.abs(nodeData.x - targetData.x) + Math.abs(nodeData.y - targetData.y) + * + * const result = Graph.astar(graph, { source: 0, target: 2, cost: (edgeData) => edgeData, heuristic }) + * if (Option.isSome(result)) { + * console.log(result.value.path) // [0, 1, 2] - shortest path + * console.log(result.value.distance) // 2 - total distance + * } + * ``` + * + * @since 3.18.0 + * @category algorithms + */ +export const astar = ( + graph: Graph | MutableGraph, + config: AstarConfig +): Option.Option> => { + const { cost, heuristic, source, target } = config + // Validate that source and target nodes exist + if (!graph.nodes.has(source)) { + throw missingNode(source) + } + if (!graph.nodes.has(target)) { + throw missingNode(target) + } + + // Early return if source equals target + if (source === target) { + return Option.some({ + path: [source], + distance: 0, + costs: [] + }) + } + + // Get target node data for heuristic calculations + const targetNodeData = graph.nodes.get(target) + if (targetNodeData === undefined) { + throw new Error(`Target node ${target} data not found`) + } + + // Distance tracking (g-score) and f-score (g + h) + const gScore = new Map() + const fScore = new Map() + const previous = new Map() + const visited = new Set() + + // Initialize scores + // Iterate directly over node keys + for (const node of graph.nodes.keys()) { + gScore.set(node, node === source ? 0 : Infinity) + fScore.set(node, Infinity) + previous.set(node, null) + } + + // Calculate initial f-score for source + const sourceNodeData = graph.nodes.get(source) + if (sourceNodeData !== undefined) { + const h = heuristic(sourceNodeData, targetNodeData) + fScore.set(source, h) + } + + // Priority queue using f-score (total estimated cost) + const openSet: Array<{ node: NodeIndex; fScore: number }> = [ + { node: source, fScore: fScore.get(source)! } + ] + + while (openSet.length > 0) { + // Find node with lowest f-score + let minIndex = 0 + for (let i = 1; i < openSet.length; i++) { + if (openSet[i].fScore < openSet[minIndex].fScore) { + minIndex = i + } + } + + const current = openSet.splice(minIndex, 1)[0] + const currentNode = current.node + + // Skip if already visited + if (visited.has(currentNode)) { + continue + } + + visited.add(currentNode) + + // Early termination if we reached the target + if (currentNode === target) { + break + } + + // Get current g-score + const currentGScore = gScore.get(currentNode)! + + // Examine all outgoing edges + const adjacencyList = graph.adjacency.get(currentNode) + if (adjacencyList !== undefined) { + for (const edgeIndex of adjacencyList) { + const edge = graph.edges.get(edgeIndex) + if (edge !== undefined) { + const neighbor = edge.target + const weight = cost(edge.data) + + // Validate non-negative weights + if (weight < 0) { + throw new Error(`A* algorithm requires non-negative edge weights, found ${weight}`) + } + + const tentativeGScore = currentGScore + weight + const neighborGScore = gScore.get(neighbor)! + + // If this path to neighbor is better than any previous one + if (tentativeGScore < neighborGScore) { + // Update g-score and previous + gScore.set(neighbor, tentativeGScore) + previous.set(neighbor, { node: currentNode, edgeData: edge.data }) + + // Calculate f-score using heuristic + const neighborNodeData = graph.nodes.get(neighbor) + if (neighborNodeData !== undefined) { + const h = heuristic(neighborNodeData, targetNodeData) + const f = tentativeGScore + h + fScore.set(neighbor, f) + + // Add to open set if not visited + if (!visited.has(neighbor)) { + openSet.push({ node: neighbor, fScore: f }) + } + } + } + } + } + } + } + + // Check if target is reachable + const targetGScore = gScore.get(target)! + if (targetGScore === Infinity) { + return Option.none() // No path exists + } + + // Reconstruct path + const path: Array = [] + const costs: Array = [] + let currentNode: NodeIndex | null = target + + while (currentNode !== null) { + path.unshift(currentNode) + const prev: { node: NodeIndex; edgeData: E } | null = previous.get(currentNode)! + if (prev !== null) { + costs.unshift(prev.edgeData) + currentNode = prev.node + } else { + currentNode = null + } + } + + return Option.some({ + path, + distance: targetGScore, + costs + }) +} + +/** + * Find the shortest path between two nodes using Bellman-Ford algorithm. + * + * Bellman-Ford algorithm can handle negative edge weights and detects negative cycles. + * It has O(VE) time complexity, slower than Dijkstra's but more versatile. + * Returns Option.none() if a negative cycle is detected that affects the path. + * + * @example + * ```ts + * import { Graph, Option } from "effect" + * + * const graph = Graph.directed((mutable) => { + * const a = Graph.addNode(mutable, "A") + * const b = Graph.addNode(mutable, "B") + * const c = Graph.addNode(mutable, "C") + * Graph.addEdge(mutable, a, b, -1) // Negative weight allowed + * Graph.addEdge(mutable, b, c, 3) + * Graph.addEdge(mutable, a, c, 5) + * }) + * + * const result = Graph.bellmanFord(graph, { source: 0, target: 2, cost: (edgeData) => edgeData }) + * if (Option.isSome(result)) { + * console.log(result.value.path) // [0, 1, 2] - shortest path A->B->C + * console.log(result.value.distance) // 2 - total distance + * } + * ``` + * + * @since 3.18.0 + * @category algorithms + */ +export const bellmanFord = ( + graph: Graph | MutableGraph, + config: BellmanFordConfig +): Option.Option> => { + const { cost, source, target } = config + // Validate that source and target nodes exist + if (!graph.nodes.has(source)) { + throw missingNode(source) + } + if (!graph.nodes.has(target)) { + throw missingNode(target) + } + + // Early return if source equals target + if (source === target) { + return Option.some({ + path: [source], + distance: 0, + costs: [] + }) + } + + // Initialize distances and predecessors + const distances = new Map() + const previous = new Map() + // Iterate directly over node keys + + for (const node of graph.nodes.keys()) { + distances.set(node, node === source ? 0 : Infinity) + previous.set(node, null) + } + + // Collect all edges for relaxation + const edges: Array<{ source: NodeIndex; target: NodeIndex; weight: number; edgeData: E }> = [] + for (const [, edgeData] of graph.edges) { + const weight = cost(edgeData.data) + edges.push({ + source: edgeData.source, + target: edgeData.target, + weight, + edgeData: edgeData.data + }) + } + + // Relax edges up to V-1 times + const nodeCount = graph.nodes.size + for (let i = 0; i < nodeCount - 1; i++) { + let hasUpdate = false + + for (const edge of edges) { + const sourceDistance = distances.get(edge.source)! + const targetDistance = distances.get(edge.target)! + + // Relaxation step + if (sourceDistance !== Infinity && sourceDistance + edge.weight < targetDistance) { + distances.set(edge.target, sourceDistance + edge.weight) + previous.set(edge.target, { node: edge.source, edgeData: edge.edgeData }) + hasUpdate = true + } + } + + // Early termination if no updates + if (!hasUpdate) { + break + } + } + + // Check for negative cycles + for (const edge of edges) { + const sourceDistance = distances.get(edge.source)! + const targetDistance = distances.get(edge.target)! + + if (sourceDistance !== Infinity && sourceDistance + edge.weight < targetDistance) { + // Negative cycle detected - check if it affects the path to target + const affectedNodes = new Set() + const queue = [edge.target] + + while (queue.length > 0) { + const node = queue.shift()! + if (affectedNodes.has(node)) continue + affectedNodes.add(node) + + // Add all nodes reachable from this node + const adjacencyList = graph.adjacency.get(node) + if (adjacencyList !== undefined) { + for (const edgeIndex of adjacencyList) { + const edge = graph.edges.get(edgeIndex) + if (edge !== undefined) { + queue.push(edge.target) + } + } + } + } + + // If target is affected by negative cycle, return null + if (affectedNodes.has(target)) { + return Option.none() + } + } + } + + // Check if target is reachable + const targetDistance = distances.get(target)! + if (targetDistance === Infinity) { + return Option.none() // No path exists + } + + // Reconstruct path + const path: Array = [] + const costs: Array = [] + let currentNode: NodeIndex | null = target + + while (currentNode !== null) { + path.unshift(currentNode) + const prev: { node: NodeIndex; edgeData: E } | null = previous.get(currentNode)! + if (prev !== null) { + costs.unshift(prev.edgeData) + currentNode = prev.node + } else { + currentNode = null + } + } + + return Option.some({ + path, + distance: targetDistance, + costs + }) +} + +/** + * Concrete class for iterables that produce [NodeIndex, NodeData] tuples. + * + * This class provides a common abstraction for all iterables that return node data, + * including traversal iterators (DFS, BFS, etc.) and element iterators (nodes, externals). + * It uses a mapEntry function pattern for flexible iteration and transformation. + * + * @example + * ```ts + * import { Graph } from "effect" + * + * const graph = Graph.directed((mutable) => { + * const a = Graph.addNode(mutable, "A") + * const b = Graph.addNode(mutable, "B") + * Graph.addEdge(mutable, a, b, 1) + * }) + * + * // Both traversal and element iterators return NodeWalker + * const dfsNodes: Graph.NodeWalker = Graph.dfs(graph, { start: [0] }) + * const allNodes: Graph.NodeWalker = Graph.nodes(graph) + * + * // Common interface for working with node iterables + * function processNodes(nodeIterable: Graph.NodeWalker): Array { + * return Array.from(Graph.indices(nodeIterable)) + * } + * + * // Access node data using values() or entries() + * const nodeData = Array.from(Graph.values(dfsNodes)) // ["A", "B"] + * const nodeEntries = Array.from(Graph.entries(allNodes)) // [[0, "A"], [1, "B"]] + * ``` + * + * @since 3.18.0 + * @category models + */ +export class Walker implements Iterable<[T, N]> { + /** + * @since 3.18.0 + */ + // @ts-ignore + readonly [Symbol.iterator]: () => Iterator<[T, N]> + + /** + * Visits each element and maps it to a value using the provided function. + * + * Takes a function that receives the index and data, + * and returns an iterable of the mapped values. Skips elements that + * no longer exist in the graph. + * + * @example + * ```ts + * import { Graph } from "effect" + * + * const graph = Graph.directed((mutable) => { + * const a = Graph.addNode(mutable, "A") + * const b = Graph.addNode(mutable, "B") + * Graph.addEdge(mutable, a, b, 1) + * }) + * + * const dfs = Graph.dfs(graph, { start: [0] }) + * + * // Map to just the node data + * const values = Array.from(dfs.visit((index, data) => data)) + * console.log(values) // ["A", "B"] + * + * // Map to custom objects + * const custom = Array.from(dfs.visit((index, data) => ({ id: index, name: data }))) + * console.log(custom) // [{ id: 0, name: "A" }, { id: 1, name: "B" }] + * ``` + * + * @since 3.18.0 + * @category iterators + */ + readonly visit: (f: (index: T, data: N) => U) => Iterable + + constructor( + /** + * Visits each element and maps it to a value using the provided function. + * + * Takes a function that receives the index and data, + * and returns an iterable of the mapped values. Skips elements that + * no longer exist in the graph. + * + * @example + * ```ts + * import { Graph } from "effect" + * + * const graph = Graph.directed((mutable) => { + * const a = Graph.addNode(mutable, "A") + * const b = Graph.addNode(mutable, "B") + * Graph.addEdge(mutable, a, b, 1) + * }) + * + * const dfs = Graph.dfs(graph, { start: [0] }) + * + * // Map to just the node data + * const values = Array.from(dfs.visit((index, data) => data)) + * console.log(values) // ["A", "B"] + * + * // Map to custom objects + * const custom = Array.from(dfs.visit((index, data) => ({ id: index, name: data }))) + * console.log(custom) // [{ id: 0, name: "A" }, { id: 1, name: "B" }] + * ``` + * + * @since 3.18.0 + * @category iterators + */ + visit: (f: (index: T, data: N) => U) => Iterable + ) { + this.visit = visit + this[Symbol.iterator] = visit((index, data) => [index, data] as [T, N])[Symbol.iterator] + } +} + +/** + * Type alias for node iteration using Walker. + * NodeWalker is represented as Walker. + * + * @since 3.18.0 + * @category models + */ +export type NodeWalker = Walker + +/** + * Type alias for edge iteration using Walker. + * EdgeWalker is represented as Walker>. + * + * @since 3.18.0 + * @category models + */ +export type EdgeWalker = Walker> + +/** + * Returns an iterator over the indices in the walker. + * + * @example + * ```ts + * import { Graph } from "effect" + * + * const graph = Graph.directed((mutable) => { + * const a = Graph.addNode(mutable, "A") + * const b = Graph.addNode(mutable, "B") + * Graph.addEdge(mutable, a, b, 1) + * }) + * + * const dfs = Graph.dfs(graph, { start: [0] }) + * const indices = Array.from(Graph.indices(dfs)) + * console.log(indices) // [0, 1] + * ``` + * + * @since 3.18.0 + * @category utilities + */ +export const indices = (walker: Walker): Iterable => walker.visit((index, _) => index) + +/** + * Returns an iterator over the values (data) in the walker. + * + * @example + * ```ts + * import { Graph } from "effect" + * + * const graph = Graph.directed((mutable) => { + * const a = Graph.addNode(mutable, "A") + * const b = Graph.addNode(mutable, "B") + * Graph.addEdge(mutable, a, b, 1) + * }) + * + * const dfs = Graph.dfs(graph, { start: [0] }) + * const values = Array.from(Graph.values(dfs)) + * console.log(values) // ["A", "B"] + * ``` + * + * @since 3.18.0 + * @category utilities + */ +export const values = (walker: Walker): Iterable => walker.visit((_, data) => data) + +/** + * Returns an iterator over [index, data] entries in the walker. + * + * @example + * ```ts + * import { Graph } from "effect" + * + * const graph = Graph.directed((mutable) => { + * const a = Graph.addNode(mutable, "A") + * const b = Graph.addNode(mutable, "B") + * Graph.addEdge(mutable, a, b, 1) + * }) + * + * const dfs = Graph.dfs(graph, { start: [0] }) + * const entries = Array.from(Graph.entries(dfs)) + * console.log(entries) // [[0, "A"], [1, "B"]] + * ``` + * + * @since 3.18.0 + * @category utilities + */ +export const entries = (walker: Walker): Iterable<[T, N]> => + walker.visit((index, data) => [index, data] as [T, N]) + +/** + * Configuration for graph search iterators. + * + * @since 3.18.0 + * @category models + */ +export interface SearchConfig { + readonly start?: Array + readonly direction?: Direction +} + +/** + * Creates a new DFS iterator with optional configuration. + * + * The iterator maintains a stack of nodes to visit and tracks discovered nodes. + * It provides lazy evaluation of the depth-first search. + * + * @example + * ```ts + * import { Graph } from "effect" + * + * const graph = Graph.directed((mutable) => { + * const a = Graph.addNode(mutable, "A") + * const b = Graph.addNode(mutable, "B") + * const c = Graph.addNode(mutable, "C") + * Graph.addEdge(mutable, a, b, 1) + * Graph.addEdge(mutable, b, c, 1) + * }) + * + * // Start from a specific node + * const dfs1 = Graph.dfs(graph, { start: [0] }) + * for (const nodeIndex of Graph.indices(dfs1)) { + * console.log(nodeIndex) // Traverses in DFS order: 0, 1, 2 + * } + * + * // Empty iterator (no starting nodes) + * const dfs2 = Graph.dfs(graph) + * // Can be used programmatically + * ``` + * + * @since 3.18.0 + * @category iterators + */ +export const dfs = ( + graph: Graph | MutableGraph, + config: SearchConfig = {} +): NodeWalker => { + const start = config.start ?? [] + const direction = config.direction ?? "outgoing" + + // Validate that all start nodes exist + for (const nodeIndex of start) { + if (!hasNode(graph, nodeIndex)) { + throw missingNode(nodeIndex) + } + } + + return new Walker((f) => ({ + [Symbol.iterator]: () => { + const stack = [...start] + const discovered = new Set() + + const nextMapped = () => { + while (stack.length > 0) { + const current = stack.pop()! + + if (discovered.has(current)) { + continue + } + + discovered.add(current) + + const nodeDataOption = graph.nodes.get(current) + if (nodeDataOption === undefined) { + continue + } + + const neighbors = neighborsDirected(graph, current, direction) + for (let i = neighbors.length - 1; i >= 0; i--) { + const neighbor = neighbors[i] + if (!discovered.has(neighbor)) { + stack.push(neighbor) + } + } + + return { done: false, value: f(current, nodeDataOption) } + } + + return { done: true, value: undefined } as const + } + + return { next: nextMapped } + } + })) +} + +/** + * Creates a new BFS iterator with optional configuration. + * + * The iterator maintains a queue of nodes to visit and tracks discovered nodes. + * It provides lazy evaluation of the breadth-first search. + * + * @example + * ```ts + * import { Graph } from "effect" + * + * const graph = Graph.directed((mutable) => { + * const a = Graph.addNode(mutable, "A") + * const b = Graph.addNode(mutable, "B") + * const c = Graph.addNode(mutable, "C") + * Graph.addEdge(mutable, a, b, 1) + * Graph.addEdge(mutable, b, c, 1) + * }) + * + * // Start from a specific node + * const bfs1 = Graph.bfs(graph, { start: [0] }) + * for (const nodeIndex of Graph.indices(bfs1)) { + * console.log(nodeIndex) // Traverses in BFS order: 0, 1, 2 + * } + * + * // Empty iterator (no starting nodes) + * const bfs2 = Graph.bfs(graph) + * // Can be used programmatically + * ``` + * + * @since 3.18.0 + * @category iterators + */ +export const bfs = ( + graph: Graph | MutableGraph, + config: SearchConfig = {} +): NodeWalker => { + const start = config.start ?? [] + const direction = config.direction ?? "outgoing" + + // Validate that all start nodes exist + for (const nodeIndex of start) { + if (!hasNode(graph, nodeIndex)) { + throw missingNode(nodeIndex) + } + } + + return new Walker((f) => ({ + [Symbol.iterator]: () => { + const queue = [...start] + const discovered = new Set() + + const nextMapped = () => { + while (queue.length > 0) { + const current = queue.shift()! + + if (!discovered.has(current)) { + discovered.add(current) + + const neighbors = neighborsDirected(graph, current, direction) + for (const neighbor of neighbors) { + if (!discovered.has(neighbor)) { + queue.push(neighbor) + } + } + + const nodeData = getNode(graph, current) + if (Option.isSome(nodeData)) { + return { done: false, value: f(current, nodeData.value) } + } + return nextMapped() + } + } + + return { done: true, value: undefined } as const + } + + return { next: nextMapped } + } + })) +} + +/** + * Configuration options for topological sort iterator. + * + * @since 3.18.0 + * @category models + */ +export interface TopoConfig { + readonly initials?: Array +} + +/** + * Creates a new topological sort iterator with optional configuration. + * + * The iterator uses Kahn's algorithm to lazily produce nodes in topological order. + * Throws an error if the graph contains cycles. + * + * @example + * ```ts + * import { Graph } from "effect" + * + * const graph = Graph.directed((mutable) => { + * const a = Graph.addNode(mutable, "A") + * const b = Graph.addNode(mutable, "B") + * const c = Graph.addNode(mutable, "C") + * Graph.addEdge(mutable, a, b, 1) + * Graph.addEdge(mutable, b, c, 1) + * }) + * + * // Standard topological sort + * const topo1 = Graph.topo(graph) + * for (const nodeIndex of Graph.indices(topo1)) { + * console.log(nodeIndex) // 0, 1, 2 (topological order) + * } + * + * // With initial nodes + * const topo2 = Graph.topo(graph, { initials: [0] }) + * + * // Throws error for cyclic graph + * const cyclicGraph = Graph.directed((mutable) => { + * const a = Graph.addNode(mutable, "A") + * const b = Graph.addNode(mutable, "B") + * Graph.addEdge(mutable, a, b, 1) + * Graph.addEdge(mutable, b, a, 2) // Creates cycle + * }) + * + * try { + * Graph.topo(cyclicGraph) // Throws: "Cannot perform topological sort on cyclic graph" + * } catch (error) { + * console.log((error as Error).message) + * } + * ``` + * + * @since 3.18.0 + * @category iterators + */ +export const topo = ( + graph: Graph | MutableGraph, + config: TopoConfig = {} +): NodeWalker => { + // Check if graph is acyclic first + if (!isAcyclic(graph)) { + throw new Error("Cannot perform topological sort on cyclic graph") + } + + const initials = config.initials ?? [] + + // Validate that all initial nodes exist + for (const nodeIndex of initials) { + if (!hasNode(graph, nodeIndex)) { + throw missingNode(nodeIndex) + } + } + + return new Walker((f) => ({ + [Symbol.iterator]: () => { + const inDegree = new Map() + const remaining = new Set() + const queue = [...initials] + + // Initialize in-degree counts + for (const [nodeIndex] of graph.nodes) { + inDegree.set(nodeIndex, 0) + remaining.add(nodeIndex) + } + + // Calculate in-degrees + for (const [, edgeData] of graph.edges) { + const currentInDegree = inDegree.get(edgeData.target) || 0 + inDegree.set(edgeData.target, currentInDegree + 1) + } + + // Add nodes with zero in-degree to queue if no initials provided + if (initials.length === 0) { + for (const [nodeIndex, degree] of inDegree) { + if (degree === 0) { + queue.push(nodeIndex) + } + } + } + + const nextMapped = () => { + while (queue.length > 0) { + const current = queue.shift()! + + if (remaining.has(current)) { + remaining.delete(current) + + // Process outgoing edges, reducing in-degree of targets + const neighbors = neighborsDirected(graph, current, "outgoing") + for (const neighbor of neighbors) { + if (remaining.has(neighbor)) { + const currentInDegree = inDegree.get(neighbor) || 0 + const newInDegree = currentInDegree - 1 + inDegree.set(neighbor, newInDegree) + + // If in-degree becomes 0, add to queue + if (newInDegree === 0) { + queue.push(neighbor) + } + } + } + + const nodeData = getNode(graph, current) + if (Option.isSome(nodeData)) { + return { done: false, value: f(current, nodeData.value) } + } + return nextMapped() + } + } + + return { done: true, value: undefined } as const + } + + return { next: nextMapped } + } + })) +} + +/** + * Creates a new DFS postorder iterator with optional configuration. + * + * The iterator maintains a stack with visit state tracking and emits nodes + * in postorder (after all descendants have been processed). Essential for + * dependency resolution and tree destruction algorithms. + * + * @example + * ```ts + * import { Graph } from "effect" + * + * const graph = Graph.directed((mutable) => { + * const root = Graph.addNode(mutable, "root") + * const child1 = Graph.addNode(mutable, "child1") + * const child2 = Graph.addNode(mutable, "child2") + * Graph.addEdge(mutable, root, child1, 1) + * Graph.addEdge(mutable, root, child2, 1) + * }) + * + * // Postorder: children before parents + * const postOrder = Graph.dfsPostOrder(graph, { start: [0] }) + * for (const node of postOrder) { + * console.log(node) // 1, 2, 0 + * } + * ``` + * + * @since 3.18.0 + * @category iterators + */ +export const dfsPostOrder = ( + graph: Graph | MutableGraph, + config: SearchConfig = {} +): NodeWalker => { + const start = config.start ?? [] + const direction = config.direction ?? "outgoing" + + // Validate that all start nodes exist + for (const nodeIndex of start) { + if (!hasNode(graph, nodeIndex)) { + throw missingNode(nodeIndex) + } + } + + return new Walker((f) => ({ + [Symbol.iterator]: () => { + const stack: Array<{ node: NodeIndex; visitedChildren: boolean }> = [] + const discovered = new Set() + const finished = new Set() + + // Initialize stack with start nodes + for (let i = start.length - 1; i >= 0; i--) { + stack.push({ node: start[i], visitedChildren: false }) + } + + const nextMapped = () => { + while (stack.length > 0) { + const current = stack[stack.length - 1] + + if (!discovered.has(current.node)) { + discovered.add(current.node) + current.visitedChildren = false + } + + if (!current.visitedChildren) { + current.visitedChildren = true + const neighbors = neighborsDirected(graph, current.node, direction) + + for (let i = neighbors.length - 1; i >= 0; i--) { + const neighbor = neighbors[i] + if (!discovered.has(neighbor) && !finished.has(neighbor)) { + stack.push({ node: neighbor, visitedChildren: false }) + } + } + } else { + const nodeToEmit = stack.pop()!.node + + if (!finished.has(nodeToEmit)) { + finished.add(nodeToEmit) + + const nodeData = getNode(graph, nodeToEmit) + if (Option.isSome(nodeData)) { + return { done: false, value: f(nodeToEmit, nodeData.value) } + } + return nextMapped() + } + } + } + + return { done: true, value: undefined } as const + } + + return { next: nextMapped } + } + })) +} + +/** + * Creates an iterator over all node indices in the graph. + * + * The iterator produces node indices in the order they were added to the graph. + * This provides access to all nodes regardless of connectivity. + * + * @example + * ```ts + * import { Graph } from "effect" + * + * const graph = Graph.directed((mutable) => { + * const a = Graph.addNode(mutable, "A") + * const b = Graph.addNode(mutable, "B") + * const c = Graph.addNode(mutable, "C") + * Graph.addEdge(mutable, a, b, 1) + * }) + * + * const indices = Array.from(Graph.indices(Graph.nodes(graph))) + * console.log(indices) // [0, 1, 2] + * ``` + * + * @since 3.18.0 + * @category iterators + */ +export const nodes = ( + graph: Graph | MutableGraph +): NodeWalker => + new Walker((f) => ({ + [Symbol.iterator]() { + const nodeMap = graph.nodes + const iterator = nodeMap.entries() + + return { + next() { + const result = iterator.next() + if (result.done) { + return { done: true, value: undefined } + } + const [nodeIndex, nodeData] = result.value + return { done: false, value: f(nodeIndex, nodeData) } + } + } + } + })) + +/** + * Creates an iterator over all edge indices in the graph. + * + * The iterator produces edge indices in the order they were added to the graph. + * This provides access to all edges regardless of connectivity. + * + * @example + * ```ts + * import { Graph } from "effect" + * + * const graph = Graph.directed((mutable) => { + * const a = Graph.addNode(mutable, "A") + * const b = Graph.addNode(mutable, "B") + * const c = Graph.addNode(mutable, "C") + * Graph.addEdge(mutable, a, b, 1) + * Graph.addEdge(mutable, b, c, 2) + * }) + * + * const indices = Array.from(Graph.indices(Graph.edges(graph))) + * console.log(indices) // [0, 1] + * ``` + * + * @since 3.18.0 + * @category iterators + */ +export const edges = ( + graph: Graph | MutableGraph +): EdgeWalker => + new Walker((f) => ({ + [Symbol.iterator]() { + const edgeMap = graph.edges + const iterator = edgeMap.entries() + + return { + next() { + const result = iterator.next() + if (result.done) { + return { done: true, value: undefined } + } + const [edgeIndex, edgeData] = result.value + return { done: false, value: f(edgeIndex, edgeData) } + } + } + } + })) + +/** + * Configuration for externals iterator. + * + * @since 3.18.0 + * @category models + */ +export interface ExternalsConfig { + readonly direction?: Direction +} + +/** + * Creates an iterator over external nodes (nodes without edges in specified direction). + * + * External nodes are nodes that have no outgoing edges (direction="outgoing") or + * no incoming edges (direction="incoming"). These are useful for finding + * sources, sinks, or isolated nodes. + * + * @example + * ```ts + * import { Graph } from "effect" + * + * const graph = Graph.directed((mutable) => { + * const source = Graph.addNode(mutable, "source") // 0 - no incoming + * const middle = Graph.addNode(mutable, "middle") // 1 - has both + * const sink = Graph.addNode(mutable, "sink") // 2 - no outgoing + * const isolated = Graph.addNode(mutable, "isolated") // 3 - no edges + * + * Graph.addEdge(mutable, source, middle, 1) + * Graph.addEdge(mutable, middle, sink, 2) + * }) + * + * // Nodes with no outgoing edges (sinks + isolated) + * const sinks = Array.from(Graph.indices(Graph.externals(graph, { direction: "outgoing" }))) + * console.log(sinks) // [2, 3] + * + * // Nodes with no incoming edges (sources + isolated) + * const sources = Array.from(Graph.indices(Graph.externals(graph, { direction: "incoming" }))) + * console.log(sources) // [0, 3] + * ``` + * + * @since 3.18.0 + * @category iterators + */ +export const externals = ( + graph: Graph | MutableGraph, + config: ExternalsConfig = {} +): NodeWalker => { + const direction = config.direction ?? "outgoing" + + return new Walker((f) => ({ + [Symbol.iterator]: () => { + const nodeMap = graph.nodes + const adjacencyMap = direction === "incoming" + ? graph.reverseAdjacency + : graph.adjacency + + const nodeIterator = nodeMap.entries() + + const nextMapped = () => { + let current = nodeIterator.next() + while (!current.done) { + const [nodeIndex, nodeData] = current.value + const adjacencyList = adjacencyMap.get(nodeIndex) + + // Node is external if it has no edges in the specified direction + if (adjacencyList === undefined || adjacencyList.length === 0) { + return { done: false, value: f(nodeIndex, nodeData) } + } + current = nodeIterator.next() + } + + return { done: true, value: undefined } as const + } + + return { next: nextMapped } + } + })) +} diff --git a/repos/effect/packages/effect/src/GroupBy.ts b/repos/effect/packages/effect/src/GroupBy.ts new file mode 100644 index 0000000..315f0b6 --- /dev/null +++ b/repos/effect/packages/effect/src/GroupBy.ts @@ -0,0 +1,103 @@ +/** + * @since 2.0.0 + */ +import * as internal from "./internal/groupBy.js" +import type { Pipeable } from "./Pipeable.js" +import type { Predicate } from "./Predicate.js" +import type * as Queue from "./Queue.js" +import type * as Stream from "./Stream.js" +import type * as Take from "./Take.js" +import type { Covariant, NoInfer } from "./Types.js" + +/** + * @since 2.0.0 + * @category symbols + */ +export const GroupByTypeId: unique symbol = internal.GroupByTypeId + +/** + * @since 2.0.0 + * @category symbols + */ +export type GroupByTypeId = typeof GroupByTypeId + +/** + * Representation of a grouped stream. This allows to filter which groups will + * be processed. Once this is applied all groups will be processed in parallel + * and the results will be merged in arbitrary order. + * + * @since 2.0.0 + * @category models + */ +export interface GroupBy extends GroupBy.Variance, Pipeable { + readonly grouped: Stream.Stream>], E, R> +} + +/** + * @since 2.0.0 + */ +export declare namespace GroupBy { + /** + * @since 2.0.0 + * @category models + */ + export interface Variance { + readonly [GroupByTypeId]: { + readonly _K: Covariant + readonly _V: Covariant + readonly _E: Covariant + readonly _R: Covariant + } + } +} + +/** + * Run the function across all groups, collecting the results in an + * arbitrary order. + * + * @since 2.0.0 + * @category destructors + */ +export const evaluate: { + ( + f: (key: K, stream: Stream.Stream) => Stream.Stream, + options?: { readonly bufferSize?: number | undefined } | undefined + ): (self: GroupBy) => Stream.Stream + ( + self: GroupBy, + f: (key: K, stream: Stream.Stream) => Stream.Stream, + options?: { readonly bufferSize?: number | undefined } | undefined + ): Stream.Stream +} = internal.evaluate + +/** + * Filter the groups to be processed. + * + * @since 2.0.0 + * @category utils + */ +export const filter: { + (predicate: Predicate>): (self: GroupBy) => GroupBy + (self: GroupBy, predicate: Predicate): GroupBy +} = internal.filter + +/** + * Only consider the first `n` groups found in the `Stream`. + * + * @since 2.0.0 + * @category utils + */ +export const first: { + (n: number): (self: GroupBy) => GroupBy + (self: GroupBy, n: number): GroupBy +} = internal.first + +/** + * Constructs a `GroupBy` from a `Stream`. + * + * @since 2.0.0 + * @category constructors + */ +export const make: ( + grouped: Stream.Stream>], E, R> +) => GroupBy = internal.make diff --git a/repos/effect/packages/effect/src/HKT.ts b/repos/effect/packages/effect/src/HKT.ts new file mode 100644 index 0000000..c013c64 --- /dev/null +++ b/repos/effect/packages/effect/src/HKT.ts @@ -0,0 +1,45 @@ +/** + * @since 2.0.0 + */ +import type * as Types from "./Types.js" + +/** + * @since 2.0.0 + */ +export declare const URI: unique symbol + +/** + * @since 2.0.0 + */ +export interface TypeClass { + readonly [URI]?: F +} + +/** + * @since 2.0.0 + */ +export interface TypeLambda { + readonly In: unknown + readonly Out2: unknown + readonly Out1: unknown + readonly Target: unknown +} + +/** + * @since 2.0.0 + */ +export type Kind = F extends { + readonly type: unknown +} ? (F & { + readonly In: In + readonly Out2: Out2 + readonly Out1: Out1 + readonly Target: Target + })["type"] + : { + readonly F: F + readonly In: Types.Contravariant + readonly Out2: Types.Covariant + readonly Out1: Types.Covariant + readonly Target: Types.Invariant + } diff --git a/repos/effect/packages/effect/src/Hash.ts b/repos/effect/packages/effect/src/Hash.ts new file mode 100644 index 0000000..f015be6 --- /dev/null +++ b/repos/effect/packages/effect/src/Hash.ts @@ -0,0 +1,195 @@ +/** + * @since 2.0.0 + */ +import { pipe } from "./Function.js" +import { globalValue } from "./GlobalValue.js" +import { hasProperty } from "./Predicate.js" +import { structuralRegionState } from "./Utils.js" + +/** @internal */ +const randomHashCache = globalValue( + Symbol.for("effect/Hash/randomHashCache"), + () => new WeakMap() +) + +/** + * @since 2.0.0 + * @category symbols + */ +export const symbol: unique symbol = Symbol.for("effect/Hash") + +/** + * @since 2.0.0 + * @category models + */ +export interface Hash { + [symbol](): number +} + +/** + * @since 2.0.0 + * @category hashing + */ +export const hash:
(self: A) => number = (self: A) => { + if (structuralRegionState.enabled === true) { + return 0 + } + + switch (typeof self) { + case "number": + return number(self) + case "bigint": + return string(self.toString(10)) + case "boolean": + return string(String(self)) + case "symbol": + return string(String(self)) + case "string": + return string(self) + case "undefined": + return string("undefined") + case "function": + case "object": { + if (self === null) { + return string("null") + } else if (self instanceof Date) { + if (Number.isNaN(self.getTime())) { + return string("Invalid Date") + } + return hash(self.toISOString()) + } else if (self instanceof URL) { + return hash(self.href) + } else if (isHash(self)) { + return self[symbol]() + } else { + return random(self) + } + } + default: + throw new Error( + `BUG: unhandled typeof ${typeof self} - please report an issue at https://github.com/Effect-TS/effect/issues` + ) + } +} + +/** + * @since 2.0.0 + * @category hashing + */ +export const random: (self: A) => number = (self) => { + if (!randomHashCache.has(self)) { + randomHashCache.set(self, number(Math.floor(Math.random() * Number.MAX_SAFE_INTEGER))) + } + return randomHashCache.get(self)! +} + +/** + * @since 2.0.0 + * @category hashing + */ +export const combine: (b: number) => (self: number) => number = (b) => (self) => (self * 53) ^ b + +/** + * @since 2.0.0 + * @category hashing + */ +export const optimize = (n: number): number => (n & 0xbfffffff) | ((n >>> 1) & 0x40000000) + +/** + * @since 2.0.0 + * @category guards + */ +export const isHash = (u: unknown): u is Hash => hasProperty(u, symbol) + +/** + * @since 2.0.0 + * @category hashing + */ +export const number = (n: number) => { + if (n !== n || n === Infinity) { + return 0 + } + let h = n | 0 + if (h !== n) { + h ^= n * 0xffffffff + } + while (n > 0xffffffff) { + h ^= n /= 0xffffffff + } + return optimize(h) +} + +/** + * @since 2.0.0 + * @category hashing + */ +export const string = (str: string) => { + let h = 5381, i = str.length + while (i) { + h = (h * 33) ^ str.charCodeAt(--i) + } + return optimize(h) +} + +/** + * @since 2.0.0 + * @category hashing + */ +export const structureKeys = (o: A, keys: ReadonlyArray) => { + let h = 12289 + for (let i = 0; i < keys.length; i++) { + h ^= pipe(string(keys[i]! as string), combine(hash((o as any)[keys[i]!]))) + } + return optimize(h) +} + +/** + * @since 2.0.0 + * @category hashing + */ +export const structure = (o: A) => + structureKeys(o, Object.keys(o) as unknown as ReadonlyArray) + +/** + * @since 2.0.0 + * @category hashing + */ +export const array = (arr: ReadonlyArray) => { + let h = 6151 + for (let i = 0; i < arr.length; i++) { + h = pipe(h, combine(hash(arr[i]))) + } + return optimize(h) +} + +/** + * @since 2.0.0 + * @category hashing + */ +export const cached: { + (self: object): (hash: number) => number + (self: object, hash: number): number +} = function() { + if (arguments.length === 1) { + const self = arguments[0] as object + return function(hash: number) { + Object.defineProperty(self, symbol, { + value() { + return hash + }, + enumerable: false + }) + return hash + } as any + } + const self = arguments[0] as object + const hash = arguments[1] as number + Object.defineProperty(self, symbol, { + value() { + return hash + }, + enumerable: false + }) + + return hash +} diff --git a/repos/effect/packages/effect/src/HashMap.ts b/repos/effect/packages/effect/src/HashMap.ts new file mode 100644 index 0000000..314da39 --- /dev/null +++ b/repos/effect/packages/effect/src/HashMap.ts @@ -0,0 +1,519 @@ +/** + * @since 2.0.0 + */ + +import type { Equal } from "./Equal.js" +import type { HashSet } from "./HashSet.js" +import type { Inspectable } from "./Inspectable.js" +import * as HM from "./internal/hashMap.js" +import * as keySet_ from "./internal/hashMap/keySet.js" +import type { Option } from "./Option.js" +import type { Pipeable } from "./Pipeable.js" +import type { NoInfer } from "./Types.js" + +const TypeId: unique symbol = HM.HashMapTypeId as TypeId + +/** + * @since 2.0.0 + * @category symbol + */ +export type TypeId = typeof TypeId + +/** + * @since 2.0.0 + * @category models + */ +export interface HashMap extends Iterable<[Key, Value]>, Equal, Pipeable, Inspectable { + readonly [TypeId]: TypeId +} + +/** + * @since 2.0.0 + */ +export declare namespace HashMap { + /** + * @since 2.0.0 + * @category models + */ + export type UpdateFn = (option: Option) => Option + /** + * This type-level utility extracts the key type `K` from a `HashMap` type. + * + * @example + * ```ts + * import { HashMap } from "effect" + * + * declare const hm: HashMap.HashMap + * + * // $ExpectType string + * type K = HashMap.HashMap.Key + * + * ``` + * @since 2.0.0 + * @category type-level + */ + export type Key> = [T] extends [HashMap] ? _K : never + /** + * This type-level utility extracts the value type `V` from a `HashMap` type. + * + * @example + * ```ts + * import { HashMap } from "effect" + * + * declare const hm: HashMap.HashMap + * + * // $ExpectType number + * type V = HashMap.HashMap.Value + * + * ``` + * @since 2.0.0 + * @category type-level + */ + export type Value> = [T] extends [HashMap] ? _V : never + + /** + * This type-level utility extracts the entry type `[K, V]` from a `HashMap` type. + * + * @example + * ```ts + * import { HashMap } from "effect" + * + * declare const hm: HashMap.HashMap + * + * // $ExpectType [string, number] + * type V = HashMap.HashMap.Entry + * + * ``` + * @since 3.9.0 + * @category type-level + */ + export type Entry> = [Key, Value] +} + +/** + * @since 2.0.0 + * @category refinements + */ +export const isHashMap: { + (u: Iterable): u is HashMap + (u: unknown): u is HashMap +} = HM.isHashMap + +/** + * Creates a new `HashMap`. + * + * @since 2.0.0 + * @category constructors + */ +export const empty: () => HashMap = HM.empty + +/** + * Constructs a new `HashMap` from an array of key/value pairs. + * + * @since 2.0.0 + * @category constructors + */ +export const make: >( + ...entries: Entries +) => HashMap< + Entries[number] extends readonly [infer K, any] ? K : never, + Entries[number] extends readonly [any, infer V] ? V : never +> = HM.make + +/** + * Creates a new `HashMap` from an iterable collection of key/value pairs. + * + * @since 2.0.0 + * @category constructors + */ +export const fromIterable: (entries: Iterable) => HashMap = HM.fromIterable + +/** + * Checks if the `HashMap` contains any entries. + * + * @since 2.0.0 + * @category elements + */ +export const isEmpty: (self: HashMap) => boolean = HM.isEmpty + +/** + * Safely lookup the value for the specified key in the `HashMap` using the + * internal hashing function. + * + * @since 2.0.0 + * @category elements + */ +export const get: { + (key: K1): (self: HashMap) => Option + (self: HashMap, key: K1): Option +} = HM.get + +/** + * Lookup the value for the specified key in the `HashMap` using a custom hash. + * + * @since 2.0.0 + * @category elements + */ +export const getHash: { + (key: K1, hash: number): (self: HashMap) => Option + (self: HashMap, key: K1, hash: number): Option +} = HM.getHash + +/** + * Unsafely lookup the value for the specified key in the `HashMap` using the + * internal hashing function. + * + * @since 2.0.0 + * @category unsafe + */ +export const unsafeGet: { + (key: K1): (self: HashMap) => V + (self: HashMap, key: K1): V +} = HM.unsafeGet + +/** + * Checks if the specified key has an entry in the `HashMap`. + * + * @since 2.0.0 + * @category elements + */ +export const has: { + (key: K1): (self: HashMap) => boolean + (self: HashMap, key: K1): boolean +} = HM.has + +/** + * Checks if the specified key has an entry in the `HashMap` using a custom + * hash. + * + * @since 2.0.0 + * @category elements + */ +export const hasHash: { + (key: K1, hash: number): (self: HashMap) => boolean + (self: HashMap, key: K1, hash: number): boolean +} = HM.hasHash + +/** + * Checks if an element matching the given predicate exists in the given `HashMap`. + * + * @example + * ```ts + * import { HashMap } from "effect" + * + * const hm = HashMap.make([1, 'a']) + * HashMap.hasBy(hm, (value, key) => value === 'a' && key === 1); // -> true + * HashMap.hasBy(hm, (value) => value === 'b'); // -> false + * + * ``` + * + * @since 3.16.0 + * @category elements + */ +export const hasBy: { + (predicate: (value: NoInfer, key: NoInfer) => boolean): (self: HashMap) => boolean + (self: HashMap, predicate: (value: NoInfer, key: NoInfer) => boolean): boolean +} = HM.hasBy + +/** + * Sets the specified key to the specified value using the internal hashing + * function. + * + * @since 2.0.0 + */ +export const set: { + (key: K, value: V): (self: HashMap) => HashMap + (self: HashMap, key: K, value: V): HashMap +} = HM.set + +/** + * Returns an `IterableIterator` of the keys within the `HashMap`. + * + * @since 2.0.0 + * @category getters + */ +export const keys: (self: HashMap) => IterableIterator = HM.keys + +/** + * Returns a `HashSet` of keys within the `HashMap`. + * + * @since 2.0.0 + * @category getter + */ +export const keySet: (self: HashMap) => HashSet = keySet_.keySet + +/** + * Returns an `IterableIterator` of the values within the `HashMap`. + * + * @since 2.0.0 + * @category getters + */ +export const values: (self: HashMap) => IterableIterator = HM.values + +/** + * Returns an `Array` of the values within the `HashMap`. + * + * @since 3.13.0 + * @category getters + */ +export const toValues = (self: HashMap): Array => Array.from(values(self)) + +/** + * Returns an `IterableIterator` of the entries within the `HashMap`. + * + * @since 2.0.0 + * @category getters + */ +export const entries: (self: HashMap) => IterableIterator<[K, V]> = HM.entries + +/** + * Returns an `Array<[K, V]>` of the entries within the `HashMap`. + * + * @since 2.0.0 + * @category getters + */ +export const toEntries = (self: HashMap): Array<[K, V]> => Array.from(entries(self)) + +/** + * Returns the number of entries within the `HashMap`. + * + * @since 2.0.0 + * @category getters + */ +export const size: (self: HashMap) => number = HM.size + +/** + * Counts all the element of the given HashMap that pass the given predicate + * + * **Example** + * + * ```ts + * import { HashMap } from "effect" + * + * const map = HashMap.make([1, "a"], [2, "b"], [3, "c"]) + * const result = HashMap.countBy(map, (_v, key) => key % 2 === 1) + * console.log(result) // 2 + * ``` + * + * @since 3.17.0 + * @category folding + */ +export const countBy: { + (predicate: (value: NoInfer, key: NoInfer) => boolean): (self: HashMap) => number + (self: HashMap, predicate: (value: NoInfer, key: NoInfer) => boolean): number +} = HM.countBy + +/** + * Marks the `HashMap` as mutable. + * + * @since 2.0.0 + */ +export const beginMutation: (self: HashMap) => HashMap = HM.beginMutation + +/** + * Marks the `HashMap` as immutable. + * + * @since 2.0.0 + */ +export const endMutation: (self: HashMap) => HashMap = HM.endMutation + +/** + * Mutates the `HashMap` within the context of the provided function. + * + * @since 2.0.0 + */ +export const mutate: { + (f: (self: HashMap) => void): (self: HashMap) => HashMap + (self: HashMap, f: (self: HashMap) => void): HashMap +} = HM.mutate + +/** + * Set or remove the specified key in the `HashMap` using the specified + * update function. The value of the specified key will be computed using the + * provided hash. + * + * The update function will be invoked with the current value of the key if it + * exists, or `None` if no such value exists. + * + * @since 2.0.0 + */ +export const modifyAt: { + (key: K, f: HashMap.UpdateFn): (self: HashMap) => HashMap + (self: HashMap, key: K, f: HashMap.UpdateFn): HashMap +} = HM.modifyAt + +/** + * Alter the value of the specified key in the `HashMap` using the specified + * update function. The value of the specified key will be computed using the + * provided hash. + * + * The update function will be invoked with the current value of the key if it + * exists, or `None` if no such value exists. + * + * This function will always either update or insert a value into the `HashMap`. + * + * @since 2.0.0 + */ +export const modifyHash: { + (key: K, hash: number, f: HashMap.UpdateFn): (self: HashMap) => HashMap + (self: HashMap, key: K, hash: number, f: HashMap.UpdateFn): HashMap +} = HM.modifyHash + +/** + * Updates the value of the specified key within the `HashMap` if it exists. + * + * @since 2.0.0 + */ +export const modify: { + (key: K, f: (v: V) => V): (self: HashMap) => HashMap + (self: HashMap, key: K, f: (v: V) => V): HashMap +} = HM.modify + +/** + * Performs a union of this `HashMap` and that `HashMap`. + * + * @since 2.0.0 + */ +export const union: { + (that: HashMap): (self: HashMap) => HashMap + (self: HashMap, that: HashMap): HashMap +} = HM.union + +/** + * Remove the entry for the specified key in the `HashMap` using the internal + * hashing function. + * + * @since 2.0.0 + */ +export const remove: { + (key: K): (self: HashMap) => HashMap + (self: HashMap, key: K): HashMap +} = HM.remove + +/** + * Removes all entries in the `HashMap` which have the specified keys. + * + * @since 2.0.0 + */ +export const removeMany: { + (keys: Iterable): (self: HashMap) => HashMap + (self: HashMap, keys: Iterable): HashMap +} = HM.removeMany + +/** + * Maps over the entries of the `HashMap` using the specified function. + * + * @since 2.0.0 + * @category mapping + */ +export const map: { + (f: (value: V, key: K) => A): (self: HashMap) => HashMap + (self: HashMap, f: (value: V, key: K) => A): HashMap +} = HM.map + +/** + * Chains over the entries of the `HashMap` using the specified function. + * + * **NOTE**: the hash and equal of both maps have to be the same. + * + * @since 2.0.0 + * @category sequencing + */ +export const flatMap: { + (f: (value: A, key: K) => HashMap): (self: HashMap) => HashMap + (self: HashMap, f: (value: A, key: K) => HashMap): HashMap +} = HM.flatMap + +/** + * Applies the specified function to the entries of the `HashMap`. + * + * @since 2.0.0 + * @category traversing + */ +export const forEach: { + (f: (value: V, key: K) => void): (self: HashMap) => void + (self: HashMap, f: (value: V, key: K) => void): void +} = HM.forEach + +/** + * Reduces the specified state over the entries of the `HashMap`. + * + * @since 2.0.0 + * @category folding + */ +export const reduce: { + (zero: Z, f: (accumulator: Z, value: V, key: K) => Z): (self: HashMap) => Z + (self: HashMap, zero: Z, f: (accumulator: Z, value: V, key: K) => Z): Z +} = HM.reduce + +/** + * Filters entries out of a `HashMap` using the specified predicate. + * + * @since 2.0.0 + * @category filtering + */ +export const filter: { + (f: (a: NoInfer, k: K) => a is B): (self: HashMap) => HashMap + (f: (a: NoInfer, k: K) => boolean): (self: HashMap) => HashMap + (self: HashMap, f: (a: A, k: K) => a is B): HashMap + (self: HashMap, f: (a: A, k: K) => boolean): HashMap +} = HM.filter + +/** + * Filters out `None` values from a `HashMap` of `Options`s. + * + * @since 2.0.0 + * @category filtering + */ +export const compact: (self: HashMap>) => HashMap = HM.compact + +/** + * Maps over the entries of the `HashMap` using the specified partial function + * and filters out `None` values. + * + * @since 2.0.0 + * @category filtering + */ +export const filterMap: { + (f: (value: A, key: K) => Option): (self: HashMap) => HashMap + (self: HashMap, f: (value: A, key: K) => Option): HashMap +} = HM.filterMap + +/** + * Returns the first element that satisfies the specified + * predicate, or `None` if no such element exists. + * + * @category elements + * @since 2.0.0 + */ +export const findFirst: { + (predicate: (a: NoInfer, k: K) => a is B): (self: HashMap) => Option<[K, B]> + (predicate: (a: NoInfer, k: K) => boolean): (self: HashMap) => Option<[K, A]> + (self: HashMap, predicate: (a: A, k: K) => a is B): Option<[K, B]> + (self: HashMap, predicate: (a: A, k: K) => boolean): Option<[K, A]> +} = HM.findFirst + +/** + * Checks if any entry in a hashmap meets a specific condition. + * + * @since 3.13.0 + * @category elements + */ +export const some: { + (predicate: (a: NoInfer, k: K) => boolean): (self: HashMap) => boolean + (self: HashMap, predicate: (a: A, k: K) => boolean): boolean +} = HM.some + +/** + * Checks if all entries in a hashmap meets a specific condition. + * + * @param self - The hashmap to check. + * @param predicate - The condition to test entries (value, key). + * + * @since 3.14.0 + * @category elements + */ +export const every: { + (predicate: (a: NoInfer, k: K) => boolean): (self: HashMap) => boolean + (self: HashMap, predicate: (a: A, k: K) => boolean): boolean +} = HM.every diff --git a/repos/effect/packages/effect/src/HashRing.ts b/repos/effect/packages/effect/src/HashRing.ts new file mode 100644 index 0000000..47f2e8d --- /dev/null +++ b/repos/effect/packages/effect/src/HashRing.ts @@ -0,0 +1,317 @@ +/** + * @since 3.19.0 + * @experimental + */ +import { dual } from "./Function.js" +import * as Hash from "./Hash.js" +import * as Inspectable from "./Inspectable.js" +import * as Iterable from "./Iterable.js" +import { type Pipeable, pipeArguments } from "./Pipeable.js" +import { hasProperty } from "./Predicate.js" +import * as PrimaryKey from "./PrimaryKey.js" + +const TypeId = "~effect/cluster/HashRing" as const + +/** + * @since 3.19.0 + * @category Models + * @experimental + */ +export interface HashRing extends Pipeable, Iterable { + readonly [TypeId]: typeof TypeId + readonly baseWeight: number + totalWeightCache: number + readonly nodes: Map + ring: Array<[hash: number, node: string]> +} + +/** + * @since 3.19.0 + * @category Guards + * @experimental + */ +export const isHashRing = (u: unknown): u is HashRing => hasProperty(u, TypeId) + +/** + * @since 3.19.0 + * @category Constructors + * @experimental + */ +export const make = (options?: { + readonly baseWeight?: number | undefined +}): HashRing => { + const self = Object.create(Proto) + self.baseWeight = Math.max(options?.baseWeight ?? 128, 1) + self.totalWeightCache = 0 + self.nodes = new Map() + self.ring = [] + return self +} + +const Proto = { + [TypeId]: TypeId, + [Symbol.iterator](this: HashRing): Iterator { + return Iterable.map(this.nodes.values(), ([n]) => n)[Symbol.iterator]() + }, + pipe() { + return pipeArguments(this, arguments) + }, + ...Inspectable.BaseProto, + toJSON(this: HashRing) { + return { + _id: "HashRing", + baseWeight: this.baseWeight, + nodes: this.ring.map(([, n]) => this.nodes.get(n)![0]) + } + } +} + +/** + * Add new nodes to the ring. If a node already exists in the ring, it + * will be updated. For example, you can use this to update the node's weight. + * + * @since 3.19.0 + * @category Combinators + * @experimental + */ +export const addMany: { + (nodes: Iterable, options?: { + readonly weight?: number | undefined + }): (self: HashRing) => HashRing + (self: HashRing, nodes: Iterable, options?: { + readonly weight?: number | undefined + }): HashRing +} = dual( + (args) => isHashRing(args[0]), + (self: HashRing, nodes: Iterable, options?: { + readonly weight?: number | undefined + }): HashRing => { + const weight = Math.max(options?.weight ?? 1, 0.1) + const keys: Array = [] + let toRemove: Set | undefined + for (const node of nodes) { + const key = PrimaryKey.value(node) + const entry = self.nodes.get(key) + if (entry) { + if (entry[1] === weight) continue + toRemove ??= new Set() + toRemove.add(key) + self.totalWeightCache -= entry[1] + self.totalWeightCache += weight + entry[1] = weight + } else { + self.nodes.set(key, [node, weight]) + self.totalWeightCache += weight + } + keys.push(key) + } + if (toRemove) { + self.ring = self.ring.filter(([, n]) => !toRemove.has(n)) + } + addNodesToRing(self, keys, Math.round(weight * self.baseWeight)) + return self + } +) + +function addNodesToRing(self: HashRing, keys: Array, weight: number) { + for (let i = weight; i > 0; i--) { + for (let j = 0; j < keys.length; j++) { + const key = keys[j] + self.ring.push([ + Hash.string(`${key}:${i}`), + key + ]) + } + } + self.ring.sort((a, b) => a[0] - b[0]) +} + +/** + * Add a new node to the ring. If the node already exists in the ring, it + * will be updated. For example, you can use this to update the node's weight. + * + * @since 3.19.0 + * @category Combinators + * @experimental + */ +export const add: { + (node: A, options?: { + readonly weight?: number | undefined + }): (self: HashRing) => HashRing + (self: HashRing, node: A, options?: { + readonly weight?: number | undefined + }): HashRing +} = dual((args) => isHashRing(args[0]), (self: HashRing, node: A, options?: { + readonly weight?: number | undefined +}): HashRing => addMany(self, [node], options)) + +/** + * Removes the node from the ring. No-op's if the node does not exist. + * + * @since 3.19.0 + * @category Combinators + * @experimental + */ +export const remove: { + (node: A): (self: HashRing) => HashRing + (self: HashRing, node: A): HashRing +} = dual(2, (self: HashRing, node: A): HashRing => { + const key = PrimaryKey.value(node) + const entry = self.nodes.get(key) + if (entry) { + self.nodes.delete(key) + self.ring = self.ring.filter(([, n]) => n !== key) + self.totalWeightCache -= entry[1] + } + return self +}) + +/** + * @since 3.19.0 + * @category Combinators + * @experimental + */ +export const has: { + (node: A): (self: HashRing) => boolean + (self: HashRing, node: A): boolean +} = dual( + 2, + (self: HashRing, node: A): boolean => self.nodes.has(PrimaryKey.value(node)) +) + +/** + * Gets the node which should handle the given input. Returns undefined if + * the hashring has no elements with weight. + * + * @since 3.19.0 + * @category Combinators + * @experimental + */ +export const get = (self: HashRing, input: string): A | undefined => { + if (self.ring.length === 0) { + return undefined + } + const index = getIndexForInput(self, Hash.string(input))[0] + const node = self.ring[index][1]! + return self.nodes.get(node)![0] +} + +/** + * Distributes `count` shards across the nodes in the ring, attempting to + * balance the number of shards allocated to each node. Returns undefined if + * the hashring has no elements with weight. + * + * @since 3.19.0 + * @category Combinators + * @experimental + */ +export const getShards = (self: HashRing, count: number): Array | undefined => { + if (self.ring.length === 0) { + return undefined + } + + const shards = new Array(count) + + // for tracking how many shards have been allocated to each node + const allocations = new Map() + // for tracking which shards still need to be allocated + const remaining = new Set() + // for tracking which nodes have reached the max allocation + const exclude = new Set() + + // First pass - allocate the closest nodes, skipping nodes that have reached + // max + const distances = new Array<[shard: number, node: string, distance: number]>(count) + for (let shard = 0; shard < count; shard++) { + const hash = (shardHashes[shard] ??= Hash.string(`shard-${shard}`)) + const [index, distance] = getIndexForInput(self, hash) + const node = self.ring[index][1]! + distances[shard] = [shard, node, distance] + remaining.add(shard) + } + distances.sort((a, b) => a[2] - b[2]) + for (let i = 0; i < count; i++) { + const [shard, node] = distances[i] + if (exclude.has(node)) continue + const [value, weight] = self.nodes.get(node)! + shards[shard] = value + remaining.delete(shard) + const nodeCount = (allocations.get(node) ?? 0) + 1 + allocations.set(node, nodeCount) + const maxPerNode = Math.max(1, Math.floor(count * (weight / self.totalWeightCache))) + if (nodeCount >= maxPerNode) { + exclude.add(node) + } + } + + // Second pass - allocate any remaining shards, skipping nodes that have + // reached max + let allAtMax = exclude.size === self.nodes.size + remaining.forEach((shard) => { + const index = getIndexForInput(self, shardHashes[shard], allAtMax ? undefined : exclude)[0] + const node = self.ring[index][1] + const [value, weight] = self.nodes.get(node)! + shards[shard] = value + + if (allAtMax) return + const nodeCount = (allocations.get(node) ?? 0) + 1 + allocations.set(node, nodeCount) + const maxPerNode = Math.max(1, Math.floor(count * (weight / self.totalWeightCache))) + if (nodeCount >= maxPerNode) { + exclude.add(node) + if (exclude.size === self.nodes.size) { + allAtMax = true + } + } + }) + + return shards +} + +const shardHashes: Array = [] + +function getIndexForInput( + self: HashRing, + hash: number, + exclude?: ReadonlySet | undefined +): readonly [index: number, distance: number] { + const ring = self.ring + const len = ring.length + + let mid: number + let lo = 0 + let hi = len - 1 + + while (lo <= hi) { + mid = ((lo + hi) / 2) >>> 0 + if (ring[mid][0] >= hash) { + hi = mid - 1 + } else { + lo = mid + 1 + } + } + const a = lo === len ? lo - 1 : lo + const distA = Math.abs(ring[a][0] - hash) + if (exclude === undefined) { + const b = lo - 1 + if (b < 0) { + return [a, distA] + } + const distB = Math.abs(ring[b][0] - hash) + return distA <= distB ? [a, distA] : [b, distB] + } else if (!exclude.has(ring[a][1])) { + return [a, distA] + } + const range = Math.max(lo, len - lo) + for (let i = 1; i < range; i++) { + let index = lo - i + if (index >= 0 && index < len && !exclude.has(ring[index][1])) { + return [index, Math.abs(ring[index][0] - hash)] + } + index = lo + i + if (index >= 0 && index < len && !exclude.has(ring[index][1])) { + return [index, Math.abs(ring[index][0] - hash)] + } + } + return [a, distA] +} diff --git a/repos/effect/packages/effect/src/HashSet.ts b/repos/effect/packages/effect/src/HashSet.ts new file mode 100644 index 0000000..70b70e8 --- /dev/null +++ b/repos/effect/packages/effect/src/HashSet.ts @@ -0,0 +1,2346 @@ +/** + * # HashSet + * + * An immutable `HashSet` provides a collection of unique values with efficient + * lookup, insertion and removal. Once created, a `HashSet` cannot be modified; + * any operation that would alter the set instead returns a new `HashSet` with + * the changes. This immutability offers benefits like predictable state + * management and easier reasoning about your code. + * + * ## What Problem Does It Solve? + * + * `HashSet` solves the problem of maintaining an unsorted collection where each + * value appears exactly once, with fast operations for checking membership and + * adding/removing values. + * + * ## When to Use + * + * Use `HashSet` when you need: + * + * - A collection with no duplicate values + * - Efficient membership testing (**`O(1)`** average complexity) + * - Set operations like union, intersection, and difference + * - An immutable data structure that preserves functional programming patterns + * + * ## Advanced Features + * + * HashSet provides operations for: + * + * - Transforming sets with map and flatMap + * - Filtering elements with filter + * - Combining sets with union, intersection and difference + * - Performance optimizations via mutable operations in controlled contexts + * + * ## Performance Characteristics + * + * - **Lookup** operations ({@link module:HashSet.has}): **`O(1)`** average time + * complexity + * - **Insertion** operations ({@link module:HashSet.add}): **`O(1)`** average time + * complexity + * - **Removal** operations ({@link module:HashSet.remove}): **`O(1)`** average + * time complexity + * - **Set** operations ({@link module:HashSet.union}, + * {@link module:HashSet.intersection}): **`O(n)`** where n is the size of the + * smaller set + * - **Iteration**: **`O(n)`** where n is the size of the set + * + * The HashSet data structure implements the following traits: + * + * - {@link Iterable}: allows iterating over the values in the set + * - {@link Equal}: allows comparing two sets for value-based equality + * - {@link Pipeable}: allows chaining operations with the pipe operator + * - {@link Inspectable}: allows inspecting the contents of the set + * + * ## Operations Reference + * + * | Category | Operation | Description | Complexity | + * | ------------ | ----------------------------------- | ------------------------------------------- | ---------- | + * | constructors | {@link module:HashSet.empty} | Creates an empty HashSet | O(1) | + * | constructors | {@link module:HashSet.fromIterable} | Creates a HashSet from an iterable | O(n) | + * | constructors | {@link module:HashSet.make} | Creates a HashSet from multiple values | O(n) | + * | | | | | + * | elements | {@link module:HashSet.has} | Checks if a value exists in the set | O(1) avg | + * | elements | {@link module:HashSet.some} | Checks if any element satisfies a predicate | O(n) | + * | elements | {@link module:HashSet.every} | Checks if all elements satisfy a predicate | O(n) | + * | elements | {@link module:HashSet.isSubset} | Checks if a set is a subset of another | O(n) | + * | | | | | + * | getters | {@link module:HashSet.values} | Gets an iterator of all values | O(1) | + * | getters | {@link module:HashSet.toValues} | Gets an array of all values | O(n) | + * | getters | {@link module:HashSet.size} | Gets the number of elements | O(1) | + * | | | | | + * | mutations | {@link module:HashSet.add} | Adds a value to the set | O(1) avg | + * | mutations | {@link module:HashSet.remove} | Removes a value from the set | O(1) avg | + * | mutations | {@link module:HashSet.toggle} | Toggles a value's presence | O(1) avg | + * | | | | | + * | operations | {@link module:HashSet.difference} | Computes set difference (A - B) | O(n) | + * | operations | {@link module:HashSet.intersection} | Computes set intersection (A ∩ B) | O(n) | + * | operations | {@link module:HashSet.union} | Computes set union (A ∪ B) | O(n) | + * | | | | | + * | mapping | {@link module:HashSet.map} | Transforms each element | O(n) | + * | | | | | + * | sequencing | {@link module:HashSet.flatMap} | Transforms and flattens elements | O(n) | + * | | | | | + * | traversing | {@link module:HashSet.forEach} | Applies a function to each element | O(n) | + * | | | | | + * | folding | {@link module:HashSet.reduce} | Reduces the set to a single value | O(n) | + * | | | | | + * | filtering | {@link module:HashSet.filter} | Keeps elements that satisfy a predicate | O(n) | + * | | | | | + * | partitioning | {@link module:HashSet.partition} | Splits into two sets by a predicate | O(n) | + * + * ## Notes + * + * ### Composability with the Effect Ecosystem: + * + * This `HashSet` is designed to work seamlessly within the Effect ecosystem. It + * implements the {@link Iterable}, {@link Equal}, {@link Pipeable}, and + * {@link Inspectable} traits from Effect. This ensures compatibility with other + * Effect data structures and functionalities. For example, you can easily use + * Effect's `pipe` method to chain operations on the `HashSet`. + * + * **Equality of Elements with Effect's {@link Equal `Equal`} Trait:** + * + * This `HashSet` relies on Effect's {@link Equal} trait to determine the + * uniqueness of elements within the set. The way equality is checked depends on + * the type of the elements: + * + * - **Primitive Values:** For primitive JavaScript values like strings, numbers, + * booleans, `null`, and `undefined`, equality is determined by their value + * (similar to the `===` operator). + * - **Objects and Custom Types:** For objects and other custom types, equality is + * determined by whether those types implement the {@link Equal} interface + * themselves. If an element type implements `Equal`, the `HashSet` will + * delegate to that implementation to perform the equality check. This allows + * you to define custom logic for determining when two instances of your + * objects should be considered equal based on their properties, rather than + * just their object identity. + * + * ```ts + * import { Equal, Hash, HashSet } from "effect" + * + * class Person implements Equal.Equal { + * constructor( + * readonly id: number, // Unique identifier + * readonly name: string, + * readonly age: number + * ) {} + * + * // Define equality based on id, name, and age + * [Equal.symbol](that: Equal.Equal): boolean { + * if (that instanceof Person) { + * return ( + * Equal.equals(this.id, that.id) && + * Equal.equals(this.name, that.name) && + * Equal.equals(this.age, that.age) + * ) + * } + * return false + * } + * + * // Generate a hash code based on the unique id + * [Hash.symbol](): number { + * return Hash.hash(this.id) + * } + * } + * + * // Creating a HashSet with objects that implement the Equal interface + * const set = HashSet.empty().pipe( + * HashSet.add(new Person(1, "Alice", 30)), + * HashSet.add(new Person(1, "Alice", 30)) + * ) + * + * // HashSet recognizes them as equal, so only one element is stored + * console.log(HashSet.size(set)) + * // Output: 1 + * ``` + * + * **Simplifying Equality and Hashing with `Data` and `Schema`:** + * + * Effect's {@link Data} and {@link Schema `Schema.Data`} modules offer powerful + * ways to automatically handle the implementation of both the {@link Equal} and + * {@link Hash} traits for your custom data structures. + * + * - **`Data` Module:** By using constructors like `Data.struct`, `Data.tuple`, + * `Data.array`, or `Data.case` to define your data types, Effect + * automatically generates the necessary implementations for value-based + * equality and consistent hashing. This significantly reduces boilerplate and + * ensures correctness. + * + * ```ts + * import { HashSet, Data, Equal } from "effect" + * import assert from "node:assert/strict" + * + * // Data.* implements the `Equal` traits for us + * const person1 = Data.struct({ id: 1, name: "Alice", age: 30 }) + * const person2 = Data.struct({ id: 1, name: "Alice", age: 30 }) + * + * assert(Equal.equals(person1, person2)) + * + * const set = HashSet.empty().pipe( + * HashSet.add(person1), + * HashSet.add(person2) + * ) + * + * // HashSet recognizes them as equal, so only one element is stored + * console.log(HashSet.size(set)) // Output: 1 + * ``` + * + * - **`Schema` Module:** When defining data schemas using the {@link Schema} + * module, you can use `Schema.Data` to automatically include the `Equal` and + * `Hash` traits in the decoded objects. This is particularly important when + * working with `HashSet`. **For decoded objects to be correctly recognized as + * equal within a `HashSet`, ensure that the schema for those objects is + * defined using `Schema.Data`.** + * + * ```ts + * import { Equal, HashSet, Schema } from "effect" + * import assert from "node:assert/strict" + * + * // Schema.Data implements the `Equal` traits for us + * const PersonSchema = Schema.Data( + * Schema.Struct({ + * id: Schema.Number, + * name: Schema.String, + * age: Schema.Number + * }) + * ) + * + * const Person = Schema.decode(PersonSchema) + * + * const person1 = Person({ id: 1, name: "Alice", age: 30 }) + * const person2 = Person({ id: 1, name: "Alice", age: 30 }) + * + * assert(Equal.equals(person1, person2)) // Output: true + * + * const set = HashSet.empty().pipe( + * HashSet.add(person1), + * HashSet.add(person2) + * ) + * + * // HashSet thanks to Schema.Data implementation of the `Equal` trait, recognizes the two Person as equal, so only one element is stored + * console.log(HashSet.size(set)) // Output: 1 + * ``` + * + * ### Interoperability with the JavaScript Runtime: + * + * To interoperate with the regular JavaScript runtime, Effect's `HashSet` + * provides methods to access its elements in formats readily usable by + * JavaScript APIs: {@link values `HashSet.values`}, + * {@link toValues `HashSet.toValues`} + * + * ```ts + * import { HashSet } from "effect" + * + * const hashSet: HashSet.HashSet = HashSet.make(1, 2, 3) + * + * // Using HashSet.values to convert HashSet.HashSet to IterableIterator + * const iterable: IterableIterator = HashSet.values(hashSet) + * + * console.log(...iterable) // Logs: 1 2 3 + * + * // Using HashSet.toValues to convert HashSet.HashSet to Array + * const array: Array = HashSet.toValues(hashSet) + * + * console.log(array) // Logs: [ 1, 2, 3 ] + * ``` + * + * Be mindful of performance implications (both time and space complexity) when + * frequently converting between Effect's immutable HashSet and mutable + * JavaScript data structures, especially for large collections. + * + * @module HashSet + * @since 2.0.0 + */ + +import type { Equal } from "./Equal.js" +import type { Inspectable } from "./Inspectable.js" +import * as HS from "./internal/hashSet.js" +import type { Pipeable } from "./Pipeable.js" +import type { Predicate, Refinement } from "./Predicate.js" +import type { NoInfer } from "./Types.js" + +const TypeId: unique symbol = HS.HashSetTypeId as TypeId + +/** + * @since 2.0.0 + * @category symbol + */ +export type TypeId = typeof TypeId + +/** + * @memberof HashSet + * @since 2.0.0 + * @category models + * @example + * + * ```ts + * // Syntax + * import { HashSet } from "effect" + * + * let numberSet: HashSet.HashSet + * ``` + * + * @interface + */ +export interface HashSet extends Iterable, Equal, Pipeable, Inspectable { + readonly [TypeId]: TypeId +} + +/** + * @memberof HashSet + * @since 2.0.0 + * @category refinements + */ +export const isHashSet: { + /** + * Type guard function to determine if a given iterable is a `HashSet`. + * + * This overload preserves the type of the iterable's elements. + * + * @example + * + * ```ts + * import { HashSet } from "effect" + * + * const numberIterable: Iterable<1 | 2 | 3> = [1, 2, 3] + * + * if ( + * // if passed an Iterable the type guard that preserves the type parameter + * HashSet.isHashSet(numberIterable) + * ) { + * const HashSet: HashSet.HashSet<1 | 2 | 3> = numberIterable + * } + * ``` + * + * @param u - The iterable input to be checked. + * @returns A boolean indicating whether the provided iterable is a `HashSet`. + */ + (u: Iterable): u is HashSet + + /** + * Type guard function that checks if the provided value is a `HashSet` of + * unknown type. + * + * @example + * + * ```ts + * import { HashSet } from "effect" + * import assert from "node:assert/strict" + * + * // Check if a value is a HashSet + * const set = HashSet.make(1, 2, 3) + * + * assert.equal(HashSet.isHashSet(set), true) // true + * assert.equal(HashSet.isHashSet(HashSet.empty()), true) + * + * // Works with any type + * assert.equal(HashSet.isHashSet(null), false) // false + * assert.equal(HashSet.isHashSet({}), false) // false + * assert.equal(HashSet.isHashSet([1, 2, 3]), false) // false + * ``` + * + * @param u - The value to check. + * @returns A boolean indicating whether the value is a `HashSet`. + */ + (u: unknown): u is HashSet +} = HS.isHashSet + +/** + * Creates an empty `HashSet`. + * + * Time complexity: **`O(1)`** + * + * @memberof HashSet + * @since 2.0.0 + * @category constructors + * @example + * + * ```ts + * import { HashSet, pipe } from "effect" + * + * console.log( + * pipe( + * // Provide a type argument to create a HashSet of a specific type + * HashSet.empty(), + * HashSet.add(1), + * HashSet.add(1), // Notice the duplicate + * HashSet.add(2), + * HashSet.toValues + * ) + * ) // Output: [1, 2] + * ``` + * + * @see Other `HashSet` constructors are {@link module:HashSet.make} {@link module:HashSet.fromIterable} + */ +export const empty: () => HashSet = HS.empty + +/** + * Creates a new `HashSet` from an iterable collection of values. + * + * Time complexity: **`O(n)`** where n is the number of elements in the iterable + * + * @memberof HashSet + * @since 2.0.0 + * @category constructors + * @example + * + * ```ts + * // Creating a HashSet from an Array + * import { HashSet, pipe } from "effect" + * + * console.log( + * pipe( + * [1, 2, 3, 4, 5, 1, 2, 3], // Array is an Iterable; Note the duplicates. + * HashSet.fromIterable, + * HashSet.toValues + * ) + * ) // Output: [1, 2, 3, 4, 5] + * ``` + * + * @example + * + * ```ts + * // Creating a HashSet from a Set + * import { HashSet, pipe } from "effect" + * + * console.log( + * pipe( + * new Set(["apple", "banana", "orange", "apple"]), // Set is an Iterable + * HashSet.fromIterable, + * HashSet.toValues + * ) + * ) // Output: ["apple", "banana", "orange"] + * ``` + * + * @example + * + * ```ts + * // Creating a HashSet from a Generator + * import { HashSet } from "effect" + * + * // Generator functions return iterables + * function* fibonacci(n: number): Generator { + * let [a, b] = [0, 1] + * for (let i = 0; i < n; i++) { + * yield a + * ;[a, b] = [b, a + b] + * } + * } + * + * // Create a HashSet from the first 10 Fibonacci numbers + * const fibonacciSet = HashSet.fromIterable(fibonacci(10)) + * + * console.log(HashSet.toValues(fibonacciSet)) + * // Outputs: [0, 1, 2, 3, 5, 8, 13, 21, 34] but in unsorted order + * ``` + * + * @example + * + * ```ts + * // Creating a HashSet from another HashSet + * import { HashSet, pipe } from "effect" + * + * console.log( + * pipe( + * // since HashSet implements the Iterable interface, we can use it to create a new HashSet + * HashSet.make(1, 2, 3, 4), + * HashSet.fromIterable, + * HashSet.toValues // turns the HashSet back into an array + * ) + * ) // Output: [1, 2, 3, 4] + * ``` + * + * @example + * + * ```ts + * // Creating a HashSet from other Effect's data structures like Chunk + * import { Chunk, HashSet, pipe } from "effect" + * + * console.log( + * pipe( + * Chunk.make(1, 2, 3, 4), // Iterable + * HashSet.fromIterable, + * HashSet.toValues // turns the HashSet back into an array + * ) + * ) // Outputs: [1, 2, 3, 4] + * ``` + * + * @see Other `HashSet` constructors are {@link module:HashSet.empty} {@link module:HashSet.make} + */ +export const fromIterable: (elements: Iterable) => HashSet = HS.fromIterable + +/** + * Construct a new `HashSet` from a variable number of values. + * + * Time complexity: **`O(n)`** where n is the number of elements + * + * @memberof HashSet + * @since 2.0.0 + * @category constructors + * @example + * + * ```ts + * import { Equal, Hash, HashSet, pipe } from "effect" + * import assert from "node:assert/strict" + * + * class Character implements Equal.Equal { + * readonly name: string + * readonly trait: string + * + * constructor(name: string, trait: string) { + * this.name = name + * this.trait = trait + * } + * + * // Define equality based on name, and trait + * [Equal.symbol](that: Equal.Equal): boolean { + * if (that instanceof Character) { + * return ( + * Equal.equals(this.name, that.name) && + * Equal.equals(this.trait, that.trait) + * ) + * } + * return false + * } + * + * // Generate a hash code based on the sum of the character's name and trait + * [Hash.symbol](): number { + * return Hash.hash(this.name + this.trait) + * } + * + * static readonly of = (name: string, trait: string): Character => { + * return new Character(name, trait) + * } + * } + * + * assert.strictEqual( + * Equal.equals( + * HashSet.make( + * Character.of("Alice", "Curious"), + * Character.of("Alice", "Curious"), + * Character.of("White Rabbit", "Always late"), + * Character.of("Mad Hatter", "Tea enthusiast") + * ), + * // Is the same as adding each character to an empty set + * pipe( + * HashSet.empty(), + * HashSet.add(Character.of("Alice", "Curious")), + * HashSet.add(Character.of("Alice", "Curious")), // Alice tried to attend twice! + * HashSet.add(Character.of("White Rabbit", "Always late")), + * HashSet.add(Character.of("Mad Hatter", "Tea enthusiast")) + * ) + * ), + * true, + * "`HashSet.make` and `HashSet.empty() + HashSet.add()` should be equal" + * ) + * + * assert.strictEqual( + * Equal.equals( + * HashSet.make( + * Character.of("Alice", "Curious"), + * Character.of("Alice", "Curious"), + * Character.of("White Rabbit", "Always late"), + * Character.of("Mad Hatter", "Tea enthusiast") + * ), + * HashSet.fromIterable([ + * Character.of("Alice", "Curious"), + * Character.of("Alice", "Curious"), + * Character.of("White Rabbit", "Always late"), + * Character.of("Mad Hatter", "Tea enthusiast") + * ]) + * ), + * true, + * "`HashSet.make` and `HashSet.fromIterable` should be equal" + * ) + * ``` + * + * @see Other `HashSet` constructors are {@link module:HashSet.fromIterable} {@link module:HashSet.empty} + */ +export const make: >(...elements: As) => HashSet = HS.make + +/** + * Checks if the specified value exists in the `HashSet`. + * + * Time complexity: **`O(1)`** average + * + * @memberof HashSet + * @since 2.0.0 + * @category elements + * @example + * + * ```ts + * // Syntax + * import { HashSet, pipe } from "effect" + * + * // with `data-last`, a.k.a. `pipeable` API + * pipe(HashSet.make(0, 1, 2), HashSet.has(3)) // false + * + * // or piped with the pipe function + * HashSet.make(0, 1, 2).pipe(HashSet.has(3)) // false + * + * // or with `data-first` API + * HashSet.has(HashSet.make(0, 1, 2), 3) // false + * ``` + * + * @returns A `boolean` signaling the presence of the value in the HashSet + * @see Other `HashSet` elements are {@link module:HashSet.some} {@link module:HashSet.every} {@link module:HashSet.isSubset} + */ +export const has: { + /** + * @example + * + * ```ts + * // `data-last` a.k.a. `pipeable` API + * import * as assert from "node:assert/strict" + * import { HashSet, pipe } from "effect" + * + * const set = HashSet.make(0, 1, 2) + * + * assert.equal(pipe(set, HashSet.has(0)), true) + * assert.equal(pipe(set, HashSet.has(1)), true) + * assert.equal(pipe(set, HashSet.has(2)), true) + * assert.equal(pipe(set, HashSet.has(3)), false) + * ``` + */ + (value: A): (self: HashSet) => boolean + + /** + * @example + * + * ```ts + * // `data-first` API + * import * as assert from "node:assert/strict" + * import { HashSet, pipe } from "effect" + * + * const set = HashSet.make(0, 1, 2) + * + * assert.equal(HashSet.has(set, 0), true) + * assert.equal(HashSet.has(set, 1), true) + * assert.equal(HashSet.has(set, 2), true) + * assert.equal(HashSet.has(set, 3), false) + * ``` + */ + (self: HashSet, value: A): boolean +} = HS.has + +/** + * Check if a predicate holds true for some `HashSet` element. + * + * Time complexity: **`O(n)`** where n is the number of elements in the set + * + * @memberof HashSet + * @since 2.0.0 + * @category elements + * @example + * + * ```ts + * // Syntax + * import { HashSet, pipe } from "effect" + * + * const set: HashSet.HashSet = HashSet.make(0, 1, 2) + * + * // with `data-last`, a.k.a. `pipeable` API + * pipe( + * set, + * HashSet.some((n) => n > 0) + * ) // true + * + * // or piped with the pipe function + * set.pipe(HashSet.some((n) => n > 0)) // true + * + * // or with `data-first` API + * HashSet.some(set, (n) => n > 0) // true + * ``` + * + * @see Other `HashSet` elements are {@link module:HashSet.has} {@link module:HashSet.every} {@link module:HashSet.isSubset} + */ +export const some: { + /** + * @example + * + * ```ts + * // `data-last` a.k.a. `pipeable` API + * import * as assert from "node:assert/strict" + * import { HashSet, pipe } from "effect" + * + * const set = HashSet.make(0, 1, 2) + * + * assert.equal( + * pipe( + * set, + * HashSet.some((n) => n > 0) + * ), + * true + * ) + * + * assert.equal( + * pipe( + * set, + * HashSet.some((n) => n > 2) + * ), + * false + * ) + * ``` + */ + (f: Predicate): (self: HashSet) => boolean + + /** + * @example + * + * ```ts + * // `data-first` API + * import * as assert from "node:assert/strict" + * import { HashSet } from "effect" + * + * const set = HashSet.make(0, 1, 2) + * + * assert.equal( + * HashSet.some(set, (n) => n > 0), + * true + * ) + * + * assert.equal( + * HashSet.some(set, (n) => n > 2), + * false + * ) + * ``` + */ + (self: HashSet, f: Predicate): boolean +} = HS.some + +/** + * Check if a predicate holds true for every `HashSet` element. + * + * Time complexity is **`O(n)`** as it needs to traverse the whole HashSet + * collection + * + * @memberof HashSet + * @since 2.0.0 + * @category elements + * @example + * + * ```ts + * // Syntax with Refinement + * import { HashSet, pipe, Predicate } from "effect" + * + * const numberOrString = HashSet.make(1, "1", "one", "uno") + * + * // with `data-last`, a.k.a. `pipeable` API and `Refinement` + * pipe( + * numberOrString, // HashSet.HashSet + * HashSet.every(Predicate.isString) + * ) // HashSet.HashSet + * + * // or piped with the pipe function and `Refinement` + * numberOrString // HashSet.HashSet + * .pipe(HashSet.every(Predicate.isString)) // HashSet.HashSet + * + * // or with `data-first` API and `Refinement` + * HashSet.every( + * numberOrString, // HashSet.HashSet + * Predicate.isString + * ) // HashSet.HashSet + * ``` + * + * @example + * + * ```ts + * // Syntax with Predicate + * import { HashSet, pipe } from "effect" + * + * const set = HashSet.make(1, 2, 3) + * + * // with `data-last`, a.k.a. `pipeable` API + * pipe( + * set, + * HashSet.every((n) => n >= 0) + * ) // true + * + * // or piped with the pipe function + * set.pipe(HashSet.every((n) => n >= 0)) // true + * + * // or with `data-first` API + * HashSet.every(set, (n) => n >= 0) // true + * ``` + * + * @returns A boolean once it has evaluated that whole collection fulfill the + * Predicate function + * @see Other `HashSet` elements are {@link module:HashSet.has} {@link module:HashSet.some} {@link module:HashSet.isSubset} + */ +export const every: { + /** + * @example + * + * ```ts + * import * as assert from "node:assert/strict" + * import { Effect, HashSet, pipe, Predicate } from "effect" + * + * const numberOrString: HashSet.HashSet = HashSet.make( + * 1, + * "1", + * "one", + * "uno" + * ) + * + * assert.equal( + * pipe( + * numberOrString, // HashSet.HashSet + * HashSet.every(Predicate.isString) + * ), // HashSet.HashSet + * false + * ) + * ``` + */ + ( + refinement: Refinement, B> + ): (self: HashSet) => self is HashSet + + /** + * @example + * + * ```ts + * import * as assert from "node:assert/strict" + * import { HashSet, pipe } from "effect" + * + * const set = HashSet.make(0, 1, 2) + * + * assert.equal( + * pipe( + * set, + * HashSet.every((n) => n >= 0) + * ), + * true + * ) + * ``` + */ + (predicate: Predicate): (self: HashSet) => boolean + + /** + * @example + * + * ```ts + * import * as assert from "node:assert/strict" + * import { Effect, HashSet, pipe, Predicate } from "effect" + * + * const numberOrString: HashSet.HashSet = HashSet.make( + * 1, + * "1", + * "one", + * "uno" + * ) + * + * assert.equal( + * HashSet.every( + * numberOrString, // HashSet.HashSet + * Predicate.isString + * ), // HashSet.HashSet + * false + * ) + * ``` + */ + ( + self: HashSet, + refinement: Refinement + ): self is HashSet + + /** + * @example + * + * ```ts + * import * as assert from "node:assert/strict" + * import { HashSet } from "effect" + * + * const set = HashSet.make(0, 1, 2) + * + * assert.equal( + * HashSet.every(set, (n) => n >= 0), + * true + * ) + * ``` + */ + (self: HashSet, predicate: Predicate): boolean +} = HS.every + +/** + * Returns `true` if and only if every element in the this `HashSet` is an + * element of the second set, + * + * **NOTE**: the hash and equal of both sets must be the same. + * + * Time complexity analysis is of **`O(n)`** + * + * @memberof HashSet + * @since 2.0.0 + * @category elements + * @example + * + * ```ts + * // Syntax + * import { HashSet, pipe } from "effect" + * + * const set1 = HashSet.make(0, 1) + * const set2 = HashSet.make(1, 2) + * const set3 = HashSet.make(0, 1, 2) + * + * // with `data-last`, a.k.a. `pipeable` API + * pipe(set1, HashSet.isSubset(set2)) // false + * pipe(set1, HashSet.isSubset(set3)) // true + * + * // or piped with the pipe function + * set1.pipe(HashSet.isSubset(set2)) // false + * set1.pipe(HashSet.isSubset(set3)) // true + * + * // or with `data-first` API + * HashSet.isSubset(set1, set2) // false + * HashSet.isSubset(set1, set3) // true) + * ``` + * + * @see Other `HashSet` elements are {@link module:HashSet.has} {@link module:HashSet.some} {@link module:HashSet.every} + */ +export const isSubset: { + /** + * @example + * + * ```ts + * import { HashSet, pipe } from "effect" + * import * as assert from "node:assert/strict" + * + * assert.equal( + * pipe( + * HashSet.make(0, 1), // + * HashSet.isSubset(HashSet.make(1, 2)) + * ), + * false + * ) + * + * assert.equal( + * pipe( + * HashSet.make(0, 1), // + * HashSet.isSubset(HashSet.make(0, 1, 2)) + * ), + * true + * ) + * ``` + */ + (that: HashSet): (self: HashSet) => boolean + + /** + * @example + * + * ```ts + * import { HashSet } from "effect" + * import * as assert from "node:assert/strict" + * + * assert.equal(HashSet.isSubset(set1, set2), false) + * + * assert.equal(HashSet.isSubset(set1, set3), true) + * ``` + */ + (self: HashSet, that: HashSet): boolean +} = HS.isSubset + +/** + * Returns an `IterableIterator` of the values in the `HashSet`. + * + * Time complexity: **`O(1)`** + * + * @memberof HashSet + * @since 2.0.0 + * @category getters + * @example + * + * ```ts + * import { HashSet, pipe } from "effect" + * + * const numberIterable = pipe( + * HashSet.make(0, 1, 1, 2), // HashSet.HashSet + * HashSet.values // takes an HashSet and returns an IterableIterator + * ) + * + * for (const number of numberIterable) { + * console.log(number) // it will logs: 0, 1, 2 + * } + * ``` + * + * @see Other `HashSet` getters are {@link module:HashSet.toValues} {@link module:HashSet.size} + */ +export const values: (self: HashSet) => IterableIterator = HS.values + +/** + * Returns an `Array` of the values within the `HashSet`. + * + * Time complexity: **`O(n)`** where n is the number of elements in the set + * + * @memberof HashSet + * @since 3.13.0 + * @category getters + * @example + * + * ```ts + * import { HashSet, pipe } from "effect" + * import { deepStrictEqual } from "node:assert/strict" + * + * deepStrictEqual( + * pipe( + * HashSet.make(0, 1, 1, 2), // HashSet + * HashSet.toValues // takes an HashSet and returns an Array + * ), + * Array.of(0, 1, 2) + * ) + * ``` + * + * @see Other `HashSet` getters are {@link module:HashSet.values} {@link module:HashSet.size} + */ +export const toValues = (self: HashSet): Array => Array.from(values(self)) + +/** + * Calculates the number of values in the `HashSet`. + * + * Time complexity: **`O(1)`** + * + * @memberof HashSet + * @since 2.0.0 + * @category getters + * @example + * + * ```ts + * import { HashSet, pipe } from "effect" + * import assert from "node:assert/strict" + * + * assert.deepStrictEqual(pipe(HashSet.empty(), HashSet.size), 0) + * + * assert.deepStrictEqual( + * pipe(HashSet.make(1, 2, 2, 3, 4, 3), HashSet.size), + * 4 + * ) + * ``` + * + * @see Other `HashSet` getters are {@link module:HashSet.values} {@link module:HashSet.toValues} + */ +export const size: (self: HashSet) => number = HS.size + +/** + * Creates a new mutable version of the `HashSet` + * + * When a `HashSet` is mutable, operations like {@link add} and {@link remove} + * modify the data structure in place instead of creating a new one, which is + * more efficient when performing multiple operations. + * + * @memberof HashSet + * @since 2.0.0 + * @example + * + * ```ts + * import { HashSet } from "effect" + * import assert from "node:assert/strict" + * + * const UPPER_BOUND = 10_000 + * + * const immutableSet = HashSet.empty().pipe(HashSet.add(0)) + * + * // Create a mutable version of the immutableSet + * const mutableSet = HashSet.beginMutation(immutableSet) + * + * for (let i = 1; i < UPPER_BOUND; i++) { + * // Operations now modify the set in place instead of creating new instances + * // This is more efficient when making multiple changes + * const pointerToMutableSet = HashSet.add(mutableSet, i) + * + * // the two sets have the same identity, hence `add` is mutating mutableSet and not returning a new HashSet instance + * assert(Object.is(mutableSet, pointerToMutableSet)) + * assert.equal(HashSet.has(mutableSet, i), true) // `i` is in the mutableSet + * assert.equal(HashSet.has(immutableSet, i), false) // `i` is not in the immutableSet + * } + * + * const next = UPPER_BOUND + 1 + * // When done, mark the set as immutable again + * HashSet.endMutation(mutableSet).pipe( + * HashSet.add(next) // since this returns a new HashSet, it will not be logged as part of the mutableSet + * ) + * assert.equal(HashSet.has(mutableSet, next), false) + * + * console.log(HashSet.toValues(immutableSet)) // [0] + * console.log(HashSet.toValues(mutableSet).sort((a, b) => a - b)) // [0, 1, 2, 3, ...rest] + * ``` + * + * @see Other `HashSet` mutations are {@link module:HashSet.add} {@link module:HashSet.remove} {@link module:HashSet.toggle} {@link module:HashSet.endMutation} {@link module:HashSet.mutate} + */ +export const beginMutation: (self: HashSet) => HashSet = HS.beginMutation + +/** + * Makes the `HashSet` immutable again. + * + * After calling `endMutation`, operations like {@link add} and {@link remove} + * will create new instances of the `HashSet` instead of modifying the existing + * one. + * + * @memberof HashSet + * @since 2.0.0 + * @example + * + * ```ts + * import { HashSet } from "effect" + * import assert from "node:assert/strict" + * + * // Create a mutable set + * const mutableSet = HashSet.beginMutation(HashSet.empty()) + * + * // Add some elements to the mutable set + * HashSet.add(mutableSet, 1) + * HashSet.add(mutableSet, 2) + * + * // Before endMutation, operations modify the set in place + * const sameSet = HashSet.add(mutableSet, 3) + * assert(Object.is(mutableSet, sameSet)) // true - same object reference + * assert.deepStrictEqual(HashSet.toValues(mutableSet).sort(), [1, 2, 3]) + * + * // Make the set immutable again + * const immutableSet = HashSet.endMutation(mutableSet) + * + * // endMutation returns the same set instance, now made immutable + * assert(Object.is(mutableSet, immutableSet)) // true - same object reference + * + * // After endMutation, operations create new instances + * const newSet = HashSet.add(immutableSet, 4) + * assert(!Object.is(immutableSet, newSet)) // false - different object references + * + * // The original set remains unchanged + * assert.deepStrictEqual(HashSet.toValues(immutableSet).sort(), [1, 2, 3]) + * + * // The new set contains the added element + * assert.deepStrictEqual(HashSet.toValues(newSet).sort(), [1, 2, 3, 4]) + * ``` + * + * @see Other `HashSet` mutations are {@link module:HashSet.add} {@link module:HashSet.remove} {@link module:HashSet.toggle} {@link module:HashSet.beginMutation} {@link module:HashSet.mutate} + */ +export const endMutation: (self: HashSet) => HashSet = HS.endMutation + +/** + * Mutates the `HashSet` within the context of the provided function. + * + * You can consider it a functional abstraction on top of the lower-level + * mutation primitives of {@link module:HashSet.beginMutation} `->` `mutable + * context` `->` {@link HashSet.endMutation}. + * + * @memberof HashSet + * @since 2.0.0 + * @example + * + * ```ts + * // Syntax + * import { HashSet, pipe } from "effect" + * + * // with data-last, a.k.a. pipeable API + * pipe( + * HashSet.make(1, 2, 3), + * HashSet.mutate((set) => { + * HashSet.add(set, 4) + * HashSet.remove(set, 1) + * }) + * ) + * + * // or piped with the pipe function + * HashSet.make(1, 2, 3).pipe( + * HashSet.mutate((set) => { + * HashSet.add(set, 4) + * HashSet.remove(set, 1) + * }) + * ) + * + * // or with data-first API + * HashSet.mutate(HashSet.make(1, 2, 3), (set) => { + * HashSet.add(set, 4) + * HashSet.remove(set, 1) + * }) + * ``` + * + * @see Other `HashSet` mutations are {@link module:HashSet.add} {@link module:HashSet.remove} {@link module:HashSet.toggle} {@link module:HashSet.beginMutation} {@link module:HashSet.endMutation} + */ +export const mutate: { + /** + * @example + * + * ```ts + * // `data-last` a.k.a. `pipeable` API + * import { HashSet, pipe } from "effect" + * import assert from "node:assert/strict" + * + * // Create a set with initial values + * const immutableSet = HashSet.make(1, 2, 3) + * + * // Use mutate to perform multiple operations efficiently + * const result = pipe( + * immutableSet, + * HashSet.mutate((set) => { + * assert.equal(Object.is(immutableSet, set), false) + * + * // The set is temporarily mutable inside this function + * const mod1 = HashSet.add(set, 4) + * const mod2 = HashSet.remove(set, 1) + * assert.equal(Object.is(mod1, mod2), true) // they are the same object by reference + * }) + * ) + * + * // The original set is unchanged + * assert.equal(Object.is(immutableSet, result), false) + * assert.deepStrictEqual( + * HashSet.toValues(immutableSet).sort(), + * [1, 2, 3] + * ) + * + * // The result contains the mutations + * assert.deepStrictEqual(HashSet.toValues(result).sort(), [2, 3, 4]) + * ``` + */ + (f: (set: HashSet) => void): (self: HashSet) => HashSet + + /** + * @example + * + * ```ts + * // `data-first` API + * import { HashSet } from "effect" + * import assert from "node:assert/strict" + * + * // Create a set with initial values + * const immutableSet = HashSet.make(1, 2, 3) + * + * // Use mutate with data-first API + * const result = HashSet.mutate(immutableSet, (set) => { + * // The set is temporarily mutable inside this function + * HashSet.add(set, 4) + * HashSet.remove(set, 1) + * }) + * + * // The original set is unchanged + * assert.equal(Object.is(immutableSet, result), false) + * assert.deepStrictEqual( + * HashSet.toValues(immutableSet).sort(), + * [1, 2, 3] + * ) + * + * // The result contains the mutations + * assert.deepStrictEqual(HashSet.toValues(result).sort(), [2, 3, 4]) + * ``` + */ + (self: HashSet, f: (set: HashSet) => void): HashSet +} = HS.mutate + +/** + * Adds a value to the `HashSet`. + * + * Time complexity: **`O(1)`** average + * + * @remarks + * Remember that a `HashSet` is a collection of unique values, so adding a value + * that already exists in the `HashSet` will not add a duplicate. + * + * Remember that HashSet is an immutable data structure, so the `add` function, + * like all other functions that modify the HashSet, will return a new HashSet + * with the added value. + * @memberof HashSet + * @since 2.0.0 + * @example + * + * ```ts + * // Syntax + * import { HashSet, pipe } from "effect" + * + * // with data-last, a.k.a. pipeable API + * pipe(HashSet.empty(), HashSet.add(0), HashSet.add(0)) + * + * // or piped with the pipe function + * HashSet.empty().pipe(HashSet.add(0)) + * + * // or with data-first API + * HashSet.add(HashSet.empty(), 0) + * ``` + * + * @see Other `HashSet` mutations are {@link module:HashSet.remove} {@link module:HashSet.toggle} {@link module:HashSet.beginMutation} {@link module:HashSet.endMutation} {@link module:HashSet.mutate} + */ +export const add: { + /** + * @example + * + * ```ts + * // `data-last` a.k.a. `pipeable` API + * import { HashSet, pipe } from "effect" + * import assert from "node:assert/strict" + * + * assert.deepStrictEqual( + * pipe( + * HashSet.empty(), // HashSet.HashSet + * HashSet.add(0), + * HashSet.add(1), + * HashSet.add(1), + * HashSet.add(2), + * HashSet.toValues + * ), + * Array.of(0, 1, 2) + * ) + * ``` + */ + (value: A): (self: HashSet) => HashSet + + /** + * @example + * + * ```ts + * // `data-first` API + * import { HashSet, pipe } from "effect" + * import assert from "node:assert/strict" + * + * const empty = HashSet.empty() + * const withZero = HashSet.add(empty, 0) + * const withOne = HashSet.add(withZero, 1) + * const withTwo = HashSet.add(withOne, 2) + * const withTwoTwo = HashSet.add(withTwo, 2) + * + * assert.deepStrictEqual(HashSet.toValues(withTwoTwo), Array.of(0, 1, 2)) + * ``` + */ + (self: HashSet, value: A): HashSet +} = HS.add + +/** + * Removes a value from the `HashSet`. + * + * Time complexity: **`O(1)`** average + * + * @memberof HashSet + * @since 2.0.0 + * @example + * + * ```ts + * // Syntax + * import { HashSet, pipe } from "effect" + * + * // with `data-last`, a.k.a. `pipeable` API + * pipe(HashSet.make(0, 1, 2), HashSet.remove(0)) + * + * // or piped with the pipe function + * HashSet.make(0, 1, 2).pipe(HashSet.remove(0)) + * + * // or with `data-first` API + * HashSet.remove(HashSet.make(0, 1, 2), 0) + * ``` + * + * @see Other `HashSet` mutations are {@link module:HashSet.add} {@link module:HashSet.toggle} {@link module:HashSet.beginMutation} {@link module:HashSet.endMutation} {@link module:HashSet.mutate} + */ +export const remove: { + /** + * @example + * + * ```ts + * // `data-last` a.k.a. `pipeable` API + * import { HashSet, pipe } from "effect" + * import * as assert from "node:assert/strict" + * + * const set = HashSet.make(0, 1, 2) + * const result = pipe(set, HashSet.remove(0)) + * + * assert.equal(pipe(result, HashSet.has(0)), false) // it has correctly removed 0 + * assert.equal(pipe(set, HashSet.has(0)), true) // it does not mutate the original set + * assert.equal(pipe(result, HashSet.has(1)), true) + * assert.equal(pipe(result, HashSet.has(2)), true) + * ``` + */ + (value: A): (self: HashSet) => HashSet + + /** + * @example + * + * ```ts + * // `data-first` API + * import { HashSet, pipe } from "effect" + * import * as assert from "node:assert/strict" + * + * const set = HashSet.make(0, 1, 2) + * const result = HashSet.remove(set, 0) + * + * assert.equal(HashSet.has(result, 0), false) // it has correctly removed 0 + * assert.equal(HashSet.has(set, 0), true) // it does not mutate the original set + * assert.equal(HashSet.has(result, 1), true) + * assert.equal(HashSet.has(result, 2), true) + * ``` + */ + (self: HashSet, value: A): HashSet +} = HS.remove + +/** + * Computes the set difference `(A - B)` between this `HashSet` and the + * specified `Iterable`. + * + * Time complexity: **`O(n)`** where n is the number of elements in the set + * + * **NOTE**: the hash and equal of the values in both the set and the iterable + * must be the same; meaning we cannot compute a difference between a `HashSet + * of bananas` and a `HashSet of elephants` as they are not the same type and + * won't implement the Equal trait in the same way. + * + * @memberof HashSet + * @since 2.0.0 + * @example + * + * ```ts + * // Syntax + * import { HashSet, pipe } from "effect" + * + * // with data-last, a.k.a. pipeable API + * pipe(HashSet.make(1, 2, 3), HashSet.difference(HashSet.make(3, 4, 5))) + * + * // or piped with the pipe function + * HashSet.make(1, 2, 3).pipe(HashSet.difference(HashSet.make(3, 4, 5))) + * + * // or with data-first API + * HashSet.difference(HashSet.make(1, 2, 3), HashSet.make(3, 4, 5)) + * ``` + * + * @see Other `HashSet` operations are {@link module:HashSet.intersection} {@link module:HashSet.union} + */ +export const difference: { + /** + * @example + * + * ```ts + * // `data-last` a.k.a. `pipeable` API + * import { HashSet, pipe } from "effect" + * import * as assert from "node:assert/strict" + * + * // Create two sets with some overlapping elements + * const thisSet = HashSet.make(1, 2, 3) + * const thatIterable = HashSet.make(3, 4, 5) + * + * // Compute the difference (elements in thisSet that are not in thatIterable) + * const result = pipe(thisSet, HashSet.difference(thatIterable)) + * + * // The result contains only elements from thisSet that are not in thatIterable + * assert.deepStrictEqual(HashSet.toValues(result).sort(), [1, 2]) + * + * // The original sets are unchanged + * assert.deepStrictEqual(HashSet.toValues(thisSet).sort(), [1, 2, 3]) + * assert.deepStrictEqual( + * HashSet.toValues(thatIterable).sort(), + * [3, 4, 5] + * ) + * + * // You can also use arrays or other iterables + * const diffWithArray = pipe(thisSet, HashSet.difference([3, 4])) + * assert.deepStrictEqual(HashSet.toValues(diffWithArray).sort(), [1, 2]) + * ``` + */ + (that: Iterable): (self: HashSet) => HashSet + + /** + * @example + * + * ```ts + * // `data-first` API + * import { HashSet } from "effect" + * import * as assert from "node:assert/strict" + * + * // Create two sets with some overlapping elements + * const thisSet = HashSet.make(1, 2, 3) + * const thatIterable = HashSet.make(3, 4, 5) + * + * // Compute the difference using data-first API + * const result = HashSet.difference(thisSet, thatIterable) + * + * // The result contains only elements from thisSet that are not in thatIterable + * assert.deepStrictEqual(HashSet.toValues(result).sort(), [1, 2]) + * + * // The original sets are unchanged + * assert.deepStrictEqual(HashSet.toValues(thisSet).sort(), [1, 2, 3]) + * assert.deepStrictEqual( + * HashSet.toValues(thatIterable).sort(), + * [3, 4, 5] + * ) + * + * // You can also compute the difference in the other direction + * const reverseResult = HashSet.difference(thatIterable, thisSet) + * assert.deepStrictEqual(HashSet.toValues(reverseResult).sort(), [4, 5]) + * ``` + */ + (self: HashSet, that: Iterable): HashSet +} = HS.difference + +/** + * Returns a `HashSet` of values which are present in both this set and that + * `Iterable`. Computes set intersection (A ∩ B) + * + * Time complexity: **`O(n)`** where n is the number of elements in the smaller + * set + * + * **NOTE**: the hash and equal of the values in both the set and the iterable + * must be the same. + * + * @memberof HashSet + * @since 2.0.0 + * @example + * + * ```ts + * // Syntax + * import { HashSet, pipe } from "effect" + * + * // with data-last, a.k.a. pipeable API + * pipe(HashSet.make(1, 2, 3), HashSet.intersection(HashSet.make(2, 3, 4))) + * + * // or piped with the pipe function + * HashSet.make(1, 2, 3).pipe(HashSet.intersection(HashSet.make(2, 3, 4))) + * + * // or with data-first API + * HashSet.intersection(HashSet.make(1, 2, 3), HashSet.make(2, 3, 4)) + * ``` + * + * @see Other `HashSet` operations are {@link module:HashSet.difference} {@link module:HashSet.union} + */ +export const intersection: { + /** + * @example + * + * ```ts + * // `data-last` a.k.a. `pipeable` API + * import { HashSet, pipe } from "effect" + * import * as assert from "node:assert/strict" + * + * // Create two sets with some overlapping elements + * const set1 = HashSet.make(1, 2, 3) + * const set2 = HashSet.make(2, 3, 4) + * + * // Compute the intersection (elements that are in both sets) + * const result = pipe(set1, HashSet.intersection(set2)) + * + * // The result contains only elements that are in both sets + * assert.deepStrictEqual(HashSet.toValues(result).sort(), [2, 3]) + * + * // The original sets are unchanged + * assert.deepStrictEqual(HashSet.toValues(set1).sort(), [1, 2, 3]) + * assert.deepStrictEqual(HashSet.toValues(set2).sort(), [2, 3, 4]) + * + * // You can also use arrays or other iterables + * const intersectWithArray = pipe(set1, HashSet.intersection([2, 3, 5])) + * assert.deepStrictEqual( + * HashSet.toValues(intersectWithArray).sort(), + * [2, 3] + * ) + * ``` + */ + (that: Iterable): (self: HashSet) => HashSet + + /** + * @example + * + * ```ts + * // `data-first` API + * import { HashSet } from "effect" + * import * as assert from "node:assert/strict" + * + * // Create two sets with some overlapping elements + * const set1 = HashSet.make(1, 2, 3) + * const set2 = HashSet.make(2, 3, 4) + * + * // Compute the intersection using data-first API + * const result = HashSet.intersection(set1, set2) + * + * // The result contains only elements that are in both sets + * assert.deepStrictEqual(HashSet.toValues(result).sort(), [2, 3]) + * + * // The original sets are unchanged + * assert.deepStrictEqual(HashSet.toValues(set1).sort(), [1, 2, 3]) + * assert.deepStrictEqual(HashSet.toValues(set2).sort(), [2, 3, 4]) + * + * // You can also use arrays or other iterables + * const intersectWithArray = HashSet.intersection(set1, [2, 3, 5]) + * assert.deepStrictEqual( + * HashSet.toValues(intersectWithArray).sort(), + * [2, 3] + * ) + * ``` + */ + (self: HashSet, that: Iterable): HashSet +} = HS.intersection + +/** + * Computes the set union `( self ∪ that )` between this `HashSet` and the + * specified `Iterable`. + * + * Time complexity: **`O(n)`** where n is the number of elements in the set + * + * **NOTE**: the hash and equal of the values in both the set and the iterable + * must be the same. + * + * @memberof HashSet + * @since 2.0.0 + * @example + * + * ```ts + * // Syntax + * import { HashSet, pipe } from "effect" + * + * // with data-last, a.k.a. pipeable API + * pipe(HashSet.make(1, 2, 3), HashSet.union(HashSet.make(3, 4, 5))) + * + * // or piped with the pipe function + * HashSet.make(1, 2, 3).pipe(HashSet.union(HashSet.make(3, 4, 5))) + * + * // or with data-first API + * HashSet.union(HashSet.make(1, 2, 3), HashSet.make(3, 4, 5)) + * ``` + * + * @see Other `HashSet` operations are {@link module:HashSet.difference} {@link module:HashSet.intersection} + */ +export const union: { + /** + * @example + * + * ```ts + * // `data-last` a.k.a. `pipeable` API + * import { HashSet, pipe } from "effect" + * import * as assert from "node:assert/strict" + * + * // Create two sets with some overlapping elements + * const selfSet = HashSet.make(1, 2, 3) + * const thatIterable = HashSet.make(3, 4, 5) + * + * // Compute the union (all elements from both sets) + * const result = pipe(selfSet, HashSet.union(thatIterable)) + * + * // The result contains all elements from both sets (without duplicates) + * assert.deepStrictEqual( + * HashSet.toValues(result).sort(), + * [1, 2, 3, 4, 5] + * ) + * + * // The original sets are unchanged + * assert.deepStrictEqual(HashSet.toValues(selfSet).sort(), [1, 2, 3]) + * assert.deepStrictEqual( + * HashSet.toValues(thatIterable).sort(), + * [3, 4, 5] + * ) + * + * // You can also use arrays or other iterables + * const unionWithArray = pipe(selfSet, HashSet.union([4, 5, 6])) + * assert.deepStrictEqual( + * HashSet.toValues(unionWithArray).sort(), + * [1, 2, 3, 4, 5, 6] + * ) + * ``` + */ + (that: Iterable): (self: HashSet) => HashSet + + /** + * @example + * + * ```ts + * // `data-first` API + * import { HashSet } from "effect" + * import * as assert from "node:assert/strict" + * + * // Create two sets with some overlapping elements + * const selfSet = HashSet.make(1, 2, 3) + * const thatIterable = HashSet.make(3, 4, 5) + * + * // Compute the union using data-first API + * const result = HashSet.union(selfSet, thatIterable) + * + * // The result contains all elements from both sets (without duplicates) + * assert.deepStrictEqual( + * HashSet.toValues(result).sort(), + * [1, 2, 3, 4, 5] + * ) + * + * // The original sets are unchanged + * assert.deepStrictEqual(HashSet.toValues(selfSet).sort(), [1, 2, 3]) + * assert.deepStrictEqual( + * HashSet.toValues(thatIterable).sort(), + * [3, 4, 5] + * ) + * + * // You can also use arrays or other iterables + * const unionWithArray = HashSet.union(selfSet, [4, 5, 6]) + * assert.deepStrictEqual( + * HashSet.toValues(unionWithArray).sort(), + * [1, 2, 3, 4, 5, 6] + * ) + * ``` + */ + (self: HashSet, that: Iterable): HashSet +} = HS.union + +/** + * Checks if a value is present in the `HashSet`. If it is present, the value + * will be removed from the `HashSet`, otherwise the value will be added to the + * `HashSet`. + * + * Time complexity: **`O(1)`** average + * + * @memberof HashSet + * @since 2.0.0 + * @example + * + * ```ts + * // Syntax + * import { HashSet, pipe } from "effect" + * + * // with `data-last`, a.k.a. `pipeable` API + * pipe(HashSet.make(0, 1, 2), HashSet.toggle(0)) + * + * // or piped with the pipe function + * HashSet.make(0, 1, 2).pipe(HashSet.toggle(0)) + * + * // or with `data-first` API + * HashSet.toggle(HashSet.make(0, 1, 2), 0) + * ``` + * + * @returns A new `HashSet` where the toggled value is being either added or + * removed based on the initial `HashSet` state. + * @see Other `HashSet` mutations are {@link module:HashSet.add} {@link module:HashSet.remove} {@link module:HashSet.beginMutation} {@link module:HashSet.endMutation} {@link module:HashSet.mutate} + */ +export const toggle: { + /** + * @example + * + * ```ts + * // `data-last` a.k.a. `pipeable` API + * import { HashSet, pipe } from "effect" + * import assert from "node:assert/strict" + * + * // arrange + * let set = HashSet.make(0, 1, 2) + * + * // assert 1: 0 is in the set + * assert.equal(pipe(set, HashSet.has(0)), true) + * + * // act 2: toggle 0 once on the set + * set = pipe(set, HashSet.toggle(0)) + * + * // assert 2: 0 is not in the set any longer + * assert.equal(pipe(set, HashSet.has(0)), false) + * + * // act 3: toggle 0 once again on the set + * set = pipe(set, HashSet.toggle(0)) + * + * // assert 3: 0 in now back in the set + * assert.equal(pipe(set, HashSet.has(0)), true) + * ``` + */ + (value: A): (self: HashSet) => HashSet + + /** + * @example + * + * ```ts + * // `data-first` API + * import { HashSet, pipe } from "effect" + * import assert from "node:assert/strict" + * + * // arrange + * let set = HashSet.make(0, 1, 2) + * + * // assert 1: 0 is in the set + * assert.equal(HashSet.has(set, 0), true) + * + * // act 2: toggle 0 once on the set + * set = HashSet.toggle(set, 0) + * + * // assert 2: 0 is not in the set any longer + * assert.equal(HashSet.has(set, 0), false) + * + * // act 3: toggle 0 once again on the set + * set = HashSet.toggle(set, 0) + * + * // assert 3: 0 in now back in the set + * assert.equal(HashSet.has(set, 0), true) + * ``` + */ + (self: HashSet, value: A): HashSet +} = HS.toggle + +/** + * Maps over the values of the `HashSet` using the specified function. + * + * The time complexity is of **`O(n)`**. + * + * @memberof HashSet + * @since 2.0.0 + * @category mapping + * @example + * + * ```ts + * // Syntax + * import { HashSet, pipe } from "effect" + * + * // with `data-last`, a.k.a. `pipeable` API + * pipe( + * HashSet.make(0, 1, 2), // HashSet.HashSet + * HashSet.map(String) // HashSet.HashSet + * ) + * + * // or piped with the pipe method + * HashSet.make(0, 1, 2).pipe(HashSet.map(String)) + * + * // or with `data-first` API + * HashSet.map(HashSet.make(0, 1, 2), String) + * ``` + */ +export const map: { + /** + * @example + * + * ```ts + * import { HashSet, pipe } from "effect" + * import * as assert from "node:assert/strict" + * + * assert.deepStrictEqual( + * pipe( + * HashSet.make(0, 1, 2), // HashSet.HashSet + * HashSet.map((n) => String(n + 1)) // HashSet.HashSet + * ), + * HashSet.make("1", "2", "3") + * ) + * ``` + */ + (f: (a: A) => B): (self: HashSet) => HashSet + + /** + * @example + * + * ```ts + * import { HashSet, pipe } from "effect" + * import * as assert from "node:assert/strict" + * + * assert.deepStrictEqual( + * HashSet.map( + * HashSet.make(0, 1, 2), // HashSet.HashSet + * (n) => String(n + 1) + * ), // HashSet.HashSet + * HashSet.make("1", "2", "3") + * ) + * ``` + */ + (self: HashSet, f: (a: A) => B): HashSet +} = HS.map + +/** + * Chains over the values of the `HashSet` using the specified function. + * + * The time complexity is of **`O(n)`**. + * + * @memberof HashSet + * @since 2.0.0 + * @category sequencing + * @example + * + * ```ts + * // Syntax + * import { HashSet, pipe } from "effect" + * + * // with `data-last`, a.k.a. `pipeable` API + * pipe( + * HashSet.make(0, 1, 2), // HashSet.HashSet + * HashSet.flatMap((n) => Array.of(String(n))) // HashSet.HashSet + * ) + * + * // or piped with the pipe method + * HashSet.make(0, 1, 2) // HashSet.HashSet + * .pipe( + * HashSet.flatMap((n) => Array.of(String(n))) // HashSet.HashSet + * ) + * + * // or with `data-first` API + * HashSet.flatMap(HashSet.make(0, 1, 2), (n) => Array.of(String(n))) + * ``` + */ +export const flatMap: { + /** + * @example + * + * ```ts + * import { HashSet, pipe, List } from "effect" + * import * as assert from "node:assert/strict" + * + * assert.deepStrictEqual( + * pipe( + * HashSet.make(0, 1, 2), + * HashSet.flatMap((n) => List.of(String(n * n))) // needs to return an Iterable + * ), + * HashSet.make("0", "1", "4") + * ) + * ``` + */ + (f: (a: A) => Iterable): (self: HashSet) => HashSet + + /** + * @example + * + * ```ts + * import { HashSet, pipe, List } from "effect" + * import * as assert from "node:assert/strict" + * + * assert.deepStrictEqual( + * HashSet.flatMap(HashSet.make(0, 1, 2), (n) => + * List.of(String(n * n * n)) + * ), // needs to return an Iterable + * HashSet.make("0", "1", "8") + * ) + * ``` + */ + (self: HashSet, f: (a: A) => Iterable): HashSet +} = HS.flatMap + +/** + * Applies the specified function to the values of the `HashSet`. + * + * The time complexity is of **`O(n)`**. + * + * @memberof HashSet + * @since 2.0.0 + * @category traversing + * @example + * + * ```ts + * // Syntax + * import { HashSet, pipe } from "effect" + * + * // with `data-last`, a.k.a. `pipeable` API + * pipe(HashSet.make(0, 1, 2), HashSet.forEach(console.log)) // logs: 0 1 2 + * + * // or piped with the pipe method + * HashSet.make(0, 1, 2).pipe(HashSet.forEach(console.log)) // logs: 0 1 2 + * + * // or with `data-first` API + * HashSet.forEach(HashSet.make(0, 1, 2), console.log) // logs: 0 1 2 + * ``` + */ +export const forEach: { + /** + * @example + * + * ```ts + * import { HashSet, pipe } from "effect" + * import * as assert from "node:assert/strict" + * + * const result: Array = [] + * + * pipe( + * HashSet.make(0, 1, 2), + * HashSet.forEach((n): void => { + * result.push(n) + * }) + * ) + * + * assert.deepStrictEqual(result, [0, 1, 2]) + * ``` + */ + (f: (value: A) => void): (self: HashSet) => void + + /** + * @example + * + * ```ts + * import { HashSet, pipe } from "effect" + * import * as assert from "node:assert/strict" + * + * const result: Array = [] + * + * HashSet.forEach(HashSet.make(0, 1, 2), (n): void => { + * result.push(n) + * }) + * + * assert.deepStrictEqual(result, [0, 1, 2]) + * ``` + */ + (self: HashSet, f: (value: A) => void): void +} = HS.forEach + +/** + * Reduces the specified state over the values of the `HashSet`. + * + * The time complexity is of **`O(n)`**. + * + * @memberof HashSet + * @since 2.0.0 + * @category folding + * @example + * + * ```ts + * // Syntax + * import { HashSet, pipe } from "effect" + * + * const sum = (a: number, b: number): number => a + b + * + * // with `data-last`, a.k.a. `pipeable` API + * pipe(HashSet.make(0, 1, 2), HashSet.reduce(0, sum)) + * + * // or with the pipe method + * HashSet.make(0, 1, 2).pipe(HashSet.reduce(0, sum)) + * + * // or with `data-first` API + * HashSet.reduce(HashSet.make(0, 1, 2), 0, sum) + * ``` + */ +export const reduce: { + /** + * @example + * + * ```ts + * import { HashSet, pipe } from "effect" + * import * as assert from "node:assert/strict" + * + * assert.equal( + * pipe( + * HashSet.make(0, 1, 2), + * HashSet.reduce(10, (accumulator, value) => accumulator + value) + * ), + * 13 + * ) + * ``` + */ + (zero: Z, f: (accumulator: Z, value: A) => Z): (self: HashSet) => Z + + /** + * @example + * + * ```ts + * import { HashSet } from "effect" + * import * as assert from "node:assert/strict" + * + * assert.equal( + * HashSet.reduce( + * HashSet.make(0, 1, 2), + * -3, + * (accumulator, value) => accumulator + value + * ), + * 0 + * ) + * ``` + */ + (self: HashSet, zero: Z, f: (accumulator: Z, value: A) => Z): Z +} = HS.reduce + +/** + * Filters values out of a `HashSet` using the specified predicate. + * + * The time complexity is of **`O(n)`**. + * + * @memberof HashSet + * @since 2.0.0 + * @category filtering + * @example + * + * ```ts + * // Syntax with Predicate + * import { HashSet, type Predicate, pipe } from "effect" + * + * const filterPositiveNumbers: Predicate.Predicate = (n) => n > 0 + * + * // with `data-last`, a.k.a. `pipeable` API + * pipe( + * HashSet.make(-2, -1, 0, 1, 2), + * HashSet.filter(filterPositiveNumbers) + * ) + * + * // or with the pipe method + * HashSet.make(-2, -1, 0, 1, 2).pipe(HashSet.filter(filterPositiveNumbers)) + * + * // or with `data-first` API + * HashSet.filter(HashSet.make(-2, -1, 0, 1, 2), filterPositiveNumbers) + * ``` + * + * @example + * + * ```ts + * /// Syntax with Refinement + * import { HashSet, pipe } from "effect" + * + * const stringRefinement = (value: unknown): value is string => + * typeof value === "string" + * + * // with `data-last`, a.k.a. `pipeable` API + * pipe( + * HashSet.make(1, "unos", 2, "two", 3, "trois", 4, "vier"), // // HashSet.HashSet + * HashSet.filter(stringRefinement) + * ) // HashSet.HashSet + * + * // or with the pipe method + * HashSet.make(1, "unos", 2, "two", 3, "trois", 4, "vier") // HashSet.HashSet + * .pipe(HashSet.filter(stringRefinement)) // HashSet.HashSet + * + * // or with `data-first` API + * HashSet.filter( + * HashSet.make(1, "unos", 2, "two", 3, "trois", 4, "vier"), // HashSet.HashSet + * stringRefinement + * ) // HashSet.HashSet + * ``` + */ +export const filter: { + /** + * @example + * + * ```ts + * import { HashSet, pipe, Predicate } from "effect" + * import * as assert from "node:assert/strict" + * + * const numbersAndStringsHashSet: HashSet.HashSet = + * HashSet.make(1, "unos", 2, "two", 3, "trois", 4, "vier") + * + * const stringRefinement: Predicate.Refinement< + * string | number, + * string + * > = (value) => typeof value === "string" + * + * const stringHashSet: HashSet.HashSet = pipe( + * numbersAndStringsHashSet, + * HashSet.filter(stringRefinement) + * ) + * + * assert.equal( + * pipe(stringHashSet, HashSet.every(Predicate.isString)), + * true + * ) + * ``` + */ + ( + refinement: Refinement, B> + ): (self: HashSet) => HashSet + + /** + * @example + * + * ```ts + * import { HashSet, pipe, type Predicate } from "effect" + * import * as assert from "node:assert/strict" + * + * const filterPositiveNumbers: Predicate.Predicate = (n) => n > 0 + * + * assert.deepStrictEqual( + * pipe( + * HashSet.make(-2, -1, 0, 1, 2), + * HashSet.filter(filterPositiveNumbers) + * ), + * HashSet.make(1, 2) + * ) + * ``` + */ + (predicate: Predicate>): (self: HashSet) => HashSet + + /** + * @example + * + * ```ts + * import { HashSet, Predicate } from "effect" + * import * as assert from "node:assert/strict" + * + * const numbersAndStringsHashSet: HashSet.HashSet = + * HashSet.make(1, "unos", 2, "two", 3, "trois", 4, "vier") + * + * const stringRefinement: Predicate.Refinement< + * string | number, + * string + * > = (value) => typeof value === "string" + * + * const stringHashSet: HashSet.HashSet = HashSet.filter( + * numbersAndStringsHashSet, + * stringRefinement + * ) + * + * assert.equal(HashSet.every(stringHashSet, Predicate.isString), true) + * ``` + */ + ( + self: HashSet, + refinement: Refinement + ): HashSet + + /** + * @example + * + * ```ts + * import { HashSet, pipe, type Predicate } from "effect" + * import * as assert from "node:assert/strict" + * + * const filterPositiveNumbers: Predicate.Predicate = (n) => n > 0 + * + * assert.deepStrictEqual( + * HashSet.filter(HashSet.make(-2, -1, 0, 1, 2), filterPositiveNumbers), + * HashSet.make(1, 2) + * ) + * ``` + */ + (self: HashSet, predicate: Predicate): HashSet +} = HS.filter + +/** + * Partition the values of a `HashSet` using the specified predicate. + * + * If a value matches the predicate, it will be placed into the `HashSet` on the + * right side of the resulting `Tuple`, otherwise the value will be placed into + * the left side. + * + * Time complexity is of **`O(n)`**. + * + * @memberof HashSet + * @since 2.0.0 + * @category partitioning + * @example + * + * ```ts + * // Syntax with Predicate + * import { HashSet, pipe, Predicate } from "effect" + * + * // with `data-last`, a.k.a. `pipeable` API + * pipe( + * HashSet.make(0, 1, 2, 3, 4, 5), + * HashSet.partition((n) => n % 2 === 0) + * ) + * + * // or with the pipe method + * HashSet.make(0, 1, 2, 3, 4, 5).pipe( + * HashSet.partition((n) => n % 2 === 0) + * ) + * + * // or with `data-first` API + * HashSet.partition(HashSet.make(0, 1, 2, 3, 4, 5), (n) => n % 2 === 0) + * ``` + * + * @example + * + * ```ts + * // Syntax with Refinement + * import { HashSet, pipe, Predicate } from "effect" + * + * const stringRefinement: Predicate.Refinement = ( + * value + * ) => typeof value === "string" + * + * // with `data-last`, a.k.a. `pipeable` API + * pipe( + * HashSet.make(1, "unos", 2, "two", 3, "trois", 4, "vier"), + * HashSet.partition(stringRefinement) + * ) + * + * // or with the pipe method + * HashSet.make(1, "unos", 2, "two", 3, "trois", 4, "vier").pipe( + * HashSet.partition(stringRefinement) + * ) + * + * // or with `data-first` API + * HashSet.partition( + * HashSet.make(1, "unos", 2, "two", 3, "trois", 4, "vier"), + * stringRefinement + * ) + * ``` + */ +export const partition: { + /** + * @example + * + * ```ts + * import { HashSet, pipe, Predicate } from "effect" + * import * as assert from "node:assert/strict" + * + * const numbersAndStringsHashSet: HashSet.HashSet = + * HashSet.make(1, "unos", 2, "two", 3, "trois", 4, "vier") + * + * const stringRefinement: Predicate.Refinement< + * string | number, + * string + * > = (value) => typeof value === "string" + * + * const [ + * excluded, // HashSet.HashSet + * satisfying // HashSet.HashSet + * ] = pipe(numbersAndStringsHashSet, HashSet.partition(stringRefinement)) + * + * assert.equal(pipe(satisfying, HashSet.every(Predicate.isString)), true) + * assert.equal(pipe(excluded, HashSet.every(Predicate.isNumber)), true) + * + * assert.deepStrictEqual(excluded, HashSet.make(1, 2, 3, 4)) + * assert.deepStrictEqual( + * satisfying, + * HashSet.make("unos", "two", "trois", "vier") + * ) + * ``` + */ + ( + refinement: Refinement, B> + ): ( + self: HashSet + ) => [excluded: HashSet>, satisfying: HashSet] + + /** + * @example + * + * ```ts + * import { HashSet, pipe } from "effect" + * import * as assert from "node:assert/strict" + * + * const [excluded, satisfying] = pipe( + * HashSet.make(0, 1, 2, 3, 4, 5), + * HashSet.partition((n) => n % 2 === 0) + * ) + * + * assert.deepStrictEqual(excluded, HashSet.make(1, 3, 5)) + * assert.deepStrictEqual(satisfying, HashSet.make(0, 2, 4)) + * ``` + */ + ( + predicate: Predicate> + ): (self: HashSet) => [excluded: HashSet, satisfying: HashSet] + + /** + * @example + * + * ```ts + * import { HashSet, pipe, Predicate } from "effect" + * import * as assert from "node:assert/strict" + * + * const numbersAndStringsHashSet: HashSet.HashSet = + * HashSet.make(1, "unos", 2, "two", 3, "trois", 4, "vier") + * + * const stringRefinement: Predicate.Refinement< + * string | number, + * string + * > = (value) => typeof value === "string" + * + * const [ + * excluded, // HashSet.HashSet + * satisfying // HashSet.HashSet + * ] = HashSet.partition(numbersAndStringsHashSet, stringRefinement) + * + * assert.equal(HashSet.every(satisfying, Predicate.isString), true) + * assert.equal(HashSet.every(excluded, Predicate.isNumber), true) + * + * assert.deepStrictEqual(excluded, HashSet.make(1, 2, 3, 4)) + * assert.deepStrictEqual( + * satisfying, + * HashSet.make("unos", "two", "trois", "vier") + * ) + * ``` + */ + ( + self: HashSet, + refinement: Refinement + ): [excluded: HashSet>, satisfying: HashSet] + + /** + * @example + * + * ```ts + * import { HashSet } from "effect" + * import * as assert from "node:assert/strict" + * + * const [excluded, satisfying] = HashSet.partition( + * HashSet.make(0, 1, 2, 3, 4, 5), + * (n) => n % 2 === 0 + * ) + * + * assert.deepStrictEqual(excluded, HashSet.make(1, 3, 5)) + * assert.deepStrictEqual(satisfying, HashSet.make(0, 2, 4)) + * ``` + */ + ( + self: HashSet, + predicate: Predicate + ): [excluded: HashSet, satisfying: HashSet] +} = HS.partition diff --git a/repos/effect/packages/effect/src/Inspectable.ts b/repos/effect/packages/effect/src/Inspectable.ts new file mode 100644 index 0000000..9bab2d1 --- /dev/null +++ b/repos/effect/packages/effect/src/Inspectable.ts @@ -0,0 +1,287 @@ +/** + * @since 2.0.0 + */ +import type * as FiberRefs from "./FiberRefs.js" +import { globalValue } from "./GlobalValue.js" +import * as Predicate from "./Predicate.js" + +/** + * @since 2.0.0 + * @category symbols + */ +export const NodeInspectSymbol = Symbol.for("nodejs.util.inspect.custom") + +/** + * @since 2.0.0 + * @category symbols + */ +export type NodeInspectSymbol = typeof NodeInspectSymbol + +/** + * @since 2.0.0 + * @category models + */ +export interface Inspectable { + toString(): string + toJSON(): unknown + [NodeInspectSymbol](): unknown +} + +/** + * @since 2.0.0 + */ +export const toJSON = (x: unknown): unknown => { + try { + if ( + Predicate.hasProperty(x, "toJSON") && Predicate.isFunction(x["toJSON"]) && + x["toJSON"].length === 0 + ) { + return x.toJSON() + } else if (Array.isArray(x)) { + return x.map(toJSON) + } + } catch { + return {} + } + return redact(x) +} + +const CIRCULAR = "[Circular]" + +/** @internal */ +export function formatDate(date: Date): string { + try { + return date.toISOString() + } catch { + return "Invalid Date" + } +} + +function safeToString(input: any): string { + try { + const s = input.toString() + return typeof s === "string" ? s : String(s) + } catch { + return "[toString threw]" + } +} + +/** @internal */ +export function formatPropertyKey(name: PropertyKey): string { + return Predicate.isString(name) ? JSON.stringify(name) : String(name) +} + +/** @internal */ +export function formatUnknown( + input: unknown, + options?: { + readonly space?: number | string | undefined + readonly ignoreToString?: boolean | undefined + } +): string { + const space = options?.space ?? 0 + const seen = new WeakSet() + const gap = !space ? "" : (Predicate.isNumber(space) ? " ".repeat(space) : space) + const ind = (d: number) => gap.repeat(d) + + const wrap = (v: unknown, body: string): string => { + const ctor = (v as any)?.constructor + return ctor && ctor !== Object.prototype.constructor && ctor.name ? `${ctor.name}(${body})` : body + } + + const ownKeys = (o: object): Array => { + try { + return Reflect.ownKeys(o) + } catch { + return ["[ownKeys threw]"] + } + } + + function go(v: unknown, d = 0): string { + if (Array.isArray(v)) { + if (seen.has(v)) return CIRCULAR + seen.add(v) + if (!gap || v.length <= 1) return `[${v.map((x) => go(x, d)).join(",")}]` + const inner = v.map((x) => go(x, d + 1)).join(",\n" + ind(d + 1)) + return `[\n${ind(d + 1)}${inner}\n${ind(d)}]` + } + + if (Predicate.isDate(v)) return formatDate(v) + + if ( + !options?.ignoreToString && + Predicate.hasProperty(v, "toString") && + Predicate.isFunction(v["toString"]) && + v["toString"] !== Object.prototype.toString && + v["toString"] !== Array.prototype.toString + ) { + const s = safeToString(v) + if (v instanceof Error && v.cause) { + return `${s} (cause: ${go(v.cause, d)})` + } + return s + } + + if (Predicate.isString(v)) return JSON.stringify(v) + + if ( + Predicate.isNumber(v) || + v == null || + Predicate.isBoolean(v) || + Predicate.isSymbol(v) + ) return String(v) + + if (Predicate.isBigInt(v)) return String(v) + "n" + + if (v instanceof Set || v instanceof Map) { + if (seen.has(v)) return CIRCULAR + seen.add(v) + return `${v.constructor.name}(${go(Array.from(v), d)})` + } + + if (Predicate.isObject(v)) { + if (seen.has(v)) return CIRCULAR + seen.add(v) + const keys = ownKeys(v) + if (!gap || keys.length <= 1) { + const body = `{${keys.map((k) => `${formatPropertyKey(k)}:${go((v as any)[k], d)}`).join(",")}}` + return wrap(v, body) + } + const body = `{\n${ + keys.map((k) => `${ind(d + 1)}${formatPropertyKey(k)}: ${go((v as any)[k], d + 1)}`).join(",\n") + }\n${ind(d)}}` + return wrap(v, body) + } + + return String(v) + } + + return go(input, 0) +} + +/** + * @since 2.0.0 + */ +export const format = (x: unknown): string => JSON.stringify(x, null, 2) + +/** + * @since 2.0.0 + */ +export const BaseProto: Inspectable = { + toJSON() { + return toJSON(this) + }, + [NodeInspectSymbol]() { + return this.toJSON() + }, + toString() { + return format(this.toJSON()) + } +} + +/** + * @since 2.0.0 + */ +export abstract class Class { + /** + * @since 2.0.0 + */ + abstract toJSON(): unknown + /** + * @since 2.0.0 + */ + [NodeInspectSymbol]() { + return this.toJSON() + } + /** + * @since 2.0.0 + */ + toString() { + return format(this.toJSON()) + } +} + +/** + * @since 2.0.0 + */ +export const toStringUnknown = (u: unknown, whitespace: number | string | undefined = 2): string => { + if (typeof u === "string") { + return u + } + try { + return typeof u === "object" ? stringifyCircular(u, whitespace) : String(u) + } catch { + return String(u) + } +} + +/** + * @since 2.0.0 + */ +export const stringifyCircular = (obj: unknown, whitespace?: number | string | undefined): string => { + let cache: Array = [] + const retVal = JSON.stringify( + obj, + (_key, value) => + typeof value === "object" && value !== null + ? cache.includes(value) + ? undefined // circular reference + : cache.push(value) && (redactableState.fiberRefs !== undefined && isRedactable(value) + ? value[symbolRedactable](redactableState.fiberRefs) + : value) + : value, + whitespace + ) + ;(cache as any) = undefined + return retVal +} + +/** + * @since 3.10.0 + * @category redactable + */ +export interface Redactable { + readonly [symbolRedactable]: (fiberRefs: FiberRefs.FiberRefs) => unknown +} + +/** + * @since 3.10.0 + * @category redactable + */ +export const symbolRedactable: unique symbol = Symbol.for("effect/Inspectable/Redactable") + +/** + * @since 3.10.0 + * @category redactable + */ +export const isRedactable = (u: unknown): u is Redactable => + typeof u === "object" && u !== null && symbolRedactable in u + +const redactableState = globalValue("effect/Inspectable/redactableState", () => ({ + fiberRefs: undefined as FiberRefs.FiberRefs | undefined +})) + +/** + * @since 3.10.0 + * @category redactable + */ +export const withRedactableContext = (context: FiberRefs.FiberRefs, f: () => A): A => { + const prev = redactableState.fiberRefs + redactableState.fiberRefs = context + try { + return f() + } finally { + redactableState.fiberRefs = prev + } +} + +/** + * @since 3.10.0 + * @category redactable + */ +export const redact = (u: unknown): unknown => { + if (isRedactable(u) && redactableState.fiberRefs !== undefined) { + return u[symbolRedactable](redactableState.fiberRefs) + } + return u +} diff --git a/repos/effect/packages/effect/src/Iterable.ts b/repos/effect/packages/effect/src/Iterable.ts new file mode 100644 index 0000000..ef6e742 --- /dev/null +++ b/repos/effect/packages/effect/src/Iterable.ts @@ -0,0 +1,1119 @@ +/** + * This module provides utility functions for working with Iterables in TypeScript. + * + * @since 2.0.0 + */ + +import type { NonEmptyArray } from "./Array.js" +import type { Either } from "./Either.js" +import * as E from "./Either.js" +import * as Equal from "./Equal.js" +import { dual, identity } from "./Function.js" +import type { Option } from "./Option.js" +import * as O from "./Option.js" +import { isBoolean } from "./Predicate.js" +import type * as Record from "./Record.js" +import * as Tuple from "./Tuple.js" +import type { NoInfer } from "./Types.js" + +/** + * Return a `Iterable` with element `i` initialized with `f(i)`. + * + * If the `length` is not specified, the `Iterable` will be infinite. + * + * **Note**. `length` is normalized to an integer >= 1. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { makeBy } from "effect/Iterable" + * + * assert.deepStrictEqual(Array.from(makeBy(n => n * 2, { length: 5 })), [0, 2, 4, 6, 8]) + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const makeBy = (f: (i: number) => A, options?: { + readonly length?: number +}): Iterable => { + const max = options?.length !== undefined ? Math.max(1, Math.floor(options.length)) : Infinity + return { + [Symbol.iterator]() { + let i = 0 + return { + next(): IteratorResult { + if (i < max) { + return { value: f(i++), done: false } + } + return { done: true, value: undefined } + } + } + } + } +} + +/** + * Return a `Iterable` containing a range of integers, including both endpoints. + * + * If `end` is omitted, the range will not have an upper bound. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { range } from "effect/Iterable" + * + * assert.deepStrictEqual(Array.from(range(1, 3)), [1, 2, 3]) + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const range = (start: number, end?: number): Iterable => { + if (end === undefined) { + return makeBy((i) => start + i) + } + return makeBy((i) => start + i, { + length: start <= end ? end - start + 1 : 1 + }) +} + +/** + * Return a `Iterable` containing a value repeated the specified number of times. + * + * **Note**. `n` is normalized to an integer >= 1. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { replicate } from "effect/Iterable" + * + * assert.deepStrictEqual(Array.from(replicate("a", 3)), ["a", "a", "a"]) + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const replicate: { + (n: number): (a: A) => Iterable + (a: A, n: number): Iterable +} = dual(2, (a: A, n: number): Iterable => makeBy(() => a, { length: n })) + +/** + * Takes a record and returns an Iterable of tuples containing its keys and values. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { fromRecord } from "effect/Iterable" + * + * const x = { a: 1, b: 2, c: 3 } + * assert.deepStrictEqual(Array.from(fromRecord(x)), [["a", 1], ["b", 2], ["c", 3]]) + * ``` + * + * @category conversions + * @since 2.0.0 + */ +export const fromRecord = (self: Readonly>): Iterable<[K, A]> => ({ + *[Symbol.iterator]() { + for (const key in self) { + if (Object.prototype.hasOwnProperty.call(self, key)) { + yield [key, self[key]] + } + } + } +}) + +/** + * Prepend an element to the front of an `Iterable`, creating a new `Iterable`. + * + * @category concatenating + * @since 2.0.0 + */ +export const prepend: { + (head: B): (self: Iterable) => Iterable + (self: Iterable, head: B): Iterable +} = dual(2, (self: Iterable, head: B): Iterable => prependAll(self, [head])) + +/** + * Prepends the specified prefix iterable to the beginning of the specified iterable. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { Iterable } from "effect" + * + * assert.deepStrictEqual( + * Array.from(Iterable.prependAll([1, 2], ["a", "b"])), + * ["a", "b", 1, 2] + * ) + * ``` + * + * @category concatenating + * @since 2.0.0 + */ +export const prependAll: { + (that: Iterable): (self: Iterable) => Iterable + (self: Iterable, that: Iterable): Iterable +} = dual( + 2, + (self: Iterable, that: Iterable): Iterable => appendAll(that, self) +) + +/** + * Append an element to the end of an `Iterable`, creating a new `Iterable`. + * + * @category concatenating + * @since 2.0.0 + */ +export const append: { + (last: B): (self: Iterable) => Iterable + (self: Iterable, last: B): Iterable +} = dual(2, (self: Iterable, last: B): Iterable => appendAll(self, [last])) + +/** + * Concatenates two iterables, combining their elements. + * + * @category concatenating + * @since 2.0.0 + */ +export const appendAll: { + (that: Iterable): (self: Iterable) => Iterable + (self: Iterable, that: Iterable): Iterable +} = dual( + 2, + (self: Iterable, that: Iterable): Iterable => ({ + [Symbol.iterator]() { + const iterA = self[Symbol.iterator]() + let doneA = false + let iterB: Iterator + return { + next() { + if (!doneA) { + const r = iterA.next() + if (r.done) { + doneA = true + iterB = that[Symbol.iterator]() + return iterB.next() + } + return r + } + return iterB.next() + } + } + } + }) +) + +/** + * Reduce an `Iterable` from the left, keeping all intermediate results instead of only the final result. + * + * @category folding + * @since 2.0.0 + */ +export const scan: { + (b: B, f: (b: B, a: A) => B): (self: Iterable) => Iterable + (self: Iterable, b: B, f: (b: B, a: A) => B): Iterable +} = dual(3, (self: Iterable, b: B, f: (b: B, a: A) => B): Iterable => ({ + [Symbol.iterator]() { + let acc = b + let iterator: Iterator | undefined + function next() { + if (iterator === undefined) { + iterator = self[Symbol.iterator]() + return { done: false, value: acc } + } + const result = iterator.next() + if (result.done) { + return result + } + acc = f(acc, result.value) + return { done: false, value: acc } + } + return { next } + } +})) + +/** + * Determine if an `Iterable` is empty + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { isEmpty } from "effect/Iterable" + * + * assert.deepStrictEqual(isEmpty([]), true); + * assert.deepStrictEqual(isEmpty([1, 2, 3]), false); + * ``` + * + * @category guards + * @since 2.0.0 + */ +export const isEmpty = (self: Iterable): self is Iterable => { + const iterator = self[Symbol.iterator]() + return iterator.next().done === true +} + +/** + * Return the number of elements in a `Iterable`. + * + * @category getters + * @since 2.0.0 + */ +export const size = (self: Iterable): number => { + const iterator = self[Symbol.iterator]() + let count = 0 + while (!iterator.next().done) { + count++ + } + return count +} + +/** + * Get the first element of a `Iterable`, or `None` if the `Iterable` is empty. + * + * @category getters + * @since 2.0.0 + */ +export const head = (self: Iterable): Option => { + const iterator = self[Symbol.iterator]() + const result = iterator.next() + return result.done ? O.none() : O.some(result.value) +} + +/** + * Get the first element of a `Iterable`, or throw an error if the `Iterable` is empty. + * + * @category getters + * @since 3.3.0 + */ +export const unsafeHead = (self: Iterable): A => { + const iterator = self[Symbol.iterator]() + const result = iterator.next() + if (result.done) throw new Error("unsafeHead: empty iterable") + return result.value +} + +/** + * Keep only a max number of elements from the start of an `Iterable`, creating a new `Iterable`. + * + * **Note**. `n` is normalized to a non negative integer. + * + * @category getters + * @since 2.0.0 + */ +export const take: { + (n: number): (self: Iterable) => Iterable + (self: Iterable, n: number): Iterable +} = dual(2, (self: Iterable, n: number): Iterable => ({ + [Symbol.iterator]() { + let i = 0 + const iterator = self[Symbol.iterator]() + return { + next() { + if (i < n) { + i++ + return iterator.next() + } + return { done: true, value: undefined } + } + } + } +})) + +/** + * Calculate the longest initial Iterable for which all element satisfy the specified predicate, creating a new `Iterable`. + * + * @category getters + * @since 2.0.0 + */ +export const takeWhile: { + (refinement: (a: NoInfer, i: number) => a is B): (self: Iterable) => Iterable + (predicate: (a: NoInfer, i: number) => boolean): (self: Iterable) => Iterable + (self: Iterable, refinement: (a: A, i: number) => a is B): Iterable + (self: Iterable, predicate: (a: A, i: number) => boolean): Iterable +} = dual(2, (self: Iterable, predicate: (a: A, i: number) => boolean): Iterable => ({ + [Symbol.iterator]() { + const iterator = self[Symbol.iterator]() + let i = 0 + return { + next() { + const result = iterator.next() + if (result.done || !predicate(result.value, i++)) { + return { done: true, value: undefined } + } + return result + } + } + } +})) + +/** + * Drop a max number of elements from the start of an `Iterable` + * + * **Note**. `n` is normalized to a non negative integer. + * + * @category getters + * @since 2.0.0 + */ +export const drop: { + (n: number): (self: Iterable) => Iterable + (self: Iterable, n: number): Iterable +} = dual(2, (self: Iterable, n: number): Iterable => ({ + [Symbol.iterator]() { + const iterator = self[Symbol.iterator]() + let i = 0 + return { + next() { + while (i < n) { + const result = iterator.next() + if (result.done) { + return { done: true, value: undefined } + } + i++ + } + return iterator.next() + } + } + } +})) + +/** + * Returns the first element that satisfies the specified + * predicate, or `None` if no such element exists. + * + * @category elements + * @since 2.0.0 + */ +export const findFirst: { + (f: (a: NoInfer, i: number) => Option): (self: Iterable) => Option + (refinement: (a: NoInfer, i: number) => a is B): (self: Iterable) => Option + (predicate: (a: NoInfer, i: number) => boolean): (self: Iterable) => Option + (self: Iterable, f: (a: A, i: number) => Option): Option + (self: Iterable, refinement: (a: A, i: number) => a is B): Option + (self: Iterable, predicate: (a: A, i: number) => boolean): Option +} = dual( + 2, + (self: Iterable, f: ((a: A, i: number) => boolean) | ((a: A, i: number) => Option)): Option => { + let i = 0 + for (const a of self) { + const o = f(a, i) + if (isBoolean(o)) { + if (o) { + return O.some(a) + } + } else { + if (O.isSome(o)) { + return o + } + } + i++ + } + return O.none() + } +) + +/** + * Find the last element for which a predicate holds. + * + * @category elements + * @since 2.0.0 + */ +export const findLast: { + (f: (a: NoInfer, i: number) => Option): (self: Iterable) => Option + (refinement: (a: NoInfer, i: number) => a is B): (self: Iterable) => Option + (predicate: (a: NoInfer, i: number) => boolean): (self: Iterable) => Option + (self: Iterable, f: (a: A, i: number) => Option): Option + (self: Iterable, refinement: (a: A, i: number) => a is B): Option + (self: Iterable, predicate: (a: A, i: number) => boolean): Option +} = dual( + 2, + (self: Iterable, f: ((a: A, i: number) => boolean) | ((a: A, i: number) => Option)): Option => { + let i = 0 + let last: Option = O.none() + for (const a of self) { + const o = f(a, i) + if (isBoolean(o)) { + if (o) { + last = O.some(a) + } + } else { + if (O.isSome(o)) { + last = o + } + } + i++ + } + return last + } +) + +/** + * Takes two `Iterable`s and returns an `Iterable` of corresponding pairs. + * + * @category zipping + * @since 2.0.0 + */ +export const zip: { + (that: Iterable): (self: Iterable) => Iterable<[A, B]> + (self: Iterable, that: Iterable): Iterable<[A, B]> +} = dual( + 2, + (self: Iterable, that: Iterable): Iterable<[A, B]> => zipWith(self, that, Tuple.make) +) + +/** + * Apply a function to pairs of elements at the same index in two `Iterable`s, collecting the results. If one + * input `Iterable` is short, excess elements of the longer `Iterable` are discarded. + * + * @category zipping + * @since 2.0.0 + */ +export const zipWith: { + (that: Iterable, f: (a: A, b: B) => C): (self: Iterable) => Iterable + (self: Iterable, that: Iterable, f: (a: A, b: B) => C): Iterable +} = dual(3, (self: Iterable, that: Iterable, f: (a: A, b: B) => C): Iterable => ({ + [Symbol.iterator]() { + const selfIterator = self[Symbol.iterator]() + const thatIterator = that[Symbol.iterator]() + return { + next() { + const selfResult = selfIterator.next() + const thatResult = thatIterator.next() + if (selfResult.done || thatResult.done) { + return { done: true, value: undefined } + } + return { done: false, value: f(selfResult.value, thatResult.value) } + } + } + } +})) + +/** + * Places an element in between members of an `Iterable`. + * If the input is a non-empty array, the result is also a non-empty array. + * + * @since 2.0.0 + */ +export const intersperse: { + (middle: B): (self: Iterable) => Iterable + (self: Iterable, middle: B): Iterable +} = dual(2, (self: Iterable, middle: B): Iterable => ({ + [Symbol.iterator]() { + const iterator = self[Symbol.iterator]() + let next = iterator.next() + let emitted = false + return { + next() { + if (next.done) { + return next + } else if (emitted) { + emitted = false + return { done: false, value: middle } + } + emitted = true + const result = next + next = iterator.next() + return result + } + } + } +})) + +/** + * Returns a function that checks if an `Iterable` contains a given value using a provided `isEquivalent` function. + * + * @category elements + * @since 2.0.0 + */ +export const containsWith = (isEquivalent: (self: A, that: A) => boolean): { + (a: A): (self: Iterable) => boolean + (self: Iterable, a: A): boolean +} => + dual(2, (self: Iterable, a: A): boolean => { + for (const i of self) { + if (isEquivalent(a, i)) { + return true + } + } + return false + }) + +const _equivalence = Equal.equivalence() + +/** + * Returns a function that checks if a `Iterable` contains a given value using the default `Equivalence`. + * + * @category elements + * @since 2.0.0 + */ +export const contains: { + (a: A): (self: Iterable) => boolean + (self: Iterable, a: A): boolean +} = containsWith(_equivalence) + +/** + * Splits an `Iterable` into length-`n` pieces. The last piece will be shorter if `n` does not evenly divide the length of + * the `Iterable`. + * + * @category splitting + * @since 2.0.0 + */ +export const chunksOf: { + (n: number): (self: Iterable) => Iterable> + (self: Iterable, n: number): Iterable> +} = dual(2, (self: Iterable, n: number): Iterable> => { + const safeN = Math.max(1, Math.floor(n)) + return ({ + [Symbol.iterator]() { + let iterator: Iterator | undefined = self[Symbol.iterator]() + return { + next() { + if (iterator === undefined) { + return { done: true, value: undefined } + } + + const chunk: Array = [] + for (let i = 0; i < safeN; i++) { + const result = iterator.next() + if (result.done) { + iterator = undefined + return chunk.length === 0 ? { done: true, value: undefined } : { done: false, value: chunk } + } + chunk.push(result.value) + } + + return { done: false, value: chunk } + } + } + } + }) +}) + +/** + * Group equal, consecutive elements of an `Iterable` into `NonEmptyArray`s using the provided `isEquivalent` function. + * + * @category grouping + * @since 2.0.0 + */ +export const groupWith: { + (isEquivalent: (self: A, that: A) => boolean): (self: Iterable) => Iterable> + (self: Iterable, isEquivalent: (self: A, that: A) => boolean): Iterable> +} = dual( + 2, + (self: Iterable, isEquivalent: (self: A, that: A) => boolean): Iterable> => ({ + [Symbol.iterator]() { + const iterator = self[Symbol.iterator]() + let nextResult: IteratorResult | undefined + return { + next() { + let result: IteratorResult + if (nextResult !== undefined) { + if (nextResult.done) { + return { done: true, value: undefined } + } + result = nextResult + nextResult = undefined + } else { + result = iterator.next() + if (result.done) { + return { done: true, value: undefined } + } + } + const chunk: NonEmptyArray = [result.value] + + while (true) { + const next = iterator.next() + if (next.done || !isEquivalent(result.value, next.value)) { + nextResult = next + return { done: false, value: chunk } + } + chunk.push(next.value) + } + } + } + } + }) +) + +/** + * Group equal, consecutive elements of an `Iterable` into `NonEmptyArray`s. + * + * @category grouping + * @since 2.0.0 + */ +export const group: (self: Iterable) => Iterable> = groupWith( + Equal.equivalence() +) + +/** + * Splits an `Iterable` into sub-non-empty-arrays stored in an object, based on the result of calling a `string`-returning + * function on each element, and grouping the results according to values returned + * + * @category grouping + * @since 2.0.0 + */ +export const groupBy: { + ( + f: (a: A) => K + ): (self: Iterable) => Record, NonEmptyArray> + ( + self: Iterable, + f: (a: A) => K + ): Record, NonEmptyArray> +} = dual(2, ( + self: Iterable, + f: (a: A) => K +): Record, NonEmptyArray> => { + const out: Record> = {} + for (const a of self) { + const k = f(a) + if (Object.prototype.hasOwnProperty.call(out, k)) { + out[k].push(a) + } else { + out[k] = [a] + } + } + return out +}) + +const constEmpty: Iterable = { + [Symbol.iterator]() { + return constEmptyIterator + } +} +const constEmptyIterator: Iterator = { + next() { + return { done: true, value: undefined } + } +} + +/** + * @category constructors + * @since 2.0.0 + */ +export const empty = (): Iterable => constEmpty + +/** + * Constructs a new `Iterable` from the specified value. + * + * @category constructors + * @since 2.0.0 + */ +export const of = (a: A): Iterable => [a] + +/** + * @category mapping + * @since 2.0.0 + */ +export const map: { + ( + f: (a: NoInfer, i: number) => B + ): (self: Iterable) => Iterable + (self: Iterable, f: (a: NoInfer, i: number) => B): Iterable +} = dual(2, (self: Iterable, f: (a: A, i: number) => B): Iterable => ({ + [Symbol.iterator]() { + const iterator = self[Symbol.iterator]() + let i = 0 + return { + next() { + const result = iterator.next() + if (result.done) { + return { done: true, value: undefined } + } + return { done: false, value: f(result.value, i++) } + } + } + } +})) + +/** + * Applies a function to each element in an Iterable and returns a new Iterable containing the concatenated mapped elements. + * + * @category sequencing + * @since 2.0.0 + */ +export const flatMap: { + ( + f: (a: NoInfer, i: number) => Iterable + ): (self: Iterable) => Iterable + (self: Iterable, f: (a: NoInfer, i: number) => Iterable): Iterable +} = dual( + 2, + (self: Iterable, f: (a: A, i: number) => Iterable): Iterable => flatten(map(self, f)) +) + +/** + * Flattens an Iterable of Iterables into a single Iterable + * + * @category sequencing + * @since 2.0.0 + */ +export const flatten = (self: Iterable>): Iterable => ({ + [Symbol.iterator]() { + const outerIterator = self[Symbol.iterator]() + let innerIterator: Iterator | undefined + function next() { + if (innerIterator === undefined) { + const next = outerIterator.next() + if (next.done) { + return next + } + innerIterator = next.value[Symbol.iterator]() + } + const result = innerIterator.next() + if (result.done) { + innerIterator = undefined + return next() + } + return result + } + return { next } + } +}) + +/** + * @category filtering + * @since 2.0.0 + */ +export const filterMap: { + (f: (a: A, i: number) => Option): (self: Iterable) => Iterable + (self: Iterable, f: (a: A, i: number) => Option): Iterable +} = dual( + 2, + (self: Iterable, f: (a: A, i: number) => Option): Iterable => ({ + [Symbol.iterator]() { + const iterator = self[Symbol.iterator]() + let i = 0 + return { + next() { + let result = iterator.next() + while (!result.done) { + const b = f(result.value, i++) + if (O.isSome(b)) { + return { done: false, value: b.value } + } + result = iterator.next() + } + return { done: true, value: undefined } + } + } + } + }) +) + +/** + * Transforms all elements of the `Iterable` for as long as the specified function returns some value + * + * @category filtering + * @since 2.0.0 + */ +export const filterMapWhile: { + (f: (a: A, i: number) => Option): (self: Iterable) => Iterable + (self: Iterable, f: (a: A, i: number) => Option): Iterable +} = dual(2, (self: Iterable, f: (a: A, i: number) => Option) => ({ + [Symbol.iterator]() { + const iterator = self[Symbol.iterator]() + let i = 0 + return { + next() { + const result = iterator.next() + if (result.done) { + return { done: true, value: undefined } + } + const b = f(result.value, i++) + if (O.isSome(b)) { + return { done: false, value: b.value } + } + return { done: true, value: undefined } + } + } + } +})) + +/** + * Retrieves the `Some` values from an `Iterable` of `Option`s. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { Iterable, Option } from "effect" + * + * assert.deepStrictEqual( + * Array.from(Iterable.getSomes([Option.some(1), Option.none(), Option.some(2)])), + * [1, 2] + * ) + * ``` + * + * @category filtering + * @since 2.0.0 + */ +export const getSomes: (self: Iterable>) => Iterable = filterMap(identity) + +/** + * Retrieves the `Left` values from an `Iterable` of `Either`s. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { Iterable, Either } from "effect" + * + * assert.deepStrictEqual( + * Array.from(Iterable.getLefts([Either.right(1), Either.left("err"), Either.right(2)])), + * ["err"] + * ) + * ``` + * + * @category filtering + * @since 2.0.0 + */ +export const getLefts = (self: Iterable>): Iterable => filterMap(self, E.getLeft) + +/** + * Retrieves the `Right` values from an `Iterable` of `Either`s. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { Iterable, Either } from "effect" + * + * assert.deepStrictEqual( + * Array.from(Iterable.getRights([Either.right(1), Either.left("err"), Either.right(2)])), + * [1, 2] + * ) + * ``` + * + * @category filtering + * @since 2.0.0 + */ +export const getRights = (self: Iterable>): Iterable => filterMap(self, E.getRight) + +/** + * @category filtering + * @since 2.0.0 + */ +export const filter: { + (refinement: (a: NoInfer, i: number) => a is B): (self: Iterable) => Iterable + (predicate: (a: NoInfer, i: number) => boolean): (self: Iterable) => Iterable + (self: Iterable, refinement: (a: A, i: number) => a is B): Iterable + (self: Iterable, predicate: (a: A, i: number) => boolean): Iterable +} = dual( + 2, + (self: Iterable, predicate: (a: A, i: number) => boolean): Iterable => ({ + [Symbol.iterator]() { + const iterator = self[Symbol.iterator]() + let i = 0 + return { + next() { + let result = iterator.next() + while (!result.done) { + if (predicate(result.value, i++)) { + return { done: false, value: result.value } + } + result = iterator.next() + } + return { done: true, value: undefined } + } + } + } + }) +) + +/** + * @category sequencing + * @since 2.0.0 + */ +export const flatMapNullable: { + (f: (a: A) => B | null | undefined): (self: Iterable) => Iterable> + (self: Iterable, f: (a: A) => B | null | undefined): Iterable> +} = dual( + 2, + (self: Iterable, f: (a: A) => B | null | undefined): Iterable> => + filterMap(self, (a) => { + const b = f(a) + return b == null ? O.none() : O.some(b) + }) +) + +/** + * Check if a predicate holds true for some `Iterable` element. + * + * @category elements + * @since 2.0.0 + */ +export const some: { + (predicate: (a: A, i: number) => boolean): (self: Iterable) => boolean + (self: Iterable, predicate: (a: A, i: number) => boolean): boolean +} = dual( + 2, + (self: Iterable, predicate: (a: A, i: number) => boolean): boolean => { + let i = 0 + for (const a of self) { + if (predicate(a, i++)) { + return true + } + } + return false + } +) + +/** + * @category constructors + * @since 2.0.0 + */ +export const unfold = (b: B, f: (b: B) => Option): Iterable => ({ + [Symbol.iterator]() { + let next = b + return { + next() { + const o = f(next) + if (O.isNone(o)) { + return { done: true, value: undefined } + } + const [a, b] = o.value + next = b + return { done: false, value: a } + } + } + } +}) + +/** + * Iterate over the `Iterable` applying `f`. + * + * @since 2.0.0 + */ +export const forEach: { + (f: (a: A, i: number) => void): (self: Iterable) => void + (self: Iterable, f: (a: A, i: number) => void): void +} = dual(2, (self: Iterable, f: (a: A, i: number) => void): void => { + let i = 0 + for (const a of self) { + f(a, i++) + } +}) + +/** + * @category folding + * @since 2.0.0 + */ +export const reduce: { + (b: B, f: (b: B, a: A, i: number) => B): (self: Iterable) => B + (self: Iterable, b: B, f: (b: B, a: A, i: number) => B): B +} = dual(3, (self: Iterable, b: B, f: (b: B, a: A, i: number) => B): B => { + if (Array.isArray(self)) { + return self.reduce(f, b) + } + let i = 0 + let result = b + for (const n of self) { + result = f(result, n, i++) + } + return result +}) + +/** + * Deduplicates adjacent elements that are identical using the provided `isEquivalent` function. + * + * @since 2.0.0 + */ +export const dedupeAdjacentWith: { + (isEquivalent: (self: A, that: A) => boolean): (self: Iterable) => Iterable + (self: Iterable, isEquivalent: (self: A, that: A) => boolean): Iterable +} = dual(2, (self: Iterable, isEquivalent: (self: A, that: A) => boolean): Iterable => ({ + [Symbol.iterator]() { + const iterator = self[Symbol.iterator]() + let first = true + let last: A + function next(): IteratorResult { + const result = iterator.next() + if (result.done) { + return { done: true, value: undefined } + } + if (first) { + first = false + last = result.value + return result + } + const current = result.value + if (isEquivalent(last, current)) { + return next() + } + last = current + return result + } + return { next } + } +})) + +/** + * Deduplicates adjacent elements that are identical. + * + * @since 2.0.0 + */ +export const dedupeAdjacent: (self: Iterable) => Iterable = dedupeAdjacentWith(Equal.equivalence()) + +/** + * Zips this Iterable crosswise with the specified Iterable using the specified combiner. + * + * @since 2.0.0 + * @category elements + */ +export const cartesianWith: { + (that: Iterable, f: (a: A, b: B) => C): (self: Iterable) => Iterable + (self: Iterable, that: Iterable, f: (a: A, b: B) => C): Iterable +} = dual( + 3, + (self: Iterable, that: Iterable, f: (a: A, b: B) => C): Iterable => + flatMap(self, (a) => map(that, (b) => f(a, b))) +) + +/** + * Zips this Iterable crosswise with the specified Iterable. + * + * @since 2.0.0 + * @category elements + */ +export const cartesian: { + (that: Iterable): (self: Iterable) => Iterable<[A, B]> + (self: Iterable, that: Iterable): Iterable<[A, B]> +} = dual( + 2, + (self: Iterable, that: Iterable): Iterable<[A, B]> => cartesianWith(self, that, (a, b) => [a, b]) +) + +/** + * Counts all the element of the given iterable that pass the given predicate + * + * **Example** + * + * ```ts + * import { Iterable } from "effect" + * + * const result = Iterable.countBy([1, 2, 3, 4, 5], n => n % 2 === 0) + * console.log(result) // 2 + * ``` + * + * @category folding + * @since 3.16.0 + */ +export const countBy: { + (predicate: (a: NoInfer, i: number) => boolean): (self: Iterable) => number + (self: Iterable, predicate: (a: A, i: number) => boolean): number +} = dual( + 2, + ( + self: Iterable, + f: (a: A, i: number) => boolean + ): number => { + let count = 0 + let i = 0 + for (const a of self) { + if (f(a, i)) { + count++ + } + i++ + } + return count + } +) diff --git a/repos/effect/packages/effect/src/JSONSchema.ts b/repos/effect/packages/effect/src/JSONSchema.ts new file mode 100644 index 0000000..4b5220e --- /dev/null +++ b/repos/effect/packages/effect/src/JSONSchema.ts @@ -0,0 +1,1044 @@ +/** + * @since 3.10.0 + */ + +import * as Arr from "./Array.js" +import * as errors_ from "./internal/schema/errors.js" +import * as schemaId_ from "./internal/schema/schemaId.js" +import * as Option from "./Option.js" +import * as ParseResult from "./ParseResult.js" +import * as Predicate from "./Predicate.js" +import * as Record from "./Record.js" +import type * as Schema from "./Schema.js" +import * as AST from "./SchemaAST.js" + +type JsonValue = string | number | boolean | null | Array | { [key: string]: JsonValue } + +/** + * @category model + * @since 3.10.0 + */ +export interface JsonSchemaAnnotations { + title?: string + description?: string + default?: JsonValue + examples?: Array +} + +/** + * @category model + * @since 3.11.5 + */ +export interface JsonSchema7Never extends JsonSchemaAnnotations { + $id: "/schemas/never" + not: {} +} + +/** + * @category model + * @since 3.10.0 + */ +export interface JsonSchema7Any extends JsonSchemaAnnotations { + $id: "/schemas/any" +} + +/** + * @category model + * @since 3.10.0 + */ +export interface JsonSchema7Unknown extends JsonSchemaAnnotations { + $id: "/schemas/unknown" +} + +/** + * @category model + * @since 3.10.0 + */ +export interface JsonSchema7Void extends JsonSchemaAnnotations { + $id: "/schemas/void" +} + +/** + * @category model + * @since 3.10.0 + */ +export interface JsonSchema7object extends JsonSchemaAnnotations { + $id: "/schemas/object" + anyOf: [ + { type: "object" }, + { type: "array" } + ] +} + +/** + * @category model + * @since 3.10.0 + */ +export interface JsonSchema7empty extends JsonSchemaAnnotations { + $id: "/schemas/%7B%7D" + anyOf: [ + { type: "object" }, + { type: "array" } + ] +} + +/** + * @category model + * @since 3.10.0 + */ +export interface JsonSchema7Ref extends JsonSchemaAnnotations { + $ref: string +} + +/** + * @category model + * @since 3.11.7 + */ +export interface JsonSchema7Null extends JsonSchemaAnnotations { + type: "null" +} + +/** + * @category model + * @since 3.10.0 + */ +export interface JsonSchema7String extends JsonSchemaAnnotations { + type: "string" + minLength?: number + maxLength?: number + pattern?: string + format?: string + contentMediaType?: string + allOf?: Array<{ + minLength?: number + maxLength?: number + pattern?: string + }> +} + +/** + * @category model + * @since 3.10.0 + */ +export interface JsonSchema7Numeric extends JsonSchemaAnnotations { + minimum?: number + exclusiveMinimum?: number + maximum?: number + exclusiveMaximum?: number + multipleOf?: number + allOf?: Array<{ + minimum?: number + exclusiveMinimum?: number + maximum?: number + exclusiveMaximum?: number + multipleOf?: number + }> +} + +/** + * @category model + * @since 3.10.0 + */ +export interface JsonSchema7Number extends JsonSchema7Numeric { + type: "number" +} + +/** + * @category model + * @since 3.10.0 + */ +export interface JsonSchema7Integer extends JsonSchema7Numeric { + type: "integer" +} + +/** + * @category model + * @since 3.10.0 + */ +export interface JsonSchema7Boolean extends JsonSchemaAnnotations { + type: "boolean" +} + +/** + * @category model + * @since 3.10.0 + */ +export interface JsonSchema7Array extends JsonSchemaAnnotations { + type: "array" + items?: JsonSchema7 | Array | false + prefixItems?: Array + minItems?: number + maxItems?: number + additionalItems?: JsonSchema7 | boolean +} + +/** + * @category model + * @since 3.10.0 + */ +export interface JsonSchema7Enum extends JsonSchemaAnnotations { + type?: "string" | "number" | "boolean" + enum: Array +} + +/** + * @category model + * @since 3.10.0 + */ +export interface JsonSchema7Enums extends JsonSchemaAnnotations { + $comment: "/schemas/enums" + anyOf: Array<{ + type: "string" | "number" + title: string + enum: [string | number] + }> +} + +/** + * @category model + * @since 3.10.0 + */ +export interface JsonSchema7AnyOf extends JsonSchemaAnnotations { + anyOf: Array +} + +/** + * @category model + * @since 3.10.0 + */ +export interface JsonSchema7Object extends JsonSchemaAnnotations { + type: "object" + required: Array + properties: Record + additionalProperties?: boolean | JsonSchema7 + patternProperties?: Record + propertyNames?: JsonSchema7 +} + +/** + * @category model + * @since 3.10.0 + */ +export type JsonSchema7 = + | JsonSchema7Never + | JsonSchema7Any + | JsonSchema7Unknown + | JsonSchema7Void + | JsonSchema7object + | JsonSchema7empty + | JsonSchema7Ref + | JsonSchema7Null + | JsonSchema7String + | JsonSchema7Number + | JsonSchema7Integer + | JsonSchema7Boolean + | JsonSchema7Array + | JsonSchema7Enum + | JsonSchema7Enums + | JsonSchema7AnyOf + | JsonSchema7Object + +/** + * @category model + * @since 3.10.0 + */ +export type JsonSchema7Root = JsonSchema7 & { + $schema?: string + $defs?: Record +} + +/** + * Generates a JSON Schema from a schema. + * + * **Options** + * + * - `target`: The target JSON Schema version. Possible values are: + * - `"jsonSchema7"`: JSON Schema draft-07 (default behavior). + * - `"jsonSchema2019-09"`: JSON Schema draft-2019-09. + * - `"jsonSchema2020-12"`: JSON Schema draft-2020-12. + * - `"openApi3.1"`: OpenAPI 3.1. + * + * @category encoding + * @since 3.10.0 + */ +export const make = (schema: Schema.Schema, options?: { + readonly target?: Target | undefined +}): JsonSchema7Root => { + const definitions: Record = {} + const target = options?.target ?? "jsonSchema7" + const ast = AST.isTransformation(schema.ast) && isParseJsonTransformation(schema.ast.from) + // Special case top level `parseJson` transformations + ? schema.ast.to + : schema.ast + const jsonSchema = fromAST(ast, { + definitions, + target + }) + const out: JsonSchema7Root = { + $schema: getMetaSchemaUri(target), + $defs: {}, + ...jsonSchema + } + if (Record.isEmptyRecord(definitions)) { + delete out.$defs + } else { + out.$defs = definitions + } + return out +} + +type Target = "jsonSchema7" | "jsonSchema2019-09" | "openApi3.1" | "jsonSchema2020-12" + +type TopLevelReferenceStrategy = "skip" | "keep" + +type AdditionalPropertiesStrategy = "allow" | "strict" + +/** @internal */ +export function getMetaSchemaUri(target: Target) { + switch (target) { + case "jsonSchema7": + return "http://json-schema.org/draft-07/schema#" + case "jsonSchema2019-09": + return "https://json-schema.org/draft/2019-09/schema" + case "jsonSchema2020-12": + case "openApi3.1": + return "https://json-schema.org/draft/2020-12/schema" + } +} + +/** + * Returns a JSON Schema with additional options and definitions. + * + * **Warning** + * + * This function is experimental and subject to change. + * + * **Options** + * + * - `definitions`: A record of definitions that are included in the schema. + * - `definitionPath`: The path to the definitions within the schema (defaults + * to "#/$defs/"). + * - `target`: Which spec to target. Possible values are: + * - `'jsonSchema7'`: JSON Schema draft-07 (default behavior). + * - `'jsonSchema2019-09'`: JSON Schema draft-2019-09. + * - `'openApi3.1'`: OpenAPI 3.1. + * - `topLevelReferenceStrategy`: Controls the handling of the top-level + * reference. Possible values are: + * - `"keep"`: Keep the top-level reference (default behavior). + * - `"skip"`: Skip the top-level reference. + * - `additionalPropertiesStrategy`: Controls the handling of additional properties. Possible values are: + * - `"strict"`: Disallow additional properties (default behavior). + * - `"allow"`: Allow additional properties. + * + * @category encoding + * @since 3.11.5 + * @experimental + */ +export const fromAST = (ast: AST.AST, options: { + readonly definitions: Record + readonly definitionPath?: string | undefined + readonly target?: Target | undefined + readonly topLevelReferenceStrategy?: TopLevelReferenceStrategy | undefined + readonly additionalPropertiesStrategy?: AdditionalPropertiesStrategy | undefined +}): JsonSchema7 => { + const definitionPath = options.definitionPath ?? "#/$defs/" + const getRef = (id: string) => definitionPath + id + const target = options.target ?? "jsonSchema7" + const topLevelReferenceStrategy = options.topLevelReferenceStrategy ?? "keep" + const additionalPropertiesStrategy = options.additionalPropertiesStrategy ?? "strict" + return go( + ast, + options.definitions, + "handle-identifier", + [], + { + getRef, + target, + topLevelReferenceStrategy, + additionalPropertiesStrategy + }, + "handle-annotation", + "handle-errors" + ) +} + +const constNever: JsonSchema7Never = { + $id: "/schemas/never", + not: {} +} + +const constAny: JsonSchema7Any = { + $id: "/schemas/any" +} + +const constUnknown: JsonSchema7Unknown = { + $id: "/schemas/unknown" +} + +const constVoid: JsonSchema7Void = { + $id: "/schemas/void" +} + +const constObject: JsonSchema7object = { + $id: "/schemas/object", + "anyOf": [ + { "type": "object" }, + { "type": "array" } + ] +} + +const constEmptyStruct: JsonSchema7empty = { + $id: "/schemas/%7B%7D", + "anyOf": [ + { "type": "object" }, + { "type": "array" } + ] +} + +function getRawDescription(annotated: AST.Annotated | undefined): string | undefined { + if (annotated !== undefined) return Option.getOrUndefined(AST.getDescriptionAnnotation(annotated)) +} + +function getRawTitle(annotated: AST.Annotated | undefined): string | undefined { + if (annotated !== undefined) return Option.getOrUndefined(AST.getTitleAnnotation(annotated)) +} + +function getRawDefault(annotated: AST.Annotated | undefined): Option.Option { + if (annotated !== undefined) return AST.getDefaultAnnotation(annotated) + return Option.none() +} + +function encodeDefault(ast: AST.AST, def: unknown): Option.Option { + const getOption = ParseResult.getOption(ast, false) + return getOption(def) +} + +function getRawExamples(annotated: AST.Annotated | undefined): ReadonlyArray | undefined { + if (annotated !== undefined) return Option.getOrUndefined(AST.getExamplesAnnotation(annotated)) +} + +function encodeExamples(ast: AST.AST, examples: ReadonlyArray): Array | undefined { + const getOption = ParseResult.getOption(ast, false) + const out = Arr.filterMap(examples, (e) => getOption(e).pipe(Option.filter(isJsonValue))) + return out.length > 0 ? out : undefined +} + +function filterBuiltIn(ast: AST.AST, annotation: string | undefined, key: symbol): string | undefined { + if (annotation !== undefined) { + switch (ast._tag) { + case "StringKeyword": + return annotation !== AST.stringKeyword.annotations[key] ? annotation : undefined + case "NumberKeyword": + return annotation !== AST.numberKeyword.annotations[key] ? annotation : undefined + case "BooleanKeyword": + return annotation !== AST.booleanKeyword.annotations[key] ? annotation : undefined + } + } + return annotation +} + +function isJsonValue(value: unknown, visited: Set = new Set()): value is JsonValue { + if (value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean") { + return true + } + if (Array.isArray(value) || typeof value === "object") { + // Check for cyclic references + if (visited.has(value)) { + return false + } + visited.add(value) + try { + if (Array.isArray(value)) { + return value.every((item) => isJsonValue(item, visited)) + } + // Exclude non-plain objects (Date, RegExp, etc.) by checking constructor + const proto = Object.getPrototypeOf(value) + if (proto !== null && proto !== Object.prototype) { + return false + } + // JSON only allows string keys, so exclude objects with Symbol keys + if (Object.getOwnPropertySymbols(value).length > 0) { + return false + } + // Check all values are JSON values + return Object.values(value).every((v) => isJsonValue(v, visited)) + } finally { + visited.delete(value) + } + } + return false +} + +function pruneJsonSchemaAnnotations( + ast: AST.AST, + description: string | undefined, + title: string | undefined, + def: Option.Option, + examples: ReadonlyArray | undefined +): JsonSchemaAnnotations | undefined { + const out: JsonSchemaAnnotations = {} + if (description !== undefined) out.description = description + if (title !== undefined) out.title = title + if (Option.isSome(def)) { + const o = encodeDefault(ast, def.value) + if (Option.isSome(o) && isJsonValue(o.value)) { + out.default = o.value + } + } + if (examples !== undefined) { + const encodedExamples = encodeExamples(ast, examples) + if (encodedExamples !== undefined) { + out.examples = encodedExamples + } + } + if (Object.keys(out).length === 0) { + return undefined + } + return out +} + +function getContextJsonSchemaAnnotations(ast: AST.AST, annotated: AST.Annotated): JsonSchemaAnnotations | undefined { + return pruneJsonSchemaAnnotations( + ast, + getRawDescription(annotated), + getRawTitle(annotated), + getRawDefault(annotated), + getRawExamples(annotated) + ) +} + +function getJsonSchemaAnnotations(ast: AST.AST): JsonSchemaAnnotations | undefined { + return pruneJsonSchemaAnnotations( + ast, + filterBuiltIn(ast, getRawDescription(ast), AST.DescriptionAnnotationId), + filterBuiltIn(ast, getRawTitle(ast), AST.TitleAnnotationId), + getRawDefault(ast), + getRawExamples(ast) + ) +} + +function mergeJsonSchemaAnnotations( + jsonSchema: JsonSchema7, + jsonSchemaAnnotations: JsonSchemaAnnotations | undefined +): JsonSchema7 { + if (jsonSchemaAnnotations) { + if ("$ref" in jsonSchema) { + return { allOf: [jsonSchema], ...jsonSchemaAnnotations } as any + } + return { ...jsonSchema, ...jsonSchemaAnnotations } + } + return jsonSchema +} + +const pruneUndefined = (ast: AST.AST): AST.AST | undefined => { + if (Option.isNone(AST.getJSONSchemaAnnotation(ast))) { + return AST.pruneUndefined(ast, pruneUndefined, (ast) => pruneUndefined(ast.from)) + } +} + +const isParseJsonTransformation = (ast: AST.AST): boolean => + ast.annotations[AST.SchemaIdAnnotationId] === AST.ParseJsonSchemaId + +const isOverrideAnnotation = (ast: AST.AST, jsonSchema: JsonSchema7): boolean => { + if (AST.isRefinement(ast)) { + const schemaId = ast.annotations[AST.SchemaIdAnnotationId] + if (schemaId === schemaId_.IntSchemaId) { + return "type" in jsonSchema && jsonSchema.type !== "integer" + } + } + return ("type" in jsonSchema) || ("oneOf" in jsonSchema) || ("anyOf" in jsonSchema) || ("$ref" in jsonSchema) +} + +const mergeRefinements = (from: any, jsonSchema: any, ast: AST.AST): any => { + const out: any = { ...from, ...getJsonSchemaAnnotations(ast), ...jsonSchema } + out.allOf ??= [] + + const handle = (name: string, filter: (i: any) => boolean) => { + if (name in jsonSchema && name in from) { + out.allOf.unshift({ [name]: from[name] }) + out.allOf = out.allOf.filter(filter) + } + } + + handle("minLength", (i) => i.minLength > jsonSchema.minLength) + handle("maxLength", (i) => i.maxLength < jsonSchema.maxLength) + handle("pattern", (i) => i.pattern !== jsonSchema.pattern) + handle("minItems", (i) => i.minItems > jsonSchema.minItems) + handle("maxItems", (i) => i.maxItems < jsonSchema.maxItems) + handle("minimum", (i) => i.minimum > jsonSchema.minimum) + handle("maximum", (i) => i.maximum < jsonSchema.maximum) + handle("exclusiveMinimum", (i) => i.exclusiveMinimum > jsonSchema.exclusiveMinimum) + handle("exclusiveMaximum", (i) => i.exclusiveMaximum < jsonSchema.exclusiveMaximum) + handle("multipleOf", (i) => i.multipleOf !== jsonSchema.multipleOf) + + if (out.allOf.length === 0) { + delete out.allOf + } + return out +} + +type GoOptions = { + readonly getRef: (id: string) => string + readonly target: Target + readonly topLevelReferenceStrategy: TopLevelReferenceStrategy + readonly additionalPropertiesStrategy: AdditionalPropertiesStrategy +} + +function isContentSchemaSupported(options: GoOptions): boolean { + switch (options.target) { + case "jsonSchema7": + return false + case "jsonSchema2019-09": + case "jsonSchema2020-12": + case "openApi3.1": + return true + } +} + +function getAdditionalProperties(options: GoOptions): boolean { + switch (options.additionalPropertiesStrategy) { + case "allow": + return true + case "strict": + return false + } +} + +function addASTAnnotations(jsonSchema: JsonSchema7, ast: AST.AST): JsonSchema7 { + return addAnnotations(jsonSchema, getJsonSchemaAnnotations(ast)) +} + +function addAnnotations(jsonSchema: JsonSchema7, annotations: JsonSchemaAnnotations | undefined): JsonSchema7 { + if (annotations === undefined || Object.keys(annotations).length === 0) { + return jsonSchema + } + if ("$ref" in jsonSchema) { + return { allOf: [jsonSchema], ...annotations } as any + } + return { ...jsonSchema, ...annotations } +} + +function getIdentifierAnnotation(ast: AST.AST): string | undefined { + const identifier = Option.getOrUndefined(AST.getJSONIdentifier(ast)) + if (identifier === undefined) { + if (AST.isSuspend(ast)) { + return getIdentifierAnnotation(ast.f()) + } + if (AST.isTransformation(ast) && AST.isTypeLiteral(ast.from) && AST.isDeclaration(ast.to)) { + const to = ast.to + const surrogate = AST.getSurrogateAnnotation(to) + if (Option.isSome(surrogate)) { + return getIdentifierAnnotation(to) + } + } + } + return identifier +} + +function go( + ast: AST.AST, + $defs: Record, + identifier: "handle-identifier" | "ignore-identifier", + path: ReadonlyArray, + options: GoOptions, + annotation: "handle-annotation" | "ignore-annotation", + errors: "handle-errors" | "ignore-errors" +): JsonSchema7 { + if ( + identifier === "handle-identifier" && + (options.topLevelReferenceStrategy !== "skip" || AST.isSuspend(ast)) + ) { + const id = getIdentifierAnnotation(ast) + if (id !== undefined) { + const escapedId = id.replace(/~/ig, "~0").replace(/\//ig, "~1") + const out = { $ref: options.getRef(escapedId) } + if (!Record.has($defs, id)) { + $defs[id] = out + $defs[id] = go(ast, $defs, "ignore-identifier", path, options, "handle-annotation", errors) + } + return out + } + } + if (annotation === "handle-annotation") { + const hook = AST.getJSONSchemaAnnotation(ast) + if (Option.isSome(hook)) { + const handler = hook.value as JsonSchema7 + if (isOverrideAnnotation(ast, handler)) { + switch (ast._tag) { + case "Declaration": + return addASTAnnotations(handler, ast) + default: + return handler + } + } else { + switch (ast._tag) { + case "Refinement": { + const t = AST.getTransformationFrom(ast) + if (t === undefined) { + return mergeRefinements( + go(ast.from, $defs, identifier, path, options, "handle-annotation", errors), + handler, + ast + ) + } else { + return go(t, $defs, identifier, path, options, "handle-annotation", errors) + } + } + default: + return { + ...go(ast, $defs, identifier, path, options, "ignore-annotation", errors), + ...handler + } as any + } + } + } + } + const surrogate = AST.getSurrogateAnnotation(ast) + if (Option.isSome(surrogate)) { + return go(surrogate.value, $defs, identifier, path, options, "handle-annotation", errors) + } + switch (ast._tag) { + // Unsupported + case "Declaration": + case "UndefinedKeyword": + case "BigIntKeyword": + case "UniqueSymbol": + case "SymbolKeyword": { + if (errors === "ignore-errors") return addASTAnnotations(constAny, ast) + throw new Error(errors_.getJSONSchemaMissingAnnotationErrorMessage(path, ast)) + } + case "Suspend": { + if (identifier === "handle-identifier") { + if (errors === "ignore-errors") return addASTAnnotations(constAny, ast) + throw new Error(errors_.getJSONSchemaMissingIdentifierAnnotationErrorMessage(path, ast)) + } + return go(ast.f(), $defs, "ignore-identifier", path, options, "handle-annotation", errors) + } + // Primitives + case "NeverKeyword": + return addASTAnnotations(constNever, ast) + case "VoidKeyword": + return addASTAnnotations(constVoid, ast) + case "UnknownKeyword": + return addASTAnnotations(constUnknown, ast) + case "AnyKeyword": + return addASTAnnotations(constAny, ast) + case "ObjectKeyword": + return addASTAnnotations(constObject, ast) + case "StringKeyword": + return addASTAnnotations({ type: "string" }, ast) + case "NumberKeyword": + return addASTAnnotations({ type: "number" }, ast) + case "BooleanKeyword": + return addASTAnnotations({ type: "boolean" }, ast) + case "Literal": { + const literal = ast.literal + if (literal === null) { + return addASTAnnotations({ type: "null" }, ast) + } else if (Predicate.isString(literal)) { + return addASTAnnotations({ type: "string", enum: [literal] }, ast) + } else if (Predicate.isNumber(literal)) { + return addASTAnnotations({ type: "number", enum: [literal] }, ast) + } else if (Predicate.isBoolean(literal)) { + return addASTAnnotations({ type: "boolean", enum: [literal] }, ast) + } + if (errors === "ignore-errors") return addASTAnnotations(constAny, ast) + throw new Error(errors_.getJSONSchemaMissingAnnotationErrorMessage(path, ast)) + } + case "Enums": { + const anyOf = ast.enums.map((e) => { + const type: "string" | "number" = Predicate.isNumber(e[1]) ? "number" : "string" + return { type, title: e[0], enum: [e[1]] } + }) + return anyOf.length >= 1 ? + addASTAnnotations({ + $comment: "/schemas/enums", + anyOf + }, ast) : + addASTAnnotations(constNever, ast) + } + case "TupleType": { + const elements = ast.elements.map((e, i) => + mergeJsonSchemaAnnotations( + go(e.type, $defs, "handle-identifier", path.concat(i), options, "handle-annotation", errors), + getContextJsonSchemaAnnotations(e.type, e) + ) + ) + const rest = ast.rest.map((type) => + mergeJsonSchemaAnnotations( + go(type.type, $defs, "handle-identifier", path, options, "handle-annotation", errors), + getContextJsonSchemaAnnotations(type.type, type) + ) + ) + const output: JsonSchema7Array = { type: "array" } + // --------------------------------------------- + // handle elements + // --------------------------------------------- + const len = ast.elements.length + if (len > 0) { + output.minItems = len - ast.elements.filter((element) => element.isOptional).length + if (options.target === "jsonSchema7") { + output.items = elements + } else { + output.prefixItems = elements + } + } + // --------------------------------------------- + // handle rest element + // --------------------------------------------- + const restLength = rest.length + if (restLength > 0) { + const head = rest[0] + const isHomogeneous = restLength === 1 && ast.elements.every((e) => e.type === ast.rest[0].type) + if (isHomogeneous) { + if (options.target === "jsonSchema7") { + output.items = head + } else { + output.items = head + delete output.prefixItems + } + } else { + if (options.target === "jsonSchema7") { + output.additionalItems = head + } else { + output.items = head + } + } + + // --------------------------------------------- + // handle post rest elements + // --------------------------------------------- + if (restLength > 1) { + if (errors === "ignore-errors") return addASTAnnotations(constAny, ast) + throw new Error(errors_.getJSONSchemaUnsupportedPostRestElementsErrorMessage(path)) + } + } else { + if (len > 0) { + if (options.target === "jsonSchema7") { + output.additionalItems = false + } else { + output.items = false + } + } else { + output.maxItems = 0 + } + } + + return addASTAnnotations(output, ast) + } + case "TypeLiteral": { + if (ast.propertySignatures.length === 0 && ast.indexSignatures.length === 0) { + return addASTAnnotations(constEmptyStruct, ast) + } + const output: JsonSchema7Object = { + type: "object", + required: [], + properties: {}, + additionalProperties: getAdditionalProperties(options) + } + let patternProperties: JsonSchema7 | undefined = undefined + let propertyNames: JsonSchema7 | undefined = undefined + for (const is of ast.indexSignatures) { + const pruned = pruneUndefined(is.type) ?? is.type + const parameter = is.parameter + switch (parameter._tag) { + case "StringKeyword": { + output.additionalProperties = go( + pruned, + $defs, + "handle-identifier", + path, + options, + "handle-annotation", + errors + ) + break + } + case "TemplateLiteral": { + patternProperties = go(pruned, $defs, "handle-identifier", path, options, "handle-annotation", errors) + propertyNames = { + type: "string", + pattern: AST.getTemplateLiteralRegExp(parameter).source + } + break + } + case "Refinement": { + patternProperties = go(pruned, $defs, "handle-identifier", path, options, "handle-annotation", errors) + propertyNames = go(parameter, $defs, "handle-identifier", path, options, "handle-annotation", errors) + break + } + case "SymbolKeyword": { + const indexSignaturePath = path.concat("[symbol]") + output.additionalProperties = go( + pruned, + $defs, + "handle-identifier", + indexSignaturePath, + options, + "handle-annotation", + errors + ) + propertyNames = go( + parameter, + $defs, + "handle-identifier", + indexSignaturePath, + options, + "handle-annotation", + errors + ) + break + } + } + } + // --------------------------------------------- + // handle property signatures + // --------------------------------------------- + for (let i = 0; i < ast.propertySignatures.length; i++) { + const ps = ast.propertySignatures[i] + const name = ps.name + if (Predicate.isString(name)) { + const pruned = pruneUndefined(ps.type) + const type = pruned ?? ps.type + output.properties[name] = mergeJsonSchemaAnnotations( + go(type, $defs, "handle-identifier", path.concat(ps.name), options, "handle-annotation", errors), + getContextJsonSchemaAnnotations(type, ps) + ) + // --------------------------------------------- + // handle optional property signatures + // --------------------------------------------- + if (!ps.isOptional && pruned === undefined) { + output.required.push(name) + } + } else { + if (errors === "ignore-errors") return addASTAnnotations(constAny, ast) + throw new Error(errors_.getJSONSchemaUnsupportedKeyErrorMessage(name, path)) + } + } + // --------------------------------------------- + // handle index signatures + // --------------------------------------------- + if (patternProperties !== undefined) { + delete output.additionalProperties + output.patternProperties = { "": patternProperties } + } + if (propertyNames !== undefined) { + output.propertyNames = propertyNames + } + + return addASTAnnotations(output, ast) + } + case "Union": { + const members: Array = ast.types.map((t) => + go(t, $defs, "handle-identifier", path, options, "handle-annotation", errors) + ) + const anyOf = compactUnion(members) + switch (anyOf.length) { + case 0: + return constNever + case 1: + return addASTAnnotations(anyOf[0], ast) + default: + return addASTAnnotations({ anyOf }, ast) + } + } + case "Refinement": + return go(ast.from, $defs, identifier, path, options, "handle-annotation", errors) + case "TemplateLiteral": { + const regex = AST.getTemplateLiteralRegExp(ast) + return addASTAnnotations({ + type: "string", + title: String(ast), + description: "a template literal", + pattern: regex.source + }, ast) + } + case "Transformation": { + if (isParseJsonTransformation(ast.from)) { + const out: JsonSchema7String & { contentSchema?: JsonSchema7 } = { + "type": "string", + "contentMediaType": "application/json" + } + if (isContentSchemaSupported(options)) { + out["contentSchema"] = go(ast.to, $defs, identifier, path, options, "handle-annotation", errors) + } + return out + } + const from = go(ast.from, $defs, identifier, path, options, "handle-annotation", errors) + if ( + ast.transformation._tag === "TypeLiteralTransformation" && + isJsonSchema7Object(from) + ) { + const to = go(ast.to, {}, "ignore-identifier", path, options, "handle-annotation", "ignore-errors") + if (isJsonSchema7Object(to)) { + for (const t of ast.transformation.propertySignatureTransformations) { + const toKey = t.to + const fromKey = t.from + if (Predicate.isString(toKey) && Predicate.isString(fromKey)) { + const toProperty = to.properties[toKey] + if (Predicate.isRecord(toProperty)) { + const fromProperty = from.properties[fromKey] + if (Predicate.isRecord(fromProperty)) { + const annotations: JsonSchemaAnnotations = {} + if (Predicate.isString(toProperty.title)) annotations.title = toProperty.title + if (Predicate.isString(toProperty.description)) annotations.description = toProperty.description + if (Array.isArray(toProperty.examples)) annotations.examples = toProperty.examples + if (Object.hasOwn(toProperty, "default") && toProperty.default !== undefined) { + annotations.default = toProperty.default + } + from.properties[fromKey] = addAnnotations(fromProperty, annotations) + } + } + } + } + } + } + return addASTAnnotations(from, ast) + } + } +} + +function isJsonSchema7Object(jsonSchema: unknown): jsonSchema is JsonSchema7Object { + return Predicate.isRecord(jsonSchema) && jsonSchema.type === "object" && Predicate.isRecord(jsonSchema.properties) +} + +function isNeverWithoutCustomAnnotations(jsonSchema: JsonSchema7): boolean { + return jsonSchema === constNever || (Predicate.hasProperty(jsonSchema, "$id") && jsonSchema.$id === constNever.$id && + Object.keys(jsonSchema).length === 3 && jsonSchema.title === AST.neverKeyword.annotations[AST.TitleAnnotationId]) +} + +function isAny(jsonSchema: JsonSchema7): jsonSchema is JsonSchema7Any { + return "$id" in jsonSchema && jsonSchema.$id === constAny.$id +} + +function isUnknown(jsonSchema: JsonSchema7): jsonSchema is JsonSchema7Unknown { + return "$id" in jsonSchema && jsonSchema.$id === constUnknown.$id +} + +function isVoid(jsonSchema: JsonSchema7): jsonSchema is JsonSchema7Void { + return "$id" in jsonSchema && jsonSchema.$id === constVoid.$id +} + +function isCompactableLiteral(jsonSchema: JsonSchema7 | undefined): jsonSchema is JsonSchema7Enum { + return Predicate.hasProperty(jsonSchema, "enum") && "type" in jsonSchema && Object.keys(jsonSchema).length === 2 +} + +function compactUnion(members: Array): Array { + const out: Array = [] + for (const m of members) { + if (isNeverWithoutCustomAnnotations(m)) continue + if (isAny(m) || isUnknown(m) || isVoid(m)) return [m] + if (isCompactableLiteral(m) && out.length > 0) { + const last = out[out.length - 1] + if (isCompactableLiteral(last) && last.type === m.type) { + out[out.length - 1] = { + type: last.type, + enum: [...last.enum, ...m.enum] + } as JsonSchema7Enum + continue + } + } + out.push(m) + } + return out +} diff --git a/repos/effect/packages/effect/src/KeyedPool.ts b/repos/effect/packages/effect/src/KeyedPool.ts new file mode 100644 index 0000000..5fe529f --- /dev/null +++ b/repos/effect/packages/effect/src/KeyedPool.ts @@ -0,0 +1,167 @@ +/** + * @since 2.0.0 + */ +import type * as Duration from "./Duration.js" +import type * as Effect from "./Effect.js" +import * as internal from "./internal/keyedPool.js" +import type { Pipeable } from "./Pipeable.js" +import type * as Scope from "./Scope.js" +import type * as Types from "./Types.js" + +/** + * @since 2.0.0 + * @category symbols + */ +export const KeyedPoolTypeId: unique symbol = internal.KeyedPoolTypeId + +/** + * @since 2.0.0 + * @category symbols + */ +export type KeyedPoolTypeId = typeof KeyedPoolTypeId + +/** + * A `KeyedPool` is a pool of `Pool`s of items of type `A`. Each pool + * in the `KeyedPool` is associated with a key of type `K`. + * + * @since 2.0.0 + * @category models + */ +export interface KeyedPool extends KeyedPool.Variance, Pipeable { + /** + * Retrieves an item from the pool belonging to the given key in a scoped + * effect. Note that if acquisition fails, then the returned effect will fail + * for that same reason. Retrying a failed acquisition attempt will repeat the + * acquisition attempt. + */ + get(key: K): Effect.Effect + + /** + * Invalidates the specified item. This will cause the pool to eventually + * reallocate the item, although this reallocation may occur lazily rather + * than eagerly. + */ + invalidate(item: A): Effect.Effect +} + +/** + * @since 2.0.0 + */ +export declare namespace KeyedPool { + /** + * @since 2.0.0 + * @category models + */ + export interface Variance { + readonly [KeyedPoolTypeId]: { + readonly _K: Types.Contravariant + readonly _A: Types.Invariant + readonly _E: Types.Covariant + } + } +} + +/** + * Makes a new pool of the specified fixed size. The pool is returned in a + * `Scope`, which governs the lifetime of the pool. When the pool is shutdown + * because the `Scope` is closed, the individual items allocated by the pool + * will be released in some unspecified order. + * + * @since 2.0.0 + * @category constructors + */ +export const make: ( + options: { + readonly acquire: (key: K) => Effect.Effect + readonly size: number + } +) => Effect.Effect, never, Scope.Scope | R> = internal.make + +/** + * Makes a new pool of the specified fixed size. The pool is returned in a + * `Scope`, which governs the lifetime of the pool. When the pool is shutdown + * because the `Scope` is closed, the individual items allocated by the pool + * will be released in some unspecified order. + * + * The size of the underlying pools can be configured per key. + * + * @since 2.0.0 + * @category constructors + */ +export const makeWith: ( + options: { + readonly acquire: (key: K) => Effect.Effect + readonly size: (key: K) => number + } +) => Effect.Effect, never, Scope.Scope | R> = internal.makeWith + +/** + * Makes a new pool with the specified minimum and maximum sizes and time to + * live before a pool whose excess items are not being used will be shrunk + * down to the minimum size. The pool is returned in a `Scope`, which governs + * the lifetime of the pool. When the pool is shutdown because the `Scope` is + * used, the individual items allocated by the pool will be released in some + * unspecified order. + * + * The size of the underlying pools can be configured per key. + * + * @since 2.0.0 + * @category constructors + */ +export const makeWithTTL: ( + options: { + readonly acquire: (key: K) => Effect.Effect + readonly min: (key: K) => number + readonly max: (key: K) => number + readonly timeToLive: Duration.DurationInput + } +) => Effect.Effect, never, Scope.Scope | R> = internal.makeWithTTL + +/** + * Makes a new pool with the specified minimum and maximum sizes and time to + * live before a pool whose excess items are not being used will be shrunk + * down to the minimum size. The pool is returned in a `Scope`, which governs + * the lifetime of the pool. When the pool is shutdown because the `Scope` is + * used, the individual items allocated by the pool will be released in some + * unspecified order. + * + * The size of the underlying pools can be configured per key. + * + * @since 2.0.0 + * @category constructors + */ +export const makeWithTTLBy: ( + options: { + readonly acquire: (key: K) => Effect.Effect + readonly min: (key: K) => number + readonly max: (key: K) => number + readonly timeToLive: (key: K) => Duration.DurationInput + } +) => Effect.Effect, never, Scope.Scope | R> = internal.makeWithTTLBy + +/** + * Retrieves an item from the pool belonging to the given key in a scoped + * effect. Note that if acquisition fails, then the returned effect will fail + * for that same reason. Retrying a failed acquisition attempt will repeat the + * acquisition attempt. + * + * @since 2.0.0 + * @category combinators + */ +export const get: { + (key: K): (self: KeyedPool) => Effect.Effect + (self: KeyedPool, key: K): Effect.Effect +} = internal.get + +/** + * Invalidates the specified item. This will cause the pool to eventually + * reallocate the item, although this reallocation may occur lazily rather + * than eagerly. + * + * @since 2.0.0 + * @category combinators + */ +export const invalidate: { + (item: A): (self: KeyedPool) => Effect.Effect + (self: KeyedPool, item: A): Effect.Effect +} = internal.invalidate diff --git a/repos/effect/packages/effect/src/Layer.ts b/repos/effect/packages/effect/src/Layer.ts new file mode 100644 index 0000000..945e365 --- /dev/null +++ b/repos/effect/packages/effect/src/Layer.ts @@ -0,0 +1,1280 @@ +/** + * A `Layer` describes how to build one or more services in your + * application. Services can be injected into effects via + * `Effect.provideService`. Effects can require services via `Effect.service`. + * + * Layer can be thought of as recipes for producing bundles of services, given + * their dependencies (other services). + * + * Construction of services can be effectful and utilize resources that must be + * acquired and safely released when the services are done being utilized. + * + * By default layers are shared, meaning that if the same layer is used twice + * the layer will only be allocated a single time. + * + * Because of their excellent composition properties, layers are the idiomatic + * way in Effect-TS to create services that depend on other services. + * + * @since 2.0.0 + */ +import type * as Cause from "./Cause.js" +import type * as Clock from "./Clock.js" +import type { ConfigProvider } from "./ConfigProvider.js" +import * as Context from "./Context.js" +import type * as Effect from "./Effect.js" +import type * as Exit from "./Exit.js" +import type { FiberRef } from "./FiberRef.js" +import { dual, type LazyArg } from "./Function.js" +import { clockTag } from "./internal/clock.js" +import * as core from "./internal/core.js" +import * as defaultServices from "./internal/defaultServices.js" +import * as fiberRuntime from "./internal/fiberRuntime.js" +import * as internal from "./internal/layer.js" +import * as circularLayer from "./internal/layer/circular.js" +import * as query from "./internal/query.js" +import { randomTag } from "./internal/random.js" +import type { LogLevel } from "./LogLevel.js" +import type * as Option from "./Option.js" +import type { Pipeable } from "./Pipeable.js" +import type * as Random from "./Random.js" +import type * as Request from "./Request.js" +import type * as Runtime from "./Runtime.js" +import type * as Schedule from "./Schedule.js" +import * as Scheduler from "./Scheduler.js" +import type * as Scope from "./Scope.js" +import type * as Stream from "./Stream.js" +import type * as Tracer from "./Tracer.js" +import type * as Types from "./Types.js" + +/** + * @since 2.0.0 + * @category symbols + */ +export const LayerTypeId: unique symbol = internal.LayerTypeId + +/** + * @since 2.0.0 + * @category symbols + */ +export type LayerTypeId = typeof LayerTypeId + +/** + * @since 2.0.0 + * @category models + */ +export interface Layer extends Layer.Variance, Pipeable {} + +/** + * @since 2.0.0 + */ +export declare namespace Layer { + /** + * @since 2.0.0 + * @category models + */ + export interface Variance { + readonly [LayerTypeId]: { + readonly _ROut: Types.Contravariant + readonly _E: Types.Covariant + readonly _RIn: Types.Covariant + } + } + /** + * @since 3.9.0 + * @category type-level + */ + export interface Any { + readonly [LayerTypeId]: { + readonly _ROut: Types.Contravariant + readonly _E: Types.Covariant + readonly _RIn: Types.Covariant + } + } + /** + * @since 2.0.0 + * @category type-level + */ + export type Context = [T] extends [Layer] ? _RIn + : never + /** + * @since 2.0.0 + * @category type-level + */ + export type Error = [T] extends [Layer] ? _E + : never + /** + * @since 2.0.0 + * @category type-level + */ + export type Success = [T] extends [Layer] ? _ROut + : never +} + +/** + * @since 2.0.0 + * @category symbols + */ +export const MemoMapTypeId: unique symbol = internal.MemoMapTypeId + +/** + * @since 2.0.0 + * @category symbols + */ +export type MemoMapTypeId = typeof MemoMapTypeId + +/** + * @since 2.0.0 + * @category models + */ +export interface MemoMap { + readonly [MemoMapTypeId]: MemoMapTypeId + + /** @internal */ + readonly getOrElseMemoize: ( + layer: Layer, + scope: Scope.Scope + ) => Effect.Effect, E, RIn> +} + +/** + * @since 3.13.0 + * @category models + */ +export interface CurrentMemoMap { + readonly _: unique symbol +} + +/** + * @since 3.13.0 + * @category models + */ +export const CurrentMemoMap: Context.Reference = internal.CurrentMemoMap + +/** + * Returns `true` if the specified value is a `Layer`, `false` otherwise. + * + * @since 2.0.0 + * @category getters + */ +export const isLayer: (u: unknown) => u is Layer = internal.isLayer + +/** + * Returns `true` if the specified `Layer` is a fresh version that will not be + * shared, `false` otherwise. + * + * @since 2.0.0 + * @category getters + */ +export const isFresh: (self: Layer) => boolean = internal.isFresh + +/** + * @since 3.3.0 + * @category tracing + */ +export const annotateLogs: { + (key: string, value: unknown): (self: Layer) => Layer + (values: Record): (self: Layer) => Layer + (self: Layer, key: string, value: unknown): Layer + (self: Layer, values: Record): Layer +} = internal.annotateLogs + +/** + * @since 3.3.0 + * @category tracing + */ +export const annotateSpans: { + (key: string, value: unknown): (self: Layer) => Layer + (values: Record): (self: Layer) => Layer + (self: Layer, key: string, value: unknown): Layer + (self: Layer, values: Record): Layer +} = internal.annotateSpans + +/** + * Builds a layer into a scoped value. + * + * @since 2.0.0 + * @category destructors + */ +export const build: ( + self: Layer +) => Effect.Effect, E, Scope.Scope | RIn> = internal.build + +/** + * Builds a layer into an `Effect` value. Any resources associated with this + * layer will be released when the specified scope is closed unless their scope + * has been extended. This allows building layers where the lifetime of some of + * the services output by the layer exceed the lifetime of the effect the + * layer is provided to. + * + * @since 2.0.0 + * @category destructors + */ +export const buildWithScope: { + (scope: Scope.Scope): (self: Layer) => Effect.Effect, E, RIn> + (self: Layer, scope: Scope.Scope): Effect.Effect, E, RIn> +} = internal.buildWithScope + +/** + * Recovers from all errors. + * + * @since 2.0.0 + * @category error handling + */ +export const catchAll: { + ( + onError: (error: E) => Layer + ): (self: Layer) => Layer + ( + self: Layer, + onError: (error: E) => Layer + ): Layer +} = internal.catchAll + +/** + * Recovers from all errors. + * + * @since 2.0.0 + * @category error handling + */ +export const catchAllCause: { + ( + onError: (cause: Cause.Cause) => Layer + ): (self: Layer) => Layer + ( + self: Layer, + onError: (cause: Cause.Cause) => Layer + ): Layer +} = internal.catchAllCause + +/** + * Constructs a `Layer` that passes along the specified context as an + * output. + * + * @since 2.0.0 + * @category constructors + */ +export const context: () => Layer = internal.context + +/** + * Constructs a layer that dies with the specified defect. + * + * @since 2.0.0 + * @category constructors + */ +export const die: (defect: unknown) => Layer = internal.die + +/** + * Constructs a layer that dies with the specified defect. + * + * @since 2.0.0 + * @category constructors + */ +export const dieSync: (evaluate: LazyArg) => Layer = internal.dieSync + +/** + * Replaces the layer's output with `never` and includes the layer only for its + * side-effects. + * + * @since 2.0.0 + * @category mapping + */ +export const discard: (self: Layer) => Layer = internal.discard + +/** + * Constructs a layer from the specified effect. + * + * @since 2.0.0 + * @category constructors + */ +export const effect: { + (tag: Context.Tag): (effect: Effect.Effect, E, R>) => Layer + (tag: Context.Tag, effect: Effect.Effect, E, R>): Layer +} = internal.fromEffect + +/** + * Constructs a layer from the specified effect, discarding its output. + * + * @since 2.0.0 + * @category constructors + */ +export const effectDiscard: (effect: Effect.Effect) => Layer = internal.fromEffectDiscard + +/** + * Constructs a layer from the specified effect, which must return one or more + * services. + * + * @since 2.0.0 + * @category constructors + */ +export const effectContext: (effect: Effect.Effect, E, R>) => Layer = + internal.fromEffectContext + +/** + * A Layer that constructs an empty Context. + * + * @since 2.0.0 + * @category constructors + */ +export const empty: Layer = internal.empty + +/** + * Extends the scope of this layer, returning a new layer that when provided + * to an effect will not immediately release its associated resources when + * that effect completes execution but instead when the scope the resulting + * effect depends on is closed. + * + * @since 2.0.0 + * @category utils + */ +export const extendScope: (self: Layer) => Layer = + internal.extendScope + +/** + * Constructs a layer that fails with the specified error. + * + * @since 2.0.0 + * @category constructors + */ +export const fail: (error: E) => Layer = internal.fail + +/** + * Constructs a layer that fails with the specified error. + * + * @since 2.0.0 + * @category constructors + */ +export const failSync: (evaluate: LazyArg) => Layer = internal.failSync + +/** + * Constructs a layer that fails with the specified cause. + * + * @since 2.0.0 + * @category constructors + */ +export const failCause: (cause: Cause.Cause) => Layer = internal.failCause + +/** + * Constructs a layer that fails with the specified cause. + * + * @since 2.0.0 + * @category constructors + */ +export const failCauseSync: (evaluate: LazyArg>) => Layer = internal.failCauseSync + +/** + * Constructs a layer dynamically based on the output of this layer. + * + * @since 2.0.0 + * @category sequencing + */ +export const flatMap: { + ( + f: (context: Context.Context) => Layer + ): (self: Layer) => Layer + ( + self: Layer, + f: (context: Context.Context) => Layer + ): Layer +} = internal.flatMap + +/** + * Flattens layers nested in the context of an effect. + * + * @since 2.0.0 + * @category sequencing + */ +export const flatten: { + (tag: Context.Tag>): (self: Layer) => Layer + (self: Layer, tag: Context.Tag>): Layer +} = internal.flatten + +/** + * Creates a fresh version of this layer that will not be shared. + * + * @since 2.0.0 + * @category utils + */ +export const fresh: (self: Layer) => Layer = internal.fresh + +/** + * @since 3.17.0 + * @category Testing + */ +export type PartialEffectful = Types.Simplify< + & { + [ + K in keyof A as A[K] extends + | Effect.Effect + | Stream.Stream + | ((...args: any) => Effect.Effect | Stream.Stream) ? K + : never + ]?: A[K] + } + & { + [ + K in keyof A as A[K] extends + | Effect.Effect + | Stream.Stream + | ((...args: any) => Effect.Effect | Stream.Stream) ? never + : K + ]: A[K] + } +> + +/** + * Creates a mock layer for testing purposes. You can provide a partial + * implementation of the service, and any methods not provided will + * throw an `UnimplementedError` defect when called. + * + * **Example** + * + * ```ts + * import { Context, Effect, Layer } from "effect" + * + * class MyService extends Context.Tag("MyService")< + * MyService, + * { + * one: Effect.Effect + * two(): Effect.Effect + * } + * >() {} + * + * const MyServiceTest = Layer.mock(MyService, { + * two: () => Effect.succeed(2) + * }) + * ``` + * + * @since 3.17.0 + * @category Testing + */ +export const mock: { + (tag: Context.Tag): (service: PartialEffectful) => Layer + (tag: Context.Tag, service: PartialEffectful): Layer +} = internal.mock + +const fromFunction: ( + tagA: Context.Tag, + tagB: Context.Tag, + f: (a: Types.NoInfer) => Types.NoInfer +) => Layer = internal.fromFunction + +export { + /** + * Constructs a layer from the context using the specified function. + * + * @since 2.0.0 + * @category constructors + */ + fromFunction as function +} + +/** + * Builds this layer and uses it until it is interrupted. This is useful when + * your entire application is a layer, such as an HTTP server. + * + * @since 2.0.0 + * @category conversions + */ +export const launch: (self: Layer) => Effect.Effect = internal.launch + +/** + * Returns a new layer whose output is mapped by the specified function. + * + * @since 2.0.0 + * @category mapping + */ +export const map: { + (f: (context: Context.Context) => Context.Context): (self: Layer) => Layer + (self: Layer, f: (context: Context.Context) => Context.Context): Layer +} = internal.map + +/** + * Returns a layer with its error channel mapped using the specified function. + * + * @since 2.0.0 + * @category mapping + */ +export const mapError: { + (f: (error: E) => E2): (self: Layer) => Layer + (self: Layer, f: (error: E) => E2): Layer +} = internal.mapError + +/** + * Feeds the error or output services of this layer into the input of either + * the specified `failure` or `success` layers, resulting in a new layer with + * the inputs of this layer, and the error or outputs of the specified layer. + * + * @since 2.0.0 + * @category folding + */ +export const match: { + ( + options: { + readonly onFailure: (error: E) => Layer + readonly onSuccess: (context: Context.Context) => Layer + } + ): (self: Layer) => Layer + ( + self: Layer, + options: { + readonly onFailure: (error: E) => Layer + readonly onSuccess: (context: Context.Context) => Layer + } + ): Layer +} = internal.match + +/** + * Feeds the error or output services of this layer into the input of either + * the specified `failure` or `success` layers, resulting in a new layer with + * the inputs of this layer, and the error or outputs of the specified layer. + * + * @since 2.0.0 + * @category folding + */ +export const matchCause: { + ( + options: { + readonly onFailure: (cause: Cause.Cause) => Layer + readonly onSuccess: (context: Context.Context) => Layer + } + ): (self: Layer) => Layer + ( + self: Layer, + options: { + readonly onFailure: (cause: Cause.Cause) => Layer + readonly onSuccess: (context: Context.Context) => Layer + } + ): Layer +} = internal.matchCause + +/** + * Returns a scoped effect that, if evaluated, will return the lazily computed + * result of this layer. + * + * @since 2.0.0 + * @category utils + */ +export const memoize: ( + self: Layer +) => Effect.Effect, never, Scope.Scope> = internal.memoize + +/** + * Merges this layer with the specified layer concurrently, producing a new layer with combined input and output types. + * + * @since 2.0.0 + * @category zipping + */ +export const merge: { + ( + that: Layer + ): (self: Layer) => Layer + ( + self: Layer, + that: Layer + ): Layer +} = internal.merge + +/** + * Combines all the provided layers concurrently, creating a new layer with merged input, error, and output types. + * + * @since 2.0.0 + * @category zipping + */ +export const mergeAll: , ...Array>]>( + ...layers: Layers +) => Layer< + { [k in keyof Layers]: Layer.Success }[number], + { [k in keyof Layers]: Layer.Error }[number], + { [k in keyof Layers]: Layer.Context }[number] +> = internal.mergeAll + +/** + * Translates effect failure into death of the fiber, making all failures + * unchecked and not a part of the type of the layer. + * + * @since 2.0.0 + * @category error handling + */ +export const orDie: (self: Layer) => Layer = internal.orDie + +/** + * Executes this layer and returns its output, if it succeeds, but otherwise + * executes the specified layer. + * + * @since 2.0.0 + * @category error handling + */ +export const orElse: { + (that: LazyArg>): (self: Layer) => Layer + (self: Layer, that: LazyArg>): Layer +} = internal.orElse + +/** + * Returns a new layer that produces the outputs of this layer but also + * passes through the inputs. + * + * @since 2.0.0 + * @category utils + */ +export const passthrough: (self: Layer) => Layer = internal.passthrough + +/** + * Projects out part of one of the services output by this layer using the + * specified function. + * + * @since 2.0.0 + * @category utils + */ +export const project: { + ( + tagA: Context.Tag, + tagB: Context.Tag, + f: (a: Types.NoInfer) => Types.NoInfer + ): (self: Layer) => Layer + ( + self: Layer, + tagA: Context.Tag, + tagB: Context.Tag, + f: (a: Types.NoInfer) => Types.NoInfer + ): Layer +} = internal.project + +/** + * @since 2.0.0 + * @category utils + */ +export const locallyEffect: { + ( + f: (_: Effect.Effect>) => Effect.Effect> + ): (self: Layer) => Layer + ( + self: Layer, + f: (_: Effect.Effect>) => Effect.Effect> + ): Layer +} = internal.locallyEffect + +/** + * @since 2.0.0 + * @category utils + */ +export const locally: { + ( + ref: FiberRef, + value: X + ): (self: Layer) => Layer + ( + self: Layer, + ref: FiberRef, + value: X + ): Layer +} = internal.fiberRefLocally + +/** + * @since 2.0.0 + * @category utils + */ +export const locallyWith: { + (ref: FiberRef, value: (_: X) => X): (self: Layer) => Layer + (self: Layer, ref: FiberRef, value: (_: X) => X): Layer +} = internal.fiberRefLocallyWith + +/** + * @since 2.0.0 + * @category utils + */ +export const locallyScoped: (self: FiberRef, value: A) => Layer = internal.fiberRefLocallyScoped + +/** + * @since 2.0.0 + * @category utils + */ +export const fiberRefLocallyScopedWith: (self: FiberRef, value: (_: A) => A) => Layer = + internal.fiberRefLocallyScopedWith + +/** + * Retries constructing this layer according to the specified schedule. + * + * @since 2.0.0 + * @category retrying + */ +export const retry: { + ( + schedule: Schedule.Schedule, RIn2> + ): (self: Layer) => Layer + ( + self: Layer, + schedule: Schedule.Schedule + ): Layer +} = internal.retry + +/** + * A layer that constructs a scope and closes it when the workflow the layer + * is provided to completes execution, whether by success, failure, or + * interruption. This can be used to close a scope when providing a layer to a + * workflow. + * + * @since 2.0.0 + * @category constructors + */ +export const scope: Layer = internal.scope + +/** + * Constructs a layer from the specified scoped effect. + * + * @since 2.0.0 + * @category constructors + */ +export const scoped: { + ( + tag: Context.Tag + ): (effect: Effect.Effect, E, R>) => Layer> + ( + tag: Context.Tag, + effect: Effect.Effect, E, R> + ): Layer> +} = internal.scoped + +/** + * Constructs a layer from the specified scoped effect, discarding its output. + * + * @since 2.0.0 + * @category constructors + */ +export const scopedDiscard: (effect: Effect.Effect) => Layer> = + internal.scopedDiscard + +/** + * Constructs a layer from the specified scoped effect, which must return one + * or more services. + * + * @since 2.0.0 + * @category constructors + */ +export const scopedContext: ( + effect: Effect.Effect, E, R> +) => Layer> = internal.scopedContext + +/** + * Constructs a layer that accesses and returns the specified service from the + * context. + * + * @since 2.0.0 + * @category constructors + */ +export const service: (tag: Context.Tag) => Layer = internal.service + +/** + * Constructs a layer from the specified value. + * + * @since 2.0.0 + * @category constructors + */ +export const succeed: { + (tag: Context.Tag): (resource: Types.NoInfer) => Layer + (tag: Context.Tag, resource: Types.NoInfer): Layer +} = internal.succeed + +/** + * Constructs a layer from the specified value, which must return one or more + * services. + * + * @since 2.0.0 + * @category constructors + */ +export const succeedContext: (context: Context.Context) => Layer = internal.succeedContext + +/** + * Lazily constructs a layer. This is useful to avoid infinite recursion when + * creating layers that refer to themselves. + * + * @since 2.0.0 + * @category constructors + */ +export const suspend: (evaluate: LazyArg>) => Layer = internal.suspend + +/** + * Lazily constructs a layer from the specified value. + * + * @since 2.0.0 + * @category constructors + */ +export const sync: { + (tag: Context.Tag): (evaluate: LazyArg>) => Layer + (tag: Context.Tag, evaluate: LazyArg>): Layer +} = internal.sync + +/** + * Lazily constructs a layer from the specified value, which must return one or more + * services. + * + * @since 2.0.0 + * @category constructors + */ +export const syncContext: (evaluate: LazyArg>) => Layer = internal.syncContext + +/** + * Performs the specified effect if this layer succeeds. + * + * @since 2.0.0 + * @category sequencing + */ +export const tap: { + ( + f: (context: Context.Context) => Effect.Effect + ): (self: Layer) => Layer + ( + self: Layer, + f: (context: Context.Context) => Effect.Effect + ): Layer +} = internal.tap + +/** + * Performs the specified effect if this layer fails. + * + * @since 2.0.0 + * @category sequencing + */ +export const tapError: { + ( + f: (e: XE) => Effect.Effect + ): (self: Layer) => Layer + ( + self: Layer, + f: (e: XE) => Effect.Effect + ): Layer +} = internal.tapError + +/** + * Performs the specified effect if this layer fails. + * + * @since 2.0.0 + * @category sequencing + */ +export const tapErrorCause: { + ( + f: (cause: Cause.Cause) => Effect.Effect + ): (self: Layer) => Layer + ( + self: Layer, + f: (cause: Cause.Cause) => Effect.Effect + ): Layer +} = internal.tapErrorCause + +/** + * Converts a layer that requires no services into a scoped runtime, which can + * be used to execute effects. + * + * @since 2.0.0 + * @category conversions + */ +export const toRuntime: ( + self: Layer +) => Effect.Effect, E, Scope.Scope | RIn> = internal.toRuntime + +/** + * Converts a layer that requires no services into a scoped runtime, which can + * be used to execute effects. + * + * @since 2.0.0 + * @category conversions + */ +export const toRuntimeWithMemoMap: { + ( + memoMap: MemoMap + ): (self: Layer) => Effect.Effect, E, Scope.Scope | RIn> + ( + self: Layer, + memoMap: MemoMap + ): Effect.Effect, E, Scope.Scope | RIn> +} = internal.toRuntimeWithMemoMap + +/** + * Feeds the output services of this builder into the input of the specified + * builder, resulting in a new builder with the inputs of this builder as + * well as any leftover inputs, and the outputs of the specified builder. + * + * @since 2.0.0 + * @category utils + */ +export const provide: { + ( + that: Layer + ): (self: Layer) => Layer> + ]>( + that: Layers + ): ( + self: Layer + ) => Layer< + A, + E | { [k in keyof Layers]: Layer.Error }[number], + | { [k in keyof Layers]: Layer.Context }[number] + | Exclude }[number]> + > + ( + self: Layer, + that: Layer + ): Layer> + ]>( + self: Layer, + that: Layers + ): Layer< + A, + E | { [k in keyof Layers]: Layer.Error }[number], + | { [k in keyof Layers]: Layer.Context }[number] + | Exclude }[number]> + > +} = internal.provide + +/** + * Feeds the output services of this layer into the input of the specified + * layer, resulting in a new layer with the inputs of this layer, and the + * outputs of both layers. + * + * @since 2.0.0 + * @category utils + */ +export const provideMerge: { + ( + self: Layer + ): (that: Layer) => Layer> + ( + that: Layer, + self: Layer + ): Layer> +} = internal.provideMerge + +/** + * Combines this layer with the specified layer concurrently, creating a new layer with merged input types and + * combined output types using the provided function. + * + * @since 2.0.0 + * @category zipping + */ +export const zipWith: { + ( + that: Layer, + f: (a: Context.Context, b: Context.Context) => Context.Context + ): (self: Layer) => Layer + ( + self: Layer, + that: Layer, + f: (a: Context.Context, b: Context.Context) => Context.Context + ): Layer +} = internal.zipWith + +/** + * @since 2.0.0 + * @category utils + */ +export const unwrapEffect: (self: Effect.Effect, E, R>) => Layer = + internal.unwrapEffect + +/** + * @since 2.0.0 + * @category utils + */ +export const unwrapScoped: ( + self: Effect.Effect, E, R> +) => Layer> = internal.unwrapScoped + +/** + * @since 2.0.0 + * @category clock + */ +export const setClock: (clock: A) => Layer = ( + clock: A +): Layer => + scopedDiscard( + fiberRuntime.fiberRefLocallyScopedWith(defaultServices.currentServices, Context.add(clockTag, clock)) + ) + +/** + * Sets the current `ConfigProvider`. + * + * @since 2.0.0 + * @category config + */ +export const setConfigProvider: (configProvider: ConfigProvider) => Layer = circularLayer.setConfigProvider + +/** + * Adds the provided span to the span stack. + * + * @since 2.0.0 + * @category tracing + */ +export const parentSpan: (span: Tracer.AnySpan) => Layer = circularLayer.parentSpan + +/** + * @since 3.15.0 + * @category Random + */ +export const setRandom = (random: A): Layer => + scopedDiscard( + fiberRuntime.fiberRefLocallyScopedWith(defaultServices.currentServices, Context.add(randomTag, random)) + ) + +/** + * @since 2.0.0 + * @category requests & batching + */ +export const setRequestBatching: (requestBatching: boolean) => Layer = ( + requestBatching: boolean +) => + scopedDiscard( + fiberRuntime.fiberRefLocallyScoped(core.currentRequestBatching, requestBatching) + ) + +/** + * @since 2.0.0 + * @category requests & batching + */ +export const setRequestCaching: (requestCaching: boolean) => Layer = ( + requestCaching: boolean +) => + scopedDiscard( + fiberRuntime.fiberRefLocallyScoped(query.currentCacheEnabled, requestCaching) + ) + +/** + * @since 2.0.0 + * @category requests & batching + */ +export const setRequestCache: { + ( + cache: Effect.Effect + ): Layer> + ( + cache: Request.Cache + ): Layer +} = ((cache: Request.Cache | Effect.Effect) => + scopedDiscard( + core.isEffect(cache) ? + core.flatMap(cache, (x) => fiberRuntime.fiberRefLocallyScoped(query.currentCache as any, x)) : + fiberRuntime.fiberRefLocallyScoped(query.currentCache as any, cache) + )) as any + +/** + * @since 2.0.0 + * @category scheduler + */ +export const setScheduler: (scheduler: Scheduler.Scheduler) => Layer = ( + scheduler: Scheduler.Scheduler +): Layer => + scopedDiscard( + fiberRuntime.fiberRefLocallyScoped(Scheduler.currentScheduler, scheduler) + ) + +/** + * Create and add a span to the current span stack. + * + * The span is ended when the Layer is released. + * + * @since 2.0.0 + * @category tracing + */ +export const span: ( + name: string, + options?: Tracer.SpanOptions & { + readonly onEnd?: + | ((span: Tracer.Span, exit: Exit.Exit) => Effect.Effect) + | undefined + } +) => Layer = circularLayer.span + +/** + * Create a Layer that sets the current Tracer + * + * @since 2.0.0 + * @category tracing + */ +export const setTracer: (tracer: Tracer.Tracer) => Layer = circularLayer.setTracer + +/** + * @since 2.0.0 + * @category tracing + */ +export const setTracerEnabled: (enabled: boolean) => Layer = (enabled: boolean) => + scopedDiscard( + fiberRuntime.fiberRefLocallyScoped(core.currentTracerEnabled, enabled) + ) + +/** + * @since 2.0.0 + * @category tracing + */ +export const setTracerTiming: (enabled: boolean) => Layer = (enabled: boolean) => + scopedDiscard( + fiberRuntime.fiberRefLocallyScoped(core.currentTracerTimingEnabled, enabled) + ) + +/** + * @since 2.0.0 + * @category logging + */ +export const setUnhandledErrorLogLevel: (level: Option.Option) => Layer = ( + level: Option.Option +): Layer => + scopedDiscard( + fiberRuntime.fiberRefLocallyScoped(core.currentUnhandledErrorLogLevel, level) + ) + +/** + * @since 3.17.0 + * @category logging + */ +export const setVersionMismatchErrorLogLevel: (level: Option.Option) => Layer = ( + level: Option.Option +): Layer => + scopedDiscard( + fiberRuntime.fiberRefLocallyScoped(core.currentVersionMismatchErrorLogLevel, level) + ) + +/** + * @since 2.0.0 + * @category tracing + */ +export const withSpan: { + ( + name: string, + options?: Tracer.SpanOptions & { + readonly onEnd?: + | ((span: Tracer.Span, exit: Exit.Exit) => Effect.Effect) + | undefined + } + ): (self: Layer) => Layer> + ( + self: Layer, + name: string, + options?: Tracer.SpanOptions & { + readonly onEnd?: + | ((span: Tracer.Span, exit: Exit.Exit) => Effect.Effect) + | undefined + } + ): Layer> +} = internal.withSpan + +/** + * @since 2.0.0 + * @category tracing + */ +export const withParentSpan: { + (span: Tracer.AnySpan): (self: Layer) => Layer> + (self: Layer, span: Tracer.AnySpan): Layer> +} = internal.withParentSpan + +// ----------------------------------------------------------------------------- +// memo map +// ----------------------------------------------------------------------------- + +/** + * Constructs a `MemoMap` that can be used to build additional layers. + * + * @since 2.0.0 + * @category memo map + */ +export const makeMemoMap: Effect.Effect = internal.makeMemoMap + +/** + * Builds a layer into an `Effect` value, using the specified `MemoMap` to memoize + * the layer construction. + * + * @since 2.0.0 + * @category memo map + */ +export const buildWithMemoMap: { + ( + memoMap: MemoMap, + scope: Scope.Scope + ): (self: Layer) => Effect.Effect, E, RIn> + ( + self: Layer, + memoMap: MemoMap, + scope: Scope.Scope + ): Effect.Effect, E, RIn> +} = internal.buildWithMemoMap + +/** + * Updates a service in the context with a new implementation. + * + * **Details** + * + * This function modifies the existing implementation of a service in the + * context. It retrieves the current service, applies the provided + * transformation function `f`, and replaces the old service with the + * transformed one. + * + * **When to Use** + * + * This is useful for adapting or extending a service's behavior during the + * creation of a layer. + * + * @since 3.13.0 + * @category utils + */ +export const updateService = dual< + ( + tag: Context.Tag, + f: (a: A) => A + ) => (layer: Layer) => Layer, + ( + layer: Layer, + tag: Context.Tag, + f: (a: A) => A + ) => Layer +>(3, (layer, tag, f) => + provide( + layer, + map(context(), (c) => Context.add(c, tag, f(Context.unsafeGet(c, tag)))) + )) + +// ----------------------------------------------------------------------------- +// Type constraints +// ----------------------------------------------------------------------------- + +/** + * A no-op type constraint that enforces the success channel of a Layer conforms to + * the specified success type `ROut`. + * + * @example + * import { Layer } from "effect" + * + * // Ensure that the layer produces the expected services. + * const program = Layer.succeed(MyService, new MyServiceImpl()).pipe(Layer.ensureSuccessType()) + * + * @since 3.20.0 + * @category Type constraints + */ +export const ensureSuccessType = + () => (layer: Layer): Layer => layer + +/** + * A no-op type constraint that enforces the error channel of a Layer conforms to + * the specified error type `E`. + * + * @example + * import { Layer } from "effect" + * + * // Ensure that the layer does not expose any unhandled errors. + * const program = Layer.succeed(MyService, new MyServiceImpl()).pipe(Layer.ensureErrorType()) + * + * @since 3.20.0 + * @category Type constraints + */ +export const ensureErrorType = () => (layer: Layer): Layer => + layer + +/** + * A no-op type constraint that enforces the requirements channel of a Layer conforms to + * the specified requirements type `RIn`. + * + * @example + * import { Layer } from "effect" + * + * // Ensure that the layer does not have any requirements. + * const program = Layer.succeed(MyService, new MyServiceImpl()).pipe(Layer.ensureRequirementsType()) + * + * @since 3.20.0 + * @category Type constraints + */ +export const ensureRequirementsType = + () => (layer: Layer): Layer => layer diff --git a/repos/effect/packages/effect/src/LayerMap.ts b/repos/effect/packages/effect/src/LayerMap.ts new file mode 100644 index 0000000..15deb39 --- /dev/null +++ b/repos/effect/packages/effect/src/LayerMap.ts @@ -0,0 +1,436 @@ +/** + * @since 3.14.0 + * @experimental + */ +import * as Context from "./Context.js" +import type * as Duration from "./Duration.js" +import * as Effect from "./Effect.js" +import * as FiberRefsPatch from "./FiberRefsPatch.js" +import { identity } from "./Function.js" +import * as core from "./internal/core.js" +import * as Layer from "./Layer.js" +import * as RcMap from "./RcMap.js" +import * as Runtime from "./Runtime.js" +import * as Scope from "./Scope.js" +import type { Mutable, NoExcessProperties } from "./Types.js" + +/** + * @since 3.14.0 + * @category Symbols + */ +export const TypeId: unique symbol = Symbol.for("effect/LayerMap") + +/** + * @since 3.14.0 + * @category Symbols + */ +export type TypeId = typeof TypeId + +/** + * @since 3.14.0 + * @category Models + * @experimental + */ +export interface LayerMap { + readonly [TypeId]: TypeId + + /** + * The internal RcMap that stores the resources. + */ + readonly rcMap: RcMap.RcMap + readonly runtimeEffect: Effect.Effect, E, Scope.Scope> + }, E> + + /** + * Retrieves a Layer for the resources associated with the key. + */ + get(key: K): Layer.Layer + + /** + * Retrieves a Runtime for the resources associated with the key. + */ + runtime(key: K): Effect.Effect, E, Scope.Scope> + + /** + * Invalidates the resource associated with the key. + */ + invalidate(key: K): Effect.Effect +} + +/** + * @since 3.14.0 + * @category Constructors + * @experimental + * + * A `LayerMap` allows you to create a map of Layer's that can be used to + * dynamically access resources based on a key. + * + * ```ts + * import { NodeRuntime } from "@effect/platform-node" + * import { Context, Effect, FiberRef, Layer, LayerMap } from "effect" + * + * class Greeter extends Context.Tag("Greeter") + * }>() {} + * + * // create a service that wraps a LayerMap + * class GreeterMap extends LayerMap.Service()("GreeterMap", { + * // define the lookup function for the layer map + * // + * // The returned Layer will be used to provide the Greeter service for the + * // given name. + * lookup: (name: string) => + * Layer.succeed(Greeter, { + * greet: Effect.succeed(`Hello, ${name}!`) + * }).pipe( + * Layer.merge(Layer.locallyScoped(FiberRef.currentConcurrency, 123)) + * ), + * + * // If a layer is not used for a certain amount of time, it can be removed + * idleTimeToLive: "5 seconds", + * + * // Supply the dependencies for the layers in the LayerMap + * dependencies: [] + * }) {} + * + * // usage + * const program: Effect.Effect = Effect.gen(function*() { + * // access and use the Greeter service + * const greeter = yield* Greeter + * yield* Effect.log(yield* greeter.greet) + * }).pipe( + * // use the GreeterMap service to provide a variant of the Greeter service + * Effect.provide(GreeterMap.get("John")) + * ) + * + * // run the program + * program.pipe( + * Effect.provide(GreeterMap.Default), + * NodeRuntime.runMain + * ) + * ``` + */ +export const make: < + K, + L extends Layer.Layer, + PreloadKeys extends Iterable | undefined = undefined +>( + lookup: (key: K) => L, + options?: { + readonly idleTimeToLive?: Duration.DurationInput | undefined + readonly preloadKeys?: PreloadKeys + } | undefined +) => Effect.Effect< + LayerMap< + K, + L extends Layer.Layer ? _A : never, + L extends Layer.Layer ? _E : never + >, + PreloadKeys extends undefined ? never : L extends Layer.Layer ? _E : never, + Scope.Scope | (L extends Layer.Layer ? _R : never) +> = Effect.fnUntraced(function*( + lookup: (key: K) => Layer.Layer, + options?: { + readonly idleTimeToLive?: Duration.DurationInput | undefined + readonly preloadKeys?: Iterable | undefined + } | undefined +) { + const context = yield* Effect.context() + + // If we are inside another layer build, use the current memo map, + // otherwise create a new one. + const memoMap = context.unsafeMap.has(Layer.CurrentMemoMap.key) + ? Context.get(context, Layer.CurrentMemoMap) + : yield* Layer.makeMemoMap + + const rcMap = yield* RcMap.make({ + lookup: (key: K) => + Effect.scopeWith((scope) => Effect.diffFiberRefs(Layer.buildWithMemoMap(lookup(key), memoMap, scope))).pipe( + Effect.map(([patch, context]) => ({ + layer: Layer.scopedContext( + core.withFiberRuntime, any, Scope.Scope>((fiber) => { + const scope = Context.unsafeGet(fiber.currentContext, Scope.Scope) + const oldRefs = fiber.getFiberRefs() + const newRefs = FiberRefsPatch.patch(patch, fiber.id(), oldRefs) + const revert = FiberRefsPatch.diff(newRefs, oldRefs) + fiber.setFiberRefs(newRefs) + return Effect.as( + Scope.addFinalizerExit(scope, () => { + fiber.setFiberRefs(FiberRefsPatch.patch(revert, fiber.id(), fiber.getFiberRefs())) + return Effect.void + }), + context + ) + }) + ), + runtimeEffect: Effect.withFiberRuntime, any, Scope.Scope>((fiber) => { + const fiberRefs = FiberRefsPatch.patch(patch, fiber.id(), fiber.getFiberRefs()) + return Effect.succeed(Runtime.make({ + context, + fiberRefs, + runtimeFlags: Runtime.defaultRuntime.runtimeFlags + })) + }) + } as const)) + ), + idleTimeToLive: options?.idleTimeToLive + }) + + if (options?.preloadKeys) { + for (const key of options.preloadKeys) { + yield* (RcMap.get(rcMap, key) as Effect.Effect) + } + } + + return identity, any>>({ + [TypeId]: TypeId, + rcMap, + get: (key) => Layer.unwrapScoped(Effect.map(RcMap.get(rcMap, key), ({ layer }) => layer)), + runtime: (key) => Effect.flatMap(RcMap.get(rcMap, key), ({ runtimeEffect }) => runtimeEffect), + invalidate: (key) => RcMap.invalidate(rcMap, key) + }) +}) + +/** + * @since 3.14.0 + * @category Constructors + * @experimental + */ +export const fromRecord = < + const Layers extends Record>, + const Preload extends boolean = false +>( + layers: Layers, + options?: { + readonly idleTimeToLive?: Duration.DurationInput | undefined + readonly preload?: Preload | undefined + } | undefined +): Effect.Effect< + LayerMap< + keyof Layers, + Layers[keyof Layers] extends Layer.Layer ? _A : never, + Preload extends true ? never : Layers[keyof Layers] extends Layer.Layer ? _E : never + >, + Preload extends true ? never : Layers[keyof Layers] extends Layer.Layer ? _E : never, + Scope.Scope | (Layers[keyof Layers] extends Layer.Layer ? _R : never) +> => + make((key: keyof Layers) => layers[key], { + ...options, + preloadKeys: options?.preload ? Object.keys(layers) : undefined + }) as any + +/** + * @since 3.14.0 + * @category Service + */ +export interface TagClass< + in out Self, + in out Id extends string, + in out K, + in out I, + in out E, + in out R, + in out LE, + in out Deps extends Layer.Layer +> extends Context.TagClass> { + /** + * A default layer for the `LayerMap` service. + */ + readonly Default: Layer.Layer< + Self, + (Deps extends Layer.Layer ? _E : never) | LE, + | Exclude ? _A : never)> + | (Deps extends Layer.Layer ? _R : never) + > + + /** + * A default layer for the `LayerMap` service without the dependencies provided. + */ + readonly DefaultWithoutDependencies: Layer.Layer + + /** + * Retrieves a Layer for the resources associated with the key. + */ + readonly get: (key: K) => Layer.Layer + + /** + * Retrieves a Runtime for the resources associated with the key. + */ + readonly runtime: (key: K) => Effect.Effect, E, Scope.Scope | Self> + + /** + * Invalidates the resource associated with the key. + */ + readonly invalidate: (key: K) => Effect.Effect +} + +/** + * @since 3.14.0 + * @category Service + * @experimental + * + * Create a `LayerMap` service that provides a dynamic set of resources based on + * a key. + * + * ```ts + * import { NodeRuntime } from "@effect/platform-node" + * import { Context, Effect, FiberRef, Layer, LayerMap } from "effect" + * + * class Greeter extends Context.Tag("Greeter") + * }>() {} + * + * // create a service that wraps a LayerMap + * class GreeterMap extends LayerMap.Service()("GreeterMap", { + * // define the lookup function for the layer map + * // + * // The returned Layer will be used to provide the Greeter service for the + * // given name. + * lookup: (name: string) => + * Layer.succeed(Greeter, { + * greet: Effect.succeed(`Hello, ${name}!`) + * }).pipe( + * Layer.merge(Layer.locallyScoped(FiberRef.currentConcurrency, 123)) + * ), + * + * // If a layer is not used for a certain amount of time, it can be removed + * idleTimeToLive: "5 seconds", + * + * // Supply the dependencies for the layers in the LayerMap + * dependencies: [] + * }) {} + * + * // usage + * const program: Effect.Effect = Effect.gen(function*() { + * // access and use the Greeter service + * const greeter = yield* Greeter + * yield* Effect.log(yield* greeter.greet) + * }).pipe( + * // use the GreeterMap service to provide a variant of the Greeter service + * Effect.provide(GreeterMap.get("John")) + * ) + * + * // run the program + * program.pipe( + * Effect.provide(GreeterMap.Default), + * NodeRuntime.runMain + * ) + * ``` + */ +export const Service = () => +< + const Id extends string, + Options extends + | NoExcessProperties< + { + readonly lookup: (key: any) => Layer.Layer + readonly dependencies?: ReadonlyArray> + readonly idleTimeToLive?: Duration.DurationInput | undefined + readonly preloadKeys?: + | Iterable any } ? K : never> + | undefined + }, + Options + > + | NoExcessProperties<{ + readonly layers: Record> + readonly dependencies?: ReadonlyArray> + readonly idleTimeToLive?: Duration.DurationInput | undefined + readonly preload?: boolean + }, Options> +>( + id: Id, + options: Options +): TagClass< + Self, + Id, + Options extends { readonly lookup: (key: infer K) => any } ? K + : Options extends { readonly layers: infer Layers } ? keyof Layers + : never, + Service.Success, + Options extends { readonly preload: true } ? never : Service.Error, + Service.Context, + Options extends { readonly preload: true } ? Service.Error + : Options extends { readonly preloadKey: Iterable } ? Service.Error + : never, + Options extends { readonly dependencies: ReadonlyArray } ? Options["dependencies"][number] : never +> => { + const Err = globalThis.Error as any + const limit = Err.stackTraceLimit + Err.stackTraceLimit = 2 + const creationError = new Err() + Err.stackTraceLimit = limit + + function TagClass() {} + const TagClass_ = TagClass as any as Mutable> + Object.setPrototypeOf(TagClass, Object.getPrototypeOf(Context.GenericTag(id))) + TagClass.key = id + Object.defineProperty(TagClass, "stack", { + get() { + return creationError.stack + } + }) + + TagClass_.DefaultWithoutDependencies = Layer.scoped( + TagClass_, + "lookup" in options + ? make(options.lookup, options) + : fromRecord(options.layers as any, options) + ) + TagClass_.Default = options.dependencies && options.dependencies.length > 0 ? + Layer.provide(TagClass_.DefaultWithoutDependencies, options.dependencies as any) : + TagClass_.DefaultWithoutDependencies + + TagClass_.get = (key: string) => Layer.unwrapScoped(Effect.map(TagClass_, (layerMap) => layerMap.get(key))) + TagClass_.runtime = (key: string) => Effect.flatMap(TagClass_, (layerMap) => layerMap.runtime(key)) + TagClass_.invalidate = (key: string) => Effect.flatMap(TagClass_, (layerMap) => layerMap.invalidate(key)) + + return TagClass as any +} + +/** + * @since 3.14.0 + * @category Service + * @experimental + */ +export declare namespace Service { + /** + * @since 3.14.0 + * @category Service + * @experimental + */ + export type Key = Options extends { readonly lookup: (key: infer K) => any } ? K + : Options extends { readonly layers: infer Layers } ? keyof Layers + : never + + /** + * @since 3.14.0 + * @category Service + * @experimental + */ + export type Layers = Options extends { readonly lookup: (key: infer _K) => infer Layers } ? Layers + : Options extends { readonly layers: infer Layers } ? Layers[keyof Layers] + : never + + /** + * @since 3.14.0 + * @category Service + * @experimental + */ + export type Success = Layers extends Layer.Layer ? _A : never + + /** + * @since 3.14.0 + * @category Service + * @experimental + */ + export type Error = Layers extends Layer.Layer ? _E : never + + /** + * @since 3.14.0 + * @category Service + * @experimental + */ + export type Context = Layers extends Layer.Layer ? _R : never +} diff --git a/repos/effect/packages/effect/src/List.ts b/repos/effect/packages/effect/src/List.ts new file mode 100644 index 0000000..528a899 --- /dev/null +++ b/repos/effect/packages/effect/src/List.ts @@ -0,0 +1,977 @@ +/** + * A data type for immutable linked lists representing ordered collections of elements of type `A`. + * + * This data type is optimal for last-in-first-out (LIFO), stack-like access patterns. If you need another access pattern, for example, random access or FIFO, consider using a collection more suited to this than `List`. + * + * **Performance** + * + * - Time: `List` has `O(1)` prepend and head/tail access. Most other operations are `O(n)` on the number of elements in the list. This includes the index-based lookup of elements, `length`, `append` and `reverse`. + * - Space: `List` implements structural sharing of the tail list. This means that many operations are either zero- or constant-memory cost. + * + * @since 2.0.0 + */ + +/** + * This file is ported from + * + * Scala (https://www.scala-lang.org) + * + * Copyright EPFL and Lightbend, Inc. + * + * Licensed under Apache License 2.0 + * (http://www.apache.org/licenses/LICENSE-2.0). + */ +import * as Arr from "./Array.js" +import * as Chunk from "./Chunk.js" +import * as Either from "./Either.js" +import * as Equal from "./Equal.js" +import * as Equivalence from "./Equivalence.js" +import { dual, identity, unsafeCoerce } from "./Function.js" +import * as Hash from "./Hash.js" +import { format, type Inspectable, NodeInspectSymbol, toJSON } from "./Inspectable.js" +import type { nonEmpty, NonEmptyIterable } from "./NonEmptyIterable.js" +import * as Option from "./Option.js" +import type { Pipeable } from "./Pipeable.js" +import { pipeArguments } from "./Pipeable.js" +import { hasProperty, type Predicate, type Refinement } from "./Predicate.js" +import type { NoInfer } from "./Types.js" + +/** + * Represents an immutable linked list of elements of type `A`. + * + * A `List` is optimal for last-in-first-out (LIFO), stack-like access patterns. + * If you need another access pattern, for example, random access or FIFO, + * consider using a collection more suited for that other than `List`. + * + * @since 2.0.0 + * @category models + */ +export type List = Cons | Nil + +/** + * @since 2.0.0 + * @category symbol + */ +export const TypeId: unique symbol = Symbol.for("effect/List") + +/** + * @since 2.0.0 + * @category symbol + */ +export type TypeId = typeof TypeId + +/** + * @since 2.0.0 + * @category models + */ +export interface Nil extends Iterable, Equal.Equal, Pipeable, Inspectable { + readonly [TypeId]: TypeId + readonly _tag: "Nil" +} + +/** + * @since 2.0.0 + * @category models + */ +export interface Cons extends NonEmptyIterable, Equal.Equal, Pipeable, Inspectable { + readonly [TypeId]: TypeId + readonly _tag: "Cons" + readonly head: A + readonly tail: List +} + +/** + * Converts the specified `List` to an `Array`. + * + * @category conversions + * @since 2.0.0 + */ +export const toArray = (self: List): Array => Arr.fromIterable(self) + +/** + * @category equivalence + * @since 2.0.0 + */ +export const getEquivalence = (isEquivalent: Equivalence.Equivalence): Equivalence.Equivalence> => + Equivalence.mapInput(Arr.getEquivalence(isEquivalent), toArray) + +const _equivalence = getEquivalence(Equal.equals) + +const ConsProto: Omit, "head" | "tail" | typeof nonEmpty> = { + [TypeId]: TypeId, + _tag: "Cons", + toString(this: Cons) { + return format(this.toJSON()) + }, + toJSON(this: Cons) { + return { + _id: "List", + _tag: "Cons", + values: toArray(this).map(toJSON) + } + }, + [NodeInspectSymbol]() { + return this.toJSON() + }, + [Equal.symbol](this: Cons, that: unknown): boolean { + return isList(that) && + this._tag === that._tag && + _equivalence(this, that) + }, + [Hash.symbol](this: Cons): number { + return Hash.cached(this, Hash.array(toArray(this))) + }, + [Symbol.iterator](this: Cons): Iterator { + let done = false + // eslint-disable-next-line @typescript-eslint/no-this-alias + let self: List = this + return { + next() { + if (done) { + return this.return!() + } + if (self._tag === "Nil") { + done = true + return this.return!() + } + const value: unknown = self.head + self = self.tail + return { done, value } + }, + return(value?: unknown) { + if (!done) { + done = true + } + return { done: true, value } + } + } + }, + pipe() { + return pipeArguments(this, arguments) + } +} + +interface MutableCons extends Cons { + head: A + tail: List +} + +const makeCons = (head: A, tail: List): MutableCons => { + const cons = Object.create(ConsProto) + cons.head = head + cons.tail = tail + return cons +} + +const NilHash = Hash.string("Nil") +const NilProto: Nil = { + [TypeId]: TypeId, + _tag: "Nil", + toString() { + return format(this.toJSON()) + }, + toJSON() { + return { + _id: "List", + _tag: "Nil" + } + }, + [NodeInspectSymbol]() { + return this.toJSON() + }, + [Hash.symbol](): number { + return NilHash + }, + [Equal.symbol](that: unknown): boolean { + return isList(that) && this._tag === that._tag + }, + [Symbol.iterator](): Iterator { + return { + next() { + return { done: true, value: undefined } + } + } + }, + pipe() { + return pipeArguments(this, arguments) + } +} as const + +const _Nil = Object.create(NilProto) as Nil + +/** + * Returns `true` if the specified value is a `List`, `false` otherwise. + * + * @since 2.0.0 + * @category refinements + */ +export const isList: { + (u: Iterable): u is List + (u: unknown): u is List +} = (u: unknown): u is List => hasProperty(u, TypeId) + +/** + * Returns `true` if the specified value is a `List.Nil`, `false` otherwise. + * + * @since 2.0.0 + * @category refinements + */ +export const isNil = (self: List): self is Nil => self._tag === "Nil" + +/** + * Returns `true` if the specified value is a `List.Cons`, `false` otherwise. + * + * @since 2.0.0 + * @category refinements + */ +export const isCons = (self: List): self is Cons => self._tag === "Cons" + +/** + * Returns the number of elements contained in the specified `List` + * + * @since 2.0.0 + * @category getters + */ +export const size = (self: List): number => { + let these = self + let len = 0 + while (!isNil(these)) { + len += 1 + these = these.tail + } + return len +} + +/** + * Constructs a new empty `List`. + * + * @since 2.0.0 + * @category constructors + */ +export const nil = (): List => _Nil + +/** + * Constructs a new `List.Cons` from the specified `head` and `tail` values. + * + * @since 2.0.0 + * @category constructors + */ +export const cons = (head: A, tail: List): Cons => makeCons(head, tail) + +/** + * Constructs a new empty `List`. + * + * Alias of {@link nil}. + * + * @since 2.0.0 + * @category constructors + */ +export const empty = nil + +/** + * Constructs a new `List` from the specified value. + * + * @since 2.0.0 + * @category constructors + */ +export const of = (value: A): Cons => makeCons(value, _Nil) + +/** + * Creates a new `List` from an iterable collection of values. + * + * @since 2.0.0 + * @category constructors + */ +export const fromIterable = (prefix: Iterable): List => { + const iterator = prefix[Symbol.iterator]() + let next: IteratorResult + if ((next = iterator.next()) && !next.done) { + const result = makeCons(next.value, _Nil) + let curr = result + while ((next = iterator.next()) && !next.done) { + const temp = makeCons(next.value, _Nil) + curr.tail = temp + curr = temp + } + return result + } else { + return _Nil + } +} + +/** + * Constructs a new `List` from the specified values. + * + * @since 2.0.0 + * @category constructors + */ +export const make = ]>( + ...elements: Elements +): Cons => fromIterable(elements) as any + +/** + * Appends the specified element to the end of the `List`, creating a new `Cons`. + * + * @category concatenating + * @since 2.0.0 + */ +export const append: { + (element: B): (self: List) => Cons + (self: List, element: B): Cons +} = dual(2, (self: List, element: B): Cons => appendAll(self, of(element))) + +/** + * Concatenates two lists, combining their elements. + * If either list is non-empty, the result is also a non-empty list. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { List } from "effect" + * + * assert.deepStrictEqual( + * List.make(1, 2).pipe(List.appendAll(List.make("a", "b")), List.toArray), + * [1, 2, "a", "b"] + * ) + * ``` + * + * @category concatenating + * @since 2.0.0 + */ +export const appendAll: { + , T extends List>(that: T): (self: S) => List.OrNonEmpty | List.Infer> + (self: List, that: Cons): Cons + (self: Cons, that: List): Cons + (self: List, that: List): List +} = dual(2, (self: List, that: List): List => prependAll(that, self)) + +/** + * Prepends the specified element to the beginning of the list. + * + * @category concatenating + * @since 2.0.0 + */ +export const prepend: { + (element: B): (self: List) => Cons + (self: List, element: B): Cons +} = dual(2, (self: List, element: B): Cons => cons(element, self)) + +/** + * Prepends the specified prefix list to the beginning of the specified list. + * If either list is non-empty, the result is also a non-empty list. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { List } from "effect" + * + * assert.deepStrictEqual( + * List.make(1, 2).pipe(List.prependAll(List.make("a", "b")), List.toArray), + * ["a", "b", 1, 2] + * ) + * ``` + * + * @category concatenating + * @since 2.0.0 + */ +export const prependAll: { + , T extends List>(that: T): (self: S) => List.OrNonEmpty | List.Infer> + (self: List, that: Cons): Cons + (self: Cons, that: List): Cons + (self: List, that: List): List +} = dual(2, (self: List, prefix: List): List => { + if (isNil(self)) { + return prefix + } else if (isNil(prefix)) { + return self + } else { + const result = makeCons(prefix.head, self) + let curr = result + let that = prefix.tail + while (!isNil(that)) { + const temp = makeCons(that.head, self) + curr.tail = temp + curr = temp + that = that.tail + } + return result + } +}) + +/** + * Prepends the specified prefix list (in reverse order) to the beginning of the + * specified list. + * + * @category concatenating + * @since 2.0.0 + */ +export const prependAllReversed: { + (prefix: List): (self: List) => List + (self: List, prefix: List): List +} = dual(2, (self: List, prefix: List): List => { + let out: List = self + let pres = prefix + while (isCons(pres)) { + out = makeCons(pres.head, out) + pres = pres.tail + } + return out +}) + +/** + * Drops the first `n` elements from the specified list. + * + * @since 2.0.0 + * @category combinators + */ +export const drop: { + (n: number): (self: List) => List + (self: List, n: number): List +} = dual(2, (self: List, n: number): List => { + if (n <= 0) { + return self + } + if (n >= size(self)) { + return _Nil + } + let these = self + let i = 0 + while (!isNil(these) && i < n) { + these = these.tail + i += 1 + } + return these +}) + +/** + * Check if a predicate holds true for every `List` element. + * + * @since 2.0.0 + * @category elements + */ +export const every: { + (refinement: Refinement, B>): (self: List) => self is List + (predicate: Predicate): (self: List) => boolean + (self: List, refinement: Refinement): self is List + (self: List, predicate: Predicate): boolean +} = dual(2, (self: List, refinement: Refinement): self is List => { + for (const a of self) { + if (!refinement(a)) { + return false + } + } + return true +}) + +/** + * Check if a predicate holds true for some `List` element. + * + * @since 2.0.0 + * @category elements + */ +export const some: { + (predicate: Predicate>): (self: List) => self is Cons + (self: List, predicate: Predicate): self is Cons +} = dual(2, (self: List, predicate: Predicate): self is Cons => { + let these = self + while (!isNil(these)) { + if (predicate(these.head)) { + return true + } + these = these.tail + } + return false +}) + +/** + * Filters a list using the specified predicate. + * + * @since 2.0.0 + * @category combinators + */ +export const filter: { + (refinement: Refinement, B>): (self: List) => List + (predicate: Predicate>): (self: List) => List + (self: List, refinement: Refinement): List + (self: List, predicate: Predicate): List +} = dual(2, (self: List, predicate: Predicate): List => noneIn(self, predicate, false)) + +// everything seen so far is not included +const noneIn = ( + self: List, + predicate: Predicate, + isFlipped: boolean +): List => { + while (true) { + if (isNil(self)) { + return _Nil + } else { + if (predicate(self.head) !== isFlipped) { + return allIn(self, self.tail, predicate, isFlipped) + } else { + self = self.tail + } + } + } +} + +// everything from 'start' is included, if everything from this point is in we can return the origin +// start otherwise if we discover an element that is out we must create a new partial list. +const allIn = ( + start: List, + remaining: List, + predicate: Predicate, + isFlipped: boolean +): List => { + while (true) { + if (isNil(remaining)) { + return start + } else { + if (predicate(remaining.head) !== isFlipped) { + remaining = remaining.tail + } else { + return partialFill(start, remaining, predicate, isFlipped) + } + } + } +} + +// we have seen elements that should be included then one that should be excluded, start building +const partialFill = ( + origStart: List, + firstMiss: List, + predicate: Predicate, + isFlipped: boolean +): List => { + const newHead = makeCons(unsafeHead(origStart)!, _Nil) + let toProcess = unsafeTail(origStart)! as Cons + let currentLast = newHead + + // we know that all elements are :: until at least firstMiss.tail + while (!(toProcess === firstMiss)) { + const newElem = makeCons(unsafeHead(toProcess)!, _Nil) + currentLast.tail = newElem + currentLast = unsafeCoerce(newElem) + toProcess = unsafeCoerce(toProcess.tail) + } + + // at this point newHead points to a list which is a duplicate of all the 'in' elements up to the first miss. + // currentLast is the last element in that list. + + // now we are going to try and share as much of the tail as we can, only moving elements across when we have to. + let next = firstMiss.tail + let nextToCopy: Cons = unsafeCoerce(next) // the next element we would need to copy to our list if we cant share. + while (!isNil(next)) { + // generally recommended is next.isNonEmpty but this incurs an extra method call. + const head = unsafeHead(next)! + if (predicate(head) !== isFlipped) { + next = next.tail + } else { + // its not a match - do we have outstanding elements? + while (!(nextToCopy === next)) { + const newElem = makeCons(unsafeHead(nextToCopy)!, _Nil) + currentLast.tail = newElem + currentLast = newElem + nextToCopy = unsafeCoerce(nextToCopy.tail) + } + nextToCopy = unsafeCoerce(next.tail) + next = next.tail + } + } + + // we have remaining elements - they are unchanged attach them to the end + if (!isNil(nextToCopy)) { + currentLast.tail = nextToCopy + } + return newHead +} + +/** + * Filters and maps a list using the specified partial function. The resulting + * list may be smaller than the input list due to the possibility of the partial + * function not being defined for some elements. + * + * @since 2.0.0 + * @category combinators + */ +export const filterMap: { + (f: (a: A) => Option.Option): (self: List) => List + (self: List, f: (a: A) => Option.Option): List +} = dual(2, (self: List, f: (a: A) => Option.Option): List => { + const bs: Array = [] + for (const a of self) { + const oa = f(a) + if (Option.isSome(oa)) { + bs.push(oa.value) + } + } + return fromIterable(bs) +}) + +/** + * Removes all `None` values from the specified list. + * + * @since 2.0.0 + * @category combinators + */ +export const compact = (self: List>): List => filterMap(self, identity) + +/** + * Returns the first element that satisfies the specified + * predicate, or `None` if no such element exists. + * + * @category elements + * @since 2.0.0 + */ +export const findFirst: { + (refinement: Refinement, B>): (self: List) => Option.Option + (predicate: Predicate>): (self: List) => Option.Option + (self: List, refinement: Refinement): Option.Option + (self: List, predicate: Predicate): Option.Option +} = dual(2, (self: List, predicate: Predicate): Option.Option => { + let these = self + while (!isNil(these)) { + if (predicate(these.head)) { + return Option.some(these.head) + } + these = these.tail + } + return Option.none() +}) + +/** + * Applies a function to each element in a list and returns a new list containing the concatenated mapped elements. + * + * @since 2.0.0 + * @category sequencing + */ +export const flatMap: { + , T extends List>( + f: (a: List.Infer, i: number) => T + ): (self: S) => List.AndNonEmpty> + (self: Cons, f: (a: A, i: number) => Cons): Cons + (self: List, f: (a: A, i: number) => List): List +} = dual(2, (self: List, f: (a: A) => List): List => { + let rest = self + let head: MutableCons | undefined = undefined + let tail: MutableCons | undefined = undefined + while (!isNil(rest)) { + let bs = f(rest.head) + while (!isNil(bs)) { + const next = makeCons(bs.head, _Nil) + if (tail === undefined) { + head = next + } else { + tail.tail = next + } + tail = next + bs = bs.tail + } + rest = rest.tail + } + if (head === undefined) { + return _Nil + } + return head +}) + +/** + * Applies the specified function to each element of the `List`. + * + * @since 2.0.0 + * @category combinators + */ +export const forEach: { + (f: (a: A) => B): (self: List) => void + (self: List, f: (a: A) => B): void +} = dual(2, (self: List, f: (a: A) => B): void => { + let these = self + while (!isNil(these)) { + f(these.head) + these = these.tail + } +}) + +/** + * Returns the first element of the specified list, or `None` if the list is + * empty. + * + * @since 2.0.0 + * @category getters + */ +export const head = (self: List): Option.Option => isNil(self) ? Option.none() : Option.some(self.head) + +/** + * Returns the last element of the specified list, or `None` if the list is + * empty. + * + * @since 2.0.0 + * @category getters + */ +export const last = (self: List): Option.Option => isNil(self) ? Option.none() : Option.some(unsafeLast(self)!) + +/** + * @since 2.0.0 + */ +export declare namespace List { + /** + * @since 2.0.0 + */ + export type Infer> = S extends List ? A : never + + /** + * @since 2.0.0 + */ + export type With, A> = S extends Cons ? Cons : List + + /** + * @since 2.0.0 + */ + export type OrNonEmpty, T extends List, A> = S extends Cons ? Cons + : T extends Cons ? Cons + : List + + /** + * @since 2.0.0 + */ + export type AndNonEmpty, T extends List, A> = S extends Cons ? + T extends Cons ? Cons + : List : + List +} + +/** + * Applies the specified mapping function to each element of the list. + * + * @since 2.0.0 + * @category mapping + */ +export const map: { + , B>(f: (a: List.Infer, i: number) => B): (self: S) => List.With + , B>(self: S, f: (a: List.Infer, i: number) => B): List.With +} = dual(2, (self: List, f: (a: A, i: number) => B): List => { + if (isNil(self)) { + return self as unknown as List + } else { + let i = 0 + const head = makeCons(f(self.head, i++), _Nil) + let nextHead = head + let rest = self.tail + while (!isNil(rest)) { + const next = makeCons(f(rest.head, i++), _Nil) + nextHead.tail = next + nextHead = next + rest = rest.tail + } + return head + } +}) + +/** + * Partition a list into two lists, where the first list contains all elements + * that did not satisfy the specified predicate, and the second list contains + * all elements that did satisfy the specified predicate. + * + * @since 2.0.0 + * @category combinators + */ +export const partition: { + ( + refinement: Refinement, B> + ): (self: List) => [excluded: List>, satisfying: List] + (predicate: Predicate>): (self: List) => [excluded: List, satisfying: List] + (self: List, refinement: Refinement): [excluded: List>, satisfying: List] + (self: List, predicate: Predicate): [excluded: List, satisfying: List] +} = dual(2, (self: List, predicate: Predicate): [excluded: List, satisfying: List] => { + const left: Array = [] + const right: Array = [] + for (const a of self) { + if (predicate(a)) { + right.push(a) + } else { + left.push(a) + } + } + return [fromIterable(left), fromIterable(right)] +}) + +/** + * Partition a list into two lists, where the first list contains all elements + * for which the specified function returned a `Left`, and the second list + * contains all elements for which the specified function returned a `Right`. + * + * @since 2.0.0 + * @category combinators + */ +export const partitionMap: { + (f: (a: A) => Either.Either): (self: List) => [left: List, right: List] + (self: List, f: (a: A) => Either.Either): [left: List, right: List] +} = dual(2, (self: List, f: (a: A) => Either.Either): [left: List, right: List] => { + const left: Array = [] + const right: Array = [] + for (const a of self) { + const e = f(a) + if (Either.isLeft(e)) { + left.push(e.left) + } else { + right.push(e.right) + } + } + return [fromIterable(left), fromIterable(right)] +}) + +/** + * Folds over the elements of the list using the specified function, using the + * specified initial value. + * + * @since 2.0.0 + * @category folding + */ +export const reduce: { + (zero: Z, f: (b: Z, a: A) => Z): (self: List) => Z + (self: List, zero: Z, f: (b: Z, a: A) => Z): Z +} = dual(3, (self: List, zero: Z, f: (b: Z, a: A) => Z): Z => { + let acc = zero + let these = self + while (!isNil(these)) { + acc = f(acc, these.head) + these = these.tail + } + return acc +}) + +/** + * Folds over the elements of the list using the specified function, beginning + * with the last element of the list, using the specified initial value. + * + * @since 2.0.0 + * @category folding + */ +export const reduceRight: { + (zero: Z, f: (accumulator: Z, value: A) => Z): (self: List) => Z + (self: List, zero: Z, f: (accumulator: Z, value: A) => Z): Z +} = dual(3, (self: List, zero: Z, f: (accumulator: Z, value: A) => Z): Z => { + let acc = zero + let these = reverse(self) + while (!isNil(these)) { + acc = f(acc, these.head) + these = these.tail + } + return acc +}) + +/** + * Returns a new list with the elements of the specified list in reverse order. + * + * @since 2.0.0 + * @category elements + */ +export const reverse = (self: List): List => { + let result = empty() + let these = self + while (!isNil(these)) { + result = prepend(result, these.head) + these = these.tail + } + return result +} + +/** + * Splits the specified list into two lists at the specified index. + * + * @since 2.0.0 + * @category combinators + */ +export const splitAt: { + (n: number): (self: List) => [beforeIndex: List, fromIndex: List] + (self: List, n: number): [beforeIndex: List, fromIndex: List] +} = dual(2, (self: List, n: number): [List, List] => [take(self, n), drop(self, n)]) + +/** + * Returns the tail of the specified list, or `None` if the list is empty. + * + * @since 2.0.0 + * @category getters + */ +export const tail = (self: List): Option.Option> => isNil(self) ? Option.none() : Option.some(self.tail) + +/** + * Takes the specified number of elements from the beginning of the specified + * list. + * + * @since 2.0.0 + * @category combinators + */ +export const take: { + (n: number): (self: List) => List + (self: List, n: number): List +} = dual(2, (self: List, n: number): List => { + if (n <= 0) { + return _Nil + } + if (n >= size(self)) { + return self + } + let these = make(unsafeHead(self)) + let current = unsafeTail(self)! + for (let i = 1; i < n; i++) { + these = makeCons(unsafeHead(current), these) + current = unsafeTail(current!) + } + return reverse(these) +}) + +/** + * Converts the specified `List` to a `Chunk`. + * + * @since 2.0.0 + * @category conversions + */ +export const toChunk = (self: List): Chunk.Chunk => Chunk.fromIterable(self) + +const getExpectedListToBeNonEmptyErrorMessage = "Expected List to be non-empty" + +/** + * Unsafely returns the first element of the specified `List`. + * + * @since 2.0.0 + * @category unsafe + */ +export const unsafeHead = (self: List): A => { + if (isNil(self)) { + throw new Error(getExpectedListToBeNonEmptyErrorMessage) + } + return self.head +} + +/** + * Unsafely returns the last element of the specified `List`. + * + * @since 2.0.0 + * @category unsafe + */ +export const unsafeLast = (self: List): A => { + if (isNil(self)) { + throw new Error(getExpectedListToBeNonEmptyErrorMessage) + } + let these = self + let scout = self.tail + while (!isNil(scout)) { + these = scout + scout = scout.tail + } + return these.head +} + +/** + * Unsafely returns the tail of the specified `List`. + * + * @since 2.0.0 + * @category unsafe + */ +export const unsafeTail = (self: List): List => { + if (isNil(self)) { + throw new Error(getExpectedListToBeNonEmptyErrorMessage) + } + return self.tail +} diff --git a/repos/effect/packages/effect/src/LogLevel.ts b/repos/effect/packages/effect/src/LogLevel.ts new file mode 100644 index 0000000..3d974f0 --- /dev/null +++ b/repos/effect/packages/effect/src/LogLevel.ts @@ -0,0 +1,285 @@ +/** + * @since 2.0.0 + */ +import type * as Effect from "./Effect.js" +import { dual, pipe } from "./Function.js" +import * as core from "./internal/core.js" +import * as number from "./Number.js" +import * as order from "./Order.js" +import type { Pipeable } from "./Pipeable.js" + +/** + * A `LogLevel` represents the log level associated with an individual logging + * operation. Log levels are used both to describe the granularity (or + * importance) of individual log statements, as well as to enable tuning + * verbosity of log output. + * + * @since 2.0.0 + * @category model + * @property ordinal - The priority of the log message. Larger values indicate higher priority. + * @property label - A label associated with the log level. + * @property syslog -The syslog severity level of the log level. + */ +export type LogLevel = All | Fatal | Error | Warning | Info | Debug | Trace | None + +/** + * @since 2.0.0 + * @category model + */ +export type Literal = LogLevel["_tag"] + +/** + * @since 2.0.0 + * @category model + */ +export interface All extends Pipeable { + readonly _tag: "All" + readonly label: "ALL" + readonly syslog: 0 + readonly ordinal: number +} + +/** + * @since 2.0.0 + * @category model + */ +export interface Fatal extends Pipeable { + readonly _tag: "Fatal" + readonly label: "FATAL" + readonly syslog: 2 + readonly ordinal: number +} + +/** + * @since 2.0.0 + * @category model + */ +export interface Error extends Pipeable { + readonly _tag: "Error" + readonly label: "ERROR" + readonly syslog: 3 + readonly ordinal: number +} + +/** + * @since 2.0.0 + * @category model + */ +export interface Warning extends Pipeable { + readonly _tag: "Warning" + readonly label: "WARN" + readonly syslog: 4 + readonly ordinal: number +} + +/** + * @since 2.0.0 + * @category model + */ +export interface Info extends Pipeable { + readonly _tag: "Info" + readonly label: "INFO" + readonly syslog: 6 + readonly ordinal: number +} + +/** + * @since 2.0.0 + * @category model + */ +export interface Debug extends Pipeable { + readonly _tag: "Debug" + readonly label: "DEBUG" + readonly syslog: 7 + readonly ordinal: number +} + +/** + * @since 2.0.0 + * @category model + */ +export interface Trace extends Pipeable { + readonly _tag: "Trace" + readonly label: "TRACE" + readonly syslog: 7 + readonly ordinal: number +} + +/** + * @since 2.0.0 + * @category model + */ +export interface None extends Pipeable { + readonly _tag: "None" + readonly label: "OFF" + readonly syslog: 7 + readonly ordinal: number +} + +/** + * @since 2.0.0 + * @category constructors + */ +export const All: LogLevel = core.logLevelAll + +/** + * @since 2.0.0 + * @category constructors + */ +export const Fatal: LogLevel = core.logLevelFatal + +/** + * @since 2.0.0 + * @category constructors + */ +export const Error: LogLevel = core.logLevelError + +/** + * @since 2.0.0 + * @category constructors + */ +export const Warning: LogLevel = core.logLevelWarning + +/** + * @since 2.0.0 + * @category constructors + */ +export const Info: LogLevel = core.logLevelInfo + +/** + * @since 2.0.0 + * @category constructors + */ +export const Debug: LogLevel = core.logLevelDebug + +/** + * @since 2.0.0 + * @category constructors + */ +export const Trace: LogLevel = core.logLevelTrace + +/** + * @since 2.0.0 + * @category constructors + */ +export const None: LogLevel = core.logLevelNone + +/** + * @since 2.0.0 + * @category constructors + */ +export const allLevels = core.allLogLevels + +/** + * Temporarily sets a `LogLevel` for an `Effect` workflow. + * + * **Details** + * + * This function allows you to apply a specific `LogLevel` locally to an + * `Effect` workflow. Once the workflow completes, the `LogLevel` reverts to its + * previous state. + * + * **When to Use** + * + * This is particularly useful when you want to adjust the verbosity of logging + * for specific parts of your program without affecting the global log level. + * + * @example + * ```ts + * import { Effect, LogLevel } from "effect" + * + * const program = Effect.gen(function*() { + * yield* Effect.log("message1") + * yield* Effect.gen(function*() { + * yield* Effect.log("message2") + * yield* Effect.log("message3") + * }).pipe(LogLevel.locally(LogLevel.Warning)) + * }) + * + * Effect.runFork(program) + * // timestamp=... level=INFO fiber=#0 message=message1 + * // timestamp=... level=WARN fiber=#0 message=message2 + * // timestamp=... level=WARN fiber=#0 message=message3 + * ``` + * + * @since 2.0.0 + * @category utils + */ +export const locally: { + (self: LogLevel): (use: Effect.Effect) => Effect.Effect + (use: Effect.Effect, self: LogLevel): Effect.Effect +} = dual( + 2, + (use: Effect.Effect, self: LogLevel): Effect.Effect => + core.fiberRefLocally(use, core.currentLogLevel, self) +) + +/** + * @since 2.0.0 + * @category instances + */ +export const Order: order.Order = pipe( + number.Order, + order.mapInput((level: LogLevel) => level.ordinal) +) + +/** + * @since 2.0.0 + * @category ordering + */ +export const lessThan: { + (that: LogLevel): (self: LogLevel) => boolean + (self: LogLevel, that: LogLevel): boolean +} = order.lessThan(Order) + +/** + * @since 2.0.0 + * @category ordering + */ +export const lessThanEqual: { + (that: LogLevel): (self: LogLevel) => boolean + (self: LogLevel, that: LogLevel): boolean +} = order.lessThanOrEqualTo(Order) + +/** + * @since 2.0.0 + * @category ordering + */ +export const greaterThan: { + (that: LogLevel): (self: LogLevel) => boolean + (self: LogLevel, that: LogLevel): boolean +} = order.greaterThan(Order) + +/** + * @since 2.0.0 + * @category ordering + */ +export const greaterThanEqual: { + (that: LogLevel): (self: LogLevel) => boolean + (self: LogLevel, that: LogLevel): boolean +} = order.greaterThanOrEqualTo(Order) + +/** + * @since 2.0.0 + * @category conversions + */ +export const fromLiteral = (literal: Literal): LogLevel => { + switch (literal) { + case "All": + return All + case "Debug": + return Debug + case "Error": + return Error + case "Fatal": + return Fatal + case "Info": + return Info + case "Trace": + return Trace + case "None": + return None + case "Warning": + return Warning + } +} diff --git a/repos/effect/packages/effect/src/LogSpan.ts b/repos/effect/packages/effect/src/LogSpan.ts new file mode 100644 index 0000000..6c898be --- /dev/null +++ b/repos/effect/packages/effect/src/LogSpan.ts @@ -0,0 +1,25 @@ +/** + * @since 2.0.0 + */ +import * as internal from "./internal/logSpan.js" + +/** + * @since 2.0.0 + * @category models + */ +export interface LogSpan { + readonly label: string + readonly startTime: number +} + +/** + * @since 2.0.0 + * @category constructors + */ +export const make: (label: string, startTime: number) => LogSpan = internal.make + +/** + * @since 2.0.0 + * @category destructors + */ +export const render: (now: number) => (self: LogSpan) => string = internal.render diff --git a/repos/effect/packages/effect/src/Logger.ts b/repos/effect/packages/effect/src/Logger.ts new file mode 100644 index 0000000..3080e29 --- /dev/null +++ b/repos/effect/packages/effect/src/Logger.ts @@ -0,0 +1,702 @@ +/** + * @since 2.0.0 + */ +import type * as Cause from "./Cause.js" +import type { DurationInput } from "./Duration.js" +import type { Effect } from "./Effect.js" +import type * as FiberId from "./FiberId.js" +import type * as FiberRefs from "./FiberRefs.js" +import type { LazyArg } from "./Function.js" +import type * as HashMap from "./HashMap.js" +import * as fiberRuntime from "./internal/fiberRuntime.js" +import * as circular from "./internal/layer/circular.js" +import * as internalCircular from "./internal/logger-circular.js" +import * as internal from "./internal/logger.js" +import type * as Layer from "./Layer.js" +import type * as List from "./List.js" +import type * as LogLevel from "./LogLevel.js" +import type * as LogSpan from "./LogSpan.js" +import type * as Option from "./Option.js" +import type { Pipeable } from "./Pipeable.js" +import type { Scope } from "./Scope.js" +import type * as Types from "./Types.js" + +/** + * @since 2.0.0 + * @category symbols + */ +export const LoggerTypeId: unique symbol = internal.LoggerTypeId + +/** + * @since 2.0.0 + * @category symbols + */ +export type LoggerTypeId = typeof LoggerTypeId + +/** + * @since 2.0.0 + * @category models + */ +export interface Logger extends Logger.Variance, Pipeable { + log(options: Logger.Options): Output +} + +/** + * @since 2.0.0 + */ +export declare namespace Logger { + /** + * @since 2.0.0 + * @category models + */ + export interface Variance { + readonly [LoggerTypeId]: { + readonly _Message: Types.Contravariant + readonly _Output: Types.Covariant + } + } + + /** + * @since 2.0.0 + * @category models + */ + export interface Options { + readonly fiberId: FiberId.FiberId + readonly logLevel: LogLevel.LogLevel + readonly message: Message + readonly cause: Cause.Cause + readonly context: FiberRefs.FiberRefs + readonly spans: List.List + readonly annotations: HashMap.HashMap + readonly date: Date + } +} + +/** + * Creates a custom logger that formats log messages according to the provided + * function. + * + * @example + * ```ts + * import { Effect, Logger, LogLevel } from "effect" + * + * const logger = Logger.make(({ logLevel, message }) => { + * globalThis.console.log(`[${logLevel.label}] ${message}`) + * }) + * + * const task1 = Effect.logDebug("task1 done") + * const task2 = Effect.logDebug("task2 done") + * + * const program = Effect.gen(function*() { + * yield* Effect.log("start") + * yield* task1 + * yield* task2 + * yield* Effect.log("done") + * }).pipe( + * Logger.withMinimumLogLevel(LogLevel.Debug), + * Effect.provide(Logger.replace(Logger.defaultLogger, logger)) + * ) + * + * Effect.runFork(program) + * // [INFO] start + * // [DEBUG] task1 done + * // [DEBUG] task2 done + * // [INFO] done + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const make: (log: (options: Logger.Options) => Output) => Logger = + internal.makeLogger + +/** + * @since 2.0.0 + * @category context + */ +export const add: (logger: Logger) => Layer.Layer = circular.addLogger + +/** + * @since 2.0.0 + * @category context + */ +export const addEffect: (effect: Effect, E, R>) => Layer.Layer = + circular.addLoggerEffect + +/** + * @since 2.0.0 + * @category context + */ +export const addScoped: ( + effect: Effect, E, R> +) => Layer.Layer> = circular.addLoggerScoped + +/** + * @since 2.0.0 + * @category mapping + */ +export const mapInput: { + ( + f: (message: Message2) => Message + ): (self: Logger) => Logger + ( + self: Logger, + f: (message: Message2) => Message + ): Logger +} = internal.mapInput + +/** + * @since 2.0.0 + * @category mapping + */ +export const mapInputOptions: { + ( + f: (options: Logger.Options) => Logger.Options + ): (self: Logger) => Logger + ( + self: Logger, + f: (options: Logger.Options) => Logger.Options + ): Logger +} = internal.mapInputOptions + +/** + * Returns a version of this logger that only logs messages when the log level + * satisfies the specified predicate. + * + * @since 2.0.0 + * @category filtering + */ +export const filterLogLevel: { + ( + f: (logLevel: LogLevel.LogLevel) => boolean + ): (self: Logger) => Logger> + ( + self: Logger, + f: (logLevel: LogLevel.LogLevel) => boolean + ): Logger> +} = internal.filterLogLevel + +/** + * @since 2.0.0 + * @category mapping + */ +export const map: { + ( + f: (output: Output) => Output2 + ): (self: Logger) => Logger + ( + self: Logger, + f: (output: Output) => Output2 + ): Logger +} = internal.map + +/** + * Creates a batched logger that groups log messages together and processes them + * in intervals. + * + * @example + * ```ts + * import { Console, Effect, Logger } from "effect" + * + * const LoggerLive = Logger.replaceScoped( + * Logger.defaultLogger, + * Logger.logfmtLogger.pipe( + * Logger.batched("500 millis", (messages) => Console.log("BATCH", `[\n${messages.join("\n")}\n]`)) + * ) + * ) + * + * const program = Effect.gen(function*() { + * yield* Effect.log("one") + * yield* Effect.log("two") + * yield* Effect.log("three") + * }).pipe(Effect.provide(LoggerLive)) + * + * Effect.runFork(program) + * // BATCH [ + * // timestamp=... level=INFO fiber=#0 message=one + * // timestamp=... level=INFO fiber=#0 message=two + * // timestamp=... level=INFO fiber=#0 message=three + * // ] + * ``` + * + * @since 2.0.0 + * @category mapping + */ +export const batched: { + ( + window: DurationInput, + f: (messages: Array>) => Effect + ): (self: Logger) => Effect, never, R | Scope> + ( + self: Logger, + window: DurationInput, + f: (messages: Array>) => Effect + ): Effect, never, Scope | R> +} = fiberRuntime.batchedLogger + +/** + * @since 2.0.0 + * @category console + */ +export const withConsoleLog: (self: Logger) => Logger = fiberRuntime.loggerWithConsoleLog + +/** + * Takes a `Logger` and returns a logger that calls the respective `Console` method + * based on the log level. + * + * @example + * ```ts + * import { Logger, Effect } from "effect" + * + * const loggerLayer = Logger.replace( + * Logger.defaultLogger, + * Logger.withLeveledConsole(Logger.stringLogger), + * ) + * + * Effect.gen(function* () { + * yield* Effect.logError("an error") + * yield* Effect.logInfo("an info") + * }).pipe(Effect.provide(loggerLayer)) + * ``` + * + * @since 3.8.0 + * @category console + */ +export const withLeveledConsole: (self: Logger) => Logger = fiberRuntime.loggerWithLeveledLog + +/** + * @since 2.0.0 + * @category console + */ +export const withConsoleError: (self: Logger) => Logger = fiberRuntime.loggerWithConsoleError + +/** + * A logger that does nothing in response to logging events. + * + * @since 2.0.0 + * @category constructors + */ +export const none: Logger = internal.none + +/** + * @since 2.0.0 + * @category context + */ +export const remove: (logger: Logger) => Layer.Layer = circular.removeLogger + +/** + * @since 2.0.0 + * @category context + */ +export const replace: { + (that: Logger): (self: Logger) => Layer.Layer + (self: Logger, that: Logger): Layer.Layer +} = circular.replaceLogger + +/** + * @since 2.0.0 + * @category context + */ +export const replaceEffect: { + (that: Effect, E, R>): (self: Logger) => Layer.Layer + (self: Logger, that: Effect, E, R>): Layer.Layer +} = circular.replaceLoggerEffect + +/** + * @since 2.0.0 + * @category context + */ +export const replaceScoped: { + ( + that: Effect, E, R> + ): (self: Logger) => Layer.Layer> + ( + self: Logger, + that: Effect, E, R> + ): Layer.Layer> +} = circular.replaceLoggerScoped + +/** + * @since 2.0.0 + * @category constructors + */ +export const simple: (log: (a: A) => B) => Logger = internal.simple + +/** + * @since 2.0.0 + * @category constructors + */ +export const succeed: (value: A) => Logger = internal.succeed + +/** + * @since 2.0.0 + * @category constructors + */ +export const sync: (evaluate: LazyArg) => Logger = internal.sync + +/** + * @since 2.0.0 + * @category constructors + */ +export const test: { + (input: Message): (self: Logger) => Output + (self: Logger, input: Message): Output +} = internalCircular.test + +/** + * Sets the minimum log level for subsequent logging operations, allowing + * control over which log messages are displayed based on their severity. + * + * @example + * ```ts + * import { Effect, Logger, LogLevel } from "effect" + * + * const program = Effect.logDebug("message1").pipe(Logger.withMinimumLogLevel(LogLevel.Debug)) + * + * Effect.runFork(program) + * // timestamp=... level=DEBUG fiber=#0 message=message1 + * ``` + * + * @since 2.0.0 + * @category context + */ +export const withMinimumLogLevel: { + (level: LogLevel.LogLevel): (self: Effect) => Effect + (self: Effect, level: LogLevel.LogLevel): Effect +} = circular.withMinimumLogLevel + +/** + * @since 2.0.0 + * @category tracing + */ +export const withSpanAnnotations: (self: Logger) => Logger = + fiberRuntime.loggerWithSpanAnnotations + +/** + * Combines this logger with the specified logger to produce a new logger that + * logs to both this logger and that logger. + * + * @since 2.0.0 + * @category zipping + */ +export const zip: { + ( + that: Logger + ): (self: Logger) => Logger + ( + self: Logger, + that: Logger + ): Logger +} = internal.zip + +/** + * @since 2.0.0 + * @category zipping + */ +export const zipLeft: { + ( + that: Logger + ): (self: Logger) => Logger + ( + self: Logger, + that: Logger + ): Logger +} = internal.zipLeft + +/** + * @since 2.0.0 + * @category zipping + */ +export const zipRight: { + ( + that: Logger + ): (self: Logger) => Logger + ( + self: Logger, + that: Logger + ): Logger +} = internal.zipRight + +/** + * @since 2.0.0 + * @category constructors + */ +export const defaultLogger: Logger = fiberRuntime.defaultLogger + +/** + * The `jsonLogger` logger formats log entries as JSON objects, making them easy to + * integrate with logging systems that consume JSON data. + * + * @example + * ```ts + * import { Effect, Logger } from "effect" + * + * const program = Effect.log("message1", "message2").pipe( + * Effect.annotateLogs({ key1: "value1", key2: "value2" }), + * Effect.withLogSpan("myspan") + * ) + * + * Effect.runFork(program.pipe(Effect.provide(Logger.json))) + * // {"message":["message1","message2"],"logLevel":"INFO","timestamp":"...","annotations":{"key2":"value2","key1":"value1"},"spans":{"myspan":0},"fiberId":"#0"} + * ``` + * + * @since 2.0.0 + * @category constructors + */ +export const jsonLogger: Logger = internal.jsonLogger + +/** + * This logger outputs logs in a human-readable format that is easy to read + * during development or in a production console. + * + * @example + * ```ts + * import { Effect, Logger } from "effect" + * + * const program = Effect.log("message1", "message2").pipe( + * Effect.annotateLogs({ key1: "value1", key2: "value2" }), + * Effect.withLogSpan("myspan") + * ) + * + * Effect.runFork(program.pipe(Effect.provide(Logger.logFmt))) + * // timestamp=... level=INFO fiber=#0 message=message1 message=message2 myspan=0ms key2=value2 key1=value1 + * ``` + * + * @since 2.0.0 + * @category constructors + */ +export const logfmtLogger: Logger = internal.logfmtLogger + +/** + * @since 2.0.0 + * @category constructors + */ +export const stringLogger: Logger = internal.stringLogger + +/** + * The pretty logger utilizes the capabilities of the console API to generate + * visually engaging and color-enhanced log outputs. This feature is + * particularly useful for improving the readability of log messages during + * development and debugging processes. + * + * @example + * ```ts + * import { Effect, Logger } from "effect" + * + * const program = Effect.log("message1", "message2").pipe( + * Effect.annotateLogs({ key1: "value1", key2: "value2" }), + * Effect.withLogSpan("myspan") + * ) + * + * Effect.runFork(program.pipe(Effect.provide(Logger.pretty))) + * // green --v v-- bold and cyan + * // [07:51:54.434] INFO (#0) myspan=1ms: message1 + * // message2 + * // v-- bold + * // key2: value2 + * // key1: value1 + * ``` + * + * @since 3.5.0 + * @category constructors + */ +export const prettyLogger: ( + options?: { + readonly colors?: "auto" | boolean | undefined + readonly stderr?: boolean | undefined + readonly formatDate?: ((date: Date) => string) | undefined + readonly mode?: "browser" | "tty" | "auto" | undefined + } +) => Logger = internal.prettyLogger + +/** + * A default version of the pretty logger. + * + * @since 3.8.0 + * @category constructors + */ +export const prettyLoggerDefault: Logger = internal.prettyLoggerDefault + +/** + * The structured logger provides detailed log outputs, structured in a way that + * retains comprehensive traceability of the events, suitable for deeper + * analysis and troubleshooting. + * + * @example + * ```ts + * import { Effect, Logger } from "effect" + * + * const program = Effect.log("message1", "message2").pipe( + * Effect.annotateLogs({ key1: "value1", key2: "value2" }), + * Effect.withLogSpan("myspan") + * ) + * + * Effect.runFork(program.pipe(Effect.provide(Logger.structured))) + * // { + * // message: [ 'message1', 'message2' ], + * // logLevel: 'INFO', + * // timestamp: '2024-07-09T14:05:41.623Z', + * // cause: undefined, + * // annotations: { key2: 'value2', key1: 'value1' }, + * // spans: { myspan: 0 }, + * // fiberId: '#0' + * // } + * ``` + * + * @since 2.0.0 + * @category constructors + */ +export const structuredLogger: Logger< + unknown, + { + readonly logLevel: string + readonly fiberId: string + readonly timestamp: string + readonly message: unknown + readonly cause: string | undefined + readonly annotations: Record + readonly spans: Record + } +> = internal.structuredLogger + +/** + * @since 2.0.0 + * @category constructors + */ +export const tracerLogger: Logger = fiberRuntime.tracerLogger + +/** + * The `json` logger formats log entries as JSON objects, making them easy to + * integrate with logging systems that consume JSON data. + * + * @example + * ```ts + * import { Effect, Logger } from "effect" + * + * const program = Effect.log("message1", "message2").pipe( + * Effect.annotateLogs({ key1: "value1", key2: "value2" }), + * Effect.withLogSpan("myspan") + * ) + * + * Effect.runFork(program.pipe(Effect.provide(Logger.json))) + * // {"message":["message1","message2"],"logLevel":"INFO","timestamp":"...","annotations":{"key2":"value2","key1":"value1"},"spans":{"myspan":0},"fiberId":"#0"} + * ``` + * + * @since 2.0.0 + * @category constructors + */ +export const json: Layer.Layer = replace(fiberRuntime.defaultLogger, fiberRuntime.jsonLogger) + +/** + * This logger outputs logs in a human-readable format that is easy to read + * during development or in a production console. + * + * @example + * ```ts + * import { Effect, Logger } from "effect" + * + * const program = Effect.log("message1", "message2").pipe( + * Effect.annotateLogs({ key1: "value1", key2: "value2" }), + * Effect.withLogSpan("myspan") + * ) + * + * Effect.runFork(program.pipe(Effect.provide(Logger.logFmt))) + * // timestamp=... level=INFO fiber=#0 message=message1 message=message2 myspan=0ms key2=value2 key1=value1 + * ``` + * + * @since 2.0.0 + * @category constructors + */ +export const logFmt: Layer.Layer = replace(fiberRuntime.defaultLogger, fiberRuntime.logFmtLogger) + +/** + * The pretty logger utilizes the capabilities of the console API to generate + * visually engaging and color-enhanced log outputs. This feature is + * particularly useful for improving the readability of log messages during + * development and debugging processes. + * + * @example + * ```ts + * import { Effect, Logger } from "effect" + * + * const program = Effect.log("message1", "message2").pipe( + * Effect.annotateLogs({ key1: "value1", key2: "value2" }), + * Effect.withLogSpan("myspan") + * ) + * + * Effect.runFork(program.pipe(Effect.provide(Logger.pretty))) + * // green --v v-- bold and cyan + * // [07:51:54.434] INFO (#0) myspan=1ms: message1 + * // message2 + * // v-- bold + * // key2: value2 + * // key1: value1 + * ``` + * + * @since 3.5.0 + * @category constructors + */ +export const pretty: Layer.Layer = replace(fiberRuntime.defaultLogger, fiberRuntime.prettyLogger) + +/** + * The structured logger provides detailed log outputs, structured in a way that + * retains comprehensive traceability of the events, suitable for deeper + * analysis and troubleshooting. + * + * @example + * ```ts + * import { Effect, Logger } from "effect" + * + * const program = Effect.log("message1", "message2").pipe( + * Effect.annotateLogs({ key1: "value1", key2: "value2" }), + * Effect.withLogSpan("myspan") + * ) + * + * Effect.runFork(program.pipe(Effect.provide(Logger.structured))) + * // { + * // message: [ 'message1', 'message2' ], + * // logLevel: 'INFO', + * // timestamp: '2024-07-09T14:05:41.623Z', + * // cause: undefined, + * // annotations: { key2: 'value2', key1: 'value1' }, + * // spans: { myspan: 0 }, + * // fiberId: '#0' + * // } + * ``` + * + * @since 2.0.0 + * @category constructors + */ +export const structured: Layer.Layer = replace(fiberRuntime.defaultLogger, fiberRuntime.structuredLogger) + +/** + * Sets the minimum log level for logging operations, allowing control over + * which log messages are displayed based on their severity. + * + * @example + * ```ts + * import { Effect, Logger, LogLevel } from "effect" + * + * const program = Effect.gen(function*() { + * yield* Effect.log("Executing task...") + * yield* Effect.sleep("100 millis") + * console.log("task done") + * }) + * + * // Logging disabled using a layer + * Effect.runFork(program.pipe(Effect.provide(Logger.minimumLogLevel(LogLevel.None)))) + * // task done + * ``` + * + * @since 2.0.0 + * @category context + */ +export const minimumLogLevel: (level: LogLevel.LogLevel) => Layer.Layer = circular.minimumLogLevel + +/** + * Returns `true` if the specified value is a `Logger`, otherwise returns `false`. + * + * @since 1.0.0 + * @category guards + */ +export const isLogger: (u: unknown) => u is Logger = internal.isLogger diff --git a/repos/effect/packages/effect/src/Mailbox.ts b/repos/effect/packages/effect/src/Mailbox.ts new file mode 100644 index 0000000..972fbf5 --- /dev/null +++ b/repos/effect/packages/effect/src/Mailbox.ts @@ -0,0 +1,268 @@ +/** + * @since 3.8.0 + * @experimental + */ +import type { Cause, NoSuchElementException } from "./Cause.js" +import type { Channel } from "./Channel.js" +import type { Chunk } from "./Chunk.js" +import type { Effect } from "./Effect.js" +import type { Exit } from "./Exit.js" +import type { Inspectable } from "./Inspectable.js" +import * as internal from "./internal/mailbox.js" +import type { Option } from "./Option.js" +import { hasProperty } from "./Predicate.js" +import type { Scope } from "./Scope.js" +import type { Stream } from "./Stream.js" + +/** + * @since 3.8.0 + * @experimental + * @category type ids + */ +export const TypeId: unique symbol = internal.TypeId + +/** + * @since 3.8.0 + * @experimental + * @category type ids + */ +export type TypeId = typeof TypeId + +/** + * @since 3.8.0 + * @experimental + * @category type ids + */ +export const ReadonlyTypeId: unique symbol = internal.ReadonlyTypeId + +/** + * @since 3.8.0 + * @experimental + * @category type ids + */ +export type ReadonlyTypeId = typeof ReadonlyTypeId + +/** + * @since 3.8.0 + * @experimental + * @category guards + */ +export const isMailbox = (u: unknown): u is Mailbox => hasProperty(u, TypeId) + +/** + * @since 3.8.0 + * @experimental + * @category guards + */ +export const isReadonlyMailbox = (u: unknown): u is ReadonlyMailbox => + hasProperty(u, ReadonlyTypeId) + +/** + * A `Mailbox` is a queue that can be signaled to be done or failed. + * + * @since 3.8.0 + * @experimental + * @category models + */ +export interface Mailbox extends ReadonlyMailbox { + readonly [TypeId]: TypeId + /** + * Add a message to the mailbox. Returns `false` if the mailbox is done. + */ + readonly offer: (message: A) => Effect + /** + * Add a message to the mailbox. Returns `false` if the mailbox is done. + */ + readonly unsafeOffer: (message: A) => boolean + /** + * Add multiple messages to the mailbox. Returns the remaining messages that + * were not added. + */ + readonly offerAll: (messages: Iterable) => Effect> + /** + * Add multiple messages to the mailbox. Returns the remaining messages that + * were not added. + */ + readonly unsafeOfferAll: (messages: Iterable) => Chunk + /** + * Fail the mailbox with an error. If the mailbox is already done, `false` is + * returned. + */ + readonly fail: (error: E) => Effect + /** + * Fail the mailbox with a cause. If the mailbox is already done, `false` is + * returned. + */ + readonly failCause: (cause: Cause) => Effect + /** + * Signal that the mailbox is complete. If the mailbox is already done, `false` is + * returned. + */ + readonly end: Effect + /** + * Signal that the mailbox is done. If the mailbox is already done, `false` is + * returned. + */ + readonly done: (exit: Exit) => Effect + /** + * Signal that the mailbox is done. If the mailbox is already done, `false` is + * returned. + */ + readonly unsafeDone: (exit: Exit) => boolean + /** + * Shutdown the mailbox, canceling any pending operations. + * If the mailbox is already done, `false` is returned. + */ + readonly shutdown: Effect +} + +/** + * A `ReadonlyMailbox` represents a mailbox that can only be read from. + * + * @since 3.8.0 + * @experimental + * @category models + */ +export interface ReadonlyMailbox + extends Effect, done: boolean], E>, Inspectable +{ + readonly [ReadonlyTypeId]: ReadonlyTypeId + /** + * Take all messages from the mailbox, returning an empty Chunk if the mailbox + * is empty or done. + */ + readonly clear: Effect, E> + /** + * Take all messages from the mailbox, or wait for messages to be available. + * + * If the mailbox is done, the `done` flag will be `true`. If the mailbox + * fails, the Effect will fail with the error. + */ + readonly takeAll: Effect, done: boolean], E> + /** + * Take a specified number of messages from the mailbox. It will only take + * up to the capacity of the mailbox. + * + * If the mailbox is done, the `done` flag will be `true`. If the mailbox + * fails, the Effect will fail with the error. + */ + readonly takeN: (n: number) => Effect, done: boolean], E> + /** + * Take a single message from the mailbox, or wait for a message to be + * available. + * + * If the mailbox is done, it will fail with `NoSuchElementException`. If the + * mailbox fails, the Effect will fail with the error. + */ + readonly take: Effect + /** Wait for the mailbox to be done. */ + readonly await: Effect + /** + * Check the size of the mailbox. + * + * If the mailbox is complete, it will return `None`. + */ + readonly size: Effect> + /** + * Check the size of the mailbox. + * + * If the mailbox is complete, it will return `None`. + */ + readonly unsafeSize: () => Option +} + +/** + * A `Mailbox` is a queue that can be signaled to be done or failed. + * + * @since 3.8.0 + * @experimental + * @category constructors + * @example + * ```ts + * import * as assert from "node:assert" + * import { Effect, Mailbox } from "effect" + * + * Effect.gen(function*() { + * const mailbox = yield* Mailbox.make() + * + * // add messages to the mailbox + * yield* mailbox.offer(1) + * yield* mailbox.offer(2) + * yield* mailbox.offerAll([3, 4, 5]) + * + * // take messages from the mailbox + * const [messages, done] = yield* mailbox.takeAll + * assert.deepStrictEqual(messages, [1, 2, 3, 4, 5]) + * assert.strictEqual(done, false) + * + * // signal that the mailbox is done + * yield* mailbox.end + * const [messages2, done2] = yield* mailbox.takeAll + * assert.deepStrictEqual(messages2, []) + * assert.strictEqual(done2, true) + * + * // signal that the mailbox has failed + * yield* mailbox.fail("boom") + * }) + * ``` + */ +export const make: ( + capacity?: number | { + readonly capacity?: number + readonly strategy?: "suspend" | "dropping" | "sliding" + } | undefined +) => Effect> = internal.make + +/** + * Run an `Effect` into a `Mailbox`, where success ends the mailbox and failure + * fails the mailbox. + * + * @since 3.8.0 + * @experimental + * @category combinators + */ +export const into: { + (self: Mailbox): (effect: Effect) => Effect + (effect: Effect, self: Mailbox): Effect +} = internal.into + +/** + * Create a `Channel` from a `Mailbox`. + * + * @since 3.8.0 + * @experimental + * @category conversions + */ +export const toChannel: (self: ReadonlyMailbox) => Channel, unknown, E> = internal.toChannel + +/** + * Create a `Stream` from a `Mailbox`. + * + * @since 3.8.0 + * @experimental + * @category conversions + */ +export const toStream: (self: ReadonlyMailbox) => Stream = internal.toStream + +/** + * Create a `ReadonlyMailbox` from a `Stream`. + * + * @since 3.11.0 + * @experimental + * @category conversions + */ +export const fromStream: { + ( + options?: { + readonly capacity?: number | undefined + readonly strategy?: "suspend" | "dropping" | "sliding" | undefined + } + ): (self: Stream) => Effect, never, R | Scope> + ( + self: Stream, + options?: { + readonly capacity?: number | undefined + readonly strategy?: "suspend" | "dropping" | "sliding" | undefined + } + ): Effect, never, R | Scope> +} = internal.fromStream diff --git a/repos/effect/packages/effect/src/ManagedRuntime.ts b/repos/effect/packages/effect/src/ManagedRuntime.ts new file mode 100644 index 0000000..97e4a46 --- /dev/null +++ b/repos/effect/packages/effect/src/ManagedRuntime.ts @@ -0,0 +1,180 @@ +/** + * @since 2.0.0 + */ +import type * as Effect from "./Effect.js" +import type * as Exit from "./Exit.js" +import type * as Fiber from "./Fiber.js" +import * as internal from "./internal/managedRuntime.js" +import * as circular from "./internal/managedRuntime/circular.js" +import type * as Layer from "./Layer.js" +import type * as Runtime from "./Runtime.js" +import type * as Unify from "./Unify.js" + +/** + * @since 3.9.0 + * @category symbol + */ +export const TypeId: unique symbol = circular.TypeId as TypeId + +/** + * @since 3.9.0 + * @category symbol + */ +export type TypeId = typeof TypeId + +/** + * Checks if the provided argument is a `ManagedRuntime`. + * + * @since 3.9.0 + * @category guards + */ +export const isManagedRuntime: (input: unknown) => input is ManagedRuntime = internal.isManagedRuntime + +/** + * @since 3.4.0 + */ +export declare namespace ManagedRuntime { + /** + * @category type-level + * @since 3.4.0 + */ + export type Context> = [T] extends [ManagedRuntime] ? R + : never + /** + * @category type-level + * @since 3.4.0 + */ + export type Error> = [T] extends [ManagedRuntime] ? E : never +} + +/** + * @since 2.0.0 + * @category models + */ +export interface ManagedRuntime extends Effect.Effect, ER> { + readonly [TypeId]: TypeId + readonly memoMap: Layer.MemoMap + readonly runtimeEffect: Effect.Effect, ER> + readonly runtime: () => Promise> + + /** + * Executes the effect using the provided Scheduler or using the global + * Scheduler if not provided + */ + readonly runFork: ( + self: Effect.Effect, + options?: Runtime.RunForkOptions + ) => Fiber.RuntimeFiber + + /** + * Executes the effect synchronously returning the exit. + * + * This method is effectful and should only be invoked at the edges of your + * program. + */ + readonly runSyncExit: (effect: Effect.Effect) => Exit.Exit + + /** + * Executes the effect synchronously throwing in case of errors or async boundaries. + * + * This method is effectful and should only be invoked at the edges of your + * program. + */ + readonly runSync: (effect: Effect.Effect) => A + + /** + * Executes the effect asynchronously, eventually passing the exit value to + * the specified callback. + * + * This method is effectful and should only be invoked at the edges of your + * program. + */ + readonly runCallback: ( + effect: Effect.Effect, + options?: Runtime.RunCallbackOptions | undefined + ) => Runtime.Cancel + + /** + * Runs the `Effect`, returning a JavaScript `Promise` that will be resolved + * with the value of the effect once the effect has been executed, or will be + * rejected with the first error or exception throw by the effect. + * + * This method is effectful and should only be used at the edges of your + * program. + */ + readonly runPromise: (effect: Effect.Effect, options?: { + readonly signal?: AbortSignal | undefined + }) => Promise + + /** + * Runs the `Effect`, returning a JavaScript `Promise` that will be resolved + * with the `Exit` state of the effect once the effect has been executed. + * + * This method is effectful and should only be used at the edges of your + * program. + */ + readonly runPromiseExit: (effect: Effect.Effect, options?: { + readonly signal?: AbortSignal | undefined + }) => Promise> + + /** + * Dispose of the resources associated with the runtime. + */ + readonly dispose: () => Promise + + /** + * Dispose of the resources associated with the runtime. + */ + readonly disposeEffect: Effect.Effect + + readonly [Unify.typeSymbol]?: unknown + readonly [Unify.unifySymbol]?: ManagedRuntimeUnify + readonly [Unify.ignoreSymbol]?: ManagedRuntimeUnifyIgnore +} + +/** + * @category models + * @since 3.9.0 + */ +export interface ManagedRuntimeUnify extends Effect.EffectUnify { + ManagedRuntime?: () => Extract> +} + +/** + * @category models + * @since 3.9.0 + */ +export interface ManagedRuntimeUnifyIgnore extends Effect.EffectUnifyIgnore { + Effect?: true +} + +/** + * Convert a Layer into an ManagedRuntime, that can be used to run Effect's using + * your services. + * + * @since 2.0.0 + * @category runtime class + * @example + * ```ts + * import { Console, Effect, Layer, ManagedRuntime } from "effect" + * + * class Notifications extends Effect.Tag("Notifications")< + * Notifications, + * { readonly notify: (message: string) => Effect.Effect } + * >() { + * static Live = Layer.succeed(this, { notify: (message) => Console.log(message) }) + * } + * + * async function main() { + * const runtime = ManagedRuntime.make(Notifications.Live) + * await runtime.runPromise(Notifications.notify("Hello, world!")) + * await runtime.dispose() + * } + * + * main() + * ``` + */ +export const make: ( + layer: Layer.Layer, + memoMap?: Layer.MemoMap | undefined +) => ManagedRuntime = internal.make diff --git a/repos/effect/packages/effect/src/Match.ts b/repos/effect/packages/effect/src/Match.ts new file mode 100644 index 0000000..293dcf6 --- /dev/null +++ b/repos/effect/packages/effect/src/Match.ts @@ -0,0 +1,1477 @@ +/** + * The `effect/match` module provides a type-safe pattern matching system for + * TypeScript. Inspired by functional programming, it simplifies conditional + * logic by replacing verbose if/else or switch statements with a structured and + * expressive API. + * + * This module supports matching against types, values, and discriminated unions + * while enforcing exhaustiveness checking to ensure all cases are handled. + * + * Although pattern matching is not yet a native JavaScript feature, + * `effect/match` offers a reliable implementation that is available today. + * + * **How Pattern Matching Works** + * + * Pattern matching follows a structured process: + * + * - **Creating a matcher**: Define a `Matcher` that operates on either a + * specific `Match.type` or `Match.value`. + * + * - **Defining patterns**: Use combinators such as `Match.when`, `Match.not`, + * and `Match.tag` to specify matching conditions. + * + * - **Completing the match**: Apply a finalizer such as `Match.exhaustive`, + * `Match.orElse`, or `Match.option` to determine how unmatched cases should + * be handled. + * + * @since 1.0.0 + */ +import type * as Either from "./Either.js" +import * as internal from "./internal/matcher.js" +import type * as Option from "./Option.js" +import type { Pipeable } from "./Pipeable.js" +import * as Predicate from "./Predicate.js" +import type * as T from "./Types.js" +import type { Unify } from "./Unify.js" + +/** + * @category Symbols + * @since 1.0.0 + */ +export const MatcherTypeId: unique symbol = internal.TypeId + +/** + * @category Symbols + * @since 1.0.0 + */ +export type MatcherTypeId = typeof MatcherTypeId + +/** + * Pattern matching follows a structured process: + * + * - **Creating a matcher**: Define a `Matcher` that operates on either a + * specific `Match.type` or `Match.value`. + * + * - **Defining patterns**: Use combinators such as `Match.when`, `Match.not`, + * and `Match.tag` to specify matching conditions. + * + * - **Completing the match**: Apply a finalizer such as `Match.exhaustive`, + * `Match.orElse`, or `Match.option` to determine how unmatched cases should + * be handled. + * + * @example + * ```ts + * import { Match } from "effect" + * + * // Simulated dynamic input that can be a string or a number + * const input: string | number = "some input" + * + * // ┌─── string + * // ▼ + * const result = Match.value(input).pipe( + * // Match if the value is a number + * Match.when(Match.number, (n) => `number: ${n}`), + * // Match if the value is a string + * Match.when(Match.string, (s) => `string: ${s}`), + * // Ensure all possible cases are covered + * Match.exhaustive + * ) + * + * console.log(result) + * // Output: "string: some input" + * ``` + * + * @category Model + * @since 1.0.0 + */ +export type Matcher = + | TypeMatcher + | ValueMatcher + +/** + * @category Model + * @since 1.0.0 + */ +export interface TypeMatcher extends Pipeable { + readonly _tag: "TypeMatcher" + readonly [MatcherTypeId]: { + readonly _input: T.Contravariant + readonly _filters: T.Covariant + readonly _remaining: T.Covariant + readonly _result: T.Covariant + readonly _return: T.Covariant + } + readonly cases: ReadonlyArray + add(_case: Case): TypeMatcher +} + +/** + * @category Model + * @since 1.0.0 + */ +export interface ValueMatcher + extends Pipeable +{ + readonly _tag: "ValueMatcher" + readonly [MatcherTypeId]: { + readonly _input: T.Contravariant + readonly _filters: T.Covariant + readonly _remaining: T.Covariant + readonly _result: T.Covariant + readonly _provided: T.Covariant + readonly _return: T.Covariant + } + readonly provided: Provided + readonly value: Either.Either + add(_case: Case): ValueMatcher +} + +/** + * @category Model + * @since 1.0.0 + */ +export type Case = When | Not + +/** + * @category Model + * @since 1.0.0 + */ +export interface When { + readonly _tag: "When" + guard(u: unknown): boolean + evaluate(input: unknown): any +} + +/** + * @category Model + * @since 1.0.0 + */ +export interface Not { + readonly _tag: "Not" + guard(u: unknown): boolean + evaluate(input: unknown): any +} + +/** + * Creates a matcher for a specific type. + * + * **Details** + * + * This function defines a `Matcher` that operates on a given type, allowing you + * to specify conditions for handling different cases. Once the matcher is + * created, you can use pattern-matching functions like {@link when} to define + * how different values should be processed. + * + * **Example** (Matching Numbers and Strings) + * + * ```ts + * import { Match } from "effect" + * + * // Create a matcher for values that are either strings or numbers + * // + * // ┌─── (u: string | number) => string + * // ▼ + * const match = Match.type().pipe( + * // Match when the value is a number + * Match.when(Match.number, (n) => `number: ${n}`), + * // Match when the value is a string + * Match.when(Match.string, (s) => `string: ${s}`), + * // Ensure all possible cases are handled + * Match.exhaustive + * ) + * + * console.log(match(0)) + * // Output: "number: 0" + * + * console.log(match("hello")) + * // Output: "string: hello" + * ``` + * + * @see {@link value} for creating a matcher from a specific value. + * + * @category Creating a matcher + * @since 1.0.0 + */ +export const type: () => Matcher, I, never, never> = internal.type + +/** + * Creates a matcher from a specific value. + * + * **Details** + * + * This function allows you to define a `Matcher` directly from a given value, + * rather than from a type. This is useful when working with known values, + * enabling structured pattern matching on objects, primitives, or any data + * structure. + * + * Once the matcher is created, you can use pattern-matching functions like + * {@link when} to define how different cases should be handled. + * + * **Example** (Matching an Object by Property) + * + * ```ts + * import { Match } from "effect" + * + * const input = { name: "John", age: 30 } + * + * // Create a matcher for the specific object + * const result = Match.value(input).pipe( + * // Match when the 'name' property is "John" + * Match.when( + * { name: "John" }, + * (user) => `${user.name} is ${user.age} years old` + * ), + * // Provide a fallback if no match is found + * Match.orElse(() => "Oh, not John") + * ) + * + * console.log(result) + * // Output: "John is 30 years old" + * ``` + * + * @see {@link type} for creating a matcher from a specific type. + * + * @category Creating a matcher + * @since 1.0.0 + */ +export const value: ( + i: I +) => Matcher, I, never, I> = internal.value + +/** + * @category Creating a matcher + * @since 1.0.0 + */ +export const valueTags: { + < + const I, + P extends + & { readonly [Tag in Types.Tags<"_tag", I> & string]: (_: Extract) => any } + & { readonly [Tag in Exclude>]: never } + >(fields: P): (input: I) => Unify> + < + const I, + P extends + & { readonly [Tag in Types.Tags<"_tag", I> & string]: (_: Extract) => any } + & { readonly [Tag in Exclude>]: never } + >(input: I, fields: P): Unify> +} = internal.valueTags + +/** + * @category Creating a matcher + * @since 1.0.0 + */ +export const typeTags: { + (): < + P extends + & { + readonly [Tag in Types.Tags<"_tag", I> & string]: ( + _: Extract + ) => Ret + } + & { readonly [Tag in Exclude>]: never } + >(fields: P) => (input: I) => Ret + (): < + P extends + & { + readonly [Tag in Types.Tags<"_tag", I> & string]: ( + _: Extract + ) => any + } + & { readonly [Tag in Exclude>]: never } + >(fields: P) => (input: I) => Unify> +} = internal.typeTags + +/** + * Ensures that all branches of a matcher return a specific type. + * + * **Details** + * + * This function enforces a consistent return type across all pattern-matching + * branches. By specifying a return type, TypeScript will check that every + * matching condition produces a value of the expected type. + * + * **Important:** This function must be the first step in the matcher pipeline. + * If used later, TypeScript will not enforce type consistency correctly. + * + * **Example** (Validating Return Type Consistency) + * + * ```ts + * import { Match } from "effect" + * + * const match = Match.type<{ a: number } | { b: string }>().pipe( + * // Ensure all branches return a string + * Match.withReturnType(), + * // ❌ Type error: 'number' is not assignable to type 'string' + * // @ts-expect-error + * Match.when({ a: Match.number }, (_) => _.a), + * // ✅ Correct: returns a string + * Match.when({ b: Match.string }, (_) => _.b), + * Match.exhaustive + * ) + * ``` + * + * @since 1.0.0 + */ +export const withReturnType: () => ( + self: Matcher +) => [Ret] extends [[A] extends [never] ? any : A] ? Matcher + : "withReturnType constraint does not extend Result type" = internal.withReturnType + +/** + * Defines a condition for matching values. + * + * **Details** + * + * This function enables pattern matching by checking whether a given value + * satisfies a condition. It supports both direct value comparisons and + * predicate functions. If the condition is met, the associated function is + * executed. + * + * This function is useful when defining matchers that need to check for + * specific values or apply logical conditions to determine a match. It works + * well with structured objects and primitive types. + * + * **Example** (Matching with Values and Predicates) + * + * ```ts + * import { Match } from "effect" + * + * // Create a matcher for objects with an "age" property + * const match = Match.type<{ age: number }>().pipe( + * // Match when age is greater than 18 + * Match.when({ age: (age) => age > 18 }, (user) => `Age: ${user.age}`), + * // Match when age is exactly 18 + * Match.when({ age: 18 }, () => "You can vote"), + * // Fallback case for all other ages + * Match.orElse((user) => `${user.age} is too young`) + * ) + * + * console.log(match({ age: 20 })) + * // Output: "Age: 20" + * + * console.log(match({ age: 18 })) + * // Output: "You can vote" + * + * console.log(match({ age: 4 })) + * // Output: "4 is too young" + * ``` + * + * @see {@link whenOr} Use this when multiple patterns should match in a single + * condition. + * @see {@link whenAnd} Use this when a value must match all provided patterns. + * @see {@link orElse} Provides a fallback when no patterns match. + * + * @category Defining patterns + * @since 1.0.0 + */ +export const when: < + R, + const P extends Types.PatternPrimitive | Types.PatternBase, + Ret, + Fn extends (_: Types.WhenMatch) => Ret +>( + pattern: P, + f: Fn +) => ( + self: Matcher +) => Matcher< + I, + Types.AddWithout>, + Types.ApplyFilters>>, + A | ReturnType, + Pr, + Ret +> = internal.when + +/** + * Matches one of multiple patterns in a single condition. + * + * **Details** + * + * This function allows defining a condition where a value matches any of the + * provided patterns. If a match is found, the associated function is executed. + * It simplifies cases where multiple patterns share the same handling logic. + * + * Unlike {@link when}, which requires separate conditions for each pattern, + * this function enables combining them into a single statement, making the + * matcher more concise. + * + * @example + * ```ts + * import { Match } from "effect" + * + * type ErrorType = + * | { readonly _tag: "NetworkError"; readonly message: string } + * | { readonly _tag: "TimeoutError"; readonly duration: number } + * | { readonly _tag: "ValidationError"; readonly field: string } + * + * const handleError = Match.type().pipe( + * Match.whenOr( + * { _tag: "NetworkError" }, + * { _tag: "TimeoutError" }, + * () => "Retry the request" + * ), + * Match.when({ _tag: "ValidationError" }, (_) => `Invalid field: ${_.field}`), + * Match.exhaustive + * ) + * + * console.log(handleError({ _tag: "NetworkError", message: "No connection" })) + * // Output: "Retry the request" + * + * console.log(handleError({ _tag: "ValidationError", field: "email" })) + * // Output: "Invalid field: email" + * ``` + * + * @category Defining patterns + * @since 1.0.0 + */ +export const whenOr: < + R, + const P extends ReadonlyArray | Types.PatternBase>, + Ret, + Fn extends (_: Types.WhenMatch) => Ret +>( + ...args: [...patterns: P, f: Fn] +) => ( + self: Matcher +) => Matcher< + I, + Types.AddWithout>, + Types.ApplyFilters>>, + A | ReturnType, + Pr, + Ret +> = internal.whenOr + +/** + * Matches a value that satisfies all provided patterns. + * + * **Details** + * + * This function allows defining a condition where a value must match all the + * given patterns simultaneously. If the value satisfies every pattern, the + * associated function is executed. + * + * Unlike {@link when}, which matches a single pattern at a time, this function + * ensures that multiple conditions are met before executing the callback. It is + * useful when checking for values that need to fulfill multiple criteria at + * once. + * + * @example + * ```ts + * import { Match } from "effect" + * + * type User = { readonly age: number; readonly role: "admin" | "user" } + * + * const checkUser = Match.type().pipe( + * Match.whenAnd( + * { age: (n) => n >= 18 }, + * { role: "admin" }, + * () => "Admin access granted" + * ), + * Match.orElse(() => "Access denied") + * ) + * + * console.log(checkUser({ age: 20, role: "admin" })) + * // Output: "Admin access granted" + * + * console.log(checkUser({ age: 20, role: "user" })) + * // Output: "Access denied" + * ``` + * + * @category Defining patterns + * @since 1.0.0 + */ +export const whenAnd: < + R, + const P extends ReadonlyArray | Types.PatternBase>, + Ret, + Fn extends (_: Types.WhenMatch>) => Ret +>( + ...args: [...patterns: P, f: Fn] +) => ( + self: Matcher +) => Matcher< + I, + Types.AddWithout>>, + Types.ApplyFilters>>>, + A | ReturnType, + Pr +> = internal.whenAnd + +/** + * Matches values based on a specified discriminant field. + * + * **Details** + * + * This function is used to define pattern matching on objects that follow a + * **discriminated union** structure, where a specific field (e.g., `type`, + * `kind`, `_tag`) determines the variant of the object. It allows matching + * multiple values of the discriminant and provides a function to handle the + * matched cases. + * + * @example + * ```ts + * import { Match, pipe } from "effect" + * + * const match = pipe( + * Match.type<{ type: "A"; a: string } | { type: "B"; b: number } | { type: "C"; c: boolean }>(), + * Match.discriminator("type")("A", "B", (_) => `A or B: ${_.type}`), + * Match.discriminator("type")("C", (_) => `C(${_.c})`), + * Match.exhaustive + * ) + * ``` + * + * @category Defining patterns + * @since 1.0.0 + */ +export const discriminator: ( + field: D +) => & string, Ret, Fn extends (_: Extract>) => Ret>( + ...pattern: [first: P, ...values: Array

, f: Fn] +) => ( + self: Matcher +) => Matcher< + I, + Types.AddWithout>>, + Types.ApplyFilters>>>, + ReturnType | A, + Pr, + Ret +> = internal.tag + +/** + * Matches values where the `_tag` field starts with a given prefix. + * + * **Details** + * + * This function allows you to match on values in a **discriminated union** + * based on whether the `_tag` field starts with a specified prefix. It is + * useful for handling hierarchical or namespaced tags, where multiple related + * cases share a common prefix. + * + * @example + * ```ts + * import { Match, pipe } from "effect" + * + * const match = pipe( + * Match.type<{ _tag: "A" } | { _tag: "B" } | { _tag: "A.A" } | {}>(), + * Match.tagStartsWith("A", (_) => 1 as const), + * Match.tagStartsWith("B", (_) => 2 as const), + * Match.orElse((_) => 3 as const) + * ) + * + * console.log(match({ _tag: "A" })) // 1 + * console.log(match({ _tag: "B" })) // 2 + * console.log(match({ _tag: "A.A" })) // 1 + * ``` + * + * @category Defining patterns + * @since 1.0.0 + */ +export const tagStartsWith: < + R, + P extends string, + Ret, + Fn extends (_: Extract>) => Ret +>( + pattern: P, + f: Fn +) => ( + self: Matcher +) => Matcher< + I, + Types.AddWithout>>, + Types.ApplyFilters>>>, + ReturnType | A, + Pr, + Ret +> = internal.tagStartsWith + +/** + * Matches values based on their `_tag` field, mapping each tag to a + * corresponding handler. + * + * **Details** + * + * This function provides a way to handle discriminated unions by mapping `_tag` + * values to specific functions. Each handler receives the matched value and + * returns a transformed result. If all possible tags are handled, you can + * enforce exhaustiveness using `Match.exhaustive` to ensure no case is missed. + * + * @example + * ```ts + * import { Match, pipe } from "effect" + * + * const match = pipe( + * Match.type<{ _tag: "A"; a: string } | { _tag: "B"; b: number } | { _tag: "C"; c: boolean }>(), + * Match.tags({ + * A: (a) => a.a, + * B: (b) => b.b, + * C: (c) => c.c + * }), + * Match.exhaustive + * ) + * ``` + * + * @category Defining patterns + * @since 1.0.0 + */ +export const tags: < + R, + Ret, + P extends + & { readonly [Tag in Types.Tags<"_tag", R> & string]?: ((_: Extract>) => Ret) | undefined } + & { readonly [Tag in Exclude>]: never } +>( + fields: P +) => ( + self: Matcher +) => Matcher< + I, + Types.AddWithout>>, + Types.ApplyFilters>>>, + A | ReturnType, + Pr, + Ret +> = internal.tags + +/** + * Matches values based on their `_tag` field and requires handling of all + * possible cases. + * + * **Details** + * + * This function is designed for **discriminated unions** where every possible + * `_tag` value must have a corresponding handler. Unlike {@link tags}, this + * function ensures **exhaustiveness**, meaning all cases must be explicitly + * handled. If a `_tag` value is missing from the mapping, TypeScript will + * report an error. + * + * @example + * ```ts + * import { Match, pipe } from "effect" + * + * const match = pipe( + * Match.type<{ _tag: "A"; a: string } | { _tag: "B"; b: number } | { _tag: "C"; c: boolean }>(), + * Match.tagsExhaustive({ + * A: (a) => a.a, + * B: (b) => b.b, + * C: (c) => c.c + * }) + * ) + * ``` + * + * @category Defining patterns + * @since 1.0.0 + */ +export const tagsExhaustive: < + R, + Ret, + P extends + & { readonly [Tag in Types.Tags<"_tag", R> & string]: (_: Extract>) => Ret } + & { readonly [Tag in Exclude>]: never } +>( + fields: P +) => ( + self: Matcher +) => [Pr] extends [never] ? (u: I) => Unify> : Unify> = + internal.tagsExhaustive + +/** + * Excludes a specific value from matching while allowing all others. + * + * **Details** + * + * This function is useful when you need to **handle all values except one or + * more specific cases**. Instead of listing all possible matches manually, this + * function simplifies the logic by allowing you to specify values to exclude. + * Any excluded value will bypass the provided function and continue matching + * through other cases. + * + * **Example** (Ignoring a Specific Value) + * + * ```ts + * import { Match } from "effect" + * + * // Create a matcher for string or number values + * const match = Match.type().pipe( + * // Match any value except "hi", returning "ok" + * Match.not("hi", () => "ok"), + * // Fallback case for when the value is "hi" + * Match.orElse(() => "fallback") + * ) + * + * console.log(match("hello")) + * // Output: "ok" + * + * console.log(match("hi")) + * // Output: "fallback" + * ``` + * + * @category Defining patterns + * @since 1.0.0 + */ +export const not: < + R, + const P extends Types.PatternPrimitive | Types.PatternBase, + Ret, + Fn extends (_: Types.NotMatch) => Ret +>( + pattern: P, + f: Fn +) => ( + self: Matcher +) => Matcher< + I, + Types.AddOnly>, + Types.ApplyFilters>>, + A | ReturnType, + Pr, + Ret +> = internal.not + +/** + * Matches non-empty strings. + * + * @category Predicates + * @since 1.0.0 + */ +export const nonEmptyString: SafeRefinement = internal.nonEmptyString + +/** + * Matches a specific set of literal values (e.g., `Match.is("a", 42, true)`). + * + * @category Predicates + * @since 1.0.0 + */ +export const is: < + Literals extends ReadonlyArray +>(...literals: Literals) => SafeRefinement = internal.is + +/** + * Matches values of type `string`. + * + * @category Predicates + * @since 1.0.0 + */ +export const string: Predicate.Refinement = Predicate.isString + +/** + * Matches values of type `number`. + * + * @category Predicates + * @since 1.0.0 + */ +export const number: Predicate.Refinement = Predicate.isNumber + +/** + * Matches any value without restrictions. + * + * @category Predicates + * @since 1.0.0 + */ +export const any: SafeRefinement = internal.any + +/** + * Matches any defined (non-null and non-undefined) value. + * + * @category Predicates + * @since 1.0.0 + */ +export const defined: (u: A) => u is A & {} = internal.defined + +/** + * Matches values of type `boolean`. + * + * @category Predicates + * @since 1.0.0 + */ +export const boolean: Predicate.Refinement = Predicate.isBoolean + +const _undefined: Predicate.Refinement = Predicate.isUndefined +export { + /** + * Matches the value `undefined`. + * + * @category Predicates + * @since 1.0.0 + */ + _undefined as undefined +} + +const _null: Predicate.Refinement = Predicate.isNull +export { + /** + * Matches the value `null`. + * + * @category Predicates + * @since 1.0.0 + */ + _null as null +} + +/** + * Matches values of type `bigint`. + * + * @category Predicates + * @since 1.0.0 + */ +export const bigint: Predicate.Refinement = Predicate.isBigInt + +/** + * Matches values of type `symbol`. + * + * @category Predicates + * @since 1.0.0 + */ +export const symbol: Predicate.Refinement = Predicate.isSymbol + +/** + * Matches values that are instances of `Date`. + * + * @category Predicates + * @since 1.0.0 + */ +export const date: Predicate.Refinement = Predicate.isDate + +/** + * Matches objects where keys are `string` or `symbol` and values are `unknown`. + * + * @category Predicates + * @since 1.0.0 + */ +export const record: Predicate.Refinement = Predicate.isRecord + +/** + * Matches instances of a given class. + * + * @category Predicates + * @since 1.0.0 + */ +export const instanceOf: any>( + constructor: A +) => SafeRefinement, never> = internal.instanceOf + +/** + * @category Predicates + * @since 1.0.0 + */ +export const instanceOfUnsafe: any>( + constructor: A +) => SafeRefinement, InstanceType> = internal.instanceOf + +/** + * Provides a fallback value when no patterns match. + * + * **Details** + * + * This function ensures that a matcher always returns a valid result, even if + * no defined patterns match. It acts as a default case, similar to the + * `default` clause in a `switch` statement or the final `else` in an `if-else` + * chain. + * + * **Example** (Providing a Default Value When No Patterns Match) + * + * ```ts + * import { Match } from "effect" + * + * // Create a matcher for string or number values + * const match = Match.type().pipe( + * // Match when the value is "a" + * Match.when("a", () => "ok"), + * // Fallback when no patterns match + * Match.orElse(() => "fallback") + * ) + * + * console.log(match("a")) + * // Output: "ok" + * + * console.log(match("b")) + * // Output: "fallback" + * ``` + * + * @category Completion + * @since 1.0.0 + */ +export const orElse: Ret>( + f: F +) => ( + self: Matcher +) => [Pr] extends [never] ? (input: I) => Unify | A> : Unify | A> = internal.orElse + +// TODO(4.0): Rename to "orThrow"? Like Either.getOrThrow +/** + * Throws an error if no pattern matches. + * + * **Details** + * + * This function finalizes a matcher by ensuring that if no patterns match, an + * error is thrown. It is useful when all cases should be covered, and any + * unexpected input should trigger an error instead of returning a default + * value. + * + * When used, this function removes the need for an explicit fallback case and + * ensures that an unmatched value is never silently ignored. + * + * @category Completion + * @since 1.0.0 + */ +export const orElseAbsurd: ( + self: Matcher +) => [Pr] extends [never] ? (input: I) => Unify : Unify = internal.orElseAbsurd + +/** + * Wraps the match result in an `Either`, distinguishing matched and unmatched + * cases. + * + * **Details** + * + * This function ensures that the result of a matcher is always wrapped in an + * `Either`, allowing clear differentiation between successful matches + * (`Right(value)`) and cases where no pattern matched (`Left(unmatched + * value)`). + * + * This approach is particularly useful when handling optional values or when an + * unmatched case should be explicitly handled rather than returning a default + * value or throwing an error. + * + * **Example** (Extracting a User Role with `Match.either`) + * + * ```ts + * import { Match } from "effect" + * + * type User = { readonly role: "admin" | "editor" | "viewer" } + * + * // Create a matcher to extract user roles + * const getRole = Match.type().pipe( + * Match.when({ role: "admin" }, () => "Has full access"), + * Match.when({ role: "editor" }, () => "Can edit content"), + * Match.either // Wrap the result in an Either + * ) + * + * console.log(getRole({ role: "admin" })) + * // Output: { _id: 'Either', _tag: 'Right', right: 'Has full access' } + * + * console.log(getRole({ role: "viewer" })) + * // Output: { _id: 'Either', _tag: 'Left', left: { role: 'viewer' } } + * ``` + * + * @category Completion + * @since 1.0.0 + */ +export const either: ( + self: Matcher +) => [Pr] extends [never] ? (input: I) => Either.Either, R> : Either.Either, R> = internal.either + +/** + * Wraps the match result in an `Option`, representing an optional match. + * + * **Details** + * + * This function ensures that the result of a matcher is wrapped in an `Option`, + * making it easy to handle cases where no pattern matches. If a match is found, + * it returns `Some(value)`, otherwise, it returns `None`. + * + * This is useful in cases where a missing match is expected and should be + * handled explicitly rather than throwing an error or returning a default + * value. + * + * **Example** (Extracting a User Role with `Match.option`) + * + * ```ts + * import { Match } from "effect" + * + * type User = { readonly role: "admin" | "editor" | "viewer" } + * + * // Create a matcher to extract user roles + * const getRole = Match.type().pipe( + * Match.when({ role: "admin" }, () => "Has full access"), + * Match.when({ role: "editor" }, () => "Can edit content"), + * Match.option // Wrap the result in an Option + * ) + * + * console.log(getRole({ role: "admin" })) + * // Output: { _id: 'Option', _tag: 'Some', value: 'Has full access' } + * + * console.log(getRole({ role: "viewer" })) + * // Output: { _id: 'Option', _tag: 'None' } + * ``` + * + * @category Completion + * @since 1.0.0 + */ +export const option: ( + self: Matcher +) => [Pr] extends [never] ? (input: I) => Option.Option> : Option.Option> = internal.option + +/** + * The `Match.exhaustive` method finalizes the pattern matching process by + * ensuring that all possible cases are accounted for. If any case is missing, + * TypeScript will produce a type error. This is particularly useful when + * working with unions, as it helps prevent unintended gaps in pattern matching. + * + * **Example** (Ensuring All Cases Are Covered) + * + * ```ts + * import { Match } from "effect" + * + * // Create a matcher for string or number values + * const match = Match.type().pipe( + * // Match when the value is a number + * Match.when(Match.number, (n) => `number: ${n}`), + * // Mark the match as exhaustive, ensuring all cases are handled + * // TypeScript will throw an error if any case is missing + * // @ts-expect-error Type 'string' is not assignable to type 'never' + * Match.exhaustive + * ) + * ``` + * + * @category Completion + * @since 1.0.0 + */ +export const exhaustive: ( + self: Matcher +) => [Pr] extends [never] ? (u: I) => Unify : Unify = internal.exhaustive + +/** + * @since 1.0.0 + * @category Symbols + */ +export const SafeRefinementId = Symbol.for("effect/SafeRefinement") + +/** + * @since 1.0.0 + * @category Symbols + */ +export type SafeRefinementId = typeof SafeRefinementId + +/** + * @category Model + * @since 1.0.0 + */ +export interface SafeRefinement { + readonly [SafeRefinementId]: (a: A) => R +} + +const Fail = Symbol.for("effect/Fail") +type Fail = typeof Fail + +/** + * @since 1.0.0 + */ +export declare namespace Types { + /** + * @since 1.0.0 + */ + export type WhenMatch = + // check for any + [0] extends [1 & R] ? ResolvePred

: + P extends SafeRefinement ? SP + : P extends Predicate.Refinement + // try to narrow refinement + ? [Extract] extends [infer X] ? [X] extends [never] + // fallback to original refinement + ? RP + : X + : never + : P extends PredicateA ? PP + : ExtractMatch + + /** + * @since 1.0.0 + */ + export type NotMatch = Exclude>> + + type PForNotMatch

= [ToInvertedRefinement

] extends [infer X] ? X + : never + + /** + * @since 1.0.0 + */ + export type PForMatch

= [ResolvePred

] extends [infer X] ? X + : never + + /** + * @since 1.0.0 + */ + export type PForExclude

= [SafeRefinementR>] extends [infer X] ? X + : never + + // utilities + type PredicateA = Predicate.Predicate | Predicate.Refinement + + type SafeRefinementR = A extends never ? never + : A extends SafeRefinement ? R + : A extends Function ? A + : A extends Record ? { [K in keyof A]: SafeRefinementR } + : A + + type ResolvePred = A extends never ? never + : A extends SafeRefinement ? _A + : A extends Predicate.Refinement ? P + : A extends Predicate.Predicate ? P + : A extends Record ? { [K in keyof A]: ResolvePred } + : A + + type ToSafeRefinement = A extends never ? never + : A extends Predicate.Refinement ? SafeRefinement + : A extends Predicate.Predicate ? SafeRefinement + : A extends SafeRefinement ? A + : A extends Record ? { [K in keyof A]: ToSafeRefinement } + : NonLiteralsTo + + type ToInvertedRefinement = A extends never ? never + : A extends Predicate.Refinement ? SafeRefinement

+ : A extends Predicate.Predicate ? SafeRefinement + : A extends SafeRefinement ? SafeRefinement<_R> + : A extends Record ? { [K in keyof A]: ToInvertedRefinement } + : NonLiteralsTo + + type NonLiteralsTo = [A] extends [string | number | boolean | bigint] ? [string] extends [A] ? T + : [number] extends [A] ? T + : [boolean] extends [A] ? T + : [bigint] extends [A] ? T + : A + : A + + /** + * @since 1.0.0 + */ + export type PatternBase = A extends ReadonlyArray ? ReadonlyArray | PatternPrimitive + : A extends Record ? Partial< + { [K in keyof A]: PatternPrimitive | PatternBase } + > + : never + + /** + * @since 1.0.0 + */ + export type PatternPrimitive = PredicateA | A | SafeRefinement + + /** + * @since 1.0.0 + */ + export interface Without { + readonly _tag: "Without" + readonly _X: X + } + + /** + * @since 1.0.0 + */ + export interface Only { + readonly _tag: "Only" + readonly _X: X + } + + /** + * @since 1.0.0 + */ + export type AddWithout = [A] extends [Without] ? Without + : [A] extends [Only] ? Only> + : never + + /** + * @since 1.0.0 + */ + export type AddOnly = [A] extends [Without] ? [X] extends [WX] ? never + : Only + : [A] extends [Only] ? [X] extends [OX] ? Only + : never + : never + + /** + * @since 1.0.0 + */ + export type ApplyFilters = A extends Only ? X + : A extends Without ? Exclude + : never + + /** + * @since 1.0.0 + */ + export type Tags = P extends Record ? X : never + + /** + * @since 1.0.0 + */ + export type ArrayToIntersection> = T.UnionToIntersection< + A[number] + > + + /** + * @since 1.0.0 + */ + export type ExtractMatch = [ExtractAndNarrow] extends [infer EI] ? EI + : never + + type Replace = A extends Function ? A + : A extends Record ? { [K in keyof A]: K extends keyof B ? Replace : A[K] } + : [B] extends [A] ? B + : A + + type MaybeReplace = [P] extends [I] ? P + : [I] extends [P] ? Replace + : Fail + + type BuiltInObjects = + | Function + | Date + | RegExp + | Generator + | { readonly [Symbol.toStringTag]: string } + + type IsPlainObject = T extends BuiltInObjects ? false + : T extends Record ? true + : false + + type Simplify = { [K in keyof A]: A[K] } & {} + + type ExtractAndNarrow = P extends Predicate.Refinement ? + _Out extends Input ? Extract<_Out, Input> + : Extract : + P extends SafeRefinement ? [0] extends [1 & _R] ? Input + : _In extends Input ? Extract<_In, Input> + : Extract + : P extends Predicate.Predicate ? Extract + : Input extends infer I ? Exclude< + I extends ReadonlyArray ? P extends ReadonlyArray ? { + readonly [K in keyof I]: K extends keyof P ? ExtractAndNarrow + : I[K] + } extends infer R ? Fail extends R[keyof R] ? never + : R + : never + : never + : IsPlainObject extends true ? string extends keyof I ? I extends P ? I + : never + : symbol extends keyof I ? I extends P ? I + : never + : Simplify< + & { [RK in Extract]-?: ExtractAndNarrow } + & Omit + > extends infer R ? keyof P extends NonFailKeys ? R + : never + : never + : MaybeReplace extends infer R ? [I] extends [R] ? I + : R + : never, + Fail + > : + never + + type NonFailKeys = keyof A & {} extends infer K ? K extends keyof A ? A[K] extends Fail ? never : K + : never : + never +} diff --git a/repos/effect/packages/effect/src/MergeDecision.ts b/repos/effect/packages/effect/src/MergeDecision.ts new file mode 100644 index 0000000..7ca92d1 --- /dev/null +++ b/repos/effect/packages/effect/src/MergeDecision.ts @@ -0,0 +1,95 @@ +/** + * @since 2.0.0 + */ +import type * as Effect from "./Effect.js" +import type * as Exit from "./Exit.js" +import * as internal from "./internal/channel/mergeDecision.js" +import type * as Types from "./Types.js" + +/** + * @since 2.0.0 + * @category symbols + */ +export const MergeDecisionTypeId: unique symbol = internal.MergeDecisionTypeId + +/** + * @since 2.0.0 + * @category symbols + */ +export type MergeDecisionTypeId = typeof MergeDecisionTypeId + +/** + * @since 2.0.0 + * @category models + */ +export interface MergeDecision extends MergeDecision.Variance {} + +/** + * @since 2.0.0 + */ +export declare namespace MergeDecision { + /** + * @since 2.0.0 + * @category models + */ + export interface Variance { + readonly [MergeDecisionTypeId]: { + _R: Types.Covariant + _E0: Types.Contravariant + _Z0: Types.Contravariant + _E: Types.Covariant + _Z: Types.Covariant + } + } +} + +/** + * @since 2.0.0 + * @category constructors + */ +export const Done: (effect: Effect.Effect) => MergeDecision = internal.Done + +/** + * @since 2.0.0 + * @category constructors + */ +export const Await: ( + f: (exit: Exit.Exit) => Effect.Effect +) => MergeDecision = internal.Await + +/** + * @since 2.0.0 + * @category constructors + */ +export const AwaitConst: (effect: Effect.Effect) => MergeDecision = + internal.AwaitConst + +/** + * Returns `true` if the specified value is a `MergeDecision`, `false` + * otherwise. + * + * @since 2.0.0 + * @category refinements + */ +export const isMergeDecision: (u: unknown) => u is MergeDecision = + internal.isMergeDecision + +/** + * @since 2.0.0 + * @category folding + */ +export const match: { + ( + options: { + readonly onDone: (effect: Effect.Effect) => Z2 + readonly onAwait: (f: (exit: Exit.Exit) => Effect.Effect) => Z2 + } + ): (self: MergeDecision) => Z2 + ( + self: MergeDecision, + options: { + readonly onDone: (effect: Effect.Effect) => Z2 + readonly onAwait: (f: (exit: Exit.Exit) => Effect.Effect) => Z2 + } + ): Z2 +} = internal.match diff --git a/repos/effect/packages/effect/src/MergeState.ts b/repos/effect/packages/effect/src/MergeState.ts new file mode 100644 index 0000000..e06b673 --- /dev/null +++ b/repos/effect/packages/effect/src/MergeState.ts @@ -0,0 +1,172 @@ +/** + * @since 2.0.0 + */ +import type * as Effect from "./Effect.js" +import type * as Either from "./Either.js" +import type * as Exit from "./Exit.js" +import type * as Fiber from "./Fiber.js" +import * as internal from "./internal/channel/mergeState.js" + +/** + * @since 2.0.0 + * @category symbols + */ +export const MergeStateTypeId: unique symbol = internal.MergeStateTypeId + +/** + * @since 2.0.0 + * @category symbols + */ +export type MergeStateTypeId = typeof MergeStateTypeId + +/** + * @since 2.0.0 + * @category models + */ +export type MergeState = + | BothRunning + | LeftDone + | RightDone + +/** + * @since 2.0.0 + */ +export declare namespace MergeState { + /** + * @since 2.0.0 + * @category models + */ + export interface Proto { + readonly [MergeStateTypeId]: MergeStateTypeId + } +} + +/** + * @since 2.0.0 + * @category models + */ +export interface BothRunning<_Env, out Err, out Err1, _Err2, out Elem, out Done, out Done1, _Done2> + extends MergeState.Proto +{ + readonly _tag: "BothRunning" + readonly left: Fiber.Fiber, Err> + readonly right: Fiber.Fiber, Err1> +} + +/** + * @since 2.0.0 + * @category models + */ +export interface LeftDone + extends MergeState.Proto +{ + readonly _tag: "LeftDone" + f(exit: Exit.Exit): Effect.Effect +} + +/** + * @since 2.0.0 + * @category models + */ +export interface RightDone + extends MergeState.Proto +{ + readonly _tag: "RightDone" + f(exit: Exit.Exit): Effect.Effect +} + +/** + * @since 2.0.0 + * @category constructors + */ +export const BothRunning: ( + left: Fiber.Fiber, Err>, + right: Fiber.Fiber, Err1> +) => MergeState = internal.BothRunning + +/** + * @since 2.0.0 + * @category constructors + */ +export const LeftDone: ( + f: (exit: Exit.Exit) => Effect.Effect +) => MergeState = internal.LeftDone + +/** + * @since 2.0.0 + * @category constructors + */ +export const RightDone: ( + f: (exit: Exit.Exit) => Effect.Effect +) => MergeState = internal.RightDone + +/** + * Returns `true` if the specified value is a `MergeState`, `false` otherwise. + * + * @since 2.0.0 + * @category refinements + */ +export const isMergeState: ( + u: unknown +) => u is MergeState = internal.isMergeState + +/** + * Returns `true` if the specified `MergeState` is a `BothRunning`, `false` + * otherwise. + * + * @since 2.0.0 + * @category refinements + */ +export const isBothRunning: ( + self: MergeState +) => self is BothRunning = internal.isBothRunning + +/** + * Returns `true` if the specified `MergeState` is a `LeftDone`, `false` + * otherwise. + * + * @since 2.0.0 + * @category refinements + */ +export const isLeftDone: ( + self: MergeState +) => self is LeftDone = internal.isLeftDone + +/** + * Returns `true` if the specified `MergeState` is a `RightDone`, `false` + * otherwise. + * + * @since 2.0.0 + * @category refinements + */ +export const isRightDone: ( + self: MergeState +) => self is RightDone = internal.isRightDone + +/** + * @since 2.0.0 + * @category folding + */ +export const match: { + ( + options: { + readonly onBothRunning: ( + left: Fiber.Fiber, Err>, + right: Fiber.Fiber, Err1> + ) => Z + readonly onLeftDone: (f: (exit: Exit.Exit) => Effect.Effect) => Z + readonly onRightDone: (f: (exit: Exit.Exit) => Effect.Effect) => Z + } + ): (self: MergeState) => Z + ( + self: MergeState, + options: { + readonly onBothRunning: ( + left: Fiber.Fiber, Err>, + right: Fiber.Fiber, Err1> + ) => Z + readonly onLeftDone: (f: (exit: Exit.Exit) => Effect.Effect) => Z + readonly onRightDone: (f: (exit: Exit.Exit) => Effect.Effect) => Z + } + ): Z +} = internal.match diff --git a/repos/effect/packages/effect/src/MergeStrategy.ts b/repos/effect/packages/effect/src/MergeStrategy.ts new file mode 100644 index 0000000..2b8a991 --- /dev/null +++ b/repos/effect/packages/effect/src/MergeStrategy.ts @@ -0,0 +1,107 @@ +/** + * @since 2.0.0 + */ +import * as internal from "./internal/channel/mergeStrategy.js" + +/** + * @since 2.0.0 + * @category symbols + */ +export const MergeStrategyTypeId: unique symbol = internal.MergeStrategyTypeId + +/** + * @since 2.0.0 + * @category symbols + */ +export type MergeStrategyTypeId = typeof MergeStrategyTypeId + +/** + * @since 2.0.0 + * @category models + */ +export type MergeStrategy = BackPressure | BufferSliding + +/** + * @since 2.0.0 + */ +export declare namespace MergeStrategy { + /** + * @since 2.0.0 + * @category models + */ + export interface Proto { + readonly [MergeStrategyTypeId]: MergeStrategyTypeId + } +} + +/** + * @since 2.0.0 + * @category models + */ +export interface BackPressure extends MergeStrategy.Proto { + readonly _tag: "BackPressure" +} + +/** + * @since 2.0.0 + * @category models + */ +export interface BufferSliding extends MergeStrategy.Proto { + readonly _tag: "BufferSliding" +} + +/** + * @since 2.0.0 + * @category constructors + */ +export const BackPressure: (_: void) => MergeStrategy = internal.BackPressure + +/** + * @since 2.0.0 + * @category constructors + */ +export const BufferSliding: (_: void) => MergeStrategy = internal.BufferSliding + +/** + * Returns `true` if the specified value is a `MergeStrategy`, `false` + * otherwise. + * + * @since 2.0.0 + * @category refinements + */ +export const isMergeStrategy: (u: unknown) => u is MergeStrategy = internal.isMergeStrategy + +/** + * Returns `true` if the specified `MergeStrategy` is a `BackPressure`, `false` + * otherwise. + * + * @since 2.0.0 + * @category refinements + */ +export const isBackPressure: (self: MergeStrategy) => self is BackPressure = internal.isBackPressure + +/** + * Returns `true` if the specified `MergeStrategy` is a `BufferSliding`, `false` + * otherwise. + * + * @since 2.0.0 + * @category refinements + */ +export const isBufferSliding: (self: MergeStrategy) => self is BufferSliding = internal.isBufferSliding + +/** + * Folds an `MergeStrategy` into a value of type `A`. + * + * @since 2.0.0 + * @category folding + */ +export const match: { + (options: { + readonly onBackPressure: () => A + readonly onBufferSliding: () => A + }): (self: MergeStrategy) => A + (self: MergeStrategy, options: { + readonly onBackPressure: () => A + readonly onBufferSliding: () => A + }): A +} = internal.match diff --git a/repos/effect/packages/effect/src/Metric.ts b/repos/effect/packages/effect/src/Metric.ts new file mode 100644 index 0000000..54a3f87 --- /dev/null +++ b/repos/effect/packages/effect/src/Metric.ts @@ -0,0 +1,780 @@ +/** + * @since 2.0.0 + */ +import type * as Duration from "./Duration.js" +import type * as Effect from "./Effect.js" +import type { LazyArg } from "./Function.js" +import * as fiberRuntime from "./internal/fiberRuntime.js" +import * as internal from "./internal/metric.js" +import type * as MetricBoundaries from "./MetricBoundaries.js" +import type * as MetricKey from "./MetricKey.js" +import type * as MetricKeyType from "./MetricKeyType.js" +import type * as MetricLabel from "./MetricLabel.js" +import type * as MetricPair from "./MetricPair.js" +import type * as MetricRegistry from "./MetricRegistry.js" +import type * as MetricState from "./MetricState.js" +import type { Pipeable } from "./Pipeable.js" +import type * as Types from "./Types.js" + +/** + * @since 2.0.0 + * @category symbols + */ +export const MetricTypeId: unique symbol = internal.MetricTypeId + +/** + * @since 2.0.0 + * @category symbols + */ +export type MetricTypeId = typeof MetricTypeId + +/** + * A `Metric` represents a concurrent metric which accepts + * updates of type `In` and are aggregated to a stateful value of type `Out`. + * + * For example, a counter metric would have type `Metric`, + * representing the fact that the metric can be updated with numbers (the amount + * to increment or decrement the counter by), and the state of the counter is a + * number. + * + * There are five primitive metric types supported by Effect: + * + * - Counters + * - Frequencies + * - Gauges + * - Histograms + * - Summaries + * + * @since 2.0.0 + * @category models + */ +export interface Metric extends Metric.Variance, Pipeable { + /** + * The type of the underlying primitive metric. For example, this could be + * `MetricKeyType.Counter` or `MetricKeyType.Gauge`. + */ + readonly keyType: Type + unsafeUpdate(input: In, extraTags: ReadonlyArray): void + unsafeValue(extraTags: ReadonlyArray): Out + unsafeModify(input: In, extraTags: ReadonlyArray): void + register(): this + (effect: Effect.Effect): Effect.Effect +} + +/** + * @since 2.0.0 + * @category models + */ +export interface MetricApply { + ( + keyType: Type, + unsafeUpdate: (input: In, extraTags: ReadonlyArray) => void, + unsafeValue: (extraTags: ReadonlyArray) => Out, + unsafeModify: (input: In, extraTags: ReadonlyArray) => void + ): Metric +} + +/** + * @since 2.0.0 + */ +export declare namespace Metric { + /** + * @since 2.0.0 + * @category models + */ + export interface Counter + extends Metric, In, MetricState.MetricState.Counter> + {} + + /** + * @since 2.0.0 + * @category models + */ + export interface Gauge + extends Metric, In, MetricState.MetricState.Gauge> + {} + + /** + * @since 2.0.0 + * @category models + */ + export interface Frequency + extends Metric + {} + + /** + * @since 2.0.0 + * @category models + */ + export interface Histogram + extends Metric + {} + + /** + * @since 2.0.0 + * @category models + */ + export interface Summary + extends Metric + {} + + /** + * @since 2.0.0 + * @category models + */ + export interface Variance { + readonly [MetricTypeId]: { + readonly _Type: Types.Invariant + readonly _In: Types.Contravariant + readonly _Out: Types.Covariant + } + } +} + +/** + * @since 2.0.0 + * @category globals + */ +export const globalMetricRegistry: MetricRegistry.MetricRegistry = internal.globalMetricRegistry + +/** + * @since 2.0.0 + * @category constructors + */ +export const make: MetricApply = internal.make + +/** + * Returns a new metric that is powered by this one, but which accepts updates + * of the specified new type, which must be transformable to the input type of + * this metric. + * + * @since 2.0.0 + * @category mapping + */ +export const mapInput: { + (f: (input: In2) => In): (self: Metric) => Metric + (self: Metric, f: (input: In2) => In): Metric +} = internal.mapInput + +/** + * Represents a Counter metric that tracks cumulative numerical values over time. + * Counters can be incremented and decremented and provide a running total of changes. + * + * **Options** + * + * - description - A description of the counter. + * - bigint - Indicates if the counter uses 'bigint' data type. + * - incremental - Set to 'true' for a counter that only increases. With this configuration, Effect ensures that non-incremental updates have no impact on the counter, making it exclusively suitable for counting upwards. + * + * @example + * ```ts + * import { Metric } from "effect" + * + * const numberCounter = Metric.counter("count", { + * description: "A number counter" + * }); + * + * const bigintCounter = Metric.counter("count", { + * description: "A bigint counter", + * bigint: true + * }); + * ``` + * + * @since 2.0.0 + * @category constructors + */ +export const counter: { + ( + name: string, + options?: { + readonly description?: string | undefined + readonly bigint?: false | undefined + readonly incremental?: boolean | undefined + } + ): Metric.Counter + ( + name: string, + options: { + readonly description?: string | undefined + readonly bigint: true + readonly incremental?: boolean | undefined + } + ): Metric.Counter +} = internal.counter + +/** + * Creates a Frequency metric to count occurrences of events. + * Frequency metrics are used to count the number of times specific events or incidents occur. + * + * @example + * ```ts + * import { Metric } from "effect" + * + * const errorFrequency = Metric.frequency("error_frequency", { + * description: "Counts the occurrences of errors." + * }); + * ``` + * + * @since 2.0.0 + * @category constructors + */ +export const frequency: ( + name: string, + options?: + | { readonly description?: string | undefined; readonly preregisteredWords?: ReadonlyArray | undefined } + | undefined +) => Metric.Frequency = internal.frequency + +/** + * Returns a new metric that is powered by this one, but which accepts updates + * of any type, and translates them to updates with the specified constant + * update value. + * + * @since 2.0.0 + * @category constructors + */ +export const withConstantInput: { + (input: In): (self: Metric) => Metric + (self: Metric, input: In): Metric +} = internal.withConstantInput + +/** + * @since 2.0.0 + * @category constructors + */ +export const fromMetricKey: >( + key: MetricKey.MetricKey +) => Metric, MetricKeyType.MetricKeyType.OutType> = + internal.fromMetricKey + +/** + * Represents a Gauge metric that tracks and reports a single numerical value at a specific moment. + * Gauges are suitable for metrics that represent instantaneous values, such as memory usage or CPU load. + * + * **Options** + * + * - description - A description of the gauge metric. + * - bigint - Indicates if the counter uses 'bigint' data type. + * + * @example + * ```ts + * import { Metric } from "effect" + * + * const numberGauge = Metric.gauge("memory_usage", { + * description: "A gauge for memory usage" + * }); + * + * const bigintGauge = Metric.gauge("cpu_load", { + * description: "A gauge for CPU load", + * bigint: true + * }); + * ``` + * + * @since 2.0.0 + * @category constructors + */ +export const gauge: { + (name: string, options?: { + readonly description?: string | undefined + readonly bigint?: false | undefined + }): Metric.Gauge + (name: string, options: { + readonly description?: string | undefined + readonly bigint: true + }): Metric.Gauge +} = internal.gauge + +/** + * Represents a Histogram metric that records observations in specified value boundaries. + * Histogram metrics are useful for measuring the distribution of values within a range. + * + * @example + * ```ts + * import { Metric, MetricBoundaries } from "effect" + * + * const latencyHistogram = Metric.histogram("latency_histogram", + * MetricBoundaries.linear({ start: 0, width: 10, count: 11 }), + * "Measures the distribution of request latency." + * ); + * ``` + * + * @since 2.0.0 + * @category constructors + */ +export const histogram: ( + name: string, + boundaries: MetricBoundaries.MetricBoundaries, + description?: string +) => Metric = internal.histogram + +/** + * @since 2.0.0 + * @category combinators + */ +export const increment: ( + self: Metric.Counter | Metric.Counter | Metric.Gauge | Metric.Gauge +) => Effect.Effect = internal.increment + +/** + * @since 2.0.0 + * @category combinators + */ +export const incrementBy: { + (amount: number): (self: Metric.Counter | Metric.Counter) => Effect.Effect + (amount: bigint): (self: Metric.Counter | Metric.Gauge) => Effect.Effect + (self: Metric.Counter | Metric.Gauge, amount: number): Effect.Effect + (self: Metric.Counter | Metric.Gauge, amount: bigint): Effect.Effect +} = internal.incrementBy + +/** + * Returns a new metric that is powered by this one, but which outputs a new + * state type, determined by transforming the state type of this metric by the + * specified function. + * + * @since 2.0.0 + * @category mapping + */ +export const map: { + (f: (out: Out) => Out2): (self: Metric) => Metric + (self: Metric, f: (out: Out) => Out2): Metric +} = internal.map + +/** + * @since 2.0.0 + * @category mapping + */ +export const mapType: { + (f: (type: Type) => Type2): (self: Metric) => Metric + (self: Metric, f: (type: Type) => Type2): Metric +} = internal.mapType + +/** + * Modifies the metric with the specified update message. For example, if the + * metric were a gauge, the update would increment the method by the provided + * amount. + * + * @since 3.6.5 + * @category utils + */ +export const modify: { + (input: In): (self: Metric) => Effect.Effect + (self: Metric, input: In): Effect.Effect +} = internal.modify + +/** + * @since 2.0.0 + * @category aspects + */ +export const set: { + (value: number): (self: Metric.Gauge) => Effect.Effect + (value: bigint): (self: Metric.Gauge) => Effect.Effect + (self: Metric.Gauge, value: number): Effect.Effect + (self: Metric.Gauge, value: bigint): Effect.Effect +} = internal.set + +/** + * Captures a snapshot of all metrics recorded by the application. + * + * @since 2.0.0 + * @category getters + */ +export const snapshot: Effect.Effect> = internal.snapshot + +/** + * Creates a metric that ignores input and produces constant output. + * + * @since 2.0.0 + * @category constructors + */ +export const succeed: (out: Out) => Metric = internal.succeed + +/** + * Creates a metric that ignores input and produces constant output. + * + * @since 2.0.0 + * @category constructors + */ +export const sync: (evaluate: LazyArg) => Metric = internal.sync + +/** + * Creates a Summary metric that records observations and calculates quantiles. + * Summary metrics provide statistical information about a set of values, including quantiles. + * + * **Options** + * + * - name - The name of the Summary metric. + * - maxAge - The maximum age of observations to retain. + * - maxSize - The maximum number of observations to keep. + * - error - The error percentage when calculating quantiles. + * - quantiles - An `Chunk` of quantiles to calculate (e.g., [0.5, 0.9]). + * - description - An optional description of the Summary metric. + * + * @example + * ```ts + * import { Metric, Chunk } from "effect" + * + * const responseTimesSummary = Metric.summary({ + * name: "response_times_summary", + * maxAge: "60 seconds", // Retain observations for 60 seconds. + * maxSize: 1000, // Keep a maximum of 1000 observations. + * error: 0.01, // Allow a 1% error when calculating quantiles. + * quantiles: [0.5, 0.9, 0.99], // Calculate 50th, 90th, and 99th percentiles. + * description: "Measures the distribution of response times." + * }); + * ``` + * + * @since 2.0.0 + * @category constructors + */ +export const summary: ( + options: { + readonly name: string + readonly maxAge: Duration.DurationInput + readonly maxSize: number + readonly error: number + readonly quantiles: ReadonlyArray + readonly description?: string | undefined + } +) => Metric.Summary = internal.summary + +/** + * @since 2.0.0 + * @category constructors + */ +export const summaryTimestamp: ( + options: { + readonly name: string + readonly maxAge: Duration.DurationInput + readonly maxSize: number + readonly error: number + readonly quantiles: ReadonlyArray + readonly description?: string | undefined + } +) => Metric.Summary // readonly because contravariant + = internal.summaryTimestamp + +/** + * Returns a new metric, which is identical in every way to this one, except + * the specified tags have been added to the tags of this metric. + * + * @since 2.0.0 + * @category utils + */ +export const tagged: { + (key: string, value: string): (self: Metric) => Metric + (self: Metric, key: string, value: string): Metric +} = internal.tagged + +/** + * Returns a new metric, which is identical in every way to this one, except + * dynamic tags are added based on the update values. Note that the metric + * returned by this method does not return any useful information, due to the + * dynamic nature of the added tags. + * + * @since 2.0.0 + * @category utils + */ +export const taggedWithLabelsInput: { + ( + f: (input: In) => Iterable + ): (self: Metric) => Metric + ( + self: Metric, + f: (input: In) => Iterable + ): Metric +} = internal.taggedWithLabelsInput + +/** + * Returns a new metric, which is identical in every way to this one, except + * the specified tags have been added to the tags of this metric. + * + * @since 2.0.0 + * @category utils + */ +export const taggedWithLabels: { + (extraTags: Iterable): (self: Metric) => Metric + (self: Metric, extraTags: Iterable): Metric +} = internal.taggedWithLabels + +/** + * Creates a timer metric, based on a histogram, which keeps track of + * durations in milliseconds. The unit of time will automatically be added to + * the metric as a tag (i.e. `"time_unit: milliseconds"`). + * + * @since 2.0.0 + * @category constructors + */ +export const timer: ( + name: string, + description?: string +) => Metric = + internal.timer + +/** + * Creates a timer metric, based on a histogram created from the provided + * boundaries, which keeps track of durations in milliseconds. The unit of time + * will automatically be added to the metric as a tag (i.e. + * `"time_unit: milliseconds"`). + * + * @since 2.0.0 + * @category constructors + */ +export const timerWithBoundaries: ( + name: string, + boundaries: ReadonlyArray, + description?: string +) => Metric = + internal.timerWithBoundaries + +/** + * Returns an aspect that will update this metric with the specified constant + * value every time the aspect is applied to an effect, regardless of whether + * that effect fails or succeeds. + * + * @since 2.0.0 + * @category aspects + */ +export const trackAll: { + ( + input: In + ): (self: Metric) => (effect: Effect.Effect) => Effect.Effect + ( + self: Metric, + input: In + ): (effect: Effect.Effect) => Effect.Effect +} = internal.trackAll + +/** + * Returns an aspect that will update this metric with the defects of the + * effects that it is applied to. + * + * @since 2.0.0 + * @category aspects + */ +export const trackDefect: { + (metric: Metric): (self: Effect.Effect) => Effect.Effect + (self: Effect.Effect, metric: Metric): Effect.Effect +} = internal.trackDefect + +/** + * Returns an aspect that will update this metric with the result of applying + * the specified function to the defect throwables of the effects that the + * aspect is applied to. + * + * @since 2.0.0 + * @category aspects + */ +export const trackDefectWith: { + ( + metric: Metric, + f: (defect: unknown) => In + ): (self: Effect.Effect) => Effect.Effect + ( + self: Effect.Effect, + metric: Metric, + f: (defect: unknown) => In + ): Effect.Effect +} = internal.trackDefectWith + +/** + * Returns an aspect that will update this metric with the duration that the + * effect takes to execute. To call this method, the input type of the metric + * must be `Duration`. + * + * @since 2.0.0 + * @category aspects + */ +export const trackDuration: { + ( + metric: Metric + ): (self: Effect.Effect) => Effect.Effect + ( + self: Effect.Effect, + metric: Metric + ): Effect.Effect +} = internal.trackDuration + +/** + * Returns an aspect that will update this metric with the duration that the + * effect takes to execute. To call this method, you must supply a function + * that can convert the `Duration` to the input type of this metric. + * + * @since 2.0.0 + * @category aspects + */ +export const trackDurationWith: { + ( + metric: Metric, + f: (duration: Duration.Duration) => In + ): (effect: Effect.Effect) => Effect.Effect + ( + self: Effect.Effect, + metric: Metric, + f: (duration: Duration.Duration) => In + ): Effect.Effect +} = internal.trackDurationWith + +/** + * Returns an aspect that will update this metric with the failure value of + * the effects that it is applied to. + * + * @since 2.0.0 + * @category aspects + */ +export const trackError: { + ( + metric: Metric + ): (self: Effect.Effect) => Effect.Effect + ( + self: Effect.Effect, + metric: Metric + ): Effect.Effect +} = internal.trackError + +/** + * Returns an aspect that will update this metric with the result of applying + * the specified function to the error value of the effects that the aspect is + * applied to. + * + * @since 2.0.0 + * @category aspects + */ +export const trackErrorWith: { + ( + metric: Metric, + f: (error: In2) => In + ): (effect: Effect.Effect) => Effect.Effect + ( + self: Effect.Effect, + metric: Metric, + f: (error: In2) => In + ): Effect.Effect +} = internal.trackErrorWith + +/** + * Returns an aspect that will update this metric with the success value of + * the effects that it is applied to. + * + * @since 2.0.0 + * @category aspects + */ +export const trackSuccess: { + ( + metric: Metric + ): (self: Effect.Effect) => Effect.Effect + ( + self: Effect.Effect, + metric: Metric + ): Effect.Effect +} = internal.trackSuccess + +/** + * Returns an aspect that will update this metric with the result of applying + * the specified function to the success value of the effects that the aspect is + * applied to. + * + * @since 2.0.0 + * @category aspects + */ +export const trackSuccessWith: { + ( + metric: Metric, + f: (value: Types.NoInfer) => In + ): (self: Effect.Effect) => Effect.Effect + ( + self: Effect.Effect, + metric: Metric, + f: (value: Types.NoInfer) => In + ): Effect.Effect +} = internal.trackSuccessWith + +/** + * Updates the metric with the specified update message. For example, if the + * metric were a counter, the update would increment the method by the + * provided amount. + * + * @since 2.0.0 + * @category utils + */ +export const update: { + (input: In): (self: Metric) => Effect.Effect + (self: Metric, input: In): Effect.Effect +} = internal.update + +/** + * Retrieves a snapshot of the value of the metric at this moment in time. + * + * @since 2.0.0 + * @category getters + */ +export const value: (self: Metric) => Effect.Effect = internal.value + +/** + * @since 2.0.0 + * @category utils + */ +export const withNow: (self: Metric) => Metric = + internal.withNow + +/** + * @since 2.0.0 + * @category zipping + */ +export const zip: { + ( + that: Metric + ): ( + self: Metric + ) => Metric< + readonly [Type, Type2], // readonly because invariant + readonly [In, In2], // readonly because contravariant + [Out, Out2] + > + ( + self: Metric, + that: Metric + ): Metric< + readonly [Type, Type2], // readonly because invariant + readonly [In, In2], // readonly because contravariant + [Out, Out2] + > +} = internal.zip + +/** + * Unsafely captures a snapshot of all metrics recorded by the application. + * + * @since 2.0.0 + * @category unsafe + */ +export const unsafeSnapshot: (_: void) => ReadonlyArray = internal.unsafeSnapshot + +/** + * @since 2.0.0 + * @category metrics + */ +export const fiberStarted: Metric.Counter = fiberRuntime.fiberStarted + +/** + * @since 2.0.0 + * @category metrics + */ +export const fiberSuccesses: Metric.Counter = fiberRuntime.fiberSuccesses + +/** + * @since 2.0.0 + * @category metrics + */ +export const fiberFailures: Metric.Counter = fiberRuntime.fiberFailures + +/** + * @since 2.0.0 + * @category metrics + */ +export const fiberLifetimes: Metric = + fiberRuntime.fiberLifetimes + +/** + * @since 2.0.0 + * @category metrics + */ +export const fiberActive: Metric.Counter = fiberRuntime.fiberActive diff --git a/repos/effect/packages/effect/src/MetricBoundaries.ts b/repos/effect/packages/effect/src/MetricBoundaries.ts new file mode 100644 index 0000000..a4cb78a --- /dev/null +++ b/repos/effect/packages/effect/src/MetricBoundaries.ts @@ -0,0 +1,69 @@ +/** + * @since 2.0.0 + */ +import type * as Equal from "./Equal.js" +import * as internal from "./internal/metric/boundaries.js" +import type { Pipeable } from "./Pipeable.js" + +/** + * @since 2.0.0 + * @category symbols + */ +export const MetricBoundariesTypeId: unique symbol = internal.MetricBoundariesTypeId + +/** + * @since 2.0.0 + * @category symbols + */ +export type MetricBoundariesTypeId = typeof MetricBoundariesTypeId + +/** + * @since 2.0.0 + * @category models + */ +export interface MetricBoundaries extends Equal.Equal, Pipeable { + readonly [MetricBoundariesTypeId]: MetricBoundariesTypeId + readonly values: ReadonlyArray +} + +/** + * @since 2.0.0 + * @category refinements + */ +export const isMetricBoundaries: (u: unknown) => u is MetricBoundaries = internal.isMetricBoundaries + +/** + * @since 2.0.0 + * @category constructors + */ +export const fromIterable: (iterable: Iterable) => MetricBoundaries = internal.fromIterable + +/** + * A helper method to create histogram bucket boundaries for a histogram + * with linear increasing values. + * + * @since 2.0.0 + * @category constructors + */ +export const linear: ( + options: { + readonly start: number + readonly width: number + readonly count: number + } +) => MetricBoundaries = internal.linear + +/** + * A helper method to create histogram bucket boundaries for a histogram + * with exponentially increasing values. + * + * @since 2.0.0 + * @category constructors + */ +export const exponential: ( + options: { + readonly start: number + readonly factor: number + readonly count: number + } +) => MetricBoundaries = internal.exponential diff --git a/repos/effect/packages/effect/src/MetricHook.ts b/repos/effect/packages/effect/src/MetricHook.ts new file mode 100644 index 0000000..a480c25 --- /dev/null +++ b/repos/effect/packages/effect/src/MetricHook.ts @@ -0,0 +1,151 @@ +/** + * @since 2.0.0 + */ +import type { LazyArg } from "./Function.js" +import * as internal from "./internal/metric/hook.js" +import type * as MetricKey from "./MetricKey.js" +import type * as MetricState from "./MetricState.js" +import type { Pipeable } from "./Pipeable.js" +import type * as Types from "./Types.js" + +/** + * @since 2.0.0 + * @category symbols + */ +export const MetricHookTypeId: unique symbol = internal.MetricHookTypeId + +/** + * @since 2.0.0 + * @category symbols + */ +export type MetricHookTypeId = typeof MetricHookTypeId + +/** + * @since 2.0.0 + * @category models + */ +export interface MetricHook extends MetricHook.Variance, Pipeable { + get(): Out + update(input: In): void + modify(input: In): void +} + +/** + * @since 2.0.0 + */ +export declare namespace MetricHook { + /** + * @since 2.0.0 + * @category models + */ + export type Root = MetricHook + + /** + * @since 2.0.0 + * @category models + */ + export type Untyped = MetricHook + + /** + * @since 2.0.0 + * @category models + */ + export type Counter = MetricHook> + + /** + * @since 2.0.0 + * @category models + */ + export type Gauge = MetricHook> + + /** + * @since 2.0.0 + * @category models + */ + export type Frequency = MetricHook + + /** + * @since 2.0.0 + * @category models + */ + export type Histogram = MetricHook + + /** + * @since 2.0.0 + * @category models + */ + export type Summary = MetricHook + + /** + * @since 2.0.0 + * @category models + */ + export interface Variance { + readonly [MetricHookTypeId]: { + readonly _In: Types.Contravariant + readonly _Out: Types.Covariant + } + } +} + +/** + * @since 2.0.0 + * @category constructors + */ +export const make: (options: { + readonly get: LazyArg + readonly update: (input: In) => void + readonly modify: (input: In) => void +}) => MetricHook = internal.make + +/** + * @since 2.0.0 + * @category constructors + */ +export const counter: (key: MetricKey.MetricKey.Counter) => MetricHook.Counter = + internal.counter + +/** + * @since 2.0.0 + * @category constructors + */ +export const frequency: (_key: MetricKey.MetricKey.Frequency) => MetricHook.Frequency = internal.frequency + +/** + * @since 2.0.0 + * @category constructors + */ +export const gauge: { + (key: MetricKey.MetricKey.Gauge, startAt: number): MetricHook.Gauge + (key: MetricKey.MetricKey.Gauge, startAt: bigint): MetricHook.Gauge +} = internal.gauge + +/** + * @since 2.0.0 + * @category constructors + */ +export const histogram: (key: MetricKey.MetricKey.Histogram) => MetricHook.Histogram = internal.histogram + +/** + * @since 2.0.0 + * @category constructors + */ +export const summary: (key: MetricKey.MetricKey.Summary) => MetricHook.Summary = internal.summary + +/** + * @since 2.0.0 + * @category utils + */ +export const onUpdate: { + (f: (input: In) => void): (self: MetricHook) => MetricHook + (self: MetricHook, f: (input: In) => void): MetricHook +} = internal.onUpdate + +/** + * @since 3.6.5 + * @category utils + */ +export const onModify: { + (f: (input: In) => void): (self: MetricHook) => MetricHook + (self: MetricHook, f: (input: In) => void): MetricHook +} = internal.onModify diff --git a/repos/effect/packages/effect/src/MetricKey.ts b/repos/effect/packages/effect/src/MetricKey.ts new file mode 100644 index 0000000..d7fadf4 --- /dev/null +++ b/repos/effect/packages/effect/src/MetricKey.ts @@ -0,0 +1,224 @@ +/** + * @since 2.0.0 + */ +import type * as Duration from "./Duration.js" +import type * as Equal from "./Equal.js" +import * as internal from "./internal/metric/key.js" +import type * as MetricBoundaries from "./MetricBoundaries.js" +import type * as MetricKeyType from "./MetricKeyType.js" +import type * as MetricLabel from "./MetricLabel.js" +import type * as Option from "./Option.js" +import type { Pipeable } from "./Pipeable.js" +import type * as Types from "./Types.js" + +/** + * @since 2.0.0 + * @category symbols + */ +export const MetricKeyTypeId: unique symbol = internal.MetricKeyTypeId + +/** + * @since 2.0.0 + * @category symbols + */ +export type MetricKeyTypeId = typeof MetricKeyTypeId + +/** + * A `MetricKey` is a unique key associated with each metric. The key is based + * on a combination of the metric type, the name and tags associated with the + * metric, an optional description of the key, and any other information to + * describe a metric, such as the boundaries of a histogram. In this way, it is + * impossible to ever create different metrics with conflicting keys. + * + * @since 2.0.0 + * @category models + */ +export interface MetricKey> + extends MetricKey.Variance, Equal.Equal, Pipeable +{ + readonly name: string + readonly keyType: Type + readonly description: Option.Option + readonly tags: ReadonlyArray +} + +/** + * @since 2.0.0 + */ +export declare namespace MetricKey { + /** + * @since 2.0.0 + * @category models + */ + export type Untyped = MetricKey + + /** + * @since 2.0.0 + * @category models + */ + export type Counter = MetricKey> + + /** + * @since 2.0.0 + * @category models + */ + export type Gauge = MetricKey> + + /** + * @since 2.0.0 + * @category models + */ + export type Frequency = MetricKey + + /** + * @since 2.0.0 + * @category models + */ + export type Histogram = MetricKey + + /** + * @since 2.0.0 + * @category models + */ + export type Summary = MetricKey + + /** + * @since 2.0.0 + * @category models + */ + export interface Variance { + readonly [MetricKeyTypeId]: { + _Type: Types.Covariant + } + } +} + +/** + * @since 2.0.0 + * @category refinements + */ +export const isMetricKey: (u: unknown) => u is MetricKey> = + internal.isMetricKey + +/** + * Creates a metric key for a counter, with the specified name. + * + * @since 2.0.0 + * @category constructors + */ +export const counter: { + ( + name: string, + options?: { + readonly description?: string | undefined + readonly bigint?: false | undefined + readonly incremental?: boolean | undefined + } + ): MetricKey.Counter + ( + name: string, + options: { + readonly description?: string | undefined + readonly bigint: true + readonly incremental?: boolean | undefined + } + ): MetricKey.Counter +} = internal.counter + +/** + * Creates a metric key for a categorical frequency table, with the specified + * name. + * + * @since 2.0.0 + * @category constructors + */ +export const frequency: ( + name: string, + options?: + | { + readonly description?: string | undefined + readonly preregisteredWords?: ReadonlyArray | undefined + } + | undefined +) => MetricKey.Frequency = internal.frequency + +/** + * Creates a metric key for a gauge, with the specified name. + * + * @since 2.0.0 + * @category constructors + */ +export const gauge: { + (name: string, options?: { + readonly description?: string | undefined + readonly bigint?: false | undefined + }): MetricKey.Gauge + (name: string, options: { + readonly description?: string | undefined + readonly bigint: true + }): MetricKey.Gauge +} = internal.gauge + +/** + * Creates a metric key for a histogram, with the specified name and boundaries. + * + * @since 2.0.0 + * @category constructors + */ +export const histogram: ( + name: string, + boundaries: MetricBoundaries.MetricBoundaries, + description?: string +) => MetricKey.Histogram = internal.histogram + +/** + * Creates a metric key for a summary, with the specified name, maxAge, + * maxSize, error, and quantiles. + * + * @since 2.0.0 + * @category constructors + */ +export const summary: ( + options: { + readonly name: string + readonly maxAge: Duration.DurationInput + readonly maxSize: number + readonly error: number + readonly quantiles: ReadonlyArray + readonly description?: string | undefined + } +) => MetricKey.Summary = internal.summary + +/** + * Returns a new `MetricKey` with the specified tag appended. + * + * @since 2.0.0 + * @category constructors + */ +export const tagged: { + ( + key: string, + value: string + ): >(self: MetricKey) => MetricKey + >( + self: MetricKey, + key: string, + value: string + ): MetricKey +} = internal.tagged + +/** + * Returns a new `MetricKey` with the specified tags appended. + * + * @since 2.0.0 + * @category constructors + */ +export const taggedWithLabels: { + ( + extraTags: ReadonlyArray + ): >(self: MetricKey) => MetricKey + >( + self: MetricKey, + extraTags: ReadonlyArray + ): MetricKey +} = internal.taggedWithLabels diff --git a/repos/effect/packages/effect/src/MetricKeyType.ts b/repos/effect/packages/effect/src/MetricKeyType.ts new file mode 100644 index 0000000..ed325d8 --- /dev/null +++ b/repos/effect/packages/effect/src/MetricKeyType.ts @@ -0,0 +1,262 @@ +/** + * @since 2.0.0 + */ +import type * as Duration from "./Duration.js" +import type * as Equal from "./Equal.js" +import * as internal from "./internal/metric/keyType.js" +import type * as MetricBoundaries from "./MetricBoundaries.js" +import type * as MetricState from "./MetricState.js" +import type { Pipeable } from "./Pipeable.js" +import type * as Types from "./Types.js" + +/** + * @since 2.0.0 + * @category symbols + */ +export const MetricKeyTypeTypeId: unique symbol = internal.MetricKeyTypeTypeId + +/** + * @since 2.0.0 + * @category symbols + */ +export type MetricKeyTypeTypeId = typeof MetricKeyTypeTypeId + +/** + * @since 2.0.0 + * @category symbols + */ +export const CounterKeyTypeTypeId: unique symbol = internal.CounterKeyTypeTypeId + +/** + * @since 2.0.0 + * @category symbols + */ +export type CounterKeyTypeTypeId = typeof CounterKeyTypeTypeId + +/** + * @since 2.0.0 + * @category symbols + */ +export const FrequencyKeyTypeTypeId: unique symbol = internal.FrequencyKeyTypeTypeId + +/** + * @since 2.0.0 + * @category symbols + */ +export type FrequencyKeyTypeTypeId = typeof FrequencyKeyTypeTypeId + +/** + * @since 2.0.0 + * @category symbols + */ +export const GaugeKeyTypeTypeId: unique symbol = internal.GaugeKeyTypeTypeId + +/** + * @since 2.0.0 + * @category symbols + */ +export type GaugeKeyTypeTypeId = typeof GaugeKeyTypeTypeId + +/** + * @since 2.0.0 + * @category symbols + */ +export const HistogramKeyTypeTypeId: unique symbol = internal.HistogramKeyTypeTypeId + +/** + * @since 2.0.0 + * @category symbols + */ +export type HistogramKeyTypeTypeId = typeof HistogramKeyTypeTypeId + +/** + * @since 2.0.0 + * @category symbols + */ +export const SummaryKeyTypeTypeId: unique symbol = internal.SummaryKeyTypeTypeId + +/** + * @since 2.0.0 + * @category symbols + */ +export type SummaryKeyTypeTypeId = typeof SummaryKeyTypeTypeId + +/** + * @since 2.0.0 + * @category modelz + */ +export interface MetricKeyType extends MetricKeyType.Variance, Equal.Equal, Pipeable {} + +/** + * @since 2.0.0 + */ +export declare namespace MetricKeyType { + /** + * @since 2.0.0 + * @category models + */ + export type Untyped = MetricKeyType + + /** + * @since 2.0.0 + * @category models + */ + export type Counter = MetricKeyType> & { + readonly [CounterKeyTypeTypeId]: CounterKeyTypeTypeId + readonly incremental: boolean + readonly bigint: boolean + } + + /** + * @since 2.0.0 + * @category models + */ + export type Frequency = MetricKeyType & { + readonly [FrequencyKeyTypeTypeId]: FrequencyKeyTypeTypeId + readonly preregisteredWords: ReadonlyArray + } + + /** + * @since 2.0.0 + * @category models + */ + export type Gauge = MetricKeyType> & { + readonly [GaugeKeyTypeTypeId]: GaugeKeyTypeTypeId + readonly bigint: boolean + } + + /** + * @since 2.0.0 + * @category models + */ + export type Histogram = MetricKeyType & { + readonly [HistogramKeyTypeTypeId]: HistogramKeyTypeTypeId + readonly boundaries: MetricBoundaries.MetricBoundaries + } + + /** + * @since 2.0.0 + * @category models + */ + export type Summary = MetricKeyType & { + readonly [SummaryKeyTypeTypeId]: SummaryKeyTypeTypeId + readonly maxAge: Duration.Duration + readonly maxSize: number + readonly error: number + readonly quantiles: ReadonlyArray + } + + /** + * @since 2.0.0 + * @category models + */ + export interface Variance { + readonly [MetricKeyTypeTypeId]: { + readonly _In: Types.Contravariant + readonly _Out: Types.Covariant + } + } + + /** + * @since 2.0.0 + * @category models + */ + export type InType> = [Type] extends [ + { + readonly [MetricKeyTypeTypeId]: { + readonly _In: (_: infer In) => void + } + } + ] ? In + : never + + /** + * @since 2.0.0 + * @category models + */ + export type OutType> = [Type] extends [ + { + readonly [MetricKeyTypeTypeId]: { + readonly _Out: (_: never) => infer Out + } + } + ] ? Out + : never +} + +/** + * @since 2.0.0 + * @category constructors + */ +export const counter: () => MetricKeyType.Counter = internal.counter + +/** + * @since 2.0.0 + * @category constructors + */ +export const frequency: ( + options?: { + readonly preregisteredWords?: ReadonlyArray | undefined + } | undefined +) => MetricKeyType.Frequency = internal.frequency + +/** + * @since 2.0.0 + * @category constructors + */ +export const gauge: () => MetricKeyType.Gauge = internal.gauge + +/** + * @since 2.0.0 + * @category constructors + */ +export const histogram: (boundaries: MetricBoundaries.MetricBoundaries) => MetricKeyType.Histogram = internal.histogram + +/** + * @since 2.0.0 + * @category constructors + */ +export const summary: ( + options: { + readonly maxAge: Duration.DurationInput + readonly maxSize: number + readonly error: number + readonly quantiles: ReadonlyArray + } +) => MetricKeyType.Summary = internal.summary + +/** + * @since 2.0.0 + * @category refinements + */ +export const isMetricKeyType: (u: unknown) => u is MetricKeyType = internal.isMetricKeyType + +/** + * @since 2.0.0 + * @category refinements + */ +export const isCounterKey: (u: unknown) => u is MetricKeyType.Counter = internal.isCounterKey + +/** + * @since 2.0.0 + * @category refinements + */ +export const isFrequencyKey: (u: unknown) => u is MetricKeyType.Frequency = internal.isFrequencyKey + +/** + * @since 2.0.0 + * @category refinements + */ +export const isGaugeKey: (u: unknown) => u is MetricKeyType.Gauge = internal.isGaugeKey + +/** + * @since 2.0.0 + * @category refinements + */ +export const isHistogramKey: (u: unknown) => u is MetricKeyType.Histogram = internal.isHistogramKey + +/** + * @since 2.0.0 + * @category refinements + */ +export const isSummaryKey: (u: unknown) => u is MetricKeyType.Summary = internal.isSummaryKey diff --git a/repos/effect/packages/effect/src/MetricLabel.ts b/repos/effect/packages/effect/src/MetricLabel.ts new file mode 100644 index 0000000..dddaf77 --- /dev/null +++ b/repos/effect/packages/effect/src/MetricLabel.ts @@ -0,0 +1,47 @@ +/** + * @since 2.0.0 + */ +import type * as Equal from "./Equal.js" +import * as internal from "./internal/metric/label.js" +import type { Pipeable } from "./Pipeable.js" + +/** + * @since 2.0.0 + * @category symbols + */ +export const MetricLabelTypeId: unique symbol = internal.MetricLabelTypeId + +/** + * @since 2.0.0 + * @category symbols + */ +export type MetricLabelTypeId = typeof MetricLabelTypeId + +/** + * A `MetricLabel` represents a key value pair that allows analyzing metrics at + * an additional level of granularity. + * + * For example if a metric tracks the response time of a service labels could + * be used to create separate versions that track response times for different + * clients. + * + * @since 2.0.0 + * @category models + */ +export interface MetricLabel extends Equal.Equal, Pipeable { + readonly [MetricLabelTypeId]: MetricLabelTypeId + readonly key: string + readonly value: string +} + +/** + * @since 2.0.0 + * @category constructors + */ +export const make: (key: string, value: string) => MetricLabel = internal.make + +/** + * @since 2.0.0 + * @category refinements + */ +export const isMetricLabel: (u: unknown) => u is MetricLabel = internal.isMetricLabel diff --git a/repos/effect/packages/effect/src/MetricPair.ts b/repos/effect/packages/effect/src/MetricPair.ts new file mode 100644 index 0000000..0ed12b2 --- /dev/null +++ b/repos/effect/packages/effect/src/MetricPair.ts @@ -0,0 +1,71 @@ +/** + * @since 2.0.0 + */ +import * as internal from "./internal/metric/pair.js" +import type * as MetricKey from "./MetricKey.js" +import type * as MetricKeyType from "./MetricKeyType.js" +import type * as MetricState from "./MetricState.js" +import type { Pipeable } from "./Pipeable.js" +import type * as Types from "./Types.js" + +/** + * @since 2.0.0 + * @category symbols + */ +export const MetricPairTypeId: unique symbol = internal.MetricPairTypeId + +/** + * @since 2.0.0 + * @category symbols + */ +export type MetricPairTypeId = typeof MetricPairTypeId + +/** + * @since 2.0.0 + * @category model + */ +export interface MetricPair> + extends MetricPair.Variance, Pipeable +{ + readonly metricKey: MetricKey.MetricKey + readonly metricState: MetricState.MetricState> +} + +/** + * @since 2.0.0 + */ +export declare namespace MetricPair { + /** + * @since 2.0.0 + * @category models + */ + export interface Untyped extends MetricPair> {} + + /** + * @since 2.0.0 + * @category models + */ + export interface Variance> { + readonly [MetricPairTypeId]: { + readonly _Type: Types.Covariant + } + } +} + +/** + * @since 2.0.0 + * @category constructors + */ +export const make: >( + metricKey: MetricKey.MetricKey, + metricState: MetricState.MetricState> +) => MetricPair.Untyped = internal.make + +/** + * @since 2.0.0 + * @category unsafe + */ +export const unsafeMake: >( + metricKey: MetricKey.MetricKey, + metricState: MetricState.MetricState.Untyped +) => MetricPair.Untyped = internal.unsafeMake diff --git a/repos/effect/packages/effect/src/MetricPolling.ts b/repos/effect/packages/effect/src/MetricPolling.ts new file mode 100644 index 0000000..87aeed1 --- /dev/null +++ b/repos/effect/packages/effect/src/MetricPolling.ts @@ -0,0 +1,148 @@ +/** + * @since 2.0.0 + */ +import type * as Effect from "./Effect.js" +import type * as Fiber from "./Fiber.js" +import * as internal from "./internal/metric/polling.js" +import type * as Metric from "./Metric.js" +import type { Pipeable } from "./Pipeable.js" +import type * as Schedule from "./Schedule.js" +import type * as Scope from "./Scope.js" + +/** + * @since 2.0.0 + * @category symbols + */ +export const MetricPollingTypeId: unique symbol = internal.MetricPollingTypeId + +/** + * @since 2.0.0 + * @category symbols + */ +export type MetricPollingTypeId = typeof MetricPollingTypeId + +/** + * A `MetricPolling` is a combination of a metric and an effect that polls for + * updates to the metric. + * + * @since 2.0.0 + * @category models + */ +export interface MetricPolling extends Pipeable { + readonly [MetricPollingTypeId]: MetricPollingTypeId + /** + * The metric that this `MetricPolling` polls to update. + */ + readonly metric: Metric.Metric + /** + * An effect that polls a value that may be fed to the metric. + */ + readonly poll: Effect.Effect +} + +/** + * Constructs a new polling metric from a metric and poll effect. + * + * @since 2.0.0 + * @category constructors + */ +export const make: ( + metric: Metric.Metric, + poll: Effect.Effect +) => MetricPolling = internal.make + +/** + * Collects all of the polling metrics into a single polling metric, which + * polls for, updates, and produces the outputs of all individual metrics. + * + * @since 2.0.0 + * @category constructors + */ +export const collectAll: ( + iterable: Iterable> +) => MetricPolling, Array, R, E, Array> = internal.collectAll + +/** + * Returns an effect that will launch the polling metric in a background + * fiber, using the specified schedule. + * + * @since 2.0.0 + * @category utils + */ +export const launch: { + ( + schedule: Schedule.Schedule + ): ( + self: MetricPolling + ) => Effect.Effect, never, R2 | R | Scope.Scope> + ( + self: MetricPolling, + schedule: Schedule.Schedule + ): Effect.Effect, never, Scope.Scope | R | R2> +} = internal.launch + +/** + * An effect that polls a value that may be fed to the metric. + * + * @since 2.0.0 + * @category utils + */ +export const poll: (self: MetricPolling) => Effect.Effect = + internal.poll + +/** + * An effect that polls for a value and uses the value to update the metric. + * + * @since 2.0.0 + * @category utils + */ +export const pollAndUpdate: ( + self: MetricPolling +) => Effect.Effect = internal.pollAndUpdate + +/** + * Returns a new polling metric whose poll function will be retried with the + * specified retry policy. + * + * @since 2.0.0 + * @category constructors + */ +export const retry: { + ( + policy: Schedule.Schedule, R2> + ): (self: MetricPolling) => MetricPolling + ( + self: MetricPolling, + policy: Schedule.Schedule + ): MetricPolling +} = internal.retry + +/** + * Zips this polling metric with the specified polling metric. + * + * @since 2.0.0 + * @category utils + */ +export const zip: { + ( + that: MetricPolling + ): ( + self: MetricPolling + ) => MetricPolling< + readonly [Type, Type2], // readonly because invariant + readonly [In, In2], // readonly because contravariant + R2 | R, + E2 | E, + [Out, Out2] + > + ( + self: MetricPolling, + that: MetricPolling + ): MetricPolling< + readonly [Type, Type2], // readonly because invariant + readonly [In, In2], // readonly because contravariant + R | R2, + E | E2, + [Out, Out2] + > +} = internal.zip diff --git a/repos/effect/packages/effect/src/MetricRegistry.ts b/repos/effect/packages/effect/src/MetricRegistry.ts new file mode 100644 index 0000000..e43d851 --- /dev/null +++ b/repos/effect/packages/effect/src/MetricRegistry.ts @@ -0,0 +1,48 @@ +/** + * @since 2.0.0 + */ +import * as internal from "./internal/metric/registry.js" +import type * as MetricHook from "./MetricHook.js" +import type * as MetricKey from "./MetricKey.js" +import type * as MetricKeyType from "./MetricKeyType.js" +import type * as MetricPair from "./MetricPair.js" + +/** + * @since 2.0.0 + * @category symbols + */ +export const MetricRegistryTypeId: unique symbol = internal.MetricRegistryTypeId + +/** + * @since 2.0.0 + * @category symbols + */ +export type MetricRegistryTypeId = typeof MetricRegistryTypeId + +/** + * @since 2.0.0 + * @category models + */ +export interface MetricRegistry { + readonly [MetricRegistryTypeId]: MetricRegistryTypeId + snapshot(): Array + get>( + key: MetricKey.MetricKey + ): MetricHook.MetricHook< + MetricKeyType.MetricKeyType.InType, + MetricKeyType.MetricKeyType.OutType + > + getCounter( + key: MetricKey.MetricKey.Counter + ): MetricHook.MetricHook.Counter + getFrequency(key: MetricKey.MetricKey.Frequency): MetricHook.MetricHook.Frequency + getGauge(key: MetricKey.MetricKey.Gauge): MetricHook.MetricHook.Gauge + getHistogram(key: MetricKey.MetricKey.Histogram): MetricHook.MetricHook.Histogram + getSummary(key: MetricKey.MetricKey.Summary): MetricHook.MetricHook.Summary +} + +/** + * @since 2.0.0 + * @category constructors + */ +export const make: (_: void) => MetricRegistry = internal.make diff --git a/repos/effect/packages/effect/src/MetricState.ts b/repos/effect/packages/effect/src/MetricState.ts new file mode 100644 index 0000000..4c33ca8 --- /dev/null +++ b/repos/effect/packages/effect/src/MetricState.ts @@ -0,0 +1,257 @@ +/** + * @since 2.0.0 + */ +import type * as Equal from "./Equal.js" +import * as internal from "./internal/metric/state.js" +import type * as MetricKeyType from "./MetricKeyType.js" +import type * as Option from "./Option.js" +import type { Pipeable } from "./Pipeable.js" +import type * as Types from "./Types.js" + +/** + * @since 2.0.0 + * @category symbols + */ +export const MetricStateTypeId: unique symbol = internal.MetricStateTypeId + +/** + * @since 2.0.0 + * @category symbols + */ +export type MetricStateTypeId = typeof MetricStateTypeId + +/** + * @since 2.0.0 + * @category symbols + */ +export const CounterStateTypeId: unique symbol = internal.CounterStateTypeId + +/** + * @since 2.0.0 + * @category symbols + */ +export type CounterStateTypeId = typeof CounterStateTypeId + +/** + * @since 2.0.0 + * @category symbols + */ +export const FrequencyStateTypeId: unique symbol = internal.FrequencyStateTypeId + +/** + * @since 2.0.0 + * @category symbols + */ +export type FrequencyStateTypeId = typeof FrequencyStateTypeId + +/** + * @since 2.0.0 + * @category symbols + */ +export const GaugeStateTypeId: unique symbol = internal.GaugeStateTypeId + +/** + * @since 2.0.0 + * @category symbols + */ +export type GaugeStateTypeId = typeof GaugeStateTypeId + +/** + * @since 2.0.0 + * @category symbols + */ +export const HistogramStateTypeId: unique symbol = internal.HistogramStateTypeId + +/** + * @since 2.0.0 + * @category symbols + */ +export type HistogramStateTypeId = typeof HistogramStateTypeId + +/** + * @since 2.0.0 + * @category symbols + */ +export const SummaryStateTypeId: unique symbol = internal.SummaryStateTypeId + +/** + * @since 2.0.0 + * @category symbols + */ +export type SummaryStateTypeId = typeof SummaryStateTypeId + +/** + * A `MetricState` describes the state of a metric. The type parameter of a + * metric state corresponds to the type of the metric key (`MetricStateType`). + * This phantom type parameter is used to tie keys to their expected states. + * + * @since 2.0.0 + * @category models + */ +export interface MetricState extends MetricState.Variance, Equal.Equal, Pipeable {} + +/** + * @since 2.0.0 + */ +export declare namespace MetricState { + /** + * @since 2.0.0 + * @category models + */ + export interface Untyped extends MetricState {} + + /** + * @since 2.0.0 + * @category models + */ + export interface Counter + extends MetricState> + { + readonly [CounterStateTypeId]: CounterStateTypeId + readonly count: A + } + + /** + * @since 2.0.0 + * @category models + */ + export interface Frequency extends MetricState { + readonly [FrequencyStateTypeId]: FrequencyStateTypeId + readonly occurrences: ReadonlyMap + } + + /** + * @since 2.0.0 + * @category models + */ + export interface Gauge extends MetricState> { + readonly [GaugeStateTypeId]: GaugeStateTypeId + readonly value: A + } + + /** + * @since 2.0.0 + * @category models + */ + export interface Histogram extends MetricState { + readonly [HistogramStateTypeId]: HistogramStateTypeId + readonly buckets: ReadonlyArray + readonly count: number + readonly min: number + readonly max: number + readonly sum: number + } + + /** + * @since 2.0.0 + * @category models + */ + export interface Summary extends MetricState { + readonly [SummaryStateTypeId]: SummaryStateTypeId + readonly error: number + readonly quantiles: ReadonlyArray]> + readonly count: number + readonly min: number + readonly max: number + readonly sum: number + } + + /** + * @since 2.0.0 + * @category models + */ + export interface Variance { + readonly [MetricStateTypeId]: { + readonly _A: Types.Contravariant + } + } +} + +/** + * @since 2.0.0 + * @category constructors + */ +export const counter: { + (count: number): MetricState.Counter + (count: bigint): MetricState.Counter +} = internal.counter + +/** + * @since 2.0.0 + * @category constructors + */ +export const frequency: (occurrences: ReadonlyMap) => MetricState.Frequency = internal.frequency + +/** + * @since 2.0.0 + * @category constructors + */ +export const gauge: { + (count: number): MetricState.Gauge + (count: bigint): MetricState.Gauge +} = internal.gauge + +/** + * @since 2.0.0 + * @category constructors + */ +export const histogram: ( + options: { + readonly buckets: ReadonlyArray + readonly count: number + readonly min: number + readonly max: number + readonly sum: number + } +) => MetricState.Histogram = internal.histogram + +/** + * @since 2.0.0 + * @category constructors + */ +export const summary: ( + options: { + readonly error: number + readonly quantiles: ReadonlyArray]> + readonly count: number + readonly min: number + readonly max: number + readonly sum: number + } +) => MetricState.Summary = internal.summary + +/** + * @since 2.0.0 + * @category refinements + */ +export const isMetricState: (u: unknown) => u is MetricState.Counter = internal.isMetricState + +/** + * @since 2.0.0 + * @category refinements + */ +export const isCounterState: (u: unknown) => u is MetricState.Counter = internal.isCounterState + +/** + * @since 2.0.0 + * @category refinements + */ +export const isFrequencyState: (u: unknown) => u is MetricState.Frequency = internal.isFrequencyState + +/** + * @since 2.0.0 + * @category refinements + */ +export const isGaugeState: (u: unknown) => u is MetricState.Gauge = internal.isGaugeState + +/** + * @since 2.0.0 + * @category refinements + */ +export const isHistogramState: (u: unknown) => u is MetricState.Histogram = internal.isHistogramState + +/** + * @since 2.0.0 + * @category refinements + */ +export const isSummaryState: (u: unknown) => u is MetricState.Summary = internal.isSummaryState diff --git a/repos/effect/packages/effect/src/Micro.ts b/repos/effect/packages/effect/src/Micro.ts new file mode 100644 index 0000000..da290c2 --- /dev/null +++ b/repos/effect/packages/effect/src/Micro.ts @@ -0,0 +1,4405 @@ +/** + * A lightweight alternative to the `Effect` data type, with a subset of the functionality. + * + * @since 3.4.0 + * @experimental + */ +import * as Arr from "./Array.js" +import type { Channel } from "./Channel.js" +import * as Context from "./Context.js" +import type { Effect, EffectUnify, EffectUnifyIgnore } from "./Effect.js" +import * as Effectable from "./Effectable.js" +import * as Either from "./Either.js" +import * as Equal from "./Equal.js" +import type { LazyArg } from "./Function.js" +import { constTrue, constVoid, dual, identity } from "./Function.js" +import { globalValue } from "./GlobalValue.js" +import * as Hash from "./Hash.js" +import type { TypeLambda } from "./HKT.js" +import type { Inspectable } from "./Inspectable.js" +import { format, NodeInspectSymbol, toStringUnknown } from "./Inspectable.js" +import * as InternalContext from "./internal/context.js" +import * as doNotation from "./internal/doNotation.js" +import { StructuralPrototype } from "./internal/effectable.js" +import * as Option from "./Option.js" +import type { Pipeable } from "./Pipeable.js" +import { pipeArguments } from "./Pipeable.js" +import type { Predicate, Refinement } from "./Predicate.js" +import { hasProperty, isIterable, isTagged } from "./Predicate.js" +import type { Sink } from "./Sink.js" +import type { Stream } from "./Stream.js" +import type { Concurrency, Covariant, Equals, NoExcessProperties, NotFunction, Simplify } from "./Types.js" +import type * as Unify from "./Unify.js" +import { SingleShotGen, YieldWrap, yieldWrapGet } from "./Utils.js" + +/** + * @since 3.4.0 + * @experimental + * @category type ids + */ +export const TypeId: unique symbol = Symbol.for("effect/Micro") + +/** + * @since 3.4.0 + * @experimental + * @category type ids + */ +export type TypeId = typeof TypeId + +/** + * @since 3.4.0 + * @experimental + * @category MicroExit + */ +export const MicroExitTypeId: unique symbol = Symbol.for( + "effect/Micro/MicroExit" +) + +/** + * @since 3.4.0 + * @experimental + * @category MicroExit + */ +export type MicroExitTypeId = typeof TypeId + +/** + * A lightweight alternative to the `Effect` data type, with a subset of the functionality. + * + * @since 3.4.0 + * @experimental + * @category models + */ +export interface Micro extends Effect { + readonly [TypeId]: Micro.Variance + [Symbol.iterator](): MicroIterator> + [Unify.typeSymbol]?: unknown + [Unify.unifySymbol]?: MicroUnify + [Unify.ignoreSymbol]?: MicroUnifyIgnore +} + +/** + * @category models + * @since 3.4.3 + */ +export interface MicroUnify extends EffectUnify { + Micro?: () => A[Unify.typeSymbol] extends Micro | infer _ ? Micro : never +} + +/** + * @category models + * @since 3.4.3 + */ +export interface MicroUnifyIgnore extends EffectUnifyIgnore { + Effect?: true +} +/** + * @category type lambdas + * @since 3.4.1 + */ +export interface MicroTypeLambda extends TypeLambda { + readonly type: Micro +} + +/** + * @since 3.4.0 + * @experimental + */ +export declare namespace Micro { + /** + * @since 3.4.0 + * @experimental + */ + export interface Variance { + _A: Covariant + _E: Covariant + _R: Covariant + } + + /** + * @since 3.4.0 + * @experimental + */ + export type Success = T extends Micro ? _A : never + + /** + * @since 3.4.0 + * @experimental + */ + export type Error = T extends Micro ? _E : never + + /** + * @since 3.4.0 + * @experimental + */ + export type Context = T extends Micro ? _R : never +} + +/** + * @since 3.4.0 + * @experimental + * @category guards + */ +export const isMicro = (u: unknown): u is Micro => typeof u === "object" && u !== null && TypeId in u + +/** + * @since 3.4.0 + * @experimental + * @category models + */ +export interface MicroIterator> { + next(...args: ReadonlyArray): IteratorResult, Micro.Success> +} + +// ---------------------------------------------------------------------------- +// MicroCause +// ---------------------------------------------------------------------------- + +/** + * @since 3.4.6 + * @experimental + * @category MicroCause + */ +export const MicroCauseTypeId = Symbol.for("effect/Micro/MicroCause") + +/** + * @since 3.4.6 + * @experimental + * @category MicroCause + */ +export type MicroCauseTypeId = typeof MicroCauseTypeId + +/** + * A `MicroCause` is a data type that represents the different ways a `Micro` can fail. + * + * **Details** + * + * `MicroCause` comes in three forms: + * + * - `Die`: Indicates an unforeseen defect that wasn't planned for in the system's logic. + * - `Fail`: Covers anticipated errors that are recognized and typically handled within the application. + * - `Interrupt`: Signifies an operation that has been purposefully stopped. + * + * @since 3.4.6 + * @experimental + * @category MicroCause + */ +export type MicroCause = + | MicroCause.Die + | MicroCause.Fail + | MicroCause.Interrupt + +/** + * @since 3.6.6 + * @experimental + * @category guards + */ +export const isMicroCause = (self: unknown): self is MicroCause => hasProperty(self, MicroCauseTypeId) + +/** + * @since 3.4.6 + * @experimental + * @category MicroCause + */ +export declare namespace MicroCause { + /** + * @since 3.4.6 + * @experimental + */ + export type Error = T extends MicroCause.Fail ? E : never + + /** + * @since 3.4.0 + * @experimental + */ + export interface Proto extends Pipeable, globalThis.Error { + readonly [MicroCauseTypeId]: { + _E: Covariant + } + readonly _tag: Tag + readonly traces: ReadonlyArray + } + + /** + * @since 3.4.6 + * @experimental + * @category MicroCause + */ + export interface Die extends Proto<"Die", never> { + readonly defect: unknown + } + + /** + * @since 3.4.6 + * @experimental + * @category MicroCause + */ + export interface Fail extends Proto<"Fail", E> { + readonly error: E + } + + /** + * @since 3.4.6 + * @experimental + * @category MicroCause + */ + export interface Interrupt extends Proto<"Interrupt", never> {} +} + +const microCauseVariance = { + _E: identity +} + +abstract class MicroCauseImpl extends globalThis.Error implements MicroCause.Proto { + readonly [MicroCauseTypeId]: { + _E: Covariant + } + constructor( + readonly _tag: Tag, + originalError: unknown, + readonly traces: ReadonlyArray + ) { + const causeName = `MicroCause.${_tag}` + let name: string + let message: string + let stack: string + if (originalError instanceof globalThis.Error) { + name = `(${causeName}) ${originalError.name}` + message = originalError.message as string + const messageLines = message.split("\n").length + stack = originalError.stack + ? `(${causeName}) ${ + originalError.stack + .split("\n") + .slice(0, messageLines + 3) + .join("\n") + }` + : `${name}: ${message}` + } else { + name = causeName + message = toStringUnknown(originalError, 0) + stack = `${name}: ${message}` + } + if (traces.length > 0) { + stack += `\n ${traces.join("\n ")}` + } + super(message) + this[MicroCauseTypeId] = microCauseVariance + this.name = name + this.stack = stack + } + pipe() { + return pipeArguments(this, arguments) + } + toString() { + return this.stack + } + [NodeInspectSymbol]() { + return this.stack + } +} + +class Fail extends MicroCauseImpl<"Fail", E> implements MicroCause.Fail { + constructor( + readonly error: E, + traces: ReadonlyArray = [] + ) { + super("Fail", error, traces) + } +} + +/** + * @since 3.4.6 + * @experimental + * @category MicroCause + */ +export const causeFail = ( + error: E, + traces: ReadonlyArray = [] +): MicroCause => new Fail(error, traces) + +class Die extends MicroCauseImpl<"Die", never> implements MicroCause.Die { + constructor( + readonly defect: unknown, + traces: ReadonlyArray = [] + ) { + super("Die", defect, traces) + } +} + +/** + * @since 3.4.6 + * @experimental + * @category MicroCause + */ +export const causeDie = ( + defect: unknown, + traces: ReadonlyArray = [] +): MicroCause => new Die(defect, traces) + +class Interrupt extends MicroCauseImpl<"Interrupt", never> implements MicroCause.Interrupt { + constructor(traces: ReadonlyArray = []) { + super("Interrupt", "interrupted", traces) + } +} + +/** + * @since 3.4.6 + * @experimental + * @category MicroCause + */ +export const causeInterrupt = ( + traces: ReadonlyArray = [] +): MicroCause => new Interrupt(traces) + +/** + * @since 3.4.6 + * @experimental + * @category MicroCause + */ +export const causeIsFail = ( + self: MicroCause +): self is MicroCause.Fail => self._tag === "Fail" + +/** + * @since 3.4.6 + * @experimental + * @category MicroCause + */ +export const causeIsDie = (self: MicroCause): self is MicroCause.Die => self._tag === "Die" + +/** + * @since 3.4.6 + * @experimental + * @category MicroCause + */ +export const causeIsInterrupt = ( + self: MicroCause +): self is MicroCause.Interrupt => self._tag === "Interrupt" + +/** + * @since 3.4.6 + * @experimental + * @category MicroCause + */ +export const causeSquash = (self: MicroCause): unknown => + self._tag === "Fail" ? self.error : self._tag === "Die" ? self.defect : self + +/** + * @since 3.4.6 + * @experimental + * @category MicroCause + */ +export const causeWithTrace: { + (trace: string): (self: MicroCause) => MicroCause + (self: MicroCause, trace: string): MicroCause +} = dual(2, (self: MicroCause, trace: string): MicroCause => { + const traces = [...self.traces, trace] + switch (self._tag) { + case "Die": + return causeDie(self.defect, traces) + case "Interrupt": + return causeInterrupt(traces) + case "Fail": + return causeFail(self.error, traces) + } +}) + +// ---------------------------------------------------------------------------- +// MicroFiber +// ---------------------------------------------------------------------------- + +/** + * @since 3.11.0 + * @experimental + * @category MicroFiber + */ +export const MicroFiberTypeId = Symbol.for("effect/Micro/MicroFiber") + +/** + * @since 3.11.0 + * @experimental + * @category MicroFiber + */ +export type MicroFiberTypeId = typeof MicroFiberTypeId + +/** + * @since 3.11.0 + * @experimental + * @category MicroFiber + */ +export interface MicroFiber { + readonly [MicroFiberTypeId]: MicroFiber.Variance + + readonly currentOpCount: number + readonly getRef: (ref: Context.Reference) => A + readonly context: Context.Context + readonly addObserver: (cb: (exit: MicroExit) => void) => () => void + readonly unsafeInterrupt: () => void + readonly unsafePoll: () => MicroExit | undefined +} + +/** + * @since 3.11.0 + * @experimental + * @category MicroFiber + */ +export declare namespace MicroFiber { + /** + * @since 3.11.0 + * @experimental + * @category MicroFiber + */ + export interface Variance { + readonly _A: Covariant + readonly _E: Covariant + } +} + +const fiberVariance = { + _A: identity, + _E: identity +} + +class MicroFiberImpl implements MicroFiber { + readonly [MicroFiberTypeId]: MicroFiber.Variance + + readonly _stack: Array = [] + readonly _observers: Array<(exit: MicroExit) => void> = [] + _exit: MicroExit | undefined + public _children: Set> | undefined + + public currentOpCount = 0 + + constructor( + public context: Context.Context, + public interruptible = true + ) { + this[MicroFiberTypeId] = fiberVariance + } + + getRef(ref: Context.Reference): A { + return InternalContext.unsafeGetReference(this.context, ref) + } + + addObserver(cb: (exit: MicroExit) => void): () => void { + if (this._exit) { + cb(this._exit) + return constVoid + } + this._observers.push(cb) + return () => { + const index = this._observers.indexOf(cb) + if (index >= 0) { + this._observers.splice(index, 1) + } + } + } + + _interrupted = false + unsafeInterrupt(): void { + if (this._exit) { + return + } + this._interrupted = true + if (this.interruptible) { + this.evaluate(exitInterrupt as any) + } + } + + unsafePoll(): MicroExit | undefined { + return this._exit + } + + evaluate(effect: Primitive): void { + if (this._exit) { + return + } else if (this._yielded !== undefined) { + const yielded = this._yielded as () => void + this._yielded = undefined + yielded() + } + const exit = this.runLoop(effect) + if (exit === Yield) { + return + } + + // the interruptChildren middlware is added in Micro.fork, so it can be + // tree-shaken if not used + const interruptChildren = fiberMiddleware.interruptChildren && fiberMiddleware.interruptChildren(this) + if (interruptChildren !== undefined) { + return this.evaluate(flatMap(interruptChildren, () => exit) as any) + } + + this._exit = exit + for (let i = 0; i < this._observers.length; i++) { + this._observers[i](exit) + } + this._observers.length = 0 + } + + runLoop(effect: Primitive): MicroExit | Yield { + let yielding = false + let current: Primitive | Yield = effect + this.currentOpCount = 0 + try { + while (true) { + this.currentOpCount++ + if (!yielding && this.getRef(CurrentScheduler).shouldYield(this as any)) { + yielding = true + const prev = current + current = flatMap(yieldNow, () => prev as any) as any + } + current = (current as any)[evaluate](this) + if (current === Yield) { + const yielded = this._yielded! + if (MicroExitTypeId in yielded) { + this._yielded = undefined + return yielded + } + return Yield + } + } + } catch (error) { + if (!hasProperty(current, evaluate)) { + return exitDie(`MicroFiber.runLoop: Not a valid effect: ${String(current)}`) + } + return exitDie(error) + } + } + + getCont( + symbol: S + ): Primitive & Record Primitive> | undefined { + while (true) { + const op = this._stack.pop() + if (!op) return undefined + const cont = op[ensureCont] && op[ensureCont](this) + if (cont) return { [symbol]: cont } as any + if (op[symbol]) return op as any + } + } + + // cancel the yielded operation, or for the yielded exit value + _yielded: MicroExit | (() => void) | undefined = undefined + yieldWith(value: MicroExit | (() => void)): Yield { + this._yielded = value + return Yield + } + + children(): Set> { + return this._children ??= new Set() + } +} + +const fiberMiddleware = globalValue("effect/Micro/fiberMiddleware", () => ({ + interruptChildren: undefined as ((fiber: MicroFiberImpl) => Micro | undefined) | undefined +})) + +const fiberInterruptChildren = (fiber: MicroFiberImpl) => { + if (fiber._children === undefined || fiber._children.size === 0) { + return undefined + } + return fiberInterruptAll(fiber._children) +} + +/** + * @since 3.11.0 + * @experimental + * @category MicroFiber + */ +export const fiberAwait = (self: MicroFiber): Micro> => + async((resume) => sync(self.addObserver((exit) => resume(succeed(exit))))) + +/** + * @since 3.11.2 + * @experimental + * @category MicroFiber + */ +export const fiberJoin = (self: MicroFiber): Micro => flatten(fiberAwait(self)) + +/** + * @since 3.11.0 + * @experimental + * @category MicroFiber + */ +export const fiberInterrupt = (self: MicroFiber): Micro => + suspend(() => { + self.unsafeInterrupt() + return asVoid(fiberAwait(self)) + }) + +/** + * @since 3.11.0 + * @experimental + * @category MicroFiber + */ +export const fiberInterruptAll = >>(fibers: A): Micro => + suspend(() => { + for (const fiber of fibers) fiber.unsafeInterrupt() + const iter = fibers[Symbol.iterator]() + const wait: Micro = suspend(() => { + let result = iter.next() + while (!result.done) { + if (result.value.unsafePoll()) { + result = iter.next() + continue + } + const fiber = result.value + return async((resume) => { + fiber.addObserver((_) => { + resume(wait) + }) + }) + } + return exitVoid + }) + return wait + }) + +const identifier = Symbol.for("effect/Micro/identifier") +type identifier = typeof identifier + +const args = Symbol.for("effect/Micro/args") +type args = typeof args + +const evaluate = Symbol.for("effect/Micro/evaluate") +type evaluate = typeof evaluate + +const successCont = Symbol.for("effect/Micro/successCont") +type successCont = typeof successCont + +const failureCont = Symbol.for("effect/Micro/failureCont") +type failureCont = typeof failureCont + +const ensureCont = Symbol.for("effect/Micro/ensureCont") +type ensureCont = typeof ensureCont + +const Yield = Symbol.for("effect/Micro/Yield") +type Yield = typeof Yield + +interface Primitive { + readonly [identifier]: string + readonly [successCont]: ((value: unknown, fiber: MicroFiberImpl) => Primitive | Yield) | undefined + readonly [failureCont]: + | ((cause: MicroCause, fiber: MicroFiberImpl) => Primitive | Yield) + | undefined + readonly [ensureCont]: + | ((fiber: MicroFiberImpl) => + | ((value: unknown, fiber: MicroFiberImpl) => Primitive | Yield) + | undefined) + | undefined + [evaluate](fiber: MicroFiberImpl): Primitive | Yield +} + +const microVariance = { + _A: identity, + _E: identity, + _R: identity +} + +const MicroProto = { + ...Effectable.EffectPrototype, + _op: "Micro", + [TypeId]: microVariance, + pipe() { + return pipeArguments(this, arguments) + }, + [Symbol.iterator]() { + return new SingleShotGen(new YieldWrap(this)) as any + }, + toJSON(this: Primitive) { + return { + _id: "Micro", + op: this[identifier], + ...(args in this ? { args: this[args] } : undefined) + } + }, + toString() { + return format(this) + }, + [NodeInspectSymbol]() { + return format(this) + } +} + +function defaultEvaluate(_fiber: MicroFiberImpl): Primitive | Yield { + return exitDie(`Micro.evaluate: Not implemented`) as any +} + +const makePrimitiveProto = (options: { + readonly op: Op + readonly eval?: (fiber: MicroFiberImpl) => Primitive | Micro | Yield + readonly contA?: (this: Primitive, value: any, fiber: MicroFiberImpl) => Primitive | Micro | Yield + readonly contE?: ( + this: Primitive, + cause: MicroCause, + fiber: MicroFiberImpl + ) => Primitive | Micro | Yield + readonly ensure?: (this: Primitive, fiber: MicroFiberImpl) => void | ((value: any, fiber: MicroFiberImpl) => void) +}): Primitive => ({ + ...MicroProto, + [identifier]: options.op, + [evaluate]: options.eval ?? defaultEvaluate, + [successCont]: options.contA, + [failureCont]: options.contE, + [ensureCont]: options.ensure +} as any) + +const makePrimitive = ) => any, Single extends boolean = true>(options: { + readonly op: string + readonly single?: Single + readonly eval?: ( + this: Primitive & { readonly [args]: Single extends true ? Parameters[0] : Parameters }, + fiber: MicroFiberImpl + ) => Primitive | Micro | Yield + readonly contA?: ( + this: Primitive & { readonly [args]: Single extends true ? Parameters[0] : Parameters }, + value: any, + fiber: MicroFiberImpl + ) => Primitive | Micro | Yield + readonly contE?: ( + this: Primitive & { readonly [args]: Single extends true ? Parameters[0] : Parameters }, + cause: MicroCause, + fiber: MicroFiberImpl + ) => Primitive | Micro | Yield + readonly ensure?: ( + this: Primitive & { readonly [args]: Single extends true ? Parameters[0] : Parameters }, + fiber: MicroFiberImpl + ) => void | ((value: any, fiber: MicroFiberImpl) => void) +}): Fn => { + const Proto = makePrimitiveProto(options as any) + return function() { + const self = Object.create(Proto) + self[args] = options.single === false ? arguments : arguments[0] + return self + } as Fn +} + +const makeExit = ) => any, Prop extends string>(options: { + readonly op: "Success" | "Failure" + readonly prop: Prop + readonly eval: ( + this: + & MicroExit + & { [args]: Parameters[0] }, + fiber: MicroFiberImpl + ) => Primitive | Yield +}): Fn => { + const Proto = { + ...makePrimitiveProto(options), + [MicroExitTypeId]: MicroExitTypeId, + _tag: options.op, + get [options.prop](): any { + return (this as any)[args] + }, + toJSON(this: any) { + return { + _id: "MicroExit", + _tag: options.op, + [options.prop]: this[args] + } + }, + [Equal.symbol](this: any, that: any): boolean { + return isMicroExit(that) && that._tag === options.op && + Equal.equals(this[args], (that as any)[args]) + }, + [Hash.symbol](this: any): number { + return Hash.cached(this, Hash.combine(Hash.string(options.op))(Hash.hash(this[args]))) + } + } + return function(value: unknown) { + const self = Object.create(Proto) + self[args] = value + self[successCont] = undefined + self[failureCont] = undefined + self[ensureCont] = undefined + return self + } as Fn +} + +/** + * Creates a `Micro` effect that will succeed with the specified constant value. + * + * @since 3.4.0 + * @experimental + * @category constructors + */ +export const succeed: (value: A) => Micro = makeExit({ + op: "Success", + prop: "value", + eval(fiber) { + const cont = fiber.getCont(successCont) + return cont ? cont[successCont](this[args], fiber) : fiber.yieldWith(this) + } +}) + +/** + * Creates a `Micro` effect that will fail with the specified `MicroCause`. + * + * @since 3.4.6 + * @experimental + * @category constructors + */ +export const failCause: (cause: MicroCause) => Micro = makeExit({ + op: "Failure", + prop: "cause", + eval(fiber) { + let cont = fiber.getCont(failureCont) + while (causeIsInterrupt(this[args]) && cont && fiber.interruptible) { + cont = fiber.getCont(failureCont) + } + return cont ? cont[failureCont](this[args], fiber) : fiber.yieldWith(this) + } +}) + +/** + * Creates a `Micro` effect that fails with the given error. + * + * This results in a `Fail` variant of the `MicroCause` type, where the error is + * tracked at the type level. + * + * @since 3.4.0 + * @experimental + * @category constructors + */ +export const fail = (error: E): Micro => failCause(causeFail(error)) + +/** + * Creates a `Micro` effect that succeeds with a lazily evaluated value. + * + * If the evaluation of the value throws an error, the effect will fail with a + * `Die` variant of the `MicroCause` type. + * + * @since 3.4.0 + * @experimental + * @category constructors + */ +export const sync: (evaluate: LazyArg) => Micro = makePrimitive({ + op: "Sync", + eval(fiber): Primitive | Yield { + const value = this[args]() + const cont = fiber.getCont(successCont) + return cont ? cont[successCont](value, fiber) : fiber.yieldWith(exitSucceed(value)) + } +}) + +/** + * Lazily creates a `Micro` effect from the given side-effect. + * + * @since 3.4.0 + * @experimental + * @category constructors + */ +export const suspend: (evaluate: LazyArg>) => Micro = makePrimitive({ + op: "Suspend", + eval(_fiber) { + return this[args]() + } +}) + +/** + * Pause the execution of the current `Micro` effect, and resume it on the next + * scheduler tick. + * + * @since 3.4.0 + * @experimental + * @category constructors + */ +export const yieldNowWith: (priority?: number) => Micro = makePrimitive({ + op: "Yield", + eval(fiber) { + let resumed = false + fiber.getRef(CurrentScheduler).scheduleTask(() => { + if (resumed) return + fiber.evaluate(exitVoid as any) + }, this[args] ?? 0) + return fiber.yieldWith(() => { + resumed = true + }) + } +}) + +/** + * Pause the execution of the current `Micro` effect, and resume it on the next + * scheduler tick. + * + * @since 3.4.0 + * @experimental + * @category constructors + */ +export const yieldNow: Micro = yieldNowWith(0) + +/** + * Creates a `Micro` effect that will succeed with the value wrapped in `Some`. + * + * @since 3.4.0 + * @experimental + * @category constructors + */ +export const succeedSome = (a: A): Micro> => succeed(Option.some(a)) + +/** + * Creates a `Micro` effect that succeeds with `None`. + * + * @since 3.4.0 + * @experimental + * @category constructors + */ +export const succeedNone: Micro> = succeed(Option.none()) + +/** + * Creates a `Micro` effect that will fail with the lazily evaluated `MicroCause`. + * + * @since 3.4.0 + * @experimental + * @category constructors + */ +export const failCauseSync = (evaluate: LazyArg>): Micro => + suspend(() => failCause(evaluate())) + +/** + * Creates a `Micro` effect that will die with the specified error. + * + * This results in a `Die` variant of the `MicroCause` type, where the error is + * not tracked at the type level. + * + * @since 3.4.0 + * @experimental + * @category constructors + */ +export const die = (defect: unknown): Micro => exitDie(defect) + +/** + * Creates a `Micro` effect that will fail with the lazily evaluated error. + * + * This results in a `Fail` variant of the `MicroCause` type, where the error is + * tracked at the type level. + * + * @since 3.4.6 + * @experimental + * @category constructors + */ +export const failSync = (error: LazyArg): Micro => suspend(() => fail(error())) + +/** + * Converts an `Option` into a `Micro` effect, that will fail with + * `NoSuchElementException` if the option is `None`. Otherwise, it will succeed with the + * value of the option. + * + * @since 3.4.0 + * @experimental + * @category constructors + */ +export const fromOption = (option: Option.Option): Micro => + option._tag === "Some" ? succeed(option.value) : fail(new NoSuchElementException({})) + +/** + * Converts an `Either` into a `Micro` effect, that will fail with the left side + * of the either if it is a `Left`. Otherwise, it will succeed with the right + * side of the either. + * + * @since 3.4.0 + * @experimental + * @category constructors + */ +export const fromEither = (either: Either.Either): Micro => + either._tag === "Right" ? succeed(either.right) : fail(either.left) + +const void_: Micro = succeed(void 0) +export { + /** + * A `Micro` effect that will succeed with `void` (`undefined`). + * + * @since 3.4.0 + * @experimental + * @category constructors + */ + void_ as void +} + +const try_ = (options: { + try: LazyArg + catch: (error: unknown) => E +}): Micro => + suspend(() => { + try { + return succeed(options.try()) + } catch (err) { + return fail(options.catch(err)) + } + }) +export { + /** + * The `Micro` equivalent of a try / catch block, which allows you to map + * thrown errors to a specific error type. + * + * @example + * ```ts + * import { Micro } from "effect" + * + * Micro.try({ + * try: () => { throw new Error("boom") }, + * catch: (cause) => new Error("caught", { cause }) + * }) + * ``` + * + * @since 3.4.0 + * @experimental + * @category constructors + */ + try_ as try +} + +/** + * Wrap a `Promise` into a `Micro` effect. + * + * Any errors will result in a `Die` variant of the `MicroCause` type, where the + * error is not tracked at the type level. + * + * @since 3.4.0 + * @experimental + * @category constructors + */ +export const promise = (evaluate: (signal: AbortSignal) => PromiseLike): Micro => + asyncOptions(function(resume, signal) { + evaluate(signal!).then( + (a) => resume(succeed(a)), + (e) => resume(die(e)) + ) + }, evaluate.length !== 0) + +/** + * Wrap a `Promise` into a `Micro` effect. Any errors will be caught and + * converted into a specific error type. + * + * @example + * ```ts + * import { Micro } from "effect" + * + * Micro.tryPromise({ + * try: () => Promise.resolve("success"), + * catch: (cause) => new Error("caught", { cause }) + * }) + * ``` + * + * @since 3.4.0 + * @experimental + * @category constructors + */ +export const tryPromise = (options: { + readonly try: (signal: AbortSignal) => PromiseLike + readonly catch: (error: unknown) => E +}): Micro => + asyncOptions(function(resume, signal) { + try { + options.try(signal!).then( + (a) => resume(succeed(a)), + (e) => resume(fail(options.catch(e))) + ) + } catch (err) { + resume(fail(options.catch(err))) + } + }, options.try.length !== 0) + +/** + * Create a `Micro` effect using the current `MicroFiber`. + * + * @since 3.4.0 + * @experimental + * @category constructors + */ +export const withMicroFiber: ( + evaluate: (fiber: MicroFiberImpl) => Micro +) => Micro = makePrimitive({ + op: "WithMicroFiber", + eval(fiber) { + return this[args](fiber) + } +}) + +/** + * Flush any yielded effects that are waiting to be executed. + * + * @since 3.4.0 + * @experimental + * @category constructors + */ +export const yieldFlush: Micro = withMicroFiber((fiber) => { + fiber.getRef(CurrentScheduler).flush() + return exitVoid +}) + +const asyncOptions: ( + register: ( + resume: (effect: Micro) => void, + signal?: AbortSignal + ) => void | Micro, + withSignal: boolean +) => Micro = makePrimitive({ + op: "Async", + single: false, + eval(fiber) { + const register = this[args][0] + let resumed = false + let yielded: boolean | Primitive = false + const controller = this[args][1] ? new AbortController() : undefined + const onCancel = register((effect) => { + if (resumed) return + resumed = true + if (yielded) { + fiber.evaluate(effect as any) + } else { + yielded = effect as any + } + }, controller?.signal) + if (yielded !== false) return yielded + yielded = true + fiber._yielded = () => { + resumed = true + } + if (controller === undefined && onCancel === undefined) { + return Yield + } + fiber._stack.push(asyncFinalizer(() => { + resumed = true + controller?.abort() + return onCancel ?? exitVoid + })) + return Yield + } +}) +const asyncFinalizer: (onInterrupt: () => Micro) => Primitive = makePrimitive({ + op: "AsyncFinalizer", + ensure(fiber) { + if (fiber.interruptible) { + fiber.interruptible = false + fiber._stack.push(setInterruptible(true)) + } + }, + contE(cause, _fiber) { + return causeIsInterrupt(cause) + ? flatMap(this[args](), () => failCause(cause)) + : failCause(cause) + } +}) + +/** + * Create a `Micro` effect from an asynchronous computation. + * + * You can return a cleanup effect that will be run when the effect is aborted. + * It is also passed an `AbortSignal` that is triggered when the effect is + * aborted. + * + * @since 3.4.0 + * @experimental + * @category constructors + */ +export const async = ( + register: ( + resume: (effect: Micro) => void, + signal: AbortSignal + ) => void | Micro +): Micro => asyncOptions(register as any, register.length >= 2) + +/** + * A `Micro` that will never succeed or fail. It wraps `setInterval` to prevent + * the Javascript runtime from exiting. + * + * @since 3.4.0 + * @experimental + * @category constructors + */ +export const never: Micro = async(function() { + const interval = setInterval(constVoid, 2147483646) + return sync(() => clearInterval(interval)) +}) + +/** + * @since 3.4.0 + * @experimental + * @category constructors + */ +export const gen = >, AEff>( + ...args: + | [self: Self, body: (this: Self) => Generator] + | [body: () => Generator] +): Micro< + AEff, + [Eff] extends [never] ? never : [Eff] extends [YieldWrap>] ? E : never, + [Eff] extends [never] ? never : [Eff] extends [YieldWrap>] ? R : never +> => suspend(() => fromIterator(args.length === 1 ? args[0]() : args[1].call(args[0]) as any)) + +const fromIterator: ( + iterator: Iterator>> +) => Micro = makePrimitive({ + op: "Iterator", + contA(value, fiber) { + const state = this[args].next(value) + if (state.done) return succeed(state.value) + fiber._stack.push(this) + return yieldWrapGet(state.value) + }, + eval(this: any, fiber: MicroFiberImpl) { + return this[successCont](undefined, fiber) + } +}) + +// ---------------------------------------------------------------------------- +// mapping & sequencing +// ---------------------------------------------------------------------------- + +/** + * Create a `Micro` effect that will replace the success value of the given + * effect. + * + * @since 3.4.0 + * @experimental + * @category mapping & sequencing + */ +export const as: { + (value: B): (self: Micro) => Micro + (self: Micro, value: B): Micro +} = dual(2, (self: Micro, value: B): Micro => map(self, (_) => value)) + +/** + * Wrap the success value of this `Micro` effect in a `Some`. + * + * @since 3.4.0 + * @experimental + * @category mapping & sequencing + */ +export const asSome = (self: Micro): Micro, E, R> => map(self, Option.some) + +/** + * Swap the error and success types of the `Micro` effect. + * + * @since 3.4.0 + * @experimental + * @category mapping & sequencing + */ +export const flip = (self: Micro): Micro => + matchEffect(self, { + onFailure: succeed, + onSuccess: fail + }) + +/** + * A more flexible version of `flatMap` that combines `map` and `flatMap` into a + * single API. + * + * It also lets you directly pass a `Micro` effect, which will be executed after + * the current effect. + * + * @since 3.4.0 + * @experimental + * @category mapping & sequencing + */ +export const andThen: { + ( + f: (a: A) => X + ): ( + self: Micro + ) => [X] extends [Micro] ? Micro + : Micro + ( + f: NotFunction + ): ( + self: Micro + ) => [X] extends [Micro] ? Micro + : Micro + ( + self: Micro, + f: (a: A) => X + ): [X] extends [Micro] ? Micro + : Micro + ( + self: Micro, + f: NotFunction + ): [X] extends [Micro] ? Micro + : Micro +} = dual( + 2, + (self: Micro, f: any): Micro => + flatMap(self, (a) => { + const value = isMicro(f) ? f : typeof f === "function" ? f(a) : f + return isMicro(value) ? value : succeed(value) + }) +) + +/** + * Execute a side effect from the success value of the `Micro` effect. + * + * It is similar to the `andThen` api, but the success value is ignored. + * + * @since 3.4.0 + * @experimental + * @category mapping & sequencing + */ +export const tap: { + ( + f: (a: NoInfer) => X + ): ( + self: Micro + ) => [X] extends [Micro] ? Micro + : Micro + ( + f: NotFunction + ): ( + self: Micro + ) => [X] extends [Micro] ? Micro + : Micro + ( + self: Micro, + f: (a: NoInfer) => X + ): [X] extends [Micro] ? Micro + : Micro + ( + self: Micro, + f: NotFunction + ): [X] extends [Micro] ? Micro + : Micro +} = dual( + 2, + (self: Micro, f: (a: A) => Micro): Micro => + flatMap(self, (a) => { + const value = isMicro(f) ? f : typeof f === "function" ? f(a) : f + return isMicro(value) ? as(value, a) : succeed(a) + }) +) + +/** + * Replace the success value of the `Micro` effect with `void`. + * + * @since 3.4.0 + * @experimental + * @category mapping & sequencing + */ +export const asVoid = (self: Micro): Micro => flatMap(self, (_) => exitVoid) + +/** + * Access the `MicroExit` of the given `Micro` effect. + * + * @since 3.4.6 + * @experimental + * @category mapping & sequencing + */ +export const exit = (self: Micro): Micro, never, R> => + matchCause(self, { + onFailure: exitFailCause, + onSuccess: exitSucceed + }) + +/** + * Replace the error type of the given `Micro` with the full `MicroCause` object. + * + * @since 3.4.0 + * @experimental + * @category mapping & sequencing + */ +export const sandbox = (self: Micro): Micro, R> => catchAllCause(self, fail) + +/** + * Returns an effect that races all the specified effects, + * yielding the value of the first effect to succeed with a value. Losers of + * the race will be interrupted immediately + * + * @since 3.4.0 + * @experimental + * @category sequencing + */ +export const raceAll = >( + all: Iterable +): Micro, Micro.Error, Micro.Context> => + withMicroFiber((parent) => + async((resume) => { + const effects = Arr.fromIterable(all) + const len = effects.length + let doneCount = 0 + let done = false + const fibers = new Set>() + const causes: Array> = [] + const onExit = (exit: MicroExit) => { + doneCount++ + if (exit._tag === "Failure") { + causes.push(exit.cause) + if (doneCount >= len) { + resume(failCause(causes[0])) + } + return + } + done = true + resume(fibers.size === 0 ? exit : flatMap(uninterruptible(fiberInterruptAll(fibers)), () => exit)) + } + + for (let i = 0; i < len; i++) { + if (done) break + const fiber = unsafeFork(parent, interruptible(effects[i]), true, true) + fibers.add(fiber) + fiber.addObserver((exit) => { + fibers.delete(fiber) + onExit(exit) + }) + } + + return fiberInterruptAll(fibers) + }) + ) + +/** + * Returns an effect that races all the specified effects, + * yielding the value of the first effect to succeed or fail. Losers of + * the race will be interrupted immediately. + * + * @since 3.4.0 + * @experimental + * @category sequencing + */ +export const raceAllFirst = >( + all: Iterable +): Micro, Micro.Error, Micro.Context> => + withMicroFiber((parent) => + async((resume) => { + let done = false + const fibers = new Set>() + const onExit = (exit: MicroExit) => { + done = true + resume(fibers.size === 0 ? exit : flatMap(fiberInterruptAll(fibers), () => exit)) + } + + for (const effect of all) { + if (done) break + const fiber = unsafeFork(parent, interruptible(effect), true, true) + fibers.add(fiber) + fiber.addObserver((exit) => { + fibers.delete(fiber) + onExit(exit) + }) + } + + return fiberInterruptAll(fibers) + }) + ) + +/** + * Returns an effect that races two effects, yielding the value of the first + * effect to succeed. Losers of the race will be interrupted immediately. + * + * @since 3.4.0 + * @experimental + * @category sequencing + */ +export const race: { + (that: Micro): (self: Micro) => Micro + (self: Micro, that: Micro): Micro +} = dual( + 2, + (self: Micro, that: Micro): Micro => + raceAll([self, that]) +) + +/** + * Returns an effect that races two effects, yielding the value of the first + * effect to succeed *or* fail. Losers of the race will be interrupted immediately. + * + * @since 3.4.0 + * @experimental + * @category sequencing + */ +export const raceFirst: { + (that: Micro): (self: Micro) => Micro + (self: Micro, that: Micro): Micro +} = dual( + 2, + (self: Micro, that: Micro): Micro => + raceAllFirst([self, that]) +) + +/** + * Map the success value of this `Micro` effect to another `Micro` effect, then + * flatten the result. + * + * @since 3.4.0 + * @experimental + * @category mapping & sequencing + */ +export const flatMap: { + ( + f: (a: A) => Micro + ): (self: Micro) => Micro + ( + self: Micro, + f: (a: A) => Micro + ): Micro +} = dual( + 2, + ( + self: Micro, + f: (a: A) => Micro + ): Micro => { + const onSuccess = Object.create(OnSuccessProto) + onSuccess[args] = self + onSuccess[successCont] = f + return onSuccess + } +) +const OnSuccessProto = makePrimitiveProto({ + op: "OnSuccess", + eval(this: any, fiber: MicroFiberImpl): Primitive { + fiber._stack.push(this) + return this[args] + } +}) + +// ---------------------------------------------------------------------------- +// mapping & sequencing +// ---------------------------------------------------------------------------- + +/** + * Flattens any nested `Micro` effects, merging the error and requirement types. + * + * @since 3.4.0 + * @experimental + * @category mapping & sequencing + */ +export const flatten = ( + self: Micro, E2, R2> +): Micro => flatMap(self, identity) + +/** + * Transforms the success value of the `Micro` effect with the specified + * function. + * + * @since 3.4.0 + * @experimental + * @category mapping & sequencing + */ +export const map: { + (f: (a: A) => B): (self: Micro) => Micro + (self: Micro, f: (a: A) => B): Micro +} = dual( + 2, + (self: Micro, f: (a: A) => B): Micro => flatMap(self, (a) => succeed(f(a))) +) + +// ---------------------------------------------------------------------------- +// MicroExit +// ---------------------------------------------------------------------------- + +/** + * The `MicroExit` type is used to represent the result of a `Micro` computation. It + * can either be successful, containing a value of type `A`, or it can fail, + * containing an error of type `E` wrapped in a `MicroCause`. + * + * @since 3.4.6 + * @experimental + * @category MicroExit + */ +export type MicroExit = + | MicroExit.Success + | MicroExit.Failure + +/** + * @since 3.4.6 + * @experimental + * @category MicroExit + */ +export declare namespace MicroExit { + /** + * @since 3.4.6 + * @experimental + * @category MicroExit + */ + export interface Proto extends Micro { + readonly [MicroExitTypeId]: MicroExitTypeId + } + + /** + * @since 3.4.6 + * @experimental + * @category MicroExit + */ + export interface Success extends Proto { + readonly _tag: "Success" + readonly value: A + } + + /** + * @since 3.4.6 + * @experimental + * @category MicroExit + */ + export interface Failure extends Proto { + readonly _tag: "Failure" + readonly cause: MicroCause + } +} + +/** + * @since 3.4.6 + * @experimental + * @category MicroExit + */ +export const isMicroExit = (u: unknown): u is MicroExit => hasProperty(u, MicroExitTypeId) + +/** + * @since 3.4.6 + * @experimental + * @category MicroExit + */ +export const exitSucceed: (a: A) => MicroExit = succeed as any + +/** + * @since 3.4.6 + * @experimental + * @category MicroExit + */ +export const exitFailCause: (cause: MicroCause) => MicroExit = failCause as any + +/** + * @since 3.4.6 + * @experimental + * @category MicroExit + */ +export const exitInterrupt: MicroExit = exitFailCause(causeInterrupt()) + +/** + * @since 3.4.6 + * @experimental + * @category MicroExit + */ +export const exitFail = (e: E): MicroExit => exitFailCause(causeFail(e)) + +/** + * @since 3.4.6 + * @experimental + * @category MicroExit + */ +export const exitDie = (defect: unknown): MicroExit => exitFailCause(causeDie(defect)) + +/** + * @since 3.4.6 + * @experimental + * @category MicroExit + */ +export const exitIsSuccess = ( + self: MicroExit +): self is MicroExit.Success => self._tag === "Success" + +/** + * @since 3.4.6 + * @experimental + * @category MicroExit + */ +export const exitIsFailure = ( + self: MicroExit +): self is MicroExit.Failure => self._tag === "Failure" + +/** + * @since 3.4.6 + * @experimental + * @category MicroExit + */ +export const exitIsInterrupt = ( + self: MicroExit +): self is MicroExit.Failure & { + readonly cause: MicroCause.Interrupt +} => exitIsFailure(self) && self.cause._tag === "Interrupt" + +/** + * @since 3.4.6 + * @experimental + * @category MicroExit + */ +export const exitIsFail = ( + self: MicroExit +): self is MicroExit.Failure & { + readonly cause: MicroCause.Fail +} => exitIsFailure(self) && self.cause._tag === "Fail" + +/** + * @since 3.4.6 + * @experimental + * @category MicroExit + */ +export const exitIsDie = ( + self: MicroExit +): self is MicroExit.Failure & { + readonly cause: MicroCause.Die +} => exitIsFailure(self) && self.cause._tag === "Die" + +/** + * @since 3.4.6 + * @experimental + * @category MicroExit + */ +export const exitVoid: MicroExit = exitSucceed(void 0) + +/** + * @since 3.11.0 + * @experimental + * @category MicroExit + */ +export const exitVoidAll = >>( + exits: I +): MicroExit> ? _E : never> => { + for (const exit of exits) { + if (exit._tag === "Failure") { + return exit + } + } + return exitVoid +} + +// ---------------------------------------------------------------------------- +// scheduler +// ---------------------------------------------------------------------------- + +/** + * @since 3.5.9 + * @experimental + * @category scheduler + */ +export interface MicroScheduler { + readonly scheduleTask: (task: () => void, priority: number) => void + readonly shouldYield: (fiber: MicroFiber) => boolean + readonly flush: () => void +} + +const setImmediate = "setImmediate" in globalThis + ? globalThis.setImmediate + : (f: () => void) => setTimeout(f, 0) + +/** + * @since 3.5.9 + * @experimental + * @category scheduler + */ +export class MicroSchedulerDefault implements MicroScheduler { + private tasks: Array<() => void> = [] + private running = false + + /** + * @since 3.5.9 + */ + scheduleTask(task: () => void, _priority: number) { + this.tasks.push(task) + if (!this.running) { + this.running = true + setImmediate(this.afterScheduled) + } + } + + /** + * @since 3.5.9 + */ + afterScheduled = () => { + this.running = false + this.runTasks() + } + + /** + * @since 3.5.9 + */ + runTasks() { + const tasks = this.tasks + this.tasks = [] + for (let i = 0, len = tasks.length; i < len; i++) { + tasks[i]() + } + } + + /** + * @since 3.5.9 + */ + shouldYield(fiber: MicroFiber) { + return fiber.currentOpCount >= fiber.getRef(MaxOpsBeforeYield) + } + + /** + * @since 3.5.9 + */ + flush() { + while (this.tasks.length > 0) { + this.runTasks() + } + } +} + +/** + * Access the given `Context.Tag` from the environment. + * + * @since 3.4.0 + * @experimental + * @category environment + */ +export const service: { + (tag: Context.Reference): Micro + (tag: Context.Tag): Micro +} = + ((tag: Context.Tag): Micro => + withMicroFiber((fiber) => succeed(Context.unsafeGet(fiber.context, tag)))) as any + +/** + * Access the given `Context.Tag` from the environment, without tracking the + * dependency at the type level. + * + * It will return an `Option` of the service, depending on whether it is + * available in the environment or not. + * + * @since 3.4.0 + * @experimental + * @category environment + */ +export const serviceOption = ( + tag: Context.Tag +): Micro> => withMicroFiber((fiber) => succeed(Context.getOption(fiber.context, tag))) + +/** + * Update the Context with the given mapping function. + * + * @since 3.11.0 + * @experimental + * @category environment + */ +export const updateContext: { + ( + f: (context: Context.Context) => Context.Context> + ): (self: Micro) => Micro + (self: Micro, f: (context: Context.Context) => Context.Context>): Micro +} = dual( + 2, + ( + self: Micro, + f: (context: Context.Context) => Context.Context> + ): Micro => + withMicroFiber((fiber) => { + const prev = fiber.context as Context.Context + fiber.context = f(prev) + return onExit( + self as any, + () => { + fiber.context = prev + return void_ + } + ) + }) +) + +/** + * Update the service for the given `Context.Tag` in the environment. + * + * @since 3.11.0 + * @experimental + * @category environment + */ +export const updateService: { + ( + tag: Context.Reference, + f: (value: A) => A + ): (self: Micro) => Micro + ( + tag: Context.Tag, + f: (value: A) => A + ): (self: Micro) => Micro + ( + self: Micro, + tag: Context.Reference, + f: (value: A) => A + ): Micro + ( + self: Micro, + tag: Context.Tag, + f: (value: A) => A + ): Micro +} = dual( + 3, + ( + self: Micro, + tag: Context.Reference, + f: (value: A) => A + ): Micro => + withMicroFiber((fiber) => { + const prev = Context.unsafeGet(fiber.context, tag) + fiber.context = Context.add(fiber.context, tag, f(prev)) + return onExit( + self, + () => { + fiber.context = Context.add(fiber.context, tag, prev) + return void_ + } + ) + }) +) + +/** + * Access the current `Context` from the environment. + * + * @since 3.4.0 + * @experimental + * @category environment + */ +export const context = (): Micro> => getContext as any +const getContext = withMicroFiber((fiber) => succeed(fiber.context)) + +/** + * Merge the given `Context` with the current context. + * + * @since 3.4.0 + * @experimental + * @category environment + */ +export const provideContext: { + ( + context: Context.Context + ): (self: Micro) => Micro> + ( + self: Micro, + context: Context.Context + ): Micro> +} = dual( + 2, + ( + self: Micro, + provided: Context.Context + ): Micro> => updateContext(self, Context.merge(provided)) as any +) + +/** + * Add the provided service to the current context. + * + * @since 3.4.0 + * @experimental + * @category environment + */ +export const provideService: { + ( + tag: Context.Tag, + service: S + ): (self: Micro) => Micro> + ( + self: Micro, + tag: Context.Tag, + service: S + ): Micro> +} = dual( + 3, + ( + self: Micro, + tag: Context.Tag, + service: S + ): Micro> => updateContext(self, Context.add(tag, service)) as any +) + +/** + * Create a service using the provided `Micro` effect, and add it to the + * current context. + * + * @since 3.4.6 + * @experimental + * @category environment + */ +export const provideServiceEffect: { + ( + tag: Context.Tag, + acquire: Micro + ): (self: Micro) => Micro | R2> + ( + self: Micro, + tag: Context.Tag, + acquire: Micro + ): Micro | R2> +} = dual( + 3, + ( + self: Micro, + tag: Context.Tag, + acquire: Micro + ): Micro | R2> => flatMap(acquire, (service) => provideService(self, tag, service)) +) + +// ======================================================================== +// References +// ======================================================================== + +/** + * @since 3.11.0 + * @experimental + * @category references + */ +export class MaxOpsBeforeYield extends Context.Reference()< + "effect/Micro/currentMaxOpsBeforeYield", + number +>( + "effect/Micro/currentMaxOpsBeforeYield", + { defaultValue: () => 2048 } +) {} + +/** + * @since 3.11.0 + * @experimental + * @category environment refs + */ +export class CurrentConcurrency extends Context.Reference()< + "effect/Micro/currentConcurrency", + "unbounded" | number +>( + "effect/Micro/currentConcurrency", + { defaultValue: () => "unbounded" } +) {} + +/** + * @since 3.11.0 + * @experimental + * @category environment refs + */ +export class CurrentScheduler extends Context.Reference()< + "effect/Micro/currentScheduler", + MicroScheduler +>( + "effect/Micro/currentScheduler", + { defaultValue: () => new MicroSchedulerDefault() } +) {} + +/** + * If you have a `Micro` that uses `concurrency: "inherit"`, you can use this + * api to control the concurrency of that `Micro` when it is run. + * + * @example + * ```ts + * import * as Micro from "effect/Micro" + * + * Micro.forEach([1, 2, 3], (n) => Micro.succeed(n), { + * concurrency: "inherit" + * }).pipe( + * Micro.withConcurrency(2) // use a concurrency of 2 + * ) + * ``` + * + * @since 3.4.0 + * @experimental + * @category environment refs + */ +export const withConcurrency: { + ( + concurrency: "unbounded" | number + ): (self: Micro) => Micro + ( + self: Micro, + concurrency: "unbounded" | number + ): Micro +} = dual( + 2, + ( + self: Micro, + concurrency: "unbounded" | number + ): Micro => provideService(self, CurrentConcurrency, concurrency) +) + +// ---------------------------------------------------------------------------- +// zipping +// ---------------------------------------------------------------------------- + +/** + * Combine two `Micro` effects into a single effect that produces a tuple of + * their results. + * + * @since 3.4.0 + * @experimental + * @category zipping + */ +export const zip: { + ( + that: Micro, + options?: + | { readonly concurrent?: boolean | undefined } + | undefined + ): (self: Micro) => Micro<[A, A2], E2 | E, R2 | R> + ( + self: Micro, + that: Micro, + options?: { readonly concurrent?: boolean | undefined } + ): Micro<[A, A2], E | E2, R | R2> +} = dual((args) => isMicro(args[1]), ( + self: Micro, + that: Micro, + options?: { readonly concurrent?: boolean | undefined } +): Micro<[A, A2], E | E2, R | R2> => zipWith(self, that, (a, a2) => [a, a2], options)) + +/** + * The `Micro.zipWith` function combines two `Micro` effects and allows you to + * apply a function to the results of the combined effects, transforming them + * into a single value. + * + * @since 3.4.3 + * @experimental + * @category zipping + */ +export const zipWith: { + ( + that: Micro, + f: (a: A, b: A2) => B, + options?: { readonly concurrent?: boolean | undefined } + ): (self: Micro) => Micro + ( + self: Micro, + that: Micro, + f: (a: A, b: A2) => B, + options?: { readonly concurrent?: boolean | undefined } + ): Micro +} = dual((args) => isMicro(args[1]), ( + self: Micro, + that: Micro, + f: (a: A, b: A2) => B, + options?: { readonly concurrent?: boolean | undefined } +): Micro => + options?.concurrent + // Use `all` exclusively for concurrent cases, as it introduces additional overhead due to the management of concurrency + ? map(all([self, that], { concurrency: 2 }), ([a, a2]) => f(a, a2)) + : flatMap(self, (a) => map(that, (a2) => f(a, a2)))) + +// ---------------------------------------------------------------------------- +// filtering & conditionals +// ---------------------------------------------------------------------------- + +/** + * Filter the specified effect with the provided function, failing with specified + * `MicroCause` if the predicate fails. + * + * In addition to the filtering capabilities discussed earlier, you have the option to further + * refine and narrow down the type of the success channel by providing a + * + * @since 3.4.0 + * @experimental + * @category filtering & conditionals + */ +export const filterOrFailCause: { + ( + refinement: Refinement, + orFailWith: (a: NoInfer) => MicroCause + ): (self: Micro) => Micro + ( + predicate: Predicate>, + orFailWith: (a: NoInfer) => MicroCause + ): (self: Micro) => Micro + ( + self: Micro, + refinement: Refinement, + orFailWith: (a: A) => MicroCause + ): Micro + ( + self: Micro, + predicate: Predicate, + orFailWith: (a: A) => MicroCause + ): Micro +} = dual((args) => isMicro(args[0]), ( + self: Micro, + refinement: Refinement, + orFailWith: (a: A) => MicroCause +): Micro => flatMap(self, (a) => refinement(a) ? succeed(a) : failCause(orFailWith(a)))) + +/** + * Filter the specified effect with the provided function, failing with specified + * error if the predicate fails. + * + * In addition to the filtering capabilities discussed earlier, you have the option to further + * refine and narrow down the type of the success channel by providing a + * + * @since 3.4.0 + * @experimental + * @category filtering & conditionals + */ +export const filterOrFail: { + ( + refinement: Refinement, + orFailWith: (a: NoInfer) => E2 + ): (self: Micro) => Micro + ( + predicate: Predicate>, + orFailWith: (a: NoInfer) => E2 + ): (self: Micro) => Micro + ( + self: Micro, + refinement: Refinement, + orFailWith: (a: A) => E2 + ): Micro + (self: Micro, predicate: Predicate, orFailWith: (a: A) => E2): Micro +} = dual((args) => isMicro(args[0]), ( + self: Micro, + refinement: Refinement, + orFailWith: (a: A) => E2 +): Micro => flatMap(self, (a) => refinement(a) ? succeed(a) : fail(orFailWith(a)))) + +/** + * The moral equivalent of `if (p) exp`. + * + * @since 3.4.0 + * @experimental + * @category filtering & conditionals + */ +export const when: { + ( + condition: LazyArg | Micro + ): (self: Micro) => Micro, E | E2, R | R2> + ( + self: Micro, + condition: LazyArg | Micro + ): Micro, E | E2, R | R2> +} = dual( + 2, + ( + self: Micro, + condition: LazyArg | Micro + ): Micro, E | E2, R | R2> => + flatMap(isMicro(condition) ? condition : sync(condition), (pass) => pass ? asSome(self) : succeedNone) +) + +// ---------------------------------------------------------------------------- +// repetition +// ---------------------------------------------------------------------------- + +/** + * Repeat the given `Micro` using the provided options. + * + * The `while` predicate will be checked after each iteration, and can use the + * fall `MicroExit` of the effect to determine if the repetition should continue. + * + * @since 3.4.6 + * @experimental + * @category repetition + */ +export const repeatExit: { + (options: { + while: Predicate> + times?: number | undefined + schedule?: MicroSchedule | undefined + }): (self: Micro) => Micro + (self: Micro, options: { + while: Predicate> + times?: number | undefined + schedule?: MicroSchedule | undefined + }): Micro +} = dual(2, (self: Micro, options: { + while: Predicate> + times?: number | undefined + schedule?: MicroSchedule | undefined +}): Micro => + suspend(() => { + const startedAt = options.schedule ? Date.now() : 0 + let attempt = 0 + + const loop: Micro = flatMap(exit(self), (exit) => { + if (options.while !== undefined && !options.while(exit)) { + return exit + } else if (options.times !== undefined && attempt >= options.times) { + return exit + } + attempt++ + let delayEffect = yieldNow + if (options.schedule !== undefined) { + const elapsed = Date.now() - startedAt + const duration = options.schedule(attempt, elapsed) + if (Option.isNone(duration)) { + return exit + } + delayEffect = sleep(duration.value) + } + return flatMap(delayEffect, () => loop) + }) + + return loop + })) + +/** + * Repeat the given `Micro` effect using the provided options. Only successful + * results will be repeated. + * + * @since 3.4.0 + * @experimental + * @category repetition + */ +export const repeat: { + ( + options?: { + while?: Predicate | undefined + times?: number | undefined + schedule?: MicroSchedule | undefined + } | undefined + ): (self: Micro) => Micro + ( + self: Micro, + options?: { + while?: Predicate | undefined + times?: number | undefined + schedule?: MicroSchedule | undefined + } | undefined + ): Micro +} = dual((args) => isMicro(args[0]), ( + self: Micro, + options?: { + while?: Predicate | undefined + times?: number | undefined + schedule?: MicroSchedule | undefined + } | undefined +): Micro => + repeatExit(self, { + ...options, + while: (exit) => exit._tag === "Success" && (options?.while === undefined || options.while(exit.value)) + })) + +/** + * Replicates the given effect `n` times. + * + * @since 3.11.0 + * @experimental + * @category repetition + */ +export const replicate: { + (n: number): (self: Micro) => Array> + (self: Micro, n: number): Array> +} = dual( + 2, + (self: Micro, n: number): Array> => Array.from({ length: n }, () => self) +) + +/** + * Performs this effect the specified number of times and collects the + * results. + * + * @since 3.11.0 + * @category repetition + */ +export const replicateEffect: { + ( + n: number, + options?: { + readonly concurrency?: Concurrency | undefined + readonly discard?: false | undefined + } + ): (self: Micro) => Micro, E, R> + ( + n: number, + options: { + readonly concurrency?: Concurrency | undefined + readonly discard: true + } + ): (self: Micro) => Micro + ( + self: Micro, + n: number, + options?: { + readonly concurrency?: Concurrency | undefined + readonly discard?: false | undefined + } + ): Micro, E, R> + ( + self: Micro, + n: number, + options: { + readonly concurrency?: Concurrency | undefined + readonly discard: true + } + ): Micro +} = dual( + (args) => isMicro(args[0]), + ( + self: Micro, + n: number, + options: { + readonly concurrency?: Concurrency | undefined + readonly discard: true + } + ): Micro => all(replicate(self, n), options) +) + +/** + * Repeat the given `Micro` effect forever, only stopping if the effect fails. + * + * @since 3.4.0 + * @experimental + * @category repetition + */ +export const forever = (self: Micro): Micro => repeat(self) as any + +// ---------------------------------------------------------------------------- +// scheduling +// ---------------------------------------------------------------------------- + +/** + * The `MicroSchedule` type represents a function that can be used to calculate + * the delay between repeats. + * + * The function takes the current attempt number and the elapsed time since the + * first attempt, and returns the delay for the next attempt. If the function + * returns `None`, the repetition will stop. + * + * @since 3.4.6 + * @experimental + * @category scheduling + */ +export type MicroSchedule = (attempt: number, elapsed: number) => Option.Option + +/** + * Create a `MicroSchedule` that will stop repeating after the specified number + * of attempts. + * + * @since 3.4.6 + * @experimental + * @category scheduling + */ +export const scheduleRecurs = (n: number): MicroSchedule => (attempt) => attempt <= n ? Option.some(0) : Option.none() + +/** + * Create a `MicroSchedule` that will generate a constant delay. + * + * @since 3.4.6 + * @experimental + * @category scheduling + */ +export const scheduleSpaced = (millis: number): MicroSchedule => () => Option.some(millis) + +/** + * Create a `MicroSchedule` that will generate a delay with an exponential backoff. + * + * @since 3.4.6 + * @experimental + * @category scheduling + */ +export const scheduleExponential = (baseMillis: number, factor = 2): MicroSchedule => (attempt) => + Option.some(Math.pow(factor, attempt) * baseMillis) + +/** + * Returns a new `MicroSchedule` with an added calculated delay to each delay + * returned by this schedule. + * + * @since 3.4.6 + * @experimental + * @category scheduling + */ +export const scheduleAddDelay: { + (f: () => number): (self: MicroSchedule) => MicroSchedule + (self: MicroSchedule, f: () => number): MicroSchedule +} = dual( + 2, + (self: MicroSchedule, f: () => number): MicroSchedule => (attempt, elapsed) => + Option.map(self(attempt, elapsed), (duration) => duration + f()) +) + +/** + * Transform a `MicroSchedule` to one that will have a delay that will never exceed + * the specified maximum. + * + * @since 3.4.6 + * @experimental + * @category scheduling + */ +export const scheduleWithMaxDelay: { + (max: number): (self: MicroSchedule) => MicroSchedule + (self: MicroSchedule, max: number): MicroSchedule +} = dual( + 2, + (self: MicroSchedule, max: number): MicroSchedule => (attempt, elapsed) => + Option.map(self(attempt, elapsed), (duration) => Math.min(duration, max)) +) + +/** + * Transform a `MicroSchedule` to one that will stop repeating after the specified + * amount of time. + * + * @since 3.4.6 + * @experimental + * @category scheduling + */ +export const scheduleWithMaxElapsed: { + (max: number): (self: MicroSchedule) => MicroSchedule + (self: MicroSchedule, max: number): MicroSchedule +} = dual( + 2, + (self: MicroSchedule, max: number): MicroSchedule => (attempt, elapsed) => + elapsed < max ? self(attempt, elapsed) : Option.none() +) + +/** + * Combines two `MicroSchedule`s, by recurring if either schedule wants to + * recur, using the minimum of the two durations between recurrences. + * + * @since 3.4.6 + * @experimental + * @category scheduling + */ +export const scheduleUnion: { + (that: MicroSchedule): (self: MicroSchedule) => MicroSchedule + (self: MicroSchedule, that: MicroSchedule): MicroSchedule +} = dual( + 2, + (self: MicroSchedule, that: MicroSchedule): MicroSchedule => (attempt, elapsed) => + Option.zipWith(self(attempt, elapsed), that(attempt, elapsed), (d1, d2) => Math.min(d1, d2)) +) + +/** + * Combines two `MicroSchedule`s, by recurring only if both schedules want to + * recur, using the maximum of the two durations between recurrences. + * + * @since 3.4.6 + * @experimental + * @category scheduling + */ +export const scheduleIntersect: { + (that: MicroSchedule): (self: MicroSchedule) => MicroSchedule + (self: MicroSchedule, that: MicroSchedule): MicroSchedule +} = dual( + 2, + (self: MicroSchedule, that: MicroSchedule): MicroSchedule => (attempt, elapsed) => + Option.zipWith(self(attempt, elapsed), that(attempt, elapsed), (d1, d2) => Math.max(d1, d2)) +) + +// ---------------------------------------------------------------------------- +// error handling +// ---------------------------------------------------------------------------- + +/** + * Catch the full `MicroCause` object of the given `Micro` effect, allowing you to + * recover from any kind of cause. + * + * @since 3.4.6 + * @experimental + * @category error handling + */ +export const catchAllCause: { + ( + f: (cause: NoInfer>) => Micro + ): (self: Micro) => Micro + ( + self: Micro, + f: (cause: NoInfer>) => Micro + ): Micro +} = dual( + 2, + ( + self: Micro, + f: (cause: NoInfer>) => Micro + ): Micro => { + const onFailure = Object.create(OnFailureProto) + onFailure[args] = self + onFailure[failureCont] = f + return onFailure + } +) +const OnFailureProto = makePrimitiveProto({ + op: "OnFailure", + eval(this: any, fiber: MicroFiberImpl): Primitive { + fiber._stack.push(this as any) + return this[args] + } +}) + +/** + * Selectively catch a `MicroCause` object of the given `Micro` effect, + * using the provided predicate to determine if the failure should be caught. + * + * @since 3.4.6 + * @experimental + * @category error handling + */ +export const catchCauseIf: { + >( + refinement: Refinement, EB>, + f: (cause: EB) => Micro + ): ( + self: Micro + ) => Micro> | E2, R | R2> + ( + predicate: Predicate>>, + f: (cause: NoInfer>) => Micro + ): (self: Micro) => Micro + >( + self: Micro, + refinement: Refinement, EB>, + f: (cause: EB) => Micro + ): Micro> | E2, R | R2> + ( + self: Micro, + predicate: Predicate>>, + f: (cause: NoInfer>) => Micro + ): Micro +} = dual( + 3, + ( + self: Micro, + predicate: Predicate>, + f: (cause: MicroCause) => Micro + ): Micro => + catchAllCause(self, (cause) => predicate(cause) ? f(cause) : failCause(cause) as any) +) + +/** + * Catch the error of the given `Micro` effect, allowing you to recover from it. + * + * It only catches expected errors. + * + * @since 3.4.6 + * @experimental + * @category error handling + */ +export const catchAll: { + ( + f: (e: NoInfer) => Micro + ): (self: Micro) => Micro + (self: Micro, f: (e: NoInfer) => Micro): Micro +} = dual( + 2, + ( + self: Micro, + f: (a: NoInfer) => Micro + ): Micro => catchCauseIf(self, causeIsFail, (cause) => f(cause.error)) +) + +/** + * Catch any unexpected errors of the given `Micro` effect, allowing you to recover from them. + * + * @since 3.4.6 + * @experimental + * @category error handling + */ +export const catchAllDefect: { + ( + f: (defect: unknown) => Micro + ): (self: Micro) => Micro + (self: Micro, f: (defect: unknown) => Micro): Micro +} = dual( + 2, + (self: Micro, f: (defect: unknown) => Micro): Micro => + catchCauseIf(self, causeIsDie, (die) => f(die.defect)) +) + +/** + * Perform a side effect using the full `MicroCause` object of the given `Micro`. + * + * @since 3.4.6 + * @experimental + * @category error handling + */ +export const tapErrorCause: { + ( + f: (cause: NoInfer>) => Micro + ): (self: Micro) => Micro + ( + self: Micro, + f: (cause: NoInfer>) => Micro + ): Micro +} = dual( + 2, + ( + self: Micro, + f: (cause: NoInfer>) => Micro + ): Micro => tapErrorCauseIf(self, constTrue, f) +) + +/** + * Perform a side effect using if a `MicroCause` object matches the specified + * predicate. + * + * @since 3.4.0 + * @experimental + * @category error handling + */ +export const tapErrorCauseIf: { + >( + refinement: Refinement, EB>, + f: (a: EB) => Micro + ): (self: Micro) => Micro + ( + predicate: (cause: NoInfer>) => boolean, + f: (a: NoInfer>) => Micro + ): (self: Micro) => Micro + >( + self: Micro, + refinement: Refinement, EB>, + f: (a: EB) => Micro + ): Micro + ( + self: Micro, + predicate: (cause: NoInfer>) => boolean, + f: (a: NoInfer>) => Micro + ): Micro +} = dual( + 3, + >( + self: Micro, + refinement: Refinement, EB>, + f: (a: EB) => Micro + ): Micro => catchCauseIf(self, refinement, (cause) => andThen(f(cause), failCause(cause))) +) + +/** + * Perform a side effect from expected errors of the given `Micro`. + * + * @since 3.4.6 + * @experimental + * @category error handling + */ +export const tapError: { + ( + f: (e: NoInfer) => Micro + ): (self: Micro) => Micro + (self: Micro, f: (e: NoInfer) => Micro): Micro +} = dual( + 2, + (self: Micro, f: (e: NoInfer) => Micro): Micro => + tapErrorCauseIf(self, causeIsFail, (fail) => f(fail.error)) +) + +/** + * Perform a side effect from unexpected errors of the given `Micro`. + * + * @since 3.4.6 + * @experimental + * @category error handling + */ +export const tapDefect: { + ( + f: (defect: unknown) => Micro + ): (self: Micro) => Micro + (self: Micro, f: (defect: unknown) => Micro): Micro +} = dual( + 2, + (self: Micro, f: (defect: unknown) => Micro): Micro => + tapErrorCauseIf(self, causeIsDie, (die) => f(die.defect)) +) + +/** + * Catch any expected errors that match the specified predicate. + * + * @since 3.4.0 + * @experimental + * @category error handling + */ +export const catchIf: { + ( + refinement: Refinement, EB>, + f: (e: EB) => Micro + ): (self: Micro) => Micro, R2 | R> + ( + predicate: Predicate>, + f: (e: NoInfer) => Micro + ): (self: Micro) => Micro + ( + self: Micro, + refinement: Refinement, + f: (e: EB) => Micro + ): Micro, R | R2> + ( + self: Micro, + predicate: Predicate, + f: (e: E) => Micro + ): Micro +} = dual( + 3, + ( + self: Micro, + predicate: Predicate, + f: (e: E) => Micro + ): Micro => + catchCauseIf( + self, + (f): f is MicroCause.Fail => causeIsFail(f) && predicate(f.error), + (fail) => f(fail.error) + ) +) + +/** + * Recovers from the specified tagged error. + * + * @since 3.4.0 + * @experimental + * @category error handling + */ +export const catchTag: { + ( + k: K, + f: (e: Extract) => Micro + ): (self: Micro) => Micro, R1 | R> + ( + self: Micro, + k: K, + f: (e: Extract) => Micro + ): Micro, R | R1> +} = dual(3, ( + self: Micro, + k: K, + f: (e: Extract) => Micro +): Micro, R | R1> => + catchIf(self, isTagged(k) as Refinement>, f) as any) + +/** + * Transform the full `MicroCause` object of the given `Micro` effect. + * + * @since 3.4.6 + * @experimental + * @category error handling + */ +export const mapErrorCause: { + (f: (e: MicroCause) => MicroCause): (self: Micro) => Micro + (self: Micro, f: (e: MicroCause) => MicroCause): Micro +} = dual( + 2, + (self: Micro, f: (e: MicroCause) => MicroCause): Micro => + catchAllCause(self, (cause) => failCause(f(cause))) +) + +/** + * Transform any expected errors of the given `Micro` effect. + * + * @since 3.4.0 + * @experimental + * @category error handling + */ +export const mapError: { + (f: (e: E) => E2): (self: Micro) => Micro + (self: Micro, f: (e: E) => E2): Micro +} = dual( + 2, + (self: Micro, f: (e: E) => E2): Micro => catchAll(self, (error) => fail(f(error))) +) + +/** + * Elevate any expected errors of the given `Micro` effect to unexpected errors, + * resulting in an error type of `never`. + * + * @since 3.4.0 + * @experimental + * @category error handling + */ +export const orDie = (self: Micro): Micro => catchAll(self, die) + +/** + * Recover from all errors by succeeding with the given value. + * + * @since 3.4.0 + * @experimental + * @category error handling + */ +export const orElseSucceed: { + (f: LazyArg): (self: Micro) => Micro + (self: Micro, f: LazyArg): Micro +} = dual( + 2, + (self: Micro, f: LazyArg): Micro => catchAll(self, (_) => sync(f)) +) + +/** + * Ignore any expected errors of the given `Micro` effect, returning `void`. + * + * @since 3.4.0 + * @experimental + * @category error handling + */ +export const ignore = (self: Micro): Micro => + matchEffect(self, { onFailure: (_) => void_, onSuccess: (_) => void_ }) + +/** + * Ignore any expected errors of the given `Micro` effect, returning `void`. + * + * @since 3.4.0 + * @experimental + * @category error handling + */ +export const ignoreLogged = (self: Micro): Micro => + matchEffect(self, { + // eslint-disable-next-line no-console + onFailure: (error) => sync(() => console.error(error)), + onSuccess: (_) => void_ + }) + +/** + * Replace the success value of the given `Micro` effect with an `Option`, + * wrapping the success value in `Some` and returning `None` if the effect fails + * with an expected error. + * + * @since 3.4.0 + * @experimental + * @category error handling + */ +export const option = (self: Micro): Micro, never, R> => + match(self, { onFailure: Option.none, onSuccess: Option.some }) + +/** + * Replace the success value of the given `Micro` effect with an `Either`, + * wrapping the success value in `Right` and wrapping any expected errors with + * a `Left`. + * + * @since 3.4.0 + * @experimental + * @category error handling + */ +export const either = (self: Micro): Micro, never, R> => + match(self, { onFailure: Either.left, onSuccess: Either.right }) + +/** + * Retry the given `Micro` effect using the provided options. + * + * @since 3.4.0 + * @experimental + * @category error handling + */ +export const retry: { + ( + options?: { + while?: Predicate | undefined + times?: number | undefined + schedule?: MicroSchedule | undefined + } | undefined + ): (self: Micro) => Micro + ( + self: Micro, + options?: { + while?: Predicate | undefined + times?: number | undefined + schedule?: MicroSchedule | undefined + } | undefined + ): Micro +} = dual((args) => isMicro(args[0]), ( + self: Micro, + options?: { + while?: Predicate | undefined + times?: number | undefined + schedule?: MicroSchedule | undefined + } | undefined +): Micro => + repeatExit(self, { + ...options, + while: (exit) => + exit._tag === "Failure" && exit.cause._tag === "Fail" && + (options?.while === undefined || options.while(exit.cause.error)) + })) + +/** + * Add a stack trace to any failures that occur in the effect. The trace will be + * added to the `traces` field of the `MicroCause` object. + * + * @since 3.4.0 + * @experimental + * @category error handling + */ +export const withTrace: { + (name: string): (self: Micro) => Micro + (self: Micro, name: string): Micro +} = function() { + const prevLimit = globalThis.Error.stackTraceLimit + globalThis.Error.stackTraceLimit = 2 + const error = new globalThis.Error() + globalThis.Error.stackTraceLimit = prevLimit + function generate(name: string, cause: MicroCause) { + const stack = error.stack + if (!stack) { + return cause + } + const line = stack.split("\n")[2]?.trim().replace(/^at /, "") + if (!line) { + return cause + } + const lineMatch = line.match(/\((.*)\)$/) + return causeWithTrace(cause, `at ${name} (${lineMatch ? lineMatch[1] : line})`) + } + const f = (name: string) => (self: Micro) => onError(self, (cause) => failCause(generate(name, cause))) + if (arguments.length === 2) { + return f(arguments[1])(arguments[0]) + } + return f(arguments[0]) +} as any + +// ---------------------------------------------------------------------------- +// pattern matching +// ---------------------------------------------------------------------------- + +/** + * @since 3.4.6 + * @experimental + * @category pattern matching + */ +export const matchCauseEffect: { + (options: { + readonly onFailure: (cause: MicroCause) => Micro + readonly onSuccess: (a: A) => Micro + }): (self: Micro) => Micro + ( + self: Micro, + options: { + readonly onFailure: (cause: MicroCause) => Micro + readonly onSuccess: (a: A) => Micro + } + ): Micro +} = dual( + 2, + ( + self: Micro, + options: { + readonly onFailure: (cause: MicroCause) => Micro + readonly onSuccess: (a: A) => Micro + } + ): Micro => { + const primitive = Object.create(OnSuccessAndFailureProto) + primitive[args] = self + primitive[successCont] = options.onSuccess + primitive[failureCont] = options.onFailure + return primitive + } +) +const OnSuccessAndFailureProto = makePrimitiveProto({ + op: "OnSuccessAndFailure", + eval(this: any, fiber: MicroFiberImpl): Primitive { + fiber._stack.push(this) + return this[args] + } +}) + +/** + * @since 3.4.6 + * @experimental + * @category pattern matching + */ +export const matchCause: { + ( + options: { + readonly onFailure: (cause: MicroCause) => A2 + readonly onSuccess: (a: A) => A3 + } + ): (self: Micro) => Micro + ( + self: Micro, + options: { + readonly onFailure: (cause: MicroCause) => A2 + readonly onSuccess: (a: A) => A3 + } + ): Micro +} = dual( + 2, + ( + self: Micro, + options: { + readonly onFailure: (cause: MicroCause) => A2 + readonly onSuccess: (a: A) => A3 + } + ): Micro => + matchCauseEffect(self, { + onFailure: (cause) => sync(() => options.onFailure(cause)), + onSuccess: (value) => sync(() => options.onSuccess(value)) + }) +) + +/** + * @since 3.4.6 + * @experimental + * @category pattern matching + */ +export const matchEffect: { + ( + options: { + readonly onFailure: (e: E) => Micro + readonly onSuccess: (a: A) => Micro + } + ): (self: Micro) => Micro + ( + self: Micro, + options: { + readonly onFailure: (e: E) => Micro + readonly onSuccess: (a: A) => Micro + } + ): Micro +} = dual( + 2, + ( + self: Micro, + options: { + readonly onFailure: (e: E) => Micro + readonly onSuccess: (a: A) => Micro + } + ): Micro => + matchCauseEffect(self, { + onFailure: (cause) => cause._tag === "Fail" ? options.onFailure(cause.error) : failCause(cause), + onSuccess: options.onSuccess + }) +) + +/** + * @since 3.4.0 + * @experimental + * @category pattern matching + */ +export const match: { + ( + options: { + readonly onFailure: (error: E) => A2 + readonly onSuccess: (value: A) => A3 + } + ): (self: Micro) => Micro + ( + self: Micro, + options: { + readonly onFailure: (error: E) => A2 + readonly onSuccess: (value: A) => A3 + } + ): Micro +} = dual( + 2, + ( + self: Micro, + options: { + readonly onFailure: (error: E) => A2 + readonly onSuccess: (value: A) => A3 + } + ): Micro => + matchEffect(self, { + onFailure: (error) => sync(() => options.onFailure(error)), + onSuccess: (value) => sync(() => options.onSuccess(value)) + }) +) + +// ---------------------------------------------------------------------------- +// delays & timeouts +// ---------------------------------------------------------------------------- + +/** + * Create a `Micro` effect that will sleep for the specified duration. + * + * @since 3.4.0 + * @experimental + * @category delays & timeouts + */ +export const sleep = (millis: number): Micro => + async((resume) => { + const timeout = setTimeout(() => { + resume(void_) + }, millis) + return sync(() => { + clearTimeout(timeout) + }) + }) + +/** + * Returns an effect that will delay the execution of this effect by the + * specified duration. + * + * @since 3.4.0 + * @experimental + * @category delays & timeouts + */ +export const delay: { + (millis: number): (self: Micro) => Micro + (self: Micro, millis: number): Micro +} = dual( + 2, + (self: Micro, millis: number): Micro => andThen(sleep(millis), self) +) + +/** + * Returns an effect that will timeout this effect, that will execute the + * fallback effect if the timeout elapses before the effect has produced a value. + * + * If the timeout elapses, the running effect will be safely interrupted. + * + * @since 3.4.0 + * @experimental + * @category delays & timeouts + */ +export const timeoutOrElse: { + (options: { + readonly duration: number + readonly onTimeout: LazyArg> + }): (self: Micro) => Micro + (self: Micro, options: { + readonly duration: number + readonly onTimeout: LazyArg> + }): Micro +} = dual( + 2, + (self: Micro, options: { + readonly duration: number + readonly onTimeout: LazyArg> + }): Micro => + raceFirst(self, andThen(interruptible(sleep(options.duration)), options.onTimeout)) +) + +/** + * Returns an effect that will timeout this effect, that will fail with a + * `TimeoutException` if the timeout elapses before the effect has produced a + * value. + * + * If the timeout elapses, the running effect will be safely interrupted. + * + * @since 3.4.0 + * @experimental + * @category delays & timeouts + */ +export const timeout: { + (millis: number): (self: Micro) => Micro + (self: Micro, millis: number): Micro +} = dual( + 2, + (self: Micro, millis: number): Micro => + timeoutOrElse(self, { duration: millis, onTimeout: () => fail(new TimeoutException()) }) +) + +/** + * Returns an effect that will timeout this effect, succeeding with a `None` + * if the timeout elapses before the effect has produced a value; and `Some` of + * the produced value otherwise. + * + * If the timeout elapses, the running effect will be safely interrupted. + * + * @since 3.4.0 + * @experimental + * @category delays & timeouts + */ +export const timeoutOption: { + (millis: number): (self: Micro) => Micro, E, R> + (self: Micro, millis: number): Micro, E, R> +} = dual( + 2, + (self: Micro, millis: number): Micro, E, R> => + raceFirst( + asSome(self), + as(interruptible(sleep(millis)), Option.none()) + ) +) + +// ---------------------------------------------------------------------------- +// resources & finalization +// ---------------------------------------------------------------------------- + +/** + * @since 3.4.0 + * @experimental + * @category resources & finalization + */ +export const MicroScopeTypeId: unique symbol = Symbol.for("effect/Micro/MicroScope") + +/** + * @since 3.4.0 + * @experimental + * @category resources & finalization + */ +export type MicroScopeTypeId = typeof MicroScopeTypeId + +/** + * @since 3.4.0 + * @experimental + * @category resources & finalization + */ +export interface MicroScope { + readonly [MicroScopeTypeId]: MicroScopeTypeId + readonly addFinalizer: (finalizer: (exit: MicroExit) => Micro) => Micro + readonly fork: Micro +} + +/** + * @since 3.4.0 + * @experimental + * @category resources & finalization + */ +export declare namespace MicroScope { + /** + * @since 3.4.0 + * @experimental + * @category resources & finalization + */ + export interface Closeable extends MicroScope { + readonly close: (exit: MicroExit) => Micro + } +} + +/** + * @since 3.4.0 + * @experimental + * @category resources & finalization + */ +export const MicroScope: Context.Tag = Context.GenericTag("effect/Micro/MicroScope") + +class MicroScopeImpl implements MicroScope.Closeable { + readonly [MicroScopeTypeId]: MicroScopeTypeId + state: { + readonly _tag: "Open" + readonly finalizers: Set<(exit: MicroExit) => Micro> + } | { + readonly _tag: "Closed" + readonly exit: MicroExit + } = { _tag: "Open", finalizers: new Set() } + + constructor() { + this[MicroScopeTypeId] = MicroScopeTypeId + } + + unsafeAddFinalizer(finalizer: (exit: MicroExit) => Micro): void { + if (this.state._tag === "Open") { + this.state.finalizers.add(finalizer) + } + } + addFinalizer(finalizer: (exit: MicroExit) => Micro): Micro { + return suspend(() => { + if (this.state._tag === "Open") { + this.state.finalizers.add(finalizer) + return void_ + } + return finalizer(this.state.exit) + }) + } + unsafeRemoveFinalizer(finalizer: (exit: MicroExit) => Micro): void { + if (this.state._tag === "Open") { + this.state.finalizers.delete(finalizer) + } + } + close(microExit: MicroExit): Micro { + return suspend(() => { + if (this.state._tag === "Open") { + const finalizers = Array.from(this.state.finalizers).reverse() + this.state = { _tag: "Closed", exit: microExit } + return flatMap( + forEach(finalizers, (finalizer) => exit(finalizer(microExit))), + exitVoidAll + ) + } + return void_ + }) + } + get fork() { + return sync(() => { + const newScope = new MicroScopeImpl() + if (this.state._tag === "Closed") { + newScope.state = this.state + return newScope + } + function fin(exit: MicroExit) { + return newScope.close(exit) + } + this.state.finalizers.add(fin) + newScope.unsafeAddFinalizer((_) => sync(() => this.unsafeRemoveFinalizer(fin))) + return newScope + }) + } +} + +/** + * @since 3.4.0 + * @experimental + * @category resources & finalization + */ +export const scopeMake: Micro = sync(() => new MicroScopeImpl()) + +/** + * @since 3.4.0 + * @experimental + * @category resources & finalization + */ +export const scopeUnsafeMake = (): MicroScope.Closeable => new MicroScopeImpl() + +/** + * Access the current `MicroScope`. + * + * @since 3.4.0 + * @experimental + * @category resources & finalization + */ +export const scope: Micro = service(MicroScope) + +/** + * Provide a `MicroScope` to an effect. + * + * @since 3.4.0 + * @experimental + * @category resources & finalization + */ +export const provideScope: { + (scope: MicroScope): (self: Micro) => Micro> + (self: Micro, scope: MicroScope): Micro> +} = dual( + 2, + (self: Micro, scope: MicroScope): Micro> => + provideService(self, MicroScope, scope) +) + +/** + * Provide a `MicroScope` to the given effect, closing it after the effect has + * finished executing. + * + * @since 3.4.0 + * @experimental + * @category resources & finalization + */ +export const scoped = (self: Micro): Micro> => + suspend(() => { + const scope = new MicroScopeImpl() + return onExit(provideService(self, MicroScope, scope), (exit) => scope.close(exit)) + }) + +/** + * Create a resource with a cleanup `Micro` effect, ensuring the cleanup is + * executed when the `MicroScope` is closed. + * + * @since 3.4.0 + * @experimental + * @category resources & finalization + */ +export const acquireRelease = ( + acquire: Micro, + release: (a: A, exit: MicroExit) => Micro +): Micro => + uninterruptible(flatMap( + scope, + (scope) => tap(acquire, (a) => scope.addFinalizer((exit) => release(a, exit))) + )) + +/** + * Add a finalizer to the current `MicroScope`. + * + * @since 3.4.0 + * @experimental + * @category resources & finalization + */ +export const addFinalizer = ( + finalizer: (exit: MicroExit) => Micro +): Micro => flatMap(scope, (scope) => scope.addFinalizer(finalizer)) + +/** + * When the `Micro` effect is completed, run the given finalizer effect with the + * `MicroExit` of the executed effect. + * + * @since 3.4.6 + * @experimental + * @category resources & finalization + */ +export const onExit: { + ( + f: (exit: MicroExit) => Micro + ): (self: Micro) => Micro + ( + self: Micro, + f: (exit: MicroExit) => Micro + ): Micro +} = dual( + 2, + ( + self: Micro, + f: (exit: MicroExit) => Micro + ): Micro => + uninterruptibleMask((restore) => + matchCauseEffect(restore(self), { + onFailure: (cause) => flatMap(f(exitFailCause(cause)), () => failCause(cause)), + onSuccess: (a) => flatMap(f(exitSucceed(a)), () => succeed(a)) + }) + ) +) + +/** + * Regardless of the result of the this `Micro` effect, run the finalizer effect. + * + * @since 3.4.0 + * @experimental + * @category resources & finalization + */ +export const ensuring: { + ( + finalizer: Micro + ): (self: Micro) => Micro + ( + self: Micro, + finalizer: Micro + ): Micro +} = dual( + 2, + ( + self: Micro, + finalizer: Micro + ): Micro => onExit(self, (_) => finalizer) +) + +/** + * When the `Micro` effect is completed, run the given finalizer effect if it + * matches the specified predicate. + * + * @since 3.4.6 + * @experimental + * @category resources & finalization + */ +export const onExitIf: { + >( + refinement: Refinement, B>, + f: (exit: B) => Micro + ): (self: Micro) => Micro + ( + predicate: Predicate, NoInfer>>, + f: (exit: MicroExit, NoInfer>) => Micro + ): (self: Micro) => Micro + >( + self: Micro, + refinement: Refinement, B>, + f: (exit: B) => Micro + ): Micro + ( + self: Micro, + predicate: Predicate, NoInfer>>, + f: (exit: MicroExit, NoInfer>) => Micro + ): Micro +} = dual( + 3, + >( + self: Micro, + refinement: Refinement, B>, + f: (exit: B) => Micro + ): Micro => onExit(self, (exit) => (refinement(exit) ? f(exit) : exitVoid)) +) + +/** + * When the `Micro` effect fails, run the given finalizer effect with the + * `MicroCause` of the executed effect. + * + * @since 3.4.6 + * @experimental + * @category resources & finalization + */ +export const onError: { + ( + f: (cause: MicroCause>) => Micro + ): (self: Micro) => Micro + ( + self: Micro, + f: (cause: MicroCause>) => Micro + ): Micro +} = dual( + 2, + ( + self: Micro, + f: (cause: MicroCause>) => Micro + ): Micro => onExitIf(self, exitIsFailure, (exit) => f(exit.cause)) +) + +/** + * If this `Micro` effect is aborted, run the finalizer effect. + * + * @since 3.4.6 + * @experimental + * @category resources & finalization + */ +export const onInterrupt: { + ( + finalizer: Micro + ): (self: Micro) => Micro + (self: Micro, finalizer: Micro): Micro +} = dual( + 2, + (self: Micro, finalizer: Micro): Micro => + onExitIf(self, exitIsInterrupt, (_) => finalizer) +) + +/** + * Acquire a resource, use it, and then release the resource when the `use` + * effect has completed. + * + * @since 3.4.0 + * @experimental + * @category resources & finalization + */ +export const acquireUseRelease = ( + acquire: Micro, + use: (a: Resource) => Micro, + release: (a: Resource, exit: MicroExit) => Micro +): Micro => + uninterruptibleMask((restore) => + flatMap( + acquire, + (a) => + flatMap( + exit(restore(use(a))), + (exit) => andThen(release(a, exit), exit) + ) + ) + ) + +// ---------------------------------------------------------------------------- +// interruption +// ---------------------------------------------------------------------------- + +/** + * Abort the current `Micro` effect. + * + * @since 3.4.6 + * @experimental + * @category interruption + */ +export const interrupt: Micro = failCause(causeInterrupt()) + +/** + * Flag the effect as uninterruptible, which means that when the effect is + * interrupted, it will be allowed to continue running until completion. + * + * @since 3.4.0 + * @experimental + * @category flags + */ +export const uninterruptible = ( + self: Micro +): Micro => + withMicroFiber((fiber) => { + if (!fiber.interruptible) return self + fiber.interruptible = false + fiber._stack.push(setInterruptible(true)) + return self + }) + +const setInterruptible: (interruptible: boolean) => Primitive = makePrimitive({ + op: "SetInterruptible", + ensure(fiber) { + fiber.interruptible = this[args] + if (fiber._interrupted && fiber.interruptible) { + return () => exitInterrupt + } + } +}) + +/** + * Flag the effect as interruptible, which means that when the effect is + * interrupted, it will be interrupted immediately. + * + * @since 3.4.0 + * @experimental + * @category flags + */ +export const interruptible = ( + self: Micro +): Micro => + withMicroFiber((fiber) => { + if (fiber.interruptible) return self + fiber.interruptible = true + fiber._stack.push(setInterruptible(false)) + if (fiber._interrupted) return exitInterrupt + return self + }) + +/** + * Wrap the given `Micro` effect in an uninterruptible region, preventing the + * effect from being aborted. + * + * You can use the `restore` function to restore a `Micro` effect to the + * interruptibility state before the `uninterruptibleMask` was applied. + * + * @example + * ```ts + * import * as Micro from "effect/Micro" + * + * Micro.uninterruptibleMask((restore) => + * Micro.sleep(1000).pipe( // uninterruptible + * Micro.andThen(restore(Micro.sleep(1000))) // interruptible + * ) + * ) + * ``` + * + * @since 3.4.0 + * @experimental + * @category interruption + */ +export const uninterruptibleMask = ( + f: ( + restore: (effect: Micro) => Micro + ) => Micro +): Micro => + withMicroFiber((fiber) => { + if (!fiber.interruptible) return f(identity) + fiber.interruptible = false + fiber._stack.push(setInterruptible(true)) + return f(interruptible) + }) + +// ======================================================================== +// collecting & elements +// ======================================================================== + +/** + * @since 3.4.0 + * @experimental + */ +export declare namespace All { + /** + * @since 3.4.0 + * @experimental + */ + export type MicroAny = Micro + + /** + * @since 3.4.0 + * @experimental + */ + export type ReturnIterable, Discard extends boolean> = [T] extends + [Iterable>] ? Micro< + Discard extends true ? void : Array, + E, + R + > + : never + + /** + * @since 3.4.0 + * @experimental + */ + export type ReturnTuple, Discard extends boolean> = Micro< + Discard extends true ? void + : T[number] extends never ? [] + : { -readonly [K in keyof T]: T[K] extends Micro ? _A : never }, + T[number] extends never ? never + : T[number] extends Micro ? _E + : never, + T[number] extends never ? never + : T[number] extends Micro ? _R + : never + > extends infer X ? X : never + + /** + * @since 3.4.0 + * @experimental + */ + export type ReturnObject = [T] extends [{ [K: string]: MicroAny }] ? Micro< + Discard extends true ? void : + { -readonly [K in keyof T]: [T[K]] extends [Micro] ? _A : never }, + keyof T extends never ? never + : T[keyof T] extends Micro ? _E + : never, + keyof T extends never ? never + : T[keyof T] extends Micro ? _R + : never + > + : never + + /** + * @since 3.4.0 + * @experimental + */ + export type IsDiscard = [Extract] extends [never] ? false : true + + /** + * @since 3.4.0 + * @experimental + */ + export type Return< + Arg extends Iterable | Record, + O extends NoExcessProperties<{ + readonly concurrency?: Concurrency | undefined + readonly discard?: boolean | undefined + }, O> + > = [Arg] extends [ReadonlyArray] ? ReturnTuple> + : [Arg] extends [Iterable] ? ReturnIterable> + : [Arg] extends [Record] ? ReturnObject> + : never +} + +/** + * Runs all the provided effects in sequence respecting the structure provided in input. + * + * Supports multiple arguments, a single argument tuple / array or record / struct. + * + * @since 3.4.0 + * @experimental + * @category collecting & elements + */ +export const all = < + const Arg extends Iterable> | Record>, + O extends NoExcessProperties<{ + readonly concurrency?: Concurrency | undefined + readonly discard?: boolean | undefined + }, O> +>(arg: Arg, options?: O): All.Return => { + if (Array.isArray(arg) || isIterable(arg)) { + return (forEach as any)(arg, identity, options) + } else if (options?.discard) { + return (forEach as any)(Object.values(arg), identity, options) + } + return suspend(() => { + const out: Record = {} + return as( + forEach(Object.entries(arg), ([key, effect]) => + map(effect, (value) => { + out[key] = value + }), { + discard: true, + concurrency: options?.concurrency + }), + out + ) + }) as any +} + +/** + * @since 3.11.0 + * @experimental + * @category collecting & elements + */ +export const whileLoop: (options: { + readonly while: LazyArg + readonly body: LazyArg> + readonly step: (a: A) => void +}) => Micro = makePrimitive({ + op: "While", + contA(value, fiber) { + this[args].step(value) + if (this[args].while()) { + fiber._stack.push(this) + return this[args].body() + } + return exitVoid + }, + eval(fiber) { + if (this[args].while()) { + fiber._stack.push(this) + return this[args].body() + } + return exitVoid + } +}) + +/** + * For each element of the provided iterable, run the effect and collect the + * results. + * + * If the `discard` option is set to `true`, the results will be discarded and + * the effect will return `void`. + * + * The `concurrency` option can be set to control how many effects are run + * concurrently. By default, the effects are run sequentially. + * + * @since 3.4.0 + * @experimental + * @category collecting & elements + */ +export const forEach: { + (iterable: Iterable, f: (a: A, index: number) => Micro, options?: { + readonly concurrency?: Concurrency | undefined + readonly discard?: false | undefined + }): Micro, E, R> + (iterable: Iterable, f: (a: A, index: number) => Micro, options: { + readonly concurrency?: Concurrency | undefined + readonly discard: true + }): Micro +} = < + A, + B, + E, + R +>(iterable: Iterable, f: (a: A, index: number) => Micro, options?: { + readonly concurrency?: Concurrency | undefined + readonly discard?: boolean | undefined +}): Micro => + withMicroFiber((parent) => { + const concurrencyOption = options?.concurrency === "inherit" + ? parent.getRef(CurrentConcurrency) + : options?.concurrency ?? 1 + const concurrency = concurrencyOption === "unbounded" + ? Number.POSITIVE_INFINITY + : Math.max(1, concurrencyOption) + + const items = Arr.fromIterable(iterable) + let length = items.length + if (length === 0) { + return options?.discard ? void_ : succeed([]) + } + + const out: Array | undefined = options?.discard ? undefined : new Array(length) + let index = 0 + + if (concurrency === 1) { + return as( + whileLoop({ + while: () => index < items.length, + body: () => f(items[index], index), + step: out ? + (b) => out[index++] = b : + (_) => index++ + }), + out as any + ) + } + return async((resume) => { + const fibers = new Set>() + let result: MicroExit | undefined = undefined + let inProgress = 0 + let doneCount = 0 + let pumping = false + let interrupted = false + function pump() { + pumping = true + while (inProgress < concurrency && index < length) { + const currentIndex = index + const item = items[currentIndex] + index++ + inProgress++ + try { + const child = unsafeFork(parent, f(item, currentIndex), true, true) + fibers.add(child) + child.addObserver((exit) => { + fibers.delete(child) + if (interrupted) { + return + } else if (exit._tag === "Failure") { + if (result === undefined) { + result = exit + length = index + fibers.forEach((fiber) => fiber.unsafeInterrupt()) + } + } else if (out !== undefined) { + out[currentIndex] = exit.value + } + doneCount++ + inProgress-- + if (doneCount === length) { + resume(result ?? succeed(out)) + } else if (!pumping && inProgress < concurrency) { + pump() + } + }) + } catch (err) { + result = exitDie(err) + length = index + fibers.forEach((fiber) => fiber.unsafeInterrupt()) + } + } + pumping = false + } + pump() + + return suspend(() => { + interrupted = true + index = length + return fiberInterruptAll(fibers) + }) + }) + }) + +/** + * Effectfully filter the elements of the provided iterable. + * + * Use the `concurrency` option to control how many elements are processed + * concurrently. + * + * @since 3.4.0 + * @experimental + * @category collecting & elements + */ +export const filter = (iterable: Iterable, f: (a: NoInfer) => Micro, options?: { + readonly concurrency?: Concurrency | undefined + readonly negate?: boolean | undefined +}): Micro, E, R> => + filterMap(iterable, (a) => + map(f(a), (pass) => { + pass = options?.negate ? !pass : pass + return pass ? Option.some(a) : Option.none() + }), options) + +/** + * Effectfully filter the elements of the provided iterable. + * + * Use the `concurrency` option to control how many elements are processed + * concurrently. + * + * @since 3.4.0 + * @experimental + * @category collecting & elements + */ +export const filterMap = ( + iterable: Iterable, + f: (a: NoInfer) => Micro, E, R>, + options?: { + readonly concurrency?: Concurrency | undefined + } +): Micro, E, R> => + suspend(() => { + const out: Array = [] + return as( + forEach(iterable, (a) => + map(f(a), (o) => { + if (o._tag === "Some") { + out.push(o.value) + } + }), { + discard: true, + concurrency: options?.concurrency + }), + out + ) + }) + +// ---------------------------------------------------------------------------- +// do notation +// ---------------------------------------------------------------------------- + +/** + * Start a do notation block. + * + * @since 3.4.0 + * @experimental + * @category do notation + */ +export const Do: Micro<{}> = succeed({}) + +/** + * Bind the success value of this `Micro` effect to the provided name. + * + * @since 3.4.0 + * @experimental + * @category do notation + */ +export const bindTo: { + (name: N): (self: Micro) => Micro<{ [K in N]: A }, E, R> + (self: Micro, name: N): Micro<{ [K in N]: A }, E, R> +} = doNotation.bindTo(map) + +/** + * Bind the success value of this `Micro` effect to the provided name. + * + * @since 3.4.0 + * @experimental + * @category do notation + */ +export const bind: { + , B, E2, R2>( + name: N, + f: (a: NoInfer) => Micro + ): (self: Micro) => Micro & { [K in N]: B }>, E | E2, R | R2> + , E, R, B, E2, R2, N extends string>( + self: Micro, + name: N, + f: (a: NoInfer) => Micro + ): Micro & { [K in N]: B }>, E | E2, R | R2> +} = doNotation.bind(map, flatMap) + +const let_: { + , B>( + name: N, + f: (a: NoInfer) => B + ): (self: Micro) => Micro & { [K in N]: B }>, E, R> + , E, R, B, N extends string>( + self: Micro, + name: N, + f: (a: NoInfer) => B + ): Micro & { [K in N]: B }>, E, R> +} = doNotation.let_(map) + +export { + /** + * Bind the result of a synchronous computation to the given name. + * + * @since 3.4.0 + * @experimental + * @category do notation + */ + let_ as let +} + +// ---------------------------------------------------------------------------- +// fibers & forking +// ---------------------------------------------------------------------------- + +/** + * Run the `Micro` effect in a new `MicroFiber` that can be awaited, joined, or + * aborted. + * + * When the parent `Micro` finishes, this `Micro` will be aborted. + * + * @since 3.4.0 + * @experimental + * @category fiber & forking + */ +export const fork = ( + self: Micro +): Micro, never, R> => + withMicroFiber((fiber) => { + fiberMiddleware.interruptChildren ??= fiberInterruptChildren + return succeed(unsafeFork(fiber, self)) + }) + +const unsafeFork = ( + parent: MicroFiberImpl, + effect: Micro, + immediate = false, + daemon = false +): MicroFiber => { + const child = new MicroFiberImpl(parent.context, parent.interruptible) + if (!daemon) { + parent.children().add(child) + child.addObserver(() => parent.children().delete(child)) + } + if (immediate) { + child.evaluate(effect as any) + } else { + parent.getRef(CurrentScheduler).scheduleTask(() => child.evaluate(effect as any), 0) + } + return child +} + +/** + * Run the `Micro` effect in a new `MicroFiber` that can be awaited, joined, or + * aborted. + * + * It will not be aborted when the parent `Micro` finishes. + * + * @since 3.4.0 + * @experimental + * @category fiber & forking + */ +export const forkDaemon = ( + self: Micro +): Micro, never, R> => withMicroFiber((fiber) => succeed(unsafeFork(fiber, self, false, true))) + +/** + * Run the `Micro` effect in a new `MicroFiber` that can be awaited, joined, or + * aborted. + * + * The lifetime of the handle will be attached to the provided `MicroScope`. + * + * @since 3.4.0 + * @experimental + * @category fiber & forking + */ +export const forkIn: { + (scope: MicroScope): (self: Micro) => Micro, never, R> + (self: Micro, scope: MicroScope): Micro, never, R> +} = dual( + 2, + (self: Micro, scope: MicroScope): Micro, never, R> => + uninterruptibleMask((restore) => + flatMap(scope.fork, (scope) => + tap( + restore(forkDaemon(onExit(self, (exit) => scope.close(exit)))), + (fiber) => scope.addFinalizer((_) => fiberInterrupt(fiber)) + )) + ) +) + +/** + * Run the `Micro` effect in a new `MicroFiber` that can be awaited, joined, or + * aborted. + * + * The lifetime of the handle will be attached to the current `MicroScope`. + * + * @since 3.4.0 + * @experimental + * @category fiber & forking + */ +export const forkScoped = (self: Micro): Micro, never, R | MicroScope> => + flatMap(scope, (scope) => forkIn(self, scope)) + +// ---------------------------------------------------------------------------- +// execution +// ---------------------------------------------------------------------------- + +/** + * Execute the `Micro` effect and return a `MicroFiber` that can be awaited, joined, + * or aborted. + * + * You can listen for the result by adding an observer using the handle's + * `addObserver` method. + * + * @example + * ```ts + * import * as Micro from "effect/Micro" + * + * const handle = Micro.succeed(42).pipe( + * Micro.delay(1000), + * Micro.runFork + * ) + * + * handle.addObserver((exit) => { + * console.log(exit) + * }) + * ``` + * + * @since 3.4.0 + * @experimental + * @category execution + */ +export const runFork = ( + effect: Micro, + options?: { + readonly signal?: AbortSignal | undefined + readonly scheduler?: MicroScheduler | undefined + } | undefined +): MicroFiberImpl => { + const fiber = new MicroFiberImpl(CurrentScheduler.context( + options?.scheduler ?? new MicroSchedulerDefault() + )) + fiber.evaluate(effect as any) + if (options?.signal) { + if (options.signal.aborted) { + fiber.unsafeInterrupt() + } else { + const abort = () => fiber.unsafeInterrupt() + options.signal.addEventListener("abort", abort, { once: true }) + fiber.addObserver(() => options.signal!.removeEventListener("abort", abort)) + } + } + return fiber +} + +/** + * Execute the `Micro` effect and return a `Promise` that resolves with the + * `MicroExit` of the computation. + * + * @since 3.4.6 + * @experimental + * @category execution + */ +export const runPromiseExit = ( + effect: Micro, + options?: { + readonly signal?: AbortSignal | undefined + readonly scheduler?: MicroScheduler | undefined + } | undefined +): Promise> => + new Promise((resolve, _reject) => { + const handle = runFork(effect, options) + handle.addObserver(resolve) + }) + +/** + * Execute the `Micro` effect and return a `Promise` that resolves with the + * successful value of the computation. + * + * @since 3.4.0 + * @experimental + * @category execution + */ +export const runPromise = ( + effect: Micro, + options?: { + readonly signal?: AbortSignal | undefined + readonly scheduler?: MicroScheduler | undefined + } | undefined +): Promise => + runPromiseExit(effect, options).then((exit) => { + if (exit._tag === "Failure") { + throw exit.cause + } + return exit.value + }) + +/** + * Attempt to execute the `Micro` effect synchronously and return the `MicroExit`. + * + * If any asynchronous effects are encountered, the function will return a + * `CauseDie` containing the `MicroFiber`. + * + * @since 3.4.6 + * @experimental + * @category execution + */ +export const runSyncExit = (effect: Micro): MicroExit => { + const scheduler = new MicroSchedulerDefault() + const fiber = runFork(effect, { scheduler }) + scheduler.flush() + return fiber._exit ?? exitDie(fiber) +} + +/** + * Attempt to execute the `Micro` effect synchronously and return the success + * value. + * + * @since 3.4.0 + * @experimental + * @category execution + */ +export const runSync = (effect: Micro): A => { + const exit = runSyncExit(effect) + if (exit._tag === "Failure") throw exit.cause + return exit.value +} + +// ---------------------------------------------------------------------------- +// Errors +// ---------------------------------------------------------------------------- + +/** + * @since 3.4.0 + * @experimental + * @category errors + */ +export interface YieldableError extends Pipeable, Inspectable, Readonly { + readonly [Effectable.EffectTypeId]: Effect.VarianceStruct + readonly [Effectable.StreamTypeId]: Stream.VarianceStruct + readonly [Effectable.SinkTypeId]: Sink.VarianceStruct + readonly [Effectable.ChannelTypeId]: Channel.VarianceStruct + readonly [TypeId]: Micro.Variance + [Symbol.iterator](): MicroIterator> +} + +const YieldableError: new(message?: string) => YieldableError = (function() { + class YieldableError extends globalThis.Error {} + // @effect-diagnostics-next-line floatingEffect:off + Object.assign(YieldableError.prototype, MicroProto, StructuralPrototype, { + [identifier]: "Failure", + [evaluate]() { + return fail(this) + }, + toString(this: Error) { + return this.message ? `${this.name}: ${this.message}` : this.name + }, + toJSON() { + return { ...this } + }, + [NodeInspectSymbol](this: Error): string { + const stack = this.stack + if (stack) { + return `${this.toString()}\n${stack.split("\n").slice(1).join("\n")}` + } + return this.toString() + } + }) + return YieldableError as any +})() + +/** + * @since 3.4.0 + * @experimental + * @category errors + */ +export const Error: new = {}>( + args: Equals extends true ? void + : { readonly [P in keyof A]: A[P] } +) => YieldableError & Readonly = (function() { + return class extends YieldableError { + constructor(args: any) { + super() + if (args) { + Object.assign(this, args) + } + } + } as any +})() + +/** + * @since 3.4.0 + * @experimental + * @category errors + */ +export const TaggedError = (tag: Tag): new = {}>( + args: Equals extends true ? void + : { readonly [P in keyof A as P extends "_tag" ? never : P]: A[P] } +) => YieldableError & { readonly _tag: Tag } & Readonly => { + class Base extends Error<{}> { + readonly _tag = tag + } + ;(Base.prototype as any).name = tag + return Base as any +} + +/** + * Represents a checked exception which occurs when an expected element was + * unable to be found. + * + * @since 3.4.4 + * @experimental + * @category errors + */ +export class NoSuchElementException extends TaggedError("NoSuchElementException")<{ message?: string | undefined }> {} + +/** + * Represents a checked exception which occurs when a timeout occurs. + * + * @since 3.4.4 + * @experimental + * @category errors + */ +export class TimeoutException extends TaggedError("TimeoutException") {} diff --git a/repos/effect/packages/effect/src/ModuleVersion.ts b/repos/effect/packages/effect/src/ModuleVersion.ts new file mode 100644 index 0000000..a8e5d25 --- /dev/null +++ b/repos/effect/packages/effect/src/ModuleVersion.ts @@ -0,0 +1,18 @@ +/** + * @since 2.0.0 + * + * Enables low level framework authors to run on their own isolated effect version + */ +import * as internal from "./internal/version.js" + +/** + * @since 2.0.0 + * @category version + */ +export const getCurrentVersion: () => string = internal.getCurrentVersion + +/** + * @since 2.0.0 + * @category version + */ +export const setCurrentVersion: (version: string) => void = internal.setCurrentVersion diff --git a/repos/effect/packages/effect/src/MutableHashMap.ts b/repos/effect/packages/effect/src/MutableHashMap.ts new file mode 100644 index 0000000..3a53cb3 --- /dev/null +++ b/repos/effect/packages/effect/src/MutableHashMap.ts @@ -0,0 +1,411 @@ +/** + * @since 2.0.0 + */ +import type { NonEmptyArray } from "./Array.js" +import * as Equal from "./Equal.js" +import { dual } from "./Function.js" +import * as Hash from "./Hash.js" +import { format, type Inspectable, NodeInspectSymbol, toJSON } from "./Inspectable.js" +import * as Option from "./Option.js" +import type { Pipeable } from "./Pipeable.js" +import { pipeArguments } from "./Pipeable.js" + +const TypeId: unique symbol = Symbol.for("effect/MutableHashMap") as TypeId + +/** + * @since 2.0.0 + * @category symbol + */ +export type TypeId = typeof TypeId + +/** + * @since 2.0.0 + * @category models + */ +export interface MutableHashMap extends Iterable<[K, V]>, Pipeable, Inspectable { + readonly [TypeId]: TypeId + /** @internal */ + readonly referential: Map + /** @internal */ + readonly buckets: Map> + /** @internal */ + bucketsSize: number +} + +const MutableHashMapProto: Omit, "referential" | "buckets" | "bucketsSize"> = { + [TypeId]: TypeId, + [Symbol.iterator](this: MutableHashMap): Iterator<[unknown, unknown]> { + return new MutableHashMapIterator(this) + }, + toString() { + return format(this.toJSON()) + }, + toJSON() { + return { + _id: "MutableHashMap", + values: Array.from(this).map(toJSON) + } + }, + [NodeInspectSymbol]() { + return this.toJSON() + }, + pipe() { + return pipeArguments(this, arguments) + } +} + +class MutableHashMapIterator implements IterableIterator<[K, V]> { + readonly referentialIterator: Iterator<[K, V]> + bucketIterator: Iterator<[K, V]> | undefined + + constructor(readonly self: MutableHashMap) { + this.referentialIterator = self.referential[Symbol.iterator]() + } + next(): IteratorResult<[K, V]> { + if (this.bucketIterator !== undefined) { + return this.bucketIterator.next() + } + const result = this.referentialIterator.next() + if (result.done) { + this.bucketIterator = new BucketIterator(this.self.buckets.values()) + return this.next() + } + return result + } + + [Symbol.iterator](): IterableIterator<[K, V]> { + return new MutableHashMapIterator(this.self) + } +} + +class BucketIterator implements Iterator<[K, V]> { + constructor(readonly backing: Iterator>) {} + currentBucket: Iterator | undefined + next(): IteratorResult<[K, V]> { + if (this.currentBucket === undefined) { + const result = this.backing.next() + if (result.done) { + return result + } + this.currentBucket = result.value[Symbol.iterator]() + } + const result = this.currentBucket.next() + if (result.done) { + this.currentBucket = undefined + return this.next() + } + return result as IteratorResult<[K, V]> + } +} + +/** + * @since 2.0.0 + * @category constructors + */ +export const empty = (): MutableHashMap => { + const self = Object.create(MutableHashMapProto) + self.referential = new Map() + self.buckets = new Map() + self.bucketsSize = 0 + return self +} + +/** + * @since 2.0.0 + * @category constructors + */ +export const make: >( + ...entries: Entries +) => MutableHashMap< + Entries[number] extends readonly [infer K, any] ? K : never, + Entries[number] extends readonly [any, infer V] ? V : never +> = (...entries) => fromIterable(entries) + +/** + * Creates a new `MutableHashMap` from an iterable collection of key/value pairs. + * + * @since 2.0.0 + * @category constructors + */ +export const fromIterable = (entries: Iterable): MutableHashMap => { + const self = empty() + for (const [key, value] of entries) { + set(self, key, value) + } + return self +} + +/** + * @since 2.0.0 + * @category elements + */ +export const get: { + (key: K): (self: MutableHashMap) => Option.Option + (self: MutableHashMap, key: K): Option.Option +} = dual< + (key: K) => (self: MutableHashMap) => Option.Option, + (self: MutableHashMap, key: K) => Option.Option +>(2, (self: MutableHashMap, key: K): Option.Option => { + if (Equal.isEqual(key) === false) { + return self.referential.has(key) ? Option.some(self.referential.get(key)!) : Option.none() + } + + const hash = key[Hash.symbol]() + const bucket = self.buckets.get(hash) + if (bucket === undefined) { + return Option.none() + } + + return getFromBucket(self, bucket, key) +}) + +/** + * @since 3.8.0 + * @category elements + */ +export const keys = (self: MutableHashMap): Array => { + const keys = Array.from(self.referential.keys()) + for (const bucket of self.buckets.values()) { + for (let i = 0, len = bucket.length; i < len; i++) { + keys.push(bucket[i][0]) + } + } + return keys +} + +/** + * @since 3.8.0 + * @category elements + */ +export const values = (self: MutableHashMap): Array => { + const values = Array.from(self.referential.values()) + for (const bucket of self.buckets.values()) { + for (let i = 0, len = bucket.length; i < len; i++) { + values.push(bucket[i][1]) + } + } + return values +} + +const getFromBucket = ( + self: MutableHashMap, + bucket: NonEmptyArray, + key: K & Equal.Equal, + remove = false +): Option.Option => { + for (let i = 0, len = bucket.length; i < len; i++) { + if (key[Equal.symbol](bucket[i][0])) { + const value = bucket[i][1] + if (remove) { + bucket.splice(i, 1) + self.bucketsSize-- + } + return Option.some(value) + } + } + + return Option.none() +} + +/** + * @since 2.0.0 + * @category elements + */ +export const has: { + (key: K): (self: MutableHashMap) => boolean + (self: MutableHashMap, key: K): boolean +} = dual< + (key: K) => (self: MutableHashMap) => boolean, + (self: MutableHashMap, key: K) => boolean +>(2, (self, key) => Option.isSome(get(self, key))) + +/** + * @since 2.0.0 + */ +export const set: { + (key: K, value: V): (self: MutableHashMap) => MutableHashMap + (self: MutableHashMap, key: K, value: V): MutableHashMap +} = dual< + (key: K, value: V) => (self: MutableHashMap) => MutableHashMap, + (self: MutableHashMap, key: K, value: V) => MutableHashMap +>(3, (self: MutableHashMap, key: K, value: V) => { + if (Equal.isEqual(key) === false) { + self.referential.set(key, value) + return self + } + + const hash = key[Hash.symbol]() + const bucket = self.buckets.get(hash) + if (bucket === undefined) { + self.buckets.set(hash, [[key, value]]) + self.bucketsSize++ + return self + } + + removeFromBucket(self, bucket, key) + bucket.push([key, value]) + self.bucketsSize++ + return self +}) + +const removeFromBucket = ( + self: MutableHashMap, + bucket: NonEmptyArray, + key: K & Equal.Equal +) => { + for (let i = 0, len = bucket.length; i < len; i++) { + if (key[Equal.symbol](bucket[i][0])) { + bucket.splice(i, 1) + self.bucketsSize-- + return + } + } +} + +/** + * Updates the value of the specified key within the `MutableHashMap` if it exists. + * + * @since 2.0.0 + */ +export const modify: { + (key: K, f: (v: V) => V): (self: MutableHashMap) => MutableHashMap + (self: MutableHashMap, key: K, f: (v: V) => V): MutableHashMap +} = dual< + (key: K, f: (v: V) => V) => (self: MutableHashMap) => MutableHashMap, + (self: MutableHashMap, key: K, f: (v: V) => V) => MutableHashMap +>(3, (self: MutableHashMap, key: K, f: (v: V) => V) => { + if (Equal.isEqual(key) === false) { + if (self.referential.has(key)) { + self.referential.set(key, f(self.referential.get(key)!)) + } + return self + } + + const hash = key[Hash.symbol]() + const bucket = self.buckets.get(hash) + if (bucket === undefined) { + return self + } + + const value = getFromBucket(self, bucket, key, true) + if (Option.isNone(value)) { + return self + } + bucket.push([key, f(value.value)]) + self.bucketsSize++ + return self +}) + +/** + * Set or remove the specified key in the `MutableHashMap` using the specified + * update function. + * + * @since 2.0.0 + */ +export const modifyAt: { + (key: K, f: (value: Option.Option) => Option.Option): (self: MutableHashMap) => MutableHashMap + (self: MutableHashMap, key: K, f: (value: Option.Option) => Option.Option): MutableHashMap +} = dual< + ( + key: K, + f: (value: Option.Option) => Option.Option + ) => (self: MutableHashMap) => MutableHashMap, + ( + self: MutableHashMap, + key: K, + f: (value: Option.Option) => Option.Option + ) => MutableHashMap +>(3, (self, key, f) => { + if (Equal.isEqual(key) === false) { + const result = f(get(self, key)) + if (Option.isSome(result)) { + set(self, key, result.value) + } else { + remove(self, key) + } + return self + } + + const hash = key[Hash.symbol]() + const bucket = self.buckets.get(hash) + if (bucket === undefined) { + const result = f(Option.none()) + return Option.isSome(result) ? set(self, key, result.value) : self + } + + const result = f(getFromBucket(self, bucket, key, true)) + if (Option.isNone(result)) { + if (bucket.length === 0) { + self.buckets.delete(hash) + } + return self + } + bucket.push([key, result.value]) + self.bucketsSize++ + return self +}) + +/** + * @since 2.0.0 + */ +export const remove: { + (key: K): (self: MutableHashMap) => MutableHashMap + (self: MutableHashMap, key: K): MutableHashMap +} = dual< + (key: K) => (self: MutableHashMap) => MutableHashMap, + (self: MutableHashMap, key: K) => MutableHashMap +>(2, (self: MutableHashMap, key: K) => { + if (Equal.isEqual(key) === false) { + self.referential.delete(key) + return self + } + + const hash = key[Hash.symbol]() + const bucket = self.buckets.get(hash) + if (bucket === undefined) { + return self + } + removeFromBucket(self, bucket, key) + if (bucket.length === 0) { + self.buckets.delete(hash) + } + return self +}) + +/** + * @since 2.0.0 + */ +export const clear = (self: MutableHashMap) => { + self.referential.clear() + self.buckets.clear() + self.bucketsSize = 0 + return self +} + +/** + * @since 2.0.0 + * @category elements + */ +export const size = (self: MutableHashMap): number => { + return self.referential.size + self.bucketsSize +} + +/** + * @since 2.0.0 + */ +export const isEmpty = (self: MutableHashMap): boolean => size(self) === 0 + +/** + * @since 2.0.0 + */ +export const forEach: { + (f: (value: V, key: K) => void): (self: MutableHashMap) => void + (self: MutableHashMap, f: (value: V, key: K) => void): void +} = dual(2, (self: MutableHashMap, f: (value: V, key: K) => void) => { + self.referential.forEach(f) + for (const bucket of self.buckets.values()) { + for (const [key, value] of bucket) { + f(value, key) + } + } +}) diff --git a/repos/effect/packages/effect/src/MutableHashSet.ts b/repos/effect/packages/effect/src/MutableHashSet.ts new file mode 100644 index 0000000..ad202d3 --- /dev/null +++ b/repos/effect/packages/effect/src/MutableHashSet.ts @@ -0,0 +1,706 @@ +/** + * # MutableHashSet + * + * A mutable `MutableHashSet` provides a collection of unique values with + * efficient lookup, insertion and removal. Unlike its immutable sibling + * {@link module:HashSet}, a `MutableHashSet` can be modified in-place; + * operations like add, remove, and clear directly modify the original set + * rather than creating a new one. This mutability offers benefits like improved + * performance in scenarios where you need to build or modify a set + * incrementally. + * + * ## What Problem Does It Solve? + * + * `MutableHashSet` solves the problem of maintaining an unsorted collection + * where each value appears exactly once, with fast operations for checking + * membership and adding/removing values, in contexts where mutability is + * preferred for performance or implementation simplicity. + * + * ## When to Use + * + * Use `MutableHashSet` when you need: + * + * - A collection with no duplicate values + * - Efficient membership testing (**`O(1)`** average complexity) + * - In-place modifications for better performance + * - A set that will be built or modified incrementally + * - Local mutability in otherwise immutable code + * + * ## Advanced Features + * + * MutableHashSet provides operations for: + * + * - Adding and removing elements with direct mutation + * - Checking for element existence + * - Clearing all elements at once + * - Converting to/from other collection types + * + * ## Performance Characteristics + * + * - **Lookup** operations ({@link module:MutableHashSet.has}): **`O(1)`** average + * time complexity + * - **Insertion** operations ({@link module:MutableHashSet.add}): **`O(1)`** + * average time complexity + * - **Removal** operations ({@link module:MutableHashSet.remove}): **`O(1)`** + * average time complexity + * - **Iteration**: **`O(n)`** where n is the size of the set + * + * The MutableHashSet data structure implements the following traits: + * + * - {@link Iterable}: allows iterating over the values in the set + * - {@link Pipeable}: allows chaining operations with the pipe operator + * - {@link Inspectable}: allows inspecting the contents of the set + * + * ## Operations Reference + * + * | Category | Operation | Description | Complexity | + * | ------------ | ------------------------------------------ | ----------------------------------- | ---------- | + * | constructors | {@link module:MutableHashSet.empty} | Creates an empty MutableHashSet | O(1) | + * | constructors | {@link module:MutableHashSet.fromIterable} | Creates a set from an iterable | O(n) | + * | constructors | {@link module:MutableHashSet.make} | Creates a set from multiple values | O(n) | + * | | | | | + * | elements | {@link module:MutableHashSet.has} | Checks if a value exists in the set | O(1) avg | + * | elements | {@link module:MutableHashSet.add} | Adds a value to the set | O(1) avg | + * | elements | {@link module:MutableHashSet.remove} | Removes a value from the set | O(1) avg | + * | elements | {@link module:MutableHashSet.size} | Gets the number of elements | O(1) | + * | elements | {@link module:MutableHashSet.clear} | Removes all values from the set | O(1) | + * + * ## Notes + * + * ### Mutability Considerations: + * + * Unlike most data structures in the Effect ecosystem, `MutableHashSet` is + * mutable. This means that operations like `add`, `remove`, and `clear` modify + * the original set rather than creating a new one. This can lead to more + * efficient code in some scenarios, but requires careful handling to avoid + * unexpected side effects. + * + * ### When to Choose `MutableHashSet` vs {@link module:HashSet}: + * + * - Use `MutableHashSet` when you need to build or modify a set incrementally and + * performance is a priority + * - Use `HashSet` when you want immutability guarantees and functional + * programming patterns + * - Consider using {@link module:HashSet}'s bounded mutation context (via + * {@link module:HashSet.beginMutation}, {@link module:HashSet.endMutation}, and + * {@link module:HashSet.mutate} methods) when you need temporary mutability + * within an otherwise immutable context - this approach might be sufficient + * for many use cases without requiring a separate `MutableHashSet` + * - `MutableHashSet` is often useful for local operations where the mutability is + * contained and doesn't leak into the broader application + * + * @module MutableHashSet + * @since 2.0.0 + */ +import * as Dual from "./Function.js" +import { format, type Inspectable, NodeInspectSymbol, toJSON } from "./Inspectable.js" +import * as MutableHashMap from "./MutableHashMap.js" +import type { Pipeable } from "./Pipeable.js" +import { pipeArguments } from "./Pipeable.js" + +const TypeId: unique symbol = Symbol.for("effect/MutableHashSet") as TypeId + +/** + * @since 2.0.0 + * @category symbol + */ +export type TypeId = typeof TypeId + +/** + * @since 2.0.0 + * @category models + */ +export interface MutableHashSet extends Iterable, Pipeable, Inspectable { + readonly [TypeId]: TypeId + + /** @internal */ + readonly keyMap: MutableHashMap.MutableHashMap +} + +const MutableHashSetProto: Omit, "keyMap"> = { + [TypeId]: TypeId, + [Symbol.iterator](this: MutableHashSet): Iterator { + return Array.from(this.keyMap) + .map(([_]) => _)[Symbol.iterator]() + }, + toString() { + return format(this.toJSON()) + }, + toJSON() { + return { + _id: "MutableHashSet", + values: Array.from(this).map(toJSON) + } + }, + [NodeInspectSymbol]() { + return this.toJSON() + }, + pipe() { + return pipeArguments(this, arguments) + } +} + +const fromHashMap = ( + keyMap: MutableHashMap.MutableHashMap +): MutableHashSet => { + const set = Object.create(MutableHashSetProto) + set.keyMap = keyMap + return set +} + +/** + * Creates an empty mutable hash set. + * + * This function initializes and returns an empty `MutableHashSet` instance, + * which allows for efficient storage and manipulation of unique elements. + * + * Time complexity: **`O(1)`** + * + * @memberof MutableHashSet + * @since 2.0.0 + * @category constructors + * @example + * + * ```ts + * import { MutableHashSet } from "effect" + * + * type T = unknown // replace with your type + * + * // in places where the type can't be inferred, replace with your type + * const set: MutableHashSet.MutableHashSet = MutableHashSet.empty() + * ``` + * + * @template K - The type of the elements to be stored in the hash set. Defaults + * to `never` if not specified. + * @returns A new mutable instance of `MutableHashSet` containing no elements + * for the specified type `K`. + * @see Other `MutableHashSet` constructors are {@link module:MutableHashSet.make} {@link module:MutableHashSet.fromIterable} + */ +export const empty = (): MutableHashSet => fromHashMap(MutableHashMap.empty()) + +/** + * Creates a new `MutableHashSet` from an iterable collection of values. + * Duplicate values are omitted. + * + * Time complexity: **`O(n)`** where n is the number of elements in the iterable + * + * Creating a `MutableHashSet` from an {@link Array} + * + * ```ts + * import { MutableHashSet } from "effect" + * + * const array: Iterable = [1, 2, 3, 4, 5, 1, 2, 3] // Array is also Iterable + * const mutableHashSet: MutableHashSet.MutableHashSet = + * MutableHashSet.fromIterable(array) + * + * console.log( + * // MutableHashSet.MutableHashSet is also an Iterable + * Array.from(mutableHashSet) + * ) // Output: [1, 2, 3, 4, 5] + * ``` + * + * Creating a `MutableHashSet` from a {@link Set} + * + * ```ts + * import { MutableHashSet, pipe } from "effect" + * + * console.log( + * pipe( + * // Set is an Iterable + * new Set(["apple", "banana", "orange", "apple"]), + * // constructs MutableHashSet from an Iterable Set + * MutableHashSet.fromIterable, + * // since MutableHashSet it is itself an Iterable, we can pass it to other functions expecting an Iterable + * Array.from + * ) + * ) // Output: ["apple", "banana", "orange"] + * ``` + * + * Creating a `MutableHashSet` from a {@link Generator} + * + * ```ts + * import { MutableHashSet } from "effect" + * + * // Generator functions return iterables + * function* fibonacci(n: number): Generator { + * let [a, b] = [0, 1] + * for (let i = 0; i < n; i++) { + * yield a + * ;[a, b] = [b, a + b] + * } + * } + * + * // Create a MutableHashSet from the first 10 Fibonacci numbers + * const fibonacciSet = MutableHashSet.fromIterable(fibonacci(10)) + * + * console.log(Array.from(fibonacciSet)) + * // Outputs: [0, 1, 2, 3, 5, 8, 13, 21, 34] but in unsorted order + * ``` + * + * Creating a `MutableHashSet` from another {@link module:MutableHashSet} + * + * ```ts + * import { MutableHashSet, pipe } from "effect" + * + * console.log( + * pipe( + * MutableHashSet.make(1, 2, 3, 4), + * MutableHashSet.fromIterable, + * Array.from + * ) + * ) // Output: [1, 2, 3, 4] + * ``` + * + * Creating a `MutableHashSet` from an {@link module:HashSet} + * + * ```ts + * import { HashSet, MutableHashSet, pipe } from "effect" + * + * console.log( + * pipe( + * HashSet.make(1, 2, 3, 4), // it works also with its immutable HashSet sibling + * MutableHashSet.fromIterable, + * Array.from + * ) + * ) // Output: [1, 2, 3, 4] + * ``` + * + * Creating a `MutableHashSet` from other Effect's data structures like + * {@link Chunk} + * + * ```ts + * import { Chunk, MutableHashSet, pipe } from "effect" + * + * console.log( + * pipe( + * Chunk.make(1, 2, 3, 4), // Chunk is also an Iterable + * MutableHashSet.fromIterable, + * Array.from + * ) + * ) // Outputs: [1, 2, 3, 4] + * ``` + * + * @memberof MutableHashSet + * @since 2.0.0 + * @category constructors + * @template K - The type of elements to be stored in the resulting + * `MutableHashSet`. + * @param keys - An `Iterable` collection containing the keys to be added to the + * `MutableHashSet`. + * @returns A new `MutableHashSet` containing just the unique elements from the + * provided iterable. + * @see Other `MutableHashSet` constructors are {@link module:MutableHashSet.empty} {@link module:MutableHashSet.make} + */ +export const fromIterable = (keys: Iterable): MutableHashSet => + fromHashMap( + MutableHashMap.fromIterable(Array.from(keys).map((k) => [k, true])) + ) + +/** + * Construct a new `MutableHashSet` from a variable number of values. + * + * Time complexity: **`O(n)`** where n is the number of elements + * + * @memberof MutableHashSet + * @since 2.0.0 + * @category constructors + * @example + * + * ```ts + * import { Equal, Hash, MutableHashSet } from "effect" + * import assert from "node:assert/strict" + * + * class Character implements Equal.Equal { + * readonly name: string + * readonly trait: string + * + * constructor(name: string, trait: string) { + * this.name = name + * this.trait = trait + * } + * + * // Define equality based on name, and trait + * [Equal.symbol](that: Equal.Equal): boolean { + * if (that instanceof Character) { + * return ( + * Equal.equals(this.name, that.name) && + * Equal.equals(this.trait, that.trait) + * ) + * } + * return false + * } + * + * // Generate a hash code based on the sum of the character's name and trait + * [Hash.symbol](): number { + * return Hash.hash(this.name + this.trait) + * } + * + * static readonly of = (name: string, trait: string): Character => { + * return new Character(name, trait) + * } + * } + * + * const mutableCharacterHashSet = MutableHashSet.make( + * Character.of("Alice", "Curious"), + * Character.of("Alice", "Curious"), + * Character.of("White Rabbit", "Always late"), + * Character.of("Mad Hatter", "Tea enthusiast") + * ) + * + * assert.equal( + * MutableHashSet.has( + * mutableCharacterHashSet, + * Character.of("Alice", "Curious") + * ), + * true + * ) + * assert.equal( + * MutableHashSet.has( + * mutableCharacterHashSet, + * Character.of("Fluffy", "Kind") + * ), + * false + * ) + * ``` + * + * @see Other `MutableHashSet` constructors are {@link module:MutableHashSet.fromIterable} {@link module:MutableHashSet.empty} + */ +export const make = >( + ...keys: Keys +): MutableHashSet => fromIterable(keys) + +/** + * **Checks** whether the `MutableHashSet` contains the given element, and + * **adds** it if not. + * + * Time complexity: **`O(1)`** average + * + * **Syntax** + * + * ```ts + * import { MutableHashSet, pipe } from "effect" + * + * // with data-last, a.k.a. pipeable API + * pipe( + * MutableHashSet.empty(), + * MutableHashSet.add(0), + * MutableHashSet.add(0) + * ) + * + * // or piped with the pipe function + * MutableHashSet.empty().pipe(MutableHashSet.add(0)) + * + * // or with data-first API + * MutableHashSet.add(MutableHashSet.empty(), 0) + * ``` + * + * @memberof MutableHashSet + * @since 2.0.0 + * @category elements + * @see Other `MutableHashSet` elements are {@link module:MutableHashSet.remove} {@link module:MutableHashSet.size} {@link module:MutableHashSet.clear} {@link module:MutableHashSet.has} + */ +export const add: { + /** + * `data-last` a.k.a. `pipeable` API + * + * ```ts + * import { MutableHashSet, pipe } from "effect" + * import assert from "node:assert/strict" + * + * const mutableHashSet = pipe( + * MutableHashSet.empty(), // MutableHashSet.MutableHashSet + * MutableHashSet.add(0), + * MutableHashSet.add(1), + * MutableHashSet.add(1), + * MutableHashSet.add(2) + * ) + * + * assert.deepStrictEqual( + * Array.from(mutableHashSet), // remember that MutableHashSet is also an Iterable + * Array.of(0, 1, 2) + * ) + * ``` + * + * @template V - The type of elements stored in the `MutableHashSet`. + * @param key - The key to be added to the `MutableHashSet` if not already + * present. + * @returns A function that accepts a `MutableHashSet` and returns the + * reference of the updated `MutableHashSet` including the key. + */ + (key: V): (self: MutableHashSet) => MutableHashSet + + /** + * `data-first` API + * + * ```ts + * import { MutableHashSet } from "effect" + * import assert from "node:assert/strict" + * + * const empty = MutableHashSet.empty() + * const withZero = MutableHashSet.add(empty, 0) + * const withOne = MutableHashSet.add(withZero, 1) + * const withTwo = MutableHashSet.add(withOne, 2) + * const withTwoTwo = MutableHashSet.add(withTwo, 2) + * + * assert(Object.is(withTwoTwo, empty)) // proof that it does mutate the original set + * + * assert.deepStrictEqual( + * Array.from(withTwoTwo), // remember that MutableHashSet is also an Iterable + * Array.of(0, 1, 2) + * ) + * ``` + * + * @template V - The type of elements stored in the `MutableHashSet`. + * @param self - The `MutableHashSet` instance from which the key should be + * added to. + * @param key - The key to be added to the `MutableHashSet` if not already + * present. + * @returns The reference of the updated `MutableHashSet` including the key. + */ + (self: MutableHashSet, key: V): MutableHashSet +} = Dual.dual< + (key: V) => (self: MutableHashSet) => MutableHashSet, + (self: MutableHashSet, key: V) => MutableHashSet +>(2, (self, key) => (MutableHashMap.set(self.keyMap, key, true), self)) + +/** + * Checks if the specified value exists in the `MutableHashSet`. + * + * Time complexity: `O(1)` average + * + * **Syntax** + * + * ```ts + * import { MutableHashSet, pipe } from "effect" + * import assert from "node:assert/strict" + * + * assert.equal( + * // with `data-last`, a.k.a. `pipeable` API + * pipe(MutableHashSet.make(0, 1, 2), MutableHashSet.has(3)), + * false + * ) + * + * assert.equal( + * // or piped with the pipe function + * MutableHashSet.make(0, 1, 2).pipe(MutableHashSet.has(3)), + * false + * ) + * + * assert.equal( + * // or with `data-first` API + * MutableHashSet.has(MutableHashSet.make(0, 1, 2), 3), + * false + * ) + * ``` + * + * @memberof MutableHashSet + * @since 2.0.0 + * @category elements + * @see Other `MutableHashSet` elements are {@link module:MutableHashSet.add} {@link module:MutableHashSet.remove} {@link module:MutableHashSet.size} {@link module:MutableHashSet.clear} + */ +export const has: { + /** + * `data-last` a.k.a. `pipeable` API + * + * ```ts + * import * as assert from "node:assert/strict" + * import { MutableHashSet, pipe } from "effect" + * + * const set = MutableHashSet.make(0, 1, 2) + * + * assert.equal(pipe(set, MutableHashSet.has(0)), true) + * assert.equal(pipe(set, MutableHashSet.has(1)), true) + * assert.equal(pipe(set, MutableHashSet.has(2)), true) + * assert.equal(pipe(set, MutableHashSet.has(3)), false) + * ``` + */ + (key: V): (self: MutableHashSet) => boolean + + /** + * `data-first` API + * + * ```ts + * import * as assert from "node:assert/strict" + * import { MutableHashSet, pipe } from "effect" + * + * const set = MutableHashSet.make(0, 1, 2) + * + * assert.equal(MutableHashSet.has(set, 0), true) + * assert.equal(MutableHashSet.has(set, 1), true) + * assert.equal(MutableHashSet.has(set, 2), true) + * assert.equal(MutableHashSet.has(set, 3), false) + * ``` + */ + (self: MutableHashSet, key: V): boolean +} = Dual.dual< + (key: V) => (self: MutableHashSet) => boolean, + (self: MutableHashSet, key: V) => boolean +>(2, (self, key) => MutableHashMap.has(self.keyMap, key)) + +/** + * Removes a value from the `MutableHashSet`. + * + * Time complexity: **`O(1)`** average + * + * **Syntax** + * + * ```ts + * import { MutableHashSet, pipe } from "effect" + * import assert from "node:assert/strict" + * + * assert.equal( + * // with `data-last`, a.k.a. `pipeable` API + * pipe( + * MutableHashSet.make(0, 1, 2), + * MutableHashSet.remove(0), + * MutableHashSet.has(0) + * ), + * false + * ) + * + * assert.equal( + * // or piped with the pipe function + * MutableHashSet.make(0, 1, 2).pipe( + * MutableHashSet.remove(0), + * MutableHashSet.has(0) + * ), + * false + * ) + * + * assert.equal( + * // or with `data-first` API + * MutableHashSet.remove(MutableHashSet.make(0, 1, 2), 0).pipe( + * MutableHashSet.has(0) + * ), + * false + * ) + * ``` + * + * @memberof MutableHashSet + * @since 2.0.0 + * @category elements + * @see Other `MutableHashSet` elements are {@link module:MutableHashSet.add} {@link module:MutableHashSet.has} {@link module:MutableHashSet.size} {@link module:MutableHashSet.clear} + */ +export const remove: { + /** + * `data-last` a.k.a. `pipeable` API + * + * ```ts + * import { MutableHashSet, pipe } from "effect" + * import assert from "node:assert/strict" + * + * const set: MutableHashSet.MutableHashSet = MutableHashSet.make( + * 0, + * 1, + * 2 + * ) + * const result: MutableHashSet.MutableHashSet = pipe( + * set, + * MutableHashSet.remove(0) + * ) + * + * assert(Object.is(set, result)) // set and result have the same identity + * assert.equal(pipe(result, MutableHashSet.has(0)), false) // it has correctly removed 0 + * assert.equal(pipe(set, MutableHashSet.has(0)), false) // another proof that we are mutating the original MutableHashSet + * assert.equal(pipe(result, MutableHashSet.has(1)), true) + * assert.equal(pipe(result, MutableHashSet.has(2)), true) + * ``` + * + * @template V - The type of the elements in the `MutableHashSet`. + * @param key - The key to be removed from the `MutableHashSet`. + * @returns A function that takes a `MutableHashSet` as input and returns the + * reference to the same `MutableHashSet` with the specified key removed. + */ + (key: V): (self: MutableHashSet) => MutableHashSet + + /** + * `data-first` API + * + * ```ts + * import { MutableHashSet, pipe } from "effect" + * import assert from "node:assert/strict" + * + * const set = MutableHashSet.make(0, 1, 2) + * const result = MutableHashSet.remove(set, 0) + * + * assert(Object.is(set, result)) // set and result have the same identity + * assert.equal(MutableHashSet.has(result, 0), false) // it has correctly removed 0 + * assert.equal(MutableHashSet.has(set, 0), false) // it mutates the original MutableHashSet + * assert.equal(MutableHashSet.has(result, 1), true) + * assert.equal(MutableHashSet.has(result, 2), true) + * ``` + * + * @template V - The type of the elements in the `MutableHashSet`. + * @param self - The `MutableHashSet` to which the key will be removed from. + * @param key - The value to be removed from the `MutableHashSet` if present. + * @returns The reference to the updated `MutableHashSet`. + */ + (self: MutableHashSet, key: V): MutableHashSet +} = Dual.dual< + (key: V) => (self: MutableHashSet) => MutableHashSet, + (self: MutableHashSet, key: V) => MutableHashSet +>(2, (self, key) => (MutableHashMap.remove(self.keyMap, key), self)) + +/** + * Calculates the number of values in the `HashSet`. + * + * Time complexity: **`O(1)`** + * + * @memberof MutableHashSet + * @since 2.0.0 + * @category elements + * @example + * + * ```ts + * import { MutableHashSet } from "effect" + * import assert from "node:assert/strict" + * + * assert.equal(MutableHashSet.size(MutableHashSet.empty()), 0) + * + * assert.equal( + * MutableHashSet.size(MutableHashSet.make(1, 2, 2, 3, 4, 3)), + * 4 + * ) + * ``` + * + * @template V - The type of the elements to be stored in the `MutableHashSet`. + * @param self - The `MutableHashSet` instance for which the size is to be + * determined. + * @returns The total number of elements within the `MutableHashSet`. + * @see Other `MutableHashSet` elements are {@link module:MutableHashSet.add} {@link module:MutableHashSet.has} {@link module:MutableHashSet.remove} {@link module:MutableHashSet.clear} + */ +export const size = (self: MutableHashSet): number => MutableHashMap.size(self.keyMap) + +/** + * Removes all values from the `MutableHashSet`. + * + * This function operates by delegating the clearing action to the underlying + * key map associated with the given `MutableHashSet`. It ensures that the hash + * set becomes empty while maintaining its existence and structure. + * + * @memberof MutableHashSet + * @since 2.0.0 + * @category elements + * @example + * + * ```ts + * import { MutableHashSet, pipe } from "effect" + * import assert from "node:assert/strict" + * + * assert.deepStrictEqual( + * pipe( + * MutableHashSet.make(1, 2, 3, 4), + * MutableHashSet.clear, + * MutableHashSet.size + * ), + * 0 + * ) + * ``` + * + * @param self - The `MutableHashSet` to clear. + * @returns The same `MutableHashSet` after all elements have been removed. + * @see Other `MutableHashSet` elements are {@link module:MutableHashSet.add} {@link module:MutableHashSet.has} {@link module:MutableHashSet.remove} {@link module:MutableHashSet.size} + */ +export const clear = (self: MutableHashSet): MutableHashSet => ( + MutableHashMap.clear(self.keyMap), self +) diff --git a/repos/effect/packages/effect/src/MutableList.ts b/repos/effect/packages/effect/src/MutableList.ts new file mode 100644 index 0000000..be61d23 --- /dev/null +++ b/repos/effect/packages/effect/src/MutableList.ts @@ -0,0 +1,297 @@ +/** + * @since 2.0.0 + */ +import * as Dual from "./Function.js" +import { format, NodeInspectSymbol, toJSON } from "./Inspectable.js" +import type { Inspectable } from "./Inspectable.js" +import type { Pipeable } from "./Pipeable.js" +import { pipeArguments } from "./Pipeable.js" + +const TypeId: unique symbol = Symbol.for("effect/MutableList") as TypeId + +/** + * @since 2.0.0 + * @category symbol + */ +export type TypeId = typeof TypeId + +/** + * @since 2.0.0 + * @category model + */ +export interface MutableList extends Iterable, Pipeable, Inspectable { + readonly [TypeId]: TypeId + + /** @internal */ + head: LinkedListNode | undefined + /** @internal */ + tail: LinkedListNode | undefined +} + +const MutableListProto: Omit, "head" | "tail"> = { + [TypeId]: TypeId, + [Symbol.iterator](this: MutableList): Iterator { + let done = false + let head: LinkedListNode | undefined = this.head + return { + next() { + if (done) { + return this.return!() + } + if (head == null) { + done = true + return this.return!() + } + const value = head.value + head = head.next + return { done, value } + }, + return(value?: unknown) { + if (!done) { + done = true + } + return { done: true, value } + } + } + }, + toString() { + return format(this.toJSON()) + }, + toJSON() { + return { + _id: "MutableList", + values: Array.from(this).map(toJSON) + } + }, + [NodeInspectSymbol]() { + return this.toJSON() + }, + pipe() { + return pipeArguments(this, arguments) + } +} + +interface MutableListImpl extends MutableList { + _length: number +} + +/** @internal */ +interface LinkedListNode { + removed: boolean + value: T + prev: LinkedListNode | undefined + next: LinkedListNode | undefined +} + +/** @internal */ +const makeNode = (value: T): LinkedListNode => ({ + value, + removed: false, + prev: undefined, + next: undefined +}) + +/** + * Creates an empty `MutableList`. + * + * @since 2.0.0 + * @category constructors + */ +export const empty = (): MutableList => { + const list = Object.create(MutableListProto) + list.head = undefined + list.tail = undefined + list._length = 0 + return list +} + +/** + * Creates a new `MutableList` from an iterable collection of values. + * + * @since 2.0.0 + * @category constructors + */ +export const fromIterable = (iterable: Iterable): MutableList => { + const list = empty() + for (const element of iterable) { + append(list, element) + } + return list +} + +/** + * Creates a new `MutableList` from the specified elements. + * + * @since 2.0.0 + * @category constructors + */ +export const make = (...elements: ReadonlyArray): MutableList => fromIterable(elements) + +/** + * Returns `true` if the list contains zero elements, `false`, otherwise. + * + * @since 2.0.0 + * @category getters + */ +export const isEmpty = (self: MutableList): boolean => length(self) === 0 + +/** + * Returns the length of the list. + * + * @since 2.0.0 + * @category getters + */ +export const length = (self: MutableList): number => (self as MutableListImpl)._length + +/** + * Returns the last element of the list, if it exists. + * + * @since 2.0.0 + * @category getters + */ +export const tail = (self: MutableList): A | undefined => self.tail === undefined ? undefined : self.tail.value + +/** + * Returns the first element of the list, if it exists. + * + * @since 2.0.0 + * @category getters + */ +export const head = (self: MutableList): A | undefined => self.head === undefined ? undefined : self.head.value + +/** + * Executes the specified function `f` for each element in the list. + * + * @since 2.0.0 + * @category traversing + */ +export const forEach: { + (f: (element: A) => void): (self: MutableList) => void + (self: MutableList, f: (element: A) => void): void +} = Dual.dual< + (f: (element: A) => void) => (self: MutableList) => void, + (self: MutableList, f: (element: A) => void) => void +>(2, (self, f) => { + let current = self.head + while (current !== undefined) { + f(current.value) + current = current.next + } +}) + +/** + * Removes all elements from the doubly-linked list. + * + * @since 2.0.0 + */ +export const reset = (self: MutableList): MutableList => { + ;(self as MutableListImpl)._length = 0 + self.head = undefined + self.tail = undefined + return self +} + +/** + * Appends the specified element to the end of the `MutableList`. + * + * @category concatenating + * @since 2.0.0 + */ +export const append: { + (value: A): (self: MutableList) => MutableList + (self: MutableList, value: A): MutableList +} = Dual.dual< + (value: A) => (self: MutableList) => MutableList, + (self: MutableList, value: A) => MutableList +>(2, (self: MutableList, value: A) => { + const node = makeNode(value) + if (self.head === undefined) { + self.head = node + } + if (self.tail === undefined) { + self.tail = node + } else { + self.tail.next = node + node.prev = self.tail + self.tail = node + } + ;(self as MutableListImpl)._length += 1 + return self +}) + +/** + * Removes the first value from the list and returns it, if it exists. + * + * @since 0.0.1 + */ +export const shift = (self: MutableList): A | undefined => { + const head = self.head + if (head !== undefined) { + remove(self, head) + return head.value + } + return undefined +} + +/** + * Removes the last value from the list and returns it, if it exists. + * + * @since 0.0.1 + */ +export const pop = (self: MutableList): A | undefined => { + const tail = self.tail + if (tail !== undefined) { + remove(self, tail) + return tail.value + } + return undefined +} + +/** + * Prepends the specified value to the beginning of the list. + * + * @category concatenating + * @since 2.0.0 + */ +export const prepend: { + (value: A): (self: MutableList) => MutableList + (self: MutableList, value: A): MutableList +} = Dual.dual< + (value: A) => (self: MutableList) => MutableList, + (self: MutableList, value: A) => MutableList +>(2, (self: MutableList, value: A) => { + const node = makeNode(value) + node.next = self.head + if (self.head !== undefined) { + self.head.prev = node + } + self.head = node + if (self.tail === undefined) { + self.tail = node + } + ;(self as MutableListImpl)._length += 1 + return self +}) + +const remove = (self: MutableList, node: LinkedListNode): void => { + if (node.removed) { + return + } + node.removed = true + if (node.prev !== undefined && node.next !== undefined) { + node.prev.next = node.next + node.next.prev = node.prev + } else if (node.prev !== undefined) { + self.tail = node.prev + node.prev.next = undefined + } else if (node.next !== undefined) { + self.head = node.next + node.next.prev = undefined + } else { + self.tail = undefined + self.head = undefined + } + if ((self as MutableListImpl)._length > 0) { + ;(self as MutableListImpl)._length -= 1 + } +} diff --git a/repos/effect/packages/effect/src/MutableQueue.ts b/repos/effect/packages/effect/src/MutableQueue.ts new file mode 100644 index 0000000..bbd2326 --- /dev/null +++ b/repos/effect/packages/effect/src/MutableQueue.ts @@ -0,0 +1,227 @@ +/** + * @since 2.0.0 + */ +import * as Chunk from "./Chunk.js" +import * as Dual from "./Function.js" +import { format, type Inspectable, NodeInspectSymbol, toJSON } from "./Inspectable.js" +import * as MutableList from "./MutableList.js" +import type { Pipeable } from "./Pipeable.js" +import { pipeArguments } from "./Pipeable.js" + +const TypeId: unique symbol = Symbol.for("effect/MutableQueue") as TypeId + +/** + * @since 2.0.0 + * @category symbol + */ +export type TypeId = typeof TypeId + +/** + * @since 2.0.0 + * @category symbol + */ +export const EmptyMutableQueue = Symbol.for("effect/mutable/MutableQueue/Empty") + +/** + * @since 2.0.0 + * @category model + */ +export interface MutableQueue extends Iterable, Pipeable, Inspectable { + readonly [TypeId]: TypeId + + /** @internal */ + queue: MutableList.MutableList + /** @internal */ + capacity: number | undefined +} + +/** + * @since 2.0.0 + */ +export declare namespace MutableQueue { + /** + * @since 2.0.0 + */ + export type Empty = typeof EmptyMutableQueue +} + +const MutableQueueProto: Omit, "queue" | "capacity"> = { + [TypeId]: TypeId, + [Symbol.iterator](this: MutableQueue): Iterator { + return Array.from(this.queue)[Symbol.iterator]() + }, + toString() { + return format(this.toJSON()) + }, + toJSON() { + return { + _id: "MutableQueue", + values: Array.from(this).map(toJSON) + } + }, + [NodeInspectSymbol]() { + return this.toJSON() + }, + pipe() { + return pipeArguments(this, arguments) + } +} + +const make = (capacity: number | undefined): MutableQueue => { + const queue = Object.create(MutableQueueProto) + queue.queue = MutableList.empty() + queue.capacity = capacity + return queue +} + +/** + * Creates a new bounded `MutableQueue`. + * + * @since 2.0.0 + * @category constructors + */ +export const bounded = (capacity: number): MutableQueue => make(capacity) + +/** + * Creates a new unbounded `MutableQueue`. + * + * @since 2.0.0 + * @category constructors + */ +export const unbounded = (): MutableQueue => make(undefined) + +/** + * Returns the current number of elements in the queue. + * + * @since 2.0.0 + * @category getters + */ +export const length = (self: MutableQueue): number => MutableList.length(self.queue) + +/** + * Returns `true` if the queue is empty, `false` otherwise. + * + * @since 2.0.0 + * @category getters + */ +export const isEmpty = (self: MutableQueue): boolean => MutableList.isEmpty(self.queue) + +/** + * Returns `true` if the queue is full, `false` otherwise. + * + * @since 2.0.0 + * @category getters + */ +export const isFull = (self: MutableQueue): boolean => + self.capacity === undefined ? false : MutableList.length(self.queue) === self.capacity + +/** + * The **maximum** number of elements that a queue can hold. + * + * **Note**: unbounded queues can still implement this interface with + * `capacity = Infinity`. + * + * @since 2.0.0 + * @category getters + */ +export const capacity = (self: MutableQueue): number => self.capacity === undefined ? Infinity : self.capacity + +/** + * Offers an element to the queue. + * + * Returns whether the enqueue was successful or not. + * + * @since 2.0.0 + */ +export const offer: { + (self: MutableQueue, value: A): boolean + (value: A): (self: MutableQueue) => boolean +} = Dual.dual< + (value: A) => (self: MutableQueue) => boolean, + (self: MutableQueue, value: A) => boolean +>(2, (self: MutableQueue, value: A) => { + const queueLength = MutableList.length(self.queue) + if (self.capacity !== undefined && queueLength === self.capacity) { + return false + } + MutableList.append(value)(self.queue) + return true +}) + +/** + * Enqueues a collection of values into the queue. + * + * Returns a `Chunk` of the values that were **not** able to be enqueued. + * + * @since 2.0.0 + */ +export const offerAll: { + (values: Iterable): (self: MutableQueue) => Chunk.Chunk + (self: MutableQueue, values: Iterable): Chunk.Chunk +} = Dual.dual< + (values: Iterable) => (self: MutableQueue) => Chunk.Chunk, + (self: MutableQueue, values: Iterable) => Chunk.Chunk +>(2, (self: MutableQueue, values: Iterable) => { + const iterator = values[Symbol.iterator]() + let next: IteratorResult | undefined + let remainder = Chunk.empty() + let offering = true + while (offering && (next = iterator.next()) && !next.done) { + offering = offer(next.value)(self) + } + while (next != null && !next.done) { + remainder = Chunk.prepend(next.value)(remainder) + next = iterator.next() + } + return Chunk.reverse(remainder) +}) + +/** + * Dequeues an element from the queue. + * + * Returns either an element from the queue, or the `def` param. + * + * **Note**: if there is no meaningful default for your type, you can always + * use `poll(MutableQueue.EmptyMutableQueue)`. + * + * @since 2.0.0 + */ +export const poll: { + (def: D): (self: MutableQueue) => D | A + (self: MutableQueue, def: D): A | D +} = Dual.dual< + (def: D) => (self: MutableQueue) => A | D, + (self: MutableQueue, def: D) => A | D +>(2, (self, def) => { + if (MutableList.isEmpty(self.queue)) { + return def + } + return MutableList.shift(self.queue)! +}) + +/** + * Dequeues up to `n` elements from the queue. + * + * Returns a `List` of up to `n` elements. + * + * @since 2.0.0 + */ +export const pollUpTo: { + (n: number): (self: MutableQueue) => Chunk.Chunk + (self: MutableQueue, n: number): Chunk.Chunk +} = Dual.dual< + (n: number) => (self: MutableQueue) => Chunk.Chunk, + (self: MutableQueue, n: number) => Chunk.Chunk +>(2, (self: MutableQueue, n: number) => { + let result = Chunk.empty() + let count = 0 + while (count < n) { + const element = poll(EmptyMutableQueue)(self) + if (element === EmptyMutableQueue) { + break + } + result = Chunk.prepend(element)(result) + count += 1 + } + return Chunk.reverse(result) +}) diff --git a/repos/effect/packages/effect/src/MutableRef.ts b/repos/effect/packages/effect/src/MutableRef.ts new file mode 100644 index 0000000..3118f1f --- /dev/null +++ b/repos/effect/packages/effect/src/MutableRef.ts @@ -0,0 +1,202 @@ +/** + * @since 2.0.0 + */ +import * as Equal from "./Equal.js" +import * as Dual from "./Function.js" +import { format, type Inspectable, NodeInspectSymbol, toJSON } from "./Inspectable.js" +import type { Pipeable } from "./Pipeable.js" +import { pipeArguments } from "./Pipeable.js" + +const TypeId: unique symbol = Symbol.for("effect/MutableRef") as TypeId + +/** + * @since 2.0.0 + * @category symbol + */ +export type TypeId = typeof TypeId + +/** + * @since 2.0.0 + * @category models + */ +export interface MutableRef extends Pipeable, Inspectable { + readonly [TypeId]: TypeId + current: T +} + +const MutableRefProto: Omit, "current"> = { + [TypeId]: TypeId, + toString(this: MutableRef): string { + return format(this.toJSON()) + }, + toJSON(this: MutableRef) { + return { + _id: "MutableRef", + current: toJSON(this.current) + } + }, + [NodeInspectSymbol]() { + return this.toJSON() + }, + pipe() { + return pipeArguments(this, arguments) + } +} + +/** + * @since 2.0.0 + * @category constructors + */ +export const make = (value: T): MutableRef => { + const ref = Object.create(MutableRefProto) + ref.current = value + return ref +} + +/** + * @since 2.0.0 + * @category general + */ +export const compareAndSet: { + (oldValue: T, newValue: T): (self: MutableRef) => boolean + (self: MutableRef, oldValue: T, newValue: T): boolean +} = Dual.dual< + (oldValue: T, newValue: T) => (self: MutableRef) => boolean, + (self: MutableRef, oldValue: T, newValue: T) => boolean +>(3, (self, oldValue, newValue) => { + if (Equal.equals(oldValue, self.current)) { + self.current = newValue + return true + } + return false +}) + +/** + * @since 2.0.0 + * @category numeric + */ +export const decrement = (self: MutableRef): MutableRef => update(self, (n) => n - 1) + +/** + * @since 2.0.0 + * @category numeric + */ +export const decrementAndGet = (self: MutableRef): number => updateAndGet(self, (n) => n - 1) + +/** + * @since 2.0.0 + * @category general + */ +export const get = (self: MutableRef): T => self.current + +/** + * @since 2.0.0 + * @category numeric + */ +export const getAndDecrement = (self: MutableRef): number => getAndUpdate(self, (n) => n - 1) + +/** + * @since 2.0.0 + * @category numeric + */ +export const getAndIncrement = (self: MutableRef): number => getAndUpdate(self, (n) => n + 1) + +/** + * @since 2.0.0 + * @category general + */ +export const getAndSet: { + (value: T): (self: MutableRef) => T + (self: MutableRef, value: T): T +} = Dual.dual< + (value: T) => (self: MutableRef) => T, + (self: MutableRef, value: T) => T +>(2, (self, value) => { + const ret = self.current + self.current = value + return ret +}) + +/** + * @since 2.0.0 + * @category general + */ +export const getAndUpdate: { + (f: (value: T) => T): (self: MutableRef) => T + (self: MutableRef, f: (value: T) => T): T +} = Dual.dual< + (f: (value: T) => T) => (self: MutableRef) => T, + (self: MutableRef, f: (value: T) => T) => T +>(2, (self, f) => getAndSet(self, f(get(self)))) + +/** + * @since 2.0.0 + * @category numeric + */ +export const increment = (self: MutableRef): MutableRef => update(self, (n) => n + 1) + +/** + * @since 2.0.0 + * @category numeric + */ +export const incrementAndGet = (self: MutableRef): number => updateAndGet(self, (n) => n + 1) + +/** + * @since 2.0.0 + * @category general + */ +export const set: { + (value: T): (self: MutableRef) => MutableRef + (self: MutableRef, value: T): MutableRef +} = Dual.dual< + (value: T) => (self: MutableRef) => MutableRef, + (self: MutableRef, value: T) => MutableRef +>(2, (self, value) => { + self.current = value + return self +}) + +/** + * @since 2.0.0 + * @category general + */ +export const setAndGet: { + (value: T): (self: MutableRef) => T + (self: MutableRef, value: T): T +} = Dual.dual< + (value: T) => (self: MutableRef) => T, + (self: MutableRef, value: T) => T +>(2, (self, value) => { + self.current = value + return self.current +}) + +/** + * @since 2.0.0 + * @category general + */ +export const update: { + (f: (value: T) => T): (self: MutableRef) => MutableRef + (self: MutableRef, f: (value: T) => T): MutableRef +} = Dual.dual< + (f: (value: T) => T) => (self: MutableRef) => MutableRef, + (self: MutableRef, f: (value: T) => T) => MutableRef +>(2, (self, f) => set(self, f(get(self)))) + +/** + * @since 2.0.0 + * @category general + */ +export const updateAndGet: { + (f: (value: T) => T): (self: MutableRef) => T + (self: MutableRef, f: (value: T) => T): T +} = Dual.dual< + (f: (value: T) => T) => (self: MutableRef) => T, + (self: MutableRef, f: (value: T) => T) => T +>(2, (self, f) => setAndGet(self, f(get(self)))) + +/** + * @since 2.0.0 + * @category boolean + */ +export const toggle = (self: MutableRef): MutableRef => update(self, (_) => !_) diff --git a/repos/effect/packages/effect/src/NonEmptyIterable.ts b/repos/effect/packages/effect/src/NonEmptyIterable.ts new file mode 100644 index 0000000..c798360 --- /dev/null +++ b/repos/effect/packages/effect/src/NonEmptyIterable.ts @@ -0,0 +1,32 @@ +/** + * @since 2.0.0 + */ + +/** + * @category symbol + * @since 2.0.0 + */ +export declare const nonEmpty: unique symbol + +/** + * @category model + * @since 2.0.0 + */ +export interface NonEmptyIterable extends Iterable { + readonly [nonEmpty]: A +} + +/** + * @category getters + * @since 2.0.0 + */ +export const unprepend = (self: NonEmptyIterable): [firstElement: A, remainingElements: Iterator] => { + const iterator = self[Symbol.iterator]() + const next = iterator.next() + if (next.done) { + throw new Error( + "BUG: NonEmptyIterator should not be empty - please report an issue at https://github.com/Effect-TS/effect/issues" + ) + } + return [next.value, iterator] +} diff --git a/repos/effect/packages/effect/src/Number.ts b/repos/effect/packages/effect/src/Number.ts new file mode 100644 index 0000000..829c3e3 --- /dev/null +++ b/repos/effect/packages/effect/src/Number.ts @@ -0,0 +1,1071 @@ +/** + * # Number + * + * This module provides utility functions and type class instances for working + * with the `number` type in TypeScript. It includes functions for basic + * arithmetic operations, as well as type class instances for `Equivalence` and + * `Order`. + * + * ## Operations Reference + * + * | Category | Operation | Description | Domain | Co-domain | + * | ------------ | ------------------------------------------ | ------------------------------------------------------- | ------------------------------ | --------------------- | + * | constructors | {@link module:Number.parse} | Safely parses a string to a number | `string` | `Option` | + * | | | | | | + * | math | {@link module:Number.sum} | Adds two numbers | `number`, `number` | `number` | + * | math | {@link module:Number.sumAll} | Sums all numbers in a collection | `Iterable` | `number` | + * | math | {@link module:Number.subtract} | Subtracts one number from another | `number`, `number` | `number` | + * | math | {@link module:Number.multiply} | Multiplies two numbers | `number`, `number` | `number` | + * | math | {@link module:Number.multiplyAll} | Multiplies all numbers in a collection | `Iterable` | `number` | + * | math | {@link module:Number.divide} | Safely divides handling division by zero | `number`, `number` | `Option` | + * | math | {@link module:Number.unsafeDivide} | Divides but misbehaves for division by zero | `number`, `number` | `number` | + * | math | {@link module:Number.remainder} | Calculates remainder of division | `number`, `number` | `number` | + * | math | {@link module:Number.increment} | Adds 1 to a number | `number` | `number` | + * | math | {@link module:Number.decrement} | Subtracts 1 from a number | `number` | `number` | + * | math | {@link module:Number.sign} | Determines the sign of a number | `number` | `Ordering` | + * | math | {@link module:Number.nextPow2} | Finds the next power of 2 | `number` | `number` | + * | math | {@link module:Number.round} | Rounds a number with specified precision | `number`, `number` | `number` | + * | | | | | | + * | predicates | {@link module:Number.between} | Checks if a number is in a range | `number`, `{minimum, maximum}` | `boolean` | + * | predicates | {@link module:Number.lessThan} | Checks if one number is less than another | `number`, `number` | `boolean` | + * | predicates | {@link module:Number.lessThanOrEqualTo} | Checks if one number is less than or equal | `number`, `number` | `boolean` | + * | predicates | {@link module:Number.greaterThan} | Checks if one number is greater than another | `number`, `number` | `boolean` | + * | predicates | {@link module:Number.greaterThanOrEqualTo} | Checks if one number is greater or equal | `number`, `number` | `boolean` | + * | | | | | | + * | guards | {@link module:Number.isNumber} | Type guard for JavaScript numbers | `unknown` | `boolean` | + * | | | | | | + * | comparison | {@link module:Number.min} | Returns the minimum of two numbers | `number`, `number` | `number` | + * | comparison | {@link module:Number.max} | Returns the maximum of two numbers | `number`, `number` | `number` | + * | comparison | {@link module:Number.clamp} | Restricts a number to a range | `number`, `{minimum, maximum}` | `number` | + * | | | | | | + * | instances | {@link module:Number.Equivalence} | Equivalence instance for numbers | | `Equivalence` | + * | instances | {@link module:Number.Order} | Order instance for numbers | | `Order` | + * | | | | | | + * | errors | {@link module:Number.DivisionByZeroError} | Error thrown by unsafeDivide | | | + * + * ## Composition Patterns and Type Safety + * + * When building function pipelines, understanding how types flow through + * operations is critical: + * + * ### Composing with type-preserving operations + * + * Most operations in this module are type-preserving (`number → number`), + * making them easily composable in pipelines: + * + * ```ts + * import { pipe } from "effect" + * import * as Number from "effect/Number" + * + * const result = pipe( + * 10, + * Number.increment, // number → number + * Number.multiply(2), // number → number + * Number.round(1) // number → number + * ) // Result: number (21) + * ``` + * + * ### Working with Option results + * + * Operations that might fail (like division by zero) return Option types and + * require Option combinators: + * + * ```ts + * import { pipe, Option } from "effect" + * import * as Number from "effect/Number" + * + * const result = pipe( + * 10, + * Number.divide(0), // number → Option + * Option.getOrElse(() => 0) // Option → number + * ) // Result: number (0) + * ``` + * + * ### Composition best practices + * + * - Chain type-preserving operations for maximum composability + * - Use Option combinators when working with potentially failing operations + * - Consider using Effect for operations that might fail with specific errors + * - Remember that all operations maintain JavaScript's floating-point precision + * limitations + * + * @module Number + * @since 2.0.0 + * @see {@link module:BigInt} for more similar operations on `bigint` types + * @see {@link module:BigDecimal} for more similar operations on `BigDecimal` types + */ + +import * as equivalence from "./Equivalence.js" +import { dual } from "./Function.js" +import * as option from "./internal/option.js" +import * as _Iterable from "./Iterable.js" +import type { Option } from "./Option.js" +import * as order from "./Order.js" +import type { Ordering } from "./Ordering.js" +import * as predicate from "./Predicate.js" + +/** + * Type guard that tests if a value is a member of the set of JavaScript + * numbers. + * + * @memberof Number + * @since 2.0.0 + * @category guards + * @example + * + * ```ts + * import * as assert from "node:assert/strict" + * import * as Number from "effect/Number" + * + * // Regular numbers + * assert.equal(Number.isNumber(2), true) + * assert.equal(Number.isNumber(-3.14), true) + * assert.equal(Number.isNumber(0), true) + * + * // Special numeric values + * assert.equal(Number.isNumber(Infinity), true) + * assert.equal(Number.isNumber(NaN), true) + * + * // Non-number values + * assert.equal(Number.isNumber("2"), false) + * assert.equal(Number.isNumber(true), false) + * assert.equal(Number.isNumber(null), false) + * assert.equal(Number.isNumber(undefined), false) + * assert.equal(Number.isNumber({}), false) + * assert.equal(Number.isNumber([]), false) + * + * // Using as a type guard in conditionals + * function processValue(value: unknown): string { + * if (Number.isNumber(value)) { + * // TypeScript now knows 'value' is a number + * return `Numeric value: ${value.toFixed(2)}` + * } + * return "Not a number" + * } + * + * assert.strictEqual(processValue(42), "Numeric value: 42.00") + * assert.strictEqual(processValue("hello"), "Not a number") + * + * // Filtering for numbers in an array + * const mixed = [1, "two", 3, false, 5] + * const onlyNumbers = mixed.filter(Number.isNumber) + * assert.equal(onlyNumbers, [1, 3, 5]) + * ``` + * + * @param input - The value to test for membership in the set of JavaScript + * numbers + * + * @returns `true` if the input is a JavaScript number, `false` otherwise + */ +export const isNumber: (input: unknown) => input is number = predicate.isNumber + +/** + * Returns the additive inverse of a number, effectively negating it. + * + * @memberof Number + * @since 3.14.6 + * @example + * + * ```ts + * import * as assert from "node:assert/strict" + * import { pipe } from "effect" + * import * as Number from "effect/Number" + * + * assert.equal( + * Number.negate(5), // + * -5 + * ) + * + * assert.equal( + * Number.negate(-5), // + * 5 + * ) + * + * assert.equal( + * Number.negate(0), // + * 0 + * ) + * ``` + * + * @param n - The number value to be negated. + * + * @returns The negated number value. + */ +export const negate = (n: number): number => multiply(n, -1) + +/** + * Performs addition in the set of JavaScript numbers. + * + * @memberof Number + * @since 2.0.0 + * @category math + * @example + * + * ```ts + * import * as assert from "node:assert/strict" + * import { pipe } from "effect" + * import * as Number from "effect/Number" + * + * // Data-first style (direct application) + * assert.equal(Number.sum(2, 3), 5) + * assert.equal(Number.sum(-10, 5), -5) + * assert.equal(Number.sum(0.1, 0.2), 0.30000000000000004) // Note: floating-point precision limitation + * + * // Data-last style (pipeable) + * assert.equal( + * pipe( + * 10, + * Number.sum(5) // 10 + 5 = 15 + * ), + * 15 + * ) + * + * // Chaining multiple additions + * assert.equal( + * pipe( + * 1, + * Number.sum(2), // 1 + 2 = 3 + * Number.sum(3), // 3 + 3 = 6 + * Number.sum(4) // 6 + 4 = 10 + * ), + * 10 + * ) + * + * // Identity property: a + 0 = a + * assert.equal(Number.sum(42, 0), 42) + * + * // Commutative property: a + b = b + a + * assert.equal(Number.sum(5, 3), Number.sum(3, 5)) + * ``` + */ +export const sum: { + /** + * Returns a function that adds a specified number to its argument. + * + * @param that - The number to add to the input of the resulting function + * + * @returns A function that takes a number and returns the sum of that number + * and `that` + */ + (that: number): (self: number) => number + + /** + * Adds two numbers together. + * + * @param self - The first addend + * @param that - The second addend + * + * @returns The sum of the two numbers + */ + (self: number, that: number): number +} = dual(2, (self: number, that: number): number => self + that) + +/** + * Computes the sum of all elements in an iterable collection of numbers. + * + * @memberof Number + * @since 2.0.0 + * @category math + * @example + * + * ```ts + * import * as assert from "node:assert/strict" + * import * as Number from "effect/Number" + * + * // Basic sums + * assert.equal(Number.sumAll([2, 3, 4]), 9) // 2 + 3 + 4 = 9 + * assert.equal(Number.sumAll([1.1, 2.2, 3.3]), 6.6) // 1.1 + 2.2 + 3.3 = 6.6 + * + * // Empty collection returns the additive identity (0) + * assert.equal(Number.sumAll([]), 0) + * + * // Single element collection + * assert.equal(Number.sumAll([42]), 42) + * + * // Sums with negative numbers + * assert.equal(Number.sumAll([2, -3, 4]), 3) // 2 + (-3) + 4 = 3 + * assert.equal(Number.sumAll([-2, -3, -4]), -9) // (-2) + (-3) + (-4) = -9 + * + * // Works with any iterable + * assert.equal(Number.sumAll(new Set([2, 3, 4])), 9) + * + * // Using with generated sequences + * function* range(start: number, end: number) { + * for (let i = start; i <= end; i++) yield i + * } + * + * // Compute sum of first 5 natural numbers: 1 + 2 + 3 + 4 + 5 = 15 + * assert.equal(Number.sumAll(range(1, 5)), 15) + * + * // Floating point precision example + * assert.equal( + * Number.sumAll([0.1, 0.2]), + * 0.30000000000000004 // Note IEEE 754 precision limitation + * ) + * ``` + * + * @param collection - An `iterable` containing the `numbers` to sum + * + * @returns The sum of all numbers in the collection, or 0 if the collection is + * empty + */ +export const sumAll = (collection: Iterable): number => _Iterable.reduce(collection, 0, sum) + +/** + * Performs subtraction in the set of JavaScript numbers. + * + * @memberof Number + * @since 2.0.0 + * @category math + * @example + * + * ```ts + * import * as assert from "node:assert/strict" + * import { pipe } from "effect" + * import * as Number from "effect/Number" + * + * // Data-first style (direct application) + * assert.equal(Number.subtract(2, 3), -1) // 2 - 3 = -1 + * assert.equal(Number.subtract(10, 5), 5) // 10 - 5 = 5 + * assert.equal(Number.subtract(0.3, 0.1), 0.19999999999999998) // Note: floating-point precision limitation + * + * // Data-last style (pipeable) + * assert.equal( + * pipe( + * 10, + * Number.subtract(5) // 10 - 5 = 5 + * ), + * 5 + * ) + * + * // Chaining multiple subtractions + * assert.equal( + * pipe( + * 20, + * Number.subtract(5), // 20 - 5 = 15 + * Number.subtract(3), // 15 - 3 = 12 + * Number.subtract(2) // 12 - 2 = 10 + * ), + * 10 + * ) + * + * // Right identity property: a - 0 = a + * assert.equal(Number.subtract(42, 0), 42) + * + * // Self-annihilation property: a - a = 0 + * assert.equal(Number.subtract(42, 42), 0) + * + * // Non-commutative property: a - b ≠ b - a + * assert.equal(Number.subtract(5, 3), 2) // 5 - 3 = 2 + * assert.equal(Number.subtract(3, 5), -2) // 3 - 5 = -2 + * + * // Inverse relation: a - b = -(b - a) + * assert.equal(Number.subtract(5, 3), -Number.subtract(3, 5)) + * ``` + */ +export const subtract: { + /** + * Returns a function that subtracts a specified number from its argument. + * + * @param subtrahend - The number to subtract from the input of the resulting + * function + * + * @returns A function that takes a minuend and returns the difference of + * subtracting the subtrahend from it + */ + (subtrahend: number): (minuend: number) => number + + /** + * Subtracts the subtrahend from the minuend and returns the difference. + * + * @param minuend - The number from which another number is to be subtracted + * @param subtrahend - The number to subtract from the minuend + * + * @returns The difference of the minuend minus the subtrahend + */ + (minuend: number, subtrahend: number): number +} = dual( + 2, + (minuend: number, subtrahend: number): number => minuend - subtrahend +) + +/** + * Performs **multiplication** in the set of JavaScript numbers. + * + * @memberof Number + * @since 2.0.0 + * @category math + * @example + * + * ```ts + * import * as assert from "node:assert/strict" + * import { pipe } from "effect" + * import * as Number from "effect/Number" + * + * // Data-first style (direct application) + * assert.equal(Number.multiply(2, 3), 6) // 2 × 3 = 6 + * assert.equal(Number.multiply(-4, 5), -20) // (-4) × 5 = -20 + * assert.equal(Number.multiply(-3, -2), 6) // (-3) × (-2) = 6 + * assert.equal(Number.multiply(0.1, 0.2), 0.020000000000000004) // Note: floating-point precision limitation + * + * // Data-last style (pipeable) + * assert.equal( + * pipe( + * 10, + * Number.multiply(5) // 10 × 5 = 50 + * ), + * 50 + * ) + * + * // Chaining multiple multiplications + * assert.equal( + * pipe( + * 2, + * Number.multiply(3), // 2 × 3 = 6 + * Number.multiply(4), // 6 × 4 = 24 + * Number.multiply(0.5) // 24 × 0.5 = 12 + * ), + * 12 + * ) + * + * // Identity property: a × 1 = a + * assert.equal(Number.multiply(42, 1), 42) + * + * // Zero property: a × 0 = 0 + * assert.equal(Number.multiply(42, 0), 0) + * + * // Commutative property: a × b = b × a + * assert.equal(Number.multiply(5, 3), Number.multiply(3, 5)) + * + * // Associative property: (a × b) × c = a × (b × c) + * const a = 2, + * b = 3, + * c = 4 + * assert.equal( + * Number.multiply(Number.multiply(a, b), c), + * Number.multiply(a, Number.multiply(b, c)) + * ) + * ``` + */ +export const multiply: { + /** + * Returns a function that multiplies a specified number with its argument. + * + * @param multiplicand - The number to multiply with the input of the + * resulting function + * + * @returns A function that takes a multiplier and returns the product of that + * multiplier and the multiplicand + */ + (multiplicand: number): (multiplier: number) => number + + /** + * Multiplies two numbers together. + * + * @param multiplier - The first factor + * @param multiplicand - The second factor + * + * @returns The product of the two numbers + */ + (multiplier: number, multiplicand: number): number +} = dual( + 2, + (multiplier: number, multiplicand: number): number => multiplier * multiplicand +) + +/** + * Computes the product of all elements in an iterable collection of numbers. + * + * @memberof Number + * @since 2.0.0 + * @category math + * @example + * + * ```ts + * import * as assert from "node:assert/strict" + * import * as Number from "effect/Number" + * + * // Basic products + * assert.equal(Number.multiplyAll([2, 3, 4]), 24) // 2 × 3 × 4 = 24 + * assert.equal(Number.multiplyAll([1.5, 2, 3]), 9) // 1.5 × 2 × 3 = 9 + * + * // Empty collection returns the multiplicative identity (1) + * assert.equal(Number.multiplyAll([]), 1) + * + * // Single element collection + * assert.equal(Number.multiplyAll([42]), 42) + * + * // Products with negative numbers + * assert.equal(Number.multiplyAll([2, -3, 4]), -24) // 2 × (-3) × 4 = -24 + * assert.equal(Number.multiplyAll([-2, -3]), 6) // (-2) × (-3) = 6 + * + * // Zero property - if any element is zero, product is zero + * assert.equal(Number.multiplyAll([2, 0, 3]), 0) + * + * // Works with any iterable + * assert.equal(Number.multiplyAll(new Set([2, 3, 4])), 24) + * + * // Using with generated sequences + * function* range(start: number, end: number) { + * for (let i = start; i <= end; i++) yield i + * } + * + * // Compute factorial: 5! = 5 × 4 × 3 × 2 × 1 = 120 + * assert.equal(Number.multiplyAll(range(1, 5)), 120) + * ``` + * + * @param collection - An `iterable` containing the `numbers` to multiply + * + * @returns The product of all numbers in the collection, or 1 if the collection + * is empty + */ +export const multiplyAll = (collection: Iterable): number => { + let out = 1 + for (const n of collection) { + if (n === 0) { + return 0 + } + out *= n + } + return out +} + +/** + * Performs division in the set of JavaScript numbers, returning the result + * wrapped in an `Option` to handle division by zero. + * + * @memberof Number + * @since 2.0.0 + * @category math + * @example + * + * ```ts + * import * as assert from "node:assert/strict" + * import { pipe, Option } from "effect" + * import * as Number from "effect/Number" + * + * // Data-first style (direct application) + * assert.equal(Number.divide(6, 3), Option.some(2)) // 6 ÷ 3 = 2 + * assert.equal(Number.divide(-8, 4), Option.some(-2)) // (-8) ÷ 4 = -2 + * assert.equal(Number.divide(-10, -5), Option.some(2)) // (-10) ÷ (-5) = 2 + * assert.equal(Number.divide(1, 3), Option.some(0.3333333333333333)) // Note: floating-point approximation + * + * // Handling division by zero + * assert.equal(Number.divide(6, 0), Option.none()) // 6 ÷ 0 is undefined + * + * // Data-last style (pipeable) + * assert.equal( + * pipe( + * 10, + * Number.divide(2) // 10 ÷ 2 = 5 + * ), + * Option.some(5) + * ) + * + * // Chaining multiple divisions using Option combinators + * assert.equal( + * pipe( + * Option.some(24), + * Option.flatMap((n) => Number.divide(n, 2)), // 24 ÷ 2 = 12 + * Option.flatMap(Number.divide(3)), // 12 ÷ 3 = 4 + * Option.flatMap(Number.divide(2)) // 4 ÷ 2 = 2 + * ), + * Option.some(2) + * ) + * + * // Division-by-one property: a ÷ 1 = a + * assert.equal(Number.divide(42, 1), Option.some(42)) + * + * // Self-division property: a ÷ a = 1 (for a ≠ 0) + * assert.equal(Number.divide(42, 42), Option.some(1)) + * + * // Non-commutative property: a ÷ b ≠ b ÷ a + * assert.notDeepStrictEqual( + * Number.divide(6, 3), // 6 ÷ 3 = 2 + * Number.divide(3, 6) // 3 ÷ 6 = 0.5 + * ) + * ``` + */ +export const divide: { + /** + * Returns a function that divides its input by a specified divisor. + * + * @param divisor - The number to divide by + * + * @returns A function that takes a dividend and returns the quotient wrapped + * in an Option (Option.none() if divisor is 0) + */ + (divisor: number): (dividend: number) => Option + + /** + * Divides the dividend by the divisor and returns the quotient wrapped in an + * Option. + * + * @param dividend - The number to be divided + * @param divisor - The number to divide by + * + * @returns Some(quotient) if the divisor is not 0, None otherwise + */ + (dividend: number, divisor: number): Option +} = dual(2, (dividend: number, divisor: number) => divisor === 0 ? option.none : option.some(dividend / divisor)) + +/** + * Performs division in the set of JavaScript numbers, but misbehaves for + * division by zero. + * + * Unlike {@link module:Number.divide} which returns an Option, this function + * directly returns a number or `Infinity` or `NaN`. + * + * - If the `divisor` is zero, it returns `Infinity`. + * - If both the `dividend` and the `divisor` are zero, then it returns `NaN`. + * + * @memberof Number + * @since 2.0.0 + * @category math + * @example + * + * ```ts + * import * as assert from "node:assert/strict" + * import { pipe } from "effect" + * import * as Number from "effect/Number" + * + * // Data-first style (direct application) + * assert.equal(Number.unsafeDivide(6, 3), 2) // 6 ÷ 3 = 2 + * assert.equal(Number.unsafeDivide(-8, 4), -2) // (-8) ÷ 4 = -2 + * assert.equal(Number.unsafeDivide(-10, -5), 2) // (-10) ÷ (-5) = 2 + * assert.equal(Number.unsafeDivide(1, 3), 0.3333333333333333) + * + * // Data-last style (pipeable) + * assert.equal( + * pipe( + * 10, + * Number.unsafeDivide(2) // 10 ÷ 2 = 5 + * ), + * 5 + * ) + * + * // Chaining multiple divisions + * assert.equal( + * pipe( + * 24, + * Number.unsafeDivide(2), // 24 ÷ 2 = 12 + * Number.unsafeDivide(3), // 12 ÷ 3 = 4 + * Number.unsafeDivide(2) // 4 ÷ 2 = 2 + * ), + * 2 + * ) + * + * assert.equal(Number.unsafeDivide(6, 0), Infinity) + * + * assert.equal(Number.unsafeDivide(0, 0), NaN) + * + * // Compare with safe division + * const safeResult = Number.divide(6, 3) // Option.some(2) + * const unsafeResult = Number.unsafeDivide(6, 3) // 2 directly + * ``` + * + * @throws - An {@link module:Number.DivisionByZeroError} if the divisor is zero. + * @see {@link module:Number.divide} - Safe division returning an Option + */ +export const unsafeDivide: { + /** + * Returns a function that divides its input by a specified divisor. + * + * @param divisor - The number to divide by + * + * @returns A function that takes a dividend and returns the quotient + * @throws - An {@link module:Number.DivisionByZeroError} if the divisor is + * zero + */ + (divisor: number): (dividend: number) => number + + /** + * Divides the dividend by the divisor and returns the quotient. + * + * If the divisor is zero, it returns Infinity. + * + * @param dividend - The number to be divided + * @param divisor - The number to divide by + * + * @returns The quotient of the division + */ + (dividend: number, divisor: number): number +} = dual(2, (dividend: number, divisor: number): number => dividend / divisor) + +/** + * Returns the result of adding `1` to a given number. + * + * @memberof Number + * @since 2.0.0 + * @category math + * @example + * + * ```ts + * import * as assert from "node:assert/strict" + * import { increment } from "effect/Number" + * + * assert.equal(increment(2), 3) + * ``` + */ +export const increment = (n: number): number => sum(n, 1) + +/** + * Decrements a number by `1`. + * + * @memberof Number + * @since 2.0.0 + * @category math + * @example + * + * ```ts + * import * as assert from "node:assert/strict" + * import { decrement } from "effect/Number" + * + * assert.equal(decrement(3), 2) + * ``` + */ +export const decrement = (n: number): number => subtract(n, 1) + +/** + * @memberof Number + * @since 2.0.0 + * @category instances + */ +export const Equivalence: equivalence.Equivalence = equivalence.number + +/** + * @memberof Number + * @since 2.0.0 + * @category instances + */ +export const Order: order.Order = order.number + +/** + * Returns `true` if the first argument is less than the second, otherwise + * `false`. + * + * @memberof Number + * @since 2.0.0 + * @category predicates + * @example + * + * ```ts + * import * as assert from "node:assert/strict" + * import { lessThan } from "effect/Number" + * + * assert.equal(lessThan(2, 3), true) + * assert.equal(lessThan(3, 3), false) + * assert.equal(lessThan(4, 3), false) + * ``` + */ +export const lessThan: { + (that: number): (self: number) => boolean + (self: number, that: number): boolean +} = order.lessThan(Order) + +/** + * Returns a function that checks if a given `number` is less than or equal to + * the provided one. + * + * @memberof Number + * @since 2.0.0 + * @category predicates + * @example + * + * ```ts + * import * as assert from "node:assert/strict" + * import { lessThanOrEqualTo } from "effect/Number" + * + * assert.equal(lessThanOrEqualTo(2, 3), true) + * assert.equal(lessThanOrEqualTo(3, 3), true) + * assert.equal(lessThanOrEqualTo(4, 3), false) + * ``` + */ +export const lessThanOrEqualTo: { + (that: number): (self: number) => boolean + (self: number, that: number): boolean +} = order.lessThanOrEqualTo(Order) + +/** + * Returns `true` if the first argument is greater than the second, otherwise + * `false`. + * + * @memberof Number + * @since 2.0.0 + * @category predicates + * @example + * + * ```ts + * import * as assert from "node:assert/strict" + * import { greaterThan } from "effect/Number" + * + * assert.equal(greaterThan(2, 3), false) + * assert.equal(greaterThan(3, 3), false) + * assert.equal(greaterThan(4, 3), true) + * ``` + */ +export const greaterThan: { + (that: number): (self: number) => boolean + (self: number, that: number): boolean +} = order.greaterThan(Order) + +/** + * Returns a function that checks if a given `number` is greater than or equal + * to the provided one. + * + * @memberof Number + * @since 2.0.0 + * @category predicates + * @example + * + * ```ts + * import * as assert from "node:assert/strict" + * import { greaterThanOrEqualTo } from "effect/Number" + * + * assert.equal(greaterThanOrEqualTo(2, 3), false) + * assert.equal(greaterThanOrEqualTo(3, 3), true) + * assert.equal(greaterThanOrEqualTo(4, 3), true) + * ``` + */ +export const greaterThanOrEqualTo: { + (that: number): (self: number) => boolean + (self: number, that: number): boolean +} = order.greaterThanOrEqualTo(Order) + +/** + * Checks if a `number` is between a `minimum` and `maximum` value (inclusive). + * + * @memberof Number + * @since 2.0.0 + * @category predicates + * @example + * + * ```ts + * import * as assert from "node:assert/strict" + * import { Number } from "effect" + * + * const between = Number.between({ minimum: 0, maximum: 5 }) + * + * assert.equal(between(3), true) + * assert.equal(between(-1), false) + * assert.equal(between(6), false) + * ``` + */ +export const between: { + (options: { minimum: number; maximum: number }): (self: number) => boolean + ( + self: number, + options: { + minimum: number + maximum: number + } + ): boolean +} = order.between(Order) + +/** + * Restricts the given `number` to be within the range specified by the + * `minimum` and `maximum` values. + * + * - If the `number` is less than the `minimum` value, the function returns the + * `minimum` value. + * - If the `number` is greater than the `maximum` value, the function returns the + * `maximum` value. + * - Otherwise, it returns the original `number`. + * + * @memberof Number + * @since 2.0.0 + * @example + * + * ```ts + * import * as assert from "node:assert/strict" + * import { Number } from "effect" + * + * const clamp = Number.clamp({ minimum: 1, maximum: 5 }) + * + * assert.equal(clamp(3), 3) + * assert.equal(clamp(0), 1) + * assert.equal(clamp(6), 5) + * ``` + */ +export const clamp: { + (options: { minimum: number; maximum: number }): (self: number) => number + ( + self: number, + options: { + minimum: number + maximum: number + } + ): number +} = order.clamp(Order) + +/** + * Returns the minimum between two `number`s. + * + * @memberof Number + * @since 2.0.0 + * @example + * + * ```ts + * import * as assert from "node:assert/strict" + * import { min } from "effect/Number" + * + * assert.equal(min(2, 3), 2) + * ``` + */ +export const min: { + (that: number): (self: number) => number + (self: number, that: number): number +} = order.min(Order) + +/** + * Returns the maximum between two `number`s. + * + * @memberof Number + * @since 2.0.0 + * @example + * + * ```ts + * import * as assert from "node:assert/strict" + * import { max } from "effect/Number" + * + * assert.equal(max(2, 3), 3) + * ``` + */ +export const max: { + (that: number): (self: number) => number + (self: number, that: number): number +} = order.max(Order) + +/** + * Determines the sign of a given `number`. + * + * @memberof Number + * @since 2.0.0 + * @category math + * @example + * + * ```ts + * import * as assert from "node:assert/strict" + * import { sign } from "effect/Number" + * + * assert.equal(sign(-5), -1) + * assert.equal(sign(0), 0) + * assert.equal(sign(5), 1) + * ``` + */ +export const sign = (n: number): Ordering => Order(n, 0) + +/** + * Returns the remainder left over when one operand is divided by a second + * operand. + * + * It always takes the sign of the dividend. + * + * @memberof Number + * @since 2.0.0 + * @category math + * @example + * + * ```ts + * import * as assert from "node:assert/strict" + * import { remainder } from "effect/Number" + * + * assert.equal(remainder(2, 2), 0) + * assert.equal(remainder(3, 2), 1) + * assert.equal(remainder(-4, 2), -0) + * ``` + */ +export const remainder: { + (divisor: number): (dividend: number) => number + (dividend: number, divisor: number): number +} = dual(2, (dividend: number, divisor: number): number => { + // https://stackoverflow.com/questions/3966484/why-does-modulus-operator-return-fractional-number-in-javascript/31711034#31711034 + const selfDecCount = (dividend.toString().split(".")[1] || "").length + const divisorDecCount = (divisor.toString().split(".")[1] || "").length + const decCount = selfDecCount > divisorDecCount ? selfDecCount : divisorDecCount + const selfInt = parseInt(dividend.toFixed(decCount).replace(".", "")) + const divisorInt = parseInt(divisor.toFixed(decCount).replace(".", "")) + return (selfInt % divisorInt) / Math.pow(10, decCount) +}) + +/** + * Returns the next power of 2 greater than or equal to the given number. + * + * - For `positive` inputs, returns the smallest power of 2 that is >= the input + * - For `zero`, returns 2 + * - For `negative` inputs, returns NaN (as logarithms of negative numbers are + * undefined) + * - For `NaN` input, returns NaN + * - For `Infinity`, returns Infinity + * + * @memberof Number + * @since 2.0.0 + * @category math + * @example + * + * ```ts + * import * as assert from "node:assert/strict" + * import { nextPow2 } from "effect/Number" + * + * assert.equal(nextPow2(5), 8) + * assert.equal(nextPow2(17), 32) + * assert.equal(nextPow2(0), 2) + * assert.equal(Number.isNaN(nextPow2(-1)), true) // Negative inputs result in NaN + * ``` + */ +export const nextPow2 = (n: number): number => { + const nextPow = Math.ceil(Math.log(n) / Math.log(2)) + return Math.max(Math.pow(2, nextPow), 2) +} + +/** + * Tries to parse a `number` from a `string` using the `Number()` function. The + * following special string values are supported: "NaN", "Infinity", + * "-Infinity". + * + * @memberof Number + * @since 2.0.0 + * @category constructors + */ +export const parse: { + (s: string): Option +} = (s) => { + if (s === "NaN") { + return option.some(NaN) + } + if (s === "Infinity") { + return option.some(Infinity) + } + if (s === "-Infinity") { + return option.some(-Infinity) + } + if (s.trim() === "") { + return option.none + } + const n = Number(s) + return Number.isNaN(n) ? option.none : option.some(n) +} + +/** + * Returns the number rounded with the given precision. + * + * @memberof Number + * @since 3.8.0 + * @category math + * @example + * + * ```ts + * import * as assert from "node:assert/strict" + * import { round } from "effect/Number" + * + * assert.equal(round(1.1234, 2), 1.12) + * assert.equal(round(1.567, 2), 1.57) + * ``` + */ +export const round: { + (precision: number): (self: number) => number + (self: number, precision: number): number +} = dual(2, (self: number, precision: number): number => { + const factor = Math.pow(10, precision) + return Math.round(self * factor) / factor +}) diff --git a/repos/effect/packages/effect/src/Option.ts b/repos/effect/packages/effect/src/Option.ts new file mode 100644 index 0000000..9edcb23 --- /dev/null +++ b/repos/effect/packages/effect/src/Option.ts @@ -0,0 +1,2170 @@ +/** + * @since 2.0.0 + */ +import type { Either } from "./Either.js" +import * as Equal from "./Equal.js" +import * as Equivalence from "./Equivalence.js" +import type { LazyArg } from "./Function.js" +import { constNull, constUndefined, dual, identity, isFunction } from "./Function.js" +import type { TypeLambda } from "./HKT.js" +import type { Inspectable } from "./Inspectable.js" +import * as doNotation from "./internal/doNotation.js" +import * as either from "./internal/either.js" +import * as option from "./internal/option.js" +import type { Order } from "./Order.js" +import * as order from "./Order.js" +import type { Pipeable } from "./Pipeable.js" +import type { Predicate, Refinement } from "./Predicate.js" +import type { Covariant, NoInfer, NotFunction } from "./Types.js" +import type * as Unify from "./Unify.js" +import * as Gen from "./Utils.js" + +/** + * The `Option` data type represents optional values. An `Option` can either + * be `Some`, containing a value of type `A`, or `None`, representing the + * absence of a value. + * + * **When to Use** + * + * You can use `Option` in scenarios like: + * + * - Using it for initial values + * - Returning values from functions that are not defined for all possible + * inputs (referred to as “partial functions”) + * - Managing optional fields in data structures + * - Handling optional function arguments + * + * @category Models + * @since 2.0.0 + */ +export type Option = None | Some + +/** + * @category Symbols + * @since 2.0.0 + */ +export const TypeId: unique symbol = Symbol.for("effect/Option") + +/** + * @category Symbols + * @since 2.0.0 + */ +export type TypeId = typeof TypeId + +/** + * @category Models + * @since 2.0.0 + */ +export interface None extends Pipeable, Inspectable { + readonly _tag: "None" + readonly _op: "None" + readonly [TypeId]: { + readonly _A: Covariant + } + [Unify.typeSymbol]?: unknown + [Unify.unifySymbol]?: OptionUnify + [Unify.ignoreSymbol]?: OptionUnifyIgnore +} + +/** + * @category Models + * @since 2.0.0 + */ +export interface Some extends Pipeable, Inspectable { + readonly _tag: "Some" + readonly _op: "Some" + readonly value: A + readonly [TypeId]: { + readonly _A: Covariant + } + [Unify.typeSymbol]?: unknown + [Unify.unifySymbol]?: OptionUnify + [Unify.ignoreSymbol]?: OptionUnifyIgnore +} + +/** + * @category Models + * @since 2.0.0 + */ +export interface OptionUnify { + Option?: () => A[Unify.typeSymbol] extends Option | infer _ ? Option : never +} + +/** + * @since 2.0.0 + */ +export declare namespace Option { + /** + * Extracts the type of the value contained in an `Option`. + * + * **Example** (Getting the Value Type of an Option) + * + * ```ts + * import { Option } from "effect" + * + * // Declare an Option holding a string + * declare const myOption: Option.Option + * + * // Extract the type of the value within the Option + * // + * // ┌─── string + * // ▼ + * type MyType = Option.Option.Value + * ``` + * + * @since 2.0.0 + * @category Type-level Utils + */ + export type Value> = [T] extends [Option] ? _A : never +} + +/** + * @category Models + * @since 2.0.0 + */ +export interface OptionUnifyIgnore {} + +/** + * @category Type Lambdas + * @since 2.0.0 + */ +export interface OptionTypeLambda extends TypeLambda { + readonly type: Option +} + +/** + * Represents the absence of a value by creating an empty `Option`. + * + * `Option.none` returns an `Option`, which is a subtype of `Option`. + * This means you can use it in place of any `Option` regardless of the type + * `A`. + * + * **Example** (Creating an Option with No Value) + * + * ```ts + * import { Option } from "effect" + * + * // An Option holding no value + * // + * // ┌─── Option + * // ▼ + * const noValue = Option.none() + * + * console.log(noValue) + * // Output: { _id: 'Option', _tag: 'None' } + * ``` + * + * @see {@link some} for the opposite operation. + * + * @category Constructors + * @since 2.0.0 + */ +export const none = (): Option => option.none + +/** + * Wraps the given value into an `Option` to represent its presence. + * + * **Example** (Creating an Option with a Value) + * + * ```ts + * import { Option } from "effect" + * + * // An Option holding the number 1 + * // + * // ┌─── Option + * // ▼ + * const value = Option.some(1) + * + * console.log(value) + * // Output: { _id: 'Option', _tag: 'Some', value: 1 } + * ``` + * + * @see {@link none} for the opposite operation. + * + * @category Constructors + * @since 2.0.0 + */ +export const some: (value: A) => Option = option.some + +/** + * Determines whether the given value is an `Option`. + * + * **Details** + * + * This function checks if a value is an instance of `Option`. It returns `true` + * if the value is either `Option.some` or `Option.none`, and `false` otherwise. + * This is particularly useful when working with unknown values or when you need + * to ensure type safety in your code. + * + * @example + * ```ts + * import { Option } from "effect" + * + * console.log(Option.isOption(Option.some(1))) + * // Output: true + * + * console.log(Option.isOption(Option.none())) + * // Output: true + * + * console.log(Option.isOption({})) + * // Output: false + * ``` + * + * @category Guards + * @since 2.0.0 + */ +export const isOption: (input: unknown) => input is Option = option.isOption + +/** + * Checks whether an `Option` represents the absence of a value (`None`). + * + * @example + * ```ts + * import { Option } from "effect" + * + * console.log(Option.isNone(Option.some(1))) + * // Output: false + * + * console.log(Option.isNone(Option.none())) + * // Output: true + * ``` + * + * @see {@link isSome} for the opposite check. + * + * @category Guards + * @since 2.0.0 + */ +export const isNone: (self: Option) => self is None = option.isNone + +/** + * Checks whether an `Option` contains a value (`Some`). + * + * @example + * ```ts + * import { Option } from "effect" + * + * console.log(Option.isSome(Option.some(1))) + * // Output: true + * + * console.log(Option.isSome(Option.none())) + * // Output: false + * ``` + * + * @see {@link isNone} for the opposite check. + * + * @category Guards + * @since 2.0.0 + */ +export const isSome: (self: Option) => self is Some = option.isSome + +/** + * Performs pattern matching on an `Option` to handle both `Some` and `None` + * cases. + * + * **Details** + * + * This function allows you to match against an `Option` and handle both + * scenarios: when the `Option` is `None` (i.e., contains no value), and when + * the `Option` is `Some` (i.e., contains a value). It executes one of the + * provided functions based on the case: + * + * - If the `Option` is `None`, the `onNone` function is executed and its result + * is returned. + * - If the `Option` is `Some`, the `onSome` function is executed with the + * contained value, and its result is returned. + * + * This function provides a concise and functional way to handle optional values + * without resorting to `if` or manual checks, making your code more declarative + * and readable. + * + * **Example** (Pattern Matching with Option) + * + * ```ts + * import { Option } from "effect" + * + * const foo = Option.some(1) + * + * const message = Option.match(foo, { + * onNone: () => "Option is empty", + * onSome: (value) => `Option has a value: ${value}` + * }) + * + * console.log(message) + * // Output: "Option has a value: 1" + * ``` + * + * @category Pattern matching + * @since 2.0.0 + */ +export const match: { + (options: { + readonly onNone: LazyArg + readonly onSome: (a: A) => C + }): (self: Option) => B | C + (self: Option, options: { + readonly onNone: LazyArg + readonly onSome: (a: A) => C + }): B | C +} = dual( + 2, + (self: Option, { onNone, onSome }: { + readonly onNone: LazyArg + readonly onSome: (a: A) => C + }): B | C => isNone(self) ? onNone() : onSome(self.value) +) + +/** + * Converts an `Option`-returning function into a type guard. + * + * **Details** + * + * This function transforms a function that returns an `Option` into a type + * guard, ensuring type safety when validating or narrowing types. The returned + * type guard function checks whether the input satisfies the condition defined + * in the original `Option`-returning function. + * + * If the original function returns `Option.some`, the type guard evaluates to + * `true`, confirming the input is of the desired type. If the function returns + * `Option.none`, the type guard evaluates to `false`. + * + * This utility is especially useful for validating types in union types, + * filtering arrays, or ensuring safe handling of specific subtypes. + * + * @example + * ```ts + * import { Option } from "effect" + * + * type MyData = string | number + * + * const parseString = (data: MyData): Option.Option => + * typeof data === "string" ? Option.some(data) : Option.none() + * + * // ┌─── (a: MyData) => a is string + * // ▼ + * const isString = Option.toRefinement(parseString) + * + * console.log(isString("a")) + * // Output: true + * + * console.log(isString(1)) + * // Output: false + * ``` + * + * @category Conversions + * @since 2.0.0 + */ +export const toRefinement = (f: (a: A) => Option): (a: A) => a is B => (a: A): a is B => isSome(f(a)) + +/** + * Converts an `Iterable` into an `Option`, wrapping the first element if it + * exists. + * + * **Details** + * + * This function takes an `Iterable` (e.g., an array, a generator, or any object + * implementing the `Iterable` interface) and returns an `Option` based on its + * content: + * + * - If the `Iterable` contains at least one element, the first element is + * wrapped in a `Some` and returned. + * - If the `Iterable` is empty, `None` is returned, representing the absence of + * a value. + * + * This utility is useful for safely handling collections that might be empty, + * ensuring you explicitly handle both cases where a value exists or doesn't. + * + * @example + * ```ts + * import { Option } from "effect" + * + * console.log(Option.fromIterable([1, 2, 3])) + * // Output: { _id: 'Option', _tag: 'Some', value: 1 } + * + * console.log(Option.fromIterable([])) + * // Output: { _id: 'Option', _tag: 'None' } + * ``` + * + * @category Constructors + * @since 2.0.0 + */ +export const fromIterable = (collection: Iterable): Option => { + for (const a of collection) { + return some(a) + } + return none() +} + +/** + * Converts an `Either` into an `Option` by discarding the error and extracting + * the right value. + * + * **Details** + * + * This function takes an `Either` and returns an `Option` based on its value: + * + * - If the `Either` is a `Right`, its value is wrapped in a `Some` and + * returned. + * - If the `Either` is a `Left`, the error is discarded, and `None` is + * returned. + * + * This is particularly useful when you only care about the success case + * (`Right`) of an `Either` and want to handle the result using `Option`. By + * using this function, you can convert `Either` into a simpler structure for + * cases where error handling is not required. + * + * @example + * ```ts + * import { Either, Option } from "effect" + * + * console.log(Option.getRight(Either.right("ok"))) + * // Output: { _id: 'Option', _tag: 'Some', value: 'ok' } + * + * console.log(Option.getRight(Either.left("err"))) + * // Output: { _id: 'Option', _tag: 'None' } + * ``` + * + * @see {@link getLeft} for the opposite operation. + * + * @category Conversions + * @since 2.0.0 + */ +export const getRight: (self: Either) => Option = either.getRight + +/** + * Converts an `Either` into an `Option` by discarding the right value and + * extracting the left value. + * + * **Details** + * + * This function transforms an `Either` into an `Option` as follows: + * + * - If the `Either` is a `Left`, its value is wrapped in a `Some` and returned. + * - If the `Either` is a `Right`, the value is discarded, and `None` is + * returned. + * + * This utility is useful when you only care about the error case (`Left`) of an + * `Either` and want to handle it as an `Option`. By discarding the right value, + * it simplifies error-focused workflows. + * + * @example + * ```ts + * import { Either, Option } from "effect" + * + * console.log(Option.getLeft(Either.right("ok"))) + * // Output: { _id: 'Option', _tag: 'None' } + * + * console.log(Option.getLeft(Either.left("err"))) + * // Output: { _id: 'Option', _tag: 'Some', value: 'err' } + * ``` + * + * @see {@link getRight} for the opposite operation. + * + * @category Conversions + * @since 2.0.0 + */ +export const getLeft: (self: Either) => Option = either.getLeft + +/** + * Returns the value contained in the `Option` if it is `Some`, otherwise + * evaluates and returns the result of `onNone`. + * + * **Details** + * + * This function allows you to provide a fallback value or computation for when + * an `Option` is `None`. If the `Option` contains a value (`Some`), that value + * is returned. If it is empty (`None`), the `onNone` function is executed, and + * its result is returned instead. + * + * This utility is helpful for safely handling `Option` values by ensuring you + * always receive a meaningful result, whether or not the `Option` contains a + * value. It is particularly useful for providing default values or alternative + * logic when working with optional values. + * + * @example + * ```ts + * import { Option } from "effect" + * + * console.log(Option.some(1).pipe(Option.getOrElse(() => 0))) + * // Output: 1 + * + * console.log(Option.none().pipe(Option.getOrElse(() => 0))) + * // Output: 0 + * ``` + * + * @see {@link getOrNull} for a version that returns `null` instead of executing a function. + * @see {@link getOrUndefined} for a version that returns `undefined` instead of executing a function. + * + * @category Getters + * @since 2.0.0 + */ +export const getOrElse: { + (onNone: LazyArg): (self: Option) => B | A + (self: Option, onNone: LazyArg): A | B +} = dual( + 2, + (self: Option, onNone: LazyArg): A | B => isNone(self) ? onNone() : self.value +) + +/** + * Returns the provided `Option` `that` if the current `Option` (`self`) is + * `None`; otherwise, it returns `self`. + * + * **Details** + * + * This function provides a fallback mechanism for `Option` values. If the + * current `Option` is `None` (i.e., it contains no value), the `that` function + * is evaluated, and its resulting `Option` is returned. If the current `Option` + * is `Some` (i.e., it contains a value), the original `Option` is returned + * unchanged. + * + * This is particularly useful for chaining fallback values or computations, + * allowing you to provide alternative `Option` values when the first one is + * empty. + * + * @example + * ```ts + * import { Option } from "effect" + * + * console.log(Option.none().pipe(Option.orElse(() => Option.none()))) + * // Output: { _id: 'Option', _tag: 'None' } + * + * console.log(Option.some("a").pipe(Option.orElse(() => Option.none()))) + * // Output: { _id: 'Option', _tag: 'Some', value: 'a' } + * + * console.log(Option.none().pipe(Option.orElse(() => Option.some("b")))) + * // Output: { _id: 'Option', _tag: 'Some', value: 'b' } + * + * console.log(Option.some("a").pipe(Option.orElse(() => Option.some("b")))) + * // Output: { _id: 'Option', _tag: 'Some', value: 'a' } + * ``` + * + * @category Error handling + * @since 2.0.0 + */ +export const orElse: { + (that: LazyArg>): (self: Option) => Option + (self: Option, that: LazyArg>): Option +} = dual( + 2, + (self: Option, that: LazyArg>): Option => isNone(self) ? that() : self +) + +/** + * Returns the provided default value wrapped in `Some` if the current `Option` + * (`self`) is `None`; otherwise, returns `self`. + * + * **Details** + * + * This function provides a way to supply a default value for cases where an + * `Option` is `None`. If the current `Option` is empty (`None`), the `onNone` + * function is executed to compute the default value, which is then wrapped in a + * `Some`. If the current `Option` contains a value (`Some`), it is returned as + * is. + * + * This is particularly useful for handling optional values where a fallback + * default needs to be provided explicitly in case of absence. + * + * @example + * ```ts + * import { Option } from "effect" + * + * console.log(Option.none().pipe(Option.orElseSome(() => "b"))) + * // Output: { _id: 'Option', _tag: 'Some', value: 'b' } + * + * console.log(Option.some("a").pipe(Option.orElseSome(() => "b"))) + * // Output: { _id: 'Option', _tag: 'Some', value: 'a' } + * ``` + * + * @category Error handling + * @since 2.0.0 + */ +export const orElseSome: { + (onNone: LazyArg): (self: Option) => Option + (self: Option, onNone: LazyArg): Option +} = dual( + 2, + (self: Option, onNone: LazyArg): Option => isNone(self) ? some(onNone()) : self +) + +/** + * Similar to {@link orElse}, but returns an `Either` wrapped in an `Option` to + * indicate the source of the value. + * + * **Details** + * + * This function allows you to provide a fallback `Option` in case the current + * `Option` (`self`) is `None`. However, unlike `orElse`, it returns the value + * wrapped in an `Either` object, providing additional information about where + * the value came from: + * + * - If the value is from the fallback `Option` (`that`), it is wrapped in an + * `Either.right`. + * - If the value is from the original `Option` (`self`), it is wrapped in an + * `Either.left`. + * + * This is especially useful when you need to differentiate between values + * originating from the primary `Option` and those coming from the fallback, + * while still maintaining the `Option`-style handling. + * + * @category Error handling + * @since 2.0.0 + */ +export const orElseEither: { + (that: LazyArg>): (self: Option) => Option> + (self: Option, that: LazyArg>): Option> +} = dual( + 2, + (self: Option, that: LazyArg>): Option> => + isNone(self) ? map(that(), either.right) : map(self, either.left) +) + +/** + * Returns the first `Some` value found in an `Iterable` collection of + * `Option`s, or `None` if no `Some` is found. + * + * **Details** + * + * This function iterates over a collection of `Option` values and returns the + * first `Some` it encounters. If the collection contains only `None` values, + * the result will also be `None`. This utility is useful for efficiently + * finding the first valid value in a sequence of potentially empty or invalid + * options. + * + * The iteration stops as soon as a `Some` is found, making this function + * efficient for large collections. + * + * @example + * ```ts + * import { Option } from "effect" + * + * console.log(Option.firstSomeOf([ + * Option.none(), + * Option.some(1), + * Option.some(2) + * ])) + * // Output: { _id: 'Option', _tag: 'Some', value: 1 } + * ``` + * + * @category Error handling + * @since 2.0.0 + */ +export const firstSomeOf = > = Iterable>>( + collection: C +): [C] extends [Iterable>] ? Option : never => { + let out: Option = none() + for (out of collection) { + if (isSome(out)) { + return out as any + } + } + return out as any +} + +/** + * Converts a nullable value into an `Option`. Returns `None` if the value is + * `null` or `undefined`, otherwise wraps the value in a `Some`. + * + * @example + * ```ts + * import { Option } from "effect" + * + * console.log(Option.fromNullable(undefined)) + * // Output: { _id: 'Option', _tag: 'None' } + * + * console.log(Option.fromNullable(null)) + * // Output: { _id: 'Option', _tag: 'None' } + * + * console.log(Option.fromNullable(1)) + * // Output: { _id: 'Option', _tag: 'Some', value: 1 } + * ``` + * + * @category Conversions + * @since 2.0.0 + */ +export const fromNullable = ( + nullableValue: A +): Option> => (nullableValue == null ? none() : some(nullableValue as NonNullable)) + +/** + * Lifts a function that returns `null` or `undefined` into the `Option` + * context. + * + * **Details** + * + * This function takes a function `f` that might return `null` or `undefined` + * and transforms it into a function that returns an `Option`. The resulting + * function will return: + * - `Some` if the original function produces a non-null, non-undefined value. + * - `None` if the original function produces `null` or `undefined`. + * + * @example + * ```ts + * import { Option } from "effect" + * + * const parse = (s: string): number | undefined => { + * const n = parseFloat(s) + * return isNaN(n) ? undefined : n + * } + * + * const parseOption = Option.liftNullable(parse) + * + * console.log(parseOption("1")) + * // Output: { _id: 'Option', _tag: 'Some', value: 1 } + * + * console.log(parseOption("not a number")) + * // Output: { _id: 'Option', _tag: 'None' } + * ``` + * + * @category Conversions + * @since 2.0.0 + */ +export const liftNullable = , B>( + f: (...a: A) => B | null | undefined +): (...a: A) => Option> => +(...a) => fromNullable(f(...a)) + +/** + * Returns the value contained in the `Option` if it is `Some`; otherwise, + * returns `null`. + * + * **Details** + * + * This function provides a way to extract the value of an `Option` while + * falling back to `null` if the `Option` is `None`. + * + * It is particularly useful in scenarios where `null` is an acceptable + * placeholder for the absence of a value, such as when interacting with APIs or + * systems that use `null` as a default for missing values. + * + * @example + * ```ts + * import { Option } from "effect" + * + * console.log(Option.getOrNull(Option.some(1))) + * // Output: 1 + * + * console.log(Option.getOrNull(Option.none())) + * // Output: null + * ``` + * + * @category Getters + * @since 2.0.0 + */ +export const getOrNull: (self: Option) => A | null = getOrElse(constNull) + +/** + * Returns the value contained in the `Option` if it is `Some`; otherwise, + * returns `undefined`. + * + * **Details** + * + * This function provides a way to extract the value of an `Option` while + * falling back to `undefined` if the `Option` is `None`. + * + * It is particularly useful in scenarios where `undefined` is an acceptable + * placeholder for the absence of a value, such as when interacting with APIs or + * systems that use `undefined` as a default for missing values. + * + * @example + * ```ts + * import { Option } from "effect" + * + * console.log(Option.getOrUndefined(Option.some(1))) + * // Output: 1 + * + * console.log(Option.getOrUndefined(Option.none())) + * // Output: undefined + * ``` + * + * @category Getters + * @since 2.0.0 + */ +export const getOrUndefined: (self: Option) => A | undefined = getOrElse(constUndefined) + +/** + * Lifts a function that throws exceptions into a function that returns an + * `Option`. + * + * **Details** + * + * This utility function takes a function `f` that might throw an exception and + * transforms it into a safer function that returns an `Option`. If the original + * function executes successfully, the result is wrapped in a `Some`. If an + * exception is thrown, the result is `None`, allowing the developer to handle + * errors in a functional, type-safe way. + * + * @example + * ```ts + * import { Option } from "effect" + * + * const parse = Option.liftThrowable(JSON.parse) + * + * console.log(parse("1")) + * // Output: { _id: 'Option', _tag: 'Some', value: 1 } + * + * console.log(parse("")) + * // Output: { _id: 'Option', _tag: 'None' } + * ``` + * + * @category Conversions + * @since 2.0.0 + */ +export const liftThrowable = , B>( + f: (...a: A) => B +): (...a: A) => Option => +(...a) => { + try { + return some(f(...a)) + } catch { + return none() + } +} + +/** + * Extracts the value of an `Option` or throws an error if the `Option` is + * `None`, using a custom error factory. + * + * **Details** + * + * This function allows you to extract the value of an `Option` when it is + * `Some`. If the `Option` is `None`, it throws an error generated by the + * provided `onNone` function. This utility is particularly useful when you need + * a fail-fast behavior for empty `Option` values and want to provide a custom + * error message or object. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { Option } from "effect" + * + * assert.deepStrictEqual( + * Option.getOrThrowWith(Option.some(1), () => new Error('Unexpected None')), + * 1 + * ) + * assert.throws(() => Option.getOrThrowWith(Option.none(), () => new Error('Unexpected None'))) + * ``` + * + * @see {@link getOrThrow} for a version that throws a default error. + * + * @category Conversions + * @since 2.0.0 + */ +export const getOrThrowWith: { + (onNone: () => unknown): (self: Option) => A + (self: Option, onNone: () => unknown): A +} = dual(2, (self: Option, onNone: () => unknown): A => { + if (isSome(self)) { + return self.value + } + throw onNone() +}) + +/** + * Extracts the value of an `Option` or throws a default error if the `Option` + * is `None`. + * + * **Details** + * + * This function extracts the value from an `Option` if it is `Some`. If the + * `Option` is `None`, it throws a default error. It is useful for fail-fast + * scenarios where the absence of a value is treated as an exceptional case and + * a default error is sufficient. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { Option } from "effect" + * + * assert.deepStrictEqual(Option.getOrThrow(Option.some(1)), 1) + * assert.throws(() => Option.getOrThrow(Option.none())) + * ``` + * + * @see {@link getOrThrowWith} for a version that allows you to provide a custom error. + * + * @category Conversions + * @since 2.0.0 + */ +export const getOrThrow: (self: Option) => A = getOrThrowWith(() => new Error("getOrThrow called on a None")) + +/** + * Transforms the value inside a `Some` to a new value using the provided + * function, while leaving `None` unchanged. + * + * **Details** + * + * This function applies a mapping function `f` to the value inside an `Option` + * if it is a `Some`. If the `Option` is `None`, it remains unchanged. The + * result is a new `Option` with the transformed value (if it was a `Some`) or + * still `None`. + * + * This utility is particularly useful for chaining transformations in a + * functional way without needing to manually handle `None` cases. + * + * @example + * ```ts + * import { Option } from "effect" + * + * // Mapping over a `Some` + * const someValue = Option.some(2) + * + * console.log(Option.map(someValue, (n) => n * 2)) + * // Output: { _id: 'Option', _tag: 'Some', value: 4 } + * + * // Mapping over a `None` + * const noneValue = Option.none() + * + * console.log(Option.map(noneValue, (n) => n * 2)) + * // Output: { _id: 'Option', _tag: 'None' } + * ``` + * + * @category Mapping + * @since 2.0.0 + */ +export const map: { + (f: (a: A) => B): (self: Option) => Option + (self: Option, f: (a: A) => B): Option +} = dual( + 2, + (self: Option, f: (a: A) => B): Option => isNone(self) ? none() : some(f(self.value)) +) + +/** + * Replaces the value inside a `Some` with the specified constant value, leaving + * `None` unchanged. + * + * **Details** + * + * This function transforms an `Option` by replacing the value inside a `Some` + * with the given constant value `b`. If the `Option` is `None`, it remains + * unchanged. + * + * This is useful when you want to preserve the presence of a value (`Some`) but + * replace its content with a fixed value. + * + * @example + * ```ts + * import { Option } from "effect" + * + * // Replacing the value of a `Some` + * const someValue = Option.some(42) + * + * console.log(Option.as(someValue, "new value")) + * // Output: { _id: 'Option', _tag: 'Some', value: 'new value' } + * + * // Replacing a `None` (no effect) + * const noneValue = Option.none() + * + * console.log(Option.as(noneValue, "new value")) + * // Output: { _id: 'Option', _tag: 'None' } + * ``` + * + * @category Mapping + * @since 2.0.0 + */ +export const as: { + (b: B): (self: Option) => Option + (self: Option, b: B): Option +} = dual(2, (self: Option, b: B): Option => map(self, () => b)) + +/** + * Replaces the value inside a `Some` with the constant value `void`, leaving + * `None` unchanged. + * + * **Details** + * + * This function transforms an `Option` by replacing the value inside a `Some` + * with `void`. If the `Option` is `None`, it remains unchanged. + * + * This is particularly useful in scenarios where the presence or absence of a + * value is significant, but the actual content of the value is irrelevant. + * + * @category Mapping + * @since 2.0.0 + */ +export const asVoid: <_>(self: Option<_>) => Option = as(undefined) + +const void_: Option = some(undefined) +export { + /** + * @since 2.0.0 + */ + void_ as void +} + +/** + * Applies a function to the value of a `Some` and flattens the resulting + * `Option`. If the input is `None`, it remains `None`. + * + * **Details** + * + * This function allows you to chain computations that return `Option` values. + * If the input `Option` is `Some`, the provided function `f` is applied to the + * contained value, and the resulting `Option` is returned. If the input is + * `None`, the function is not applied, and the result remains `None`. + * + * This utility is particularly useful for sequencing operations that may fail + * or produce optional results, enabling clean and concise workflows for + * handling such cases. + * + * @example + * ```ts + * import { Option } from "effect" + * + * interface Address { + * readonly city: string + * readonly street: Option.Option + * } + * + * interface User { + * readonly id: number + * readonly username: string + * readonly email: Option.Option + * readonly address: Option.Option

+ * } + * + * const user: User = { + * id: 1, + * username: "john_doe", + * email: Option.some("john.doe@example.com"), + * address: Option.some({ + * city: "New York", + * street: Option.some("123 Main St") + * }) + * } + * + * // Use flatMap to extract the street value + * const street = user.address.pipe( + * Option.flatMap((address) => address.street) + * ) + * + * console.log(street) + * // Output: { _id: 'Option', _tag: 'Some', value: '123 Main St' } + * ``` + * + * @category Sequencing + * @since 2.0.0 + */ +export const flatMap: { + (f: (a: A) => Option): (self: Option) => Option + (self: Option, f: (a: A) => Option): Option +} = dual( + 2, + (self: Option, f: (a: A) => Option): Option => isNone(self) ? none() : f(self.value) +) + +/** + * Chains two `Option`s together. The second `Option` can either be a static + * value or depend on the result of the first `Option`. + * + * **Details** + * + * This function enables sequencing of two `Option` computations. If the first + * `Option` is `Some`, the second `Option` is evaluated. The second `Option` can + * either: + * + * - Be a static `Option` value. + * - Be a function that produces an `Option`, optionally based on the value of + * the first `Option`. + * + * If the first `Option` is `None`, the function skips the evaluation of the + * second `Option` and directly returns `None`. + * + * @category Sequencing + * @since 2.0.0 + */ +export const andThen: { + (f: (a: A) => Option): (self: Option) => Option + (f: Option): (self: Option) => Option + (f: (a: A) => B): (self: Option) => Option + (f: NotFunction): (self: Option) => Option + (self: Option, f: (a: A) => Option): Option + (self: Option, f: Option): Option + (self: Option, f: (a: A) => B): Option + (self: Option, f: NotFunction): Option +} = dual( + 2, + (self: Option, f: (a: A) => Option | Option): Option => + flatMap(self, (a) => { + const b = isFunction(f) ? f(a) : f + return isOption(b) ? b : some(b) + }) +) + +/** + * Combines `flatMap` and `fromNullable`, transforming the value inside a `Some` + * using a function that may return `null` or `undefined`. + * + * **Details** + * + * This function applies a transformation function `f` to the value inside a + * `Some`. The function `f` may return a value, `null`, or `undefined`. If `f` + * returns a value, it is wrapped in a `Some`. If `f` returns `null` or + * `undefined`, the result is `None`. If the input `Option` is `None`, the + * function is not applied, and `None` is returned. + * + * This utility is particularly useful when working with deeply nested optional + * values or chaining computations that may result in `null` or `undefined` at + * some point. + * + * @example + * ```ts + * import { Option } from "effect" + * + * interface Employee { + * company?: { + * address?: { + * street?: { + * name?: string + * } + * } + * } + * } + * + * const employee1: Employee = { company: { address: { street: { name: "high street" } } } } + * + * // Extracting a deeply nested property + * console.log( + * Option.some(employee1) + * .pipe(Option.flatMapNullable((employee) => employee.company?.address?.street?.name)) + * ) + * // Output: { _id: 'Option', _tag: 'Some', value: 'high street' } + * + * const employee2: Employee = { company: { address: { street: {} } } } + * + * // Property does not exist + * console.log( + * Option.some(employee2) + * .pipe(Option.flatMapNullable((employee) => employee.company?.address?.street?.name)) + * ) + * // Output: { _id: 'Option', _tag: 'None' } + * ``` + * + * @category Sequencing + * @since 2.0.0 + */ +export const flatMapNullable: { + (f: (a: A) => B | null | undefined): (self: Option) => Option> + (self: Option, f: (a: A) => B | null | undefined): Option> +} = dual( + 2, + (self: Option, f: (a: A) => B | null | undefined): Option> => + isNone(self) ? none() : fromNullable(f(self.value)) +) + +/** + * Flattens an `Option` of `Option` into a single `Option`. + * + * **Details** + * + * This function takes an `Option` that wraps another `Option` and flattens it + * into a single `Option`. If the outer `Option` is `Some`, the function + * extracts the inner `Option`. If the outer `Option` is `None`, the result + * remains `None`. + * + * This is useful for simplifying nested `Option` structures that may arise + * during functional operations. + * + * @category Sequencing + * @since 2.0.0 + */ +export const flatten: (self: Option>) => Option = flatMap(identity) + +/** + * Combines two `Option`s, keeping the value from the second `Option` if both + * are `Some`. + * + * **Details** + * + * This function takes two `Option`s and returns the second one if the first is + * `Some`. If the first `Option` is `None`, the result will also be `None`, + * regardless of the second `Option`. It effectively "zips" the two `Option`s + * while discarding the value from the first `Option`. + * + * This is particularly useful when sequencing computations where the result of + * the first computation is not needed, and you only care about the result of + * the second computation. + * + * @category Zipping + * @since 2.0.0 + */ +export const zipRight: { + (that: Option): <_>(self: Option<_>) => Option + (self: Option, that: Option): Option +} = dual(2, (self: Option, that: Option): Option => flatMap(self, () => that)) + +/** + * Combines two `Option`s, keeping the value from the first `Option` if both are + * `Some`. + * + * **Details** + * + * This function takes two `Option`s and returns the first one if it is `Some`. + * If either the first `Option` or the second `Option` is `None`, the result + * will be `None`. This operation "zips" the two `Option`s while discarding the + * value from the second `Option`. + * + * This is useful when sequencing computations where the second `Option` + * represents a dependency or condition that must hold, but its value is + * irrelevant. + * + * @category Zipping + * @since 2.0.0 + */ +export const zipLeft: { + <_>(that: Option<_>): (self: Option) => Option + (self: Option, that: Option): Option +} = dual(2, (self: Option, that: Option): Option => tap(self, () => that)) + +/** + * Composes two functions that return `Option` values, creating a new function + * that chains them together. + * + * **Details** + * + * This function allows you to compose two computations, each represented by a + * function that returns an `Option`. The result of the first function is passed + * to the second function if it is `Some`. If the first function returns `None`, + * the composed function short-circuits and returns `None` without invoking the + * second function. + * + * @example + * ```ts + * import { Option } from "effect" + * + * const parse = (s: string): Option.Option => isNaN(Number(s)) ? Option.none() : Option.some(Number(s)) + * + * const double = (n: number): Option.Option => n > 0 ? Option.some(n * 2) : Option.none() + * + * const parseAndDouble = Option.composeK(parse, double) + * + * console.log(parseAndDouble("42")) + * // Output: { _id: 'Option', _tag: 'Some', value: 84 } + * + * console.log(parseAndDouble("not a number")) + * // Output: { _id: 'Option', _tag: 'None' } + * ``` + * + * @category Sequencing + * @since 2.0.0 + */ +export const composeK: { + (bfc: (b: B) => Option): (afb: (a: A) => Option) => (a: A) => Option + (afb: (a: A) => Option, bfc: (b: B) => Option): (a: A) => Option +} = dual(2, (afb: (a: A) => Option, bfc: (b: B) => Option) => (a: A): Option => flatMap(afb(a), bfc)) + +/** + * Applies the provided function `f` to the value of the `Option` if it is + * `Some` and returns the original `Option`, unless `f` returns `None`, in which + * case it returns `None`. + * + * **Details** + * + * This function allows you to perform additional computations on the value of + * an `Option` without modifying its original value. If the `Option` is `Some`, + * the provided function `f` is executed with the value, and its result + * determines whether the original `Option` is returned (`Some`) or the result + * is `None` if `f` returns `None`. If the input `Option` is `None`, the + * function is not executed, and `None` is returned. + * + * This is particularly useful for applying side conditions or performing + * validation checks while retaining the original `Option`'s value. + * + * @example + * ```ts + * import { Option } from "effect" + * + * const getInteger = (n: number) => Number.isInteger(n) ? Option.some(n) : Option.none() + * + * console.log(Option.tap(Option.none(), getInteger)) + * // Output: { _id: 'Option', _tag: 'None' } + * + * console.log(Option.tap(Option.some(1), getInteger)) + * // Output: { _id: 'Option', _tag: 'Some', value: 1 } + * + * console.log(Option.tap(Option.some(1.14), getInteger)) + * // Output: { _id: 'Option', _tag: 'None' } + * ``` + * + * @category Sequencing + * @since 2.0.0 + */ +export const tap: { + (f: (a: A) => Option): (self: Option) => Option + (self: Option, f: (a: A) => Option): Option +} = dual(2, (self: Option, f: (a: A) => Option): Option => flatMap(self, (a) => map(f(a), () => a))) + +/** + * Combines two `Option` values into a single `Option` containing a tuple of + * their values if both are `Some`. + * + * **Details** + * + * This function takes two `Option`s and combines their values into a tuple `[A, + * B]` if both are `Some`. If either of the `Option`s is `None`, the result is + * `None`. This is particularly useful for combining multiple `Option` values + * into a single one, ensuring both contain valid values. + * + * @category Combining + * @since 2.0.0 + */ +export const product = (self: Option, that: Option): Option<[A, B]> => + isSome(self) && isSome(that) ? some([self.value, that.value]) : none() + +/** + * Combines an `Option` with a collection of `Option`s into a single `Option` + * containing a tuple of their values if all are `Some`. + * + * **Details** + * + * This function takes a primary `Option` and a collection of `Option`s and + * combines their values into a tuple `[A, ...Array]` if all are `Some`. If + * the primary `Option` or any `Option` in the collection is `None`, the result + * is `None`. + * + * @category Combining + * @since 2.0.0 + */ +export const productMany = ( + self: Option, + collection: Iterable> +): Option<[A, ...Array]> => { + if (isNone(self)) { + return none() + } + const out: [A, ...Array] = [self.value] + for (const o of collection) { + if (isNone(o)) { + return none() + } + out.push(o.value) + } + return some(out) +} + +/** + * Combines a structure of `Option`s into a single `Option` containing the + * values with the same structure. + * + * **Details** + * + * This function takes a structure of `Option`s (a tuple, struct, or iterable) + * and produces a single `Option` that contains the values from the input + * structure if all `Option`s are `Some`. If any `Option` in the input is + * `None`, the result is `None`. The structure of the input is preserved in the + * output. + * + * - If the input is a tuple (e.g., an array), the result will be an `Option` + * containing a tuple with the same length. + * - If the input is a struct (e.g., an object), the result will be an `Option` + * containing a struct with the same keys. + * - If the input is an iterable, the result will be an `Option` containing an + * array. + * + * @example + * ```ts + * import { Option } from "effect" + * + * const maybeName: Option.Option = Option.some("John") + * const maybeAge: Option.Option = Option.some(25) + * + * // ┌─── Option<[string, number]> + * // ▼ + * const tuple = Option.all([maybeName, maybeAge]) + * console.log(tuple) + * // Output: + * // { _id: 'Option', _tag: 'Some', value: [ 'John', 25 ] } + * + * // ┌─── Option<{ name: string; age: number; }> + * // ▼ + * const struct = Option.all({ name: maybeName, age: maybeAge }) + * console.log(struct) + * // Output: + * // { _id: 'Option', _tag: 'Some', value: { name: 'John', age: 25 } } + * ``` + * + * @category Combining + * @since 2.0.0 + */ +// @ts-expect-error +export const all: > | Record>>( + input: I +) => [I] extends [ReadonlyArray>] ? Option< + { -readonly [K in keyof I]: [I[K]] extends [Option] ? A : never } + > + : [I] extends [Iterable>] ? Option> + : Option<{ -readonly [K in keyof I]: [I[K]] extends [Option] ? A : never }> = ( + input: Iterable> | Record> + ): Option => { + if (Symbol.iterator in input) { + const out: Array> = [] + for (const o of (input as Iterable>)) { + if (isNone(o)) { + return none() + } + out.push(o.value) + } + return some(out) + } + + const out: Record = {} + for (const key of Object.keys(input)) { + const o = input[key] + if (isNone(o)) { + return none() + } + out[key] = o.value + } + return some(out) + } + +/** + * Combines two `Option` values into a new `Option` by applying a provided + * function to their values. + * + * **Details** + * + * This function takes two `Option` values (`self` and `that`) and a combining + * function `f`. If both `Option` values are `Some`, the function `f` is applied + * to their values, and the result is wrapped in a new `Some`. If either + * `Option` is `None`, the result is `None`. + * + * This utility is useful for combining two optional computations into a single + * result while maintaining type safety and avoiding explicit checks for `None`. + * + * @example + * ```ts + * import { Option } from "effect" + * + * const maybeName: Option.Option = Option.some("John") + * const maybeAge: Option.Option = Option.some(25) + * + * // Combine the name and age into a person object + * const person = Option.zipWith(maybeName, maybeAge, (name, age) => ({ + * name: name.toUpperCase(), + * age + * })) + * + * console.log(person) + * // Output: + * // { _id: 'Option', _tag: 'Some', value: { name: 'JOHN', age: 25 } } + * ``` + * + * @category Zipping + * @since 2.0.0 + */ +export const zipWith: { + (that: Option, f: (a: A, b: B) => C): (self: Option) => Option + (self: Option, that: Option, f: (a: A, b: B) => C): Option +} = dual( + 3, + (self: Option, that: Option, f: (a: A, b: B) => C): Option => + map(product(self, that), ([a, b]) => f(a, b)) +) + +/** + * Applies a function inside a `Some` to a value inside another `Some`, + * combining them into a new `Option`. + * + * **Details** + * + * This function allows you to apply a function wrapped in an `Option` (`self`) + * to a value wrapped in another `Option` (`that`). If both `Option`s are + * `Some`, the function is applied to the value, and the result is wrapped in a + * new `Some`. If either `Option` is `None`, the result is `None`. + * + * @category Combining + * @since 2.0.0 + */ +export const ap: { + (that: Option): (self: Option<(a: A) => B>) => Option + (self: Option<(a: A) => B>, that: Option): Option +} = dual(2, (self: Option<(a: A) => B>, that: Option): Option => zipWith(self, that, (f, a) => f(a))) + +/** + * Reduces an `Iterable` of `Option` to a single value of type `B`, ignoring + * elements that are `None`. + * + * **Details** + * + * This function takes an initial value of type `B` and a reducing function `f` + * that combines the accumulator with values of type `A`. It processes an + * iterable of `Option`, applying `f` only to the `Some` values while + * ignoring the `None` values. The result is a single value of type `B`. + * + * This utility is particularly useful for aggregating values from an iterable + * of `Option`s while skipping the absent (`None`) values. + * + * @example + * ```ts + * import { Option, pipe } from "effect" + * + * const iterable = [Option.some(1), Option.none(), Option.some(2), Option.none()] + * + * console.log(pipe(iterable, Option.reduceCompact(0, (b, a) => b + a))) + * // Output: 3 + * ``` + * + * @category Reducing + * @since 2.0.0 + */ +export const reduceCompact: { + (b: B, f: (b: B, a: A) => B): (self: Iterable>) => B + (self: Iterable>, b: B, f: (b: B, a: A) => B): B +} = dual( + 3, + (self: Iterable>, b: B, f: (b: B, a: A) => B): B => { + let out: B = b + for (const oa of self) { + if (isSome(oa)) { + out = f(out, oa.value) + } + } + return out + } +) + +/** + * Converts an `Option` into an `Array`. + * If the input is `None`, an empty array is returned. + * If the input is `Some`, its value is wrapped in a single-element array. + * + * @example + * ```ts + * import { Option } from "effect" + * + * console.log(Option.toArray(Option.some(1))) + * // Output: [1] + * + * console.log(Option.toArray(Option.none())) + * // Output: [] + * ``` + * + * @category Conversions + * @since 2.0.0 + */ +export const toArray = (self: Option): Array => isNone(self) ? [] : [self.value] + +/** + * Splits an `Option` into two `Option`s based on the result of a mapping + * function that produces an `Either`. + * + * **Details** + * + * This function takes an `Option` and a mapping function `f` that converts its + * value into an `Either`. It returns a tuple of two `Option`s: + * + * - The first `Option` (`left`) contains the value from the `Left` side of the + * `Either` if it exists, otherwise `None`. + * - The second `Option` (`right`) contains the value from the `Right` side of + * the `Either` if it exists, otherwise `None`. + * + * If the input `Option` is `None`, both returned `Option`s are `None`. + * + * This utility is useful for filtering and categorizing the contents of an + * `Option` based on a bifurcating computation. + * + * @category Filtering + * @since 2.0.0 + */ +export const partitionMap: { + (f: (a: A) => Either): (self: Option) => [left: Option, right: Option] + (self: Option, f: (a: A) => Either): [left: Option, right: Option] +} = dual(2, ( + self: Option, + f: (a: A) => Either +): [excluded: Option, satisfying: Option] => { + if (isNone(self)) { + return [none(), none()] + } + const e = f(self.value) + return either.isLeft(e) ? [some(e.left), none()] : [none(), some(e.right)] +}) + +// TODO(4.0): remove? +/** + * Alias of {@link flatMap}. + * + * @example + * ```ts + * import { Option } from "effect" + * + * // Transform and filter numbers + * const transformEven = (n: Option.Option): Option.Option => + * Option.filterMap(n, (n) => (n % 2 === 0 ? Option.some(`Even: ${n}`) : Option.none())) + * + * console.log(transformEven(Option.none())) + * // Output: { _id: 'Option', _tag: 'None' } + * + * console.log(transformEven(Option.some(1))) + * // Output: { _id: 'Option', _tag: 'None' } + * + * console.log(transformEven(Option.some(2))) + * // Output: { _id: 'Option', _tag: 'Some', value: 'Even: 2' } + * ``` + * + * @category Filtering + * @since 2.0.0 + */ +export const filterMap: { + (f: (a: A) => Option): (self: Option) => Option + (self: Option, f: (a: A) => Option): Option +} = flatMap + +/** + * Filters an `Option` using a predicate. If the predicate is not satisfied or the `Option` is `None` returns `None`. + * + * If you need to change the type of the `Option` in addition to filtering, see `filterMap`. + * + * @example + * ```ts + * import { Option } from "effect" + * + * const removeEmptyString = (input: Option.Option) => + * Option.filter(input, (value) => value !== "") + * + * console.log(removeEmptyString(Option.none())) + * // Output: { _id: 'Option', _tag: 'None' } + * + * console.log(removeEmptyString(Option.some(""))) + * // Output: { _id: 'Option', _tag: 'None' } + * + * console.log(removeEmptyString(Option.some("a"))) + * // Output: { _id: 'Option', _tag: 'Some', value: 'a' } + * ``` + * + * @category Filtering + * @since 2.0.0 + */ +export const filter: { + (refinement: Refinement, B>): (self: Option) => Option + (predicate: Predicate>): (self: Option) => Option + (self: Option, refinement: Refinement): Option + (self: Option, predicate: Predicate): Option +} = dual( + 2, + (self: Option, predicate: Predicate): Option => + filterMap(self, (b) => (predicate(b) ? option.some(b) : option.none)) +) + +/** + * Creates an `Equivalence` instance for comparing `Option` values, using a + * provided `Equivalence` for the inner type. + * + * **Details** + * + * This function takes an `Equivalence` instance for a specific type `A` and + * produces an `Equivalence` instance for `Option`. The resulting + * `Equivalence` determines whether two `Option` values are equivalent: + * + * - Two `None`s are considered equivalent. + * - A `Some` and a `None` are not equivalent. + * - Two `Some` values are equivalent if their inner values are equivalent + * according to the provided `Equivalence`. + * + * **Example** (Comparing Optional Numbers for Equivalence) + * + * ```ts + * import { Number, Option } from "effect" + * + * const isEquivalent = Option.getEquivalence(Number.Equivalence) + * + * console.log(isEquivalent(Option.none(), Option.none())) + * // Output: true + * + * console.log(isEquivalent(Option.none(), Option.some(1))) + * // Output: false + * + * console.log(isEquivalent(Option.some(1), Option.none())) + * // Output: false + * + * console.log(isEquivalent(Option.some(1), Option.some(2))) + * // Output: false + * + * console.log(isEquivalent(Option.some(1), Option.some(1))) + * // Output: true + * ``` + * + * @category Equivalence + * @since 2.0.0 + */ +export const getEquivalence = (isEquivalent: Equivalence.Equivalence): Equivalence.Equivalence> => + Equivalence.make((x, y) => isNone(x) ? isNone(y) : isNone(y) ? false : isEquivalent(x.value, y.value)) + +/** + * Creates an `Order` instance for comparing `Option` values, using a provided + * `Order` for the inner type. + * + * **Details** + * + * This function produces an `Order` instance for `Option`, allowing `Option` + * values to be compared: + * + * - `None` is always considered less than any `Some` value. + * - If both are `Some`, their inner values are compared using the provided + * `Order` instance. + * + * @example + * ```ts + * import { Number, Option } from "effect" + * + * const order = Option.getOrder(Number.Order) + * + * console.log(order(Option.none(), Option.none())) + * // Output: 0 + * + * console.log(order(Option.none(), Option.some(1))) + * // Output: -1 + * + * console.log(order(Option.some(1), Option.none())) + * // Output: 1 + * + * console.log(order(Option.some(1), Option.some(2))) + * // Output: -1 + * + * console.log(order(Option.some(1), Option.some(1))) + * // Output: 0 + * ``` + * + * @category Sorting + * @since 2.0.0 + */ +export const getOrder = (O: Order): Order> => + order.make((self, that) => isSome(self) ? (isSome(that) ? O(self.value, that.value) : 1) : -1) + +/** + * Lifts a binary function to work with `Option` values, allowing the function + * to operate on two `Option`s. + * + * **Details** + * + * This function takes a binary function `f` and returns a new function that + * applies `f` to the values of two `Option`s (`self` and `that`). If both + * `Option`s are `Some`, the binary function `f` is applied to their values, and + * the result is wrapped in a new `Some`. If either `Option` is `None`, the + * result is `None`. + * + * @example + * ```ts + * import { Option } from "effect" + * + * // A binary function to add two numbers + * const add = (a: number, b: number): number => a + b + * + * // Lift the `add` function to work with `Option` values + * const addOptions = Option.lift2(add) + * + * // Both `Option`s are `Some` + * console.log(addOptions(Option.some(2), Option.some(3))) + * // Output: { _id: 'Option', _tag: 'Some', value: 5 } + * + * // One `Option` is `None` + * console.log(addOptions(Option.some(2), Option.none())) + * // Output: { _id: 'Option', _tag: 'None' } + * ``` + * + * @category Lifting + * @since 2.0.0 + */ +export const lift2 = (f: (a: A, b: B) => C): { + (that: Option): (self: Option) => Option + (self: Option, that: Option): Option +} => dual(2, (self: Option, that: Option): Option => zipWith(self, that, f)) + +/** + * Lifts a `Predicate` or `Refinement` into the `Option` context, returning a + * `Some` of the input value if the predicate is satisfied, or `None` otherwise. + * + * **Details** + * + * This function transforms a `Predicate` (or a more specific `Refinement`) into + * a function that produces an `Option`. If the predicate evaluates to `true`, + * the input value is wrapped in a `Some`. If the predicate evaluates to + * `false`, the result is `None`. + * + * @example + * ```ts + * import { Option } from "effect" + * + * // Check if a number is positive + * const isPositive = (n: number) => n > 0 + * + * // ┌─── (b: number) => Option + * // ▼ + * const parsePositive = Option.liftPredicate(isPositive) + * + * console.log(parsePositive(1)) + * // Output: { _id: 'Option', _tag: 'Some', value: 1 } + * + * console.log(parsePositive(-1)) + * // OUtput: { _id: 'Option', _tag: 'None' } + * ``` + * + * @category Lifting + * @since 2.0.0 + */ +export const liftPredicate: { // Note: I intentionally avoid using the NoInfer pattern here. + (refinement: Refinement): (a: A) => Option + (predicate: Predicate): (b: B) => Option + ( + self: A, + refinement: Refinement + ): Option + ( + self: B, + predicate: Predicate + ): Option +} = dual( + 2, + (b: B, predicate: Predicate): Option => predicate(b) ? some(b) : none() +) + +/** + * Returns a function that checks if an `Option` contains a specified value, + * using a provided equivalence function. + * + * **Details** + * + * This function allows you to check whether an `Option` contains a specific + * value. It uses an equivalence function `isEquivalent` to compare the value + * inside the `Option` to the provided value. If the `Option` is `Some` and the + * equivalence function returns `true`, the result is `true`. If the `Option` is + * `None` or the values are not equivalent, the result is `false`. + * + * @example + * ```ts + * import { Number, Option } from "effect" + * + * const contains = Option.containsWith(Number.Equivalence) + * + * console.log(Option.some(2).pipe(contains(2))) + * // Output: true + * + * console.log(Option.some(1).pipe(contains(2))) + * // Output: false + * + * console.log(Option.none().pipe(contains(2))) + * // Output: false + * ``` + * + * @see {@link contains} for a version that uses the default `Equivalence`. + * + * @category Elements + * @since 2.0.0 + */ +export const containsWith = (isEquivalent: (self: A, that: A) => boolean): { + (a: A): (self: Option) => boolean + (self: Option, a: A): boolean +} => dual(2, (self: Option, a: A): boolean => isNone(self) ? false : isEquivalent(self.value, a)) + +const _equivalence = Equal.equivalence() + +/** + * Returns a function that checks if an `Option` contains a specified value + * using the default `Equivalence`. + * + * **Details** + * + * This function allows you to check whether an `Option` contains a specific + * value. It uses the default `Equivalence` for equality comparison. If the + * `Option` is `Some` and its value is equivalent to the provided value, the + * result is `true`. If the `Option` is `None` or the values are not equivalent, + * the result is `false`. + * + * @example + * ```ts + * import { Option } from "effect" + * + * console.log(Option.some(2).pipe(Option.contains(2))) + * // Output: true + * + * console.log(Option.some(1).pipe(Option.contains(2))) + * // Output: false + * + * console.log(Option.none().pipe(Option.contains(2))) + * // Output: false + * ``` + * + * @see {@link containsWith} for a version that allows you to specify a custom equivalence function. + * + * @category Elements + * @since 2.0.0 + */ +export const contains: { + (a: A): (self: Option) => boolean + (self: Option, a: A): boolean +} = containsWith(_equivalence) + +/** + * Checks if a value in an `Option` satisfies a given predicate or refinement. + * + * **Details** + * + * This function allows you to check if a value inside a `Some` meets a + * specified condition. If the `Option` is `None`, the result is `false`. If the + * `Option` is `Some`, the provided predicate or refinement is applied to the + * value: + * + * - If the condition is met, the result is `true`. + * - If the condition is not met, the result is `false`. + * + * @example + * ```ts + * import { Option } from "effect" + * + * const isEven = (n: number) => n % 2 === 0 + * + * console.log(Option.some(2).pipe(Option.exists(isEven))) + * // Output: true + * + * console.log(Option.some(1).pipe(Option.exists(isEven))) + * // Output: false + * + * console.log(Option.none().pipe(Option.exists(isEven))) + * // Output: false + * ``` + * + * @category Elements + * @since 2.0.0 + */ +export const exists: { + (refinement: Refinement, B>): (self: Option) => self is Option + (predicate: Predicate>): (self: Option) => boolean + (self: Option, refinement: Refinement): self is Option + (self: Option, predicate: Predicate): boolean +} = dual( + 2, + (self: Option, refinement: Refinement): self is Option => + isNone(self) ? false : refinement(self.value) +) + +// ------------------------------------------------------------------------------------- +// do notation +// ------------------------------------------------------------------------------------- + +/** + * The "do simulation" in Effect allows you to write code in a more declarative style, similar to the "do notation" in other programming languages. It provides a way to define variables and perform operations on them using functions like `bind` and `let`. + * + * Here's how the do simulation works: + * + * 1. Start the do simulation using the `Do` value + * 2. Within the do simulation scope, you can use the `bind` function to define variables and bind them to `Option` values + * 3. You can accumulate multiple `bind` statements to define multiple variables within the scope + * 4. Inside the do simulation scope, you can also use the `let` function to define variables and bind them to simple values + * 5. Regular `Option` functions like `map` and `filter` can still be used within the do simulation. These functions will receive the accumulated variables as arguments within the scope + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { Option, pipe } from "effect" + * + * const result = pipe( + * Option.Do, + * Option.bind("x", () => Option.some(2)), + * Option.bind("y", () => Option.some(3)), + * Option.let("sum", ({ x, y }) => x + y), + * Option.filter(({ x, y }) => x * y > 5) + * ) + * assert.deepStrictEqual(result, Option.some({ x: 2, y: 3, sum: 5 })) + * ``` + * + * @see {@link Do} + * @see {@link bind} + * @see {@link let_ let} + * + * @category Do notation + * @since 2.0.0 + */ +export const bindTo: { + (name: N): (self: Option) => Option<{ [K in N]: A }> + (self: Option, name: N): Option<{ [K in N]: A }> +} = doNotation.bindTo(map) + +const let_: { + ( + name: Exclude, + f: (a: NoInfer) => B + ): (self: Option) => Option<{ [K in N | keyof A]: K extends keyof A ? A[K] : B }> + ( + self: Option, + name: Exclude, + f: (a: NoInfer) => B + ): Option<{ [K in N | keyof A]: K extends keyof A ? A[K] : B }> +} = doNotation.let_(map) + +export { + /** + * The "do simulation" in Effect allows you to write code in a more declarative style, similar to the "do notation" in other programming languages. It provides a way to define variables and perform operations on them using functions like `bind` and `let`. + * + * Here's how the do simulation works: + * + * 1. Start the do simulation using the `Do` value + * 2. Within the do simulation scope, you can use the `bind` function to define variables and bind them to `Option` values + * 3. You can accumulate multiple `bind` statements to define multiple variables within the scope + * 4. Inside the do simulation scope, you can also use the `let` function to define variables and bind them to simple values + * 5. Regular `Option` functions like `map` and `filter` can still be used within the do simulation. These functions will receive the accumulated variables as arguments within the scope + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { Option, pipe } from "effect" + * + * const result = pipe( + * Option.Do, + * Option.bind("x", () => Option.some(2)), + * Option.bind("y", () => Option.some(3)), + * Option.let("sum", ({ x, y }) => x + y), + * Option.filter(({ x, y }) => x * y > 5) + * ) + * assert.deepStrictEqual(result, Option.some({ x: 2, y: 3, sum: 5 })) + * ``` + * + * @see {@link Do} + * @see {@link bind} + * @see {@link bindTo} + * + * @category Do notation + * @since 2.0.0 + */ + let_ as let +} + +/** + * The "do simulation" in Effect allows you to write code in a more declarative style, similar to the "do notation" in other programming languages. It provides a way to define variables and perform operations on them using functions like `bind` and `let`. + * + * Here's how the do simulation works: + * + * 1. Start the do simulation using the `Do` value + * 2. Within the do simulation scope, you can use the `bind` function to define variables and bind them to `Option` values + * 3. You can accumulate multiple `bind` statements to define multiple variables within the scope + * 4. Inside the do simulation scope, you can also use the `let` function to define variables and bind them to simple values + * 5. Regular `Option` functions like `map` and `filter` can still be used within the do simulation. These functions will receive the accumulated variables as arguments within the scope + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { Option, pipe } from "effect" + * + * const result = pipe( + * Option.Do, + * Option.bind("x", () => Option.some(2)), + * Option.bind("y", () => Option.some(3)), + * Option.let("sum", ({ x, y }) => x + y), + * Option.filter(({ x, y }) => x * y > 5) + * ) + * assert.deepStrictEqual(result, Option.some({ x: 2, y: 3, sum: 5 })) + * ``` + * + * @see {@link Do} + * @see {@link bindTo} + * @see {@link let_ let} + * + * @category Do notation + * @since 2.0.0 + */ +export const bind: { + ( + name: Exclude, + f: (a: NoInfer) => Option + ): (self: Option) => Option<{ [K in N | keyof A]: K extends keyof A ? A[K] : B }> + ( + self: Option, + name: Exclude, + f: (a: NoInfer) => Option + ): Option<{ [K in N | keyof A]: K extends keyof A ? A[K] : B }> +} = doNotation.bind(map, flatMap) + +/** + * The "do simulation" in Effect allows you to write code in a more declarative style, similar to the "do notation" in other programming languages. It provides a way to define variables and perform operations on them using functions like `bind` and `let`. + * + * Here's how the do simulation works: + * + * 1. Start the do simulation using the `Do` value + * 2. Within the do simulation scope, you can use the `bind` function to define variables and bind them to `Option` values + * 3. You can accumulate multiple `bind` statements to define multiple variables within the scope + * 4. Inside the do simulation scope, you can also use the `let` function to define variables and bind them to simple values + * 5. Regular `Option` functions like `map` and `filter` can still be used within the do simulation. These functions will receive the accumulated variables as arguments within the scope + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { Option, pipe } from "effect" + * + * const result = pipe( + * Option.Do, + * Option.bind("x", () => Option.some(2)), + * Option.bind("y", () => Option.some(3)), + * Option.let("sum", ({ x, y }) => x + y), + * Option.filter(({ x, y }) => x * y > 5) + * ) + * assert.deepStrictEqual(result, Option.some({ x: 2, y: 3, sum: 5 })) + * ``` + * + * @see {@link bindTo} + * @see {@link bind} + * @see {@link let_ let} + * + * @category Do notation + * @since 2.0.0 + */ +export const Do: Option<{}> = some({}) + +const adapter = Gen.adapter() + +/** + * Similar to `Effect.gen`, `Option.gen` provides a more readable, + * generator-based syntax for working with `Option` values, making code that + * involves `Option` easier to write and understand. This approach is similar to + * using `async/await` but tailored for `Option`. + * + * **Example** (Using `Option.gen` to Create a Combined Value) + * + * ```ts + * import { Option } from "effect" + * + * const maybeName: Option.Option = Option.some("John") + * const maybeAge: Option.Option = Option.some(25) + * + * const person = Option.gen(function* () { + * const name = (yield* maybeName).toUpperCase() + * const age = yield* maybeAge + * return { name, age } + * }) + * + * console.log(person) + * // Output: + * // { _id: 'Option', _tag: 'Some', value: { name: 'JOHN', age: 25 } } + * ``` + * + * @category Generators + * @since 2.0.0 + */ +export const gen: Gen.Gen> = (...args) => { + const f = args.length === 1 ? args[0] : args[1].bind(args[0]) + const iterator = f(adapter) + let state: IteratorResult = iterator.next() + while (!state.done) { + const current = Gen.isGenKind(state.value) + ? state.value.value + : Gen.yieldWrapGet(state.value) + if (isNone(current)) { + return current + } + state = iterator.next(current.value as never) + } + return some(state.value) +} + +/** + * Merges two optional values, applying a function if both exist. + * Unlike {@link zipWith}, this function returns `None` only if both inputs are `None`. + * + * @internal + */ +export const mergeWith = (f: (a1: A, a2: A) => A) => (o1: Option, o2: Option): Option => { + if (isNone(o1)) { + return o2 + } else if (isNone(o2)) { + return o1 + } + return some(f(o1.value, o2.value)) +} diff --git a/repos/effect/packages/effect/src/Order.ts b/repos/effect/packages/effect/src/Order.ts new file mode 100644 index 0000000..c4cce40 --- /dev/null +++ b/repos/effect/packages/effect/src/Order.ts @@ -0,0 +1,373 @@ +/** + * This module provides an implementation of the `Order` type class which is used to define a total ordering on some type `A`. + * An order is defined by a relation `<=`, which obeys the following laws: + * + * - either `x <= y` or `y <= x` (totality) + * - if `x <= y` and `y <= x`, then `x == y` (antisymmetry) + * - if `x <= y` and `y <= z`, then `x <= z` (transitivity) + * + * The truth table for compare is defined as follows: + * + * | `x <= y` | `x >= y` | Ordering | | + * | -------- | -------- | -------- | --------------------- | + * | `true` | `true` | `0` | corresponds to x == y | + * | `true` | `false` | `< 0` | corresponds to x < y | + * | `false` | `true` | `> 0` | corresponds to x > y | + * + * @since 2.0.0 + */ +import { dual } from "./Function.js" +import type { TypeLambda } from "./HKT.js" + +/** + * @category type class + * @since 2.0.0 + */ +export interface Order { + (self: A, that: A): -1 | 0 | 1 +} + +/** + * @category type lambdas + * @since 2.0.0 + */ +export interface OrderTypeLambda extends TypeLambda { + readonly type: Order +} + +/** + * @category constructors + * @since 2.0.0 + */ +export const make = ( + compare: (self: A, that: A) => -1 | 0 | 1 +): Order => +(self, that) => self === that ? 0 : compare(self, that) + +/** + * @category instances + * @since 2.0.0 + */ +export const string: Order = make((self, that) => self < that ? -1 : 1) + +/** + * @category instances + * @since 2.0.0 + */ +export const number: Order = make((self, that) => self < that ? -1 : 1) + +/** + * @category instances + * @since 2.0.0 + */ +export const boolean: Order = make((self, that) => self < that ? -1 : 1) + +/** + * @category instances + * @since 2.0.0 + */ +export const bigint: Order = make((self, that) => self < that ? -1 : 1) + +/** + * @since 2.0.0 + */ +export const reverse = (O: Order): Order => make((self, that) => O(that, self)) + +/** + * @category combining + * @since 2.0.0 + */ +export const combine: { + (that: Order): (self: Order) => Order + (self: Order, that: Order): Order +} = dual(2, (self: Order, that: Order): Order => + make((a1, a2) => { + const out = self(a1, a2) + if (out !== 0) { + return out + } + return that(a1, a2) + })) + +/** + * @category combining + * @since 2.0.0 + */ +export const combineMany: { + (collection: Iterable>): (self: Order) => Order + (self: Order, collection: Iterable>): Order +} = dual(2, (self: Order, collection: Iterable>): Order => + make((a1, a2) => { + let out = self(a1, a2) + if (out !== 0) { + return out + } + for (const O of collection) { + out = O(a1, a2) + if (out !== 0) { + return out + } + } + return out + })) + +/** + * @since 2.0.0 + */ +export const empty = (): Order => make(() => 0) + +/** + * @category combining + * @since 2.0.0 + */ +export const combineAll = (collection: Iterable>): Order => combineMany(empty(), collection) + +/** + * @category mapping + * @since 2.0.0 + */ +export const mapInput: { + (f: (b: B) => A): (self: Order) => Order + (self: Order, f: (b: B) => A): Order +} = dual( + 2, + (self: Order, f: (b: B) => A): Order => make((b1, b2) => self(f(b1), f(b2))) +) + +/** + * @category instances + * @since 2.0.0 + */ +export const Date: Order = mapInput(number, (date) => date.getTime()) + +/** + * @category combining + * @since 2.0.0 + */ +export const product: { + (that: Order): (self: Order) => Order // readonly because invariant + (self: Order, that: Order): Order // readonly because invariant +} = dual(2, (self: Order, that: Order): Order => + make(([xa, xb], [ya, yb]) => { + const o = self(xa, ya) + return o !== 0 ? o : that(xb, yb) + })) + +/** + * @category combining + * @since 2.0.0 + */ +export const all = (collection: Iterable>): Order> => { + return make((x, y) => { + const len = Math.min(x.length, y.length) + let collectionLength = 0 + for (const O of collection) { + if (collectionLength >= len) { + break + } + const o = O(x[collectionLength], y[collectionLength]) + if (o !== 0) { + return o + } + collectionLength++ + } + return 0 + }) +} + +/** + * @category combining + * @since 2.0.0 + */ +export const productMany: { + (collection: Iterable>): (self: Order) => Order]> // readonly because invariant + (self: Order, collection: Iterable>): Order]> // readonly because invariant +} = dual(2, (self: Order, collection: Iterable>): Order]> => { + const O = all(collection) + return make((x, y) => { + const o = self(x[0], y[0]) + return o !== 0 ? o : O(x.slice(1), y.slice(1)) + }) +}) + +/** + * Similar to `Promise.all` but operates on `Order`s. + * + * ``` + * [Order, Order, ...] -> Order<[A, B, ...]> + * ``` + * + * This function creates and returns a new `Order` for a tuple of values based on the given `Order`s for each element in the tuple. + * The returned `Order` compares two tuples of the same type by applying the corresponding `Order` to each element in the tuple. + * It is useful when you need to compare two tuples of the same type and you have a specific way of comparing each element + * of the tuple. + * + * @category combinators + * @since 2.0.0 + */ +export const tuple = >>( + ...elements: T +): Order] ? A : never }>> => all(elements) as any + +/** + * This function creates and returns a new `Order` for an array of values based on a given `Order` for the elements of the array. + * The returned `Order` compares two arrays by applying the given `Order` to each element in the arrays. + * If all elements are equal, the arrays are then compared based on their length. + * It is useful when you need to compare two arrays of the same type and you have a specific way of comparing each element of the array. + * + * @category combinators + * @since 2.0.0 + */ +export const array = (O: Order): Order> => + make((self, that) => { + const aLen = self.length + const bLen = that.length + const len = Math.min(aLen, bLen) + for (let i = 0; i < len; i++) { + const o = O(self[i], that[i]) + if (o !== 0) { + return o + } + } + return number(aLen, bLen) + }) + +/** + * This function creates and returns a new `Order` for a struct of values based on the given `Order`s + * for each property in the struct. + * + * @category combinators + * @since 2.0.0 + */ +export const struct = }>( + fields: R +): Order<{ [K in keyof R]: [R[K]] extends [Order] ? A : never }> => { + const keys = Object.keys(fields) + return make((self, that) => { + for (const key of keys) { + const o = fields[key](self[key], that[key]) + if (o !== 0) { + return o + } + } + return 0 + }) +} + +/** + * Test whether one value is _strictly less than_ another. + * + * @since 2.0.0 + */ +export const lessThan = (O: Order): { + (that: A): (self: A) => boolean + (self: A, that: A): boolean +} => dual(2, (self: A, that: A) => O(self, that) === -1) + +/** + * Test whether one value is _strictly greater than_ another. + * + * @since 2.0.0 + */ +export const greaterThan = (O: Order): { + (that: A): (self: A) => boolean + (self: A, that: A): boolean +} => dual(2, (self: A, that: A) => O(self, that) === 1) + +/** + * Test whether one value is _non-strictly less than_ another. + * + * @since 2.0.0 + */ +export const lessThanOrEqualTo = (O: Order): { + (that: A): (self: A) => boolean + (self: A, that: A): boolean +} => dual(2, (self: A, that: A) => O(self, that) !== 1) + +/** + * Test whether one value is _non-strictly greater than_ another. + * + * @since 2.0.0 + */ +export const greaterThanOrEqualTo = (O: Order): { + (that: A): (self: A) => boolean + (self: A, that: A): boolean +} => dual(2, (self: A, that: A) => O(self, that) !== -1) + +/** + * Take the minimum of two values. If they are considered equal, the first argument is chosen. + * + * @since 2.0.0 + */ +export const min = (O: Order): { + (that: A): (self: A) => A + (self: A, that: A): A +} => dual(2, (self: A, that: A) => self === that || O(self, that) < 1 ? self : that) + +/** + * Take the maximum of two values. If they are considered equal, the first argument is chosen. + * + * @since 2.0.0 + */ +export const max = (O: Order): { + (that: A): (self: A) => A + (self: A, that: A): A +} => dual(2, (self: A, that: A) => self === that || O(self, that) > -1 ? self : that) + +/** + * Clamp a value between a minimum and a maximum. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { Order, Number } from "effect" + * + * const clamp = Order.clamp(Number.Order)({ minimum: 1, maximum: 5 }) + * + * assert.equal(clamp(3), 3) + * assert.equal(clamp(0), 1) + * assert.equal(clamp(6), 5) + * ``` + * + * @since 2.0.0 + */ +export const clamp = (O: Order): { + (options: { + minimum: A + maximum: A + }): (self: A) => A + (self: A, options: { + minimum: A + maximum: A + }): A +} => + dual( + 2, + (self: A, options: { + minimum: A + maximum: A + }): A => min(O)(options.maximum, max(O)(options.minimum, self)) + ) + +/** + * Test whether a value is between a minimum and a maximum (inclusive). + * + * @since 2.0.0 + */ +export const between = (O: Order): { + (options: { + minimum: A + maximum: A + }): (self: A) => boolean + (self: A, options: { + minimum: A + maximum: A + }): boolean +} => + dual( + 2, + (self: A, options: { + minimum: A + maximum: A + }): boolean => !lessThan(O)(self, options.minimum) && !greaterThan(O)(self, options.maximum) + ) diff --git a/repos/effect/packages/effect/src/Ordering.ts b/repos/effect/packages/effect/src/Ordering.ts new file mode 100644 index 0000000..73f8812 --- /dev/null +++ b/repos/effect/packages/effect/src/Ordering.ts @@ -0,0 +1,111 @@ +/** + * @since 2.0.0 + */ +import type { LazyArg } from "./Function.js" +import { dual } from "./Function.js" + +/** + * @category model + * @since 2.0.0 + */ +export type Ordering = -1 | 0 | 1 + +/** + * Inverts the ordering of the input `Ordering`. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { reverse } from "effect/Ordering" + * + * assert.deepStrictEqual(reverse(1), -1) + * assert.deepStrictEqual(reverse(-1), 1) + * assert.deepStrictEqual(reverse(0), 0) + * ``` + * + * @since 2.0.0 + */ +export const reverse = (o: Ordering): Ordering => (o === -1 ? 1 : o === 1 ? -1 : 0) + +/** + * Depending on the `Ordering` parameter given to it, returns a value produced by one of the 3 functions provided as parameters. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { Ordering } from "effect" + * import { constant } from "effect/Function" + * + * const toMessage = Ordering.match({ + * onLessThan: constant('less than'), + * onEqual: constant('equal'), + * onGreaterThan: constant('greater than') + * }) + * + * assert.deepStrictEqual(toMessage(-1), "less than") + * assert.deepStrictEqual(toMessage(0), "equal") + * assert.deepStrictEqual(toMessage(1), "greater than") + * ``` + * + * @category pattern matching + * @since 2.0.0 + */ +export const match: { + ( + options: { + readonly onLessThan: LazyArg + readonly onEqual: LazyArg + readonly onGreaterThan: LazyArg + } + ): (self: Ordering) => A | B | C + ( + o: Ordering, + options: { + readonly onLessThan: LazyArg + readonly onEqual: LazyArg + readonly onGreaterThan: LazyArg + } + ): A | B | C +} = dual(2, ( + self: Ordering, + { onEqual, onGreaterThan, onLessThan }: { + readonly onLessThan: LazyArg + readonly onEqual: LazyArg + readonly onGreaterThan: LazyArg + } +): A | B | C => self === -1 ? onLessThan() : self === 0 ? onEqual() : onGreaterThan()) + +/** + * @category combining + * @since 2.0.0 + */ +export const combine: { + (that: Ordering): (self: Ordering) => Ordering + (self: Ordering, that: Ordering): Ordering +} = dual(2, (self: Ordering, that: Ordering): Ordering => self !== 0 ? self : that) + +/** + * @category combining + * @since 2.0.0 + */ +export const combineMany: { + (collection: Iterable): (self: Ordering) => Ordering + (self: Ordering, collection: Iterable): Ordering +} = dual(2, (self: Ordering, collection: Iterable): Ordering => { + let ordering = self + if (ordering !== 0) { + return ordering + } + for (ordering of collection) { + if (ordering !== 0) { + return ordering + } + } + return ordering +}) + +/** + * @category combining + * @since 2.0.0 + */ +export const combineAll = (collection: Iterable): Ordering => combineMany(0, collection) diff --git a/repos/effect/packages/effect/src/ParseResult.ts b/repos/effect/packages/effect/src/ParseResult.ts new file mode 100644 index 0000000..c89f829 --- /dev/null +++ b/repos/effect/packages/effect/src/ParseResult.ts @@ -0,0 +1,2030 @@ +/** + * @since 3.10.0 + */ + +import * as Arr from "./Array.js" +import * as Cause from "./Cause.js" +import { TaggedError } from "./Data.js" +import * as Effect from "./Effect.js" +import * as Either from "./Either.js" +import * as Exit from "./Exit.js" +import type { LazyArg } from "./Function.js" +import { dual } from "./Function.js" +import { globalValue } from "./GlobalValue.js" +import * as Inspectable from "./Inspectable.js" +import * as util_ from "./internal/schema/util.js" +import * as Option from "./Option.js" +import * as Predicate from "./Predicate.js" +import * as Scheduler from "./Scheduler.js" +import type * as Schema from "./Schema.js" +import * as AST from "./SchemaAST.js" +import type { Concurrency } from "./Types.js" + +/** + * `ParseIssue` is a type that represents the different types of errors that can occur when decoding/encoding a value. + * + * @category model + * @since 3.10.0 + */ +export type ParseIssue = + // leaf + | Type + | Missing + | Unexpected + | Forbidden + // composite + | Pointer + | Refinement + | Transformation + | Composite + +/** + * @category model + * @since 3.10.0 + */ +export type SingleOrNonEmpty = A | Arr.NonEmptyReadonlyArray + +/** + * @category model + * @since 3.10.0 + */ +export type Path = SingleOrNonEmpty + +/** + * @category model + * @since 3.10.0 + */ +export class Pointer { + /** + * @since 3.10.0 + */ + readonly _tag = "Pointer" + constructor( + readonly path: Path, + readonly actual: unknown, + readonly issue: ParseIssue + ) {} +} + +/** + * Error that occurs when an unexpected key or index is present. + * + * @category model + * @since 3.10.0 + */ +export class Unexpected { + /** + * @since 3.10.0 + */ + readonly _tag = "Unexpected" + constructor( + readonly actual: unknown, + /** + * @since 3.10.0 + */ + readonly message?: string + ) {} +} + +/** + * Error that occurs when a required key or index is missing. + * + * @category model + * @since 3.10.0 + */ +export class Missing { + /** + * @since 3.10.0 + */ + readonly _tag = "Missing" + /** + * @since 3.10.0 + */ + readonly actual = undefined + constructor( + /** + * @since 3.10.0 + */ + readonly ast: AST.Type, + /** + * @since 3.10.0 + */ + readonly message?: string + ) {} +} + +/** + * Error that contains multiple issues. + * + * @category model + * @since 3.10.0 + */ +export class Composite { + /** + * @since 3.10.0 + */ + readonly _tag = "Composite" + constructor( + readonly ast: AST.AST, + readonly actual: unknown, + readonly issues: SingleOrNonEmpty, + readonly output?: unknown + ) {} +} + +/** + * Error that occurs when a refinement has an error. + * + * @category model + * @since 3.10.0 + */ +export class Refinement { + /** + * @since 3.10.0 + */ + readonly _tag = "Refinement" + constructor( + readonly ast: AST.Refinement, + readonly actual: unknown, + readonly kind: "From" | "Predicate", + readonly issue: ParseIssue + ) {} +} + +/** + * Error that occurs when a transformation has an error. + * + * @category model + * @since 3.10.0 + */ +export class Transformation { + /** + * @since 3.10.0 + */ + readonly _tag = "Transformation" + constructor( + readonly ast: AST.Transformation, + readonly actual: unknown, + readonly kind: "Encoded" | "Transformation" | "Type", + readonly issue: ParseIssue + ) {} +} + +/** + * The `Type` variant of the `ParseIssue` type represents an error that occurs when the `actual` value is not of the expected type. + * The `ast` field specifies the expected type, and the `actual` field contains the value that caused the error. + * + * @category model + * @since 3.10.0 + */ +export class Type { + /** + * @since 3.10.0 + */ + readonly _tag = "Type" + constructor( + readonly ast: AST.AST, + readonly actual: unknown, + readonly message?: string + ) {} +} + +/** + * The `Forbidden` variant of the `ParseIssue` type represents a forbidden operation, such as when encountering an Effect that is not allowed to execute (e.g., using `runSync`). + * + * @category model + * @since 3.10.0 + */ +export class Forbidden { + /** + * @since 3.10.0 + */ + readonly _tag = "Forbidden" + constructor( + readonly ast: AST.AST, + readonly actual: unknown, + readonly message?: string + ) {} +} + +/** + * @category type id + * @since 3.10.0 + */ +export const ParseErrorTypeId: unique symbol = Symbol.for("effect/Schema/ParseErrorTypeId") + +/** + * @category type id + * @since 3.10.0 + */ +export type ParseErrorTypeId = typeof ParseErrorTypeId + +/** + * @since 3.10.0 + */ +export const isParseError = (u: unknown): u is ParseError => Predicate.hasProperty(u, ParseErrorTypeId) + +/** + * @since 3.10.0 + */ +export class ParseError extends TaggedError("ParseError")<{ readonly issue: ParseIssue }> { + /** + * @since 3.10.0 + */ + readonly [ParseErrorTypeId] = ParseErrorTypeId + + get message() { + return this.toString() + } + /** + * @since 3.10.0 + */ + toString() { + return TreeFormatter.formatIssueSync(this.issue) + } + /** + * @since 3.10.0 + */ + toJSON() { + return { + _id: "ParseError", + message: this.toString() + } + } + /** + * @since 3.10.0 + */ + [Inspectable.NodeInspectSymbol]() { + return this.toJSON() + } +} + +/** + * @category constructors + * @since 3.10.0 + */ +export const parseError = (issue: ParseIssue): ParseError => new ParseError({ issue }) + +/** + * @category constructors + * @since 3.10.0 + */ +export const succeed: (a: A) => Either.Either = Either.right + +/** + * @category constructors + * @since 3.10.0 + */ +export const fail: (issue: ParseIssue) => Either.Either = Either.left + +const _try: (options: { + try: LazyArg + catch: (e: unknown) => ParseIssue +}) => Either.Either = Either.try + +export { + /** + * @category constructors + * @since 3.10.0 + */ + _try as try +} + +/** + * @category constructors + * @since 3.10.0 + */ +export const fromOption: { + (onNone: () => ParseIssue): (self: Option.Option) => Either.Either + (self: Option.Option, onNone: () => ParseIssue): Either.Either +} = Either.fromOption + +const isEither: (self: Effect.Effect) => self is Either.Either = Either.isEither as any + +/** + * @category optimisation + * @since 3.10.0 + */ +export const flatMap: { + ( + f: (a: A) => Effect.Effect + ): (self: Effect.Effect) => Effect.Effect + ( + self: Effect.Effect, + f: (a: A) => Effect.Effect + ): Effect.Effect +} = dual(2, ( + self: Effect.Effect, + f: (a: A) => Effect.Effect +): Effect.Effect => { + return isEither(self) ? + Either.match(self, { onLeft: Either.left, onRight: f }) : + Effect.flatMap(self, f) +}) + +/** + * @category optimisation + * @since 3.10.0 + */ +export const map: { + (f: (a: A) => B): (self: Effect.Effect) => Effect.Effect + (self: Effect.Effect, f: (a: A) => B): Effect.Effect +} = dual(2, (self: Effect.Effect, f: (a: A) => B): Effect.Effect => { + return isEither(self) ? + Either.map(self, f) : + Effect.map(self, f) +}) + +/** + * @category optimisation + * @since 3.10.0 + */ +export const mapError: { + (f: (e: E) => E2): (self: Effect.Effect) => Effect.Effect + (self: Effect.Effect, f: (e: E) => E2): Effect.Effect +} = dual(2, (self: Effect.Effect, f: (e: E) => E2): Effect.Effect => { + return isEither(self) ? + Either.mapLeft(self, f) : + Effect.mapError(self, f) +}) + +// TODO(4.0): remove +/** + * @category optimisation + * @since 3.10.0 + */ +export const eitherOrUndefined = ( + self: Effect.Effect +): Either.Either | undefined => { + if (isEither(self)) { + return self + } +} + +/** + * @category optimisation + * @since 3.10.0 + */ +export const mapBoth: { + ( + options: { readonly onFailure: (e: E) => E2; readonly onSuccess: (a: A) => A2 } + ): (self: Effect.Effect) => Effect.Effect + ( + self: Effect.Effect, + options: { readonly onFailure: (e: E) => E2; readonly onSuccess: (a: A) => A2 } + ): Effect.Effect +} = dual(2, ( + self: Effect.Effect, + options: { readonly onFailure: (e: E) => E2; readonly onSuccess: (a: A) => A2 } +): Effect.Effect => { + return isEither(self) ? + Either.mapBoth(self, { onLeft: options.onFailure, onRight: options.onSuccess }) : + Effect.mapBoth(self, options) +}) + +/** + * @category optimisation + * @since 3.10.0 + */ +export const orElse: { + ( + f: (e: E) => Effect.Effect + ): (self: Effect.Effect) => Effect.Effect + ( + self: Effect.Effect, + f: (e: E) => Effect.Effect + ): Effect.Effect +} = dual(2, ( + self: Effect.Effect, + f: (e: E) => Effect.Effect +): Effect.Effect => { + return isEither(self) ? + Either.match(self, { onLeft: f, onRight: Either.right }) : + Effect.catchAll(self, f) +}) + +/** + * @since 3.10.0 + */ +export type DecodeUnknown = (u: unknown, options?: AST.ParseOptions) => Effect.Effect + +/** + * @since 3.10.0 + */ +export type DeclarationDecodeUnknown = ( + u: unknown, + options: AST.ParseOptions, + ast: AST.Declaration +) => Effect.Effect + +/** @internal */ +export const mergeInternalOptions = ( + options: InternalOptions | undefined, + overrideOptions: InternalOptions | number | undefined +): InternalOptions | undefined => { + if (overrideOptions === undefined || Predicate.isNumber(overrideOptions)) { + return options + } + if (options === undefined) { + return overrideOptions + } + return { ...options, ...overrideOptions } +} + +const getEither = (ast: AST.AST, isDecoding: boolean, options?: AST.ParseOptions) => { + const parser = goMemo(ast, isDecoding) + return (u: unknown, overrideOptions?: AST.ParseOptions): Either.Either => + parser(u, mergeInternalOptions(options, overrideOptions)) as any +} + +const getSync = (ast: AST.AST, isDecoding: boolean, options?: AST.ParseOptions) => { + const parser = getEither(ast, isDecoding, options) + return (input: unknown, overrideOptions?: AST.ParseOptions) => + Either.getOrThrowWith(parser(input, overrideOptions), parseError) +} + +/** @internal */ +export const getOption = (ast: AST.AST, isDecoding: boolean, options?: AST.ParseOptions) => { + const parser = getEither(ast, isDecoding, options) + return (input: unknown, overrideOptions?: AST.ParseOptions): Option.Option => + Option.getRight(parser(input, overrideOptions)) +} + +const getEffect = (ast: AST.AST, isDecoding: boolean, options?: AST.ParseOptions) => { + const parser = goMemo(ast, isDecoding) + return (input: unknown, overrideOptions?: AST.ParseOptions): Effect.Effect => + parser(input, { ...mergeInternalOptions(options, overrideOptions), isEffectAllowed: true }) +} + +/** + * @throws `ParseError` + * @category decoding + * @since 3.10.0 + */ +export const decodeUnknownSync = ( + schema: Schema.Schema, + options?: AST.ParseOptions +): (u: unknown, overrideOptions?: AST.ParseOptions) => A => getSync(schema.ast, true, options) + +/** + * @category decoding + * @since 3.10.0 + */ +export const decodeUnknownOption = ( + schema: Schema.Schema, + options?: AST.ParseOptions +): (u: unknown, overrideOptions?: AST.ParseOptions) => Option.Option => getOption(schema.ast, true, options) + +/** + * @category decoding + * @since 3.10.0 + */ +export const decodeUnknownEither = ( + schema: Schema.Schema, + options?: AST.ParseOptions +): (u: unknown, overrideOptions?: AST.ParseOptions) => Either.Either => + getEither(schema.ast, true, options) + +/** + * @category decoding + * @since 3.10.0 + */ +export const decodeUnknownPromise = ( + schema: Schema.Schema, + options?: AST.ParseOptions +) => { + const parser = decodeUnknown(schema, options) + return (u: unknown, overrideOptions?: AST.ParseOptions): Promise => Effect.runPromise(parser(u, overrideOptions)) +} + +/** + * @category decoding + * @since 3.10.0 + */ +export const decodeUnknown = ( + schema: Schema.Schema, + options?: AST.ParseOptions +): (u: unknown, overrideOptions?: AST.ParseOptions) => Effect.Effect => + getEffect(schema.ast, true, options) + +/** + * @throws `ParseError` + * @category encoding + * @since 3.10.0 + */ +export const encodeUnknownSync = ( + schema: Schema.Schema, + options?: AST.ParseOptions +): (u: unknown, overrideOptions?: AST.ParseOptions) => I => getSync(schema.ast, false, options) + +/** + * @category encoding + * @since 3.10.0 + */ +export const encodeUnknownOption = ( + schema: Schema.Schema, + options?: AST.ParseOptions +): (u: unknown, overrideOptions?: AST.ParseOptions) => Option.Option => getOption(schema.ast, false, options) + +/** + * @category encoding + * @since 3.10.0 + */ +export const encodeUnknownEither = ( + schema: Schema.Schema, + options?: AST.ParseOptions +): (u: unknown, overrideOptions?: AST.ParseOptions) => Either.Either => + getEither(schema.ast, false, options) + +/** + * @category encoding + * @since 3.10.0 + */ +export const encodeUnknownPromise = ( + schema: Schema.Schema, + options?: AST.ParseOptions +) => { + const parser = encodeUnknown(schema, options) + return (u: unknown, overrideOptions?: AST.ParseOptions): Promise => Effect.runPromise(parser(u, overrideOptions)) +} + +/** + * @category encoding + * @since 3.10.0 + */ +export const encodeUnknown = ( + schema: Schema.Schema, + options?: AST.ParseOptions +): (u: unknown, overrideOptions?: AST.ParseOptions) => Effect.Effect => + getEffect(schema.ast, false, options) + +/** + * @category decoding + * @since 3.10.0 + */ +export const decodeSync: ( + schema: Schema.Schema, + options?: AST.ParseOptions +) => (i: I, overrideOptions?: AST.ParseOptions) => A = decodeUnknownSync + +/** + * @category decoding + * @since 3.10.0 + */ +export const decodeOption: ( + schema: Schema.Schema, + options?: AST.ParseOptions +) => (i: I, overrideOptions?: AST.ParseOptions) => Option.Option = decodeUnknownOption + +/** + * @category decoding + * @since 3.10.0 + */ +export const decodeEither: ( + schema: Schema.Schema, + options?: AST.ParseOptions +) => (i: I, overrideOptions?: AST.ParseOptions) => Either.Either = decodeUnknownEither + +/** + * @category decoding + * @since 3.10.0 + */ +export const decodePromise: ( + schema: Schema.Schema, + options?: AST.ParseOptions +) => (i: I, overrideOptions?: AST.ParseOptions) => Promise = decodeUnknownPromise + +/** + * @category decoding + * @since 3.10.0 + */ +export const decode: ( + schema: Schema.Schema, + options?: AST.ParseOptions +) => (i: I, overrideOptions?: AST.ParseOptions) => Effect.Effect = decodeUnknown + +/** + * @throws `ParseError` + * @category validation + * @since 3.10.0 + */ +export const validateSync = ( + schema: Schema.Schema, + options?: AST.ParseOptions +): (u: unknown, overrideOptions?: AST.ParseOptions) => A => getSync(AST.typeAST(schema.ast), true, options) + +/** + * @category validation + * @since 3.10.0 + */ +export const validateOption = ( + schema: Schema.Schema, + options?: AST.ParseOptions +): (u: unknown, overrideOptions?: AST.ParseOptions) => Option.Option => + getOption(AST.typeAST(schema.ast), true, options) + +/** + * @category validation + * @since 3.10.0 + */ +export const validateEither = ( + schema: Schema.Schema, + options?: AST.ParseOptions +): (u: unknown, overrideOptions?: AST.ParseOptions) => Either.Either => + getEither(AST.typeAST(schema.ast), true, options) + +/** + * @category validation + * @since 3.10.0 + */ +export const validatePromise = ( + schema: Schema.Schema, + options?: AST.ParseOptions +) => { + const parser = validate(schema, options) + return (u: unknown, overrideOptions?: AST.ParseOptions): Promise => Effect.runPromise(parser(u, overrideOptions)) +} + +/** + * @category validation + * @since 3.10.0 + */ +export const validate = ( + schema: Schema.Schema, + options?: AST.ParseOptions +): (a: unknown, overrideOptions?: AST.ParseOptions) => Effect.Effect => + getEffect(AST.typeAST(schema.ast), true, options) + +/** + * By default the option `exact` is set to `true`. + * + * @category validation + * @since 3.10.0 + */ +export const is = (schema: Schema.Schema, options?: AST.ParseOptions) => { + const parser = goMemo(AST.typeAST(schema.ast), true) + return (u: unknown, overrideOptions?: AST.ParseOptions | number): u is A => + Either.isRight(parser(u, { exact: true, ...mergeInternalOptions(options, overrideOptions) }) as any) +} + +/** + * By default the option `exact` is set to `true`. + * + * @throws `ParseError` + * @category validation + * @since 3.10.0 + */ +export const asserts = (schema: Schema.Schema, options?: AST.ParseOptions) => { + const parser = goMemo(AST.typeAST(schema.ast), true) + return (u: unknown, overrideOptions?: AST.ParseOptions): asserts u is A => { + const result: Either.Either = parser(u, { + exact: true, + ...mergeInternalOptions(options, overrideOptions) + }) as any + if (Either.isLeft(result)) { + throw parseError(result.left) + } + } +} + +/** + * @category encoding + * @since 3.10.0 + */ +export const encodeSync: ( + schema: Schema.Schema, + options?: AST.ParseOptions +) => (a: A, overrideOptions?: AST.ParseOptions) => I = encodeUnknownSync + +/** + * @category encoding + * @since 3.10.0 + */ +export const encodeOption: ( + schema: Schema.Schema, + options?: AST.ParseOptions +) => (input: A, overrideOptions?: AST.ParseOptions) => Option.Option = encodeUnknownOption + +/** + * @category encoding + * @since 3.10.0 + */ +export const encodeEither: ( + schema: Schema.Schema, + options?: AST.ParseOptions +) => (a: A, overrideOptions?: AST.ParseOptions) => Either.Either = encodeUnknownEither + +/** + * @category encoding + * @since 3.10.0 + */ +export const encodePromise: ( + schema: Schema.Schema, + options?: AST.ParseOptions +) => (a: A, overrideOptions?: AST.ParseOptions) => Promise = encodeUnknownPromise + +/** + * @category encoding + * @since 3.10.0 + */ +export const encode: ( + schema: Schema.Schema, + options?: AST.ParseOptions +) => (a: A, overrideOptions?: AST.ParseOptions) => Effect.Effect = encodeUnknown + +interface InternalOptions extends AST.ParseOptions { + readonly isEffectAllowed?: boolean +} + +interface Parser { + (i: any, options?: InternalOptions): Effect.Effect +} + +const decodeMemoMap = globalValue( + Symbol.for("effect/ParseResult/decodeMemoMap"), + () => new WeakMap() +) +const encodeMemoMap = globalValue( + Symbol.for("effect/ParseResult/encodeMemoMap"), + () => new WeakMap() +) + +const goMemo = (ast: AST.AST, isDecoding: boolean): Parser => { + const memoMap = isDecoding ? decodeMemoMap : encodeMemoMap + const memo = memoMap.get(ast) + if (memo) { + return memo + } + const raw = go(ast, isDecoding) + const parseOptionsAnnotation = AST.getParseOptionsAnnotation(ast) + const parserWithOptions: Parser = Option.isSome(parseOptionsAnnotation) + ? (i, options) => raw(i, mergeInternalOptions(options, parseOptionsAnnotation.value)) + : raw + const decodingFallbackAnnotation = AST.getDecodingFallbackAnnotation(ast) + const parser: Parser = isDecoding && Option.isSome(decodingFallbackAnnotation) + ? (i, options) => + handleForbidden(orElse(parserWithOptions(i, options), decodingFallbackAnnotation.value), ast, i, options) + : parserWithOptions + memoMap.set(ast, parser) + return parser +} + +const getConcurrency = (ast: AST.AST): Concurrency | undefined => + Option.getOrUndefined(AST.getConcurrencyAnnotation(ast)) + +const getBatching = (ast: AST.AST): boolean | "inherit" | undefined => + Option.getOrUndefined(AST.getBatchingAnnotation(ast)) + +const go = (ast: AST.AST, isDecoding: boolean): Parser => { + switch (ast._tag) { + case "Refinement": { + if (isDecoding) { + const from = goMemo(ast.from, true) + return (i, options) => { + options = options ?? AST.defaultParseOption + const allErrors = options?.errors === "all" + const result = flatMap( + orElse(from(i, options), (ef) => { + const issue = new Refinement(ast, i, "From", ef) + if (allErrors && AST.hasStableFilter(ast) && isComposite(ef)) { + return Option.match( + ast.filter(i, options, ast), + { + onNone: () => Either.left(issue), + onSome: (ep) => Either.left(new Composite(ast, i, [issue, new Refinement(ast, i, "Predicate", ep)])) + } + ) + } + return Either.left(issue) + }), + (a) => + Option.match( + ast.filter(a, options, ast), + { + onNone: () => Either.right(a), + onSome: (ep) => Either.left(new Refinement(ast, i, "Predicate", ep)) + } + ) + ) + return handleForbidden(result, ast, i, options) + } + } else { + const from = goMemo(AST.typeAST(ast), true) + const to = goMemo(dropRightRefinement(ast.from), false) + return (i, options) => handleForbidden(flatMap(from(i, options), (a) => to(a, options)), ast, i, options) + } + } + case "Transformation": { + const transform = getFinalTransformation(ast.transformation, isDecoding) + const from = isDecoding ? goMemo(ast.from, true) : goMemo(ast.to, false) + const to = isDecoding ? goMemo(ast.to, true) : goMemo(ast.from, false) + return (i, options) => + handleForbidden( + flatMap( + mapError( + from(i, options), + (e) => new Transformation(ast, i, isDecoding ? "Encoded" : "Type", e) + ), + (a) => + flatMap( + mapError( + transform(a, options ?? AST.defaultParseOption, ast, i), + (e) => new Transformation(ast, i, "Transformation", e) + ), + (i2) => + mapError( + to(i2, options), + (e) => new Transformation(ast, i, isDecoding ? "Type" : "Encoded", e) + ) + ) + ), + ast, + i, + options + ) + } + case "Declaration": { + const parse = isDecoding + ? ast.decodeUnknown(...ast.typeParameters) + : ast.encodeUnknown(...ast.typeParameters) + return (i, options) => handleForbidden(parse(i, options ?? AST.defaultParseOption, ast), ast, i, options) + } + case "Literal": + return fromRefinement(ast, (u): u is typeof ast.literal => u === ast.literal) + case "UniqueSymbol": + return fromRefinement(ast, (u): u is typeof ast.symbol => u === ast.symbol) + case "UndefinedKeyword": + return fromRefinement(ast, Predicate.isUndefined) + case "NeverKeyword": + return fromRefinement(ast, Predicate.isNever) + case "UnknownKeyword": + case "AnyKeyword": + case "VoidKeyword": + return Either.right + case "StringKeyword": + return fromRefinement(ast, Predicate.isString) + case "NumberKeyword": + return fromRefinement(ast, Predicate.isNumber) + case "BooleanKeyword": + return fromRefinement(ast, Predicate.isBoolean) + case "BigIntKeyword": + return fromRefinement(ast, Predicate.isBigInt) + case "SymbolKeyword": + return fromRefinement(ast, Predicate.isSymbol) + case "ObjectKeyword": + return fromRefinement(ast, Predicate.isObject) + case "Enums": + return fromRefinement(ast, (u): u is any => ast.enums.some(([_, value]) => value === u)) + case "TemplateLiteral": { + const regex = AST.getTemplateLiteralRegExp(ast) + return fromRefinement(ast, (u): u is any => Predicate.isString(u) && regex.test(u)) + } + case "TupleType": { + const elements = ast.elements.map((e) => goMemo(e.type, isDecoding)) + const rest = ast.rest.map((annotatedAST) => goMemo(annotatedAST.type, isDecoding)) + let requiredTypes: Array = ast.elements.filter((e) => !e.isOptional) + if (ast.rest.length > 0) { + requiredTypes = requiredTypes.concat(ast.rest.slice(1)) + } + const requiredLen = requiredTypes.length + const expectedIndexes = ast.elements.length > 0 ? ast.elements.map((_, i) => i).join(" | ") : "never" + const concurrency = getConcurrency(ast) + const batching = getBatching(ast) + return (input: unknown, options) => { + if (!Arr.isArray(input)) { + return Either.left(new Type(ast, input)) + } + const allErrors = options?.errors === "all" + const es: Array<[number, ParseIssue]> = [] + let stepKey = 0 + const output: Array<[number, any]> = [] + // --------------------------------------------- + // handle missing indexes + // --------------------------------------------- + const len = input.length + for (let i = len; i <= requiredLen - 1; i++) { + const e = new Pointer(i, input, new Missing(requiredTypes[i - len])) + if (allErrors) { + es.push([stepKey++, e]) + continue + } else { + return Either.left(new Composite(ast, input, e, output)) + } + } + + // --------------------------------------------- + // handle excess indexes + // --------------------------------------------- + if (ast.rest.length === 0) { + for (let i = ast.elements.length; i <= len - 1; i++) { + const e = new Pointer(i, input, new Unexpected(input[i], `is unexpected, expected: ${expectedIndexes}`)) + if (allErrors) { + es.push([stepKey++, e]) + continue + } else { + return Either.left(new Composite(ast, input, e, output)) + } + } + } + + let i = 0 + type State = { + es: typeof es + output: typeof output + } + let queue: + | Array<(_: State) => Effect.Effect> + | undefined = undefined + + // --------------------------------------------- + // handle elements + // --------------------------------------------- + for (; i < elements.length; i++) { + if (len < i + 1) { + if (ast.elements[i].isOptional) { + // the input element is missing + continue + } + } else { + const parser = elements[i] + const te = parser(input[i], options) + if (isEither(te)) { + if (Either.isLeft(te)) { + // the input element is present but is not valid + const e = new Pointer(i, input, te.left) + if (allErrors) { + es.push([stepKey++, e]) + continue + } else { + return Either.left(new Composite(ast, input, e, sortByIndex(output))) + } + } + output.push([stepKey++, te.right]) + } else { + const nk = stepKey++ + const index = i + if (!queue) { + queue = [] + } + queue.push(({ es, output }: State) => + Effect.flatMap(Effect.either(te), (t) => { + if (Either.isLeft(t)) { + // the input element is present but is not valid + const e = new Pointer(index, input, t.left) + if (allErrors) { + es.push([nk, e]) + return Effect.void + } else { + return Either.left(new Composite(ast, input, e, sortByIndex(output))) + } + } + output.push([nk, t.right]) + return Effect.void + }) + ) + } + } + } + // --------------------------------------------- + // handle rest element + // --------------------------------------------- + if (Arr.isNonEmptyReadonlyArray(rest)) { + const [head, ...tail] = rest + for (; i < len - tail.length; i++) { + const te = head(input[i], options) + if (isEither(te)) { + if (Either.isLeft(te)) { + const e = new Pointer(i, input, te.left) + if (allErrors) { + es.push([stepKey++, e]) + continue + } else { + return Either.left(new Composite(ast, input, e, sortByIndex(output))) + } + } else { + output.push([stepKey++, te.right]) + } + } else { + const nk = stepKey++ + const index = i + if (!queue) { + queue = [] + } + queue.push( + ({ es, output }: State) => + Effect.flatMap(Effect.either(te), (t) => { + if (Either.isLeft(t)) { + const e = new Pointer(index, input, t.left) + if (allErrors) { + es.push([nk, e]) + return Effect.void + } else { + return Either.left(new Composite(ast, input, e, sortByIndex(output))) + } + } else { + output.push([nk, t.right]) + return Effect.void + } + }) + ) + } + } + // --------------------------------------------- + // handle post rest elements + // --------------------------------------------- + for (let j = 0; j < tail.length; j++) { + const index = i + j + if (len < index + 1) { + continue + } else { + const te = tail[j](input[index], options) + if (isEither(te)) { + if (Either.isLeft(te)) { + // the input element is present but is not valid + const e = new Pointer(index, input, te.left) + if (allErrors) { + es.push([stepKey++, e]) + continue + } else { + return Either.left(new Composite(ast, input, e, sortByIndex(output))) + } + } + output.push([stepKey++, te.right]) + } else { + const nk = stepKey++ + if (!queue) { + queue = [] + } + queue.push( + ({ es, output }: State) => + Effect.flatMap(Effect.either(te), (t) => { + if (Either.isLeft(t)) { + // the input element is present but is not valid + const e = new Pointer(index, input, t.left) + if (allErrors) { + es.push([nk, e]) + return Effect.void + } else { + return Either.left(new Composite(ast, input, e, sortByIndex(output))) + } + } + output.push([nk, t.right]) + return Effect.void + }) + ) + } + } + } + } + + // --------------------------------------------- + // compute result + // --------------------------------------------- + const computeResult = ({ es, output }: State) => + Arr.isNonEmptyArray(es) ? + Either.left(new Composite(ast, input, sortByIndex(es), sortByIndex(output))) : + Either.right(sortByIndex(output)) + if (queue && queue.length > 0) { + const cqueue = queue + return Effect.suspend(() => { + const state: State = { + es: Arr.copy(es), + output: Arr.copy(output) + } + return Effect.flatMap( + Effect.forEach(cqueue, (f) => f(state), { concurrency, batching, discard: true }), + () => computeResult(state) + ) + }) + } + return computeResult({ output, es }) + } + } + case "TypeLiteral": { + if (ast.propertySignatures.length === 0 && ast.indexSignatures.length === 0) { + return fromRefinement(ast, Predicate.isNotNullable) + } + + const propertySignatures: Array = [] + const expectedKeysMap: Record = {} + const expectedKeys: Array = [] + for (const ps of ast.propertySignatures) { + propertySignatures.push([goMemo(ps.type, isDecoding), ps]) + expectedKeysMap[ps.name] = null + expectedKeys.push(ps.name) + } + + const indexSignatures = ast.indexSignatures.map((is) => + [ + goMemo(is.parameter, isDecoding), + goMemo(is.type, isDecoding), + is.parameter + ] as const + ) + const expectedAST = AST.Union.make( + ast.indexSignatures.map((is): AST.AST => is.parameter).concat( + expectedKeys.map((key) => Predicate.isSymbol(key) ? new AST.UniqueSymbol(key) : new AST.Literal(key)) + ) + ) + const expected = goMemo(expectedAST, isDecoding) + const concurrency = getConcurrency(ast) + const batching = getBatching(ast) + return (input: unknown, options) => { + if (!Predicate.isRecord(input)) { + return Either.left(new Type(ast, input)) + } + const allErrors = options?.errors === "all" + const es: Array<[number, ParseIssue]> = [] + let stepKey = 0 + + // --------------------------------------------- + // handle excess properties + // --------------------------------------------- + const onExcessPropertyError = options?.onExcessProperty === "error" + const onExcessPropertyPreserve = options?.onExcessProperty === "preserve" + const output: Record = {} + let inputKeys: Array | undefined + if (onExcessPropertyError || onExcessPropertyPreserve) { + inputKeys = Reflect.ownKeys(input) + for (const key of inputKeys) { + const te = expected(key, options) + if (isEither(te) && Either.isLeft(te)) { + // key is unexpected + if (onExcessPropertyError) { + const e = new Pointer( + key, + input, + new Unexpected(input[key], `is unexpected, expected: ${String(expectedAST)}`) + ) + if (allErrors) { + es.push([stepKey++, e]) + continue + } else { + return Either.left(new Composite(ast, input, e, output)) + } + } else { + // preserve key + output[key] = input[key] + } + } + } + } + + // --------------------------------------------- + // handle property signatures + // --------------------------------------------- + type State = { + es: typeof es + output: typeof output + } + let queue: + | Array<(state: State) => Effect.Effect> + | undefined = undefined + + const isExact = options?.exact === true + for (let i = 0; i < propertySignatures.length; i++) { + const ps = propertySignatures[i][1] + const name = ps.name + const hasKey = Object.prototype.hasOwnProperty.call(input, name) + if (!hasKey) { + if (ps.isOptional) { + continue + } else if (isExact) { + const e = new Pointer(name, input, new Missing(ps)) + if (allErrors) { + es.push([stepKey++, e]) + continue + } else { + return Either.left(new Composite(ast, input, e, output)) + } + } + } + const parser = propertySignatures[i][0] + const te = parser(input[name], options) + if (isEither(te)) { + if (Either.isLeft(te)) { + const e = new Pointer(name, input, hasKey ? te.left : new Missing(ps)) + if (allErrors) { + es.push([stepKey++, e]) + continue + } else { + return Either.left(new Composite(ast, input, e, output)) + } + } + output[name] = te.right + } else { + const nk = stepKey++ + const index = name + if (!queue) { + queue = [] + } + queue.push( + ({ es, output }: State) => + Effect.flatMap(Effect.either(te), (t) => { + if (Either.isLeft(t)) { + const e = new Pointer(index, input, hasKey ? t.left : new Missing(ps)) + if (allErrors) { + es.push([nk, e]) + return Effect.void + } else { + return Either.left(new Composite(ast, input, e, output)) + } + } + output[index] = t.right + return Effect.void + }) + ) + } + } + + // --------------------------------------------- + // handle index signatures + // --------------------------------------------- + for (let i = 0; i < indexSignatures.length; i++) { + const indexSignature = indexSignatures[i] + const parameter = indexSignature[0] + const type = indexSignature[1] + const keys = util_.getKeysForIndexSignature(input, indexSignature[2]) + for (const key of keys) { + // --------------------------------------------- + // handle keys + // --------------------------------------------- + const keu = parameter(key, options) + if (isEither(keu) && Either.isRight(keu)) { + // --------------------------------------------- + // handle values + // --------------------------------------------- + const vpr = type(input[key], options) + if (isEither(vpr)) { + if (Either.isLeft(vpr)) { + const e = new Pointer(key, input, vpr.left) + if (allErrors) { + es.push([stepKey++, e]) + continue + } else { + return Either.left(new Composite(ast, input, e, output)) + } + } else { + if (!Object.prototype.hasOwnProperty.call(expectedKeysMap, key)) { + output[key] = vpr.right + } + } + } else { + const nk = stepKey++ + const index = key + if (!queue) { + queue = [] + } + queue.push( + ({ es, output }: State) => + Effect.flatMap( + Effect.either(vpr), + (tv) => { + if (Either.isLeft(tv)) { + const e = new Pointer(index, input, tv.left) + if (allErrors) { + es.push([nk, e]) + return Effect.void + } else { + return Either.left(new Composite(ast, input, e, output)) + } + } else { + if (!Object.prototype.hasOwnProperty.call(expectedKeysMap, key)) { + output[key] = tv.right + } + return Effect.void + } + } + ) + ) + } + } + } + } + // --------------------------------------------- + // compute result + // --------------------------------------------- + const computeResult = ({ es, output }: State) => { + if (Arr.isNonEmptyArray(es)) { + return Either.left(new Composite(ast, input, sortByIndex(es), output)) + } + if (options?.propertyOrder === "original") { + // preserve input keys order + const keys = inputKeys || Reflect.ownKeys(input) + for (const name of expectedKeys) { + if (keys.indexOf(name) === -1) { + keys.push(name) + } + } + const out: any = {} + for (const key of keys) { + if (Object.prototype.hasOwnProperty.call(output, key)) { + out[key] = output[key] + } + } + return Either.right(out) + } + return Either.right(output) + } + if (queue && queue.length > 0) { + const cqueue = queue + return Effect.suspend(() => { + const state: State = { + es: Arr.copy(es), + output: Object.assign({}, output) + } + return Effect.flatMap( + Effect.forEach(cqueue, (f) => f(state), { concurrency, batching, discard: true }), + () => computeResult(state) + ) + }) + } + return computeResult({ es, output }) + } + } + case "Union": { + const searchTree = getSearchTree(ast.types, isDecoding) + const ownKeys = Reflect.ownKeys(searchTree.keys) + const ownKeysLen = ownKeys.length + const astTypesLen = ast.types.length + const map = new Map() + for (let i = 0; i < astTypesLen; i++) { + map.set(ast.types[i], goMemo(ast.types[i], isDecoding)) + } + const concurrency = getConcurrency(ast) ?? 1 + const batching = getBatching(ast) + return (input, options) => { + const es: Array<[number, ParseIssue]> = [] + let stepKey = 0 + let candidates: Array = [] + if (ownKeysLen > 0) { + if (Predicate.isRecordOrArray(input)) { + for (let i = 0; i < ownKeysLen; i++) { + const name = ownKeys[i] + const buckets = searchTree.keys[name].buckets + // for each property that should contain a literal, check if the input contains that property + if (Object.prototype.hasOwnProperty.call(input, name)) { + const literal = String(input[name]) + // check that the value obtained from the input for the property corresponds to an existing bucket + if (Object.prototype.hasOwnProperty.call(buckets, literal)) { + // retrive the minimal set of candidates for decoding + candidates = candidates.concat(buckets[literal]) + } else { + const { candidates, literals } = searchTree.keys[name] + const literalsUnion = AST.Union.make(literals) + const errorAst = candidates.length === astTypesLen + ? new AST.TypeLiteral([new AST.PropertySignature(name, literalsUnion, false, true)], []) + : AST.Union.make(candidates) + es.push([ + stepKey++, + new Composite(errorAst, input, new Pointer(name, input, new Type(literalsUnion, input[name]))) + ]) + } + } else { + const { candidates, literals } = searchTree.keys[name] + const fakePropertySignature = new AST.PropertySignature(name, AST.Union.make(literals), false, true) + const errorAst = candidates.length === astTypesLen + ? new AST.TypeLiteral([fakePropertySignature], []) + : AST.Union.make(candidates) + es.push([ + stepKey++, + new Composite(errorAst, input, new Pointer(name, input, new Missing(fakePropertySignature))) + ]) + } + } + } else { + const errorAst = searchTree.candidates.length === astTypesLen + ? ast + : AST.Union.make(searchTree.candidates) + es.push([stepKey++, new Type(errorAst, input)]) + } + } + if (searchTree.otherwise.length > 0) { + candidates = candidates.concat(searchTree.otherwise) + } + + let queue: + | Array<(state: State) => Effect.Effect> + | undefined = undefined + + type State = { + finalResult?: any + es: typeof es + } + + for (let i = 0; i < candidates.length; i++) { + const candidate = candidates[i] + const pr = map.get(candidate)!(input, options) + // the members of a union are ordered based on which one should be decoded first, + // therefore if one member has added a task, all subsequent members must + // also add a task to the queue even if they are synchronous + if (isEither(pr) && (!queue || queue.length === 0)) { + if (Either.isRight(pr)) { + return pr + } else { + es.push([stepKey++, pr.left]) + } + } else { + const nk = stepKey++ + if (!queue) { + queue = [] + } + queue.push( + (state) => + Effect.suspend(() => { + if ("finalResult" in state) { + return Effect.void + } else { + return Effect.flatMap(Effect.either(pr), (t) => { + if (Either.isRight(t)) { + state.finalResult = t + } else { + state.es.push([nk, t.left]) + } + return Effect.void + }) + } + }) + ) + } + } + + // --------------------------------------------- + // compute result + // --------------------------------------------- + const computeResult = (es: State["es"]) => + Arr.isNonEmptyArray(es) ? + es.length === 1 && es[0][1]._tag === "Type" ? + Either.left(es[0][1]) : + Either.left(new Composite(ast, input, sortByIndex(es))) : + // this should never happen + Either.left(new Type(ast, input)) + + if (queue && queue.length > 0) { + const cqueue = queue + return Effect.suspend(() => { + const state: State = { es: Arr.copy(es) } + return Effect.flatMap( + Effect.forEach(cqueue, (f) => f(state), { concurrency, batching, discard: true }), + () => { + if ("finalResult" in state) { + return state.finalResult + } + return computeResult(state.es) + } + ) + }) + } + return computeResult(es) + } + } + case "Suspend": { + const get = util_.memoizeThunk(() => goMemo(ast.f(), isDecoding)) + return (a, options) => get()(a, options) + } + } +} + +const fromRefinement = (ast: AST.AST, refinement: (u: unknown) => u is A): Parser => (u) => + refinement(u) ? Either.right(u) : Either.left(new Type(ast, u)) + +/** @internal */ +export const getLiterals = ( + ast: AST.AST, + isDecoding: boolean +): ReadonlyArray<[PropertyKey, AST.Literal]> => { + switch (ast._tag) { + case "Declaration": { + const annotation = AST.getSurrogateAnnotation(ast) + if (Option.isSome(annotation)) { + return getLiterals(annotation.value, isDecoding) + } + break + } + case "TypeLiteral": { + const out: Array<[PropertyKey, AST.Literal]> = [] + for (let i = 0; i < ast.propertySignatures.length; i++) { + const propertySignature = ast.propertySignatures[i] + const type = isDecoding ? AST.encodedAST(propertySignature.type) : AST.typeAST(propertySignature.type) + if (AST.isLiteral(type) && !propertySignature.isOptional) { + out.push([propertySignature.name, type]) + } + } + return out + } + case "TupleType": { + const out: Array<[PropertyKey, AST.Literal]> = [] + for (let i = 0; i < ast.elements.length; i++) { + const element = ast.elements[i] + const type = isDecoding ? AST.encodedAST(element.type) : AST.typeAST(element.type) + if (AST.isLiteral(type) && !element.isOptional) { + out.push([i, type]) + } + } + return out + } + case "Refinement": + return getLiterals(ast.from, isDecoding) + case "Suspend": + return getLiterals(ast.f(), isDecoding) + case "Transformation": + return getLiterals(isDecoding ? ast.from : ast.to, isDecoding) + } + return [] +} + +/** + * The purpose of the algorithm is to narrow down the pool of possible + * candidates for decoding as much as possible. + * + * This function separates the schemas into two groups, `keys` and `otherwise`: + * + * - `keys`: the schema has at least one property with a literal value + * - `otherwise`: the schema has no properties with a literal value + * + * If a schema has at least one property with a literal value, so it ends up in + * `keys`, first a namespace is created for the name of the property containing + * the literal, and then within this namespace a "bucket" is created for the + * literal value in which to store all the schemas that have the same property + * and literal value. + * + * @internal + */ +export const getSearchTree = ( + members: ReadonlyArray, + isDecoding: boolean +): { + keys: { + readonly [key: PropertyKey]: { + buckets: { [literal: string]: ReadonlyArray } + literals: ReadonlyArray // this is for error messages + candidates: ReadonlyArray + } + } + otherwise: ReadonlyArray + candidates: ReadonlyArray +} => { + const keys: { + [key: PropertyKey]: { + buckets: { [literal: string]: Array } + literals: Array + candidates: Array + } + } = {} + const otherwise: Array = [] + const candidates: Array = [] + for (let i = 0; i < members.length; i++) { + const member = members[i] + const tags = getLiterals(member, isDecoding) + if (tags.length > 0) { + candidates.push(member) + for (let j = 0; j < tags.length; j++) { + const [key, literal] = tags[j] + const hash = String(literal.literal) + keys[key] = keys[key] || { buckets: {}, literals: [], candidates: [] } + const buckets = keys[key].buckets + if (Object.prototype.hasOwnProperty.call(buckets, hash)) { + if (j < tags.length - 1) { + continue + } + buckets[hash].push(member) + keys[key].literals.push(literal) + keys[key].candidates.push(member) + } else { + buckets[hash] = [member] + keys[key].literals.push(literal) + keys[key].candidates.push(member) + break + } + } + } else { + otherwise.push(member) + } + } + return { keys, otherwise, candidates } +} + +const dropRightRefinement = (ast: AST.AST): AST.AST => AST.isRefinement(ast) ? dropRightRefinement(ast.from) : ast + +const handleForbidden = ( + effect: Effect.Effect, + ast: AST.AST, + actual: unknown, + options: InternalOptions | undefined +): Effect.Effect => { + // If effects are allowed, return the original effect + if (options?.isEffectAllowed === true) { + return effect + } + + // If the effect is already an Either, return it directly + if (isEither(effect)) { + return effect + } + + // Otherwise, attempt to execute the effect synchronously + const scheduler = new Scheduler.SyncScheduler() + const fiber = Effect.runFork(effect as Effect.Effect, { scheduler }) + scheduler.flush() + const exit = fiber.unsafePoll() + + if (exit) { + if (Exit.isSuccess(exit)) { + // If the effect successfully resolves, wrap the value in a Right + return Either.right(exit.value) + } + const cause = exit.cause + if (Cause.isFailType(cause)) { + // The effect executed synchronously but failed due to a ParseIssue + return Either.left(cause.error) + } + // The effect executed synchronously but failed due to a defect (e.g., a missing dependency) + return Either.left(new Forbidden(ast, actual, Cause.pretty(cause))) + } + + // The effect could not be resolved synchronously, meaning it performs async work + return Either.left( + new Forbidden( + ast, + actual, + "cannot be be resolved synchronously, this is caused by using runSync on an effect that performs async work" + ) + ) +} + +const compare = ([a]: [number, ...Array], [b]: [number, ...Array]) => a > b ? 1 : a < b ? -1 : 0 + +function sortByIndex( + es: Arr.NonEmptyArray<[number, T]> +): Arr.NonEmptyArray +function sortByIndex(es: Array<[number, T]>): Array +function sortByIndex(es: Array<[number, any]>) { + return es.sort(compare).map((t) => t[1]) +} + +// ------------------------------------------------------------------------------------- +// transformations interpreter +// ------------------------------------------------------------------------------------- + +/** @internal */ +export const getFinalTransformation = ( + transformation: AST.TransformationKind, + isDecoding: boolean +): ( + fromA: any, + options: AST.ParseOptions, + self: AST.Transformation, + fromI: any +) => Effect.Effect => { + switch (transformation._tag) { + case "FinalTransformation": + return isDecoding ? transformation.decode : transformation.encode + case "ComposeTransformation": + return Either.right + case "TypeLiteralTransformation": + return (input) => { + let out: Effect.Effect = Either.right(input) + + // --------------------------------------------- + // handle property signature transformations + // --------------------------------------------- + for (const pst of transformation.propertySignatureTransformations) { + const [from, to] = isDecoding ? + [pst.from, pst.to] : + [pst.to, pst.from] + const transformation = isDecoding ? pst.decode : pst.encode + const f = (input: any) => { + const o = transformation( + Object.prototype.hasOwnProperty.call(input, from) ? + Option.some(input[from]) : + Option.none() + ) + delete input[from] + if (Option.isSome(o)) { + input[to] = o.value + } + return input + } + out = map(out, f) + } + return out + } + } +} + +// ---------------- +// Formatters +// ---------------- + +interface Forest extends ReadonlyArray> {} + +interface Tree { + readonly value: A + readonly forest: Forest +} + +const makeTree = (value: A, forest: Forest = []): Tree => ({ + value, + forest +}) + +/** + * @category formatting + * @since 3.10.0 + */ +export interface ParseResultFormatter { + readonly formatIssue: (issue: ParseIssue) => Effect.Effect + readonly formatIssueSync: (issue: ParseIssue) => A + readonly formatError: (error: ParseError) => Effect.Effect + readonly formatErrorSync: (error: ParseError) => A +} + +/** + * @category formatting + * @since 3.10.0 + */ +export const TreeFormatter: ParseResultFormatter = { + formatIssue: (issue) => map(formatTree(issue), drawTree), + formatIssueSync: (issue) => { + const e = TreeFormatter.formatIssue(issue) + return isEither(e) ? Either.getOrThrow(e) : Effect.runSync(e) + }, + formatError: (error) => TreeFormatter.formatIssue(error.issue), + formatErrorSync: (error) => TreeFormatter.formatIssueSync(error.issue) +} + +const drawTree = (tree: Tree): string => tree.value + draw("\n", tree.forest) + +const draw = (indentation: string, forest: Forest): string => { + let r = "" + const len = forest.length + let tree: Tree + for (let i = 0; i < len; i++) { + tree = forest[i] + const isLast = i === len - 1 + r += indentation + (isLast ? "└" : "├") + "─ " + tree.value + r += draw(indentation + (len > 1 && !isLast ? "│ " : " "), tree.forest) + } + return r +} + +const formatTransformationKind = (kind: Transformation["kind"]): string => { + switch (kind) { + case "Encoded": + return "Encoded side transformation failure" + case "Transformation": + return "Transformation process failure" + case "Type": + return "Type side transformation failure" + } +} + +const formatRefinementKind = (kind: Refinement["kind"]): string => { + switch (kind) { + case "From": + return "From side refinement failure" + case "Predicate": + return "Predicate refinement failure" + } +} + +const getAnnotated = (issue: ParseIssue): Option.Option => + "ast" in issue ? Option.some(issue.ast) : Option.none() + +interface CurrentMessage { + readonly message: string + readonly override: boolean +} + +// TODO: replace with Either.void when 3.13 lands +const Either_void = Either.right(undefined) + +const getCurrentMessage = (issue: ParseIssue): Effect.Effect => + getAnnotated(issue).pipe( + Option.flatMap(AST.getMessageAnnotation), + Option.match({ + onNone: () => Either_void, + onSome: (messageAnnotation) => { + const union = messageAnnotation(issue) + if (Predicate.isString(union)) { + return Either.right({ message: union, override: false }) + } + if (Effect.isEffect(union)) { + return Effect.map(union, (message) => ({ message, override: false })) + } + if (Predicate.isString(union.message)) { + return Either.right({ message: union.message, override: union.override }) + } + return Effect.map(union.message, (message) => ({ message, override: union.override })) + } + }) + ) + +const createParseIssueGuard = + (tag: T) => (issue: ParseIssue): issue is Extract => + issue._tag === tag + +/** + * Returns `true` if the value is a `Composite`. + * + * @category guards + * @since 3.10.0 + */ +export const isComposite = createParseIssueGuard("Composite") + +const isRefinement = createParseIssueGuard("Refinement") +const isTransformation = createParseIssueGuard("Transformation") + +const getMessage = (issue: ParseIssue): Effect.Effect => + flatMap(getCurrentMessage(issue), (currentMessage) => { + if (currentMessage !== undefined) { + const useInnerMessage = !currentMessage.override && ( + isComposite(issue) || + (isRefinement(issue) && issue.kind === "From") || + (isTransformation(issue) && issue.kind !== "Transformation") + ) + return useInnerMessage + ? isTransformation(issue) || isRefinement(issue) ? getMessage(issue.issue) : Either_void + : Either.right(currentMessage.message) + } + return Either_void + }) + +const getParseIssueTitleAnnotation = (issue: ParseIssue): string | undefined => + getAnnotated(issue).pipe( + Option.flatMap(AST.getParseIssueTitleAnnotation), + Option.flatMapNullable((annotation) => annotation(issue)), + Option.getOrUndefined + ) + +/** @internal */ +export function getRefinementExpected(ast: AST.Refinement): string { + return AST.getDescriptionAnnotation(ast).pipe( + Option.orElse(() => AST.getTitleAnnotation(ast)), + Option.orElse(() => AST.getAutoTitleAnnotation(ast)), + Option.orElse(() => AST.getIdentifierAnnotation(ast)), + Option.getOrElse(() => `{ ${ast.from} | filter }`) + ) +} + +function getDefaultTypeMessage(issue: Type): string { + if (issue.message !== undefined) { + return issue.message + } + const expected = AST.isRefinement(issue.ast) ? getRefinementExpected(issue.ast) : String(issue.ast) + return `Expected ${expected}, actual ${Inspectable.formatUnknown(issue.actual)}` +} + +const formatTypeMessage = (issue: Type): Effect.Effect => + map( + getMessage(issue), + (message) => message ?? getParseIssueTitleAnnotation(issue) ?? getDefaultTypeMessage(issue) + ) + +const getParseIssueTitle = ( + issue: Forbidden | Transformation | Refinement | Composite +): string => getParseIssueTitleAnnotation(issue) ?? String(issue.ast) + +const formatForbiddenMessage = (issue: Forbidden): string => issue.message ?? "is forbidden" + +const formatUnexpectedMessage = (issue: Unexpected): string => issue.message ?? "is unexpected" + +const formatMissingMessage = (issue: Missing): Effect.Effect => { + const missingMessageAnnotation = AST.getMissingMessageAnnotation(issue.ast) + if (Option.isSome(missingMessageAnnotation)) { + const annotation = missingMessageAnnotation.value() + return Predicate.isString(annotation) ? Either.right(annotation) : annotation + } + return Either.right(issue.message ?? "is missing") +} + +const formatTree = (issue: ParseIssue): Effect.Effect> => { + switch (issue._tag) { + case "Type": + return map(formatTypeMessage(issue), makeTree) + case "Forbidden": + return Either.right(makeTree(getParseIssueTitle(issue), [makeTree(formatForbiddenMessage(issue))])) + case "Unexpected": + return Either.right(makeTree(formatUnexpectedMessage(issue))) + case "Missing": + return map(formatMissingMessage(issue), makeTree) + case "Transformation": + return flatMap(getMessage(issue), (message) => { + if (message !== undefined) { + return Either.right(makeTree(message)) + } + return map( + formatTree(issue.issue), + (tree) => makeTree(getParseIssueTitle(issue), [makeTree(formatTransformationKind(issue.kind), [tree])]) + ) + }) + case "Refinement": + return flatMap(getMessage(issue), (message) => { + if (message !== undefined) { + return Either.right(makeTree(message)) + } + return map( + formatTree(issue.issue), + (tree) => makeTree(getParseIssueTitle(issue), [makeTree(formatRefinementKind(issue.kind), [tree])]) + ) + }) + case "Pointer": + return map(formatTree(issue.issue), (tree) => makeTree(util_.formatPath(issue.path), [tree])) + case "Composite": + return flatMap(getMessage(issue), (message) => { + if (message !== undefined) { + return Either.right(makeTree(message)) + } + const parseIssueTitle = getParseIssueTitle(issue) + return util_.isNonEmpty(issue.issues) + ? map(Effect.forEach(issue.issues, formatTree), (forest) => makeTree(parseIssueTitle, forest)) + : map(formatTree(issue.issues), (tree) => makeTree(parseIssueTitle, [tree])) + }) + } +} + +/** + * Represents an issue returned by the {@link ArrayFormatter} formatter. + * + * @category model + * @since 3.10.0 + */ +export interface ArrayFormatterIssue { + /** + * The tag identifying the type of parse issue. + */ + readonly _tag: ParseIssue["_tag"] + + /** + * The path to the property where the issue occurred. + */ + readonly path: ReadonlyArray + + /** + * A descriptive message explaining the issue. + */ + readonly message: string +} + +const makeArrayFormatterIssue = ( + _tag: ArrayFormatterIssue["_tag"], + path: ArrayFormatterIssue["path"], + message: ArrayFormatterIssue["message"] +): ArrayFormatterIssue => ({ _tag, path, message }) + +/** + * @category formatting + * @since 3.10.0 + */ +export const ArrayFormatter: ParseResultFormatter> = { + formatIssue: (issue) => getArrayFormatterIssues(issue, undefined, []), + formatIssueSync: (issue) => { + const e = ArrayFormatter.formatIssue(issue) + return isEither(e) ? Either.getOrThrow(e) : Effect.runSync(e) + }, + formatError: (error) => ArrayFormatter.formatIssue(error.issue), + formatErrorSync: (error) => ArrayFormatter.formatIssueSync(error.issue) +} + +const getArrayFormatterIssues = ( + issue: ParseIssue, + parentTag: ArrayFormatterIssue["_tag"] | undefined, + path: ReadonlyArray +): Effect.Effect> => { + const _tag = issue._tag + switch (_tag) { + case "Type": + return map(formatTypeMessage(issue), (message) => [makeArrayFormatterIssue(parentTag ?? _tag, path, message)]) + case "Forbidden": + return Either.right([makeArrayFormatterIssue(_tag, path, formatForbiddenMessage(issue))]) + case "Unexpected": + return Either.right([makeArrayFormatterIssue(_tag, path, formatUnexpectedMessage(issue))]) + case "Missing": + return map(formatMissingMessage(issue), (message) => [makeArrayFormatterIssue(_tag, path, message)]) + case "Pointer": + return getArrayFormatterIssues(issue.issue, undefined, path.concat(issue.path)) + case "Composite": + return flatMap(getMessage(issue), (message) => { + if (message !== undefined) { + return Either.right([makeArrayFormatterIssue(_tag, path, message)]) + } + return util_.isNonEmpty(issue.issues) + ? map(Effect.forEach(issue.issues, (issue) => getArrayFormatterIssues(issue, undefined, path)), Arr.flatten) + : getArrayFormatterIssues(issue.issues, undefined, path) + }) + case "Refinement": + return flatMap(getMessage(issue), (message) => { + if (message !== undefined) { + return Either.right([makeArrayFormatterIssue(_tag, path, message)]) + } + return getArrayFormatterIssues(issue.issue, issue.kind === "Predicate" ? _tag : undefined, path) + }) + case "Transformation": + return flatMap(getMessage(issue), (message) => { + if (message !== undefined) { + return Either.right([makeArrayFormatterIssue(_tag, path, message)]) + } + return getArrayFormatterIssues(issue.issue, issue.kind === "Transformation" ? _tag : undefined, path) + }) + } +} diff --git a/repos/effect/packages/effect/src/PartitionedSemaphore.ts b/repos/effect/packages/effect/src/PartitionedSemaphore.ts new file mode 100644 index 0000000..3a64a4e --- /dev/null +++ b/repos/effect/packages/effect/src/PartitionedSemaphore.ts @@ -0,0 +1,200 @@ +/** + * @since 3.19.4 + * @experimental + */ +import * as Effect from "./Effect.js" +import * as Iterable from "./Iterable.js" +import * as MutableHashMap from "./MutableHashMap.js" +import * as Option from "./Option.js" + +/** + * @since 3.19.4 + * @category Models + * @experimental + */ +export const TypeId: TypeId = "~effect/PartitionedSemaphore" + +/** + * @since 3.19.4 + * @category Models + * @experimental + */ +export type TypeId = "~effect/PartitionedSemaphore" + +/** + * A `PartitionedSemaphore` is a concurrency primitive that can be used to + * control concurrent access to a resource across multiple partitions identified + * by keys. + * + * The total number of permits is shared across all partitions, with waiting + * permits equally distributed among partitions using a round-robin strategy. + * + * This is useful when you want to limit the total number of concurrent accesses + * to a resource, while still allowing for fair distribution of access across + * different partitions. + * + * @since 3.19.4 + * @category Models + * @experimental + */ +export interface PartitionedSemaphore { + readonly [TypeId]: TypeId + + readonly withPermits: ( + key: K, + permits: number + ) => (effect: Effect.Effect) => Effect.Effect +} + +/** + * A `PartitionedSemaphore` is a concurrency primitive that can be used to + * control concurrent access to a resource across multiple partitions identified + * by keys. + * + * The total number of permits is shared across all partitions, with waiting + * permits equally distributed among partitions using a round-robin strategy. + * + * This is useful when you want to limit the total number of concurrent accesses + * to a resource, while still allowing for fair distribution of access across + * different partitions. + * + * @since 3.19.4 + * @category Constructors + * @experimental + */ +export const makeUnsafe = (options: { + readonly permits: number +}): PartitionedSemaphore => { + const maxPermits = Math.max(0, options.permits) + + if (!Number.isFinite(maxPermits)) { + return { + [TypeId]: TypeId, + withPermits: () => (effect) => effect + } + } + + let totalPermits = maxPermits + let waitingPermits = 0 + + type Waiter = { + permits: number + readonly resume: () => void + } + const partitions = MutableHashMap.empty>() + + const take = (key: K, permits: number) => + Effect.async((resume) => { + if (maxPermits < permits) { + return resume(Effect.never) + } else if (totalPermits >= permits) { + totalPermits -= permits + return resume(Effect.void) + } + + const needed = permits - totalPermits + const taken = permits - needed + if (totalPermits > 0) { + totalPermits = 0 + } + waitingPermits += needed + + const waiters = Option.getOrElse( + MutableHashMap.get(partitions, key), + () => { + const set = new Set() + MutableHashMap.set(partitions, key, set) + return set + } + ) + + const entry: Waiter = { + permits: needed, + resume() { + cleanup() + resume(Effect.void) + } + } + function cleanup() { + waiters.delete(entry) + if (waiters.size === 0) { + MutableHashMap.remove(partitions, key) + } + } + waiters.add(entry) + return Effect.sync(() => { + cleanup() + waitingPermits -= entry.permits + if (taken > 0) { + releaseUnsafe(taken) + } + }) + }) + + let iterator = partitions[Symbol.iterator]() + const releaseUnsafe = (permits: number) => { + while (permits > 0) { + if (waitingPermits === 0) { + totalPermits += permits + return + } + + let state = iterator.next() + if (state.done) { + iterator = partitions[Symbol.iterator]() + state = iterator.next() + if (state.done) return + } + + const entry = Iterable.unsafeHead(state.value[1]) + entry.permits-- + waitingPermits-- + if (entry.permits === 0) entry.resume() + permits-- + } + } + + return { + [TypeId]: TypeId, + withPermits: (key, permits) => { + const takePermits = take(key, permits) + const release: (effect: Effect.Effect) => Effect.Effect = Effect.matchCauseEffect({ + onFailure(cause) { + releaseUnsafe(permits) + return Effect.failCause(cause) + }, + onSuccess(value) { + releaseUnsafe(permits) + return Effect.succeed(value) + } + }) + return (effect) => + Effect.uninterruptibleMask((restore) => + Effect.flatMap( + restore(takePermits), + () => release(restore(effect)) + ) + ) + } + } +} + +/** + * A `PartitionedSemaphore` is a concurrency primitive that can be used to + * control concurrent access to a resource across multiple partitions identified + * by keys. + * + * The total number of permits is shared across all partitions, with waiting + * permits equally distributed among partitions using a round-robin strategy. + * + * This is useful when you want to limit the total number of concurrent accesses + * to a resource, while still allowing for fair distribution of access across + * different partitions. + * + * @since 3.19.4 + * @category Constructors + * @experimental + */ +export const make = (options: { + readonly permits: number +}): Effect.Effect> => Effect.sync(() => makeUnsafe(options)) diff --git a/repos/effect/packages/effect/src/Pipeable.ts b/repos/effect/packages/effect/src/Pipeable.ts new file mode 100644 index 0000000..2b54e6c --- /dev/null +++ b/repos/effect/packages/effect/src/Pipeable.ts @@ -0,0 +1,566 @@ +/** + * @since 2.0.0 + */ + +import type { Ctor } from "./Types.js" + +/** + * @since 2.0.0 + * @category Models + */ +export interface Pipeable { + pipe(this: A): A + pipe(this: A, ab: (_: A) => B): B + pipe(this: A, ab: (_: A) => B, bc: (_: B) => C): C + pipe(this: A, ab: (_: A) => B, bc: (_: B) => C, cd: (_: C) => D): D + pipe( + this: A, + ab: (_: A) => B, + bc: (_: B) => C, + cd: (_: C) => D, + de: (_: D) => E + ): E + pipe( + this: A, + ab: (_: A) => B, + bc: (_: B) => C, + cd: (_: C) => D, + de: (_: D) => E, + ef: (_: E) => F + ): F + pipe( + this: A, + ab: (_: A) => B, + bc: (_: B) => C, + cd: (_: C) => D, + de: (_: D) => E, + ef: (_: E) => F, + fg: (_: F) => G + ): G + pipe( + this: A, + ab: (_: A) => B, + bc: (_: B) => C, + cd: (_: C) => D, + de: (_: D) => E, + ef: (_: E) => F, + fg: (_: F) => G, + gh: (_: G) => H + ): H + pipe( + this: A, + ab: (_: A) => B, + bc: (_: B) => C, + cd: (_: C) => D, + de: (_: D) => E, + ef: (_: E) => F, + fg: (_: F) => G, + gh: (_: G) => H, + hi: (_: H) => I + ): I + pipe( + this: A, + ab: (_: A) => B, + bc: (_: B) => C, + cd: (_: C) => D, + de: (_: D) => E, + ef: (_: E) => F, + fg: (_: F) => G, + gh: (_: G) => H, + hi: (_: H) => I, + ij: (_: I) => J + ): J + pipe( + this: A, + ab: (_: A) => B, + bc: (_: B) => C, + cd: (_: C) => D, + de: (_: D) => E, + ef: (_: E) => F, + fg: (_: F) => G, + gh: (_: G) => H, + hi: (_: H) => I, + ij: (_: I) => J, + jk: (_: J) => K + ): K + pipe< + A, + B = never, + C = never, + D = never, + E = never, + F = never, + G = never, + H = never, + I = never, + J = never, + K = never, + L = never + >( + this: A, + ab: (_: A) => B, + bc: (_: B) => C, + cd: (_: C) => D, + de: (_: D) => E, + ef: (_: E) => F, + fg: (_: F) => G, + gh: (_: G) => H, + hi: (_: H) => I, + ij: (_: I) => J, + jk: (_: J) => K, + kl: (_: K) => L + ): L + pipe< + A, + B = never, + C = never, + D = never, + E = never, + F = never, + G = never, + H = never, + I = never, + J = never, + K = never, + L = never, + M = never + >( + this: A, + ab: (_: A) => B, + bc: (_: B) => C, + cd: (_: C) => D, + de: (_: D) => E, + ef: (_: E) => F, + fg: (_: F) => G, + gh: (_: G) => H, + hi: (_: H) => I, + ij: (_: I) => J, + jk: (_: J) => K, + kl: (_: K) => L, + lm: (_: L) => M + ): M + pipe< + A, + B = never, + C = never, + D = never, + E = never, + F = never, + G = never, + H = never, + I = never, + J = never, + K = never, + L = never, + M = never, + N = never + >( + this: A, + ab: (_: A) => B, + bc: (_: B) => C, + cd: (_: C) => D, + de: (_: D) => E, + ef: (_: E) => F, + fg: (_: F) => G, + gh: (_: G) => H, + hi: (_: H) => I, + ij: (_: I) => J, + jk: (_: J) => K, + kl: (_: K) => L, + lm: (_: L) => M, + mn: (_: M) => N + ): N + pipe< + A, + B = never, + C = never, + D = never, + E = never, + F = never, + G = never, + H = never, + I = never, + J = never, + K = never, + L = never, + M = never, + N = never, + O = never + >( + this: A, + ab: (_: A) => B, + bc: (_: B) => C, + cd: (_: C) => D, + de: (_: D) => E, + ef: (_: E) => F, + fg: (_: F) => G, + gh: (_: G) => H, + hi: (_: H) => I, + ij: (_: I) => J, + jk: (_: J) => K, + kl: (_: K) => L, + lm: (_: L) => M, + mn: (_: M) => N, + no: (_: N) => O + ): O + pipe< + A, + B = never, + C = never, + D = never, + E = never, + F = never, + G = never, + H = never, + I = never, + J = never, + K = never, + L = never, + M = never, + N = never, + O = never, + P = never + >( + this: A, + ab: (_: A) => B, + bc: (_: B) => C, + cd: (_: C) => D, + de: (_: D) => E, + ef: (_: E) => F, + fg: (_: F) => G, + gh: (_: G) => H, + hi: (_: H) => I, + ij: (_: I) => J, + jk: (_: J) => K, + kl: (_: K) => L, + lm: (_: L) => M, + mn: (_: M) => N, + no: (_: N) => O, + op: (_: O) => P + ): P + pipe< + A, + B = never, + C = never, + D = never, + E = never, + F = never, + G = never, + H = never, + I = never, + J = never, + K = never, + L = never, + M = never, + N = never, + O = never, + P = never, + Q = never + >( + this: A, + ab: (_: A) => B, + bc: (_: B) => C, + cd: (_: C) => D, + de: (_: D) => E, + ef: (_: E) => F, + fg: (_: F) => G, + gh: (_: G) => H, + hi: (_: H) => I, + ij: (_: I) => J, + jk: (_: J) => K, + kl: (_: K) => L, + lm: (_: L) => M, + mn: (_: M) => N, + no: (_: N) => O, + op: (_: O) => P, + pq: (_: P) => Q + ): Q + pipe< + A, + B = never, + C = never, + D = never, + E = never, + F = never, + G = never, + H = never, + I = never, + J = never, + K = never, + L = never, + M = never, + N = never, + O = never, + P = never, + Q = never, + R = never + >( + this: A, + ab: (_: A) => B, + bc: (_: B) => C, + cd: (_: C) => D, + de: (_: D) => E, + ef: (_: E) => F, + fg: (_: F) => G, + gh: (_: G) => H, + hi: (_: H) => I, + ij: (_: I) => J, + jk: (_: J) => K, + kl: (_: K) => L, + lm: (_: L) => M, + mn: (_: M) => N, + no: (_: N) => O, + op: (_: O) => P, + pq: (_: P) => Q, + qr: (_: Q) => R + ): R + pipe< + A, + B = never, + C = never, + D = never, + E = never, + F = never, + G = never, + H = never, + I = never, + J = never, + K = never, + L = never, + M = never, + N = never, + O = never, + P = never, + Q = never, + R = never, + S = never + >( + this: A, + ab: (_: A) => B, + bc: (_: B) => C, + cd: (_: C) => D, + de: (_: D) => E, + ef: (_: E) => F, + fg: (_: F) => G, + gh: (_: G) => H, + hi: (_: H) => I, + ij: (_: I) => J, + jk: (_: J) => K, + kl: (_: K) => L, + lm: (_: L) => M, + mn: (_: M) => N, + no: (_: N) => O, + op: (_: O) => P, + pq: (_: P) => Q, + qr: (_: Q) => R, + rs: (_: R) => S + ): S + pipe< + A, + B = never, + C = never, + D = never, + E = never, + F = never, + G = never, + H = never, + I = never, + J = never, + K = never, + L = never, + M = never, + N = never, + O = never, + P = never, + Q = never, + R = never, + S = never, + T = never + >( + this: A, + ab: (_: A) => B, + bc: (_: B) => C, + cd: (_: C) => D, + de: (_: D) => E, + ef: (_: E) => F, + fg: (_: F) => G, + gh: (_: G) => H, + hi: (_: H) => I, + ij: (_: I) => J, + jk: (_: J) => K, + kl: (_: K) => L, + lm: (_: L) => M, + mn: (_: M) => N, + no: (_: N) => O, + op: (_: O) => P, + pq: (_: P) => Q, + qr: (_: Q) => R, + rs: (_: R) => S, + st: (_: S) => T + ): T + pipe< + A, + B = never, + C = never, + D = never, + E = never, + F = never, + G = never, + H = never, + I = never, + J = never, + K = never, + L = never, + M = never, + N = never, + O = never, + P = never, + Q = never, + R = never, + S = never, + T = never, + U = never + >( + this: A, + ab: (_: A) => B, + bc: (_: B) => C, + cd: (_: C) => D, + de: (_: D) => E, + ef: (_: E) => F, + fg: (_: F) => G, + gh: (_: G) => H, + hi: (_: H) => I, + ij: (_: I) => J, + jk: (_: J) => K, + kl: (_: K) => L, + lm: (_: L) => M, + mn: (_: M) => N, + no: (_: N) => O, + op: (_: O) => P, + pq: (_: P) => Q, + qr: (_: Q) => R, + rs: (_: R) => S, + st: (_: S) => T, + tu: (_: T) => U + ): U + pipe< + A, + B = never, + C = never, + D = never, + E = never, + F = never, + G = never, + H = never, + I = never, + J = never, + K = never, + L = never, + M = never, + N = never, + O = never, + P = never, + Q = never, + R = never, + S = never, + T = never, + U = never + >( + this: A, + ab: (_: A) => B, + bc: (_: B) => C, + cd: (_: C) => D, + de: (_: D) => E, + ef: (_: E) => F, + fg: (_: F) => G, + gh: (_: G) => H, + hi: (_: H) => I, + ij: (_: I) => J, + jk: (_: J) => K, + kl: (_: K) => L, + lm: (_: L) => M, + mn: (_: M) => N, + no: (_: N) => O, + op: (_: O) => P, + pq: (_: P) => Q, + qr: (_: Q) => R, + rs: (_: R) => S, + st: (_: S) => T, + tu: (_: T) => U + ): U +} + +/** + * @since 2.0.0 + */ +export const pipeArguments = (self: A, args: IArguments): unknown => { + switch (args.length) { + case 0: + return self + case 1: + return args[0](self) + case 2: + return args[1](args[0](self)) + case 3: + return args[2](args[1](args[0](self))) + case 4: + return args[3](args[2](args[1](args[0](self)))) + case 5: + return args[4](args[3](args[2](args[1](args[0](self))))) + case 6: + return args[5](args[4](args[3](args[2](args[1](args[0](self)))))) + case 7: + return args[6](args[5](args[4](args[3](args[2](args[1](args[0](self))))))) + case 8: + return args[7](args[6](args[5](args[4](args[3](args[2](args[1](args[0](self)))))))) + case 9: + return args[8](args[7](args[6](args[5](args[4](args[3](args[2](args[1](args[0](self))))))))) + default: { + let ret = self + for (let i = 0, len = args.length; i < len; i++) { + ret = args[i](ret) + } + return ret + } + } +} + +/** + * @since 3.15.0 + * @category Models + */ +export interface PipeableConstructor { + new(...args: Array): Pipeable +} + +/** + * @since 3.15.0 + * @category Prototypes + */ +export const Prototype: Pipeable = { + pipe() { + return pipeArguments(this, arguments) + } +} + +const Base: PipeableConstructor = (function() { + function PipeableBase() {} + PipeableBase.prototype = Prototype + return PipeableBase as any +})() + +/** + * @since 3.15.0 + * @category Constructors + */ +export const Class: { + (): PipeableConstructor + (klass: TBase): TBase & PipeableConstructor +} = (klass?: Ctor) => + klass ? + class extends klass { + pipe() { + return pipeArguments(this, arguments) + } + } + : Base diff --git a/repos/effect/packages/effect/src/Pool.ts b/repos/effect/packages/effect/src/Pool.ts new file mode 100644 index 0000000..1e0ccf3 --- /dev/null +++ b/repos/effect/packages/effect/src/Pool.ts @@ -0,0 +1,204 @@ +/** + * @since 2.0.0 + */ +import type * as Duration from "./Duration.js" +import type * as Effect from "./Effect.js" +import * as internal from "./internal/pool.js" +import type { Pipeable } from "./Pipeable.js" +import type * as Scope from "./Scope.js" +import type * as Types from "./Types.js" +import type * as Unify from "./Unify.js" + +/** + * @since 2.0.0 + * @category symbols + */ +export const PoolTypeId: unique symbol = internal.PoolTypeId + +/** + * @since 2.0.0 + * @category symbols + */ +export type PoolTypeId = typeof PoolTypeId + +/** + * A `Pool` is a pool of items of type `A`, each of which may be + * associated with the acquisition and release of resources. An attempt to get + * an item `A` from a pool may fail with an error of type `E`. + * + * @since 2.0.0 + * @category models + */ +export interface Pool extends Pool.Variance, Effect.Effect, Pipeable { + /** + * Retrieves an item from the pool in a scoped effect. Note that if + * acquisition fails, then the returned effect will fail for that same reason. + * Retrying a failed acquisition attempt will repeat the acquisition attempt. + */ + readonly get: Effect.Effect + + /** + * Invalidates the specified item. This will cause the pool to eventually + * reallocate the item, although this reallocation may occur lazily rather + * than eagerly. + */ + invalidate(item: A): Effect.Effect + + readonly [Unify.typeSymbol]?: unknown + readonly [Unify.unifySymbol]?: PoolUnify + readonly [Unify.ignoreSymbol]?: PoolUnifyIgnore +} + +/** + * @category models + * @since 3.9.0 + */ +export interface PoolUnify extends Effect.EffectUnify { + Pool?: () => Extract> extends Pool | infer _ ? + A0 extends any ? Extract> extends Pool ? Pool + : never + : never : + never +} + +/** + * @category models + * @since 3.9.0 + */ +export interface PoolUnifyIgnore extends Effect.EffectUnifyIgnore { + Effect?: true +} + +/** + * @since 2.0.0 + */ +export declare namespace Pool { + /** + * @since 2.0.0 + * @category models + */ + export interface Variance { + readonly [PoolTypeId]: { + readonly _A: Types.Invariant + readonly _E: Types.Covariant + } + } +} + +/** + * Returns `true` if the specified value is a `Pool`, `false` otherwise. + * + * @since 2.0.0 + * @category refinements + */ +export const isPool: (u: unknown) => u is Pool = internal.isPool + +/** + * Makes a new pool of the specified fixed size. The pool is returned in a + * `Scope`, which governs the lifetime of the pool. When the pool is shutdown + * because the `Scope` is closed, the individual items allocated by the pool + * will be released in some unspecified order. + * + * By setting the `concurrency` parameter, you can control the level of concurrent + * access per pool item. By default, the number of permits is set to `1`. + * + * `targetUtilization` determines when to create new pool items. It is a value + * between 0 and 1, where 1 means only create new pool items when all the existing + * items are fully utilized. + * + * A `targetUtilization` of 0.5 will create new pool items when the existing items are + * 50% utilized. + * + * @since 2.0.0 + * @category constructors + */ +export const make: ( + options: { + readonly acquire: Effect.Effect + readonly size: number + readonly concurrency?: number | undefined + readonly targetUtilization?: number | undefined + } +) => Effect.Effect, never, Scope.Scope | R> = internal.make + +/** + * Makes a new pool with the specified minimum and maximum sizes and time to + * live before a pool whose excess items are not being used will be shrunk + * down to the minimum size. The pool is returned in a `Scope`, which governs + * the lifetime of the pool. When the pool is shutdown because the `Scope` is + * used, the individual items allocated by the pool will be released in some + * unspecified order. + * + * By setting the `concurrency` parameter, you can control the level of concurrent + * access per pool item. By default, the number of permits is set to `1`. + * + * `targetUtilization` determines when to create new pool items. It is a value + * between 0 and 1, where 1 means only create new pool items when all the existing + * items are fully utilized. + * + * A `targetUtilization` of 0.5 will create new pool items when the existing items are + * 50% utilized. + * + * The `timeToLiveStrategy` determines how items are invalidated. If set to + * "creation", then items are invalidated based on their creation time. If set + * to "usage", then items are invalidated based on pool usage. + * + * By default, the `timeToLiveStrategy` is set to "usage". + * + * ```ts skip-type-checking + * import { createConnection } from "mysql2"; + * import { Duration, Effect, Pool } from "effect" + * + * const acquireDBConnection = Effect.acquireRelease( + * Effect.sync(() => createConnection('mysql://...')), + * (connection) => Effect.sync(() => connection.end(() => {})), + * ) + * + * const connectionPool = Effect.flatMap( + * Pool.makeWithTTL({ + * acquire: acquireDBConnection, + * min: 10, + * max: 20, + * timeToLive: Duration.seconds(60) + * }), + * (pool) => pool.get + * ) + * ``` + * + * @since 2.0.0 + * @category constructors + */ +export const makeWithTTL: ( + options: { + readonly acquire: Effect.Effect + readonly min: number + readonly max: number + readonly concurrency?: number | undefined + readonly targetUtilization?: number | undefined + readonly timeToLive: Duration.DurationInput + readonly timeToLiveStrategy?: "creation" | "usage" | undefined + } +) => Effect.Effect, never, Scope.Scope | R> = internal.makeWithTTL + +/** + * Retrieves an item from the pool in a scoped effect. Note that if + * acquisition fails, then the returned effect will fail for that same reason. + * Retrying a failed acquisition attempt will repeat the acquisition attempt. + * + * @since 2.0.0 + * @category getters + */ +export const get: (self: Pool) => Effect.Effect = internal.get + +/** + * Invalidates the specified item. This will cause the pool to eventually + * reallocate the item, although this reallocation may occur lazily rather + * than eagerly. + * + * @since 2.0.0 + * @category combinators + */ +export const invalidate: { + (value: A): (self: Pool) => Effect.Effect + (self: Pool, value: A): Effect.Effect +} = internal.invalidate diff --git a/repos/effect/packages/effect/src/Predicate.ts b/repos/effect/packages/effect/src/Predicate.ts new file mode 100644 index 0000000..8dc69e0 --- /dev/null +++ b/repos/effect/packages/effect/src/Predicate.ts @@ -0,0 +1,1405 @@ +/** + * This module provides a collection of functions for working with predicates and refinements. + * + * A `Predicate` is a function that takes a value of type `A` and returns a boolean. + * It is used to check if a value satisfies a certain condition. + * + * A `Refinement` is a special type of predicate that not only checks a condition + * but also provides a type guard, allowing TypeScript to narrow the type of the input + * value from `A` to a more specific type `B` within a conditional block. + * + * The module includes: + * - Basic predicates and refinements for common types (e.g., `isString`, `isNumber`). + * - Combinators to create new predicates from existing ones (e.g., `and`, `or`, `not`). + * - Advanced combinators for working with data structures (e.g., `tuple`, `struct`). + * - Type-level utilities for inspecting predicate and refinement types. + * + * @since 2.0.0 + */ +import { dual, isFunction as isFunction_ } from "./Function.js" +import type { TypeLambda } from "./HKT.js" +import type { TupleOf, TupleOfAtLeast } from "./Types.js" + +/** + * Represents a function that takes a value of type `A` and returns `true` if the value + * satisfies some condition, `false` otherwise. + * + * @example + * ```ts + * import { Predicate } from "effect" + * import * as assert from "node:assert" + * + * const isEven: Predicate.Predicate = (n) => n % 2 === 0 + * + * assert.strictEqual(isEven(2), true) + * assert.strictEqual(isEven(3), false) + * ``` + * + * @category models + * @since 2.0.0 + */ +export interface Predicate { + (a: A): boolean +} + +/** + * A `TypeLambda` for `Predicate`. This is used to support higher-kinded types + * and allows `Predicate` to be used in generic contexts within the `effect` ecosystem. + * + * @category type lambdas + * @since 2.0.0 + */ +export interface PredicateTypeLambda extends TypeLambda { + readonly type: Predicate +} + +/** + * Represents a function that serves as a type guard. + * + * A `Refinement` is a function that takes a value of type `A` and returns a + * type predicate `a is B`, where `B` is a subtype of `A`. If the function returns + * `true`, TypeScript will narrow the type of the input variable to `B`. + * + * @example + * ```ts + * import { Predicate } from "effect" + * import * as assert from "node:assert" + * + * const isString: Predicate.Refinement = (u): u is string => typeof u === "string" + * + * const value: unknown = "hello" + * + * if (isString(value)) { + * // value is now known to be a string + * assert.strictEqual(value.toUpperCase(), "HELLO") + * } + * ``` + * + * @category models + * @since 2.0.0 + */ +export interface Refinement { + (a: A): a is B +} + +/** + * A namespace for type-level utilities for `Predicate`. + * + * @since 3.6.0 + * @category type-level + */ +export declare namespace Predicate { + /** + * Extracts the input type `A` from a `Predicate`. + * + * @example + * ```ts + * import { type Predicate } from "effect" + * + * type T = Predicate.Predicate.In> // T is string + * ``` + * + * @since 3.6.0 + * @category type-level + */ + export type In = [T] extends [Predicate] ? _A : never + /** + * A type representing any `Predicate`. + * + * @since 3.6.0 + * @category type-level + */ + export type Any = Predicate +} + +/** + * A namespace for type-level utilities for `Refinement`. + * + * @since 3.6.0 + * @category type-level + */ +export declare namespace Refinement { + /** + * Extracts the input type `A` from a `Refinement`. + * + * @example + * ```ts + * import { type Predicate } from "effect" + * + * type IsString = Predicate.Refinement + * type T = Predicate.Refinement.In // T is unknown + * ``` + * + * @since 3.6.0 + * @category type-level + */ + export type In = [T] extends [Refinement] ? _A : never + /** + * Extracts the output (refined) type `B` from a `Refinement`. + * + * @example + * ```ts + * import { type Predicate } from "effect" + * + * type IsString = Predicate.Refinement + * type T = Predicate.Refinement.Out // T is string + * ``` + * + * @since 3.6.0 + * @category type-level + */ + export type Out = [T] extends [Refinement] ? _B : never + /** + * A type representing any `Refinement`. + * + * @since 3.6.0 + * @category type-level + */ + export type Any = Refinement +} + +/** + * Transforms a `Predicate` into a `Predicate` by applying a function `(b: B) => A` + * to the input before passing it to the predicate. This is also known as "contramap" or + * "pre-composition". + * + * @example + * ```ts + * import { Predicate, Number } from "effect" + * import * as assert from "node:assert" + * + * // A predicate on numbers + * const isPositive: Predicate.Predicate = Number.greaterThan(0) + * + * // A function from `string` to `number` + * const stringLength = (s: string): number => s.length + * + * // Create a new predicate on strings by mapping the input + * const hasPositiveLength = Predicate.mapInput(isPositive, stringLength) + * + * assert.strictEqual(hasPositiveLength("hello"), true) + * assert.strictEqual(hasPositiveLength(""), false) + * ``` + * + * @category combinators + * @since 2.0.0 + */ +export const mapInput: { + (f: (b: B) => A): (self: Predicate) => Predicate + (self: Predicate, f: (b: B) => A): Predicate +} = dual(2, (self: Predicate, f: (b: B) => A): Predicate => (b) => self(f(b))) + +/** + * A refinement that checks if a `ReadonlyArray` is a tuple with exactly `N` elements. + * If the check is successful, the type is narrowed to `TupleOf`. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { isTupleOf } from "effect/Predicate" + * + * const isTupleOf3 = isTupleOf(3) + * + * assert.strictEqual(isTupleOf3([1, 2, 3]), true); + * assert.strictEqual(isTupleOf3([1, 2]), false); + * + * const arr: number[] = [1, 2, 3]; + * if (isTupleOf(arr, 3)) { + * // The type of arr is now [number, number, number] + * const [a, b, c] = arr; + * assert.deepStrictEqual([a, b, c], [1, 2, 3]) + * } + * ``` + * + * @category guards + * @since 3.3.0 + */ +export const isTupleOf: { + (n: N): (self: ReadonlyArray) => self is TupleOf + (self: ReadonlyArray, n: N): self is TupleOf +} = dual(2, (self: ReadonlyArray, n: N): self is TupleOf => self.length === n) + +/** + * A refinement that checks if a `ReadonlyArray` is a tuple with at least `N` elements. + * If the check is successful, the type is narrowed to `TupleOfAtLeast`. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { isTupleOfAtLeast } from "effect/Predicate" + * + * const isTupleOfAtLeast3 = isTupleOfAtLeast(3) + * + * assert.strictEqual(isTupleOfAtLeast3([1, 2, 3]), true); + * assert.strictEqual(isTupleOfAtLeast3([1, 2, 3, 4]), true); + * assert.strictEqual(isTupleOfAtLeast3([1, 2]), false); + * + * const arr: number[] = [1, 2, 3, 4]; + * if (isTupleOfAtLeast(arr, 3)) { + * // The type of arr is now [number, number, number, ...number[]] + * const [a, b, c] = arr; + * assert.deepStrictEqual([a, b, c], [1, 2, 3]) + * } + * ``` + * + * @category guards + * @since 3.3.0 + */ +export const isTupleOfAtLeast: { + (n: N): (self: ReadonlyArray) => self is TupleOfAtLeast + (self: ReadonlyArray, n: N): self is TupleOfAtLeast +} = dual(2, (self: ReadonlyArray, n: N): self is TupleOfAtLeast => self.length >= n) + +/** + * A predicate that checks if a value is "truthy" in JavaScript. + * Fails for `false`, `0`, `-0`, `0n`, `""`, `null`, `undefined`, and `NaN`. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { isTruthy } from "effect/Predicate" + * + * assert.strictEqual(isTruthy(1), true) + * assert.strictEqual(isTruthy("hello"), true) + * assert.strictEqual(isTruthy({}), true) + * + * assert.strictEqual(isTruthy(0), false) + * assert.strictEqual(isTruthy(""), false) + * assert.strictEqual(isTruthy(null), false) + * assert.strictEqual(isTruthy(undefined), false) + * ``` + * + * @category guards + * @since 2.0.0 + */ +export const isTruthy = (input: unknown) => !!input + +/** + * A refinement that checks if a value is a `Set`. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { isSet } from "effect/Predicate" + * + * assert.strictEqual(isSet(new Set([1, 2])), true) + * assert.strictEqual(isSet(new Set()), true) + * + * assert.strictEqual(isSet({}), false) + * assert.strictEqual(isSet([1, 2]), false) + * ``` + * + * @category guards + * @since 2.0.0 + */ +export const isSet = (input: unknown): input is Set => input instanceof Set + +/** + * A refinement that checks if a value is a `Map`. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { isMap } from "effect/Predicate" + * + * assert.strictEqual(isMap(new Map()), true) + * + * assert.strictEqual(isMap({}), false) + * assert.strictEqual(isMap(new Set()), false) + * ``` + * + * @category guards + * @since 2.0.0 + */ +export const isMap = (input: unknown): input is Map => input instanceof Map + +/** + * A refinement that checks if a value is a `string`. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { isString } from "effect/Predicate" + * + * assert.strictEqual(isString("hello"), true) + * assert.strictEqual(isString(""), true) + * + * assert.strictEqual(isString(123), false) + * assert.strictEqual(isString(null), false) + * ``` + * + * @category guards + * @since 2.0.0 + */ +export const isString = (input: unknown): input is string => typeof input === "string" + +/** + * A refinement that checks if a value is a `number`. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { isNumber } from "effect/Predicate" + * + * assert.strictEqual(isNumber(123), true) + * assert.strictEqual(isNumber(0), true) + * assert.strictEqual(isNumber(-1.5), true) + * assert.strictEqual(isNumber(NaN), true) + * + * assert.strictEqual(isNumber("123"), false) + * ``` + * + * @category guards + * @since 2.0.0 + */ +export const isNumber = (input: unknown): input is number => typeof input === "number" + +/** + * A refinement that checks if a value is a `boolean`. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { isBoolean } from "effect/Predicate" + * + * assert.strictEqual(isBoolean(true), true) + * assert.strictEqual(isBoolean(false), true) + * + * assert.strictEqual(isBoolean("true"), false) + * assert.strictEqual(isBoolean(0), false) + * ``` + * + * @category guards + * @since 2.0.0 + */ +export const isBoolean = (input: unknown): input is boolean => typeof input === "boolean" + +/** + * A refinement that checks if a value is a `bigint`. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { isBigInt } from "effect/Predicate" + * + * assert.strictEqual(isBigInt(1n), true) + * + * assert.strictEqual(isBigInt(1), false) + * assert.strictEqual(isBigInt("1"), false) + * ``` + * + * @category guards + * @since 2.0.0 + */ +export const isBigInt = (input: unknown): input is bigint => typeof input === "bigint" + +/** + * A refinement that checks if a value is a `symbol`. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { isSymbol } from "effect/Predicate" + * + * assert.strictEqual(isSymbol(Symbol.for("a")), true) + * + * assert.strictEqual(isSymbol("a"), false) + * ``` + * + * @category guards + * @since 2.0.0 + */ +export const isSymbol = (input: unknown): input is symbol => typeof input === "symbol" + +// TODO: make public +/** + * A refinement that checks if a value is a valid `PropertyKey` (a `string`, `number`, or `symbol`). + * @internal + */ +export const isPropertyKey = (u: unknown): u is PropertyKey => isString(u) || isNumber(u) || isSymbol(u) + +/** + * A refinement that checks if a value is a `Function`. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { isFunction } from "effect/Predicate" + * + * assert.strictEqual(isFunction(() => {}), true) + * assert.strictEqual(isFunction(isFunction), true) + * + * assert.strictEqual(isFunction("function"), false) + * ``` + * + * @category guards + * @since 2.0.0 + */ +export const isFunction: (input: unknown) => input is Function = isFunction_ + +/** + * A refinement that checks if a value is `undefined`. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { isUndefined } from "effect/Predicate" + * + * assert.strictEqual(isUndefined(undefined), true) + * + * assert.strictEqual(isUndefined(null), false) + * assert.strictEqual(isUndefined("undefined"), false) + * ``` + * + * @category guards + * @since 2.0.0 + */ +export const isUndefined = (input: unknown): input is undefined => input === undefined + +/** + * A refinement that checks if a value is not `undefined`. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { isNotUndefined } from "effect/Predicate" + * + * assert.strictEqual(isNotUndefined(null), true) + * assert.strictEqual(isNotUndefined("value"), true) + * + * assert.strictEqual(isNotUndefined(undefined), false) + * ``` + * + * @category guards + * @since 2.0.0 + */ +export const isNotUndefined = (input: A): input is Exclude => input !== undefined + +/** + * A refinement that checks if a value is `null`. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { isNull } from "effect/Predicate" + * + * assert.strictEqual(isNull(null), true) + * + * assert.strictEqual(isNull(undefined), false) + * assert.strictEqual(isNull("null"), false) + * ``` + * + * @category guards + * @since 2.0.0 + */ +export const isNull = (input: unknown): input is null => input === null + +/** + * A refinement that checks if a value is not `null`. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { isNotNull } from "effect/Predicate" + * + * assert.strictEqual(isNotNull(undefined), true) + * assert.strictEqual(isNotNull("value"), true) + * + * assert.strictEqual(isNotNull(null), false) + * ``` + * + * @category guards + * @since 2.0.0 + */ +export const isNotNull = (input: A): input is Exclude => input !== null + +/** + * A refinement that always returns `false`. The type is narrowed to `never`. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { isNever } from "effect/Predicate" + * + * assert.strictEqual(isNever(1), false) + * assert.strictEqual(isNever(null), false) + * assert.strictEqual(isNever({}), false) + * ``` + * + * @category guards + * @since 2.0.0 + */ +export const isNever: (input: unknown) => input is never = (_: unknown): _ is never => false + +/** + * A refinement that always returns `true`. The type is narrowed to `unknown`. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { isUnknown } from "effect/Predicate" + * + * assert.strictEqual(isUnknown(1), true) + * assert.strictEqual(isUnknown(null), true) + * assert.strictEqual(isUnknown({}), true) + * ``` + * + * @category guards + * @since 2.0.0 + */ +export const isUnknown: (input: unknown) => input is unknown = (_): _ is unknown => true + +/** + * Checks if the input is an object or an array. + * @internal + */ +export const isRecordOrArray = (input: unknown): input is { [x: PropertyKey]: unknown } => + typeof input === "object" && input !== null + +/** + * A refinement that checks if a value is an `object`. Note that in JavaScript, + * arrays and functions are also considered objects. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { isObject } from "effect/Predicate" + * + * assert.strictEqual(isObject({}), true) + * assert.strictEqual(isObject([]), true) + * assert.strictEqual(isObject(() => {}), true) + * + * assert.strictEqual(isObject(null), false) + * assert.strictEqual(isObject("hello"), false) + * ``` + * + * @category guards + * @since 2.0.0 + * @see isRecord to check for plain objects (excluding arrays and functions). + */ +export const isObject = (input: unknown): input is object => isRecordOrArray(input) || isFunction(input) + +/** + * A refinement that checks if a value is an object-like value and has a specific property key. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { hasProperty } from "effect/Predicate" + * + * assert.strictEqual(hasProperty({ a: 1 }, "a"), true) + * assert.strictEqual(hasProperty({ a: 1 }, "b"), false) + * + * const value: unknown = { name: "Alice" }; + * if (hasProperty(value, "name")) { + * // The type of `value` is narrowed to `{ name: unknown }` + * // and we can safely access `value.name` + * console.log(value.name) + * } + * ``` + * + * @category guards + * @since 2.0.0 + */ +export const hasProperty: { +

(property: P): (self: unknown) => self is { [K in P]: unknown } +

(self: unknown, property: P): self is { [K in P]: unknown } +} = dual( + 2, +

(self: unknown, property: P): self is { [K in P]: unknown } => + isObject(self) && (property in self) +) + +/** + * A refinement that checks if a value is an object with a `_tag` property + * that matches the given tag. This is a powerful tool for working with + * discriminated union types. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { isTagged } from "effect/Predicate" + * + * type Shape = { _tag: "circle"; radius: number } | { _tag: "square"; side: number } + * + * const isCircle = isTagged("circle") + * + * const shape1: Shape = { _tag: "circle", radius: 10 } + * const shape2: Shape = { _tag: "square", side: 5 } + * + * assert.strictEqual(isCircle(shape1), true) + * assert.strictEqual(isCircle(shape2), false) + * + * if (isCircle(shape1)) { + * // shape1 is now narrowed to { _tag: "circle"; radius: number } + * assert.strictEqual(shape1.radius, 10) + * } + * ``` + * + * @category guards + * @since 2.0.0 + */ +export const isTagged: { + (tag: K): (self: unknown) => self is { _tag: K } + (self: unknown, tag: K): self is { _tag: K } +} = dual( + 2, + (self: unknown, tag: K): self is { _tag: K } => hasProperty(self, "_tag") && self["_tag"] === tag +) + +/** + * A refinement that checks if a value is either `null` or `undefined`. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { isNullable } from "effect/Predicate" + * + * assert.strictEqual(isNullable(null), true) + * assert.strictEqual(isNullable(undefined), true) + * + * assert.strictEqual(isNullable(0), false) + * assert.strictEqual(isNullable(""), false) + * ``` + * + * @category guards + * @since 2.0.0 + * @see isNotNullable + */ +export const isNullable = (input: A): input is Extract => input === null || input === undefined + +/** + * A refinement that checks if a value is neither `null` nor `undefined`. + * The type is narrowed to `NonNullable`. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { isNotNullable } from "effect/Predicate" + * + * assert.strictEqual(isNotNullable(0), true) + * assert.strictEqual(isNotNullable("hello"), true) + * + * assert.strictEqual(isNotNullable(null), false) + * assert.strictEqual(isNotNullable(undefined), false) + * ``` + * + * @category guards + * @since 2.0.0 + * @see isNullable + */ +export const isNotNullable = (input: A): input is NonNullable => input !== null && input !== undefined + +/** + * A refinement that checks if a value is an instance of `Error`. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { isError } from "effect/Predicate" + * + * assert.strictEqual(isError(new Error("boom")), true) + * assert.strictEqual(isError(new TypeError("boom")), true) + * + * assert.strictEqual(isError({ message: "boom" }), false) + * assert.strictEqual(isError("boom"), false) + * ``` + * + * @category guards + * @since 2.0.0 + */ +export const isError = (input: unknown): input is Error => input instanceof Error + +/** + * A refinement that checks if a value is a `Uint8Array`. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { isUint8Array } from "effect/Predicate" + * + * assert.strictEqual(isUint8Array(new Uint8Array()), true) + * + * assert.strictEqual(isUint8Array(new Uint16Array()), false) + * assert.strictEqual(isUint8Array([1, 2, 3]), false) + * ``` + * + * @category guards + * @since 2.0.0 + */ +export const isUint8Array = (input: unknown): input is Uint8Array => input instanceof Uint8Array + +/** + * A refinement that checks if a value is a `Date` object. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { isDate } from "effect/Predicate" + * + * assert.strictEqual(isDate(new Date()), true) + * + * assert.strictEqual(isDate(Date.now()), false) // `Date.now()` returns a number + * assert.strictEqual(isDate("2023-01-01"), false) + * ``` + * + * @category guards + * @since 2.0.0 + */ +export const isDate = (input: unknown): input is Date => input instanceof Date + +/** + * A refinement that checks if a value is an `Iterable`. + * Many built-in types are iterable, such as `Array`, `string`, `Map`, and `Set`. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { isIterable } from "effect/Predicate" + * + * assert.strictEqual(isIterable([]), true) + * assert.strictEqual(isIterable("hello"), true) + * assert.strictEqual(isIterable(new Set()), true) + * + * assert.strictEqual(isIterable({}), false) + * assert.strictEqual(isIterable(123), false) + * ``` + * + * @category guards + * @since 2.0.0 + */ +export const isIterable = (input: unknown): input is Iterable => + typeof input === "string" || hasProperty(input, Symbol.iterator) + +/** + * A refinement that checks if a value is a record (i.e., a plain object). + * This check returns `false` for arrays, `null`, and functions. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { isRecord } from "effect/Predicate" + * + * assert.strictEqual(isRecord({}), true) + * assert.strictEqual(isRecord({ a: 1 }), true) + * + * assert.strictEqual(isRecord([]), false) + * assert.strictEqual(isRecord(new Date()), false) + * assert.strictEqual(isRecord(null), false) + * assert.strictEqual(isRecord(() => null), false) + * ``` + * + * @category guards + * @since 2.0.0 + * @see isObject + */ +export const isRecord = (input: unknown): input is { [x: string | symbol]: unknown } => + isRecordOrArray(input) && !Array.isArray(input) + +/** + * A refinement that checks if a value is a readonly record (i.e., a plain object). + * This check returns `false` for arrays, `null`, and functions. + * + * This is an alias for `isRecord`. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { isReadonlyRecord } from "effect/Predicate" + * + * assert.strictEqual(isReadonlyRecord({}), true) + * assert.strictEqual(isReadonlyRecord({ a: 1 }), true) + * + * assert.strictEqual(isReadonlyRecord([]), false) + * assert.strictEqual(isReadonlyRecord(null), false) + * ``` + * + * @category guards + * @since 2.0.0 + */ +export const isReadonlyRecord: ( + input: unknown +) => input is { readonly [x: string | symbol]: unknown } = isRecord + +/** + * A refinement that checks if a value is a `Promise`. It performs a duck-typing check + * for `.then` and `.catch` methods. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { isPromise } from "effect/Predicate" + * + * assert.strictEqual(isPromise(Promise.resolve(1)), true) + * assert.strictEqual(isPromise(new Promise(() => {})), true) + * + * assert.strictEqual(isPromise({ then() {} }), false) // Missing .catch + * assert.strictEqual(isPromise({}), false) + * ``` + * + * @category guards + * @since 2.0.0 + * @see isPromiseLike + */ +export const isPromise = ( + input: unknown +): input is Promise => + hasProperty(input, "then") && "catch" in input && isFunction(input.then) && isFunction(input.catch) + +/** + * A refinement that checks if a value is `PromiseLike`. It performs a duck-typing + * check for a `.then` method. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { isPromiseLike } from "effect/Predicate" + * + * assert.strictEqual(isPromiseLike(Promise.resolve(1)), true) + * assert.strictEqual(isPromiseLike({ then: () => {} }), true) + * + * assert.strictEqual(isPromiseLike({}), false) + * ``` + * + * @category guards + * @since 2.0.0 + * @see isPromise + */ +export const isPromiseLike = ( + input: unknown +): input is PromiseLike => hasProperty(input, "then") && isFunction(input.then) + +/** + * A refinement that checks if a value is a `RegExp`. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { Predicate } from "effect" + * + * assert.strictEqual(Predicate.isRegExp(/a/), true) + * assert.strictEqual(Predicate.isRegExp(new RegExp("a")), true) + * + * assert.strictEqual(Predicate.isRegExp("/a/"), false) + * ``` + * + * @category guards + * @since 3.9.0 + */ +export const isRegExp = (input: unknown): input is RegExp => input instanceof RegExp + +/** + * Composes a `Refinement` with another `Refinement` or `Predicate`. + * + * This can be used to chain checks. The first refinement is applied, and if it + * passes, the second check is applied to the same value, potentially refining + * the type further. + * + * @example + * ```ts + * import { Predicate } from "effect" + * import * as assert from "node:assert" + * + * const isString = (u: unknown): u is string => typeof u === "string" + * const minLength = (n: number) => (s: string): boolean => s.length >= n + * + * // Create a refinement that checks for a string with a minimum length of 3 + * const isLongString = Predicate.compose(isString, minLength(3)) + * + * let value: unknown = "hello" + * + * assert.strictEqual(isLongString(value), true) + * if (isLongString(value)) { + * // value is narrowed to string + * assert.strictEqual(value.toUpperCase(), "HELLO") + * } + * assert.strictEqual(isLongString("hi"), false) + * ``` + * + * @since 2.0.0 + */ +export const compose: { + (bc: Refinement): (ab: Refinement) => Refinement + (bc: Predicate>): (ab: Refinement) => Refinement + (ab: Refinement, bc: Refinement): Refinement + (ab: Refinement, bc: Predicate>): Refinement +} = dual( + 2, + (ab: Refinement, bc: Refinement): Refinement => + (a): a is D => ab(a) && bc(a as C) +) + +/** + * Combines two predicates to test a tuple of two values. The first predicate tests the + * first element of the tuple, and the second predicate tests the second element. + * + * @category combining + * @since 2.0.0 + */ +export const product = + (self: Predicate, that: Predicate): Predicate /* readonly because contravariant */ => + ([a, b]) => self(a) && that(b) + +/** + * Takes an iterable of predicates and returns a new predicate that tests an array of values. + * The new predicate returns `true` if each predicate at a given index is satisfied by the + * value at the same index in the array. The check stops at the length of the shorter of + * the two iterables (predicates or values). + * + * @category combining + * @since 2.0.0 + * @see tuple for a more powerful, variadic version. + */ +export const all = ( + collection: Iterable> +): Predicate> => { + return (as) => { + let collectionIndex = 0 + for (const p of collection) { + if (collectionIndex >= as.length) { + break + } + if (p(as[collectionIndex]) === false) { + return false + } + collectionIndex++ + } + return true + } +} + +/** + * Combines a predicate for a single value and an iterable of predicates for the rest of an array. + * Useful for checking the head and tail of an array separately. + * + * @category combining + * @since 2.0.0 + */ +export const productMany = ( + self: Predicate, + collection: Iterable> +): Predicate]> /* readonly because contravariant */ => { + const rest = all(collection) + return ([head, ...tail]) => self(head) === false ? false : rest(tail) +} + +/** + * Combines an array of predicates into a single predicate that tests an array of values. + * This function is highly type-aware and will produce a `Refinement` if any of the provided + * predicates are `Refinement`s, allowing for powerful type-narrowing of tuples. + * + * - If all predicates are `Predicate`, the result is `Predicate<[T, T, ...]>`. + * - If any predicate is a `Refinement`, the result is a `Refinement` that narrows + * the input tuple type to a more specific tuple type. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { Predicate } from "effect" + * + * const isString = (u: unknown): u is string => typeof u === "string" + * const isNumber = (u: unknown): u is number => typeof u === "number" + * + * // Create a refinement for a [string, number] tuple + * const isStringNumberTuple = Predicate.tuple(isString, isNumber) + * + * const value: [unknown, unknown] = ["hello", 123] + * if (isStringNumberTuple(value)) { + * // value is narrowed to [string, number] + * const [s, n] = value + * assert.strictEqual(s.toUpperCase(), "HELLO") + * assert.strictEqual(n.toFixed(2), "123.00") + * } + * assert.strictEqual(isStringNumberTuple(["hello", "123"]), false) + * ``` + * + * @since 2.0.0 + */ +export const tuple: { + >( + ...elements: T + ): [Extract] extends [never] ? Predicate<{ readonly [I in keyof T]: Predicate.In }> + : Refinement< + { readonly [I in keyof T]: T[I] extends Refinement.Any ? Refinement.In : Predicate.In }, + { readonly [I in keyof T]: T[I] extends Refinement.Any ? Refinement.Out : Predicate.In } + > +} = (...elements: ReadonlyArray) => all(elements) as any + +/** + * Combines a record of predicates into a single predicate that tests a record of values. + * This function is highly type-aware and will produce a `Refinement` if any of the provided + * predicates are `Refinement`s, allowing for powerful type-narrowing of structs. + * + * - If all predicates are `Predicate`, the result is `Predicate<{ k: T, ... }>`. + * - If any predicate is a `Refinement`, the result is a `Refinement` that narrows + * the input record type to a more specific record type. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { Predicate } from "effect" + * + * const isString = (u: unknown): u is string => typeof u === "string" + * const isNumber = (u: unknown): u is number => typeof u === "number" + * + * const personPredicate = Predicate.struct({ + * name: isString, + * age: isNumber + * }) + * + * const value: { name: unknown; age: unknown } = { name: "Alice", age: 30 } + * if (personPredicate(value)) { + * // value is narrowed to { name: string; age: number } + * assert.strictEqual(value.name.toUpperCase(), "ALICE") + * assert.strictEqual(value.age.toFixed(0), "30") + * } + * assert.strictEqual(personPredicate({ name: "Bob", age: "40" }), false) + * ``` + * + * @since 2.0.0 + */ +export const struct: { + >( + fields: R + ): [Extract] extends [never] ? + Predicate<{ readonly [K in keyof R]: Predicate.In }> : + Refinement< + { readonly [K in keyof R]: R[K] extends Refinement.Any ? Refinement.In : Predicate.In }, + { readonly [K in keyof R]: R[K] extends Refinement.Any ? Refinement.Out : Predicate.In } + > +} = (>(fields: R) => { + const keys = Object.keys(fields) + return (a: Record) => { + for (const key of keys) { + if (!fields[key](a[key] as never)) { + return false + } + } + return true + } +}) as any + +/** + * Returns a new predicate that is the logical negation of the given predicate. + * + * **Note**: If the input is a `Refinement`, the resulting predicate will be a + * simple `Predicate`, as TypeScript cannot infer the negative type. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { Predicate, Number } from "effect" + * + * const isNonPositive = Predicate.not(Number.greaterThan(0)) + * + * assert.strictEqual(isNonPositive(-1), true) + * assert.strictEqual(isNonPositive(0), true) + * assert.strictEqual(isNonPositive(1), false) + * ``` + * + * @category combinators + * @since 2.0.0 + */ +export const not = (self: Predicate): Predicate => (a) => !self(a) + +/** + * Combines two predicates with a logical "OR". The resulting predicate returns `true` + * if at least one of the predicates returns `true`. + * + * If both predicates are `Refinement`s, the resulting predicate is a `Refinement` to the + * union of their target types (`B | C`). + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { Predicate } from "effect" + * + * const isString = (u: unknown): u is string => typeof u === "string" + * const isNumber = (u: unknown): u is number => typeof u === "number" + * + * const isStringOrNumber = Predicate.or(isString, isNumber) + * + * assert.strictEqual(isStringOrNumber("hello"), true) + * assert.strictEqual(isStringOrNumber(123), true) + * assert.strictEqual(isStringOrNumber(null), false) + * + * const value: unknown = "world" + * if (isStringOrNumber(value)) { + * // value is narrowed to string | number + * console.log(value) + * } + * ``` + * + * @category combinators + * @since 2.0.0 + */ +export const or: { + (that: Refinement): (self: Refinement) => Refinement + (self: Refinement, that: Refinement): Refinement + (that: Predicate): (self: Predicate) => Predicate + (self: Predicate, that: Predicate): Predicate +} = dual(2, (self: Predicate, that: Predicate): Predicate => (a) => self(a) || that(a)) + +/** + * Combines two predicates with a logical "AND". The resulting predicate returns `true` + * only if both of the predicates return `true`. + * + * If both predicates are `Refinement`s, the resulting predicate is a `Refinement` to the + * intersection of their target types (`B & C`). + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { Predicate } from "effect" + * + * type Person = { name: string } + * type Employee = { id: number } + * + * const hasName = (u: unknown): u is Person => Predicate.hasProperty(u, "name") && typeof (u as any).name === "string" + * const hasId = (u: unknown): u is Employee => Predicate.hasProperty(u, "id") && typeof (u as any).id === "number" + * + * const isPersonAndEmployee = Predicate.and(hasName, hasId) + * + * const val: unknown = { name: "Alice", id: 123 } + * if (isPersonAndEmployee(val)) { + * // val is narrowed to Person & Employee + * assert.strictEqual(val.name, "Alice") + * assert.strictEqual(val.id, 123) + * } + * + * assert.strictEqual(isPersonAndEmployee({ name: "Bob" }), false) // Missing id + * assert.strictEqual(isPersonAndEmployee({ id: 456 }), false) // Missing name + * ``` + * + * @category combinators + * @since 2.0.0 + */ +export const and: { + (that: Refinement): (self: Refinement) => Refinement + (self: Refinement, that: Refinement): Refinement + (that: Predicate): (self: Predicate) => Predicate + (self: Predicate, that: Predicate): Predicate +} = dual(2, (self: Predicate, that: Predicate): Predicate => (a) => self(a) && that(a)) + +/** + * Combines two predicates with a logical "XOR" (exclusive OR). The resulting predicate + * returns `true` if one of the predicates returns `true`, but not both. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { Predicate } from "effect" + * + * const isPositive = (n: number) => n > 0 + * const isEven = (n: number) => n % 2 === 0 + * + * const isPositiveXorEven = Predicate.xor(isPositive, isEven) + * + * assert.strictEqual(isPositiveXorEven(4), false) // both true -> false + * assert.strictEqual(isPositiveXorEven(3), true) // one true -> true + * assert.strictEqual(isPositiveXorEven(-2), true) // one true -> true + * assert.strictEqual(isPositiveXorEven(-1), false) // both false -> false + * ``` + * + * @category combinators + * @since 2.0.0 + */ +export const xor: { + (that: Predicate): (self: Predicate) => Predicate + (self: Predicate, that: Predicate): Predicate +} = dual(2, (self: Predicate, that: Predicate): Predicate => (a) => self(a) !== that(a)) + +/** + * Combines two predicates with a logical "EQV" (equivalence). The resulting predicate + * returns `true` if both predicates return the same boolean value (both `true` or both `false`). + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { Predicate } from "effect" + * + * const isPositive = (n: number) => n > 0 + * const isEven = (n: number) => n % 2 === 0 + * + * const isPositiveEqvEven = Predicate.eqv(isPositive, isEven) + * + * assert.strictEqual(isPositiveEqvEven(4), true) // both true -> true + * assert.strictEqual(isPositiveEqvEven(3), false) // different -> false + * assert.strictEqual(isPositiveEqvEven(-2), false) // different -> false + * assert.strictEqual(isPositiveEqvEven(-1), true) // both false -> true + * ``` + * + * @category combinators + * @since 2.0.0 + */ +export const eqv: { + (that: Predicate): (self: Predicate) => Predicate + (self: Predicate, that: Predicate): Predicate +} = dual(2, (self: Predicate, that: Predicate): Predicate => (a) => self(a) === that(a)) + +/** + * Creates a predicate that represents a logical "if-then" rule. + * + * Think of it as a conditional promise: **"If `antecedent` holds true, then I promise `consequent` will also be true."** + * + * This function is invaluable for defining complex validation logic where one condition dictates another. + * + * ### How It Works + * + * The rule only fails (returns `false`) when the "if" part is `true`, but the "then" part is `false`. + * In all other cases, the promise is considered kept, and the result is `true`. + * + * This includes the concept of **"vacuous truth"**: if the "if" part is `false`, the rule doesn't apply, + * so the promise isn't broken, and the result is `true`. (e.g., "If it rains, I'll bring an umbrella." + * If it doesn't rain, you haven't broken your promise, no matter what). + * + * ### Key Details + * + * - **Logical Equivalence**: `implies(p, q)` is the same as `not(p).or(q)`, or simply `!p || q` + * in plain JavaScript. This can be a helpful way to reason about its behavior. + * + * - **Type-Safety Warning**: This function always returns a `Predicate`, never a type-narrowing + * `Refinement`. A `true` result doesn't guarantee the `consequent` passed (it could be `true` + * simply because the `antecedent` was `false`), so it cannot be used to safely narrow a type. + * + * @example + * ```ts + * // Rule: A user can only be an admin if they also belong to the "staff" group. + * import * as assert from "node:assert" + * import { Predicate } from "effect" + * + * type User = { + * isStaff: boolean + * isAdmin: boolean + * } + * + * const isValidUserPermission = Predicate.implies( + * // antecedent: "if" the user is an admin... + * (user: User) => user.isAdmin, + * // consequent: "then" they must be staff. + * (user: User) => user.isStaff + * ) + * + * // A non-admin who is not staff. Rule doesn't apply (antecedent is false). + * assert.strictEqual(isValidUserPermission({ isStaff: false, isAdmin: false }), true) + * + * // A staff member who is not an admin. Rule doesn't apply (antecedent is false). + * assert.strictEqual(isValidUserPermission({ isStaff: true, isAdmin: false }), true) + * + * // An admin who is also staff. The rule was followed. + * assert.strictEqual(isValidUserPermission({ isStaff: true, isAdmin: true }), true) + * + * // An admin who is NOT staff. The rule was broken! + * assert.strictEqual(isValidUserPermission({ isStaff: false, isAdmin: true }), false) + * ``` + * + * @category combinators + * @since 2.0.0 + */ +export const implies: { + (consequent: Predicate): (antecedent: Predicate) => Predicate + (antecedent: Predicate, consequent: Predicate): Predicate +} = dual( + 2, + (antecedent: Predicate, consequent: Predicate): Predicate => (a) => antecedent(a) ? consequent(a) : true +) + +/** + * Combines two predicates with a logical "NOR" (negated OR). The resulting predicate + * returns `true` only if both predicates return `false`. + * This is equivalent to `not(or(p, q))`. + * + * @category combinators + * @since 2.0.0 + */ +export const nor: { + (that: Predicate): (self: Predicate) => Predicate + (self: Predicate, that: Predicate): Predicate +} = dual( + 2, + (self: Predicate, that: Predicate): Predicate => (a) => !(self(a) || that(a)) +) + +/** + * Combines two predicates with a logical "NAND" (negated AND). The resulting predicate + * returns `true` if at least one of the predicates returns `false`. + * This is equivalent to `not(and(p, q))`. + * + * @category combinators + * @since 2.0.0 + */ +export const nand: { + (that: Predicate): (self: Predicate) => Predicate + (self: Predicate, that: Predicate): Predicate +} = dual( + 2, + (self: Predicate, that: Predicate): Predicate => (a) => !(self(a) && that(a)) +) + +/** + * Takes an iterable of predicates and returns a new predicate. The new predicate + * returns `true` if all predicates in the collection return `true` for a given value. + * + * This is like `Array.prototype.every` but for a collection of predicates. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { Predicate } from "effect" + * + * const isPositive = (n: number) => n > 0 + * const isEven = (n: number) => n % 2 === 0 + * + * const isPositiveAndEven = Predicate.every([isPositive, isEven]) + * + * assert.strictEqual(isPositiveAndEven(4), true) + * assert.strictEqual(isPositiveAndEven(3), false) + * assert.strictEqual(isPositiveAndEven(-2), false) + * ``` + * + * @category elements + * @since 2.0.0 + * @see some + */ +export const every = (collection: Iterable>): Predicate => (a: A) => { + for (const p of collection) { + if (!p(a)) { + return false + } + } + return true +} + +/** + * Takes an iterable of predicates and returns a new predicate. The new predicate + * returns `true` if at least one predicate in the collection returns `true` for a given value. + * + * This is like `Array.prototype.some` but for a collection of predicates. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { Predicate } from "effect" + * + * const isNegative = (n: number) => n < 0 + * const isOdd = (n: number) => n % 2 !== 0 + * + * const isNegativeOrOdd = Predicate.some([isNegative, isOdd]) + * + * assert.strictEqual(isNegativeOrOdd(-2), true) // isNegative is true + * assert.strictEqual(isNegativeOrOdd(3), true) // isOdd is true + * assert.strictEqual(isNegativeOrOdd(4), false) // both are false + * ``` + * + * @category elements + * @since 2.0.0 + * @see every + */ +export const some = (collection: Iterable>): Predicate => (a) => { + for (const p of collection) { + if (p(a)) { + return true + } + } + return false +} diff --git a/repos/effect/packages/effect/src/Pretty.ts b/repos/effect/packages/effect/src/Pretty.ts new file mode 100644 index 0000000..a772b7b --- /dev/null +++ b/repos/effect/packages/effect/src/Pretty.ts @@ -0,0 +1,205 @@ +/** + * @since 3.10.0 + */ +import * as Arr from "./Array.js" +import * as Inspectable from "./Inspectable.js" +import * as errors_ from "./internal/schema/errors.js" +import * as util_ from "./internal/schema/util.js" +import * as Option from "./Option.js" +import * as ParseResult from "./ParseResult.js" +import type * as Schema from "./Schema.js" +import * as AST from "./SchemaAST.js" + +/** + * @category model + * @since 3.10.0 + */ +export interface Pretty { + (a: To): string +} + +/** + * @category annotations + * @since 3.10.0 + */ +export type PrettyAnnotation = readonly []> = ( + ...pretties: { readonly [K in keyof TypeParameters]: Pretty } +) => Pretty + +/** + * @category prettify + * @since 3.10.0 + */ +export const make = (schema: Schema.Schema): (a: A) => string => compile(schema.ast, []) + +const getPrettyAnnotation = AST.getAnnotation>(AST.PrettyAnnotationId) + +const getMatcher = (defaultPretty: Pretty) => (ast: AST.AST): Pretty => + Option.match(getPrettyAnnotation(ast), { + onNone: () => defaultPretty, + onSome: (handler) => handler() + }) + +const toString = getMatcher((a) => String(a)) + +const stringify = getMatcher((a) => JSON.stringify(a)) + +const formatUnknown = getMatcher(Inspectable.formatUnknown) + +/** + * @since 3.10.0 + */ +export const match: AST.Match> = { + "Declaration": (ast, go, path) => { + const annotation = getPrettyAnnotation(ast) + if (Option.isSome(annotation)) { + return annotation.value(...ast.typeParameters.map((tp) => go(tp, path))) + } + throw new Error(errors_.getPrettyMissingAnnotationErrorMessage(path, ast)) + }, + "VoidKeyword": getMatcher(() => "void(0)"), + "NeverKeyword": getMatcher(() => { + throw new Error(errors_.getPrettyNeverErrorMessage) + }), + "Literal": getMatcher((literal: AST.LiteralValue): string => + typeof literal === "bigint" ? + `${String(literal)}n` : + JSON.stringify(literal) + ), + "SymbolKeyword": toString, + "UniqueSymbol": toString, + "TemplateLiteral": stringify, + "UndefinedKeyword": toString, + "UnknownKeyword": formatUnknown, + "AnyKeyword": formatUnknown, + "ObjectKeyword": formatUnknown, + "StringKeyword": stringify, + "NumberKeyword": toString, + "BooleanKeyword": toString, + "BigIntKeyword": getMatcher((a) => `${String(a)}n`), + "Enums": stringify, + "TupleType": (ast, go, path) => { + const hook = getPrettyAnnotation(ast) + if (Option.isSome(hook)) { + return hook.value() + } + const elements = ast.elements.map((e, i) => go(e.type, path.concat(i))) + const rest = ast.rest.map((annotatedAST) => go(annotatedAST.type, path)) + return (input: ReadonlyArray) => { + const output: Array = [] + let i = 0 + // --------------------------------------------- + // handle elements + // --------------------------------------------- + for (; i < elements.length; i++) { + if (input.length < i + 1) { + if (ast.elements[i].isOptional) { + continue + } + } else { + output.push(elements[i](input[i])) + } + } + // --------------------------------------------- + // handle rest element + // --------------------------------------------- + if (Arr.isNonEmptyReadonlyArray(rest)) { + const [head, ...tail] = rest + for (; i < input.length - tail.length; i++) { + output.push(head(input[i])) + } + // --------------------------------------------- + // handle post rest elements + // --------------------------------------------- + for (let j = 0; j < tail.length; j++) { + i += j + output.push(tail[j](input[i])) + } + } + + return "[" + output.join(", ") + "]" + } + }, + "TypeLiteral": (ast, go, path) => { + const hook = getPrettyAnnotation(ast) + if (Option.isSome(hook)) { + return hook.value() + } + const propertySignaturesTypes = ast.propertySignatures.map((ps) => go(ps.type, path.concat(ps.name))) + const indexSignatureTypes = ast.indexSignatures.map((is) => go(is.type, path)) + const expectedKeys: any = {} + for (let i = 0; i < propertySignaturesTypes.length; i++) { + expectedKeys[ast.propertySignatures[i].name] = null + } + return (input: { readonly [x: PropertyKey]: unknown }) => { + const output: Array = [] + // --------------------------------------------- + // handle property signatures + // --------------------------------------------- + for (let i = 0; i < propertySignaturesTypes.length; i++) { + const ps = ast.propertySignatures[i] + const name = ps.name + if (ps.isOptional && !Object.prototype.hasOwnProperty.call(input, name)) { + continue + } + output.push( + `${Inspectable.formatPropertyKey(name)}: ${propertySignaturesTypes[i](input[name])}` + ) + } + // --------------------------------------------- + // handle index signatures + // --------------------------------------------- + if (indexSignatureTypes.length > 0) { + for (let i = 0; i < indexSignatureTypes.length; i++) { + const type = indexSignatureTypes[i] + const keys = util_.getKeysForIndexSignature(input, ast.indexSignatures[i].parameter) + for (const key of keys) { + if (Object.prototype.hasOwnProperty.call(expectedKeys, key)) { + continue + } + output.push(`${Inspectable.formatPropertyKey(key)}: ${type(input[key])}`) + } + } + } + + return Arr.isNonEmptyReadonlyArray(output) ? "{ " + output.join(", ") + " }" : "{}" + } + }, + "Union": (ast, go, path) => { + const hook = getPrettyAnnotation(ast) + if (Option.isSome(hook)) { + return hook.value() + } + const types = ast.types.map((ast) => [ParseResult.is({ ast } as any), go(ast, path)] as const) + return (a) => { + const index = types.findIndex(([is]) => is(a)) + if (index === -1) { + throw new Error(errors_.getPrettyNoMatchingSchemaErrorMessage(a, path, ast)) + } + return types[index][1](a) + } + }, + "Suspend": (ast, go, path) => { + return Option.match(getPrettyAnnotation(ast), { + onNone: () => { + const get = util_.memoizeThunk(() => go(ast.f(), path)) + return (a) => get()(a) + }, + onSome: (handler) => handler() + }) + }, + "Refinement": (ast, go, path) => { + return Option.match(getPrettyAnnotation(ast), { + onNone: () => go(ast.from, path), + onSome: (handler) => handler() + }) + }, + "Transformation": (ast, go, path) => { + return Option.match(getPrettyAnnotation(ast), { + onNone: () => go(ast.to, path), + onSome: (handler) => handler() + }) + } +} + +const compile = AST.getCompiler(match) diff --git a/repos/effect/packages/effect/src/PrimaryKey.ts b/repos/effect/packages/effect/src/PrimaryKey.ts new file mode 100644 index 0000000..dda1d11 --- /dev/null +++ b/repos/effect/packages/effect/src/PrimaryKey.ts @@ -0,0 +1,23 @@ +/** + * @since 2.0.0 + */ + +/** + * @since 2.0.0 + * @category symbols + */ +export const symbol: unique symbol = Symbol.for("effect/PrimaryKey") + +/** + * @since 2.0.0 + * @category models + */ +export interface PrimaryKey { + [symbol](): string +} + +/** + * @since 2.0.0 + * @category accessors + */ +export const value = (self: PrimaryKey): string => self[symbol]() diff --git a/repos/effect/packages/effect/src/PubSub.ts b/repos/effect/packages/effect/src/PubSub.ts new file mode 100644 index 0000000..581e80b --- /dev/null +++ b/repos/effect/packages/effect/src/PubSub.ts @@ -0,0 +1,182 @@ +/** + * @since 2.0.0 + */ +import type * as Effect from "./Effect.js" +import * as internal from "./internal/pubsub.js" +import type { Pipeable } from "./Pipeable.js" +import type * as Queue from "./Queue.js" +import type * as Scope from "./Scope.js" + +/** + * A `PubSub` is an asynchronous message hub into which publishers can publish + * messages of type `A` and subscribers can subscribe to take messages of type + * `A`. + * + * @since 2.0.0 + * @category models + */ +export interface PubSub extends Queue.Enqueue, Pipeable { + /** + * Publishes a message to the `PubSub`, returning whether the message was published + * to the `PubSub`. + */ + publish(value: A): Effect.Effect + + /** + * Publishes all of the specified messages to the `PubSub`, returning whether they + * were published to the `PubSub`. + */ + publishAll(elements: Iterable): Effect.Effect + + /** + * Subscribes to receive messages from the `PubSub`. The resulting subscription can + * be evaluated multiple times within the scope to take a message from the `PubSub` + * each time. + */ + readonly subscribe: Effect.Effect, never, Scope.Scope> +} + +/** + * Creates a bounded `PubSub` with the back pressure strategy. The `PubSub` will retain + * messages until they have been taken by all subscribers, applying back + * pressure to publishers if the `PubSub` is at capacity. + * + * For best performance use capacities that are powers of two. + * + * @since 2.0.0 + * @category constructors + */ +export const bounded: ( + capacity: number | { readonly capacity: number; readonly replay?: number | undefined } +) => Effect.Effect> = internal.bounded + +/** + * Creates a bounded `PubSub` with the dropping strategy. The `PubSub` will drop new + * messages if the `PubSub` is at capacity. + * + * For best performance use capacities that are powers of two. + * + * @since 2.0.0 + * @category constructors + */ +export const dropping: ( + capacity: number | { readonly capacity: number; readonly replay?: number | undefined } +) => Effect.Effect> = internal.dropping + +/** + * Creates a bounded `PubSub` with the sliding strategy. The `PubSub` will add new + * messages and drop old messages if the `PubSub` is at capacity. + * + * For best performance use capacities that are powers of two. + * + * @since 2.0.0 + * @category constructors + */ +export const sliding: ( + capacity: number | { readonly capacity: number; readonly replay?: number | undefined } +) => Effect.Effect> = internal.sliding + +/** + * Creates an unbounded `PubSub`. + * + * @since 2.0.0 + * @category constructors + */ +export const unbounded: (options?: { readonly replay?: number | undefined }) => Effect.Effect> = + internal.unbounded + +/** + * Returns the number of elements the queue can hold. + * + * @since 2.0.0 + * @category getters + */ +export const capacity: (self: PubSub) => number = internal.capacity + +/** + * Retrieves the size of the queue, which is equal to the number of elements + * in the queue. This may be negative if fibers are suspended waiting for + * elements to be added to the queue. + * + * @since 2.0.0 + * @category getters + */ +export const size: (self: PubSub) => Effect.Effect = internal.size + +/** + * Returns `true` if the `Queue` contains at least one element, `false` + * otherwise. + * + * @since 2.0.0 + * @category getters + */ +export const isFull: (self: PubSub) => Effect.Effect = internal.isFull + +/** + * Returns `true` if the `Queue` contains zero elements, `false` otherwise. + * + * @since 2.0.0 + * @category getters + */ +export const isEmpty: (self: PubSub) => Effect.Effect = internal.isEmpty + +/** + * Interrupts any fibers that are suspended on `offer` or `take`. Future calls + * to `offer*` and `take*` will be interrupted immediately. + * + * @since 2.0.0 + * @category utils + */ +export const shutdown: (self: PubSub) => Effect.Effect = internal.shutdown + +/** + * Returns `true` if `shutdown` has been called, otherwise returns `false`. + * + * @since 2.0.0 + * @category getters + */ +export const isShutdown: (self: PubSub) => Effect.Effect = internal.isShutdown + +/** + * Waits until the queue is shutdown. The `Effect` returned by this method will + * not resume until the queue has been shutdown. If the queue is already + * shutdown, the `Effect` will resume right away. + * + * @since 2.0.0 + * @category utils + */ +export const awaitShutdown: (self: PubSub) => Effect.Effect = internal.awaitShutdown + +/** + * Publishes a message to the `PubSub`, returning whether the message was published + * to the `PubSub`. + * + * @since 2.0.0 + * @category utils + */ +export const publish: { + (value: A): (self: PubSub) => Effect.Effect + (self: PubSub, value: A): Effect.Effect +} = internal.publish + +/** + * Publishes all of the specified messages to the `PubSub`, returning whether they + * were published to the `PubSub`. + * + * @since 2.0.0 + * @category utils + */ +export const publishAll: { + (elements: Iterable): (self: PubSub) => Effect.Effect + (self: PubSub, elements: Iterable): Effect.Effect +} = internal.publishAll + +/** + * Subscribes to receive messages from the `PubSub`. The resulting subscription can + * be evaluated multiple times within the scope to take a message from the `PubSub` + * each time. + * + * @since 2.0.0 + * @category utils + */ +export const subscribe: (self: PubSub) => Effect.Effect, never, Scope.Scope> = internal.subscribe diff --git a/repos/effect/packages/effect/src/Queue.ts b/repos/effect/packages/effect/src/Queue.ts new file mode 100644 index 0000000..e947896 --- /dev/null +++ b/repos/effect/packages/effect/src/Queue.ts @@ -0,0 +1,644 @@ +/** + * @since 2.0.0 + */ +import type * as Chunk from "./Chunk.js" +import type * as Deferred from "./Deferred.js" +import type * as Effect from "./Effect.js" +import * as internal from "./internal/queue.js" +import type * as MutableQueue from "./MutableQueue.js" +import type * as MutableRef from "./MutableRef.js" +import type * as Option from "./Option.js" +import type { Pipeable } from "./Pipeable.js" +import type * as Types from "./Types.js" +import type * as Unify from "./Unify.js" + +/** + * @since 2.0.0 + * @category symbols + */ +export const EnqueueTypeId: unique symbol = internal.EnqueueTypeId + +/** + * @since 2.0.0 + * @category symbols + */ +export type EnqueueTypeId = typeof EnqueueTypeId + +/** + * @since 2.0.0 + * @category symbols + */ +export const DequeueTypeId: unique symbol = internal.DequeueTypeId + +/** + * @since 2.0.0 + * @category symbols + */ +export type DequeueTypeId = typeof DequeueTypeId + +/** + * @since 2.0.0 + * @category symbols + */ +export const QueueStrategyTypeId: unique symbol = internal.QueueStrategyTypeId + +/** + * @since 2.0.0 + * @category symbols + */ +export type QueueStrategyTypeId = typeof QueueStrategyTypeId + +/** + * @since 2.0.0 + * @category symbols + */ +export const BackingQueueTypeId: unique symbol = internal.BackingQueueTypeId + +/** + * @since 2.0.0 + * @category symbols + */ +export type BackingQueueTypeId = typeof BackingQueueTypeId + +/** + * @since 2.0.0 + * @category models + */ +export interface Queue extends Enqueue, Dequeue { + /** @internal */ + readonly queue: BackingQueue + /** @internal */ + readonly takers: MutableQueue.MutableQueue> + /** @internal */ + readonly shutdownHook: Deferred.Deferred + /** @internal */ + readonly shutdownFlag: MutableRef.MutableRef + /** @internal */ + readonly strategy: Strategy + + readonly [Unify.typeSymbol]?: unknown + readonly [Unify.unifySymbol]?: QueueUnify + readonly [Unify.ignoreSymbol]?: QueueUnifyIgnore +} + +/** + * @category models + * @since 3.8.0 + */ +export interface QueueUnify extends DequeueUnify { + Queue?: () => Extract> +} + +/** + * @category models + * @since 3.8.0 + */ +export interface QueueUnifyIgnore extends DequeueUnifyIgnore { + Dequeue?: true +} + +/** + * @since 2.0.0 + * @category models + */ +export interface Enqueue extends Queue.EnqueueVariance, BaseQueue, Pipeable { + /** + * Places one value in the queue. + */ + offer(value: A): Effect.Effect + + /** + * Places one value in the queue when possible without needing the fiber runtime. + */ + unsafeOffer(value: A): boolean + + /** + * For Bounded Queue: uses the `BackPressure` Strategy, places the values in + * the queue and always returns true. If the queue has reached capacity, then + * the fiber performing the `offerAll` will be suspended until there is room + * in the queue. + * + * For Unbounded Queue: Places all values in the queue and returns true. + * + * For Sliding Queue: uses `Sliding` Strategy If there is room in the queue, + * it places the values otherwise it removes the old elements and enqueues the + * new ones. Always returns true. + * + * For Dropping Queue: uses `Dropping` Strategy, It places the values in the + * queue but if there is no room it will not enqueue them and return false. + */ + offerAll(iterable: Iterable): Effect.Effect +} + +/** + * @since 2.0.0 + * @category models + */ +export interface Dequeue extends Effect.Effect, Queue.DequeueVariance, BaseQueue { + /** + * Takes the oldest value in the queue. If the queue is empty, this will return + * a computation that resumes when an item has been added to the queue. + */ + readonly take: Effect.Effect + + /** + * Takes all the values in the queue and returns the values. If the queue is + * empty returns an empty collection. + */ + readonly takeAll: Effect.Effect> + + /** + * Takes up to max number of values from the queue. + */ + takeUpTo(max: number): Effect.Effect> + + /** + * Takes a number of elements from the queue between the specified minimum and + * maximum. If there are fewer than the minimum number of elements available, + * suspends until at least the minimum number of elements have been collected. + */ + takeBetween(min: number, max: number): Effect.Effect> + + readonly [Unify.typeSymbol]?: unknown + readonly [Unify.unifySymbol]?: DequeueUnify + readonly [Unify.ignoreSymbol]?: DequeueUnifyIgnore +} + +/** + * @category models + * @since 3.8.0 + */ +export interface DequeueUnify extends Effect.EffectUnify { + Dequeue?: () => A[Unify.typeSymbol] extends Dequeue | infer _ ? Dequeue : never +} + +/** + * @category models + * @since 3.8.0 + */ +export interface DequeueUnifyIgnore extends Effect.EffectUnifyIgnore { + Effect?: true +} + +/** + * The base interface that all `Queue`s must implement. + * + * @since 2.0.0 + * @category models + */ +export interface BaseQueue { + /** + * Returns the number of elements the queue can hold. + */ + capacity(): number + + /** + * Returns false if shutdown has been called. + */ + isActive(): boolean + + /** + * Retrieves the size of the queue, which is equal to the number of elements + * in the queue. This may be negative if fibers are suspended waiting for + * elements to be added to the queue. + */ + readonly size: Effect.Effect + + /** + * Retrieves the size of the queue, which is equal to the number of elements + * in the queue. This may be negative if fibers are suspended waiting for + * elements to be added to the queue. Returns None if shutdown has been called + */ + unsafeSize(): Option.Option + + /** + * Returns `true` if the `Queue` contains at least one element, `false` + * otherwise. + */ + readonly isFull: Effect.Effect + + /** + * Returns `true` if the `Queue` contains zero elements, `false` otherwise. + */ + readonly isEmpty: Effect.Effect + + /** + * Interrupts any fibers that are suspended on `offer` or `take`. Future calls + * to `offer*` and `take*` will be interrupted immediately. + */ + readonly shutdown: Effect.Effect + + /** + * Returns `true` if `shutdown` has been called, otherwise returns `false`. + */ + readonly isShutdown: Effect.Effect + + /** + * Waits until the queue is shutdown. The `Effect` returned by this method will + * not resume until the queue has been shutdown. If the queue is already + * shutdown, the `Effect` will resume right away. + */ + readonly awaitShutdown: Effect.Effect +} + +/** + * @since 2.0.0 + * @category models + */ +export interface Strategy extends Queue.StrategyVariance { + /** + * Returns the number of surplus values that were unable to be added to the + * `Queue` + */ + surplusSize(): number + + /** + * Determines how the `Queue.Strategy` should shut down when the `Queue` is + * shut down. + */ + readonly shutdown: Effect.Effect + + /** + * Determines the behavior of the `Queue.Strategy` when there are surplus + * values that could not be added to the `Queue` following an `offer` + * operation. + */ + handleSurplus( + iterable: Iterable, + queue: BackingQueue, + takers: MutableQueue.MutableQueue>, + isShutdown: MutableRef.MutableRef + ): Effect.Effect + + /** + * It is called when the backing queue is empty but there are some + * takers that can be completed + */ + onCompleteTakersWithEmptyQueue( + takers: MutableQueue.MutableQueue> + ): void + + /** + * Determines the behavior of the `Queue.Strategy` when the `Queue` has empty + * slots following a `take` operation. + */ + unsafeOnQueueEmptySpace( + queue: BackingQueue, + takers: MutableQueue.MutableQueue> + ): void +} + +/** + * @since 2.0.0 + * @category models + */ +export interface BackingQueue extends Queue.BackingQueueVariance { + /** + * Dequeues an element from the queue. + * Returns either an element from the queue, or the `def` param. + */ + poll(def: Def): A | Def + /** + * Dequeues up to `limit` elements from the queue. + */ + pollUpTo(limit: number): Chunk.Chunk + /** + * Enqueues a collection of values into the queue. + * + * Returns a `Chunk` of the values that were **not** able to be enqueued. + */ + offerAll(elements: Iterable): Chunk.Chunk + /** + * Offers an element to the queue. + * + * Returns whether the enqueue was successful or not. + */ + offer(element: A): boolean + /** + * The **maximum** number of elements that a queue can hold. + * + * **Note**: unbounded queues can still implement this interface with + * `capacity = Infinity`. + */ + capacity(): number + /** + * Returns the number of elements currently in the queue + */ + length(): number +} + +/** + * @since 2.0.0 + */ +export declare namespace Queue { + /** + * @since 2.0.0 + * @category models + */ + export interface EnqueueVariance { + readonly [EnqueueTypeId]: { + readonly _In: Types.Contravariant + } + } + + /** + * @since 2.0.0 + * @category models + */ + export interface DequeueVariance { + readonly [DequeueTypeId]: { + readonly _Out: Types.Covariant + } + } + + /** + * @since 2.0.0 + * @category models + */ + export interface StrategyVariance { + readonly [QueueStrategyTypeId]: { + readonly _A: Types.Invariant + } + } + + /** + * @since 2.0.0 + * @category models + */ + export interface BackingQueueVariance { + readonly [BackingQueueTypeId]: { + readonly _A: Types.Invariant + } + } +} + +/** + * Returns `true` if the specified value is a `Queue`, `false` otherwise. + * + * @since 2.0.0 + * @category refinements + */ +export const isQueue: (u: unknown) => u is Queue = internal.isQueue + +/** + * Returns `true` if the specified value is a `Dequeue`, `false` otherwise. + * + * @since 2.0.0 + * @category refinements + */ +export const isDequeue: (u: unknown) => u is Dequeue = internal.isDequeue + +/** + * Returns `true` if the specified value is a `Enqueue`, `false` otherwise. + * + * @since 2.0.0 + * @category refinements + */ +export const isEnqueue: (u: unknown) => u is Enqueue = internal.isEnqueue + +/** + * @since 2.0.0 + * @category strategies + */ +export const backPressureStrategy: () => Strategy = internal.backPressureStrategy + +/** + * @since 2.0.0 + * @category strategies + */ +export const droppingStrategy: () => Strategy = internal.droppingStrategy + +/** + * @since 2.0.0 + * @category strategies + */ +export const slidingStrategy: () => Strategy = internal.slidingStrategy + +/** + * @since 2.0.0 + * @category constructors + */ +export const make: (queue: BackingQueue, strategy: Strategy) => Effect.Effect> = internal.make + +/** + * Makes a new bounded `Queue`. When the capacity of the queue is reached, any + * additional calls to `offer` will be suspended until there is more room in + * the queue. + * + * **Note**: When possible use only power of 2 capacities; this will provide + * better performance by utilising an optimised version of the underlying + * `RingBuffer`. + * + * @since 2.0.0 + * @category constructors + */ +export const bounded: (requestedCapacity: number) => Effect.Effect> = internal.bounded + +/** + * Makes a new bounded `Queue` with the dropping strategy. + * + * When the capacity of the queue is reached, new elements will be dropped and the + * old elements will remain. + * + * **Note**: When possible use only power of 2 capacities; this will provide + * better performance by utilising an optimised version of the underlying + * `RingBuffer`. + * + * @since 2.0.0 + * @category constructors + */ +export const dropping: (requestedCapacity: number) => Effect.Effect> = internal.dropping + +/** + * Makes a new bounded `Queue` with the sliding strategy. + * + * When the capacity of the queue is reached, new elements will be added and the + * old elements will be dropped. + * + * **Note**: When possible use only power of 2 capacities; this will provide + * better performance by utilising an optimised version of the underlying + * `RingBuffer`. + * + * @since 2.0.0 + * @category constructors + */ +export const sliding: (requestedCapacity: number) => Effect.Effect> = internal.sliding + +/** + * Creates a new unbounded `Queue`. + * + * @since 2.0.0 + * @category constructors + */ +export const unbounded: () => Effect.Effect> = internal.unbounded + +/** + * Returns the number of elements the queue can hold. + * + * @since 2.0.0 + * @category getters + */ +export const capacity: (self: Dequeue | Enqueue) => number = internal.capacity + +/** + * Retrieves the size of the queue, which is equal to the number of elements + * in the queue. This may be negative if fibers are suspended waiting for + * elements to be added to the queue. + * + * @since 2.0.0 + * @category getters + */ +export const size: (self: Dequeue | Enqueue) => Effect.Effect = internal.size + +/** + * Returns `true` if the `Queue` contains zero elements, `false` otherwise. + * + * @since 2.0.0 + * @category getters + */ +export const isEmpty: (self: Dequeue | Enqueue) => Effect.Effect = internal.isEmpty + +/** + * Returns `true` if the `Queue` contains at least one element, `false` + * otherwise. + * + * @since 2.0.0 + * @category getters + */ +export const isFull: (self: Dequeue | Enqueue) => Effect.Effect = internal.isFull + +/** + * Returns `true` if `shutdown` has been called, otherwise returns `false`. + * + * @since 2.0.0 + * @category getters + */ +export const isShutdown: (self: Dequeue | Enqueue) => Effect.Effect = internal.isShutdown + +/** + * Waits until the queue is shutdown. The `Effect` returned by this method will + * not resume until the queue has been shutdown. If the queue is already + * shutdown, the `Effect` will resume right away. + * + * @since 2.0.0 + * @category utils + */ +export const awaitShutdown: (self: Dequeue | Enqueue) => Effect.Effect = internal.awaitShutdown + +/** + * Interrupts any fibers that are suspended on `offer` or `take`. Future calls + * to `offer*` and `take*` will be interrupted immediately. + * + * @since 2.0.0 + * @category utils + */ +export const shutdown: (self: Dequeue | Enqueue) => Effect.Effect = internal.shutdown + +/** + * Places one value in the queue. + * + * @since 2.0.0 + * @category utils + */ +export const offer: { + (value: A): (self: Enqueue) => Effect.Effect + (self: Enqueue, value: A): Effect.Effect +} = internal.offer + +/** + * Places one value in the queue. + * + * @since 2.0.0 + * @category utils + */ +export const unsafeOffer: { + (value: A): (self: Enqueue) => boolean + (self: Enqueue, value: A): boolean +} = internal.unsafeOffer + +/** + * For Bounded Queue: uses the `BackPressure` Strategy, places the values in + * the queue and always returns true. If the queue has reached capacity, then + * the fiber performing the `offerAll` will be suspended until there is room + * in the queue. + * + * For Unbounded Queue: Places all values in the queue and returns true. + * + * For Sliding Queue: uses `Sliding` Strategy If there is room in the queue, + * it places the values otherwise it removes the old elements and enqueues the + * new ones. Always returns true. + * + * For Dropping Queue: uses `Dropping` Strategy, It places the values in the + * queue but if there is no room it will not enqueue them and return false. + * + * @since 2.0.0 + * @category utils + */ +export const offerAll: { + (iterable: Iterable): (self: Enqueue) => Effect.Effect + (self: Enqueue, iterable: Iterable): Effect.Effect +} = internal.offerAll + +/** + * Returns the first value in the `Queue` as a `Some`, or `None` if the queue + * is empty. + * + * @since 2.0.0 + * @category utils + */ +export const poll: (self: Dequeue) => Effect.Effect> = internal.poll + +/** + * Takes the oldest value in the queue. If the queue is empty, this will return + * a computation that resumes when an item has been added to the queue. + * + * @since 2.0.0 + * @category utils + */ +export const take: (self: Dequeue) => Effect.Effect = internal.take + +/** + * Takes all the values in the queue and returns the values. If the queue is + * empty returns an empty collection. + * + * @since 2.0.0 + * @category utils + */ +export const takeAll: (self: Dequeue) => Effect.Effect> = internal.takeAll + +/** + * Takes up to max number of values from the queue. + * + * @since 2.0.0 + * @category utils + */ +export const takeUpTo: { + (max: number): (self: Dequeue) => Effect.Effect> + (self: Dequeue, max: number): Effect.Effect> +} = internal.takeUpTo + +/** + * Takes a number of elements from the queue between the specified minimum and + * maximum. If there are fewer than the minimum number of elements available, + * suspends until at least the minimum number of elements have been collected. + * + * @since 2.0.0 + * @category utils + */ +export const takeBetween: { + (min: number, max: number): (self: Dequeue) => Effect.Effect> + (self: Dequeue, min: number, max: number): Effect.Effect> +} = internal.takeBetween + +/** + * Takes the specified number of elements from the queue. If there are fewer + * than the specified number of elements available, it suspends until they + * become available. + * + * @since 2.0.0 + * @category utils + */ +export const takeN: { + (n: number): (self: Dequeue) => Effect.Effect> + (self: Dequeue, n: number): Effect.Effect> +} = internal.takeN diff --git a/repos/effect/packages/effect/src/Random.ts b/repos/effect/packages/effect/src/Random.ts new file mode 100644 index 0000000..5ebdbb5 --- /dev/null +++ b/repos/effect/packages/effect/src/Random.ts @@ -0,0 +1,204 @@ +/** + * @since 2.0.0 + */ +import type * as Array from "./Array.js" +import type * as Cause from "./Cause.js" +import type * as Chunk from "./Chunk.js" +import type * as Context from "./Context.js" +import type * as Effect from "./Effect.js" +import * as defaultServices from "./internal/defaultServices.js" +import * as internal from "./internal/random.js" +import type * as NonEmptyIterable from "./NonEmptyIterable.js" + +/** + * @since 2.0.0 + * @category symbols + */ +export const RandomTypeId: unique symbol = internal.RandomTypeId + +/** + * @since 2.0.0 + * @category symbols + */ +export type RandomTypeId = typeof RandomTypeId + +/** + * @since 2.0.0 + * @category models + */ +export interface Random { + readonly [RandomTypeId]: RandomTypeId + /** + * Returns the next numeric value from the pseudo-random number generator. + */ + readonly next: Effect.Effect + /** + * Returns the next boolean value from the pseudo-random number generator. + */ + readonly nextBoolean: Effect.Effect + /** + * Returns the next integer value from the pseudo-random number generator. + */ + readonly nextInt: Effect.Effect + /** + * Returns the next numeric value in the specified range from the + * pseudo-random number generator. + */ + nextRange(min: number, max: number): Effect.Effect + /** + * Returns the next integer value in the specified range from the + * pseudo-random number generator. + */ + nextIntBetween(min: number, max: number): Effect.Effect + /** + * Uses the pseudo-random number generator to shuffle the specified iterable. + */ + shuffle(elements: Iterable): Effect.Effect> +} + +/** + * Returns the next numeric value from the pseudo-random number generator. + * + * @since 2.0.0 + * @category constructors + */ +export const next: Effect.Effect = defaultServices.next + +/** + * Returns the next integer value from the pseudo-random number generator. + * + * @since 2.0.0 + * @category constructors + */ +export const nextInt: Effect.Effect = defaultServices.nextInt + +/** + * Returns the next boolean value from the pseudo-random number generator. + * + * @since 2.0.0 + * @category constructors + */ +export const nextBoolean: Effect.Effect = defaultServices.nextBoolean + +/** + * Returns the next numeric value in the specified range from the + * pseudo-random number generator. + * + * @since 2.0.0 + * @category constructors + */ +export const nextRange: (min: number, max: number) => Effect.Effect = defaultServices.nextRange + +/** + * Returns the next integer value in the specified range from the + * pseudo-random number generator. + * + * @since 2.0.0 + * @category constructors + */ +export const nextIntBetween: (min: number, max: number) => Effect.Effect = defaultServices.nextIntBetween + +/** + * Uses the pseudo-random number generator to shuffle the specified iterable. + * + * @since 2.0.0 + * @category constructors + */ +export const shuffle: (elements: Iterable) => Effect.Effect> = defaultServices.shuffle + +/** + * Get a random element from an iterable. + * + * @example + * ```ts + * import { Effect, Random } from "effect" + * + * Effect.gen(function* () { + * const randomItem = yield* Random.choice([1, 2, 3]) + * console.log(randomItem) + * }) + * ``` + * + * @since 3.6.0 + * @category constructors + */ +export const choice: >( + elements: Self +) => Self extends NonEmptyIterable.NonEmptyIterable ? Effect.Effect + : Self extends Array.NonEmptyReadonlyArray ? Effect.Effect + : Self extends Iterable ? Effect.Effect + : never = defaultServices.choice + +/** + * Retreives the `Random` service from the context and uses it to run the + * specified workflow. + * + * @since 2.0.0 + * @category constructors + */ +export const randomWith: (f: (random: Random) => Effect.Effect) => Effect.Effect = + defaultServices.randomWith + +/** + * @since 2.0.0 + * @category context + */ +export const Random: Context.Tag = internal.randomTag + +/** + * Constructs the `Random` service, seeding the pseudo-random number generator + * with an hash of the specified seed. + * This constructor is useful for generating predictable sequences of random values for specific use cases. + * + * Example uses: + * - Generating random UI data for visual tests. + * - Creating data that needs to change daily but remain the same throughout a single day, such as using a date as the seed. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { Effect, Random } from "effect" + * + * const random1 = Random.make("myseed") + * const random2 = Random.make("myseed") + * + * assert.equal(Effect.runSync(random1.next), Effect.runSync(random2.next)) + * ``` + * + * @since 3.5.0 + * @category constructors + */ +export const make: (seed: A) => Random = internal.make + +/** + * Constructs the `Random` service from an array of literal values. + * The service will cycle through the provided values in order when generating random values. + * This constructor is useful for creating deterministic sequences for testing or when specific values need to be returned. + * + * @example + * ```ts + * import { Effect, Random } from "effect" + * + * Effect.gen(function* () { + * console.log(yield* Random.next) // 0.2 + * console.log(yield* Random.next) // 0.5 + * console.log(yield* Random.next) // 0.8 + * console.log(yield* Random.next) // 0.2 (cycles back) + * }).pipe(Effect.withRandom(Random.fixed([0.2, 0.5, 0.8]))) + * ``` + * + * @example + * ```ts + * import { Effect, Random } from "effect" + * + * Effect.gen(function* () { + * console.log(yield* Random.nextBoolean) // true + * console.log(yield* Random.nextBoolean) // false + * console.log(yield* Random.nextBoolean) // true + * }).pipe(Effect.withRandom(Random.fixed([true, false, true]))) + * ``` + * + * @since 3.11.0 + * @category constructors + */ +export const fixed: >(values: T) => Random = internal.fixed diff --git a/repos/effect/packages/effect/src/RateLimiter.ts b/repos/effect/packages/effect/src/RateLimiter.ts new file mode 100644 index 0000000..6d84e67 --- /dev/null +++ b/repos/effect/packages/effect/src/RateLimiter.ts @@ -0,0 +1,138 @@ +/** + * Limits the number of calls to a resource to a maximum amount in some interval. + * + * @since 2.0.0 + */ +import type { DurationInput } from "./Duration.js" +import type { Effect } from "./Effect.js" +import * as internal from "./internal/rateLimiter.js" +import type { Scope } from "./Scope.js" + +/** + * Limits the number of calls to a resource to a maximum amount in some interval. + * + * Note that only the moment of starting the effect is rate limited: the number + * of concurrent executions is not bounded. + * + * @since 2.0.0 + * @category models + */ +export interface RateLimiter { + (task: Effect): Effect +} + +/** + * @since 2.0.0 + */ +export declare namespace RateLimiter { + /** + * @since 2.0.0 + * @category models + */ + export interface Options { + /** + * The maximum number of requests that should be allowed. + */ + readonly limit: number + /** + * The interval to utilize for rate-limiting requests. The semantics of the + * specified `interval` vary depending on the chosen `algorithm`: + * + * `token-bucket`: The maximum number of requests will be spread out over + * the provided interval if no tokens are available. + * + * For example, for a `RateLimiter` using the `token-bucket` algorithm with + * a `limit` of `10` and an `interval` of `1 seconds`, `1` request can be + * made every `100 millis`. + * + * `fixed-window`: The maximum number of requests will be reset during each + * interval. For example, for a `RateLimiter` using the `fixed-window` + * algorithm with a `limit` of `10` and an `interval` of `1 seconds`, a + * maximum of `10` requests can be made each second. + */ + readonly interval: DurationInput + /** + * The algorithm to utilize for rate-limiting requests. + * + * Defaults to `token-bucket`. + */ + readonly algorithm?: "fixed-window" | "token-bucket" + } +} + +/** + * Constructs a new `RateLimiter` which will utilize the specified algorithm + * to limit requests (defaults to `token-bucket`). + * + * Notes + * - Only the moment of starting the effect is rate limited. The number of concurrent executions is not bounded. + * - Instances of `RateLimiter` can be composed. + * - The "cost" per effect can be changed. See {@link withCost} + * + * @example + * ```ts + * import { Effect, RateLimiter } from "effect"; + * import { compose } from "effect/Function" + * + * const program = Effect.scoped( + * Effect.gen(function* ($) { + * const perMinuteRL = yield* $(RateLimiter.make({ limit: 30, interval: "1 minutes" })) + * const perSecondRL = yield* $(RateLimiter.make({ limit: 2, interval: "1 seconds" })) + * + * // This rate limiter respects both the 30 calls per minute + * // and the 2 calls per second constraints. + * const rateLimit = compose(perMinuteRL, perSecondRL) + * + * // simulate repeated calls + * for (let n = 0; n < 100; n++) { + * // wrap the effect we want to limit with rateLimit + * yield* $(rateLimit(Effect.log("Calling RateLimited Effect"))); + * } + * }) + * ); + * ``` + * + * @since 2.0.0 + * @category constructors + */ +export const make: (options: RateLimiter.Options) => Effect = internal.make + +/** + * Alters the per-effect cost of the rate-limiter. + * + * This can be used for "credit" based rate-limiting where different API endpoints + * cost a different number of credits within a time window. + * Eg: 1000 credits / hour, where a query costs 1 credit and a mutation costs 5 credits. + * + * @example + * ```ts + * import { Effect, RateLimiter } from "effect"; + * import { compose } from "effect/Function"; + * + * const program = Effect.scoped( + * Effect.gen(function* ($) { + * // Create a rate limiter that has an hourly limit of 1000 credits + * const rateLimiter = yield* $(RateLimiter.make({ limit: 1000, interval: "1 hours" })); + * // Query API costs 1 credit per call ( 1 is the default cost ) + * const queryAPIRL = compose(rateLimiter, RateLimiter.withCost(1)); + * // Mutation API costs 5 credits per call + * const mutationAPIRL = compose(rateLimiter, RateLimiter.withCost(5)); + + * // Use the pre-defined rate limiters + * yield* $(queryAPIRL(Effect.log("Sample Query"))); + * yield* $(mutationAPIRL(Effect.log("Sample Mutation"))); + * + * // Or set a cost on-the-fly + * yield* $( + * rateLimiter(Effect.log("Another query with a different cost")).pipe( + * RateLimiter.withCost(3) + * ) + * ); + * }) + * ); + * ``` + * + * @since 2.0.0 + * @category combinators + */ +export const withCost: (cost: number) => (effect: Effect) => Effect = internal.withCost diff --git a/repos/effect/packages/effect/src/RcMap.ts b/repos/effect/packages/effect/src/RcMap.ts new file mode 100644 index 0000000..9e78cdc --- /dev/null +++ b/repos/effect/packages/effect/src/RcMap.ts @@ -0,0 +1,142 @@ +/** + * @since 3.5.0 + */ +import type * as Cause from "./Cause.js" +import type * as Duration from "./Duration.js" +import type * as Effect from "./Effect.js" +import * as internal from "./internal/rcMap.js" +import { type Pipeable } from "./Pipeable.js" +import type * as Scope from "./Scope.js" +import type * as Types from "./Types.js" + +/** + * @since 3.5.0 + * @category type ids + */ +export const TypeId: unique symbol = internal.TypeId + +/** + * @since 3.5.0 + * @category type ids + */ +export type TypeId = typeof TypeId + +/** + * @since 3.5.0 + * @category models + */ +export interface RcMap extends Pipeable { + readonly [TypeId]: RcMap.Variance +} + +/** + * @since 3.5.0 + * @category models + */ +export declare namespace RcMap { + /** + * @since 3.5.0 + * @category models + */ + export interface Variance { + readonly _K: Types.Contravariant + readonly _A: Types.Covariant + readonly _E: Types.Covariant + } +} + +/** + * An `RcMap` can contain multiple reference counted resources that can be indexed + * by a key. The resources are lazily acquired on the first call to `get` and + * released when the last reference is released. + * + * Complex keys can extend `Equal` and `Hash` to allow lookups by value. + * + * **Options** + * + * - `capacity`: The maximum number of resources that can be held in the map. + * - `idleTimeToLive`: When the reference count reaches zero, the resource will be released after this duration. + * Can be a static duration or a function that returns a duration based on the key. + * + * @since 3.5.0 + * @category models + * @example + * ```ts + * import { Effect, RcMap } from "effect" + * + * Effect.gen(function*() { + * const map = yield* RcMap.make({ + * lookup: (key: string) => + * Effect.acquireRelease( + * Effect.succeed(`acquired ${key}`), + * () => Effect.log(`releasing ${key}`) + * ) + * }) + * + * // Get "foo" from the map twice, which will only acquire it once. + * // It will then be released once the scope closes. + * yield* RcMap.get(map, "foo").pipe( + * Effect.andThen(RcMap.get(map, "foo")), + * Effect.scoped + * ) + * }) + * ``` + */ +export const make: { + ( + options: { + readonly lookup: (key: K) => Effect.Effect + readonly idleTimeToLive?: Duration.DurationInput | ((key: K) => Duration.DurationInput) | undefined + readonly capacity?: undefined + } + ): Effect.Effect, never, Scope.Scope | R> + ( + options: { + readonly lookup: (key: K) => Effect.Effect + readonly idleTimeToLive?: Duration.DurationInput | ((key: K) => Duration.DurationInput) | undefined + readonly capacity: number + } + ): Effect.Effect, never, Scope.Scope | R> +} = internal.make + +/** + * @since 3.5.0 + * @category combinators + */ +export const get: { + (key: K): (self: RcMap) => Effect.Effect + (self: RcMap, key: K): Effect.Effect +} = internal.get + +/** + * @since 3.17.7 + * @category combinators + */ +export const has: { + (key: K): (self: RcMap) => Effect.Effect + (self: RcMap, key: K): Effect.Effect +} = internal.has + +/** + * @since 3.8.0 + * @category combinators + */ +export const keys: (self: RcMap) => Effect.Effect> = internal.keys + +/** + * @since 3.13.0 + * @category combinators + */ +export const invalidate: { + (key: K): (self: RcMap) => Effect.Effect + (self: RcMap, key: K): Effect.Effect +} = internal.invalidate + +/** + * @since 3.13.0 + * @category combinators + */ +export const touch: { + (key: K): (self: RcMap) => Effect.Effect + (self: RcMap, key: K): Effect.Effect +} = internal.touch diff --git a/repos/effect/packages/effect/src/RcRef.ts b/repos/effect/packages/effect/src/RcRef.ts new file mode 100644 index 0000000..af2bd21 --- /dev/null +++ b/repos/effect/packages/effect/src/RcRef.ts @@ -0,0 +1,122 @@ +/** + * @since 3.5.0 + */ +import type * as Duration from "./Duration.js" +import type * as Effect from "./Effect.js" +import * as internal from "./internal/rcRef.js" +import type * as Readable from "./Readable.js" +import type * as Scope from "./Scope.js" +import type * as Types from "./Types.js" +import type * as Unify from "./Unify.js" + +/** + * @since 3.5.0 + * @category type ids + */ +export const TypeId: unique symbol = internal.TypeId + +/** + * @since 3.5.0 + * @category type ids + */ +export type TypeId = typeof TypeId + +/** + * @since 3.5.0 + * @category models + */ +export interface RcRef + extends Effect.Effect, Readable.Readable +{ + readonly [TypeId]: RcRef.Variance + readonly [Unify.typeSymbol]?: unknown + readonly [Unify.unifySymbol]?: RcRefUnify + readonly [Unify.ignoreSymbol]?: RcRefUnifyIgnore +} + +/** + * @category models + * @since 3.8.0 + */ +export interface RcRefUnify extends Effect.EffectUnify { + RcRef?: () => A[Unify.typeSymbol] extends RcRef | infer _ ? RcRef + : never +} + +/** + * @category models + * @since 3.8.0 + */ +export interface RcRefUnifyIgnore extends Effect.EffectUnifyIgnore { + Effect?: true +} +/** + * @since 3.5.0 + * @category models + */ +export declare namespace RcRef { + /** + * @since 3.5.0 + * @category models + */ + export interface Variance { + readonly _A: Types.Covariant + readonly _E: Types.Covariant + } +} + +/** + * Create an `RcRef` from an acquire `Effect`. + * + * An RcRef wraps a reference counted resource that can be acquired and released + * multiple times. + * + * The resource is lazily acquired on the first call to `get` and released when + * the last reference is released. + * + * @since 3.5.0 + * @category constructors + * @example + * ```ts + * import { Effect, RcRef } from "effect" + * + * Effect.gen(function*() { + * const ref = yield* RcRef.make({ + * acquire: Effect.acquireRelease( + * Effect.succeed("foo"), + * () => Effect.log("release foo") + * ) + * }) + * + * // will only acquire the resource once, and release it + * // when the scope is closed + * yield* RcRef.get(ref).pipe( + * Effect.andThen(RcRef.get(ref)), + * Effect.scoped + * ) + * }) + * ``` + */ +export const make: ( + options: { + readonly acquire: Effect.Effect + /** + * When the reference count reaches zero, the resource will be released + * after this duration. + */ + readonly idleTimeToLive?: Duration.DurationInput | undefined + } +) => Effect.Effect, never, R | Scope.Scope> = internal.make + +/** + * @since 3.5.0 + * @category combinators + */ +export const get: (self: RcRef) => Effect.Effect = internal.get + +/** + * @since 3.19.6 + * @category combinators + * @experimental + */ +export const invalidate: (self: RcRef) => Effect.Effect = internal.invalidate diff --git a/repos/effect/packages/effect/src/Readable.ts b/repos/effect/packages/effect/src/Readable.ts new file mode 100644 index 0000000..e2b28ef --- /dev/null +++ b/repos/effect/packages/effect/src/Readable.ts @@ -0,0 +1,93 @@ +/** + * @since 2.0.0 + */ +import type { Effect } from "./Effect.js" +import { dual } from "./Function.js" +import * as core from "./internal/core.js" +import { type Pipeable, pipeArguments } from "./Pipeable.js" +import { hasProperty } from "./Predicate.js" +import type { NoInfer } from "./Types.js" + +/** + * @since 2.0.0 + * @category type ids + */ +export const TypeId: unique symbol = Symbol.for("effect/Readable") + +/** + * @since 2.0.0 + * @category type ids + */ +export type TypeId = typeof TypeId + +/** + * @since 2.0.0 + * @category models + */ +export interface Readable extends Pipeable { + readonly [TypeId]: TypeId + readonly get: Effect +} + +/** + * @since 2.0.0 + * @category refinements + */ +export const isReadable = (u: unknown): u is Readable => hasProperty(u, TypeId) + +const Proto: Omit, "get"> = { + [TypeId]: TypeId, + pipe() { + return pipeArguments(this, arguments) + } +} + +/** + * @since 2.0.0 + * @category constructors + */ +export const make = (get: Effect): Readable => { + const self = Object.create(Proto) + self.get = get + return self +} + +/** + * @since 2.0.0 + * @category combinators + */ +export const map: { + (f: (a: NoInfer) => B): (fa: Readable) => Readable + (self: Readable, f: (a: NoInfer) => B): Readable +} = dual( + 2, + (self: Readable, f: (a: NoInfer) => B): Readable => make(core.map(self.get, f)) +) + +/** + * @since 2.0.0 + * @category combinators + */ +export const mapEffect: { + ( + f: (a: NoInfer) => Effect + ): (fa: Readable) => Readable + ( + self: Readable, + f: (a: NoInfer) => Effect + ): Readable +} = dual(2, ( + self: Readable, + f: (a: NoInfer) => Effect +): Readable => make(core.flatMap(self.get, f))) + +/** + * @since 2.0.0 + * @category constructors + */ +export const unwrap = ( + effect: Effect, E1, R1> +): Readable => + make( + core.flatMap(effect, (s) => s.get) + ) diff --git a/repos/effect/packages/effect/src/Record.ts b/repos/effect/packages/effect/src/Record.ts new file mode 100644 index 0000000..79d1912 --- /dev/null +++ b/repos/effect/packages/effect/src/Record.ts @@ -0,0 +1,1274 @@ +/** + * This module provides utility functions for working with records in TypeScript. + * + * @since 2.0.0 + */ + +import type { Either } from "./Either.js" +import * as E from "./Either.js" +import * as Equal from "./Equal.js" +import type { Equivalence } from "./Equivalence.js" +import { dual, identity } from "./Function.js" +import type { TypeLambda } from "./HKT.js" +import * as Option from "./Option.js" +import type { NoInfer } from "./Types.js" + +/** + * @category models + * @since 2.0.0 + */ +export type ReadonlyRecord = { + readonly [P in K]: A +} + +/** + * @since 2.0.0 + */ +export declare namespace ReadonlyRecord { + type IsFiniteString = T extends "" ? true : + [T] extends [`${infer Head}${infer Rest}`] + ? string extends Head ? false : `${number}` extends Head ? false : Rest extends "" ? true : IsFiniteString + : false + + /** + * @since 2.0.0 + */ + export type NonLiteralKey = K extends string ? IsFiniteString extends true ? string : K + : symbol + + /** + * @since 2.0.0 + */ + export type IntersectKeys = [string] extends [K1 | K2] ? + NonLiteralKey & NonLiteralKey + : K1 & K2 +} + +/** + * @category type lambdas + * @since 2.0.0 + */ +export interface ReadonlyRecordTypeLambda extends TypeLambda { + readonly type: ReadonlyRecord +} + +/** + * Creates a new, empty record. + * + * @category constructors + * @since 2.0.0 + */ +export const empty = (): Record< + ReadonlyRecord.NonLiteralKey, + V +> => ({} as any) + +/** + * Determine if a record is empty. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { isEmptyRecord } from "effect/Record" + * + * assert.deepStrictEqual(isEmptyRecord({}), true); + * assert.deepStrictEqual(isEmptyRecord({ a: 3 }), false); + * ``` + * + * @category guards + * @since 2.0.0 + */ +export const isEmptyRecord = (self: Record): self is Record => + keys(self).length === 0 + +/** + * Determine if a record is empty. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { isEmptyReadonlyRecord } from "effect/Record" + * + * assert.deepStrictEqual(isEmptyReadonlyRecord({}), true); + * assert.deepStrictEqual(isEmptyReadonlyRecord({ a: 3 }), false); + * ``` + * + * @category guards + * @since 2.0.0 + */ +export const isEmptyReadonlyRecord: ( + self: ReadonlyRecord +) => self is ReadonlyRecord = isEmptyRecord + +/** + * Takes an iterable and a projection function and returns a record. + * The projection function maps each value of the iterable to a tuple of a key and a value, which is then added to the resulting record. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { fromIterableWith } from "effect/Record" + * + * const input = [1, 2, 3, 4] + * + * assert.deepStrictEqual( + * fromIterableWith(input, a => [String(a), a * 2]), + * { '1': 2, '2': 4, '3': 6, '4': 8 } + * ) + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const fromIterableWith: { + ( + f: (a: A) => readonly [K, B] + ): (self: Iterable) => Record, B> + ( + self: Iterable, + f: (a: A) => readonly [K, B] + ): Record, B> +} = dual( + 2, + ( + self: Iterable, + f: (a: A) => readonly [K, B] + ): Record, B> => { + const out: Record = empty() + for (const a of self) { + const [k, b] = f(a) + out[k] = b + } + return out + } +) + +/** + * Creates a new record from an iterable, utilizing the provided function to determine the key for each element. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { fromIterableBy } from "effect/Record" + * + * const users = [ + * { id: "2", name: "name2" }, + * { id: "1", name: "name1" } + * ] + * + * assert.deepStrictEqual( + * fromIterableBy(users, user => user.id), + * { + * "2": { id: "2", name: "name2" }, + * "1": { id: "1", name: "name1" } + * } + * ) + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const fromIterableBy = ( + items: Iterable, + f: (a: A) => K +): Record, A> => fromIterableWith(items, (a) => [f(a), a]) + +/** + * Builds a record from an iterable of key-value pairs. + * + * If there are conflicting keys when using `fromEntries`, the last occurrence of the key/value pair will overwrite the + * previous ones. So the resulting record will only have the value of the last occurrence of each key. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { fromEntries } from "effect/Record" + * + * const input: Array<[string, number]> = [["a", 1], ["b", 2]] + * + * assert.deepStrictEqual(fromEntries(input), { a: 1, b: 2 }) + * ``` + * + * @since 2.0.0 + * @category constructors + */ +export const fromEntries: ( + entries: Iterable +) => Record, Entry[1]> = Object.fromEntries + +/** + * Transforms the values of a record into an `Array` with a custom mapping function. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { collect } from "effect/Record" + * + * const x = { a: 1, b: 2, c: 3 } + * assert.deepStrictEqual(collect(x, (key, n) => [key, n]), [["a", 1], ["b", 2], ["c", 3]]) + * ``` + * + * @category conversions + * @since 2.0.0 + */ +export const collect: { + (f: (key: K, a: A) => B): (self: ReadonlyRecord) => Array + (self: ReadonlyRecord, f: (key: K, a: A) => B): Array +} = dual( + 2, + (self: ReadonlyRecord, f: (key: K, a: A) => B): Array => { + const out: Array = [] + for (const key of keys(self)) { + out.push(f(key, self[key])) + } + return out + } +) + +/** + * Takes a record and returns an array of tuples containing its keys and values. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { toEntries } from "effect/Record" + * + * const x = { a: 1, b: 2, c: 3 } + * assert.deepStrictEqual(toEntries(x), [["a", 1], ["b", 2], ["c", 3]]) + * ``` + * + * @category conversions + * @since 2.0.0 + */ +export const toEntries: (self: ReadonlyRecord) => Array<[K, A]> = collect(( + key, + value +) => [key, value]) + +/** + * Returns the number of key/value pairs in a record. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { size } from "effect/Record"; + * + * assert.deepStrictEqual(size({ a: "a", b: 1, c: true }), 3); + * ``` + * + * @since 2.0.0 + */ +export const size = (self: ReadonlyRecord): number => keys(self).length + +/** + * Check if a given `key` exists in a record. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { empty, has } from "effect/Record" + * + * assert.deepStrictEqual(has({ a: 1, b: 2 }, "a"), true); + * assert.deepStrictEqual(has(empty(), "c"), false); + * ``` + * + * @since 2.0.0 + */ +export const has: { + ( + key: NoInfer + ): (self: ReadonlyRecord) => boolean + ( + self: ReadonlyRecord, + key: NoInfer + ): boolean +} = dual( + 2, + ( + self: ReadonlyRecord, + key: NoInfer + ): boolean => Object.prototype.hasOwnProperty.call(self, key) +) + +/** + * Retrieve a value at a particular key from a record, returning it wrapped in an `Option`. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { Record as R, Option } from "effect" + * + * const person: Record = { name: "John Doe", age: 35 } + * + * assert.deepStrictEqual(R.get(person, "name"), Option.some("John Doe")) + * assert.deepStrictEqual(R.get(person, "email"), Option.none()) + * ``` + * + * @since 2.0.0 + */ +export const get: { + (key: NoInfer): (self: ReadonlyRecord) => Option.Option + (self: ReadonlyRecord, key: NoInfer): Option.Option +} = dual( + 2, + (self: ReadonlyRecord, key: NoInfer): Option.Option => + has(self, key) ? Option.some(self[key]) : Option.none() +) + +/** + * Apply a function to the element at the specified key, creating a new record. + * If the key does not exist, the record is returned unchanged. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { Record as R } from "effect" + * + * const f = (x: number) => x * 2 + * + * assert.deepStrictEqual( + * R.modify({ a: 3 }, 'a', f), + * { a: 6 } + * ) + * assert.deepStrictEqual( + * R.modify({ a: 3 } as Record, 'b', f), + * { a: 3 } + * ) + * ``` + * + * @since 2.0.0 + */ +export const modify: { + ( + key: NoInfer, + f: (a: A) => B + ): (self: ReadonlyRecord) => Record + (self: ReadonlyRecord, key: NoInfer, f: (a: A) => B): Record +} = dual( + 3, + (self: ReadonlyRecord, key: NoInfer, f: (a: A) => B): Record => { + if (!has(self, key)) { + return { ...self } + } + return { ...self, [key]: f(self[key]) } + } +) + +/** + * Apply a function to the element at the specified key, creating a new record, + * or return `None` if the key doesn't exist. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { Record as R, Option } from "effect" + * + * const f = (x: number) => x * 2 + * + * assert.deepStrictEqual( + * R.modifyOption({ a: 3 }, 'a', f), + * Option.some({ a: 6 }) + * ) + * assert.deepStrictEqual( + * R.modifyOption({ a: 3 } as Record, 'b', f), + * Option.none() + * ) + * ``` + * + * @since 2.0.0 + */ +export const modifyOption: { + ( + key: NoInfer, + f: (a: A) => B + ): (self: ReadonlyRecord) => Option.Option> + ( + self: ReadonlyRecord, + key: NoInfer, + f: (a: A) => B + ): Option.Option> +} = dual( + 3, + ( + self: ReadonlyRecord, + key: NoInfer, + f: (a: A) => B + ): Option.Option> => { + if (!has(self, key)) { + return Option.none() + } + return Option.some({ ...self, [key]: f(self[key]) }) + } +) + +/** + * Replaces a value in the record with the new value passed as parameter. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { Record, Option } from "effect" + * + * assert.deepStrictEqual( + * Record.replaceOption({ a: 1, b: 2, c: 3 }, 'a', 10), + * Option.some({ a: 10, b: 2, c: 3 }) + * ) + * assert.deepStrictEqual(Record.replaceOption(Record.empty(), 'a', 10), Option.none()) + * ``` + * + * @since 2.0.0 + */ +export const replaceOption: { + ( + key: NoInfer, + b: B + ): (self: ReadonlyRecord) => Option.Option> + ( + self: ReadonlyRecord, + key: NoInfer, + b: B + ): Option.Option> +} = dual( + 3, + ( + self: ReadonlyRecord, + key: NoInfer, + b: B + ): Option.Option> => modifyOption(self, key, () => b) +) + +/** + * If the given key exists in the record, returns a new record with the key removed, + * otherwise returns a copy of the original record. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { remove } from "effect/Record" + * + * assert.deepStrictEqual(remove({ a: 1, b: 2 }, "a"), { b: 2 }) + * ``` + * + * @since 2.0.0 + */ +export const remove: { + (key: X): (self: ReadonlyRecord) => Record, A> + (self: ReadonlyRecord, key: X): Record, A> +} = dual( + 2, + (self: ReadonlyRecord, key: X): Record, A> => { + if (!has(self, key)) { + return { ...self } + } + const out = { ...self } + delete out[key] + return out + } +) + +/** + * Retrieves the value of the property with the given `key` from a record and returns an `Option` + * of a tuple with the value and the record with the removed property. + * If the key is not present, returns `O.none`. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { Record as R, Option } from "effect" + * + * assert.deepStrictEqual(R.pop({ a: 1, b: 2 }, "a"), Option.some([1, { b: 2 }])) + * assert.deepStrictEqual(R.pop({ a: 1, b: 2 } as Record, "c"), Option.none()) + * ``` + * + * @category record + * @since 2.0.0 + */ +export const pop: { + ( + key: X + ): (self: ReadonlyRecord) => Option.Option<[A, Record, A>]> + ( + self: ReadonlyRecord, + key: X + ): Option.Option<[A, Record, A>]> +} = dual(2, ( + self: ReadonlyRecord, + key: X +): Option.Option<[A, Record, A>]> => + has(self, key) ? Option.some([self[key], remove(self, key)]) : Option.none()) + +/** + * Maps a record into another record by applying a transformation function to each of its values. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { map } from "effect/Record" + * + * const f = (n: number) => `-${n}` + * + * assert.deepStrictEqual(map({ a: 3, b: 5 }, f), { a: "-3", b: "-5" }) + * + * const g = (n: number, key: string) => `${key.toUpperCase()}-${n}` + * + * assert.deepStrictEqual(map({ a: 3, b: 5 }, g), { a: "A-3", b: "B-5" }) + * ``` + * + * @category mapping + * @since 2.0.0 + */ +export const map: { + (f: (a: A, key: NoInfer) => B): (self: ReadonlyRecord) => Record + (self: ReadonlyRecord, f: (a: A, key: NoInfer) => B): Record +} = dual( + 2, + (self: ReadonlyRecord, f: (a: A, key: NoInfer) => B): Record => { + const out: Record = { ...self } as any + for (const key of keys(self)) { + out[key] = f(self[key], key) + } + return out + } +) + +/** + * Maps the keys of a `ReadonlyRecord` while preserving the corresponding values. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { mapKeys } from "effect/Record" + * + * assert.deepStrictEqual(mapKeys({ a: 3, b: 5 }, (key) => key.toUpperCase()), { A: 3, B: 5 }) + * ``` + * + * @category mapping + * @since 2.0.0 + */ +export const mapKeys: { + ( + f: (key: K, a: A) => K2 + ): (self: ReadonlyRecord) => Record + ( + self: ReadonlyRecord, + f: (key: K, a: A) => K2 + ): Record +} = dual( + 2, + ( + self: ReadonlyRecord, + f: (key: K, a: A) => K2 + ): Record => { + const out: Record = {} as any + for (const key of keys(self)) { + const a = self[key] + out[f(key, a)] = a + } + return out + } +) + +/** + * Maps entries of a `ReadonlyRecord` using the provided function, allowing modification of both keys and corresponding values. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { mapEntries } from "effect/Record" + * + * assert.deepStrictEqual(mapEntries({ a: 3, b: 5 }, (a, key) => [key.toUpperCase(), a + 1]), { A: 4, B: 6 }) + * ``` + * + * @category mapping + * @since 2.0.0 + */ +export const mapEntries: { + ( + f: (a: A, key: K) => readonly [K2, B] + ): (self: ReadonlyRecord) => Record + ( + self: ReadonlyRecord, + f: (a: A, key: K) => [K2, B] + ): Record +} = dual( + 2, + ( + self: ReadonlyRecord, + f: (a: A, key: K) => [K2, B] + ): Record => { + const out = > {} + for (const key of keys(self)) { + const [k, b] = f(self[key], key) + out[k] = b + } + return out + } +) + +/** + * Transforms a record into a record by applying the function `f` to each key and value in the original record. + * If the function returns `Some`, the key-value pair is included in the output record. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { Record, Option } from "effect" + * + * const x = { a: 1, b: 2, c: 3 } + * const f = (a: number, key: string) => a > 2 ? Option.some(a * 2) : Option.none() + * assert.deepStrictEqual(Record.filterMap(x, f), { c: 6 }) + * ``` + * + * @since 2.0.0 + */ +export const filterMap: { + ( + f: (a: A, key: K) => Option.Option + ): (self: ReadonlyRecord) => Record, B> + ( + self: ReadonlyRecord, + f: (a: A, key: K) => Option.Option + ): Record, B> +} = dual( + 2, + ( + self: ReadonlyRecord, + f: (a: A, key: K) => Option.Option + ): Record, B> => { + const out: Record = empty() + for (const key of keys(self)) { + const o = f(self[key], key) + if (Option.isSome(o)) { + out[key] = o.value + } + } + return out + } +) + +/** + * Selects properties from a record whose values match the given predicate. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { filter } from "effect/Record" + * + * const x = { a: 1, b: 2, c: 3, d: 4 } + * assert.deepStrictEqual(filter(x, (n) => n > 2), { c: 3, d: 4 }) + * ``` + * + * @category filtering + * @since 2.0.0 + */ +export const filter: { + ( + refinement: (a: NoInfer, key: K) => a is B + ): (self: ReadonlyRecord) => Record, B> + ( + predicate: (A: NoInfer, key: K) => boolean + ): (self: ReadonlyRecord) => Record, A> + ( + self: ReadonlyRecord, + refinement: (a: A, key: K) => a is B + ): Record, B> + ( + self: ReadonlyRecord, + predicate: (a: A, key: K) => boolean + ): Record, A> +} = dual( + 2, + ( + self: ReadonlyRecord, + predicate: (a: A, key: K) => boolean + ): Record, A> => { + const out: Record = empty() + for (const key of keys(self)) { + if (predicate(self[key], key)) { + out[key] = self[key] + } + } + return out + } +) + +/** + * Given a record with `Option` values, returns a new record containing only the `Some` values, preserving the original keys. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { Record, Option } from "effect" + * + * assert.deepStrictEqual( + * Record.getSomes({ a: Option.some(1), b: Option.none(), c: Option.some(2) }), + * { a: 1, c: 2 } + * ) + * ``` + * + * @category filtering + * @since 2.0.0 + */ +export const getSomes: ( + self: ReadonlyRecord> +) => Record, A> = filterMap( + identity +) + +/** + * Given a record with `Either` values, returns a new record containing only the `Left` values, preserving the original keys. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { Record, Either } from "effect" + * + * assert.deepStrictEqual( + * Record.getLefts({ a: Either.right(1), b: Either.left("err"), c: Either.right(2) }), + * { b: "err" } + * ) + * ``` + * + * @category filtering + * @since 2.0.0 + */ +export const getLefts = ( + self: ReadonlyRecord> +): Record, L> => { + const out: Record = empty() + for (const key of keys(self)) { + const value = self[key] + if (E.isLeft(value)) { + out[key] = value.left + } + } + + return out +} + +/** + * Given a record with `Either` values, returns a new record containing only the `Right` values, preserving the original keys. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { Record, Either } from "effect" + * + * assert.deepStrictEqual( + * Record.getRights({ a: Either.right(1), b: Either.left("err"), c: Either.right(2) }), + * { a: 1, c: 2 } + * ) + * ``` + * + * @category filtering + * @since 2.0.0 + */ +export const getRights = ( + self: ReadonlyRecord> +): Record => { + const out: Record = empty() + for (const key of keys(self)) { + const value = self[key] + if (E.isRight(value)) { + out[key] = value.right + } + } + + return out +} + +/** + * Partitions the elements of a record into two groups: those that match a predicate, and those that don't. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { Record, Either } from "effect" + * + * const x = { a: 1, b: 2, c: 3 } + * const f = (n: number) => (n % 2 === 0 ? Either.right(n) : Either.left(n)) + * assert.deepStrictEqual(Record.partitionMap(x, f), [{ a: 1, c: 3 }, { b: 2}]) + * ``` + * + * @category filtering + * @since 2.0.0 + */ +export const partitionMap: { + ( + f: (a: A, key: K) => Either + ): ( + self: ReadonlyRecord + ) => [left: Record, B>, right: Record, C>] + ( + self: ReadonlyRecord, + f: (a: A, key: K) => Either + ): [left: Record, B>, right: Record, C>] +} = dual( + 2, + ( + self: ReadonlyRecord, + f: (a: A, key: K) => Either + ): [left: Record, B>, right: Record, C>] => { + const left: Record = empty() + const right: Record = empty() + for (const key of keys(self)) { + const e = f(self[key], key) + if (E.isLeft(e)) { + left[key] = e.left + } else { + right[key] = e.right + } + } + return [left, right] + } +) + +/** + * Partitions a record of `Either` values into two separate records, + * one with the `Left` values and one with the `Right` values. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { Record, Either } from "effect" + * + * assert.deepStrictEqual( + * Record.separate({ a: Either.left("e"), b: Either.right(1) }), + * [{ a: "e" }, { b: 1 }] + * ) + * ``` + * + * @category filtering + * @since 2.0.0 + */ +export const separate: ( + self: ReadonlyRecord> +) => [Record, A>, Record, B>] = partitionMap(identity) + +/** + * Partitions a record into two separate records based on the result of a predicate function. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { partition } from "effect/Record" + * + * assert.deepStrictEqual( + * partition({ a: 1, b: 3 }, (n) => n > 2), + * [{ a: 1 }, { b: 3 }] + * ) + * ``` + * + * @category filtering + * @since 2.0.0 + */ +export const partition: { + (refinement: (a: NoInfer, key: K) => a is B): ( + self: ReadonlyRecord + ) => [ + excluded: Record, Exclude>, + satisfying: Record, B> + ] + ( + predicate: (a: NoInfer, key: K) => boolean + ): ( + self: ReadonlyRecord + ) => [excluded: Record, A>, satisfying: Record, A>] + ( + self: ReadonlyRecord, + refinement: (a: A, key: K) => a is B + ): [ + excluded: Record, Exclude>, + satisfying: Record, B> + ] + ( + self: ReadonlyRecord, + predicate: (a: A, key: K) => boolean + ): [excluded: Record, A>, satisfying: Record, A>] +} = dual( + 2, + ( + self: ReadonlyRecord, + predicate: (a: A, key: K) => boolean + ): [excluded: Record, A>, satisfying: Record, A>] => { + const left: Record = empty() + const right: Record = empty() + for (const key of keys(self)) { + if (predicate(self[key], key)) { + right[key] = self[key] + } else { + left[key] = self[key] + } + } + return [left, right] + } +) + +/** + * Retrieve the keys of a given record as an array. + * + * @since 2.0.0 + */ +export const keys = (self: ReadonlyRecord): Array => + Object.keys(self) as Array + +/** + * Retrieve the values of a given record as an array. + * + * @since 2.0.0 + */ +export const values = (self: ReadonlyRecord): Array => collect(self, (_, a) => a) + +/** + * Add a new key-value pair or update an existing key's value in a record. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { set } from "effect/Record" + * + * assert.deepStrictEqual(set("a", 5)({ a: 1, b: 2 }), { a: 5, b: 2 }); + * assert.deepStrictEqual(set("c", 5)({ a: 1, b: 2 }), { a: 1, b: 2, c: 5 }); + * ``` + * + * @since 2.0.0 + */ +export const set: { + ( + key: K1, + value: B + ): (self: ReadonlyRecord) => Record + ( + self: ReadonlyRecord, + key: K1, + value: B + ): Record +} = dual( + 3, + ( + self: ReadonlyRecord, + key: K1, + value: B + ): Record => { + return { ...self, [key]: value } as any + } +) + +/** + * Replace a key's value in a record and return the updated record. + * If the key does not exist in the record, a copy of the original record is returned. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { Record } from "effect" + * + * assert.deepStrictEqual(Record.replace("a", 3)({ a: 1, b: 2 }), { a: 3, b: 2 }); + * assert.deepStrictEqual(Record.replace("c", 3)({ a: 1, b: 2 }), { a: 1, b: 2 }); + * ``` + * + * @since 2.0.0 + */ +export const replace: { + (key: NoInfer, value: B): (self: ReadonlyRecord) => Record + (self: ReadonlyRecord, key: NoInfer, value: B): Record +} = dual( + 3, + (self: ReadonlyRecord, key: NoInfer, value: B): Record => { + if (has(self, key)) { + return { ...self, [key]: value } + } + return { ...self } + } +) + +/** + * Check if all the keys and values in one record are also found in another record. + * + * @since 2.0.0 + */ +export const isSubrecordBy = (equivalence: Equivalence): { + (that: ReadonlyRecord): (self: ReadonlyRecord) => boolean + (self: ReadonlyRecord, that: ReadonlyRecord): boolean +} => + dual(2, (self: ReadonlyRecord, that: ReadonlyRecord): boolean => { + for (const key of keys(self)) { + if (!has(that, key) || !equivalence(self[key], that[key])) { + return false + } + } + return true + }) + +/** + * Check if one record is a subrecord of another, meaning it contains all the keys and values found in the second record. + * This comparison uses default equality checks (`Equal.equivalence()`). + * + * @since 2.0.0 + */ +export const isSubrecord: { + (that: ReadonlyRecord): (self: ReadonlyRecord) => boolean + (self: ReadonlyRecord, that: ReadonlyRecord): boolean +} = isSubrecordBy(Equal.equivalence()) + +/** + * Reduce a record to a single value by combining its entries with a specified function. + * + * @category folding + * @since 2.0.0 + */ +export const reduce: { + ( + zero: Z, + f: (accumulator: Z, value: V, key: K) => Z + ): (self: ReadonlyRecord) => Z + (self: ReadonlyRecord, zero: Z, f: (accumulator: Z, value: V, key: K) => Z): Z +} = dual( + 3, + ( + self: ReadonlyRecord, + zero: Z, + f: (accumulator: Z, value: V, key: K) => Z + ): Z => { + let out: Z = zero + for (const key of keys(self)) { + out = f(out, self[key], key) + } + return out + } +) + +/** + * Check if all entries in a record meet a specific condition. + * + * @since 2.0.0 + */ +export const every: { + ( + refinement: (value: A, key: K) => value is B + ): (self: ReadonlyRecord) => self is ReadonlyRecord + (predicate: (value: A, key: K) => boolean): (self: ReadonlyRecord) => boolean + ( + self: ReadonlyRecord, + refinement: (value: A, key: K) => value is B + ): self is ReadonlyRecord + (self: ReadonlyRecord, predicate: (value: A, key: K) => boolean): boolean +} = dual( + 2, + ( + self: ReadonlyRecord, + refinement: (value: A, key: K) => value is B + ): self is ReadonlyRecord => { + for (const key of keys(self)) { + if (!refinement(self[key], key)) { + return false + } + } + return true + } +) + +/** + * Check if any entry in a record meets a specific condition. + * + * @since 2.0.0 + */ +export const some: { + (predicate: (value: A, key: K) => boolean): (self: ReadonlyRecord) => boolean + (self: ReadonlyRecord, predicate: (value: A, key: K) => boolean): boolean +} = dual( + 2, + (self: ReadonlyRecord, predicate: (value: A, key: K) => boolean): boolean => { + for (const key of keys(self)) { + if (predicate(self[key], key)) { + return true + } + } + return false + } +) + +/** + * Merge two records, preserving entries that exist in either of the records. + * + * @since 2.0.0 + */ +export const union: { + ( + that: ReadonlyRecord, + combine: (selfValue: A, thatValue: B) => C + ): (self: ReadonlyRecord) => Record + ( + self: ReadonlyRecord, + that: ReadonlyRecord, + combine: (selfValue: A, thatValue: B) => C + ): Record +} = dual( + 3, + ( + self: ReadonlyRecord, + that: ReadonlyRecord, + combine: (selfValue: A, thatValue: B) => C + ): Record => { + if (isEmptyRecord(self)) { + return { ...that } as any + } + if (isEmptyRecord(that)) { + return { ...self } as any + } + const out: Record = empty() + for (const key of keys(self)) { + if (has(that, key as any)) { + out[key] = combine(self[key], that[key as unknown as K1]) + } else { + out[key] = self[key] + } + } + for (const key of keys(that)) { + if (!has(out, key)) { + out[key] = that[key] + } + } + return out + } +) + +/** + * Merge two records, retaining only the entries that exist in both records. + * + * @since 2.0.0 + */ +export const intersection: { + ( + that: ReadonlyRecord, + combine: (selfValue: A, thatValue: B) => C + ): (self: ReadonlyRecord) => Record, C> + ( + self: ReadonlyRecord, + that: ReadonlyRecord, + combine: (selfValue: A, thatValue: B) => C + ): Record, C> +} = dual( + 3, + ( + self: ReadonlyRecord, + that: ReadonlyRecord, + combine: (selfValue: A, thatValue: B) => C + ): Record, C> => { + const out: Record = empty() + if (isEmptyRecord(self) || isEmptyRecord(that)) { + return out + } + for (const key of keys(self)) { + if (has(that, key as any)) { + out[key] = combine(self[key], that[key as unknown as K1]) + } + } + return out + } +) + +/** + * Merge two records, preserving only the entries that are unique to each record. + * + * @since 2.0.0 + */ +export const difference: { + ( + that: ReadonlyRecord + ): (self: ReadonlyRecord) => Record + ( + self: ReadonlyRecord, + that: ReadonlyRecord + ): Record +} = dual(2, ( + self: ReadonlyRecord, + that: ReadonlyRecord +): Record => { + if (isEmptyRecord(self)) { + return { ...that } as any + } + if (isEmptyRecord(that)) { + return { ...self } as any + } + const out = > {} + for (const key of keys(self)) { + if (!has(that, key as any)) { + out[key] = self[key] + } + } + for (const key of keys(that)) { + if (!has(self, key as any)) { + out[key] = that[key] + } + } + return out +}) + +/** + * Create an `Equivalence` for records using the provided `Equivalence` for values. + * + * @category instances + * @since 2.0.0 + */ +export const getEquivalence = ( + equivalence: Equivalence +): Equivalence> => { + const is = isSubrecordBy(equivalence) + return (self, that) => is(self, that) && is(that, self) +} + +/** + * Create a non-empty record from a single element. + * + * @category constructors + * @since 2.0.0 + */ +export const singleton = (key: K, value: A): Record => ({ + [key]: value +} as any) + +/** + * Returns the first entry that satisfies the specified + * predicate, or `None` if no such entry exists. + * + * @example + * ```ts + * import { Record, Option } from "effect" + * + * const record = { a: 1, b: 2, c: 3 } + * const result = Record.findFirst(record, (value, key) => value > 1 && key !== "b") + * console.log(result) // Option.Some(["c", 3]) + * ``` + * + * @category elements + * @since 3.14.0 + */ +export const findFirst: { + ( + refinement: (value: NoInfer, key: NoInfer) => value is V2 + ): (self: ReadonlyRecord) => Option.Option<[K, V2]> + ( + predicate: (value: NoInfer, key: NoInfer) => boolean + ): (self: ReadonlyRecord) => Option.Option<[K, V]> + ( + self: ReadonlyRecord, + refinement: (value: NoInfer, key: NoInfer) => value is V2 + ): Option.Option<[K, V2]> + ( + self: ReadonlyRecord, + predicate: (value: NoInfer, key: NoInfer) => boolean + ): Option.Option<[K, V]> +} = dual( + 2, + (self: ReadonlyRecord, f: (value: V, key: K) => boolean) => { + const k = keys(self) + for (let i = 0; i < k.length; i++) { + const key = k[i] + if (f(self[key], key)) { + return Option.some([key, self[key]]) + } + } + return Option.none() + } +) diff --git a/repos/effect/packages/effect/src/RedBlackTree.ts b/repos/effect/packages/effect/src/RedBlackTree.ts new file mode 100644 index 0000000..41209e2 --- /dev/null +++ b/repos/effect/packages/effect/src/RedBlackTree.ts @@ -0,0 +1,421 @@ +/** + * @since 2.0.0 + */ +import type { Chunk } from "./Chunk.js" +import type { Equal } from "./Equal.js" +import type { Inspectable } from "./Inspectable.js" +import * as RBT from "./internal/redBlackTree.js" +import * as RBTI from "./internal/redBlackTree/iterator.js" +import type { Option } from "./Option.js" +import type { Order } from "./Order.js" +import type { Pipeable } from "./Pipeable.js" +import type * as Types from "./Types.js" + +const TypeId: unique symbol = RBT.RedBlackTreeTypeId as TypeId + +/** + * @since 2.0.0 + * @category symbol + */ +export type TypeId = typeof TypeId + +/** + * @since 2.0.0 + * @category constants + */ +export const Direction = RBTI.Direction + +/** + * A Red-Black Tree. + * + * @since 2.0.0 + * @category models + */ +export interface RedBlackTree extends Iterable<[Key, Value]>, Equal, Pipeable, Inspectable { + readonly [TypeId]: { + readonly _Key: Types.Invariant + readonly _Value: Types.Covariant + } +} + +/** + * @since 2.0.0 + */ +export declare namespace RedBlackTree { + /** + * @since 2.0.0 + */ + export type Direction = number & { + readonly Direction: unique symbol + } +} + +/** + * @since 2.0.0 + * @category refinements + */ +export const isRedBlackTree: { + (u: Iterable): u is RedBlackTree + (u: unknown): u is RedBlackTree +} = RBT.isRedBlackTree + +/** + * Creates an empty `RedBlackTree`. + * + * @since 2.0.0 + * @category constructors + */ +export const empty: (ord: Order) => RedBlackTree = RBT.empty + +/** + * Creates a new `RedBlackTree` from an iterable collection of key/value pairs. + * + * @since 2.0.0 + * @category constructors + */ +export const fromIterable: { + (ord: Order): (entries: Iterable) => RedBlackTree + (entries: Iterable, ord: Order): RedBlackTree +} = RBT.fromIterable + +/** + * Constructs a new `RedBlackTree` from the specified entries. + * + * @since 2.0.0 + * @category constructors + */ +export const make: ( + ord: Order +) => >( + ...entries: Entries +) => RedBlackTree = RBT.make + +/** + * Returns an iterator that points to the element at the specified index of the + * tree. + * + * **Note**: The iterator will run through elements in order. + * + * @since 2.0.0 + * @category traversing + */ +export const at: { + (index: number): (self: RedBlackTree) => Iterable<[K, V]> + (self: RedBlackTree, index: number): Iterable<[K, V]> +} = RBT.atForwards + +/** + * Returns an iterator that points to the element at the specified index of the + * tree. + * + * **Note**: The iterator will run through elements in reverse order. + * + * @since 2.0.0 + * @category traversing + */ +export const atReversed: { + (index: number): (self: RedBlackTree) => Iterable<[K, V]> + (self: RedBlackTree, index: number): Iterable<[K, V]> +} = RBT.atBackwards + +/** + * Finds all values in the tree associated with the specified key. + * + * @since 2.0.0 + * @category elements + */ +export const findAll: { + (key: K): (self: RedBlackTree) => Chunk + (self: RedBlackTree, key: K): Chunk +} = RBT.findAll + +/** + * Finds the first value in the tree associated with the specified key, if it exists. + * + * @category elements + * @since 2.0.0 + */ +export const findFirst: { + (key: K): (self: RedBlackTree) => Option + (self: RedBlackTree, key: K): Option +} = RBT.findFirst + +/** + * Returns the first entry in the tree, if it exists. + * + * @since 2.0.0 + * @category getters + */ +export const first: (self: RedBlackTree) => Option<[K, V]> = RBT.first + +/** + * Returns the element at the specified index within the tree or `None` if the + * specified index does not exist. + * + * @since 2.0.0 + * @category elements + */ +export const getAt: { + (index: number): (self: RedBlackTree) => Option<[K, V]> + (self: RedBlackTree, index: number): Option<[K, V]> +} = RBT.getAt + +/** + * Gets the `Order` that the `RedBlackTree` is using. + * + * @since 2.0.0 + * @category getters + */ +export const getOrder: (self: RedBlackTree) => Order = RBT.getOrder + +/** + * Returns an iterator that traverse entries in order with keys greater than the + * specified key. + * + * @since 2.0.0 + * @category traversing + */ +export const greaterThan: { + (key: K): (self: RedBlackTree) => Iterable<[K, V]> + (self: RedBlackTree, key: K): Iterable<[K, V]> +} = RBT.greaterThanForwards + +/** + * Returns an iterator that traverse entries in reverse order with keys greater + * than the specified key. + * + * @since 2.0.0 + * @category traversing + */ +export const greaterThanReversed: { + (key: K): (self: RedBlackTree) => Iterable<[K, V]> + (self: RedBlackTree, key: K): Iterable<[K, V]> +} = RBT.greaterThanBackwards + +/** + * Returns an iterator that traverse entries in order with keys greater than or + * equal to the specified key. + * + * @since 2.0.0 + * @category traversing + */ +export const greaterThanEqual: { + (key: K): (self: RedBlackTree) => Iterable<[K, V]> + (self: RedBlackTree, key: K): Iterable<[K, V]> +} = RBT.greaterThanEqualForwards + +/** + * Returns an iterator that traverse entries in reverse order with keys greater + * than or equal to the specified key. + * + * @since 2.0.0 + * @category traversing + */ +export const greaterThanEqualReversed: { + (key: K): (self: RedBlackTree) => Iterable<[K, V]> + (self: RedBlackTree, key: K): Iterable<[K, V]> +} = RBT.greaterThanEqualBackwards + +/** + * Finds the item with key, if it exists. + * + * @since 2.0.0 + * @category elements + */ +export const has: { + (key: K): (self: RedBlackTree) => boolean + (self: RedBlackTree, key: K): boolean +} = RBT.has + +/** + * Insert a new item into the tree. + * + * @since 2.0.0 + */ +export const insert: { + (key: K, value: V): (self: RedBlackTree) => RedBlackTree + (self: RedBlackTree, key: K, value: V): RedBlackTree +} = RBT.insert + +/** + * Get all the keys present in the tree in order. + * + * @since 2.0.0 + * @category getters + */ +export const keys: (self: RedBlackTree) => IterableIterator = RBT.keysForward + +/** + * Get all the keys present in the tree in reverse order. + * + * @since 2.0.0 + * @category getters + */ +export const keysReversed: (self: RedBlackTree) => IterableIterator = RBT.keysBackward + +/** + * Returns the last entry in the tree, if it exists. + * + * @since 2.0.0 + * @category getters + */ +export const last: (self: RedBlackTree) => Option<[K, V]> = RBT.last + +/** + * Returns an iterator that traverse entries in order with keys less than the + * specified key. + * + * @since 2.0.0 + * @category traversing + */ +export const lessThan: { + (key: K): (self: RedBlackTree) => Iterable<[K, V]> + (self: RedBlackTree, key: K): Iterable<[K, V]> +} = RBT.lessThanForwards + +/** + * Returns an iterator that traverse entries in reverse order with keys less + * than the specified key. + * + * @since 2.0.0 + * @category traversing + */ +export const lessThanReversed: { + (key: K): (self: RedBlackTree) => Iterable<[K, V]> + (self: RedBlackTree, key: K): Iterable<[K, V]> +} = RBT.lessThanBackwards + +/** + * Returns an iterator that traverse entries in order with keys less than or + * equal to the specified key. + * + * @since 2.0.0 + * @category traversing + */ +export const lessThanEqual: { + (key: K): (self: RedBlackTree) => Iterable<[K, V]> + (self: RedBlackTree, key: K): Iterable<[K, V]> +} = RBT.lessThanEqualForwards + +/** + * Returns an iterator that traverse entries in reverse order with keys less + * than or equal to the specified key. + * + * @since 2.0.0 + * @category traversing + */ +export const lessThanEqualReversed: { + (key: K): (self: RedBlackTree) => Iterable<[K, V]> + (self: RedBlackTree, key: K): Iterable<[K, V]> +} = RBT.lessThanEqualBackwards + +/** + * Execute the specified function for each node of the tree, in order. + * + * @since 2.0.0 + * @category traversing + */ +export const forEach: { + (f: (key: K, value: V) => void): (self: RedBlackTree) => void + (self: RedBlackTree, f: (key: K, value: V) => void): void +} = RBT.forEach + +/** + * Visit each node of the tree in order with key greater then or equal to max. + * + * @since 2.0.0 + * @category traversing + */ +export const forEachGreaterThanEqual: { + (min: K, f: (key: K, value: V) => void): (self: RedBlackTree) => void + (self: RedBlackTree, min: K, f: (key: K, value: V) => void): void +} = RBT.forEachGreaterThanEqual + +/** + * Visit each node of the tree in order with key lower then max. + * + * @since 2.0.0 + * @category traversing + */ +export const forEachLessThan: { + (max: K, f: (key: K, value: V) => void): (self: RedBlackTree) => void + (self: RedBlackTree, max: K, f: (key: K, value: V) => void): void +} = RBT.forEachLessThan + +/** + * Visit each node of the tree in order with key lower than max and greater + * than or equal to min. + * + * @since 2.0.0 + * @category traversing + */ +export const forEachBetween: { + ( + options: { + readonly min: K + readonly max: K + readonly body: (key: K, value: V) => void + } + ): (self: RedBlackTree) => void + ( + self: RedBlackTree, + options: { + readonly min: K + readonly max: K + readonly body: (key: K, value: V) => void + } + ): void +} = RBT.forEachBetween + +/** + * Reduce a state over the entries of the tree. + * + * @since 2.0.0 + * @category folding + */ +export const reduce: { + (zero: Z, f: (accumulator: Z, value: V, key: K) => Z): (self: RedBlackTree) => Z + (self: RedBlackTree, zero: Z, f: (accumulator: Z, value: V, key: K) => Z): Z +} = RBT.reduce + +/** + * Removes the entry with the specified key, if it exists. + * + * @since 2.0.0 + */ +export const removeFirst: { + (key: K): (self: RedBlackTree) => RedBlackTree + (self: RedBlackTree, key: K): RedBlackTree +} = RBT.removeFirst + +/** + * Traverse the tree in reverse order. + * + * @since 2.0.0 + * @category traversing + */ +export const reversed: (self: RedBlackTree) => Iterable<[K, V]> = RBT.reversed + +/** + * Returns the size of the tree. + * + * @since 2.0.0 + * @category getters + */ +export const size: (self: RedBlackTree) => number = RBT.size + +/** + * Get all values present in the tree in order. + * + * @since 2.0.0 + * @category getters + */ +export const values: (self: RedBlackTree) => IterableIterator = RBT.valuesForward + +/** + * Get all values present in the tree in reverse order. + * + * @since 2.0.0 + * @category getters + */ +export const valuesReversed: (self: RedBlackTree) => IterableIterator = RBT.valuesBackward diff --git a/repos/effect/packages/effect/src/Redacted.ts b/repos/effect/packages/effect/src/Redacted.ts new file mode 100644 index 0000000..0e9b7b8 --- /dev/null +++ b/repos/effect/packages/effect/src/Redacted.ts @@ -0,0 +1,144 @@ +/** + * The Redacted module provides functionality for handling sensitive information + * securely within your application. By using the `Redacted` data type, you can + * ensure that sensitive values are not accidentally exposed in logs or error + * messages. + * + * @since 3.3.0 + */ +import type * as Equal from "./Equal.js" +import * as Equivalence from "./Equivalence.js" +import * as redacted_ from "./internal/redacted.js" +import type { Pipeable } from "./Pipeable.js" +import type { Covariant } from "./Types.js" + +/** + * @since 3.3.0 + * @category symbols + */ +export const RedactedTypeId: unique symbol = redacted_.RedactedTypeId + +/** + * @since 3.3.0 + * @category symbols + */ +export type RedactedTypeId = typeof RedactedTypeId + +/** + * @since 3.3.0 + * @category models + */ +export interface Redacted extends Redacted.Variance, Equal.Equal, Pipeable { +} + +/** + * @since 3.3.0 + */ +export declare namespace Redacted { + /** + * @since 3.3.0 + * @category models + */ + export interface Variance { + readonly [RedactedTypeId]: { + readonly _A: Covariant + } + } + + /** + * @since 3.3.0 + * @category type-level + */ + export type Value> = [T] extends [Redacted] ? _A : never +} + +/** + * @since 3.3.0 + * @category refinements + */ +export const isRedacted: (u: unknown) => u is Redacted = redacted_.isRedacted + +/** + * This function creates a `Redacted` instance from a given value `A`, + * securely hiding its content. + * + * @example + * ```ts + * import { Redacted } from "effect" + * + * const API_KEY = Redacted.make("1234567890") + * ``` + * + * @since 3.3.0 + * @category constructors + */ +export const make: (value: A) => Redacted = redacted_.make + +/** + * Retrieves the original value from a `Redacted` instance. Use this function + * with caution, as it exposes the sensitive data. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { Redacted } from "effect" + * + * const API_KEY = Redacted.make("1234567890") + * + * assert.equal(Redacted.value(API_KEY), "1234567890") + * ``` + * + * @since 3.3.0 + * @category getters + */ +export const value: (self: Redacted) => A = redacted_.value + +/** + * Erases the underlying value of a `Redacted` instance, rendering it unusable. + * This function is intended to ensure that sensitive data does not remain in + * memory longer than necessary. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { Redacted } from "effect" + * + * const API_KEY = Redacted.make("1234567890") + * + * assert.equal(Redacted.value(API_KEY), "1234567890") + * + * Redacted.unsafeWipe(API_KEY) + * + * assert.throws(() => Redacted.value(API_KEY), new Error("Unable to get redacted value")) + * ``` + * + * @since 3.3.0 + * @category unsafe + */ +export const unsafeWipe: (self: Redacted) => boolean = redacted_.unsafeWipe + +/** + * Generates an equivalence relation for `Redacted` values based on an + * equivalence relation for the underlying values `A`. This function is useful + * for comparing `Redacted` instances without exposing their contents. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { Redacted, Equivalence } from "effect" + * + * const API_KEY1 = Redacted.make("1234567890") + * const API_KEY2 = Redacted.make("1-34567890") + * const API_KEY3 = Redacted.make("1234567890") + * + * const equivalence = Redacted.getEquivalence(Equivalence.string) + * + * assert.equal(equivalence(API_KEY1, API_KEY2), false) + * assert.equal(equivalence(API_KEY1, API_KEY3), true) + * ``` + * + * @category equivalence + * @since 3.3.0 + */ +export const getEquivalence = (isEquivalent: Equivalence.Equivalence): Equivalence.Equivalence> => + Equivalence.make((x, y) => isEquivalent(value(x), value(y))) diff --git a/repos/effect/packages/effect/src/Ref.ts b/repos/effect/packages/effect/src/Ref.ts new file mode 100644 index 0000000..0f63bae --- /dev/null +++ b/repos/effect/packages/effect/src/Ref.ts @@ -0,0 +1,180 @@ +/** + * @since 2.0.0 + */ +import type * as Effect from "./Effect.js" +import * as internal from "./internal/ref.js" +import type * as Option from "./Option.js" +import type * as Readable from "./Readable.js" +import type * as Types from "./Types.js" +import type * as Unify from "./Unify.js" + +/** + * @since 2.0.0 + * @category symbols + */ +export const RefTypeId: unique symbol = internal.RefTypeId + +/** + * @since 2.0.0 + * @category symbols + */ +export type RefTypeId = typeof RefTypeId + +/** + * @since 2.0.0 + * @category models + */ +export interface Ref extends Ref.Variance, Effect.Effect, Readable.Readable { + modify(f: (a: A) => readonly [B, A]): Effect.Effect + readonly [Unify.typeSymbol]?: unknown + readonly [Unify.unifySymbol]?: RefUnify + readonly [Unify.ignoreSymbol]?: RefUnifyIgnore +} + +/** + * @category models + * @since 3.8.0 + */ +export interface RefUnify extends Effect.EffectUnify { + Ref?: () => Extract> +} + +/** + * @category models + * @since 3.8.0 + */ +export interface RefUnifyIgnore extends Effect.EffectUnifyIgnore { + Effect?: true +} + +/** + * @since 2.0.0 + * @category models + */ +export declare namespace Ref { + /** + * @since 2.0.0 + */ + export interface Variance { + readonly [RefTypeId]: { + readonly _A: Types.Invariant + } + } +} + +/** + * @since 2.0.0 + * @category constructors + */ +export const make: (value: A) => Effect.Effect> = internal.make + +/** + * @since 2.0.0 + * @category getters + */ +export const get: (self: Ref) => Effect.Effect = internal.get + +/** + * @since 2.0.0 + * @category utils + */ +export const getAndSet: { + (value: A): (self: Ref) => Effect.Effect + (self: Ref, value: A): Effect.Effect +} = internal.getAndSet + +/** + * @since 2.0.0 + * @category utils + */ +export const getAndUpdate: { + (f: (a: A) => A): (self: Ref) => Effect.Effect + (self: Ref, f: (a: A) => A): Effect.Effect +} = internal.getAndUpdate + +/** + * @since 2.0.0 + * @category utils + */ +export const getAndUpdateSome: { + (pf: (a: A) => Option.Option): (self: Ref) => Effect.Effect + (self: Ref, pf: (a: A) => Option.Option): Effect.Effect +} = internal.getAndUpdateSome + +/** + * @since 2.0.0 + * @category utils + */ +export const modify: { + (f: (a: A) => readonly [B, A]): (self: Ref) => Effect.Effect + (self: Ref, f: (a: A) => readonly [B, A]): Effect.Effect +} = internal.modify + +/** + * @since 2.0.0 + * @category utils + */ +export const modifySome: { + (fallback: B, pf: (a: A) => Option.Option): (self: Ref) => Effect.Effect + (self: Ref, fallback: B, pf: (a: A) => Option.Option): Effect.Effect +} = internal.modifySome + +/** + * @since 2.0.0 + * @category utils + */ +export const set: { + (value: A): (self: Ref) => Effect.Effect + (self: Ref, value: A): Effect.Effect +} = internal.set + +/** + * @since 2.0.0 + * @category utils + */ +export const setAndGet: { + (value: A): (self: Ref) => Effect.Effect + (self: Ref, value: A): Effect.Effect +} = internal.setAndGet + +/** + * @since 2.0.0 + * @category utils + */ +export const update: { + (f: (a: A) => A): (self: Ref) => Effect.Effect + (self: Ref, f: (a: A) => A): Effect.Effect +} = internal.update + +/** + * @since 2.0.0 + * @category utils + */ +export const updateAndGet: { + (f: (a: A) => A): (self: Ref) => Effect.Effect + (self: Ref, f: (a: A) => A): Effect.Effect +} = internal.updateAndGet + +/** + * @since 2.0.0 + * @category utils + */ +export const updateSome: { + (f: (a: A) => Option.Option): (self: Ref) => Effect.Effect + (self: Ref, f: (a: A) => Option.Option): Effect.Effect +} = internal.updateSome + +/** + * @since 2.0.0 + * @category utils + */ +export const updateSomeAndGet: { + (pf: (a: A) => Option.Option): (self: Ref) => Effect.Effect + (self: Ref, pf: (a: A) => Option.Option): Effect.Effect +} = internal.updateSomeAndGet + +/** + * @since 2.0.0 + * @category unsafe + */ +export const unsafeMake: (value: A) => Ref = internal.unsafeMake diff --git a/repos/effect/packages/effect/src/RegExp.ts b/repos/effect/packages/effect/src/RegExp.ts new file mode 100644 index 0000000..9ee5a58 --- /dev/null +++ b/repos/effect/packages/effect/src/RegExp.ts @@ -0,0 +1,38 @@ +/** + * This module provides utility functions for working with RegExp in TypeScript. + * + * @since 2.0.0 + */ +import * as predicate from "./Predicate.js" + +/** + * Tests if a value is a `RegExp`. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { RegExp } from "effect" + * + * assert.deepStrictEqual(RegExp.isRegExp(/a/), true) + * assert.deepStrictEqual(RegExp.isRegExp("a"), false) + * ``` + * + * @category guards + * @since 3.9.0 + */ +export const isRegExp: (input: unknown) => input is RegExp = predicate.isRegExp + +/** + * Escapes special characters in a regular expression pattern. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { RegExp } from "effect" + * + * assert.deepStrictEqual(RegExp.escape("a*b"), "a\\*b") + * ``` + * + * @since 2.0.0 + */ +export const escape = (string: string): string => string.replace(/[/\\^$*+?.()|[\]{}]/g, "\\$&") diff --git a/repos/effect/packages/effect/src/Reloadable.ts b/repos/effect/packages/effect/src/Reloadable.ts new file mode 100644 index 0000000..f8ce5b4 --- /dev/null +++ b/repos/effect/packages/effect/src/Reloadable.ts @@ -0,0 +1,127 @@ +/** + * @since 2.0.0 + */ +import type * as Context from "./Context.js" +import type * as Effect from "./Effect.js" +import * as internal from "./internal/reloadable.js" +import type * as Layer from "./Layer.js" +import type * as Schedule from "./Schedule.js" +import type * as ScopedRef from "./ScopedRef.js" +import type * as Types from "./Types.js" + +/** + * @since 2.0.0 + * @category symbols + */ +export const ReloadableTypeId: unique symbol = internal.ReloadableTypeId + +/** + * @since 2.0.0 + * @category symbols + */ +export type ReloadableTypeId = typeof ReloadableTypeId + +/** + * A `Reloadable` is an implementation of some service that can be dynamically + * reloaded, or swapped out for another implementation on-the-fly. + * + * @since 2.0.0 + * @category models + */ +export interface Reloadable extends Reloadable.Variance { + /** + * @internal + */ + readonly scopedRef: ScopedRef.ScopedRef + /** + * @internal + */ + readonly reload: Effect.Effect +} + +/** + * @since 2.0.0 + */ +export declare namespace Reloadable { + /** + * @since 2.0.0 + * @category models + */ + export interface Variance { + readonly [ReloadableTypeId]: { + readonly _A: Types.Invariant + } + } +} + +/** + * Makes a new reloadable service from a layer that describes the construction + * of a static service. The service is automatically reloaded according to the + * provided schedule. + * + * @since 2.0.0 + * @category constructors + */ +export const auto: ( + tag: Context.Tag, + options: { readonly layer: Layer.Layer; readonly schedule: Schedule.Schedule } +) => Layer.Layer, E, R | In> = internal.auto + +/** + * Makes a new reloadable service from a layer that describes the construction + * of a static service. The service is automatically reloaded according to a + * schedule, which is extracted from the input to the layer. + * + * @since 2.0.0 + * @category constructors + */ +export const autoFromConfig: ( + tag: Context.Tag, + options: { + readonly layer: Layer.Layer + readonly scheduleFromConfig: (context: Context.Context) => Schedule.Schedule + } +) => Layer.Layer, E, R | In> = internal.autoFromConfig + +/** + * Retrieves the current version of the reloadable service. + * + * @since 2.0.0 + * @category getters + */ +export const get: (tag: Context.Tag) => Effect.Effect> = internal.get + +/** + * Makes a new reloadable service from a layer that describes the construction + * of a static service. + * + * @since 2.0.0 + * @category constructors + */ +export const manual: ( + tag: Context.Tag, + options: { readonly layer: Layer.Layer } +) => Layer.Layer, E, In> = internal.manual + +/** + * Reloads the specified service. + * + * @since 2.0.0 + * @category constructors + */ +export const reload: (tag: Context.Tag) => Effect.Effect> = internal.reload + +/** + * @since 2.0.0 + * @category context + */ +export const tag: (tag: Context.Tag) => Context.Tag, Reloadable> = internal.reloadableTag + +/** + * Forks the reload of the service in the background, ignoring any errors. + * + * @since 2.0.0 + * @category constructors + */ +export const reloadFork: (tag: Context.Tag) => Effect.Effect> = + internal.reloadFork diff --git a/repos/effect/packages/effect/src/Request.ts b/repos/effect/packages/effect/src/Request.ts new file mode 100644 index 0000000..081c277 --- /dev/null +++ b/repos/effect/packages/effect/src/Request.ts @@ -0,0 +1,347 @@ +/** + * @since 2.0.0 + */ +import type * as _Cache from "./Cache.js" +import type { Cause } from "./Cause.js" +import type { Deferred } from "./Deferred.js" +import type { DurationInput } from "./Duration.js" +import type * as Effect from "./Effect.js" +import type * as Exit from "./Exit.js" +import type { FiberId } from "./FiberId.js" +import * as RequestBlock_ from "./internal/blockedRequests.js" +import * as cache from "./internal/cache.js" +import * as core from "./internal/core.js" +import * as fiberRuntime from "./internal/fiberRuntime.js" +import * as internal from "./internal/request.js" +import type * as Option from "./Option.js" +import type * as Types from "./Types.js" + +/** + * @since 2.0.0 + * @category symbols + */ +export const RequestTypeId: unique symbol = internal.RequestTypeId + +/** + * @since 2.0.0 + * @category symbols + */ +export type RequestTypeId = typeof RequestTypeId + +/** + * A `Request` is a request from a data source for a value of type `A` + * that may fail with an `E`. + * + * @since 2.0.0 + * @category models + */ +export interface Request extends Request.Variance {} + +/** + * @since 2.0.0 + */ +export declare namespace Request { + /** + * @since 2.0.0 + * @category models + */ + export interface Variance { + readonly [RequestTypeId]: { + readonly _A: Types.Covariant + readonly _E: Types.Covariant + } + } + + /** + * @since 2.0.0 + * @category models + */ + export interface Constructor, T extends keyof R = never> { + (args: Omit, Request.Error>)>): R + } + + /** + * A utility type to extract the error type from a `Request`. + * + * @since 2.0.0 + * @category type-level + */ + export type Error> = [T] extends [Request] ? _E : never + + /** + * A utility type to extract the value type from a `Request`. + * + * @since 2.0.0 + * @category type-level + */ + export type Success> = [T] extends [Request] ? _A : never + + /** + * A utility type to extract the result type from a `Request`. + * + * @since 2.0.0 + * @category type-level + */ + export type Result> = T extends Request ? Exit.Exit : never + + /** + * A utility type to extract the optional result type from a `Request`. + * + * @since 2.0.0 + * @category type-level + */ + export type OptionalResult> = T extends Request + ? Exit.Exit, E> + : never +} + +/** + * Returns `true` if the specified value is a `Request`, `false` otherwise. + * + * @since 2.0.0 + * @category refinements + */ +export const isRequest: (u: unknown) => u is Request = internal.isRequest + +/** + * Constructs a new `Request`. + * + * @since 2.0.0 + * @category constructors + */ +export const of: >() => Request.Constructor = internal.of + +/** + * Constructs a new `Request`. + * + * @since 2.0.0 + * @category constructors + */ +export const tagged: & { _tag: string }>( + tag: R["_tag"] +) => Request.Constructor = internal.tagged + +/** + * Provides a constructor for a Request Class. + * + * @example + * ```ts + * import { Request } from "effect" + * + * type Success = string + * type Error = never + * + * class MyRequest extends Request.Class {} + * ``` + * + * @since 2.0.0 + * @category constructors + */ +export const Class: new>( + args: Types.Equals>, {}> extends true ? void + : { readonly [P in keyof A as P extends keyof Request ? never : P]: A[P] } +) => Request & Readonly = internal.Class as any + +/** + * Provides a Tagged constructor for a Request Class. + * + * @example + * ```ts + * import { Request } from "effect" + * + * type Success = string + * type Error = never + * + * class MyRequest extends Request.TaggedClass("MyRequest") {} + * ``` + * + * @since 2.0.0 + * @category constructors + */ +export const TaggedClass: ( + tag: Tag +) => new>( + args: Types.Equals>, {}> extends true ? void + : { readonly [P in keyof A as P extends "_tag" | keyof Request ? never : P]: A[P] } +) => Request & Readonly & { readonly _tag: Tag } = internal.TaggedClass as any + +/** + * Complete a `Request` with the specified result. + * + * @since 2.0.0 + * @category request completion + */ +export const complete: { + >(result: Request.Result): (self: A) => Effect.Effect + >(self: A, result: Request.Result): Effect.Effect +} = internal.complete + +/** + * Interrupts the child effect when requests are no longer needed + * + * @since 2.0.0 + * @category request completion + */ +export const interruptWhenPossible: { + (all: Iterable>): (self: Effect.Effect) => Effect.Effect + (self: Effect.Effect, all: Iterable>): Effect.Effect +} = fiberRuntime.interruptWhenPossible + +/** + * Complete a `Request` with the specified effectful computation, failing the + * request with the error from the effect workflow if it fails, and completing + * the request with the value of the effect workflow if it succeeds. + * + * @since 2.0.0 + * @category request completion + */ +export const completeEffect: { + , R>( + effect: Effect.Effect, Request.Error, R> + ): (self: A) => Effect.Effect + , R>( + self: A, + effect: Effect.Effect, Request.Error, R> + ): Effect.Effect +} = internal.completeEffect + +/** + * Complete a `Request` with the specified error. + * + * @since 2.0.0 + * @category request completion + */ +export const fail: { + >(error: Request.Error): (self: A) => Effect.Effect + >(self: A, error: Request.Error): Effect.Effect +} = internal.fail + +/** + * Complete a `Request` with the specified cause. + * + * @since 2.0.0 + * @category request completion + */ +export const failCause: { + >(cause: Cause>): (self: A) => Effect.Effect + >(self: A, cause: Cause>): Effect.Effect +} = internal.failCause + +/** + * Complete a `Request` with the specified value. + * + * @since 2.0.0 + * @category request completion + */ +export const succeed: { + >(value: Request.Success): (self: A) => Effect.Effect + >(self: A, value: Request.Success): Effect.Effect +} = internal.succeed + +/** + * @category models + * @since 2.0.0 + */ +export interface Listeners { + readonly count: number + readonly observers: Set<(count: number) => void> + interrupted: boolean + addObserver(f: (count: number) => void): void + removeObserver(f: (count: number) => void): void + increment(): void + decrement(): void +} + +/** + * @category models + * @since 2.0.0 + */ +export interface Cache extends + _Cache.ConsumerCache, { + listeners: Listeners + handle: Deferred + }> +{} + +/** + * @since 2.0.0 + * @category models + */ +export const makeCache = ( + options: { + readonly capacity: number + readonly timeToLive: DurationInput + } +): Effect.Effect => + cache.make({ + ...options, + lookup: () => + core.map(core.deferredMake(), (handle) => ({ listeners: new internal.Listeners(), handle })) + }) + +/** + * @since 2.0.0 + * @category symbols + */ +export const EntryTypeId: unique symbol = Symbol.for("effect/RequestBlock.Entry") + +/** + * @since 2.0.0 + * @category symbols + */ +export type EntryTypeId = typeof EntryTypeId + +/** + * A `Entry` keeps track of a request of type `A` along with a + * `Ref` containing the result of the request, existentially hiding the result + * type. This is used internally by the library to support data sources that + * return different result types for different requests while guaranteeing that + * results will be of the type requested. + * + * @since 2.0.0 + * @category models + */ +export interface Entry extends Entry.Variance { + readonly request: R + readonly result: Deferred< + [R] extends [Request] ? _A : never, + [R] extends [Request] ? _E : never + > + readonly listeners: Listeners + readonly ownerId: FiberId + readonly state: { + completed: boolean + } +} + +/** + * @since 2.0.0 + * @category models + */ +export declare namespace Entry { + /** + * @since 2.0.0 + * @category models + */ + export interface Variance { + readonly [EntryTypeId]: { + readonly _R: Types.Covariant + } + } +} + +/** + * @since 2.0.0 + * @category guards + */ +export const isEntry = RequestBlock_.isEntry + +/** + * @since 2.0.0 + * @category constructors + */ +export const makeEntry = RequestBlock_.makeEntry diff --git a/repos/effect/packages/effect/src/RequestBlock.ts b/repos/effect/packages/effect/src/RequestBlock.ts new file mode 100644 index 0000000..9077100 --- /dev/null +++ b/repos/effect/packages/effect/src/RequestBlock.ts @@ -0,0 +1,118 @@ +/** + * @since 2.0.0 + */ +import * as RequestBlock_ from "./internal/blockedRequests.js" +import type * as Request from "./Request.js" +import type * as RequestResolver from "./RequestResolver.js" + +/** + * `RequestBlock` captures a collection of blocked requests as a data + * structure. By doing this the library is able to preserve information about + * which requests must be performed sequentially and which can be performed in + * parallel, allowing for maximum possible batching and pipelining while + * preserving ordering guarantees. + * + * @since 2.0.0 + * @category models + */ +export type RequestBlock = Empty | Par | Seq | Single + +/** + * @since 2.0.0 + * @category models + */ +export declare namespace RequestBlock { + /** + * @since 2.0.0 + * @category models + */ + export interface Reducer { + emptyCase(): Z + parCase(left: Z, right: Z): Z + singleCase( + dataSource: RequestResolver.RequestResolver, + blockedRequest: Request.Entry + ): Z + seqCase(left: Z, right: Z): Z + } +} + +/** + * @since 2.0.0 + * @category models + */ +export interface Empty { + readonly _tag: "Empty" +} + +/** + * @since 2.0.0 + * @category models + */ +export interface Par { + readonly _tag: "Par" + readonly left: RequestBlock + readonly right: RequestBlock +} + +/** + * @since 2.0.0 + * @category models + */ +export interface Seq { + readonly _tag: "Seq" + readonly left: RequestBlock + readonly right: RequestBlock +} + +/** + * @since 2.0.0 + * @category models + */ +export interface Single { + readonly _tag: "Single" + readonly dataSource: RequestResolver.RequestResolver + readonly blockedRequest: Request.Entry +} + +/** + * @since 2.0.0 + * @category constructors + */ +export const single: ( + dataSource: RequestResolver.RequestResolver, + blockedRequest: Request.Entry +) => RequestBlock = RequestBlock_.single + +/** + * @since 2.0.0 + * @category constructors + */ +export const empty: RequestBlock = RequestBlock_.empty + +/** + * @since 2.0.0 + * @category constructors + */ +export const mapRequestResolvers: ( + self: RequestBlock, + f: (dataSource: RequestResolver.RequestResolver) => RequestResolver.RequestResolver +) => RequestBlock = RequestBlock_.mapRequestResolvers + +/** + * @since 2.0.0 + * @category constructors + */ +export const parallel: (self: RequestBlock, that: RequestBlock) => RequestBlock = RequestBlock_.par + +/** + * @since 2.0.0 + * @category constructors + */ +export const reduce: (self: RequestBlock, reducer: RequestBlock.Reducer) => Z = RequestBlock_.reduce + +/** + * @since 2.0.0 + * @category constructors + */ +export const sequential: (self: RequestBlock, that: RequestBlock) => RequestBlock = RequestBlock_.seq diff --git a/repos/effect/packages/effect/src/RequestResolver.ts b/repos/effect/packages/effect/src/RequestResolver.ts new file mode 100644 index 0000000..7b36f6a --- /dev/null +++ b/repos/effect/packages/effect/src/RequestResolver.ts @@ -0,0 +1,366 @@ +/** + * @since 2.0.0 + */ + +import type { NonEmptyArray } from "./Array.js" +import * as Context from "./Context.js" +import * as Effect from "./Effect.js" +import type * as Either from "./Either.js" +import type * as Equal from "./Equal.js" +import type { FiberRef } from "./FiberRef.js" +import * as core from "./internal/core.js" +import * as internal from "./internal/dataSource.js" +import type { Pipeable } from "./Pipeable.js" +import type * as Request from "./Request.js" +import type * as Types from "./Types.js" + +/** + * @since 2.0.0 + * @category symbols + */ +export const RequestResolverTypeId: unique symbol = core.RequestResolverTypeId + +/** + * @since 2.0.0 + * @category symbols + */ +export type RequestResolverTypeId = typeof RequestResolverTypeId + +/** + * The `RequestResolver` interface requires an environment `R` and handles + * the execution of requests of type `A`. + * + * Implementations must provide a `runAll` method, which processes a collection + * of requests and produces an effect that fulfills these requests. Requests are + * organized into a `Array>`, where the outer `Array` groups requests + * into batches that are executed sequentially, and each inner `Array` contains + * requests that can be executed in parallel. This structure allows + * implementations to analyze all incoming requests collectively and optimize + * query execution accordingly. + * + * Implementations are typically specialized for a subtype of `Request`. + * However, they are not strictly limited to these subtypes as long as they can + * map any given request type to `Request`. Implementations should inspect + * the collection of requests to identify the needed information and execute the + * corresponding queries. It is imperative that implementations resolve all the + * requests they receive. Failing to do so will lead to a `QueryFailure` error + * during query execution. + * + * @since 2.0.0 + * @category models + */ +export interface RequestResolver extends RequestResolver.Variance, Equal.Equal, Pipeable { + /** + * Execute a collection of requests. The outer `Array` represents batches + * of requests that must be performed sequentially. The inner `Array` + * represents a batch of requests that can be performed in parallel. + */ + runAll(requests: Array>>): Effect.Effect + + /** + * Identify the data source using the specific identifier + */ + identified(...identifiers: Array): RequestResolver +} + +/** + * @since 2.0.0 + */ +export declare namespace RequestResolver { + /** + * @since 2.0.0 + * @category models + */ + export interface Variance { + readonly [RequestResolverTypeId]: { + readonly _A: Types.Contravariant + readonly _R: Types.Covariant + } + } +} + +/** + * @since 2.0.0 + * @category utils + */ +export const contextFromEffect = >(self: RequestResolver) => + Effect.contextWith((_: Context.Context) => provideContext(self, _)) + +/** + * @since 2.0.0 + * @category utils + */ +export const contextFromServices = + >>(...services: Services) => + >( + self: RequestResolver + ): Effect.Effect< + RequestResolver }[number]>>, + never, + { [k in keyof Services]: Effect.Effect.Context }[number] + > => Effect.contextWith((_) => provideContext(self as any, Context.pick(...services)(_ as any))) + +/** + * Returns `true` if the specified value is a `RequestResolver`, `false` otherwise. + * + * @since 2.0.0 + * @category refinements + */ +export const isRequestResolver: (u: unknown) => u is RequestResolver = core.isRequestResolver + +/** + * Constructs a data source with the specified identifier and method to run + * requests. + * + * @since 2.0.0 + * @category constructors + */ +export const make: ( + runAll: (requests: Array>) => Effect.Effect +) => RequestResolver = internal.make + +/** + * Constructs a data source with the specified identifier and method to run + * requests. + * + * @since 2.0.0 + * @category constructors + */ +export const makeWithEntry: ( + runAll: (requests: Array>>) => Effect.Effect +) => RequestResolver = internal.makeWithEntry + +/** + * Constructs a data source from a function taking a collection of requests. + * + * @since 2.0.0 + * @category constructors + */ +export const makeBatched: , R>( + run: (requests: NonEmptyArray) => Effect.Effect +) => RequestResolver = internal.makeBatched + +/** + * A data source aspect that executes requests between two effects, `before` + * and `after`, where the result of `before` can be used by `after`. + * + * @since 2.0.0 + * @category combinators + */ +export const around: { + ( + before: Effect.Effect, + after: (a: A2) => Effect.Effect + ): (self: RequestResolver) => RequestResolver + ( + self: RequestResolver, + before: Effect.Effect, + after: (a: A2) => Effect.Effect + ): RequestResolver +} = internal.around + +/** + * A data source aspect that executes requests between two effects, `before` + * and `after`, where the result of `before` can be used by `after`. + * + * The `before` and `after` effects are provided with the requests being executed. + * + * @since 2.0.0 + * @category combinators + * @example + * ```ts + * import { Effect, Request, RequestResolver } from "effect" + * + * interface GetUserById extends Request.Request { + * readonly id: number + * } + * + * const resolver = RequestResolver.fromFunction( + * (request: GetUserById) => ({ id: request.id, name: "John" }) + * ) + * + * RequestResolver.aroundRequests( + * resolver, + * (requests) => Effect.log(`got ${requests.length} requests`), + * (requests, _) => Effect.log(`finised running ${requests.length} requests`) + * ) + * ``` + */ +export const aroundRequests: { + ( + before: (requests: ReadonlyArray>) => Effect.Effect, + after: (requests: ReadonlyArray>, _: A2) => Effect.Effect + ): (self: RequestResolver) => RequestResolver + ( + self: RequestResolver, + before: (requests: ReadonlyArray>) => Effect.Effect, + after: (requests: ReadonlyArray>, _: A2) => Effect.Effect + ): RequestResolver +} = internal.aroundRequests + +/** + * Returns a data source that executes at most `n` requests in parallel. + * + * @since 2.0.0 + * @category combinators + */ +export const batchN: { + (n: number): (self: RequestResolver) => RequestResolver + (self: RequestResolver, n: number): RequestResolver +} = internal.batchN + +/** + * Provides this data source with part of its required context. + * + * @since 2.0.0 + * @category context + */ +export const mapInputContext: { + ( + f: (context: Context.Context) => Context.Context + ): >(self: RequestResolver) => RequestResolver + , R0>( + self: RequestResolver, + f: (context: Context.Context) => Context.Context + ): RequestResolver +} = internal.mapInputContext + +/** + * Returns a new data source that executes requests of type `C` using the + * specified function to transform `C` requests into requests that either this + * data source or that data source can execute. + * + * @since 2.0.0 + * @category combinators + */ +export const eitherWith: { + , R2, B extends Request.Request, C extends Request.Request>( + that: RequestResolver, + f: (_: Request.Entry) => Either.Either, Request.Entry> + ): (self: RequestResolver) => RequestResolver + < + R, + A extends Request.Request, + R2, + B extends Request.Request, + C extends Request.Request + >( + self: RequestResolver, + that: RequestResolver, + f: (_: Request.Entry) => Either.Either, Request.Entry> + ): RequestResolver +} = internal.eitherWith + +/** + * Constructs a data source from a pure function. + * + * @since 2.0.0 + * @category constructors + */ +export const fromFunction: >( + f: (request: A) => Request.Request.Success +) => RequestResolver = internal.fromFunction + +/** + * Constructs a data source from a pure function that takes a list of requests + * and returns a list of results of the same size. Each item in the result + * list must correspond to the item at the same index in the request list. + * + * @since 2.0.0 + * @category constructors + */ +export const fromFunctionBatched: >( + f: (chunk: NonEmptyArray) => Iterable> +) => RequestResolver = internal.fromFunctionBatched + +/** + * Constructs a data source from an effectual function. + * + * @since 2.0.0 + * @category constructors + */ +export const fromEffect: >( + f: (a: A) => Effect.Effect, Request.Request.Error, R> +) => RequestResolver = internal.fromEffect + +/** + * Constructs a data source from a list of tags paired to functions, that takes + * a list of requests and returns a list of results of the same size. Each item + * in the result list must correspond to the item at the same index in the + * request list. + * + * @since 2.0.0 + * @category constructors + */ +export const fromEffectTagged: & { readonly _tag: string }>() => < + Fns extends { + readonly [Tag in A["_tag"]]: [Extract] extends [infer Req] + ? Req extends Request.Request + ? (requests: Array) => Effect.Effect, ReqE, any> + : never + : never + } +>( + fns: Fns +) => RequestResolver extends Effect.Effect ? R : never> = + internal.fromEffectTagged + +/** + * A data source that never executes requests. + * + * @since 2.0.0 + * @category constructors + */ +export const never: RequestResolver = internal.never + +/** + * Provides this data source with its required context. + * + * @since 2.0.0 + * @category context + */ +export const provideContext: { + ( + context: Context.Context + ): >(self: RequestResolver) => RequestResolver + >( + self: RequestResolver, + context: Context.Context + ): RequestResolver +} = internal.provideContext + +/** + * Returns a new data source that executes requests by sending them to this + * data source and that data source, returning the results from the first data + * source to complete and safely interrupting the loser. + * + * @since 2.0.0 + * @category combinators + */ +export const race: { + , R2>( + that: RequestResolver + ): , R>(self: RequestResolver) => RequestResolver + , R, A2 extends Request.Request, R2>( + self: RequestResolver, + that: RequestResolver + ): RequestResolver +} = internal.race + +/** + * Returns a new data source with a localized FiberRef + * + * @since 2.0.0 + * @category combinators + */ +export const locally: { + ( + self: FiberRef, + value: A + ): >(use: RequestResolver) => RequestResolver + , A>( + use: RequestResolver, + self: FiberRef, + value: A + ): RequestResolver +} = core.resolverLocally diff --git a/repos/effect/packages/effect/src/Resource.ts b/repos/effect/packages/effect/src/Resource.ts new file mode 100644 index 0000000..1f9713e --- /dev/null +++ b/repos/effect/packages/effect/src/Resource.ts @@ -0,0 +1,119 @@ +/** + * @since 2.0.0 + */ +import type * as Effect from "./Effect.js" +import type * as Exit from "./Exit.js" +import * as internal from "./internal/resource.js" +import type * as Schedule from "./Schedule.js" +import type * as Scope from "./Scope.js" +import type * as ScopedRef from "./ScopedRef.js" +import type * as Types from "./Types.js" +import type * as Unify from "./Unify.js" + +/** + * @since 2.0.0 + * @category symbols + */ +export const ResourceTypeId: unique symbol = internal.ResourceTypeId + +/** + * @since 2.0.0 + * @category symbols + */ +export type ResourceTypeId = typeof ResourceTypeId + +/** + * A `Resource` is a possibly resourceful value that is loaded into memory, and + * which can be refreshed either manually or automatically. + * + * @since 2.0.0 + * @category models + */ +export interface Resource extends Effect.Effect, Resource.Variance { + /** @internal */ + readonly scopedRef: ScopedRef.ScopedRef> + /** @internal */ + readonly acquire: Effect.Effect + + readonly [Unify.typeSymbol]?: unknown + readonly [Unify.unifySymbol]?: ResourceUnify + readonly [Unify.ignoreSymbol]?: ResourceUnifyIgnore +} + +/** + * @category models + * @since 3.9.0 + */ +export interface ResourceUnify extends Effect.EffectUnify { + Resource?: () => Extract> +} + +/** + * @category models + * @since 3.9.0 + */ +export interface ResourceUnifyIgnore extends Effect.EffectUnifyIgnore { + Effect?: true +} + +/** + * @since 2.0.0 + */ +export declare namespace Resource { + /** + * @since 2.0.0 + * @category models + */ + export interface Variance { + readonly [ResourceTypeId]: { + _A: Types.Invariant + _E: Types.Invariant + } + } +} + +/** + * Creates a new `Resource` value that is automatically refreshed according to + * the specified policy. Note that error retrying is not performed + * automatically, so if you want to retry on errors, you should first apply + * retry policies to the acquisition effect before passing it to this + * constructor. + * + * @since 2.0.0 + * @category constructors + */ +export const auto: ( + acquire: Effect.Effect, + policy: Schedule.Schedule +) => Effect.Effect, never, R | R2 | Scope.Scope> = internal.auto + +/** + * Retrieves the current value stored in the cache. + * + * @since 2.0.0 + * @category getters + */ +export const get: (self: Resource) => Effect.Effect = internal.get + +/** + * Creates a new `Resource` value that must be manually refreshed by calling + * the refresh method. Note that error retrying is not performed + * automatically, so if you want to retry on errors, you should first apply + * retry policies to the acquisition effect before passing it to this + * constructor. + * + * @since 2.0.0 + * @category constructors + */ +export const manual: ( + acquire: Effect.Effect +) => Effect.Effect, never, Scope.Scope | R> = internal.manual + +/** + * Refreshes the cache. This method will not return until either the refresh + * is successful, or the refresh operation fails. + * + * @since 2.0.0 + * @category utils + */ +export const refresh: (self: Resource) => Effect.Effect = internal.refresh diff --git a/repos/effect/packages/effect/src/Runtime.ts b/repos/effect/packages/effect/src/Runtime.ts new file mode 100644 index 0000000..58cfa71 --- /dev/null +++ b/repos/effect/packages/effect/src/Runtime.ts @@ -0,0 +1,383 @@ +/** + * @since 2.0.0 + */ +import type { Cause } from "./Cause.js" +import type * as Context from "./Context.js" +import type * as Effect from "./Effect.js" +import type * as Exit from "./Exit.js" +import type * as Fiber from "./Fiber.js" +import type * as FiberId from "./FiberId.js" +import type * as FiberRef from "./FiberRef.js" +import type * as FiberRefs from "./FiberRefs.js" +import type { Inspectable } from "./Inspectable.js" +import * as internal from "./internal/runtime.js" +import type { Pipeable } from "./Pipeable.js" +import type * as RuntimeFlags from "./RuntimeFlags.js" +import type { Scheduler } from "./Scheduler.js" +import type { Scope } from "./Scope.js" + +/** + * @since 2.0.0 + * @category models + */ +export interface AsyncFiberException { + readonly _tag: "AsyncFiberException" + readonly fiber: Fiber.RuntimeFiber +} + +/** + * @since 2.0.0 + * @category models + */ +export interface Cancel { + (fiberId?: FiberId.FiberId, options?: RunCallbackOptions | undefined): void +} + +/** + * @since 2.0.0 + * @category models + */ +export interface Runtime extends Pipeable { + /** + * The context used as initial for forks + */ + readonly context: Context.Context + /** + * The runtime flags used as initial for forks + */ + readonly runtimeFlags: RuntimeFlags.RuntimeFlags + /** + * The fiber references used as initial for forks + */ + readonly fiberRefs: FiberRefs.FiberRefs +} + +/** + * @since 3.12.0 + */ +export declare namespace Runtime { + /** + * @since 3.12.0 + * @category Type Extractors + */ + export type Context> = [T] extends [Runtime] ? R : never +} + +/** + * @since 2.0.0 + * @category models + */ +export interface RunForkOptions { + readonly scheduler?: Scheduler | undefined + readonly updateRefs?: ((refs: FiberRefs.FiberRefs, fiberId: FiberId.Runtime) => FiberRefs.FiberRefs) | undefined + readonly immediate?: boolean + readonly scope?: Scope +} + +/** + * Executes the effect using the provided Scheduler or using the global + * Scheduler if not provided + * + * @since 2.0.0 + * @category execution + */ +export const runFork: { + ( + runtime: Runtime + ): (effect: Effect.Effect, options?: RunForkOptions | undefined) => Fiber.RuntimeFiber + ( + runtime: Runtime, + effect: Effect.Effect, + options?: RunForkOptions | undefined + ): Fiber.RuntimeFiber +} = internal.unsafeFork + +/** + * Executes the effect synchronously returning the exit. + * + * This method is effectful and should only be invoked at the edges of your + * program. + * + * @since 2.0.0 + * @category execution + */ +export const runSyncExit: { + (runtime: Runtime, effect: Effect.Effect): Exit.Exit + (runtime: Runtime): (effect: Effect.Effect) => Exit.Exit +} = internal.unsafeRunSyncExit + +/** + * Executes the effect synchronously throwing in case of errors or async boundaries. + * + * This method is effectful and should only be invoked at the edges of your + * program. + * + * @since 2.0.0 + * @category execution + */ +export const runSync: { + (runtime: Runtime, effect: Effect.Effect): A + (runtime: Runtime): (effect: Effect.Effect) => A +} = internal.unsafeRunSync + +/** + * @since 2.0.0 + * @category models + */ +export interface RunCallbackOptions extends RunForkOptions { + readonly onExit?: ((exit: Exit.Exit) => void) | undefined +} + +/** + * Executes the effect asynchronously, eventually passing the exit value to + * the specified callback. + * + * This method is effectful and should only be invoked at the edges of your + * program. + * + * @since 2.0.0 + * @category execution + */ +export const runCallback: { + ( + runtime: Runtime + ): ( + effect: Effect.Effect, + options?: RunCallbackOptions | undefined + ) => (fiberId?: FiberId.FiberId, options?: RunCallbackOptions | undefined) => void + ( + runtime: Runtime, + effect: Effect.Effect, + options?: RunCallbackOptions | undefined + ): (fiberId?: FiberId.FiberId, options?: RunCallbackOptions | undefined) => void +} = internal.unsafeRunCallback + +/** + * Runs the `Effect`, returning a JavaScript `Promise` that will be resolved + * with the value of the effect once the effect has been executed, or will be + * rejected with the first error or exception throw by the effect. + * + * This method is effectful and should only be used at the edges of your + * program. + * + * @since 2.0.0 + * @category execution + */ +export const runPromise: { + ( + runtime: Runtime + ): (effect: Effect.Effect, options?: { readonly signal?: AbortSignal } | undefined) => Promise + ( + runtime: Runtime, + effect: Effect.Effect, + options?: { readonly signal?: AbortSignal } | undefined + ): Promise +} = internal.unsafeRunPromise + +/** + * Runs the `Effect`, returning a JavaScript `Promise` that will be resolved + * with the `Exit` state of the effect once the effect has been executed. + * + * This method is effectful and should only be used at the edges of your + * program. + * + * @since 2.0.0 + * @category execution + */ +export const runPromiseExit: { + ( + runtime: Runtime + ): ( + effect: Effect.Effect, + options?: { readonly signal?: AbortSignal } | undefined + ) => Promise> + ( + runtime: Runtime, + effect: Effect.Effect, + options?: { readonly signal?: AbortSignal } | undefined + ): Promise> +} = internal.unsafeRunPromiseExit + +/** + * @since 2.0.0 + * @category constructors + */ +export const defaultRuntime: Runtime = internal.defaultRuntime + +/** + * @since 2.0.0 + * @category constructors + */ +export const defaultRuntimeFlags: RuntimeFlags.RuntimeFlags = internal.defaultRuntimeFlags + +/** + * @since 2.0.0 + * @category constructors + */ +export const make: ( + options: { + readonly context: Context.Context + readonly runtimeFlags: RuntimeFlags.RuntimeFlags + readonly fiberRefs: FiberRefs.FiberRefs + } +) => Runtime = internal.make + +/** + * @since 2.0.0 + * @category symbols + */ +export const FiberFailureId = Symbol.for("effect/Runtime/FiberFailure") +/** + * @since 2.0.0 + * @category symbols + */ +export type FiberFailureId = typeof FiberFailureId + +/** + * @since 2.0.0 + * @category symbols + */ +export const FiberFailureCauseId: unique symbol = internal.FiberFailureCauseId + +/** + * @since 2.0.0 + * @category exports + */ +export type FiberFailureCauseId = typeof FiberFailureCauseId + +/** + * @since 2.0.0 + * @category models + */ +export interface FiberFailure extends Error, Inspectable { + readonly [FiberFailureId]: FiberFailureId + readonly [FiberFailureCauseId]: Cause +} + +/** + * @since 2.0.0 + * @category guards + */ +export const isAsyncFiberException: (u: unknown) => u is AsyncFiberException = + internal.isAsyncFiberException + +/** + * @since 2.0.0 + * @category guards + */ +export const isFiberFailure: (u: unknown) => u is FiberFailure = internal.isFiberFailure + +/** + * @since 2.0.0 + * @category constructors + */ +export const makeFiberFailure: (cause: Cause) => FiberFailure = internal.fiberFailure + +/** + * @since 2.0.0 + * @category runtime flags + */ +export const updateRuntimeFlags: { + (f: (flags: RuntimeFlags.RuntimeFlags) => RuntimeFlags.RuntimeFlags): (self: Runtime) => Runtime + (self: Runtime, f: (flags: RuntimeFlags.RuntimeFlags) => RuntimeFlags.RuntimeFlags): Runtime +} = internal.updateRuntimeFlags + +/** + * @since 2.0.0 + * @category runtime flags + */ +export const enableRuntimeFlag: { + (flag: RuntimeFlags.RuntimeFlag): (self: Runtime) => Runtime + (self: Runtime, flag: RuntimeFlags.RuntimeFlag): Runtime +} = internal.enableRuntimeFlag + +/** + * @since 2.0.0 + * @category runtime flags + */ +export const disableRuntimeFlag: { + (flag: RuntimeFlags.RuntimeFlag): (self: Runtime) => Runtime + (self: Runtime, flag: RuntimeFlags.RuntimeFlag): Runtime +} = internal.disableRuntimeFlag + +/** + * @since 2.0.0 + * @category context + */ +export const updateContext: { + (f: (context: Context.Context) => Context.Context): (self: Runtime) => Runtime + (self: Runtime, f: (context: Context.Context) => Context.Context): Runtime +} = internal.updateContext + +/** + * @since 2.0.0 + * @category context + * @example + * ```ts + * import { Context, Runtime } from "effect" + * + * class Name extends Context.Tag("Name")() {} + * + * const runtime: Runtime.Runtime = Runtime.defaultRuntime.pipe( + * Runtime.provideService(Name, "John") + * ) + * ``` + */ +export const provideService: { + (tag: Context.Tag, service: S): (self: Runtime) => Runtime + (self: Runtime, tag: Context.Tag, service: S): Runtime +} = internal.provideService + +/** + * @since 2.0.0 + * @category fiber refs + */ +export const updateFiberRefs: { + (f: (fiberRefs: FiberRefs.FiberRefs) => FiberRefs.FiberRefs): (self: Runtime) => Runtime + (self: Runtime, f: (fiberRefs: FiberRefs.FiberRefs) => FiberRefs.FiberRefs): Runtime +} = internal.updateFiberRefs + +/** + * @since 2.0.0 + * @category fiber refs + * @example + * ```ts + * import { Effect, FiberRef, Runtime } from "effect" + * + * const ref = FiberRef.unsafeMake(0) + * + * const updatedRuntime = Runtime.defaultRuntime.pipe( + * Runtime.setFiberRef(ref, 1) + * ) + * + * // returns 1 + * const result = Runtime.runSync(updatedRuntime)(FiberRef.get(ref)) + * ``` + */ +export const setFiberRef: { + (fiberRef: FiberRef.FiberRef, value: A): (self: Runtime) => Runtime + (self: Runtime, fiberRef: FiberRef.FiberRef, value: A): Runtime +} = internal.setFiberRef + +/** + * @since 2.0.0 + * @category fiber refs + * @example + * ```ts + * import { Effect, FiberRef, Runtime } from "effect" + * + * const ref = FiberRef.unsafeMake(0) + * + * const updatedRuntime = Runtime.defaultRuntime.pipe( + * Runtime.setFiberRef(ref, 1), + * Runtime.deleteFiberRef(ref) + * ) + * + * // returns 0 + * const result = Runtime.runSync(updatedRuntime)(FiberRef.get(ref)) + * ``` + */ +export const deleteFiberRef: { + (fiberRef: FiberRef.FiberRef): (self: Runtime) => Runtime + (self: Runtime, fiberRef: FiberRef.FiberRef): Runtime +} = internal.deleteFiberRef diff --git a/repos/effect/packages/effect/src/RuntimeFlags.ts b/repos/effect/packages/effect/src/RuntimeFlags.ts new file mode 100644 index 0000000..2bce910 --- /dev/null +++ b/repos/effect/packages/effect/src/RuntimeFlags.ts @@ -0,0 +1,336 @@ +/** + * @since 2.0.0 + */ + +import type * as Differ from "./Differ.js" +import * as circular from "./internal/layer/circular.js" +import * as internal from "./internal/runtimeFlags.js" +import type * as Layer from "./Layer.js" +import type * as RuntimeFlagsPatch from "./RuntimeFlagsPatch.js" + +/** + * Represents a set of `RuntimeFlag`s. `RuntimeFlag`s affect the operation of + * the Effect runtime system. They are exposed to application-level code because + * they affect the behavior and performance of application code. + * + * @since 2.0.0 + * @category models + */ +export type RuntimeFlags = number & { + readonly RuntimeFlags: unique symbol +} + +/** + * Represents a flag that can be set to enable or disable a particular feature + * of the Effect runtime. + * + * @since 2.0.0 + * @category models + */ +export type RuntimeFlag = number & { + readonly RuntimeFlag: unique symbol +} + +/** + * No runtime flags. + * + * @since 2.0.0 + * @category constructors + */ +export const None: RuntimeFlag = internal.None + +/** + * The interruption flag determines whether or not the Effect runtime system will + * interrupt a fiber. + * + * @since 2.0.0 + * @category constructors + */ +export const Interruption: RuntimeFlag = internal.Interruption + +/** + * The op supervision flag determines whether or not the Effect runtime system + * will supervise all operations of the Effect runtime. Use of this flag will + * negatively impact performance, but is required for some operations, such as + * profiling. + * + * @since 2.0.0 + * @category constructors + */ +export const OpSupervision: RuntimeFlag = internal.OpSupervision + +/** + * The runtime metrics flag determines whether or not the Effect runtime system + * will collect metrics about the Effect runtime. Use of this flag will have a + * very small negative impact on performance, but generates very helpful + * operational insight into running Effect applications that can be exported to + * Prometheus or other tools via Effect Metrics. + * + * @since 2.0.0 + * @category constructors + */ +export const RuntimeMetrics: RuntimeFlag = internal.RuntimeMetrics + +/** + * The wind down flag determines whether the Effect runtime system will execute + * effects in wind-down mode. In wind-down mode, even if interruption is + * enabled and a fiber has been interrupted, the fiber will continue its + * execution uninterrupted. + * + * @since 2.0.0 + * @category constructors + */ +export const WindDown: RuntimeFlag = internal.WindDown + +/** + * The cooperative yielding flag determines whether the Effect runtime will + * yield to another fiber. + * + * @since 2.0.0 + * @category constructors + */ +export const CooperativeYielding: RuntimeFlag = internal.CooperativeYielding + +/** + * Returns `true` if the `CooperativeYielding` `RuntimeFlag` is enabled, `false` + * otherwise. + * + * @since 2.0.0 + * @category getters + */ +export const cooperativeYielding: (self: RuntimeFlags) => boolean = internal.cooperativeYielding + +/** + * Creates a `RuntimeFlagsPatch` which describes the difference between `self` + * and `that`. + * + * @since 2.0.0 + * @category diffing + */ +export const diff: { + (that: RuntimeFlags): (self: RuntimeFlags) => RuntimeFlagsPatch.RuntimeFlagsPatch + (self: RuntimeFlags, that: RuntimeFlags): RuntimeFlagsPatch.RuntimeFlagsPatch +} = internal.diff + +/** + * Constructs a differ that knows how to diff `RuntimeFlags` values. + * + * @since 2.0.0 + * @category utils + */ +export const differ: Differ.Differ = internal.differ + +/** + * Disables the specified `RuntimeFlag`. + * + * @since 2.0.0 + * @category utils + */ +export const disable: { + (flag: RuntimeFlag): (self: RuntimeFlags) => RuntimeFlags + (self: RuntimeFlags, flag: RuntimeFlag): RuntimeFlags +} = internal.disable + +/** + * Disables all of the `RuntimeFlag`s in the specified set of `RuntimeFlags`. + * + * @since 2.0.0 + * @category utils + */ +export const disableAll: { + (flags: RuntimeFlags): (self: RuntimeFlags) => RuntimeFlags + (self: RuntimeFlags, flags: RuntimeFlags): RuntimeFlags +} = internal.disableAll + +/** + * @since 2.0.0 + * @category context + */ +export const disableCooperativeYielding: Layer.Layer = circular.disableCooperativeYielding + +/** + * @since 2.0.0 + * @category context + */ +export const disableInterruption: Layer.Layer = circular.disableInterruption + +/** + * @since 2.0.0 + * @category context + */ +export const disableOpSupervision: Layer.Layer = circular.disableOpSupervision + +/** + * @since 2.0.0 + * @category context + */ +export const disableRuntimeMetrics: Layer.Layer = circular.disableRuntimeMetrics + +/** + * @since 2.0.0 + * @category context + */ +export const disableWindDown: Layer.Layer = circular.disableWindDown + +/** + * Enables the specified `RuntimeFlag`. + * + * @since 2.0.0 + * @category utils + */ +export const enable: { + (flag: RuntimeFlag): (self: RuntimeFlags) => RuntimeFlags + (self: RuntimeFlags, flag: RuntimeFlag): RuntimeFlags +} = internal.enable + +/** + * Enables all of the `RuntimeFlag`s in the specified set of `RuntimeFlags`. + * + * @since 2.0.0 + * @category utils + */ +export const enableAll: { + (flags: RuntimeFlags): (self: RuntimeFlags) => RuntimeFlags + (self: RuntimeFlags, flags: RuntimeFlags): RuntimeFlags +} = internal.enableAll + +/** + * @since 2.0.0 + * @category context + */ +export const enableCooperativeYielding: Layer.Layer = circular.enableCooperativeYielding + +/** + * @since 2.0.0 + * @category context + */ +export const enableInterruption: Layer.Layer = circular.enableInterruption + +/** + * @since 2.0.0 + * @category context + */ +export const enableOpSupervision: Layer.Layer = circular.enableOpSupervision + +/** + * @since 2.0.0 + * @category context + */ +export const enableRuntimeMetrics: Layer.Layer = circular.enableRuntimeMetrics + +/** + * @since 2.0.0 + * @category context + */ +export const enableWindDown: Layer.Layer = circular.enableWindDown + +/** + * Returns true only if the `Interruption` flag is **enabled** and the + * `WindDown` flag is **disabled**. + * + * A fiber is said to be interruptible if interruption is enabled and the fiber + * is not in its wind-down phase, in which it takes care of cleanup activities + * related to fiber shutdown. + * + * @since 2.0.0 + * @category getters + */ +export const interruptible: (self: RuntimeFlags) => boolean = internal.interruptible + +/** + * Returns `true` if the `Interruption` `RuntimeFlag` is enabled, `false` + * otherwise. + * + * @since 2.0.0 + * @category getters + */ +export const interruption: (self: RuntimeFlags) => boolean = internal.interruption + +/** + * Returns `true` if the specified `RuntimeFlag` is enabled, `false` otherwise. + * + * @since 2.0.0 + * @category elements + */ +export const isEnabled: { + (flag: RuntimeFlag): (self: RuntimeFlags) => boolean + (self: RuntimeFlags, flag: RuntimeFlag): boolean +} = internal.isEnabled + +/** + * Returns `true` if the specified `RuntimeFlag` is disabled, `false` otherwise. + * + * @since 2.0.0 + * @category elements + */ +export const isDisabled: { + (flag: RuntimeFlag): (self: RuntimeFlags) => boolean + (self: RuntimeFlags, flag: RuntimeFlag): boolean +} = internal.isDisabled + +/** + * @since 2.0.0 + * @category constructors + */ +export const make: (...flags: ReadonlyArray) => RuntimeFlags = internal.make + +/** + * @since 2.0.0 + * @category constructors + */ +export const none: RuntimeFlags = internal.none + +/** + * Returns `true` if the `OpSupervision` `RuntimeFlag` is enabled, `false` + * otherwise. + * + * @since 2.0.0 + * @category getters + */ +export const opSupervision: (self: RuntimeFlags) => boolean = internal.opSupervision + +/** + * Patches a set of `RuntimeFlag`s with a `RuntimeFlagsPatch`, returning the + * patched set of `RuntimeFlag`s. + * + * @since 2.0.0 + * @category utils + */ +export const patch: { + (patch: RuntimeFlagsPatch.RuntimeFlagsPatch): (self: RuntimeFlags) => RuntimeFlags + (self: RuntimeFlags, patch: RuntimeFlagsPatch.RuntimeFlagsPatch): RuntimeFlags +} = internal.patch + +/** + * Converts the provided `RuntimeFlags` into a `string`. + * + * @category conversions + * @since 2.0.0 + */ +export const render: (self: RuntimeFlags) => string = internal.render + +/** + * Returns `true` if the `RuntimeMetrics` `RuntimeFlag` is enabled, `false` + * otherwise. + * + * @since 2.0.0 + * @category getters + */ +export const runtimeMetrics: (self: RuntimeFlags) => boolean = internal.runtimeMetrics + +/** + * Converts the provided `RuntimeFlags` into a `ReadonlySet`. + * + * @category conversions + * @since 2.0.0 + */ +export const toSet: (self: RuntimeFlags) => ReadonlySet = internal.toSet + +/** + * Returns `true` if the `WindDown` `RuntimeFlag` is enabled, `false` + * otherwise. + * + * @since 2.0.0 + * @category getters + */ +export const windDown: (self: RuntimeFlags) => boolean = internal.windDown diff --git a/repos/effect/packages/effect/src/RuntimeFlagsPatch.ts b/repos/effect/packages/effect/src/RuntimeFlagsPatch.ts new file mode 100644 index 0000000..08e731f --- /dev/null +++ b/repos/effect/packages/effect/src/RuntimeFlagsPatch.ts @@ -0,0 +1,183 @@ +/** + * @since 2.0.0 + */ +import * as runtimeFlags from "./internal/runtimeFlags.js" +import * as internal from "./internal/runtimeFlagsPatch.js" +import type * as RuntimeFlags from "./RuntimeFlags.js" + +/** + * @since 2.0.0 + * @category models + */ +export type RuntimeFlagsPatch = number & { + readonly RuntimeFlagsPatch: unique symbol +} + +/** + * The empty `RuntimeFlagsPatch`. + * + * @since 2.0.0 + * @category constructors + */ +export const empty: RuntimeFlagsPatch = internal.empty + +/** + * @since 2.0.0 + * @category constructors + */ +export const make: (active: number, enabled: number) => RuntimeFlagsPatch = internal.make + +/** + * Creates a `RuntimeFlagsPatch` describing enabling the provided `RuntimeFlag`. + * + * @since 2.0.0 + * @category constructors + */ +export const enable: (flag: RuntimeFlags.RuntimeFlag) => RuntimeFlagsPatch = internal.enable + +/** + * Creates a `RuntimeFlagsPatch` describing disabling the provided `RuntimeFlag`. + * + * @since 2.0.0 + * @category constructors + */ +export const disable: (flag: RuntimeFlags.RuntimeFlag) => RuntimeFlagsPatch = internal.disable + +/** + * Returns `true` if the specified `RuntimeFlagsPatch` is empty. + * + * @since 2.0.0 + * @category getters + */ +export const isEmpty: (patch: RuntimeFlagsPatch) => boolean = internal.isEmpty + +/** + * Returns `true` if the `RuntimeFlagsPatch` describes the specified + * `RuntimeFlag` as active. + * + * @since 2.0.0 + * @category elements + */ +export const isActive: { + (flag: RuntimeFlagsPatch): (self: RuntimeFlagsPatch) => boolean + (self: RuntimeFlagsPatch, flag: RuntimeFlagsPatch): boolean +} = internal.isActive + +/** + * Returns `true` if the `RuntimeFlagsPatch` describes the specified + * `RuntimeFlag` as enabled. + * + * @since 2.0.0 + * @category elements + */ +export const isEnabled: { + (flag: RuntimeFlags.RuntimeFlag): (self: RuntimeFlagsPatch) => boolean + (self: RuntimeFlagsPatch, flag: RuntimeFlags.RuntimeFlag): boolean +} = internal.isEnabled + +/** + * Returns `true` if the `RuntimeFlagsPatch` describes the specified + * `RuntimeFlag` as disabled. + * + * @since 2.0.0 + * @category elements + */ +export const isDisabled: { + (flag: RuntimeFlags.RuntimeFlag): (self: RuntimeFlagsPatch) => boolean + (self: RuntimeFlagsPatch, flag: RuntimeFlags.RuntimeFlag): boolean +} = internal.isDisabled + +/** + * Returns `true` if the `RuntimeFlagsPatch` includes the specified + * `RuntimeFlag`, `false` otherwise. + * + * @since 2.0.0 + * @category elements + */ +export const includes: { + (flag: RuntimeFlagsPatch): (self: RuntimeFlagsPatch) => boolean + (self: RuntimeFlagsPatch, flag: RuntimeFlagsPatch): boolean +} = internal.isActive + +/** + * Creates a `RuntimeFlagsPatch` describing the application of the `self` patch, + * followed by `that` patch. + * + * @since 2.0.0 + * @category utils + */ +export const andThen: { + (that: RuntimeFlagsPatch): (self: RuntimeFlagsPatch) => RuntimeFlagsPatch + (self: RuntimeFlagsPatch, that: RuntimeFlagsPatch): RuntimeFlagsPatch +} = internal.andThen + +/** + * Creates a `RuntimeFlagsPatch` describing application of both the `self` patch + * and `that` patch. + * + * @since 2.0.0 + * @category utils + */ +export const both: { + (that: RuntimeFlagsPatch): (self: RuntimeFlagsPatch) => RuntimeFlagsPatch + (self: RuntimeFlagsPatch, that: RuntimeFlagsPatch): RuntimeFlagsPatch +} = internal.both + +/** + * Creates a `RuntimeFlagsPatch` describing application of either the `self` + * patch or `that` patch. + * + * @since 2.0.0 + * @category utils + */ +export const either: { + (that: RuntimeFlagsPatch): (self: RuntimeFlagsPatch) => RuntimeFlagsPatch + (self: RuntimeFlagsPatch, that: RuntimeFlagsPatch): RuntimeFlagsPatch +} = internal.either + +/** + * Creates a `RuntimeFlagsPatch` which describes exclusion of the specified + * `RuntimeFlag` from the set of `RuntimeFlags`. + * + * @category utils + * @since 2.0.0 + */ +export const exclude: { + (flag: RuntimeFlags.RuntimeFlag): (self: RuntimeFlagsPatch) => RuntimeFlagsPatch + (self: RuntimeFlagsPatch, flag: RuntimeFlags.RuntimeFlag): RuntimeFlagsPatch +} = internal.exclude + +/** + * Creates a `RuntimeFlagsPatch` which describes the inverse of the patch + * specified by the provided `RuntimeFlagsPatch`. + * + * @since 2.0.0 + * @category utils + */ +export const inverse: (patch: RuntimeFlagsPatch) => RuntimeFlagsPatch = internal.inverse + +/** + * Returns a `ReadonlySet` containing the `RuntimeFlags` described as + * enabled by the specified `RuntimeFlagsPatch`. + * + * @since 2.0.0 + * @category destructors + */ +export const enabledSet: (self: RuntimeFlagsPatch) => ReadonlySet = runtimeFlags.enabledSet + +/** + * Returns a `ReadonlySet` containing the `RuntimeFlags` described as + * disabled by the specified `RuntimeFlagsPatch`. + * + * @since 2.0.0 + * @category destructors + */ +export const disabledSet: (self: RuntimeFlagsPatch) => ReadonlySet = runtimeFlags.disabledSet + +/** + * Renders the provided `RuntimeFlagsPatch` to a string. + * + * @since 2.0.0 + * @category destructors + */ +export const render: (self: RuntimeFlagsPatch) => string = runtimeFlags.renderPatch diff --git a/repos/effect/packages/effect/src/STM.ts b/repos/effect/packages/effect/src/STM.ts new file mode 100644 index 0000000..c0ea8bd --- /dev/null +++ b/repos/effect/packages/effect/src/STM.ts @@ -0,0 +1,2045 @@ +/** + * @since 2.0.0 + */ +import * as Cause from "./Cause.js" +import * as Chunk from "./Chunk.js" +import type * as Context from "./Context.js" +import type * as Effect from "./Effect.js" +import type * as Either from "./Either.js" +import type * as FiberId from "./FiberId.js" +import type { LazyArg } from "./Function.js" +import type { TypeLambda } from "./HKT.js" +import * as core from "./internal/stm/core.js" +import * as stm from "./internal/stm/stm.js" +import type * as Option from "./Option.js" +import type { Pipeable } from "./Pipeable.js" +import type { Predicate, Refinement } from "./Predicate.js" +import type { Covariant, MergeRecord, NoExcessProperties, NoInfer } from "./Types.js" +import type * as Unify from "./Unify.js" +import type { YieldWrap } from "./Utils.js" + +/** + * @since 2.0.0 + * @category symbols + */ +export const STMTypeId: unique symbol = core.STMTypeId + +/** + * @since 2.0.0 + * @category symbols + */ +export type STMTypeId = typeof STMTypeId + +/** + * `STM` represents an effect that can be performed transactionally, + * resulting in a failure `E` or a value `A` that may require an environment + * `R` to execute. + * + * Software Transactional Memory is a technique which allows composition of + * arbitrary atomic operations. It is the software analog of transactions in + * database systems. + * + * The API is lifted directly from the Haskell package Control.Concurrent.STM + * although the implementation does not resemble the Haskell one at all. + * + * See http://hackage.haskell.org/package/stm-2.5.0.0/docs/Control-Concurrent-STM.html + * + * STM in Haskell was introduced in: + * + * Composable memory transactions, by Tim Harris, Simon Marlow, Simon Peyton + * Jones, and Maurice Herlihy, in ACM Conference on Principles and Practice of + * Parallel Programming 2005. + * + * See https://www.microsoft.com/en-us/research/publication/composable-memory-transactions/ + * + * See also: + * Lock Free Data Structures using STMs in Haskell, by Anthony Discolo, Tim + * Harris, Simon Marlow, Simon Peyton Jones, Satnam Singh) FLOPS 2006: Eighth + * International Symposium on Functional and Logic Programming, Fuji Susono, + * JAPAN, April 2006 + * + * https://www.microsoft.com/en-us/research/publication/lock-free-data-structures-using-stms-in-haskell/ + * + * The implemtation is based on the ZIO STM module, while JS environments have + * no race conditions from multiple threads STM provides greater benefits for + * synchronization of Fibers and transactional data-types can be quite useful. + * + * @since 2.0.0 + * @category models + */ +export interface STM + extends Effect.Effect, STM.Variance, Pipeable +{ + [Unify.typeSymbol]?: unknown + [Unify.unifySymbol]?: STMUnify + [Unify.ignoreSymbol]?: STMUnifyIgnore + [Symbol.iterator](): Effect.EffectGenerator> +} + +/** + * @since 2.0.0 + * @category models + */ +export interface STMUnify extends Effect.EffectUnify { + STM?: () => A[Unify.typeSymbol] extends STM | infer _ ? STM : never +} + +/** + * @category models + * @since 2.0.0 + */ +export interface STMUnifyIgnore extends Effect.EffectUnifyIgnore { + Effect?: true +} + +/** + * @category type lambdas + * @since 2.0.0 + */ +export interface STMTypeLambda extends TypeLambda { + readonly type: STM +} + +/** + * @since 2.0.0 + * @category models + */ +declare module "./Context.js" { + interface Tag extends STM {} + // eslint-disable-next-line @typescript-eslint/no-unused-vars + interface Reference extends STM {} +} + +/** + * @since 2.0.0 + * @category models + */ +declare module "./Either.js" { + interface Left extends STM { + readonly _tag: "Left" + } + interface Right extends STM { + readonly _tag: "Right" + } +} + +/** + * @since 2.0.0 + * @category models + */ +declare module "./Option.js" { + interface None extends STM { + readonly _tag: "None" + } + interface Some extends STM { + readonly _tag: "Some" + } +} + +/** + * @since 2.0.0 + */ +export declare namespace STM { + /** + * @since 2.0.0 + * @category models + */ + export interface Variance { + readonly [STMTypeId]: { + readonly _A: Covariant + readonly _E: Covariant + readonly _R: Covariant + } + } +} + +/** + * Returns `true` if the provided value is an `STM`, `false` otherwise. + * + * @since 2.0.0 + * @category refinements + */ +export const isSTM: (u: unknown) => u is STM = core.isSTM + +/** + * Treats the specified `acquire` transaction as the acquisition of a + * resource. The `acquire` transaction will be executed interruptibly. If it + * is a success and is committed the specified `release` workflow will be + * executed uninterruptibly as soon as the `use` workflow completes execution. + * + * @since 2.0.0 + * @category constructors + */ +export const acquireUseRelease: { + ( + use: (resource: A) => STM, + release: (resource: A) => STM + ): (acquire: STM) => Effect.Effect + ( + acquire: STM, + use: (resource: A) => STM, + release: (resource: A) => STM + ): Effect.Effect +} = stm.acquireUseRelease + +/** + * @since 2.0.0 + * @category utils + */ +export declare namespace All { + type STMAny = STM + + type ReturnTuple>, Discard extends boolean> = STM< + Discard extends true ? void + : T[number] extends never ? [] + : { -readonly [K in keyof T]: [T[K]] extends [STM] ? A : never }, + T[number] extends never ? never + : [T[number]] extends [{ [STMTypeId]: { _E: (_: never) => infer E } }] ? E + : never, + T[number] extends never ? never + : [T[number]] extends [{ [STMTypeId]: { _R: (_: never) => infer R } }] ? R + : never + > extends infer X ? X : never + + type ReturnIterable, Discard extends boolean> = [T] extends + [Iterable>] ? STM, E, R> : never + + type ReturnObject, Discard extends boolean> = STM< + Discard extends true ? void + : { -readonly [K in keyof T]: [T[K]] extends [STM.Variance] ? A : never }, + keyof T extends never ? never + : [T[keyof T]] extends [{ [STMTypeId]: { _E: (_: never) => infer E } }] ? E + : never, + keyof T extends never ? never + : [T[keyof T]] extends [{ [STMTypeId]: { _R: (_: never) => infer R } }] ? R + : never + > + + /** + * @since 2.0.0 + * @category utils + */ + export type Options = { + readonly discard?: boolean | undefined + } + type IsDiscard = [Extract] extends [never] ? false : true + type Narrow = (A extends [] ? [] : never) | A + + /** + * @since 2.0.0 + * @category utils + */ + export interface Signature { + < + Arg extends ReadonlyArray | Iterable | Record, + O extends NoExcessProperties + >( + arg: Narrow, + options?: O + ): [Arg] extends [ReadonlyArray] ? ReturnTuple> + : [Arg] extends [Iterable] ? ReturnIterable> + : [Arg] extends [Record] ? ReturnObject> + : never + } +} + +/** + * Runs all the provided transactional effects in sequence respecting the + * structure provided in input. + * + * Supports multiple arguments, a single argument tuple / array or record / + * struct. + * + * @since 2.0.0 + * @category constructors + */ +export const all: All.Signature = stm.all + +/** + * Maps the success value of this effect to the specified constant value. + * + * @since 2.0.0 + * @category mapping + */ +export const as: { + (value: A2): (self: STM) => STM + (self: STM, value: A2): STM +} = stm.as + +/** + * Maps the success value of this effect to an optional value. + * + * @since 2.0.0 + * @category mapping + */ +export const asSome: (self: STM) => STM, E, R> = stm.asSome + +/** + * Maps the error value of this effect to an optional value. + * + * @since 2.0.0 + * @category mapping + */ +export const asSomeError: (self: STM) => STM, R> = stm.asSomeError + +/** + * This function maps the success value of an `STM` to `void`. If the original + * `STM` succeeds, the returned `STM` will also succeed. If the original `STM` + * fails, the returned `STM` will fail with the same error. + * + * @since 2.0.0 + * @category mapping + */ +export const asVoid: (self: STM) => STM = stm.asVoid + +/** + * Creates an `STM` value from a partial (but pure) function. + * + * @since 2.0.0 + * @category constructors + */ +export const attempt: (evaluate: LazyArg) => STM = stm.attempt + +/** + * Recovers from all errors. + * + * @since 2.0.0 + * @category error handling + */ +export const catchAll: { + (f: (e: E) => STM): (self: STM) => STM + (self: STM, f: (e: E) => STM): STM +} = core.catchAll + +/** + * Recovers from some or all of the error cases. + * + * @since 2.0.0 + * @category error handling + */ +export const catchSome: { + ( + pf: (error: E) => Option.Option> + ): (self: STM) => STM + ( + self: STM, + pf: (error: E) => Option.Option> + ): STM +} = stm.catchSome + +/** + * Recovers from the specified tagged error. + * + * @since 2.0.0 + * @category error handling + */ +export const catchTag: { + ( + k: K, + f: (e: Extract) => STM + ): (self: STM) => STM, R1 | R> + ( + self: STM, + k: K, + f: (e: Extract) => STM + ): STM, R | R1> +} = stm.catchTag + +/** + * Recovers from multiple tagged errors. + * + * @since 2.0.0 + * @category error handling + */ +export const catchTags: { + < + E extends { _tag: string }, + Cases extends { [K in E["_tag"]]+?: ((error: Extract) => STM) } + >( + cases: Cases + ): ( + self: STM + ) => STM< + | A + | { [K in keyof Cases]: Cases[K] extends (...args: Array) => STM ? A : never }[keyof Cases], + | Exclude + | { [K in keyof Cases]: Cases[K] extends (...args: Array) => STM ? E : never }[keyof Cases], + | R + | { [K in keyof Cases]: Cases[K] extends (...args: Array) => STM ? R : never }[keyof Cases] + > + < + R, + E extends { _tag: string }, + A, + Cases extends { [K in E["_tag"]]+?: ((error: Extract) => STM) } + >( + self: STM, + cases: Cases + ): STM< + | A + | { [K in keyof Cases]: Cases[K] extends (...args: Array) => STM ? A : never }[keyof Cases], + | Exclude + | { [K in keyof Cases]: Cases[K] extends (...args: Array) => STM ? E : never }[keyof Cases], + | R + | { [K in keyof Cases]: Cases[K] extends (...args: Array) => STM ? R : never }[keyof Cases] + > +} = stm.catchTags + +/** + * Checks the condition, and if it's true, returns unit, otherwise, retries. + * + * @since 2.0.0 + * @category constructors + */ +export const check: (predicate: LazyArg) => STM = stm.check + +/** + * Simultaneously filters and maps the value produced by this effect. + * + * @since 2.0.0 + * @category mutations + */ +export const collect: { + (pf: (a: A) => Option.Option): (self: STM) => STM + (self: STM, pf: (a: A) => Option.Option): STM +} = stm.collect + +/** + * Simultaneously filters and maps the value produced by this effect. + * + * @since 2.0.0 + * @category mutations + */ +export const collectSTM: { + (pf: (a: A) => Option.Option>): (self: STM) => STM + (self: STM, pf: (a: A) => Option.Option>): STM +} = stm.collectSTM + +/** + * Commits this transaction atomically. + * + * @since 2.0.0 + * @category destructors + */ +export const commit: (self: STM) => Effect.Effect = core.commit + +/** + * Commits this transaction atomically, regardless of whether the transaction + * is a success or a failure. + * + * @since 2.0.0 + * @category destructors + */ +export const commitEither: (self: STM) => Effect.Effect = stm.commitEither + +/** + * Similar to Either.cond, evaluate the predicate, return the given A as + * success if predicate returns true, and the given E as error otherwise + * + * @since 2.0.0 + * @category constructors + */ +export const cond: (predicate: LazyArg, error: LazyArg, result: LazyArg) => STM = stm.cond + +/** + * Retrieves the environment inside an stm. + * + * @since 2.0.0 + * @category constructors + */ +export const context: () => STM, never, R> = core.context + +/** + * Accesses the environment of the transaction to perform a transaction. + * + * @since 2.0.0 + * @category constructors + */ +export const contextWith: (f: (environment: Context.Context) => R) => STM = core.contextWith + +/** + * Accesses the environment of the transaction to perform a transaction. + * + * @since 2.0.0 + * @category constructors + */ +export const contextWithSTM: ( + f: (environment: Context.Context) => STM +) => STM = core.contextWithSTM + +/** + * Transforms the environment being provided to this effect with the specified + * function. + * + * @since 2.0.0 + * @category context + */ +export const mapInputContext: { + (f: (context: Context.Context) => Context.Context): (self: STM) => STM + (self: STM, f: (context: Context.Context) => Context.Context): STM +} = core.mapInputContext + +/** + * Fails the transactional effect with the specified defect. + * + * @since 2.0.0 + * @category constructors + */ +export const die: (defect: unknown) => STM = core.die + +/** + * Kills the fiber running the effect with a `Cause.RuntimeException` that + * contains the specified message. + * + * @since 2.0.0 + * @category constructors + */ +export const dieMessage: (message: string) => STM = core.dieMessage + +/** + * Fails the transactional effect with the specified lazily evaluated defect. + * + * @since 2.0.0 + * @category constructors + */ +export const dieSync: (evaluate: LazyArg) => STM = core.dieSync + +/** + * Converts the failure channel into an `Either`. + * + * @since 2.0.0 + * @category mutations + */ +export const either: (self: STM) => STM, never, R> = stm.either + +/** + * Executes the specified finalization transaction whether or not this effect + * succeeds. Note that as with all STM transactions, if the full transaction + * fails, everything will be rolled back. + * + * @since 2.0.0 + * @category finalization + */ +export const ensuring: { + (finalizer: STM): (self: STM) => STM + (self: STM, finalizer: STM): STM +} = core.ensuring + +/** + * Returns an effect that ignores errors and runs repeatedly until it + * eventually succeeds. + * + * @since 2.0.0 + * @category mutations + */ +export const eventually: (self: STM) => STM = stm.eventually + +/** + * Determines whether all elements of the `Iterable` satisfy the effectual + * predicate. + * + * @since 2.0.0 + * @category constructors + */ +export const every: { + (predicate: (a: NoInfer) => STM): (iterable: Iterable) => STM + (iterable: Iterable, predicate: (a: A) => STM): STM +} = stm.every + +/** + * Determines whether any element of the `Iterable[A]` satisfies the effectual + * predicate `f`. + * + * @since 2.0.0 + * @category constructors + */ +export const exists: { + (predicate: (a: NoInfer) => STM): (iterable: Iterable) => STM + (iterable: Iterable, predicate: (a: A) => STM): STM +} = stm.exists + +/** + * Fails the transactional effect with the specified error. + * + * @since 2.0.0 + * @category constructors + */ +export const fail: (error: E) => STM = core.fail + +/** + * Fails the transactional effect with the specified lazily evaluated error. + * + * @since 2.0.0 + * @category constructors + */ +export const failSync: (evaluate: LazyArg) => STM = core.failSync + +/** + * Returns the fiber id of the fiber committing the transaction. + * + * @since 2.0.0 + * @category constructors + */ +export const fiberId: STM = stm.fiberId + +/** + * Filters the collection using the specified effectual predicate. + * + * @since 2.0.0 + * @category constructors + */ +export const filter: { + (predicate: (a: NoInfer) => STM): (iterable: Iterable) => STM, E, R> + (iterable: Iterable, predicate: (a: A) => STM): STM, E, R> +} = stm.filter + +/** + * Filters the collection using the specified effectual predicate, removing + * all elements that satisfy the predicate. + * + * @since 2.0.0 + * @category constructors + */ +export const filterNot: { + (predicate: (a: NoInfer) => STM): (iterable: Iterable) => STM, E, R> + (iterable: Iterable, predicate: (a: A) => STM): STM, E, R> +} = stm.filterNot + +/** + * Dies with specified defect if the predicate fails. + * + * @since 2.0.0 + * @category filtering + */ +export const filterOrDie: { + ( + refinement: Refinement, B>, + defect: LazyArg + ): (self: STM) => STM + (predicate: Predicate>, defect: LazyArg): (self: STM) => STM + (self: STM, refinement: Refinement, defect: LazyArg): STM + (self: STM, predicate: Predicate, defect: LazyArg): STM +} = stm.filterOrDie + +/** + * Dies with a `Cause.RuntimeException` having the specified message if the + * predicate fails. + * + * @since 2.0.0 + * @category filtering + */ +export const filterOrDieMessage: { + (refinement: Refinement, B>, message: string): (self: STM) => STM + (predicate: Predicate>, message: string): (self: STM) => STM + (self: STM, refinement: Refinement, message: string): STM + (self: STM, predicate: Predicate, message: string): STM +} = stm.filterOrDieMessage + +/** + * Supplies `orElse` if the predicate fails. + * + * @since 2.0.0 + * @category filtering + */ +export const filterOrElse: { + ( + refinement: Refinement, B>, + orElse: (a: NoInfer) => STM + ): (self: STM) => STM + ( + predicate: Predicate>, + orElse: (a: NoInfer) => STM + ): (self: STM) => STM + ( + self: STM, + refinement: Refinement, + orElse: (a: A) => STM + ): STM + ( + self: STM, + predicate: Predicate, + orElse: (a: A) => STM + ): STM +} = stm.filterOrElse + +/** + * Fails with the specified error if the predicate fails. + * + * @since 2.0.0 + * @category filtering + */ +export const filterOrFail: { + ( + refinement: Refinement, B>, + orFailWith: (a: NoInfer) => E2 + ): (self: STM) => STM + ( + predicate: Predicate>, + orFailWith: (a: NoInfer) => E2 + ): (self: STM) => STM + ( + self: STM, + refinement: Refinement, + orFailWith: (a: A) => E2 + ): STM + (self: STM, predicate: Predicate, orFailWith: (a: A) => E2): STM +} = stm.filterOrFail + +/** + * Feeds the value produced by this effect to the specified function, and then + * runs the returned effect as well to produce its results. + * + * @since 2.0.0 + * @category sequencing + */ +export const flatMap: { + (f: (a: A) => STM): (self: STM) => STM + (self: STM, f: (a: A) => STM): STM +} = core.flatMap + +/** + * Flattens out a nested `STM` effect. + * + * @since 2.0.0 + * @category sequencing + */ +export const flatten: (self: STM, E, R>) => STM = stm.flatten + +/** + * Flips the success and failure channels of this transactional effect. This + * allows you to use all methods on the error channel, possibly before + * flipping back. + * + * @since 2.0.0 + * @category mutations + */ +export const flip: (self: STM) => STM = stm.flip + +/** + * Swaps the error/value parameters, applies the function `f` and flips the + * parameters back + * + * @since 2.0.0 + * @category mutations + */ +export const flipWith: { + (f: (stm: STM) => STM): (self: STM) => STM + (self: STM, f: (stm: STM) => STM): STM +} = stm.flipWith + +/** + * Folds over the `STM` effect, handling both failure and success, but not + * retry. + * + * @since 2.0.0 + * @category folding + */ +export const match: { + ( + options: { readonly onFailure: (error: E) => A2; readonly onSuccess: (value: A) => A3 } + ): (self: STM) => STM + ( + self: STM, + options: { readonly onFailure: (error: E) => A2; readonly onSuccess: (value: A) => A3 } + ): STM +} = stm.match + +/** + * Effectfully folds over the `STM` effect, handling both failure and success. + * + * @since 2.0.0 + * @category folding + */ +export const matchSTM: { + ( + options: { readonly onFailure: (e: E) => STM; readonly onSuccess: (a: A) => STM } + ): (self: STM) => STM + ( + self: STM, + options: { readonly onFailure: (e: E) => STM; readonly onSuccess: (a: A) => STM } + ): STM +} = core.matchSTM + +/** + * Applies the function `f` to each element of the `Iterable` and returns + * a transactional effect that produces a new `Chunk`. + * + * @since 2.0.0 + * @category traversing + */ +export const forEach: { + ( + f: (a: A) => STM, + options?: { readonly discard?: false | undefined } | undefined + ): (elements: Iterable) => STM, E, R> + ( + f: (a: A) => STM, + options: { readonly discard: true } + ): (elements: Iterable) => STM + ( + elements: Iterable, + f: (a: A) => STM, + options?: { readonly discard?: false | undefined } | undefined + ): STM, E, R> + (elements: Iterable, f: (a: A) => STM, options: { readonly discard: true }): STM +} = stm.forEach + +/** + * Lifts an `Either` into a `STM`. + * + * @since 2.0.0 + * @category constructors + */ +export const fromEither: (either: Either.Either) => STM = stm.fromEither + +/** + * Lifts an `Option` into a `STM`. + * + * @since 2.0.0 + * @category constructors + */ +export const fromOption: (option: Option.Option) => STM> = stm.fromOption + +/** + * @since 2.0.0 + * @category models + */ +export interface Adapter { + (self: STM): STM + (a: A, ab: (a: A) => STM<_A, _E, _R>): STM<_A, _E, _R> + (a: A, ab: (a: A) => B, bc: (b: B) => STM<_A, _E, _R>): STM<_A, _E, _R> + (a: A, ab: (a: A) => B, bc: (b: B) => C, cd: (c: C) => STM<_A, _E, _R>): STM<_A, _E, _R> + ( + a: A, + ab: (a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D, + de: (d: D) => STM<_A, _E, _R> + ): STM<_A, _E, _R> + ( + a: A, + ab: (a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D, + de: (d: D) => E, + ef: (e: E) => STM<_A, _E, _R> + ): STM<_A, _E, _R> + ( + a: A, + ab: (a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D, + de: (d: D) => E, + ef: (e: E) => F, + fg: (f: F) => STM<_A, _E, _R> + ): STM<_A, _E, _R> + ( + a: A, + ab: (a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D, + de: (d: D) => E, + ef: (e: E) => F, + fg: (f: F) => G, + gh: (g: F) => STM<_A, _E, _R> + ): STM<_A, _E, _R> + ( + a: A, + ab: (a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D, + de: (d: D) => E, + ef: (e: E) => F, + fg: (f: F) => G, + gh: (g: G) => H, + hi: (g: H) => STM<_A, _E, _R> + ): STM<_A, _E, _R> + ( + a: A, + ab: (a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D, + de: (d: D) => E, + ef: (e: E) => F, + fg: (f: F) => G, + gh: (g: G) => H, + hi: (h: H) => I, + ij: (i: I) => STM<_A, _E, _R> + ): STM<_A, _E, _R> + ( + a: A, + ab: (a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D, + de: (d: D) => E, + ef: (e: E) => F, + fg: (f: F) => G, + gh: (g: G) => H, + hi: (h: H) => I, + ij: (i: I) => J, + jk: (j: J) => STM<_A, _E, _R> + ): STM<_A, _E, _R> + ( + a: A, + ab: (a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D, + de: (d: D) => E, + ef: (e: E) => F, + fg: (f: F) => G, + gh: (g: G) => H, + hi: (h: H) => I, + ij: (i: I) => J, + jk: (j: J) => K, + kl: (k: K) => STM<_A, _E, _R> + ): STM<_A, _E, _R> + ( + a: A, + ab: (a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D, + de: (d: D) => E, + ef: (e: E) => F, + fg: (f: F) => G, + gh: (g: G) => H, + hi: (h: H) => I, + ij: (i: I) => J, + jk: (j: J) => K, + kl: (k: K) => L, + lm: (l: L) => STM<_A, _E, _R> + ): STM<_A, _E, _R> + ( + a: A, + ab: (a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D, + de: (d: D) => E, + ef: (e: E) => F, + fg: (f: F) => G, + gh: (g: G) => H, + hi: (h: H) => I, + ij: (i: I) => J, + jk: (j: J) => K, + kl: (k: K) => L, + lm: (l: L) => M, + mn: (m: M) => STM<_A, _E, _R> + ): STM<_A, _E, _R> + ( + a: A, + ab: (a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D, + de: (d: D) => E, + ef: (e: E) => F, + fg: (f: F) => G, + gh: (g: G) => H, + hi: (h: H) => I, + ij: (i: I) => J, + jk: (j: J) => K, + kl: (k: K) => L, + lm: (l: L) => M, + mn: (m: M) => N, + no: (n: N) => STM<_A, _E, _R> + ): STM<_A, _E, _R> + ( + a: A, + ab: (a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D, + de: (d: D) => E, + ef: (e: E) => F, + fg: (f: F) => G, + gh: (g: G) => H, + hi: (h: H) => I, + ij: (i: I) => J, + jk: (j: J) => K, + kl: (k: K) => L, + lm: (l: L) => M, + mn: (m: M) => N, + no: (n: N) => O, + op: (o: O) => STM<_A, _E, _R> + ): STM<_A, _E, _R> + ( + a: A, + ab: (a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D, + de: (d: D) => E, + ef: (e: E) => F, + fg: (f: F) => G, + gh: (g: G) => H, + hi: (h: H) => I, + ij: (i: I) => J, + jk: (j: J) => K, + kl: (k: K) => L, + lm: (l: L) => M, + mn: (m: M) => N, + no: (n: N) => O, + op: (o: O) => P, + pq: (p: P) => STM<_A, _E, _R> + ): STM<_A, _E, _R> + ( + a: A, + ab: (a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D, + de: (d: D) => E, + ef: (e: E) => F, + fg: (f: F) => G, + gh: (g: G) => H, + hi: (h: H) => I, + ij: (i: I) => J, + jk: (j: J) => K, + kl: (k: K) => L, + lm: (l: L) => M, + mn: (m: M) => N, + no: (n: N) => O, + op: (o: O) => P, + pq: (p: P) => Q, + qr: (q: Q) => STM<_A, _E, _R> + ): STM<_A, _E, _R> + ( + a: A, + ab: (a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D, + de: (d: D) => E, + ef: (e: E) => F, + fg: (f: F) => G, + gh: (g: G) => H, + hi: (h: H) => I, + ij: (i: I) => J, + jk: (j: J) => K, + kl: (k: K) => L, + lm: (l: L) => M, + mn: (m: M) => N, + no: (n: N) => O, + op: (o: O) => P, + pq: (p: P) => Q, + qr: (q: Q) => R, + rs: (r: R) => STM<_A, _E, _R> + ): STM<_A, _E, _R> + ( + a: A, + ab: (a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D, + de: (d: D) => E, + ef: (e: E) => F, + fg: (f: F) => G, + gh: (g: G) => H, + hi: (h: H) => I, + ij: (i: I) => J, + jk: (j: J) => K, + kl: (k: K) => L, + lm: (l: L) => M, + mn: (m: M) => N, + no: (n: N) => O, + op: (o: O) => P, + pq: (p: P) => Q, + qr: (q: Q) => R, + rs: (r: R) => S, + st: (s: S) => STM<_A, _E, _R> + ): STM<_A, _E, _R> + ( + a: A, + ab: (a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D, + de: (d: D) => E, + ef: (e: E) => F, + fg: (f: F) => G, + gh: (g: G) => H, + hi: (h: H) => I, + ij: (i: I) => J, + jk: (j: J) => K, + kl: (k: K) => L, + lm: (l: L) => M, + mn: (m: M) => N, + no: (n: N) => O, + op: (o: O) => P, + pq: (p: P) => Q, + qr: (q: Q) => R, + rs: (r: R) => S, + st: (s: S) => T, + tu: (s: T) => STM<_A, _E, _R> + ): STM<_A, _E, _R> +} + +/** + * @since 2.0.0 + * @category constructors + */ +export const gen: >, AEff>( + ...args: + | [ + self: Self, + body: (this: Self, resume: Adapter) => Generator + ] + | [body: (resume: Adapter) => Generator] +) => STM< + AEff, + [Eff] extends [never] ? never : [Eff] extends [YieldWrap>] ? E : never, + [Eff] extends [never] ? never : [Eff] extends [YieldWrap>] ? R : never +> = stm.gen + +/** + * Returns a successful effect with the head of the list if the list is + * non-empty or fails with the error `None` if the list is empty. + * + * @since 2.0.0 + * @category getters + */ +export const head: (self: STM, E, R>) => STM, R> = stm.head + +const if_: { + (options: { + readonly onTrue: STM + readonly onFalse: STM /** + * Flattens out a nested `STM` effect. + * + * @since 2.0.0 + * @category sequencing + */ + }): (self: boolean | STM) => STM + ( + self: boolean, + options: { readonly onTrue: STM; readonly onFalse: STM } + ): STM + ( + self: STM, + options: { readonly onTrue: STM; readonly onFalse: STM } + ): STM +} = stm.if_ + +export { + /** + * Runs `onTrue` if the result of `b` is `true` and `onFalse` otherwise. + * + * @since 2.0.0 + * @category mutations + */ + if_ as if +} + +/** + * Returns a new effect that ignores the success or failure of this effect. + * + * @since 2.0.0 + * @category mutations + */ +export const ignore: (self: STM) => STM = stm.ignore + +/** + * Interrupts the fiber running the effect. + * + * @since 2.0.0 + * @category constructors + */ +export const interrupt: STM = core.interrupt + +/** + * Interrupts the fiber running the effect with the specified `FiberId`. + * + * @since 2.0.0 + * @category constructors + */ +export const interruptAs: (fiberId: FiberId.FiberId) => STM = core.interruptAs + +/** + * Returns whether this transactional effect is a failure. + * + * @since 2.0.0 + * @category getters + */ +export const isFailure: (self: STM) => STM = stm.isFailure + +/** + * Returns whether this transactional effect is a success. + * + * @since 2.0.0 + * @category getters + */ +export const isSuccess: (self: STM) => STM = stm.isSuccess + +/** + * Iterates with the specified transactional function. The moral equivalent + * of: + * + * ```ts skip-type-checking + * const s = initial + * + * while (cont(s)) { + * s = body(s) + * } + * + * return s + * ``` + * + * @since 2.0.0 + * @category constructors + */ +export const iterate: ( + initial: Z, + options: { + readonly while: Predicate + readonly body: (z: Z) => STM + } +) => STM = stm.iterate + +/** + * Loops with the specified transactional function, collecting the results + * into a list. The moral equivalent of: + * + * ```ts skip-type-checking + * const as = [] + * let s = initial + * + * while (cont(s)) { + * as.push(body(s)) + * s = inc(s) + * } + * + * return as + * ``` + * + * @since 2.0.0 + * @category constructors + */ +export const loop: { + ( + initial: Z, + options: { + readonly while: (z: Z) => boolean + readonly step: (z: Z) => Z + readonly body: (z: Z) => STM + readonly discard?: false | undefined + } + ): STM, E, R> + ( + initial: Z, + options: { + readonly while: (z: Z) => boolean + readonly step: (z: Z) => Z + readonly body: (z: Z) => STM + readonly discard: true + } + ): STM +} = stm.loop + +/** + * Maps the value produced by the effect. + * + * @since 2.0.0 + * @category mapping + */ +export const map: { + (f: (a: A) => B): (self: STM) => STM + (self: STM, f: (a: A) => B): STM +} = core.map + +/** + * Maps the value produced by the effect with the specified function that may + * throw exceptions but is otherwise pure, translating any thrown exceptions + * into typed failed effects. + * + * @since 2.0.0 + * @category mapping + */ +export const mapAttempt: { + (f: (a: A) => B): (self: STM) => STM + (self: STM, f: (a: A) => B): STM +} = stm.mapAttempt + +/** + * Returns an `STM` effect whose failure and success channels have been mapped + * by the specified pair of functions, `f` and `g`. + * + * @since 2.0.0 + * @category mapping + */ +export const mapBoth: { + ( + options: { readonly onFailure: (error: E) => E2; readonly onSuccess: (value: A) => A2 } + ): (self: STM) => STM + ( + self: STM, + options: { readonly onFailure: (error: E) => E2; readonly onSuccess: (value: A) => A2 } + ): STM +} = stm.mapBoth + +/** + * Maps from one error type to another. + * + * @since 2.0.0 + * @category mapping + */ +export const mapError: { + (f: (error: E) => E2): (self: STM) => STM + (self: STM, f: (error: E) => E2): STM +} = stm.mapError + +/** + * Returns a new effect where the error channel has been merged into the + * success channel to their common combined type. + * + * @since 2.0.0 + * @category mutations + */ +export const merge: (self: STM) => STM = stm.merge + +/** + * Merges an `Iterable` to a single `STM`, working sequentially. + * + * @since 2.0.0 + * @category constructors + */ +export const mergeAll: { + (zero: A2, f: (a2: A2, a: A) => A2): (iterable: Iterable>) => STM + (iterable: Iterable>, zero: A2, f: (a2: A2, a: A) => A2): STM +} = stm.mergeAll + +/** + * Returns a new effect where boolean value of this effect is negated. + * + * @since 2.0.0 + * @category mutations + */ +export const negate: (self: STM) => STM = stm.negate + +/** + * Requires the option produced by this value to be `None`. + * + * @since 2.0.0 + * @category mutations + */ +export const none: (self: STM, E, R>) => STM, R> = stm.none + +/** + * Converts the failure channel into an `Option`. + * + * @since 2.0.0 + * @category mutations + */ +export const option: (self: STM) => STM, never, R> = stm.option + +/** + * Translates `STM` effect failure into death of the fiber, making all + * failures unchecked and not a part of the type of the effect. + * + * @since 2.0.0 + * @category error handling + */ +export const orDie: (self: STM) => STM = stm.orDie + +/** + * Keeps none of the errors, and terminates the fiber running the `STM` effect + * with them, using the specified function to convert the `E` into a defect. + * + * @since 2.0.0 + * @category error handling + */ +export const orDieWith: { + (f: (error: E) => unknown): (self: STM) => STM + (self: STM, f: (error: E) => unknown): STM +} = stm.orDieWith + +/** + * Tries this effect first, and if it fails or retries, tries the other + * effect. + * + * @since 2.0.0 + * @category error handling + */ +export const orElse: { + (that: LazyArg>): (self: STM) => STM + (self: STM, that: LazyArg>): STM +} = stm.orElse + +/** + * Returns a transactional effect that will produce the value of this effect + * in left side, unless it fails or retries, in which case, it will produce + * the value of the specified effect in right side. + * + * @since 2.0.0 + * @category error handling + */ +export const orElseEither: { + (that: LazyArg>): (self: STM) => STM, E2, R2 | R> + (self: STM, that: LazyArg>): STM, E2, R | R2> +} = stm.orElseEither + +/** + * Tries this effect first, and if it fails or retries, fails with the + * specified error. + * + * @since 2.0.0 + * @category error handling + */ +export const orElseFail: { + (error: LazyArg): (self: STM) => STM + (self: STM, error: LazyArg): STM +} = stm.orElseFail + +/** + * Returns an effect that will produce the value of this effect, unless it + * fails with the `None` value, in which case it will produce the value of the + * specified effect. + * + * @since 2.0.0 + * @category error handling + */ +export const orElseOptional: { + ( + that: LazyArg, R2>> + ): (self: STM, R>) => STM, R2 | R> + ( + self: STM, R>, + that: LazyArg, R2>> + ): STM, R | R2> +} = stm.orElseOptional + +/** + * Tries this effect first, and if it fails or retries, succeeds with the + * specified value. + * + * @since 2.0.0 + * @category error handling + */ +export const orElseSucceed: { + (value: LazyArg): (self: STM) => STM + (self: STM, value: LazyArg): STM +} = stm.orElseSucceed + +/** + * Tries this effect first, and if it enters retry, then it tries the other + * effect. This is an equivalent of Haskell's orElse. + * + * @since 2.0.0 + * @category error handling + */ +export const orTry: { + (that: LazyArg>): (self: STM) => STM + (self: STM, that: LazyArg>): STM +} = core.orTry + +/** + * Feeds elements of type `A` to a function `f` that returns an effect. + * Collects all successes and failures in a tupled fashion. + * + * @since 2.0.0 + * @category traversing + */ +export const partition: { + ( + f: (a: A) => STM + ): (elements: Iterable) => STM<[excluded: Array, satisfying: Array], never, R> + ( + elements: Iterable, + f: (a: A) => STM + ): STM<[excluded: Array, satisfying: Array], never, R> +} = stm.partition + +/** + * Provides the transaction its required environment, which eliminates its + * dependency on `R`. + * + * @since 2.0.0 + * @category context + */ +export const provideContext: { + (env: Context.Context): (self: STM) => STM + (self: STM, env: Context.Context): STM +} = stm.provideContext + +/** + * Splits the context into two parts, providing one part using the + * specified layer and leaving the remainder `R0`. + * + * @since 2.0.0 + * @category context + */ +export const provideSomeContext: { + (context: Context.Context): (self: STM) => STM> + (self: STM, context: Context.Context): STM> +} = stm.provideSomeContext + +/** + * Provides the effect with the single service it requires. If the transactional + * effect requires more than one service use `provideEnvironment` instead. + * + * @since 2.0.0 + * @category context + */ +export const provideService: { + (tag: Context.Tag, resource: NoInfer): (self: STM) => STM> + (self: STM, tag: Context.Tag, resource: NoInfer): STM> +} = stm.provideService + +/** + * Provides the effect with the single service it requires. If the transactional + * effect requires more than one service use `provideEnvironment` instead. + * + * @since 2.0.0 + * @category context + */ +export const provideServiceSTM: { + ( + tag: Context.Tag, + stm: STM, E1, R1> + ): (self: STM) => STM> + ( + self: STM, + tag: Context.Tag, + stm: STM, E1, R1> + ): STM> +} = stm.provideServiceSTM + +/** + * Folds an `Iterable` using an effectual function f, working sequentially + * from left to right. + * + * @since 2.0.0 + * @category constructors + */ +export const reduce: { + (zero: S, f: (s: S, a: A) => STM): (iterable: Iterable) => STM + (iterable: Iterable, zero: S, f: (s: S, a: A) => STM): STM +} = stm.reduce + +/** + * Reduces an `Iterable` to a single `STM`, working sequentially. + * + * @since 2.0.0 + * @category constructors + */ +export const reduceAll: { + ( + initial: STM, + f: (x: A, y: A) => A + ): (iterable: Iterable>) => STM + ( + iterable: Iterable>, + initial: STM, + f: (x: A, y: A) => A + ): STM +} = stm.reduceAll + +/** + * Folds an `Iterable` using an effectual function f, working sequentially + * from right to left. + * + * @since 2.0.0 + * @category constructors + */ +export const reduceRight: { + (zero: S, f: (s: S, a: A) => STM): (iterable: Iterable) => STM + (iterable: Iterable, zero: S, f: (s: S, a: A) => STM): STM +} = stm.reduceRight + +/** + * Keeps some of the errors, and terminates the fiber with the rest. + * + * @since 2.0.0 + * @category mutations + */ +export const refineOrDie: { + (pf: (error: E) => Option.Option): (self: STM) => STM + (self: STM, pf: (error: E) => Option.Option): STM +} = stm.refineOrDie + +/** + * Keeps some of the errors, and terminates the fiber with the rest, using the + * specified function to convert the `E` into a `Throwable`. + * + * @since 2.0.0 + * @category mutations + */ +export const refineOrDieWith: { + (pf: (error: E) => Option.Option, f: (error: E) => unknown): (self: STM) => STM + (self: STM, pf: (error: E) => Option.Option, f: (error: E) => unknown): STM +} = stm.refineOrDieWith + +/** + * Fail with the returned value if the `PartialFunction` matches, otherwise + * continue with our held value. + * + * @since 2.0.0 + * @category mutations + */ +export const reject: { + (pf: (a: A) => Option.Option): (self: STM) => STM + (self: STM, pf: (a: A) => Option.Option): STM +} = stm.reject + +/** + * Continue with the returned computation if the specified partial function + * matches, translating the successful match into a failure, otherwise continue + * with our held value. + * + * @since 2.0.0 + * @category mutations + */ +export const rejectSTM: { + (pf: (a: A) => Option.Option>): (self: STM) => STM + (self: STM, pf: (a: A) => Option.Option>): STM +} = stm.rejectSTM + +/** + * Repeats this `STM` effect until its result satisfies the specified + * predicate. + * + * **WARNING**: `repeatUntil` uses a busy loop to repeat the effect and will + * consume a thread until it completes (it cannot yield). This is because STM + * describes a single atomic transaction which must either complete, retry or + * fail a transaction before yielding back to the Effect runtime. + * - Use `retryUntil` instead if you don't need to maintain transaction + * state for repeats. + * - Ensure repeating the STM effect will eventually satisfy the predicate. + * + * @since 2.0.0 + * @category mutations + */ +export const repeatUntil: { + (predicate: Predicate): (self: STM) => STM + (self: STM, predicate: Predicate): STM +} = stm.repeatUntil + +/** + * Repeats this `STM` effect while its result satisfies the specified + * predicate. + * + * **WARNING**: `repeatWhile` uses a busy loop to repeat the effect and will + * consume a thread until it completes (it cannot yield). This is because STM + * describes a single atomic transaction which must either complete, retry or + * fail a transaction before yielding back to the Effect runtime. + * - Use `retryWhile` instead if you don't need to maintain transaction + * state for repeats. + * - Ensure repeating the STM effect will eventually not satisfy the + * predicate. + * + * @since 2.0.0 + * @category mutations + */ +export const repeatWhile: { + (predicate: Predicate): (self: STM) => STM + (self: STM, predicate: Predicate): STM +} = stm.repeatWhile + +/** + * Replicates the given effect n times. If 0 or negative numbers are given, an + * empty `Chunk` will be returned. + * + * @since 2.0.0 + * @category constructors + */ +export const replicate: { + (n: number): (self: STM) => Array> + (self: STM, n: number): Array> +} = stm.replicate + +/** + * Performs this transaction the specified number of times and collects the + * results. + * + * @since 2.0.0 + * @category constructors + */ +export const replicateSTM: { + (n: number): (self: STM) => STM, E, R> + (self: STM, n: number): STM, E, R> +} = stm.replicateSTM + +/** + * Performs this transaction the specified number of times, discarding the + * results. + * + * @since 2.0.0 + * @category constructors + */ +export const replicateSTMDiscard: { + (n: number): (self: STM) => STM + (self: STM, n: number): STM +} = stm.replicateSTMDiscard + +/** + * Abort and retry the whole transaction when any of the underlying + * transactional variables have changed. + * + * @since 2.0.0 + * @category error handling + */ +export const retry: STM = core.retry + +/** + * Filters the value produced by this effect, retrying the transaction until + * the predicate returns `true` for the value. + * + * @since 2.0.0 + * @category mutations + */ +export const retryUntil: { + (refinement: Refinement, B>): (self: STM) => STM + (predicate: Predicate): (self: STM) => STM + (self: STM, refinement: Refinement): STM + (self: STM, predicate: Predicate): STM +} = stm.retryUntil + +/** + * Filters the value produced by this effect, retrying the transaction while + * the predicate returns `true` for the value. + * + * @since 2.0.0 + * @category mutations + */ +export const retryWhile: { + (predicate: Predicate): (self: STM) => STM + (self: STM, predicate: Predicate): STM +} = stm.retryWhile + +/** + * Converts an option on values into an option on errors. + * + * @since 2.0.0 + * @category getters + */ +export const some: (self: STM, E, R>) => STM, R> = stm.some + +/** + * Returns an `STM` effect that succeeds with the specified value. + * + * @since 2.0.0 + * @category constructors + */ +export const succeed: (value: A) => STM = core.succeed + +/** + * Returns an effect with the empty value. + * + * @since 2.0.0 + * @category constructors + */ +export const succeedNone: STM> = stm.succeedNone + +/** + * Returns an effect with the optional value. + * + * @since 2.0.0 + * @category constructors + */ +export const succeedSome: (value: A) => STM> = stm.succeedSome + +/** + * Summarizes a `STM` effect by computing a provided value before and after + * execution, and then combining the values to produce a summary, together + * with the result of execution. + * + * @since 2.0.0 + * @category mutations + */ +export const summarized: { + ( + summary: STM, + f: (before: A2, after: A2) => A3 + ): (self: STM) => STM<[A3, A], E2 | E, R2 | R> + ( + self: STM, + summary: STM, + f: (before: A2, after: A2) => A3 + ): STM<[A3, A], E | E2, R | R2> +} = stm.summarized + +/** + * Suspends creation of the specified transaction lazily. + * + * @since 2.0.0 + * @category constructors + */ +export const suspend: (evaluate: LazyArg>) => STM = stm.suspend + +/** + * Returns an `STM` effect that succeeds with the specified lazily evaluated + * value. + * + * @since 2.0.0 + * @category constructors + */ +export const sync: (evaluate: () => A) => STM = core.sync + +/** + * "Peeks" at the success of transactional effect. + * + * @since 2.0.0 + * @category sequencing + */ +export const tap: { + (f: (a: A) => STM): (self: STM) => STM + (self: STM, f: (a: A) => STM): STM +} = stm.tap + +/** + * "Peeks" at both sides of an transactional effect. + * + * @since 2.0.0 + * @category sequencing + */ +export const tapBoth: { + ( + options: { readonly onFailure: (error: XE) => STM; readonly onSuccess: (value: XA) => STM } + ): (self: STM) => STM + ( + self: STM, + options: { readonly onFailure: (error: XE) => STM; readonly onSuccess: (value: XA) => STM } + ): STM +} = stm.tapBoth + +/** + * "Peeks" at the error of the transactional effect. + * + * @since 2.0.0 + * @category sequencing + */ +export const tapError: { + (f: (error: NoInfer) => STM): (self: STM) => STM + (self: STM, f: (error: E) => STM): STM +} = stm.tapError + +const try_: { + (options: { + readonly try: LazyArg + readonly catch: (u: unknown) => E + }): STM + (try_: LazyArg): STM +} = stm.try_ + +export { + /** + * Imports a synchronous side-effect into a pure value, translating any thrown + * exceptions into typed failed effects. + * + * @since 2.0.0 + * @category constructors + */ + try_ as try +} + +/** + * The moral equivalent of `if (!p) exp` + * + * @since 2.0.0 + * @category mutations + */ +export const unless: { + (predicate: LazyArg): (self: STM) => STM, E, R> + (self: STM, predicate: LazyArg): STM, E, R> +} = stm.unless + +/** + * The moral equivalent of `if (!p) exp` when `p` has side-effects + * + * @since 2.0.0 + * @category mutations + */ +export const unlessSTM: { + (predicate: STM): (self: STM) => STM, E2 | E, R2 | R> + (self: STM, predicate: STM): STM, E | E2, R | R2> +} = stm.unlessSTM + +/** + * Converts an option on errors into an option on values. + * + * @since 2.0.0 + * @category getters + */ +export const unsome: (self: STM, R>) => STM, E, R> = stm.unsome + +const void_: STM = stm.void +export { + /** + * Returns an `STM` effect that succeeds with `void`. + * + * @since 2.0.0 + * @category constructors + */ + void_ as void +} + +/** + * Feeds elements of type `A` to `f` and accumulates all errors in error + * channel or successes in success channel. + * + * This combinator is lossy meaning that if there are errors all successes + * will be lost. To retain all information please use `STM.partition`. + * + * @since 2.0.0 + * @category mutations + */ +export const validateAll: { + (f: (a: A) => STM): (elements: Iterable) => STM, [E, ...Array], R> + (elements: Iterable, f: (a: A) => STM): STM, [E, ...Array], R> +} = stm.validateAll + +/** + * Feeds elements of type `A` to `f` until it succeeds. Returns first success + * or the accumulation of all errors. + * + * @since 2.0.0 + * @category mutations + */ +export const validateFirst: { + (f: (a: A) => STM): (elements: Iterable) => STM, R> + (elements: Iterable, f: (a: A) => STM): STM, R> +} = stm.validateFirst + +/** + * The moral equivalent of `if (p) exp`. + * + * @since 2.0.0 + * @category mutations + */ +export const when: { + (predicate: LazyArg): (self: STM) => STM, E, R> + (self: STM, predicate: LazyArg): STM, E, R> +} = stm.when + +/** + * The moral equivalent of `if (p) exp` when `p` has side-effects. + * + * @since 2.0.0 + * @category mutations + */ +export const whenSTM: { + (predicate: STM): (self: STM) => STM, E2 | E, R2 | R> + (self: STM, predicate: STM): STM, E | E2, R | R2> +} = stm.whenSTM + +/** + * Sequentially zips this value with the specified one. + * + * @since 2.0.0 + * @category zipping + */ +export const zip: { + (that: STM): (self: STM) => STM<[A, A1], E1 | E, R1 | R> + (self: STM, that: STM): STM<[A, A1], E | E1, R | R1> +} = core.zip + +/** + * Sequentially zips this value with the specified one, discarding the second + * element of the tuple. + * + * @since 2.0.0 + * @category zipping + */ +export const zipLeft: { + (that: STM): (self: STM) => STM + (self: STM, that: STM): STM +} = core.zipLeft + +/** + * Sequentially zips this value with the specified one, discarding the first + * element of the tuple. + * + * @since 2.0.0 + * @category zipping + */ +export const zipRight: { + (that: STM): (self: STM) => STM + (self: STM, that: STM): STM +} = core.zipRight + +/** + * Sequentially zips this value with the specified one, combining the values + * using the specified combiner function. + * + * @since 2.0.0 + * @category zipping + */ +export const zipWith: { + ( + that: STM, + f: (a: A, b: A1) => A2 + ): (self: STM) => STM + (self: STM, that: STM, f: (a: A, b: A1) => A2): STM +} = core.zipWith + +/** + * This function takes an iterable of `STM` values and returns a new + * `STM` value that represents the first `STM` value in the iterable + * that succeeds. If all of the `Effect` values in the iterable fail, then + * the resulting `STM` value will fail as well. + * + * This function is sequential, meaning that the `STM` values in the + * iterable will be executed in sequence, and the first one that succeeds + * will determine the outcome of the resulting `STM` value. + * + * Returns a new `STM` value that represents the first successful + * `STM` value in the iterable, or a failed `STM` value if all of the + * `STM` values in the iterable fail. + * + * @since 2.0.0 + * @category elements + */ +export const firstSuccessOf = (effects: Iterable>): STM => + suspend(() => { + const list = Chunk.fromIterable(effects) + if (!Chunk.isNonEmpty(list)) { + return dieSync(() => new Cause.IllegalArgumentException(`Received an empty collection of effects`)) + } + return Chunk.reduce( + Chunk.tailNonEmpty(list), + Chunk.headNonEmpty(list), + (left, right) => orElse(left, () => right) + ) + }) + +/** + * @category do notation + * @since 2.0.0 + */ +export const Do: STM<{}> = succeed({}) + +/** + * @category do notation + * @since 2.0.0 + */ +export const bind: { + ( + tag: Exclude, + f: (_: NoInfer) => STM + ): (self: STM) => STM, E2 | E, R2 | R> + ( + self: STM, + tag: Exclude, + f: (_: NoInfer) => STM + ): STM, E | E2, R | R2> +} = stm.bind + +const let_: { + ( + tag: Exclude, + f: (_: NoInfer) => A + ): (self: STM) => STM, E, R> + ( + self: STM, + tag: Exclude, + f: (_: NoInfer) => A + ): STM, E, R> +} = stm.let_ + +export { + /** + * @category do notation + * @since 2.0.0 + */ + let_ as let +} + +/** + * @category do notation + * @since 2.0.0 + */ +export const bindTo: { + (tag: N): (self: STM) => STM, E, R> + (self: STM, tag: N): STM, E, R> +} = stm.bindTo diff --git a/repos/effect/packages/effect/src/Schedule.ts b/repos/effect/packages/effect/src/Schedule.ts new file mode 100644 index 0000000..a7414af --- /dev/null +++ b/repos/effect/packages/effect/src/Schedule.ts @@ -0,0 +1,2219 @@ +/** + * @since 2.0.0 + */ +import type * as Cause from "./Cause.js" +import type * as Chunk from "./Chunk.js" +import type * as Context from "./Context.js" +import type * as Cron from "./Cron.js" +import type * as DateTime from "./DateTime.js" +import type * as Duration from "./Duration.js" +import type * as Effect from "./Effect.js" +import type * as Either from "./Either.js" +import type { LazyArg } from "./Function.js" +import * as internal from "./internal/schedule.js" +import type * as Option from "./Option.js" +import type { Pipeable } from "./Pipeable.js" +import type { Predicate } from "./Predicate.js" +import type * as Ref from "./Ref.js" +import type * as ScheduleDecision from "./ScheduleDecision.js" +import type * as Intervals from "./ScheduleIntervals.js" +import type * as Types from "./Types.js" + +/** + * @since 2.0.0 + * @category Symbols + */ +export const ScheduleTypeId: unique symbol = internal.ScheduleTypeId + +/** + * @since 2.0.0 + * @category Symbols + */ +export type ScheduleTypeId = typeof ScheduleTypeId + +/** + * @since 2.0.0 + * @category Symbols + */ +export const ScheduleDriverTypeId: unique symbol = internal.ScheduleDriverTypeId + +/** + * @since 2.0.0 + * @category Symbols + */ +export type ScheduleDriverTypeId = typeof ScheduleDriverTypeId + +/** + * A `Schedule` defines a recurring schedule, which consumes values + * of type `In`, and which returns values of type `Out`. + * + * The `Schedule` type is structured as follows: + * + * ```ts skip-type-checking + * // ┌─── The type of output produced by the schedule + * // │ ┌─── The type of input consumed by the schedule + * // │ │ ┌─── Additional requirements for the schedule + * // ▼ ▼ ▼ + * Schedule + * ``` + * + * A schedule operates by consuming values of type `In` (such as errors in the + * case of `Effect.retry`, or values in the case of `Effect.repeat`) and + * producing values of type `Out`. It determines when to halt or continue the + * execution based on input values and its internal state. + * + * The inclusion of a `Requirements` parameter allows the schedule to leverage + * additional services or resources as needed. + * + * Schedules are defined as a possibly infinite set of intervals spread out over + * time. Each interval defines a window in which recurrence is possible. + * + * When schedules are used to repeat or retry effects, the starting boundary of + * each interval produced by a schedule is used as the moment when the effect + * will be executed again. + * + * Schedules can be composed in different ways: + * + * - Union: Combines two schedules and recurs if either schedule wants to + * continue, using the shorter delay. + * - Intersection: Combines two schedules and recurs only if both schedules want + * to continue, using the longer delay. + * - Sequencing: Combines two schedules by running the first one fully, then + * switching to the second. + * + * In addition, schedule inputs and outputs can be transformed, filtered (to + * terminate a schedule early in response to some input or output), and so + * forth. + * + * A variety of other operators exist for transforming and combining schedules, + * and the companion object for `Schedule` contains all common types of + * schedules, both for performing retrying, as well as performing repetition. + * + * @category Model + * @since 2.0.0 + */ +export interface Schedule extends Schedule.Variance, Pipeable { + /** + * Initial State + */ + readonly initial: any + /** + * Schedule Step + */ + step( + now: number, + input: In, + state: any + ): Effect.Effect +} + +/** + * @since 2.0.0 + */ +export declare namespace Schedule { + /** + * @since 2.0.0 + * @category Models + */ + export interface Variance { + readonly [ScheduleTypeId]: { + readonly _Out: Types.Covariant + readonly _In: Types.Contravariant + readonly _R: Types.Covariant + } + } + + /** + * @since 2.0.0 + */ + export interface DriverVariance { + readonly [ScheduleDriverTypeId]: { + readonly _Out: Types.Covariant + readonly _In: Types.Contravariant + readonly _R: Types.Covariant + } + } +} + +/** + * @since 2.0.0 + * @category Models + */ +export interface ScheduleDriver extends Schedule.DriverVariance { + readonly state: Effect.Effect + readonly iterationMeta: Ref.Ref + readonly last: Effect.Effect + readonly reset: Effect.Effect + next(input: In): Effect.Effect, R> +} + +/** + * Creates a new schedule with a custom state and step function. + * + * **Details** + * + * This function constructs a `Schedule` by defining its initial state and a + * step function, which determines how the schedule progresses over time. The + * step function is called on each iteration with the current time, an input + * value, and the schedule's current state. It returns the next state, an output + * value, and a decision on whether the schedule should continue or stop. + * + * This function is useful for creating custom scheduling logic that goes beyond + * predefined schedules like fixed intervals or exponential backoff. It allows + * full control over how the schedule behaves at each step. + * + * @since 2.0.0 + * @category Constructors + */ +export const makeWithState: ( + initial: S, + step: ( + now: number, + input: In, + state: S + ) => Effect.Effect +) => Schedule = internal.makeWithState + +/** + * Checks whether a given value is a `Schedule`. + * + * @since 2.0.0 + * @category Guards + */ +export const isSchedule: (u: unknown) => u is Schedule = internal.isSchedule + +/** + * Adds a delay to every interval in a schedule. + * + * **Details** + * + * This function modifies a given schedule by applying an additional delay to + * every interval it defines. The delay is determined by the provided function, + * which takes the schedule's output and returns a delay duration. + * + * @see {@link addDelayEffect} If you need to compute the delay using an effectful function. + * + * @since 2.0.0 + * @category Timing & Delay + */ +export const addDelay: { + (f: (out: Out) => Duration.DurationInput): (self: Schedule) => Schedule + (self: Schedule, f: (out: Out) => Duration.DurationInput): Schedule +} = internal.addDelay + +/** + * Adds an effectfully computed delay to every interval in a schedule. + * + * **Details** + * + * This function modifies a given schedule by applying an additional delay to + * each interval, where the delay is determined by an effectful function. The + * function takes the schedule’s output and returns an effect that produces a + * delay duration. + * + * @see {@link addDelay} If you need to compute the delay using a pure function. + * + * @since 2.0.0 + * @category Timing & Delay + */ +export const addDelayEffect: { + ( + f: (out: Out) => Effect.Effect + ): (self: Schedule) => Schedule + ( + self: Schedule, + f: (out: Out) => Effect.Effect + ): Schedule +} = internal.addDelayEffect + +/** + * Runs two schedules sequentially, merging their outputs. + * + * **Details** + * + * This function executes two schedules one after the other. The first schedule + * runs to completion, and then the second schedule begins execution. Unlike + * {@link andThenEither}, this function merges the outputs instead of wrapping + * them in `Either`, allowing both schedules to contribute their results + * directly. + * + * This is useful when a workflow consists of two phases where the second phase + * should start only after the first one has fully completed. + * + * @see {@link andThenEither} If you need to keep track of which schedule + * produced each result. + * + * @since 2.0.0 + * @category Sequential Composition + */ +export const andThen: { + ( + that: Schedule + ): (self: Schedule) => Schedule + ( + self: Schedule, + that: Schedule + ): Schedule +} = internal.andThen + +/** + * Runs two schedules sequentially, collecting results in an `Either`. + * + * **Details** + * + * This function combines two schedules in sequence. The first schedule runs to + * completion, and then the second schedule starts and runs to completion as + * well. The outputs of both schedules are collected into an `Either` structure: + * - `Either.Left` contains the output of the second schedule. + * - `Either.Right` contains the output of the first schedule. + * + * This is useful when you need to switch from one schedule to another after the + * first one finishes, while still keeping track of which schedule produced each + * result. + * + * @see {@link andThen} If you need to merge the outputs of both schedules. + * + * @since 2.0.0 + * @category Sequential Composition + */ +export const andThenEither: { + ( + that: Schedule + ): (self: Schedule) => Schedule, In & In2, R2 | R> + ( + self: Schedule, + that: Schedule + ): Schedule, In & In2, R | R2> +} = internal.andThenEither + +/** + * Transforms a schedule to always produce a constant output. + * + * **Details** + * + * This function modifies a given schedule so that instead of returning its + * computed outputs, it always returns a constant value. + * + * This is useful when you need a schedule for timing but don’t care about its + * actual output, or when you want to standardize results across different + * scheduling strategies. + * + * @since 2.0.0 + * @category Mapping + */ +export const as: { + (out: Out2): (self: Schedule) => Schedule + (self: Schedule, out: Out2): Schedule +} = internal.as + +/** + * Transforms a schedule to always return `void` instead of its output. + * + * **Details** + * + * This function modifies a given schedule so that it no longer returns + * meaningful output—each execution produces `void`. This is useful when the + * schedule is used only for timing purposes and the actual output of the + * schedule is irrelevant. + * + * The schedule still determines when executions should occur, but the results + * are discarded. + * + * @since 2.0.0 + * @category Mapping + */ +export const asVoid: (self: Schedule) => Schedule = internal.asVoid + +// TODO(4.0): rename to `zip`? +/** + * Combines two schedules, preserving both their inputs and outputs. + * + * **Details** + * + * This function merges two schedules so that both their input types and output + * types are retained. When executed, the resulting schedule will take inputs + * from both original schedules and produce a tuple containing both outputs. + * + * It recurs if either schedule wants to continue, using the shorter delay. + * + * This is useful when you want to track multiple schedules simultaneously, + * ensuring that both receive the same inputs and produce combined results. + * + * @since 2.0.0 + * @category Zipping + */ +export const bothInOut: { + ( + that: Schedule + ): (self: Schedule) => Schedule<[Out, Out2], readonly [In, In2], R2 | R> + ( + self: Schedule, + that: Schedule + ): Schedule<[Out, Out2], readonly [In, In2], R | R2> +} = internal.bothInOut + +/** + * Filters schedule executions based on a custom condition. + * + * **Details** + * + * This function modifies a schedule by applying a custom test function to each + * input-output pair. The test function determines whether the schedule should + * continue or stop. If the function returns `true`, the schedule proceeds as + * usual; if it returns `false`, the schedule terminates. + * + * This is useful for conditional retries, custom stop conditions, or + * dynamically controlling execution based on observed inputs and outputs. + * + * @see {@link checkEffect} If you need to use an effectful test function. + * + * @since 2.0.0 + * @category Recurrence Conditions + */ +export const check: { + (test: (input: In, output: Out) => boolean): (self: Schedule) => Schedule + (self: Schedule, test: (input: In, output: Out) => boolean): Schedule +} = internal.check + +/** + * Conditionally filters schedule executions using an effectful function. + * + * **Details** + * + * This function modifies a schedule by applying a custom effectful test + * function to each input-output pair. The test function determines whether the + * schedule should continue (`true`) or stop (`false`). + * + * This is useful when the decision to continue depends on external factors such + * as database lookups, API calls, or other asynchronous computations. + * + * @see {@link check} If you need to use a pure test function. + * + * @since 2.0.0 + * @category Recurrence Conditions + */ +export const checkEffect: { + ( + test: (input: In, output: Out) => Effect.Effect + ): (self: Schedule) => Schedule + ( + self: Schedule, + test: (input: In, output: Out) => Effect.Effect + ): Schedule +} = internal.checkEffect + +/** + * A schedule that collects all inputs into a `Chunk`. + * + * **Details** + * + * This function creates a schedule that never terminates and continuously + * collects every input it receives into a `Chunk`. Each time the schedule runs, + * it appends the new input to the collected list. + * + * This is useful when you need to track all received inputs over time, such as + * logging user actions, recording retry attempts, or accumulating data for + * later processing. + * + * @see {@link collectAllOutputs} If you need to collect outputs instead of + * inputs. + * + * @since 2.0.0 + * @category Collecting + */ +export const collectAllInputs: () => Schedule, A> = internal.collectAllInputs + +/** + * Collects all outputs of a schedule into a `Chunk`. + * + * **Details** + * + * This function modifies a given schedule so that instead of returning + * individual outputs, it accumulates them into a `Chunk`. The schedule + * continues to run, appending each output to the collected list. + * + * This is useful when you need to track all results over time, such as logging + * outputs, aggregating data, or keeping a history of previous values. + * + * @see {@link collectAllInputs} If you need to collect inputs instead of + * outputs. + * + * @since 2.0.0 + * @category Collecting + */ +export const collectAllOutputs: (self: Schedule) => Schedule, In, R> = + internal.collectAllOutputs + +/** + * Collects all inputs into a `Chunk` until a condition fails. + * + * **Details** + * + * This function creates a schedule that continuously collects inputs into a + * `Chunk` until the given predicate function `f` evaluates to `false`. Once the + * condition fails, the schedule stops. + * + * @since 2.0.0 + * @category Collecting + */ +export const collectUntil: (f: Predicate) => Schedule, A> = internal.collectUntil + +/** + * Collects all inputs into a `Chunk` until an effectful condition fails. + * + * **Details** + * + * This function creates a schedule that continuously collects inputs into a + * `Chunk` until the given effectful predicate `f` returns `false`. The + * predicate runs as an effect, meaning it can involve asynchronous computations + * like API calls, database lookups, or randomness. + * + * @since 2.0.0 + * @category Collecting + */ +export const collectUntilEffect: ( + f: (a: A) => Effect.Effect +) => Schedule, A, R> = internal.collectUntilEffect + +/** + * Collects all inputs into a `Chunk` while a condition holds. + * + * **Details** + * + * This function creates a schedule that continuously collects inputs into a + * `Chunk` while the given predicate function `f` evaluates to `true`. As soon + * as the condition fails, the schedule stops. + * + * @since 2.0.0 + * @category Collecting + */ +export const collectWhile: (f: Predicate) => Schedule, A> = internal.collectWhile + +/** + * Collects all inputs into a `Chunk` while an effectful condition holds. + * + * **Details** + * + * This function creates a schedule that continuously collects inputs into a + * `Chunk` while the given effectful predicate `f` returns `true`. The predicate + * returns an effect, meaning it can depend on external state, such as database + * queries, API responses, or real-time user conditions. + * + * As soon as the effectful condition returns `false`, the schedule stops. This + * is useful for dynamically controlled data collection, where stopping depends + * on an external or asynchronous factor. + * + * @since 2.0.0 + * @category Collecting + */ +export const collectWhileEffect: ( + f: (a: A) => Effect.Effect +) => Schedule, A, R> = internal.collectWhileEffect + +/** + * Chains two schedules, passing the output of the first as the input to the + * second, while selecting the shorter delay between them. + * + * **Details** + * + * This function composes two schedules so that the output of the first schedule + * becomes the input of the second schedule. The first schedule executes first, + * and once it produces a result, the second schedule receives that result and + * continues execution based on it. + * + * This is useful for building complex scheduling workflows where one schedule's + * behavior determines how the next schedule behaves. + * + * @since 2.0.0 + * @category Composition + */ +export const compose: { + (that: Schedule): (self: Schedule) => Schedule + (self: Schedule, that: Schedule): Schedule +} = internal.compose + +/** + * Transforms the input type of a schedule. + * + * **Details** + * + * This function modifies a given schedule by applying a transformation function + * to its inputs. Instead of directly receiving values of type `In`, the + * schedule will now accept values of type `In2`, which are converted to `In` + * using the provided mapping function `f`. + * + * This is useful when you have a schedule that expects a specific input type + * but you need to adapt it to work with a different type. + * + * @see {@link mapInputEffect} If you need to use an effectful transformation function. + * + * @since 2.0.0 + * @category Mapping + */ +export const mapInput: { + (f: (in2: In2) => In): (self: Schedule) => Schedule + (self: Schedule, f: (in2: In2) => In): Schedule +} = internal.mapInput + +/** + * Transforms the input type of a schedule using an effectful function. + * + * **Details** + * + * This function modifies a schedule by applying an effectful transformation to + * its inputs. Instead of directly receiving values of type `In`, the schedule + * will now accept values of type `In2`, which are converted to `In` via an + * effectful function `f`. + * + * This is useful when the input transformation involves external dependencies, + * such as API calls, database lookups, or other asynchronous computations. + * + * @see {@link mapInput} If you need to use a pure transformation function. + * + * @since 2.0.0 + * @category Mapping + */ +export const mapInputEffect: { + ( + f: (in2: In2) => Effect.Effect + ): (self: Schedule) => Schedule + ( + self: Schedule, + f: (in2: In2) => Effect.Effect + ): Schedule +} = internal.mapInputEffect + +/** + * Transforms the required context of a schedule. + * + * **Details** + * + * This function modifies a schedule by mapping its required context (`R`) into + * a new context (`R0`) using the provided function `f`. + * + * This is useful when you need to adapt a schedule to work with a different + * dependency environment without changing its core logic. + * + * @since 2.0.0 + * @category Mapping + */ +export const mapInputContext: { + ( + f: (env0: Context.Context) => Context.Context + ): (self: Schedule) => Schedule + ( + self: Schedule, + f: (env0: Context.Context) => Context.Context + ): Schedule +} = internal.mapInputContext + +/** + * A schedule that recurs indefinitely, counting the number of recurrences. + * + * **Details** + * + * This schedule never stops and simply counts how many times it has executed. + * Each recurrence increases the count, starting from `0`. + * + * This is useful when tracking the number of attempts in retry policies, + * measuring execution loops, or implementing infinite polling scenarios. + * + * @since 2.0.0 + * @category Constructors + */ +export const count: Schedule = internal.count + +/** + * Creates a schedule that recurs based on a cron expression. + * + * **Details** + * + * This schedule automatically executes at intervals defined by a cron + * expression. It triggers at the beginning of each matched interval and + * produces timestamps representing the start and end of the cron window. + * + * The cron `expression` is validated lazily, meaning errors may only be + * detected when the schedule is executed. + * + * @since 2.0.0 + * @category Cron + */ +export const cron: { + (cron: Cron.Cron): Schedule<[number, number]> + (expression: string, tz?: DateTime.TimeZone | string): Schedule<[number, number]> +} = internal.cron + +/** + * Cron-like schedule that recurs at a specific second of each minute. + * + * **Details** + * + * This schedule triggers at the specified `second` of each minute, + * starting at zero nanoseconds. It produces a count of executions + * (0, 1, 2, ...). The `second` parameter is validated lazily, meaning + * invalid values will only be caught at runtime. + * + * @since 2.0.0 + * @category Cron + */ +export const secondOfMinute: (second: number) => Schedule = internal.secondOfMinute + +/** + * Creates a schedule that recurs every specified minute of each hour. + * + * **Details** + * + * This schedule triggers once per hour at the specified `minute`, starting + * exactly at `minute:00` (zero seconds). The schedule produces a count of + * executions (`0, 1, 2, ...`), representing how many times it has run. + * + * The `minute` parameter must be between `0` and `59`. It is validated lazily, + * meaning an invalid value will cause errors only when the schedule is + * executed. + * + * @since 2.0.0 + * @category Cron + */ +export const minuteOfHour: (minute: number) => Schedule = internal.minuteOfHour + +/** + * Creates a schedule that recurs at a specific hour of each day. + * + * **Details** + * + * This schedule triggers once per day at the specified `hour`, starting at zero + * minutes of that hour. The schedule produces a count of executions (`0, 1, 2, + * ...`), indicating how many times it has been triggered. + * + * The `hour` parameter must be between `0` (midnight) and `23` (11 PM). It is + * validated lazily, meaning an invalid value will cause errors only when the + * schedule is executed. + * + * This is useful for scheduling daily recurring tasks at a fixed time, such as + * running batch jobs or refreshing data. + * + * @since 2.0.0 + * @category Cron + */ +export const hourOfDay: (hour: number) => Schedule = internal.hourOfDay + +/** + * Creates a schedule that recurs on a specific day of the month. + * + * **Details** + * + * This schedule triggers at midnight on the specified day of each month. It + * will not execute in months that have fewer days than the given day. For + * example, if the schedule is set to run on the 31st, it will not execute in + * months with only 30 days. + * + * The schedule produces a count of executions, starting at 0 and incrementing + * with each recurrence. + * + * The `day` parameter is validated lazily, meaning errors may only be detected + * when the schedule is executed. + * + * @since 2.0.0 + * @category Cron + */ +export const dayOfMonth: (day: number) => Schedule = internal.dayOfMonth + +/** + * Creates a schedule that recurs on a specific day of the week. + * + * **Details** + * + * This schedule triggers at midnight on the specified day of the week. The + * `day` parameter follows the standard convention where `Monday = 1` and + * `Sunday = 7`. The schedule produces a count of executions, starting at 0 and + * incrementing with each recurrence. + * + * The `day` parameter is validated lazily, meaning errors may only be detected + * when the schedule is executed. + * + * @since 2.0.0 + * @category Cron + */ +export const dayOfWeek: (day: number) => Schedule = internal.dayOfWeek + +/** + * Modifies a schedule by adding a computed delay before each execution. + * + * **Details** + * + * This function adjusts an existing schedule by applying a transformation to + * its delays. Instead of using the default interval, each delay is modified + * using the provided function `f`, which takes the current delay and returns a + * new delay. + * + * This is useful for dynamically adjusting wait times between executions, such + * as introducing jitter, exponential backoff, or custom delay logic. + * + * @see {@link delayedEffect} If you need to compute the delay using an effectful function. + * + * @since 2.0.0 + * @category Timing & Delay + */ +export const delayed: { + ( + f: (duration: Duration.Duration) => Duration.DurationInput + ): (self: Schedule) => Schedule + ( + self: Schedule, + f: (duration: Duration.Duration) => Duration.DurationInput + ): Schedule +} = internal.delayed + +/** + * Modifies a schedule by adding an effectfully computed delay before each + * execution. + * + * **Details** + * + * This function adjusts an existing schedule by introducing a delay that is + * computed via an effect. Instead of using a fixed delay, each interval is + * dynamically adjusted based on an effectful function `f`, which takes the + * current delay and returns a new delay wrapped in an `Effect`. + * + * This is useful for adaptive scheduling where delays depend on external + * factors, such as API calls, database queries, or dynamic system conditions. + * + * @see {@link delayed} If you need to compute the delay using a pure function. + * + * @since 2.0.0 + * @category Timing & Delay + */ +export const delayedEffect: { + ( + f: (duration: Duration.Duration) => Effect.Effect + ): (self: Schedule) => Schedule + ( + self: Schedule, + f: (duration: Duration.Duration) => Effect.Effect + ): Schedule +} = internal.delayedEffect + +/** + * Uses the delays produced by a schedule to further delay its intervals. + * + * **Details** + * + * This function modifies a schedule by using its own output delays to control + * its execution timing. Instead of executing immediately at each interval, the + * schedule will be delayed by the duration it produces. + * + * @since 2.0.0 + * @category Timing & Delay + */ +export const delayedSchedule: ( + schedule: Schedule +) => Schedule = internal.delayedSchedule + +/** + * Transforms a schedule to output the delay between each occurrence. + * + * **Details** + * + * This function modifies an existing schedule so that instead of producing its + * original output, it now returns the delay between each scheduled execution. + * + * @since 2.0.0 + * @category Monitoring + */ +export const delays: (self: Schedule) => Schedule = internal.delays + +/** + * Transforms both the input and output of a schedule. + * + * **Details** + * + * This function modifies an existing schedule by applying a transformation to + * both its input values and its output values. The provided transformation + * functions `onInput` and `onOutput` allow you to map the schedule to work with + * a different input type while modifying its outputs as well. + * + * @see {@link mapBothEffect} If you need to use effectful transformation functions. + * + * @since 2.0.0 + * @category Mapping + */ +export const mapBoth: { + ( + options: { readonly onInput: (in2: In2) => In; readonly onOutput: (out: Out) => Out2 } + ): (self: Schedule) => Schedule + ( + self: Schedule, + options: { readonly onInput: (in2: In2) => In; readonly onOutput: (out: Out) => Out2 } + ): Schedule +} = internal.mapBoth + +/** + * Transforms both the input and output of a schedule using effectful + * computations. + * + * **Details** + * + * This function modifies an existing schedule by applying effectful + * transformations to both its input values and its output values. The provided + * effectful functions `onInput` and `onOutput` allow you to transform inputs + * and outputs using computations that may involve additional logic, resource + * access, or side effects. + * + * @see {@link mapBoth} If you need to use pure transformation functions. + * + * @since 2.0.0 + * @category Mapping + */ +export const mapBothEffect: { + ( + options: { + readonly onInput: (input: In2) => Effect.Effect + readonly onOutput: (out: Out) => Effect.Effect + } + ): (self: Schedule) => Schedule + ( + self: Schedule, + options: { + readonly onInput: (input: In2) => Effect.Effect + readonly onOutput: (out: Out) => Effect.Effect + } + ): Schedule +} = internal.mapBothEffect + +/** + * Creates a driver to manually control the execution of a schedule. + * + * **Details** + * + * This function returns a `ScheduleDriver`, which allows stepping through a + * schedule manually while handling delays and sleeping appropriately. A driver + * is useful when you need fine-grained control over how a schedule progresses, + * rather than relying on automatic execution. + * + * The returned driver exposes methods for retrieving the current state, + * executing the next step, and resetting the schedule when needed. + * + * @since 2.0.0 + * @category getter + */ +export const driver: ( + self: Schedule +) => Effect.Effect> = internal.driver + +// TODO(4.0): remove? +/** + * Alias of {@link fromDelay}. + * + * @since 2.0.0 + * @category Constructors + */ +export const duration: (duration: Duration.DurationInput) => Schedule = internal.duration + +// TODO(4.0): remove? +/** + * Alias of {@link union}. + * + * @since 2.0.0 + * @category Alternatives + */ +export const either: { + ( + that: Schedule + ): (self: Schedule) => Schedule<[Out, Out2], In & In2, R2 | R> + ( + self: Schedule, + that: Schedule + ): Schedule<[Out, Out2], In & In2, R | R2> +} = internal.either + +// TODO(4.0): remove? +/** + * Alias of {@link unionWith}. + * + * @since 2.0.0 + * @category Alternatives + */ +export const eitherWith: { + ( + that: Schedule, + f: (x: Intervals.Intervals, y: Intervals.Intervals) => Intervals.Intervals + ): (self: Schedule) => Schedule<[Out, Out2], In & In2, R2 | R> + ( + self: Schedule, + that: Schedule, + f: (x: Intervals.Intervals, y: Intervals.Intervals) => Intervals.Intervals + ): Schedule<[Out, Out2], In & In2, R | R2> +} = internal.eitherWith + +/** + * Creates a schedule that tracks the total elapsed duration since it started. + * + * **Details** + * + * This schedule executes continuously and returns the total time that has + * passed since the first execution. The duration keeps increasing with each + * step, providing a way to measure elapsed time. + * + * This is useful for tracking execution time, monitoring delays, or + * implementing logic based on how long a process has been running. + * + * @since 2.0.0 + * @category Constructors + */ +export const elapsed: Schedule = internal.elapsed + +/** + * Attaches a finalizer to a schedule that runs when the schedule completes. + * + * **Details** + * + * This function returns a new schedule that executes a given finalizer when the + * schedule reaches completion. Unlike `Effect.ensuring`, this method does not + * guarantee the finalizer will run in all cases. If the schedule never + * initializes or is not driven to completion, the finalizer may not execute. + * However, if the schedule decides not to continue, the finalizer will be + * invoked. + * + * This is useful for cleaning up resources, logging, or executing other side + * effects when a schedule completes. + * + * @since 2.0.0 + * @category Finalization + */ +export const ensuring: { + (finalizer: Effect.Effect): (self: Schedule) => Schedule + (self: Schedule, finalizer: Effect.Effect): Schedule +} = internal.ensuring + +/** + * Creates a schedule that recurs indefinitely with exponentially increasing + * delays. + * + * **Details** + * + * This schedule starts with an initial delay of `base` and increases the delay + * exponentially on each repetition using the formula `base * factor^n`, where + * `n` is the number of times the schedule has executed so far. If no `factor` + * is provided, it defaults to `2`, causing the delay to double after each + * execution. + * + * @since 2.0.0 + * @category Constructors + */ +export const exponential: ( + base: Duration.DurationInput, + factor?: number +) => Schedule = internal.exponential + +/** + * Creates a schedule that recurs indefinitely with Fibonacci-based increasing + * delays. + * + * **Details** + * + * This schedule starts with an initial delay of `one` and increases subsequent + * delays by summing the two previous delays, following the Fibonacci sequence. + * The delay pattern follows: `one, one, one + one, (one + one) + one, ...`, + * resulting in `1s, 1s, 2s, 3s, 5s, 8s, 13s, ...` if `one = 1s`. + * + * This is useful for progressive backoff strategies, where delays grow + * naturally over time without increasing as aggressively as an exponential + * schedule. + * + * @since 2.0.0 + * @category Constructors + */ +export const fibonacci: (one: Duration.DurationInput) => Schedule = internal.fibonacci + +/** + * Creates a schedule that recurs at a fixed interval. + * + * **Details** + * + * This schedule executes at regular, evenly spaced intervals, returning the + * number of times it has run so far. If the action being executed takes longer + * than the interval, the next execution will happen immediately to prevent + * "pile-ups," ensuring that the schedule remains consistent without overlapping + * executions. + * + * ```text + * |-----interval-----|-----interval-----|-----interval-----| + * |---------action--------||action|-----|action|-----------| + * ``` + * + * @see {@link spaced} If you need to run from the end of the last execution. + * + * @since 2.0.0 + * @category Constructors + */ +export const fixed: (interval: Duration.DurationInput) => Schedule = internal.fixed + +/** + * Creates a schedule that recurs indefinitely, producing a count of + * repetitions. + * + * **Details** + * + * This schedule runs indefinitely, returning an increasing count of executions + * (`0, 1, 2, 3, ...`). Each step increments the count by one, allowing tracking + * of how many times it has executed. + * + * @since 2.0.0 + * @category Constructors + */ +export const forever: Schedule = internal.forever + +/** + * Creates a schedule that recurs once after a specified duration. + * + * **Details** + * + * This schedule executes a single time after waiting for the given duration. + * Once it has executed, it does not repeat. + * + * @see {@link fromDelays} If you need to create a schedule with multiple delays. + * + * @since 2.0.0 + * @category Constructors + */ +export const fromDelay: (delay: Duration.DurationInput) => Schedule = internal.fromDelay + +/** + * Creates a schedule that recurs once for each specified duration, applying the + * given delays sequentially. + * + * **Details** + * + * This schedule executes multiple times, each time waiting for the + * corresponding duration from the provided list of delays. The first execution + * waits for `delay`, the next for the second value in `delays`, and so on. Once + * all delays have been used, the schedule stops executing. + * + * This is useful for defining a custom delay sequence that does not follow a + * fixed pattern like exponential or Fibonacci backoff. + * + * @since 2.0.0 + * @category Constructors + */ +export const fromDelays: ( + delay: Duration.DurationInput, + ...delays: Array +) => Schedule = internal.fromDelays + +/** + * Creates a schedule that always recurs, transforming input values using the + * specified function. + * + * **Details** + * + * This schedule continuously executes and applies the given function `f` to + * each input value, producing a transformed output. The schedule itself does + * not control delays or stopping conditions; it simply transforms the input + * values as they are processed. + * + * This is useful when defining schedules that map inputs to outputs, allowing + * dynamic transformations of incoming data. + * + * @since 2.0.0 + * @category Constructors + */ +export const fromFunction: (f: (a: A) => B) => Schedule = internal.fromFunction + +/** + * Creates a schedule that always recurs, passing inputs directly as outputs. + * + * **Details** + * + * This schedule runs indefinitely, returning each input value as its output + * without modification. It effectively acts as a pass-through that simply + * echoes its input values at each step. + * + * @since 2.0.0 + * @category Constructors + */ +export const identity: () => Schedule = internal.identity + +/** + * Transforms a schedule to pass through its inputs as outputs. + * + * **Details** + * + * This function modifies an existing schedule so that it returns its input + * values instead of its original output values. The schedule's timing remains + * unchanged, but its outputs are replaced with whatever inputs it receives. + * + * @since 2.0.0 + */ +export const passthrough: (self: Schedule) => Schedule = internal.passthrough + +/** + * Combines two schedules, continuing only if both schedules want to continue, + * using the longer delay. + * + * **Details** + * + * This function takes two schedules and creates a new schedule that only + * continues execution if both schedules allow it. The interval between + * recurrences is determined by the longer delay between the two schedules. + * + * The output of the resulting schedule is a tuple containing the outputs of + * both schedules. The input type is the intersection of both schedules' input + * types. + * + * This is useful when coordinating multiple scheduling conditions where + * execution should proceed only when both schedules permit it. + * + * @see {@link intersectWith} If you need to use a custom merge function. + * + * @since 2.0.0 + * @category Composition + */ +export const intersect: { + ( + that: Schedule + ): (self: Schedule) => Schedule<[Out, Out2], In & In2, R2 | R> + ( + self: Schedule, + that: Schedule + ): Schedule<[Out, Out2], In & In2, R | R2> +} = internal.intersect + +/** + * Combines two schedules, continuing only if both want to continue, merging + * intervals using a custom function. + * + * **Details** + * + * This function takes two schedules and creates a new schedule that only + * continues execution if both schedules allow it. Instead of automatically + * using the longer delay (like {@link intersect}), this function applies a + * user-provided merge function `f` to determine the next interval between + * executions. + * + * The output of the resulting schedule is a tuple containing the outputs of + * both schedules, and the input type is the intersection of both schedules' + * input types. + * + * @since 2.0.0 + * @category Composition + */ +export const intersectWith: { + ( + that: Schedule, + f: (x: Intervals.Intervals, y: Intervals.Intervals) => Intervals.Intervals + ): (self: Schedule) => Schedule<[Out, Out2], In & In2, R2 | R> + ( + self: Schedule, + that: Schedule, + f: (x: Intervals.Intervals, y: Intervals.Intervals) => Intervals.Intervals + ): Schedule<[Out, Out2], In & In2, R | R2> +} = internal.intersectWith + +/** + * Returns a new schedule that randomly adjusts the interval size within a + * range. + * + * **Details** + * + * This function modifies a schedule so that its delay between executions is + * randomly varied within a range. By default, the delay is adjusted between + * `80%` (`0.8 * interval`) and `120%` (`1.2 * interval`) of the original + * interval size. + * + * This is useful for adding randomness to repeated executions, reducing + * contention in distributed systems, and avoiding synchronized execution + * patterns that can cause bottlenecks. + * + * @see {@link jitteredWith} If you need to specify custom min/max values. + * + * @since 2.0.0 + * @category Timing & Delay + */ +export const jittered: (self: Schedule) => Schedule = internal.jittered + +/** + * Returns a new schedule that randomly adjusts the interval size within a + * user-defined range. + * + * **Details** + * + * This function modifies a schedule so that its delay between executions is + * randomly varied within a specified range. Instead of using the default `0.8 - + * 1.2` range like {@link jittered}, this function allows customizing the `min` + * and `max` multipliers. + * + * The delay for each step will be adjusted within `min * original_interval` and + * `max * original_interval`. If `min` and `max` are not provided, the defaults + * are `0.8` and `1.2`, respectively. + * + * This is useful for introducing randomness into scheduling behavior while + * having precise control over the jitter range. + * + * @since 2.0.0 + * @category Timing & Delay + */ +export const jitteredWith: { + ( + options: { min?: number | undefined; max?: number | undefined } + ): (self: Schedule) => Schedule + ( + self: Schedule, + options: { min?: number | undefined; max?: number | undefined } + ): Schedule +} = internal.jitteredWith + +/** + * Creates a schedule that recurs indefinitely, increasing the delay linearly. + * + * **Details** + * + * This schedule starts with an initial delay of `base` and increases the delay + * on each recurrence in a linear fashion, following the formula: + * + * `delay = base * n` + * + * where `n` is the number of times the schedule has executed so far. This + * results in increasing intervals between executions. + * + * This is useful for implementing linear backoff strategies where the wait time + * between retries increases at a steady rate. + * + * @since 2.0.0 + * @category Constructors + */ +export const linear: (base: Duration.DurationInput) => Schedule = internal.linear + +/** + * Returns a new schedule that transforms its output using the specified + * function. + * + * **Details** + * + * This function modifies an existing schedule so that its outputs are + * transformed by the provided function `f`. The timing and recurrence behavior + * of the schedule remain unchanged, but the values it produces are mapped to + * new values. + * + * This is useful when composing schedules where you need to adjust the output + * format or apply additional processing. + * + * @see {@link mapEffect} If you need to use an effectful transformation + * function. + * + * @since 2.0.0 + * @category Mapping + */ +export const map: { + (f: (out: Out) => Out2): (self: Schedule) => Schedule + (self: Schedule, f: (out: Out) => Out2): Schedule +} = internal.map + +/** + * Returns a new schedule that applies an effectful transformation to its + * output. + * + * **Details** + * + * This function modifies an existing schedule by applying an effectful function + * `f` to its output values. The timing and recurrence behavior of the schedule + * remain unchanged, but each output is mapped to a new value within an + * `Effect`. + * + * This is useful when you need to perform side effects or asynchronous + * transformations before passing the output forward. + * + * @see {@link map} If you need to use a pure transformation function. + * + * @since 2.0.0 + * @category Mapping + */ +export const mapEffect: { + ( + f: (out: Out) => Effect.Effect + ): (self: Schedule) => Schedule + ( + self: Schedule, + f: (out: Out) => Effect.Effect + ): Schedule +} = internal.mapEffect + +/** + * Returns a new schedule that modifies the delay between executions using a + * custom function. + * + * **Details** + * + * This function transforms an existing schedule by applying `f` to modify the + * delay before each execution. The function receives both the schedule's output + * (`out`) and the originally computed delay (`duration`), and returns a new + * adjusted delay. + * + * @see {@link modifyDelayEffect} If you need to use an effectful function. + * + * @since 2.0.0 + * @category Timing & Delay + */ +export const modifyDelay: { + ( + f: (out: Out, duration: Duration.Duration) => Duration.DurationInput + ): (self: Schedule) => Schedule + ( + self: Schedule, + f: (out: Out, duration: Duration.Duration) => Duration.DurationInput + ): Schedule +} = internal.modifyDelay + +/** + * Returns a new schedule that modifies the delay before execution using an + * effectful function. + * + * **Details** + * + * This function takes an existing schedule and applies an effectful function + * `f` to dynamically adjust the delay before each execution. The function + * receives both the schedule's output (`out`) and the originally computed delay + * (`duration`), returning a new adjusted delay wrapped in an `Effect`. + * + * @see {@link modifyDelay} If you need to use a pure function. + * + * @since 2.0.0 + * @category Timing & Delay + */ +export const modifyDelayEffect: { + ( + f: (out: Out, duration: Duration.Duration) => Effect.Effect + ): (self: Schedule) => Schedule + ( + self: Schedule, + f: (out: Out, duration: Duration.Duration) => Effect.Effect + ): Schedule +} = internal.modifyDelayEffect + +/** + * Returns a new schedule that executes an effect every time the schedule makes + * a decision. + * + * **Details** + * + * This function enhances an existing schedule by running an effectful function + * `f` whenever a scheduling decision is made. The function receives the current + * schedule output (`out`) and the decision (`ScheduleDecision`), allowing + * additional logic to be executed, such as logging, monitoring, or side + * effects. + * + * @since 2.0.0 + */ +export const onDecision: { + ( + f: (out: Out, decision: ScheduleDecision.ScheduleDecision) => Effect.Effect + ): (self: Schedule) => Schedule + ( + self: Schedule, + f: (out: Out, decision: ScheduleDecision.ScheduleDecision) => Effect.Effect + ): Schedule +} = internal.onDecision + +/** + * A schedule that executes only once and then stops. + * + * **Details** + * + * This schedule triggers a single execution and then terminates. It does not + * repeat or apply any additional logic. + * + * @since 2.0.0 + * @category Constructors + */ +export const once: Schedule = internal.once + +/** + * Returns a new schedule with a provided context, eliminating the need for + * external dependencies. + * + * **Details** + * + * This function supplies a required `context` to a schedule, allowing it to run + * without requiring external dependencies. After calling this function, the + * schedule can be used freely without needing to pass a context at execution + * time. + * + * This is useful when working with schedules that rely on contextual + * information, such as logging services, database connections, or configuration + * settings. + * + * @since 2.0.0 + * @category Context + */ +export const provideContext: { + (context: Context.Context): (self: Schedule) => Schedule + (self: Schedule, context: Context.Context): Schedule +} = internal.provideContext + +/** + * Returns a new schedule with a single required service provided, eliminating + * the need for external dependencies. + * + * **Details** + * + * This function supplies a single service dependency to a schedule, allowing it + * to run without requiring that service externally. If a schedule depends on + * multiple services, consider using `provideContext` instead. + * + * This is useful when working with schedules that require a specific service, + * such as logging, metrics, or configuration retrieval. + * + * @since 2.0.0 + * @category Context + */ +export const provideService: { + ( + tag: Context.Tag, + service: Types.NoInfer + ): (self: Schedule) => Schedule> + ( + self: Schedule, + tag: Context.Tag, + service: Types.NoInfer + ): Schedule> +} = internal.provideService + +/** + * A schedule that recurs until the given predicate evaluates to true. + * + * **Details** + * + * This schedule will continue executing as long as the provided predicate `f` + * returns `false` for the input value. Once `f` evaluates to `true`, the + * schedule stops recurring. + * + * This is useful for defining schedules that should stop when a certain + * condition is met, such as detecting a success state, reaching a threshold, or + * avoiding unnecessary retries. + * + * @see {@link recurUntilEffect} If you need to use an effectful predicate. + * + * @since 2.0.0 + * @category Recurrence Conditions + */ +export const recurUntil: (f: Predicate) => Schedule = internal.recurUntil + +/** + * A schedule that recurs until the given effectful predicate evaluates to true. + * + * **Details** + * + * This schedule continues executing as long as the provided effectful predicate + * `f` returns `false`. Once `f` evaluates to `true`, the schedule stops + * recurring. Unlike {@link recurUntil}, this function allows the stopping + * condition to be computed asynchronously or based on external dependencies. + * + * This is useful when the stopping condition depends on an effectful + * computation, such as checking a database, making an API call, or retrieving + * system state dynamically. + * + * @see {@link recurUntil} If you need to use a pure predicate. + * + * @since 2.0.0 + * @category Recurrence Conditions + */ +export const recurUntilEffect: (f: (a: A) => Effect.Effect) => Schedule = + internal.recurUntilEffect + +/** + * A schedule that recurs until the input value matches a partial function, then + * maps the value. + * + * **Details** + * + * This schedule continues executing until the provided partial function `pf` + * returns `Some(value)`. At that point, it stops and maps the resulting value + * to an `Option`. If `pf` returns `None`, the schedule continues. + * + * This is useful when defining schedules that should stop once a certain + * condition is met and transform the final value before completion. + * + * @since 2.0.0 + * @category Recurrence Conditions + */ +export const recurUntilOption: (pf: (a: A) => Option.Option) => Schedule, A> = + internal.recurUntilOption + +/** + * A schedule that recurs until the specified duration has elapsed. + * + * **Details** + * + * This schedule continues executing for the given `duration`, after which it + * stops. The schedule outputs the elapsed time on each recurrence. + * + * This is useful for limiting the duration of retries, enforcing time-based + * constraints, or ensuring that an operation does not run indefinitely. + * + * @since 2.0.0 + * @category Recurrence Conditions + */ +export const recurUpTo: (duration: Duration.DurationInput) => Schedule = internal.recurUpTo + +/** + * A schedule that recurs as long as the given predicate evaluates to true. + * + * **Details* + * + * This schedule continues executing as long as the provided predicate `f` + * returns `true` for the input value. Once `f` evaluates to `false`, the + * schedule stops recurring. + * + * @see {@link recurWhileEffect} If you need to use an effectful predicate. + * + * @since 2.0.0 + * @category Recurrence Conditions + */ +export const recurWhile: (f: Predicate) => Schedule = internal.recurWhile + +/** + * A schedule that recurs as long as the given effectful predicate evaluates to + * true. + * + * **Details** + * + * This schedule continues executing as long as the provided effectful predicate + * `f` returns `true`. Once `f` evaluates to `false`, the schedule stops + * recurring. Unlike {@link recurWhile}, this function allows the condition to + * be computed dynamically using an effectful computation. + * + * @see {@link recurWhile} If you need to use a pure predicate. + * + * @since 2.0.0 + * @category Recurrence Conditions + */ +export const recurWhileEffect: (f: (a: A) => Effect.Effect) => Schedule = + internal.recurWhileEffect + +/** + * A schedule that recurs a fixed number of times before terminating. + * + * **Details** + * + * This schedule will continue executing until it has been stepped `n` times, + * after which it will stop. The output of the schedule is the current count of + * recurrences. + * + * @category Constructors + * @since 2.0.0 + */ +export const recurs: (n: number) => Schedule = internal.recurs + +/** + * Returns a new schedule that folds over the outputs of this one. + * + * **Details** + * + * This schedule transforms the output by accumulating values over time using a + * reducer function `f`. It starts with an initial value `zero` and updates it + * each time the schedule produces an output. + * + * This is useful for tracking statistics, aggregating results, or summarizing + * data across multiple executions. + * + * @see {@link reduceEffect} If you need to use an effectful reducer function. + * + * @since 2.0.0 + * @category Reducing + */ +export const reduce: { + (zero: Z, f: (z: Z, out: Out) => Z): (self: Schedule) => Schedule + (self: Schedule, zero: Z, f: (z: Z, out: Out) => Z): Schedule +} = internal.reduce + +/** + * Returns a new schedule that effectfully folds over the outputs of this one. + * + * **Details** + * + * This schedule accumulates outputs over time using an effectful reducer + * function `f`. It starts with an initial value `zero` and updates it + * asynchronously or based on external dependencies. + * + * This is useful for asynchronous state tracking, logging, external metrics + * aggregation, or any scenario where accumulation needs to involve an effectful + * computation. + * + * @see {@link reduce} If you need to use a pure reducer function. + * + * @since 2.0.0 + * @category Reducing + */ +export const reduceEffect: { + ( + zero: Z, + f: (z: Z, out: Out) => Effect.Effect + ): (self: Schedule) => Schedule + ( + self: Schedule, + zero: Z, + f: (z: Z, out: Out) => Effect.Effect + ): Schedule +} = internal.reduceEffect + +// TODO(4.0): remove? +/** + * Alias of {@link forever}. + * + * @since 2.0.0 + * @category Constructors + */ +export const repeatForever: Schedule = internal.forever + +/** + * Returns a new schedule that outputs the number of repetitions of this one. + * + * **Details** + * + * This schedule tracks how many times the given schedule has executed and + * outputs the count instead of the original values. The first execution starts + * at `0`, and the count increases with each recurrence. + * + * @since 2.0.0 + * @category Monitoring + */ +export const repetitions: (self: Schedule) => Schedule = internal.repetitions + +/** + * Returns a new schedule that automatically resets to its initial state after a + * period of inactivity defined by `duration`. + * + * **Details** + * + * This function modifies a schedule so that if no inputs are received for the + * specified `duration`, the schedule resets as if it were new. + * + * @see {@link resetWhen} If you need to reset based on output values. + * + * @since 2.0.0 + * @category State Management + */ +export const resetAfter: { + (duration: Duration.DurationInput): (self: Schedule) => Schedule + (self: Schedule, duration: Duration.DurationInput): Schedule +} = internal.resetAfter + +/** + * Resets the schedule when the specified predicate on the schedule output + * evaluates to `true`. + * + * **Details** + * + * This function modifies a schedule so that it resets to its initial state + * whenever the provided predicate `f` returns `true` for an output value. + * + * @see {@link resetAfter} If you need to reset based on inactivity. + * + * @since 2.0.0 + * @category State Management + */ +export const resetWhen: { + (f: Predicate): (self: Schedule) => Schedule + (self: Schedule, f: Predicate): Schedule +} = internal.resetWhen + +/** + * Runs a schedule using the provided inputs and collects all outputs. + * + * **Details** + * + * This function executes a given schedule with a sequence of input values and + * accumulates all outputs into a `Chunk`. The schedule starts execution at the + * specified `now` timestamp and proceeds according to its defined behavior. + * + * This is useful for batch processing, simulating execution, or testing + * schedules with predefined input sequences. + * + * @since 2.0.0 + * @category Execution + */ +export const run: { + ( + now: number, + input: Iterable + ): (self: Schedule) => Effect.Effect, never, R> + (self: Schedule, now: number, input: Iterable): Effect.Effect, never, R> +} = internal.run + +/** + * Returns a schedule that recurs continuously, with each repetition + * spaced by the specified `duration` from the last run. + * + * **Details** + * + * This schedule ensures that executions occur at a fixed interval, + * maintaining a consistent delay between repetitions. The delay starts + * from the end of the last execution, not from the schedule start time. + * + * @see {@link fixed} If you need to run at a fixed interval from the start. + * + * @since 2.0.0 + * @category Constructors + */ +export const spaced: (duration: Duration.DurationInput) => Schedule = internal.spaced + +/** + * A schedule that does not recur and stops immediately. + * + * @since 2.0.0 + * @category Constructors + */ +export const stop: Schedule = internal.stop + +/** + * Returns a schedule that recurs indefinitely, always producing the specified + * constant value. + * + * @since 2.0.0 + * @category Constructors + */ +export const succeed: (value: A) => Schedule = internal.succeed + +/** + * Returns a schedule that recurs indefinitely, evaluating the given function to + * produce a constant value. + * + * @category Constructors + * @since 2.0.0 + */ +export const sync: (evaluate: LazyArg) => Schedule = internal.sync + +/** + * Returns a new schedule that runs the given effectful function for each input + * before continuing execution. + * + * **Details** + * + * This function allows side effects to be performed on each input processed by + * the schedule. It does not modify the schedule’s behavior but ensures that the + * provided function `f` runs before each step. + * + * @since 2.0.0 + * @category Tapping + */ +export const tapInput: { + ( + f: (input: In2) => Effect.Effect + ): (self: Schedule) => Schedule + ( + self: Schedule, + f: (input: In2) => Effect.Effect + ): Schedule +} = internal.tapInput + +/** + * Returns a new schedule that runs the given effectful function for each output + * before continuing execution. + * + * **Details** + * + * This function allows side effects to be performed on each output produced by + * the schedule. It does not modify the schedule’s behavior but ensures that the + * provided function `f` runs after each step. + * + * @since 2.0.0 + * @category Tapping + */ +export const tapOutput: { + ( + f: (out: Types.NoInfer) => Effect.Effect + ): (self: Schedule) => Schedule + ( + self: Schedule, + f: (out: Out) => Effect.Effect + ): Schedule +} = internal.tapOutput + +/** + * Creates a schedule that repeatedly applies a function to transform a state + * value, producing a sequence of values. + * + * **Details** + * + * This function starts with an `initial` value and applies `f` recursively to + * generate the next state at each step. The schedule continues indefinitely, + * producing a stream of values by unfolding the state over time. + * + * @since 2.0.0 + * @category Constructors + */ +export const unfold: (initial: A, f: (a: A) => A) => Schedule = internal.unfold + +/** + * Combines two schedules, continuing execution as long as at least one of them + * allows it, using the shorter delay. + * + * **Details** + * + * This function combines two schedules into a single schedule that executes in + * parallel. If either schedule allows continuation, the merged schedule + * continues. When both schedules produce delays, the schedule selects the + * shorter delay to determine the next step. + * + * The output of the new schedule is a tuple containing the outputs of both + * schedules. The input type is the intersection of both schedules' input types. + * + * This is useful for scenarios where multiple scheduling conditions should be + * considered, ensuring execution proceeds if at least one schedule permits it. + * + * @see {@link unionWith} If you need to use a custom merge function. + * + * @since 2.0.0 + * @category Composition + */ +export const union: { + ( + that: Schedule + ): (self: Schedule) => Schedule<[Out, Out2], In & In2, R2 | R> + ( + self: Schedule, + that: Schedule + ): Schedule<[Out, Out2], In & In2, R | R2> +} = internal.union + +/** + * Combines two schedules, continuing execution as long as at least one of them + * wants to continue, merging their intervals using a custom merge function. + * + * **Details** + * + * This function allows you to combine two schedules while defining how their + * intervals should be merged. Unlike {@link union}, which simply selects the + * shorter delay, this function lets you specify a custom merging strategy for + * the schedules’ intervals. + * + * The merged schedule continues execution as long as at least one of the input + * schedules allows it. The next interval is determined by applying the provided + * merge function to the intervals of both schedules. + * + * The output of the resulting schedule is a tuple containing the outputs of + * both schedules. The input type is the intersection of both schedules' input + * types. + * + * @see {@link union} If you need to use the shorter delay. + * + * @since 2.0.0 + * @category Composition + */ +export const unionWith: { + ( + that: Schedule, + f: (x: Intervals.Intervals, y: Intervals.Intervals) => Intervals.Intervals + ): (self: Schedule) => Schedule<[Out, Out2], In & In2, R2 | R> + ( + self: Schedule, + that: Schedule, + f: (x: Intervals.Intervals, y: Intervals.Intervals) => Intervals.Intervals + ): Schedule<[Out, Out2], In & In2, R | R2> +} = internal.unionWith + +/** + * Returns a new schedule that stops execution when the given predicate on the + * input evaluates to `true`. + * + * **Details** + * + * This function modifies an existing schedule so that it continues executing + * only while the provided predicate returns `false` for incoming inputs. Once + * an input satisfies the condition, the schedule terminates immediately. + * + * @see {@link untilInputEffect} If you need to use an effectful predicate. + * + * @since 2.0.0 + * @category Recurrence Conditions + */ +export const untilInput: { + (f: Predicate): (self: Schedule) => Schedule + (self: Schedule, f: Predicate): Schedule +} = internal.untilInput + +/** + * Returns a new schedule that stops execution when the given effectful + * predicate on the input evaluates to `true`. + * + * **Details** + * + * This function modifies an existing schedule so that it continues executing + * only while the provided effectful predicate returns `false` for incoming + * inputs. The predicate is an `Effect`, meaning it can involve asynchronous + * computations or dependency-based logic. + * + * @see {@link untilInput} If you need to use a pure predicate. + * + * @since 2.0.0 + * @category Recurrence Conditions + */ +export const untilInputEffect: { + ( + f: (input: In) => Effect.Effect + ): (self: Schedule) => Schedule + ( + self: Schedule, + f: (input: In) => Effect.Effect + ): Schedule +} = internal.untilInputEffect + +/** + * Returns a new schedule that stops execution when the given predicate on the + * output evaluates to `true`. + * + * **Details** + * + * This function modifies an existing schedule so that it only continues + * executing while the given predicate returns false for its output values. Once + * the predicate evaluates to `true`, execution stops. + * + * The output of the resulting schedule remains the same, but its duration is + * now constrained by a stopping condition based on its own output. + * + * @see {@link untilOutputEffect} If you need to use an effectful predicate. + * + * @since 2.0.0 + * @category Recurrence Conditions + */ +export const untilOutput: { + (f: Predicate): (self: Schedule) => Schedule + (self: Schedule, f: Predicate): Schedule +} = internal.untilOutput + +/** + * Returns a new schedule that stops execution when the given effectful + * predicate on the output evaluates to `true`. + * + * **Details** + * + * This function modifies an existing schedule so that it only continues + * executing while the provided effectful predicate returns `false` for its + * output values. Once the predicate returns `true`, execution stops. + * + * @see {@link untilOutput} If you need to use a pure predicate. + * + * @since 2.0.0 + * @category Recurrence Conditions + */ +export const untilOutputEffect: { + ( + f: (out: Out) => Effect.Effect + ): (self: Schedule) => Schedule + ( + self: Schedule, + f: (out: Out) => Effect.Effect + ): Schedule +} = internal.untilOutputEffect + +/** + * Returns a new schedule that limits execution to a fixed duration. + * + * **Details** + * + * This function modifies an existing schedule to stop execution after a + * specified duration has passed. The schedule continues as normal until the + * duration is reached, at which point it stops automatically. + * + * @since 2.0.0 + * @category Recurrence Conditions + */ +export const upTo: { + (duration: Duration.DurationInput): (self: Schedule) => Schedule + (self: Schedule, duration: Duration.DurationInput): Schedule +} = internal.upTo + +/** + * Returns a new schedule that continues execution as long as the given + * predicate on the input is true. + * + * **Details** + * + * This function modifies an existing schedule so that it only continues + * execution while a specified predicate holds true for its input. If the + * predicate evaluates to `false` at any step, the schedule stops. + * + * @see {@link whileInputEffect} If you need to use an effectful predicate. + * + * @since 2.0.0 + * @category Recurrence Conditions + */ +export const whileInput: { + (f: Predicate): (self: Schedule) => Schedule + (self: Schedule, f: Predicate): Schedule +} = internal.whileInput + +/** + * Returns a new schedule that continues execution for as long as the given + * effectful predicate on the input evaluates to `true`. + * + * **Details** + * + * This function modifies an existing schedule so that it only continues + * execution while an effectful predicate holds true for its input. If the + * predicate evaluates to `false` at any step, the schedule stops. + * + * @see {@link whileInput} If you need to use a pure predicate. + * + * @since 2.0.0 + * @category Recurrence Conditions + */ +export const whileInputEffect: { + ( + f: (input: In) => Effect.Effect + ): (self: Schedule) => Schedule + ( + self: Schedule, + f: (input: In) => Effect.Effect + ): Schedule +} = internal.whileInputEffect + +/** + * Returns a new schedule that continues execution for as long as the given + * predicate on the output evaluates to `true`. + * + * **Details** + * + * This function modifies an existing schedule so that it only continues + * execution while a provided condition holds true for its output. If the + * predicate returns `false`, the schedule stops. + * + * @see {@link whileOutputEffect} If you need to use an effectful predicate. + * + * @since 2.0.0 + * @category Recurrence Conditions + */ +export const whileOutput: { + (f: Predicate): (self: Schedule) => Schedule + (self: Schedule, f: Predicate): Schedule +} = internal.whileOutput + +/** + * Returns a new schedule that continues execution for as long as the given + * effectful predicate on the output evaluates to `true`. + * + * **Details** + * + * This function modifies an existing schedule so that it only continues + * execution while an effectful condition holds true for its output. If the + * effectful predicate returns `false`, the schedule stops. + * + * @see {@link whileOutput} If you need to use a pure predicate. + * + * @since 2.0.0 + * @category Recurrence Conditions + */ +export const whileOutputEffect: { + ( + f: (out: Out) => Effect.Effect + ): (self: Schedule) => Schedule + ( + self: Schedule, + f: (out: Out) => Effect.Effect + ): Schedule +} = internal.whileOutputEffect + +/** + * Creates a schedule that divides time into fixed `interval`-long windows, + * triggering execution at the start of each new window. + * + * **Details** + * + * This function produces a schedule that waits until the next time window + * boundary before executing. Each window spans a fixed duration specified by + * `interval`. If an action completes midway through a window, the schedule + * waits until the next full window starts before proceeding. + * + * For example, `windowed(Duration.seconds(10))` would produce a schedule as + * follows: + * + * ```text + * 10s 10s 10s 10s + * |----------|----------|----------|----------| + * |action------|sleep---|act|-sleep|action----| + * ``` + * + * @since 2.0.0 + * @category Constructors + */ +export const windowed: (interval: Duration.DurationInput) => Schedule = internal.windowed + +/** + * The same as {@link intersect} but ignores the right output. + * + * @since 2.0.0 + * @category Composition + */ +export const zipLeft: { + ( + that: Schedule + ): (self: Schedule) => Schedule + ( + self: Schedule, + that: Schedule + ): Schedule +} = internal.zipLeft + +/** + * The same as {@link intersect} but ignores the left output. + * + * @since 2.0.0 + * @category Composition + */ +export const zipRight: { + ( + that: Schedule + ): (self: Schedule) => Schedule + ( + self: Schedule, + that: Schedule + ): Schedule +} = internal.zipRight + +/** + * Equivalent to {@link intersect} followed by {@link map}. + * + * @since 2.0.0 + * @category Composition + */ +export const zipWith: { + ( + that: Schedule, + f: (out: Out, out2: Out2) => Out3 + ): (self: Schedule) => Schedule + ( + self: Schedule, + that: Schedule, + f: (out: Out, out2: Out2) => Out3 + ): Schedule +} = internal.zipWith + +/** + * @since 3.15.0 + * @category models + */ +export interface CurrentIterationMetadata { + readonly _: unique symbol +} + +/** + * @since 3.15.0 + * @category models + */ +export interface IterationMetadata { + readonly input: unknown + readonly output: unknown + readonly recurrence: number + readonly start: number + readonly now: number + readonly elapsed: Duration.Duration + readonly elapsedSincePrevious: Duration.Duration +} + +/** + * @since 3.15.0 + * @category models + */ +export const CurrentIterationMetadata: Context.Reference< + CurrentIterationMetadata, + IterationMetadata +> = internal.CurrentIterationMetadata diff --git a/repos/effect/packages/effect/src/ScheduleDecision.ts b/repos/effect/packages/effect/src/ScheduleDecision.ts new file mode 100644 index 0000000..edfae43 --- /dev/null +++ b/repos/effect/packages/effect/src/ScheduleDecision.ts @@ -0,0 +1,62 @@ +/** + * @since 2.0.0 + */ +import * as internal from "./internal/schedule/decision.js" +import type * as Interval from "./ScheduleInterval.js" +import type * as Intervals from "./ScheduleIntervals.js" + +/** + * @since 2.0.0 + * @category models + */ +export type ScheduleDecision = Continue | Done + +/** + * @since 2.0.0 + * @category models + */ +export interface Continue { + readonly _tag: "Continue" + readonly intervals: Intervals.Intervals +} + +/** + * @since 2.0.0 + * @category models + */ +export interface Done { + readonly _tag: "Done" +} + +const _continue = internal._continue +export { + /** + * @since 2.0.0 + * @category constructors + */ + _continue as continue +} + +/** + * @since 2.0.0 + * @category constructors + */ +export const continueWith: (interval: Interval.Interval) => ScheduleDecision = internal.continueWith + +/** + * @since 2.0.0 + * @category constructors + */ +export const done: ScheduleDecision = internal.done + +/** + * @since 2.0.0 + * @category refinements + */ +export const isContinue: (self: ScheduleDecision) => self is Continue = internal.isContinue + +/** + * @since 2.0.0 + * @category refinements + */ +export const isDone: (self: ScheduleDecision) => self is Done = internal.isDone diff --git a/repos/effect/packages/effect/src/ScheduleInterval.ts b/repos/effect/packages/effect/src/ScheduleInterval.ts new file mode 100644 index 0000000..02257d6 --- /dev/null +++ b/repos/effect/packages/effect/src/ScheduleInterval.ts @@ -0,0 +1,151 @@ +/** + * @since 2.0.0 + */ +import type * as Duration from "./Duration.js" +import * as internal from "./internal/schedule/interval.js" +import type * as Option from "./Option.js" + +/** + * @since 2.0.0 + * @category symbols + */ +export const IntervalTypeId: unique symbol = internal.IntervalTypeId + +/** + * @since 2.0.0 + * @category symbols + */ +export type IntervalTypeId = typeof IntervalTypeId + +/** + * An `Interval` represents an interval of time. Intervals can encompass all + * time, or no time at all. + * + * @since 2.0.0 + * @category models + */ +export interface Interval { + readonly [IntervalTypeId]: IntervalTypeId + readonly startMillis: number + readonly endMillis: number +} + +/** + * Constructs a new interval from the two specified endpoints. If the start + * endpoint greater than the end endpoint, then a zero size interval will be + * returned. + * + * @since 2.0.0 + * @category constructors + */ +export const make: (startMillis: number, endMillis: number) => Interval = internal.make + +/** + * An `Interval` of zero-width. + * + * @since 2.0.0 + * @category constructors + */ +export const empty: Interval = internal.empty + +/** + * Returns `true` if this `Interval` is less than `that` interval, `false` + * otherwise. + * + * @since 2.0.0 + * @category ordering + */ +export const lessThan: { + (that: Interval): (self: Interval) => boolean + (self: Interval, that: Interval): boolean +} = internal.lessThan + +/** + * Returns the minimum of two `Interval`s. + * + * @since 2.0.0 + * @category ordering + */ +export const min: { + (that: Interval): (self: Interval) => Interval + (self: Interval, that: Interval): Interval +} = internal.min + +/** + * Returns the maximum of two `Interval`s. + * + * @since 2.0.0 + * @category ordering + */ +export const max: { + (that: Interval): (self: Interval) => Interval + (self: Interval, that: Interval): Interval +} = internal.max + +/** + * Returns `true` if the specified `Interval` is empty, `false` otherwise. + * + * @since 2.0.0 + * @category ordering + */ +export const isEmpty: (self: Interval) => boolean = internal.isEmpty + +/** + * Returns `true` if the specified `Interval` is non-empty, `false` otherwise. + * + * @since 2.0.0 + * @category ordering + */ +export const isNonEmpty: (self: Interval) => boolean = internal.isNonEmpty + +/** + * Computes a new `Interval` which is the intersection of this `Interval` and + * that `Interval`. + * + * @since 2.0.0 + * @category ordering + */ +export const intersect: { + (that: Interval): (self: Interval) => Interval + (self: Interval, that: Interval): Interval +} = internal.intersect + +/** + * Calculates the size of the `Interval` as the `Duration` from the start of the + * interval to the end of the interval. + * + * @since 2.0.0 + * @category getters + */ +export const size: (self: Interval) => Duration.Duration = internal.size + +/** + * Computes a new `Interval` which is the union of this `Interval` and that + * `Interval` as a `Some`, otherwise returns `None` if the two intervals cannot + * form a union. + * + * @since 2.0.0 + * @category utils + */ +export const union: { + (that: Interval): (self: Interval) => Option.Option + (self: Interval, that: Interval): Option.Option +} = internal.union + +/** + * Construct an `Interval` that includes all time equal to and after the + * specified start time. + * + * @since 2.0.0 + * @category constructors + */ +export const after: (startMilliseconds: number) => Interval = internal.after + +/** + * Construct an `Interval` that includes all time equal to and before the + * specified end time. + * + * @category constructors + * @since 2.0.0 + */ +export const before: (endMilliseconds: number) => Interval = internal.before diff --git a/repos/effect/packages/effect/src/ScheduleIntervals.ts b/repos/effect/packages/effect/src/ScheduleIntervals.ts new file mode 100644 index 0000000..df48c3f --- /dev/null +++ b/repos/effect/packages/effect/src/ScheduleIntervals.ts @@ -0,0 +1,122 @@ +/** + * @since 2.0.0 + */ +import type * as Check from "./Chunk.js" +import * as internal from "./internal/schedule/intervals.js" +import type * as Interval from "./ScheduleInterval.js" + +/** + * @since 2.0.0 + * @category symbols + */ +export const IntervalsTypeId: unique symbol = internal.IntervalsTypeId + +/** + * @since 2.0.0 + * @category symbols + */ +export type IntervalsTypeId = typeof IntervalsTypeId + +/** + * An `Intervals` represents a list of several `Interval`s. + * + * @since 2.0.0 + * @category models + */ +export interface Intervals { + readonly [IntervalsTypeId]: IntervalsTypeId + readonly intervals: Check.Chunk +} + +/** + * Creates a new `Intervals` from a `List` of `Interval`s. + * + * @since 2.0.0 + * @category constructors + */ +export const make: (intervals: Check.Chunk) => Intervals = internal.make + +/** + * Constructs an empty list of `Interval`s. + * + * @since 2.0.0 + * @category constructors + */ +export const empty: Intervals = internal.empty + +/** + * Creates `Intervals` from the specified `Iterable`. + * + * @since 2.0.0 + * @category constructors + */ +export const fromIterable: (intervals: Iterable) => Intervals = internal.fromIterable + +/** + * Computes the union of this `Intervals` and that `Intervals` + * + * @since 2.0.0 + * @category utils + */ +export const union: { + (that: Intervals): (self: Intervals) => Intervals + (self: Intervals, that: Intervals): Intervals +} = internal.union + +/** + * Produces the intersection of this `Intervals` and that `Intervals`. + * + * @since 2.0.0 + * @category utils + */ +export const intersect: { + (that: Intervals): (self: Intervals) => Intervals + (self: Intervals, that: Intervals): Intervals +} = internal.intersect + +/** + * The start of the earliest interval in the specified `Intervals`. + * + * @since 2.0.0 + * @category getters + */ +export const start: (self: Intervals) => number = internal.start + +/** + * The end of the latest interval in the specified `Intervals`. + * + * @since 2.0.0 + * @category getters + */ +export const end: (self: Intervals) => number = internal.end + +/** + * Returns `true` if the start of this `Intervals` is before the start of that + * `Intervals`, `false` otherwise. + * + * @since 2.0.0 + * @category ordering + */ +export const lessThan: { + (that: Intervals): (self: Intervals) => boolean + (self: Intervals, that: Intervals): boolean +} = internal.lessThan + +/** + * Returns `true` if this `Intervals` is non-empty, `false` otherwise. + * + * @since 2.0.0 + * @category getters + */ +export const isNonEmpty: (self: Intervals) => boolean = internal.isNonEmpty + +/** + * Returns the maximum of the two `Intervals` (i.e. which has the latest start). + * + * @since 2.0.0 + * @category ordering + */ +export const max: { + (that: Intervals): (self: Intervals) => Intervals + (self: Intervals, that: Intervals): Intervals +} = internal.max diff --git a/repos/effect/packages/effect/src/Scheduler.ts b/repos/effect/packages/effect/src/Scheduler.ts new file mode 100644 index 0000000..68d0543 --- /dev/null +++ b/repos/effect/packages/effect/src/Scheduler.ts @@ -0,0 +1,362 @@ +/** + * @since 2.0.0 + */ + +import type { Effect } from "./Effect.js" +import type { RuntimeFiber } from "./Fiber.js" +import type { FiberRef } from "./FiberRef.js" +import { dual } from "./Function.js" +import { globalValue } from "./GlobalValue.js" +import * as core from "./internal/core.js" + +/** + * @since 2.0.0 + * @category models + */ +export type Task = () => void + +/** + * @since 2.0.0 + * @category models + */ +export interface Scheduler { + shouldYield(fiber: RuntimeFiber): number | false + scheduleTask(task: Task, priority: number, fiber?: RuntimeFiber): void +} + +/** + * @since 3.20.0 + * @category models + */ +export class SchedulerRunner { + running = false + tasks = new PriorityBuckets() + + constructor( + readonly scheduleDrain: (depth: number, drain: (depth: number) => void) => void + ) {} + + private starveInternal = (depth: number) => { + const tasks = this.tasks.buckets + this.tasks.buckets = [] + for (const [_, toRun] of tasks) { + for (let i = 0; i < toRun.length; i++) { + toRun[i]() + } + } + if (this.tasks.buckets.length === 0) { + this.running = false + } else { + this.starve(depth) + } + } + + private starve(depth = 0) { + this.scheduleDrain(depth, this.starveInternal) + } + + scheduleTask(task: Task, priority: number) { + this.tasks.scheduleTask(task, priority) + if (!this.running) { + this.running = true + this.starve() + } + } + /** + * @since 3.20.0 + * @category constructors + */ + static cached( + scheduleDrain: (depth: number, drain: (depth: number) => void) => void + ) { + const fallback = new SchedulerRunner(scheduleDrain) + const runners = new WeakMap, SchedulerRunner>() + + return (fiber?: RuntimeFiber) => { + if (fiber === undefined) { + return fallback + } + let runner = runners.get(fiber) + if (runner === undefined) { + runner = new SchedulerRunner(scheduleDrain) + runners.set(fiber, runner) + } + return runner + } + } +} + +/** + * @since 2.0.0 + * @category utils + */ +export class PriorityBuckets { + /** + * @since 2.0.0 + */ + public buckets: Array<[number, Array]> = [] + /** + * @since 2.0.0 + */ + scheduleTask(task: T, priority: number) { + const length = this.buckets.length + let bucket: [number, Array] | undefined = undefined + let index = 0 + for (; index < length; index++) { + if (this.buckets[index][0] <= priority) { + bucket = this.buckets[index] + } else { + break + } + } + if (bucket && bucket[0] === priority) { + bucket[1].push(task) + } else if (index === length) { + this.buckets.push([priority, [task]]) + } else { + this.buckets.splice(index, 0, [priority, [task]]) + } + } +} + +/** + * @since 2.0.0 + * @category constructors + */ +export class MixedScheduler implements Scheduler { + private readonly getRunner = SchedulerRunner.cached((depth, drain) => { + if (depth >= this.maxNextTickBeforeTimer) { + setTimeout(() => drain(0), 0) + } else { + Promise.resolve(void 0).then(() => drain(depth + 1)) + } + }) + + constructor( + /** + * @since 2.0.0 + */ + readonly maxNextTickBeforeTimer: number + ) {} + + /** + * @since 2.0.0 + */ + shouldYield(fiber: RuntimeFiber): number | false { + return fiber.currentOpCount > fiber.getFiberRef(core.currentMaxOpsBeforeYield) + ? fiber.getFiberRef(core.currentSchedulingPriority) + : false + } + + /** + * @since 2.0.0 + */ + scheduleTask(task: Task, priority: number, fiber?: RuntimeFiber) { + this.getRunner(fiber).scheduleTask(task, priority) + } +} + +/** + * @since 2.0.0 + * @category schedulers + */ +export const defaultScheduler: Scheduler = globalValue( + Symbol.for("effect/Scheduler/defaultScheduler"), + () => new MixedScheduler(2048) +) + +/** + * @since 2.0.0 + * @category constructors + */ +export class SyncScheduler implements Scheduler { + /** + * @since 2.0.0 + */ + tasks = new PriorityBuckets() + + /** + * @since 2.0.0 + */ + deferred = false + + /** + * @since 2.0.0 + */ + scheduleTask(task: Task, priority: number, fiber?: RuntimeFiber) { + if (this.deferred) { + defaultScheduler.scheduleTask(task, priority, fiber) + } else { + this.tasks.scheduleTask(task, priority) + } + } + + /** + * @since 2.0.0 + */ + shouldYield(fiber: RuntimeFiber): number | false { + return fiber.currentOpCount > fiber.getFiberRef(core.currentMaxOpsBeforeYield) + ? fiber.getFiberRef(core.currentSchedulingPriority) + : false + } + + /** + * @since 2.0.0 + */ + flush() { + while (this.tasks.buckets.length > 0) { + const tasks = this.tasks.buckets + this.tasks.buckets = [] + for (const [_, toRun] of tasks) { + for (let i = 0; i < toRun.length; i++) { + toRun[i]() + } + } + } + this.deferred = true + } +} + +/** + * @since 2.0.0 + * @category constructors + */ +export class ControlledScheduler implements Scheduler { + /** + * @since 2.0.0 + */ + tasks = new PriorityBuckets() + + /** + * @since 2.0.0 + */ + deferred = false + + /** + * @since 2.0.0 + */ + scheduleTask(task: Task, priority: number, fiber?: RuntimeFiber) { + if (this.deferred) { + defaultScheduler.scheduleTask(task, priority, fiber) + } else { + this.tasks.scheduleTask(task, priority) + } + } + + /** + * @since 2.0.0 + */ + shouldYield(fiber: RuntimeFiber): number | false { + return fiber.currentOpCount > fiber.getFiberRef(core.currentMaxOpsBeforeYield) + ? fiber.getFiberRef(core.currentSchedulingPriority) + : false + } + + /** + * @since 2.0.0 + */ + step() { + const tasks = this.tasks.buckets + this.tasks.buckets = [] + for (const [_, toRun] of tasks) { + for (let i = 0; i < toRun.length; i++) { + toRun[i]() + } + } + } +} + +/** + * @since 2.0.0 + * @category constructors + */ +export const makeMatrix = (...record: Array<[number, Scheduler]>): Scheduler => { + const index = record.sort(([p0], [p1]) => p0 < p1 ? -1 : p0 > p1 ? 1 : 0) + return { + shouldYield(fiber) { + for (const scheduler of record) { + const priority = scheduler[1].shouldYield(fiber) + if (priority !== false) { + return priority + } + } + return false + }, + scheduleTask(task, priority, fiber) { + let scheduler: Scheduler | undefined = undefined + for (const i of index) { + if (priority >= i[0]) { + scheduler = i[1] + } else { + return (scheduler ?? defaultScheduler).scheduleTask(task, priority, fiber) + } + } + return (scheduler ?? defaultScheduler).scheduleTask(task, priority, fiber) + } + } +} + +/** + * @since 2.0.0 + * @category utilities + */ +export const defaultShouldYield: Scheduler["shouldYield"] = (fiber) => { + return fiber.currentOpCount > fiber.getFiberRef(core.currentMaxOpsBeforeYield) + ? fiber.getFiberRef(core.currentSchedulingPriority) + : false +} + +/** + * @since 2.0.0 + * @category constructors + */ +export const make = ( + scheduleTask: Scheduler["scheduleTask"], + shouldYield: Scheduler["shouldYield"] = defaultShouldYield +): Scheduler => ({ + scheduleTask, + shouldYield +}) + +/** + * @since 2.0.0 + * @category constructors + */ +export const makeBatched = ( + callback: (runBatch: () => void) => void, + shouldYield: Scheduler["shouldYield"] = defaultShouldYield +) => { + const getRunner = SchedulerRunner.cached((_, drain) => { + callback(() => drain(0)) + }) + + return make((task, priority, fiber) => { + getRunner(fiber).scheduleTask(task, priority) + }, shouldYield) +} + +/** + * @since 2.0.0 + * @category constructors + */ +export const timer = (ms: number, shouldYield: Scheduler["shouldYield"] = defaultShouldYield) => + make((task) => setTimeout(task, ms), shouldYield) + +/** + * @since 2.0.0 + * @category constructors + */ +export const timerBatched = (ms: number, shouldYield: Scheduler["shouldYield"] = defaultShouldYield) => + makeBatched((task) => setTimeout(task, ms), shouldYield) + +/** @internal */ +export const currentScheduler: FiberRef = globalValue( + Symbol.for("effect/FiberRef/currentScheduler"), + () => core.fiberRefUnsafeMake(defaultScheduler) +) + +/** @internal */ +export const withScheduler = dual< + (scheduler: Scheduler) => (self: Effect) => Effect, + (self: Effect, scheduler: Scheduler) => Effect +>(2, (self, scheduler) => core.fiberRefLocally(self, currentScheduler, scheduler)) diff --git a/repos/effect/packages/effect/src/Schema.ts b/repos/effect/packages/effect/src/Schema.ts new file mode 100644 index 0000000..08719a0 --- /dev/null +++ b/repos/effect/packages/effect/src/Schema.ts @@ -0,0 +1,10914 @@ +/** + * @since 3.10.0 + */ + +import type { StandardSchemaV1 } from "@standard-schema/spec" +import type { ArbitraryAnnotation, ArbitraryGenerationContext, LazyArbitrary } from "./Arbitrary.js" +import * as array_ from "./Array.js" +import * as bigDecimal_ from "./BigDecimal.js" +import * as bigInt_ from "./BigInt.js" +import * as boolean_ from "./Boolean.js" +import type { Brand } from "./Brand.js" +import * as cause_ from "./Cause.js" +import * as chunk_ from "./Chunk.js" +import * as config_ from "./Config.js" +import * as configError_ from "./ConfigError.js" +import * as data_ from "./Data.js" +import * as dateTime from "./DateTime.js" +import * as duration_ from "./Duration.js" +import * as Effect from "./Effect.js" +import * as either_ from "./Either.js" +import * as Encoding from "./Encoding.js" +import * as Equal from "./Equal.js" +import * as Equivalence from "./Equivalence.js" +import * as exit_ from "./Exit.js" +import * as fastCheck_ from "./FastCheck.js" +import * as fiberId_ from "./FiberId.js" +import type { LazyArg } from "./Function.js" +import { dual, identity } from "./Function.js" +import { globalValue } from "./GlobalValue.js" +import * as hashMap_ from "./HashMap.js" +import * as hashSet_ from "./HashSet.js" +import * as Inspectable from "./Inspectable.js" +import * as internalCause_ from "./internal/cause.js" +import * as errors_ from "./internal/schema/errors.js" +import * as schemaId_ from "./internal/schema/schemaId.js" +import * as util_ from "./internal/schema/util.js" +import * as list_ from "./List.js" +import * as number_ from "./Number.js" +import * as option_ from "./Option.js" +import type * as Order from "./Order.js" +import * as ParseResult from "./ParseResult.js" +import type { Pipeable } from "./Pipeable.js" +import { pipeArguments } from "./Pipeable.js" +import * as Predicate from "./Predicate.js" +import type * as pretty_ from "./Pretty.js" +import * as redacted_ from "./Redacted.js" +import * as Request from "./Request.js" +import * as scheduler_ from "./Scheduler.js" +import type { ParseOptions } from "./SchemaAST.js" +import * as AST from "./SchemaAST.js" +import * as sortedSet_ from "./SortedSet.js" +import * as string_ from "./String.js" +import * as struct_ from "./Struct.js" +import type * as Types from "./Types.js" + +/** + * @since 3.10.0 + */ +export type Simplify = { [K in keyof A]: A[K] } & {} + +/** + * @since 3.10.0 + */ +export type SimplifyMutable = { + -readonly [K in keyof A]: A[K] +} extends infer B ? B : never + +/** + * @since 3.10.0 + * @category symbol + */ +export const TypeId: unique symbol = Symbol.for("effect/Schema") + +/** + * @since 3.10.0 + * @category symbol + */ +export type TypeId = typeof TypeId + +/** + * @category model + * @since 3.10.0 + */ +export interface Schema extends Schema.Variance, Pipeable { + readonly Type: A + readonly Encoded: I + readonly Context: R + readonly ast: AST.AST + /** + * Merges a set of new annotations with existing ones, potentially overwriting + * any duplicates. + */ + annotations(annotations: Annotations.GenericSchema): Schema +} + +/** + * @category annotations + * @since 3.10.0 + */ +export interface Annotable, A, I = A, R = never> extends Schema { + annotations(annotations: Annotations.GenericSchema): Self +} + +/** + * @category annotations + * @since 3.10.0 + */ +export interface AnnotableClass, A, I = A, R = never> extends Annotable { + new(_: never): Schema.Variance +} + +/** + * @category model + * @since 3.10.0 + */ +export interface SchemaClass extends AnnotableClass, A, I, R> {} + +/** + * @category constructors + * @since 3.10.0 + */ +export function make(ast: AST.AST): SchemaClass { + return class SchemaClass { + [TypeId] = variance + static ast = ast + static annotations(annotations: Annotations.GenericSchema) { + return make(mergeSchemaAnnotations(this.ast, annotations)) + } + static pipe() { + return pipeArguments(this, arguments) + } + static toString() { + return String(ast) + } + static Type: A + static Encoded: I + static Context: R + static [TypeId] = variance + } +} + +const variance = { + /* c8 ignore next */ + _A: (_: any) => _, + /* c8 ignore next */ + _I: (_: any) => _, + /* c8 ignore next */ + _R: (_: never) => _ +} + +const makeStandardResult = (exit: exit_.Exit>): StandardSchemaV1.Result => + exit_.isSuccess(exit) ? exit.value : makeStandardFailureResult(cause_.pretty(exit.cause)) + +const makeStandardFailureResult = (message: string): StandardSchemaV1.FailureResult => ({ + issues: [{ message }] +}) + +const makeStandardFailureFromParseIssue = ( + issue: ParseResult.ParseIssue +): Effect.Effect => + Effect.map(ParseResult.ArrayFormatter.formatIssue(issue), (issues) => ({ + issues: issues.map((issue) => ({ + path: issue.path, + message: issue.message + })) + })) + +/** + * Returns a "Standard Schema" object conforming to the [Standard Schema + * v1](https://standardschema.dev/) specification. + * + * This function creates a schema whose `validate` method attempts to decode and + * validate the provided input synchronously. If the underlying `Schema` + * includes any asynchronous components (e.g., asynchronous message resolutions + * or checks), then validation will necessarily return a `Promise` instead. + * + * Any detected defects will be reported via a single issue containing no + * `path`. + * + * @example + * ```ts + * import { Schema } from "effect" + * + * const schema = Schema.Struct({ + * name: Schema.String + * }) + * + * // ┌─── StandardSchemaV1<{ readonly name: string; }> + * // ▼ + * const standardSchema = Schema.standardSchemaV1(schema) + * ``` + * + * @category Standard Schema + * @since 3.13.0 + */ +export const standardSchemaV1 = ( + schema: Schema, + overrideOptions?: AST.ParseOptions +): StandardSchemaV1 & SchemaClass => { + const decodeUnknown = ParseResult.decodeUnknown(schema, { errors: "all" }) + return class StandardSchemaV1Class extends make(schema.ast) { + static "~standard" = { + version: 1, + vendor: "effect", + validate(value) { + const scheduler = new scheduler_.SyncScheduler() + const fiber = Effect.runFork( + Effect.matchEffect(decodeUnknown(value, overrideOptions), { + onFailure: makeStandardFailureFromParseIssue, + onSuccess: (value) => Effect.succeed({ value }) + }), + { scheduler } + ) + scheduler.flush() + const exit = fiber.unsafePoll() + if (exit) { + return makeStandardResult(exit) + } + return new Promise((resolve) => { + fiber.addObserver((exit) => { + resolve(makeStandardResult(exit)) + }) + }) + } + } + } +} + +interface AllAnnotations> + extends Annotations.Schema, PropertySignature.Annotations +{} + +const builtInAnnotations = { + typeConstructor: AST.TypeConstructorAnnotationId, + schemaId: AST.SchemaIdAnnotationId, + message: AST.MessageAnnotationId, + missingMessage: AST.MissingMessageAnnotationId, + identifier: AST.IdentifierAnnotationId, + title: AST.TitleAnnotationId, + description: AST.DescriptionAnnotationId, + examples: AST.ExamplesAnnotationId, + default: AST.DefaultAnnotationId, + documentation: AST.DocumentationAnnotationId, + jsonSchema: AST.JSONSchemaAnnotationId, + arbitrary: AST.ArbitraryAnnotationId, + pretty: AST.PrettyAnnotationId, + equivalence: AST.EquivalenceAnnotationId, + concurrency: AST.ConcurrencyAnnotationId, + batching: AST.BatchingAnnotationId, + parseIssueTitle: AST.ParseIssueTitleAnnotationId, + parseOptions: AST.ParseOptionsAnnotationId, + decodingFallback: AST.DecodingFallbackAnnotationId +} + +const toASTAnnotations = >( + annotations?: AllAnnotations +): AST.Annotations => { + if (!annotations) { + return {} + } + const out: Types.Mutable = { ...annotations } + + for (const key in builtInAnnotations) { + if (key in annotations) { + const id = builtInAnnotations[key as keyof typeof builtInAnnotations] + out[id] = annotations[key as keyof typeof annotations] + delete out[key] + } + } + + return out +} + +const mergeSchemaAnnotations = (ast: AST.AST, annotations: Annotations.Schema): AST.AST => + AST.annotations(ast, toASTAnnotations(annotations)) + +/** + * @category annotations + * @since 3.10.0 + */ +export declare namespace Annotable { + /** + * @since 3.10.0 + */ + export type Self = ReturnType + + /** + * @since 3.10.0 + */ + export type Any = Annotable + + /** + * @since 3.10.0 + */ + export type All = + | Any + | Annotable + | Annotable + | Annotable +} + +/** + * @since 3.10.0 + */ +export function asSchema( + schema: S +): Schema, Schema.Encoded, Schema.Context> { + return schema as any +} + +/** + * @category formatting + * @since 3.10.0 + */ +export const format = (schema: S): string => String(schema.ast) + +/** + * @since 3.10.0 + */ +export declare namespace Schema { + /** + * @since 3.10.0 + */ + export interface Variance { + readonly [TypeId]: { + readonly _A: Types.Invariant + readonly _I: Types.Invariant + readonly _R: Types.Covariant + } + } + + /** + * @since 3.10.0 + */ + export type Type = S extends Schema.Variance ? A : never + + /** + * @since 3.10.0 + */ + export type Encoded = S extends Schema.Variance ? I : never + + /** + * @since 3.10.0 + */ + export type Context = S extends Schema.Variance ? R : never + + /** + * @since 3.10.0 + */ + export type ToAsserts = ( + input: unknown, + options?: AST.ParseOptions + ) => asserts input is Schema.Type + + /** + * Any schema, except for `never`. + * + * @since 3.10.0 + */ + export type Any = Schema + + /** + * Any schema with `Context = never`, except for `never`. + * + * @since 3.10.0 + */ + export type AnyNoContext = Schema + + /** + * Any schema, including `never`. + * + * @since 3.10.0 + */ + export type All = + | Any + | Schema + | Schema + | Schema + + /** + * Type-level counterpart of `Schema.asSchema` function. + * + * @since 3.10.0 + */ + export type AsSchema = Schema, Encoded, Context> +} + +/** + * The `encodedSchema` function allows you to extract the `Encoded` portion of a + * schema, creating a new schema that conforms to the properties defined in the + * original schema without retaining any refinements or transformations that + * were applied previously. + * + * @since 3.10.0 + */ +export const encodedSchema = (schema: Schema): SchemaClass => make(AST.encodedAST(schema.ast)) + +/** + * The `encodedBoundSchema` function is similar to `encodedSchema` but preserves + * the refinements up to the first transformation point in the original schema. + * + * @since 3.10.0 + */ +export const encodedBoundSchema = (schema: Schema): SchemaClass => + make(AST.encodedBoundAST(schema.ast)) + +/** + * The `typeSchema` function allows you to extract the `Type` portion of a + * schema, creating a new schema that conforms to the properties defined in the + * original schema without considering the initial encoding or transformation + * processes. + * + * @since 3.10.0 + */ +export const typeSchema = (schema: Schema): SchemaClass => make(AST.typeAST(schema.ast)) + +/* c8 ignore start */ +export { + /** + * By default the option `exact` is set to `true`. + * + * @throws `ParseError` + * @category validation + * @since 3.10.0 + */ + asserts, + /** + * @category decoding + * @since 3.10.0 + */ + decodeOption, + /** + * @throws `ParseError` + * @category decoding + * @since 3.10.0 + */ + decodeSync, + /** + * @category decoding + * @since 3.10.0 + */ + decodeUnknownOption, + /** + * @throws `ParseError` + * @category decoding + * @since 3.10.0 + */ + decodeUnknownSync, + /** + * @category encoding + * @since 3.10.0 + */ + encodeOption, + /** + * @throws `ParseError` + * @category encoding + * @since 3.10.0 + */ + encodeSync, + /** + * @category encoding + * @since 3.10.0 + */ + encodeUnknownOption, + /** + * @throws `ParseError` + * @category encoding + * @since 3.10.0 + */ + encodeUnknownSync, + /** + * By default the option `exact` is set to `true`. + * + * @category validation + * @since 3.10.0 + */ + is, + /** + * @category validation + * @since 3.10.0 + */ + validateOption, + /** + * @throws `ParseError` + * @category validation + * @since 3.10.0 + */ + validateSync +} from "./ParseResult.js" +/* c8 ignore end */ + +/** + * @category encoding + * @since 3.10.0 + */ +export const encodeUnknown = ( + schema: Schema, + options?: ParseOptions +) => { + const encodeUnknown = ParseResult.encodeUnknown(schema, options) + return (u: unknown, overrideOptions?: ParseOptions): Effect.Effect => + ParseResult.mapError(encodeUnknown(u, overrideOptions), ParseResult.parseError) +} + +/** + * @category encoding + * @since 3.10.0 + */ +export const encodeUnknownEither = ( + schema: Schema, + options?: ParseOptions +) => { + const encodeUnknownEither = ParseResult.encodeUnknownEither(schema, options) + return (u: unknown, overrideOptions?: ParseOptions): either_.Either => + either_.mapLeft(encodeUnknownEither(u, overrideOptions), ParseResult.parseError) +} + +/** + * @category encoding + * @since 3.10.0 + */ +export const encodeUnknownPromise = ( + schema: Schema, + options?: ParseOptions +) => { + const parser = encodeUnknown(schema, options) + return (u: unknown, overrideOptions?: ParseOptions): Promise => Effect.runPromise(parser(u, overrideOptions)) +} + +/** + * @category encoding + * @since 3.10.0 + */ +export const encode: ( + schema: Schema, + options?: ParseOptions +) => (a: A, overrideOptions?: ParseOptions) => Effect.Effect = encodeUnknown + +/** + * @category encoding + * @since 3.10.0 + */ +export const encodeEither: ( + schema: Schema, + options?: ParseOptions +) => (a: A, overrideOptions?: ParseOptions) => either_.Either = encodeUnknownEither + +/** + * @category encoding + * @since 3.10.0 + */ +export const encodePromise: ( + schema: Schema, + options?: ParseOptions +) => (a: A, overrideOptions?: ParseOptions) => Promise = encodeUnknownPromise + +/** + * @category decoding + * @since 3.10.0 + */ +export const decodeUnknown = ( + schema: Schema, + options?: ParseOptions +) => { + const decodeUnknown = ParseResult.decodeUnknown(schema, options) + return (u: unknown, overrideOptions?: ParseOptions): Effect.Effect => + ParseResult.mapError(decodeUnknown(u, overrideOptions), ParseResult.parseError) +} + +/** + * @category decoding + * @since 3.10.0 + */ +export const decodeUnknownEither = ( + schema: Schema, + options?: ParseOptions +) => { + const decodeUnknownEither = ParseResult.decodeUnknownEither(schema, options) + return (u: unknown, overrideOptions?: ParseOptions): either_.Either => + either_.mapLeft(decodeUnknownEither(u, overrideOptions), ParseResult.parseError) +} + +/** + * @category decoding + * @since 3.10.0 + */ +export const decodeUnknownPromise = ( + schema: Schema, + options?: ParseOptions +) => { + const parser = decodeUnknown(schema, options) + return (u: unknown, overrideOptions?: ParseOptions): Promise => Effect.runPromise(parser(u, overrideOptions)) +} + +/** + * @category decoding + * @since 3.10.0 + */ +export const decode: ( + schema: Schema, + options?: ParseOptions +) => (i: I, overrideOptions?: ParseOptions) => Effect.Effect = decodeUnknown + +/** + * @category decoding + * @since 3.10.0 + */ +export const decodeEither: ( + schema: Schema, + options?: ParseOptions +) => (i: I, overrideOptions?: ParseOptions) => either_.Either = decodeUnknownEither + +/** + * @category decoding + * @since 3.10.0 + */ +export const decodePromise: ( + schema: Schema, + options?: ParseOptions +) => (i: I, overrideOptions?: ParseOptions) => Promise = decodeUnknownPromise + +/** + * @category validation + * @since 3.10.0 + */ +export const validate = ( + schema: Schema, + options?: ParseOptions +) => { + const validate = ParseResult.validate(schema, options) + return (u: unknown, overrideOptions?: ParseOptions): Effect.Effect => + ParseResult.mapError(validate(u, overrideOptions), ParseResult.parseError) +} + +/** + * @category validation + * @since 3.10.0 + */ +export const validateEither = ( + schema: Schema, + options?: ParseOptions +) => { + const validateEither = ParseResult.validateEither(schema, options) + return (u: unknown, overrideOptions?: ParseOptions): either_.Either => + either_.mapLeft(validateEither(u, overrideOptions), ParseResult.parseError) +} + +/** + * @category validation + * @since 3.10.0 + */ +export const validatePromise = ( + schema: Schema, + options?: ParseOptions +) => { + const parser = validate(schema, options) + return (u: unknown, overrideOptions?: ParseOptions): Promise => Effect.runPromise(parser(u, overrideOptions)) +} + +/** + * Tests if a value is a `Schema`. + * + * @category guards + * @since 3.10.0 + */ +export const isSchema = (u: unknown): u is Schema.Any => + Predicate.hasProperty(u, TypeId) && Predicate.isObject(u[TypeId]) + +/** + * @category api interface + * @since 3.10.0 + */ +export interface Literal> + extends AnnotableClass, Literals[number]> +{ + readonly literals: Readonly +} + +function getDefaultLiteralAST>( + literals: Literals +): AST.AST { + return AST.isMembers(literals) + ? AST.Union.make(AST.mapMembers(literals, (literal) => new AST.Literal(literal))) + : new AST.Literal(literals[0]) +} + +function makeLiteralClass>( + literals: Literals, + ast: AST.AST = getDefaultLiteralAST(literals) +): Literal { + return class LiteralClass extends make(ast) { + static override annotations(annotations: Annotations.Schema): Literal { + return makeLiteralClass(this.literals, mergeSchemaAnnotations(this.ast, annotations)) + } + static literals = [...literals] as Literals + } +} + +/** + * @category constructors + * @since 3.10.0 + */ +export function Literal>( + ...literals: Literals +): Literal +export function Literal(): Never +export function Literal>( + ...literals: Literals +): SchemaClass +export function Literal>( + ...literals: Literals +): SchemaClass | Never { + return array_.isNonEmptyReadonlyArray(literals) ? makeLiteralClass(literals) : Never +} + +/** + * Creates a new `Schema` from a literal schema. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { Either, Schema } from "effect" + * + * const schema = Schema.Literal("a", "b", "c").pipe(Schema.pickLiteral("a", "b")) + * + * assert.deepStrictEqual(Schema.decodeSync(schema)("a"), "a") + * assert.deepStrictEqual(Schema.decodeSync(schema)("b"), "b") + * assert.strictEqual(Either.isLeft(Schema.decodeUnknownEither(schema)("c")), true) + * ``` + * + * @category constructors + * @since 3.10.0 + */ +export const pickLiteral = + >(...literals: L) => + (_schema: Schema): Literal<[...L]> => Literal(...literals) + +/** + * @category constructors + * @since 3.10.0 + */ +export const UniqueSymbolFromSelf = (symbol: S): SchemaClass => make(new AST.UniqueSymbol(symbol)) + +/** + * @category api interface + * @since 3.10.0 + */ +export interface Enums extends AnnotableClass, A[keyof A]> { + readonly enums: A +} + +/** + * @since 3.10.0 + */ +export type EnumsDefinition = { [x: string]: string | number } + +const getDefaultEnumsAST = (enums: A) => + new AST.Enums( + Object.keys(enums).filter( + (key) => typeof enums[enums[key]] !== "number" + ).map((key) => [key, enums[key]]) + ) + +const makeEnumsClass = ( + enums: A, + ast: AST.AST = getDefaultEnumsAST(enums) +): Enums => (class EnumsClass extends make(ast) { + static override annotations(annotations: Annotations.Schema) { + return makeEnumsClass(this.enums, mergeSchemaAnnotations(this.ast, annotations)) + } + + static enums = { ...enums } +}) + +/** + * @category constructors + * @since 3.10.0 + */ +export const Enums = (enums: A): Enums => makeEnumsClass(enums) + +type AppendType< + Template extends string, + Next +> = Next extends AST.LiteralValue ? `${Template}${Next}` + : Next extends Schema ? `${Template}${A}` + : never + +type GetTemplateLiteralType = Params extends [...infer Init, infer Last] ? + AppendType, Last> + : `` + +/** + * @category API interface + * @since 3.10.0 + */ +export interface TemplateLiteral extends SchemaClass {} + +type TemplateLiteralParameter = Schema.AnyNoContext | AST.LiteralValue + +/** + * @category template literal + * @since 3.10.0 + */ +export const TemplateLiteral = >( + ...[head, ...tail]: Params +): TemplateLiteral> => { + const spans: Array = [] + let h = "" + let ts = tail + + if (isSchema(head)) { + if (AST.isLiteral(head.ast)) { + h = String(head.ast.literal) + } else { + ts = [head, ...ts] + } + } else { + h = String(head) + } + + for (let i = 0; i < ts.length; i++) { + const item = ts[i] + if (isSchema(item)) { + if (i < ts.length - 1) { + const next = ts[i + 1] + if (isSchema(next)) { + if (AST.isLiteral(next.ast)) { + spans.push(new AST.TemplateLiteralSpan(item.ast, String(next.ast.literal))) + i++ + continue + } + } else { + spans.push(new AST.TemplateLiteralSpan(item.ast, String(next))) + i++ + continue + } + } + spans.push(new AST.TemplateLiteralSpan(item.ast, "")) + } else { + spans.push(new AST.TemplateLiteralSpan(new AST.Literal(item), "")) + } + } + + if (array_.isNonEmptyArray(spans)) { + return make(new AST.TemplateLiteral(h, spans)) + } else { + return make(new AST.TemplateLiteral("", [new AST.TemplateLiteralSpan(new AST.Literal(h), "")])) + } +} + +type TemplateLiteralParserParameters = Schema.Any | AST.LiteralValue + +type GetTemplateLiteralParserType = Params extends [infer Head, ...infer Tail] ? readonly [ + Head extends Schema ? A : Head, + ...GetTemplateLiteralParserType + ] + : [] + +type AppendEncoded< + Template extends string, + Next +> = Next extends AST.LiteralValue ? `${Template}${Next}` + : Next extends Schema ? `${Template}${I}` + : never + +type GetTemplateLiteralParserEncoded = Params extends [...infer Init, infer Last] ? + AppendEncoded, Last> + : `` + +/** + * @category API interface + * @since 3.10.0 + */ +export interface TemplateLiteralParser> + extends + Schema< + GetTemplateLiteralParserType, + GetTemplateLiteralParserEncoded, + Schema.Context + > +{ + readonly params: Params +} + +function getTemplateLiteralParserCoercedElement(encoded: Schema.Any, schema: Schema.Any): Schema.Any | undefined { + const ast = encoded.ast + switch (ast._tag) { + case "Literal": { + const literal = ast.literal + if (!Predicate.isString(literal)) { + const s = String(literal) + return transform(Literal(s), schema, { + strict: true, + decode: () => literal, + encode: () => s + }) + } + break + } + case "NumberKeyword": + return compose(NumberFromString, schema) + case "Union": { + const members: Array = [] + let hasCoercions = false + for (const member of ast.types) { + const schema = make(member) + const encoded = encodedSchema(schema) + const coerced = getTemplateLiteralParserCoercedElement(encoded, schema) + if (coerced) { + hasCoercions = true + } + members.push(coerced ?? schema) + } + return hasCoercions ? compose(Union(...members), schema) : schema + } + } +} + +/** + * @category template literal + * @since 3.10.0 + */ +export const TemplateLiteralParser = >( + ...params: Params +): TemplateLiteralParser => { + const encodedSchemas: Array = [] + const elements: Array = [] + const schemas: Array = [] + let coerced = false + for (let i = 0; i < params.length; i++) { + const param = params[i] + const schema = isSchema(param) ? param : Literal(param) + schemas.push(schema) + const encoded = encodedSchema(schema) + encodedSchemas.push(encoded) + const element = getTemplateLiteralParserCoercedElement(encoded, schema) + if (element) { + elements.push(element) + coerced = true + } else { + elements.push(schema) + } + } + const from = TemplateLiteral(...encodedSchemas as any) + const re = AST.getTemplateLiteralCapturingRegExp(from.ast as AST.TemplateLiteral) + let to = Tuple(...elements) + if (coerced) { + to = to.annotations({ [AST.AutoTitleAnnotationId]: format(Tuple(...schemas)) }) + } + return class TemplateLiteralParserClass extends transformOrFail(from, to, { + strict: false, + decode: (i, _, ast) => { + const match = re.exec(i) + return match + ? ParseResult.succeed(match.slice(1, params.length + 1)) + : ParseResult.fail(new ParseResult.Type(ast, i, `${re.source}: no match for ${JSON.stringify(i)}`)) + }, + encode: (tuple) => ParseResult.succeed(tuple.join("")) + }) { + static params = params.slice() + } as any +} + +const declareConstructor = < + const TypeParameters extends ReadonlyArray, + I, + A +>( + typeParameters: TypeParameters, + options: { + readonly decode: ( + ...typeParameters: { + readonly [K in keyof TypeParameters]: Schema< + Schema.Type, + Schema.Encoded, + never + > + } + ) => ( + input: unknown, + options: ParseOptions, + ast: AST.Declaration + ) => Effect.Effect + readonly encode: ( + ...typeParameters: { + readonly [K in keyof TypeParameters]: Schema< + Schema.Type, + Schema.Encoded, + never + > + } + ) => ( + input: unknown, + options: ParseOptions, + ast: AST.Declaration + ) => Effect.Effect + }, + annotations?: Annotations.Schema +): SchemaClass> => + makeDeclareClass( + typeParameters, + new AST.Declaration( + typeParameters.map((tp) => tp.ast), + (...typeParameters) => options.decode(...typeParameters.map(make) as any), + (...typeParameters) => options.encode(...typeParameters.map(make) as any), + toASTAnnotations(annotations) + ) + ) + +const declarePrimitive = ( + is: (input: unknown) => input is A, + annotations?: Annotations.Schema +): SchemaClass => { + const decodeUnknown = () => (input: unknown, _: ParseOptions, ast: AST.Declaration) => + is(input) ? ParseResult.succeed(input) : ParseResult.fail(new ParseResult.Type(ast, input)) + const encodeUnknown = decodeUnknown + return makeDeclareClass([], new AST.Declaration([], decodeUnknown, encodeUnknown, toASTAnnotations(annotations))) +} + +/** + * @category api interface + * @since 3.13.3 + */ +export interface declare< + A, + I = A, + P extends ReadonlyArray = readonly [], + R = Schema.Context +> extends AnnotableClass, A, I, R> { + readonly typeParameters: Readonly

+} + +/** + * @category api interface + * @since 3.13.3 + */ +export interface AnnotableDeclare< + Self extends declare, + A, + I = A, + P extends ReadonlyArray = readonly [], + R = Schema.Context +> extends declare { + annotations(annotations: Annotations.Schema): Self +} + +function makeDeclareClass

, A, I, R>( + typeParameters: P, + ast: AST.AST +): declare { + return class DeclareClass extends make(ast) { + static override annotations(annotations: Annotations.Schema): declare { + return makeDeclareClass(this.typeParameters, mergeSchemaAnnotations(this.ast, annotations)) + } + static typeParameters = [...typeParameters] as any as P + } +} + +/** + * The constraint `R extends Schema.Context` enforces dependencies solely from `typeParameters`. + * This ensures that when you call `Schema.to` or `Schema.from`, you receive a schema with a `never` context. + * + * @category constructors + * @since 3.10.0 + */ +export const declare: { + ( + is: (input: unknown) => input is A, + annotations?: Annotations.Schema + ): declare + >( + typeParameters: P, + options: { + readonly decode: ( + ...typeParameters: { readonly [K in keyof P]: Schema, Schema.Encoded, never> } + ) => ( + input: unknown, + options: ParseOptions, + ast: AST.Declaration + ) => Effect.Effect + readonly encode: ( + ...typeParameters: { readonly [K in keyof P]: Schema, Schema.Encoded, never> } + ) => ( + input: unknown, + options: ParseOptions, + ast: AST.Declaration + ) => Effect.Effect + }, + annotations?: Annotations.Schema }> + ): declare +} = function() { + if (Array.isArray(arguments[0])) { + const typeParameters = arguments[0] + const options = arguments[1] + const annotations = arguments[2] + return declareConstructor(typeParameters, options, annotations) + } + const is = arguments[0] + const annotations = arguments[1] + return declarePrimitive(is, annotations) +} as any + +/** + * @category schema id + * @since 3.10.0 + */ +export const BrandSchemaId: unique symbol = Symbol.for("effect/SchemaId/Brand") + +/** + * @category constructors + * @since 3.10.0 + */ +export const fromBrand = , A extends Brand.Unbranded>( + constructor: Brand.Constructor, + annotations?: Annotations.Filter +) => +(self: Schema): BrandSchema => { + const out = makeBrandClass( + self, + new AST.Refinement( + self.ast, + function predicate(a: A, _: ParseOptions, ast: AST.AST): option_.Option { + const either = constructor.either(a) + return either_.isLeft(either) ? + option_.some(new ParseResult.Type(ast, a, either.left.map((v) => v.message).join(", "))) : + option_.none() + }, + toASTAnnotations({ + schemaId: BrandSchemaId, + [BrandSchemaId]: { constructor }, + ...annotations + }) + ) + ) + return out as any +} + +/** + * @category schema id + * @since 3.10.0 + */ +export const InstanceOfSchemaId: unique symbol = Symbol.for("effect/SchemaId/InstanceOf") + +/** + * @category api interface + * @since 3.10.0 + */ +export interface instanceOf extends AnnotableDeclare, A> {} + +/** + * @category constructors + * @since 3.10.0 + */ +export const instanceOf = any>( + constructor: A, + annotations?: Annotations.Schema> +): instanceOf> => + declare( + (u): u is InstanceType => u instanceof constructor, + { + title: constructor.name, + description: `an instance of ${constructor.name}`, + pretty: (): pretty_.Pretty> => String, + schemaId: InstanceOfSchemaId, + [InstanceOfSchemaId]: { constructor }, + ...annotations + } + ) + +/** + * @category primitives + * @since 3.10.0 + */ +export class Undefined extends make(AST.undefinedKeyword) {} + +/** + * @category primitives + * @since 3.10.0 + */ +export class Void extends make(AST.voidKeyword) {} + +/** + * @category primitives + * @since 3.10.0 + */ +export class Null extends make(AST.null) {} + +/** + * @category primitives + * @since 3.10.0 + */ +export class Never extends make(AST.neverKeyword) {} + +/** + * @category primitives + * @since 3.10.0 + */ +export class Unknown extends make(AST.unknownKeyword) {} + +/** + * @category primitives + * @since 3.10.0 + */ +export class Any extends make(AST.anyKeyword) {} + +/** + * @category primitives + * @since 3.10.0 + */ +export class BigIntFromSelf extends make(AST.bigIntKeyword) {} + +/** + * @category primitives + * @since 3.10.0 + */ +export class SymbolFromSelf extends make(AST.symbolKeyword) {} + +/** @ignore */ +class String$ extends make(AST.stringKeyword) {} + +/** @ignore */ +class Number$ extends make(AST.numberKeyword) {} + +/** @ignore */ +class Boolean$ extends make(AST.booleanKeyword) {} + +/** @ignore */ +class Object$ extends make(AST.objectKeyword) {} + +export { + /** + * @category primitives + * @since 3.10.0 + */ + Boolean$ as Boolean, + /** + * @category primitives + * @since 3.10.0 + */ + Number$ as Number, + /** + * @category primitives + * @since 3.10.0 + */ + Object$ as Object, + /** + * @category primitives + * @since 3.10.0 + */ + String$ as String +} + +/** + * @category api interface + * @since 3.10.0 + */ +export interface Union> extends + AnnotableClass< + Union, + Schema.Type, + Schema.Encoded, + Schema.Context + > +{ + readonly members: Readonly +} + +const getDefaultUnionAST = >(members: Members): AST.AST => + AST.Union.make(members.map((m) => m.ast)) + +function makeUnionClass>( + members: Members, + ast: AST.AST = getDefaultUnionAST(members) +): Union { + return class UnionClass extends make< + Schema.Type, + Schema.Encoded, + Schema.Context + >(ast) { + static override annotations(annotations: Annotations.Schema>): Union { + return makeUnionClass(this.members, mergeSchemaAnnotations(this.ast, annotations)) + } + + static members = [...members] + } +} + +/** + * @category combinators + * @since 3.10.0 + */ +export function Union>(...members: Members): Union +export function Union(member: Member): Member +export function Union(): typeof Never +export function Union>( + ...members: Members +): Schema, Schema.Encoded, Schema.Context> +export function Union>( + ...members: Members +) { + return AST.isMembers(members) + ? makeUnionClass(members) + : array_.isNonEmptyReadonlyArray(members) + ? members[0] + : Never +} + +/** + * @category api interface + * @since 3.10.0 + */ +export interface NullOr extends Union<[S, typeof Null]> { + annotations(annotations: Annotations.Schema | null>): NullOr +} + +/** + * @category combinators + * @since 3.10.0 + */ +export const NullOr = (self: S): NullOr => Union(self, Null) + +/** + * @category api interface + * @since 3.10.0 + */ +export interface UndefinedOr extends Union<[S, typeof Undefined]> { + annotations(annotations: Annotations.Schema | undefined>): UndefinedOr +} + +/** + * @category combinators + * @since 3.10.0 + */ +export const UndefinedOr = (self: S): UndefinedOr => Union(self, Undefined) + +/** + * @category api interface + * @since 3.10.0 + */ +export interface NullishOr extends Union<[S, typeof Null, typeof Undefined]> { + annotations(annotations: Annotations.Schema | null | undefined>): NullishOr +} + +/** + * @category combinators + * @since 3.10.0 + */ +export const NullishOr = (self: S): NullishOr => Union(self, Null, Undefined) + +/** + * @category combinators + * @since 3.10.0 + */ +export const keyof = (self: Schema): SchemaClass => make(AST.keyof(self.ast)) + +/** + * @since 3.10.0 + */ +export declare namespace Element { + /** + * @since 3.10.0 + */ + export interface Annotations extends Annotations.Doc { + readonly missingMessage?: AST.MissingMessageAnnotation + } + + /** + * @since 3.10.0 + */ + export type Token = "" | "?" +} + +/** + * @category API interface + * @since 3.10.0 + */ +export interface Element + extends Schema.Variance, Schema.Encoded, Schema.Context> +{ + readonly _Token: Token + readonly ast: AST.OptionalType + readonly from: S + annotations(annotations: Element.Annotations>): Element +} + +/** + * @since 3.10.0 + */ +export const element = (self: S): Element => + new ElementImpl(new AST.OptionalType(self.ast, false), self) + +/** + * @since 3.10.0 + */ +export const optionalElement = (self: S): Element => + new ElementImpl(new AST.OptionalType(self.ast, true), self) + +class ElementImpl implements Element { + readonly [TypeId]!: Schema.Variance, Schema.Encoded, Schema.Context>[TypeId] + readonly _Token!: Token + constructor( + readonly ast: AST.OptionalType, + readonly from: S + ) {} + annotations( + annotations: Annotations.Schema> + ): ElementImpl { + return new ElementImpl( + new AST.OptionalType( + this.ast.type, + this.ast.isOptional, + { ...this.ast.annotations, ...toASTAnnotations(annotations) } + ), + this.from + ) + } + toString() { + return `${this.ast.type}${this.ast.isOptional ? "?" : ""}` + } +} + +/** + * @since 3.10.0 + */ +export declare namespace TupleType { + type ElementsType< + Elements, + Out extends ReadonlyArray = readonly [] + > = Elements extends readonly [infer Head, ...infer Tail] ? + Head extends Element ? ElementsType?]> + : ElementsType]> + : Out + + type ElementsEncoded< + Elements, + Out extends ReadonlyArray = readonly [] + > = Elements extends readonly [infer Head, ...infer Tail] ? + Head extends Element ? ElementsEncoded?]> + : ElementsEncoded]> + : Out + + /** + * @since 3.10.0 + */ + export type Elements = ReadonlyArray> + + /** + * @since 3.10.0 + */ + export type Rest = ReadonlyArray> + + /** + * @since 3.10.0 + */ + export type Type = Rest extends + [infer Head, ...infer Tail] ? Readonly<[ + ...ElementsType, + ...ReadonlyArray>, + ...{ readonly [K in keyof Tail]: Schema.Type } + ]> : + ElementsType + + /** + * @since 3.10.0 + */ + export type Encoded = Rest extends + [infer Head, ...infer Tail] ? Readonly<[ + ...ElementsEncoded, + ...ReadonlyArray>, + ...{ readonly [K in keyof Tail]: Schema.Encoded } + ]> : + ElementsEncoded +} + +/** + * @category api interface + * @since 3.10.0 + */ +export interface TupleType extends + AnnotableClass< + TupleType, + TupleType.Type, + TupleType.Encoded, + Schema.Context | Schema.Context + > +{ + readonly elements: Readonly + readonly rest: Readonly +} + +const getDefaultTupleTypeAST = ( + elements: Elements, + rest: Rest +) => + new AST.TupleType( + elements.map((el) => isSchema(el) ? new AST.OptionalType(el.ast, false) : el.ast), + rest.map((el) => isSchema(el) ? new AST.Type(el.ast) : el.ast), + true + ) + +function makeTupleTypeClass( + elements: Elements, + rest: Rest, + ast: AST.AST = getDefaultTupleTypeAST(elements, rest) +) { + return class TupleTypeClass extends make< + TupleType.Type, + TupleType.Encoded, + Schema.Context | Schema.Context + >(ast) { + static override annotations( + annotations: Annotations.Schema> + ): TupleType { + return makeTupleTypeClass(this.elements, this.rest, mergeSchemaAnnotations(this.ast, annotations)) + } + + static elements = [...elements] as any as Elements + + static rest = [...rest] as any as Rest + } +} + +/** + * @category api interface + * @since 3.10.0 + */ +export interface Tuple extends TupleType { + annotations(annotations: Annotations.Schema>): Tuple +} + +/** + * @category api interface + * @since 3.13.3 + */ +export interface Tuple2 extends + AnnotableClass< + Tuple2, + readonly [Schema.Type, Schema.Type], + readonly [Schema.Encoded, Schema.Encoded], + Schema.Context | Schema.Context + > +{ + readonly elements: readonly [Fst, Snd] + readonly rest: readonly [] +} + +/** + * @category constructors + * @since 3.10.0 + */ +export function Tuple< + const Elements extends TupleType.Elements, + Rest extends array_.NonEmptyReadonlyArray +>(elements: Elements, ...rest: Rest): TupleType +export function Tuple(fst: Fst, snd: Snd): Tuple2 +export function Tuple(...elements: Elements): Tuple +export function Tuple(...args: ReadonlyArray): any { + return Array.isArray(args[0]) + ? makeTupleTypeClass(args[0], args.slice(1)) + : makeTupleTypeClass(args, []) +} + +/** + * @category api interface + * @since 3.10.0 + */ +export interface Array$ extends TupleType<[], [Value]> { + readonly value: Value + annotations(annotations: Annotations.Schema>): Array$ +} + +function makeArrayClass( + value: Value, + ast?: AST.AST +): Array$ { + return class ArrayClass extends makeTupleTypeClass<[], [Value]>([], [value], ast) { + static override annotations(annotations: Annotations.Schema>) { + return makeArrayClass(this.value, mergeSchemaAnnotations(this.ast, annotations)) + } + + static value = value + } +} + +const Array$ = (value: Value): Array$ => makeArrayClass(value) + +export { + /** + * @category constructors + * @since 3.10.0 + */ + Array$ as Array +} + +/** + * @category api interface + * @since 3.10.0 + */ +export interface NonEmptyArray extends + AnnotableClass< + NonEmptyArray, + array_.NonEmptyReadonlyArray>, + array_.NonEmptyReadonlyArray>, + Schema.Context + > +{ + readonly elements: readonly [Value] + readonly rest: readonly [Value] + readonly value: Value +} + +function makeNonEmptyArrayClass( + value: Value, + ast?: AST.AST +) { + return class NonEmptyArrayClass extends makeTupleTypeClass<[Value], [Value]>([value], [value], ast) { + static override annotations(annotations: Annotations.Schema>) { + return makeNonEmptyArrayClass(this.value, mergeSchemaAnnotations(this.ast, annotations)) + } + + static value = value + } +} + +/** + * @category constructors + * @since 3.10.0 + */ +export const NonEmptyArray = (value: Value): NonEmptyArray => + makeNonEmptyArrayClass(value) as any + +/** + * @category api interface + * @since 3.10.0 + */ +export interface ArrayEnsure + extends transform]>, Array$>>> +{} + +/** + * @category constructors + * @since 3.10.0 + */ +export function ArrayEnsure(value: Value): ArrayEnsure { + return transform(Union(value, Array$(value)), Array$(typeSchema(asSchema(value))), { + strict: true, + decode: (i) => array_.ensure(i), + encode: (a) => a.length === 1 ? a[0] : a + }) +} + +/** + * @category api interface + * @since 3.10.0 + */ +export interface NonEmptyArrayEnsure + extends transform]>, NonEmptyArray>>> +{} + +/** + * @category constructors + * @since 3.10.0 + */ +export function NonEmptyArrayEnsure(value: Value): NonEmptyArrayEnsure { + return transform(Union(value, NonEmptyArray(value)), NonEmptyArray(typeSchema(asSchema(value))), { + strict: true, + decode: (i) => array_.isNonEmptyReadonlyArray(i) ? i : array_.of(i), + encode: (a) => a.length === 1 ? a[0] : a + }) +} + +/** + * @since 3.10.0 + */ +export declare namespace PropertySignature { + /** + * @since 3.10.0 + */ + export type Token = "?:" | ":" + + /** + * @since 3.10.0 + */ + export type Any = PropertySignature< + Token, + any, + Key, + Token, + any, + boolean, + unknown + > + + /** + * @since 3.10.0 + */ + export type All = + | Any + | PropertySignature + | PropertySignature + | PropertySignature + + /** + * @since 3.10.0 + */ + export type AST = + | PropertySignatureDeclaration + | PropertySignatureTransformation + + /** + * @since 3.10.0 + */ + export interface Annotations extends Annotations.Doc { + readonly missingMessage?: AST.MissingMessageAnnotation + } +} + +const formatPropertySignatureToken = (isOptional: boolean): string => isOptional ? "\"?:\"" : "\":\"" + +/** + * @category PropertySignature + * @since 3.10.0 + */ +export class PropertySignatureDeclaration extends AST.OptionalType { + /** + * @since 3.10.0 + */ + readonly _tag = "PropertySignatureDeclaration" + constructor( + type: AST.AST, + isOptional: boolean, + readonly isReadonly: boolean, + annotations: AST.Annotations, + readonly defaultValue: (() => unknown) | undefined + ) { + super(type, isOptional, annotations) + } + /** + * @since 3.10.0 + */ + toString() { + const token = formatPropertySignatureToken(this.isOptional) + const type = String(this.type) + return `PropertySignature<${token}, ${type}, never, ${token}, ${type}>` + } +} + +/** + * @category PropertySignature + * @since 3.10.0 + */ +export class FromPropertySignature extends AST.OptionalType { + constructor( + type: AST.AST, + isOptional: boolean, + readonly isReadonly: boolean, + annotations: AST.Annotations, + readonly fromKey?: PropertyKey | undefined + ) { + super(type, isOptional, annotations) + } +} + +/** + * @category PropertySignature + * @since 3.10.0 + */ +export class ToPropertySignature extends AST.OptionalType { + constructor( + type: AST.AST, + isOptional: boolean, + readonly isReadonly: boolean, + annotations: AST.Annotations, + readonly defaultValue: (() => unknown) | undefined + ) { + super(type, isOptional, annotations) + } +} + +const formatPropertyKey = (p: PropertyKey | undefined): string => { + if (p === undefined) { + return "never" + } + if (Predicate.isString(p)) { + return JSON.stringify(p) + } + return String(p) +} + +/** + * @category PropertySignature + * @since 3.10.0 + */ +export class PropertySignatureTransformation { + /** + * @since 3.10.0 + */ + readonly _tag = "PropertySignatureTransformation" + constructor( + readonly from: FromPropertySignature, + readonly to: ToPropertySignature, + readonly decode: AST.PropertySignatureTransformation["decode"], + readonly encode: AST.PropertySignatureTransformation["encode"] + ) {} + /** + * @since 3.10.0 + */ + toString() { + return `PropertySignature<${formatPropertySignatureToken(this.to.isOptional)}, ${this.to.type}, ${ + formatPropertyKey(this.from.fromKey) + }, ${formatPropertySignatureToken(this.from.isOptional)}, ${this.from.type}>` + } +} + +const mergeSignatureAnnotations = ( + ast: PropertySignature.AST, + annotations: AST.Annotations +): PropertySignature.AST => { + switch (ast._tag) { + case "PropertySignatureDeclaration": { + return new PropertySignatureDeclaration( + ast.type, + ast.isOptional, + ast.isReadonly, + { ...ast.annotations, ...annotations }, + ast.defaultValue + ) + } + case "PropertySignatureTransformation": { + return new PropertySignatureTransformation( + ast.from, + new ToPropertySignature(ast.to.type, ast.to.isOptional, ast.to.isReadonly, { + ...ast.to.annotations, + ...annotations + }, ast.to.defaultValue), + ast.decode, + ast.encode + ) + } + } +} + +/** + * @since 3.10.0 + * @category symbol + */ +export const PropertySignatureTypeId: unique symbol = Symbol.for("effect/PropertySignature") + +/** + * @since 3.10.0 + * @category symbol + */ +export type PropertySignatureTypeId = typeof PropertySignatureTypeId + +/** + * @since 3.10.0 + * @category guards + */ +export const isPropertySignature = (u: unknown): u is PropertySignature.All => + Predicate.hasProperty(u, PropertySignatureTypeId) + +/** + * @category PropertySignature + * @since 3.10.0 + */ +export interface PropertySignature< + TypeToken extends PropertySignature.Token, + Type, + Key extends PropertyKey, + EncodedToken extends PropertySignature.Token, + Encoded, + HasDefault extends boolean = false, + R = never +> extends Schema.Variance, Pipeable { + readonly [PropertySignatureTypeId]: null + readonly _TypeToken: TypeToken + readonly _EncodedToken: EncodedToken + readonly _HasDefault: HasDefault + readonly _Key: Key + readonly ast: PropertySignature.AST + + annotations( + annotations: PropertySignature.Annotations + ): PropertySignature +} + +class PropertySignatureImpl< + TypeToken extends PropertySignature.Token, + Type, + Key extends PropertyKey, + EncodedToken extends PropertySignature.Token, + Encoded, + HasDefault extends boolean = false, + R = never +> implements PropertySignature { + readonly [TypeId]!: Schema.Variance[TypeId] + readonly [PropertySignatureTypeId] = null + readonly _TypeToken!: TypeToken + readonly _Key!: Key + readonly _EncodedToken!: EncodedToken + readonly _HasDefault!: HasDefault + + constructor( + readonly ast: PropertySignature.AST + ) {} + + pipe() { + return pipeArguments(this, arguments) + } + + annotations( + annotations: PropertySignature.Annotations + ): PropertySignature { + return new PropertySignatureImpl(mergeSignatureAnnotations(this.ast, toASTAnnotations(annotations))) + } + + toString() { + return String(this.ast) + } +} + +/** + * @category PropertySignature + * @since 3.10.0 + */ +export const makePropertySignature = < + TypeToken extends PropertySignature.Token, + Type, + Key extends PropertyKey, + EncodedToken extends PropertySignature.Token, + Encoded, + HasDefault extends boolean = false, + R = never +>(ast: PropertySignature.AST) => + new PropertySignatureImpl(ast) + +class PropertySignatureWithFromImpl< + From extends Schema.All, + TypeToken extends PropertySignature.Token, + Type, + Key extends PropertyKey, + EncodedToken extends PropertySignature.Token, + Encoded, + HasDefault extends boolean = false, + R = never +> extends PropertySignatureImpl { + constructor(ast: PropertySignature.AST, readonly from: From) { + super(ast) + } + annotations( + annotations: PropertySignature.Annotations + ): PropertySignatureWithFromImpl { + return new PropertySignatureWithFromImpl( + mergeSignatureAnnotations(this.ast, toASTAnnotations(annotations)), + this.from + ) + } +} + +/** + * @category API interface + * @since 1.0.0 + */ +export interface propertySignature + extends PropertySignature<":", Schema.Type, never, ":", Schema.Encoded, false, Schema.Context> +{ + readonly from: S + annotations(annotations: PropertySignature.Annotations>): propertySignature +} + +/** + * Lifts a `Schema` into a `PropertySignature`. + * + * @category PropertySignature + * @since 3.10.0 + */ +export const propertySignature = ( + self: S +): propertySignature => + new PropertySignatureWithFromImpl( + new PropertySignatureDeclaration(self.ast, false, true, {}, undefined), + self + ) + +/** + * Enhances a property signature with a default constructor value. + * + * @category PropertySignature + * @since 3.10.0 + */ +export const withConstructorDefault: { + (defaultValue: () => Types.NoInfer): < + TypeToken extends PropertySignature.Token, + Key extends PropertyKey, + EncodedToken extends PropertySignature.Token, + Encoded, + R + >( + self: PropertySignature + ) => PropertySignature + < + TypeToken extends PropertySignature.Token, + Type, + Key extends PropertyKey, + EncodedToken extends PropertySignature.Token, + Encoded, + R + >( + self: PropertySignature, + defaultValue: () => Types.NoInfer + ): PropertySignature +} = dual(2, < + TypeToken extends PropertySignature.Token, + Type, + Key extends PropertyKey, + EncodedToken extends PropertySignature.Token, + Encoded, + R +>( + self: PropertySignature, + defaultValue: () => Types.NoInfer +): PropertySignature => { + const ast = self.ast + switch (ast._tag) { + case "PropertySignatureDeclaration": + return makePropertySignature( + new PropertySignatureDeclaration(ast.type, ast.isOptional, ast.isReadonly, ast.annotations, defaultValue) + ) + case "PropertySignatureTransformation": + return makePropertySignature( + new PropertySignatureTransformation( + ast.from, + new ToPropertySignature(ast.to.type, ast.to.isOptional, ast.to.isReadonly, ast.to.annotations, defaultValue), + ast.decode, + ast.encode + ) + ) + } +}) + +const applyDefaultValue = (o: option_.Option, defaultValue: () => A) => + option_.match(o, { + onNone: () => option_.some(defaultValue()), + onSome: (value) => option_.some(value === undefined ? defaultValue() : value) + }) + +const pruneUndefined = (ast: AST.AST): AST.AST | undefined => + AST.pruneUndefined(ast, pruneUndefined, (ast) => { + const pruned = pruneUndefined(ast.to) + if (pruned) { + return new AST.Transformation(ast.from, pruned, ast.transformation) + } + }) + +/** + * Enhances a property signature with a default decoding value. + * + * @category PropertySignature + * @since 3.10.0 + */ +export const withDecodingDefault: { + (defaultValue: () => Types.NoInfer>): < + Key extends PropertyKey, + Encoded, + R + >( + self: PropertySignature<"?:", Type, Key, "?:", Encoded, false, R> + ) => PropertySignature<":", Exclude, Key, "?:", Encoded, false, R> + < + Type, + Key extends PropertyKey, + Encoded, + R + >( + self: PropertySignature<"?:", Type, Key, "?:", Encoded, false, R>, + defaultValue: () => Types.NoInfer> + ): PropertySignature<":", Exclude, Key, "?:", Encoded, false, R> +} = dual(2, < + Type, + Key extends PropertyKey, + Encoded, + R +>( + self: PropertySignature<"?:", Type, Key, "?:", Encoded, false, R>, + defaultValue: () => Types.NoInfer> +): PropertySignature<":", Exclude, Key, "?:", Encoded, false, R> => { + const ast = self.ast + switch (ast._tag) { + case "PropertySignatureDeclaration": { + const to = AST.typeAST(ast.type) + return makePropertySignature( + new PropertySignatureTransformation( + new FromPropertySignature(ast.type, ast.isOptional, ast.isReadonly, ast.annotations), + new ToPropertySignature(pruneUndefined(to) ?? to, false, true, {}, ast.defaultValue), + (o) => applyDefaultValue(o, defaultValue), + identity + ) + ) + } + case "PropertySignatureTransformation": { + const to = ast.to.type + return makePropertySignature( + new PropertySignatureTransformation( + ast.from, + new ToPropertySignature( + pruneUndefined(to) ?? to, + false, + ast.to.isReadonly, + ast.to.annotations, + ast.to.defaultValue + ), + (o) => applyDefaultValue(ast.decode(o), defaultValue), + ast.encode + ) + ) + } + } +}) + +/** + * Enhances a property signature with a default decoding value and a default constructor value. + * + * @category PropertySignature + * @since 3.10.0 + */ +export const withDefaults: { + (defaults: { + constructor: () => Types.NoInfer> + decoding: () => Types.NoInfer> + }): < + Key extends PropertyKey, + Encoded, + R + >( + self: PropertySignature<"?:", Type, Key, "?:", Encoded, boolean, R> + ) => PropertySignature<":", Exclude, Key, "?:", Encoded, true, R> + < + Type, + Key extends PropertyKey, + Encoded, + R + >( + self: PropertySignature<"?:", Type, Key, "?:", Encoded, boolean, R>, + defaults: { + constructor: () => Types.NoInfer> + decoding: () => Types.NoInfer> + } + ): PropertySignature<":", Exclude, Key, "?:", Encoded, true, R> +} = dual(2, < + Type, + Key extends PropertyKey, + Encoded, + R +>( + self: PropertySignature<"?:", Type, Key, "?:", Encoded, false, R>, + defaults: { + constructor: () => Types.NoInfer> + decoding: () => Types.NoInfer> + } +): PropertySignature<":", Exclude, Key, "?:", Encoded, true, R> => + self.pipe(withDecodingDefault(defaults.decoding), withConstructorDefault(defaults.constructor))) + +/** + * Enhances a property signature by specifying a different key for it in the Encoded type. + * + * @category PropertySignature + * @since 3.10.0 + */ +export const fromKey: { + (key: Key): < + TypeToken extends PropertySignature.Token, + Type, + EncodedToken extends PropertySignature.Token, + Encoded, + HasDefault extends boolean, + R + >( + self: PropertySignature + ) => PropertySignature + < + Type, + TypeToken extends PropertySignature.Token, + Encoded, + EncodedToken extends PropertySignature.Token, + HasDefault extends boolean, + R, + Key extends PropertyKey + >( + self: PropertySignature, + key: Key + ): PropertySignature +} = dual(2, < + Type, + TypeToken extends PropertySignature.Token, + Encoded, + EncodedToken extends PropertySignature.Token, + HasDefault extends boolean, + R, + Key extends PropertyKey +>( + self: PropertySignature, + key: Key +): PropertySignature => { + const ast = self.ast + switch (ast._tag) { + case "PropertySignatureDeclaration": { + return makePropertySignature( + new PropertySignatureTransformation( + new FromPropertySignature( + ast.type, + ast.isOptional, + ast.isReadonly, + ast.annotations, + key + ), + new ToPropertySignature(AST.typeAST(ast.type), ast.isOptional, ast.isReadonly, {}, ast.defaultValue), + identity, + identity + ) + ) + } + case "PropertySignatureTransformation": + return makePropertySignature( + new PropertySignatureTransformation( + new FromPropertySignature( + ast.from.type, + ast.from.isOptional, + ast.from.isReadonly, + ast.from.annotations, + key + ), + ast.to, + ast.decode, + ast.encode + ) + ) + } +}) + +/** + * Converts an optional property to a required one through a transformation `Option -> Type`. + * + * - `decode`: `none` as argument means the value is missing in the input. + * - `encode`: `none` as return value means the value will be missing in the output. + * + * @category PropertySignature + * @since 3.10.0 + */ +export const optionalToRequired = ( + from: Schema, + to: Schema, + options: { + readonly decode: (o: option_.Option) => TI + readonly encode: (ti: TI) => option_.Option + } +): PropertySignature<":", TA, never, "?:", FI, false, FR | TR> => + makePropertySignature( + new PropertySignatureTransformation( + new FromPropertySignature(from.ast, true, true, {}, undefined), + new ToPropertySignature(to.ast, false, true, {}, undefined), + (o) => option_.some(options.decode(o)), + option_.flatMap(options.encode) + ) + ) + +/** + * Converts an optional property to a required one through a transformation `Type -> Option`. + * + * - `decode`: `none` as return value means the value will be missing in the output. + * - `encode`: `none` as argument means the value is missing in the input. + * + * @category PropertySignature + * @since 3.10.0 + */ +export const requiredToOptional = ( + from: Schema, + to: Schema, + options: { + readonly decode: (fa: FA) => option_.Option + readonly encode: (o: option_.Option) => FA + } +): PropertySignature<"?:", TA, never, ":", FI, false, FR | TR> => + makePropertySignature( + new PropertySignatureTransformation( + new FromPropertySignature(from.ast, false, true, {}, undefined), + new ToPropertySignature(to.ast, true, true, {}, undefined), + option_.flatMap(options.decode), + (o) => option_.some(options.encode(o)) + ) + ) + +/** + * Converts an optional property to another optional property through a transformation `Option -> Option`. + * + * - `decode`: + * - `none` as argument means the value is missing in the input. + * - `none` as return value means the value will be missing in the output. + * - `encode`: + * - `none` as argument means the value is missing in the input. + * - `none` as return value means the value will be missing in the output. + * + * @category PropertySignature + * @since 3.10.0 + */ +export const optionalToOptional = ( + from: Schema, + to: Schema, + options: { + readonly decode: (o: option_.Option) => option_.Option + readonly encode: (o: option_.Option) => option_.Option + } +): PropertySignature<"?:", TA, never, "?:", FI, false, FR | TR> => + makePropertySignature( + new PropertySignatureTransformation( + new FromPropertySignature(from.ast, true, true, {}, undefined), + new ToPropertySignature(to.ast, true, true, {}, undefined), + options.decode, + options.encode + ) + ) + +/** + * @since 3.10.0 + */ +export type OptionalOptions = { + readonly default?: never + readonly as?: never + readonly exact?: true + readonly nullable?: true +} | { + readonly default: LazyArg + readonly as?: never + readonly exact?: true + readonly nullable?: true +} | { + readonly as: "Option" + readonly default?: never + readonly exact?: never + readonly nullable?: never + readonly onNoneEncoding?: LazyArg> +} | { + readonly as: "Option" + readonly default?: never + readonly exact?: never + readonly nullable: true + readonly onNoneEncoding?: LazyArg> +} | { + readonly as: "Option" + readonly default?: never + readonly exact: true + readonly nullable?: never + readonly onNoneEncoding?: never +} | { + readonly as: "Option" + readonly default?: never + readonly exact: true + readonly nullable: true + readonly onNoneEncoding?: LazyArg> +} | undefined + +/** + * @category api interface + * @since 3.10.0 + */ +export interface optional extends + PropertySignature< + "?:", + Schema.Type | undefined, + never, + "?:", + Schema.Encoded | undefined, + false, + Schema.Context + > +{ + readonly from: S + annotations(annotations: PropertySignature.Annotations | undefined>): optional +} + +/** + * @category api interface + * @since 3.10.0 + */ +export interface optionalWith extends + PropertySignature< + Types.Has extends true ? ":" : "?:", + | (Types.Has extends true ? option_.Option> : Schema.Type) + | (Types.Has extends true ? never : undefined), + never, + "?:", + | Schema.Encoded + | (Types.Has extends true ? null : never) + | (Types.Has extends true ? never : undefined), + Types.Has, + Schema.Context + > +{ + readonly from: S + annotations( + annotations: PropertySignature.Annotations< + | (Types.Has extends true ? option_.Option> : Schema.Type) + | (Types.Has extends true ? never : undefined) + > + ): optionalWith +} + +const optionalPropertySignatureAST = ( + self: Schema, + options?: { + readonly exact?: true + readonly default?: () => A + readonly nullable?: true + readonly as?: "Option" + readonly onNoneEncoding?: () => option_.Option + } +): PropertySignature.AST => { + const isExact = options?.exact + const defaultValue = options?.default + const isNullable = options?.nullable + const asOption = options?.as == "Option" + const asOptionEncode = options?.onNoneEncoding ? option_.orElse(options.onNoneEncoding) : identity + + if (isExact) { + if (defaultValue) { + if (isNullable) { + return withConstructorDefault( + optionalToRequired( + NullOr(self), + typeSchema(self), + { + decode: option_.match({ onNone: defaultValue, onSome: (a) => a === null ? defaultValue() : a }), + encode: option_.some + } + ), + defaultValue + ).ast + } else { + return withConstructorDefault( + optionalToRequired( + self, + typeSchema(self), + { decode: option_.match({ onNone: defaultValue, onSome: identity }), encode: option_.some } + ), + defaultValue + ).ast + } + } else if (asOption) { + const to = OptionFromSelf_(typeSchema(self)) + if (isNullable) { + return optionalToRequired( + NullOr(self), + to, + { + decode: option_.filter(Predicate.isNotNull), + encode: asOptionEncode + } + ).ast + } else { + return optionalToRequired( + self, + to, + { decode: identity, encode: identity } + ).ast + } + } else { + if (isNullable) { + return optionalToOptional( + NullOr(self), + typeSchema(self), + { decode: option_.filter(Predicate.isNotNull), encode: identity } + ).ast + } else { + return new PropertySignatureDeclaration(self.ast, true, true, {}, undefined) + } + } + } else { + if (defaultValue) { + if (isNullable) { + return withConstructorDefault( + optionalToRequired( + NullishOr(self), + typeSchema(self), + { + decode: option_.match({ onNone: defaultValue, onSome: (a) => (a == null ? defaultValue() : a) }), + encode: option_.some + } + ), + defaultValue + ).ast + } else { + return withConstructorDefault( + optionalToRequired( + UndefinedOr(self), + typeSchema(self), + { + decode: option_.match({ onNone: defaultValue, onSome: (a) => (a === undefined ? defaultValue() : a) }), + encode: option_.some + } + ), + defaultValue + ).ast + } + } else if (asOption) { + const to = OptionFromSelf_(typeSchema(self)) + if (isNullable) { + return optionalToRequired( + NullishOr(self), + to, + { + decode: option_.filter((a): a is A => a != null), + encode: asOptionEncode + } + ).ast + } else { + return optionalToRequired( + UndefinedOr(self), + to, + { + decode: option_.filter(Predicate.isNotUndefined), + encode: asOptionEncode + } + ).ast + } + } else { + if (isNullable) { + return optionalToOptional( + NullishOr(self), + UndefinedOr(typeSchema(self)), + { decode: option_.filter(Predicate.isNotNull), encode: identity } + ).ast + } else { + return new PropertySignatureDeclaration(UndefinedOr(self).ast, true, true, {}, undefined) + } + } + } +} + +/** + * @category PropertySignature + * @since 3.10.0 + */ +export const optional = (self: S): optional => { + const ast = self.ast === AST.undefinedKeyword || self.ast === AST.neverKeyword + ? AST.undefinedKeyword + : UndefinedOr(self).ast + return new PropertySignatureWithFromImpl(new PropertySignatureDeclaration(ast, true, true, {}, undefined), self) +} + +/** + * @category PropertySignature + * @since 3.10.0 + */ +export const optionalWith: { + >>( + options: Options + ): (self: S) => optionalWith + >>( + self: S, + options: Options + ): optionalWith +} = dual((args) => isSchema(args[0]), (self, options) => { + return new PropertySignatureWithFromImpl(optionalPropertySignatureAST(self, options), self) +}) + +/** + * @since 3.10.0 + */ +export declare namespace Struct { + /** + * Useful for creating a type that can be used to add custom constraints to the fields of a struct. + * + * ```ts + * import { Schema } from "effect" + * + * const f = >( + * schema: Schema.Struct + * ) => { + * return schema.omit("a") + * } + * + * // ┌─── Schema.Struct<{ b: typeof Schema.Number; }> + * // ▼ + * const result = f(Schema.Struct({ a: Schema.String, b: Schema.Number })) + * ``` + * @since 3.13.11 + */ + export type Field = + | Schema.All + | PropertySignature.All + + /** + * @since 3.10.0 + */ + export type Fields = { readonly [x: PropertyKey]: Field } + + type OptionalEncodedPropertySignature = + | PropertySignature + | PropertySignature + | PropertySignature + | PropertySignature + + type EncodedOptionalKeys = { + [K in keyof Fields]: Fields[K] extends OptionalEncodedPropertySignature ? K + : never + }[keyof Fields] + + type OptionalTypePropertySignature = + | PropertySignature<"?:", any, PropertyKey, PropertySignature.Token, any, boolean, unknown> + | PropertySignature<"?:", any, PropertyKey, PropertySignature.Token, never, boolean, unknown> + | PropertySignature<"?:", never, PropertyKey, PropertySignature.Token, any, boolean, unknown> + | PropertySignature<"?:", never, PropertyKey, PropertySignature.Token, never, boolean, unknown> + + // type TypeOptionalKeys = { + // [K in keyof Fields]: Fields[K] extends OptionalTypePropertySignature ? K : never + // }[keyof Fields] + + /** + * @since 3.10.0 + */ + export type Type = Types.UnionToIntersection< + { + [K in keyof F]: F[K] extends OptionalTypePropertySignature ? { readonly [H in K]?: Schema.Type } : + { readonly [h in K]: Schema.Type } + }[keyof F] + > extends infer Q ? Q : never + + type Key = [K] extends [never] ? never : + F[K] extends PropertySignature.All ? [Key] extends [never] ? K : Key : + K + + /** + * @since 3.10.0 + */ + export type Encoded = + & { readonly [K in Exclude> as Key]: Schema.Encoded } + & { readonly [K in EncodedOptionalKeys as Key]?: Schema.Encoded } + + /** + * @since 3.10.0 + */ + export type Context = Schema.Context + + type PropertySignatureWithDefault = + | PropertySignature + | PropertySignature + | PropertySignature + | PropertySignature + + /** + * @since 3.10.0 + */ + export type Constructor = Types.UnionToIntersection< + { + [K in keyof F]: F[K] extends OptionalTypePropertySignature ? { readonly [H in K]?: Schema.Type } : + F[K] extends PropertySignatureWithDefault ? { readonly [H in K]?: Schema.Type } : + { readonly [h in K]: Schema.Type } + }[keyof F] + > extends infer Q ? Q : never +} + +/** + * @since 3.10.0 + */ +export declare namespace IndexSignature { + /** + * @since 3.10.0 + */ + export type Record = { readonly key: Schema.All; readonly value: Schema.All } + + /** + * @since 3.10.0 + */ + export type Records = ReadonlyArray + + /** + * @since 3.10.0 + */ + export type NonEmptyRecords = array_.NonEmptyReadonlyArray + + type MergeTuple> = T extends readonly [infer Head, ...infer Tail] ? + Head & MergeTuple + : {} + + /** + * @since 3.10.0 + */ + export type Type = MergeTuple< + { + readonly [K in keyof Records]: { + readonly [P in Schema.Type]: Schema.Type + } + } + > + + /** + * @since 3.10.0 + */ + export type Encoded = MergeTuple< + { + readonly [K in keyof Records]: { + readonly [P in Schema.Encoded]: Schema.Encoded + } + } + > + + /** + * @since 3.10.0 + */ + export type Context = { + [K in keyof Records]: Schema.Context | Schema.Context + }[number] +} + +/** + * @since 3.10.0 + */ +export declare namespace TypeLiteral { + /** + * @since 3.10.0 + */ + export type Type = + & Struct.Type + & IndexSignature.Type + + /** + * @since 3.10.0 + */ + export type Encoded = + & Struct.Encoded + & IndexSignature.Encoded + + /** + * @since 3.10.0 + */ + export type Constructor = + & Struct.Constructor + & IndexSignature.Type +} + +/** + * @category api interface + * @since 3.10.0 + */ +export interface TypeLiteral< + Fields extends Struct.Fields, + Records extends IndexSignature.Records +> extends + AnnotableClass< + TypeLiteral, + Simplify>, + Simplify>, + | Struct.Context + | IndexSignature.Context + > +{ + readonly fields: Readonly + readonly records: Readonly + annotations( + annotations: Annotations.Schema>> + ): TypeLiteral + make( + props: RequiredKeys> extends never + ? void | Simplify> + : Simplify>, + options?: MakeOptions + ): Simplify> +} + +const preserveMissingMessageAnnotation = AST.pickAnnotations([AST.MissingMessageAnnotationId]) + +const getDefaultTypeLiteralAST = < + Fields extends Struct.Fields, + const Records extends IndexSignature.Records +>(fields: Fields, records: Records) => { + const ownKeys = Reflect.ownKeys(fields) + const pss: Array = [] + if (ownKeys.length > 0) { + const from: Array = [] + const to: Array = [] + const transformations: Array = [] + for (let i = 0; i < ownKeys.length; i++) { + const key = ownKeys[i] + const field = fields[key] + if (isPropertySignature(field)) { + const ast: PropertySignature.AST = field.ast + switch (ast._tag) { + case "PropertySignatureDeclaration": { + const type = ast.type + const isOptional = ast.isOptional + const toAnnotations = ast.annotations + from.push(new AST.PropertySignature(key, type, isOptional, true, preserveMissingMessageAnnotation(ast))) + to.push(new AST.PropertySignature(key, AST.typeAST(type), isOptional, true, toAnnotations)) + pss.push( + new AST.PropertySignature(key, type, isOptional, true, toAnnotations) + ) + break + } + case "PropertySignatureTransformation": { + const fromKey = ast.from.fromKey ?? key + from.push( + new AST.PropertySignature(fromKey, ast.from.type, ast.from.isOptional, true, ast.from.annotations) + ) + to.push( + new AST.PropertySignature(key, ast.to.type, ast.to.isOptional, true, ast.to.annotations) + ) + transformations.push(new AST.PropertySignatureTransformation(fromKey, key, ast.decode, ast.encode)) + break + } + } + } else { + from.push(new AST.PropertySignature(key, field.ast, false, true)) + to.push(new AST.PropertySignature(key, AST.typeAST(field.ast), false, true)) + pss.push(new AST.PropertySignature(key, field.ast, false, true)) + } + } + if (array_.isNonEmptyReadonlyArray(transformations)) { + const issFrom: Array = [] + const issTo: Array = [] + for (const r of records) { + const { indexSignatures, propertySignatures } = AST.record(r.key.ast, r.value.ast) + propertySignatures.forEach((ps) => { + from.push(ps) + to.push( + new AST.PropertySignature(ps.name, AST.typeAST(ps.type), ps.isOptional, ps.isReadonly, ps.annotations) + ) + }) + indexSignatures.forEach((is) => { + issFrom.push(is) + issTo.push(new AST.IndexSignature(is.parameter, AST.typeAST(is.type), is.isReadonly)) + }) + } + return new AST.Transformation( + new AST.TypeLiteral(from, issFrom, { [AST.AutoTitleAnnotationId]: "Struct (Encoded side)" }), + new AST.TypeLiteral(to, issTo, { [AST.AutoTitleAnnotationId]: "Struct (Type side)" }), + new AST.TypeLiteralTransformation(transformations) + ) + } + } + const iss: Array = [] + for (const r of records) { + const { indexSignatures, propertySignatures } = AST.record(r.key.ast, r.value.ast) + propertySignatures.forEach((ps) => pss.push(ps)) + indexSignatures.forEach((is) => iss.push(is)) + } + return new AST.TypeLiteral(pss, iss) +} + +const lazilyMergeDefaults = ( + fields: Struct.Fields, + out: Record +): { [x: string | symbol]: unknown } => { + const ownKeys = Reflect.ownKeys(fields) + for (const key of ownKeys) { + const field = fields[key] + if (out[key] === undefined && isPropertySignature(field)) { + const ast = field.ast + const defaultValue = ast._tag === "PropertySignatureDeclaration" ? ast.defaultValue : ast.to.defaultValue + if (defaultValue !== undefined) { + out[key] = defaultValue() + } + } + } + return out +} + +function makeTypeLiteralClass( + fields: Fields, + records: Records, + ast: AST.AST = getDefaultTypeLiteralAST(fields, records) +): TypeLiteral { + return class TypeLiteralClass extends make< + Simplify>, + Simplify>, + | Struct.Context + | IndexSignature.Context + >(ast) { + static override annotations( + annotations: Annotations.Schema>> + ): TypeLiteral { + return makeTypeLiteralClass(this.fields, this.records, mergeSchemaAnnotations(this.ast, annotations)) + } + + static fields = { ...fields } + + static records = [...records] as Records + + static make = ( + props: Simplify>, + options?: MakeOptions + ): Simplify> => { + const propsWithDefaults: any = lazilyMergeDefaults(fields, { ...props as any }) + return getDisableValidationMakeOption(options) + ? propsWithDefaults + : ParseResult.validateSync(this)(propsWithDefaults) + } + + static pick(...keys: Array): Struct>> { + return Struct(struct_.pick(fields, ...keys) as any) + } + + static omit(...keys: Array): Struct>> { + return Struct(struct_.omit(fields, ...keys) as any) + } + } +} + +/** + * @category api interface + * @since 3.10.0 + */ +export interface Struct extends + AnnotableClass< + Struct, + Simplify>, + Simplify>, + Struct.Context + > +{ + readonly fields: Readonly + readonly records: readonly [] + make( + props: RequiredKeys> extends never ? void | Simplify> + : Simplify>, + options?: MakeOptions + ): Simplify> + + annotations(annotations: Annotations.Schema>>): Struct + pick>(...keys: Keys): Struct>> + omit>(...keys: Keys): Struct>> +} + +/** + * @category constructors + * @since 3.10.0 + */ +export function Struct( + fields: Fields, + ...records: Records +): TypeLiteral +export function Struct(fields: Fields): Struct +export function Struct( + fields: Fields, + ...records: Records +): TypeLiteral { + return makeTypeLiteralClass(fields, records) +} + +/** + * @category api interface + * @since 3.10.0 + */ +export interface tag extends PropertySignature<":", Tag, never, ":", Tag, true, never> {} + +/** + * Returns a property signature that represents a tag. + * A tag is a literal value that is used to distinguish between different types of objects. + * The tag is optional when using the `make` method. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { Schema } from "effect" + * + * const User = Schema.Struct({ + * _tag: Schema.tag("User"), + * name: Schema.String, + * age: Schema.Number + * }) + * + * assert.deepStrictEqual(User.make({ name: "John", age: 44 }), { _tag: "User", name: "John", age: 44 }) + * ``` + * + * @see {@link TaggedStruct} + * + * @since 3.10.0 + */ +export const tag = (tag: Tag): tag => + Literal(tag).pipe(propertySignature, withConstructorDefault(() => tag)) + +/** + * @category api interface + * @since 3.10.0 + */ +export type TaggedStruct = Struct< + { _tag: tag } & Fields +> + +/** + * A tagged struct is a struct that has a tag property that is used to distinguish between different types of objects. + * + * The tag is optional when using the `make` method. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { Schema } from "effect" + * + * const User = Schema.TaggedStruct("User", { + * name: Schema.String, + * age: Schema.Number + * }) + * + * assert.deepStrictEqual(User.make({ name: "John", age: 44 }), { _tag: "User", name: "John", age: 44 }) + * ``` + * + * @category constructors + * @since 3.10.0 + */ +export const TaggedStruct = ( + value: Tag, + fields: Fields +): TaggedStruct => Struct({ _tag: tag(value), ...fields }) + +/** + * @category api interface + * @since 3.10.0 + */ +export interface Record$ extends + AnnotableClass< + Record$, + { readonly [P in Schema.Type]: Schema.Type }, + { readonly [P in Schema.Encoded]: Schema.Encoded }, + | Schema.Context + | Schema.Context + > +{ + readonly fields: {} + readonly records: readonly [{ readonly key: K; readonly value: V }] + readonly key: K + readonly value: V + make( + props: void | { readonly [P in Schema.Type]: Schema.Type }, + options?: MakeOptions + ): { readonly [P in Schema.Type]: Schema.Type } + annotations(annotations: Annotations.Schema<{ readonly [P in Schema.Type]: Schema.Type }>): Record$ +} + +function makeRecordClass( + key: K, + value: V, + ast?: AST.AST +): Record$ { + return class RecordClass extends makeTypeLiteralClass({}, [{ key, value }], ast) { + static override annotations( + annotations: Annotations.Schema<{ readonly [P in Schema.Type]: Schema.Type }> + ): Record$ { + return makeRecordClass(key, value, mergeSchemaAnnotations(this.ast, annotations)) + } + + static key = key + + static value = value + } +} + +/** + * @category constructors + * @since 3.10.0 + */ +export const Record = ( + options: { readonly key: K; readonly value: V } +): Record$ => makeRecordClass(options.key, options.value) + +/** + * @category struct transformations + * @since 3.10.0 + */ +export const pick = >(...keys: Keys) => +( + self: Schema +): SchemaClass>, Simplify>, R> => make(AST.pick(self.ast, keys)) + +/** + * @category struct transformations + * @since 3.10.0 + */ +export const omit = >(...keys: Keys) => +( + self: Schema +): SchemaClass>, Simplify>, R> => make(AST.omit(self.ast, keys)) + +/** + * Given a schema `Schema` and a key `key: K`, this function extracts a specific field from the `A` type, + * producing a new schema that represents a transformation from the `{ readonly [key]: I[K] }` type to `A[K]`. + * + * @example + * ```ts + * import * as Schema from "effect/Schema" + * + * // --------------------------------------------- + * // use case: pull out a single field from a + * // struct through a transformation + * // --------------------------------------------- + * + * const mytable = Schema.Struct({ + * column1: Schema.NumberFromString, + * column2: Schema.Number + * }) + * + * // const pullOutColumn: S.Schema + * const pullOutColumn = mytable.pipe(Schema.pluck("column1")) + * + * console.log(Schema.decodeUnknownEither(Schema.Array(pullOutColumn))([{ column1: "1", column2: 100 }, { column1: "2", column2: 300 }])) + * // Output: { _id: 'Either', _tag: 'Right', right: [ 1, 2 ] } + * ``` + * + * @category struct transformations + * @since 3.10.0 + */ +export const pluck: { + ( + key: K + ): (schema: Schema) => SchemaClass>, R> + ( + schema: Schema, + key: K + ): SchemaClass>, R> +} = dual( + 2, + ( + schema: Schema, + key: K + ): Schema, R> => { + const ps = AST.getPropertyKeyIndexedAccess(AST.typeAST(schema.ast), key) + const value = make(ps.isOptional ? AST.orUndefined(ps.type) : ps.type) + const out = transform( + schema.pipe(pick(key)), + value, + { + strict: true, + decode: (i) => i[key], + encode: (a) => ps.isOptional && a === undefined ? {} : { [key]: a } as any + } + ) + return out + } +) + +/** + * @category branding + * @since 3.10.0 + */ +export interface BrandSchema, I = A, R = never> + extends AnnotableClass, A, I, R> +{ + make(a: Brand.Unbranded, options?: MakeOptions): A +} + +/** + * @category api interface + * @since 3.10.0 + */ +export interface brand + extends BrandSchema & Brand, Schema.Encoded, Schema.Context> +{ + readonly from: S + annotations(annotations: Annotations.Schema & Brand>): brand +} + +function makeBrandClass( + from: S, + ast: AST.AST +): brand { + return class BrandClass extends make & Brand, Schema.Encoded, Schema.Context>(ast) { + static override annotations(annotations: Annotations.Schema & Brand>): brand { + return makeBrandClass(this.from, mergeSchemaAnnotations(this.ast, annotations)) + } + + static make = (a: Brand.Unbranded & Brand>, options?: MakeOptions): Schema.Type & Brand => { + return getDisableValidationMakeOption(options) ? a : ParseResult.validateSync(this)(a) + } + + static from = from + } +} + +/** + * Returns a nominal branded schema by applying a brand to a given schema. + * + * ``` + * Schema + B -> Schema> + * ``` + * + * @example + * ```ts + * import * as Schema from "effect/Schema" + * + * const Int = Schema.Number.pipe(Schema.int(), Schema.brand("Int")) + * type Int = Schema.Schema.Type // number & Brand<"Int"> + * ``` + * + * @category branding + * @since 3.10.0 + */ +export const brand = ( + brand: B, + annotations?: Annotations.Schema & Brand> +) => +(self: S): brand => { + const annotation: AST.BrandAnnotation = option_.match(AST.getBrandAnnotation(self.ast), { + onNone: () => [brand], + onSome: (brands) => [...brands, brand] + }) + const ast = AST.annotations( + self.ast, + toASTAnnotations({ + [AST.BrandAnnotationId]: annotation, + ...annotations + }) + ) + return makeBrandClass(self, ast) +} + +/** + * @category combinators + * @since 3.10.0 + */ +export const partial = ( + self: Schema +): SchemaClass<{ [K in keyof A]?: A[K] | undefined }, { [K in keyof I]?: I[K] | undefined }, R> => + make(AST.partial(self.ast)) + +/** + * @category combinators + * @since 3.10.0 + */ +export const partialWith: { + (options: Options): ( + self: Schema + ) => SchemaClass<{ [K in keyof A]?: A[K] }, { [K in keyof I]?: I[K] }, R> + ( + self: Schema, + options: Options + ): SchemaClass<{ [K in keyof A]?: A[K] }, { [K in keyof I]?: I[K] }, R> +} = dual((args) => isSchema(args[0]), ( + self: Schema, + options: { readonly exact: true } +): SchemaClass, Partial, R> => make(AST.partial(self.ast, options))) + +/** + * @category combinators + * @since 3.10.0 + */ +export const required = ( + self: Schema +): SchemaClass<{ [K in keyof A]-?: A[K] }, { [K in keyof I]-?: I[K] }, R> => make(AST.required(self.ast)) + +/** + * @category api interface + * @since 3.10.0 + */ +export interface mutable extends + AnnotableClass< + mutable, + SimplifyMutable>, + SimplifyMutable>, + Schema.Context + > +{} + +/** + * Creates a new schema with shallow mutability applied to its properties. + * + * @category combinators + * @since 3.10.0 + */ +export const mutable = (schema: S): mutable => make(AST.mutable(schema.ast)) + +const intersectTypeLiterals = ( + x: AST.AST, + y: AST.AST, + path: ReadonlyArray +): AST.TypeLiteral => { + if (AST.isTypeLiteral(x) && AST.isTypeLiteral(y)) { + const propertySignatures = [...x.propertySignatures] + for (const ps of y.propertySignatures) { + const name = ps.name + const i = propertySignatures.findIndex((ps) => ps.name === name) + if (i === -1) { + propertySignatures.push(ps) + } else { + const { isOptional, type } = propertySignatures[i] + propertySignatures[i] = new AST.PropertySignature( + name, + extendAST(type, ps.type, path.concat(name)), + isOptional, + true + ) + } + } + return new AST.TypeLiteral( + propertySignatures, + x.indexSignatures.concat(y.indexSignatures) + ) + } + throw new Error(errors_.getSchemaExtendErrorMessage(x, y, path)) +} + +const preserveRefinementAnnotations = AST.omitAnnotations([AST.IdentifierAnnotationId]) + +const addRefinementToMembers = (refinement: AST.Refinement, asts: ReadonlyArray): Array => + asts.map((ast) => new AST.Refinement(ast, refinement.filter, preserveRefinementAnnotations(refinement))) + +const extendAST = (x: AST.AST, y: AST.AST, path: ReadonlyArray): AST.AST => + AST.Union.make(intersectUnionMembers([x], [y], path)) + +const getTypes = (ast: AST.AST): ReadonlyArray => AST.isUnion(ast) ? ast.types : [ast] + +const intersectUnionMembers = ( + xs: ReadonlyArray, + ys: ReadonlyArray, + path: ReadonlyArray +): Array => + array_.flatMap(xs, (x) => + array_.flatMap(ys, (y) => { + switch (y._tag) { + case "Literal": { + if ( + (Predicate.isString(y.literal) && AST.isStringKeyword(x) || + (Predicate.isNumber(y.literal) && AST.isNumberKeyword(x)) || + (Predicate.isBoolean(y.literal) && AST.isBooleanKeyword(x))) + ) { + return [y] + } + break + } + case "StringKeyword": { + if (y === AST.stringKeyword) { + if (AST.isStringKeyword(x) || (AST.isLiteral(x) && Predicate.isString(x.literal))) { + return [x] + } else if (AST.isRefinement(x)) { + return addRefinementToMembers(x, intersectUnionMembers(getTypes(x.from), [y], path)) + } + } else if (x === AST.stringKeyword) { + return [y] + } + break + } + case "NumberKeyword": { + if (y === AST.numberKeyword) { + if (AST.isNumberKeyword(x) || (AST.isLiteral(x) && Predicate.isNumber(x.literal))) { + return [x] + } else if (AST.isRefinement(x)) { + return addRefinementToMembers(x, intersectUnionMembers(getTypes(x.from), [y], path)) + } + } else if (x === AST.numberKeyword) { + return [y] + } + break + } + case "BooleanKeyword": { + if (y === AST.booleanKeyword) { + if (AST.isBooleanKeyword(x) || (AST.isLiteral(x) && Predicate.isBoolean(x.literal))) { + return [x] + } else if (AST.isRefinement(x)) { + return addRefinementToMembers(x, intersectUnionMembers(getTypes(x.from), [y], path)) + } + } else if (x === AST.booleanKeyword) { + return [y] + } + break + } + case "Union": + return intersectUnionMembers(getTypes(x), y.types, path) + case "Suspend": + return [new AST.Suspend(() => extendAST(x, y.f(), path))] + case "Refinement": + return addRefinementToMembers(y, intersectUnionMembers(getTypes(x), getTypes(y.from), path)) + case "TypeLiteral": { + switch (x._tag) { + case "Union": + return intersectUnionMembers(x.types, [y], path) + case "Suspend": + return [new AST.Suspend(() => extendAST(x.f(), y, path))] + case "Refinement": + return addRefinementToMembers(x, intersectUnionMembers(getTypes(x.from), [y], path)) + case "TypeLiteral": + return [intersectTypeLiterals(x, y, path)] + case "Transformation": { + const transformation = x.transformation + const from = intersectTypeLiterals(x.from, y, path) + const to = intersectTypeLiterals(x.to, AST.typeAST(y), path) + switch (transformation._tag) { + case "TypeLiteralTransformation": + return [ + new AST.Transformation( + from, + to, + new AST.TypeLiteralTransformation(transformation.propertySignatureTransformations) + ) + ] + case "ComposeTransformation": + return [new AST.Transformation(from, to, AST.composeTransformation)] + case "FinalTransformation": + return [ + new AST.Transformation( + from, + to, + new AST.FinalTransformation( + (fromA, options, ast, fromI) => + ParseResult.map( + transformation.decode(fromA, options, ast, fromI), + (partial) => ({ ...fromA, ...partial }) + ), + (toI, options, ast, toA) => + ParseResult.map( + transformation.encode(toI, options, ast, toA), + (partial) => ({ ...toI, ...partial }) + ) + ) + ) + ] + } + } + } + break + } + case "Transformation": { + if (AST.isTransformation(x)) { + if ( + AST.isTypeLiteralTransformation(y.transformation) && AST.isTypeLiteralTransformation(x.transformation) + ) { + return [ + new AST.Transformation( + intersectTypeLiterals(x.from, y.from, path), + intersectTypeLiterals(x.to, y.to, path), + new AST.TypeLiteralTransformation( + y.transformation.propertySignatureTransformations.concat( + x.transformation.propertySignatureTransformations + ) + ) + ) + ] + } + } else { + return intersectUnionMembers([y], [x], path) + } + break + } + } + throw new Error(errors_.getSchemaExtendErrorMessage(x, y, path)) + })) + +/** + * @category api interface + * @since 3.10.0 + */ +export interface extend extends + AnnotableClass< + extend, + Schema.Type & Schema.Type, + Schema.Encoded & Schema.Encoded, + Schema.Context | Schema.Context + > +{} + +/** + * Extends a schema with another schema. + * + * Not all extensions are supported, and their support depends on the nature of + * the involved schemas. + * + * Possible extensions include: + * - `Schema.String` with another `Schema.String` refinement or a string literal + * - `Schema.Number` with another `Schema.Number` refinement or a number literal + * - `Schema.Boolean` with another `Schema.Boolean` refinement or a boolean + * literal + * - A struct with another struct where overlapping fields support extension + * - A struct with in index signature + * - A struct with a union of supported schemas + * - A refinement of a struct with a supported schema + * - A suspend of a struct with a supported schema + * - A transformation between structs where the “from” and “to” sides have no + * overlapping fields with the target struct + * + * @example + * ```ts + * import * as Schema from "effect/Schema" + * + * const schema = Schema.Struct({ + * a: Schema.String, + * b: Schema.String + * }) + * + * // const extended: Schema< + * // { + * // readonly a: string + * // readonly b: string + * // } & { + * // readonly c: string + * // } & { + * // readonly [x: string]: string + * // } + * // > + * const extended = Schema.asSchema(schema.pipe( + * Schema.extend(Schema.Struct({ c: Schema.String })), // <= you can add more fields + * Schema.extend(Schema.Record({ key: Schema.String, value: Schema.String })) // <= you can add index signatures + * )) + * ``` + * + * @category combinators + * @since 3.10.0 + */ +export const extend: { + (that: That): (self: Self) => extend + (self: Self, that: That): extend +} = dual( + 2, + (self: Self, that: That) => make(extendAST(self.ast, that.ast, [])) +) + +/** + * @category combinators + * @since 3.10.0 + */ +export const compose: { + >( + to: To & Schema, C, Schema.Context> + ): (from: From) => transform + ( + to: To + ): >( + from: From & Schema, Schema.Context> + ) => transform + ( + to: To, + options?: { readonly strict: true } + ): ( + from: From & Schema, Schema.Encoded, Schema.Context> + ) => transform + ( + to: To, + options: { readonly strict: false } + ): (from: From) => transform + + >( + from: From, + to: To & Schema, C, Schema.Context> + ): transform + , To extends Schema.Any>( + from: From & Schema, Schema.Context>, + to: To + ): transform + ( + from: From & Schema, Schema.Encoded, Schema.Context>, + to: To, + options?: { readonly strict: true } + ): transform + ( + from: From, + to: To, + options: { readonly strict: false } + ): transform +} = dual( + (args) => isSchema(args[1]), + (from: Schema, to: Schema): SchemaClass => + makeTransformationClass(from, to, AST.compose(from.ast, to.ast)) +) + +/** + * @category api interface + * @since 3.10.0 + */ +export interface suspend extends AnnotableClass, A, I, R> {} + +/** + * @category constructors + * @since 3.10.0 + */ +export const suspend = (f: () => Schema): suspend => make(new AST.Suspend(() => f().ast)) + +/** + * @since 3.10.0 + * @category symbol + */ +export const RefineSchemaId: unique symbol = Symbol.for("effect/SchemaId/Refine") + +/** + * @since 3.10.0 + * @category symbol + */ +export type RefineSchemaId = typeof RefineSchemaId + +/** + * @category api interface + * @since 3.10.0 + */ +export interface refine + extends AnnotableClass, A, Schema.Encoded, Schema.Context> +{ + /** The following is required for {@link HasFields} to work */ + readonly [RefineSchemaId]: From + readonly from: From + readonly filter: ( + a: Schema.Type, + options: ParseOptions, + self: AST.Refinement + ) => option_.Option + make(a: Schema.Type, options?: MakeOptions): A +} + +function makeRefineClass( + from: From, + filter: (a: Schema.Type, options: ParseOptions, self: AST.Refinement) => option_.Option, + ast: AST.AST +): refine { + return class RefineClass extends make, Schema.Context>(ast) { + static override annotations(annotations: Annotations.Schema): refine { + return makeRefineClass(this.from, this.filter, mergeSchemaAnnotations(this.ast, annotations)) + } + + static [RefineSchemaId] = from + + static from = from + + static filter = filter + + static make = (a: Schema.Type, options?: MakeOptions): A => { + return getDisableValidationMakeOption(options) ? a : ParseResult.validateSync(this)(a) + } + } +} + +/** + * @category api interface + * @since 3.10.0 + */ +export interface filter extends refine, From> {} + +const fromFilterPredicateReturnTypeItem = ( + item: FilterOutput, + ast: AST.Refinement | AST.Transformation, + input: unknown +): option_.Option => { + if (Predicate.isBoolean(item)) { + return item + ? option_.none() + : option_.some(new ParseResult.Type(ast, input)) + } + if (Predicate.isString(item)) { + return option_.some(new ParseResult.Type(ast, input, item)) + } + if (item !== undefined) { + if ("_tag" in item) { + return option_.some(item) + } + const issue = new ParseResult.Type(ast, input, item.message) + return option_.some( + array_.isNonEmptyReadonlyArray(item.path) ? new ParseResult.Pointer(item.path, input, issue) : issue + ) + } + return option_.none() +} + +const toFilterParseIssue = ( + out: FilterReturnType, + ast: AST.Refinement | AST.Transformation, + input: unknown +): option_.Option => { + if (util_.isSingle(out)) { + return fromFilterPredicateReturnTypeItem(out, ast, input) + } + if (array_.isNonEmptyReadonlyArray(out)) { + const issues = array_.filterMap(out, (issue) => fromFilterPredicateReturnTypeItem(issue, ast, input)) + if (array_.isNonEmptyReadonlyArray(issues)) { + return option_.some(issues.length === 1 ? issues[0] : new ParseResult.Composite(ast, input, issues)) + } + } + return option_.none() +} + +/** + * @category filtering + * @since 3.10.0 + */ +export interface FilterIssue { + readonly path: ReadonlyArray + readonly message: string +} + +/** + * @category filtering + * @since 3.10.0 + */ +export type FilterOutput = undefined | boolean | string | ParseResult.ParseIssue | FilterIssue + +type FilterReturnType = FilterOutput | ReadonlyArray + +/** + * @category filtering + * @since 3.10.0 + */ +export function filter( + refinement: (a: A, options: ParseOptions, self: AST.Refinement) => a is B, + annotations?: Annotations.Filter +): (self: Schema) => refine> +export function filter( + refinement: (a: A, options: ParseOptions, self: AST.Refinement) => a is B, + annotations?: Annotations.Filter +): (self: Schema) => refine> +export function filter( + predicate: ( + a: Types.NoInfer>, + options: ParseOptions, + self: AST.Refinement + ) => FilterReturnType, + annotations?: Annotations.Filter>> +): (self: S) => filter +export function filter( + predicate: ( + a: A, + options: ParseOptions, + self: AST.Refinement + ) => FilterReturnType, + annotations?: Annotations.Filter +): (self: Schema) => refine> { + return (self: Schema) => { + function filter(input: A, options: AST.ParseOptions, ast: AST.Refinement) { + return toFilterParseIssue(predicate(input, options, ast), ast, input) + } + const ast = new AST.Refinement( + self.ast, + filter, + toASTAnnotations(annotations) + ) + return makeRefineClass(self, filter, ast) + } +} + +/** + * @category api interface + * @since 3.10.0 + */ +export interface filterEffect + extends transformOrFail>, FD> +{} + +/** + * @category transformations + * @since 3.10.0 + */ +export const filterEffect: { + ( + f: ( + a: Types.NoInfer>, + options: ParseOptions, + self: AST.Transformation + ) => Effect.Effect + ): (self: S) => filterEffect + ( + self: S, + f: ( + a: Types.NoInfer>, + options: ParseOptions, + self: AST.Transformation + ) => Effect.Effect + ): filterEffect +} = dual(2, ( + self: S, + f: ( + a: Types.NoInfer>, + options: ParseOptions, + self: AST.Transformation + ) => Effect.Effect +): filterEffect => + transformOrFail( + self, + typeSchema(self), + { + strict: true, + decode: (i, options, ast) => + ParseResult.flatMap( + f(i, options, ast), + (filterReturnType) => + option_.match(toFilterParseIssue(filterReturnType, ast, i), { + onNone: () => ParseResult.succeed(i), + onSome: ParseResult.fail + }) + ), + encode: (a) => ParseResult.succeed(a) + } + )) + +/** + * @category api interface + * @since 3.10.0 + */ +export interface transformOrFail extends + AnnotableClass< + transformOrFail, + Schema.Type, + Schema.Encoded, + Schema.Context | Schema.Context | R + > +{ + readonly from: From + readonly to: To +} + +function makeTransformationClass( + from: From, + to: To, + ast: AST.AST +): transformOrFail { + return class TransformationClass + extends make, Schema.Encoded, Schema.Context | Schema.Context | R>(ast) + { + static override annotations(annotations: Annotations.Schema>) { + return makeTransformationClass( + this.from, + this.to, + mergeSchemaAnnotations(this.ast, annotations) + ) + } + + static from = from + + static to = to + } +} + +/** + * Create a new `Schema` by transforming the input and output of an existing `Schema` + * using the provided decoding functions. + * + * @category transformations + * @since 3.10.0 + */ +export const transformOrFail: { + ( + to: To, + options: { + readonly decode: ( + fromA: Schema.Type, + options: ParseOptions, + ast: AST.Transformation, + fromI: Schema.Encoded + ) => Effect.Effect, ParseResult.ParseIssue, RD> + readonly encode: ( + toI: Schema.Encoded, + options: ParseOptions, + ast: AST.Transformation, + toA: Schema.Type + ) => Effect.Effect, ParseResult.ParseIssue, RE> + readonly strict?: true + } | { + readonly decode: ( + fromA: Schema.Type, + options: ParseOptions, + ast: AST.Transformation, + fromI: Schema.Encoded + ) => Effect.Effect + readonly encode: ( + toI: Schema.Encoded, + options: ParseOptions, + ast: AST.Transformation, + toA: Schema.Type + ) => Effect.Effect + readonly strict: false + } + ): (from: From) => transformOrFail + ( + from: From, + to: To, + options: { + readonly decode: ( + fromA: Schema.Type, + options: ParseOptions, + ast: AST.Transformation, + fromI: Schema.Encoded + ) => Effect.Effect, ParseResult.ParseIssue, RD> + readonly encode: ( + toI: Schema.Encoded, + options: ParseOptions, + ast: AST.Transformation, + toA: Schema.Type + ) => Effect.Effect, ParseResult.ParseIssue, RE> + readonly strict?: true + } | { + readonly decode: ( + fromA: Schema.Type, + options: ParseOptions, + ast: AST.Transformation, + fromI: Schema.Encoded + ) => Effect.Effect + readonly encode: ( + toI: Schema.Encoded, + options: ParseOptions, + ast: AST.Transformation, + toA: Schema.Type + ) => Effect.Effect + readonly strict: false + } + ): transformOrFail +} = dual((args) => isSchema(args[0]) && isSchema(args[1]), ( + from: Schema, + to: Schema, + options: { + readonly decode: ( + fromA: FromA, + options: ParseOptions, + ast: AST.Transformation, + fromI: FromI + ) => Effect.Effect + readonly encode: ( + toI: ToI, + options: ParseOptions, + ast: AST.Transformation, + toA: ToA + ) => Effect.Effect + } +): Schema => + makeTransformationClass( + from, + to, + new AST.Transformation( + from.ast, + to.ast, + new AST.FinalTransformation(options.decode, options.encode) + ) + )) + +/** + * @category api interface + * @since 3.10.0 + */ +export interface transform extends transformOrFail { + annotations(annotations: Annotations.Schema>): transform +} + +/** + * Create a new `Schema` by transforming the input and output of an existing `Schema` + * using the provided mapping functions. + * + * @category transformations + * @since 3.10.0 + */ +export const transform: { + ( + to: To, + options: { + readonly decode: (fromA: Schema.Type, fromI: Schema.Encoded) => Schema.Encoded + readonly encode: (toI: Schema.Encoded, toA: Schema.Type) => Schema.Type + readonly strict?: true + } | { + readonly decode: (fromA: Schema.Type, fromI: Schema.Encoded) => unknown + readonly encode: (toI: Schema.Encoded, toA: Schema.Type) => unknown + readonly strict: false + } + ): (from: From) => transform + ( + from: From, + to: To, + options: { + readonly decode: (fromA: Schema.Type, fromI: Schema.Encoded) => Schema.Encoded + readonly encode: (toI: Schema.Encoded, toA: Schema.Type) => Schema.Type + readonly strict?: true + } | { + readonly decode: (fromA: Schema.Type, fromI: Schema.Encoded) => unknown + readonly encode: (toI: Schema.Encoded, toA: Schema.Type) => unknown + readonly strict: false + } + ): transform +} = dual( + (args) => isSchema(args[0]) && isSchema(args[1]), + ( + from: Schema, + to: Schema, + options: { + readonly decode: (fromA: FromA, fromI: FromI) => ToI + readonly encode: (toI: ToI, toA: ToA) => FromA + } + ): Schema => + transformOrFail( + from, + to, + { + strict: true, + decode: (fromA, _options, _ast, toA) => ParseResult.succeed(options.decode(fromA, toA)), + encode: (toI, _options, _ast, toA) => ParseResult.succeed(options.encode(toI, toA)) + } + ) +) + +/** + * @category api interface + * @since 3.10.0 + */ +export interface transformLiteral + extends transform, Literal<[Type]>> +{ + annotations(annotations: Annotations.Schema): transformLiteral +} + +/** + * Creates a new `Schema` which transforms literal values. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import * as S from "effect/Schema" + * + * const schema = S.transformLiteral(0, "a") + * + * assert.deepStrictEqual(S.decodeSync(schema)(0), "a") + * ``` + * + * @category constructors + * @since 3.10.0 + */ +export function transformLiteral( + from: Encoded, + to: Type +): transformLiteral { + return transform(Literal(from), Literal(to), { + strict: true, + decode: () => to, + encode: () => from + }) +} + +/** + * Creates a new `Schema` which maps between corresponding literal values. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import * as S from "effect/Schema" + * + * const Animal = S.transformLiterals( + * [0, "cat"], + * [1, "dog"], + * [2, "cow"] + * ) + * + * assert.deepStrictEqual(S.decodeSync(Animal)(1), "dog") + * ``` + * + * @category constructors + * @since 3.10.0 + */ +export function transformLiterals>( + ...pairs: A +): Union<{ -readonly [I in keyof A]: transformLiteral }> +export function transformLiterals( + pairs: [Encoded, Type] +): transformLiteral +export function transformLiterals< + const A extends ReadonlyArray +>(...pairs: A): Schema +export function transformLiterals< + const A extends ReadonlyArray +>(...pairs: A): Schema { + return Union(...pairs.map(([from, to]) => transformLiteral(from, to))) +} + +/** + * Attaches a property signature with the specified key and value to the schema. + * This API is useful when you want to add a property to your schema which doesn't describe the shape of the input, + * but rather maps to another schema, for example when you want to add a discriminant to a simple union. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import * as S from "effect/Schema" + * import { pipe } from "effect/Function" + * + * const Circle = S.Struct({ radius: S.Number }) + * const Square = S.Struct({ sideLength: S.Number }) + * const Shape = S.Union( + * Circle.pipe(S.attachPropertySignature("kind", "circle")), + * Square.pipe(S.attachPropertySignature("kind", "square")) + * ) + * + * assert.deepStrictEqual(S.decodeSync(Shape)({ radius: 10 }), { + * kind: "circle", + * radius: 10 + * }) + * ``` + * + * @category combinators + * @since 3.10.0 + */ +export const attachPropertySignature: { + ( + key: K, + value: V, + annotations?: Annotations.Schema + ): ( + schema: Schema + ) => SchemaClass + ( + schema: Schema, + key: K, + value: V, + annotations?: Annotations.Schema + ): SchemaClass +} = dual( + (args) => isSchema(args[0]), + ( + schema: Schema, + key: K, + value: V, + annotations?: Annotations.Schema + ): SchemaClass => { + const ast = extend( + typeSchema(schema), + Struct({ [key]: Predicate.isSymbol(value) ? UniqueSymbolFromSelf(value) : Literal(value) }) + ).ast + return make( + new AST.Transformation( + schema.ast, + annotations ? mergeSchemaAnnotations(ast, annotations) : ast, + new AST.TypeLiteralTransformation( + [ + new AST.PropertySignatureTransformation( + key, + key, + () => option_.some(value), + () => option_.none() + ) + ] + ) + ) + ) + } +) + +/** + * @category annotations + * @since 3.10.0 + */ +export declare namespace Annotations { + /** + * @category annotations + * @since 3.10.0 + */ + export interface Doc extends AST.Annotations { + readonly title?: AST.TitleAnnotation + readonly description?: AST.DescriptionAnnotation + readonly documentation?: AST.DocumentationAnnotation + readonly examples?: AST.ExamplesAnnotation + readonly default?: AST.DefaultAnnotation + } + + /** + * @since 3.10.0 + */ + export interface Schema = readonly []> extends Doc { + readonly typeConstructor?: AST.TypeConstructorAnnotation + readonly identifier?: AST.IdentifierAnnotation + readonly message?: AST.MessageAnnotation + readonly schemaId?: AST.SchemaIdAnnotation + readonly jsonSchema?: AST.JSONSchemaAnnotation + readonly arbitrary?: ArbitraryAnnotation + readonly pretty?: pretty_.PrettyAnnotation + readonly equivalence?: AST.EquivalenceAnnotation + readonly concurrency?: AST.ConcurrencyAnnotation + readonly batching?: AST.BatchingAnnotation + readonly parseIssueTitle?: AST.ParseIssueTitleAnnotation + readonly parseOptions?: AST.ParseOptions + readonly decodingFallback?: AST.DecodingFallbackAnnotation + } + + /** + * @since 3.11.6 + */ + export interface GenericSchema extends Schema { + readonly arbitrary?: (..._: any) => LazyArbitrary + readonly pretty?: (..._: any) => pretty_.Pretty + readonly equivalence?: (..._: any) => Equivalence.Equivalence + } + + // TODO(4.0): replace `readonly [P]` with `readonly []` + /** + * @since 3.10.0 + */ + export interface Filter extends Schema {} +} + +/** + * Merges a set of new annotations with existing ones, potentially overwriting + * any duplicates. + * + * @category annotations + * @since 3.10.0 + */ +export const annotations: { + (annotations: Annotations.GenericSchema>): (self: S) => Annotable.Self + (self: S, annotations: Annotations.GenericSchema>): Annotable.Self +} = dual( + 2, + (self: Schema, annotations: Annotations.GenericSchema): Schema => + self.annotations(annotations) +) + +type Rename = { + [ + K in keyof A as K extends keyof M ? M[K] extends PropertyKey ? M[K] + : never + : K + ]: A[K] +} + +/** + * @category renaming + * @since 3.10.0 + */ +export const rename: { + < + A, + const M extends + & { readonly [K in keyof A]?: PropertyKey } + & { readonly [K in Exclude]: never } + >( + mapping: M + ): (self: Schema) => SchemaClass>, I, R> + < + A, + I, + R, + const M extends + & { readonly [K in keyof A]?: PropertyKey } + & { readonly [K in Exclude]: never } + >( + self: Schema, + mapping: M + ): SchemaClass>, I, R> +} = dual( + 2, + < + A, + I, + R, + const M extends + & { readonly [K in keyof A]?: PropertyKey } + & { readonly [K in Exclude]: never } + >( + self: Schema, + mapping: M + ): SchemaClass>, I, R> => make(AST.rename(self.ast, mapping)) +) + +/** + * @category schema id + * @since 3.10.0 + */ +export const TrimmedSchemaId: unique symbol = Symbol.for("effect/SchemaId/Trimmed") + +/** + * Verifies that a string contains no leading or trailing whitespaces. + * + * Note. This combinator does not make any transformations, it only validates. + * If what you were looking for was a combinator to trim strings, then check out the `trim` combinator. + * + * @category string filters + * @since 3.10.0 + */ +export const trimmed = ( + annotations?: Annotations.Filter> +) => +(self: S & Schema, Schema.Context>): filter => + self.pipe( + filter((a) => a === a.trim(), { + schemaId: TrimmedSchemaId, + title: "trimmed", + description: "a string with no leading or trailing whitespace", + jsonSchema: { pattern: "^\\S[\\s\\S]*\\S$|^\\S$|^$" }, + ...annotations + }) + ) + +/** + * @category schema id + * @since 3.10.0 + */ +export const MaxLengthSchemaId: unique symbol = schemaId_.MaxLengthSchemaId + +/** + * @category schema id + * @since 3.10.0 + */ +export type MaxLengthSchemaId = typeof MaxLengthSchemaId + +/** + * @category string filters + * @since 3.10.0 + */ +export const maxLength = + (maxLength: number, annotations?: Annotations.Filter>) => + (self: S & Schema, Schema.Context>): filter => + self.pipe( + filter( + (a) => a.length <= maxLength, + { + schemaId: MaxLengthSchemaId, + title: `maxLength(${maxLength})`, + description: `a string at most ${maxLength} character(s) long`, + jsonSchema: { maxLength }, + ...annotations + } + ) + ) + +/** + * @category schema id + * @since 3.10.0 + */ +export const MinLengthSchemaId: unique symbol = schemaId_.MinLengthSchemaId + +/** + * @category schema id + * @since 3.10.0 + */ +export type MinLengthSchemaId = typeof MinLengthSchemaId + +/** + * @category string filters + * @since 3.10.0 + */ +export const minLength = ( + minLength: number, + annotations?: Annotations.Filter> +) => +(self: S & Schema, Schema.Context>): filter => + self.pipe( + filter( + (a) => a.length >= minLength, + { + schemaId: MinLengthSchemaId, + title: `minLength(${minLength})`, + description: `a string at least ${minLength} character(s) long`, + jsonSchema: { minLength }, + ...annotations + } + ) + ) + +/** + * @category schema id + * @since 3.10.0 + */ +export const LengthSchemaId: unique symbol = schemaId_.LengthSchemaId + +/** + * @category schema id + * @since 3.10.0 + */ +export type LengthSchemaId = typeof LengthSchemaId + +/** + * @category string filters + * @since 3.10.0 + */ +export const length = ( + length: number | { readonly min: number; readonly max: number }, + annotations?: Annotations.Filter> +) => +(self: S & Schema, Schema.Context>): filter => { + const minLength = Predicate.isObject(length) ? Math.max(0, Math.floor(length.min)) : Math.max(0, Math.floor(length)) + const maxLength = Predicate.isObject(length) ? Math.max(minLength, Math.floor(length.max)) : minLength + if (minLength !== maxLength) { + return self.pipe( + filter((a) => a.length >= minLength && a.length <= maxLength, { + schemaId: LengthSchemaId, + title: `length({ min: ${minLength}, max: ${maxLength})`, + description: `a string at least ${minLength} character(s) and at most ${maxLength} character(s) long`, + jsonSchema: { minLength, maxLength }, + ...annotations + }) + ) + } + return self.pipe( + filter((a) => a.length === minLength, { + schemaId: LengthSchemaId, + title: `length(${minLength})`, + description: minLength === 1 ? `a single character` : `a string ${minLength} character(s) long`, + jsonSchema: { minLength, maxLength: minLength }, + ...annotations + }) + ) +} + +/** + * @category schema id + * @since 3.10.0 + */ +export const PatternSchemaId: unique symbol = Symbol.for("effect/SchemaId/Pattern") + +/** + * @category string filters + * @since 3.10.0 + */ +export const pattern = ( + regex: RegExp, + annotations?: Annotations.Filter> +) => +(self: S & Schema, Schema.Context>): filter => { + const source = regex.source + return self.pipe( + filter( + (a) => { + // The following line ensures that `lastIndex` is reset to `0` in case the user has specified the `g` flag + regex.lastIndex = 0 + return regex.test(a) + }, + { + schemaId: PatternSchemaId, + [PatternSchemaId]: { regex }, + // title: `pattern(/${source}/)`, // avoiding this because it can be very long + description: `a string matching the pattern ${source}`, + jsonSchema: { pattern: source }, + ...annotations + } + ) + ) +} + +/** + * @category schema id + * @since 3.10.0 + */ +export const StartsWithSchemaId: unique symbol = Symbol.for("effect/SchemaId/StartsWith") + +/** + * @category string filters + * @since 3.10.0 + */ +export const startsWith = ( + startsWith: string, + annotations?: Annotations.Filter> +) => +(self: S & Schema, Schema.Context>): filter => { + const formatted = JSON.stringify(startsWith) + return self.pipe( + filter( + (a) => a.startsWith(startsWith), + { + schemaId: StartsWithSchemaId, + [StartsWithSchemaId]: { startsWith }, + title: `startsWith(${formatted})`, + description: `a string starting with ${formatted}`, + jsonSchema: { pattern: `^${startsWith}` }, + ...annotations + } + ) + ) +} + +/** + * @category schema id + * @since 3.10.0 + */ +export const EndsWithSchemaId: unique symbol = Symbol.for("effect/SchemaId/EndsWith") + +/** + * @category string filters + * @since 3.10.0 + */ +export const endsWith = ( + endsWith: string, + annotations?: Annotations.Filter> +) => +(self: S & Schema, Schema.Context>): filter => { + const formatted = JSON.stringify(endsWith) + return self.pipe( + filter( + (a) => a.endsWith(endsWith), + { + schemaId: EndsWithSchemaId, + [EndsWithSchemaId]: { endsWith }, + title: `endsWith(${formatted})`, + description: `a string ending with ${formatted}`, + jsonSchema: { pattern: `^.*${endsWith}$` }, + ...annotations + } + ) + ) +} + +/** + * @category schema id + * @since 3.10.0 + */ +export const IncludesSchemaId: unique symbol = Symbol.for("effect/SchemaId/Includes") + +/** + * @category string filters + * @since 3.10.0 + */ +export const includes = ( + searchString: string, + annotations?: Annotations.Filter> +) => +(self: S & Schema, Schema.Context>): filter => { + const formatted = JSON.stringify(searchString) + return self.pipe( + filter( + (a) => a.includes(searchString), + { + schemaId: IncludesSchemaId, + [IncludesSchemaId]: { includes: searchString }, + title: `includes(${formatted})`, + description: `a string including ${formatted}`, + jsonSchema: { pattern: `.*${searchString}.*` }, + ...annotations + } + ) + ) +} + +/** + * @category schema id + * @since 3.10.0 + */ +export const LowercasedSchemaId: unique symbol = Symbol.for("effect/SchemaId/Lowercased") + +/** + * Verifies that a string is lowercased. + * + * @category string filters + * @since 3.10.0 + */ +export const lowercased = + (annotations?: Annotations.Filter>) => + (self: S & Schema, Schema.Context>): filter => + self.pipe( + filter((a) => a === a.toLowerCase(), { + schemaId: LowercasedSchemaId, + title: "lowercased", + description: "a lowercase string", + jsonSchema: { pattern: "^[^A-Z]*$" }, + ...annotations + }) + ) + +/** + * @category string constructors + * @since 3.10.0 + */ +export class Lowercased extends String$.pipe( + lowercased({ identifier: "Lowercased" }) +) {} + +/** + * @category schema id + * @since 3.10.0 + */ +export const UppercasedSchemaId: unique symbol = Symbol.for("effect/SchemaId/Uppercased") + +/** + * Verifies that a string is uppercased. + * + * @category string filters + * @since 3.10.0 + */ +export const uppercased = + (annotations?: Annotations.Filter>) => + (self: S & Schema, Schema.Context>): filter => + self.pipe( + filter((a) => a === a.toUpperCase(), { + schemaId: UppercasedSchemaId, + title: "uppercased", + description: "an uppercase string", + jsonSchema: { pattern: "^[^a-z]*$" }, + ...annotations + }) + ) + +/** + * @category string constructors + * @since 3.10.0 + */ +export class Uppercased extends String$.pipe( + uppercased({ identifier: "Uppercased" }) +) {} + +/** + * @category schema id + * @since 3.10.0 + */ +export const CapitalizedSchemaId: unique symbol = Symbol.for("effect/SchemaId/Capitalized") + +/** + * Verifies that a string is capitalized. + * + * @category string filters + * @since 3.10.0 + */ +export const capitalized = + (annotations?: Annotations.Filter>) => + (self: S & Schema, Schema.Context>): filter => + self.pipe( + filter((a) => a[0]?.toUpperCase() === a[0], { + schemaId: CapitalizedSchemaId, + title: "capitalized", + description: "a capitalized string", + jsonSchema: { pattern: "^[^a-z]?.*$" }, + ...annotations + }) + ) + +/** + * @category string constructors + * @since 3.10.0 + */ +export class Capitalized extends String$.pipe( + capitalized({ identifier: "Capitalized" }) +) {} + +/** + * @category schema id + * @since 3.10.0 + */ +export const UncapitalizedSchemaId: unique symbol = Symbol.for("effect/SchemaId/Uncapitalized") + +/** + * Verifies that a string is uncapitalized. + * + * @category string filters + * @since 3.10.0 + */ +export const uncapitalized = + (annotations?: Annotations.Filter>) => + (self: S & Schema, Schema.Context>): filter => + self.pipe( + filter((a) => a[0]?.toLowerCase() === a[0], { + schemaId: UncapitalizedSchemaId, + title: "uncapitalized", + description: "a uncapitalized string", + jsonSchema: { pattern: "^[^A-Z]?.*$" }, + ...annotations + }) + ) + +/** + * @category string constructors + * @since 3.10.0 + */ +export class Uncapitalized extends String$.pipe( + uncapitalized({ identifier: "Uncapitalized" }) +) {} + +/** + * A schema representing a single character. + * + * @category string constructors + * @since 3.10.0 + */ +export class Char extends String$.pipe(length(1, { identifier: "Char" })) {} + +/** + * @category string filters + * @since 3.10.0 + */ +export const nonEmptyString = ( + annotations?: Annotations.Filter> +): (self: S & Schema, Schema.Context>) => filter => + minLength(1, { + title: "nonEmptyString", + description: "a non empty string", + ...annotations + }) + +/** + * This schema converts a string to lowercase. + * + * @category string transformations + * @since 3.10.0 + */ +export class Lowercase extends transform( + String$.annotations({ description: "a string that will be converted to lowercase" }), + Lowercased, + { + strict: true, + decode: (i) => i.toLowerCase(), + encode: identity + } +).annotations({ identifier: "Lowercase" }) {} + +/** + * This schema converts a string to uppercase. + * + * @category string transformations + * @since 3.10.0 + */ +export class Uppercase extends transform( + String$.annotations({ description: "a string that will be converted to uppercase" }), + Uppercased, + { + strict: true, + decode: (i) => i.toUpperCase(), + encode: identity + } +).annotations({ identifier: "Uppercase" }) {} + +/** + * This schema converts a string to capitalized one. + * + * @category string transformations + * @since 3.10.0 + */ +export class Capitalize extends transform( + String$.annotations({ description: "a string that will be converted to a capitalized format" }), + Capitalized, + { + strict: true, + decode: (i) => string_.capitalize(i), + encode: identity + } +).annotations({ identifier: "Capitalize" }) {} + +/** + * This schema converts a string to uncapitalized one. + * + * @category string transformations + * @since 3.10.0 + */ +export class Uncapitalize extends transform( + String$.annotations({ description: "a string that will be converted to an uncapitalized format" }), + Uncapitalized, + { + strict: true, + decode: (i) => string_.uncapitalize(i), + encode: identity + } +).annotations({ identifier: "Uncapitalize" }) {} + +/** + * @category string constructors + * @since 3.10.0 + */ +export class Trimmed extends String$.pipe( + trimmed({ identifier: "Trimmed" }) +) {} + +/** + * Useful for validating strings that must contain meaningful characters without + * leading or trailing whitespace. + * + * @example + * ```ts + * import { Schema } from "effect" + * + * console.log(Schema.decodeOption(Schema.NonEmptyTrimmedString)("")) // Option.none() + * console.log(Schema.decodeOption(Schema.NonEmptyTrimmedString)(" a ")) // Option.none() + * console.log(Schema.decodeOption(Schema.NonEmptyTrimmedString)("a")) // Option.some("a") + * ``` + * + * @category string constructors + * @since 3.10.0 + */ +export class NonEmptyTrimmedString extends Trimmed.pipe( + nonEmptyString({ identifier: "NonEmptyTrimmedString" }) +) {} + +/** + * This schema allows removing whitespaces from the beginning and end of a string. + * + * @category string transformations + * @since 3.10.0 + */ +export class Trim extends transform( + String$.annotations({ description: "a string that will be trimmed" }), + Trimmed, + { + strict: true, + decode: (i) => i.trim(), + encode: identity + } +).annotations({ identifier: "Trim" }) {} + +/** + * Returns a schema that allows splitting a string into an array of strings. + * + * @category string transformations + * @since 3.10.0 + */ +export const split = (separator: string): transform, Array$> => + transform( + String$.annotations({ description: "a string that will be split" }), + Array$(String$), + { + strict: true, + decode: (i) => i.split(separator), + encode: (a) => a.join(separator) + } + ) + +/** + * @since 3.10.0 + */ +export type ParseJsonOptions = { + readonly reviver?: Parameters[1] + readonly replacer?: Parameters[1] + readonly space?: Parameters[2] +} + +const getErrorMessage = (e: unknown): string => e instanceof Error ? e.message : String(e) + +const getParseJsonTransformation = (options?: ParseJsonOptions): SchemaClass => + transformOrFail( + String$.annotations({ description: "a string to be decoded into JSON" }), + Unknown, + { + strict: true, + decode: (i, _, ast) => + ParseResult.try({ + try: () => JSON.parse(i, options?.reviver), + catch: (e) => new ParseResult.Type(ast, i, getErrorMessage(e)) + }), + encode: (a, _, ast) => + ParseResult.try({ + try: () => JSON.stringify(a, options?.replacer, options?.space), + catch: (e) => new ParseResult.Type(ast, a, getErrorMessage(e)) + }) + } + ).annotations({ + title: "parseJson", + schemaId: AST.ParseJsonSchemaId + }) + +/** + * The `ParseJson` combinator provides a method to convert JSON strings into the `unknown` type using the underlying + * functionality of `JSON.parse`. It also utilizes `JSON.stringify` for encoding. + * + * You can optionally provide a `ParseJsonOptions` to configure both `JSON.parse` and `JSON.stringify` executions. + * + * Optionally, you can pass a schema `Schema` to obtain an `A` type instead of `unknown`. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import * as Schema from "effect/Schema" + * + * assert.deepStrictEqual(Schema.decodeUnknownSync(Schema.parseJson())(`{"a":"1"}`), { a: "1" }) + * assert.deepStrictEqual(Schema.decodeUnknownSync(Schema.parseJson(Schema.Struct({ a: Schema.NumberFromString })))(`{"a":"1"}`), { a: 1 }) + * ``` + * + * @category string transformations + * @since 3.10.0 + */ +export const parseJson: { + (schema: S, options?: ParseJsonOptions): transform, S> + (options?: ParseJsonOptions): SchemaClass +} = (schemaOrOptions?: Schema | ParseJsonOptions, o?: ParseJsonOptions) => + isSchema(schemaOrOptions) + ? compose(parseJson(o), schemaOrOptions) as any + : getParseJsonTransformation(schemaOrOptions as ParseJsonOptions | undefined) + +/** + * @category string constructors + * @since 3.10.0 + */ +export class NonEmptyString extends String$.pipe( + nonEmptyString({ identifier: "NonEmptyString" }) +) {} + +/** + * @category schema id + * @since 3.10.0 + */ +export const UUIDSchemaId: unique symbol = Symbol.for("effect/SchemaId/UUID") + +const uuidRegexp = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/i + +/** + * Represents a Universally Unique Identifier (UUID). + * + * This schema ensures that the provided string adheres to the standard UUID format. + * + * @category string constructors + * @since 3.10.0 + */ +export class UUID extends String$.pipe( + pattern(uuidRegexp, { + schemaId: UUIDSchemaId, + identifier: "UUID", + jsonSchema: { + format: "uuid", + pattern: uuidRegexp.source + }, + description: "a Universally Unique Identifier", + arbitrary: (): LazyArbitrary => (fc) => fc.uuid() + }) +) {} + +/** + * @category schema id + * @since 3.10.0 + */ +export const ULIDSchemaId: unique symbol = Symbol.for("effect/SchemaId/ULID") + +const ulidRegexp = /^[0-7][0-9A-HJKMNP-TV-Z]{25}$/i + +/** + * Represents a Universally Unique Lexicographically Sortable Identifier (ULID). + * + * ULIDs are designed to be compact, URL-safe, and ordered, making them suitable for use as identifiers. + * This schema ensures that the provided string adheres to the standard ULID format. + * + * @category string constructors + * @since 3.10.0 + */ +export class ULID extends String$.pipe( + pattern(ulidRegexp, { + schemaId: ULIDSchemaId, + identifier: "ULID", + description: "a Universally Unique Lexicographically Sortable Identifier", + arbitrary: (): LazyArbitrary => (fc) => fc.ulid() + }) +) {} + +/** + * Defines a schema that represents a `URL` object. + * + * @category URL constructors + * @since 3.11.0 + */ +export class URLFromSelf extends instanceOf(URL, { + typeConstructor: { _tag: "URL" }, + identifier: "URLFromSelf", + arbitrary: (): LazyArbitrary => (fc) => fc.webUrl().map((s) => new URL(s)), + pretty: () => (url) => url.toString() +}) {} + +/** @ignore */ +class URL$ extends transformOrFail( + String$.annotations({ description: "a string to be decoded into a URL" }), + URLFromSelf, + { + strict: true, + decode: (i, _, ast) => + ParseResult.try({ + try: () => new URL(i), + catch: (e) => + new ParseResult.Type( + ast, + i, + `Unable to decode ${JSON.stringify(i)} into a URL. ${getErrorMessage(e)}` + ) + }), + encode: (a) => ParseResult.succeed(a.toString()) + } +).annotations({ + identifier: "URL", + pretty: () => (url) => url.toString() +}) {} + +export { + /** + * Defines a schema that attempts to convert a `string` to a `URL` object using + * the `new URL` constructor. + * + * @category URL transformations + * @since 3.11.0 + */ + URL$ as URL +} + +/** + * @category schema id + * @since 3.10.0 + */ +export const FiniteSchemaId: unique symbol = schemaId_.FiniteSchemaId + +/** + * @category schema id + * @since 3.10.0 + */ +export type FiniteSchemaId = typeof FiniteSchemaId + +/** + * Ensures that the provided value is a finite number (excluding NaN, +Infinity, and -Infinity). + * + * @category number filters + * @since 3.10.0 + */ +export const finite = + (annotations?: Annotations.Filter>) => + (self: S & Schema, Schema.Context>): filter => + self.pipe( + filter(Number.isFinite, { + schemaId: FiniteSchemaId, + title: "finite", + description: "a finite number", + jsonSchema: {}, + ...annotations + }) + ) + +/** + * @category schema id + * @since 3.10.0 + */ +export const GreaterThanSchemaId: unique symbol = schemaId_.GreaterThanSchemaId + +/** + * @category schema id + * @since 3.10.0 + */ +export type GreaterThanSchemaId = typeof GreaterThanSchemaId + +/** + * This filter checks whether the provided number is greater than the specified minimum. + * + * @category number filters + * @since 3.10.0 + */ +export const greaterThan = ( + exclusiveMinimum: number, + annotations?: Annotations.Filter> +) => +(self: S & Schema, Schema.Context>): filter => + self.pipe( + filter((a) => a > exclusiveMinimum, { + schemaId: GreaterThanSchemaId, + title: `greaterThan(${exclusiveMinimum})`, + description: exclusiveMinimum === 0 ? "a positive number" : `a number greater than ${exclusiveMinimum}`, + jsonSchema: { exclusiveMinimum }, + ...annotations + }) + ) + +/** + * @category schema id + * @since 3.10.0 + */ +export const GreaterThanOrEqualToSchemaId: unique symbol = schemaId_.GreaterThanOrEqualToSchemaId + +/** + * @category schema id + * @since 3.10.0 + */ +export type GreaterThanOrEqualToSchemaId = typeof GreaterThanOrEqualToSchemaId + +/** + * This filter checks whether the provided number is greater than or equal to the specified minimum. + * + * @category number filters + * @since 3.10.0 + */ +export const greaterThanOrEqualTo = ( + minimum: number, + annotations?: Annotations.Filter> +) => +(self: S & Schema, Schema.Context>): filter => + self.pipe( + filter((a) => a >= minimum, { + schemaId: GreaterThanOrEqualToSchemaId, + title: `greaterThanOrEqualTo(${minimum})`, + description: minimum === 0 ? "a non-negative number" : `a number greater than or equal to ${minimum}`, + jsonSchema: { minimum }, + ...annotations + }) + ) + +/** + * @category schema id + * @since 3.10.0 + */ +export const MultipleOfSchemaId: unique symbol = Symbol.for("effect/SchemaId/MultipleOf") + +/** + * @category number filters + * @since 3.10.0 + */ +export const multipleOf = ( + divisor: number, + annotations?: Annotations.Filter> +) => +(self: S & Schema, Schema.Context>): filter => { + const positiveDivisor = Math.abs(divisor) // spec requires positive divisor + return self.pipe( + filter((a) => number_.remainder(a, divisor) === 0, { + schemaId: MultipleOfSchemaId, + title: `multipleOf(${positiveDivisor})`, + description: `a number divisible by ${positiveDivisor}`, + jsonSchema: { multipleOf: positiveDivisor }, + ...annotations + }) + ) +} + +/** + * @category schema id + * @since 3.10.0 + */ +export const IntSchemaId: unique symbol = schemaId_.IntSchemaId + +/** + * @category schema id + * @since 3.10.0 + */ +export type IntSchemaId = typeof IntSchemaId + +/** + * Ensures that the provided value is an integer number (excluding NaN, +Infinity, and -Infinity). + * + * @category number filters + * @since 3.10.0 + */ +export const int = + (annotations?: Annotations.Filter>) => + (self: S & Schema, Schema.Context>): filter => + self.pipe( + filter((a) => Number.isSafeInteger(a), { + schemaId: IntSchemaId, + title: "int", + description: "an integer", + jsonSchema: { type: "integer" }, + ...annotations + }) + ) + +/** + * @category schema id + * @since 3.10.0 + */ +export const LessThanSchemaId: unique symbol = schemaId_.LessThanSchemaId + +/** + * @category schema id + * @since 3.10.0 + */ +export type LessThanSchemaId = typeof LessThanSchemaId + +/** + * This filter checks whether the provided number is less than the specified maximum. + * + * @category number filters + * @since 3.10.0 + */ +export const lessThan = + (exclusiveMaximum: number, annotations?: Annotations.Filter>) => + (self: S & Schema, Schema.Context>): filter => + self.pipe( + filter((a) => a < exclusiveMaximum, { + schemaId: LessThanSchemaId, + title: `lessThan(${exclusiveMaximum})`, + description: exclusiveMaximum === 0 ? "a negative number" : `a number less than ${exclusiveMaximum}`, + jsonSchema: { exclusiveMaximum }, + ...annotations + }) + ) + +/** + * @category schema id + * @since 3.10.0 + */ +export const LessThanOrEqualToSchemaId: unique symbol = schemaId_.LessThanOrEqualToSchemaId + +/** + * @category schema id + * @since 3.10.0 + */ +export type LessThanOrEqualToSchemaId = typeof LessThanOrEqualToSchemaId + +/** + * This schema checks whether the provided number is less than or equal to the specified maximum. + * + * @category number filters + * @since 3.10.0 + */ +export const lessThanOrEqualTo = ( + maximum: number, + annotations?: Annotations.Filter> +) => +(self: S & Schema, Schema.Context>): filter => + self.pipe( + filter((a) => a <= maximum, { + schemaId: LessThanOrEqualToSchemaId, + title: `lessThanOrEqualTo(${maximum})`, + description: maximum === 0 ? "a non-positive number" : `a number less than or equal to ${maximum}`, + jsonSchema: { maximum }, + ...annotations + }) + ) + +/** + * @category schema id + * @since 3.10.0 + */ +export const BetweenSchemaId: unique symbol = schemaId_.BetweenSchemaId + +/** + * @category schema id + * @since 3.10.0 + */ +export type BetweenSchemaId = typeof BetweenSchemaId + +/** + * This filter checks whether the provided number falls within the specified minimum and maximum values. + * + * @category number filters + * @since 3.10.0 + */ +export const between = ( + minimum: number, + maximum: number, + annotations?: Annotations.Filter> +) => +(self: S & Schema, Schema.Context>): filter => + self.pipe( + filter((a) => a >= minimum && a <= maximum, { + schemaId: BetweenSchemaId, + title: `between(${minimum}, ${maximum})`, + description: `a number between ${minimum} and ${maximum}`, + jsonSchema: { minimum, maximum }, + ...annotations + }) + ) + +/** + * @category schema id + * @since 3.10.0 + */ +export const NonNaNSchemaId: unique symbol = schemaId_.NonNaNSchemaId + +/** + * @category schema id + * @since 3.10.0 + */ +export type NonNaNSchemaId = typeof NonNaNSchemaId + +/** + * @category number filters + * @since 3.10.0 + */ +export const nonNaN = + (annotations?: Annotations.Filter>) => + (self: S & Schema, Schema.Context>): filter => + self.pipe( + filter((a) => !Number.isNaN(a), { + schemaId: NonNaNSchemaId, + title: "nonNaN", + description: "a number excluding NaN", + ...annotations + }) + ) + +/** + * @category number filters + * @since 3.10.0 + */ +export const positive = ( + annotations?: Annotations.Filter> +): (self: S & Schema, Schema.Context>) => filter => + greaterThan(0, { title: "positive", ...annotations }) + +/** + * @category number filters + * @since 3.10.0 + */ +export const negative = ( + annotations?: Annotations.Filter> +): (self: S & Schema, Schema.Context>) => filter => + lessThan(0, { title: "negative", ...annotations }) + +/** + * @category number filters + * @since 3.10.0 + */ +export const nonPositive = ( + annotations?: Annotations.Filter> +): (self: S & Schema, Schema.Context>) => filter => + lessThanOrEqualTo(0, { title: "nonPositive", ...annotations }) + +/** + * @category number filters + * @since 3.10.0 + */ +export const nonNegative = ( + annotations?: Annotations.Filter> +): (self: S & Schema, Schema.Context>) => filter => + greaterThanOrEqualTo(0, { title: "nonNegative", ...annotations }) + +/** + * Clamps a number between a minimum and a maximum value. + * + * @category number transformations + * @since 3.10.0 + */ +export const clamp = (minimum: number, maximum: number) => +( + self: S & Schema, Schema.Context> +): transform>> => { + return transform( + self, + typeSchema(self).pipe(between(minimum, maximum)), + { + strict: false, + decode: (i) => number_.clamp(i, { minimum, maximum }), + encode: identity + } + ) +} + +/** + * Transforms a `string` into a `number` by parsing the string using the `parse` + * function of the `effect/Number` module. + * + * It returns an error if the value can't be converted (for example when + * non-numeric characters are provided). + * + * The following special string values are supported: "NaN", "Infinity", + * "-Infinity". + * + * @category number transformations + * @since 3.10.0 + */ +export function parseNumber( + self: S & Schema, Schema.Context> +): transformOrFail { + return transformOrFail( + self, + Number$, + { + strict: false, + decode: (i, _, ast) => + ParseResult.fromOption( + number_.parse(i), + () => new ParseResult.Type(ast, i, `Unable to decode ${JSON.stringify(i)} into a number`) + ), + encode: (a) => ParseResult.succeed(String(a)) + } + ) +} + +/** + * This schema transforms a `string` into a `number` by parsing the string using the `parse` function of the `effect/Number` module. + * + * It returns an error if the value can't be converted (for example when non-numeric characters are provided). + * + * The following special string values are supported: "NaN", "Infinity", "-Infinity". + * + * @category number transformations + * @since 3.10.0 + */ +export class NumberFromString extends parseNumber(String$.annotations({ + description: "a string to be decoded into a number" +})).annotations({ identifier: "NumberFromString" }) {} + +/** + * @category number constructors + * @since 3.10.0 + */ +export class Finite extends Number$.pipe(finite({ identifier: "Finite" })) {} + +/** + * @category number constructors + * @since 3.10.0 + */ +export class Int extends Number$.pipe(int({ identifier: "Int" })) {} + +/** + * @category number constructors + * @since 3.10.0 + */ +export class NonNaN extends Number$.pipe(nonNaN({ identifier: "NonNaN" })) {} + +/** + * @category number constructors + * @since 3.10.0 + */ +export class Positive extends Number$.pipe( + positive({ identifier: "Positive" }) +) {} + +/** + * @category number constructors + * @since 3.10.0 + */ +export class Negative extends Number$.pipe( + negative({ identifier: "Negative" }) +) {} + +/** + * @category number constructors + * @since 3.10.0 + */ +export class NonPositive extends Number$.pipe( + nonPositive({ identifier: "NonPositive" }) +) {} + +/** + * @category number constructors + * @since 3.10.0 + */ +export class NonNegative extends Number$.pipe( + nonNegative({ identifier: "NonNegative" }) +) {} + +/** + * @category schema id + * @since 3.10.0 + */ +export const JsonNumberSchemaId: unique symbol = schemaId_.JsonNumberSchemaId + +/** + * @category schema id + * @since 3.10.0 + */ +export type JsonNumberSchemaId = typeof JsonNumberSchemaId + +/** + * The `JsonNumber` is a schema for representing JSON numbers. It ensures that the provided value is a valid + * number by filtering out `NaN` and `(+/-) Infinity`. This is useful when you want to validate and represent numbers in JSON + * format. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import * as Schema from "effect/Schema" + * + * const is = Schema.is(Schema.JsonNumber) + * + * assert.deepStrictEqual(is(42), true) + * assert.deepStrictEqual(is(Number.NaN), false) + * assert.deepStrictEqual(is(Number.POSITIVE_INFINITY), false) + * assert.deepStrictEqual(is(Number.NEGATIVE_INFINITY), false) + * ``` + * + * @category number constructors + * @since 3.10.0 + */ +export class JsonNumber extends Number$.pipe( + finite({ + schemaId: JsonNumberSchemaId, + identifier: "JsonNumber" + }) +) {} + +/** + * @category boolean transformations + * @since 3.10.0 + */ +export class Not extends transform(Boolean$.annotations({ description: "a boolean that will be negated" }), Boolean$, { + strict: true, + decode: (i) => boolean_.not(i), + encode: (a) => boolean_.not(a) +}) {} + +const encodeSymbol = (sym: symbol, ast: AST.AST) => { + const key = Symbol.keyFor(sym) + return key === undefined + ? ParseResult.fail( + new ParseResult.Type(ast, sym, `Unable to encode a unique symbol ${String(sym)} into a string`) + ) + : ParseResult.succeed(key) +} + +const decodeSymbol = (s: string) => ParseResult.succeed(Symbol.for(s)) + +/** @ignore */ +class Symbol$ extends transformOrFail( + String$.annotations({ description: "a string to be decoded into a globally shared symbol" }), + SymbolFromSelf, + { + strict: false, + decode: (i) => decodeSymbol(i), + encode: (a, _, ast) => encodeSymbol(a, ast) + } +).annotations({ identifier: "Symbol" }) {} + +export { + /** + * Converts a string key into a globally shared symbol. + * + * @category symbol transformations + * @since 3.10.0 + */ + Symbol$ as Symbol +} + +/** + * @category schema id + * @since 3.10.0 + */ +export const GreaterThanBigIntSchemaId: unique symbol = schemaId_.GreaterThanBigintSchemaId + +/** + * @category schema id + * @since 3.10.0 + */ +export type GreaterThanBigIntSchemaId = typeof GreaterThanBigIntSchemaId + +/** + * @category bigint filters + * @since 3.10.0 + */ +export const greaterThanBigInt = ( + min: bigint, + annotations?: Annotations.Filter> +) => +(self: S & Schema, Schema.Context>): filter => + self.pipe( + filter((a) => a > min, { + schemaId: GreaterThanBigIntSchemaId, + [GreaterThanBigIntSchemaId]: { min }, + title: `greaterThanBigInt(${min})`, + description: min === 0n ? "a positive bigint" : `a bigint greater than ${min}n`, + ...annotations + }) + ) + +/** + * @category schema id + * @since 3.10.0 + */ +export const GreaterThanOrEqualToBigIntSchemaId: unique symbol = schemaId_.GreaterThanOrEqualToBigIntSchemaId + +/** + * @category schema id + * @since 3.10.0 + */ +export type GreaterThanOrEqualToBigIntSchemaId = typeof GreaterThanOrEqualToBigIntSchemaId + +/** + * @category bigint filters + * @since 3.10.0 + */ +export const greaterThanOrEqualToBigInt = ( + min: bigint, + annotations?: Annotations.Filter> +) => +(self: S & Schema, Schema.Context>): filter => + self.pipe( + filter((a) => a >= min, { + schemaId: GreaterThanOrEqualToBigIntSchemaId, + [GreaterThanOrEqualToBigIntSchemaId]: { min }, + title: `greaterThanOrEqualToBigInt(${min})`, + description: min === 0n + ? "a non-negative bigint" + : `a bigint greater than or equal to ${min}n`, + ...annotations + }) + ) + +/** + * @category schema id + * @since 3.10.0 + */ +export const LessThanBigIntSchemaId: unique symbol = schemaId_.LessThanBigIntSchemaId + +/** + * @category schema id + * @since 3.10.0 + */ +export type LessThanBigIntSchemaId = typeof LessThanBigIntSchemaId + +/** + * @category bigint filters + * @since 3.10.0 + */ +export const lessThanBigInt = ( + max: bigint, + annotations?: Annotations.Filter> +) => +(self: S & Schema, Schema.Context>): filter => + self.pipe( + filter((a) => a < max, { + schemaId: LessThanBigIntSchemaId, + [LessThanBigIntSchemaId]: { max }, + title: `lessThanBigInt(${max})`, + description: max === 0n ? "a negative bigint" : `a bigint less than ${max}n`, + ...annotations + }) + ) + +/** + * @category schema id + * @since 3.10.0 + */ +export const LessThanOrEqualToBigIntSchemaId: unique symbol = schemaId_.LessThanOrEqualToBigIntSchemaId + +/** + * @category schema id + * @since 3.10.0 + */ +export type LessThanOrEqualToBigIntSchemaId = typeof LessThanOrEqualToBigIntSchemaId + +/** + * @category bigint filters + * @since 3.10.0 + */ +export const lessThanOrEqualToBigInt = ( + max: bigint, + annotations?: Annotations.Filter> +) => +(self: S & Schema, Schema.Context>): filter => + self.pipe( + filter((a) => a <= max, { + schemaId: LessThanOrEqualToBigIntSchemaId, + [LessThanOrEqualToBigIntSchemaId]: { max }, + title: `lessThanOrEqualToBigInt(${max})`, + description: max === 0n ? "a non-positive bigint" : `a bigint less than or equal to ${max}n`, + ...annotations + }) + ) + +/** + * @category schema id + * @since 3.10.0 + */ +export const BetweenBigIntSchemaId: unique symbol = schemaId_.BetweenBigintSchemaId + +/** + * @category schema id + * @since 3.10.0 + */ +export type BetweenBigIntSchemaId = typeof BetweenBigIntSchemaId + +/** + * @category bigint filters + * @since 3.10.0 + */ +export const betweenBigInt = ( + min: bigint, + max: bigint, + annotations?: Annotations.Filter> +) => +(self: S & Schema, Schema.Context>): filter => + self.pipe( + filter((a) => a >= min && a <= max, { + schemaId: BetweenBigIntSchemaId, + [BetweenBigIntSchemaId]: { min, max }, + title: `betweenBigInt(${min}, ${max})`, + description: `a bigint between ${min}n and ${max}n`, + ...annotations + }) + ) + +/** + * @category bigint filters + * @since 3.10.0 + */ +export const positiveBigInt = ( + annotations?: Annotations.Filter> +): (self: S & Schema, Schema.Context>) => filter => + greaterThanBigInt(0n, { title: "positiveBigInt", ...annotations }) + +/** + * @category bigint filters + * @since 3.10.0 + */ +export const negativeBigInt = ( + annotations?: Annotations.Filter> +): (self: S & Schema, Schema.Context>) => filter => + lessThanBigInt(0n, { title: "negativeBigInt", ...annotations }) + +/** + * @category bigint filters + * @since 3.10.0 + */ +export const nonNegativeBigInt = ( + annotations?: Annotations.Filter> +): (self: S & Schema, Schema.Context>) => filter => + greaterThanOrEqualToBigInt(0n, { title: "nonNegativeBigInt", ...annotations }) + +/** + * @category bigint filters + * @since 3.10.0 + */ +export const nonPositiveBigInt = ( + annotations?: Annotations.Filter> +): (self: S & Schema, Schema.Context>) => filter => + lessThanOrEqualToBigInt(0n, { title: "nonPositiveBigInt", ...annotations }) + +/** + * Clamps a bigint between a minimum and a maximum value. + * + * @category bigint transformations + * @since 3.10.0 + */ +export const clampBigInt = (minimum: bigint, maximum: bigint) => +( + self: S & Schema, Schema.Context> +): transform>> => + transform( + self, + self.pipe(typeSchema, betweenBigInt(minimum, maximum)), + { + strict: false, + decode: (i) => bigInt_.clamp(i, { minimum, maximum }), + encode: identity + } + ) + +/** @ignore */ +class BigInt$ extends transformOrFail( + String$.annotations({ description: "a string to be decoded into a bigint" }), + BigIntFromSelf, + { + strict: true, + decode: (i, _, ast) => + ParseResult.fromOption( + bigInt_.fromString(i), + () => new ParseResult.Type(ast, i, `Unable to decode ${JSON.stringify(i)} into a bigint`) + ), + encode: (a) => ParseResult.succeed(String(a)) + } +).annotations({ identifier: "BigInt" }) {} + +export { + /** + * This schema transforms a `string` into a `bigint` by parsing the string using the `BigInt` function. + * + * It returns an error if the value can't be converted (for example when non-numeric characters are provided). + * + * @category bigint transformations + * @since 3.10.0 + */ + BigInt$ as BigInt +} + +/** + * @category bigint constructors + * @since 3.10.0 + */ +export const PositiveBigIntFromSelf: filter> = BigIntFromSelf.pipe( + positiveBigInt({ identifier: "PositiveBigintFromSelf" }) +) + +/** + * @category bigint constructors + * @since 3.10.0 + */ +export const PositiveBigInt: filter> = BigInt$.pipe( + positiveBigInt({ identifier: "PositiveBigint" }) +) + +/** + * @category bigint constructors + * @since 3.10.0 + */ +export const NegativeBigIntFromSelf: filter> = BigIntFromSelf.pipe( + negativeBigInt({ identifier: "NegativeBigintFromSelf" }) +) + +/** + * @category bigint constructors + * @since 3.10.0 + */ +export const NegativeBigInt: filter> = BigInt$.pipe( + negativeBigInt({ identifier: "NegativeBigint" }) +) + +/** + * @category bigint constructors + * @since 3.10.0 + */ +export const NonPositiveBigIntFromSelf: filter> = BigIntFromSelf.pipe( + nonPositiveBigInt({ identifier: "NonPositiveBigintFromSelf" }) +) + +/** + * @category bigint constructors + * @since 3.10.0 + */ +export const NonPositiveBigInt: filter> = BigInt$.pipe( + nonPositiveBigInt({ identifier: "NonPositiveBigint" }) +) + +/** + * @category bigint constructors + * @since 3.10.0 + */ +export const NonNegativeBigIntFromSelf: filter> = BigIntFromSelf.pipe( + nonNegativeBigInt({ identifier: "NonNegativeBigintFromSelf" }) +) + +/** + * @category bigint constructors + * @since 3.10.0 + */ +export const NonNegativeBigInt: filter> = BigInt$.pipe( + nonNegativeBigInt({ identifier: "NonNegativeBigint" }) +) + +/** + * This schema transforms a `number` into a `bigint` by parsing the number using the `BigInt` function. + * + * It returns an error if the value can't be safely encoded as a `number` due to being out of range. + * + * @category bigint transformations + * @since 3.10.0 + */ +export class BigIntFromNumber extends transformOrFail( + Number$.annotations({ description: "a number to be decoded into a bigint" }), + BigIntFromSelf.pipe(betweenBigInt(BigInt(Number.MIN_SAFE_INTEGER), BigInt(Number.MAX_SAFE_INTEGER))), + { + strict: true, + decode: (i, _, ast) => + ParseResult.fromOption( + bigInt_.fromNumber(i), + () => new ParseResult.Type(ast, i, `Unable to decode ${i} into a bigint`) + ), + encode: (a, _, ast) => + ParseResult.fromOption( + bigInt_.toNumber(a), + () => new ParseResult.Type(ast, a, `Unable to encode ${a}n into a number`) + ) + } +).annotations({ identifier: "BigIntFromNumber" }) {} + +const redactedArbitrary = (value: LazyArbitrary): LazyArbitrary> => (fc) => + value(fc).map(redacted_.make) + +const toComposite = ( + eff: Effect.Effect, + onSuccess: (a: A) => B, + ast: AST.AST, + actual: unknown +): Effect.Effect => + ParseResult.mapBoth(eff, { + onFailure: (e) => new ParseResult.Composite(ast, actual, e), + onSuccess + }) + +const redactedParse = ( + decodeUnknown: ParseResult.DecodeUnknown +): ParseResult.DeclarationDecodeUnknown, R> => +(u, options, ast) => + redacted_.isRedacted(u) ? + toComposite(decodeUnknown(redacted_.value(u), options), redacted_.make, ast, u) : + ParseResult.fail(new ParseResult.Type(ast, u)) + +/** + * @category api interface + * @since 3.10.0 + */ +export interface RedactedFromSelf extends + AnnotableDeclare< + RedactedFromSelf, + redacted_.Redacted>, + redacted_.Redacted>, + [Value] + > +{} + +/** + * @category Redacted constructors + * @since 3.10.0 + */ +export const RedactedFromSelf = (value: Value): RedactedFromSelf => + declare( + [value], + { + decode: (value) => redactedParse(ParseResult.decodeUnknown(value)), + encode: (value) => redactedParse(ParseResult.encodeUnknown(value)) + }, + { + typeConstructor: { _tag: "effect/Redacted" }, + description: "Redacted()", + pretty: () => () => "Redacted()", + arbitrary: redactedArbitrary, + equivalence: redacted_.getEquivalence + } + ) + +/** + * @category api interface + * @since 3.10.0 + */ +export interface Redacted + extends transform>>> +{} + +/** + * A transformation that transform a `Schema` into a + * `RedactedFromSelf`. + * + * @category Redacted transformations + * @since 3.10.0 + */ +export function Redacted(value: Value): Redacted { + return transform( + value, + RedactedFromSelf(typeSchema(asSchema(value))), + { + strict: true, + decode: (i) => redacted_.make(i), + encode: (a) => redacted_.value(a) + } + ) +} + +/** + * @category Duration constructors + * @since 3.10.0 + */ +export class DurationFromSelf extends declare( + duration_.isDuration, + { + typeConstructor: { _tag: "effect/Duration" }, + identifier: "DurationFromSelf", + pretty: (): pretty_.Pretty => String, + arbitrary: (): LazyArbitrary => (fc) => + fc.oneof( + fc.constant(duration_.infinity), + fc.bigInt({ min: 0n }).map((_) => duration_.nanos(_)), + fc.maxSafeNat().map((_) => duration_.millis(_)) + ), + equivalence: (): Equivalence.Equivalence => duration_.Equivalence + } +) {} + +/** + * A schema that transforms a non negative `bigint` into a `Duration`. Treats + * the value as the number of nanoseconds. + * + * @category Duration transformations + * @since 3.10.0 + */ +export class DurationFromNanos extends transformOrFail( + NonNegativeBigIntFromSelf.annotations({ description: "a bigint to be decoded into a Duration" }), + DurationFromSelf.pipe(filter((duration) => duration_.isFinite(duration), { description: "a finite duration" })), + { + strict: true, + decode: (i) => ParseResult.succeed(duration_.nanos(i)), + encode: (a, _, ast) => + option_.match(duration_.toNanos(a), { + onNone: () => ParseResult.fail(new ParseResult.Type(ast, a, `Unable to encode ${a} into a bigint`)), + onSome: (nanos) => ParseResult.succeed(nanos) + }) + } +).annotations({ identifier: "DurationFromNanos" }) {} + +/** + * A non-negative integer. +Infinity is excluded. + * + * @category number constructors + * @since 3.11.10 + */ +export const NonNegativeInt = NonNegative.pipe(int()).annotations({ identifier: "NonNegativeInt" }) + +/** + * A schema that transforms a (possibly Infinite) non negative number into a + * `Duration`. Treats the value as the number of milliseconds. + * + * @category Duration transformations + * @since 3.10.0 + */ +export class DurationFromMillis extends transform( + NonNegative.annotations({ + description: "a non-negative number to be decoded into a Duration" + }), + DurationFromSelf, + { + strict: true, + decode: (i) => duration_.millis(i), + encode: (a) => duration_.toMillis(a) + } +).annotations({ identifier: "DurationFromMillis" }) {} + +const DurationValueMillis = TaggedStruct("Millis", { millis: NonNegativeInt }) +const DurationValueNanos = TaggedStruct("Nanos", { nanos: BigInt$ }) +const DurationValueInfinity = TaggedStruct("Infinity", {}) +const durationValueInfinity = DurationValueInfinity.make({}) + +/** + * @category Duration utils + * @since 3.12.8 + */ +export type DurationEncoded = + | { + readonly _tag: "Millis" + readonly millis: number + } + | { + readonly _tag: "Nanos" + readonly nanos: string + } + | { + readonly _tag: "Infinity" + } + +const DurationValue: Schema = Union( + DurationValueMillis, + DurationValueNanos, + DurationValueInfinity +).annotations({ + identifier: "DurationValue", + description: "an JSON-compatible tagged union to be decoded into a Duration" +}) + +const FiniteHRTime = Tuple( + element(NonNegativeInt).annotations({ title: "seconds" }), + element(NonNegativeInt).annotations({ title: "nanos" }) +).annotations({ identifier: "FiniteHRTime" }) + +const InfiniteHRTime = Tuple(Literal(-1), Literal(0)).annotations({ identifier: "InfiniteHRTime" }) + +const HRTime: Schema = Union(FiniteHRTime, InfiniteHRTime).annotations({ + identifier: "HRTime", + description: "a tuple of seconds and nanos to be decoded into a Duration" +}) + +const isDurationValue = (u: duration_.DurationValue | typeof HRTime.Type): u is duration_.DurationValue => + typeof u === "object" + +// TODO(4.0): remove HRTime union member +/** + * A schema that converts a JSON-compatible tagged union into a `Duration`. + * + * @category Duration transformations + * @since 3.10.0 + */ +export class Duration extends transform( + Union(DurationValue, HRTime), + DurationFromSelf, + { + strict: true, + decode: (i) => { + if (isDurationValue(i)) { + switch (i._tag) { + case "Millis": + return duration_.millis(i.millis) + case "Nanos": + return duration_.nanos(i.nanos) + case "Infinity": + return duration_.infinity + } + } + const [seconds, nanos] = i + return seconds === -1 ? duration_.infinity : duration_.nanos(BigInt(seconds) * BigInt(1e9) + BigInt(nanos)) + }, + encode: (a) => { + switch (a.value._tag) { + case "Millis": + return DurationValueMillis.make({ millis: a.value.millis }) + case "Nanos": + return DurationValueNanos.make({ nanos: a.value.nanos }) + case "Infinity": + return durationValueInfinity + } + } + } +).annotations({ identifier: "Duration" }) {} + +/** + * Clamps a `Duration` between a minimum and a maximum value. + * + * @category Duration transformations + * @since 3.10.0 + */ +export const clampDuration = + (minimum: duration_.DurationInput, maximum: duration_.DurationInput) => + ( + self: S & Schema, Schema.Context> + ): transform>> => + transform( + self, + self.pipe(typeSchema, betweenDuration(minimum, maximum)), + { + strict: false, + decode: (i) => duration_.clamp(i, { minimum, maximum }), + encode: identity + } + ) + +/** + * @category schema id + * @since 3.10.0 + */ +export const LessThanDurationSchemaId: unique symbol = Symbol.for("effect/SchemaId/LessThanDuration") + +/** + * @category Duration filters + * @since 3.10.0 + */ +export const lessThanDuration = ( + max: duration_.DurationInput, + annotations?: Annotations.Filter> +) => +(self: S & Schema, Schema.Context>): filter => + self.pipe( + filter((a) => duration_.lessThan(a, max), { + schemaId: LessThanDurationSchemaId, + [LessThanDurationSchemaId]: { max }, + title: `lessThanDuration(${max})`, + description: `a Duration less than ${duration_.decode(max)}`, + ...annotations + }) + ) + +/** + * @category schema id + * @since 3.10.0 + */ +export const LessThanOrEqualToDurationSchemaId: unique symbol = Symbol.for( + "effect/schema/LessThanOrEqualToDuration" +) + +/** + * @category Duration filters + * @since 3.10.0 + */ +export const lessThanOrEqualToDuration = ( + max: duration_.DurationInput, + annotations?: Annotations.Filter> +) => +(self: S & Schema, Schema.Context>): filter => + self.pipe( + filter((a) => duration_.lessThanOrEqualTo(a, max), { + schemaId: LessThanDurationSchemaId, + [LessThanDurationSchemaId]: { max }, + title: `lessThanOrEqualToDuration(${max})`, + description: `a Duration less than or equal to ${duration_.decode(max)}`, + ...annotations + }) + ) + +/** + * @category schema id + * @since 3.10.0 + */ +export const GreaterThanDurationSchemaId: unique symbol = Symbol.for("effect/SchemaId/GreaterThanDuration") + +/** + * @category Duration filters + * @since 3.10.0 + */ +export const greaterThanDuration = ( + min: duration_.DurationInput, + annotations?: Annotations.Filter> +) => +(self: S & Schema, Schema.Context>): filter => + self.pipe( + filter((a) => duration_.greaterThan(a, min), { + schemaId: GreaterThanDurationSchemaId, + [GreaterThanDurationSchemaId]: { min }, + title: `greaterThanDuration(${min})`, + description: `a Duration greater than ${duration_.decode(min)}`, + ...annotations + }) + ) + +/** + * @category schema id + * @since 3.10.0 + */ +export const GreaterThanOrEqualToDurationSchemaId: unique symbol = Symbol.for( + "effect/schema/GreaterThanOrEqualToDuration" +) + +/** + * @category Duration filters + * @since 3.10.0 + */ +export const greaterThanOrEqualToDuration = ( + min: duration_.DurationInput, + annotations?: Annotations.Filter> +) => +(self: S & Schema, Schema.Context>): filter => + self.pipe( + filter((a) => duration_.greaterThanOrEqualTo(a, min), { + schemaId: GreaterThanOrEqualToDurationSchemaId, + [GreaterThanOrEqualToDurationSchemaId]: { min }, + title: `greaterThanOrEqualToDuration(${min})`, + description: `a Duration greater than or equal to ${duration_.decode(min)}`, + ...annotations + }) + ) + +/** + * @category schema id + * @since 3.10.0 + */ +export const BetweenDurationSchemaId: unique symbol = Symbol.for("effect/SchemaId/BetweenDuration") + +/** + * @category Duration filters + * @since 3.10.0 + */ +export const betweenDuration = ( + minimum: duration_.DurationInput, + maximum: duration_.DurationInput, + annotations?: Annotations.Filter> +) => +(self: S & Schema, Schema.Context>): filter => + self.pipe( + filter((a) => duration_.between(a, { minimum, maximum }), { + schemaId: BetweenDurationSchemaId, + [BetweenDurationSchemaId]: { maximum, minimum }, + title: `betweenDuration(${minimum}, ${maximum})`, + description: `a Duration between ${duration_.decode(minimum)} and ${duration_.decode(maximum)}`, + ...annotations + }) + ) + +/** + * @category Uint8Array constructors + * @since 3.10.0 + */ +export class Uint8ArrayFromSelf extends declare( + Predicate.isUint8Array, + { + typeConstructor: { _tag: "Uint8Array" }, + identifier: "Uint8ArrayFromSelf", + pretty: (): pretty_.Pretty => (u8arr) => `new Uint8Array(${JSON.stringify(Array.from(u8arr))})`, + arbitrary: (): LazyArbitrary => (fc) => fc.uint8Array(), + equivalence: (): Equivalence.Equivalence => array_.getEquivalence(Equal.equals) as any + } +) {} + +/** + * @category number constructors + * @since 3.11.10 + */ +export class Uint8 extends Number$.pipe( + between(0, 255, { + identifier: "Uint8", + description: "a 8-bit unsigned integer" + }) +) {} + +/** @ignore */ +class Uint8Array$ extends transform( + Array$(Uint8).annotations({ + description: "an array of 8-bit unsigned integers to be decoded into a Uint8Array" + }), + Uint8ArrayFromSelf, + { + strict: true, + decode: (i) => Uint8Array.from(i), + encode: (a) => Array.from(a) + } +).annotations({ identifier: "Uint8Array" }) {} + +export { + /** + * A schema that transforms an array of numbers into a `Uint8Array`. + * + * @category Uint8Array transformations + * @since 3.10.0 + */ + Uint8Array$ as Uint8Array +} + +const makeUint8ArrayTransformation = ( + id: string, + decode: (s: string) => either_.Either, + encode: (u: Uint8Array) => string +) => + transformOrFail( + String$.annotations({ description: "a string to be decoded into a Uint8Array" }), + Uint8ArrayFromSelf, + { + strict: true, + decode: (i, _, ast) => + either_.mapLeft( + decode(i), + (decodeException) => new ParseResult.Type(ast, i, decodeException.message) + ), + encode: (a) => ParseResult.succeed(encode(a)) + } + ).annotations({ identifier: id }) + +/** + * Decodes a base64 (RFC4648) encoded string into a `Uint8Array`. + * + * @category Uint8Array transformations + * @since 3.10.0 + */ +export const Uint8ArrayFromBase64: Schema = makeUint8ArrayTransformation( + "Uint8ArrayFromBase64", + Encoding.decodeBase64, + Encoding.encodeBase64 +) + +/** + * Decodes a base64 (URL) encoded string into a `Uint8Array`. + * + * @category Uint8Array transformations + * @since 3.10.0 + */ +export const Uint8ArrayFromBase64Url: Schema = makeUint8ArrayTransformation( + "Uint8ArrayFromBase64Url", + Encoding.decodeBase64Url, + Encoding.encodeBase64Url +) + +/** + * Decodes a hex encoded string into a `Uint8Array`. + * + * @category Uint8Array transformations + * @since 3.10.0 + */ +export const Uint8ArrayFromHex: Schema = makeUint8ArrayTransformation( + "Uint8ArrayFromHex", + Encoding.decodeHex, + Encoding.encodeHex +) + +const makeEncodingTransformation = ( + id: string, + decode: (s: string) => either_.Either, + encode: (u: string) => string +) => + transformOrFail( + String$.annotations({ + description: `A string that is interpreted as being ${id}-encoded and will be decoded into a UTF-8 string` + }), + String$, + { + strict: true, + decode: (i, _, ast) => + either_.mapLeft( + decode(i), + (decodeException) => new ParseResult.Type(ast, i, decodeException.message) + ), + encode: (a) => ParseResult.succeed(encode(a)) + } + ).annotations({ identifier: `StringFrom${id}` }) + +/** + * Decodes a base64 (RFC4648) encoded string into a UTF-8 string. + * + * @category string transformations + * @since 3.10.0 + */ +export const StringFromBase64: Schema = makeEncodingTransformation( + "Base64", + Encoding.decodeBase64String, + Encoding.encodeBase64 +) + +/** + * Decodes a base64 (URL) encoded string into a UTF-8 string. + * + * @category string transformations + * @since 3.10.0 + */ +export const StringFromBase64Url: Schema = makeEncodingTransformation( + "Base64Url", + Encoding.decodeBase64UrlString, + Encoding.encodeBase64Url +) + +/** + * Decodes a hex encoded string into a UTF-8 string. + * + * @category string transformations + * @since 3.10.0 + */ +export const StringFromHex: Schema = makeEncodingTransformation( + "Hex", + Encoding.decodeHexString, + Encoding.encodeHex +) + +/** + * Decodes a URI component encoded string into a UTF-8 string. + * Can be used to store data in a URL. + * + * @example + * ```ts + * import { Schema } from "effect" + * + * const PaginationSchema = Schema.Struct({ + * maxItemPerPage: Schema.Number, + * page: Schema.Number + * }) + * + * const UrlSchema = Schema.compose(Schema.StringFromUriComponent, Schema.parseJson(PaginationSchema)) + * + * console.log(Schema.encodeSync(UrlSchema)({ maxItemPerPage: 10, page: 1 })) + * // Output: %7B%22maxItemPerPage%22%3A10%2C%22page%22%3A1%7D + * ``` + * + * @category string transformations + * @since 3.12.0 + */ +export const StringFromUriComponent = transformOrFail( + String$.annotations({ + description: `A string that is interpreted as being UriComponent-encoded and will be decoded into a UTF-8 string` + }), + String$, + { + strict: true, + decode: (i, _, ast) => + either_.mapLeft( + Encoding.decodeUriComponent(i), + (decodeException) => new ParseResult.Type(ast, i, decodeException.message) + ), + encode: (a, _, ast) => + either_.mapLeft( + Encoding.encodeUriComponent(a), + (encodeException) => new ParseResult.Type(ast, a, encodeException.message) + ) + } +).annotations({ identifier: `StringFromUriComponent` }) + +/** + * @category schema id + * @since 3.10.0 + */ +export const MinItemsSchemaId: unique symbol = schemaId_.MinItemsSchemaId + +/** + * @category schema id + * @since 3.10.0 + */ +export type MinItemsSchemaId = typeof MinItemsSchemaId + +/** + * @category ReadonlyArray filters + * @since 3.10.0 + */ +export const minItems = ( + n: number, + annotations?: Annotations.Filter> +) => +>(self: S & Schema, Schema.Context>): filter => { + const minItems = Math.floor(n) + if (minItems < 1) { + throw new Error( + errors_.getInvalidArgumentErrorMessage(`Expected an integer greater than or equal to 1, actual ${n}`) + ) + } + return self.pipe( + filter( + (a) => a.length >= minItems, + { + schemaId: MinItemsSchemaId, + title: `minItems(${minItems})`, + description: `an array of at least ${minItems} item(s)`, + jsonSchema: { minItems }, + [AST.StableFilterAnnotationId]: true, + ...annotations + } + ) + ) +} + +/** + * @category schema id + * @since 3.10.0 + */ +export const MaxItemsSchemaId: unique symbol = schemaId_.MaxItemsSchemaId + +/** + * @category schema id + * @since 3.10.0 + */ +export type MaxItemsSchemaId = typeof MaxItemsSchemaId + +/** + * @category ReadonlyArray filters + * @since 3.10.0 + */ +export const maxItems = ( + n: number, + annotations?: Annotations.Filter> +) => +>(self: S & Schema, Schema.Context>): filter => { + const maxItems = Math.floor(n) + if (maxItems < 1) { + throw new Error( + errors_.getInvalidArgumentErrorMessage(`Expected an integer greater than or equal to 1, actual ${n}`) + ) + } + return self.pipe( + filter((a) => a.length <= maxItems, { + schemaId: MaxItemsSchemaId, + title: `maxItems(${maxItems})`, + description: `an array of at most ${maxItems} item(s)`, + jsonSchema: { maxItems }, + [AST.StableFilterAnnotationId]: true, + ...annotations + }) + ) +} + +/** + * @category schema id + * @since 3.10.0 + */ +export const ItemsCountSchemaId: unique symbol = schemaId_.ItemsCountSchemaId + +/** + * @category schema id + * @since 3.10.0 + */ +export type ItemsCountSchemaId = typeof ItemsCountSchemaId + +/** + * @category ReadonlyArray filters + * @since 3.10.0 + */ +export const itemsCount = ( + n: number, + annotations?: Annotations.Filter> +) => +>(self: S & Schema, Schema.Context>): filter => { + const itemsCount = Math.floor(n) + if (itemsCount < 0) { + throw new Error( + errors_.getInvalidArgumentErrorMessage(`Expected an integer greater than or equal to 0, actual ${n}`) + ) + } + return self.pipe( + filter((a) => a.length === itemsCount, { + schemaId: ItemsCountSchemaId, + title: `itemsCount(${itemsCount})`, + description: `an array of exactly ${itemsCount} item(s)`, + jsonSchema: { minItems: itemsCount, maxItems: itemsCount }, + [AST.StableFilterAnnotationId]: true, + ...annotations + }) + ) +} + +/** + * @category ReadonlyArray transformations + * @since 3.10.0 + */ +export const getNumberIndexedAccess = , I extends ReadonlyArray, R>( + self: Schema +): SchemaClass => make(AST.getNumberIndexedAccess(self.ast)) + +/** + * Get the first element of a `ReadonlyArray`, or `None` if the array is empty. + * + * @category ReadonlyArray transformations + * @since 3.10.0 + */ +export function head>( + self: S & Schema, Schema.Context> +): transform>> { + return transform( + self, + OptionFromSelf(getNumberIndexedAccess(typeSchema(self))), + { + strict: false, + decode: (i) => array_.head(i), + encode: (a) => + option_.match(a, { + onNone: () => [], + onSome: array_.of + }) + } + ) +} + +/** + * Get the first element of a `NonEmptyReadonlyArray`. + * + * @category NonEmptyReadonlyArray transformations + * @since 3.12.0 + */ +export function headNonEmpty>( + self: S & Schema, Schema.Context> +): transform> { + return transform( + self, + getNumberIndexedAccess(typeSchema(self)), + { + strict: false, + decode: (i) => array_.headNonEmpty(i), + encode: (a) => array_.of(a) + } + ) +} + +/** + * Retrieves the first element of a `ReadonlyArray`. + * + * If the array is empty, it returns the `fallback` argument if provided; otherwise, it fails. + * + * @category ReadonlyArray transformations + * @since 3.10.0 + */ +export const headOrElse: { + >( + fallback?: LazyArg + ): ( + self: S & Schema, Schema.Context> + ) => transform> + >( + self: S & Schema, Schema.Context>, + fallback?: LazyArg + ): transform> +} = dual( + (args) => isSchema(args[0]), + ( + self: Schema, I, R>, + fallback?: LazyArg + ): transform, I, R>, SchemaClass> => + transformOrFail( + self, + getNumberIndexedAccess(typeSchema(self)), + { + strict: true, + decode: (i, _, ast) => + i.length > 0 + ? ParseResult.succeed(i[0]) + : fallback + ? ParseResult.succeed(fallback()) + : ParseResult.fail(new ParseResult.Type(ast, i, "Unable to retrieve the first element of an empty array")), + encode: (a) => ParseResult.succeed(array_.of(a)) + } + ) +) + +/** + * @category schema id + * @since 3.10.0 + */ +export const ValidDateSchemaId: unique symbol = Symbol.for("effect/SchemaId/ValidDate") + +/** + * Defines a filter that specifically rejects invalid dates, such as `new + * Date("Invalid Date")`. This filter ensures that only properly formatted and + * valid date objects are accepted, enhancing data integrity by preventing + * erroneous date values from being processed. + * + * @category Date filters + * @since 3.10.0 + */ +export const validDate = + (annotations?: Annotations.Filter>) => + (self: S & Schema, Schema.Context>): filter => + self.pipe( + filter((a) => !Number.isNaN(a.getTime()), { + schemaId: ValidDateSchemaId, + [ValidDateSchemaId]: { noInvalidDate: true }, + title: "validDate", + description: "a valid Date", + ...annotations + }) + ) + +/** + * @category schema id + * @since 3.10.0 + */ +export const LessThanDateSchemaId: unique symbol = Symbol.for("effect/SchemaId/LessThanDate") + +/** + * @category Date filters + * @since 3.10.0 + */ +export const lessThanDate = ( + max: Date, + annotations?: Annotations.Filter> +) => +(self: S & Schema, Schema.Context>): filter => + self.pipe( + filter((a: Date) => a < max, { + schemaId: LessThanDateSchemaId, + [LessThanDateSchemaId]: { max }, + title: `lessThanDate(${Inspectable.formatDate(max)})`, + description: `a date before ${Inspectable.formatDate(max)}`, + ...annotations + }) + ) + +/** + * @category schema id + * @since 3.10.0 + */ +export const LessThanOrEqualToDateSchemaId: unique symbol = Symbol.for( + "effect/schema/LessThanOrEqualToDate" +) + +/** + * @category Date filters + * @since 3.10.0 + */ +export const lessThanOrEqualToDate = ( + max: Date, + annotations?: Annotations.Filter> +) => +(self: S & Schema, Schema.Context>): filter => + self.pipe( + filter((a: Date) => a <= max, { + schemaId: LessThanOrEqualToDateSchemaId, + [LessThanOrEqualToDateSchemaId]: { max }, + title: `lessThanOrEqualToDate(${Inspectable.formatDate(max)})`, + description: `a date before or equal to ${Inspectable.formatDate(max)}`, + ...annotations + }) + ) + +/** + * @category schema id + * @since 3.10.0 + */ +export const GreaterThanDateSchemaId: unique symbol = Symbol.for("effect/SchemaId/GreaterThanDate") + +/** + * @category Date filters + * @since 3.10.0 + */ +export const greaterThanDate = ( + min: Date, + annotations?: Annotations.Filter> +) => +(self: S & Schema, Schema.Context>): filter => + self.pipe( + filter((a: Date) => a > min, { + schemaId: GreaterThanDateSchemaId, + [GreaterThanDateSchemaId]: { min }, + title: `greaterThanDate(${Inspectable.formatDate(min)})`, + description: `a date after ${Inspectable.formatDate(min)}`, + ...annotations + }) + ) + +/** + * @category schema id + * @since 3.10.0 + */ +export const GreaterThanOrEqualToDateSchemaId: unique symbol = Symbol.for( + "effect/schema/GreaterThanOrEqualToDate" +) + +/** + * @category Date filters + * @since 3.10.0 + */ +export const greaterThanOrEqualToDate = ( + min: Date, + annotations?: Annotations.Filter> +) => +(self: S & Schema, Schema.Context>): filter => + self.pipe( + filter((a: Date) => a >= min, { + schemaId: GreaterThanOrEqualToDateSchemaId, + [GreaterThanOrEqualToDateSchemaId]: { min }, + title: `greaterThanOrEqualToDate(${Inspectable.formatDate(min)})`, + description: `a date after or equal to ${Inspectable.formatDate(min)}`, + ...annotations + }) + ) + +/** + * @category schema id + * @since 3.10.0 + */ +export const BetweenDateSchemaId: unique symbol = Symbol.for("effect/SchemaId/BetweenDate") + +/** + * @category Date filters + * @since 3.10.0 + */ +export const betweenDate = ( + min: Date, + max: Date, + annotations?: Annotations.Filter> +) => +(self: S & Schema, Schema.Context>): filter => + self.pipe( + filter((a: Date) => a <= max && a >= min, { + schemaId: BetweenDateSchemaId, + [BetweenDateSchemaId]: { max, min }, + title: `betweenDate(${Inspectable.formatDate(min)}, ${Inspectable.formatDate(max)})`, + description: `a date between ${Inspectable.formatDate(min)} and ${Inspectable.formatDate(max)}`, + ...annotations + }) + ) + +/** + * @category schema id + * @since 3.11.8 + */ +export const DateFromSelfSchemaId: unique symbol = schemaId_.DateFromSelfSchemaId + +/** + * @category schema id + * @since 3.11.8 + */ +export type DateFromSelfSchemaId = typeof DateFromSelfSchemaId + +/** + * Describes a schema that accommodates potentially invalid `Date` instances, + * such as `new Date("Invalid Date")`, without rejection. + * + * @category Date constructors + * @since 3.10.0 + */ +export class DateFromSelf extends declare( + Predicate.isDate, + { + typeConstructor: { _tag: "Date" }, + identifier: "DateFromSelf", + schemaId: DateFromSelfSchemaId, + [DateFromSelfSchemaId]: { noInvalidDate: false }, + description: "a potentially invalid Date instance", + pretty: () => (date) => `new Date(${JSON.stringify(date)})`, + arbitrary: () => (fc) => fc.date({ noInvalidDate: false }), + equivalence: () => Equivalence.Date + } +) {} + +/** + * Defines a schema that ensures only valid dates are accepted. This schema + * rejects values like `new Date("Invalid Date")`, which, despite being a `Date` + * instance, represents an invalid date. Such stringent validation ensures that + * all date objects processed through this schema are properly formed and + * represent real dates. + * + * @category Date constructors + * @since 3.10.0 + */ +export class ValidDateFromSelf extends DateFromSelf.pipe( + validDate({ + identifier: "ValidDateFromSelf", + description: "a valid Date instance" + }) +) {} + +/** + * Defines a schema that attempts to convert a `string` to a `Date` object using + * the `new Date` constructor. This conversion is lenient, meaning it does not + * reject strings that do not form valid dates (e.g., using `new Date("Invalid + * Date")` results in a `Date` object, despite being invalid). + * + * @category Date transformations + * @since 3.10.0 + */ +export class DateFromString extends transform( + String$.annotations({ description: "a string to be decoded into a Date" }), + DateFromSelf, + { + strict: true, + decode: (i) => new Date(i), + encode: (a) => Inspectable.formatDate(a) + } +).annotations({ identifier: "DateFromString" }) {} + +/** @ignore */ +class Date$ extends DateFromString.pipe( + validDate({ identifier: "Date" }) +) {} + +export { + /** + * This schema converts a `string` into a `Date` object using the `new Date` + * constructor. It ensures that only valid date strings are accepted, + * rejecting any strings that would result in an invalid date, such as `new + * Date("Invalid Date")`. + * + * @category Date transformations + * @since 3.10.0 + */ + Date$ as Date +} + +/** + * Defines a schema that converts a `number` into a `Date` object using the `new + * Date` constructor. This schema does not validate the numerical input, + * allowing potentially invalid values such as `NaN`, `Infinity`, and + * `-Infinity` to be converted into `Date` objects. During the encoding process, + * any invalid `Date` object will be encoded to `NaN`. + * + * @category Date transformations + * @since 3.10.0 + */ +export class DateFromNumber extends transform( + Number$.annotations({ description: "a number to be decoded into a Date" }), + DateFromSelf, + { + strict: true, + decode: (i) => new Date(i), + encode: (a) => a.getTime() + } +).annotations({ identifier: "DateFromNumber" }) {} + +/** + * Describes a schema that represents a `DateTime.Utc` instance. + * + * @category DateTime.Utc constructors + * @since 3.10.0 + */ +export class DateTimeUtcFromSelf extends declare( + (u) => dateTime.isDateTime(u) && dateTime.isUtc(u), + { + typeConstructor: { _tag: "effect/DateTime.Utc" }, + identifier: "DateTimeUtcFromSelf", + description: "a DateTime.Utc instance", + pretty: (): pretty_.Pretty => (dateTime) => dateTime.toString(), + arbitrary: (): LazyArbitrary => (fc) => + fc.date({ noInvalidDate: true }).map((date) => dateTime.unsafeFromDate(date)), + equivalence: () => dateTime.Equivalence + } +) {} + +const decodeDateTimeUtc = (input: A, ast: AST.AST) => + ParseResult.try({ + try: () => dateTime.unsafeMake(input), + catch: () => + new ParseResult.Type(ast, input, `Unable to decode ${Inspectable.formatUnknown(input)} into a DateTime.Utc`) + }) + +/** + * Defines a schema that attempts to convert a `number` to a `DateTime.Utc` instance using the `DateTime.unsafeMake` constructor. + * + * @category DateTime.Utc transformations + * @since 3.10.0 + */ +export class DateTimeUtcFromNumber extends transformOrFail( + Number$.annotations({ description: "a number to be decoded into a DateTime.Utc" }), + DateTimeUtcFromSelf, + { + strict: true, + decode: (i, _, ast) => decodeDateTimeUtc(i, ast), + encode: (a) => ParseResult.succeed(dateTime.toEpochMillis(a)) + } +).annotations({ identifier: "DateTimeUtcFromNumber" }) {} + +/** + * Defines a schema that attempts to convert a `Date` to a `DateTime.Utc` instance using the `DateTime.unsafeMake` constructor. + * + * @category DateTime.Utc transformations + * @since 3.12.0 + */ +export class DateTimeUtcFromDate extends transformOrFail( + DateFromSelf.annotations({ description: "a Date to be decoded into a DateTime.Utc" }), + DateTimeUtcFromSelf, + { + strict: true, + decode: (i, _, ast) => decodeDateTimeUtc(i, ast), + encode: (a) => ParseResult.succeed(dateTime.toDateUtc(a)) + } +).annotations({ identifier: "DateTimeUtcFromDate" }) {} + +/** + * Defines a schema that attempts to convert a `string` to a `DateTime.Utc` instance using the `DateTime.unsafeMake` constructor. + * + * @category DateTime.Utc transformations + * @since 3.10.0 + */ +export class DateTimeUtc extends transformOrFail( + String$.annotations({ description: "a string to be decoded into a DateTime.Utc" }), + DateTimeUtcFromSelf, + { + strict: true, + decode: (i, _, ast) => decodeDateTimeUtc(i, ast), + encode: (a) => ParseResult.succeed(dateTime.formatIso(a)) + } +).annotations({ identifier: "DateTimeUtc" }) {} + +const timeZoneOffsetArbitrary = (): LazyArbitrary => (fc) => + fc.integer({ min: -12 * 60 * 60 * 1000, max: 14 * 60 * 60 * 1000 }).map(dateTime.zoneMakeOffset) + +/** + * Describes a schema that represents a `TimeZone.Offset` instance. + * + * @category TimeZone constructors + * @since 3.10.0 + */ +export class TimeZoneOffsetFromSelf extends declare( + dateTime.isTimeZoneOffset, + { + typeConstructor: { _tag: "effect/DateTime.TimeZone.Offset" }, + identifier: "TimeZoneOffsetFromSelf", + description: "a TimeZone.Offset instance", + pretty: (): pretty_.Pretty => (zone) => zone.toString(), + arbitrary: timeZoneOffsetArbitrary + } +) {} + +/** + * Defines a schema that converts a `number` to a `TimeZone.Offset` instance using the `DateTime.zoneMakeOffset` constructor. + * + * @category TimeZone transformations + * @since 3.10.0 + */ +export class TimeZoneOffset extends transform( + Number$.annotations({ description: "a number to be decoded into a TimeZone.Offset" }), + TimeZoneOffsetFromSelf, + { + strict: true, + decode: (i) => dateTime.zoneMakeOffset(i), + encode: (a) => a.offset + } +).annotations({ identifier: "TimeZoneOffset" }) {} + +const timeZoneNamedArbitrary = (): LazyArbitrary => (fc) => + fc.constantFrom(...Intl.supportedValuesOf("timeZone")).map(dateTime.zoneUnsafeMakeNamed) + +/** + * Describes a schema that represents a `TimeZone.Named` instance. + * + * @category TimeZone constructors + * @since 3.10.0 + */ +export class TimeZoneNamedFromSelf extends declare( + dateTime.isTimeZoneNamed, + { + typeConstructor: { _tag: "effect/DateTime.TimeZone.Named" }, + identifier: "TimeZoneNamedFromSelf", + description: "a TimeZone.Named instance", + pretty: (): pretty_.Pretty => (zone) => zone.toString(), + arbitrary: timeZoneNamedArbitrary + } +) {} + +/** + * Defines a schema that attempts to convert a `string` to a `TimeZone.Named` instance using the `DateTime.zoneUnsafeMakeNamed` constructor. + * + * @category TimeZone transformations + * @since 3.10.0 + */ +export class TimeZoneNamed extends transformOrFail( + String$.annotations({ description: "a string to be decoded into a TimeZone.Named" }), + TimeZoneNamedFromSelf, + { + strict: true, + decode: (i, _, ast) => + ParseResult.try({ + try: () => dateTime.zoneUnsafeMakeNamed(i), + catch: () => new ParseResult.Type(ast, i, `Unable to decode ${JSON.stringify(i)} into a TimeZone.Named`) + }), + encode: (a) => ParseResult.succeed(a.id) + } +).annotations({ identifier: "TimeZoneNamed" }) {} + +/** + * @category TimeZone constructors + * @since 3.10.0 + */ +export class TimeZoneFromSelf extends Union(TimeZoneOffsetFromSelf, TimeZoneNamedFromSelf) {} + +/** + * Defines a schema that attempts to convert a `string` to a `TimeZone` using the `DateTime.zoneFromString` constructor. + * + * @category TimeZone transformations + * @since 3.10.0 + */ +export class TimeZone extends transformOrFail( + String$.annotations({ description: "a string to be decoded into a TimeZone" }), + TimeZoneFromSelf, + { + strict: true, + decode: (i, _, ast) => + option_.match(dateTime.zoneFromString(i), { + onNone: () => + ParseResult.fail(new ParseResult.Type(ast, i, `Unable to decode ${JSON.stringify(i)} into a TimeZone`)), + onSome: ParseResult.succeed + }), + encode: (a) => ParseResult.succeed(dateTime.zoneToString(a)) + } +).annotations({ identifier: "TimeZone" }) {} + +const timeZoneArbitrary: LazyArbitrary = (fc) => + fc.oneof( + timeZoneOffsetArbitrary()(fc), + timeZoneNamedArbitrary()(fc) + ) + +/** + * Describes a schema that represents a `DateTime.Zoned` instance. + * + * @category DateTime.Zoned constructors + * @since 3.10.0 + */ +export class DateTimeZonedFromSelf extends declare( + (u) => dateTime.isDateTime(u) && dateTime.isZoned(u), + { + typeConstructor: { _tag: "effect/DateTime.Zoned" }, + identifier: "DateTimeZonedFromSelf", + description: "a DateTime.Zoned instance", + pretty: (): pretty_.Pretty => (dateTime) => dateTime.toString(), + arbitrary: (): LazyArbitrary => (fc) => + fc.tuple( + fc.integer({ + // time zone db supports +/- 1000 years or so + min: -31536000000000, + max: 31536000000000 + }), + timeZoneArbitrary(fc) + ).map(([millis, timeZone]) => dateTime.unsafeMakeZoned(millis, { timeZone })), + equivalence: () => dateTime.Equivalence + } +) {} + +/** + * Defines a schema that attempts to convert a `string` to a `DateTime.Zoned` instance. + * + * @category DateTime.Zoned transformations + * @since 3.10.0 + */ +export class DateTimeZoned extends transformOrFail( + String$.annotations({ description: "a string to be decoded into a DateTime.Zoned" }), + DateTimeZonedFromSelf, + { + strict: true, + decode: (i, _, ast) => + option_.match(dateTime.makeZonedFromString(i), { + onNone: () => + ParseResult.fail(new ParseResult.Type(ast, i, `Unable to decode ${JSON.stringify(i)} into a DateTime.Zoned`)), + onSome: ParseResult.succeed + }), + encode: (a) => ParseResult.succeed(dateTime.formatIsoZoned(a)) + } +).annotations({ identifier: "DateTimeZoned" }) {} + +/** + * @category Option utils + * @since 3.10.0 + */ +export type OptionEncoded = + | { + readonly _tag: "None" + } + | { + readonly _tag: "Some" + readonly value: I + } + +const OptionNoneEncoded = Struct({ + _tag: Literal("None") +}).annotations({ description: "NoneEncoded" }) + +const optionSomeEncoded = (value: Value) => + Struct({ + _tag: Literal("Some"), + value + }).annotations({ description: `SomeEncoded<${format(value)}>` }) + +const optionEncoded = (value: Value) => + Union( + OptionNoneEncoded, + optionSomeEncoded(value) + ).annotations({ + description: `OptionEncoded<${format(value)}>` + }) + +const optionDecode = (input: OptionEncoded): option_.Option => + input._tag === "None" ? option_.none() : option_.some(input.value) + +const optionArbitrary = + (value: LazyArbitrary, ctx: ArbitraryGenerationContext): LazyArbitrary> => (fc) => + fc.oneof( + ctx, + fc.record({ _tag: fc.constant("None" as const) }), + fc.record({ _tag: fc.constant("Some" as const), value: value(fc) }) + ).map(optionDecode) + +const optionPretty = (value: pretty_.Pretty): pretty_.Pretty> => + option_.match({ + onNone: () => "none()", + onSome: (a) => `some(${value(a)})` + }) + +const optionParse = + (decodeUnknown: ParseResult.DecodeUnknown): ParseResult.DeclarationDecodeUnknown, R> => + (u, options, ast) => + option_.isOption(u) ? + option_.isNone(u) ? + ParseResult.succeed(option_.none()) + : toComposite(decodeUnknown(u.value, options), option_.some, ast, u) + : ParseResult.fail(new ParseResult.Type(ast, u)) + +/** + * @category api interface + * @since 3.10.0 + */ +export interface OptionFromSelf extends + AnnotableDeclare< + OptionFromSelf, + option_.Option>, + option_.Option>, + [Value] + > +{} + +const OptionFromSelf_ = (value: Value): OptionFromSelf => { + return declare( + [value], + { + decode: (value) => optionParse(ParseResult.decodeUnknown(value)), + encode: (value) => optionParse(ParseResult.encodeUnknown(value)) + }, + { + typeConstructor: { _tag: "effect/Option" }, + pretty: optionPretty, + arbitrary: optionArbitrary, + equivalence: option_.getEquivalence + } + ) +} + +/** + * @category Option transformations + * @since 3.10.0 + */ +export const OptionFromSelf = (value: Value): OptionFromSelf => { + return OptionFromSelf_(value).annotations({ description: `Option<${format(value)}>` }) +} + +/** + * @category api interface + * @since 3.10.0 + */ +export interface Option extends + transform< + Union<[ + Struct<{ _tag: Literal<["None"]> }>, + Struct<{ _tag: Literal<["Some"]>; value: Value }> + ]>, + OptionFromSelf>> + > +{} + +const makeNoneEncoded = { + _tag: "None" +} as const + +const makeSomeEncoded = (value: A) => ({ + _tag: "Some", + value +} as const) + +/** + * @category Option transformations + * @since 3.10.0 + */ +export function Option(value: Value): Option { + const value_ = asSchema(value) + const out = transform( + optionEncoded(value_), + OptionFromSelf(typeSchema(value_)), + { + strict: true, + decode: (i) => optionDecode(i), + encode: (a) => + option_.match(a, { + onNone: () => makeNoneEncoded, + onSome: makeSomeEncoded + }) + } + ) + return out as any +} + +/** + * @category api interface + * @since 3.10.0 + */ +export interface OptionFromNullOr + extends transform, OptionFromSelf>>> +{} + +/** + * @category Option transformations + * @since 3.10.0 + */ +export function OptionFromNullOr(value: Value): OptionFromNullOr { + return transform(NullOr(value), OptionFromSelf(typeSchema(asSchema(value))), { + strict: true, + decode: (i) => option_.fromNullable(i), + encode: (a) => option_.getOrNull(a) + }) +} + +/** + * @category api interface + * @since 3.10.0 + */ +export interface OptionFromNullishOr + extends transform, OptionFromSelf>>> +{} + +/** + * @category Option transformations + * @since 3.10.0 + */ +export function OptionFromNullishOr( + value: Value, + onNoneEncoding: null | undefined +): OptionFromNullishOr { + return transform( + NullishOr(value), + OptionFromSelf(typeSchema(asSchema(value))), + { + strict: true, + decode: (i) => option_.fromNullable(i), + encode: onNoneEncoding === null ? + (a) => option_.getOrNull(a) : + (a) => option_.getOrUndefined(a) + } + ) +} + +/** + * @category api interface + * @since 3.10.0 + */ +export interface OptionFromUndefinedOr + extends transform, OptionFromSelf>>> +{} + +/** + * @category Option transformations + * @since 3.10.0 + */ +export function OptionFromUndefinedOr(value: Value): OptionFromUndefinedOr { + return transform(UndefinedOr(value), OptionFromSelf(typeSchema(asSchema(value))), { + strict: true, + decode: (i) => option_.fromNullable(i), + encode: (a) => option_.getOrUndefined(a) + }) +} + +/** + * Transforms strings into an Option type, effectively filtering out empty or + * whitespace-only strings by trimming them and checking their length. Returns + * `none` for invalid inputs and `some` for valid non-empty strings. + * + * @example + * ```ts + * import { Schema } from "effect" + * + * console.log(Schema.decodeSync(Schema.OptionFromNonEmptyTrimmedString)("")) // Option.none() + * console.log(Schema.decodeSync(Schema.OptionFromNonEmptyTrimmedString)(" a ")) // Option.some("a") + * console.log(Schema.decodeSync(Schema.OptionFromNonEmptyTrimmedString)("a")) // Option.some("a") + * ``` + * + * @category Option transformations + * @since 3.10.0 + */ +export class OptionFromNonEmptyTrimmedString extends transform(String$, OptionFromSelf(NonEmptyTrimmedString), { + strict: true, + decode: (i) => option_.filter(option_.some(i.trim()), string_.isNonEmpty), + encode: (a) => option_.getOrElse(a, () => "") +}) {} + +/** + * @category Either utils + * @since 3.10.0 + */ +export type RightEncoded = { + readonly _tag: "Right" + readonly right: IA +} + +/** + * @category Either utils + * @since 3.10.0 + */ +export type LeftEncoded = { + readonly _tag: "Left" + readonly left: IE +} + +/** + * @category Either utils + * @since 3.10.0 + */ +export type EitherEncoded = RightEncoded | LeftEncoded + +const rightEncoded = (right: Right) => + Struct({ + _tag: Literal("Right"), + right + }).annotations({ description: `RightEncoded<${format(right)}>` }) + +const leftEncoded = (left: Left) => + Struct({ + _tag: Literal("Left"), + left + }).annotations({ description: `LeftEncoded<${format(left)}>` }) + +const eitherEncoded = ( + right: Right, + left: Left +) => + Union(rightEncoded(right), leftEncoded(left)).annotations({ + description: `EitherEncoded<${format(left)}, ${format(right)}>` + }) + +const eitherDecode = (input: EitherEncoded): either_.Either => + input._tag === "Left" ? either_.left(input.left) : either_.right(input.right) + +const eitherArbitrary = ( + right: LazyArbitrary, + left: LazyArbitrary +): LazyArbitrary> => +(fc) => + fc.oneof( + fc.record({ _tag: fc.constant("Left" as const), left: left(fc) }), + fc.record({ _tag: fc.constant("Right" as const), right: right(fc) }) + ).map(eitherDecode) + +const eitherPretty = ( + right: pretty_.Pretty, + left: pretty_.Pretty +): pretty_.Pretty> => + either_.match({ + onLeft: (e) => `left(${left(e)})`, + onRight: (a) => `right(${right(a)})` + }) + +const eitherParse = ( + parseRight: ParseResult.DecodeUnknown, + decodeUnknownLeft: ParseResult.DecodeUnknown +): ParseResult.DeclarationDecodeUnknown, LR | RR> => +(u, options, ast) => + either_.isEither(u) ? + either_.match(u, { + onLeft: (left) => toComposite(decodeUnknownLeft(left, options), either_.left, ast, u), + onRight: (right) => toComposite(parseRight(right, options), either_.right, ast, u) + }) + : ParseResult.fail(new ParseResult.Type(ast, u)) + +/** + * @category api interface + * @since 3.10.0 + */ +export interface EitherFromSelf extends + AnnotableDeclare< + EitherFromSelf, + either_.Either, Schema.Type>, + either_.Either, Schema.Encoded>, + [R, L] + > +{} + +/** + * @category Either transformations + * @since 3.10.0 + */ +export const EitherFromSelf = ({ left, right }: { + readonly left: L + readonly right: R +}): EitherFromSelf => { + return declare( + [right, left], + { + decode: (right, left) => eitherParse(ParseResult.decodeUnknown(right), ParseResult.decodeUnknown(left)), + encode: (right, left) => eitherParse(ParseResult.encodeUnknown(right), ParseResult.encodeUnknown(left)) + }, + { + typeConstructor: { _tag: "effect/Either" }, + description: `Either<${format(right)}, ${format(left)}>`, + pretty: eitherPretty, + arbitrary: eitherArbitrary, + equivalence: (right, left) => either_.getEquivalence({ left, right }) + } + ) +} + +const makeLeftEncoded = (left: E) => (({ + _tag: "Left", + left +}) as const) +const makeRightEncoded = (right: A) => (({ + _tag: "Right", + right +}) as const) + +/** + * @category api interface + * @since 3.10.0 + */ +export interface Either extends + transform< + Union<[ + Struct<{ + _tag: Literal<["Right"]> + right: Right + }>, + Struct<{ + _tag: Literal<["Left"]> + left: Left + }> + ]>, + EitherFromSelf>, SchemaClass>> + > +{} + +/** + * @category Either transformations + * @since 3.10.0 + */ +export const Either = ({ left, right }: { + readonly left: L + readonly right: R +}): Either => { + const right_ = asSchema(right) + const left_ = asSchema(left) + const out = transform( + eitherEncoded(right_, left_), + EitherFromSelf({ left: typeSchema(left_), right: typeSchema(right_) }), + { + strict: true, + decode: (i) => eitherDecode(i), + encode: (a) => + either_.match(a, { + onLeft: makeLeftEncoded, + onRight: makeRightEncoded + }) + } + ) + return out as any +} + +/** + * @category api interface + * @since 3.10.0 + */ +export interface EitherFromUnion extends + transform< + Union<[ + transform; right: SchemaClass> }>>, + transform; right: SchemaClass> }>> + ]>, + EitherFromSelf>, SchemaClass>> + > +{} + +/** + * @example + * ```ts + * import * as Schema from "effect/Schema" + * + * // Schema> + * Schema.EitherFromUnion({ left: Schema.String, right: Schema.Number }) + * ``` + * + * @category Either transformations + * @since 3.10.0 + */ +export const EitherFromUnion = ({ left, right }: { + readonly left: Left + readonly right: Right +}): EitherFromUnion => { + const right_ = asSchema(right) + const left_ = asSchema(left) + const toright = typeSchema(right_) + const toleft = typeSchema(left_) + const fromRight = transform(right_, rightEncoded(toright), { + strict: true, + decode: (i) => makeRightEncoded(i), + encode: (a) => a.right + }) + const fromLeft = transform(left_, leftEncoded(toleft), { + strict: true, + decode: (i) => makeLeftEncoded(i), + encode: (a) => a.left + }) + const out = transform( + Union(fromRight, fromLeft), + EitherFromSelf({ left: toleft, right: toright }), + { + strict: true, + decode: (i) => i._tag === "Left" ? either_.left(i.left) : either_.right(i.right), + encode: (a) => + either_.match(a, { + onLeft: makeLeftEncoded, + onRight: makeRightEncoded + }) + } + ) + return out as any +} + +const mapArbitrary = ( + key: LazyArbitrary, + value: LazyArbitrary, + ctx: ArbitraryGenerationContext +): LazyArbitrary> => { + return (fc) => { + const items = fc.array(fc.tuple(key(fc), value(fc))) + return (ctx.depthIdentifier !== undefined ? fc.oneof(ctx, fc.constant([]), items) : items).map((as) => new Map(as)) + } +} + +const readonlyMapPretty = ( + key: pretty_.Pretty, + value: pretty_.Pretty +): pretty_.Pretty> => +(map) => + `new Map([${ + Array.from(map.entries()) + .map(([k, v]) => `[${key(k)}, ${value(v)}]`) + .join(", ") + }])` + +const readonlyMapEquivalence = ( + key: Equivalence.Equivalence, + value: Equivalence.Equivalence +): Equivalence.Equivalence> => { + const arrayEquivalence = array_.getEquivalence( + Equivalence.make<[K, V]>(([ka, va], [kb, vb]) => key(ka, kb) && value(va, vb)) + ) + return Equivalence.make((a, b) => arrayEquivalence(Array.from(a.entries()), Array.from(b.entries()))) +} + +const readonlyMapParse = ( + decodeUnknown: ParseResult.DecodeUnknown, R> +): ParseResult.DeclarationDecodeUnknown, R> => +(u, options, ast) => + Predicate.isMap(u) ? + toComposite(decodeUnknown(Array.from(u.entries()), options), (as) => new Map(as), ast, u) + : ParseResult.fail(new ParseResult.Type(ast, u)) + +/** + * @category api interface + * @since 3.10.0 + */ +export interface ReadonlyMapFromSelf extends + AnnotableDeclare< + ReadonlyMapFromSelf, + ReadonlyMap, Schema.Type>, + ReadonlyMap, Schema.Encoded>, + [K, V] + > +{} + +const mapFromSelf_ = ( + key: K, + value: V, + description: string +): ReadonlyMapFromSelf => + declare( + [key, value], + { + decode: (Key, Value) => readonlyMapParse(ParseResult.decodeUnknown(Array$(Tuple(Key, Value)))), + encode: (Key, Value) => readonlyMapParse(ParseResult.encodeUnknown(Array$(Tuple(Key, Value)))) + }, + { + typeConstructor: { _tag: "ReadonlyMap" }, + description, + pretty: readonlyMapPretty, + arbitrary: mapArbitrary, + equivalence: readonlyMapEquivalence + } + ) + +/** + * @category ReadonlyMap + * @since 3.10.0 + */ +export const ReadonlyMapFromSelf = ({ key, value }: { + readonly key: K + readonly value: V +}): ReadonlyMapFromSelf => mapFromSelf_(key, value, `ReadonlyMap<${format(key)}, ${format(value)}>`) + +/** + * @category api interface + * @since 3.10.0 + */ +export interface MapFromSelf extends + AnnotableDeclare< + MapFromSelf, + Map, Schema.Type>, + ReadonlyMap, Schema.Encoded>, + [K, V] + > +{} + +/** + * @category Map + * @since 3.10.0 + */ +export const MapFromSelf = ({ key, value }: { + readonly key: K + readonly value: V +}): MapFromSelf => mapFromSelf_(key, value, `Map<${format(key)}, ${format(value)}>`) as any + +/** + * @category api interface + * @since 3.10.0 + */ +export interface ReadonlyMap$ + extends transform>, ReadonlyMapFromSelf>, SchemaClass>>> +{} + +/** + * @category ReadonlyMap transformations + * @since 3.10.0 + */ +export function ReadonlyMap({ key, value }: { + readonly key: K + readonly value: V +}): ReadonlyMap$ { + return transform( + Array$(Tuple(key, value)), + ReadonlyMapFromSelf({ key: typeSchema(asSchema(key)), value: typeSchema(asSchema(value)) }), + { + strict: true, + decode: (i) => new Map(i), + encode: (a) => Array.from(a.entries()) + } + ) +} + +/** + * @category api interface + * @since 3.10.0 + */ +export interface Map$ + extends transform>, MapFromSelf>, SchemaClass>>> +{} + +/** @ignore */ +function map({ key, value }: { + readonly key: K + readonly value: V +}): Map$ { + return transform( + Array$(Tuple(key, value)), + MapFromSelf({ key: typeSchema(asSchema(key)), value: typeSchema(asSchema(value)) }), + { + strict: true, + decode: (i) => new Map(i), + encode: (a) => Array.from(a.entries()) + } + ) +} + +export { + /** + * @category Map transformations + * @since 3.10.0 + */ + map as Map +} + +/** + * @category ReadonlyMap transformations + * @since 3.10.0 + */ +export const ReadonlyMapFromRecord = ({ key, value }: { + key: Schema + value: Schema +}): SchemaClass, { readonly [x: string]: VI }, KR | VR> => + transform( + Record({ key: encodedBoundSchema(key), value }).annotations({ + description: "a record to be decoded into a ReadonlyMap" + }), + ReadonlyMapFromSelf({ key, value: typeSchema(value) }), + { + strict: true, + decode: (i) => new Map(Object.entries(i)), + encode: (a) => Object.fromEntries(a) + } + ) + +/** + * @category Map transformations + * @since 3.10.0 + */ +export const MapFromRecord = ({ key, value }: { + key: Schema + value: Schema +}): SchemaClass, { readonly [x: string]: VI }, KR | VR> => + transform( + Record({ key: encodedBoundSchema(key), value }).annotations({ + description: "a record to be decoded into a Map" + }), + MapFromSelf({ key, value: typeSchema(value) }), + { + strict: true, + decode: (i) => new Map(Object.entries(i)), + encode: (a) => Object.fromEntries(a) + } + ) + +const setArbitrary = + (item: LazyArbitrary, ctx: ArbitraryGenerationContext): LazyArbitrary> => (fc) => { + const items = fc.array(item(fc)) + return (ctx.depthIdentifier !== undefined ? fc.oneof(ctx, fc.constant([]), items) : items).map((as) => new Set(as)) + } + +const readonlySetPretty = (item: pretty_.Pretty): pretty_.Pretty> => (set) => + `new Set([${Array.from(set.values()).map((a) => item(a)).join(", ")}])` + +const readonlySetEquivalence = ( + item: Equivalence.Equivalence +): Equivalence.Equivalence> => { + const arrayEquivalence = array_.getEquivalence(item) + return Equivalence.make((a, b) => arrayEquivalence(Array.from(a.values()), Array.from(b.values()))) +} + +const readonlySetParse = ( + decodeUnknown: ParseResult.DecodeUnknown, R> +): ParseResult.DeclarationDecodeUnknown, R> => +(u, options, ast) => + Predicate.isSet(u) ? + toComposite(decodeUnknown(Array.from(u.values()), options), (as) => new Set(as), ast, u) + : ParseResult.fail(new ParseResult.Type(ast, u)) + +/** + * @category api interface + * @since 3.10.0 + */ +export interface ReadonlySetFromSelf extends + AnnotableDeclare< + ReadonlySetFromSelf, + ReadonlySet>, + ReadonlySet>, + [Value] + > +{} + +const setFromSelf_ = (value: Value, description: string): ReadonlySetFromSelf => + declare( + [value], + { + decode: (item) => readonlySetParse(ParseResult.decodeUnknown(Array$(item))), + encode: (item) => readonlySetParse(ParseResult.encodeUnknown(Array$(item))) + }, + { + typeConstructor: { _tag: "ReadonlySet" }, + description, + pretty: readonlySetPretty, + arbitrary: setArbitrary, + equivalence: readonlySetEquivalence + } + ) + +/** + * @category ReadonlySet + * @since 3.10.0 + */ +export const ReadonlySetFromSelf = (value: Value): ReadonlySetFromSelf => + setFromSelf_(value, `ReadonlySet<${format(value)}>`) + +/** + * @category api interface + * @since 3.10.0 + */ +export interface SetFromSelf extends + AnnotableDeclare< + SetFromSelf, + Set>, + ReadonlySet>, + [Value] + > +{} + +/** + * @category Set + * @since 3.10.0 + */ +export const SetFromSelf = (value: Value): SetFromSelf => + setFromSelf_(value, `Set<${format(value)}>`) as any + +/** + * @category api interface + * @since 3.10.0 + */ +export interface ReadonlySet$ + extends transform, ReadonlySetFromSelf>>> +{} + +/** + * @category ReadonlySet transformations + * @since 3.10.0 + */ +export function ReadonlySet(value: Value): ReadonlySet$ { + return transform( + Array$(value), + ReadonlySetFromSelf(typeSchema(asSchema(value))), + { + strict: true, + decode: (i) => new Set(i), + encode: (a) => Array.from(a) + } + ) +} + +/** + * @category api interface + * @since 3.10.0 + */ +export interface Set$ + extends transform, SetFromSelf>>> +{} + +/** @ignore */ +function set(value: Value): Set$ { + return transform( + Array$(value), + SetFromSelf(typeSchema(asSchema(value))), + { + strict: true, + decode: (i) => new Set(i), + encode: (a) => Array.from(a) + } + ) +} + +export { + /** + * @category Set transformations + * @since 3.10.0 + */ + set as Set +} + +const bigDecimalPretty = (): pretty_.Pretty => (val) => + `BigDecimal(${bigDecimal_.format(bigDecimal_.normalize(val))})` + +const bigDecimalArbitrary = (): LazyArbitrary => (fc) => + fc.tuple(fc.bigInt(), fc.integer({ min: -18, max: 18 })) + .map(([value, scale]) => bigDecimal_.make(value, scale)) + +/** + * @category BigDecimal constructors + * @since 3.10.0 + */ +export class BigDecimalFromSelf extends declare( + bigDecimal_.isBigDecimal, + { + typeConstructor: { _tag: "effect/BigDecimal" }, + identifier: "BigDecimalFromSelf", + pretty: bigDecimalPretty, + arbitrary: bigDecimalArbitrary, + equivalence: () => bigDecimal_.Equivalence + } +) {} + +/** + * @category BigDecimal transformations + * @since 3.10.0 + */ +export class BigDecimal extends transformOrFail( + String$.annotations({ description: "a string to be decoded into a BigDecimal" }), + BigDecimalFromSelf, + { + strict: true, + decode: (i, _, ast) => + bigDecimal_.fromString(i).pipe(option_.match({ + onNone: () => + ParseResult.fail(new ParseResult.Type(ast, i, `Unable to decode ${JSON.stringify(i)} into a BigDecimal`)), + onSome: (val) => ParseResult.succeed(bigDecimal_.normalize(val)) + })), + encode: (a) => ParseResult.succeed(bigDecimal_.format(bigDecimal_.normalize(a))) + } +).annotations({ identifier: "BigDecimal" }) {} + +/** + * A schema that transforms a `number` into a `BigDecimal`. + * When encoding, this Schema will produce incorrect results if the BigDecimal exceeds the 64-bit range of a number. + * + * @category BigDecimal transformations + * @since 3.10.0 + */ +export class BigDecimalFromNumber extends transform( + Number$.annotations({ description: "a number to be decoded into a BigDecimal" }), + BigDecimalFromSelf, + { + strict: true, + decode: (i) => bigDecimal_.unsafeFromNumber(i), + encode: (a) => bigDecimal_.unsafeToNumber(a) + } +).annotations({ identifier: "BigDecimalFromNumber" }) {} + +/** + * @category schema id + * @since 3.10.0 + */ +export const GreaterThanBigDecimalSchemaId: unique symbol = Symbol.for("effect/SchemaId/GreaterThanBigDecimal") + +/** + * @category BigDecimal filters + * @since 3.10.0 + */ +export const greaterThanBigDecimal = + (min: bigDecimal_.BigDecimal, annotations?: Annotations.Filter>) => + (self: S & Schema, Schema.Context>): filter => { + const formatted = bigDecimal_.format(min) + return self.pipe( + filter((a) => bigDecimal_.greaterThan(a, min), { + schemaId: GreaterThanBigDecimalSchemaId, + [GreaterThanBigDecimalSchemaId]: { min }, + title: `greaterThanBigDecimal(${formatted})`, + description: `a BigDecimal greater than ${formatted}`, + ...annotations + }) + ) + } + +/** + * @category schema id + * @since 3.10.0 + */ +export const GreaterThanOrEqualToBigDecimalSchemaId: unique symbol = Symbol.for( + "effect/schema/GreaterThanOrEqualToBigDecimal" +) + +/** + * @category BigDecimal filters + * @since 3.10.0 + */ +export const greaterThanOrEqualToBigDecimal = + (min: bigDecimal_.BigDecimal, annotations?: Annotations.Filter>) => + (self: S & Schema, Schema.Context>): filter => { + const formatted = bigDecimal_.format(min) + return self.pipe( + filter((a) => bigDecimal_.greaterThanOrEqualTo(a, min), { + schemaId: GreaterThanOrEqualToBigDecimalSchemaId, + [GreaterThanOrEqualToBigDecimalSchemaId]: { min }, + title: `greaterThanOrEqualToBigDecimal(${formatted})`, + description: `a BigDecimal greater than or equal to ${formatted}`, + ...annotations + }) + ) + } + +/** + * @category schema id + * @since 3.10.0 + */ +export const LessThanBigDecimalSchemaId: unique symbol = Symbol.for("effect/SchemaId/LessThanBigDecimal") + +/** + * @category BigDecimal filters + * @since 3.10.0 + */ +export const lessThanBigDecimal = + (max: bigDecimal_.BigDecimal, annotations?: Annotations.Filter>) => + (self: S & Schema, Schema.Context>): filter => { + const formatted = bigDecimal_.format(max) + return self.pipe( + filter((a) => bigDecimal_.lessThan(a, max), { + schemaId: LessThanBigDecimalSchemaId, + [LessThanBigDecimalSchemaId]: { max }, + title: `lessThanBigDecimal(${formatted})`, + description: `a BigDecimal less than ${formatted}`, + ...annotations + }) + ) + } + +/** + * @category schema id + * @since 3.10.0 + */ +export const LessThanOrEqualToBigDecimalSchemaId: unique symbol = Symbol.for( + "effect/schema/LessThanOrEqualToBigDecimal" +) + +/** + * @category BigDecimal filters + * @since 3.10.0 + */ +export const lessThanOrEqualToBigDecimal = + (max: bigDecimal_.BigDecimal, annotations?: Annotations.Filter>) => + (self: S & Schema, Schema.Context>): filter => { + const formatted = bigDecimal_.format(max) + return self.pipe( + filter((a) => bigDecimal_.lessThanOrEqualTo(a, max), { + schemaId: LessThanOrEqualToBigDecimalSchemaId, + [LessThanOrEqualToBigDecimalSchemaId]: { max }, + title: `lessThanOrEqualToBigDecimal(${formatted})`, + description: `a BigDecimal less than or equal to ${formatted}`, + ...annotations + }) + ) + } + +/** + * @category schema id + * @since 3.10.0 + */ +export const PositiveBigDecimalSchemaId: unique symbol = Symbol.for( + "effect/schema/PositiveBigDecimal" +) + +/** + * @category BigDecimal filters + * @since 3.10.0 + */ +export const positiveBigDecimal = + (annotations?: Annotations.Filter>) => + (self: S & Schema, Schema.Context>): filter => + self.pipe( + filter((a) => bigDecimal_.isPositive(a), { + schemaId: PositiveBigDecimalSchemaId, + title: "positiveBigDecimal", + description: `a positive BigDecimal`, + ...annotations + }) + ) + +/** + * @category BigDecimal constructors + * @since 3.10.0 + */ +export const PositiveBigDecimalFromSelf: filter> = BigDecimalFromSelf.pipe( + positiveBigDecimal({ identifier: "PositiveBigDecimalFromSelf" }) +) + +/** + * @category schema id + * @since 3.10.0 + */ +export const NonNegativeBigDecimalSchemaId: unique symbol = Symbol.for( + "effect/schema/NonNegativeBigDecimal" +) + +/** + * @category BigDecimal filters + * @since 3.10.0 + */ +export const nonNegativeBigDecimal = + (annotations?: Annotations.Filter>) => + (self: S & Schema, Schema.Context>): filter => + self.pipe( + filter((a) => a.value >= 0n, { + schemaId: NonNegativeBigDecimalSchemaId, + title: "nonNegativeBigDecimal", + description: `a non-negative BigDecimal`, + ...annotations + }) + ) + +/** + * @category BigDecimal constructors + * @since 3.10.0 + */ +export const NonNegativeBigDecimalFromSelf: filter> = BigDecimalFromSelf.pipe( + nonNegativeBigDecimal({ identifier: "NonNegativeBigDecimalFromSelf" }) +) + +/** + * @category schema id + * @since 3.10.0 + */ +export const NegativeBigDecimalSchemaId: unique symbol = Symbol.for( + "effect/schema/NegativeBigDecimal" +) + +/** + * @category BigDecimal filters + * @since 3.10.0 + */ +export const negativeBigDecimal = + (annotations?: Annotations.Filter>) => + (self: S & Schema, Schema.Context>): filter => + self.pipe( + filter((a) => bigDecimal_.isNegative(a), { + schemaId: NegativeBigDecimalSchemaId, + title: "negativeBigDecimal", + description: `a negative BigDecimal`, + ...annotations + }) + ) + +/** + * @category BigDecimal constructors + * @since 3.10.0 + */ +export const NegativeBigDecimalFromSelf: filter> = BigDecimalFromSelf.pipe( + negativeBigDecimal({ identifier: "NegativeBigDecimalFromSelf" }) +) + +/** + * @category schema id + * @since 3.10.0 + */ +export const NonPositiveBigDecimalSchemaId: unique symbol = Symbol.for( + "effect/schema/NonPositiveBigDecimal" +) + +/** + * @category BigDecimal filters + * @since 3.10.0 + */ +export const nonPositiveBigDecimal = + (annotations?: Annotations.Filter>) => + (self: S & Schema, Schema.Context>): filter => + self.pipe( + filter((a) => a.value <= 0n, { + schemaId: NonPositiveBigDecimalSchemaId, + title: "nonPositiveBigDecimal", + description: `a non-positive BigDecimal`, + ...annotations + }) + ) + +/** + * @category BigDecimal constructors + * @since 3.10.0 + */ +export const NonPositiveBigDecimalFromSelf: filter> = BigDecimalFromSelf.pipe( + nonPositiveBigDecimal({ identifier: "NonPositiveBigDecimalFromSelf" }) +) + +/** + * @category schema id + * @since 3.10.0 + */ +export const BetweenBigDecimalSchemaId: unique symbol = Symbol.for("effect/SchemaId/BetweenBigDecimal") + +/** + * @category BigDecimal filters + * @since 3.10.0 + */ +export const betweenBigDecimal = ( + minimum: bigDecimal_.BigDecimal, + maximum: bigDecimal_.BigDecimal, + annotations?: Annotations.Filter> +) => +(self: S & Schema, Schema.Context>): filter => { + const formattedMinimum = bigDecimal_.format(minimum) + const formattedMaximum = bigDecimal_.format(maximum) + return self.pipe( + filter((a) => bigDecimal_.between(a, { minimum, maximum }), { + schemaId: BetweenBigDecimalSchemaId, + [BetweenBigDecimalSchemaId]: { maximum, minimum }, + title: `betweenBigDecimal(${formattedMinimum}, ${formattedMaximum})`, + description: `a BigDecimal between ${formattedMinimum} and ${formattedMaximum}`, + ...annotations + }) + ) +} + +/** + * Clamps a `BigDecimal` between a minimum and a maximum value. + * + * @category BigDecimal transformations + * @since 3.10.0 + */ +export const clampBigDecimal = + (minimum: bigDecimal_.BigDecimal, maximum: bigDecimal_.BigDecimal) => + ( + self: S & Schema, Schema.Context> + ): transform>> => + transform( + self, + self.pipe(typeSchema, betweenBigDecimal(minimum, maximum)), + { + strict: false, + decode: (i) => bigDecimal_.clamp(i, { minimum, maximum }), + encode: identity + } + ) + +const chunkArbitrary = + (item: LazyArbitrary, ctx: ArbitraryGenerationContext): LazyArbitrary> => (fc) => { + const items = fc.array(item(fc)) + return (ctx.depthIdentifier !== undefined ? fc.oneof(ctx, fc.constant([]), items) : items).map(chunk_.fromIterable) + } + +const chunkPretty = (item: pretty_.Pretty): pretty_.Pretty> => (c) => + `Chunk(${chunk_.toReadonlyArray(c).map(item).join(", ")})` + +const chunkParse = ( + decodeUnknown: ParseResult.DecodeUnknown, R> +): ParseResult.DeclarationDecodeUnknown, R> => +(u, options, ast) => + chunk_.isChunk(u) ? + chunk_.isEmpty(u) ? + ParseResult.succeed(chunk_.empty()) + : toComposite(decodeUnknown(chunk_.toReadonlyArray(u), options), chunk_.fromIterable, ast, u) + : ParseResult.fail(new ParseResult.Type(ast, u)) + +/** + * @category api interface + * @since 3.10.0 + */ +export interface ChunkFromSelf extends + AnnotableDeclare< + ChunkFromSelf, + chunk_.Chunk>, + chunk_.Chunk>, + [Value] + > +{} + +/** + * @category Chunk + * @since 3.10.0 + */ +export const ChunkFromSelf = (value: Value): ChunkFromSelf => { + return declare( + [value], + { + decode: (item) => chunkParse(ParseResult.decodeUnknown(Array$(item))), + encode: (item) => chunkParse(ParseResult.encodeUnknown(Array$(item))) + }, + { + typeConstructor: { _tag: "effect/Chunk" }, + description: `Chunk<${format(value)}>`, + pretty: chunkPretty, + arbitrary: chunkArbitrary, + equivalence: chunk_.getEquivalence + } + ) +} + +/** + * @category api interface + * @since 3.10.0 + */ +export interface Chunk + extends transform, ChunkFromSelf>>> +{} + +/** + * @category Chunk transformations + * @since 3.10.0 + */ +export function Chunk(value: Value): Chunk { + return transform( + Array$(value), + ChunkFromSelf(typeSchema(asSchema(value))), + { + strict: true, + decode: (i) => i.length === 0 ? chunk_.empty() : chunk_.fromIterable(i), + encode: (a) => chunk_.toReadonlyArray(a) + } + ) +} + +/** + * @category api interface + * @since 3.10.0 + */ +export interface NonEmptyChunkFromSelf extends + AnnotableDeclare< + NonEmptyChunkFromSelf, + chunk_.NonEmptyChunk>, + chunk_.NonEmptyChunk>, + [Value] + > +{} + +const nonEmptyChunkArbitrary = (item: LazyArbitrary): LazyArbitrary> => (fc) => + fastCheck_.array(item(fc), { minLength: 1 }).map((as) => chunk_.unsafeFromNonEmptyArray(as as any)) + +const nonEmptyChunkPretty = (item: pretty_.Pretty): pretty_.Pretty> => (c) => + `NonEmptyChunk(${chunk_.toReadonlyArray(c).map(item).join(", ")})` + +const nonEmptyChunkParse = ( + decodeUnknown: ParseResult.DecodeUnknown, R> +): ParseResult.DeclarationDecodeUnknown, R> => +(u, options, ast) => + chunk_.isChunk(u) && chunk_.isNonEmpty(u) + ? toComposite(decodeUnknown(chunk_.toReadonlyArray(u), options), chunk_.unsafeFromNonEmptyArray, ast, u) + : ParseResult.fail(new ParseResult.Type(ast, u)) + +/** + * @category Chunk + * @since 3.10.0 + */ +export const NonEmptyChunkFromSelf = (value: Value): NonEmptyChunkFromSelf => { + return declare( + [value], + { + decode: (item) => nonEmptyChunkParse(ParseResult.decodeUnknown(NonEmptyArray(item))), + encode: (item) => nonEmptyChunkParse(ParseResult.encodeUnknown(NonEmptyArray(item))) + }, + { + typeConstructor: { _tag: "effect/Chunk.NonEmptyChunk" }, + description: `NonEmptyChunk<${format(value)}>`, + pretty: nonEmptyChunkPretty, + arbitrary: nonEmptyChunkArbitrary, + equivalence: chunk_.getEquivalence + } + ) +} + +/** + * @category api interface + * @since 3.10.0 + */ +export interface NonEmptyChunk + extends transform, NonEmptyChunkFromSelf>>> +{} + +/** + * @category Chunk transformations + * @since 3.10.0 + */ +export function NonEmptyChunk(value: Value): NonEmptyChunk { + return transform( + NonEmptyArray(value), + NonEmptyChunkFromSelf(typeSchema(asSchema(value))), + { + strict: true, + decode: (i) => chunk_.unsafeFromNonEmptyArray(i), + encode: (a) => chunk_.toReadonlyArray(a) + } + ) +} + +const decodeData = > | ReadonlyArray>(a: A): A => + Array.isArray(a) ? data_.array(a) : data_.struct(a) + +const dataArbitrary = > | ReadonlyArray>( + item: LazyArbitrary +): LazyArbitrary => +(fc) => item(fc).map(decodeData) + +const dataPretty = > | ReadonlyArray>( + item: pretty_.Pretty +): pretty_.Pretty => +(d) => `Data(${item(d)})` + +const dataParse = > | ReadonlyArray>( + decodeUnknown: ParseResult.DecodeUnknown +): ParseResult.DeclarationDecodeUnknown => +(u, options, ast) => + Equal.isEqual(u) ? + toComposite(decodeUnknown(u, options), decodeData, ast, u) + : ParseResult.fail(new ParseResult.Type(ast, u)) + +/** + * @category api interface + * @since 3.13.3 + */ +export interface DataFromSelf extends + AnnotableDeclare< + DataFromSelf, + Schema.Type, + Schema.Encoded, + [Value] + > +{} + +/** + * Type and Encoded must extend `Readonly> | + * ReadonlyArray` to be compatible with this API. + * + * @category Data transformations + * @since 3.10.0 + */ +export const DataFromSelf = < + S extends Schema.Any, + A extends Readonly> | ReadonlyArray, + I extends Readonly> | ReadonlyArray +>(value: S & Schema, I & Schema.Encoded, Schema.Context>): DataFromSelf => { + return declare( + [value], + { + decode: (item) => dataParse(ParseResult.decodeUnknown(item)), + encode: (item) => dataParse(ParseResult.encodeUnknown(item)) + }, + { + description: `Data<${format(value)}>`, + pretty: dataPretty, + arbitrary: dataArbitrary + } + ) +} + +/** + * @category api interface + * @since 3.13.3 + */ +export interface Data + extends transform>>> +{} + +/** + * Type and Encoded must extend `Readonly> | + * ReadonlyArray` to be compatible with this API. + * + * @category Data transformations + * @since 3.10.0 + */ +export const Data = < + S extends Schema.Any, + A extends Readonly> | ReadonlyArray, + I extends Readonly> | ReadonlyArray +>(value: S & Schema, I & Schema.Encoded, Schema.Context>): Data => { + return transform( + value, + DataFromSelf(typeSchema(value)), + { + strict: false, + decode: (i) => decodeData(i), + encode: (a) => Array.isArray(a) ? Array.from(a) : Object.assign({}, a) + } + ) +} + +type MissingSelfGeneric = + `Missing \`Self\` generic - use \`class Self extends ${Usage}()(${Params}{ ... })\`` + +type RequiredKeys = { + [K in keyof T]-?: {} extends Pick ? never : K +}[keyof T] + +type ClassAnnotations = + | Annotations.Schema + | readonly [ + // Annotations for the "to" schema + Annotations.Schema | undefined, + // Annotations for the "transformation schema + (Annotations.Schema | undefined)?, + // Annotations for the "from" schema + Annotations.Schema? + ] + +/** + * @category api interface + * @since 3.10.0 + */ +export interface Class + extends Schema, R> +{ + new( + props: RequiredKeys extends never ? void | Simplify : Simplify, + options?: MakeOptions + ): Struct.Type & Inherited & Proto + + /** @since 3.10.0 */ + readonly ast: AST.Transformation + + make) => any>(this: C, ...args: ConstructorParameters): InstanceType + + annotations(annotations: Annotations.Schema): SchemaClass, R> + + readonly fields: { readonly [K in keyof Fields]: Fields[K] } + + readonly identifier: string + + /** + * @example + * ```ts + * import { Schema } from "effect" + * + * class MyClass extends Schema.Class("MyClass")({ + * myField: Schema.String + * }) { + * myMethod() { + * return this.myField + "my" + * } + * } + * + * class NextClass extends MyClass.extend("NextClass")({ + * nextField: Schema.Number + * }) { + * nextMethod() { + * return this.myMethod() + this.myField + this.nextField + * } + * } + * ``` + */ + extend(identifier: string): ( + fields: NewFields | HasFields, + annotations?: ClassAnnotations>> + ) => [Extended] extends [never] ? MissingSelfGeneric<"Base.extend"> + : Class< + Extended, + Fields & NewFields, + I & Struct.Encoded, + R | Struct.Context, + C & Struct.Constructor, + Self, + Proto + > + + /** + * @example + * ```ts + * import { Effect, Schema } from "effect" + * + * class MyClass extends Schema.Class("MyClass")({ + * myField: Schema.String + * }) { + * myMethod() { + * return this.myField + "my" + * } + * } + * + * class NextClass extends MyClass.transformOrFail("NextClass")({ + * nextField: Schema.Number + * }, { + * decode: (i) => + * Effect.succeed({ + * myField: i.myField, + * nextField: i.myField.length + * }), + * encode: (a) => Effect.succeed({ myField: a.myField }) + * }) { + * nextMethod() { + * return this.myMethod() + this.myField + this.nextField + * } + * } + * ``` + */ + transformOrFail(identifier: string): < + NewFields extends Struct.Fields, + R2, + R3 + >( + fields: NewFields, + options: { + readonly decode: ( + input: Simplify>, + options: ParseOptions, + ast: AST.Transformation + ) => Effect.Effect>, ParseResult.ParseIssue, R2> + readonly encode: ( + input: Simplify>, + options: ParseOptions, + ast: AST.Transformation + ) => Effect.Effect, ParseResult.ParseIssue, R3> + }, + annotations?: ClassAnnotations>> + ) => [Transformed] extends [never] ? MissingSelfGeneric<"Base.transformOrFail"> + : Class< + Transformed, + Fields & NewFields, + I, + R | Struct.Context | R2 | R3, + C & Struct.Constructor, + Self, + Proto + > + + /** + * @example + * ```ts + * import { Effect, Schema } from "effect" + * + * class MyClass extends Schema.Class("MyClass")({ + * myField: Schema.String + * }) { + * myMethod() { + * return this.myField + "my" + * } + * } + * + * class NextClass extends MyClass.transformOrFailFrom("NextClass")({ + * nextField: Schema.Number + * }, { + * decode: (i) => + * Effect.succeed({ + * myField: i.myField, + * nextField: i.myField.length + * }), + * encode: (a) => Effect.succeed({ myField: a.myField }) + * }) { + * nextMethod() { + * return this.myMethod() + this.myField + this.nextField + * } + * } + * ``` + */ + transformOrFailFrom(identifier: string): < + NewFields extends Struct.Fields, + R2, + R3 + >( + fields: NewFields, + options: { + readonly decode: ( + input: Simplify, + options: ParseOptions, + ast: AST.Transformation + ) => Effect.Effect>, ParseResult.ParseIssue, R2> + readonly encode: ( + input: Simplify>, + options: ParseOptions, + ast: AST.Transformation + ) => Effect.Effect + }, + annotations?: ClassAnnotations>> + ) => [Transformed] extends [never] ? MissingSelfGeneric<"Base.transformOrFailFrom"> + : Class< + Transformed, + Fields & NewFields, + I, + R | Struct.Context | R2 | R3, + C & Struct.Constructor, + Self, + Proto + > +} + +type HasFields = Struct | { + readonly [RefineSchemaId]: HasFields +} + +const isField = (u: unknown) => isSchema(u) || isPropertySignature(u) + +const isFields = (fields: object): fields is Fields => + Reflect.ownKeys(fields).every((key) => isField((fields as any)[key])) + +const getFields = (hasFields: HasFields): Fields => + "fields" in hasFields ? hasFields.fields : getFields(hasFields[RefineSchemaId]) + +const getSchemaFromFieldsOr = (fieldsOr: Fields | HasFields): Schema.Any => + isFields(fieldsOr) ? Struct(fieldsOr) : isSchema(fieldsOr) ? fieldsOr : Struct(getFields(fieldsOr)) + +const getFieldsFromFieldsOr = (fieldsOr: Fields | HasFields): Fields => + isFields(fieldsOr) ? fieldsOr : getFields(fieldsOr) + +/** + * @example + * ```ts + * import { Schema } from "effect" + * + * class MyClass extends Schema.Class("MyClass")({ + * someField: Schema.String + * }) { + * someMethod() { + * return this.someField + "bar" + * } + * } + * ``` + * + * @category classes + * @since 3.10.0 + */ +export const Class = (identifier: string) => +( + fieldsOr: Fields | HasFields, + annotations?: ClassAnnotations>> +): [Self] extends [never] ? MissingSelfGeneric<"Class"> + : Class< + Self, + Fields, + Struct.Encoded, + Struct.Context, + Struct.Constructor, + {}, + {} + > => + makeClass({ + kind: "Class", + identifier, + schema: getSchemaFromFieldsOr(fieldsOr), + fields: getFieldsFromFieldsOr(fieldsOr), + Base: data_.Class, + annotations + }) + +/** @internal */ +export const getClassTag = (tag: Tag) => + withConstructorDefault(propertySignature(Literal(tag)), () => tag) + +/** + * @category api interface + * @since 3.10.0 + */ +export interface TaggedClass extends + Class< + Self, + Fields, + Struct.Encoded, + Struct.Context, + Struct.Constructor>, + {}, + {} + > +{ + readonly _tag: Tag +} + +/** + * @example + * ```ts + * import { Schema } from "effect" + * + * class MyClass extends Schema.TaggedClass("MyClass")("MyClass", { + * a: Schema.String + * }) {} + * ``` + * + * @category classes + * @since 3.10.0 + */ +export const TaggedClass = (identifier?: string) => +( + tag: Tag, + fieldsOr: Fields | HasFields, + annotations?: ClassAnnotations } & Fields>>> +): [Self] extends [never] ? MissingSelfGeneric<"TaggedClass", `"Tag", `> + : TaggedClass } & Fields> => +{ + const fields = getFieldsFromFieldsOr(fieldsOr) + const schema = getSchemaFromFieldsOr(fieldsOr) + const newFields = { _tag: getClassTag(tag) } + const taggedFields = extendFields(newFields, fields) + return class TaggedClass extends makeClass({ + kind: "TaggedClass", + identifier: identifier ?? tag, + schema: extend(schema, Struct(newFields)), + fields: taggedFields, + Base: data_.Class, + annotations + }) { + static _tag = tag + } as any +} + +/** + * @category api interface + * @since 3.10.0 + */ +export interface TaggedErrorClass extends + Class< + Self, + Fields, + Struct.Encoded, + Struct.Context, + Struct.Constructor>, + {}, + cause_.YieldableError + > +{ + readonly _tag: Tag +} + +/** + * @example + * ```ts + * import { Schema } from "effect" + * + * class MyError extends Schema.TaggedError("MyError")( + * "MyError", + * { + * module: Schema.String, + * method: Schema.String, + * description: Schema.String + * } + * ) { + * get message(): string { + * return `${this.module}.${this.method}: ${this.description}` + * } + * } + * ``` + * @category classes + * @since 3.10.0 + */ +export const TaggedError = (identifier?: string) => +( + tag: Tag, + fieldsOr: Fields | HasFields, + annotations?: ClassAnnotations } & Fields>>> +): [Self] extends [never] ? MissingSelfGeneric<"TaggedError", `"Tag", `> + : TaggedErrorClass< + Self, + Tag, + { readonly _tag: tag } & Fields + > => +{ + class Base extends data_.Error {} + ;(Base.prototype as any).name = tag + const fields = getFieldsFromFieldsOr(fieldsOr) + const schema = getSchemaFromFieldsOr(fieldsOr) + const newFields = { _tag: getClassTag(tag) } + const taggedFields = extendFields(newFields, fields) + const hasMessageField = "message" in taggedFields + class TaggedErrorClass extends makeClass({ + kind: "TaggedError", + identifier: identifier ?? tag, + schema: extend(schema, Struct(newFields)), + fields: taggedFields, + Base, + annotations, + disableToString: true + }) { + static _tag = tag + } + + if (!hasMessageField) { + Object.defineProperty(TaggedErrorClass.prototype, "message", { + get() { + return `{ ${ + Reflect.ownKeys(fields) + .map((p: any) => `${Inspectable.formatPropertyKey(p)}: ${Inspectable.formatUnknown((this)[p])}`) + .join(", ") + } }` + }, + enumerable: false, // mirrors the built-in Error.prototype.message, whose descriptor is also non-enumerable + configurable: true + }) + } + + return TaggedErrorClass as any +} + +const extendFields = (a: Struct.Fields, b: Struct.Fields): Struct.Fields => { + const out = { ...a } + for (const key of Reflect.ownKeys(b)) { + if (key in a) { + throw new Error(errors_.getASTDuplicatePropertySignatureErrorMessage(key)) + } + out[key] = b[key] + } + return out +} + +/** + * @category Constructor utils + * @since 3.13.4 + */ +export type MakeOptions = boolean | { + readonly disableValidation?: boolean | undefined +} + +function getDisableValidationMakeOption(options: MakeOptions | undefined): boolean { + return Predicate.isBoolean(options) ? options : options?.disableValidation ?? false +} + +const astCache = globalValue("effect/Schema/astCache", () => new WeakMap()) + +const getClassAnnotations = ( + annotations: ClassAnnotations | undefined +): [Annotations.Schema?, Annotations.Schema?, Annotations.Schema?] => { + if (annotations === undefined) { + return [] + } else if (Array.isArray(annotations)) { + return annotations as any + } else { + return [annotations] as any + } +} + +const makeClass = ( + { Base, annotations, disableToString, fields, identifier, kind, schema }: { + kind: "Class" | "TaggedClass" | "TaggedError" | "TaggedRequest" + identifier: string + schema: Schema.Any + fields: Fields + Base: new(...args: ReadonlyArray) => any + annotations?: ClassAnnotations | undefined + disableToString?: boolean | undefined + } +): any => { + const classSymbol = Symbol.for(`effect/Schema/${kind}/${identifier}`) + + const [typeAnnotations, transformationAnnotations, encodedAnnotations] = getClassAnnotations(annotations) + + const typeSchema_ = typeSchema(schema) + + const declarationSurrogate = typeSchema_.annotations({ + identifier, + ...typeAnnotations + }) + + const typeSide = typeSchema_.annotations({ + [AST.AutoTitleAnnotationId]: `${identifier} (Type side)`, + ...typeAnnotations + }) + + const constructorSchema = schema.annotations({ + [AST.AutoTitleAnnotationId]: `${identifier} (Constructor)`, + ...typeAnnotations + }) + + const encodedSide = schema.annotations({ + [AST.AutoTitleAnnotationId]: `${identifier} (Encoded side)`, + ...encodedAnnotations + }) + + const transformationSurrogate = schema.annotations({ + ...encodedAnnotations, + ...typeAnnotations, + ...transformationAnnotations + }) + + const fallbackInstanceOf = (u: unknown) => Predicate.hasProperty(u, classSymbol) && ParseResult.is(typeSide)(u) + + const klass = class extends Base { + constructor( + props: { [x: string | symbol]: unknown } = {}, + options: MakeOptions = false + ) { + props = { ...props } + if (kind !== "Class") { + delete props["_tag"] + } + props = lazilyMergeDefaults(fields, props) + if (!getDisableValidationMakeOption(options)) { + props = ParseResult.validateSync(constructorSchema)(props) + } + super(props, true) + } + + // ---------------- + // Schema interface + // ---------------- + + static [TypeId] = variance + + static get ast(): AST.AST { + let out = astCache.get(this) + if (out) { + return out + } + + const declaration: Schema.Any = declare( + [schema], + { + decode: () => (input, _, ast) => + input instanceof this || fallbackInstanceOf(input) + ? ParseResult.succeed(input) + : ParseResult.fail(new ParseResult.Type(ast, input)), + encode: () => (input, options) => + input instanceof this + ? ParseResult.succeed(input) + : ParseResult.map( + ParseResult.encodeUnknown(typeSide)(input, options), + (props) => new this(props, true) + ) + }, + { + identifier, + pretty: (pretty) => (self: any) => `${identifier}(${pretty(self)})`, + // @ts-expect-error + arbitrary: (arb) => (fc) => arb(fc).map((props) => new this(props)), + equivalence: identity, + [AST.SurrogateAnnotationId]: declarationSurrogate.ast, + ...typeAnnotations + } + ) + + out = transform( + encodedSide, + declaration, + { + strict: true, + decode: (i) => new this(i, true), + encode: identity + } + ).annotations({ + [AST.SurrogateAnnotationId]: transformationSurrogate.ast, + ...transformationAnnotations + }).ast + + astCache.set(this, out) + + return out + } + + static pipe() { + return pipeArguments(this, arguments) + } + + static annotations(annotations: Annotations.Schema) { + return make(this.ast).annotations(annotations) + } + + static toString() { + return `(${String(encodedSide)} <-> ${identifier})` + } + + // ---------------- + // Class interface + // ---------------- + + static make(...args: Array) { + return new this(...args) + } + + static fields = { ...fields } + + static identifier = identifier + + static extend(identifier: string) { + return ( + newFieldsOr: NewFields | HasFields, + annotations?: ClassAnnotations>> + ) => { + const newFields = getFieldsFromFieldsOr(newFieldsOr) + const newSchema = getSchemaFromFieldsOr(newFieldsOr) + const extendedFields = extendFields(fields, newFields) + return makeClass({ + kind, + identifier, + schema: extend(schema, newSchema), + fields: extendedFields, + Base: this, + annotations + }) + } + } + + static transformOrFail(identifier: string) { + return ( + newFieldsOr: NewFields, + options: any, + annotations?: ClassAnnotations>> + ) => { + const transformedFields: Struct.Fields = extendFields(fields, newFieldsOr) + return makeClass({ + kind, + identifier, + schema: transformOrFail( + schema, + typeSchema(Struct(transformedFields)), + options + ), + fields: transformedFields, + Base: this, + annotations + }) + } + } + + static transformOrFailFrom(identifier: string) { + return ( + newFields: NewFields, + options: any, + annotations?: ClassAnnotations>> + ) => { + const transformedFields: Struct.Fields = extendFields(fields, newFields) + return makeClass({ + kind, + identifier, + schema: transformOrFail( + encodedSchema(schema), + Struct(transformedFields), + options + ), + fields: transformedFields, + Base: this, + annotations + }) + } + } + + // ---------------- + // other + // ---------------- + + get [classSymbol]() { + return classSymbol + } + } + if (disableToString !== true) { + Object.defineProperty(klass.prototype, "toString", { + value() { + return `${identifier}({ ${ + Reflect.ownKeys(fields).map((p: any) => + `${Inspectable.formatPropertyKey(p)}: ${Inspectable.formatUnknown(this[p])}` + ) + .join(", ") + } })` + }, + configurable: true, + writable: true + }) + } + return klass +} + +/** + * @category FiberId + * @since 3.10.0 + */ +export type FiberIdEncoded = + | { + readonly _tag: "Composite" + readonly left: FiberIdEncoded + readonly right: FiberIdEncoded + } + | { + readonly _tag: "None" + } + | { + readonly _tag: "Runtime" + readonly id: number + readonly startTimeMillis: number + } + +const FiberIdNoneEncoded = Struct({ + _tag: Literal("None") +}).annotations({ identifier: "FiberIdNoneEncoded" }) + +const FiberIdRuntimeEncoded = Struct({ + _tag: Literal("Runtime"), + id: Int, + startTimeMillis: Int +}).annotations({ identifier: "FiberIdRuntimeEncoded" }) + +const FiberIdCompositeEncoded = Struct({ + _tag: Literal("Composite"), + left: suspend(() => FiberIdEncoded), + right: suspend(() => FiberIdEncoded) +}).annotations({ identifier: "FiberIdCompositeEncoded" }) + +const FiberIdEncoded: Schema = Union( + FiberIdNoneEncoded, + FiberIdRuntimeEncoded, + FiberIdCompositeEncoded +).annotations({ identifier: "FiberIdEncoded" }) + +const fiberIdArbitrary: LazyArbitrary = (fc) => + fc.letrec((tie) => ({ + None: fc.record({ _tag: fc.constant("None" as const) }), + Runtime: fc.record({ _tag: fc.constant("Runtime" as const), id: fc.integer(), startTimeMillis: fc.integer() }), + Composite: fc.record({ _tag: fc.constant("Composite" as const), left: tie("FiberId"), right: tie("FiberId") }), + FiberId: fc.oneof(tie("None"), tie("Runtime"), tie("Composite")) as any as fastCheck_.Arbitrary + })).FiberId.map(fiberIdDecode) + +const fiberIdPretty: pretty_.Pretty = (fiberId) => { + switch (fiberId._tag) { + case "None": + return "FiberId.none" + case "Runtime": + return `FiberId.runtime(${fiberId.id}, ${fiberId.startTimeMillis})` + case "Composite": + return `FiberId.composite(${fiberIdPretty(fiberId.right)}, ${fiberIdPretty(fiberId.left)})` + } +} + +/** + * @category FiberId constructors + * @since 3.10.0 + */ +export class FiberIdFromSelf extends declare( + fiberId_.isFiberId, + { + typeConstructor: { _tag: "effect/FiberId" }, + identifier: "FiberIdFromSelf", + pretty: () => fiberIdPretty, + arbitrary: () => fiberIdArbitrary + } +) {} + +const fiberIdDecode = (input: FiberIdEncoded): fiberId_.FiberId => { + switch (input._tag) { + case "None": + return fiberId_.none + case "Runtime": + return fiberId_.runtime(input.id, input.startTimeMillis) + case "Composite": + return fiberId_.composite(fiberIdDecode(input.left), fiberIdDecode(input.right)) + } +} + +const fiberIdEncode = (input: fiberId_.FiberId): FiberIdEncoded => { + switch (input._tag) { + case "None": + return { _tag: "None" } + case "Runtime": + return { _tag: "Runtime", id: input.id, startTimeMillis: input.startTimeMillis } + case "Composite": + return { + _tag: "Composite", + left: fiberIdEncode(input.left), + right: fiberIdEncode(input.right) + } + } +} + +/** + * @category FiberId transformations + * @since 3.10.0 + */ +export class FiberId extends transform( + FiberIdEncoded, + FiberIdFromSelf, + { + strict: true, + decode: (i) => fiberIdDecode(i), + encode: (a) => fiberIdEncode(a) + } +).annotations({ identifier: "FiberId" }) {} + +/** + * @category Cause utils + * @since 3.10.0 + */ +export type CauseEncoded = + | { + readonly _tag: "Empty" + } + | { + readonly _tag: "Fail" + readonly error: E + } + | { + readonly _tag: "Die" + readonly defect: D + } + | { + readonly _tag: "Interrupt" + readonly fiberId: FiberIdEncoded + } + | { + readonly _tag: "Sequential" + readonly left: CauseEncoded + readonly right: CauseEncoded + } + | { + readonly _tag: "Parallel" + readonly left: CauseEncoded + readonly right: CauseEncoded + } + +const causeDieEncoded = (defect: Defect) => + Struct({ + _tag: Literal("Die"), + defect + }) + +const CauseEmptyEncoded = Struct({ + _tag: Literal("Empty") +}) + +const causeFailEncoded = (error: E) => + Struct({ + _tag: Literal("Fail"), + error + }) + +const CauseInterruptEncoded = Struct({ + _tag: Literal("Interrupt"), + fiberId: FiberIdEncoded +}) + +let causeEncodedId = 0 + +const causeEncoded = ( + error: E, + defect: D +): SchemaClass< + CauseEncoded, Schema.Type>, + CauseEncoded, Schema.Encoded>, + Schema.Context | Schema.Context +> => { + const error_ = asSchema(error) + const defect_ = asSchema(defect) + const suspended = suspend((): Schema< + CauseEncoded, Schema.Type>, + CauseEncoded, Schema.Encoded>, + Schema.Context | Schema.Context + > => out) + const out = Union( + CauseEmptyEncoded, + causeFailEncoded(error_), + causeDieEncoded(defect_), + CauseInterruptEncoded, + Struct({ + _tag: Literal("Sequential"), + left: suspended, + right: suspended + }), + Struct({ + _tag: Literal("Parallel"), + left: suspended, + right: suspended + }) + ).annotations({ + title: `CauseEncoded<${format(error)}>`, + [AST.JSONIdentifierAnnotationId]: `CauseEncoded${causeEncodedId++}` + }) + return out +} + +const causeArbitrary = ( + error: LazyArbitrary, + defect: LazyArbitrary +): LazyArbitrary> => +(fc) => + fc.letrec((tie) => ({ + Empty: fc.record({ _tag: fc.constant("Empty" as const) }), + Fail: fc.record({ _tag: fc.constant("Fail" as const), error: error(fc) }), + Die: fc.record({ _tag: fc.constant("Die" as const), defect: defect(fc) }), + Interrupt: fc.record({ _tag: fc.constant("Interrupt" as const), fiberId: fiberIdArbitrary(fc) }), + Sequential: fc.record({ _tag: fc.constant("Sequential" as const), left: tie("Cause"), right: tie("Cause") }), + Parallel: fc.record({ _tag: fc.constant("Parallel" as const), left: tie("Cause"), right: tie("Cause") }), + Cause: fc.oneof( + tie("Empty"), + tie("Fail"), + tie("Die"), + tie("Interrupt"), + tie("Sequential"), + tie("Parallel") + ) as any as fastCheck_.Arbitrary> + })).Cause.map(causeDecode) + +const causePretty = (error: pretty_.Pretty): pretty_.Pretty> => (cause) => { + const f = (cause: cause_.Cause): string => { + switch (cause._tag) { + case "Empty": + return "Cause.empty" + case "Fail": + return `Cause.fail(${error(cause.error)})` + case "Die": + return `Cause.die(${cause_.pretty(cause)})` + case "Interrupt": + return `Cause.interrupt(${fiberIdPretty(cause.fiberId)})` + case "Sequential": + return `Cause.sequential(${f(cause.left)}, ${f(cause.right)})` + case "Parallel": + return `Cause.parallel(${f(cause.left)}, ${f(cause.right)})` + } + } + return f(cause) +} + +const causeParse = ( + decodeUnknown: ParseResult.DecodeUnknown, R> +): ParseResult.DeclarationDecodeUnknown, R> => +(u, options, ast) => + cause_.isCause(u) ? + toComposite(decodeUnknown(causeEncode(u), options), causeDecode, ast, u) + : ParseResult.fail(new ParseResult.Type(ast, u)) + +/** + * @category api interface + * @since 3.10.0 + */ +export interface CauseFromSelf extends + AnnotableDeclare< + CauseFromSelf, + cause_.Cause>, + cause_.Cause>, + [E, D] + > +{} + +/** + * @category Cause transformations + * @since 3.10.0 + */ +export const CauseFromSelf = ({ defect, error }: { + readonly error: E + readonly defect: D +}): CauseFromSelf => { + return declare( + [error, defect], + { + decode: (error, defect) => causeParse(ParseResult.decodeUnknown(causeEncoded(error, defect))), + encode: (error, defect) => causeParse(ParseResult.encodeUnknown(causeEncoded(error, defect))) + }, + { + typeConstructor: { _tag: "effect/Cause" }, + title: `Cause<${error.ast}>`, + pretty: causePretty, + arbitrary: causeArbitrary + } + ) +} + +function causeDecode(cause: CauseEncoded): cause_.Cause { + switch (cause._tag) { + case "Empty": + return cause_.empty + case "Fail": + return cause_.fail(cause.error) + case "Die": + return cause_.die(cause.defect) + case "Interrupt": + return cause_.interrupt(fiberIdDecode(cause.fiberId)) + case "Sequential": + return cause_.sequential(causeDecode(cause.left), causeDecode(cause.right)) + case "Parallel": + return cause_.parallel(causeDecode(cause.left), causeDecode(cause.right)) + } +} + +function causeEncode(cause: cause_.Cause): CauseEncoded { + switch (cause._tag) { + case "Empty": + return { _tag: "Empty" } + case "Fail": + return { _tag: "Fail", error: cause.error } + case "Die": + return { _tag: "Die", defect: cause.defect } + case "Interrupt": + return { _tag: "Interrupt", fiberId: cause.fiberId } + case "Sequential": + return { + _tag: "Sequential", + left: causeEncode(cause.left), + right: causeEncode(cause.right) + } + case "Parallel": + return { + _tag: "Parallel", + left: causeEncode(cause.left), + right: causeEncode(cause.right) + } + } +} + +/** + * @category api interface + * @since 3.10.0 + */ +export interface Cause extends + transform< + SchemaClass< + CauseEncoded, Schema.Type>, + CauseEncoded, Schema.Encoded>, + Schema.Context | Schema.Context + >, + CauseFromSelf>, SchemaClass>> + > +{} + +/** + * @category Cause transformations + * @since 3.10.0 + */ +export const Cause = ({ defect, error }: { + readonly error: E + readonly defect: D +}): Cause => { + const error_ = asSchema(error) + const defect_ = asSchema(defect) + const out = transform( + causeEncoded(error_, defect_), + CauseFromSelf({ error: typeSchema(error_), defect: typeSchema(defect_) }), + { + strict: false, + decode: (i) => causeDecode(i), + encode: (a) => causeEncode(a) + } + ) + return out as any +} + +/** + * Defines a schema for handling JavaScript errors (`Error` instances) and other types of defects. + * It decodes objects into Error instances if they match the expected structure (i.e., have a `message` and optionally a `name` and `stack`), + * or converts other values to their string representations. + * + * When encoding, it converts `Error` instances back into plain objects containing only the error's name and message, + * or other values into their string forms. + * + * This is useful for serializing and deserializing errors across network boundaries where error objects do not natively serialize. + * + * @category defect + * @since 3.10.0 + */ +export class Defect extends transform( + Unknown, + Unknown, + { + strict: true, + decode: (i) => { + if (Predicate.isObject(i) && "message" in i && typeof i.message === "string") { + const err = new Error(i.message, { cause: i }) + if ("name" in i && typeof i.name === "string") { + err.name = i.name + } + err.stack = "stack" in i && typeof i.stack === "string" ? i.stack : "" + return err + } + return internalCause_.prettyErrorMessage(i) + }, + encode: (a) => { + if (a instanceof Error) { + return { + name: a.name, + message: a.message + // no stack because of security reasons + } + } + return internalCause_.prettyErrorMessage(a) + } + } +).annotations({ identifier: "Defect" }) {} + +/** + * @category Exit utils + * @since 3.10.0 + */ +export type ExitEncoded = + | { + readonly _tag: "Failure" + readonly cause: CauseEncoded + } + | { + readonly _tag: "Success" + readonly value: A + } + +const exitFailureEncoded = ( + error: E, + defect: D +) => + Struct({ + _tag: Literal("Failure"), + cause: causeEncoded(error, defect) + }) + +const exitSuccessEncoded = ( + value: A +) => + Struct({ + _tag: Literal("Success"), + value + }) + +const exitEncoded = ( + value: A, + error: E, + defect: D +) => { + return Union( + exitFailureEncoded(error, defect), + exitSuccessEncoded(value) + ).annotations({ + title: `ExitEncoded<${format(value)}, ${format(error)}, ${format(defect)}>` + }) +} + +const exitDecode = (input: ExitEncoded): exit_.Exit => { + switch (input._tag) { + case "Failure": + return exit_.failCause(causeDecode(input.cause)) + case "Success": + return exit_.succeed(input.value) + } +} + +const exitArbitrary = ( + value: LazyArbitrary, + error: LazyArbitrary, + defect: LazyArbitrary +): LazyArbitrary> => +(fc) => + fc.oneof( + fc.record({ _tag: fc.constant("Failure" as const), cause: causeArbitrary(error, defect)(fc) }), + fc.record({ _tag: fc.constant("Success" as const), value: value(fc) }) + ).map(exitDecode) + +const exitPretty = + (value: pretty_.Pretty, error: pretty_.Pretty): pretty_.Pretty> => (exit) => + exit._tag === "Failure" + ? `Exit.failCause(${causePretty(error)(exit.cause)})` + : `Exit.succeed(${value(exit.value)})` + +const exitParse = ( + decodeUnknownValue: ParseResult.DecodeUnknown, + decodeUnknownCause: ParseResult.DecodeUnknown, ER> +): ParseResult.DeclarationDecodeUnknown, ER | R> => +(u, options, ast) => + exit_.isExit(u) ? + exit_.match(u, { + onFailure: (cause) => toComposite(decodeUnknownCause(cause, options), exit_.failCause, ast, u), + onSuccess: (value) => toComposite(decodeUnknownValue(value, options), exit_.succeed, ast, u) + }) + : ParseResult.fail(new ParseResult.Type(ast, u)) + +/** + * @category api interface + * @since 3.10.0 + */ +export interface ExitFromSelf + extends + AnnotableDeclare< + ExitFromSelf, + exit_.Exit, Schema.Type>, + exit_.Exit, Schema.Encoded>, + [A, E, D] + > +{} + +/** + * @category Exit transformations + * @since 3.10.0 + */ +export const ExitFromSelf = ( + { defect, failure, success }: { + readonly failure: E + readonly success: A + readonly defect: D + } +): ExitFromSelf => + declare( + [success, failure, defect], + { + decode: (success, failure, defect) => + exitParse( + ParseResult.decodeUnknown(success), + ParseResult.decodeUnknown(CauseFromSelf({ error: failure, defect })) + ), + encode: (success, failure, defect) => + exitParse( + ParseResult.encodeUnknown(success), + ParseResult.encodeUnknown(CauseFromSelf({ error: failure, defect })) + ) + }, + { + typeConstructor: { _tag: "effect/Exit" }, + title: `Exit<${success.ast}, ${failure.ast}>`, + pretty: exitPretty, + arbitrary: exitArbitrary + } + ) + +/** + * @category api interface + * @since 3.10.0 + */ +export interface Exit extends + transform< + Union<[ + Struct<{ + _tag: Literal<["Failure"]> + cause: SchemaClass< + CauseEncoded, Schema.Type>, + CauseEncoded, Schema.Encoded>, + Schema.Context | Schema.Context + > + }>, + Struct<{ + _tag: Literal<["Success"]> + value: A + }> + ]>, + ExitFromSelf>, SchemaClass>, SchemaClass>> + > +{} + +/** + * @category Exit transformations + * @since 3.10.0 + */ +export const Exit = ( + { defect, failure, success }: { + readonly failure: E + readonly success: A + readonly defect: D + } +): Exit => { + const success_ = asSchema(success) + const failure_ = asSchema(failure) + const defect_ = asSchema(defect) + const out = transform( + exitEncoded(success_, failure_, defect_), + ExitFromSelf({ failure: typeSchema(failure_), success: typeSchema(success_), defect: typeSchema(defect_) }), + { + strict: false, + decode: (i) => exitDecode(i), + encode: (a) => + a._tag === "Failure" + ? { _tag: "Failure", cause: a.cause } as const + : { _tag: "Success", value: a.value } as const + } + ) + return out as any +} + +const hashSetArbitrary = + (item: LazyArbitrary, ctx: ArbitraryGenerationContext): LazyArbitrary> => (fc) => { + const items = fc.array(item(fc)) + return (ctx.depthIdentifier !== undefined ? fc.oneof(ctx, fc.constant([]), items) : items).map( + hashSet_.fromIterable + ) + } + +const hashSetPretty = (item: pretty_.Pretty): pretty_.Pretty> => (set) => + `HashSet(${Array.from(set).map((a) => item(a)).join(", ")})` + +const hashSetEquivalence = ( + item: Equivalence.Equivalence +): Equivalence.Equivalence> => { + const arrayEquivalence = array_.getEquivalence(item) + return Equivalence.make((a, b) => arrayEquivalence(Array.from(a), Array.from(b))) +} + +const hashSetParse = ( + decodeUnknown: ParseResult.DecodeUnknown, R> +): ParseResult.DeclarationDecodeUnknown, R> => +(u, options, ast) => + hashSet_.isHashSet(u) ? + toComposite(decodeUnknown(Array.from(u), options), hashSet_.fromIterable, ast, u) + : ParseResult.fail(new ParseResult.Type(ast, u)) + +/** + * @category api interface + * @since 3.10.0 + */ +export interface HashSetFromSelf extends + AnnotableDeclare< + HashSetFromSelf, + hashSet_.HashSet>, + hashSet_.HashSet>, + [Value] + > +{} + +/** + * @category HashSet transformations + * @since 3.10.0 + */ +export const HashSetFromSelf = ( + value: Value +): HashSetFromSelf => { + return declare( + [value], + { + decode: (item) => hashSetParse(ParseResult.decodeUnknown(Array$(item))), + encode: (item) => hashSetParse(ParseResult.encodeUnknown(Array$(item))) + }, + { + typeConstructor: { _tag: "effect/HashSet" }, + description: `HashSet<${format(value)}>`, + pretty: hashSetPretty, + arbitrary: hashSetArbitrary, + equivalence: hashSetEquivalence + } + ) +} + +/** + * @category api interface + * @since 3.10.0 + */ +export interface HashSet + extends transform, HashSetFromSelf>>> +{} + +/** + * @category HashSet transformations + * @since 3.10.0 + */ +export function HashSet(value: Value): HashSet { + return transform( + Array$(value), + HashSetFromSelf(typeSchema(asSchema(value))), + { + strict: true, + decode: (i) => hashSet_.fromIterable(i), + encode: (a) => Array.from(a) + } + ) +} + +const hashMapArbitrary = ( + key: LazyArbitrary, + value: LazyArbitrary, + ctx: ArbitraryGenerationContext +): LazyArbitrary> => +(fc) => { + const items = fc.array(fc.tuple(key(fc), value(fc))) + return (ctx.depthIdentifier !== undefined ? fc.oneof(ctx, fc.constant([]), items) : items).map(hashMap_.fromIterable) +} + +const hashMapPretty = ( + key: pretty_.Pretty, + value: pretty_.Pretty +): pretty_.Pretty> => +(map) => + `HashMap([${ + Array.from(map) + .map(([k, v]) => `[${key(k)}, ${value(v)}]`) + .join(", ") + }])` + +const hashMapEquivalence = ( + key: Equivalence.Equivalence, + value: Equivalence.Equivalence +): Equivalence.Equivalence> => { + const arrayEquivalence = array_.getEquivalence( + Equivalence.make<[K, V]>(([ka, va], [kb, vb]) => key(ka, kb) && value(va, vb)) + ) + return Equivalence.make((a, b) => arrayEquivalence(Array.from(a), Array.from(b))) +} + +const hashMapParse = ( + decodeUnknown: ParseResult.DecodeUnknown, R> +): ParseResult.DeclarationDecodeUnknown, R> => +(u, options, ast) => + hashMap_.isHashMap(u) ? + toComposite(decodeUnknown(Array.from(u), options), hashMap_.fromIterable, ast, u) + : ParseResult.fail(new ParseResult.Type(ast, u)) + +/** + * @category api interface + * @since 3.10.0 + */ +export interface HashMapFromSelf extends + AnnotableDeclare< + HashMapFromSelf, + hashMap_.HashMap, Schema.Type>, + hashMap_.HashMap, Schema.Encoded>, + [K, V] + > +{} + +/** + * @category HashMap transformations + * @since 3.10.0 + */ +export const HashMapFromSelf = ({ key, value }: { + readonly key: K + readonly value: V +}): HashMapFromSelf => { + return declare( + [key, value], + { + decode: (key, value) => hashMapParse(ParseResult.decodeUnknown(Array$(Tuple(key, value)))), + encode: (key, value) => hashMapParse(ParseResult.encodeUnknown(Array$(Tuple(key, value)))) + }, + { + typeConstructor: { _tag: "effect/HashMap" }, + description: `HashMap<${format(key)}, ${format(value)}>`, + pretty: hashMapPretty, + arbitrary: hashMapArbitrary, + equivalence: hashMapEquivalence + } + ) +} + +/** + * @category api interface + * @since 3.10.0 + */ +export interface HashMap + extends transform>, HashMapFromSelf>, SchemaClass>>> +{} + +/** + * @category HashMap transformations + * @since 3.10.0 + */ +export const HashMap = ({ key, value }: { + readonly key: K + readonly value: V +}): HashMap => { + return transform( + Array$(Tuple(key, value)), + HashMapFromSelf({ key: typeSchema(asSchema(key)), value: typeSchema(asSchema(value)) }), + { + strict: true, + decode: (i) => hashMap_.fromIterable(i), + encode: (a) => Array.from(a) + } + ) +} + +const listArbitrary = + (item: LazyArbitrary, ctx: ArbitraryGenerationContext): LazyArbitrary> => (fc) => { + const items = fc.array(item(fc)) + return (ctx.depthIdentifier !== undefined ? fc.oneof(ctx, fc.constant([]), items) : items).map(list_.fromIterable) + } + +const listPretty = (item: pretty_.Pretty): pretty_.Pretty> => (set) => + `List(${Array.from(set).map((a) => item(a)).join(", ")})` + +const listEquivalence = ( + item: Equivalence.Equivalence +): Equivalence.Equivalence> => { + const arrayEquivalence = array_.getEquivalence(item) + return Equivalence.make((a, b) => arrayEquivalence(Array.from(a), Array.from(b))) +} + +const listParse = ( + decodeUnknown: ParseResult.DecodeUnknown, R> +): ParseResult.DeclarationDecodeUnknown, R> => +(u, options, ast) => + list_.isList(u) ? + toComposite(decodeUnknown(Array.from(u), options), list_.fromIterable, ast, u) + : ParseResult.fail(new ParseResult.Type(ast, u)) + +/** + * @category api interface + * @since 3.10.0 + */ +export interface ListFromSelf extends + AnnotableDeclare< + ListFromSelf, + list_.List>, + list_.List>, + [Value] + > +{} + +/** + * @category List transformations + * @since 3.10.0 + */ +export const ListFromSelf = ( + value: Value +): ListFromSelf => { + return declare( + [value], + { + decode: (item) => listParse(ParseResult.decodeUnknown(Array$(item))), + encode: (item) => listParse(ParseResult.encodeUnknown(Array$(item))) + }, + { + typeConstructor: { _tag: "effect/List" }, + description: `List<${format(value)}>`, + pretty: listPretty, + arbitrary: listArbitrary, + equivalence: listEquivalence + } + ) +} + +/** + * @category api interface + * @since 3.10.0 + */ +export interface List + extends transform, ListFromSelf>>> +{} + +/** + * @category List transformations + * @since 3.10.0 + */ +export function List(value: Value): List { + return transform( + Array$(value), + ListFromSelf(typeSchema(asSchema(value))), + { + strict: true, + decode: (i) => list_.fromIterable(i), + encode: (a) => Array.from(a) + } + ) +} + +const sortedSetArbitrary = ( + item: LazyArbitrary, + ord: Order.Order, + ctx: ArbitraryGenerationContext +): LazyArbitrary> => +(fc) => { + const items = fc.array(item(fc)) + return (ctx.depthIdentifier !== undefined ? fc.oneof(ctx, fc.constant([]), items) : items).map((as) => + sortedSet_.fromIterable(as, ord) + ) +} + +const sortedSetPretty = (item: pretty_.Pretty): pretty_.Pretty> => (set) => + `new SortedSet([${Array.from(sortedSet_.values(set)).map((a) => item(a)).join(", ")}])` + +const sortedSetParse = ( + decodeUnknown: ParseResult.DecodeUnknown, R>, + ord: Order.Order +): ParseResult.DeclarationDecodeUnknown, R> => +(u, options, ast) => + sortedSet_.isSortedSet(u) ? + toComposite( + decodeUnknown(Array.from(sortedSet_.values(u)), options), + (as): sortedSet_.SortedSet => sortedSet_.fromIterable(as, ord), + ast, + u + ) + : ParseResult.fail(new ParseResult.Type(ast, u)) + +/** + * @category api interface + * @since 3.10.0 + */ +export interface SortedSetFromSelf extends + AnnotableDeclare< + SortedSetFromSelf, + sortedSet_.SortedSet>, + sortedSet_.SortedSet>, + [Value] + > +{} + +/** + * @category SortedSet transformations + * @since 3.10.0 + */ +export const SortedSetFromSelf = ( + value: Value, + ordA: Order.Order>, + ordI: Order.Order> +): SortedSetFromSelf => { + return declare( + [value], + { + decode: (item) => sortedSetParse(ParseResult.decodeUnknown(Array$(item)), ordA), + encode: (item) => sortedSetParse(ParseResult.encodeUnknown(Array$(item)), ordI) + }, + { + typeConstructor: { _tag: "effect/SortedSet" }, + description: `SortedSet<${format(value)}>`, + pretty: sortedSetPretty, + arbitrary: (arb, ctx) => sortedSetArbitrary(arb, ordA, ctx), + equivalence: () => sortedSet_.getEquivalence>() + } + ) +} + +/** + * @category api interface + * @since 3.10.0 + */ +export interface SortedSet + extends transform, SortedSetFromSelf>>> +{} + +/** + * @category SortedSet transformations + * @since 3.10.0 + */ +export function SortedSet( + value: Value, + ordA: Order.Order> +): SortedSet { + const to = typeSchema(asSchema(value)) + return transform( + Array$(value), + SortedSetFromSelf(to, ordA, ordA), + { + strict: true, + decode: (i) => sortedSet_.fromIterable(i, ordA), + encode: (a) => Array.from(sortedSet_.values(a)) + } + ) +} + +/** + * Converts an arbitrary value to a `boolean` by testing whether it is truthy. + * Uses `!!val` to coerce the value to a `boolean`. + * + * @see https://developer.mozilla.org/docs/Glossary/Truthy + * + * @category boolean constructors + * @since 3.10.0 + */ +export class BooleanFromUnknown extends transform( + Unknown, + Boolean$, + { + strict: true, + decode: (i) => Predicate.isTruthy(i), + encode: identity + } +).annotations({ identifier: "BooleanFromUnknown" }) {} + +/** + * Converts an `string` value into its corresponding `boolean` + * ("true" as `true` and "false" as `false`). + * + * @category boolean transformations + * @since 3.11.0 + */ +export class BooleanFromString extends transform( + Literal("true", "false").annotations({ description: "a string to be decoded into a boolean" }), + Boolean$, + { + strict: true, + decode: (i) => i === "true", + encode: (a) => a ? "true" : "false" + } +).annotations({ identifier: "BooleanFromString" }) {} + +/** + * @category Config validations + * @since 3.10.0 + */ +export const Config = (name: string, schema: Schema): config_.Config => { + const decodeUnknownEither = ParseResult.decodeUnknownEither(schema) + return config_.string(name).pipe( + config_.mapOrFail((s) => + decodeUnknownEither(s).pipe( + either_.mapLeft((error) => configError_.InvalidData([], ParseResult.TreeFormatter.formatIssueSync(error))) + ) + ) + ) +} + +// --------------------------------------------- +// Serializable +// --------------------------------------------- + +/** + * @since 3.10.0 + * @category symbol + */ +export const symbolSerializable: unique symbol = Symbol.for( + "effect/Schema/Serializable/symbol" +) + +/** + * The `Serializable` trait allows objects to define their own schema for + * serialization. + * + * @since 3.10.0 + * @category model + */ +export interface Serializable { + readonly [symbolSerializable]: Schema +} + +/** + * @since 3.10.0 + * @category model + */ +export declare namespace Serializable { + /** + * @since 3.10.0 + */ + export type Type = T extends Serializable ? A : never + /** + * @since 3.10.0 + */ + export type Encoded = T extends Serializable ? I : never + /** + * @since 3.10.0 + */ + export type Context = T extends Serializable ? R : never + /** + * @since 3.10.0 + */ + export type Any = Serializable + /** + * @since 3.10.0 + */ + export type All = + | Any + | Serializable + | Serializable + | Serializable +} + +/** + * @since 3.10.0 + */ +export const asSerializable = ( + serializable: S +): Serializable, Serializable.Encoded, Serializable.Context> => serializable as any + +/** + * @since 3.10.0 + * @category accessor + */ +export const serializableSchema = (self: Serializable): Schema => self[symbolSerializable] + +/** + * @since 3.10.0 + * @category encoding + */ +export const serialize = (self: Serializable): Effect.Effect => + encodeUnknown(self[symbolSerializable])(self) + +/** + * @since 3.10.0 + * @category decoding + */ +export const deserialize: { + (value: unknown): (self: Serializable) => Effect.Effect + (self: Serializable, value: unknown): Effect.Effect +} = dual( + 2, + (self: Serializable, value: unknown): Effect.Effect => + decodeUnknown(self[symbolSerializable])(value) +) + +/** + * @since 3.10.0 + * @category symbol + */ +export const symbolWithResult: unique symbol = Symbol.for( + "effect/Schema/Serializable/symbolResult" +) + +/** + * The `WithResult` trait is designed to encapsulate the outcome of an + * operation, distinguishing between success and failure cases. Each case is + * associated with a schema that defines the structure and types of the success + * or failure data. + * + * @since 3.10.0 + * @category model + */ +export interface WithResult { + readonly [symbolWithResult]: { + readonly success: Schema + readonly failure: Schema + } +} + +/** + * @since 3.10.0 + * @category model + */ +export declare namespace WithResult { + /** + * @since 3.10.0 + */ + export type Success = T extends WithResult ? _A : never + /** + * @since 3.10.0 + */ + export type SuccessEncoded = T extends WithResult ? _I : never + /** + * @since 3.10.0 + */ + export type Failure = T extends WithResult ? _E : never + /** + * @since 3.10.0 + */ + export type FailureEncoded = T extends WithResult ? _EI : never + + /** + * @since 3.10.0 + */ + export type Context = T extends WithResult ? R : never + /** + * @since 3.10.0 + */ + export type Any = WithResult + /** + * @since 3.10.0 + */ + export type All = + | Any + | WithResult +} + +/** + * @since 3.10.0 + */ +export const asWithResult = ( + withExit: WR +): WithResult< + WithResult.Success, + WithResult.SuccessEncoded, + WithResult.Failure, + WithResult.FailureEncoded, + WithResult.Context +> => withExit as any + +/** + * @since 3.10.0 + * @category accessor + */ +export const failureSchema = (self: WithResult): Schema => + self[symbolWithResult].failure + +/** + * @since 3.10.0 + * @category accessor + */ +export const successSchema = (self: WithResult): Schema => + self[symbolWithResult].success + +const exitSchemaCache = globalValue( + "effect/Schema/Serializable/exitSchemaCache", + () => new WeakMap>() +) + +/** + * @since 3.10.0 + * @category accessor + */ +export const exitSchema = (self: WithResult): Schema< + exit_.Exit, + ExitEncoded, + R +> => { + const proto = Object.getPrototypeOf(self) + if (!(symbolWithResult in proto)) { + return Exit({ + failure: failureSchema(self), + success: successSchema(self), + defect: Defect + }) + } + let schema = exitSchemaCache.get(proto) + if (schema === undefined) { + schema = Exit({ + failure: failureSchema(self), + success: successSchema(self), + defect: Defect + }) + exitSchemaCache.set(proto, schema) + } + return schema +} + +/** + * @since 3.10.0 + * @category encoding + */ +export const serializeFailure: { + (value: FA): ( + self: WithResult + ) => Effect.Effect + (self: WithResult, value: FA): Effect.Effect +} = dual( + 2, + (self: WithResult, value: FA): Effect.Effect => + encode(self[symbolWithResult].failure)(value) +) + +/** + * @since 3.10.0 + * @category decoding + */ +export const deserializeFailure: { + ( + value: unknown + ): (self: WithResult) => Effect.Effect + (self: WithResult, value: unknown): Effect.Effect +} = dual( + 2, + ( + self: WithResult, + value: unknown + ): Effect.Effect => decodeUnknown(self[symbolWithResult].failure)(value) +) + +/** + * @since 3.10.0 + * @category encoding + */ +export const serializeSuccess: { + (value: SA): ( + self: WithResult + ) => Effect.Effect + (self: WithResult, value: SA): Effect.Effect +} = dual( + 2, + (self: WithResult, value: SA): Effect.Effect => + encode(self[symbolWithResult].success)(value) +) + +/** + * @since 3.10.0 + * @category decoding + */ +export const deserializeSuccess: { + (value: unknown): ( + self: WithResult + ) => Effect.Effect + (self: WithResult, value: unknown): Effect.Effect +} = dual( + 2, + ( + self: WithResult, + value: unknown + ): Effect.Effect => decodeUnknown(self[symbolWithResult].success)(value) +) + +/** + * @since 3.10.0 + * @category encoding + */ +export const serializeExit: { + (value: exit_.Exit): ( + self: WithResult + ) => Effect.Effect, ParseResult.ParseError, R> + ( + self: WithResult, + value: exit_.Exit + ): Effect.Effect, ParseResult.ParseError, R> +} = dual(2, ( + self: WithResult, + value: exit_.Exit +): Effect.Effect, ParseResult.ParseError, R> => encode(exitSchema(self))(value)) + +/** + * @since 3.10.0 + * @category decoding + */ +export const deserializeExit: { + (value: unknown): ( + self: WithResult + ) => Effect.Effect, ParseResult.ParseError, R> + ( + self: WithResult, + value: unknown + ): Effect.Effect, ParseResult.ParseError, R> +} = dual(2, ( + self: WithResult, + value: unknown +): Effect.Effect, ParseResult.ParseError, R> => decodeUnknown(exitSchema(self))(value)) + +// --------------------------------------------- +// SerializableWithResult +// --------------------------------------------- + +/** + * The `SerializableWithResult` trait is specifically designed to model remote + * procedures that require serialization of their input and output, managing + * both successful and failed outcomes. + * + * This trait combines functionality from both the `Serializable` and `WithResult` + * traits to handle data serialization and the bifurcation of operation results + * into success or failure categories. + * + * @since 3.10.0 + * @category model + */ +export interface SerializableWithResult< + A, + I, + R, + Success, + SuccessEncoded, + Failure, + FailureEncoded, + ResultR +> extends Serializable, WithResult {} + +/** + * @since 3.10.0 + * @category model + */ +export declare namespace SerializableWithResult { + /** + * @since 3.10.0 + */ + export type Context

, + f: Fn + ] +) => { + const f = pattern[pattern.length - 1] + const values: Array

= pattern.slice(0, -1) as any + const pred = values.length === 1 + ? (_: any) => _ != null && _[field] === values[0] + : (_: any) => _ != null && values.includes(_[field]) + + return ( + self: Matcher + ): Matcher< + I, + Types.AddWithout>>, + Types.ApplyFilters>>>, + A | ReturnType, + Pr, + Ret + > => (self as any).add(makeWhen(pred, f as any)) as any +} + +/** @internal */ +export const discriminatorStartsWith = (field: D) => +< + R, + P extends string, + Ret, + Fn extends (_: Extract>) => Ret +>( + pattern: P, + f: Fn +) => { + const pred = (_: any) => _ != null && typeof _[field] === "string" && _[field].startsWith(pattern) + + return ( + self: Matcher + ): Matcher< + I, + Types.AddWithout>>, + Types.ApplyFilters< + I, + Types.AddWithout>> + >, + A | ReturnType, + Pr, + Ret + > => (self as any).add(makeWhen(pred, f as any)) as any +} + +/** @internal */ +export const discriminators = (field: D) => +< + R, + Ret, + P extends + & { + readonly [Tag in Types.Tags & string]?: + | ((_: Extract>) => Ret) + | undefined + } + & { readonly [Tag in Exclude>]: never } +>( + fields: P +) => { + const predicate = makeWhen( + (arg: any) => arg != null && arg[field] in fields, + (data: any) => (fields as any)[data[field]](data) + ) + + return ( + self: Matcher + ): Matcher< + I, + Types.AddWithout>>, + Types.ApplyFilters>>>, + A | ReturnType, + Pr, + Ret + > => (self as any).add(predicate) +} + +/** @internal */ +export const discriminatorsExhaustive: ( + field: D +) => < + R, + Ret, + P extends + & { + readonly [Tag in Types.Tags & string]: ( + _: Extract> + ) => Ret + } + & { readonly [Tag in Exclude>]: never } +>( + fields: P +) => ( + self: Matcher +) => [Pr] extends [never] ? (u: I) => Unify> + : Unify> = (field: string) => (fields: object) => { + const addCases = discriminators(field)(fields) + return (matcher: any) => exhaustive(addCases(matcher)) + } + +/** @internal */ +export const tag: < + R, + P extends Types.Tags<"_tag", R> & string, + Ret, + Fn extends (_: Extract>) => Ret +>( + ...pattern: [ + first: P, + ...values: Array

, + f: Fn + ] +) => ( + self: Matcher +) => Matcher< + I, + Types.AddWithout>>, + Types.ApplyFilters>>>, + ReturnType | A, + Pr, + Ret +> = discriminator("_tag") + +/** @internal */ +export const tagStartsWith = discriminatorStartsWith("_tag") + +/** @internal */ +export const tags = discriminators("_tag") + +/** @internal */ +export const tagsExhaustive = discriminatorsExhaustive("_tag") + +/** @internal */ +export const not = < + R, + const P extends Types.PatternPrimitive | Types.PatternBase, + Ret, + Fn extends (_: Types.NotMatch) => Ret +>( + pattern: P, + f: Fn +) => +( + self: Matcher +): Matcher< + I, + Types.AddOnly>, + Types.ApplyFilters>>, + A | ReturnType, + Pr, + Ret +> => (self as any).add(makeNot(makePredicate(pattern), f as any)) + +/** @internal */ +export const nonEmptyString: SafeRefinement = + ((u: unknown) => typeof u === "string" && u.length > 0) as any + +/** @internal */ +export const is: < + Literals extends ReadonlyArray +>( + ...literals: Literals +) => SafeRefinement = (...literals): any => { + const len = literals.length + return (u: unknown) => { + for (let i = 0; i < len; i++) { + if (u === literals[i]) { + return true + } + } + return false + } +} + +/** @internal */ +export const any: SafeRefinement = (() => true) as any + +/** @internal */ +export const defined = (u: A): u is A & {} => (u !== undefined && u !== null) as any + +/** @internal */ +export const instanceOf = any>( + constructor: A +): SafeRefinement, never> => ((u: unknown) => u instanceof constructor) as any + +/** @internal */ +export const instanceOfUnsafe: any>( + constructor: A +) => SafeRefinement, InstanceType> = instanceOf + +/** @internal */ +export const orElse = + Ret>(f: F) => + (self: Matcher): [Pr] extends [never] ? (input: I) => Unify | A> + : Unify | A> => + { + const result = either(self) + + if (Either.isEither(result)) { + // @ts-expect-error + return result._tag === "Right" ? result.right : f(result.left) + } + + // @ts-expect-error + return (input: I) => { + const a = result(input) + return a._tag === "Right" ? a.right : f(a.left) + } + } + +/** @internal */ +export const orElseAbsurd = ( + self: Matcher +): [Pr] extends [never] ? (input: I) => Unify : Unify => + orElse(() => { + throw new Error("effect/Match/orElseAbsurd: absurd") + })(self) + +/** @internal */ +export const either: ( + self: Matcher +) => [Pr] extends [never] ? (input: I) => Either.Either, R> + : Either.Either, R> = ((self: Matcher) => { + if (self._tag === "ValueMatcher") { + return self.value + } + + const len = self.cases.length + if (len === 1) { + const _case = self.cases[0] + return (input: I): Either.Either => { + if (_case._tag === "When" && _case.guard(input) === true) { + return Either.right(_case.evaluate(input)) + } else if (_case._tag === "Not" && _case.guard(input) === false) { + return Either.right(_case.evaluate(input)) + } + return Either.left(input as any) + } + } + return (input: I): Either.Either => { + for (let i = 0; i < len; i++) { + const _case = self.cases[i] + if (_case._tag === "When" && _case.guard(input) === true) { + return Either.right(_case.evaluate(input)) + } else if (_case._tag === "Not" && _case.guard(input) === false) { + return Either.right(_case.evaluate(input)) + } + } + + return Either.left(input as any) + } + }) as any + +/** @internal */ +export const option: ( + self: Matcher +) => [Pr] extends [never] ? (input: I) => Option.Option> + : Option.Option> = ((self: Matcher) => { + const toEither = either(self) + if (Either.isEither(toEither)) { + return Either.match(toEither, { + onLeft: () => Option.none(), + onRight: Option.some + }) + } + return (input: I): Option.Option => + Either.match((toEither as any)(input), { + onLeft: () => Option.none(), + onRight: Option.some as any + }) + }) as any + +const getExhaustiveAbsurdErrorMessage = "effect/Match/exhaustive: absurd" + +/** @internal */ +export const exhaustive: ( + self: Matcher +) => [Pr] extends [never] ? (u: I) => Unify : Unify = (( + self: Matcher +) => { + const toEither = either(self as any) + + if (Either.isEither(toEither)) { + if (toEither._tag === "Right") { + return toEither.right + } + + throw new Error(getExhaustiveAbsurdErrorMessage) + } + + return (u: I): A => { + // @ts-expect-error + const result = toEither(u) + + if (result._tag === "Right") { + return result.right as any + } + + throw new Error(getExhaustiveAbsurdErrorMessage) + } +}) as any diff --git a/repos/effect/packages/effect/src/internal/metric.ts b/repos/effect/packages/effect/src/internal/metric.ts new file mode 100644 index 0000000..e4fcb23 --- /dev/null +++ b/repos/effect/packages/effect/src/internal/metric.ts @@ -0,0 +1,577 @@ +import * as Arr from "../Array.js" +import * as Clock from "../Clock.js" +import * as Duration from "../Duration.js" +import type * as Effect from "../Effect.js" +import type { LazyArg } from "../Function.js" +import { constVoid, dual, identity, pipe } from "../Function.js" +import { globalValue } from "../GlobalValue.js" +import type * as Metric from "../Metric.js" +import type * as MetricBoundaries from "../MetricBoundaries.js" +import type * as MetricHook from "../MetricHook.js" +import type * as MetricKey from "../MetricKey.js" +import type * as MetricKeyType from "../MetricKeyType.js" +import type * as MetricLabel from "../MetricLabel.js" +import type * as MetricPair from "../MetricPair.js" +import type * as MetricRegistry from "../MetricRegistry.js" +import type * as MetricState from "../MetricState.js" +import { pipeArguments } from "../Pipeable.js" +import * as Cause from "./cause.js" +import * as effect_ from "./core-effect.js" +import * as core from "./core.js" +import * as metricBoundaries from "./metric/boundaries.js" +import * as metricKey from "./metric/key.js" +import * as metricKeyType from "./metric/keyType.js" +import * as metricLabel from "./metric/label.js" +import * as metricRegistry from "./metric/registry.js" + +/** @internal */ +const MetricSymbolKey = "effect/Metric" + +/** @internal */ +export const MetricTypeId: Metric.MetricTypeId = Symbol.for( + MetricSymbolKey +) as Metric.MetricTypeId + +const metricVariance = { + /* c8 ignore next */ + _Type: (_: any) => _, + /* c8 ignore next */ + _In: (_: unknown) => _, + /* c8 ignore next */ + _Out: (_: never) => _ +} + +/** @internal */ +export const globalMetricRegistry: MetricRegistry.MetricRegistry = globalValue( + Symbol.for("effect/Metric/globalMetricRegistry"), + () => metricRegistry.make() +) + +/** @internal */ +export const make: Metric.MetricApply = function( + keyType: Type, + unsafeUpdate: (input: In, extraTags: ReadonlyArray) => void, + unsafeValue: (extraTags: ReadonlyArray) => Out, + unsafeModify: (input: In, extraTags: ReadonlyArray) => void +): Metric.Metric { + const metric: Metric.Metric = Object.assign( + (effect: Effect.Effect): Effect.Effect => + core.tap(effect, (a) => update(metric, a)), + { + [MetricTypeId]: metricVariance, + keyType, + unsafeUpdate, + unsafeValue, + unsafeModify, + register() { + this.unsafeValue([]) + return this as any + }, + pipe() { + return pipeArguments(this, arguments) + } + } as const + ) + return metric +} + +/** @internal */ +export const mapInput = dual< + (f: (input: In2) => In) => (self: Metric.Metric) => Metric.Metric, + (self: Metric.Metric, f: (input: In2) => In) => Metric.Metric +>(2, (self, f) => + make( + self.keyType, + (input, extraTags) => self.unsafeUpdate(f(input), extraTags), + self.unsafeValue, + (input, extraTags) => self.unsafeModify(f(input), extraTags) + )) + +/** @internal */ +export const counter: { + (name: string, options?: { + readonly description?: string | undefined + readonly bigint?: false | undefined + readonly incremental?: boolean | undefined + }): Metric.Metric.Counter + (name: string, options: { + readonly description?: string | undefined + readonly bigint: true + readonly incremental?: boolean | undefined + }): Metric.Metric.Counter +} = (name, options) => fromMetricKey(metricKey.counter(name, options as any)) as any + +/** @internal */ +export const frequency = (name: string, options?: { + readonly description?: string | undefined + readonly preregisteredWords?: ReadonlyArray | undefined +}): Metric.Metric.Frequency => fromMetricKey(metricKey.frequency(name, options)) + +/** @internal */ +export const withConstantInput = dual< + (input: In) => (self: Metric.Metric) => Metric.Metric, + (self: Metric.Metric, input: In) => Metric.Metric +>(2, (self, input) => mapInput(self, () => input)) + +/** @internal */ +export const fromMetricKey = >( + key: MetricKey.MetricKey +): Metric.Metric< + Type, + MetricKeyType.MetricKeyType.InType, + MetricKeyType.MetricKeyType.OutType +> => { + let untaggedHook: + | MetricHook.MetricHook< + MetricKeyType.MetricKeyType.InType, + MetricKeyType.MetricKeyType.OutType + > + | undefined + const hookCache = new WeakMap, MetricHook.MetricHook>() + + const hook = (extraTags: ReadonlyArray): MetricHook.MetricHook< + MetricKeyType.MetricKeyType.InType, + MetricKeyType.MetricKeyType.OutType + > => { + if (extraTags.length === 0) { + if (untaggedHook !== undefined) { + return untaggedHook + } + untaggedHook = globalMetricRegistry.get(key) + return untaggedHook + } + + let hook = hookCache.get(extraTags) + if (hook !== undefined) { + return hook + } + hook = globalMetricRegistry.get(metricKey.taggedWithLabels(key, extraTags)) + hookCache.set(extraTags, hook) + return hook + } + + return make( + key.keyType, + (input, extraTags) => hook(extraTags).update(input), + (extraTags) => hook(extraTags).get(), + (input, extraTags) => hook(extraTags).modify(input) + ) +} + +/** @internal */ +export const gauge: { + (name: string, options?: { + readonly description?: string | undefined + readonly bigint?: false | undefined + }): Metric.Metric.Gauge + (name: string, options: { + readonly description?: string | undefined + readonly bigint: true + }): Metric.Metric.Gauge +} = (name, options) => fromMetricKey(metricKey.gauge(name, options as any)) as any + +/** @internal */ +export const histogram = (name: string, boundaries: MetricBoundaries.MetricBoundaries, description?: string) => + fromMetricKey(metricKey.histogram(name, boundaries, description)) + +/* @internal */ +export const increment = ( + self: + | Metric.Metric.Counter + | Metric.Metric.Counter + | Metric.Metric.Gauge + | Metric.Metric.Gauge +): Effect.Effect => + metricKeyType.isCounterKey(self.keyType) + ? update(self as Metric.Metric.Counter, self.keyType.bigint ? BigInt(1) as any : 1) + : modify(self as Metric.Metric.Gauge, self.keyType.bigint ? BigInt(1) as any : 1) + +/* @internal */ +export const incrementBy = dual< + { + (amount: number): (self: Metric.Metric.Counter | Metric.Metric.Counter) => Effect.Effect + (amount: bigint): (self: Metric.Metric.Counter | Metric.Metric.Gauge) => Effect.Effect + }, + { + (self: Metric.Metric.Counter | Metric.Metric.Gauge, amount: number): Effect.Effect + (self: Metric.Metric.Counter | Metric.Metric.Gauge, amount: bigint): Effect.Effect + } +>(2, (self, amount) => + metricKeyType.isCounterKey(self.keyType) + ? update(self as any, amount) + : modify(self as any, amount)) + +/** @internal */ +export const map = dual< + (f: (out: Out) => Out2) => (self: Metric.Metric) => Metric.Metric, + (self: Metric.Metric, f: (out: Out) => Out2) => Metric.Metric +>(2, (self, f) => + make( + self.keyType, + self.unsafeUpdate, + (extraTags) => f(self.unsafeValue(extraTags)), + self.unsafeModify + )) + +/** @internal */ +export const mapType = dual< + ( + f: (type: Type) => Type2 + ) => ( + self: Metric.Metric + ) => Metric.Metric, + ( + self: Metric.Metric, + f: (type: Type) => Type2 + ) => Metric.Metric +>(2, (self, f) => + make( + f(self.keyType), + self.unsafeUpdate, + self.unsafeValue, + self.unsafeModify + )) + +/** @internal */ +export const modify = dual< + (input: In) => (self: Metric.Metric) => Effect.Effect, + (self: Metric.Metric, input: In) => Effect.Effect +>(2, (self, input) => + core.fiberRefGetWith( + core.currentMetricLabels, + (tags) => core.sync(() => self.unsafeModify(input, tags)) + )) + +/* @internal */ +export const set = dual< + { + (value: number): (self: Metric.Metric.Gauge) => Effect.Effect + (value: bigint): (self: Metric.Metric.Gauge) => Effect.Effect + }, + { + (self: Metric.Metric.Gauge, value: number): Effect.Effect + (self: Metric.Metric.Gauge, value: bigint): Effect.Effect + } +>(2, (self, value) => update(self as any, value)) + +/** @internal */ +export const succeed = (out: Out): Metric.Metric => + make(void 0 as void, constVoid, () => out, constVoid) + +/** @internal */ +export const sync = (evaluate: LazyArg): Metric.Metric => + make(void 0 as void, constVoid, evaluate, constVoid) + +/** @internal */ +export const summary = ( + options: { + readonly name: string + readonly maxAge: Duration.DurationInput + readonly maxSize: number + readonly error: number + readonly quantiles: ReadonlyArray + readonly description?: string | undefined + } +): Metric.Metric.Summary => withNow(summaryTimestamp(options)) + +/** @internal */ +export const summaryTimestamp = ( + options: { + readonly name: string + readonly maxAge: Duration.DurationInput + readonly maxSize: number + readonly error: number + readonly quantiles: ReadonlyArray + readonly description?: string | undefined + } +): Metric.Metric.Summary => fromMetricKey(metricKey.summary(options)) + +/** @internal */ +export const tagged = dual< + (key: string, value: string) => (self: Metric.Metric) => Metric.Metric, + (self: Metric.Metric, key: string, value: string) => Metric.Metric +>(3, (self, key, value) => taggedWithLabels(self, [metricLabel.make(key, value)])) + +/** @internal */ +export const taggedWithLabelsInput = dual< + ( + f: (input: In) => Iterable + ) => (self: Metric.Metric) => Metric.Metric, + ( + self: Metric.Metric, + f: (input: In) => Iterable + ) => Metric.Metric +>(2, (self, f) => + map( + make( + self.keyType, + (input, extraTags) => + self.unsafeUpdate( + input, + Arr.union(f(input), extraTags) + ), + self.unsafeValue, + (input, extraTags) => + self.unsafeModify( + input, + Arr.union(f(input), extraTags) + ) + ), + constVoid + )) + +/** @internal */ +export const taggedWithLabels = dual< + ( + extraTags: Iterable + ) => (self: Metric.Metric) => Metric.Metric, + ( + self: Metric.Metric, + extraTags: Iterable + ) => Metric.Metric +>(2, (self, extraTags) => { + return make( + self.keyType, + (input, extraTags1) => self.unsafeUpdate(input, Arr.union(extraTags, extraTags1)), + (extraTags1) => self.unsafeValue(Arr.union(extraTags, extraTags1)), + (input, extraTags1) => self.unsafeModify(input, Arr.union(extraTags, extraTags1)) + ) +}) + +/** @internal */ +export const timer = (name: string, description?: string): Metric.Metric< + MetricKeyType.MetricKeyType.Histogram, + Duration.Duration, + MetricState.MetricState.Histogram +> => { + const boundaries = metricBoundaries.exponential({ + start: 0.5, + factor: 2, + count: 35 + }) + const base = pipe(histogram(name, boundaries, description), tagged("time_unit", "milliseconds")) + return mapInput(base, Duration.toMillis) +} + +/** @internal */ +export const timerWithBoundaries = ( + name: string, + boundaries: ReadonlyArray, + description?: string +): Metric.Metric< + MetricKeyType.MetricKeyType.Histogram, + Duration.Duration, + MetricState.MetricState.Histogram +> => { + const base = pipe( + histogram(name, metricBoundaries.fromIterable(boundaries), description), + tagged("time_unit", "milliseconds") + ) + return mapInput(base, Duration.toMillis) +} + +/* @internal */ +export const trackAll = dual< + ( + input: In + ) => ( + self: Metric.Metric + ) => (effect: Effect.Effect) => Effect.Effect, + ( + self: Metric.Metric, + input: In + ) => (effect: Effect.Effect) => Effect.Effect +>(2, (self, input) => (effect) => + core.matchCauseEffect(effect, { + onFailure: (cause) => core.zipRight(update(self, input), core.failCause(cause)), + onSuccess: (value) => core.zipRight(update(self, input), core.succeed(value)) + })) + +/* @internal */ +export const trackDefect = dual< + ( + metric: Metric.Metric + ) => (self: Effect.Effect) => Effect.Effect, + ( + self: Effect.Effect, + metric: Metric.Metric + ) => Effect.Effect +>(2, (self, metric) => trackDefectWith(self, metric, identity)) + +/* @internal */ +export const trackDefectWith = dual< + ( + metric: Metric.Metric, + f: (defect: unknown) => In + ) => (self: Effect.Effect) => Effect.Effect, + ( + self: Effect.Effect, + metric: Metric.Metric, + f: (defect: unknown) => In + ) => Effect.Effect +>(3, (self, metric, f) => { + const updater = (defect: unknown) => update(metric, f(defect)) + return effect_.tapDefect(self, (cause) => core.forEachSequentialDiscard(Cause.defects(cause), updater)) +}) + +/* @internal */ +export const trackDuration = dual< + ( + metric: Metric.Metric + ) => (self: Effect.Effect) => Effect.Effect, + ( + self: Effect.Effect, + metric: Metric.Metric + ) => Effect.Effect +>(2, (self, metric) => trackDurationWith(self, metric, identity)) + +/* @internal */ +export const trackDurationWith = dual< + ( + metric: Metric.Metric, + f: (duration: Duration.Duration) => In + ) => (effect: Effect.Effect) => Effect.Effect, + ( + self: Effect.Effect, + metric: Metric.Metric, + f: (duration: Duration.Duration) => In + ) => Effect.Effect +>(3, (self, metric, f) => + Clock.clockWith((clock) => { + const startTime = clock.unsafeCurrentTimeNanos() + return core.tap(self, (_) => { + const endTime = clock.unsafeCurrentTimeNanos() + const duration = Duration.nanos(endTime - startTime) + return update(metric, f(duration)) + }) + })) + +/* @internal */ +export const trackError = dual< + ( + metric: Metric.Metric + ) => (self: Effect.Effect) => Effect.Effect, + ( + self: Effect.Effect, + metric: Metric.Metric + ) => Effect.Effect +>(2, ( + self: Effect.Effect, + metric: Metric.Metric +) => trackErrorWith(self, metric, (a: In) => a)) + +/* @internal */ +export const trackErrorWith = dual< + ( + metric: Metric.Metric, + f: (error: In2) => In + ) => (effect: Effect.Effect) => Effect.Effect, + ( + self: Effect.Effect, + metric: Metric.Metric, + f: (error: In2) => In + ) => Effect.Effect +>(3, ( + self: Effect.Effect, + metric: Metric.Metric, + f: (error: In2) => In +) => { + const updater = (error: E): Effect.Effect => update(metric, f(error)) + return effect_.tapError(self, updater) +}) + +/* @internal */ +export const trackSuccess = dual< + ( + metric: Metric.Metric + ) => (self: Effect.Effect) => Effect.Effect, + ( + self: Effect.Effect, + metric: Metric.Metric + ) => Effect.Effect +>(2, ( + self: Effect.Effect, + metric: Metric.Metric +) => trackSuccessWith(self, metric, (a: In) => a)) + +/* @internal */ +export const trackSuccessWith = dual< + ( + metric: Metric.Metric, + f: (value: In2) => In + ) => (self: Effect.Effect) => Effect.Effect, + ( + self: Effect.Effect, + metric: Metric.Metric, + f: (value: In2) => In + ) => Effect.Effect +>(3, ( + self: Effect.Effect, + metric: Metric.Metric, + f: (value: In2) => In +) => { + const updater = (value: A): Effect.Effect => update(metric, f(value)) + return core.tap(self, updater) +}) + +/* @internal */ +export const update = dual< + (input: In) => (self: Metric.Metric) => Effect.Effect, + (self: Metric.Metric, input: In) => Effect.Effect +>(2, (self, input) => + core.fiberRefGetWith( + core.currentMetricLabels, + (tags) => core.sync(() => self.unsafeUpdate(input, tags)) + )) + +/* @internal */ +export const value = ( + self: Metric.Metric +): Effect.Effect => + core.fiberRefGetWith( + core.currentMetricLabels, + (tags) => core.sync(() => self.unsafeValue(tags)) + ) + +/** @internal */ +export const withNow = ( + self: Metric.Metric +): Metric.Metric => mapInput(self, (input: In) => [input, Date.now()] as const) + +/** @internal */ +export const zip = dual< + ( + that: Metric.Metric + ) => ( + self: Metric.Metric + ) => Metric.Metric, + ( + self: Metric.Metric, + that: Metric.Metric + ) => Metric.Metric +>( + 2, + (self: Metric.Metric, that: Metric.Metric) => + make( + [self.keyType, that.keyType] as const, + (input: readonly [In, In2], extraTags) => { + const [l, r] = input + self.unsafeUpdate(l, extraTags) + that.unsafeUpdate(r, extraTags) + }, + (extraTags) => [self.unsafeValue(extraTags), that.unsafeValue(extraTags)], + (input: readonly [In, In2], extraTags) => { + const [l, r] = input + self.unsafeModify(l, extraTags) + that.unsafeModify(r, extraTags) + } + ) +) + +/** @internal */ +export const unsafeSnapshot = (): Array => globalMetricRegistry.snapshot() + +/** @internal */ +export const snapshot: Effect.Effect> = core.sync( + unsafeSnapshot +) diff --git a/repos/effect/packages/effect/src/internal/metric/boundaries.ts b/repos/effect/packages/effect/src/internal/metric/boundaries.ts new file mode 100644 index 0000000..6c5a67f --- /dev/null +++ b/repos/effect/packages/effect/src/internal/metric/boundaries.ts @@ -0,0 +1,75 @@ +import * as Arr from "../../Array.js" +import * as Chunk from "../../Chunk.js" +import * as Equal from "../../Equal.js" +import { pipe } from "../../Function.js" +import * as Hash from "../../Hash.js" +import type * as MetricBoundaries from "../../MetricBoundaries.js" +import { pipeArguments } from "../../Pipeable.js" +import { hasProperty } from "../../Predicate.js" + +/** @internal */ +const MetricBoundariesSymbolKey = "effect/MetricBoundaries" + +/** @internal */ +export const MetricBoundariesTypeId: MetricBoundaries.MetricBoundariesTypeId = Symbol.for( + MetricBoundariesSymbolKey +) as MetricBoundaries.MetricBoundariesTypeId + +/** @internal */ +class MetricBoundariesImpl implements MetricBoundaries.MetricBoundaries { + readonly [MetricBoundariesTypeId]: MetricBoundaries.MetricBoundariesTypeId = MetricBoundariesTypeId + constructor(readonly values: ReadonlyArray) { + this._hash = pipe( + Hash.string(MetricBoundariesSymbolKey), + Hash.combine(Hash.array(this.values)) + ) + } + readonly _hash: number; + [Hash.symbol](): number { + return this._hash + } + [Equal.symbol](u: unknown): boolean { + return isMetricBoundaries(u) && Equal.equals(this.values, u.values) + } + pipe() { + return pipeArguments(this, arguments) + } +} + +/** @internal */ +export const isMetricBoundaries = (u: unknown): u is MetricBoundaries.MetricBoundaries => + hasProperty(u, MetricBoundariesTypeId) + +/** @internal */ +export const fromIterable = (iterable: Iterable): MetricBoundaries.MetricBoundaries => { + const values = pipe( + iterable, + Arr.appendAll(Chunk.of(Number.POSITIVE_INFINITY)), + Arr.dedupe + ) + return new MetricBoundariesImpl(values) +} + +/** @internal */ +export const linear = (options: { + readonly start: number + readonly width: number + readonly count: number +}): MetricBoundaries.MetricBoundaries => + pipe( + Arr.makeBy(options.count - 1, (i) => options.start + i * options.width), + Chunk.unsafeFromArray, + fromIterable + ) + +/** @internal */ +export const exponential = (options: { + readonly start: number + readonly factor: number + readonly count: number +}): MetricBoundaries.MetricBoundaries => + pipe( + Arr.makeBy(options.count - 1, (i) => options.start * Math.pow(options.factor, i)), + Chunk.unsafeFromArray, + fromIterable + ) diff --git a/repos/effect/packages/effect/src/internal/metric/hook.ts b/repos/effect/packages/effect/src/internal/metric/hook.ts new file mode 100644 index 0000000..eeb9d1d --- /dev/null +++ b/repos/effect/packages/effect/src/internal/metric/hook.ts @@ -0,0 +1,483 @@ +import * as Arr from "../../Array.js" +import * as Duration from "../../Duration.js" +import type { LazyArg } from "../../Function.js" +import { dual, pipe } from "../../Function.js" +import type * as MetricHook from "../../MetricHook.js" +import type * as MetricKey from "../../MetricKey.js" +import type * as MetricState from "../../MetricState.js" +import * as number from "../../Number.js" +import * as Option from "../../Option.js" +import { pipeArguments } from "../../Pipeable.js" +import * as metricState from "./state.js" + +/** @internal */ +const MetricHookSymbolKey = "effect/MetricHook" + +/** @internal */ +export const MetricHookTypeId: MetricHook.MetricHookTypeId = Symbol.for( + MetricHookSymbolKey +) as MetricHook.MetricHookTypeId + +const metricHookVariance = { + /* c8 ignore next */ + _In: (_: unknown) => _, + /* c8 ignore next */ + _Out: (_: never) => _ +} + +/** @internal */ +export const make = ( + options: { + readonly get: LazyArg + readonly update: (input: In) => void + readonly modify: (input: In) => void + } +): MetricHook.MetricHook => ({ + [MetricHookTypeId]: metricHookVariance, + pipe() { + return pipeArguments(this, arguments) + }, + ...options +}) + +/** @internal */ +export const onModify = dual< + (f: (input: In) => void) => (self: MetricHook.MetricHook) => MetricHook.MetricHook, + (self: MetricHook.MetricHook, f: (input: In) => void) => MetricHook.MetricHook +>(2, (self, f) => ({ + [MetricHookTypeId]: metricHookVariance, + pipe() { + return pipeArguments(this, arguments) + }, + get: self.get, + update: self.update, + modify: (input) => { + self.modify(input) + return f(input) + } +})) + +/** @internal */ +export const onUpdate = dual< + (f: (input: In) => void) => (self: MetricHook.MetricHook) => MetricHook.MetricHook, + (self: MetricHook.MetricHook, f: (input: In) => void) => MetricHook.MetricHook +>(2, (self, f) => ({ + [MetricHookTypeId]: metricHookVariance, + pipe() { + return pipeArguments(this, arguments) + }, + get: self.get, + update: (input) => { + self.update(input) + return f(input) + }, + modify: self.modify +})) + +const bigint0 = BigInt(0) + +/** @internal */ +export const counter = ( + key: MetricKey.MetricKey.Counter +): MetricHook.MetricHook.Counter => { + let sum: A = key.keyType.bigint ? bigint0 as A : 0 as A + const canUpdate = key.keyType.incremental + ? key.keyType.bigint + ? (value: A) => value >= bigint0 + : (value: A) => value >= 0 + : (_value: A) => true + const update = (value: A) => { + if (canUpdate(value)) { + sum = (sum as any) + value + } + } + return make({ + get: () => metricState.counter(sum as number) as unknown as MetricState.MetricState.Counter, + update, + modify: update + }) +} + +/** @internal */ +export const frequency = (key: MetricKey.MetricKey.Frequency): MetricHook.MetricHook.Frequency => { + const values = new Map() + for (const word of key.keyType.preregisteredWords) { + values.set(word, 0) + } + const update = (word: string) => { + const slotCount = values.get(word) ?? 0 + values.set(word, slotCount + 1) + } + return make({ + get: () => metricState.frequency(values), + update, + modify: update + }) +} + +/** @internal */ +export const gauge: { + (key: MetricKey.MetricKey.Gauge, startAt: number): MetricHook.MetricHook.Gauge + (key: MetricKey.MetricKey.Gauge, startAt: bigint): MetricHook.MetricHook.Gauge +} = ( + _key: MetricKey.MetricKey.Gauge, + startAt: A +): MetricHook.MetricHook.Gauge => { + let value = startAt + return make({ + get: () => metricState.gauge(value as number) as unknown as MetricState.MetricState.Gauge, + update: (v) => { + value = v + }, + modify: (v) => { + value = (value as any) + v + } + }) +} + +/** @internal */ +export const histogram = (key: MetricKey.MetricKey.Histogram): MetricHook.MetricHook.Histogram => { + const bounds = key.keyType.boundaries.values + const size = bounds.length + const values = new Uint32Array(size + 1) + // NOTE: while 64-bit floating point precision shoule be enough for any + // practical histogram boundary values, there is still a small chance that + // precision will be lost with very large / very small numbers. If we find + // that is the case, a more complex approach storing the histogram boundary + // values as a tuple of `[original: string, numeric: number]` may be warranted + const boundaries = new Float64Array(size) + let count = 0 + let sum = 0 + let min = Number.MAX_VALUE + let max = Number.MIN_VALUE + + pipe( + bounds, + Arr.sort(number.Order), + Arr.map((n, i) => { + boundaries[i] = n + }) + ) + + // Insert the value into the right bucket with a binary search + const update = (value: number) => { + let from = 0 + let to = size + while (from !== to) { + const mid = Math.floor(from + (to - from) / 2) + const boundary = boundaries[mid] + if (value <= boundary) { + to = mid + } else { + from = mid + } + // The special case when to / from have a distance of one + if (to === from + 1) { + if (value <= boundaries[from]) { + to = from + } else { + from = to + } + } + } + values[from] = values[from]! + 1 + count = count + 1 + sum = sum + value + if (value < min) { + min = value + } + if (value > max) { + max = value + } + } + + const getBuckets = (): ReadonlyArray => { + const builder: Array = Arr.allocate(size) as any + let cumulated = 0 + for (let i = 0; i < size; i++) { + const boundary = boundaries[i] + const value = values[i] + cumulated = cumulated + value + builder[i] = [boundary, cumulated] + } + return builder + } + + return make({ + get: () => + metricState.histogram({ + buckets: getBuckets(), + count, + min, + max, + sum + }), + update, + modify: update + }) +} + +/** @internal */ +export const summary = (key: MetricKey.MetricKey.Summary): MetricHook.MetricHook.Summary => { + const { error, maxAge, maxSize, quantiles } = key.keyType + const sortedQuantiles = pipe(quantiles, Arr.sort(number.Order)) + const values = Arr.allocate(maxSize) + + let head = 0 + let count = 0 + let sum = 0 + let min = 0 + let max = 0 + + // Just before the snapshot we filter out all values older than maxAge + const snapshot = (now: number): ReadonlyArray]> => { + const builder: Array = [] + // If the buffer is not full yet it contains valid items at the 0..last + // indices and null values at the rest of the positions. + // + // If the buffer is already full then all elements contains a valid + // measurement with timestamp. + // + // At any given point in time we can enumerate all the non-null elements in + // the buffer and filter them by timestamp to get a valid view of a time + // window. + // + // The order does not matter because it gets sorted before passing to + // `calculateQuantiles`. + let i = 0 + while (i !== maxSize - 1) { + const item = values[i] + if (item != null) { + const [t, v] = item + const age = Duration.millis(now - t) + if (Duration.greaterThanOrEqualTo(age, Duration.zero) && Duration.lessThanOrEqualTo(age, maxAge)) { + builder.push(v) + } + } + i = i + 1 + } + return calculateQuantiles( + error, + sortedQuantiles, + Arr.sort(builder, number.Order) + ) + } + + const observe = (value: number, timestamp: number) => { + if (maxSize > 0) { + head = head + 1 + const target = head % maxSize + values[target] = [timestamp, value] as const + } + + min = count === 0 ? value : Math.min(min, value) + max = count === 0 ? value : Math.max(max, value) + + count = count + 1 + sum = sum + value + } + + return make({ + get: () => + metricState.summary({ + error, + quantiles: snapshot(Date.now()), + count, + min, + max, + sum + }), + update: ([value, timestamp]) => observe(value, timestamp), + modify: ([value, timestamp]) => observe(value, timestamp) + }) +} + +/** @internal */ +interface ResolvedQuantile { + /** + * The quantile that shall be resolved. + */ + readonly quantile: number + /** + * `Some` if a value for the quantile could be found, otherwise + * `None`. + */ + readonly value: Option.Option + /** + * How many samples have been consumed prior to this quantile. + */ + readonly consumed: number + /** + * The rest of the samples after the quantile has been resolved. + */ + readonly rest: ReadonlyArray +} + +/** @internal */ +const calculateQuantiles = ( + error: number, + sortedQuantiles: ReadonlyArray, + sortedSamples: ReadonlyArray +): ReadonlyArray]> => { + // The number of samples examined + const sampleCount = sortedSamples.length + if (!Arr.isNonEmptyReadonlyArray(sortedQuantiles)) { + return Arr.empty() + } + const head = sortedQuantiles[0] + const tail = sortedQuantiles.slice(1) + const resolvedHead = resolveQuantile( + error, + sampleCount, + Option.none(), + 0, + head, + sortedSamples + ) + const resolved = Arr.of(resolvedHead) + tail.forEach((quantile) => { + resolved.push( + resolveQuantile( + error, + sampleCount, + resolvedHead.value, + resolvedHead.consumed, + quantile, + resolvedHead.rest + ) + ) + }) + return Arr.map(resolved, (rq) => [rq.quantile, rq.value] as const) +} + +/** @internal */ +const resolveQuantile = ( + error: number, + sampleCount: number, + current: Option.Option, + consumed: number, + quantile: number, + rest: ReadonlyArray +): ResolvedQuantile => { + let error_1 = error + let sampleCount_1 = sampleCount + let current_1 = current + let consumed_1 = consumed + let quantile_1 = quantile + let rest_1 = rest + let error_2 = error + let sampleCount_2 = sampleCount + let current_2 = current + let consumed_2 = consumed + let quantile_2 = quantile + let rest_2 = rest + // eslint-disable-next-line no-constant-condition + while (1) { + // If the remaining list of samples is empty, there is nothing more to resolve + if (!Arr.isNonEmptyReadonlyArray(rest_1)) { + return { + quantile: quantile_1, + value: Option.none(), + consumed: consumed_1, + rest: [] + } + } + // If the quantile is the 100% quantile, we can take the maximum of all the + // remaining values as the result + if (quantile_1 === 1) { + return { + quantile: quantile_1, + value: Option.some(Arr.lastNonEmpty(rest_1)), + consumed: consumed_1 + rest_1.length, + rest: [] + } + } + // Split into two chunks - the first chunk contains all elements of the same + // value as the chunk head + const headValue = Arr.headNonEmpty(rest_1) // Get head value since rest_1 is non-empty + const sameHead = Arr.span(rest_1, (n) => n === headValue) + // How many elements do we want to accept for this quantile + const desired = quantile_1 * sampleCount_1 + // The error margin + const allowedError = (error_1 / 2) * desired + // Taking into account the elements consumed from the samples so far and the + // number of same elements at the beginning of the chunk, calculate the number + // of elements we would have if we selected the current head as result + const candConsumed = consumed_1 + sameHead[0].length + const candError = Math.abs(candConsumed - desired) + // If we haven't got enough elements yet, recurse + if (candConsumed < desired - allowedError) { + error_2 = error_1 + sampleCount_2 = sampleCount_1 + current_2 = Arr.head(rest_1) + consumed_2 = candConsumed + quantile_2 = quantile_1 + rest_2 = sameHead[1] + error_1 = error_2 + sampleCount_1 = sampleCount_2 + current_1 = current_2 + consumed_1 = consumed_2 + quantile_1 = quantile_2 + rest_1 = rest_2 + continue + } + // If consuming this chunk leads to too many elements (rank is too high) + if (candConsumed > desired + allowedError) { + const valueToReturn = Option.isNone(current_1) + ? Option.some(headValue) + : current_1 + return { + quantile: quantile_1, + value: valueToReturn, + consumed: consumed_1, + rest: rest_1 + } + } + // If we are in the target interval, select the current head and hand back the leftover after dropping all elements + // from the sample chunk that are equal to the current head + switch (current_1._tag) { + case "None": { + error_2 = error_1 + sampleCount_2 = sampleCount_1 + current_2 = Arr.head(rest_1) + consumed_2 = candConsumed + quantile_2 = quantile_1 + rest_2 = sameHead[1] + error_1 = error_2 + sampleCount_1 = sampleCount_2 + current_1 = current_2 + consumed_1 = consumed_2 + quantile_1 = quantile_2 + rest_1 = rest_2 + continue + } + case "Some": { + const prevError = Math.abs(desired - current_1.value) + if (candError < prevError) { + error_2 = error_1 + sampleCount_2 = sampleCount_1 + current_2 = Arr.head(rest_1) + consumed_2 = candConsumed + quantile_2 = quantile_1 + rest_2 = sameHead[1] + error_1 = error_2 + sampleCount_1 = sampleCount_2 + current_1 = current_2 + consumed_1 = consumed_2 + quantile_1 = quantile_2 + rest_1 = rest_2 + continue + } + return { + quantile: quantile_1, + value: Option.some(current_1.value), + consumed: consumed_1, + rest: rest_1 + } + } + } + } + throw new Error( + "BUG: MetricHook.resolveQuantiles - please report an issue at https://github.com/Effect-TS/effect/issues" + ) +} diff --git a/repos/effect/packages/effect/src/internal/metric/key.ts b/repos/effect/packages/effect/src/internal/metric/key.ts new file mode 100644 index 0000000..50f4439 --- /dev/null +++ b/repos/effect/packages/effect/src/internal/metric/key.ts @@ -0,0 +1,167 @@ +import * as Arr from "../../Array.js" +import type * as Duration from "../../Duration.js" +import * as Equal from "../../Equal.js" +import { dual, pipe } from "../../Function.js" +import * as Hash from "../../Hash.js" +import type * as MetricBoundaries from "../../MetricBoundaries.js" +import type * as MetricKey from "../../MetricKey.js" +import type * as MetricKeyType from "../../MetricKeyType.js" +import type * as MetricLabel from "../../MetricLabel.js" +import * as Option from "../../Option.js" +import { pipeArguments } from "../../Pipeable.js" +import { hasProperty } from "../../Predicate.js" +import * as metricKeyType from "./keyType.js" +import * as metricLabel from "./label.js" + +/** @internal */ +const MetricKeySymbolKey = "effect/MetricKey" + +/** @internal */ +export const MetricKeyTypeId: MetricKey.MetricKeyTypeId = Symbol.for( + MetricKeySymbolKey +) as MetricKey.MetricKeyTypeId + +const metricKeyVariance = { + /* c8 ignore next */ + _Type: (_: never) => _ +} + +const arrayEquivilence = Arr.getEquivalence(Equal.equals) + +/** @internal */ +class MetricKeyImpl> implements MetricKey.MetricKey { + readonly [MetricKeyTypeId] = metricKeyVariance + constructor( + readonly name: string, + readonly keyType: Type, + readonly description: Option.Option, + readonly tags: ReadonlyArray = [] + ) { + this._hash = pipe( + Hash.string(this.name + this.description), + Hash.combine(Hash.hash(this.keyType)), + Hash.combine(Hash.array(this.tags)) + ) + } + readonly _hash: number; + [Hash.symbol](): number { + return this._hash + } + [Equal.symbol](u: unknown): boolean { + return isMetricKey(u) && + this.name === u.name && + Equal.equals(this.keyType, u.keyType) && + Equal.equals(this.description, u.description) && + arrayEquivilence(this.tags, u.tags) + } + pipe() { + return pipeArguments(this, arguments) + } +} + +/** @internal */ +export const isMetricKey = (u: unknown): u is MetricKey.MetricKey> => + hasProperty(u, MetricKeyTypeId) + +/** @internal */ +export const counter: { + (name: string, options?: { + readonly description?: string | undefined + readonly bigint?: false | undefined + readonly incremental?: boolean | undefined + }): MetricKey.MetricKey.Counter + (name: string, options: { + readonly description?: string | undefined + readonly bigint: true + readonly incremental?: boolean | undefined + }): MetricKey.MetricKey.Counter +} = (name: string, options) => + new MetricKeyImpl( + name, + metricKeyType.counter(options as any), + Option.fromNullable(options?.description) + ) + +/** @internal */ +export const frequency = (name: string, options?: { + readonly description?: string | undefined + readonly preregisteredWords?: ReadonlyArray | undefined +}): MetricKey.MetricKey.Frequency => + new MetricKeyImpl(name, metricKeyType.frequency(options), Option.fromNullable(options?.description)) + +/** @internal */ +export const gauge: { + (name: string, options?: { + readonly description?: string | undefined + readonly bigint?: false | undefined + }): MetricKey.MetricKey.Gauge + (name: string, options: { + readonly description?: string | undefined + readonly bigint: true + }): MetricKey.MetricKey.Gauge +} = (name, options) => + new MetricKeyImpl( + name, + metricKeyType.gauge(options as any), + Option.fromNullable(options?.description) + ) + +/** @internal */ +export const histogram = ( + name: string, + boundaries: MetricBoundaries.MetricBoundaries, + description?: string +): MetricKey.MetricKey.Histogram => + new MetricKeyImpl( + name, + metricKeyType.histogram(boundaries), + Option.fromNullable(description) + ) + +/** @internal */ +export const summary = ( + options: { + readonly name: string + readonly maxAge: Duration.DurationInput + readonly maxSize: number + readonly error: number + readonly quantiles: ReadonlyArray + readonly description?: string | undefined + } +): MetricKey.MetricKey.Summary => + new MetricKeyImpl( + options.name, + metricKeyType.summary(options), + Option.fromNullable(options.description) + ) + +/** @internal */ +export const tagged = dual< + ( + key: string, + value: string + ) => >( + self: MetricKey.MetricKey + ) => MetricKey.MetricKey, + >( + self: MetricKey.MetricKey, + key: string, + value: string + ) => MetricKey.MetricKey +>(3, (self, key, value) => taggedWithLabels(self, [metricLabel.make(key, value)])) + +/** @internal */ +export const taggedWithLabels = dual< + ( + extraTags: ReadonlyArray + ) => >( + self: MetricKey.MetricKey + ) => MetricKey.MetricKey, + >( + self: MetricKey.MetricKey, + extraTags: ReadonlyArray + ) => MetricKey.MetricKey +>(2, (self, extraTags) => + extraTags.length === 0 + ? self + : new MetricKeyImpl(self.name, self.keyType, self.description, Arr.union(self.tags, extraTags))) diff --git a/repos/effect/packages/effect/src/internal/metric/keyType.ts b/repos/effect/packages/effect/src/internal/metric/keyType.ts new file mode 100644 index 0000000..4757565 --- /dev/null +++ b/repos/effect/packages/effect/src/internal/metric/keyType.ts @@ -0,0 +1,238 @@ +import * as Duration from "../../Duration.js" +import * as Equal from "../../Equal.js" +import { pipe } from "../../Function.js" +import * as Hash from "../../Hash.js" +import type * as MetricBoundaries from "../../MetricBoundaries.js" +import type * as MetricKeyType from "../../MetricKeyType.js" +import { pipeArguments } from "../../Pipeable.js" +import { hasProperty } from "../../Predicate.js" + +/** @internal */ +const MetricKeyTypeSymbolKey = "effect/MetricKeyType" + +/** @internal */ +export const MetricKeyTypeTypeId: MetricKeyType.MetricKeyTypeTypeId = Symbol.for( + MetricKeyTypeSymbolKey +) as MetricKeyType.MetricKeyTypeTypeId + +/** @internal */ +const CounterKeyTypeSymbolKey = "effect/MetricKeyType/Counter" + +/** @internal */ +export const CounterKeyTypeTypeId: MetricKeyType.CounterKeyTypeTypeId = Symbol.for( + CounterKeyTypeSymbolKey +) as MetricKeyType.CounterKeyTypeTypeId + +/** @internal */ +const FrequencyKeyTypeSymbolKey = "effect/MetricKeyType/Frequency" + +/** @internal */ +export const FrequencyKeyTypeTypeId: MetricKeyType.FrequencyKeyTypeTypeId = Symbol.for( + FrequencyKeyTypeSymbolKey +) as MetricKeyType.FrequencyKeyTypeTypeId + +/** @internal */ +const GaugeKeyTypeSymbolKey = "effect/MetricKeyType/Gauge" + +/** @internal */ +export const GaugeKeyTypeTypeId: MetricKeyType.GaugeKeyTypeTypeId = Symbol.for( + GaugeKeyTypeSymbolKey +) as MetricKeyType.GaugeKeyTypeTypeId + +/** @internal */ +const HistogramKeyTypeSymbolKey = "effect/MetricKeyType/Histogram" + +/** @internal */ +export const HistogramKeyTypeTypeId: MetricKeyType.HistogramKeyTypeTypeId = Symbol.for( + HistogramKeyTypeSymbolKey +) as MetricKeyType.HistogramKeyTypeTypeId + +/** @internal */ +const SummaryKeyTypeSymbolKey = "effect/MetricKeyType/Summary" + +/** @internal */ +export const SummaryKeyTypeTypeId: MetricKeyType.SummaryKeyTypeTypeId = Symbol.for( + SummaryKeyTypeSymbolKey +) as MetricKeyType.SummaryKeyTypeTypeId + +const metricKeyTypeVariance = { + /* c8 ignore next */ + _In: (_: unknown) => _, + /* c8 ignore next */ + _Out: (_: never) => _ +} + +/** @internal */ +class CounterKeyType implements MetricKeyType.MetricKeyType.Counter { + readonly [MetricKeyTypeTypeId] = metricKeyTypeVariance + readonly [CounterKeyTypeTypeId]: MetricKeyType.CounterKeyTypeTypeId = CounterKeyTypeTypeId + constructor(readonly incremental: boolean, readonly bigint: boolean) { + this._hash = Hash.string(CounterKeyTypeSymbolKey) + } + readonly _hash: number; + [Hash.symbol](): number { + return this._hash + } + [Equal.symbol](that: unknown): boolean { + return isCounterKey(that) + } + pipe() { + return pipeArguments(this, arguments) + } +} + +const FrequencyKeyTypeHash = Hash.string(FrequencyKeyTypeSymbolKey) + +/** @internal */ +class FrequencyKeyType implements MetricKeyType.MetricKeyType.Frequency { + readonly [MetricKeyTypeTypeId] = metricKeyTypeVariance + readonly [FrequencyKeyTypeTypeId]: MetricKeyType.FrequencyKeyTypeTypeId = FrequencyKeyTypeTypeId + constructor(readonly preregisteredWords: ReadonlyArray) {} + [Hash.symbol](): number { + return FrequencyKeyTypeHash + } + [Equal.symbol](that: unknown): boolean { + return isFrequencyKey(that) + } + pipe() { + return pipeArguments(this, arguments) + } +} + +const GaugeKeyTypeHash = Hash.string(GaugeKeyTypeSymbolKey) + +/** @internal */ +class GaugeKeyType implements MetricKeyType.MetricKeyType.Gauge { + readonly [MetricKeyTypeTypeId] = metricKeyTypeVariance + readonly [GaugeKeyTypeTypeId]: MetricKeyType.GaugeKeyTypeTypeId = GaugeKeyTypeTypeId + constructor(readonly bigint: boolean) {} + [Hash.symbol](): number { + return GaugeKeyTypeHash + } + [Equal.symbol](that: unknown): boolean { + return isGaugeKey(that) + } + pipe() { + return pipeArguments(this, arguments) + } +} + +/** @internal */ +export class HistogramKeyType implements MetricKeyType.MetricKeyType.Histogram { + readonly [MetricKeyTypeTypeId] = metricKeyTypeVariance + readonly [HistogramKeyTypeTypeId]: MetricKeyType.HistogramKeyTypeTypeId = HistogramKeyTypeTypeId + constructor(readonly boundaries: MetricBoundaries.MetricBoundaries) { + this._hash = pipe( + Hash.string(HistogramKeyTypeSymbolKey), + Hash.combine(Hash.hash(this.boundaries)) + ) + } + readonly _hash: number; + [Hash.symbol](): number { + return this._hash + } + [Equal.symbol](that: unknown): boolean { + return isHistogramKey(that) && Equal.equals(this.boundaries, that.boundaries) + } + pipe() { + return pipeArguments(this, arguments) + } +} + +/** @internal */ +class SummaryKeyType implements MetricKeyType.MetricKeyType.Summary { + readonly [MetricKeyTypeTypeId] = metricKeyTypeVariance + readonly [SummaryKeyTypeTypeId]: MetricKeyType.SummaryKeyTypeTypeId = SummaryKeyTypeTypeId + constructor( + readonly maxAge: Duration.Duration, + readonly maxSize: number, + readonly error: number, + readonly quantiles: ReadonlyArray + ) { + this._hash = pipe( + Hash.string(SummaryKeyTypeSymbolKey), + Hash.combine(Hash.hash(this.maxAge)), + Hash.combine(Hash.hash(this.maxSize)), + Hash.combine(Hash.hash(this.error)), + Hash.combine(Hash.array(this.quantiles)) + ) + } + readonly _hash: number; + [Hash.symbol](): number { + return this._hash + } + [Equal.symbol](that: unknown): boolean { + return isSummaryKey(that) && + Equal.equals(this.maxAge, that.maxAge) && + this.maxSize === that.maxSize && + this.error === that.error && + Equal.equals(this.quantiles, that.quantiles) + } + pipe() { + return pipeArguments(this, arguments) + } +} + +/** @internal */ +export const counter: (options?: { + readonly bigint: boolean + readonly incremental: boolean +}) => CounterKeyType = (options) => + new CounterKeyType( + options?.incremental ?? false, + options?.bigint ?? false + ) + +/** @internal */ +export const frequency = (options?: { + readonly preregisteredWords?: ReadonlyArray | undefined +}): MetricKeyType.MetricKeyType.Frequency => new FrequencyKeyType(options?.preregisteredWords ?? []) + +/** @internal */ +export const gauge: (options?: { + readonly bigint: boolean +}) => GaugeKeyType = (options) => + new GaugeKeyType( + options?.bigint ?? false + ) + +/** @internal */ +export const histogram = (boundaries: MetricBoundaries.MetricBoundaries): MetricKeyType.MetricKeyType.Histogram => { + return new HistogramKeyType(boundaries) +} + +/** @internal */ +export const summary = ( + options: { + readonly maxAge: Duration.DurationInput + readonly maxSize: number + readonly error: number + readonly quantiles: ReadonlyArray + } +): MetricKeyType.MetricKeyType.Summary => { + return new SummaryKeyType(Duration.decode(options.maxAge), options.maxSize, options.error, options.quantiles) +} + +/** @internal */ +export const isMetricKeyType = (u: unknown): u is MetricKeyType.MetricKeyType => + hasProperty(u, MetricKeyTypeTypeId) + +/** @internal */ +export const isCounterKey = (u: unknown): u is MetricKeyType.MetricKeyType.Counter => + hasProperty(u, CounterKeyTypeTypeId) + +/** @internal */ +export const isFrequencyKey = (u: unknown): u is MetricKeyType.MetricKeyType.Frequency => + hasProperty(u, FrequencyKeyTypeTypeId) + +/** @internal */ +export const isGaugeKey = (u: unknown): u is MetricKeyType.MetricKeyType.Gauge => + hasProperty(u, GaugeKeyTypeTypeId) + +/** @internal */ +export const isHistogramKey = (u: unknown): u is MetricKeyType.MetricKeyType.Histogram => + hasProperty(u, HistogramKeyTypeTypeId) + +/** @internal */ +export const isSummaryKey = (u: unknown): u is MetricKeyType.MetricKeyType.Summary => + hasProperty(u, SummaryKeyTypeTypeId) diff --git a/repos/effect/packages/effect/src/internal/metric/label.ts b/repos/effect/packages/effect/src/internal/metric/label.ts new file mode 100644 index 0000000..35a63f2 --- /dev/null +++ b/repos/effect/packages/effect/src/internal/metric/label.ts @@ -0,0 +1,41 @@ +import * as Equal from "../../Equal.js" +import * as Hash from "../../Hash.js" +import type * as MetricLabel from "../../MetricLabel.js" +import { pipeArguments } from "../../Pipeable.js" +import { hasProperty } from "../../Predicate.js" + +/** @internal */ +const MetricLabelSymbolKey = "effect/MetricLabel" + +/** @internal */ +export const MetricLabelTypeId: MetricLabel.MetricLabelTypeId = Symbol.for( + MetricLabelSymbolKey +) as MetricLabel.MetricLabelTypeId + +/** @internal */ +class MetricLabelImpl implements MetricLabel.MetricLabel { + readonly [MetricLabelTypeId]: MetricLabel.MetricLabelTypeId = MetricLabelTypeId + readonly _hash: number + constructor(readonly key: string, readonly value: string) { + this._hash = Hash.string(MetricLabelSymbolKey + this.key + this.value) + } + [Hash.symbol](): number { + return this._hash + } + [Equal.symbol](that: unknown): boolean { + return isMetricLabel(that) && + this.key === that.key && + this.value === that.value + } + pipe() { + return pipeArguments(this, arguments) + } +} + +/** @internal */ +export const make = (key: string, value: string): MetricLabel.MetricLabel => { + return new MetricLabelImpl(key, value) +} + +/** @internal */ +export const isMetricLabel = (u: unknown): u is MetricLabel.MetricLabel => hasProperty(u, MetricLabelTypeId) diff --git a/repos/effect/packages/effect/src/internal/metric/pair.ts b/repos/effect/packages/effect/src/internal/metric/pair.ts new file mode 100644 index 0000000..7df92e8 --- /dev/null +++ b/repos/effect/packages/effect/src/internal/metric/pair.ts @@ -0,0 +1,48 @@ +import type * as MetricKey from "../../MetricKey.js" +import type * as MetricKeyType from "../../MetricKeyType.js" +import type * as MetricPair from "../../MetricPair.js" +import type * as MetricState from "../../MetricState.js" +import { pipeArguments } from "../../Pipeable.js" + +/** @internal */ +const MetricPairSymbolKey = "effect/MetricPair" + +/** @internal */ +export const MetricPairTypeId: MetricPair.MetricPairTypeId = Symbol.for( + MetricPairSymbolKey +) as MetricPair.MetricPairTypeId + +const metricPairVariance = { + /* c8 ignore next */ + _Type: (_: never) => _ +} + +/** @internal */ +export const make = >( + metricKey: MetricKey.MetricKey, + metricState: MetricState.MetricState> +): MetricPair.MetricPair.Untyped => { + return { + [MetricPairTypeId]: metricPairVariance, + metricKey, + metricState, + pipe() { + return pipeArguments(this, arguments) + } + } +} + +/** @internal */ +export const unsafeMake = >( + metricKey: MetricKey.MetricKey, + metricState: MetricState.MetricState.Untyped +): MetricPair.MetricPair.Untyped => { + return { + [MetricPairTypeId]: metricPairVariance, + metricKey, + metricState, + pipe() { + return pipeArguments(this, arguments) + } + } +} diff --git a/repos/effect/packages/effect/src/internal/metric/polling.ts b/repos/effect/packages/effect/src/internal/metric/polling.ts new file mode 100644 index 0000000..14d4890 --- /dev/null +++ b/repos/effect/packages/effect/src/internal/metric/polling.ts @@ -0,0 +1,149 @@ +import type * as Effect from "../../Effect.js" +import type * as Fiber from "../../Fiber.js" +import { dual, pipe } from "../../Function.js" +import type * as Metric from "../../Metric.js" +import type * as MetricPolling from "../../MetricPolling.js" +import { pipeArguments } from "../../Pipeable.js" +import type * as Schedule from "../../Schedule.js" +import type * as Scope from "../../Scope.js" +import * as core from "../core.js" +import * as metric from "../metric.js" +import * as schedule_ from "../schedule.js" + +/** @internal */ +const MetricPollingSymbolKey = "effect/MetricPolling" + +/** @internal */ +export const MetricPollingTypeId: MetricPolling.MetricPollingTypeId = Symbol.for( + MetricPollingSymbolKey +) as MetricPolling.MetricPollingTypeId + +/** @internal */ +export const make = ( + metric: Metric.Metric, + poll: Effect.Effect +): MetricPolling.MetricPolling => { + return { + [MetricPollingTypeId]: MetricPollingTypeId, + pipe() { + return pipeArguments(this, arguments) + }, + metric, + poll + } +} + +/** @internal */ +export const collectAll = ( + iterable: Iterable> +): MetricPolling.MetricPolling, Array, R, E, Array> => { + const metrics = Array.from(iterable) + return { + [MetricPollingTypeId]: MetricPollingTypeId, + pipe() { + return pipeArguments(this, arguments) + }, + metric: metric.make( + Array.of(void 0) as Array, + (inputs: Array, extraTags) => { + for (let i = 0; i < inputs.length; i++) { + const pollingMetric = metrics[i]! + const input = pipe(inputs, (x) => x[i]) + pollingMetric.metric.unsafeUpdate(input, extraTags) + } + }, + (extraTags) => + Array.from( + metrics.map((pollingMetric) => pollingMetric.metric.unsafeValue(extraTags)) + ), + (inputs: Array, extraTags) => { + for (let i = 0; i < inputs.length; i++) { + const pollingMetric = metrics[i]! + const input = pipe(inputs, (x) => x[i]) + pollingMetric.metric.unsafeModify(input, extraTags) + } + } + ), + poll: core.forEachSequential(metrics, (metric) => metric.poll) + } +} + +/** @internal */ +export const launch = dual< + ( + schedule: Schedule.Schedule + ) => ( + self: MetricPolling.MetricPolling + ) => Effect.Effect, never, R | R2 | Scope.Scope>, + ( + self: MetricPolling.MetricPolling, + schedule: Schedule.Schedule + ) => Effect.Effect, never, R | R2 | Scope.Scope> +>(2, (self, schedule) => + pipe( + pollAndUpdate(self), + core.zipRight(metric.value(self.metric)), + schedule_.scheduleForked(schedule) + )) + +/** @internal */ +export const poll = ( + self: MetricPolling.MetricPolling +): Effect.Effect => self.poll + +/** @internal */ +export const pollAndUpdate = ( + self: MetricPolling.MetricPolling +): Effect.Effect => core.flatMap(self.poll, (value) => metric.update(self.metric, value)) + +/** @internal */ +export const retry = dual< + ( + policy: Schedule.Schedule + ) => ( + self: MetricPolling.MetricPolling + ) => MetricPolling.MetricPolling, + ( + self: MetricPolling.MetricPolling, + policy: Schedule.Schedule + ) => MetricPolling.MetricPolling +>(2, (self, policy) => ({ + [MetricPollingTypeId]: MetricPollingTypeId, + pipe() { + return pipeArguments(this, arguments) + }, + metric: self.metric, + poll: schedule_.retry_Effect(self.poll, policy) +})) + +/** @internal */ +export const zip = dual< + ( + that: MetricPolling.MetricPolling + ) => ( + self: MetricPolling.MetricPolling + ) => MetricPolling.MetricPolling< + readonly [Type, Type2], + readonly [In, In2], + R | R2, + E | E2, + [Out, Out2] + >, + ( + self: MetricPolling.MetricPolling, + that: MetricPolling.MetricPolling + ) => MetricPolling.MetricPolling< + readonly [Type, Type2], + readonly [In, In2], + R | R2, + E | E2, + [Out, Out2] + > +>(2, (self, that) => ({ + [MetricPollingTypeId]: MetricPollingTypeId, + pipe() { + return pipeArguments(this, arguments) + }, + metric: pipe(self.metric, metric.zip(that.metric)), + poll: core.zip(self.poll, that.poll) +})) diff --git a/repos/effect/packages/effect/src/internal/metric/registry.ts b/repos/effect/packages/effect/src/internal/metric/registry.ts new file mode 100644 index 0000000..4417aca --- /dev/null +++ b/repos/effect/packages/effect/src/internal/metric/registry.ts @@ -0,0 +1,187 @@ +import { pipe } from "../../Function.js" +import type * as MetricHook from "../../MetricHook.js" +import type * as MetricKey from "../../MetricKey.js" +import type * as MetricKeyType from "../../MetricKeyType.js" +import type * as MetricPair from "../../MetricPair.js" +import type * as MetricRegistry from "../../MetricRegistry.js" +import * as MutableHashMap from "../../MutableHashMap.js" +import * as Option from "../../Option.js" +import * as metricHook from "./hook.js" +import * as metricKeyType from "./keyType.js" +import * as metricPair from "./pair.js" + +/** @internal */ +const MetricRegistrySymbolKey = "effect/MetricRegistry" + +/** @internal */ +export const MetricRegistryTypeId: MetricRegistry.MetricRegistryTypeId = Symbol.for( + MetricRegistrySymbolKey +) as MetricRegistry.MetricRegistryTypeId + +/** @internal */ +class MetricRegistryImpl implements MetricRegistry.MetricRegistry { + readonly [MetricRegistryTypeId]: MetricRegistry.MetricRegistryTypeId = MetricRegistryTypeId + + private map = MutableHashMap.empty< + MetricKey.MetricKey, + MetricHook.MetricHook.Root + >() + + snapshot(): Array { + const result: Array = [] + for (const [key, hook] of this.map) { + result.push(metricPair.unsafeMake(key, hook.get())) + } + return result + } + + get>( + key: MetricKey.MetricKey + ): MetricHook.MetricHook< + MetricKeyType.MetricKeyType.InType, + MetricKeyType.MetricKeyType.OutType + > { + const hook = pipe( + this.map, + MutableHashMap.get(key as MetricKey.MetricKey), + Option.getOrUndefined + ) + if (hook == null) { + if (metricKeyType.isCounterKey(key.keyType)) { + return this.getCounter(key as unknown as MetricKey.MetricKey.Counter) as any + } + if (metricKeyType.isGaugeKey(key.keyType)) { + return this.getGauge(key as unknown as MetricKey.MetricKey.Gauge) as any + } + if (metricKeyType.isFrequencyKey(key.keyType)) { + return this.getFrequency(key as unknown as MetricKey.MetricKey.Frequency) as any + } + if (metricKeyType.isHistogramKey(key.keyType)) { + return this.getHistogram(key as unknown as MetricKey.MetricKey.Histogram) as any + } + if (metricKeyType.isSummaryKey(key.keyType)) { + return this.getSummary(key as unknown as MetricKey.MetricKey.Summary) as any + } + throw new Error( + "BUG: MetricRegistry.get - unknown MetricKeyType - please report an issue at https://github.com/Effect-TS/effect/issues" + ) + } else { + return hook as any + } + } + + getCounter(key: MetricKey.MetricKey.Counter): MetricHook.MetricHook.Counter { + let value = pipe( + this.map, + MutableHashMap.get(key as MetricKey.MetricKey), + Option.getOrUndefined + ) + if (value == null) { + const counter = metricHook.counter(key) + if (!pipe(this.map, MutableHashMap.has(key as MetricKey.MetricKey))) { + pipe( + this.map, + MutableHashMap.set( + key as MetricKey.MetricKey, + counter as MetricHook.MetricHook.Root + ) + ) + } + value = counter + } + return value as MetricHook.MetricHook.Counter + } + + getFrequency(key: MetricKey.MetricKey.Frequency): MetricHook.MetricHook.Frequency { + let value = pipe( + this.map, + MutableHashMap.get(key as MetricKey.MetricKey), + Option.getOrUndefined + ) + if (value == null) { + const frequency = metricHook.frequency(key) + if (!pipe(this.map, MutableHashMap.has(key as MetricKey.MetricKey))) { + pipe( + this.map, + MutableHashMap.set( + key as MetricKey.MetricKey, + frequency as MetricHook.MetricHook.Root + ) + ) + } + value = frequency + } + return value as MetricHook.MetricHook.Frequency + } + + getGauge(key: MetricKey.MetricKey.Gauge): MetricHook.MetricHook.Gauge { + let value = pipe( + this.map, + MutableHashMap.get(key as MetricKey.MetricKey), + Option.getOrUndefined + ) + if (value == null) { + const gauge = metricHook.gauge(key as any, key.keyType.bigint ? BigInt(0) as any : 0) + if (!pipe(this.map, MutableHashMap.has(key as MetricKey.MetricKey))) { + pipe( + this.map, + MutableHashMap.set( + key as MetricKey.MetricKey, + gauge as MetricHook.MetricHook.Root + ) + ) + } + value = gauge + } + return value as MetricHook.MetricHook.Gauge + } + + getHistogram(key: MetricKey.MetricKey.Histogram): MetricHook.MetricHook.Histogram { + let value = pipe( + this.map, + MutableHashMap.get(key as MetricKey.MetricKey), + Option.getOrUndefined + ) + if (value == null) { + const histogram = metricHook.histogram(key) + if (!pipe(this.map, MutableHashMap.has(key as MetricKey.MetricKey))) { + pipe( + this.map, + MutableHashMap.set( + key as MetricKey.MetricKey, + histogram as MetricHook.MetricHook.Root + ) + ) + } + value = histogram + } + return value as MetricHook.MetricHook.Histogram + } + + getSummary(key: MetricKey.MetricKey.Summary): MetricHook.MetricHook.Summary { + let value = pipe( + this.map, + MutableHashMap.get(key as MetricKey.MetricKey), + Option.getOrUndefined + ) + if (value == null) { + const summary = metricHook.summary(key) + if (!pipe(this.map, MutableHashMap.has(key as MetricKey.MetricKey))) { + pipe( + this.map, + MutableHashMap.set( + key as MetricKey.MetricKey, + summary as MetricHook.MetricHook.Root + ) + ) + } + value = summary + } + return value as MetricHook.MetricHook.Summary + } +} + +/** @internal */ +export const make = (): MetricRegistry.MetricRegistry => { + return new MetricRegistryImpl() +} diff --git a/repos/effect/packages/effect/src/internal/metric/state.ts b/repos/effect/packages/effect/src/internal/metric/state.ts new file mode 100644 index 0000000..aff3621 --- /dev/null +++ b/repos/effect/packages/effect/src/internal/metric/state.ts @@ -0,0 +1,290 @@ +import * as Arr from "../../Array.js" +import * as Equal from "../../Equal.js" +import { pipe } from "../../Function.js" +import * as Hash from "../../Hash.js" +import type * as MetricState from "../../MetricState.js" +import type * as Option from "../../Option.js" +import { pipeArguments } from "../../Pipeable.js" +import { hasProperty } from "../../Predicate.js" + +/** @internal */ +const MetricStateSymbolKey = "effect/MetricState" + +/** @internal */ +export const MetricStateTypeId: MetricState.MetricStateTypeId = Symbol.for( + MetricStateSymbolKey +) as MetricState.MetricStateTypeId + +/** @internal */ +const CounterStateSymbolKey = "effect/MetricState/Counter" + +/** @internal */ +export const CounterStateTypeId: MetricState.CounterStateTypeId = Symbol.for( + CounterStateSymbolKey +) as MetricState.CounterStateTypeId + +/** @internal */ +const FrequencyStateSymbolKey = "effect/MetricState/Frequency" + +/** @internal */ +export const FrequencyStateTypeId: MetricState.FrequencyStateTypeId = Symbol.for( + FrequencyStateSymbolKey +) as MetricState.FrequencyStateTypeId + +/** @internal */ +const GaugeStateSymbolKey = "effect/MetricState/Gauge" + +/** @internal */ +export const GaugeStateTypeId: MetricState.GaugeStateTypeId = Symbol.for( + GaugeStateSymbolKey +) as MetricState.GaugeStateTypeId + +/** @internal */ +const HistogramStateSymbolKey = "effect/MetricState/Histogram" + +/** @internal */ +export const HistogramStateTypeId: MetricState.HistogramStateTypeId = Symbol.for( + HistogramStateSymbolKey +) as MetricState.HistogramStateTypeId + +/** @internal */ +const SummaryStateSymbolKey = "effect/MetricState/Summary" + +/** @internal */ +export const SummaryStateTypeId: MetricState.SummaryStateTypeId = Symbol.for( + SummaryStateSymbolKey +) as MetricState.SummaryStateTypeId + +const metricStateVariance = { + /* c8 ignore next */ + _A: (_: unknown) => _ +} + +/** @internal */ +class CounterState implements MetricState.MetricState.Counter { + readonly [MetricStateTypeId] = metricStateVariance + readonly [CounterStateTypeId]: MetricState.CounterStateTypeId = CounterStateTypeId + constructor(readonly count: A) {} + [Hash.symbol](): number { + return pipe( + Hash.hash(CounterStateSymbolKey), + Hash.combine(Hash.hash(this.count)), + Hash.cached(this) + ) + } + [Equal.symbol](that: unknown): boolean { + return isCounterState(that) && this.count === that.count + } + pipe() { + return pipeArguments(this, arguments) + } +} + +const arrayEquals = Arr.getEquivalence(Equal.equals) + +/** @internal */ +class FrequencyState implements MetricState.MetricState.Frequency { + readonly [MetricStateTypeId] = metricStateVariance + readonly [FrequencyStateTypeId]: MetricState.FrequencyStateTypeId = FrequencyStateTypeId + constructor(readonly occurrences: ReadonlyMap) {} + _hash: number | undefined; + [Hash.symbol](): number { + return pipe( + Hash.string(FrequencyStateSymbolKey), + Hash.combine(Hash.array(Arr.fromIterable(this.occurrences.entries()))), + Hash.cached(this) + ) + } + [Equal.symbol](that: unknown): boolean { + return isFrequencyState(that) && arrayEquals( + Arr.fromIterable(this.occurrences.entries()), + Arr.fromIterable(that.occurrences.entries()) + ) + } + pipe() { + return pipeArguments(this, arguments) + } +} + +/** @internal */ +class GaugeState implements MetricState.MetricState.Gauge { + readonly [MetricStateTypeId] = metricStateVariance + readonly [GaugeStateTypeId]: MetricState.GaugeStateTypeId = GaugeStateTypeId + constructor(readonly value: A) {} + [Hash.symbol](): number { + return pipe( + Hash.hash(GaugeStateSymbolKey), + Hash.combine(Hash.hash(this.value)), + Hash.cached(this) + ) + } + [Equal.symbol](u: unknown): boolean { + return isGaugeState(u) && this.value === u.value + } + pipe() { + return pipeArguments(this, arguments) + } +} + +/** @internal */ +export class HistogramState implements MetricState.MetricState.Histogram { + readonly [MetricStateTypeId] = metricStateVariance + readonly [HistogramStateTypeId]: MetricState.HistogramStateTypeId = HistogramStateTypeId + constructor( + readonly buckets: ReadonlyArray, + readonly count: number, + readonly min: number, + readonly max: number, + readonly sum: number + ) {} + [Hash.symbol](): number { + return pipe( + Hash.hash(HistogramStateSymbolKey), + Hash.combine(Hash.hash(this.buckets)), + Hash.combine(Hash.hash(this.count)), + Hash.combine(Hash.hash(this.min)), + Hash.combine(Hash.hash(this.max)), + Hash.combine(Hash.hash(this.sum)), + Hash.cached(this) + ) + } + [Equal.symbol](that: unknown): boolean { + return isHistogramState(that) && + Equal.equals(this.buckets, that.buckets) && + this.count === that.count && + this.min === that.min && + this.max === that.max && + this.sum === that.sum + } + pipe() { + return pipeArguments(this, arguments) + } +} + +/** @internal */ +export class SummaryState implements MetricState.MetricState.Summary { + readonly [MetricStateTypeId] = metricStateVariance + readonly [SummaryStateTypeId]: MetricState.SummaryStateTypeId = SummaryStateTypeId + constructor( + readonly error: number, + readonly quantiles: ReadonlyArray]>, + readonly count: number, + readonly min: number, + readonly max: number, + readonly sum: number + ) {} + [Hash.symbol](): number { + return pipe( + Hash.hash(SummaryStateSymbolKey), + Hash.combine(Hash.hash(this.error)), + Hash.combine(Hash.hash(this.quantiles)), + Hash.combine(Hash.hash(this.count)), + Hash.combine(Hash.hash(this.min)), + Hash.combine(Hash.hash(this.max)), + Hash.combine(Hash.hash(this.sum)), + Hash.cached(this) + ) + } + [Equal.symbol](that: unknown): boolean { + return isSummaryState(that) && + this.error === that.error && + Equal.equals(this.quantiles, that.quantiles) && + this.count === that.count && + this.min === that.min && + this.max === that.max && + this.sum === that.sum + } + pipe() { + return pipeArguments(this, arguments) + } +} + +/** @internal */ +export const counter: { + (count: number): MetricState.MetricState.Counter + (count: bigint): MetricState.MetricState.Counter +} = (count) => new CounterState(count) as any + +/** @internal */ +export const frequency = (occurrences: ReadonlyMap): MetricState.MetricState.Frequency => { + return new FrequencyState(occurrences) +} + +/** @internal */ +export const gauge: { + (count: number): MetricState.MetricState.Gauge + (count: bigint): MetricState.MetricState.Gauge +} = (count) => new GaugeState(count) as any + +/** @internal */ +export const histogram = ( + options: { + readonly buckets: ReadonlyArray + readonly count: number + readonly min: number + readonly max: number + readonly sum: number + } +): MetricState.MetricState.Histogram => + new HistogramState( + options.buckets, + options.count, + options.min, + options.max, + options.sum + ) + +/** @internal */ +export const summary = ( + options: { + readonly error: number + readonly quantiles: ReadonlyArray]> + readonly count: number + readonly min: number + readonly max: number + readonly sum: number + } +): MetricState.MetricState.Summary => + new SummaryState( + options.error, + options.quantiles, + options.count, + options.min, + options.max, + options.sum + ) + +/** @internal */ +export const isMetricState = (u: unknown): u is MetricState.MetricState.Counter => + hasProperty(u, MetricStateTypeId) + +/** @internal */ +export const isCounterState = (u: unknown): u is MetricState.MetricState.Counter => + hasProperty(u, CounterStateTypeId) + +/** + * @since 2.0.0 + * @category refinements + */ +export const isFrequencyState = (u: unknown): u is MetricState.MetricState.Frequency => + hasProperty(u, FrequencyStateTypeId) + +/** + * @since 2.0.0 + * @category refinements + */ +export const isGaugeState = (u: unknown): u is MetricState.MetricState.Gauge => + hasProperty(u, GaugeStateTypeId) + +/** + * @since 2.0.0 + * @category refinements + */ +export const isHistogramState = (u: unknown): u is MetricState.MetricState.Histogram => + hasProperty(u, HistogramStateTypeId) + +/** + * @since 2.0.0 + * @category refinements + */ +export const isSummaryState = (u: unknown): u is MetricState.MetricState.Summary => hasProperty(u, SummaryStateTypeId) diff --git a/repos/effect/packages/effect/src/internal/opCodes/cause.ts b/repos/effect/packages/effect/src/internal/opCodes/cause.ts new file mode 100644 index 0000000..b8c1d85 --- /dev/null +++ b/repos/effect/packages/effect/src/internal/opCodes/cause.ts @@ -0,0 +1,35 @@ +/** @internal */ +export const OP_DIE = "Die" as const + +/** @internal */ +export type OP_DIE = typeof OP_DIE + +/** @internal */ +export const OP_EMPTY = "Empty" as const + +/** @internal */ +export type OP_EMPTY = typeof OP_EMPTY + +/** @internal */ +export const OP_FAIL = "Fail" as const + +/** @internal */ +export type OP_FAIL = typeof OP_FAIL + +/** @internal */ +export const OP_INTERRUPT = "Interrupt" as const + +/** @internal */ +export type OP_INTERRUPT = typeof OP_INTERRUPT + +/** @internal */ +export const OP_PARALLEL = "Parallel" as const + +/** @internal */ +export type OP_PARALLEL = typeof OP_PARALLEL + +/** @internal */ +export const OP_SEQUENTIAL = "Sequential" as const + +/** @internal */ +export type OP_SEQUENTIAL = typeof OP_SEQUENTIAL diff --git a/repos/effect/packages/effect/src/internal/opCodes/channel.ts b/repos/effect/packages/effect/src/internal/opCodes/channel.ts new file mode 100644 index 0000000..1a5a3f0 --- /dev/null +++ b/repos/effect/packages/effect/src/internal/opCodes/channel.ts @@ -0,0 +1,83 @@ +/** @internal */ +export const OP_BRACKET_OUT = "BracketOut" as const + +/** @internal */ +export type OP_BRACKET_OUT = typeof OP_BRACKET_OUT + +/** @internal */ +export const OP_BRIDGE = "Bridge" as const + +/** @internal */ +export type OP_BRIDGE = typeof OP_BRIDGE + +/** @internal */ +export const OP_CONCAT_ALL = "ConcatAll" as const + +/** @internal */ +export type OP_CONCAT_ALL = typeof OP_CONCAT_ALL + +/** @internal */ +export const OP_EMIT = "Emit" as const + +/** @internal */ +export type OP_EMIT = typeof OP_EMIT + +/** @internal */ +export const OP_ENSURING = "Ensuring" as const + +/** @internal */ +export type OP_ENSURING = typeof OP_ENSURING + +/** @internal */ +export const OP_FAIL = "Fail" as const + +/** @internal */ +export type OP_FAIL = typeof OP_FAIL + +/** @internal */ +export const OP_FOLD = "Fold" as const + +/** @internal */ +export type OP_FOLD = typeof OP_FOLD + +/** @internal */ +export const OP_FROM_EFFECT = "FromEffect" as const + +/** @internal */ +export type OP_FROM_EFFECT = typeof OP_FROM_EFFECT + +/** @internal */ +export const OP_PIPE_TO = "PipeTo" as const + +/** @internal */ +export type OP_PIPE_TO = typeof OP_PIPE_TO + +/** @internal */ +export const OP_PROVIDE = "Provide" as const + +/** @internal */ +export type OP_PROVIDE = typeof OP_PROVIDE + +/** @internal */ +export const OP_READ = "Read" as const + +/** @internal */ +export type OP_READ = typeof OP_READ + +/** @internal */ +export const OP_SUCCEED = "Succeed" as const + +/** @internal */ +export type OP_SUCCEED = typeof OP_SUCCEED + +/** @internal */ +export const OP_SUCCEED_NOW = "SucceedNow" as const + +/** @internal */ +export type OP_SUCCEED_NOW = typeof OP_SUCCEED_NOW + +/** @internal */ +export const OP_SUSPEND = "Suspend" as const + +/** @internal */ +export type OP_SUSPEND = typeof OP_SUSPEND diff --git a/repos/effect/packages/effect/src/internal/opCodes/channelChildExecutorDecision.ts b/repos/effect/packages/effect/src/internal/opCodes/channelChildExecutorDecision.ts new file mode 100644 index 0000000..2c78ac7 --- /dev/null +++ b/repos/effect/packages/effect/src/internal/opCodes/channelChildExecutorDecision.ts @@ -0,0 +1,17 @@ +/** @internal */ +export const OP_CONTINUE = "Continue" as const + +/** @internal */ +export type OP_CONTINUE = typeof OP_CONTINUE + +/** @internal */ +export const OP_CLOSE = "Close" as const + +/** @internal */ +export type OP_CLOSE = typeof OP_CLOSE + +/** @internal */ +export const OP_YIELD = "Yield" as const + +/** @internal */ +export type OP_YIELD = typeof OP_YIELD diff --git a/repos/effect/packages/effect/src/internal/opCodes/channelMergeDecision.ts b/repos/effect/packages/effect/src/internal/opCodes/channelMergeDecision.ts new file mode 100644 index 0000000..e6ea508 --- /dev/null +++ b/repos/effect/packages/effect/src/internal/opCodes/channelMergeDecision.ts @@ -0,0 +1,11 @@ +/** @internal */ +export const OP_DONE = "Done" as const + +/** @internal */ +export type OP_DONE = typeof OP_DONE + +/** @internal */ +export const OP_AWAIT = "Await" as const + +/** @internal */ +export type OP_AWAIT = typeof OP_AWAIT diff --git a/repos/effect/packages/effect/src/internal/opCodes/channelMergeState.ts b/repos/effect/packages/effect/src/internal/opCodes/channelMergeState.ts new file mode 100644 index 0000000..7f02521 --- /dev/null +++ b/repos/effect/packages/effect/src/internal/opCodes/channelMergeState.ts @@ -0,0 +1,17 @@ +/** @internal */ +export const OP_BOTH_RUNNING = "BothRunning" as const + +/** @internal */ +export type OP_BOTH_RUNNING = typeof OP_BOTH_RUNNING + +/** @internal */ +export const OP_LEFT_DONE = "LeftDone" as const + +/** @internal */ +export type OP_LEFT_DONE = typeof OP_LEFT_DONE + +/** @internal */ +export const OP_RIGHT_DONE = "RightDone" as const + +/** @internal */ +export type OP_RIGHT_DONE = typeof OP_RIGHT_DONE diff --git a/repos/effect/packages/effect/src/internal/opCodes/channelMergeStrategy.ts b/repos/effect/packages/effect/src/internal/opCodes/channelMergeStrategy.ts new file mode 100644 index 0000000..9555082 --- /dev/null +++ b/repos/effect/packages/effect/src/internal/opCodes/channelMergeStrategy.ts @@ -0,0 +1,11 @@ +/** @internal */ +export const OP_BACK_PRESSURE = "BackPressure" as const + +/** @internal */ +export type OP_BACK_PRESSURE = typeof OP_BACK_PRESSURE + +/** @internal */ +export const OP_BUFFER_SLIDING = "BufferSliding" as const + +/** @internal */ +export type OP_BUFFER_SLIDING = typeof OP_BUFFER_SLIDING diff --git a/repos/effect/packages/effect/src/internal/opCodes/channelState.ts b/repos/effect/packages/effect/src/internal/opCodes/channelState.ts new file mode 100644 index 0000000..b5930e8 --- /dev/null +++ b/repos/effect/packages/effect/src/internal/opCodes/channelState.ts @@ -0,0 +1,23 @@ +/** @internal */ +export const OP_DONE = "Done" as const + +/** @internal */ +export type OP_DONE = typeof OP_DONE + +/** @internal */ +export const OP_EMIT = "Emit" as const + +/** @internal */ +export type OP_EMIT = typeof OP_EMIT + +/** @internal */ +export const OP_FROM_EFFECT = "FromEffect" as const + +/** @internal */ +export type OP_FROM_EFFECT = typeof OP_FROM_EFFECT + +/** @internal */ +export const OP_READ = "Read" as const + +/** @internal */ +export type OP_READ = typeof OP_READ diff --git a/repos/effect/packages/effect/src/internal/opCodes/channelUpstreamPullRequest.ts b/repos/effect/packages/effect/src/internal/opCodes/channelUpstreamPullRequest.ts new file mode 100644 index 0000000..2ba8d28 --- /dev/null +++ b/repos/effect/packages/effect/src/internal/opCodes/channelUpstreamPullRequest.ts @@ -0,0 +1,11 @@ +/** @internal */ +export const OP_PULLED = "Pulled" as const + +/** @internal */ +export type OP_PULLED = typeof OP_PULLED + +/** @internal */ +export const OP_NO_UPSTREAM = "NoUpstream" as const + +/** @internal */ +export type OP_NO_UPSTREAM = typeof OP_NO_UPSTREAM diff --git a/repos/effect/packages/effect/src/internal/opCodes/channelUpstreamPullStrategy.ts b/repos/effect/packages/effect/src/internal/opCodes/channelUpstreamPullStrategy.ts new file mode 100644 index 0000000..621c98e --- /dev/null +++ b/repos/effect/packages/effect/src/internal/opCodes/channelUpstreamPullStrategy.ts @@ -0,0 +1,11 @@ +/** @internal */ +export const OP_PULL_AFTER_NEXT = "PullAfterNext" as const + +/** @internal */ +export type OP_PULL_AFTER_NEXT = typeof OP_PULL_AFTER_NEXT + +/** @internal */ +export const OP_PULL_AFTER_ALL_ENQUEUED = "PullAfterAllEnqueued" as const + +/** @internal */ +export type OP_PULL_AFTER_ALL_ENQUEUED = typeof OP_PULL_AFTER_ALL_ENQUEUED diff --git a/repos/effect/packages/effect/src/internal/opCodes/config.ts b/repos/effect/packages/effect/src/internal/opCodes/config.ts new file mode 100644 index 0000000..55ca44e --- /dev/null +++ b/repos/effect/packages/effect/src/internal/opCodes/config.ts @@ -0,0 +1,65 @@ +/** @internal */ +export type OP_CONSTANT = typeof OP_CONSTANT + +/** @internal */ +export const OP_CONSTANT = "Constant" as const + +/** @internal */ +export type OP_FAIL = typeof OP_FAIL + +/** @internal */ +export const OP_FAIL = "Fail" as const + +/** @internal */ +export type OP_FALLBACK = typeof OP_FALLBACK + +/** @internal */ +export const OP_FALLBACK = "Fallback" as const + +/** @internal */ +export type OP_DESCRIBED = typeof OP_DESCRIBED + +/** @internal */ +export const OP_DESCRIBED = "Described" as const + +/** @internal */ +export type OP_LAZY = typeof OP_LAZY + +/** @internal */ +export const OP_LAZY = "Lazy" as const + +/** @internal */ +export type OP_MAP_OR_FAIL = typeof OP_MAP_OR_FAIL + +/** @internal */ +export const OP_MAP_OR_FAIL = "MapOrFail" as const + +/** @internal */ +export type OP_NESTED = typeof OP_NESTED + +/** @internal */ +export const OP_NESTED = "Nested" as const + +/** @internal */ +export type OP_PRIMITIVE = typeof OP_PRIMITIVE + +/** @internal */ +export const OP_PRIMITIVE = "Primitive" as const + +/** @internal */ +export type OP_SEQUENCE = typeof OP_SEQUENCE + +/** @internal */ +export const OP_SEQUENCE = "Sequence" as const + +/** @internal */ +export type OP_HASHMAP = typeof OP_HASHMAP + +/** @internal */ +export const OP_HASHMAP = "HashMap" as const + +/** @internal */ +export type OP_ZIP_WITH = typeof OP_ZIP_WITH + +/** @internal */ +export const OP_ZIP_WITH = "ZipWith" as const diff --git a/repos/effect/packages/effect/src/internal/opCodes/configError.ts b/repos/effect/packages/effect/src/internal/opCodes/configError.ts new file mode 100644 index 0000000..e61b9f3 --- /dev/null +++ b/repos/effect/packages/effect/src/internal/opCodes/configError.ts @@ -0,0 +1,35 @@ +/** @internal */ +export type OP_AND = typeof OP_AND + +/** @internal */ +export const OP_AND = "And" as const + +/** @internal */ +export type OP_OR = typeof OP_OR + +/** @internal */ +export const OP_OR = "Or" as const + +/** @internal */ +export type OP_INVALID_DATA = typeof OP_INVALID_DATA + +/** @internal */ +export const OP_INVALID_DATA = "InvalidData" as const + +/** @internal */ +export type OP_MISSING_DATA = typeof OP_MISSING_DATA + +/** @internal */ +export const OP_MISSING_DATA = "MissingData" as const + +/** @internal */ +export type OP_SOURCE_UNAVAILABLE = typeof OP_SOURCE_UNAVAILABLE + +/** @internal */ +export const OP_SOURCE_UNAVAILABLE = "SourceUnavailable" as const + +/** @internal */ +export type OP_UNSUPPORTED = typeof OP_UNSUPPORTED + +/** @internal */ +export const OP_UNSUPPORTED = "Unsupported" as const diff --git a/repos/effect/packages/effect/src/internal/opCodes/continuation.ts b/repos/effect/packages/effect/src/internal/opCodes/continuation.ts new file mode 100644 index 0000000..77e4def --- /dev/null +++ b/repos/effect/packages/effect/src/internal/opCodes/continuation.ts @@ -0,0 +1,11 @@ +/** @internal */ +export const OP_CONTINUATION_K = "ContinuationK" as const + +/** @internal */ +export type OP_CONTINUATION_K = typeof OP_CONTINUATION_K + +/** @internal */ +export const OP_CONTINUATION_FINALIZER = "ContinuationFinalizer" as const + +/** @internal */ +export type OP_CONTINUATION_FINALIZER = typeof OP_CONTINUATION_FINALIZER diff --git a/repos/effect/packages/effect/src/internal/opCodes/deferred.ts b/repos/effect/packages/effect/src/internal/opCodes/deferred.ts new file mode 100644 index 0000000..d0622fd --- /dev/null +++ b/repos/effect/packages/effect/src/internal/opCodes/deferred.ts @@ -0,0 +1,11 @@ +/** @internal */ +export const OP_STATE_PENDING = "Pending" as const + +/** @internal */ +export type OP_STATE_PENDING = typeof OP_STATE_PENDING + +/** @internal */ +export const OP_STATE_DONE = "Done" as const + +/** @internal */ +export type OP_STATE_DONE = typeof OP_STATE_DONE diff --git a/repos/effect/packages/effect/src/internal/opCodes/effect.ts b/repos/effect/packages/effect/src/internal/opCodes/effect.ts new file mode 100644 index 0000000..84731a0 --- /dev/null +++ b/repos/effect/packages/effect/src/internal/opCodes/effect.ts @@ -0,0 +1,89 @@ +/** @internal */ +export type OP_ASYNC = typeof OP_ASYNC + +/** @internal */ +export const OP_ASYNC = "Async" as const + +/** @internal */ +export type OP_COMMIT = typeof OP_COMMIT + +/** @internal */ +export const OP_COMMIT = "Commit" as const + +/** @internal */ +export type OP_FAILURE = typeof OP_FAILURE + +/** @internal */ +export const OP_FAILURE = "Failure" as const + +/** @internal */ +export type OP_ON_FAILURE = typeof OP_ON_FAILURE + +/** @internal */ +export const OP_ON_FAILURE = "OnFailure" as const + +/** @internal */ +export type OP_ON_SUCCESS = typeof OP_ON_SUCCESS + +/** @internal */ +export const OP_ON_SUCCESS = "OnSuccess" as const + +/** @internal */ +export type OP_ON_SUCCESS_AND_FAILURE = typeof OP_ON_SUCCESS_AND_FAILURE + +/** @internal */ +export const OP_ON_SUCCESS_AND_FAILURE = "OnSuccessAndFailure" as const + +/** @internal */ +export type OP_SUCCESS = typeof OP_SUCCESS + +/** @internal */ +export const OP_SUCCESS = "Success" as const + +/** @internal */ +export type OP_SYNC = typeof OP_SYNC + +/** @internal */ +export const OP_SYNC = "Sync" as const + +/** @internal */ +export const OP_TAG = "Tag" as const + +/** @internal */ +export type OP_TAG = typeof OP_TAG + +/** @internal */ +export type OP_UPDATE_RUNTIME_FLAGS = typeof OP_UPDATE_RUNTIME_FLAGS + +/** @internal */ +export const OP_UPDATE_RUNTIME_FLAGS = "UpdateRuntimeFlags" as const + +/** @internal */ +export type OP_WHILE = typeof OP_WHILE + +/** @internal */ +export const OP_WHILE = "While" as const + +/** @internal */ +export type OP_ITERATOR = typeof OP_ITERATOR + +/** @internal */ +export const OP_ITERATOR = "Iterator" as const + +/** @internal */ +export type OP_WITH_RUNTIME = typeof OP_WITH_RUNTIME + +/** @internal */ +export const OP_WITH_RUNTIME = "WithRuntime" as const + +/** @internal */ +export type OP_YIELD = typeof OP_YIELD + +/** @internal */ +export const OP_YIELD = "Yield" as const + +/** @internal */ +export type OP_REVERT_FLAGS = typeof OP_REVERT_FLAGS + +/** @internal */ +export const OP_REVERT_FLAGS = "RevertFlags" as const diff --git a/repos/effect/packages/effect/src/internal/opCodes/layer.ts b/repos/effect/packages/effect/src/internal/opCodes/layer.ts new file mode 100644 index 0000000..690b9cb --- /dev/null +++ b/repos/effect/packages/effect/src/internal/opCodes/layer.ts @@ -0,0 +1,59 @@ +/** @internal */ +export const OP_EXTEND_SCOPE = "ExtendScope" as const + +/** @internal */ +export type OP_EXTEND_SCOPE = typeof OP_EXTEND_SCOPE + +/** @internal */ +export const OP_FOLD = "Fold" as const + +/** @internal */ +export type OP_FOLD = typeof OP_FOLD + +/** @internal */ +export const OP_FRESH = "Fresh" as const + +/** @internal */ +export type OP_FRESH = typeof OP_FRESH + +/** @internal */ +export const OP_FROM_EFFECT = "FromEffect" as const + +/** @internal */ +export type OP_FROM_EFFECT = typeof OP_FROM_EFFECT + +/** @internal */ +export const OP_SCOPED = "Scoped" as const + +/** @internal */ +export type OP_SCOPED = typeof OP_SCOPED + +/** @internal */ +export const OP_SUSPEND = "Suspend" as const + +/** @internal */ +export type OP_SUSPEND = typeof OP_SUSPEND + +/** @internal */ +export const OP_PROVIDE = "Provide" as const + +/** @internal */ +export type OP_PROVIDE = typeof OP_PROVIDE + +/** @internal */ +export const OP_PROVIDE_MERGE = "ProvideMerge" as const + +/** @internal */ +export type OP_PROVIDE_MERGE = typeof OP_PROVIDE_MERGE + +/** @internal */ +export const OP_MERGE_ALL = "MergeAll" as const + +/** @internal */ +export type OP_MERGE_ALL = typeof OP_MERGE_ALL + +/** @internal */ +export const OP_ZIP_WITH = "ZipWith" as const + +/** @internal */ +export type OP_ZIP_WITH = typeof OP_ZIP_WITH diff --git a/repos/effect/packages/effect/src/internal/opCodes/streamHaltStrategy.ts b/repos/effect/packages/effect/src/internal/opCodes/streamHaltStrategy.ts new file mode 100644 index 0000000..d92d530 --- /dev/null +++ b/repos/effect/packages/effect/src/internal/opCodes/streamHaltStrategy.ts @@ -0,0 +1,23 @@ +/** @internal */ +export const OP_LEFT = "Left" as const + +/** @internal */ +export type OP_LEFT = typeof OP_LEFT + +/** @internal */ +export const OP_RIGHT = "Right" as const + +/** @internal */ +export type OP_RIGHT = typeof OP_RIGHT + +/** @internal */ +export const OP_BOTH = "Both" as const + +/** @internal */ +export type OP_BOTH = typeof OP_BOTH + +/** @internal */ +export const OP_EITHER = "Either" as const + +/** @internal */ +export type OP_EITHER = typeof OP_EITHER diff --git a/repos/effect/packages/effect/src/internal/option.ts b/repos/effect/packages/effect/src/internal/option.ts new file mode 100644 index 0000000..7d97ade --- /dev/null +++ b/repos/effect/packages/effect/src/internal/option.ts @@ -0,0 +1,80 @@ +/** + * @since 2.0.0 + */ + +import * as Equal from "../Equal.js" +import * as Hash from "../Hash.js" +import { format, NodeInspectSymbol, toJSON } from "../Inspectable.js" +import type * as Option from "../Option.js" +import { hasProperty } from "../Predicate.js" +import { EffectPrototype } from "./effectable.js" + +const TypeId: Option.TypeId = Symbol.for("effect/Option") as Option.TypeId + +const CommonProto = { + ...EffectPrototype, + [TypeId]: { + _A: (_: never) => _ + }, + [NodeInspectSymbol](this: Option.Option) { + return this.toJSON() + }, + toString(this: Option.Option) { + return format(this.toJSON()) + } +} + +const SomeProto = Object.assign(Object.create(CommonProto), { + _tag: "Some", + _op: "Some", + [Equal.symbol](this: Option.Some, that: unknown): boolean { + return isOption(that) && isSome(that) && Equal.equals(this.value, that.value) + }, + [Hash.symbol](this: Option.Some) { + return Hash.cached(this, Hash.combine(Hash.hash(this._tag))(Hash.hash(this.value))) + }, + toJSON(this: Option.Some) { + return { + _id: "Option", + _tag: this._tag, + value: toJSON(this.value) + } + } +}) + +const NoneHash = Hash.hash("None") +const NoneProto = Object.assign(Object.create(CommonProto), { + _tag: "None", + _op: "None", + [Equal.symbol](this: Option.None, that: unknown): boolean { + return isOption(that) && isNone(that) + }, + [Hash.symbol](this: Option.None) { + return NoneHash + }, + toJSON(this: Option.None) { + return { + _id: "Option", + _tag: this._tag + } + } +}) + +/** @internal */ +export const isOption = (input: unknown): input is Option.Option => hasProperty(input, TypeId) + +/** @internal */ +export const isNone = (fa: Option.Option): fa is Option.None => fa._tag === "None" + +/** @internal */ +export const isSome = (fa: Option.Option): fa is Option.Some => fa._tag === "Some" + +/** @internal */ +export const none: Option.Option = Object.create(NoneProto) + +/** @internal */ +export const some = (value: A): Option.Option => { + const a = Object.create(SomeProto) + a.value = value + return a +} diff --git a/repos/effect/packages/effect/src/internal/pool.ts b/repos/effect/packages/effect/src/internal/pool.ts new file mode 100644 index 0000000..98a7075 --- /dev/null +++ b/repos/effect/packages/effect/src/internal/pool.ts @@ -0,0 +1,432 @@ +import type { Cause } from "../Cause.js" +import * as Context from "../Context.js" +import * as Duration from "../Duration.js" +import type { Effect, Semaphore } from "../Effect.js" +import * as Effectable from "../Effectable.js" +import type { Exit } from "../Exit.js" +import { dual, identity } from "../Function.js" +import * as Iterable from "../Iterable.js" +import * as Option from "../Option.js" +import { pipeArguments } from "../Pipeable.js" +import type { Pool, PoolTypeId as PoolTypeId_ } from "../Pool.js" +import { hasProperty } from "../Predicate.js" +import type { Scope } from "../Scope.js" +import * as coreEffect from "./core-effect.js" +import * as core from "./core.js" +import * as defaultServices from "./defaultServices.js" +import * as circular from "./effect/circular.js" +import * as fiberRuntime from "./fiberRuntime.js" +import * as internalQueue from "./queue.js" + +/** @internal */ +export const PoolTypeId: PoolTypeId_ = Symbol.for("effect/Pool") as PoolTypeId_ + +const poolVariance = { + /* c8 ignore next */ + _E: (_: never) => _, + /* c8 ignore next */ + _A: (_: any) => _ +} + +/** @internal */ +export const isPool = (u: unknown): u is Pool => hasProperty(u, PoolTypeId) + +/** @internal */ +export const makeWith = (options: { + readonly acquire: Effect + readonly min: number + readonly max: number + readonly concurrency?: number | undefined + readonly targetUtilization?: number | undefined + readonly strategy: Strategy +}): Effect, never, Scope | R> => + core.uninterruptibleMask((restore) => + core.flatMap(core.context(), (context) => { + const scope = Context.get(context, fiberRuntime.scopeTag) + const acquire = core.mapInputContext( + options.acquire, + (input) => Context.merge(context, input) + ) as Effect< + A, + E, + Scope + > + const pool = new PoolImpl( + scope, + acquire, + options.concurrency ?? 1, + options.min, + options.max, + options.strategy, + Math.min(Math.max(options.targetUtilization ?? 1, 0.1), 1) + ) + const initialize = core.tap(fiberRuntime.forkDaemon(restore(pool.resize)), (fiber) => + scope.addFinalizer(() => core.interruptFiber(fiber))) + const runStrategy = core.tap(fiberRuntime.forkDaemon(restore(options.strategy.run(pool))), (fiber) => + scope.addFinalizer(() => + core.interruptFiber(fiber) + )) + return core.succeed(pool).pipe( + core.zipLeft(scope.addFinalizer(() => + pool.shutdown + )), + core.zipLeft(initialize), + core.zipLeft(runStrategy) + ) + }) + ) + +/** @internal */ +export const make = (options: { + readonly acquire: Effect + readonly size: number + readonly concurrency?: number | undefined + readonly targetUtilization?: number | undefined +}): Effect, never, R | Scope> => + makeWith({ ...options, min: options.size, max: options.size, strategy: strategyNoop() }) + +/** @internal */ +export const makeWithTTL = (options: { + readonly acquire: Effect + readonly min: number + readonly max: number + readonly concurrency?: number | undefined + readonly targetUtilization?: number | undefined + readonly timeToLive: Duration.DurationInput + readonly timeToLiveStrategy?: "creation" | "usage" | undefined +}): Effect, never, R | Scope> => + core.flatMap( + options.timeToLiveStrategy === "creation" ? + strategyCreationTTL(options.timeToLive) : + strategyUsageTTL(options.timeToLive), + (strategy) => makeWith({ ...options, strategy }) + ) + +/** @internal */ +export const get = (self: Pool): Effect => self.get + +/** @internal */ +export const invalidate: { + (item: A): (self: Pool) => Effect + (self: Pool, item: A): Effect +} = dual(2, (self: Pool, item: A): Effect => self.invalidate(item)) + +interface PoolItem { + readonly exit: Exit + finalizer: Effect + refCount: number + disableReclaim: boolean +} + +interface Strategy { + readonly run: (pool: PoolImpl) => Effect + readonly onAcquire: (item: PoolItem) => Effect + readonly reclaim: (pool: PoolImpl) => Effect>> +} + +class PoolImpl extends Effectable.Class implements Pool { + readonly [PoolTypeId]: Pool.Variance[PoolTypeId_] + + isShuttingDown = false + readonly semaphore: Semaphore + readonly items = new Set>() + readonly available = new Set>() + readonly availableLatch = circular.unsafeMakeLatch(false) + readonly invalidated = new Set>() + waiters = 0 + + constructor( + readonly scope: Scope, + readonly acquire: Effect, + readonly concurrency: number, + readonly minSize: number, + readonly maxSize: number, + readonly strategy: Strategy, + readonly targetUtilization: number + ) { + super() + this[PoolTypeId] = poolVariance + this.semaphore = circular.unsafeMakeSemaphore(concurrency * maxSize) + } + + readonly allocate: Effect> = core.acquireUseRelease( + fiberRuntime.scopeMake(), + (scope) => + this.acquire.pipe( + fiberRuntime.scopeExtend(scope), + core.exit, + core.flatMap((exit) => { + const item: PoolItem = { + exit, + finalizer: core.catchAllCause(scope.close(exit), reportUnhandledError), + refCount: 0, + disableReclaim: false + } + this.items.add(item) + this.available.add(item) + return core.as( + exit._tag === "Success" + ? this.strategy.onAcquire(item) + : core.zipRight(item.finalizer, this.strategy.onAcquire(item)), + item + ) + }) + ), + (scope, exit) => exit._tag === "Failure" ? scope.close(exit) : core.void + ) + + get currentUsage() { + let count = this.waiters + for (const item of this.items) { + count += item.refCount + } + return count + } + + get targetSize() { + if (this.isShuttingDown) return 0 + const utilization = this.currentUsage / this.targetUtilization + const target = Math.ceil(utilization / this.concurrency) + return Math.min(Math.max(this.minSize, target), this.maxSize) + } + + get activeSize() { + return this.items.size - this.invalidated.size + } + + readonly resizeLoop: Effect = core.suspend(() => { + if (this.activeSize >= this.targetSize) { + return core.void + } + const toAcquire = this.targetSize - this.activeSize + return this.strategy.reclaim(this).pipe( + core.flatMap(Option.match({ + onNone: () => this.allocate, + onSome: core.succeed + })), + fiberRuntime.replicateEffect(toAcquire, { concurrency: toAcquire }), + core.zipLeft(this.availableLatch.open), + core.flatMap((items) => items.some((_) => _.exit._tag === "Failure") ? core.void : this.resizeLoop) + ) + }) + readonly resizeSemaphore = circular.unsafeMakeSemaphore(1) + readonly resize = this.resizeSemaphore.withPermits(1)(this.resizeLoop) + + readonly getPoolItem: Effect, never, Scope> = core.uninterruptibleMask((restore) => + restore(this.semaphore.take(1)).pipe( + core.zipRight(fiberRuntime.scopeTag), + core.flatMap((scope) => + core.suspend(() => { + this.waiters++ + if (this.isShuttingDown) { + return core.interrupt + } else if (this.targetSize > this.activeSize) { + // eslint-disable-next-line @typescript-eslint/no-this-alias + const self = this + return core.flatMap( + this.resizeSemaphore.withPermitsIfAvailable(1)( + circular.forkIn(core.interruptible(this.resize), this.scope) + ), + function loop(): Effect> { + if (self.isShuttingDown) { + return core.interrupt + } else if (self.available.size > 0) { + return core.succeed(Iterable.unsafeHead(self.available)) + } + self.availableLatch.unsafeClose() + return core.flatMap(self.availableLatch.await, loop) + } + ) + } + return core.succeed(Iterable.unsafeHead(this.available)) + }).pipe( + fiberRuntime.ensuring(core.sync(() => this.waiters--)), + core.tap((item) => { + if (item.exit._tag === "Failure") { + this.items.delete(item) + this.invalidated.delete(item) + this.available.delete(item) + return this.semaphore.release(1) + } + item.refCount++ + this.available.delete(item) + if (item.refCount < this.concurrency) { + this.available.add(item) + } + return scope.addFinalizer(() => + core.zipRight( + core.suspend(() => { + item.refCount-- + if (this.invalidated.has(item)) { + return this.invalidatePoolItem(item) + } + this.available.add(item) + return core.exitVoid + }), + this.semaphore.release(1) + ) + ) + }), + core.onInterrupt(() => this.semaphore.release(1)) + ) + ) + ) + ) + + commit() { + return this.get + } + + readonly get: Effect = core.flatMap( + core.suspend(() => this.isShuttingDown ? core.interrupt : this.getPoolItem), + (_) => _.exit + ) + + invalidate(item: A): Effect { + return core.suspend(() => { + if (this.isShuttingDown) return core.void + for (const poolItem of this.items) { + if (poolItem.exit._tag === "Success" && poolItem.exit.value === item) { + poolItem.disableReclaim = true + return core.uninterruptible(this.invalidatePoolItem(poolItem)) + } + } + return core.void + }) + } + + invalidatePoolItem(poolItem: PoolItem): Effect { + return core.suspend(() => { + if (!this.items.has(poolItem)) { + return core.void + } else if (poolItem.refCount === 0) { + this.items.delete(poolItem) + this.available.delete(poolItem) + this.invalidated.delete(poolItem) + return core.zipRight( + poolItem.finalizer, + circular.forkIn(core.interruptible(this.resize), this.scope) + ) + } + this.invalidated.add(poolItem) + this.available.delete(poolItem) + return core.void + }) + } + + get shutdown(): Effect { + return core.suspend(() => { + if (this.isShuttingDown) return core.void + this.isShuttingDown = true + const size = this.items.size + const semaphore = circular.unsafeMakeSemaphore(size) + return core.forEachSequentialDiscard(this.items, (item) => { + if (item.refCount > 0) { + item.finalizer = core.zipLeft(item.finalizer, semaphore.release(1)) + this.invalidated.add(item) + return semaphore.take(1) + } + this.items.delete(item) + this.available.delete(item) + this.invalidated.delete(item) + return item.finalizer + }).pipe( + core.zipRight(this.semaphore.releaseAll), + core.zipRight(this.availableLatch.open), + core.zipRight(semaphore.take(size)) + ) + }) + } + + pipe() { + return pipeArguments(this, arguments) + } +} + +const strategyNoop = (): Strategy => ({ + run: (_) => core.void, + onAcquire: (_) => core.void, + reclaim: (_) => coreEffect.succeedNone +}) + +const strategyCreationTTL = (ttl: Duration.DurationInput) => + defaultServices.clockWith((clock) => + core.map(internalQueue.unbounded>(), (queue) => { + const ttlMillis = Duration.toMillis(ttl) + const creationTimes = new WeakMap, number>() + return identity>({ + run: (pool) => { + const process = (item: PoolItem): Effect => + core.suspend(() => { + if (!pool.items.has(item) || pool.invalidated.has(item)) { + return core.void + } + const now = clock.unsafeCurrentTimeMillis() + const created = creationTimes.get(item)! + const remaining = ttlMillis - (now - created) + return remaining > 0 + ? coreEffect.delay(process(item), remaining) + : pool.invalidatePoolItem(item) + }) + return queue.take.pipe( + core.tap(process), + coreEffect.forever + ) + }, + onAcquire: (item) => + core.suspend(() => { + creationTimes.set(item, clock.unsafeCurrentTimeMillis()) + return queue.offer(item) + }), + reclaim: (_) => coreEffect.succeedNone + }) + }) + ) + +const strategyUsageTTL = (ttl: Duration.DurationInput) => + core.map(internalQueue.unbounded>(), (queue) => { + return identity>({ + run: (pool) => { + const process: Effect = core.suspend(() => { + const excess = pool.activeSize - pool.targetSize + if (excess <= 0) return core.void + return queue.take.pipe( + core.tap((item) => pool.invalidatePoolItem(item)), + core.zipRight(process) + ) + }) + return process.pipe( + coreEffect.delay(ttl), + coreEffect.forever + ) + }, + onAcquire: (item) => queue.offer(item), + reclaim(pool) { + return core.suspend((): Effect>> => { + if (pool.invalidated.size === 0) { + return coreEffect.succeedNone + } + const item = Iterable.head( + Iterable.filter(pool.invalidated, (item) => !item.disableReclaim) + ) + if (item._tag === "None") { + return coreEffect.succeedNone + } + pool.invalidated.delete(item.value) + if (item.value.refCount < pool.concurrency) { + pool.available.add(item.value) + } + return core.as(queue.offer(item.value), item) + }) + } + }) + }) + +const reportUnhandledError = (cause: Cause) => + core.withFiberRuntime((fiber) => { + const unhandledLogLevel = fiber.getFiberRef(core.currentUnhandledErrorLogLevel) + if (unhandledLogLevel._tag === "Some") { + fiber.log("Unhandled error in pool finalizer", cause, unhandledLogLevel) + } + return core.void + }) diff --git a/repos/effect/packages/effect/src/internal/pubsub.ts b/repos/effect/packages/effect/src/internal/pubsub.ts new file mode 100644 index 0000000..e736c18 --- /dev/null +++ b/repos/effect/packages/effect/src/internal/pubsub.ts @@ -0,0 +1,1762 @@ +import * as Chunk from "../Chunk.js" +import type * as Deferred from "../Deferred.js" +import type * as Effect from "../Effect.js" +import * as Effectable from "../Effectable.js" +import { dual, pipe } from "../Function.js" +import * as MutableQueue from "../MutableQueue.js" +import * as MutableRef from "../MutableRef.js" +import { nextPow2 } from "../Number.js" +import * as Option from "../Option.js" +import { pipeArguments } from "../Pipeable.js" +import type * as PubSub from "../PubSub.js" +import type * as Queue from "../Queue.js" +import type * as Scope from "../Scope.js" +import * as core from "./core.js" +import * as executionStrategy from "./executionStrategy.js" +import * as fiberRuntime from "./fiberRuntime.js" +import * as queue from "./queue.js" + +const AbsentValue = Symbol.for("effect/PubSub/AbsentValue") +type AbsentValue = typeof AbsentValue + +/** @internal */ +export interface AtomicPubSub { + readonly capacity: number + isEmpty(): boolean + isFull(): boolean + size(): number + publish(value: A): boolean + publishAll(elements: Iterable): Chunk.Chunk + slide(): void + subscribe(): Subscription + replayWindow(): ReplayWindow +} + +/** @internal */ +interface Subscription { + isEmpty(): boolean + size(): number + poll(default_: D): A | D + pollUpTo(n: number): Chunk.Chunk + unsubscribe(): void +} + +/** @internal */ +type Subscribers = Map< + Subscription, + Set>> +> + +const addSubscribers = ( + subscription: Subscription, + pollers: MutableQueue.MutableQueue> +) => +(subscribers: Subscribers) => { + if (!subscribers.has(subscription)) { + subscribers.set(subscription, new Set()) + } + const set = subscribers.get(subscription)! + set.add(pollers) +} + +const removeSubscribers = ( + subscription: Subscription, + pollers: MutableQueue.MutableQueue> +) => +(subscribers: Subscribers) => { + if (!subscribers.has(subscription)) { + return + } + const set = subscribers.get(subscription)! + set.delete(pollers) + if (set.size === 0) { + subscribers.delete(subscription) + } +} + +/** @internal */ +export const bounded = ( + capacity: number | { + readonly capacity: number + readonly replay?: number | undefined + } +): Effect.Effect> => + core.suspend(() => { + const pubsub = makeBoundedPubSub(capacity) + return makePubSub(pubsub, new BackPressureStrategy()) + }) + +/** @internal */ +export const dropping = ( + capacity: number | { + readonly capacity: number + readonly replay?: number | undefined + } +): Effect.Effect> => + core.suspend(() => { + const pubsub = makeBoundedPubSub(capacity) + return makePubSub(pubsub, new DroppingStrategy()) + }) + +/** @internal */ +export const sliding = ( + capacity: number | { + readonly capacity: number + readonly replay?: number | undefined + } +): Effect.Effect> => + core.suspend(() => { + const pubsub = makeBoundedPubSub(capacity) + return makePubSub(pubsub, new SlidingStrategy()) + }) + +/** @internal */ +export const unbounded = (options?: { + readonly replay?: number | undefined +}): Effect.Effect> => + core.suspend(() => { + const pubsub = makeUnboundedPubSub(options) + return makePubSub(pubsub, new DroppingStrategy()) + }) + +/** @internal */ +export const capacity = (self: PubSub.PubSub): number => self.capacity() + +/** @internal */ +export const size = (self: PubSub.PubSub): Effect.Effect => self.size + +/** @internal */ +export const isFull = (self: PubSub.PubSub): Effect.Effect => self.isFull + +/** @internal */ +export const isEmpty = (self: PubSub.PubSub): Effect.Effect => self.isEmpty + +/** @internal */ +export const shutdown = (self: PubSub.PubSub): Effect.Effect => self.shutdown + +/** @internal */ +export const isShutdown = (self: PubSub.PubSub): Effect.Effect => self.isShutdown + +/** @internal */ +export const awaitShutdown = (self: PubSub.PubSub): Effect.Effect => self.awaitShutdown + +/** @internal */ +export const publish = dual< + (value: A) => (self: PubSub.PubSub) => Effect.Effect, + (self: PubSub.PubSub, value: A) => Effect.Effect +>(2, (self, value) => self.publish(value)) + +/** @internal */ +export const publishAll = dual< + (elements: Iterable) => (self: PubSub.PubSub) => Effect.Effect, + (self: PubSub.PubSub, elements: Iterable) => Effect.Effect +>(2, (self, elements) => self.publishAll(elements)) + +/** @internal */ +export const subscribe = (self: PubSub.PubSub): Effect.Effect, never, Scope.Scope> => + self.subscribe + +/** @internal */ +const makeBoundedPubSub = ( + capacity: number | { + readonly capacity: number + readonly replay?: number | undefined + } +): AtomicPubSub => { + const options = typeof capacity === "number" ? { capacity } : capacity + ensureCapacity(options.capacity) + const replayBuffer = options.replay && options.replay > 0 ? new ReplayBuffer(Math.ceil(options.replay)) : undefined + if (options.capacity === 1) { + return new BoundedPubSubSingle(replayBuffer) + } else if (nextPow2(options.capacity) === options.capacity) { + return new BoundedPubSubPow2(options.capacity, replayBuffer) + } else { + return new BoundedPubSubArb(options.capacity, replayBuffer) + } +} + +/** @internal */ +const makeUnboundedPubSub = (options?: { + readonly replay?: number | undefined +}): AtomicPubSub => new UnboundedPubSub(options?.replay ? new ReplayBuffer(options.replay) : undefined) + +/** @internal */ +const makeSubscription = ( + pubsub: AtomicPubSub, + subscribers: Subscribers, + strategy: PubSubStrategy +): Effect.Effect> => + core.map(core.deferredMake(), (deferred) => + unsafeMakeSubscription( + pubsub, + subscribers, + pubsub.subscribe(), + MutableQueue.unbounded>(), + deferred, + MutableRef.make(false), + strategy + )) + +/** @internal */ +export const unsafeMakeSubscription = ( + pubsub: AtomicPubSub, + subscribers: Subscribers, + subscription: Subscription, + pollers: MutableQueue.MutableQueue>, + shutdownHook: Deferred.Deferred, + shutdownFlag: MutableRef.MutableRef, + strategy: PubSubStrategy +): Queue.Dequeue => + new SubscriptionImpl( + pubsub, + subscribers, + subscription, + pollers, + shutdownHook, + shutdownFlag, + strategy, + pubsub.replayWindow() + ) + +/** @internal */ +class BoundedPubSubArb implements AtomicPubSub { + array: Array + publisherIndex = 0 + subscribers: Array + subscriberCount = 0 + subscribersIndex = 0 + + constructor(readonly capacity: number, readonly replayBuffer: ReplayBuffer | undefined) { + this.array = Array.from({ length: capacity }) + this.subscribers = Array.from({ length: capacity }) + } + + replayWindow(): ReplayWindow { + return this.replayBuffer ? new ReplayWindowImpl(this.replayBuffer) : emptyReplayWindow + } + + isEmpty(): boolean { + return this.publisherIndex === this.subscribersIndex + } + + isFull(): boolean { + return this.publisherIndex === this.subscribersIndex + this.capacity + } + + size(): number { + return this.publisherIndex - this.subscribersIndex + } + + publish(value: A): boolean { + if (this.isFull()) { + return false + } + if (this.subscriberCount !== 0) { + const index = this.publisherIndex % this.capacity + this.array[index] = value + this.subscribers[index] = this.subscriberCount + this.publisherIndex += 1 + } + if (this.replayBuffer) { + this.replayBuffer.offer(value) + } + return true + } + + publishAll(elements: Iterable): Chunk.Chunk { + if (this.subscriberCount === 0) { + if (this.replayBuffer) { + this.replayBuffer.offerAll(elements) + } + return Chunk.empty() + } + const chunk = Chunk.fromIterable(elements) + const n = chunk.length + const size = this.publisherIndex - this.subscribersIndex + const available = this.capacity - size + const forPubSub = Math.min(n, available) + if (forPubSub === 0) { + return chunk + } + let iteratorIndex = 0 + const publishAllIndex = this.publisherIndex + forPubSub + while (this.publisherIndex !== publishAllIndex) { + const a = Chunk.unsafeGet(chunk, iteratorIndex++) + const index = this.publisherIndex % this.capacity + this.array[index] = a + this.subscribers[index] = this.subscriberCount + this.publisherIndex += 1 + if (this.replayBuffer) { + this.replayBuffer.offer(a) + } + } + return Chunk.drop(chunk, iteratorIndex) + } + + slide(): void { + if (this.subscribersIndex !== this.publisherIndex) { + const index = this.subscribersIndex % this.capacity + this.array[index] = AbsentValue as unknown as A + this.subscribers[index] = 0 + this.subscribersIndex += 1 + } + if (this.replayBuffer) { + this.replayBuffer.slide() + } + } + + subscribe(): Subscription { + this.subscriberCount += 1 + return new BoundedPubSubArbSubscription(this, this.publisherIndex, false) + } +} + +class BoundedPubSubArbSubscription implements Subscription { + constructor( + private self: BoundedPubSubArb, + private subscriberIndex: number, + private unsubscribed: boolean + ) { + } + + isEmpty(): boolean { + return ( + this.unsubscribed || + this.self.publisherIndex === this.subscriberIndex || + this.self.publisherIndex === this.self.subscribersIndex + ) + } + + size() { + if (this.unsubscribed) { + return 0 + } + return this.self.publisherIndex - Math.max(this.subscriberIndex, this.self.subscribersIndex) + } + + poll(default_: D): A | D { + if (this.unsubscribed) { + return default_ + } + this.subscriberIndex = Math.max(this.subscriberIndex, this.self.subscribersIndex) + if (this.subscriberIndex !== this.self.publisherIndex) { + const index = this.subscriberIndex % this.self.capacity + const elem = this.self.array[index]! + this.self.subscribers[index] -= 1 + if (this.self.subscribers[index] === 0) { + this.self.array[index] = AbsentValue as unknown as A + this.self.subscribersIndex += 1 + } + this.subscriberIndex += 1 + return elem + } + return default_ + } + + pollUpTo(n: number): Chunk.Chunk { + if (this.unsubscribed) { + return Chunk.empty() + } + this.subscriberIndex = Math.max(this.subscriberIndex, this.self.subscribersIndex) + const size = this.self.publisherIndex - this.subscriberIndex + const toPoll = Math.min(n, size) + if (toPoll <= 0) { + return Chunk.empty() + } + const builder: Array = [] + const pollUpToIndex = this.subscriberIndex + toPoll + while (this.subscriberIndex !== pollUpToIndex) { + const index = this.subscriberIndex % this.self.capacity + const a = this.self.array[index] as A + this.self.subscribers[index] -= 1 + if (this.self.subscribers[index] === 0) { + this.self.array[index] = AbsentValue as unknown as A + this.self.subscribersIndex += 1 + } + builder.push(a) + this.subscriberIndex += 1 + } + + return Chunk.fromIterable(builder) + } + + unsubscribe(): void { + if (!this.unsubscribed) { + this.unsubscribed = true + this.self.subscriberCount -= 1 + this.subscriberIndex = Math.max(this.subscriberIndex, this.self.subscribersIndex) + while (this.subscriberIndex !== this.self.publisherIndex) { + const index = this.subscriberIndex % this.self.capacity + this.self.subscribers[index] -= 1 + if (this.self.subscribers[index] === 0) { + this.self.array[index] = AbsentValue as unknown as A + this.self.subscribersIndex += 1 + } + this.subscriberIndex += 1 + } + } + } +} + +/** @internal */ +class BoundedPubSubPow2 implements AtomicPubSub { + array: Array + mask: number + publisherIndex = 0 + subscribers: Array + subscriberCount = 0 + subscribersIndex = 0 + + constructor(readonly capacity: number, readonly replayBuffer: ReplayBuffer | undefined) { + this.array = Array.from({ length: capacity }) + this.mask = capacity - 1 + this.subscribers = Array.from({ length: capacity }) + } + + replayWindow(): ReplayWindow { + return this.replayBuffer ? new ReplayWindowImpl(this.replayBuffer) : emptyReplayWindow + } + + isEmpty(): boolean { + return this.publisherIndex === this.subscribersIndex + } + + isFull(): boolean { + return this.publisherIndex === this.subscribersIndex + this.capacity + } + + size(): number { + return this.publisherIndex - this.subscribersIndex + } + + publish(value: A): boolean { + if (this.isFull()) { + return false + } + if (this.subscriberCount !== 0) { + const index = this.publisherIndex & this.mask + this.array[index] = value + this.subscribers[index] = this.subscriberCount + this.publisherIndex += 1 + } + if (this.replayBuffer) { + this.replayBuffer.offer(value) + } + return true + } + + publishAll(elements: Iterable): Chunk.Chunk { + if (this.subscriberCount === 0) { + if (this.replayBuffer) { + this.replayBuffer.offerAll(elements) + } + return Chunk.empty() + } + const chunk = Chunk.fromIterable(elements) + const n = chunk.length + const size = this.publisherIndex - this.subscribersIndex + const available = this.capacity - size + const forPubSub = Math.min(n, available) + if (forPubSub === 0) { + return chunk + } + let iteratorIndex = 0 + const publishAllIndex = this.publisherIndex + forPubSub + while (this.publisherIndex !== publishAllIndex) { + const elem = Chunk.unsafeGet(chunk, iteratorIndex++) + const index = this.publisherIndex & this.mask + this.array[index] = elem + this.subscribers[index] = this.subscriberCount + this.publisherIndex += 1 + if (this.replayBuffer) { + this.replayBuffer.offer(elem) + } + } + return Chunk.drop(chunk, iteratorIndex) + } + + slide(): void { + if (this.subscribersIndex !== this.publisherIndex) { + const index = this.subscribersIndex & this.mask + this.array[index] = AbsentValue as unknown as A + this.subscribers[index] = 0 + this.subscribersIndex += 1 + } + if (this.replayBuffer) { + this.replayBuffer.slide() + } + } + + subscribe(): Subscription { + this.subscriberCount += 1 + return new BoundedPubSubPow2Subscription(this, this.publisherIndex, false) + } +} + +/** @internal */ +class BoundedPubSubPow2Subscription implements Subscription { + constructor( + private self: BoundedPubSubPow2, + private subscriberIndex: number, + private unsubscribed: boolean + ) { + } + + isEmpty(): boolean { + return ( + this.unsubscribed || + this.self.publisherIndex === this.subscriberIndex || + this.self.publisherIndex === this.self.subscribersIndex + ) + } + + size() { + if (this.unsubscribed) { + return 0 + } + return this.self.publisherIndex - Math.max(this.subscriberIndex, this.self.subscribersIndex) + } + + poll(default_: D): A | D { + if (this.unsubscribed) { + return default_ + } + this.subscriberIndex = Math.max(this.subscriberIndex, this.self.subscribersIndex) + if (this.subscriberIndex !== this.self.publisherIndex) { + const index = this.subscriberIndex & this.self.mask + const elem = this.self.array[index]! + this.self.subscribers[index] -= 1 + if (this.self.subscribers[index] === 0) { + this.self.array[index] = AbsentValue as unknown as A + this.self.subscribersIndex += 1 + } + this.subscriberIndex += 1 + return elem + } + return default_ + } + + pollUpTo(n: number): Chunk.Chunk { + if (this.unsubscribed) { + return Chunk.empty() + } + this.subscriberIndex = Math.max(this.subscriberIndex, this.self.subscribersIndex) + const size = this.self.publisherIndex - this.subscriberIndex + const toPoll = Math.min(n, size) + if (toPoll <= 0) { + return Chunk.empty() + } + const builder: Array = [] + const pollUpToIndex = this.subscriberIndex + toPoll + while (this.subscriberIndex !== pollUpToIndex) { + const index = this.subscriberIndex & this.self.mask + const elem = this.self.array[index] as A + this.self.subscribers[index] -= 1 + if (this.self.subscribers[index] === 0) { + this.self.array[index] = AbsentValue as unknown as A + this.self.subscribersIndex += 1 + } + builder.push(elem) + this.subscriberIndex += 1 + } + return Chunk.fromIterable(builder) + } + + unsubscribe(): void { + if (!this.unsubscribed) { + this.unsubscribed = true + this.self.subscriberCount -= 1 + this.subscriberIndex = Math.max(this.subscriberIndex, this.self.subscribersIndex) + while (this.subscriberIndex !== this.self.publisherIndex) { + const index = this.subscriberIndex & this.self.mask + this.self.subscribers[index] -= 1 + if (this.self.subscribers[index] === 0) { + this.self.array[index] = AbsentValue as unknown as A + this.self.subscribersIndex += 1 + } + this.subscriberIndex += 1 + } + } + } +} + +/** @internal */ +class BoundedPubSubSingle implements AtomicPubSub { + publisherIndex = 0 + subscriberCount = 0 + subscribers = 0 + value: A = AbsentValue as unknown as A + + readonly capacity = 1 + constructor(readonly replayBuffer: ReplayBuffer | undefined) {} + + replayWindow(): ReplayWindow { + return this.replayBuffer ? new ReplayWindowImpl(this.replayBuffer) : emptyReplayWindow + } + + pipe() { + return pipeArguments(this, arguments) + } + + isEmpty(): boolean { + return this.subscribers === 0 + } + + isFull(): boolean { + return !this.isEmpty() + } + + size(): number { + return this.isEmpty() ? 0 : 1 + } + + publish(value: A): boolean { + if (this.isFull()) { + return false + } + if (this.subscriberCount !== 0) { + this.value = value + this.subscribers = this.subscriberCount + this.publisherIndex += 1 + } + if (this.replayBuffer) { + this.replayBuffer.offer(value) + } + return true + } + + publishAll(elements: Iterable): Chunk.Chunk { + if (this.subscriberCount === 0) { + if (this.replayBuffer) { + this.replayBuffer.offerAll(elements) + } + return Chunk.empty() + } + const chunk = Chunk.fromIterable(elements) + if (Chunk.isEmpty(chunk)) { + return chunk + } + if (this.publish(Chunk.unsafeHead(chunk))) { + return Chunk.drop(chunk, 1) + } else { + return chunk + } + } + + slide(): void { + if (this.isFull()) { + this.subscribers = 0 + this.value = AbsentValue as unknown as A + } + if (this.replayBuffer) { + this.replayBuffer.slide() + } + } + + subscribe(): Subscription { + this.subscriberCount += 1 + return new BoundedPubSubSingleSubscription(this, this.publisherIndex, false) + } +} + +/** @internal */ +class BoundedPubSubSingleSubscription implements Subscription { + constructor( + private self: BoundedPubSubSingle, + private subscriberIndex: number, + private unsubscribed: boolean + ) { + } + + isEmpty(): boolean { + return ( + this.unsubscribed || + this.self.subscribers === 0 || + this.subscriberIndex === this.self.publisherIndex + ) + } + + size() { + return this.isEmpty() ? 0 : 1 + } + + poll(default_: D): A | D { + if (this.isEmpty()) { + return default_ + } + const elem = this.self.value + this.self.subscribers -= 1 + if (this.self.subscribers === 0) { + this.self.value = AbsentValue as unknown as A + } + this.subscriberIndex += 1 + return elem + } + + pollUpTo(n: number): Chunk.Chunk { + if (this.isEmpty() || n < 1) { + return Chunk.empty() + } + const a = this.self.value + this.self.subscribers -= 1 + if (this.self.subscribers === 0) { + this.self.value = AbsentValue as unknown as A + } + this.subscriberIndex += 1 + return Chunk.of(a) + } + + unsubscribe(): void { + if (!this.unsubscribed) { + this.unsubscribed = true + this.self.subscriberCount -= 1 + if (this.subscriberIndex !== this.self.publisherIndex) { + this.self.subscribers -= 1 + if (this.self.subscribers === 0) { + this.self.value = AbsentValue as unknown as A + } + } + } + } +} + +/** @internal */ +interface Node { + value: A | AbsentValue + subscribers: number + next: Node | null +} + +/** @internal */ +class UnboundedPubSub implements AtomicPubSub { + publisherHead: Node = { + value: AbsentValue, + subscribers: 0, + next: null + } + publisherTail = this.publisherHead + publisherIndex = 0 + subscribersIndex = 0 + + readonly capacity = Number.MAX_SAFE_INTEGER + constructor(readonly replayBuffer: ReplayBuffer | undefined) {} + + replayWindow(): ReplayWindow { + return this.replayBuffer ? new ReplayWindowImpl(this.replayBuffer) : emptyReplayWindow + } + + isEmpty(): boolean { + return this.publisherHead === this.publisherTail + } + + isFull(): boolean { + return false + } + + size(): number { + return this.publisherIndex - this.subscribersIndex + } + + publish(value: A): boolean { + const subscribers = this.publisherTail.subscribers + if (subscribers !== 0) { + this.publisherTail.next = { + value, + subscribers, + next: null + } + this.publisherTail = this.publisherTail.next + this.publisherIndex += 1 + } + if (this.replayBuffer) { + this.replayBuffer.offer(value) + } + return true + } + + publishAll(elements: Iterable): Chunk.Chunk { + if (this.publisherTail.subscribers !== 0) { + for (const a of elements) { + this.publish(a) + } + } else if (this.replayBuffer) { + this.replayBuffer.offerAll(elements) + } + return Chunk.empty() + } + + slide(): void { + if (this.publisherHead !== this.publisherTail) { + this.publisherHead = this.publisherHead.next! + this.publisherHead.value = AbsentValue + this.subscribersIndex += 1 + } + if (this.replayBuffer) { + this.replayBuffer.slide() + } + } + + subscribe(): Subscription { + this.publisherTail.subscribers += 1 + return new UnboundedPubSubSubscription( + this, + this.publisherTail, + this.publisherIndex, + false + ) + } +} + +/** @internal */ +class UnboundedPubSubSubscription implements Subscription { + constructor( + private self: UnboundedPubSub, + private subscriberHead: Node, + private subscriberIndex: number, + private unsubscribed: boolean + ) { + } + + isEmpty(): boolean { + if (this.unsubscribed) { + return true + } + let empty = true + let loop = true + while (loop) { + if (this.subscriberHead === this.self.publisherTail) { + loop = false + } else { + if (this.subscriberHead.next!.value !== AbsentValue) { + empty = false + loop = false + } else { + this.subscriberHead = this.subscriberHead.next! + this.subscriberIndex += 1 + } + } + } + return empty + } + + size() { + if (this.unsubscribed) { + return 0 + } + return this.self.publisherIndex - Math.max(this.subscriberIndex, this.self.subscribersIndex) + } + + poll(default_: D): A | D { + if (this.unsubscribed) { + return default_ + } + let loop = true + let polled: A | D = default_ + while (loop) { + if (this.subscriberHead === this.self.publisherTail) { + loop = false + } else { + const elem = this.subscriberHead.next!.value + if (elem !== AbsentValue) { + polled = elem + this.subscriberHead.subscribers -= 1 + if (this.subscriberHead.subscribers === 0) { + this.self.publisherHead = this.self.publisherHead.next! + this.self.publisherHead.value = AbsentValue + this.self.subscribersIndex += 1 + } + loop = false + } + this.subscriberHead = this.subscriberHead.next! + this.subscriberIndex += 1 + } + } + return polled + } + + pollUpTo(n: number): Chunk.Chunk { + const builder: Array = [] + const default_ = AbsentValue + let i = 0 + while (i !== n) { + const a = this.poll(default_ as unknown as A) + if (a === default_) { + i = n + } else { + builder.push(a) + i += 1 + } + } + return Chunk.fromIterable(builder) + } + + unsubscribe(): void { + if (!this.unsubscribed) { + this.unsubscribed = true + this.self.publisherTail.subscribers -= 1 + while (this.subscriberHead !== this.self.publisherTail) { + if (this.subscriberHead.next!.value !== AbsentValue) { + this.subscriberHead.subscribers -= 1 + if (this.subscriberHead.subscribers === 0) { + this.self.publisherHead = this.self.publisherHead.next! + this.self.publisherHead.value = AbsentValue + this.self.subscribersIndex += 1 + } + } + this.subscriberHead = this.subscriberHead.next! + } + } + } +} + +/** @internal */ +class SubscriptionImpl extends Effectable.Class implements Queue.Dequeue { + [queue.DequeueTypeId] = queue.dequeueVariance + + constructor( + readonly pubsub: AtomicPubSub, + readonly subscribers: Subscribers, + readonly subscription: Subscription, + readonly pollers: MutableQueue.MutableQueue>, + readonly shutdownHook: Deferred.Deferred, + readonly shutdownFlag: MutableRef.MutableRef, + readonly strategy: PubSubStrategy, + readonly replayWindow: ReplayWindow + ) { + super() + } + + commit() { + return this.take + } + + pipe() { + return pipeArguments(this, arguments) + } + + capacity(): number { + return this.pubsub.capacity + } + + isActive(): boolean { + return !MutableRef.get(this.shutdownFlag) + } + + get size(): Effect.Effect { + return core.suspend(() => + MutableRef.get(this.shutdownFlag) + ? core.interrupt + : core.succeed(this.subscription.size() + this.replayWindow.remaining) + ) + } + + unsafeSize(): Option.Option { + if (MutableRef.get(this.shutdownFlag)) { + return Option.none() + } + return Option.some(this.subscription.size() + this.replayWindow.remaining) + } + + get isFull(): Effect.Effect { + return core.suspend(() => + MutableRef.get(this.shutdownFlag) + ? core.interrupt + : core.succeed(this.subscription.size() === this.capacity()) + ) + } + + get isEmpty(): Effect.Effect { + return core.map(this.size, (size) => size === 0) + } + + get shutdown(): Effect.Effect { + return core.uninterruptible( + core.withFiberRuntime((state) => { + MutableRef.set(this.shutdownFlag, true) + return pipe( + fiberRuntime.forEachParUnbounded( + unsafePollAllQueue(this.pollers), + (d) => core.deferredInterruptWith(d, state.id()), + false + ), + core.zipRight(core.sync(() => { + this.subscribers.delete(this.subscription) + this.subscription.unsubscribe() + this.strategy.unsafeOnPubSubEmptySpace(this.pubsub, this.subscribers) + })), + core.whenEffect(core.deferredSucceed(this.shutdownHook, void 0)), + core.asVoid + ) + }) + ) + } + + get isShutdown(): Effect.Effect { + return core.sync(() => MutableRef.get(this.shutdownFlag)) + } + + get awaitShutdown(): Effect.Effect { + return core.deferredAwait(this.shutdownHook) + } + + get take(): Effect.Effect { + return core.withFiberRuntime((state) => { + if (MutableRef.get(this.shutdownFlag)) { + return core.interrupt + } + if (this.replayWindow.remaining > 0) { + const message = this.replayWindow.take()! + return core.succeed(message) + } + const message = MutableQueue.isEmpty(this.pollers) + ? this.subscription.poll(MutableQueue.EmptyMutableQueue) + : MutableQueue.EmptyMutableQueue + if (message === MutableQueue.EmptyMutableQueue) { + const deferred = core.deferredUnsafeMake(state.id()) + return pipe( + core.suspend(() => { + pipe(this.pollers, MutableQueue.offer(deferred)) + pipe(this.subscribers, addSubscribers(this.subscription, this.pollers)) + this.strategy.unsafeCompletePollers( + this.pubsub, + this.subscribers, + this.subscription, + this.pollers + ) + return MutableRef.get(this.shutdownFlag) ? core.interrupt : core.deferredAwait(deferred) + }), + core.onInterrupt(() => core.sync(() => unsafeRemove(this.pollers, deferred))) + ) + } else { + this.strategy.unsafeOnPubSubEmptySpace(this.pubsub, this.subscribers) + return core.succeed(message) + } + }) + } + + get takeAll(): Effect.Effect> { + return core.suspend(() => { + if (MutableRef.get(this.shutdownFlag)) { + return core.interrupt + } + const as = MutableQueue.isEmpty(this.pollers) + ? unsafePollAllSubscription(this.subscription) + : Chunk.empty() + this.strategy.unsafeOnPubSubEmptySpace(this.pubsub, this.subscribers) + if (this.replayWindow.remaining > 0) { + return core.succeed(Chunk.appendAll(this.replayWindow.takeAll(), as)) + } + return core.succeed(as) + }) + } + + takeUpTo(this: this, max: number): Effect.Effect> { + return core.suspend(() => { + if (MutableRef.get(this.shutdownFlag)) { + return core.interrupt + } + let replay: Chunk.Chunk | undefined = undefined + if (this.replayWindow.remaining >= max) { + const as = this.replayWindow.takeN(max) + return core.succeed(as) + } else if (this.replayWindow.remaining > 0) { + replay = this.replayWindow.takeAll() + max = max - replay.length + } + const as = MutableQueue.isEmpty(this.pollers) + ? unsafePollN(this.subscription, max) + : Chunk.empty() + this.strategy.unsafeOnPubSubEmptySpace(this.pubsub, this.subscribers) + return replay ? core.succeed(Chunk.appendAll(replay, as)) : core.succeed(as) + }) + } + + takeBetween(min: number, max: number): Effect.Effect> { + return core.suspend(() => takeRemainderLoop(this, min, max, Chunk.empty())) + } +} + +/** @internal */ +const takeRemainderLoop = ( + self: Queue.Dequeue, + min: number, + max: number, + acc: Chunk.Chunk +): Effect.Effect> => { + if (max < min) { + return core.succeed(acc) + } + return pipe( + self.takeUpTo(max), + core.flatMap((bs) => { + const remaining = min - bs.length + if (remaining === 1) { + return pipe(self.take, core.map((b) => pipe(acc, Chunk.appendAll(bs), Chunk.append(b)))) + } + if (remaining > 1) { + return pipe( + self.take, + core.flatMap((b) => + takeRemainderLoop( + self, + remaining - 1, + max - bs.length - 1, + pipe(acc, Chunk.appendAll(bs), Chunk.append(b)) + ) + ) + ) + } + return core.succeed(pipe(acc, Chunk.appendAll(bs))) + }) + ) +} + +/** @internal */ +class PubSubImpl implements PubSub.PubSub { + readonly [queue.EnqueueTypeId] = queue.enqueueVariance + readonly [queue.DequeueTypeId] = queue.dequeueVariance + + constructor( + readonly pubsub: AtomicPubSub, + readonly subscribers: Subscribers, + readonly scope: Scope.Scope.Closeable, + readonly shutdownHook: Deferred.Deferred, + readonly shutdownFlag: MutableRef.MutableRef, + readonly strategy: PubSubStrategy + ) {} + + capacity(): number { + return this.pubsub.capacity + } + + get size(): Effect.Effect { + return core.suspend(() => + MutableRef.get(this.shutdownFlag) ? + core.interrupt : + core.sync(() => this.pubsub.size()) + ) + } + + unsafeSize(): Option.Option { + if (MutableRef.get(this.shutdownFlag)) { + return Option.none() + } + return Option.some(this.pubsub.size()) + } + + get isFull(): Effect.Effect { + return core.map(this.size, (size) => size === this.capacity()) + } + + get isEmpty(): Effect.Effect { + return core.map(this.size, (size) => size === 0) + } + + get awaitShutdown(): Effect.Effect { + return core.deferredAwait(this.shutdownHook) + } + + get isShutdown(): Effect.Effect { + return core.sync(() => MutableRef.get(this.shutdownFlag)) + } + + get shutdown(): Effect.Effect { + return core.uninterruptible(core.withFiberRuntime((state) => { + pipe(this.shutdownFlag, MutableRef.set(true)) + return pipe( + this.scope.close(core.exitInterrupt(state.id())), + core.zipRight(this.strategy.shutdown), + core.whenEffect(core.deferredSucceed(this.shutdownHook, void 0)), + core.asVoid + ) + })) + } + + publish(value: A): Effect.Effect { + return core.suspend(() => { + if (MutableRef.get(this.shutdownFlag)) { + return core.interrupt + } + + if (this.pubsub.publish(value)) { + this.strategy.unsafeCompleteSubscribers(this.pubsub, this.subscribers) + return core.succeed(true) + } + + return this.strategy.handleSurplus( + this.pubsub, + this.subscribers, + Chunk.of(value), + this.shutdownFlag + ) + }) + } + + isActive(): boolean { + return !MutableRef.get(this.shutdownFlag) + } + + unsafeOffer(value: A): boolean { + if (MutableRef.get(this.shutdownFlag)) { + return false + } + + if ((this.pubsub as AtomicPubSub).publish(value)) { + this.strategy.unsafeCompleteSubscribers(this.pubsub, this.subscribers) + return true + } + + return false + } + + publishAll(elements: Iterable): Effect.Effect { + return core.suspend(() => { + if (MutableRef.get(this.shutdownFlag)) { + return core.interrupt + } + const surplus = unsafePublishAll(this.pubsub, elements) + this.strategy.unsafeCompleteSubscribers(this.pubsub, this.subscribers) + if (Chunk.isEmpty(surplus)) { + return core.succeed(true) + } + return this.strategy.handleSurplus( + this.pubsub, + this.subscribers, + surplus, + this.shutdownFlag + ) + }) + } + + get subscribe(): Effect.Effect, never, Scope.Scope> { + const acquire = core.tap( + fiberRuntime.all([ + this.scope.fork(executionStrategy.sequential), + makeSubscription(this.pubsub, this.subscribers, this.strategy) + ]), + (tuple) => tuple[0].addFinalizer(() => tuple[1].shutdown) + ) + return core.map( + fiberRuntime.acquireRelease(acquire, (tuple, exit) => tuple[0].close(exit)), + (tuple) => tuple[1] + ) + } + + offer(value: A): Effect.Effect { + return this.publish(value) + } + + offerAll(elements: Iterable): Effect.Effect { + return this.publishAll(elements) + } + + pipe() { + return pipeArguments(this, arguments) + } +} + +/** @internal */ +export const makePubSub = ( + pubsub: AtomicPubSub, + strategy: PubSubStrategy +): Effect.Effect> => + core.flatMap( + fiberRuntime.scopeMake(), + (scope) => + core.map(core.deferredMake(), (deferred) => + unsafeMakePubSub( + pubsub, + new Map(), + scope, + deferred, + MutableRef.make(false), + strategy + )) + ) + +/** @internal */ +export const unsafeMakePubSub = ( + pubsub: AtomicPubSub, + subscribers: Subscribers, + scope: Scope.Scope.Closeable, + shutdownHook: Deferred.Deferred, + shutdownFlag: MutableRef.MutableRef, + strategy: PubSubStrategy +): PubSub.PubSub => new PubSubImpl(pubsub, subscribers, scope, shutdownHook, shutdownFlag, strategy) + +/** @internal */ +const ensureCapacity = (capacity: number): void => { + if (capacity <= 0) { + throw new core.InvalidPubSubCapacityException(`Cannot construct PubSub with capacity of ${capacity}`) + } +} + +/** @internal */ +const unsafeCompleteDeferred = (deferred: Deferred.Deferred, a: A): void => { + core.deferredUnsafeDone(deferred, core.succeed(a)) +} + +/** @internal */ +const unsafeOfferAll = (queue: MutableQueue.MutableQueue, as: Iterable): Chunk.Chunk => { + return pipe(queue, MutableQueue.offerAll(as)) +} + +/** @internal */ +const unsafePollAllQueue = (queue: MutableQueue.MutableQueue): Chunk.Chunk => { + return pipe(queue, MutableQueue.pollUpTo(Number.POSITIVE_INFINITY)) +} + +/** @internal */ +const unsafePollAllSubscription = (subscription: Subscription): Chunk.Chunk => { + return subscription.pollUpTo(Number.POSITIVE_INFINITY) +} + +/** @internal */ +const unsafePollN = (subscription: Subscription, max: number): Chunk.Chunk => { + return subscription.pollUpTo(max) +} + +/** @internal */ +const unsafePublishAll = (pubsub: AtomicPubSub, as: Iterable): Chunk.Chunk => { + return pubsub.publishAll(as) +} + +/** @internal */ +const unsafeRemove = (queue: MutableQueue.MutableQueue, value: A): void => { + unsafeOfferAll( + queue, + pipe(unsafePollAllQueue(queue), Chunk.filter((elem) => elem !== value)) + ) +} + +// ----------------------------------------------------------------------------- +// PubSub.Strategy +// ----------------------------------------------------------------------------- + +/** + * A `PubSubStrategy` describes the protocol for how publishers and subscribers + * will communicate with each other through the `PubSub`. + * + * @internal + */ +export interface PubSubStrategy { + /** + * Describes any finalization logic associated with this strategy. + */ + readonly shutdown: Effect.Effect + + /** + * Describes how publishers should signal to subscribers that they are + * waiting for space to become available in the `PubSub`. + */ + handleSurplus( + pubsub: AtomicPubSub, + subscribers: Subscribers, + elements: Iterable, + isShutdown: MutableRef.MutableRef + ): Effect.Effect + + /** + * Describes how subscribers should signal to publishers waiting for space + * to become available in the `PubSub` that space may be available. + */ + unsafeOnPubSubEmptySpace( + pubsub: AtomicPubSub, + subscribers: Subscribers + ): void + + /** + * Describes how subscribers waiting for additional values from the `PubSub` + * should take those values and signal to publishers that they are no + * longer waiting for additional values. + */ + unsafeCompletePollers( + pubsub: AtomicPubSub, + subscribers: Subscribers, + subscription: Subscription, + pollers: MutableQueue.MutableQueue> + ): void + + /** + * Describes how publishers should signal to subscribers waiting for + * additional values from the `PubSub` that new values are available. + */ + unsafeCompleteSubscribers( + pubsub: AtomicPubSub, + subscribers: Subscribers + ): void +} + +/** + * A strategy that applies back pressure to publishers when the `PubSub` is at + * capacity. This guarantees that all subscribers will receive all messages + * published to the `PubSub` while they are subscribed. However, it creates the + * risk that a slow subscriber will slow down the rate at which messages + * are published and received by other subscribers. + * + * @internal + */ +class BackPressureStrategy implements PubSubStrategy { + publishers: MutableQueue.MutableQueue< + readonly [A, Deferred.Deferred, boolean] + > = MutableQueue.unbounded() + + get shutdown(): Effect.Effect { + return core.flatMap(core.fiberId, (fiberId) => + core.flatMap( + core.sync(() => unsafePollAllQueue(this.publishers)), + (publishers) => + fiberRuntime.forEachConcurrentDiscard( + publishers, + ([_, deferred, last]) => + last ? + pipe(core.deferredInterruptWith(deferred, fiberId), core.asVoid) : + core.void, + false, + false + ) + )) + } + + handleSurplus( + pubsub: AtomicPubSub, + subscribers: Subscribers, + elements: Iterable, + isShutdown: MutableRef.MutableRef + ): Effect.Effect { + return core.withFiberRuntime((state) => { + const deferred = core.deferredUnsafeMake(state.id()) + return pipe( + core.suspend(() => { + this.unsafeOffer(elements, deferred) + this.unsafeOnPubSubEmptySpace(pubsub, subscribers) + this.unsafeCompleteSubscribers(pubsub, subscribers) + return MutableRef.get(isShutdown) ? + core.interrupt : + core.deferredAwait(deferred) + }), + core.onInterrupt(() => core.sync(() => this.unsafeRemove(deferred))) + ) + }) + } + + unsafeOnPubSubEmptySpace( + pubsub: AtomicPubSub, + subscribers: Subscribers + ): void { + let keepPolling = true + while (keepPolling && !pubsub.isFull()) { + const publisher = pipe(this.publishers, MutableQueue.poll(MutableQueue.EmptyMutableQueue)) + if (publisher === MutableQueue.EmptyMutableQueue) { + keepPolling = false + } else { + const published = pubsub.publish(publisher[0]) + if (published && publisher[2]) { + unsafeCompleteDeferred(publisher[1], true) + } else if (!published) { + unsafeOfferAll( + this.publishers, + pipe(unsafePollAllQueue(this.publishers), Chunk.prepend(publisher)) + ) + } + this.unsafeCompleteSubscribers(pubsub, subscribers) + } + } + } + + unsafeCompletePollers( + pubsub: AtomicPubSub, + subscribers: Subscribers, + subscription: Subscription, + pollers: MutableQueue.MutableQueue> + ): void { + return unsafeStrategyCompletePollers(this, pubsub, subscribers, subscription, pollers) + } + + unsafeCompleteSubscribers(pubsub: AtomicPubSub, subscribers: Subscribers): void { + return unsafeStrategyCompleteSubscribers(this, pubsub, subscribers) + } + + private unsafeOffer(elements: Iterable, deferred: Deferred.Deferred): void { + const iterator = elements[Symbol.iterator]() + let next: IteratorResult = iterator.next() + if (!next.done) { + // eslint-disable-next-line no-constant-condition + while (1) { + const value = next.value + next = iterator.next() + if (next.done) { + pipe( + this.publishers, + MutableQueue.offer([value, deferred, true as boolean] as const) + ) + break + } + pipe( + this.publishers, + MutableQueue.offer([value, deferred, false as boolean] as const) + ) + } + } + } + + unsafeRemove(deferred: Deferred.Deferred): void { + unsafeOfferAll( + this.publishers, + pipe(unsafePollAllQueue(this.publishers), Chunk.filter(([_, a]) => a !== deferred)) + ) + } +} + +/** + * A strategy that drops new messages when the `PubSub` is at capacity. This + * guarantees that a slow subscriber will not slow down the rate at which + * messages are published. However, it creates the risk that a slow + * subscriber will slow down the rate at which messages are received by + * other subscribers and that subscribers may not receive all messages + * published to the `PubSub` while they are subscribed. + * + * @internal + */ +export class DroppingStrategy implements PubSubStrategy { + get shutdown(): Effect.Effect { + return core.void + } + + handleSurplus( + _pubsub: AtomicPubSub, + _subscribers: Subscribers, + _elements: Iterable, + _isShutdown: MutableRef.MutableRef + ): Effect.Effect { + return core.succeed(false) + } + + unsafeOnPubSubEmptySpace( + _pubsub: AtomicPubSub, + _subscribers: Subscribers + ): void { + // + } + + unsafeCompletePollers( + pubsub: AtomicPubSub, + subscribers: Subscribers, + subscription: Subscription, + pollers: MutableQueue.MutableQueue> + ): void { + return unsafeStrategyCompletePollers(this, pubsub, subscribers, subscription, pollers) + } + + unsafeCompleteSubscribers(pubsub: AtomicPubSub, subscribers: Subscribers): void { + return unsafeStrategyCompleteSubscribers(this, pubsub, subscribers) + } +} + +/** + * A strategy that adds new messages and drops old messages when the `PubSub` is + * at capacity. This guarantees that a slow subscriber will not slow down + * the rate at which messages are published and received by other + * subscribers. However, it creates the risk that a slow subscriber will + * not receive some messages published to the `PubSub` while it is subscribed. + * + * @internal + */ +export class SlidingStrategy implements PubSubStrategy { + get shutdown(): Effect.Effect { + return core.void + } + + handleSurplus( + pubsub: AtomicPubSub, + subscribers: Subscribers, + elements: Iterable, + _isShutdown: MutableRef.MutableRef + ): Effect.Effect { + return core.sync(() => { + this.unsafeSlidingPublish(pubsub, elements) + this.unsafeCompleteSubscribers(pubsub, subscribers) + return true + }) + } + + unsafeOnPubSubEmptySpace( + _pubsub: AtomicPubSub, + _subscribers: Subscribers + ): void { + // + } + + unsafeCompletePollers( + pubsub: AtomicPubSub, + subscribers: Subscribers, + subscription: Subscription, + pollers: MutableQueue.MutableQueue> + ): void { + return unsafeStrategyCompletePollers(this, pubsub, subscribers, subscription, pollers) + } + + unsafeCompleteSubscribers(pubsub: AtomicPubSub, subscribers: Subscribers): void { + return unsafeStrategyCompleteSubscribers(this, pubsub, subscribers) + } + + unsafeSlidingPublish(pubsub: AtomicPubSub, elements: Iterable): void { + const it = elements[Symbol.iterator]() + let next = it.next() + if (!next.done && pubsub.capacity > 0) { + let a = next.value + let loop = true + while (loop) { + pubsub.slide() + const pub = pubsub.publish(a) + if (pub && (next = it.next()) && !next.done) { + a = next.value + } else if (pub) { + loop = false + } + } + } + } +} + +/** @internal */ +const unsafeStrategyCompletePollers = ( + strategy: PubSubStrategy, + pubsub: AtomicPubSub, + subscribers: Subscribers, + subscription: Subscription, + pollers: MutableQueue.MutableQueue> +): void => { + let keepPolling = true + while (keepPolling && !subscription.isEmpty()) { + const poller = pipe(pollers, MutableQueue.poll(MutableQueue.EmptyMutableQueue)) + if (poller === MutableQueue.EmptyMutableQueue) { + pipe(subscribers, removeSubscribers(subscription, pollers)) + if (MutableQueue.isEmpty(pollers)) { + keepPolling = false + } else { + pipe(subscribers, addSubscribers(subscription, pollers)) + } + } else { + const pollResult = subscription.poll(MutableQueue.EmptyMutableQueue) + if (pollResult === MutableQueue.EmptyMutableQueue) { + unsafeOfferAll(pollers, pipe(unsafePollAllQueue(pollers), Chunk.prepend(poller))) + } else { + unsafeCompleteDeferred(poller, pollResult) + strategy.unsafeOnPubSubEmptySpace(pubsub, subscribers) + } + } + } +} + +/** @internal */ +const unsafeStrategyCompleteSubscribers = ( + strategy: PubSubStrategy, + pubsub: AtomicPubSub, + subscribers: Subscribers +): void => { + for ( + const [subscription, pollersSet] of subscribers + ) { + for (const pollers of pollersSet) { + strategy.unsafeCompletePollers(pubsub, subscribers, subscription, pollers) + } + } +} + +interface ReplayNode { + value: A | AbsentValue + next: ReplayNode | null +} + +class ReplayBuffer { + constructor(readonly capacity: number) {} + + head: ReplayNode = { value: AbsentValue, next: null } + tail: ReplayNode = this.head + size = 0 + index = 0 + + slide() { + this.index++ + } + offer(a: A): void { + this.tail.value = a + this.tail.next = { + value: AbsentValue, + next: null + } + this.tail = this.tail.next + if (this.size === this.capacity) { + this.head = this.head.next! + } else { + this.size += 1 + } + } + offerAll(as: Iterable): void { + for (const a of as) { + this.offer(a) + } + } +} + +interface ReplayWindow { + take(): A | undefined + takeN(n: number): Chunk.Chunk + takeAll(): Chunk.Chunk + readonly remaining: number +} + +class ReplayWindowImpl implements ReplayWindow { + head: ReplayNode + index: number + remaining: number + constructor(readonly buffer: ReplayBuffer) { + this.index = buffer.index + this.remaining = buffer.size + this.head = buffer.head + } + fastForward() { + while (this.index < this.buffer.index) { + this.head = this.head.next! + this.index++ + } + } + take(): A | undefined { + if (this.remaining === 0) { + return undefined + } else if (this.index < this.buffer.index) { + this.fastForward() + } + this.remaining-- + const value = this.head.value + this.head = this.head.next! + return value as A + } + takeN(n: number): Chunk.Chunk { + if (this.remaining === 0) { + return Chunk.empty() + } else if (this.index < this.buffer.index) { + this.fastForward() + } + const len = Math.min(n, this.remaining) + const items = new Array(len) + for (let i = 0; i < len; i++) { + const value = this.head.value as A + this.head = this.head.next! + items[i] = value + } + this.remaining -= len + return Chunk.unsafeFromArray(items) + } + takeAll(): Chunk.Chunk { + return this.takeN(this.remaining) + } +} + +const emptyReplayWindow: ReplayWindow = { + remaining: 0, + take: () => undefined, + takeN: () => Chunk.empty(), + takeAll: () => Chunk.empty() +} diff --git a/repos/effect/packages/effect/src/internal/query.ts b/repos/effect/packages/effect/src/internal/query.ts new file mode 100644 index 0000000..3ba1322 --- /dev/null +++ b/repos/effect/packages/effect/src/internal/query.ts @@ -0,0 +1,204 @@ +import type * as Cache from "../Cache.js" +import type { Deferred } from "../Deferred.js" +import { seconds } from "../Duration.js" +import type * as Effect from "../Effect.js" +import { dual } from "../Function.js" +import { globalValue } from "../GlobalValue.js" +import type * as Request from "../Request.js" +import type * as RequestResolver from "../RequestResolver.js" +import * as BlockedRequests from "./blockedRequests.js" +import { unsafeMakeWith } from "./cache.js" +import * as core from "./core.js" +import { ensuring } from "./fiberRuntime.js" +import { Listeners } from "./request.js" + +type RequestCache = Cache.Cache, { + listeners: Request.Listeners + handle: Deferred +}> + +/** @internal */ +export const currentCache = globalValue( + Symbol.for("effect/FiberRef/currentCache"), + () => + core.fiberRefUnsafeMake(unsafeMakeWith, { + listeners: Request.Listeners + handle: Deferred + }>( + 65536, + () => core.map(core.deferredMake(), (handle) => ({ listeners: new Listeners(), handle })), + () => seconds(60) + )) +) + +/** @internal */ +export const currentCacheEnabled = globalValue( + Symbol.for("effect/FiberRef/currentCacheEnabled"), + () => core.fiberRefUnsafeMake(false) +) + +/** @internal */ +export const fromRequest = < + A extends Request.Request, + Ds extends + | RequestResolver.RequestResolver + | Effect.Effect, any, any> +>( + request: A, + dataSource: Ds +): Effect.Effect< + Request.Request.Success, + Request.Request.Error, + [Ds] extends [Effect.Effect] ? Effect.Effect.Context : never +> => + core.flatMap( + (core.isEffect(dataSource) ? dataSource : core.succeed(dataSource)) as Effect.Effect< + RequestResolver.RequestResolver + >, + (ds) => + core.fiberIdWith((id) => { + const proxy = new Proxy(request, {}) + return core.fiberRefGetWith(currentCacheEnabled, (cacheEnabled) => { + if (cacheEnabled) { + const cached: Effect.Effect = core.fiberRefGetWith(currentCache, (cache) => + core.flatMap(cache.getEither(proxy), (orNew) => { + switch (orNew._tag) { + case "Left": { + if (orNew.left.listeners.interrupted) { + return core.flatMap( + cache.invalidateWhen(proxy, (entry) => entry.handle === orNew.left.handle), + () => cached + ) + } + orNew.left.listeners.increment() + return core.uninterruptibleMask((restore) => + core.flatMap( + core.exit(core.blocked( + BlockedRequests.empty, + restore(core.deferredAwait(orNew.left.handle)) + )), + (exit) => { + orNew.left.listeners.decrement() + return exit + } + ) + ) + } + case "Right": { + orNew.right.listeners.increment() + return core.uninterruptibleMask((restore) => + core.flatMap( + core.exit( + core.blocked( + BlockedRequests.single( + ds as RequestResolver.RequestResolver, + BlockedRequests.makeEntry({ + request: proxy, + result: orNew.right.handle, + listeners: orNew.right.listeners, + ownerId: id, + state: { completed: false } + }) + ), + restore(core.deferredAwait(orNew.right.handle)) + ) + ), + () => { + orNew.right.listeners.decrement() + return core.deferredAwait(orNew.right.handle) + } + ) + ) + } + } + })) + return cached + } + const listeners = new Listeners() + listeners.increment() + return core.flatMap( + core.deferredMake, Request.Request.Error>(), + (ref) => + ensuring( + core.blocked( + BlockedRequests.single( + ds as RequestResolver.RequestResolver, + BlockedRequests.makeEntry({ + request: proxy, + result: ref, + listeners, + ownerId: id, + state: { completed: false } + }) + ), + core.deferredAwait(ref) + ), + core.sync(() => + listeners.decrement() + ) + ) + ) + }) + }) + ) + +/** @internal */ +export const cacheRequest = >( + request: A, + result: Request.Request.Result +): Effect.Effect => { + return core.fiberRefGetWith(currentCacheEnabled, (cacheEnabled) => { + if (cacheEnabled) { + return core.fiberRefGetWith(currentCache, (cache) => + core.flatMap(cache.getEither(request), (orNew) => { + switch (orNew._tag) { + case "Left": { + return core.void + } + case "Right": { + return core.deferredComplete(orNew.right.handle, result) + } + } + })) + } + return core.void + }) +} + +/** @internal */ +export const withRequestCaching: { + (strategy: boolean): (self: Effect.Effect) => Effect.Effect + ( + self: Effect.Effect, + strategy: boolean + ): Effect.Effect +} = dual< + ( + strategy: boolean + ) => (self: Effect.Effect) => Effect.Effect, + ( + self: Effect.Effect, + strategy: boolean + ) => Effect.Effect +>(2, (self, strategy) => core.fiberRefLocally(self, currentCacheEnabled, strategy)) + +/** @internal */ +export const withRequestCache: { + (cache: Request.Cache): (self: Effect.Effect) => Effect.Effect + ( + self: Effect.Effect, + cache: Request.Cache + ): Effect.Effect +} = dual< + ( + cache: Request.Cache + ) => (self: Effect.Effect) => Effect.Effect, + ( + self: Effect.Effect, + cache: Request.Cache + ) => Effect.Effect +>( + 2, + // @ts-expect-error + (self, cache) => core.fiberRefLocally(self, currentCache, cache) +) diff --git a/repos/effect/packages/effect/src/internal/queue.ts b/repos/effect/packages/effect/src/internal/queue.ts new file mode 100644 index 0000000..2a40a4b --- /dev/null +++ b/repos/effect/packages/effect/src/internal/queue.ts @@ -0,0 +1,766 @@ +import * as Arr from "../Array.js" +import * as Chunk from "../Chunk.js" +import type * as Deferred from "../Deferred.js" +import type * as Effect from "../Effect.js" +import * as Effectable from "../Effectable.js" +import { dual, pipe } from "../Function.js" +import * as MutableQueue from "../MutableQueue.js" +import * as MutableRef from "../MutableRef.js" +import * as Option from "../Option.js" +import { pipeArguments } from "../Pipeable.js" +import { hasProperty } from "../Predicate.js" +import type * as Queue from "../Queue.js" +import * as core from "./core.js" +import * as fiberRuntime from "./fiberRuntime.js" + +/** @internal */ +const EnqueueSymbolKey = "effect/QueueEnqueue" + +/** @internal */ +export const EnqueueTypeId: Queue.EnqueueTypeId = Symbol.for(EnqueueSymbolKey) as Queue.EnqueueTypeId + +/** @internal */ +const DequeueSymbolKey = "effect/QueueDequeue" + +/** @internal */ +export const DequeueTypeId: Queue.DequeueTypeId = Symbol.for(DequeueSymbolKey) as Queue.DequeueTypeId + +/** @internal */ +const QueueStrategySymbolKey = "effect/QueueStrategy" + +/** @internal */ +export const QueueStrategyTypeId: Queue.QueueStrategyTypeId = Symbol.for( + QueueStrategySymbolKey +) as Queue.QueueStrategyTypeId + +/** @internal */ +const BackingQueueSymbolKey = "effect/BackingQueue" + +/** @internal */ +export const BackingQueueTypeId: Queue.BackingQueueTypeId = Symbol.for( + BackingQueueSymbolKey +) as Queue.BackingQueueTypeId + +const queueStrategyVariance = { + /* c8 ignore next */ + _A: (_: any) => _ +} + +const backingQueueVariance = { + /* c8 ignore next */ + _A: (_: any) => _ +} + +/** @internal */ +export const enqueueVariance = { + /* c8 ignore next */ + _In: (_: unknown) => _ +} + +/** @internal */ +export const dequeueVariance = { + /* c8 ignore next */ + _Out: (_: never) => _ +} + +/** @internal */ +class QueueImpl extends Effectable.Class implements Queue.Queue { + readonly [EnqueueTypeId] = enqueueVariance + readonly [DequeueTypeId] = dequeueVariance + + constructor( + /** @internal */ + readonly queue: Queue.BackingQueue, + /** @internal */ + readonly takers: MutableQueue.MutableQueue>, + /** @internal */ + readonly shutdownHook: Deferred.Deferred, + /** @internal */ + readonly shutdownFlag: MutableRef.MutableRef, + /** @internal */ + readonly strategy: Queue.Strategy + ) { + super() + } + + pipe() { + return pipeArguments(this, arguments) + } + + commit() { + return this.take + } + + capacity(): number { + return this.queue.capacity() + } + + get size(): Effect.Effect { + return core.suspend(() => core.catchAll(this.unsafeSize(), () => core.interrupt)) + } + + unsafeSize() { + if (MutableRef.get(this.shutdownFlag)) { + return Option.none() + } + return Option.some( + this.queue.length() - + MutableQueue.length(this.takers) + + this.strategy.surplusSize() + ) + } + + get isEmpty(): Effect.Effect { + return core.map(this.size, (size) => size <= 0) + } + + get isFull(): Effect.Effect { + return core.map(this.size, (size) => size >= this.capacity()) + } + + get shutdown(): Effect.Effect { + return core.uninterruptible( + core.withFiberRuntime((state) => { + pipe(this.shutdownFlag, MutableRef.set(true)) + return pipe( + fiberRuntime.forEachConcurrentDiscard( + unsafePollAll(this.takers), + (d) => core.deferredInterruptWith(d, state.id()), + false, + false + ), + core.zipRight(this.strategy.shutdown), + core.whenEffect(core.deferredSucceed(this.shutdownHook, void 0)), + core.asVoid + ) + }) + ) + } + + get isShutdown(): Effect.Effect { + return core.sync(() => MutableRef.get(this.shutdownFlag)) + } + + get awaitShutdown(): Effect.Effect { + return core.deferredAwait(this.shutdownHook) + } + + isActive() { + return !MutableRef.get(this.shutdownFlag) + } + + unsafeOffer(value: A): boolean { + if (MutableRef.get(this.shutdownFlag)) { + return false + } + let noRemaining: boolean + if (this.queue.length() === 0) { + const taker = pipe( + this.takers, + MutableQueue.poll(MutableQueue.EmptyMutableQueue) + ) + if (taker !== MutableQueue.EmptyMutableQueue) { + unsafeCompleteDeferred(taker, value) + noRemaining = true + } else { + noRemaining = false + } + } else { + noRemaining = false + } + if (noRemaining) { + return true + } + // Not enough takers, offer to the queue + const succeeded = this.queue.offer(value) + unsafeCompleteTakers(this.strategy, this.queue, this.takers) + return succeeded + } + + offer(value: A): Effect.Effect { + return core.suspend(() => { + if (MutableRef.get(this.shutdownFlag)) { + return core.interrupt + } + let noRemaining: boolean + if (this.queue.length() === 0) { + const taker = pipe( + this.takers, + MutableQueue.poll(MutableQueue.EmptyMutableQueue) + ) + if (taker !== MutableQueue.EmptyMutableQueue) { + unsafeCompleteDeferred(taker, value) + noRemaining = true + } else { + noRemaining = false + } + } else { + noRemaining = false + } + if (noRemaining) { + return core.succeed(true) + } + // Not enough takers, offer to the queue + const succeeded = this.queue.offer(value) + unsafeCompleteTakers(this.strategy, this.queue, this.takers) + return succeeded + ? core.succeed(true) + : this.strategy.handleSurplus([value], this.queue, this.takers, this.shutdownFlag) + }) + } + + offerAll(iterable: Iterable): Effect.Effect { + return core.suspend(() => { + if (MutableRef.get(this.shutdownFlag)) { + return core.interrupt + } + const values = Arr.fromIterable(iterable) + const pTakers = this.queue.length() === 0 + ? Arr.fromIterable(unsafePollN(this.takers, values.length)) + : Arr.empty + const [forTakers, remaining] = pipe(values, Arr.splitAt(pTakers.length)) + for (let i = 0; i < pTakers.length; i++) { + const taker = (pTakers as any)[i] + const item = forTakers[i] + unsafeCompleteDeferred(taker, item) + } + if (remaining.length === 0) { + return core.succeed(true) + } + // Not enough takers, offer to the queue + const surplus = this.queue.offerAll(remaining) + unsafeCompleteTakers(this.strategy, this.queue, this.takers) + return Chunk.isEmpty(surplus) + ? core.succeed(true) + : this.strategy.handleSurplus(surplus, this.queue, this.takers, this.shutdownFlag) + }) + } + + get take(): Effect.Effect { + return core.withFiberRuntime((state) => { + if (MutableRef.get(this.shutdownFlag)) { + return core.interrupt + } + const item = this.queue.poll(MutableQueue.EmptyMutableQueue) + if (item !== MutableQueue.EmptyMutableQueue) { + this.strategy.unsafeOnQueueEmptySpace(this.queue, this.takers) + return core.succeed(item) + } else { + // Add the deferred to takers, then: + // - Try to take again in case a value was added since + // - Wait for the deferred to be completed + // - Clean up resources in case of interruption + const deferred = core.deferredUnsafeMake(state.id()) + return pipe( + core.suspend(() => { + pipe(this.takers, MutableQueue.offer(deferred)) + unsafeCompleteTakers(this.strategy, this.queue, this.takers) + return MutableRef.get(this.shutdownFlag) ? + core.interrupt : + core.deferredAwait(deferred) + }), + core.onInterrupt(() => { + return core.sync(() => unsafeRemove(this.takers, deferred)) + }) + ) + } + }) + } + + get takeAll(): Effect.Effect> { + return core.suspend(() => { + return MutableRef.get(this.shutdownFlag) + ? core.interrupt + : core.sync(() => { + const values = this.queue.pollUpTo(Number.POSITIVE_INFINITY) + this.strategy.unsafeOnQueueEmptySpace(this.queue, this.takers) + return Chunk.fromIterable(values) + }) + }) + } + + takeUpTo(max: number): Effect.Effect> { + return core.suspend(() => + MutableRef.get(this.shutdownFlag) + ? core.interrupt + : core.sync(() => { + const values = this.queue.pollUpTo(max) + this.strategy.unsafeOnQueueEmptySpace(this.queue, this.takers) + return Chunk.fromIterable(values) + }) + ) + } + + takeBetween(min: number, max: number): Effect.Effect> { + return core.suspend(() => + takeRemainderLoop( + this, + min, + max, + Chunk.empty() + ) + ) + } +} + +/** @internal */ +const takeRemainderLoop = ( + self: Queue.Dequeue, + min: number, + max: number, + acc: Chunk.Chunk +): Effect.Effect> => { + if (max < min) { + return core.succeed(acc) + } + return pipe( + takeUpTo(self, max), + core.flatMap((bs) => { + const remaining = min - bs.length + if (remaining === 1) { + return pipe( + take(self), + core.map((b) => pipe(acc, Chunk.appendAll(bs), Chunk.append(b))) + ) + } + if (remaining > 1) { + return pipe( + take(self), + core.flatMap((b) => + takeRemainderLoop( + self, + remaining - 1, + max - bs.length - 1, + pipe(acc, Chunk.appendAll(bs), Chunk.append(b)) + ) + ) + ) + } + return core.succeed(pipe(acc, Chunk.appendAll(bs))) + }) + ) +} + +/** @internal */ +export const isQueue = (u: unknown): u is Queue.Queue => isEnqueue(u) && isDequeue(u) + +/** @internal */ +export const isEnqueue = (u: unknown): u is Queue.Enqueue => hasProperty(u, EnqueueTypeId) + +/** @internal */ +export const isDequeue = (u: unknown): u is Queue.Dequeue => hasProperty(u, DequeueTypeId) + +/** @internal */ +export const bounded = (requestedCapacity: number): Effect.Effect> => + pipe( + core.sync(() => MutableQueue.bounded(requestedCapacity)), + core.flatMap((queue) => make(backingQueueFromMutableQueue(queue), backPressureStrategy())) + ) + +/** @internal */ +export const dropping = (requestedCapacity: number): Effect.Effect> => + pipe( + core.sync(() => MutableQueue.bounded(requestedCapacity)), + core.flatMap((queue) => make(backingQueueFromMutableQueue(queue), droppingStrategy())) + ) + +/** @internal */ +export const sliding = (requestedCapacity: number): Effect.Effect> => + pipe( + core.sync(() => MutableQueue.bounded(requestedCapacity)), + core.flatMap((queue) => make(backingQueueFromMutableQueue(queue), slidingStrategy())) + ) + +/** @internal */ +export const unbounded = (): Effect.Effect> => + pipe( + core.sync(() => MutableQueue.unbounded()), + core.flatMap((queue) => make(backingQueueFromMutableQueue(queue), droppingStrategy())) + ) + +/** @internal */ +const unsafeMake = ( + queue: Queue.BackingQueue, + takers: MutableQueue.MutableQueue>, + shutdownHook: Deferred.Deferred, + shutdownFlag: MutableRef.MutableRef, + strategy: Queue.Strategy +): Queue.Queue => { + return new QueueImpl(queue, takers, shutdownHook, shutdownFlag, strategy) +} + +/** @internal */ +export const make = ( + queue: Queue.BackingQueue, + strategy: Queue.Strategy +): Effect.Effect> => + pipe( + core.deferredMake(), + core.map((deferred) => + unsafeMake( + queue, + MutableQueue.unbounded(), + deferred, + MutableRef.make(false), + strategy + ) + ) + ) + +/** @internal */ +export class BackingQueueFromMutableQueue implements Queue.BackingQueue { + readonly [BackingQueueTypeId] = backingQueueVariance + constructor(readonly mutable: MutableQueue.MutableQueue) {} + poll(def: Def): A | Def { + return MutableQueue.poll(this.mutable, def) + } + pollUpTo(limit: number): Chunk.Chunk { + return MutableQueue.pollUpTo(this.mutable, limit) + } + offerAll(elements: Iterable): Chunk.Chunk { + return MutableQueue.offerAll(this.mutable, elements) + } + offer(element: A): boolean { + return MutableQueue.offer(this.mutable, element) + } + capacity(): number { + return MutableQueue.capacity(this.mutable) + } + length(): number { + return MutableQueue.length(this.mutable) + } +} + +/** @internal */ +export const backingQueueFromMutableQueue = (mutable: MutableQueue.MutableQueue): Queue.BackingQueue => + new BackingQueueFromMutableQueue(mutable) + +/** @internal */ +export const capacity = (self: Queue.Dequeue | Queue.Enqueue): number => self.capacity() + +/** @internal */ +export const size = (self: Queue.Dequeue | Queue.Enqueue): Effect.Effect => self.size + +/** @internal */ +export const isFull = (self: Queue.Dequeue | Queue.Enqueue): Effect.Effect => self.isFull + +/** @internal */ +export const isEmpty = (self: Queue.Dequeue | Queue.Enqueue): Effect.Effect => self.isEmpty + +/** @internal */ +export const isShutdown = (self: Queue.Dequeue | Queue.Enqueue): Effect.Effect => self.isShutdown + +/** @internal */ +export const awaitShutdown = (self: Queue.Dequeue | Queue.Enqueue): Effect.Effect => self.awaitShutdown + +/** @internal */ +export const shutdown = (self: Queue.Dequeue | Queue.Enqueue): Effect.Effect => self.shutdown + +/** @internal */ +export const offer = dual< + (value: A) => (self: Queue.Enqueue) => Effect.Effect, + (self: Queue.Enqueue, value: A) => Effect.Effect +>(2, (self, value) => self.offer(value)) + +/** @internal */ +export const unsafeOffer = dual< + (value: A) => (self: Queue.Enqueue) => boolean, + (self: Queue.Enqueue, value: A) => boolean +>(2, (self, value) => self.unsafeOffer(value)) + +/** @internal */ +export const offerAll = dual< + ( + iterable: Iterable + ) => (self: Queue.Enqueue) => Effect.Effect, + ( + self: Queue.Enqueue, + iterable: Iterable + ) => Effect.Effect +>(2, (self, iterable) => self.offerAll(iterable)) + +/** @internal */ +export const poll = (self: Queue.Dequeue): Effect.Effect> => + core.map(self.takeUpTo(1), Chunk.head) + +/** @internal */ +export const take = (self: Queue.Dequeue): Effect.Effect => self.take + +/** @internal */ +export const takeAll = (self: Queue.Dequeue): Effect.Effect> => self.takeAll + +/** @internal */ +export const takeUpTo = dual< + (max: number) => (self: Queue.Dequeue) => Effect.Effect>, + (self: Queue.Dequeue, max: number) => Effect.Effect> +>(2, (self, max) => self.takeUpTo(max)) + +/** @internal */ +export const takeBetween = dual< + (min: number, max: number) => (self: Queue.Dequeue) => Effect.Effect>, + (self: Queue.Dequeue, min: number, max: number) => Effect.Effect> +>(3, (self, min, max) => self.takeBetween(min, max)) + +/** @internal */ +export const takeN = dual< + (n: number) => (self: Queue.Dequeue) => Effect.Effect>, + (self: Queue.Dequeue, n: number) => Effect.Effect> +>(2, (self, n) => self.takeBetween(n, n)) + +// ----------------------------------------------------------------------------- +// Strategy +// ----------------------------------------------------------------------------- + +/** @internal */ +export const backPressureStrategy = (): Queue.Strategy => new BackPressureStrategy() + +/** @internal */ +export const droppingStrategy = (): Queue.Strategy => new DroppingStrategy() + +/** @internal */ +export const slidingStrategy = (): Queue.Strategy => new SlidingStrategy() + +/** @internal */ +class BackPressureStrategy implements Queue.Strategy { + readonly [QueueStrategyTypeId] = queueStrategyVariance + + readonly putters = MutableQueue.unbounded, boolean]>() + + surplusSize(): number { + return MutableQueue.length(this.putters) + } + + onCompleteTakersWithEmptyQueue(takers: MutableQueue.MutableQueue>): void { + while (!MutableQueue.isEmpty(this.putters) && !MutableQueue.isEmpty(takers)) { + const taker = MutableQueue.poll(takers, void 0)! + const putter = MutableQueue.poll(this.putters, void 0)! + if (putter[2]) { + unsafeCompleteDeferred(putter[1], true) + } + unsafeCompleteDeferred(taker, putter[0]) + } + } + + get shutdown(): Effect.Effect { + return pipe( + core.fiberId, + core.flatMap((fiberId) => + pipe( + core.sync(() => unsafePollAll(this.putters)), + core.flatMap((putters) => + fiberRuntime.forEachConcurrentDiscard( + putters, + ([_, deferred, isLastItem]) => + isLastItem ? + pipe( + core.deferredInterruptWith(deferred, fiberId), + core.asVoid + ) : + core.void, + false, + false + ) + ) + ) + ) + ) + } + + handleSurplus( + iterable: Iterable, + queue: Queue.BackingQueue, + takers: MutableQueue.MutableQueue>, + isShutdown: MutableRef.MutableRef + ): Effect.Effect { + return core.withFiberRuntime((state) => { + const deferred = core.deferredUnsafeMake(state.id()) + return pipe( + core.suspend(() => { + this.unsafeOffer(iterable, deferred) + this.unsafeOnQueueEmptySpace(queue, takers) + unsafeCompleteTakers(this, queue, takers) + return MutableRef.get(isShutdown) ? core.interrupt : core.deferredAwait(deferred) + }), + core.onInterrupt(() => core.sync(() => this.unsafeRemove(deferred))) + ) + }) + } + + unsafeOnQueueEmptySpace( + queue: Queue.BackingQueue, + takers: MutableQueue.MutableQueue> + ): void { + let keepPolling = true + while (keepPolling && (queue.capacity() === Number.POSITIVE_INFINITY || queue.length() < queue.capacity())) { + const putter = pipe(this.putters, MutableQueue.poll(MutableQueue.EmptyMutableQueue)) + if (putter === MutableQueue.EmptyMutableQueue) { + keepPolling = false + } else { + const offered = queue.offer(putter[0]) + if (offered && putter[2]) { + unsafeCompleteDeferred(putter[1], true) + } else if (!offered) { + unsafeOfferAll(this.putters, pipe(unsafePollAll(this.putters), Chunk.prepend(putter))) + } + unsafeCompleteTakers(this, queue, takers) + } + } + } + + unsafeOffer(iterable: Iterable, deferred: Deferred.Deferred): void { + const stuff = Arr.fromIterable(iterable) + for (let i = 0; i < stuff.length; i++) { + const value = stuff[i] + if (i === stuff.length - 1) { + pipe(this.putters, MutableQueue.offer([value, deferred, true as boolean] as const)) + } else { + pipe(this.putters, MutableQueue.offer([value, deferred, false as boolean] as const)) + } + } + } + + unsafeRemove(deferred: Deferred.Deferred): void { + unsafeOfferAll( + this.putters, + pipe(unsafePollAll(this.putters), Chunk.filter(([, _]) => _ !== deferred)) + ) + } +} + +/** @internal */ +class DroppingStrategy implements Queue.Strategy { + readonly [QueueStrategyTypeId] = queueStrategyVariance + + surplusSize(): number { + return 0 + } + + get shutdown(): Effect.Effect { + return core.void + } + + onCompleteTakersWithEmptyQueue(): void { + } + + handleSurplus( + _iterable: Iterable, + _queue: Queue.BackingQueue, + _takers: MutableQueue.MutableQueue>, + _isShutdown: MutableRef.MutableRef + ): Effect.Effect { + return core.succeed(false) + } + + unsafeOnQueueEmptySpace( + _queue: Queue.BackingQueue, + _takers: MutableQueue.MutableQueue> + ): void { + // + } +} + +/** @internal */ +class SlidingStrategy implements Queue.Strategy { + readonly [QueueStrategyTypeId] = queueStrategyVariance + + surplusSize(): number { + return 0 + } + + get shutdown(): Effect.Effect { + return core.void + } + + onCompleteTakersWithEmptyQueue(): void { + } + + handleSurplus( + iterable: Iterable, + queue: Queue.BackingQueue, + takers: MutableQueue.MutableQueue>, + _isShutdown: MutableRef.MutableRef + ): Effect.Effect { + return core.sync(() => { + this.unsafeOffer(queue, iterable) + unsafeCompleteTakers(this, queue, takers) + return true + }) + } + + unsafeOnQueueEmptySpace( + _queue: Queue.BackingQueue, + _takers: MutableQueue.MutableQueue> + ): void { + // + } + + unsafeOffer(queue: Queue.BackingQueue, iterable: Iterable): void { + const iterator = iterable[Symbol.iterator]() + let next: IteratorResult + let offering = true + while (!(next = iterator.next()).done && offering) { + if (queue.capacity() === 0) { + return + } + // Poll 1 and retry + queue.poll(MutableQueue.EmptyMutableQueue) + offering = queue.offer(next.value) + } + } +} + +/** @internal */ +const unsafeCompleteDeferred = (deferred: Deferred.Deferred, a: A): void => { + return core.deferredUnsafeDone(deferred, core.succeed(a)) +} + +/** @internal */ +const unsafeOfferAll = (queue: MutableQueue.MutableQueue, as: Iterable): Chunk.Chunk => { + return pipe(queue, MutableQueue.offerAll(as)) +} + +/** @internal */ +const unsafePollAll = (queue: MutableQueue.MutableQueue): Chunk.Chunk => { + return pipe(queue, MutableQueue.pollUpTo(Number.POSITIVE_INFINITY)) +} + +/** @internal */ +const unsafePollN = (queue: MutableQueue.MutableQueue, max: number): Chunk.Chunk => { + return pipe(queue, MutableQueue.pollUpTo(max)) +} + +/** @internal */ +export const unsafeRemove = (queue: MutableQueue.MutableQueue, a: A): void => { + unsafeOfferAll( + queue, + pipe(unsafePollAll(queue), Chunk.filter((b) => a !== b)) + ) +} + +/** @internal */ +export const unsafeCompleteTakers = ( + strategy: Queue.Strategy, + queue: Queue.BackingQueue, + takers: MutableQueue.MutableQueue> +): void => { + // Check both a taker and an item are in the queue, starting with the taker + let keepPolling = true + while (keepPolling && queue.length() !== 0) { + const taker = pipe(takers, MutableQueue.poll(MutableQueue.EmptyMutableQueue)) + if (taker !== MutableQueue.EmptyMutableQueue) { + const element = queue.poll(MutableQueue.EmptyMutableQueue) + if (element !== MutableQueue.EmptyMutableQueue) { + unsafeCompleteDeferred(taker, element) + strategy.unsafeOnQueueEmptySpace(queue, takers) + } else { + unsafeOfferAll(takers, pipe(unsafePollAll(takers), Chunk.prepend(taker))) + } + keepPolling = true + } else { + keepPolling = false + } + } + if (keepPolling && queue.length() === 0 && !MutableQueue.isEmpty(takers)) { + strategy.onCompleteTakersWithEmptyQueue(takers) + } +} diff --git a/repos/effect/packages/effect/src/internal/random.ts b/repos/effect/packages/effect/src/internal/random.ts new file mode 100644 index 0000000..dd79e2a --- /dev/null +++ b/repos/effect/packages/effect/src/internal/random.ts @@ -0,0 +1,161 @@ +import type * as Arr from "../Array.js" +import * as Chunk from "../Chunk.js" +import * as Context from "../Context.js" +import type * as Effect from "../Effect.js" +import { pipe } from "../Function.js" +import * as Hash from "../Hash.js" +import type * as Random from "../Random.js" +import * as PCGRandom from "../Utils.js" +import * as core from "./core.js" + +/** @internal */ +const RandomSymbolKey = "effect/Random" + +/** @internal */ +export const RandomTypeId: Random.RandomTypeId = Symbol.for( + RandomSymbolKey +) as Random.RandomTypeId + +/** @internal */ +export const randomTag: Context.Tag = Context.GenericTag("effect/Random") +/** @internal */ +class RandomImpl implements Random.Random { + readonly [RandomTypeId]: Random.RandomTypeId = RandomTypeId + + readonly PRNG: PCGRandom.PCGRandom + + constructor(readonly seed: number) { + this.PRNG = new PCGRandom.PCGRandom(seed) + } + + get next(): Effect.Effect { + return core.sync(() => this.PRNG.number()) + } + + get nextBoolean(): Effect.Effect { + return core.map(this.next, (n) => n > 0.5) + } + + get nextInt(): Effect.Effect { + return core.sync(() => this.PRNG.integer(Number.MAX_SAFE_INTEGER)) + } + + nextRange(min: number, max: number): Effect.Effect { + return core.map(this.next, (n) => (max - min) * n + min) + } + + nextIntBetween(min: number, max: number): Effect.Effect { + return core.sync(() => this.PRNG.integer(max - min) + min) + } + + shuffle(elements: Iterable): Effect.Effect> { + return shuffleWith(elements, (n) => this.nextIntBetween(0, n)) + } +} + +const shuffleWith = ( + elements: Iterable, + nextIntBounded: (n: number) => Effect.Effect +): Effect.Effect> => { + return core.suspend(() => + pipe( + core.sync(() => Array.from(elements)), + core.flatMap((buffer) => { + const numbers: Array = [] + for (let i = buffer.length; i >= 2; i = i - 1) { + numbers.push(i) + } + return pipe( + numbers, + core.forEachSequentialDiscard((n) => + pipe( + nextIntBounded(n), + core.map((k) => swap(buffer, n - 1, k)) + ) + ), + core.as(Chunk.fromIterable(buffer)) + ) + }) + ) + ) +} + +const swap = (buffer: Array, index1: number, index2: number): Array => { + const tmp = buffer[index1]! + buffer[index1] = buffer[index2]! + buffer[index2] = tmp + return buffer +} + +export const make = (seed: A): Random.Random => new RandomImpl(Hash.hash(seed)) + +/** @internal */ +class FixedRandomImpl implements Random.Random { + readonly [RandomTypeId]: Random.RandomTypeId = RandomTypeId + + private index = 0 + + constructor(readonly values: Arr.NonEmptyArray) { + if (values.length === 0) { + throw new Error("Requires at least one value") + } + } + + private getNextValue(): any { + const value = this.values[this.index] + this.index = (this.index + 1) % this.values.length + return value + } + + get next(): Effect.Effect { + return core.sync(() => { + const value = this.getNextValue() + if (typeof value === "number") { + return Math.max(0, Math.min(1, value)) + } + return Hash.hash(value) / 2147483647 + }) + } + + get nextBoolean(): Effect.Effect { + return core.sync(() => { + const value = this.getNextValue() + if (typeof value === "boolean") { + return value + } + return Hash.hash(value) % 2 === 0 + }) + } + + get nextInt(): Effect.Effect { + return core.sync(() => { + const value = this.getNextValue() + if (typeof value === "number" && Number.isFinite(value)) { + return Math.round(value) + } + return Math.abs(Hash.hash(value)) + }) + } + + nextRange(min: number, max: number): Effect.Effect { + return core.map(this.next, (n) => (max - min) * n + min) + } + + nextIntBetween(min: number, max: number): Effect.Effect { + return core.sync(() => { + const value = this.getNextValue() + if (typeof value === "number" && Number.isFinite(value)) { + return Math.max(min, Math.min(max - 1, Math.round(value))) + } + const hash = Math.abs(Hash.hash(value)) + return min + (hash % (max - min)) + }) + } + + shuffle(elements: Iterable): Effect.Effect> { + return shuffleWith(elements, (n) => this.nextIntBetween(0, n)) + } +} + +/** @internal */ +export const fixed = >(values: T): Random.Random => new FixedRandomImpl(values) diff --git a/repos/effect/packages/effect/src/internal/rateLimiter.ts b/repos/effect/packages/effect/src/internal/rateLimiter.ts new file mode 100644 index 0000000..1207370 --- /dev/null +++ b/repos/effect/packages/effect/src/internal/rateLimiter.ts @@ -0,0 +1,93 @@ +import type { DurationInput } from "../Duration.js" +import * as Duration from "../Duration.js" +import * as Effect from "../Effect.js" +import * as FiberRef from "../FiberRef.js" +import { pipe } from "../Function.js" +import { globalValue } from "../GlobalValue.js" +import type * as RateLimiter from "../RateLimiter.js" +import type * as Scope from "../Scope.js" + +/** @internal */ +export const make = ({ + algorithm = "token-bucket", + interval, + limit +}: RateLimiter.RateLimiter.Options): Effect.Effect< + RateLimiter.RateLimiter, + never, + Scope.Scope +> => { + switch (algorithm) { + case "fixed-window": { + return fixedWindow(limit, interval) + } + case "token-bucket": { + return tokenBucket(limit, interval) + } + } +} + +const tokenBucket = (limit: number, window: DurationInput): Effect.Effect< + RateLimiter.RateLimiter, + never, + Scope.Scope +> => + Effect.gen(function*() { + const millisPerToken = Math.ceil(Duration.toMillis(window) / limit) + const semaphore = yield* Effect.makeSemaphore(limit) + const latch = yield* Effect.makeSemaphore(0) + const refill: Effect.Effect = Effect.sleep(millisPerToken).pipe( + Effect.zipRight(latch.releaseAll), + Effect.zipRight(semaphore.release(1)), + Effect.flatMap((free) => free === limit ? Effect.void : refill) + ) + yield* pipe( + latch.take(1), + Effect.zipRight(refill), + Effect.forever, + Effect.forkScoped, + Effect.interruptible + ) + const take = Effect.uninterruptibleMask((restore) => + Effect.flatMap( + FiberRef.get(currentCost), + (cost) => Effect.zipRight(restore(semaphore.take(cost)), latch.release(1)) + ) + ) + return (effect) => Effect.zipRight(take, effect) + }) + +const fixedWindow = (limit: number, window: DurationInput): Effect.Effect< + RateLimiter.RateLimiter, + never, + Scope.Scope +> => + Effect.gen(function*() { + const semaphore = yield* Effect.makeSemaphore(limit) + const latch = yield* Effect.makeSemaphore(0) + yield* pipe( + latch.take(1), + Effect.zipRight(Effect.sleep(window)), + Effect.zipRight(latch.releaseAll), + Effect.zipRight(semaphore.releaseAll), + Effect.forever, + Effect.forkScoped, + Effect.interruptible + ) + const take = Effect.uninterruptibleMask((restore) => + Effect.flatMap( + FiberRef.get(currentCost), + (cost) => Effect.zipRight(restore(semaphore.take(cost)), latch.release(1)) + ) + ) + return (effect) => Effect.zipRight(take, effect) + }) + +/** @internal */ +const currentCost = globalValue( + Symbol.for("effect/RateLimiter/currentCost"), + () => FiberRef.unsafeMake(1) +) + +/** @internal */ +export const withCost = (cost: number) => Effect.locally(currentCost, cost) diff --git a/repos/effect/packages/effect/src/internal/rcMap.ts b/repos/effect/packages/effect/src/internal/rcMap.ts new file mode 100644 index 0000000..b598022 --- /dev/null +++ b/repos/effect/packages/effect/src/internal/rcMap.ts @@ -0,0 +1,295 @@ +import type * as Cause from "../Cause.js" +import * as Context from "../Context.js" +import type * as Deferred from "../Deferred.js" +import * as Duration from "../Duration.js" +import type { Effect } from "../Effect.js" +import type { RuntimeFiber } from "../Fiber.js" +import { constant, dual, flow, identity } from "../Function.js" +import * as MutableHashMap from "../MutableHashMap.js" +import { pipeArguments } from "../Pipeable.js" +import type * as RcMap from "../RcMap.js" +import type * as Scope from "../Scope.js" +import * as coreEffect from "./core-effect.js" +import * as core from "./core.js" +import * as circular from "./effect/circular.js" +import * as fiberRuntime from "./fiberRuntime.js" + +/** @internal */ +export const TypeId: RcMap.TypeId = Symbol.for("effect/RcMap") as RcMap.TypeId + +type State = State.Open | State.Closed + +declare namespace State { + interface Open { + readonly _tag: "Open" + readonly map: MutableHashMap.MutableHashMap> + } + + interface Closed { + readonly _tag: "Closed" + } + + interface Entry { + readonly deferred: Deferred.Deferred + readonly scope: Scope.CloseableScope + readonly finalizer: Effect + readonly idleTimeToLive: Duration.Duration + fiber: RuntimeFiber | undefined + expiresAt: number + refCount: number + } +} + +const variance: RcMap.RcMap.Variance = { + _K: identity, + _A: identity, + _E: identity +} + +class RcMapImpl implements RcMap.RcMap { + readonly [TypeId]: RcMap.RcMap.Variance + + state: State = { + _tag: "Open", + map: MutableHashMap.empty() + } + readonly semaphore = circular.unsafeMakeSemaphore(1) + + constructor( + readonly lookup: (key: K) => Effect, + readonly context: Context.Context, + readonly scope: Scope.Scope, + readonly idleTimeToLive: ((key: K) => Duration.Duration) | undefined, + readonly capacity: number + ) { + this[TypeId] = variance + } + + pipe() { + return pipeArguments(this, arguments) + } +} + +/** @internal */ +export const make: { + (options: { + readonly lookup: (key: K) => Effect + readonly idleTimeToLive?: Duration.DurationInput | ((key: K) => Duration.DurationInput) | undefined + readonly capacity?: undefined + }): Effect, never, Scope.Scope | R> + (options: { + readonly lookup: (key: K) => Effect + readonly idleTimeToLive?: Duration.DurationInput | ((key: K) => Duration.DurationInput) | undefined + readonly capacity: number + }): Effect, never, Scope.Scope | R> +} = (options: { + readonly lookup: (key: K) => Effect + readonly idleTimeToLive?: Duration.DurationInput | ((key: K) => Duration.DurationInput) | undefined + readonly capacity?: number | undefined +}) => + core.withFiberRuntime, never, R | Scope.Scope>((fiber) => { + const context = fiber.getFiberRef(core.currentContext) as Context.Context + const scope = Context.get(context, fiberRuntime.scopeTag) + const idleTimeToLive = options.idleTimeToLive === undefined + ? undefined + : typeof options.idleTimeToLive === "function" + ? flow(options.idleTimeToLive, Duration.decode) + : constant(Duration.decode(options.idleTimeToLive)) + const self = new RcMapImpl( + options.lookup as any, + context, + scope, + idleTimeToLive, + Math.max(options.capacity ?? Number.POSITIVE_INFINITY, 0) + ) + return core.as( + scope.addFinalizer(() => + core.suspend(() => { + if (self.state._tag === "Closed") { + return core.void + } + const map = self.state.map + self.state = { _tag: "Closed" } + return core.forEachSequentialDiscard( + map, + ([, entry]) => core.scopeClose(entry.scope, core.exitVoid) + ).pipe( + core.tap(() => { + MutableHashMap.clear(map) + }), + self.semaphore.withPermits(1) + ) + }) + ), + self + ) + }) + +/** @internal */ +export const get: { + (key: K): (self: RcMap.RcMap) => Effect + (self: RcMap.RcMap, key: K): Effect +} = dual(2, (self_: RcMap.RcMap, key: K): Effect => { + const self = self_ as RcMapImpl + return core.uninterruptibleMask((restore) => getImpl(self, key, restore as any)) +}) + +const getImpl = core.fnUntraced(function*(self: RcMapImpl, key: K, restore: (a: A) => A) { + if (self.state._tag === "Closed") { + return yield* core.interrupt + } + const state = self.state + const o = MutableHashMap.get(state.map, key) + let entry: State.Entry + if (o._tag === "Some") { + entry = o.value + entry.refCount++ + } else if (Number.isFinite(self.capacity) && MutableHashMap.size(self.state.map) >= self.capacity) { + return yield* core.fail( + new core.ExceededCapacityException(`RcMap attempted to exceed capacity of ${self.capacity}`) + ) as Effect + } else { + entry = yield* self.semaphore.withPermits(1)(acquire(self, key, restore)) + } + const scope = yield* fiberRuntime.scopeTag + yield* scope.addFinalizer(() => entry.finalizer) + return yield* restore(core.deferredAwait(entry.deferred)) +}) + +const acquire = core.fnUntraced(function*(self: RcMapImpl, key: K, restore: (a: A) => A) { + const scope = yield* fiberRuntime.scopeMake() + const deferred = yield* core.deferredMake() + const acquire = self.lookup(key) + const contextMap = new Map(self.context.unsafeMap) + yield* restore(core.mapInputContext( + acquire as Effect, + (inputContext: Context.Context) => { + inputContext.unsafeMap.forEach((value, key) => { + contextMap.set(key, value) + }) + contextMap.set(fiberRuntime.scopeTag.key, scope) + return Context.unsafeMake(contextMap) + } + )).pipe( + core.exit, + core.flatMap((exit) => core.deferredDone(deferred, exit)), + circular.forkIn(scope) + ) + const idleTimeToLive = self.idleTimeToLive ? self.idleTimeToLive(key) : Duration.zero + const entry: State.Entry = { + deferred, + scope, + finalizer: undefined as any, + idleTimeToLive, + fiber: undefined, + expiresAt: 0, + refCount: 1 + } + ;(entry as any).finalizer = release(self, key, entry) + if (self.state._tag === "Open") { + MutableHashMap.set(self.state.map, key, entry) + } + return entry +}) + +const release = (self: RcMapImpl, key: K, entry: State.Entry) => + coreEffect.clockWith((clock) => { + entry.refCount-- + if (entry.refCount > 0) { + return core.void + } else if ( + self.state._tag === "Closed" + || !MutableHashMap.has(self.state.map, key) + || Duration.isZero(entry.idleTimeToLive) + ) { + if (self.state._tag === "Open") { + MutableHashMap.remove(self.state.map, key) + } + return core.scopeClose(entry.scope, core.exitVoid) + } + + if (!Duration.isFinite(entry.idleTimeToLive)) { + return core.void + } + + entry.expiresAt = clock.unsafeCurrentTimeMillis() + Duration.toMillis(entry.idleTimeToLive) + if (entry.fiber) return core.void + + return core.interruptibleMask(function loop(restore): Effect { + const now = clock.unsafeCurrentTimeMillis() + const remaining = entry.expiresAt - now + if (remaining <= 0) { + if (self.state._tag === "Closed" || entry.refCount > 0) return core.void + MutableHashMap.remove(self.state.map, key) + return restore(core.scopeClose(entry.scope, core.exitVoid)) + } + return core.flatMap(clock.sleep(Duration.millis(remaining)), () => loop(restore)) + }).pipe( + fiberRuntime.ensuring(core.sync(() => { + entry.fiber = undefined + })), + circular.forkIn(self.scope), + core.tap((fiber) => { + entry.fiber = fiber + }), + self.semaphore.withPermits(1) + ) + }) + +/** @internal */ +export const keys = (self: RcMap.RcMap): Effect> => { + const impl = self as RcMapImpl + return core.suspend(() => + impl.state._tag === "Closed" ? core.interrupt : core.succeed(MutableHashMap.keys(impl.state.map)) + ) +} + +/** @internal */ +export const invalidate: { + (key: K): (self: RcMap.RcMap) => Effect + (self: RcMap.RcMap, key: K): Effect +} = dual( + 2, + core.fnUntraced(function*(self_: RcMap.RcMap, key: K) { + const self = self_ as RcMapImpl + if (self.state._tag === "Closed") return + const o = MutableHashMap.get(self.state.map, key) + if (o._tag === "None") return + const entry = o.value + MutableHashMap.remove(self.state.map, key) + if (entry.refCount > 0) return + yield* core.scopeClose(entry.scope, core.exitVoid) + if (entry.fiber) yield* core.interruptFiber(entry.fiber) + }) +) + +/** @internal */ +export const has: { + (key: K): (self: RcMap.RcMap) => Effect + (self: RcMap.RcMap, key: K): Effect +} = dual(2, (self_: RcMap.RcMap, key: K): Effect => { + const self = self_ as RcMapImpl + return core.sync(() => { + if (self.state._tag === "Closed") return false + return MutableHashMap.has(self.state.map, key) + }) +}) + +/** @internal */ +export const touch: { + (key: K): (self: RcMap.RcMap) => Effect + (self: RcMap.RcMap, key: K): Effect +} = dual( + 2, + (self_: RcMap.RcMap, key: K) => + coreEffect.clockWith((clock) => { + const self = self_ as RcMapImpl + if (self.state._tag === "Closed") return core.void + const o = MutableHashMap.get(self.state.map, key) + if (o._tag === "None") return core.void + const entry = o.value + if (Duration.isZero(entry.idleTimeToLive)) return core.void + entry.expiresAt = clock.unsafeCurrentTimeMillis() + Duration.toMillis(entry.idleTimeToLive) + return core.void + }) +) diff --git a/repos/effect/packages/effect/src/internal/rcRef.ts b/repos/effect/packages/effect/src/internal/rcRef.ts new file mode 100644 index 0000000..8ff2ca4 --- /dev/null +++ b/repos/effect/packages/effect/src/internal/rcRef.ts @@ -0,0 +1,192 @@ +import * as Context from "../Context.js" +import * as Duration from "../Duration.js" +import type { Effect } from "../Effect.js" +import * as Effectable from "../Effectable.js" +import type { RuntimeFiber } from "../Fiber.js" +import { identity } from "../Function.js" +import type * as RcRef from "../RcRef.js" +import * as Readable from "../Readable.js" +import type * as Scope from "../Scope.js" +import * as coreEffect from "./core-effect.js" +import * as core from "./core.js" +import * as circular from "./effect/circular.js" +import * as fiberRuntime from "./fiberRuntime.js" + +/** @internal */ +export const TypeId: RcRef.TypeId = Symbol.for("effect/RcRef") as RcRef.TypeId + +type State = State.Empty | State.Acquired | State.Closed + +declare namespace State { + interface Empty { + readonly _tag: "Empty" + } + + interface Acquired { + readonly _tag: "Acquired" + readonly value: A + readonly scope: Scope.CloseableScope + fiber: RuntimeFiber | undefined + refCount: number + } + + interface Closed { + readonly _tag: "Closed" + } +} + +const stateEmpty: State = { _tag: "Empty" } +const stateClosed: State = { _tag: "Closed" } + +const variance: RcRef.RcRef.Variance = { + _A: identity, + _E: identity +} + +class RcRefImpl extends Effectable.Class implements RcRef.RcRef { + readonly [TypeId]: RcRef.RcRef.Variance = variance + readonly [Readable.TypeId]: Readable.TypeId = Readable.TypeId + + state: State = stateEmpty + readonly semaphore = circular.unsafeMakeSemaphore(1) + + constructor( + readonly acquire: Effect, + readonly context: Context.Context, + readonly scope: Scope.Scope, + readonly idleTimeToLive: Duration.Duration | undefined + ) { + super() + this.get = get(this) + } + readonly get: Effect + + commit() { + return this.get + } +} + +/** @internal */ +export const make = (options: { + readonly acquire: Effect + readonly idleTimeToLive?: Duration.DurationInput | undefined +}) => + core.withFiberRuntime, never, R | Scope.Scope>((fiber) => { + const context = fiber.getFiberRef(core.currentContext) as Context.Context + const scope = Context.get(context, fiberRuntime.scopeTag) + const ref = new RcRefImpl( + options.acquire as Effect, + context, + scope, + options.idleTimeToLive ? Duration.decode(options.idleTimeToLive) : undefined + ) + return core.as( + scope.addFinalizer(() => + ref.semaphore.withPermits(1)(core.suspend(() => { + const close = ref.state._tag === "Acquired" + ? core.scopeClose(ref.state.scope, core.exitVoid) + : core.void + ref.state = stateClosed + return close + })) + ), + ref + ) + }) + +/** @internal */ +export const get = ( + self_: RcRef.RcRef +): Effect => { + const self = self_ as RcRefImpl + const isInfinite = self.idleTimeToLive && !Duration.isFinite(self.idleTimeToLive) + return core.uninterruptibleMask((restore) => + core.suspend(() => { + switch (self.state._tag) { + case "Closed": { + return core.interrupt + } + case "Acquired": { + self.state.refCount++ + return self.state.fiber + ? core.as(core.interruptFiber(self.state.fiber), self.state) + : core.succeed(self.state) + } + case "Empty": { + return fiberRuntime.scopeMake().pipe( + coreEffect.bindTo("scope"), + coreEffect.bind("value", ({ scope }) => + restore(core.fiberRefLocally( + self.acquire as Effect, + core.currentContext, + Context.add(self.context, fiberRuntime.scopeTag, scope) + ))), + core.map(({ scope, value }) => { + const state: State.Acquired = { + _tag: "Acquired", + value, + scope, + fiber: undefined, + refCount: 1 + } + self.state = state + return state + }) + ) + } + } + }) + ).pipe( + self.semaphore.withPermits(1), + coreEffect.bindTo("state"), + coreEffect.bind("scope", () => fiberRuntime.scopeTag), + core.tap(({ scope, state }) => + scope.addFinalizer(() => + core.suspend(() => { + state.refCount-- + if (state.refCount > 0 || isInfinite) { + return core.void + } + if (self.idleTimeToLive === undefined) { + self.state = stateEmpty + return core.scopeClose(state.scope, core.exitVoid) + } + return coreEffect.sleep(self.idleTimeToLive).pipe( + core.interruptible, + core.zipRight(core.suspend(() => { + if (self.state._tag === "Acquired" && self.state.refCount === 0) { + self.state = stateEmpty + return core.scopeClose(state.scope, core.exitVoid) + } + return core.void + })), + fiberRuntime.ensuring(core.sync(() => { + state.fiber = undefined + })), + circular.forkIn(self.scope), + core.tap((fiber) => { + state.fiber = fiber + }), + self.semaphore.withPermits(1) + ) + }) + ) + ), + core.map(({ state }) => state.value) + ) +} + +/** @internal */ +export const invalidate = ( + self_: RcRef.RcRef +): Effect => { + const self = self_ as RcRefImpl + return core.uninterruptible(core.suspend(() => { + if (self.state._tag !== "Acquired") { + return core.void + } + const state = self.state + self.state = stateEmpty + return state.scope.close(core.exitVoid) + })) +} diff --git a/repos/effect/packages/effect/src/internal/redBlackTree.ts b/repos/effect/packages/effect/src/internal/redBlackTree.ts new file mode 100644 index 0000000..9076ff9 --- /dev/null +++ b/repos/effect/packages/effect/src/internal/redBlackTree.ts @@ -0,0 +1,1245 @@ +import * as Chunk from "../Chunk.js" +import * as Equal from "../Equal.js" +import { dual, pipe } from "../Function.js" +import * as Hash from "../Hash.js" +import { format, NodeInspectSymbol, toJSON } from "../Inspectable.js" +import * as Option from "../Option.js" +import type * as Order from "../Order.js" +import type * as Ordering from "../Ordering.js" +import { pipeArguments } from "../Pipeable.js" +import { hasProperty } from "../Predicate.js" +import type * as RBT from "../RedBlackTree.js" +import { Direction, RedBlackTreeIterator } from "./redBlackTree/iterator.js" +import * as Node from "./redBlackTree/node.js" +import * as Stack from "./stack.js" + +const RedBlackTreeSymbolKey = "effect/RedBlackTree" + +/** @internal */ +export const RedBlackTreeTypeId: RBT.TypeId = Symbol.for(RedBlackTreeSymbolKey) as RBT.TypeId + +/** @internal */ +export interface RedBlackTreeImpl extends RBT.RedBlackTree { + readonly _ord: Order.Order + readonly _root: Node.Node | undefined +} + +const redBlackTreeVariance = { + /* c8 ignore next */ + _Key: (_: any) => _, + /* c8 ignore next */ + _Value: (_: never) => _ +} + +const RedBlackTreeProto: RBT.RedBlackTree = { + [RedBlackTreeTypeId]: redBlackTreeVariance, + [Hash.symbol](this: RBT.RedBlackTree): number { + let hash = Hash.hash(RedBlackTreeSymbolKey) + for (const item of this) { + hash ^= pipe(Hash.hash(item[0]), Hash.combine(Hash.hash(item[1]))) + } + return Hash.cached(this, hash) + }, + [Equal.symbol](this: RedBlackTreeImpl, that: unknown): boolean { + if (isRedBlackTree(that)) { + if ((this._root?.count ?? 0) !== ((that as RedBlackTreeImpl)._root?.count ?? 0)) { + return false + } + const entries = Array.from(that) + return Array.from(this).every((itemSelf, i) => { + const itemThat = entries[i] + return Equal.equals(itemSelf[0], itemThat[0]) && Equal.equals(itemSelf[1], itemThat[1]) + }) + } + return false + }, + [Symbol.iterator](this: RedBlackTreeImpl): RedBlackTreeIterator { + const stack: Array> = [] + let n = this._root + while (n != null) { + stack.push(n) + n = n.left + } + return new RedBlackTreeIterator(this, stack, Direction.Forward) + }, + toString() { + return format(this.toJSON()) + }, + toJSON() { + return { + _id: "RedBlackTree", + values: Array.from(this).map(toJSON) + } + }, + [NodeInspectSymbol]() { + return this.toJSON() + }, + pipe() { + return pipeArguments(this, arguments) + } +} + +const makeImpl = (ord: Order.Order, root: Node.Node | undefined): RedBlackTreeImpl => { + const tree = Object.create(RedBlackTreeProto) + tree._ord = ord + tree._root = root + return tree +} + +/** @internal */ +export const isRedBlackTree: { + (u: Iterable): u is RBT.RedBlackTree + (u: unknown): u is RBT.RedBlackTree +} = (u: unknown): u is RBT.RedBlackTree => hasProperty(u, RedBlackTreeTypeId) + +/** @internal */ +export const empty = (ord: Order.Order): RBT.RedBlackTree => makeImpl(ord, undefined) + +/** @internal */ +export const fromIterable = dual< + (ord: Order.Order) => (entries: Iterable) => RBT.RedBlackTree, + (entries: Iterable, ord: Order.Order) => RBT.RedBlackTree +>(2, (entries: Iterable, ord: Order.Order) => { + let tree = empty(ord) + for (const [key, value] of entries) { + tree = insert(tree, key, value) + } + return tree +}) + +/** @internal */ +export const make = + (ord: Order.Order) => + >(...entries: Entries): RBT.RedBlackTree< + K, + Entries[number] extends readonly [any, infer V] ? V : never + > => { + return fromIterable(entries, ord) + } + +/** @internal */ +export const atBackwards = dual< + (index: number) => (self: RBT.RedBlackTree) => Iterable<[K, V]>, + (self: RBT.RedBlackTree, index: number) => Iterable<[K, V]> +>(2, (self, index) => at(self, index, Direction.Backward)) + +/** @internal */ +export const atForwards = dual< + (index: number) => (self: RBT.RedBlackTree) => Iterable<[K, V]>, + (self: RBT.RedBlackTree, index: number) => Iterable<[K, V]> +>(2, (self, index) => at(self, index, Direction.Forward)) + +const at = ( + self: RBT.RedBlackTree, + index: number, + direction: RBT.RedBlackTree.Direction +): Iterable<[K, V]> => { + return { + [Symbol.iterator]: () => { + if (index < 0) { + return new RedBlackTreeIterator(self, [], direction) + } + let node = (self as RedBlackTreeImpl)._root + const stack: Array> = [] + while (node !== undefined) { + stack.push(node) + if (node.left !== undefined) { + if (index < node.left.count) { + node = node.left + continue + } + index -= node.left.count + } + if (!index) { + return new RedBlackTreeIterator(self, stack, direction) + } + index -= 1 + if (node.right !== undefined) { + if (index >= node.right.count) { + break + } + node = node.right + } else { + break + } + } + return new RedBlackTreeIterator(self, [], direction) + } + } +} + +/** @internal */ +export const findAll = dual< + (key: K) => (self: RBT.RedBlackTree) => Chunk.Chunk, + (self: RBT.RedBlackTree, key: K) => Chunk.Chunk +>(2, (self: RBT.RedBlackTree, key: K) => { + const stack: Array> = [] + let node = (self as RedBlackTreeImpl)._root + let result = Chunk.empty() + while (node !== undefined || stack.length > 0) { + if (node) { + stack.push(node) + node = node.left + } else { + const current = stack.pop()! + if (Equal.equals(key, current.key)) { + result = Chunk.prepend(current.value)(result) + } + node = current.right + } + } + return result +}) + +/** @internal */ +export const findFirst = dual< + (key: K) => (self: RBT.RedBlackTree) => Option.Option, + (self: RBT.RedBlackTree, key: K) => Option.Option +>(2, (self: RBT.RedBlackTree, key: K) => { + const cmp = (self as RedBlackTreeImpl)._ord + let node = (self as RedBlackTreeImpl)._root + while (node !== undefined) { + const d = cmp(key, node.key) + if (Equal.equals(key, node.key)) { + return Option.some(node.value) + } + if (d <= 0) { + node = node.left + } else { + node = node.right + } + } + return Option.none() +}) + +/** @internal */ +export const first = (self: RBT.RedBlackTree): Option.Option<[K, V]> => { + let node: Node.Node | undefined = (self as RedBlackTreeImpl)._root + let current: Node.Node | undefined = (self as RedBlackTreeImpl)._root + while (node !== undefined) { + current = node + node = node.left + } + return current ? Option.some([current.key, current.value]) : Option.none() +} + +/** @internal */ +export const getAt = dual< + (index: number) => (self: RBT.RedBlackTree) => Option.Option<[K, V]>, + (self: RBT.RedBlackTree, index: number) => Option.Option<[K, V]> +>(2, (self: RBT.RedBlackTree, index: number) => { + if (index < 0) { + return Option.none() + } + let root = (self as RedBlackTreeImpl)._root + let node: Node.Node | undefined = undefined + while (root !== undefined) { + node = root + if (root.left) { + if (index < root.left.count) { + root = root.left + continue + } + index -= root.left.count + } + if (!index) { + return Option.some([node.key, node.value]) + } + index -= 1 + if (root.right) { + if (index >= root.right.count) { + break + } + root = root.right + } else { + break + } + } + return Option.none() +}) + +/** @internal */ +export const getOrder = (tree: RBT.RedBlackTree): Order.Order => (tree as RedBlackTreeImpl)._ord + +/** @internal */ +export const has = dual< + (key: K) => (self: RBT.RedBlackTree) => boolean, + (self: RBT.RedBlackTree, key: K) => boolean +>(2, (self, key) => Option.isSome(findFirst(self, key))) + +/** @internal */ +export const insert = dual< + (key: K, value: V) => (self: RBT.RedBlackTree) => RBT.RedBlackTree, + (self: RBT.RedBlackTree, key: K, value: V) => RBT.RedBlackTree +>(3, (self: RBT.RedBlackTree, key: K, value: V) => { + const cmp = (self as RedBlackTreeImpl)._ord + // Find point to insert new node at + let n: Node.Node | undefined = (self as RedBlackTreeImpl)._root + const n_stack: Array> = [] + const d_stack: Array = [] + while (n != null) { + const d = cmp(key, n.key) + n_stack.push(n) + d_stack.push(d) + if (d <= 0) { + n = n.left + } else { + n = n.right + } + } + // Rebuild path to leaf node + n_stack.push({ + color: Node.Color.Red, + key, + value, + left: undefined, + right: undefined, + count: 1 + }) + for (let s = n_stack.length - 2; s >= 0; --s) { + const n2 = n_stack[s]! + if (d_stack[s]! <= 0) { + n_stack[s] = { + color: n2.color, + key: n2.key, + value: n2.value, + left: n_stack[s + 1], + right: n2.right, + count: n2.count + 1 + } + } else { + n_stack[s] = { + color: n2.color, + key: n2.key, + value: n2.value, + left: n2.left, + right: n_stack[s + 1], + count: n2.count + 1 + } + } + } + // Rebalance tree using rotations + for (let s = n_stack.length - 1; s > 1; --s) { + const p = n_stack[s - 1]! + const n3 = n_stack[s]! + if (p.color === Node.Color.Black || n3.color === Node.Color.Black) { + break + } + const pp = n_stack[s - 2]! + if (pp.left === p) { + if (p.left === n3) { + const y = pp.right + if (y && y.color === Node.Color.Red) { + p.color = Node.Color.Black + pp.right = Node.repaint(y, Node.Color.Black) + pp.color = Node.Color.Red + s -= 1 + } else { + pp.color = Node.Color.Red + pp.left = p.right + p.color = Node.Color.Black + p.right = pp + n_stack[s - 2] = p + n_stack[s - 1] = n3 + Node.recount(pp) + Node.recount(p) + if (s >= 3) { + const ppp = n_stack[s - 3]! + if (ppp.left === pp) { + ppp.left = p + } else { + ppp.right = p + } + } + break + } + } else { + const y = pp.right + if (y && y.color === Node.Color.Red) { + p.color = Node.Color.Black + pp.right = Node.repaint(y, Node.Color.Black) + pp.color = Node.Color.Red + s -= 1 + } else { + p.right = n3.left + pp.color = Node.Color.Red + pp.left = n3.right + n3.color = Node.Color.Black + n3.left = p + n3.right = pp + n_stack[s - 2] = n3 + n_stack[s - 1] = p + Node.recount(pp) + Node.recount(p) + Node.recount(n3) + if (s >= 3) { + const ppp = n_stack[s - 3]! + if (ppp.left === pp) { + ppp.left = n3 + } else { + ppp.right = n3 + } + } + break + } + } + } else { + if (p.right === n3) { + const y = pp.left + if (y && y.color === Node.Color.Red) { + p.color = Node.Color.Black + pp.left = Node.repaint(y, Node.Color.Black) + pp.color = Node.Color.Red + s -= 1 + } else { + pp.color = Node.Color.Red + pp.right = p.left + p.color = Node.Color.Black + p.left = pp + n_stack[s - 2] = p + n_stack[s - 1] = n3 + Node.recount(pp) + Node.recount(p) + if (s >= 3) { + const ppp = n_stack[s - 3]! + if (ppp.right === pp) { + ppp.right = p + } else { + ppp.left = p + } + } + break + } + } else { + const y = pp.left + if (y && y.color === Node.Color.Red) { + p.color = Node.Color.Black + pp.left = Node.repaint(y, Node.Color.Black) + pp.color = Node.Color.Red + s -= 1 + } else { + p.left = n3.right + pp.color = Node.Color.Red + pp.right = n3.left + n3.color = Node.Color.Black + n3.right = p + n3.left = pp + n_stack[s - 2] = n3 + n_stack[s - 1] = p + Node.recount(pp) + Node.recount(p) + Node.recount(n3) + if (s >= 3) { + const ppp = n_stack[s - 3]! + if (ppp.right === pp) { + ppp.right = n3 + } else { + ppp.left = n3 + } + } + break + } + } + } + } + // Return new tree + n_stack[0]!.color = Node.Color.Black + return makeImpl((self as RedBlackTreeImpl)._ord, n_stack[0]) +}) + +/** @internal */ +export const keysForward = (self: RBT.RedBlackTree): IterableIterator => keys(self, Direction.Forward) + +/** @internal */ +export const keysBackward = (self: RBT.RedBlackTree): IterableIterator => keys(self, Direction.Backward) + +const keys = ( + self: RBT.RedBlackTree, + direction: RBT.RedBlackTree.Direction +): IterableIterator => { + const begin: RedBlackTreeIterator = self[Symbol.iterator]() as RedBlackTreeIterator + let count = 0 + return { + [Symbol.iterator]: () => keys(self, direction), + next: (): IteratorResult => { + count++ + const entry = begin.key + if (direction === Direction.Forward) { + begin.moveNext() + } else { + begin.movePrev() + } + switch (entry._tag) { + case "None": { + return { done: true, value: count } + } + case "Some": { + return { done: false, value: entry.value } + } + } + } + } +} + +/** @internal */ +export const last = (self: RBT.RedBlackTree): Option.Option<[K, V]> => { + let node: Node.Node | undefined = (self as RedBlackTreeImpl)._root + let current: Node.Node | undefined = (self as RedBlackTreeImpl)._root + while (node !== undefined) { + current = node + node = node.right + } + return current ? Option.some([current.key, current.value]) : Option.none() +} + +/** @internal */ +export const reversed = (self: RBT.RedBlackTree): Iterable<[K, V]> => { + return { + [Symbol.iterator]: () => { + const stack: Array> = [] + let node = (self as RedBlackTreeImpl)._root + while (node !== undefined) { + stack.push(node) + node = node.right + } + return new RedBlackTreeIterator(self, stack, Direction.Backward) + } + } +} + +/** @internal */ +export const greaterThanBackwards = dual< + (key: K) => (self: RBT.RedBlackTree) => Iterable<[K, V]>, + (self: RBT.RedBlackTree, key: K) => Iterable<[K, V]> +>(2, (self, key) => greaterThan(self, key, Direction.Backward)) + +/** @internal */ +export const greaterThanForwards = dual< + (key: K) => (self: RBT.RedBlackTree) => Iterable<[K, V]>, + (self: RBT.RedBlackTree, key: K) => Iterable<[K, V]> +>(2, (self, key) => greaterThan(self, key, Direction.Forward)) + +const greaterThan = ( + self: RBT.RedBlackTree, + key: K, + direction: RBT.RedBlackTree.Direction +): Iterable<[K, V]> => { + return { + [Symbol.iterator]: () => { + const cmp = (self as RedBlackTreeImpl)._ord + let node = (self as RedBlackTreeImpl)._root + const stack = [] + let last_ptr = 0 + while (node !== undefined) { + const d = cmp(key, node.key) + stack.push(node) + if (d < 0) { + last_ptr = stack.length + } + if (d < 0) { + node = node.left + } else { + node = node.right + } + } + stack.length = last_ptr + return new RedBlackTreeIterator(self, stack, direction) + } + } +} + +/** @internal */ +export const greaterThanEqualBackwards = dual< + (key: K) => (self: RBT.RedBlackTree) => Iterable<[K, V]>, + (self: RBT.RedBlackTree, key: K) => Iterable<[K, V]> +>(2, (self, key) => greaterThanEqual(self, key, Direction.Backward)) + +/** @internal */ +export const greaterThanEqualForwards = dual< + (key: K) => (self: RBT.RedBlackTree) => Iterable<[K, V]>, + (self: RBT.RedBlackTree, key: K) => Iterable<[K, V]> +>(2, (self, key) => greaterThanEqual(self, key, Direction.Forward)) + +const greaterThanEqual = ( + self: RBT.RedBlackTree, + key: K, + direction: RBT.RedBlackTree.Direction = Direction.Forward +): Iterable<[K, V]> => { + return { + [Symbol.iterator]: () => { + const cmp = (self as RedBlackTreeImpl)._ord + let node = (self as RedBlackTreeImpl)._root + const stack = [] + let last_ptr = 0 + while (node !== undefined) { + const d = cmp(key, node.key) + stack.push(node) + if (d <= 0) { + last_ptr = stack.length + } + if (d <= 0) { + node = node.left + } else { + node = node.right + } + } + stack.length = last_ptr + return new RedBlackTreeIterator(self, stack, direction) + } + } +} + +/** @internal */ +export const lessThanBackwards = dual< + (key: K) => (self: RBT.RedBlackTree) => Iterable<[K, V]>, + (self: RBT.RedBlackTree, key: K) => Iterable<[K, V]> +>(2, (self, key) => lessThan(self, key, Direction.Backward)) + +/** @internal */ +export const lessThanForwards = dual< + (key: K) => (self: RBT.RedBlackTree) => Iterable<[K, V]>, + (self: RBT.RedBlackTree, key: K) => Iterable<[K, V]> +>(2, (self, key) => lessThan(self, key, Direction.Forward)) + +const lessThan = ( + self: RBT.RedBlackTree, + key: K, + direction: RBT.RedBlackTree.Direction +): Iterable<[K, V]> => { + return { + [Symbol.iterator]: () => { + const cmp = (self as RedBlackTreeImpl)._ord + let node = (self as RedBlackTreeImpl)._root + const stack = [] + let last_ptr = 0 + while (node !== undefined) { + const d = cmp(key, node.key) + stack.push(node) + if (d > 0) { + last_ptr = stack.length + } + if (d <= 0) { + node = node.left + } else { + node = node.right + } + } + stack.length = last_ptr + return new RedBlackTreeIterator(self, stack, direction) + } + } +} + +/** @internal */ +export const lessThanEqualBackwards = dual< + (key: K) => (self: RBT.RedBlackTree) => Iterable<[K, V]>, + (self: RBT.RedBlackTree, key: K) => Iterable<[K, V]> +>(2, (self, key) => lessThanEqual(self, key, Direction.Backward)) + +/** @internal */ +export const lessThanEqualForwards = dual< + (key: K) => (self: RBT.RedBlackTree) => Iterable<[K, V]>, + (self: RBT.RedBlackTree, key: K) => Iterable<[K, V]> +>(2, (self, key) => lessThanEqual(self, key, Direction.Forward)) + +const lessThanEqual = ( + self: RBT.RedBlackTree, + key: K, + direction: RBT.RedBlackTree.Direction +): Iterable<[K, V]> => { + return { + [Symbol.iterator]: () => { + const cmp = (self as RedBlackTreeImpl)._ord + let node = (self as RedBlackTreeImpl)._root + const stack = [] + let last_ptr = 0 + while (node !== undefined) { + const d = cmp(key, node.key) + stack.push(node) + if (d >= 0) { + last_ptr = stack.length + } + if (d < 0) { + node = node.left + } else { + node = node.right + } + } + stack.length = last_ptr + return new RedBlackTreeIterator(self, stack, direction) + } + } +} + +/** @internal */ +export const forEach = dual< + (f: (key: K, value: V) => void) => (self: RBT.RedBlackTree) => void, + (self: RBT.RedBlackTree, f: (key: K, value: V) => void) => void +>(2, (self: RBT.RedBlackTree, f: (key: K, value: V) => void) => { + const root = (self as RedBlackTreeImpl)._root + if (root !== undefined) { + visitFull(root, (key, value) => { + f(key, value) + return Option.none() + }) + } +}) + +/** @internal */ +export const forEachGreaterThanEqual = dual< + (min: K, f: (key: K, value: V) => void) => (self: RBT.RedBlackTree) => void, + (self: RBT.RedBlackTree, min: K, f: (key: K, value: V) => void) => void +>(3, (self: RBT.RedBlackTree, min: K, f: (key: K, value: V) => void) => { + const root = (self as RedBlackTreeImpl)._root + const ord = (self as RedBlackTreeImpl)._ord + if (root !== undefined) { + visitGreaterThanEqual(root, min, ord, (key, value) => { + f(key, value) + return Option.none() + }) + } +}) + +/** @internal */ +export const forEachLessThan = dual< + (max: K, f: (key: K, value: V) => void) => (self: RBT.RedBlackTree) => void, + (self: RBT.RedBlackTree, max: K, f: (key: K, value: V) => void) => void +>(3, (self: RBT.RedBlackTree, max: K, f: (key: K, value: V) => void) => { + const root = (self as RedBlackTreeImpl)._root + const ord = (self as RedBlackTreeImpl)._ord + if (root !== undefined) { + visitLessThan(root, max, ord, (key, value) => { + f(key, value) + return Option.none() + }) + } +}) + +/** @internal */ +export const forEachBetween = dual< + (options: { + readonly min: K + readonly max: K + readonly body: (key: K, value: V) => void + }) => (self: RBT.RedBlackTree) => void, + (self: RBT.RedBlackTree, options: { + readonly min: K + readonly max: K + readonly body: (key: K, value: V) => void + }) => void +>(2, (self: RBT.RedBlackTree, { body, max, min }: { + readonly min: K + readonly max: K + readonly body: (key: K, value: V) => void +}) => { + const root = (self as RedBlackTreeImpl)._root + const ord = (self as RedBlackTreeImpl)._ord + if (root) { + visitBetween(root, min, max, ord, (key, value) => { + body(key, value) + return Option.none() + }) + } +}) + +/** @internal */ +export const reduce = dual< + ( + zero: Z, + f: (accumulator: Z, value: V, key: K) => Z + ) => (self: RBT.RedBlackTree) => Z, + (self: RBT.RedBlackTree, zero: Z, f: (accumulator: Z, value: V, key: K) => Z) => Z +>(3, (self, zero, f) => { + let accumulator = zero + for (const entry of self) { + accumulator = f(accumulator, entry[1], entry[0]) + } + return accumulator +}) + +/** @internal */ +export const removeFirst = dual< + (key: K) => (self: RBT.RedBlackTree) => RBT.RedBlackTree, + (self: RBT.RedBlackTree, key: K) => RBT.RedBlackTree +>(2, (self: RBT.RedBlackTree, key: K) => { + if (!has(self, key)) { + return self + } + const ord = (self as RedBlackTreeImpl)._ord + const cmp = ord + let node: Node.Node | undefined = (self as RedBlackTreeImpl)._root + const stack = [] + while (node !== undefined) { + const d = cmp(key, node.key) + stack.push(node) + if (Equal.equals(key, node.key)) { + node = undefined + } else if (d <= 0) { + node = node.left + } else { + node = node.right + } + } + if (stack.length === 0) { + return self + } + const cstack = new Array>(stack.length) + let n = stack[stack.length - 1]! + cstack[cstack.length - 1] = { + color: n.color, + key: n.key, + value: n.value, + left: n.left, + right: n.right, + count: n.count + } + for (let i = stack.length - 2; i >= 0; --i) { + n = stack[i]! + if (n.left === stack[i + 1]) { + cstack[i] = { + color: n.color, + key: n.key, + value: n.value, + left: cstack[i + 1], + right: n.right, + count: n.count + } + } else { + cstack[i] = { + color: n.color, + key: n.key, + value: n.value, + left: n.left, + right: cstack[i + 1], + count: n.count + } + } + } + // Get node + n = cstack[cstack.length - 1]! + // If not leaf, then swap with previous node + if (n.left !== undefined && n.right !== undefined) { + // First walk to previous leaf + const split = cstack.length + n = n.left + while (n.right != null) { + cstack.push(n) + n = n.right + } + // Copy path to leaf + const v = cstack[split - 1] + cstack.push({ + color: n.color, + key: v!.key, + value: v!.value, + left: n.left, + right: n.right, + count: n.count + }) + cstack[split - 1]!.key = n.key + cstack[split - 1]!.value = n.value + // Fix up stack + for (let i = cstack.length - 2; i >= split; --i) { + n = cstack[i]! + cstack[i] = { + color: n.color, + key: n.key, + value: n.value, + left: n.left, + right: cstack[i + 1], + count: n.count + } + } + cstack[split - 1]!.left = cstack[split] + } + + // Remove leaf node + n = cstack[cstack.length - 1]! + if (n.color === Node.Color.Red) { + // Easy case: removing red leaf + const p = cstack[cstack.length - 2]! + if (p.left === n) { + p.left = undefined + } else if (p.right === n) { + p.right = undefined + } + cstack.pop() + for (let i = 0; i < cstack.length; ++i) { + cstack[i]!.count-- + } + return makeImpl(ord, cstack[0]) + } else { + if (n.left !== undefined || n.right !== undefined) { + // Second easy case: Single child black parent + if (n.left !== undefined) { + Node.swap(n, n.left) + } else if (n.right !== undefined) { + Node.swap(n, n.right) + } + // Child must be red, so repaint it black to balance color + n.color = Node.Color.Black + for (let i = 0; i < cstack.length - 1; ++i) { + cstack[i]!.count-- + } + return makeImpl(ord, cstack[0]) + } else if (cstack.length === 1) { + // Third easy case: root + return makeImpl(ord, undefined) + } else { + // Hard case: Repaint n, and then do some nasty stuff + for (let i = 0; i < cstack.length; ++i) { + cstack[i]!.count-- + } + const parent = cstack[cstack.length - 2] + fixDoubleBlack(cstack) + // Fix up links + if (parent!.left === n) { + parent!.left = undefined + } else { + parent!.right = undefined + } + } + } + return makeImpl(ord, cstack[0]) +}) + +/** @internal */ +export const size = (self: RBT.RedBlackTree): number => (self as RedBlackTreeImpl)._root?.count ?? 0 + +/** @internal */ +export const valuesForward = (self: RBT.RedBlackTree): IterableIterator => + values(self, Direction.Forward) + +/** @internal */ +export const valuesBackward = (self: RBT.RedBlackTree): IterableIterator => + values(self, Direction.Backward) + +/** @internal */ +const values = ( + self: RBT.RedBlackTree, + direction: RBT.RedBlackTree.Direction +): IterableIterator => { + const begin: RedBlackTreeIterator = self[Symbol.iterator]() as RedBlackTreeIterator + let count = 0 + return { + [Symbol.iterator]: () => values(self, direction), + next: (): IteratorResult => { + count++ + const entry = begin.value + if (direction === Direction.Forward) { + begin.moveNext() + } else { + begin.movePrev() + } + switch (entry._tag) { + case "None": { + return { done: true, value: count } + } + case "Some": { + return { done: false, value: entry.value } + } + } + } + } +} + +const visitFull = ( + node: Node.Node, + visit: (key: K, value: V) => Option.Option +): Option.Option => { + let current: Node.Node | undefined = node + let stack: Stack.Stack> | undefined = undefined + let done = false + while (!done) { + if (current != null) { + stack = Stack.make(current, stack) + current = current.left + } else if (stack != null) { + const value = visit(stack.value.key, stack.value.value) + if (Option.isSome(value)) { + return value + } + current = stack.value.right + stack = stack.previous + } else { + done = true + } + } + return Option.none() +} + +const visitGreaterThanEqual = ( + node: Node.Node, + min: K, + ord: Order.Order, + visit: (key: K, value: V) => Option.Option +): Option.Option => { + let current: Node.Node | undefined = node + let stack: Stack.Stack> | undefined = undefined + let done = false + while (!done) { + if (current !== undefined) { + stack = Stack.make(current, stack) + if (ord(min, current.key) <= 0) { + current = current.left + } else { + current = undefined + } + } else if (stack !== undefined) { + if (ord(min, stack.value.key) <= 0) { + const value = visit(stack.value.key, stack.value.value) + if (Option.isSome(value)) { + return value + } + } + current = stack.value.right + stack = stack.previous + } else { + done = true + } + } + return Option.none() +} + +const visitLessThan = ( + node: Node.Node, + max: K, + ord: Order.Order, + visit: (key: K, value: V) => Option.Option +): Option.Option => { + let current: Node.Node | undefined = node + let stack: Stack.Stack> | undefined = undefined + let done = false + while (!done) { + if (current !== undefined) { + stack = Stack.make(current, stack) + current = current.left + } else if (stack !== undefined && ord(max, stack.value.key) > 0) { + const value = visit(stack.value.key, stack.value.value) + if (Option.isSome(value)) { + return value + } + current = stack.value.right + stack = stack.previous + } else { + done = true + } + } + return Option.none() +} + +const visitBetween = ( + node: Node.Node, + min: K, + max: K, + ord: Order.Order, + visit: (key: K, value: V) => Option.Option +): Option.Option => { + let current: Node.Node | undefined = node + let stack: Stack.Stack> | undefined = undefined + let done = false + while (!done) { + if (current !== undefined) { + stack = Stack.make(current, stack) + if (ord(min, current.key) <= 0) { + current = current.left + } else { + current = undefined + } + } else if (stack !== undefined && ord(max, stack.value.key) > 0) { + if (ord(min, stack.value.key) <= 0) { + const value = visit(stack.value.key, stack.value.value) + if (Option.isSome(value)) { + return value + } + } + current = stack.value.right + stack = stack.previous + } else { + done = true + } + } + return Option.none() +} + +/** + * Fix up a double black node in a Red-Black Tree. + */ +const fixDoubleBlack = (stack: Array>) => { + let n, p, s, z + for (let i = stack.length - 1; i >= 0; --i) { + n = stack[i]! + if (i === 0) { + n.color = Node.Color.Black + return + } + p = stack[i - 1]! + if (p.left === n) { + s = p.right + if (s !== undefined && s.right !== undefined && s.right.color === Node.Color.Red) { + s = p.right = Node.clone(s) + z = s.right = Node.clone(s.right!) + p.right = s.left + s.left = p + s.right = z + s.color = p.color + n.color = Node.Color.Black + p.color = Node.Color.Black + z.color = Node.Color.Black + Node.recount(p) + Node.recount(s) + if (i > 1) { + const pp = stack[i - 2]! + if (pp.left === p) { + pp.left = s + } else { + pp.right = s + } + } + stack[i - 1] = s + return + } else if (s !== undefined && s.left !== undefined && s.left.color === Node.Color.Red) { + s = p.right = Node.clone(s) + z = s.left = Node.clone(s.left!) + p.right = z.left + s.left = z.right + z.left = p + z.right = s + z.color = p.color + p.color = Node.Color.Black + s.color = Node.Color.Black + n.color = Node.Color.Black + Node.recount(p) + Node.recount(s) + Node.recount(z) + if (i > 1) { + const pp = stack[i - 2]! + if (pp.left === p) { + pp.left = z + } else { + pp.right = z + } + } + stack[i - 1] = z + return + } + if (s !== undefined && s.color === Node.Color.Black) { + if (p.color === Node.Color.Red) { + p.color = Node.Color.Black + p.right = Node.repaint(s, Node.Color.Red) + return + } else { + p.right = Node.repaint(s, Node.Color.Red) + continue + } + } else if (s !== undefined) { + s = Node.clone(s) + p.right = s.left + s.left = p + s.color = p.color + p.color = Node.Color.Red + Node.recount(p) + Node.recount(s) + if (i > 1) { + const pp = stack[i - 2]! + if (pp.left === p) { + pp.left = s + } else { + pp.right = s + } + } + stack[i - 1] = s + stack[i] = p + if (i + 1 < stack.length) { + stack[i + 1] = n + } else { + stack.push(n) + } + i = i + 2 + } + } else { + s = p.left + if (s !== undefined && s.left !== undefined && s.left.color === Node.Color.Red) { + s = p.left = Node.clone(s) + z = s.left = Node.clone(s.left!) + p.left = s.right + s.right = p + s.left = z + s.color = p.color + n.color = Node.Color.Black + p.color = Node.Color.Black + z.color = Node.Color.Black + Node.recount(p) + Node.recount(s) + if (i > 1) { + const pp = stack[i - 2]! + if (pp.right === p) { + pp.right = s + } else { + pp.left = s + } + } + stack[i - 1] = s + return + } else if (s !== undefined && s.right !== undefined && s.right.color === Node.Color.Red) { + s = p.left = Node.clone(s) + z = s.right = Node.clone(s.right!) + p.left = z.right + s.right = z.left + z.right = p + z.left = s + z.color = p.color + p.color = Node.Color.Black + s.color = Node.Color.Black + n.color = Node.Color.Black + Node.recount(p) + Node.recount(s) + Node.recount(z) + if (i > 1) { + const pp = stack[i - 2]! + if (pp.right === p) { + pp.right = z + } else { + pp.left = z + } + } + stack[i - 1] = z + return + } + if (s !== undefined && s.color === Node.Color.Black) { + if (p.color === Node.Color.Red) { + p.color = Node.Color.Black + p.left = Node.repaint(s, Node.Color.Red) + return + } else { + p.left = Node.repaint(s, Node.Color.Red) + continue + } + } else if (s !== undefined) { + s = Node.clone(s) + p.left = s.right + s.right = p + s.color = p.color + p.color = Node.Color.Red + Node.recount(p) + Node.recount(s) + if (i > 1) { + const pp = stack[i - 2]! + if (pp.right === p) { + pp.right = s + } else { + pp.left = s + } + } + stack[i - 1] = s + stack[i] = p + if (i + 1 < stack.length) { + stack[i + 1] = n + } else { + stack.push(n) + } + i = i + 2 + } + } + } +} diff --git a/repos/effect/packages/effect/src/internal/redBlackTree/iterator.ts b/repos/effect/packages/effect/src/internal/redBlackTree/iterator.ts new file mode 100644 index 0000000..ae240c7 --- /dev/null +++ b/repos/effect/packages/effect/src/internal/redBlackTree/iterator.ts @@ -0,0 +1,200 @@ +import * as Arr from "../../Array.js" +import * as Option from "../../Option.js" +import type * as RBT from "../../RedBlackTree.js" +import type { RedBlackTreeImpl } from "../redBlackTree.js" +import type * as Node from "./node.js" + +/** @internal */ +export const Direction = { + Forward: 0 as RBT.RedBlackTree.Direction, + Backward: 1 << 0 as RBT.RedBlackTree.Direction +} as const + +/** @internal */ +export class RedBlackTreeIterator implements Iterator<[K, V]> { + private count = 0 + + constructor( + readonly self: RBT.RedBlackTree, + readonly stack: Array>, + readonly direction: RBT.RedBlackTree.Direction + ) {} + + /** + * Clones the iterator + */ + clone(): RedBlackTreeIterator { + return new RedBlackTreeIterator(this.self, this.stack.slice(), this.direction) + } + + /** + * Reverse the traversal direction + */ + reversed(): RedBlackTreeIterator { + return new RedBlackTreeIterator( + this.self, + this.stack.slice(), + this.direction === Direction.Forward ? Direction.Backward : Direction.Forward + ) + } + + /** + * Iterator next + */ + next(): IteratorResult<[K, V], number> { + const entry = this.entry + this.count++ + if (this.direction === Direction.Forward) { + this.moveNext() + } else { + this.movePrev() + } + switch (entry._tag) { + case "None": { + return { done: true, value: this.count } + } + case "Some": { + return { done: false, value: entry.value } + } + } + } + + /** + * Returns the key + */ + get key(): Option.Option { + if (this.stack.length > 0) { + return Option.some(this.stack[this.stack.length - 1]!.key) + } + return Option.none() + } + + /** + * Returns the value + */ + get value(): Option.Option { + if (this.stack.length > 0) { + return Option.some(this.stack[this.stack.length - 1]!.value) + } + return Option.none() + } + + /** + * Returns the key + */ + get entry(): Option.Option<[K, V]> { + return Option.map(Arr.last(this.stack), (node) => [node.key, node.value]) + } + + /** + * Returns the position of this iterator in the sorted list + */ + get index(): number { + let idx = 0 + const stack = this.stack + if (stack.length === 0) { + const r = (this.self as RedBlackTreeImpl)._root + if (r != null) { + return r.count + } + return 0 + } else if (stack[stack.length - 1]!.left != null) { + idx = stack[stack.length - 1]!.left!.count + } + for (let s = stack.length - 2; s >= 0; --s) { + if (stack[s + 1] === stack[s]!.right) { + ++idx + if (stack[s]!.left != null) { + idx += stack[s]!.left!.count + } + } + } + return idx + } + + /** + * Advances iterator to next element in list + */ + moveNext() { + const stack = this.stack + if (stack.length === 0) { + return + } + let n: Node.Node | undefined = stack[stack.length - 1]! + if (n.right != null) { + n = n.right + while (n != null) { + stack.push(n) + n = n.left + } + } else { + stack.pop() + while (stack.length > 0 && stack[stack.length - 1]!.right === n) { + n = stack[stack.length - 1] + stack.pop() + } + } + } + + /** + * Checks if there is a next element + */ + get hasNext() { + const stack = this.stack + if (stack.length === 0) { + return false + } + if (stack[stack.length - 1]!.right != null) { + return true + } + for (let s = stack.length - 1; s > 0; --s) { + if (stack[s - 1]!.left === stack[s]) { + return true + } + } + return false + } + + /** + * Advances iterator to previous element in list + */ + movePrev() { + const stack = this.stack + if (stack.length === 0) { + return + } + let n: Node.Node | undefined = stack[stack.length - 1] + if (n != null && n.left != null) { + n = n.left + while (n != null) { + stack.push(n) + n = n.right + } + } else { + stack.pop() + while (stack.length > 0 && stack[stack.length - 1]!.left === n) { + n = stack[stack.length - 1] + stack.pop() + } + } + } + + /** + * Checks if there is a previous element + */ + get hasPrev() { + const stack = this.stack + if (stack.length === 0) { + return false + } + if (stack[stack.length - 1]!.left != null) { + return true + } + for (let s = stack.length - 1; s > 0; --s) { + if (stack[s - 1]!.right === stack[s]) { + return true + } + } + return false + } +} diff --git a/repos/effect/packages/effect/src/internal/redBlackTree/node.ts b/repos/effect/packages/effect/src/internal/redBlackTree/node.ts new file mode 100644 index 0000000..904a6cf --- /dev/null +++ b/repos/effect/packages/effect/src/internal/redBlackTree/node.ts @@ -0,0 +1,68 @@ +/** @internal */ +export const Color = { + Red: 0 as Node.Color, + Black: 1 << 0 as Node.Color +} as const + +export declare namespace Node { + export type Color = number & { + readonly Color: unique symbol + } +} + +export interface Node { + color: Node.Color + key: K + value: V + left: Node | undefined + right: Node | undefined + count: number +} + +/** @internal */ +export const clone = ({ + color, + count, + key, + left, + right, + value +}: Node) => ({ + color, + key, + value, + left, + right, + count +}) + +/** @internal */ +export function swap(n: Node, v: Node) { + n.key = v.key + n.value = v.value + n.left = v.left + n.right = v.right + n.color = v.color + n.count = v.count +} + +/** @internal */ +export const repaint = ({ + count, + key, + left, + right, + value +}: Node, color: Node.Color) => ({ + color, + key, + value, + left, + right, + count +}) + +/** @internal */ +export const recount = (node: Node) => { + node.count = 1 + (node.left?.count ?? 0) + (node.right?.count ?? 0) +} diff --git a/repos/effect/packages/effect/src/internal/redacted.ts b/repos/effect/packages/effect/src/internal/redacted.ts new file mode 100644 index 0000000..ab10cf0 --- /dev/null +++ b/repos/effect/packages/effect/src/internal/redacted.ts @@ -0,0 +1,73 @@ +import * as Equal from "../Equal.js" +import { pipe } from "../Function.js" +import { globalValue } from "../GlobalValue.js" +import * as Hash from "../Hash.js" +import { NodeInspectSymbol } from "../Inspectable.js" +import { pipeArguments } from "../Pipeable.js" +import { hasProperty } from "../Predicate.js" +import type * as Redacted from "../Redacted.js" + +/** @internal */ +const RedactedSymbolKey = "effect/Redacted" + +/** @internal */ +export const redactedRegistry = globalValue( + "effect/Redacted/redactedRegistry", + () => new WeakMap, any>() +) + +/** @internal */ +export const RedactedTypeId: Redacted.RedactedTypeId = Symbol.for( + RedactedSymbolKey +) as Redacted.RedactedTypeId + +/** @internal */ +export const proto = { + [RedactedTypeId]: { + _A: (_: never) => _ + }, + pipe() { + return pipeArguments(this, arguments) + }, + toString() { + return "" + }, + toJSON() { + return "" + }, + [NodeInspectSymbol]() { + return "" + }, + [Hash.symbol](this: Redacted.Redacted): number { + return pipe( + Hash.hash(RedactedSymbolKey), + Hash.combine(Hash.hash(redactedRegistry.get(this))), + Hash.cached(this) + ) + }, + [Equal.symbol](this: Redacted.Redacted, that: unknown): boolean { + return isRedacted(that) && Equal.equals(redactedRegistry.get(this), redactedRegistry.get(that)) + } +} + +/** @internal */ +export const isRedacted = (u: unknown): u is Redacted.Redacted => hasProperty(u, RedactedTypeId) + +/** @internal */ +export const make = (value: T): Redacted.Redacted => { + const redacted = Object.create(proto) + redactedRegistry.set(redacted, value) + return redacted +} + +/** @internal */ +export const value = (self: Redacted.Redacted): T => { + if (redactedRegistry.has(self)) { + return redactedRegistry.get(self) + } else { + throw new Error("Unable to get redacted value") + } +} + +/** @internal */ +export const unsafeWipe = (self: Redacted.Redacted): boolean => redactedRegistry.delete(self) diff --git a/repos/effect/packages/effect/src/internal/ref.ts b/repos/effect/packages/effect/src/internal/ref.ts new file mode 100644 index 0000000..cc10f6b --- /dev/null +++ b/repos/effect/packages/effect/src/internal/ref.ts @@ -0,0 +1,171 @@ +import type * as Effect from "../Effect.js" +import * as Effectable from "../Effectable.js" +import { dual } from "../Function.js" +import * as MutableRef from "../MutableRef.js" +import * as Option from "../Option.js" +import * as Readable from "../Readable.js" +import type * as Ref from "../Ref.js" +import * as core from "./core.js" + +/** @internal */ +export const RefTypeId: Ref.RefTypeId = Symbol.for("effect/Ref") as Ref.RefTypeId + +/** @internal */ +export const refVariance = { + /* c8 ignore next */ + _A: (_: any) => _ +} + +class RefImpl extends Effectable.Class implements Ref.Ref { + commit() { + return this.get + } + readonly [RefTypeId] = refVariance + readonly [Readable.TypeId]: Readable.TypeId = Readable.TypeId + constructor(readonly ref: MutableRef.MutableRef) { + super() + this.get = core.sync(() => MutableRef.get(this.ref)) + } + readonly get: Effect.Effect + modify(f: (a: A) => readonly [B, A]): Effect.Effect { + return core.sync(() => { + const current = MutableRef.get(this.ref) + const [b, a] = f(current) + if ((current as unknown) !== (a as unknown)) { + MutableRef.set(a)(this.ref) + } + return b + }) + } +} + +/** @internal */ +export const unsafeMake = (value: A): Ref.Ref => new RefImpl(MutableRef.make(value)) + +/** @internal */ +export const make = (value: A): Effect.Effect> => core.sync(() => unsafeMake(value)) + +/** @internal */ +export const get = (self: Ref.Ref) => self.get + +/** @internal */ +export const set = dual< + (value: A) => (self: Ref.Ref) => Effect.Effect, + (self: Ref.Ref, value: A) => Effect.Effect +>(2, (self: Ref.Ref, value: A) => self.modify((): [void, A] => [void 0, value])) + +/** @internal */ +export const getAndSet = dual< + (value: A) => (self: Ref.Ref) => Effect.Effect, + (self: Ref.Ref, value: A) => Effect.Effect +>(2, (self: Ref.Ref, value: A) => self.modify((a): [A, A] => [a, value])) + +/** @internal */ +export const getAndUpdate = dual< + (f: (a: A) => A) => (self: Ref.Ref) => Effect.Effect, + (self: Ref.Ref, f: (a: A) => A) => Effect.Effect +>(2, (self: Ref.Ref, f: (a: A) => A) => self.modify((a): [A, A] => [a, f(a)])) + +/** @internal */ +export const getAndUpdateSome = dual< + (pf: (a: A) => Option.Option) => (self: Ref.Ref) => Effect.Effect, + (self: Ref.Ref, pf: (a: A) => Option.Option) => Effect.Effect +>(2, (self: Ref.Ref, pf: (a: A) => Option.Option) => + self.modify((value): [A, A] => { + const option = pf(value) + switch (option._tag) { + case "None": { + return [value, value] + } + case "Some": { + return [value, option.value] + } + } + })) + +/** @internal */ +export const setAndGet = dual< + (value: A) => (self: Ref.Ref) => Effect.Effect, + (self: Ref.Ref, value: A) => Effect.Effect +>(2, (self: Ref.Ref, value: A) => self.modify((): [A, A] => [value, value])) + +/** @internal */ +export const modify = dual< + (f: (a: A) => readonly [B, A]) => (self: Ref.Ref) => Effect.Effect, + (self: Ref.Ref, f: (a: A) => readonly [B, A]) => Effect.Effect +>(2, (self, f) => self.modify(f)) + +/** @internal */ +export const modifySome = dual< + ( + fallback: B, + pf: (a: A) => Option.Option + ) => (self: Ref.Ref) => Effect.Effect, + ( + self: Ref.Ref, + fallback: B, + pf: (a: A) => Option.Option + ) => Effect.Effect +>(3, (self, fallback, pf) => + self.modify((value) => { + const option = pf(value) + switch (option._tag) { + case "None": { + return [fallback, value] + } + case "Some": { + return option.value + } + } + })) + +/** @internal */ +export const update = dual< + (f: (a: A) => A) => (self: Ref.Ref) => Effect.Effect, + (self: Ref.Ref, f: (a: A) => A) => Effect.Effect +>(2, (self: Ref.Ref, f: (a: A) => A) => self.modify((a): [void, A] => [void 0, f(a)])) + +/** @internal */ +export const updateAndGet = dual< + (f: (a: A) => A) => (self: Ref.Ref) => Effect.Effect, + (self: Ref.Ref, f: (a: A) => A) => Effect.Effect +>(2, (self: Ref.Ref, f: (a: A) => A) => + self.modify((a): [A, A] => { + const result = f(a) + return [result, result] + })) + +/** @internal */ +export const updateSome = dual< + (f: (a: A) => Option.Option) => (self: Ref.Ref) => Effect.Effect, + (self: Ref.Ref, f: (a: A) => Option.Option) => Effect.Effect +>(2, (self: Ref.Ref, f: (a: A) => Option.Option) => + self.modify( + (a): [void, A] => [ + void 0, + Option.match(f(a), { + onNone: () => a, + onSome: (b) => b + }) + ] + )) + +/** @internal */ +export const updateSomeAndGet = dual< + (pf: (a: A) => Option.Option) => (self: Ref.Ref) => Effect.Effect, + (self: Ref.Ref, pf: (a: A) => Option.Option) => Effect.Effect +>(2, (self: Ref.Ref, pf: (a: A) => Option.Option) => + self.modify((value): [A, A] => { + const option = pf(value) + switch (option._tag) { + case "None": { + return [value, value] + } + case "Some": { + return [option.value, option.value] + } + } + })) + +/** @internal */ +export const unsafeGet = (self: Ref.Ref): A => MutableRef.get((self as RefImpl).ref) diff --git a/repos/effect/packages/effect/src/internal/reloadable.ts b/repos/effect/packages/effect/src/internal/reloadable.ts new file mode 100644 index 0000000..f871ff6 --- /dev/null +++ b/repos/effect/packages/effect/src/internal/reloadable.ts @@ -0,0 +1,140 @@ +import * as Context from "../Context.js" +import type * as Effect from "../Effect.js" +import { pipe } from "../Function.js" +import type * as Layer from "../Layer.js" +import type * as Reloadable from "../Reloadable.js" +import type * as Schedule from "../Schedule.js" +import * as effect from "./core-effect.js" +import * as core from "./core.js" +import * as fiberRuntime from "./fiberRuntime.js" +import * as layer_ from "./layer.js" +import * as schedule_ from "./schedule.js" +import * as scopedRef from "./scopedRef.js" + +/** @internal */ +const ReloadableSymbolKey = "effect/Reloadable" + +/** @internal */ +export const ReloadableTypeId: Reloadable.ReloadableTypeId = Symbol.for( + ReloadableSymbolKey +) as Reloadable.ReloadableTypeId + +const reloadableVariance = { + /* c8 ignore next */ + _A: (_: any) => _ +} + +/** @internal */ +export const auto = ( + tag: Context.Tag, + options: { + readonly layer: Layer.Layer + readonly schedule: Schedule.Schedule + } +): Layer.Layer, E, R | In> => + layer_.scoped( + reloadableTag(tag), + pipe( + layer_.build(manual(tag, { layer: options.layer })), + core.map(Context.unsafeGet(reloadableTag(tag))), + core.tap((reloadable) => + fiberRuntime.acquireRelease( + pipe( + reloadable.reload, + effect.ignoreLogged, + schedule_.schedule_Effect(options.schedule), + fiberRuntime.forkDaemon + ), + core.interruptFiber + ) + ) + ) + ) + +/** @internal */ +export const autoFromConfig = ( + tag: Context.Tag, + options: { + readonly layer: Layer.Layer + readonly scheduleFromConfig: (context: Context.Context) => Schedule.Schedule + } +): Layer.Layer, E, R | In> => + layer_.scoped( + reloadableTag(tag), + pipe( + core.context(), + core.flatMap((env) => + pipe( + layer_.build(auto(tag, { + layer: options.layer, + schedule: options.scheduleFromConfig(env) + })), + core.map(Context.unsafeGet(reloadableTag(tag))) + ) + ) + ) + ) + +/** @internal */ +export const get = ( + tag: Context.Tag +): Effect.Effect> => + core.flatMap( + reloadableTag(tag), + (reloadable) => scopedRef.get(reloadable.scopedRef) + ) + +/** @internal */ +export const manual = ( + tag: Context.Tag, + options: { + readonly layer: Layer.Layer + } +): Layer.Layer, E, In> => + layer_.scoped( + reloadableTag(tag), + pipe( + core.context(), + core.flatMap((env) => + pipe( + scopedRef.fromAcquire(pipe(layer_.build(options.layer), core.map(Context.unsafeGet(tag)))), + core.map((ref) => ({ + [ReloadableTypeId]: reloadableVariance, + scopedRef: ref, + reload: pipe( + scopedRef.set(ref, pipe(layer_.build(options.layer), core.map(Context.unsafeGet(tag)))), + core.provideContext(env) + ) + })) + ) + ) + ) + ) + +/** @internal */ +export const reloadableTag = ( + tag: Context.Tag +): Context.Tag, Reloadable.Reloadable> => { + return Context.GenericTag, Reloadable.Reloadable>(`effect/Reloadable<${tag.key}>`) +} + +/** @internal */ +export const reload = ( + tag: Context.Tag +): Effect.Effect> => + core.flatMap( + reloadableTag(tag), + (reloadable) => reloadable.reload + ) + +/** @internal */ +export const reloadFork = ( + tag: Context.Tag +): Effect.Effect> => + core.flatMap(reloadableTag(tag), (reloadable) => + pipe( + reloadable.reload, + effect.ignoreLogged, + fiberRuntime.forkDaemon, + core.asVoid + )) diff --git a/repos/effect/packages/effect/src/internal/request.ts b/repos/effect/packages/effect/src/internal/request.ts new file mode 100644 index 0000000..1c5d44e --- /dev/null +++ b/repos/effect/packages/effect/src/internal/request.ts @@ -0,0 +1,177 @@ +import type * as Cause from "../Cause.js" +import type * as Effect from "../Effect.js" +import { dual } from "../Function.js" +import { hasProperty } from "../Predicate.js" +import type * as Request from "../Request.js" +import type * as Types from "../Types.js" +import * as completedRequestMap from "./completedRequestMap.js" +import * as core from "./core.js" +import { StructuralPrototype } from "./effectable.js" + +/** @internal */ +const RequestSymbolKey = "effect/Request" + +/** @internal */ +export const RequestTypeId: Request.RequestTypeId = Symbol.for( + RequestSymbolKey +) as Request.RequestTypeId + +const requestVariance = { + /* c8 ignore next */ + _E: (_: never) => _, + /* c8 ignore next */ + _A: (_: never) => _ +} + +const RequestPrototype = { + ...StructuralPrototype, + [RequestTypeId]: requestVariance +} + +/** @internal */ +export const isRequest = (u: unknown): u is Request.Request => hasProperty(u, RequestTypeId) + +/** @internal */ +export const of = >(): Request.Request.Constructor => (args) => + Object.assign(Object.create(RequestPrototype), args) + +/** @internal */ +export const tagged = & { _tag: string }>( + tag: R["_tag"] +): Request.Request.Constructor => +(args) => { + const request = Object.assign(Object.create(RequestPrototype), args) + request._tag = tag + return request +} + +/** @internal */ +export const Class: new>( + args: Types.Equals>, {}> extends true ? void + : { readonly [P in keyof A as P extends keyof Request.Request ? never : P]: A[P] } +) => Request.Request & Readonly = (function() { + function Class(this: any, args: any) { + if (args) { + Object.assign(this, args) + } + } + Class.prototype = RequestPrototype + return Class as any +})() + +/** @internal */ +export const TaggedClass = ( + tag: Tag +): new>( + args: Types.Equals>, {}> extends true ? void + : { readonly [P in keyof A as P extends "_tag" | keyof Request.Request ? never : P]: A[P] } +) => Request.Request & Readonly & { readonly _tag: Tag } => { + return class TaggedClass extends Class { + readonly _tag = tag + } as any +} + +/** @internal */ +export const complete = dual< + >( + result: Request.Request.Result + ) => (self: A) => Effect.Effect, + >( + self: A, + result: Request.Request.Result + ) => Effect.Effect +>(2, (self, result) => + core.fiberRefGetWith( + completedRequestMap.currentRequestMap, + (map) => + core.sync(() => { + if (map.has(self)) { + const entry = map.get(self)! + if (!entry.state.completed) { + entry.state.completed = true + core.deferredUnsafeDone(entry.result, result) + } + } + }) + )) + +/** @internal */ +export const completeEffect = dual< + , R>( + effect: Effect.Effect, Request.Request.Error, R> + ) => (self: A) => Effect.Effect, + , R>( + self: A, + effect: Effect.Effect, Request.Request.Error, R> + ) => Effect.Effect +>(2, (self, effect) => + core.matchEffect(effect, { + onFailure: (error) => complete(self, core.exitFail(error) as any), + onSuccess: (value) => complete(self, core.exitSucceed(value) as any) + })) + +/** @internal */ +export const fail = dual< + >( + error: Request.Request.Error + ) => (self: A) => Effect.Effect, + >( + self: A, + error: Request.Request.Error + ) => Effect.Effect +>(2, (self, error) => complete(self, core.exitFail(error) as any)) + +/** @internal */ +export const failCause = dual< + >( + cause: Cause.Cause> + ) => (self: A) => Effect.Effect, + >( + self: A, + cause: Cause.Cause> + ) => Effect.Effect +>(2, (self, cause) => complete(self, core.exitFailCause(cause) as any)) + +/** @internal */ +export const succeed = dual< + >( + value: Request.Request.Success + ) => (self: A) => Effect.Effect, + >( + self: A, + value: Request.Request.Success + ) => Effect.Effect +>(2, (self, value) => complete(self, core.exitSucceed(value) as any)) + +/** @internal */ +export class Listeners { + count = 0 + observers: Set<(count: number) => void> = new Set() + interrupted = false + addObserver(f: (count: number) => void): void { + this.observers.add(f) + } + removeObserver(f: (count: number) => void): void { + this.observers.delete(f) + } + increment() { + this.count++ + this.observers.forEach((f) => f(this.count)) + } + decrement() { + this.count-- + this.observers.forEach((f) => f(this.count)) + } +} + +/** + * @internal + */ +export const filterOutCompleted = >(requests: Array) => + core.fiberRefGetWith( + completedRequestMap.currentRequestMap, + (map) => + core.succeed( + requests.filter((request) => !(map.get(request)?.state.completed === true)) + ) + ) diff --git a/repos/effect/packages/effect/src/internal/resource.ts b/repos/effect/packages/effect/src/internal/resource.ts new file mode 100644 index 0000000..bb57c5c --- /dev/null +++ b/repos/effect/packages/effect/src/internal/resource.ts @@ -0,0 +1,76 @@ +import type * as Effect from "../Effect.js" +import { identity, pipe } from "../Function.js" +import type * as Resource from "../Resource.js" +import type * as Schedule from "../Schedule.js" +import type * as Scope from "../Scope.js" +import * as core from "./core.js" +import * as effectable from "./effectable.js" +import * as fiberRuntime from "./fiberRuntime.js" +import * as schedule_ from "./schedule.js" +import * as scopedRef from "./scopedRef.js" + +/** @internal */ +const ResourceSymbolKey = "effect/Resource" + +/** @internal */ +export const ResourceTypeId: Resource.ResourceTypeId = Symbol.for( + ResourceSymbolKey +) as Resource.ResourceTypeId + +const resourceVariance = { + /* c8 ignore next */ + _E: (_: any) => _, + /* c8 ignore next */ + _A: (_: any) => _ +} + +/** @internal */ +const proto: ThisType> = { + ...effectable.CommitPrototype, + commit() { + return get(this) + }, + [ResourceTypeId]: resourceVariance +} + +/** @internal */ +export const auto = ( + acquire: Effect.Effect, + policy: Schedule.Schedule +): Effect.Effect, never, R | R2 | Scope.Scope> => + core.tap(manual(acquire), (manual) => + fiberRuntime.acquireRelease( + pipe( + refresh(manual), + schedule_.schedule_Effect(policy), + core.interruptible, + fiberRuntime.forkDaemon + ), + core.interruptFiber + )) + +/** @internal */ +export const manual = ( + acquire: Effect.Effect +): Effect.Effect, never, R | Scope.Scope> => + core.flatMap(core.context(), (env) => + pipe( + scopedRef.fromAcquire(core.exit(acquire)), + core.map((ref) => { + const resource = Object.create(proto) + resource.scopedRef = ref + resource.acquire = core.provideContext(acquire, env) + return resource + }) + )) + +/** @internal */ +export const get = (self: Resource.Resource): Effect.Effect => + core.flatMap(scopedRef.get(self.scopedRef), identity) + +/** @internal */ +export const refresh = (self: Resource.Resource): Effect.Effect => + scopedRef.set( + self.scopedRef, + core.map(self.acquire, core.exitSucceed) + ) diff --git a/repos/effect/packages/effect/src/internal/ringBuffer.ts b/repos/effect/packages/effect/src/internal/ringBuffer.ts new file mode 100644 index 0000000..93a1274 --- /dev/null +++ b/repos/effect/packages/effect/src/internal/ringBuffer.ts @@ -0,0 +1,68 @@ +import * as Chunk from "../Chunk.js" +import { constUndefined } from "../Function.js" +import * as Option from "../Option.js" + +/** @internal */ +export class RingBuffer { + private array: Array + private size = 0 + private current = 0 + + constructor(public readonly capacity: number) { + this.array = Array.from({ length: capacity }, constUndefined) + } + + head(): Option.Option { + return Option.fromNullable(this.array[this.current]) + } + + lastOrNull(): A | undefined { + if (this.size === 0) { + return undefined + } + + const index = this.current === 0 ? this.array.length - 1 : this.current - 1 + + return this.array[index] ?? undefined + } + + put(value: A): void { + this.array[this.current] = value + this.increment() + } + + dropLast(): void { + if (this.size > 0) { + this.decrement() + this.array[this.current] = undefined + } + } + + toChunk(): Chunk.Chunk { + const begin = this.current - this.size + const newArray = begin < 0 + ? [ + ...this.array.slice(this.capacity + begin, this.capacity), + ...this.array.slice(0, this.current) + ] + : this.array.slice(begin, this.current) + + return Chunk.fromIterable(newArray) as Chunk.Chunk + } + + private increment(): void { + if (this.size < this.capacity) { + this.size += 1 + } + this.current = (this.current + 1) % this.capacity + } + + private decrement(): void { + this.size -= 1 + if (this.current > 0) { + this.current -= 1 + } else { + this.current = this.capacity - 1 + } + } +} diff --git a/repos/effect/packages/effect/src/internal/runtime.ts b/repos/effect/packages/effect/src/internal/runtime.ts new file mode 100644 index 0000000..dd4b69b --- /dev/null +++ b/repos/effect/packages/effect/src/internal/runtime.ts @@ -0,0 +1,558 @@ +import type * as ReadonlyArray from "../Array.js" +import type * as Cause from "../Cause.js" +import * as Context from "../Context.js" +import type * as Effect from "../Effect.js" +import { equals } from "../Equal.js" +import * as Exit from "../Exit.js" +import * as Fiber from "../Fiber.js" +import * as FiberId from "../FiberId.js" +import type * as FiberRef from "../FiberRef.js" +import * as FiberRefs from "../FiberRefs.js" +import { dual, pipe } from "../Function.js" +import * as Inspectable from "../Inspectable.js" +import * as Option from "../Option.js" +import { pipeArguments } from "../Pipeable.js" +import * as Predicate from "../Predicate.js" +import type * as Runtime from "../Runtime.js" +import type * as RuntimeFlags from "../RuntimeFlags.js" +import * as scheduler_ from "../Scheduler.js" +import * as scope_ from "../Scope.js" +import * as InternalCause from "./cause.js" +import * as core from "./core.js" +import * as executionStrategy from "./executionStrategy.js" +import * as FiberRuntime from "./fiberRuntime.js" +import * as fiberScope from "./fiberScope.js" +import * as OpCodes from "./opCodes/effect.js" +import * as runtimeFlags from "./runtimeFlags.js" +import * as supervisor_ from "./supervisor.js" + +const makeDual = , Return>( + f: (runtime: Runtime.Runtime, effect: Effect.Effect, ...args: Args) => Return +): { + (runtime: Runtime.Runtime): (effect: Effect.Effect, ...args: Args) => Return + (runtime: Runtime.Runtime, effect: Effect.Effect, ...args: Args): Return +} => + function(this: any) { + if (arguments.length === 1) { + const runtime = arguments[0] + return (effect: any, ...args: Args) => f(runtime, effect, ...args) + } + return f.apply(this, arguments as any) + } as any + +/** @internal */ +export const unsafeFork: { + (runtime: Runtime.Runtime): ( + effect: Effect.Effect, + options?: Runtime.RunForkOptions | undefined + ) => Fiber.RuntimeFiber + ( + runtime: Runtime.Runtime, + effect: Effect.Effect, + options?: Runtime.RunForkOptions | undefined + ): Fiber.RuntimeFiber +} = makeDual(( + runtime: Runtime.Runtime, + self: Effect.Effect, + options?: Runtime.RunForkOptions +): Fiber.RuntimeFiber => { + const fiberId = FiberId.unsafeMake() + const fiberRefUpdates: ReadonlyArray.NonEmptyArray< + readonly [FiberRef.FiberRef, ReadonlyArray.NonEmptyReadonlyArray] + > = [[core.currentContext, [[fiberId, runtime.context]]]] + + if (options?.scheduler) { + fiberRefUpdates.push([scheduler_.currentScheduler, [[fiberId, options.scheduler]]]) + } + + let fiberRefs = FiberRefs.updateManyAs(runtime.fiberRefs, { + entries: fiberRefUpdates, + forkAs: fiberId + }) + + if (options?.updateRefs) { + fiberRefs = options.updateRefs(fiberRefs, fiberId) + } + + const fiberRuntime: FiberRuntime.FiberRuntime = new FiberRuntime.FiberRuntime( + fiberId, + fiberRefs, + runtime.runtimeFlags + ) + + let effect: Effect.Effect = self + + if (options?.scope) { + effect = core.flatMap( + scope_.fork(options.scope, executionStrategy.sequential), + (closeableScope) => + core.zipRight( + core.scopeAddFinalizer( + closeableScope, + core.fiberIdWith((id) => + equals(id, fiberRuntime.id()) ? core.void : core.interruptAsFiber(fiberRuntime, id) + ) + ), + core.onExit(self, (exit) => scope_.close(closeableScope, exit)) + ) + ) + } + + const supervisor = fiberRuntime.currentSupervisor + + // we can compare by reference here as _supervisor.none is wrapped with globalValue + if (supervisor !== supervisor_.none) { + supervisor.onStart(runtime.context, effect, Option.none(), fiberRuntime) + + fiberRuntime.addObserver((exit) => supervisor.onEnd(exit, fiberRuntime)) + } + + fiberScope.globalScope.add(runtime.runtimeFlags, fiberRuntime) + + // Only an explicit false will prevent immediate execution + if (options?.immediate === false) { + fiberRuntime.resume(effect) + } else { + fiberRuntime.start(effect) + } + + return fiberRuntime +}) + +/** @internal */ +export const unsafeRunCallback: { + (runtime: Runtime.Runtime): ( + effect: Effect.Effect, + options?: Runtime.RunCallbackOptions | undefined + ) => (fiberId?: FiberId.FiberId, options?: Runtime.RunCallbackOptions | undefined) => void + ( + runtime: Runtime.Runtime, + effect: Effect.Effect, + options?: Runtime.RunCallbackOptions | undefined + ): (fiberId?: FiberId.FiberId, options?: Runtime.RunCallbackOptions | undefined) => void +} = makeDual(( + runtime, + effect, + options: Runtime.RunCallbackOptions = {} +): (fiberId?: FiberId.FiberId, options?: Runtime.RunCallbackOptions | undefined) => void => { + const fiberRuntime = unsafeFork(runtime, effect, options) + + if (options.onExit) { + fiberRuntime.addObserver((exit) => { + options.onExit!(exit) + }) + } + + return (id, cancelOptions) => + unsafeRunCallback(runtime)( + pipe(fiberRuntime, Fiber.interruptAs(id ?? FiberId.none)), + { + ...cancelOptions, + onExit: cancelOptions?.onExit + ? (exit) => cancelOptions.onExit!(Exit.flatten(exit)) + : undefined + } + ) +}) + +/** @internal */ +export const unsafeRunSync: { + (runtime: Runtime.Runtime, effect: Effect.Effect): A + (runtime: Runtime.Runtime): (effect: Effect.Effect) => A +} = makeDual((runtime, effect) => { + const result = unsafeRunSyncExit(runtime)(effect) + if (result._tag === "Failure") { + throw fiberFailure(result.effect_instruction_i0) + } + return result.effect_instruction_i0 +}) + +class AsyncFiberExceptionImpl extends Error implements Runtime.AsyncFiberException { + readonly _tag = "AsyncFiberException" + constructor(readonly fiber: Fiber.RuntimeFiber) { + super( + `Fiber #${fiber.id().id} cannot be resolved synchronously. This is caused by using runSync on an effect that performs async work` + ) + this.name = this._tag + this.stack = this.message + } +} + +const asyncFiberException = (fiber: Fiber.RuntimeFiber): Runtime.AsyncFiberException => { + const limit = Error.stackTraceLimit + Error.stackTraceLimit = 0 + const error = new AsyncFiberExceptionImpl(fiber) + Error.stackTraceLimit = limit + return error +} + +/** @internal */ +export const isAsyncFiberException = (u: unknown): u is Runtime.AsyncFiberException => + Predicate.isTagged(u, "AsyncFiberException") && "fiber" in u + +/** @internal */ +export const FiberFailureId: Runtime.FiberFailureId = Symbol.for("effect/Runtime/FiberFailure") as any +/** @internal */ +export const FiberFailureCauseId: Runtime.FiberFailureCauseId = Symbol.for( + "effect/Runtime/FiberFailure/Cause" +) as any + +class FiberFailureImpl extends Error implements Runtime.FiberFailure { + readonly [FiberFailureId]: Runtime.FiberFailureId + readonly [FiberFailureCauseId]: Cause.Cause + constructor(cause: Cause.Cause) { + const head = InternalCause.prettyErrors(cause)[0] + + super(head?.message || "An error has occurred") + this[FiberFailureId] = FiberFailureId + this[FiberFailureCauseId] = cause + + this.name = head ? `(FiberFailure) ${head.name}` : "FiberFailure" + if (head?.stack) { + this.stack = head.stack + } + } + + toJSON(): unknown { + return { + _id: "FiberFailure", + cause: this[FiberFailureCauseId].toJSON() + } + } + + toString(): string { + return "(FiberFailure) " + InternalCause.pretty(this[FiberFailureCauseId], { renderErrorCause: true }) + } + [Inspectable.NodeInspectSymbol](): unknown { + return this.toString() + } +} + +/** @internal */ +export const fiberFailure = (cause: Cause.Cause): Runtime.FiberFailure => { + const limit = Error.stackTraceLimit + Error.stackTraceLimit = 0 + const error = new FiberFailureImpl(cause) + Error.stackTraceLimit = limit + return error +} + +/** @internal */ +export const isFiberFailure = (u: unknown): u is Runtime.FiberFailure => Predicate.hasProperty(u, FiberFailureId) + +const fastPath = (effect: Effect.Effect): Exit.Exit | undefined => { + const op = effect as core.Primitive + switch (op._op) { + case "Failure": + case "Success": { + // @ts-expect-error + return op + } + case "Left": { + return core.exitFail(op.left) + } + case "Right": { + return core.exitSucceed(op.right) + } + case "Some": { + return core.exitSucceed(op.value) + } + case "None": { + // @ts-expect-error + return core.exitFail(new core.NoSuchElementException()) + } + } +} + +/** @internal */ +export const unsafeRunSyncExit: { + (runtime: Runtime.Runtime, effect: Effect.Effect): Exit.Exit + (runtime: Runtime.Runtime): (effect: Effect.Effect) => Exit.Exit +} = makeDual((runtime, effect) => { + const op = fastPath(effect) + if (op) { + return op + } + const scheduler = new scheduler_.SyncScheduler() + const fiberRuntime = unsafeFork(runtime)(effect, { scheduler }) + scheduler.flush() + const result = fiberRuntime.unsafePoll() + if (result) { + return result + } + return core.exitDie(core.capture(asyncFiberException(fiberRuntime), core.currentSpanFromFiber(fiberRuntime))) +}) + +/** @internal */ +export const unsafeRunPromise: { + (runtime: Runtime.Runtime): ( + effect: Effect.Effect, + options?: { + readonly signal?: AbortSignal | undefined + } | undefined + ) => Promise + ( + runtime: Runtime.Runtime, + effect: Effect.Effect, + options?: { + readonly signal?: AbortSignal | undefined + } | undefined + ): Promise +} = makeDual(( + runtime, + effect, + options?: { + readonly signal?: AbortSignal | undefined + } | undefined +) => + unsafeRunPromiseExit(runtime, effect, options).then((result) => { + switch (result._tag) { + case OpCodes.OP_SUCCESS: { + return result.effect_instruction_i0 + } + case OpCodes.OP_FAILURE: { + throw fiberFailure(result.effect_instruction_i0) + } + } + }) +) + +/** @internal */ +export const unsafeRunPromiseExit: { + ( + runtime: Runtime.Runtime + ): ( + effect: Effect.Effect, + options?: { readonly signal?: AbortSignal | undefined } | undefined + ) => Promise> + ( + runtime: Runtime.Runtime, + effect: Effect.Effect, + options?: { readonly signal?: AbortSignal | undefined } | undefined + ): Promise> +} = makeDual(( + runtime, + effect, + options?: { + readonly signal?: AbortSignal | undefined + } | undefined +) => + new Promise>((resolve) => { + const op = fastPath(effect) + if (op) { + resolve(op) + } + const fiber = unsafeFork(runtime)(effect) + fiber.addObserver((exit) => { + resolve(exit) + }) + if (options?.signal !== undefined) { + if (options.signal.aborted) { + fiber.unsafeInterruptAsFork(fiber.id()) + } else { + options.signal.addEventListener("abort", () => { + fiber.unsafeInterruptAsFork(fiber.id()) + }, { once: true }) + } + } + }) +) + +/** @internal */ +export class RuntimeImpl implements Runtime.Runtime { + constructor( + readonly context: Context.Context, + readonly runtimeFlags: RuntimeFlags.RuntimeFlags, + readonly fiberRefs: FiberRefs.FiberRefs + ) {} + + pipe() { + return pipeArguments(this, arguments) + } +} + +/** @internal */ +export const make = ( + options: { + readonly context: Context.Context + readonly runtimeFlags: RuntimeFlags.RuntimeFlags + readonly fiberRefs: FiberRefs.FiberRefs + } +): Runtime.Runtime => new RuntimeImpl(options.context, options.runtimeFlags, options.fiberRefs) + +/** @internal */ +export const runtime = (): Effect.Effect, never, R> => + core.withFiberRuntime((state, status) => + core.succeed( + new RuntimeImpl( + state.getFiberRef(core.currentContext as unknown as FiberRef.FiberRef>), + status.runtimeFlags, + state.getFiberRefs() + ) + ) + ) + +/** @internal */ +export const defaultRuntimeFlags: RuntimeFlags.RuntimeFlags = runtimeFlags.make( + runtimeFlags.Interruption, + runtimeFlags.CooperativeYielding, + runtimeFlags.RuntimeMetrics +) + +/** @internal */ +export const defaultRuntime = make({ + context: Context.empty(), + runtimeFlags: defaultRuntimeFlags, + fiberRefs: FiberRefs.empty() +}) + +/** @internal */ +export const updateRuntimeFlags: { + ( + f: (flags: RuntimeFlags.RuntimeFlags) => RuntimeFlags.RuntimeFlags + ): (self: Runtime.Runtime) => Runtime.Runtime + (self: Runtime.Runtime, f: (flags: RuntimeFlags.RuntimeFlags) => RuntimeFlags.RuntimeFlags): Runtime.Runtime +} = dual( + 2, + (self: Runtime.Runtime, f: (flags: RuntimeFlags.RuntimeFlags) => RuntimeFlags.RuntimeFlags) => + make({ + context: self.context, + runtimeFlags: f(self.runtimeFlags), + fiberRefs: self.fiberRefs + }) +) + +/** @internal */ +export const disableRuntimeFlag: { + (flag: RuntimeFlags.RuntimeFlag): (self: Runtime.Runtime) => Runtime.Runtime + (self: Runtime.Runtime, flag: RuntimeFlags.RuntimeFlag): Runtime.Runtime +} = dual( + 2, + (self: Runtime.Runtime, flag: RuntimeFlags.RuntimeFlag) => updateRuntimeFlags(self, runtimeFlags.disable(flag)) +) + +/** @internal */ +export const enableRuntimeFlag: { + (flag: RuntimeFlags.RuntimeFlag): (self: Runtime.Runtime) => Runtime.Runtime + (self: Runtime.Runtime, flag: RuntimeFlags.RuntimeFlag): Runtime.Runtime +} = dual( + 2, + (self: Runtime.Runtime, flag: RuntimeFlags.RuntimeFlag) => updateRuntimeFlags(self, runtimeFlags.enable(flag)) +) + +/** @internal */ +export const updateContext: { + (f: (context: Context.Context) => Context.Context): (self: Runtime.Runtime) => Runtime.Runtime + (self: Runtime.Runtime, f: (context: Context.Context) => Context.Context): Runtime.Runtime +} = dual( + 2, + (self: Runtime.Runtime, f: (context: Context.Context) => Context.Context) => + make({ + context: f(self.context), + runtimeFlags: self.runtimeFlags, + fiberRefs: self.fiberRefs + }) +) + +/** @internal */ +export const provideService: { + (tag: Context.Tag, service: S): (self: Runtime.Runtime) => Runtime.Runtime + (self: Runtime.Runtime, tag: Context.Tag, service: S): Runtime.Runtime +} = dual( + 3, + (self: Runtime.Runtime, tag: Context.Tag, service: S) => + updateContext(self, Context.add(tag, service)) +) + +/** @internal */ +export const updateFiberRefs: { + (f: (fiberRefs: FiberRefs.FiberRefs) => FiberRefs.FiberRefs): (self: Runtime.Runtime) => Runtime.Runtime + (self: Runtime.Runtime, f: (fiberRefs: FiberRefs.FiberRefs) => FiberRefs.FiberRefs): Runtime.Runtime +} = dual( + 2, + (self: Runtime.Runtime, f: (fiberRefs: FiberRefs.FiberRefs) => FiberRefs.FiberRefs): Runtime.Runtime => + make({ + context: self.context, + runtimeFlags: self.runtimeFlags, + fiberRefs: f(self.fiberRefs) + }) +) + +/** @internal */ +export const setFiberRef: { + (fiberRef: FiberRef.FiberRef, value: A): (self: Runtime.Runtime) => Runtime.Runtime + (self: Runtime.Runtime, fiberRef: FiberRef.FiberRef, value: A): Runtime.Runtime +} = dual( + 3, + (self: Runtime.Runtime, fiberRef: FiberRef.FiberRef, value: A): Runtime.Runtime => + updateFiberRefs( + self, + FiberRefs.updateAs({ + fiberId: FiberId.none, + fiberRef, + value + }) + ) +) + +/** @internal */ +export const deleteFiberRef: { + (fiberRef: FiberRef.FiberRef): (self: Runtime.Runtime) => Runtime.Runtime + (self: Runtime.Runtime, fiberRef: FiberRef.FiberRef): Runtime.Runtime +} = dual( + 2, + (self: Runtime.Runtime, fiberRef: FiberRef.FiberRef): Runtime.Runtime => + updateFiberRefs(self, FiberRefs.delete(fiberRef)) +) + +/** @internal */ +export const unsafeRunEffect = unsafeRunCallback(defaultRuntime) + +/** @internal */ +export const unsafeForkEffect = unsafeFork(defaultRuntime) + +/** @internal */ +export const unsafeRunPromiseEffect = unsafeRunPromise(defaultRuntime) + +/** @internal */ +export const unsafeRunPromiseExitEffect = unsafeRunPromiseExit(defaultRuntime) + +/** @internal */ +export const unsafeRunSyncEffect = unsafeRunSync(defaultRuntime) + +/** @internal */ +export const unsafeRunSyncExitEffect = unsafeRunSyncExit(defaultRuntime) + +// circular with Effect + +/** @internal */ +export const asyncEffect = ( + register: ( + callback: (_: Effect.Effect) => void + ) => Effect.Effect | void, E2, R2> +): Effect.Effect => + core.suspend(() => { + let cleanup: Effect.Effect | void = undefined + return core.flatMap( + core.deferredMake(), + (deferred) => + core.flatMap(runtime(), (runtime) => + core.uninterruptibleMask((restore) => + core.zipRight( + FiberRuntime.fork(restore( + core.matchCauseEffect( + register((cb) => unsafeRunCallback(runtime)(core.intoDeferred(cb, deferred))), + { + onFailure: (cause) => core.deferredFailCause(deferred, cause), + onSuccess: (cleanup_) => { + cleanup = cleanup_ + return core.void + } + } + ) + )), + restore(core.onInterrupt(core.deferredAwait(deferred), () => cleanup ?? core.void)) + ) + )) + ) + }) diff --git a/repos/effect/packages/effect/src/internal/runtimeFlags.ts b/repos/effect/packages/effect/src/internal/runtimeFlags.ts new file mode 100644 index 0000000..c76ee36 --- /dev/null +++ b/repos/effect/packages/effect/src/internal/runtimeFlags.ts @@ -0,0 +1,178 @@ +import type * as Differ from "../Differ.js" +import { dual } from "../Function.js" +import type * as RuntimeFlags from "../RuntimeFlags.js" +import type * as RuntimeFlagsPatch from "../RuntimeFlagsPatch.js" +import * as internalDiffer from "./differ.js" +import * as runtimeFlagsPatch from "./runtimeFlagsPatch.js" + +/** @internal */ +export const None: RuntimeFlags.RuntimeFlag = 0 as RuntimeFlags.RuntimeFlag + +/** @internal */ +export const Interruption: RuntimeFlags.RuntimeFlag = 1 << 0 as RuntimeFlags.RuntimeFlag + +/** @internal */ +export const OpSupervision: RuntimeFlags.RuntimeFlag = 1 << 1 as RuntimeFlags.RuntimeFlag + +/** @internal */ +export const RuntimeMetrics: RuntimeFlags.RuntimeFlag = 1 << 2 as RuntimeFlags.RuntimeFlag + +/** @internal */ +export const WindDown: RuntimeFlags.RuntimeFlag = 1 << 4 as RuntimeFlags.RuntimeFlag + +/** @internal */ +export const CooperativeYielding: RuntimeFlags.RuntimeFlag = 1 << 5 as RuntimeFlags.RuntimeFlag + +/** @internal */ +export const allFlags: ReadonlyArray = [ + None, + Interruption, + OpSupervision, + RuntimeMetrics, + WindDown, + CooperativeYielding +] + +const print = (flag: RuntimeFlags.RuntimeFlag) => { + switch (flag) { + case CooperativeYielding: { + return "CooperativeYielding" + } + case WindDown: { + return "WindDown" + } + case RuntimeMetrics: { + return "RuntimeMetrics" + } + case OpSupervision: { + return "OpSupervision" + } + case Interruption: { + return "Interruption" + } + case None: { + return "None" + } + } +} + +/** @internal */ +export const cooperativeYielding = (self: RuntimeFlags.RuntimeFlags): boolean => isEnabled(self, CooperativeYielding) + +/** @internal */ +export const disable = dual< + (flag: RuntimeFlags.RuntimeFlag) => (self: RuntimeFlags.RuntimeFlags) => RuntimeFlags.RuntimeFlags, + (self: RuntimeFlags.RuntimeFlags, flag: RuntimeFlags.RuntimeFlag) => RuntimeFlags.RuntimeFlags +>(2, (self, flag) => (self & ~flag) as RuntimeFlags.RuntimeFlags) + +/** @internal */ +export const disableAll = dual< + (flags: RuntimeFlags.RuntimeFlags) => (self: RuntimeFlags.RuntimeFlags) => RuntimeFlags.RuntimeFlags, + (self: RuntimeFlags.RuntimeFlags, flags: RuntimeFlags.RuntimeFlags) => RuntimeFlags.RuntimeFlags +>(2, (self, flags) => (self & ~flags) as RuntimeFlags.RuntimeFlags) + +/** @internal */ +export const enable = dual< + (flag: RuntimeFlags.RuntimeFlag) => (self: RuntimeFlags.RuntimeFlags) => RuntimeFlags.RuntimeFlags, + (self: RuntimeFlags.RuntimeFlags, flag: RuntimeFlags.RuntimeFlag) => RuntimeFlags.RuntimeFlags +>(2, (self, flag) => (self | flag) as RuntimeFlags.RuntimeFlags) + +/** @internal */ +export const enableAll = dual< + (flags: RuntimeFlags.RuntimeFlags) => (self: RuntimeFlags.RuntimeFlags) => RuntimeFlags.RuntimeFlags, + (self: RuntimeFlags.RuntimeFlags, flags: RuntimeFlags.RuntimeFlags) => RuntimeFlags.RuntimeFlags +>(2, (self, flags) => (self | flags) as RuntimeFlags.RuntimeFlags) + +/** @internal */ +export const interruptible = (self: RuntimeFlags.RuntimeFlags): boolean => interruption(self) && !windDown(self) + +/** @internal */ +export const interruption = (self: RuntimeFlags.RuntimeFlags): boolean => isEnabled(self, Interruption) + +/** @internal */ +export const isDisabled = dual< + (flag: RuntimeFlags.RuntimeFlag) => (self: RuntimeFlags.RuntimeFlags) => boolean, + (self: RuntimeFlags.RuntimeFlags, flag: RuntimeFlags.RuntimeFlag) => boolean +>(2, (self, flag) => !isEnabled(self, flag)) + +/** @internal */ +export const isEnabled = dual< + (flag: RuntimeFlags.RuntimeFlag) => (self: RuntimeFlags.RuntimeFlags) => boolean, + (self: RuntimeFlags.RuntimeFlags, flag: RuntimeFlags.RuntimeFlag) => boolean +>(2, (self, flag) => (self & flag) !== 0) + +/** @internal */ +export const make = (...flags: ReadonlyArray): RuntimeFlags.RuntimeFlags => + flags.reduce((a, b) => a | b, 0) as RuntimeFlags.RuntimeFlags + +/** @internal */ +export const none: RuntimeFlags.RuntimeFlags = make(None) + +/** @internal */ +export const opSupervision = (self: RuntimeFlags.RuntimeFlags): boolean => isEnabled(self, OpSupervision) + +/** @internal */ +export const render = (self: RuntimeFlags.RuntimeFlags): string => { + const active: Array = [] + allFlags.forEach((flag) => { + if (isEnabled(self, flag)) { + active.push(`${print(flag)}`) + } + }) + return `RuntimeFlags(${active.join(", ")})` +} + +/** @internal */ +export const runtimeMetrics = (self: RuntimeFlags.RuntimeFlags): boolean => isEnabled(self, RuntimeMetrics) + +/** @internal */ +export const toSet = (self: RuntimeFlags.RuntimeFlags): ReadonlySet => + new Set(allFlags.filter((flag) => isEnabled(self, flag))) + +export const windDown = (self: RuntimeFlags.RuntimeFlags): boolean => isEnabled(self, WindDown) + +// circular with RuntimeFlagsPatch + +/** @internal */ +export const enabledSet = (self: RuntimeFlagsPatch.RuntimeFlagsPatch): ReadonlySet => + toSet((runtimeFlagsPatch.active(self) & runtimeFlagsPatch.enabled(self)) as RuntimeFlags.RuntimeFlags) + +/** @internal */ +export const disabledSet = (self: RuntimeFlagsPatch.RuntimeFlagsPatch): ReadonlySet => + toSet((runtimeFlagsPatch.active(self) & ~runtimeFlagsPatch.enabled(self)) as RuntimeFlags.RuntimeFlags) + +/** @internal */ +export const diff = dual< + (that: RuntimeFlags.RuntimeFlags) => (self: RuntimeFlags.RuntimeFlags) => RuntimeFlagsPatch.RuntimeFlagsPatch, + (self: RuntimeFlags.RuntimeFlags, that: RuntimeFlags.RuntimeFlags) => RuntimeFlagsPatch.RuntimeFlagsPatch +>(2, (self, that) => runtimeFlagsPatch.make(self ^ that, that)) + +/** @internal */ +export const patch = dual< + (patch: RuntimeFlagsPatch.RuntimeFlagsPatch) => (self: RuntimeFlags.RuntimeFlags) => RuntimeFlags.RuntimeFlags, + (self: RuntimeFlags.RuntimeFlags, patch: RuntimeFlagsPatch.RuntimeFlagsPatch) => RuntimeFlags.RuntimeFlags +>(2, (self, patch) => + ( + (self & (runtimeFlagsPatch.invert(runtimeFlagsPatch.active(patch)) | runtimeFlagsPatch.enabled(patch))) | + (runtimeFlagsPatch.active(patch) & runtimeFlagsPatch.enabled(patch)) + ) as RuntimeFlags.RuntimeFlags) + +/** @internal */ +export const renderPatch = (self: RuntimeFlagsPatch.RuntimeFlagsPatch): string => { + const enabled = Array.from(enabledSet(self)) + .map((flag) => print(flag)) + .join(", ") + const disabled = Array.from(disabledSet(self)) + .map((flag) => print(flag)) + .join(", ") + return `RuntimeFlagsPatch(enabled = (${enabled}), disabled = (${disabled}))` +} + +/** @internal */ +export const differ: Differ.Differ = internalDiffer + .make({ + empty: runtimeFlagsPatch.empty, + diff: (oldValue, newValue) => diff(oldValue, newValue), + combine: (first, second) => runtimeFlagsPatch.andThen(second)(first), + patch: (_patch, oldValue) => patch(oldValue, _patch) + }) diff --git a/repos/effect/packages/effect/src/internal/runtimeFlagsPatch.ts b/repos/effect/packages/effect/src/internal/runtimeFlagsPatch.ts new file mode 100644 index 0000000..ed28cc5 --- /dev/null +++ b/repos/effect/packages/effect/src/internal/runtimeFlagsPatch.ts @@ -0,0 +1,103 @@ +import { dual } from "../Function.js" +import type * as RuntimeFlags from "../RuntimeFlags.js" +import type * as RuntimeFlagsPatch from "../RuntimeFlagsPatch.js" + +/** @internal */ +const BIT_MASK = 0xff + +/** @internal */ +const BIT_SHIFT = 0x08 + +/** @internal */ +export const active = (patch: RuntimeFlagsPatch.RuntimeFlagsPatch): number => patch & BIT_MASK + +/** @internal */ +export const enabled = (patch: RuntimeFlagsPatch.RuntimeFlagsPatch): number => (patch >> BIT_SHIFT) & BIT_MASK + +/** @internal */ +export const make = (active: number, enabled: number): RuntimeFlagsPatch.RuntimeFlagsPatch => + ((active & BIT_MASK) + (((enabled & active) & BIT_MASK) << BIT_SHIFT)) as RuntimeFlagsPatch.RuntimeFlagsPatch + +/** @internal */ +export const empty = make(0, 0) + +/** @internal */ +export const enable = (flag: RuntimeFlags.RuntimeFlag): RuntimeFlagsPatch.RuntimeFlagsPatch => make(flag, flag) + +/** @internal */ +export const disable = (flag: RuntimeFlags.RuntimeFlag): RuntimeFlagsPatch.RuntimeFlagsPatch => make(flag, 0) + +/** @internal */ +export const isEmpty = (patch: RuntimeFlagsPatch.RuntimeFlagsPatch): boolean => patch === 0 + +/** @internal */ +export const isActive = dual< + (flag: RuntimeFlagsPatch.RuntimeFlagsPatch) => (self: RuntimeFlagsPatch.RuntimeFlagsPatch) => boolean, + (self: RuntimeFlagsPatch.RuntimeFlagsPatch, flag: RuntimeFlagsPatch.RuntimeFlagsPatch) => boolean +>(2, (self, flag) => (active(self) & flag) !== 0) + +/** @internal */ +export const isEnabled = dual< + (flag: RuntimeFlags.RuntimeFlag) => (self: RuntimeFlagsPatch.RuntimeFlagsPatch) => boolean, + (self: RuntimeFlagsPatch.RuntimeFlagsPatch, flag: RuntimeFlags.RuntimeFlag) => boolean +>(2, (self, flag) => (enabled(self) & flag) !== 0) + +/** @internal */ +export const isDisabled = dual< + (flag: RuntimeFlags.RuntimeFlag) => (self: RuntimeFlagsPatch.RuntimeFlagsPatch) => boolean, + (self: RuntimeFlagsPatch.RuntimeFlagsPatch, flag: RuntimeFlags.RuntimeFlag) => boolean +>(2, (self, flag) => ((active(self) & flag) !== 0) && ((enabled(self) & flag) === 0)) + +/** @internal */ +export const exclude = dual< + ( + flag: RuntimeFlags.RuntimeFlag + ) => (self: RuntimeFlagsPatch.RuntimeFlagsPatch) => RuntimeFlagsPatch.RuntimeFlagsPatch, + (self: RuntimeFlagsPatch.RuntimeFlagsPatch, flag: RuntimeFlags.RuntimeFlag) => RuntimeFlagsPatch.RuntimeFlagsPatch +>(2, (self, flag) => make(active(self) & ~flag, enabled(self))) + +/** @internal */ +export const both = dual< + ( + that: RuntimeFlagsPatch.RuntimeFlagsPatch + ) => ( + self: RuntimeFlagsPatch.RuntimeFlagsPatch + ) => RuntimeFlagsPatch.RuntimeFlagsPatch, + ( + self: RuntimeFlagsPatch.RuntimeFlagsPatch, + that: RuntimeFlagsPatch.RuntimeFlagsPatch + ) => RuntimeFlagsPatch.RuntimeFlagsPatch +>(2, (self, that) => make(active(self) | active(that), enabled(self) & enabled(that))) + +/** @internal */ +export const either = dual< + ( + that: RuntimeFlagsPatch.RuntimeFlagsPatch + ) => ( + self: RuntimeFlagsPatch.RuntimeFlagsPatch + ) => RuntimeFlagsPatch.RuntimeFlagsPatch, + ( + self: RuntimeFlagsPatch.RuntimeFlagsPatch, + that: RuntimeFlagsPatch.RuntimeFlagsPatch + ) => RuntimeFlagsPatch.RuntimeFlagsPatch +>(2, (self, that) => make(active(self) | active(that), enabled(self) | enabled(that))) + +/** @internal */ +export const andThen = dual< + ( + that: RuntimeFlagsPatch.RuntimeFlagsPatch + ) => ( + self: RuntimeFlagsPatch.RuntimeFlagsPatch + ) => RuntimeFlagsPatch.RuntimeFlagsPatch, + ( + self: RuntimeFlagsPatch.RuntimeFlagsPatch, + that: RuntimeFlagsPatch.RuntimeFlagsPatch + ) => RuntimeFlagsPatch.RuntimeFlagsPatch +>(2, (self, that) => (self | that) as RuntimeFlagsPatch.RuntimeFlagsPatch) + +/** @internal */ +export const inverse = (patch: RuntimeFlagsPatch.RuntimeFlagsPatch): RuntimeFlagsPatch.RuntimeFlagsPatch => + make(enabled(patch), invert(active(patch))) + +/** @internal */ +export const invert = (n: number): number => (~n >>> 0) & BIT_MASK diff --git a/repos/effect/packages/effect/src/internal/schedule.ts b/repos/effect/packages/effect/src/internal/schedule.ts new file mode 100644 index 0000000..4c54b1d --- /dev/null +++ b/repos/effect/packages/effect/src/internal/schedule.ts @@ -0,0 +1,2199 @@ +import type * as Cause from "../Cause.js" +import * as Chunk from "../Chunk.js" +import * as Clock from "../Clock.js" +import * as Context from "../Context.js" +import * as Cron from "../Cron.js" +import type * as DateTime from "../DateTime.js" +import * as Duration from "../Duration.js" +import type * as Effect from "../Effect.js" +import * as Either from "../Either.js" +import * as Equal from "../Equal.js" +import type * as Fiber from "../Fiber.js" +import type { LazyArg } from "../Function.js" +import { constVoid, dual, pipe } from "../Function.js" +import * as Option from "../Option.js" +import { pipeArguments } from "../Pipeable.js" +import { hasProperty, type Predicate } from "../Predicate.js" +import * as Random from "../Random.js" +import type * as Ref from "../Ref.js" +import type * as Schedule from "../Schedule.js" +import * as ScheduleDecision from "../ScheduleDecision.js" +import * as Interval from "../ScheduleInterval.js" +import * as Intervals from "../ScheduleIntervals.js" +import type { Scope } from "../Scope.js" +import type * as Types from "../Types.js" +import * as internalCause from "./cause.js" +import * as effect from "./core-effect.js" +import * as core from "./core.js" +import { forkScoped } from "./effect/circular.js" +import * as ref from "./ref.js" + +/** @internal */ +const ScheduleSymbolKey = "effect/Schedule" + +/** @internal */ +export const ScheduleTypeId: Schedule.ScheduleTypeId = Symbol.for( + ScheduleSymbolKey +) as Schedule.ScheduleTypeId + +/** @internal */ +export const isSchedule = (u: unknown): u is Schedule.Schedule => + hasProperty(u, ScheduleTypeId) + +/** @internal */ +const ScheduleDriverSymbolKey = "effect/ScheduleDriver" + +/** @internal */ +export const ScheduleDriverTypeId: Schedule.ScheduleDriverTypeId = Symbol.for( + ScheduleDriverSymbolKey +) as Schedule.ScheduleDriverTypeId + +/** @internal */ +const defaultIterationMetadata: Schedule.IterationMetadata = { + start: 0, + now: 0, + input: undefined, + output: undefined, + elapsed: Duration.zero, + elapsedSincePrevious: Duration.zero, + recurrence: 0 +} + +/** @internal */ +export const CurrentIterationMetadata = Context.Reference()( + "effect/Schedule/CurrentIterationMetadata", + { defaultValue: () => defaultIterationMetadata } +) + +const scheduleVariance = { + /* c8 ignore next */ + _Out: (_: never) => _, + /* c8 ignore next */ + _In: (_: unknown) => _, + /* c8 ignore next */ + _R: (_: never) => _ +} + +const scheduleDriverVariance = { + /* c8 ignore next */ + _Out: (_: never) => _, + /* c8 ignore next */ + _In: (_: unknown) => _, + /* c8 ignore next */ + _R: (_: never) => _ +} + +/** @internal */ +class ScheduleImpl implements Schedule.Schedule { + [ScheduleTypeId] = scheduleVariance + constructor( + readonly initial: S, + readonly step: ( + now: number, + input: In, + state: S + ) => Effect.Effect + ) { + } + pipe() { + return pipeArguments(this, arguments) + } +} + +/** @internal */ +const updateInfo = ( + iterationMetaRef: Ref.Ref, + now: number, + input: unknown, + output: unknown +) => + ref.update(iterationMetaRef, (prev) => + (prev.recurrence === 0) ? + { + now, + input, + output, + recurrence: prev.recurrence + 1, + elapsed: Duration.zero, + elapsedSincePrevious: Duration.zero, + start: now + } : + { + now, + input, + output, + recurrence: prev.recurrence + 1, + elapsed: Duration.millis(now - prev.start), + elapsedSincePrevious: Duration.millis(now - prev.now), + start: prev.start + }) + +/** @internal */ +class ScheduleDriverImpl implements Schedule.ScheduleDriver { + [ScheduleDriverTypeId] = scheduleDriverVariance + + constructor( + readonly schedule: Schedule.Schedule, + readonly ref: Ref.Ref, any]> + ) {} + + get state(): Effect.Effect { + return core.map(ref.get(this.ref), (tuple) => tuple[1]) + } + + get last(): Effect.Effect { + return core.flatMap(ref.get(this.ref), ([element, _]) => { + switch (element._tag) { + case "None": { + return core.failSync(() => new core.NoSuchElementException()) + } + case "Some": { + return core.succeed(element.value) + } + } + }) + } + + iterationMeta = ref.unsafeMake(defaultIterationMetadata) + + get reset(): Effect.Effect { + return ref.set(this.ref, [Option.none(), this.schedule.initial]).pipe( + core.zipLeft(ref.set(this.iterationMeta, defaultIterationMetadata)) + ) + } + + next(input: In): Effect.Effect, R> { + return pipe( + core.map(ref.get(this.ref), (tuple) => tuple[1]), + core.flatMap((state) => + pipe( + Clock.currentTimeMillis, + core.flatMap((now) => + pipe( + core.suspend(() => this.schedule.step(now, input, state)), + core.flatMap(([state, out, decision]) => { + const setState = ref.set(this.ref, [Option.some(out), state] as const) + if (ScheduleDecision.isDone(decision)) { + return setState.pipe( + core.zipRight(core.fail(Option.none())) + ) + } + const millis = Intervals.start(decision.intervals) - now + if (millis <= 0) { + return setState.pipe( + core.zipRight(updateInfo(this.iterationMeta, now, input, out)), + core.as(out) + ) + } + const duration = Duration.millis(millis) + return pipe( + setState, + core.zipRight(updateInfo(this.iterationMeta, now, input, out)), + core.zipRight(effect.sleep(duration)), + core.as(out) + ) + }) + ) + ) + ) + ) + ) + } +} + +/** @internal */ +export const makeWithState = ( + initial: S, + step: ( + now: number, + input: In, + state: S + ) => Effect.Effect +): Schedule.Schedule => new ScheduleImpl(initial, step) + +/** @internal */ +export const addDelay = dual< + ( + f: (out: Out) => Duration.DurationInput + ) => (self: Schedule.Schedule) => Schedule.Schedule, + ( + self: Schedule.Schedule, + f: (out: Out) => Duration.DurationInput + ) => Schedule.Schedule +>(2, (self, f) => addDelayEffect(self, (out) => core.sync(() => f(out)))) + +/** @internal */ +export const addDelayEffect = dual< + ( + f: (out: Out) => Effect.Effect + ) => (self: Schedule.Schedule) => Schedule.Schedule, + ( + self: Schedule.Schedule, + f: (out: Out) => Effect.Effect + ) => Schedule.Schedule +>(2, (self, f) => + modifyDelayEffect(self, (out, duration) => + core.map( + f(out), + (delay) => Duration.sum(duration, Duration.decode(delay)) + ))) + +/** @internal */ +export const andThen = dual< + ( + that: Schedule.Schedule + ) => (self: Schedule.Schedule) => Schedule.Schedule< + Out | Out2, + In & In2, + R | R2 + >, + ( + self: Schedule.Schedule, + that: Schedule.Schedule + ) => Schedule.Schedule< + Out | Out2, + In & In2, + R | R2 + > +>(2, (self, that) => map(andThenEither(self, that), Either.merge)) + +/** @internal */ +export const andThenEither = dual< + ( + that: Schedule.Schedule + ) => ( + self: Schedule.Schedule + ) => Schedule.Schedule, In & In2, R | R2>, + ( + self: Schedule.Schedule, + that: Schedule.Schedule + ) => Schedule.Schedule, In & In2, R | R2> +>(2, ( + self: Schedule.Schedule, + that: Schedule.Schedule +): Schedule.Schedule, In & In2, R | R2> => + makeWithState( + [self.initial, that.initial, true as boolean] as const, + (now, input, state) => + state[2] ? + core.flatMap(self.step(now, input, state[0]), ([lState, out, decision]) => { + if (ScheduleDecision.isDone(decision)) { + return core.map(that.step(now, input, state[1]), ([rState, out, decision]) => + [ + [lState, rState, false as boolean] as const, + Either.right(out) as Either.Either, + decision as ScheduleDecision.ScheduleDecision + ] as const) + } + return core.succeed( + [ + [lState, state[1], true as boolean] as const, + Either.left(out), + decision + ] as const + ) + }) : + core.map(that.step(now, input, state[1]), ([rState, out, decision]) => + [ + [state[0], rState, false as boolean] as const, + Either.right(out) as Either.Either, + decision + ] as const) + )) + +/** @internal */ +export const as = dual< + (out: Out2) => (self: Schedule.Schedule) => Schedule.Schedule, + (self: Schedule.Schedule, out: Out2) => Schedule.Schedule +>(2, (self, out) => map(self, () => out)) + +/** @internal */ +export const asVoid = ( + self: Schedule.Schedule +): Schedule.Schedule => map(self, constVoid) + +/** @internal */ +export const bothInOut = dual< + ( + that: Schedule.Schedule + ) => ( + self: Schedule.Schedule + ) => Schedule.Schedule<[Out, Out2], readonly [In, In2], R | R2>, + ( + self: Schedule.Schedule, + that: Schedule.Schedule + ) => Schedule.Schedule<[Out, Out2], readonly [In, In2], R | R2> +>(2, (self, that) => + makeWithState([self.initial, that.initial], (now, [in1, in2], state) => + core.zipWith( + self.step(now, in1, state[0]), + that.step(now, in2, state[1]), + ([lState, out, lDecision], [rState, out2, rDecision]) => { + if (ScheduleDecision.isContinue(lDecision) && ScheduleDecision.isContinue(rDecision)) { + const interval = pipe(lDecision.intervals, Intervals.union(rDecision.intervals)) + return [ + [lState, rState], + [out, out2], + ScheduleDecision.continue(interval) + ] + } + return [[lState, rState], [out, out2], ScheduleDecision.done] + } + ))) + +/** @internal */ +export const check = dual< + ( + test: (input: In, output: Out) => boolean + ) => (self: Schedule.Schedule) => Schedule.Schedule, + ( + self: Schedule.Schedule, + test: (input: In, output: Out) => boolean + ) => Schedule.Schedule +>(2, (self, test) => checkEffect(self, (input, out) => core.sync(() => test(input, out)))) + +/** @internal */ +export const checkEffect = dual< + ( + test: (input: In, output: Out) => Effect.Effect + ) => (self: Schedule.Schedule) => Schedule.Schedule, + ( + self: Schedule.Schedule, + test: (input: In, output: Out) => Effect.Effect + ) => Schedule.Schedule +>(2, (self, test) => + makeWithState( + self.initial, + (now, input, state) => + core.flatMap(self.step(now, input, state), ([state, out, decision]) => { + if (ScheduleDecision.isDone(decision)) { + return core.succeed([state, out, ScheduleDecision.done] as const) + } + return core.map(test(input, out), (cont) => + cont ? + [state, out, decision] as const : + [state, out, ScheduleDecision.done] as const) + }) + )) +/** @internal */ +export const collectAllInputs = (): Schedule.Schedule, A> => collectAllOutputs(identity()) + +/** @internal */ +export const collectAllOutputs = ( + self: Schedule.Schedule +): Schedule.Schedule, In, R> => + reduce(self, Chunk.empty(), (outs, out) => pipe(outs, Chunk.append(out))) + +/** @internal */ +export const collectUntil = (f: Predicate): Schedule.Schedule, A> => + collectAllOutputs(recurUntil(f)) + +/** @internal */ +export const collectUntilEffect = ( + f: (a: A) => Effect.Effect +): Schedule.Schedule, A, R> => collectAllOutputs(recurUntilEffect(f)) + +/** @internal */ +export const collectWhile = (f: Predicate): Schedule.Schedule, A> => + collectAllOutputs(recurWhile(f)) + +/** @internal */ +export const collectWhileEffect = ( + f: (a: A) => Effect.Effect +): Schedule.Schedule, A, R> => collectAllOutputs(recurWhileEffect(f)) + +/** @internal */ +export const compose = dual< + ( + that: Schedule.Schedule + ) => (self: Schedule.Schedule) => Schedule.Schedule, + ( + self: Schedule.Schedule, + that: Schedule.Schedule + ) => Schedule.Schedule +>(2, (self, that) => + makeWithState( + [self.initial, that.initial] as const, + (now, input, state) => + core.flatMap( + self.step(now, input, state[0]), + ([lState, out, lDecision]) => + core.map(that.step(now, out, state[1]), ([rState, out2, rDecision]) => + ScheduleDecision.isDone(lDecision) + ? [[lState, rState] as const, out2, ScheduleDecision.done] as const + : ScheduleDecision.isDone(rDecision) + ? [[lState, rState] as const, out2, ScheduleDecision.done] as const + : [ + [lState, rState] as const, + out2, + ScheduleDecision.continue(pipe(lDecision.intervals, Intervals.max(rDecision.intervals))) + ] as const) + ) + )) + +/** @internal */ +export const mapInput = dual< + ( + f: (in2: In2) => In + ) => (self: Schedule.Schedule) => Schedule.Schedule, + ( + self: Schedule.Schedule, + f: (in2: In2) => In + ) => Schedule.Schedule +>(2, (self, f) => mapInputEffect(self, (input2) => core.sync(() => f(input2)))) + +/** @internal */ +export const mapInputContext = dual< + ( + f: (env0: Context.Context) => Context.Context + ) => (self: Schedule.Schedule) => Schedule.Schedule, + ( + self: Schedule.Schedule, + f: (env0: Context.Context) => Context.Context + ) => Schedule.Schedule +>(2, (self, f) => + makeWithState( + self.initial, + (now, input, state) => core.mapInputContext(self.step(now, input, state), f) + )) + +/** @internal */ +export const mapInputEffect = dual< + ( + f: (in2: In2) => Effect.Effect + ) => (self: Schedule.Schedule) => Schedule.Schedule, + ( + self: Schedule.Schedule, + f: (in2: In2) => Effect.Effect + ) => Schedule.Schedule +>(2, (self, f) => + makeWithState(self.initial, (now, input2, state) => + core.flatMap( + f(input2), + (input) => self.step(now, input, state) + ))) + +/** @internal */ +export const cron: { + (expression: Cron.Cron): Schedule.Schedule<[number, number]> + (expression: string, tz?: DateTime.TimeZone | string): Schedule.Schedule<[number, number]> +} = (expression: string | Cron.Cron, tz?: DateTime.TimeZone | string): Schedule.Schedule<[number, number]> => { + const parsed = Cron.isCron(expression) ? Either.right(expression) : Cron.parse(expression, tz) + return makeWithState<[boolean, [number, number, number]], unknown, [number, number]>( + [true, [Number.MIN_SAFE_INTEGER, 0, 0]], + (now, _, [initial, previous]) => { + if (now < previous[0]) { + return core.succeed([ + [false, previous], + [previous[1], previous[2]], + ScheduleDecision.continueWith(Interval.make(previous[1], previous[2])) + ]) + } + + if (Either.isLeft(parsed)) { + return core.die(parsed.left) + } + + const cron = parsed.right + const date = new Date(now) + + let next: number + if (initial && Cron.match(cron, date)) { + next = now + } + + next = Cron.next(cron, date).getTime() + const start = beginningOfSecond(next) + const end = endOfSecond(next) + return core.succeed([ + [false, [next, start, end]], + [start, end], + ScheduleDecision.continueWith(Interval.make(start, end)) + ]) + } + ) +} + +/** @internal */ +export const dayOfMonth = (day: number): Schedule.Schedule => { + return makeWithState<[number, number], unknown, number>( + [Number.NEGATIVE_INFINITY, 0], + (now, _, state) => { + if (!Number.isInteger(day) || day < 1 || 31 < day) { + return core.dieSync(() => + new core.IllegalArgumentException( + `Invalid argument in: dayOfMonth(${day}). Must be in range 1...31` + ) + ) + } + const n = state[1] + const initial = n === 0 + const day0 = nextDayOfMonth(now, day, initial) + const start = beginningOfDay(day0) + const end = endOfDay(day0) + const interval = Interval.make(start, end) + return core.succeed( + [ + [end, n + 1], + n, + ScheduleDecision.continueWith(interval) + ] + ) + } + ) +} + +/** @internal */ +export const dayOfWeek = (day: number): Schedule.Schedule => { + return makeWithState<[number, number], unknown, number>( + [Number.MIN_SAFE_INTEGER, 0], + (now, _, state) => { + if (!Number.isInteger(day) || day < 1 || 7 < day) { + return core.dieSync(() => + new core.IllegalArgumentException( + `Invalid argument in: dayOfWeek(${day}). Must be in range 1 (Monday)...7 (Sunday)` + ) + ) + } + const n = state[1] + const initial = n === 0 + const day0 = nextDay(now, day, initial) + const start = beginningOfDay(day0) + const end = endOfDay(day0) + const interval = Interval.make(start, end) + return core.succeed( + [ + [end, n + 1], + n, + ScheduleDecision.continueWith(interval) + ] + ) + } + ) +} + +/** @internal */ +export const delayed = dual< + ( + f: (duration: Duration.Duration) => Duration.DurationInput + ) => (self: Schedule.Schedule) => Schedule.Schedule, + ( + self: Schedule.Schedule, + f: (duration: Duration.Duration) => Duration.DurationInput + ) => Schedule.Schedule +>(2, (self, f) => delayedEffect(self, (duration) => core.sync(() => f(duration)))) + +/** @internal */ +export const delayedEffect = dual< + ( + f: (duration: Duration.Duration) => Effect.Effect + ) => (self: Schedule.Schedule) => Schedule.Schedule, + ( + self: Schedule.Schedule, + f: (duration: Duration.Duration) => Effect.Effect + ) => Schedule.Schedule +>(2, (self, f) => modifyDelayEffect(self, (_, delay) => f(delay))) + +/** @internal */ +export const delayedSchedule = ( + schedule: Schedule.Schedule +): Schedule.Schedule => addDelay(schedule, (x) => x) + +/** @internal */ +export const delays = ( + self: Schedule.Schedule +): Schedule.Schedule => + makeWithState(self.initial, (now, input, state) => + pipe( + self.step(now, input, state), + core.flatMap(( + [state, _, decision] + ): Effect.Effect<[any, Duration.Duration, ScheduleDecision.ScheduleDecision]> => { + if (ScheduleDecision.isDone(decision)) { + return core.succeed([state, Duration.zero, decision]) + } + return core.succeed( + [ + state, + Duration.millis(Intervals.start(decision.intervals) - now), + decision + ] + ) + }) + )) + +/** @internal */ +export const mapBoth = dual< + ( + options: { + readonly onInput: (in2: In2) => In + readonly onOutput: (out: Out) => Out2 + } + ) => (self: Schedule.Schedule) => Schedule.Schedule, + ( + self: Schedule.Schedule, + options: { + readonly onInput: (in2: In2) => In + readonly onOutput: (out: Out) => Out2 + } + ) => Schedule.Schedule +>(2, (self, { onInput, onOutput }) => map(mapInput(self, onInput), onOutput)) + +/** @internal */ +export const mapBothEffect = dual< + ( + options: { + readonly onInput: (input: In2) => Effect.Effect + readonly onOutput: (out: Out) => Effect.Effect + } + ) => (self: Schedule.Schedule) => Schedule.Schedule, + ( + self: Schedule.Schedule, + options: { + readonly onInput: (input: In2) => Effect.Effect + readonly onOutput: (out: Out) => Effect.Effect + } + ) => Schedule.Schedule +>(2, (self, { onInput, onOutput }) => mapEffect(mapInputEffect(self, onInput), onOutput)) + +/** @internal */ +export const driver = ( + self: Schedule.Schedule +): Effect.Effect> => + pipe( + ref.make, any]>([Option.none(), self.initial]), + core.map((ref) => new ScheduleDriverImpl(self, ref)) + ) + +/** @internal */ +export const duration = ( + durationInput: Duration.DurationInput +): Schedule.Schedule => { + const duration = Duration.decode(durationInput) + const durationMillis = Duration.toMillis(duration) + return makeWithState(true as boolean, (now, _, state) => + core.succeed( + state + ? [ + false, + duration, + ScheduleDecision.continueWith(Interval.after(now + durationMillis)) + ] as const + : [false, Duration.zero, ScheduleDecision.done] as const + )) +} + +/** @internal */ +export const either = dual< + ( + that: Schedule.Schedule + ) => (self: Schedule.Schedule) => Schedule.Schedule<[Out, Out2], In & In2, R | R2>, + ( + self: Schedule.Schedule, + that: Schedule.Schedule + ) => Schedule.Schedule<[Out, Out2], In & In2, R | R2> +>(2, (self, that) => union(self, that)) + +/** @internal */ +export const eitherWith = dual< + ( + that: Schedule.Schedule, + f: (x: Intervals.Intervals, y: Intervals.Intervals) => Intervals.Intervals + ) => (self: Schedule.Schedule) => Schedule.Schedule<[Out, Out2], In & In2, R | R2>, + ( + self: Schedule.Schedule, + that: Schedule.Schedule, + f: (x: Intervals.Intervals, y: Intervals.Intervals) => Intervals.Intervals + ) => Schedule.Schedule<[Out, Out2], In & In2, R | R2> +>(3, (self, that, f) => unionWith(self, that, f)) + +/** @internal */ +export const ensuring = dual< + ( + finalizer: Effect.Effect + ) => (self: Schedule.Schedule) => Schedule.Schedule, + ( + self: Schedule.Schedule, + finalizer: Effect.Effect + ) => Schedule.Schedule +>(2, (self, finalizer) => + makeWithState( + self.initial, + (now, input, state) => + core.flatMap(self.step(now, input, state), ([state, out, decision]) => + ScheduleDecision.isDone(decision) + ? core.as(finalizer, [state, out, decision as ScheduleDecision.ScheduleDecision] as const) + : core.succeed([state, out, decision] as const)) + )) + +/** @internal */ +export const exponential = ( + baseInput: Duration.DurationInput, + factor = 2.0 +): Schedule.Schedule => { + const base = Duration.decode(baseInput) + return delayedSchedule( + map(forever, (i) => Duration.times(base, Math.pow(factor, i))) + ) +} + +/** @internal */ +export const fibonacci = (oneInput: Duration.DurationInput): Schedule.Schedule => { + const one = Duration.decode(oneInput) + return delayedSchedule( + pipe( + unfold( + [one, one] as const, + ([a, b]) => [b, Duration.sum(a, b)] as const + ), + map((out) => out[0]) + ) + ) +} + +/** @internal */ +export const fixed = (intervalInput: Duration.DurationInput): Schedule.Schedule => { + const interval = Duration.decode(intervalInput) + const intervalMillis = Duration.toMillis(interval) + return makeWithState<[Option.Option<[number, number]>, number], unknown, number>( + [Option.none(), 0], + (now, _, [option, n]) => + core.sync(() => { + switch (option._tag) { + case "None": { + return [ + [Option.some([now, now + intervalMillis]), n + 1], + n, + ScheduleDecision.continueWith(Interval.after(now + intervalMillis)) + ] + } + case "Some": { + const [startMillis, lastRun] = option.value + const runningBehind = now > (lastRun + intervalMillis) + const boundary = Equal.equals(interval, Duration.zero) + ? interval + : Duration.millis(intervalMillis - ((now - startMillis) % intervalMillis)) + const sleepTime = Equal.equals(boundary, Duration.zero) ? interval : boundary + const nextRun = runningBehind ? now : now + Duration.toMillis(sleepTime) + return [ + [Option.some([startMillis, nextRun]), n + 1], + n, + ScheduleDecision.continueWith(Interval.after(nextRun)) + ] + } + } + }) + ) +} + +/** @internal */ +export const fromDelay = (delay: Duration.DurationInput): Schedule.Schedule => duration(delay) + +/** @internal */ +export const fromDelays = ( + delay: Duration.DurationInput, + ...delays: Array +): Schedule.Schedule => + makeWithState( + [[delay, ...delays].map((_) => Duration.decode(_)) as Array, true as boolean] as const, + (now, _, [durations, cont]) => + core.sync(() => { + if (cont) { + const x = durations[0]! + const interval = Interval.after(now + Duration.toMillis(x)) + if (durations.length >= 2) { + return [ + [durations.slice(1), true] as const, + x, + ScheduleDecision.continueWith(interval) + ] as const + } + const y = durations.slice(1) + return [ + [[x, ...y] as Array, false] as const, + x, + ScheduleDecision.continueWith(interval) + ] as const + } + return [[durations, false] as const, Duration.zero, ScheduleDecision.done] as const + }) + ) + +/** @internal */ +export const fromFunction = (f: (a: A) => B): Schedule.Schedule => map(identity(), f) + +/** @internal */ +export const hourOfDay = (hour: number): Schedule.Schedule => + makeWithState<[number, number], unknown, number>( + [Number.NEGATIVE_INFINITY, 0], + (now, _, state) => { + if (!Number.isInteger(hour) || hour < 0 || 23 < hour) { + return core.dieSync(() => + new core.IllegalArgumentException( + `Invalid argument in: hourOfDay(${hour}). Must be in range 0...23` + ) + ) + } + const n = state[1] + const initial = n === 0 + const hour0 = nextHour(now, hour, initial) + const start = beginningOfHour(hour0) + const end = endOfHour(hour0) + const interval = Interval.make(start, end) + return core.succeed( + [ + [end, n + 1], + n, + ScheduleDecision.continueWith(interval) + ] + ) + } + ) + +/** @internal */ +export const identity = (): Schedule.Schedule => + makeWithState(void 0, (now, input, state) => + core.succeed( + [ + state, + input, + ScheduleDecision.continueWith(Interval.after(now)) + ] as const + )) + +/** @internal */ +export const intersect = dual< + ( + that: Schedule.Schedule + ) => (self: Schedule.Schedule) => Schedule.Schedule<[Out, Out2], In & In2, R | R2>, + ( + self: Schedule.Schedule, + that: Schedule.Schedule + ) => Schedule.Schedule<[Out, Out2], In & In2, R | R2> +>(2, (self, that) => intersectWith(self, that, Intervals.intersect)) + +/** @internal */ +export const intersectWith = dual< + ( + that: Schedule.Schedule, + f: (x: Intervals.Intervals, y: Intervals.Intervals) => Intervals.Intervals + ) => (self: Schedule.Schedule) => Schedule.Schedule<[Out, Out2], In & In2, R | R2>, + ( + self: Schedule.Schedule, + that: Schedule.Schedule, + f: (x: Intervals.Intervals, y: Intervals.Intervals) => Intervals.Intervals + ) => Schedule.Schedule<[Out, Out2], In & In2, R | R2> +>(3, ( + self: Schedule.Schedule, + that: Schedule.Schedule, + f: (x: Intervals.Intervals, y: Intervals.Intervals) => Intervals.Intervals +): Schedule.Schedule<[Out, Out2], In & In2, Env | Env2> => + makeWithState<[any, any], In & In2, [Out, Out2], Env | Env2>( + [self.initial, that.initial], + (now, input: In & In2, state) => + pipe( + core.zipWith( + self.step(now, input, state[0]), + that.step(now, input, state[1]), + (a, b) => [a, b] as const + ), + core.flatMap(([ + [lState, out, lDecision], + [rState, out2, rDecision] + ]) => { + if (ScheduleDecision.isContinue(lDecision) && ScheduleDecision.isContinue(rDecision)) { + return intersectWithLoop( + self, + that, + input, + lState, + out, + lDecision.intervals, + rState, + out2, + rDecision.intervals, + f + ) + } + return core.succeed( + [ + [lState, rState], + [out, out2], + ScheduleDecision.done + ] + ) + }) + ) + )) + +/** @internal */ +const intersectWithLoop = ( + self: Schedule.Schedule, + that: Schedule.Schedule, + input: In & In1, + lState: State, + out: Out, + lInterval: Intervals.Intervals, + rState: State1, + out2: Out2, + rInterval: Intervals.Intervals, + f: (x: Intervals.Intervals, y: Intervals.Intervals) => Intervals.Intervals +): Effect.Effect< + [[State, State1], [Out, Out2], ScheduleDecision.ScheduleDecision], + never, + Env | Env1 +> => { + const combined = f(lInterval, rInterval) + if (Intervals.isNonEmpty(combined)) { + return core.succeed([ + [lState, rState], + [out, out2], + ScheduleDecision.continue(combined) + ]) + } + + if (pipe(lInterval, Intervals.lessThan(rInterval))) { + return core.flatMap(self.step(Intervals.end(lInterval), input, lState), ([lState, out, decision]) => { + if (ScheduleDecision.isDone(decision)) { + return core.succeed([ + [lState, rState], + [out, out2], + ScheduleDecision.done + ]) + } + return intersectWithLoop( + self, + that, + input, + lState, + out, + decision.intervals, + rState, + out2, + rInterval, + f + ) + }) + } + return core.flatMap(that.step(Intervals.end(rInterval), input, rState), ([rState, out2, decision]) => { + if (ScheduleDecision.isDone(decision)) { + return core.succeed([ + [lState, rState], + [out, out2], + ScheduleDecision.done + ]) + } + return intersectWithLoop( + self, + that, + input, + lState, + out, + lInterval, + rState, + out2, + decision.intervals, + f + ) + }) +} + +/** @internal */ +export const jittered = (self: Schedule.Schedule): Schedule.Schedule => + jitteredWith(self, { min: 0.8, max: 1.2 }) + +/** @internal */ +export const jitteredWith = dual< + (options: { min?: number | undefined; max?: number | undefined }) => ( + self: Schedule.Schedule + ) => Schedule.Schedule, + ( + self: Schedule.Schedule, + options: { min?: number | undefined; max?: number | undefined } + ) => Schedule.Schedule +>(2, (self, options) => { + const { max, min } = Object.assign({ min: 0.8, max: 1.2 }, options) + return delayedEffect(self, (duration) => + core.map(Random.next, (random) => { + const d = Duration.toMillis(duration) + const jittered = d * min * (1 - random) + d * max * random + return Duration.millis(jittered) + })) +}) + +/** @internal */ +export const linear = (baseInput: Duration.DurationInput): Schedule.Schedule => { + const base = Duration.decode(baseInput) + return delayedSchedule(map(forever, (i) => Duration.times(base, i + 1))) +} + +/** @internal */ +export const map = dual< + ( + f: (out: Out) => Out2 + ) => (self: Schedule.Schedule) => Schedule.Schedule, + ( + self: Schedule.Schedule, + f: (out: Out) => Out2 + ) => Schedule.Schedule +>(2, (self, f) => mapEffect(self, (out) => core.sync(() => f(out)))) + +/** @internal */ +export const mapEffect = dual< + ( + f: (out: Out) => Effect.Effect + ) => (self: Schedule.Schedule) => Schedule.Schedule, + ( + self: Schedule.Schedule, + f: (out: Out) => Effect.Effect + ) => Schedule.Schedule +>(2, (self, f) => + makeWithState( + self.initial, + (now, input, state) => + core.flatMap(self.step(now, input, state), ([state, out, decision]) => + core.map( + f(out), + (out2) => [state, out2, decision] as const + )) + )) + +/** @internal */ +export const minuteOfHour = (minute: number): Schedule.Schedule => + makeWithState<[number, number], unknown, number>( + [Number.MIN_SAFE_INTEGER, 0], + (now, _, state) => { + if (!Number.isInteger(minute) || minute < 0 || 59 < minute) { + return core.dieSync(() => + new core.IllegalArgumentException( + `Invalid argument in: minuteOfHour(${minute}). Must be in range 0...59` + ) + ) + } + const n = state[1] + const initial = n === 0 + const minute0 = nextMinute(now, minute, initial) + const start = beginningOfMinute(minute0) + const end = endOfMinute(minute0) + const interval = Interval.make(start, end) + return core.succeed( + [ + [end, n + 1], + n, + ScheduleDecision.continueWith(interval) + ] + ) + } + ) + +/** @internal */ +export const modifyDelay = dual< + ( + f: (out: Out, duration: Duration.Duration) => Duration.DurationInput + ) => (self: Schedule.Schedule) => Schedule.Schedule, + ( + self: Schedule.Schedule, + f: (out: Out, duration: Duration.Duration) => Duration.DurationInput + ) => Schedule.Schedule +>(2, (self, f) => modifyDelayEffect(self, (out, duration) => core.sync(() => f(out, duration)))) + +/** @internal */ +export const modifyDelayEffect = dual< + ( + f: (out: Out, duration: Duration.Duration) => Effect.Effect + ) => (self: Schedule.Schedule) => Schedule.Schedule, + ( + self: Schedule.Schedule, + f: (out: Out, duration: Duration.Duration) => Effect.Effect + ) => Schedule.Schedule +>(2, (self, f) => + makeWithState( + self.initial, + (now, input, state) => + core.flatMap(self.step(now, input, state), ([state, out, decision]) => { + if (ScheduleDecision.isDone(decision)) { + return core.succeed([state, out, decision] as const) + } + const intervals = decision.intervals + const delay = Interval.size(Interval.make(now, Intervals.start(intervals))) + return core.map(f(out, delay), (durationInput) => { + const duration = Duration.decode(durationInput) + const oldStart = Intervals.start(intervals) + const newStart = now + Duration.toMillis(duration) + const delta = newStart - oldStart + const newEnd = Math.max(0, Intervals.end(intervals) + delta) + const newInterval = Interval.make(newStart, newEnd) + return [state, out, ScheduleDecision.continueWith(newInterval)] as const + }) + }) + )) + +/** @internal */ +export const onDecision = dual< + ( + f: (out: Out, decision: ScheduleDecision.ScheduleDecision) => Effect.Effect + ) => (self: Schedule.Schedule) => Schedule.Schedule, + ( + self: Schedule.Schedule, + f: (out: Out, decision: ScheduleDecision.ScheduleDecision) => Effect.Effect + ) => Schedule.Schedule +>(2, (self, f) => + makeWithState( + self.initial, + (now, input, state) => + core.flatMap( + self.step(now, input, state), + ([state, out, decision]) => core.as(f(out, decision), [state, out, decision] as const) + ) + )) + +/** @internal */ +export const passthrough = ( + self: Schedule.Schedule +): Schedule.Schedule => + makeWithState(self.initial, (now, input, state) => + pipe( + self.step(now, input, state), + core.map(([state, _, decision]) => [state, input, decision] as const) + )) + +/** @internal */ +export const provideContext = dual< + ( + context: Context.Context + ) => (self: Schedule.Schedule) => Schedule.Schedule, + ( + self: Schedule.Schedule, + context: Context.Context + ) => Schedule.Schedule +>(2, (self, context) => + makeWithState(self.initial, (now, input, state) => + core.provideContext( + self.step(now, input, state), + context + ))) + +/** @internal */ +export const provideService = dual< + ( + tag: Context.Tag, + service: Types.NoInfer + ) => ( + self: Schedule.Schedule + ) => Schedule.Schedule>, + ( + self: Schedule.Schedule, + tag: Context.Tag, + service: Types.NoInfer + ) => Schedule.Schedule> +>(3, ( + self: Schedule.Schedule, + tag: Context.Tag, + service: Types.NoInfer +): Schedule.Schedule> => + makeWithState(self.initial, (now, input, state) => + core.contextWithEffect((env) => + core.provideContext( + // @ts-expect-error + self.step(now, input, state), + Context.add(env, tag, service) + ) + ))) + +/** @internal */ +export const recurUntil = (f: Predicate): Schedule.Schedule => untilInput(identity(), f) + +/** @internal */ +export const recurUntilEffect = ( + f: (a: A) => Effect.Effect +): Schedule.Schedule => untilInputEffect(identity(), f) + +/** @internal */ +export const recurUntilOption = (pf: (a: A) => Option.Option): Schedule.Schedule, A> => + untilOutput(map(identity(), pf), Option.isSome) + +/** @internal */ +export const recurUpTo = ( + durationInput: Duration.DurationInput +): Schedule.Schedule => { + const duration = Duration.decode(durationInput) + return whileOutput(elapsed, (elapsed) => Duration.lessThan(elapsed, duration)) +} + +/** @internal */ +export const recurWhile = (f: Predicate): Schedule.Schedule => whileInput(identity(), f) + +/** @internal */ +export const recurWhileEffect = ( + f: (a: A) => Effect.Effect +): Schedule.Schedule => whileInputEffect(identity(), f) + +/** @internal */ +export const recurs = (n: number): Schedule.Schedule => whileOutput(forever, (out) => out < n) + +/** @internal */ +export const reduce = dual< + ( + zero: Z, + f: (z: Z, out: Out) => Z + ) => (self: Schedule.Schedule) => Schedule.Schedule, + ( + self: Schedule.Schedule, + zero: Z, + f: (z: Z, out: Out) => Z + ) => Schedule.Schedule +>(3, (self, zero, f) => reduceEffect(self, zero, (z, out) => core.sync(() => f(z, out)))) + +/** @internal */ +export const reduceEffect = dual< + ( + zero: Z, + f: (z: Z, out: Out) => Effect.Effect + ) => (self: Schedule.Schedule) => Schedule.Schedule, + ( + self: Schedule.Schedule, + zero: Z, + f: (z: Z, out: Out) => Effect.Effect + ) => Schedule.Schedule +>(3, (self, zero, f) => + makeWithState( + [self.initial, zero] as const, + (now, input, [s, z]) => + core.flatMap(self.step(now, input, s), ([s, out, decision]) => + ScheduleDecision.isDone(decision) + ? core.succeed([[s, z], z, decision as ScheduleDecision.ScheduleDecision] as const) + : core.map(f(z, out), (z2) => [[s, z2], z, decision] as const)) + )) + +/** @internal */ +export const repeatForever = (self: Schedule.Schedule): Schedule.Schedule => + makeWithState(self.initial, (now, input, state) => { + const step = ( + now: number, + input: In, + state: any + ): Effect.Effect<[any, Out, ScheduleDecision.ScheduleDecision], never, Env> => + core.flatMap( + self.step(now, input, state), + ([state, out, decision]) => + ScheduleDecision.isDone(decision) + ? step(now, input, self.initial) + : core.succeed([state, out, decision]) + ) + return step(now, input, state) + }) + +/** @internal */ +export const repetitions = (self: Schedule.Schedule): Schedule.Schedule => + reduce(self, 0, (n, _) => n + 1) + +/** @internal */ +export const resetAfter = dual< + ( + duration: Duration.DurationInput + ) => ( + self: Schedule.Schedule + ) => Schedule.Schedule, + ( + self: Schedule.Schedule, + duration: Duration.DurationInput + ) => Schedule.Schedule +>(2, (self, durationInput) => { + const duration = Duration.decode(durationInput) + return pipe( + self, + intersect(elapsed), + resetWhen(([, time]) => Duration.greaterThanOrEqualTo(time, duration)), + map((out) => out[0]) + ) +}) + +/** @internal */ +export const resetWhen = dual< + (f: Predicate) => (self: Schedule.Schedule) => Schedule.Schedule, + (self: Schedule.Schedule, f: Predicate) => Schedule.Schedule +>(2, (self, f) => + makeWithState( + self.initial, + (now, input, state) => + core.flatMap(self.step(now, input, state), ([state, out, decision]) => + f(out) + ? self.step(now, input, self.initial) + : core.succeed([state, out, decision] as const)) + )) + +/** @internal */ +export const run = dual< + ( + now: number, + input: Iterable + ) => (self: Schedule.Schedule) => Effect.Effect, never, R>, + ( + self: Schedule.Schedule, + now: number, + input: Iterable + ) => Effect.Effect, never, R> +>(3, (self, now, input) => + pipe( + runLoop(self, now, Chunk.fromIterable(input), self.initial, Chunk.empty()), + core.map((list) => Chunk.reverse(list)) + )) + +/** @internal */ +const runLoop = ( + self: Schedule.Schedule, + now: number, + inputs: Chunk.Chunk, + state: any, + acc: Chunk.Chunk +): Effect.Effect, never, Env> => { + if (!Chunk.isNonEmpty(inputs)) { + return core.succeed(acc) + } + const input = Chunk.headNonEmpty(inputs) + const nextInputs = Chunk.tailNonEmpty(inputs) + return core.flatMap(self.step(now, input, state), ([state, out, decision]) => { + if (ScheduleDecision.isDone(decision)) { + return core.sync(() => pipe(acc, Chunk.prepend(out))) + } + return runLoop( + self, + Intervals.start(decision.intervals), + nextInputs, + state, + Chunk.prepend(acc, out) + ) + }) +} + +/** @internal */ +export const secondOfMinute = (second: number): Schedule.Schedule => + makeWithState<[number, number], unknown, number>( + [Number.NEGATIVE_INFINITY, 0], + (now, _, state) => { + if (!Number.isInteger(second) || second < 0 || 59 < second) { + return core.dieSync(() => + new core.IllegalArgumentException( + `Invalid argument in: secondOfMinute(${second}). Must be in range 0...59` + ) + ) + } + const n = state[1] + const initial = n === 0 + const second0 = nextSecond(now, second, initial) + const start = beginningOfSecond(second0) + const end = endOfSecond(second0) + const interval = Interval.make(start, end) + return core.succeed( + [ + [end, n + 1], + n, + ScheduleDecision.continueWith(interval) + ] + ) + } + ) + +/** @internal */ +export const spaced = (duration: Duration.DurationInput): Schedule.Schedule => addDelay(forever, () => duration) + +/** @internal */ +export const succeed = (value: A): Schedule.Schedule => map(forever, () => value) + +/** @internal */ +export const sync = (evaluate: LazyArg): Schedule.Schedule => map(forever, evaluate) + +/** @internal */ +export const tapInput = dual< + ( + f: (input: In2) => Effect.Effect + ) => (self: Schedule.Schedule) => Schedule.Schedule, + ( + self: Schedule.Schedule, + f: (input: In2) => Effect.Effect + ) => Schedule.Schedule +>(2, (self, f) => + makeWithState(self.initial, (now, input, state) => + core.zipRight( + f(input), + self.step(now, input, state) + ))) + +/** @internal */ +export const tapOutput = dual< + ( + f: (out: Types.NoInfer) => Effect.Effect + ) => (self: Schedule.Schedule) => Schedule.Schedule, + ( + self: Schedule.Schedule, + f: (out: Out) => Effect.Effect + ) => Schedule.Schedule +>( + 2, + ( + self: Schedule.Schedule, + f: (out: Out) => Effect.Effect + ): Schedule.Schedule => + makeWithState(self.initial, (now, input, state) => + core.tap( + self.step(now, input, state), + ([, out]) => f(out) + )) +) + +/** @internal */ +export const unfold = (initial: A, f: (a: A) => A): Schedule.Schedule => + makeWithState(initial, (now, _, state) => + core.sync(() => + [ + f(state), + state, + ScheduleDecision.continueWith(Interval.after(now)) + ] as const + )) + +/** @internal */ +export const union = dual< + ( + that: Schedule.Schedule + ) => (self: Schedule.Schedule) => Schedule.Schedule<[Out, Out2], In & In2, R | R2>, + ( + self: Schedule.Schedule, + that: Schedule.Schedule + ) => Schedule.Schedule<[Out, Out2], In & In2, R | R2> +>(2, (self, that) => unionWith(self, that, Intervals.union)) + +/** @internal */ +export const unionWith = dual< + ( + that: Schedule.Schedule, + f: (x: Intervals.Intervals, y: Intervals.Intervals) => Intervals.Intervals + ) => (self: Schedule.Schedule) => Schedule.Schedule<[Out, Out2], In & In2, R | R2>, + ( + self: Schedule.Schedule, + that: Schedule.Schedule, + f: (x: Intervals.Intervals, y: Intervals.Intervals) => Intervals.Intervals + ) => Schedule.Schedule<[Out, Out2], In & In2, R | R2> +>(3, (self, that, f) => + makeWithState([self.initial, that.initial], (now, input, state) => + core.zipWith( + self.step(now, input, state[0]), + that.step(now, input, state[1]), + ([lState, l, lDecision], [rState, r, rDecision]) => { + if (ScheduleDecision.isDone(lDecision) && ScheduleDecision.isDone(rDecision)) { + return [[lState, rState], [l, r], ScheduleDecision.done] + } + if (ScheduleDecision.isDone(lDecision) && ScheduleDecision.isContinue(rDecision)) { + return [ + [lState, rState], + [l, r], + ScheduleDecision.continue(rDecision.intervals) + ] + } + if (ScheduleDecision.isContinue(lDecision) && ScheduleDecision.isDone(rDecision)) { + return [ + [lState, rState], + [l, r], + ScheduleDecision.continue(lDecision.intervals) + ] + } + if (ScheduleDecision.isContinue(lDecision) && ScheduleDecision.isContinue(rDecision)) { + const combined = f(lDecision.intervals, rDecision.intervals) + return [ + [lState, rState], + [l, r], + ScheduleDecision.continue(combined) + ] + } + throw new Error( + "BUG: Schedule.unionWith - please report an issue at https://github.com/Effect-TS/effect/issues" + ) + } + ))) + +/** @internal */ +export const untilInput = dual< + (f: Predicate) => (self: Schedule.Schedule) => Schedule.Schedule, + (self: Schedule.Schedule, f: Predicate) => Schedule.Schedule +>(2, (self, f) => check(self, (input, _) => !f(input))) + +/** @internal */ +export const untilInputEffect = dual< + ( + f: (input: In) => Effect.Effect + ) => (self: Schedule.Schedule) => Schedule.Schedule, + ( + self: Schedule.Schedule, + f: (input: In) => Effect.Effect + ) => Schedule.Schedule +>(2, (self, f) => checkEffect(self, (input, _) => effect.negate(f(input)))) + +/** @internal */ +export const untilOutput = dual< + (f: Predicate) => (self: Schedule.Schedule) => Schedule.Schedule, + (self: Schedule.Schedule, f: Predicate) => Schedule.Schedule +>(2, (self, f) => check(self, (_, out) => !f(out))) + +/** @internal */ +export const untilOutputEffect = dual< + ( + f: (out: Out) => Effect.Effect + ) => (self: Schedule.Schedule) => Schedule.Schedule, + ( + self: Schedule.Schedule, + f: (out: Out) => Effect.Effect + ) => Schedule.Schedule +>(2, (self, f) => checkEffect(self, (_, out) => effect.negate(f(out)))) + +/** @internal */ +export const upTo = dual< + (duration: Duration.DurationInput) => ( + self: Schedule.Schedule + ) => Schedule.Schedule, + ( + self: Schedule.Schedule, + duration: Duration.DurationInput + ) => Schedule.Schedule +>(2, (self, duration) => zipLeft(self, recurUpTo(duration))) + +/** @internal */ +export const whileInput = dual< + (f: Predicate) => (self: Schedule.Schedule) => Schedule.Schedule, + (self: Schedule.Schedule, f: Predicate) => Schedule.Schedule +>(2, (self, f) => check(self, (input, _) => f(input))) + +/** @internal */ +export const whileInputEffect = dual< + ( + f: (input: In) => Effect.Effect + ) => (self: Schedule.Schedule) => Schedule.Schedule, + ( + self: Schedule.Schedule, + f: (input: In) => Effect.Effect + ) => Schedule.Schedule +>(2, (self, f) => checkEffect(self, (input, _) => f(input))) + +/** @internal */ +export const whileOutput = dual< + (f: Predicate) => (self: Schedule.Schedule) => Schedule.Schedule, + (self: Schedule.Schedule, f: Predicate) => Schedule.Schedule +>(2, (self, f) => check(self, (_, out) => f(out))) + +/** @internal */ +export const whileOutputEffect = dual< + ( + f: (out: Out) => Effect.Effect + ) => (self: Schedule.Schedule) => Schedule.Schedule, + ( + self: Schedule.Schedule, + f: (out: Out) => Effect.Effect + ) => Schedule.Schedule +>(2, (self, f) => checkEffect(self, (_, out) => f(out))) + +/** @internal */ +export const windowed = (intervalInput: Duration.DurationInput): Schedule.Schedule => { + const interval = Duration.decode(intervalInput) + const millis = Duration.toMillis(interval) + return makeWithState<[Option.Option, number], unknown, number>( + [Option.none(), 0], + (now, _, [option, n]) => { + switch (option._tag) { + case "None": { + return core.succeed( + [ + [Option.some(now), n + 1], + n, + ScheduleDecision.continueWith(Interval.after(now + millis)) + ] + ) + } + case "Some": { + return core.succeed( + [ + [Option.some(option.value), n + 1], + n, + ScheduleDecision.continueWith( + Interval.after(now + (millis - ((now - option.value) % millis))) + ) + ] + ) + } + } + } + ) +} + +/** @internal */ +export const zipLeft = dual< + ( + that: Schedule.Schedule + ) => (self: Schedule.Schedule) => Schedule.Schedule, + ( + self: Schedule.Schedule, + that: Schedule.Schedule + ) => Schedule.Schedule +>(2, (self, that) => map(intersect(self, that), (out) => out[0])) + +/** @internal */ +export const zipRight = dual< + ( + that: Schedule.Schedule + ) => (self: Schedule.Schedule) => Schedule.Schedule, + ( + self: Schedule.Schedule, + that: Schedule.Schedule + ) => Schedule.Schedule +>(2, (self, that) => map(intersect(self, that), (out) => out[1])) + +/** @internal */ +export const zipWith = dual< + ( + that: Schedule.Schedule, + f: (out: Out, out2: Out2) => Out3 + ) => (self: Schedule.Schedule) => Schedule.Schedule, + ( + self: Schedule.Schedule, + that: Schedule.Schedule, + f: (out: Out, out2: Out2) => Out3 + ) => Schedule.Schedule +>(3, (self, that, f) => map(intersect(self, that), ([out, out2]) => f(out, out2))) + +// ----------------------------------------------------------------------------- +// Seconds +// ----------------------------------------------------------------------------- + +/** @internal */ +export const beginningOfSecond = (now: number): number => { + const date = new Date(now) + return new Date( + date.getFullYear(), + date.getMonth(), + date.getDate(), + date.getHours(), + date.getMinutes(), + date.getSeconds(), + 0 + ).getTime() +} + +/** @internal */ +export const endOfSecond = (now: number): number => { + const date = new Date(beginningOfSecond(now)) + return date.setSeconds(date.getSeconds() + 1) +} + +/** @internal */ +export const nextSecond = (now: number, second: number, initial: boolean): number => { + const date = new Date(now) + if (date.getSeconds() === second && initial) { + return now + } + if (date.getSeconds() < second) { + return date.setSeconds(second) + } + // Set seconds to the provided value and add one minute + const newDate = new Date(date.setSeconds(second)) + return newDate.setTime(newDate.getTime() + 1000 * 60) +} + +// ----------------------------------------------------------------------------- +// Minutes +// ----------------------------------------------------------------------------- + +/** @internal */ +export const beginningOfMinute = (now: number): number => { + const date = new Date(now) + return new Date( + date.getFullYear(), + date.getMonth(), + date.getDate(), + date.getHours(), + date.getMinutes(), + 0, + 0 + ).getTime() +} + +/** @internal */ +export const endOfMinute = (now: number): number => { + const date = new Date(beginningOfMinute(now)) + return date.setMinutes(date.getMinutes() + 1) +} + +/** @internal */ +export const nextMinute = (now: number, minute: number, initial: boolean): number => { + const date = new Date(now) + if (date.getMinutes() === minute && initial) { + return now + } + if (date.getMinutes() < minute) { + return date.setMinutes(minute) + } + // Set minutes to the provided value and add one hour + const newDate = new Date(date.setMinutes(minute)) + return newDate.setTime(newDate.getTime() + 1000 * 60 * 60) +} + +// ----------------------------------------------------------------------------- +// Hours +// ----------------------------------------------------------------------------- + +/** @internal */ +export const beginningOfHour = (now: number): number => { + const date = new Date(now) + return new Date( + date.getFullYear(), + date.getMonth(), + date.getDate(), + date.getHours(), + 0, + 0, + 0 + ).getTime() +} + +/** @internal */ +export const endOfHour = (now: number): number => { + const date = new Date(beginningOfHour(now)) + return date.setHours(date.getHours() + 1) +} + +/** @internal */ +export const nextHour = (now: number, hour: number, initial: boolean): number => { + const date = new Date(now) + if (date.getHours() === hour && initial) { + return now + } + if (date.getHours() < hour) { + return date.setHours(hour) + } + // Set hours to the provided value and add one day + const newDate = new Date(date.setHours(hour)) + return newDate.setTime(newDate.getTime() + 1000 * 60 * 60 * 24) +} + +// ----------------------------------------------------------------------------- +// Days +// ----------------------------------------------------------------------------- + +/** @internal */ +export const beginningOfDay = (now: number): number => { + const date = new Date(now) + return new Date( + date.getFullYear(), + date.getMonth(), + date.getDate(), + 0, + 0, + 0, + 0 + ).getTime() +} + +/** @internal */ +export const endOfDay = (now: number): number => { + const date = new Date(beginningOfDay(now)) + return date.setDate(date.getDate() + 1) +} + +/** @internal */ +export const nextDay = (now: number, dayOfWeek: number, initial: boolean): number => { + const date = new Date(now) + if (date.getDay() === dayOfWeek && initial) { + return now + } + const nextDayOfWeek = (7 + dayOfWeek - date.getDay()) % 7 + return date.setDate(date.getDate() + (nextDayOfWeek === 0 ? 7 : nextDayOfWeek)) +} + +/** @internal */ +export const nextDayOfMonth = (now: number, day: number, initial: boolean): number => { + const date = new Date(now) + if (date.getDate() === day && initial) { + return now + } + if (date.getDate() < day) { + return date.setDate(day) + } + return findNextMonth(now, day, 1) +} + +/** @internal */ +export const findNextMonth = (now: number, day: number, months: number): number => { + const d = new Date(now) + const tmp1 = new Date(d.setDate(day)) + const tmp2 = new Date(tmp1.setMonth(tmp1.getMonth() + months)) + if (tmp2.getDate() === day) { + const d2 = new Date(now) + const tmp3 = new Date(d2.setDate(day)) + return tmp3.setMonth(tmp3.getMonth() + months) + } + return findNextMonth(now, day, months + 1) +} + +// circular with Effect + +const ScheduleDefectTypeId = Symbol.for("effect/Schedule/ScheduleDefect") +class ScheduleDefect { + readonly [ScheduleDefectTypeId]: typeof ScheduleDefectTypeId + constructor(readonly error: E) { + this[ScheduleDefectTypeId] = ScheduleDefectTypeId + } +} +const isScheduleDefect = (u: unknown): u is ScheduleDefect => hasProperty(u, ScheduleDefectTypeId) +const scheduleDefectWrap = (self: Effect.Effect) => + core.catchAll(self, (e) => core.die(new ScheduleDefect(e))) + +/** @internal */ +export const scheduleDefectRefailCause = (cause: Cause.Cause) => + Option.match( + internalCause.find( + cause, + (_) => internalCause.isDieType(_) && isScheduleDefect(_.defect) ? Option.some(_.defect) : Option.none() + ), + { + onNone: () => cause, + onSome: (error) => internalCause.fail(error.error) + } + ) + +/** @internal */ +export const scheduleDefectRefail = (effect: Effect.Effect) => + core.catchAllCause(effect, (cause) => core.failCause(scheduleDefectRefailCause(cause))) + +/** @internal */ +export const repeat_Effect = dual< + ( + schedule: Schedule.Schedule + ) => (self: Effect.Effect) => Effect.Effect, + ( + self: Effect.Effect, + schedule: Schedule.Schedule + ) => Effect.Effect +>(2, (self, schedule) => repeatOrElse_Effect(self, schedule, (e, _) => core.fail(e))) + +/** @internal */ +export const repeat_combined = dual<{ + , O>, A>( + options: O + ): (self: Effect.Effect) => Effect.Repeat.Return + ( + schedule: Schedule.Schedule + ): (self: Effect.Effect) => Effect.Effect +}, { + , O>>( + self: Effect.Effect, + options: O + ): Effect.Repeat.Return + ( + self: Effect.Effect, + schedule: Schedule.Schedule + ): Effect.Effect +}>( + 2, + (self: Effect.Effect, options: Effect.Repeat.Options | Schedule.Schedule) => { + if (isSchedule(options)) { + return repeat_Effect(self, options) + } + + const base = options.schedule ?? passthrough(forever) + const withWhile = options.while ? + whileInputEffect(base, (a) => { + const applied = options.while!(a) + if (typeof applied === "boolean") { + return core.succeed(applied) + } + return scheduleDefectWrap(applied) + }) : + base + const withUntil = options.until ? + untilInputEffect(withWhile, (a) => { + const applied = options.until!(a) + if (typeof applied === "boolean") { + return core.succeed(applied) + } + return scheduleDefectWrap(applied) + }) : + withWhile + const withTimes = options.times ? + intersect(withUntil, recurs(options.times)).pipe(map((intersectionPair) => intersectionPair[0])) : + withUntil + return scheduleDefectRefail(repeat_Effect(self, withTimes)) + } +) + +/** @internal */ +export const repeatOrElse_Effect = dual< + ( + schedule: Schedule.Schedule, + orElse: (error: E, option: Option.Option) => Effect.Effect + ) => (self: Effect.Effect) => Effect.Effect, + ( + self: Effect.Effect, + schedule: Schedule.Schedule, + orElse: (error: E, option: Option.Option) => Effect.Effect + ) => Effect.Effect +>(3, (self, schedule, orElse) => + core.flatMap(driver(schedule), (driver) => + core.matchEffect(self, { + onFailure: (error) => orElse(error, Option.none()), + onSuccess: (value) => + repeatOrElseEffectLoop( + effect.provideServiceEffect( + self, + CurrentIterationMetadata, + ref.get(driver.iterationMeta) + ), + driver, + (error, option) => + effect.provideServiceEffect( + orElse(error, option), + CurrentIterationMetadata, + ref.get(driver.iterationMeta) + ), + value + ) + }))) + +/** @internal */ +const repeatOrElseEffectLoop = ( + self: Effect.Effect, + driver: Schedule.ScheduleDriver, + orElse: (error: E, option: Option.Option) => Effect.Effect, + value: A +): Effect.Effect => + core.matchEffect(driver.next(value), { + onFailure: () => core.orDie(driver.last), + onSuccess: (b) => + core.matchEffect(self, { + onFailure: (error) => orElse(error, Option.some(b)), + onSuccess: (value) => repeatOrElseEffectLoop(self, driver, orElse, value) + }) + }) + +/** @internal */ +export const retry_Effect = dual< + ( + policy: Schedule.Schedule + ) => (self: Effect.Effect) => Effect.Effect, + ( + self: Effect.Effect, + policy: Schedule.Schedule + ) => Effect.Effect +>(2, (self, policy) => retryOrElse_Effect(self, policy, (e, _) => core.fail(e))) + +/** @internal */ +export const retry_combined: { + , O>>( + options: O + ): ( + self: Effect.Effect + ) => Effect.Retry.Return + ( + policy: Schedule.Schedule, R1> + ): (self: Effect.Effect) => Effect.Effect + , O>>( + self: Effect.Effect, + options: O + ): Effect.Retry.Return + ( + self: Effect.Effect, + policy: Schedule.Schedule, R1> + ): Effect.Effect +} = dual( + 2, + ( + self: Effect.Effect, + options: Effect.Retry.Options | Schedule.Schedule + ) => { + if (isSchedule(options)) { + return retry_Effect(self, options) + } + return scheduleDefectRefail(retry_Effect(self, fromRetryOptions(options))) + } +) + +/** @internal */ +export const fromRetryOptions = (options: Effect.Retry.Options): Schedule.Schedule => { + const base = options.schedule ?? forever + const withWhile = options.while ? + whileInputEffect(base, (e) => { + const applied = options.while!(e) + if (typeof applied === "boolean") { + return core.succeed(applied) + } + return scheduleDefectWrap(applied) + }) : + base + const withUntil = options.until ? + untilInputEffect(withWhile, (e) => { + const applied = options.until!(e) + if (typeof applied === "boolean") { + return core.succeed(applied) + } + return scheduleDefectWrap(applied) + }) : + withWhile + return options.times !== undefined ? + intersect(withUntil, recurs(options.times)) : + withUntil +} + +/** @internal */ +export const retryOrElse_Effect = dual< + ( + policy: Schedule.Schedule, R1>, + orElse: (e: Types.NoInfer, out: A1) => Effect.Effect + ) => (self: Effect.Effect) => Effect.Effect, + ( + self: Effect.Effect, + policy: Schedule.Schedule, R1>, + orElse: (e: Types.NoInfer, out: A1) => Effect.Effect + ) => Effect.Effect +>(3, (self, policy, orElse) => + core.flatMap( + driver(policy), + (driver) => + retryOrElse_EffectLoop( + effect.provideServiceEffect( + self, + CurrentIterationMetadata, + ref.get(driver.iterationMeta) + ), + driver, + (e, out) => + effect.provideServiceEffect( + orElse(e, out), + CurrentIterationMetadata, + ref.get(driver.iterationMeta) + ) + ) + )) + +/** @internal */ +const retryOrElse_EffectLoop = ( + self: Effect.Effect, + driver: Schedule.ScheduleDriver, + orElse: (e: E, out: A1) => Effect.Effect +): Effect.Effect => { + return core.catchAll( + self, + (e) => + core.matchEffect(driver.next(e), { + onFailure: () => + pipe( + driver.last, + core.orDie, + core.flatMap((out) => orElse(e, out)) + ), + onSuccess: () => retryOrElse_EffectLoop(self, driver, orElse) + }) + ) +} + +/** @internal */ +export const schedule_Effect = dual< + ( + schedule: Schedule.Schedule | undefined, R2> + ) => (self: Effect.Effect) => Effect.Effect, + ( + self: Effect.Effect, + schedule: Schedule.Schedule + ) => Effect.Effect +>(2, ( + self: Effect.Effect, + schedule: Schedule.Schedule +) => scheduleFrom_Effect(self, void 0, schedule)) + +/** @internal */ +export const scheduleFrom_Effect = dual< + ( + initial: In, + schedule: Schedule.Schedule + ) => (self: Effect.Effect) => Effect.Effect, + ( + self: Effect.Effect, + initial: In, + schedule: Schedule.Schedule + ) => Effect.Effect +>(3, (self, initial, schedule) => + core.flatMap( + driver(schedule), + (driver) => + scheduleFrom_EffectLoop( + effect.provideServiceEffect( + self, + CurrentIterationMetadata, + ref.get(driver.iterationMeta) + ), + initial, + driver + ) + )) + +/** @internal */ +const scheduleFrom_EffectLoop = ( + self: Effect.Effect, + initial: In, + driver: Schedule.ScheduleDriver +): Effect.Effect => + core.matchEffect(driver.next(initial), { + onFailure: () => core.orDie(driver.last), + onSuccess: () => + core.flatMap( + self, + (a) => scheduleFrom_EffectLoop(self, a, driver) + ) + }) + +/** @internal */ +export const count: Schedule.Schedule = unfold(0, (n) => n + 1) + +/** @internal */ +export const elapsed: Schedule.Schedule = makeWithState( + Option.none() as Option.Option, + (now, _, state) => { + switch (state._tag) { + case "None": { + return core.succeed( + [ + Option.some(now), + Duration.zero, + ScheduleDecision.continueWith(Interval.after(now)) + ] as const + ) + } + case "Some": { + return core.succeed( + [ + Option.some(state.value), + Duration.millis(now - state.value), + ScheduleDecision.continueWith(Interval.after(now)) + ] as const + ) + } + } + } +) + +/** @internal */ +export const forever: Schedule.Schedule = unfold(0, (n) => n + 1) + +/** @internal */ +export const once: Schedule.Schedule = asVoid(recurs(1)) + +/** @internal */ +export const stop: Schedule.Schedule = asVoid(recurs(0)) + +/** @internal */ +export const scheduleForked = dual< + ( + schedule: Schedule.Schedule + ) => ( + self: Effect.Effect + ) => Effect.Effect, never, R | R2 | Scope>, + ( + self: Effect.Effect, + schedule: Schedule.Schedule + ) => Effect.Effect, never, R | R2 | Scope> +>(2, (self, schedule) => forkScoped(schedule_Effect(self, schedule))) diff --git a/repos/effect/packages/effect/src/internal/schedule/decision.ts b/repos/effect/packages/effect/src/internal/schedule/decision.ts new file mode 100644 index 0000000..b981bd3 --- /dev/null +++ b/repos/effect/packages/effect/src/internal/schedule/decision.ts @@ -0,0 +1,47 @@ +import * as Chunk from "../../Chunk.js" +import type * as ScheduleDecision from "../../ScheduleDecision.js" +import type * as Interval from "../../ScheduleInterval.js" +import * as Intervals from "../../ScheduleIntervals.js" + +/** @internal */ +export const OP_CONTINUE = "Continue" as const + +/** @internal */ +export type OP_CONTINUE = typeof OP_CONTINUE + +/** @internal */ +export const OP_DONE = "Done" as const + +/** @internal */ +export type OP_DONE = typeof OP_DONE + +/** @internal */ +export const _continue = (intervals: Intervals.Intervals): ScheduleDecision.ScheduleDecision => { + return { + _tag: OP_CONTINUE, + intervals + } +} + +/** @internal */ +export const continueWith = (interval: Interval.Interval): ScheduleDecision.ScheduleDecision => { + return { + _tag: OP_CONTINUE, + intervals: Intervals.make(Chunk.of(interval)) + } +} + +/** @internal */ +export const done: ScheduleDecision.ScheduleDecision = { + _tag: OP_DONE +} + +/** @internal */ +export const isContinue = (self: ScheduleDecision.ScheduleDecision): self is ScheduleDecision.Continue => { + return self._tag === OP_CONTINUE +} + +/** @internal */ +export const isDone = (self: ScheduleDecision.ScheduleDecision): self is ScheduleDecision.Done => { + return self._tag === OP_DONE +} diff --git a/repos/effect/packages/effect/src/internal/schedule/interval.ts b/repos/effect/packages/effect/src/internal/schedule/interval.ts new file mode 100644 index 0000000..c459001 --- /dev/null +++ b/repos/effect/packages/effect/src/internal/schedule/interval.ts @@ -0,0 +1,101 @@ +import * as Duration from "../../Duration.js" +import { dual } from "../../Function.js" +import * as Option from "../../Option.js" +import type * as Interval from "../../ScheduleInterval.js" + +/** @internal */ +const IntervalSymbolKey = "effect/ScheduleInterval" + +/** @internal */ +export const IntervalTypeId: Interval.IntervalTypeId = Symbol.for( + IntervalSymbolKey +) as Interval.IntervalTypeId + +/** @internal */ +export const empty: Interval.Interval = { + [IntervalTypeId]: IntervalTypeId, + startMillis: 0, + endMillis: 0 +} + +/** @internal */ +export const make = (startMillis: number, endMillis: number): Interval.Interval => { + if (startMillis > endMillis) { + return empty + } + return { + [IntervalTypeId]: IntervalTypeId, + startMillis, + endMillis + } +} + +/** @internal */ +export const lessThan = dual< + (that: Interval.Interval) => (self: Interval.Interval) => boolean, + (self: Interval.Interval, that: Interval.Interval) => boolean +>(2, (self, that) => min(self, that) === self) + +/** @internal */ +export const min = dual< + (that: Interval.Interval) => (self: Interval.Interval) => Interval.Interval, + (self: Interval.Interval, that: Interval.Interval) => Interval.Interval +>(2, (self, that) => { + if (self.endMillis <= that.startMillis) return self + if (that.endMillis <= self.startMillis) return that + if (self.startMillis < that.startMillis) return self + if (that.startMillis < self.startMillis) return that + if (self.endMillis <= that.endMillis) return self + return that +}) + +/** @internal */ +export const max = dual< + (that: Interval.Interval) => (self: Interval.Interval) => Interval.Interval, + (self: Interval.Interval, that: Interval.Interval) => Interval.Interval +>(2, (self, that) => min(self, that) === self ? that : self) + +/** @internal */ +export const isEmpty = (self: Interval.Interval): boolean => { + return self.startMillis >= self.endMillis +} + +/** @internal */ +export const isNonEmpty = (self: Interval.Interval): boolean => { + return !isEmpty(self) +} + +/** @internal */ +export const intersect = dual< + (that: Interval.Interval) => (self: Interval.Interval) => Interval.Interval, + (self: Interval.Interval, that: Interval.Interval) => Interval.Interval +>(2, (self, that) => { + const start = Math.max(self.startMillis, that.startMillis) + const end = Math.min(self.endMillis, that.endMillis) + return make(start, end) +}) + +/** @internal */ +export const size = (self: Interval.Interval): Duration.Duration => { + return Duration.millis(self.endMillis - self.startMillis) +} + +/** @internal */ +export const union = dual< + (that: Interval.Interval) => (self: Interval.Interval) => Option.Option, + (self: Interval.Interval, that: Interval.Interval) => Option.Option +>(2, (self, that) => { + const start = Math.max(self.startMillis, that.startMillis) + const end = Math.min(self.endMillis, that.endMillis) + return start < end ? Option.none() : Option.some(make(start, end)) +}) + +/** @internal */ +export const after = (startMilliseconds: number): Interval.Interval => { + return make(startMilliseconds, Number.POSITIVE_INFINITY) +} + +/** @internal */ +export const before = (endMilliseconds: number): Interval.Interval => { + return make(Number.NEGATIVE_INFINITY, endMilliseconds) +} diff --git a/repos/effect/packages/effect/src/internal/schedule/intervals.ts b/repos/effect/packages/effect/src/internal/schedule/intervals.ts new file mode 100644 index 0000000..f3b8de5 --- /dev/null +++ b/repos/effect/packages/effect/src/internal/schedule/intervals.ts @@ -0,0 +1,180 @@ +import * as Chunk from "../../Chunk.js" +import { dual, pipe } from "../../Function.js" +import * as Option from "../../Option.js" +import * as Interval from "../../ScheduleInterval.js" +import type * as Intervals from "../../ScheduleIntervals.js" +import { getBugErrorMessage } from "../errors.js" + +/** @internal */ +const IntervalsSymbolKey = "effect/ScheduleIntervals" + +/** @internal */ +export const IntervalsTypeId: Intervals.IntervalsTypeId = Symbol.for( + IntervalsSymbolKey +) as Intervals.IntervalsTypeId + +/** @internal */ +export const make = (intervals: Chunk.Chunk): Intervals.Intervals => { + return { + [IntervalsTypeId]: IntervalsTypeId, + intervals + } +} +/** @internal */ +export const empty: Intervals.Intervals = make(Chunk.empty()) + +/** @internal */ +export const fromIterable = (intervals: Iterable): Intervals.Intervals => + Array.from(intervals).reduce( + (intervals, interval) => pipe(intervals, union(make(Chunk.of(interval)))), + empty + ) + +/** @internal */ +export const union = dual< + (that: Intervals.Intervals) => (self: Intervals.Intervals) => Intervals.Intervals, + (self: Intervals.Intervals, that: Intervals.Intervals) => Intervals.Intervals +>(2, (self, that) => { + if (!Chunk.isNonEmpty(that.intervals)) { + return self + } + if (!Chunk.isNonEmpty(self.intervals)) { + return that + } + if (Chunk.headNonEmpty(self.intervals).startMillis < Chunk.headNonEmpty(that.intervals).startMillis) { + return unionLoop( + Chunk.tailNonEmpty(self.intervals), + that.intervals, + Chunk.headNonEmpty(self.intervals), + Chunk.empty() + ) + } + return unionLoop( + self.intervals, + Chunk.tailNonEmpty(that.intervals), + Chunk.headNonEmpty(that.intervals), + Chunk.empty() + ) +}) + +/** @internal */ +const unionLoop = ( + _self: Chunk.Chunk, + _that: Chunk.Chunk, + _interval: Interval.Interval, + _acc: Chunk.Chunk +): Intervals.Intervals => { + let self = _self + let that = _that + let interval = _interval + let acc = _acc + while (Chunk.isNonEmpty(self) || Chunk.isNonEmpty(that)) { + if (!Chunk.isNonEmpty(self) && Chunk.isNonEmpty(that)) { + if (interval.endMillis < Chunk.headNonEmpty(that).startMillis) { + acc = pipe(acc, Chunk.prepend(interval)) + interval = Chunk.headNonEmpty(that) + that = Chunk.tailNonEmpty(that) + self = Chunk.empty() + } else { + interval = Interval.make(interval.startMillis, Chunk.headNonEmpty(that).endMillis) + that = Chunk.tailNonEmpty(that) + self = Chunk.empty() + } + } else if (Chunk.isNonEmpty(self) && Chunk.isEmpty(that)) { + if (interval.endMillis < Chunk.headNonEmpty(self).startMillis) { + acc = pipe(acc, Chunk.prepend(interval)) + interval = Chunk.headNonEmpty(self) + that = Chunk.empty() + self = Chunk.tailNonEmpty(self) + } else { + interval = Interval.make(interval.startMillis, Chunk.headNonEmpty(self).endMillis) + that = Chunk.empty() + self = Chunk.tailNonEmpty(self) + } + } else if (Chunk.isNonEmpty(self) && Chunk.isNonEmpty(that)) { + if (Chunk.headNonEmpty(self).startMillis < Chunk.headNonEmpty(that).startMillis) { + if (interval.endMillis < Chunk.headNonEmpty(self).startMillis) { + acc = pipe(acc, Chunk.prepend(interval)) + interval = Chunk.headNonEmpty(self) + self = Chunk.tailNonEmpty(self) + } else { + interval = Interval.make(interval.startMillis, Chunk.headNonEmpty(self).endMillis) + self = Chunk.tailNonEmpty(self) + } + } else if (interval.endMillis < Chunk.headNonEmpty(that).startMillis) { + acc = pipe(acc, Chunk.prepend(interval)) + interval = Chunk.headNonEmpty(that) + that = Chunk.tailNonEmpty(that) + } else { + interval = Interval.make(interval.startMillis, Chunk.headNonEmpty(that).endMillis) + that = Chunk.tailNonEmpty(that) + } + } else { + throw new Error(getBugErrorMessage("Intervals.unionLoop")) + } + } + return make(pipe(acc, Chunk.prepend(interval), Chunk.reverse)) +} + +/** @internal */ +export const intersect = dual< + (that: Intervals.Intervals) => (self: Intervals.Intervals) => Intervals.Intervals, + (self: Intervals.Intervals, that: Intervals.Intervals) => Intervals.Intervals +>(2, (self, that) => intersectLoop(self.intervals, that.intervals, Chunk.empty())) + +/** @internal */ +const intersectLoop = ( + _left: Chunk.Chunk, + _right: Chunk.Chunk, + _acc: Chunk.Chunk +): Intervals.Intervals => { + let left = _left + let right = _right + let acc = _acc + while (Chunk.isNonEmpty(left) && Chunk.isNonEmpty(right)) { + const interval = pipe(Chunk.headNonEmpty(left), Interval.intersect(Chunk.headNonEmpty(right))) + const intervals = Interval.isEmpty(interval) ? acc : pipe(acc, Chunk.prepend(interval)) + if (pipe(Chunk.headNonEmpty(left), Interval.lessThan(Chunk.headNonEmpty(right)))) { + left = Chunk.tailNonEmpty(left) + } else { + right = Chunk.tailNonEmpty(right) + } + acc = intervals + } + return make(Chunk.reverse(acc)) +} + +/** @internal */ +export const start = (self: Intervals.Intervals): number => { + return pipe( + self.intervals, + Chunk.head, + Option.getOrElse(() => Interval.empty) + ).startMillis +} + +/** @internal */ +export const end = (self: Intervals.Intervals): number => { + return pipe( + self.intervals, + Chunk.head, + Option.getOrElse(() => Interval.empty) + ).endMillis +} + +/** @internal */ +export const lessThan = dual< + (that: Intervals.Intervals) => (self: Intervals.Intervals) => boolean, + (self: Intervals.Intervals, that: Intervals.Intervals) => boolean +>(2, (self, that) => start(self) < start(that)) + +/** @internal */ +export const isNonEmpty = (self: Intervals.Intervals): boolean => { + return Chunk.isNonEmpty(self.intervals) +} + +/** @internal */ +export const max = dual< + (that: Intervals.Intervals) => (self: Intervals.Intervals) => Intervals.Intervals, + (self: Intervals.Intervals, that: Intervals.Intervals) => Intervals.Intervals +>(2, (self, that) => lessThan(self, that) ? that : self) diff --git a/repos/effect/packages/effect/src/internal/schema/errors.ts b/repos/effect/packages/effect/src/internal/schema/errors.ts new file mode 100644 index 0000000..80ea2f5 --- /dev/null +++ b/repos/effect/packages/effect/src/internal/schema/errors.ts @@ -0,0 +1,191 @@ +import * as array_ from "../../Array.js" +import * as Inspectable from "../../Inspectable.js" +import type * as AST from "../../SchemaAST.js" +import * as util_ from "./util.js" + +const getErrorMessage = ( + reason: string, + details?: string, + path?: ReadonlyArray, + ast?: AST.AST +): string => { + let out = reason + + if (path && array_.isNonEmptyReadonlyArray(path)) { + out += `\nat path: ${util_.formatPath(path)}` + } + + if (details !== undefined) { + out += `\ndetails: ${details}` + } + + if (ast) { + out += `\nschema (${ast._tag}): ${ast}` + } + + return out +} + +// --------------------------------------------- +// generic +// --------------------------------------------- + +/** @internal */ +export const getInvalidArgumentErrorMessage = (details: string) => getErrorMessage("Invalid Argument", details) + +const getUnsupportedSchemaErrorMessage = (details?: string, path?: ReadonlyArray, ast?: AST.AST): string => + getErrorMessage("Unsupported schema", details, path, ast) + +const getMissingAnnotationErrorMessage = (details?: string, path?: ReadonlyArray, ast?: AST.AST): string => + getErrorMessage("Missing annotation", details, path, ast) + +// --------------------------------------------- +// Arbitrary +// --------------------------------------------- + +/** @internal */ +export const getArbitraryUnsupportedErrorMessage = (path: ReadonlyArray, ast: AST.AST) => + getUnsupportedSchemaErrorMessage("Cannot build an Arbitrary for this schema", path, ast) + +/** @internal */ +export const getArbitraryMissingAnnotationErrorMessage = ( + path: ReadonlyArray, + ast: AST.AST +) => + getMissingAnnotationErrorMessage( + `Generating an Arbitrary for this schema requires an "arbitrary" annotation`, + path, + ast + ) + +/** @internal */ +export const getArbitraryEmptyEnumErrorMessage = (path: ReadonlyArray) => + getErrorMessage("Empty Enums schema", "Generating an Arbitrary for this schema requires at least one enum", path) + +// --------------------------------------------- +// Equivalence +// --------------------------------------------- + +/** @internal */ +export const getEquivalenceUnsupportedErrorMessage = (ast: AST.AST, path: ReadonlyArray) => + getUnsupportedSchemaErrorMessage("Cannot build an Equivalence", path, ast) + +// --------------------------------------------- +// JSON Schema +// --------------------------------------------- + +/** @internal */ +export const getJSONSchemaMissingAnnotationErrorMessage = ( + path: ReadonlyArray, + ast: AST.AST +) => + getMissingAnnotationErrorMessage( + `Generating a JSON Schema for this schema requires a "jsonSchema" annotation`, + path, + ast + ) + +/** @internal */ +export const getJSONSchemaMissingIdentifierAnnotationErrorMessage = ( + path: ReadonlyArray, + ast: AST.AST +) => + getMissingAnnotationErrorMessage( + `Generating a JSON Schema for this schema requires an "identifier" annotation`, + path, + ast + ) + +/** @internal */ +export const getJSONSchemaUnsupportedPostRestElementsErrorMessage = (path: ReadonlyArray): string => + getErrorMessage( + "Generating a JSON Schema for post-rest elements is not currently supported. You're welcome to contribute by submitting a Pull Request", + undefined, + path + ) + +/** @internal */ +export const getJSONSchemaUnsupportedKeyErrorMessage = (key: PropertyKey, path: ReadonlyArray): string => + getErrorMessage("Unsupported key", `Cannot encode ${Inspectable.formatPropertyKey(key)} key to JSON Schema`, path) + +// --------------------------------------------- +// Pretty +// --------------------------------------------- + +/** @internal */ +export const getPrettyMissingAnnotationErrorMessage = ( + path: ReadonlyArray, + ast: AST.AST +) => getMissingAnnotationErrorMessage(`Generating a Pretty for this schema requires a "pretty" annotation`, path, ast) + +/** @internal */ +export const getPrettyNeverErrorMessage = "Cannot pretty print a `never` value" + +/** @internal */ +export const getPrettyNoMatchingSchemaErrorMessage = ( + actual: unknown, + path: ReadonlyArray, + ast: AST.AST +) => + getErrorMessage( + "Unexpected Error", + `Cannot find a matching schema for ${Inspectable.formatUnknown(actual)}`, + path, + ast + ) + +// --------------------------------------------- +// Schema +// --------------------------------------------- + +/** @internal */ +export const getSchemaExtendErrorMessage = (x: AST.AST, y: AST.AST, path: ReadonlyArray) => + getErrorMessage("Unsupported schema or overlapping types", `cannot extend ${x} with ${y}`, path) + +/** @internal */ +export const getSchemaUnsupportedLiteralSpanErrorMessage = (ast: AST.AST) => + getErrorMessage("Unsupported template literal span", undefined, undefined, ast) + +// --------------------------------------------- +// AST +// --------------------------------------------- + +/** @internal */ +export const getASTUnsupportedSchemaErrorMessage = (ast: AST.AST) => + getUnsupportedSchemaErrorMessage(undefined, undefined, ast) + +/** @internal */ +export const getASTUnsupportedKeySchemaErrorMessage = (ast: AST.AST) => + getErrorMessage("Unsupported key schema", undefined, undefined, ast) + +/** @internal */ +export const getASTUnsupportedLiteralErrorMessage = (literal: AST.LiteralValue) => + getErrorMessage("Unsupported literal", `literal value: ${Inspectable.formatUnknown(literal)}`) + +/** @internal */ +export const getASTDuplicateIndexSignatureErrorMessage = (type: "string" | "symbol"): string => + getErrorMessage("Duplicate index signature", `${type} index signature`) + +/** @internal */ +export const getASTIndexSignatureParameterErrorMessage = getErrorMessage( + "Unsupported index signature parameter", + "An index signature parameter type must be `string`, `symbol`, a template literal type or a refinement of the previous types" +) + +/** @internal */ +export const getASTRequiredElementFollowinAnOptionalElementErrorMessage = getErrorMessage( + "Invalid element", + "A required element cannot follow an optional element. ts(1257)" +) + +/** @internal */ +export const getASTDuplicatePropertySignatureTransformationErrorMessage = (key: PropertyKey): string => + getErrorMessage("Duplicate property signature transformation", `Duplicate key ${Inspectable.formatUnknown(key)}`) + +/** @internal */ +export const getASTUnsupportedRenameSchemaErrorMessage = (ast: AST.AST): string => + getUnsupportedSchemaErrorMessage(undefined, undefined, ast) + +/** @internal */ +export const getASTDuplicatePropertySignatureErrorMessage = (key: PropertyKey): string => + getErrorMessage("Duplicate property signature", `Duplicate key ${Inspectable.formatUnknown(key)}`) diff --git a/repos/effect/packages/effect/src/internal/schema/schemaId.ts b/repos/effect/packages/effect/src/internal/schema/schemaId.ts new file mode 100644 index 0000000..21164b9 --- /dev/null +++ b/repos/effect/packages/effect/src/internal/schema/schemaId.ts @@ -0,0 +1,106 @@ +import type * as Schema from "../../Schema.js" + +/** @internal */ +export const DateFromSelfSchemaId: Schema.DateFromSelfSchemaId = Symbol.for( + "effect/SchemaId/DateFromSelf" +) as Schema.DateFromSelfSchemaId + +/** @internal */ +export const GreaterThanSchemaId: Schema.GreaterThanSchemaId = Symbol.for( + "effect/SchemaId/GreaterThan" +) as Schema.GreaterThanSchemaId + +/** @internal */ +export const GreaterThanOrEqualToSchemaId: Schema.GreaterThanOrEqualToSchemaId = Symbol.for( + "effect/SchemaId/GreaterThanOrEqualTo" +) as Schema.GreaterThanOrEqualToSchemaId + +/** @internal */ +export const LessThanSchemaId: Schema.LessThanSchemaId = Symbol.for( + "effect/SchemaId/LessThan" +) as Schema.LessThanSchemaId + +/** @internal */ +export const LessThanOrEqualToSchemaId: Schema.LessThanOrEqualToSchemaId = Symbol.for( + "effect/SchemaId/LessThanOrEqualTo" +) as Schema.LessThanOrEqualToSchemaId + +/** @internal */ +export const IntSchemaId: Schema.IntSchemaId = Symbol.for( + "effect/SchemaId/Int" +) as Schema.IntSchemaId + +/** @internal */ +export const NonNaNSchemaId: Schema.NonNaNSchemaId = Symbol.for( + "effect/SchemaId/NonNaN" +) as Schema.NonNaNSchemaId + +/** @internal */ +export const FiniteSchemaId: Schema.FiniteSchemaId = Symbol.for( + "effect/SchemaId/Finite" +) as Schema.FiniteSchemaId + +/** @internal */ +export const JsonNumberSchemaId: Schema.JsonNumberSchemaId = Symbol.for( + "effect/SchemaId/JsonNumber" +) as Schema.JsonNumberSchemaId + +/** @internal */ +export const BetweenSchemaId: Schema.BetweenSchemaId = Symbol.for( + "effect/SchemaId/Between" +) as Schema.BetweenSchemaId + +/** @internal */ +export const GreaterThanBigintSchemaId: Schema.GreaterThanBigIntSchemaId = Symbol.for( + "effect/SchemaId/GreaterThanBigint" +) as Schema.GreaterThanBigIntSchemaId + +/** @internal */ +export const GreaterThanOrEqualToBigIntSchemaId: Schema.GreaterThanOrEqualToBigIntSchemaId = Symbol.for( + "effect/SchemaId/GreaterThanOrEqualToBigint" +) as Schema.GreaterThanOrEqualToBigIntSchemaId + +/** @internal */ +export const LessThanBigIntSchemaId: Schema.LessThanBigIntSchemaId = Symbol.for( + "effect/SchemaId/LessThanBigint" +) as Schema.LessThanBigIntSchemaId + +/** @internal */ +export const LessThanOrEqualToBigIntSchemaId: Schema.LessThanOrEqualToBigIntSchemaId = Symbol.for( + "effect/SchemaId/LessThanOrEqualToBigint" +) as Schema.LessThanOrEqualToBigIntSchemaId + +/** @internal */ +export const BetweenBigintSchemaId: Schema.BetweenBigIntSchemaId = Symbol.for( + "effect/SchemaId/BetweenBigint" +) as Schema.BetweenBigIntSchemaId + +/** @internal */ +export const MinLengthSchemaId: Schema.MinLengthSchemaId = Symbol.for( + "effect/SchemaId/MinLength" +) as Schema.MinLengthSchemaId + +/** @internal */ +export const MaxLengthSchemaId: Schema.MaxLengthSchemaId = Symbol.for( + "effect/SchemaId/MaxLength" +) as Schema.MaxLengthSchemaId + +/** @internal */ +export const LengthSchemaId: Schema.LengthSchemaId = Symbol.for( + "effect/SchemaId/Length" +) as Schema.LengthSchemaId + +/** @internal */ +export const MinItemsSchemaId: Schema.MinItemsSchemaId = Symbol.for( + "effect/SchemaId/MinItems" +) as Schema.MinItemsSchemaId + +/** @internal */ +export const MaxItemsSchemaId: Schema.MaxItemsSchemaId = Symbol.for( + "effect/SchemaId/MaxItems" +) as Schema.MaxItemsSchemaId + +/** @internal */ +export const ItemsCountSchemaId: Schema.ItemsCountSchemaId = Symbol.for( + "effect/SchemaId/ItemsCount" +) as Schema.ItemsCountSchemaId diff --git a/repos/effect/packages/effect/src/internal/schema/util.ts b/repos/effect/packages/effect/src/internal/schema/util.ts new file mode 100644 index 0000000..d17bdbd --- /dev/null +++ b/repos/effect/packages/effect/src/internal/schema/util.ts @@ -0,0 +1,50 @@ +import type { NonEmptyReadonlyArray } from "../../Array.js" +import * as Inspectable from "../../Inspectable.js" +import type * as ParseResult from "../../ParseResult.js" +import type * as AST from "../../SchemaAST.js" + +/** @internal */ +export const getKeysForIndexSignature = ( + input: { readonly [x: PropertyKey]: unknown }, + parameter: AST.Parameter +): ReadonlyArray | ReadonlyArray => { + switch (parameter._tag) { + case "StringKeyword": + case "TemplateLiteral": + return Object.keys(input) + case "SymbolKeyword": + return Object.getOwnPropertySymbols(input) + case "Refinement": + return getKeysForIndexSignature(input, parameter.from) + } +} + +/** @internal */ +export const memoizeThunk = (f: () => A): () => A => { + let done = false + let a: A + return () => { + if (done) { + return a + } + a = f() + done = true + return a + } +} + +/** @internal */ +export type SingleOrArray = A | ReadonlyArray + +/** @internal */ +export const isNonEmpty = (x: ParseResult.SingleOrNonEmpty): x is NonEmptyReadonlyArray => Array.isArray(x) + +/** @internal */ +export const isSingle = (x: A | ReadonlyArray): x is A => !Array.isArray(x) + +/** @internal */ +export const formatPathKey = (key: PropertyKey): string => `[${Inspectable.formatPropertyKey(key)}]` + +/** @internal */ +export const formatPath = (path: ParseResult.Path): string => + isNonEmpty(path) ? path.map(formatPathKey).join("") : formatPathKey(path) diff --git a/repos/effect/packages/effect/src/internal/scopedCache.ts b/repos/effect/packages/effect/src/internal/scopedCache.ts new file mode 100644 index 0000000..6eddd6c --- /dev/null +++ b/repos/effect/packages/effect/src/internal/scopedCache.ts @@ -0,0 +1,644 @@ +import type * as Cache from "../Cache.js" +import type * as Clock from "../Clock.js" +import * as Context from "../Context.js" +import * as Data from "../Data.js" +import * as Duration from "../Duration.js" +import type * as Effect from "../Effect.js" +import * as Equal from "../Equal.js" +import * as Exit from "../Exit.js" +import { pipe } from "../Function.js" +import * as HashSet from "../HashSet.js" +import * as MutableHashMap from "../MutableHashMap.js" +import * as MutableQueue from "../MutableQueue.js" +import * as MutableRef from "../MutableRef.js" +import * as Option from "../Option.js" +import { pipeArguments } from "../Pipeable.js" +import * as Scope from "../Scope.js" +import type * as ScopedCache from "../ScopedCache.js" +import * as cache_ from "./cache.js" +import * as effect from "./core-effect.js" +import * as core from "./core.js" +import * as fiberRuntime from "./fiberRuntime.js" + +/** + * The `CacheState` represents the mutable state underlying the cache. + * + * @internal + */ +export interface CacheState { + map: MutableHashMap.MutableHashMap> // mutable by design + keys: cache_.KeySet // mutable by design + accesses: MutableQueue.MutableQueue> // mutable by design + updating: MutableRef.MutableRef // mutable by design + hits: number // mutable by design + misses: number // mutable by design +} + +/** @internal */ +export const makeCacheState = ( + map: MutableHashMap.MutableHashMap>, + keys: cache_.KeySet, + accesses: MutableQueue.MutableQueue>, + updating: MutableRef.MutableRef, + hits: number, + misses: number +): CacheState => ({ + map, + keys, + accesses, + updating, + hits, + misses +}) + +/** + * Constructs an initial cache state. + * + * @internal + */ +export const initialCacheState = (): CacheState => + makeCacheState( + MutableHashMap.empty(), + cache_.makeKeySet(), + MutableQueue.unbounded(), + MutableRef.make(false), + 0, + 0 + ) + +/** + * A `MapValue` represents a value in the cache. A value may either be + * `Pending` with a `Promise` that will contain the result of computing the + * lookup function, when it is available, or `Complete` with an `Exit` value + * that contains the result of computing the lookup function. + * + * @internal + */ +export type MapValue = + | Complete + | Pending + | Refreshing + +/** @internal */ +export interface Complete { + readonly _tag: "Complete" + readonly key: cache_.MapKey + readonly exit: Exit.Exit + readonly ownerCount: MutableRef.MutableRef + readonly entryStats: Cache.EntryStats + readonly timeToLive: number +} + +/** @internal */ +export interface Pending { + readonly _tag: "Pending" + readonly key: cache_.MapKey + readonly scoped: Effect.Effect> +} + +/** @internal */ +export interface Refreshing { + readonly _tag: "Refreshing" + readonly scoped: Effect.Effect> + readonly complete: Complete +} + +/** @internal */ +export const complete = ( + key: cache_.MapKey, + exit: Exit.Exit, + ownerCount: MutableRef.MutableRef, + entryStats: Cache.EntryStats, + timeToLive: number +): Complete => + Data.struct({ + _tag: "Complete", + key, + exit, + ownerCount, + entryStats, + timeToLive + }) + +/** @internal */ +export const pending = ( + key: cache_.MapKey, + scoped: Effect.Effect> +): Pending => + Data.struct({ + _tag: "Pending", + key, + scoped + }) + +/** @internal */ +export const refreshing = ( + scoped: Effect.Effect>, + complete: Complete +): Refreshing => + Data.struct({ + _tag: "Refreshing", + scoped, + complete + }) + +/** @internal */ +export const toScoped = ( + self: Complete +): Effect.Effect => + Exit.matchEffect(self.exit, { + onFailure: (cause) => core.failCause(cause), + onSuccess: ([value]) => + fiberRuntime.acquireRelease( + core.as(core.sync(() => MutableRef.incrementAndGet(self.ownerCount)), value), + () => releaseOwner(self) + ) + }) + +/** @internal */ +export const releaseOwner = ( + self: Complete +): Effect.Effect => + Exit.matchEffect(self.exit, { + onFailure: () => core.void, + onSuccess: ([, finalizer]) => + core.flatMap( + core.sync(() => MutableRef.decrementAndGet(self.ownerCount)), + (numOwner) => effect.when(finalizer(Exit.void), () => numOwner === 0) + ) + }) + +/** @internal */ +const ScopedCacheSymbolKey = "effect/ScopedCache" + +/** @internal */ +export const ScopedCacheTypeId: ScopedCache.ScopedCacheTypeId = Symbol.for( + ScopedCacheSymbolKey +) as ScopedCache.ScopedCacheTypeId + +const scopedCacheVariance = { + /* c8 ignore next */ + _Key: (_: unknown) => _, + /* c8 ignore next */ + _Error: (_: never) => _, + /* c8 ignore next */ + _Value: (_: never) => _ +} + +class ScopedCacheImpl + implements ScopedCache.ScopedCache +{ + readonly [ScopedCacheTypeId] = scopedCacheVariance + readonly cacheState: CacheState + constructor( + readonly capacity: number, + readonly scopedLookup: ScopedCache.Lookup, + readonly clock: Clock.Clock, + readonly timeToLive: (exit: Exit.Exit) => Duration.Duration, + readonly context: Context.Context + ) { + this.cacheState = initialCacheState() + } + + pipe() { + return pipeArguments(this, arguments) + } + + get cacheStats(): Effect.Effect { + return core.sync(() => + cache_.makeCacheStats({ + hits: this.cacheState.hits, + misses: this.cacheState.misses, + size: MutableHashMap.size(this.cacheState.map) + }) + ) + } + + getOption(key: Key): Effect.Effect, Error, Scope.Scope> { + return core.suspend(() => + Option.match(MutableHashMap.get(this.cacheState.map, key), { + onNone: () => effect.succeedNone, + onSome: (value) => core.flatten(this.resolveMapValue(value)) + }) + ) + } + + getOptionComplete(key: Key): Effect.Effect, never, Scope.Scope> { + return core.suspend(() => + Option.match(MutableHashMap.get(this.cacheState.map, key), { + onNone: () => effect.succeedNone, + onSome: (value) => + core.flatten(this.resolveMapValue(value, true)) as Effect.Effect, never, Scope.Scope> + }) + ) + } + + contains(key: Key): Effect.Effect { + return core.sync(() => MutableHashMap.has(this.cacheState.map, key)) + } + + entryStats(key: Key): Effect.Effect> { + return core.sync(() => { + const value = Option.getOrUndefined(MutableHashMap.get(this.cacheState.map, key)) + if (value === undefined) { + return Option.none() + } + switch (value._tag) { + case "Complete": { + return Option.some(cache_.makeEntryStats(value.entryStats.loadedMillis)) + } + case "Pending": { + return Option.none() + } + case "Refreshing": { + return Option.some(cache_.makeEntryStats(value.complete.entryStats.loadedMillis)) + } + } + }) + } + + get(key: Key): Effect.Effect { + return pipe( + this.lookupValueOf(key), + effect.memoize, + core.flatMap((lookupValue) => + core.suspend(() => { + let k: cache_.MapKey | undefined = undefined + let value = Option.getOrUndefined(MutableHashMap.get(this.cacheState.map, key)) + if (value === undefined) { + k = cache_.makeMapKey(key) + if (MutableHashMap.has(this.cacheState.map, key)) { + value = Option.getOrUndefined(MutableHashMap.get(this.cacheState.map, key)) + } else { + MutableHashMap.set(this.cacheState.map, key, pending(k, lookupValue)) + } + } + if (value === undefined) { + this.trackMiss() + return core.zipRight( + this.ensureMapSizeNotExceeded(k!), + lookupValue + ) + } + + return core.map( + this.resolveMapValue(value), + core.flatMap(Option.match({ + onNone: () => { + const val = value as Complete + const current = Option.getOrUndefined(MutableHashMap.get(this.cacheState.map, key)) + if (Equal.equals(current, value)) { + MutableHashMap.remove(this.cacheState.map, key) + } + return pipe( + this.ensureMapSizeNotExceeded(val.key), + core.zipRight(releaseOwner(val)), + core.zipRight(this.get(key)) + ) + }, + onSome: core.succeed + })) + ) + }) + ), + core.flatten + ) + } + + invalidate(key: Key): Effect.Effect { + return core.suspend(() => { + if (MutableHashMap.has(this.cacheState.map, key)) { + const mapValue = Option.getOrUndefined(MutableHashMap.get(this.cacheState.map, key))! + MutableHashMap.remove(this.cacheState.map, key) + switch (mapValue._tag) { + case "Complete": { + return releaseOwner(mapValue) + } + case "Pending": { + return core.void + } + case "Refreshing": { + return releaseOwner(mapValue.complete) + } + } + } + return core.void + }) + } + + get invalidateAll(): Effect.Effect { + return fiberRuntime.forEachConcurrentDiscard( + HashSet.fromIterable(Array.from(this.cacheState.map).map(([key]) => key)), + (key) => this.invalidate(key), + false, + false + ) + } + + refresh(key: Key): Effect.Effect { + return pipe( + this.lookupValueOf(key), + effect.memoize, + core.flatMap((scoped) => { + let value = Option.getOrUndefined(MutableHashMap.get(this.cacheState.map, key)) + let newKey: cache_.MapKey | undefined = undefined + if (value === undefined) { + newKey = cache_.makeMapKey(key) + if (MutableHashMap.has(this.cacheState.map, key)) { + value = Option.getOrUndefined(MutableHashMap.get(this.cacheState.map, key)) + } else { + MutableHashMap.set(this.cacheState.map, key, pending(newKey, scoped)) + } + } + let finalScoped: Effect.Effect> + if (value === undefined) { + finalScoped = core.zipRight( + this.ensureMapSizeNotExceeded(newKey!), + scoped + ) + } else { + switch (value._tag) { + case "Complete": { + if (this.hasExpired(value.timeToLive)) { + finalScoped = core.succeed(this.get(key)) + } else { + const current = Option.getOrUndefined(MutableHashMap.get(this.cacheState.map, key)) + if (Equal.equals(current, value)) { + const mapValue = refreshing(scoped, value) + MutableHashMap.set(this.cacheState.map, key, mapValue) + finalScoped = scoped + } else { + finalScoped = core.succeed(this.get(key)) + } + } + break + } + case "Pending": { + finalScoped = value.scoped + break + } + case "Refreshing": { + finalScoped = value.scoped + break + } + } + } + return core.flatMap(finalScoped, (s) => fiberRuntime.scopedEffect(core.asVoid(s))) + }) + ) + } + + get size(): Effect.Effect { + return core.sync(() => MutableHashMap.size(this.cacheState.map)) + } + + resolveMapValue( + value: MapValue, + ignorePending = false + ): Effect.Effect, Error, Scope.Scope>> { + switch (value._tag) { + case "Complete": { + this.trackHit() + if (this.hasExpired(value.timeToLive)) { + return core.succeed(effect.succeedNone) + } + return core.as( + this.ensureMapSizeNotExceeded(value.key), + effect.asSome(toScoped(value)) + ) + } + case "Pending": { + this.trackHit() + + if (ignorePending) { + return core.succeed(effect.succeedNone) + } + + return core.zipRight( + this.ensureMapSizeNotExceeded(value.key), + core.map(value.scoped, effect.asSome) + ) + } + case "Refreshing": { + this.trackHit() + if (this.hasExpired(value.complete.timeToLive)) { + if (ignorePending) { + return core.succeed(effect.succeedNone) + } + return core.zipRight( + this.ensureMapSizeNotExceeded(value.complete.key), + core.map(value.scoped, effect.asSome) + ) + } + return core.as( + this.ensureMapSizeNotExceeded(value.complete.key), + effect.asSome(toScoped(value.complete)) + ) + } + } + } + + lookupValueOf(key: Key): Effect.Effect> { + return pipe( + core.onInterrupt( + core.flatMap(Scope.make(), (scope) => + pipe( + this.scopedLookup(key), + core.provideContext(pipe(this.context, Context.add(Scope.Scope, scope))), + core.exit, + core.map((exit) => [exit, ((exit) => Scope.close(scope, exit)) as Scope.Scope.Finalizer] as const) + )), + () => core.sync(() => MutableHashMap.remove(this.cacheState.map, key)) + ), + core.flatMap(([exit, release]) => { + const now = this.clock.unsafeCurrentTimeMillis() + const expiredAt = now + Duration.toMillis(this.timeToLive(exit)) + switch (exit._tag) { + case "Success": { + const exitWithFinalizer: Exit.Exit<[Value, Scope.Scope.Finalizer]> = Exit.succeed([ + exit.value, + release + ]) + const completedResult = complete( + cache_.makeMapKey(key), + exitWithFinalizer, + MutableRef.make(1), + cache_.makeEntryStats(now), + expiredAt + ) + let previousValue: MapValue | undefined = undefined + if (MutableHashMap.has(this.cacheState.map, key)) { + previousValue = Option.getOrUndefined(MutableHashMap.get(this.cacheState.map, key)) + } + MutableHashMap.set(this.cacheState.map, key, completedResult) + return core.sync(() => + core.flatten( + core.as( + this.cleanMapValue(previousValue), + toScoped(completedResult) + ) + ) + ) + } + case "Failure": { + const completedResult = complete( + cache_.makeMapKey(key), + exit as Exit.Exit, + MutableRef.make(0), + cache_.makeEntryStats(now), + expiredAt + ) + let previousValue: MapValue | undefined = undefined + if (MutableHashMap.has(this.cacheState.map, key)) { + previousValue = Option.getOrUndefined(MutableHashMap.get(this.cacheState.map, key)) + } + MutableHashMap.set(this.cacheState.map, key, completedResult) + return core.zipRight( + release(exit), + core.sync(() => + core.flatten( + core.as( + this.cleanMapValue(previousValue), + toScoped(completedResult) + ) + ) + ) + ) + } + } + }), + effect.memoize, + core.flatten + ) + } + + hasExpired(timeToLive: number): boolean { + return this.clock.unsafeCurrentTimeMillis() > timeToLive + } + + trackHit(): void { + this.cacheState.hits = this.cacheState.hits + 1 + } + + trackMiss(): void { + this.cacheState.misses = this.cacheState.misses + 1 + } + + trackAccess(key: cache_.MapKey): Array> { + const cleanedKeys: Array> = [] + MutableQueue.offer(this.cacheState.accesses, key) + if (MutableRef.compareAndSet(this.cacheState.updating, false, true)) { + let loop = true + while (loop) { + const key = MutableQueue.poll(this.cacheState.accesses, MutableQueue.EmptyMutableQueue) + if (key === MutableQueue.EmptyMutableQueue) { + loop = false + } else { + this.cacheState.keys.add(key) + } + } + let size = MutableHashMap.size(this.cacheState.map) + loop = size > this.capacity + while (loop) { + const key = this.cacheState.keys.remove() + if (key === undefined) { + loop = false + } else { + if (MutableHashMap.has(this.cacheState.map, key.current)) { + const removed = Option.getOrUndefined(MutableHashMap.get(this.cacheState.map, key.current))! + MutableHashMap.remove(this.cacheState.map, key.current) + size = size - 1 + cleanedKeys.push(removed) + loop = size > this.capacity + } + } + } + MutableRef.set(this.cacheState.updating, false) + } + return cleanedKeys + } + + cleanMapValue(mapValue: MapValue | undefined): Effect.Effect { + if (mapValue === undefined) { + return core.void + } + switch (mapValue._tag) { + case "Complete": { + return releaseOwner(mapValue) + } + case "Pending": { + return core.void + } + case "Refreshing": { + return releaseOwner(mapValue.complete) + } + } + } + + ensureMapSizeNotExceeded(key: cache_.MapKey): Effect.Effect { + return fiberRuntime.forEachConcurrentDiscard( + this.trackAccess(key), + (cleanedMapValue) => this.cleanMapValue(cleanedMapValue), + false, + false + ) + } +} + +/** @internal */ +export const make = ( + options: { + readonly lookup: ScopedCache.Lookup + readonly capacity: number + readonly timeToLive: Duration.DurationInput + } +): Effect.Effect, never, Environment | Scope.Scope> => { + const timeToLive = Duration.decode(options.timeToLive) + return makeWith({ + capacity: options.capacity, + lookup: options.lookup, + timeToLive: () => timeToLive + }) +} + +/** @internal */ +export const makeWith = ( + options: { + readonly capacity: number + readonly lookup: ScopedCache.Lookup + readonly timeToLive: (exit: Exit.Exit) => Duration.DurationInput + } +): Effect.Effect, never, Environment | Scope.Scope> => + core.flatMap( + effect.clock, + (clock) => + buildWith( + options.capacity, + options.lookup, + clock, + (exit) => Duration.decode(options.timeToLive(exit)) + ) + ) + +const buildWith = ( + capacity: number, + scopedLookup: ScopedCache.Lookup, + clock: Clock.Clock, + timeToLive: (exit: Exit.Exit) => Duration.Duration +): Effect.Effect, never, Environment | Scope.Scope> => + fiberRuntime.acquireRelease( + core.flatMap( + core.context(), + (context) => + core.sync(() => + new ScopedCacheImpl( + capacity, + scopedLookup, + clock, + timeToLive, + context + ) + ) + ), + (cache) => cache.invalidateAll + ) diff --git a/repos/effect/packages/effect/src/internal/scopedRef.ts b/repos/effect/packages/effect/src/internal/scopedRef.ts new file mode 100644 index 0000000..dca8d7d --- /dev/null +++ b/repos/effect/packages/effect/src/internal/scopedRef.ts @@ -0,0 +1,118 @@ +import * as Context from "../Context.js" +import type * as Effect from "../Effect.js" +import type { LazyArg } from "../Function.js" +import { dual, pipe } from "../Function.js" +import type * as Scope from "../Scope.js" +import type * as ScopedRef from "../ScopedRef.js" +import * as core from "./core.js" +import * as circular from "./effect/circular.js" +import * as effectable from "./effectable.js" +import * as fiberRuntime from "./fiberRuntime.js" +import * as ref from "./ref.js" +import * as synchronized from "./synchronizedRef.js" + +/** @internal */ +const ScopedRefSymbolKey = "effect/ScopedRef" + +/** @internal */ +export const ScopedRefTypeId: ScopedRef.ScopedRefTypeId = Symbol.for( + ScopedRefSymbolKey +) as ScopedRef.ScopedRefTypeId + +/** @internal */ +const scopedRefVariance = { + /* c8 ignore next */ + _A: (_: any) => _ +} + +/** @internal */ +const proto: ThisType> = { + ...effectable.CommitPrototype, + commit() { + return get(this) + }, + [ScopedRefTypeId]: scopedRefVariance +} + +/** @internal */ +const close = (self: ScopedRef.ScopedRef): Effect.Effect => + core.flatMap(ref.get(self.ref), (tuple) => tuple[0].close(core.exitVoid)) + +/** @internal */ +export const fromAcquire = ( + acquire: Effect.Effect +): Effect.Effect, E, R | Scope.Scope> => + core.uninterruptible( + fiberRuntime.scopeMake().pipe(core.flatMap((newScope) => + acquire.pipe( + core.mapInputContext(Context.add(fiberRuntime.scopeTag, newScope)), + core.onError((cause) => newScope.close(core.exitFail(cause))), + core.flatMap((value) => + circular.makeSynchronized([newScope, value] as const).pipe( + core.flatMap((ref) => { + const scopedRef = Object.create(proto) + scopedRef.ref = ref + return pipe( + fiberRuntime.addFinalizer(() => close(scopedRef)), + core.as(scopedRef) + ) + }) + ) + ) + ) + )) + ) + +/** @internal */ +export const get = (self: ScopedRef.ScopedRef): Effect.Effect => + core.map(ref.get(self.ref), (tuple) => tuple[1]) + +/** @internal */ +export const make = (evaluate: LazyArg): Effect.Effect, never, Scope.Scope> => + fromAcquire(core.sync(evaluate)) + +/** @internal */ +export const set = dual< + ( + acquire: Effect.Effect + ) => (self: ScopedRef.ScopedRef) => Effect.Effect>, + ( + self: ScopedRef.ScopedRef, + acquire: Effect.Effect + ) => Effect.Effect> +>(2, ( + self: ScopedRef.ScopedRef, + acquire: Effect.Effect +) => + core.flatten( + synchronized.modifyEffect(self.ref, ([oldScope, value]) => + core.uninterruptible( + core.scopeClose(oldScope, core.exitVoid).pipe( + core.zipRight(fiberRuntime.scopeMake()), + core.flatMap((newScope) => + core.exit(fiberRuntime.scopeExtend(acquire, newScope)).pipe( + core.flatMap((exit) => + core.exitMatch(exit, { + onFailure: (cause) => + core.scopeClose(newScope, core.exitVoid).pipe( + core.as( + [ + core.failCause(cause) as Effect.Effect, + [oldScope, value] as const + ] as const + ) + ), + onSuccess: (value) => + core.succeed( + [ + core.void as Effect.Effect, + [newScope, value] as const + ] as const + ) + }) + ) + ) + ) + ) + )) + )) diff --git a/repos/effect/packages/effect/src/internal/secret.ts b/repos/effect/packages/effect/src/internal/secret.ts new file mode 100644 index 0000000..2d12b35 --- /dev/null +++ b/repos/effect/packages/effect/src/internal/secret.ts @@ -0,0 +1,89 @@ +import * as Arr from "../Array.js" +import { hasProperty } from "../Predicate.js" +import type * as Secret from "../Secret.js" +import * as redacted_ from "./redacted.js" + +/** + * @internal + * @deprecated + */ +const SecretSymbolKey = "effect/Secret" + +/** + * @internal + * @deprecated + */ +export const SecretTypeId: Secret.SecretTypeId = Symbol.for( + SecretSymbolKey +) as Secret.SecretTypeId + +/** + * @internal + * @deprecated + */ +export const isSecret = (u: unknown): u is Secret.Secret => hasProperty(u, SecretTypeId) + +const SecretProto = { + ...redacted_.proto, + [SecretTypeId]: SecretTypeId +} + +/** + * @internal + * @deprecated + */ +export const make = (bytes: Array): Secret.Secret => { + const secret = Object.create(SecretProto) + Object.defineProperty(secret, "toString", { + enumerable: false, + value() { + return "Secret()" + } + }) + Object.defineProperty(secret, "toJSON", { + enumerable: false, + value() { + return "" + } + }) + Object.defineProperty(secret, "raw", { + enumerable: false, + value: bytes + }) + redacted_.redactedRegistry.set(secret, bytes.map((byte) => String.fromCharCode(byte)).join("")) + return secret +} + +/** + * @internal + * @deprecated + */ +export const fromIterable = (iterable: Iterable): Secret.Secret => + make(Arr.fromIterable(iterable).map((char) => char.charCodeAt(0))) + +/** + * @internal + * @deprecated + */ +export const fromString = (text: string): Secret.Secret => { + return make(text.split("").map((char) => char.charCodeAt(0))) +} + +/** + * @internal + * @deprecated + */ +export const value = (self: Secret.Secret): string => { + return self.raw.map((byte) => String.fromCharCode(byte)).join("") +} + +/** + * @internal + * @deprecated + */ +export const unsafeWipe = (self: Secret.Secret): void => { + for (let i = 0; i < self.raw.length; i++) { + self.raw[i] = 0 + } + redacted_.redactedRegistry.delete(self) +} diff --git a/repos/effect/packages/effect/src/internal/singleShotGen.ts b/repos/effect/packages/effect/src/internal/singleShotGen.ts new file mode 100644 index 0000000..1b93344 --- /dev/null +++ b/repos/effect/packages/effect/src/internal/singleShotGen.ts @@ -0,0 +1,35 @@ +/** @internal */ +export class SingleShotGen implements Generator { + called = false + + constructor(readonly self: T) { + } + + next(a: A): IteratorResult { + return this.called ? + ({ + value: a, + done: true + }) : + (this.called = true, + ({ + value: this.self, + done: false + })) + } + + return(a: A): IteratorResult { + return ({ + value: a, + done: true + }) + } + + throw(e: unknown): IteratorResult { + throw e + } + + [Symbol.iterator](): Generator { + return new SingleShotGen(this.self) + } +} diff --git a/repos/effect/packages/effect/src/internal/sink.ts b/repos/effect/packages/effect/src/internal/sink.ts new file mode 100644 index 0000000..bb24b36 --- /dev/null +++ b/repos/effect/packages/effect/src/internal/sink.ts @@ -0,0 +1,2120 @@ +import * as Arr from "../Array.js" +import * as Cause from "../Cause.js" +import type * as Channel from "../Channel.js" +import * as Chunk from "../Chunk.js" +import * as Clock from "../Clock.js" +import type * as Context from "../Context.js" +import * as Duration from "../Duration.js" +import * as Effect from "../Effect.js" +import * as Either from "../Either.js" +import * as Exit from "../Exit.js" +import { constTrue, dual, identity, pipe } from "../Function.js" +import type { LazyArg } from "../Function.js" +import * as HashMap from "../HashMap.js" +import * as HashSet from "../HashSet.js" +import type * as MergeDecision from "../MergeDecision.js" +import * as Option from "../Option.js" +import { pipeArguments } from "../Pipeable.js" +import { hasProperty, type Predicate, type Refinement } from "../Predicate.js" +import * as PubSub from "../PubSub.js" +import * as Queue from "../Queue.js" +import * as Ref from "../Ref.js" +import * as Scope from "../Scope.js" +import type * as Sink from "../Sink.js" +import type * as Types from "../Types.js" +import * as channel from "./channel.js" +import * as mergeDecision from "./channel/mergeDecision.js" +import * as core from "./core-stream.js" + +/** @internal */ +export const SinkTypeId: Sink.SinkTypeId = Symbol.for("effect/Sink") as Sink.SinkTypeId + +const sinkVariance = { + /* c8 ignore next */ + _A: (_: never) => _, + /* c8 ignore next */ + _In: (_: unknown) => _, + /* c8 ignore next */ + _L: (_: never) => _, + /* c8 ignore next */ + _E: (_: never) => _, + /* c8 ignore next */ + _R: (_: never) => _ +} + +/** @internal */ +export class SinkImpl + implements Sink.Sink +{ + readonly [SinkTypeId] = sinkVariance + constructor( + readonly channel: Channel.Channel, Chunk.Chunk, E, never, A, unknown, R> + ) { + } + pipe() { + return pipeArguments(this, arguments) + } +} + +/** @internal */ +export const isSink = (u: unknown): u is Sink.Sink => + hasProperty(u, SinkTypeId) + +/** @internal */ +export const suspend = (evaluate: LazyArg>): Sink.Sink => + new SinkImpl(core.suspend(() => toChannel(evaluate()))) + +/** @internal */ +export const as = dual< + (a: A2) => (self: Sink.Sink) => Sink.Sink, + (self: Sink.Sink, a: A2) => Sink.Sink +>( + 2, + (self, a) => pipe(self, map(() => a)) +) + +/** @internal */ +export const collectAll = (): Sink.Sink, In> => new SinkImpl(collectAllLoop(Chunk.empty())) + +/** @internal */ +const collectAllLoop = ( + acc: Chunk.Chunk +): Channel.Channel, never, never, Chunk.Chunk, unknown> => + core.readWithCause({ + onInput: (chunk: Chunk.Chunk) => collectAllLoop(pipe(acc, Chunk.appendAll(chunk))), + onFailure: core.failCause, + onDone: () => core.succeed(acc) + }) + +/** @internal */ +export const collectAllN = (n: number): Sink.Sink, In, In> => + suspend(() => fromChannel(collectAllNLoop(n, Chunk.empty()))) + +/** @internal */ +const collectAllNLoop = ( + n: number, + acc: Chunk.Chunk +): Channel.Channel, Chunk.Chunk, never, never, Chunk.Chunk, unknown> => + core.readWithCause({ + onInput: (chunk: Chunk.Chunk) => { + const [collected, leftovers] = Chunk.splitAt(chunk, n) + if (collected.length < n) { + return collectAllNLoop(n - collected.length, Chunk.appendAll(acc, collected)) + } + if (Chunk.isEmpty(leftovers)) { + return core.succeed(Chunk.appendAll(acc, collected)) + } + return core.flatMap(core.write(leftovers), () => core.succeed(Chunk.appendAll(acc, collected))) + }, + onFailure: core.failCause, + onDone: () => core.succeed(acc) + }) + +/** @internal */ +export const collectAllFrom = ( + self: Sink.Sink +): Sink.Sink, In, L, E, R> => + collectAllWhileWith(self, { + initial: Chunk.empty(), + while: constTrue, + body: (chunk, a) => pipe(chunk, Chunk.append(a)) + }) + +/** @internal */ +export const collectAllToMap = ( + key: (input: In) => K, + merge: (x: In, y: In) => In +): Sink.Sink, In> => { + return foldLeftChunks(HashMap.empty(), (map, chunk) => + pipe( + chunk, + Chunk.reduce(map, (map, input) => { + const k: K = key(input) + const v: In = pipe(map, HashMap.has(k)) ? + merge(pipe(map, HashMap.unsafeGet(k)), input) : + input + return pipe(map, HashMap.set(k, v)) + }) + )) +} + +/** @internal */ +export const collectAllToMapN = ( + n: number, + key: (input: In) => K, + merge: (x: In, y: In) => In +): Sink.Sink, In, In> => { + return foldWeighted, In>({ + initial: HashMap.empty(), + maxCost: n, + cost: (acc, input) => pipe(acc, HashMap.has(key(input))) ? 0 : 1, + body: (acc, input) => { + const k: K = key(input) + const v: In = pipe(acc, HashMap.has(k)) ? + merge(pipe(acc, HashMap.unsafeGet(k)), input) : + input + return pipe(acc, HashMap.set(k, v)) + } + }) +} + +/** @internal */ +export const collectAllToSet = (): Sink.Sink, In> => + foldLeftChunks, In>( + HashSet.empty(), + (acc, chunk) => pipe(chunk, Chunk.reduce(acc, (acc, input) => pipe(acc, HashSet.add(input)))) + ) + +/** @internal */ +export const collectAllToSetN = (n: number): Sink.Sink, In, In> => + foldWeighted, In>({ + initial: HashSet.empty(), + maxCost: n, + cost: (acc, input) => HashSet.has(acc, input) ? 0 : 1, + body: (acc, input) => HashSet.add(acc, input) + }) + +/** @internal */ +export const collectAllUntil = (p: Predicate): Sink.Sink, In, In> => { + return pipe( + fold<[Chunk.Chunk, boolean], In>( + [Chunk.empty(), true], + (tuple) => tuple[1], + ([chunk, _], input) => [pipe(chunk, Chunk.append(input)), !p(input)] + ), + map((tuple) => tuple[0]) + ) +} + +/** @internal */ +export const collectAllUntilEffect = (p: (input: In) => Effect.Effect) => { + return pipe( + foldEffect<[Chunk.Chunk, boolean], In, E, R>( + [Chunk.empty(), true], + (tuple) => tuple[1], + ([chunk, _], input) => pipe(p(input), Effect.map((bool) => [pipe(chunk, Chunk.append(input)), !bool])) + ), + map((tuple) => tuple[0]) + ) +} + +/** @internal */ +export const collectAllWhile: { + (refinement: Refinement): Sink.Sink, In, In> + (predicate: Predicate): Sink.Sink, In, In> +} = (predicate: Predicate): Sink.Sink, In, In> => + fromChannel(collectAllWhileReader(predicate, Chunk.empty())) + +/** @internal */ +const collectAllWhileReader = ( + predicate: Predicate, + done: Chunk.Chunk +): Channel.Channel, Chunk.Chunk, never, never, Chunk.Chunk, unknown> => + core.readWith({ + onInput: (input: Chunk.Chunk) => { + const [collected, leftovers] = pipe(Chunk.toReadonlyArray(input), Arr.span(predicate)) + if (leftovers.length === 0) { + return collectAllWhileReader( + predicate, + pipe(done, Chunk.appendAll(Chunk.unsafeFromArray(collected))) + ) + } + return pipe( + core.write(Chunk.unsafeFromArray(leftovers)), + channel.zipRight(core.succeed(pipe(done, Chunk.appendAll(Chunk.unsafeFromArray(collected))))) + ) + }, + onFailure: core.fail, + onDone: () => core.succeed(done) + }) + +/** @internal */ +export const collectAllWhileEffect = ( + predicate: (input: In) => Effect.Effect +): Sink.Sink, In, In, E, R> => fromChannel(collectAllWhileEffectReader(predicate, Chunk.empty())) + +/** @internal */ +const collectAllWhileEffectReader = ( + predicate: (input: In) => Effect.Effect, + done: Chunk.Chunk +): Channel.Channel, Chunk.Chunk, E, never, Chunk.Chunk, unknown, R> => + core.readWith({ + onInput: (input: Chunk.Chunk) => + pipe( + core.fromEffect(pipe(input, Effect.takeWhile(predicate), Effect.map(Chunk.unsafeFromArray))), + core.flatMap((collected) => { + const leftovers = pipe(input, Chunk.drop(collected.length)) + if (Chunk.isEmpty(leftovers)) { + return collectAllWhileEffectReader(predicate, pipe(done, Chunk.appendAll(collected))) + } + return pipe(core.write(leftovers), channel.zipRight(core.succeed(pipe(done, Chunk.appendAll(collected))))) + }) + ), + onFailure: core.fail, + onDone: () => core.succeed(done) + }) + +/** @internal */ +export const collectAllWhileWith: { + ( + options: { + readonly initial: S + readonly while: Predicate + readonly body: (s: S, a: A) => S + } + ): (self: Sink.Sink) => Sink.Sink + ( + self: Sink.Sink, + options: { + readonly initial: S + readonly while: Predicate + readonly body: (s: S, a: A) => S + } + ): Sink.Sink +} = dual( + 2, + ( + self: Sink.Sink, + options: { + readonly initial: S + readonly while: Predicate + readonly body: (s: S, a: A) => S + } + ): Sink.Sink => { + const refs = pipe( + Ref.make(Chunk.empty()), + Effect.zip(Ref.make(false)) + ) + const newChannel = pipe( + core.fromEffect(refs), + core.flatMap(([leftoversRef, upstreamDoneRef]) => { + const upstreamMarker: Channel.Channel, Chunk.Chunk, never, never, unknown, unknown> = core + .readWith({ + onInput: (input) => pipe(core.write(input), core.flatMap(() => upstreamMarker)), + onFailure: core.fail, + onDone: (done) => pipe(core.fromEffect(Ref.set(upstreamDoneRef, true)), channel.as(done)) + }) + return pipe( + upstreamMarker, + core.pipeTo(channel.bufferChunk(leftoversRef)), + core.pipeTo( + collectAllWhileWithLoop(self, leftoversRef, upstreamDoneRef, options.initial, options.while, options.body) + ) + ) + }) + ) + return new SinkImpl(newChannel) + } +) + +const collectAllWhileWithLoop = ( + self: Sink.Sink, + leftoversRef: Ref.Ref>, + upstreamDoneRef: Ref.Ref, + currentResult: S, + p: Predicate, + f: (s: S, z: Z) => S +): Channel.Channel, Chunk.Chunk, E, never, S, unknown, R> => { + return pipe( + toChannel(self), + channel.doneCollect, + channel.foldChannel({ + onFailure: core.fail, + onSuccess: ([leftovers, doneValue]) => + p(doneValue) + ? pipe( + core.fromEffect( + Ref.set(leftoversRef, Chunk.flatten(leftovers as Chunk.Chunk>)) + ), + core.flatMap(() => + pipe( + core.fromEffect(Ref.get(upstreamDoneRef)), + core.flatMap((upstreamDone) => { + const accumulatedResult = f(currentResult, doneValue) + return upstreamDone + ? pipe(core.write(Chunk.flatten(leftovers)), channel.as(accumulatedResult)) + : collectAllWhileWithLoop(self, leftoversRef, upstreamDoneRef, accumulatedResult, p, f) + }) + ) + ) + ) + : pipe(core.write(Chunk.flatten(leftovers)), channel.as(currentResult)) + }) + ) +} + +/** @internal */ +export const collectLeftover = ( + self: Sink.Sink +): Sink.Sink<[A, Chunk.Chunk], In, never, E, R> => + new SinkImpl(pipe(core.collectElements(toChannel(self)), channel.map(([chunks, z]) => [z, Chunk.flatten(chunks)]))) + +/** @internal */ +export const mapInput = dual< + (f: (input: In0) => In) => (self: Sink.Sink) => Sink.Sink, + (self: Sink.Sink, f: (input: In0) => In) => Sink.Sink +>( + 2, + (self: Sink.Sink, f: (input: In0) => In): Sink.Sink => + pipe(self, mapInputChunks(Chunk.map(f))) +) + +/** @internal */ +export const mapInputEffect = dual< + ( + f: (input: In0) => Effect.Effect + ) => (self: Sink.Sink) => Sink.Sink, + ( + self: Sink.Sink, + f: (input: In0) => Effect.Effect + ) => Sink.Sink +>( + 2, + ( + self: Sink.Sink, + f: (input: In0) => Effect.Effect + ): Sink.Sink => + mapInputChunksEffect( + self, + (chunk) => + Effect.map( + Effect.forEach(chunk, (v) => f(v)), + Chunk.unsafeFromArray + ) + ) +) + +/** @internal */ +export const mapInputChunks = dual< + ( + f: (chunk: Chunk.Chunk) => Chunk.Chunk + ) => (self: Sink.Sink) => Sink.Sink, + ( + self: Sink.Sink, + f: (chunk: Chunk.Chunk) => Chunk.Chunk + ) => Sink.Sink +>( + 2, + ( + self: Sink.Sink, + f: (chunk: Chunk.Chunk) => Chunk.Chunk + ): Sink.Sink => { + const loop: Channel.Channel, Chunk.Chunk, never, never, unknown, unknown, R> = core.readWith({ + onInput: (chunk) => pipe(core.write(f(chunk)), core.flatMap(() => loop)), + onFailure: core.fail, + onDone: core.succeed + }) + return new SinkImpl(pipe(loop, core.pipeTo(toChannel(self)))) + } +) + +/** @internal */ +export const mapInputChunksEffect = dual< + ( + f: (chunk: Chunk.Chunk) => Effect.Effect, E2, R2> + ) => (self: Sink.Sink) => Sink.Sink, + ( + self: Sink.Sink, + f: (chunk: Chunk.Chunk) => Effect.Effect, E2, R2> + ) => Sink.Sink +>( + 2, + ( + self: Sink.Sink, + f: (chunk: Chunk.Chunk) => Effect.Effect, E2, R2> + ): Sink.Sink => { + const loop: Channel.Channel, Chunk.Chunk, E2, never, unknown, unknown, R | R2> = core + .readWith({ + onInput: (chunk) => pipe(core.fromEffect(f(chunk)), core.flatMap(core.write), core.flatMap(() => loop)), + onFailure: core.fail, + onDone: core.succeed + }) + return new SinkImpl(pipe(loop, channel.pipeToOrFail(toChannel(self)))) + } +) + +/** @internal */ +export const die = (defect: unknown): Sink.Sink => failCause(Cause.die(defect)) + +/** @internal */ +export const dieMessage = (message: string): Sink.Sink => + failCause(Cause.die(new Cause.RuntimeException(message))) + +/** @internal */ +export const dieSync = (evaluate: LazyArg): Sink.Sink => + failCauseSync(() => Cause.die(evaluate())) + +/** @internal */ +export const dimap = dual< + ( + options: { + readonly onInput: (input: In0) => In + readonly onDone: (a: A) => A2 + } + ) => (self: Sink.Sink) => Sink.Sink, + ( + self: Sink.Sink, + options: { + readonly onInput: (input: In0) => In + readonly onDone: (a: A) => A2 + } + ) => Sink.Sink +>( + 2, + ( + self: Sink.Sink, + options: { + readonly onInput: (input: In0) => In + readonly onDone: (a: A) => A2 + } + ): Sink.Sink => map(mapInput(self, options.onInput), options.onDone) +) + +/** @internal */ +export const dimapEffect = dual< + ( + options: { + readonly onInput: (input: In0) => Effect.Effect + readonly onDone: (a: A) => Effect.Effect + } + ) => (self: Sink.Sink) => Sink.Sink, + ( + self: Sink.Sink, + options: { + readonly onInput: (input: In0) => Effect.Effect + readonly onDone: (a: A) => Effect.Effect + } + ) => Sink.Sink +>( + 2, + (self, options) => + mapEffect( + mapInputEffect(self, options.onInput), + options.onDone + ) +) + +/** @internal */ +export const dimapChunks = dual< + ( + options: { + readonly onInput: (chunk: Chunk.Chunk) => Chunk.Chunk + readonly onDone: (a: A) => A2 + } + ) => (self: Sink.Sink) => Sink.Sink, + ( + self: Sink.Sink, + options: { + readonly onInput: (chunk: Chunk.Chunk) => Chunk.Chunk + readonly onDone: (a: A) => A2 + } + ) => Sink.Sink +>( + 2, + (self, options) => + map( + mapInputChunks(self, options.onInput), + options.onDone + ) +) + +/** @internal */ +export const dimapChunksEffect = dual< + ( + options: { + readonly onInput: (chunk: Chunk.Chunk) => Effect.Effect, E2, R2> + readonly onDone: (a: A) => Effect.Effect + } + ) => (self: Sink.Sink) => Sink.Sink, + ( + self: Sink.Sink, + options: { + readonly onInput: (chunk: Chunk.Chunk) => Effect.Effect, E2, R2> + readonly onDone: (a: A) => Effect.Effect + } + ) => Sink.Sink +>( + 2, + (self, options) => mapEffect(mapInputChunksEffect(self, options.onInput), options.onDone) +) + +/** @internal */ +export const drain: Sink.Sink = new SinkImpl( + channel.drain(channel.identityChannel()) +) + +/** @internal */ +export const drop = (n: number): Sink.Sink => suspend(() => new SinkImpl(dropLoop(n))) + +/** @internal */ +const dropLoop = ( + n: number +): Channel.Channel, Chunk.Chunk, never, never, unknown, unknown> => + core.readWith({ + onInput: (input: Chunk.Chunk) => { + const dropped = pipe(input, Chunk.drop(n)) + const leftover = Math.max(n - input.length, 0) + const more = Chunk.isEmpty(input) || leftover > 0 + if (more) { + return dropLoop(leftover) + } + return pipe( + core.write(dropped), + channel.zipRight(channel.identityChannel, never, unknown>()) + ) + }, + onFailure: core.fail, + onDone: () => core.void + }) + +/** @internal */ +export const dropUntil = (predicate: Predicate): Sink.Sink => + new SinkImpl( + pipe(toChannel(dropWhile((input: In) => !predicate(input))), channel.pipeToOrFail(toChannel(drop(1)))) + ) + +/** @internal */ +export const dropUntilEffect = ( + predicate: (input: In) => Effect.Effect +): Sink.Sink => suspend(() => new SinkImpl(dropUntilEffectReader(predicate))) + +/** @internal */ +const dropUntilEffectReader = ( + predicate: (input: In) => Effect.Effect +): Channel.Channel, Chunk.Chunk, E, E, unknown, unknown, R> => + core.readWith({ + onInput: (input: Chunk.Chunk) => + pipe( + input, + Effect.dropUntil(predicate), + Effect.map((leftover) => { + const more = leftover.length === 0 + return more ? + dropUntilEffectReader(predicate) : + pipe( + core.write(Chunk.unsafeFromArray(leftover)), + channel.zipRight(channel.identityChannel, E, unknown>()) + ) + }), + channel.unwrap + ), + onFailure: core.fail, + onDone: () => core.void + }) + +/** @internal */ +export const dropWhile = (predicate: Predicate): Sink.Sink => + new SinkImpl(dropWhileReader(predicate)) + +/** @internal */ +const dropWhileReader = ( + predicate: Predicate +): Channel.Channel, Chunk.Chunk, never, never, unknown, unknown> => + core.readWith({ + onInput: (input: Chunk.Chunk) => { + const out = pipe(input, Chunk.dropWhile(predicate)) + if (Chunk.isEmpty(out)) { + return dropWhileReader(predicate) + } + return pipe(core.write(out), channel.zipRight(channel.identityChannel, never, unknown>())) + }, + onFailure: core.fail, + onDone: core.succeedNow + }) + +/** @internal */ +export const dropWhileEffect = ( + predicate: (input: In) => Effect.Effect +): Sink.Sink => suspend(() => new SinkImpl(dropWhileEffectReader(predicate))) + +/** @internal */ +const dropWhileEffectReader = ( + predicate: (input: In) => Effect.Effect +): Channel.Channel, Chunk.Chunk, E, E, unknown, unknown, R> => + core.readWith({ + onInput: (input: Chunk.Chunk) => + pipe( + input, + Effect.dropWhile(predicate), + Effect.map((leftover) => { + const more = leftover.length === 0 + return more ? + dropWhileEffectReader(predicate) : + pipe( + core.write(Chunk.unsafeFromArray(leftover)), + channel.zipRight(channel.identityChannel, E, unknown>()) + ) + }), + channel.unwrap + ), + onFailure: core.fail, + onDone: () => core.void + }) + +/** @internal */ +export const ensuring = dual< + ( + finalizer: Effect.Effect + ) => (self: Sink.Sink) => Sink.Sink, + ( + self: Sink.Sink, + finalizer: Effect.Effect + ) => Sink.Sink +>( + 2, + (self, finalizer) => new SinkImpl(pipe(self, toChannel, channel.ensuring(finalizer))) +) + +/** @internal */ +export const ensuringWith = dual< + ( + finalizer: (exit: Exit.Exit) => Effect.Effect + ) => (self: Sink.Sink) => Sink.Sink, + ( + self: Sink.Sink, + finalizer: (exit: Exit.Exit) => Effect.Effect + ) => Sink.Sink +>( + 2, + (self, finalizer) => new SinkImpl(pipe(self, toChannel, core.ensuringWith(finalizer))) +) + +/** @internal */ +export const context = (): Sink.Sink, unknown, never, never, R> => fromEffect(Effect.context()) + +/** @internal */ +export const contextWith = ( + f: (context: Context.Context) => Z +): Sink.Sink => pipe(context(), map(f)) + +/** @internal */ +export const contextWithEffect = ( + f: (context: Context.Context) => Effect.Effect +): Sink.Sink => pipe(context(), mapEffect(f)) + +/** @internal */ +export const contextWithSink = ( + f: (context: Context.Context) => Sink.Sink +): Sink.Sink => + new SinkImpl(channel.unwrap(Effect.contextWith((context) => toChannel(f(context))))) + +/** @internal */ +export const every = (predicate: Predicate): Sink.Sink => + fold(true, identity, (acc, input) => acc && predicate(input)) + +/** @internal */ +export const fail = (e: E): Sink.Sink => new SinkImpl(core.fail(e)) + +/** @internal */ +export const failSync = (evaluate: LazyArg): Sink.Sink => + new SinkImpl(core.failSync(evaluate)) + +/** @internal */ +export const failCause = (cause: Cause.Cause): Sink.Sink => + new SinkImpl(core.failCause(cause)) + +/** @internal */ +export const failCauseSync = (evaluate: LazyArg>): Sink.Sink => + new SinkImpl(core.failCauseSync(evaluate)) + +/** @internal */ +export const filterInput: { + ( + f: Refinement + ): (self: Sink.Sink) => Sink.Sink + (f: Predicate): (self: Sink.Sink) => Sink.Sink +} = (f: Predicate) => { + return (self: Sink.Sink): Sink.Sink => + pipe(self, mapInputChunks(Chunk.filter(f))) +} + +/** @internal */ +export const filterInputEffect = dual< + ( + f: (input: In1) => Effect.Effect + ) => (self: Sink.Sink) => Sink.Sink, + ( + self: Sink.Sink, + f: (input: In1) => Effect.Effect + ) => Sink.Sink +>( + 2, + (self, f) => + mapInputChunksEffect( + self, + (chunk) => Effect.map(Effect.filter(chunk, f), Chunk.unsafeFromArray) + ) +) + +/** @internal */ +export const findEffect = dual< + ( + f: (a: A) => Effect.Effect + ) => (self: Sink.Sink) => Sink.Sink, In, L, E2 | E, R2 | R>, + ( + self: Sink.Sink, + f: (a: A) => Effect.Effect + ) => Sink.Sink, In, L, E2 | E, R2 | R> +>( + 2, + ( + self: Sink.Sink, + f: (a: A) => Effect.Effect + ): Sink.Sink, In, L, E2 | E, R2 | R> => { + const newChannel = pipe( + core.fromEffect(pipe( + Ref.make(Chunk.empty()), + Effect.zip(Ref.make(false)) + )), + core.flatMap(([leftoversRef, upstreamDoneRef]) => { + const upstreamMarker: Channel.Channel, Chunk.Chunk, never, never, unknown, unknown> = core + .readWith({ + onInput: (input) => pipe(core.write(input), core.flatMap(() => upstreamMarker)), + onFailure: core.fail, + onDone: (done) => pipe(core.fromEffect(Ref.set(upstreamDoneRef, true)), channel.as(done)) + }) + const loop: Channel.Channel, Chunk.Chunk, E | E2, never, Option.Option, unknown, R | R2> = + channel.foldChannel(core.collectElements(toChannel(self)), { + onFailure: core.fail, + onSuccess: ([leftovers, doneValue]) => + pipe( + core.fromEffect(f(doneValue)), + core.flatMap((satisfied) => + pipe( + core.fromEffect(Ref.set(leftoversRef, Chunk.flatten(leftovers))), + channel.zipRight( + pipe( + core.fromEffect(Ref.get(upstreamDoneRef)), + core.flatMap((upstreamDone) => { + if (satisfied) { + return pipe(core.write(Chunk.flatten(leftovers)), channel.as(Option.some(doneValue))) + } + if (upstreamDone) { + return pipe(core.write(Chunk.flatten(leftovers)), channel.as(Option.none())) + } + return loop + }) + ) + ) + ) + ) + ) + }) + return pipe(upstreamMarker, core.pipeTo(channel.bufferChunk(leftoversRef)), core.pipeTo(loop)) + }) + ) + return new SinkImpl(newChannel) + } +) + +/** @internal */ +export const fold = ( + s: S, + contFn: Predicate, + f: (s: S, input: In) => S +): Sink.Sink => suspend(() => new SinkImpl(foldReader(s, contFn, f))) + +/** @internal */ +const foldReader = ( + s: S, + contFn: Predicate, + f: (z: S, input: In) => S +): Channel.Channel, Chunk.Chunk, never, never, S, unknown> => { + if (!contFn(s)) { + return core.succeedNow(s) + } + return core.readWith({ + onInput: (input: Chunk.Chunk) => { + const [nextS, leftovers] = foldChunkSplit(s, input, contFn, f, 0, input.length) + if (Chunk.isNonEmpty(leftovers)) { + return pipe(core.write(leftovers), channel.as(nextS)) + } + return foldReader(nextS, contFn, f) + }, + onFailure: core.fail, + onDone: () => core.succeedNow(s) + }) +} + +/** @internal */ +const foldChunkSplit = ( + s: S, + chunk: Chunk.Chunk, + contFn: Predicate, + f: (z: S, input: In) => S, + index: number, + length: number +): [S, Chunk.Chunk] => { + if (index === length) { + return [s, Chunk.empty()] + } + const s1 = f(s, pipe(chunk, Chunk.unsafeGet(index))) + if (contFn(s1)) { + return foldChunkSplit(s1, chunk, contFn, f, index + 1, length) + } + return [s1, pipe(chunk, Chunk.drop(index + 1))] +} + +/** @internal */ +export const foldSink = dual< + ( + options: { + readonly onFailure: (err: E) => Sink.Sink + readonly onSuccess: (a: A) => Sink.Sink + } + ) => (self: Sink.Sink) => Sink.Sink, + ( + self: Sink.Sink, + options: { + readonly onFailure: (err: E) => Sink.Sink + readonly onSuccess: (a: A) => Sink.Sink + } + ) => Sink.Sink +>( + 2, + ( + self: Sink.Sink, + options: { + readonly onFailure: (err: E) => Sink.Sink + readonly onSuccess: (z: A) => Sink.Sink + } + ): Sink.Sink => { + const newChannel: Channel.Channel< + Chunk.Chunk, + Chunk.Chunk, + E1 | E2, + never, + A1 | A2, + unknown, + R | R1 | R2 + > = pipe( + toChannel(self), + core.collectElements, + channel.foldChannel({ + onFailure: (error) => toChannel(options.onFailure(error)), + onSuccess: ([leftovers, z]) => + core.suspend(() => { + const leftoversRef = { + ref: pipe(leftovers, Chunk.filter(Chunk.isNonEmpty)) as Chunk.Chunk> + } + const refReader = pipe( + core.sync(() => { + const ref = leftoversRef.ref + leftoversRef.ref = Chunk.empty() + return ref + }), + // This cast is safe because of the L1 >: L <: In1 bound. It follows that + // L <: In1 and therefore Chunk[L] can be safely cast to Chunk[In1]. + core.flatMap((chunk) => channel.writeChunk(chunk as Chunk.Chunk>)) + ) + const passthrough = channel.identityChannel, never, unknown>() + const continuationSink = pipe( + refReader, + channel.zipRight(passthrough), + core.pipeTo(toChannel(options.onSuccess(z))) + ) + return core.flatMap( + core.collectElements(continuationSink), + ([newLeftovers, z1]) => + pipe( + core.succeed(leftoversRef.ref), + core.flatMap(channel.writeChunk), + channel.zipRight(channel.writeChunk(newLeftovers)), + channel.as(z1) + ) + ) + }) + }) + ) + return new SinkImpl(newChannel) + } +) + +/** @internal */ +export const foldChunks = ( + s: S, + contFn: Predicate, + f: (s: S, chunk: Chunk.Chunk) => S +): Sink.Sink => suspend(() => new SinkImpl(foldChunksReader(s, contFn, f))) + +/** @internal */ +const foldChunksReader = ( + s: S, + contFn: Predicate, + f: (s: S, chunk: Chunk.Chunk) => S +): Channel.Channel, never, never, S, unknown> => { + if (!contFn(s)) { + return core.succeedNow(s) + } + return core.readWith({ + onInput: (input: Chunk.Chunk) => foldChunksReader(f(s, input), contFn, f), + onFailure: core.fail, + onDone: () => core.succeedNow(s) + }) +} + +/** @internal */ +export const foldChunksEffect = ( + s: S, + contFn: Predicate, + f: (s: S, chunk: Chunk.Chunk) => Effect.Effect +): Sink.Sink => suspend(() => new SinkImpl(foldChunksEffectReader(s, contFn, f))) + +/** @internal */ +const foldChunksEffectReader = ( + s: S, + contFn: Predicate, + f: (s: S, chunk: Chunk.Chunk) => Effect.Effect +): Channel.Channel, E, E, S, unknown, R> => { + if (!contFn(s)) { + return core.succeedNow(s) + } + return core.readWith({ + onInput: (input: Chunk.Chunk) => + pipe( + core.fromEffect(f(s, input)), + core.flatMap((s) => foldChunksEffectReader(s, contFn, f)) + ), + onFailure: core.fail, + onDone: () => core.succeedNow(s) + }) +} + +/** @internal */ +export const foldEffect = ( + s: S, + contFn: Predicate, + f: (s: S, input: In) => Effect.Effect +): Sink.Sink => suspend(() => new SinkImpl(foldEffectReader(s, contFn, f))) + +/** @internal */ +const foldEffectReader = ( + s: S, + contFn: Predicate, + f: (s: S, input: In) => Effect.Effect +): Channel.Channel, Chunk.Chunk, E, E, S, unknown, R> => { + if (!contFn(s)) { + return core.succeedNow(s) + } + return core.readWith({ + onInput: (input: Chunk.Chunk) => + pipe( + core.fromEffect(foldChunkSplitEffect(s, input, contFn, f)), + core.flatMap(([nextS, leftovers]) => + pipe( + leftovers, + Option.match({ + onNone: () => foldEffectReader(nextS, contFn, f), + onSome: (leftover) => pipe(core.write(leftover), channel.as(nextS)) + }) + ) + ) + ), + onFailure: core.fail, + onDone: () => core.succeedNow(s) + }) +} + +/** @internal */ +const foldChunkSplitEffect = ( + s: S, + chunk: Chunk.Chunk, + contFn: Predicate, + f: (s: S, input: In) => Effect.Effect +): Effect.Effect<[S, Option.Option>], E, R> => + foldChunkSplitEffectInternal(s, chunk, 0, chunk.length, contFn, f) + +/** @internal */ +const foldChunkSplitEffectInternal = ( + s: S, + chunk: Chunk.Chunk, + index: number, + length: number, + contFn: Predicate, + f: (s: S, input: In) => Effect.Effect +): Effect.Effect<[S, Option.Option>], E, R> => { + if (index === length) { + return Effect.succeed([s, Option.none()]) + } + return pipe( + f(s, pipe(chunk, Chunk.unsafeGet(index))), + Effect.flatMap((s1) => + contFn(s1) ? + foldChunkSplitEffectInternal(s1, chunk, index + 1, length, contFn, f) : + Effect.succeed([s1, Option.some(pipe(chunk, Chunk.drop(index + 1)))]) + ) + ) +} + +/** @internal */ +export const foldLeft = (s: S, f: (s: S, input: In) => S): Sink.Sink => + ignoreLeftover(fold(s, constTrue, f)) + +/** @internal */ +export const foldLeftChunks = ( + s: S, + f: (s: S, chunk: Chunk.Chunk) => S +): Sink.Sink => foldChunks(s, constTrue, f) + +/** @internal */ +export const foldLeftChunksEffect = ( + s: S, + f: (s: S, chunk: Chunk.Chunk) => Effect.Effect +): Sink.Sink => ignoreLeftover(foldChunksEffect(s, constTrue, f)) + +/** @internal */ +export const foldLeftEffect = ( + s: S, + f: (s: S, input: In) => Effect.Effect +): Sink.Sink => foldEffect(s, constTrue, f) + +/** @internal */ +export const foldUntil = (s: S, max: number, f: (s: S, input: In) => S): Sink.Sink => + pipe( + fold<[S, number], In>( + [s, 0], + (tuple) => tuple[1] < max, + ([output, count], input) => [f(output, input), count + 1] + ), + map((tuple) => tuple[0]) + ) + +/** @internal */ +export const foldUntilEffect = ( + s: S, + max: number, + f: (s: S, input: In) => Effect.Effect +): Sink.Sink => + pipe( + foldEffect( + [s, 0 as number] as const, + (tuple) => tuple[1] < max, + ([output, count], input: In) => pipe(f(output, input), Effect.map((s) => [s, count + 1] as const)) + ), + map((tuple) => tuple[0]) + ) + +/** @internal */ +export const foldWeighted = ( + options: { + readonly initial: S + readonly maxCost: number + readonly cost: (s: S, input: In) => number + readonly body: (s: S, input: In) => S + } +): Sink.Sink => + foldWeightedDecompose({ + ...options, + decompose: Chunk.of + }) + +/** @internal */ +export const foldWeightedDecompose = ( + options: { + readonly initial: S + readonly maxCost: number + readonly cost: (s: S, input: In) => number + readonly decompose: (input: In) => Chunk.Chunk + readonly body: (s: S, input: In) => S + } +): Sink.Sink => + suspend(() => + new SinkImpl( + foldWeightedDecomposeLoop( + options.initial, + 0, + false, + options.maxCost, + options.cost, + options.decompose, + options.body + ) + ) + ) + +/** @internal */ +const foldWeightedDecomposeLoop = ( + s: S, + cost: number, + dirty: boolean, + max: number, + costFn: (s: S, input: In) => number, + decompose: (input: In) => Chunk.Chunk, + f: (s: S, input: In) => S +): Channel.Channel, Chunk.Chunk, never, never, S, unknown> => + core.readWith({ + onInput: (input: Chunk.Chunk) => { + const [nextS, nextCost, nextDirty, leftovers] = foldWeightedDecomposeFold( + input, + s, + cost, + dirty, + max, + costFn, + decompose, + f + ) + if (Chunk.isNonEmpty(leftovers)) { + return pipe(core.write(leftovers), channel.zipRight(core.succeedNow(nextS))) + } + if (cost > max) { + return core.succeedNow(nextS) + } + return foldWeightedDecomposeLoop(nextS, nextCost, nextDirty, max, costFn, decompose, f) + }, + onFailure: core.fail, + onDone: () => core.succeedNow(s) + }) + +/** @internal */ +const foldWeightedDecomposeFold = ( + input: Chunk.Chunk, + s: S, + cost: number, + dirty: boolean, + max: number, + costFn: (s: S, input: In) => number, + decompose: (input: In) => Chunk.Chunk, + f: (s: S, input: In) => S +): [S, number, boolean, Chunk.Chunk] => { + for (let index = 0; index < input.length; index++) { + const elem = Chunk.unsafeGet(input, index) + const prevCost = cost + cost = cost + costFn(s, elem) + if (cost <= max) { + s = f(s, elem) + dirty = true + continue + } + const decomposed = decompose(elem) + if (decomposed.length <= 1 && !dirty) { + // If `elem` cannot be decomposed, we need to cross the `max` threshold. To + // minimize "injury", we only allow this when we haven't added anything else + // to the aggregate (dirty = false). + return [f(s, elem), cost, true, Chunk.drop(input, index + 1)] + } + if (decomposed.length <= 1 && dirty) { + // If the state is dirty and `elem` cannot be decomposed, we stop folding + // and include `elem` in the leftovers. + return [s, prevCost, dirty, Chunk.drop(input, index)] + } + // `elem` got decomposed, so we will recurse with the decomposed elements pushed + // into the chunk we're processing and see if we can aggregate further. + input = Chunk.appendAll(decomposed, Chunk.drop(input, index + 1)) + cost = prevCost + index = -1 + } + return [s, cost, dirty, Chunk.empty()] +} + +/** @internal */ +export const foldWeightedDecomposeEffect = ( + options: { + readonly initial: S + readonly maxCost: number + readonly cost: (s: S, input: In) => Effect.Effect + readonly decompose: (input: In) => Effect.Effect, E2, R2> + readonly body: (s: S, input: In) => Effect.Effect + } +): Sink.Sink => + suspend(() => + new SinkImpl( + foldWeightedDecomposeEffectLoop( + options.initial, + options.maxCost, + options.cost, + options.decompose, + options.body, + 0, + false + ) + ) + ) + +/** @internal */ +export const foldWeightedEffect = ( + options: { + readonly initial: S + readonly maxCost: number + readonly cost: (s: S, input: In) => Effect.Effect + readonly body: (s: S, input: In) => Effect.Effect + } +): Sink.Sink => + foldWeightedDecomposeEffect({ + ...options, + decompose: (input) => Effect.succeed(Chunk.of(input)) + }) + +const foldWeightedDecomposeEffectLoop = ( + s: S, + max: number, + costFn: (s: S, input: In) => Effect.Effect, + decompose: (input: In) => Effect.Effect, E2, R2>, + f: (s: S, input: In) => Effect.Effect, + cost: number, + dirty: boolean +): Channel.Channel, Chunk.Chunk, E | E2 | E3, E | E2 | E3, S, unknown, R | R2 | R3> => + core.readWith({ + onInput: (input: Chunk.Chunk) => + pipe( + core.fromEffect(foldWeightedDecomposeEffectFold(s, max, costFn, decompose, f, input, dirty, cost, 0)), + core.flatMap(([nextS, nextCost, nextDirty, leftovers]) => { + if (Chunk.isNonEmpty(leftovers)) { + return pipe(core.write(leftovers), channel.zipRight(core.succeedNow(nextS))) + } + if (cost > max) { + return core.succeedNow(nextS) + } + return foldWeightedDecomposeEffectLoop(nextS, max, costFn, decompose, f, nextCost, nextDirty) + }) + ), + onFailure: core.fail, + onDone: () => core.succeedNow(s) + }) + +/** @internal */ +const foldWeightedDecomposeEffectFold = ( + s: S, + max: number, + costFn: (s: S, input: In) => Effect.Effect, + decompose: (input: In) => Effect.Effect, E2, R2>, + f: (s: S, input: In) => Effect.Effect, + input: Chunk.Chunk, + dirty: boolean, + cost: number, + index: number +): Effect.Effect<[S, number, boolean, Chunk.Chunk], E | E2 | E3, R | R2 | R3> => { + if (index === input.length) { + return Effect.succeed([s, cost, dirty, Chunk.empty()]) + } + const elem = pipe(input, Chunk.unsafeGet(index)) + return pipe( + costFn(s, elem), + Effect.map((newCost) => cost + newCost), + Effect.flatMap((total) => { + if (total <= max) { + return pipe( + f(s, elem), + Effect.flatMap((s) => + foldWeightedDecomposeEffectFold(s, max, costFn, decompose, f, input, true, total, index + 1) + ) + ) + } + return pipe( + decompose(elem), + Effect.flatMap((decomposed) => { + if (decomposed.length <= 1 && !dirty) { + // If `elem` cannot be decomposed, we need to cross the `max` threshold. To + // minimize "injury", we only allow this when we haven't added anything else + // to the aggregate (dirty = false). + return pipe( + f(s, elem), + Effect.map((s) => [s, total, true, pipe(input, Chunk.drop(index + 1))]) + ) + } + if (decomposed.length <= 1 && dirty) { + // If the state is dirty and `elem` cannot be decomposed, we stop folding + // and include `elem` in th leftovers. + return Effect.succeed([s, cost, dirty, pipe(input, Chunk.drop(index))]) + } + // `elem` got decomposed, so we will recurse with the decomposed elements pushed + // into the chunk we're processing and see if we can aggregate further. + const next = pipe(decomposed, Chunk.appendAll(pipe(input, Chunk.drop(index + 1)))) + return foldWeightedDecomposeEffectFold(s, max, costFn, decompose, f, next, dirty, cost, 0) + }) + ) + }) + ) +} + +/** @internal */ +export const flatMap = dual< + ( + f: (a: A) => Sink.Sink + ) => (self: Sink.Sink) => Sink.Sink, + ( + self: Sink.Sink, + f: (a: A) => Sink.Sink + ) => Sink.Sink +>( + 2, + (self, f) => foldSink(self, { onFailure: fail, onSuccess: f }) +) + +/** @internal */ +export const forEach = (f: (input: In) => Effect.Effect): Sink.Sink => { + const process: Channel.Channel, E, E, void, unknown, R> = core.readWithCause({ + onInput: (input: Chunk.Chunk) => + pipe(core.fromEffect(Effect.forEach(input, (v) => f(v), { discard: true })), core.flatMap(() => process)), + onFailure: core.failCause, + onDone: () => core.void + }) + return new SinkImpl(process) +} + +/** @internal */ +export const forEachChunk = ( + f: (input: Chunk.Chunk) => Effect.Effect +): Sink.Sink => { + const process: Channel.Channel, E, E, void, unknown, R> = core.readWithCause({ + onInput: (input: Chunk.Chunk) => pipe(core.fromEffect(f(input)), core.flatMap(() => process)), + onFailure: core.failCause, + onDone: () => core.void + }) + return new SinkImpl(process) +} + +/** @internal */ +export const forEachWhile = ( + f: (input: In) => Effect.Effect +): Sink.Sink => { + const process: Channel.Channel, Chunk.Chunk, E, E, void, unknown, R> = core.readWithCause({ + onInput: (input: Chunk.Chunk) => forEachWhileReader(f, input, 0, input.length, process), + onFailure: core.failCause, + onDone: () => core.void + }) + return new SinkImpl(process) +} + +/** @internal */ +const forEachWhileReader = ( + f: (input: In) => Effect.Effect, + input: Chunk.Chunk, + index: number, + length: number, + cont: Channel.Channel, Chunk.Chunk, E, E, void, unknown, R> +): Channel.Channel, Chunk.Chunk, E, E, void, unknown, R> => { + if (index === length) { + return cont + } + return pipe( + core.fromEffect(f(pipe(input, Chunk.unsafeGet(index)))), + core.flatMap((bool) => + bool ? + forEachWhileReader(f, input, index + 1, length, cont) : + core.write(pipe(input, Chunk.drop(index))) + ), + channel.catchAll((error) => pipe(core.write(pipe(input, Chunk.drop(index))), channel.zipRight(core.fail(error)))) + ) +} + +/** @internal */ +export const forEachChunkWhile = ( + f: (input: Chunk.Chunk) => Effect.Effect +): Sink.Sink => { + const reader: Channel.Channel, E, E, void, unknown, R> = core.readWith({ + onInput: (input: Chunk.Chunk) => + pipe( + core.fromEffect(f(input)), + core.flatMap((cont) => cont ? reader : core.void) + ), + onFailure: core.fail, + onDone: () => core.void + }) + return new SinkImpl(reader) +} + +/** @internal */ +export const fromChannel = ( + channel: Channel.Channel, Chunk.Chunk, E, never, A, unknown, R> +): Sink.Sink => new SinkImpl(channel) + +/** @internal */ +export const fromEffect = (effect: Effect.Effect): Sink.Sink => + new SinkImpl(core.fromEffect(effect)) + +/** @internal */ +export const fromPubSub = ( + pubsub: PubSub.PubSub, + options?: { + readonly shutdown?: boolean | undefined + } +): Sink.Sink => fromQueue(pubsub, options) + +/** @internal */ +export const fromPush = ( + push: Effect.Effect< + (_: Option.Option>) => Effect.Effect, Chunk.Chunk], R>, + never, + R + > +): Sink.Sink> => + new SinkImpl(channel.unwrapScoped(pipe(push, Effect.map(fromPushPull)))) + +const fromPushPull = ( + push: ( + option: Option.Option> + ) => Effect.Effect, Chunk.Chunk], R> +): Channel.Channel, Chunk.Chunk, E, never, Z, unknown, R> => + core.readWith({ + onInput: (input: Chunk.Chunk) => + channel.foldChannel(core.fromEffect(push(Option.some(input))), { + onFailure: ([either, leftovers]) => + Either.match(either, { + onLeft: (error) => pipe(core.write(leftovers), channel.zipRight(core.fail(error))), + onRight: (z) => pipe(core.write(leftovers), channel.zipRight(core.succeedNow(z))) + }), + onSuccess: () => fromPushPull(push) + }), + onFailure: core.fail, + onDone: () => + channel.foldChannel(core.fromEffect(push(Option.none())), { + onFailure: ([either, leftovers]) => + Either.match(either, { + onLeft: (error) => pipe(core.write(leftovers), channel.zipRight(core.fail(error))), + onRight: (z) => pipe(core.write(leftovers), channel.zipRight(core.succeedNow(z))) + }), + onSuccess: () => + core.fromEffect( + Effect.dieMessage( + "BUG: Sink.fromPush - please report an issue at https://github.com/Effect-TS/effect/issues" + ) + ) + }) + }) + +/** @internal */ +export const fromQueue = ( + queue: Queue.Enqueue, + options?: { + readonly shutdown?: boolean | undefined + } +): Sink.Sink => + options?.shutdown ? + unwrapScoped( + Effect.map( + Effect.acquireRelease(Effect.succeed(queue), Queue.shutdown), + fromQueue + ) + ) : + forEachChunk((input: Chunk.Chunk) => Queue.offerAll(queue, input)) + +/** @internal */ +export const head = (): Sink.Sink, In, In> => + fold( + Option.none() as Option.Option, + Option.isNone, + (option, input) => + Option.match(option, { + onNone: () => Option.some(input), + onSome: () => option + }) + ) + +/** @internal */ +export const ignoreLeftover = (self: Sink.Sink): Sink.Sink => + new SinkImpl(channel.drain(toChannel(self))) + +/** @internal */ +export const last = (): Sink.Sink, In, In> => + foldLeftChunks(Option.none(), (s, input) => Option.orElse(Chunk.last(input), () => s)) + +/** @internal */ +export const leftover = (chunk: Chunk.Chunk): Sink.Sink => + new SinkImpl(core.suspend(() => core.write(chunk))) + +/** @internal */ +export const map = dual< + (f: (a: A) => A2) => (self: Sink.Sink) => Sink.Sink, + (self: Sink.Sink, f: (a: A) => A2) => Sink.Sink +>(2, (self, f) => { + return new SinkImpl(pipe(toChannel(self), channel.map(f))) +}) + +/** @internal */ +export const mapEffect = dual< + ( + f: (a: A) => Effect.Effect + ) => (self: Sink.Sink) => Sink.Sink, + ( + self: Sink.Sink, + f: (a: A) => Effect.Effect + ) => Sink.Sink +>( + 2, + (self, f) => new SinkImpl(pipe(toChannel(self), channel.mapEffect(f))) +) + +/** @internal */ +export const mapError = dual< + (f: (error: E) => E2) => (self: Sink.Sink) => Sink.Sink, + (self: Sink.Sink, f: (error: E) => E2) => Sink.Sink +>( + 2, + (self, f) => new SinkImpl(pipe(toChannel(self), channel.mapError(f))) +) + +/** @internal */ +export const mapLeftover = dual< + (f: (leftover: L) => L2) => (self: Sink.Sink) => Sink.Sink, + (self: Sink.Sink, f: (leftover: L) => L2) => Sink.Sink +>( + 2, + (self, f) => new SinkImpl(pipe(toChannel(self), channel.mapOut(Chunk.map(f)))) +) + +/** @internal */ +export const never: Sink.Sink = fromEffect(Effect.never) + +/** @internal */ +export const orElse = dual< + ( + that: LazyArg> + ) => (self: Sink.Sink) => Sink.Sink, + ( + self: Sink.Sink, + that: LazyArg> + ) => Sink.Sink +>( + 2, + ( + self: Sink.Sink, + that: LazyArg> + ): Sink.Sink => + new SinkImpl( + pipe(toChannel(self), channel.orElse(() => toChannel(that()))) + ) +) + +/** @internal */ +export const provideContext = dual< + (context: Context.Context) => (self: Sink.Sink) => Sink.Sink, + (self: Sink.Sink, context: Context.Context) => Sink.Sink +>( + 2, + (self, context) => new SinkImpl(pipe(toChannel(self), core.provideContext(context))) +) + +/** @internal */ +export const race = dual< + ( + that: Sink.Sink + ) => (self: Sink.Sink) => Sink.Sink, + ( + self: Sink.Sink, + that: Sink.Sink + ) => Sink.Sink +>( + 2, + (self, that) => pipe(self, raceBoth(that), map(Either.merge)) +) + +/** @internal */ +export const raceBoth = dual< + ( + that: Sink.Sink, + options?: { + readonly capacity?: number | undefined + } + ) => ( + self: Sink.Sink + ) => Sink.Sink, In & In1, L1 | L, E1 | E, R1 | R>, + ( + self: Sink.Sink, + that: Sink.Sink, + options?: { + readonly capacity?: number | undefined + } + ) => Sink.Sink, In & In1, L1 | L, E1 | E, R1 | R> +>( + (args) => isSink(args[1]), + (self, that, options) => + raceWith(self, { + other: that, + onSelfDone: (selfDone) => mergeDecision.Done(Effect.map(selfDone, Either.left)), + onOtherDone: (thatDone) => mergeDecision.Done(Effect.map(thatDone, Either.right)), + capacity: options?.capacity ?? 16 + }) +) + +/** @internal */ +export const raceWith = dual< + ( + options: { + readonly other: Sink.Sink + readonly onSelfDone: (exit: Exit.Exit) => MergeDecision.MergeDecision + readonly onOtherDone: (exit: Exit.Exit) => MergeDecision.MergeDecision + readonly capacity?: number | undefined + } + ) => (self: Sink.Sink) => Sink.Sink, + ( + self: Sink.Sink, + options: { + readonly other: Sink.Sink + readonly onSelfDone: (exit: Exit.Exit) => MergeDecision.MergeDecision + readonly onOtherDone: (exit: Exit.Exit) => MergeDecision.MergeDecision + readonly capacity?: number | undefined + } + ) => Sink.Sink +>( + 2, + ( + self: Sink.Sink, + options: { + readonly other: Sink.Sink + readonly onSelfDone: (exit: Exit.Exit) => MergeDecision.MergeDecision + readonly onOtherDone: (exit: Exit.Exit) => MergeDecision.MergeDecision + readonly capacity?: number | undefined + } + ): Sink.Sink => { + function race(scope: Scope.Scope) { + return Effect.gen(function*() { + const pubsub = yield* PubSub.bounded< + Either.Either, Exit.Exit> + >(options?.capacity ?? 16) + const subscription1 = yield* Scope.extend(PubSub.subscribe(pubsub), scope) + const subscription2 = yield* Scope.extend(PubSub.subscribe(pubsub), scope) + const reader = channel.toPubSub(pubsub) + const writer = channel.fromQueue(subscription1).pipe( + core.pipeTo(toChannel(self)), + channel.zipLeft(core.fromEffect(Queue.shutdown(subscription1))), + channel.mergeWith({ + other: channel.fromQueue(subscription2).pipe( + core.pipeTo(toChannel(options.other)), + channel.zipLeft(core.fromEffect(Queue.shutdown(subscription2))) + ), + onSelfDone: options.onSelfDone, + onOtherDone: options.onOtherDone + }) + ) + const racedChannel = channel.mergeWith(reader, { + other: writer, + onSelfDone: () => mergeDecision.Await(identity), + onOtherDone: (exit) => mergeDecision.Done(exit) + }) as Channel.Channel< + Chunk.Chunk, + Chunk.Chunk, + E | E2, + never, + A3 | A4, + unknown, + R | R2 + > + return new SinkImpl(racedChannel) + }) + } + return unwrapScopedWith(race) + } +) + +/** @internal */ +export const refineOrDie = dual< + ( + pf: (error: E) => Option.Option + ) => (self: Sink.Sink) => Sink.Sink, + ( + self: Sink.Sink, + pf: (error: E) => Option.Option + ) => Sink.Sink +>( + 2, + (self, pf) => pipe(self, refineOrDieWith(pf, identity)) +) + +/** @internal */ +export const refineOrDieWith = dual< + ( + pf: (error: E) => Option.Option, + f: (error: E) => unknown + ) => (self: Sink.Sink) => Sink.Sink, + ( + self: Sink.Sink, + pf: (error: E) => Option.Option, + f: (error: E) => unknown + ) => Sink.Sink +>( + 3, + (self, pf, f) => { + const newChannel = pipe( + self, + toChannel, + channel.catchAll((error) => + Option.match(pf(error), { + onNone: () => core.failCauseSync(() => Cause.die(f(error))), + onSome: core.fail + }) + ) + ) + return new SinkImpl(newChannel) + } +) + +/** @internal */ +export const service = ( + tag: Context.Tag +): Sink.Sink => serviceWith(tag, identity) + +/** @internal */ +export const serviceWith = ( + tag: Context.Tag, + f: (service: Types.NoInfer) => Z +): Sink.Sink => fromEffect(Effect.map(tag, f)) + +/** @internal */ +export const serviceWithEffect = ( + tag: Context.Tag, + f: (service: Types.NoInfer) => Effect.Effect +): Sink.Sink => fromEffect(Effect.flatMap(tag, f)) + +/** @internal */ +export const serviceWithSink = ( + tag: Context.Tag, + f: (service: Types.NoInfer) => Sink.Sink +): Sink.Sink => + new SinkImpl(pipe(Effect.map(tag, (service) => toChannel(f(service))), channel.unwrap)) + +/** @internal */ +export const some = (predicate: Predicate): Sink.Sink => + fold(false, (bool) => !bool, (acc, input) => acc || predicate(input)) + +/** @internal */ +export const splitWhere = dual< + (f: Predicate) => (self: Sink.Sink) => Sink.Sink, + (self: Sink.Sink, f: Predicate) => Sink.Sink +>(2, (self: Sink.Sink, f: Predicate): Sink.Sink => { + const newChannel = pipe( + core.fromEffect(Ref.make(Chunk.empty())), + core.flatMap((ref) => + pipe( + splitWhereSplitter(false, ref, f), + channel.pipeToOrFail(toChannel(self)), + core.collectElements, + core.flatMap(([leftovers, z]) => + pipe( + core.fromEffect(Ref.get(ref)), + core.flatMap((leftover) => + pipe( + core.write>(pipe(leftover, Chunk.appendAll(Chunk.flatten(leftovers)))), + channel.zipRight(core.succeed(z)) + ) + ) + ) + ) + ) + ) + ) + return new SinkImpl(newChannel) +}) + +/** @internal */ +const splitWhereSplitter = ( + written: boolean, + leftovers: Ref.Ref>, + f: Predicate +): Channel.Channel, Chunk.Chunk, E, never, unknown, unknown> => + core.readWithCause({ + onInput: (input) => { + if (Chunk.isEmpty(input)) { + return splitWhereSplitter(written, leftovers, f) + } + if (written) { + const index = indexWhere(input, f) + if (index === -1) { + return channel.zipRight( + core.write(input), + splitWhereSplitter(true, leftovers, f) + ) + } + const [left, right] = Chunk.splitAt(input, index) + return channel.zipRight( + core.write(left), + core.fromEffect(Ref.set(leftovers, right)) + ) + } + const index = indexWhere(input, f, 1) + if (index === -1) { + return channel.zipRight( + core.write(input), + splitWhereSplitter(true, leftovers, f) + ) + } + const [left, right] = pipe(input, Chunk.splitAt(Math.max(index, 1))) + return channel.zipRight(core.write(left), core.fromEffect(Ref.set(leftovers, right))) + }, + onFailure: core.failCause, + onDone: core.succeed + }) + +/** @internal */ +const indexWhere = (self: Chunk.Chunk, predicate: Predicate, from = 0): number => { + const iterator = self[Symbol.iterator]() + let index = 0 + let result = -1 + let next: IteratorResult + while (result < 0 && (next = iterator.next()) && !next.done) { + const a = next.value + if (index >= from && predicate(a)) { + result = index + } + index = index + 1 + } + return result +} + +/** @internal */ +export const succeed = (a: A): Sink.Sink => new SinkImpl(core.succeed(a)) + +/** @internal */ +export const sum: Sink.Sink = foldLeftChunks( + 0, + (acc, chunk) => acc + Chunk.reduce(chunk, 0, (s, a) => s + a) +) + +/** @internal */ +export const summarized = dual< + ( + summary: Effect.Effect, + f: (start: A2, end: A2) => A3 + ) => (self: Sink.Sink) => Sink.Sink<[A, A3], In, L, E2 | E, R2 | R>, + ( + self: Sink.Sink, + summary: Effect.Effect, + f: (start: A2, end: A2) => A3 + ) => Sink.Sink<[A, A3], In, L, E2 | E, R2 | R> +>( + 3, + (self, summary, f) => { + const newChannel = pipe( + core.fromEffect(summary), + core.flatMap((start) => + pipe( + self, + toChannel, + core.flatMap((done) => + pipe( + core.fromEffect(summary), + channel.map((end) => [done, f(start, end)]) + ) + ) + ) + ) + ) + return new SinkImpl(newChannel) + } +) + +/** @internal */ +export const sync = (evaluate: LazyArg): Sink.Sink => new SinkImpl(core.sync(evaluate)) + +/** @internal */ +export const take = (n: number): Sink.Sink, In, In> => + pipe( + foldChunks, In>( + Chunk.empty(), + (chunk) => chunk.length < n, + (acc, chunk) => pipe(acc, Chunk.appendAll(chunk)) + ), + flatMap((acc) => { + const [taken, leftover] = pipe(acc, Chunk.splitAt(n)) + return new SinkImpl(pipe(core.write(leftover), channel.zipRight(core.succeedNow(taken)))) + }) + ) + +/** @internal */ +export const toChannel = ( + self: Sink.Sink +): Channel.Channel, Chunk.Chunk, E, never, A, unknown, R> => + Effect.isEffect(self) ? + toChannel(fromEffect(self as Effect.Effect)) : + (self as SinkImpl).channel + +/** @internal */ +export const unwrap = ( + effect: Effect.Effect, E, R> +): Sink.Sink => + new SinkImpl( + channel.unwrap(pipe(effect, Effect.map((sink) => toChannel(sink)))) + ) + +/** @internal */ +export const unwrapScoped = ( + effect: Effect.Effect, E, R> +): Sink.Sink> => + new SinkImpl( + channel.unwrapScoped(effect.pipe( + Effect.map((sink) => toChannel(sink)) + )) + ) + +/** @internal */ +export const unwrapScopedWith = ( + f: (scope: Scope.Scope) => Effect.Effect, E, R> +): Sink.Sink => + new SinkImpl( + channel.unwrapScopedWith((scope) => + f(scope).pipe( + Effect.map((sink) => toChannel(sink)) + ) + ) + ) + +/** @internal */ +export const withDuration = ( + self: Sink.Sink +): Sink.Sink<[A, Duration.Duration], In, L, E, R> => + pipe(self, summarized(Clock.currentTimeMillis, (start, end) => Duration.millis(end - start))) + +/** @internal */ +export const zip = dual< + ( + that: Sink.Sink, + options?: { + readonly concurrent?: boolean | undefined + } + ) => (self: Sink.Sink) => Sink.Sink<[A, A2], In & In2, L | L2, E2 | E, R2 | R>, + ( + self: Sink.Sink, + that: Sink.Sink, + options?: { + readonly concurrent?: boolean | undefined + } + ) => Sink.Sink<[A, A2], In & In2, L | L2, E2 | E, R2 | R> +>( + (args) => isSink(args[1]), + ( + self: Sink.Sink, + that: Sink.Sink, + options?: { + readonly concurrent?: boolean | undefined + } + ): Sink.Sink<[A, A2], In & In2, L | L2, E2 | E, R2 | R> => zipWith(self, that, (z, z2) => [z, z2], options) +) + +/** @internal */ +export const zipLeft = dual< + ( + that: Sink.Sink, + options?: { + readonly concurrent?: boolean | undefined + } + ) => (self: Sink.Sink) => Sink.Sink, + ( + self: Sink.Sink, + that: Sink.Sink, + options?: { + readonly concurrent?: boolean | undefined + } + ) => Sink.Sink +>( + (args) => isSink(args[1]), + ( + self: Sink.Sink, + that: Sink.Sink, + options?: { + readonly concurrent?: boolean | undefined + } + ): Sink.Sink => zipWith(self, that, (z, _) => z, options) +) + +/** @internal */ +export const zipRight = dual< + ( + that: Sink.Sink, + options?: { + readonly concurrent?: boolean | undefined + } + ) => (self: Sink.Sink) => Sink.Sink, + ( + self: Sink.Sink, + that: Sink.Sink, + options?: { + readonly concurrent?: boolean | undefined + } + ) => Sink.Sink +>( + (args) => isSink(args[1]), + ( + self: Sink.Sink, + that: Sink.Sink, + options?: { + readonly concurrent?: boolean | undefined + } + ): Sink.Sink => zipWith(self, that, (_, z2) => z2, options) +) + +/** @internal */ +export const zipWith = dual< + ( + that: Sink.Sink, + f: (a: A, a2: A2) => A3, + options?: { + readonly concurrent?: boolean | undefined + } + ) => (self: Sink.Sink) => Sink.Sink, + ( + self: Sink.Sink, + that: Sink.Sink, + f: (a: A, a2: A2) => A3, + options?: { + readonly concurrent?: boolean | undefined + } + ) => Sink.Sink +>( + (args) => isSink(args[1]), + ( + self: Sink.Sink, + that: Sink.Sink, + f: (a: A, a2: A2) => A3, + options?: { + readonly concurrent?: boolean | undefined + } + ): Sink.Sink => + options?.concurrent ? + raceWith(self, { + other: that, + onSelfDone: Exit.match({ + onFailure: (cause) => mergeDecision.Done(Effect.failCause(cause)), + onSuccess: (leftZ) => + mergeDecision.Await( + Exit.match({ + onFailure: Effect.failCause, + onSuccess: (rightZ) => Effect.succeed(f(leftZ, rightZ)) + }) + ) + }), + onOtherDone: Exit.match({ + onFailure: (cause) => mergeDecision.Done(Effect.failCause(cause)), + onSuccess: (rightZ) => + mergeDecision.Await( + Exit.match({ + onFailure: Effect.failCause, + onSuccess: (leftZ) => Effect.succeed(f(leftZ, rightZ)) + }) + ) + }) + }) : + flatMap(self, (z) => map(that, (z2) => f(z, z2))) +) + +// Circular with Channel + +/** @internal */ +export const channelToSink = ( + self: Channel.Channel, Chunk.Chunk, OutErr, InErr, OutDone, unknown, Env> +): Sink.Sink => new SinkImpl(self) + +// Constants + +/** @internal */ +export const count: Sink.Sink = foldLeftChunks( + 0, + (acc, chunk) => acc + chunk.length +) + +/** @internal */ +export const mkString: Sink.Sink = suspend(() => { + const strings: Array = [] + return pipe( + foldLeftChunks(void 0, (_, elems) => + Chunk.map(elems, (elem) => { + strings.push(String(elem)) + })), + map(() => strings.join("")) + ) +}) + +/** @internal */ +export const timed: Sink.Sink = pipe( + withDuration(drain), + map((tuple) => tuple[1]) +) diff --git a/repos/effect/packages/effect/src/internal/stack.ts b/repos/effect/packages/effect/src/internal/stack.ts new file mode 100644 index 0000000..27a9ec9 --- /dev/null +++ b/repos/effect/packages/effect/src/internal/stack.ts @@ -0,0 +1,10 @@ +/** @internal */ +export interface Stack { + readonly value: A + readonly previous: Stack | undefined +} + +export const make = (value: A, previous?: Stack): Stack => ({ + value, + previous +}) diff --git a/repos/effect/packages/effect/src/internal/stm/core.ts b/repos/effect/packages/effect/src/internal/stm/core.ts new file mode 100644 index 0000000..d3a16b2 --- /dev/null +++ b/repos/effect/packages/effect/src/internal/stm/core.ts @@ -0,0 +1,817 @@ +import * as Cause from "../../Cause.js" +import * as Context from "../../Context.js" +import * as Effect from "../../Effect.js" +import * as Either from "../../Either.js" +import * as Equal from "../../Equal.js" +import * as Exit from "../../Exit.js" +import type * as FiberId from "../../FiberId.js" +import * as FiberRef from "../../FiberRef.js" +import type { LazyArg } from "../../Function.js" +import { constVoid, dual, pipe } from "../../Function.js" +import * as Hash from "../../Hash.js" +import type * as Option from "../../Option.js" +import { pipeArguments } from "../../Pipeable.js" +import { hasProperty } from "../../Predicate.js" +import type * as Scheduler from "../../Scheduler.js" +import type * as STM from "../../STM.js" +import { internalCall, YieldWrap } from "../../Utils.js" +import { ChannelTypeId } from "../core-stream.js" +import { withFiberRuntime } from "../core.js" +import { effectVariance, StreamTypeId } from "../effectable.js" +import { OP_COMMIT } from "../opCodes/effect.js" +import { SingleShotGen } from "../singleShotGen.js" +import { SinkTypeId } from "../sink.js" +import * as Journal from "./journal.js" +import * as OpCodes from "./opCodes/stm.js" +import * as TExitOpCodes from "./opCodes/tExit.js" +import * as TryCommitOpCodes from "./opCodes/tryCommit.js" +import * as STMState from "./stmState.js" +import * as TExit from "./tExit.js" +import * as TryCommit from "./tryCommit.js" +import * as TxnId from "./txnId.js" + +/** @internal */ +const STMSymbolKey = "effect/STM" + +/** @internal */ +export const STMTypeId: STM.STMTypeId = Symbol.for( + STMSymbolKey +) as STM.STMTypeId + +/** @internal */ +export type Primitive = + | STMEffect + | STMOnFailure + | STMOnRetry + | STMOnSuccess + | STMProvide + | STMSync + | STMSucceed + | STMRetry + | STMFail + | STMDie + | STMInterrupt + +/** @internal */ +type Op = STM.STM & Body & { + readonly _op: OP_COMMIT + readonly effect_instruction_i0: Tag +} + +/** @internal */ +interface STMEffect extends + Op + ) => STM.STM + }> +{} + +/** @internal */ +interface STMOnFailure extends + Op + readonly effect_instruction_i2: (error: unknown) => STM.STM + }> +{} + +/** @internal */ +interface STMOnRetry extends + Op + readonly effect_instruction_i2: () => STM.STM + }> +{} + +/** @internal */ +interface STMOnSuccess extends + Op + readonly effect_instruction_i2: (a: unknown) => STM.STM + }> +{} + +/** @internal */ +interface STMProvide extends + Op + readonly effect_instruction_i2: (context: Context.Context) => Context.Context + }> +{} + +/** @internal */ +interface STMSync extends + Op unknown + }> +{} + +/** @internal */ +interface STMSucceed extends + Op +{} + +/** @internal */ +interface STMRetry extends Op {} + +/** @internal */ +interface STMFail extends + Op + }> +{} + +/** @internal */ +interface STMDie extends + Op + }> +{} + +/** @internal */ +interface STMInterrupt extends + Op +{} + +const stmVariance = { + /* c8 ignore next */ + _R: (_: never) => _, + /* c8 ignore next */ + _E: (_: never) => _, + /* c8 ignore next */ + _A: (_: never) => _ +} + +/** @internal */ +class STMPrimitive implements STM.STM { + public _op = OP_COMMIT + public effect_instruction_i1: any = undefined + public effect_instruction_i2: any = undefined; + [Effect.EffectTypeId]: any; + [StreamTypeId]: any; + [SinkTypeId]: any; + [ChannelTypeId]: any + get [STMTypeId]() { + return stmVariance + } + constructor(readonly effect_instruction_i0: Primitive["effect_instruction_i0"]) { + this[Effect.EffectTypeId] = effectVariance + this[StreamTypeId] = stmVariance + this[SinkTypeId] = stmVariance + this[ChannelTypeId] = stmVariance + } + [Equal.symbol](this: {}, that: unknown) { + return this === that + } + [Hash.symbol](this: {}) { + return Hash.cached(this, Hash.random(this)) + } + [Symbol.iterator]() { + return new SingleShotGen(new YieldWrap(this)) as any + } + commit(this: STM.STM): Effect.Effect { + return unsafeAtomically(this, constVoid, constVoid) + } + pipe() { + return pipeArguments(this, arguments) + } +} + +/** @internal */ +export const isSTM = (u: unknown): u is STM.STM => hasProperty(u, STMTypeId) + +/** @internal */ +export const commit = (self: STM.STM): Effect.Effect => + unsafeAtomically(self, constVoid, constVoid) + +/** @internal */ +export const unsafeAtomically = ( + self: STM.STM, + onDone: (exit: Exit.Exit) => unknown, + onInterrupt: LazyArg +): Effect.Effect => + withFiberRuntime((state) => { + const fiberId = state.id() + const env = state.getFiberRef(FiberRef.currentContext) as Context.Context + const scheduler = state.getFiberRef(FiberRef.currentScheduler) + const priority = state.getFiberRef(FiberRef.currentSchedulingPriority) + const commitResult = tryCommitSync(fiberId, self, env, scheduler, priority) + switch (commitResult._tag) { + case TryCommitOpCodes.OP_DONE: { + onDone(commitResult.exit) + return commitResult.exit + } + case TryCommitOpCodes.OP_SUSPEND: { + const txnId = TxnId.make() + const state: { value: STMState.STMState } = { value: STMState.running } + const effect = Effect.async( + (k: (effect: Effect.Effect) => unknown): void => + tryCommitAsync(fiberId, self, txnId, state, env, scheduler, priority, k) + ) + return Effect.uninterruptibleMask((restore) => + pipe( + restore(effect), + Effect.catchAllCause((cause) => { + let currentState = state.value + if (STMState.isRunning(currentState)) { + state.value = STMState.interrupted + } + currentState = state.value + if (STMState.isDone(currentState)) { + onDone(currentState.exit) + return currentState.exit + } + onInterrupt() + return Effect.failCause(cause) + }) + ) + ) + } + } + }) + +/** @internal */ +const tryCommit = ( + fiberId: FiberId.FiberId, + stm: STM.STM, + state: { value: STMState.STMState }, + env: Context.Context, + scheduler: Scheduler.Scheduler, + priority: number +): TryCommit.TryCommit => { + const journal: Journal.Journal = new Map() + const tExit = new STMDriver(stm, journal, fiberId, env).run() + const analysis = Journal.analyzeJournal(journal) + + if (analysis === Journal.JournalAnalysisReadWrite) { + Journal.commitJournal(journal) + } else if (analysis === Journal.JournalAnalysisInvalid) { + throw new Error( + "BUG: STM.TryCommit.tryCommit - please report an issue at https://github.com/Effect-TS/effect/issues" + ) + } + + switch (tExit._tag) { + case TExitOpCodes.OP_SUCCEED: { + state.value = STMState.fromTExit(tExit) + return completeTodos(Exit.succeed(tExit.value), journal, scheduler, priority) + } + case TExitOpCodes.OP_FAIL: { + state.value = STMState.fromTExit(tExit) + const cause = Cause.fail(tExit.error) + return completeTodos( + Exit.failCause(cause), + journal, + scheduler, + priority + ) + } + case TExitOpCodes.OP_DIE: { + state.value = STMState.fromTExit(tExit) + const cause = Cause.die(tExit.defect) + return completeTodos( + Exit.failCause(cause), + journal, + scheduler, + priority + ) + } + case TExitOpCodes.OP_INTERRUPT: { + state.value = STMState.fromTExit(tExit) + const cause = Cause.interrupt(fiberId) + return completeTodos( + Exit.failCause(cause), + journal, + scheduler, + priority + ) + } + case TExitOpCodes.OP_RETRY: { + return TryCommit.suspend(journal) + } + } +} + +/** @internal */ +const tryCommitSync = ( + fiberId: FiberId.FiberId, + stm: STM.STM, + env: Context.Context, + scheduler: Scheduler.Scheduler, + priority: number +): TryCommit.TryCommit => { + const journal: Journal.Journal = new Map() + const tExit = new STMDriver(stm, journal, fiberId, env).run() + const analysis = Journal.analyzeJournal(journal) + + if (analysis === Journal.JournalAnalysisReadWrite && TExit.isSuccess(tExit)) { + Journal.commitJournal(journal) + } else if (analysis === Journal.JournalAnalysisInvalid) { + throw new Error( + "BUG: STM.TryCommit.tryCommitSync - please report an issue at https://github.com/Effect-TS/effect/issues" + ) + } + + switch (tExit._tag) { + case TExitOpCodes.OP_SUCCEED: { + return completeTodos(Exit.succeed(tExit.value), journal, scheduler, priority) + } + case TExitOpCodes.OP_FAIL: { + const cause = Cause.fail(tExit.error) + return completeTodos( + Exit.failCause(cause), + journal, + scheduler, + priority + ) + } + case TExitOpCodes.OP_DIE: { + const cause = Cause.die(tExit.defect) + return completeTodos( + Exit.failCause(cause), + journal, + scheduler, + priority + ) + } + case TExitOpCodes.OP_INTERRUPT: { + const cause = Cause.interrupt(fiberId) + return completeTodos( + Exit.failCause(cause), + journal, + scheduler, + priority + ) + } + case TExitOpCodes.OP_RETRY: { + return TryCommit.suspend(journal) + } + } +} + +/** @internal */ +const tryCommitAsync = ( + fiberId: FiberId.FiberId, + self: STM.STM, + txnId: TxnId.TxnId, + state: { value: STMState.STMState }, + context: Context.Context, + scheduler: Scheduler.Scheduler, + priority: number, + k: (effect: Effect.Effect) => unknown +) => { + if (STMState.isRunning(state.value)) { + const result = tryCommit(fiberId, self, state, context, scheduler, priority) + switch (result._tag) { + case TryCommitOpCodes.OP_DONE: { + completeTryCommit(result.exit, k) + break + } + case TryCommitOpCodes.OP_SUSPEND: { + Journal.addTodo( + txnId, + result.journal, + () => tryCommitAsync(fiberId, self, txnId, state, context, scheduler, priority, k) + ) + break + } + } + } +} + +/** @internal */ +const completeTodos = ( + exit: Exit.Exit, + journal: Journal.Journal, + scheduler: Scheduler.Scheduler, + priority: number +): TryCommit.TryCommit => { + const todos = Journal.collectTodos(journal) + if (todos.size > 0) { + scheduler.scheduleTask(() => Journal.execTodos(todos), priority) + } + return TryCommit.done(exit) +} + +/** @internal */ +const completeTryCommit = ( + exit: Exit.Exit, + k: (effect: Effect.Effect) => unknown +): void => { + k(exit) +} + +/** @internal */ +type Continuation = STMOnFailure | STMOnSuccess | STMOnRetry + +/** @internal */ +export const context = (): STM.STM, never, R> => + effect>((_, __, env) => env) + +/** @internal */ +export const contextWith = (f: (environment: Context.Context) => R): STM.STM => + map(context(), f) + +/** @internal */ +export const contextWithSTM = ( + f: (environment: Context.Context) => STM.STM +): STM.STM => flatMap(context(), f) + +/** @internal */ +export class STMDriver { + private contStack: Array = [] + private env: Context.Context + + constructor( + readonly self: STM.STM, + readonly journal: Journal.Journal, + readonly fiberId: FiberId.FiberId, + r0: Context.Context + ) { + this.env = r0 as Context.Context + } + + getEnv(): Context.Context { + return this.env + } + + pushStack(cont: Continuation) { + this.contStack.push(cont) + } + + popStack() { + return this.contStack.pop() + } + + nextSuccess() { + let current = this.popStack() + while (current !== undefined && current.effect_instruction_i0 !== OpCodes.OP_ON_SUCCESS) { + current = this.popStack() + } + return current + } + + nextFailure() { + let current = this.popStack() + while (current !== undefined && current.effect_instruction_i0 !== OpCodes.OP_ON_FAILURE) { + current = this.popStack() + } + return current + } + + nextRetry() { + let current = this.popStack() + while (current !== undefined && current.effect_instruction_i0 !== OpCodes.OP_ON_RETRY) { + current = this.popStack() + } + return current + } + + run(): TExit.TExit { + let curr = this.self as Primitive | Context.Tag | Either.Either | Option.Option | undefined + let exit: TExit.TExit | undefined = undefined + while (exit === undefined && curr !== undefined) { + try { + const current = curr + if (current) { + switch (current._op) { + case "Tag": { + curr = effect((_, __, env) => Context.unsafeGet(env, current)) as Primitive + break + } + case "Left": { + curr = fail(current.left) as Primitive + break + } + case "None": { + curr = fail(new Cause.NoSuchElementException()) as Primitive + break + } + case "Right": { + curr = succeed(current.right) as Primitive + break + } + case "Some": { + curr = succeed(current.value) as Primitive + break + } + case "Commit": { + switch (current.effect_instruction_i0) { + case OpCodes.OP_DIE: { + exit = TExit.die(internalCall(() => current.effect_instruction_i1())) + break + } + case OpCodes.OP_FAIL: { + const cont = this.nextFailure() + if (cont === undefined) { + exit = TExit.fail(internalCall(() => current.effect_instruction_i1())) + } else { + curr = internalCall(() => + cont.effect_instruction_i2( + internalCall(() => current.effect_instruction_i1()) + ) as Primitive + ) + } + break + } + case OpCodes.OP_RETRY: { + const cont = this.nextRetry() + if (cont === undefined) { + exit = TExit.retry + } else { + curr = internalCall(() => cont.effect_instruction_i2() as Primitive) + } + break + } + case OpCodes.OP_INTERRUPT: { + exit = TExit.interrupt(this.fiberId) + break + } + case OpCodes.OP_WITH_STM_RUNTIME: { + curr = internalCall(() => + current.effect_instruction_i1(this as STMDriver) as Primitive + ) + break + } + case OpCodes.OP_ON_SUCCESS: + case OpCodes.OP_ON_FAILURE: + case OpCodes.OP_ON_RETRY: { + this.pushStack(current) + curr = current.effect_instruction_i1 as Primitive + break + } + case OpCodes.OP_PROVIDE: { + const env = this.env + this.env = internalCall(() => current.effect_instruction_i2(env)) + curr = pipe( + current.effect_instruction_i1, + ensuring(sync(() => (this.env = env))) + ) as Primitive + break + } + case OpCodes.OP_SUCCEED: { + const value = current.effect_instruction_i1 + const cont = this.nextSuccess() + if (cont === undefined) { + exit = TExit.succeed(value) + } else { + curr = internalCall(() => cont.effect_instruction_i2(value) as Primitive) + } + break + } + case OpCodes.OP_SYNC: { + const value = internalCall(() => current.effect_instruction_i1()) + const cont = this.nextSuccess() + if (cont === undefined) { + exit = TExit.succeed(value) + } else { + curr = internalCall(() => cont.effect_instruction_i2(value) as Primitive) + } + break + } + } + break + } + } + } + } catch (e) { + curr = die(e) as Primitive + } + } + return exit as TExit.TExit + } +} + +/** @internal */ +export const catchAll = dual< + ( + f: (e: E) => STM.STM + ) => ( + self: STM.STM + ) => STM.STM, + ( + self: STM.STM, + f: (e: E) => STM.STM + ) => STM.STM +>(2, (self, f) => { + const stm = new STMPrimitive(OpCodes.OP_ON_FAILURE) + stm.effect_instruction_i1 = self + stm.effect_instruction_i2 = f + return stm +}) + +/** @internal */ +export const mapInputContext = dual< + ( + f: (context: Context.Context) => Context.Context + ) => ( + self: STM.STM + ) => STM.STM, + ( + self: STM.STM, + f: (context: Context.Context) => Context.Context + ) => STM.STM +>(2, (self, f) => { + const stm = new STMPrimitive(OpCodes.OP_PROVIDE) + stm.effect_instruction_i1 = self + stm.effect_instruction_i2 = f + return stm +}) + +/** @internal */ +export const die = (defect: unknown): STM.STM => dieSync(() => defect) + +/** @internal */ +export const dieMessage = (message: string): STM.STM => dieSync(() => new Cause.RuntimeException(message)) + +/** @internal */ +export const dieSync = (evaluate: LazyArg): STM.STM => { + const stm = new STMPrimitive(OpCodes.OP_DIE) + stm.effect_instruction_i1 = evaluate + return stm as any +} + +/** @internal */ +export const effect = ( + f: (journal: Journal.Journal, fiberId: FiberId.FiberId, environment: Context.Context) => A +): STM.STM => withSTMRuntime((_) => succeed(f(_.journal, _.fiberId, _.getEnv()))) + +/** @internal */ +export const ensuring = dual< + (finalizer: STM.STM) => (self: STM.STM) => STM.STM, + (self: STM.STM, finalizer: STM.STM) => STM.STM +>(2, (self, finalizer) => + matchSTM(self, { + onFailure: (e) => zipRight(finalizer, fail(e)), + onSuccess: (a) => zipRight(finalizer, succeed(a)) + })) + +/** @internal */ +export const fail = (error: E): STM.STM => failSync(() => error) + +/** @internal */ +export const failSync = (evaluate: LazyArg): STM.STM => { + const stm = new STMPrimitive(OpCodes.OP_FAIL) + stm.effect_instruction_i1 = evaluate + return stm as any +} + +/** @internal */ +export const flatMap = dual< + (f: (a: A) => STM.STM) => (self: STM.STM) => STM.STM, + (self: STM.STM, f: (a: A) => STM.STM) => STM.STM +>(2, (self, f) => { + const stm = new STMPrimitive(OpCodes.OP_ON_SUCCESS) + stm.effect_instruction_i1 = self + stm.effect_instruction_i2 = f + return stm +}) + +/** @internal */ +export const matchSTM = dual< + ( + options: { + readonly onFailure: (e: E) => STM.STM + readonly onSuccess: (a: A) => STM.STM + } + ) => (self: STM.STM) => STM.STM, + ( + self: STM.STM, + options: { + readonly onFailure: (e: E) => STM.STM + readonly onSuccess: (a: A) => STM.STM + } + ) => STM.STM +>(2, ( + self: STM.STM, + { onFailure, onSuccess }: { + readonly onFailure: (e: E) => STM.STM + readonly onSuccess: (a: A) => STM.STM + } +): STM.STM => + pipe( + self, + map(Either.right), + catchAll((e) => pipe(onFailure(e), map(Either.left))), + flatMap((either): STM.STM => { + switch (either._tag) { + case "Left": { + return succeed(either.left) + } + case "Right": { + return onSuccess(either.right) + } + } + }) + )) + +/** @internal */ +export const withSTMRuntime = ( + f: (runtime: STMDriver) => STM.STM +): STM.STM => { + const stm = new STMPrimitive(OpCodes.OP_WITH_STM_RUNTIME) + stm.effect_instruction_i1 = f + return stm +} + +/** @internal */ +export const interrupt: STM.STM = withSTMRuntime((_) => { + const stm = new STMPrimitive(OpCodes.OP_INTERRUPT) + stm.effect_instruction_i1 = _.fiberId + return stm as any +}) + +/** @internal */ +export const interruptAs = (fiberId: FiberId.FiberId): STM.STM => { + const stm = new STMPrimitive(OpCodes.OP_INTERRUPT) + stm.effect_instruction_i1 = fiberId + return stm as any +} + +/** @internal */ +export const map = dual< + (f: (a: A) => B) => (self: STM.STM) => STM.STM, + (self: STM.STM, f: (a: A) => B) => STM.STM +>(2, (self, f) => pipe(self, flatMap((a) => sync(() => f(a))))) + +/** @internal */ +export const orTry = dual< + ( + that: LazyArg> + ) => ( + self: STM.STM + ) => STM.STM, + ( + self: STM.STM, + that: LazyArg> + ) => STM.STM +>(2, (self, that) => { + const stm = new STMPrimitive(OpCodes.OP_ON_RETRY) + stm.effect_instruction_i1 = self + stm.effect_instruction_i2 = that + return stm +}) + +/** @internal */ +export const retry: STM.STM = new STMPrimitive(OpCodes.OP_RETRY) + +/** @internal */ +export const succeed = (value: A): STM.STM => { + const stm = new STMPrimitive(OpCodes.OP_SUCCEED) + stm.effect_instruction_i1 = value + return stm as any +} + +/** @internal */ +export const sync = (evaluate: () => A): STM.STM => { + const stm = new STMPrimitive(OpCodes.OP_SYNC) + stm.effect_instruction_i1 = evaluate + return stm as any +} + +/** @internal */ +export const zip = dual< + ( + that: STM.STM + ) => ( + self: STM.STM + ) => STM.STM<[A, A1], E1 | E, R1 | R>, + ( + self: STM.STM, + that: STM.STM + ) => STM.STM<[A, A1], E1 | E, R1 | R> +>(2, (self, that) => pipe(self, zipWith(that, (a, a1) => [a, a1]))) + +/** @internal */ +export const zipLeft = dual< + (that: STM.STM) => (self: STM.STM) => STM.STM, + (self: STM.STM, that: STM.STM) => STM.STM +>(2, (self, that) => pipe(self, flatMap((a) => pipe(that, map(() => a))))) + +/** @internal */ +export const zipRight = dual< + (that: STM.STM) => (self: STM.STM) => STM.STM, + (self: STM.STM, that: STM.STM) => STM.STM +>(2, (self, that) => pipe(self, flatMap(() => that))) + +/** @internal */ +export const zipWith = dual< + ( + that: STM.STM, + f: (a: A, b: A1) => A2 + ) => ( + self: STM.STM + ) => STM.STM, + ( + self: STM.STM, + that: STM.STM, + f: (a: A, b: A1) => A2 + ) => STM.STM +>( + 3, + (self, that, f) => pipe(self, flatMap((a) => pipe(that, map((b) => f(a, b))))) +) diff --git a/repos/effect/packages/effect/src/internal/stm/entry.ts b/repos/effect/packages/effect/src/internal/stm/entry.ts new file mode 100644 index 0000000..82aab45 --- /dev/null +++ b/repos/effect/packages/effect/src/internal/stm/entry.ts @@ -0,0 +1,59 @@ +import type * as TRef from "../../TRef.js" +import * as Versioned from "./versioned.js" + +/** @internal */ +export interface Entry { + readonly ref: TRef.TRef + readonly expected: Versioned.Versioned + isChanged: boolean // mutable by design + readonly isNew: boolean + newValue: any // mutable by design +} + +/** @internal */ +export const make = (ref: TRef.TRef, isNew: boolean): Entry => ({ + ref, + isNew, + isChanged: false, + expected: ref.versioned, + newValue: ref.versioned.value +}) + +export const unsafeGet = (self: Entry): unknown => { + return self.newValue +} + +/** @internal */ +export const unsafeSet = (self: Entry, value: unknown): void => { + self.isChanged = true + self.newValue = value +} + +/** @internal */ +export const commit = (self: Entry): void => { + self.ref.versioned = new Versioned.Versioned(self.newValue) +} + +/** @internal */ +export const copy = (self: Entry): Entry => ({ + ref: self.ref, + isNew: self.isNew, + isChanged: self.isChanged, + expected: self.expected, + newValue: self.newValue +}) + +/** @internal */ +export const isValid = (self: Entry): boolean => { + return self.ref.versioned === self.expected +} + +/** @internal */ +export const isInvalid = (self: Entry): boolean => { + return self.ref.versioned !== self.expected +} + +/** @internal */ +export const isChanged = (self: Entry): boolean => { + return self.isChanged +} diff --git a/repos/effect/packages/effect/src/internal/stm/journal.ts b/repos/effect/packages/effect/src/internal/stm/journal.ts new file mode 100644 index 0000000..6226d9a --- /dev/null +++ b/repos/effect/packages/effect/src/internal/stm/journal.ts @@ -0,0 +1,123 @@ +import type * as TRef from "../../TRef.js" +import * as Entry from "./entry.js" +import type * as TxnId from "./txnId.js" + +/** @internal */ +export type Journal = Map, Entry.Entry> + +/** @internal */ +export type Todo = () => unknown + +/** @internal */ +export type JournalAnalysis = JournalAnalysisInvalid | JournalAnalysisReadWrite | JournalAnalysisReadOnly + +/** @internal */ +export const JournalAnalysisInvalid = "Invalid" as const + +/** @internal */ +export type JournalAnalysisInvalid = typeof JournalAnalysisInvalid + +/** @internal */ +export const JournalAnalysisReadWrite = "ReadWrite" as const + +/** @internal */ +export type JournalAnalysisReadWrite = typeof JournalAnalysisReadWrite + +/** @internal */ +export const JournalAnalysisReadOnly = "ReadOnly" as const + +/** @internal */ +export type JournalAnalysisReadOnly = typeof JournalAnalysisReadOnly + +/** @internal */ +export const commitJournal = (journal: Journal) => { + for (const entry of journal) { + Entry.commit(entry[1]) + } +} + +/** + * Analyzes the journal, determining whether it is valid and whether it is + * read only in a single pass. Note that information on whether the + * journal is read only will only be accurate if the journal is valid, due + * to short-circuiting that occurs on an invalid journal. + * + * @internal + */ +export const analyzeJournal = (journal: Journal): JournalAnalysis => { + let val: JournalAnalysis = JournalAnalysisReadOnly + for (const [, entry] of journal) { + val = Entry.isInvalid(entry) ? JournalAnalysisInvalid : Entry.isChanged(entry) ? JournalAnalysisReadWrite : val + if (val === JournalAnalysisInvalid) { + return val + } + } + return val +} + +/** @internal */ +export const prepareResetJournal = (journal: Journal): () => void => { + const saved: Journal = new Map, Entry.Entry>() + for (const entry of journal) { + saved.set(entry[0], Entry.copy(entry[1])) + } + return () => { + journal.clear() + for (const entry of saved) { + journal.set(entry[0], entry[1]) + } + } +} + +/** @internal */ +export const collectTodos = (journal: Journal): Map => { + const allTodos: Map = new Map() + for (const [, entry] of journal) { + for (const todo of entry.ref.todos) { + allTodos.set(todo[0], todo[1]) + } + entry.ref.todos = new Map() + } + return allTodos +} + +/** @internal */ +export const execTodos = (todos: Map) => { + const todosSorted = Array.from(todos.entries()).sort((x, y) => x[0] - y[0]) + for (const [_, todo] of todosSorted) { + todo() + } +} + +/** @internal */ +export const addTodo = ( + txnId: TxnId.TxnId, + journal: Journal, + todoEffect: Todo +): boolean => { + let added = false + for (const [, entry] of journal) { + if (!entry.ref.todos.has(txnId)) { + entry.ref.todos.set(txnId, todoEffect) + added = true + } + } + return added +} + +/** @internal */ +export const isValid = (journal: Journal): boolean => { + let valid = true + for (const [, entry] of journal) { + valid = Entry.isValid(entry) + if (!valid) { + return valid + } + } + return valid +} + +/** @internal */ +export const isInvalid = (journal: Journal): boolean => { + return !isValid(journal) +} diff --git a/repos/effect/packages/effect/src/internal/stm/opCodes/stm.ts b/repos/effect/packages/effect/src/internal/stm/opCodes/stm.ts new file mode 100644 index 0000000..afe4163 --- /dev/null +++ b/repos/effect/packages/effect/src/internal/stm/opCodes/stm.ts @@ -0,0 +1,71 @@ +/** @internal */ +export const OP_WITH_STM_RUNTIME = "WithSTMRuntime" as const + +/** @internal */ +export type OP_WITH_STM_RUNTIME = typeof OP_WITH_STM_RUNTIME + +/** @internal */ +export const OP_ON_FAILURE = "OnFailure" as const + +/** @internal */ +export type OP_ON_FAILURE = typeof OP_ON_FAILURE + +/** @internal */ +export const OP_ON_RETRY = "OnRetry" as const + +/** @internal */ +export type OP_ON_RETRY = typeof OP_ON_RETRY + +/** @internal */ +export const OP_ON_SUCCESS = "OnSuccess" as const + +/** @internal */ +export type OP_ON_SUCCESS = typeof OP_ON_SUCCESS + +/** @internal */ +export const OP_PROVIDE = "Provide" as const + +/** @internal */ +export type OP_PROVIDE = typeof OP_PROVIDE + +/** @internal */ +export const OP_SYNC = "Sync" as const + +/** @internal */ +export type OP_SYNC = typeof OP_SYNC + +/** @internal */ +export const OP_SUCCEED = "Succeed" as const + +/** @internal */ +export type OP_SUCCEED = typeof OP_SUCCEED + +/** @internal */ +export const OP_RETRY = "Retry" as const + +/** @internal */ +export type OP_RETRY = typeof OP_RETRY + +/** @internal */ +export const OP_FAIL = "Fail" as const + +/** @internal */ +export type OP_FAIL = typeof OP_FAIL + +/** @internal */ +export const OP_DIE = "Die" as const + +/** @internal */ +export type OP_DIE = typeof OP_DIE + +/** @internal */ +export const OP_INTERRUPT = "Interrupt" as const + +/** @internal */ +export type OP_INTERRUPT = typeof OP_INTERRUPT + +/** @internal */ +export const OP_TRACED = "Traced" as const + +/** @internal */ +export type OP_TRACED = typeof OP_TRACED diff --git a/repos/effect/packages/effect/src/internal/stm/opCodes/stmState.ts b/repos/effect/packages/effect/src/internal/stm/opCodes/stmState.ts new file mode 100644 index 0000000..ffdd5e0 --- /dev/null +++ b/repos/effect/packages/effect/src/internal/stm/opCodes/stmState.ts @@ -0,0 +1,17 @@ +/** @internal */ +export const OP_DONE = "Done" as const + +/** @internal */ +export type OP_DONE = typeof OP_DONE + +/** @internal */ +export const OP_INTERRUPTED = "Interrupted" as const + +/** @internal */ +export type OP_INTERRUPTED = typeof OP_INTERRUPTED + +/** @internal */ +export const OP_RUNNING = "Running" as const + +/** @internal */ +export type OP_RUNNING = typeof OP_RUNNING diff --git a/repos/effect/packages/effect/src/internal/stm/opCodes/strategy.ts b/repos/effect/packages/effect/src/internal/stm/opCodes/strategy.ts new file mode 100644 index 0000000..3b260a5 --- /dev/null +++ b/repos/effect/packages/effect/src/internal/stm/opCodes/strategy.ts @@ -0,0 +1,17 @@ +/** @internal */ +export const OP_BACKPRESSURE_STRATEGY = "BackPressure" as const + +/** @internal */ +export type OP_BACKPRESSURE_STRATEGY = typeof OP_BACKPRESSURE_STRATEGY + +/** @internal */ +export const OP_DROPPING_STRATEGY = "Dropping" as const + +/** @internal */ +export type OP_DROPPING_STRATEGY = typeof OP_DROPPING_STRATEGY + +/** @internal */ +export const OP_SLIDING_STRATEGY = "Sliding" as const + +/** @internal */ +export type OP_SLIDING_STRATEGY = typeof OP_SLIDING_STRATEGY diff --git a/repos/effect/packages/effect/src/internal/stm/opCodes/tExit.ts b/repos/effect/packages/effect/src/internal/stm/opCodes/tExit.ts new file mode 100644 index 0000000..b700d4b --- /dev/null +++ b/repos/effect/packages/effect/src/internal/stm/opCodes/tExit.ts @@ -0,0 +1,29 @@ +/** @internal */ +export const OP_FAIL = "Fail" as const + +/** @internal */ +export type OP_FAIL = typeof OP_FAIL + +/** @internal */ +export const OP_DIE = "Die" as const + +/** @internal */ +export type OP_DIE = typeof OP_DIE + +/** @internal */ +export const OP_INTERRUPT = "Interrupt" as const + +/** @internal */ +export type OP_INTERRUPT = typeof OP_INTERRUPT + +/** @internal */ +export const OP_SUCCEED = "Succeed" as const + +/** @internal */ +export type OP_SUCCEED = typeof OP_SUCCEED + +/** @internal */ +export const OP_RETRY = "Retry" as const + +/** @internal */ +export type OP_RETRY = typeof OP_RETRY diff --git a/repos/effect/packages/effect/src/internal/stm/opCodes/tryCommit.ts b/repos/effect/packages/effect/src/internal/stm/opCodes/tryCommit.ts new file mode 100644 index 0000000..b5a1bdd --- /dev/null +++ b/repos/effect/packages/effect/src/internal/stm/opCodes/tryCommit.ts @@ -0,0 +1,11 @@ +/** @internal */ +export const OP_DONE = "Done" as const + +/** @internal */ +export type OP_DONE = typeof OP_DONE + +/** @internal */ +export const OP_SUSPEND = "Suspend" as const + +/** @internal */ +export type OP_SUSPEND = typeof OP_SUSPEND diff --git a/repos/effect/packages/effect/src/internal/stm/stm.ts b/repos/effect/packages/effect/src/internal/stm/stm.ts new file mode 100644 index 0000000..42ac738 --- /dev/null +++ b/repos/effect/packages/effect/src/internal/stm/stm.ts @@ -0,0 +1,1453 @@ +import * as RA from "../../Array.js" +import * as Cause from "../../Cause.js" +import * as Chunk from "../../Chunk.js" +import * as Context from "../../Context.js" +import * as Effect from "../../Effect.js" +import * as Either from "../../Either.js" +import * as Exit from "../../Exit.js" +import type * as FiberId from "../../FiberId.js" +import type { LazyArg } from "../../Function.js" +import { constFalse, constTrue, constVoid, dual, identity, pipe } from "../../Function.js" +import * as Option from "../../Option.js" +import type { Predicate, Refinement } from "../../Predicate.js" +import * as predicate from "../../Predicate.js" +import type * as STM from "../../STM.js" +import type * as Types from "../../Types.js" +import { yieldWrapGet } from "../../Utils.js" +import * as effectCore from "../core.js" +import * as core from "./core.js" +import * as Journal from "./journal.js" +import * as STMState from "./stmState.js" + +/** @internal */ +export const acquireUseRelease = dual< + ( + use: (resource: A) => STM.STM, + release: (resource: A) => STM.STM + ) => ( + acquire: STM.STM + ) => Effect.Effect, + ( + acquire: STM.STM, + use: (resource: A) => STM.STM, + release: (resource: A) => STM.STM + ) => Effect.Effect +>(3, ( + acquire: STM.STM, + use: (resource: A) => STM.STM, + release: (resource: A) => STM.STM +): Effect.Effect => + Effect.uninterruptibleMask((restore) => { + let state: STMState.STMState = STMState.running + return pipe( + restore( + core.unsafeAtomically( + acquire, + (exit) => { + state = STMState.done(exit) + }, + () => { + state = STMState.interrupted + } + ) + ), + Effect.matchCauseEffect({ + onFailure: (cause) => { + if (STMState.isDone(state) && Exit.isSuccess(state.exit)) { + return pipe( + release(state.exit.value), + Effect.matchCauseEffect({ + onFailure: (cause2) => Effect.failCause(Cause.parallel(cause, cause2)), + onSuccess: () => Effect.failCause(cause) + }) + ) + } + return Effect.failCause(cause) + }, + onSuccess: (a) => + pipe( + restore(use(a)), + Effect.matchCauseEffect({ + onFailure: (cause) => + pipe( + release(a), + Effect.matchCauseEffect({ + onFailure: (cause2) => Effect.failCause(Cause.parallel(cause, cause2)), + onSuccess: () => Effect.failCause(cause) + }) + ), + onSuccess: (a2) => pipe(release(a), Effect.as(a2)) + }) + ) + }) + ) + })) + +/** @internal */ +export const as = dual< + (value: A2) => (self: STM.STM) => STM.STM, + (self: STM.STM, value: A2) => STM.STM +>(2, (self, value) => pipe(self, core.map(() => value))) + +/** @internal */ +export const asSome = (self: STM.STM): STM.STM, E, R> => + pipe(self, core.map(Option.some)) + +/** @internal */ +export const asSomeError = (self: STM.STM): STM.STM, R> => + pipe(self, mapError(Option.some)) + +/** @internal */ +export const asVoid = (self: STM.STM): STM.STM => pipe(self, core.map(constVoid)) + +/** @internal */ +export const attempt = (evaluate: LazyArg): STM.STM => + suspend(() => { + try { + return core.succeed(evaluate()) + } catch (defect) { + return core.fail(defect) + } + }) + +export const bind = dual< + ( + tag: Exclude, + f: (_: K) => STM.STM + ) => (self: STM.STM) => STM.STM, E | E2, R | R2>, + ( + self: STM.STM, + tag: Exclude, + f: (_: K) => STM.STM + ) => STM.STM, E | E2, R | R2> +>(3, ( + self: STM.STM, + tag: Exclude, + f: (_: K) => STM.STM +) => + core.flatMap(self, (k) => + core.map( + f(k), + (a): Types.MergeRecord => ({ ...k, [tag]: a } as any) + ))) + +/* @internal */ +export const bindTo = dual< + (tag: N) => (self: STM.STM) => STM.STM< + Record, + E, + R + >, + ( + self: STM.STM, + tag: N + ) => STM.STM< + Record, + E, + R + > +>( + 2, + (self: STM.STM, tag: N): STM.STM, E, R> => + core.map(self, (a) => ({ [tag]: a } as Record)) +) + +/* @internal */ +export const let_ = dual< + ( + tag: Exclude, + f: (_: K) => A + ) => (self: STM.STM) => STM.STM< + Types.MergeRecord, + E, + R + >, + ( + self: STM.STM, + tag: Exclude, + f: (_: K) => A + ) => STM.STM< + Types.MergeRecord, + E, + R + > +>(3, (self: STM.STM, tag: Exclude, f: (_: K) => A) => + core.map( + self, + (k): Types.MergeRecord => ({ ...k, [tag]: f(k) } as any) + )) + +/** @internal */ +export const catchSome = dual< + ( + pf: (error: E) => Option.Option> + ) => ( + self: STM.STM + ) => STM.STM, + ( + self: STM.STM, + pf: (error: E) => Option.Option> + ) => STM.STM +>(2, ( + self: STM.STM, + pf: (error: E) => Option.Option> +): STM.STM => + core.catchAll( + self, + (e): STM.STM => Option.getOrElse(pf(e), () => core.fail(e)) + )) + +/** @internal */ +export const catchTag = dual< + ( + k: K, + f: (e: Extract) => STM.STM + ) => (self: STM.STM) => STM.STM | E1, R | R1>, + ( + self: STM.STM, + k: K, + f: (e: Extract) => STM.STM + ) => STM.STM | E1, R | R1> +>(3, (self, k, f) => + core.catchAll(self, (e) => { + if ("_tag" in e && e["_tag"] === k) { + return f(e as any) + } + return core.fail(e as any) + })) + +/** @internal */ +export const catchTags: { + < + E extends { _tag: string }, + Cases extends { + [K in E["_tag"]]+?: (error: Extract) => STM.STM + } + >( + cases: Cases + ): (self: STM.STM) => STM.STM< + | A + | { + [K in keyof Cases]: Cases[K] extends ((...args: Array) => STM.STM) ? A : never + }[keyof Cases], + | Exclude + | { + [K in keyof Cases]: Cases[K] extends ((...args: Array) => STM.STM) ? E : never + }[keyof Cases], + | R + | { + [K in keyof Cases]: Cases[K] extends ((...args: Array) => STM.STM) ? R : never + }[keyof Cases] + > + < + R, + E extends { _tag: string }, + A, + Cases extends { + [K in E["_tag"]]+?: (error: Extract) => STM.STM + } + >( + self: STM.STM, + cases: Cases + ): STM.STM< + | A + | { + [K in keyof Cases]: Cases[K] extends ((...args: Array) => STM.STM) ? A : never + }[keyof Cases], + | Exclude + | { + [K in keyof Cases]: Cases[K] extends ((...args: Array) => STM.STM) ? E : never + }[keyof Cases], + | R + | { + [K in keyof Cases]: Cases[K] extends ((...args: Array) => STM.STM) ? R : never + }[keyof Cases] + > +} = dual(2, (self, cases) => + core.catchAll(self, (e: any) => { + const keys = Object.keys(cases) + if ("_tag" in e && keys.includes(e["_tag"])) { + return cases[e["_tag"]](e as any) + } + return core.fail(e as any) + })) + +/** @internal */ +export const check = (predicate: LazyArg): STM.STM => suspend(() => predicate() ? void_ : core.retry) + +/** @internal */ +export const collect = dual< + (pf: (a: A) => Option.Option) => (self: STM.STM) => STM.STM, + (self: STM.STM, pf: (a: A) => Option.Option) => STM.STM +>(2, (self, pf) => + collectSTM( + self, + (a) => Option.map(pf(a), core.succeed) + )) + +/** @internal */ +export const collectSTM = dual< + ( + pf: (a: A) => Option.Option> + ) => ( + self: STM.STM + ) => STM.STM, + ( + self: STM.STM, + pf: (a: A) => Option.Option> + ) => STM.STM +>(2, (self, pf) => + core.matchSTM(self, { + onFailure: core.fail, + onSuccess: (a) => { + const option = pf(a) + return Option.isSome(option) ? option.value : core.retry + } + })) + +/** @internal */ +export const commitEither = (self: STM.STM): Effect.Effect => + Effect.flatten(core.commit(either(self))) + +/** @internal */ +export const cond = ( + predicate: LazyArg, + error: LazyArg, + result: LazyArg +): STM.STM => { + return suspend( + () => predicate() ? core.sync(result) : core.failSync(error) + ) +} + +/** @internal */ +export const either = (self: STM.STM): STM.STM, never, R> => + match(self, { onFailure: Either.left, onSuccess: Either.right }) + +/** @internal */ +export const eventually = (self: STM.STM): STM.STM => + core.matchSTM(self, { onFailure: () => eventually(self), onSuccess: core.succeed }) + +/** @internal */ +export const every = dual< + ( + predicate: (a: Types.NoInfer) => STM.STM + ) => (iterable: Iterable) => STM.STM, + (iterable: Iterable, predicate: (a: A) => STM.STM) => STM.STM +>( + 2, + ( + iterable: Iterable, + predicate: (a: A) => STM.STM + ): STM.STM => + core.flatMap(core.sync(() => iterable[Symbol.iterator]()), (iterator) => { + const loop: STM.STM = suspend(() => { + const next = iterator.next() + if (next.done) { + return core.succeed(true) + } + return pipe( + predicate(next.value), + core.flatMap((bool) => bool ? loop : core.succeed(bool)) + ) + }) + return loop + }) +) + +/** @internal */ +export const exists = dual< + ( + predicate: (a: Types.NoInfer) => STM.STM + ) => (iterable: Iterable) => STM.STM, + (iterable: Iterable, predicate: (a: A) => STM.STM) => STM.STM +>( + 2, + (iterable: Iterable, predicate: (a: A) => STM.STM): STM.STM => + core.flatMap(core.sync(() => iterable[Symbol.iterator]()), (iterator) => { + const loop: STM.STM = suspend(() => { + const next = iterator.next() + if (next.done) { + return core.succeed(false) + } + return core.flatMap( + predicate(next.value), + (bool) => bool ? core.succeed(bool) : loop + ) + }) + return loop + }) +) + +/** @internal */ +export const fiberId: STM.STM = core.effect((_, fiberId) => fiberId) + +/** @internal */ +export const filter = dual< + ( + predicate: (a: Types.NoInfer) => STM.STM + ) => (iterable: Iterable) => STM.STM, E, R>, + (iterable: Iterable, predicate: (a: A) => STM.STM) => STM.STM, E, R> +>( + 2, + (iterable: Iterable, predicate: (a: A) => STM.STM): STM.STM, E, R> => + Array.from(iterable).reduce( + (acc, curr) => + pipe( + acc, + core.zipWith(predicate(curr), (as, p) => { + if (p) { + as.push(curr) + return as + } + return as + }) + ), + core.succeed([]) as STM.STM, E, R> + ) +) + +/** @internal */ +export const filterNot = dual< + ( + predicate: (a: Types.NoInfer) => STM.STM + ) => (iterable: Iterable) => STM.STM, E, R>, + (iterable: Iterable, predicate: (a: A) => STM.STM) => STM.STM, E, R> +>( + 2, + (iterable: Iterable, predicate: (a: A) => STM.STM): STM.STM, E, R> => + filter(iterable, (a) => negate(predicate(a))) +) + +/** @internal */ +export const filterOrDie: { + ( + refinement: Refinement, B>, + defect: LazyArg + ): (self: STM.STM) => STM.STM + ( + predicate: Predicate>, + defect: LazyArg + ): (self: STM.STM) => STM.STM + ( + self: STM.STM, + refinement: Refinement, + defect: LazyArg + ): STM.STM + (self: STM.STM, predicate: Predicate, defect: LazyArg): STM.STM +} = dual( + 3, + (self: STM.STM, predicate: Predicate, defect: LazyArg): STM.STM => + filterOrElse(self, predicate, () => core.dieSync(defect)) +) + +/** @internal */ +export const filterOrDieMessage: { + ( + refinement: Refinement, B>, + message: string + ): (self: STM.STM) => STM.STM + (predicate: Predicate>, message: string): (self: STM.STM) => STM.STM + (self: STM.STM, refinement: Refinement, message: string): STM.STM + (self: STM.STM, predicate: Predicate, message: string): STM.STM +} = dual( + 3, + (self: STM.STM, predicate: Predicate, message: string): STM.STM => + filterOrElse(self, predicate, () => core.dieMessage(message)) +) + +/** @internal */ +export const filterOrElse: { + ( + refinement: Refinement, B>, + orElse: (a: Types.NoInfer) => STM.STM + ): (self: STM.STM) => STM.STM + ( + predicate: Predicate>, + orElse: (a: Types.NoInfer) => STM.STM + ): (self: STM.STM) => STM.STM + ( + self: STM.STM, + refinement: Refinement, + orElse: (a: A) => STM.STM + ): STM.STM + ( + self: STM.STM, + predicate: Predicate, + orElse: (a: A) => STM.STM + ): STM.STM +} = dual( + 3, + ( + self: STM.STM, + predicate: Predicate, + orElse: (a: A) => STM.STM + ): STM.STM => + core.flatMap(self, (a): STM.STM => predicate(a) ? core.succeed(a) : orElse(a)) +) + +/** @internal */ +export const filterOrFail: { + ( + refinement: Refinement, B>, + orFailWith: (a: Types.NoInfer) => E2 + ): (self: STM.STM) => STM.STM + ( + predicate: Predicate>, + orFailWith: (a: Types.NoInfer) => E2 + ): (self: STM.STM) => STM.STM + ( + self: STM.STM, + refinement: Refinement, + orFailWith: (a: A) => E2 + ): STM.STM + (self: STM.STM, predicate: Predicate, orFailWith: (a: A) => E2): STM.STM +} = dual( + 3, + (self: STM.STM, predicate: Predicate, orFailWith: (a: A) => E2): STM.STM => + filterOrElse( + self, + predicate, + (a) => core.failSync(() => orFailWith(a)) + ) +) + +/** @internal */ +export const flatten = (self: STM.STM, E, R>): STM.STM => + core.flatMap(self, identity) + +/** @internal */ +export const flip = (self: STM.STM): STM.STM => + core.matchSTM(self, { onFailure: core.succeed, onSuccess: core.fail }) + +/** @internal */ +export const flipWith = dual< + ( + f: (stm: STM.STM) => STM.STM + ) => ( + self: STM.STM + ) => STM.STM, + ( + self: STM.STM, + f: (stm: STM.STM) => STM.STM + ) => STM.STM +>(2, (self, f) => flip(f(flip(self)))) + +/** @internal */ +export const match = dual< + (options: { + readonly onFailure: (error: E) => A2 + readonly onSuccess: (value: A) => A3 + }) => (self: STM.STM) => STM.STM, + (self: STM.STM, options: { + readonly onFailure: (error: E) => A2 + readonly onSuccess: (value: A) => A3 + }) => STM.STM +>(2, (self, { onFailure, onSuccess }) => + core.matchSTM(self, { + onFailure: (e) => core.succeed(onFailure(e)), + onSuccess: (a) => core.succeed(onSuccess(a)) + })) + +/** @internal */ +export const forEach = dual< + { + (f: (a: A) => STM.STM, options?: { + readonly discard?: false | undefined + }): (elements: Iterable) => STM.STM, E, R> + (f: (a: A) => STM.STM, options: { + readonly discard: true + }): (elements: Iterable) => STM.STM + }, + { + (elements: Iterable, f: (a: A) => STM.STM, options?: { + readonly discard?: false | undefined + }): STM.STM, E, R> + (elements: Iterable, f: (a: A) => STM.STM, options: { + readonly discard: true + }): STM.STM + } +>( + (args) => predicate.isIterable(args[0]), + (iterable: Iterable, f: (a: A) => STM.STM, options?: { + readonly discard?: boolean | undefined + }): STM.STM => { + if (options?.discard) { + return pipe( + core.sync(() => iterable[Symbol.iterator]()), + core.flatMap((iterator) => { + const loop: STM.STM = suspend(() => { + const next = iterator.next() + if (next.done) { + return void_ + } + return pipe(f(next.value), core.flatMap(() => loop)) + }) + return loop + }) + ) + } + + return suspend(() => + RA.fromIterable(iterable).reduce( + (acc, curr) => + core.zipWith(acc, f(curr), (array, elem) => { + array.push(elem) + return array + }), + core.succeed([]) as STM.STM, E, R> + ) + ) + } +) + +/** @internal */ +export const fromEither = (either: Either.Either): STM.STM => { + switch (either._tag) { + case "Left": { + return core.fail(either.left) + } + case "Right": { + return core.succeed(either.right) + } + } +} + +/** @internal */ +export const fromOption = (option: Option.Option): STM.STM> => + Option.match(option, { + onNone: () => core.fail(Option.none()), + onSome: core.succeed + }) + +/** + * Inspired by https://github.com/tusharmath/qio/pull/22 (revised) + * @internal + */ +export const gen: typeof STM.gen = (...args) => + suspend(() => { + const f = (args.length === 1) + ? args[0] + : args[1].bind(args[0]) + const iterator = f(pipe) + const state = iterator.next() + const run = ( + state: IteratorYieldResult | IteratorReturnResult + ): STM.STM => + state.done ? + core.succeed(state.value) : + core.flatMap(yieldWrapGet(state.value) as any, (val: any) => run(iterator.next(val as never))) + return run(state) + }) + +/** @internal */ +export const head = (self: STM.STM, E, R>): STM.STM, R> => + pipe( + self, + core.matchSTM({ + onFailure: (e) => core.fail(Option.some(e)), + onSuccess: (a) => { + const i = a[Symbol.iterator]() + const res = i.next() + if (res.done) { + return core.fail(Option.none()) + } else { + return core.succeed(res.value) + } + } + }) + ) + +/** @internal */ +export const if_ = dual< + ( + options: { + readonly onTrue: STM.STM + readonly onFalse: STM.STM + } + ) => ( + self: STM.STM | boolean + ) => STM.STM, + { + ( + self: boolean, + options: { + readonly onTrue: STM.STM + readonly onFalse: STM.STM + } + ): STM.STM + ( + self: STM.STM, + options: { + readonly onTrue: STM.STM + readonly onFalse: STM.STM + } + ): STM.STM + } +>( + (args) => typeof args[0] === "boolean" || core.isSTM(args[0]), + ( + self: STM.STM | boolean, + { onFalse, onTrue }: { + readonly onTrue: STM.STM + readonly onFalse: STM.STM + } + ) => { + if (typeof self === "boolean") { + return self ? onTrue : onFalse + } + + return core.flatMap(self, (bool): STM.STM => bool ? onTrue : onFalse) + } +) + +/** @internal */ +export const ignore = (self: STM.STM): STM.STM => + match(self, { onFailure: () => void_, onSuccess: () => void_ }) + +/** @internal */ +export const isFailure = (self: STM.STM): STM.STM => + match(self, { onFailure: constTrue, onSuccess: constFalse }) + +/** @internal */ +export const isSuccess = (self: STM.STM): STM.STM => + match(self, { onFailure: constFalse, onSuccess: constTrue }) + +/** @internal */ +export const iterate = ( + initial: Z, + options: { + readonly while: (z: Z) => boolean + readonly body: (z: Z) => STM.STM + } +): STM.STM => iterateLoop(initial, options.while, options.body) + +const iterateLoop = ( + initial: Z, + cont: (z: Z) => boolean, + body: (z: Z) => STM.STM +): STM.STM => { + if (cont(initial)) { + return pipe( + body(initial), + core.flatMap((z) => iterateLoop(z, cont, body)) + ) + } + return core.succeed(initial) +} + +/** @internal */ +export const loop: { + ( + initial: Z, + options: { + readonly while: (z: Z) => boolean + readonly step: (z: Z) => Z + readonly body: (z: Z) => STM.STM + readonly discard?: false | undefined + } + ): STM.STM, E, R> + ( + initial: Z, + options: { + readonly while: (z: Z) => boolean + readonly step: (z: Z) => Z + readonly body: (z: Z) => STM.STM + readonly discard: true + } + ): STM.STM +} = ( + initial: Z, + options: { + readonly while: (z: Z) => boolean + readonly step: (z: Z) => Z + readonly body: (z: Z) => STM.STM + readonly discard?: boolean | undefined + } +): STM.STM => + options.discard ? + loopDiscardLoop(initial, options.while, options.step, options.body) : + core.map(loopLoop(initial, options.while, options.step, options.body), (a) => Array.from(a)) + +const loopLoop = ( + initial: Z, + cont: (z: Z) => boolean, + inc: (z: Z) => Z, + body: (z: Z) => STM.STM +): STM.STM, E, R> => { + if (cont(initial)) { + return pipe( + body(initial), + core.flatMap((a) => pipe(loopLoop(inc(initial), cont, inc, body), core.map(Chunk.append(a)))) + ) + } + return core.succeed(Chunk.empty()) +} + +const loopDiscardLoop = ( + initial: Z, + cont: (z: Z) => boolean, + inc: (z: Z) => Z, + body: (z: Z) => STM.STM +): STM.STM => { + if (cont(initial)) { + return pipe( + body(initial), + core.flatMap(() => loopDiscardLoop(inc(initial), cont, inc, body)) + ) + } + return void_ +} + +/** @internal */ +export const mapAttempt = dual< + (f: (a: A) => B) => (self: STM.STM) => STM.STM, + (self: STM.STM, f: (a: A) => B) => STM.STM +>(2, (self: STM.STM, f: (a: A) => B): STM.STM => + core.matchSTM(self, { + onFailure: (e) => core.fail(e), + onSuccess: (a) => attempt(() => f(a)) + })) + +/** @internal */ +export const mapBoth = dual< + (options: { + readonly onFailure: (error: E) => E2 + readonly onSuccess: (value: A) => A2 + }) => (self: STM.STM) => STM.STM, + (self: STM.STM, options: { + readonly onFailure: (error: E) => E2 + readonly onSuccess: (value: A) => A2 + }) => STM.STM +>(2, (self, { onFailure, onSuccess }) => + core.matchSTM(self, { + onFailure: (e) => core.fail(onFailure(e)), + onSuccess: (a) => core.succeed(onSuccess(a)) + })) + +/** @internal */ +export const mapError = dual< + (f: (error: E) => E2) => (self: STM.STM) => STM.STM, + (self: STM.STM, f: (error: E) => E2) => STM.STM +>(2, (self, f) => + core.matchSTM(self, { + onFailure: (e) => core.fail(f(e)), + onSuccess: core.succeed + })) + +/** @internal */ +export const merge = (self: STM.STM): STM.STM => + core.matchSTM(self, { onFailure: (e) => core.succeed(e), onSuccess: core.succeed }) + +/** @internal */ +export const mergeAll = dual< + (zero: A2, f: (a2: A2, a: A) => A2) => (iterable: Iterable>) => STM.STM, + (iterable: Iterable>, zero: A2, f: (a2: A2, a: A) => A2) => STM.STM +>( + 3, + (iterable: Iterable>, zero: A2, f: (a2: A2, a: A) => A2): STM.STM => + suspend(() => + Array.from(iterable).reduce( + (acc, curr) => pipe(acc, core.zipWith(curr, f)), + core.succeed(zero) as STM.STM + ) + ) +) + +/** @internal */ +export const negate = (self: STM.STM): STM.STM => pipe(self, core.map((b) => !b)) + +/** @internal */ +export const none = (self: STM.STM, E, R>): STM.STM, R> => + core.matchSTM(self, { + onFailure: (e) => core.fail(Option.some(e)), + onSuccess: Option.match({ + onNone: () => void_, + onSome: () => core.fail(Option.none()) + }) + }) + +/** @internal */ +export const option = (self: STM.STM): STM.STM, never, R> => + match(self, { onFailure: () => Option.none(), onSuccess: Option.some }) + +/** @internal */ +export const orDie = (self: STM.STM): STM.STM => pipe(self, orDieWith(identity)) + +/** @internal */ +export const orDieWith = dual< + (f: (error: E) => unknown) => (self: STM.STM) => STM.STM, + (self: STM.STM, f: (error: E) => unknown) => STM.STM +>(2, (self, f) => pipe(self, mapError(f), core.catchAll(core.die))) + +/** @internal */ +export const orElse = dual< + (that: LazyArg>) => (self: STM.STM) => STM.STM, + (self: STM.STM, that: LazyArg>) => STM.STM +>( + 2, + (self: STM.STM, that: LazyArg>): STM.STM => + core.flatMap(core.effect>((journal) => Journal.prepareResetJournal(journal)), (reset) => + pipe( + core.orTry(self, () => core.flatMap(core.sync(reset), that)), + core.catchAll(() => core.flatMap(core.sync(reset), that)) + )) +) + +/** @internal */ +export const orElseEither = dual< + ( + that: LazyArg> + ) => ( + self: STM.STM + ) => STM.STM, E2, R2 | R>, + ( + self: STM.STM, + that: LazyArg> + ) => STM.STM, E2, R2 | R> +>( + 2, + ( + self: STM.STM, + that: LazyArg> + ): STM.STM, E2, R2 | R> => + orElse(core.map(self, Either.left), () => core.map(that(), Either.right)) +) + +/** @internal */ +export const orElseFail = dual< + (error: LazyArg) => (self: STM.STM) => STM.STM, + (self: STM.STM, error: LazyArg) => STM.STM +>( + 2, + (self: STM.STM, error: LazyArg): STM.STM => + orElse(self, () => core.failSync(error)) +) + +/** @internal */ +export const orElseOptional = dual< + ( + that: LazyArg, R2>> + ) => ( + self: STM.STM, R> + ) => STM.STM, R2 | R>, + ( + self: STM.STM, R>, + that: LazyArg, R2>> + ) => STM.STM, R2 | R> +>( + 2, + ( + self: STM.STM, R>, + that: LazyArg, R2>> + ): STM.STM, R2 | R> => + core.catchAll( + self, + Option.match({ + onNone: that, + onSome: (e) => core.fail(Option.some(e)) + }) + ) +) + +/** @internal */ +export const orElseSucceed = dual< + (value: LazyArg) => (self: STM.STM) => STM.STM, + (self: STM.STM, value: LazyArg) => STM.STM +>( + 2, + (self: STM.STM, value: LazyArg): STM.STM => + orElse(self, () => core.sync(value)) +) + +/** @internal */ +export const provideContext = dual< + (env: Context.Context) => (self: STM.STM) => STM.STM, + (self: STM.STM, env: Context.Context) => STM.STM +>(2, (self, env) => core.mapInputContext(self, (_: Context.Context) => env)) + +/** @internal */ +export const provideSomeContext = dual< + (context: Context.Context) => (self: STM.STM) => STM.STM>, + (self: STM.STM, context: Context.Context) => STM.STM> +>(2, ( + self: STM.STM, + context: Context.Context +): STM.STM> => + core.mapInputContext( + self, + (parent: Context.Context>): Context.Context => Context.merge(parent, context) as any + )) + +/** @internal */ +export const provideService = dual< + ( + tag: Context.Tag, + resource: Types.NoInfer + ) => ( + self: STM.STM + ) => STM.STM>, + ( + self: STM.STM, + tag: Context.Tag, + resource: Types.NoInfer + ) => STM.STM> +>(3, (self, tag, resource) => provideServiceSTM(self, tag, core.succeed(resource))) + +/** @internal */ +export const provideServiceSTM = dual< + ( + tag: Context.Tag, + stm: STM.STM, E1, R1> + ) => ( + self: STM.STM + ) => STM.STM>, + ( + self: STM.STM, + tag: Context.Tag, + stm: STM.STM, E1, R1> + ) => STM.STM> +>(3, ( + self: STM.STM, + tag: Context.Tag, + stm: STM.STM, E1, R1> +): STM.STM> => + core.contextWithSTM((env: Context.Context>) => + core.flatMap( + stm, + (service) => + provideContext( + self, + Context.add(env, tag, service) as Context.Context + ) + ) + )) + +/** @internal */ +export const reduce = dual< + (zero: S, f: (s: S, a: A) => STM.STM) => (iterable: Iterable) => STM.STM, + (iterable: Iterable, zero: S, f: (s: S, a: A) => STM.STM) => STM.STM +>( + 3, + (iterable: Iterable, zero: S, f: (s: S, a: A) => STM.STM): STM.STM => + suspend(() => + Array.from(iterable).reduce( + (acc, curr) => pipe(acc, core.flatMap((s) => f(s, curr))), + core.succeed(zero) as STM.STM + ) + ) +) + +/** @internal */ +export const reduceAll = dual< + ( + initial: STM.STM, + f: (x: A, y: A) => A + ) => ( + iterable: Iterable> + ) => STM.STM, + ( + iterable: Iterable>, + initial: STM.STM, + f: (x: A, y: A) => A + ) => STM.STM +>(3, ( + iterable: Iterable>, + initial: STM.STM, + f: (x: A, y: A) => A +): STM.STM => + suspend(() => + Array.from(iterable).reduce( + (acc, curr) => pipe(acc, core.zipWith(curr, f)), + initial as STM.STM + ) + )) + +/** @internal */ +export const reduceRight = dual< + (zero: S, f: (s: S, a: A) => STM.STM) => (iterable: Iterable) => STM.STM, + (iterable: Iterable, zero: S, f: (s: S, a: A) => STM.STM) => STM.STM +>( + 3, + (iterable: Iterable, zero: S, f: (s: S, a: A) => STM.STM): STM.STM => + suspend(() => + Array.from(iterable).reduceRight( + (acc, curr) => pipe(acc, core.flatMap((s) => f(s, curr))), + core.succeed(zero) as STM.STM + ) + ) +) + +/** @internal */ +export const refineOrDie = dual< + (pf: (error: E) => Option.Option) => (self: STM.STM) => STM.STM, + (self: STM.STM, pf: (error: E) => Option.Option) => STM.STM +>(2, (self, pf) => refineOrDieWith(self, pf, identity)) + +/** @internal */ +export const refineOrDieWith = dual< + ( + pf: (error: E) => Option.Option, + f: (error: E) => unknown + ) => ( + self: STM.STM + ) => STM.STM, + ( + self: STM.STM, + pf: (error: E) => Option.Option, + f: (error: E) => unknown + ) => STM.STM +>(3, (self, pf, f) => + core.catchAll( + self, + (e) => + Option.match(pf(e), { + onNone: () => core.die(f(e)), + onSome: core.fail + }) + )) + +/** @internal */ +export const reject = dual< + (pf: (a: A) => Option.Option) => (self: STM.STM) => STM.STM, + (self: STM.STM, pf: (a: A) => Option.Option) => STM.STM +>(2, (self, pf) => + rejectSTM( + self, + (a) => Option.map(pf(a), core.fail) + )) + +/** @internal */ +export const rejectSTM = dual< + ( + pf: (a: A) => Option.Option> + ) => ( + self: STM.STM + ) => STM.STM, + ( + self: STM.STM, + pf: (a: A) => Option.Option> + ) => STM.STM +>(2, (self, pf) => + core.flatMap(self, (a) => + Option.match(pf(a), { + onNone: () => core.succeed(a), + onSome: core.flatMap(core.fail) + }))) + +/** @internal */ +export const repeatUntil = dual< + (predicate: Predicate) => (self: STM.STM) => STM.STM, + (self: STM.STM, predicate: Predicate) => STM.STM +>(2, (self, predicate) => repeatUntilLoop(self, predicate)) + +const repeatUntilLoop = (self: STM.STM, predicate: Predicate): STM.STM => + core.flatMap(self, (a) => + predicate(a) ? + core.succeed(a) : + repeatUntilLoop(self, predicate)) + +/** @internal */ +export const repeatWhile = dual< + (predicate: Predicate) => (self: STM.STM) => STM.STM, + (self: STM.STM, predicate: Predicate) => STM.STM +>(2, (self, predicate) => repeatWhileLoop(self, predicate)) + +const repeatWhileLoop = (self: STM.STM, predicate: Predicate): STM.STM => + core.flatMap(self, (a) => + predicate(a) ? + repeatWhileLoop(self, predicate) : + core.succeed(a)) + +/** @internal */ +export const replicate = dual< + (n: number) => (self: STM.STM) => Array>, + (self: STM.STM, n: number) => Array> +>(2, (self, n) => Array.from({ length: n }, () => self)) + +/** @internal */ +export const replicateSTM = dual< + (n: number) => (self: STM.STM) => STM.STM, E, R>, + (self: STM.STM, n: number) => STM.STM, E, R> +>(2, (self, n) => all(replicate(self, n))) + +/** @internal */ +export const replicateSTMDiscard = dual< + (n: number) => (self: STM.STM) => STM.STM, + (self: STM.STM, n: number) => STM.STM +>(2, (self, n) => all(replicate(self, n), { discard: true })) + +/** @internal */ +export const retryUntil = dual< + { + (refinement: Refinement, B>): (self: STM.STM) => STM.STM + (predicate: Predicate): (self: STM.STM) => STM.STM + }, + { + (self: STM.STM, refinement: Refinement): STM.STM + (self: STM.STM, predicate: Predicate): STM.STM + } +>( + 2, + (self: STM.STM, predicate: Predicate) => + core.matchSTM(self, { onFailure: core.fail, onSuccess: (a) => predicate(a) ? core.succeed(a) : core.retry }) +) + +/** @internal */ +export const retryWhile = dual< + (predicate: Predicate) => (self: STM.STM) => STM.STM, + (self: STM.STM, predicate: Predicate) => STM.STM +>( + 2, + (self, predicate) => + core.matchSTM(self, { onFailure: core.fail, onSuccess: (a) => !predicate(a) ? core.succeed(a) : core.retry }) +) + +/** @internal */ +export const partition = dual< + ( + f: (a: A) => STM.STM + ) => ( + elements: Iterable + ) => STM.STM<[excluded: Array, satisfying: Array], never, R>, + ( + elements: Iterable, + f: (a: A) => STM.STM + ) => STM.STM<[excluded: Array, satisfying: Array], never, R> +>(2, (elements, f) => + pipe( + forEach(elements, (a) => either(f(a))), + core.map((as) => effectCore.partitionMap(as, identity)) + )) + +/** @internal */ +export const some = (self: STM.STM, E, R>): STM.STM, R> => + core.matchSTM(self, { + onFailure: (e) => core.fail(Option.some(e)), + onSuccess: Option.match({ + onNone: () => core.fail(Option.none()), + onSome: core.succeed + }) + }) + +/* @internal */ +export const all = (( + input: Iterable | Record, + options?: STM.All.Options +): STM.STM => { + if (Symbol.iterator in input) { + return forEach(input, identity, options as any) + } else if (options?.discard) { + return forEach(Object.values(input), identity, options as any) + } + + return core.map( + forEach( + Object.entries(input), + ([_, e]) => core.map(e, (a) => [_, a] as const) + ), + (values) => { + const res = {} + for (const [k, v] of values) { + ;(res as any)[k] = v + } + return res + } + ) +}) as STM.All.Signature + +/** @internal */ +export const succeedNone: STM.STM> = core.succeed(Option.none()) + +/** @internal */ +export const succeedSome = (value: A): STM.STM> => core.succeed(Option.some(value)) + +/** @internal */ +export const summarized = dual< + ( + summary: STM.STM, + f: (before: A2, after: A2) => A3 + ) => ( + self: STM.STM + ) => STM.STM<[A3, A], E2 | E, R2 | R>, + ( + self: STM.STM, + summary: STM.STM, + f: (before: A2, after: A2) => A3 + ) => STM.STM<[A3, A], E2 | E, R2 | R> +>(3, (self, summary, f) => + core.flatMap(summary, (start) => + core.flatMap(self, (value) => + core.map( + summary, + (end) => [f(start, end), value] + )))) + +/** @internal */ +export const suspend = (evaluate: LazyArg>): STM.STM => flatten(core.sync(evaluate)) + +/** @internal */ +export const tap: { + (f: (a: A) => STM.STM): (self: STM.STM) => STM.STM + (self: STM.STM, f: (a: A) => STM.STM): STM.STM +} = dual( + 2, + (self: STM.STM, f: (a: A) => STM.STM): STM.STM => + core.flatMap(self, (a) => as(f(a), a)) +) + +/** @internal */ +export const tapBoth = dual< + ( + options: { + readonly onFailure: (error: XE) => STM.STM + readonly onSuccess: (value: XA) => STM.STM + } + ) => ( + self: STM.STM + ) => STM.STM, + ( + self: STM.STM, + options: { + readonly onFailure: (error: XE) => STM.STM + readonly onSuccess: (value: XA) => STM.STM + } + ) => STM.STM +>(2, (self, { onFailure, onSuccess }) => + core.matchSTM(self, { + onFailure: (e) => pipe(onFailure(e as any), core.zipRight(core.fail(e))), + onSuccess: (a) => pipe(onSuccess(a as any), as(a)) + })) + +/** @internal */ +export const tapError: { + ( + f: (error: Types.NoInfer) => STM.STM + ): (self: STM.STM) => STM.STM + (self: STM.STM, f: (error: E) => STM.STM): STM.STM +} = dual( + 2, + (self: STM.STM, f: (error: E) => STM.STM): STM.STM => + core.matchSTM(self, { + onFailure: (e) => core.zipRight(f(e), core.fail(e)), + onSuccess: core.succeed + }) +) + +/** @internal */ +export const try_: { + (options: { + readonly try: LazyArg + readonly catch: (u: unknown) => E + }): STM.STM + (try_: LazyArg): STM.STM +} = ( + arg: LazyArg | { + readonly try: LazyArg + readonly catch: (u: unknown) => E + } +) => { + const evaluate = typeof arg === "function" ? arg : arg.try + return suspend(() => { + try { + return core.succeed(evaluate()) + } catch (error) { + return core.fail("catch" in arg ? arg.catch(error) : error) + } + }) +} + +/** @internal */ +const void_: STM.STM = core.succeed(void 0) +export { + /** @internal */ + void_ as void +} + +/** @internal */ +export const unless = dual< + (predicate: LazyArg) => (self: STM.STM) => STM.STM, E, R>, + (self: STM.STM, predicate: LazyArg) => STM.STM, E, R> +>(2, (self, predicate) => + suspend( + () => predicate() ? succeedNone : asSome(self) + )) + +/** @internal */ +export const unlessSTM = dual< + ( + predicate: STM.STM + ) => ( + self: STM.STM + ) => STM.STM, E2 | E, R2 | R>, + ( + self: STM.STM, + predicate: STM.STM + ) => STM.STM, E2 | E, R2 | R> +>(2, (self, predicate) => + core.flatMap( + predicate, + (bool) => bool ? succeedNone : asSome(self) + )) + +/** @internal */ +export const unsome = (self: STM.STM, R>): STM.STM, E, R> => + core.matchSTM(self, { + onFailure: Option.match({ + onNone: () => core.succeed(Option.none()), + onSome: core.fail + }), + onSuccess: (a) => core.succeed(Option.some(a)) + }) + +/** @internal */ +export const validateAll = dual< + ( + f: (a: A) => STM.STM + ) => ( + elements: Iterable + ) => STM.STM, RA.NonEmptyArray, R>, + ( + elements: Iterable, + f: (a: A) => STM.STM + ) => STM.STM, RA.NonEmptyArray, R> +>( + 2, + (elements, f) => + core.flatMap(partition(elements, f), ([errors, values]) => + RA.isNonEmptyArray(errors) ? + core.fail(errors) : + core.succeed(values)) +) + +/** @internal */ +export const validateFirst = dual< + (f: (a: A) => STM.STM) => (elements: Iterable) => STM.STM, R>, + (elements: Iterable, f: (a: A) => STM.STM) => STM.STM, R> +>(2, (elements, f) => flip(forEach(elements, (a) => flip(f(a))))) + +/** @internal */ +export const when = dual< + (predicate: LazyArg) => (self: STM.STM) => STM.STM, E, R>, + (self: STM.STM, predicate: LazyArg) => STM.STM, E, R> +>(2, (self, predicate) => + suspend( + () => predicate() ? asSome(self) : succeedNone + )) + +/** @internal */ +export const whenSTM = dual< + ( + predicate: STM.STM + ) => ( + self: STM.STM + ) => STM.STM, E2 | E, R2 | R>, + ( + self: STM.STM, + predicate: STM.STM + ) => STM.STM, E2 | E, R2 | R> +>(2, (self, predicate) => + core.flatMap( + predicate, + (bool) => bool ? asSome(self) : succeedNone + )) diff --git a/repos/effect/packages/effect/src/internal/stm/stmState.ts b/repos/effect/packages/effect/src/internal/stm/stmState.ts new file mode 100644 index 0000000..ebe1b2d --- /dev/null +++ b/repos/effect/packages/effect/src/internal/stm/stmState.ts @@ -0,0 +1,136 @@ +import * as Equal from "../../Equal.js" +import * as Exit from "../../Exit.js" +import { pipe } from "../../Function.js" +import * as Hash from "../../Hash.js" +import { hasProperty } from "../../Predicate.js" +import * as OpCodes from "./opCodes/stmState.js" +import * as TExitOpCodes from "./opCodes/tExit.js" +import type * as TExit from "./tExit.js" + +/** @internal */ +const STMStateSymbolKey = "effect/STM/State" + +/** @internal */ +export const STMStateTypeId = Symbol.for(STMStateSymbolKey) + +/** @internal */ +export type STMStateTypeId = typeof STMStateTypeId + +/** @internal */ +export type STMState = Done | Interrupted | Running + +/** @internal */ +export interface Done extends Equal.Equal { + readonly [STMStateTypeId]: STMStateTypeId + readonly _tag: OpCodes.OP_DONE + readonly exit: Exit.Exit +} + +/** @internal */ +export interface Interrupted extends Equal.Equal { + readonly [STMStateTypeId]: STMStateTypeId + readonly _tag: OpCodes.OP_INTERRUPTED +} + +/** @internal */ +export interface Running extends Equal.Equal { + readonly [STMStateTypeId]: STMStateTypeId + readonly _tag: OpCodes.OP_RUNNING +} + +/** @internal */ +export const isSTMState = (u: unknown): u is STMState => hasProperty(u, STMStateTypeId) + +/** @internal */ +export const isRunning = (self: STMState): self is Running => { + return self._tag === OpCodes.OP_RUNNING +} + +/** @internal */ +export const isDone = (self: STMState): self is Done => { + return self._tag === OpCodes.OP_DONE +} + +/** @internal */ +export const isInterrupted = (self: STMState): self is Interrupted => { + return self._tag === OpCodes.OP_INTERRUPTED +} + +/** @internal */ +export const done = (exit: Exit.Exit): STMState => { + return { + [STMStateTypeId]: STMStateTypeId, + _tag: OpCodes.OP_DONE, + exit, + [Hash.symbol](): number { + return pipe( + Hash.hash(STMStateSymbolKey), + Hash.combine(Hash.hash(OpCodes.OP_DONE)), + Hash.combine(Hash.hash(exit)), + Hash.cached(this) + ) + }, + [Equal.symbol](that: unknown): boolean { + return isSTMState(that) && that._tag === OpCodes.OP_DONE && Equal.equals(exit, that.exit) + } + } +} + +const interruptedHash = pipe( + Hash.hash(STMStateSymbolKey), + Hash.combine(Hash.hash(OpCodes.OP_INTERRUPTED)), + Hash.combine(Hash.hash("interrupted")) +) + +/** @internal */ +export const interrupted: STMState = { + [STMStateTypeId]: STMStateTypeId, + _tag: OpCodes.OP_INTERRUPTED, + [Hash.symbol](): number { + return interruptedHash + }, + [Equal.symbol](that: unknown): boolean { + return isSTMState(that) && that._tag === OpCodes.OP_INTERRUPTED + } +} + +const runningHash = pipe( + Hash.hash(STMStateSymbolKey), + Hash.combine(Hash.hash(OpCodes.OP_RUNNING)), + Hash.combine(Hash.hash("running")) +) + +/** @internal */ +export const running: STMState = { + [STMStateTypeId]: STMStateTypeId, + _tag: OpCodes.OP_RUNNING, + [Hash.symbol](): number { + return runningHash + }, + [Equal.symbol](that: unknown): boolean { + return isSTMState(that) && that._tag === OpCodes.OP_RUNNING + } +} + +/** @internal */ +export const fromTExit = (tExit: TExit.TExit): STMState => { + switch (tExit._tag) { + case TExitOpCodes.OP_FAIL: { + return done(Exit.fail(tExit.error)) + } + case TExitOpCodes.OP_DIE: { + return done(Exit.die(tExit.defect)) + } + case TExitOpCodes.OP_INTERRUPT: { + return done(Exit.interrupt(tExit.fiberId)) + } + case TExitOpCodes.OP_SUCCEED: { + return done(Exit.succeed(tExit.value)) + } + case TExitOpCodes.OP_RETRY: { + throw new Error( + "BUG: STM.STMState.fromTExit - please report an issue at https://github.com/Effect-TS/effect/issues" + ) + } + } +} diff --git a/repos/effect/packages/effect/src/internal/stm/tArray.ts b/repos/effect/packages/effect/src/internal/stm/tArray.ts new file mode 100644 index 0000000..b3d292e --- /dev/null +++ b/repos/effect/packages/effect/src/internal/stm/tArray.ts @@ -0,0 +1,550 @@ +import * as Equal from "../../Equal.js" +import { dual, pipe } from "../../Function.js" +import * as Option from "../../Option.js" +import * as Order from "../../Order.js" +import type { Predicate } from "../../Predicate.js" +import type * as STM from "../../STM.js" +import type * as TArray from "../../TArray.js" +import type * as TRef from "../../TRef.js" +import * as core from "./core.js" +import * as stm from "./stm.js" +import * as tRef from "./tRef.js" + +/** @internal */ +const TArraySymbolKey = "effect/TArray" + +/** @internal */ +export const TArrayTypeId: TArray.TArrayTypeId = Symbol.for(TArraySymbolKey) as TArray.TArrayTypeId + +const tArrayVariance = { + /* c8 ignore next */ + _A: (_: any) => _ +} + +/** @internal */ +export class TArrayImpl implements TArray.TArray { + readonly [TArrayTypeId] = tArrayVariance + constructor(readonly chunk: Array>) {} +} + +/** @internal */ +export const collectFirst = dual< + (pf: (a: A) => Option.Option) => (self: TArray.TArray) => STM.STM>, + (self: TArray.TArray, pf: (a: A) => Option.Option) => STM.STM> +>(2, (self, pf) => + collectFirstSTM( + self, + (a) => pipe(pf(a), Option.map(core.succeed)) + )) + +/** @internal */ +export const collectFirstSTM = dual< + ( + pf: (a: A) => Option.Option> + ) => ( + self: TArray.TArray + ) => STM.STM, E, R>, + ( + self: TArray.TArray, + pf: (a: A) => Option.Option> + ) => STM.STM, E, R> +>( + 2, + (self: TArray.TArray, pf: (a: A) => Option.Option>) => + core.withSTMRuntime((runtime) => { + let index = 0 + let result: Option.Option> = Option.none() + while (Option.isNone(result) && index < self.chunk.length) { + const element = pipe(self.chunk[index], tRef.unsafeGet(runtime.journal)) + const option = pf(element) + if (Option.isSome(option)) { + result = option + } + index = index + 1 + } + return pipe( + result, + Option.match({ + onNone: () => stm.succeedNone, + onSome: core.map(Option.some) + }) + ) + }) +) + +/** @internal */ +export const contains = dual< + (value: A) => (self: TArray.TArray) => STM.STM, + (self: TArray.TArray, value: A) => STM.STM +>(2, (self, value) => some(self, (a) => Equal.equals(a)(value))) + +/** @internal */ +export const count = dual< + (predicate: Predicate) => (self: TArray.TArray) => STM.STM, + (self: TArray.TArray, predicate: Predicate) => STM.STM +>(2, (self, predicate) => + reduce( + self, + 0, + (n, a) => predicate(a) ? n + 1 : n + )) + +/** @internal */ +export const countSTM = dual< + (predicate: (value: A) => STM.STM) => (self: TArray.TArray) => STM.STM, + (self: TArray.TArray, predicate: (value: A) => STM.STM) => STM.STM +>(2, (self, predicate) => + reduceSTM( + self, + 0, + (n, a) => core.map(predicate(a), (bool) => bool ? n + 1 : n) + )) + +/** @internal */ +export const empty = (): STM.STM> => fromIterable([]) + +/** @internal */ +export const every = dual< + (predicate: Predicate) => (self: TArray.TArray) => STM.STM, + (self: TArray.TArray, predicate: Predicate) => STM.STM +>(2, (self, predicate) => stm.negate(some(self, (a) => !predicate(a)))) + +/** @internal */ +export const everySTM = dual< + (predicate: (value: A) => STM.STM) => (self: TArray.TArray) => STM.STM, + (self: TArray.TArray, predicate: (value: A) => STM.STM) => STM.STM +>(2, (self, predicate) => + core.map( + countSTM(self, predicate), + (count) => count === self.chunk.length + )) + +/** @internal */ +export const findFirst = dual< + (predicate: Predicate) => (self: TArray.TArray) => STM.STM>, + (self: TArray.TArray, predicate: Predicate) => STM.STM> +>(2, (self, predicate) => + collectFirst(self, (a) => + predicate(a) + ? Option.some(a) + : Option.none())) + +/** @internal */ +export const findFirstIndex = dual< + (value: A) => (self: TArray.TArray) => STM.STM>, + (self: TArray.TArray, value: A) => STM.STM> +>(2, (self, value) => findFirstIndexFrom(self, value, 0)) + +/** @internal */ +export const findFirstIndexFrom = dual< + (value: A, from: number) => (self: TArray.TArray) => STM.STM>, + (self: TArray.TArray, value: A, from: number) => STM.STM> +>(3, (self, value, from) => + findFirstIndexWhereFrom( + self, + (a) => Equal.equals(a)(value), + from + )) + +/** @internal */ +export const findFirstIndexWhere = dual< + (predicate: Predicate) => (self: TArray.TArray) => STM.STM>, + (self: TArray.TArray, predicate: Predicate) => STM.STM> +>(2, (self, predicate) => findFirstIndexWhereFrom(self, predicate, 0)) + +/** @internal */ +export const findFirstIndexWhereFrom = dual< + ( + predicate: Predicate, + from: number + ) => (self: TArray.TArray) => STM.STM>, + (self: TArray.TArray, predicate: Predicate, from: number) => STM.STM> +>(3, (self, predicate, from) => { + if (from < 0) { + return stm.succeedNone + } + return core.effect>((journal) => { + let index: number = from + let found = false + while (!found && index < self.chunk.length) { + const element = tRef.unsafeGet(self.chunk[index], journal) + found = predicate(element) + index = index + 1 + } + if (found) { + return Option.some(index - 1) + } + return Option.none() + }) +}) + +/** @internal */ +export const findFirstIndexWhereSTM = dual< + ( + predicate: (value: A) => STM.STM + ) => (self: TArray.TArray) => STM.STM, E, R>, + ( + self: TArray.TArray, + predicate: (value: A) => STM.STM + ) => STM.STM, E, R> +>(2, (self, predicate) => findFirstIndexWhereFromSTM(self, predicate, 0)) + +/** @internal */ +export const findFirstIndexWhereFromSTM = dual< + ( + predicate: (value: A) => STM.STM, + from: number + ) => (self: TArray.TArray) => STM.STM, E, R>, + ( + self: TArray.TArray, + predicate: (value: A) => STM.STM, + from: number + ) => STM.STM, E, R> +>(3, ( + self: TArray.TArray, + predicate: (value: A) => STM.STM, + from: number +) => { + const forIndex = (index: number): STM.STM, E, R> => + index < self.chunk.length + ? pipe( + tRef.get(self.chunk[index]), + core.flatMap(predicate), + core.flatMap((bool) => + bool ? + core.succeed(Option.some(index)) : + forIndex(index + 1) + ) + ) + : stm.succeedNone + return from < 0 + ? stm.succeedNone + : forIndex(from) +}) + +/** @internal */ +export const findFirstSTM = dual< + ( + predicate: (value: A) => STM.STM + ) => ( + self: TArray.TArray + ) => STM.STM, E, R>, + ( + self: TArray.TArray, + predicate: (value: A) => STM.STM + ) => STM.STM, E, R> +>(2, (self: TArray.TArray, predicate: (value: A) => STM.STM) => { + const init = [Option.none() as Option.Option, 0 as number] as const + const cont = (state: readonly [Option.Option, number]) => + Option.isNone(state[0]) && state[1] < self.chunk.length - 1 + return core.map( + stm.iterate(init, { + while: cont, + body: (state) => { + const index = state[1] + return pipe( + tRef.get(self.chunk[index]), + core.flatMap((value) => + core.map( + predicate(value), + (bool) => [bool ? Option.some(value) : Option.none(), index + 1] as const + ) + ) + ) + } + }), + (state) => state[0] + ) +}) + +/** @internal */ +export const findLast = dual< + (predicate: Predicate) => (self: TArray.TArray) => STM.STM>, + (self: TArray.TArray, predicate: Predicate) => STM.STM> +>(2, (self: TArray.TArray, predicate: Predicate) => + core.effect>((journal) => { + let index = self.chunk.length - 1 + let result: Option.Option = Option.none() + while (Option.isNone(result) && index >= 0) { + const element = tRef.unsafeGet(self.chunk[index], journal) + if (predicate(element)) { + result = Option.some(element) + } + index = index - 1 + } + return result + })) + +/** @internal */ +export const findLastIndex = dual< + (value: A) => (self: TArray.TArray) => STM.STM>, + (self: TArray.TArray, value: A) => STM.STM> +>(2, (self, value) => findLastIndexFrom(self, value, self.chunk.length - 1)) + +/** @internal */ +export const findLastIndexFrom = dual< + (value: A, end: number) => (self: TArray.TArray) => STM.STM>, + (self: TArray.TArray, value: A, end: number) => STM.STM> +>(3, (self, value, end) => { + if (end >= self.chunk.length) { + return stm.succeedNone + } + return core.effect>((journal) => { + let index: number = end + let found = false + while (!found && index >= 0) { + const element = tRef.unsafeGet(self.chunk[index], journal) + found = Equal.equals(element)(value) + index = index - 1 + } + if (found) { + return Option.some(index + 1) + } + return Option.none() + }) +}) + +/** @internal */ +export const findLastSTM = dual< + ( + predicate: (value: A) => STM.STM + ) => (self: TArray.TArray) => STM.STM, E, R>, + ( + self: TArray.TArray, + predicate: (value: A) => STM.STM + ) => STM.STM, E, R> +>(2, (self: TArray.TArray, predicate: (value: A) => STM.STM) => { + const init = [Option.none() as Option.Option, self.chunk.length - 1] as const + const cont = (state: readonly [Option.Option, number]) => Option.isNone(state[0]) && state[1] >= 0 + return core.map( + stm.iterate(init, { + while: cont, + body: (state) => { + const index = state[1] + return pipe( + tRef.get(self.chunk[index]), + core.flatMap((value) => + core.map( + predicate(value), + (bool) => [bool ? Option.some(value) : Option.none(), index - 1] as const + ) + ) + ) + } + }), + (state) => state[0] + ) +}) + +/** @internal */ +export const forEach = dual< + (f: (value: A) => STM.STM) => (self: TArray.TArray) => STM.STM, + (self: TArray.TArray, f: (value: A) => STM.STM) => STM.STM +>(2, (self, f) => reduceSTM(self, void 0 as void, (_, a) => f(a))) + +/** @internal */ +export const fromIterable = (iterable: Iterable): STM.STM> => + core.map( + stm.forEach(iterable, tRef.make), + (chunk) => new TArrayImpl(chunk) + ) + +/** @internal */ +export const get = dual< + (index: number) => (self: TArray.TArray) => STM.STM, + (self: TArray.TArray, index: number) => STM.STM +>(2, (self, index) => { + if (index < 0 || index >= self.chunk.length) { + return core.dieMessage("Index out of bounds") + } + return tRef.get(self.chunk[index]) +}) + +/** @internal */ +export const headOption = (self: TArray.TArray): STM.STM> => + self.chunk.length === 0 ? + core.succeed(Option.none()) : + core.map(tRef.get(self.chunk[0]), Option.some) + +/** @internal */ +export const lastOption = (self: TArray.TArray): STM.STM> => + self.chunk.length === 0 ? + stm.succeedNone : + core.map(tRef.get(self.chunk[self.chunk.length - 1]), Option.some) + +/** @internal */ +export const make = ]>( + ...elements: Elements +): STM.STM> => fromIterable(elements) + +/** @internal */ +export const maxOption = dual< + (order: Order.Order) => (self: TArray.TArray) => STM.STM>, + (self: TArray.TArray, order: Order.Order) => STM.STM> +>(2, (self, order) => { + const greaterThan = Order.greaterThan(order) + return reduceOption(self, (acc, curr) => greaterThan(acc)(curr) ? curr : acc) +}) + +/** @internal */ +export const minOption = dual< + (order: Order.Order) => (self: TArray.TArray) => STM.STM>, + (self: TArray.TArray, order: Order.Order) => STM.STM> +>(2, (self, order) => { + const lessThan = Order.lessThan(order) + return reduceOption(self, (acc, curr) => lessThan(acc)(curr) ? curr : acc) +}) + +/** @internal */ +export const reduce = dual< + (zero: Z, f: (accumulator: Z, current: A) => Z) => (self: TArray.TArray) => STM.STM, + (self: TArray.TArray, zero: Z, f: (accumulator: Z, current: A) => Z) => STM.STM +>( + 3, + (self: TArray.TArray, zero: Z, f: (accumulator: Z, current: A) => Z) => + core.effect((journal) => { + let index = 0 + let result = zero + while (index < self.chunk.length) { + const element = tRef.unsafeGet(self.chunk[index], journal) + result = f(result, element) + index = index + 1 + } + return result + }) +) + +/** @internal */ +export const reduceOption = dual< + (f: (x: A, y: A) => A) => (self: TArray.TArray) => STM.STM>, + (self: TArray.TArray, f: (x: A, y: A) => A) => STM.STM> +>( + 2, + (self: TArray.TArray, f: (x: A, y: A) => A) => + core.effect>((journal) => { + let index = 0 + let result: A | undefined = undefined + while (index < self.chunk.length) { + const element = tRef.unsafeGet(self.chunk[index], journal) + result = result === undefined ? element : f(result, element) + index = index + 1 + } + return Option.fromNullable(result) + }) +) + +/** @internal */ +export const reduceOptionSTM = dual< + (f: (x: A, y: A) => STM.STM) => (self: TArray.TArray) => STM.STM, E, R>, + (self: TArray.TArray, f: (x: A, y: A) => STM.STM) => STM.STM, E, R> +>( + 2, + (self: TArray.TArray, f: (x: A, y: A) => STM.STM) => + reduceSTM(self, Option.none(), (acc, curr) => + Option.isSome(acc) + ? core.map(f(acc.value, curr), Option.some) + : stm.succeedSome(curr)) +) + +/** @internal */ +export const reduceSTM = dual< + ( + zero: Z, + f: (accumulator: Z, current: A) => STM.STM + ) => (self: TArray.TArray) => STM.STM, + ( + self: TArray.TArray, + zero: Z, + f: (accumulator: Z, current: A) => STM.STM + ) => STM.STM +>(3, (self, zero, f) => + core.flatMap( + toArray(self), + stm.reduce(zero, f) + )) + +/** @internal */ +export const size = (self: TArray.TArray): number => self.chunk.length + +/** @internal */ +export const some = dual< + (predicate: Predicate) => (self: TArray.TArray) => STM.STM, + (self: TArray.TArray, predicate: Predicate) => STM.STM +>(2, (self, predicate) => + core.map( + findFirst(self, predicate), + Option.isSome + )) + +/** @internal */ +export const someSTM = dual< + (predicate: (value: A) => STM.STM) => (self: TArray.TArray) => STM.STM, + (self: TArray.TArray, predicate: (value: A) => STM.STM) => STM.STM +>(2, (self, predicate) => core.map(countSTM(self, predicate), (n) => n > 0)) + +/** @internal */ +export const toArray = (self: TArray.TArray): STM.STM> => stm.forEach(self.chunk, tRef.get) + +/** @internal */ +export const transform = dual< + (f: (value: A) => A) => (self: TArray.TArray) => STM.STM, + (self: TArray.TArray, f: (value: A) => A) => STM.STM +>(2, (self, f) => + core.effect((journal) => { + let index = 0 + while (index < self.chunk.length) { + const ref = self.chunk[index] + tRef.unsafeSet(ref, f(tRef.unsafeGet(ref, journal)), journal) + index = index + 1 + } + return void 0 + })) + +/** @internal */ +export const transformSTM = dual< + (f: (value: A) => STM.STM) => (self: TArray.TArray) => STM.STM, + (self: TArray.TArray, f: (value: A) => STM.STM) => STM.STM +>(2, (self: TArray.TArray, f: (value: A) => STM.STM) => + core.flatMap( + stm.forEach( + self.chunk, + (ref) => core.flatMap(tRef.get(ref), f) + ), + (chunk) => + core.effect((journal) => { + const iterator = chunk[Symbol.iterator]() + let index = 0 + let next: IteratorResult + while ((next = iterator.next()) && !next.done) { + tRef.unsafeSet(self.chunk[index], next.value, journal) + index = index + 1 + } + return void 0 + }) + )) + +/** @internal */ +export const update = dual< + (index: number, f: (value: A) => A) => (self: TArray.TArray) => STM.STM, + (self: TArray.TArray, index: number, f: (value: A) => A) => STM.STM +>(3, (self, index, f) => { + if (index < 0 || index >= self.chunk.length) { + return core.dieMessage("Index out of bounds") + } + return tRef.update(self.chunk[index], f) +}) + +/** @internal */ +export const updateSTM = dual< + (index: number, f: (value: A) => STM.STM) => (self: TArray.TArray) => STM.STM, + (self: TArray.TArray, index: number, f: (value: A) => STM.STM) => STM.STM +>(3, (self, index, f) => { + if (index < 0 || index >= self.chunk.length) { + return core.dieMessage("Index out of bounds") + } + return pipe( + tRef.get(self.chunk[index]), + core.flatMap(f), + core.flatMap((updated) => tRef.set(self.chunk[index], updated)) + ) +}) diff --git a/repos/effect/packages/effect/src/internal/stm/tDeferred.ts b/repos/effect/packages/effect/src/internal/stm/tDeferred.ts new file mode 100644 index 0000000..ebb819d --- /dev/null +++ b/repos/effect/packages/effect/src/internal/stm/tDeferred.ts @@ -0,0 +1,81 @@ +import * as Either from "../../Either.js" +import { dual } from "../../Function.js" +import * as Option from "../../Option.js" +import type * as STM from "../../STM.js" +import type * as TDeferred from "../../TDeferred.js" +import type * as TRef from "../../TRef.js" +import * as core from "./core.js" +import * as stm from "./stm.js" +import * as tRef from "./tRef.js" + +/** @internal */ +const TDeferredSymbolKey = "effect/TDeferred" + +/** @internal */ +export const TDeferredTypeId: TDeferred.TDeferredTypeId = Symbol.for( + TDeferredSymbolKey +) as TDeferred.TDeferredTypeId + +/** @internal */ +const tDeferredVariance = { + /* c8 ignore next */ + _A: (_: any) => _, + /* c8 ignore next */ + _E: (_: any) => _ +} + +/** @internal */ +class TDeferredImpl implements TDeferred.TDeferred { + readonly [TDeferredTypeId] = tDeferredVariance + constructor(readonly ref: TRef.TRef>>) {} +} + +/** @internal */ +export const _await = (self: TDeferred.TDeferred): STM.STM => + stm.flatten( + stm.collect(tRef.get(self.ref), (option) => + Option.isSome(option) ? + Option.some(stm.fromEither(option.value)) : + Option.none()) + ) + +/** @internal */ +export const done = dual< + (either: Either.Either) => (self: TDeferred.TDeferred) => STM.STM, + (self: TDeferred.TDeferred, either: Either.Either) => STM.STM +>(2, (self, either) => + core.flatMap( + tRef.get(self.ref), + Option.match({ + onNone: () => + core.zipRight( + tRef.set(self.ref, Option.some(either)), + core.succeed(true) + ), + onSome: () => core.succeed(false) + }) + )) + +/** @internal */ +export const fail = dual< + (error: E) => (self: TDeferred.TDeferred) => STM.STM, + (self: TDeferred.TDeferred, error: E) => STM.STM +>(2, (self, error) => done(self, Either.left(error))) + +/** @internal */ +export const make = (): STM.STM> => + core.map( + tRef.make>>(Option.none()), + (ref) => new TDeferredImpl(ref) + ) + +/** @internal */ +export const poll = ( + self: TDeferred.TDeferred +): STM.STM>> => tRef.get(self.ref) + +/** @internal */ +export const succeed = dual< + (value: A) => (self: TDeferred.TDeferred) => STM.STM, + (self: TDeferred.TDeferred, value: A) => STM.STM +>(2, (self, value) => done(self, Either.right(value))) diff --git a/repos/effect/packages/effect/src/internal/stm/tExit.ts b/repos/effect/packages/effect/src/internal/stm/tExit.ts new file mode 100644 index 0000000..478e424 --- /dev/null +++ b/repos/effect/packages/effect/src/internal/stm/tExit.ts @@ -0,0 +1,190 @@ +import * as Equal from "../../Equal.js" +import type * as FiberId from "../../FiberId.js" +import { pipe } from "../../Function.js" +import * as Hash from "../../Hash.js" +import { hasProperty } from "../../Predicate.js" +import type * as Types from "../../Types.js" +import * as OpCodes from "./opCodes/tExit.js" + +/** @internal */ +const TExitSymbolKey = "effect/TExit" + +/** @internal */ +export const TExitTypeId = Symbol.for(TExitSymbolKey) + +/** @internal */ +export type TExitTypeId = typeof TExitTypeId + +/** @internal */ +export type TExit = Fail | Die | Interrupt | Succeed | Retry + +/** @internal */ +export declare namespace TExit { + /** @internal */ + export interface Variance { + readonly [TExitTypeId]: { + readonly _A: Types.Covariant + readonly _E: Types.Covariant + } + } +} + +const variance = { + /* c8 ignore next */ + _A: (_: never) => _, + /* c8 ignore next */ + _E: (_: never) => _ +} + +/** @internal */ +export interface Fail extends TExit.Variance, Equal.Equal { + readonly _tag: OpCodes.OP_FAIL + readonly error: E +} + +/** @internal */ +export interface Die extends TExit.Variance, Equal.Equal { + readonly _tag: OpCodes.OP_DIE + readonly defect: unknown +} + +/** @internal */ +export interface Interrupt extends TExit.Variance, Equal.Equal { + readonly _tag: OpCodes.OP_INTERRUPT + readonly fiberId: FiberId.FiberId +} + +/** @internal */ +export interface Succeed extends TExit.Variance, Equal.Equal { + readonly _tag: OpCodes.OP_SUCCEED + readonly value: A +} + +/** @internal */ +export interface Retry extends TExit.Variance, Equal.Equal { + readonly _tag: OpCodes.OP_RETRY +} + +/** @internal */ +export const isExit = (u: unknown): u is TExit => hasProperty(u, TExitTypeId) + +/** @internal */ +export const isFail = (self: TExit): self is Fail => { + return self._tag === OpCodes.OP_FAIL +} + +/** @internal */ +export const isDie = (self: TExit): self is Die => { + return self._tag === OpCodes.OP_DIE +} + +/** @internal */ +export const isInterrupt = (self: TExit): self is Interrupt => { + return self._tag === OpCodes.OP_INTERRUPT +} + +/** @internal */ +export const isSuccess = (self: TExit): self is Succeed => { + return self._tag === OpCodes.OP_SUCCEED +} + +/** @internal */ +export const isRetry = (self: TExit): self is Retry => { + return self._tag === OpCodes.OP_RETRY +} + +/** @internal */ +export const fail = (error: E): TExit => ({ + [TExitTypeId]: variance, + _tag: OpCodes.OP_FAIL, + error, + [Hash.symbol](): number { + return pipe( + Hash.hash(TExitSymbolKey), + Hash.combine(Hash.hash(OpCodes.OP_FAIL)), + Hash.combine(Hash.hash(error)), + Hash.cached(this) + ) + }, + [Equal.symbol](that: unknown): boolean { + return isExit(that) && that._tag === OpCodes.OP_FAIL && Equal.equals(error, that.error) + } +}) + +/** @internal */ +export const die = (defect: unknown): TExit => ({ + [TExitTypeId]: variance, + _tag: OpCodes.OP_DIE, + defect, + [Hash.symbol](): number { + return pipe( + Hash.hash(TExitSymbolKey), + Hash.combine(Hash.hash(OpCodes.OP_DIE)), + Hash.combine(Hash.hash(defect)), + Hash.cached(this) + ) + }, + [Equal.symbol](that: unknown): boolean { + return isExit(that) && that._tag === OpCodes.OP_DIE && Equal.equals(defect, that.defect) + } +}) + +/** @internal */ +export const interrupt = (fiberId: FiberId.FiberId): TExit => ({ + [TExitTypeId]: variance, + _tag: OpCodes.OP_INTERRUPT, + fiberId, + [Hash.symbol](): number { + return pipe( + Hash.hash(TExitSymbolKey), + Hash.combine(Hash.hash(OpCodes.OP_INTERRUPT)), + Hash.combine(Hash.hash(fiberId)), + Hash.cached(this) + ) + }, + [Equal.symbol](that: unknown): boolean { + return isExit(that) && that._tag === OpCodes.OP_INTERRUPT && Equal.equals(fiberId, that.fiberId) + } +}) + +/** @internal */ +export const succeed = (value: A): TExit => ({ + [TExitTypeId]: variance, + _tag: OpCodes.OP_SUCCEED, + value, + [Hash.symbol](): number { + return pipe( + Hash.hash(TExitSymbolKey), + Hash.combine(Hash.hash(OpCodes.OP_SUCCEED)), + Hash.combine(Hash.hash(value)), + Hash.cached(this) + ) + }, + [Equal.symbol](that: unknown): boolean { + return isExit(that) && that._tag === OpCodes.OP_SUCCEED && Equal.equals(value, that.value) + } +}) + +const retryHash = pipe( + Hash.hash(TExitSymbolKey), + Hash.combine(Hash.hash(OpCodes.OP_RETRY)), + Hash.combine(Hash.hash("retry")) +) + +/** @internal */ +export const retry: TExit = { + [TExitTypeId]: variance, + _tag: OpCodes.OP_RETRY, + [Hash.symbol](): number { + return retryHash + }, + [Equal.symbol](that: unknown): boolean { + return isExit(that) && isRetry(that) + } +} + +const void_: TExit = succeed(undefined) +export { + /** @internal */ + void_ as void +} diff --git a/repos/effect/packages/effect/src/internal/stm/tMap.ts b/repos/effect/packages/effect/src/internal/stm/tMap.ts new file mode 100644 index 0000000..b5545fc --- /dev/null +++ b/repos/effect/packages/effect/src/internal/stm/tMap.ts @@ -0,0 +1,824 @@ +import * as RA from "../../Array.js" +import * as Chunk from "../../Chunk.js" +import * as Equal from "../../Equal.js" +import type { LazyArg } from "../../Function.js" +import { dual, pipe } from "../../Function.js" +import * as Hash from "../../Hash.js" +import * as HashMap from "../../HashMap.js" +import * as Option from "../../Option.js" +import { hasProperty } from "../../Predicate.js" +import * as STM from "../../STM.js" +import type * as TArray from "../../TArray.js" +import type * as TMap from "../../TMap.js" +import type * as TRef from "../../TRef.js" +import * as core from "./core.js" +import type * as Journal from "./journal.js" +import * as stm from "./stm.js" +import * as tArray from "./tArray.js" +import * as tRef from "./tRef.js" + +/** @internal */ +const TMapSymbolKey = "effect/TMap" + +/** @internal */ +export const TMapTypeId: TMap.TMapTypeId = Symbol.for( + TMapSymbolKey +) as TMap.TMapTypeId + +const tMapVariance = { + /* c8 ignore next */ + _K: (_: any) => _, + /* c8 ignore next */ + _V: (_: any) => _ +} + +/** @internal */ +class TMapImpl implements TMap.TMap { + readonly [TMapTypeId] = tMapVariance + constructor( + readonly tBuckets: TRef.TRef>>, + readonly tSize: TRef.TRef + ) {} +} + +const isTMap = (u: unknown) => hasProperty(u, TMapTypeId) + +/** @internal */ +const InitialCapacity = 16 +const LoadFactor = 0.75 + +/** @internal */ +const nextPowerOfTwo = (size: number): number => { + const n = -1 >>> Math.clz32(size - 1) + return n < 0 ? 1 : n + 1 +} + +/** @internal */ +const hash = (key: K): number => { + const h = Hash.hash(key) + return h ^ (h >>> 16) +} + +/** @internal */ +const indexOf = (k: K, capacity: number): number => hash(k) & (capacity - 1) + +/** @internal */ +const allocate = ( + capacity: number, + data: Chunk.Chunk +): STM.STM> => { + const buckets = Array.from({ length: capacity }, () => Chunk.empty()) + const distinct = new Map(data) + let size = 0 + for (const entry of distinct) { + const index = indexOf(entry[0], capacity) + buckets[index] = pipe(buckets[index], Chunk.prepend(entry)) + size = size + 1 + } + return pipe( + tArray.fromIterable(buckets), + core.flatMap((buckets) => + pipe( + tRef.make(buckets), + core.flatMap((tBuckets) => + pipe( + tRef.make(size), + core.map((tSize) => new TMapImpl(tBuckets, tSize)) + ) + ) + ) + ) + ) +} + +/** @internal */ +export const empty = (): STM.STM> => fromIterable([]) + +/** @internal */ +export const find = dual< + ( + pf: (key: K, value: V) => Option.Option + ) => (self: TMap.TMap) => STM.STM>, + ( + self: TMap.TMap, + pf: (key: K, value: V) => Option.Option + ) => STM.STM> +>(2, (self, pf) => + findSTM(self, (key, value) => { + const option = pf(key, value) + if (Option.isSome(option)) { + return core.succeed(option.value) + } + return core.fail(Option.none()) + })) + +/** @internal */ +export const findSTM = dual< + ( + f: (key: K, value: V) => STM.STM, R> + ) => (self: TMap.TMap) => STM.STM, E, R>, + ( + self: TMap.TMap, + f: (key: K, value: V) => STM.STM, R> + ) => STM.STM, E, R> +>(2, ( + self: TMap.TMap, + f: (key: K, value: V) => STM.STM, R> +) => + reduceSTM(self, Option.none(), (acc, value, key) => + Option.isNone(acc) ? + core.matchSTM(f(key, value), { + onFailure: Option.match({ + onNone: () => stm.succeedNone, + onSome: core.fail + }), + onSuccess: stm.succeedSome + }) : + STM.succeed(acc))) + +/** @internal */ +export const findAll = dual< + ( + pf: (key: K, value: V) => Option.Option + ) => (self: TMap.TMap) => STM.STM>, + ( + self: TMap.TMap, + pf: (key: K, value: V) => Option.Option + ) => STM.STM> +>(2, (self, pf) => + findAllSTM(self, (key, value) => { + const option = pf(key, value) + if (Option.isSome(option)) { + return core.succeed(option.value) + } + return core.fail(Option.none()) + })) + +/** @internal */ +export const findAllSTM = dual< + ( + pf: (key: K, value: V) => STM.STM, R> + ) => (self: TMap.TMap) => STM.STM, E, R>, + ( + self: TMap.TMap, + pf: (key: K, value: V) => STM.STM, R> + ) => STM.STM, E, R> +>(2, ( + self: TMap.TMap, + pf: (key: K, value: V) => STM.STM, R> +) => + core.map( + reduceSTM(self, Chunk.empty(), (acc, value, key) => + core.matchSTM(pf(key, value), { + onFailure: Option.match({ + onNone: () => core.succeed(acc), + onSome: core.fail + }), + onSuccess: (a) => core.succeed(Chunk.append(acc, a)) + })), + (a) => Array.from(a) + )) + +/** @internal */ +export const forEach = dual< + (f: (key: K, value: V) => STM.STM) => (self: TMap.TMap) => STM.STM, + (self: TMap.TMap, f: (key: K, value: V) => STM.STM) => STM.STM +>(2, (self, f) => + reduceSTM( + self, + void 0 as void, + (_, value, key) => stm.asVoid(f(key, value)) + )) + +/** @internal */ +export const fromIterable = (iterable: Iterable): STM.STM> => + stm.suspend(() => { + const data = Chunk.fromIterable(iterable) + const capacity = data.length < InitialCapacity + ? InitialCapacity + : nextPowerOfTwo(data.length) + return allocate(capacity, data) + }) + +/** @internal */ +export const get = dual< + (key: K) => (self: TMap.TMap) => STM.STM>, + (self: TMap.TMap, key: K) => STM.STM> +>(2, (self: TMap.TMap, key: K) => + core.effect>((journal) => { + const buckets = tRef.unsafeGet(self.tBuckets, journal) + const index = indexOf(key, buckets.chunk.length) + const bucket = tRef.unsafeGet(buckets.chunk[index], journal) + return pipe( + Chunk.findFirst(bucket, (entry) => Equal.equals(entry[0])(key)), + Option.map((entry) => entry[1]) + ) + })) + +/** @internal */ +export const getOrElse = dual< + (key: K, fallback: LazyArg) => (self: TMap.TMap) => STM.STM, + (self: TMap.TMap, key: K, fallback: LazyArg) => STM.STM +>(3, (self, key, fallback) => + core.map( + get(self, key), + Option.getOrElse(fallback) + )) + +/** @internal */ +export const has = dual< + (key: K) => (self: TMap.TMap) => STM.STM, + (self: TMap.TMap, key: K) => STM.STM +>(2, (self, key) => core.map(get(self, key), Option.isSome)) + +/** @internal */ +export const isEmpty = (self: TMap.TMap): STM.STM => + core.map(tRef.get(self.tSize), (size) => size === 0) + +/** @internal */ +export const keys = (self: TMap.TMap): STM.STM> => + core.map(toReadonlyArray(self), RA.map((entry) => entry[0])) + +/** @internal */ +export const make = (...entries: Array): STM.STM> => fromIterable(entries) + +/** @internal */ +export const merge = dual< + (key: K, value: V, f: (x: V, y: V) => V) => (self: TMap.TMap) => STM.STM, + (self: TMap.TMap, key: K, value: V, f: (x: V, y: V) => V) => STM.STM +>(4, (self, key, value, f) => + core.flatMap( + get(self, key), + Option.match({ + onNone: () => stm.as(set(self, key, value), value), + onSome: (v0) => { + const v1 = f(v0, value) + return stm.as(set(self, key, v1), v1) + } + }) + )) + +/** @internal */ +export const reduce = dual< + (zero: Z, f: (acc: Z, value: V, key: K) => Z) => (self: TMap.TMap) => STM.STM, + (self: TMap.TMap, zero: Z, f: (acc: Z, value: V, key: K) => Z) => STM.STM +>( + 3, + (self: TMap.TMap, zero: Z, f: (acc: Z, value: V, key: K) => Z) => + core.effect((journal) => { + const buckets = tRef.unsafeGet(self.tBuckets, journal) + let result = zero + let index = 0 + while (index < buckets.chunk.length) { + const bucket = buckets.chunk[index] + const items = tRef.unsafeGet(bucket, journal) + result = Chunk.reduce(items, result, (acc, entry) => f(acc, entry[1], entry[0])) + index = index + 1 + } + return result + }) +) + +/** @internal */ +export const reduceSTM = dual< + ( + zero: Z, + f: (acc: Z, value: V, key: K) => STM.STM + ) => (self: TMap.TMap) => STM.STM, + ( + self: TMap.TMap, + zero: Z, + f: (acc: Z, value: V, key: K) => STM.STM + ) => STM.STM +>(3, (self, zero, f) => + core.flatMap( + toReadonlyArray(self), + stm.reduce(zero, (acc, entry) => f(acc, entry[1], entry[0])) + )) + +/** @internal */ +export const remove = dual< + (key: K) => (self: TMap.TMap) => STM.STM, + (self: TMap.TMap, key: K) => STM.STM +>(2, (self, key) => + core.effect((journal) => { + const buckets = tRef.unsafeGet(self.tBuckets, journal) + const index = indexOf(key, buckets.chunk.length) + const bucket = tRef.unsafeGet(buckets.chunk[index], journal) + const [toRemove, toRetain] = Chunk.partition(bucket, (entry) => Equal.equals(entry[1], key)) + if (Chunk.isNonEmpty(toRemove)) { + const currentSize = tRef.unsafeGet(self.tSize, journal) + tRef.unsafeSet(buckets.chunk[index], toRetain, journal) + tRef.unsafeSet(self.tSize, currentSize - 1, journal) + } + })) + +/** @internal */ +export const removeAll = dual< + (keys: Iterable) => (self: TMap.TMap) => STM.STM, + (self: TMap.TMap, keys: Iterable) => STM.STM +>(2, (self: TMap.TMap, keys: Iterable) => + core.effect((journal) => { + const iterator = keys[Symbol.iterator]() + let next: IteratorResult + while ((next = iterator.next()) && !next.done) { + const buckets = tRef.unsafeGet(self.tBuckets, journal) + const index = indexOf(next.value, buckets.chunk.length) + const bucket = tRef.unsafeGet(buckets.chunk[index], journal) + const [toRemove, toRetain] = Chunk.partition(bucket, (entry) => Equal.equals(next.value)(entry[0])) + if (Chunk.isNonEmpty(toRemove)) { + const currentSize = tRef.unsafeGet(self.tSize, journal) + tRef.unsafeSet(buckets.chunk[index], toRetain, journal) + tRef.unsafeSet(self.tSize, currentSize - 1, journal) + } + } + })) + +/** @internal */ +export const removeIf: { + ( + predicate: (key: K, value: V) => boolean, + options: { + readonly discard: true + } + ): (self: TMap.TMap) => STM.STM + ( + predicate: (key: K, value: V) => boolean, + options?: { + readonly discard: false + } + ): (self: TMap.TMap) => STM.STM> + ( + self: TMap.TMap, + predicate: (key: K, value: V) => boolean, + options: { + readonly discard: true + } + ): STM.STM + ( + self: TMap.TMap, + predicate: (key: K, value: V) => boolean, + options?: { + readonly discard: false + } + ): STM.STM> +} = dual((args) => isTMap(args[0]), ( + self: TMap.TMap, + predicate: (key: K, value: V) => boolean, + options?: { + readonly discard: boolean + } +) => + core.effect((journal) => { + const discard = options?.discard === true + const buckets = tRef.unsafeGet(self.tBuckets, journal) + const capacity = buckets.chunk.length + const removed: Array<[K, V]> = [] + let index = 0 + let newSize = 0 + while (index < capacity) { + const bucket = tRef.unsafeGet(buckets.chunk[index], journal) + const iterator = bucket[Symbol.iterator]() + let next: IteratorResult + let newBucket = Chunk.empty() + while ((next = iterator.next()) && !next.done) { + const [k, v] = next.value + if (!predicate(k, v)) { + newBucket = Chunk.prepend(newBucket, next.value) + newSize = newSize + 1 + } else { + if (!discard) { + removed.push([k, v]) + } + } + } + tRef.unsafeSet(buckets.chunk[index], newBucket, journal) + index = index + 1 + } + tRef.unsafeSet(self.tSize, newSize, journal) + if (!discard) { + return removed + } + })) + +/** @internal */ +export const retainIf: { + ( + predicate: (key: K, value: V) => boolean, + options: { + readonly discard: true + } + ): (self: TMap.TMap) => STM.STM + ( + predicate: (key: K, value: V) => boolean, + options?: { + readonly discard: false + } + ): (self: TMap.TMap) => STM.STM> + ( + self: TMap.TMap, + predicate: (key: K, value: V) => boolean, + options: { + readonly discard: true + } + ): STM.STM + ( + self: TMap.TMap, + predicate: (key: K, value: V) => boolean, + options?: { + readonly discard: false + } + ): STM.STM> +} = dual( + (args) => isTMap(args[0]), + (self, predicate, options) => removeIf(self, (key, value) => !predicate(key, value), options) +) + +/** @internal */ +export const set = dual< + (key: K, value: V) => (self: TMap.TMap) => STM.STM, + (self: TMap.TMap, key: K, value: V) => STM.STM +>(3, (self: TMap.TMap, key: K, value: V) => { + const resize = (journal: Journal.Journal, buckets: TArray.TArray>): void => { + const capacity = buckets.chunk.length + const newCapacity = capacity << 1 + const newBuckets = Array.from({ length: newCapacity }, () => Chunk.empty()) + let index = 0 + while (index < capacity) { + const pairs = tRef.unsafeGet(buckets.chunk[index], journal) + const iterator = pairs[Symbol.iterator]() + let next: IteratorResult + while ((next = iterator.next()) && !next.done) { + const newIndex = indexOf(next.value[0], newCapacity) + newBuckets[newIndex] = Chunk.prepend(newBuckets[newIndex], next.value) + } + index = index + 1 + } + // insert new pair + const newIndex = indexOf(key, newCapacity) + newBuckets[newIndex] = Chunk.prepend(newBuckets[newIndex], [key, value] as const) + + const newArray: Array>> = [] + index = 0 + while (index < newCapacity) { + newArray[index] = new tRef.TRefImpl(newBuckets[index]) + index = index + 1 + } + const newTArray: TArray.TArray> = new tArray.TArrayImpl(newArray) + tRef.unsafeSet(self.tBuckets, newTArray, journal) + } + return core.effect((journal) => { + const buckets = tRef.unsafeGet(self.tBuckets, journal) + const capacity = buckets.chunk.length + const index = indexOf(key, capacity) + const bucket = tRef.unsafeGet(buckets.chunk[index], journal) + const shouldUpdate = Chunk.some(bucket, (entry) => Equal.equals(key)(entry[0])) + if (shouldUpdate) { + const newBucket = Chunk.map(bucket, (entry) => + Equal.equals(key)(entry[0]) ? + [key, value] as const : + entry) + tRef.unsafeSet(buckets.chunk[index], newBucket, journal) + } else { + const newSize = tRef.unsafeGet(self.tSize, journal) + 1 + tRef.unsafeSet(self.tSize, newSize, journal) + if (capacity * LoadFactor < newSize) { + resize(journal, buckets) + } else { + const newBucket = Chunk.prepend(bucket, [key, value] as const) + tRef.unsafeSet(buckets.chunk[index], newBucket, journal) + } + } + }) +}) + +/** @internal */ +export const setIfAbsent = dual< + (key: K, value: V) => (self: TMap.TMap) => STM.STM, + (self: TMap.TMap, key: K, value: V) => STM.STM +>(3, (self, key, value) => + core.flatMap( + get(self, key), + Option.match({ + onNone: () => set(self, key, value), + onSome: () => stm.void + }) + )) + +/** @internal */ +export const size = (self: TMap.TMap): STM.STM => tRef.get(self.tSize) + +/** @internal */ +export const takeFirst = dual< + (pf: (key: K, value: V) => Option.Option) => (self: TMap.TMap) => STM.STM, + (self: TMap.TMap, pf: (key: K, value: V) => Option.Option) => STM.STM +>(2, (self: TMap.TMap, pf: (key: K, value: V) => Option.Option) => + pipe( + core.effect>((journal) => { + const buckets = tRef.unsafeGet(self.tBuckets, journal) + const capacity = buckets.chunk.length + const size = tRef.unsafeGet(self.tSize, journal) + let result: Option.Option = Option.none() + let index = 0 + while (index < capacity && Option.isNone(result)) { + const bucket = tRef.unsafeGet(buckets.chunk[index], journal) + const recreate = Chunk.some(bucket, (entry) => Option.isSome(pf(entry[0], entry[1]))) + if (recreate) { + const iterator = bucket[Symbol.iterator]() + let newBucket = Chunk.empty() + let next: IteratorResult + while ((next = iterator.next()) && !next.done && Option.isNone(result)) { + const option = pf(next.value[0], next.value[1]) + if (Option.isSome(option) && Option.isNone(result)) { + result = option + } else { + newBucket = Chunk.prepend(newBucket, next.value) + } + } + tRef.unsafeSet(buckets.chunk[index], newBucket, journal) + } + index = index + 1 + } + if (Option.isSome(result)) { + tRef.unsafeSet(self.tSize, size - 1, journal) + } + return result + }), + stm.collect((option) => + Option.isSome(option) ? + Option.some(option.value) : + Option.none() + ) + )) + +/** @internal */ +export const takeFirstSTM = dual< + ( + pf: (key: K, value: V) => STM.STM, R> + ) => (self: TMap.TMap) => STM.STM, + (self: TMap.TMap, pf: (key: K, value: V) => STM.STM, R>) => STM.STM +>(2, (self, pf) => + pipe( + findSTM(self, (key, value) => core.map(pf(key, value), (a) => [key, a] as const)), + stm.collect((option) => Option.isSome(option) ? Option.some(option.value) : Option.none()), + core.flatMap((entry) => stm.as(remove(self, entry[0]), entry[1])) + )) + +/** @internal */ +export const takeSome = dual< + ( + pf: (key: K, value: V) => Option.Option + ) => (self: TMap.TMap) => STM.STM>, + ( + self: TMap.TMap, + pf: (key: K, value: V) => Option.Option + ) => STM.STM> +>(2, (self: TMap.TMap, pf: (key: K, value: V) => Option.Option) => + pipe( + core.effect>>((journal) => { + const buckets = tRef.unsafeGet(self.tBuckets, journal) + const capacity = buckets.chunk.length + const builder: Array = [] + let newSize = 0 + let index = 0 + while (index < capacity) { + const bucket = tRef.unsafeGet(buckets.chunk[index], journal) + const recreate = Chunk.some(bucket, (entry) => Option.isSome(pf(entry[0], entry[1]))) + if (recreate) { + const iterator = bucket[Symbol.iterator]() + let newBucket = Chunk.empty() + let next: IteratorResult + while ((next = iterator.next()) && !next.done) { + const option = pf(next.value[0], next.value[1]) + if (Option.isSome(option)) { + builder.push(option.value) + } else { + newBucket = Chunk.prepend(newBucket, next.value) + newSize = newSize + 1 + } + } + tRef.unsafeSet(buckets.chunk[index], newBucket, journal) + } else { + newSize = newSize + bucket.length + } + index = index + 1 + } + tRef.unsafeSet(self.tSize, newSize, journal) + if (builder.length > 0) { + return Option.some(builder as RA.NonEmptyArray) + } + return Option.none() + }), + stm.collect((option) => + Option.isSome(option) ? + Option.some(option.value) : + Option.none>() + ) + )) + +/** @internal */ +export const takeSomeSTM = dual< + ( + pf: (key: K, value: V) => STM.STM, R> + ) => (self: TMap.TMap) => STM.STM, E, R>, + ( + self: TMap.TMap, + pf: (key: K, value: V) => STM.STM, R> + ) => STM.STM, E, R> +>(2, ( + self: TMap.TMap, + pf: (key: K, value: V) => STM.STM, R> +) => + pipe( + findAllSTM( + self, + (key, value) => core.map(pf(key, value), (a) => [key, a] as const) + ), + core.map((chunk) => + RA.isNonEmptyArray(chunk) ? + Option.some(chunk) : + Option.none() + ), + stm.collect((option) => + Option.isSome(option) ? + Option.some(option.value) : + Option.none() + ), + core.flatMap((entries) => + stm.as( + removeAll(self, entries.map((entry) => entry[0])), + RA.map(entries, (entry) => entry[1]) as RA.NonEmptyArray + ) + ) + )) + +const toReadonlyArray = (self: TMap.TMap): STM.STM> => + core.effect>((journal) => { + const buckets = tRef.unsafeGet(self.tBuckets, journal) + const capacity = buckets.chunk.length + const builder: Array = [] + let index = 0 + while (index < capacity) { + const bucket = buckets.chunk[index] + for (const entry of tRef.unsafeGet(bucket, journal)) { + builder.push(entry) + } + index = index + 1 + } + return builder + }) + +/** @internal */ +export const toChunk = (self: TMap.TMap): STM.STM> => + reduce( + self, + Chunk.empty<[K, V]>(), + (acc, value, key) => Chunk.append(acc, [key, value]) + ) + +/** @internal */ +export const toHashMap = (self: TMap.TMap): STM.STM> => + reduce( + self, + HashMap.empty(), + (acc, value, key) => pipe(acc, HashMap.set(key, value)) + ) + +/** @internal */ +export const toArray = (self: TMap.TMap): STM.STM> => + reduce( + self, + [] as Array<[K, V]>, + (acc, value, key) => { + acc.unshift([key, value]) + return acc + } + ) + +/** @internal */ +export const toMap = (self: TMap.TMap): STM.STM> => + reduce( + self, + new Map(), + (acc, value, key) => acc.set(key, value) + ) + +/** @internal */ +export const transform = dual< + (f: (key: K, value: V) => readonly [K, V]) => (self: TMap.TMap) => STM.STM, + (self: TMap.TMap, f: (key: K, value: V) => readonly [K, V]) => STM.STM +>( + 2, + (self: TMap.TMap, f: (key: K, value: V) => readonly [K, V]) => + core.effect((journal) => { + const buckets = pipe(self.tBuckets, tRef.unsafeGet(journal)) + const capacity = buckets.chunk.length + const newBuckets = Array.from({ length: capacity }, () => Chunk.empty()) + let newSize = 0 + let index = 0 + while (index < capacity) { + const bucket = buckets.chunk[index] + const pairs = tRef.unsafeGet(bucket, journal) + const iterator = pairs[Symbol.iterator]() + let next: IteratorResult + while ((next = iterator.next()) && !next.done) { + const newPair = f(next.value[0], next.value[1]) + const index = indexOf(newPair[0], capacity) + const newBucket = newBuckets[index] + if (!Chunk.some(newBucket, (entry) => Equal.equals(entry[0], newPair[0]))) { + newBuckets[index] = Chunk.prepend(newBucket, newPair) + newSize = newSize + 1 + } + } + index = index + 1 + } + index = 0 + while (index < capacity) { + tRef.unsafeSet(buckets.chunk[index], newBuckets[index], journal) + index = index + 1 + } + tRef.unsafeSet(self.tSize, newSize, journal) + }) +) + +/** @internal */ +export const transformSTM = dual< + ( + f: (key: K, value: V) => STM.STM + ) => (self: TMap.TMap) => STM.STM, + (self: TMap.TMap, f: (key: K, value: V) => STM.STM) => STM.STM +>( + 2, + (self: TMap.TMap, f: (key: K, value: V) => STM.STM) => + pipe( + core.flatMap( + toReadonlyArray(self), + stm.forEach((entry) => f(entry[0], entry[1])) + ), + core.flatMap((newData) => + core.effect((journal) => { + const buckets = tRef.unsafeGet(self.tBuckets, journal) + const capacity = buckets.chunk.length + const newBuckets = Array.from({ length: capacity }, () => Chunk.empty()) + const iterator = newData[Symbol.iterator]() + let newSize = 0 + let next: IteratorResult + while ((next = iterator.next()) && !next.done) { + const index = indexOf(next.value[0], capacity) + const newBucket = newBuckets[index] + if (!Chunk.some(newBucket, (entry) => Equal.equals(entry[0])(next.value[0]))) { + newBuckets[index] = Chunk.prepend(newBucket, next.value) + newSize = newSize + 1 + } + } + let index = 0 + while (index < capacity) { + tRef.unsafeSet(buckets.chunk[index], newBuckets[index], journal) + index = index + 1 + } + tRef.unsafeSet(self.tSize, newSize, journal) + }) + ) + ) +) + +/** @internal */ +export const transformValues = dual< + (f: (value: V) => V) => (self: TMap.TMap) => STM.STM, + (self: TMap.TMap, f: (value: V) => V) => STM.STM +>(2, (self, f) => transform(self, (key, value) => [key, f(value)])) + +/** @internal */ +export const transformValuesSTM = dual< + (f: (value: V) => STM.STM) => (self: TMap.TMap) => STM.STM, + (self: TMap.TMap, f: (value: V) => STM.STM) => STM.STM +>(2, (self, f) => + transformSTM( + self, + (key, value) => core.map(f(value), (value) => [key, value]) + )) + +/** @internal */ +export const updateWith = dual< + ( + key: K, + f: (value: Option.Option) => Option.Option + ) => (self: TMap.TMap) => STM.STM>, + ( + self: TMap.TMap, + key: K, + f: (value: Option.Option) => Option.Option + ) => STM.STM> +>(3, (self, key, f) => + core.flatMap(get(self, key), (option) => + Option.match( + f(option), + { + onNone: () => stm.as(remove(self, key), Option.none()), + onSome: (value) => stm.as(set(self, key, value), Option.some(value)) + } + ))) + +/** @internal */ +export const values = (self: TMap.TMap): STM.STM> => + core.map(toReadonlyArray(self), RA.map((entry) => entry[1])) diff --git a/repos/effect/packages/effect/src/internal/stm/tPriorityQueue.ts b/repos/effect/packages/effect/src/internal/stm/tPriorityQueue.ts new file mode 100644 index 0000000..dbc7801 --- /dev/null +++ b/repos/effect/packages/effect/src/internal/stm/tPriorityQueue.ts @@ -0,0 +1,267 @@ +import * as Arr from "../../Array.js" +import * as Chunk from "../../Chunk.js" +import { dual, pipe } from "../../Function.js" +import * as Option from "../../Option.js" +import type * as Order from "../../Order.js" +import type { Predicate } from "../../Predicate.js" +import * as SortedMap from "../../SortedMap.js" +import type * as STM from "../../STM.js" +import type * as TPriorityQueue from "../../TPriorityQueue.js" +import type * as TRef from "../../TRef.js" +import * as core from "./core.js" +import * as tRef from "./tRef.js" + +/** @internal */ +const TPriorityQueueSymbolKey = "effect/TPriorityQueue" + +/** @internal */ +export const TPriorityQueueTypeId: TPriorityQueue.TPriorityQueueTypeId = Symbol.for( + TPriorityQueueSymbolKey +) as TPriorityQueue.TPriorityQueueTypeId + +const tPriorityQueueVariance = { + /* c8 ignore next */ + _A: (_: any) => _ +} + +/** @internal */ +export class TPriorityQueueImpl implements TPriorityQueue.TPriorityQueue { + readonly [TPriorityQueueTypeId] = tPriorityQueueVariance + constructor(readonly ref: TRef.TRef]>>) {} +} + +/** @internal */ +export const empty = (order: Order.Order): STM.STM> => + pipe( + tRef.make(SortedMap.empty]>(order)), + core.map((ref) => new TPriorityQueueImpl(ref)) + ) + +/** @internal */ +export const fromIterable = + (order: Order.Order) => (iterable: Iterable): STM.STM> => + pipe( + tRef.make( + Arr.fromIterable(iterable).reduce( + (map, value) => + pipe( + map, + SortedMap.set( + value, + pipe( + map, + SortedMap.get(value), + Option.match({ + onNone: () => Arr.of(value), + onSome: Arr.prepend(value) + }) + ) + ) + ), + SortedMap.empty]>(order) + ) + ), + core.map((ref) => new TPriorityQueueImpl(ref)) + ) + +/** @internal */ +export const isEmpty = (self: TPriorityQueue.TPriorityQueue): STM.STM => + core.map(tRef.get(self.ref), SortedMap.isEmpty) + +/** @internal */ +export const isNonEmpty = (self: TPriorityQueue.TPriorityQueue): STM.STM => + core.map(tRef.get(self.ref), SortedMap.isNonEmpty) + +/** @internal */ +export const make = (order: Order.Order) => (...elements: Array): STM.STM> => + fromIterable(order)(elements) + +/** @internal */ +export const offer = dual< + (value: A) => (self: TPriorityQueue.TPriorityQueue) => STM.STM, + (self: TPriorityQueue.TPriorityQueue, value: A) => STM.STM +>(2, (self, value) => + tRef.update(self.ref, (map) => + SortedMap.set( + map, + value, + Option.match(SortedMap.get(map, value), { + onNone: () => Arr.of(value), + onSome: Arr.prepend(value) + }) + ))) + +/** @internal */ +export const offerAll = dual< + (values: Iterable) => (self: TPriorityQueue.TPriorityQueue) => STM.STM, + (self: TPriorityQueue.TPriorityQueue, values: Iterable) => STM.STM +>(2, (self, values) => + tRef.update(self.ref, (map) => + Arr.fromIterable(values).reduce( + (map, value) => + SortedMap.set( + map, + value, + Option.match(SortedMap.get(map, value), { + onNone: () => Arr.of(value), + onSome: Arr.prepend(value) + }) + ), + map + ))) + +/** @internal */ +export const peek = (self: TPriorityQueue.TPriorityQueue): STM.STM => + core.withSTMRuntime((runtime) => { + const map = tRef.unsafeGet(self.ref, runtime.journal) + return Option.match( + SortedMap.headOption(map), + { + onNone: () => core.retry, + onSome: (elements) => core.succeed(elements[0]) + } + ) + }) + +/** @internal */ +export const peekOption = (self: TPriorityQueue.TPriorityQueue): STM.STM> => + tRef.modify(self.ref, (map) => [ + Option.map(SortedMap.headOption(map), (elements) => elements[0]), + map + ]) + +/** @internal */ +export const removeIf = dual< + (predicate: Predicate) => (self: TPriorityQueue.TPriorityQueue) => STM.STM, + (self: TPriorityQueue.TPriorityQueue, predicate: Predicate) => STM.STM +>(2, (self, predicate) => retainIf(self, (a) => !predicate(a))) + +/** @internal */ +export const retainIf = dual< + (predicate: Predicate) => (self: TPriorityQueue.TPriorityQueue) => STM.STM, + (self: TPriorityQueue.TPriorityQueue, predicate: Predicate) => STM.STM +>( + 2, + (self: TPriorityQueue.TPriorityQueue, predicate: Predicate) => + tRef.update( + self.ref, + (map) => + SortedMap.reduce(map, SortedMap.empty(SortedMap.getOrder(map)), (map, value, key) => { + const filtered: ReadonlyArray = Arr.filter(value, predicate) + return filtered.length > 0 ? + SortedMap.set(map, key, filtered as [A, ...Array]) : + SortedMap.remove(map, key) + }) + ) +) + +/** @internal */ +export const size = (self: TPriorityQueue.TPriorityQueue): STM.STM => + tRef.modify( + self.ref, + (map) => [SortedMap.reduce(map, 0, (n, as) => n + as.length), map] + ) + +/** @internal */ +export const take = (self: TPriorityQueue.TPriorityQueue): STM.STM => + core.withSTMRuntime((runtime) => { + const map = tRef.unsafeGet(self.ref, runtime.journal) + return Option.match(SortedMap.headOption(map), { + onNone: () => core.retry, + onSome: (values) => { + const head = values[1][0] + const tail = values[1].slice(1) + tRef.unsafeSet( + self.ref, + tail.length > 0 ? + SortedMap.set(map, head, tail as [A, ...Array]) : + SortedMap.remove(map, head), + runtime.journal + ) + return core.succeed(head) + } + }) + }) + +/** @internal */ +export const takeAll = (self: TPriorityQueue.TPriorityQueue): STM.STM> => + tRef.modify(self.ref, (map) => { + const builder: Array = [] + for (const entry of map) { + for (const value of entry[1]) { + builder.push(value) + } + } + return [builder, SortedMap.empty(SortedMap.getOrder(map))] + }) + +/** @internal */ +export const takeOption = (self: TPriorityQueue.TPriorityQueue): STM.STM> => + core.effect>((journal) => { + const map = pipe(self.ref, tRef.unsafeGet(journal)) + return Option.match(SortedMap.headOption(map), { + onNone: (): Option.Option => Option.none(), + onSome: ([key, value]) => { + const tail = value.slice(1) + tRef.unsafeSet( + self.ref, + tail.length > 0 ? + SortedMap.set(map, key, tail as [A, ...Array]) : + SortedMap.remove(map, key), + journal + ) + return Option.some(value[0]) + } + }) + }) + +/** @internal */ +export const takeUpTo = dual< + (n: number) => (self: TPriorityQueue.TPriorityQueue) => STM.STM>, + (self: TPriorityQueue.TPriorityQueue, n: number) => STM.STM> +>(2, (self: TPriorityQueue.TPriorityQueue, n: number) => + tRef.modify(self.ref, (map) => { + const builder: Array = [] + const iterator = map[Symbol.iterator]() + let updated = map + let index = 0 + let next: IteratorResult]], any> + while ((next = iterator.next()) && !next.done && index < n) { + const [key, value] = next.value + const [left, right] = pipe(value, Arr.splitAt(n - index)) + for (const value of left) { + builder.push(value) + } + if (right.length > 0) { + updated = SortedMap.set(updated, key, right as [A, ...Array]) + } else { + updated = SortedMap.remove(updated, key) + } + index = index + left.length + } + return [builder, updated] + })) + +/** @internal */ +export const toChunk = (self: TPriorityQueue.TPriorityQueue): STM.STM> => + tRef.modify(self.ref, (map) => { + const builder: Array = [] + for (const entry of map) { + for (const value of entry[1]) { + builder.push(value) + } + } + return [Chunk.unsafeFromArray(builder), map] + }) + +/** @internal */ +export const toArray = (self: TPriorityQueue.TPriorityQueue): STM.STM> => + tRef.modify(self.ref, (map) => { + const builder: Array = [] + for (const entry of map) { + for (const value of entry[1]) { + builder.push(value) + } + } + return [builder, map] + }) diff --git a/repos/effect/packages/effect/src/internal/stm/tPubSub.ts b/repos/effect/packages/effect/src/internal/stm/tPubSub.ts new file mode 100644 index 0000000..b838ddb --- /dev/null +++ b/repos/effect/packages/effect/src/internal/stm/tPubSub.ts @@ -0,0 +1,551 @@ +import * as RA from "../../Array.js" +import * as Effect from "../../Effect.js" +import { dual, identity, pipe } from "../../Function.js" +import * as HashSet from "../../HashSet.js" +import * as Option from "../../Option.js" +import type * as Scope from "../../Scope.js" +import type * as STM from "../../STM.js" +import type * as TPubSub from "../../TPubSub.js" +import type * as TQueue from "../../TQueue.js" +import type * as TRef from "../../TRef.js" +import * as core from "./core.js" +import * as OpCodes from "./opCodes/strategy.js" +import * as stm from "./stm.js" +import * as tQueue from "./tQueue.js" +import * as tRef from "./tRef.js" + +/** @internal */ +const TPubSubSymbolKey = "effect/TPubSub" + +/** @internal */ +export const TPubSubTypeId: TPubSub.TPubSubTypeId = Symbol.for(TPubSubSymbolKey) as TPubSub.TPubSubTypeId + +const AbsentValue = Symbol.for("effect/TPubSub/AbsentValue") +type AbsentValue = typeof AbsentValue + +/** @internal */ +export interface Node { + readonly head: A | AbsentValue + readonly subscribers: number + readonly tail: TRef.TRef | undefined> +} + +/** @internal */ +export const makeNode = ( + head: A | AbsentValue, + subscribers: number, + tail: TRef.TRef | undefined> +): Node => ({ + head, + subscribers, + tail +}) + +/** @internal */ +class TPubSubImpl implements TPubSub.TPubSub { + readonly [TPubSubTypeId] = { + _A: (_: any) => _ + } + readonly [tQueue.TEnqueueTypeId] = tQueue.tEnqueueVariance + constructor( + readonly pubsubSize: TRef.TRef, + readonly publisherHead: TRef.TRef | undefined>>, + readonly publisherTail: TRef.TRef | undefined> | undefined>, + readonly requestedCapacity: number, + readonly strategy: tQueue.TQueueStrategy, + readonly subscriberCount: TRef.TRef, + readonly subscribers: TRef.TRef> | undefined>>> + ) {} + + isShutdown: STM.STM = core.effect((journal) => { + const currentPublisherTail = tRef.unsafeGet(this.publisherTail, journal) + return currentPublisherTail === undefined + }) + + awaitShutdown: STM.STM = core.flatMap( + this.isShutdown, + (isShutdown) => isShutdown ? stm.void : core.retry + ) + + capacity(): number { + return this.requestedCapacity + } + + size: STM.STM = core.withSTMRuntime((runtime) => { + const currentPublisherTail = tRef.unsafeGet(this.publisherTail, runtime.journal) + if (currentPublisherTail === undefined) { + return core.interruptAs(runtime.fiberId) + } + return core.succeed(tRef.unsafeGet(this.pubsubSize, runtime.journal)) + }) + + isEmpty: STM.STM = core.map(this.size, (size) => size === 0) + + isFull: STM.STM = core.map(this.size, (size) => size === this.capacity()) + + offer(value: A): STM.STM { + return core.withSTMRuntime((runtime) => { + const currentPublisherTail = tRef.unsafeGet(this.publisherTail, runtime.journal) + if (currentPublisherTail === undefined) { + return core.interruptAs(runtime.fiberId) + } + const currentSubscriberCount = tRef.unsafeGet(this.subscriberCount, runtime.journal) + if (currentSubscriberCount === 0) { + return core.succeed(true) + } + const currentPubSubSize = tRef.unsafeGet(this.pubsubSize, runtime.journal) + if (currentPubSubSize < this.requestedCapacity) { + const updatedPublisherTail: TRef.TRef | undefined> = new tRef.TRefImpl | undefined>(void 0) + const updatedNode = makeNode(value, currentSubscriberCount, updatedPublisherTail) + tRef.unsafeSet | undefined>(currentPublisherTail, updatedNode, runtime.journal) + tRef.unsafeSet | undefined> | undefined>( + this.publisherTail, + updatedPublisherTail, + runtime.journal + ) + tRef.unsafeSet(this.pubsubSize, currentPubSubSize + 1, runtime.journal) + return core.succeed(true) + } + switch (this.strategy._tag) { + case OpCodes.OP_BACKPRESSURE_STRATEGY: { + return core.retry + } + case OpCodes.OP_DROPPING_STRATEGY: { + return core.succeed(false) + } + case OpCodes.OP_SLIDING_STRATEGY: { + if (this.requestedCapacity > 0) { + let currentPublisherHead: TRef.TRef | undefined> = tRef.unsafeGet( + this.publisherHead, + runtime.journal + ) + let loop = true + while (loop) { + const node = tRef.unsafeGet(currentPublisherHead, runtime.journal) + if (node === undefined) { + return core.retry + } + const head = node.head + const tail = node.tail + if (head !== AbsentValue) { + const updatedNode = makeNode(AbsentValue, node.subscribers, node.tail as any) + tRef.unsafeSet | undefined>( + currentPublisherHead as any, + updatedNode as any, + runtime.journal + ) + tRef.unsafeSet(this.publisherHead, tail, runtime.journal) + loop = false + } else { + currentPublisherHead = tail + } + } + } + const updatedPublisherTail: TRef.TRef | undefined> = new tRef.TRefImpl | undefined>(void 0) + const updatedNode = makeNode(value, currentSubscriberCount, updatedPublisherTail) + tRef.unsafeSet | undefined>(currentPublisherTail, updatedNode, runtime.journal) + tRef.unsafeSet | undefined> | undefined>( + this.publisherTail, + updatedPublisherTail, + runtime.journal + ) + return core.succeed(true) + } + } + }) + } + + offerAll(iterable: Iterable): STM.STM { + return core.map( + stm.forEach(iterable, (a) => this.offer(a)), + RA.every(identity) + ) + } + + shutdown: STM.STM = core.effect((journal) => { + const currentPublisherTail = tRef.unsafeGet(this.publisherTail, journal) + if (currentPublisherTail !== undefined) { + tRef.unsafeSet | undefined> | undefined>(this.publisherTail, void 0, journal) + const currentSubscribers = tRef.unsafeGet(this.subscribers, journal) + HashSet.forEach(currentSubscribers, (subscriber) => { + tRef.unsafeSet> | undefined>(subscriber, void 0, journal) + }) + tRef.unsafeSet(this.subscribers, HashSet.empty> | undefined>>(), journal) + } + }) +} + +/** @internal */ +class TPubSubSubscriptionImpl implements TQueue.TDequeue { + readonly [TPubSubTypeId]: TPubSub.TPubSubTypeId = TPubSubTypeId + readonly [tQueue.TDequeueTypeId] = tQueue.tDequeueVariance + constructor( + readonly pubsubSize: TRef.TRef, + readonly publisherHead: TRef.TRef | undefined>>, + readonly requestedCapacity: number, + readonly subscriberHead: TRef.TRef | undefined> | undefined>, + readonly subscriberCount: TRef.TRef, + readonly subscribers: TRef.TRef> | undefined>>> + ) {} + + isShutdown: STM.STM = core.effect((journal) => { + const currentSubscriberHead = tRef.unsafeGet(this.subscriberHead, journal) + return currentSubscriberHead === undefined + }) + + awaitShutdown: STM.STM = core.flatMap( + this.isShutdown, + (isShutdown) => isShutdown ? stm.void : core.retry + ) + + capacity(): number { + return this.requestedCapacity + } + + size: STM.STM = core.withSTMRuntime((runtime) => { + let currentSubscriberHead = tRef.unsafeGet(this.subscriberHead, runtime.journal) + if (currentSubscriberHead === undefined) { + return core.interruptAs(runtime.fiberId) + } + let loop = true + let size = 0 + while (loop) { + const node = tRef.unsafeGet(currentSubscriberHead, runtime.journal) + if (node === undefined) { + loop = false + } else { + const head = node.head + const tail: TRef.TRef | undefined> = node.tail + if (head !== AbsentValue) { + size = size + 1 + if (size >= Number.MAX_SAFE_INTEGER) { + loop = false + } + } + currentSubscriberHead = tail + } + } + return core.succeed(size) + }) + + isEmpty: STM.STM = core.map(this.size, (size) => size === 0) + + isFull: STM.STM = core.map(this.size, (size) => size === this.capacity()) + + peek: STM.STM = core.withSTMRuntime((runtime) => { + let currentSubscriberHead = tRef.unsafeGet(this.subscriberHead, runtime.journal) + if (currentSubscriberHead === undefined) { + return core.interruptAs(runtime.fiberId) + } + let value: A | AbsentValue = AbsentValue + let loop = true + while (loop) { + const node = tRef.unsafeGet(currentSubscriberHead, runtime.journal) + if (node === undefined) { + return core.retry + } + const head = node.head + const tail: TRef.TRef | undefined> = node.tail + if (head !== AbsentValue) { + value = head + loop = false + } else { + currentSubscriberHead = tail + } + } + return core.succeed(value as A) + }) + + peekOption: STM.STM> = core.withSTMRuntime((runtime) => { + let currentSubscriberHead = tRef.unsafeGet(this.subscriberHead, runtime.journal) + if (currentSubscriberHead === undefined) { + return core.interruptAs(runtime.fiberId) + } + let value: Option.Option = Option.none() + let loop = true + while (loop) { + const node = tRef.unsafeGet(currentSubscriberHead, runtime.journal) + if (node === undefined) { + value = Option.none() + loop = false + } else { + const head = node.head + const tail: TRef.TRef | undefined> = node.tail + if (head !== AbsentValue) { + value = Option.some(head) + loop = false + } else { + currentSubscriberHead = tail + } + } + } + return core.succeed(value) + }) + + shutdown: STM.STM = core.effect((journal) => { + let currentSubscriberHead = tRef.unsafeGet(this.subscriberHead, journal) + if (currentSubscriberHead !== undefined) { + tRef.unsafeSet | undefined> | undefined>(this.subscriberHead, void 0, journal) + let loop = true + while (loop) { + const node = tRef.unsafeGet(currentSubscriberHead, journal) + if (node === undefined) { + loop = false + } else { + const head = node.head + const tail: TRef.TRef | undefined> = node.tail + if (head !== AbsentValue) { + const subscribers = node.subscribers + if (subscribers === 1) { + const size = tRef.unsafeGet(this.pubsubSize, journal) + const updatedNode = makeNode(AbsentValue, 0, tail) + tRef.unsafeSet | undefined>(currentSubscriberHead, updatedNode, journal) + tRef.unsafeSet(this.publisherHead, tail as any, journal) + tRef.unsafeSet(this.pubsubSize, size - 1, journal) + } else { + const updatedNode = makeNode(head, subscribers - 1, tail) + tRef.unsafeSet | undefined>(currentSubscriberHead, updatedNode, journal) + } + } + currentSubscriberHead = tail + } + } + const currentSubscriberCount = tRef.unsafeGet(this.subscriberCount, journal) + tRef.unsafeSet(this.subscriberCount, currentSubscriberCount - 1, journal) + tRef.unsafeSet( + this.subscribers, + HashSet.remove( + tRef.unsafeGet(this.subscribers, journal), + this.subscriberHead as any + ), + journal + ) + } + }) + + take: STM.STM = core.withSTMRuntime((runtime) => { + let currentSubscriberHead = tRef.unsafeGet(this.subscriberHead, runtime.journal) + if (currentSubscriberHead === undefined) { + return core.interruptAs(runtime.fiberId) + } + let value: A | AbsentValue = AbsentValue + let loop = true + while (loop) { + const node = tRef.unsafeGet(currentSubscriberHead, runtime.journal) + if (node === undefined) { + return core.retry + } + const head = node.head + const tail: TRef.TRef | undefined> = node.tail + if (head !== AbsentValue) { + const subscribers = node.subscribers + if (subscribers === 1) { + const size = tRef.unsafeGet(this.pubsubSize, runtime.journal) + const updatedNode = makeNode(AbsentValue, 0, tail) + tRef.unsafeSet | undefined>(currentSubscriberHead, updatedNode, runtime.journal) + tRef.unsafeSet(this.publisherHead, tail as any, runtime.journal) + tRef.unsafeSet(this.pubsubSize, size - 1, runtime.journal) + } else { + const updatedNode = makeNode(head, subscribers - 1, tail) + tRef.unsafeSet | undefined>(currentSubscriberHead, updatedNode, runtime.journal) + } + tRef.unsafeSet | undefined> | undefined>( + this.subscriberHead, + tail, + runtime.journal + ) + value = head + loop = false + } else { + currentSubscriberHead = tail + } + } + return core.succeed(value as A) + }) + + takeAll: STM.STM> = this.takeUpTo(Number.POSITIVE_INFINITY) + + takeUpTo(max: number): STM.STM> { + return core.withSTMRuntime((runtime) => { + let currentSubscriberHead = tRef.unsafeGet(this.subscriberHead, runtime.journal) + if (currentSubscriberHead === undefined) { + return core.interruptAs(runtime.fiberId) + } + const builder: Array = [] + let n = 0 + while (n !== max) { + const node = tRef.unsafeGet(currentSubscriberHead, runtime.journal) + if (node === undefined) { + n = max + } else { + const head = node.head + const tail: TRef.TRef | undefined> = node.tail + if (head !== AbsentValue) { + const subscribers = node.subscribers + if (subscribers === 1) { + const size = tRef.unsafeGet(this.pubsubSize, runtime.journal) + const updatedNode = makeNode(AbsentValue, 0, tail) + tRef.unsafeSet | undefined>(currentSubscriberHead, updatedNode, runtime.journal) + tRef.unsafeSet(this.publisherHead, tail as any, runtime.journal) + tRef.unsafeSet(this.pubsubSize, size - 1, runtime.journal) + } else { + const updatedNode = makeNode(head, subscribers - 1, tail) + tRef.unsafeSet | undefined>(currentSubscriberHead, updatedNode, runtime.journal) + } + builder.push(head) + n = n + 1 + } + currentSubscriberHead = tail + } + } + tRef.unsafeSet | undefined> | undefined>( + this.subscriberHead, + currentSubscriberHead, + runtime.journal + ) + return core.succeed(builder) + }) + } +} + +/** @internal */ +const makeTPubSub = ( + requestedCapacity: number, + strategy: tQueue.TQueueStrategy +): STM.STM> => + pipe( + stm.all([ + tRef.make | undefined>(void 0), + tRef.make(0) + ]), + core.flatMap(([empty, pubsubSize]) => + pipe( + stm.all([ + tRef.make(empty), + tRef.make(empty), + tRef.make(0), + tRef.make(HashSet.empty()) + ]), + core.map(([publisherHead, publisherTail, subscriberCount, subscribers]) => + new TPubSubImpl( + pubsubSize, + publisherHead, + publisherTail as any, + requestedCapacity, + strategy, + subscriberCount, + subscribers as any + ) + ) + ) + ) + ) + +const makeSubscription = ( + pubsubSize: TRef.TRef, + publisherHead: TRef.TRef | undefined>>, + publisherTail: TRef.TRef | undefined> | undefined>, + requestedCapacity: number, + subscriberCount: TRef.TRef, + subscribers: TRef.TRef> | undefined>>> +): STM.STM> => + pipe( + tRef.get(publisherTail), + core.flatMap((currentPublisherTail) => + pipe( + stm.all([ + tRef.make(currentPublisherTail), + tRef.get(subscriberCount), + tRef.get(subscribers) + ]), + stm.tap(([_, currentSubscriberCount]) => + pipe( + subscriberCount, + tRef.set(currentSubscriberCount + 1) + ) + ), + stm.tap(([subscriberHead, _, currentSubscribers]) => + pipe( + subscribers as any, + tRef.set(pipe(currentSubscribers as any, HashSet.add(subscriberHead))) + ) + ), + core.map(([subscriberHead]) => + new TPubSubSubscriptionImpl( + pubsubSize, + publisherHead, + requestedCapacity, + subscriberHead as any, + subscriberCount, + subscribers + ) + ) + ) + ) + ) + +/** @internal */ +export const awaitShutdown = (self: TPubSub.TPubSub): STM.STM => self.awaitShutdown + +/** @internal */ +export const bounded = (requestedCapacity: number): STM.STM> => + makeTPubSub(requestedCapacity, tQueue.BackPressure) + +/** @internal */ +export const capacity = (self: TPubSub.TPubSub): number => self.capacity() + +/** @internal */ +export const dropping = (requestedCapacity: number): STM.STM> => + makeTPubSub(requestedCapacity, tQueue.Dropping) + +/** @internal */ +export const isEmpty = (self: TPubSub.TPubSub): STM.STM => self.isEmpty + +/** @internal */ +export const isFull = (self: TPubSub.TPubSub): STM.STM => self.isFull + +/** @internal */ +export const isShutdown = (self: TPubSub.TPubSub): STM.STM => self.isShutdown + +/** @internal */ +export const publish = dual< + (value: A) => (self: TPubSub.TPubSub) => STM.STM, + (self: TPubSub.TPubSub, value: A) => STM.STM +>(2, (self, value) => self.offer(value)) + +/** @internal */ +export const publishAll = dual< + (iterable: Iterable) => (self: TPubSub.TPubSub) => STM.STM, + (self: TPubSub.TPubSub, iterable: Iterable) => STM.STM +>(2, (self, iterable) => self.offerAll(iterable)) + +/** @internal */ +export const size = (self: TPubSub.TPubSub): STM.STM => self.size + +/** @internal */ +export const shutdown = (self: TPubSub.TPubSub): STM.STM => self.shutdown + +/** @internal */ +export const sliding = (requestedCapacity: number): STM.STM> => + makeTPubSub(requestedCapacity, tQueue.Sliding) + +/** @internal */ +export const subscribe = (self: TPubSub.TPubSub): STM.STM> => + makeSubscription( + self.pubsubSize, + self.publisherHead, + self.publisherTail, + self.requestedCapacity, + self.subscriberCount, + self.subscribers + ) + +/** @internal */ +export const subscribeScoped = (self: TPubSub.TPubSub): Effect.Effect, never, Scope.Scope> => + Effect.acquireRelease( + subscribe(self), + (dequeue) => tQueue.shutdown(dequeue) + ) + +/** @internal */ +export const unbounded = (): STM.STM> => makeTPubSub(Number.MAX_SAFE_INTEGER, tQueue.Dropping) diff --git a/repos/effect/packages/effect/src/internal/stm/tQueue.ts b/repos/effect/packages/effect/src/internal/stm/tQueue.ts new file mode 100644 index 0000000..8ca27b9 --- /dev/null +++ b/repos/effect/packages/effect/src/internal/stm/tQueue.ts @@ -0,0 +1,393 @@ +import * as RA from "../../Array.js" +import * as Chunk from "../../Chunk.js" +import { dual, pipe } from "../../Function.js" +import * as Option from "../../Option.js" +import { hasProperty, type Predicate } from "../../Predicate.js" +import type * as STM from "../../STM.js" +import type * as TQueue from "../../TQueue.js" +import type * as TRef from "../../TRef.js" +import * as core from "./core.js" +import * as OpCodes from "./opCodes/strategy.js" +import * as stm from "./stm.js" +import * as tRef from "./tRef.js" + +const TEnqueueSymbolKey = "effect/TQueue/TEnqueue" + +/** @internal */ +export const TEnqueueTypeId: TQueue.TEnqueueTypeId = Symbol.for(TEnqueueSymbolKey) as TQueue.TEnqueueTypeId + +const TDequeueSymbolKey = "effect/TQueue/TDequeue" + +/** @internal */ +export const TDequeueTypeId: TQueue.TDequeueTypeId = Symbol.for(TDequeueSymbolKey) as TQueue.TDequeueTypeId + +/** + * A `Strategy` describes how the queue will handle values if the queue is at + * capacity. + * + * @internal + */ +export type TQueueStrategy = BackPressure | Dropping | Sliding + +/** + * A strategy that retries if the queue is at capacity. + * + * @internal + */ +export interface BackPressure { + readonly _tag: OpCodes.OP_BACKPRESSURE_STRATEGY +} + +/** + * A strategy that drops new values if the queue is at capacity. + * + * @internal + */ +export interface Dropping { + readonly _tag: OpCodes.OP_DROPPING_STRATEGY +} + +/** + * A strategy that drops old values if the queue is at capacity. + * + * @internal + */ +export interface Sliding { + readonly _tag: OpCodes.OP_SLIDING_STRATEGY +} + +/** @internal */ +export const BackPressure: TQueueStrategy = { + _tag: OpCodes.OP_BACKPRESSURE_STRATEGY +} + +/** @internal */ +export const Dropping: TQueueStrategy = { + _tag: OpCodes.OP_DROPPING_STRATEGY +} + +/** @internal */ +export const Sliding: TQueueStrategy = { + _tag: OpCodes.OP_SLIDING_STRATEGY +} + +/** @internal */ +export const tDequeueVariance = { + /* c8 ignore next */ + _Out: (_: never) => _ +} + +/** @internal */ +export const tEnqueueVariance = { + /* c8 ignore next */ + _In: (_: unknown) => _ +} + +class TQueueImpl implements TQueue.TQueue { + readonly [TDequeueTypeId] = tDequeueVariance + readonly [TEnqueueTypeId] = tEnqueueVariance + constructor( + readonly ref: TRef.TRef | undefined>, + readonly requestedCapacity: number, + readonly strategy: TQueueStrategy + ) {} + + capacity(): number { + return this.requestedCapacity + } + + size: STM.STM = core.withSTMRuntime((runtime) => { + const queue = tRef.unsafeGet(this.ref, runtime.journal) + if (queue === undefined) { + return core.interruptAs(runtime.fiberId) + } + return core.succeed(queue.length) + }) + + isFull: STM.STM = core.map(this.size, (size) => size === this.requestedCapacity) + + isEmpty: STM.STM = core.map(this.size, (size) => size === 0) + + shutdown: STM.STM = core.withSTMRuntime((runtime) => { + tRef.unsafeSet(this.ref, void 0, runtime.journal) + return stm.void + }) + + isShutdown: STM.STM = core.effect((journal) => { + const queue = tRef.unsafeGet(this.ref, journal) + return queue === undefined + }) + + awaitShutdown: STM.STM = core.flatMap( + this.isShutdown, + (isShutdown) => isShutdown ? stm.void : core.retry + ) + + offer(value: A): STM.STM { + return core.withSTMRuntime((runtime) => { + const queue = pipe(this.ref, tRef.unsafeGet(runtime.journal)) + if (queue === undefined) { + return core.interruptAs(runtime.fiberId) + } + if (queue.length < this.requestedCapacity) { + queue.push(value) + tRef.unsafeSet(this.ref, queue, runtime.journal) + return core.succeed(true) + } + switch (this.strategy._tag) { + case OpCodes.OP_BACKPRESSURE_STRATEGY: { + return core.retry + } + case OpCodes.OP_DROPPING_STRATEGY: { + return core.succeed(false) + } + case OpCodes.OP_SLIDING_STRATEGY: { + if (queue.length === 0) { + return core.succeed(true) + } + queue.shift() + queue.push(value) + tRef.unsafeSet(this.ref, queue, runtime.journal) + return core.succeed(true) + } + } + }) + } + + offerAll(iterable: Iterable): STM.STM { + return core.withSTMRuntime((runtime) => { + const as = Array.from(iterable) + const queue = tRef.unsafeGet(this.ref, runtime.journal) + if (queue === undefined) { + return core.interruptAs(runtime.fiberId) + } + if (queue.length + as.length <= this.requestedCapacity) { + tRef.unsafeSet(this.ref, [...queue, ...as], runtime.journal) + return core.succeed(true) + } + switch (this.strategy._tag) { + case OpCodes.OP_BACKPRESSURE_STRATEGY: { + return core.retry + } + case OpCodes.OP_DROPPING_STRATEGY: { + const forQueue = as.slice(0, this.requestedCapacity - queue.length) + tRef.unsafeSet(this.ref, [...queue, ...forQueue], runtime.journal) + return core.succeed(false) + } + case OpCodes.OP_SLIDING_STRATEGY: { + const forQueue = as.slice(0, this.requestedCapacity - queue.length) + const toDrop = queue.length + forQueue.length - this.requestedCapacity + const newQueue = queue.slice(toDrop) + tRef.unsafeSet(this.ref, [...newQueue, ...forQueue], runtime.journal) + return core.succeed(true) + } + } + }) + } + + peek: STM.STM = core.withSTMRuntime((runtime) => { + const queue = tRef.unsafeGet(this.ref, runtime.journal) + if (queue === undefined) { + return core.interruptAs(runtime.fiberId) + } + if (queue.length === 0) { + return core.retry + } + return core.succeed(queue[0]) + }) + + peekOption: STM.STM> = core.withSTMRuntime((runtime) => { + const queue = tRef.unsafeGet(this.ref, runtime.journal) + if (queue === undefined) { + return core.interruptAs(runtime.fiberId) + } + return core.succeed(Option.fromNullable(queue[0])) + }) + + take: STM.STM = core.withSTMRuntime((runtime) => { + const queue = tRef.unsafeGet(this.ref, runtime.journal) + if (queue === undefined) { + return core.interruptAs(runtime.fiberId) + } + if (queue.length === 0) { + return core.retry + } + const dequeued = queue.shift()! + tRef.unsafeSet(this.ref, queue, runtime.journal) + return core.succeed(dequeued) + }) + + takeAll: STM.STM> = core.withSTMRuntime((runtime) => { + const queue = tRef.unsafeGet(this.ref, runtime.journal) + if (queue === undefined) { + return core.interruptAs(runtime.fiberId) + } + tRef.unsafeSet(this.ref, [], runtime.journal) + return core.succeed(queue) + }) + + takeUpTo(max: number): STM.STM> { + return core.withSTMRuntime((runtime) => { + const queue = tRef.unsafeGet(this.ref, runtime.journal) + if (queue === undefined) { + return core.interruptAs(runtime.fiberId) + } + const [toTake, remaining] = Chunk.splitAt(Chunk.unsafeFromArray(queue), max) + tRef.unsafeSet | undefined>(this.ref, Array.from(remaining), runtime.journal) + return core.succeed(Array.from(toTake)) + }) + } +} + +/** @internal */ +export const isTQueue = (u: unknown): u is TQueue.TQueue => { + return isTEnqueue(u) && isTDequeue(u) +} + +/** @internal */ +export const isTEnqueue = (u: unknown): u is TQueue.TEnqueue => hasProperty(u, TEnqueueTypeId) + +/** @internal */ +export const isTDequeue = (u: unknown): u is TQueue.TDequeue => hasProperty(u, TDequeueTypeId) + +/** @internal */ +export const awaitShutdown = (self: TQueue.TDequeue | TQueue.TEnqueue): STM.STM => self.awaitShutdown + +/** @internal */ +export const bounded = (requestedCapacity: number): STM.STM> => + makeQueue(requestedCapacity, BackPressure) + +/** @internal */ +export const capacity = (self: TQueue.TDequeue | TQueue.TEnqueue): number => { + return self.capacity() +} + +/** @internal */ +export const dropping = (requestedCapacity: number): STM.STM> => + makeQueue(requestedCapacity, Dropping) + +/** @internal */ +export const isEmpty = (self: TQueue.TDequeue | TQueue.TEnqueue): STM.STM => self.isEmpty + +/** @internal */ +export const isFull = (self: TQueue.TDequeue | TQueue.TEnqueue): STM.STM => self.isFull + +/** @internal */ +export const isShutdown = (self: TQueue.TDequeue | TQueue.TEnqueue): STM.STM => self.isShutdown + +/** @internal */ +export const offer = dual< + (value: A) => (self: TQueue.TEnqueue) => STM.STM, + (self: TQueue.TEnqueue, value: A) => STM.STM +>(2, (self, value) => self.offer(value)) + +/** @internal */ +export const offerAll = dual< + (iterable: Iterable) => (self: TQueue.TEnqueue) => STM.STM, + (self: TQueue.TEnqueue, iterable: Iterable) => STM.STM +>(2, (self, iterable) => self.offerAll(iterable)) + +/** @internal */ +export const peek = (self: TQueue.TDequeue): STM.STM => self.peek + +/** @internal */ +export const peekOption = (self: TQueue.TDequeue): STM.STM> => self.peekOption + +/** @internal */ +export const poll = (self: TQueue.TDequeue): STM.STM> => + pipe(self.takeUpTo(1), core.map(RA.head)) + +/** @internal */ +export const seek = dual< + (predicate: Predicate) => (self: TQueue.TDequeue) => STM.STM, + (self: TQueue.TDequeue, predicate: Predicate) => STM.STM +>(2, (self, predicate) => seekLoop(self, predicate)) + +const seekLoop = (self: TQueue.TDequeue, predicate: Predicate): STM.STM => + core.flatMap( + self.take, + (a) => predicate(a) ? core.succeed(a) : seekLoop(self, predicate) + ) + +/** @internal */ +export const shutdown = (self: TQueue.TDequeue | TQueue.TEnqueue): STM.STM => self.shutdown + +/** @internal */ +export const size = (self: TQueue.TDequeue | TQueue.TEnqueue): STM.STM => self.size + +/** @internal */ +export const sliding = (requestedCapacity: number): STM.STM> => + makeQueue(requestedCapacity, Sliding) + +/** @internal */ +export const take = (self: TQueue.TDequeue): STM.STM => self.take + +/** @internal */ +export const takeAll = (self: TQueue.TDequeue): STM.STM> => self.takeAll + +/** @internal */ +export const takeBetween = dual< + (min: number, max: number) => (self: TQueue.TDequeue) => STM.STM>, + (self: TQueue.TDequeue, min: number, max: number) => STM.STM> +>( + 3, + (self: TQueue.TDequeue, min: number, max: number): STM.STM> => + stm.suspend(() => { + const takeRemainder = ( + min: number, + max: number, + acc: Chunk.Chunk + ): STM.STM> => { + if (max < min) { + return core.succeed(acc) + } + return pipe( + self.takeUpTo(max), + core.flatMap((taken) => { + const remaining = min - taken.length + if (remaining === 1) { + return pipe( + self.take, + core.map((a) => pipe(acc, Chunk.appendAll(Chunk.unsafeFromArray(taken)), Chunk.append(a))) + ) + } + if (remaining > 1) { + return pipe( + self.take, + core.flatMap((a) => + takeRemainder( + remaining - 1, + max - taken.length - 1, + pipe(acc, Chunk.appendAll(Chunk.unsafeFromArray(taken)), Chunk.append(a)) + ) + ) + ) + } + return core.succeed(pipe(acc, Chunk.appendAll(Chunk.unsafeFromArray(taken)))) + }) + ) + } + return core.map(takeRemainder(min, max, Chunk.empty()), (c) => Array.from(c)) + }) +) + +/** @internal */ +export const takeN = dual< + (n: number) => (self: TQueue.TDequeue) => STM.STM>, + (self: TQueue.TDequeue, n: number) => STM.STM> +>(2, (self, n) => pipe(self, takeBetween(n, n))) + +/** @internal */ +export const takeUpTo = dual< + (max: number) => (self: TQueue.TDequeue) => STM.STM>, + (self: TQueue.TDequeue, max: number) => STM.STM> +>(2, (self, max) => self.takeUpTo(max)) + +/** @internal */ +export const unbounded = (): STM.STM> => makeQueue(Number.MAX_SAFE_INTEGER, Dropping) + +const makeQueue = (requestedCapacity: number, strategy: TQueueStrategy): STM.STM> => + core.map( + tRef.make | undefined>([]), + (ref) => new TQueueImpl(ref, requestedCapacity, strategy) + ) diff --git a/repos/effect/packages/effect/src/internal/stm/tRandom.ts b/repos/effect/packages/effect/src/internal/stm/tRandom.ts new file mode 100644 index 0000000..6e5c4b4 --- /dev/null +++ b/repos/effect/packages/effect/src/internal/stm/tRandom.ts @@ -0,0 +1,140 @@ +import * as Context from "../../Context.js" +import { pipe } from "../../Function.js" +import * as Layer from "../../Layer.js" +import type * as STM from "../../STM.js" +import type * as TArray from "../../TArray.js" +import type * as TRandom from "../../TRandom.js" +import type * as TRef from "../../TRef.js" +import * as Random from "../../Utils.js" +import * as core from "./core.js" +import * as stm from "./stm.js" +import * as tArray from "./tArray.js" +import * as tRef from "./tRef.js" + +const TRandomSymbolKey = "effect/TRandom" + +/** @internal */ +export const TRandomTypeId: TRandom.TRandomTypeId = Symbol.for( + TRandomSymbolKey +) as TRandom.TRandomTypeId + +const randomInteger = (state: Random.PCGRandomState): [number, Random.PCGRandomState] => { + const prng = new Random.PCGRandom() + prng.setState(state) + return [prng.integer(0), prng.getState()] +} + +const randomIntegerBetween = (low: number, high: number) => { + return (state: Random.PCGRandomState): [number, Random.PCGRandomState] => { + const prng = new Random.PCGRandom() + prng.setState(state) + return [prng.integer(high - low) + low, prng.getState()] + } +} + +const randomNumber = (state: Random.PCGRandomState): [number, Random.PCGRandomState] => { + const prng = new Random.PCGRandom() + prng.setState(state) + return [prng.number(), prng.getState()] +} + +const withState = ( + state: TRef.TRef, + f: (state: Random.PCGRandomState) => [A, Random.PCGRandomState] +): STM.STM => { + return pipe(state, tRef.modify(f)) +} + +const shuffleWith = ( + iterable: Iterable, + nextIntBounded: (n: number) => STM.STM +): STM.STM> => { + const swap = (buffer: TArray.TArray, index1: number, index2: number): STM.STM => + pipe( + buffer, + tArray.get(index1), + core.flatMap((tmp) => + pipe( + buffer, + tArray.updateSTM(index1, () => pipe(buffer, tArray.get(index2))), + core.zipRight( + pipe( + buffer, + tArray.update(index2, () => tmp) + ) + ) + ) + ) + ) + return pipe( + tArray.fromIterable(iterable), + core.flatMap((buffer) => { + const array: Array = [] + for (let i = array.length; i >= 2; i = i - 1) { + array.push(i) + } + return pipe( + array, + stm.forEach((n) => pipe(nextIntBounded(n), core.flatMap((k) => swap(buffer, n - 1, k))), { discard: true }), + core.zipRight(tArray.toArray(buffer)) + ) + }) + ) +} + +/** @internal */ +export const Tag = Context.GenericTag("effect/TRandom") + +class TRandomImpl implements TRandom.TRandom { + readonly [TRandomTypeId]: TRandom.TRandomTypeId = TRandomTypeId + constructor(readonly state: TRef.TRef) { + this.next = withState(this.state, randomNumber) + this.nextBoolean = core.flatMap(this.next, (n) => core.succeed(n > 0.5)) + this.nextInt = withState(this.state, randomInteger) + } + + next: STM.STM + nextBoolean: STM.STM + nextInt: STM.STM + + nextRange(min: number, max: number): STM.STM { + return core.flatMap(this.next, (n) => core.succeed((max - min) * n + min)) + } + nextIntBetween(low: number, high: number): STM.STM { + return withState(this.state, randomIntegerBetween(low, high)) + } + shuffle(elements: Iterable): STM.STM> { + return shuffleWith(elements, (n) => this.nextIntBetween(0, n)) + } +} + +/** @internal */ +export const live: Layer.Layer = Layer.effect( + Tag, + pipe( + tRef.make(new Random.PCGRandom((Math.random() * 4294967296) >>> 0).getState()), + core.map((seed) => new TRandomImpl(seed)), + core.commit + ) +) + +/** @internal */ +export const next: STM.STM = core.flatMap(Tag, (random) => random.next) + +/** @internal */ +export const nextBoolean: STM.STM = core.flatMap(Tag, (random) => random.nextBoolean) + +/** @internal */ +export const nextInt: STM.STM = core.flatMap(Tag, (random) => random.nextInt) + +/** @internal */ +export const nextIntBetween = (low: number, high: number): STM.STM => + core.flatMap(Tag, (random) => random.nextIntBetween(low, high)) + +/** @internal */ +export const nextRange = (min: number, max: number): STM.STM => + core.flatMap(Tag, (random) => random.nextRange(min, max)) + +/** @internal */ +export const shuffle = (elements: Iterable): STM.STM, never, TRandom.TRandom> => + core.flatMap(Tag, (random) => random.shuffle(elements)) diff --git a/repos/effect/packages/effect/src/internal/stm/tReentrantLock.ts b/repos/effect/packages/effect/src/internal/stm/tReentrantLock.ts new file mode 100644 index 0000000..2949f7f --- /dev/null +++ b/repos/effect/packages/effect/src/internal/stm/tReentrantLock.ts @@ -0,0 +1,352 @@ +import * as Effect from "../../Effect.js" +import * as Equal from "../../Equal.js" +import * as FiberId from "../../FiberId.js" +import { dual } from "../../Function.js" +import * as HashMap from "../../HashMap.js" +import * as Option from "../../Option.js" +import type * as Scope from "../../Scope.js" +import type * as STM from "../../STM.js" +import type * as TReentrantLock from "../../TReentrantLock.js" +import type * as TRef from "../../TRef.js" +import * as core from "./core.js" +import * as tRef from "./tRef.js" + +const TReentrantLockSymbolKey = "effect/TReentrantLock" + +/** @internal */ +export const TReentrantLockTypeId: TReentrantLock.TReentrantLockTypeId = Symbol.for( + TReentrantLockSymbolKey +) as TReentrantLock.TReentrantLockTypeId + +const WriteLockTypeId = Symbol.for("effect/TReentrantLock/WriteLock") + +type WriteLockTypeId = typeof WriteLockTypeId + +const ReadLockTypeId = Symbol.for("effect/TReentrantLock/ReadLock") + +type ReadLockTypeId = typeof ReadLockTypeId + +class TReentranLockImpl implements TReentrantLock.TReentrantLock { + readonly [TReentrantLockTypeId]: TReentrantLock.TReentrantLockTypeId = TReentrantLockTypeId + constructor(readonly state: TRef.TRef) {} +} + +/** @internal */ +export interface LockState { + /** + * Computes the total number of read locks acquired. + */ + readonly readLocks: number + /** + * Computes the total number of write locks acquired. + */ + readonly writeLocks: number + /** + * Computes the number of read locks held by the specified fiber id. + */ + readLocksHeld(fiberId: FiberId.FiberId): number + /** + * Computes the number of write locks held by the specified fiber id. + */ + writeLocksHeld(fiberId: FiberId.FiberId): number +} + +/** + * This data structure describes the state of the lock when multiple fibers + * have acquired read locks. The state is tracked as a map from fiber identity + * to number of read locks acquired by the fiber. This level of detail permits + * upgrading a read lock to a write lock. + * + * @internal + */ +export class ReadLock implements LockState { + readonly [ReadLockTypeId]: ReadLockTypeId = ReadLockTypeId + constructor(readonly readers: HashMap.HashMap) {} + get readLocks(): number { + return Array.from(this.readers).reduce((acc, curr) => acc + curr[1], 0) + } + get writeLocks(): number { + return 0 + } + readLocksHeld(fiberId: FiberId.FiberId): number { + return Option.getOrElse( + HashMap.get(this.readers, fiberId), + () => 0 + ) + } + writeLocksHeld(_fiberId: FiberId.FiberId): number { + return 0 + } +} + +/** + * This data structure describes the state of the lock when a single fiber has + * a write lock. The fiber has an identity, and may also have acquired a + * certain number of read locks. + * + * @internal + */ +export class WriteLock implements LockState { + readonly [WriteLockTypeId]: WriteLockTypeId = WriteLockTypeId + constructor( + readonly readLocks: number, + readonly writeLocks: number, + readonly fiberId: FiberId.FiberId + ) {} + readLocksHeld(fiberId: FiberId.FiberId): number { + return Equal.equals(fiberId)(this.fiberId) ? this.readLocks : 0 + } + writeLocksHeld(fiberId: FiberId.FiberId): number { + return Equal.equals(fiberId)(this.fiberId) ? this.writeLocks : 0 + } +} + +const isReadLock = (lock: LockState): lock is ReadLock => { + return ReadLockTypeId in lock +} + +const isWriteLock = (lock: LockState): lock is WriteLock => { + return WriteLockTypeId in lock +} + +/** + * An empty read lock state, in which no fiber holds any read locks. + */ +const emptyReadLock = new ReadLock(HashMap.empty()) + +/** + * Creates a new read lock where the specified fiber holds the specified + * number of read locks. + */ +const makeReadLock = (fiberId: FiberId.FiberId, count: number): ReadLock => { + if (count <= 0) { + return emptyReadLock + } + return new ReadLock(HashMap.make([fiberId, count])) +} + +/** + * Determines if there is no other holder of read locks aside from the + * specified fiber id. If there are no other holders of read locks aside + * from the specified fiber id, then it is safe to upgrade the read lock + * into a write lock. + */ +const noOtherHolder = (readLock: ReadLock, fiberId: FiberId.FiberId): boolean => { + return HashMap.isEmpty(readLock.readers) || + (HashMap.size(readLock.readers) === 1 && HashMap.has(readLock.readers, fiberId)) +} + +/** + * Adjusts the number of read locks held by the specified fiber id. + */ +const adjustReadLock = (readLock: ReadLock, fiberId: FiberId.FiberId, adjustment: number): ReadLock => { + const total = readLock.readLocksHeld(fiberId) + const newTotal = total + adjustment + if (newTotal < 0) { + throw new Error( + "BUG - TReentrantLock.ReadLock.adjust - please report an issue at https://github.com/Effect-TS/effect/issues" + ) + } + if (newTotal === 0) { + return new ReadLock(HashMap.remove(readLock.readers, fiberId)) + } + return new ReadLock(HashMap.set(readLock.readers, fiberId, newTotal)) +} + +const adjustRead = (self: TReentrantLock.TReentrantLock, delta: number): STM.STM => + core.withSTMRuntime((runtime) => { + const lock = tRef.unsafeGet(self.state, runtime.journal) + if (isReadLock(lock)) { + const result = adjustReadLock(lock, runtime.fiberId, delta) + tRef.unsafeSet(self.state, result, runtime.journal) + return core.succeed(result.readLocksHeld(runtime.fiberId)) + } + if (isWriteLock(lock) && Equal.equals(runtime.fiberId)(lock.fiberId)) { + const newTotal = lock.readLocks + delta + if (newTotal < 0) { + throw new Error( + `Defect: Fiber ${ + FiberId.threadName(runtime.fiberId) + } releasing read locks it does not hold, newTotal: ${newTotal}` + ) + } + tRef.unsafeSet( + self.state, + new WriteLock(newTotal, lock.writeLocks, runtime.fiberId), + runtime.journal + ) + return core.succeed(newTotal) + } + return core.retry + }) + +/** @internal */ +export const acquireRead = (self: TReentrantLock.TReentrantLock): STM.STM => adjustRead(self, 1) + +/** @internal */ +export const acquireWrite = (self: TReentrantLock.TReentrantLock): STM.STM => + core.withSTMRuntime((runtime) => { + const lock = tRef.unsafeGet(self.state, runtime.journal) + if (isReadLock(lock) && noOtherHolder(lock, runtime.fiberId)) { + tRef.unsafeSet( + self.state, + new WriteLock(lock.readLocksHeld(runtime.fiberId), 1, runtime.fiberId), + runtime.journal + ) + return core.succeed(1) + } + if (isWriteLock(lock) && Equal.equals(runtime.fiberId)(lock.fiberId)) { + tRef.unsafeSet( + self.state, + new WriteLock(lock.readLocks, lock.writeLocks + 1, runtime.fiberId), + runtime.journal + ) + return core.succeed(lock.writeLocks + 1) + } + return core.retry + }) + +/** @internal */ +export const fiberReadLocks = (self: TReentrantLock.TReentrantLock): STM.STM => + core.effect((journal, fiberId) => + tRef.unsafeGet( + self.state, + journal + ).readLocksHeld(fiberId) + ) + +/** @internal */ +export const fiberWriteLocks = (self: TReentrantLock.TReentrantLock): STM.STM => + core.effect((journal, fiberId) => + tRef.unsafeGet( + self.state, + journal + ).writeLocksHeld(fiberId) + ) + +/** @internal */ +export const lock = (self: TReentrantLock.TReentrantLock): Effect.Effect => writeLock(self) + +/** @internal */ +export const locked = (self: TReentrantLock.TReentrantLock): STM.STM => + core.zipWith( + readLocked(self), + writeLocked(self), + (x, y) => x || y + ) + +/** @internal */ +export const make: STM.STM = core.map( + tRef.make(emptyReadLock), + (readLock) => new TReentranLockImpl(readLock) +) + +/** @internal */ +export const readLock = (self: TReentrantLock.TReentrantLock): Effect.Effect => + Effect.acquireRelease( + core.commit(acquireRead(self)), + () => core.commit(releaseRead(self)) + ) + +/** @internal */ +export const readLocks = (self: TReentrantLock.TReentrantLock): STM.STM => + core.map( + tRef.get(self.state), + (state) => state.readLocks + ) + +/** @internal */ +export const readLocked = (self: TReentrantLock.TReentrantLock): STM.STM => + core.map( + tRef.get(self.state), + (state) => state.readLocks > 0 + ) + +/** @internal */ +export const releaseRead = (self: TReentrantLock.TReentrantLock): STM.STM => adjustRead(self, -1) + +/** @internal */ +export const releaseWrite = (self: TReentrantLock.TReentrantLock): STM.STM => + core.withSTMRuntime((runtime) => { + const lock = tRef.unsafeGet(self.state, runtime.journal) + if (isWriteLock(lock) && lock.writeLocks === 1 && Equal.equals(runtime.fiberId)(lock.fiberId)) { + const result = makeReadLock(lock.fiberId, lock.readLocks) + tRef.unsafeSet(self.state, result, runtime.journal) + return core.succeed(result.writeLocksHeld(runtime.fiberId)) + } + if (isWriteLock(lock) && Equal.equals(runtime.fiberId)(lock.fiberId)) { + const result = new WriteLock(lock.readLocks, lock.writeLocks - 1, runtime.fiberId) + tRef.unsafeSet(self.state, result, runtime.journal) + return core.succeed(result.writeLocksHeld(runtime.fiberId)) + } + throw new Error( + `Defect: Fiber ${FiberId.threadName(runtime.fiberId)} releasing write lock it does not hold` + ) + }) + +/** @internal */ +export const withLock = dual< + ( + self: TReentrantLock.TReentrantLock + ) => (effect: Effect.Effect) => Effect.Effect, + ( + effect: Effect.Effect, + self: TReentrantLock.TReentrantLock + ) => Effect.Effect +>(2, (effect, self) => withWriteLock(effect, self)) + +/** @internal */ +export const withReadLock = dual< + (self: TReentrantLock.TReentrantLock) => ( + effect: Effect.Effect + ) => Effect.Effect, + ( + effect: Effect.Effect, + self: TReentrantLock.TReentrantLock + ) => Effect.Effect +>(2, (effect, self) => + Effect.uninterruptibleMask((restore) => + Effect.zipRight( + restore(core.commit(acquireRead(self))), + Effect.ensuring( + effect, + core.commit(releaseRead(self)) + ) + ) + )) + +/** @internal */ +export const withWriteLock = dual< + (self: TReentrantLock.TReentrantLock) => (effect: Effect.Effect) => Effect.Effect, + (effect: Effect.Effect, self: TReentrantLock.TReentrantLock) => Effect.Effect +>(2, (effect, self) => + Effect.uninterruptibleMask((restore) => + Effect.zipRight( + restore(core.commit(acquireWrite(self))), + Effect.ensuring( + effect, + core.commit(releaseWrite(self)) + ) + ) + )) + +/** @internal */ +export const writeLock = (self: TReentrantLock.TReentrantLock): Effect.Effect => + Effect.acquireRelease( + core.commit(acquireWrite(self)), + () => core.commit(releaseWrite(self)) + ) + +/** @internal */ +export const writeLocked = (self: TReentrantLock.TReentrantLock): STM.STM => + core.map( + tRef.get(self.state), + (state) => state.writeLocks > 0 + ) + +/** @internal */ +export const writeLocks = (self: TReentrantLock.TReentrantLock): STM.STM => + core.map( + tRef.get(self.state), + (state) => state.writeLocks + ) diff --git a/repos/effect/packages/effect/src/internal/stm/tRef.ts b/repos/effect/packages/effect/src/internal/stm/tRef.ts new file mode 100644 index 0000000..cb4f7eb --- /dev/null +++ b/repos/effect/packages/effect/src/internal/stm/tRef.ts @@ -0,0 +1,195 @@ +import { dual } from "../../Function.js" +import * as Option from "../../Option.js" +import type { Pipeable } from "../../Pipeable.js" +import { pipeArguments } from "../../Pipeable.js" +import type * as STM from "../../STM.js" +import type * as TRef from "../../TRef.js" +import * as core from "./core.js" +import * as Entry from "./entry.js" +import type * as Journal from "./journal.js" +import type * as TxnId from "./txnId.js" +import * as Versioned from "./versioned.js" + +/** @internal */ +const TRefSymbolKey = "effect/TRef" + +/** @internal */ +export const TRefTypeId: TRef.TRefTypeId = Symbol.for( + TRefSymbolKey +) as TRef.TRefTypeId + +export const tRefVariance = { + /* c8 ignore next */ + _A: (_: any) => _ +} + +/** @internal */ +export class TRefImpl implements TRef.TRef, Pipeable { + readonly [TRefTypeId] = tRefVariance + /** @internal */ + todos: Map + /** @internal */ + versioned: Versioned.Versioned + constructor(value: A) { + this.versioned = new Versioned.Versioned(value) + this.todos = new Map() + } + modify(f: (a: A) => readonly [B, A]): STM.STM { + return core.effect((journal) => { + const entry = getOrMakeEntry(this, journal) + const [retValue, newValue] = f(Entry.unsafeGet(entry) as A) + Entry.unsafeSet(entry, newValue) + return retValue + }) + } + pipe() { + return pipeArguments(this, arguments) + } +} + +/** @internal */ +export const make = (value: A): STM.STM> => + core.effect>((journal) => { + const ref = new TRefImpl(value) + journal.set(ref, Entry.make(ref, true)) + return ref + }) + +/** @internal */ +export const get = (self: TRef.TRef) => self.modify((a) => [a, a]) + +/** @internal */ +export const set = dual< + (value: A) => (self: TRef.TRef) => STM.STM, + (self: TRef.TRef, value: A) => STM.STM +>( + 2, + (self: TRef.TRef, value: A): STM.STM => self.modify((): [void, A] => [void 0, value]) +) + +/** @internal */ +export const getAndSet = dual< + (value: A) => (self: TRef.TRef) => STM.STM, + (self: TRef.TRef, value: A) => STM.STM +>(2, (self, value) => self.modify((a) => [a, value])) + +/** @internal */ +export const getAndUpdate = dual< + (f: (a: A) => A) => (self: TRef.TRef) => STM.STM, + (self: TRef.TRef, f: (a: A) => A) => STM.STM +>(2, (self, f) => self.modify((a) => [a, f(a)])) + +/** @internal */ +export const getAndUpdateSome = dual< + (f: (a: A) => Option.Option) => (self: TRef.TRef) => STM.STM, + (self: TRef.TRef, f: (a: A) => Option.Option) => STM.STM +>(2, (self, f) => + self.modify((a) => + Option.match(f(a), { + onNone: () => [a, a], + onSome: (b) => [a, b] + }) + )) + +/** @internal */ +export const setAndGet = dual< + (value: A) => (self: TRef.TRef) => STM.STM, + (self: TRef.TRef, value: A) => STM.STM +>(2, (self, value) => self.modify(() => [value, value])) + +/** @internal */ +export const modify = dual< + (f: (a: A) => readonly [B, A]) => (self: TRef.TRef) => STM.STM, + (self: TRef.TRef, f: (a: A) => readonly [B, A]) => STM.STM +>(2, (self, f) => self.modify(f)) + +/** @internal */ +export const modifySome = dual< + (fallback: B, f: (a: A) => Option.Option) => (self: TRef.TRef) => STM.STM, + (self: TRef.TRef, fallback: B, f: (a: A) => Option.Option) => STM.STM +>(3, (self, fallback, f) => + self.modify((a) => + Option.match(f(a), { + onNone: () => [fallback, a], + onSome: (b) => b + }) + )) + +/** @internal */ +export const update = dual< + (f: (a: A) => A) => (self: TRef.TRef) => STM.STM, + (self: TRef.TRef, f: (a: A) => A) => STM.STM +>(2, (self, f) => self.modify((a) => [void 0, f(a)])) + +/** @internal */ +export const updateAndGet = dual< + (f: (a: A) => A) => (self: TRef.TRef) => STM.STM, + (self: TRef.TRef, f: (a: A) => A) => STM.STM +>(2, (self, f) => + self.modify((a) => { + const b = f(a) + return [b, b] + })) + +/** @internal */ +export const updateSome = dual< + (f: (a: A) => Option.Option) => (self: TRef.TRef) => STM.STM, + (self: TRef.TRef, f: (a: A) => Option.Option) => STM.STM +>( + 2, + (self, f) => + self.modify((a) => [ + void 0, + Option.match(f(a), { + onNone: () => a, + onSome: (b) => b + }) + ]) +) + +/** @internal */ +export const updateSomeAndGet = dual< + (f: (a: A) => Option.Option) => (self: TRef.TRef) => STM.STM, + (self: TRef.TRef, f: (a: A) => Option.Option) => STM.STM +>( + 2, + (self, f) => + self.modify((a) => + Option.match(f(a), { + onNone: () => [a, a], + onSome: (b) => [b, b] + }) + ) +) + +/** @internal */ +const getOrMakeEntry = (self: TRef.TRef, journal: Journal.Journal): Entry.Entry => { + if (journal.has(self)) { + return journal.get(self)! + } + const entry = Entry.make(self, false) + journal.set(self, entry) + return entry +} + +/** @internal */ +export const unsafeGet: { + (journal: Journal.Journal): (self: TRef.TRef) => A + (self: TRef.TRef, journal: Journal.Journal): A +} = dual< + (journal: Journal.Journal) => (self: TRef.TRef) => A, + (self: TRef.TRef, journal: Journal.Journal) => A +>(2, (self: TRef.TRef, journal: Journal.Journal) => Entry.unsafeGet(getOrMakeEntry(self, journal)) as A) + +/** @internal */ +export const unsafeSet: { + (value: A, journal: Journal.Journal): (self: TRef.TRef) => void + (self: TRef.TRef, value: A, journal: Journal.Journal): void +} = dual< + (value: A, journal: Journal.Journal) => (self: TRef.TRef) => void, + (self: TRef.TRef, value: A, journal: Journal.Journal) => void +>(3, (self, value, journal) => { + const entry = getOrMakeEntry(self, journal) + Entry.unsafeSet(entry, value) + return undefined +}) diff --git a/repos/effect/packages/effect/src/internal/stm/tSemaphore.ts b/repos/effect/packages/effect/src/internal/stm/tSemaphore.ts new file mode 100644 index 0000000..b766c8f --- /dev/null +++ b/repos/effect/packages/effect/src/internal/stm/tSemaphore.ts @@ -0,0 +1,113 @@ +import * as Cause from "../../Cause.js" +import * as Effect from "../../Effect.js" +import { dual } from "../../Function.js" +import type * as Scope from "../../Scope.js" +import * as STM from "../../STM.js" +import type * as TRef from "../../TRef.js" +import type * as TSemaphore from "../../TSemaphore.js" +import * as core from "./core.js" +import * as tRef from "./tRef.js" + +/** @internal */ +const TSemaphoreSymbolKey = "effect/TSemaphore" + +/** @internal */ +export const TSemaphoreTypeId: TSemaphore.TSemaphoreTypeId = Symbol.for( + TSemaphoreSymbolKey +) as TSemaphore.TSemaphoreTypeId + +/** @internal */ +class TSemaphoreImpl implements TSemaphore.TSemaphore { + readonly [TSemaphoreTypeId]: TSemaphore.TSemaphoreTypeId = TSemaphoreTypeId + constructor(readonly permits: TRef.TRef) {} +} + +/** @internal */ +export const make = (permits: number): STM.STM => + STM.map(tRef.make(permits), (permits) => new TSemaphoreImpl(permits)) + +/** @internal */ +export const acquire = (self: TSemaphore.TSemaphore): STM.STM => acquireN(self, 1) + +/** @internal */ +export const acquireN = dual< + (n: number) => (self: TSemaphore.TSemaphore) => STM.STM, + (self: TSemaphore.TSemaphore, n: number) => STM.STM +>(2, (self, n) => + core.withSTMRuntime((driver) => { + if (n < 0) { + throw new Cause.IllegalArgumentException(`Unexpected negative value ${n} passed to Semaphore.acquireN`) + } + const value = tRef.unsafeGet(self.permits, driver.journal) + if (value < n) { + return STM.retry + } else { + return STM.succeed(tRef.unsafeSet(self.permits, value - n, driver.journal)) + } + })) + +/** @internal */ +export const available = (self: TSemaphore.TSemaphore) => tRef.get(self.permits) + +/** @internal */ +export const release = (self: TSemaphore.TSemaphore): STM.STM => releaseN(self, 1) + +/** @internal */ +export const releaseN = dual< + (n: number) => (self: TSemaphore.TSemaphore) => STM.STM, + (self: TSemaphore.TSemaphore, n: number) => STM.STM +>(2, (self, n) => + core.withSTMRuntime((driver) => { + if (n < 0) { + throw new Cause.IllegalArgumentException(`Unexpected negative value ${n} passed to Semaphore.releaseN`) + } + const current = tRef.unsafeGet(self.permits, driver.journal) + return STM.succeed(tRef.unsafeSet(self.permits, current + n, driver.journal)) + })) + +/** @internal */ +export const withPermit = dual< + (semaphore: TSemaphore.TSemaphore) => (self: Effect.Effect) => Effect.Effect, + (self: Effect.Effect, semaphore: TSemaphore.TSemaphore) => Effect.Effect +>(2, (self, semaphore) => withPermits(self, semaphore, 1)) + +/** @internal */ +export const withPermits = dual< + ( + semaphore: TSemaphore.TSemaphore, + permits: number + ) => (self: Effect.Effect) => Effect.Effect, + ( + self: Effect.Effect, + semaphore: TSemaphore.TSemaphore, + permits: number + ) => Effect.Effect +>(3, (self, semaphore, permits) => + Effect.uninterruptibleMask((restore) => + Effect.zipRight( + restore(core.commit(acquireN(permits)(semaphore))), + Effect.ensuring( + self, + core.commit(releaseN(permits)(semaphore)) + ) + ) + )) + +/** @internal */ +export const withPermitScoped = (self: TSemaphore.TSemaphore): Effect.Effect => + withPermitsScoped(self, 1) + +/** @internal */ +export const withPermitsScoped = dual< + (permits: number) => (self: TSemaphore.TSemaphore) => Effect.Effect, + (self: TSemaphore.TSemaphore, permits: number) => Effect.Effect +>(2, (self, permits) => + Effect.acquireReleaseInterruptible( + core.commit(acquireN(self, permits)), + () => core.commit(releaseN(self, permits)) + )) + +/** @internal */ +export const unsafeMakeSemaphore = (permits: number): TSemaphore.TSemaphore => { + return new TSemaphoreImpl(new tRef.TRefImpl(permits)) +} diff --git a/repos/effect/packages/effect/src/internal/stm/tSet.ts b/repos/effect/packages/effect/src/internal/stm/tSet.ts new file mode 100644 index 0000000..902ebbd --- /dev/null +++ b/repos/effect/packages/effect/src/internal/stm/tSet.ts @@ -0,0 +1,259 @@ +import * as RA from "../../Array.js" +import * as Chunk from "../../Chunk.js" +import { dual, pipe } from "../../Function.js" +import * as HashSet from "../../HashSet.js" +import type * as Option from "../../Option.js" +import { hasProperty, type Predicate } from "../../Predicate.js" +import * as STM from "../../STM.js" +import type * as TMap from "../../TMap.js" +import type * as TSet from "../../TSet.js" +import * as core from "./core.js" +import * as tMap from "./tMap.js" + +/** @internal */ +const TSetSymbolKey = "effect/TSet" + +/** @internal */ +export const TSetTypeId: TSet.TSetTypeId = Symbol.for( + TSetSymbolKey +) as TSet.TSetTypeId + +const tSetVariance = { + /* c8 ignore next */ + _A: (_: any) => _ +} + +/** @internal */ +class TSetImpl implements TSet.TSet { + readonly [TSetTypeId] = tSetVariance + constructor(readonly tMap: TMap.TMap) {} +} + +const isTSet = (u: unknown) => hasProperty(u, TSetTypeId) + +/** @internal */ +export const add = dual< + (value: A) => (self: TSet.TSet) => STM.STM, + (self: TSet.TSet, value: A) => STM.STM +>(2, (self, value) => tMap.set(self.tMap, value, void 0 as void)) + +/** @internal */ +export const difference = dual< + (other: TSet.TSet) => (self: TSet.TSet) => STM.STM, + (self: TSet.TSet, other: TSet.TSet) => STM.STM +>(2, (self, other) => + core.flatMap( + toHashSet(other), + (values) => removeIf(self, (value) => HashSet.has(values, value), { discard: true }) + )) + +/** @internal */ +export const empty = (): STM.STM> => fromIterable([]) + +/** @internal */ +export const forEach = dual< + (f: (value: A) => STM.STM) => (self: TSet.TSet) => STM.STM, + (self: TSet.TSet, f: (value: A) => STM.STM) => STM.STM +>(2, (self, f) => reduceSTM(self, void 0 as void, (_, value) => f(value))) + +/** @internal */ +export const fromIterable = (iterable: Iterable): STM.STM> => + core.map( + tMap.fromIterable(Array.from(iterable).map((a): [A, void] => [a, void 0])), + (tMap) => new TSetImpl(tMap) + ) + +/** @internal */ +export const has = dual< + (value: A) => (self: TSet.TSet) => STM.STM, + (self: TSet.TSet, value: A) => STM.STM +>(2, (self, value) => tMap.has(self.tMap, value)) + +/** @internal */ +export const intersection = dual< + (other: TSet.TSet) => (self: TSet.TSet) => STM.STM, + (self: TSet.TSet, other: TSet.TSet) => STM.STM +>(2, (self, other) => + core.flatMap( + toHashSet(other), + (values) => pipe(self, retainIf((value) => pipe(values, HashSet.has(value)), { discard: true })) + )) + +/** @internal */ +export const isEmpty = (self: TSet.TSet): STM.STM => tMap.isEmpty(self.tMap) + +/** @internal */ +export const make = >( + ...elements: Elements +): STM.STM> => fromIterable(elements) + +/** @internal */ +export const reduce = dual< + (zero: Z, f: (accumulator: Z, value: A) => Z) => (self: TSet.TSet) => STM.STM, + (self: TSet.TSet, zero: Z, f: (accumulator: Z, value: A) => Z) => STM.STM +>(3, (self, zero, f) => + tMap.reduce( + self.tMap, + zero, + (acc, _, key) => f(acc, key) + )) + +/** @internal */ +export const reduceSTM = dual< + (zero: Z, f: (accumulator: Z, value: A) => STM.STM) => (self: TSet.TSet) => STM.STM, + (self: TSet.TSet, zero: Z, f: (accumulator: Z, value: A) => STM.STM) => STM.STM +>(3, (self, zero, f) => + tMap.reduceSTM( + self.tMap, + zero, + (acc, _, key) => f(acc, key) + )) + +/** @internal */ +export const remove = dual< + (value: A) => (self: TSet.TSet) => STM.STM, + (self: TSet.TSet, value: A) => STM.STM +>(2, (self, value) => tMap.remove(self.tMap, value)) + +/** @internal */ +export const removeAll = dual< + (iterable: Iterable) => (self: TSet.TSet) => STM.STM, + (self: TSet.TSet, iterable: Iterable) => STM.STM +>(2, (self, iterable) => tMap.removeAll(self.tMap, iterable)) + +/** @internal */ +export const removeIf: { + (predicate: Predicate, options: { + readonly discard: true + }): (self: TSet.TSet) => STM.STM + ( + predicate: Predicate, + options?: { + readonly discard: false + } + ): (self: TSet.TSet) => STM.STM> + (self: TSet.TSet, predicate: Predicate, options: { + readonly discard: true + }): STM.STM + ( + self: TSet.TSet, + predicate: Predicate, + options?: { + readonly discard: false + } + ): STM.STM> +} = dual( + (args) => isTSet(args[0]), + (self, predicate, options) => + options?.discard === true ? tMap.removeIf(self.tMap, (key) => predicate(key), { discard: true }) : pipe( + tMap.removeIf(self.tMap, (key) => predicate(key)), + core.map(RA.map((entry) => entry[0])) + ) +) + +/** @internal */ +export const retainIf: { + (predicate: Predicate, options: { + readonly discard: true + }): (self: TSet.TSet) => STM.STM + ( + predicate: Predicate, + options?: { + readonly discard: false + } + ): (self: TSet.TSet) => STM.STM> + (self: TSet.TSet, predicate: Predicate, options: { + readonly discard: true + }): STM.STM + ( + self: TSet.TSet, + predicate: Predicate, + options?: { + readonly discard: false + } + ): STM.STM> +} = dual((args) => isTSet(args[0]), (self, predicate, options) => + options?.discard === true ? + tMap.retainIf(self.tMap, (key) => predicate(key), { discard: true }) : + pipe( + tMap.retainIf(self.tMap, (key) => predicate(key)), + core.map(RA.map((entry) => entry[0])) + )) + +/** @internal */ +export const size = (self: TSet.TSet): STM.STM => core.map(toChunk(self), (chunk) => chunk.length) + +/** @internal */ +export const takeFirst = dual< + (pf: (a: A) => Option.Option) => (self: TSet.TSet) => STM.STM, + (self: TSet.TSet, pf: (a: A) => Option.Option) => STM.STM +>(2, (self, pf) => tMap.takeFirst(self.tMap, (key) => pf(key))) + +/** @internal */ +export const takeFirstSTM = dual< + (pf: (a: A) => STM.STM, R>) => (self: TSet.TSet) => STM.STM, + (self: TSet.TSet, pf: (a: A) => STM.STM, R>) => STM.STM +>(2, (self, pf) => tMap.takeFirstSTM(self.tMap, (key) => pf(key))) + +/** @internal */ +export const takeSome = dual< + (pf: (a: A) => Option.Option) => (self: TSet.TSet) => STM.STM>, + (self: TSet.TSet, pf: (a: A) => Option.Option) => STM.STM> +>(2, (self, pf) => tMap.takeSome(self.tMap, (key) => pf(key))) + +/** @internal */ +export const takeSomeSTM = dual< + ( + pf: (a: A) => STM.STM, R> + ) => (self: TSet.TSet) => STM.STM, E, R>, + ( + self: TSet.TSet, + pf: (a: A) => STM.STM, R> + ) => STM.STM, E, R> +>(2, (self, pf) => tMap.takeSomeSTM(self.tMap, (key) => pf(key))) + +/** @internal */ +export const toChunk = (self: TSet.TSet): STM.STM> => + tMap.keys(self.tMap).pipe(STM.map(Chunk.unsafeFromArray)) + +/** @internal */ +export const toHashSet = (self: TSet.TSet): STM.STM> => + reduce( + self, + HashSet.empty(), + (acc, value) => pipe(acc, HashSet.add(value)) + ) + +/** @internal */ +export const toArray = (self: TSet.TSet): STM.STM> => + reduce, A>( + self, + [], + (acc, value) => [...acc, value] + ) + +/** @internal */ +export const toReadonlySet = (self: TSet.TSet): STM.STM> => + core.map(toArray(self), (values) => new Set(values)) + +/** @internal */ +export const transform = dual< + (f: (a: A) => A) => (self: TSet.TSet) => STM.STM, + (self: TSet.TSet, f: (a: A) => A) => STM.STM +>(2, (self, f) => tMap.transform(self.tMap, (key, value) => [f(key), value])) + +/** @internal */ +export const transformSTM = dual< + (f: (a: A) => STM.STM) => (self: TSet.TSet) => STM.STM, + (self: TSet.TSet, f: (a: A) => STM.STM) => STM.STM +>(2, (self, f) => + tMap.transformSTM( + self.tMap, + (key, value) => core.map(f(key), (a) => [a, value]) + )) + +/** @internal */ +export const union = dual< + (other: TSet.TSet) => (self: TSet.TSet) => STM.STM, + (self: TSet.TSet, other: TSet.TSet) => STM.STM +>(2, (self, other) => forEach(other, (value) => add(self, value))) diff --git a/repos/effect/packages/effect/src/internal/stm/tSubscriptionRef.ts b/repos/effect/packages/effect/src/internal/stm/tSubscriptionRef.ts new file mode 100644 index 0000000..94a4924 --- /dev/null +++ b/repos/effect/packages/effect/src/internal/stm/tSubscriptionRef.ts @@ -0,0 +1,286 @@ +import * as Effect from "../../Effect.js" +import { dual, pipe } from "../../Function.js" +import * as Option from "../../Option.js" +import { pipeArguments } from "../../Pipeable.js" +import * as STM from "../../STM.js" +import * as TPubSub from "../../TPubSub.js" +import * as TQueue from "../../TQueue.js" +import * as TRef from "../../TRef.js" +import type * as TSubscriptionRef from "../../TSubscriptionRef.js" +import * as stream from "../stream.js" +import { tDequeueVariance } from "./tQueue.js" +import { tRefVariance } from "./tRef.js" + +/** @internal */ +const TSubscriptionRefSymbolKey = "effect/TSubscriptionRef" + +/** @internal */ +export const TSubscriptionRefTypeId: TSubscriptionRef.TSubscriptionRefTypeId = Symbol.for( + TSubscriptionRefSymbolKey +) as TSubscriptionRef.TSubscriptionRefTypeId + +const TSubscriptionRefVariance = { + /* c8 ignore next */ + _A: (_: any) => _ +} + +class TDequeueMerge implements TQueue.TDequeue { + [TQueue.TDequeueTypeId] = tDequeueVariance + + constructor( + readonly first: TQueue.TDequeue, + readonly second: TQueue.TDequeue + ) {} + + peek: STM.STM = STM.gen(this, function*() { + const first = yield* this.peekOption + if (first._tag === "Some") { + return first.value + } + return yield* STM.retry + }) + + peekOption: STM.STM> = STM.gen(this, function*() { + const first = yield* this.first.peekOption + if (first._tag === "Some") { + return first + } + const second = yield* this.second.peekOption + if (second._tag === "Some") { + return second + } + return Option.none() + }) + + take: STM.STM = STM.gen(this, function*() { + if (!(yield* this.first.isEmpty)) { + return yield* this.first.take + } + if (!(yield* this.second.isEmpty)) { + return yield* this.second.take + } + return yield* STM.retry + }) + + takeAll: STM.STM> = STM.gen(this, function*() { + return [...yield* this.first.takeAll, ...yield* this.second.takeAll] + }) + + takeUpTo(max: number): STM.STM> { + return STM.gen(this, function*() { + const first = yield* this.first.takeUpTo(max) + if (first.length >= max) { + return first + } + return [...first, ...yield* this.second.takeUpTo(max - first.length)] + }) + } + + capacity(): number { + return this.first.capacity() + this.second.capacity() + } + + size: STM.STM = STM.gen(this, function*() { + return (yield* this.first.size) + (yield* this.second.size) + }) + + isFull: STM.STM = STM.gen(this, function*() { + return (yield* this.first.isFull) && (yield* this.second.isFull) + }) + + isEmpty: STM.STM = STM.gen(this, function*() { + return (yield* this.first.isEmpty) && (yield* this.second.isEmpty) + }) + + shutdown: STM.STM = STM.gen(this, function*() { + yield* this.first.shutdown + yield* this.second.shutdown + }) + + isShutdown: STM.STM = STM.gen(this, function*() { + return (yield* this.first.isShutdown) && (yield* this.second.isShutdown) + }) + + awaitShutdown: STM.STM = STM.gen(this, function*() { + yield* this.first.awaitShutdown + yield* this.second.awaitShutdown + }) +} + +/** @internal */ +class TSubscriptionRefImpl implements TSubscriptionRef.TSubscriptionRef { + readonly [TSubscriptionRefTypeId] = TSubscriptionRefVariance + readonly [TRef.TRefTypeId] = tRefVariance + + constructor( + readonly ref: TRef.TRef, + readonly pubsub: TPubSub.TPubSub + ) {} + + get todos() { + return this.ref.todos + } + + get versioned() { + return this.ref.versioned + } + + pipe() { + return pipeArguments(this, arguments) + } + + get changes(): STM.STM> { + return STM.gen(this, function*() { + const first = yield* TQueue.unbounded() + yield* TQueue.offer(first, yield* TRef.get(this.ref)) + return new TDequeueMerge(first, yield* TPubSub.subscribe(this.pubsub)) + }) + } + + modify(f: (a: A) => readonly [B, A]): STM.STM { + return pipe( + TRef.get(this.ref), + STM.map(f), + STM.flatMap(([b, a]) => + pipe( + TRef.set(this.ref, a), + STM.as(b), + STM.zipLeft(TPubSub.publish(this.pubsub, a)) + ) + ) + ) + } +} + +/** @internal */ +export const make = (value: A): STM.STM> => + pipe( + STM.all([ + TPubSub.unbounded(), + TRef.make(value) + ]), + STM.map(([pubsub, ref]) => new TSubscriptionRefImpl(ref, pubsub)) + ) + +/** @internal */ +export const get = (self: TSubscriptionRef.TSubscriptionRef) => TRef.get(self.ref) + +/** @internal */ +export const set = dual< + (value: A) => (self: TSubscriptionRef.TSubscriptionRef) => STM.STM, + (self: TSubscriptionRef.TSubscriptionRef, value: A) => STM.STM +>( + 2, + (self: TSubscriptionRef.TSubscriptionRef, value: A): STM.STM => + self.modify((): [void, A] => [void 0, value]) +) + +/** @internal */ +export const getAndSet = dual< + (value: A) => (self: TSubscriptionRef.TSubscriptionRef) => STM.STM, + (self: TSubscriptionRef.TSubscriptionRef, value: A) => STM.STM +>(2, (self, value) => self.modify((a) => [a, value])) + +/** @internal */ +export const getAndUpdate = dual< + (f: (a: A) => A) => (self: TSubscriptionRef.TSubscriptionRef) => STM.STM, + (self: TSubscriptionRef.TSubscriptionRef, f: (a: A) => A) => STM.STM +>(2, (self, f) => self.modify((a) => [a, f(a)])) + +/** @internal */ +export const getAndUpdateSome = dual< + (f: (a: A) => Option.Option) => (self: TSubscriptionRef.TSubscriptionRef) => STM.STM, + (self: TSubscriptionRef.TSubscriptionRef, f: (a: A) => Option.Option) => STM.STM +>(2, (self, f) => + self.modify((a) => + Option.match(f(a), { + onNone: () => [a, a], + onSome: (b) => [a, b] + }) + )) + +/** @internal */ +export const setAndGet = dual< + (value: A) => (self: TSubscriptionRef.TSubscriptionRef) => STM.STM, + (self: TSubscriptionRef.TSubscriptionRef, value: A) => STM.STM +>(2, (self, value) => self.modify(() => [value, value])) + +/** @internal */ +export const modify = dual< + (f: (a: A) => readonly [B, A]) => (self: TSubscriptionRef.TSubscriptionRef) => STM.STM, + (self: TSubscriptionRef.TSubscriptionRef, f: (a: A) => readonly [B, A]) => STM.STM +>(2, (self, f) => self.modify(f)) + +/** @internal */ +export const modifySome = dual< + ( + fallback: B, + f: (a: A) => Option.Option + ) => (self: TSubscriptionRef.TSubscriptionRef) => STM.STM, + ( + self: TSubscriptionRef.TSubscriptionRef, + fallback: B, + f: (a: A) => Option.Option + ) => STM.STM +>(3, (self, fallback, f) => + self.modify((a) => + Option.match(f(a), { + onNone: () => [fallback, a], + onSome: (b) => b + }) + )) + +/** @internal */ +export const update = dual< + (f: (a: A) => A) => (self: TSubscriptionRef.TSubscriptionRef) => STM.STM, + (self: TSubscriptionRef.TSubscriptionRef, f: (a: A) => A) => STM.STM +>(2, (self, f) => self.modify((a) => [void 0, f(a)])) + +/** @internal */ +export const updateAndGet = dual< + (f: (a: A) => A) => (self: TSubscriptionRef.TSubscriptionRef) => STM.STM, + (self: TSubscriptionRef.TSubscriptionRef, f: (a: A) => A) => STM.STM +>(2, (self, f) => + self.modify((a) => { + const b = f(a) + return [b, b] + })) + +/** @internal */ +export const updateSome = dual< + (f: (a: A) => Option.Option) => (self: TSubscriptionRef.TSubscriptionRef) => STM.STM, + (self: TSubscriptionRef.TSubscriptionRef, f: (a: A) => Option.Option) => STM.STM +>( + 2, + (self, f) => + self.modify((a) => [ + void 0, + Option.match(f(a), { + onNone: () => a, + onSome: (b) => b + }) + ]) +) + +/** @internal */ +export const updateSomeAndGet = dual< + (f: (a: A) => Option.Option) => (self: TSubscriptionRef.TSubscriptionRef) => STM.STM, + (self: TSubscriptionRef.TSubscriptionRef, f: (a: A) => Option.Option) => STM.STM +>( + 2, + (self, f) => + self.modify((a) => + Option.match(f(a), { + onNone: () => [a, a], + onSome: (b) => [b, b] + }) + ) +) + +/** @internal */ +export const changesScoped = (self: TSubscriptionRef.TSubscriptionRef) => + Effect.acquireRelease(self.changes, TQueue.shutdown) + +/** @internal */ +export const changesStream = (self: TSubscriptionRef.TSubscriptionRef) => + stream.unwrap(Effect.map(self.changes, stream.fromTQueue)) diff --git a/repos/effect/packages/effect/src/internal/stm/tryCommit.ts b/repos/effect/packages/effect/src/internal/stm/tryCommit.ts new file mode 100644 index 0000000..d620323 --- /dev/null +++ b/repos/effect/packages/effect/src/internal/stm/tryCommit.ts @@ -0,0 +1,34 @@ +import type * as Exit from "../../Exit.js" +import type * as Journal from "./journal.js" +import * as OpCodes from "./opCodes/tryCommit.js" + +/** @internal */ +export type TryCommit = Done | Suspend + +/** @internal */ +export interface Done { + readonly _tag: OpCodes.OP_DONE + readonly exit: Exit.Exit +} + +/** @internal */ +export interface Suspend { + readonly _tag: OpCodes.OP_SUSPEND + readonly journal: Journal.Journal +} + +/** @internal */ +export const done = (exit: Exit.Exit): TryCommit => { + return { + _tag: OpCodes.OP_DONE, + exit + } +} + +/** @internal */ +export const suspend = (journal: Journal.Journal): TryCommit => { + return { + _tag: OpCodes.OP_SUSPEND, + journal + } +} diff --git a/repos/effect/packages/effect/src/internal/stm/txnId.ts b/repos/effect/packages/effect/src/internal/stm/txnId.ts new file mode 100644 index 0000000..895bd89 --- /dev/null +++ b/repos/effect/packages/effect/src/internal/stm/txnId.ts @@ -0,0 +1,14 @@ +/** @internal */ +export type TxnId = number & { + readonly TransactioId: unique symbol +} + +/** @internal */ +const txnCounter = { ref: 0 } + +/** @internal */ +export const make = (): TxnId => { + const newId = txnCounter.ref + 1 + txnCounter.ref = newId + return newId as TxnId +} diff --git a/repos/effect/packages/effect/src/internal/stm/versioned.ts b/repos/effect/packages/effect/src/internal/stm/versioned.ts new file mode 100644 index 0000000..4b1a6d4 --- /dev/null +++ b/repos/effect/packages/effect/src/internal/stm/versioned.ts @@ -0,0 +1,4 @@ +/** @internal */ +export class Versioned { + constructor(readonly value: A) {} +} diff --git a/repos/effect/packages/effect/src/internal/stream.ts b/repos/effect/packages/effect/src/internal/stream.ts new file mode 100644 index 0000000..8c2d46d --- /dev/null +++ b/repos/effect/packages/effect/src/internal/stream.ts @@ -0,0 +1,8802 @@ +import * as Cause from "../Cause.js" +import type * as Channel from "../Channel.js" +import * as Chunk from "../Chunk.js" +import * as Clock from "../Clock.js" +import * as Context from "../Context.js" +import * as Deferred from "../Deferred.js" +import * as Duration from "../Duration.js" +import * as Effect from "../Effect.js" +import * as Either from "../Either.js" +import * as Equal from "../Equal.js" +import type { ExecutionPlan } from "../ExecutionPlan.js" +import * as Exit from "../Exit.js" +import * as Fiber from "../Fiber.js" +import * as FiberRef from "../FiberRef.js" +import type { LazyArg } from "../Function.js" +import { constTrue, dual, identity, pipe } from "../Function.js" +import * as internalExecutionPlan from "../internal/executionPlan.js" +import * as Layer from "../Layer.js" +import * as MergeDecision from "../MergeDecision.js" +import * as Option from "../Option.js" +import type * as Order from "../Order.js" +import { pipeArguments } from "../Pipeable.js" +import { hasProperty, type Predicate, type Refinement } from "../Predicate.js" +import * as PubSub from "../PubSub.js" +import * as Queue from "../Queue.js" +import * as RcRef from "../RcRef.js" +import * as Ref from "../Ref.js" +import * as Runtime from "../Runtime.js" +import * as Schedule from "../Schedule.js" +import type * as Scope from "../Scope.js" +import type * as Sink from "../Sink.js" +import type * as Stream from "../Stream.js" +import type * as Emit from "../StreamEmit.js" +import * as HaltStrategy from "../StreamHaltStrategy.js" +import type * as Take from "../Take.js" +import * as TPubSub from "../TPubSub.js" +import * as TQueue from "../TQueue.js" +import type * as Tracer from "../Tracer.js" +import * as Tuple from "../Tuple.js" +import type * as Types from "../Types.js" +import * as channel from "./channel.js" +import * as channelExecutor from "./channel/channelExecutor.js" +import * as MergeStrategy from "./channel/mergeStrategy.js" +import * as core from "./core-stream.js" +import * as doNotation from "./doNotation.js" +import { RingBuffer } from "./ringBuffer.js" +import * as InternalSchedule from "./schedule.js" +import * as sink_ from "./sink.js" +import * as DebounceState from "./stream/debounceState.js" +import * as emit from "./stream/emit.js" +import * as haltStrategy from "./stream/haltStrategy.js" +import * as Handoff from "./stream/handoff.js" +import * as HandoffSignal from "./stream/handoffSignal.js" +import * as pull from "./stream/pull.js" +import * as SinkEndReason from "./stream/sinkEndReason.js" +import * as ZipAllState from "./stream/zipAllState.js" +import * as ZipChunksState from "./stream/zipChunksState.js" +import * as InternalTake from "./take.js" +import * as InternalTracer from "./tracer.js" + +/** @internal */ +const StreamSymbolKey = "effect/Stream" + +/** @internal */ +export const StreamTypeId: Stream.StreamTypeId = Symbol.for( + StreamSymbolKey +) as Stream.StreamTypeId + +/** @internal */ +const streamVariance = { + _R: (_: never) => _, + _E: (_: never) => _, + _A: (_: never) => _ +} + +/** @internal */ +export class StreamImpl implements Stream.Stream { + readonly [StreamTypeId] = streamVariance + constructor( + readonly channel: Channel.Channel, unknown, E, unknown, unknown, unknown, R> + ) { + } + + pipe() { + return pipeArguments(this, arguments) + } +} + +/** @internal */ +export const isStream = (u: unknown): u is Stream.Stream => + hasProperty(u, StreamTypeId) || Effect.isEffect(u) + +/** @internal */ +export const DefaultChunkSize = 4096 + +/** @internal */ +export const accumulate = (self: Stream.Stream): Stream.Stream, E, R> => + chunks(accumulateChunks(self)) + +/** @internal */ +export const accumulateChunks = (self: Stream.Stream): Stream.Stream => { + const accumulator = ( + s: Chunk.Chunk + ): Channel.Channel, Chunk.Chunk, E, E, void, unknown> => + core.readWith({ + onInput: (input: Chunk.Chunk) => { + const next = Chunk.appendAll(s, input) + return core.flatMap( + core.write(next), + () => accumulator(next) + ) + }, + onFailure: core.fail, + onDone: () => core.void + }) + return new StreamImpl(core.pipeTo(toChannel(self), accumulator(Chunk.empty()))) +} + +/** @internal */ +export const acquireRelease = ( + acquire: Effect.Effect, + release: (resource: A, exit: Exit.Exit) => Effect.Effect +): Stream.Stream => scoped(Effect.acquireRelease(acquire, release)) + +/** @internal */ +export const aggregate = dual< + ( + sink: Sink.Sink + ) => (self: Stream.Stream) => Stream.Stream, + ( + self: Stream.Stream, + sink: Sink.Sink + ) => Stream.Stream +>( + 2, + ( + self: Stream.Stream, + sink: Sink.Sink + ): Stream.Stream => aggregateWithin(self, sink, Schedule.forever) +) + +/** @internal */ +export const aggregateWithin = dual< + ( + sink: Sink.Sink, + schedule: Schedule.Schedule, R3> + ) => (self: Stream.Stream) => Stream.Stream, + ( + self: Stream.Stream, + sink: Sink.Sink, + schedule: Schedule.Schedule, R3> + ) => Stream.Stream +>( + 3, + ( + self: Stream.Stream, + sink: Sink.Sink, + schedule: Schedule.Schedule, R3> + ): Stream.Stream => + filterMap( + aggregateWithinEither(self, sink, schedule), + (_) => + Either.match(_, { + onLeft: Option.none, + onRight: Option.some + }) + ) +) + +/** @internal */ +export const aggregateWithinEither = dual< + ( + sink: Sink.Sink, + schedule: Schedule.Schedule, R3> + ) => (self: Stream.Stream) => Stream.Stream, E2 | E, R2 | R3 | R>, + ( + self: Stream.Stream, + sink: Sink.Sink, + schedule: Schedule.Schedule, R3> + ) => Stream.Stream, E2 | E, R2 | R3 | R> +>( + 3, + ( + self: Stream.Stream, + sink: Sink.Sink, + schedule: Schedule.Schedule, R3> + ): Stream.Stream, E2 | E, R2 | R3 | R> => { + const layer = Effect.all([ + Handoff.make>(), + Ref.make(SinkEndReason.ScheduleEnd), + Ref.make(Chunk.empty()), + Schedule.driver(schedule), + Ref.make(false), + Ref.make(false) + ]) + return fromEffect(layer).pipe( + flatMap(([handoff, sinkEndReason, sinkLeftovers, scheduleDriver, consumed, endAfterEmit]) => { + const handoffProducer: Channel.Channel, never, E | E2, unknown, unknown> = core + .readWithCause({ + onInput: (input: Chunk.Chunk) => + core.flatMap( + core.fromEffect(pipe( + handoff, + Handoff.offer>(HandoffSignal.emit(input)), + Effect.when(() => Chunk.isNonEmpty(input)) + )), + () => handoffProducer + ), + onFailure: (cause) => + core.fromEffect( + Handoff.offer>( + handoff, + HandoffSignal.halt(cause) + ) + ), + onDone: () => + core.fromEffect( + Handoff.offer>( + handoff, + HandoffSignal.end(SinkEndReason.UpstreamEnd) + ) + ) + }) + const handoffConsumer: Channel.Channel, unknown, E | E2, unknown, void, unknown> = pipe( + Ref.getAndSet(sinkLeftovers, Chunk.empty()), + Effect.flatMap((leftovers) => { + if (Chunk.isNonEmpty(leftovers)) { + return pipe( + Ref.set(consumed, true), + Effect.zipRight(Effect.succeed(pipe( + core.write(leftovers), + core.flatMap(() => handoffConsumer) + ))) + ) + } + return pipe( + Handoff.take(handoff), + Effect.map((signal) => { + switch (signal._tag) { + case HandoffSignal.OP_EMIT: { + return pipe( + core.fromEffect(Ref.set(consumed, true)), + channel.zipRight(core.write(signal.elements)), + channel.zipRight(core.fromEffect(Ref.get(endAfterEmit))), + core.flatMap((bool) => bool ? core.void : handoffConsumer) + ) + } + case HandoffSignal.OP_HALT: { + return core.failCause(signal.cause) + } + case HandoffSignal.OP_END: { + if (signal.reason._tag === SinkEndReason.OP_SCHEDULE_END) { + return pipe( + Ref.get(consumed), + Effect.map((bool) => + bool ? + core.fromEffect( + pipe( + Ref.set(sinkEndReason, SinkEndReason.ScheduleEnd), + Effect.zipRight(Ref.set(endAfterEmit, true)) + ) + ) : + pipe( + core.fromEffect( + pipe( + Ref.set(sinkEndReason, SinkEndReason.ScheduleEnd), + Effect.zipRight(Ref.set(endAfterEmit, true)) + ) + ), + core.flatMap(() => handoffConsumer) + ) + ), + channel.unwrap + ) + } + return pipe( + Ref.set(sinkEndReason, signal.reason), + Effect.zipRight(Ref.set(endAfterEmit, true)), + core.fromEffect + ) + } + } + }) + ) + }), + channel.unwrap + ) + const timeout = (lastB: Option.Option): Effect.Effect, R2 | R3> => + scheduleDriver.next(lastB) + const scheduledAggregator = ( + sinkFiber: Fiber.RuntimeFiber>, B], E | E2>, + scheduleFiber: Fiber.RuntimeFiber>, + scope: Scope.Scope + ): Channel.Channel>, unknown, E | E2, unknown, unknown, unknown, R2 | R3> => { + const forkSink = pipe( + Ref.set(consumed, false), + Effect.zipRight(Ref.set(endAfterEmit, false)), + Effect.zipRight( + pipe( + handoffConsumer, + channel.pipeToOrFail(sink_.toChannel(sink)), + core.collectElements, + channel.run, + Effect.forkIn(scope) + ) + ) + ) + const handleSide = ( + leftovers: Chunk.Chunk>, + b: B, + c: Option.Option + ): Channel.Channel>, unknown, E | E2, unknown, unknown, unknown, R2 | R3> => + pipe( + Ref.set(sinkLeftovers, Chunk.flatten(leftovers)), + Effect.zipRight( + Effect.map(Ref.get(sinkEndReason), (reason) => { + switch (reason._tag) { + case SinkEndReason.OP_SCHEDULE_END: { + return pipe( + Effect.all([ + Ref.get(consumed), + forkSink, + pipe(timeout(Option.some(b)), Effect.forkIn(scope)) + ]), + Effect.map(([wasConsumed, sinkFiber, scheduleFiber]) => { + const toWrite = pipe( + c, + Option.match({ + onNone: (): Chunk.Chunk> => Chunk.of(Either.right(b)), + onSome: (c): Chunk.Chunk> => + Chunk.make(Either.right(b), Either.left(c)) + }) + ) + if (wasConsumed) { + return pipe( + core.write(toWrite), + core.flatMap(() => scheduledAggregator(sinkFiber, scheduleFiber, scope)) + ) + } + return scheduledAggregator(sinkFiber, scheduleFiber, scope) + }), + channel.unwrap + ) + } + case SinkEndReason.OP_UPSTREAM_END: { + return pipe( + Ref.get(consumed), + Effect.map((wasConsumed) => + wasConsumed ? + core.write(Chunk.of>(Either.right(b))) : + core.void + ), + channel.unwrap + ) + } + } + }) + ), + channel.unwrap + ) + return channel.unwrap( + Effect.raceWith(Fiber.join(sinkFiber), Fiber.join(scheduleFiber), { + onSelfDone: (sinkExit, _) => + pipe( + Fiber.interrupt(scheduleFiber), + Effect.zipRight(pipe( + Effect.suspend(() => sinkExit), + Effect.map(([leftovers, b]) => handleSide(leftovers, b, Option.none())) + )) + ), + onOtherDone: (scheduleExit, _) => + Effect.matchCauseEffect(Effect.suspend(() => scheduleExit), { + onFailure: (cause) => + Either.match( + Cause.failureOrCause(cause), + { + onLeft: () => + pipe( + handoff, + Handoff.offer>( + HandoffSignal.end(SinkEndReason.ScheduleEnd) + ), + Effect.forkDaemon, + Effect.zipRight( + pipe( + Fiber.join(sinkFiber), + Effect.map(([leftovers, b]) => handleSide(leftovers, b, Option.none())) + ) + ) + ), + onRight: (cause) => + pipe( + handoff, + Handoff.offer>( + HandoffSignal.halt(cause) + ), + Effect.forkDaemon, + Effect.zipRight( + pipe( + Fiber.join(sinkFiber), + Effect.map(([leftovers, b]) => handleSide(leftovers, b, Option.none())) + ) + ) + ) + } + ), + onSuccess: (c) => + pipe( + handoff, + Handoff.offer>( + HandoffSignal.end(SinkEndReason.ScheduleEnd) + ), + Effect.forkDaemon, + Effect.zipRight( + pipe( + Fiber.join(sinkFiber), + Effect.map(([leftovers, b]) => handleSide(leftovers, b, Option.some(c))) + ) + ) + ) + }) + }) + ) + } + return unwrapScopedWith((scope) => + core.pipeTo(toChannel(self), handoffProducer).pipe( + channel.run, + Effect.forkIn(scope), + Effect.zipRight( + channel.pipeToOrFail(handoffConsumer, sink_.toChannel(sink)).pipe( + core.collectElements, + channel.run, + Effect.forkIn(scope), + Effect.flatMap((sinkFiber) => + timeout(Option.none()).pipe( + Effect.forkIn(scope), + Effect.map((scheduleFiber) => + new StreamImpl( + scheduledAggregator(sinkFiber, scheduleFiber, scope) + ) + ) + ) + ) + ) + ) + ) + ) + }) + ) + } +) + +/** @internal */ +export const as = dual< + (value: B) => (self: Stream.Stream) => Stream.Stream, + (self: Stream.Stream, value: B) => Stream.Stream +>(2, (self: Stream.Stream, value: B): Stream.Stream => map(self, () => value)) + +const queueFromBufferOptions = ( + bufferSize?: number | "unbounded" | { + readonly bufferSize?: number | undefined + readonly strategy?: "dropping" | "sliding" | "suspend" | undefined + } | undefined +): Effect.Effect>> => { + if (bufferSize === "unbounded") { + return Queue.unbounded() + } else if (typeof bufferSize === "number" || bufferSize === undefined) { + return Queue.bounded(bufferSize ?? 16) + } + switch (bufferSize.strategy) { + case "dropping": + return Queue.dropping(bufferSize.bufferSize ?? 16) + case "sliding": + return Queue.sliding(bufferSize.bufferSize ?? 16) + default: + return Queue.bounded(bufferSize.bufferSize ?? 16) + } +} + +/** @internal */ +export const _async = ( + register: ( + emit: Emit.Emit + ) => Effect.Effect | void, + bufferSize?: number | "unbounded" | { + readonly bufferSize?: number | undefined + readonly strategy?: "dropping" | "sliding" | "suspend" | undefined + } | undefined +): Stream.Stream => + Effect.acquireRelease( + queueFromBufferOptions(bufferSize), + (queue) => Queue.shutdown(queue) + ).pipe( + Effect.flatMap((output) => + Effect.runtime().pipe( + Effect.flatMap((runtime) => + Effect.sync(() => { + const runPromiseExit = Runtime.runPromiseExit(runtime) + const canceler = register(emit.make((resume) => + InternalTake.fromPull(resume).pipe( + Effect.flatMap((take) => Queue.offer(output, take)), + Effect.asVoid, + runPromiseExit + ).then((exit) => { + if (Exit.isFailure(exit)) { + if (!Cause.isInterrupted(exit.cause)) { + throw Cause.squash(exit.cause) + } + } + }) + )) + return canceler + }) + ), + Effect.map((value) => { + const loop: Channel.Channel, unknown, E, unknown, void, unknown> = Queue.take(output).pipe( + Effect.flatMap((take) => InternalTake.done(take)), + Effect.match({ + onFailure: (maybeError) => + core.fromEffect(Queue.shutdown(output)).pipe( + channel.zipRight(Option.match(maybeError, { + onNone: () => core.void, + onSome: (error) => core.fail(error) + })) + ), + onSuccess: (chunk) => core.write(chunk).pipe(core.flatMap(() => loop)) + }), + channel.unwrap + ) + return fromChannel(loop).pipe(ensuring(value ?? Effect.void)) + }) + ) + ), + unwrapScoped + ) + +/** @internal */ +export const asyncEffect = ( + register: (emit: Emit.Emit) => Effect.Effect, + bufferSize?: number | "unbounded" | { + readonly bufferSize?: number | undefined + readonly strategy?: "dropping" | "sliding" | "suspend" | undefined + } | undefined +): Stream.Stream => + pipe( + Effect.acquireRelease( + queueFromBufferOptions(bufferSize), + (queue) => Queue.shutdown(queue) + ), + Effect.flatMap((output) => + pipe( + Effect.runtime(), + Effect.flatMap((runtime) => + pipe( + register( + emit.make((k) => + pipe( + InternalTake.fromPull(k), + Effect.flatMap((take) => Queue.offer(output, take)), + Effect.asVoid, + Runtime.runPromiseExit(runtime) + ).then((exit) => { + if (Exit.isFailure(exit)) { + if (!Cause.isInterrupted(exit.cause)) { + throw Cause.squash(exit.cause) + } + } + }) + ) + ), + Effect.map(() => { + const loop: Channel.Channel, unknown, E, unknown, void, unknown> = pipe( + Queue.take(output), + Effect.flatMap(InternalTake.done), + Effect.match({ + onFailure: (maybeError) => + pipe( + core.fromEffect(Queue.shutdown(output)), + channel.zipRight(Option.match(maybeError, { onNone: () => core.void, onSome: core.fail })) + ), + onSuccess: (chunk) => pipe(core.write(chunk), core.flatMap(() => loop)) + }), + channel.unwrap + ) + return loop + }) + ) + ) + ) + ), + channel.unwrapScoped, + fromChannel + ) + +const queueFromBufferOptionsPush = ( + options?: { readonly bufferSize: "unbounded" } | { + readonly bufferSize?: number | undefined + readonly strategy?: "dropping" | "sliding" | undefined + } | undefined +): Effect.Effect | Exit.Exit>> => { + if (options?.bufferSize === "unbounded" || (options?.bufferSize === undefined && options?.strategy === undefined)) { + return Queue.unbounded() + } + switch (options?.strategy) { + case "sliding": + return Queue.sliding(options.bufferSize ?? 16) + default: + return Queue.dropping(options?.bufferSize ?? 16) + } +} + +/** @internal */ +export const asyncPush = ( + register: (emit: Emit.EmitOpsPush) => Effect.Effect, + options?: { + readonly bufferSize: "unbounded" + } | { + readonly bufferSize?: number | undefined + readonly strategy?: "dropping" | "sliding" | undefined + } | undefined +): Stream.Stream> => + Effect.acquireRelease( + queueFromBufferOptionsPush(options), + Queue.shutdown + ).pipe( + Effect.tap((queue) => + FiberRef.getWith(FiberRef.currentScheduler, (scheduler) => register(emit.makePush(queue, scheduler))) + ), + Effect.map((queue) => { + const loop: Channel.Channel, unknown, E> = core.flatMap(Queue.take(queue), (item) => + Exit.isExit(item) + ? Exit.isSuccess(item) ? core.void : core.failCause(item.cause) + : channel.zipRight(core.write(Chunk.unsafeFromArray(item)), loop)) + return loop + }), + channel.unwrapScoped, + fromChannel + ) + +/** @internal */ +export const asyncScoped = ( + register: (emit: Emit.Emit) => Effect.Effect, + bufferSize?: number | "unbounded" | { + readonly bufferSize?: number | undefined + readonly strategy?: "dropping" | "sliding" | "suspend" | undefined + } | undefined +): Stream.Stream> => + pipe( + Effect.acquireRelease( + queueFromBufferOptions(bufferSize), + (queue) => Queue.shutdown(queue) + ), + Effect.flatMap((output) => + pipe( + Effect.runtime(), + Effect.flatMap((runtime) => + pipe( + register( + emit.make((k) => + pipe( + InternalTake.fromPull(k), + Effect.flatMap((take) => Queue.offer(output, take)), + Effect.asVoid, + Runtime.runPromiseExit(runtime) + ).then((exit) => { + if (Exit.isFailure(exit)) { + if (!Cause.isInterrupted(exit.cause)) { + throw Cause.squash(exit.cause) + } + } + }) + ) + ), + Effect.zipRight(Ref.make(false)), + Effect.flatMap((ref) => + pipe( + Ref.get(ref), + Effect.map((isDone) => + isDone ? + pull.end() : + pipe( + Queue.take(output), + Effect.flatMap(InternalTake.done), + Effect.onError(() => + pipe( + Ref.set(ref, true), + Effect.zipRight(Queue.shutdown(output)) + ) + ) + ) + ) + ) + ) + ) + ) + ) + ), + scoped, + flatMap(repeatEffectChunkOption) + ) + +/** @internal */ +export const branchAfter = dual< + ( + n: number, + f: (input: Chunk.Chunk) => Stream.Stream + ) => (self: Stream.Stream) => Stream.Stream, + ( + self: Stream.Stream, + n: number, + f: (input: Chunk.Chunk) => Stream.Stream + ) => Stream.Stream +>( + 3, + ( + self: Stream.Stream, + n: number, + f: (input: Chunk.Chunk) => Stream.Stream + ) => + suspend(() => { + const buffering = ( + acc: Chunk.Chunk + ): Channel.Channel, Chunk.Chunk, E2, never, unknown, unknown, R | R2> => + core.readWith({ + onInput: (input) => { + const nextSize = acc.length + input.length + if (nextSize >= n) { + const [b1, b2] = pipe(input, Chunk.splitAt(n - acc.length)) + return running(pipe(acc, Chunk.appendAll(b1)), b2) + } + return buffering(pipe(acc, Chunk.appendAll(input))) + }, + onFailure: core.fail, + onDone: () => running(acc, Chunk.empty()) + }) + const running = ( + prefix: Chunk.Chunk, + leftover: Chunk.Chunk + ): Channel.Channel, Chunk.Chunk, E2, never, unknown, unknown, R | R2> => + core.pipeTo( + channel.zipRight( + core.write(leftover), + channel.identityChannel() + ), + toChannel(f(prefix)) + ) + return new StreamImpl(pipe(toChannel(self), channel.pipeToOrFail(buffering(Chunk.empty())))) + }) +) + +/** @internal */ +export const broadcast = dual< + ( + n: N, + maximumLag: number | { + readonly capacity: "unbounded" + readonly replay?: number | undefined + } | { + readonly capacity: number + readonly strategy?: "sliding" | "dropping" | "suspend" | undefined + readonly replay?: number | undefined + } + ) => ( + self: Stream.Stream + ) => Effect.Effect>, never, Scope.Scope | R>, + ( + self: Stream.Stream, + n: N, + maximumLag: number | { + readonly capacity: "unbounded" + readonly replay?: number | undefined + } | { + readonly capacity: number + readonly strategy?: "sliding" | "dropping" | "suspend" | undefined + readonly replay?: number | undefined + } + ) => Effect.Effect>, never, Scope.Scope | R> +>(3, ( + self: Stream.Stream, + n: N, + maximumLag: number | { + readonly capacity: "unbounded" + readonly replay?: number | undefined + } | { + readonly capacity: number + readonly strategy?: "sliding" | "dropping" | "suspend" | undefined + readonly replay?: number | undefined + } +): Effect.Effect>, never, Scope.Scope | R> => + pipe( + self, + broadcastedQueues(n, maximumLag), + Effect.map((tuple) => + tuple.map((queue) => flattenTake(fromQueue(queue, { shutdown: true }))) as Types.TupleOf> + ) + )) + +/** @internal */ +export const broadcastDynamic = dual< + ( + maximumLag: number | { + readonly capacity: "unbounded" + readonly replay?: number | undefined + } | { + readonly capacity: number + readonly strategy?: "sliding" | "dropping" | "suspend" | undefined + readonly replay?: number | undefined + } + ) => (self: Stream.Stream) => Effect.Effect, never, Scope.Scope | R>, + ( + self: Stream.Stream, + maximumLag: number | { + readonly capacity: "unbounded" + readonly replay?: number | undefined + } | { + readonly capacity: number + readonly strategy?: "sliding" | "dropping" | "suspend" | undefined + readonly replay?: number | undefined + } + ) => Effect.Effect, never, Scope.Scope | R> +>(2, ( + self: Stream.Stream, + maximumLag: number | { + readonly capacity: "unbounded" + readonly replay?: number | undefined + } | { + readonly capacity: number + readonly strategy?: "sliding" | "dropping" | "suspend" | undefined + readonly replay?: number | undefined + } +): Effect.Effect, never, Scope.Scope | R> => + Effect.map(toPubSub(self, maximumLag), (pubsub) => flattenTake(fromPubSub(pubsub)))) + +export const share = dual< + ( + config: { + readonly capacity: "unbounded" + readonly replay?: number | undefined + readonly idleTimeToLive?: Duration.DurationInput | undefined + } | { + readonly capacity: number + readonly strategy?: "sliding" | "dropping" | "suspend" | undefined + readonly replay?: number | undefined + readonly idleTimeToLive?: Duration.DurationInput | undefined + } + ) => ( + self: Stream.Stream + ) => Effect.Effect, never, R | Scope.Scope>, + ( + self: Stream.Stream, + config: { + readonly capacity: "unbounded" + readonly replay?: number | undefined + readonly idleTimeToLive?: Duration.DurationInput | undefined + } | { + readonly capacity: number + readonly strategy?: "sliding" | "dropping" | "suspend" | undefined + readonly replay?: number | undefined + readonly idleTimeToLive?: Duration.DurationInput | undefined + } + ) => Effect.Effect, never, R | Scope.Scope> +>( + 2, + ( + self: Stream.Stream, + options: { + readonly capacity: "unbounded" + readonly replay?: number | undefined + readonly idleTimeToLive?: Duration.DurationInput | undefined + } | { + readonly capacity: number + readonly strategy?: "sliding" | "dropping" | "suspend" | undefined + readonly replay?: number | undefined + readonly idleTimeToLive?: Duration.DurationInput | undefined + } + ): Effect.Effect, never, R | Scope.Scope> => + Effect.map( + RcRef.make({ + acquire: broadcastDynamic(self, options), + idleTimeToLive: options.idleTimeToLive + }), + (rcRef) => unwrapScoped(RcRef.get(rcRef)) + ) +) + +/** @internal */ +export const broadcastedQueues = dual< + ( + n: N, + maximumLag: number | { + readonly capacity: "unbounded" + readonly replay?: number | undefined + } | { + readonly capacity: number + readonly strategy?: "sliding" | "dropping" | "suspend" | undefined + readonly replay?: number | undefined + } + ) => ( + self: Stream.Stream + ) => Effect.Effect>>, never, Scope.Scope | R>, + ( + self: Stream.Stream, + n: N, + maximumLag: number | { + readonly capacity: "unbounded" + readonly replay?: number | undefined + } | { + readonly capacity: number + readonly strategy?: "sliding" | "dropping" | "suspend" | undefined + readonly replay?: number | undefined + } + ) => Effect.Effect>>, never, Scope.Scope | R> +>(3, ( + self: Stream.Stream, + n: N, + maximumLag: number | { + readonly capacity: "unbounded" + readonly replay?: number | undefined + } | { + readonly capacity: number + readonly strategy?: "sliding" | "dropping" | "suspend" | undefined + readonly replay?: number | undefined + } +): Effect.Effect>>, never, Scope.Scope | R> => + Effect.flatMap(pubsubFromOptions(maximumLag), (pubsub) => + pipe( + Effect.all(Array.from({ length: n }, () => PubSub.subscribe(pubsub))) as Effect.Effect< + Types.TupleOf>>, + never, + R + >, + Effect.tap(() => Effect.forkScoped(runIntoPubSubScoped(self, pubsub))) + ))) + +/** @internal */ +export const broadcastedQueuesDynamic = dual< + ( + maximumLag: number | { + readonly capacity: "unbounded" + readonly replay?: number | undefined + } | { + readonly capacity: number + readonly strategy?: "sliding" | "dropping" | "suspend" | undefined + readonly replay?: number | undefined + } + ) => ( + self: Stream.Stream + ) => Effect.Effect>, never, Scope.Scope>, never, Scope.Scope | R>, + ( + self: Stream.Stream, + maximumLag: number | { + readonly capacity: "unbounded" + readonly replay?: number | undefined + } | { + readonly capacity: number + readonly strategy?: "sliding" | "dropping" | "suspend" | undefined + readonly replay?: number | undefined + } + ) => Effect.Effect>, never, Scope.Scope>, never, Scope.Scope | R> +>(2, ( + self: Stream.Stream, + maximumLag: number | { + readonly capacity: "unbounded" + readonly replay?: number | undefined + } | { + readonly capacity: number + readonly strategy?: "sliding" | "dropping" | "suspend" | undefined + readonly replay?: number | undefined + } +): Effect.Effect>, never, Scope.Scope>, never, Scope.Scope | R> => + Effect.map(toPubSub(self, maximumLag), PubSub.subscribe)) + +/** @internal */ +export const buffer = dual< + ( + options: { + readonly capacity: "unbounded" + } | { + readonly capacity: number + readonly strategy?: "dropping" | "sliding" | "suspend" | undefined + } + ) => (self: Stream.Stream) => Stream.Stream, + ( + self: Stream.Stream, + options: { + readonly capacity: "unbounded" + } | { + readonly capacity: number + readonly strategy?: "dropping" | "sliding" | "suspend" | undefined + } + ) => Stream.Stream +>(2, ( + self: Stream.Stream, + options: { + readonly capacity: "unbounded" + } | { + readonly capacity: number + readonly strategy?: "dropping" | "sliding" | "suspend" | undefined + } +): Stream.Stream => { + if (options.capacity === "unbounded") { + return bufferUnbounded(self) + } else if (options.strategy === "dropping") { + return bufferDropping(self, options.capacity) + } else if (options.strategy === "sliding") { + return bufferSliding(self, options.capacity) + } + const queue = toQueueOfElements(self, options) + return new StreamImpl( + channel.unwrapScoped( + Effect.map(queue, (queue) => { + const process: Channel.Channel, unknown, E, unknown, void, unknown> = pipe( + core.fromEffect(Queue.take(queue)), + core.flatMap(Exit.match({ + onFailure: (cause) => + pipe( + Cause.flipCauseOption(cause), + Option.match({ onNone: () => core.void, onSome: core.failCause }) + ), + onSuccess: (value) => core.flatMap(core.write(Chunk.of(value)), () => process) + })) + ) + return process + }) + ) + ) +}) + +/** @internal */ +export const bufferChunks = dual< + (options: { + readonly capacity: number + readonly strategy?: "dropping" | "sliding" | "suspend" | undefined + }) => (self: Stream.Stream) => Stream.Stream, + (self: Stream.Stream, options: { + readonly capacity: number + readonly strategy?: "dropping" | "sliding" | "suspend" | undefined + }) => Stream.Stream +>(2, (self: Stream.Stream, options: { + readonly capacity: number + readonly strategy?: "dropping" | "sliding" | "suspend" | undefined +}): Stream.Stream => { + if (options.strategy === "dropping") { + return bufferChunksDropping(self, options.capacity) + } else if (options.strategy === "sliding") { + return bufferChunksSliding(self, options.capacity) + } + const queue = toQueue(self, options) + return new StreamImpl( + channel.unwrapScoped( + Effect.map(queue, (queue) => { + const process: Channel.Channel, unknown, E, unknown, void, unknown> = pipe( + core.fromEffect(Queue.take(queue)), + core.flatMap(InternalTake.match({ + onEnd: () => core.void, + onFailure: core.failCause, + onSuccess: (value) => pipe(core.write(value), core.flatMap(() => process)) + })) + ) + return process + }) + ) + ) +}) + +const bufferChunksDropping = dual< + (capacity: number) => (self: Stream.Stream) => Stream.Stream, + (self: Stream.Stream, capacity: number) => Stream.Stream +>(2, (self: Stream.Stream, capacity: number): Stream.Stream => { + const queue = Effect.acquireRelease( + Queue.dropping, Deferred.Deferred]>(capacity), + (queue) => Queue.shutdown(queue) + ) + return new StreamImpl(bufferSignal(queue, toChannel(self))) +}) + +const bufferChunksSliding = dual< + (capacity: number) => (self: Stream.Stream) => Stream.Stream, + (self: Stream.Stream, capacity: number) => Stream.Stream +>(2, (self: Stream.Stream, capacity: number): Stream.Stream => { + const queue = Effect.acquireRelease( + Queue.sliding, Deferred.Deferred]>(capacity), + (queue) => Queue.shutdown(queue) + ) + return new StreamImpl(bufferSignal(queue, toChannel(self))) +}) + +const bufferDropping = dual< + (capacity: number) => (self: Stream.Stream) => Stream.Stream, + (self: Stream.Stream, capacity: number) => Stream.Stream +>(2, (self: Stream.Stream, capacity: number): Stream.Stream => { + const queue = Effect.acquireRelease( + Queue.dropping, Deferred.Deferred]>(capacity), + (queue) => Queue.shutdown(queue) + ) + return new StreamImpl(bufferSignal(queue, toChannel(rechunk(1)(self)))) +}) + +const bufferSliding = dual< + (capacity: number) => (self: Stream.Stream) => Stream.Stream, + (self: Stream.Stream, capacity: number) => Stream.Stream +>(2, (self: Stream.Stream, capacity: number): Stream.Stream => { + const queue = Effect.acquireRelease( + Queue.sliding, Deferred.Deferred]>(capacity), + (queue) => Queue.shutdown(queue) + ) + return new StreamImpl(bufferSignal(queue, toChannel(pipe(self, rechunk(1))))) +}) + +const bufferUnbounded = (self: Stream.Stream): Stream.Stream => { + const queue = toQueue(self, { strategy: "unbounded" }) + return new StreamImpl( + channel.unwrapScoped( + Effect.map(queue, (queue) => { + const process: Channel.Channel, unknown, E, unknown, void, unknown> = pipe( + core.fromEffect(Queue.take(queue)), + core.flatMap(InternalTake.match({ + onEnd: () => core.void, + onFailure: core.failCause, + onSuccess: (value) => core.flatMap(core.write(value), () => process) + })) + ) + return process + }) + ) + ) +} + +const bufferSignal = ( + scoped: Effect.Effect, Deferred.Deferred]>, never, Scope.Scope>, + bufferChannel: Channel.Channel, unknown, E, unknown, void, unknown, R> +): Channel.Channel, unknown, E, unknown, void, unknown, R> => { + const producer = ( + queue: Queue.Queue, Deferred.Deferred]>, + ref: Ref.Ref> + ): Channel.Channel, never, E, unknown, unknown, R> => { + const terminate = (take: Take.Take): Channel.Channel, never, E, unknown, unknown, R> => + pipe( + Ref.get(ref), + Effect.tap(Deferred.await), + Effect.zipRight(Deferred.make()), + Effect.flatMap((deferred) => + pipe( + Queue.offer(queue, [take, deferred] as const), + Effect.zipRight(Ref.set(ref, deferred)), + Effect.zipRight(Deferred.await(deferred)) + ) + ), + Effect.asVoid, + core.fromEffect + ) + return core.readWithCause({ + onInput: (input: Chunk.Chunk) => + pipe( + Deferred.make(), + Effect.flatMap( + (deferred) => + pipe( + Queue.offer(queue, [InternalTake.chunk(input), deferred] as const), + Effect.flatMap((added) => pipe(Ref.set(ref, deferred), Effect.when(() => added))) + ) + ), + Effect.asVoid, + core.fromEffect, + core.flatMap(() => producer(queue, ref)) + ), + onFailure: (error) => terminate(InternalTake.failCause(error)), + onDone: () => terminate(InternalTake.end) + }) + } + const consumer = ( + queue: Queue.Queue, Deferred.Deferred]> + ): Channel.Channel, unknown, E, unknown, void, unknown, R> => { + const process: Channel.Channel, unknown, E, unknown, void, unknown> = pipe( + core.fromEffect(Queue.take(queue)), + core.flatMap(([take, deferred]) => + channel.zipRight( + core.fromEffect(Deferred.succeed(deferred, void 0)), + InternalTake.match(take, { + onEnd: () => core.void, + onFailure: core.failCause, + onSuccess: (value) => pipe(core.write(value), core.flatMap(() => process)) + }) + ) + ) + ) + return process + } + return channel.unwrapScoped( + pipe( + scoped, + Effect.flatMap((queue) => + pipe( + Deferred.make(), + Effect.tap((start) => Deferred.succeed(start, void 0)), + Effect.flatMap((start) => + pipe( + Ref.make(start), + Effect.flatMap((ref) => + pipe( + bufferChannel, + core.pipeTo(producer(queue, ref)), + channel.runScoped, + Effect.forkScoped + ) + ), + Effect.as(consumer(queue)) + ) + ) + ) + ) + ) + ) +} + +/** @internal */ +export const catchAll = dual< + ( + f: (error: E) => Stream.Stream + ) => (self: Stream.Stream) => Stream.Stream, + ( + self: Stream.Stream, + f: (error: E) => Stream.Stream + ) => Stream.Stream +>(2, ( + self: Stream.Stream, + f: (error: E) => Stream.Stream +): Stream.Stream => + catchAllCause(self, (cause) => + Either.match(Cause.failureOrCause(cause), { + onLeft: f, + onRight: failCause + }))) + +/** @internal */ +export const catchAllCause = dual< + ( + f: (cause: Cause.Cause) => Stream.Stream + ) => (self: Stream.Stream) => Stream.Stream, + ( + self: Stream.Stream, + f: (cause: Cause.Cause) => Stream.Stream + ) => Stream.Stream +>( + 2, + ( + self: Stream.Stream, + f: (cause: Cause.Cause) => Stream.Stream + ): Stream.Stream => + new StreamImpl(pipe(toChannel(self), core.catchAllCause((cause) => toChannel(f(cause))))) +) + +/** @internal */ +export const catchSome = dual< + ( + pf: (error: E) => Option.Option> + ) => (self: Stream.Stream) => Stream.Stream, + ( + self: Stream.Stream, + pf: (error: E) => Option.Option> + ) => Stream.Stream +>( + 2, + ( + self: Stream.Stream, + pf: (error: E) => Option.Option> + ): Stream.Stream => + pipe(self, catchAll((error) => pipe(pf(error), Option.getOrElse(() => fail(error))))) +) + +/** @internal */ +export const catchSomeCause = dual< + ( + pf: (cause: Cause.Cause) => Option.Option> + ) => (self: Stream.Stream) => Stream.Stream, + ( + self: Stream.Stream, + pf: (cause: Cause.Cause) => Option.Option> + ) => Stream.Stream +>( + 2, + ( + self: Stream.Stream, + pf: (cause: Cause.Cause) => Option.Option> + ): Stream.Stream => + pipe(self, catchAllCause((cause) => pipe(pf(cause), Option.getOrElse(() => failCause(cause))))) +) + +/* @internal */ +export const catchTag = dual< + ( + k: K, + f: (e: Extract) => Stream.Stream + ) => (self: Stream.Stream) => Stream.Stream | E1, R | R1>, + ( + self: Stream.Stream, + k: K, + f: (e: Extract) => Stream.Stream + ) => Stream.Stream | E1, R | R1> +>(3, (self, k, f) => + catchAll(self, (e) => { + if ("_tag" in e && e["_tag"] === k) { + return f(e as any) + } + return fail(e as any) + })) + +/** @internal */ +export const catchTags: { + < + E extends { _tag: string }, + Cases extends { + [K in E["_tag"]]+?: (error: Extract) => Stream.Stream + } + >( + cases: Cases + ): (self: Stream.Stream) => Stream.Stream< + | A + | { + [K in keyof Cases]: Cases[K] extends + ((...args: Array) => Stream.Stream.Variance) ? A + : never + }[keyof Cases], + | Exclude + | { + [K in keyof Cases]: Cases[K] extends + ((...args: Array) => Stream.Stream.Variance) ? E + : never + }[keyof Cases], + | R + | { + [K in keyof Cases]: Cases[K] extends + ((...args: Array) => Stream.Stream.Variance) ? R + : never + }[keyof Cases] + > + < + A, + E extends { _tag: string }, + R, + Cases extends { + [K in E["_tag"]]+?: (error: Extract) => Stream.Stream + } + >( + self: Stream.Stream, + cases: Cases + ): Stream.Stream< + | A + | { + [K in keyof Cases]: Cases[K] extends + ((...args: Array) => Stream.Stream.Variance) ? A + : never + }[keyof Cases], + | Exclude + | { + [K in keyof Cases]: Cases[K] extends + ((...args: Array) => Stream.Stream.Variance) ? E + : never + }[keyof Cases], + | R + | { + [K in keyof Cases]: Cases[K] extends + ((...args: Array) => Stream.Stream.Variance) ? R + : never + }[keyof Cases] + > +} = dual(2, (self, cases) => + catchAll(self, (e: any) => { + const keys = Object.keys(cases) + if ("_tag" in e && keys.includes(e["_tag"])) { + return cases[e["_tag"]](e as any) + } + return fail(e as any) + })) + +/** @internal */ +export const changes = (self: Stream.Stream): Stream.Stream => + pipe(self, changesWith((x, y) => Equal.equals(y)(x))) + +/** @internal */ +export const changesWith = dual< + (f: (x: A, y: A) => boolean) => (self: Stream.Stream) => Stream.Stream, + (self: Stream.Stream, f: (x: A, y: A) => boolean) => Stream.Stream +>(2, (self: Stream.Stream, f: (x: A, y: A) => boolean): Stream.Stream => { + const writer = ( + last: Option.Option + ): Channel.Channel, Chunk.Chunk, E, E, void, unknown> => + core.readWithCause({ + onInput: (input: Chunk.Chunk) => { + const [newLast, newChunk] = Chunk.reduce( + input, + [last, Chunk.empty()] as const, + ([option, outputs], output) => { + if (Option.isSome(option) && f(option.value, output)) { + return [Option.some(output), outputs] as const + } + return [Option.some(output), pipe(outputs, Chunk.append(output))] as const + } + ) + if (Chunk.isEmpty(newChunk)) return writer(newLast) + return core.flatMap( + core.write(newChunk), + () => writer(newLast) + ) + }, + onFailure: core.failCause, + onDone: () => core.void + }) + return new StreamImpl(pipe(toChannel(self), core.pipeTo(writer(Option.none())))) +}) + +/** @internal */ +export const changesWithEffect = dual< + ( + f: (x: A, y: A) => Effect.Effect + ) => (self: Stream.Stream) => Stream.Stream, + ( + self: Stream.Stream, + f: (x: A, y: A) => Effect.Effect + ) => Stream.Stream +>( + 2, + ( + self: Stream.Stream, + f: (x: A, y: A) => Effect.Effect + ): Stream.Stream => { + const writer = ( + last: Option.Option + ): Channel.Channel, Chunk.Chunk, E | E2, E | E2, void, unknown, R | R2> => + core.readWithCause({ + onInput: (input: Chunk.Chunk) => + pipe( + input, + Effect.reduce([last, Chunk.empty()] as const, ([option, outputs], output) => { + if (Option.isSome(option)) { + return pipe( + f(option.value, output), + Effect.map((bool) => + bool ? + [Option.some(output), outputs] as const : + [Option.some(output), pipe(outputs, Chunk.append(output))] as const + ) + ) + } + return Effect.succeed( + [ + Option.some(output), + pipe(outputs, Chunk.append(output)) + ] as const + ) + }), + core.fromEffect, + core.flatMap(([newLast, newChunk]) => + pipe( + core.write(newChunk), + core.flatMap(() => writer(newLast)) + ) + ) + ), + onFailure: core.failCause, + onDone: () => core.void + }) + return new StreamImpl(pipe(toChannel(self), core.pipeTo(writer(Option.none())))) + } +) + +/** @internal */ +export const chunks = (self: Stream.Stream): Stream.Stream, E, R> => + pipe(self, mapChunks(Chunk.of)) + +/** @internal */ +export const chunksWith = dual< + ( + f: (stream: Stream.Stream, E, R>) => Stream.Stream, E2, R2> + ) => (self: Stream.Stream) => Stream.Stream, + ( + self: Stream.Stream, + f: (stream: Stream.Stream, E, R>) => Stream.Stream, E2, R2> + ) => Stream.Stream +>( + 2, + ( + self: Stream.Stream, + f: (stream: Stream.Stream, E, R>) => Stream.Stream, E2, R2> + ): Stream.Stream => flattenChunks(f(chunks(self))) +) + +const unsome = (effect: Effect.Effect, R>): Effect.Effect, E, R> => + Effect.catchAll( + Effect.asSome(effect), + (o) => o._tag === "None" ? Effect.succeedNone : Effect.fail(o.value) + ) + +/** @internal */ +export const combine = dual< + ( + that: Stream.Stream, + s: S, + f: ( + s: S, + pullLeft: Effect.Effect, R3>, + pullRight: Effect.Effect, R4> + ) => Effect.Effect>, never, R5> + ) => (self: Stream.Stream) => Stream.Stream, + ( + self: Stream.Stream, + that: Stream.Stream, + s: S, + f: ( + s: S, + pullLeft: Effect.Effect, R3>, + pullRight: Effect.Effect, R4> + ) => Effect.Effect>, never, R5> + ) => Stream.Stream +>(4, ( + self: Stream.Stream, + that: Stream.Stream, + s: S, + f: ( + s: S, + pullLeft: Effect.Effect, R3>, + pullRight: Effect.Effect, R4> + ) => Effect.Effect>, never, R5> +): Stream.Stream => { + function producer( + handoff: Handoff.Handoff>>, + latch: Handoff.Handoff + ): Channel.Channel { + return core.fromEffect(Handoff.take(latch)).pipe( + channel.zipRight(core.readWithCause({ + onInput: (input) => + core.flatMap( + core.fromEffect( + Handoff.offer>>( + handoff, + Exit.succeed(input) + ) + ), + () => producer(handoff, latch) + ), + onFailure: (cause) => + core.fromEffect( + Handoff.offer>>( + handoff, + Exit.failCause(pipe(cause, Cause.map(Option.some))) + ) + ), + onDone: () => + core.flatMap( + core.fromEffect( + Handoff.offer>>( + handoff, + Exit.fail(Option.none()) + ) + ), + () => producer(handoff, latch) + ) + })) + ) + } + return new StreamImpl( + channel.unwrapScopedWith((scope) => + Effect.all([ + Handoff.make>>(), + Handoff.make>>(), + Handoff.make(), + Handoff.make() + ]).pipe( + Effect.tap(([left, _, latchL]) => + toChannel(self).pipe( + channel.concatMap(channel.writeChunk), + core.pipeTo(producer(left, latchL)), + channelExecutor.runIn(scope), + Effect.forkIn(scope) + ) + ), + Effect.tap(([, right, _, rightL]) => + toChannel(that).pipe( + channel.concatMap(channel.writeChunk), + core.pipeTo(producer(right, rightL)), + channelExecutor.runIn(scope), + Effect.forkIn(scope) + ) + ), + Effect.map(([left, right, latchL, latchR]) => { + const pullLeft = Handoff.offer(latchL, void 0).pipe( + Effect.zipRight(Handoff.take(left).pipe(Effect.flatMap(identity))) + ) + const pullRight = Handoff.offer(latchR, void 0).pipe( + Effect.zipRight(Handoff.take(right).pipe(Effect.flatMap(identity))) + ) + return toChannel(unfoldEffect(s, (s) => Effect.flatMap(f(s, pullLeft, pullRight), unsome))) + }) + ) + ) + ) +}) + +/** @internal */ +export const combineChunks = dual< + ( + that: Stream.Stream, + s: S, + f: ( + s: S, + pullLeft: Effect.Effect, Option.Option, R3>, + pullRight: Effect.Effect, Option.Option, R4> + ) => Effect.Effect, S], Option.Option>, never, R5> + ) => (self: Stream.Stream) => Stream.Stream, + ( + self: Stream.Stream, + that: Stream.Stream, + s: S, + f: ( + s: S, + pullLeft: Effect.Effect, Option.Option, R3>, + pullRight: Effect.Effect, Option.Option, R4> + ) => Effect.Effect, S], Option.Option>, never, R5> + ) => Stream.Stream +>(4, ( + self: Stream.Stream, + that: Stream.Stream, + s: S, + f: ( + s: S, + pullLeft: Effect.Effect, Option.Option, R3>, + pullRight: Effect.Effect, Option.Option, R4> + ) => Effect.Effect, S], Option.Option>, never, R5> +): Stream.Stream => { + const producer = ( + handoff: Handoff.Handoff>, + latch: Handoff.Handoff + ): Channel.Channel, never, Err, unknown, unknown, R> => + channel.zipRight( + core.fromEffect(Handoff.take(latch)), + core.readWithCause({ + onInput: (input) => + core.flatMap( + core.fromEffect(pipe( + handoff, + Handoff.offer>(InternalTake.chunk(input)) + )), + () => producer(handoff, latch) + ), + onFailure: (cause) => + core.fromEffect( + Handoff.offer>( + handoff, + InternalTake.failCause(cause) + ) + ), + onDone: (): Channel.Channel, never, Err, unknown, unknown, R> => + core.fromEffect(Handoff.offer>(handoff, InternalTake.end)) + }) + ) + return new StreamImpl( + channel.unwrapScopedWith((scope) => + Effect.all([ + Handoff.make>(), + Handoff.make>(), + Handoff.make(), + Handoff.make() + ]).pipe( + Effect.tap(([left, _, latchL]) => + core.pipeTo(toChannel(self), producer(left, latchL)).pipe( + channelExecutor.runIn(scope), + Effect.forkIn(scope) + ) + ), + Effect.tap(([_, right, __, latchR]) => + core.pipeTo(toChannel(that), producer(right, latchR)).pipe( + channelExecutor.runIn(scope), + Effect.forkIn(scope) + ) + ), + Effect.map(([left, right, latchL, latchR]) => { + const pullLeft = Handoff.offer(latchL, void 0).pipe( + Effect.zipRight(Handoff.take(left).pipe(Effect.flatMap(InternalTake.done))) + ) + const pullRight = Handoff.offer(latchR, void 0).pipe( + Effect.zipRight(Handoff.take(right).pipe(Effect.flatMap(InternalTake.done))) + ) + return toChannel(unfoldChunkEffect(s, (s) => Effect.flatMap(f(s, pullLeft, pullRight), unsome))) + }) + ) + ) + ) +}) + +/** @internal */ +export const concat = dual< + ( + that: Stream.Stream + ) => (self: Stream.Stream) => Stream.Stream, + ( + self: Stream.Stream, + that: Stream.Stream + ) => Stream.Stream +>( + 2, + ( + self: Stream.Stream, + that: Stream.Stream + ): Stream.Stream => + new StreamImpl(pipe(toChannel(self), channel.zipRight(toChannel(that)))) +) + +/** @internal */ +export const concatAll = (streams: Chunk.Chunk>): Stream.Stream => + suspend(() => pipe(streams, Chunk.reduce(empty as Stream.Stream, (x, y) => concat(y)(x)))) + +/** @internal */ +export const cross: { + ( + right: Stream.Stream + ): (left: Stream.Stream) => Stream.Stream<[AL, AR], EL | ER, RL | RR> + ( + left: Stream.Stream, + right: Stream.Stream + ): Stream.Stream<[AL, AR], EL | ER, RL | RR> +} = dual( + 2, + ( + left: Stream.Stream, + right: Stream.Stream + ): Stream.Stream<[AL, AR], EL | ER, RL | RR> => pipe(left, crossWith(right, (a, a2) => [a, a2])) +) + +/** @internal */ +export const crossLeft: { + ( + right: Stream.Stream + ): (left: Stream.Stream) => Stream.Stream + ( + left: Stream.Stream, + right: Stream.Stream + ): Stream.Stream +} = dual( + 2, + ( + left: Stream.Stream, + right: Stream.Stream + ): Stream.Stream => pipe(left, crossWith(right, (a, _) => a)) +) + +/** @internal */ +export const crossRight: { + ( + right: Stream.Stream + ): (left: Stream.Stream) => Stream.Stream + ( + left: Stream.Stream, + right: Stream.Stream + ): Stream.Stream +} = dual( + 2, + ( + left: Stream.Stream, + right: Stream.Stream + ): Stream.Stream => flatMap(left, () => right) +) + +/** @internal */ +export const crossWith: { + ( + right: Stream.Stream, + f: (left: AL, right: AR) => A + ): (left: Stream.Stream) => Stream.Stream + ( + left: Stream.Stream, + right: Stream.Stream, + f: (left: AL, right: AR) => A + ): Stream.Stream +} = dual( + 3, + ( + left: Stream.Stream, + right: Stream.Stream, + f: (left: AL, right: AR) => A + ): Stream.Stream => pipe(left, flatMap((a) => pipe(right, map((b) => f(a, b))))) +) + +/** @internal */ +export const debounce = dual< + (duration: Duration.DurationInput) => (self: Stream.Stream) => Stream.Stream, + (self: Stream.Stream, duration: Duration.DurationInput) => Stream.Stream +>( + 2, + (self: Stream.Stream, duration: Duration.DurationInput): Stream.Stream => + unwrapScopedWith((scope) => + Effect.gen(function*() { + const handoff = yield* Handoff.make>() + + function enqueue(last: Chunk.Chunk): Effect.Effect< + Channel.Channel, unknown, E, unknown, unknown, unknown> + > { + return Clock.sleep(duration).pipe( + Effect.as(last), + Effect.forkIn(scope), + Effect.map((fiber) => consumer(DebounceState.previous(fiber))) + ) + } + + const producer: Channel.Channel, E, E, unknown, unknown> = core.readWithCause({ + onInput: (input: Chunk.Chunk) => + Option.match(Chunk.last(input), { + onNone: () => producer, + onSome: (elem) => + core.fromEffect(Handoff.offer(handoff, HandoffSignal.emit(Chunk.of(elem)))).pipe( + core.flatMap(() => producer) + ) + }), + onFailure: (cause) => + core.fromEffect( + Handoff.offer>(handoff, HandoffSignal.halt(cause)) + ), + onDone: () => + core.fromEffect( + Handoff.offer>( + handoff, + HandoffSignal.end(SinkEndReason.UpstreamEnd) + ) + ) + }) + + function consumer( + state: DebounceState.DebounceState + ): Channel.Channel, unknown, E, unknown, unknown, unknown> { + switch (state._tag) { + case DebounceState.OP_NOT_STARTED: { + return channel.unwrap( + Handoff.take(handoff).pipe( + Effect.map((signal) => { + switch (signal._tag) { + case HandoffSignal.OP_EMIT: { + return channel.unwrap(enqueue(signal.elements)) + } + case HandoffSignal.OP_HALT: { + return core.failCause(signal.cause) + } + case HandoffSignal.OP_END: { + return core.void + } + } + }) + ) + ) + } + case DebounceState.OP_PREVIOUS: { + return channel.unwrap( + Handoff.take(handoff).pipe( + Effect.forkIn(scope), + Effect.flatMap((handoffFiber) => + Effect.raceWith(Fiber.join(state.fiber), Fiber.join(handoffFiber), { + onSelfDone: (leftExit, current) => + Exit.match(leftExit, { + onFailure: (cause) => + Fiber.interrupt(current).pipe( + Effect.as(core.failCause(cause)) + ), + onSuccess: (chunk) => + Fiber.interrupt(current).pipe( + Effect.zipRight(Effect.succeed( + core.write(chunk).pipe( + core.flatMap(() => consumer(DebounceState.current(handoffFiber))) + ) + )) + ) + }), + onOtherDone: (rightExit, previous) => + Exit.match(rightExit, { + onFailure: (cause) => + Fiber.interrupt(previous).pipe( + Effect.as(core.failCause(cause)) + ), + onSuccess: (signal) => { + switch (signal._tag) { + case HandoffSignal.OP_EMIT: { + return Fiber.interrupt(previous).pipe( + Effect.zipRight(enqueue(signal.elements)) + ) + } + case HandoffSignal.OP_HALT: { + return Fiber.interrupt(previous).pipe( + Effect.as(core.failCause(signal.cause)) + ) + } + case HandoffSignal.OP_END: { + return Fiber.join(previous).pipe( + Effect.map((chunk) => + core.write(chunk).pipe( + channel.zipRight(core.void) + ) + ) + ) + } + } + } + }) + }) + ) + ) + ) + } + case DebounceState.OP_CURRENT: { + return channel.unwrap( + Fiber.join(state.fiber).pipe( + Effect.map((signal) => { + switch (signal._tag) { + case HandoffSignal.OP_EMIT: { + return channel.unwrap(enqueue(signal.elements)) + } + case HandoffSignal.OP_HALT: { + return core.failCause(signal.cause) + } + case HandoffSignal.OP_END: { + return core.void + } + } + }) + ) + ) + } + } + } + + return scopedWith((scope) => + core.pipeTo(toChannel(self), producer).pipe( + channelExecutor.runIn(scope), + Effect.forkIn(scope) + ) + ).pipe(crossRight(new StreamImpl(consumer(DebounceState.notStarted)))) + }) + ) +) + +/** @internal */ +export const die = (defect: unknown): Stream.Stream => fromEffect(Effect.die(defect)) + +/** @internal */ +export const dieSync = (evaluate: LazyArg): Stream.Stream => fromEffect(Effect.dieSync(evaluate)) + +/** @internal */ +export const dieMessage = (message: string): Stream.Stream => fromEffect(Effect.dieMessage(message)) + +/** @internal */ +export const distributedWith = dual< + ( + options: { + readonly size: N + readonly maximumLag: number + readonly decide: (a: A) => Effect.Effect> + } + ) => ( + self: Stream.Stream + ) => Effect.Effect< + Types.TupleOf>>>, + never, + Scope.Scope | R + >, + ( + self: Stream.Stream, + options: { + readonly size: N + readonly maximumLag: number + readonly decide: (a: A) => Effect.Effect> + } + ) => Effect.Effect< + Types.TupleOf>>>, + never, + Scope.Scope | R + > +>( + 2, + ( + self: Stream.Stream, + options: { + readonly size: N + readonly maximumLag: number + readonly decide: (a: A) => Effect.Effect> + } + ): Effect.Effect< + Types.TupleOf>>>, + never, + Scope.Scope | R + > => + pipe( + Deferred.make<(a: A) => Effect.Effect>>(), + Effect.flatMap((deferred) => + pipe( + self, + distributedWithDynamic({ + maximumLag: options.maximumLag, + decide: (a) => Effect.flatMap(Deferred.await(deferred), (f) => f(a)) + }), + Effect.flatMap((next) => + pipe( + Effect.all( + Chunk.map( + Chunk.range(0, options.size - 1), + (id) => Effect.map(next, ([key, queue]) => [[key, id], queue] as const) + ) + ), + Effect.map(Chunk.unsafeFromArray), + Effect.flatMap((entries) => { + const [mappings, queues] = Chunk.reduceRight( + entries, + [ + new Map(), + Chunk.empty>>>() + ] as const, + ([mappings, queues], [mapping, queue]) => + [ + mappings.set(mapping[0], mapping[1]), + pipe(queues, Chunk.prepend(queue)) + ] as const + ) + return pipe( + Deferred.succeed(deferred, (a: A) => + Effect.map(options.decide(a), (f) => (key: number) => f(mappings.get(key)!))), + Effect.as( + Array.from(queues) as Types.TupleOf>>> + ) + ) + }) + ) + ) + ) + ) + ) +) + +/** @internal */ +const distributedWithDynamicId = { ref: 0 } + +const newDistributedWithDynamicId = () => { + const current = distributedWithDynamicId.ref + distributedWithDynamicId.ref = current + 1 + return current +} + +/** @internal */ +export const distributedWithDynamic = dual< + ( + options: { + readonly maximumLag: number + readonly decide: (a: A) => Effect.Effect> + } + ) => ( + self: Stream.Stream + ) => Effect.Effect< + Effect.Effect<[number, Queue.Dequeue>>]>, + never, + Scope.Scope | R + >, + ( + self: Stream.Stream, + options: { + readonly maximumLag: number + readonly decide: (a: A) => Effect.Effect> + } + ) => Effect.Effect< + Effect.Effect<[number, Queue.Dequeue>>]>, + never, + Scope.Scope | R + > +>(2, ( + self: Stream.Stream, + options: { + readonly maximumLag: number + readonly decide: (a: A) => Effect.Effect> + } +): Effect.Effect< + Effect.Effect<[number, Queue.Dequeue>>]>, + never, + Scope.Scope | R +> => distributedWithDynamicCallback(self, options.maximumLag, options.decide, () => Effect.void)) + +/** @internal */ +export const distributedWithDynamicCallback = dual< + ( + maximumLag: number, + decide: (a: A) => Effect.Effect>, + done: (exit: Exit.Exit>) => Effect.Effect + ) => ( + self: Stream.Stream + ) => Effect.Effect< + Effect.Effect<[number, Queue.Dequeue>>]>, + never, + Scope.Scope | R + >, + ( + self: Stream.Stream, + maximumLag: number, + decide: (a: A) => Effect.Effect>, + done: (exit: Exit.Exit>) => Effect.Effect + ) => Effect.Effect< + Effect.Effect<[number, Queue.Dequeue>>]>, + never, + Scope.Scope | R + > +>(4, ( + self: Stream.Stream, + maximumLag: number, + decide: (a: A) => Effect.Effect>, + done: (exit: Exit.Exit>) => Effect.Effect +): Effect.Effect< + Effect.Effect<[number, Queue.Dequeue>>]>, + never, + Scope.Scope | R +> => + pipe( + Effect.acquireRelease( + Ref.make>>>>(new Map()), + (ref, _) => pipe(Ref.get(ref), Effect.flatMap((queues) => pipe(queues.values(), Effect.forEach(Queue.shutdown)))) + ), + Effect.flatMap((queuesRef) => + Effect.gen(function*() { + const offer = (a: A): Effect.Effect => + pipe( + decide(a), + Effect.flatMap((shouldProcess) => + pipe( + Ref.get(queuesRef), + Effect.flatMap((queues) => + pipe( + queues.entries(), + Effect.reduce(Chunk.empty(), (acc, [id, queue]) => { + if (shouldProcess(id)) { + return pipe( + Queue.offer(queue, Exit.succeed(a)), + Effect.matchCauseEffect({ + onFailure: (cause) => + // Ignore all downstream queues that were shut + // down and remove them later + Cause.isInterrupted(cause) ? + Effect.succeed(pipe(acc, Chunk.prepend(id))) : + Effect.failCause(cause), + onSuccess: () => Effect.succeed(acc) + }) + ) + } + return Effect.succeed(acc) + }), + Effect.flatMap((ids) => { + if (Chunk.isNonEmpty(ids)) { + return Ref.update(queuesRef, (map) => { + for (const id of ids) { + map.delete(id) + } + return map + }) + } + return Effect.void + }) + ) + ) + ) + ), + Effect.asVoid + ) + const queuesLock = yield* Effect.makeSemaphore(1) + const newQueue = yield* Ref.make>>]>>( + pipe( + Queue.bounded>>(maximumLag), + Effect.flatMap((queue) => { + const id = newDistributedWithDynamicId() + return pipe( + Ref.update(queuesRef, (map) => map.set(id, queue)), + Effect.as([id, queue]) + ) + }) + ) + ) + const finalize = (endTake: Exit.Exit>): Effect.Effect => + // Make sure that no queues are currently being added + queuesLock.withPermits(1)( + pipe( + Ref.set( + newQueue, + pipe( + // All newly created queues should end immediately + Queue.bounded>>(1), + Effect.tap((queue) => Queue.offer(queue, endTake)), + Effect.flatMap((queue) => { + const id = newDistributedWithDynamicId() + return pipe( + Ref.update(queuesRef, (map) => map.set(id, queue)), + Effect.as(Tuple.make(id, queue)) + ) + }) + ) + ), + Effect.zipRight( + pipe( + Ref.get(queuesRef), + Effect.flatMap((map) => + pipe( + Chunk.fromIterable(map.values()), + Effect.forEach((queue) => + pipe( + Queue.offer(queue, endTake), + Effect.catchSomeCause((cause) => + Cause.isInterrupted(cause) ? Option.some(Effect.void) : Option.none() + ) + ) + ) + ) + ) + ) + ), + Effect.zipRight(done(endTake)), + Effect.asVoid + ) + ) + yield* pipe( + self, + runForEachScoped(offer), + Effect.matchCauseEffect({ + onFailure: (cause) => finalize(Exit.failCause(pipe(cause, Cause.map(Option.some)))), + onSuccess: () => finalize(Exit.fail(Option.none())) + }), + Effect.forkScoped + ) + return queuesLock.withPermits(1)( + Effect.flatten(Ref.get(newQueue)) + ) + }) + ) + )) + +/** @internal */ +export const drain = (self: Stream.Stream): Stream.Stream => + new StreamImpl(channel.drain(toChannel(self))) + +/** @internal */ +export const drainFork = dual< + ( + that: Stream.Stream + ) => (self: Stream.Stream) => Stream.Stream, + ( + self: Stream.Stream, + that: Stream.Stream + ) => Stream.Stream +>( + 2, + ( + self: Stream.Stream, + that: Stream.Stream + ): Stream.Stream => + fromEffect(Deferred.make()).pipe(flatMap((backgroundDied) => + scopedWith((scope) => + toChannel(that).pipe( + channel.drain, + channelExecutor.runIn(scope), + Effect.catchAllCause((cause) => Deferred.failCause(backgroundDied, cause)), + Effect.forkIn(scope) + ) + ).pipe(crossRight(interruptWhenDeferred(self, backgroundDied))) + )) +) + +/** @internal */ +export const drop = dual< + (n: number) => (self: Stream.Stream) => Stream.Stream, + (self: Stream.Stream, n: number) => Stream.Stream +>(2, (self: Stream.Stream, n: number): Stream.Stream => { + const loop = (r: number): Channel.Channel, Chunk.Chunk, never, never, unknown, unknown> => + core.readWith({ + onInput: (input: Chunk.Chunk) => { + const dropped = pipe(input, Chunk.drop(r)) + const leftover = Math.max(0, r - input.length) + const more = Chunk.isEmpty(input) || leftover > 0 + if (more) { + return loop(leftover) + } + return pipe( + core.write(dropped), + channel.zipRight(channel.identityChannel, never, unknown>()) + ) + }, + onFailure: core.fail, + onDone: () => core.void + }) + return new StreamImpl(pipe(toChannel(self), channel.pipeToOrFail(loop(n)))) +}) + +/** @internal */ +export const dropRight = dual< + (n: number) => (self: Stream.Stream) => Stream.Stream, + (self: Stream.Stream, n: number) => Stream.Stream +>(2, (self: Stream.Stream, n: number): Stream.Stream => { + if (n <= 0) { + return identityStream() + } + return suspend(() => { + const queue = new RingBuffer(n) + const reader: Channel.Channel, Chunk.Chunk, E, E, void, unknown> = core.readWith({ + onInput: (input: Chunk.Chunk) => { + const outputs = pipe( + input, + Chunk.filterMap((elem) => { + const head = queue.head() + queue.put(elem) + return head + }) + ) + return pipe(core.write(outputs), core.flatMap(() => reader)) + }, + onFailure: core.fail, + onDone: () => core.void + }) + return new StreamImpl(pipe(toChannel(self), channel.pipeToOrFail(reader))) + }) +}) + +/** @internal */ +export const dropUntil = dual< + (predicate: Predicate>) => (self: Stream.Stream) => Stream.Stream, + (self: Stream.Stream, predicate: Predicate) => Stream.Stream +>( + 2, + (self: Stream.Stream, predicate: Predicate): Stream.Stream => + drop(dropWhile(self, (a) => !predicate(a)), 1) +) + +/** @internal */ +export const dropUntilEffect = dual< + ( + predicate: (a: Types.NoInfer) => Effect.Effect + ) => (self: Stream.Stream) => Stream.Stream, + ( + self: Stream.Stream, + predicate: (a: Types.NoInfer) => Effect.Effect + ) => Stream.Stream +>( + 2, + ( + self: Stream.Stream, + predicate: (a: Types.NoInfer) => Effect.Effect + ): Stream.Stream => { + const loop: Channel.Channel, Chunk.Chunk, E | E2, E, unknown, unknown, R2> = core.readWith({ + onInput: (input: Chunk.Chunk) => + pipe( + Effect.dropUntil(input, predicate), + Effect.map(Chunk.unsafeFromArray), + Effect.map((leftover) => { + const more = Chunk.isEmpty(leftover) + if (more) { + return core.suspend(() => loop) + } + return pipe( + core.write(leftover), + channel.zipRight(channel.identityChannel, E | E2, unknown>()) + ) + }), + channel.unwrap + ), + onFailure: core.fail, + onDone: () => core.void + }) + return new StreamImpl(pipe(toChannel(self), channel.pipeToOrFail(loop))) + } +) + +/** @internal */ +export const dropWhile = dual< + (predicate: Predicate>) => (self: Stream.Stream) => Stream.Stream, + (self: Stream.Stream, predicate: Predicate) => Stream.Stream +>(2, (self: Stream.Stream, predicate: Predicate): Stream.Stream => { + const loop: Channel.Channel, Chunk.Chunk, never, never, unknown, unknown> = core.readWith({ + onInput: (input: Chunk.Chunk) => { + const output = Chunk.dropWhile(input, predicate) + if (Chunk.isEmpty(output)) { + return core.suspend(() => loop) + } + return channel.zipRight( + core.write(output), + channel.identityChannel, never, unknown>() + ) + }, + onFailure: core.fail, + onDone: core.succeedNow + }) + return new StreamImpl(channel.pipeToOrFail(toChannel(self), loop)) +}) + +/** @internal */ +export const dropWhileEffect = dual< + ( + predicate: (a: Types.NoInfer) => Effect.Effect + ) => (self: Stream.Stream) => Stream.Stream, + ( + self: Stream.Stream, + predicate: (a: A) => Effect.Effect + ) => Stream.Stream +>( + 2, + ( + self: Stream.Stream, + predicate: (a: A) => Effect.Effect + ): Stream.Stream => { + const loop: Channel.Channel, Chunk.Chunk, E | E2, E, unknown, unknown, R2> = core.readWith({ + onInput: (input: Chunk.Chunk) => + pipe( + Effect.dropWhile(input, predicate), + Effect.map(Chunk.unsafeFromArray), + Effect.map((leftover) => { + const more = Chunk.isEmpty(leftover) + if (more) { + return core.suspend(() => loop) + } + return channel.zipRight( + core.write(leftover), + channel.identityChannel, E | E2, unknown>() + ) + }), + channel.unwrap + ), + onFailure: core.fail, + onDone: () => core.void + }) + return new StreamImpl(channel.pipeToOrFail( + toChannel(self), + loop + )) + } +) + +/** @internal */ +export const either = (self: Stream.Stream): Stream.Stream, never, R> => + pipe(self, map(Either.right), catchAll((error) => make(Either.left(error)))) + +/** @internal */ +export const empty: Stream.Stream = new StreamImpl(core.void) + +/** @internal */ +export const ensuring = dual< + ( + finalizer: Effect.Effect + ) => (self: Stream.Stream) => Stream.Stream, + (self: Stream.Stream, finalizer: Effect.Effect) => Stream.Stream +>( + 2, + (self: Stream.Stream, finalizer: Effect.Effect): Stream.Stream => + new StreamImpl(pipe(toChannel(self), channel.ensuring(finalizer))) +) + +/** @internal */ +export const ensuringWith = dual< + ( + finalizer: (exit: Exit.Exit) => Effect.Effect + ) => (self: Stream.Stream) => Stream.Stream, + ( + self: Stream.Stream, + finalizer: (exit: Exit.Exit) => Effect.Effect + ) => Stream.Stream +>(2, (self, finalizer) => new StreamImpl(core.ensuringWith(toChannel(self), finalizer))) + +/** @internal */ +export const context = (): Stream.Stream, never, R> => fromEffect(Effect.context()) + +/** @internal */ +export const contextWith = (f: (env: Context.Context) => A): Stream.Stream => + pipe(context(), map(f)) + +/** @internal */ +export const contextWithEffect = ( + f: (env: Context.Context) => Effect.Effect +): Stream.Stream => pipe(context(), mapEffectSequential(f)) + +/** @internal */ +export const contextWithStream = ( + f: (env: Context.Context) => Stream.Stream +): Stream.Stream => pipe(context(), flatMap(f)) + +/** @internal */ +export const execute = (effect: Effect.Effect): Stream.Stream => + drain(fromEffect(effect)) + +/** @internal */ +export const fail = (error: E): Stream.Stream => fromEffectOption(Effect.fail(Option.some(error))) + +/** @internal */ +export const failSync = (evaluate: LazyArg): Stream.Stream => + fromEffectOption(Effect.failSync(() => Option.some(evaluate()))) + +/** @internal */ +export const failCause = (cause: Cause.Cause): Stream.Stream => fromEffect(Effect.failCause(cause)) + +/** @internal */ +export const failCauseSync = (evaluate: LazyArg>): Stream.Stream => + fromEffect(Effect.failCauseSync(evaluate)) + +/** @internal */ +export const filter: { + ( + refinement: Refinement, B> + ): (self: Stream.Stream) => Stream.Stream + (predicate: Predicate): (self: Stream.Stream) => Stream.Stream + (self: Stream.Stream, refinement: Refinement): Stream.Stream + (self: Stream.Stream, predicate: Predicate): Stream.Stream +} = dual( + 2, + (self: Stream.Stream, predicate: Predicate) => mapChunks(self, Chunk.filter(predicate)) +) + +/** @internal */ +export const filterEffect = dual< + ( + f: (a: Types.NoInfer) => Effect.Effect + ) => (self: Stream.Stream) => Stream.Stream, + ( + self: Stream.Stream, + f: (a: A) => Effect.Effect + ) => Stream.Stream +>( + 2, + ( + self: Stream.Stream, + f: (a: A) => Effect.Effect + ): Stream.Stream => { + const loop = ( + iterator: Iterator + ): Channel.Channel, Chunk.Chunk, E | E2, E, unknown, unknown, R2> => { + const next = iterator.next() + if (next.done) { + return core.readWithCause({ + onInput: (input) => loop(input[Symbol.iterator]()), + onFailure: core.failCause, + onDone: core.succeed + }) + } else { + return pipe( + f(next.value), + Effect.map((bool) => + bool ? + pipe(core.write(Chunk.of(next.value)), core.flatMap(() => loop(iterator))) : + loop(iterator) + ), + channel.unwrap + ) + } + } + return new StreamImpl( + core.suspend(() => pipe(toChannel(self), core.pipeTo(loop(Chunk.empty()[Symbol.iterator]())))) + ) + } +) + +/** @internal */ +export const filterMap = dual< + (pf: (a: A) => Option.Option) => (self: Stream.Stream) => Stream.Stream, + (self: Stream.Stream, pf: (a: A) => Option.Option) => Stream.Stream +>( + 2, + (self: Stream.Stream, pf: (a: A) => Option.Option): Stream.Stream => + mapChunks(self, Chunk.filterMap(pf)) +) + +/** @internal */ +export const filterMapEffect = dual< + ( + pf: (a: A) => Option.Option> + ) => (self: Stream.Stream) => Stream.Stream, + ( + self: Stream.Stream, + pf: (a: A) => Option.Option> + ) => Stream.Stream +>( + 2, + ( + self: Stream.Stream, + pf: (a: A) => Option.Option> + ): Stream.Stream => + suspend(() => { + const loop = ( + iterator: Iterator + ): Channel.Channel, Chunk.Chunk, E | E2, E, unknown, unknown, R | R2> => { + const next = iterator.next() + if (next.done) { + return core.readWithCause({ + onInput: (input) => loop(input[Symbol.iterator]()), + onFailure: core.failCause, + onDone: core.succeed + }) + } else { + return pipe( + pf(next.value), + Option.match({ + onNone: () => Effect.sync(() => loop(iterator)), + onSome: Effect.map((a2) => core.flatMap(core.write(Chunk.of(a2)), () => loop(iterator))) + }), + channel.unwrap + ) + } + } + return new StreamImpl(pipe(toChannel(self), core.pipeTo(loop(Chunk.empty()[Symbol.iterator]())))) + }) +) + +/** @internal */ +export const filterMapWhile = dual< + ( + pf: (a: A) => Option.Option + ) => (self: Stream.Stream) => Stream.Stream, + (self: Stream.Stream, pf: (a: A) => Option.Option) => Stream.Stream +>( + 2, + (self: Stream.Stream, pf: (a: A) => Option.Option) => { + const loop: Channel.Channel, Chunk.Chunk, E, E, unknown, unknown> = core.readWith({ + onInput: (input: Chunk.Chunk) => { + const mapped = Chunk.filterMapWhile(input, pf) + if (mapped.length === input.length) { + return pipe(core.write(mapped), core.flatMap(() => loop)) + } + return core.write(mapped) + }, + onFailure: core.fail, + onDone: core.succeed + }) + return new StreamImpl(pipe(toChannel(self), channel.pipeToOrFail(loop))) + } +) + +/** @internal */ +export const filterMapWhileEffect = dual< + ( + pf: (a: A) => Option.Option> + ) => (self: Stream.Stream) => Stream.Stream, + ( + self: Stream.Stream, + pf: (a: A) => Option.Option> + ) => Stream.Stream +>( + 2, + ( + self: Stream.Stream, + pf: (a: A) => Option.Option> + ): Stream.Stream => + suspend(() => { + const loop = ( + iterator: Iterator + ): Channel.Channel, Chunk.Chunk, E | E2, E, unknown, unknown, R | R2> => { + const next = iterator.next() + if (next.done) { + return core.readWithCause({ + onInput: (input) => loop(input[Symbol.iterator]()), + onFailure: core.failCause, + onDone: core.succeed + }) + } else { + return channel.unwrap( + Option.match(pf(next.value), { + onNone: () => Effect.succeed(core.void), + onSome: Effect.map( + (a2) => core.flatMap(core.write(Chunk.of(a2)), () => loop(iterator)) + ) + }) + ) + } + } + return new StreamImpl(pipe(toChannel(self), channel.pipeToOrFail(loop(Chunk.empty()[Symbol.iterator]())))) + }) +) + +/** @internal */ +export const finalizer = (finalizer: Effect.Effect): Stream.Stream => + acquireRelease(Effect.void, () => finalizer) + +/** @internal */ +export const find: { + ( + refinement: Refinement, B> + ): (self: Stream.Stream) => Stream.Stream + (predicate: Predicate>): (self: Stream.Stream) => Stream.Stream + (self: Stream.Stream, refinement: Refinement): Stream.Stream + (self: Stream.Stream, predicate: Predicate): Stream.Stream +} = dual(2, (self: Stream.Stream, predicate: Predicate): Stream.Stream => { + const loop: Channel.Channel, Chunk.Chunk, E, E, unknown, unknown, R> = core.readWith({ + onInput: (input: Chunk.Chunk) => + Option.match(Chunk.findFirst(input, predicate), { + onNone: () => loop, + onSome: (n) => core.write(Chunk.of(n)) + }), + onFailure: core.fail, + onDone: () => core.void + }) + return new StreamImpl(pipe(toChannel(self), core.pipeTo(loop))) +}) + +/** @internal */ +export const findEffect: { + ( + predicate: (a: Types.NoInfer) => Effect.Effect + ): (self: Stream.Stream) => Stream.Stream + ( + self: Stream.Stream, + predicate: (a: Types.NoInfer) => Effect.Effect + ): Stream.Stream +} = dual( + 2, + ( + self: Stream.Stream, + predicate: (a: Types.NoInfer) => Effect.Effect + ): Stream.Stream => { + const loop: Channel.Channel, Chunk.Chunk, E | E2, E, unknown, unknown, R2> = core.readWith({ + onInput: (input: Chunk.Chunk) => + pipe( + Effect.findFirst(input, predicate), + Effect.map(Option.match({ + onNone: () => loop, + onSome: (n) => core.write(Chunk.of(n)) + })), + channel.unwrap + ), + onFailure: core.fail, + onDone: () => core.void + }) + return new StreamImpl(pipe(toChannel(self), core.pipeTo(loop))) + } +) + +/** @internal */ +export const flatMap = dual< + ( + f: (a: A) => Stream.Stream, + options?: { + readonly concurrency?: number | "unbounded" | undefined + readonly bufferSize?: number | undefined + readonly switch?: boolean | undefined + } + ) => (self: Stream.Stream) => Stream.Stream, + ( + self: Stream.Stream, + f: (a: A) => Stream.Stream, + options?: { + readonly concurrency?: number | "unbounded" | undefined + readonly bufferSize?: number | undefined + readonly switch?: boolean | undefined + } + ) => Stream.Stream +>( + (args) => isStream(args[0]), + ( + self: Stream.Stream, + f: (a: A) => Stream.Stream, + options?: { + readonly concurrency?: number | "unbounded" | undefined + readonly bufferSize?: number | undefined + readonly switch?: boolean | undefined + } + ): Stream.Stream => { + const bufferSize = options?.bufferSize ?? 16 + + if (options?.switch) { + return matchConcurrency( + options?.concurrency, + () => flatMapParSwitchBuffer(self, 1, bufferSize, f), + (n) => flatMapParSwitchBuffer(self, n, bufferSize, f) + ) + } + + return matchConcurrency( + options?.concurrency, + () => + new StreamImpl( + channel.concatMap( + toChannel(self), + (as) => + pipe( + as, + Chunk.map((a) => toChannel(f(a))), + Chunk.reduce( + core.void as Channel.Channel, unknown, E2, unknown, unknown, unknown, R2>, + (left, right) => pipe(left, channel.zipRight(right)) + ) + ) + ) + ), + (_) => + new StreamImpl( + pipe( + toChannel(self), + channel.concatMap(channel.writeChunk), + channel.mergeMap((out) => toChannel(f(out)), options as any) + ) + ) + ) + } +) + +/** @internal */ +export const matchConcurrency = ( + concurrency: number | "unbounded" | undefined, + sequential: () => A, + bounded: (n: number) => A +) => { + switch (concurrency) { + case undefined: + return sequential() + case "unbounded": + return bounded(Number.MAX_SAFE_INTEGER) + default: + return concurrency > 1 ? bounded(concurrency) : sequential() + } +} + +const flatMapParSwitchBuffer = dual< + ( + n: number, + bufferSize: number, + f: (a: A) => Stream.Stream + ) => (self: Stream.Stream) => Stream.Stream, + ( + self: Stream.Stream, + n: number, + bufferSize: number, + f: (a: A) => Stream.Stream + ) => Stream.Stream +>( + 4, + ( + self: Stream.Stream, + n: number, + bufferSize: number, + f: (a: A) => Stream.Stream + ): Stream.Stream => + new StreamImpl( + pipe( + toChannel(self), + channel.concatMap(channel.writeChunk), + channel.mergeMap((out) => toChannel(f(out)), { + concurrency: n, + mergeStrategy: MergeStrategy.BufferSliding(), + bufferSize + }) + ) + ) +) + +/** @internal */ +export const flatten = dual< + (options?: { + readonly concurrency?: number | "unbounded" | undefined + readonly bufferSize?: number | undefined + }) => ( + self: Stream.Stream, E, R> + ) => Stream.Stream, + ( + self: Stream.Stream, E, R>, + options?: { + readonly concurrency?: number | "unbounded" | undefined + readonly bufferSize?: number | undefined + } + ) => Stream.Stream +>((args) => isStream(args[0]), (self, options) => flatMap(self, identity, options)) + +/** @internal */ +export const flattenChunks = (self: Stream.Stream, E, R>): Stream.Stream => { + const flatten: Channel.Channel, Chunk.Chunk>, E, E, unknown, unknown> = core + .readWithCause({ + onInput: (chunks: Chunk.Chunk>) => + core.flatMap( + channel.writeChunk(chunks), + () => flatten + ), + onFailure: core.failCause, + onDone: () => core.void + }) + return new StreamImpl(pipe(toChannel(self), core.pipeTo(flatten))) +} + +/** @internal */ +export const flattenEffect = dual< + ( + options?: { + readonly concurrency?: number | "unbounded" | undefined + readonly unordered?: boolean | undefined + } + ) => ( + self: Stream.Stream, E, R> + ) => Stream.Stream, + ( + self: Stream.Stream, E, R>, + options?: { + readonly concurrency?: number | "unbounded" | undefined + readonly unordered?: boolean | undefined + } + ) => Stream.Stream +>( + (args) => isStream(args[0]), + (self, options) => + options?.unordered ? + flatMap(self, (a) => fromEffect(a), { concurrency: options.concurrency }) : + matchConcurrency( + options?.concurrency, + () => mapEffectSequential(self, identity), + (n) => + new StreamImpl( + pipe( + toChannel(self), + channel.concatMap(channel.writeChunk), + channel.mapOutEffectPar(identity, n), + channel.mapOut(Chunk.of) + ) + ) + ) +) + +/** @internal */ +export const flattenExitOption = ( + self: Stream.Stream>, E, R> +): Stream.Stream => { + const processChunk = ( + chunk: Chunk.Chunk>>, + cont: Channel.Channel, Chunk.Chunk>>, E | E2, E, unknown, unknown, R> + ) => { + const [toEmit, rest] = pipe(chunk, Chunk.splitWhere((exit) => !Exit.isSuccess(exit))) + const next = pipe( + Chunk.head(rest), + Option.match({ + onNone: () => cont, + onSome: Exit.match({ + onFailure: (cause) => + Option.match(Cause.flipCauseOption(cause), { + onNone: () => core.void, + onSome: core.failCause + }), + onSuccess: () => core.void + }) + }) + ) + return pipe( + core.write(pipe( + toEmit, + Chunk.filterMap((exit) => + Exit.isSuccess(exit) ? + Option.some(exit.value) : + Option.none() + ) + )), + core.flatMap(() => next) + ) + } + const process: Channel.Channel< + Chunk.Chunk, + Chunk.Chunk>>, + E | E2, + E, + unknown, + unknown, + R + > = core.readWithCause({ + onInput: (chunk: Chunk.Chunk>>) => processChunk(chunk, process), + onFailure: (cause) => core.failCause(cause), + onDone: () => core.void + }) + return new StreamImpl(pipe(toChannel(self), core.pipeTo(process))) +} + +/** @internal */ +export const flattenIterables = (self: Stream.Stream, E, R>): Stream.Stream => + pipe(self, map(Chunk.fromIterable), flattenChunks) + +/** @internal */ +export const flattenTake = (self: Stream.Stream, E, R>): Stream.Stream => + flattenChunks(flattenExitOption(pipe(self, map((take) => take.exit)))) + +/** @internal */ +export const forever = (self: Stream.Stream): Stream.Stream => + new StreamImpl(channel.repeated(toChannel(self))) + +/** @internal */ +export const fromAsyncIterable = ( + iterable: AsyncIterable, + onError: (e: unknown) => E +) => + pipe( + Effect.acquireRelease( + Effect.sync(() => iterable[Symbol.asyncIterator]()), + (iterator) => iterator.return ? Effect.promise(async () => iterator.return!()) : Effect.void + ), + Effect.map((iterator) => + repeatEffectOption(pipe( + Effect.tryPromise({ + try: async () => iterator.next(), + catch: (reason) => Option.some(onError(reason)) + }), + Effect.flatMap((result) => result.done ? Effect.fail(Option.none()) : Effect.succeed(result.value)) + )) + ), + unwrapScoped + ) + +/** @internal */ +export const fromChannel = ( + channel: Channel.Channel, unknown, E, unknown, unknown, unknown, R> +): Stream.Stream => new StreamImpl(channel) + +/** @internal */ +export const toChannel = ( + stream: Stream.Stream +): Channel.Channel, unknown, E, unknown, unknown, unknown, R> => { + if ("channel" in stream) { + return (stream as StreamImpl).channel + } else if (Effect.isEffect(stream)) { + return toChannel(fromEffect(stream)) as any + } else { + throw new TypeError(`Expected a Stream.`) + } +} + +/** @internal */ +export const fromChunk = (chunk: Chunk.Chunk): Stream.Stream => + new StreamImpl(Chunk.isEmpty(chunk) ? core.void : core.write(chunk)) + +/** @internal */ +export const fromChunkPubSub: { + (pubsub: PubSub.PubSub>, options: { + readonly scoped: true + readonly shutdown?: boolean | undefined + }): Effect.Effect, never, Scope.Scope> + (pubsub: PubSub.PubSub>, options?: { + readonly scoped?: false | undefined + readonly shutdown?: boolean | undefined + }): Stream.Stream +} = (pubsub, options): any => { + if (options?.scoped) { + const effect = Effect.map(PubSub.subscribe(pubsub), fromChunkQueue) + return options.shutdown ? Effect.map(effect, ensuring(PubSub.shutdown(pubsub))) : effect + } + const stream = flatMap(scoped(PubSub.subscribe(pubsub)), fromChunkQueue) + return options?.shutdown ? ensuring(stream, PubSub.shutdown(pubsub)) : stream +} + +/** @internal */ +export const fromChunkQueue = (queue: Queue.Dequeue>, options?: { + readonly shutdown?: boolean | undefined +}): Stream.Stream => + pipe( + Queue.take(queue), + Effect.catchAllCause((cause) => + pipe( + Queue.isShutdown(queue), + Effect.flatMap((isShutdown) => + isShutdown && Cause.isInterrupted(cause) ? + pull.end() : + pull.failCause(cause) + ) + ) + ), + repeatEffectChunkOption, + options?.shutdown ? ensuring(Queue.shutdown(queue)) : identity + ) + +/** @internal */ +export const fromChunks = ( + ...chunks: Array> +): Stream.Stream => pipe(fromIterable(chunks), flatMap(fromChunk)) + +/** @internal */ +export const fromEffect = (effect: Effect.Effect): Stream.Stream => + pipe(effect, Effect.mapError(Option.some), fromEffectOption) + +/** @internal */ +export const fromEffectOption = (effect: Effect.Effect, R>): Stream.Stream => + new StreamImpl( + channel.unwrap( + Effect.match(effect, { + onFailure: Option.match({ + onNone: () => core.void, + onSome: core.fail + }), + onSuccess: (a) => core.write(Chunk.of(a)) + }) + ) + ) + +/** @internal */ +export const fromPubSub: { + (pubsub: PubSub.PubSub, options: { + readonly scoped: true + readonly maxChunkSize?: number | undefined + readonly shutdown?: boolean | undefined + }): Effect.Effect, never, Scope.Scope> + (pubsub: PubSub.PubSub, options?: { + readonly scoped?: false | undefined + readonly maxChunkSize?: number | undefined + readonly shutdown?: boolean | undefined + }): Stream.Stream +} = (pubsub, options): any => { + const maxChunkSize = options?.maxChunkSize ?? DefaultChunkSize + + if (options?.scoped) { + const effect = Effect.map( + PubSub.subscribe(pubsub), + (queue) => fromQueue(queue, { maxChunkSize, shutdown: true }) + ) + + return options.shutdown ? Effect.map(effect, ensuring(PubSub.shutdown(pubsub))) : effect + } + const stream = flatMap( + scoped(PubSub.subscribe(pubsub)), + (queue) => fromQueue(queue, { maxChunkSize }) + ) + return options?.shutdown ? ensuring(stream, PubSub.shutdown(pubsub)) : stream +} + +/** @internal */ +export const fromTPubSub = (pubsub: TPubSub.TPubSub): Stream.Stream => { + return unwrapScoped(Effect.map( + TPubSub.subscribeScoped(pubsub), + (queue) => fromTQueue(queue) + )) +} + +/** @internal */ +export const fromIterable = (iterable: Iterable): Stream.Stream => + suspend(() => + Chunk.isChunk(iterable) ? + fromChunk(iterable) : + fromIteratorSucceed(iterable[Symbol.iterator]()) + ) + +/** @internal */ +export const fromIterableEffect = ( + effect: Effect.Effect, E, R> +): Stream.Stream => pipe(effect, Effect.map(fromIterable), unwrap) + +/** @internal */ +export const fromIteratorSucceed = ( + iterator: Iterator, + maxChunkSize = DefaultChunkSize +): Stream.Stream => { + return pipe( + Effect.sync(() => { + let builder: Array = [] + const loop = ( + iterator: Iterator + ): Channel.Channel, unknown, never, unknown, unknown, unknown> => + pipe( + Effect.sync(() => { + let next: IteratorResult = iterator.next() + if (maxChunkSize === 1) { + if (next.done) { + return core.void + } + return pipe( + core.write(Chunk.of(next.value)), + core.flatMap(() => loop(iterator)) + ) + } + builder = [] + let count = 0 + while (next.done === false) { + builder.push(next.value) + count = count + 1 + if (count >= maxChunkSize) { + break + } + next = iterator.next() + } + if (count > 0) { + return pipe( + core.write(Chunk.unsafeFromArray(builder)), + core.flatMap(() => loop(iterator)) + ) + } + return core.void + }), + channel.unwrap + ) + return new StreamImpl(loop(iterator)) + }), + unwrap + ) +} + +/** @internal */ +export const fromPull = ( + effect: Effect.Effect, Option.Option, R2>, never, R | Scope.Scope> +): Stream.Stream | R2> => pipe(effect, Effect.map(repeatEffectChunkOption), unwrapScoped) + +/** @internal */ +export const fromQueue = ( + queue: Queue.Dequeue, + options?: { + readonly maxChunkSize?: number | undefined + readonly shutdown?: boolean | undefined + } +): Stream.Stream => + pipe( + Queue.takeBetween(queue, 1, options?.maxChunkSize ?? DefaultChunkSize), + Effect.catchAllCause((cause) => + pipe( + Queue.isShutdown(queue), + Effect.flatMap((isShutdown) => + isShutdown && Cause.isInterrupted(cause) ? + pull.end() : + pull.failCause(cause) + ) + ) + ), + repeatEffectChunkOption, + options?.shutdown ? ensuring(Queue.shutdown(queue)) : identity + ) + +/** @internal */ +export const fromTQueue = (queue: TQueue.TDequeue): Stream.Stream => + pipe( + TQueue.take(queue), + Effect.map(Chunk.of), + Effect.catchAllCause((cause) => + pipe( + TQueue.isShutdown(queue), + Effect.flatMap((isShutdown) => + isShutdown && Cause.isInterrupted(cause) ? + pull.end() : + pull.failCause(cause) + ) + ) + ), + repeatEffectChunkOption + ) + +/** @internal */ +export const fromSchedule = (schedule: Schedule.Schedule): Stream.Stream => + pipe( + Schedule.driver(schedule), + Effect.map((driver) => repeatEffectOption(driver.next(void 0))), + unwrap + ) + +/** @internal */ +export const fromReadableStream: { + ( + options: { + readonly evaluate: LazyArg> + readonly onError: (error: unknown) => E + readonly releaseLockOnEnd?: boolean | undefined + } + ): Stream.Stream + ( + evaluate: LazyArg>, + onError: (error: unknown) => E + ): Stream.Stream +} = ( + ...args: [options: { + readonly evaluate: LazyArg> + readonly onError: (error: unknown) => E + readonly releaseLockOnEnd?: boolean | undefined + }] | [ + evaluate: LazyArg>, + onError: (error: unknown) => E + ] +): Stream.Stream => { + const evaluate = args.length === 1 ? args[0].evaluate : args[0] + const onError = args.length === 1 ? args[0].onError : args[1] + const releaseLockOnEnd = args.length === 1 ? args[0].releaseLockOnEnd === true : false + return unwrapScoped(Effect.map( + Effect.acquireRelease( + Effect.sync(() => evaluate().getReader()), + (reader) => + releaseLockOnEnd + ? Effect.sync(() => reader.releaseLock()) + : Effect.promise(() => reader.cancel()) + ), + (reader) => + repeatEffectOption( + Effect.flatMap( + Effect.tryPromise({ + try: () => reader.read(), + catch: (reason) => Option.some(onError(reason)) + }), + ({ done, value }) => done ? Effect.fail(Option.none()) : Effect.succeed(value) + ) + ) + )) +} + +/** @internal */ +export const fromReadableStreamByob: { + ( + options: { + readonly evaluate: LazyArg> + readonly onError: (error: unknown) => E + readonly bufferSize?: number | undefined + readonly releaseLockOnEnd?: boolean | undefined + } + ): Stream.Stream + ( + evaluate: LazyArg>, + onError: (error: unknown) => E, + allocSize?: number + ): Stream.Stream +} = ( + ...args: [options: { + readonly evaluate: LazyArg> + readonly onError: (error: unknown) => E + readonly bufferSize?: number | undefined + readonly releaseLockOnEnd?: boolean | undefined + }] | [ + evaluate: LazyArg>, + onError: (error: unknown) => E, + allocSize?: number | undefined + ] +): Stream.Stream => { + const evaluate = args.length === 1 ? args[0].evaluate : args[0] + const onError = args.length === 1 ? args[0].onError : args[1] + const allocSize = (args.length === 1 ? args[0].bufferSize : args[2]) ?? 4096 + const releaseLockOnEnd = args.length === 1 ? args[0].releaseLockOnEnd === true : false + return unwrapScoped(Effect.map( + Effect.acquireRelease( + Effect.sync(() => evaluate().getReader({ mode: "byob" })), + (reader) => releaseLockOnEnd ? Effect.sync(() => reader.releaseLock()) : Effect.promise(() => reader.cancel()) + ), + (reader) => + catchAll( + forever(readChunkStreamByobReader(reader, onError, allocSize)), + (error) => error === EOF ? empty : fail(error) + ) + )) +} + +const EOF = Symbol.for("effect/Stream/EOF") + +const readChunkStreamByobReader = ( + reader: ReadableStreamBYOBReader, + onError: (error: unknown) => E, + size: number +): Stream.Stream => { + const buffer = new ArrayBuffer(size) + return paginateEffect(0, (offset) => + Effect.flatMap( + Effect.tryPromise({ + try: () => reader.read(new Uint8Array(buffer, offset, buffer.byteLength - offset)), + catch: (reason) => onError(reason) + }), + ({ done, value }) => { + if (done) { + return Effect.fail(EOF) + } + const newOffset = offset + value.byteLength + return Effect.succeed([ + value, + newOffset >= buffer.byteLength + ? Option.none() + : Option.some(newOffset) + ]) + } + )) +} + +/** @internal */ +export const groupAdjacentBy = dual< + ( + f: (a: A) => K + ) => (self: Stream.Stream) => Stream.Stream<[K, Chunk.NonEmptyChunk], E, R>, + ( + self: Stream.Stream, + f: (a: A) => K + ) => Stream.Stream<[K, Chunk.NonEmptyChunk], E, R> +>( + 2, + ( + self: Stream.Stream, + f: (a: A) => K + ): Stream.Stream<[K, Chunk.NonEmptyChunk], E, R> => { + type Output = [K, Chunk.NonEmptyChunk] + const groupAdjacentByChunk = ( + state: Option.Option, + chunk: Chunk.Chunk + ): [Option.Option, Chunk.Chunk] => { + if (Chunk.isEmpty(chunk)) { + return [state, Chunk.empty()] + } + const builder: Array = [] + let from = 0 + let until = 0 + let key: K | undefined = undefined + let previousChunk = Chunk.empty() + switch (state._tag) { + case "Some": { + const tuple = state.value + key = tuple[0] + let loop = true + while (loop && until < chunk.length) { + const input = Chunk.unsafeGet(chunk, until) + const updatedKey = f(input) + if (!Equal.equals(key, updatedKey)) { + const previousChunk = tuple[1] + const additionalChunk = Chunk.unsafeFromArray(Array.from(chunk).slice(from, until)) + const group = Chunk.appendAll(previousChunk, additionalChunk) + builder.push([key, group]) + key = updatedKey + from = until + loop = false + } + until = until + 1 + } + if (loop) { + previousChunk = tuple[1] + } + break + } + case "None": { + key = f(Chunk.unsafeGet(chunk, until)) + until = until + 1 + break + } + } + while (until < chunk.length) { + const input = Chunk.unsafeGet(chunk, until) + const updatedKey = f(input) + if (!Equal.equals(key, updatedKey)) { + builder.push([key, Chunk.unsafeFromArray(Array.from(chunk).slice(from, until)) as Chunk.NonEmptyChunk]) + key = updatedKey + from = until + } + until = until + 1 + } + const nonEmptyChunk = Chunk.appendAll(previousChunk, Chunk.unsafeFromArray(Array.from(chunk).slice(from, until))) + const output = Chunk.unsafeFromArray(builder) + return [Option.some([key, nonEmptyChunk as Chunk.NonEmptyChunk]), output] + } + + const groupAdjacent = ( + state: Option.Option + ): Channel.Channel, Chunk.Chunk, never, never, unknown, unknown> => + core.readWithCause({ + onInput: (input: Chunk.Chunk) => { + const [updatedState, output] = groupAdjacentByChunk(state, input) + return Chunk.isEmpty(output) + ? groupAdjacent(updatedState) + : core.flatMap(core.write(output), () => groupAdjacent(updatedState)) + }, + onFailure: (cause) => + Option.match(state, { + onNone: () => core.failCause(cause), + onSome: (output) => core.flatMap(core.write(Chunk.of(output)), () => core.failCause(cause)) + }), + onDone: (done) => + Option.match(state, { + onNone: () => core.succeedNow(done), + onSome: (output) => core.flatMap(core.write(Chunk.of(output)), () => core.succeedNow(done)) + }) + }) + return new StreamImpl(channel.pipeToOrFail(toChannel(self), groupAdjacent(Option.none()))) + } +) + +/** @internal */ +export const grouped = dual< + (chunkSize: number) => (self: Stream.Stream) => Stream.Stream, E, R>, + (self: Stream.Stream, chunkSize: number) => Stream.Stream, E, R> +>( + 2, + (self: Stream.Stream, chunkSize: number): Stream.Stream, E, R> => + pipe(self, rechunk(chunkSize), chunks) +) + +/** @internal */ +export const groupedWithin = dual< + ( + chunkSize: number, + duration: Duration.DurationInput + ) => (self: Stream.Stream) => Stream.Stream, E, R>, + ( + self: Stream.Stream, + chunkSize: number, + duration: Duration.DurationInput + ) => Stream.Stream, E, R> +>( + 3, + ( + self: Stream.Stream, + chunkSize: number, + duration: Duration.DurationInput + ): Stream.Stream, E, R> => + aggregateWithin(self, sink_.collectAllN(chunkSize), Schedule.spaced(duration)) +) + +/** @internal */ +export const haltWhen = dual< + ( + effect: Effect.Effect + ) => (self: Stream.Stream) => Stream.Stream, + ( + self: Stream.Stream, + effect: Effect.Effect + ) => Stream.Stream +>( + 2, + ( + self: Stream.Stream, + effect: Effect.Effect + ): Stream.Stream => { + const writer = ( + fiber: Fiber.Fiber + ): Channel.Channel, Chunk.Chunk, E | E2, E | E2, void, unknown, R2> => + pipe( + Fiber.poll(fiber), + Effect.map(Option.match({ + onNone: () => + core.readWith({ + onInput: (input: Chunk.Chunk) => core.flatMap(core.write(input), () => writer(fiber)), + onFailure: core.fail, + onDone: () => core.void + }), + onSome: Exit.match({ + onFailure: core.failCause, + onSuccess: () => core.void + }) + })), + channel.unwrap + ) + return new StreamImpl( + channel.unwrapScopedWith((scope) => + effect.pipe( + Effect.forkIn(scope), + Effect.map((fiber) => toChannel(self).pipe(core.pipeTo(writer(fiber)))) + ) + ) + ) + } +) + +/** @internal */ +export const haltAfter = dual< + (duration: Duration.DurationInput) => (self: Stream.Stream) => Stream.Stream, + (self: Stream.Stream, duration: Duration.DurationInput) => Stream.Stream +>( + 2, + (self: Stream.Stream, duration: Duration.DurationInput): Stream.Stream => + pipe(self, haltWhen(Clock.sleep(duration))) +) + +/** @internal */ +export const haltWhenDeferred = dual< + (deferred: Deferred.Deferred) => (self: Stream.Stream) => Stream.Stream, + (self: Stream.Stream, deferred: Deferred.Deferred) => Stream.Stream +>( + 2, + (self: Stream.Stream, deferred: Deferred.Deferred): Stream.Stream => { + const writer: Channel.Channel, Chunk.Chunk, E | E2, E | E2, void, unknown, R> = pipe( + Deferred.poll(deferred), + Effect.map(Option.match({ + onNone: () => + core.readWith({ + onInput: (input: Chunk.Chunk) => pipe(core.write(input), core.flatMap(() => writer)), + onFailure: core.fail, + onDone: () => core.void + }), + onSome: (effect) => + channel.unwrap(Effect.match(effect, { + onFailure: core.fail, + onSuccess: () => core.void + })) + })), + channel.unwrap + ) + return new StreamImpl(pipe(toChannel(self), core.pipeTo(writer))) + } +) + +/** @internal */ +export const identityStream = (): Stream.Stream => + new StreamImpl( + channel.identityChannel() as Channel.Channel, unknown, E, unknown, unknown, unknown> + ) + +/** @internal */ +export const interleave = dual< + ( + that: Stream.Stream + ) => (self: Stream.Stream) => Stream.Stream, + ( + self: Stream.Stream, + that: Stream.Stream + ) => Stream.Stream +>( + 2, + ( + self: Stream.Stream, + that: Stream.Stream + ): Stream.Stream => pipe(self, interleaveWith(that, forever(make(true, false)))) +) + +/** @internal */ +export const interleaveWith = dual< + ( + that: Stream.Stream, + decider: Stream.Stream + ) => (self: Stream.Stream) => Stream.Stream, + ( + self: Stream.Stream, + that: Stream.Stream, + decider: Stream.Stream + ) => Stream.Stream +>( + 3, + ( + self: Stream.Stream, + that: Stream.Stream, + decider: Stream.Stream + ): Stream.Stream => { + const producer = ( + handoff: Handoff.Handoff> + ): Channel.Channel => + core.readWithCause({ + onInput: (value: A | A2) => + core.flatMap( + core.fromEffect( + Handoff.offer>(handoff, InternalTake.of(value)) + ), + () => producer(handoff) + ), + onFailure: (cause) => + core.fromEffect( + Handoff.offer>( + handoff, + InternalTake.failCause(cause) + ) + ), + onDone: () => + core.fromEffect( + Handoff.offer>(handoff, InternalTake.end) + ) + }) + return new StreamImpl( + channel.unwrapScopedWith((scope) => + pipe( + Handoff.make>(), + Effect.zip(Handoff.make>()), + Effect.tap(([left]) => + toChannel(self).pipe( + channel.concatMap(channel.writeChunk), + core.pipeTo(producer(left)), + channelExecutor.runIn(scope), + Effect.forkIn(scope) + ) + ), + Effect.tap(([_, right]) => + toChannel(that).pipe( + channel.concatMap(channel.writeChunk), + core.pipeTo(producer(right)), + channelExecutor.runIn(scope), + Effect.forkIn(scope) + ) + ), + Effect.map(([left, right]) => { + const process = ( + leftDone: boolean, + rightDone: boolean + ): Channel.Channel, boolean, E | E2 | E3, E | E2 | E3, void, unknown, R> => + core.readWithCause({ + onInput: (bool: boolean) => { + if (bool && !leftDone) { + return pipe( + core.fromEffect(Handoff.take(left)), + core.flatMap(InternalTake.match({ + onEnd: () => rightDone ? core.void : process(true, rightDone), + onFailure: core.failCause, + onSuccess: (chunk) => pipe(core.write(chunk), core.flatMap(() => process(leftDone, rightDone))) + })) + ) + } + if (!bool && !rightDone) { + return pipe( + core.fromEffect(Handoff.take(right)), + core.flatMap(InternalTake.match({ + onEnd: () => leftDone ? core.void : process(leftDone, true), + onFailure: core.failCause, + onSuccess: (chunk) => pipe(core.write(chunk), core.flatMap(() => process(leftDone, rightDone))) + })) + ) + } + return process(leftDone, rightDone) + }, + onFailure: core.failCause, + onDone: () => core.void + }) + return pipe( + toChannel(decider), + channel.concatMap(channel.writeChunk), + core.pipeTo(process(false, false)) + ) + }) + ) + ) + ) + } +) + +/** @internal */ +export const intersperse = dual< + (element: A2) => (self: Stream.Stream) => Stream.Stream, + (self: Stream.Stream, element: A2) => Stream.Stream +>(2, (self: Stream.Stream, element: A2): Stream.Stream => + new StreamImpl( + pipe( + toChannel(self), + channel.pipeToOrFail( + core.suspend(() => { + const writer = ( + isFirst: boolean + ): Channel.Channel, Chunk.Chunk, E, E, unknown, unknown> => + core.readWithCause({ + onInput: (chunk: Chunk.Chunk) => { + const builder: Array = [] + let flagResult = isFirst + for (const output of chunk) { + if (flagResult) { + flagResult = false + builder.push(output) + } else { + builder.push(element) + builder.push(output) + } + } + return pipe( + core.write(Chunk.unsafeFromArray(builder)), + core.flatMap(() => writer(flagResult)) + ) + }, + onFailure: core.failCause, + onDone: () => core.void + }) + return writer(true) + }) + ) + ) + )) + +/** @internal */ +export const intersperseAffixes = dual< + ( + options: { + readonly start: A2 + readonly middle: A3 + readonly end: A4 + } + ) => (self: Stream.Stream) => Stream.Stream, + ( + self: Stream.Stream, + options: { + readonly start: A2 + readonly middle: A3 + readonly end: A4 + } + ) => Stream.Stream +>( + 2, + ( + self: Stream.Stream, + { end, middle, start }: { + readonly start: A2 + readonly middle: A3 + readonly end: A4 + } + ): Stream.Stream => + pipe( + make(start), + concat(pipe(self, intersperse(middle))), + concat(make(end)) + ) +) + +/** @internal */ +export const interruptAfter = dual< + (duration: Duration.DurationInput) => (self: Stream.Stream) => Stream.Stream, + (self: Stream.Stream, duration: Duration.DurationInput) => Stream.Stream +>( + 2, + (self: Stream.Stream, duration: Duration.DurationInput): Stream.Stream => + pipe(self, interruptWhen(Clock.sleep(duration))) +) + +/** @internal */ +export const interruptWhen = dual< + ( + effect: Effect.Effect + ) => (self: Stream.Stream) => Stream.Stream, + ( + self: Stream.Stream, + effect: Effect.Effect + ) => Stream.Stream +>( + 2, + ( + self: Stream.Stream, + effect: Effect.Effect + ): Stream.Stream => new StreamImpl(pipe(toChannel(self), channel.interruptWhen(effect))) +) + +/** @internal */ +export const interruptWhenDeferred = dual< + (deferred: Deferred.Deferred) => (self: Stream.Stream) => Stream.Stream, + (self: Stream.Stream, deferred: Deferred.Deferred) => Stream.Stream +>( + 2, + (self: Stream.Stream, deferred: Deferred.Deferred): Stream.Stream => + new StreamImpl(pipe(toChannel(self), channel.interruptWhenDeferred(deferred))) +) + +/** @internal */ +export const iterate = (value: A, next: (value: A) => A): Stream.Stream => + unfold(value, (a) => Option.some([a, next(a)] as const)) + +/** @internal */ +export const make = >(...as: As): Stream.Stream => fromIterable(as) + +/** @internal */ +export const map = dual< + (f: (a: A) => B) => (self: Stream.Stream) => Stream.Stream, + (self: Stream.Stream, f: (a: A) => B) => Stream.Stream +>( + 2, + (self: Stream.Stream, f: (a: A) => B): Stream.Stream => + new StreamImpl(pipe(toChannel(self), channel.mapOut(Chunk.map(f)))) +) + +/** @internal */ +export const mapAccum = dual< + ( + s: S, + f: (s: S, a: A) => readonly [S, A2] + ) => (self: Stream.Stream) => Stream.Stream, + (self: Stream.Stream, s: S, f: (s: S, a: A) => readonly [S, A2]) => Stream.Stream +>( + 3, + ( + self: Stream.Stream, + s: S, + f: (s: S, a: A) => readonly [S, A2] + ): Stream.Stream => { + const accumulator = (s: S): Channel.Channel, Chunk.Chunk, E, E, void, unknown> => + core.readWith({ + onInput: (input: Chunk.Chunk) => { + const [nextS, chunk] = Chunk.mapAccum(input, s, f) + return core.flatMap( + core.write(chunk), + () => accumulator(nextS) + ) + }, + onFailure: core.fail, + onDone: () => core.void + }) + return new StreamImpl(pipe(toChannel(self), core.pipeTo(accumulator(s)))) + } +) + +/** @internal */ +export const mapAccumEffect = dual< + ( + s: S, + f: (s: S, a: A) => Effect.Effect + ) => (self: Stream.Stream) => Stream.Stream, + ( + self: Stream.Stream, + s: S, + f: (s: S, a: A) => Effect.Effect + ) => Stream.Stream +>( + 3, + ( + self: Stream.Stream, + s: S, + f: (s: S, a: A) => Effect.Effect + ): Stream.Stream => + suspend(() => { + const accumulator = ( + s: S + ): Channel.Channel, Chunk.Chunk, E | E2, E, unknown, unknown, R | R2> => + core.readWith({ + onInput: (input: Chunk.Chunk) => + pipe( + Effect.suspend(() => { + const outputs: Array = [] + const emit = (output: A2) => + Effect.sync(() => { + outputs.push(output) + }) + return pipe( + input, + Effect.reduce(s, (s, a) => + pipe( + f(s, a), + Effect.flatMap(([s, a]) => pipe(emit(a), Effect.as(s))) + )), + Effect.match({ + onFailure: (error) => { + if (outputs.length !== 0) { + return channel.zipRight(core.write(Chunk.unsafeFromArray(outputs)), core.fail(error)) + } + return core.fail(error) + }, + onSuccess: (s) => core.flatMap(core.write(Chunk.unsafeFromArray(outputs)), () => accumulator(s)) + }) + ) + }), + channel.unwrap + ), + onFailure: core.fail, + onDone: () => core.void + }) + return new StreamImpl(pipe(toChannel(self), channel.pipeToOrFail(accumulator(s)))) + }) +) + +/** @internal */ +export const mapBoth = dual< + ( + options: { + readonly onFailure: (e: E) => E2 + readonly onSuccess: (a: A) => A2 + } + ) => (self: Stream.Stream) => Stream.Stream, + ( + self: Stream.Stream, + options: { + readonly onFailure: (e: E) => E2 + readonly onSuccess: (a: A) => A2 + } + ) => Stream.Stream +>( + 2, + ( + self: Stream.Stream, + options: { + readonly onFailure: (e: E) => E2 + readonly onSuccess: (a: A) => A2 + } + ): Stream.Stream => pipe(self, mapError(options.onFailure), map(options.onSuccess)) +) + +/** @internal */ +export const mapChunks = dual< + ( + f: (chunk: Chunk.Chunk) => Chunk.Chunk + ) => (self: Stream.Stream) => Stream.Stream, + (self: Stream.Stream, f: (chunk: Chunk.Chunk) => Chunk.Chunk) => Stream.Stream +>( + 2, + (self: Stream.Stream, f: (chunk: Chunk.Chunk) => Chunk.Chunk): Stream.Stream => + new StreamImpl(pipe(toChannel(self), channel.mapOut(f))) +) + +/** @internal */ +export const mapChunksEffect = dual< + ( + f: (chunk: Chunk.Chunk) => Effect.Effect, E2, R2> + ) => (self: Stream.Stream) => Stream.Stream, + ( + self: Stream.Stream, + f: (chunk: Chunk.Chunk) => Effect.Effect, E2, R2> + ) => Stream.Stream +>( + 2, + ( + self: Stream.Stream, + f: (chunk: Chunk.Chunk) => Effect.Effect, E2, R2> + ): Stream.Stream => new StreamImpl(pipe(toChannel(self), channel.mapOutEffect(f))) +) + +/** @internal */ +export const mapConcat = dual< + (f: (a: A) => Iterable) => (self: Stream.Stream) => Stream.Stream, + (self: Stream.Stream, f: (a: A) => Iterable) => Stream.Stream +>( + 2, + (self: Stream.Stream, f: (a: A) => Iterable): Stream.Stream => + pipe(self, mapConcatChunk((a) => Chunk.fromIterable(f(a)))) +) + +/** @internal */ +export const mapConcatChunk = dual< + (f: (a: A) => Chunk.Chunk) => (self: Stream.Stream) => Stream.Stream, + (self: Stream.Stream, f: (a: A) => Chunk.Chunk) => Stream.Stream +>( + 2, + (self: Stream.Stream, f: (a: A) => Chunk.Chunk): Stream.Stream => + pipe(self, mapChunks(Chunk.flatMap(f))) +) + +/** @internal */ +export const mapConcatChunkEffect = dual< + ( + f: (a: A) => Effect.Effect, E2, R2> + ) => (self: Stream.Stream) => Stream.Stream, + ( + self: Stream.Stream, + f: (a: A) => Effect.Effect, E2, R2> + ) => Stream.Stream +>( + 2, + ( + self: Stream.Stream, + f: (a: A) => Effect.Effect, E2, R2> + ): Stream.Stream => pipe(self, mapEffectSequential(f), mapConcatChunk(identity)) +) + +/** @internal */ +export const mapConcatEffect = dual< + ( + f: (a: A) => Effect.Effect, E2, R2> + ) => (self: Stream.Stream) => Stream.Stream, + ( + self: Stream.Stream, + f: (a: A) => Effect.Effect, E2, R2> + ) => Stream.Stream +>( + 2, + ( + self: Stream.Stream, + f: (a: A) => Effect.Effect, E2, R2> + ): Stream.Stream => + pipe(self, mapEffectSequential((a) => pipe(f(a), Effect.map(Chunk.fromIterable))), mapConcatChunk(identity)) +) + +/** @internal */ +export const mapEffectSequential = dual< + ( + f: (a: A) => Effect.Effect + ) => (self: Stream.Stream) => Stream.Stream, + ( + self: Stream.Stream, + f: (a: A) => Effect.Effect + ) => Stream.Stream +>( + 2, + ( + self: Stream.Stream, + f: (a: A) => Effect.Effect + ): Stream.Stream => { + const loop = ( + iterator: Iterator + ): Channel.Channel, Chunk.Chunk, E | E2, E, unknown, unknown, R2> => { + const next = iterator.next() + if (next.done) { + return core.readWithCause({ + onInput: (elem) => loop(elem[Symbol.iterator]()), + onFailure: core.failCause, + onDone: core.succeed + }) + } else { + const value = next.value + return channel.unwrap( + Effect.map(f(value), (a2) => + core.flatMap( + core.write(Chunk.of(a2)), + () => loop(iterator) + )) + ) + } + } + return new StreamImpl(pipe( + toChannel(self), + core.pipeTo(core.suspend(() => loop(Chunk.empty()[Symbol.iterator]()))) + )) + } +) + +/** @internal */ +export const mapEffectPar = dual< + ( + n: number, + f: (a: A) => Effect.Effect + ) => (self: Stream.Stream) => Stream.Stream, + ( + self: Stream.Stream, + n: number, + f: (a: A) => Effect.Effect + ) => Stream.Stream +>( + 3, + ( + self: Stream.Stream, + n: number, + f: (a: A) => Effect.Effect + ): Stream.Stream => + new StreamImpl( + pipe( + toChannel(self), + channel.concatMap(channel.writeChunk), + channel.mapOutEffectPar(f, n), + channel.mapOut(Chunk.of) + ) + ) +) + +/** @internal */ +export const mapError = dual< + (f: (error: E) => E2) => (self: Stream.Stream) => Stream.Stream, + (self: Stream.Stream, f: (error: E) => E2) => Stream.Stream +>( + 2, + (self: Stream.Stream, f: (error: E) => E2): Stream.Stream => + new StreamImpl(pipe(toChannel(self), channel.mapError(f))) +) + +/** @internal */ +export const mapErrorCause = dual< + ( + f: (cause: Cause.Cause) => Cause.Cause + ) => (self: Stream.Stream) => Stream.Stream, + (self: Stream.Stream, f: (cause: Cause.Cause) => Cause.Cause) => Stream.Stream +>( + 2, + (self: Stream.Stream, f: (cause: Cause.Cause) => Cause.Cause): Stream.Stream => + new StreamImpl(pipe(toChannel(self), channel.mapErrorCause(f))) +) + +/** @internal */ +export const merge = dual< + ( + that: Stream.Stream, + options?: { + readonly haltStrategy?: HaltStrategy.HaltStrategyInput | undefined + } + ) => (self: Stream.Stream) => Stream.Stream, + ( + self: Stream.Stream, + that: Stream.Stream, + options?: { + readonly haltStrategy?: HaltStrategy.HaltStrategyInput | undefined + } + ) => Stream.Stream +>( + (args) => isStream(args[1]), + ( + self: Stream.Stream, + that: Stream.Stream, + options?: { + readonly haltStrategy?: HaltStrategy.HaltStrategyInput | undefined + } + ): Stream.Stream => + mergeWith(self, that, { + onSelf: identity, + onOther: identity, + haltStrategy: options?.haltStrategy + }) +) + +/** @internal */ +export const mergeAll = dual< + (options: { + readonly concurrency: number | "unbounded" + readonly bufferSize?: number | undefined + }) => (streams: Iterable>) => Stream.Stream, + (streams: Iterable>, options: { + readonly concurrency: number | "unbounded" + readonly bufferSize?: number | undefined + }) => Stream.Stream +>((args) => Symbol.iterator in args[0], (streams, options) => flatten(fromIterable(streams), options)) + +/** @internal */ +export const mergeWithTag: { + }>( + streams: S, + options: { + readonly concurrency: number | "unbounded" + readonly bufferSize?: number | undefined + } + ): Stream.Stream< + { [K in keyof S]: { _tag: K; value: Stream.Stream.Success } }[keyof S], + Stream.Stream.Error, + Stream.Stream.Context + > + (options: { + readonly concurrency: number | "unbounded" + readonly bufferSize?: number | undefined + }): }>(streams: S) => Stream.Stream< + { [K in keyof S]: { _tag: K; value: Stream.Stream.Success } }[keyof S], + Stream.Stream.Error, + Stream.Stream.Context + > +} = dual(2, (streams, options) => { + const keys = Object.keys(streams) + const values = keys.map((key) => streams[key].pipe(map((value) => ({ _tag: key, value })))) as any + return mergeAll(values, options) +}) + +/** @internal */ +export const mergeEither = dual< + ( + that: Stream.Stream + ) => (self: Stream.Stream) => Stream.Stream, E2 | E, R2 | R>, + ( + self: Stream.Stream, + that: Stream.Stream + ) => Stream.Stream, E2 | E, R2 | R> +>( + 2, + ( + self: Stream.Stream, + that: Stream.Stream + ): Stream.Stream, E2 | E, R2 | R> => + mergeWith(self, that, { onSelf: Either.left, onOther: Either.right }) +) + +/** @internal */ +export const mergeLeft = dual< + ( + right: Stream.Stream + ) => (left: Stream.Stream) => Stream.Stream, + ( + left: Stream.Stream, + right: Stream.Stream + ) => Stream.Stream +>( + 2, + ( + left: Stream.Stream, + right: Stream.Stream + ): Stream.Stream => pipe(left, merge(drain(right))) +) + +/** @internal */ +export const mergeRight = dual< + ( + right: Stream.Stream + ) => (left: Stream.Stream) => Stream.Stream, + ( + left: Stream.Stream, + right: Stream.Stream + ) => Stream.Stream +>( + 2, + ( + left: Stream.Stream, + right: Stream.Stream + ): Stream.Stream => pipe(drain(left), merge(right)) +) + +/** @internal */ +export const mergeWith = dual< + ( + other: Stream.Stream, + options: { + readonly onSelf: (a: A) => A3 + readonly onOther: (a2: A2) => A4 + readonly haltStrategy?: HaltStrategy.HaltStrategyInput | undefined + } + ) => (self: Stream.Stream) => Stream.Stream, + ( + self: Stream.Stream, + other: Stream.Stream, + options: { + readonly onSelf: (a: A) => A3 + readonly onOther: (a2: A2) => A4 + readonly haltStrategy?: HaltStrategy.HaltStrategyInput | undefined + } + ) => Stream.Stream +>( + 3, + ( + self: Stream.Stream, + other: Stream.Stream, + options: { + readonly onSelf: (a: A) => A3 + readonly onOther: (a2: A2) => A4 + readonly haltStrategy?: HaltStrategy.HaltStrategyInput | undefined + } + ): Stream.Stream => { + const strategy = options.haltStrategy ? haltStrategy.fromInput(options.haltStrategy) : HaltStrategy.Both + const handler = + (terminate: boolean) => + (exit: Exit.Exit): MergeDecision.MergeDecision => + terminate || !Exit.isSuccess(exit) ? + // TODO: remove + MergeDecision.Done(Effect.suspend(() => exit)) : + MergeDecision.Await((exit) => Effect.suspend(() => exit)) + + return new StreamImpl( + channel.mergeWith(toChannel(map(self, options.onSelf)), { + other: toChannel(map(other, options.onOther)), + onSelfDone: handler(strategy._tag === "Either" || strategy._tag === "Left"), + onOtherDone: handler(strategy._tag === "Either" || strategy._tag === "Right") + }) + ) + } +) + +/** @internal */ +export const mkString = (self: Stream.Stream): Effect.Effect => + run(self, sink_.mkString) + +/** @internal */ +export const never: Stream.Stream = fromEffect(Effect.never) + +/** @internal */ +export const onEnd: { + <_, E2, R2>( + effect: Effect.Effect<_, E2, R2> + ): (self: Stream.Stream) => Stream.Stream + ( + self: Stream.Stream, + effect: Effect.Effect<_, E2, R2> + ): Stream.Stream +} = dual( + 2, + ( + self: Stream.Stream, + effect: Effect.Effect<_, E2, R2> + ): Stream.Stream => concat(self, drain(fromEffect(effect))) +) + +/** @internal */ +export const onError = dual< + ( + cleanup: (cause: Cause.Cause) => Effect.Effect + ) => (self: Stream.Stream) => Stream.Stream, + ( + self: Stream.Stream, + cleanup: (cause: Cause.Cause) => Effect.Effect + ) => Stream.Stream +>( + 2, + ( + self: Stream.Stream, + cleanup: (cause: Cause.Cause) => Effect.Effect + ): Stream.Stream => + pipe(self, catchAllCause((cause) => fromEffect(pipe(cleanup(cause), Effect.zipRight(Effect.failCause(cause)))))) +) + +/** @internal */ +export const onDone = dual< + ( + cleanup: () => Effect.Effect + ) => (self: Stream.Stream) => Stream.Stream, + ( + self: Stream.Stream, + cleanup: () => Effect.Effect + ) => Stream.Stream +>( + 2, + ( + self: Stream.Stream, + cleanup: () => Effect.Effect + ): Stream.Stream => + new StreamImpl( + pipe(toChannel(self), core.ensuringWith((exit) => Exit.isSuccess(exit) ? cleanup() : Effect.void)) + ) +) + +/** @internal */ +export const onStart: { + <_, E2, R2>( + effect: Effect.Effect<_, E2, R2> + ): (self: Stream.Stream) => Stream.Stream + ( + self: Stream.Stream, + effect: Effect.Effect<_, E2, R2> + ): Stream.Stream +} = dual( + 2, + ( + self: Stream.Stream, + effect: Effect.Effect<_, E2, R2> + ): Stream.Stream => unwrap(Effect.as(effect, self)) +) + +/** @internal */ +export const orDie = (self: Stream.Stream): Stream.Stream => + pipe(self, orDieWith(identity)) + +/** @internal */ +export const orDieWith = dual< + (f: (e: E) => unknown) => (self: Stream.Stream) => Stream.Stream, + (self: Stream.Stream, f: (e: E) => unknown) => Stream.Stream +>( + 2, + (self: Stream.Stream, f: (e: E) => unknown): Stream.Stream => + new StreamImpl(pipe(toChannel(self), channel.orDieWith(f))) +) + +/** @internal */ +export const orElse = dual< + ( + that: LazyArg> + ) => (self: Stream.Stream) => Stream.Stream, + ( + self: Stream.Stream, + that: LazyArg> + ) => Stream.Stream +>( + 2, + ( + self: Stream.Stream, + that: LazyArg> + ): Stream.Stream => + new StreamImpl(pipe(toChannel(self), channel.orElse(() => toChannel(that())))) +) + +/** @internal */ +export const orElseEither = dual< + ( + that: LazyArg> + ) => (self: Stream.Stream) => Stream.Stream, E2, R2 | R>, + ( + self: Stream.Stream, + that: LazyArg> + ) => Stream.Stream, E2, R2 | R> +>( + 2, + ( + self: Stream.Stream, + that: LazyArg> + ): Stream.Stream, E2, R2 | R> => + pipe(self, map(Either.left), orElse(() => pipe(that(), map(Either.right)))) +) + +/** @internal */ +export const orElseFail = dual< + (error: LazyArg) => (self: Stream.Stream) => Stream.Stream, + (self: Stream.Stream, error: LazyArg) => Stream.Stream +>( + 2, + (self: Stream.Stream, error: LazyArg): Stream.Stream => + pipe(self, orElse(() => failSync(error))) +) + +/** @internal */ +export const orElseIfEmpty = dual< + (element: LazyArg) => (self: Stream.Stream) => Stream.Stream, + (self: Stream.Stream, element: LazyArg) => Stream.Stream +>( + 2, + (self: Stream.Stream, element: LazyArg): Stream.Stream => + pipe(self, orElseIfEmptyChunk(() => Chunk.of(element()))) +) + +/** @internal */ +export const orElseIfEmptyChunk = dual< + (chunk: LazyArg>) => (self: Stream.Stream) => Stream.Stream, + (self: Stream.Stream, chunk: LazyArg>) => Stream.Stream +>( + 2, + (self: Stream.Stream, chunk: LazyArg>): Stream.Stream => + pipe(self, orElseIfEmptyStream(() => new StreamImpl(core.write(chunk())))) +) + +/** @internal */ +export const orElseIfEmptyStream = dual< + ( + stream: LazyArg> + ) => (self: Stream.Stream) => Stream.Stream, + ( + self: Stream.Stream, + stream: LazyArg> + ) => Stream.Stream +>( + 2, + ( + self: Stream.Stream, + stream: LazyArg> + ): Stream.Stream => { + const writer: Channel.Channel, Chunk.Chunk, E | E2, E, unknown, unknown, R2> = core.readWith( + { + onInput: (input: Chunk.Chunk) => { + if (Chunk.isEmpty(input)) { + return core.suspend(() => writer) + } + return pipe( + core.write(input), + channel.zipRight(channel.identityChannel, E, unknown>()) + ) + }, + onFailure: core.fail, + onDone: () => core.suspend(() => toChannel(stream())) + } + ) + return new StreamImpl(pipe(toChannel(self), core.pipeTo(writer))) + } +) + +/** @internal */ +export const orElseSucceed = dual< + (value: LazyArg) => (self: Stream.Stream) => Stream.Stream, + (self: Stream.Stream, value: LazyArg) => Stream.Stream +>( + 2, + (self: Stream.Stream, value: LazyArg): Stream.Stream => + pipe(self, orElse(() => sync(value))) +) + +/** @internal */ +export const paginate = (s: S, f: (s: S) => readonly [A, Option.Option]): Stream.Stream => + paginateChunk(s, (s) => { + const page = f(s) + return [Chunk.of(page[0]), page[1]] as const + }) + +/** @internal */ +export const paginateChunk = ( + s: S, + f: (s: S) => readonly [Chunk.Chunk, Option.Option] +): Stream.Stream => { + const loop = (s: S): Channel.Channel, unknown, never, unknown, unknown, unknown> => { + const page = f(s) + return Option.match(page[1], { + onNone: () => channel.zipRight(core.write(page[0]), core.void), + onSome: (s) => core.flatMap(core.write(page[0]), () => loop(s)) + }) + } + return new StreamImpl(core.suspend(() => loop(s))) +} + +/** @internal */ +export const paginateChunkEffect = ( + s: S, + f: (s: S) => Effect.Effect, Option.Option], E, R> +): Stream.Stream => { + const loop = (s: S): Channel.Channel, unknown, E, unknown, unknown, unknown, R> => + channel.unwrap( + Effect.map(f(s), ([chunk, option]) => + Option.match(option, { + onNone: () => channel.zipRight(core.write(chunk), core.void), + onSome: (s) => core.flatMap(core.write(chunk), () => loop(s)) + })) + ) + return new StreamImpl(core.suspend(() => loop(s))) +} + +/** @internal */ +export const paginateEffect = ( + s: S, + f: (s: S) => Effect.Effect], E, R> +): Stream.Stream => + paginateChunkEffect(s, (s) => pipe(f(s), Effect.map(([a, s]) => [Chunk.of(a), s] as const))) + +/** @internal */ +export const peel = dual< + ( + sink: Sink.Sink + ) => ( + self: Stream.Stream + ) => Effect.Effect<[A2, Stream.Stream], E2 | E, Scope.Scope | R2 | R>, + ( + self: Stream.Stream, + sink: Sink.Sink + ) => Effect.Effect<[A2, Stream.Stream], E2 | E, Scope.Scope | R2 | R> +>(2, ( + self: Stream.Stream, + sink: Sink.Sink +): Effect.Effect<[A2, Stream.Stream], E2 | E, Scope.Scope | R2 | R> => { + type Signal = Emit | Halt | End + const OP_EMIT = "Emit" as const + type OP_EMIT = typeof OP_EMIT + const OP_HALT = "Halt" as const + type OP_HALT = typeof OP_HALT + const OP_END = "End" as const + type OP_END = typeof OP_END + interface Emit { + readonly _tag: OP_EMIT + readonly elements: Chunk.Chunk + } + interface Halt { + readonly _tag: OP_HALT + readonly cause: Cause.Cause + } + interface End { + readonly _tag: OP_END + } + return pipe( + Deferred.make(), + Effect.flatMap((deferred) => + pipe( + Handoff.make(), + Effect.map((handoff) => { + const consumer = sink_.foldSink(sink_.collectLeftover(sink), { + onFailure: (error) => + sink_.zipRight( + sink_.fromEffect(Deferred.fail(deferred, error)), + sink_.fail(error) + ), + onSuccess: ([z, leftovers]) => { + const loop: Channel.Channel, Chunk.Chunk, E | E2, E, void, unknown> = core + .readWithCause({ + onInput: (elements) => + core.flatMap( + core.fromEffect( + Handoff.offer(handoff, { _tag: OP_EMIT, elements }) + ), + () => loop + ), + onFailure: (cause) => + channel.zipRight( + core.fromEffect(Handoff.offer(handoff, { _tag: OP_HALT, cause })), + core.failCause(cause) + ), + onDone: (_) => + channel.zipRight( + core.fromEffect(Handoff.offer(handoff, { _tag: OP_END })), + core.void + ) + }) + return sink_.fromChannel( + pipe( + core.fromEffect(Deferred.succeed(deferred, z)), + channel.zipRight(core.fromEffect( + pipe( + handoff, + Handoff.offer({ _tag: OP_EMIT, elements: leftovers }) + ) + )), + channel.zipRight(loop) + ) + ) + } + }) + + const producer: Channel.Channel, unknown, E, unknown, void, unknown> = pipe( + Handoff.take(handoff), + Effect.map((signal) => { + switch (signal._tag) { + case OP_EMIT: { + return pipe(core.write(signal.elements), core.flatMap(() => producer)) + } + case OP_HALT: { + return core.failCause(signal.cause) + } + case OP_END: { + return core.void + } + } + }), + channel.unwrap + ) + + return pipe( + self, + tapErrorCause((cause) => Deferred.failCause(deferred, cause)), + run(consumer), + Effect.forkScoped, + Effect.zipRight(Deferred.await(deferred)), + Effect.map((z) => [z, new StreamImpl(producer)] as [A2, StreamImpl]) + ) + }) + ) + ), + Effect.flatten + ) +}) + +/** @internal */ +export const partition: { + ( + refinement: Refinement, B>, + options?: { + bufferSize?: number | undefined + } + ): ( + self: Stream.Stream + ) => Effect.Effect< + [excluded: Stream.Stream, E>, satisfying: Stream.Stream], + E, + Scope.Scope | R + > + ( + predicate: Predicate, + options?: { + bufferSize?: number | undefined + } + ): ( + self: Stream.Stream + ) => Effect.Effect<[excluded: Stream.Stream, satisfying: Stream.Stream], E, Scope.Scope | R> + ( + self: Stream.Stream, + refinement: Refinement, + options?: { + bufferSize?: number | undefined + } + ): Effect.Effect< + [excluded: Stream.Stream, E>, satisfying: Stream.Stream], + E, + Scope.Scope | R + > + ( + self: Stream.Stream, + predicate: Predicate, + options?: { + bufferSize?: number | undefined + } + ): Effect.Effect<[excluded: Stream.Stream, satisfying: Stream.Stream], E, Scope.Scope | R> +} = dual( + (args) => typeof args[1] === "function", + ( + self: Stream.Stream, + predicate: Predicate, + options?: { + readonly bufferSize?: number | undefined + } + ): Effect.Effect< + [Stream.Stream, Stream.Stream], + E, + R | Scope.Scope + > => + partitionEither( + self, + (a) => Effect.succeed(predicate(a) ? Either.right(a) : Either.left(a)), + options + ) +) + +/** @internal */ +export const partitionEither = dual< + ( + predicate: (a: Types.NoInfer) => Effect.Effect, E2, R2>, + options?: { + readonly bufferSize?: number | undefined + } + ) => ( + self: Stream.Stream + ) => Effect.Effect< + [left: Stream.Stream, right: Stream.Stream], + E2 | E, + Scope.Scope | R2 | R + >, + ( + self: Stream.Stream, + predicate: (a: A) => Effect.Effect, E2, R2>, + options?: { + readonly bufferSize?: number | undefined + } + ) => Effect.Effect< + [left: Stream.Stream, right: Stream.Stream], + E2 | E, + Scope.Scope | R2 | R + > +>( + (args) => typeof args[1] === "function", + ( + self: Stream.Stream, + predicate: (a: A) => Effect.Effect, E2, R2>, + options?: { + readonly bufferSize?: number | undefined + } + ): Effect.Effect< + [left: Stream.Stream, right: Stream.Stream], + E | E2, + R | R2 | Scope.Scope + > => + pipe( + mapEffectSequential(self, predicate), + distributedWith({ + size: 2, + maximumLag: options?.bufferSize ?? 16, + decide: Either.match({ + onLeft: () => Effect.succeed((n) => n === 0), + onRight: () => Effect.succeed((n) => n === 1) + }) + }), + Effect.flatMap(([queue1, queue2]) => + Effect.succeed([ + filterMap( + flattenExitOption(fromQueue(queue1, { shutdown: true })), + (_) => + Either.match(_, { + onLeft: Option.some, + onRight: Option.none + }) + ), + filterMap( + flattenExitOption(fromQueue(queue2, { shutdown: true })), + (_) => + Either.match(_, { + onLeft: Option.none, + onRight: Option.some + }) + ) + ]) + ) + ) +) + +/** @internal */ +export const pipeThrough = dual< + ( + sink: Sink.Sink + ) => (self: Stream.Stream) => Stream.Stream, + ( + self: Stream.Stream, + sink: Sink.Sink + ) => Stream.Stream +>( + 2, + ( + self: Stream.Stream, + sink: Sink.Sink + ): Stream.Stream => + new StreamImpl(pipe(toChannel(self), channel.pipeToOrFail(sink_.toChannel(sink)))) +) + +/** @internal */ +export const pipeThroughChannel = dual< + ( + channel: Channel.Channel, Chunk.Chunk, E2, E, unknown, unknown, R2> + ) => (self: Stream.Stream) => Stream.Stream, + ( + self: Stream.Stream, + channel: Channel.Channel, Chunk.Chunk, E2, E, unknown, unknown, R2> + ) => Stream.Stream +>( + 2, + ( + self: Stream.Stream, + channel: Channel.Channel, Chunk.Chunk, E2, E, unknown, unknown, R2> + ): Stream.Stream => new StreamImpl(core.pipeTo(toChannel(self), channel)) +) + +/** @internal */ +export const pipeThroughChannelOrFail = dual< + ( + chan: Channel.Channel, Chunk.Chunk, E2, E, unknown, unknown, R2> + ) => (self: Stream.Stream) => Stream.Stream, + ( + self: Stream.Stream, + chan: Channel.Channel, Chunk.Chunk, E2, E, unknown, unknown, R2> + ) => Stream.Stream +>( + 2, + ( + self: Stream.Stream, + chan: Channel.Channel, Chunk.Chunk, E2, E, unknown, unknown, R2> + ): Stream.Stream => new StreamImpl(pipe(toChannel(self), channel.pipeToOrFail(chan))) +) + +/** @internal */ +export const prepend = dual< + (values: Chunk.Chunk) => (self: Stream.Stream) => Stream.Stream, + (self: Stream.Stream, values: Chunk.Chunk) => Stream.Stream +>(2, (self, values) => + new StreamImpl( + channel.zipRight( + core.write(values as Chunk.Chunk), + toChannel(self) + ) + )) + +/** @internal */ +export const provideContext = dual< + (context: Context.Context) => (self: Stream.Stream) => Stream.Stream, + (self: Stream.Stream, context: Context.Context) => Stream.Stream +>( + 2, + (self: Stream.Stream, context: Context.Context): Stream.Stream => + new StreamImpl(pipe(toChannel(self), core.provideContext(context))) +) + +/** @internal */ +export const provideSomeContext = dual< + (context: Context.Context) => (self: Stream.Stream) => Stream.Stream>, + (self: Stream.Stream, context: Context.Context) => Stream.Stream> +>( + 2, + (self: Stream.Stream, context: Context.Context): Stream.Stream> => + mapInputContext(self as any, Context.merge(context)) +) + +/** @internal */ +export const provideLayer = dual< + ( + layer: Layer.Layer + ) => (self: Stream.Stream) => Stream.Stream, + ( + self: Stream.Stream, + layer: Layer.Layer + ) => Stream.Stream +>( + 2, + ( + self: Stream.Stream, + layer: Layer.Layer + ): Stream.Stream => + new StreamImpl( + channel.unwrapScopedWith((scope) => + Layer.buildWithScope(layer, scope).pipe( + Effect.map((env) => pipe(toChannel(self), core.provideContext(env))) + ) + ) + ) +) + +/** @internal */ +export const provideService = dual< + ( + tag: Context.Tag, + resource: Types.NoInfer + ) => (self: Stream.Stream) => Stream.Stream>, + ( + self: Stream.Stream, + tag: Context.Tag, + resource: Types.NoInfer + ) => Stream.Stream> +>( + 3, + ( + self: Stream.Stream, + tag: Context.Tag, + resource: Types.NoInfer + ) => provideServiceEffect(self, tag, Effect.succeed(resource)) +) + +/** @internal */ +export const provideServiceEffect = dual< + ( + tag: Context.Tag, + effect: Effect.Effect, E2, R2> + ) => (self: Stream.Stream) => Stream.Stream>, + ( + self: Stream.Stream, + tag: Context.Tag, + effect: Effect.Effect, E2, R2> + ) => Stream.Stream> +>( + 3, + ( + self: Stream.Stream, + tag: Context.Tag, + effect: Effect.Effect, E2, R2> + ) => provideServiceStream(self, tag, fromEffect(effect)) +) + +/** @internal */ +export const provideServiceStream = dual< + ( + tag: Context.Tag, + stream: Stream.Stream, E2, R2> + ) => (self: Stream.Stream) => Stream.Stream>, + ( + self: Stream.Stream, + tag: Context.Tag, + stream: Stream.Stream, E2, R2> + ) => Stream.Stream> +>( + 3, + ( + self: Stream.Stream, + tag: Context.Tag, + stream: Stream.Stream, E2, R2> + ): Stream.Stream> => + contextWithStream((env: Context.Context>) => + flatMap( + stream, + (service) => pipe(self, provideContext(Context.add(env, tag, service) as Context.Context)) + ) + ) +) + +/** @internal */ +export const mapInputContext = dual< + ( + f: (env: Context.Context) => Context.Context + ) => (self: Stream.Stream) => Stream.Stream, + ( + self: Stream.Stream, + f: (env: Context.Context) => Context.Context + ) => Stream.Stream +>( + 2, + ( + self: Stream.Stream, + f: (env: Context.Context) => Context.Context + ): Stream.Stream => contextWithStream((env) => pipe(self, provideContext(f(env)))) +) + +/** @internal */ +export const provideSomeLayer = dual< + ( + layer: Layer.Layer + ) => (self: Stream.Stream) => Stream.Stream>, + ( + self: Stream.Stream, + layer: Layer.Layer + ) => Stream.Stream> +>( + 2, + ( + self: Stream.Stream, + layer: Layer.Layer + ): Stream.Stream> => + // @ts-expect-error + // @effect-diagnostics-next-line missingEffectContext:off + pipe( + self, + provideLayer(pipe(Layer.context(), Layer.merge(layer))) + ) +) + +/** @internal */ +export const range = (min: number, max: number, chunkSize = DefaultChunkSize): Stream.Stream => + suspend(() => { + if (min > max) { + return empty as Stream.Stream + } + const go = ( + min: number, + max: number, + chunkSize: number + ): Channel.Channel, unknown, never, unknown, unknown, unknown> => { + const remaining = max - min + 1 + if (remaining > chunkSize) { + return pipe( + core.write(Chunk.range(min, min + chunkSize - 1)), + core.flatMap(() => go(min + chunkSize, max, chunkSize)) + ) + } + return core.write(Chunk.range(min, min + remaining - 1)) + } + return new StreamImpl(go(min, max, chunkSize)) + }) + +/** @internal */ +export const race: { + ( + right: Stream.Stream + ): (left: Stream.Stream) => Stream.Stream + ( + left: Stream.Stream, + right: Stream.Stream + ): Stream.Stream +} = dual( + 2, + ( + left: Stream.Stream, + right: Stream.Stream + ): Stream.Stream => raceAll(left, right) +) + +/** @internal */ +export const raceAll = >>( + ...streams: S +): Stream.Stream< + Stream.Stream.Success, + Stream.Stream.Error, + Stream.Stream.Context +> => + Deferred.make().pipe( + Effect.map((halt) => { + let winner: number | null = null + return mergeAll( + streams.map((stream, index) => + stream.pipe( + takeWhile(() => { + if (winner === null) { + winner = index + Deferred.unsafeDone(halt, Exit.void) + return true + } + return winner === index + }), + interruptWhen( + Deferred.await(halt).pipe( + Effect.flatMap(() => winner === index ? Effect.never : Effect.void) + ) + ) + ) + ), + { concurrency: streams.length } + ) + }), + unwrap + ) + +/** @internal */ +export const rechunk = dual< + (n: number) => (self: Stream.Stream) => Stream.Stream, + (self: Stream.Stream, n: number) => Stream.Stream +>(2, (self: Stream.Stream, n: number): Stream.Stream => + suspend(() => { + const target = Math.max(n, 1) + const process = rechunkProcess(new StreamRechunker(target), target) + return new StreamImpl(pipe(toChannel(self), core.pipeTo(process))) + })) + +/** @internal */ +const rechunkProcess = ( + rechunker: StreamRechunker, + target: number +): Channel.Channel, Chunk.Chunk, E, E, unknown, unknown> => + core.readWithCause({ + onInput: (chunk: Chunk.Chunk) => { + if (chunk.length === target && rechunker.isEmpty()) { + return core.flatMap( + core.write(chunk), + () => rechunkProcess(rechunker, target) + ) + } + if (chunk.length > 0) { + const chunks: Array> = [] + let result: Chunk.Chunk | undefined = undefined + let index = 0 + while (index < chunk.length) { + while (index < chunk.length && result === undefined) { + result = rechunker.write(pipe(chunk, Chunk.unsafeGet(index))) + index = index + 1 + } + if (result !== undefined) { + chunks.push(result) + result = undefined + } + } + return core.flatMap( + channel.writeAll(...chunks), + () => rechunkProcess(rechunker, target) + ) + } + return core.suspend(() => rechunkProcess(rechunker, target)) + }, + onFailure: (cause) => channel.zipRight(rechunker.emitIfNotEmpty(), core.failCause(cause)), + onDone: () => rechunker.emitIfNotEmpty() + }) + +class StreamRechunker { + private builder: Array = [] + private pos = 0 + + constructor(readonly n: number) { + } + + isEmpty(): boolean { + return this.pos === 0 + } + + write(elem: A): Chunk.Chunk | undefined { + this.builder.push(elem) + this.pos += 1 + + if (this.pos === this.n) { + const result = Chunk.unsafeFromArray(this.builder) + this.builder = [] + this.pos = 0 + return result + } + + return undefined + } + + emitIfNotEmpty(): Channel.Channel, unknown, E, E, void, unknown> { + if (this.pos !== 0) { + return core.write(Chunk.unsafeFromArray(this.builder)) + } + return core.void + } +} + +/** @internal */ +export const refineOrDie = dual< + (pf: (error: E) => Option.Option) => (self: Stream.Stream) => Stream.Stream, + (self: Stream.Stream, pf: (error: E) => Option.Option) => Stream.Stream +>( + 2, + (self: Stream.Stream, pf: (error: E) => Option.Option): Stream.Stream => + pipe(self, refineOrDieWith(pf, identity)) +) + +/** @internal */ +export const refineOrDieWith = dual< + ( + pf: (error: E) => Option.Option, + f: (error: E) => unknown + ) => (self: Stream.Stream) => Stream.Stream, + ( + self: Stream.Stream, + pf: (error: E) => Option.Option, + f: (error: E) => unknown + ) => Stream.Stream +>( + 3, + ( + self: Stream.Stream, + pf: (error: E) => Option.Option, + f: (error: E) => unknown + ): Stream.Stream => + new StreamImpl( + channel.catchAll(toChannel(self), (error) => + Option.match(pf(error), { + onNone: () => core.failCause(Cause.die(f(error))), + onSome: core.fail + })) + ) +) + +/** @internal */ +export const repeat = dual< + ( + schedule: Schedule.Schedule + ) => (self: Stream.Stream) => Stream.Stream, + ( + self: Stream.Stream, + schedule: Schedule.Schedule + ) => Stream.Stream +>( + 2, + ( + self: Stream.Stream, + schedule: Schedule.Schedule + ): Stream.Stream => + filterMap( + repeatEither(self, schedule), + (_) => + Either.match(_, { + onLeft: Option.none, + onRight: Option.some + }) + ) +) + +/** @internal */ +export const repeatEffect = (effect: Effect.Effect): Stream.Stream => + repeatEffectOption(pipe(effect, Effect.mapError(Option.some))) + +/** @internal */ +export const repeatEffectChunk = (effect: Effect.Effect, E, R>): Stream.Stream => + repeatEffectChunkOption(pipe(effect, Effect.mapError(Option.some))) + +/** @internal */ +export const repeatEffectChunkOption = ( + effect: Effect.Effect, Option.Option, R> +): Stream.Stream => + unfoldChunkEffect(effect, (effect) => + pipe( + Effect.map(effect, (chunk) => Option.some([chunk, effect] as const)), + Effect.catchAll(Option.match({ + onNone: () => Effect.succeed(Option.none()), + onSome: Effect.fail + })) + )) + +/** @internal */ +export const repeatEffectOption = (effect: Effect.Effect, R>): Stream.Stream => + repeatEffectChunkOption(pipe(effect, Effect.map(Chunk.of))) + +/** @internal */ +export const repeatEither = dual< + ( + schedule: Schedule.Schedule + ) => (self: Stream.Stream) => Stream.Stream, E, R2 | R>, + ( + self: Stream.Stream, + schedule: Schedule.Schedule + ) => Stream.Stream, E, R2 | R> +>( + 2, + ( + self: Stream.Stream, + schedule: Schedule.Schedule + ): Stream.Stream, E, R2 | R> => + repeatWith(self, schedule, { + onElement: (a): Either.Either => Either.right(a), + onSchedule: Either.left + }) +) + +/** @internal */ +export const repeatElements = dual< + ( + schedule: Schedule.Schedule + ) => (self: Stream.Stream) => Stream.Stream, + ( + self: Stream.Stream, + schedule: Schedule.Schedule + ) => Stream.Stream +>( + 2, + ( + self: Stream.Stream, + schedule: Schedule.Schedule + ): Stream.Stream => + filterMap( + repeatElementsWith(self, schedule, { onElement: (a) => Option.some(a), onSchedule: Option.none }), + identity + ) +) + +/** @internal */ +export const repeatElementsWith = dual< + ( + schedule: Schedule.Schedule, + options: { + readonly onElement: (a: A) => C + readonly onSchedule: (b: B) => C + } + ) => (self: Stream.Stream) => Stream.Stream, + ( + self: Stream.Stream, + schedule: Schedule.Schedule, + options: { + readonly onElement: (a: A) => C + readonly onSchedule: (b: B) => C + } + ) => Stream.Stream +>( + 3, + ( + self: Stream.Stream, + schedule: Schedule.Schedule, + options: { + readonly onElement: (a: A) => C + readonly onSchedule: (b: B) => C + } + ): Stream.Stream => { + const driver = pipe( + Schedule.driver(schedule), + Effect.map((driver) => { + const feed = ( + input: Chunk.Chunk + ): Channel.Channel, Chunk.Chunk, E, E, void, unknown, R2> => + Option.match(Chunk.head(input), { + onNone: () => loop, + onSome: (a) => + channel.zipRight( + core.write(Chunk.of(options.onElement(a))), + step(pipe(input, Chunk.drop(1)), a) + ) + }) + const step = ( + input: Chunk.Chunk, + a: A + ): Channel.Channel, Chunk.Chunk, E, E, void, unknown, R2> => { + const advance = pipe( + driver.next(a), + Effect.as(pipe(core.write(Chunk.of(options.onElement(a))), core.flatMap(() => step(input, a)))) + ) + const reset: Effect.Effect< + Channel.Channel, Chunk.Chunk, E, E, void, unknown, R2>, + never, + R2 + > = pipe( + driver.last, + Effect.orDie, + Effect.flatMap((b) => + pipe( + driver.reset, + Effect.map(() => + pipe( + core.write(Chunk.of(options.onSchedule(b))), + channel.zipRight(feed(input)) + ) + ) + ) + ) + ) + return pipe(advance, Effect.orElse(() => reset), channel.unwrap) + } + const loop: Channel.Channel, Chunk.Chunk, E, E, void, unknown, R2> = core.readWith({ + onInput: feed, + onFailure: core.fail, + onDone: () => core.void + }) + return loop + }), + channel.unwrap + ) + return new StreamImpl(pipe(toChannel(self), core.pipeTo(driver))) + } +) + +/** @internal */ +export const repeatValue = (value: A): Stream.Stream => + new StreamImpl( + channel.repeated(core.write(Chunk.of(value))) + ) + +/** @internal */ +export const repeatWith = dual< + ( + schedule: Schedule.Schedule, + options: { + readonly onElement: (a: A) => C + readonly onSchedule: (b: B) => C + } + ) => (self: Stream.Stream) => Stream.Stream, + ( + self: Stream.Stream, + schedule: Schedule.Schedule, + options: { + readonly onElement: (a: A) => C + readonly onSchedule: (b: B) => C + } + ) => Stream.Stream +>( + 3, + ( + self: Stream.Stream, + schedule: Schedule.Schedule, + options: { + readonly onElement: (a: A) => C + readonly onSchedule: (b: B) => C + } + ): Stream.Stream => { + return pipe( + Schedule.driver(schedule), + Effect.map((driver) => { + const provideLastIterationInfo = provideServiceEffect( + Schedule.CurrentIterationMetadata, + Ref.get(driver.iterationMeta) + ) + + const process = pipe(self, provideLastIterationInfo, map(options.onElement), toChannel) + const loop: Channel.Channel, unknown, E, unknown, void, unknown, R | R2> = channel.unwrap( + Effect.match( + driver.next(void 0), + { + onFailure: () => core.void, + onSuccess: (output) => + core.flatMap( + process, + () => channel.zipRight(core.write(Chunk.of(options.onSchedule(output))), loop) + ) + } + ) + ) + return new StreamImpl(channel.zipRight(process, loop)) + }), + unwrap + ) + } +) + +const repeatWithSchedule = ( + value: A, + schedule: Schedule.Schedule +): Stream.Stream => repeatEffectWithSchedule(Effect.succeed(value), schedule) + +/** @internal */ +export const repeatEffectWithSchedule = ( + effect: Effect.Effect, + schedule: Schedule.Schedule +): Stream.Stream => + flatMap( + fromEffect(Effect.zip(effect, Schedule.driver(schedule))), + ([a, driver]) => { + const provideLastIterationInfo = Effect.provideServiceEffect( + Schedule.CurrentIterationMetadata, + Ref.get(driver.iterationMeta) + ) + return concat( + succeed(a), + unfoldEffect(a, (s) => + Effect.matchEffect(driver.next(s as A0), { + onFailure: Effect.succeed, + onSuccess: () => + Effect.map(provideLastIterationInfo(effect), (nextA) => Option.some([nextA, nextA] as const)) + })) + ) + } + ) + +/** @internal */ +export const retry = dual< + ( + policy: Schedule.Schedule, R2> + ) => (self: Stream.Stream) => Stream.Stream, + ( + self: Stream.Stream, + policy: Schedule.Schedule, R2> + ) => Stream.Stream +>( + 2, + ( + self: Stream.Stream, + policy: Schedule.Schedule, R2> + ): Stream.Stream => + Schedule.driver(policy).pipe( + Effect.map((driver) => { + const provideLastIterationInfo = provideServiceEffect( + Schedule.CurrentIterationMetadata, + Ref.get(driver.iterationMeta) + ) + + const loop: Channel.Channel< + Chunk.Chunk, + unknown, + E, + unknown, + unknown, + unknown, + R2 | R + > = toChannel(provideLastIterationInfo(self)).pipe( + channel.mapOutEffect((out) => Effect.as(driver.reset, out)), + channel.catchAll((error) => + driver.next(error).pipe( + Effect.match({ + onFailure: () => core.fail(error), + onSuccess: () => loop + }), + channel.unwrap + ) + ) + ) + return loop + }), + channel.unwrap, + fromChannel + ) +) + +/** @internal */ +export const withExecutionPlan: { + ( + policy: ExecutionPlan<{ + provides: Provides + input: Input + error: PolicyE + requirements: R2 + }>, + options?: { + readonly preventFallbackOnPartialStream?: boolean | undefined + } + ): ( + self: Stream.Stream + ) => Stream.Stream> + ( + self: Stream.Stream, + policy: ExecutionPlan<{ + provides: Provides + input: Input + error: PolicyE + requirements: R2 + }>, + options?: { + readonly preventFallbackOnPartialStream?: boolean | undefined + } + ): Stream.Stream> +} = dual((args) => isStream(args[0]), ( + self: Stream.Stream, + policy: ExecutionPlan<{ + provides: Provides + input: Input + error: PolicyE + requirements: R2 + }>, + options?: { + readonly preventFallbackOnPartialStream?: boolean | undefined + } +): Stream.Stream> => + suspend(() => { + const preventFallbackOnPartialStream = options?.preventFallbackOnPartialStream ?? false + let i = 0 + let lastError = Option.none() + const loop: Stream.Stream< + A, + E | PolicyE, + R2 | Exclude + > = suspend(() => { + const step = policy.steps[i++] + if (!step) { + return fail(Option.getOrThrow(lastError)) + } + + let nextStream: Stream.Stream> = Context.isContext(step.provide) + ? provideSomeContext(self, step.provide) + : provideSomeLayer(self, step.provide as Layer.Layer) + let receivedElements = false + + if (Option.isSome(lastError)) { + const error = lastError.value + let attempted = false + const wrapped = nextStream + // ensure the schedule is applied at least once + nextStream = suspend(() => { + if (attempted) return wrapped + attempted = true + return fail(error) + }) + nextStream = scheduleDefectRefail(retry(nextStream, internalExecutionPlan.scheduleFromStep(step, false)!)) + } else { + const schedule = internalExecutionPlan.scheduleFromStep(step, true) + nextStream = schedule ? scheduleDefectRefail(retry(nextStream, schedule)) : nextStream + } + + return catchAll( + preventFallbackOnPartialStream ? + mapChunks(nextStream, (chunk) => { + receivedElements = true + return chunk + }) : + nextStream, + (error) => { + if (preventFallbackOnPartialStream && receivedElements) { + return fail(error) + } + lastError = Option.some(error) + return loop + } + ) + }) + return loop + })) + +const scheduleDefectRefail = (self: Stream.Stream) => + catchAllCause(self, (cause) => failCause(InternalSchedule.scheduleDefectRefailCause(cause))) + +/** @internal */ +export const run = dual< + ( + sink: Sink.Sink + ) => ( + self: Stream.Stream + ) => Effect.Effect, + ( + self: Stream.Stream, + sink: Sink.Sink + ) => Effect.Effect +>(2, ( + self: Stream.Stream, + sink: Sink.Sink +): Effect.Effect => + toChannel(self).pipe( + channel.pipeToOrFail(sink_.toChannel(sink)), + channel.runDrain + )) + +/** @internal */ +export const runCollect = ( + self: Stream.Stream +): Effect.Effect, E, R> => run(self, sink_.collectAll()) + +/** @internal */ +export const runCount = (self: Stream.Stream): Effect.Effect => run(self, sink_.count) + +/** @internal */ +export const runDrain = (self: Stream.Stream): Effect.Effect => run(self, sink_.drain) + +/** @internal */ +export const runFold = dual< + ( + s: S, + f: (s: S, a: A) => S + ) => ( + self: Stream.Stream + ) => Effect.Effect, + ( + self: Stream.Stream, + s: S, + f: (s: S, a: A) => S + ) => Effect.Effect +>( + 3, + (self: Stream.Stream, s: S, f: (s: S, a: A) => S): Effect.Effect => + runFoldWhile(self, s, constTrue, f) +) + +/** @internal */ +export const runFoldEffect = dual< + ( + s: S, + f: (s: S, a: A) => Effect.Effect + ) => (self: Stream.Stream) => Effect.Effect, + ( + self: Stream.Stream, + s: S, + f: (s: S, a: A) => Effect.Effect + ) => Effect.Effect +>(3, ( + self: Stream.Stream, + s: S, + f: (s: S, a: A) => Effect.Effect +): Effect.Effect => runFoldWhileEffect(self, s, constTrue, f)) + +/** @internal */ +export const runFoldScoped = dual< + (s: S, f: (s: S, a: A) => S) => (self: Stream.Stream) => Effect.Effect, + (self: Stream.Stream, s: S, f: (s: S, a: A) => S) => Effect.Effect +>( + 3, + (self: Stream.Stream, s: S, f: (s: S, a: A) => S): Effect.Effect => + pipe(self, runFoldWhileScoped(s, constTrue, f)) +) + +/** @internal */ +export const runFoldScopedEffect = dual< + ( + s: S, + f: (s: S, a: A) => Effect.Effect + ) => (self: Stream.Stream) => Effect.Effect, + ( + self: Stream.Stream, + s: S, + f: (s: S, a: A) => Effect.Effect + ) => Effect.Effect +>(3, ( + self: Stream.Stream, + s: S, + f: (s: S, a: A) => Effect.Effect +): Effect.Effect => pipe(self, runFoldWhileScopedEffect(s, constTrue, f))) + +/** @internal */ +export const runFoldWhile = dual< + ( + s: S, + cont: Predicate, + f: (s: S, a: A) => S + ) => ( + self: Stream.Stream + ) => Effect.Effect, + ( + self: Stream.Stream, + s: S, + cont: Predicate, + f: (s: S, a: A) => S + ) => Effect.Effect +>(4, ( + self: Stream.Stream, + s: S, + cont: Predicate, + f: (s: S, a: A) => S +): Effect.Effect => run(self, sink_.fold(s, cont, f))) + +/** @internal */ +export const runFoldWhileEffect = dual< + ( + s: S, + cont: Predicate, + f: (s: S, a: A) => Effect.Effect + ) => ( + self: Stream.Stream + ) => Effect.Effect, + ( + self: Stream.Stream, + s: S, + cont: Predicate, + f: (s: S, a: A) => Effect.Effect + ) => Effect.Effect +>(4, ( + self: Stream.Stream, + s: S, + cont: Predicate, + f: (s: S, a: A) => Effect.Effect +): Effect.Effect => run(self, sink_.foldEffect(s, cont, f))) + +/** @internal */ +export const runFoldWhileScoped = dual< + ( + s: S, + cont: Predicate, + f: (s: S, a: A) => S + ) => (self: Stream.Stream) => Effect.Effect, + ( + self: Stream.Stream, + s: S, + cont: Predicate, + f: (s: S, a: A) => S + ) => Effect.Effect +>(4, ( + self: Stream.Stream, + s: S, + cont: Predicate, + f: (s: S, a: A) => S +): Effect.Effect => pipe(self, runScoped(sink_.fold(s, cont, f)))) + +/** @internal */ +export const runFoldWhileScopedEffect = dual< + ( + s: S, + cont: Predicate, + f: (s: S, a: A) => Effect.Effect + ) => (self: Stream.Stream) => Effect.Effect, + ( + self: Stream.Stream, + s: S, + cont: Predicate, + f: (s: S, a: A) => Effect.Effect + ) => Effect.Effect +>(4, ( + self: Stream.Stream, + s: S, + cont: Predicate, + f: (s: S, a: A) => Effect.Effect +): Effect.Effect => pipe(self, runScoped(sink_.foldEffect(s, cont, f)))) + +/** @internal */ +export const runForEach = dual< + ( + f: (a: A) => Effect.Effect + ) => ( + self: Stream.Stream + ) => Effect.Effect, + ( + self: Stream.Stream, + f: (a: A) => Effect.Effect + ) => Effect.Effect +>(2, ( + self: Stream.Stream, + f: (a: A) => Effect.Effect +): Effect.Effect => run(self, sink_.forEach(f))) + +/** @internal */ +export const runForEachChunk = dual< + ( + f: (a: Chunk.Chunk) => Effect.Effect + ) => ( + self: Stream.Stream + ) => Effect.Effect, + ( + self: Stream.Stream, + f: (a: Chunk.Chunk) => Effect.Effect + ) => Effect.Effect +>(2, ( + self: Stream.Stream, + f: (a: Chunk.Chunk) => Effect.Effect +): Effect.Effect => run(self, sink_.forEachChunk(f))) + +/** @internal */ +export const runForEachChunkScoped = dual< + ( + f: (a: Chunk.Chunk) => Effect.Effect + ) => (self: Stream.Stream) => Effect.Effect, + ( + self: Stream.Stream, + f: (a: Chunk.Chunk) => Effect.Effect + ) => Effect.Effect +>(2, ( + self: Stream.Stream, + f: (a: Chunk.Chunk) => Effect.Effect +): Effect.Effect => pipe(self, runScoped(sink_.forEachChunk(f)))) + +/** @internal */ +export const runForEachScoped = dual< + ( + f: (a: A) => Effect.Effect + ) => (self: Stream.Stream) => Effect.Effect, + ( + self: Stream.Stream, + f: (a: A) => Effect.Effect + ) => Effect.Effect +>(2, ( + self: Stream.Stream, + f: (a: A) => Effect.Effect +): Effect.Effect => pipe(self, runScoped(sink_.forEach(f)))) + +/** @internal */ +export const runForEachWhile = dual< + ( + f: (a: A) => Effect.Effect + ) => ( + self: Stream.Stream + ) => Effect.Effect, + ( + self: Stream.Stream, + f: (a: A) => Effect.Effect + ) => Effect.Effect +>(2, ( + self: Stream.Stream, + f: (a: A) => Effect.Effect +): Effect.Effect => run(self, sink_.forEachWhile(f))) + +/** @internal */ +export const runForEachWhileScoped = dual< + ( + f: (a: A) => Effect.Effect + ) => (self: Stream.Stream) => Effect.Effect, + ( + self: Stream.Stream, + f: (a: A) => Effect.Effect + ) => Effect.Effect +>(2, ( + self: Stream.Stream, + f: (a: A) => Effect.Effect +): Effect.Effect => pipe(self, runScoped(sink_.forEachWhile(f)))) + +/** @internal */ +export const runHead = ( + self: Stream.Stream +): Effect.Effect, E, R> => run(self, sink_.head()) + +/** @internal */ +export const runIntoPubSub = dual< + ( + pubsub: PubSub.PubSub> + ) => (self: Stream.Stream) => Effect.Effect>, + ( + self: Stream.Stream, + pubsub: PubSub.PubSub> + ) => Effect.Effect> +>( + 2, + ( + self: Stream.Stream, + pubsub: PubSub.PubSub> + ): Effect.Effect> => pipe(self, runIntoQueue(pubsub)) +) + +/** @internal */ +export const runIntoPubSubScoped = dual< + ( + pubsub: PubSub.PubSub> + ) => (self: Stream.Stream) => Effect.Effect, + ( + self: Stream.Stream, + pubsub: PubSub.PubSub> + ) => Effect.Effect +>(2, ( + self: Stream.Stream, + pubsub: PubSub.PubSub> +): Effect.Effect => pipe(self, runIntoQueueScoped(pubsub))) + +/** @internal */ +export const runIntoQueue = dual< + ( + queue: Queue.Enqueue> + ) => (self: Stream.Stream) => Effect.Effect>, + ( + self: Stream.Stream, + queue: Queue.Enqueue> + ) => Effect.Effect> +>( + 2, + ( + self: Stream.Stream, + queue: Queue.Enqueue> + ): Effect.Effect> => pipe(self, runIntoQueueScoped(queue), Effect.scoped) +) + +/** @internal */ +export const runIntoQueueElementsScoped = dual< + ( + queue: Queue.Enqueue>> + ) => (self: Stream.Stream) => Effect.Effect, + ( + self: Stream.Stream, + queue: Queue.Enqueue>> + ) => Effect.Effect +>(2, ( + self: Stream.Stream, + queue: Queue.Enqueue>> +): Effect.Effect => { + const writer: Channel.Channel>, Chunk.Chunk, never, E, unknown, unknown, R> = core + .readWithCause({ + onInput: (input: Chunk.Chunk) => + core.flatMap( + core.fromEffect(Queue.offerAll(queue, Chunk.map(input, Exit.succeed))), + () => writer + ), + onFailure: (cause) => core.fromEffect(Queue.offer(queue, Exit.failCause(Cause.map(cause, Option.some)))), + onDone: () => core.fromEffect(Queue.offer(queue, Exit.fail(Option.none()))) + }) + return pipe( + core.pipeTo(toChannel(self), writer), + channel.drain, + channel.runScoped, + Effect.asVoid + ) +}) + +/** @internal */ +export const runIntoQueueScoped = dual< + ( + queue: Queue.Enqueue> + ) => (self: Stream.Stream) => Effect.Effect, + ( + self: Stream.Stream, + queue: Queue.Enqueue> + ) => Effect.Effect +>(2, ( + self: Stream.Stream, + queue: Queue.Enqueue> +): Effect.Effect => { + const writer: Channel.Channel, Chunk.Chunk, never, E, unknown, unknown, R> = core + .readWithCause({ + onInput: (input: Chunk.Chunk) => core.flatMap(core.write(InternalTake.chunk(input)), () => writer), + onFailure: (cause) => core.write(InternalTake.failCause(cause)), + onDone: () => core.write(InternalTake.end) + }) + return pipe( + core.pipeTo(toChannel(self), writer), + channel.mapOutEffect((take) => Queue.offer(queue, take)), + channel.drain, + channel.runScoped, + Effect.asVoid + ) +}) + +/** @internal */ +export const runLast = ( + self: Stream.Stream +): Effect.Effect, E, R> => run(self, sink_.last()) + +/** @internal */ +export const runScoped = dual< + ( + sink: Sink.Sink + ) => (self: Stream.Stream) => Effect.Effect, + ( + self: Stream.Stream, + sink: Sink.Sink + ) => Effect.Effect +>(2, ( + self: Stream.Stream, + sink: Sink.Sink +): Effect.Effect => + pipe( + toChannel(self), + channel.pipeToOrFail(sink_.toChannel(sink)), + channel.drain, + channel.runScoped + )) + +/** @internal */ +export const runSum = (self: Stream.Stream): Effect.Effect => run(self, sink_.sum) + +/** @internal */ +export const scan = dual< + (s: S, f: (s: S, a: A) => S) => (self: Stream.Stream) => Stream.Stream, + (self: Stream.Stream, s: S, f: (s: S, a: A) => S) => Stream.Stream +>( + 3, + (self: Stream.Stream, s: S, f: (s: S, a: A) => S): Stream.Stream => + pipe(self, scanEffect(s, (s, a) => Effect.succeed(f(s, a)))) +) + +/** @internal */ +export const scanReduce = dual< + (f: (a2: A2 | A, a: A) => A2) => (self: Stream.Stream) => Stream.Stream, + (self: Stream.Stream, f: (a2: A2 | A, a: A) => A2) => Stream.Stream +>( + 2, + (self: Stream.Stream, f: (a2: A | A2, a: A) => A2): Stream.Stream => + pipe(self, scanReduceEffect((a2, a) => Effect.succeed(f(a2, a)))) +) + +/** @internal */ +export const scanReduceEffect = dual< + ( + f: (a2: A2 | A, a: A) => Effect.Effect + ) => (self: Stream.Stream) => Stream.Stream, + ( + self: Stream.Stream, + f: (a2: A2 | A, a: A) => Effect.Effect + ) => Stream.Stream +>( + 2, + ( + self: Stream.Stream, + f: (a2: A | A2, a: A) => Effect.Effect + ): Stream.Stream => + pipe( + self, + mapAccumEffect, A, A | A2, E2, R2>(Option.none() as Option.Option, (option, a) => { + switch (option._tag) { + case "None": { + return Effect.succeed([Option.some(a), a] as const) + } + case "Some": { + return pipe( + f(option.value, a), + Effect.map((b) => [Option.some(b), b] as const) + ) + } + } + }) + ) +) + +/** @internal */ +export const schedule = dual< + ( + schedule: Schedule.Schedule + ) => (self: Stream.Stream) => Stream.Stream, + ( + self: Stream.Stream, + schedule: Schedule.Schedule + ) => Stream.Stream +>( + 2, + ( + self: Stream.Stream, + schedule: Schedule.Schedule + ): Stream.Stream => + filterMap( + scheduleWith(self, schedule, { onElement: Option.some, onSchedule: Option.none }), + identity + ) +) + +/** @internal */ +export const scheduleWith = dual< + ( + schedule: Schedule.Schedule, + options: { + readonly onElement: (a: A) => C + readonly onSchedule: (b: B) => C + } + ) => (self: Stream.Stream) => Stream.Stream, + ( + self: Stream.Stream, + schedule: Schedule.Schedule, + options: { + readonly onElement: (a: A) => C + readonly onSchedule: (b: B) => C + } + ) => Stream.Stream +>( + 3, + ( + self: Stream.Stream, + schedule: Schedule.Schedule, + options: { + readonly onElement: (a: A) => C + readonly onSchedule: (b: B) => C + } + ): Stream.Stream => { + const loop = ( + driver: Schedule.ScheduleDriver, + iterator: Iterator + ): Channel.Channel, Chunk.Chunk, E, E, unknown, unknown, R2> => { + const next = iterator.next() + if (next.done) { + return core.readWithCause({ + onInput: (chunk: Chunk.Chunk) => loop(driver, chunk[Symbol.iterator]()), + onFailure: core.failCause, + onDone: core.succeedNow + }) + } + return channel.unwrap( + Effect.matchEffect(driver.next(next.value as A0), { + onFailure: () => + pipe( + driver.last, + Effect.orDie, + Effect.map((b) => + pipe( + core.write(Chunk.make(options.onElement(next.value), options.onSchedule(b))), + core.flatMap(() => loop(driver, iterator)) + ) + ), + Effect.zipLeft(driver.reset) + ), + onSuccess: () => + Effect.succeed(pipe( + core.write(Chunk.of(options.onElement(next.value))), + core.flatMap(() => loop(driver, iterator)) + )) + }) + ) + } + return new StreamImpl( + pipe( + core.fromEffect(Schedule.driver(schedule)), + core.flatMap((driver) => + pipe( + toChannel(self), + core.pipeTo(loop(driver, Chunk.empty()[Symbol.iterator]())) + ) + ) + ) + ) + } +) + +/** @internal */ +export const scanEffect = dual< + ( + s: S, + f: (s: S, a: A) => Effect.Effect + ) => (self: Stream.Stream) => Stream.Stream, + ( + self: Stream.Stream, + s: S, + f: (s: S, a: A) => Effect.Effect + ) => Stream.Stream +>( + 3, + ( + self: Stream.Stream, + s: S, + f: (s: S, a: A) => Effect.Effect + ): Stream.Stream => + new StreamImpl( + pipe( + core.write(Chunk.of(s)), + core.flatMap(() => + toChannel(pipe( + self, + mapAccumEffect(s, (s, a) => pipe(f(s, a), Effect.map((s) => [s, s]))) + )) + ) + ) + ) +) + +/** @internal */ +export const scoped = ( + effect: Effect.Effect +): Stream.Stream> => + new StreamImpl(channel.ensuring(channel.scoped(pipe(effect, Effect.map(Chunk.of))), Effect.void)) + +/** @internal */ +export const scopedWith = ( + f: (scope: Scope.Scope) => Effect.Effect +): Stream.Stream => + new StreamImpl(channel.scopedWith((scope) => + f(scope).pipe( + Effect.map(Chunk.of) + ) + )) + +/** @internal */ +export const some = (self: Stream.Stream, E, R>): Stream.Stream, R> => + pipe(self, mapError(Option.some), someOrFail(() => Option.none())) + +/** @internal */ +export const someOrElse = dual< + (fallback: LazyArg) => (self: Stream.Stream, E, R>) => Stream.Stream, + (self: Stream.Stream, E, R>, fallback: LazyArg) => Stream.Stream +>( + 2, + (self: Stream.Stream, E, R>, fallback: LazyArg): Stream.Stream => + pipe(self, map(Option.getOrElse(fallback))) +) + +/** @internal */ +export const someOrFail = dual< + (error: LazyArg) => (self: Stream.Stream, E, R>) => Stream.Stream, + (self: Stream.Stream, E, R>, error: LazyArg) => Stream.Stream +>( + 2, + (self: Stream.Stream, E, R>, error: LazyArg): Stream.Stream => + mapEffectSequential( + self, + Option.match({ + onNone: () => Effect.failSync(error), + onSome: Effect.succeed + }) + ) +) + +/** @internal */ +export const sliding = dual< + ( + chunkSize: number + ) => (self: Stream.Stream) => Stream.Stream, E, R>, + (self: Stream.Stream, chunkSize: number) => Stream.Stream, E, R> +>( + 2, + (self: Stream.Stream, chunkSize: number): Stream.Stream, E, R> => + slidingSize(self, chunkSize, 1) +) + +/** @internal */ +export const slidingSize = dual< + ( + chunkSize: number, + stepSize: number + ) => (self: Stream.Stream) => Stream.Stream, E, R>, + (self: Stream.Stream, chunkSize: number, stepSize: number) => Stream.Stream, E, R> +>( + 3, + (self: Stream.Stream, chunkSize: number, stepSize: number): Stream.Stream, E, R> => { + if (chunkSize <= 0 || stepSize <= 0) { + return die( + new Cause.IllegalArgumentException("Invalid bounds - `chunkSize` and `stepSize` must be greater than zero") + ) + } + return new StreamImpl(core.suspend(() => { + const queue = new RingBuffer(chunkSize) + const emitOnStreamEnd = ( + queueSize: number, + channelEnd: Channel.Channel>, Chunk.Chunk, E, E, unknown, unknown> + ) => { + if (queueSize < chunkSize) { + const items = queue.toChunk() + const result = Chunk.isEmpty(items) ? Chunk.empty>() : Chunk.of(items) + return pipe(core.write(result), core.flatMap(() => channelEnd)) + } + const lastEmitIndex = queueSize - (queueSize - chunkSize) % stepSize + if (lastEmitIndex === queueSize) { + return channelEnd + } + const leftovers = queueSize - (lastEmitIndex - chunkSize + stepSize) + const lastItems = pipe(queue.toChunk(), Chunk.takeRight(leftovers)) + const result = Chunk.isEmpty(lastItems) ? Chunk.empty>() : Chunk.of(lastItems) + return pipe(core.write(result), core.flatMap(() => channelEnd)) + } + const reader = ( + queueSize: number + ): Channel.Channel>, Chunk.Chunk, E, E, unknown, unknown> => + core.readWithCause({ + onInput: (input: Chunk.Chunk) => + core.flatMap( + core.write( + Chunk.filterMap(input, (element, index) => { + queue.put(element) + const currentIndex = queueSize + index + 1 + if (currentIndex < chunkSize || (currentIndex - chunkSize) % stepSize > 0) { + return Option.none() + } + return Option.some(queue.toChunk()) + }) + ), + () => reader(queueSize + input.length) + ), + onFailure: (cause) => emitOnStreamEnd(queueSize, core.failCause(cause)), + onDone: () => emitOnStreamEnd(queueSize, core.void) + }) + return pipe(toChannel(self), core.pipeTo(reader(0))) + })) + } +) + +/** @internal */ +export const split: { + ( + refinement: Refinement, B> + ): (self: Stream.Stream) => Stream.Stream>, E, R> + ( + predicate: Predicate> + ): (self: Stream.Stream) => Stream.Stream, E, R> + ( + self: Stream.Stream, + refinement: Refinement + ): Stream.Stream>, E, R> + (self: Stream.Stream, predicate: Predicate): Stream.Stream, E, R> +} = dual( + 2, + ( + self: Stream.Stream, + predicate: Predicate + ): Stream.Stream, E, R> => { + const split = ( + leftovers: Chunk.Chunk, + input: Chunk.Chunk + ): Channel.Channel>, Chunk.Chunk, E, E, unknown, unknown, R> => { + const [chunk, remaining] = pipe(leftovers, Chunk.appendAll(input), Chunk.splitWhere(predicate)) + if (Chunk.isEmpty(chunk) || Chunk.isEmpty(remaining)) { + return loop(pipe(chunk, Chunk.appendAll(pipe(remaining, Chunk.drop(1))))) + } + return pipe( + core.write(Chunk.of(chunk)), + core.flatMap(() => split(Chunk.empty(), pipe(remaining, Chunk.drop(1)))) + ) + } + const loop = ( + leftovers: Chunk.Chunk + ): Channel.Channel>, Chunk.Chunk, E, E, unknown, unknown, R> => + core.readWith({ + onInput: (input: Chunk.Chunk) => split(leftovers, input), + onFailure: core.fail, + onDone: () => { + if (Chunk.isEmpty(leftovers)) { + return core.void + } + if (Option.isNone(pipe(leftovers, Chunk.findFirst(predicate)))) { + return channel.zipRight(core.write(Chunk.of(leftovers)), core.void) + } + return channel.zipRight( + split(Chunk.empty(), leftovers), + core.void + ) + } + }) + return new StreamImpl(pipe(toChannel(self), core.pipeTo(loop(Chunk.empty())))) + } +) + +/** @internal */ +export const splitOnChunk = dual< + (delimiter: Chunk.Chunk) => (self: Stream.Stream) => Stream.Stream, E, R>, + (self: Stream.Stream, delimiter: Chunk.Chunk) => Stream.Stream, E, R> +>(2, (self: Stream.Stream, delimiter: Chunk.Chunk): Stream.Stream, E, R> => { + const next = ( + leftover: Option.Option>, + delimiterIndex: number + ): Channel.Channel>, Chunk.Chunk, E, E, unknown, unknown, R> => + core.readWithCause({ + onInput: (inputChunk: Chunk.Chunk) => { + let buffer: Array> | undefined + const [carry, delimiterCursor] = pipe( + inputChunk, + Chunk.reduce( + [pipe(leftover, Option.getOrElse(() => Chunk.empty())), delimiterIndex] as const, + ([carry, delimiterCursor], a) => { + const concatenated = pipe(carry, Chunk.append(a)) + if ( + delimiterCursor < delimiter.length && + Equal.equals(a, pipe(delimiter, Chunk.unsafeGet(delimiterCursor))) + ) { + if (delimiterCursor + 1 === delimiter.length) { + if (buffer === undefined) { + buffer = [] + } + buffer.push(pipe(concatenated, Chunk.take(concatenated.length - delimiter.length))) + return [Chunk.empty(), 0] as const + } + return [concatenated, delimiterCursor + 1] as const + } + return [concatenated, Equal.equals(a, pipe(delimiter, Chunk.unsafeGet(0))) ? 1 : 0] as const + } + ) + ) + const output = buffer === undefined ? Chunk.empty>() : Chunk.unsafeFromArray(buffer) + return core.flatMap( + core.write(output), + () => next(Chunk.isNonEmpty(carry) ? Option.some(carry) : Option.none(), delimiterCursor) + ) + }, + onFailure: (cause) => + Option.match(leftover, { + onNone: () => core.failCause(cause), + onSome: (chunk) => + channel.zipRight( + core.write(Chunk.of(chunk)), + core.failCause(cause) + ) + }), + onDone: (done) => + Option.match(leftover, { + onNone: () => core.succeed(done), + onSome: (chunk) => channel.zipRight(core.write(Chunk.of(chunk)), core.succeed(done)) + }) + }) + return new StreamImpl(pipe(toChannel(self), core.pipeTo(next(Option.none(), 0)))) +}) + +/** @internal */ +export const splitLines = (self: Stream.Stream): Stream.Stream => + pipeThroughChannel(self, channel.splitLines()) + +/** @internal */ +export const succeed = (value: A): Stream.Stream => fromChunk(Chunk.of(value)) + +/** @internal */ +export const sync = (evaluate: LazyArg): Stream.Stream => suspend(() => fromChunk(Chunk.of(evaluate()))) + +/** @internal */ +export const suspend = (stream: LazyArg>): Stream.Stream => + new StreamImpl(core.suspend(() => toChannel(stream()))) + +/** @internal */ +export const take = dual< + (n: number) => (self: Stream.Stream) => Stream.Stream, + (self: Stream.Stream, n: number) => Stream.Stream +>(2, (self: Stream.Stream, n: number): Stream.Stream => { + if (!Number.isInteger(n)) { + return die(new Cause.IllegalArgumentException(`${n} must be an integer`)) + } + const loop = (n: number): Channel.Channel, Chunk.Chunk, never, never, unknown, unknown> => + core.readWith({ + onInput: (input: Chunk.Chunk) => { + const taken = pipe(input, Chunk.take(Math.min(n, Number.POSITIVE_INFINITY))) + const leftover = Math.max(0, n - taken.length) + const more = leftover > 0 + if (more) { + return pipe(core.write(taken), core.flatMap(() => loop(leftover))) + } + return core.write(taken) + }, + onFailure: core.fail, + onDone: core.succeed + }) + return new StreamImpl( + pipe( + toChannel(self), + channel.pipeToOrFail(0 < n ? loop(n) : core.void) + ) + ) +}) + +/** @internal */ +export const takeRight = dual< + (n: number) => (self: Stream.Stream) => Stream.Stream, + (self: Stream.Stream, n: number) => Stream.Stream +>(2, (self: Stream.Stream, n: number): Stream.Stream => { + if (n <= 0) { + return empty + } + return new StreamImpl( + pipe( + Effect.succeed(new RingBuffer(n)), + Effect.map((queue) => { + const reader: Channel.Channel, Chunk.Chunk, E, E, void, unknown> = core.readWith({ + onInput: (input: Chunk.Chunk) => { + for (const element of input) { + queue.put(element) + } + return reader + }, + onFailure: core.fail, + onDone: () => pipe(core.write(queue.toChunk()), channel.zipRight(core.void)) + }) + return pipe(toChannel(self), core.pipeTo(reader)) + }), + channel.unwrap + ) + ) +}) + +/** @internal */ +export const takeUntil: { + (predicate: Predicate>): (self: Stream.Stream) => Stream.Stream + (self: Stream.Stream, predicate: Predicate): Stream.Stream +} = dual(2, (self: Stream.Stream, predicate: Predicate): Stream.Stream => { + const loop: Channel.Channel, Chunk.Chunk, never, never, unknown, unknown> = core.readWith({ + onInput: (input: Chunk.Chunk) => { + const taken = pipe(input, Chunk.takeWhile((a) => !predicate(a))) + const last = pipe(input, Chunk.drop(taken.length), Chunk.take(1)) + if (Chunk.isEmpty(last)) { + return pipe(core.write(taken), core.flatMap(() => loop)) + } + return core.write(pipe(taken, Chunk.appendAll(last))) + }, + onFailure: core.fail, + onDone: core.succeed + }) + return new StreamImpl(pipe(toChannel(self), channel.pipeToOrFail(loop))) +}) + +/** @internal */ +export const takeUntilEffect: { + ( + predicate: (a: Types.NoInfer) => Effect.Effect + ): (self: Stream.Stream) => Stream.Stream + ( + self: Stream.Stream, + predicate: (a: A) => Effect.Effect + ): Stream.Stream +} = dual( + 2, + ( + self: Stream.Stream, + predicate: (a: A) => Effect.Effect + ): Stream.Stream => { + const loop = ( + iterator: Iterator + ): Channel.Channel, Chunk.Chunk, E | E2, E, unknown, unknown, R2> => { + const next = iterator.next() + if (next.done) { + return core.readWithCause({ + onInput: (elem) => loop(elem[Symbol.iterator]()), + onFailure: core.failCause, + onDone: core.succeed + }) + } + return pipe( + predicate(next.value), + Effect.map((bool) => + bool ? + core.write(Chunk.of(next.value)) : + pipe( + core.write(Chunk.of(next.value)), + core.flatMap(() => loop(iterator)) + ) + ), + channel.unwrap + ) + } + return new StreamImpl(pipe(toChannel(self), core.pipeTo(loop(Chunk.empty()[Symbol.iterator]())))) + } +) + +/** @internal */ +export const takeWhile: { + ( + refinement: Refinement, B> + ): (self: Stream.Stream) => Stream.Stream + (predicate: Predicate>): (self: Stream.Stream) => Stream.Stream + (self: Stream.Stream, refinement: Refinement): Stream.Stream + (self: Stream.Stream, predicate: Predicate): Stream.Stream +} = dual(2, (self: Stream.Stream, predicate: Predicate): Stream.Stream => { + const loop: Channel.Channel, Chunk.Chunk, never, never, unknown, unknown> = core.readWith({ + onInput: (input: Chunk.Chunk) => { + const taken = pipe(input, Chunk.takeWhile(predicate)) + const more = taken.length === input.length + if (more) { + return pipe(core.write(taken), core.flatMap(() => loop)) + } + return core.write(taken) + }, + onFailure: core.fail, + onDone: core.succeed + }) + return new StreamImpl(pipe(toChannel(self), channel.pipeToOrFail(loop))) +}) + +/** @internal */ +export const tap: { + ( + f: (a: Types.NoInfer) => Effect.Effect + ): (self: Stream.Stream) => Stream.Stream + ( + self: Stream.Stream, + f: (a: Types.NoInfer) => Effect.Effect + ): Stream.Stream +} = dual( + 2, + ( + self: Stream.Stream, + f: (a: Types.NoInfer) => Effect.Effect + ): Stream.Stream => mapEffectSequential(self, (a) => Effect.as(f(a), a)) +) + +/** @internal */ +export const tapBoth: { + ( + options: { + readonly onFailure: (e: Types.NoInfer) => Effect.Effect + readonly onSuccess: (a: Types.NoInfer) => Effect.Effect + } + ): (self: Stream.Stream) => Stream.Stream + ( + self: Stream.Stream, + options: { + readonly onFailure: (e: Types.NoInfer) => Effect.Effect + readonly onSuccess: (a: Types.NoInfer) => Effect.Effect + } + ): Stream.Stream +} = dual( + 2, + ( + self: Stream.Stream, + options: { + readonly onFailure: (e: Types.NoInfer) => Effect.Effect + readonly onSuccess: (a: Types.NoInfer) => Effect.Effect + } + ): Stream.Stream => pipe(self, tapError(options.onFailure), tap(options.onSuccess)) +) + +/** @internal */ +export const tapError: { + ( + f: (error: Types.NoInfer) => Effect.Effect + ): (self: Stream.Stream) => Stream.Stream + ( + self: Stream.Stream, + f: (error: E) => Effect.Effect + ): Stream.Stream +} = dual( + 2, + ( + self: Stream.Stream, + f: (error: E) => Effect.Effect + ): Stream.Stream => + catchAll(self, (error) => fromEffect(Effect.zipRight(f(error), Effect.fail(error)))) +) + +/** @internal */ +export const tapErrorCause: { + ( + f: (cause: Cause.Cause>) => Effect.Effect + ): (self: Stream.Stream) => Stream.Stream + ( + self: Stream.Stream, + f: (cause: Cause.Cause) => Effect.Effect + ): Stream.Stream +} = dual( + 2, + ( + self: Stream.Stream, + f: (cause: Cause.Cause) => Effect.Effect + ): Stream.Stream => { + const loop: Channel.Channel, Chunk.Chunk, E | E2, E, unknown, unknown, R | R2> = core + .readWithCause({ + onInput: (chunk) => core.flatMap(core.write(chunk), () => loop), + onFailure: (cause) => core.fromEffect(Effect.zipRight(f(cause), Effect.failCause(cause))), + onDone: core.succeedNow + }) + + return new StreamImpl(pipe(toChannel(self), core.pipeTo(loop))) + } +) + +/** @internal */ +export const tapSink = dual< + ( + sink: Sink.Sink + ) => (self: Stream.Stream) => Stream.Stream, + ( + self: Stream.Stream, + sink: Sink.Sink + ) => Stream.Stream +>( + 2, + ( + self: Stream.Stream, + sink: Sink.Sink + ): Stream.Stream => + pipe( + fromEffect(Effect.all([Queue.bounded>(1), Deferred.make()])), + flatMap(([queue, deferred]) => { + const right = flattenTake(fromQueue(queue, { maxChunkSize: 1 })) + const loop: Channel.Channel, Chunk.Chunk, E | E2, E, unknown, unknown, R2> = core + .readWithCause({ + onInput: (chunk: Chunk.Chunk) => + pipe( + core.fromEffect(Queue.offer(queue, InternalTake.chunk(chunk))), + core.foldCauseChannel({ + onFailure: () => core.flatMap(core.write(chunk), () => channel.identityChannel()), + onSuccess: () => core.flatMap(core.write(chunk), () => loop) + }) + ) as Channel.Channel, Chunk.Chunk, E | E2, E, unknown, unknown, R2>, + onFailure: (cause: Cause.Cause) => + pipe( + core.fromEffect(Queue.offer(queue, InternalTake.failCause(cause))), + core.foldCauseChannel({ + onFailure: () => core.failCause(cause), + onSuccess: () => core.failCause(cause) + }) + ), + onDone: () => + pipe( + core.fromEffect(Queue.offer(queue, InternalTake.end)), + core.foldCauseChannel({ + onFailure: () => core.void, + onSuccess: () => core.void + }) + ) + }) + return pipe( + new StreamImpl(pipe( + core.pipeTo(toChannel(self), loop), + channel.ensuring(Effect.zipRight( + Effect.forkDaemon(Queue.offer(queue, InternalTake.end)), + Deferred.await(deferred) + )) + )), + merge( + execute(pipe( + run(right, sink), + Effect.ensuring(Effect.zipRight( + Queue.shutdown(queue), + Deferred.succeed(deferred, void 0) + )) + )) + ) + ) + }) + ) +) + +/** @internal */ +export const throttle = dual< + ( + options: { + readonly cost: (chunk: Chunk.Chunk) => number + readonly units: number + readonly duration: Duration.DurationInput + readonly burst?: number | undefined + readonly strategy?: "enforce" | "shape" | undefined + } + ) => (self: Stream.Stream) => Stream.Stream, + ( + self: Stream.Stream, + options: { + readonly cost: (chunk: Chunk.Chunk) => number + readonly units: number + readonly duration: Duration.DurationInput + readonly burst?: number | undefined + readonly strategy?: "enforce" | "shape" | undefined + } + ) => Stream.Stream +>( + 2, + ( + self: Stream.Stream, + options: { + readonly cost: (chunk: Chunk.Chunk) => number + readonly units: number + readonly duration: Duration.DurationInput + readonly burst?: number | undefined + readonly strategy?: "enforce" | "shape" | undefined + } + ): Stream.Stream => + throttleEffect(self, { + ...options, + cost: (chunk) => Effect.succeed(options.cost(chunk)) + }) +) + +/** @internal */ +export const throttleEffect = dual< + ( + options: { + readonly cost: (chunk: Chunk.Chunk) => Effect.Effect + readonly units: number + readonly duration: Duration.DurationInput + readonly burst?: number | undefined + readonly strategy?: "enforce" | "shape" | undefined + } + ) => (self: Stream.Stream) => Stream.Stream, + ( + self: Stream.Stream, + options: { + readonly cost: (chunk: Chunk.Chunk) => Effect.Effect + readonly units: number + readonly duration: Duration.DurationInput + readonly burst?: number | undefined + readonly strategy?: "enforce" | "shape" | undefined + } + ) => Stream.Stream +>( + 2, + ( + self: Stream.Stream, + options: { + readonly cost: (chunk: Chunk.Chunk) => Effect.Effect + readonly units: number + readonly duration: Duration.DurationInput + readonly burst?: number | undefined + readonly strategy?: "enforce" | "shape" | undefined + } + ): Stream.Stream => { + if (options.strategy === "enforce") { + return throttleEnforceEffect(self, options.cost, options.units, options.duration, options.burst ?? 0) + } + return throttleShapeEffect(self, options.cost, options.units, options.duration, options.burst ?? 0) + } +) + +const throttleEnforceEffect = ( + self: Stream.Stream, + cost: (chunk: Chunk.Chunk) => Effect.Effect, + units: number, + duration: Duration.DurationInput, + burst: number +): Stream.Stream => { + const loop = ( + tokens: number, + timestampMillis: number + ): Channel.Channel, Chunk.Chunk, E | E2, E, void, unknown, R2> => + core.readWithCause({ + onInput: (input: Chunk.Chunk) => + pipe( + cost(input), + Effect.zip(Clock.currentTimeMillis), + Effect.map(([weight, currentTimeMillis]) => { + const elapsed = currentTimeMillis - timestampMillis + const cycles = elapsed / Duration.toMillis(duration) + const sum = tokens + (cycles * units) + const max = units + burst < 0 ? Number.POSITIVE_INFINITY : units + burst + const available = sum < 0 ? max : Math.min(sum, max) + if (weight <= available) { + return pipe( + core.write(input), + core.flatMap(() => loop(available - weight, currentTimeMillis)) + ) + } + return loop(tokens, timestampMillis) + }), + channel.unwrap + ), + onFailure: core.failCause, + onDone: () => core.void + }) + const throttled = pipe( + Clock.currentTimeMillis, + Effect.map((currentTimeMillis) => loop(units, currentTimeMillis)), + channel.unwrap + ) + return new StreamImpl(pipe(toChannel(self), channel.pipeToOrFail(throttled))) +} + +const throttleShapeEffect = ( + self: Stream.Stream, + costFn: (chunk: Chunk.Chunk) => Effect.Effect, + units: number, + duration: Duration.DurationInput, + burst: number +): Stream.Stream => { + const loop = ( + tokens: number, + timestampMillis: number + ): Channel.Channel, Chunk.Chunk, E | E2, E, void, unknown, R2> => + core.readWithCause({ + onInput: (input: Chunk.Chunk) => + pipe( + costFn(input), + Effect.zip(Clock.currentTimeMillis), + Effect.map(([weight, currentTimeMillis]) => { + const elapsed = currentTimeMillis - timestampMillis + const cycles = elapsed / Duration.toMillis(duration) + const sum = tokens + (cycles * units) + const max = units + burst < 0 ? Number.POSITIVE_INFINITY : units + burst + const available = sum < 0 ? max : Math.min(sum, max) + const remaining = available - weight + const waitCycles = remaining >= 0 ? 0 : -remaining / units + const delay = Duration.millis(Math.max(0, waitCycles * Duration.toMillis(duration))) + if (Duration.greaterThan(delay, Duration.zero)) { + return pipe( + core.fromEffect(Clock.sleep(delay)), + channel.zipRight(core.write(input)), + core.flatMap(() => loop(remaining, currentTimeMillis)) + ) + } + return core.flatMap( + core.write(input), + () => loop(remaining, currentTimeMillis) + ) + }), + channel.unwrap + ), + onFailure: core.failCause, + onDone: () => core.void + }) + const throttled = pipe( + Clock.currentTimeMillis, + Effect.map((currentTimeMillis) => loop(units, currentTimeMillis)), + channel.unwrap + ) + return new StreamImpl(pipe(toChannel(self), channel.pipeToOrFail(throttled))) +} + +/** @internal */ +export const tick = (interval: Duration.DurationInput): Stream.Stream => + repeatWithSchedule(void 0, Schedule.spaced(interval)) + +/** @internal */ +export const timeout = dual< + (duration: Duration.DurationInput) => (self: Stream.Stream) => Stream.Stream, + (self: Stream.Stream, duration: Duration.DurationInput) => Stream.Stream +>(2, (self: Stream.Stream, duration: Duration.DurationInput): Stream.Stream => + pipe( + toPull(self), + Effect.map(Effect.timeoutFail>({ + onTimeout: () => Option.none(), + duration + })), + fromPull + )) + +/** @internal */ +export const timeoutFail = dual< + ( + error: LazyArg, + duration: Duration.DurationInput + ) => (self: Stream.Stream) => Stream.Stream, + ( + self: Stream.Stream, + error: LazyArg, + duration: Duration.DurationInput + ) => Stream.Stream +>( + 3, + ( + self: Stream.Stream, + error: LazyArg, + duration: Duration.DurationInput + ): Stream.Stream => pipe(self, timeoutTo(duration, failSync(error))) +) + +/** @internal */ +export const timeoutFailCause = dual< + ( + cause: LazyArg>, + duration: Duration.DurationInput + ) => (self: Stream.Stream) => Stream.Stream, + ( + self: Stream.Stream, + cause: LazyArg>, + duration: Duration.DurationInput + ) => Stream.Stream +>( + 3, + ( + self: Stream.Stream, + cause: LazyArg>, + duration: Duration.DurationInput + ): Stream.Stream => + pipe( + toPull(self), + Effect.map( + Effect.timeoutFailCause>({ + onTimeout: () => Cause.map(cause(), Option.some), + duration + }) + ), + fromPull + ) +) + +/** @internal */ +export const timeoutTo = dual< + ( + duration: Duration.DurationInput, + that: Stream.Stream + ) => (self: Stream.Stream) => Stream.Stream, + ( + self: Stream.Stream, + duration: Duration.DurationInput, + that: Stream.Stream + ) => Stream.Stream +>( + 3, + ( + self: Stream.Stream, + duration: Duration.DurationInput, + that: Stream.Stream + ): Stream.Stream => { + const StreamTimeout = new Cause.RuntimeException("Stream Timeout") + return pipe( + self, + timeoutFailCause(() => Cause.die(StreamTimeout), duration), + catchSomeCause((cause) => + Cause.isDieType(cause) && + Cause.isRuntimeException(cause.defect) && + cause.defect.message !== undefined && + cause.defect.message === "Stream Timeout" ? + Option.some(that) : + Option.none() + ) + ) + } +) + +const pubsubFromOptions = ( + options: number | { + readonly capacity: "unbounded" + readonly replay?: number | undefined + } | { + readonly capacity: number + readonly strategy?: "sliding" | "dropping" | "suspend" | undefined + readonly replay?: number | undefined + } +): Effect.Effect>> => { + if (typeof options === "number") { + return PubSub.bounded(options) + } else if (options.capacity === "unbounded") { + return PubSub.unbounded({ replay: options.replay }) + } + switch (options.strategy) { + case "dropping": + return PubSub.dropping(options) + case "sliding": + return PubSub.sliding(options) + default: + return PubSub.bounded(options) + } +} + +/** @internal */ +export const toPubSub = dual< + ( + capacity: number | { + readonly capacity: "unbounded" + readonly replay?: number | undefined + } | { + readonly capacity: number + readonly strategy?: "sliding" | "dropping" | "suspend" | undefined + readonly replay?: number | undefined + } + ) => (self: Stream.Stream) => Effect.Effect>, never, Scope.Scope | R>, + ( + self: Stream.Stream, + capacity: number | { + readonly capacity: "unbounded" + readonly replay?: number | undefined + } | { + readonly capacity: number + readonly strategy?: "sliding" | "dropping" | "suspend" | undefined + readonly replay?: number | undefined + } + ) => Effect.Effect>, never, Scope.Scope | R> +>(2, ( + self: Stream.Stream, + capacity: number | { + readonly capacity: "unbounded" + readonly replay?: number | undefined + } | { + readonly capacity: number + readonly strategy?: "sliding" | "dropping" | "suspend" | undefined + readonly replay?: number | undefined + } +): Effect.Effect>, never, Scope.Scope | R> => + pipe( + Effect.acquireRelease(pubsubFromOptions(capacity), (pubsub) => PubSub.shutdown(pubsub)), + Effect.tap((pubsub) => pipe(self, runIntoPubSubScoped(pubsub), Effect.forkScoped)) + )) + +/** @internal */ +export const toPull = ( + self: Stream.Stream +): Effect.Effect, Option.Option, R>, never, R | Scope.Scope> => + Effect.map(channel.toPull(toChannel(self)), (pull) => + pipe( + pull, + Effect.mapError(Option.some), + Effect.flatMap(Either.match({ + onLeft: () => Effect.fail(Option.none()), + onRight: Effect.succeed + })) + )) + +/** @internal */ +export const toQueue = dual< + ( + options?: { + readonly strategy?: "suspend" | "sliding" | "dropping" | undefined + readonly capacity?: number | undefined + } | { + readonly strategy: "unbounded" + } + ) => ( + self: Stream.Stream + ) => Effect.Effect>, never, R | Scope.Scope>, + ( + self: Stream.Stream, + options?: { + readonly strategy?: "suspend" | "sliding" | "dropping" | undefined + readonly capacity?: number | undefined + } | { + readonly strategy: "unbounded" + } + ) => Effect.Effect>, never, R | Scope.Scope> +>((args) => isStream(args[0]), ( + self: Stream.Stream, + options?: { + readonly strategy?: "suspend" | "sliding" | "dropping" | undefined + readonly capacity?: number | undefined + } | { + readonly strategy: "unbounded" + } +) => + Effect.tap( + Effect.acquireRelease( + options?.strategy === "unbounded" ? + Queue.unbounded>() : + options?.strategy === "dropping" ? + Queue.dropping>(options.capacity ?? 2) : + options?.strategy === "sliding" ? + Queue.sliding>(options.capacity ?? 2) : + Queue.bounded>(options?.capacity ?? 2), + (queue) => Queue.shutdown(queue) + ), + (queue) => Effect.forkScoped(runIntoQueueScoped(self, queue)) + )) + +/** @internal */ +export const toQueueOfElements = dual< + (options?: { + readonly capacity?: number | undefined + }) => ( + self: Stream.Stream + ) => Effect.Effect>>, never, R | Scope.Scope>, + ( + self: Stream.Stream, + options?: { + readonly capacity?: number | undefined + } + ) => Effect.Effect>>, never, R | Scope.Scope> +>((args) => isStream(args[0]), ( + self: Stream.Stream, + options?: { + readonly capacity?: number | undefined + } +) => + Effect.tap( + Effect.acquireRelease( + Queue.bounded>>(options?.capacity ?? 2), + (queue) => Queue.shutdown(queue) + ), + (queue) => Effect.forkScoped(runIntoQueueElementsScoped(self, queue)) + )) + +/** @internal */ +export const toReadableStream = dual< + ( + options?: { readonly strategy?: QueuingStrategy | undefined } + ) => (self: Stream.Stream) => ReadableStream, + ( + self: Stream.Stream, + options?: { readonly strategy?: QueuingStrategy | undefined } + ) => ReadableStream +>( + (args) => isStream(args[0]), + ( + self: Stream.Stream, + options?: { readonly strategy?: QueuingStrategy | undefined } + ) => toReadableStreamRuntime(self, Runtime.defaultRuntime, options) +) + +/** @internal */ +export const toReadableStreamEffect = dual< + ( + options?: { readonly strategy?: QueuingStrategy | undefined } + ) => (self: Stream.Stream) => Effect.Effect, never, R>, + ( + self: Stream.Stream, + options?: { readonly strategy?: QueuingStrategy | undefined } + ) => Effect.Effect, never, R> +>( + (args) => isStream(args[0]), + ( + self: Stream.Stream, + options?: { readonly strategy?: QueuingStrategy | undefined } + ) => Effect.map(Effect.runtime(), (runtime) => toReadableStreamRuntime(self, runtime, options)) +) + +/** @internal */ +export const toReadableStreamRuntime = dual< + ( + runtime: Runtime.Runtime, + options?: { readonly strategy?: QueuingStrategy | undefined } + ) => (self: Stream.Stream) => ReadableStream, + ( + self: Stream.Stream, + runtime: Runtime.Runtime, + options?: { readonly strategy?: QueuingStrategy | undefined } + ) => ReadableStream +>( + (args) => isStream(args[0]), + ( + self: Stream.Stream, + runtime: Runtime.Runtime, + options?: { readonly strategy?: QueuingStrategy | undefined } + ): ReadableStream => { + const runFork = Runtime.runFork(runtime) + let currentResolve: (() => void) | undefined = undefined + let fiber: Fiber.RuntimeFiber | undefined = undefined + const latch = Effect.unsafeMakeLatch(false) + + return new ReadableStream({ + start(controller) { + fiber = runFork(runForEachChunk(self, (chunk) => { + if (chunk.length === 0) return Effect.void + return latch.whenOpen(Effect.sync(() => { + latch.unsafeClose() + for (const item of chunk) { + controller.enqueue(item) + } + currentResolve!() + currentResolve = undefined + })) + })) + fiber.addObserver((exit) => { + try { + if (exit._tag === "Failure") { + controller.error(Cause.squash(exit.cause)) + } else { + controller.close() + } + } catch { + // ignore + } + }) + }, + pull() { + return new Promise((resolve) => { + currentResolve = resolve + Effect.runSync(latch.open) + }) + }, + cancel() { + if (!fiber) return + return Effect.runPromise(Effect.asVoid(Fiber.interrupt(fiber))) + } + }, options?.strategy) + } +) + +/** @internal */ +export const transduce = dual< + ( + sink: Sink.Sink + ) => (self: Stream.Stream) => Stream.Stream, + ( + self: Stream.Stream, + sink: Sink.Sink + ) => Stream.Stream +>( + 2, + ( + self: Stream.Stream, + sink: Sink.Sink + ): Stream.Stream => { + const newChannel = core.suspend(() => { + const leftovers = { ref: Chunk.empty>() } + const upstreamDone = { ref: false } + const buffer: Channel.Channel, Chunk.Chunk, E, E, unknown, unknown> = core.suspend( + () => { + const leftover = leftovers.ref + if (Chunk.isEmpty(leftover)) { + return core.readWith({ + onInput: (input) => pipe(core.write(input), core.flatMap(() => buffer)), + onFailure: core.fail, + onDone: core.succeedNow + }) + } + leftovers.ref = Chunk.empty>() + return pipe(channel.writeChunk(leftover), core.flatMap(() => buffer)) + } + ) + const concatAndGet = (chunk: Chunk.Chunk>): Chunk.Chunk> => { + const leftover = leftovers.ref + const concatenated = Chunk.appendAll(leftover, Chunk.filter(chunk, (chunk) => chunk.length !== 0)) + leftovers.ref = concatenated + return concatenated + } + const upstreamMarker: Channel.Channel, Chunk.Chunk, E, E, unknown, unknown> = core + .readWith({ + onInput: (input: Chunk.Chunk) => core.flatMap(core.write(input), () => upstreamMarker), + onFailure: core.fail, + onDone: (done) => + channel.zipRight( + core.sync(() => { + upstreamDone.ref = true + }), + core.succeedNow(done) + ) + }) + const transducer: Channel.Channel, Chunk.Chunk, E | E2, never, void, unknown, R | R2> = pipe( + sink, + sink_.toChannel, + core.collectElements, + core.flatMap(([leftover, z]) => + pipe( + core.succeed([upstreamDone.ref, concatAndGet(leftover)] as const), + core.flatMap(([done, newLeftovers]) => { + const nextChannel = done && Chunk.isEmpty(newLeftovers) ? + core.void : + transducer + return pipe(core.write(Chunk.of(z)), core.flatMap(() => nextChannel)) + }) + ) + ) + ) + return pipe( + toChannel(self), + core.pipeTo(upstreamMarker), + core.pipeTo(buffer), + channel.pipeToOrFail(transducer) + ) + }) + return new StreamImpl(newChannel) + } +) + +/** @internal */ +export const toAsyncIterableRuntime = dual< + ( + runtime: Runtime.Runtime + ) => (self: Stream.Stream) => AsyncIterable, + ( + self: Stream.Stream, + runtime: Runtime.Runtime + ) => AsyncIterable +>( + (args) => isStream(args[0]), + ( + self: Stream.Stream, + runtime: Runtime.Runtime + ): AsyncIterable => { + const runFork = Runtime.runFork(runtime) + return { + [Symbol.asyncIterator]() { + let currentResolve: ((value: IteratorResult) => void) | undefined = undefined + let currentReject: ((reason: any) => void) | undefined = undefined + let fiber: Fiber.RuntimeFiber | undefined = undefined + const latch = Effect.unsafeMakeLatch(false) + let returned = false + return { + next() { + if (!fiber) { + fiber = runFork(runForEach(self, (value) => + latch.whenOpen(Effect.sync(() => { + latch.unsafeClose() + currentResolve!({ done: false, value }) + currentResolve = currentReject = undefined + })))) + fiber.addObserver((exit) => { + if (returned) return + fiber = Effect.runFork(latch.whenOpen(Effect.sync(() => { + if (exit._tag === "Failure") { + currentReject!(Cause.squash(exit.cause)) + } else { + currentResolve!({ done: true, value: void 0 }) + } + currentResolve = currentReject = undefined + }))) + }) + } + return new Promise>((resolve, reject) => { + currentResolve = resolve + currentReject = reject + latch.unsafeOpen() + }) + }, + return() { + returned = true + if (!fiber) return Promise.resolve({ done: true, value: void 0 }) + return Effect.runPromise(Effect.as(Fiber.interrupt(fiber), { done: true, value: void 0 })) + } + } + } + } + } +) + +/** @internal */ +export const toAsyncIterable = (self: Stream.Stream): AsyncIterable => + toAsyncIterableRuntime(self, Runtime.defaultRuntime) + +/** @internal */ +export const toAsyncIterableEffect = ( + self: Stream.Stream +): Effect.Effect, never, R> => + Effect.map(Effect.runtime(), (runtime) => toAsyncIterableRuntime(self, runtime)) + +/** @internal */ +export const unfold = (s: S, f: (s: S) => Option.Option): Stream.Stream => + unfoldChunk(s, (s) => pipe(f(s), Option.map(([a, s]) => [Chunk.of(a), s]))) + +/** @internal */ +export const unfoldChunk = ( + s: S, + f: (s: S) => Option.Option, S]> +): Stream.Stream => { + const loop = (s: S): Channel.Channel, unknown, never, unknown, unknown, unknown> => + Option.match(f(s), { + onNone: () => core.void, + onSome: ([chunk, s]) => core.flatMap(core.write(chunk), () => loop(s)) + }) + return new StreamImpl(core.suspend(() => loop(s))) +} + +/** @internal */ +export const unfoldChunkEffect = ( + s: S, + f: (s: S) => Effect.Effect, S]>, E, R> +): Stream.Stream => + suspend(() => { + const loop = (s: S): Channel.Channel, unknown, E, unknown, unknown, unknown, R> => + channel.unwrap( + Effect.map( + f(s), + Option.match({ + onNone: () => core.void, + onSome: ([chunk, s]) => core.flatMap(core.write(chunk), () => loop(s)) + }) + ) + ) + return new StreamImpl(loop(s)) + }) + +/** @internal */ +export const unfoldEffect = ( + s: S, + f: (s: S) => Effect.Effect, E, R> +): Stream.Stream => + unfoldChunkEffect(s, (s) => pipe(f(s), Effect.map(Option.map(([a, s]) => [Chunk.of(a), s])))) + +const void_: Stream.Stream = succeed(void 0) +export { + /** @internal */ + void_ as void +} + +/** @internal */ +export const unwrap = ( + effect: Effect.Effect, E, R> +): Stream.Stream => flatten(fromEffect(effect)) + +/** @internal */ +export const unwrapScoped = ( + effect: Effect.Effect, E, R> +): Stream.Stream | R2> => flatten(scoped(effect)) + +/** @internal */ +export const unwrapScopedWith = ( + f: (scope: Scope.Scope) => Effect.Effect, E, R> +): Stream.Stream => flatten(scopedWith((scope) => f(scope))) + +/** @internal */ +export const updateService = dual< + ( + tag: Context.Tag, + f: (service: Types.NoInfer) => Types.NoInfer + ) => (self: Stream.Stream) => Stream.Stream, + ( + self: Stream.Stream, + tag: Context.Tag, + f: (service: Types.NoInfer) => Types.NoInfer + ) => Stream.Stream +>( + 3, + ( + self: Stream.Stream, + tag: Context.Tag, + f: (service: Types.NoInfer) => Types.NoInfer + ): Stream.Stream => + pipe( + self, + mapInputContext((context) => + pipe( + context, + Context.add(tag, f(pipe(context, Context.unsafeGet(tag)))) + ) + ) + ) +) + +/** @internal */ +export const when = dual< + (test: LazyArg) => (self: Stream.Stream) => Stream.Stream, + (self: Stream.Stream, test: LazyArg) => Stream.Stream +>( + 2, + (self: Stream.Stream, test: LazyArg): Stream.Stream => + pipe(self, whenEffect(Effect.sync(test))) +) + +/** @internal */ +export const whenCase = ( + evaluate: LazyArg, + pf: (a: A) => Option.Option> +) => whenCaseEffect(pf)(Effect.sync(evaluate)) + +/** @internal */ +export const whenCaseEffect = dual< + ( + pf: (a: A) => Option.Option> + ) => (self: Effect.Effect) => Stream.Stream, + ( + self: Effect.Effect, + pf: (a: A) => Option.Option> + ) => Stream.Stream +>( + 2, + ( + self: Effect.Effect, + pf: (a: A) => Option.Option> + ): Stream.Stream => + pipe( + fromEffect(self), + flatMap((a) => pipe(pf(a), Option.getOrElse(() => empty))) + ) +) + +/** @internal */ +export const whenEffect = dual< + ( + effect: Effect.Effect + ) => (self: Stream.Stream) => Stream.Stream, + ( + self: Stream.Stream, + effect: Effect.Effect + ) => Stream.Stream +>( + 2, + ( + self: Stream.Stream, + effect: Effect.Effect + ): Stream.Stream => pipe(fromEffect(effect), flatMap((bool) => bool ? self : empty)) +) + +/** @internal */ +export const withSpan: { + ( + name: string, + options?: Tracer.SpanOptions + ): (self: Stream.Stream) => Stream.Stream> + ( + self: Stream.Stream, + name: string, + options?: Tracer.SpanOptions + ): Stream.Stream> +} = function() { + const dataFirst = typeof arguments[0] !== "string" + const name = dataFirst ? arguments[1] : arguments[0] + const options = InternalTracer.addSpanStackTrace(dataFirst ? arguments[2] : arguments[1]) + if (dataFirst) { + const self = arguments[0] + return new StreamImpl(channel.withSpan(toChannel(self), name, options)) + } + return (self: Stream.Stream) => new StreamImpl(channel.withSpan(toChannel(self), name, options)) +} as any + +/** @internal */ +export const zip = dual< + ( + that: Stream.Stream + ) => (self: Stream.Stream) => Stream.Stream<[A, A2], E2 | E, R2 | R>, + ( + self: Stream.Stream, + that: Stream.Stream + ) => Stream.Stream<[A, A2], E2 | E, R2 | R> +>( + 2, + ( + self: Stream.Stream, + that: Stream.Stream + ): Stream.Stream<[A, A2], E2 | E, R2 | R> => pipe(self, zipWith(that, (a, a2) => [a, a2])) +) + +/** @internal */ +export const zipFlatten = dual< + ( + that: Stream.Stream + ) => , E, R>( + self: Stream.Stream + ) => Stream.Stream<[...A, A2], E2 | E, R2 | R>, + , E, R, A2, E2, R2>( + self: Stream.Stream, + that: Stream.Stream + ) => Stream.Stream<[...A, A2], E2 | E, R2 | R> +>( + 2, + , E, R, A2, E2, R2>( + self: Stream.Stream, + that: Stream.Stream + ): Stream.Stream<[...A, A2], E2 | E, R2 | R> => pipe(self, zipWith(that, (a, a2) => [...a, a2])) +) + +/** @internal */ +export const zipAll = dual< + ( + options: { + readonly other: Stream.Stream + readonly defaultSelf: A + readonly defaultOther: A2 + } + ) => (self: Stream.Stream) => Stream.Stream<[A, A2], E2 | E, R2 | R>, + ( + self: Stream.Stream, + options: { + readonly other: Stream.Stream + readonly defaultSelf: A + readonly defaultOther: A2 + } + ) => Stream.Stream<[A, A2], E2 | E, R2 | R> +>( + 2, + ( + self: Stream.Stream, + options: { + readonly other: Stream.Stream + readonly defaultSelf: A + readonly defaultOther: A2 + } + ): Stream.Stream<[A, A2], E2 | E, R2 | R> => + zipAllWith(self, { + other: options.other, + onSelf: (a) => [a, options.defaultOther], + onOther: (a2) => [options.defaultSelf, a2], + onBoth: (a, a2) => [a, a2] + }) +) + +/** @internal */ +export const zipAllLeft = dual< + ( + that: Stream.Stream, + defaultLeft: A + ) => (self: Stream.Stream) => Stream.Stream, + ( + self: Stream.Stream, + that: Stream.Stream, + defaultLeft: A + ) => Stream.Stream +>( + 3, + ( + self: Stream.Stream, + other: Stream.Stream, + defaultSelf: A + ): Stream.Stream => + zipAllWith(self, { + other, + onSelf: identity, + onOther: () => defaultSelf, + onBoth: (a) => a + }) +) + +/** @internal */ +export const zipAllRight = dual< + ( + that: Stream.Stream, + defaultRight: A2 + ) => (self: Stream.Stream) => Stream.Stream, + ( + self: Stream.Stream, + that: Stream.Stream, + defaultRight: A2 + ) => Stream.Stream +>( + 3, + ( + self: Stream.Stream, + other: Stream.Stream, + defaultRight: A2 + ): Stream.Stream => + zipAllWith(self, { + other, + onSelf: () => defaultRight, + onOther: identity, + onBoth: (_, a2) => a2 + }) +) + +/** @internal */ +export const zipAllSortedByKey = dual< + ( + options: { + readonly other: Stream.Stream + readonly defaultSelf: A + readonly defaultOther: A2 + readonly order: Order.Order + } + ) => ( + self: Stream.Stream + ) => Stream.Stream<[K, [A, A2]], E2 | E, R2 | R>, + ( + self: Stream.Stream, + options: { + readonly other: Stream.Stream + readonly defaultSelf: A + readonly defaultOther: A2 + readonly order: Order.Order + } + ) => Stream.Stream<[K, [A, A2]], E2 | E, R2 | R> +>( + 2, + ( + self: Stream.Stream, + options: { + readonly other: Stream.Stream + readonly defaultSelf: A + readonly defaultOther: A2 + readonly order: Order.Order + } + ): Stream.Stream<[K, [A, A2]], E2 | E, R2 | R> => + zipAllSortedByKeyWith(self, { + other: options.other, + onSelf: (a) => [a, options.defaultOther], + onOther: (a2) => [options.defaultSelf, a2], + onBoth: (a, a2) => [a, a2], + order: options.order + }) +) + +/** @internal */ +export const zipAllSortedByKeyLeft = dual< + ( + options: { + readonly other: Stream.Stream + readonly defaultSelf: A + readonly order: Order.Order + } + ) => (self: Stream.Stream) => Stream.Stream<[K, A], E2 | E, R2 | R>, + ( + self: Stream.Stream, + options: { + readonly other: Stream.Stream + readonly defaultSelf: A + readonly order: Order.Order + } + ) => Stream.Stream<[K, A], E2 | E, R2 | R> +>( + 2, + ( + self: Stream.Stream, + options: { + readonly other: Stream.Stream + readonly defaultSelf: A + readonly order: Order.Order + } + ): Stream.Stream<[K, A], E2 | E, R2 | R> => + zipAllSortedByKeyWith(self, { + other: options.other, + onSelf: identity, + onOther: () => options.defaultSelf, + onBoth: (a) => a, + order: options.order + }) +) + +/** @internal */ +export const zipAllSortedByKeyRight = dual< + ( + options: { + readonly other: Stream.Stream + readonly defaultOther: A2 + readonly order: Order.Order + } + ) => (self: Stream.Stream) => Stream.Stream<[K, A2], E2 | E, R2 | R>, + ( + self: Stream.Stream, + options: { + readonly other: Stream.Stream + readonly defaultOther: A2 + readonly order: Order.Order + } + ) => Stream.Stream<[K, A2], E2 | E, R2 | R> +>( + 2, + ( + self: Stream.Stream, + options: { + readonly other: Stream.Stream + readonly defaultOther: A2 + readonly order: Order.Order + } + ): Stream.Stream<[K, A2], E2 | E, R2 | R> => + zipAllSortedByKeyWith(self, { + other: options.other, + onSelf: () => options.defaultOther, + onOther: identity, + onBoth: (_, a2) => a2, + order: options.order + }) +) + +/** @internal */ +export const zipAllSortedByKeyWith = dual< + ( + options: { + readonly other: Stream.Stream + readonly onSelf: (a: A) => A3 + readonly onOther: (a2: A2) => A3 + readonly onBoth: (a: A, a2: A2) => A3 + readonly order: Order.Order + } + ) => (self: Stream.Stream) => Stream.Stream<[K, A3], E2 | E, R2 | R>, + ( + self: Stream.Stream, + options: { + readonly other: Stream.Stream + readonly onSelf: (a: A) => A3 + readonly onOther: (a2: A2) => A3 + readonly onBoth: (a: A, a2: A2) => A3 + readonly order: Order.Order + } + ) => Stream.Stream<[K, A3], E2 | E, R2 | R> +>( + 2, + ( + self: Stream.Stream, + options: { + readonly other: Stream.Stream + readonly onSelf: (a: A) => A3 + readonly onOther: (a2: A2) => A3 + readonly onBoth: (a: A, a2: A2) => A3 + readonly order: Order.Order + } + ): Stream.Stream<[K, A3], E2 | E, R2 | R> => { + const pull = ( + state: ZipAllState.ZipAllState, + pullLeft: Effect.Effect, Option.Option, R>, + pullRight: Effect.Effect, Option.Option, R2> + ): Effect.Effect< + Exit.Exit< + readonly [ + Chunk.Chunk<[K, A3]>, + ZipAllState.ZipAllState + ], + Option.Option + >, + never, + R | R2 + > => { + switch (state._tag) { + case ZipAllState.OP_DRAIN_LEFT: { + return pipe( + pullLeft, + Effect.match({ + onFailure: Exit.fail, + onSuccess: (leftChunk) => + Exit.succeed( + [ + Chunk.map(leftChunk, ([k, a]) => [k, options.onSelf(a)]), + ZipAllState.DrainLeft + ] as const + ) + }) + ) + } + case ZipAllState.OP_DRAIN_RIGHT: { + return pipe( + pullRight, + Effect.match({ + onFailure: Exit.fail, + onSuccess: (rightChunk) => + Exit.succeed( + [ + Chunk.map(rightChunk, ([k, a2]) => [k, options.onOther(a2)]), + ZipAllState.DrainRight + ] as const + ) + }) + ) + } + case ZipAllState.OP_PULL_BOTH: { + return pipe( + unsome(pullLeft), + Effect.zip(unsome(pullRight), { concurrent: true }), + Effect.matchEffect({ + onFailure: (error) => Effect.succeed(Exit.fail(Option.some(error))), + onSuccess: ([leftOption, rightOption]) => { + if (Option.isSome(leftOption) && Option.isSome(rightOption)) { + if (Chunk.isEmpty(leftOption.value) && Chunk.isEmpty(rightOption.value)) { + return pull(ZipAllState.PullBoth, pullLeft, pullRight) + } + if (Chunk.isEmpty(leftOption.value)) { + return pull(ZipAllState.PullLeft(rightOption.value), pullLeft, pullRight) + } + if (Chunk.isEmpty(rightOption.value)) { + return pull(ZipAllState.PullRight(leftOption.value), pullLeft, pullRight) + } + return Effect.succeed(Exit.succeed(merge(leftOption.value, rightOption.value))) + } + if (Option.isSome(leftOption) && Option.isNone(rightOption)) { + if (Chunk.isEmpty(leftOption.value)) { + return pull(ZipAllState.DrainLeft, pullLeft, pullRight) + } + return Effect.succeed( + Exit.succeed( + [ + pipe(leftOption.value, Chunk.map(([k, a]) => [k, options.onSelf(a)])), + ZipAllState.DrainLeft + ] as const + ) + ) + } + if (Option.isNone(leftOption) && Option.isSome(rightOption)) { + if (Chunk.isEmpty(rightOption.value)) { + return pull(ZipAllState.DrainRight, pullLeft, pullRight) + } + return Effect.succeed( + Exit.succeed( + [ + pipe(rightOption.value, Chunk.map(([k, a2]) => [k, options.onOther(a2)])), + ZipAllState.DrainRight + ] as const + ) + ) + } + return Effect.succeed(Exit.fail>(Option.none())) + } + }) + ) + } + case ZipAllState.OP_PULL_LEFT: { + return Effect.matchEffect(pullLeft, { + onFailure: Option.match({ + onNone: () => + Effect.succeed( + Exit.succeed([ + pipe(state.rightChunk, Chunk.map(([k, a2]) => [k, options.onOther(a2)])), + ZipAllState.DrainRight + ]) + ), + onSome: (error) => + Effect.succeed< + Exit.Exit< + readonly [ + Chunk.Chunk<[K, A3]>, + ZipAllState.ZipAllState + ], + Option.Option + > + >(Exit.fail(Option.some(error))) + }), + onSuccess: (leftChunk) => + Chunk.isEmpty(leftChunk) ? + pull(ZipAllState.PullLeft(state.rightChunk), pullLeft, pullRight) : + Effect.succeed(Exit.succeed(merge(leftChunk, state.rightChunk))) + }) + } + case ZipAllState.OP_PULL_RIGHT: { + return Effect.matchEffect(pullRight, { + onFailure: Option.match({ + onNone: () => + Effect.succeed( + Exit.succeed( + [ + Chunk.map(state.leftChunk, ([k, a]) => [k, options.onSelf(a)]), + ZipAllState.DrainLeft + ] as const + ) + ), + onSome: (error) => + Effect.succeed< + Exit.Exit< + readonly [ + Chunk.Chunk<[K, A3]>, + ZipAllState.ZipAllState + ], + Option.Option + > + >(Exit.fail(Option.some(error))) + }), + onSuccess: (rightChunk) => + Chunk.isEmpty(rightChunk) ? + pull(ZipAllState.PullRight(state.leftChunk), pullLeft, pullRight) : + Effect.succeed(Exit.succeed(merge(state.leftChunk, rightChunk))) + }) + } + } + } + const merge = ( + leftChunk: Chunk.Chunk, + rightChunk: Chunk.Chunk + ): readonly [ + Chunk.Chunk<[K, A3]>, + ZipAllState.ZipAllState + ] => { + const hasNext = (chunk: Chunk.Chunk, index: number) => index < chunk.length - 1 + const builder: Array<[K, A3]> = [] + let state: + | ZipAllState.ZipAllState< + readonly [K, A], + readonly [K, A2] + > + | undefined = undefined + let leftIndex = 0 + let rightIndex = 0 + let leftTuple = pipe(leftChunk, Chunk.unsafeGet(leftIndex)) + let rightTuple = pipe(rightChunk, Chunk.unsafeGet(rightIndex)) + let k1 = leftTuple[0] + let a = leftTuple[1] + let k2 = rightTuple[0] + let a2 = rightTuple[1] + let loop = true + while (loop) { + const compare = options.order(k1, k2) + if (compare === 0) { + builder.push([k1, options.onBoth(a, a2)]) + if (hasNext(leftChunk, leftIndex) && hasNext(rightChunk, rightIndex)) { + leftIndex = leftIndex + 1 + rightIndex = rightIndex + 1 + leftTuple = pipe(leftChunk, Chunk.unsafeGet(leftIndex)) + rightTuple = pipe(rightChunk, Chunk.unsafeGet(rightIndex)) + k1 = leftTuple[0] + a = leftTuple[1] + k2 = rightTuple[0] + a2 = rightTuple[1] + } else if (hasNext(leftChunk, leftIndex)) { + state = ZipAllState.PullRight(pipe(leftChunk, Chunk.drop(leftIndex + 1))) + loop = false + } else if (hasNext(rightChunk, rightIndex)) { + state = ZipAllState.PullLeft(pipe(rightChunk, Chunk.drop(rightIndex + 1))) + loop = false + } else { + state = ZipAllState.PullBoth + loop = false + } + } else if (compare < 0) { + builder.push([k1, options.onSelf(a)]) + if (hasNext(leftChunk, leftIndex)) { + leftIndex = leftIndex + 1 + leftTuple = pipe(leftChunk, Chunk.unsafeGet(leftIndex)) + k1 = leftTuple[0] + a = leftTuple[1] + } else { + const rightBuilder: Array = [] + rightBuilder.push(rightTuple) + while (hasNext(rightChunk, rightIndex)) { + rightIndex = rightIndex + 1 + rightTuple = pipe(rightChunk, Chunk.unsafeGet(rightIndex)) + rightBuilder.push(rightTuple) + } + state = ZipAllState.PullLeft(Chunk.unsafeFromArray(rightBuilder)) + loop = false + } + } else { + builder.push([k2, options.onOther(a2)]) + if (hasNext(rightChunk, rightIndex)) { + rightIndex = rightIndex + 1 + rightTuple = pipe(rightChunk, Chunk.unsafeGet(rightIndex)) + k2 = rightTuple[0] + a2 = rightTuple[1] + } else { + const leftBuilder: Array = [] + leftBuilder.push(leftTuple) + while (hasNext(leftChunk, leftIndex)) { + leftIndex = leftIndex + 1 + leftTuple = pipe(leftChunk, Chunk.unsafeGet(leftIndex)) + leftBuilder.push(leftTuple) + } + state = ZipAllState.PullRight(Chunk.unsafeFromArray(leftBuilder)) + loop = false + } + } + } + return [Chunk.unsafeFromArray(builder), state!] + } + return combineChunks(self, options.other, ZipAllState.PullBoth, pull) + } +) + +/** @internal */ +export const zipAllWith = dual< + ( + options: { + readonly other: Stream.Stream + readonly onSelf: (a: A) => A3 + readonly onOther: (a2: A2) => A3 + readonly onBoth: (a: A, a2: A2) => A3 + } + ) => (self: Stream.Stream) => Stream.Stream, + ( + self: Stream.Stream, + options: { + readonly other: Stream.Stream + readonly onSelf: (a: A) => A3 + readonly onOther: (a2: A2) => A3 + readonly onBoth: (a: A, a2: A2) => A3 + } + ) => Stream.Stream +>( + 2, + ( + self: Stream.Stream, + options: { + readonly other: Stream.Stream + readonly onSelf: (a: A) => A3 + readonly onOther: (a2: A2) => A3 + readonly onBoth: (a: A, a2: A2) => A3 + } + ): Stream.Stream => { + const pull = ( + state: ZipAllState.ZipAllState, + pullLeft: Effect.Effect, Option.Option, R>, + pullRight: Effect.Effect, Option.Option, R2> + ): Effect.Effect< + Exit.Exit, ZipAllState.ZipAllState], Option.Option>, + never, + R | R2 + > => { + switch (state._tag) { + case ZipAllState.OP_DRAIN_LEFT: { + return Effect.matchEffect(pullLeft, { + onFailure: (error) => Effect.succeed(Exit.fail(error)), + onSuccess: (leftChunk) => + Effect.succeed(Exit.succeed( + [ + Chunk.map(leftChunk, options.onSelf), + ZipAllState.DrainLeft + ] as const + )) + }) + } + case ZipAllState.OP_DRAIN_RIGHT: { + return Effect.matchEffect(pullRight, { + onFailure: (error) => Effect.succeed(Exit.fail(error)), + onSuccess: (rightChunk) => + Effect.succeed(Exit.succeed( + [ + Chunk.map(rightChunk, options.onOther), + ZipAllState.DrainRight + ] as const + )) + }) + } + case ZipAllState.OP_PULL_BOTH: { + return pipe( + unsome(pullLeft), + Effect.zip(unsome(pullRight), { concurrent: true }), + Effect.matchEffect({ + onFailure: (error) => Effect.succeed(Exit.fail(Option.some(error))), + onSuccess: ([leftOption, rightOption]) => { + if (Option.isSome(leftOption) && Option.isSome(rightOption)) { + if (Chunk.isEmpty(leftOption.value) && Chunk.isEmpty(rightOption.value)) { + return pull(ZipAllState.PullBoth, pullLeft, pullRight) + } + if (Chunk.isEmpty(leftOption.value)) { + return pull(ZipAllState.PullLeft(rightOption.value), pullLeft, pullRight) + } + if (Chunk.isEmpty(rightOption.value)) { + return pull(ZipAllState.PullRight(leftOption.value), pullLeft, pullRight) + } + return Effect.succeed(Exit.succeed(zip(leftOption.value, rightOption.value, options.onBoth))) + } + if (Option.isSome(leftOption) && Option.isNone(rightOption)) { + return Effect.succeed(Exit.succeed( + [ + Chunk.map(leftOption.value, options.onSelf), + ZipAllState.DrainLeft + ] as const + )) + } + if (Option.isNone(leftOption) && Option.isSome(rightOption)) { + return Effect.succeed(Exit.succeed( + [ + Chunk.map(rightOption.value, options.onOther), + ZipAllState.DrainRight + ] as const + )) + } + return Effect.succeed(Exit.fail>(Option.none())) + } + }) + ) + } + case ZipAllState.OP_PULL_LEFT: { + return Effect.matchEffect(pullLeft, { + onFailure: Option.match({ + onNone: () => + Effect.succeed(Exit.succeed( + [ + Chunk.map(state.rightChunk, options.onOther), + ZipAllState.DrainRight + ] as const + )), + onSome: (error) => + Effect.succeed< + Exit.Exit, ZipAllState.ZipAllState], Option.Option> + >( + Exit.fail(Option.some(error)) + ) + }), + onSuccess: (leftChunk) => { + if (Chunk.isEmpty(leftChunk)) { + return pull(ZipAllState.PullLeft(state.rightChunk), pullLeft, pullRight) + } + if (Chunk.isEmpty(state.rightChunk)) { + return pull(ZipAllState.PullRight(leftChunk), pullLeft, pullRight) + } + return Effect.succeed(Exit.succeed(zip(leftChunk, state.rightChunk, options.onBoth))) + } + }) + } + case ZipAllState.OP_PULL_RIGHT: { + return Effect.matchEffect(pullRight, { + onFailure: Option.match({ + onNone: () => + Effect.succeed( + Exit.succeed( + [ + Chunk.map(state.leftChunk, options.onSelf), + ZipAllState.DrainLeft + ] as const + ) + ), + onSome: (error) => + Effect.succeed< + Exit.Exit, ZipAllState.ZipAllState], Option.Option> + >( + Exit.fail(Option.some(error)) + ) + }), + onSuccess: (rightChunk) => { + if (Chunk.isEmpty(rightChunk)) { + return pull( + ZipAllState.PullRight(state.leftChunk), + pullLeft, + pullRight + ) + } + if (Chunk.isEmpty(state.leftChunk)) { + return pull( + ZipAllState.PullLeft(rightChunk), + pullLeft, + pullRight + ) + } + return Effect.succeed(Exit.succeed(zip(state.leftChunk, rightChunk, options.onBoth))) + } + }) + } + } + } + const zip = ( + leftChunk: Chunk.Chunk, + rightChunk: Chunk.Chunk, + f: (a: A, a2: A2) => A3 + ): readonly [Chunk.Chunk, ZipAllState.ZipAllState] => { + const [output, either] = zipChunks(leftChunk, rightChunk, f) + switch (either._tag) { + case "Left": { + if (Chunk.isEmpty(either.left)) { + return [output, ZipAllState.PullBoth] as const + } + return [output, ZipAllState.PullRight(either.left)] as const + } + case "Right": { + if (Chunk.isEmpty(either.right)) { + return [output, ZipAllState.PullBoth] as const + } + return [output, ZipAllState.PullLeft(either.right)] as const + } + } + } + return combineChunks(self, options.other, ZipAllState.PullBoth, pull) + } +) + +/** @internal */ +export const zipLatest: { + ( + right: Stream.Stream + ): (left: Stream.Stream) => Stream.Stream<[AL, AR], EL | ER, RL | RR> + ( + left: Stream.Stream, + right: Stream.Stream + ): Stream.Stream<[AL, AR], EL | ER, RL | RR> +} = dual( + 2, + ( + left: Stream.Stream, + right: Stream.Stream + ): Stream.Stream<[AL, AR], EL | ER, RL | RR> => pipe(left, zipLatestWith(right, (a, a2) => [a, a2])) +) + +export const zipLatestAll = >>( + ...streams: T +): Stream.Stream< + [T[number]] extends [never] ? never + : { [K in keyof T]: T[K] extends Stream.Stream ? A : never }, + [T[number]] extends [never] ? never : T[number] extends Stream.Stream ? _E : never, + [T[number]] extends [never] ? never : T[number] extends Stream.Stream ? _R : never +> => { + if (streams.length === 0) { + return empty + } else if (streams.length === 1) { + return map(streams[0]!, (x) => [x]) as any + } + const [head, ...tail] = streams + return zipLatestWith( + head, + zipLatestAll(...tail), + (first, second) => [first, ...second] + ) as any +} + +/** @internal */ +export const zipLatestWith: { + ( + right: Stream.Stream, + f: (left: AL, right: AR) => A + ): (left: Stream.Stream) => Stream.Stream + ( + left: Stream.Stream, + right: Stream.Stream, + f: (left: AL, right: AR) => A + ): Stream.Stream +} = dual( + 3, + ( + left: Stream.Stream, + right: Stream.Stream, + f: (left: AL, right: AR) => A + ): Stream.Stream => { + const pullNonEmpty = <_R, _E, _A>( + pull: Effect.Effect, Option.Option<_E>, _R> + ): Effect.Effect, Option.Option<_E>, _R> => + pipe(pull, Effect.flatMap((chunk) => Chunk.isEmpty(chunk) ? pullNonEmpty(pull) : Effect.succeed(chunk))) + return pipe( + toPull(left), + Effect.map(pullNonEmpty), + Effect.zip(pipe(toPull(right), Effect.map(pullNonEmpty))), + Effect.flatMap(([left, right]) => + pipe( + fromEffectOption, Chunk.Chunk, boolean], EL | ER, RL | RR>( + Effect.raceWith(left, right, { + onSelfDone: (leftDone, rightFiber) => + pipe( + Effect.suspend(() => leftDone), + Effect.zipWith(Fiber.join(rightFiber), (l, r) => [l, r, true] as const) + ), + onOtherDone: (rightDone, leftFiber) => + pipe( + Effect.suspend(() => rightDone), + Effect.zipWith(Fiber.join(leftFiber), (l, r) => [r, l, false] as const) + ) + }) + ), + flatMap(([l, r, leftFirst]) => + pipe( + fromEffect( + Ref.make([Chunk.unsafeLast(l), Chunk.unsafeLast(r)] as const) + ), + flatMap((latest) => + pipe( + fromChunk( + leftFirst ? + pipe(r, Chunk.map((a2) => f(Chunk.unsafeLast(l), a2))) : + pipe(l, Chunk.map((a) => f(a, Chunk.unsafeLast(r)))) + ), + concat( + pipe( + repeatEffectOption(left), + mergeEither(repeatEffectOption(right)), + mapEffectSequential(Either.match({ + onLeft: (leftChunk) => + Ref.modify(latest, ([_, rightLatest]) => + [ + pipe(leftChunk, Chunk.map((a) => f(a, rightLatest))), + [Chunk.unsafeLast(leftChunk), rightLatest] as const + ] as const), + onRight: (rightChunk) => + Ref.modify(latest, ([leftLatest, _]) => + [ + pipe(rightChunk, Chunk.map((a2) => f(leftLatest, a2))), + [leftLatest, Chunk.unsafeLast(rightChunk)] as const + ] as const) + })), + flatMap(fromChunk) + ) + ) + ) + ) + ) + ), + toPull + ) + ), + fromPull + ) + } +) + +/** @internal */ +export const zipLeft: { + ( + right: Stream.Stream + ): (left: Stream.Stream) => Stream.Stream + ( + left: Stream.Stream, + right: Stream.Stream + ): Stream.Stream +} = dual( + 2, + ( + left: Stream.Stream, + right: Stream.Stream + ): Stream.Stream => + pipe( + left, + zipWithChunks(right, (left, right) => { + if (left.length > right.length) { + return [ + pipe(left, Chunk.take(right.length)), + Either.left(pipe(left, Chunk.take(right.length))) + ] as const + } + return [ + left, + Either.right(pipe(right, Chunk.drop(left.length))) + ] + }) + ) +) + +/** @internal */ +export const zipRight: { + ( + right: Stream.Stream + ): (left: Stream.Stream) => Stream.Stream + ( + left: Stream.Stream, + right: Stream.Stream + ): Stream.Stream +} = dual( + 2, + ( + left: Stream.Stream, + right: Stream.Stream + ): Stream.Stream => + pipe( + left, + zipWithChunks(right, (left, right) => { + if (left.length > right.length) { + return [ + right, + Either.left(pipe(left, Chunk.take(right.length))) + ] as const + } + return [ + pipe(right, Chunk.take(left.length)), + Either.right(pipe(right, Chunk.drop(left.length))) + ] + }) + ) +) + +/** @internal */ +export const zipWith: { + ( + right: Stream.Stream, + f: (left: AL, right: AR) => A + ): (left: Stream.Stream) => Stream.Stream + ( + left: Stream.Stream, + right: Stream.Stream, + f: (left: AL, right: AR) => A + ): Stream.Stream +} = dual( + 3, + ( + left: Stream.Stream, + right: Stream.Stream, + f: (left: AL, right: AR) => A + ): Stream.Stream => + pipe(left, zipWithChunks(right, (leftChunk, rightChunk) => zipChunks(leftChunk, rightChunk, f))) +) + +/** @internal */ +export const zipWithChunks = dual< + ( + that: Stream.Stream, + f: ( + left: Chunk.Chunk, + right: Chunk.Chunk + ) => readonly [Chunk.Chunk, Either.Either, Chunk.Chunk>] + ) => (self: Stream.Stream) => Stream.Stream, + ( + self: Stream.Stream, + that: Stream.Stream, + f: ( + left: Chunk.Chunk, + right: Chunk.Chunk + ) => readonly [Chunk.Chunk, Either.Either, Chunk.Chunk>] + ) => Stream.Stream +>(3, ( + self: Stream.Stream, + that: Stream.Stream, + f: ( + left: Chunk.Chunk, + right: Chunk.Chunk + ) => readonly [Chunk.Chunk, Either.Either, Chunk.Chunk>] +): Stream.Stream => { + const pull = ( + state: ZipChunksState.ZipChunksState, + pullLeft: Effect.Effect, Option.Option, R>, + pullRight: Effect.Effect, Option.Option, R2> + ): Effect.Effect< + Exit.Exit, ZipChunksState.ZipChunksState], Option.Option>, + never, + R | R2 + > => { + switch (state._tag) { + case ZipChunksState.OP_PULL_BOTH: { + return pipe( + unsome(pullLeft), + Effect.zip(unsome(pullRight), { concurrent: true }), + Effect.matchEffect({ + onFailure: (error) => Effect.succeed(Exit.fail(Option.some(error))), + onSuccess: ([leftOption, rightOption]) => { + if (Option.isSome(leftOption) && Option.isSome(rightOption)) { + if (Chunk.isEmpty(leftOption.value) && Chunk.isEmpty(rightOption.value)) { + return pull(ZipChunksState.PullBoth, pullLeft, pullRight) + } + if (Chunk.isEmpty(leftOption.value)) { + return pull(ZipChunksState.PullLeft(rightOption.value), pullLeft, pullRight) + } + if (Chunk.isEmpty(rightOption.value)) { + return pull(ZipChunksState.PullRight(leftOption.value), pullLeft, pullRight) + } + return Effect.succeed(Exit.succeed(zip(leftOption.value, rightOption.value))) + } + return Effect.succeed(Exit.fail(Option.none())) + } + }) + ) + } + case ZipChunksState.OP_PULL_LEFT: { + return Effect.matchEffect(pullLeft, { + onFailure: (error) => Effect.succeed(Exit.fail(error)), + onSuccess: (leftChunk) => { + if (Chunk.isEmpty(leftChunk)) { + return pull(ZipChunksState.PullLeft(state.rightChunk), pullLeft, pullRight) + } + if (Chunk.isEmpty(state.rightChunk)) { + return pull(ZipChunksState.PullRight(leftChunk), pullLeft, pullRight) + } + return Effect.succeed(Exit.succeed(zip(leftChunk, state.rightChunk))) + } + }) + } + case ZipChunksState.OP_PULL_RIGHT: { + return Effect.matchEffect(pullRight, { + onFailure: (error) => Effect.succeed(Exit.fail(error)), + onSuccess: (rightChunk) => { + if (Chunk.isEmpty(rightChunk)) { + return pull(ZipChunksState.PullRight(state.leftChunk), pullLeft, pullRight) + } + if (Chunk.isEmpty(state.leftChunk)) { + return pull(ZipChunksState.PullLeft(rightChunk), pullLeft, pullRight) + } + return Effect.succeed(Exit.succeed(zip(state.leftChunk, rightChunk))) + } + }) + } + } + } + const zip = ( + leftChunk: Chunk.Chunk, + rightChunk: Chunk.Chunk + ): readonly [Chunk.Chunk, ZipChunksState.ZipChunksState] => { + const [output, either] = f(leftChunk, rightChunk) + switch (either._tag) { + case "Left": { + if (Chunk.isEmpty(either.left)) { + return [output, ZipChunksState.PullBoth] as const + } + return [output, ZipChunksState.PullRight(either.left)] as const + } + case "Right": { + if (Chunk.isEmpty(either.right)) { + return [output, ZipChunksState.PullBoth] as const + } + return [output, ZipChunksState.PullLeft(either.right)] as const + } + } + } + return pipe( + self, + combineChunks(that, ZipChunksState.PullBoth, pull) + ) +}) + +/** @internal */ +export const zipWithIndex = (self: Stream.Stream): Stream.Stream<[A, number], E, R> => + pipe(self, mapAccum(0, (index, a) => [index + 1, [a, index]])) + +/** @internal */ +export const zipWithNext = ( + self: Stream.Stream +): Stream.Stream<[A, Option.Option], E, R> => { + const process = ( + last: Option.Option + ): Channel.Channel]>, Chunk.Chunk, never, never, void, unknown> => + core.readWithCause({ + onInput: (input: Chunk.Chunk) => { + const [newLast, chunk] = Chunk.mapAccum( + input, + last, + (prev, curr) => [Option.some(curr), pipe(prev, Option.map((a) => [a, curr] as const))] as const + ) + const output = Chunk.filterMap( + chunk, + (option) => + Option.isSome(option) ? + Option.some([option.value[0], Option.some(option.value[1])] as const) : + Option.none() + ) + return core.flatMap( + core.write(output), + () => process(newLast) + ) + }, + onFailure: core.failCause, + onDone: () => + Option.match(last, { + onNone: () => core.void, + onSome: (value) => + channel.zipRight( + core.write(Chunk.of]>([value, Option.none()])), + core.void + ) + }) + }) + return new StreamImpl(pipe(toChannel(self), channel.pipeToOrFail(process(Option.none())))) +} + +/** @internal */ +export const zipWithPrevious = ( + self: Stream.Stream +): Stream.Stream<[Option.Option, A], E, R> => + pipe( + self, + mapAccum, A, [Option.Option, A]>( + Option.none(), + (prev, curr) => [Option.some(curr), [prev, curr]] + ) + ) + +/** @internal */ +export const zipWithPreviousAndNext = ( + self: Stream.Stream +): Stream.Stream<[Option.Option, A, Option.Option], E, R> => + pipe( + zipWithNext(zipWithPrevious(self)), + map(([[prev, curr], next]) => [prev, curr, pipe(next, Option.map((tuple) => tuple[1]))]) + ) + +/** @internal */ +const zipChunks = ( + left: Chunk.Chunk, + right: Chunk.Chunk, + f: (a: A, b: B) => C +): [Chunk.Chunk, Either.Either, Chunk.Chunk>] => { + if (left.length > right.length) { + return [ + pipe(left, Chunk.take(right.length), Chunk.zipWith(right, f)), + Either.left(pipe(left, Chunk.drop(right.length))) + ] + } + return [ + pipe(left, Chunk.zipWith(pipe(right, Chunk.take(left.length)), f)), + Either.right(pipe(right, Chunk.drop(left.length))) + ] +} + +// Do notation + +/** @internal */ +export const Do: Stream.Stream<{}> = succeed({}) + +/** @internal */ +export const bind = dual< + ( + tag: Exclude, + f: (_: Types.NoInfer) => Stream.Stream, + options?: { + readonly concurrency?: number | "unbounded" | undefined + readonly bufferSize?: number | undefined + } + ) => (self: Stream.Stream) => Stream.Stream< + { [K in keyof A | N]: K extends keyof A ? A[K] : B }, + E | E2, + R | R2 + >, + ( + self: Stream.Stream, + tag: Exclude, + f: (_: Types.NoInfer) => Stream.Stream, + options?: { + readonly concurrency?: number | "unbounded" | undefined + readonly bufferSize?: number | undefined + } + ) => Stream.Stream< + { [K in keyof A | N]: K extends keyof A ? A[K] : B }, + E | E2, + R | R2 + > +>((args) => typeof args[0] !== "string", ( + self: Stream.Stream, + tag: Exclude, + f: (_: A) => Stream.Stream, + options?: { + readonly concurrency?: number | "unbounded" | undefined + readonly bufferSize?: number | undefined + } +) => + flatMap(self, (k) => + map( + f(k), + (a) => ({ ...k, [tag]: a } as { [K in keyof A | N]: K extends keyof A ? A[K] : B }) + ), options)) + +/* @internal */ +export const bindTo: { + (name: N): (self: Stream.Stream) => Stream.Stream<{ [K in N]: A }, E, R> + (self: Stream.Stream, name: N): Stream.Stream<{ [K in N]: A }, E, R> +} = doNotation.bindTo(map) + +/* @internal */ +export const let_: { + ( + name: Exclude, + f: (a: Types.NoInfer) => B + ): ( + self: Stream.Stream + ) => Stream.Stream<{ [K in N | keyof A]: K extends keyof A ? A[K] : B }, E, R> + ( + self: Stream.Stream, + name: Exclude, + f: (a: Types.NoInfer) => B + ): Stream.Stream<{ [K in N | keyof A]: K extends keyof A ? A[K] : B }, E, R> +} = doNotation.let_(map) + +// Circular with Channel + +/** @internal */ +export const channelToStream = ( + self: Channel.Channel, unknown, OutErr, unknown, OutDone, unknown, Env> +): Stream.Stream => { + return new StreamImpl(self) +} + +// ============================================================================= +// encoding +// ============================================================================= + +/** @internal */ +export const decodeText = dual< + (encoding?: string) => (self: Stream.Stream) => Stream.Stream, + (self: Stream.Stream, encoding?: string) => Stream.Stream +>((args) => isStream(args[0]), (self, encoding = "utf-8") => + suspend(() => { + const decoder = new TextDecoder(encoding) + return map(self, (s) => decoder.decode(s, { stream: true })) + })) + +/** @internal */ +export const encodeText = (self: Stream.Stream): Stream.Stream => + suspend(() => { + const encoder = new TextEncoder() + return map(self, (s) => encoder.encode(s)) + }) + +/** @internal */ +export const fromEventListener = ( + target: Stream.EventListener, + type: string, + options?: boolean | { + readonly capture?: boolean + readonly passive?: boolean + readonly once?: boolean + readonly bufferSize?: number | "unbounded" | undefined + } | undefined +): Stream.Stream => + asyncPush((emit) => + Effect.acquireRelease( + Effect.sync(() => target.addEventListener(type, emit.single as any, options)), + () => Effect.sync(() => target.removeEventListener(type, emit.single, options)) + ), { bufferSize: typeof options === "object" ? options.bufferSize : undefined }) diff --git a/repos/effect/packages/effect/src/internal/stream/debounceState.ts b/repos/effect/packages/effect/src/internal/stream/debounceState.ts new file mode 100644 index 0000000..3df63a8 --- /dev/null +++ b/repos/effect/packages/effect/src/internal/stream/debounceState.ts @@ -0,0 +1,57 @@ +import type * as Chunk from "../../Chunk.js" +import type * as Fiber from "../../Fiber.js" +import type * as HandoffSignal from "./handoffSignal.js" + +/** @internal */ +export type DebounceState = NotStarted | Previous | Current + +/** @internal */ +export const OP_NOT_STARTED = "NotStarted" as const + +/** @internal */ +export type OP_NOT_STARTED = typeof OP_NOT_STARTED + +/** @internal */ +export const OP_PREVIOUS = "Previous" as const + +/** @internal */ +export type OP_PREVIOUS = typeof OP_PREVIOUS + +/** @internal */ +export const OP_CURRENT = "Current" as const + +/** @internal */ +export type OP_CURRENT = typeof OP_CURRENT + +export interface NotStarted { + readonly _tag: OP_NOT_STARTED +} + +/** @internal */ +export interface Previous { + readonly _tag: OP_PREVIOUS + readonly fiber: Fiber.Fiber> +} + +/** @internal */ +export interface Current { + readonly _tag: OP_CURRENT + readonly fiber: Fiber.Fiber, E> +} + +/** @internal */ +export const notStarted: DebounceState = { + _tag: OP_NOT_STARTED +} + +/** @internal */ +export const previous = (fiber: Fiber.Fiber>): DebounceState => ({ + _tag: OP_PREVIOUS, + fiber +}) + +/** @internal */ +export const current = (fiber: Fiber.Fiber, E>): DebounceState => ({ + _tag: OP_CURRENT, + fiber +}) diff --git a/repos/effect/packages/effect/src/internal/stream/emit.ts b/repos/effect/packages/effect/src/internal/stream/emit.ts new file mode 100644 index 0000000..7dd1623 --- /dev/null +++ b/repos/effect/packages/effect/src/internal/stream/emit.ts @@ -0,0 +1,123 @@ +import * as Cause from "../../Cause.js" +import * as Chunk from "../../Chunk.js" +import * as Effect from "../../Effect.js" +import * as Exit from "../../Exit.js" +import { pipe } from "../../Function.js" +import * as Option from "../../Option.js" +import type * as Queue from "../../Queue.js" +import type * as Scheduler from "../../Scheduler.js" +import type * as Emit from "../../StreamEmit.js" + +/** @internal */ +export const make = ( + emit: (f: Effect.Effect, Option.Option, R>) => Promise +): Emit.Emit => { + const ops: Emit.EmitOps = { + chunk(this: Emit.Emit, as: Chunk.Chunk) { + return this(Effect.succeed(as)) + }, + die(this: Emit.Emit, defect: Err) { + return this(Effect.die(defect)) + }, + dieMessage(this: Emit.Emit, message: string) { + return this(Effect.dieMessage(message)) + }, + done(this: Emit.Emit, exit: Exit.Exit) { + return this(Effect.suspend(() => Exit.mapBoth(exit, { onFailure: Option.some, onSuccess: Chunk.of }))) + }, + end(this: Emit.Emit) { + return this(Effect.fail(Option.none())) + }, + fail(this: Emit.Emit, e: E) { + return this(Effect.fail(Option.some(e))) + }, + fromEffect(this: Emit.Emit, effect: Effect.Effect) { + return this(Effect.mapBoth(effect, { onFailure: Option.some, onSuccess: Chunk.of })) + }, + fromEffectChunk(this: Emit.Emit, effect: Effect.Effect, E, R>) { + return this(pipe(effect, Effect.mapError(Option.some))) + }, + halt(this: Emit.Emit, cause: Cause.Cause) { + return this(Effect.failCause(pipe(cause, Cause.map(Option.some)))) + }, + single(this: Emit.Emit, value: A) { + return this(Effect.succeed(Chunk.of(value))) + } + } + return Object.assign(emit, ops) +} + +/** @internal */ +export const makePush = ( + queue: Queue.Queue | Exit.Exit>, + scheduler: Scheduler.Scheduler +): Emit.EmitOpsPush => { + let finished = false + let buffer: Array = [] + let running = false + function array(items: ReadonlyArray) { + if (finished) return false + if (items.length <= 50_000) { + buffer.push.apply(buffer, items as Array) + } else { + for (let i = 0; i < items.length; i++) { + buffer.push(items[0]) + } + } + if (!running) { + running = true + scheduler.scheduleTask(flush, 0) + } + return true + } + function flush() { + running = false + if (buffer.length > 0) { + queue.unsafeOffer(buffer) + buffer = [] + } + } + function done(exit: Exit.Exit) { + if (finished) return + finished = true + if (exit._tag === "Success") { + buffer.push(exit.value) + } + flush() + queue.unsafeOffer(exit._tag === "Success" ? Exit.void : exit) + } + return { + single(value: A) { + if (finished) return false + buffer.push(value) + if (!running) { + running = true + scheduler.scheduleTask(flush, 0) + } + return true + }, + array, + chunk(chunk) { + return array(Chunk.toReadonlyArray(chunk)) + }, + done, + end() { + if (finished) return + finished = true + flush() + queue.unsafeOffer(Exit.void) + }, + halt(cause: Cause.Cause) { + return done(Exit.failCause(cause)) + }, + fail(error: E) { + return done(Exit.fail(error)) + }, + die(defect: Err): void { + return done(Exit.die(defect)) + }, + dieMessage(message: string): void { + return done(Exit.die(new Error(message))) + } + } +} diff --git a/repos/effect/packages/effect/src/internal/stream/haltStrategy.ts b/repos/effect/packages/effect/src/internal/stream/haltStrategy.ts new file mode 100644 index 0000000..0115554 --- /dev/null +++ b/repos/effect/packages/effect/src/internal/stream/haltStrategy.ts @@ -0,0 +1,94 @@ +import { dual } from "../../Function.js" +import type * as HaltStrategy from "../../StreamHaltStrategy.js" +import * as OpCodes from "../opCodes/streamHaltStrategy.js" + +/** @internal */ +export const Left: HaltStrategy.HaltStrategy = { + _tag: OpCodes.OP_LEFT +} + +/** @internal */ +export const Right: HaltStrategy.HaltStrategy = { + _tag: OpCodes.OP_RIGHT +} + +/** @internal */ +export const Both: HaltStrategy.HaltStrategy = { + _tag: OpCodes.OP_BOTH +} + +/** @internal */ +export const Either: HaltStrategy.HaltStrategy = { + _tag: OpCodes.OP_EITHER +} + +/** @internal */ +export const fromInput = (input: HaltStrategy.HaltStrategyInput): HaltStrategy.HaltStrategy => { + switch (input) { + case "left": + return Left + case "right": + return Right + case "both": + return Both + case "either": + return Either + default: + return input + } +} + +/** @internal */ +export const isLeft = (self: HaltStrategy.HaltStrategy): self is HaltStrategy.Left => self._tag === OpCodes.OP_LEFT + +/** @internal */ +export const isRight = (self: HaltStrategy.HaltStrategy): self is HaltStrategy.Right => self._tag === OpCodes.OP_RIGHT + +/** @internal */ +export const isBoth = (self: HaltStrategy.HaltStrategy): self is HaltStrategy.Both => self._tag === OpCodes.OP_BOTH + +/** @internal */ +export const isEither = (self: HaltStrategy.HaltStrategy): self is HaltStrategy.Either => + self._tag === OpCodes.OP_EITHER + +/** @internal */ +export const match = dual< + (options: { + readonly onLeft: () => Z + readonly onRight: () => Z + readonly onBoth: () => Z + readonly onEither: () => Z + }) => (self: HaltStrategy.HaltStrategy) => Z, + ( + self: HaltStrategy.HaltStrategy, + options: { + readonly onLeft: () => Z + readonly onRight: () => Z + readonly onBoth: () => Z + readonly onEither: () => Z + } + ) => Z +>(2, ( + self: HaltStrategy.HaltStrategy, + options: { + readonly onLeft: () => Z + readonly onRight: () => Z + readonly onBoth: () => Z + readonly onEither: () => Z + } +): Z => { + switch (self._tag) { + case OpCodes.OP_LEFT: { + return options.onLeft() + } + case OpCodes.OP_RIGHT: { + return options.onRight() + } + case OpCodes.OP_BOTH: { + return options.onBoth() + } + case OpCodes.OP_EITHER: { + return options.onEither() + } + } +}) diff --git a/repos/effect/packages/effect/src/internal/stream/handoff.ts b/repos/effect/packages/effect/src/internal/stream/handoff.ts new file mode 100644 index 0000000..175213b --- /dev/null +++ b/repos/effect/packages/effect/src/internal/stream/handoff.ts @@ -0,0 +1,187 @@ +import * as Deferred from "../../Deferred.js" +import * as Effect from "../../Effect.js" +import { dual, pipe } from "../../Function.js" +import * as Option from "../../Option.js" +import * as Ref from "../../Ref.js" + +/** @internal */ +export const HandoffTypeId = Symbol.for("effect/Stream/Handoff") + +/** @internal */ +export type HandoffTypeId = typeof HandoffTypeId + +/** + * A synchronous queue-like abstraction that allows a producer to offer an + * element and wait for it to be taken, and allows a consumer to wait for an + * element to be available. + * + * @internal + */ +export interface Handoff extends Handoff.Variance { + readonly ref: Ref.Ref> +} + +/** @internal */ +export const OP_HANDOFF_STATE_EMPTY = "Empty" as const + +/** @internal */ +export type OP_HANDOFF_STATE_EMPTY = typeof OP_HANDOFF_STATE_EMPTY + +/** @internal */ +export const OP_HANDOFF_STATE_FULL = "Full" as const + +/** @internal */ +export type OP_HANDOFF_STATE_FULL = typeof OP_HANDOFF_STATE_FULL + +/** @internal */ +export declare namespace Handoff { + /** @internal */ + export interface Variance { + readonly [HandoffTypeId]: { + readonly _A: (_: A) => A + } + } + + /** @internal */ + export type State = Empty | Full + + /** @internal */ + export interface Empty { + readonly _tag: OP_HANDOFF_STATE_EMPTY + readonly notifyConsumer: Deferred.Deferred + } + + /** @internal */ + export interface Full { + readonly _tag: OP_HANDOFF_STATE_FULL + readonly value: A + readonly notifyProducer: Deferred.Deferred + } +} + +/** @internal */ +const handoffStateEmpty = (notifyConsumer: Deferred.Deferred): Handoff.State => ({ + _tag: OP_HANDOFF_STATE_EMPTY, + notifyConsumer +}) + +/** @internal */ +const handoffStateFull = (value: A, notifyProducer: Deferred.Deferred): Handoff.State => ({ + _tag: OP_HANDOFF_STATE_FULL, + value, + notifyProducer +}) + +/** @internal */ +const handoffStateMatch = ( + onEmpty: (notifyConsumer: Deferred.Deferred) => Z, + onFull: (value: A, notifyProducer: Deferred.Deferred) => Z +) => { + return (self: Handoff.State): Z => { + switch (self._tag) { + case OP_HANDOFF_STATE_EMPTY: { + return onEmpty(self.notifyConsumer) + } + case OP_HANDOFF_STATE_FULL: { + return onFull(self.value, self.notifyProducer) + } + } + } +} + +const handoffVariance = { + /* c8 ignore next */ + _A: (_: any) => _ +} + +/** @internal */ +export const make = (): Effect.Effect> => + pipe( + Deferred.make(), + Effect.flatMap((deferred) => Ref.make(handoffStateEmpty(deferred))), + Effect.map((ref): Handoff => ({ + [HandoffTypeId]: handoffVariance, + ref + })) + ) + +/** @internal */ +export const offer = dual< + (value: A) => (self: Handoff) => Effect.Effect, + (self: Handoff, value: A) => Effect.Effect +>(2, (self, value): Effect.Effect => { + return Effect.flatMap(Deferred.make(), (deferred) => + Effect.flatten( + Ref.modify(self.ref, (state) => + pipe( + state, + handoffStateMatch( + (notifyConsumer) => [ + Effect.zipRight( + Deferred.succeed(notifyConsumer, void 0), + Deferred.await(deferred) + ), + handoffStateFull(value, deferred) + ], + (_, notifyProducer) => [ + Effect.flatMap( + Deferred.await(notifyProducer), + () => pipe(self, offer(value)) + ), + state + ] + ) + )) + )) +}) + +/** @internal */ +export const take = (self: Handoff): Effect.Effect => + Effect.flatMap(Deferred.make(), (deferred) => + Effect.flatten( + Ref.modify(self.ref, (state) => + pipe( + state, + handoffStateMatch( + (notifyConsumer) => + [ + Effect.flatMap( + Deferred.await(notifyConsumer), + () => take(self) + ), + state + ] as const, + (value, notifyProducer) => [ + Effect.as( + Deferred.succeed(notifyProducer, void 0), + value + ), + handoffStateEmpty(deferred) + ] + ) + )) + )) + +/** @internal */ +export const poll = (self: Handoff): Effect.Effect> => + Effect.flatMap(Deferred.make(), (deferred) => + Effect.flatten( + Ref.modify(self.ref, (state) => + pipe( + state, + handoffStateMatch( + () => + [ + Effect.succeed(Option.none()), + state + ] as const, + (value, notifyProducer) => [ + Effect.as( + Deferred.succeed(notifyProducer, void 0), + Option.some(value) + ), + handoffStateEmpty(deferred) + ] + ) + )) + )) diff --git a/repos/effect/packages/effect/src/internal/stream/handoffSignal.ts b/repos/effect/packages/effect/src/internal/stream/handoffSignal.ts new file mode 100644 index 0000000..f42a26d --- /dev/null +++ b/repos/effect/packages/effect/src/internal/stream/handoffSignal.ts @@ -0,0 +1,59 @@ +import type * as Cause from "../../Cause.js" +import type * as Chunk from "../../Chunk.js" +import type * as SinkEndReason from "./sinkEndReason.js" + +/** @internal */ +export type HandoffSignal = Emit | Halt | End + +/** @internal */ +export const OP_EMIT = "Emit" as const + +/** @internal */ +export type OP_EMIT = typeof OP_EMIT + +/** @internal */ +export const OP_HALT = "Halt" as const + +/** @internal */ +export type OP_HALT = typeof OP_HALT + +/** @internal */ +export const OP_END = "End" as const + +/** @internal */ +export type OP_END = typeof OP_END + +export interface Emit { + readonly _tag: OP_EMIT + readonly elements: Chunk.Chunk +} + +/** @internal */ +export interface Halt { + readonly _tag: OP_HALT + readonly cause: Cause.Cause +} + +/** @internal */ +export interface End { + readonly _tag: OP_END + readonly reason: SinkEndReason.SinkEndReason +} + +/** @internal */ +export const emit = (elements: Chunk.Chunk): HandoffSignal => ({ + _tag: OP_EMIT, + elements +}) + +/** @internal */ +export const halt = (cause: Cause.Cause): HandoffSignal => ({ + _tag: OP_HALT, + cause +}) + +/** @internal */ +export const end = (reason: SinkEndReason.SinkEndReason): HandoffSignal => ({ + _tag: OP_END, + reason +}) diff --git a/repos/effect/packages/effect/src/internal/stream/pull.ts b/repos/effect/packages/effect/src/internal/stream/pull.ts new file mode 100644 index 0000000..2f1d8e3 --- /dev/null +++ b/repos/effect/packages/effect/src/internal/stream/pull.ts @@ -0,0 +1,34 @@ +import type * as Cause from "../../Cause.js" +import * as Chunk from "../../Chunk.js" +import * as Effect from "../../Effect.js" +import * as Option from "../../Option.js" +import * as Queue from "../../Queue.js" +import type * as Take from "../../Take.js" +import * as take from "../take.js" + +/** @internal */ +export interface Pull extends Effect.Effect, Option.Option, R> {} + +/** @internal */ +export const emit = (value: A): Effect.Effect> => Effect.succeed(Chunk.of(value)) + +/** @internal */ +export const emitChunk = (chunk: Chunk.Chunk): Effect.Effect> => Effect.succeed(chunk) + +/** @internal */ +export const empty = (): Effect.Effect> => Effect.succeed(Chunk.empty()) + +/** @internal */ +export const end = (): Effect.Effect> => Effect.fail(Option.none()) + +/** @internal */ +export const fail = (error: E): Effect.Effect> => Effect.fail(Option.some(error)) + +/** @internal */ +export const failCause = (cause: Cause.Cause): Effect.Effect> => + Effect.mapError(Effect.failCause(cause), Option.some) + +/** @internal */ +export const fromDequeue = ( + dequeue: Queue.Dequeue> +): Effect.Effect, Option.Option> => Effect.flatMap(Queue.take(dequeue), take.done) diff --git a/repos/effect/packages/effect/src/internal/stream/sinkEndReason.ts b/repos/effect/packages/effect/src/internal/stream/sinkEndReason.ts new file mode 100644 index 0000000..5c0ab6c --- /dev/null +++ b/repos/effect/packages/effect/src/internal/stream/sinkEndReason.ts @@ -0,0 +1,30 @@ +/** @internal */ +export type SinkEndReason = ScheduleEnd | UpstreamEnd + +/** @internal */ +export const OP_SCHEDULE_END = "ScheduleEnd" as const + +/** @internal */ +export type OP_SCHEDULE_END = typeof OP_SCHEDULE_END + +/** @internal */ +export const OP_UPSTREAM_END = "UpstreamEnd" as const + +/** @internal */ +export type OP_UPSTREAM_END = typeof OP_UPSTREAM_END + +/** @internal */ +export interface ScheduleEnd { + readonly _tag: OP_SCHEDULE_END +} + +/** @internal */ +export interface UpstreamEnd { + readonly _tag: OP_UPSTREAM_END +} + +/** @internal */ +export const ScheduleEnd: SinkEndReason = { _tag: OP_SCHEDULE_END } + +/** @internal */ +export const UpstreamEnd: SinkEndReason = { _tag: OP_UPSTREAM_END } diff --git a/repos/effect/packages/effect/src/internal/stream/zipAllState.ts b/repos/effect/packages/effect/src/internal/stream/zipAllState.ts new file mode 100644 index 0000000..7102783 --- /dev/null +++ b/repos/effect/packages/effect/src/internal/stream/zipAllState.ts @@ -0,0 +1,88 @@ +import type * as Chunk from "../../Chunk.js" + +/** @internal */ +export type ZipAllState = DrainLeft | DrainRight | PullBoth | PullLeft | PullRight + +/** @internal */ +export const OP_DRAIN_LEFT = "DrainLeft" as const + +/** @internal */ +export type OP_DRAIN_LEFT = typeof OP_DRAIN_LEFT + +/** @internal */ +export const OP_DRAIN_RIGHT = "DrainRight" as const + +/** @internal */ +export type OP_DRAIN_RIGHT = typeof OP_DRAIN_RIGHT + +/** @internal */ +export const OP_PULL_BOTH = "PullBoth" as const + +/** @internal */ +export type OP_PULL_BOTH = typeof OP_PULL_BOTH + +/** @internal */ +export const OP_PULL_LEFT = "PullLeft" as const + +/** @internal */ +export type OP_PULL_LEFT = typeof OP_PULL_LEFT + +/** @internal */ +export const OP_PULL_RIGHT = "PullRight" as const + +/** @internal */ +export type OP_PULL_RIGHT = typeof OP_PULL_RIGHT + +/** @internal */ +export interface DrainLeft { + readonly _tag: OP_DRAIN_LEFT +} + +/** @internal */ +export interface DrainRight { + readonly _tag: OP_DRAIN_RIGHT +} + +/** @internal */ +export interface PullBoth { + readonly _tag: OP_PULL_BOTH +} + +/** @internal */ +export interface PullLeft { + readonly _tag: OP_PULL_LEFT + readonly rightChunk: Chunk.Chunk +} + +/** @internal */ +export interface PullRight { + readonly _tag: OP_PULL_RIGHT + readonly leftChunk: Chunk.Chunk +} + +/** @internal */ +export const DrainLeft: ZipAllState = { + _tag: OP_DRAIN_LEFT +} + +/** @internal */ +export const DrainRight: ZipAllState = { + _tag: OP_DRAIN_RIGHT +} + +/** @internal */ +export const PullBoth: ZipAllState = { + _tag: OP_PULL_BOTH +} + +/** @internal */ +export const PullLeft = (rightChunk: Chunk.Chunk): ZipAllState => ({ + _tag: OP_PULL_LEFT, + rightChunk +}) + +/** @internal */ +export const PullRight = (leftChunk: Chunk.Chunk): ZipAllState => ({ + _tag: OP_PULL_RIGHT, + leftChunk +}) diff --git a/repos/effect/packages/effect/src/internal/stream/zipChunksState.ts b/repos/effect/packages/effect/src/internal/stream/zipChunksState.ts new file mode 100644 index 0000000..a873407 --- /dev/null +++ b/repos/effect/packages/effect/src/internal/stream/zipChunksState.ts @@ -0,0 +1,56 @@ +import type * as Chunk from "../../Chunk.js" + +/** @internal */ +export type ZipChunksState = PullBoth | PullLeft | PullRight + +/** @internal */ +export const OP_PULL_BOTH = "PullBoth" as const + +/** @internal */ +export type OP_PULL_BOTH = typeof OP_PULL_BOTH + +/** @internal */ +export const OP_PULL_LEFT = "PullLet" as const + +/** @internal */ +export type OP_PULL_LEFT = typeof OP_PULL_LEFT + +/** @internal */ +export const OP_PULL_RIGHT = "PullRight" as const + +/** @internal */ +export type OP_PULL_RIGHT = typeof OP_PULL_RIGHT + +/** @internal */ +export interface PullBoth { + readonly _tag: OP_PULL_BOTH +} + +/** @internal */ +export interface PullLeft { + readonly _tag: OP_PULL_LEFT + readonly rightChunk: Chunk.Chunk +} + +/** @internal */ +export interface PullRight { + readonly _tag: OP_PULL_RIGHT + readonly leftChunk: Chunk.Chunk +} + +/** @internal */ +export const PullBoth: ZipChunksState = { + _tag: OP_PULL_BOTH +} + +/** @internal */ +export const PullLeft = (rightChunk: Chunk.Chunk): ZipChunksState => ({ + _tag: OP_PULL_LEFT, + rightChunk +}) + +/** @internal */ +export const PullRight = (leftChunk: Chunk.Chunk): ZipChunksState => ({ + _tag: OP_PULL_RIGHT, + leftChunk +}) diff --git a/repos/effect/packages/effect/src/internal/string-utils.ts b/repos/effect/packages/effect/src/internal/string-utils.ts new file mode 100644 index 0000000..b59d6d0 --- /dev/null +++ b/repos/effect/packages/effect/src/internal/string-utils.ts @@ -0,0 +1,107 @@ +/** + * Adapted from the `change-case` library. + * + * Copyright (c) 2014 Blake Embrey (hello@blakeembrey.com) + */ + +/** @internal */ +export const lowerCase = (str: string) => str.toLowerCase() + +/** @internal */ +export const upperCase = (str: string) => str.toUpperCase() + +interface Options { + splitRegexp?: RegExp | ReadonlyArray + stripRegexp?: RegExp | ReadonlyArray + delimiter?: string + transform?: (part: string, index: number, parts: ReadonlyArray) => string +} + +/** + * Replace `re` in the input string with the replacement value. + */ +const replace = (input: string, re: RegExp | ReadonlyArray, value: string): string => + re instanceof RegExp + ? input.replace(re, value) + : re.reduce((input, re) => input.replace(re, value), input) + +// Support camel case ("camelCase" -> "camel Case" and "CAMELCase" -> "CAMEL Case"). +const DEFAULT_SPLIT_REGEXP = [/([a-z0-9])([A-Z])/g, /([A-Z])([A-Z][a-z])/g] + +// Remove all non-word characters. +const DEFAULT_STRIP_REGEXP = /[^A-Z0-9]+/gi + +/** + * Normalize the string into something other libraries can manipulate easier. + */ +const noCase = (input: string, options: Options = {}): string => { + const { + delimiter = " ", + splitRegexp = DEFAULT_SPLIT_REGEXP, + stripRegexp = DEFAULT_STRIP_REGEXP, + transform = lowerCase + } = options + const result = replace(replace(input, splitRegexp, "$1\0$2"), stripRegexp, "\0") + let start = 0 + let end = result.length + // Trim the delimiter from around the output string. + while (result.charAt(start) === "\0") { + start++ + } + while (result.charAt(end - 1) === "\0") { + end-- + } + // Transform each token independently. + return result.slice(start, end).split("\0").map(transform).join(delimiter) +} + +const pascalCaseTransform = (input: string, index: number): string => { + const firstChar = input.charAt(0) + const lowerChars = input.substring(1).toLowerCase() + if (index > 0 && firstChar >= "0" && firstChar <= "9") { + return `_${firstChar}${lowerChars}` + } + return `${firstChar.toUpperCase()}${lowerChars}` +} + +/** @internal */ +export const pascalCase = (input: string, options?: Options): string => + noCase(input, { + delimiter: "", + transform: pascalCaseTransform, + ...options + }) + +const camelCaseTransform = (input: string, index: number): string => + index === 0 + ? input.toLowerCase() + : pascalCaseTransform(input, index) + +/** @internal */ +export const camelCase = (input: string, options?: Options): string => + pascalCase(input, { + transform: camelCaseTransform, + ...options + }) + +/** @internal */ +export const constantCase = (input: string, options?: Options): string => + noCase(input, { + delimiter: "_", + transform: upperCase, + ...options + }) + +/** @internal */ +export const kebabCase = (input: string, options?: Options) => + noCase(input, { + delimiter: "-", + ...options + }) + +/** @internal */ +export const snakeCase = (input: string, options?: Options) => + noCase(input, { + delimiter: "_", + ...options + }) diff --git a/repos/effect/packages/effect/src/internal/subscriptionRef.ts b/repos/effect/packages/effect/src/internal/subscriptionRef.ts new file mode 100644 index 0000000..24bb69c --- /dev/null +++ b/repos/effect/packages/effect/src/internal/subscriptionRef.ts @@ -0,0 +1,138 @@ +import * as Effect from "../Effect.js" +import * as Effectable from "../Effectable.js" +import { dual, pipe } from "../Function.js" +import * as PubSub from "../PubSub.js" +import * as Readable from "../Readable.js" +import * as Ref from "../Ref.js" +import type { Stream } from "../Stream.js" +import * as Subscribable from "../Subscribable.js" +import type * as SubscriptionRef from "../SubscriptionRef.js" +import * as Synchronized from "../SynchronizedRef.js" +import * as circular_ from "./effect/circular.js" +import * as ref_ from "./ref.js" +import * as stream from "./stream.js" + +/** @internal */ +const SubscriptionRefSymbolKey = "effect/SubscriptionRef" + +/** @internal */ +export const SubscriptionRefTypeId: SubscriptionRef.SubscriptionRefTypeId = Symbol.for( + SubscriptionRefSymbolKey +) as SubscriptionRef.SubscriptionRefTypeId + +const subscriptionRefVariance = { + /* c8 ignore next */ + _A: (_: any) => _ +} + +/** @internal */ +class SubscriptionRefImpl extends Effectable.Class implements SubscriptionRef.SubscriptionRef { + readonly [Readable.TypeId]: Readable.TypeId = Readable.TypeId + readonly [Subscribable.TypeId]: Subscribable.TypeId = Subscribable.TypeId + readonly [Ref.RefTypeId] = ref_.refVariance + readonly [Synchronized.SynchronizedRefTypeId] = circular_.synchronizedVariance + readonly [SubscriptionRefTypeId] = subscriptionRefVariance + constructor( + readonly ref: Ref.Ref, + readonly pubsub: PubSub.PubSub, + readonly semaphore: Effect.Semaphore + ) { + super() + this.get = Ref.get(this.ref) + } + commit() { + return this.get + } + readonly get: Effect.Effect + get changes(): Stream { + return pipe( + Ref.get(this.ref), + Effect.flatMap((a) => + Effect.map( + stream.fromPubSub(this.pubsub, { scoped: true }), + (s) => + stream.concat( + stream.make(a), + s + ) + ) + ), + this.semaphore.withPermits(1), + stream.unwrapScoped + ) + } + modify(f: (a: A) => readonly [B, A]): Effect.Effect { + return this.modifyEffect((a) => Effect.succeed(f(a))) + } + modifyEffect(f: (a: A) => Effect.Effect): Effect.Effect { + return pipe( + Ref.get(this.ref), + Effect.flatMap(f), + Effect.flatMap(([b, a]) => + pipe( + Ref.set(this.ref, a), + Effect.as(b), + Effect.zipLeft(PubSub.publish(this.pubsub, a)) + ) + ), + this.semaphore.withPermits(1) + ) + } +} + +/** @internal */ +export const get = (self: SubscriptionRef.SubscriptionRef): Effect.Effect => Ref.get(self.ref) + +/** @internal */ +export const make = (value: A): Effect.Effect> => + pipe( + Effect.all([ + PubSub.unbounded(), + Ref.make(value), + Effect.makeSemaphore(1) + ]), + Effect.map(([pubsub, ref, semaphore]) => new SubscriptionRefImpl(ref, pubsub, semaphore)) + ) + +/** @internal */ +export const modify = dual< + (f: (a: A) => readonly [B, A]) => (self: SubscriptionRef.SubscriptionRef) => Effect.Effect, + ( + self: SubscriptionRef.SubscriptionRef, + f: (a: A) => readonly [B, A] + ) => Effect.Effect +>(2, ( + self: SubscriptionRef.SubscriptionRef, + f: (a: A) => readonly [B, A] +): Effect.Effect => self.modify(f)) + +/** @internal */ +export const modifyEffect = dual< + ( + f: (a: A) => Effect.Effect + ) => (self: SubscriptionRef.SubscriptionRef) => Effect.Effect, + ( + self: SubscriptionRef.SubscriptionRef, + f: (a: A) => Effect.Effect + ) => Effect.Effect +>(2, ( + self: SubscriptionRef.SubscriptionRef, + f: (a: A) => Effect.Effect +): Effect.Effect => self.modifyEffect(f)) + +/** @internal */ +export const set = dual< + (value: A) => (self: SubscriptionRef.SubscriptionRef) => Effect.Effect, + ( + self: SubscriptionRef.SubscriptionRef, + value: A + ) => Effect.Effect +>(2, ( + self: SubscriptionRef.SubscriptionRef, + value: A +): Effect.Effect => + pipe( + Ref.set(self.ref, value), + Effect.zipLeft(PubSub.publish(self.pubsub, value)), + self.semaphore.withPermits(1) + )) diff --git a/repos/effect/packages/effect/src/internal/supervisor.ts b/repos/effect/packages/effect/src/internal/supervisor.ts new file mode 100644 index 0000000..285df2e --- /dev/null +++ b/repos/effect/packages/effect/src/internal/supervisor.ts @@ -0,0 +1,303 @@ +import type * as Context from "../Context.js" +import type * as Effect from "../Effect.js" +import type * as Exit from "../Exit.js" +import type * as Fiber from "../Fiber.js" +import { pipe } from "../Function.js" +import { globalValue } from "../GlobalValue.js" +import * as MutableRef from "../MutableRef.js" +import type * as Option from "../Option.js" +import { hasProperty, isTagged } from "../Predicate.js" +import * as SortedSet from "../SortedSet.js" +import type * as Supervisor from "../Supervisor.js" +import * as core from "./core.js" + +/** @internal */ +const SupervisorSymbolKey = "effect/Supervisor" + +/** @internal */ +export const SupervisorTypeId: Supervisor.SupervisorTypeId = Symbol.for( + SupervisorSymbolKey +) as Supervisor.SupervisorTypeId + +/** @internal */ +export const supervisorVariance = { + /* c8 ignore next */ + _T: (_: never) => _ +} + +/** @internal */ +export class ProxySupervisor implements Supervisor.Supervisor { + readonly [SupervisorTypeId] = supervisorVariance + + constructor( + readonly underlying: Supervisor.Supervisor, + readonly value0: Effect.Effect + ) { + } + + get value(): Effect.Effect { + return this.value0 + } + + onStart( + context: Context.Context, + effect: Effect.Effect, + parent: Option.Option>, + fiber: Fiber.RuntimeFiber + ): void { + this.underlying.onStart(context, effect, parent, fiber) + } + + onEnd(value: Exit.Exit, fiber: Fiber.RuntimeFiber): void { + this.underlying.onEnd(value, fiber) + } + + onEffect(fiber: Fiber.RuntimeFiber, effect: Effect.Effect): void { + this.underlying.onEffect(fiber, effect) + } + + onSuspend(fiber: Fiber.RuntimeFiber): void { + this.underlying.onSuspend(fiber) + } + + onResume(fiber: Fiber.RuntimeFiber): void { + this.underlying.onResume(fiber) + } + + map(f: (a: T) => B): Supervisor.Supervisor { + return new ProxySupervisor(this, pipe(this.value, core.map(f))) + } + + zip(right: Supervisor.Supervisor): Supervisor.Supervisor<[T, B]> { + return new Zip(this, right) + } +} + +/** @internal */ +export class Zip implements Supervisor.Supervisor { + readonly _tag = "Zip" + readonly [SupervisorTypeId] = supervisorVariance + + constructor( + readonly left: Supervisor.Supervisor, + readonly right: Supervisor.Supervisor + ) { + } + + get value(): Effect.Effect<[T0, T1]> { + return core.zip(this.left.value, this.right.value) + } + + onStart( + context: Context.Context, + effect: Effect.Effect, + parent: Option.Option>, + fiber: Fiber.RuntimeFiber + ): void { + this.left.onStart(context, effect, parent, fiber) + this.right.onStart(context, effect, parent, fiber) + } + + onEnd(value: Exit.Exit, fiber: Fiber.RuntimeFiber): void { + this.left.onEnd(value, fiber) + this.right.onEnd(value, fiber) + } + + onEffect(fiber: Fiber.RuntimeFiber, effect: Effect.Effect): void { + this.left.onEffect(fiber, effect) + this.right.onEffect(fiber, effect) + } + + onSuspend(fiber: Fiber.RuntimeFiber): void { + this.left.onSuspend(fiber) + this.right.onSuspend(fiber) + } + + onResume(fiber: Fiber.RuntimeFiber): void { + this.left.onResume(fiber) + this.right.onResume(fiber) + } + + map(f: (a: [T0, T1]) => B): Supervisor.Supervisor { + return new ProxySupervisor(this, pipe(this.value, core.map(f))) + } + + zip(right: Supervisor.Supervisor): Supervisor.Supervisor<[[T0, T1], A]> { + return new Zip(this, right) + } +} + +/** @internal */ +export const isZip = (self: unknown): self is Zip => + hasProperty(self, SupervisorTypeId) && isTagged(self, "Zip") + +/** @internal */ +export class Track implements Supervisor.Supervisor>> { + readonly [SupervisorTypeId] = supervisorVariance + + readonly fibers: Set> = new Set() + + get value(): Effect.Effect>> { + return core.sync(() => Array.from(this.fibers)) + } + + onStart( + _context: Context.Context, + _effect: Effect.Effect, + _parent: Option.Option>, + fiber: Fiber.RuntimeFiber + ): void { + this.fibers.add(fiber) + } + + onEnd(_value: Exit.Exit, fiber: Fiber.RuntimeFiber): void { + this.fibers.delete(fiber) + } + + onEffect(_fiber: Fiber.RuntimeFiber, _effect: Effect.Effect): void { + // + } + + onSuspend(_fiber: Fiber.RuntimeFiber): void { + // + } + + onResume(_fiber: Fiber.RuntimeFiber): void { + // + } + + map(f: (a: Array>) => B): Supervisor.Supervisor { + return new ProxySupervisor(this, pipe(this.value, core.map(f))) + } + + zip( + right: Supervisor.Supervisor + ): Supervisor.Supervisor<[Array>, A]> { + return new Zip(this, right) + } + + onRun(execution: () => X, _fiber: Fiber.RuntimeFiber): X { + return execution() + } +} + +/** @internal */ +export class Const implements Supervisor.Supervisor { + readonly [SupervisorTypeId] = supervisorVariance + + constructor(readonly effect: Effect.Effect) { + } + + get value(): Effect.Effect { + return this.effect + } + + onStart( + _context: Context.Context, + _effect: Effect.Effect, + _parent: Option.Option>, + _fiber: Fiber.RuntimeFiber + ): void { + // + } + + onEnd(_value: Exit.Exit, _fiber: Fiber.RuntimeFiber): void { + // + } + + onEffect(_fiber: Fiber.RuntimeFiber, _effect: Effect.Effect): void { + // + } + + onSuspend(_fiber: Fiber.RuntimeFiber): void { + // + } + + onResume(_fiber: Fiber.RuntimeFiber): void { + // + } + + map(f: (a: T) => B): Supervisor.Supervisor { + return new ProxySupervisor(this, pipe(this.value, core.map(f))) + } + + zip(right: Supervisor.Supervisor): Supervisor.Supervisor<[T, A]> { + return new Zip(this, right) + } + + onRun(execution: () => X, _fiber: Fiber.RuntimeFiber): X { + return execution() + } +} + +class FibersIn implements Supervisor.Supervisor>> { + readonly [SupervisorTypeId] = supervisorVariance + + constructor(readonly ref: MutableRef.MutableRef>>) { + } + + get value(): Effect.Effect>> { + return core.sync(() => MutableRef.get(this.ref)) + } + + onStart( + _context: Context.Context, + _effect: Effect.Effect, + _parent: Option.Option>, + fiber: Fiber.RuntimeFiber + ): void { + pipe(this.ref, MutableRef.set(pipe(MutableRef.get(this.ref), SortedSet.add(fiber)))) + } + + onEnd(_value: Exit.Exit, fiber: Fiber.RuntimeFiber): void { + pipe(this.ref, MutableRef.set(pipe(MutableRef.get(this.ref), SortedSet.remove(fiber)))) + } + + onEffect(_fiber: Fiber.RuntimeFiber, _effect: Effect.Effect): void { + // + } + + onSuspend(_fiber: Fiber.RuntimeFiber): void { + // + } + + onResume(_fiber: Fiber.RuntimeFiber): void { + // + } + + map(f: (a: SortedSet.SortedSet>) => B): Supervisor.Supervisor { + return new ProxySupervisor(this, pipe(this.value, core.map(f))) + } + + zip( + right: Supervisor.Supervisor + ): Supervisor.Supervisor<[SortedSet.SortedSet>, A]> { + return new Zip(this, right) + } + + onRun(execution: () => X, _fiber: Fiber.RuntimeFiber): X { + return execution() + } +} + +/** @internal */ +export const unsafeTrack = (): Supervisor.Supervisor>> => { + return new Track() +} + +/** @internal */ +export const track: Effect.Effect>>> = core.sync(unsafeTrack) + +/** @internal */ +export const fromEffect = (effect: Effect.Effect): Supervisor.Supervisor => { + return new Const(effect) +} + +/** @internal */ +export const none = globalValue("effect/Supervisor/none", () => fromEffect(core.void)) + +/** @internal */ +export const fibersIn = ( + ref: MutableRef.MutableRef>> +): Effect.Effect>>> => + core.sync(() => new FibersIn(ref)) diff --git a/repos/effect/packages/effect/src/internal/supervisor/patch.ts b/repos/effect/packages/effect/src/internal/supervisor/patch.ts new file mode 100644 index 0000000..1f1a4bd --- /dev/null +++ b/repos/effect/packages/effect/src/internal/supervisor/patch.ts @@ -0,0 +1,190 @@ +import * as Chunk from "../../Chunk.js" +import * as Differ from "../../Differ.js" +import * as Equal from "../../Equal.js" +import { pipe } from "../../Function.js" +import * as HashSet from "../../HashSet.js" +import type * as Supervisor from "../../Supervisor.js" +import * as supervisor from "../supervisor.js" + +/** @internal */ +export type SupervisorPatch = Empty | AddSupervisor | RemoveSupervisor | AndThen + +/** @internal */ +export const OP_EMPTY = "Empty" as const + +/** @internal */ +export type OP_EMPTY = typeof OP_EMPTY + +/** @internal */ +export const OP_ADD_SUPERVISOR = "AddSupervisor" as const + +/** @internal */ +export type OP_ADD_SUPERVISOR = typeof OP_ADD_SUPERVISOR + +/** @internal */ +export const OP_REMOVE_SUPERVISOR = "RemoveSupervisor" as const + +/** @internal */ +export type OP_REMOVE_SUPERVISOR = typeof OP_REMOVE_SUPERVISOR + +/** @internal */ +export const OP_AND_THEN = "AndThen" as const + +/** @internal */ +export type OP_AND_THEN = typeof OP_AND_THEN + +/** @internal */ +export interface Empty { + readonly _tag: OP_EMPTY +} + +/** @internal */ +export interface AddSupervisor { + readonly _tag: OP_ADD_SUPERVISOR + readonly supervisor: Supervisor.Supervisor +} + +/** @internal */ +export interface RemoveSupervisor { + readonly _tag: OP_REMOVE_SUPERVISOR + readonly supervisor: Supervisor.Supervisor +} + +/** @internal */ +export interface AndThen { + readonly _tag: OP_AND_THEN + readonly first: SupervisorPatch + readonly second: SupervisorPatch +} + +/** + * The empty `SupervisorPatch`. + * + * @internal + */ +export const empty: SupervisorPatch = { _tag: OP_EMPTY } + +/** + * Combines two patches to produce a new patch that describes applying the + * updates from this patch and then the updates from the specified patch. + * + * @internal + */ +export const combine = (self: SupervisorPatch, that: SupervisorPatch): SupervisorPatch => { + return { + _tag: OP_AND_THEN, + first: self, + second: that + } +} + +/** + * Applies a `SupervisorPatch` to a `Supervisor` to produce a new `Supervisor`. + * + * @internal + */ +export const patch = ( + self: SupervisorPatch, + supervisor: Supervisor.Supervisor +): Supervisor.Supervisor => { + return patchLoop(supervisor, Chunk.of(self)) +} + +/** @internal */ +const patchLoop = ( + _supervisor: Supervisor.Supervisor, + _patches: Chunk.Chunk +): Supervisor.Supervisor => { + let supervisor = _supervisor + let patches = _patches + while (Chunk.isNonEmpty(patches)) { + const head = Chunk.headNonEmpty(patches) + switch (head._tag) { + case OP_EMPTY: { + patches = Chunk.tailNonEmpty(patches) + break + } + case OP_ADD_SUPERVISOR: { + supervisor = supervisor.zip(head.supervisor) + patches = Chunk.tailNonEmpty(patches) + break + } + case OP_REMOVE_SUPERVISOR: { + supervisor = removeSupervisor(supervisor, head.supervisor) + patches = Chunk.tailNonEmpty(patches) + break + } + case OP_AND_THEN: { + patches = Chunk.prepend(head.first)(Chunk.prepend(head.second)(Chunk.tailNonEmpty(patches))) + break + } + } + } + return supervisor +} + +/** @internal */ +const removeSupervisor = ( + self: Supervisor.Supervisor, + that: Supervisor.Supervisor +): Supervisor.Supervisor => { + if (Equal.equals(self, that)) { + return supervisor.none + } else { + if (supervisor.isZip(self)) { + return removeSupervisor(self.left, that).zip(removeSupervisor(self.right, that)) + } else { + return self + } + } +} + +/** @internal */ +const toSet = (self: Supervisor.Supervisor): HashSet.HashSet> => { + if (Equal.equals(self, supervisor.none)) { + return HashSet.empty() + } else { + if (supervisor.isZip(self)) { + return pipe(toSet(self.left), HashSet.union(toSet(self.right))) + } else { + return HashSet.make(self) + } + } +} + +/** @internal */ +export const diff = ( + oldValue: Supervisor.Supervisor, + newValue: Supervisor.Supervisor +): SupervisorPatch => { + if (Equal.equals(oldValue, newValue)) { + return empty + } + const oldSupervisors = toSet(oldValue) + const newSupervisors = toSet(newValue) + const added = pipe( + newSupervisors, + HashSet.difference(oldSupervisors), + HashSet.reduce( + empty as SupervisorPatch, + (patch, supervisor) => combine(patch, { _tag: OP_ADD_SUPERVISOR, supervisor }) + ) + ) + const removed = pipe( + oldSupervisors, + HashSet.difference(newSupervisors), + HashSet.reduce( + empty as SupervisorPatch, + (patch, supervisor) => combine(patch, { _tag: OP_REMOVE_SUPERVISOR, supervisor }) + ) + ) + return combine(added, removed) +} + +/** @internal */ +export const differ = Differ.make, SupervisorPatch>({ + empty, + patch, + combine, + diff +}) diff --git a/repos/effect/packages/effect/src/internal/synchronizedRef.ts b/repos/effect/packages/effect/src/internal/synchronizedRef.ts new file mode 100644 index 0000000..7726e3e --- /dev/null +++ b/repos/effect/packages/effect/src/internal/synchronizedRef.ts @@ -0,0 +1,114 @@ +import type * as Effect from "../Effect.js" +import { dual, pipe } from "../Function.js" +import * as Option from "../Option.js" +import type * as Synchronized from "../SynchronizedRef.js" +import * as core from "./core.js" + +/** @internal */ +export const getAndUpdateEffect = dual< + (f: (a: A) => Effect.Effect) => (self: Synchronized.SynchronizedRef) => Effect.Effect, + (self: Synchronized.SynchronizedRef, f: (a: A) => Effect.Effect) => Effect.Effect +>(2, (self, f) => + self.modifyEffect( + (value) => core.map(f(value), (result) => [value, result] as const) + )) + +/** @internal */ +export const getAndUpdateSomeEffect = dual< + ( + pf: (a: A) => Option.Option> + ) => (self: Synchronized.SynchronizedRef) => Effect.Effect, + ( + self: Synchronized.SynchronizedRef, + pf: (a: A) => Option.Option> + ) => Effect.Effect +>(2, (self, pf) => + self.modifyEffect((value) => { + const result = pf(value) + switch (result._tag) { + case "None": { + return core.succeed([value, value] as const) + } + case "Some": { + return core.map(result.value, (newValue) => [value, newValue] as const) + } + } + })) + +/** @internal */ +export const modify = dual< + (f: (a: A) => readonly [B, A]) => (self: Synchronized.SynchronizedRef) => Effect.Effect, + (self: Synchronized.SynchronizedRef, f: (a: A) => readonly [B, A]) => Effect.Effect +>(2, (self, f) => self.modify(f)) + +/** @internal */ +export const modifyEffect = dual< + ( + f: (a: A) => Effect.Effect + ) => (self: Synchronized.SynchronizedRef) => Effect.Effect, + ( + self: Synchronized.SynchronizedRef, + f: (a: A) => Effect.Effect + ) => Effect.Effect +>(2, (self, f) => self.modifyEffect(f)) + +/** @internal */ +export const modifySomeEffect = dual< + ( + fallback: B, + pf: (a: A) => Option.Option> + ) => (self: Synchronized.SynchronizedRef) => Effect.Effect, + ( + self: Synchronized.SynchronizedRef, + fallback: B, + pf: (a: A) => Option.Option> + ) => Effect.Effect +>(3, (self, fallback, pf) => + self.modifyEffect( + (value) => pipe(pf(value), Option.getOrElse(() => core.succeed([fallback, value] as const))) + )) + +/** @internal */ +export const updateEffect = dual< + ( + f: (a: A) => Effect.Effect + ) => (self: Synchronized.SynchronizedRef) => Effect.Effect, + (self: Synchronized.SynchronizedRef, f: (a: A) => Effect.Effect) => Effect.Effect +>(2, (self, f) => + self.modifyEffect((value) => + core.map( + f(value), + (result) => [undefined as void, result] as const + ) + )) + +/** @internal */ +export const updateAndGetEffect = dual< + (f: (a: A) => Effect.Effect) => (self: Synchronized.SynchronizedRef) => Effect.Effect, + (self: Synchronized.SynchronizedRef, f: (a: A) => Effect.Effect) => Effect.Effect +>(2, (self, f) => + self.modifyEffect( + (value) => core.map(f(value), (result) => [result, result] as const) + )) + +/** @internal */ +export const updateSomeEffect = dual< + ( + pf: (a: A) => Option.Option> + ) => (self: Synchronized.SynchronizedRef) => Effect.Effect, + ( + self: Synchronized.SynchronizedRef, + pf: (a: A) => Option.Option> + ) => Effect.Effect +>(2, (self, pf) => + self.modifyEffect((value) => { + const result = pf(value) + switch (result._tag) { + case "None": { + return core.succeed([void 0, value] as const) + } + case "Some": { + return core.map(result.value, (a) => [void 0, a] as const) + } + } + })) diff --git a/repos/effect/packages/effect/src/internal/take.ts b/repos/effect/packages/effect/src/internal/take.ts new file mode 100644 index 0000000..4509cc8 --- /dev/null +++ b/repos/effect/packages/effect/src/internal/take.ts @@ -0,0 +1,199 @@ +import * as Cause from "../Cause.js" +import * as Chunk from "../Chunk.js" +import * as Effect from "../Effect.js" +import * as Exit from "../Exit.js" +import { constFalse, constTrue, dual, pipe } from "../Function.js" +import * as Option from "../Option.js" +import { pipeArguments } from "../Pipeable.js" +import type * as Take from "../Take.js" + +/** @internal */ +const TakeSymbolKey = "effect/Take" + +/** @internal */ +export const TakeTypeId: Take.TakeTypeId = Symbol.for( + TakeSymbolKey +) as Take.TakeTypeId + +const takeVariance = { + /* c8 ignore next */ + _A: (_: never) => _, + /* c8 ignore next */ + _E: (_: never) => _ +} + +/** @internal */ +export class TakeImpl implements Take.Take { + readonly [TakeTypeId] = takeVariance + constructor(readonly exit: Exit.Exit, Option.Option>) { + } + pipe() { + return pipeArguments(this, arguments) + } +} + +/** @internal */ +export const chunk = (chunk: Chunk.Chunk): Take.Take => new TakeImpl(Exit.succeed(chunk)) + +/** @internal */ +export const die = (defect: unknown): Take.Take => new TakeImpl(Exit.die(defect)) + +/** @internal */ +export const dieMessage = (message: string): Take.Take => + new TakeImpl(Exit.die(new Cause.RuntimeException(message))) + +/** @internal */ +export const done = (self: Take.Take): Effect.Effect, Option.Option> => + Effect.suspend(() => self.exit) + +/** @internal */ +export const end: Take.Take = new TakeImpl(Exit.fail(Option.none())) + +/** @internal */ +export const fail = (error: E): Take.Take => new TakeImpl(Exit.fail(Option.some(error))) + +/** @internal */ +export const failCause = (cause: Cause.Cause): Take.Take => + new TakeImpl(Exit.failCause(pipe(cause, Cause.map(Option.some)))) + +/** @internal */ +export const fromEffect = (effect: Effect.Effect): Effect.Effect, never, R> => + Effect.matchCause(effect, { onFailure: failCause, onSuccess: of }) + +/** @internal */ +export const fromExit = (exit: Exit.Exit): Take.Take => + new TakeImpl(pipe(exit, Exit.mapBoth({ onFailure: Option.some, onSuccess: Chunk.of }))) + +/** @internal */ +export const fromPull = ( + pull: Effect.Effect, Option.Option, R> +): Effect.Effect, never, R> => + Effect.matchCause(pull, { + onFailure: (cause) => + Option.match(Cause.flipCauseOption(cause), { + onNone: () => end, + onSome: failCause + }), + onSuccess: chunk + }) + +/** @internal */ +export const isDone = (self: Take.Take): boolean => + Exit.match(self.exit, { + onFailure: (cause) => Option.isNone(Cause.flipCauseOption(cause)), + onSuccess: constFalse + }) + +/** @internal */ +export const isFailure = (self: Take.Take): boolean => + Exit.match(self.exit, { + onFailure: (cause) => Option.isSome(Cause.flipCauseOption(cause)), + onSuccess: constFalse + }) + +/** @internal */ +export const isSuccess = (self: Take.Take): boolean => + Exit.match(self.exit, { + onFailure: constFalse, + onSuccess: constTrue + }) + +/** @internal */ +export const make = ( + exit: Exit.Exit, Option.Option> +): Take.Take => new TakeImpl(exit) + +/** @internal */ +export const match = dual< + ( + options: { + readonly onEnd: () => Z + readonly onFailure: (cause: Cause.Cause) => Z2 + readonly onSuccess: (chunk: Chunk.Chunk) => Z3 + } + ) => (self: Take.Take) => Z | Z2 | Z3, + ( + self: Take.Take, + options: { + readonly onEnd: () => Z + readonly onFailure: (cause: Cause.Cause) => Z2 + readonly onSuccess: (chunk: Chunk.Chunk) => Z3 + } + ) => Z | Z2 | Z3 +>(2, ( + self: Take.Take, + { onEnd, onFailure, onSuccess }: { + readonly onEnd: () => Z + readonly onFailure: (cause: Cause.Cause) => Z2 + readonly onSuccess: (chunk: Chunk.Chunk) => Z3 + } +): Z | Z2 | Z3 => + Exit.match(self.exit, { + onFailure: (cause) => + Option.match(Cause.flipCauseOption(cause), { + onNone: onEnd, + onSome: onFailure + }), + onSuccess + })) + +/** @internal */ +export const matchEffect = dual< + ( + options: { + readonly onEnd: Effect.Effect + readonly onFailure: (cause: Cause.Cause) => Effect.Effect + readonly onSuccess: (chunk: Chunk.Chunk) => Effect.Effect + } + ) => (self: Take.Take) => Effect.Effect, + ( + self: Take.Take, + options: { + readonly onEnd: Effect.Effect + readonly onFailure: (cause: Cause.Cause) => Effect.Effect + readonly onSuccess: (chunk: Chunk.Chunk) => Effect.Effect + } + ) => Effect.Effect +>(2, ( + self: Take.Take, + { onEnd, onFailure, onSuccess }: { + readonly onEnd: Effect.Effect + readonly onFailure: (cause: Cause.Cause) => Effect.Effect + readonly onSuccess: (chunk: Chunk.Chunk) => Effect.Effect + } +): Effect.Effect => + Exit.matchEffect(self.exit, { + onFailure: (cause) => + Option.match, Effect.Effect>(Cause.flipCauseOption(cause), { + onNone: () => onEnd, + onSome: onFailure + }), + onSuccess + })) + +/** @internal */ +export const map = dual< + (f: (a: A) => B) => (self: Take.Take) => Take.Take, + (self: Take.Take, f: (a: A) => B) => Take.Take +>( + 2, + (self: Take.Take, f: (a: A) => B): Take.Take => + new TakeImpl(pipe(self.exit, Exit.map(Chunk.map(f)))) +) + +/** @internal */ +export const of = (value: A): Take.Take => new TakeImpl(Exit.succeed(Chunk.of(value))) + +/** @internal */ +export const tap = dual< + ( + f: (chunk: Chunk.Chunk) => Effect.Effect + ) => (self: Take.Take) => Effect.Effect, + ( + self: Take.Take, + f: (chunk: Chunk.Chunk) => Effect.Effect + ) => Effect.Effect +>(2, ( + self: Take.Take, + f: (chunk: Chunk.Chunk) => Effect.Effect +): Effect.Effect => pipe(self.exit, Exit.forEachEffect(f), Effect.asVoid)) diff --git a/repos/effect/packages/effect/src/internal/testing/sleep.ts b/repos/effect/packages/effect/src/internal/testing/sleep.ts new file mode 100644 index 0000000..f49e833 --- /dev/null +++ b/repos/effect/packages/effect/src/internal/testing/sleep.ts @@ -0,0 +1,27 @@ +import type * as Deferred from "../../Deferred.js" +import type * as Duration from "../../Duration.js" +import type * as FiberId from "../../FiberId.js" + +/** + * `Sleep` represents the state of a scheduled effect, including the time the + * effect is scheduled to run, a promise that can be completed to resume + * execution of the effect, and the fiber executing the effect. + * + * @internal + */ +export interface Sleep { + readonly duration: Duration.Duration + readonly deferred: Deferred.Deferred + readonly fiberId: FiberId.FiberId +} + +/** @internal */ +export const make = ( + duration: Duration.Duration, + deferred: Deferred.Deferred, + fiberId: FiberId.FiberId +): Sleep => ({ + duration, + deferred, + fiberId +}) diff --git a/repos/effect/packages/effect/src/internal/testing/suspendedWarningData.ts b/repos/effect/packages/effect/src/internal/testing/suspendedWarningData.ts new file mode 100644 index 0000000..5742f30 --- /dev/null +++ b/repos/effect/packages/effect/src/internal/testing/suspendedWarningData.ts @@ -0,0 +1,85 @@ +import type * as Fiber from "../../Fiber.js" + +/** @internal */ +export type SuspendedWarningData = Start | Pending | Done + +/** @internal */ +export const OP_SUSPENDED_WARNING_DATA_START = "Start" as const + +/** @internal */ +export type OP_SUSPENDED_WARNING_DATA_START = typeof OP_SUSPENDED_WARNING_DATA_START + +/** @internal */ +export const OP_SUSPENDED_WARNING_DATA_PENDING = "Pending" as const + +/** @internal */ +export type OP_SUSPENDED_WARNING_DATA_PENDING = typeof OP_SUSPENDED_WARNING_DATA_PENDING + +/** @internal */ +export const OP_SUSPENDED_WARNING_DATA_DONE = "Done" as const + +/** @internal */ +export type OP_SUSPENDED_WARNING_DATA_DONE = typeof OP_SUSPENDED_WARNING_DATA_DONE + +/** @internal */ +export interface Start { + readonly _tag: OP_SUSPENDED_WARNING_DATA_START +} + +/** @internal */ +export interface Pending { + readonly _tag: OP_SUSPENDED_WARNING_DATA_PENDING + readonly fiber: Fiber.Fiber +} + +/** @internal */ +export interface Done { + readonly _tag: OP_SUSPENDED_WARNING_DATA_DONE +} + +/** + * State indicating that a test has not adjusted the clock. + * + * @internal + */ +export const start: SuspendedWarningData = { + _tag: OP_SUSPENDED_WARNING_DATA_START +} + +/** + * State indicating that a test has adjusted the clock but a fiber is still + * running with a reference to the fiber that will display the warning + * message. + * + * @internal + */ +export const pending = (fiber: Fiber.Fiber): SuspendedWarningData => { + return { + _tag: OP_SUSPENDED_WARNING_DATA_PENDING, + fiber + } +} + +/** + * State indicating that the warning message has already been displayed. + * + * @internal + */ +export const done: SuspendedWarningData = { + _tag: OP_SUSPENDED_WARNING_DATA_DONE +} + +/** @internal */ +export const isStart = (self: SuspendedWarningData): self is Start => { + return self._tag === OP_SUSPENDED_WARNING_DATA_START +} + +/** @internal */ +export const isPending = (self: SuspendedWarningData): self is Pending => { + return self._tag === OP_SUSPENDED_WARNING_DATA_PENDING +} + +/** @internal */ +export const isDone = (self: SuspendedWarningData): self is Done => { + return self._tag === OP_SUSPENDED_WARNING_DATA_DONE +} diff --git a/repos/effect/packages/effect/src/internal/testing/warningData.ts b/repos/effect/packages/effect/src/internal/testing/warningData.ts new file mode 100644 index 0000000..5144dad --- /dev/null +++ b/repos/effect/packages/effect/src/internal/testing/warningData.ts @@ -0,0 +1,94 @@ +import type * as Fiber from "../../Fiber.js" + +/** + * `WarningData` describes the state of the warning message that is displayed + * if a test is using time by is not advancing the `TestClock`. The possible + * states are `Start` if a test has not used time, `Pending` if a test has + * used time but has not adjusted the `TestClock`, and `Done` if a test has + * adjusted the `TestClock` or the warning message has already been displayed. + * + * @internal + */ +export type WarningData = Start | Pending | Done + +/** @internal */ +export const OP_WARNING_DATA_START = "Start" as const + +/** @internal */ +export type OP_WARNING_DATA_START = typeof OP_WARNING_DATA_START + +/** @internal */ +export const OP_WARNING_DATA_PENDING = "Pending" as const + +/** @internal */ +export type OP_WARNING_DATA_PENDING = typeof OP_WARNING_DATA_PENDING + +/** @internal */ +export const OP_WARNING_DATA_DONE = "Done" as const + +/** @internal */ +export type OP_WARNING_DATA_DONE = typeof OP_WARNING_DATA_DONE + +/** @internal */ +export interface Start { + readonly _tag: OP_WARNING_DATA_START +} + +/** @internal */ +export interface Pending { + readonly _tag: OP_WARNING_DATA_PENDING + readonly fiber: Fiber.Fiber +} + +/** @internal */ +export interface Done { + readonly _tag: OP_WARNING_DATA_DONE +} + +/** + * State indicating that a test has not used time. + * + * @internal + */ +export const start: WarningData = { + _tag: OP_WARNING_DATA_START +} + +/** + * State indicating that a test has used time but has not adjusted the + * `TestClock` with a reference to the fiber that will display the warning + * message. + * + * @internal + */ +export const pending = (fiber: Fiber.Fiber): WarningData => { + return { + _tag: OP_WARNING_DATA_PENDING, + fiber + } +} + +/** + * State indicating that a test has used time or the warning message has + * already been displayed. + * + * @internal + */ +export const done: WarningData = { + _tag: OP_WARNING_DATA_DONE +} + +/** @internal */ +export const isStart = (self: WarningData): self is Start => { + return self._tag === OP_WARNING_DATA_START +} + +/** @internal */ +export const isPending = (self: WarningData): self is Pending => { + return self._tag === OP_WARNING_DATA_PENDING +} + +/** @internal */ +export const isDone = (self: WarningData): self is Done => { + return self._tag === OP_WARNING_DATA_DONE +} diff --git a/repos/effect/packages/effect/src/internal/tracer.ts b/repos/effect/packages/effect/src/internal/tracer.ts new file mode 100644 index 0000000..ae37dc2 --- /dev/null +++ b/repos/effect/packages/effect/src/internal/tracer.ts @@ -0,0 +1,150 @@ +/** + * @since 2.0.0 + */ +import * as Context from "../Context.js" +import type * as Exit from "../Exit.js" +import { constFalse } from "../Function.js" +import type * as Option from "../Option.js" +import type * as Tracer from "../Tracer.js" + +/** @internal */ +export const TracerTypeId: Tracer.TracerTypeId = Symbol.for("effect/Tracer") as Tracer.TracerTypeId + +/** @internal */ +export const make = (options: Omit): Tracer.Tracer => ({ + [TracerTypeId]: TracerTypeId, + ...options +}) + +/** @internal */ +export const tracerTag = Context.GenericTag("effect/Tracer") + +/** @internal */ +export const spanTag = Context.GenericTag("effect/ParentSpan") + +const randomHexString = (function() { + const characters = "abcdef0123456789" + const charactersLength = characters.length + return function(length: number) { + let result = "" + for (let i = 0; i < length; i++) { + result += characters.charAt(Math.floor(Math.random() * charactersLength)) + } + return result + } +})() + +/** @internal */ +export class NativeSpan implements Tracer.Span { + readonly _tag = "Span" + readonly spanId: string + readonly traceId: string = "native" + readonly sampled = true + + status: Tracer.SpanStatus + attributes: Map + events: Array<[name: string, startTime: bigint, attributes: Record]> = [] + links: Array + + constructor( + readonly name: string, + readonly parent: Option.Option, + readonly context: Context.Context, + links: Iterable, + readonly startTime: bigint, + readonly kind: Tracer.SpanKind + ) { + this.status = { + _tag: "Started", + startTime + } + this.attributes = new Map() + this.traceId = parent._tag === "Some" ? parent.value.traceId : randomHexString(32) + this.spanId = randomHexString(16) + this.links = Array.from(links) + } + + end(endTime: bigint, exit: Exit.Exit): void { + this.status = { + _tag: "Ended", + endTime, + exit, + startTime: this.status.startTime + } + } + + attribute(key: string, value: unknown): void { + this.attributes.set(key, value) + } + + event(name: string, startTime: bigint, attributes?: Record): void { + this.events.push([name, startTime, attributes ?? {}]) + } + + addLinks(links: ReadonlyArray): void { + // eslint-disable-next-line no-restricted-syntax + this.links.push(...links) + } +} + +/** @internal */ +export const nativeTracer: Tracer.Tracer = make({ + span: (name, parent, context, links, startTime, kind) => + new NativeSpan( + name, + parent, + context, + links, + startTime, + kind + ), + context: (f) => f() +}) + +/** @internal */ +export const externalSpan = (options: { + readonly spanId: string + readonly traceId: string + readonly sampled?: boolean | undefined + readonly context?: Context.Context | undefined +}): Tracer.ExternalSpan => ({ + _tag: "ExternalSpan", + spanId: options.spanId, + traceId: options.traceId, + sampled: options.sampled ?? true, + context: options.context ?? Context.empty() +}) + +/** @internal */ +export const addSpanStackTrace = (options: Tracer.SpanOptions | undefined): Tracer.SpanOptions => { + if (options?.captureStackTrace === false) { + return options + } else if (options?.captureStackTrace !== undefined && typeof options.captureStackTrace !== "boolean") { + return options + } + const limit = Error.stackTraceLimit + Error.stackTraceLimit = 3 + const traceError = new Error() + Error.stackTraceLimit = limit + let cache: false | string = false + return { + ...options, + captureStackTrace: () => { + if (cache !== false) { + return cache + } + if (traceError.stack !== undefined) { + const stack = traceError.stack.split("\n") + if (stack[3] !== undefined) { + cache = stack[3].trim() + return cache + } + } + } + } +} + +/** @internal */ +export const DisablePropagation = Context.Reference()("effect/Tracer/DisablePropagation", { + defaultValue: constFalse +}) diff --git a/repos/effect/packages/effect/src/internal/trie.ts b/repos/effect/packages/effect/src/internal/trie.ts new file mode 100644 index 0000000..b6c3033 --- /dev/null +++ b/repos/effect/packages/effect/src/internal/trie.ts @@ -0,0 +1,722 @@ +import * as Equal from "../Equal.js" +import { dual, identity, pipe } from "../Function.js" +import * as Hash from "../Hash.js" +import { format, NodeInspectSymbol, toJSON } from "../Inspectable.js" +import * as Option from "../Option.js" +import type * as Ordering from "../Ordering.js" +import { pipeArguments } from "../Pipeable.js" +import { hasProperty } from "../Predicate.js" +import type * as TR from "../Trie.js" +import type { NoInfer } from "../Types.js" + +const TrieSymbolKey = "effect/Trie" + +/** @internal */ +export const TrieTypeId: TR.TypeId = Symbol.for(TrieSymbolKey) as TR.TypeId + +type TraversalMap = (k: K, v: V) => A + +type TraversalFilter = (k: K, v: V) => boolean + +/** @internal */ +export interface TrieImpl extends TR.Trie { + readonly _root: Node | undefined + readonly _count: number +} + +const trieVariance = { + /* c8 ignore next */ + _Value: (_: never) => _ +} + +const TrieProto: TR.Trie = { + [TrieTypeId]: trieVariance, + [Symbol.iterator](this: TrieImpl): Iterator<[string, V]> { + return new TrieIterator(this, (k, v) => [k, v], () => true) + }, + [Hash.symbol](this: TR.Trie): number { + let hash = Hash.hash(TrieSymbolKey) + for (const item of this) { + hash ^= pipe(Hash.hash(item[0]), Hash.combine(Hash.hash(item[1]))) + } + return Hash.cached(this, hash) + }, + [Equal.symbol](this: TrieImpl, that: unknown): boolean { + if (isTrie(that)) { + const entries = Array.from(that) + return Array.from(this).every((itemSelf, i) => { + const itemThat = entries[i] + return Equal.equals(itemSelf[0], itemThat[0]) && Equal.equals(itemSelf[1], itemThat[1]) + }) + } + return false + }, + toString() { + return format(this.toJSON()) + }, + toJSON() { + return { + _id: "Trie", + values: Array.from(this).map(toJSON) + } + }, + [NodeInspectSymbol]() { + return this.toJSON() + }, + pipe() { + return pipeArguments(this, arguments) + } +} + +const makeImpl = (root: Node | undefined): TrieImpl => { + const trie = Object.create(TrieProto) + trie._root = root + trie._count = root?.count ?? 0 + return trie +} + +class TrieIterator implements IterableIterator { + stack: Array<[Node, string, boolean]> = [] + + constructor( + readonly trie: TrieImpl, + readonly f: TraversalMap, + readonly filter: TraversalFilter + ) { + const root = trie._root !== undefined ? trie._root : undefined + if (root !== undefined) { + this.stack.push([root, "", false]) + } + } + + next(): IteratorResult { + while (this.stack.length > 0) { + const [node, keyString, isAdded] = this.stack.pop()! + + if (isAdded) { + const value = node.value + if (value !== undefined) { + const key = keyString + node.key + if (this.filter(key, value)) { + return { done: false, value: this.f(key, value) } + } + } + } else { + this.addToStack(node, keyString) + } + } + + return { done: true, value: undefined } + } + + addToStack(node: Node, keyString: string) { + if (node.right !== undefined) { + this.stack.push([node.right, keyString, false]) + } + if (node.mid !== undefined) { + this.stack.push([node.mid, keyString + node.key, false]) + } + this.stack.push([node, keyString, true]) + if (node.left !== undefined) { + this.stack.push([node.left, keyString, false]) + } + } + + [Symbol.iterator](): IterableIterator { + return new TrieIterator(this.trie, this.f, this.filter) + } +} + +/** @internal */ +export const isTrie: { + (u: Iterable): u is TR.Trie + (u: unknown): u is TR.Trie +} = (u: unknown): u is TR.Trie => hasProperty(u, TrieTypeId) + +/** @internal */ +export const empty = (): TR.Trie => makeImpl(undefined) + +/** @internal */ +export const fromIterable = (entries: Iterable) => { + let trie = empty() + for (const [key, value] of entries) { + trie = insert(trie, key, value) + } + return trie +} + +/** @internal */ +export const make = >(...entries: Entries): TR.Trie< + Entries[number] extends readonly [any, infer V] ? V : never +> => { + return fromIterable(entries) +} + +/** @internal */ +export const insert = dual< + (key: string, value: V) => (self: TR.Trie) => TR.Trie, + (self: TR.Trie, key: string, value: V) => TR.Trie +>(3, (self: TR.Trie, key: string, value: V) => { + if (key.length === 0) return self + + // -1:left | 0:mid | 1:right + const dStack: Array = [] + const nStack: Array> = [] + let n: Node = (self as TrieImpl)._root ?? { + key: key[0], + count: 0 + } + const count = n.count + 1 + let cIndex = 0 + + while (cIndex < key.length) { + const c = key[cIndex] + nStack.push(n) + if (c > n.key) { + dStack.push(1) + if (n.right === undefined) { + n = { key: c, count } + } else { + n = n.right + } + } else if (c < n.key) { + dStack.push(-1) + if (n.left === undefined) { + n = { key: c, count } + } else { + n = n.left + } + } else { + if (cIndex === key.length - 1) { + n.value = value + } else if (n.mid === undefined) { + dStack.push(0) + n = { key: key[cIndex + 1], count } + } else { + dStack.push(0) + n = n.mid + } + + cIndex += 1 + } + } + + // Rebuild path to leaf node (Path-copying immutability) + for (let s = nStack.length - 2; s >= 0; --s) { + const n2 = nStack[s] + const d = dStack[s] + if (d === -1) { + // left + nStack[s] = { + key: n2.key, + count, + value: n2.value, + left: nStack[s + 1], + mid: n2.mid, + right: n2.right + } + } else if (d === 1) { + // right + nStack[s] = { + key: n2.key, + count, + value: n2.value, + left: n2.left, + mid: n2.mid, + right: nStack[s + 1] + } + } else { + // mid + nStack[s] = { + key: n2.key, + count, + value: n2.value, + left: n2.left, + mid: nStack[s + 1], + right: n2.right + } + } + } + + nStack[0].count = count + return makeImpl(nStack[0]) +}) + +/** @internal */ +export const size = (self: TR.Trie): number => (self as TrieImpl)._root?.count ?? 0 + +/** @internal */ +export const isEmpty = (self: TR.Trie): boolean => size(self) === 0 + +/** @internal */ +export const keys = (self: TR.Trie): IterableIterator => + new TrieIterator(self as TrieImpl, (key) => key, () => true) + +/** @internal */ +export const values = (self: TR.Trie): IterableIterator => + new TrieIterator(self as TrieImpl, (_, value) => value, () => true) + +/** @internal */ +export const entries = (self: TR.Trie): IterableIterator<[string, V]> => + new TrieIterator(self as TrieImpl, (key, value) => [key, value], () => true) + +/** @internal */ +export const reduce = dual< + ( + zero: Z, + f: (accumulator: Z, value: V, key: string) => Z + ) => (self: TR.Trie) => Z, + (self: TR.Trie, zero: Z, f: (accumulator: Z, value: V, key: string) => Z) => Z +>(3, (self, zero, f) => { + let accumulator = zero + for (const entry of self) { + accumulator = f(accumulator, entry[1], entry[0]) + } + return accumulator +}) + +/** @internal */ +export const map = dual< + (f: (value: V, key: string) => A) => (self: TR.Trie) => TR.Trie, + (self: TR.Trie, f: (value: V, key: string) => A) => TR.Trie +>(2, (self, f) => + reduce( + self, + empty(), + (trie, value, key) => insert(trie, key, f(value, key)) + )) + +/** @internal */ +export const filter: { + (f: (a: NoInfer, k: string) => a is B): (self: TR.Trie) => TR.Trie + (f: (a: NoInfer, k: string) => boolean): (self: TR.Trie) => TR.Trie + (self: TR.Trie, f: (a: A, k: string) => a is B): TR.Trie + (self: TR.Trie, f: (a: A, k: string) => boolean): TR.Trie +} = dual( + 2, + (self: TR.Trie, f: (a: A, k: string) => boolean): TR.Trie => + reduce( + self, + empty(), + (trie, value, key) => f(value, key) ? insert(trie, key, value) : trie + ) +) + +/** @internal */ +export const filterMap = dual< + ( + f: (value: A, key: string) => Option.Option + ) => (self: TR.Trie) => TR.Trie, + (self: TR.Trie, f: (value: A, key: string) => Option.Option) => TR.Trie +>(2, (self, f) => + reduce( + self, + empty(), + (trie, value, key) => { + const option = f(value, key) + return Option.isSome(option) ? insert(trie, key, option.value) : trie + } + )) + +/** @internal */ +export const compact = (self: TR.Trie>) => filterMap(self, identity) + +/** @internal */ +export const forEach = dual< + (f: (value: V, key: string) => void) => (self: TR.Trie) => void, + (self: TR.Trie, f: (value: V, key: string) => void) => void +>(2, (self, f) => reduce(self, void 0 as void, (_, value, key) => f(value, key))) + +/** @internal */ +export const keysWithPrefix = dual< + (prefix: string) => (self: TR.Trie) => IterableIterator, + (self: TR.Trie, prefix: string) => IterableIterator +>( + 2, + (self: TR.Trie, prefix: string): IterableIterator => + new TrieIterator(self as TrieImpl, (key) => key, (key) => key.startsWith(prefix)) +) + +/** @internal */ +export const valuesWithPrefix = dual< + (prefix: string) => (self: TR.Trie) => IterableIterator, + (self: TR.Trie, prefix: string) => IterableIterator +>( + 2, + (self: TR.Trie, prefix: string): IterableIterator => + new TrieIterator(self as TrieImpl, (_, value) => value, (key) => key.startsWith(prefix)) +) + +/** @internal */ +export const entriesWithPrefix = dual< + (prefix: string) => (self: TR.Trie) => IterableIterator<[string, V]>, + (self: TR.Trie, prefix: string) => IterableIterator<[string, V]> +>( + 2, + (self: TR.Trie, prefix: string): IterableIterator<[string, V]> => + new TrieIterator(self as TrieImpl, (key, value) => [key, value], (key) => key.startsWith(prefix)) +) + +/** @internal */ +export const toEntriesWithPrefix = dual< + (prefix: string) => (self: TR.Trie) => Array<[string, V]>, + (self: TR.Trie, prefix: string) => Array<[string, V]> +>( + 2, + (self: TR.Trie, prefix: string): Array<[string, V]> => Array.from(entriesWithPrefix(self, prefix)) +) + +/** @internal */ +export const get = dual< + (key: string) => (self: TR.Trie) => Option.Option, + (self: TR.Trie, key: string) => Option.Option +>( + 2, + (self: TR.Trie, key: string) => { + let n: Node | undefined = (self as TrieImpl)._root + if (n === undefined || key.length === 0) return Option.none() + let cIndex = 0 + while (cIndex < key.length) { + const c = key[cIndex] + if (c > n.key) { + if (n.right === undefined) { + return Option.none() + } else { + n = n.right + } + } else if (c < n.key) { + if (n.left === undefined) { + return Option.none() + } else { + n = n.left + } + } else { + if (cIndex === key.length - 1) { + return Option.fromNullable(n.value) + } else { + if (n.mid === undefined) { + return Option.none() + } else { + n = n.mid + cIndex += 1 + } + } + } + } + return Option.none() + } +) + +/** @internal */ +export const has = dual< + (key: string) => (self: TR.Trie) => boolean, + (self: TR.Trie, key: string) => boolean +>(2, (self, key) => Option.isSome(get(self, key))) + +/** @internal */ +export const unsafeGet = dual< + (key: string) => (self: TR.Trie) => V, + (self: TR.Trie, key: string) => V +>(2, (self, key) => { + const element = get(self, key) + if (Option.isNone(element)) { + throw new Error("Expected trie to contain key") + } + return element.value +}) + +/** @internal */ +export const remove = dual< + (key: string) => (self: TR.Trie) => TR.Trie, + (self: TR.Trie, key: string) => TR.Trie +>( + 2, + (self: TR.Trie, key: string) => { + let n: Node | undefined = (self as TrieImpl)._root + if (n === undefined || key.length === 0) return self + + const count = n.count - 1 + // -1:left | 0:mid | 1:right + const dStack: Array = [] + const nStack: Array> = [] + + let cIndex = 0 + while (cIndex < key.length) { + const c = key[cIndex] + if (c > n.key) { + if (n.right === undefined) { + return self + } else { + nStack.push(n) + dStack.push(1) + n = n.right + } + } else if (c < n.key) { + if (n.left === undefined) { + return self + } else { + nStack.push(n) + dStack.push(-1) + n = n.left + } + } else { + if (cIndex === key.length - 1) { + if (n.value !== undefined) { + nStack.push(n) + dStack.push(0) + cIndex += 1 + } else { + return self + } + } else { + if (n.mid === undefined) { + return self + } else { + nStack.push(n) + dStack.push(0) + n = n.mid + cIndex += 1 + } + } + } + } + + const removeNode = nStack[nStack.length - 1] + nStack[nStack.length - 1] = { + key: removeNode.key, + count, + left: removeNode.left, + mid: removeNode.mid, + right: removeNode.right + } + + // Rebuild path to leaf node (Path-copying immutability) + for (let s = nStack.length - 2; s >= 0; --s) { + const n2 = nStack[s] + const d = dStack[s] + const child = nStack[s + 1] + const nc = child.left === undefined && child.mid === undefined && child.right === undefined ? undefined : child + if (d === -1) { + // left + nStack[s] = { + key: n2.key, + count, + value: n2.value, + left: nc, + mid: n2.mid, + right: n2.right + } + } else if (d === 1) { + // right + nStack[s] = { + key: n2.key, + count, + value: n2.value, + left: n2.left, + mid: n2.mid, + right: nc + } + } else { + // mid + nStack[s] = { + key: n2.key, + count, + value: n2.value, + left: n2.left, + mid: nc, + right: n2.right + } + } + } + + nStack[0].count = count + return makeImpl(nStack[0]) + } +) + +/** @internal */ +export const removeMany = dual< + (keys: Iterable) => (self: TR.Trie) => TR.Trie, + (self: TR.Trie, keys: Iterable) => TR.Trie +>(2, (self, keys) => { + let trie = self + for (const key of keys) { + trie = remove(key)(trie) + } + return trie +}) + +/** @internal */ +export const insertMany = dual< + (iter: Iterable<[string, V]>) => (self: TR.Trie) => TR.Trie, + (self: TR.Trie, iter: Iterable<[string, V]>) => TR.Trie +>(2, (self, iter) => { + let trie = self + for (const [key, value] of iter) { + trie = insert(key, value)(trie) + } + return trie +}) + +/** @internal */ +export const modify = dual< + (key: string, f: (v: V) => V) => (self: TR.Trie) => TR.Trie, + (self: TR.Trie, key: string, f: (v: V) => V) => TR.Trie +>( + 3, + (self: TR.Trie, key: string, f: (v: V) => V): TR.Trie => { + let n: Node | undefined = (self as TrieImpl)._root + if (n === undefined || key.length === 0) return self + + // -1:left | 0:mid | 1:right + const dStack: Array = [] + const nStack: Array> = [] + + let cIndex = 0 + while (cIndex < key.length) { + const c = key[cIndex] + if (c > n.key) { + if (n.right === undefined) { + return self + } else { + nStack.push(n) + dStack.push(1) + n = n.right + } + } else if (c < n.key) { + if (n.left === undefined) { + return self + } else { + nStack.push(n) + dStack.push(-1) + n = n.left + } + } else { + if (cIndex === key.length - 1) { + if (n.value !== undefined) { + nStack.push(n) + dStack.push(0) + cIndex += 1 + } else { + return self + } + } else { + if (n.mid === undefined) { + return self + } else { + nStack.push(n) + dStack.push(0) + n = n.mid + cIndex += 1 + } + } + } + } + + const updateNode = nStack[nStack.length - 1] + if (updateNode.value === undefined) { + return self + } + + nStack[nStack.length - 1] = { + key: updateNode.key, + count: updateNode.count, + value: f(updateNode.value), // Update + left: updateNode.left, + mid: updateNode.mid, + right: updateNode.right + } + + // Rebuild path to leaf node (Path-copying immutability) + for (let s = nStack.length - 2; s >= 0; --s) { + const n2 = nStack[s] + const d = dStack[s] + const child = nStack[s + 1] + if (d === -1) { + // left + nStack[s] = { + key: n2.key, + count: n2.count, + value: n2.value, + left: child, + mid: n2.mid, + right: n2.right + } + } else if (d === 1) { + // right + nStack[s] = { + key: n2.key, + count: n2.count, + value: n2.value, + left: n2.left, + mid: n2.mid, + right: child + } + } else { + // mid + nStack[s] = { + key: n2.key, + count: n2.count, + value: n2.value, + left: n2.left, + mid: child, + right: n2.right + } + } + } + + return makeImpl(nStack[0]) + } +) + +/** @internal */ +export const longestPrefixOf = dual< + (key: string) => (self: TR.Trie) => Option.Option<[string, V]>, + (self: TR.Trie, key: string) => Option.Option<[string, V]> +>( + 2, + (self: TR.Trie, key: string) => { + let n: Node | undefined = (self as TrieImpl)._root + if (n === undefined || key.length === 0) return Option.none() + let longestPrefixNode: [string, V] | undefined = undefined + let cIndex = 0 + while (cIndex < key.length) { + const c = key[cIndex] + if (n.value !== undefined) { + longestPrefixNode = [key.slice(0, cIndex + 1), n.value] + } + + if (c > n.key) { + if (n.right === undefined) { + break + } else { + n = n.right + } + } else if (c < n.key) { + if (n.left === undefined) { + break + } else { + n = n.left + } + } else { + if (n.mid === undefined) { + break + } else { + n = n.mid + cIndex += 1 + } + } + } + + return Option.fromNullable(longestPrefixNode) + } +) + +interface Node { + key: string + count: number + value?: V | undefined + left?: Node | undefined + mid?: Node | undefined + right?: Node | undefined +} diff --git a/repos/effect/packages/effect/src/internal/version.ts b/repos/effect/packages/effect/src/internal/version.ts new file mode 100644 index 0000000..27afe5a --- /dev/null +++ b/repos/effect/packages/effect/src/internal/version.ts @@ -0,0 +1,7 @@ +let moduleVersion = "3.21.2" + +export const getCurrentVersion = () => moduleVersion + +export const setCurrentVersion = (version: string) => { + moduleVersion = version +} diff --git a/repos/effect/packages/effect/test/Array.test.ts b/repos/effect/packages/effect/test/Array.test.ts new file mode 100644 index 0000000..989e2b9 --- /dev/null +++ b/repos/effect/packages/effect/test/Array.test.ts @@ -0,0 +1,1355 @@ +import { describe, it } from "@effect/vitest" +import { assertNone, assertSome, deepStrictEqual, strictEqual, throws } from "@effect/vitest/utils" +import { + Array as Arr, + Either, + FastCheck as fc, + identity, + Number as Num, + Option, + Order, + pipe, + type Predicate, + String as Str +} from "effect" + +const symA = Symbol.for("a") +const symB = Symbol.for("b") +const symC = Symbol.for("c") + +const double = (n: number) => n * 2 + +describe("Array", () => { + it("of", () => { + deepStrictEqual(Arr.of(1), [1]) + }) + + it("fromIterable/Array should return the same reference if the iterable is an Array", () => { + const i = [1, 2, 3] + strictEqual(Arr.fromIterable(i), i) + }) + + it("fromIterable/Iterable", () => { + deepStrictEqual(Arr.fromIterable(new Set([1, 2, 3])), [1, 2, 3]) + }) + + it("ensure", () => { + deepStrictEqual(Arr.ensure(1), [1]) + deepStrictEqual(Arr.ensure(null), [null]) + deepStrictEqual(Arr.ensure([1]), [1]) + deepStrictEqual(Arr.ensure([1, 2]), [1, 2]) + deepStrictEqual(Arr.ensure(new Set([1, 2])), [new Set([1, 2])]) + }) + + describe("iterable inputs", () => { + it("prepend", () => { + deepStrictEqual(pipe([1, 2, 3], Arr.prepend(0)), [0, 1, 2, 3]) + deepStrictEqual(pipe([[2]], Arr.prepend([1])), [[1], [2]]) + + deepStrictEqual(pipe(new Set([1, 2, 3]), Arr.prepend(0)), [0, 1, 2, 3]) + deepStrictEqual(pipe(new Set([[2]]), Arr.prepend([1])), [[1], [2]]) + }) + + it("prependAll", () => { + deepStrictEqual(pipe([3, 4], Arr.prependAll([1, 2])), [1, 2, 3, 4]) + + deepStrictEqual(pipe([3, 4], Arr.prependAll(new Set([1, 2]))), [1, 2, 3, 4]) + deepStrictEqual(pipe(new Set([3, 4]), Arr.prependAll([1, 2])), [1, 2, 3, 4]) + }) + + it("append", () => { + deepStrictEqual(pipe([1, 2, 3], Arr.append(4)), [1, 2, 3, 4]) + deepStrictEqual(pipe([[1]], Arr.append([2])), [[1], [2]]) + + deepStrictEqual(pipe(new Set([1, 2, 3]), Arr.append(4)), [1, 2, 3, 4]) + deepStrictEqual(pipe(new Set([[1]]), Arr.append([2])), [[1], [2]]) + }) + + it("appendAll", () => { + deepStrictEqual(pipe([1, 2], Arr.appendAll([3, 4])), [1, 2, 3, 4]) + + deepStrictEqual(pipe([1, 2], Arr.appendAll(new Set([3, 4]))), [1, 2, 3, 4]) + deepStrictEqual(pipe(new Set([1, 2]), Arr.appendAll([3, 4])), [1, 2, 3, 4]) + }) + + it("scan", () => { + const f = (b: number, a: number) => b - a + deepStrictEqual(pipe([1, 2, 3], Arr.scan(10, f)), [10, 9, 7, 4]) + deepStrictEqual(pipe([0], Arr.scan(10, f)), [10, 10]) + deepStrictEqual(pipe([], Arr.scan(10, f)), [10]) + + deepStrictEqual(pipe(new Set([1, 2, 3]), Arr.scan(10, f)), [10, 9, 7, 4]) + deepStrictEqual(pipe(new Set([0]), Arr.scan(10, f)), [10, 10]) + deepStrictEqual(pipe(new Set([]), Arr.scan(10, f)), [10]) + }) + + it("scanRight", () => { + const f = (b: number, a: number) => a - b + deepStrictEqual(pipe([1, 2, 3], Arr.scanRight(10, f)), [-8, 9, -7, 10]) + deepStrictEqual(pipe([0], Arr.scanRight(10, f)), [-10, 10]) + deepStrictEqual(pipe([], Arr.scanRight(10, f)), [10]) + + deepStrictEqual(pipe(new Set([1, 2, 3]), Arr.scanRight(10, f)), [-8, 9, -7, 10]) + deepStrictEqual(pipe(new Set([0]), Arr.scanRight(10, f)), [-10, 10]) + deepStrictEqual(pipe(new Set([]), Arr.scanRight(10, f)), [10]) + }) + + it("tail", () => { + assertSome(Arr.tail([1, 2, 3]), [2, 3]) + assertNone(Arr.tail([])) + + assertSome(Arr.tail(new Set([1, 2, 3])), [2, 3]) + assertNone(Arr.tail(new Set([]))) + }) + + it("init", () => { + assertSome(Arr.init([1, 2, 3]), [1, 2]) + assertNone(Arr.init([])) + + assertSome(Arr.init(new Set([1, 2, 3])), [1, 2]) + assertNone(Arr.init(new Set([]))) + }) + + it("take", () => { + deepStrictEqual(pipe([1, 2, 3, 4], Arr.take(2)), [1, 2]) + deepStrictEqual(pipe([1, 2, 3, 4], Arr.take(0)), []) + // out of bounds + deepStrictEqual(pipe([1, 2, 3, 4], Arr.take(-10)), []) + deepStrictEqual(pipe([1, 2, 3, 4], Arr.take(10)), [1, 2, 3, 4]) + + deepStrictEqual(pipe(new Set([1, 2, 3, 4]), Arr.take(2)), [1, 2]) + deepStrictEqual(pipe(new Set([1, 2, 3, 4]), Arr.take(0)), []) + // out of bounds + deepStrictEqual(pipe(new Set([1, 2, 3, 4]), Arr.take(-10)), []) + deepStrictEqual(pipe(new Set([1, 2, 3, 4]), Arr.take(10)), [1, 2, 3, 4]) + }) + + it("takeRight", () => { + deepStrictEqual(pipe(Arr.empty(), Arr.takeRight(0)), []) + deepStrictEqual(pipe([1, 2], Arr.takeRight(0)), []) + deepStrictEqual(pipe([1, 2], Arr.takeRight(1)), [2]) + deepStrictEqual(pipe([1, 2], Arr.takeRight(2)), [1, 2]) + // out of bound + deepStrictEqual(pipe(Arr.empty(), Arr.takeRight(1)), []) + deepStrictEqual(pipe(Arr.empty(), Arr.takeRight(-1)), []) + deepStrictEqual(pipe([1, 2], Arr.takeRight(3)), [1, 2]) + deepStrictEqual(pipe([1, 2], Arr.takeRight(-1)), []) + + deepStrictEqual(pipe(new Set(), Arr.takeRight(0)), []) + deepStrictEqual(pipe(new Set([1, 2]), Arr.takeRight(0)), []) + deepStrictEqual(pipe(new Set([1, 2]), Arr.takeRight(1)), [2]) + deepStrictEqual(pipe(new Set([1, 2]), Arr.takeRight(2)), [1, 2]) + // out of bound + deepStrictEqual(pipe(new Set(), Arr.takeRight(1)), []) + deepStrictEqual(pipe(new Set(), Arr.takeRight(-1)), []) + deepStrictEqual(pipe(new Set([1, 2]), Arr.takeRight(3)), [1, 2]) + deepStrictEqual(pipe(new Set([1, 2]), Arr.takeRight(-1)), []) + }) + + it("takeWhile", () => { + const f = (n: number) => n % 2 === 0 + deepStrictEqual(pipe([2, 4, 3, 6], Arr.takeWhile(f)), [2, 4]) + deepStrictEqual(pipe(Arr.empty(), Arr.takeWhile(f)), []) + deepStrictEqual(pipe([1, 2, 4], Arr.takeWhile(f)), []) + deepStrictEqual(pipe([2, 4], Arr.takeWhile(f)), [2, 4]) + + deepStrictEqual(pipe(new Set([2, 4, 3, 6]), Arr.takeWhile(f)), [2, 4]) + deepStrictEqual(pipe(new Set(), Arr.takeWhile(f)), []) + deepStrictEqual(pipe(new Set([1, 2, 4]), Arr.takeWhile(f)), []) + deepStrictEqual(pipe(new Set([2, 4]), Arr.takeWhile(f)), [2, 4]) + }) + + it("span", () => { + const f = Arr.span((n) => n % 2 === 1) + const assertSpan = ( + input: Iterable, + expectedInit: ReadonlyArray, + expectedRest: ReadonlyArray + ) => { + const [init, rest] = f(input) + deepStrictEqual(init, expectedInit) + deepStrictEqual(rest, expectedRest) + } + assertSpan([1, 3, 2, 4, 5], [1, 3], [2, 4, 5]) + assertSpan(Arr.empty(), Arr.empty(), Arr.empty()) + assertSpan([1, 3], [1, 3], Arr.empty()) + assertSpan([2, 4], Arr.empty(), [2, 4]) + + assertSpan(new Set([1, 3, 2, 4, 5]), [1, 3], [2, 4, 5]) + assertSpan(new Set(), Arr.empty(), Arr.empty()) + assertSpan(new Set([1, 3]), [1, 3], Arr.empty()) + assertSpan(new Set([2, 4]), Arr.empty(), [2, 4]) + }) + + it("splitWhere", () => { + const f = Arr.splitWhere((n) => n % 2 !== 1) + const assertSplitWhere = ( + input: Iterable, + expectedInit: ReadonlyArray, + expectedRest: ReadonlyArray + ) => { + const [init, rest] = f(input) + deepStrictEqual(init, expectedInit) + deepStrictEqual(rest, expectedRest) + } + assertSplitWhere([1, 3, 2, 4, 5], [1, 3], [2, 4, 5]) + assertSplitWhere(Arr.empty(), Arr.empty(), Arr.empty()) + assertSplitWhere([1, 3], [1, 3], Arr.empty()) + assertSplitWhere([2, 4], Arr.empty(), [2, 4]) + + assertSplitWhere(new Set([1, 3, 2, 4, 5]), [1, 3], [2, 4, 5]) + assertSplitWhere(new Set(), Arr.empty(), Arr.empty()) + assertSplitWhere(new Set([1, 3]), [1, 3], Arr.empty()) + assertSplitWhere(new Set([2, 4]), Arr.empty(), [2, 4]) + }) + + it("split", () => { + deepStrictEqual(pipe(Arr.empty(), Arr.split(2)), Arr.empty()) + deepStrictEqual(pipe(Arr.make(1), Arr.split(2)), Arr.make(Arr.make(1))) + deepStrictEqual(pipe(Arr.make(1, 2), Arr.split(2)), Arr.make(Arr.make(1), Arr.make(2))) + deepStrictEqual(pipe(Arr.make(1, 2, 3, 4, 5), Arr.split(2)), Arr.make(Arr.make(1, 2, 3), Arr.make(4, 5))) + deepStrictEqual( + pipe(Arr.make(1, 2, 3, 4, 5), Arr.split(3)), + Arr.make(Arr.make(1, 2), Arr.make(3, 4), Arr.make(5)) + ) + }) + + it("drop", () => { + deepStrictEqual(pipe(Arr.empty(), Arr.drop(0)), []) + deepStrictEqual(pipe([1, 2], Arr.drop(0)), [1, 2]) + deepStrictEqual(pipe([1, 2], Arr.drop(1)), [2]) + deepStrictEqual(pipe([1, 2], Arr.drop(2)), []) + // out of bound + deepStrictEqual(pipe(Arr.empty(), Arr.drop(1)), []) + deepStrictEqual(pipe(Arr.empty(), Arr.drop(-1)), []) + deepStrictEqual(pipe([1, 2], Arr.drop(3)), []) + deepStrictEqual(pipe([1, 2], Arr.drop(-1)), [1, 2]) + + deepStrictEqual(pipe(new Set(), Arr.drop(0)), []) + deepStrictEqual(pipe(new Set([1, 2]), Arr.drop(0)), [1, 2]) + deepStrictEqual(pipe(new Set([1, 2]), Arr.drop(1)), [2]) + deepStrictEqual(pipe(new Set([1, 2]), Arr.drop(2)), []) + // out of bound + deepStrictEqual(pipe(new Set(), Arr.drop(1)), []) + deepStrictEqual(pipe(new Set(), Arr.drop(-1)), []) + deepStrictEqual(pipe(new Set([1, 2]), Arr.drop(3)), []) + deepStrictEqual(pipe(new Set([1, 2]), Arr.drop(-1)), [1, 2]) + }) + + it("dropRight", () => { + deepStrictEqual(pipe([], Arr.dropRight(0)), []) + deepStrictEqual(pipe([1, 2], Arr.dropRight(0)), [1, 2]) + deepStrictEqual(pipe([1, 2], Arr.dropRight(1)), [1]) + deepStrictEqual(pipe([1, 2], Arr.dropRight(2)), []) + // out of bound + deepStrictEqual(pipe([], Arr.dropRight(1)), []) + deepStrictEqual(pipe([1, 2], Arr.dropRight(3)), []) + deepStrictEqual(pipe([], Arr.dropRight(-1)), []) + deepStrictEqual(pipe([1, 2], Arr.dropRight(-1)), [1, 2]) + + deepStrictEqual(pipe(new Set(), Arr.dropRight(0)), []) + deepStrictEqual(pipe(new Set([1, 2]), Arr.dropRight(0)), [1, 2]) + deepStrictEqual(pipe(new Set([1, 2]), Arr.dropRight(1)), [1]) + deepStrictEqual(pipe(new Set([1, 2]), Arr.dropRight(2)), []) + // out of bound + deepStrictEqual(pipe(new Set(), Arr.dropRight(1)), []) + deepStrictEqual(pipe(new Set([1, 2]), Arr.dropRight(3)), []) + deepStrictEqual(pipe(new Set(), Arr.dropRight(-1)), []) + deepStrictEqual(pipe(new Set([1, 2]), Arr.dropRight(-1)), [1, 2]) + }) + + it("dropWhile", () => { + const f = Arr.dropWhile((n) => n > 0) + + deepStrictEqual(f([]), []) + deepStrictEqual(f([1, 2]), Arr.empty()) + deepStrictEqual(f([-1, -2]), [-1, -2]) + deepStrictEqual(f([-1, 2]), [-1, 2]) + deepStrictEqual(f([1, -2, 3]), [-2, 3]) + + deepStrictEqual(f(new Set()), []) + deepStrictEqual(f(new Set([1, 2])), Arr.empty()) + deepStrictEqual(f(new Set([-1, -2])), [-1, -2]) + deepStrictEqual(f(new Set([-1, 2])), [-1, 2]) + deepStrictEqual(f(new Set([1, -2, 3])), [-2, 3]) + }) + + it("findFirstIndex", () => { + assertNone(pipe([], Arr.findFirstIndex((n) => n % 2 === 0))) + assertSome(pipe([1, 2, 3], Arr.findFirstIndex((n) => n % 2 === 0)), 1) + assertSome(pipe([1, 2, 3, 1], Arr.findFirstIndex((n) => n % 2 === 0)), 1) + + assertNone(pipe(new Set(), Arr.findFirstIndex((n) => n % 2 === 0))) + assertSome(pipe(new Set([1, 2, 3]), Arr.findFirstIndex((n) => n % 2 === 0)), 1) + assertSome(pipe(new Set([1, 2, 3, 4]), Arr.findFirstIndex((n) => n % 2 === 0)), 1) + }) + + it("findLastIndex", () => { + assertNone(pipe([], Arr.findLastIndex((n) => n % 2 === 0))) + assertSome(pipe([1, 2, 3], Arr.findLastIndex((n) => n % 2 === 0)), 1) + assertSome(pipe([1, 2, 3, 4], Arr.findLastIndex((n) => n % 2 === 0)), 3) + + assertNone(pipe(new Set(), Arr.findLastIndex((n) => n % 2 === 0))) + assertSome(pipe(new Set([1, 2, 3]), Arr.findLastIndex((n) => n % 2 === 0)), 1) + assertSome(pipe(new Set([1, 2, 3, 4]), Arr.findLastIndex((n) => n % 2 === 0)), 3) + }) + + describe("findFirst", () => { + it("boolean-returning overloads", () => { + assertNone(pipe([], Arr.findFirst((n) => n % 2 === 0))) + assertSome(pipe([1, 2, 3], Arr.findFirst((n) => n % 2 === 0)), 2) + assertSome(pipe([1, 2, 3, 4], Arr.findFirst((n) => n % 2 === 0)), 2) + + assertNone(pipe(new Set(), Arr.findFirst((n) => n % 2 === 0))) + assertSome(pipe(new Set([1, 2, 3]), Arr.findFirst((n) => n % 2 === 0)), 2) + assertSome(pipe(new Set([1, 2, 3, 4]), Arr.findFirst((n) => n % 2 === 0)), 2) + }) + + it("Option-returning overloads", () => { + assertNone( + pipe([], Arr.findFirst((n, i) => n % 2 === 0 ? Option.some([n, i]) : Option.none())) + ) + assertSome( + pipe([1, 2, 3], Arr.findFirst((n, i) => n % 2 === 0 ? Option.some([n, i]) : Option.none())), + [2, 1] + ) + assertSome( + pipe([1, 2, 3, 4], Arr.findFirst((n, i) => n % 2 === 0 ? Option.some([n, i]) : Option.none())), + [2, 1] + ) + + assertNone( + pipe(new Set(), Arr.findFirst((n, i) => n % 2 === 0 ? Option.some([n, i]) : Option.none())) + ) + assertSome( + pipe(new Set([1, 2, 3]), Arr.findFirst((n, i) => n % 2 === 0 ? Option.some([n, i]) : Option.none())), + [2, 1] + ) + assertSome( + pipe(new Set([1, 2, 3, 4]), Arr.findFirst((n, i) => n % 2 === 0 ? Option.some([n, i]) : Option.none())), + [2, 1] + ) + }) + }) + + describe("findFirstWithIndex", () => { + it("boolean-returning overloads", () => { + assertNone(pipe([], Arr.findFirstWithIndex((n) => n % 2 === 0))) + assertSome(pipe([1, 2, 3], Arr.findFirstWithIndex((n) => n % 2 === 0)), [2, 1]) + assertSome(pipe([1, 2, 3, 4], Arr.findFirstWithIndex((n) => n % 2 === 0)), [2, 1]) + + assertNone(pipe(new Set(), Arr.findFirstWithIndex((n) => n % 2 === 0))) + assertSome(pipe(new Set([1, 2, 3]), Arr.findFirstWithIndex((n) => n % 2 === 0)), [2, 1]) + assertSome(pipe(new Set([1, 2, 3, 4]), Arr.findFirstWithIndex((n) => n % 2 === 0)), [2, 1]) + }) + + it("Option-returning overloads", () => { + assertNone( + pipe([], Arr.findFirstWithIndex((n) => n % 2 === 0 ? Option.some(n + 1) : Option.none())) + ) + assertSome( + pipe([1, 2, 3], Arr.findFirstWithIndex((n) => n % 2 === 0 ? Option.some(n + 1) : Option.none())), + [3, 1] + ) + assertSome( + pipe([1, 2, 3, 4], Arr.findFirstWithIndex((n) => n % 2 === 0 ? Option.some(n + 1) : Option.none())), + [3, 1] + ) + + assertNone( + pipe(new Set(), Arr.findFirstWithIndex((n) => n % 2 === 0 ? Option.some(n + 1) : Option.none())) + ) + assertSome( + pipe(new Set([1, 2, 3]), Arr.findFirstWithIndex((n) => n % 2 === 0 ? Option.some(n + 1) : Option.none())), + [3, 1] + ) + assertSome( + pipe(new Set([1, 2, 3, 4]), Arr.findFirstWithIndex((n) => n % 2 === 0 ? Option.some(n + 1) : Option.none())), + [3, 1] + ) + }) + }) + + describe("findLast", () => { + it("boolean-returning overloads", () => { + assertNone(pipe([], Arr.findLast((n) => n % 2 === 0))) + assertSome(pipe([1, 2, 3], Arr.findLast((n) => n % 2 === 0)), 2) + assertSome(pipe([1, 2, 3, 4], Arr.findLast((n) => n % 2 === 0)), 4) + + assertNone(pipe(new Set(), Arr.findLast((n) => n % 2 === 0))) + assertSome(pipe(new Set([1, 2, 3]), Arr.findLast((n) => n % 2 === 0)), 2) + assertSome(pipe(new Set([1, 2, 3, 4]), Arr.findLast((n) => n % 2 === 0)), 4) + }) + + it("Option-returning overloads", () => { + assertNone( + pipe([], Arr.findLast((n, i) => n % 2 === 0 ? Option.some([n, i]) : Option.none())) + ) + assertSome( + pipe([1, 2, 3], Arr.findLast((n, i) => n % 2 === 0 ? Option.some([n, i]) : Option.none())), + [2, 1] + ) + assertSome( + pipe([1, 2, 3, 4], Arr.findLast((n, i) => n % 2 === 0 ? Option.some([n, i]) : Option.none())), + [4, 3] + ) + + assertNone( + pipe(new Set(), Arr.findLast((n, i) => n % 2 === 0 ? Option.some([n, i]) : Option.none())) + ) + assertSome( + pipe(new Set([1, 2, 3]), Arr.findLast((n, i) => n % 2 === 0 ? Option.some([n, i]) : Option.none())), + [2, 1] + ) + assertSome( + pipe(new Set([1, 2, 3, 4]), Arr.findLast((n, i) => n % 2 === 0 ? Option.some([n, i]) : Option.none())), + [4, 3] + ) + }) + }) + + it("insertAt", () => { + assertNone(Arr.insertAt(1, 1)([])) + assertSome(Arr.insertAt(0, 1)([]), [1]) + assertSome(Arr.insertAt(2, 5)([1, 2, 3, 4]), [1, 2, 5, 3, 4]) + // out of bound + assertNone(Arr.insertAt(-1, 5)([1, 2, 3, 4])) + assertNone(Arr.insertAt(10, 5)([1, 2, 3, 4])) + + assertNone(Arr.insertAt(1, 1)(new Set([]))) + assertSome(Arr.insertAt(0, 1)(new Set([])), [1]) + assertSome(Arr.insertAt(2, 5)(new Set([1, 2, 3, 4])), [1, 2, 5, 3, 4]) + // out of bound + assertNone(Arr.insertAt(-1, 5)(new Set([1, 2, 3, 4]))) + assertNone(Arr.insertAt(10, 5)(new Set([1, 2, 3, 4]))) + }) + + it("replace", () => { + deepStrictEqual(pipe([1, 2, 3], Arr.replace(1, "a")), [1, "a", 3]) + // out of bound + deepStrictEqual(pipe([], Arr.replace(1, "a")), []) + deepStrictEqual(pipe([1, 2, 3], Arr.replace(-1, "a")), [1, 2, 3]) + deepStrictEqual(pipe([1, 2, 3], Arr.replace(10, "a")), [1, 2, 3]) + + deepStrictEqual(pipe(new Set([1, 2, 3]), Arr.replace(1, "a")), [1, "a", 3]) + // out of bound + deepStrictEqual(pipe(new Set([]), Arr.replace(1, "a")), []) + deepStrictEqual(pipe(new Set([1, 2, 3]), Arr.replace(-1, "a")), [1, 2, 3]) + deepStrictEqual(pipe(new Set([1, 2, 3]), Arr.replace(10, "a")), [1, 2, 3]) + }) + + it("replaceOption", () => { + assertSome(pipe([1, 2, 3], Arr.replaceOption(1, "a")), [1, "a", 3]) + // out of bound + assertNone(pipe([], Arr.replaceOption(1, "a"))) + assertNone(pipe([1, 2, 3], Arr.replaceOption(-1, "a"))) + assertNone(pipe([1, 2, 3], Arr.replaceOption(10, "a"))) + + assertSome(pipe(new Set([1, 2, 3]), Arr.replaceOption(1, "a")), [1, "a", 3]) + // out of bound + assertNone(pipe(new Set([]), Arr.replaceOption(1, "a"))) + assertNone(pipe(new Set([1, 2, 3]), Arr.replaceOption(-1, "a"))) + assertNone(pipe(new Set([1, 2, 3]), Arr.replaceOption(10, "a"))) + }) + + it("modify", () => { + deepStrictEqual(pipe([1, 2, 3], Arr.modify(1, double)), [1, 4, 3]) + // out of bound + deepStrictEqual(pipe([], Arr.modify(1, double)), []) + deepStrictEqual(pipe([1, 2, 3], Arr.modify(10, double)), [1, 2, 3]) + + deepStrictEqual(pipe(new Set([1, 2, 3]), Arr.modify(1, double)), [1, 4, 3]) + // out of bound + deepStrictEqual(pipe(new Set([]), Arr.modify(1, double)), []) + deepStrictEqual(pipe(new Set([1, 2, 3]), Arr.modify(10, double)), [1, 2, 3]) + }) + + it("modifyOption", () => { + assertSome(pipe([1, 2, 3], Arr.modifyOption(1, double)), [1, 4, 3]) + // out of bound + assertNone(pipe([], Arr.modifyOption(1, double))) + assertNone(pipe([1, 2, 3], Arr.modifyOption(10, double))) + + assertSome(pipe(new Set([1, 2, 3]), Arr.modifyOption(1, double)), [1, 4, 3]) + // out of bound + assertNone(pipe(new Set([]), Arr.modifyOption(1, double))) + assertNone(pipe(new Set([1, 2, 3]), Arr.modifyOption(10, double))) + }) + + it("remove", () => { + deepStrictEqual(pipe([1, 2, 3], Arr.remove(0)), [2, 3]) + // out of bound + deepStrictEqual(pipe([], Arr.remove(0)), []) + deepStrictEqual(pipe([1, 2, 3], Arr.remove(-1)), [1, 2, 3]) + deepStrictEqual(pipe([1, 2, 3], Arr.remove(10)), [1, 2, 3]) + + deepStrictEqual(pipe(new Set([1, 2, 3]), Arr.remove(0)), [2, 3]) + // out of bound + deepStrictEqual(pipe(new Set([]), Arr.remove(0)), []) + deepStrictEqual(pipe(new Set([1, 2, 3]), Arr.remove(-1)), [1, 2, 3]) + deepStrictEqual(pipe(new Set([1, 2, 3]), Arr.remove(10)), [1, 2, 3]) + }) + + it("removeOption", () => { + assertSome(pipe([1, 2, 3], Arr.removeOption(0)), [2, 3]) + // out of bound + assertNone(pipe([], Arr.removeOption(0))) + assertNone(pipe([1, 2, 3], Arr.removeOption(-1))) + assertNone(pipe([1, 2, 3], Arr.removeOption(10))) + + assertSome(pipe(new Set([1, 2, 3]), Arr.removeOption(0)), [2, 3]) + // out of bound + assertNone(pipe(new Set([]), Arr.removeOption(0))) + assertNone(pipe(new Set([1, 2, 3]), Arr.removeOption(-1))) + assertNone(pipe(new Set([1, 2, 3]), Arr.removeOption(10))) + }) + + it("reverse", () => { + deepStrictEqual(Arr.reverse([]), []) + deepStrictEqual(Arr.reverse([1]), [1]) + deepStrictEqual(Arr.reverse([1, 2, 3]), [3, 2, 1]) + + deepStrictEqual(Arr.reverse(new Set([])), []) + deepStrictEqual(Arr.reverse(new Set([1])), [1]) + deepStrictEqual(Arr.reverse(new Set([1, 2, 3])), [3, 2, 1]) + }) + + it("sort", () => { + deepStrictEqual(Arr.sort(Num.Order)([]), []) + deepStrictEqual(Arr.sort(Num.Order)([1, 3, 2]), [1, 2, 3]) + + deepStrictEqual(Arr.sort(Num.Order)(new Set()), []) + deepStrictEqual(Arr.sort(Num.Order)(new Set([1, 3, 2])), [1, 2, 3]) + }) + + it("zip", () => { + deepStrictEqual(pipe(new Set([]), Arr.zip(new Set(["a", "b", "c", "d"]))), []) + deepStrictEqual(pipe(new Set([1, 2, 3]), Arr.zip(new Set([]))), []) + deepStrictEqual(pipe(new Set([1, 2, 3]), Arr.zip(new Set(["a", "b", "c", "d"]))), [ + [1, "a"], + [2, "b"], + [3, "c"] + ]) + deepStrictEqual(pipe(new Set([1, 2, 3]), Arr.zip(new Set(["a", "b", "c", "d"]))), [ + [1, "a"], + [2, "b"], + [3, "c"] + ]) + }) + + it("zipWith", () => { + deepStrictEqual( + pipe(new Set([1, 2, 3]), Arr.zipWith(new Set([]), (n, s) => s + n)), + [] + ) + deepStrictEqual( + pipe(new Set([]), Arr.zipWith(new Set(["a", "b", "c", "d"]), (n, s) => s + n)), + [] + ) + deepStrictEqual( + pipe(new Set([]), Arr.zipWith(new Set([]), (n, s) => s + n)), + [] + ) + deepStrictEqual( + pipe(new Set([1, 2, 3]), Arr.zipWith(new Set(["a", "b", "c", "d"]), (n, s) => s + n)), + ["a1", "b2", "c3"] + ) + }) + + it("unzip", () => { + deepStrictEqual(Arr.unzip(new Set([])), [[], []]) + deepStrictEqual( + Arr.unzip( + new Set( + [ + [1, "a"], + [2, "b"], + [3, "c"] + ] as const + ) + ), + [ + [1, 2, 3], + ["a", "b", "c"] + ] + ) + }) + + it("intersperse", () => { + deepStrictEqual(pipe([], Arr.intersperse(0)), []) + deepStrictEqual(pipe([1], Arr.intersperse(0)), [1]) + deepStrictEqual(pipe([1, 2, 3], Arr.intersperse(0)), [1, 0, 2, 0, 3]) + deepStrictEqual(pipe([1, 2], Arr.intersperse(0)), [1, 0, 2]) + deepStrictEqual(pipe([1, 2, 3, 4], Arr.intersperse(0)), [1, 0, 2, 0, 3, 0, 4]) + + deepStrictEqual(pipe(new Set([]), Arr.intersperse(0)), []) + deepStrictEqual(pipe(new Set([1]), Arr.intersperse(0)), [1]) + deepStrictEqual(pipe(new Set([1, 2, 3]), Arr.intersperse(0)), [1, 0, 2, 0, 3]) + deepStrictEqual(pipe(new Set([1, 2]), Arr.intersperse(0)), [1, 0, 2]) + deepStrictEqual(pipe(new Set([1, 2, 3, 4]), Arr.intersperse(0)), [1, 0, 2, 0, 3, 0, 4]) + }) + + it("rotate", () => { + deepStrictEqual(Arr.rotate(0)(Arr.empty()), Arr.empty()) + deepStrictEqual(Arr.rotate(1)(Arr.empty()), Arr.empty()) + deepStrictEqual(Arr.rotate(1)([1]), [1]) + deepStrictEqual(Arr.rotate(2)([1]), [1]) + deepStrictEqual(Arr.rotate(-1)([1]), [1]) + deepStrictEqual(Arr.rotate(-2)([1]), [1]) + deepStrictEqual(Arr.rotate(2)([1, 2]), [1, 2]) + deepStrictEqual(Arr.rotate(0)([1, 2]), [1, 2]) + deepStrictEqual(Arr.rotate(-2)([1, 2]), [1, 2]) + deepStrictEqual(Arr.rotate(1)([1, 2]), [2, 1]) + deepStrictEqual(Arr.rotate(1)(new Set([1, 2, 3, 4, 5])), [5, 1, 2, 3, 4]) + deepStrictEqual(Arr.rotate(2)(new Set([1, 2, 3, 4, 5])), [4, 5, 1, 2, 3]) + deepStrictEqual(Arr.rotate(-1)(new Set([1, 2, 3, 4, 5])), [2, 3, 4, 5, 1]) + deepStrictEqual(Arr.rotate(-2)(new Set([1, 2, 3, 4, 5])), [3, 4, 5, 1, 2]) + // out of bounds + deepStrictEqual(Arr.rotate(7)([1, 2, 3, 4, 5]), [4, 5, 1, 2, 3]) + deepStrictEqual(Arr.rotate(-7)([1, 2, 3, 4, 5]), [3, 4, 5, 1, 2]) + deepStrictEqual(Arr.rotate(2.2)([1, 2, 3, 4, 5]), [4, 5, 1, 2, 3]) + deepStrictEqual(Arr.rotate(-2.2)([1, 2, 3, 4, 5]), [3, 4, 5, 1, 2]) + }) + + it("containsWith", () => { + const contains = Arr.containsWith(Num.Equivalence) + deepStrictEqual(pipe([1, 2, 3], contains(2)), true) + deepStrictEqual(pipe([1, 2, 3], contains(0)), false) + + deepStrictEqual(pipe(new Set([1, 2, 3]), contains(2)), true) + deepStrictEqual(pipe(new Set([1, 2, 3]), contains(0)), false) + }) + + it("contains", () => { + const contains = Arr.contains + deepStrictEqual(pipe([1, 2, 3], contains(2)), true) + deepStrictEqual(pipe([1, 2, 3], contains(0)), false) + + deepStrictEqual(pipe(new Set([1, 2, 3]), contains(2)), true) + deepStrictEqual(pipe(new Set([1, 2, 3]), contains(0)), false) + }) + + it("dedupeWith", () => { + const dedupe = Arr.dedupeWith(Num.Equivalence) + deepStrictEqual(dedupe([]), []) + deepStrictEqual(dedupe([-0, -0]), [-0]) + deepStrictEqual(dedupe([0, -0]), [0]) + deepStrictEqual(dedupe([1]), [1]) + deepStrictEqual(dedupe([2, 1, 2]), [2, 1]) + deepStrictEqual(dedupe([1, 2, 1]), [1, 2]) + deepStrictEqual(dedupe([1, 2, 3, 4, 5]), [1, 2, 3, 4, 5]) + deepStrictEqual(dedupe([1, 1, 2, 2, 3, 3, 4, 4, 5, 5]), [1, 2, 3, 4, 5]) + deepStrictEqual(dedupe([1, 2, 3, 4, 5, 1, 2, 3, 4, 5]), [1, 2, 3, 4, 5]) + }) + + it("dedupeAdjacentWith", () => { + const dedupeAdjacent = Arr.dedupeAdjacentWith(Num.Equivalence) + deepStrictEqual(dedupeAdjacent([]), []) + deepStrictEqual(dedupeAdjacent([1, 2, 3]), [1, 2, 3]) + deepStrictEqual(dedupeAdjacent([1, 2, 2, 3, 3]), [1, 2, 3]) + }) + + it("splitAt", () => { + const assertSplitAt = ( + input: ReadonlyArray, + index: number, + expectedInit: ReadonlyArray, + expectedRest: ReadonlyArray + ) => { + const [init, rest] = Arr.splitAt(index)(input) + deepStrictEqual(init, expectedInit) + deepStrictEqual(rest, expectedRest) + } + deepStrictEqual(Arr.splitAt(1)([1, 2]), [[1], [2]]) + assertSplitAt([1, 2], 2, [1, 2], []) + deepStrictEqual(Arr.splitAt(2)([1, 2, 3, 4, 5]), [ + [1, 2], + [3, 4, 5] + ]) + deepStrictEqual(Arr.splitAt(2)(new Set([1, 2, 3, 4, 5])), [ + [1, 2], + [3, 4, 5] + ]) + assertSplitAt([], 0, [], []) + assertSplitAt([1, 2], 0, [], [1, 2]) + + // out of bounds + assertSplitAt([], -1, [], []) + assertSplitAt([1, 2], -1, [], [1, 2]) + assertSplitAt([1, 2], 3, [1, 2], []) + assertSplitAt([], 3, [], []) + }) + }) + + it("splitNonEmptyAt", () => { + deepStrictEqual(pipe(Arr.make(1, 2, 3, 4), Arr.splitNonEmptyAt(2)), [[1, 2], [3, 4]]) + deepStrictEqual(pipe(Arr.make(1, 2, 3, 4), Arr.splitNonEmptyAt(10)), [[1, 2, 3, 4], []]) + }) + + describe("unsafeGet", () => { + it("should throw on index out of bound", () => { + throws(() => pipe([], Arr.unsafeGet(100)), new Error("Index 100 out of bounds")) + }) + }) + + it("fromNullable", () => { + deepStrictEqual(Arr.fromNullable(undefined), []) + deepStrictEqual(Arr.fromNullable(null), []) + deepStrictEqual(Arr.fromNullable(1), [1]) + }) + + it("liftNullable", () => { + const f = Arr.liftNullable((n: number) => (n > 0 ? n : null)) + deepStrictEqual(f(1), [1]) + deepStrictEqual(f(-1), []) + }) + + it("flatMapNullable", () => { + const f = Arr.flatMapNullable((n: number) => (n > 0 ? n : null)) + deepStrictEqual(pipe([], f), []) + deepStrictEqual(pipe([1], f), [1]) + deepStrictEqual(pipe([1, 2], f), [1, 2]) + deepStrictEqual(pipe([-1], f), []) + deepStrictEqual(pipe([-1, 2], f), [2]) + }) + + it("liftPredicate", () => { + const p = (n: number): boolean => n > 2 + const f = Arr.liftPredicate(p) + deepStrictEqual(f(1), []) + deepStrictEqual(f(3), [3]) + }) + + it("liftOption", () => { + const f = Arr.liftOption((n: number) => (n > 0 ? Option.some(n) : Option.none())) + deepStrictEqual(f(1), [1]) + deepStrictEqual(f(-1), []) + }) + + it("unprepend", () => { + deepStrictEqual(Arr.unprepend([0]), [0, []]) + deepStrictEqual(Arr.unprepend([1, 2, 3, 4]), [1, [2, 3, 4]]) + }) + + it("unappend", () => { + deepStrictEqual(Arr.unappend([0]), [[], 0]) + deepStrictEqual(Arr.unappend([1, 2, 3, 4]), [ + Arr.make(1, 2, 3), + 4 + ]) + deepStrictEqual(Arr.unappend([0]), [[], 0]) + deepStrictEqual(Arr.unappend([1, 2, 3, 4]), [ + Arr.make(1, 2, 3), + 4 + ]) + }) + + it("modifyNonEmptyHead", () => { + const f = (s: string) => s + "!" + deepStrictEqual(pipe(["a"], Arr.modifyNonEmptyHead(f)), ["a!"]) + deepStrictEqual(pipe(["a", "b"], Arr.modifyNonEmptyHead(f)), ["a!", "b"]) + deepStrictEqual(pipe(["a", "b", "c"], Arr.modifyNonEmptyHead(f)), ["a!", "b", "c"]) + }) + + it("modifyNonEmptyLast", () => { + const f = (s: string) => s + "!" + deepStrictEqual(pipe(["a"], Arr.modifyNonEmptyLast(f)), ["a!"]) + deepStrictEqual(pipe(["a", "b"], Arr.modifyNonEmptyLast(f)), ["a", "b!"]) + deepStrictEqual(pipe(["a", "b", "c"], Arr.modifyNonEmptyLast(f)), ["a", "b", "c!"]) + }) + + it("setNonEmptyHead", () => { + deepStrictEqual(pipe(Arr.make("a"), Arr.setNonEmptyHead("d")), ["d"]) + deepStrictEqual(pipe(Arr.make("a", "b"), Arr.setNonEmptyHead("d")), ["d", "b"]) + deepStrictEqual(pipe(Arr.make("a", "b", "c"), Arr.setNonEmptyHead("d")), ["d", "b", "c"]) + }) + + it("setNonEmptyLast", () => { + deepStrictEqual(pipe(Arr.make("a"), Arr.setNonEmptyLast("d")), ["d"]) + deepStrictEqual(pipe(Arr.make("a", "b"), Arr.setNonEmptyLast("d")), ["a", "d"]) + deepStrictEqual(pipe(Arr.make("a", "b", "c"), Arr.setNonEmptyLast("d")), ["a", "b", "d"]) + }) + + it("liftEither", () => { + const f = Arr.liftEither((s: string) => s.length > 2 ? Either.right(s.length) : Either.left("e")) + deepStrictEqual(f("a"), []) + deepStrictEqual(f("aaa"), [3]) + }) + + it("headNonEmpty", () => { + deepStrictEqual(Arr.headNonEmpty(Arr.make(1, 2)), 1) + }) + + it("tailNonEmpty", () => { + deepStrictEqual(Arr.tailNonEmpty(Arr.make(1, 2)), [2]) + }) + + it("lastNonEmpty", () => { + deepStrictEqual(Arr.lastNonEmpty(Arr.make(1, 2, 3)), 3) + deepStrictEqual(Arr.lastNonEmpty([1]), 1) + }) + + it("initNonEmpty", () => { + deepStrictEqual( + Arr.initNonEmpty(Arr.make(1, 2, 3)), + Arr.make(1, 2) + ) + deepStrictEqual(Arr.initNonEmpty([1]), []) + }) + + it("get", () => { + assertSome(pipe([1, 2, 3], Arr.get(0)), 1) + assertNone(pipe([1, 2, 3], Arr.get(3))) + }) + + it("unfold", () => { + const as = Arr.unfold(5, (n) => (n > 0 ? Option.some([n, n - 1]) : Option.none())) + deepStrictEqual(as, [5, 4, 3, 2, 1]) + }) + + it("map", () => { + deepStrictEqual( + pipe([1, 2, 3], Arr.map((n) => n * 2)), + [2, 4, 6] + ) + deepStrictEqual( + pipe(["a", "b"], Arr.map((s, i) => s + i)), + ["a0", "b1"] + ) + }) + + it("flatMap", () => { + deepStrictEqual( + pipe([1, 2, 3], Arr.flatMap((n) => [n, n + 1])), + [1, 2, 2, 3, 3, 4] + ) + const f = Arr.flatMap((n: number, i) => [n + i]) + deepStrictEqual(pipe([], f), []) + deepStrictEqual(pipe([1, 2, 3], f), [1, 3, 5]) + }) + + it("extend", () => { + deepStrictEqual(pipe([1, 2, 3, 4], Arr.extend(Num.sumAll)), [10, 9, 7, 4]) + deepStrictEqual(pipe([1, 2, 3, 4], Arr.extend(identity)), [ + [1, 2, 3, 4], + [2, 3, 4], + [3, 4], + [4] + ]) + }) + + it("compact", () => { + deepStrictEqual(Arr.getSomes([]), []) + deepStrictEqual(Arr.getSomes([Option.some(1), Option.some(2), Option.some(3)]), [ + 1, + 2, + 3 + ]) + deepStrictEqual(Arr.getSomes([Option.some(1), Option.none(), Option.some(3)]), [ + 1, + 3 + ]) + }) + + it("separate", () => { + deepStrictEqual(Arr.separate([]), [[], []]) + deepStrictEqual(Arr.separate([Either.right(1), Either.left("e"), Either.left(2), Either.right(2)]), [ + ["e", 2], + [1, 2] + ]) + }) + + it("filter", () => { + deepStrictEqual(Arr.filter([1, 2, 3], (n) => n % 2 === 1), [1, 3]) + deepStrictEqual(Arr.filter([Option.some(3), Option.some(2), Option.some(1)], Option.isSome), [ + Option.some(3), + Option.some(2), + Option.some(1) + ]) + deepStrictEqual(Arr.filter([Option.some(3), Option.none(), Option.some(1)], Option.isSome), [ + Option.some(3), + Option.some(1) + ]) + deepStrictEqual(Arr.filter(["a", "b", "c"], (_, i) => i % 2 === 0), ["a", "c"]) + }) + + it("filterMap", () => { + const f = (n: number) => (n % 2 === 0 ? Option.none() : Option.some(n)) + deepStrictEqual(pipe([1, 2, 3], Arr.filterMap(f)), [1, 3]) + deepStrictEqual(pipe([], Arr.filterMap(f)), []) + const g = (n: number, i: number) => ((i + n) % 2 === 0 ? Option.none() : Option.some(n)) + deepStrictEqual(pipe([1, 2, 4], Arr.filterMap(g)), [1, 2]) + deepStrictEqual(pipe([], Arr.filterMap(g)), []) + }) + + it("partitionMap", () => { + deepStrictEqual(Arr.partitionMap([], identity), [[], []]) + deepStrictEqual(Arr.partitionMap([Either.right(1), Either.left("a"), Either.right(2)], identity), [["a"], [1, 2]]) + }) + + it("partition", () => { + deepStrictEqual(Arr.partition([], (n) => n > 2), [[], []]) + deepStrictEqual(Arr.partition([1, 3], (n) => n > 2), [[1], [3]]) + + deepStrictEqual(Arr.partition([], (n, i) => n + i > 2), [[], []]) + deepStrictEqual(Arr.partition([1, 2], (n, i) => n + i > 2), [[1], [2]]) + }) + + it("reduce", () => { + deepStrictEqual(pipe(["a", "b", "c"], Arr.reduce("", (b, a) => b + a)), "abc") + deepStrictEqual( + pipe( + ["a", "b"], + Arr.reduce("", (b, a, i) => b + i + a) + ), + "0a1b" + ) + }) + + it("reduceRight", () => { + const f = (b: string, a: string) => b + a + deepStrictEqual(pipe(["a", "b", "c"], Arr.reduceRight("", f)), "cba") + deepStrictEqual(pipe([], Arr.reduceRight("", f)), "") + deepStrictEqual( + pipe( + ["a", "b"], + Arr.reduceRight("", (b, a, i) => b + i + a) + ), + "1b0a" + ) + }) + + it("getOrder", () => { + const O = Arr.getOrder(Str.Order) + deepStrictEqual(O([], []), 0) + deepStrictEqual(O(["a"], ["a"]), 0) + + deepStrictEqual(O(["a"], ["b"]), -1) + deepStrictEqual(O(["b"], ["a"]), 1) + + deepStrictEqual(O([], ["a"]), -1) + deepStrictEqual(O(["a"], []), 1) + deepStrictEqual(O(["a"], ["a", "a"]), -1) + deepStrictEqual(O(["b"], ["a", "a"]), 1) + + deepStrictEqual(O(["a", "a"], ["a", "a"]), 0) + deepStrictEqual(O(["a", "b"], ["a", "b"]), 0) + + deepStrictEqual(O(["a", "b"], ["a", "a"]), 1) + deepStrictEqual(O(["a", "a"], ["a", "b"]), -1) + + deepStrictEqual(O(["b", "a"], ["a", "b"]), 1) + deepStrictEqual(O(["a", "a"], ["b", "a"]), -1) + deepStrictEqual(O(["a", "b"], ["b", "a"]), -1) + deepStrictEqual(O(["b", "a"], ["b", "b"]), -1) + deepStrictEqual(O(["b", "b"], ["b", "a"]), 1) + }) + + it("isEmptyReadonlyArray", () => { + deepStrictEqual(Arr.isEmptyReadonlyArray([1, 2, 3]), false) + deepStrictEqual(Arr.isEmptyReadonlyArray([]), true) + }) + + it("isEmptyArray", () => { + deepStrictEqual(Arr.isEmptyArray([1, 2, 3]), false) + deepStrictEqual(Arr.isEmptyArray([]), true) + }) + + it("isNonEmptyReadonlyArray", () => { + deepStrictEqual(Arr.isNonEmptyReadonlyArray([1, 2, 3]), true) + deepStrictEqual(Arr.isNonEmptyReadonlyArray([]), false) + }) + + it("isNonEmptyArray", () => { + deepStrictEqual(Arr.isNonEmptyArray([1, 2, 3]), true) + deepStrictEqual(Arr.isNonEmptyArray([]), false) + }) + + it("head", () => { + const as: ReadonlyArray = [1, 2, 3] + assertSome(Arr.head(as), 1) + assertNone(Arr.head([])) + }) + + it("last", () => { + const as: ReadonlyArray = [1, 2, 3] + assertSome(Arr.last(as), 3) + assertNone(Arr.last([])) + }) + + it("chunksOf", () => { + deepStrictEqual(Arr.chunksOf(2)([1, 2, 3, 4, 5]), [ + Arr.make(1, 2), + [3, 4], + [5] + ]) + deepStrictEqual(Arr.chunksOf(2)([1, 2, 3, 4, 5, 6]), [ + Arr.make(1, 2), + [3, 4], + [5, 6] + ]) + deepStrictEqual(Arr.chunksOf(1)([1, 2, 3, 4, 5]), [[1], [2], [3], [4], [5]]) + deepStrictEqual(Arr.chunksOf(5)([1, 2, 3, 4, 5]), [[1, 2, 3, 4, 5]]) + // out of bounds + deepStrictEqual(Arr.chunksOf(0)([1, 2, 3, 4, 5]), [[1], [2], [3], [4], [5]]) + deepStrictEqual(Arr.chunksOf(-1)([1, 2, 3, 4, 5]), [[1], [2], [3], [4], [5]]) + + const assertSingleChunk = ( + input: Arr.NonEmptyReadonlyArray, + n: number + ) => { + const chunks = Arr.chunksOf(n)(input) + strictEqual(chunks.length, 1) + deepStrictEqual(Arr.headNonEmpty(chunks), input) + } + // n = length + assertSingleChunk(Arr.make(1, 2), 2) + // n out of bounds + assertSingleChunk(Arr.make(1, 2), 3) + }) + + it("window", () => { + deepStrictEqual(Arr.window(2)([]), []) + + deepStrictEqual(Arr.window(2)([1, 2, 3, 4, 5]), [[1, 2], [2, 3], [3, 4], [4, 5]]) + deepStrictEqual(Arr.window(3)([1, 2, 3, 4, 5]), [[1, 2, 3], [2, 3, 4], [3, 4, 5]]) + + // n out of bounds + deepStrictEqual(Arr.window([1, 2, 3, 4, 5], 6), []) + deepStrictEqual(Arr.window([1, 2, 3, 4, 5], 0), []) + deepStrictEqual(Arr.window([1, 2, 3, 4, 5], -1), []) + }) + + it("min", () => { + deepStrictEqual(Arr.min(Num.Order)([2, 1, 3]), 1) + deepStrictEqual(Arr.min(Num.Order)([3]), 3) + }) + + it("max", () => { + deepStrictEqual( + Arr.max(Num.Order)(Arr.make(1, 2, 3)), + 3 + ) + deepStrictEqual(Arr.max(Num.Order)([1]), 1) + }) + + it("flatten", () => { + deepStrictEqual(Arr.flatten([[1], [2], [3]]), [1, 2, 3]) + }) + + it("groupWith", () => { + const groupWith = Arr.groupWith(Num.Equivalence) + deepStrictEqual(groupWith([1, 2, 1, 1]), [[1], [2], [1, 1]]) + deepStrictEqual(groupWith([1, 2, 1, 1, 3]), [[1], [2], [1, 1], [3]]) + }) + + it("groupBy", () => { + deepStrictEqual(Arr.groupBy((_) => "")([]), {}) + deepStrictEqual(Arr.groupBy((a) => `${a}`)([1]), { "1": [1] }) + deepStrictEqual( + Arr.groupBy((s: string) => `${s.length}`)(["foo", "bar", "foobar"]), + { + "3": ["foo", "bar"], + "6": ["foobar"] + } + ) + deepStrictEqual(Arr.groupBy(["a", "b"], (s) => s === "a" ? symA : s === "b" ? symB : symC), { + [symA]: ["a"], + [symB]: ["b"] + }) + deepStrictEqual(Arr.groupBy(["a", "b", "c", "d"], (s) => s === "a" ? symA : s === "b" ? symB : symC), { + [symA]: ["a"], + [symB]: ["b"], + [symC]: ["c", "d"] + }) + }) + + it("match", () => { + const len: (as: ReadonlyArray) => number = Arr.match({ + onEmpty: () => 0, + onNonEmpty: (as) => 1 + len(as.slice(1)) + }) + deepStrictEqual(len([1, 2, 3]), 3) + }) + + it("matchLeft", () => { + const len: (as: ReadonlyArray) => number = Arr.matchLeft({ + onEmpty: () => 0, + onNonEmpty: (_, tail) => 1 + len(tail) + }) + deepStrictEqual(len([1, 2, 3]), 3) + }) + + it("matchRight", () => { + const len: (as: ReadonlyArray) => number = Arr.matchRight({ + onEmpty: () => 0, + onNonEmpty: (init, _) => 1 + len(init) + }) + deepStrictEqual(len([1, 2, 3]), 3) + }) + + it("sortBy", () => { + interface X { + readonly a: string + readonly b: number + readonly c: boolean + } + + const byName = pipe( + Str.Order, + Order.mapInput((p: { readonly a: string; readonly b: number }) => p.a) + ) + + const byAge = pipe( + Num.Order, + Order.mapInput((p: { readonly a: string; readonly b: number }) => p.b) + ) + + const sortByNameByAge = Arr.sortBy(byName, byAge) + + const xs: Arr.NonEmptyArray = [ + { a: "a", b: 1, c: true }, + { a: "b", b: 3, c: true }, + { a: "c", b: 2, c: true }, + { a: "b", b: 2, c: true } + ] + + deepStrictEqual(Arr.sortBy()(xs), xs) + deepStrictEqual(sortByNameByAge([]), []) + deepStrictEqual(sortByNameByAge(xs), [ + { a: "a", b: 1, c: true }, + { a: "b", b: 2, c: true }, + { a: "b", b: 3, c: true }, + { a: "c", b: 2, c: true } + ]) + + deepStrictEqual(Arr.sortBy()(new Set(xs)), xs) + deepStrictEqual(sortByNameByAge(new Set([])), []) + deepStrictEqual(sortByNameByAge(new Set(xs)), [ + { a: "a", b: 1, c: true }, + { a: "b", b: 2, c: true }, + { a: "b", b: 3, c: true }, + { a: "c", b: 2, c: true } + ]) + + const sortByAgeByName = Arr.sortBy(byAge, byName) + deepStrictEqual(sortByAgeByName(xs), [ + { a: "a", b: 1, c: true }, + { a: "b", b: 2, c: true }, + { a: "c", b: 2, c: true }, + { a: "b", b: 3, c: true } + ]) + }) + + it("copy", () => { + deepStrictEqual(pipe([], Arr.copy), []) + deepStrictEqual(pipe([1, 2, 3], Arr.copy), [1, 2, 3]) + }) + + it("chop", () => { + deepStrictEqual(pipe([], Arr.chop((as) => [as[0] * 2, as.slice(1)])), []) + deepStrictEqual(pipe([1, 2, 3], Arr.chop((as) => [as[0] * 2, as.slice(1)])), [2, 4, 6]) + }) + + it("pad", () => { + deepStrictEqual(pipe([], Arr.pad(0, 0)), []) + deepStrictEqual(pipe([1, 2, 3], Arr.pad(0, 0)), []) + deepStrictEqual(pipe([1, 2, 3], Arr.pad(2, 0)), [1, 2]) + deepStrictEqual(pipe([1, 2, 3], Arr.pad(6, 0)), [1, 2, 3, 0, 0, 0]) + deepStrictEqual(pipe([1, 2, 3], Arr.pad(-2, 0)), []) + }) + + describe("chunksOf", () => { + it("should split a `ReadonlyArray` into length-n pieces", () => { + deepStrictEqual(Arr.chunksOf(2)([1, 2, 3, 4, 5]), [[1, 2], [3, 4], [5]]) + deepStrictEqual(Arr.chunksOf(2)([1, 2, 3, 4, 5, 6]), [ + [1, 2], + [3, 4], + [5, 6] + ]) + deepStrictEqual(Arr.chunksOf(1)([1, 2, 3, 4, 5]), [[1], [2], [3], [4], [5]]) + deepStrictEqual(Arr.chunksOf(5)([1, 2, 3, 4, 5]), [[1, 2, 3, 4, 5]]) + // out of bounds + deepStrictEqual(Arr.chunksOf(0)([1, 2, 3, 4, 5]), [[1], [2], [3], [4], [5]]) + deepStrictEqual(Arr.chunksOf(-1)([1, 2, 3, 4, 5]), [[1], [2], [3], [4], [5]]) + + const assertSingleChunk = (input: ReadonlyArray, n: number) => { + const chunks = Arr.chunksOf(n)(input) + deepStrictEqual(chunks.length, 1) + deepStrictEqual(chunks[0], input) + } + // n = length + assertSingleChunk([1, 2], 2) + // n out of bounds + assertSingleChunk([1, 2], 3) + }) + + it("returns an empty array if provided an empty array", () => { + const empty: ReadonlyArray = [] + deepStrictEqual(Arr.chunksOf(0)(empty), Arr.empty()) + deepStrictEqual(Arr.chunksOf(0)(Arr.empty()), Arr.empty()) + deepStrictEqual(Arr.chunksOf(1)(empty), Arr.empty()) + deepStrictEqual(Arr.chunksOf(1)(Arr.empty()), Arr.empty()) + deepStrictEqual(Arr.chunksOf(2)(empty), Arr.empty()) + deepStrictEqual(Arr.chunksOf(2)(Arr.empty()), Arr.empty()) + }) + + it("should respect the law: chunksOf(n)(xs).concat(chunksOf(n)(ys)) == chunksOf(n)(xs.concat(ys)))", () => { + const xs: ReadonlyArray = [] + const ys: ReadonlyArray = [1, 2] + deepStrictEqual( + Arr.chunksOf(2)(xs).concat(Arr.chunksOf(2)(ys)), + Arr.chunksOf(2)(xs.concat(ys)) + ) + fc.assert( + fc.property( + fc.array(fc.integer()).filter((xs) => xs.length % 2 === 0), // Ensures `xs.length` is even + fc.array(fc.integer()), + fc.integer({ min: 1, max: 1 }).map((x) => x * 2), // Generates `n` to be even so that it evenly divides `xs` + (xs, ys, n) => { + const as = Arr.chunksOf(n)(xs).concat(Arr.chunksOf(n)(ys)) + const bs = Arr.chunksOf(n)(xs.concat(ys)) + deepStrictEqual(as, bs) + } + ) + ) + }) + }) + + it("makeBy", () => { + deepStrictEqual(Arr.makeBy(5, (n) => n * 2), [0, 2, 4, 6, 8]) + deepStrictEqual(Arr.makeBy((n) => n * 2)(5), [0, 2, 4, 6, 8]) + deepStrictEqual(Arr.makeBy(2.2, (n) => n * 2), [0, 2]) + deepStrictEqual(Arr.makeBy((n) => n * 2)(2.2), [0, 2]) + }) + + it("replicate", () => { + deepStrictEqual(Arr.replicate("a", 0), ["a"]) + deepStrictEqual(Arr.replicate("a", -1), ["a"]) + deepStrictEqual(Arr.replicate("a", 3), ["a", "a", "a"]) + deepStrictEqual(Arr.replicate("a", 2.2), ["a", "a"]) + }) + + it("range", () => { + deepStrictEqual(Arr.range(0, 0), [0]) + deepStrictEqual(Arr.range(0, 1), [0, 1]) + deepStrictEqual(Arr.range(1, 5), [1, 2, 3, 4, 5]) + deepStrictEqual(Arr.range(10, 15), [10, 11, 12, 13, 14, 15]) + deepStrictEqual(Arr.range(-1, 0), [-1, 0]) + deepStrictEqual(Arr.range(-5, -1), [-5, -4, -3, -2, -1]) + // out of bound + deepStrictEqual(Arr.range(2, 1), [2]) + deepStrictEqual(Arr.range(-1, -2), [-1]) + }) + + it("unionWith", () => { + const two: ReadonlyArray = [1, 2] + deepStrictEqual(pipe(two, Arr.unionWith([3, 4], Num.Equivalence)), [1, 2, 3, 4]) + deepStrictEqual(pipe(two, Arr.unionWith([2, 3], Num.Equivalence)), [1, 2, 3]) + deepStrictEqual(pipe(two, Arr.unionWith([1, 2], Num.Equivalence)), [1, 2]) + deepStrictEqual(pipe(two, Arr.unionWith(Arr.empty(), Num.Equivalence)), two) + deepStrictEqual(pipe(Arr.empty(), Arr.unionWith(two, Num.Equivalence)), two) + deepStrictEqual( + pipe(Arr.empty(), Arr.unionWith(Arr.empty(), Num.Equivalence)), + Arr.empty() + ) + }) + + it("intersectionWith", () => { + const intersectionWith = Arr.intersectionWith(Num.Equivalence) + deepStrictEqual(pipe([1, 2], intersectionWith([3, 4])), []) + deepStrictEqual(pipe([1, 2], intersectionWith([2, 3])), [2]) + deepStrictEqual(pipe([1, 2], intersectionWith([1, 2])), [1, 2]) + deepStrictEqual(pipe([1, 2], intersectionWith([3, 4][Symbol.iterator]())), []) + deepStrictEqual(pipe([1, 2], intersectionWith([2, 3][Symbol.iterator]())), [2]) + deepStrictEqual(pipe([1, 2], intersectionWith([1, 2][Symbol.iterator]())), [1, 2]) + }) + + it("differenceWith", () => { + const differenceWith = Arr.differenceWith(Num.Equivalence) + deepStrictEqual(pipe([1, 2], differenceWith([3, 4])), [1, 2]) + deepStrictEqual(pipe([1, 2], differenceWith([2, 3])), [1]) + deepStrictEqual(pipe([1, 2], differenceWith([1, 2])), []) + deepStrictEqual(pipe([1, 2], differenceWith([3, 4][Symbol.iterator]())), [1, 2]) + deepStrictEqual(pipe([1, 2], differenceWith([2, 3][Symbol.iterator]())), [1]) + deepStrictEqual(pipe([1, 2], differenceWith([1, 2][Symbol.iterator]())), []) + }) + + it("empty", () => { + deepStrictEqual(Arr.empty.length, 0) + }) + + it("every", () => { + const isPositive: Predicate.Predicate = (n) => n > 0 + deepStrictEqual(Arr.every([1, 2, 3], isPositive), true) + deepStrictEqual(Arr.every([1, 2, -3], isPositive), false) + }) + + it("some", () => { + const isPositive: Predicate.Predicate = (n) => n > 0 + deepStrictEqual(Arr.some([-1, -2, 3], isPositive), true) + deepStrictEqual(Arr.some([-1, -2, -3], isPositive), false) + }) + + it("length", () => { + deepStrictEqual(Arr.length(Arr.empty()), 0) + deepStrictEqual(Arr.length([]), 0) + deepStrictEqual(Arr.length(["a"]), 1) + }) + + it("fromOption", () => { + deepStrictEqual(Arr.fromOption(Option.some("hello")), ["hello"]) + deepStrictEqual(Arr.fromOption(Option.none()), []) + }) + + it("forEach", () => { + const log: Array = [] + Arr.forEach(["a", "b", "c"], (a, i) => log.push(`${a}-${i}`)) + deepStrictEqual(log, ["a-0", "b-1", "c-2"]) + }) + + it("sortWith", () => { + type X = { + a: string + b: number + } + const arr: ReadonlyArray = [{ a: "a", b: 2 }, { a: "b", b: 1 }] + deepStrictEqual(Arr.sortWith(arr, (x) => x.b, Order.number), [{ a: "b", b: 1 }, { a: "a", b: 2 }]) + }) + + it("countBy", () => { + deepStrictEqual(Arr.countBy([1, 2, 3, 4, 5], (n) => n % 2 === 0), 2) + deepStrictEqual(pipe([1, 2, 3, 4, 5], Arr.countBy((n) => n % 2 === 0)), 2) + }) + + it("Do notation", () => { + const _do = Arr.Do + deepStrictEqual(_do, Arr.of({})) + + const doA = Arr.bind(_do, "a", () => ["a"]) + deepStrictEqual(doA, Arr.of({ a: "a" })) + + const doAB = Arr.bind(doA, "b", (x) => ["b", x.a + "b"]) + deepStrictEqual(doAB, [ + { a: "a", b: "b" }, + { a: "a", b: "ab" } + ]) + const doABC = Arr.let(doAB, "c", (x) => [x.a, x.b, x.a + x.b]) + deepStrictEqual(doABC, [ + { a: "a", b: "b", c: ["a", "b", "ab"] }, + { a: "a", b: "ab", c: ["a", "ab", "aab"] } + ]) + + const doABCD = Arr.bind(doABC, "d", () => Arr.empty()) + deepStrictEqual(doABCD, []) + + const doAB__proto__C = pipe( + Arr.let(doAB, "__proto__", (x) => [x.a, x.b, x.a + x.b]), + Arr.let("c", (x) => [x.a, x.b, x.a + x.b]) + ) + deepStrictEqual(doAB__proto__C, [ + { a: "a", b: "b", c: ["a", "b", "ab"], ["__proto__"]: ["a", "b", "ab"] }, + { a: "a", b: "ab", c: ["a", "ab", "aab"], ["__proto__"]: ["a", "ab", "aab"] } + ]) + }) +}) diff --git a/repos/effect/packages/effect/test/BigDecimal.test.ts b/repos/effect/packages/effect/test/BigDecimal.test.ts new file mode 100644 index 0000000..d72d94e --- /dev/null +++ b/repos/effect/packages/effect/test/BigDecimal.test.ts @@ -0,0 +1,511 @@ +import { describe, it } from "@effect/vitest" +import { + assertEquals, + assertFalse, + assertNone, + assertSome, + assertTrue, + deepStrictEqual, + strictEqual, + throws +} from "@effect/vitest/utils" +import { BigDecimal, Equal, FastCheck as fc, Option } from "effect" + +const $ = BigDecimal.unsafeFromString + +const assertDivide = (x: string, y: string, z: string) => { + assertEquals(BigDecimal.divide($(x), $(y)).pipe(Option.getOrThrow), $(z), `Expected ${x} / ${y} to be ${z}`) + assertEquals(BigDecimal.unsafeDivide($(x), $(y)), $(z), `Expected ${x} / ${y} to be ${z}`) +} + +describe("BigDecimal", () => { + it("isBigDecimal", () => { + assertTrue(BigDecimal.isBigDecimal($("0"))) + assertTrue(BigDecimal.isBigDecimal($("987"))) + assertTrue(BigDecimal.isBigDecimal($("123.0"))) + assertTrue(BigDecimal.isBigDecimal($("0.123"))) + assertTrue(BigDecimal.isBigDecimal($("123.456"))) + assertFalse(BigDecimal.isBigDecimal("1")) + assertFalse(BigDecimal.isBigDecimal(true)) + }) + + it("sign", () => { + strictEqual(BigDecimal.sign($("-5")), -1) + strictEqual(BigDecimal.sign($("0")), 0) + strictEqual(BigDecimal.sign($("5")), 1) + strictEqual(BigDecimal.sign($("-123.456")), -1) + strictEqual(BigDecimal.sign($("456.789")), 1) + }) + + it("equals", () => { + assertTrue(BigDecimal.equals($("1"), $("1"))) + assertTrue(BigDecimal.equals($("0.00012300"), $("0.000123"))) + assertTrue(BigDecimal.equals($("5"), $("5.0"))) + assertTrue(BigDecimal.equals($("123.0000"), $("123.00"))) + assertFalse(BigDecimal.equals($("1"), $("2"))) + assertFalse(BigDecimal.equals($("1"), $("1.1"))) + assertFalse(BigDecimal.equals($("1"), $("0.1"))) + }) + + it("sum", () => { + assertEquals(BigDecimal.sum($("2"), $("0")), $("2")) + assertEquals(BigDecimal.sum($("0"), $("2")), $("2")) + assertEquals(BigDecimal.sum($("0"), $("0")), $("0")) + assertEquals(BigDecimal.sum($("2"), $("1")), $("3")) + assertEquals(BigDecimal.sum($("3.00000"), $("50")), $("53")) + assertEquals(BigDecimal.sum($("1.23"), $("0.0045678")), $("1.2345678")) + assertEquals(BigDecimal.sum($("123.456"), $("-123.456")), $("0")) + }) + + it("multiply", () => { + assertEquals(BigDecimal.multiply($("3"), $("2")), $("6")) + assertEquals(BigDecimal.multiply($("3"), $("0")), $("0")) + assertEquals(BigDecimal.multiply($("3"), $("-1")), $("-3")) + assertEquals(BigDecimal.multiply($("3"), $("0.5")), $("1.5")) + assertEquals(BigDecimal.multiply($("3"), $("-2.5")), $("-7.5")) + }) + + it("subtract", () => { + assertEquals(BigDecimal.subtract($("0"), $("1")), $("-1")) + assertEquals(BigDecimal.subtract($("2.1"), $("1")), $("1.1")) + assertEquals(BigDecimal.subtract($("3"), $("1")), $("2")) + assertEquals(BigDecimal.subtract($("3"), $("0")), $("3")) + assertEquals(BigDecimal.subtract($("3"), $("-1")), $("4")) + assertEquals(BigDecimal.subtract($("3"), $("0.5")), $("2.5")) + assertEquals(BigDecimal.subtract($("3"), $("-2.5")), $("5.5")) + }) + + it("roundTerminal", () => { + strictEqual(BigDecimal.roundTerminal(0n), 0n) + strictEqual(BigDecimal.roundTerminal(4n), 0n) + strictEqual(BigDecimal.roundTerminal(5n), 1n) + strictEqual(BigDecimal.roundTerminal(9n), 1n) + strictEqual(BigDecimal.roundTerminal(49n), 0n) + strictEqual(BigDecimal.roundTerminal(59n), 1n) + strictEqual(BigDecimal.roundTerminal(99n), 1n) + strictEqual(BigDecimal.roundTerminal(-4n), 0n) + strictEqual(BigDecimal.roundTerminal(-5n), 1n) + strictEqual(BigDecimal.roundTerminal(-9n), 1n) + strictEqual(BigDecimal.roundTerminal(-49n), 0n) + strictEqual(BigDecimal.roundTerminal(-59n), 1n) + strictEqual(BigDecimal.roundTerminal(-99n), 1n) + }) + + it("divide", () => { + assertDivide("0", "1", "0") + assertDivide("0", "10", "0") + assertDivide("2", "1", "2") + assertDivide("20", "1", "20") + assertDivide("10", "10", "1") + assertDivide("100", "10.0", "10") + assertDivide("20.0", "200", "0.1") + assertDivide("4", "2", "2.0") + assertDivide("15", "3", "5.0") + assertDivide("1", "2", "0.5") + assertDivide("1", "0.02", "50") + assertDivide("1", "0.2", "5") + assertDivide("1.0", "0.02", "50") + assertDivide("1", "0.020", "50") + assertDivide("5.0", "4.00", "1.25") + assertDivide("5.0", "4.000", "1.25") + assertDivide("5", "4.000", "1.25") + assertDivide("5", "4", "1.25") + assertDivide("100", "5", "20") + assertDivide("-50", "5", "-10") + assertDivide("200", "-5", "-40.0") + assertDivide( + "1", + "3", + "0.3333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333" + ) + assertDivide( + "-2", + "-3", + "0.6666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666667" + ) + assertDivide( + "-12.34", + "1.233", + "-10.00811030008110300081103000811030008110300081103000811030008110300081103000811030008110300081103001" + ) + assertDivide( + "125348", + "352.2283", + "355.8714617763535752237966114591019517738921035021887792661748076460636467881768727839301952739175132" + ) + + assertNone(BigDecimal.divide($("5"), $("0"))) + throws(() => BigDecimal.unsafeDivide($("5"), $("0")), new RangeError("Division by zero")) + }) + + it("Equivalence", () => { + assertTrue(BigDecimal.Equivalence($("1"), $("1"))) + assertTrue(BigDecimal.Equivalence($("0.00012300"), $("0.000123"))) + assertTrue(BigDecimal.Equivalence($("5"), $("5.00"))) + assertFalse(BigDecimal.Equivalence($("1"), $("2"))) + assertFalse(BigDecimal.Equivalence($("1"), $("1.1"))) + }) + + it("Order", () => { + strictEqual(BigDecimal.Order($("1"), $("2")), -1) + strictEqual(BigDecimal.Order($("2"), $("1")), 1) + strictEqual(BigDecimal.Order($("2"), $("2")), 0) + strictEqual(BigDecimal.Order($("1"), $("1.1")), -1) + strictEqual(BigDecimal.Order($("1.1"), $("1")), 1) + strictEqual(BigDecimal.Order($("0.00012300"), $("0.000123")), 0) + strictEqual(BigDecimal.Order($("5"), $("5.000")), 0) + strictEqual(BigDecimal.Order($("5"), $("0.500")), 1) + strictEqual(BigDecimal.Order($("5"), $("50.00")), -1) + }) + + it("lessThan", () => { + assertTrue(BigDecimal.lessThan($("2"), $("3"))) + assertFalse(BigDecimal.lessThan($("3"), $("3"))) + assertFalse(BigDecimal.lessThan($("4"), $("3"))) + }) + + it("lessThanOrEqualTo", () => { + assertTrue(BigDecimal.lessThanOrEqualTo($("2"), $("3"))) + assertTrue(BigDecimal.lessThanOrEqualTo($("3"), $("3"))) + assertFalse(BigDecimal.lessThanOrEqualTo($("4"), $("3"))) + }) + + it("greaterThan", () => { + assertFalse(BigDecimal.greaterThan($("2"), $("3"))) + assertFalse(BigDecimal.greaterThan($("3"), $("3"))) + assertTrue(BigDecimal.greaterThan($("4"), $("3"))) + }) + + it("greaterThanOrEqualTo", () => { + assertFalse(BigDecimal.greaterThanOrEqualTo($("2"), $("3"))) + assertTrue(BigDecimal.greaterThanOrEqualTo($("3"), $("3"))) + assertTrue(BigDecimal.greaterThanOrEqualTo($("4"), $("3"))) + }) + + it("between", () => { + assertTrue(BigDecimal.between({ minimum: $("0"), maximum: $("5") })($("3"))) + assertFalse(BigDecimal.between({ minimum: $("0"), maximum: $("5") })($("-1"))) + assertFalse(BigDecimal.between({ minimum: $("0"), maximum: $("5") })($("6"))) + assertFalse(BigDecimal.between({ minimum: $("0.02"), maximum: $("5") })($("0.0123"))) + assertTrue(BigDecimal.between({ minimum: $("0.02"), maximum: $("5") })($("0.05"))) + + assertTrue(BigDecimal.between($("3"), { minimum: $("0"), maximum: $("5") })) + }) + + it("clamp", () => { + assertEquals(BigDecimal.clamp({ minimum: $("0"), maximum: $("5") })($("3")), $("3")) + assertEquals(BigDecimal.clamp({ minimum: $("0"), maximum: $("5") })($("-1")), $("0")) + assertEquals(BigDecimal.clamp({ minimum: $("0"), maximum: $("5") })($("6")), $("5")) + assertEquals(BigDecimal.clamp({ minimum: $("0.02"), maximum: $("5") })($("0.0123")), $("0.02")) + + assertEquals(BigDecimal.clamp($("3"), { minimum: $("0"), maximum: $("5") }), $("3")) + }) + + it("min", () => { + assertEquals(BigDecimal.min($("2"), $("3")), $("2")) + assertEquals(BigDecimal.min($("5"), $("0.1")), $("0.1")) + assertEquals(BigDecimal.min($("0.005"), $("3")), $("0.005")) + assertEquals(BigDecimal.min($("123.456"), $("1.2")), $("1.2")) + }) + + it("max", () => { + assertEquals(BigDecimal.max($("2"), $("3")), $("3")) + assertEquals(BigDecimal.max($("5"), $("0.1")), $("5")) + assertEquals(BigDecimal.max($("0.005"), $("3")), $("3")) + assertEquals(BigDecimal.max($("123.456"), $("1.2")), $("123.456")) + }) + + it("abs", () => { + assertEquals(BigDecimal.abs($("2")), $("2")) + assertEquals(BigDecimal.abs($("-3")), $("3")) + assertEquals(BigDecimal.abs($("0.000456")), $("0.000456")) + assertEquals(BigDecimal.abs($("-0.123")), $("0.123")) + }) + + it("negate", () => { + assertEquals(BigDecimal.negate($("2")), $("-2")) + assertEquals(BigDecimal.negate($("-3")), $("3")) + assertEquals(BigDecimal.negate($("0.000456")), $("-0.000456")) + assertEquals(BigDecimal.negate($("-0.123")), $("0.123")) + }) + + it("remainder", () => { + assertEquals(BigDecimal.remainder($("5"), $("2")).pipe(Option.getOrThrow), $("1")) + assertEquals(BigDecimal.remainder($("4"), $("2")).pipe(Option.getOrThrow), $("0")) + assertEquals(BigDecimal.remainder($("123.456"), $("0.2")).pipe(Option.getOrThrow), $("0.056")) + assertNone(BigDecimal.remainder($("5"), $("0"))) + }) + + it("unsafeRemainder", () => { + assertEquals(BigDecimal.unsafeRemainder($("5"), $("2")), $("1")) + assertEquals(BigDecimal.unsafeRemainder($("4"), $("2")), $("0")) + assertEquals(BigDecimal.unsafeRemainder($("123.456"), $("0.2")), $("0.056")) + throws(() => BigDecimal.unsafeRemainder($("5"), $("0")), new RangeError("Division by zero")) + }) + + it("normalize", () => { + deepStrictEqual(BigDecimal.normalize($("0")), BigDecimal.unsafeMakeNormalized(0n, 0)) + deepStrictEqual(BigDecimal.normalize($("0.123000")), BigDecimal.unsafeMakeNormalized(123n, 3)) + deepStrictEqual(BigDecimal.normalize($("123.000")), BigDecimal.unsafeMakeNormalized(123n, 0)) + deepStrictEqual(BigDecimal.normalize($("-0.000123000")), BigDecimal.unsafeMakeNormalized(-123n, 6)) + deepStrictEqual(BigDecimal.normalize($("-123.000")), BigDecimal.unsafeMakeNormalized(-123n, 0)) + deepStrictEqual(BigDecimal.normalize($("12300000")), BigDecimal.unsafeMakeNormalized(123n, -5)) + }) + + it("fromString", () => { + assertSome(BigDecimal.fromString("2"), BigDecimal.make(2n, 0)) + assertSome(BigDecimal.fromString("-2"), BigDecimal.make(-2n, 0)) + assertSome(BigDecimal.fromString("0.123"), BigDecimal.make(123n, 3)) + assertSome(BigDecimal.fromString("200"), BigDecimal.make(200n, 0)) + assertSome(BigDecimal.fromString("20000000"), BigDecimal.make(20000000n, 0)) + assertSome(BigDecimal.fromString("-20000000"), BigDecimal.make(-20000000n, 0)) + assertSome(BigDecimal.fromString("2.00"), BigDecimal.make(200n, 2)) + assertSome(BigDecimal.fromString("0.0000200"), BigDecimal.make(200n, 7)) + assertSome(BigDecimal.fromString(""), BigDecimal.normalize(BigDecimal.make(0n, 0))) + assertSome(BigDecimal.fromString("1e5"), BigDecimal.make(1n, -5)) + assertSome(BigDecimal.fromString("1E15"), BigDecimal.make(1n, -15)) + assertSome(BigDecimal.fromString("1e+5"), BigDecimal.make(1n, -5)) + assertSome(BigDecimal.fromString("1E+15"), BigDecimal.make(1n, -15)) + assertSome(BigDecimal.fromString("-1.5E3"), BigDecimal.make(-15n, -2)) + assertSome(BigDecimal.fromString("-1.5e3"), BigDecimal.make(-15n, -2)) + assertSome(BigDecimal.fromString("-.5e3"), BigDecimal.make(-5n, -2)) + assertSome(BigDecimal.fromString("-5e3"), BigDecimal.make(-5n, -3)) + assertSome(BigDecimal.fromString("-5e-3"), BigDecimal.make(-5n, 3)) + assertSome(BigDecimal.fromString("15e-3"), BigDecimal.make(15n, 3)) + assertSome(BigDecimal.fromString("0.00002e5"), BigDecimal.make(2n, 0)) + assertSome(BigDecimal.fromString("0.00002e-5"), BigDecimal.make(2n, 10)) + assertNone(BigDecimal.fromString("0.0000e2e1")) + assertNone(BigDecimal.fromString("0.1.2")) + }) + + it("format", () => { + strictEqual(BigDecimal.format($("2")), "2") + strictEqual(BigDecimal.format($("-2")), "-2") + strictEqual(BigDecimal.format($("0.123")), "0.123") + strictEqual(BigDecimal.format($("200")), "200") + strictEqual(BigDecimal.format($("20000000")), "20000000") + strictEqual(BigDecimal.format($("-20000000")), "-20000000") + strictEqual(BigDecimal.format($("2.00")), "2") + strictEqual(BigDecimal.format($("0.200")), "0.2") + strictEqual(BigDecimal.format($("0.123000")), "0.123") + strictEqual(BigDecimal.format($("-456.123")), "-456.123") + strictEqual(BigDecimal.format(BigDecimal.make(10n, -1)), "100") + strictEqual(BigDecimal.format(BigDecimal.make(1n, -25)), "1e+25") + strictEqual(BigDecimal.format(BigDecimal.make(12345n, -25)), "1.2345e+29") + strictEqual(BigDecimal.format(BigDecimal.make(12345n, 25)), "1.2345e-21") + strictEqual(BigDecimal.format(BigDecimal.make(-12345n, 20)), "-1.2345e-16") + }) + + it("toJSON()", () => { + deepStrictEqual(JSON.stringify($("2")), JSON.stringify({ _id: "BigDecimal", value: "2", scale: 0 })) + }) + + it("inspect", () => { + if (typeof window === "undefined") { + // eslint-disable-next-line @typescript-eslint/no-require-imports + const { inspect } = require("node:util") + deepStrictEqual(inspect($("2")), inspect({ _id: "BigDecimal", value: "2", scale: 0 })) + } + }) + + it("toString()", () => { + strictEqual(String($("2")), "BigDecimal(2)") + }) + + it("Equal.symbol", () => { + assertTrue(Equal.equals($("2"), $("2"))) + }) + + it("pipe()", () => { + deepStrictEqual($("2").pipe(BigDecimal.multiply($("3"))), $("6")) + }) + + it("scale", () => { + deepStrictEqual(BigDecimal.scale($("3.0005"), 3), $("3.000")) + }) + + it("fromBigInt", () => { + deepStrictEqual(BigDecimal.fromBigInt(1n), BigDecimal.make(1n, 0)) + }) + + it("fromNumber", () => { + deepStrictEqual(BigDecimal.fromNumber(123), BigDecimal.make(123n, 0)) + deepStrictEqual(BigDecimal.fromNumber(123.456), BigDecimal.make(123456n, 3)) + }) + + it("unsafeToNumber", () => { + strictEqual(BigDecimal.unsafeToNumber($("123.456")), 123.456) + }) + + it("isInteger", () => { + assertTrue(BigDecimal.isInteger($("0"))) + assertTrue(BigDecimal.isInteger($("1"))) + assertFalse(BigDecimal.isInteger($("1.1"))) + }) + + it("isZero", () => { + assertTrue(BigDecimal.isZero($("0"))) + assertFalse(BigDecimal.isZero($("1"))) + }) + + it("isNegative", () => { + assertTrue(BigDecimal.isNegative($("-1"))) + assertFalse(BigDecimal.isNegative($("0"))) + assertFalse(BigDecimal.isNegative($("1"))) + }) + + it("isPositive", () => { + assertFalse(BigDecimal.isPositive($("-1"))) + assertFalse(BigDecimal.isPositive($("0"))) + assertTrue(BigDecimal.isPositive($("1"))) + }) + + it("digitAt", () => { + assertEquals(BigDecimal.digitAt($("12.34"), -2), 0n) + assertEquals(BigDecimal.digitAt($("12.34"), -1), 1n) + assertEquals(BigDecimal.digitAt($("12.34"), 0), 2n) + assertEquals(BigDecimal.digitAt($("12.34"), 1), 3n) + assertEquals(BigDecimal.digitAt($("12.34"), 2), 4n) + assertEquals(BigDecimal.digitAt($("12.34"), 3), 0n) + }) + + it("round: ceil", () => { + assertEquals(BigDecimal.ceil($("145"), -1), $("150")) + assertEquals(BigDecimal.ceil(-1)($("145")), $("150")) + assertEquals(BigDecimal.ceil($("-14.5")), $("-14")) + + assertEquals(BigDecimal.round($("321.123"), { mode: "ceil", scale: -3 }), $("1000")) + assertEquals(BigDecimal.round($("145"), { mode: "ceil", scale: -1 }), $("150")) + assertEquals(BigDecimal.round($("-2.0"), { mode: "ceil", scale: 0 }), $("-2")) + assertEquals(BigDecimal.round($("-1.9"), { mode: "ceil", scale: 0 }), $("-1")) + assertEquals(BigDecimal.round($("0.12345678987654321"), { mode: "ceil", scale: 13 }), $("0.1234567898766")) + assertEquals(BigDecimal.round($("-0.12345678987654321"), { mode: "ceil", scale: 13 }), $("-0.1234567898765")) + }) + + it("round: floor)", () => { + assertEquals(BigDecimal.floor($("145"), -1), $("140")) + assertEquals(BigDecimal.floor(-1)($("145")), $("140")) + assertEquals(BigDecimal.floor($("-14.5")), $("-15")) + + assertEquals(BigDecimal.round($("321.123"), { mode: "floor", scale: -3 }), $("0")) + assertEquals(BigDecimal.round($("145"), { mode: "floor", scale: -1 }), $("140")) + assertEquals(BigDecimal.round($("-2.1"), { mode: "floor", scale: 0 }), $("-3")) + assertEquals(BigDecimal.round($("-1.9"), { mode: "floor", scale: 0 }), $("-2")) + assertEquals(BigDecimal.round($("0.12345678987654321"), { mode: "floor", scale: 13 }), $("0.1234567898765")) + assertEquals(BigDecimal.round($("-0.12345678987654321"), { mode: "floor", scale: 13 }), $("-0.1234567898766")) + }) + + it("round: to-zero (truncate)", () => { + assertEquals(BigDecimal.truncate($("145"), -1), $("140")) + assertEquals(BigDecimal.truncate(-1)($("145")), $("140")) + assertEquals(BigDecimal.truncate($("-14.5")), $("-14")) + + assertEquals(BigDecimal.round($("321.123"), { mode: "to-zero", scale: -3 }), $("0")) + assertEquals(BigDecimal.round($("145"), { mode: "to-zero", scale: -1 }), $("140")) + assertEquals(BigDecimal.round($("-2.1"), { mode: "to-zero", scale: 0 }), $("-2")) + assertEquals(BigDecimal.round($("-1.9"), { mode: "to-zero", scale: 0 }), $("-1")) + assertEquals(BigDecimal.round($("0.12345678987654321"), { mode: "to-zero", scale: 13 }), $("0.1234567898765")) + assertEquals(BigDecimal.round($("-0.12345678987654321"), { mode: "to-zero", scale: 13 }), $("-0.1234567898765")) + }) + + it("round: from-zero", () => { + assertEquals(BigDecimal.round($("321.123"), { mode: "from-zero", scale: -3 }), $("1000")) + assertEquals(BigDecimal.round($("145"), { mode: "from-zero", scale: -1 }), $("150")) + assertEquals(BigDecimal.round($("-2.1"), { mode: "from-zero", scale: 0 }), $("-3")) + assertEquals(BigDecimal.round($("-1.9"), { mode: "from-zero", scale: 0 }), $("-2")) + assertEquals(BigDecimal.round($("0.12345678987654321"), { mode: "from-zero", scale: 13 }), $("0.1234567898766")) + assertEquals(BigDecimal.round($("-0.12345678987654321"), { mode: "from-zero", scale: 13 }), $("-0.1234567898766")) + }) + + it("round: half-ceil", () => { + assertEquals(BigDecimal.round($("321.123"), { mode: "half-ceil", scale: -3 }), $("0")) + assertEquals(BigDecimal.round($("145"), { mode: "half-ceil", scale: -1 }), $("150")) + assertEquals(BigDecimal.round($("-2.5"), { mode: "half-ceil", scale: 0 }), $("-2")) + assertEquals(BigDecimal.round($("1.95"), { mode: "half-ceil", scale: 1 }), $("2")) + assertEquals(BigDecimal.round($("-1.95"), { mode: "half-ceil", scale: 1 }), $("-1.9")) + assertEquals(BigDecimal.round($("0.1234567898765"), { mode: "half-ceil", scale: 12 }), $("0.123456789877")) + assertEquals(BigDecimal.round($("-0.1234567898765"), { mode: "half-ceil", scale: 12 }), $("-0.123456789876")) + }) + + it("round: half-floor", () => { + assertEquals(BigDecimal.round($("321.123"), { mode: "half-floor", scale: -3 }), $("0")) + assertEquals(BigDecimal.round($("145"), { mode: "half-floor", scale: -1 }), $("140")) + assertEquals(BigDecimal.round($("-2.4"), { mode: "half-floor", scale: 0 }), $("-2")) + assertEquals(BigDecimal.round($("-2.5"), { mode: "half-floor", scale: 0 }), $("-3")) + assertEquals(BigDecimal.round($("1.95"), { mode: "half-floor", scale: 1 }), $("1.9")) + assertEquals(BigDecimal.round($("-1.95"), { mode: "half-floor", scale: 1 }), $("-2")) + assertEquals(BigDecimal.round($("0.1234567898765"), { mode: "half-floor", scale: 12 }), $("0.123456789876")) + assertEquals(BigDecimal.round($("-0.1234567898765"), { mode: "half-floor", scale: 12 }), $("-0.123456789877")) + }) + + it("round: half-to-zero", () => { + assertEquals(BigDecimal.round($("321.123"), { mode: "half-to-zero", scale: -3 }), $("0")) + assertEquals(BigDecimal.round($("145"), { mode: "half-to-zero", scale: -1 }), $("140")) + assertEquals(BigDecimal.round($("-2.4"), { mode: "half-to-zero", scale: 0 }), $("-2")) + assertEquals(BigDecimal.round($("-2.5"), { mode: "half-to-zero", scale: 0 }), $("-2")) + assertEquals(BigDecimal.round($("1.95"), { mode: "half-to-zero", scale: 1 }), $("1.9")) + assertEquals(BigDecimal.round($("-1.95"), { mode: "half-to-zero", scale: 1 }), $("-1.9")) + assertEquals(BigDecimal.round($("0.1234567898765"), { mode: "half-to-zero", scale: 12 }), $("0.123456789876")) + assertEquals(BigDecimal.round($("-0.1234567898765"), { mode: "half-to-zero", scale: 12 }), $("-0.123456789876")) + }) + + it("round: half-from-zero", () => { + assertEquals(BigDecimal.round($("321.123"), { mode: "half-from-zero", scale: -3 }), $("0")) + assertEquals(BigDecimal.round($("145"), { mode: "half-from-zero", scale: -1 }), $("150")) + assertEquals(BigDecimal.round($("-2.4"), { mode: "half-from-zero", scale: 0 }), $("-2")) + assertEquals(BigDecimal.round($("-2.5"), { mode: "half-from-zero", scale: 0 }), $("-3")) + assertEquals(BigDecimal.round($("1.95"), { mode: "half-from-zero", scale: 1 }), $("2")) + assertEquals(BigDecimal.round($("-1.95"), { mode: "half-from-zero", scale: 1 }), $("-2")) + assertEquals(BigDecimal.round($("0.1234567898765"), { mode: "half-from-zero", scale: 12 }), $("0.123456789877")) + assertEquals(BigDecimal.round($("-0.1234567898765"), { mode: "half-from-zero", scale: 12 }), $("-0.123456789877")) + }) + + it("round: half-even", () => { + assertEquals(BigDecimal.round($("321.123"), { mode: "half-even", scale: -3 }), $("0")) + assertEquals(BigDecimal.round($("145"), { mode: "half-even", scale: -1 }), $("140")) + assertEquals(BigDecimal.round($("-2.4"), { mode: "half-even", scale: 0 }), $("-2")) + assertEquals(BigDecimal.round($("-2.5"), { mode: "half-even", scale: 0 }), $("-2")) + assertEquals(BigDecimal.round($("1.95"), { mode: "half-even", scale: 1 }), $("2")) + assertEquals(BigDecimal.round($("-1.95"), { mode: "half-even", scale: 1 }), $("-2")) + assertEquals(BigDecimal.round($("0.1234567898765"), { mode: "half-even", scale: 12 }), $("0.123456789876")) + assertEquals(BigDecimal.round($("-0.1234567898765"), { mode: "half-even", scale: 12 }), $("-0.123456789876")) + }) + + it("round: half-odd", () => { + assertEquals(BigDecimal.round($("321.123"), { mode: "half-even", scale: -3 }), $("0")) + assertEquals(BigDecimal.round($("145"), { mode: "half-even", scale: -1 }), $("140")) + assertEquals(BigDecimal.round($("-2.4"), { mode: "half-even", scale: 0 }), $("-2")) + assertEquals(BigDecimal.round($("-2.5"), { mode: "half-even", scale: 0 }), $("-2")) + assertEquals(BigDecimal.round($("1.95"), { mode: "half-even", scale: 1 }), $("2")) + assertEquals(BigDecimal.round($("-1.95"), { mode: "half-even", scale: 1 }), $("-2")) + assertEquals(BigDecimal.round($("0.1234567898765"), { mode: "half-even", scale: 12 }), $("0.123456789876")) + assertEquals(BigDecimal.round($("-0.1234567898765"), { mode: "half-even", scale: 12 }), $("-0.123456789876")) + }) + + it("sumAll", () => { + assertEquals(BigDecimal.sumAll([]), $("0")) + assertEquals(BigDecimal.sumAll([$("2.5"), $("0.5")]), $("3")) + assertEquals(BigDecimal.sumAll([$("2.5"), $("1500"), $("123.456")]), $("1625.956")) + }) +}) + +// This test is skipped because it is slow. It remains here as an opt-in test for +// debugging or active development of features in the `BigDecimal` module. +describe.skip("Property based testing", () => { + const zeroArb = fc.constant(BigDecimal.unsafeMakeNormalized(0n, 0)) + const bigDecimalArb = fc.tuple(fc.bigInt(), fc.integer()).map(([value, scale]) => BigDecimal.make(value, scale)) + const arbWithZero = fc.oneof({ arbitrary: zeroArb, weight: 1 }, { arbitrary: bigDecimalArb, weight: 3 }) + + it("unsafeFromString and format should be inverses", () => { + fc.assert(fc.property(arbWithZero, (bd) => { + return BigDecimal.equals(BigDecimal.unsafeFromString(BigDecimal.format(bd)), bd) + })) + }) + + it("toExponential should harmonize with Number.prototype.toExponential", () => { + const actualNumbers = fc.float().filter((n) => Number.isFinite(n)) + fc.assert(fc.property(actualNumbers, (n) => { + return n.toExponential() === BigDecimal.toExponential(BigDecimal.unsafeFromNumber(n)) + })) + }) +}) diff --git a/repos/effect/packages/effect/test/BigInt.test.ts b/repos/effect/packages/effect/test/BigInt.test.ts new file mode 100644 index 0000000..f4122e4 --- /dev/null +++ b/repos/effect/packages/effect/test/BigInt.test.ts @@ -0,0 +1,190 @@ +import { describe, it } from "@effect/vitest" +import { + assertFalse, + assertNone, + assertSome, + assertTrue, + deepStrictEqual, + strictEqual, + throws +} from "@effect/vitest/utils" +import { BigInt as BigInt_, pipe } from "effect" + +describe("BigInt", () => { + it("sign", () => { + strictEqual(BigInt_.sign(-5n), -1) + strictEqual(BigInt_.sign(0n), 0) + strictEqual(BigInt_.sign(5n), 1) + }) + + it("isBigInt", () => { + assertTrue(BigInt_.isBigInt(1n)) + assertFalse(BigInt_.isBigInt(1)) + assertFalse(BigInt_.isBigInt("a")) + assertFalse(BigInt_.isBigInt(true)) + }) + + it("sum", () => { + deepStrictEqual(pipe(1n, BigInt_.sum(2n)), 3n) + }) + + it("multiply", () => { + deepStrictEqual(pipe(2n, BigInt_.multiply(3n)), 6n) + }) + + it("subtract", () => { + deepStrictEqual(pipe(3n, BigInt_.subtract(1n)), 2n) + }) + + it("divide", () => { + assertSome(pipe(6n, BigInt_.divide(2n)), 3n) + assertNone(pipe(6n, BigInt_.divide(0n))) + }) + + it("unsafeDivide", () => { + deepStrictEqual(pipe(6n, BigInt_.unsafeDivide(2n)), 3n) + }) + + it("increment", () => { + deepStrictEqual(BigInt_.increment(2n), 3n) + }) + + it("decrement", () => { + deepStrictEqual(BigInt_.decrement(2n), 1n) + }) + + it("Equivalence", () => { + assertTrue(BigInt_.Equivalence(1n, 1n)) + assertFalse(BigInt_.Equivalence(1n, 2n)) + }) + + it("Order", () => { + deepStrictEqual(BigInt_.Order(1n, 2n), -1) + deepStrictEqual(BigInt_.Order(2n, 1n), 1) + deepStrictEqual(BigInt_.Order(2n, 2n), 0) + }) + + it("lessThan", () => { + deepStrictEqual(BigInt_.lessThan(2n, 3n), true) + deepStrictEqual(BigInt_.lessThan(3n, 3n), false) + deepStrictEqual(BigInt_.lessThan(4n, 3n), false) + }) + + it("lessThanOrEqualTo", () => { + deepStrictEqual(BigInt_.lessThanOrEqualTo(2n, 3n), true) + deepStrictEqual(BigInt_.lessThanOrEqualTo(3n, 3n), true) + deepStrictEqual(BigInt_.lessThanOrEqualTo(4n, 3n), false) + }) + + it("greaterThan", () => { + deepStrictEqual(BigInt_.greaterThan(2n, 3n), false) + deepStrictEqual(BigInt_.greaterThan(3n, 3n), false) + deepStrictEqual(BigInt_.greaterThan(4n, 3n), true) + }) + + it("greaterThanOrEqualTo", () => { + deepStrictEqual(BigInt_.greaterThanOrEqualTo(2n, 3n), false) + deepStrictEqual(BigInt_.greaterThanOrEqualTo(3n, 3n), true) + deepStrictEqual(BigInt_.greaterThanOrEqualTo(4n, 3n), true) + }) + + it("between", () => { + deepStrictEqual(BigInt_.between({ minimum: 0n, maximum: 5n })(3n), true) + deepStrictEqual(BigInt_.between({ minimum: 0n, maximum: 5n })(-1n), false) + deepStrictEqual(BigInt_.between({ minimum: 0n, maximum: 5n })(6n), false) + + deepStrictEqual(BigInt_.between(3n, { minimum: 0n, maximum: 5n }), true) + }) + + it("clamp", () => { + deepStrictEqual(BigInt_.clamp({ minimum: 0n, maximum: 5n })(3n), 3n) + deepStrictEqual(BigInt_.clamp({ minimum: 0n, maximum: 5n })(-1n), 0n) + deepStrictEqual(BigInt_.clamp({ minimum: 0n, maximum: 5n })(6n), 5n) + + deepStrictEqual(BigInt_.clamp(3n, { minimum: 0n, maximum: 5n }), 3n) + }) + + it("min", () => { + deepStrictEqual(BigInt_.min(2n, 3n), 2n) + }) + + it("max", () => { + deepStrictEqual(BigInt_.max(2n, 3n), 3n) + }) + + it("sumAll", () => { + deepStrictEqual(BigInt_.sumAll([2n, 3n, 4n]), 9n) + }) + + it("multiplyAll", () => { + deepStrictEqual(BigInt_.multiplyAll([2n, 0n, 4n]), 0n) + deepStrictEqual(BigInt_.multiplyAll([2n, 3n, 4n]), 24n) + }) + + it("abs", () => { + deepStrictEqual(BigInt_.abs(2n), 2n) + deepStrictEqual(BigInt_.abs(-3n), 3n) + }) + + it("gcd", () => { + deepStrictEqual(BigInt_.gcd(2n, 4n), 2n) + deepStrictEqual(BigInt_.gcd(3n, 4n), 1n) + }) + + it("lcm", () => { + deepStrictEqual(BigInt_.lcm(2n, 4n), 4n) + deepStrictEqual(BigInt_.lcm(3n, 4n), 12n) + }) + + it("sqrt", () => { + assertSome(BigInt_.sqrt(1n), 1n) + assertSome(BigInt_.sqrt(16n), 4n) + assertSome(BigInt_.sqrt(81n), 9n) + assertNone(BigInt_.sqrt(-123n)) + }) + + it("sqrt", () => { + throws(() => BigInt_.unsafeSqrt(-1n), new RangeError("Cannot take the square root of a negative number")) + }) + + it("toNumber", () => { + assertSome(BigInt_.toNumber(BigInt(Number.MAX_SAFE_INTEGER)), Number.MAX_SAFE_INTEGER) + assertNone(BigInt_.toNumber(BigInt(Number.MAX_SAFE_INTEGER) + BigInt(1))) + assertSome(BigInt_.toNumber(BigInt(Number.MIN_SAFE_INTEGER)), Number.MIN_SAFE_INTEGER) + assertNone(BigInt_.toNumber(BigInt(Number.MIN_SAFE_INTEGER) - BigInt(1))) + assertSome(BigInt_.toNumber(BigInt(0)), 0) + assertSome(BigInt_.toNumber(BigInt(42)), 42) + assertSome(BigInt_.toNumber(BigInt(-42)), -42) + }) + + it("fromString", () => { + assertNone(BigInt_.fromString("NaN")) + assertNone(BigInt_.fromString("Infinity")) + assertNone(BigInt_.fromString("-Infinity")) + assertNone(BigInt_.fromString("3.14")) + assertNone(BigInt_.fromString("-3.14")) + assertNone(BigInt_.fromString("1e3")) + assertNone(BigInt_.fromString("1e-3")) + assertNone(BigInt_.fromString("")) + assertNone(BigInt_.fromString("a")) + assertSome(BigInt_.fromString("42"), BigInt(42)) + assertSome(BigInt_.fromString("\n\r\t 42 \n\r\t"), BigInt(42)) + }) + + it("fromNumber", () => { + assertSome(BigInt_.fromNumber(Number.MAX_SAFE_INTEGER), BigInt(Number.MAX_SAFE_INTEGER)) + assertNone(BigInt_.fromNumber(Number.MAX_SAFE_INTEGER + 1)) + assertSome(BigInt_.fromNumber(Number.MIN_SAFE_INTEGER), BigInt(Number.MIN_SAFE_INTEGER)) + assertNone(BigInt_.fromNumber(Number.MIN_SAFE_INTEGER - 1)) + assertNone(BigInt_.fromNumber(Infinity)) + assertNone(BigInt_.fromNumber(-Infinity)) + assertNone(BigInt_.fromNumber(NaN)) + assertNone(BigInt_.fromNumber(1e100)) + assertNone(BigInt_.fromNumber(-1e100)) + assertNone(BigInt_.fromNumber(3.14)) + assertNone(BigInt_.fromNumber(-3.14)) + assertSome(BigInt_.fromNumber(0), BigInt(0)) + assertSome(BigInt_.fromNumber(42), BigInt(42)) + assertSome(BigInt_.fromNumber(-42), BigInt(-42)) + }) +}) diff --git a/repos/effect/packages/effect/test/Boolean.test.ts b/repos/effect/packages/effect/test/Boolean.test.ts new file mode 100644 index 0000000..5327e00 --- /dev/null +++ b/repos/effect/packages/effect/test/Boolean.test.ts @@ -0,0 +1,98 @@ +import { describe, it } from "@effect/vitest" +import { assertFalse, assertTrue, deepStrictEqual } from "@effect/vitest/utils" +import { Boolean, pipe } from "effect" + +describe("Boolean", () => { + it("isBoolean", () => { + assertTrue(Boolean.isBoolean(true)) + assertTrue(Boolean.isBoolean(false)) + assertFalse(Boolean.isBoolean("a")) + assertFalse(Boolean.isBoolean(1)) + }) + + it("and", () => { + assertTrue(pipe(true, Boolean.and(true))) + assertFalse(pipe(true, Boolean.and(false))) + assertFalse(pipe(false, Boolean.and(true))) + assertFalse(pipe(false, Boolean.and(false))) + }) + + it("nand", () => { + assertFalse(pipe(true, Boolean.nand(true))) + assertTrue(pipe(true, Boolean.nand(false))) + assertTrue(pipe(false, Boolean.nand(true))) + assertTrue(pipe(false, Boolean.nand(false))) + }) + + it("or", () => { + assertTrue(pipe(true, Boolean.or(true))) + assertTrue(pipe(true, Boolean.or(false))) + assertTrue(pipe(false, Boolean.or(true))) + assertFalse(pipe(false, Boolean.or(false))) + }) + + it("nor", () => { + assertFalse(pipe(true, Boolean.nor(true))) + assertFalse(pipe(true, Boolean.nor(false))) + assertFalse(pipe(false, Boolean.nor(true))) + assertTrue(pipe(false, Boolean.nor(false))) + }) + + it("xor", () => { + assertFalse(pipe(true, Boolean.xor(true))) + assertTrue(pipe(true, Boolean.xor(false))) + assertTrue(pipe(false, Boolean.xor(true))) + assertFalse(pipe(false, Boolean.xor(false))) + }) + + it("eqv", () => { + assertTrue(pipe(true, Boolean.eqv(true))) + assertFalse(pipe(true, Boolean.eqv(false))) + assertFalse(pipe(false, Boolean.eqv(true))) + assertTrue(pipe(false, Boolean.eqv(false))) + }) + + it("implies", () => { + assertTrue(pipe(true, Boolean.implies(true))) + assertFalse(pipe(true, Boolean.implies(false))) + assertTrue(pipe(false, Boolean.implies(true))) + assertTrue(pipe(false, Boolean.implies(false))) + }) + + it("not", () => { + assertFalse(pipe(true, Boolean.not)) + assertTrue(pipe(false, Boolean.not)) + }) + + it("match", () => { + const match = Boolean.match({ + onFalse: () => "false", + onTrue: () => "true" + }) + deepStrictEqual(match(true), "true") + deepStrictEqual(match(false), "false") + }) + + it("Equivalence", () => { + assertTrue(Boolean.Equivalence(true, true)) + assertTrue(Boolean.Equivalence(false, false)) + assertFalse(Boolean.Equivalence(true, false)) + assertFalse(Boolean.Equivalence(false, true)) + }) + + it("Order", () => { + deepStrictEqual(Boolean.Order(false, true), -1) + deepStrictEqual(Boolean.Order(true, false), 1) + deepStrictEqual(Boolean.Order(true, true), 0) + }) + + it("every", () => { + assertTrue(Boolean.every([true, true, true])) + assertFalse(Boolean.every([true, false, true])) + }) + + it("some", () => { + assertTrue(Boolean.some([true, false, true])) + assertFalse(Boolean.some([false, false, false])) + }) +}) diff --git a/repos/effect/packages/effect/test/Brand.test.ts b/repos/effect/packages/effect/test/Brand.test.ts new file mode 100644 index 0000000..9f82c36 --- /dev/null +++ b/repos/effect/packages/effect/test/Brand.test.ts @@ -0,0 +1,88 @@ +import { describe, it } from "@effect/vitest" +import { + assertFalse, + assertLeft, + assertNone, + assertRight, + assertSome, + assertTrue, + deepStrictEqual, + strictEqual, + throws +} from "@effect/vitest/utils" +import { Brand, Option } from "effect" + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +declare const IntTypeId: unique symbol +type Int = number & Brand.Brand +const Int = Brand.refined( + (n) => Number.isInteger(n), + (n) => Brand.error(`Expected ${n} to be an integer`) +) + +type Positive = number & Brand.Brand<"Positive"> +const Positive = Brand.refined( + (n) => n > 0, + (n) => Brand.error(`Expected ${n} to be positive`) +) + +type PositiveInt = Positive & Int +const PositiveInt = Brand.all(Int, Positive) + +describe("Brand", () => { + it("nominal", () => { + type MyNumber = number & Brand.Brand<"MyNumber"> + const MyNumber = Brand.nominal() + strictEqual(MyNumber(1), 1) + strictEqual(MyNumber(1.1), 1.1) + assertTrue(MyNumber.is(1)) + assertTrue(MyNumber.is(1.1)) + }) + + it("refined (predicate overload)", () => { + strictEqual(Int(1), 1) + throws(() => Int(1.1)) + assertSome(Int.option(1), 1 as Int) + assertNone(Int.option(1.1)) + assertRight(Int.either(1), 1 as Int) + assertLeft( + Int.either(1.1), + Brand.error("Expected 1.1 to be an integer") + ) + assertTrue(Int.is(1)) + assertFalse(Int.is(1.1)) + throws(() => Int(1.1), (err) => { + deepStrictEqual(err, Brand.error("Expected 1.1 to be an integer")) + }) + }) + + it("refined (Option overload)", () => { + const Int = Brand.refined( + (n) => Number.isInteger(n) ? Option.none() : Option.some(Brand.error(`Expected ${n} to be an integer`)) + ) + throws(() => Int(1.1), (err) => { + deepStrictEqual(err, Brand.error("Expected 1.1 to be an integer")) + }) + }) + + it("composition", () => { + strictEqual(PositiveInt(1), 1) + throws(() => PositiveInt(1.1)) + assertSome(PositiveInt.option(1), 1 as PositiveInt) + assertNone(PositiveInt.option(1.1)) + assertRight(PositiveInt.either(1), 1 as PositiveInt) + assertLeft( + PositiveInt.either(1.1), + Brand.error("Expected 1.1 to be an integer") + ) + assertLeft( + PositiveInt.either(-1.1), + Brand.errors( + Brand.error("Expected -1.1 to be an integer"), + Brand.error("Expected -1.1 to be positive") + ) + ) + assertTrue(PositiveInt.is(1)) + assertFalse(PositiveInt.is(1.1)) + }) +}) diff --git a/repos/effect/packages/effect/test/Cache.test.ts b/repos/effect/packages/effect/test/Cache.test.ts new file mode 100644 index 0000000..b7da605 --- /dev/null +++ b/repos/effect/packages/effect/test/Cache.test.ts @@ -0,0 +1,20 @@ +import { describe, it } from "@effect/vitest" +import { strictEqual } from "@effect/vitest/utils" +import { Cache, Effect, TestClock } from "effect" + +describe("Cache", () => { + it.effect("should not increment cache hits on expired entries", () => + Effect.gen(function*() { + const cache = yield* Cache.make({ + capacity: 100, + timeToLive: "1 seconds", + lookup: (n: number): Effect.Effect => Effect.succeed(n) + }) + yield* cache.get(42) + yield* TestClock.adjust("2 seconds") + yield* cache.get(42) + const { hits, misses } = yield* cache.cacheStats + strictEqual(hits, 0) + strictEqual(misses, 2) + })) +}) diff --git a/repos/effect/packages/effect/test/Cause.test.ts b/repos/effect/packages/effect/test/Cause.test.ts new file mode 100644 index 0000000..0220598 --- /dev/null +++ b/repos/effect/packages/effect/test/Cause.test.ts @@ -0,0 +1,1084 @@ +import { describe, it } from "@effect/vitest" +import { + assertFalse, + assertInclude, + assertLeft, + assertNone, + assertRight, + assertSome, + assertTrue, + deepStrictEqual, + strictEqual +} from "@effect/vitest/utils" +import { + Array as Arr, + Cause, + Effect, + Equal, + FastCheck as fc, + FiberId, + Hash, + Inspectable, + Option, + Predicate +} from "effect" +import * as internal from "../src/internal/cause.js" +import { causes, equalCauses, errorCauseFunctions, errors } from "./utils/cause.js" + +describe("Cause", () => { + const empty = Cause.empty + const failure = Cause.fail("error") + const defect = Cause.die("defect") + const interruption = Cause.interrupt(FiberId.runtime(1, 0)) + const sequential = Cause.sequential(failure, defect) + const parallel = Cause.parallel(failure, defect) + + describe("InterruptedException", () => { + it("correctly implements toString() and the NodeInspectSymbol", () => { + // Referenced line to be included in the string output + const ex = new Cause.InterruptedException("my message") + assertInclude(ex.toString(), "InterruptedException: my message") + + // In Node.js environments, ensure the 'inspect' method includes line information + if (typeof window === "undefined") { + // eslint-disable-next-line @typescript-eslint/no-require-imports + const { inspect } = require("node:util") + assertInclude(inspect(ex), "Cause.test.ts:39") // <= reference to the line above + } + }) + }) + + describe("UnknownException", () => { + it("exposes its `error` property", () => { + strictEqual(new Cause.UnknownException("my message").error, "my message") + const { error } = new Cause.UnknownException(new Error("my error")) + assertTrue(Predicate.isError(error)) + strictEqual(error.message, "my error") + }) + + it("exposes its `cause` property", () => { + strictEqual(new Cause.UnknownException("my message").cause, "my message") + const err2 = new Cause.UnknownException(new Error("my error")) + assertTrue(Predicate.isError(err2.cause)) + strictEqual(err2.cause.message, "my error") + }) + + it("uses a default message when none is provided", () => { + strictEqual(new Cause.UnknownException("my message").message, "An unknown error occurred") + }) + + it("accepts a custom override message", () => { + strictEqual(new Cause.UnknownException(new Error("my error"), "my message").message, "my message") + }) + }) + + it("[internal] prettyErrorMessage converts errors into readable JSON-like strings", () => { + class Error1 { + readonly _tag = "WithTag" + } + strictEqual(internal.prettyErrorMessage(new Error1()), `{"_tag":"WithTag"}`) + class Error2 { + readonly _tag = "WithMessage" + readonly message = "my message" + } + strictEqual(internal.prettyErrorMessage(new Error2()), `{"_tag":"WithMessage","message":"my message"}`) + class Error3 { + readonly _tag = "WithName" + readonly name = "my name" + } + strictEqual(internal.prettyErrorMessage(new Error3()), `{"_tag":"WithName","name":"my name"}`) + class Error4 { + readonly _tag = "WithName" + readonly name = "my name" + readonly message = "my message" + } + strictEqual( + internal.prettyErrorMessage(new Error4()), + `{"_tag":"WithName","name":"my name","message":"my message"}` + ) + class Error5 { + readonly _tag = "WithToString" + toString() { + return "Error: my string" + } + } + strictEqual(internal.prettyErrorMessage(new Error5()), `Error: my string`) + }) + + describe("Cause prototype", () => { + describe("toJSON / [NodeInspectSymbol]", () => { + const expectJSON = (cause: Cause.Cause, expected: unknown) => { + deepStrictEqual(cause.toJSON(), expected) + deepStrictEqual(cause[Inspectable.NodeInspectSymbol](), expected) + } + + it("Empty", () => { + expectJSON(Cause.empty, { + _id: "Cause", + _tag: "Empty" + }) + }) + + it("Fail", () => { + expectJSON(Cause.fail(Option.some(1)), { + _id: "Cause", + _tag: "Fail", + failure: { + _id: "Option", + _tag: "Some", + value: 1 + } + }) + }) + + it("Die", () => { + expectJSON(Cause.die(Option.some(1)), { + _id: "Cause", + _tag: "Die", + defect: { + _id: "Option", + _tag: "Some", + value: 1 + } + }) + }) + + it("Interrupt", () => { + expectJSON(Cause.interrupt(FiberId.none), { + _id: "Cause", + _tag: "Interrupt", + fiberId: { + _id: "FiberId", + _tag: "None" + } + }) + expectJSON(Cause.interrupt(FiberId.runtime(1, 0)), { + _id: "Cause", + _tag: "Interrupt", + fiberId: { + _id: "FiberId", + _tag: "Runtime", + id: 1, + startTimeMillis: 0 + } + }) + expectJSON(Cause.interrupt(FiberId.composite(FiberId.none, FiberId.runtime(1, 0))), { + _id: "Cause", + _tag: "Interrupt", + fiberId: { + _id: "FiberId", + _tag: "Composite", + left: { + _id: "FiberId", + _tag: "None" + }, + right: { + _id: "FiberId", + _tag: "Runtime", + id: 1, + startTimeMillis: 0 + } + } + }) + }) + + it("Sequential", () => { + expectJSON(Cause.sequential(Cause.fail("failure 1"), Cause.fail("failure 2")), { + _id: "Cause", + _tag: "Sequential", + left: { + _id: "Cause", + _tag: "Fail", + failure: "failure 1" + }, + right: { + _id: "Cause", + _tag: "Fail", + failure: "failure 2" + } + }) + }) + + it("Parallel", () => { + expectJSON(Cause.parallel(Cause.fail("failure 1"), Cause.fail("failure 2")), { + _id: "Cause", + _tag: "Parallel", + left: { + _id: "Cause", + _tag: "Fail", + failure: "failure 1" + }, + right: { + _id: "Cause", + _tag: "Fail", + failure: "failure 2" + } + }) + }) + }) + + describe("toString", () => { + it("Empty", () => { + strictEqual(String(Cause.empty), `All fibers interrupted without errors.`) + }) + + it("Fail", () => { + strictEqual(String(Cause.fail("my failure")), `Error: my failure`) + assertInclude(String(Cause.fail(new Error("my failure"))), "Error: my failure") + }) + + it("Die", () => { + strictEqual(String(Cause.die("die message")), `Error: die message`) + assertInclude(String(Cause.die(new Error("die message"))), "Error: die message") + }) + + it("Interrupt", () => { + strictEqual(String(Cause.interrupt(FiberId.none)), `All fibers interrupted without errors.`) + strictEqual(String(Cause.interrupt(FiberId.runtime(1, 0))), `All fibers interrupted without errors.`) + strictEqual( + String(Cause.interrupt(FiberId.composite(FiberId.none, FiberId.runtime(1, 0)))), + `All fibers interrupted without errors.` + ) + }) + + it("Sequential", () => { + strictEqual( + String(Cause.sequential(Cause.fail("failure 1"), Cause.fail("failure 2"))), + `Error: failure 1\nError: failure 2` + ) + const actual = String(Cause.sequential(Cause.fail(new Error("failure 1")), Cause.fail(new Error("failure 2")))) + assertInclude(actual, "Error: failure 1") + assertInclude(actual, "Error: failure 2") + }) + + it("Parallel", () => { + strictEqual( + String(Cause.parallel(Cause.fail("failure 1"), Cause.fail("failure 2"))), + `Error: failure 1\nError: failure 2` + ) + const actual = String( + String(Cause.parallel(Cause.fail(new Error("failure 1")), Cause.fail(new Error("failure 2")))) + ) + assertInclude(actual, "Error: failure 1") + assertInclude(actual, "Error: failure 2") + }) + }) + + describe("Equal.symbol implementation", () => { + it("compares causes by value", () => { + assertTrue(Equal.equals(Cause.fail(0), Cause.fail(0))) + assertTrue(Equal.equals(Cause.die(0), Cause.die(0))) + assertFalse(Equal.equals(Cause.fail(0), Cause.fail(1))) + assertFalse(Equal.equals(Cause.die(0), Cause.die(1))) + }) + + it("is symmetric", () => { + fc.assert(fc.property(causes, causes, (causeA, causeB) => { + strictEqual( + Equal.equals(causeA, causeB), + Equal.equals(causeB, causeA) + ) + })) + }) + + it("generates identical hashes for equal causes", () => { + fc.assert(fc.property(equalCauses, ([causeA, causeB]) => { + strictEqual(Hash.hash(causeA), Hash.hash(causeB)) + })) + }) + + it("distinguishes different failure types", () => { + assertFalse(Equal.equals(Cause.die(0), Cause.fail(0))) + assertFalse( + Equal.equals( + Cause.parallel(Cause.fail("fail1"), Cause.die("fail2")), + Cause.parallel(Cause.fail("fail2"), Cause.die("fail1")) + ) + ) + assertFalse( + Equal.equals( + Cause.sequential(Cause.fail("fail1"), Cause.die("fail2")), + Cause.parallel(Cause.fail("fail1"), Cause.die("fail2")) + ) + ) + }) + }) + }) + + describe("Guards", () => { + it("isCause", () => { + assertTrue(Cause.isCause(empty)) + assertTrue(Cause.isCause(failure)) + assertTrue(Cause.isCause(defect)) + assertTrue(Cause.isCause(interruption)) + assertTrue(Cause.isCause(sequential)) + assertTrue(Cause.isCause(parallel)) + + assertFalse(Cause.isCause({})) + }) + + it("isEmptyType", () => { + assertTrue(Cause.isEmptyType(empty)) + assertFalse(Cause.isEmptyType(failure)) + assertFalse(Cause.isEmptyType(defect)) + assertFalse(Cause.isEmptyType(interruption)) + assertFalse(Cause.isEmptyType(sequential)) + assertFalse(Cause.isEmptyType(parallel)) + }) + + it("isFailType", () => { + assertFalse(Cause.isFailType(empty)) + assertTrue(Cause.isFailType(failure)) + assertFalse(Cause.isFailType(defect)) + assertFalse(Cause.isFailType(interruption)) + assertFalse(Cause.isFailType(sequential)) + assertFalse(Cause.isFailType(parallel)) + }) + + it("isDieType", () => { + assertFalse(Cause.isDieType(empty)) + assertFalse(Cause.isDieType(failure)) + assertTrue(Cause.isDieType(defect)) + assertFalse(Cause.isDieType(interruption)) + assertFalse(Cause.isDieType(sequential)) + assertFalse(Cause.isDieType(parallel)) + }) + + it("isInterruptType", () => { + assertFalse(Cause.isInterruptType(empty)) + assertFalse(Cause.isInterruptType(failure)) + assertFalse(Cause.isInterruptType(defect)) + assertTrue(Cause.isInterruptType(interruption)) + assertFalse(Cause.isInterruptType(sequential)) + assertFalse(Cause.isInterruptType(parallel)) + }) + + it("isSequentialType", () => { + assertFalse(Cause.isSequentialType(empty)) + assertFalse(Cause.isSequentialType(failure)) + assertFalse(Cause.isSequentialType(defect)) + assertFalse(Cause.isSequentialType(interruption)) + assertTrue(Cause.isSequentialType(sequential)) + assertFalse(Cause.isSequentialType(parallel)) + }) + + it("isParallelType", () => { + assertFalse(Cause.isParallelType(empty)) + assertFalse(Cause.isParallelType(failure)) + assertFalse(Cause.isParallelType(defect)) + assertFalse(Cause.isParallelType(interruption)) + assertFalse(Cause.isParallelType(sequential)) + assertTrue(Cause.isParallelType(parallel)) + }) + }) + + describe("Getters", () => { + it("isEmpty", () => { + assertTrue(Cause.isEmpty(empty)) + assertTrue(Cause.isEmpty(Cause.sequential(empty, empty))) + assertTrue(Cause.isEmpty(Cause.parallel(empty, empty))) + assertTrue(Cause.isEmpty(Cause.parallel(empty, Cause.sequential(empty, empty)))) + assertTrue(Cause.isEmpty(Cause.sequential(empty, Cause.parallel(empty, empty)))) + + assertFalse(Cause.isEmpty(defect)) + assertFalse(Cause.isEmpty(Cause.sequential(empty, failure))) + assertFalse(Cause.isEmpty(Cause.parallel(empty, failure))) + assertFalse(Cause.isEmpty(Cause.parallel(empty, Cause.sequential(empty, failure)))) + assertFalse(Cause.isEmpty(Cause.sequential(empty, Cause.parallel(empty, failure)))) + }) + + it("isFailure", () => { + assertTrue(Cause.isFailure(failure)) + assertTrue(Cause.isFailure(Cause.sequential(empty, failure))) + assertTrue(Cause.isFailure(Cause.parallel(empty, failure))) + assertTrue(Cause.isFailure(Cause.parallel(empty, Cause.sequential(empty, failure)))) + assertTrue(Cause.isFailure(Cause.sequential(empty, Cause.parallel(empty, failure)))) + + assertFalse(Cause.isFailure(Cause.sequential(empty, Cause.parallel(empty, empty)))) + }) + + it("isDie", () => { + assertTrue(Cause.isDie(defect)) + assertTrue(Cause.isDie(Cause.sequential(empty, defect))) + assertTrue(Cause.isDie(Cause.parallel(empty, defect))) + assertTrue(Cause.isDie(Cause.parallel(empty, Cause.sequential(empty, defect)))) + assertTrue(Cause.isDie(Cause.sequential(empty, Cause.parallel(empty, defect)))) + + assertFalse(Cause.isDie(Cause.sequential(empty, Cause.parallel(empty, empty)))) + }) + + it("isInterrupted", () => { + assertTrue(Cause.isInterrupted(interruption)) + assertTrue(Cause.isInterrupted(Cause.sequential(empty, interruption))) + assertTrue(Cause.isInterrupted(Cause.parallel(empty, interruption))) + assertTrue(Cause.isInterrupted(Cause.parallel(empty, Cause.sequential(empty, interruption)))) + assertTrue(Cause.isInterrupted(Cause.sequential(empty, Cause.parallel(empty, interruption)))) + + assertTrue(Cause.isInterrupted(Cause.sequential(failure, interruption))) + assertTrue(Cause.isInterrupted(Cause.parallel(failure, interruption))) + assertTrue(Cause.isInterrupted(Cause.parallel(failure, Cause.sequential(empty, interruption)))) + assertTrue(Cause.isInterrupted(Cause.sequential(failure, Cause.parallel(empty, interruption)))) + + assertFalse(Cause.isInterrupted(Cause.sequential(empty, Cause.parallel(empty, empty)))) + }) + + it("isInterruptedOnly", () => { + assertTrue(Cause.isInterruptedOnly(interruption)) + assertTrue(Cause.isInterruptedOnly(Cause.sequential(empty, interruption))) + assertTrue(Cause.isInterruptedOnly(Cause.parallel(empty, interruption))) + assertTrue(Cause.isInterruptedOnly(Cause.parallel(empty, Cause.sequential(empty, interruption)))) + assertTrue(Cause.isInterruptedOnly(Cause.sequential(empty, Cause.parallel(empty, interruption)))) + // Cause.empty is considered a valid candidate + assertTrue(Cause.isInterruptedOnly(Cause.sequential(empty, Cause.parallel(empty, empty)))) + + assertFalse(Cause.isInterruptedOnly(Cause.sequential(failure, interruption))) + assertFalse(Cause.isInterruptedOnly(Cause.parallel(failure, interruption))) + assertFalse(Cause.isInterruptedOnly(Cause.parallel(failure, Cause.sequential(empty, interruption)))) + assertFalse(Cause.isInterruptedOnly(Cause.sequential(failure, Cause.parallel(empty, interruption)))) + }) + + describe("failures", () => { + it("should return a Chunk of all recoverable errors", () => { + const expectFailures = (cause: Cause.Cause, expected: Array) => { + deepStrictEqual([...Cause.failures(cause)], expected) + } + expectFailures(empty, []) + expectFailures(failure, ["error"]) + expectFailures(Cause.parallel(Cause.fail("error1"), Cause.fail("error2")), ["error1", "error2"]) + expectFailures(Cause.sequential(Cause.fail("error1"), Cause.fail("error2")), ["error1", "error2"]) + expectFailures(Cause.parallel(failure, defect), ["error"]) + expectFailures(Cause.sequential(failure, defect), ["error"]) + expectFailures(Cause.sequential(interruption, Cause.parallel(empty, failure)), ["error"]) + }) + + it("fails safely for large parallel cause constructions", () => { + const n = 10_000 + const cause = Array.from({ length: n - 1 }, () => Cause.fail("fail")).reduce(Cause.parallel, Cause.fail("fail")) + const result = Cause.failures(cause) + strictEqual(Array.from(result).length, n) + }) + }) + + it("defects", () => { + const expectDefects = (cause: Cause.Cause, expected: Array) => { + deepStrictEqual([...Cause.defects(cause)], expected) + } + expectDefects(empty, []) + expectDefects(defect, ["defect"]) + expectDefects(Cause.parallel(Cause.die("defect1"), Cause.die("defect2")), ["defect1", "defect2"]) + expectDefects(Cause.sequential(Cause.die("defect1"), Cause.die("defect2")), ["defect1", "defect2"]) + expectDefects(Cause.parallel(failure, defect), ["defect"]) + expectDefects(Cause.sequential(failure, defect), ["defect"]) + expectDefects(Cause.sequential(interruption, Cause.parallel(empty, defect)), ["defect"]) + }) + + it("interruptors", () => { + const expectInterruptors = (cause: Cause.Cause, expected: Array) => { + deepStrictEqual([...Cause.interruptors(cause)], expected) + } + expectInterruptors(empty, []) + expectInterruptors(interruption, [FiberId.runtime(1, 0)]) + expectInterruptors( + Cause.sequential( + Cause.interrupt(FiberId.runtime(1, 0)), + Cause.parallel(empty, Cause.interrupt(FiberId.runtime(2, 0))) + ), + [FiberId.runtime(2, 0), FiberId.runtime(1, 0)] + ) + }) + + it("size", () => { + strictEqual(Cause.size(empty), 0) + strictEqual(Cause.size(failure), 1) + strictEqual(Cause.size(defect), 1) + strictEqual(Cause.size(Cause.parallel(Cause.fail("error1"), Cause.fail("error2"))), 2) + strictEqual(Cause.size(Cause.sequential(Cause.fail("error1"), Cause.fail("error2"))), 2) + strictEqual(Cause.size(Cause.parallel(failure, defect)), 2) + strictEqual(Cause.size(Cause.sequential(failure, defect)), 2) + strictEqual(Cause.size(Cause.sequential(interruption, Cause.parallel(empty, failure))), 2) + strictEqual(Cause.size(Cause.sequential(interruption, Cause.parallel(defect, failure))), 3) + }) + + it("failureOption", () => { + const expectFailureOption = (cause: Cause.Cause, expected: Option.Option) => { + deepStrictEqual(Cause.failureOption(cause), expected) + } + expectFailureOption(empty, Option.none()) + expectFailureOption(failure, Option.some("error")) + expectFailureOption(Cause.sequential(Cause.fail("error1"), Cause.fail("error2")), Option.some("error1")) + expectFailureOption(Cause.parallel(Cause.fail("error1"), Cause.fail("error2")), Option.some("error1")) + expectFailureOption(Cause.parallel(failure, defect), Option.some("error")) + expectFailureOption(Cause.sequential(failure, defect), Option.some("error")) + expectFailureOption(Cause.sequential(interruption, Cause.parallel(empty, failure)), Option.some("error")) + }) + + it("failureOrCause", () => { + const expectLeft = (cause: Cause.Cause, expected: E) => { + assertLeft(Cause.failureOrCause(cause), expected) + } + const expectRight = (cause: Cause.Cause) => { + assertRight(Cause.failureOrCause(cause), cause) + } + + expectLeft(failure, "error") + expectLeft(Cause.parallel(Cause.fail("error1"), Cause.fail("error2")), "error1") + expectLeft(Cause.sequential(Cause.fail("error1"), Cause.fail("error2")), "error1") + expectLeft(Cause.sequential(interruption, Cause.parallel(empty, failure)), "error") + + expectRight(empty) + expectRight(defect) + expectRight(interruption) + expectRight(Cause.sequential(interruption, Cause.parallel(empty, defect))) + }) + + it("flipCauseOption", () => { + assertSome(Cause.flipCauseOption(empty), empty) + assertSome(Cause.flipCauseOption(defect), defect) + assertSome(Cause.flipCauseOption(interruption), interruption) + assertNone(Cause.flipCauseOption(Cause.fail(Option.none()))) + assertSome(Cause.flipCauseOption(Cause.fail(Option.some("error"))), Cause.fail("error")) + // sequential + assertSome( + Cause.flipCauseOption(Cause.sequential(Cause.fail(Option.some("error1")), Cause.fail(Option.some("error2")))), + Cause.sequential(Cause.fail("error1"), Cause.fail("error2")) + ) + assertSome( + Cause.flipCauseOption(Cause.sequential(Cause.fail(Option.some("error1")), Cause.fail(Option.none()))), + Cause.fail("error1") + ) + assertSome( + Cause.flipCauseOption(Cause.sequential(Cause.fail(Option.none()), Cause.fail(Option.some("error2")))), + Cause.fail("error2") + ) + assertNone( + Cause.flipCauseOption(Cause.sequential(Cause.fail(Option.none()), Cause.fail(Option.none()))) + ) + // parallel + assertSome( + Cause.flipCauseOption(Cause.parallel(Cause.fail(Option.some("error1")), Cause.fail(Option.some("error2")))), + Cause.parallel(Cause.fail("error1"), Cause.fail("error2")) + ) + assertSome( + Cause.flipCauseOption(Cause.parallel(Cause.fail(Option.some("error1")), Cause.fail(Option.none()))), + Cause.fail("error1") + ) + assertSome( + Cause.flipCauseOption(Cause.parallel(Cause.fail(Option.none()), Cause.fail(Option.some("error2")))), + Cause.fail("error2") + ) + assertNone( + Cause.flipCauseOption(Cause.parallel(Cause.fail(Option.none()), Cause.fail(Option.none()))) + ) + }) + + it("dieOption", () => { + const expectDieOption = (cause: Cause.Cause, expected: Option.Option) => { + deepStrictEqual(Cause.dieOption(cause), expected) + } + expectDieOption(empty, Option.none()) + expectDieOption(defect, Option.some("defect")) + expectDieOption(Cause.parallel(Cause.die("defect1"), Cause.die("defect2")), Option.some("defect1")) + expectDieOption(Cause.sequential(Cause.die("defect1"), Cause.die("defect2")), Option.some("defect1")) + expectDieOption(Cause.parallel(failure, defect), Option.some("defect")) + expectDieOption(Cause.sequential(failure, defect), Option.some("defect")) + expectDieOption(Cause.sequential(interruption, Cause.parallel(empty, defect)), Option.some("defect")) + }) + + it("interruptOption", () => { + const expectInterruptOption = (cause: Cause.Cause, expected: Option.Option) => { + deepStrictEqual(Cause.interruptOption(cause), expected) + } + expectInterruptOption(empty, Option.none()) + expectInterruptOption(interruption, Option.some(FiberId.runtime(1, 0))) + expectInterruptOption( + Cause.sequential( + Cause.interrupt(FiberId.runtime(1, 0)), + Cause.parallel(empty, Cause.interrupt(FiberId.runtime(2, 0))) + ), + Option.some(FiberId.runtime(1, 0)) + ) + }) + + it("keepDefects", () => { + assertNone(Cause.keepDefects(empty)) + assertNone(Cause.keepDefects(failure)) + assertSome(Cause.keepDefects(defect), defect) + assertSome( + Cause.keepDefects(Cause.sequential(Cause.die("defect1"), Cause.die("defect2"))), + Cause.sequential(Cause.die("defect1"), Cause.die("defect2")) + ) + assertNone(Cause.keepDefects(Cause.sequential(empty, empty))) + assertSome(Cause.keepDefects(Cause.sequential(defect, failure)), defect) + assertNone(Cause.keepDefects(Cause.parallel(empty, empty))) + assertSome(Cause.keepDefects(Cause.parallel(defect, failure)), defect) + assertSome( + Cause.keepDefects(Cause.parallel(Cause.die("defect1"), Cause.die("defect2"))), + Cause.parallel(Cause.die("defect1"), Cause.die("defect2")) + ) + assertSome( + Cause.keepDefects( + Cause.sequential(failure, Cause.parallel(Cause.die("defect1"), Cause.die("defect2"))) + ), + Cause.parallel(Cause.die("defect1"), Cause.die("defect2")) + ) + assertSome( + Cause.keepDefects( + Cause.sequential(Cause.die("defect1"), Cause.parallel(failure, Cause.die("defect2"))) + ), + Cause.sequential(Cause.die("defect1"), Cause.die("defect2")) + ) + }) + + it("ensures isDie and keepDefects are consistent", () => { + fc.assert(fc.property(causes, (cause) => { + const result = Cause.keepDefects(cause) + if (Cause.isDie(cause)) { + return Option.isSome(result) + } else { + return Option.isNone(result) + } + })) + }) + + it("linearize", () => { + const expectLinearize = (cause: Cause.Cause, expected: Array>) => { + deepStrictEqual([...Cause.linearize(cause)], expected) + } + expectLinearize(empty, []) + expectLinearize(failure, [failure]) + expectLinearize(defect, [defect]) + expectLinearize(interruption, [interruption]) + expectLinearize(Cause.sequential(failure, defect), [Cause.sequential(failure, defect)]) + expectLinearize(Cause.parallel(failure, defect), [Cause.parallel(failure, defect)]) + expectLinearize(Cause.sequential(failure, Cause.sequential(interruption, defect)), [ + Cause.sequential(failure, Cause.sequential(interruption, defect)) + ]) + expectLinearize(Cause.parallel(failure, Cause.parallel(interruption, defect)), [ + Cause.parallel(failure, Cause.parallel(interruption, defect)) + ]) + expectLinearize( + Cause.sequential( + Cause.sequential(Cause.fail("error1"), Cause.fail("error2")), + Cause.sequential(Cause.fail("error3"), Cause.fail("error4")) + ), + [ + Cause.sequential( + Cause.sequential(Cause.fail("error1"), Cause.fail("error2")), + Cause.sequential(Cause.fail("error3"), Cause.fail("error4")) + ) + ] + ) + expectLinearize( + Cause.parallel( + Cause.parallel(Cause.fail("error1"), Cause.fail("error2")), + Cause.parallel(Cause.fail("error3"), Cause.fail("error4")) + ), + [ + Cause.parallel( + Cause.parallel(Cause.fail("error1"), Cause.fail("error2")), + Cause.parallel(Cause.fail("error3"), Cause.fail("error4")) + ) + ] + ) + }) + + it("stripFailures", () => { + const expectStripFailures = (cause: Cause.Cause, expected: Cause.Cause) => { + deepStrictEqual(Cause.stripFailures(cause), expected) + } + expectStripFailures(empty, empty) + expectStripFailures(failure, empty) + expectStripFailures(defect, defect) + expectStripFailures(interruption, interruption) + expectStripFailures(interruption, interruption) + expectStripFailures(Cause.sequential(failure, defect), Cause.sequential(empty, defect)) + expectStripFailures(Cause.parallel(failure, defect), Cause.parallel(empty, defect)) + }) + + it("stripSomeDefects", () => { + const cause1 = Cause.die({ + _tag: "NumberFormatException", + msg: "can't parse to int" + }) + const cause2 = Cause.die({ + _tag: "ArithmeticException", + msg: "division by zero" + }) + const stripNumberFormatException = Cause.stripSomeDefects((defect) => + Predicate.isTagged(defect, "NumberFormatException") + ? Option.some(defect) : + Option.none() + ) + assertSome(stripNumberFormatException(empty), empty) + assertSome(stripNumberFormatException(failure), failure) + assertSome(stripNumberFormatException(interruption), interruption) + assertNone(stripNumberFormatException(cause1)) + assertNone(stripNumberFormatException(Cause.sequential(cause1, cause1))) + assertSome(stripNumberFormatException(Cause.sequential(cause1, cause2)), cause2) + assertSome(stripNumberFormatException(Cause.sequential(cause2, cause1)), cause2) + assertSome( + stripNumberFormatException(Cause.sequential(cause2, cause2)), + Cause.sequential(cause2, cause2) + ) + assertNone(stripNumberFormatException(Cause.parallel(cause1, cause1))) + assertSome(stripNumberFormatException(Cause.parallel(cause1, cause2)), cause2) + assertSome(stripNumberFormatException(Cause.parallel(cause2, cause1)), cause2) + assertSome( + stripNumberFormatException(Cause.parallel(cause2, cause2)), + Cause.parallel(cause2, cause2) + ) + }) + }) + + describe("Mapping", () => { + it("as", () => { + const expectAs = (cause: Cause.Cause, expected: Cause.Cause) => { + deepStrictEqual(Cause.as(cause, 2), expected) + } + expectAs(empty, empty) + expectAs(failure, Cause.fail(2)) + expectAs(defect, defect) + expectAs(interruption, interruption) + expectAs(sequential, Cause.sequential(Cause.fail(2), defect)) + expectAs(parallel, Cause.parallel(Cause.fail(2), defect)) + }) + + it("map", () => { + const expectMap = (cause: Cause.Cause, expected: Cause.Cause) => { + deepStrictEqual(Cause.map(cause, () => 2), expected) + } + expectMap(empty, empty) + expectMap(failure, Cause.fail(2)) + expectMap(defect, defect) + expectMap(interruption, interruption) + expectMap(sequential, Cause.sequential(Cause.fail(2), defect)) + expectMap(parallel, Cause.parallel(Cause.fail(2), defect)) + }) + }) + + describe("Sequencing", () => { + describe("flatMap", () => { + it("obeys left identity", () => { + fc.assert(fc.property(causes, (cause) => { + const left = cause.pipe(Cause.flatMap(Cause.fail)) + const right = cause + assertTrue(Equal.equals(left, right)) + })) + }) + + it("obeys right identity", () => { + fc.assert(fc.property(errors, errorCauseFunctions, (error, f) => { + const left = Cause.fail(error).pipe(Cause.flatMap(f)) + const right = f(error) + assertTrue(Equal.equals(left, right)) + })) + }) + + it("is associative", () => { + fc.assert(fc.property(causes, errorCauseFunctions, errorCauseFunctions, (cause, f, g) => { + const left = cause.pipe(Cause.flatMap(f), Cause.flatMap(g)) + const right = cause.pipe(Cause.flatMap((error) => f(error).pipe(Cause.flatMap(g)))) + assertTrue(Equal.equals(left, right)) + })) + }) + }) + + it("andThen returns the second cause if the first one is failing", () => { + const err1 = Cause.fail("err1") + const err2 = Cause.fail("err2") + deepStrictEqual(err1.pipe(Cause.andThen(() => err2)), err2) + deepStrictEqual(err1.pipe(Cause.andThen(err2)), err2) + deepStrictEqual(Cause.andThen(err1, () => err2), err2) + deepStrictEqual(Cause.andThen(err1, err2), err2) + }) + + it("flatten", () => { + const expectFlatten = (cause: Cause.Cause>, expected: Cause.Cause) => { + deepStrictEqual(Cause.flatten(cause), expected) + } + expectFlatten(Cause.fail(empty), empty) + expectFlatten(Cause.fail(failure), failure) + expectFlatten(Cause.fail(defect), defect) + expectFlatten(Cause.fail(interruption), interruption) + expectFlatten(Cause.fail(sequential), sequential) + expectFlatten(Cause.fail(parallel), parallel) + }) + }) + + describe("Elements", () => { + it("contains", () => { + const expectContains = (cause: Cause.Cause, expected: Cause.Cause) => { + assertTrue(Cause.contains(cause, expected)) + } + + expectContains(empty, empty) + expectContains(failure, failure) + expectContains(defect, defect) + expectContains(interruption, interruption) + expectContains(sequential, sequential) + expectContains(parallel, parallel) + expectContains(sequential, failure) + expectContains(sequential, defect) + expectContains(parallel, failure) + expectContains(parallel, defect) + }) + + it("find", () => { + const expectFind = (cause: Cause.Cause, expected: Option.Option) => { + deepStrictEqual( + Cause.find( + cause, + (cause) => + Cause.isFailType(cause) && Predicate.isString(cause.error) ? Option.some(cause.error) : Option.none() + ), + expected + ) + } + + expectFind(empty, Option.none()) + expectFind(failure, Option.some("error")) + expectFind(defect, Option.none()) + expectFind(interruption, Option.none()) + expectFind(sequential, Option.some("error")) + expectFind(parallel, Option.some("error")) + }) + }) + + describe("Destructors", () => { + it("squash", () => { + const expectSquash = (cause: Cause.Cause, expected: unknown) => { + deepStrictEqual(Cause.squash(cause), expected) + } + + expectSquash(empty, new Cause.InterruptedException("Interrupted by fibers: ")) + expectSquash(failure, "error") + expectSquash(defect, "defect") + expectSquash(interruption, new Cause.InterruptedException("Interrupted by fibers: #1")) + expectSquash(sequential, "error") + expectSquash(parallel, "error") + expectSquash(Cause.sequential(empty, defect), "defect") + expectSquash(Cause.parallel(empty, defect), "defect") + }) + + it.todo("squashWith", () => { + }) + }) + + describe("Filtering", () => { + it("filter", () => { + const expectFilter = (cause: Cause.Cause, expected: Cause.Cause) => { + deepStrictEqual( + Cause.filter( + cause, + (cause) => Cause.isFailType(cause) && Predicate.isString(cause.error) && cause.error === "error" + ), + expected + ) + } + + expectFilter(empty, empty) + expectFilter(failure, failure) + expectFilter(defect, defect) + expectFilter(interruption, interruption) + expectFilter(sequential, failure) + expectFilter(Cause.sequential(failure, failure), Cause.sequential(failure, failure)) + expectFilter(Cause.sequential(defect, failure), failure) + expectFilter(Cause.sequential(defect, defect), empty) + expectFilter(parallel, failure) + expectFilter(Cause.parallel(failure, failure), Cause.parallel(failure, failure)) + expectFilter(Cause.parallel(defect, failure), failure) + expectFilter(Cause.parallel(defect, defect), empty) + }) + }) + + describe("Matching", () => { + it("match", () => { + const expectMatch = (cause: Cause.Cause, expected: string) => { + strictEqual( + Cause.match(cause, { + onEmpty: "Empty", + onFail: () => "Fail", + onDie: () => "Die", + onInterrupt: () => "Interrupt", + onSequential: () => "Sequential", + onParallel: () => "Parallel" + }), + expected + ) + } + expectMatch(empty, "Empty") + expectMatch(failure, "Fail") + expectMatch(defect, "Die") + expectMatch(interruption, "Interrupt") + expectMatch(sequential, "Sequential") + expectMatch(parallel, "Parallel") + }) + }) + + describe("Reducing", () => { + it.todo("reduce", () => { + }) + + it.todo("reduceWithContext", () => { + }) + }) + + describe("Formatting", () => { + it("prettyErrors", () => { + deepStrictEqual(Cause.prettyErrors(empty), []) + deepStrictEqual(Cause.prettyErrors(failure), [internal.makePrettyError("error")]) + deepStrictEqual(Cause.prettyErrors(defect), [internal.makePrettyError("defect")]) + deepStrictEqual(Cause.prettyErrors(interruption), []) + deepStrictEqual(Cause.prettyErrors(sequential), [ + internal.makePrettyError("error"), + internal.makePrettyError("defect") + ]) + deepStrictEqual(Cause.prettyErrors(parallel), [ + internal.makePrettyError("error"), + internal.makePrettyError("defect") + ]) + }) + + describe("pretty", () => { + const simplifyStackTrace = (s: string): Array => { + return Arr.filterMap(s.split("\n"), (s) => { + const t = s.trimStart() + if (t === "}") { + return Option.none() + } + if (t.startsWith("at [")) { + return Option.some(t.substring(0, t.indexOf("] ") + 1)) + } + if (t.startsWith("at ")) { + return Option.none() + } + return Option.some(t) + }) + } + + describe("renderErrorCause: false", () => { + const expectPretty = (cause: Cause.Cause, expected: string | undefined) => { + deepStrictEqual(Cause.pretty(cause), expected) + deepStrictEqual(Cause.pretty(cause, { renderErrorCause: false }), expected) + } + + it("handles array-based errors without throwing", () => { + expectPretty(Cause.fail([{ toString: "" }]), `Error: [{"toString":""}]`) + }) + + it("Empty", () => { + expectPretty(empty, "All fibers interrupted without errors.") + }) + + it("Fail", () => { + class Error1 { + readonly _tag = "WithTag" + } + expectPretty(Cause.fail(new Error1()), `Error: {"_tag":"WithTag"}`) + class Error2 { + readonly _tag = "WithMessage" + readonly message = "my message" + } + expectPretty(Cause.fail(new Error2()), `Error: {"_tag":"WithMessage","message":"my message"}`) + class Error3 { + readonly _tag = "WithName" + readonly name = "my name" + } + expectPretty(Cause.fail(new Error3()), `Error: {"_tag":"WithName","name":"my name"}`) + class Error4 { + readonly _tag = "WithName" + readonly name = "my name" + readonly message = "my message" + } + expectPretty(Cause.fail(new Error4()), `Error: {"_tag":"WithName","name":"my name","message":"my message"}`) + class Error5 { + readonly _tag = "WithToString" + toString() { + return "my string" + } + } + expectPretty(Cause.fail(new Error5()), `Error: my string`) + + const err1 = new Error("message", { cause: "my cause" }) + expectPretty(Cause.fail(err1), err1.stack) + }) + + it("Interrupt", () => { + strictEqual(Cause.pretty(Cause.interrupt(FiberId.none)), "All fibers interrupted without errors.") + strictEqual(Cause.pretty(Cause.interrupt(FiberId.runtime(1, 0))), "All fibers interrupted without errors.") + strictEqual( + Cause.pretty(Cause.interrupt(FiberId.composite(FiberId.none, FiberId.runtime(1, 0)))), + "All fibers interrupted without errors." + ) + }) + + describe("Die", () => { + it("with span", () => { + const exit: any = Effect.die(new Error("my message")).pipe( + Effect.withSpan("[myspan]"), + Effect.exit, + Effect.runSync + ) + const cause = exit.cause + const pretty = Cause.pretty(cause) + deepStrictEqual(simplifyStackTrace(pretty), [`Error: my message`, "at [myspan]"]) + }) + }) + }) + + describe("renderErrorCause: true", () => { + describe("Fail", () => { + it("no cause", () => { + const pretty = Cause.pretty(Cause.fail(new Error("my message")), { renderErrorCause: true }) + deepStrictEqual(simplifyStackTrace(pretty), ["Error: my message"]) + }) + + it("string cause", () => { + const pretty = Cause.pretty(Cause.fail(new Error("my message", { cause: "my cause" })), { + renderErrorCause: true + }) + deepStrictEqual(simplifyStackTrace(pretty), ["Error: my message", "[cause]: Error: my cause"]) + }) + + it("error cause", () => { + const pretty = Cause.pretty(Cause.fail(new Error("my message", { cause: new Error("my cause") })), { + renderErrorCause: true + }) + deepStrictEqual(simplifyStackTrace(pretty), ["Error: my message", "[cause]: Error: my cause"]) + }) + + it("error cause with nested cause", () => { + const pretty = Cause.pretty( + Cause.fail(new Error("my message", { cause: new Error("my cause", { cause: "nested cause" }) })), + { + renderErrorCause: true + } + ) + deepStrictEqual(simplifyStackTrace(pretty), [ + "Error: my message", + "[cause]: Error: my cause", + "[cause]: Error: nested cause" + ]) + }) + }) + + describe("Die", () => { + it("with span", () => { + const exit: any = Effect.die(new Error("my message", { cause: "my cause" })).pipe( + Effect.withSpan("[myspan]"), + Effect.exit, + Effect.runSync + ) + const cause = exit.cause + const pretty = Cause.pretty(cause, { renderErrorCause: true }) + deepStrictEqual(simplifyStackTrace(pretty), [ + `Error: my message`, + "at [myspan]", + "[cause]: Error: my cause" + ]) + }) + }) + }) + }) + }) +}) diff --git a/repos/effect/packages/effect/test/Channel/constructors.test.ts b/repos/effect/packages/effect/test/Channel/constructors.test.ts new file mode 100644 index 0000000..724907f --- /dev/null +++ b/repos/effect/packages/effect/test/Channel/constructors.test.ts @@ -0,0 +1,21 @@ +import { describe, it } from "@effect/vitest" +import { assertTrue, deepStrictEqual, strictEqual } from "@effect/vitest/utils" +import * as Channel from "effect/Channel" +import * as Chunk from "effect/Chunk" +import * as Effect from "effect/Effect" +import * as Exit from "effect/Exit" + +describe("Channel", () => { + it.effect("succeed", () => + Effect.gen(function*() { + const [chunk, value] = yield* Channel.runCollect(Channel.succeed(1)) + assertTrue(Chunk.isEmpty(chunk)) + strictEqual(value, 1) + })) + + it.effect("fail", () => + Effect.gen(function*() { + const result = yield* Effect.exit(Channel.runCollect(Channel.fail("uh oh"))) + deepStrictEqual(result, Exit.fail("uh oh")) + })) +}) diff --git a/repos/effect/packages/effect/test/Channel/environment.test.ts b/repos/effect/packages/effect/test/Channel/environment.test.ts new file mode 100644 index 0000000..708b78d --- /dev/null +++ b/repos/effect/packages/effect/test/Channel/environment.test.ts @@ -0,0 +1,112 @@ +import { describe, it } from "@effect/vitest" +import { deepStrictEqual, strictEqual } from "@effect/vitest/utils" +import * as Channel from "effect/Channel" +import * as Context from "effect/Context" +import * as Effect from "effect/Effect" +import * as Equal from "effect/Equal" +import { pipe } from "effect/Function" +import * as Hash from "effect/Hash" + +const NumberServiceSymbolKey = "effect/test/NumberService" + +const NumberServiceTypeId = Symbol.for(NumberServiceSymbolKey) + +type NumberServiceTypeId = typeof NumberServiceTypeId + +export interface NumberService extends Equal.Equal { + readonly [NumberServiceTypeId]: NumberServiceTypeId + readonly n: number +} + +export const NumberService = Context.GenericTag("NumberService") + +export class NumberServiceImpl implements NumberService { + readonly [NumberServiceTypeId]: NumberServiceTypeId = NumberServiceTypeId + + constructor(readonly n: number) {} + + [Hash.symbol](): number { + return pipe( + Hash.hash(NumberServiceSymbolKey), + Hash.combine(Hash.hash(this.n)) + ) + } + + [Equal.symbol](u: unknown): boolean { + return isNumberService(u) && u.n === this.n + } +} + +export const isNumberService = (u: unknown): u is NumberService => { + return typeof u === "object" && u != null && NumberServiceTypeId in u +} + +describe("Channel", () => { + it.effect("provide - simple", () => + Effect.gen(function*() { + const result = yield* pipe( + NumberService, + Channel.provideService(NumberService, new NumberServiceImpl(100)), + Channel.run + ) + deepStrictEqual(result, new NumberServiceImpl(100)) + })) + + it.effect("provide -> zip -> provide", () => + Effect.gen(function*() { + const result = yield* pipe( + NumberService, + Channel.provideService(NumberService, new NumberServiceImpl(100)), + Channel.zip( + pipe( + NumberService, + Channel.provideService(NumberService, new NumberServiceImpl(200)) + ) + ), + Channel.run + ) + deepStrictEqual(result, [new NumberServiceImpl(100), new NumberServiceImpl(200)]) + })) + + it.effect("concatMap(provide).provide", () => + Effect.gen(function*() { + const [chunk, value] = yield* pipe( + Channel.fromEffect(NumberService), + Channel.emitCollect, + Channel.mapOut((tuple) => tuple[1]), + Channel.concatMap((n) => + pipe( + NumberService, + Effect.map((m) => [n, m] as const), + Channel.provideService(NumberService, new NumberServiceImpl(200)), + Channel.flatMap(Channel.write) + ) + ), + Channel.provideService(NumberService, new NumberServiceImpl(100)), + Channel.runCollect + ) + deepStrictEqual(Array.from(chunk), [[new NumberServiceImpl(100), new NumberServiceImpl(200)] as const]) + strictEqual(value, undefined) + })) + + it.effect("provide is modular", () => + Effect.gen(function*() { + const channel1 = Channel.fromEffect(NumberService) + const channel2 = pipe( + NumberService, + Effect.provide(pipe(Context.empty(), Context.add(NumberService, new NumberServiceImpl(2)))), + Channel.fromEffect + ) + const channel3 = Channel.fromEffect(NumberService) + const [[result1, result2], result3] = yield* pipe( + channel1, + Channel.zip(channel2), + Channel.zip(channel3), + Channel.runDrain, + Effect.provideService(NumberService, new NumberServiceImpl(4)) + ) + deepStrictEqual(result1, new NumberServiceImpl(4)) + deepStrictEqual(result2, new NumberServiceImpl(2)) + deepStrictEqual(result3, new NumberServiceImpl(4)) + })) +}) diff --git a/repos/effect/packages/effect/test/Channel/error-handling.test.ts b/repos/effect/packages/effect/test/Channel/error-handling.test.ts new file mode 100644 index 0000000..313629f --- /dev/null +++ b/repos/effect/packages/effect/test/Channel/error-handling.test.ts @@ -0,0 +1,55 @@ +import { describe, it } from "@effect/vitest" +import { assertTrue, deepStrictEqual } from "@effect/vitest/utils" +import * as Channel from "effect/Channel" +import * as Deferred from "effect/Deferred" +import * as Effect from "effect/Effect" +import * as Exit from "effect/Exit" +import { pipe } from "effect/Function" +import * as Ref from "effect/Ref" + +describe("Channel", () => { + it.effect("catchAll - structure confusion", () => + Effect.gen(function*() { + const channel = pipe( + Channel.write(8), + Channel.catchAll(() => + pipe( + Channel.write(0), + Channel.concatMap(() => Channel.fail("error1")) + ) + ), + Channel.concatMap(() => Channel.fail("error2")) + ) + const result = yield* (Effect.exit(Channel.runCollect(channel))) + deepStrictEqual(result, Exit.fail("error2")) + })) + + it.effect("error cause is propagated on channel interruption", () => + Effect.gen(function*() { + const deferred = yield* (Deferred.make()) + const finished = yield* (Deferred.make()) + const ref = yield* (Ref.make>(Exit.void)) + const effect = pipe( + Deferred.succeed(deferred, void 0), + Effect.zipRight(Effect.never) + ) + yield* pipe( + Channel.fromEffect(effect), + Channel.runDrain, + Effect.onExit((exit) => Ref.set(ref, exit as Exit.Exit)), + Effect.ensuring(Deferred.succeed(finished, void 0)), + Effect.race(Deferred.await(deferred)), + Effect.either + ) + yield* (Deferred.await(finished)) // Note: interruption in race is now done in the background + const result = yield* (Ref.get(ref)) + assertTrue(Exit.isInterrupted(result)) + })) + + it.effect("scoped failures", () => + Effect.gen(function*() { + const channel = Channel.scoped(Effect.fail("error")) + const result = yield* pipe(Channel.runCollect(channel), Effect.exit) + deepStrictEqual(result, Exit.fail("error")) + })) +}) diff --git a/repos/effect/packages/effect/test/Channel/finalization.test.ts b/repos/effect/packages/effect/test/Channel/finalization.test.ts new file mode 100644 index 0000000..0eca2fd --- /dev/null +++ b/repos/effect/packages/effect/test/Channel/finalization.test.ts @@ -0,0 +1,168 @@ +import { describe, it } from "@effect/vitest" +import { assertTrue, deepStrictEqual } from "@effect/vitest/utils" +import * as Channel from "effect/Channel" +import * as Effect from "effect/Effect" +import * as Exit from "effect/Exit" +import { pipe } from "effect/Function" +import * as Ref from "effect/Ref" + +interface First { + readonly _tag: "First" + readonly n: number +} + +const First = (n: number): First => ({ _tag: "First", n }) + +interface Second { + readonly _tag: "Second" + readonly first: First +} + +const Second = (first: First): Second => ({ _tag: "Second", first }) + +describe("Channel", () => { + it.effect("ensuring - prompt closure between continuations", () => + Effect.gen(function*() { + const ref = yield* (Ref.make>([])) + const event = (label: string) => Ref.update(ref, (array) => [...array, label]) + const channel = pipe( + Channel.fromEffect(event("Acquire1")), + Channel.ensuring(event("Release11")), + Channel.ensuring(event("Release12")), + Channel.flatMap(() => + pipe( + Channel.fromEffect(event("Acquire2")), + Channel.ensuring(event("Release2")) + ) + ) + ) + const result = yield* pipe(Channel.runDrain(channel), Effect.zipRight(Ref.get(ref))) + deepStrictEqual(result, [ + "Acquire1", + "Release11", + "Release12", + "Acquire2", + "Release2" + ]) + })) + + it.effect("ensuring - last finalizers are deferred to the scope", () => + Effect.gen(function*() { + const ref = yield* (Ref.make>([])) + function event(label: string) { + return Ref.update(ref, (array) => [...array, label]) + } + const channel = pipe( + Channel.fromEffect(event("Acquire1")), + Channel.ensuring(event("Release11")), + Channel.ensuring(event("Release12")), + Channel.zipRight( + pipe( + Channel.fromEffect(event("Acquire2")), + Channel.ensuring(event("Release2")) + ) + ), + Channel.ensuring(event("ReleaseOuter")) + ) + const [eventsInScope, eventsOutsideScope] = yield* pipe( + Channel.toPull(channel), + Effect.flatMap((pull) => pipe(Effect.exit(pull), Effect.zipRight(Ref.get(ref)))), + Effect.scoped, + Effect.zip(Ref.get(ref)) + ) + deepStrictEqual(eventsInScope, [ + "Acquire1", + "Release11", + "Release12", + "Acquire2" + ]) + deepStrictEqual(eventsOutsideScope, [ + "Acquire1", + "Release11", + "Release12", + "Acquire2", + "Release2", + "ReleaseOuter" + ]) + })) + + it.effect("ensuring - mixture of concatMap and ensuring", () => + Effect.gen(function*() { + const ref = yield* (Ref.make>([])) + const event = (label: string) => Ref.update(ref, (array) => [...array, label]) + const channel = pipe( + Channel.writeAll(1, 2, 3), + Channel.ensuring(event("Inner")), + Channel.concatMap((i) => + pipe( + Channel.write(First(i)), + Channel.ensuring(event("First write")) + ) + ), + Channel.ensuring(event("First concatMap")), + Channel.concatMap((first) => + pipe( + Channel.write(Second(first)), + Channel.ensuring(event("Second write")) + ) + ), + Channel.ensuring(event("Second concatMap")) + ) + const [[elements], events] = yield* pipe( + Channel.runCollect(channel), + Effect.zip(Ref.get(ref)) + ) + deepStrictEqual(events, [ + "Second write", + "First write", + "Second write", + "First write", + "Second write", + "First write", + "Inner", + "First concatMap", + "Second concatMap" + ]) + deepStrictEqual(Array.from(elements), [ + Second(First(1)), + Second(First(2)), + Second(First(3)) + ]) + })) + + it.effect("ensuring - finalizer ordering 2", () => + Effect.gen(function*() { + const ref = yield* (Ref.make>([])) + const event = (label: string) => Ref.update(ref, (array) => [...array, label]) + const channel = pipe( + Channel.writeAll(1, 2), + Channel.mapOutEffect((n) => pipe(event(`pulled ${n}`), Effect.as(n))), + Channel.concatMap((n) => + pipe( + Channel.write(n), + Channel.ensuring(event(`close ${n}`)) + ) + ) + ) + yield* (Channel.runDrain(channel)) + const result = yield* (Ref.get(ref)) + deepStrictEqual(Array.from(result), [ + "pulled 1", + "close 1", + "pulled 2", + "close 2" + ]) + })) + + it.effect("ensuring - finalizer failure is propagated", () => + Effect.gen(function*() { + const result = yield* pipe( + Channel.void, + Channel.ensuring(Effect.dieMessage("die")), + Channel.ensuring(Effect.void), + Channel.runDrain, + Effect.exit + ) + assertTrue(Exit.isFailure(result)) + })) +}) diff --git a/repos/effect/packages/effect/test/Channel/foreign.test.ts b/repos/effect/packages/effect/test/Channel/foreign.test.ts new file mode 100644 index 0000000..c9b7170 --- /dev/null +++ b/repos/effect/packages/effect/test/Channel/foreign.test.ts @@ -0,0 +1,25 @@ +import { describe, it } from "@effect/vitest" +import { strictEqual } from "@effect/vitest/utils" +import { Channel, Context, Effect, Either, Exit, Option, pipe, Random } from "effect" +import { unify } from "effect/Unify" + +describe("Channel.Foreign", () => { + it.effect("Tag", () => + Effect.gen(function*() { + const tag = Context.GenericTag("number") + const result = yield* pipe(tag, Channel.run, Effect.provideService(tag, 10)) + strictEqual(result, 10) + })) + + it.effect("Unify", () => + Effect.gen(function*() { + const unifiedEffect = unify((yield* (Random.nextInt)) > 1 ? Effect.succeed(0) : Effect.fail(1)) + const unifiedExit = unify((yield* (Random.nextInt)) > 1 ? Exit.succeed(0) : Exit.fail(1)) + const unifiedEither = unify((yield* (Random.nextInt)) > 1 ? Either.right(0) : Either.left(1)) + const unifiedOption = unify((yield* (Random.nextInt)) > 1 ? Option.some(0) : Option.none()) + strictEqual(yield* (Channel.run(unifiedEffect)), 0) + strictEqual(yield* (Channel.run(unifiedExit)), 0) + strictEqual(yield* (Channel.run(unifiedEither)), 0) + strictEqual(yield* (Channel.run(unifiedOption)), 0) + })) +}) diff --git a/repos/effect/packages/effect/test/Channel/interruption.test.ts b/repos/effect/packages/effect/test/Channel/interruption.test.ts new file mode 100644 index 0000000..cf1b873 --- /dev/null +++ b/repos/effect/packages/effect/test/Channel/interruption.test.ts @@ -0,0 +1,86 @@ +import { describe, it } from "@effect/vitest" +import { assertLeft, assertTrue, strictEqual } from "@effect/vitest/utils" +import * as Channel from "effect/Channel" +import * as Deferred from "effect/Deferred" +import * as Effect from "effect/Effect" +import * as Fiber from "effect/Fiber" +import { pipe } from "effect/Function" +import * as Ref from "effect/Ref" + +describe("Channel", () => { + it.effect("interruptWhen - interrupts the current element", () => + Effect.gen(function*() { + const interrupted = yield* (Ref.make(false)) + const latch = yield* (Deferred.make()) + const halt = yield* (Deferred.make()) + const started = yield* (Deferred.make()) + const channel = pipe( + Deferred.succeed(started, void 0), + Effect.zipRight(Deferred.await(latch)), + Effect.onInterrupt(() => Ref.set(interrupted, true)), + Channel.fromEffect, + Channel.interruptWhen(Deferred.await(halt)) + ) + const fiber = yield* (Effect.fork(Channel.runDrain(channel))) + yield* pipe( + Deferred.await(started), + Effect.zipRight(Deferred.succeed(halt, void 0)) + ) + yield* (Fiber.await(fiber)) + const result = yield* (Ref.get(interrupted)) + assertTrue(result) + })) + + it.effect("interruptWhen - propagates errors", () => + Effect.gen(function*() { + const deferred = yield* (Deferred.make()) + const channel = pipe( + Channel.fromEffect(Effect.never), + Channel.interruptWhen(Deferred.await(deferred)) + ) + yield* (Deferred.fail(deferred, "fail")) + const result = yield* (Effect.either(Channel.runDrain(channel))) + assertLeft(result, "fail") + })) + + it.effect("interruptWhenDeferred - interrupts the current element", () => + Effect.gen(function*() { + const interrupted = yield* (Ref.make(false)) + const latch = yield* (Deferred.make()) + const halt = yield* (Deferred.make()) + const started = yield* (Deferred.make()) + const channel = pipe( + Deferred.succeed(started, void 0), + Effect.zipRight(Deferred.await(latch)), + Effect.onInterrupt(() => Ref.set(interrupted, true)), + Channel.fromEffect, + Channel.interruptWhenDeferred(halt) + ) + const fiber = yield* (Effect.fork(Channel.runDrain(channel))) + yield* pipe( + Deferred.await(started), + Effect.zipRight(Deferred.succeed(halt, void 0)) + ) + yield* (Fiber.await(fiber)) + const result = yield* (Ref.get(interrupted)) + assertTrue(result) + })) + + it.effect("interruptWhenDeferred - propagates errors", () => + Effect.gen(function*() { + const deferred = yield* (Deferred.make()) + const channel = pipe( + Channel.fromEffect(Effect.never), + Channel.interruptWhenDeferred(deferred) + ) + yield* (Deferred.fail(deferred, "fail")) + const result = yield* (Effect.either(Channel.runDrain(channel))) + assertLeft(result, "fail") + })) + + it.effect("runScoped - in uninterruptible region", () => + Effect.gen(function*() { + const result = yield* Effect.uninterruptible(Channel.run(Channel.void)) + strictEqual(result, undefined) + })) +}) diff --git a/repos/effect/packages/effect/test/Channel/mapping.test.ts b/repos/effect/packages/effect/test/Channel/mapping.test.ts new file mode 100644 index 0000000..a4084b6 --- /dev/null +++ b/repos/effect/packages/effect/test/Channel/mapping.test.ts @@ -0,0 +1,299 @@ +import { describe, it } from "@effect/vitest" +import { assertTrue, deepStrictEqual, strictEqual } from "@effect/vitest/utils" +import * as Array from "effect/Array" +import * as Channel from "effect/Channel" +import * as ChildExecutorDecision from "effect/ChildExecutorDecision" +import * as Chunk from "effect/Chunk" +import * as Effect from "effect/Effect" +import * as Exit from "effect/Exit" +import { constVoid, pipe } from "effect/Function" +import * as Option from "effect/Option" +import * as Ref from "effect/Ref" +import * as UpstreamPullRequest from "effect/UpstreamPullRequest" +import * as UpstreamPullStrategy from "effect/UpstreamPullStrategy" + +interface First { + readonly _tag: "First" + readonly n: number +} + +const First = (n: number): First => ({ _tag: "First", n }) + +interface Second { + readonly _tag: "Second" + readonly first: First +} + +const Second = (first: First): Second => ({ _tag: "Second", first }) + +describe("Channel", () => { + it.effect("map", () => + Effect.gen(function*() { + const [chunk, value] = yield* pipe( + Channel.succeed(1), + Channel.map((n) => n + 1), + Channel.runCollect + ) + assertTrue(Chunk.isEmpty(chunk)) + strictEqual(value, 2) + })) + + it.effect("mapError - structure confusion", () => + Effect.gen(function*() { + const result = yield* pipe( + Channel.fail("error"), + Channel.mapError(() => 1), + Channel.runCollect, + Effect.exit + ) + deepStrictEqual(result, Exit.fail(1)) + })) + + it.effect("mapOut - simple", () => + Effect.gen(function*() { + const [chunk, value] = yield* pipe( + Channel.writeAll(1, 2, 3), + Channel.mapOut((n) => n + 1), + Channel.runCollect + ) + deepStrictEqual(Chunk.toReadonlyArray(chunk), [2, 3, 4]) + strictEqual(value, undefined) + })) + + it.effect("mapOut - mixed with flatMap", () => + Effect.gen(function*() { + const [chunk, value] = yield* pipe( + Channel.write(1), + Channel.mapOut((n) => `${n}`), + Channel.flatMap(() => Channel.write("x")), + Channel.runCollect + ) + deepStrictEqual(Chunk.toReadonlyArray(chunk), ["1", "x"]) + strictEqual(value, undefined) + })) + + it.effect("concatMap - plain", () => + Effect.gen(function*() { + const [result] = yield* pipe( + Channel.writeAll(1, 2, 3), + Channel.concatMap((i) => Channel.writeAll(i, i)), + Channel.runCollect + ) + deepStrictEqual(Chunk.toReadonlyArray(result), [1, 1, 2, 2, 3, 3]) + })) + + it.effect("concatMap - complex", () => + Effect.gen(function*() { + const [result] = yield* pipe( + Channel.writeAll(1, 2), + Channel.concatMap((i) => Channel.writeAll(i, i)), + Channel.mapOut(First), + Channel.concatMap((i) => Channel.writeAll(i, i)), + Channel.mapOut(Second), + Channel.runCollect + ) + deepStrictEqual(Chunk.toReadonlyArray(result), [ + Second(First(1)), + Second(First(1)), + Second(First(1)), + Second(First(1)), + Second(First(2)), + Second(First(2)), + Second(First(2)), + Second(First(2)) + ]) + })) + + it.effect("concatMap - read from inner channel", () => + Effect.gen(function*() { + const source = Channel.writeAll(1, 2, 3, 4) + const reader = pipe( + Channel.read(), + Channel.flatMap(Channel.write) + ) + const readers = pipe( + Channel.writeAll(void 0, void 0), + Channel.concatMap(() => pipe(reader, Channel.flatMap(() => reader))) + ) + const [result] = yield* pipe( + source, + Channel.pipeTo(readers), + Channel.runCollect + ) + deepStrictEqual(Chunk.toReadonlyArray(result), [1, 2, 3, 4]) + })) + + it.effect("concatMap - downstream failure", () => + Effect.gen(function*() { + const result = yield* pipe( + Channel.write(0), + Channel.concatMap(() => Channel.fail("error")), + Channel.runCollect, + Effect.exit + ) + deepStrictEqual(result, Exit.fail("error")) + })) + + it.effect("concatMap - upstream acquireReleaseOut + downstream failure", () => + Effect.gen(function*() { + const ref = yield* (Ref.make>([])) + const event = (label: string) => Ref.update(ref, (array) => [...array, label]) + const effect = pipe( + Channel.acquireReleaseOut(event("Acquired"), () => event("Released")), + Channel.concatMap(() => Channel.fail("error")), + Channel.runDrain, + Effect.exit + ) + const [exit, events] = yield* pipe(effect, Effect.zip(Ref.get(ref))) + deepStrictEqual(exit, Exit.fail("error")) + deepStrictEqual(events, ["Acquired", "Released"]) + })) + + it.effect("concatMap - multiple concatMaps with failure in first", () => + Effect.gen(function*() { + const result = yield* pipe( + Channel.write(void 0), + Channel.concatMap(() => Channel.write(Channel.fail("error"))), + Channel.concatMap((e) => e), + Channel.runCollect, + Effect.exit + ) + deepStrictEqual(result, Exit.fail("error")) + })) + + it.effect("concatMap - with failure then flatMap", () => + Effect.gen(function*() { + const result = yield* pipe( + Channel.write(void 0), + Channel.concatMap(() => Channel.fail("error")), + Channel.flatMap(() => Channel.write(void 0)), + Channel.runCollect, + Effect.exit + ) + deepStrictEqual(result, Exit.fail("error")) + })) + + it.effect("concatMap - multiple concatMaps with failure in first and catchAll in second", () => + Effect.gen(function*() { + const result = yield* pipe( + Channel.write(void 0), + Channel.concatMap(() => Channel.write(Channel.fail("error"))), + Channel.concatMap(Channel.catchAllCause(() => Channel.fail("error2"))), + Channel.runCollect, + Effect.exit + ) + deepStrictEqual(result, Exit.fail("error2")) + })) + + it.effect("concatMap - done value combination", () => + Effect.gen(function*() { + const [chunk, [array1, array2]] = yield* pipe( + Channel.writeAll(1, 2, 3), + Channel.as(["Outer-0"]), + Channel.concatMapWith( + (i) => pipe(Channel.write(i), Channel.as([`Inner-${i}`])), + (a: Array, b) => [...a, ...b], + (a, b) => [a, b] as const + ), + Channel.runCollect + ) + deepStrictEqual(Chunk.toReadonlyArray(chunk), [1, 2, 3]) + deepStrictEqual(array1, ["Inner-1", "Inner-2", "Inner-3"]) + deepStrictEqual(array2, ["Outer-0"]) + })) + + it.effect("concatMap - custom 1", () => + Effect.gen(function*() { + const result = yield* pipe( + Channel.writeAll(1, 2, 3, 4), + Channel.concatMapWithCustom( + (x) => + Channel.writeAll( + Option.some([x, 1]) as Option.Option, + Option.none() as Option.Option, + Option.some([x, 2]) as Option.Option, + Option.none() as Option.Option, + Option.some([x, 3]) as Option.Option, + Option.none() as Option.Option, + Option.some([x, 4]) as Option.Option + ), + constVoid, + constVoid, + UpstreamPullRequest.match({ + onPulled: () => UpstreamPullStrategy.PullAfterNext(Option.none()), + onNoUpstream: () => UpstreamPullStrategy.PullAfterAllEnqueued(Option.none()) + }), + Option.match({ + onNone: () => ChildExecutorDecision.Yield(), + onSome: () => ChildExecutorDecision.Continue() + }) + ), + Channel.runCollect, + Effect.map(([chunk]) => pipe(Chunk.toReadonlyArray(chunk), Array.getSomes)) + ) + deepStrictEqual(result, [ + [1, 1] as const, + [2, 1] as const, + [3, 1] as const, + [4, 1] as const, + [1, 2] as const, + [2, 2] as const, + [3, 2] as const, + [4, 2] as const, + [1, 3] as const, + [2, 3] as const, + [3, 3] as const, + [4, 3] as const, + [1, 4] as const, + [2, 4] as const, + [3, 4] as const, + [4, 4] as const + ]) + })) + + it.effect("concatMap - custom 2", () => + Effect.gen(function*() { + const result = yield* pipe( + Channel.writeAll(1, 2, 3, 4), + Channel.concatMapWithCustom( + (x) => + Channel.writeAll( + Option.some([x, 1]) as Option.Option, + Option.none() as Option.Option, + Option.some([x, 2]) as Option.Option, + Option.none() as Option.Option, + Option.some([x, 3]) as Option.Option, + Option.none() as Option.Option, + Option.some([x, 4]) as Option.Option + ), + constVoid, + constVoid, + () => UpstreamPullStrategy.PullAfterAllEnqueued(Option.none()), + Option.match({ + onNone: () => ChildExecutorDecision.Yield(), + onSome: () => ChildExecutorDecision.Continue() + }) + ), + Channel.runCollect, + Effect.map(([chunk]) => pipe(Chunk.toReadonlyArray(chunk), Array.getSomes)) + ) + deepStrictEqual(result, [ + [1, 1] as const, + [2, 1] as const, + [1, 2] as const, + [3, 1] as const, + [2, 2] as const, + [1, 3] as const, + [4, 1] as const, + [3, 2] as const, + [2, 3] as const, + [1, 4] as const, + [4, 2] as const, + [3, 3] as const, + [2, 4] as const, + [4, 3] as const, + [3, 4] as const, + [4, 4] as const + ]) + })) +}) diff --git a/repos/effect/packages/effect/test/Channel/merging.test.ts b/repos/effect/packages/effect/test/Channel/merging.test.ts new file mode 100644 index 0000000..babd4bc --- /dev/null +++ b/repos/effect/packages/effect/test/Channel/merging.test.ts @@ -0,0 +1,148 @@ +import { describe, it } from "@effect/vitest" +import { deepStrictEqual, strictEqual } from "@effect/vitest/utils" +import * as Cause from "effect/Cause" +import * as Channel from "effect/Channel" +import * as Deferred from "effect/Deferred" +import * as Effect from "effect/Effect" +import * as Exit from "effect/Exit" +import { constTrue, pipe } from "effect/Function" +import * as MergeDecision from "effect/MergeDecision" +import * as Ref from "effect/Ref" + +describe("Channel", () => { + it.effect("mergeWith - simple merge", () => + Effect.gen(function*() { + const [chunk, value] = yield* pipe( + Channel.writeAll(1, 2, 3), + Channel.mergeWith({ + other: Channel.writeAll(4, 5, 6), + // TODO: remove + onSelfDone: (leftDone) => MergeDecision.AwaitConst(Effect.suspend(() => leftDone)), + onOtherDone: (rightDone) => MergeDecision.AwaitConst(Effect.suspend(() => rightDone)) + }), + Channel.runCollect + ) + deepStrictEqual(Array.from(chunk), [1, 2, 3, 4, 5, 6]) + strictEqual(value, undefined) + })) + + it.effect("mergeWith - merge with different types", () => + Effect.gen(function*() { + const left = pipe( + Channel.write(1), + Channel.zipRight( + pipe( + Effect.try(() => "whatever"), + Effect.catchAllCause((cause) => + Cause.isRuntimeException(cause) ? + Effect.failCause(cause) : + Effect.die(cause) + ), + Channel.fromEffect + ) + ) + ) + const right = pipe( + Channel.write(2), + Channel.zipRight( + pipe( + Effect.try(constTrue), + Effect.catchAllCause((cause) => + Cause.isIllegalArgumentException(cause) ? + Effect.failCause(cause) : + Effect.die(cause) + ), + Channel.fromEffect + ) + ) + ) + const [chunk, value] = yield* pipe( + left, + Channel.mergeWith({ + other: right, + // TODO: remove + onSelfDone: (leftDone) => + MergeDecision.Await((rightDone) => Effect.suspend(() => Exit.zip(leftDone, rightDone))), + onOtherDone: (rightDone) => + MergeDecision.Await((leftDone) => Effect.suspend(() => Exit.zip(leftDone, rightDone))) + }), + Channel.runCollect + ) + deepStrictEqual(Array.from(chunk), [1, 2]) + deepStrictEqual(value, ["whatever", true]) + })) + + it.effect("mergeWith - handles polymorphic failures", () => + Effect.gen(function*() { + const left = pipe( + Channel.write(1), + Channel.zipRight(pipe(Channel.fail("boom"), Channel.as(true))) + ) + const right = pipe( + Channel.write(2), + Channel.zipRight(pipe(Channel.fail(true), Channel.as(true))) + ) + const result = yield* pipe( + left, + Channel.mergeWith({ + other: right, + onSelfDone: (leftDone) => + MergeDecision.Await((rightDone) => + pipe( + // TODO: remove + Effect.suspend(() => leftDone), + Effect.flip, + // TODO: remove + Effect.zip(Effect.flip(Effect.suspend(() => rightDone))), + Effect.flip + ) + ), + onOtherDone: (rightDone) => + MergeDecision.Await((leftDone) => + pipe( + // TODO: remove + Effect.suspend(() => leftDone), + Effect.flip, + // TODO: remove + Effect.zip(Effect.flip(Effect.suspend(() => rightDone))), + Effect.flip + ) + ) + }), + Channel.runDrain, + Effect.exit + ) + deepStrictEqual(result, Exit.fail<[string, boolean]>(["boom", true])) + })) + + it.effect("mergeWith - interrupts losing side", () => + Effect.gen(function*() { + const latch = yield* (Deferred.make()) + const interrupted = yield* (Ref.make(false)) + const left = Channel.zipRight( + Channel.write(1), + pipe( + Deferred.succeed(latch, void 0), + Effect.zipRight(Effect.never), + Effect.onInterrupt(() => Ref.set(interrupted, true)), + Channel.fromEffect + ) + ) + const right = Channel.zipRight( + Channel.write(2), + Channel.fromEffect(Deferred.await(latch)) + ) + const merged = Channel.mergeWith(left, { + other: right, + // TODO: remove + onSelfDone: (leftDone) => MergeDecision.Done(Effect.suspend(() => leftDone)), + onOtherDone: (_rightDone) => + MergeDecision.Done(pipe( + Ref.get(interrupted), + Effect.flatMap((isInterrupted) => isInterrupted ? Effect.void : Effect.fail(void 0)) + )) + }) + const result = yield* (Effect.exit(Channel.runDrain(merged))) + deepStrictEqual(result, Exit.succeed(void 0)) + })) +}) diff --git a/repos/effect/packages/effect/test/Channel/reading.test.ts b/repos/effect/packages/effect/test/Channel/reading.test.ts new file mode 100644 index 0000000..dc65a2b --- /dev/null +++ b/repos/effect/packages/effect/test/Channel/reading.test.ts @@ -0,0 +1,303 @@ +import { describe, it } from "@effect/vitest" +import { deepStrictEqual, strictEqual } from "@effect/vitest/utils" +import * as Array from "effect/Array" +import * as Channel from "effect/Channel" +import * as Chunk from "effect/Chunk" +import * as Effect from "effect/Effect" +import * as Equal from "effect/Equal" +import { pipe } from "effect/Function" +import * as Hash from "effect/Hash" +import * as HashSet from "effect/HashSet" +import * as MergeDecision from "effect/MergeDecision" +import * as Option from "effect/Option" +import * as Random from "effect/Random" +import * as Ref from "effect/Ref" + +export const mapper = ( + f: (a: A) => B +): Channel.Channel => { + return Channel.readWith({ + onInput: (a: A) => + Channel.flatMap( + Channel.write(f(a)), + () => mapper(f) + ), + onFailure: () => Channel.void, + onDone: () => Channel.void + }) +} + +export const refWriter = ( + ref: Ref.Ref> +): Channel.Channel => { + return Channel.readWith({ + onInput: (a: A) => + Channel.flatMap( + Channel.fromEffect(Effect.asVoid(Ref.update(ref, Array.prepend(a)))), + () => refWriter(ref) + ), + onFailure: () => Channel.void, + onDone: () => Channel.void + }) +} + +export const refReader = ( + ref: Ref.Ref> +): Channel.Channel => { + return pipe( + Channel.fromEffect( + Ref.modify(ref, (array) => { + if (Array.isEmptyReadonlyArray(array)) { + return [Option.none(), Array.empty()] as const + } + return [Option.some(array[0]!), array.slice(1)] as const + }) + ), + Channel.flatMap(Option.match({ + onNone: () => Channel.void, + onSome: (i) => Channel.flatMap(Channel.write(i), () => refReader(ref)) + })) + ) +} + +describe("Channel", () => { + it.effect("simple reads", () => + Effect.gen(function*() { + class Whatever implements Equal.Equal { + constructor(readonly i: number) {} + [Hash.symbol](): number { + return Hash.hash(this.i) + } + [Equal.symbol](u: unknown): boolean { + return u instanceof Whatever && u.i === this.i + } + } + const left = Channel.writeAll(1, 2, 3) + const right = pipe( + Channel.read(), + Channel.catchAll(() => Channel.succeed(4)), + Channel.flatMap((i) => Channel.write(new Whatever(i))) + ) + const channel = pipe( + left, + Channel.pipeTo( + pipe( + right, + Channel.zipRight(right), + Channel.zipRight(right), + Channel.zipRight(right) + ) + ) + ) + const result = yield* (Channel.runCollect(channel)) + const [chunk, value] = result + deepStrictEqual(Chunk.toReadonlyArray(chunk), [ + new Whatever(1), + new Whatever(2), + new Whatever(3), + new Whatever(4) + ]) + strictEqual(value, undefined) + })) + + it.effect("read pipelining", () => + Effect.gen(function*() { + const innerChannel = pipe( + Channel.fromEffect(Ref.make>([])), + Channel.flatMap((ref) => { + const inner = (): Channel.Channel => + Channel.readWith({ + onInput: (input: number) => + pipe( + Channel.fromEffect(Ref.update(ref, (array) => [...array, input])), + Channel.zipRight(Channel.write(input)), + Channel.flatMap(inner) + ), + onFailure: () => Channel.void, + onDone: () => Channel.void + }) + return pipe( + inner(), + Channel.zipRight(Channel.fromEffect(Ref.get(ref))) + ) + }) + ) + const f = (n: number) => n + const g = (n: number) => [n, n] + const channel = pipe( + Channel.writeAll(1, 2), + Channel.pipeTo(mapper(f)), + Channel.pipeTo(pipe(mapper(g), Channel.concatMap((ns) => Channel.writeAll(...ns)), Channel.asVoid)), + Channel.pipeTo(innerChannel) + ) + const [chunk, list] = yield* (Channel.runCollect(channel)) + deepStrictEqual(Chunk.toReadonlyArray(chunk), [1, 1, 2, 2]) + deepStrictEqual(list, [1, 1, 2, 2]) + })) + + it.effect("read pipelining 2", () => + Effect.gen(function*() { + const ref = yield* (Ref.make>([])) + const intProducer: Channel.Channel = Channel.writeAll( + 1, + 2, + 3, + 4, + 5 + ) + const readIntsN = ( + n: number + ): Channel.Channel => + n > 0 + ? Channel.readWith({ + onInput: (i: number) => pipe(Channel.write(i), Channel.flatMap(() => readIntsN(n - 1))), + onFailure: () => Channel.succeed("EOF"), + onDone: () => Channel.succeed("EOF") + }) + : Channel.succeed("end") + + const sum = ( + label: string, + n: number + ): Channel.Channel => + Channel.readWith({ + onInput: (input: number) => sum(label, n + input), + onFailure: () => Channel.fromEffect(Ref.update(ref, (array) => [...array, n])), + onDone: () => Channel.fromEffect(Ref.update(ref, (array) => [...array, n])) + }) + + const channel = pipe( + intProducer, + Channel.pipeTo( + pipe( + readIntsN(2), + Channel.pipeTo(sum("left", 0)), + Channel.zipRight(readIntsN(2)), + Channel.pipeTo(sum("right", 0)) + ) + ) + ) + const result = yield* pipe(Channel.run(channel), Effect.zipRight(Ref.get(ref))) + deepStrictEqual(result, [3, 7]) + })) + + it.effect("reading with resources", () => + Effect.gen(function*() { + const ref = yield* (Ref.make>([])) + const event = (label: string) => Ref.update(ref, (array) => [...array, label]) + const left = pipe( + Channel.acquireReleaseOut( + event("Acquire outer"), + () => event("Release outer") + ), + Channel.concatMap( + () => + pipe( + Channel.writeAll(1, 2, 3), + Channel.concatMap((i) => + Channel.acquireReleaseOut( + pipe(event(`Acquire ${i}`), Effect.as(i)), + () => event(`Release ${i}`) + ) + ) + ) + ) + ) + const read = pipe( + Channel.read(), + Channel.mapEffect((i) => event(`Read ${i}`)), + Channel.asVoid + ) + const right = pipe( + read, + Channel.zipRight(read), + Channel.catchAll(() => Channel.void) + ) + const channel = pipe(left, Channel.pipeTo(right)) + const result = yield* pipe(Channel.runDrain(channel), Effect.zipRight(Ref.get(ref))) + deepStrictEqual(result, [ + "Acquire outer", + "Acquire 1", + "Read 1", + "Release 1", + "Acquire 2", + "Read 2", + "Release 2", + "Release outer" + ]) + })) + + it.effect("simple concurrent reads", () => + Effect.gen(function*() { + const capacity = 128 + const elements = yield* (Effect.replicateEffect(Random.nextInt, capacity)) + const source = yield* (Ref.make(Array.fromIterable(elements))) + const destination = yield* (Ref.make>([])) + const twoWriters = pipe( + refWriter(destination), + Channel.mergeWith({ + other: refWriter(destination), + onSelfDone: () => MergeDecision.AwaitConst(Effect.void), + onOtherDone: () => MergeDecision.AwaitConst(Effect.void) + }) + ) + const [missing, surplus] = yield* pipe( + refReader(source), + Channel.pipeTo(twoWriters), + Channel.mapEffect(() => Ref.get(destination)), + Channel.run, + Effect.map((result) => { + let missing = HashSet.fromIterable(elements) + let surplus = HashSet.fromIterable(result) + for (const value of result) { + missing = pipe(missing, HashSet.remove(value)) + } + for (const value of elements) { + surplus = pipe(surplus, HashSet.remove(value)) + } + return [missing, surplus] as const + }) + ) + + strictEqual(HashSet.size(missing), 0) + strictEqual(HashSet.size(surplus), 0) + })) + + it.effect("nested concurrent reads", () => + Effect.gen(function*() { + const capacity = 128 + const f = (n: number) => n + 1 + const elements = yield* (Effect.replicateEffect(Random.nextInt, capacity)) + const source = yield* (Ref.make(Array.fromIterable(elements))) + const destination = yield* (Ref.make>([])) + const twoWriters = pipe( + mapper(f), + Channel.pipeTo(refWriter(destination)), + Channel.mergeWith({ + other: pipe(mapper(f), Channel.pipeTo(refWriter(destination))), + onSelfDone: () => MergeDecision.AwaitConst(Effect.void), + onOtherDone: () => MergeDecision.AwaitConst(Effect.void) + }) + ) + const [missing, surplus] = yield* pipe( + refReader(source), + Channel.pipeTo(twoWriters), + Channel.mapEffect(() => Ref.get(destination)), + Channel.run, + Effect.map((result) => { + const expected = HashSet.fromIterable(elements.map(f)) + let missing = HashSet.fromIterable(expected) + let surplus = HashSet.fromIterable(result) + for (const value of result) { + missing = pipe(missing, HashSet.remove(value)) + } + for (const value of expected) { + surplus = pipe(surplus, HashSet.remove(value)) + } + return [missing, surplus] as const + }) + ) + strictEqual(HashSet.size(missing), 0) + strictEqual(HashSet.size(surplus), 0) + })) +}) diff --git a/repos/effect/packages/effect/test/Channel/scoping.test.ts b/repos/effect/packages/effect/test/Channel/scoping.test.ts new file mode 100644 index 0000000..e45d41d --- /dev/null +++ b/repos/effect/packages/effect/test/Channel/scoping.test.ts @@ -0,0 +1,68 @@ +import { describe, it } from "@effect/vitest" +import { assertLeft, strictEqual } from "@effect/vitest/utils" +import * as Cause from "effect/Cause" +import * as Channel from "effect/Channel" +import * as Deferred from "effect/Deferred" +import * as Effect from "effect/Effect" +import * as Fiber from "effect/Fiber" +import * as FiberId from "effect/FiberId" +import { pipe } from "effect/Function" +import * as Ref from "effect/Ref" + +describe("Channel", () => { + it("acquireUseReleaseOut - acquire is executed uninterruptibly", async () => { + const latch = Deferred.unsafeMake(FiberId.none) + const program = Effect.gen(function*() { + const ref = yield* (Ref.make(0)) + const acquire = Effect.zipRight(Ref.update(ref, (n) => n + 1), Effect.yieldNow()) + const release = Ref.update(ref, (n) => n - 1) + yield* pipe( + Channel.acquireReleaseOut(acquire, () => release), + Channel.as(Channel.fromEffect(Deferred.await(latch))), + Channel.runDrain, + Effect.fork, + Effect.flatMap((fiber) => pipe(Effect.yieldNow(), Effect.zipRight(Fiber.interrupt(fiber)))), + Effect.repeatN(1_000) + ) + return yield* (Ref.get(ref)) + }) + const result = await Effect.runPromise(program) + await Effect.runPromise(Deferred.succeed(latch, void 0)) + strictEqual(result, 0) + }, 35_000) + + it("scoped closes the scope", async () => { + const latch = Deferred.unsafeMake(FiberId.none) + const program = Effect.gen(function*() { + const ref = yield* (Ref.make(0)) + const acquire = Effect.zipRight(Ref.update(ref, (n) => n + 1), Effect.yieldNow()) + const release = () => Ref.update(ref, (n) => n - 1) + const scoped = Effect.acquireRelease(acquire, release) + yield* pipe( + Channel.unwrapScoped(pipe(scoped, Effect.as(Channel.fromEffect(Deferred.await(latch))))), + Channel.runDrain, + Effect.fork, + Effect.flatMap((fiber) => pipe(Effect.yieldNow(), Effect.zipRight(Fiber.interrupt(fiber)))), + Effect.repeatN(1_000) + ) + return yield* (Ref.get(ref)) + }) + const result = await Effect.runPromise(program) + await Effect.runPromise(Deferred.succeed(latch, void 0)) + strictEqual(result, 0) + }, 35_000) + + it.effect("finalizer failure is propagated", () => + Effect.gen(function*() { + const exit = yield* pipe( + Channel.void, + Channel.ensuring(Effect.die("ok")), + Channel.ensuring(Effect.void), + Channel.runDrain, + Effect.sandbox, + Effect.either + ) + + assertLeft(exit, Cause.die("ok")) + })) +}) diff --git a/repos/effect/packages/effect/test/Channel/sequencing.test.ts b/repos/effect/packages/effect/test/Channel/sequencing.test.ts new file mode 100644 index 0000000..b9b3855 --- /dev/null +++ b/repos/effect/packages/effect/test/Channel/sequencing.test.ts @@ -0,0 +1,41 @@ +import { describe, it } from "@effect/vitest" +import { assertTrue, deepStrictEqual, strictEqual } from "@effect/vitest/utils" +import * as Channel from "effect/Channel" +import * as Chunk from "effect/Chunk" +import * as Effect from "effect/Effect" +import * as Exit from "effect/Exit" +import { pipe } from "effect/Function" + +describe("Channel", () => { + it.effect("flatMap - simple", () => + Effect.gen(function*() { + const channel = pipe( + Channel.succeed(1), + Channel.flatMap((x) => + pipe( + Channel.succeed(x * 2), + Channel.flatMap((y) => + pipe( + Channel.succeed(x + y), + Channel.map((z) => x + y + z) + ) + ) + ) + ) + ) + const [chunk, value] = yield* (Channel.runCollect(channel)) + assertTrue(Chunk.isEmpty(chunk)) + strictEqual(value, 6) + })) + + it.effect("flatMap - structure confusion", () => + Effect.gen(function*() { + const channel = pipe( + Channel.write(Chunk.make(1, 2)), + Channel.concatMap(Channel.writeAll), + Channel.zipRight(Channel.fail("hello")) + ) + const result = yield* (Effect.exit(Channel.runDrain(channel))) + deepStrictEqual(result, Exit.fail("hello")) + })) +}) diff --git a/repos/effect/packages/effect/test/Channel/stack-safety.test.ts b/repos/effect/packages/effect/test/Channel/stack-safety.test.ts new file mode 100644 index 0000000..ab7fe38 --- /dev/null +++ b/repos/effect/packages/effect/test/Channel/stack-safety.test.ts @@ -0,0 +1,62 @@ +import { describe, it } from "@effect/vitest" +import { deepStrictEqual, strictEqual } from "@effect/vitest/utils" +import * as Channel from "effect/Channel" +import * as Chunk from "effect/Chunk" +import * as Effect from "effect/Effect" +import { pipe } from "effect/Function" + +describe("Channel", () => { + it.effect("mapOut is stack safe", () => + Effect.gen(function*() { + const N = 10_000 + const [chunk, value] = yield* pipe( + Chunk.range(1, N), + Chunk.reduce( + Channel.write(1), + (channel, n) => pipe(channel, Channel.mapOut((i) => i + n)) + ), + Channel.runCollect + ) + const expected = pipe( + Chunk.range(1, N), + Chunk.reduce(1, (x, y) => x + y) + ) + strictEqual(Chunk.unsafeHead(chunk), expected) + strictEqual(value, undefined) + }), 20_000) + + it.effect("concatMap is stack safe", () => + Effect.gen(function*() { + const N = 10_000 + const [chunk, value] = yield* pipe( + Chunk.range(1, N), + Chunk.reduce( + Channel.write(1), + (channel, n) => + pipe( + channel, + Channel.concatMap(() => Channel.write(n)), + Channel.asVoid + ) + ), + Channel.runCollect + ) + strictEqual(Chunk.unsafeHead(chunk), N) + strictEqual(value, undefined) + }), 20_000) + + it.effect("flatMap is stack safe", () => + Effect.gen(function*() { + const N = 10_000 + const [chunk, value] = yield* pipe( + Chunk.range(1, N), + Chunk.reduce( + Channel.write(0), + (channel, n) => pipe(channel, Channel.flatMap(() => Channel.write(n))) + ), + Channel.runCollect + ) + deepStrictEqual(Array.from(chunk), Array.from(Chunk.range(0, N))) + strictEqual(value, undefined) + }), 20_000) +}) diff --git a/repos/effect/packages/effect/test/Chunk.test.ts b/repos/effect/packages/effect/test/Chunk.test.ts new file mode 100644 index 0000000..55d8de4 --- /dev/null +++ b/repos/effect/packages/effect/test/Chunk.test.ts @@ -0,0 +1,890 @@ +import { describe, it } from "@effect/vitest" +import { + assertEquals, + assertFalse, + assertNone, + assertSome, + assertTrue, + deepStrictEqual, + doesNotThrow, + strictEqual, + throws +} from "@effect/vitest/utils" +import { + Array as Arr, + Chunk, + Either, + Equal, + FastCheck as fc, + identity, + Number as Num, + Option, + Order, + pipe, + type Predicate +} from "effect" + +const assertTuple = ( + actual: [Chunk.Chunk, Chunk.Chunk], + expected: [Chunk.Chunk, Chunk.Chunk] +) => { + assertEquals(actual[0], expected[0]) + assertEquals(actual[1], expected[1]) +} + +describe("Chunk", () => { + it("Equal.equals", () => { + assertTrue(Equal.equals(Chunk.make(0), Chunk.make(0))) + assertTrue(Equal.equals(Chunk.make(1, 2, 3), Chunk.make(1, 2, 3))) + assertFalse(Equal.equals(Chunk.make(1, 2, 3), Chunk.make(1, 2))) + assertFalse(Equal.equals(Chunk.make(1, 2), Chunk.make(1, 2, 3))) + assertFalse(Equal.equals(Chunk.make(1, 2, 3), Chunk.make(1, "a", 3))) + assertFalse(Equal.equals(Chunk.make(0), [0])) + }) + + it("toString", () => { + strictEqual( + String(Chunk.make(0, 1, 2)), + `{ + "_id": "Chunk", + "values": [ + 0, + 1, + 2 + ] +}` + ) + strictEqual( + String(Chunk.make(Chunk.make(1, 2, 3))), + `{ + "_id": "Chunk", + "values": [ + { + "_id": "Chunk", + "values": [ + 1, + 2, + 3 + ] + } + ] +}` + ) + }) + + it("toJSON", () => { + deepStrictEqual(Chunk.make(0, 1, 2).toJSON(), { _id: "Chunk", values: [0, 1, 2] }) + deepStrictEqual(Chunk.make(Chunk.make(1, 2, 3)).toJSON(), { + _id: "Chunk", + values: [{ _id: "Chunk", values: [1, 2, 3] }] + }) + }) + + it("inspect", () => { + if (typeof window === "undefined") { + // eslint-disable-next-line @typescript-eslint/no-require-imports + const { inspect } = require("node:util") + assertEquals(inspect(Chunk.make(0, 1, 2)), inspect({ _id: "Chunk", values: [0, 1, 2] })) + } + }) + + it("modifyOption", () => { + assertNone(pipe(Chunk.empty(), Chunk.modifyOption(0, (n: number) => n * 2))) + assertSome( + pipe(Chunk.make(1, 2, 3), Chunk.modifyOption(0, (n: number) => n * 2)), + Chunk.make(2, 2, 3) + ) + }) + + it("modify", () => { + assertEquals(pipe(Chunk.empty(), Chunk.modify(0, (n: number) => n * 2)), Chunk.empty()) + assertEquals(pipe(Chunk.make(1, 2, 3), Chunk.modify(0, (n: number) => n * 2)), Chunk.make(2, 2, 3)) + }) + + it("replaceOption", () => { + assertNone(pipe(Chunk.empty(), Chunk.replaceOption(0, 2))) + assertSome(pipe(Chunk.make(1, 2, 3), Chunk.replaceOption(0, 2)), Chunk.make(2, 2, 3)) + }) + + it("replace", () => { + assertEquals(pipe(Chunk.empty(), Chunk.replace(0, 2)), Chunk.empty()) + assertEquals(pipe(Chunk.make(1, 2, 3), Chunk.replace(0, 2)), Chunk.make(2, 2, 3)) + }) + + it("remove", () => { + assertEquals(pipe(Chunk.empty(), Chunk.remove(0)), Chunk.empty()) + assertEquals(pipe(Chunk.make(1, 2, 3), Chunk.remove(0)), Chunk.make(2, 3)) + }) + + it("removeOption", () => { + assertNone(pipe(Chunk.empty(), Chunk.removeOption(0))) + assertSome(pipe(Chunk.make(1, 2, 3), Chunk.removeOption(0)), Chunk.make(2, 3)) + }) + + it("chunksOf", () => { + assertEquals(pipe(Chunk.empty(), Chunk.chunksOf(2)), Chunk.empty()) + assertEquals( + pipe(Chunk.make(1, 2, 3, 4, 5), Chunk.chunksOf(2)), + Chunk.make(Chunk.make(1, 2), Chunk.make(3, 4), Chunk.make(5)) + ) + assertEquals( + pipe(Chunk.make(1, 2, 3, 4, 5, 6), Chunk.chunksOf(2)), + Chunk.make(Chunk.make(1, 2), Chunk.make(3, 4), Chunk.make(5, 6)) + ) + assertEquals( + pipe(Chunk.make(1, 2, 3, 4, 5), Chunk.chunksOf(1)), + Chunk.make(Chunk.make(1), Chunk.make(2), Chunk.make(3), Chunk.make(4), Chunk.make(5)) + ) + assertEquals(pipe(Chunk.make(1, 2, 3, 4, 5), Chunk.chunksOf(5)), Chunk.make(Chunk.make(1, 2, 3, 4, 5))) + // out of bounds + assertEquals( + pipe(Chunk.make(1, 2, 3, 4, 5), Chunk.chunksOf(0)), + Chunk.make(Chunk.make(1), Chunk.make(2), Chunk.make(3), Chunk.make(4), Chunk.make(5)) + ) + assertEquals( + pipe(Chunk.make(1, 2, 3, 4, 5), Chunk.chunksOf(-1)), + Chunk.make(Chunk.make(1), Chunk.make(2), Chunk.make(3), Chunk.make(4), Chunk.make(5)) + ) + assertEquals(pipe(Chunk.make(1, 2, 3, 4, 5), Chunk.chunksOf(10)), Chunk.make(Chunk.make(1, 2, 3, 4, 5))) + }) + + it(".pipe() method", () => { + assertEquals(Chunk.empty().pipe(Chunk.append(1)), Chunk.make(1)) + }) + + describe("toArray", () => { + it("should return an empty array for an empty chunk", () => { + deepStrictEqual(Chunk.toArray(Chunk.empty()), []) + }) + + it("should return an array with the elements of the chunk", () => { + deepStrictEqual(Chunk.toArray(Chunk.make(1, 2, 3)), [1, 2, 3]) + }) + + it("should not affect the original chunk when the array is mutated", () => { + const chunk = Chunk.make(1, 2, 3) + const arr = Chunk.toArray(chunk) + // mutate the array + arr[1] = 4 + // the chunk should not be affected + deepStrictEqual(Chunk.toArray(chunk), [1, 2, 3]) + }) + }) + + describe("toReadonlyArray", () => { + describe("Given an empty Chunk", () => { + const chunk = Chunk.empty() + it("should give back an empty readonly array", () => { + deepStrictEqual(Chunk.toReadonlyArray(chunk), []) + }) + }) + + describe("Given a large Chunk", () => { + const len = 100_000 + let chunk = Chunk.empty() + for (let i = 0; i < len; i++) chunk = Chunk.appendAll(Chunk.of(i), chunk) + + it("gives back a readonly array", () => { + doesNotThrow(() => Chunk.toReadonlyArray(chunk)) + deepStrictEqual(Chunk.toReadonlyArray(chunk), Arr.reverse(Arr.range(0, len - 1))) + }) + }) + + describe(`Given an imbalanced left and right chunk`, () => { + const len = 1_000 + let rchunk = Chunk.empty() + let lchunk = Chunk.empty() + for (let i = 0; i < len; i++) { + rchunk = Chunk.appendAll(Chunk.of(i), rchunk) + lchunk = Chunk.appendAll(lchunk, Chunk.of(i)) + } + it("should have depth of +/- 3", () => { + assertTrue(rchunk.depth >= lchunk.depth - 3) + assertTrue(rchunk.depth <= lchunk.depth + 3) + }) + }) + }) + + describe("isChunk", () => { + describe("Given a chunk", () => { + const chunk = Chunk.make(0, 1) + it("should be true", () => { + assertTrue(Chunk.isChunk(chunk)) + }) + }) + describe("Given an object", () => { + const object = {} + it("should be false", () => { + assertFalse(Chunk.isChunk(object)) + }) + }) + }) + + describe("fromIterable", () => { + describe("Given an iterable", () => { + const myIterable = { + [Symbol.iterator]() { + let i = 0 + + return { + next() { + i++ + return { value: i, done: i > 5 } + } + } + } + } + + it("should process it", () => { + assertEquals(Chunk.fromIterable(myIterable), Chunk.unsafeFromArray([1, 2, 3, 4, 5])) + }) + }) + + it("should return the same reference if the input is a Chunk", () => { + const expected = Chunk.make(1, 2, 3) + const actual = Chunk.fromIterable(expected) + assertTrue(actual === expected) + }) + }) + + describe("get", () => { + describe("Given a Chunk and an index within the bounds", () => { + const chunk = Chunk.unsafeFromArray([1, 2, 3]) + const index = 0 + + it("should a Some with the value", () => { + deepStrictEqual(pipe(chunk, Chunk.get(index)), Option.some(1)) + }) + }) + + describe("Given a Chunk and an index out of bounds", () => { + const chunk = Chunk.unsafeFromArray([1, 2, 3]) + + it("should return a None", () => { + assertNone(pipe(chunk, Chunk.get(4))) + }) + }) + }) + + describe("unsafeGet", () => { + describe("Given an empty Chunk and an index", () => { + const chunk = Chunk.empty() + const index = 4 + + it("should throw", () => { + throws(() => pipe(chunk, Chunk.unsafeGet(index))) + }) + }) + + describe("Given an appended Chunk and an index out of bounds", () => { + const chunk = pipe(Chunk.empty(), Chunk.append(1)) + const index = 4 + + it("should throw", () => { + throws(() => pipe(chunk, Chunk.unsafeGet(index))) + }) + }) + + describe("Given an appended Chunk and an index in bounds", () => { + it("should return the value", () => { + const chunk = pipe(Chunk.make(0, 1, 2), Chunk.append(3)) + strictEqual(Chunk.unsafeGet(1)(chunk), 1) + }) + }) + + describe("Given a prepended Chunk and an index out of bounds", () => { + it("should throw", () => { + fc.assert(fc.property(fc.array(fc.anything()), (array) => { + let chunk: Chunk.Chunk = Chunk.empty() + array.forEach((e) => { + chunk = pipe(chunk, Chunk.prepend(e)) + }) + throws(() => pipe(chunk, Chunk.unsafeGet(array.length))) + })) + }) + }) + + describe("Given a prepended Chunk and an index in bounds", () => { + it("should return the value", () => { + const chunk = pipe(Chunk.make(0, 1, 2), Chunk.prepend(3)) + strictEqual(Chunk.unsafeGet(1)(chunk), 0) + }) + }) + + describe("Given a singleton Chunk and an index out of bounds", () => { + const chunk = Chunk.make(1) + const index = 4 + + it("should throw", () => { + throws(() => pipe(chunk, Chunk.unsafeGet(index))) + }) + }) + + describe("Given an array Chunk and an index out of bounds", () => { + const chunk = Chunk.unsafeFromArray([1, 2]) + const index = 4 + + it("should throw", () => { + throws(() => pipe(chunk, Chunk.unsafeGet(index))) + }) + }) + + describe("Given a concat Chunk and an index out of bounds", () => { + it("should throw", () => { + fc.assert(fc.property(fc.array(fc.anything()), fc.array(fc.anything()), (arr1, arr2) => { + const chunk: Chunk.Chunk = Chunk.appendAll(Chunk.fromIterable(arr2))(Chunk.unsafeFromArray(arr1)) + throws(() => pipe(chunk, Chunk.unsafeGet(arr1.length + arr2.length))) + })) + }) + }) + + describe("Given an appended Chunk and an index in bounds", () => { + const chunk = pipe(Chunk.empty(), Chunk.append(1), Chunk.append(2)) + const index = 1 + + it("should return the value", () => { + strictEqual(pipe(chunk, Chunk.unsafeGet(index)), 2) + }) + }) + + describe("Given a prepended Chunk and an index in bounds", () => { + const chunk = pipe(Chunk.empty(), Chunk.prepend(2), Chunk.prepend(1)) + const index = 1 + + it("should return the value", () => { + strictEqual(pipe(chunk, Chunk.unsafeGet(index)), 2) + }) + }) + + describe("Given a singleton Chunk and an index in bounds", () => { + const chunk = Chunk.make(1) + const index = 0 + + it("should return the value", () => { + strictEqual(pipe(chunk, Chunk.unsafeGet(index)), 1) + }) + }) + + describe("Given an array Chunk and an index in bounds", () => { + const chunk = pipe(Chunk.unsafeFromArray([1, 2, 3])) + const index = 1 + + it("should return the value", () => { + strictEqual(pipe(chunk, Chunk.unsafeGet(index)), 2) + }) + }) + + describe("Given a concat Chunk and an index in bounds", () => { + it("should return the value", () => { + fc.assert(fc.property(fc.array(fc.anything()), fc.array(fc.anything()), (a, b) => { + const c = [...a, ...b] + const d = Chunk.appendAll(Chunk.unsafeFromArray(b))(Chunk.unsafeFromArray(a)) + for (let i = 0; i < c.length; i++) { + deepStrictEqual(Chunk.unsafeGet(i)(d), c[i]) + } + })) + }) + }) + }) + + it("append", () => { + fc.assert( + fc.property( + fc.array(fc.integer()), + fc.array(fc.integer(), { minLength: 0, maxLength: 120, size: "xlarge" }), + (a, b) => { + let chunk = Chunk.unsafeFromArray(a) + b.forEach((e) => { + chunk = Chunk.append(e)(chunk) + }) + deepStrictEqual(Chunk.toReadonlyArray(chunk), [...a, ...b]) + } + ) + ) + }) + + it("prependAll", () => { + assertEquals(pipe(Chunk.empty(), Chunk.prependAll(Chunk.make(1))), Chunk.make(1)) + assertEquals(pipe(Chunk.make(1), Chunk.prependAll(Chunk.empty())), Chunk.make(1)) + + assertEquals(pipe(Chunk.empty(), Chunk.prependAll(Chunk.make(1, 2))), Chunk.make(1, 2)) + assertEquals(pipe(Chunk.make(1, 2), Chunk.prependAll(Chunk.empty())), Chunk.make(1, 2)) + + assertEquals(pipe(Chunk.make(2, 3), Chunk.prependAll(Chunk.make(1))), Chunk.make(1, 2, 3)) + assertEquals(pipe(Chunk.make(3), Chunk.prependAll(Chunk.make(1, 2))), Chunk.make(1, 2, 3)) + }) + + it("prepend", () => { + fc.assert( + fc.property( + fc.array(fc.integer()), + fc.array(fc.integer(), { minLength: 0, maxLength: 120, size: "xlarge" }), + (a, b) => { + let chunk = Chunk.unsafeFromArray(a) + for (let i = b.length - 1; i >= 0; i--) { + chunk = Chunk.prepend(b[i])(chunk) + } + deepStrictEqual(Chunk.toReadonlyArray(chunk), [...b, ...a]) + } + ) + ) + }) + + describe("take", () => { + describe("Given a Chunk with more elements than the amount taken", () => { + it("should return the subset", () => { + assertEquals(pipe(Chunk.unsafeFromArray([1, 2, 3]), Chunk.take(2)), Chunk.unsafeFromArray([1, 2])) + }) + }) + + describe("Given a Chunk with fewer elements than the amount taken", () => { + const chunk = Chunk.unsafeFromArray([1, 2, 3]) + const amount = 5 + + it("should return the available subset", () => { + assertEquals(pipe(chunk, Chunk.take(amount)), Chunk.unsafeFromArray([1, 2, 3])) + }) + }) + + describe("Given a slice Chunk with and an amount", () => { + const chunk = pipe(Chunk.unsafeFromArray([1, 2, 3, 4, 5]), Chunk.take(4)) + const amount = 3 + + it("should return the available subset", () => { + assertEquals(pipe(chunk, Chunk.take(amount)), Chunk.unsafeFromArray([1, 2, 3])) + }) + }) + + describe("Given a singleton Chunk with and an amount > 1", () => { + const chunk = Chunk.make(1) + const amount = 2 + + it("should return the available subset", () => { + assertEquals(pipe(chunk, Chunk.take(amount)), Chunk.unsafeFromArray([1])) + }) + }) + + describe("Given a concatenated Chunk and an amount > 1", () => { + const chunk = pipe(Chunk.of(1), Chunk.appendAll(Chunk.make(2, 3, 4))) + const amount = 2 + + it("should return the available subset", () => { + deepStrictEqual(pipe(chunk, Chunk.take(amount), Chunk.toReadonlyArray), [1, 2]) + }) + }) + + describe("Given a concatenated Chunk and an amount <= self.left", () => { + it("should return the available subset", () => { + const chunk = Chunk.appendAll(Chunk.make(2, 3, 4), Chunk.of(1)) + deepStrictEqual(pipe(chunk, Chunk.take(2), Chunk.toReadonlyArray), [2, 3]) + deepStrictEqual(pipe(chunk, Chunk.take(3), Chunk.toReadonlyArray), [2, 3, 4]) + }) + }) + }) + + describe("make", () => { + it("should return a NonEmptyChunk", () => { + strictEqual(Chunk.make(0, 1).length, 2) + }) + }) + + describe("singleton", () => { + it("should return a NonEmptyChunk", () => { + strictEqual(Chunk.of(1).length, 1) + }) + it("should return a ISingleton", () => { + strictEqual(Chunk.of(1).backing._tag, "ISingleton") + }) + }) + + describe("drop", () => { + it("should return self on 0", () => { + const self = Chunk.make(0, 1) + strictEqual(Chunk.drop(0)(self), self) + }) + it("should drop twice", () => { + const self = Chunk.make(0, 1, 2, 3) + deepStrictEqual(Chunk.toReadonlyArray(Chunk.drop(1)(Chunk.drop(1)(self))), [2, 3]) + }) + it("should handle concatenated chunks", () => { + const self = pipe(Chunk.make(1), Chunk.appendAll(Chunk.make(2, 3, 4))) + deepStrictEqual(pipe(self, Chunk.drop(2), Chunk.toReadonlyArray), [3, 4]) + }) + }) + + describe("dropRight", () => { + describe("Given a Chunk and an amount to drop below the length", () => { + const chunk = Chunk.unsafeFromArray([1, 2, 3]) + const toDrop = 1 + + it("should remove the given amount of items", () => { + assertEquals(pipe(chunk, Chunk.dropRight(toDrop)), Chunk.unsafeFromArray([1, 2])) + }) + }) + + describe("Given a Chunk and an amount to drop above the length", () => { + const chunk = Chunk.unsafeFromArray([1, 2]) + const toDrop = 3 + + it("should return an empty chunk", () => { + assertEquals(pipe(chunk, Chunk.dropRight(toDrop)), Chunk.unsafeFromArray([])) + }) + }) + }) + + describe("dropWhile", () => { + describe("Given a Chunk and a criteria that applies to part of the chunk", () => { + const chunk = Chunk.unsafeFromArray([1, 2, 3]) + const criteria = (n: number) => n < 3 + + it("should return the subset that doesn't pass the criteria", () => { + assertEquals(pipe(chunk, Chunk.dropWhile(criteria)), Chunk.unsafeFromArray([3])) + }) + }) + + describe("Given a Chunk and a criteria that applies to the whole chunk", () => { + const chunk = Chunk.unsafeFromArray([1, 2, 3]) + const criteria = (n: number) => n < 4 + + it("should return an empty chunk", () => { + assertEquals(pipe(chunk, Chunk.dropWhile(criteria)), Chunk.unsafeFromArray([])) + }) + }) + }) + + describe("concat", () => { + describe("Given 2 chunks of the same length", () => { + const chunk1 = Chunk.unsafeFromArray([0, 1]) + const chunk2 = Chunk.unsafeFromArray([2, 3]) + + it("should concatenate them following order", () => { + assertEquals(pipe(chunk1, Chunk.appendAll(chunk2)), Chunk.unsafeFromArray([0, 1, 2, 3])) + }) + }) + + describe("Given 2 chunks where the first one has more elements than the second one", () => { + const chunk1 = Chunk.unsafeFromArray([1, 2]) + const chunk2 = Chunk.unsafeFromArray([3]) + + it("should concatenate them following order", () => { + assertEquals(pipe(chunk1, Chunk.appendAll(chunk2)), Chunk.unsafeFromArray([1, 2, 3])) + }) + }) + + describe("Given 2 chunks where the first one has fewer elements than the second one", () => { + const chunk1 = Chunk.unsafeFromArray([1]) + const chunk2 = Chunk.unsafeFromArray([2, 3, 4]) + + it("should concatenate them following order", () => { + assertEquals(pipe(chunk1, Chunk.appendAll(chunk2)), Chunk.unsafeFromArray([1, 2, 3, 4])) + }) + }) + + describe("Given 2 chunks where the first one is appended", () => { + const chunk1 = pipe( + Chunk.empty(), + Chunk.append(1) + ) + const chunk2 = Chunk.unsafeFromArray([2, 3, 4]) + + it("should concatenate them following order", () => { + assertEquals(pipe(chunk1, Chunk.appendAll(chunk2)), Chunk.unsafeFromArray([1, 2, 3, 4])) + }) + }) + + describe("Given 2 chunks where the second one is appended", () => { + const chunk1 = Chunk.unsafeFromArray([1]) + const chunk2 = pipe( + Chunk.empty(), + Chunk.prepend(2) + ) + + it("should concatenate them following order", () => { + assertEquals(pipe(chunk1, Chunk.appendAll(chunk2)), Chunk.unsafeFromArray([1, 2])) + }) + }) + + describe("Given 2 chunks where the first one is empty", () => { + const chunk1 = Chunk.empty() + const chunk2 = Chunk.unsafeFromArray([1, 2]) + + it("should concatenate them following order", () => { + assertEquals(pipe(chunk1, Chunk.appendAll(chunk2)), Chunk.unsafeFromArray([1, 2])) + }) + }) + + describe("Given 2 chunks where the second one is empty", () => { + const chunk1 = Chunk.unsafeFromArray([1, 2]) + const chunk2 = Chunk.empty() + + it("should concatenate them following order", () => { + assertEquals(pipe(chunk1, Chunk.appendAll(chunk2)), Chunk.unsafeFromArray([1, 2])) + }) + }) + + describe("Given several chunks concatenated with each", () => { + const chunk1 = Chunk.empty() + const chunk2 = Chunk.unsafeFromArray([1]) + const chunk3 = Chunk.unsafeFromArray([2]) + const chunk4 = Chunk.unsafeFromArray([3, 4]) + const chunk5 = Chunk.unsafeFromArray([5, 6]) + + it("should concatenate them following order", () => { + assertEquals( + pipe( + chunk1, + Chunk.appendAll(chunk2), + Chunk.appendAll(chunk3), + Chunk.appendAll(chunk4), + Chunk.appendAll(chunk5) + ), + Chunk.unsafeFromArray([1, 2, 3, 4, 5, 6]) + ) + }) + }) + }) + + it("zip", () => { + assertEquals(Chunk.zip(Chunk.empty(), Chunk.empty()), Chunk.empty()) + assertEquals(Chunk.zip(Chunk.make(1), Chunk.empty()), Chunk.empty()) + assertEquals(Chunk.zip(Chunk.empty(), Chunk.make(1)), Chunk.empty()) + deepStrictEqual(Chunk.toArray(Chunk.zip(Chunk.make(1), Chunk.make(2))), [[1, 2]]) + }) + + describe("Given two non-materialized chunks of different sizes", () => { + it("should zip the chunks together and drop the leftover", () => { + // Create two non-materialized Chunks + const left = pipe(Chunk.make(-1, 0, 1), Chunk.drop(1)) + const right = pipe(Chunk.make(1, 0, 0, 1), Chunk.drop(1)) + const zipped = pipe(left, Chunk.zipWith(pipe(right, Chunk.take(left.length)), (a, b) => [a, b])) + deepStrictEqual(Array.from(zipped), [[0, 0], [1, 0]]) + }) + }) + + it("last", () => { + assertNone(Chunk.last(Chunk.empty())) + assertSome(Chunk.last(Chunk.make(1, 2, 3)), 3) + }) + + it("map", () => { + assertEquals(Chunk.map(Chunk.empty(), (n) => n + 1), Chunk.empty()) + assertEquals(Chunk.map(Chunk.of(1), (n) => n + 1), Chunk.of(2)) + assertEquals(Chunk.map(Chunk.make(1, 2, 3), (n) => n + 1), Chunk.make(2, 3, 4)) + assertEquals(Chunk.map(Chunk.make(1, 2, 3), (n, i) => n + i), Chunk.make(1, 3, 5)) + }) + + it("mapAccum", () => { + deepStrictEqual(Chunk.mapAccum(Chunk.make(1, 2, 3), "-", (s, a) => [s + a, a + 1]), ["-123", Chunk.make(2, 3, 4)]) + }) + + it("partition", () => { + assertTuple(Chunk.partition(Chunk.empty(), (n) => n > 2), [Chunk.empty(), Chunk.empty()]) + assertTuple(Chunk.partition(Chunk.make(1, 3), (n) => n > 2), [Chunk.make(1), Chunk.make(3)]) + + assertTuple(Chunk.partition(Chunk.empty(), (n, i) => n + i > 2), [Chunk.empty(), Chunk.empty()]) + assertTuple(Chunk.partition(Chunk.make(1, 2), (n, i) => n + i > 2), [Chunk.make(1), Chunk.make(2)]) + }) + + it("partitionMap", () => { + assertTuple(Chunk.partitionMap(Chunk.empty(), identity), [Chunk.empty(), Chunk.empty()]) + assertTuple(Chunk.partitionMap(Chunk.make(Either.right(1), Either.left("a"), Either.right(2)), identity), [ + Chunk.make("a"), + Chunk.make(1, 2) + ]) + }) + + it("separate", () => { + assertTuple(Chunk.separate(Chunk.empty()), [Chunk.empty(), Chunk.empty()]) + assertTuple(Chunk.separate(Chunk.make(Either.right(1), Either.left("e"), Either.right(2))), [ + Chunk.make("e"), + Chunk.make(1, 2) + ]) + }) + + it("size", () => { + strictEqual(Chunk.size(Chunk.empty()), 0) + strictEqual(Chunk.size(Chunk.make(1, 2, 3)), 3) + }) + + it("split", () => { + assertEquals(pipe(Chunk.empty(), Chunk.split(2)), Chunk.empty()) + assertEquals(pipe(Chunk.make(1), Chunk.split(2)), Chunk.make(Chunk.make(1))) + assertEquals(pipe(Chunk.make(1, 2), Chunk.split(2)), Chunk.make(Chunk.make(1), Chunk.make(2))) + assertEquals(pipe(Chunk.make(1, 2, 3, 4, 5), Chunk.split(2)), Chunk.make(Chunk.make(1, 2, 3), Chunk.make(4, 5))) + assertEquals( + pipe(Chunk.make(1, 2, 3, 4, 5), Chunk.split(3)), + Chunk.make(Chunk.make(1, 2), Chunk.make(3, 4), Chunk.make(5)) + ) + }) + + it("tail", () => { + assertNone(Chunk.tail(Chunk.empty())) + // TODO: use assertSome? + assertEquals(Chunk.tail(Chunk.make(1, 2, 3)), Option.some(Chunk.make(2, 3))) + }) + + it("filter", () => { + assertEquals(Chunk.filter(Chunk.make(1, 2, 3), (n) => n % 2 === 1), Chunk.make(1, 3)) + assertEquals( + Chunk.filter(Chunk.make(Option.some(3), Option.some(2), Option.some(1)), Option.isSome), + Chunk.make(Option.some(3), Option.some(2), Option.some(1)) as any + ) + assertEquals( + Chunk.filter(Chunk.make(Option.some(3), Option.none(), Option.some(1)), Option.isSome), + Chunk.make(Option.some(3), Option.some(1)) as any + ) + }) + + it("filterMapWhile", () => { + assertEquals( + Chunk.filterMapWhile(Chunk.make(1, 3, 4, 5), (n) => n % 2 === 1 ? Option.some(n) : Option.none()), + Chunk.make(1, 3) + ) + }) + + it("compact", () => { + assertEquals(Chunk.compact(Chunk.empty()), Chunk.empty()) + assertEquals(Chunk.compact(Chunk.make(Option.some(1), Option.some(2), Option.some(3))), Chunk.make(1, 2, 3)) + assertEquals(Chunk.compact(Chunk.make(Option.some(1), Option.none(), Option.some(3))), Chunk.make(1, 3)) + }) + + it("dedupeAdjacent", () => { + assertEquals(Chunk.dedupeAdjacent(Chunk.empty()), Chunk.empty()) + assertEquals(Chunk.dedupeAdjacent(Chunk.make(1, 2, 3)), Chunk.make(1, 2, 3)) + assertEquals(Chunk.dedupeAdjacent(Chunk.make(1, 2, 2, 3, 3)), Chunk.make(1, 2, 3)) + }) + + it("flatMap", () => { + assertEquals(Chunk.flatMap(Chunk.make(1), (n) => Chunk.make(n, n + 1)), Chunk.make(1, 2)) + assertEquals(Chunk.flatMap(Chunk.make(1, 2, 3), (n) => Chunk.make(n, n + 1)), Chunk.make(1, 2, 2, 3, 3, 4)) + }) + + it("union", () => { + assertEquals(Chunk.union(Chunk.make(1, 2, 3), Chunk.empty()), Chunk.make(1, 2, 3)) + assertEquals(Chunk.union(Chunk.empty(), Chunk.make(1, 2, 3)), Chunk.make(1, 2, 3)) + assertEquals(Chunk.union(Chunk.make(1, 2, 3), Chunk.make(2, 3, 4)), Chunk.make(1, 2, 3, 4)) + }) + + it("intersection", () => { + assertEquals(Chunk.intersection(Chunk.make(1, 2, 3), Chunk.empty()), Chunk.empty()) + assertEquals(Chunk.intersection(Chunk.empty(), Chunk.make(2, 3, 4)), Chunk.empty()) + assertEquals(Chunk.intersection(Chunk.make(1, 2, 3), Chunk.make(2, 3, 4)), Chunk.make(2, 3)) + }) + + it("isEmpty", () => { + assertTrue(Chunk.isEmpty(Chunk.empty())) + assertFalse(Chunk.isEmpty(Chunk.make(1))) + }) + + it("unsafeLast", () => { + strictEqual(Chunk.unsafeLast(Chunk.make(1)), 1) + strictEqual(Chunk.unsafeLast(Chunk.make(1, 2, 3)), 3) + throws(() => Chunk.unsafeLast(Chunk.empty()), new Error("Index out of bounds")) + }) + + it("splitNonEmptyAt", () => { + assertTuple(Chunk.splitNonEmptyAt(Chunk.make(1, 2, 3, 4), 2), [Chunk.make(1, 2), Chunk.make(3, 4)]) + assertTuple(Chunk.splitNonEmptyAt(Chunk.make(1, 2, 3, 4), 10), [Chunk.make(1, 2, 3, 4), Chunk.empty()]) + }) + + it("splitWhere", () => { + assertTuple(Chunk.splitWhere(Chunk.empty(), (n) => n > 1), [Chunk.empty(), Chunk.empty()]) + assertTuple(Chunk.splitWhere(Chunk.make(1, 2, 3), (n) => n > 1), [Chunk.make(1), Chunk.make(2, 3)]) + }) + + it("takeWhile", () => { + assertEquals(Chunk.takeWhile(Chunk.empty(), (n) => n <= 2), Chunk.empty()) + assertEquals(Chunk.takeWhile(Chunk.make(1, 2, 3), (n) => n <= 2), Chunk.make(1, 2)) + }) + + it("dedupe", () => { + assertEquals(Chunk.dedupe(Chunk.empty()), Chunk.empty()) + assertEquals(Chunk.dedupe(Chunk.make(1, 2, 3)), Chunk.make(1, 2, 3)) + assertEquals(Chunk.dedupe(Chunk.make(1, 2, 3, 2, 1, 3)), Chunk.make(1, 2, 3)) + }) + + it("unzip", () => { + assertTuple(Chunk.unzip(Chunk.empty()), [Chunk.empty(), Chunk.empty()]) + assertTuple(Chunk.unzip(Chunk.make(["a", 1] as const, ["b", 2] as const)), [ + Chunk.make("a", "b"), + Chunk.make(1, 2) + ]) + }) + + it("reverse", () => { + assertEquals(Chunk.reverse(Chunk.empty()), Chunk.empty()) + assertEquals(Chunk.reverse(Chunk.make(1, 2, 3)), Chunk.make(3, 2, 1)) + assertEquals(Chunk.reverse(Chunk.take(Chunk.make(1, 2, 3, 4), 3)), Chunk.make(3, 2, 1)) + }) + + it("flatten", () => { + assertEquals(Chunk.flatten(Chunk.make(Chunk.make(1), Chunk.make(2), Chunk.make(3))), Chunk.make(1, 2, 3)) + }) + + it("makeBy", () => { + assertEquals(Chunk.makeBy(5, (n) => n * 2), Chunk.make(0, 2, 4, 6, 8)) + assertEquals(Chunk.makeBy(2.2, (n) => n * 2), Chunk.make(0, 2)) + }) + + it("range", () => { + assertEquals(Chunk.range(0, 0), Chunk.make(0)) + assertEquals(Chunk.range(0, 1), Chunk.make(0, 1)) + assertEquals(Chunk.range(1, 5), Chunk.make(1, 2, 3, 4, 5)) + assertEquals(Chunk.range(10, 15), Chunk.make(10, 11, 12, 13, 14, 15)) + assertEquals(Chunk.range(-1, 0), Chunk.make(-1, 0)) + assertEquals(Chunk.range(-5, -1), Chunk.make(-5, -4, -3, -2, -1)) + // out of bound + assertEquals(Chunk.range(2, 1), Chunk.make(2)) + assertEquals(Chunk.range(-1, -2), Chunk.make(-1)) + }) + + it("some", () => { + const isPositive: Predicate.Predicate = (n) => n > 0 + assertTrue(Chunk.some(Chunk.make(-1, -2, 3), isPositive)) + assertFalse(Chunk.some(Chunk.make(-1, -2, -3), isPositive)) + }) + + it("forEach", () => { + const as: Array = [] + Chunk.forEach(Chunk.make(1, 2, 3, 4), (n, i) => as.push(`${n}-${i}`)) + deepStrictEqual(as, ["1-0", "2-1", "3-2", "4-3"]) + }) + + it("sortWith", () => { + type X = { + a: string + b: number + } + const chunk: Chunk.Chunk = Chunk.make({ a: "a", b: 2 }, { a: "b", b: 1 }) + deepStrictEqual(Chunk.sortWith(chunk, (x) => x.b, Order.number), Chunk.make({ a: "b", b: 1 }, { a: "a", b: 2 })) + }) + + it("getEquivalence", () => { + const equivalence = Chunk.getEquivalence(Num.Equivalence) + assertTrue(equivalence(Chunk.empty(), Chunk.empty())) + assertTrue(equivalence(Chunk.make(1, 2, 3), Chunk.make(1, 2, 3))) + assertFalse(equivalence(Chunk.make(1, 2, 3), Chunk.make(1, 2))) + assertFalse(equivalence(Chunk.make(1, 2, 3), Chunk.make(1, 2, 4))) + }) + + it("differenceWith", () => { + const eq = (a: E, b: E) => a.id === b.id + const differenceWith = pipe(eq, Chunk.differenceWith) + + const chunk = Chunk.make({ id: 1 }, { id: 2 }, { id: 3 }) + + deepStrictEqual(differenceWith(Chunk.make({ id: 1 }, { id: 2 }), chunk), Chunk.make({ id: 3 })) + assertEquals(differenceWith(Chunk.empty(), chunk), chunk) + assertEquals(differenceWith(chunk, Chunk.empty()), Chunk.empty()) + assertEquals(differenceWith(chunk, chunk), Chunk.empty()) + }) + + it("difference", () => { + const curr = Chunk.make(1, 3, 5, 7, 9) + + assertEquals(Chunk.difference(Chunk.make(1, 2, 3, 4, 5), curr), Chunk.make(7, 9)) + assertEquals(Chunk.difference(Chunk.empty(), curr), curr) + assertEquals(Chunk.difference(curr, Chunk.empty()), Chunk.empty()) + assertEquals(Chunk.difference(curr, curr), Chunk.empty()) + }) +}) diff --git a/repos/effect/packages/effect/test/Config.test.ts b/repos/effect/packages/effect/test/Config.test.ts new file mode 100644 index 0000000..5c2d9b3 --- /dev/null +++ b/repos/effect/packages/effect/test/Config.test.ts @@ -0,0 +1,721 @@ +import { describe, it } from "@effect/vitest" +import { assertFailure, assertSuccess, assertTrue, deepStrictEqual, strictEqual } from "@effect/vitest/utils" +import { + Brand, + Cause, + Chunk, + Config, + ConfigError, + ConfigProvider, + Duration, + Effect, + Equal, + HashSet, + LogLevel, + Option, + pipe, + Redacted, + Secret +} from "effect" + +type Str = Brand.Branded +const Str = Brand.refined( + (n) => n.length > 2, + (n) => Brand.error(`Brand: Expected ${n} to be longer than 2`) +) + +const assertConfigError = ( + config: Config.Config, + map: ReadonlyArray, + error: ConfigError.ConfigError +) => { + const configProvider = ConfigProvider.fromMap(new Map(map)) + const result = Effect.runSyncExit(configProvider.load(config)) + assertFailure(result, Cause.fail(error)) +} + +const assertConfig = ( + config: Config.Config, + map: ReadonlyArray, + a: A +) => { + const configProvider = ConfigProvider.fromMap(new Map(map)) + const result = Effect.runSyncExit(configProvider.load(config)) + assertSuccess(result, a) +} + +describe("Config", () => { + describe("boolean", () => { + it("name = undefined", () => { + const config = Config.array(Config.boolean(), "ITEMS") + assertConfig(config, [["ITEMS", "true"]], [true]) + assertConfigError( + config, + [["ITEMS", "value"]], + ConfigError.InvalidData(["ITEMS"], `Expected a boolean value but received "value"`) + ) + }) + + it("name != undefined", () => { + const config = Config.boolean("BOOL") + assertConfig(config, [["BOOL", "true"]], true) + assertConfig(config, [["BOOL", "yes"]], true) + assertConfig(config, [["BOOL", "on"]], true) + assertConfig(config, [["BOOL", "1"]], true) + assertConfig(config, [["BOOL", "false"]], false) + assertConfig(config, [["BOOL", "no"]], false) + assertConfig(config, [["BOOL", "off"]], false) + assertConfig(config, [["BOOL", "0"]], false) + + assertConfigError(config, [], ConfigError.MissingData(["BOOL"], "Expected BOOL to exist in the provided map")) + assertConfigError( + config, + [["BOOL", "value"]], + ConfigError.InvalidData(["BOOL"], `Expected a boolean value but received "value"`) + ) + }) + }) + + describe("url", () => { + it("name != undefined", () => { + const config = Config.url("WEBSITE_URL") + assertConfig( + config, + [["WEBSITE_URL", "https://effect.website/docs/introduction#what-is-effect"]], + new URL("https://effect.website/docs/introduction#what-is-effect") + ) + assertConfigError( + config, + [["WEBSITE_URL", "abra-kadabra"]], + ConfigError.InvalidData(["WEBSITE_URL"], `Expected an URL value but received "abra-kadabra"`) + ) + assertConfigError( + config, + [], + ConfigError.MissingData(["WEBSITE_URL"], "Expected WEBSITE_URL to exist in the provided map") + ) + }) + }) + + describe("port", () => { + it("name != undefined", () => { + const config = Config.port("WEBSITE_PORT") + + assertConfig( + config, + [["WEBSITE_PORT", "123"]], + 123 + ) + assertConfigError( + config, + [["WEBSITE_PORT", "abra-kadabra"]], + ConfigError.InvalidData(["WEBSITE_PORT"], `Expected a network port value but received "abra-kadabra"`) + ) + assertConfigError( + config, + [], + ConfigError.MissingData(["WEBSITE_PORT"], "Expected WEBSITE_PORT to exist in the provided map") + ) + }) + }) + + describe("branded", () => { + it("name != undefined", () => { + const config = Config.branded(Config.string("STR"), Str) + + assertConfig( + config, + [["STR", "123"]], + Str("123") + ) + assertConfigError( + config, + [["STR", "1"]], + ConfigError.InvalidData(["STR"], "Brand: Expected 1 to be longer than 2") + ) + assertConfigError( + config, + [], + ConfigError.MissingData(["STR"], "Expected STR to exist in the provided map") + ) + }) + it("name != undefined from name", () => { + const config = Config.branded("STR", Str) + + assertConfig( + config, + [["STR", "123"]], + Str("123") + ) + assertConfigError( + config, + [["STR", "1"]], + ConfigError.InvalidData(["STR"], "Brand: Expected 1 to be longer than 2") + ) + assertConfigError( + config, + [], + ConfigError.MissingData(["STR"], "Expected STR to exist in the provided map") + ) + }) + }) + + describe("nonEmptyString", () => { + it("name = undefined", () => { + const config = Config.array(Config.nonEmptyString(), "ITEMS") + assertConfig(config, [["ITEMS", "foo"]], ["foo"]) + assertConfigError(config, [["ITEMS", ""]], ConfigError.MissingData(["ITEMS"], "Expected a non-empty string")) + }) + + it("name != undefined", () => { + const config = Config.nonEmptyString("NON_EMPTY_STRING") + assertConfig(config, [["NON_EMPTY_STRING", "foo"]], "foo") + assertConfig(config, [["NON_EMPTY_STRING", " "]], " ") + assertConfigError( + config, + [["NON_EMPTY_STRING", ""]], + ConfigError.MissingData(["NON_EMPTY_STRING"], "Expected a non-empty string") + ) + }) + }) + + describe("number", () => { + it("name = undefined", () => { + const config = Config.array(Config.number(), "ITEMS") + assertConfig(config, [["ITEMS", "1"]], [1]) + assertConfigError( + config, + [["ITEMS", "123qq"]], + ConfigError.InvalidData(["ITEMS"], `Expected a number value but received "123qq"`) + ) + assertConfigError( + config, + [["ITEMS", "value"]], + ConfigError.InvalidData(["ITEMS"], `Expected a number value but received "value"`) + ) + }) + + it("name != undefined", () => { + const config = Config.number("NUMBER") + assertConfig(config, [["NUMBER", "1"]], 1) + assertConfig(config, [["NUMBER", "1.2"]], 1.2) + assertConfig(config, [["NUMBER", "-1"]], -1) + assertConfig(config, [["NUMBER", "-1.2"]], -1.2) + assertConfig(config, [["NUMBER", "0"]], 0) + assertConfig(config, [["NUMBER", "-0"]], -0) + + assertConfigError(config, [], ConfigError.MissingData(["NUMBER"], "Expected NUMBER to exist in the provided map")) + assertConfigError( + config, + [["NUMBER", "value"]], + ConfigError.InvalidData(["NUMBER"], `Expected a number value but received "value"`) + ) + }) + }) + + describe("literal", () => { + it("name = undefined", () => { + const config = Config.array(Config.literal("a", "b")(), "ITEMS") + assertConfig(config, [["ITEMS", "a"]], ["a"]) + assertConfigError( + config, + [["ITEMS", "value"]], + ConfigError.InvalidData(["ITEMS"], `Expected one of (a, b) but received "value"`) + ) + }) + + it("name != undefined", () => { + const config = Config.literal("a", 0, -0.3, BigInt(5), false, null)("LITERAL") + assertConfig(config, [["LITERAL", "a"]], "a") + assertConfig(config, [["LITERAL", "0"]], 0) + assertConfig(config, [["LITERAL", "-0.3"]], -0.3) + assertConfig(config, [["LITERAL", "5"]], BigInt(5)) + assertConfig(config, [["LITERAL", "false"]], false) + assertConfig(config, [["LITERAL", "null"]], null) + + assertConfigError( + config, + [], + ConfigError.MissingData(["LITERAL"], "Expected LITERAL to exist in the provided map") + ) + assertConfigError( + config, + [["LITERAL", "value"]], + ConfigError.InvalidData(["LITERAL"], `Expected one of (a, 0, -0.3, 5, false, null) but received "value"`) + ) + }) + }) + + describe("date", () => { + it("name = undefined", () => { + const config = Config.date() + assertConfig(config, [["", "0"]], new Date(Date.parse("0"))) + assertConfigError( + config, + [["", "value"]], + ConfigError.InvalidData([], `Expected a Date value but received "value"`) + ) + }) + + it("name != undefined", () => { + const config = Config.date("DATE") + assertConfig(config, [["DATE", "0"]], new Date(Date.parse("0"))) + + assertConfigError(config, [], ConfigError.MissingData(["DATE"], "Expected DATE to exist in the provided map")) + assertConfigError( + config, + [["DATE", "value"]], + ConfigError.InvalidData(["DATE"], `Expected a Date value but received "value"`) + ) + }) + }) + + it("fail", () => { + const config = Config.fail("failure message") + assertConfigError(config, [], ConfigError.MissingData([], "failure message")) + }) + + it("mapAttempt", () => { + const config = Config.string("STRING").pipe(Config.mapAttempt((s) => { + const n = parseFloat(s) + if (Number.isNaN(n)) { + throw new Error("invalid number") + } + if (n < 0) { + throw "invalid negative number" + } + return n + })) + assertConfig(config, [["STRING", "1"]], 1) + assertConfigError( + config, + [["STRING", "value"]], + ConfigError.InvalidData(["STRING"], "invalid number") + ) + assertConfigError( + config, + [["STRING", "-1"]], + ConfigError.InvalidData(["STRING"], "invalid negative number") + ) + assertConfigError(config, [], ConfigError.MissingData(["STRING"], "Expected STRING to exist in the provided map")) + }) + + describe("logLevel", () => { + it("name = undefined", () => { + const config = Config.logLevel() + assertConfig(config, [["", "DEBUG"]], LogLevel.Debug) + + assertConfigError(config, [["", "-"]], ConfigError.InvalidData([], `Expected a log level but received "-"`)) + }) + + it("name != undefined", () => { + const config = Config.logLevel("LOG_LEVEL") + assertConfig(config, [["LOG_LEVEL", "DEBUG"]], LogLevel.Debug) + + assertConfigError( + config, + [["LOG_LEVEL", "-"]], + ConfigError.InvalidData(["LOG_LEVEL"], `Expected a log level but received "-"`) + ) + }) + }) + + describe("duration", () => { + it("name = undefined", () => { + const config = Config.duration() + assertConfig(config, [["", "10 seconds"]], Duration.decode("10 seconds")) + + assertConfigError(config, [["", "-"]], ConfigError.InvalidData([], `Expected a duration but received "-"`)) + }) + + it("name != undefined", () => { + const config = Config.duration("DURATION") + assertConfig(config, [["DURATION", "10 seconds"]], Duration.decode("10 seconds")) + + assertConfigError( + config, + [["DURATION", "-"]], + ConfigError.InvalidData(["DURATION"], `Expected a duration but received "-"`) + ) + }) + }) + + describe("validate", () => { + it("should preserve the original path", () => { + const flat = Config.number("NUMBER").pipe( + Config.validate({ + message: "a positive number", + validation: (n) => n >= 0 + }) + ) + assertConfig(flat, [["NUMBER", "1"]], 1) + assertConfig(flat, [["NUMBER", "1.2"]], 1.2) + assertConfigError( + flat, + [["NUMBER", "-1"]], + ConfigError.InvalidData(["NUMBER"], "a positive number") + ) + + const nested = flat.pipe( + Config.nested("NESTED1") + ) + assertConfig(nested, [["NESTED1.NUMBER", "1"]], 1) + assertConfig(nested, [["NESTED1.NUMBER", "1.2"]], 1.2) + assertConfigError( + nested, + [["NESTED1.NUMBER", "-1"]], + ConfigError.InvalidData(["NESTED1", "NUMBER"], "a positive number") + ) + + const doubleNested = nested.pipe(Config.nested("NESTED2")) + assertConfig(doubleNested, [["NESTED2.NESTED1.NUMBER", "1"]], 1) + assertConfig(doubleNested, [["NESTED2.NESTED1.NUMBER", "1.2"]], 1.2) + assertConfigError( + doubleNested, + [["NESTED2.NESTED1.NUMBER", "-1"]], + ConfigError.InvalidData(["NESTED2", "NESTED1", "NUMBER"], "a positive number") + ) + }) + }) + + describe("withDefault", () => { + it("recovers from missing data error", () => { + const config = pipe( + Config.integer("key"), + Config.withDefault(0) + ) + // available data + assertConfig(config, [["key", "1"]], 1) + // missing data + assertConfig(config, [], 0) + }) + + it("does not recover from other errors", () => { + const config = pipe( + Config.integer("key"), + Config.withDefault(0) + ) + assertConfig(config, [["key", "1"]], 1) + assertConfigError( + config, + [["key", "1.2"]], + // available data but not an integer + ConfigError.InvalidData(["key"], `Expected an integer value but received "1.2"`) + ) + assertConfigError( + config, + [["key", "value"]], + // available data but not an integer + ConfigError.InvalidData(["key"], `Expected an integer value but received "value"`) + ) + }) + + it("does not recover from missing data and other error", () => { + const config = pipe( + Config.integer("key1"), + Config.zip(Config.integer("key2")), + Config.withDefault([0, 0]) + ) + assertConfig(config, [], [0, 0]) + assertConfig(config, [["key1", "1"], ["key2", "2"]], [1, 2]) + assertConfigError( + config, + [["key2", "value"]], + ConfigError.And( + ConfigError.MissingData(["key1"], "Expected key1 to exist in the provided map"), + ConfigError.InvalidData(["key2"], `Expected an integer value but received "value"`) + ) + ) + }) + + it("does not recover from missing data or other error", () => { + const config = pipe( + Config.integer("key1"), + Config.orElse(() => Config.integer("key2")), + Config.withDefault(0) + ) + assertConfig(config, [], 0) + assertConfig(config, [["key1", "1"]], 1) + assertConfig(config, [["key2", "2"]], 2) + assertConfigError( + config, + [["key2", "value"]], + ConfigError.Or( + ConfigError.MissingData(["key1"], "Expected key1 to exist in the provided map"), + ConfigError.InvalidData(["key2"], `Expected an integer value but received "value"`) + ) + ) + }) + }) + + describe("option", () => { + it("recovers from missing data error", () => { + const config = Config.option(Config.integer("key")) + assertConfig(config, [], Option.none()) + assertConfig(config, [["key", "1"]], Option.some(1)) + }) + + it("does not recover from other errors", () => { + const config = Config.option(Config.integer("key")) + assertConfigError( + config, + [["key", "value"]], + ConfigError.InvalidData(["key"], `Expected an integer value but received "value"`) + ) + }) + + it("does not recover from other errors", () => { + const config = pipe( + Config.integer("key1"), + Config.zip(Config.integer("key2")), + Config.option + ) + assertConfig(config, [["key1", "1"], ["key2", "2"]], Option.some([1, 2])) + assertConfigError( + config, + [["key1", "value"]], + ConfigError.And( + ConfigError.InvalidData(["key1"], `Expected an integer value but received "value"`), + ConfigError.MissingData(["key2"], "Expected key2 to exist in the provided map") + ) + ) + assertConfigError( + config, + [["key2", "value"]], + ConfigError.And( + ConfigError.MissingData(["key1"], "Expected key1 to exist in the provided map"), + ConfigError.InvalidData(["key2"], `Expected an integer value but received "value"`) + ) + ) + }) + + it("does not recover from other errors", () => { + const config = pipe( + Config.integer("key1"), + Config.orElse(() => Config.integer("key2")), + Config.option + ) + assertConfig(config, [["key1", "1"]], Option.some(1)) + assertConfig(config, [["key1", "value"], ["key2", "2"]], Option.some(2)) + assertConfigError( + config, + [["key2", "value"]], + ConfigError.Or( + ConfigError.MissingData(["key1"], "Expected key1 to exist in the provided map"), + ConfigError.InvalidData(["key2"], `Expected an integer value but received "value"`) + ) + ) + }) + }) + + describe("Wrap", () => { + it("unwrap correctly builds config", () => { + const wrapper = ( + _: Config.Config.Wrap<{ + key1: number + list: ReadonlyArray + option: Option.Option + secret: Redacted.Redacted + nested?: + | Partial<{ + key2: string + }> + | undefined + }> + ) => Config.unwrap(_) + + const config = wrapper({ + key1: Config.integer("key1"), + list: Config.array(Config.integer(), "items"), + option: Config.option(Config.integer("option")), + secret: Config.redacted("secret"), + nested: { + key2: Config.string("key2") + } + }) + assertConfig(config, [["key1", "123"], ["items", "1,2,3"], ["option", "123"], ["secret", "sauce"], [ + "key2", + "value" + ]], { + key1: 123, + list: [1, 2, 3], + option: Option.some(123), + secret: Redacted.make("sauce"), + nested: { + key2: "value" + } + }) + assertConfigError( + config, + [["key1", "123"], ["items", "1,value,3"], ["option", "123"], ["secret", "sauce"], ["key2", "value"]], + ConfigError.InvalidData(["items"], `Expected an integer value but received "value"`) + ) + }) + }) + + it("sync", () => { + const config = Config.sync(() => 1) + assertConfig(config, [], 1) + }) + + describe("all", () => { + describe("tuple", () => { + it("length = 0", () => { + const config = Config.all([]) + assertConfig(config, [], []) + }) + + it("length = 1", () => { + const config = Config.all([Config.number("NUMBER")]) + assertConfig(config, [["NUMBER", "1"]], [1]) + }) + + it("length > 1", () => { + const config = Config.all([Config.number("NUMBER"), Config.boolean("BOOL")]) + assertConfig(config, [["NUMBER", "1"], ["BOOL", "true"]], [1, true]) + assertConfigError( + config, + [["NUMBER", "value"], ["BOOL", "true"]], + ConfigError.InvalidData(["NUMBER"], `Expected a number value but received "value"`) + ) + assertConfigError( + config, + [["NUMBER", "1"], ["BOOL", "value"]], + ConfigError.InvalidData(["BOOL"], `Expected a boolean value but received "value"`) + ) + }) + }) + + it("iterable", () => { + const set = new Set([Config.number("NUMBER"), Config.boolean("BOOL")]) + const config = Config.all(set) + assertConfig(config, [["NUMBER", "1"], ["BOOL", "true"]], [1, true]) + assertConfigError( + config, + [["NUMBER", "value"], ["BOOL", "true"]], + ConfigError.InvalidData(["NUMBER"], `Expected a number value but received "value"`) + ) + assertConfigError( + config, + [["NUMBER", "1"], ["BOOL", "value"]], + ConfigError.InvalidData(["BOOL"], `Expected a boolean value but received "value"`) + ) + }) + }) + + describe("Config.redacted", () => { + it("name = undefined", () => { + const config = Config.array(Config.redacted(), "ITEMS") + assertConfig(config, [["ITEMS", "a"]], [Redacted.make("a")]) + }) + + it("name != undefined", () => { + const config = Config.redacted("SECRET") + assertConfig(config, [["SECRET", "a"]], Redacted.make("a")) + }) + + it("can wrap generic Config", () => { + const config = Config.redacted(Config.integer("NUM")) + assertConfig(config, [["NUM", "2"]], Redacted.make(2)) + }) + }) + + describe("Secret", () => { + describe("Config.secret", () => { + it("name = undefined", () => { + const config = Config.array(Config.secret(), "ITEMS") + assertConfig(config, [["ITEMS", "a"]], [Secret.fromString("a")]) + }) + + it("name != undefined", () => { + const config = Config.secret("SECRET") + assertConfig(config, [["SECRET", "a"]], Secret.fromString("a")) + }) + }) + + it("chunk constructor", () => { + const secret = Secret.fromIterable(Chunk.fromIterable("secret".split(""))) + assertTrue(Equal.equals(secret, Secret.fromString("secret"))) + }) + + it("value", () => { + const secret = Secret.fromIterable(Chunk.fromIterable("secret".split(""))) + const value = Secret.value(secret) + strictEqual(value, "secret") + }) + + it("toString", () => { + const secret = Secret.fromString("secret") + strictEqual(`${secret}`, "Secret()") + }) + + it("toJSON", () => { + const secret = Secret.fromString("secret") + strictEqual(JSON.stringify(secret), "\"\"") + }) + + it("wipe", () => { + const secret = Secret.fromString("secret") + Secret.unsafeWipe(secret) + assertTrue( + Equal.equals( + Secret.value(secret), + Array.from({ length: "secret".length }, () => String.fromCharCode(0)).join("") + ) + ) + }) + }) + + it("withDescription", () => { + const config = Config.number("NUMBER").pipe(Config.withDescription("my description")) + assertTrue("description" in config) + }) + + describe("hashSet", () => { + it("name = undefined", () => { + const config = Config.array(Config.hashSet(Config.string()), "ITEMS") + assertConfig(config, [["ITEMS", "a,b,c"]], [HashSet.make("a", "b", "c")]) + }) + + it("name != undefined", () => { + const config = Config.hashSet(Config.string(), "HASH_SET") + assertConfig(config, [["HASH_SET", "a,b,c"]], HashSet.make("a", "b", "c")) + }) + }) + + it("can be yielded", () => { + const result = Effect.runSync(Effect.withConfigProvider( + Config.string("STRING"), + ConfigProvider.fromMap(new Map([["STRING", "value"]])) + )) + strictEqual(result, "value") + }) + + it("array nested", () => { + const result = Config.array(Config.number(), "ARRAY").pipe( + Effect.withConfigProvider( + ConfigProvider.fromMap(new Map([["NESTED.ARRAY", "1,2,3"]])).pipe( + ConfigProvider.nested("NESTED") + ) + ), + Effect.runSync + ) + deepStrictEqual(result, [1, 2, 3]) + }) + + it("ConfigError message", () => { + const missingData = ConfigError.MissingData(["PATH"], "missing PATH") + const invalidData = ConfigError.InvalidData(["PATH1"], "invalid PATH1") + const andError = ConfigError.And(missingData, invalidData) + const orError = ConfigError.Or(missingData, invalidData) + + strictEqual( + andError.message, + "(Missing data at PATH: \"missing PATH\") and (Invalid data at PATH1: \"invalid PATH1\")" + ) + strictEqual( + orError.message, + "(Missing data at PATH: \"missing PATH\") or (Invalid data at PATH1: \"invalid PATH1\")" + ) + }) +}) diff --git a/repos/effect/packages/effect/test/ConfigProvider.test.ts b/repos/effect/packages/effect/test/ConfigProvider.test.ts new file mode 100644 index 0000000..5bf2c8b --- /dev/null +++ b/repos/effect/packages/effect/test/ConfigProvider.test.ts @@ -0,0 +1,943 @@ +import { describe, it } from "@effect/vitest" +import { assertNone, assertTrue, deepStrictEqual, strictEqual } from "@effect/vitest/utils" +import { + Cause, + Chunk, + Config, + ConfigError, + ConfigProvider, + Effect, + Either, + Equal, + Exit, + HashMap, + HashSet, + LogLevel, + Option, + Secret +} from "effect" + +interface HostPort { + readonly host: string + readonly port: number +} + +const hostPortConfig: Config.Config = Config.all({ + host: Config.string("host"), + port: Config.integer("port") +}) + +interface HostPorts { + readonly hostPorts: ReadonlyArray +} + +const hostPortsConfig: Config.Config = Config.all({ + hostPorts: Config.array(hostPortConfig, "hostPorts") +}) + +interface ServiceConfig { + readonly hostPort: HostPort + readonly timeout: number +} + +const serviceConfigConfig: Config.Config = Config.all({ + hostPort: hostPortConfig.pipe(Config.nested("hostPort")), + timeout: Config.integer("timeout") +}) + +interface StockDay { + readonly date: Date + readonly open: number + readonly close: number + readonly low: number + readonly high: number + readonly volume: number +} + +const stockDayConfig: Config.Config = Config.all({ + date: Config.date("date"), + open: Config.number("open"), + close: Config.number("close"), + low: Config.number("low"), + high: Config.number("high"), + volume: Config.integer("volume") +}) + +interface SNP500 { + readonly stockDays: HashMap.HashMap +} + +const snp500Config: Config.Config = Config.all({ + stockDays: Config.hashMap(stockDayConfig) +}) + +interface WebScrapingTargets { + readonly targets: HashSet.HashSet +} + +const webScrapingTargetsConfig: Config.Config = Config.all({ + targets: Config.hashSet(Config.string(), "targets") +}) + +const webScrapingTargetsConfigWithDefault = Config.all({ + targets: Config.chunk(Config.string()).pipe( + Config.withDefault(Chunk.make("https://effect.website2", "https://github.com/Effect-TS2")) + ) +}) + +const provider = (map: Map): ConfigProvider.ConfigProvider => { + return ConfigProvider.fromMap(map) +} + +describe("ConfigProvider", () => { + it.effect("flat atoms", () => + Effect.gen(function*() { + const map = new Map([["host", "localhost"], ["port", "8080"]]) + const result = yield* provider(map).load(hostPortConfig) + deepStrictEqual(result, { + host: "localhost", + port: 8080 + }) + })) + + it.effect("nested atoms", () => + Effect.gen(function*() { + const map = new Map([ + ["hostPort.host", "localhost"], + ["hostPort.port", "8080"], + ["timeout", "1000"] + ]) + const result = yield* provider(map).load(serviceConfigConfig) + deepStrictEqual(result, { + hostPort: { + host: "localhost", + port: 8080 + }, + timeout: 1000 + }) + })) + + it.effect("top-level list with same number of elements per key", () => + Effect.gen(function*() { + const map = new Map([ + ["hostPorts.host", "localhost,localhost,localhost"], + ["hostPorts.port", "8080,8080,8080"] + ]) + const result = yield* provider(map).load(hostPortsConfig) + deepStrictEqual(result, { + hostPorts: Array.from({ length: 3 }, () => ({ host: "localhost", port: 8080 })) + }) + })) + + it.effect("top-level missing list", () => + Effect.gen(function*() { + const map = new Map() + const result = yield* Effect.exit(provider(map).load(hostPortsConfig)) + assertTrue(Exit.isFailure(result)) + })) + + it.effect("simple map", () => + Effect.gen(function*() { + const map = new Map([ + ["name", "Sherlock Holmes"], + ["address", "221B Baker Street"] + ]) + const result = yield* provider(map).load(Config.hashMap(Config.string())) + deepStrictEqual( + result, + HashMap.make( + ["name", "Sherlock Holmes"], + ["address", "221B Baker Street"] + ) + ) + })) + + it.effect("top-level lists with multi-character sequence delimiters", () => + Effect.gen(function*() { + const map = new Map([ + ["hostPorts.host", "localhost///localhost///localhost"], + ["hostPorts.port", "8080///8080///8080"] + ]) + const result = yield* ConfigProvider.fromMap(map, { seqDelim: "///" }).load(hostPortsConfig) + deepStrictEqual(result, { + hostPorts: Array.from({ length: 3 }, () => ({ host: "localhost", port: 8080 })) + }) + })) + + it.effect("top-level lists with special regex multi-character sequence delimiter", () => + Effect.gen(function*() { + const map = new Map([ + ["hostPorts.host", "localhost|||localhost|||localhost"], + ["hostPorts.port", "8080|||8080|||8080"] + ]) + const result = yield* ConfigProvider.fromMap(map, { seqDelim: "|||" }).load(hostPortsConfig) + deepStrictEqual(result, { + hostPorts: Array.from({ length: 3 }, () => ({ host: "localhost", port: 8080 })) + }) + })) + + it.effect("top-level lists with special regex character sequence delimiter", () => + Effect.gen(function*() { + const map = new Map([ + ["hostPorts.host", "localhost*localhost*localhost"], + ["hostPorts.port", "8080*8080*8080"] + ]) + const result = yield* ConfigProvider.fromMap(map, { seqDelim: "*" }).load(hostPortsConfig) + deepStrictEqual(result, { + hostPorts: Array.from({ length: 3 }, () => ({ host: "localhost", port: 8080 })) + }) + })) + + it.effect("top-level list with different number of elements per key fails", () => + Effect.gen(function*() { + const map = new Map([ + ["hostPorts.host", "localhost"], + ["hostPorts.port", "8080,8080,8080"] + ]) + const result = yield* Effect.exit(provider(map).load(hostPortsConfig)) + deepStrictEqual( + result, + Exit.fail( + ConfigError.MissingData( + ["hostPorts"], + "The element at index 1 in a sequence at path \"hostPorts\" was missing" + ) + ) + ) + })) + + it.effect("flat atoms of different types", () => + Effect.gen(function*() { + const map = new Map([ + ["date", "2022-10-28"], + ["open", "98.8"], + ["close", "150.0"], + ["low", "98.0"], + ["high", "151.5"], + ["volume", "100091990"] + ]) + const result = yield* provider(map).load(stockDayConfig) + deepStrictEqual(result, { + date: new Date("2022-10-28"), + open: 98.8, + close: 150.0, + low: 98.0, + high: 151.5, + volume: 100091990 + }) + })) + + it.effect("tables", () => + Effect.gen(function*() { + const map = new Map([ + ["Effect.date", "2022-10-28"], + ["Effect.open", "98.8"], + ["Effect.close", "150.0"], + ["Effect.low", "98.0"], + ["Effect.high", "151.5"], + ["Effect.volume", "100091990"] + ]) + const result = yield* provider(map).load(snp500Config) + deepStrictEqual(result, { + stockDays: HashMap.make([ + "Effect", + { + date: new Date("2022-10-28"), + open: 98.8, + close: 150.0, + low: 98.0, + high: 151.5, + volume: 100091990 + } + ]) + }) + })) + + it.effect("empty tables", () => + Effect.gen(function*() { + const result = yield* provider(new Map()).load(snp500Config) + deepStrictEqual(result, { stockDays: HashMap.empty() }) + })) + + it.effect("collection of atoms", () => + Effect.gen(function*() { + const map = new Map([ + ["targets", "https://effect.website,https://github.com/Effect-TS"] + ]) + const result = yield* provider(map).load(webScrapingTargetsConfig) + deepStrictEqual(result, { + targets: HashSet.make("https://effect.website", "https://github.com/Effect-TS") + }) + })) + + it.effect("collection of atoms falls back to default", () => + Effect.gen(function*() { + const map = new Map() + const result = yield* provider(map).load(webScrapingTargetsConfigWithDefault) + deepStrictEqual(result, { + targets: Chunk.make("https://effect.website2", "https://github.com/Effect-TS2") + }) + })) + + it.effect("indexed - simple", () => + Effect.gen(function*() { + const config = Config.array(Config.integer(), "id") + const map = new Map([ + ["id[0]", "1"], + ["id[1]", "2"], + ["id[2]", "3"] + ]) + const result = yield* ConfigProvider.fromMap(map).load(config) + deepStrictEqual(result, [1, 2, 3]) + })) + + it.effect("indexed sequence - simple with list values", () => + Effect.gen(function*() { + const config = Config.array(Config.array(Config.integer()), "id") + const map = new Map([ + ["id[0]", "1, 2"], + ["id[1]", "3, 4"], + ["id[2]", "5, 6"] + ]) + const result = yield* ConfigProvider.fromMap(map).load(config) + deepStrictEqual(result, [[1, 2], [3, 4], [5, 6]]) + })) + + it.effect("indexed sequence - one product type", () => + Effect.gen(function*() { + const config = Config.array( + Config.all({ + age: Config.integer("age"), + id: Config.integer("id") + }), + "employees" + ) + const map = new Map([ + ["employees[0].age", "1"], + ["employees[0].id", "1"] + ]) + const result = yield* ConfigProvider.fromMap(map).load(config) + deepStrictEqual(result, [{ age: 1, id: 1 }]) + })) + + it.effect("indexed sequence - multiple product types", () => + Effect.gen(function*() { + const config = Config.array( + Config.all({ + age: Config.integer("age"), + id: Config.integer("id") + }), + "employees" + ) + const map = new Map([ + ["employees[0].age", "1"], + ["employees[0].id", "2"], + ["employees[1].age", "3"], + ["employees[1].id", "4"] + ]) + const result = yield* ConfigProvider.fromMap(map).load(config) + deepStrictEqual(result, [{ age: 1, id: 2 }, { age: 3, id: 4 }]) + })) + + it.effect("indexed sequence - multiple product types with missing fields", () => + Effect.gen(function*() { + const config = Config.array( + Config.all({ + age: Config.integer("age"), + id: Config.integer("id") + }), + "employees" + ) + const map = new Map([ + ["employees[0].age", "1"], + ["employees[0].id", "2"], + ["employees[1].age", "3"], + ["employees[1]", "4"] + ]) + const result = yield* Effect.exit(ConfigProvider.fromMap(map).load(config)) + assertTrue( + Exit.isFailure(result) && + Cause.isFailType(result.effect_instruction_i0) && + ConfigError.isMissingData(result.effect_instruction_i0.error) && + // TODO: fix error message to not include `.[index]` + result.effect_instruction_i0.error.message === "Expected employees.[1].id to exist in the provided map" && + Equal.equals( + Chunk.unsafeFromArray(result.effect_instruction_i0.error.path), + Chunk.make("employees", "[1]", "id") + ) + ) + })) + + it.effect("indexed sequence - multiple product types with optional fields", () => + Effect.gen(function*() { + const config = Config.array( + Config.all({ + age: Config.option(Config.integer("age")), + id: Config.integer("id") + }), + "employees" + ) + const map = new Map([ + ["employees[0].age", "1"], + ["employees[0].id", "2"], + ["employees[1].id", "4"] + ]) + const result = yield* ConfigProvider.fromMap(map).load(config) + deepStrictEqual(result, [{ age: Option.some(1), id: 2 }, { age: Option.none(), id: 4 }]) + })) + + it.effect("indexed sequence - multiple product types with sequence fields", () => + Effect.gen(function*() { + const config = Config.array( + Config.all({ + refunds: Config.array(Config.integer(), "refunds"), + id: Config.integer("id") + }), + "employees" + ) + const map = new Map([ + ["employees[0].refunds", "1,2,3"], + ["employees[0].id", "0"], + ["employees[1].id", "1"], + ["employees[1].refunds", "4,5,6"] + ]) + const result = yield* ConfigProvider.fromMap(map).load(config) + deepStrictEqual(result, [{ refunds: [1, 2, 3], id: 0 }, { refunds: [4, 5, 6], id: 1 }]) + })) + + it.effect("indexed sequence - product type of indexed sequences with reusable config", () => + Effect.gen(function*() { + const idAndAge = Config.all({ + id: Config.integer("id"), + age: Config.integer("age") + }) + const config = Config.all({ + employees: Config.array(idAndAge, "employees"), + students: Config.array(idAndAge, "students") + }) + const map = new Map([ + ["employees[0].id", "0"], + ["employees[1].id", "1"], + ["employees[0].age", "10"], + ["employees[1].age", "11"], + ["students[0].id", "20"], + ["students[1].id", "30"], + ["students[0].age", "2"], + ["students[1].age", "3"] + ]) + const result = yield* ConfigProvider.fromMap(map).load(config) + deepStrictEqual(result, { + employees: [{ id: 0, age: 10 }, { id: 1, age: 11 }], + students: [{ id: 20, age: 2 }, { id: 30, age: 3 }] + }) + })) + + it.effect("indexed sequence - map of indexed sequences", () => + Effect.gen(function*() { + const employee = Config.all({ + age: Config.integer("age"), + id: Config.integer("id") + }) + const config = Config.hashMap(Config.array(employee, "employees"), "departments") + const map = new Map([ + ["departments.department1.employees[0].age", "10"], + ["departments.department1.employees[0].id", "0"], + ["departments.department1.employees[1].age", "20"], + ["departments.department1.employees[1].id", "1"], + ["departments.department2.employees[0].age", "10"], + ["departments.department2.employees[0].id", "0"], + ["departments.department2.employees[1].age", "20"], + ["departments.department2.employees[1].id", "1"] + ]) + const result = yield* ConfigProvider.fromMap(map).load(config) + const expectedEmployees = [{ age: 10, id: 0 }, { age: 20, id: 1 }] + deepStrictEqual(Array.from(result), [ + ["department1", expectedEmployees], + ["department2", expectedEmployees] + ]) + })) + + it.effect("indexed sequence - map", () => + Effect.gen(function*() { + const employee = Config.hashMap(Config.integer(), "details") + const config = Config.array(employee, "employees") + const map = new Map([ + ["employees[0].details.age", "10"], + ["employees[0].details.id", "0"], + ["employees[1].details.age", "20"], + ["employees[1].details.id", "1"] + ]) + const result = yield* ConfigProvider.fromMap(map).load(config) + deepStrictEqual(result.map((table) => Array.from(table)), [ + [["age", 10], ["id", 0]], + [["age", 20], ["id", 1]] + ]) + })) + + it.effect("indexed sequence - indexed sequences", () => + Effect.gen(function*() { + const employee = Config.all({ + age: Config.integer("age"), + id: Config.integer("id") + }) + const department = Config.array(employee, "employees") + const config = Config.array(department, "departments") + const map = new Map([ + ["departments[0].employees[0].age", "10"], + ["departments[0].employees[0].id", "0"], + ["departments[0].employees[1].age", "20"], + ["departments[0].employees[1].id", "1"], + ["departments[1].employees[0].age", "10"], + ["departments[1].employees[0].id", "0"], + ["departments[1].employees[1].age", "20"], + ["departments[1].employees[1].id", "1"] + ]) + const result = yield* ConfigProvider.fromMap(map).load(config) + const expectedEmployees = [{ age: 10, id: 0 }, { age: 20, id: 1 }] + deepStrictEqual(result, [expectedEmployees, expectedEmployees]) + })) + + it.effect("indexed sequence - multiple product types nested", () => + Effect.gen(function*() { + const employee = Config.all({ + age: Config.integer("age"), + id: Config.integer("id") + }) + const config = Config.array(employee, "employees") + const map = new Map([ + ["parent.child.employees[0].age", "1"], + ["parent.child.employees[0].id", "2"], + ["parent.child.employees[1].age", "3"], + ["parent.child.employees[1].id", "4"] + ]) + const provider = ConfigProvider.fromMap(map).pipe( + ConfigProvider.nested("child"), + ConfigProvider.nested("parent") + ) + const result = yield* provider.load(config) + deepStrictEqual(result, [{ age: 1, id: 2 }, { age: 3, id: 4 }]) + })) + + it.effect("indexed sequence - multiple product types unnested", () => + Effect.gen(function*() { + const employee = Config.all({ + age: Config.integer("age"), + id: Config.integer("id") + }) + const config = Config.array(employee, "employees").pipe( + Config.nested("child"), + Config.nested("parent") + ) + const map = new Map([ + ["employees[0].age", "1"], + ["employees[0].id", "2"], + ["employees[1].age", "3"], + ["employees[1].id", "4"] + ]) + const provider = ConfigProvider.fromMap(map).pipe( + ConfigProvider.unnested("parent"), + ConfigProvider.unnested("child") + ) + const result = yield* provider.load(config) + deepStrictEqual(result, [{ age: 1, id: 2 }, { age: 3, id: 4 }]) + })) + + it.effect("logLevel", () => + Effect.gen(function*() { + const config = Config.logLevel("level") + const map = new Map([["level", "ERROR"]]) + const result = yield* ConfigProvider.fromMap(map).load(config) + strictEqual(result, LogLevel.Error) + })) + + it.effect("accessing a non-existent key fails", () => + Effect.gen(function*() { + const map = new Map([ + ["k1.k3", "v"] + ]) + const config = Config.string("k2").pipe( + Config.nested("k1") + ) + const result = yield* Effect.exit(provider(map).load(config)) + deepStrictEqual( + result, + Exit.fail( + ConfigError.MissingData( + ["k1", "k2"], + "Expected k1.k2 to exist in the provided map" + ) + ) + ) + })) + + it.effect("values are not split unless a sequence is expected", () => + Effect.gen(function*() { + const configProvider = ConfigProvider.fromMap(new Map([["greeting", "Hello, World!"]])) + const result = yield* configProvider.load(Config.string("greeting")) + strictEqual(result, "Hello, World!") + })) + + it.effect("constantCase", () => + Effect.gen(function*() { + const configProvider = ConfigProvider.fromMap(new Map([["CONSTANT_CASE", "value"]])).pipe( + ConfigProvider.constantCase + ) + const result = yield* configProvider.load(Config.string("constant.case")) + strictEqual(result, "value") + })) + + it.effect("mapInputPath", () => + Effect.gen(function*() { + const configProvider = ConfigProvider.fromMap(new Map([["KEY", "VALUE"]])).pipe( + ConfigProvider.mapInputPath((path) => path.toUpperCase()) + ) + const result = yield* configProvider.load(Config.string("key")) + strictEqual(result, "VALUE") + })) + + it.effect("kebabCase", () => + Effect.gen(function*() { + const configProvider = ConfigProvider.fromMap(new Map([["kebab-case", "value"]])).pipe( + ConfigProvider.kebabCase + ) + const result = yield* configProvider.load(Config.string("kebabCase")) + strictEqual(result, "value") + })) + + it.effect("lowerCase", () => + Effect.gen(function*() { + const configProvider = ConfigProvider.fromMap(new Map([["lowercase", "value"]])).pipe( + ConfigProvider.lowerCase + ) + const result = yield* configProvider.load(Config.string("lowerCase")) + strictEqual(result, "value") + })) + + it.effect("nested", () => + Effect.gen(function*() { + const configProvider1 = ConfigProvider.fromMap(new Map([["nested.key", "value"]])) + const config1 = Config.string("key").pipe(Config.nested("nested")) + const configProvider2 = ConfigProvider.fromMap(new Map([["nested.key", "value"]])).pipe( + ConfigProvider.nested("nested") + ) + const config2 = Config.string("key") + const result1 = yield* configProvider1.load(config1) + const result2 = yield* configProvider2.load(config2) + strictEqual(result1, "value") + strictEqual(result2, "value") + })) + + it.effect("nested - multiple layers of nesting", () => + Effect.gen(function*() { + const configProvider1 = ConfigProvider.fromMap(new Map([["parent.child.key", "value"]])) + const config1 = Config.string("key").pipe( + Config.nested("child"), + Config.nested("parent") + ) + const configProvider2 = ConfigProvider.fromMap(new Map([["parent.child.key", "value"]])).pipe( + ConfigProvider.nested("child"), + ConfigProvider.nested("parent") + ) + const config2 = Config.string("key") + const result1 = yield* configProvider1.load(config1) + const result2 = yield* configProvider2.load(config2) + strictEqual(result1, "value") + strictEqual(result2, "value") + })) + + it.effect("orElse - with flat data", () => + Effect.gen(function*() { + const configProvider = ConfigProvider.fromMap( + new Map([ + ["key1", "value1"], + ["key4", "value41"] + ]) + ).pipe( + ConfigProvider.orElse(() => + ConfigProvider.fromMap( + new Map([ + ["key2", "value2"], + ["key4", "value42"] + ]) + ) + ) + ) + const result1 = yield* configProvider.load(Config.string("key1")) + const result2 = yield* configProvider.load(Config.string("key2")) + const result31 = yield* configProvider.load(Config.option(Config.string("key3"))) + const result32 = yield* Effect.either(configProvider.load(Config.string("key3"))) + const result4 = yield* configProvider.load(Config.string("key4")) + + strictEqual(result1, "value1") + strictEqual(result2, "value2") + assertNone(result31) + deepStrictEqual( + result32, + Either.left(ConfigError.Or( + ConfigError.MissingData(["key3"], "Expected key3 to exist in the provided map"), + ConfigError.MissingData(["key3"], "Expected key3 to exist in the provided map") + )) + ) + strictEqual(result4, "value41") + })) + + it.effect("orElse - with indexed sequences", () => + Effect.gen(function*() { + const configProvider = ConfigProvider.fromMap( + new Map([ + ["parent1.child.employees[0].age", "1"], + ["parent1.child.employees[0].id", "2"], + ["parent1.child.employees[1].age", "3"], + ["parent1.child.employees[1].id", "4"] + ]) + ).pipe( + ConfigProvider.orElse(() => + ConfigProvider.fromMap( + new Map([ + ["parent1.child.employees[2].age", "5"], + ["parent1.child.employees[2].id", "6"], + ["parent2.child.employees[0].age", "11"], + ["parent2.child.employees[0].id", "21"], + ["parent2.child.employees[1].age", "31"], + ["parent2.child.employees[1].id", "41"] + ]) + ) + ) + ) + + const product = Config.zip(Config.integer("age"), Config.integer("id")) + const arrayConfig = Config.array(product, "employees") + const config1 = arrayConfig.pipe(Config.nested("child"), Config.nested("parent1")) + const config2 = arrayConfig.pipe(Config.nested("child"), Config.nested("parent2")) + + const result1 = yield* configProvider.load(config1) + const result2 = yield* configProvider.load(config2) + + deepStrictEqual(result1, [[1, 2], [3, 4], [5, 6]]) + deepStrictEqual(result2, [[11, 21], [31, 41]]) + })) + + it.effect("orElse - with indexed sequences and each provider unnested", () => + Effect.gen(function*() { + const configProvider = ConfigProvider.fromMap( + new Map([ + ["employees[0].age", "1"], + ["employees[0].id", "2"], + ["employees[1].age", "3"], + ["employees[1].id", "4"] + ]) + ).pipe( + ConfigProvider.unnested("parent1"), + ConfigProvider.unnested("child"), + ConfigProvider.orElse(() => + ConfigProvider.fromMap( + new Map([ + ["employees[0].age", "11"], + ["employees[0].id", "21"], + ["employees[1].age", "31"], + ["employees[1].id", "41"] + ]) + ).pipe( + ConfigProvider.unnested("parent2"), + ConfigProvider.unnested("child") + ) + ) + ) + + const product = Config.zip(Config.integer("age"), Config.integer("id")) + const arrayConfig = Config.array(product, "employees") + const config1 = arrayConfig.pipe(Config.nested("child"), Config.nested("parent1")) + const config2 = arrayConfig.pipe(Config.nested("child"), Config.nested("parent2")) + const config3 = arrayConfig.pipe(Config.nested("child"), Config.nested("parent3")) + + const result1 = yield* configProvider.load(config1) + const result2 = yield* configProvider.load(config2) + const result3 = yield* Effect.either(configProvider.load(config3)) + + deepStrictEqual(result1, [[1, 2], [3, 4]]) + deepStrictEqual(result2, [[11, 21], [31, 41]]) + deepStrictEqual( + result3, + Either.left(ConfigError.And( + ConfigError.MissingData( + ["parent3", "child", "employees"], + "Expected parent1 to be in path in ConfigProvider#unnested" + ), + ConfigError.MissingData( + ["parent3", "child", "employees"], + "Expected parent2 to be in path in ConfigProvider#unnested" + ) + )) + ) + })) + + it.effect("orElse - with index sequences and combined provider unnested", () => + Effect.gen(function*() { + const configProvider = ConfigProvider.fromMap( + new Map([ + ["employees[0].age", "1"], + ["employees[0].id", "2"] + ]) + ).pipe( + ConfigProvider.orElse(() => + ConfigProvider.fromMap( + new Map([ + ["employees[1].age", "3"], + ["employees[1].id", "4"] + ]) + ) + ), + ConfigProvider.unnested("parent1"), + ConfigProvider.unnested("child") + ) + + const product = Config.zip(Config.integer("age"), Config.integer("id")) + const arrayConfig = Config.array(product, "employees") + const config = arrayConfig.pipe(Config.nested("child"), Config.nested("parent1")) + + const result = yield* configProvider.load(config) + + deepStrictEqual(result, [[1, 2], [3, 4]]) + })) + + it.effect("secret", () => + Effect.gen(function*() { + const value = "Hello, World!" + const configProvider = ConfigProvider.fromMap(new Map([["greeting", value]])) + const result = yield* configProvider.load(Config.secret("greeting")) + deepStrictEqual(result, Secret.make(value.split("").map((c) => c.charCodeAt(0)))) + })) + + it.effect("snakeCase", () => + Effect.gen(function*() { + const configProvider = ConfigProvider.fromMap(new Map([["snake_case", "value"]])).pipe( + ConfigProvider.snakeCase + ) + const result = yield* configProvider.load(Config.string("snakeCase")) + strictEqual(result, "value") + })) + + it.effect("unnested", () => + Effect.gen(function*() { + const configProvider1 = ConfigProvider.fromMap(new Map([["key", "value"]])) + const config1 = Config.string("key") + const configProvider2 = ConfigProvider.fromMap(new Map([["key", "value"]])).pipe( + ConfigProvider.unnested("nested") + ) + const config2 = Config.string("key").pipe(Config.nested("nested")) + const result1 = yield* configProvider1.load(config1) + const result2 = yield* configProvider2.load(config2) + strictEqual(result1, "value") + strictEqual(result2, "value") + })) + + it.effect("unnested - multiple layers of nesting", () => + Effect.gen(function*() { + const configProvider1 = ConfigProvider.fromMap(new Map([["key", "value"]])) + const config1 = Config.string("key") + const configProvider2 = ConfigProvider.fromMap(new Map([["key", "value"]])).pipe( + ConfigProvider.unnested("parent"), + ConfigProvider.unnested("child") + ) + const config2 = Config.string("key").pipe( + Config.nested("child"), + Config.nested("parent") + ) + const result1 = yield* configProvider1.load(config1) + const result2 = yield* configProvider2.load(config2) + strictEqual(result1, "value") + strictEqual(result2, "value") + })) + + it.effect("unnested - failure", () => + Effect.gen(function*() { + const configProvider = ConfigProvider.fromMap(new Map([["key", "value"]])).pipe( + ConfigProvider.unnested("nested") + ) + const config = Config.string("key") + const result = yield* Effect.exit(configProvider.load(config)) + const error = ConfigError.MissingData( + ["key"], + "Expected nested to be in path in ConfigProvider#unnested" + ) + deepStrictEqual(result, Exit.fail(error)) + })) + + it.effect("upperCase", () => + Effect.gen(function*() { + const configProvider = ConfigProvider.fromMap(new Map([["UPPERCASE", "value"]])).pipe( + ConfigProvider.upperCase + ) + const result = yield* configProvider.load(Config.string("upperCase")) + strictEqual(result, "value") + })) + + it.effect("within", () => + Effect.gen(function*() { + const configProvider = ConfigProvider.fromMap(new Map([["nesting1.key1", "value1"], ["nesting2.KEY2", "value2"]])) + .pipe( + ConfigProvider.within(["nesting2"], ConfigProvider.mapInputPath((s) => s.toUpperCase())) + ) + const config = Config.string("key1").pipe( + Config.nested("nesting1"), + Config.zip( + Config.string("key2").pipe( + Config.nested("nesting2") + ) + ) + ) + const result = yield* configProvider.load(config) + deepStrictEqual(result, ["value1", "value2"]) + })) + + it.effect("within - multiple layers of nesting", () => + Effect.gen(function*() { + const configProvider = ConfigProvider.fromMap( + new Map([["nesting1.key1", "value1"], ["nesting2.nesting3.KEY2", "value2"]]) + ).pipe( + ConfigProvider.within(["nesting2", "nesting3"], ConfigProvider.mapInputPath((s) => s.toUpperCase())) + ) + const config = Config.string("key1").pipe( + Config.nested("nesting1"), + Config.zip( + Config.string("key2").pipe( + Config.nested("nesting3"), + Config.nested("nesting2") + ) + ) + ) + const result = yield* configProvider.load(config) + deepStrictEqual(result, ["value1", "value2"]) + })) + + it.effect("fromJson - should load configs from flat JSON", () => + Effect.gen(function*() { + const result = yield* ConfigProvider.fromJson({ + host: "localhost", + port: 8080 + }).load(hostPortConfig) + deepStrictEqual(result, { + host: "localhost", + port: 8080 + }) + })) + + it.effect("fromJson - should load configs from nested JSON", () => + Effect.gen(function*() { + const result = yield* ConfigProvider.fromJson({ + hostPorts: [{ + host: "localhost", + port: 8080 + }, { + host: "localhost", + port: 8080 + }, { + host: "localhost", + port: 8080 + }] + }).load(hostPortsConfig) + deepStrictEqual(result, { + hostPorts: Array.from({ length: 3 }, () => ({ host: "localhost", port: 8080 })) + }) + })) +}) diff --git a/repos/effect/packages/effect/test/Context.test.ts b/repos/effect/packages/effect/test/Context.test.ts new file mode 100644 index 0000000..860c54f --- /dev/null +++ b/repos/effect/packages/effect/test/Context.test.ts @@ -0,0 +1,281 @@ +import { describe, it } from "@effect/vitest" +import { + assertFalse, + assertInclude, + assertInstanceOf, + assertMatch, + assertNone, + assertSome, + assertTrue, + deepStrictEqual, + strictEqual, + throws +} from "@effect/vitest/utils" +import { Context, Differ, Option, pipe } from "effect" + +interface A { + a: number +} +const A = Context.GenericTag("A") + +interface B { + b: number +} +const B = Context.GenericTag("B") + +interface C { + c: number +} +const C = Context.GenericTag("C") + +class D extends Context.Tag("D")() {} + +class E extends Context.Reference()("E", { + defaultValue: () => ({ e: 0 }) +}) {} + +describe("Context", () => { + it("error messages", () => { + throws(() => { + Context.unsafeGet(Context.empty(), A) + }, (e) => { + assertInstanceOf(e, Error) + assertInclude(e.message, "Service not found: A") + }) + throws(() => { + Context.get(Context.empty(), A as never) + }, (e) => { + assertInstanceOf(e, Error) + assertInclude(e.message, "Service not found: A") + }) + throws(() => { + Context.unsafeGet(Context.empty(), C) + }, (e) => { + assertInstanceOf(e, Error) + assertInclude(e.message, "Service not found: C") + }) + throws(() => { + Context.get(Context.empty(), C as never) + }, (e) => { + assertInstanceOf(e, Error) + assertInclude(e.message, "Service not found: C") + }) + if (typeof window === "undefined") { + throws( + () => { + Context.get(Context.empty(), C as never) + }, + (e) => { + assertInstanceOf(e, Error) + assertMatch(e.message, /Service not found: C \(defined at (.*)Context.test.ts:29:19\)/) + } + ) + throws( + () => { + Context.get(Context.empty(), D as never) + }, + (e) => { + assertInstanceOf(e, Error) + assertMatch(e.message, /Service not found: D \(defined at (.*)Context.test.ts:31:32\)/) + } + ) + } + }) + + it("Tag.toJson()", () => { + const json: any = A.toJSON() + strictEqual(json["_id"], "Tag") + strictEqual(json["key"], "A") + strictEqual(typeof json["stack"], "string") + }) + + it("TagClass.toJson()", () => { + const json: any = D.toJSON() + strictEqual(json["_id"], "Tag") + strictEqual(json["key"], "D") + strictEqual(typeof json["stack"], "string") + }) + + it("Context.toJson()", () => { + const json: any = Context.empty().toJSON() + strictEqual(json["_id"], "Context") + deepStrictEqual(json["services"], []) + }) + + it("aliased tags", () => { + interface Foo { + readonly _tag: "Foo" + } + interface Bar { + readonly _tag: "Bar" + } + interface FooBar { + readonly FooBar: unique symbol + } + const Service = Context.GenericTag("FooBar") + const context = Context.make(Service, { _tag: "Foo" }).pipe( + Context.add(Service, { _tag: "Foo" }) + ) + deepStrictEqual(Context.get(context, Service), { _tag: "Foo" }) + }) + + it("adds and retrieve services", () => { + const Services = pipe( + Context.make(A, { a: 0 }), + Context.add(B, { b: 1 }), + Context.add(D, { d: 2 }) + ) + + deepStrictEqual(Context.get(Services, A), { a: 0 }) + assertSome(pipe(Services, Context.getOption(B)), { b: 1 }) + deepStrictEqual(pipe(Services, Context.get(D)), { d: 2 }) + assertNone(pipe(Services, Context.getOption(C))) + deepStrictEqual(pipe(Services, Context.get(E)), { e: 0 }) + + throws(() => { + pipe( + Services, + Context.unsafeGet(C) + ) + }, (e) => { + assertInstanceOf(e, Error) + assertInclude(e.message, "Service not found: C") + }) + }) + + it("picks services in env and merges", () => { + const env = pipe( + Context.empty(), + Context.add(A, { a: 0 }), + Context.merge(pipe( + Context.empty(), + Context.add(B, { b: 1 }), + Context.add(C, { c: 2 }) + )) + ) + + const pruned = pipe( + env, + Context.pick(A, B) + ) + + deepStrictEqual(pipe(pruned, Context.get(A)), { a: 0 }) + assertSome(pipe(pruned, Context.getOption(B)), { b: 1 }) + assertNone(pipe(pruned, Context.getOption(C))) + assertSome(pipe(env, Context.getOption(C)), { c: 2 }) + }) + + it("omits services from env", () => { + const env = pipe( + Context.empty(), + Context.add(A, { a: 0 }), + Context.merge(pipe( + Context.empty(), + Context.add(B, { b: 1 }), + Context.add(C, { c: 2 }) + )) + ) + + const pruned = pipe( + env, + Context.omit(A, B) + ) + + assertNone(pipe(pruned, Context.getOption(A))) + deepStrictEqual(pipe(env, Context.get(C)), { c: 2 }) + }) + + it("applies a patch to the environment", () => { + const a: A = { a: 0 } + const b: B = { b: 1 } + const c: C = { c: 2 } + const oldEnv = pipe( + Context.empty(), + Context.add(A, a), + Context.add(B, b), + Context.add(C, c) + ) as Context.Context + const newEnv = pipe( + Context.empty(), + Context.add(A, a), + Context.add(B, { b: 3 }) + ) as Context.Context + const differ = Differ.environment() + const patch = differ.diff(oldEnv, newEnv) + const result = differ.patch(patch, oldEnv) + + assertTrue(Option.isSome(Context.getOption(A)(result))) + assertTrue(Option.isSome(Context.getOption(B)(result))) + assertTrue(Option.isNone(Context.getOption(C)(result))) + strictEqual(pipe(result, Context.get(B)).b, 3) + }) + + it("creates a proper diff", () => { + const a: A = { a: 0 } + const b: B = { b: 1 } + const c: C = { c: 2 } + const oldEnv = pipe( + Context.empty(), + Context.add(A, a), + Context.add(B, b), + Context.add(C, c) + ) as Context.Context + const newEnv = pipe( + Context.empty(), + Context.add(A, a), + Context.add(B, { b: 3 }) + ) as Context.Context + const differ = Differ.environment() + const result: any = differ.diff(oldEnv, newEnv) + + strictEqual(result.first._tag, "AndThen") + strictEqual(result.first.first._tag, "Empty") + strictEqual(result.first.second._tag, "UpdateService") + strictEqual(result.first.second.key, B.key) + strictEqual(result.second._tag, "RemoveService") + strictEqual(result.second.key, C.key) + }) + + it("pipe()", () => { + const result = Context.empty().pipe(Context.add(A, { a: 0 })) + deepStrictEqual(result.pipe(Context.get(A)), { a: 0 }) + }) + + it("tag pipe", () => { + const result = A.pipe((tag) => Context.make(tag, { a: 0 })) + deepStrictEqual(result.pipe(Context.get(A)), { a: 0 }) + }) + + it("mergeAll", () => { + const env = Context.mergeAll( + Context.make(A, { a: 0 }), + Context.make(B, { b: 1 }), + Context.make(C, { c: 2 }) + ) + + const pruned = pipe( + env, + Context.pick(A, B) + ) + + deepStrictEqual(pipe(pruned, Context.get(A)), { a: 0 }) + assertSome(pipe(pruned, Context.getOption(B)), { b: 1 }) + assertNone(pipe(pruned, Context.getOption(C))) + assertSome(pipe(env, Context.getOption(C)), { c: 2 }) + }) + + it("isContext", () => { + assertTrue(Context.isContext(Context.empty())) + assertFalse(Context.isContext(null)) + }) + + it("isTag", () => { + assertTrue(Context.isTag(Context.GenericTag("Demo"))) + assertFalse(Context.isContext(null)) + }) + + it("isReference", () => { + assertTrue(Context.isTag(E)) + assertTrue(Context.isReference(E)) + }) +}) diff --git a/repos/effect/packages/effect/test/Cron.test.ts b/repos/effect/packages/effect/test/Cron.test.ts new file mode 100644 index 0000000..8448aa6 --- /dev/null +++ b/repos/effect/packages/effect/test/Cron.test.ts @@ -0,0 +1,369 @@ +import { describe, it } from "@effect/vitest" +import { assertFalse, assertTrue, deepStrictEqual, throws } from "@effect/vitest/utils" +import { Cron, DateTime, Either, Equal, Option } from "effect" + +const match = (input: Cron.Cron | string, date: DateTime.DateTime.Input) => + Cron.match(Cron.isCron(input) ? input : Cron.unsafeParse(input), date) + +const next = (input: Cron.Cron | string, after?: DateTime.DateTime.Input) => + Cron.next(Cron.isCron(input) ? input : Cron.unsafeParse(input), after) + +const prev = (input: Cron.Cron | string, after?: DateTime.DateTime.Input) => + Cron.prev(Cron.isCron(input) ? input : Cron.unsafeParse(input), after) + +describe("Cron", () => { + it("parse", () => { + // At 04:00 on every day-of-month from 8 through 14. + deepStrictEqual( + Cron.parse("0 4 8-14 * 0-6"), + Either.right(Cron.make({ + minutes: [0], + hours: [4], + days: [8, 9, 10, 11, 12, 13, 14], + months: [], + weekdays: [] + })) + ) + // At 00:00 on day-of-month 1 and 15 and on Wednesday. + deepStrictEqual( + Cron.parse("0 0 1,15 * 3"), + Either.right(Cron.make({ + minutes: [0], + hours: [0], + days: [1, 15], + months: [], + weekdays: [3] + })) + ) + // At 00:00 on day-of-month 1 and 15 and on Wednesday. + deepStrictEqual( + Cron.parse("23 0-20/2 * * *"), + Either.right(Cron.make({ + minutes: [23], + hours: [0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20], + days: [], + months: [], + weekdays: [] + })) + ) + }) + + it("unsafeParse", () => { + throws( + () => Cron.unsafeParse(""), + new Cron.ParseError({ message: "Invalid number of segments in cron expression", input: "" }) + ) + throws( + () => Cron.unsafeParse("0 0 4 8-14 * *", ""), + new Cron.ParseError({ message: "Invalid time zone in cron expression", input: "" }) + ) + }) + + it("match", () => { + assertTrue(match("5 0 * 8 *", new Date("2024-08-01 00:05:00"))) + assertFalse(match("5 0 * 8 *", new Date("2024-09-01 00:05:00"))) + assertFalse(match("5 0 * 8 *", new Date("2024-08-01 01:05:00"))) + + assertTrue(match("15 14 1 * *", new Date("2024-02-01 14:15:00"))) + assertFalse(match("15 14 1 * *", new Date("2024-02-01 15:15:00"))) + assertFalse(match("15 14 1 * *", new Date("2024-02-02 14:15:00"))) + + assertTrue(match("23 0-20/2 * * 0", new Date("2024-01-07 00:23:00"))) + assertFalse(match("23 0-20/2 * * 0", new Date("2024-01-07 03:23:00"))) + assertFalse(match("23 0-20/2 * * 0", new Date("2024-01-08 00:23:00"))) + + assertTrue(match("5 4 * * SUN", new Date("2024-01-07 04:05:00"))) + assertFalse(match("5 4 * * SUN", new Date("2024-01-08 04:05:00"))) + assertFalse(match("5 4 * * SUN", new Date("2025-01-07 04:05:00"))) + + assertTrue(match("5 4 * DEC SUN", new Date("2024-12-01 04:05:00"))) + assertFalse(match("5 4 * DEC SUN", new Date("2024-12-01 04:06:00"))) + assertFalse(match("5 4 * DEC SUN", new Date("2024-12-02 04:05:00"))) + + assertTrue(match("5 4 * * SUN", new Date("2024-01-07 04:05:00"))) + assertFalse(match("5 4 * * SUN", new Date("2024-01-08 04:05:00"))) + assertFalse(match("5 4 * * SUN", new Date("2025-01-07 04:05:00"))) + + assertTrue(match("42 5 0 * 8 *", new Date("2024-08-01 00:05:42"))) + assertFalse(match("42 5 0 * 8 *", new Date("2024-09-01 00:05:42"))) + assertFalse(match("42 5 0 * 8 *", new Date("2024-08-01 01:05:42"))) + + const london = DateTime.zoneUnsafeMakeNamed("Europe/London") + const londonTime = DateTime.unsafeMakeZoned("2024-06-01 14:15:00Z", { + timeZone: london, + adjustForTimeZone: true + }) + + const amsterdam = DateTime.zoneUnsafeMakeNamed("Europe/Amsterdam") + const amsterdamTime = DateTime.unsafeMakeZoned("2024-06-01 15:15:00Z", { + timeZone: amsterdam, + adjustForTimeZone: true + }) + + assertTrue(match(Cron.unsafeParse("15 14 1 * *", london), londonTime)) + assertTrue(match(Cron.unsafeParse("15 14 1 * *", london), amsterdamTime)) + }) + + it("next", () => { + const after = new Date("2024-01-04 16:21:00") + deepStrictEqual(next("5 0 8 2 *", after), new Date("2024-02-08 00:05:00")) + deepStrictEqual(next("15 14 1 * *", after), new Date("2024-02-01 14:15:00")) + deepStrictEqual(next("23 0-20/2 * * 0", after), new Date("2024-01-07 00:23:00")) + deepStrictEqual(next("5 4 * * SUN", after), new Date("2024-01-07 04:05:00")) + deepStrictEqual(next("5 4 * DEC SUN", after), new Date("2024-12-01 04:05:00")) + deepStrictEqual(next("30 5 0 8 2 *", after), new Date("2024-02-08 00:05:30")) + + const london = DateTime.zoneUnsafeMakeNamed("Europe/London") + const londonTime = DateTime.unsafeMakeZoned("2024-02-08 00:05:00Z", { + timeZone: london, + adjustForTimeZone: true + }) + + const amsterdam = DateTime.zoneUnsafeMakeNamed("Europe/Amsterdam") + const amsterdamTime = DateTime.unsafeMakeZoned("2024-02-08 01:05:00Z", { + timeZone: amsterdam, + adjustForTimeZone: true + }) + + deepStrictEqual(next(Cron.unsafeParse("5 0 8 2 *", london), after), DateTime.toDateUtc(londonTime)) + deepStrictEqual(next(Cron.unsafeParse("5 0 8 2 *", london), after), DateTime.toDateUtc(amsterdamTime)) + }) + + it("prev", () => { + const utc = DateTime.zoneUnsafeMakeNamed("UTC") + const before = new Date("2024-01-04T16:21:00Z") + deepStrictEqual(prev(Cron.unsafeParse("5 0 8 2 *", utc), before), new Date("2023-02-08T00:05:00.000Z")) + deepStrictEqual(prev(Cron.unsafeParse("15 14 1 * *", utc), before), new Date("2024-01-01T14:15:00.000Z")) + deepStrictEqual(prev(Cron.unsafeParse("23 0-20/2 * * * 0", utc), before), new Date("2023-12-31T23:20:23.000Z")) + deepStrictEqual(prev(Cron.unsafeParse("5 4 * * SUN", utc), before), new Date("2023-12-31T04:05:00.000Z")) + deepStrictEqual(prev(Cron.unsafeParse("5 4 * DEC SUN", utc), before), new Date("2023-12-31T04:05:00.000Z")) + deepStrictEqual(prev(Cron.unsafeParse("30 5 0 8 2 *", utc), before), new Date("2023-02-08T00:05:30.000Z")) + + const wednesday = new Date("2025-10-22T01:00:00.000Z") + deepStrictEqual(prev(Cron.unsafeParse("0 1 * * MON", utc), wednesday), new Date("2025-10-20T01:00:00.000Z")) + deepStrictEqual(next(Cron.unsafeParse("0 1 * * MON", utc), wednesday), new Date("2025-10-27T01:00:00.000Z")) + deepStrictEqual(prev(Cron.unsafeParse("0 1 * * TUE", utc), wednesday), new Date("2025-10-21T01:00:00.000Z")) + deepStrictEqual(next(Cron.unsafeParse("0 1 * * TUE", utc), wednesday), new Date("2025-10-28T01:00:00.000Z")) + }) + + it("returns the latest second when rolling back a minute", () => { + const utc = DateTime.zoneUnsafeMakeNamed("UTC") + const expr = Cron.unsafeParse("10,30 * * * * *", utc) + const before = new Date("2024-01-01T00:00:05.000Z") + deepStrictEqual(prev(expr, before), new Date("2023-12-31T23:59:30.000Z")) + }) + + it("forward and reverse sequences stay aligned", () => { + const cases = [ + ["5 2 * * 1", "2020-01-01T00:00:01Z", "2021-01-01T00:00:01Z"], + ["0 12 1 * *", "2020-01-01T00:00:01Z", "2021-01-01T00:00:01Z"], + ["10,30 * * * * *", "2024-01-01T00:00:00Z", "2024-01-02T00:00:00Z"] + ] as const + + const gather = ( + generator: IterableIterator, + lower: Date, + upper: Date, + direction: "forward" | "reverse" + ) => { + const res: Array = [] + for (const date of generator) { + if (direction === "forward" ? date >= upper : date <= lower) { + break + } + res.push(date) + } + return res + } + + for (const [expr, lowerStr, upperStr] of cases) { + const lower = new Date(lowerStr) + const upper = new Date(upperStr) + const cron = Cron.unsafeParse(expr, DateTime.zoneUnsafeMakeNamed("UTC")) + + const forward = gather(Cron.sequence(cron, lower), lower, upper, "forward") + const reverse = gather(Cron.sequenceReverse(cron, upper), lower, upper, "reverse").reverse() + + deepStrictEqual(forward, reverse) + } + }) + + it("prev prefers the latest matching day within the previous month", () => { + const cron = Cron.unsafeParse("0 0 8 5,20 * *", DateTime.zoneUnsafeMakeNamed("UTC")) + const before = new Date("2024-06-03T00:00:00.000Z") + deepStrictEqual(prev(cron, before), new Date("2024-05-20T08:00:00.000Z")) + }) + + it("prev wraps weekday using the last allowed value", () => { + const cron = Cron.unsafeParse("0 1 * * MON,FRI", DateTime.zoneUnsafeMakeNamed("UTC")) + const sunday = new Date("2025-10-19T12:00:00.000Z") // Sunday + deepStrictEqual(prev(cron, sunday), new Date("2025-10-17T01:00:00.000Z")) // Friday + }) + + it("prev chooses the later occurrence in DST fall-back", () => { + const make = (s: string) => DateTime.makeZonedFromString(s).pipe(Option.getOrThrow) + const tz = "Europe/Berlin" + const cron = Cron.unsafeParse("0 30 2 * * *", tz) + const before = make("2024-10-27T03:30:00.000+01:00[Europe/Berlin]") + const result = DateTime.unsafeMakeZoned(prev(cron, before), { timeZone: tz }) + deepStrictEqual(result.pipe(DateTime.formatIsoZoned), "2024-10-27T02:30:00.000+02:00[Europe/Berlin]") + }) + + it("prev respects combined day-of-month and weekday constraints", () => { + const tz = DateTime.zoneUnsafeMakeNamed("UTC") + const cron = Cron.unsafeParse("0 0 9 1,15 * MON", tz) + const before = new Date("2024-04-02T12:00:00.000Z") // Tue after a matching Monday the 1st + deepStrictEqual(prev(cron, before), new Date("2024-04-01T09:00:00.000Z")) + }) + + it("prev handles step expressions across day boundary", () => { + const tz = DateTime.zoneUnsafeMakeNamed("UTC") + const cron = Cron.unsafeParse("0 */7 8-10 * * *", tz) + const before = new Date("2024-01-01T08:01:00.000Z") + deepStrictEqual(prev(cron, before), new Date("2024-01-01T08:00:00.000Z")) + }) + + it("prev works with fixed offset time zones", () => { + const offset = DateTime.zoneMakeOffset(2 * 60 * 60 * 1000) // UTC+2 + const cron = Cron.unsafeParse("0 0 10 * * *", offset) + const before = new Date("2024-05-01T07:00:00.000Z") // before 10:00 local (08:00Z) + deepStrictEqual(prev(cron, before), new Date("2024-04-30T08:00:00.000Z")) + }) + + it("prev wraps across year boundary", () => { + const tz = DateTime.zoneUnsafeMakeNamed("UTC") + const cron = Cron.unsafeParse("0 0 1 1 *", tz) + const from = new Date("2024-01-01T00:00:00.000Z") + deepStrictEqual(prev(cron, from), new Date("2023-01-01T00:00:00.000Z")) + }) + + it("prev handles day 31 skipping months without it", () => { + const tz = DateTime.zoneUnsafeMakeNamed("UTC") + const cron = Cron.unsafeParse("0 0 31 * *", tz) + const from = new Date("2024-03-01T00:00:00.000Z") + // Should skip Feb (no day 31) and go to Jan 31 + deepStrictEqual(prev(cron, from), new Date("2024-01-31T00:00:00.000Z")) + }) + + it("prev clamps to the last valid day when rolling back a month with only month constraints", () => { + const tz = DateTime.zoneUnsafeMakeNamed("UTC") + const cron = Cron.unsafeParse("0 0 0 * FEB *", tz) + const from = new Date("2024-03-31T12:00:00.000Z") + deepStrictEqual(prev(cron, from), new Date("2024-02-29T00:00:00.000Z")) + }) + + it("prev with multiple months specified", () => { + const tz = DateTime.zoneUnsafeMakeNamed("UTC") + const cron = Cron.unsafeParse("0 0 15 1,4,7,10 *", tz) // Quarterly on 15th + const from = new Date("2024-05-01T00:00:00.000Z") + deepStrictEqual(prev(cron, from), new Date("2024-04-15T00:00:00.000Z")) + }) + + it("sequence", () => { + const start = new Date("2024-01-01 00:00:00") + const generator = Cron.sequence(Cron.unsafeParse("23 0-20/2 * * 0"), start) + deepStrictEqual(generator.next().value, new Date("2024-01-07 00:23:00")) + deepStrictEqual(generator.next().value, new Date("2024-01-07 02:23:00")) + deepStrictEqual(generator.next().value, new Date("2024-01-07 04:23:00")) + deepStrictEqual(generator.next().value, new Date("2024-01-07 06:23:00")) + deepStrictEqual(generator.next().value, new Date("2024-01-07 08:23:00")) + }) + + it("sequenceReverse", () => { + const start = new Date("2024-01-01 00:00:00Z") + const utc = DateTime.zoneUnsafeMakeNamed("UTC") + const generator = Cron.sequenceReverse(Cron.unsafeParse("23 0-20/2 * * 0", utc), start) + deepStrictEqual(generator.next().value, new Date("2023-12-31 20:23:00Z")) + deepStrictEqual(generator.next().value, new Date("2023-12-31 18:23:00Z")) + deepStrictEqual(generator.next().value, new Date("2023-12-31 16:23:00Z")) + deepStrictEqual(generator.next().value, new Date("2023-12-31 14:23:00Z")) + deepStrictEqual(generator.next().value, new Date("2023-12-31 12:23:00Z")) + }) + + it("equal", () => { + const cron = Cron.unsafeParse("23 0-20/2 * * 0") + assertTrue(Equal.equals(cron, cron)) + assertTrue(Equal.equals(cron, Cron.unsafeParse("23 0-20/2 * * 0"))) + assertFalse(Equal.equals(cron, Cron.unsafeParse("23 0-20/2 * * 1"))) + assertFalse(Equal.equals(cron, Cron.unsafeParse("23 0-20/2 * * 0-6"))) + assertFalse(Equal.equals(cron, Cron.unsafeParse("23 0-20/2 1 * 0"))) + }) + + it("handles leap years", () => { + assertTrue(match("0 0 29 2 *", new Date("2024-02-29 00:00:00"))) + assertFalse(match("0 0 29 2 *", new Date("2025-02-29 00:00:00"))) + assertFalse(match("0 0 29 2 *", new Date("2026-02-29 00:00:00"))) + assertFalse(match("0 0 29 2 *", new Date("2027-02-29 00:00:00"))) + assertTrue(match("0 0 29 2 *", new Date("2028-02-29 00:00:00"))) + + deepStrictEqual(next("0 0 29 2 *", new Date("2024-01-01 00:00:00")), new Date("2024-02-29 00:00:00")) + deepStrictEqual(next("0 0 29 2 *", new Date("2025-01-01 00:00:00")), new Date("2028-02-29 00:00:00")) + }) + + it("handles transition into daylight savings time", () => { + const make = (date: string) => DateTime.makeZonedFromString(date).pipe(Option.getOrThrow) + const sequence = Cron.sequence( + Cron.unsafeParse("30 * * * *", "Europe/Berlin"), + make("2024-03-31T00:00:00.000+01:00[Europe/Berlin]") + ) + const next = (): DateTime.Zoned => DateTime.unsafeMakeZoned(sequence.next().value, { timeZone: "Europe/Berlin" }) + + const a = make("2024-03-31T00:30:00.000+01:00[Europe/Berlin]") + const b = make("2024-03-31T01:30:00.000+01:00[Europe/Berlin]") + const c = make("2024-03-31T03:30:00.000+02:00[Europe/Berlin]") + const d = make("2024-03-31T04:30:00.000+02:00[Europe/Berlin]") + + deepStrictEqual(next().pipe(DateTime.formatIsoZoned), a.pipe(DateTime.formatIsoZoned)) + deepStrictEqual(next().pipe(DateTime.formatIsoZoned), b.pipe(DateTime.formatIsoZoned)) + deepStrictEqual(next().pipe(DateTime.formatIsoZoned), c.pipe(DateTime.formatIsoZoned)) + deepStrictEqual(next().pipe(DateTime.formatIsoZoned), d.pipe(DateTime.formatIsoZoned)) + }) + + it("handles transition out of daylight savings time", () => { + // DST fall-back transition in Europe/Berlin on 2024-10-27: + // At 3:00 AM +02:00, clocks "fall back" to 2:00 AM +01:00 + // This means times from 2:00-2:59 AM occur twice (ambiguous period) + // + // Correct "once" mode behavior for cron: + // - Include all normal times (00:30, 01:30) + // - Return first occurrence only of ambiguous times (02:30 +02:00) + // - Skip second occurrence of ambiguous times (02:30 +01:00) + // - Continue normally after transition (03:30 +01:00) + + const make = (date: string) => DateTime.makeZonedFromString(date).pipe(Option.getOrThrow) + const sequence = Cron.sequence( + Cron.unsafeParse("30 * * * *", "Europe/Berlin"), + make("2024-10-27T00:00:00.000+02:00[Europe/Berlin]") + ) + const next = (): DateTime.Zoned => DateTime.unsafeMakeZoned(sequence.next().value, { timeZone: "Europe/Berlin" }) + + const a = make("2024-10-27T00:30:00.000+02:00[Europe/Berlin]") // Normal time + const b = make("2024-10-27T01:30:00.000+02:00[Europe/Berlin]") // Normal time (not ambiguous) + const c = make("2024-10-27T02:30:00.000+02:00[Europe/Berlin]") // First occurrence during DST + const d = make("2024-10-27T03:30:00.000+01:00[Europe/Berlin]") // Standard time (skips 2nd 02:30) + const e = make("2024-10-27T04:30:00.000+01:00[Europe/Berlin]") // Standard time + + deepStrictEqual(next().pipe(DateTime.formatIsoZoned), a.pipe(DateTime.formatIsoZoned)) + deepStrictEqual(next().pipe(DateTime.formatIsoZoned), b.pipe(DateTime.formatIsoZoned)) + deepStrictEqual(next().pipe(DateTime.formatIsoZoned), c.pipe(DateTime.formatIsoZoned)) + deepStrictEqual(next().pipe(DateTime.formatIsoZoned), d.pipe(DateTime.formatIsoZoned)) + deepStrictEqual(next().pipe(DateTime.formatIsoZoned), e.pipe(DateTime.formatIsoZoned)) + }) + + it("handles utc timezone", () => { + const utc = DateTime.zoneUnsafeMakeNamed("UTC") + const make = (date: string) => DateTime.makeZonedFromString(date).pipe(Option.getOrThrow) + const sequence = Cron.sequence(Cron.unsafeParse("30 * * * *", utc), make("2024-10-27T00:00:00.000+00:00[UTC]")) + const next = (): DateTime.Zoned => DateTime.unsafeMakeZoned(sequence.next().value, { timeZone: utc }) + + const a = make("2024-10-27T00:30:00.000+00:00[UTC]") + const b = make("2024-10-27T01:30:00.000+00:00[UTC]") + const c = make("2024-10-27T02:30:00.000+00:00[UTC]") + const d = make("2024-10-27T03:30:00.000+00:00[UTC]") + + deepStrictEqual(next().pipe(DateTime.formatIsoZoned), a.pipe(DateTime.formatIsoZoned)) + deepStrictEqual(next().pipe(DateTime.formatIsoZoned), b.pipe(DateTime.formatIsoZoned)) + deepStrictEqual(next().pipe(DateTime.formatIsoZoned), c.pipe(DateTime.formatIsoZoned)) + deepStrictEqual(next().pipe(DateTime.formatIsoZoned), d.pipe(DateTime.formatIsoZoned)) + }) +}) diff --git a/repos/effect/packages/effect/test/Data.test.ts b/repos/effect/packages/effect/test/Data.test.ts new file mode 100644 index 0000000..44d8139 --- /dev/null +++ b/repos/effect/packages/effect/test/Data.test.ts @@ -0,0 +1,310 @@ +import { describe, it } from "@effect/vitest" +import { assertFalse, assertTrue, deepStrictEqual, strictEqual } from "@effect/vitest/utils" +import { Data, Equal, pipe } from "effect" + +describe("Data", () => { + it("struct", () => { + const x = Data.struct({ a: 0, b: 1, c: 2 }) + const y = Data.struct({ a: 0, b: 1, c: 2 }) + const { a, b, c } = x + strictEqual(a, 0) + strictEqual(b, 1) + strictEqual(c, 2) + assertTrue(Equal.equals(x, y)) + assertFalse(Equal.equals(x, Data.struct({ a: 0 }))) + + // different keys length + assertFalse(Equal.equals(Data.struct({ a: 0, b: 1 }), Data.struct({ a: 0 }))) + // same length but different keys + assertFalse(Equal.equals(Data.struct({ a: 0 }), Data.struct({ b: 1 }))) + }) + + it("unsafeStruct", () => { + const x = Data.unsafeStruct({ a: 0, b: 1, c: 2 }) + const y = Data.unsafeStruct({ a: 0, b: 1, c: 2 }) + const { a, b, c } = x + strictEqual(a, 0) + strictEqual(b, 1) + strictEqual(c, 2) + assertTrue(Equal.equals(x, y)) + }) + + it("tuple", () => { + const x = Data.tuple(0, 1, 2) + const y = Data.tuple(0, 1, 2) + const [a, b, c] = x + strictEqual(a, 0) + strictEqual(b, 1) + strictEqual(c, 2) + assertTrue(Equal.equals(x, y)) + assertFalse(Equal.equals(x, Data.tuple(0, 1))) + }) + + it("array", () => { + const x = Data.array([0, 1, 2]) + const y = Data.array([0, 1, 2]) + const [a, b, c] = x + strictEqual(a, 0) + strictEqual(b, 1) + strictEqual(c, 2) + assertTrue(Equal.equals(x, y)) + assertTrue(Equal.equals(x, Data.tuple(0, 1, 2))) + assertFalse(Equal.equals(x, Data.array([0, 1]))) + + // different length + assertFalse(Equal.equals(Data.array([0, 1, 2]), Data.array([0, 1]))) + }) + + it("case", () => { + interface Person { + readonly name: string + } + + const Person = Data.case() + + const a = Person({ name: "Mike" }) + const b = Person({ name: "Mike" }) + const c = Person({ name: "Foo" }) + + strictEqual(a.name, "Mike") + strictEqual(b.name, "Mike") + strictEqual(c.name, "Foo") + assertTrue(Equal.equals(a, b)) + assertFalse(Equal.equals(a, c)) + + const Empty = Data.case() + assertTrue(Equal.equals(Empty(), Empty())) + }) + + it("tagged", () => { + interface Person { + readonly _tag: "Person" + readonly name: string + } + + const Person = Data.tagged("Person") + + const a = Person({ name: "Mike" }) + const b = Person({ name: "Mike" }) + const c = Person({ name: "Foo" }) + + strictEqual(a._tag, "Person") + strictEqual(a.name, "Mike") + strictEqual(b.name, "Mike") + strictEqual(c.name, "Foo") + assertTrue(Equal.equals(a, b)) + assertFalse(Equal.equals(a, c)) + }) + + it("case class", () => { + class Person extends Data.Class<{ name: string }> {} + const a = new Person({ name: "Mike" }) + const b = new Person({ name: "Mike" }) + const c = new Person({ name: "Foo" }) + + strictEqual(a.name, "Mike") + strictEqual(b.name, "Mike") + strictEqual(c.name, "Foo") + assertTrue(Equal.equals(a, b)) + assertFalse(Equal.equals(a, c)) + + // different keys length + class D extends Data.Class<{ d: string; e: string }> {} + const d = new D({ d: "d", e: "e" }) + assertFalse(Equal.equals(a, d)) + // same length but different keys + class E extends Data.Class<{ e: string }> {} + const e = new E({ e: "e" }) + assertFalse(Equal.equals(a, e)) + }) + + it("date compares by value", () => { + const date = new Date() + const a = Data.struct({ date: new Date(date.toISOString()) }) + const b = Data.struct({ date: new Date(date.toISOString()) }) + + assertTrue(Equal.equals(a, b)) + }) + + it("URL compares by value", () => { + const a = Data.struct({ date: new URL("http://example.com") }) + const b = Data.struct({ date: new URL("http://example.com") }) + const c = Data.struct({ date: new URL("https://effect.website") }) + + assertTrue(Equal.equals(a, b)) + assertFalse(Equal.equals(a, c)) + }) + + it("tagged class", () => { + class Person extends Data.TaggedClass("Person")<{ name: string }> {} + const a = new Person({ name: "Mike" }) + const b = new Person({ name: "Mike" }) + const c = new Person({ name: "Foo" }) + + strictEqual(a._tag, "Person") + strictEqual(a.name, "Mike") + strictEqual(b.name, "Mike") + strictEqual(c.name, "Foo") + assertTrue(Equal.equals(a, b)) + assertFalse(Equal.equals(a, c)) + }) + + it("tagged - empty", () => { + interface Person { + readonly _tag: "Person" + } + + const Person = Data.tagged("Person") + + const a = Person() + const b = Person() + + assertTrue(Equal.equals(a, b)) + }) + + it("TaggedClass - empty", () => { + class Person extends Data.TaggedClass("Person")<{}> {} + + const a = new Person() + const b = new Person() + + assertTrue(Equal.equals(a, b)) + }) + + it("tagged - don't override tag", () => { + interface Foo { + readonly _tag: "Foo" + readonly value: string + } + const Foo = Data.tagged("Foo") + interface Bar { + readonly _tag: "Bar" + readonly value: number + } + const Bar = Data.tagged("Bar") + + const foo = Foo({ value: "test" }) + const bar = Bar({ ...foo, value: 10 }) + + strictEqual(bar._tag, "Bar") + }) + + it("taggedEnum", () => { + type HttpError = Data.TaggedEnum<{ + NotFound: {} + InternalServerError: { reason: string } + }> + const { + $is, + $match, + InternalServerError, + NotFound + } = Data.taggedEnum() + + const a = NotFound() + const b = InternalServerError({ reason: "test" }) + const c = InternalServerError({ reason: "test" }) + + strictEqual(a._tag, "NotFound") + strictEqual(b._tag, "InternalServerError") + + strictEqual(b.reason, "test") + strictEqual(c.reason, "test") + + assertFalse(Equal.equals(a, b)) + assertTrue(Equal.equals(b, c)) + + assertTrue($is("NotFound")(a)) + assertFalse($is("InternalServerError")(a)) + const matcher = $match({ + NotFound: () => 0, + InternalServerError: () => 1 + }) + strictEqual(matcher(a), 0) + strictEqual(matcher(b), 1) + }) + + it("taggedEnum - generics", () => { + type Result = Data.TaggedEnum<{ + Success: { value: A } + Failure: { + error: E + message?: string + } + }> + interface ResultDefinition extends Data.TaggedEnum.WithGenerics<2> { + readonly taggedEnum: Result + } + const { $is, $match, Failure, Success } = Data.taggedEnum() + + const a = Success({ value: 1 }) satisfies Result + const b = Failure({ error: "test" }) satisfies Result + const c = Success({ value: 1 }) satisfies Result + + strictEqual(a._tag, "Success") + strictEqual(b._tag, "Failure") + strictEqual(c._tag, "Success") + + strictEqual(a.value, 1) + strictEqual(b.error, "test") + + assertFalse(Equal.equals(a, b)) + assertTrue(Equal.equals(a, c)) + + const aResult = Success({ value: 1 }) as Result + const bResult = Failure({ error: "boom" }) as Result + + strictEqual( + $match(aResult, { + Success: (_) => 1, + Failure: (_) => 2 + }), + 1 + ) + const result = pipe( + bResult, + $match({ + Success: (_) => _.value, + Failure: (_) => _.error + }) + ) + result satisfies string | number + strictEqual(result, "boom") + + assertTrue($is("Success")(aResult)) + aResult satisfies { readonly _tag: "Success"; readonly value: number } + strictEqual(aResult.value, 1) + + assertTrue($is("Failure")(bResult)) + bResult satisfies { readonly _tag: "Failure"; readonly error: string } + strictEqual(bResult.error, "boom") + }) + + describe("Error", () => { + it("should support a message field", () => { + class MyError extends Data.Error<{ message: string; a: number }> {} + const e = new MyError({ message: "Oh no!", a: 1 }) + strictEqual(e.message, "Oh no!") + strictEqual(e.a, 1) + }) + + it("toJSON includes all args", () => { + class MyError extends Data.Error<{ message: string; a: number; cause: string }> {} + const e = new MyError({ message: "Oh no!", a: 1, cause: "Boom" }) + deepStrictEqual(e.toJSON(), { message: "Oh no!", a: 1, cause: "Boom" }) + }) + }) + + describe("TaggedError", () => { + it("toJSON includes all args", () => { + class MyError extends Data.TaggedError("MyError")<{ message: string; a: number; cause: string }> {} + const e = new MyError({ message: "Oh no!", a: 1, cause: "Boom" }) + deepStrictEqual(e.toJSON(), { + _tag: "MyError", + message: "Oh no!", + a: 1, + cause: "Boom" + }) + }) + }) +}) diff --git a/repos/effect/packages/effect/test/DateTime.test.ts b/repos/effect/packages/effect/test/DateTime.test.ts new file mode 100644 index 0000000..6c9aa7a --- /dev/null +++ b/repos/effect/packages/effect/test/DateTime.test.ts @@ -0,0 +1,938 @@ +import { describe, it } from "@effect/vitest" +import { assertNone, assertRight, assertSome, deepStrictEqual, strictEqual } from "@effect/vitest/utils" +import { DateTime, Duration, Effect, Option, TestClock } from "effect" + +const setTo2024NZ = TestClock.setTime(new Date("2023-12-31T11:00:00.000Z").getTime()) +const assertSomeIso = (value: Option.Option, expected: string) => { + const iso = value.pipe(Option.map((value) => DateTime.formatIso(DateTime.toUtc(value)))) + assertSome(iso, expected) +} + +describe("DateTime", () => { + describe("mutate", () => { + it.effect("should mutate the date", () => + Effect.gen(function*() { + const now = yield* DateTime.now + const tomorrow = DateTime.mutate(now, (date) => { + date.setUTCDate(date.getUTCDate() + 1) + }) + const diff = DateTime.distanceDurationEither(now, tomorrow) + assertRight(diff, Duration.decode("1 day")) + })) + + it.effect("correctly preserves the time zone", () => + Effect.gen(function*() { + yield* setTo2024NZ + const now = yield* DateTime.nowInCurrentZone.pipe( + DateTime.withCurrentZoneNamed("Pacific/Auckland") + ) + const future = DateTime.mutate(now, (date) => { + date.setUTCMonth(date.getUTCMonth() + 6) + }) + strictEqual(DateTime.toDateUtc(future).toISOString(), "2024-06-30T12:00:00.000Z") + strictEqual(DateTime.toDate(future).toISOString(), "2024-07-01T00:00:00.000Z") + const plusOne = DateTime.mutate(future, (date) => { + date.setUTCDate(date.getUTCDate() + 1) + }) + strictEqual(DateTime.toDateUtc(plusOne).toISOString(), "2024-07-01T12:00:00.000Z") + strictEqual(DateTime.toDate(plusOne).toISOString(), "2024-07-02T00:00:00.000Z") + })) + }) + + describe("add", () => { + it.effect("utc", () => + Effect.gen(function*() { + const now = yield* DateTime.now + const tomorrow = DateTime.add(now, { days: 1 }) + const diff = DateTime.distanceDurationEither(now, tomorrow) + assertRight(diff, Duration.decode("1 day")) + })) + + it("to month with less days", () => { + const jan = DateTime.unsafeMake({ year: 2023, month: 1, day: 31 }) + let feb = DateTime.add(jan, { months: 1 }) + strictEqual(feb.toJSON(), "2023-02-28T00:00:00.000Z") + + const mar = DateTime.unsafeMake({ year: 2023, month: 3, day: 31 }) + feb = DateTime.subtract(mar, { months: 1 }) + strictEqual(feb.toJSON(), "2023-02-28T00:00:00.000Z") + }) + + it.effect("correctly preserves the time zone", () => + Effect.gen(function*() { + yield* setTo2024NZ + const now = yield* DateTime.nowInCurrentZone.pipe( + DateTime.withCurrentZoneNamed("Pacific/Auckland") + ) + const future = DateTime.add(now, { months: 6 }) + strictEqual(DateTime.toDateUtc(future).toISOString(), "2024-06-30T12:00:00.000Z") + strictEqual(DateTime.toDate(future).toISOString(), "2024-07-01T00:00:00.000Z") + const plusOne = DateTime.add(future, { days: 1 }) + strictEqual(DateTime.toDateUtc(plusOne).toISOString(), "2024-07-01T12:00:00.000Z") + strictEqual(DateTime.toDate(plusOne).toISOString(), "2024-07-02T00:00:00.000Z") + const minusOne = DateTime.subtract(plusOne, { days: 1 }) + strictEqual(DateTime.toDateUtc(minusOne).toISOString(), "2024-06-30T12:00:00.000Z") + strictEqual(DateTime.toDate(minusOne).toISOString(), "2024-07-01T00:00:00.000Z") + })) + + it.effect("leap years", () => + Effect.gen(function*() { + yield* setTo2024NZ + const now = yield* DateTime.make({ year: 2024, month: 2, day: 29 }) + const future = DateTime.add(now, { years: 1 }) + strictEqual(DateTime.formatIso(future), "2025-02-28T00:00:00.000Z") + })) + }) + + describe("endOf", () => { + it("month", () => { + const mar = DateTime.unsafeMake("2024-03-15T12:00:00.000Z") + const end = DateTime.endOf(mar, "month") + strictEqual(end.toJSON(), "2024-03-31T23:59:59.999Z") + }) + + it("feb leap year", () => { + const feb = DateTime.unsafeMake("2024-02-15T12:00:00.000Z") + const end = DateTime.endOf(feb, "month") + strictEqual(end.toJSON(), "2024-02-29T23:59:59.999Z") + }) + + it("week", () => { + const start = DateTime.unsafeMake("2024-03-15T12:00:00.000Z") + const end = DateTime.endOf(start, "week") + strictEqual(end.toJSON(), "2024-03-16T23:59:59.999Z") + strictEqual(DateTime.getPartUtc(end, "weekDay"), 6) + }) + + it("week last day", () => { + const start = DateTime.unsafeMake("2024-03-16T12:00:00.000Z") + const end = DateTime.endOf(start, "week") + strictEqual(end.toJSON(), "2024-03-16T23:59:59.999Z") + }) + + it("week with options", () => { + const start = DateTime.unsafeMake("2024-03-15T12:00:00.000Z") + const end = DateTime.endOf(start, "week", { + weekStartsOn: 1 + }) + strictEqual(end.toJSON(), "2024-03-17T23:59:59.999Z") + }) + + it.effect("correctly preserves the time zone", () => + Effect.gen(function*() { + yield* setTo2024NZ + const now = yield* DateTime.nowInCurrentZone.pipe( + DateTime.withCurrentZoneNamed("Pacific/Auckland") + ) + const future = DateTime.endOf(now, "month") + strictEqual(DateTime.toDateUtc(future).toISOString(), "2024-01-31T10:59:59.999Z") + strictEqual(DateTime.toDate(future).toISOString(), "2024-01-31T23:59:59.999Z") + })) + }) + + describe("startOf", () => { + it("month", () => { + const mar = DateTime.unsafeMake("2024-03-15T12:00:00.000Z") + const end = DateTime.startOf(mar, "month") + strictEqual(end.toJSON(), "2024-03-01T00:00:00.000Z") + }) + + it("month duplicated", () => { + const mar = DateTime.unsafeMake("2024-03-15T12:00:00.000Z") + const end = DateTime.startOf(mar, "month").pipe( + DateTime.startOf("month") + ) + strictEqual(end.toJSON(), "2024-03-01T00:00:00.000Z") + }) + + it("feb leap year", () => { + const feb = DateTime.unsafeMake("2024-02-15T12:00:00.000Z") + const end = DateTime.startOf(feb, "month") + strictEqual(end.toJSON(), "2024-02-01T00:00:00.000Z") + }) + + it("week", () => { + const start = DateTime.unsafeMake("2024-03-15T12:00:00.000Z") + const end = DateTime.startOf(start, "week") + strictEqual(end.toJSON(), "2024-03-10T00:00:00.000Z") + strictEqual(DateTime.getPartUtc(end, "weekDay"), 0) + }) + + it("week first day", () => { + const start = DateTime.unsafeMake("2024-03-10T12:00:00.000Z") + const end = DateTime.startOf(start, "week") + strictEqual(end.toJSON(), "2024-03-10T00:00:00.000Z") + }) + + it("week with options", () => { + const start = DateTime.unsafeMake("2024-03-15T12:00:00.000Z") + const end = DateTime.startOf(start, "week", { + weekStartsOn: 1 + }) + strictEqual(end.toJSON(), "2024-03-11T00:00:00.000Z") + }) + }) + + describe("nearest", () => { + it("month up", () => { + const mar = DateTime.unsafeMake("2024-03-16T12:00:00.000Z") + const end = DateTime.nearest(mar, "month") + strictEqual(end.toJSON(), "2024-04-01T00:00:00.000Z") + }) + + it("month down", () => { + const mar = DateTime.unsafeMake("2024-03-16T11:00:00.000Z") + const end = DateTime.nearest(mar, "month") + strictEqual(end.toJSON(), "2024-03-01T00:00:00.000Z") + }) + + it("second up", () => { + const mar = DateTime.unsafeMake("2024-03-20T12:00:00.500Z") + const end = DateTime.nearest(mar, "second") + strictEqual(end.toJSON(), "2024-03-20T12:00:01.000Z") + }) + + it("second down", () => { + const mar = DateTime.unsafeMake("2024-03-20T12:00:00.400Z") + const end = DateTime.nearest(mar, "second") + strictEqual(end.toJSON(), "2024-03-20T12:00:00.000Z") + }) + }) + + describe("format", () => { + it.effect("full", () => + Effect.gen(function*() { + const now = yield* DateTime.now + strictEqual( + DateTime.format(now, { + locale: "en-US", + dateStyle: "full", + timeStyle: "full" + }), + "Thursday, January 1, 1970 at 12:00:00 AM Coordinated Universal Time" + ) + })) + }) + + describe("formatUtc", () => { + it.effect("full", () => + Effect.gen(function*() { + const now = yield* DateTime.now + strictEqual( + DateTime.formatUtc(now, { + locale: "en-US", + dateStyle: "full", + timeStyle: "full" + }), + "Thursday, January 1, 1970 at 12:00:00 AM Coordinated Universal Time" + ) + })) + }) + + describe("format zoned", () => { + it.effect("full", () => + Effect.gen(function*() { + const now = yield* DateTime.nowInCurrentZone.pipe( + DateTime.withCurrentZoneNamed("Pacific/Auckland") + ) + strictEqual( + DateTime.format(now, { + locale: "en-US", + dateStyle: "full", + timeStyle: "full" + }), + "Thursday, January 1, 1970 at 12:00:00 PM New Zealand Standard Time" + ) + })) + + it.effect("long with offset", () => + Effect.gen(function*() { + const now = yield* DateTime.now + const formatted = now.pipe( + DateTime.setZoneOffset(10 * 60 * 60 * 1000), + DateTime.format({ + locale: "en-US", + dateStyle: "long", + timeStyle: "short" + }) + ) + strictEqual(formatted, "January 1, 1970 at 10:00 AM") + })) + }) + + describe("fromParts", () => { + it("partial", () => { + const date = DateTime.unsafeMake({ + year: 2024, + month: 12, + day: 25 + }) + strictEqual(date.toJSON(), "2024-12-25T00:00:00.000Z") + }) + + it("month is set correctly", () => { + const date = DateTime.unsafeMake({ year: 2024 }) + strictEqual(date.toJSON(), "2024-01-01T00:00:00.000Z") + }) + }) + + describe("setPartsUtc", () => { + it("partial", () => { + const date = DateTime.unsafeMake({ + year: 2024, + month: 12, + day: 25 + }) + strictEqual(date.toJSON(), "2024-12-25T00:00:00.000Z") + + const updated = DateTime.setPartsUtc(date, { + year: 2023, + month: 1 + }) + strictEqual(updated.toJSON(), "2023-01-25T00:00:00.000Z") + }) + + it("ignores time zones", () => { + const date = DateTime.unsafeMake({ + year: 2024, + month: 12, + day: 25 + }).pipe(DateTime.unsafeSetZoneNamed("Pacific/Auckland")) + strictEqual(date.toJSON(), "2024-12-25T00:00:00.000Z") + + const updated = DateTime.setPartsUtc(date, { + year: 2023, + month: 1 + }) + strictEqual(updated.toJSON(), "2023-01-25T00:00:00.000Z") + }) + }) + + describe("setParts", () => { + it("partial", () => { + const date = DateTime.unsafeMake({ + year: 2024, + month: 12, + day: 25 + }) + strictEqual(date.toJSON(), "2024-12-25T00:00:00.000Z") + + const updated = DateTime.setParts(date, { + year: 2023, + month: 1 + }) + strictEqual(updated.toJSON(), "2023-01-25T00:00:00.000Z") + }) + + it("accounts for time zone", () => { + const date = DateTime.unsafeMake({ + year: 2024, + month: 12, + day: 25 + }).pipe(DateTime.unsafeSetZoneNamed("Pacific/Auckland")) + strictEqual(date.toJSON(), "2024-12-25T00:00:00.000Z") + + const updated = DateTime.setParts(date, { + year: 2023, + month: 6, + hours: 12 + }) + strictEqual(updated.toJSON(), "2023-06-25T00:00:00.000Z") + }) + }) + + describe("formatIso", () => { + it("full", () => { + const now = DateTime.unsafeMake("2024-03-15T12:00:00.000Z") + strictEqual(DateTime.formatIso(now), "2024-03-15T12:00:00.000Z") + }) + }) + + describe("formatIsoOffset", () => { + it.effect("correctly adds offset", () => + Effect.gen(function*() { + const now = yield* DateTime.nowInCurrentZone.pipe( + DateTime.withCurrentZoneNamed("Pacific/Auckland") + ) + strictEqual(DateTime.formatIsoOffset(now), "1970-01-01T12:00:00.000+12:00") + })) + }) + + describe("layerCurrentZoneNamed", () => { + it.effect("correctly adds offset", () => + Effect.gen(function*() { + const now = yield* DateTime.nowInCurrentZone + strictEqual(DateTime.formatIsoOffset(now), "1970-01-01T12:00:00.000+12:00") + }).pipe( + Effect.provide(DateTime.layerCurrentZoneNamed("Pacific/Auckland")) + )) + }) + + describe("removeTime", () => { + it("removes time", () => { + const dt = DateTime.unsafeMakeZoned("2024-01-01T01:00:00Z", { + timeZone: "Pacific/Auckland", + adjustForTimeZone: true + }).pipe(DateTime.removeTime) + strictEqual(dt.toJSON(), "2024-01-01T00:00:00.000Z") + }) + }) + + describe("makeZonedFromString", () => { + it.effect("parses time + zone", () => + Effect.gen(function*() { + const dt = yield* DateTime.makeZonedFromString("2024-07-21T20:12:34.112546348+12:00[Pacific/Auckland]") + strictEqual(dt.toJSON(), "2024-07-21T08:12:34.112Z") + })) + + it.effect("only offset", () => + Effect.gen(function*() { + const dt = yield* DateTime.makeZonedFromString("2024-07-21T20:12:34.112546348+12:00") + strictEqual(dt.zone._tag, "Offset") + strictEqual(dt.toJSON(), "2024-07-21T08:12:34.112Z") + })) + + it.effect("only offset with 00:00", () => + Effect.gen(function*() { + const dt = yield* DateTime.makeZonedFromString("2024-07-21T20:12:34.112546348+00:00") + strictEqual(dt.zone._tag, "Offset") + strictEqual(dt.toJSON(), "2024-07-21T20:12:34.112Z") + })) + + it.effect("roundtrip", () => + Effect.gen(function*() { + const dt = yield* DateTime.makeZonedFromString("2024-07-21T20:12:34.112546348+12:00[Pacific/Auckland]").pipe( + Option.map(DateTime.formatIsoZoned), + Option.flatMap(DateTime.makeZonedFromString) + ) + deepStrictEqual(dt.zone, DateTime.zoneUnsafeMakeNamed("Pacific/Auckland")) + strictEqual(dt.toJSON(), "2024-07-21T08:12:34.112Z") + })) + }) + + it("parts equality", () => { + const d1 = DateTime.unsafeMake("2025-01-01") + const d2 = DateTime.unsafeMake("2025-01-01") + deepStrictEqual(d1, d2) + DateTime.toPartsUtc(d2) + deepStrictEqual(d1, d2) + }) + + // doesnt work in CI + it.skip("unsafeMakeZoned no options", () => { + const date = new Date("2024-07-21T20:12:34.112Z") + ;(date as any).getTimezoneOffset = () => -60 + const dt = DateTime.unsafeMakeZoned(date) + deepStrictEqual(dt.zone, DateTime.zoneMakeOffset(60 * 60 * 1000)) + }) + + describe("toUtc", () => { + it.effect("with a Utc", () => + Effect.gen(function*() { + const dt = DateTime.unsafeMake("2024-01-01T01:00:00Z") + strictEqual(dt.toJSON(), "2024-01-01T01:00:00.000Z") + })) + + it.effect("with a Zoned", () => + Effect.gen(function*() { + const dt = DateTime.unsafeMakeZoned("2024-01-01T01:00:00Z", { + timeZone: "Pacific/Auckland", + adjustForTimeZone: true + }) + strictEqual(dt.toJSON(), "2023-12-31T12:00:00.000Z") + })) + }) + + describe("nowAsDate", () => { + it.effect("should return the current Date", () => + Effect.gen(function*() { + yield* setTo2024NZ + const now = yield* DateTime.nowAsDate + deepStrictEqual(now, new Date("2023-12-31T11:00:00.000Z")) + })) + }) + + describe("unsafeMake", () => { + it("treats strings without zone info as UTC", () => { + let dt = DateTime.unsafeMake("2024-01-01 01:00:00") + strictEqual(dt.toJSON(), "2024-01-01T01:00:00.000Z") + + dt = DateTime.unsafeMake("2020-02-01T11:17:00+1100") + strictEqual(dt.toJSON(), "2020-02-01T00:17:00.000Z") + }) + }) + + describe("Disambiguation", () => { + it.each<{ + zone: string + time: Partial + description: string + expectedResults: Record + }>( + [ + { + zone: "America/New_York", + time: { year: 2024, month: 3, day: 10, hours: 2 }, + description: "America/New_York 02:00 gap time during leap year (2024)", + expectedResults: { + compatible: "2024-03-10T07:00:00.000Z", + earlier: "2024-03-10T06:00:00.000Z", + later: "2024-03-10T07:00:00.000Z", + reject: "REJECT" + } + }, + { + zone: "America/New_York", + time: { year: 2024, month: 11, day: 3, hours: 1, minutes: 30 }, + description: "America/New_York 01:30 ambiguous time during leap year (2024)", + expectedResults: { + compatible: "2024-11-03T05:30:00.000Z", + earlier: "2024-11-03T05:30:00.000Z", + later: "2024-11-03T06:30:00.000Z", + reject: "REJECT" + } + }, + { + zone: "America/New_York", + time: { year: 2025, month: 3, day: 9, hours: 1 }, + description: "America/New_York 01:00 before DST transition", + expectedResults: { + compatible: "2025-03-09T06:00:00.000Z", + earlier: "2025-03-09T06:00:00.000Z", + later: "2025-03-09T06:00:00.000Z", + reject: "2025-03-09T06:00:00.000Z" + } + }, + { + zone: "America/New_York", + time: { year: 2025, month: 3, day: 9, hours: 1, minutes: 59, seconds: 59 }, + description: "America/New_York 01:59:59 last second before DST gap", + expectedResults: { + compatible: "2025-03-09T06:59:59.000Z", + earlier: "2025-03-09T06:59:59.000Z", + later: "2025-03-09T06:59:59.000Z", + reject: "2025-03-09T06:59:59.000Z" + } + }, + { + zone: "America/New_York", + time: { year: 2025, month: 3, day: 9, hours: 2 }, + description: "America/New_York 02:00 gap time (DST spring forward)", + expectedResults: { + compatible: "2025-03-09T07:00:00.000Z", + earlier: "2025-03-09T06:00:00.000Z", + later: "2025-03-09T07:00:00.000Z", + reject: "REJECT" + } + }, + { + zone: "America/New_York", + time: { year: 2025, month: 3, day: 9, hours: 2, minutes: 15 }, + description: "America/New_York 02:15 gap time (DST spring forward)", + expectedResults: { + compatible: "2025-03-09T07:15:00.000Z", + earlier: "2025-03-09T06:15:00.000Z", + later: "2025-03-09T07:15:00.000Z", + reject: "REJECT" + } + }, + { + zone: "America/New_York", + time: { year: 2025, month: 3, day: 9, hours: 2, minutes: 30 }, + description: "America/New_York 02:30 gap time (DST spring forward)", + expectedResults: { + compatible: "2025-03-09T07:30:00.000Z", + earlier: "2025-03-09T06:30:00.000Z", + later: "2025-03-09T07:30:00.000Z", + reject: "REJECT" + } + }, + { + zone: "America/New_York", + time: { year: 2025, month: 3, day: 9, hours: 2, minutes: 45 }, + description: "America/New_York 02:45 gap time (DST spring forward)", + expectedResults: { + compatible: "2025-03-09T07:45:00.000Z", + earlier: "2025-03-09T06:45:00.000Z", + later: "2025-03-09T07:45:00.000Z", + reject: "REJECT" + } + }, + { + zone: "America/New_York", + time: { year: 2025, month: 3, day: 9, hours: 3 }, + description: "America/New_York 03:00 first valid time after DST gap", + expectedResults: { + compatible: "2025-03-09T07:00:00.000Z", + earlier: "2025-03-09T07:00:00.000Z", + later: "2025-03-09T07:00:00.000Z", + reject: "2025-03-09T07:00:00.000Z" + } + }, + { + zone: "America/New_York", + time: { year: 2025, month: 11, day: 2, hours: 1, minutes: 0, seconds: 0 }, + description: "America/New_York 01:00:00 exact start of ambiguous period", + expectedResults: { + compatible: "2025-11-02T05:00:00.000Z", + earlier: "2025-11-02T05:00:00.000Z", + later: "2025-11-02T06:00:00.000Z", + reject: "REJECT" + } + }, + { + zone: "America/New_York", + time: { year: 2025, month: 11, day: 2, hours: 1, minutes: 30 }, + description: "America/New_York 01:30 ambiguous time (DST fall back)", + expectedResults: { + compatible: "2025-11-02T05:30:00.000Z", + earlier: "2025-11-02T05:30:00.000Z", + later: "2025-11-02T06:30:00.000Z", + reject: "REJECT" + } + }, + { + zone: "America/New_York", + time: { year: 2025, month: 11, day: 2, hours: 1, minutes: 59, seconds: 59 }, + description: "America/New_York 01:59:59 last second of ambiguous period", + expectedResults: { + compatible: "2025-11-02T05:59:59.000Z", + earlier: "2025-11-02T05:59:59.000Z", + later: "2025-11-02T06:59:59.000Z", + reject: "REJECT" + } + }, + { + zone: "Asia/Kathmandu", + time: { year: 2025, month: 6, day: 15, hours: 12 }, + description: "Asia/Kathmandu 12:00 unusual offset (UTC+05:45)", + expectedResults: { + compatible: "2025-06-15T06:15:00.000Z", + earlier: "2025-06-15T06:15:00.000Z", + later: "2025-06-15T06:15:00.000Z", + reject: "2025-06-15T06:15:00.000Z" + } + }, + { + zone: "Australia/Melbourne", + time: { year: 2025, month: 10, day: 5, hours: 2 }, + description: "Australia/Melbourne 02:00 gap time (DST starts, spring forward)", + expectedResults: { + compatible: "2025-10-04T16:00:00.000Z", + earlier: "2025-10-04T15:00:00.000Z", + later: "2025-10-04T16:00:00.000Z", + reject: "REJECT" + } + }, + { + zone: "Australia/Sydney", + time: { year: 2025, month: 4, day: 6, hours: 1 }, + description: "Australia/Sydney 01:00 normal timezone conversion", + expectedResults: { + compatible: "2025-04-05T14:00:00.000Z", + earlier: "2025-04-05T14:00:00.000Z", + later: "2025-04-05T14:00:00.000Z", + reject: "2025-04-05T14:00:00.000Z" + } + }, + { + zone: "Australia/Sydney", + time: { year: 2025, month: 4, day: 6, hours: 2, minutes: 30 }, + description: "Australia/Sydney 02:30 ambiguous time (DST ends, fall back)", + expectedResults: { + compatible: "2025-04-05T15:30:00.000Z", + earlier: "2025-04-05T15:30:00.000Z", + later: "2025-04-05T16:30:00.000Z", + reject: "REJECT" + } + }, + { + zone: "Australia/Sydney", + time: { year: 2025, month: 10, day: 5, hours: 2, minutes: 30 }, + description: "Australia/Sydney 02:30 gap time (DST starts, spring forward)", + expectedResults: { + compatible: "2025-10-04T16:30:00.000Z", + earlier: "2025-10-04T15:30:00.000Z", + later: "2025-10-04T16:30:00.000Z", + reject: "REJECT" + } + }, + { + zone: "Europe/Athens", + time: { year: 2024, month: 10, day: 27, hours: 3 }, + description: "Europe/Athens 03:00 ambiguous time during leap year (2024)", + expectedResults: { + compatible: "2024-10-27T00:00:00.000Z", + earlier: "2024-10-27T00:00:00.000Z", + later: "2024-10-27T01:00:00.000Z", + reject: "REJECT" + } + }, + { + zone: "Europe/Athens", + time: { year: 2025, month: 3, day: 27, hours: 1 }, + description: "Europe/Athens 01:00 normal time before DST", + expectedResults: { + compatible: "2025-03-26T23:00:00.000Z", + earlier: "2025-03-26T23:00:00.000Z", + later: "2025-03-26T23:00:00.000Z", + reject: "2025-03-26T23:00:00.000Z" + } + }, + { + zone: "Europe/Athens", + time: { year: 2025, month: 3, day: 30, hours: 1 }, + description: "Europe/Athens 01:00 before DST transition (UTC+2)", + expectedResults: { + compatible: "2025-03-29T23:00:00.000Z", + earlier: "2025-03-29T23:00:00.000Z", + later: "2025-03-29T23:00:00.000Z", + reject: "2025-03-29T23:00:00.000Z" + } + }, + { + zone: "Europe/Athens", + time: { year: 2025, month: 3, day: 30, hours: 2, minutes: 30 }, + description: "Europe/Athens 02:30 normal time before DST transition", + expectedResults: { + compatible: "2025-03-30T00:30:00.000Z", + earlier: "2025-03-30T00:30:00.000Z", + later: "2025-03-30T00:30:00.000Z", + reject: "2025-03-30T00:30:00.000Z" + } + }, + { + zone: "Europe/Athens", + time: { year: 2025, month: 3, day: 30, hours: 3 }, + description: "Europe/Athens 03:00 gap time (DST spring forward)", + expectedResults: { + compatible: "2025-03-30T01:00:00.000Z", + earlier: "2025-03-30T00:00:00.000Z", + later: "2025-03-30T01:00:00.000Z", + reject: "REJECT" + } + }, + { + zone: "Europe/Athens", + time: { year: 2025, month: 3, day: 30, hours: 4 }, + description: "Europe/Athens 04:00 normal time after DST transition", + expectedResults: { + compatible: "2025-03-30T01:00:00.000Z", + earlier: "2025-03-30T01:00:00.000Z", + later: "2025-03-30T01:00:00.000Z", + reject: "2025-03-30T01:00:00.000Z" + } + }, + { + zone: "Europe/Athens", + time: { year: 2025, month: 10, day: 26, hours: 3 }, + description: "Europe/Athens 03:00 ambiguous time (DST fall back)", + expectedResults: { + compatible: "2025-10-26T00:00:00.000Z", + earlier: "2025-10-26T00:00:00.000Z", + later: "2025-10-26T01:00:00.000Z", + reject: "REJECT" + } + }, + { + zone: "Europe/Berlin", + time: { year: 2025, month: 10, day: 26, hours: 2, minutes: 30 }, + description: "Europe/Berlin 02:30 ambiguous time (DST fall back)", + expectedResults: { + compatible: "2025-10-26T00:30:00.000Z", + earlier: "2025-10-26T00:30:00.000Z", + later: "2025-10-26T01:30:00.000Z", + reject: "REJECT" + } + }, + { + zone: "Europe/London", + time: { year: 2024, month: 3, day: 31, hours: 1 }, + description: "Europe/London 01:00 gap time during leap year (2024)", + expectedResults: { + compatible: "2024-03-31T01:00:00.000Z", + earlier: "2024-03-31T00:00:00.000Z", + later: "2024-03-31T01:00:00.000Z", + reject: "REJECT" + } + }, + { + zone: "Europe/London", + time: { year: 2025, month: 3, day: 29, hours: 1 }, + description: "Europe/London 01:00 normal time day before DST", + expectedResults: { + compatible: "2025-03-29T01:00:00.000Z", + earlier: "2025-03-29T01:00:00.000Z", + later: "2025-03-29T01:00:00.000Z", + reject: "2025-03-29T01:00:00.000Z" + } + }, + { + zone: "Europe/London", + time: { year: 2025, month: 3, day: 30, hours: 1 }, + description: "Europe/London 01:00 gap time (DST spring forward)", + expectedResults: { + compatible: "2025-03-30T01:00:00.000Z", + earlier: "2025-03-30T00:00:00.000Z", + later: "2025-03-30T01:00:00.000Z", + reject: "REJECT" + } + }, + { + zone: "Europe/London", + time: { year: 2025, month: 10, day: 26, hours: 1, minutes: 30 }, + description: "Europe/London 01:30 ambiguous time (DST fall back)", + expectedResults: { + compatible: "2025-10-26T00:30:00.000Z", + earlier: "2025-10-26T00:30:00.000Z", + later: "2025-10-26T01:30:00.000Z", + reject: "REJECT" + } + }, + { + zone: "Pacific/Auckland", + time: { year: 2025, month: 1, day: 15, hours: 12 }, + description: "Pacific/Auckland 12:00 during DST period (NZDT, UTC+13)", + expectedResults: { + compatible: "2025-01-14T23:00:00.000Z", + earlier: "2025-01-14T23:00:00.000Z", + later: "2025-01-14T23:00:00.000Z", + reject: "2025-01-14T23:00:00.000Z" + } + }, + { + zone: "Pacific/Auckland", + time: { year: 2025, month: 4, day: 6, hours: 1, minutes: 59 }, + description: "Pacific/Auckland 01:59 last minute before DST ends", + expectedResults: { + compatible: "2025-04-05T12:59:00.000Z", + earlier: "2025-04-05T12:59:00.000Z", + later: "2025-04-05T12:59:00.000Z", + reject: "2025-04-05T12:59:00.000Z" + } + }, + { + zone: "Pacific/Auckland", + time: { year: 2025, month: 4, day: 6, hours: 2, minutes: 30 }, + description: "Pacific/Auckland 02:30 ambiguous time (DST ends, fall back)", + expectedResults: { + compatible: "2025-04-05T13:30:00.000Z", + earlier: "2025-04-05T13:30:00.000Z", + later: "2025-04-05T14:30:00.000Z", + reject: "REJECT" + } + }, + { + zone: "Pacific/Auckland", + time: { year: 2025, month: 4, day: 6, hours: 3 }, + description: "Pacific/Auckland 03:00 normal time after DST ends", + expectedResults: { + compatible: "2025-04-05T15:00:00.000Z", + earlier: "2025-04-05T15:00:00.000Z", + later: "2025-04-05T15:00:00.000Z", + reject: "2025-04-05T15:00:00.000Z" + } + }, + { + zone: "Pacific/Auckland", + time: { year: 2025, month: 7, day: 15, hours: 12 }, + description: "Pacific/Auckland 12:00 during standard time (NZST, UTC+12)", + expectedResults: { + compatible: "2025-07-15T00:00:00.000Z", + earlier: "2025-07-15T00:00:00.000Z", + later: "2025-07-15T00:00:00.000Z", + reject: "2025-07-15T00:00:00.000Z" + } + }, + { + zone: "Pacific/Auckland", + time: { year: 2025, month: 9, day: 28, hours: 1, minutes: 59, seconds: 59 }, + description: "Pacific/Auckland 01:59:59 last second before DST starts", + expectedResults: { + compatible: "2025-09-27T13:59:59.000Z", + earlier: "2025-09-27T13:59:59.000Z", + later: "2025-09-27T13:59:59.000Z", + reject: "2025-09-27T13:59:59.000Z" + } + }, + { + zone: "Pacific/Auckland", + time: { year: 2025, month: 9, day: 28, hours: 2, minutes: 30 }, + description: "Pacific/Auckland 02:30 gap time (DST starts, spring forward)", + expectedResults: { + compatible: "2025-09-27T14:30:00.000Z", + earlier: "2025-09-27T13:30:00.000Z", + later: "2025-09-27T14:30:00.000Z", + reject: "REJECT" + } + }, + { + zone: "Pacific/Auckland", + time: { year: 2025, month: 9, day: 28, hours: 3 }, + description: "Pacific/Auckland 03:00 first valid time after DST gap", + expectedResults: { + compatible: "2025-09-27T14:00:00.000Z", + earlier: "2025-09-27T14:00:00.000Z", + later: "2025-09-27T14:00:00.000Z", + reject: "2025-09-27T14:00:00.000Z" + } + }, + { + zone: "Pacific/Kiritimati", + time: { year: 2025, month: 6, day: 15, hours: 12 }, + description: "Pacific/Kiritimati 12:00 extreme positive offset (UTC+14)", + expectedResults: { + compatible: "2025-06-14T22:00:00.000Z", + earlier: "2025-06-14T22:00:00.000Z", + later: "2025-06-14T22:00:00.000Z", + reject: "2025-06-14T22:00:00.000Z" + } + }, + { + zone: "Pacific/Marquesas", + time: { year: 2025, month: 6, day: 15, hours: 12 }, + description: "Pacific/Marquesas 12:00 unusual negative offset (UTC-09:30)", + expectedResults: { + compatible: "2025-06-15T21:30:00.000Z", + earlier: "2025-06-15T21:30:00.000Z", + later: "2025-06-15T21:30:00.000Z", + reject: "2025-06-15T21:30:00.000Z" + } + } + ] + )( + "should handle $description", + ({ expectedResults, time, zone }) => { + // Test normal strategies + for (const strategy of ["compatible", "earlier", "later"] as const) { + const result = DateTime.makeZoned(time, { + timeZone: zone, + adjustForTimeZone: true, + disambiguation: strategy + }) + assertSomeIso(result, expectedResults[strategy]) + } + + // Test default behavior ("compatible") + const defaultResult = DateTime.makeZoned(time, { + timeZone: zone, + adjustForTimeZone: true + }) + assertSomeIso(defaultResult, expectedResults.compatible) + + // Test reject strategy + const rejectResult = DateTime.makeZoned(time, { + timeZone: zone, + adjustForTimeZone: true, + disambiguation: "reject" + }) + + if (expectedResults.reject === "REJECT") { + assertNone(rejectResult) + } else { + assertSomeIso(rejectResult, expectedResults.reject) + } + } + ) + }) +}) diff --git a/repos/effect/packages/effect/test/Deferred.test.ts b/repos/effect/packages/effect/test/Deferred.test.ts new file mode 100644 index 0000000..0f011c2 --- /dev/null +++ b/repos/effect/packages/effect/test/Deferred.test.ts @@ -0,0 +1,156 @@ +import { describe, it } from "@effect/vitest" +import { assertFalse, assertTrue, deepStrictEqual, strictEqual } from "@effect/vitest/utils" +import { Deferred, Effect, Exit, Option, pipe, Ref } from "effect" + +describe("Deferred", () => { + it.effect("complete a deferred using succeed", () => + Effect.gen(function*() { + const deferred = yield* Deferred.make() + const success = yield* Deferred.succeed(deferred, 32) + const result = yield* Deferred.await(deferred) + assertTrue(success) + strictEqual(result, 32) + })) + it.effect("complete a deferred using complete", () => + Effect.gen(function*() { + const deferred = yield* Deferred.make() + const ref = yield* Ref.make(13) + yield* Deferred.complete(deferred, Ref.updateAndGet(ref, (n) => n + 1)) + const result1 = yield* Deferred.await(deferred) + const result2 = yield* Deferred.await(deferred) + strictEqual(result1, 14) + strictEqual(result2, 14) + })) + it.effect("complete a deferred using completeWith", () => + Effect.gen(function*() { + const deferred = yield* Deferred.make() + const ref = yield* Ref.make(13) + yield* Deferred.completeWith(deferred, Ref.updateAndGet(ref, (n) => n + 1)) + const result1 = yield* Deferred.await(deferred) + const result2 = yield* Deferred.await(deferred) + strictEqual(result1, 14) + strictEqual(result2, 15) + })) + it.effect("complete a deferred twice", () => + Effect.gen(function*() { + const deferred = yield* Deferred.make() + yield* Deferred.succeed(deferred, 1) + const success = yield* Deferred.complete(deferred, Effect.succeed(9)) + const result = yield* Deferred.await(deferred) + assertFalse(success) + strictEqual(result, 1) + })) + it.effect("fail a deferred using fail", () => + Effect.gen(function*() { + const deferred = yield* Deferred.make() + const success = yield* Deferred.fail(deferred, "error with fail") + const result = yield* pipe(deferred, Deferred.await, Effect.exit) + assertTrue(success) + assertTrue(Exit.isFailure(result)) + })) + it.effect("fail a deferred using complete", () => + Effect.gen(function*() { + const deferred = yield* Deferred.make() + const ref = yield* Ref.make(["first error", "second error"]) + const success = yield* Deferred.complete(deferred, Effect.flip(Ref.modify(ref, (as) => [as[0]!, as.slice(1)]))) + const result1 = yield* pipe(deferred, Deferred.await, Effect.exit) + const result2 = yield* pipe(deferred, Deferred.await, Effect.exit) + assertTrue(success) + assertTrue(Exit.isFailure(result1)) + assertTrue(Exit.isFailure(result2)) + })) + it.effect("fail a deferred using completeWith", () => + Effect.gen(function*() { + const deferred = yield* Deferred.make() + const ref = yield* Ref.make(["first error", "second error"]) + const success = yield* Deferred.completeWith( + deferred, + Effect.flip( + Ref.modify(ref, (as) => [as[0]!, as.slice(1)]) + ) + ) + const result1 = yield* pipe(deferred, Deferred.await, Effect.exit) + const result2 = yield* pipe(deferred, Deferred.await, Effect.exit) + assertTrue(success) + assertTrue(Exit.isFailure(result1)) + assertTrue(Exit.isFailure(result2)) + })) + it.effect("is done when a deferred is completed", () => + Effect.gen(function*() { + const deferred = yield* Deferred.make() + yield* Deferred.succeed(deferred, 0) + const result = yield* Deferred.isDone(deferred) + assertTrue(result) + })) + it.effect("is done when a deferred is failed", () => + Effect.gen(function*() { + const deferred = yield* Deferred.make() + yield* Deferred.fail(deferred, "failure") + const result = yield* Deferred.isDone(deferred) + assertTrue(result) + })) + it.effect("should interrupt a deferred", () => + Effect.gen(function*() { + const deferred = yield* Deferred.make() + const result = yield* Deferred.interrupt(deferred) + assertTrue(result) + })) + it.effect("poll a deferred that is not completed yet", () => + Effect.gen(function*() { + const deferred = yield* Deferred.make() + const result = yield* Deferred.poll(deferred) + assertTrue(Option.isNone(result)) + })) + it.effect("poll a deferred that is completed", () => + Effect.gen(function*() { + const deferred = yield* Deferred.make() + yield* Deferred.succeed(deferred, 12) + const result = yield* Deferred.poll(deferred).pipe( + Effect.flatMap(Option.match({ + onNone: () => Effect.fail("fail"), + onSome: Effect.succeed + })), + Effect.flatten, + Effect.exit + ) + deepStrictEqual(result, Exit.succeed(12)) + })) + it.effect("poll a deferred that is failed", () => + Effect.gen(function*() { + const deferred = yield* Deferred.make() + yield* Deferred.fail(deferred, "failure") + const result = yield* Deferred.poll(deferred).pipe( + Effect.flatMap(Option.match({ + onNone: () => Effect.fail("fail"), + onSome: Effect.succeed + })), + Effect.flatten, + Effect.exit + ) + assertTrue(Exit.isFailure(result)) + })) + it.effect("poll a deferred that is interrupted", () => + Effect.gen(function*() { + const deferred = yield* Deferred.make() + yield* Deferred.interrupt(deferred) + const result = yield* Deferred.poll(deferred).pipe( + Effect.flatMap(Option.match({ + onNone: () => Effect.fail("fail"), + onSome: Effect.succeed + })), + Effect.flatten, + Effect.exit + ) + assertTrue(Exit.isInterrupted(result)) + })) + it.effect("is subtype of Effect", () => + Effect.gen(function*() { + const deferred = yield* Deferred.make() + const ref = yield* Ref.make(13) + yield* Deferred.complete(deferred, Ref.updateAndGet(ref, (n) => n + 1)) + const result1 = yield* deferred + const result2 = yield* deferred + strictEqual(result1, 14) + strictEqual(result2, 14) + })) +}) diff --git a/repos/effect/packages/effect/test/Differ.test.ts b/repos/effect/packages/effect/test/Differ.test.ts new file mode 100644 index 0000000..5ef7107 --- /dev/null +++ b/repos/effect/packages/effect/test/Differ.test.ts @@ -0,0 +1,134 @@ +import { describe, it as it_ } from "@effect/vitest" +import { assertTrue, deepStrictEqual } from "@effect/vitest/utils" +import { Array as Arr, Chunk, Differ, Equal, HashMap, HashSet, pipe } from "effect" + +function diffLaws( + differ: Differ.Differ, + gen: () => Value, + equal: (a: Value, b: Value) => boolean +): void { + const it = (name: string, f: () => void) => + it_(name, () => { + for (let i = 0; i < 100; i++) { + f() + } + }) + + describe("differ laws", () => { + it("combining patches is associative", () => { + const value1 = gen() + const value2 = gen() + const value3 = gen() + const value4 = gen() + const patch1 = differ.diff(value1, value2) + const patch2 = differ.diff(value2, value3) + const patch3 = differ.diff(value3, value4) + const left = differ.combine(differ.combine(patch1, patch2), patch3) + const right = differ.combine(patch1, differ.combine(patch2, patch3)) + assertTrue(equal(differ.patch(left, value1), differ.patch(right, value1))) + }) + + it("combining a patch with an empty patch is an identity", () => { + const oldValue = gen() + const newValue = gen() + const patch = differ.diff(oldValue, newValue) + const left = differ.combine(patch, differ.empty) + const right = differ.combine(differ.empty, patch) + assertTrue(equal(differ.patch(left, oldValue), newValue)) + assertTrue(equal(differ.patch(right, oldValue), newValue)) + }) + + it("diffing a value with itself returns an empty patch", () => { + const value = gen() + deepStrictEqual(differ.diff(value, value), differ.empty) + }) + + it("diffing and then patching is an identity", () => { + const oldValue = gen() + const newValue = gen() + const patch = differ.diff(oldValue, newValue) + assertTrue(equal(differ.patch(patch, oldValue), newValue)) + }) + + it("patching with an empty patch is an identity", () => { + const value = gen() + assertTrue(equal(differ.patch(differ.empty, value), value)) + }) + }) +} + +const min = 1 +const max = 100 + +function smallInt(): number { + return Math.floor(Math.random() * (max - min + 1) + min) +} + +function randomChunk(): Chunk.Chunk { + return Chunk.fromIterable(Array.from({ length: 20 }, smallInt)) +} + +function randomHashMap(): HashMap.HashMap { + return pipe( + Arr.fromIterable(Array.from({ length: 2 }, smallInt)), + Arr.cartesian(Arr.fromIterable(Array.from({ length: 2 }, smallInt))), + HashMap.fromIterable + ) +} + +function randomHashSet(): HashSet.HashSet { + return HashSet.fromIterable(Array.from({ length: 20 }, smallInt)) +} + +function randomReadonlyArray(): ReadonlyArray { + return Array.from({ length: 20 }, smallInt) +} + +function randomPair(): readonly [number, number] { + return [smallInt(), smallInt()] +} + +describe("Differ", () => { + describe("chunk", () => { + diffLaws( + Differ.chunk number>(Differ.update()), + randomChunk, + Equal.equals + ) + }) + + describe("hashMap", () => { + diffLaws( + Differ.hashMap number>(Differ.update()), + randomHashMap, + Equal.equals + ) + }) + + describe("hashSet", () => { + diffLaws( + Differ.hashSet(), + randomHashSet, + Equal.equals + ) + }) + + describe("readonlyArray", () => { + diffLaws( + Differ.readonlyArray number>(Differ.update()), + randomReadonlyArray, + Arr.getEquivalence(Equal.equals) + ) + }) + + describe("tuple", () => { + diffLaws( + Differ.update() + .pipe( + Differ.zip(Differ.update()) + ), + randomPair, + (a, b) => Equal.equals(a[0], b[0]) && Equal.equals(a[1], b[1]) + ) + }) +}) diff --git a/repos/effect/packages/effect/test/Duration.test.ts b/repos/effect/packages/effect/test/Duration.test.ts new file mode 100644 index 0000000..5250e6b --- /dev/null +++ b/repos/effect/packages/effect/test/Duration.test.ts @@ -0,0 +1,628 @@ +import { describe, it } from "@effect/vitest" +import { + assertFalse, + assertNone, + assertSome, + assertTrue, + deepStrictEqual, + strictEqual, + throws +} from "@effect/vitest/utils" +import { Duration, Equal, pipe } from "effect" + +describe("Duration", () => { + it("decode", () => { + const millis100 = Duration.millis(100) + assertTrue(Duration.decode(millis100) === millis100) + + deepStrictEqual(Duration.decode(100), millis100) + + deepStrictEqual(Duration.decode(10n), Duration.nanos(10n)) + + deepStrictEqual(Duration.decode("1 nano"), Duration.nanos(1n)) + deepStrictEqual(Duration.decode("10 nanos"), Duration.nanos(10n)) + deepStrictEqual(Duration.decode("1 micro"), Duration.micros(1n)) + deepStrictEqual(Duration.decode("10 micros"), Duration.micros(10n)) + deepStrictEqual(Duration.decode("1 milli"), Duration.millis(1)) + deepStrictEqual(Duration.decode("10 millis"), Duration.millis(10)) + deepStrictEqual(Duration.decode("1 second"), Duration.seconds(1)) + deepStrictEqual(Duration.decode("10 seconds"), Duration.seconds(10)) + deepStrictEqual(Duration.decode("1 minute"), Duration.minutes(1)) + deepStrictEqual(Duration.decode("10 minutes"), Duration.minutes(10)) + deepStrictEqual(Duration.decode("1 hour"), Duration.hours(1)) + deepStrictEqual(Duration.decode("10 hours"), Duration.hours(10)) + deepStrictEqual(Duration.decode("1 day"), Duration.days(1)) + deepStrictEqual(Duration.decode("10 days"), Duration.days(10)) + deepStrictEqual(Duration.decode("1 week"), Duration.weeks(1)) + deepStrictEqual(Duration.decode("10 weeks"), Duration.weeks(10)) + + deepStrictEqual(Duration.decode("1.5 seconds"), Duration.seconds(1.5)) + deepStrictEqual(Duration.decode("-1.5 seconds"), Duration.zero) + + deepStrictEqual(Duration.decode([500, 123456789]), Duration.nanos(500123456789n)) + deepStrictEqual(Duration.decode([-500, 123456789]), Duration.zero) + deepStrictEqual(Duration.decode([Infinity, 0]), Duration.infinity) + deepStrictEqual(Duration.decode([-Infinity, 0]), Duration.zero) + deepStrictEqual(Duration.decode([NaN, 0]), Duration.zero) + deepStrictEqual(Duration.decode([0, Infinity]), Duration.infinity) + deepStrictEqual(Duration.decode([0, -Infinity]), Duration.zero) + deepStrictEqual(Duration.decode([0, NaN]), Duration.zero) + throws(() => Duration.decode("1.5 secs" as any), new Error("Invalid DurationInput")) + throws(() => Duration.decode(true as any), new Error("Invalid DurationInput")) + throws(() => Duration.decode({} as any), new Error("Invalid DurationInput")) + }) + + it("decodeUnknown", () => { + const millis100 = Duration.millis(100) + assertSome(Duration.decodeUnknown(millis100), millis100) + + assertSome(Duration.decodeUnknown(100), millis100) + + assertSome(Duration.decodeUnknown(10n), Duration.nanos(10n)) + + assertSome(Duration.decodeUnknown("1 nano"), Duration.nanos(1n)) + assertSome(Duration.decodeUnknown("10 nanos"), Duration.nanos(10n)) + assertSome(Duration.decodeUnknown("1 micro"), Duration.micros(1n)) + assertSome(Duration.decodeUnknown("10 micros"), Duration.micros(10n)) + assertSome(Duration.decodeUnknown("1 milli"), Duration.millis(1)) + assertSome(Duration.decodeUnknown("10 millis"), Duration.millis(10)) + assertSome(Duration.decodeUnknown("1 second"), Duration.seconds(1)) + assertSome(Duration.decodeUnknown("10 seconds"), Duration.seconds(10)) + assertSome(Duration.decodeUnknown("1 minute"), Duration.minutes(1)) + assertSome(Duration.decodeUnknown("10 minutes"), Duration.minutes(10)) + assertSome(Duration.decodeUnknown("1 hour"), Duration.hours(1)) + assertSome(Duration.decodeUnknown("10 hours"), Duration.hours(10)) + assertSome(Duration.decodeUnknown("1 day"), Duration.days(1)) + assertSome(Duration.decodeUnknown("10 days"), Duration.days(10)) + assertSome(Duration.decodeUnknown("1 week"), Duration.weeks(1)) + assertSome(Duration.decodeUnknown("10 weeks"), Duration.weeks(10)) + + assertSome(Duration.decodeUnknown("1.5 seconds"), Duration.seconds(1.5)) + assertSome(Duration.decodeUnknown("-1.5 seconds"), Duration.zero) + + assertSome(Duration.decodeUnknown([500, 123456789]), Duration.nanos(500123456789n)) + assertSome(Duration.decodeUnknown([-500, 123456789]), Duration.zero) + assertSome(Duration.decodeUnknown([Infinity, 0]), Duration.infinity) + assertSome(Duration.decodeUnknown([-Infinity, 0]), Duration.zero) + assertSome(Duration.decodeUnknown([NaN, 0]), Duration.zero) + assertSome(Duration.decodeUnknown([0, Infinity]), Duration.infinity) + assertSome(Duration.decodeUnknown([0, -Infinity]), Duration.zero) + assertSome(Duration.decodeUnknown([0, NaN]), Duration.zero) + assertNone(Duration.decodeUnknown("1.5 secs")) + assertNone(Duration.decodeUnknown(true)) + assertNone(Duration.decodeUnknown({})) + }) + + it("Order", () => { + deepStrictEqual(Duration.Order(Duration.millis(1), Duration.millis(2)), -1) + deepStrictEqual(Duration.Order(Duration.millis(2), Duration.millis(1)), 1) + deepStrictEqual(Duration.Order(Duration.millis(2), Duration.millis(2)), 0) + + deepStrictEqual(Duration.Order(Duration.nanos(1n), Duration.nanos(2n)), -1) + deepStrictEqual(Duration.Order(Duration.nanos(2n), Duration.nanos(1n)), 1) + deepStrictEqual(Duration.Order(Duration.nanos(2n), Duration.nanos(2n)), 0) + }) + + it("Equivalence", () => { + deepStrictEqual(Duration.Equivalence(Duration.millis(1), Duration.millis(1)), true) + deepStrictEqual(Duration.Equivalence(Duration.millis(1), Duration.millis(2)), false) + deepStrictEqual(Duration.Equivalence(Duration.millis(1), Duration.millis(2)), false) + + deepStrictEqual(Duration.Equivalence(Duration.nanos(1n), Duration.nanos(1n)), true) + deepStrictEqual(Duration.Equivalence(Duration.nanos(1n), Duration.nanos(2n)), false) + deepStrictEqual(Duration.Equivalence(Duration.nanos(1n), Duration.nanos(2n)), false) + }) + + it("max", () => { + deepStrictEqual(Duration.max(Duration.millis(1), Duration.millis(2)), Duration.millis(2)) + deepStrictEqual(Duration.max(Duration.minutes(1), Duration.millis(2)), Duration.minutes(1)) + + deepStrictEqual(Duration.max("1 minutes", "2 millis"), Duration.minutes(1)) + }) + + it("min", () => { + deepStrictEqual(Duration.min(Duration.millis(1), Duration.millis(2)), Duration.millis(1)) + deepStrictEqual(Duration.min(Duration.minutes(1), Duration.millis(2)), Duration.millis(2)) + + deepStrictEqual(Duration.min("1 minutes", "2 millis"), Duration.millis(2)) + }) + + it("clamp", () => { + deepStrictEqual( + Duration.clamp(Duration.millis(1), { + minimum: Duration.millis(2), + maximum: Duration.millis(3) + }), + Duration.millis(2) + ) + deepStrictEqual( + Duration.clamp(Duration.minutes(1.5), { + minimum: Duration.minutes(1), + maximum: Duration.minutes(2) + }), + Duration.minutes(1.5) + ) + + deepStrictEqual( + Duration.clamp("1 millis", { + minimum: "2 millis", + maximum: "3 millis" + }), + Duration.millis(2) + ) + }) + + it("equals", () => { + assertTrue(pipe(Duration.hours(1), Duration.equals(Duration.minutes(60)))) + assertTrue(Duration.equals("2 seconds", "2 seconds")) + assertFalse(Duration.equals("2 seconds", "3 seconds")) + }) + + it("between", () => { + assertTrue(Duration.between(Duration.hours(1), { + minimum: Duration.minutes(59), + maximum: Duration.minutes(61) + })) + assertTrue( + Duration.between(Duration.minutes(1), { + minimum: Duration.seconds(59), + maximum: Duration.seconds(61) + }) + ) + + assertTrue(Duration.between("1 minutes", { + minimum: "59 seconds", + maximum: "61 seconds" + })) + }) + + it("divide", () => { + assertSome(Duration.divide(Duration.minutes(1), 2), Duration.seconds(30)) + assertSome(Duration.divide(Duration.seconds(1), 3), Duration.nanos(333333333n)) + assertSome(Duration.divide(Duration.nanos(2n), 2), Duration.nanos(1n)) + assertSome(Duration.divide(Duration.nanos(1n), 3), Duration.zero) + assertSome(Duration.divide(Duration.infinity, 2), Duration.infinity) + assertSome(Duration.divide(Duration.zero, 2), Duration.zero) + assertNone(Duration.divide(Duration.minutes(1), 0)) + assertNone(Duration.divide(Duration.minutes(1), -0)) + assertNone(Duration.divide(Duration.nanos(1n), 0)) + assertNone(Duration.divide(Duration.nanos(1n), -0)) + assertSome(Duration.divide(Duration.minutes(1), 0.5), Duration.minutes(2)) + assertSome(Duration.divide(Duration.minutes(1), 1.5), Duration.seconds(40)) + assertNone(Duration.divide(Duration.minutes(1), NaN)) + assertNone(Duration.divide(Duration.nanos(1n), 0.5)) + assertNone(Duration.divide(Duration.nanos(1n), 1.5)) + assertNone(Duration.divide(Duration.nanos(1n), NaN)) + + assertSome(Duration.divide("1 minute", 2), Duration.seconds(30)) + }) + + it("unsafeDivide", () => { + deepStrictEqual(Duration.unsafeDivide(Duration.minutes(1), 2), Duration.seconds(30)) + deepStrictEqual(Duration.unsafeDivide(Duration.seconds(1), 3), Duration.nanos(333333333n)) + deepStrictEqual(Duration.unsafeDivide(Duration.nanos(2n), 2), Duration.nanos(1n)) + deepStrictEqual(Duration.unsafeDivide(Duration.nanos(1n), 3), Duration.zero) + deepStrictEqual(Duration.unsafeDivide(Duration.infinity, 2), Duration.infinity) + deepStrictEqual(Duration.unsafeDivide(Duration.zero, 2), Duration.zero) + deepStrictEqual(Duration.unsafeDivide(Duration.minutes(1), 0), Duration.infinity) + deepStrictEqual(Duration.unsafeDivide(Duration.minutes(1), -0), Duration.zero) + deepStrictEqual(Duration.unsafeDivide(Duration.nanos(1n), 0), Duration.infinity) + deepStrictEqual(Duration.unsafeDivide(Duration.nanos(1n), -0), Duration.zero) + deepStrictEqual(Duration.unsafeDivide(Duration.minutes(1), 0.5), Duration.minutes(2)) + deepStrictEqual(Duration.unsafeDivide(Duration.minutes(1), 1.5), Duration.seconds(40)) + deepStrictEqual(Duration.unsafeDivide(Duration.minutes(1), NaN), Duration.zero) + throws(() => Duration.unsafeDivide(Duration.nanos(1n), 0.5)) + throws(() => Duration.unsafeDivide(Duration.nanos(1n), 1.5)) + deepStrictEqual(Duration.unsafeDivide(Duration.nanos(1n), NaN), Duration.zero) + + deepStrictEqual(Duration.unsafeDivide("1 minute", 2), Duration.seconds(30)) + }) + + it("times", () => { + deepStrictEqual(Duration.times(Duration.seconds(1), 60), Duration.minutes(1)) + deepStrictEqual(Duration.times(Duration.nanos(2n), 10), Duration.nanos(20n)) + deepStrictEqual(Duration.times(Duration.seconds(Infinity), 60), Duration.seconds(Infinity)) + + deepStrictEqual(Duration.times("1 seconds", 60), Duration.minutes(1)) + }) + + it("sum", () => { + deepStrictEqual(Duration.sum(Duration.seconds(30), Duration.seconds(30)), Duration.minutes(1)) + deepStrictEqual(Duration.sum(Duration.nanos(30n), Duration.nanos(30n)), Duration.nanos(60n)) + deepStrictEqual(Duration.sum(Duration.seconds(Infinity), Duration.seconds(30)), Duration.seconds(Infinity)) + deepStrictEqual(Duration.sum(Duration.seconds(30), Duration.seconds(Infinity)), Duration.seconds(Infinity)) + + deepStrictEqual(Duration.sum("30 seconds", "30 seconds"), Duration.minutes(1)) + }) + + it("subtract", () => { + deepStrictEqual(Duration.subtract(Duration.seconds(30), Duration.seconds(10)), Duration.seconds(20)) + deepStrictEqual(Duration.subtract(Duration.seconds(30), Duration.seconds(30)), Duration.zero) + deepStrictEqual(Duration.subtract(Duration.nanos(30n), Duration.nanos(10n)), Duration.nanos(20n)) + deepStrictEqual(Duration.subtract(Duration.nanos(30n), Duration.nanos(30n)), Duration.zero) + deepStrictEqual(Duration.subtract(Duration.seconds(Infinity), Duration.seconds(30)), Duration.seconds(Infinity)) + deepStrictEqual(Duration.subtract(Duration.seconds(30), Duration.seconds(Infinity)), Duration.zero) + + deepStrictEqual(Duration.subtract("30 seconds", "10 seconds"), Duration.seconds(20)) + }) + + it("greaterThan", () => { + assertTrue(pipe(Duration.seconds(30), Duration.greaterThan(Duration.seconds(20)))) + assertFalse(pipe(Duration.seconds(30), Duration.greaterThan(Duration.seconds(30)))) + assertFalse(pipe(Duration.seconds(30), Duration.greaterThan(Duration.seconds(60)))) + + assertTrue(pipe(Duration.nanos(30n), Duration.greaterThan(Duration.nanos(20n)))) + assertFalse(pipe(Duration.nanos(30n), Duration.greaterThan(Duration.nanos(30n)))) + assertFalse(pipe(Duration.nanos(30n), Duration.greaterThan(Duration.nanos(60n)))) + + assertTrue(pipe(Duration.millis(1), Duration.greaterThan(Duration.nanos(1n)))) + + assertFalse(Duration.greaterThan("2 seconds", "2 seconds")) + assertTrue(Duration.greaterThan("3 seconds", "2 seconds")) + assertFalse(Duration.greaterThan("2 seconds", "3 seconds")) + }) + + it("greaterThan - Infinity", () => { + assertTrue(pipe(Duration.infinity, Duration.greaterThan(Duration.seconds(20)))) + assertFalse(pipe(Duration.seconds(-Infinity), Duration.greaterThan(Duration.infinity))) + assertFalse(pipe(Duration.nanos(1n), Duration.greaterThan(Duration.infinity))) + }) + + it("greaterThanOrEqualTo", () => { + assertTrue(pipe(Duration.seconds(30), Duration.greaterThanOrEqualTo(Duration.seconds(20)))) + assertTrue(pipe(Duration.seconds(30), Duration.greaterThanOrEqualTo(Duration.seconds(30)))) + assertFalse(pipe(Duration.seconds(30), Duration.greaterThanOrEqualTo(Duration.seconds(60)))) + + assertTrue(pipe(Duration.nanos(30n), Duration.greaterThanOrEqualTo(Duration.nanos(20n)))) + assertTrue(pipe(Duration.nanos(30n), Duration.greaterThanOrEqualTo(Duration.nanos(30n)))) + assertFalse(pipe(Duration.nanos(30n), Duration.greaterThanOrEqualTo(Duration.nanos(60n)))) + + assertTrue(Duration.greaterThanOrEqualTo("2 seconds", "2 seconds")) + assertTrue(Duration.greaterThanOrEqualTo("3 seconds", "2 seconds")) + assertFalse(Duration.greaterThanOrEqualTo("2 seconds", "3 seconds")) + }) + + it("lessThan", () => { + assertTrue(pipe(Duration.seconds(20), Duration.lessThan(Duration.seconds(30)))) + assertFalse(pipe(Duration.seconds(30), Duration.lessThan(Duration.seconds(30)))) + assertFalse(pipe(Duration.seconds(60), Duration.lessThan(Duration.seconds(30)))) + + assertTrue(pipe(Duration.nanos(20n), Duration.lessThan(Duration.nanos(30n)))) + assertFalse(pipe(Duration.nanos(30n), Duration.lessThan(Duration.nanos(30n)))) + assertFalse(pipe(Duration.nanos(60n), Duration.lessThan(Duration.nanos(30n)))) + + assertTrue(pipe(Duration.nanos(1n), Duration.lessThan(Duration.millis(1)))) + + assertFalse(Duration.lessThan("2 seconds", "2 seconds")) + assertFalse(Duration.lessThan("3 seconds", "2 seconds")) + assertTrue(Duration.lessThan("2 seconds", "3 seconds")) + }) + + it("lessThanOrEqualTo", () => { + assertTrue(pipe(Duration.seconds(20), Duration.lessThanOrEqualTo(Duration.seconds(30)))) + assertTrue(pipe(Duration.seconds(30), Duration.lessThanOrEqualTo(Duration.seconds(30)))) + assertFalse(pipe(Duration.seconds(60), Duration.lessThanOrEqualTo(Duration.seconds(30)))) + + assertTrue(pipe(Duration.nanos(20n), Duration.lessThanOrEqualTo(Duration.nanos(30n)))) + assertTrue(pipe(Duration.nanos(30n), Duration.lessThanOrEqualTo(Duration.nanos(30n)))) + assertFalse(pipe(Duration.nanos(60n), Duration.lessThanOrEqualTo(Duration.nanos(30n)))) + + assertTrue(Duration.lessThanOrEqualTo("2 seconds", "2 seconds")) + assertFalse(Duration.lessThanOrEqualTo("3 seconds", "2 seconds")) + assertTrue(Duration.lessThanOrEqualTo("2 seconds", "3 seconds")) + }) + + it("String()", () => { + strictEqual(String(Duration.infinity), `Duration(Infinity)`) + strictEqual(String(Duration.nanos(10n)), `Duration(10ns)`) + strictEqual(String(Duration.millis(2)), `Duration(2ms)`) + strictEqual(String(Duration.millis(2.125)), `Duration(2ms 125000ns)`) + strictEqual(String(Duration.seconds(2)), `Duration(2s)`) + strictEqual(String(Duration.seconds(2.5)), `Duration(2s 500ms)`) + }) + + it("format", () => { + strictEqual(Duration.format(Duration.infinity), `Infinity`) + strictEqual(Duration.format(Duration.minutes(5)), `5m`) + strictEqual(Duration.format(Duration.minutes(5.325)), `5m 19s 500ms`) + strictEqual(Duration.format(Duration.hours(3)), `3h`) + strictEqual(Duration.format(Duration.hours(3.11125)), `3h 6m 40s 500ms`) + strictEqual(Duration.format(Duration.days(2)), `2d`) + strictEqual(Duration.format(Duration.days(2.25)), `2d 6h`) + strictEqual(Duration.format(Duration.weeks(1)), `7d`) + strictEqual(Duration.format(Duration.zero), `0`) + }) + + it("format", () => { + deepStrictEqual(Duration.parts(Duration.infinity), { + days: Infinity, + hours: Infinity, + minutes: Infinity, + seconds: Infinity, + millis: Infinity, + nanos: Infinity + }) + + deepStrictEqual(Duration.parts(Duration.minutes(5.325)), { + days: 0, + hours: 0, + minutes: 5, + seconds: 19, + millis: 500, + nanos: 0 + }) + + deepStrictEqual(Duration.parts(Duration.minutes(3.11125)), { + days: 0, + hours: 0, + minutes: 3, + seconds: 6, + millis: 675, + nanos: 0 + }) + }) + + it("toJSON", () => { + deepStrictEqual(Duration.seconds(2).toJSON(), { _id: "Duration", _tag: "Millis", millis: 2000 }) + }) + + it("toJSON/ non-integer millis", () => { + deepStrictEqual(Duration.millis(1.5).toJSON(), { _id: "Duration", _tag: "Nanos", hrtime: [0, 1_500_000] }) + }) + + it("toJSON/ nanos", () => { + deepStrictEqual(Duration.nanos(5n).toJSON(), { _id: "Duration", _tag: "Nanos", hrtime: [0, 5] }) + }) + + it("toJSON/ infinity", () => { + deepStrictEqual(Duration.infinity.toJSON(), { _id: "Duration", _tag: "Infinity" }) + }) + + it(`inspect`, () => { + if (typeof window === "undefined") { + // eslint-disable-next-line @typescript-eslint/no-require-imports + const { inspect } = require("node:util") + deepStrictEqual(inspect(Duration.millis(1000)), inspect({ _id: "Duration", _tag: "Millis", millis: 1000 })) + } + }) + + it("sum/ Infinity", () => { + deepStrictEqual(Duration.sum(Duration.seconds(1), Duration.infinity), Duration.infinity) + }) + + it(".pipe()", () => { + deepStrictEqual(Duration.seconds(1).pipe(Duration.sum(Duration.seconds(1))), Duration.seconds(2)) + }) + + it("isDuration", () => { + assertTrue(Duration.isDuration(Duration.millis(100))) + assertFalse(Duration.isDuration(null)) + }) + + it("zero", () => { + deepStrictEqual(Duration.zero.value, { _tag: "Millis", millis: 0 }) + }) + + it("infinity", () => { + deepStrictEqual(Duration.infinity.value, { _tag: "Infinity" }) + }) + + it("weeks", () => { + assertTrue(Equal.equals(Duration.weeks(1), Duration.days(7))) + assertFalse(Equal.equals(Duration.weeks(1), Duration.days(1))) + }) + + it("toMillis", () => { + strictEqual(Duration.millis(1).pipe(Duration.toMillis), 1) + strictEqual(Duration.nanos(1n).pipe(Duration.toMillis), 0.000001) + strictEqual(Duration.infinity.pipe(Duration.toMillis), Infinity) + + strictEqual(Duration.toMillis("1 millis"), 1) + }) + + it("toSeconds", () => { + strictEqual(Duration.millis(1).pipe(Duration.toSeconds), 0.001) + strictEqual(Duration.nanos(1n).pipe(Duration.toSeconds), 1e-9) + strictEqual(Duration.infinity.pipe(Duration.toSeconds), Infinity) + + strictEqual(Duration.toSeconds("1 seconds"), 1) + strictEqual(Duration.toSeconds("3 seconds"), 3) + strictEqual(Duration.toSeconds("3 minutes"), 180) + }) + + it("toNanos", () => { + assertSome(Duration.nanos(1n).pipe(Duration.toNanos), 1n) + assertNone(Duration.infinity.pipe(Duration.toNanos)) + assertSome(Duration.millis(1.0005).pipe(Duration.toNanos), 1_000_500n) + assertSome(Duration.millis(100).pipe(Duration.toNanos), 100_000_000n) + + assertSome(Duration.toNanos("1 nanos"), 1n) + }) + + it("unsafeToNanos", () => { + strictEqual(Duration.nanos(1n).pipe(Duration.unsafeToNanos), 1n) + throws(() => Duration.infinity.pipe(Duration.unsafeToNanos)) + strictEqual(Duration.millis(1.0005).pipe(Duration.unsafeToNanos), 1_000_500n) + strictEqual(Duration.millis(100).pipe(Duration.unsafeToNanos), 100_000_000n) + + strictEqual(Duration.unsafeToNanos("1 nanos"), 1n) + }) + + it("toHrTime", () => { + deepStrictEqual(Duration.millis(1).pipe(Duration.toHrTime), [0, 1_000_000]) + deepStrictEqual(Duration.nanos(1n).pipe(Duration.toHrTime), [0, 1]) + deepStrictEqual(Duration.nanos(1_000_000_001n).pipe(Duration.toHrTime), [1, 1]) + deepStrictEqual(Duration.millis(1001).pipe(Duration.toHrTime), [1, 1_000_000]) + deepStrictEqual(Duration.infinity.pipe(Duration.toHrTime), [Infinity, 0]) + + deepStrictEqual(Duration.toHrTime("1 millis"), [0, 1_000_000]) + }) + + it("floor is 0", () => { + deepStrictEqual(Duration.millis(-1), Duration.zero) + deepStrictEqual(Duration.nanos(-1n), Duration.zero) + }) + + it("match", () => { + const match = Duration.match({ + onMillis: () => "millis", + onNanos: () => "nanos" + }) + strictEqual(match(Duration.decode("100 millis")), "millis") + strictEqual(match(Duration.decode("10 nanos")), "nanos") + strictEqual(match(Duration.decode(Infinity)), "millis") + + strictEqual(match("100 millis"), "millis") + }) + + it("isFinite", () => { + assertTrue(Duration.isFinite(Duration.millis(100))) + assertTrue(Duration.isFinite(Duration.nanos(100n))) + assertFalse(Duration.isFinite(Duration.infinity)) + }) + + it("isZero", () => { + assertTrue(Duration.isZero(Duration.zero)) + assertTrue(Duration.isZero(Duration.millis(0))) + assertTrue(Duration.isZero(Duration.nanos(0n))) + assertFalse(Duration.isZero(Duration.infinity)) + assertFalse(Duration.isZero(Duration.millis(1))) + assertFalse(Duration.isZero(Duration.nanos(1n))) + }) + + it("toMinutes", () => { + strictEqual(Duration.millis(60000).pipe(Duration.toMinutes), 1) + strictEqual(Duration.nanos(60000000000n).pipe(Duration.toMinutes), 1) + strictEqual(Duration.infinity.pipe(Duration.toMinutes), Infinity) + + strictEqual(Duration.toMinutes("1 minute"), 1) + strictEqual(Duration.toMinutes("2 minutes"), 2) + strictEqual(Duration.toMinutes("1 hour"), 60) + }) + + it("toHours", () => { + strictEqual(Duration.millis(3_600_000).pipe(Duration.toHours), 1) + strictEqual(Duration.nanos(3_600_000_000_000n).pipe(Duration.toHours), 1) + strictEqual(Duration.infinity.pipe(Duration.toHours), Infinity) + + strictEqual(Duration.toHours("1 hour"), 1) + strictEqual(Duration.toHours("2 hours"), 2) + strictEqual(Duration.toHours("1 day"), 24) + }) + + it("toDays", () => { + strictEqual(Duration.millis(86_400_000).pipe(Duration.toDays), 1) + strictEqual(Duration.nanos(86_400_000_000_000n).pipe(Duration.toDays), 1) + strictEqual(Duration.infinity.pipe(Duration.toDays), Infinity) + + strictEqual(Duration.toDays("1 day"), 1) + strictEqual(Duration.toDays("2 days"), 2) + strictEqual(Duration.toDays("1 week"), 7) + }) + + it("toWeeks", () => { + strictEqual(Duration.millis(604_800_000).pipe(Duration.toWeeks), 1) + strictEqual(Duration.nanos(604_800_000_000_000n).pipe(Duration.toWeeks), 1) + strictEqual(Duration.infinity.pipe(Duration.toWeeks), Infinity) + + strictEqual(Duration.toWeeks("1 week"), 1) + strictEqual(Duration.toWeeks("2 weeks"), 2) + strictEqual(Duration.toWeeks("14 days"), 2) + }) + + it("formatIso", () => { + assertSome(Duration.formatIso(Duration.zero), "PT0S") + assertSome(Duration.formatIso(Duration.seconds(2)), "PT2S") + assertSome(Duration.formatIso(Duration.minutes(5)), "PT5M") + assertSome(Duration.formatIso(Duration.hours(3)), "PT3H") + assertSome(Duration.formatIso(Duration.days(1)), "P1D") + + assertSome(Duration.formatIso(Duration.minutes(90)), "PT1H30M") + assertSome(Duration.formatIso(Duration.hours(25)), "P1DT1H") + assertSome(Duration.formatIso(Duration.days(7)), "P1W") + assertSome(Duration.formatIso(Duration.days(10)), "P1W3D") + + assertSome(Duration.formatIso(Duration.millis(1500)), "PT1.5S") + assertSome(Duration.formatIso(Duration.micros(1500n)), "PT0.0015S") + assertSome(Duration.formatIso(Duration.nanos(1500n)), "PT0.0000015S") + + assertSome( + Duration.formatIso( + Duration.seconds( + 365 * 24 * 60 * 60 + // 1 year + 60 * 24 * 60 * 60 + // 2 months + 3 * 24 * 60 * 60 + // 3 days + 4 * 60 * 60 + // 4 hours + 5 * 60 + // 5 minutes + 6.789 // 6.789 seconds + ) + ), + "P1Y2M3DT4H5M6.789S" + ) + + assertSome( + Duration.formatIso( + Duration.days(1).pipe( + Duration.sum(Duration.hours(2)), + Duration.sum(Duration.minutes(30)) + ) + ), + "P1DT2H30M" + ) + + assertSome( + Duration.formatIso( + Duration.hours(2).pipe( + Duration.sum(Duration.minutes(30)), + Duration.sum(Duration.millis(1500)) + ) + ), + "PT2H30M1.5S" + ) + + assertSome(Duration.formatIso("1 day"), "P1D") + assertSome(Duration.formatIso("90 minutes"), "PT1H30M") + assertSome(Duration.formatIso("1.5 seconds"), "PT1.5S") + + assertNone(Duration.formatIso(Duration.infinity)) + }) + + it("fromIso", () => { + assertSome(Duration.fromIso("P1D"), Duration.days(1)) + assertSome(Duration.fromIso("PT1H"), Duration.hours(1)) + assertSome(Duration.fromIso("PT1M"), Duration.minutes(1)) + assertSome(Duration.fromIso("PT1.5S"), Duration.seconds(1.5)) + assertSome(Duration.fromIso("P1Y"), Duration.days(365)) + assertSome(Duration.fromIso("P1M"), Duration.days(30)) + assertSome(Duration.fromIso("P1W"), Duration.days(7)) + assertSome(Duration.fromIso("P1DT12H"), Duration.hours(36)) + assertSome( + Duration.fromIso("P1Y2M3DT4H5M6.789S"), + Duration.seconds( + 365 * 24 * 60 * 60 + // 1 year + 60 * 24 * 60 * 60 + // 2 months + 3 * 24 * 60 * 60 + // 3 days + 4 * 60 * 60 + // 4 hours + 5 * 60 + // 5 minutes + 6.789 // 6.789 seconds + ) + ) + + assertNone(Duration.fromIso("1D")) + assertNone(Duration.fromIso("P1H")) + assertNone(Duration.fromIso("PT1D")) + assertNone(Duration.fromIso("P1.5D")) + assertNone(Duration.fromIso("P1.5Y")) + assertNone(Duration.fromIso("P1.5M")) + assertNone(Duration.fromIso("PT1.5H")) + assertNone(Duration.fromIso("PT1.5M")) + assertNone(Duration.fromIso("PDT1H")) + assertNone(Duration.fromIso("P1D2H")) + assertNone(Duration.fromIso("P")) + assertNone(Duration.fromIso("PT")) + assertNone(Duration.fromIso("random string")) + assertNone(Duration.fromIso("P1YT")) + assertNone(Duration.fromIso("P1S")) + assertNone(Duration.fromIso("P1DT1S1H")) + }) +}) diff --git a/repos/effect/packages/effect/test/Effect/acquire-release.test.ts b/repos/effect/packages/effect/test/Effect/acquire-release.test.ts new file mode 100644 index 0000000..575b892 --- /dev/null +++ b/repos/effect/packages/effect/test/Effect/acquire-release.test.ts @@ -0,0 +1,111 @@ +import { describe, it } from "@effect/vitest" +import { assertTrue, strictEqual } from "@effect/vitest/utils" +import * as Cause from "effect/Cause" +import * as Chunk from "effect/Chunk" +import * as Effect from "effect/Effect" +import { equals } from "effect/Equal" +import * as Exit from "effect/Exit" +import { pipe } from "effect/Function" +import * as Ref from "effect/Ref" + +describe("Effect", () => { + it.effect("acquireUseRelease - happy path", () => + Effect.gen(function*() { + const release = yield* (Ref.make(false)) + const result = yield* ( + Effect.acquireUseRelease( + Effect.succeed(42), + (n) => Effect.succeed(n + 1), + () => Ref.set(release, true) + ) + ) + const released = yield* (Ref.get(release)) + strictEqual(result, 43) + assertTrue(released) + })) + it.effect("acquireUseRelease - happy path + disconnect", () => + Effect.gen(function*() { + const release = yield* (Ref.make(false)) + const result = yield* pipe( + Effect.acquireUseRelease( + Effect.succeed(42), + (n) => Effect.succeed(n + 1), + () => Ref.set(release, true) + ), + Effect.disconnect + ) + const released = yield* (Ref.get(release)) + strictEqual(result, 43) + assertTrue(released) + })) + it.effect("acquireUseRelease - error handling", () => + Effect.gen(function*() { + const releaseDied = new Cause.RuntimeException("release died") + const exit = yield* pipe( + Effect.acquireUseRelease( + Effect.succeed(42), + () => Effect.fail("use failed"), + () => Effect.die(releaseDied) + ), + Effect.exit + ) + const result = yield* pipe( + exit, + Exit.matchEffect({ onFailure: Effect.succeed, onSuccess: () => Effect.fail("effect should have failed") }) + ) + assertTrue(equals(Cause.failures(result), Chunk.of("use failed"))) + assertTrue(equals(Cause.defects(result), Chunk.of(releaseDied))) + })) + it.effect("acquireUseRelease - error handling + disconnect", () => + Effect.gen(function*() { + const releaseDied = new Cause.RuntimeException("release died") + const exit = yield* pipe( + Effect.acquireUseRelease( + Effect.succeed(42), + () => Effect.fail("use failed"), + () => Effect.die(releaseDied) + ), + Effect.disconnect, + Effect.exit + ) + const result = yield* pipe( + exit, + Exit.matchEffect({ + onFailure: Effect.succeed, + onSuccess: () => Effect.fail("effect should have failed") + }) + ) + assertTrue(equals(Cause.failures(result), Chunk.of("use failed"))) + assertTrue(equals(Cause.defects(result), Chunk.of(releaseDied))) + })) + it.effect("acquireUseRelease - beast mode error handling + disconnect", () => + Effect.gen(function*() { + const useDied = new Cause.RuntimeException("use died") + const release = yield* (Ref.make(false)) + const exit = yield* ( + pipe( + Effect.acquireUseRelease( + Effect.succeed(42), + (): Effect.Effect => { + throw useDied + }, + () => Ref.set(release, true) + ), + Effect.disconnect, + Effect.exit + ) + ) + const result = yield* ( + pipe( + exit, + Exit.matchEffect({ + onFailure: Effect.succeed, + onSuccess: () => Effect.fail("effect should have failed") + }) + ) + ) + const released = yield* (Ref.get(release)) + assertTrue(equals(Cause.defects(result), Chunk.of(useDied))) + assertTrue(released) + })) +}) diff --git a/repos/effect/packages/effect/test/Effect/applicative.test.ts b/repos/effect/packages/effect/test/Effect/applicative.test.ts new file mode 100644 index 0000000..6ee0835 --- /dev/null +++ b/repos/effect/packages/effect/test/Effect/applicative.test.ts @@ -0,0 +1,43 @@ +import { describe, it } from "@effect/vitest" +import { assertLeft, strictEqual } from "@effect/vitest/utils" +import { Effect, pipe } from "effect" + +describe("Effect", () => { + const add = (a: number) => (b: number) => a + b + + it.effect("two successes should succeed", () => + Effect.gen(function*() { + const result = yield* (Effect.succeed(add).pipe(Effect.ap(Effect.succeed(1)), Effect.ap(Effect.succeed(2)))) + strictEqual(result, 3) + })) + + it.effect("one failure in data-last position should fail", () => + Effect.gen(function*() { + const result = yield* pipe( + Effect.succeed(add).pipe(Effect.ap(Effect.succeed(1)), Effect.ap(Effect.fail("c"))), + Effect.either + ) + assertLeft(result, "c") + })) + + it.effect("one failure in data-first position should fail", () => + Effect.gen(function*() { + const result = yield* pipe( + Effect.succeed(add).pipe(Effect.ap(Effect.fail("b")), Effect.ap(Effect.fail("c"))), + Effect.either + ) + assertLeft(result, "b") + })) + + it.effect("an applicative operation that starts with a failure should fail", () => + Effect.gen(function*() { + const result = yield* pipe( + (Effect.fail("a") as Effect.Effect).pipe( + Effect.ap(Effect.succeed(1)), + Effect.ap(Effect.succeed(2)) + ), + Effect.either + ) + assertLeft(result, "a") + })) +}) diff --git a/repos/effect/packages/effect/test/Effect/async.test.ts b/repos/effect/packages/effect/test/Effect/async.test.ts new file mode 100644 index 0000000..aef1762 --- /dev/null +++ b/repos/effect/packages/effect/test/Effect/async.test.ts @@ -0,0 +1,176 @@ +import { describe, it } from "@effect/vitest" +import { assertNone, assertSome, deepStrictEqual, strictEqual } from "@effect/vitest/utils" +import * as Cause from "effect/Cause" +import * as Chunk from "effect/Chunk" +import * as Deferred from "effect/Deferred" +import * as Duration from "effect/Duration" +import * as Effect from "effect/Effect" +import * as Exit from "effect/Exit" +import * as Fiber from "effect/Fiber" +import { pipe } from "effect/Function" +import * as Option from "effect/Option" +import * as Ref from "effect/Ref" +import * as Runtime from "effect/Runtime" + +describe("Effect", () => { + it.effect("simple async must return", () => + Effect.gen(function*() { + const result = yield* (Effect.async((cb) => { + cb(Effect.succeed(42)) + })) + strictEqual(result, 42) + })) + it.effect("simple asyncEffect must return", () => + Effect.gen(function*() { + const result = yield* (Effect.asyncEffect((resume) => { + return Effect.succeed(resume(Effect.succeed(42))) + })) + strictEqual(result, 42) + })) + if (typeof window === "undefined") { + // eslint-disable-next-line @typescript-eslint/no-require-imports + const os = require("node:os") + it.effect("deep asyncEffect doesn't block", () => + Effect.gen(function*() { + const asyncIO = (cont: Effect.Effect): Effect.Effect => { + return Effect.asyncEffect((cb) => { + return pipe( + Effect.sleep(Duration.millis(5)), + Effect.zipRight(cont), + Effect.zipRight(Effect.succeed(cb(Effect.succeed(42)))) + ) + }) + } + const stackIOs = (count: number): Effect.Effect => { + return count < 0 ? Effect.succeed(42) : asyncIO(stackIOs(count - 1)) + } + const procNum = Effect.sync(() => os.cpus().length) + const result = yield* pipe(procNum, Effect.flatMap(stackIOs)) + strictEqual(result, 42) + })) + } + it.effect("interrupt of asyncEffect register", () => + Effect.gen(function*() { + const release = yield* (Deferred.make()) + const acquire = yield* (Deferred.make()) + const fiber = yield* pipe( + Effect.asyncEffect(() => + // This will never complete because we never call the callback + Effect.acquireUseRelease( + Deferred.succeed(acquire, void 0), + () => Effect.never, + () => Deferred.succeed(release, void 0) + ) + ), + Effect.disconnect, + Effect.fork + ) + + yield* (Deferred.await(acquire)) + yield* (Fiber.interruptFork(fiber)) + const result = yield* (Deferred.await(release)) + strictEqual(result, undefined) + })) + it.live("async should not resume fiber twice after interruption", () => + Effect.gen(function*() { + const step = yield* (Deferred.make()) + const unexpectedPlace = yield* (Ref.make(Chunk.empty())) + const runtime = yield* (Effect.runtime()) + const fiber = yield* pipe( + Effect.async((cb) => { + Runtime.runCallback(runtime)(pipe( + Deferred.await(step), + Effect.zipRight(Effect.sync(() => cb(Ref.update(unexpectedPlace, Chunk.prepend(1))))) + )) + }), + Effect.ensuring(Effect.async(() => { + // The callback is never called so this never completes + Runtime.runCallback(runtime)(Deferred.succeed(step, undefined)) + })), + Effect.ensuring(Ref.update(unexpectedPlace, Chunk.prepend(2))), + Effect.forkDaemon + ) + const result = yield* pipe(Fiber.interrupt(fiber), Effect.timeout(Duration.seconds(1)), Effect.option) + const unexpected = yield* (Ref.get(unexpectedPlace)) + deepStrictEqual(unexpected, Chunk.empty()) + assertNone(result) // the timeout should happen + })) + it.live("async should not resume fiber twice after synchronous result", () => + Effect.gen(function*() { + const step = yield* (Deferred.make()) + const unexpectedPlace = yield* (Ref.make(Chunk.empty())) + const runtime = yield* (Effect.runtime()) + const fiber = yield* pipe( + Effect.async((resume) => { + Runtime.runCallback(runtime)(pipe( + Deferred.await(step), + Effect.zipRight(Effect.sync(() => resume(Ref.update(unexpectedPlace, Chunk.prepend(1))))) + )) + return Effect.void + }), + Effect.flatMap(() => + Effect.async(() => { + // The callback is never called so this never completes + Runtime.runCallback(runtime)(Deferred.succeed(step, void 0)) + }) + ), + Effect.ensuring(Ref.update(unexpectedPlace, Chunk.prepend(2))), + Effect.uninterruptible, + Effect.forkDaemon + ) + const result = yield* pipe(Fiber.interrupt(fiber), Effect.timeout(Duration.seconds(1)), Effect.option) + const unexpected = yield* (Ref.get(unexpectedPlace)) + deepStrictEqual(unexpected, Chunk.empty()) + assertNone(result) // timeout should happen + })) + it.effect("sleep 0 must return", () => + Effect.gen(function*() { + const result = yield* (Effect.sleep(Duration.zero)) + strictEqual(result, undefined) + })) + it.effect("shallow bind of async chain", () => + Effect.gen(function*() { + const array = Array.from({ length: 10 }, (_, i) => i) + const result = yield* (array.reduce((acc, _) => + pipe( + acc, + Effect.flatMap((n) => + Effect.async((cb) => { + cb(Effect.succeed(n + 1)) + }) + ) + ), Effect.succeed(0))) + strictEqual(result, 10) + })) + it.effect("asyncEffect can fail before registering", () => + Effect.gen(function*() { + const result = yield* pipe( + Effect.asyncEffect((_) => { + return Effect.fail("ouch") + }), + Effect.flip + ) + strictEqual(result, "ouch") + })) + it.effect("asyncEffect can defect before registering", () => + Effect.gen(function*() { + const result = yield* pipe( + Effect.asyncEffect((_) => + Effect.sync(() => { + throw new Error("ouch") + }) + ), + Effect.exit, + Effect.map(Exit.match({ + onFailure: (cause) => + pipe( + Cause.defects(cause), + Chunk.head, + Option.map((e) => (e as Error).message) + ), + onSuccess: () => Option.none() + })) + ) + assertSome(result, "ouch") + })) +}) diff --git a/repos/effect/packages/effect/test/Effect/caching.test.ts b/repos/effect/packages/effect/test/Effect/caching.test.ts new file mode 100644 index 0000000..c204b62 --- /dev/null +++ b/repos/effect/packages/effect/test/Effect/caching.test.ts @@ -0,0 +1,65 @@ +import { describe, it } from "@effect/vitest" +import { assertTrue, strictEqual } from "@effect/vitest/utils" +import * as Duration from "effect/Duration" +import * as Effect from "effect/Effect" +import { pipe } from "effect/Function" +import * as Ref from "effect/Ref" +import * as TestClock from "effect/TestClock" + +describe("Effect", () => { + it.effect("cached - returns new instances after duration", () => + Effect.gen(function*() { + const ref = yield* (Ref.make(0)) + const cache = yield* pipe( + Ref.updateAndGet(ref, (n) => n + 1), + Effect.cachedWithTTL(Duration.minutes(60)) + ) + const a = yield* cache + yield* (TestClock.adjust(Duration.minutes(59))) + const b = yield* cache + yield* (TestClock.adjust(Duration.minutes(1))) + const c = yield* cache + yield* (TestClock.adjust(Duration.minutes(59))) + const d = yield* cache + strictEqual(a, b) + assertTrue(b !== c) + strictEqual(c, d) + })) + it.effect("cached - correctly handles an infinite duration time to live", () => + Effect.gen(function*() { + const ref = yield* (Ref.make(0)) + const cached = yield* pipe( + Ref.modify(ref, (curr) => [curr, curr + 1]), + Effect.cachedWithTTL(Duration.infinity) + ) + const a = yield* cached + const b = yield* cached + const c = yield* cached + strictEqual(a, 0) + strictEqual(b, 0) + strictEqual(c, 0) + })) + it.effect("cachedInvalidate - returns new instances after duration", () => + Effect.gen(function*() { + const ref = yield* (Ref.make(0)) + const [cached, invalidate] = yield* ( + pipe( + Ref.updateAndGet(ref, (n) => n + 1), + Effect.cachedInvalidateWithTTL(Duration.minutes(60)) + ) + ) + const a = yield* cached + yield* (TestClock.adjust(Duration.minutes(59))) + const b = yield* cached + yield* invalidate + const c = yield* cached + yield* (TestClock.adjust(Duration.minutes(1))) + const d = yield* cached + yield* (TestClock.adjust(Duration.minutes(59))) + const e = yield* cached + strictEqual(a, b) + assertTrue(b !== c) + strictEqual(c, d) + assertTrue(d !== e) + })) +}) diff --git a/repos/effect/packages/effect/test/Effect/cause-rendering.test.ts b/repos/effect/packages/effect/test/Effect/cause-rendering.test.ts new file mode 100644 index 0000000..52eb0c5 --- /dev/null +++ b/repos/effect/packages/effect/test/Effect/cause-rendering.test.ts @@ -0,0 +1,177 @@ +import { describe, expect, it } from "@effect/vitest" +import { assertFalse, assertInclude, assertInstanceOf, assertTrue, strictEqual } from "@effect/vitest/utils" +import { Cause, Effect, Option, pipe } from "effect" + +describe("Effect", () => { + it.effect("Cause should include span data", () => + Effect.gen(function*() { + const cause = yield* (Effect.flip(Effect.sandbox( + Effect.withSpan("spanB")( + Effect.withSpan("spanA")( + Effect.fail(new Error("ok")) + ) + ) + ))) + const rendered = Cause.pretty(cause) + assertInclude(rendered, "spanA") + assertInclude(rendered, "cause-rendering.test.ts:10:18") + assertInclude(rendered, "spanB") + assertInclude(rendered, "cause-rendering.test.ts:9:16") + })) + it.effect("catchTag should not invalidate traces", () => + Effect.gen(function*() { + class E1 { + readonly _tag = "E1" + } + class E2 { + readonly _tag = "E2" + } + const err = new E1() + const effect = Effect.withSpan("spanB")( + Effect.withSpan("spanA")( + Effect.if(Effect.sync(() => Math.random() > 1), { + onTrue: () => Effect.fail(new E2()), + onFalse: () => Effect.fail(err) + }) + ) + ).pipe(Effect.catchTag("E2", (e) => Effect.die(e))) + const cause = yield* (Effect.flip(Effect.sandbox(effect))) + const rendered = Cause.pretty(cause) + assertInclude(rendered, "spanA") + assertInclude(rendered, "spanB") + const obj = Option.getOrThrow(Cause.failureOption(cause)) + assertInstanceOf(obj, E1) + assertFalse(err === obj) + assertTrue(err === Cause.originalError(obj)) + })) + it.effect("refail should not invalidate traces", () => + Effect.gen(function*() { + class E1 { + readonly _tag = "E1" + } + class E2 { + readonly _tag = "E2" + } + const effect = Effect.withSpan("spanB")( + Effect.withSpan("spanA")( + Effect.if(Effect.sync(() => Math.random() > 1), { + onTrue: () => Effect.fail(new E2()), + onFalse: () => Effect.fail(new E1()) + }) + ) + ).pipe(Effect.catchAll((e) => Effect.fail(e))) + const cause = yield* (Effect.flip(Effect.sandbox(effect))) + const rendered = Cause.pretty(cause) + assertInclude(rendered, "spanA") + assertInclude(rendered, "spanB") + })) + it.effect("catchTags should not invalidate traces", () => + Effect.gen(function*() { + class E1 { + readonly _tag = "E1" + } + class E2 { + readonly _tag = "E2" + } + const effect = Effect.withSpan("spanB")( + Effect.withSpan("spanA")( + Effect.if(Effect.sync(() => Math.random() > 1), { + onTrue: () => Effect.fail(new E2()), + onFalse: () => Effect.fail(new E1()) + }) + ) + ).pipe(Effect.catchTags({ E2: (e) => Effect.die(e) })) + const cause = yield* (Effect.flip(Effect.sandbox(effect))) + const rendered = Cause.pretty(cause) + assertInclude(rendered, "spanA") + assertInclude(rendered, "spanB") + })) + it.effect("shows line where error was created", () => + Effect.gen(function*() { + const cause = yield* pipe( + Effect.sync(() => { + throw new Error("ok") + }), + Effect.sandbox, + Effect.flip + ) + const pretty = Cause.pretty(cause) + assertInclude(pretty, "cause-rendering.test.ts") + })) + + it.effect("functionWithSpan PrettyError stack", () => + Effect.gen(function*() { + const fail = Effect.functionWithSpan({ + body: (_id: number) => Effect.fail(new Error("boom")), + options: (id) => ({ name: `span-${id}` }) + }) + const cause = yield* fail(123).pipe(Effect.sandbox, Effect.flip) + const prettyErrors = Cause.prettyErrors(cause) + strictEqual(prettyErrors.length, 1) + const error = prettyErrors[0] + strictEqual(error.name, "Error") + assertInclude(error.stack, "cause-rendering.test.ts:105") + assertInclude(error.stack, "span-123") + assertInclude(error.stack, "cause-rendering.test.ts:108") + })) + + it.effect("includes span name in stack", () => + Effect.gen(function*() { + const fn = Effect.functionWithSpan({ + options: (n) => ({ name: `fn-${n}` }), + body: (a: number) => + Effect.sync(() => { + strictEqual(a, 2) + }) + }) + const cause = yield* fn(0).pipe( + Effect.sandbox, + Effect.flip + ) + const prettyErrors = Cause.prettyErrors(cause) + assertInclude(prettyErrors[0].stack, "at fn-0 ") + })) + + // ENABLE TO TEST EXPECT OUTPUT + it.effect.skip("shows assertion message", () => + Effect.gen(function*() { + yield* Effect.void + expect({ foo: "ok" }).toStrictEqual({ foo: "bar" }) + })) + + it.effect("multiline message", () => + Effect.gen(function*() { + const cause = yield* Effect.fail(new Error("Multi-line\nerror\nmessage")).pipe( + Effect.sandbox, + Effect.flip + ) + const pretty = Cause.pretty(cause) + assertTrue(pretty.startsWith(`Error: Multi-line +error +message + at`)) + })) + + it.effect("pretty includes error.cause with renderErrorCause: true", () => + Effect.gen(function*() { + const cause = yield* Effect.fail(new Error("parent", { cause: new Error("child") })).pipe( + Effect.sandbox, + Effect.flip + ) + const pretty = Cause.pretty(cause, { renderErrorCause: true }) + assertInclude(pretty, "[cause]: Error: child") + })) + + it.effect("pretty nested cause", () => + Effect.gen(function*() { + const cause = yield* Effect.fail( + new Error("parent", { cause: new Error("child", { cause: new Error("child2") }) }) + ).pipe( + Effect.sandbox, + Effect.flip + ) + const pretty = Cause.pretty(cause, { renderErrorCause: true }) + assertInclude(pretty, "[cause]: Error: child") + assertInclude(pretty, "[cause]: Error: child2") + })) +}) diff --git a/repos/effect/packages/effect/test/Effect/collecting.test.ts b/repos/effect/packages/effect/test/Effect/collecting.test.ts new file mode 100644 index 0000000..194ee2f --- /dev/null +++ b/repos/effect/packages/effect/test/Effect/collecting.test.ts @@ -0,0 +1,46 @@ +import { describe, it } from "@effect/vitest" +import { deepStrictEqual, notDeepStrictEqual } from "@effect/vitest/utils" +import { Cause, Effect, pipe, Ref } from "effect" + +describe("Effect", () => { + describe("all", () => { + describe("returns results in the same order", () => { + it.effect("unbounded", () => + Effect.gen(function*() { + const result = yield* (Effect.all([1, 2, 3].map(Effect.succeed), { + concurrency: "unbounded" + })) + deepStrictEqual(Array.from(result), [1, 2, 3]) + })) + + it.effect("concurrency > 1", () => + Effect.gen(function*() { + const result = yield* (Effect.all([1, 2, 3].map(Effect.succeed), { + concurrency: 2 + })) + deepStrictEqual(Array.from(result), [1, 2, 3]) + })) + }) + + it.effect("is referentially transparent", () => + Effect.gen(function*() { + const counter = yield* (Ref.make(0)) + const op = Ref.getAndUpdate(counter, (n) => n + 1) + const ops3 = Effect.all([op, op, op], { concurrency: "unbounded" }) + const result = yield* pipe(ops3, Effect.zip(ops3, { concurrent: true })) + notDeepStrictEqual(Array.from(result[0]), Array.from(result[1])) + })) + + it.effect("preserves failures", () => + Effect.gen(function*() { + const result = yield* pipe( + Effect.all(Array.from({ length: 10 }, () => Effect.fail(new Cause.RuntimeException())), { + concurrency: 5, + discard: true + }), + Effect.flip + ) + deepStrictEqual(result, new Cause.RuntimeException()) + })) + }) +}) diff --git a/repos/effect/packages/effect/test/Effect/concurrency.test.ts b/repos/effect/packages/effect/test/Effect/concurrency.test.ts new file mode 100644 index 0000000..d10959c --- /dev/null +++ b/repos/effect/packages/effect/test/Effect/concurrency.test.ts @@ -0,0 +1,333 @@ +import { describe, it } from "@effect/vitest" +import { assertFalse, assertLeft, assertRight, assertTrue, deepStrictEqual, strictEqual } from "@effect/vitest/utils" +import * as Deferred from "effect/Deferred" +import * as Duration from "effect/Duration" +import * as Effect from "effect/Effect" +import * as Either from "effect/Either" +import * as Exit from "effect/Exit" +import * as Fiber from "effect/Fiber" +import { pipe } from "effect/Function" +import * as Ref from "effect/Ref" +import { adjust } from "effect/TestClock" +import { withLatch } from "../utils/latch.js" + +export const ExampleError = new Error("Oh noes!") + +const fib = (n: number): number => { + if (n <= 1) { + return n + } + return fib(n - 1) + fib(n - 2) +} + +const concurrentFib = (n: number): Effect.Effect => { + if (n <= 1) { + return Effect.succeed(n) + } + return Effect.gen(function*() { + const fiber1 = yield* (Effect.fork(concurrentFib(n - 1))) + const fiber2 = yield* (Effect.fork(concurrentFib(n - 2))) + const v1 = yield* (Fiber.join(fiber1)) + const v2 = yield* (Fiber.join(fiber2)) + return v1 + v2 + }) +} + +describe("Effect", () => { + it.effect("shallow fork/join identity", () => + Effect.gen(function*() { + const result = yield* pipe(Effect.succeed(42), Effect.fork, Effect.flatMap(Fiber.join)) + strictEqual(result, 42) + })) + it.effect("deep fork/join identity", () => + Effect.gen(function*() { + const result = yield* (concurrentFib(20)) + strictEqual(result, fib(20)) + })) + it.effect("asyncEffect creation is interruptible", () => + Effect.gen(function*() { + const release = yield* (Deferred.make()) + const acquire = yield* (Deferred.make()) + const fiber = yield* pipe( + Effect.asyncEffect((_) => + // This will never complete because the callback is never invoked + Effect.acquireUseRelease( + Deferred.succeed(acquire, void 0), + () => Effect.never, + () => Effect.asVoid(Deferred.succeed(release, 42)) + ) + ), + Effect.fork + ) + yield* (Deferred.await(acquire)) + yield* (Fiber.interrupt(fiber)) + const result = yield* (Deferred.await(release)) + strictEqual(result, 42) + })) + it.effect("daemon fiber is unsupervised", () => + Effect.gen(function*() { + const child = (ref: Ref.Ref) => { + return withLatch((release) => + pipe( + release, + Effect.zipRight(Effect.never), + Effect.ensuring(Ref.set(ref, true)) + ) + ) + } + const ref = yield* (Ref.make(false)) + const fiber1 = yield* pipe(child(ref), Effect.forkDaemon, Effect.fork) + const fiber2 = yield* (Fiber.join(fiber1)) + const result = yield* (Ref.get(ref)) + yield* (Fiber.interrupt(fiber2)) + assertFalse(result) + })) + it.effect("daemon fiber race interruption", () => + Effect.gen(function*() { + const plus1 = (latch: Deferred.Deferred, finalizer: Effect.Effect) => { + return pipe( + Deferred.succeed(latch, void 0), + Effect.zipRight(Effect.sleep(Duration.hours(1))), + Effect.onInterrupt(() => pipe(finalizer, Effect.map((x) => x))) + ) + } + const interruptionRef = yield* (Ref.make(0)) + const latch1Start = yield* (Deferred.make()) + const latch2Start = yield* (Deferred.make()) + const inc = Ref.updateAndGet(interruptionRef, (n) => n + 1) + const left = plus1(latch1Start, inc) + const right = plus1(latch2Start, inc) + const fiber = yield* pipe(left, Effect.race(right), Effect.fork) + yield* ( + pipe( + Deferred.await(latch1Start), + Effect.zipRight(Deferred.await(latch2Start)), + Effect.zipRight(Fiber.interrupt(fiber)) + ) + ) + const result = yield* (Ref.get(interruptionRef)) + strictEqual(result, 2) + })) + it.effect("race in daemon is executed", () => + Effect.gen(function*() { + const latch1 = yield* (Deferred.make()) + const latch2 = yield* (Deferred.make()) + const deferred1 = yield* (Deferred.make()) + const deferred2 = yield* (Deferred.make()) + const loser1 = Effect.acquireUseRelease( + Deferred.succeed(latch1, void 0), + () => Effect.never, + () => Deferred.succeed(deferred1, void 0) + ) + const loser2 = Effect.acquireUseRelease( + Deferred.succeed(latch2, void 0), + () => Effect.never, + () => Deferred.succeed(deferred2, void 0) + ) + const fiber = yield* pipe(loser1, Effect.race(loser2), Effect.forkDaemon) + yield* (Deferred.await(latch1)) + yield* (Deferred.await(latch2)) + yield* (Fiber.interrupt(fiber)) + const res1 = yield* (Deferred.await(deferred1)) + const res2 = yield* (Deferred.await(deferred2)) + strictEqual(res1, undefined) + strictEqual(res2, undefined) + })) + it.live("supervise fibers", () => + Effect.gen(function*() { + const makeChild = (n: number): Effect.Effect> => { + return pipe(Effect.sleep(Duration.millis(20 * n)), Effect.zipRight(Effect.never), Effect.fork) + } + const ref = yield* (Ref.make(0)) + yield* pipe( + makeChild(1), + Effect.zipRight(makeChild(2)), + Effect.ensuringChildren((fs) => + Array.from(fs).reduce( + (acc, fiber) => + pipe( + acc, + Effect.zipRight(Fiber.interrupt(fiber)), + Effect.zipRight(Ref.update(ref, (n) => n + 1)) + ), + Effect.void + ) + ) + ) + const result = yield* (Ref.get(ref)) + strictEqual(result, 2) + })) + it.effect("race of fail with success", () => + Effect.gen(function*() { + const result = yield* pipe(Effect.fail(42), Effect.race(Effect.succeed(24)), Effect.either) + assertRight(result, 24) + })) + it.effect("race of terminate with success", () => + Effect.gen(function*() { + const result = yield* pipe(Effect.dieSync(() => new Error()), Effect.race(Effect.succeed(24))) + strictEqual(result, 24) + })) + it.effect("race of fail with fail", () => + Effect.gen(function*() { + const result = yield* pipe(Effect.fail(42), Effect.race(Effect.fail(24)), Effect.either) + assertLeft(result, 42) + })) + it.effect("race of value and never", () => + Effect.gen(function*() { + const result = yield* pipe(Effect.succeed(42), Effect.race(Effect.never)) + strictEqual(result, 42) + })) + it.effect("race in uninterruptible region", () => + Effect.gen(function*() { + const latch = yield* (Deferred.make()) + const fiber = yield* pipe( + Effect.void, + Effect.race(Effect.zip(Deferred.succeed(latch, true), Effect.sleep("45 seconds"))), + Effect.uninterruptible, + Effect.fork + ) + yield* (Deferred.await(latch)) + yield* (adjust("30 seconds")) + strictEqual(fiber.unsafePoll(), null) + yield* (adjust("60 seconds")) + assertTrue(fiber.unsafePoll() !== null) + }), 20_000) + it.effect("race of two forks does not interrupt winner", () => + Effect.gen(function*() { + const forkWaiter = ( + interrupted: Ref.Ref, + latch: Deferred.Deferred, + done: Deferred.Deferred + ) => { + return Effect.uninterruptibleMask((restore) => + pipe( + restore(Deferred.await(latch)), + Effect.onInterrupt(() => + pipe(Ref.update(interrupted, (_) => _ + 1), Effect.zipRight(Deferred.succeed(done, void 0))) + ), + Effect.fork + ) + ) + } + const interrupted = yield* (Ref.make(0)) + const latch1 = yield* (Deferred.make()) + const latch2 = yield* (Deferred.make()) + const done1 = yield* (Deferred.make()) + const done2 = yield* (Deferred.make()) + const forkWaiter1 = forkWaiter(interrupted, latch1, done1) + const forkWaiter2 = forkWaiter(interrupted, latch2, done2) + yield* pipe(forkWaiter1, Effect.race(forkWaiter2)) + const count = yield* ( + pipe( + Deferred.succeed(latch1, void 0), + Effect.zipRight(Deferred.await(done1)), + Effect.zipRight(Deferred.await(done2)), + Effect.zipRight(Ref.get(interrupted)) + ) + ) + strictEqual(count, 2) + })) + it.effect("firstSuccessOf of values", () => + Effect.gen(function*() { + const result = yield* pipe( + Effect.firstSuccessOf([ + Effect.fail(0), + Effect.succeed(100) + ]), + Effect.either + ) + assertRight(result, 100) + })) + it.live("firstSuccessOf of failures", () => + Effect.gen(function*() { + const result = yield* pipe( + Effect.firstSuccessOf([ + pipe(Effect.fail(0), Effect.delay(Duration.millis(10))), + Effect.fail(101) + ]), + Effect.either + ) + + assertLeft(result, 101) + })) + it.live("firstSuccessOf of failures & 1 success", () => + Effect.gen(function*() { + const result = yield* pipe( + Effect.firstSuccessOf([ + Effect.fail(0), + pipe(Effect.succeed(102), Effect.delay(Duration.millis(1))) + ]), + Effect.either + ) + assertRight(result, 102) + })) + it.effect("raceFirst interrupts loser on success", () => + Effect.gen(function*() { + const deferred = yield* (Deferred.make()) + const effect = yield* (Deferred.make()) + const winner = Either.right(void 0) + const loser = Effect.acquireUseRelease( + Deferred.succeed(deferred, void 0), + () => Effect.never, + () => Deferred.succeed(effect, 42) + ) + yield* pipe(winner, Effect.raceFirst(loser)) + const result = yield* (Deferred.await(effect)) + strictEqual(result, 42) + })) + it.effect("raceFirst interrupts loser on failure", () => + Effect.gen(function*() { + const deferred = yield* (Deferred.make()) + const effect = yield* (Deferred.make()) + const winner = pipe(Deferred.await(deferred), Effect.zipRight(Either.left(new Error()))) + const loser = Effect.acquireUseRelease( + Deferred.succeed(deferred, void 0), + () => Effect.never, + () => Deferred.succeed(effect, 42) + ) + yield* pipe(winner, Effect.raceFirst(loser), Effect.either) + const result = yield* (Deferred.await(effect)) + strictEqual(result, 42) + })) + it.effect("mergeAll", () => + Effect.gen(function*() { + const result = yield* ( + pipe(["a", "aa", "aaa", "aaaa"].map((a) => Effect.succeed(a)), Effect.mergeAll(0, (b, a) => b + a.length)) + ) + strictEqual(result, 10) + })) + it.effect("mergeAll - empty", () => + Effect.gen(function*() { + const result = yield* ( + pipe([] as ReadonlyArray>, Effect.mergeAll(0, (b, a) => b + a)) + ) + strictEqual(result, 0) + })) + it.effect("reduceEffect", () => + Effect.gen(function*() { + const result = yield* ( + pipe([2, 3, 4].map((n) => Effect.succeed(n)), Effect.reduceEffect(Effect.succeed(1), (acc, a) => acc + a)) + ) + strictEqual(result, 10) + })) + it.effect("reduceEffect - empty list", () => + Effect.gen(function*() { + const result = yield* ( + pipe( + [] as ReadonlyArray>, + Effect.reduceEffect(Effect.succeed(1), (acc, a) => acc + a) + ) + ) + strictEqual(result, 1) + })) + it.effect("timeout of failure", () => + Effect.gen(function*() { + const result = yield* pipe(Effect.fail("uh oh"), Effect.timeout(Duration.hours(1)), Effect.exit) + deepStrictEqual(result, Exit.fail("uh oh")) + })) + it.effect("timeout of terminate", () => + Effect.gen(function*() { + const result = yield* pipe(Effect.die(ExampleError), Effect.timeout(Duration.hours(1)), Effect.exit) + deepStrictEqual(result, Exit.die(ExampleError)) + })) +}) diff --git a/repos/effect/packages/effect/test/Effect/constructors.test.ts b/repos/effect/packages/effect/test/Effect/constructors.test.ts new file mode 100644 index 0000000..1520c34 --- /dev/null +++ b/repos/effect/packages/effect/test/Effect/constructors.test.ts @@ -0,0 +1,45 @@ +import { describe, it } from "@effect/vitest" +import { assertNone, assertSome, strictEqual } from "@effect/vitest/utils" +import * as Effect from "effect/Effect" + +describe("Effect", () => { + it.effect("can lift a value to an option", () => + Effect.gen(function*() { + const result = yield* Effect.succeedSome(42) + assertSome(result, 42) + })) + it.effect("using the none value", () => + Effect.gen(function*() { + const result = yield* Effect.succeedNone + assertNone(result) + })) + it.effect("can use .pipe for composition", () => + Effect.gen(function*() { + return yield* Effect.succeed(1) + }).pipe( + Effect.map((n) => n + 1), + Effect.flatMap((n) => + Effect.gen(function*() { + return yield* Effect.succeed(n + 1) + }) + ), + Effect.tap((n) => + Effect.sync(() => { + strictEqual(n, 3) + }) + ) + )) + it.effect("can pass this to generator", () => { + class MyService { + readonly local = 1 + compute = Effect.gen(this, function*() { + return yield* Effect.succeed(this.local + 1) + }) + } + const instance = new MyService() + + return Effect.map(instance.compute, (n) => { + strictEqual(n, 2) + }) + }) +}) diff --git a/repos/effect/packages/effect/test/Effect/destructors.test.ts b/repos/effect/packages/effect/test/Effect/destructors.test.ts new file mode 100644 index 0000000..b64c654 --- /dev/null +++ b/repos/effect/packages/effect/test/Effect/destructors.test.ts @@ -0,0 +1,88 @@ +import { describe, it } from "@effect/vitest" +import { + assertFailure, + assertFalse, + assertLeft, + assertNone, + assertRight, + assertSome, + assertTrue, + strictEqual +} from "@effect/vitest/utils" +import { Cause, Effect, Option, pipe } from "effect" + +const ExampleError = new Error("Oh noes!") + +describe("Effect", () => { + it.effect("head - on non empty list", () => + Effect.gen(function*() { + const result = yield* pipe(Effect.succeed([1, 2, 3]), Effect.head, Effect.either) + assertRight(result, 1) + })) + it.effect("head - on empty list", () => + Effect.gen(function*() { + const result = yield* pipe(Effect.succeed([] as ReadonlyArray), Effect.head, Effect.option) + assertNone(result) + })) + it.effect("head - on failure", () => + Effect.gen(function*() { + const result = yield* pipe(Effect.fail("fail"), Effect.head, Effect.either) + assertLeft(result, "fail") + })) + it.effect("isFailure - returns true when the effect is a failure", () => + Effect.gen(function*() { + const result = yield* (Effect.isFailure(Effect.fail("fail"))) + assertTrue(result) + })) + it.effect("isFailure - returns false when the effect is a success", () => + Effect.gen(function*() { + const result = yield* (Effect.isFailure(Effect.succeed("succeed"))) + assertFalse(result) + })) + it.effect("isSuccess - returns false when the effect is a failure", () => + Effect.gen(function*() { + const result = yield* (Effect.isSuccess(Effect.fail("fail"))) + assertFalse(result) + })) + it.effect("isSuccess - returns true when the effect is a success", () => + Effect.gen(function*() { + const result = yield* (Effect.isSuccess(Effect.succeed("succeed"))) + assertTrue(result) + })) + it.effect("none - on Some fails with NoSuchElementException", () => + Effect.gen(function*() { + const result = yield* (Effect.exit(Effect.none(Effect.succeed(Option.some(1))))) + assertFailure(result, Cause.fail(new Cause.NoSuchElementException())) + })) + it.effect("none - on None succeeds with undefined", () => + Effect.gen(function*() { + const result = yield* (Effect.none(Effect.succeed(Option.none()))) + strictEqual(result, undefined) + })) + it.effect("none - fails with ex when effect fails with ex", () => + Effect.gen(function*() { + const error = new Cause.RuntimeException("failed task") + const result = yield* (Effect.exit(Effect.none(Effect.fail(error)))) + assertFailure(result, Cause.fail(error)) + })) + it.effect("option - return success in Some", () => + Effect.gen(function*() { + const result = yield* (Effect.option(Effect.succeed(11))) + assertSome(result, 11) + })) + it.effect("option - return failure as None", () => + Effect.gen(function*() { + const result = yield* (Effect.option(Effect.fail(123))) + assertNone(result) + })) + it.effect("option - not catch throwable", () => + Effect.gen(function*() { + const result = yield* (Effect.exit(Effect.option(Effect.die(ExampleError)))) + assertFailure(result, Cause.die(ExampleError)) + })) + it.effect("option - catch throwable after sandboxing", () => + Effect.gen(function*() { + const result = yield* (Effect.option(Effect.sandbox(Effect.die(ExampleError)))) + assertNone(result) + })) +}) diff --git a/repos/effect/packages/effect/test/Effect/do-notation.test.ts b/repos/effect/packages/effect/test/Effect/do-notation.test.ts new file mode 100644 index 0000000..2368f70 --- /dev/null +++ b/repos/effect/packages/effect/test/Effect/do-notation.test.ts @@ -0,0 +1,128 @@ +import { describe, it } from "@effect/vitest" +import * as Util from "@effect/vitest/utils" +import * as Effect from "effect/Effect" +import * as Either from "effect/Either" +import { pipe } from "effect/Function" +import * as Option from "effect/Option" +import type { NoExcessProperties } from "effect/Types" + +const expectRight = (e: Effect.Effect, expected: R) => { + Util.deepStrictEqual(Effect.runSync(Effect.either(e)), Either.right(expected)) +} + +const expectLeft = (e: Effect.Effect, expected: L) => { + Util.deepStrictEqual(Effect.runSync(Effect.either(e)), Either.left(expected)) +} + +describe("do notation", () => { + it("Do", () => { + expectRight(Effect.Do, {}) + }) + + it("bindTo", () => { + expectRight(pipe(Effect.succeed(1), Effect.bindTo("a")), { a: 1 }) + expectLeft(pipe(Effect.fail("left"), Effect.bindTo("a")), "left") + expectRight( + pipe( + Effect.succeed(1), + Effect.bindTo("__proto__"), + Effect.bind("x", () => Effect.succeed(2)) + ), + { x: 2, ["__proto__"]: 1 } + ) + }) + + it("bind", () => { + expectRight(pipe(Effect.succeed(1), Effect.bindTo("a"), Effect.bind("b", ({ a }) => Effect.succeed(a + 1))), { + a: 1, + b: 2 + }) + expectLeft( + pipe(Effect.succeed(1), Effect.bindTo("a"), Effect.bind("b", () => Effect.fail("left"))), + "left" + ) + expectLeft( + pipe(Effect.fail("left"), Effect.bindTo("a"), Effect.bind("b", () => Effect.succeed(2))), + "left" + ) + expectRight( + pipe( + Effect.Do, + Effect.bind("__proto__", () => Effect.succeed(1)), + Effect.bind("b", ({ __proto__ }) => Effect.succeed(2)) + ), + { b: 2, ["__proto__"]: 1 } + ) + }) + + it("let", () => { + expectRight(pipe(Effect.succeed(1), Effect.bindTo("a"), Effect.let("b", ({ a }) => a + 1)), { a: 1, b: 2 }) + expectLeft( + pipe(Effect.fail("left"), Effect.bindTo("a"), Effect.let("b", () => 2)), + "left" + ) + expectRight( + pipe( + Effect.succeed(1), + Effect.bindTo("a"), + Effect.let("__proto__", ({ a }) => a + 1), + Effect.bind("x", () => Effect.succeed(3)) + ), + { a: 1, x: 3, ["__proto__"]: 2 } + ) + }) + + describe("bindAll", () => { + it("succeed", () => { + const getTest = >(options: O) => + Effect.Do.pipe( + Effect.bind("x", () => Effect.succeed(2)), + Effect.bindAll(({ x }) => ({ + a: Effect.succeed(x), + b: Effect.succeed("ops") + }), options) + ) + + expectRight(getTest({ mode: "default" }), { + a: 2, + b: "ops", + x: 2 + }) + + expectRight(getTest({ mode: "either" }), { + a: Either.right(2), + b: Either.right("ops"), + x: 2 + }) + + expectRight(getTest({ mode: "validate" }), { + a: 2, + b: "ops", + x: 2 + }) + }) + + it("with failure", () => { + const getTest = >(options: O) => + Effect.Do.pipe( + Effect.bind("x", () => Effect.succeed(2)), + Effect.bindAll(({ x }) => ({ + a: Effect.fail(x), // <-- fail + b: Effect.succeed("ops") + }), options) + ) + + expectLeft(getTest({ mode: "default" }), 2) + expectRight(getTest({ mode: "either" }), { + a: Either.left(2), + b: Either.right("ops"), + x: 2 + }) + + expectLeft(getTest({ mode: "validate" }), { + a: Option.some(2), + b: Option.none() + }) + }) + }) +}) diff --git a/repos/effect/packages/effect/test/Effect/environment.test.ts b/repos/effect/packages/effect/test/Effect/environment.test.ts new file mode 100644 index 0000000..c5d51ff --- /dev/null +++ b/repos/effect/packages/effect/test/Effect/environment.test.ts @@ -0,0 +1,249 @@ +import { describe, it } from "@effect/vitest" +import { deepStrictEqual, strictEqual } from "@effect/vitest/utils" +import * as Context from "effect/Context" +import * as Effect from "effect/Effect" +import { pipe } from "effect/Function" +import * as Layer from "effect/Layer" + +interface NumberService { + readonly n: number +} + +const NumberService = Context.GenericTag("NumberService") + +interface StringService { + readonly s: string +} + +const StringService = Context.GenericTag("StringService") + +class NumberRepo extends Context.Tag("NumberRepo") +}>() { + static numbers = Effect.serviceConstants(NumberRepo).numbers +} + +class DemoTag extends Effect.Tag("DemoTag") Array + readonly strings: Array + readonly fn: (...args: ReadonlyArray) => Array + readonly fnParamsUnion: ( + ...args: ReadonlyArray | [number] | [false, true] + ) => ReadonlyArray | [number] | [false, true] + readonly fnGen: (s: S) => Array +}>() { +} + +class DateTag extends Effect.Tag("DateTag")() { + static date = new Date(1970, 1, 1) + static Live = Layer.succeed(this, this.date) +} + +class MapTag extends Effect.Tag("MapTag")>() { + static Live = Layer.effect(this, Effect.sync(() => new Map())) +} + +class NumberTag extends Effect.Tag("NumberTag")() { + static Live = Layer.succeed(this, 100) +} + +describe("Effect", () => { + it.effect("provide runtime is additive", () => + Effect.gen(function*() { + const runtime = yield* Effect.runtime() + const env = yield* NumberService.pipe( + Effect.provide(runtime), + Effect.provideService(NumberService, { n: 1 }) + ) + deepStrictEqual(env, { n: 1 }) + })) + describe("and Then", () => { + it.effect("effect tag", () => + Effect.gen(function*() { + const [n, s, z] = yield* (Effect.all([ + Effect.andThen(Effect.void, DemoTag.getNumbers), + Effect.andThen(Effect.succeed("a"), DemoTag.strings), + Effect.andThen(Effect.succeed("a"), DemoTag.fn) + ])) + deepStrictEqual(n, [0, 1]) + deepStrictEqual(s, ["a", "b"]) + deepStrictEqual(z, ["a"]) + }).pipe(Effect.provideService(DemoTag, { + getNumbers: () => [0, 1], + strings: ["a", "b"], + fn: (...args) => Array.from(args), + fnGen: (s) => [s], + fnParamsUnion: (..._args) => _args + }))) + }) + it.effect("effect tag", () => + Effect.gen(function*() { + const [n, s, z, zUnion] = yield* (Effect.all([ + DemoTag.getNumbers(), + DemoTag.strings, + DemoTag.fn("a", "b", "c"), + DemoTag.fnParamsUnion(1) + ])) + const s2 = yield* (DemoTag.pipe(Effect.map((_) => _.strings))) + const s3 = yield* (DemoTag.use((_) => _.fnGen("hello"))) + deepStrictEqual(n, [0, 1]) + deepStrictEqual(s, ["a", "b"]) + deepStrictEqual(z, ["a", "b", "c"]) + deepStrictEqual(zUnion, [1]) + deepStrictEqual(s2, ["a", "b"]) + deepStrictEqual(s3, ["hello"]) + }).pipe(Effect.provideService(DemoTag, { + getNumbers: () => [0, 1], + strings: ["a", "b"], + fn: (...args) => Array.from(args), + fnGen: (s) => [s], + fnParamsUnion: (..._args) => _args + }))) + it.effect("effect tag with primitives", () => + Effect.gen(function*() { + strictEqual(yield* (DateTag.getTime()), DateTag.date.getTime()) + strictEqual(yield* NumberTag, 100) + deepStrictEqual(Array.from(yield* (MapTag.keys())), []) + yield* (MapTag.set("foo", "bar")) + deepStrictEqual(Array.from(yield* (MapTag.keys())), ["foo"]) + strictEqual(yield* (MapTag.get("foo")), "bar") + }).pipe( + Effect.provide(Layer.mergeAll( + DateTag.Live, + NumberTag.Live, + MapTag.Live + )) + )) + it.effect("class tag", () => + Effect.gen(function*() { + yield* ( + Effect.flatMap(NumberRepo.numbers, (_) => Effect.log(`Numbers: ${_}`)).pipe( + Effect.provideService(NumberRepo, { numbers: [0, 1, 2] }) + ) + ) + })) + it.effect("environment - provide is modular", () => + pipe( + Effect.gen(function*() { + const v1 = yield* NumberService + const v2 = yield* ( + pipe( + NumberService, + Effect.provide(Context.make(NumberService, { n: 2 })) + ) + ) + const v3 = yield* NumberService + strictEqual(v1.n, 4) + strictEqual(v2.n, 2) + strictEqual(v3.n, 4) + }), + Effect.provide(Context.make(NumberService, { n: 4 })) + )) + it.effect("environment - provideSomeContext provides context in the right order", () => + pipe( + Effect.gen(function*() { + const v1 = yield* NumberService + const v2 = yield* StringService + strictEqual(v1.n, 1) + strictEqual(v2.s, "ok") + }), + Effect.provide(Context.make(NumberService, { n: 1 })), + Effect.provide(Context.make(NumberService, { n: 2 })), + Effect.provide(Context.make(StringService, { s: "ok" })) + )) + it.effect("environment - async can use environment", () => + Effect.gen(function*() { + const result = yield* pipe( + Effect.async((cb) => cb(Effect.map(NumberService, ({ n }) => n))), + Effect.provide(Context.make(NumberService, { n: 10 })) + ) + strictEqual(result, 10) + })) + it.effect("serviceWith - effectfully accesses a service in the environment", () => + Effect.gen(function*() { + const result = yield* pipe( + Effect.flatMap(NumberService, ({ n }) => Effect.succeed(n + 3)), + Effect.provide(Context.make(NumberService, { n: 0 })) + ) + strictEqual(result, 3) + })) + // TODO: remove + // it.effect("serviceWith - traced tag", () => + // Effect.gen(function*() { + // const result = yield* ( + // Effect.flatMap(NumberService.traced(sourceLocation(new Error())), ({ n }) => Effect.succeed(n + 3)), + // Effect.provide(Context.make(NumberService, { n: 0 })) + // ) + // strictEqual(result, 3) + // })) + it.effect("updateService - updates a service in the environment", () => + pipe( + Effect.gen(function*() { + const a = yield* pipe(NumberService, Effect.updateService(NumberService, ({ n }) => ({ n: n + 1 }))) + const b = yield* NumberService + strictEqual(a.n, 1) + strictEqual(b.n, 0) + }), + Effect.provide(Context.make(NumberService, { n: 0 })) + )) + + it.effect("serviceFunctions - expose service functions", () => { + interface Service { + foo: (x: string, y: number) => Effect.Effect + } + const Service = Context.GenericTag("Service") + const { foo } = Effect.serviceFunctions(Service) + return pipe( + Effect.gen(function*() { + strictEqual(yield* foo("a", 3), "a3") + }), + Effect.provideService( + Service, + Service.of({ + foo: (x, y) => Effect.succeed(`${x}${y}`) + }) + ) + ) + }) + + it.effect("serviceConstants - expose service constants", () => { + interface Service { + baz: Effect.Effect + } + const Service = Context.GenericTag("Service") + const { baz } = Effect.serviceConstants(Service) + return pipe( + Effect.gen(function*() { + strictEqual(yield* baz, "42!") + }), + Effect.provideService( + Service, + Service.of({ + baz: Effect.succeed("42!") + }) + ) + ) + }) + + it.effect("serviceMembers - expose both service functions and constants", () => { + interface Service { + foo: (x: string, y: number) => Effect.Effect + baz: Effect.Effect + } + const Service = Context.GenericTag("Service") + const { constants, functions } = Effect.serviceMembers(Service) + return pipe( + Effect.gen(function*() { + strictEqual(yield* constants.baz, "42!") + strictEqual(yield* functions.foo("a", 3), "a3") + }), + Effect.provideService( + Service, + Service.of({ + baz: Effect.succeed("42!"), + foo: (x, y) => Effect.succeed(`${x}${y}`) + }) + ) + ) + }) +}) diff --git a/repos/effect/packages/effect/test/Effect/error-handling.test.ts b/repos/effect/packages/effect/test/Effect/error-handling.test.ts new file mode 100644 index 0000000..21220f2 --- /dev/null +++ b/repos/effect/packages/effect/test/Effect/error-handling.test.ts @@ -0,0 +1,529 @@ +import { describe, it } from "@effect/vitest" +import { assertLeft, assertTrue, deepStrictEqual, strictEqual } from "@effect/vitest/utils" +import * as Cause from "effect/Cause" +import * as Chunk from "effect/Chunk" +import * as Effect from "effect/Effect" +import * as Either from "effect/Either" +import * as Exit from "effect/Exit" +import * as fc from "effect/FastCheck" +import * as Fiber from "effect/Fiber" +import * as FiberId from "effect/FiberId" +import { constFalse, constTrue, identity, pipe } from "effect/Function" +import * as Option from "effect/Option" +import { causesArb } from "../utils/cause.js" + +const ExampleError = new Error("Oh noes!") + +export const InterruptError1 = new Error("Oh noes 1!") +export const InterruptError2 = new Error("Oh noes 2!") +export const InterruptError3 = new Error("Oh noes 3!") + +const ExampleErrorFail: Effect.Effect = Effect.fail(ExampleError) + +const deepErrorEffect = (n: number): Effect.Effect => { + if (n === 0) { + return Effect.try(() => { + throw ExampleError + }) + } + return pipe(Effect.void, Effect.zipRight(deepErrorEffect(n - 1))) +} + +const deepErrorFail = (n: number): Effect.Effect => { + if (n === 0) { + return Effect.fail(ExampleError) + } + return pipe(Effect.void, Effect.zipRight(deepErrorFail(n - 1))) +} + +describe("Effect", () => { + it.effect("attempt - error in sync effect", () => + Effect.gen(function*() { + const result = yield* pipe( + Effect.try(() => { + throw ExampleError + }), + Effect.flip + ) + deepStrictEqual(result.error, ExampleError) + })) + it.effect("attempt - fail", () => + Effect.gen(function*() { + const io1 = Effect.either(ExampleErrorFail) + const io2 = Effect.suspend(() => Effect.either(Effect.suspend(() => ExampleErrorFail))) + const [first, second] = yield* pipe(io1, Effect.zip(io2)) + assertLeft(first, ExampleError) + assertLeft(second, ExampleError) + })) + it.effect("attempt - deep attempt sync effect error", () => + Effect.gen(function*() { + const result = yield* (Effect.flip(deepErrorEffect(100))) + deepStrictEqual(result.error, ExampleError) + })) + it.effect("attempt - deep attempt fail error", () => + Effect.gen(function*() { + const result = yield* (Effect.either(deepErrorFail(100))) + assertLeft(result, ExampleError) + })) + it.effect("attempt - sandbox -> terminate", () => + Effect.gen(function*() { + const result = yield* pipe( + Effect.sync(() => { + throw ExampleError + }), + Effect.sandbox, + Effect.either + ) + assertLeft(result, Cause.die(ExampleError)) + })) + it.effect("catch - sandbox terminate", () => + Effect.gen(function*() { + const result = yield* pipe( + Effect.sync(() => { + throw ExampleError + }), + Effect.sandbox, + Effect.merge + ) + deepStrictEqual(result, Cause.die(ExampleError)) + })) + it.effect("catch failing finalizers with fail", () => + Effect.gen(function*() { + const result = yield* pipe( + Effect.fail(ExampleError), + Effect.ensuring(Effect.sync(() => { + throw InterruptError1 + })), + Effect.ensuring(Effect.sync(() => { + throw InterruptError2 + })), + Effect.ensuring(Effect.sync(() => { + throw InterruptError3 + })), + Effect.exit + ) + const expected = Cause.sequential( + Cause.sequential( + Cause.sequential(Cause.fail(ExampleError), Cause.die(InterruptError1)), + Cause.die(InterruptError2) + ), + Cause.die(InterruptError3) + ) + deepStrictEqual(result, Exit.failCause(expected)) + })) + it.effect("catch failing finalizers with terminate", () => + Effect.gen(function*() { + const result = yield* pipe( + Effect.die(ExampleError), + Effect.ensuring(Effect.sync(() => { + throw InterruptError1 + })), + Effect.ensuring(Effect.sync(() => { + throw InterruptError2 + })), + Effect.ensuring(Effect.sync(() => { + throw InterruptError3 + })), + Effect.exit + ) + const expected = Cause.sequential( + Cause.sequential( + Cause.sequential(Cause.die(ExampleError), Cause.die(InterruptError1)), + Cause.die(InterruptError2) + ), + Cause.die(InterruptError3) + ) + deepStrictEqual(result, Exit.failCause(expected)) + })) + it.effect("catchAllCause", () => + Effect.gen(function*() { + const result = yield* ( + pipe(Effect.succeed(42), Effect.zipRight(Effect.fail("uh oh")), Effect.catchAllCause(Effect.succeed)) + ) + deepStrictEqual(result, Cause.fail("uh oh")) + })) + it.effect("catchAllDefect - recovers from all defects", () => + Effect.gen(function*() { + const message = "division by zero" + const result = yield* pipe( + Effect.die(new Cause.IllegalArgumentException(message)), + Effect.catchAllDefect((e) => Effect.succeed((e as Error).message)) + ) + strictEqual(result, message) + })) + it.effect("catchAllDefect - leaves errors", () => + Effect.gen(function*() { + const error = new Cause.IllegalArgumentException("division by zero") + const result = yield* ( + pipe(Effect.fail(error), Effect.catchAllDefect((e) => Effect.succeed((e as Error).message)), Effect.exit) + ) + deepStrictEqual(result, Exit.fail(error)) + })) + it.effect("catchAllDefect - leaves values", () => + Effect.gen(function*() { + const error = new Cause.IllegalArgumentException("division by zero") + const result = yield* ( + pipe(Effect.succeed(error), Effect.catchAllDefect((e) => Effect.succeed((e as Error).message))) + ) + deepStrictEqual(result, error) + })) + it.effect("catchSomeDefect - recovers from some defects", () => + Effect.gen(function*() { + const message = "division by zero" + const result = yield* pipe( + Effect.die(new Cause.IllegalArgumentException(message)), + Effect.catchSomeDefect((e) => + Cause.isIllegalArgumentException(e) + ? Option.some(Effect.succeed(e.message)) + : Option.none() + ) + ) + strictEqual(result, message) + })) + it.effect("catchSomeDefect - leaves the rest", () => + Effect.gen(function*() { + const error = new Cause.IllegalArgumentException("division by zero") + const result = yield* pipe( + Effect.die(error), + Effect.catchSomeDefect((e) => + Cause.isRuntimeException(e) ? + Option.some(Effect.succeed(e.message)) : + Option.none() + ), + Effect.exit + ) + deepStrictEqual(result, Exit.die(error)) + })) + it.effect("catchSomeDefect - leaves errors", () => + Effect.gen(function*() { + const error = new Cause.IllegalArgumentException("division by zero") + const result = yield* pipe( + Effect.fail(error), + Effect.catchSomeDefect((e) => + Cause.isIllegalArgumentException(e) + ? Option.some(Effect.succeed(e.message)) + : Option.none() + ), + Effect.exit + ) + deepStrictEqual(result, Exit.fail(error)) + })) + it.effect("catchSomeDefect - leaves values", () => + Effect.gen(function*() { + const error = new Cause.IllegalArgumentException("division by zero") + const result = yield* pipe( + Effect.succeed(error), + Effect.catchSomeDefect((e) => + Cause.isIllegalArgumentException(e) + ? Option.some(Effect.succeed(e.message)) + : Option.none() + ) + ) + deepStrictEqual(result, error) + })) + it.effect("catch - recovers from one of several tagged errors", () => + Effect.gen(function*() { + interface ErrorA { + readonly _tag: "ErrorA" + } + interface ErrorB { + readonly _tag: "ErrorB" + } + const effect: Effect.Effect = Effect.fail({ _tag: "ErrorA" }) + const result = yield* (Effect.catch(effect, "_tag", { + failure: "ErrorA", + onFailure: Effect.succeed + })) + deepStrictEqual(result, { _tag: "ErrorA" }) + })) + it.effect("catch - does not recover from one of several tagged errors", () => + Effect.gen(function*() { + interface ErrorA { + readonly _tag: "ErrorA" + } + interface ErrorB { + readonly _tag: "ErrorB" + } + const effect: Effect.Effect = Effect.fail({ _tag: "ErrorB" }) + const result = yield* pipe( + Effect.catch(effect, "_tag", { + failure: "ErrorA", + onFailure: Effect.succeed + }), + Effect.exit + ) + deepStrictEqual(result, Exit.fail({ _tag: "ErrorB" as const })) + })) + it.effect("catchIf - does not recover from one of several tagged errors", () => + Effect.gen(function*() { + interface ErrorA { + readonly _tag: "ErrorA" + } + interface ErrorB { + readonly _tag: "ErrorB" + } + const effect: Effect.Effect = Effect.fail({ _tag: "ErrorB" }) + const result = yield* pipe( + Effect.catchIf(effect, (e): e is ErrorA => e._tag === "ErrorA", Effect.succeed), + Effect.exit + ) + deepStrictEqual(result, Exit.fail({ _tag: "ErrorB" as const })) + })) + it.effect("catchTags - recovers from one of several tagged errors", () => + Effect.gen(function*() { + interface ErrorA { + readonly _tag: "ErrorA" + } + interface ErrorB { + readonly _tag: "ErrorB" + } + const effect: Effect.Effect = Effect.fail({ _tag: "ErrorA" }) + const result = yield* (Effect.catchTags(effect, { + ErrorA: (e) => Effect.succeed(e) + })) + deepStrictEqual(result, { _tag: "ErrorA" }) + })) + it.effect("catchTags - does not recover from one of several tagged errors", () => + Effect.gen(function*() { + interface ErrorA { + readonly _tag: "ErrorA" + } + interface ErrorB { + readonly _tag: "ErrorB" + } + const effect: Effect.Effect = Effect.fail({ _tag: "ErrorB" }) + const result = yield* (Effect.exit( + Effect.catchTags(effect, { + ErrorA: (e) => Effect.succeed(e) + }) + )) + deepStrictEqual(result, Exit.fail({ _tag: "ErrorB" })) + })) + it.effect("catchTags - recovers from all tagged errors", () => + Effect.gen(function*() { + interface ErrorA { + readonly _tag: "ErrorA" + } + interface ErrorB { + readonly _tag: "ErrorB" + } + const effect: Effect.Effect = Effect.fail({ _tag: "ErrorB" }) + const result = yield* (Effect.catchTags(effect, { + ErrorA: (e) => Effect.succeed(e), + ErrorB: (e) => Effect.succeed(e) + })) + deepStrictEqual(result, { _tag: "ErrorB" }) + })) + it.effect("fold - sandbox -> terminate", () => + Effect.gen(function*() { + const result = yield* pipe( + Effect.sync(() => { + throw ExampleError + }), + Effect.sandbox, + Effect.match({ + onFailure: Option.some, + onSuccess: () => Option.none() as Option.Option> + }) + ) + deepStrictEqual(result, Option.some(Cause.die(ExampleError))) + })) + it.effect("ignore - return success as unit", () => + Effect.gen(function*() { + const result = yield* (Effect.ignore(Effect.succeed(11))) + strictEqual(result, undefined) + })) + it.effect("ignore - return failure as unit", () => + Effect.gen(function*() { + const result = yield* (Effect.ignore(Effect.fail(123))) + strictEqual(result, undefined) + })) + it.effect("ignore - not catch throwable", () => + Effect.gen(function*() { + const result = yield* (Effect.exit(Effect.ignore(Effect.die(ExampleError)))) + deepStrictEqual(result, Exit.die(ExampleError)) + })) + it.effect("orElse - does not recover from defects", () => + Effect.gen(function*() { + const error = new Error("died") + const fiberId = FiberId.make(0, 123) + const bothCause = Cause.parallel(Cause.interrupt(fiberId), Cause.die(error)) + const thenCause = Cause.sequential(Cause.interrupt(fiberId), Cause.die(error)) + const plain = yield* pipe(Effect.die(error), Effect.orElse(() => Effect.void), Effect.exit) + const both = yield* pipe(Effect.failCause(bothCause), Effect.orElse(() => Effect.void), Effect.exit) + const then = yield* pipe(Effect.failCause(thenCause), Effect.orElse(() => Effect.void), Effect.exit) + const fail = yield* pipe(Effect.fail(error), Effect.orElse(() => Effect.void), Effect.exit) + deepStrictEqual(plain, Exit.die(error)) + deepStrictEqual(both, Exit.die(error)) + deepStrictEqual(then, Exit.die(error)) + deepStrictEqual(fail, Exit.succeed(void 0)) + })) + it.effect("orElse - left failed and right died with kept cause", () => + Effect.gen(function*() { + const z1 = Effect.fail(new Cause.RuntimeException("1")) + const z2 = Effect.die(new Cause.RuntimeException("2")) + const result = yield* pipe( + z1, + Effect.orElse(() => z2), + Effect.catchAllCause((cause) => { + if (Cause.isDie(cause)) { + const defects = Cause.defects(cause) + if (Chunk.isNonEmpty(defects)) { + const head = Chunk.headNonEmpty(defects) + return Effect.succeed((head as Cause.RuntimeException).message === "2") + } + } + return Effect.succeed(false) + }) + ) + assertTrue(result) + })) + it.effect("orElse - left failed and right failed with kept cause", () => + Effect.gen(function*() { + const z1 = Effect.fail(new Cause.RuntimeException("1")) + const z2 = Effect.fail(new Cause.RuntimeException("2")) + const result = yield* pipe( + z1, + Effect.orElse(() => z2), + Effect.catchAllCause((cause) => { + if (Cause.isFailure(cause)) { + const failures = Cause.failures(cause) + if (Chunk.isNonEmpty(failures)) { + const head = Chunk.headNonEmpty(failures) + return Effect.succeed(head.message === "2") + } + } + return Effect.succeed(false) + }) + ) + assertTrue(result) + })) + it("orElse - is associative", async () => { + const smallInts = fc.integer({ min: 0, max: 100 }) + const causes = causesArb(1, smallInts, fc.string()) + const successes = smallInts.map(Effect.succeed) + const exits = fc.oneof( + causes.map((s): Either.Either, Cause.Cause> => Either.left(s)), + successes.map((s): Either.Either, Cause.Cause> => Either.right(s)) + ).map(Either.match({ + onLeft: Exit.failCause, + onRight: Exit.succeed + })) + await fc.assert(fc.asyncProperty(exits, exits, exits, async (exit1, exit2, exit3) => { + const leftEffect = pipe(exit1, Effect.orElse(() => exit2), Effect.orElse(() => exit3)) + const rightEffect = pipe(exit1, Effect.orElse(() => pipe(exit2, Effect.orElse(() => exit3)))) + const program = Effect.gen(function*() { + const left = yield* (Effect.exit(leftEffect)) + const right = yield* (Effect.exit(rightEffect)) + return { left, right } + }) + const { left, right } = await Effect.runPromise(program) + deepStrictEqual(left, right) + })) + }) + it.effect("orElseFail - executes this effect and returns its value if it succeeds", () => + Effect.gen(function*() { + const result = yield* pipe(Effect.succeed(true), Effect.orElseFail(constFalse)) + assertTrue(result) + })) + it.effect("orElseFail - otherwise fails with the specified error", () => + Effect.gen(function*() { + const result = yield* pipe(Effect.fail(false), Effect.orElseFail(constTrue), Effect.flip) + assertTrue(result) + })) + it.effect("orElseSucceed - executes this effect and returns its value if it succeeds", () => + Effect.gen(function*() { + const result = yield* pipe(Effect.succeed(true), Effect.orElseSucceed(constFalse)) + assertTrue(result) + })) + it.effect("orElseSucceed - otherwise succeeds with the specified value", () => + Effect.gen(function*() { + const result = yield* pipe(Effect.fail(false), Effect.orElseSucceed(constTrue)) + assertTrue(result) + })) + it.effect("parallelErrors - one failure", () => + Effect.gen(function*() { + const fiber1 = yield* (Effect.fork(Effect.fail("error1"))) + const fiber2 = yield* (Effect.fork(Effect.succeed("success1"))) + const result = yield* pipe(fiber1, Fiber.zip(fiber2), Fiber.join, Effect.parallelErrors, Effect.flip) + deepStrictEqual(Array.from(result), ["error1"]) + })) + it.effect("parallelErrors - all failures", () => + Effect.gen(function*() { + const fiber1 = yield* (Effect.fork(Effect.fail("error1"))) + const fiber2 = yield* (Effect.fork(Effect.fail("error2"))) + const result = yield* pipe(fiber1, Fiber.zip(fiber2), Fiber.join, Effect.parallelErrors, Effect.flip) + deepStrictEqual(Array.from(result), ["error1", "error2"]) + })) + it.effect("promise - exception does not kill fiber", () => + Effect.gen(function*() { + const result = yield* pipe( + Effect.promise(() => { + throw ExampleError + }), + Effect.exit + ) + deepStrictEqual(result, Exit.die(ExampleError)) + })) + it.effect("try = handles exceptions", () => + Effect.gen(function*() { + const message = "hello" + const result = yield* pipe( + Effect.try({ + try: () => { + throw message + }, + catch: identity + }), + Effect.exit + ) + + deepStrictEqual(result, Exit.fail(message)) + })) + it.effect("uncaught - fail", () => + Effect.gen(function*() { + const result = yield* (Effect.exit(ExampleErrorFail)) + deepStrictEqual(result, Exit.fail(ExampleError)) + })) + it.effect("uncaught - sync effect error", () => + Effect.gen(function*() { + const result = yield* pipe( + Effect.sync(() => { + throw ExampleError + }), + Effect.exit + ) + + deepStrictEqual(result, Exit.die(ExampleError)) + })) + it.effect("uncaught - deep sync effect error", () => + Effect.gen(function*() { + const result = yield* (Effect.flip(deepErrorEffect(100))) + deepStrictEqual(result.error, ExampleError) + })) + it.effect("unwraps exception", () => + Effect.gen(function*() { + const failure = Effect.fail(Cause.fail(new Error("fail"))) + const success = Effect.succeed(100) + const message = yield* pipe( + failure, + Effect.unsandbox, + Effect.matchEffect({ + onFailure: (e) => Effect.succeed(e.message), + onSuccess: () => Effect.succeed("unexpected") + }) + ) + const result = yield* (Effect.unsandbox(success)) + strictEqual(message, "fail") + strictEqual(result, 100) + })) + it.effect("no information is lost during composition", () => + Effect.gen(function*() { + const cause = (effect: Effect.Effect): Effect.Effect, never, R> => { + return Effect.cause(effect) + } + const expectedCause = Cause.fail("oh no") + const result = yield* (cause(pipe(Effect.failCause(expectedCause), Effect.sandbox, Effect.unsandbox))) + deepStrictEqual(result, expectedCause) + })) +}) diff --git a/repos/effect/packages/effect/test/Effect/error.test.ts b/repos/effect/packages/effect/test/Effect/error.test.ts new file mode 100644 index 0000000..ce734d0 --- /dev/null +++ b/repos/effect/packages/effect/test/Effect/error.test.ts @@ -0,0 +1,97 @@ +import { describe, it } from "@effect/vitest" +import { assertInclude, assertTrue, deepStrictEqual } from "@effect/vitest/utils" +import { Cause, Data, Effect, pipe } from "effect" + +class TestError extends Data.TaggedError("TestError")<{}> {} + +describe("Effect", () => { + it.effect("TaggedError has a stack", () => + Effect.gen(function*() { + const cause = yield* (Effect.flip(Effect.sandbox(Effect.withSpan("A")(new TestError())))) + const log = Cause.pretty(cause) + assertInclude(log, "TestError") + if (typeof window === "undefined") { + assertInclude(log.replaceAll("\\", "/"), "test/Effect/error.test.ts:10:77") + } + assertInclude(log, "at A") + })) + + it.effect("tryPromise", () => + Effect.gen(function*() { + const cause = yield* pipe( + Effect.tryPromise({ + try: () => Promise.reject("fail"), + catch: () => new TestError() + }), + Effect.withSpan("A"), + Effect.sandbox, + Effect.flip + ) + const log = Cause.pretty(cause) + if (typeof window === "undefined") { + assertInclude(log.replaceAll("\\", "/"), "test/Effect/error.test.ts:24") + } + assertInclude(log, "at A") + })) + + it.effect("allow message prop", () => + Effect.gen(function*() { + class MessageError extends Data.TaggedError("MessageError")<{ + readonly name: string + readonly message: string + }> {} + const cause = yield* pipe( + Effect.tryPromise({ + try: () => Promise.reject("fail"), + catch: () => new MessageError({ name: "Failure", message: "some message" }) + }), + Effect.withSpan("A"), + Effect.sandbox, + Effect.flip + ) + const log = Cause.pretty(cause) + assertInclude(log, "Failure: some message") + if (typeof window === "undefined") { + assertInclude(log.replaceAll("\\", "/"), "test/Effect/error.test.ts:46") + } + assertInclude(log, "at A") + })) + + if (typeof window === "undefined") { + // eslint-disable-next-line @typescript-eslint/no-require-imports + const { inspect } = require("node:util") + + it("inspect", () => { + class MessageError extends Data.TaggedError("MessageError") { + get message() { + return "fail" + } + } + const err = new MessageError() + assertInclude(inspect(err), "MessageError: fail") + assertInclude(inspect(err).replaceAll("\\", "/"), "test/Effect/error.test.ts:70") + }) + + it("toString", () => { + class MessageError extends Data.TaggedError("MessageError") { + toString() { + return "fail" + } + } + assertTrue(inspect(new MessageError()).startsWith("fail\n")) + deepStrictEqual(new MessageError().toJSON(), { _tag: "MessageError" }) + }) + + it("cause", () => { + class MessageError extends Data.TaggedError("MessageError")<{ + cause: unknown + }> {} + assertInclude(inspect(new MessageError({ cause: new Error("boom") })), "[cause]: Error: boom") + }) + } + + it("toJSON", () => { + class MessageError extends Data.TaggedError("MessageError")<{}> {} + deepStrictEqual(new MessageError().toJSON(), { _tag: "MessageError" }) + }) +}) diff --git a/repos/effect/packages/effect/test/Effect/filtering.test.ts b/repos/effect/packages/effect/test/Effect/filtering.test.ts new file mode 100644 index 0000000..92c7959 --- /dev/null +++ b/repos/effect/packages/effect/test/Effect/filtering.test.ts @@ -0,0 +1,237 @@ +import { describe, it } from "@effect/vitest" +import { assertLeft, assertRight, deepStrictEqual } from "@effect/vitest/utils" +import * as Cause from "effect/Cause" +import * as Effect from "effect/Effect" +import * as Either from "effect/Either" +import { pipe } from "effect/Function" +import * as Ref from "effect/Ref" +import { strictEqual } from "node:assert" + +const exactlyOnce = ( + value: A, + f: (_: Effect.Effect) => Effect.Effect +): Effect.Effect => { + return Effect.gen(function*() { + const ref = yield* (Ref.make(0)) + const res = yield* (f(pipe(Ref.update(ref, (n) => n + 1), Effect.zipRight(Effect.succeed(value))))) + const count = yield* (Ref.get(ref)) + yield* (count !== 1 ? Effect.fail("Accessed more than once") : Effect.void) + return res + }) +} + +describe("Effect", () => { + it.effect("filter - filters a collection using an effectual predicate", () => + Effect.gen(function*() { + const ref = yield* (Ref.make>([])) + const results = yield* ( + pipe( + [2, 4, 6, 3, 5, 6], + Effect.filter((n) => pipe(Ref.update(ref, (ns) => [n, ...ns]), Effect.as(n % 2 === 0))) + ) + ) + const effects = yield* (Ref.get(ref)) + deepStrictEqual(Array.from(results), [2, 4, 6, 6]) + deepStrictEqual(Array.from(effects), [2, 4, 6, 3, 5, 6]) + })) + it.effect("filter/negate - filters a collection using an effectual predicate, removing all elements that satisfy the predicate", () => + Effect.gen(function*() { + const ref = yield* (Ref.make>([])) + const results = yield* ( + pipe( + [2, 4, 6, 3, 5, 6], + Effect.filter((n) => pipe(Ref.update(ref, (ns) => [n, ...ns]), Effect.as(n % 2 === 0)), { negate: true }) + ) + ) + const effects = yield* (Ref.get(ref)) + deepStrictEqual(Array.from(results), [3, 5]) + deepStrictEqual(Array.from(effects), [2, 4, 6, 3, 5, 6]) + })) + it.effect("filter/concurrency - filters a collection in parallel using an effectual predicate", () => + Effect.gen(function*() { + const result = yield* ( + pipe( + [2, 4, 6, 3, 5, 6, 10, 11, 15, 17, 20, 22, 23, 25, 28], + Effect.filter((n) => Effect.succeed(n % 2 === 0), { concurrency: "unbounded" }) + ) + ) + deepStrictEqual(Array.from(result), [2, 4, 6, 6, 10, 20, 22, 28]) + })) + it.effect("filter/concurrency+negate - filters a collection in parallel using an effectual predicate, removing all elements that satisfy the predicate", () => + Effect.gen(function*() { + const result = yield* ( + pipe( + [2, 4, 6, 3, 5, 6, 10, 11, 15, 17, 20, 22, 23, 25, 28], + Effect.filter((n) => Effect.succeed(n % 2 === 0), { + concurrency: "unbounded", + negate: true + }) + ) + ) + deepStrictEqual(Array.from(result), [3, 5, 11, 15, 17, 23, 25]) + })) + it.effect("filterOrElse - returns checked failure from held value", () => + Effect.gen(function*() { + const goodCase = yield* pipe( + exactlyOnce(0, (effect) => + pipe( + effect, + Effect.filterOrElse( + (n) => n === 0, + (n) => Effect.fail(`${n} was not 0`) + ) + )), + Effect.sandbox, + Effect.either + ) + const badCase = yield* pipe( + exactlyOnce(1, (effect) => + pipe( + effect, + Effect.filterOrElse( + (n) => n === 0, + (n) => Effect.fail(`${n} was not 0`) + ) + )), + Effect.sandbox, + Effect.either, + Effect.map(Either.mapLeft(Cause.failureOrCause)) + ) + assertRight(goodCase, 0) + assertLeft(badCase, Either.left("1 was not 0")) + })) + it.effect("filterOrElse - returns checked failure ignoring value", () => + Effect.gen(function*() { + const goodCase = yield* pipe( + exactlyOnce(0, (effect) => + pipe( + effect, + Effect.filterOrElse( + (n) => n === 0, + () => Effect.fail("predicate failed!") + ) + )), + Effect.sandbox, + Effect.either + ) + const badCase = yield* pipe( + exactlyOnce(1, (effect) => + pipe( + effect, + Effect.filterOrElse( + (n) => n === 0, + () => Effect.fail("predicate failed!") + ) + )), + Effect.sandbox, + Effect.either, + Effect.map(Either.mapLeft(Cause.failureOrCause)) + ) + assertRight(goodCase, 0) + assertLeft(badCase, Either.left("predicate failed!")) + })) + it.effect("filterOrFail - returns failure ignoring value", () => + Effect.gen(function*() { + const goodCase = yield* pipe( + exactlyOnce(0, (effect) => + pipe( + effect, + Effect.filterOrFail( + (n) => n === 0, + () => "predicate failed!" + ) + )), + Effect.sandbox, + Effect.either + ) + const badCase = yield* pipe( + exactlyOnce(1, (effect) => + pipe( + effect, + Effect.filterOrFail( + (n) => n === 0, + () => "predicate failed!" + ) + )), + Effect.sandbox, + Effect.either, + Effect.map(Either.mapLeft(Cause.failureOrCause)) + ) + assertRight(goodCase, 0) + assertLeft(badCase, Either.left("predicate failed!")) + })) + it.effect("filterOrFail - returns failure", () => + Effect.gen(function*() { + const goodCase = yield* pipe( + exactlyOnce(0, (effect) => + pipe( + effect, + Effect.filterOrFail( + (n) => n === 0, + (n) => `predicate failed, got ${n}!` + ) + )), + Effect.sandbox, + Effect.either + ) + const badCase = yield* pipe( + exactlyOnce(1, (effect) => + pipe( + effect, + Effect.filterOrFail( + (n) => n === 0, + (n) => `predicate failed, got ${n}!` + ) + )), + Effect.sandbox, + Effect.either, + Effect.map(Either.mapLeft(Cause.failureOrCause)) + ) + assertRight(goodCase, 0) + assertLeft(badCase, Either.left("predicate failed, got 1!")) + })) + + it.effect("filterOrFail - without orFailWith", () => + Effect.gen(function*() { + const goodCase = yield* pipe( + Effect.succeed(0), + Effect.filterOrFail((n) => n === 0) + ) + const goodCaseDataFirst = yield* Effect.filterOrFail(Effect.succeed(0), (n) => n === 0) + const badCase = yield* pipe( + Effect.succeed(1), + Effect.filterOrFail((n) => n === 0), + Effect.flip + ) + deepStrictEqual(goodCase, 0) + deepStrictEqual(goodCaseDataFirst, 0) + deepStrictEqual(badCase, new Cause.NoSuchElementException()) + })) + + describe("filterEffectOrElse", () => { + it.effect("executes fallback", () => + Effect.gen(function*() { + const result = yield* Effect.succeed(1).pipe( + Effect.filterEffectOrElse({ + predicate: (n) => Effect.succeed(n === 0), + orElse: () => Effect.succeed(0) + }) + ) + strictEqual(result, 0) + })) + }) + + describe("filterEffectOrFails", () => { + it.effect("executes orFailWith", () => + Effect.gen(function*() { + const result = yield* Effect.succeed(1).pipe( + Effect.filterEffectOrElse({ + predicate: (n) => Effect.succeed(n === 0), + orElse: () => Effect.fail("boom") + }), + Effect.flip + ) + strictEqual(result, "boom") + })) + }) +}) diff --git a/repos/effect/packages/effect/test/Effect/finalization.test.ts b/repos/effect/packages/effect/test/Effect/finalization.test.ts new file mode 100644 index 0000000..ef35868 --- /dev/null +++ b/repos/effect/packages/effect/test/Effect/finalization.test.ts @@ -0,0 +1,334 @@ +import { describe, it } from "@effect/vitest" +import { assertFalse, assertTrue, deepStrictEqual, strictEqual } from "@effect/vitest/utils" +import * as Array from "effect/Array" +import * as Cause from "effect/Cause" +import * as Chunk from "effect/Chunk" +import * as Deferred from "effect/Deferred" +import * as Duration from "effect/Duration" +import * as Effect from "effect/Effect" +import * as Exit from "effect/Exit" +import * as Fiber from "effect/Fiber" +import { identity, pipe } from "effect/Function" +import * as Option from "effect/Option" +import * as Ref from "effect/Ref" + +const ExampleError = new Error("Oh noes!") + +const asyncExampleError = (): Effect.Effect => { + return Effect.async((cb) => { + cb(Effect.fail(ExampleError)) + }) +} + +const asyncVoid = (): Effect.Effect => { + return Effect.async((cb) => { + cb(Effect.void) + }) +} + +describe("Effect", () => { + it.effect("fail ensuring", () => + Effect.gen(function*() { + let finalized = false + const result = yield* pipe( + Effect.fail(ExampleError), + Effect.ensuring(Effect.sync(() => { + finalized = true + })), + Effect.exit + ) + deepStrictEqual(result, Exit.fail(ExampleError)) + assertTrue(finalized) + })) + it.effect("fail on error", () => + Effect.gen(function*() { + let finalized = false + const result = yield* pipe( + Effect.fail(ExampleError), + Effect.onError(() => + Effect.sync(() => { + finalized = true + }) + ), + Effect.exit + ) + deepStrictEqual(result, Exit.fail(ExampleError)) + assertTrue(finalized) + })) + it.effect("finalizer errors not caught", () => + Effect.gen(function*() { + const e2 = new Error("e2") + const e3 = new Error("e3") + const result = yield* ( + pipe( + Effect.fail(ExampleError), + Effect.ensuring(Effect.die(e2)), + Effect.ensuring(Effect.die(e3)), + Effect.sandbox, + Effect.flip, + Effect.map((cause) => cause) + ) + ) + const expected = Cause.sequential(Cause.sequential(Cause.fail(ExampleError), Cause.die(e2)), Cause.die(e3)) + deepStrictEqual(result, expected) + })) + it.effect("finalizer errors reported", () => + Effect.gen(function*() { + let reported: Exit.Exit | undefined + const result = yield* ( + pipe( + Effect.succeed(42), + Effect.ensuring(Effect.die(ExampleError)), + Effect.fork, + Effect.flatMap((fiber) => + pipe( + Fiber.await(fiber), + Effect.flatMap((e) => + Effect.sync(() => { + reported = e + }) + ) + ) + ) + ) + ) + strictEqual(result, undefined) + assertFalse(reported !== undefined && Exit.isSuccess(reported)) + })) + it.effect("acquireUseRelease exit.effect() is usage result", () => + Effect.gen(function*() { + const result = yield* (Effect.acquireUseRelease( + Effect.void, + () => Effect.succeed(42), + () => Effect.void + )) + strictEqual(result, 42) + })) + it.effect("error in just acquisition", () => + Effect.gen(function*() { + const result = yield* ( + pipe( + Effect.acquireUseRelease( + Effect.fail(ExampleError), + () => Effect.void, + () => Effect.void + ), + Effect.exit + ) + ) + deepStrictEqual(result, Exit.fail(ExampleError)) + })) + it.effect("error in just release", () => + Effect.gen(function*() { + const result = yield* ( + pipe( + Effect.acquireUseRelease( + Effect.void, + () => Effect.void, + () => Effect.die(ExampleError) + ), + Effect.exit + ) + ) + deepStrictEqual(result, Exit.die(ExampleError)) + })) + it.effect("error in just usage", () => + Effect.gen(function*() { + const result = yield* ( + pipe( + Effect.acquireUseRelease( + Effect.void, + () => Effect.fail(ExampleError), + () => Effect.void + ), + Effect.exit + ) + ) + deepStrictEqual(result, Exit.fail(ExampleError)) + })) + it.effect("rethrown caught error in acquisition", () => + Effect.gen(function*() { + const result = yield* pipe( + Effect.acquireUseRelease( + Effect.fail(ExampleError), + () => Effect.void, + () => Effect.void + ), + Effect.either, + Effect.flatMap(identity), + Effect.flip + ) + deepStrictEqual(result, ExampleError) + })) + it.effect("rethrown caught error in release", () => + Effect.gen(function*() { + const result = yield* ( + pipe( + Effect.acquireUseRelease( + Effect.void, + () => Effect.void, + () => Effect.die(ExampleError) + ), + Effect.exit + ) + ) + deepStrictEqual(result, Exit.die(ExampleError)) + })) + it.effect("rethrown caught error in usage", () => + Effect.gen(function*() { + const result = yield* pipe( + Effect.acquireUseRelease( + Effect.void, + () => Effect.fail(ExampleError), + () => Effect.void + ), + Effect.exit + ) + deepStrictEqual(result, Exit.fail(ExampleError)) + })) + it.effect("test eval of async fail", () => + Effect.gen(function*() { + const io1 = Effect.acquireUseRelease( + Effect.void, + () => asyncExampleError(), + () => asyncVoid() + ) + const io2 = Effect.acquireUseRelease( + asyncVoid(), + () => asyncExampleError(), + () => asyncVoid() + ) + const a1 = yield* (Effect.exit(io1)) + const a2 = yield* (Effect.exit(io2)) + const a3 = yield* pipe(io1, Effect.exit) + const a4 = yield* pipe(io2, Effect.exit) + deepStrictEqual(a1, Exit.fail(ExampleError)) + deepStrictEqual(a2, Exit.fail(ExampleError)) + deepStrictEqual(a3, Exit.fail(ExampleError)) + deepStrictEqual(a4, Exit.fail(ExampleError)) + })) + it.live("acquireUseRelease regression 1", () => + Effect.gen(function*() { + const makeLogger = (ref: Ref.Ref>) => { + return (line: string): Effect.Effect => { + return Ref.update(ref, Chunk.prepend(line)) + } + } + const ref = yield* (Ref.make(Chunk.empty())) + const log = makeLogger(ref) + const fiber = yield* pipe( + Effect.acquireUseRelease( + Effect.acquireUseRelease( + Effect.void, + () => Effect.void, + () => + pipe( + log("start 1"), + Effect.zipRight(Effect.sleep(Duration.millis(10))), + Effect.zipRight(log("release 1")) + ) + ), + () => Effect.void, + () => + pipe( + log("start 2"), + Effect.zipRight(Effect.sleep(Duration.millis(10))), + Effect.zipRight(log("release 2")) + ) + ), + Effect.fork + ) + yield* pipe( + Ref.get(ref), + Effect.zipLeft(Effect.sleep(Duration.millis(1))), + Effect.repeat({ until: (list) => pipe(list, Array.findFirst((s) => s === "start 1"), Option.isSome) }) + ) + yield* (Fiber.interrupt(fiber)) + yield* pipe( + Ref.get(ref), + Effect.zipLeft(Effect.sleep(Duration.millis(1))), + Effect.repeat({ until: (list) => pipe(list, Array.findFirst((s) => s === "release 2"), Option.isSome) }) + ) + const result = yield* (Ref.get(ref)) + assertTrue(pipe( + result, + Array.findFirst((s) => s === "start 1"), + Option.isSome + )) + assertTrue(pipe( + result, + Array.findFirst((s) => s === "release 1"), + Option.isSome + )) + assertTrue(pipe(result, Array.findFirst((s) => s === "start 2"), Option.isSome)) + assertTrue(pipe(result, Array.findFirst((s) => s === "release 2"), Option.isSome)) + })) + it.live("interrupt waits for finalizer", () => + Effect.gen(function*() { + const ref = yield* (Ref.make(false)) + const deferred1 = yield* (Deferred.make()) + const deferred2 = yield* (Deferred.make()) + const fiber = yield* ( + pipe( + Deferred.succeed(deferred1, void 0), + Effect.zipRight(Deferred.await(deferred2)), + Effect.ensuring(pipe(Ref.set(ref, true), Effect.zipRight(Effect.sleep(Duration.millis(10))))), + Effect.fork + ) + ) + yield* (Deferred.await(deferred1)) + yield* (Fiber.interrupt(fiber)) + const result = yield* (Ref.get(ref)) + assertTrue(result) + })) + it.effect("onExit - executes that a cleanup function runs when effect succeeds", () => + Effect.gen(function*() { + const ref = yield* (Ref.make(false)) + yield* pipe( + Effect.void, + Effect.onExit(Exit.match({ + onFailure: () => Effect.void, + onSuccess: () => Ref.set(ref, true) + })) + ) + const result = yield* (Ref.get(ref)) + assertTrue(result) + })) + it.effect("onExit - ensures that a cleanup function runs when an effect fails", () => + Effect.gen(function*() { + const ref = yield* (Ref.make(false)) + yield* pipe( + Effect.die(Cause.RuntimeException), + Effect.onExit((exit) => + Exit.isFailure(exit) && Cause.isDie(exit.effect_instruction_i0) ? + Ref.set(ref, true) : + Effect.void + ), + Effect.sandbox, + Effect.ignore + ) + const result = yield* (Ref.get(ref)) + assertTrue(result) + })) + it.effect("onExit - ensures that a cleanup function runs when an effect is interrupted", () => + Effect.gen(function*() { + const latch1 = yield* (Deferred.make()) + const latch2 = yield* (Deferred.make()) + const fiber = yield* ( + pipe( + Deferred.succeed(latch1, void 0), + Effect.zipRight(Effect.never), + Effect.onExit((exit) => + Exit.isFailure(exit) && Cause.isInterrupted(exit.effect_instruction_i0) ? + pipe(Deferred.succeed(latch2, void 0), Effect.asVoid) : + Effect.void + ), + Effect.fork + ) + ) + yield* (Deferred.await(latch1)) + yield* (Fiber.interrupt(fiber)) + const result = yield* (Deferred.await(latch2)) + strictEqual(result, undefined) + })) +}) diff --git a/repos/effect/packages/effect/test/Effect/fn.test.ts b/repos/effect/packages/effect/test/Effect/fn.test.ts new file mode 100644 index 0000000..a541662 --- /dev/null +++ b/repos/effect/packages/effect/test/Effect/fn.test.ts @@ -0,0 +1,119 @@ +import { describe, it } from "@effect/vitest" +import { assertEquals, assertInstanceOf, assertTrue, deepStrictEqual, strictEqual } from "@effect/vitest/utils" +import { Cause, Chunk, Effect, Stream } from "effect" + +describe("Effect.fn", () => { + it.effect("catches defects in the function", () => + Effect.gen(function*() { + let caught: Cause.Cause | undefined + const fn = Effect.fn("test")( + (): Effect.Effect => { + throw new Error("test") + }, + Effect.tapErrorCause((cause) => { + caught = cause + return Effect.void + }) + ) + const cause = yield* fn().pipe( + Effect.sandbox, + Effect.flip + ) + assertTrue(Cause.isDieType(cause)) + assertInstanceOf(cause.defect, Error) + strictEqual(cause.defect.message, "test") + strictEqual(caught, cause) + })) + + it.effect("catches defects in pipeline", () => + Effect.gen(function*() { + const fn = Effect.fn("test")( + () => Effect.void, + (_): Effect.Effect => { + throw new Error("test") + } + ) + const cause = yield* fn().pipe( + Effect.sandbox, + Effect.flip + ) + assertTrue(Cause.isDieType(cause)) + assertInstanceOf(cause.defect, Error) + strictEqual(cause.defect.message, "test") + })) + + it.effect("catches defects in both fn & pipeline", () => + Effect.gen(function*() { + const fn = Effect.fn("test")( + (): Effect.Effect => { + throw new Error("test") + }, + (_): Effect.Effect => { + throw new Error("test2") + } + ) + const cause = yield* fn().pipe( + Effect.sandbox, + Effect.flip + ) + assertTrue(Cause.isSequentialType(cause)) + assertTrue(Cause.isDieType(cause.left)) + assertTrue(Cause.isDieType(cause.right)) + assertInstanceOf(cause.left.defect, Error) + strictEqual(cause.left.defect.message, "test") + assertInstanceOf(cause.right.defect, Error) + strictEqual(cause.right.defect.message, "test2") + })) + + it("should preserve the function length", () => { + const f = function*(n: number) { + return n + } + const fn1 = Effect.fn("fn1")(f) + strictEqual(fn1.length, 1) + strictEqual(Effect.runSync(fn1(2)), 2) + const fn2 = Effect.fn(f) + strictEqual(fn2.length, 1) + strictEqual(Effect.runSync(fn2(2)), 2) + }) +}) + +describe("Effect.fnUntraced", () => { + it("should preserve the function length", () => { + const f = function*(n: number) { + return n + } + const fn1 = Effect.fnUntraced(f) + strictEqual(fn1.length, 1) + strictEqual(Effect.runSync(fn1(2)), 2) + const fn2 = Effect.fnUntraced(f, (x) => x) + strictEqual(fn2.length, 1) + strictEqual(Effect.runSync(fn2(2)), 2) + }) + + it.effect("can access args in single pipe", () => + Effect.gen(function*() { + const fn = Effect.fnUntraced( + function*(n: number) { + return n + }, + (effect, n) => Effect.map(effect, (a) => a + n), + (effect, n) => Effect.map(effect, (a) => a + n) + ) + const n = yield* fn(1) + assertEquals(n, 3) + })) + + it.effect("can return non-effects", () => + Effect.gen(function*() { + const fn = Effect.fnUntraced( + function*(n: number) { + return n + }, + (effect, n) => Effect.map(effect, (a) => a + n), + Stream.fromEffect + ) + const n = yield* Stream.runCollect(fn(1)) + deepStrictEqual(Chunk.toReadonlyArray(n), [2]) + })) +}) diff --git a/repos/effect/packages/effect/test/Effect/foreign.test.ts b/repos/effect/packages/effect/test/Effect/foreign.test.ts new file mode 100644 index 0000000..ffd3143 --- /dev/null +++ b/repos/effect/packages/effect/test/Effect/foreign.test.ts @@ -0,0 +1,53 @@ +import { describe, it } from "@effect/vitest" +import { assertLeft, deepStrictEqual } from "@effect/vitest/utils" +import { Cause, Context, Effect, Either, Exit, Option, pipe } from "effect" +import { nextInt } from "effect/Random" +import { unify } from "effect/Unify" + +describe("Foreign", () => { + it.effect("Unify", () => + Effect.gen(function*() { + const unifiedEffect = unify((yield* nextInt) > 1 ? Effect.succeed(0) : Effect.fail(1)) + const unifiedExit = unify((yield* nextInt) > 1 ? Exit.succeed(0) : Exit.fail(1)) + const unifiedEither = unify((yield* nextInt) > 1 ? Either.right(0) : Either.left(1)) + const unifiedOption = unify((yield* nextInt) > 1 ? Option.some(0) : Option.none()) + deepStrictEqual(yield* unifiedEffect, 0) + deepStrictEqual(yield* unifiedExit, 0) + deepStrictEqual(yield* unifiedEither, 0) + deepStrictEqual(yield* unifiedOption, 0) + })) + it.effect("Tag", () => + Effect.gen(function*() { + const tag = Context.GenericTag("number") + const result = yield* pipe(tag, Effect.provideService(tag, 10)) + deepStrictEqual(result, 10) + })) + it.effect("Either", () => + Effect.gen(function*() { + const a = yield* (Either.right(10)) + const b = yield* (Effect.either(Either.left(10))) + const c = yield* pipe( + Either.right(2), + Effect.flatMap( + (n) => Effect.succeed(n + 1) + ) + ) + deepStrictEqual(a, 10) + assertLeft(b, 10) + deepStrictEqual(c, 3) + })) + it.effect("Option", () => + Effect.gen(function*() { + const a = yield* (Option.some(10)) + const b = yield* (Effect.either(Option.none())) + const c = yield* pipe( + Option.some(2), + Effect.flatMap( + (n) => Effect.succeed(n + 1) + ) + ) + deepStrictEqual(a, 10) + assertLeft(b, new Cause.NoSuchElementException()) + deepStrictEqual(c, 3) + })) +}) diff --git a/repos/effect/packages/effect/test/Effect/forking.test.ts b/repos/effect/packages/effect/test/Effect/forking.test.ts new file mode 100644 index 0000000..7e6401b --- /dev/null +++ b/repos/effect/packages/effect/test/Effect/forking.test.ts @@ -0,0 +1,127 @@ +import { describe, it } from "@effect/vitest" +import { assertFalse, assertTrue, deepStrictEqual, strictEqual } from "@effect/vitest/utils" +import * as Cause from "effect/Cause" +import * as Deferred from "effect/Deferred" +import * as Effect from "effect/Effect" +import * as Exit from "effect/Exit" +import * as Fiber from "effect/Fiber" +import { pipe } from "effect/Function" +import * as Option from "effect/Option" +import * as Ref from "effect/Ref" + +describe("Effect", () => { + it.effect("fork - propagates interruption", () => + Effect.gen(function*() { + const result = yield* pipe(Effect.never, Effect.fork, Effect.flatMap(Fiber.interrupt)) + assertTrue(Exit.isInterrupted(result)) + })) + it.effect("fork - propagates interruption with zip of defect", () => + Effect.gen(function*() { + const latch = yield* (Deferred.make()) + const fiber = yield* pipe( + Deferred.succeed(latch, void 0), + Effect.zipRight(Effect.die(new Error())), + Effect.zip(Effect.never, { concurrent: true }), + Effect.fork + ) + + yield* (Deferred.await(latch)) + const result = yield* pipe(Fiber.interrupt(fiber), Effect.map(Exit.mapErrorCause((cause) => cause))) + assertTrue(Exit.isInterrupted(result)) + })) + it.effect("fork - interruption status is heritable", () => + Effect.gen(function*() { + const latch = yield* (Deferred.make()) + const ref = yield* (Ref.make(true)) + yield* pipe( + Effect.checkInterruptible((isInterruptible) => + pipe(Ref.set(ref, isInterruptible), Effect.zipRight(Deferred.succeed(latch, void 0))) + ), + Effect.fork, + Effect.zipRight(Deferred.await(latch)), + Effect.uninterruptible + ) + + const result = yield* (Ref.get(ref)) + assertFalse(result) + })) + it.effect("forkWithErrorHandler - calls provided function when task fails", () => + Effect.gen(function*() { + const deferred = yield* (Deferred.make()) + yield* pipe( + Effect.fail(void 0), + Effect.forkWithErrorHandler((e) => pipe(Deferred.succeed(deferred, e), Effect.asVoid)) + ) + const result = yield* (Deferred.await(deferred)) + strictEqual(result, undefined) + })) + it.effect("forkAll - returns the list of results in the same order", () => + Effect.gen(function*() { + const result = yield* pipe([1, 2, 3].map(Effect.succeed), Effect.forkAll(), Effect.flatMap(Fiber.join)) + deepStrictEqual(Array.from(result), [1, 2, 3]) + })) + it.effect("forkAll - happy-path", () => + Effect.gen(function*() { + const result = yield* pipe( + Array.from({ length: 1000 }, (_, i) => i + 1).map(Effect.succeed), + Effect.forkAll(), + Effect.flatMap(Fiber.join) + ) + deepStrictEqual( + Array.from(result), + Array.from({ length: 1000 }, (_, i) => i + 1) + ) + })) + it.effect("forkAll - empty input", () => + Effect.gen(function*() { + const result = yield* ( + pipe([] as ReadonlyArray>, Effect.forkAll(), Effect.flatMap(Fiber.join)) + ) + strictEqual(result.length, 0) + })) + it.effect("forkAll - propagate failures", () => + Effect.gen(function*() { + const boom = new Error() + const fail = Effect.fail(boom) + const result = yield* pipe([fail], Effect.forkAll(), Effect.flatMap((fiber) => Effect.flip(Fiber.join(fiber)))) + strictEqual(result, boom) + })) + it.effect("forkAll - propagates defects", () => + Effect.gen(function*() { + const boom = new Error("boom") + const die = Effect.die(boom) + const joinDefect = (fiber: Fiber.Fiber) => { + return pipe(fiber, Fiber.join, Effect.sandbox, Effect.flip) + } + const fiber1 = yield* (Effect.forkAll([die])) + const fiber2 = yield* (Effect.forkAll([die, Effect.succeed(42)])) + const fiber3 = yield* (Effect.forkAll([die, Effect.succeed(42), Effect.never])) + const result1 = yield* pipe(joinDefect(fiber1), Effect.map((cause) => cause)) + const result2 = yield* pipe(joinDefect(fiber2), Effect.map((cause) => cause)) + const result3 = yield* pipe(joinDefect(fiber3), Effect.map((cause) => cause)) + deepStrictEqual(Cause.dieOption(result1), Option.some(boom)) + deepStrictEqual(Cause.dieOption(result2), Option.some(boom)) + deepStrictEqual(Cause.dieOption(result3), Option.some(boom)) + assertTrue(Cause.isInterrupted(result3)) + })) + it.effect("forkAll - infers correctly", () => + Effect.gen(function*() { + const ref = yield* (Ref.make(0)) + const worker = Effect.never + const workers = Array.from({ length: 4 }, () => worker) + const fiber = yield* (Effect.forkAll(workers)) + yield* (Fiber.interrupt(fiber)) + const result = yield* (Ref.get(ref)) + strictEqual(result, 0) + })) + it.effect("forkAll - infers correctly with error type", () => + Effect.gen(function*() { + const ref = yield* (Ref.make(0)) + const worker = Effect.forever(Effect.fail(new Cause.RuntimeException("fail"))) + const workers = Array.from({ length: 4 }, () => worker) + const fiber = yield* (Effect.forkAll(workers)) + yield* (Fiber.interrupt(fiber)) + const result = yield* (Ref.get(ref)) + strictEqual(result, 0) + })) +}) diff --git a/repos/effect/packages/effect/test/Effect/interruption.test.ts b/repos/effect/packages/effect/test/Effect/interruption.test.ts new file mode 100644 index 0000000..f18d87b --- /dev/null +++ b/repos/effect/packages/effect/test/Effect/interruption.test.ts @@ -0,0 +1,605 @@ +import { describe, it } from "@effect/vitest" +import { assertFalse, assertSome, assertTrue, deepStrictEqual, strictEqual } from "@effect/vitest/utils" +import * as Array from "effect/Array" +import * as Cause from "effect/Cause" +import * as Chunk from "effect/Chunk" +import * as Deferred from "effect/Deferred" +import * as Duration from "effect/Duration" +import * as Effect from "effect/Effect" +import * as Either from "effect/Either" +import * as Exit from "effect/Exit" +import * as Fiber from "effect/Fiber" +import * as FiberId from "effect/FiberId" +import { constVoid, pipe } from "effect/Function" +import * as HashSet from "effect/HashSet" +import * as MutableRef from "effect/MutableRef" +import * as Option from "effect/Option" +import * as Ref from "effect/Ref" +import * as TestClock from "effect/TestClock" +import { withLatch, withLatchAwait } from "../utils/latch.js" + +describe("Effect", () => { + it.effect("sync forever is interruptible", () => + Effect.gen(function*() { + const fiber = yield* pipe(Effect.succeed(1), Effect.forever, Effect.fork) + const result = yield* (Fiber.interrupt(fiber)) + assertTrue(Exit.isFailure(result) && Cause.isInterruptedOnly(result.effect_instruction_i0)) + })) + it.effect("interrupt of never is interrupted with cause", () => + Effect.gen(function*() { + const fiber = yield* pipe(Effect.never, Effect.fork) + const result = yield* (Fiber.interrupt(fiber)) + assertTrue(Exit.isFailure(result) && Cause.isInterruptedOnly(result.effect_instruction_i0)) + })) + it.effect("asyncEffect is interruptible", () => + Effect.gen(function*() { + const fiber = yield* ( + pipe(Effect.asyncEffect(() => Effect.never), Effect.fork) + ) + const result = yield* (Fiber.interrupt(fiber)) + assertTrue(Exit.isFailure(result) && Cause.isInterruptedOnly(result.effect_instruction_i0)) + })) + it.effect("async is interruptible", () => + Effect.gen(function*() { + const fiber = yield* pipe(Effect.async(constVoid), Effect.fork) + const result = yield* (Fiber.interrupt(fiber)) + assertTrue(Exit.isFailure(result) && Cause.isInterruptedOnly(result.effect_instruction_i0)) + })) + it.effect("acquireUseRelease - acquire is uninterruptible", () => + Effect.gen(function*() { + const awaiter = Deferred.unsafeMake(FiberId.none) + const program = Effect.gen(function*() { + const deferred = yield* (Deferred.make()) + const fiber = yield* ( + pipe( + Effect.acquireUseRelease( + pipe(Deferred.succeed(deferred, void 0), Effect.zipLeft(Deferred.await(awaiter))), + () => Effect.void, + () => Effect.void + ), + Effect.forkDaemon + ) + ) + return yield* ( + pipe( + Deferred.await(deferred), + Effect.zipRight(pipe( + Fiber.interrupt(fiber), + Effect.timeoutTo({ + onTimeout: () => 42, + onSuccess: () => 0, + duration: Duration.millis(500) + }) + )), + Effect.zipLeft(TestClock.adjust(Duration.seconds(1)), { concurrent: true }) + ) + ) + }) + const result = yield* program + yield* (Deferred.succeed(awaiter, void 0)) + strictEqual(result, 42) + })) + it.effect("acquireUseRelease - use is interruptible", () => + Effect.gen(function*() { + const fiber = yield* ( + Effect.fork( + Effect.acquireUseRelease( + Effect.void, + () => Effect.never, + () => Effect.void + ) + ) + ) + const result = yield* (Fiber.interrupt(fiber)) + assertTrue(Exit.isInterrupted(result)) + })) + it.effect("acquireUseRelease - release is called on interrupt", () => + Effect.gen(function*() { + const deferred1 = yield* (Deferred.make()) + const deferred2 = yield* (Deferred.make()) + const fiber = yield* Effect.fork( + Effect.acquireUseRelease( + Effect.void, + () => pipe(Deferred.succeed(deferred1, void 0), Effect.zipRight(Effect.never)), + () => pipe(Deferred.succeed(deferred2, void 0), Effect.zipRight(Effect.void)) + ) + ) + yield* (Deferred.await(deferred1)) + yield* (Fiber.interrupt(fiber)) + const result = yield* pipe( + Deferred.await(deferred2), + Effect.timeoutTo({ + onTimeout: () => 42, + onSuccess: () => 0, + duration: Duration.seconds(1) + }) + ) + strictEqual(result, 0) + })) + it.effect("acquireUseRelease acquire returns immediately on interrupt", () => + Effect.gen(function*() { + const deferred1 = yield* (Deferred.make()) + const deferred2 = yield* (Deferred.make()) + const deferred3 = yield* (Deferred.make()) + const fiber = yield* ( + pipe( + Effect.acquireUseRelease( + pipe(Deferred.succeed(deferred1, void 0), Effect.zipRight(Deferred.await(deferred2))), + () => Effect.void, + () => Deferred.await(deferred3) + ), + Effect.disconnect, + Effect.fork + ) + ) + yield* (Deferred.await(deferred1)) + const result = yield* (Fiber.interrupt(fiber)) + yield* (Deferred.succeed(deferred3, void 0)) + assertTrue(Exit.isInterrupted(result)) + })) + it.effect("acquireUseRelease disconnect use is interruptible", () => + Effect.gen(function*() { + const fiber = yield* pipe( + Effect.acquireUseRelease( + Effect.void, + () => Effect.never, + () => Effect.void + ), + Effect.disconnect, + Effect.fork + ) + const result = yield* (Fiber.interrupt(fiber)) + assertTrue(Exit.isInterrupted(result)) + })) + it.effect("acquireUseRelease disconnect release called on interrupt in separate fiber", () => + Effect.gen(function*() { + const deferred1 = yield* (Deferred.make()) + const deferred2 = yield* (Deferred.make()) + const fiber = yield* ( + pipe( + Effect.acquireUseRelease( + Effect.void, + () => pipe(Deferred.succeed(deferred1, void 0), Effect.zipRight(Effect.never)), + () => pipe(Deferred.succeed(deferred2, void 0), Effect.zipRight(Effect.void)) + ), + Effect.disconnect, + Effect.fork + ) + ) + yield* (Deferred.await(deferred1)) + yield* (Fiber.interrupt(fiber)) + const result = yield* ( + pipe( + Deferred.await(deferred2), + Effect.timeoutTo({ + onTimeout: () => false, + onSuccess: () => true, + duration: Duration.seconds(10) + }) + ) + ) + assertTrue(result) + })) + it.effect("catchAll + ensuring + interrupt", () => + Effect.gen(function*() { + const latch = yield* (Deferred.make()) + const deferred = yield* (Deferred.make()) + const fiber = yield* ( + pipe( + Deferred.succeed(latch, void 0), + Effect.zipRight(Effect.never), + Effect.catchAll(Effect.fail), + Effect.ensuring(Deferred.succeed(deferred, true)), + Effect.fork + ) + ) + yield* (Deferred.await(latch)) + yield* (Fiber.interrupt(fiber)) + const result = yield* (Deferred.await(deferred)) + assertTrue(result) + })) + it.effect("finalizer can detect interruption", () => + Effect.gen(function*() { + const deferred1 = yield* (Deferred.make()) + const deferred2 = yield* (Deferred.make()) + const fiber = yield* ( + pipe( + Deferred.succeed(deferred2, void 0), + Effect.zipRight(Effect.never), + Effect.ensuring( + pipe( + Effect.descriptor, + Effect.flatMap((descriptor) => Deferred.succeed(deferred1, HashSet.size(descriptor.interruptors) > 0)) + ) + ), + Effect.fork + ) + ) + yield* (Deferred.await(deferred2)) + yield* (Fiber.interrupt(fiber)) + const result = yield* (Deferred.await(deferred1)) + assertTrue(result) + })) + it.effect("interrupted cause persists after catching", () => + Effect.gen(function*() { + const process = (list: Chunk.Chunk>): Chunk.Chunk> => { + return pipe(list, Chunk.map(Exit.mapErrorCause((cause) => cause))) + } + const latch1 = yield* (Deferred.make()) + const latch2 = yield* (Deferred.make()) + const exits = yield* (Ref.make(Chunk.empty>())) + const fiber = yield* pipe( + Effect.uninterruptibleMask((restore) => + pipe( + restore(pipe( + Effect.uninterruptibleMask((restore) => + pipe( + restore(pipe(Deferred.succeed(latch1, void 0), Effect.zipRight(Deferred.await(latch2)))), + Effect.onExit((exit) => Ref.update(exits, Chunk.prepend(exit))) + ) + ), + Effect.asVoid + )), + Effect.exit, + Effect.flatMap((exit) => Ref.update(exits, Chunk.prepend(exit))) + ) + ), + Effect.fork + ) + yield* pipe(Deferred.await(latch1), Effect.zipRight(Fiber.interrupt(fiber))) + const result = yield* pipe(Ref.get(exits), Effect.map(process)) + strictEqual(Chunk.size(result), 2) + assertTrue(pipe( + result, + Array.reduce(true, (acc, curr) => + acc && Exit.isFailure(curr) && Cause.isInterruptedOnly(curr.effect_instruction_i0)) + )) + })) + it.effect("interruption of raced", () => + Effect.gen(function*() { + const ref = yield* (Ref.make(0)) + const latch1 = yield* (Deferred.make()) + const latch2 = yield* (Deferred.make()) + const make = (deferred: Deferred.Deferred) => { + return pipe( + Deferred.succeed(deferred, void 0), + Effect.zipRight(Effect.never), + Effect.onInterrupt(() => Ref.update(ref, (n) => n + 1)) + ) + } + const raced = yield* pipe(make(latch1), Effect.race(make(latch2)), Effect.fork) + yield* pipe(Deferred.await(latch1), Effect.zipRight(Deferred.await(latch2))) + yield* (Fiber.interrupt(raced)) + const result = yield* (Ref.get(ref)) + strictEqual(result, 2) + })) + it.effect("recovery of error in finalizer", () => + Effect.gen(function*() { + const recovered = yield* (Ref.make(false)) + const fiber = yield* (withLatch((release) => + pipe( + release, + Effect.zipRight(Effect.never), + Effect.ensuring(pipe( + Effect.void, + Effect.zipRight(Effect.fail("uh oh")), + Effect.catchAll(() => Ref.set(recovered, true)) + )), + Effect.fork + ) + )) + yield* (Fiber.interrupt(fiber)) + const result = yield* (Ref.get(recovered)) + assertTrue(result) + })) + it.effect("recovery of interruptible", () => + Effect.gen(function*() { + const recovered = yield* (Ref.make(false)) + const fiber = yield* (withLatch((release) => + pipe( + release, + Effect.zipRight(pipe(Effect.never, Effect.interruptible)), + Effect.matchCauseEffect({ + onFailure: (cause) => Ref.set(recovered, Cause.isInterrupted(cause)), + onSuccess: () => Ref.set(recovered, false) + }), + Effect.uninterruptible, + Effect.fork + ) + )) + yield* (Fiber.interrupt(fiber)) + const result = yield* (Ref.get(recovered)) + assertTrue(result) + })) + it.effect("sandbox of interruptible", () => + Effect.gen(function*() { + const recovered = yield* (Ref.make>>(Option.none())) + const fiber = yield* (withLatch((release) => + pipe( + release, + Effect.zipRight(pipe(Effect.never, Effect.interruptible)), + Effect.sandbox, + Effect.either, + Effect.flatMap((either) => + Ref.set(recovered, Option.some(pipe(either, Either.mapLeft(Cause.isInterrupted)))) + ), + Effect.uninterruptible, + Effect.fork + ) + )) + yield* (Fiber.interrupt(fiber)) + const result = yield* (Ref.get(recovered)) + assertSome(result, Either.left(true)) + })) + it.effect("run of interruptible", () => + Effect.gen(function*() { + const recovered = yield* (Ref.make>(Option.none())) + const fiber = yield* (withLatch((release) => + pipe( + release, + Effect.zipRight(pipe(Effect.never, Effect.interruptible)), + Effect.exit, + Effect.flatMap((exit) => Ref.set(recovered, Option.some(Exit.isInterrupted(exit)))), + Effect.uninterruptible, + Effect.fork + ) + )) + yield* (Fiber.interrupt(fiber)) + const result = yield* (Ref.get(recovered)) + assertSome(result, true) + })) + it.effect("alternating interruptibility", () => + Effect.gen(function*() { + const counter = yield* (Ref.make(0)) + const fiber = yield* (withLatch((release) => + pipe( + release, + Effect.zipRight(pipe(Effect.never, Effect.interruptible, Effect.exit)), + Effect.zipRight(Ref.update(counter, (n) => n + 1)), + Effect.uninterruptible, + Effect.interruptible, + Effect.exit, + Effect.zipRight(Ref.update(counter, (n) => n + 1)), + Effect.uninterruptible, + Effect.fork + ) + )) + yield* (Fiber.interrupt(fiber)) + const result = yield* (Ref.get(counter)) + strictEqual(result, 2) + })) + it.effect("interruption after defect", () => + Effect.gen(function*() { + const ref = yield* (Ref.make(false)) + const fiber = yield* (withLatch((release) => + pipe( + Effect.try(() => { + throw new Error() + }), + Effect.exit, + Effect.zipRight(release), + Effect.zipRight(Effect.never), + Effect.ensuring(Ref.set(ref, true)), + Effect.fork + ) + )) + yield* (Fiber.interrupt(fiber)) + const result = yield* (Ref.get(ref)) + assertTrue(result) + })) + it.effect("interruption after defect 2", () => + Effect.gen(function*() { + const ref = yield* (Ref.make(false)) + const fiber = yield* (withLatch((release) => + pipe( + Effect.try(() => { + throw new Error() + }), + Effect.exit, + Effect.zipRight(release), + Effect.zipRight(pipe(Effect.void, Effect.forever)), + Effect.ensuring(Ref.set(ref, true)), + Effect.fork + ) + )) + yield* (Fiber.interrupt(fiber)) + const result = yield* (Ref.get(ref)) + assertTrue(result) + })) + it.effect("disconnect returns immediately on interrupt", () => + Effect.gen(function*() { + const deferred = yield* (Deferred.make()) + const fiber = yield* ( + pipe( + Deferred.succeed(deferred, void 0), + Effect.zipRight(Effect.never), + Effect.ensuring(Effect.never), + Effect.disconnect, + Effect.fork + ) + ) + yield* (Deferred.await(deferred)) + const result = yield* (Fiber.interrupt(fiber)) + assertTrue(Exit.isInterrupted(result)) + })) + it.live("disconnected effect that is then interrupted eventually performs interruption", () => + Effect.gen(function*() { + const ref = yield* (Ref.make(false)) + const deferred1 = yield* (Deferred.make()) + const deferred2 = yield* (Deferred.make()) + const fiber = yield* ( + pipe( + Deferred.succeed(deferred1, void 0), + Effect.zipRight(Effect.never), + Effect.ensuring( + pipe( + Ref.set(ref, true), + Effect.zipRight(Effect.sleep(Duration.millis(10))), + Effect.zipRight(Deferred.succeed(deferred2, void 0)) + ) + ), + Effect.disconnect, + Effect.fork + ) + ) + yield* (Deferred.await(deferred1)) + yield* (Fiber.interrupt(fiber)) + yield* (Deferred.await(deferred2)) + const result = yield* (Ref.get(ref)) + assertTrue(result) + })) + it.effect("cause reflects interruption", () => + Effect.gen(function*() { + const result = yield* pipe( + withLatch((release) => pipe(release, Effect.zipRight(Effect.fail("foo")), Effect.fork)), + Effect.flatMap(Fiber.interrupt) + ) + deepStrictEqual(result, Exit.fail("foo")) + })) + it.live("acquireRelease use inherits interrupt status", () => + Effect.gen(function*() { + const ref = yield* (Ref.make(false)) + const fiber = yield* (withLatchAwait((release2, await2) => + pipe( + withLatch((release1) => + pipe( + Effect.acquireUseRelease( + release1, + () => + pipe( + await2, + Effect.zipRight(Effect.sleep(Duration.millis(10))), + Effect.zipRight(Ref.set(ref, true)) + ), + () => Effect.void + ), + Effect.uninterruptible, + Effect.fork + ) + ), + Effect.zipLeft(release2) + ) + )) + yield* (Fiber.interrupt(fiber)) + const result = yield* (Ref.get(ref)) + assertTrue(result) + })) + it.live("acquireRelease use inherits interrupt status 2", () => + Effect.gen(function*() { + const latch1 = yield* (Deferred.make()) + const latch2 = yield* (Deferred.make()) + const ref = yield* (Ref.make(false)) + const fiber = yield* pipe( + Effect.acquireUseRelease( + Deferred.succeed(latch1, void 0), + () => + pipe( + Deferred.await(latch2), + Effect.zipRight(Effect.sleep(Duration.millis(10))), + Effect.zipRight(Ref.set(ref, true)), + Effect.asVoid + ), + () => Effect.void + ), + Effect.uninterruptible, + Effect.fork + ) + yield* (Deferred.await(latch1)) + yield* (Deferred.succeed(latch2, void 0)) + yield* (Fiber.interrupt(fiber)) + const result = yield* (Ref.get(ref)) + assertTrue(result) + })) + it.live("async can be uninterruptible", () => + Effect.gen(function*() { + const ref = yield* (Ref.make(false)) + const fiber = yield* (withLatch((release) => + pipe( + release, + Effect.zipRight(Effect.sleep(Duration.millis(10))), + Effect.zipRight(pipe(Ref.set(ref, true), Effect.asVoid)), + Effect.uninterruptible, + Effect.fork + ) + )) + yield* (Fiber.interrupt(fiber)) + const result = yield* (Ref.get(ref)) + assertTrue(result) + })) + it.live("closing scope is uninterruptible", () => + Effect.gen(function*() { + const ref = yield* (Ref.make(false)) + const deferred = yield* (Deferred.make()) + const child = pipe( + Deferred.succeed(deferred, void 0), + Effect.zipRight(Effect.sleep(Duration.millis(10))), + Effect.zipRight(Ref.set(ref, true)) + ) + const parent = pipe(child, Effect.uninterruptible, Effect.fork, Effect.zipRight(Deferred.await(deferred))) + const fiber = yield* (Effect.fork(parent)) + yield* (Deferred.await(deferred)) + yield* (Fiber.interrupt(fiber)) + const result = yield* (Ref.get(ref)) + assertTrue(result) + })) + it.effect("async cancelation", () => + Effect.gen(function*() { + const ref = MutableRef.make(0) + const effect = Effect.async(() => { + pipe(ref, MutableRef.set(MutableRef.get(ref) + 1)) + return Effect.sync(() => { + pipe(ref, MutableRef.set(MutableRef.get(ref) - 1)) + }) + }) + yield* pipe(Effect.void, Effect.race(effect)) + const result = MutableRef.get(ref) + strictEqual(result, 0) + })) + it.effect("interruption status is inheritable", () => + Effect.gen(function*() { + const latch = yield* (Deferred.make()) + const ref = yield* (Ref.make(true)) + yield* pipe( + Effect.checkInterruptible((isInterruptible) => + pipe(Ref.set(ref, isInterruptible), Effect.zipRight(Deferred.succeed(latch, void 0))) + ), + Effect.fork, + Effect.zipRight(Deferred.await(latch)), + Effect.uninterruptible + ) + const result = yield* (Ref.get(ref)) + assertFalse(result) + })) + it.effect("running an effect preserves interruption status", () => + Effect.gen(function*() { + const deferred = yield* (Deferred.make()) + const fiber = yield* ( + pipe(Deferred.succeed(deferred, void 0), Effect.zipRight(Effect.never), Effect.fork) + ) + yield* (Deferred.await(deferred)) + const result = yield* (Fiber.interrupt(fiber)) + assertTrue( + Exit.isFailure(result) && Exit.isInterrupted(result) && Cause.isInterruptedOnly(result.effect_instruction_i0) + ) + })) + it.effect("running an effect swallows inner interruption", () => + Effect.gen(function*() { + const deferred = yield* (Deferred.make()) + yield* pipe(Effect.interrupt, Effect.exit, Effect.zipRight(Deferred.succeed(deferred, 42))) + const result = yield* (Deferred.await(deferred)) + strictEqual(result, 42) + })) + it.effect("AbortSignal is aborted", () => + Effect.gen(function*() { + let signal: AbortSignal + const fiber = yield* pipe( + Effect.async((_cb, signal_) => { + signal = signal_ + }), + Effect.fork + ) + yield* (Effect.yieldNow()) + yield* (Fiber.interrupt(fiber)) + strictEqual(signal!.aborted, true) + })) +}) diff --git a/repos/effect/packages/effect/test/Effect/join-order.test.ts b/repos/effect/packages/effect/test/Effect/join-order.test.ts new file mode 100644 index 0000000..dfe6b88 --- /dev/null +++ b/repos/effect/packages/effect/test/Effect/join-order.test.ts @@ -0,0 +1,21 @@ +import { describe, it } from "@effect/vitest" +import { strictEqual } from "@effect/vitest/utils" +import * as Effect from "effect/Effect" +import * as Fiber from "effect/Fiber" +import * as FiberRef from "effect/FiberRef" +import * as TestClock from "effect/TestClock" + +describe("Effect", () => { + it.effect("zip/all joins fibers in the correct order", () => + Effect.gen(function*() { + const ref = yield* FiberRef.make(5) + const fiber = yield* Effect.fork(Effect.zip( + FiberRef.set(ref, 10).pipe(Effect.delay("2 seconds")), + FiberRef.set(ref, 15), + { concurrent: true } + )) + yield* TestClock.adjust("3 seconds") + yield* Fiber.join(fiber) + strictEqual(yield* FiberRef.get(ref), 10) + }).pipe(Effect.scoped)) +}) diff --git a/repos/effect/packages/effect/test/Effect/latch.test.ts b/repos/effect/packages/effect/test/Effect/latch.test.ts new file mode 100644 index 0000000..89ec0b3 --- /dev/null +++ b/repos/effect/packages/effect/test/Effect/latch.test.ts @@ -0,0 +1,44 @@ +import { describe, it } from "@effect/vitest" +import { deepStrictEqual, strictEqual } from "@effect/vitest/utils" +import { Effect, Exit } from "effect" + +describe("Latch", () => { + it.effect("open works", () => + Effect.gen(function*() { + const latch = yield* Effect.makeLatch() + let fiber = yield* latch.await.pipe( + Effect.fork + ) + yield* Effect.yieldNow() + strictEqual(fiber.unsafePoll(), null) + yield* latch.open + deepStrictEqual(yield* fiber.await, Exit.void) + + fiber = yield* latch.await.pipe( + Effect.fork + ) + yield* Effect.yieldNow() + deepStrictEqual(fiber.unsafePoll(), Exit.void) + + yield* latch.close + fiber = yield* Effect.void.pipe( + latch.whenOpen, + Effect.fork + ) + yield* Effect.yieldNow() + strictEqual(fiber.unsafePoll(), null) + + yield* latch.release + deepStrictEqual(yield* fiber.await, Exit.void) + })) + + it.effect("subtype of Effect", () => + Effect.gen(function*() { + const latch = yield* Effect.makeLatch() + const fiber = yield* Effect.fork(latch) + + yield* latch.open + + deepStrictEqual(yield* fiber.await, Exit.void) + })) +}) diff --git a/repos/effect/packages/effect/test/Effect/lifting.test.ts b/repos/effect/packages/effect/test/Effect/lifting.test.ts new file mode 100644 index 0000000..9b42121 --- /dev/null +++ b/repos/effect/packages/effect/test/Effect/lifting.test.ts @@ -0,0 +1,48 @@ +import { describe, it } from "@effect/vitest" +import { strictEqual } from "@effect/vitest/utils" +import * as Effect from "effect/Effect" +import { pipe } from "effect/Function" + +describe("Effect", () => { + it.effect("liftPredicate", () => { + const isPositivePredicate = (n: number) => n > 0 + const onPositivePredicateError = (n: number) => `${n} is not positive` + const isNumberRefinement = (n: string | number): n is number => typeof n === "number" + const onNumberRefinementError = (n: string | number) => `${n} is not a number` + + return Effect.gen(function*() { + strictEqual( + yield* pipe(1, Effect.liftPredicate(isPositivePredicate, onPositivePredicateError)), + 1 + ) + strictEqual( + yield* pipe(-1, Effect.liftPredicate(isPositivePredicate, onPositivePredicateError), Effect.flip), + `-1 is not positive` + ) + strictEqual( + yield* pipe(1, Effect.liftPredicate(isNumberRefinement, onNumberRefinementError)), + 1 + ) + strictEqual( + yield* pipe("string", Effect.liftPredicate(isNumberRefinement, onNumberRefinementError), Effect.flip), + `string is not a number` + ) + strictEqual( + yield* Effect.liftPredicate(1, isPositivePredicate, onPositivePredicateError), + 1 + ) + strictEqual( + yield* Effect.liftPredicate(-1, isPositivePredicate, onPositivePredicateError).pipe(Effect.flip), + `-1 is not positive` + ) + strictEqual( + yield* Effect.liftPredicate(1, isNumberRefinement, onNumberRefinementError), + 1 + ) + strictEqual( + yield* Effect.liftPredicate("string", isNumberRefinement, onNumberRefinementError).pipe(Effect.flip), + `string is not a number` + ) + }) + }) +}) diff --git a/repos/effect/packages/effect/test/Effect/mapping.test.ts b/repos/effect/packages/effect/test/Effect/mapping.test.ts new file mode 100644 index 0000000..979c13e --- /dev/null +++ b/repos/effect/packages/effect/test/Effect/mapping.test.ts @@ -0,0 +1,134 @@ +import { describe, it } from "@effect/vitest" +import { assertFalse, assertLeft, assertTrue, deepStrictEqual, strictEqual } from "@effect/vitest/utils" +import * as Cause from "effect/Cause" +import * as Effect from "effect/Effect" +import * as Exit from "effect/Exit" +import { identity, pipe } from "effect/Function" +import * as Ref from "effect/Ref" + +const ExampleError = new Error("Oh noes!") + +const parseInt = (s: string): number => { + const n = Number.parseInt(s) + if (Number.isNaN(n)) { + throw new Cause.IllegalArgumentException() + } + return n +} + +const fib = (n: number): number => { + if (n <= 1) { + return n + } + return fib(n - 1) + fib(n - 2) +} + +describe("Effect", () => { + it.effect("flip must make error into value", () => + Effect.gen(function*() { + const result = yield* (Effect.flip(Effect.fail(ExampleError))) + deepStrictEqual(result, ExampleError) + })) + it.effect("flip must make value into error", () => + Effect.gen(function*() { + const result = yield* (Effect.either(Effect.flip(Effect.succeed(42)))) + assertLeft(result, 42) + })) + it.effect("flipping twice returns the identical value", () => + Effect.gen(function*() { + const result = yield* (Effect.flip(Effect.flip(Effect.succeed(42)))) + strictEqual(result, 42) + })) + it.effect("mapBoth - maps over both error and value channels", () => + Effect.gen(function*() { + const result = yield* pipe( + Effect.fail(10), + Effect.mapBoth({ + onFailure: (n) => n.toString(), + onSuccess: identity + }), + Effect.either + ) + assertLeft(result, "10") + })) + it.effect("mapAccum", () => + Effect.gen(function*() { + const result = yield* ( + Effect.mapAccum(["a", "b"], "", (prev, cur, i) => Effect.succeed([prev + cur + i, cur])) + ) + deepStrictEqual(result, ["a0b1", ["a", "b"]]) + })) + it.effect("tryMap - returns an effect whose success is mapped by the specified side effecting function", () => + Effect.gen(function*() { + const result = yield* pipe(Effect.succeed("123"), Effect.tryMap({ try: parseInt, catch: identity })) + strictEqual(result, 123) + })) + it.effect("tryMap - translates any thrown exceptions into typed failed effects", () => + Effect.gen(function*() { + const result = yield* pipe( + Effect.succeed("hello"), + Effect.tryMap({ try: parseInt, catch: identity }), + Effect.exit + ) + deepStrictEqual(result, Exit.fail(new Cause.IllegalArgumentException())) + })) + it.effect("negate - on true returns false", () => + Effect.gen(function*() { + const result = yield* (Effect.negate(Effect.succeed(true))) + assertFalse(result) + })) + it.effect("negate - on false returns true", () => + Effect.gen(function*() { + const result = yield* (Effect.negate(Effect.succeed(false))) + assertTrue(result) + })) + it.effect("summarized - returns summary and value", () => + Effect.gen(function*() { + const counter = yield* (Ref.make(0)) + const increment = Ref.updateAndGet(counter, (n) => n + 1) + const [[start, end], value] = yield* ( + pipe(increment, Effect.summarized(increment, (start, end) => [start, end] as const)) + ) + strictEqual(start, 1) + strictEqual(value, 2) + strictEqual(end, 3) + })) + it.effect("point, bind, map", () => + Effect.gen(function*() { + const fibEffect = (n: number): Effect.Effect => { + if (n <= 1) { + return Effect.succeed(n) + } + return pipe(fibEffect(n - 1), Effect.zipWith(fibEffect(n - 2), (a, b) => a + b)) + } + const result = yield* (fibEffect(10)) + strictEqual(result, fib(10)) + })) + it.effect("effect, bind, map", () => + Effect.gen(function*() { + const fibEffect = (n: number): Effect.Effect => { + if (n <= 1) { + return Effect.try(() => n) + } + return pipe(fibEffect(n - 1), Effect.zipWith(fibEffect(n - 2), (a, b) => a + b)) + } + const result = yield* (fibEffect(10)) + strictEqual(result, fib(10)) + })) + it.effect("effect, bind, map, redeem", () => + Effect.gen(function*() { + const fibEffect = (n: number): Effect.Effect => { + if (n <= 1) { + return pipe( + Effect.try(() => { + throw ExampleError + }), + Effect.catchAll(() => Effect.try(() => n)) + ) + } + return pipe(fibEffect(n - 1), Effect.zipWith(fibEffect(n - 2), (a, b) => a + b)) + } + const result = yield* (fibEffect(10)) + strictEqual(result, fib(10)) + })) +}) diff --git a/repos/effect/packages/effect/test/Effect/memoization.test.ts b/repos/effect/packages/effect/test/Effect/memoization.test.ts new file mode 100644 index 0000000..5048031 --- /dev/null +++ b/repos/effect/packages/effect/test/Effect/memoization.test.ts @@ -0,0 +1,47 @@ +import { describe, it } from "@effect/vitest" +import { strictEqual } from "@effect/vitest/utils" +import { notStrictEqual } from "assert" +import * as Effect from "effect/Effect" +import { pipe } from "effect/Function" +import * as Random from "effect/Random" +import * as Ref from "effect/Ref" + +describe("Effect", () => { + it.effect("non-memoized returns new instances on repeated calls", () => + it.flakyTest(Effect.gen(function*() { + const random = Random.nextInt + const [first, second] = yield* pipe(random, Effect.zip(random)) + notStrictEqual(first, second) + }))) + it.effect("memoized returns the same instance on repeated calls", () => + it.flakyTest(Effect.gen(function*() { + const memo = Effect.cached(Random.nextInt) + const [first, second] = yield* pipe(memo, Effect.flatMap((effect) => pipe(effect, Effect.zip(effect)))) + strictEqual(first, second) + }))) + it.effect("memoized function returns the same instance on repeated calls", () => + it.flakyTest(Effect.gen(function*() { + const randomNumber = (n: number) => Random.nextIntBetween(n, n + n + 1) + const memoized = yield* (Effect.cachedFunction(randomNumber)) + const a = yield* (memoized(10)) + const b = yield* (memoized(10)) + const c = yield* (memoized(11)) + const d = yield* (memoized(11)) + strictEqual(a, b) + notStrictEqual(b, c) + strictEqual(c, d) + }))) + it.effect("once returns an effect that will only be executed once", () => + Effect.gen(function*() { + const ref = yield* (Ref.make(0)) + const effect: Effect.Effect = yield* pipe(Ref.update(ref, (n) => n + 1), Effect.once) + yield* ( + Effect.all(Effect.replicate(effect, 100), { + concurrency: "unbounded", + discard: true + }) + ) + const result = yield* (Ref.get(ref)) + strictEqual(result, 1) + })) +}) diff --git a/repos/effect/packages/effect/test/Effect/optional-wrapping-unwrapping.test.ts b/repos/effect/packages/effect/test/Effect/optional-wrapping-unwrapping.test.ts new file mode 100644 index 0000000..b04d88c --- /dev/null +++ b/repos/effect/packages/effect/test/Effect/optional-wrapping-unwrapping.test.ts @@ -0,0 +1,97 @@ +import { assert, describe, it } from "@effect/vitest" +import * as Effect from "effect/Effect" +import * as Either from "effect/Either" +import { pipe } from "effect/Function" +import * as Option from "effect/Option" + +describe("Effect", () => { + describe("transposeOption", () => { + it.effect("None", () => + Effect.gen(function*() { + const result = yield* Effect.transposeOption(Option.none()) + assert.ok(Option.isNone(result)) + })) + + it.effect("Some", () => + Effect.gen(function*() { + const result = yield* Effect.transposeOption(Option.some(Effect.succeed(42))) + assert.deepStrictEqual(result, Option.some(42)) + })) + }) + describe("transposeMapOption", () => { + describe("None", () => { + it.effect("Success", () => + Effect.gen(function*() { + const resultDataFirst = yield* Effect.transposeMapOption(Option.none(), () => Effect.succeed(42)) + assert.ok(Option.isNone(resultDataFirst)) + + const resultDataLast = yield* pipe( + Option.none(), + Effect.transposeMapOption(() => Effect.succeed(42)) + ) + assert.ok(Option.isNone(resultDataLast)) + })) + it.effect("Failure", () => + Effect.gen(function*() { + const resultDataFirst = yield* Effect.transposeMapOption(Option.none(), () => Effect.fail("Error")) + assert.ok(Option.isNone(resultDataFirst)) + + const resultDataLast = yield* pipe( + Option.none(), + Effect.transposeMapOption(() => Effect.fail("Error")) + ) + assert.ok(Option.isNone(resultDataLast)) + })) + }) + + describe("Some", () => { + describe("None", () => { + it.effect("Success", () => + Effect.gen(function*() { + const resultDataFirst = yield* Effect.transposeMapOption(Option.some(42), (value) => + Effect.succeed(value * 2)) + assert.deepStrictEqual(resultDataFirst, Option.some(84)) + + const resultDataLast = yield* pipe( + Option.some(42), + Effect.transposeMapOption((value) => + Effect.succeed(value * 2) + ) + ) + assert.deepStrictEqual(resultDataLast, Option.some(84)) + })) + it.effect("Failure", () => + Effect.gen(function*() { + const resultDataFirst = yield* pipe( + Effect.transposeMapOption(Option.some(42), () => Effect.fail("error")), + Effect.flip + ) + assert.equal(resultDataFirst, "error") + + const resultDataLast = yield* pipe( + Option.some(42), + Effect.transposeMapOption(() => Effect.fail("error")), + Effect.flip + ) + assert.equal(resultDataLast, "error") + })) + }) + }) + }) +}) + +describe("Either", () => { + describe("transposeOption", () => { + it.effect("None", () => + Effect.gen(function*() { + const result = yield* Either.transposeOption(Option.none()) + assert.ok(Option.isNone(result)) + })) + + it.effect("Some", () => + Effect.gen(function*() { + const result = yield* Either.transposeOption(Option.some(Either.right(42))) + assert.deepStrictEqual(result, Option.some(42)) + })) + }) +}) diff --git a/repos/effect/packages/effect/test/Effect/promise.test.ts b/repos/effect/packages/effect/test/Effect/promise.test.ts new file mode 100644 index 0000000..0feba4c --- /dev/null +++ b/repos/effect/packages/effect/test/Effect/promise.test.ts @@ -0,0 +1,66 @@ +import { describe, it } from "@effect/vitest" +import { assertFailure, assertSuccess, assertTrue } from "@effect/vitest/utils" +import { Cause, Effect, Option } from "effect" + +const succeedPromiseLike: PromiseLike = { + // @ts-ignore + then(onfulfilled) { + if (onfulfilled) { + onfulfilled("succeed") + } + return this + } +} + +const failPromiseLike: PromiseLike = { + // @ts-ignore + then(_, onrejected) { + if (onrejected) { + onrejected("fail") + } + return this + } +} + +describe("Effect", () => { + it("promise - success with AbortSignal", async () => { + let aborted = false + const effect = Effect.promise((signal) => { + signal.addEventListener("abort", () => { + aborted = true + }, { once: true }) + return new Promise((resolve) => { + setTimeout(() => { + resolve() + }, 100) + }) + }) + const program = effect.pipe( + Effect.timeout("10 millis"), + Effect.option + ) + const exit = await Effect.runPromiseExit(program) + assertSuccess(exit, Option.none()) + assertTrue(aborted) + }) + + it("PromiseLike - succeed", async () => { + const effect = Effect.promise(() => succeedPromiseLike) + const program = effect.pipe( + Effect.timeout("10 millis"), + Effect.option + ) + const exit = await Effect.runPromiseExit(program) + assertSuccess(exit, Option.some("succeed")) + }) + + it("PromiseLike - fail", async () => { + const effect = Effect.promise(() => failPromiseLike) + const program = effect.pipe( + Effect.timeout("10 millis"), + Effect.option + ) + const exit = await Effect.runPromiseExit(program) + assertFailure(exit, Cause.die("fail")) + }) +}) diff --git a/repos/effect/packages/effect/test/Effect/provide-runtime.test.ts b/repos/effect/packages/effect/test/Effect/provide-runtime.test.ts new file mode 100644 index 0000000..18b803f --- /dev/null +++ b/repos/effect/packages/effect/test/Effect/provide-runtime.test.ts @@ -0,0 +1,81 @@ +import { describe, it } from "@effect/vitest" +import { assertFalse, assertTrue, deepStrictEqual, strictEqual } from "@effect/vitest/utils" +import * as Context from "effect/Context" +import * as Effect from "effect/Effect" +import * as Equal from "effect/Equal" +import * as Exit from "effect/Exit" +import * as FiberRef from "effect/FiberRef" +import { pipe } from "effect/Function" +import * as Layer from "effect/Layer" +import * as RuntimeFlags from "effect/RuntimeFlags" +import * as Scope from "effect/Scope" + +interface A { + readonly value: number +} +const A = Context.GenericTag("A") +const LiveA = Layer.succeed(A, { value: 1 }) +const ref = FiberRef.unsafeMake(0) +const LiveEnv = Layer.mergeAll( + LiveA, + RuntimeFlags.enableOpSupervision, + Layer.scopedDiscard(Effect.locallyScoped(ref, 2)) +) + +describe("Effect", () => { + it.effect("provideSomeRuntime doesn't break env", () => { + const someServiceImpl = { + value: 42 + } as const + interface SomeService { + readonly _: unique symbol + } + const SomeService = Context.GenericTag("SomeService") + return Effect.gen(function*() { + const rt = yield* pipe(Layer.succeedContext(Context.empty()), Layer.toRuntime) + const pre = yield* Effect.context() + yield* Effect.provide(Effect.void, rt) + const post = yield* Effect.context() + assertTrue(Equal.equals(pre, post)) + }).pipe( + Effect.scoped, + Effect.provide(Layer.succeed(SomeService, someServiceImpl)) + ) + }) + it("provideSomeRuntime", async () => { + const { runtime, scope } = await Effect.runPromise( + Effect.flatMap(Scope.make(), (scope) => + Effect.map( + Scope.extend(Layer.toRuntime(LiveEnv), scope), + (runtime) => ({ runtime, scope }) + )) + ) + + const all = await Effect.runPromise(Effect.all( + [ + Effect.provide( + Effect.gen(function*() { + const a = yield* FiberRef.get(ref) + const b = yield* A + const c = RuntimeFlags.isEnabled(yield* Effect.getRuntimeFlags, RuntimeFlags.OpSupervision) + return { a, b, c } + }), + runtime + ), + Effect.gen(function*() { + const a = yield* FiberRef.get(ref) + const c = RuntimeFlags.isEnabled(yield* Effect.getRuntimeFlags, RuntimeFlags.OpSupervision) + return { a, c } + }) + ] + )) + + await Effect.runPromise(Scope.close(scope, Exit.void)) + + strictEqual(all[0].a, 2) + deepStrictEqual(all[0].b, { value: 1 }) + assertTrue(all[0].c) + strictEqual(all[1].a, 0) + assertFalse(all[1].c) + }) +}) diff --git a/repos/effect/packages/effect/test/Effect/query-deadlock.test.ts b/repos/effect/packages/effect/test/Effect/query-deadlock.test.ts new file mode 100644 index 0000000..7c30f86 --- /dev/null +++ b/repos/effect/packages/effect/test/Effect/query-deadlock.test.ts @@ -0,0 +1,36 @@ +import { describe, it } from "@effect/vitest" +import * as Effect from "effect/Effect" +import * as Exit from "effect/Exit" +import { pipe } from "effect/Function" +import * as Request from "effect/Request" +import * as Resolver from "effect/RequestResolver" + +export const userIds: ReadonlyArray = [1, 1] + +interface GetNameById extends Request.Request { + readonly _tag: "GetNameById" + readonly id: number +} +const GetNameById = Request.tagged("GetNameById") + +const UserResolver = Resolver.makeBatched((requests: Array) => + Effect.forEach(requests, (request) => + Request.complete( + request, + Exit.succeed("ok") + ), { discard: true }) +) + +const getUserNameById = (id: number) => Effect.request(GetNameById({ id }), UserResolver) +const getAllUserNames = Effect.forEach([1, 1], getUserNameById, { batching: true }) + +describe("Effect", () => { + it("requests are executed correctly", () => + Effect.runPromise( + Effect.asVoid(pipe( + getAllUserNames, + Effect.withRequestCaching(true), + Effect.withRequestBatching(true) + )) + )) +}) diff --git a/repos/effect/packages/effect/test/Effect/query-defect.test.ts b/repos/effect/packages/effect/test/Effect/query-defect.test.ts new file mode 100644 index 0000000..f7d97be --- /dev/null +++ b/repos/effect/packages/effect/test/Effect/query-defect.test.ts @@ -0,0 +1,61 @@ +import { describe, it } from "@effect/vitest" +import { assertTrue } from "@effect/vitest/utils" +import * as Effect from "effect/Effect" +import * as Exit from "effect/Exit" +import * as Fiber from "effect/Fiber" +import * as Request from "effect/Request" +import * as RequestResolver from "effect/RequestResolver" + +class GetValue extends Request.TaggedClass("GetValue") {} + +describe("batched resolver defect", () => { + // When a batched resolver dies with a defect, the request Deferreds are + // never completed and consumers hang forever. The cleanup that completes + // uncompleted entries only runs on success (flatMap/OP_ON_SUCCESS) but + // should run on all exits. + it.live("resolver defect should not hang consumers", () => + Effect.gen(function*() { + const resolver = RequestResolver.makeBatched((_requests: Array) => Effect.die("boom")) + + const fiber = yield* Effect.request(new GetValue({ id: 1 }), resolver).pipe( + Effect.fork + ) + + // Wait briefly then check if the fiber completed. + // If the bug is present, the fiber hangs on deferredAwait forever. + yield* Effect.sleep("500 millis") + const poll = yield* Fiber.poll(fiber) + + assertTrue( + poll._tag === "Some", + "Fiber should have completed — resolver defect must not leave consumers hanging" + ) + + if (poll._tag === "Some") { + assertTrue(Exit.isFailure(poll.value)) + } + })) + + it.live("resolver defect should not hang multiple consumers", () => + Effect.gen(function*() { + const resolver = RequestResolver.makeBatched((_requests: Array) => Effect.die("boom")) + + const fiber = yield* Effect.forEach( + [1, 2, 3], + (id) => Effect.request(new GetValue({ id }), resolver), + { batching: true, concurrency: "unbounded" } + ).pipe(Effect.fork) + + yield* Effect.sleep("500 millis") + const poll = yield* Fiber.poll(fiber) + + assertTrue( + poll._tag === "Some", + "Fiber should have completed — resolver defect must not leave consumers hanging" + ) + + if (poll._tag === "Some") { + assertTrue(Exit.isFailure(poll.value)) + } + })) +}) diff --git a/repos/effect/packages/effect/test/Effect/query-nested.test.ts b/repos/effect/packages/effect/test/Effect/query-nested.test.ts new file mode 100644 index 0000000..0e1e7b4 --- /dev/null +++ b/repos/effect/packages/effect/test/Effect/query-nested.test.ts @@ -0,0 +1,179 @@ +import { describe, it } from "@effect/vitest" +import { strictEqual } from "@effect/vitest/utils" +import * as Array from "effect/Array" +import * as Context from "effect/Context" +import { seconds } from "effect/Duration" +import * as Effect from "effect/Effect" +import * as Layer from "effect/Layer" +import * as Request from "effect/Request" +import * as Resolver from "effect/RequestResolver" + +interface Counter { + readonly _: unique symbol +} +const Counter = Context.GenericTag("counter") +interface Requests { + readonly _: unique symbol +} +const Requests = Context.GenericTag("requests") + +interface Parent { + readonly id: number +} + +interface Child { + readonly id: number + readonly parentId: number +} + +interface ChildInfo { + readonly id: number + readonly childId: number + readonly name: string +} + +interface ChildExtra { + readonly id: number + readonly childId: number + readonly extra: string +} + +export interface GetAllParents extends Request.Request> { + readonly _tag: "GetAllParents" +} + +export const GetAllParents = Request.tagged("GetAllParents") + +export interface GetParentChildren extends Request.Request> { + readonly _tag: "GetParentChildren" + readonly id: number +} + +export const GetParentChildren = Request.tagged("GetParentChildren") + +export interface GetChildInfo extends Request.Request { + readonly _tag: "GetChildInfo" + readonly id: number +} + +export const GetChildInfo = Request.tagged("GetChildInfo") + +export interface GetChildExtra extends Request.Request { + readonly _tag: "GetChildExtra" + readonly id: number +} + +export const GetChildExtra = Request.tagged("GetChildExtra") + +export const parents = Array.range(1, 2).map((id) => ({ id })) + +export const children: ReadonlyMap> = new Map( + Array.map(parents, (p) => [ + p.id, + Array.of({ id: p.id * 10, parentId: p.id }) + ]) +) + +const counted = (self: Effect.Effect) => Effect.tap(self, () => Effect.map(Counter, (c) => c.count++)) + +const AllResolver = Resolver.makeBatched(( + requests: Array +) => + Effect.flatMap(Requests, (r) => { + r.count += requests.length + return counted(Effect.all([ + Effect.forEach( + requests.filter((_): _ is GetParentChildren => _._tag === "GetParentChildren"), + (request) => Request.succeed(request, children.get(request.id)!) + ), + Effect.forEach( + requests.filter((_): _ is GetChildExtra => _._tag === "GetChildExtra"), + (request) => + Request.succeed(request, { + id: request.id * 10, + childId: request.id, + extra: "more stuff" + }) + ), + Effect.forEach( + requests.filter((_): _ is GetChildInfo => _._tag === "GetChildInfo"), + (request) => + Request.succeed(request, { + id: request.id * 10, + childId: request.id, + name: "Mike" + }) + ), + Effect.forEach( + requests.filter((_): _ is GetAllParents => _._tag === "GetAllParents"), + (request) => Request.succeed(request, parents) + ) + ])) + }) +).pipe( + Resolver.batchN(15), + Resolver.contextFromServices(Counter, Requests) +) + +export const getAllParents = Effect.request(GetAllParents({}), AllResolver) +export const getChildren = (id: number) => Effect.request(GetParentChildren({ id }), AllResolver) +export const getChildInfo = (id: number) => Effect.request(GetChildInfo({ id }), AllResolver) +export const getChildExtra = (id: number) => Effect.request(GetChildExtra({ id }), AllResolver) + +const EnvLive = Layer.mergeAll( + Layer.sync(Counter, () => ({ count: 0 })), + Layer.sync(Requests, () => ({ count: 0 })) +).pipe(Layer.provideMerge( + Layer.mergeAll( + Layer.setRequestCache(Request.makeCache({ + capacity: 100, + timeToLive: seconds(60) + })), + Layer.setRequestCaching(true), + Layer.setRequestBatching(true) + ) +)) + +describe("Effect", () => { + it.effect("nested queries are batched", () => + Effect.gen(function*() { + const parents = yield* getAllParents + + yield* Effect.forEach( + parents, + (parent) => + Effect.flatMap( + getChildren(parent.id), + (children) => + Effect.forEach( + children, + (child) => + Effect.zip( + getChildInfo(child.id), + getChildExtra(child.id), + { + concurrent: true, + batching: "inherit" + } + ), + { + concurrency: "unbounded", + batching: "inherit" + } + ) + ), + { + concurrency: "inherit", + batching: "inherit" + } + ) + + const count = yield* Counter + const requests = yield* Requests + + strictEqual(count.count, 3) + strictEqual(requests.count, 7) + }).pipe( + Effect.provide(EnvLive) + )) +}) diff --git a/repos/effect/packages/effect/test/Effect/query-repro.test.ts b/repos/effect/packages/effect/test/Effect/query-repro.test.ts new file mode 100644 index 0000000..c0c8fbe --- /dev/null +++ b/repos/effect/packages/effect/test/Effect/query-repro.test.ts @@ -0,0 +1,46 @@ +import { describe, it } from "@effect/vitest" +import { strictEqual } from "@effect/vitest/utils" +import * as Effect from "effect/Effect" +import * as Layer from "effect/Layer" +import * as Request from "effect/Request" +import * as RequestResolver from "effect/RequestResolver" + +export class FindIntraday extends Request.TaggedClass("FindIntraday") {} + +const make = Effect.sync(function() { + const getIntradayResolver = RequestResolver.makeBatched((requests: Array) => + Effect.all(requests.map(Request.succeed(null))) + ) + + const getIntraday = (symbol: string) => + Effect.withRequestCaching(true)( + Effect.request(new FindIntraday({ symbol }), getIntradayResolver) + ) + + return { getIntraday } +}) + +class Svc extends Effect.Tag("svc")>() { + static readonly Live = Layer.scoped(Svc, make) +} + +const getSub = (symbol: string) => + Effect.all([ + Effect.sleep("20 millis"), + Svc.getIntraday(symbol) + ], { concurrency: 2, batching: true }) + +const getItems = getSub("test_1") + +describe("interruption", () => { + it.live("forEach interrupts residual requests", () => + Effect.gen(function*() { + const exit = yield* getItems.pipe( + Effect.timeout("10 millis"), + Effect.catchAll(() => getItems), + Effect.provide(Svc.Live), + Effect.exit + ) + strictEqual(exit._tag, "Success") + })) +}) diff --git a/repos/effect/packages/effect/test/Effect/query.test.ts b/repos/effect/packages/effect/test/Effect/query.test.ts new file mode 100644 index 0000000..237e5ba --- /dev/null +++ b/repos/effect/packages/effect/test/Effect/query.test.ts @@ -0,0 +1,431 @@ +import { describe, it } from "@effect/vitest" +import { assertTrue, deepStrictEqual, strictEqual } from "@effect/vitest/utils" +import * as Array from "effect/Array" +import * as Cause from "effect/Cause" +import * as Context from "effect/Context" +import { seconds } from "effect/Duration" +import * as Effect from "effect/Effect" +import * as Exit from "effect/Exit" +import * as Fiber from "effect/Fiber" +import * as FiberRef from "effect/FiberRef" +import { pipe } from "effect/Function" +import * as Layer from "effect/Layer" +import * as Request from "effect/Request" +import * as Resolver from "effect/RequestResolver" +import * as TestClock from "effect/TestClock" +import type { Concurrency } from "effect/Types" + +interface Counter { + readonly _: unique symbol +} +const Counter = Context.GenericTag("counter") +interface Requests { + readonly _: unique symbol +} +const Requests = Context.GenericTag("requests") + +export const userIds: ReadonlyArray = Array.range(1, 26) + +export const userNames: ReadonlyMap = new Map( + Array.zipWith( + userIds, + Array.map(Array.range(97, 122), (a) => String.fromCharCode(a)), + (a, b) => [a, b] as const + ) +) + +export type UserRequest = GetAllIds | GetNameById + +export interface GetAllIds extends Request.Request> { + readonly _tag: "GetAllIds" +} + +export const GetAllIds = Request.tagged("GetAllIds") + +export class GetNameById extends Request.TaggedClass("GetNameById") {} + +const delay = (self: Effect.Effect) => + Effect.zipRight( + Effect.promise(() => new Promise((r) => setTimeout(() => r(0), 0))), + self + ) + +const counted = (self: Effect.Effect) => Effect.tap(self, () => Effect.map(Counter, (c) => c.count++)) + +const UserResolver = Resolver.makeBatched((requests: Array) => + Effect.flatMap(Requests, (r) => { + r.count += requests.length + return counted(Effect.forEach(requests, (request) => delay(processRequest(request)), { discard: true })) + }) +).pipe( + Resolver.batchN(15), + Resolver.contextFromServices(Counter, Requests) +) + +export const getAllUserIds = Effect.request(GetAllIds({}), UserResolver) + +export const interrupts = FiberRef.unsafeMake({ interrupts: 0 }) + +export const getUserNameById = (id: number) => Effect.request(new GetNameById({ id }), UserResolver) + +export const getUserNameByIdPiped = (id: number) => pipe(new GetNameById({ id }), Effect.request(UserResolver)) + +export const getAllUserNamesN = (concurrency: Concurrency) => + getAllUserIds.pipe( + Effect.flatMap(Effect.forEach(getUserNameById, { concurrency, batching: true })), + Effect.onInterrupt(() => FiberRef.getWith(interrupts, (i) => Effect.sync(() => i.interrupts++))) + ) + +export const getAllUserNamesPipedN = (concurrency: Concurrency) => + getAllUserIds.pipe( + Effect.flatMap(Effect.forEach(getUserNameById, { concurrency, batching: true })), + Effect.onInterrupt(() => FiberRef.getWith(interrupts, (i) => Effect.sync(() => i.interrupts++))) + ) + +export const getAllUserNames = getAllUserNamesN("unbounded") + +export const getAllUserNamesPiped = getAllUserNamesPipedN("unbounded") + +export const print = (request: UserRequest): string => { + switch (request._tag) { + case "GetAllIds": { + return request._tag + } + case "GetNameById": { + return `${request._tag}(${request.id})` + } + } +} + +const processRequest = (request: UserRequest): Effect.Effect => { + switch (request._tag) { + case "GetAllIds": { + return Request.complete(request, Exit.succeed(userIds)) + } + case "GetNameById": { + if (userNames.has(request.id)) { + const userName = userNames.get(request.id)! + return Request.complete(request, Exit.succeed(userName)) + } + return Request.completeEffect(request, Exit.fail("Not Found")) + } + } +} + +const UserResolverTagged = Resolver.fromEffectTagged()({ + GetAllIds: (reqs) => + counted(Effect.flatMap(Requests, (_) => { + _.count += reqs.length + return Effect.forEach(reqs, () => Effect.succeed(userIds)) + })), + GetNameById: (reqs) => + counted(Effect.flatMap(Requests, (_) => { + _.count += reqs.length + return Effect.forEach(reqs, (req) => { + if (userNames.has(req.id)) { + const userName = userNames.get(req.id)! + return Effect.succeed(userName) + } + return Effect.fail("Not Found") + }) + })) +}).pipe( + Resolver.batchN(15), + Resolver.contextFromServices(Counter, Requests) +) +export const getAllUserIdsTagged = Effect.request(GetAllIds({}), UserResolverTagged) +export const getUserNameByIdTagged = (id: number) => Effect.request(new GetNameById({ id }), UserResolverTagged) +export const getAllUserNamesTagged = getAllUserIdsTagged.pipe( + Effect.flatMap(Effect.forEach(getUserNameByIdTagged, { batching: true })) +) + +const EnvLive = Layer.mergeAll( + Layer.sync(Counter, () => ({ count: 0 })), + Layer.sync(Requests, () => ({ count: 0 })) +).pipe( + Layer.provideMerge( + Layer.mergeAll( + Layer.setRequestCache(Request.makeCache({ + capacity: 100, + timeToLive: seconds(60) + })), + Layer.setRequestCaching(true), + Layer.setRequestBatching(true) + ) + ) +) + +const provideEnv = Effect.provide(EnvLive) + +describe("Effect", () => { + it.effect("avoid false interruption when concurrency happens in resolver", () => + Effect.gen(function*() { + class RequestUserById extends Request.TaggedClass("RequestUserById") {} + let count = 0 + const resolver = Resolver.makeBatched((i) => { + count++ + return Effect.forEach(i, Request.complete(Exit.succeed(1)), { concurrency: "unbounded" }) + }) + yield* Effect.request(new RequestUserById({ id: "1" }), resolver).pipe( + Effect.withRequestCaching(true), + Effect.repeatN(3) + ) + strictEqual(count, 1) + })) + it.effect("requests are executed correctly", () => + provideEnv( + Effect.gen(function*() { + const names = yield* getAllUserNames + const count = yield* Counter + strictEqual(count.count, 3) + assertTrue(names.length > 2) + deepStrictEqual(names, userIds.map((id) => userNames.get(id))) + }) + )) + it.effect("requests with dual syntax are executed correctly", () => + provideEnv( + Effect.gen(function*() { + const names = yield* getAllUserNamesPiped + const count = yield* Counter + strictEqual(count.count, 3) + assertTrue(names.length > 2) + deepStrictEqual(names, userIds.map((id) => userNames.get(id))) + }) + )) + it.effect("requests are executed correctly with fromEffectTagged", () => + provideEnv( + Effect.gen(function*() { + const names = yield* getAllUserNamesTagged + const count = yield* Counter + strictEqual(count.count, 3) + assertTrue(names.length > 2) + deepStrictEqual(names, userIds.map((id) => userNames.get(id))) + }) + )) + it.effect("batching composes", () => + provideEnv( + Effect.gen(function*() { + const cache = yield* (FiberRef.get(FiberRef.currentRequestCache)) + yield* (cache.invalidateAll) + const names = yield* (Effect.zip(getAllUserNames, getAllUserNames, { + concurrent: true, + batching: true + })) + const count = yield* Counter + strictEqual(count.count, 3) + assertTrue(names[0].length > 2) + deepStrictEqual(names[0], userIds.map((id) => userNames.get(id))) + deepStrictEqual(names[0], names[1]) + }) + )) + it.effect("withSpan doesn't break batching", () => + provideEnv( + Effect.gen(function*() { + yield* pipe( + Effect.zip( + getAllUserIds.pipe(Effect.withSpan("A")), + getAllUserIds.pipe(Effect.withSpan("B")), + { concurrent: true, batching: true } + ), + Effect.withRequestCaching(false) + ) + const count = yield* Counter + strictEqual(count.count, 1) + }) + )) + it.effect("batching is independent from parallelism", () => + provideEnv( + Effect.gen(function*() { + const names = yield* (getAllUserNamesN(5)) + const count = yield* Counter + strictEqual(count.count, 3) + assertTrue(names.length > 2) + deepStrictEqual(names, userIds.map((id) => userNames.get(id))) + }) + )) + it.effect("batching doesn't break interruption", () => + Effect.locally(interrupts, { interrupts: 0 })( + provideEnv( + Effect.gen(function*() { + const exit = yield* pipe( + getAllUserNames, + Effect.zipLeft(Effect.interrupt, { + concurrent: true, + batching: true + }), + Effect.exit + ) + strictEqual(exit._tag, "Failure") + if (exit._tag === "Failure") { + assertTrue(Cause.isInterruptedOnly(exit.cause)) + } + const cache = yield* (FiberRef.get(FiberRef.currentRequestCache)) + const values = yield* (cache.values) + strictEqual(values[0].handle.state.current._tag, "Done") + deepStrictEqual(yield* Counter, { count: 0 }) + deepStrictEqual(yield* (FiberRef.get(interrupts)), { interrupts: 1 }) + }) + ) + )) + it.effect("requests dont't break interruption", () => + Effect.locally(interrupts, { interrupts: 0 })( + provideEnv( + Effect.gen(function*() { + const fiber = yield* pipe(getAllUserNames, Effect.fork) + yield* (Effect.yieldNow()) + yield* (Fiber.interrupt(fiber)) + const exit = yield* (Fiber.await(fiber)) + strictEqual(exit._tag, "Failure") + if (exit._tag === "Failure") { + assertTrue(Cause.isInterruptedOnly(exit.cause)) + } + deepStrictEqual(yield* Counter, { count: 0 }) + deepStrictEqual(yield* (FiberRef.get(interrupts)), { interrupts: 1 }) + }) + ) + )) + it.effect("requests work with uninterruptible", () => + Effect.locally(interrupts, { interrupts: 0 })( + provideEnv( + Effect.gen(function*() { + const fiber = yield* pipe(getAllUserNames, Effect.uninterruptible, Effect.fork) + yield* (Effect.yieldNow()) + yield* (Fiber.interrupt(fiber)) + const exit = yield* (Fiber.await(fiber)) + strictEqual(exit._tag, "Failure") + if (exit._tag === "Failure") { + assertTrue(Cause.isInterruptedOnly(exit.cause)) + } + deepStrictEqual(yield* Counter, { count: 3 }) + deepStrictEqual(yield* (FiberRef.get(interrupts)), { interrupts: 0 }) + }) + ) + )) + it.effect("batching doesn't break interruption when limited", () => + Effect.locally(interrupts, { interrupts: 0 })( + provideEnv( + Effect.gen(function*() { + const exit = yield* pipe( + getAllUserNames, + Effect.zipLeft(Effect.interrupt, { + concurrent: true, + batching: true + }), + Effect.exit + ) + strictEqual(exit._tag, "Failure") + if (exit._tag === "Failure") { + assertTrue(Cause.isInterruptedOnly(exit.cause)) + } + deepStrictEqual(yield* Counter, { count: 0 }) + deepStrictEqual(yield* (FiberRef.get(interrupts)), { interrupts: 1 }) + }) + ) + )) + it.effect("zip/parallel is not batched when specified", () => + provideEnv( + Effect.gen(function*() { + const [a, b] = yield* pipe( + Effect.zip( + getUserNameById(userIds[0]), + getUserNameById(userIds[1]), + { + concurrent: true, + batching: false + } + ), + Effect.withRequestBatching(true) + ) + const count = yield* Counter + strictEqual(count.count, 2) + deepStrictEqual(a, userNames.get(userIds[0])) + deepStrictEqual(b, userNames.get(userIds[1])) + }) + )) + it.effect("zip/parallel is batched by default", () => + provideEnv( + Effect.gen(function*() { + const [a, b] = yield* ( + Effect.zip( + getUserNameById(userIds[0]), + getUserNameById(userIds[1]), + { + concurrent: true, + batching: true + } + ) + ) + const count = yield* Counter + strictEqual(count.count, 1) + deepStrictEqual(a, userNames.get(userIds[0])) + deepStrictEqual(b, userNames.get(userIds[1])) + }) + )) + it.effect("cache respects ttl", () => + provideEnv( + Effect.gen(function*() { + yield* getAllUserIds + yield* getAllUserIds + deepStrictEqual(yield* Counter, { count: 1 }) + yield* (TestClock.adjust(seconds(10))) + yield* getAllUserIds + yield* getAllUserIds + deepStrictEqual(yield* Counter, { count: 1 }) + yield* (TestClock.adjust(seconds(60))) + yield* getAllUserIds + yield* getAllUserIds + deepStrictEqual(yield* Counter, { count: 2 }) + }) + )) + it.effect("cache can be warmed up", () => + provideEnv( + Effect.gen(function*() { + yield* (Effect.cacheRequestResult(GetAllIds({}), Exit.succeed(userIds))) + yield* getAllUserIds + yield* getAllUserIds + deepStrictEqual(yield* Counter, { count: 0 }) + yield* (TestClock.adjust(seconds(65))) + yield* getAllUserIds + yield* getAllUserIds + deepStrictEqual(yield* Counter, { count: 1 }) + }) + )) + it.effect("cache can be disabled", () => + provideEnv( + Effect.withRequestCaching(false)(Effect.gen(function*() { + yield* getAllUserIds + yield* getAllUserIds + deepStrictEqual(yield* Counter, { count: 2 }) + yield* (TestClock.adjust(seconds(10))) + yield* getAllUserIds + yield* getAllUserIds + deepStrictEqual(yield* Counter, { count: 4 }) + yield* (TestClock.adjust(seconds(60))) + yield* getAllUserIds + yield* getAllUserIds + deepStrictEqual(yield* Counter, { count: 6 }) + })) + )) + + it.effect("batching preserves individual & identical requests", () => + provideEnv( + Effect.gen(function*() { + yield* pipe( + Effect.all([getUserNameById(userIds[0]), getUserNameById(userIds[0])], { + concurrency: "unbounded", + batching: true, + discard: true + }), + Effect.withRequestCaching(false) + ) + const requests = yield* Requests + const invocations = yield* Counter + deepStrictEqual(requests.count, 2) + deepStrictEqual(invocations.count, 1) + }) + )) +}) diff --git a/repos/effect/packages/effect/test/Effect/racing.test.ts b/repos/effect/packages/effect/test/Effect/racing.test.ts new file mode 100644 index 0000000..d93f84d --- /dev/null +++ b/repos/effect/packages/effect/test/Effect/racing.test.ts @@ -0,0 +1,67 @@ +import { describe, it } from "@effect/vitest" +import { deepStrictEqual, strictEqual } from "@effect/vitest/utils" +import * as Duration from "effect/Duration" +import * as Effect from "effect/Effect" +import { pipe } from "effect/Function" +import * as TestClock from "effect/TestClock" + +describe("Effect", () => { + it.effect("raceAll waits losers interruption", () => + Effect.gen(function*() { + const messages: Array = [] + + const a = Effect.gen(function*() { + yield* Effect.addFinalizer(() => Effect.sync(() => messages.push("finalize a"))) + yield* Effect.sleep("100 millis") + yield* Effect.sync(() => messages.push("done a")) + }) + + const b = Effect.gen(function*() { + yield* Effect.addFinalizer(() => Effect.sync(() => messages.push("finalize b"))) + yield* Effect.sleep("200 millis") + yield* Effect.sync(() => messages.push("done b")) + }) + + yield* Effect.raceAll([ + Effect.scoped(a), + Effect.scoped(b) + ]).pipe( + Effect.tap(() => Effect.sync(() => messages.push("race done"))), + Effect.fork + ) + + yield* TestClock.adjust("300 millis") + + deepStrictEqual(messages, [ + "done a", + "finalize a", + "finalize b", + "race done" + ]) + })) + it.effect("returns first success", () => + Effect.gen(function*() { + const result = yield* (Effect.raceAll([Effect.fail("fail"), Effect.succeed(24)])) + strictEqual(result, 24) + })) + it.live("returns last failure", () => + Effect.gen(function*() { + const result = yield* ( + pipe( + Effect.raceAll([pipe(Effect.sleep(Duration.millis(100)), Effect.zipRight(Effect.fail(24))), Effect.fail(25)]), + Effect.flip + ) + ) + strictEqual(result, 24) + })) + it.live("returns success when it happens after failure", () => + Effect.gen(function*() { + const result = yield* ( + Effect.raceAll([ + Effect.fail(42), + pipe(Effect.succeed(24), Effect.zipLeft(Effect.sleep(Duration.millis(100)))) + ]) + ) + strictEqual(result, 24) + })) +}) diff --git a/repos/effect/packages/effect/test/Effect/rendezvous.test.ts b/repos/effect/packages/effect/test/Effect/rendezvous.test.ts new file mode 100644 index 0000000..ebd4f0c --- /dev/null +++ b/repos/effect/packages/effect/test/Effect/rendezvous.test.ts @@ -0,0 +1,49 @@ +import { describe, it } from "@effect/vitest" +import { deepStrictEqual } from "@effect/vitest/utils" +import * as Effect from "effect/Effect" +import * as Fiber from "effect/Fiber" +import * as Queue from "effect/Queue" +import * as TestClock from "effect/TestClock" + +describe("Effect", () => { + it.effect("bounded 0 is rendezvous", () => + Effect.gen(function*() { + const rendevous = yield* Queue.bounded(0) + const logs: Array = [] + + const fiber = yield* ( + Effect.fork( + Effect.gen(function*() { + yield* Effect.sleep("50 millis") + logs.push("sending message") + yield* Queue.offer(rendevous, "Hello World") + logs.push("sent message") + }) + ) + ) + + const fiber2 = yield* ( + Effect.fork( + Effect.gen(function*() { + yield* Effect.sleep("100 millis") + logs.push("receiving message") + const message = yield* Queue.take(rendevous) + logs.push("received message") + logs.push(message) + }) + ) + ) + + yield* TestClock.adjust("200 millis") + + yield* Fiber.join(Fiber.zip(fiber, fiber2)) + + deepStrictEqual(logs, [ + "sending message", + "receiving message", + "received message", + "Hello World", + "sent message" + ]) + })) +}) diff --git a/repos/effect/packages/effect/test/Effect/repeating.test.ts b/repos/effect/packages/effect/test/Effect/repeating.test.ts new file mode 100644 index 0000000..11b59f4 --- /dev/null +++ b/repos/effect/packages/effect/test/Effect/repeating.test.ts @@ -0,0 +1,205 @@ +import { describe, it } from "@effect/vitest" +import { deepStrictEqual, strictEqual } from "@effect/vitest/utils" +import * as Duration from "effect/Duration" +import * as Effect from "effect/Effect" +import { constFalse, constTrue, pipe } from "effect/Function" +import * as Ref from "effect/Ref" +import * as Schedule from "effect/Schedule" +import * as TestClock from "effect/TestClock" + +describe("Effect", () => { + it.effect("succeeds eventually", () => + Effect.gen(function*() { + const effect = (ref: Ref.Ref) => { + return pipe( + Ref.get(ref), + Effect.flatMap((n) => + n < 10 ? + pipe(Ref.update(ref, (n) => n + 1), Effect.zipRight(Effect.fail("Ouch"))) : + Effect.succeed(n) + ) + ) + } + const ref = yield* (Ref.make(0)) + const result = yield* (Effect.eventually(effect(ref))) + strictEqual(result, 10) + })) + + it.effect("repeat/until - repeats until condition is true", () => + Effect.gen(function*() { + const input = yield* (Ref.make(10)) + const output = yield* (Ref.make(0)) + yield* pipe( + Ref.updateAndGet(input, (n) => n - 1), + Effect.zipLeft(Ref.update(output, (n) => n + 1)), + Effect.repeat({ until: (n) => n === 0 }) + ) + const result = yield* (Ref.get(output)) + strictEqual(result, 10) + })) + + it.effect("repeat/until - preserves return value", () => + Effect.gen(function*() { + const input = yield* (Ref.make(10)) + const result = yield* pipe( + Ref.updateAndGet(input, (n) => n - 1), + Effect.repeat({ until: (n) => n === 0 }) + ) + strictEqual(result, 0) + })) + + it.effect("repeat/until - always evaluates effect at least once", () => + Effect.gen(function*() { + const ref = yield* (Ref.make(0)) + yield* pipe(Ref.update(ref, (n) => n + 1), Effect.repeat({ until: constTrue })) + const result = yield* (Ref.get(ref)) + strictEqual(result, 1) + })) + it.effect("repeat/until - repeats until the effectful condition is true", () => + Effect.gen(function*() { + const input = yield* (Ref.make(10)) + const output = yield* (Ref.make(0)) + yield* pipe( + Ref.updateAndGet(input, (n) => n - 1), + Effect.zipLeft(Ref.update(output, (n) => n + 1)), + Effect.repeat({ until: (n) => Effect.succeed(n === 0) }) + ) + const result = yield* (Ref.get(output)) + strictEqual(result, 10) + })) + it.effect("repeat/until - always evaluates the effect at least once", () => + Effect.gen(function*() { + const ref = yield* (Ref.make(0)) + yield* pipe(Ref.update(ref, (n) => n + 1), Effect.repeat({ until: () => Effect.succeed(true) })) + const result = yield* (Ref.get(ref)) + strictEqual(result, 1) + })) + it.effect("repeat/while - repeats while the condition is true", () => + Effect.gen(function*() { + const input = yield* (Ref.make(10)) + const output = yield* (Ref.make(0)) + yield* pipe( + Ref.updateAndGet(input, (n) => n - 1), + Effect.zipLeft(Ref.update(output, (n) => n + 1)), + Effect.repeat({ while: (n) => n >= 0 }) + ) + const result = yield* (Ref.get(output)) + strictEqual(result, 11) + })) + it.effect("repeat/while - always evaluates the effect at least once", () => + Effect.gen(function*() { + const ref = yield* (Ref.make(0)) + yield* pipe(Ref.update(ref, (n) => n + 1), Effect.repeat({ while: constFalse })) + const result = yield* (Ref.get(ref)) + strictEqual(result, 1) + })) + it.effect("repeat/while - repeats while condition is true", () => + Effect.gen(function*() { + const input = yield* (Ref.make(10)) + const output = yield* (Ref.make(0)) + yield* pipe( + Ref.updateAndGet(input, (n) => n - 1), + Effect.zipLeft(Ref.update(output, (n) => n + 1)), + Effect.repeat({ while: (v) => Effect.succeed(v >= 0) }) + ) + const result = yield* (Ref.get(output)) + strictEqual(result, 11) + })) + it.effect("repeat/while - always evaluates effect at least once", () => + Effect.gen(function*() { + const ref = yield* (Ref.make(0)) + yield* pipe(Ref.update(ref, (n) => n + 1), Effect.repeat({ while: () => Effect.succeed(false) })) + const result = yield* (Ref.get(ref)) + strictEqual(result, 1) + })) + + it.effect("repeat/schedule", () => + Effect.gen(function*() { + const ref = yield* (Ref.make(0)) + yield* pipe(Ref.update(ref, (n) => n + 1), Effect.repeat(Schedule.recurs(3))) + const result = yield* (Ref.get(ref)) + strictEqual(result, 4) + })) + + it.effect("repeat/schedule - CurrentIterationMetadata", () => + Effect.gen(function*() { + const ref = yield* Ref.make>([]) + yield* Effect.gen(function*() { + const currentIterationMeta = yield* Schedule.CurrentIterationMetadata + yield* Ref.update(ref, (infos) => [...infos, currentIterationMeta]) + }).pipe( + Effect.repeat( + Schedule.intersect(Schedule.fixed("1 second"), Schedule.recurs(2)) + ), + Effect.fork + ) + yield* TestClock.adjust(Duration.seconds(50)) + const result = yield* (Ref.get(ref)) + deepStrictEqual(result, [ + { + elapsed: Duration.zero, + elapsedSincePrevious: Duration.zero, + recurrence: 0, + input: undefined, + output: undefined, + now: 0, + start: 0 + }, + { + elapsed: Duration.zero, + elapsedSincePrevious: Duration.zero, + recurrence: 1, + input: undefined, + output: [0, 0], + now: 0, + start: 0 + }, + { + elapsed: Duration.seconds(1), + elapsedSincePrevious: Duration.seconds(1), + recurrence: 2, + input: undefined, + output: [1, 1], + now: 1000, + start: 0 + } + ]) + })) + + it.effect("repeat/schedule + until", () => + Effect.gen(function*() { + const ref = yield* (Ref.make(0)) + yield* pipe( + Ref.updateAndGet(ref, (n) => n + 1), + Effect.repeat({ schedule: Schedule.recurs(3), until: (n) => n === 3 }) + ) + const result = yield* (Ref.get(ref)) + strictEqual(result, 3) + })) + + it.effect("repeat/schedule + while", () => + Effect.gen(function*() { + const ref = yield* (Ref.make(0)) + yield* pipe( + Ref.updateAndGet(ref, (n) => n + 1), + Effect.repeat({ schedule: Schedule.recurs(3), while: (n) => n < 3 }) + ) + const result = yield* (Ref.get(ref)) + strictEqual(result, 3) + })) + + it.effect("repeat/times ", () => + Effect.gen(function*() { + const ref = yield* (Ref.make>([])) + const effectResult = yield* pipe( + Ref.updateAndGet(ref, (arr) => { + arr.push(arr.length) + return arr + }), + Effect.repeat({ times: 2 }) + ) + const result = yield* (Ref.get(ref)) + deepStrictEqual(result, [0, 1, 2]) + deepStrictEqual(effectResult, [0, 1, 2]) + })) +}) diff --git a/repos/effect/packages/effect/test/Effect/retrying.test.ts b/repos/effect/packages/effect/test/Effect/retrying.test.ts new file mode 100644 index 0000000..4d9c669 --- /dev/null +++ b/repos/effect/packages/effect/test/Effect/retrying.test.ts @@ -0,0 +1,234 @@ +import { describe, it } from "@effect/vitest" +import { deepStrictEqual, strictEqual } from "@effect/vitest/utils" +import * as Duration from "effect/Duration" +import * as Effect from "effect/Effect" +import { constFalse, constTrue, pipe } from "effect/Function" +import * as Ref from "effect/Ref" +import * as Schedule from "effect/Schedule" + +describe("Effect", () => { + it.effect("retry/until - retries until condition is true", () => + Effect.gen(function*() { + const input = yield* (Ref.make(10)) + const output = yield* (Ref.make(0)) + yield* pipe( + Ref.updateAndGet(input, (n) => n - 1), + Effect.zipLeft(Ref.update(output, (n) => n + 1)), + Effect.flipWith(Effect.retry({ until: (n) => n === 0 })) + ) + const result = yield* (Ref.get(output)) + strictEqual(result, 10) + })) + it.effect("retry/until - runs at least once", () => + Effect.gen(function*() { + const ref = yield* (Ref.make(0)) + yield* pipe(Ref.update(ref, (n) => n + 1), Effect.flipWith(Effect.retry({ until: constTrue }))) + const result = yield* (Ref.get(ref)) + strictEqual(result, 1) + })) + it.effect("retry/until - retries until condition is true", () => + Effect.gen(function*() { + const input = yield* (Ref.make(10)) + const output = yield* (Ref.make(0)) + yield* pipe( + Ref.updateAndGet(input, (n) => n - 1), + Effect.zipLeft(Ref.update(output, (n) => n + 1)), + Effect.flipWith(Effect.retry({ until: (n) => Effect.succeed(n === 0) })) + ) + const result = yield* (Ref.get(output)) + strictEqual(result, 10) + })) + it.effect("retry/until - runs at least once", () => + Effect.gen(function*() { + const ref = yield* (Ref.make(0)) + yield* pipe( + Ref.update(ref, (n) => n + 1), + Effect.flipWith(Effect.retry({ until: () => Effect.succeed(true) })) + ) + const result = yield* (Ref.get(ref)) + strictEqual(result, 1) + })) + it.effect("retry/while - retries while condition is true", () => + Effect.gen(function*() { + const input = yield* (Ref.make(10)) + const output = yield* (Ref.make(0)) + yield* pipe( + Ref.updateAndGet(input, (n) => n - 1), + Effect.zipLeft(Ref.update(output, (n) => n + 1)), + Effect.flipWith(Effect.retry({ while: (n) => n >= 0 })) + ) + const result = yield* (Ref.get(output)) + strictEqual(result, 11) + })) + it.effect("retry/while - runs at least once", () => + Effect.gen(function*() { + const ref = yield* (Ref.make(0)) + yield* pipe(Ref.update(ref, (n) => n + 1), Effect.flipWith(Effect.retry({ while: constFalse }))) + const result = yield* (Ref.get(ref)) + strictEqual(result, 1) + })) + it.effect("retry/while - retries while condition is true", () => + Effect.gen(function*() { + const input = yield* (Ref.make(10)) + const output = yield* (Ref.make(0)) + yield* pipe( + Ref.updateAndGet(input, (n) => n - 1), + Effect.zipLeft(Ref.update(output, (n) => n + 1)), + Effect.flipWith(Effect.retry({ while: (n) => Effect.succeed(n >= 0) })) + ) + const result = yield* (Ref.get(output)) + strictEqual(result, 11) + })) + it.effect("retry/while - runs at least once", () => + Effect.gen(function*() { + const ref = yield* (Ref.make(0)) + yield* pipe( + Ref.update(ref, (n) => n + 1), + Effect.flipWith(Effect.retry({ while: () => Effect.succeed(false) })) + ) + const result = yield* (Ref.get(ref)) + strictEqual(result, 1) + })) + it.effect("retry/schedule", () => + Effect.gen(function*() { + const ref = yield* (Ref.make(0)) + yield* pipe( + Ref.update(ref, (n) => n + 1), + Effect.flipWith(Effect.retry(Schedule.recurs(3))) + ) + const result = yield* (Ref.get(ref)) + strictEqual(result, 4) + })) + it.effect("retry/schedule - CurrentIterationMetadata", () => + Effect.gen(function*() { + const ref = yield* Ref.make>([]) + yield* pipe( + Effect.gen(function*() { + const currentIterationMeta = yield* Schedule.CurrentIterationMetadata + yield* Ref.update(ref, (infos) => [...infos, currentIterationMeta]) + }), + Effect.flipWith(Effect.retry(Schedule.recurs(3))) + ) + const result = yield* (Ref.get(ref)) + deepStrictEqual(result, [ + { + elapsed: Duration.zero, + elapsedSincePrevious: Duration.zero, + recurrence: 0, + input: undefined, + output: undefined, + now: 0, + start: 0 + }, + { + elapsed: Duration.zero, + elapsedSincePrevious: Duration.zero, + recurrence: 1, + input: undefined, + output: 0, + now: 0, + start: 0 + }, + { + elapsed: Duration.zero, + elapsedSincePrevious: Duration.zero, + recurrence: 2, + input: undefined, + output: 1, + now: 0, + start: 0 + }, + { + elapsed: Duration.zero, + elapsedSincePrevious: Duration.zero, + recurrence: 3, + input: undefined, + output: 2, + now: 0, + start: 0 + } + ]) + })) + + it.effect("retry/schedule + until", () => + Effect.gen(function*() { + const ref = yield* (Ref.make(0)) + yield* pipe( + Ref.updateAndGet(ref, (n) => n + 1), + Effect.flipWith(Effect.retry({ + schedule: Schedule.recurs(3), + until: (n) => n === 3 + })) + ) + const result = yield* (Ref.get(ref)) + strictEqual(result, 3) + })) + + it.effect("retry/schedule + until effect", () => + Effect.gen(function*() { + const ref = yield* (Ref.make(0)) + yield* pipe( + Ref.updateAndGet(ref, (n) => n + 1), + Effect.flipWith(Effect.retry({ + schedule: Schedule.recurs(3), + until: (n) => Effect.succeed(n === 3) + })) + ) + const result = yield* (Ref.get(ref)) + strictEqual(result, 3) + })) + + it.effect("retry/schedule + until error", () => + Effect.gen(function*() { + const ref = yield* (Ref.make(0)) + const result = yield* pipe( + Ref.updateAndGet(ref, (n) => n + 1), + Effect.flipWith(Effect.retry({ + schedule: Schedule.recurs(3), + until: (_n) => Effect.fail("err" as const) + })) + ) + strictEqual(result, "err") + })) + + it.effect("retry/schedule + while", () => + Effect.gen(function*() { + const ref = yield* (Ref.make(0)) + yield* pipe( + Ref.updateAndGet(ref, (n) => n + 1), + Effect.flipWith(Effect.retry({ + schedule: Schedule.recurs(3), + while: (n) => n < 3 + })) + ) + const result = yield* (Ref.get(ref)) + strictEqual(result, 3) + })) + + it.effect("retry/schedule + while error", () => + Effect.gen(function*() { + const ref = yield* (Ref.make(0)) + const result = yield* pipe( + Ref.updateAndGet(ref, (n) => n + 1), + Effect.flipWith(Effect.retry({ + schedule: Schedule.recurs(3), + while: (_n) => Effect.fail("err" as const) + })) + ) + strictEqual(result, "err") + })) + + it.effect("retry/schedule + while effect", () => + Effect.gen(function*() { + const ref = yield* (Ref.make(0)) + yield* pipe( + Ref.updateAndGet(ref, (n) => n + 1), + Effect.flipWith(Effect.retry({ + schedule: Schedule.recurs(3), + while: (n) => Effect.succeed(n < 3) + })) + ) + const result = yield* (Ref.get(ref)) + strictEqual(result, 3) + })) +}) diff --git a/repos/effect/packages/effect/test/Effect/runtimeFlags.test.ts b/repos/effect/packages/effect/test/Effect/runtimeFlags.test.ts new file mode 100644 index 0000000..3736338 --- /dev/null +++ b/repos/effect/packages/effect/test/Effect/runtimeFlags.test.ts @@ -0,0 +1,29 @@ +import { describe, it } from "@effect/vitest" +import { assertFalse, assertTrue } from "@effect/vitest/utils" +import * as Effect from "effect/Effect" +import { pipe } from "effect/Function" +import * as Flags from "effect/RuntimeFlags" +import * as Patch from "effect/RuntimeFlagsPatch" + +describe("Effect", () => { + it("should enable flags in the current fiber", () => + Effect.runPromise(Effect.gen(function*() { + const before = yield* Effect.getRuntimeFlags + assertFalse(Flags.isEnabled(before, Flags.OpSupervision)) + yield* Effect.patchRuntimeFlags(Patch.enable(Flags.OpSupervision)) + const after = yield* Effect.getRuntimeFlags + assertTrue(Flags.isEnabled(after, Flags.OpSupervision)) + }))) + it("should enable flags in the wrapped effect", () => + Effect.runPromise(Effect.gen(function*() { + const before = yield* Effect.getRuntimeFlags + assertFalse(Flags.isEnabled(before, Flags.OpSupervision)) + const inside = yield* pipe( + Effect.getRuntimeFlags, + Effect.withRuntimeFlagsPatch(Patch.enable(Flags.OpSupervision)) + ) + const after = yield* Effect.getRuntimeFlags + assertFalse(Flags.isEnabled(after, Flags.OpSupervision)) + assertTrue(Flags.isEnabled(inside, Flags.OpSupervision)) + }))) +}) diff --git a/repos/effect/packages/effect/test/Effect/scheduler.test.ts b/repos/effect/packages/effect/test/Effect/scheduler.test.ts new file mode 100644 index 0000000..5f6a3f3 --- /dev/null +++ b/repos/effect/packages/effect/test/Effect/scheduler.test.ts @@ -0,0 +1,75 @@ +import { describe, it } from "@effect/vitest" +import { deepStrictEqual } from "@effect/vitest/utils" +import * as Effect from "effect/Effect" +import { pipe } from "effect/Function" +import * as Scheduler from "effect/Scheduler" + +describe("Effect", () => { + it.effect("matrix schedules according to priority", () => + Effect.gen(function*() { + const ps000: Array = [] + const ps100: Array = [] + const ps200: Array = [] + const scheduler = Scheduler.makeMatrix( + [ + 0, + Scheduler.makeBatched((runBatch) => { + ps000.push(0) + setTimeout(runBatch, 0) + }) + ], + [ + 100, + Scheduler.makeBatched((runBatch) => { + ps100.push(100) + setTimeout(runBatch, 0) + }) + ], + [ + 200, + Scheduler.makeBatched((runBatch) => { + ps200.push(200) + setTimeout(runBatch, 0) + }) + ], + [ + 300, + Scheduler.makeBatched((runBatch) => { + setTimeout(runBatch, 0) + }) + ] + ) + yield* pipe( + Effect.yieldNow(), + Effect.withScheduler(scheduler) + ) + deepStrictEqual(ps000, [0]) + yield* pipe( + Effect.yieldNow({ priority: 50 }), + Effect.withScheduler(scheduler) + ) + deepStrictEqual(ps000, [0, 0]) + yield* pipe( + Effect.yieldNow({ priority: 100 }), + Effect.withScheduler(scheduler) + ) + deepStrictEqual(ps100, [100]) + yield* pipe( + Effect.yieldNow({ priority: 150 }), + Effect.withScheduler(scheduler) + ) + deepStrictEqual(ps100, [100, 100]) + yield* pipe( + Effect.yieldNow({ priority: 200 }), + Effect.withScheduler(scheduler) + ) + deepStrictEqual(ps100, [100, 100]) + deepStrictEqual(ps200, [200]) + yield* pipe( + Effect.yieldNow({ priority: 300 }), + Effect.withScheduler(scheduler) + ) + deepStrictEqual(ps100, [100, 100]) + deepStrictEqual(ps200, [200]) + })) +}) diff --git a/repos/effect/packages/effect/test/Effect/scheduling.test.ts b/repos/effect/packages/effect/test/Effect/scheduling.test.ts new file mode 100644 index 0000000..c202ab9 --- /dev/null +++ b/repos/effect/packages/effect/test/Effect/scheduling.test.ts @@ -0,0 +1,79 @@ +import { describe, it } from "@effect/vitest" +import { deepStrictEqual } from "@effect/vitest/utils" +import * as Clock from "effect/Clock" +import * as Duration from "effect/Duration" +import * as Effect from "effect/Effect" +import { pipe } from "effect/Function" +import * as Ref from "effect/Ref" +import * as Schedule from "effect/Schedule" +import * as TestClock from "effect/TestClock" + +describe("Effect", () => { + it.effect("schedule - runs effect for each recurrence of the schedule", () => + Effect.gen(function*() { + const ref = yield* Ref.make>([]) + const effect = pipe( + Clock.currentTimeMillis, + Effect.flatMap((duration) => Ref.update(ref, (array) => [...array, Duration.millis(duration)])) + ) + const schedule = pipe(Schedule.spaced(Duration.seconds(1)), Schedule.intersect(Schedule.recurs(5))) + yield* pipe(effect, Effect.schedule(schedule), Effect.fork) + yield* TestClock.adjust(Duration.seconds(5)) + const value = yield* Ref.get(ref) + const expected = [1, 2, 3, 4, 5].map(Duration.seconds) + deepStrictEqual(value, expected) + })) + + it.effect("schedule - Schedule.CurrentIterationMetadata", () => + Effect.gen(function*() { + const ref = yield* Ref.make>([]) + const effect = Effect.gen(function*() { + const lastIterationInfo = yield* Schedule.CurrentIterationMetadata + + yield* Ref.update(ref, (array) => [...array, lastIterationInfo]) + }) + const schedule = pipe(Schedule.fibonacci("1 second"), Schedule.intersect(Schedule.recurs(4))) + yield* pipe(effect, Effect.schedule(schedule), Effect.fork) + yield* TestClock.adjust(Duration.seconds(50)) + const value = yield* Ref.get(ref) + + deepStrictEqual(value, [ + { + elapsed: Duration.zero, + elapsedSincePrevious: Duration.zero, + recurrence: 1, + input: undefined, + output: [Duration.millis(1000), 0], + now: 0, + start: 0 + }, + { + elapsed: Duration.seconds(1), + elapsedSincePrevious: Duration.seconds(1), + recurrence: 2, + input: undefined, + output: [Duration.millis(1000), 1], + now: 1000, + start: 0 + }, + { + elapsed: Duration.seconds(2), + elapsedSincePrevious: Duration.seconds(1), + recurrence: 3, + input: undefined, + output: [Duration.millis(2000), 2], + now: 2000, + start: 0 + }, + { + elapsed: Duration.seconds(4), + elapsedSincePrevious: Duration.seconds(2), + recurrence: 4, + input: undefined, + output: [Duration.millis(3000), 3], + now: 4000, + start: 0 + } + ]) + })) +}) diff --git a/repos/effect/packages/effect/test/Effect/scope-ref.test.ts b/repos/effect/packages/effect/test/Effect/scope-ref.test.ts new file mode 100644 index 0000000..460b49a --- /dev/null +++ b/repos/effect/packages/effect/test/Effect/scope-ref.test.ts @@ -0,0 +1,54 @@ +import { describe, it } from "@effect/vitest" +import { deepStrictEqual } from "@effect/vitest/utils" +import { GenericTag } from "effect/Context" +import * as Effect from "effect/Effect" +import * as FiberRef from "effect/FiberRef" +import { pipe } from "effect/Function" +import * as Layer from "effect/Layer" +import * as List from "effect/List" +import * as Logger from "effect/Logger" + +const ref = FiberRef.unsafeMake(List.empty()) +const env = GenericTag<"context", number>("context") + +const withValue = (value: string) => Effect.locallyWith(ref, List.prepend(value)) + +const logRef = (msg: string) => + Effect.gen(function*() { + const stack = yield* FiberRef.get(ref) + const value = yield* env + yield* Effect.log(`${value} | ${msg} | ${List.toArray(stack).join(" > ")}`) + }) + +describe("Effect", () => { + it.effect("scoped ref", () => + Effect.gen(function*() { + const messages: Array = [] + const layer = Layer.mergeAll( + Logger.replace( + Logger.defaultLogger, + Logger.make((_) => { + messages.push(_.message) + }) + ), + Layer.succeed(env, 1) + ) + + yield* pipe( + Effect.acquireRelease( + withValue("A")(logRef("acquire")), + () => withValue("R")(logRef("release")) + ), + withValue("INNER"), + Effect.scoped, + withValue("OUTER"), + Effect.provide(layer), + withValue("EXTERN") + ) + + deepStrictEqual(messages, [ + ["1 | acquire | A > INNER > OUTER > EXTERN"], + ["1 | release | R > INNER > OUTER > EXTERN"] + ]) + })) +}) diff --git a/repos/effect/packages/effect/test/Effect/semaphore.test.ts b/repos/effect/packages/effect/test/Effect/semaphore.test.ts new file mode 100644 index 0000000..ff57199 --- /dev/null +++ b/repos/effect/packages/effect/test/Effect/semaphore.test.ts @@ -0,0 +1,85 @@ +import { assert, describe, it } from "@effect/vitest" +import { strictEqual } from "@effect/vitest/utils" +import * as D from "effect/Duration" +import * as Effect from "effect/Effect" +import * as FiberId from "effect/FiberId" +import * as Option from "effect/Option" +import * as Scheduler from "effect/Scheduler" +import * as TestClock from "effect/TestClock" + +describe("Effect", () => { + it.effect("semaphore works", () => + Effect.gen(function*() { + const sem = yield* Effect.makeSemaphore(4) + const messages: Array = [] + yield* Effect.fork(Effect.all( + [0, 1, 2, 3].map((n) => + sem.withPermits(2)(Effect.delay(D.seconds(2))(Effect.sync(() => messages.push(`process: ${n}`)))) + ), + { concurrency: "unbounded", discard: true } + )) + yield* (TestClock.adjust(D.seconds(3))) + strictEqual(messages.length, 2) + yield* (TestClock.adjust(D.seconds(3))) + strictEqual(messages.length, 4) + yield* ( + Effect.fork(Effect.all( + [0, 1, 2, 3].map((n) => + sem.withPermits(2)(Effect.delay(D.seconds(2))(Effect.sync(() => messages.push(`process: ${n}`)))) + ), + { concurrency: "unbounded", discard: true } + )) + ) + yield* (TestClock.adjust(D.seconds(3))) + strictEqual(messages.length, 6) + yield* (TestClock.adjust(D.seconds(3))) + strictEqual(messages.length, 8) + })) + + it.effect("releaseAll", () => + Effect.gen(function*() { + const sem = yield* Effect.makeSemaphore(4) + yield* sem.take(4) + yield* sem.releaseAll + yield* sem.take(1) + })) + + it.effect("resize", () => + Effect.gen(function*() { + const sem = yield* Effect.makeSemaphore(4) + yield* sem.take(4) + yield* sem.resize(2) + const fiber = yield* Effect.fork(sem.take(1)) + yield* TestClock.adjust(1) + assert.isNull(fiber.unsafePoll()) + yield* sem.release(2) + yield* TestClock.adjust(1) + assert.isNull(fiber.unsafePoll()) + yield* sem.release(1) + yield* TestClock.adjust(1) + assert.isTrue(fiber.unsafePoll() !== null) + })) + + it.effect("take interruption does not leak permits", () => + Effect.gen(function*() { + const scheduler = new Scheduler.ControlledScheduler() + const sem = yield* Effect.makeSemaphore(0) + const waiter = yield* sem.take(1).pipe( + Effect.withScheduler(scheduler), + Effect.fork + ) + + yield* Effect.yieldNow() + yield* sem.release(1).pipe(Effect.withScheduler(scheduler)) + assert.isNull(waiter.unsafePoll()) + + scheduler.step() + assert.isNull(waiter.unsafePoll()) + + waiter.unsafeInterruptAsFork(FiberId.none) + scheduler.step() + + const result = yield* sem.withPermitsIfAvailable(1)(Effect.void) + assert.isTrue(Option.isSome(result)) + })) +}) diff --git a/repos/effect/packages/effect/test/Effect/sequencing.test.ts b/repos/effect/packages/effect/test/Effect/sequencing.test.ts new file mode 100644 index 0000000..56d4fa9 --- /dev/null +++ b/repos/effect/packages/effect/test/Effect/sequencing.test.ts @@ -0,0 +1,284 @@ +import { describe, it } from "@effect/vitest" +import { assertFalse, assertLeft, assertTrue, deepStrictEqual, strictEqual } from "@effect/vitest/utils" +import * as Cause from "effect/Cause" +import * as Deferred from "effect/Deferred" +import * as Effect from "effect/Effect" +import * as Either from "effect/Either" +import * as Exit from "effect/Exit" +import * as Fiber from "effect/Fiber" +import { constFalse, constTrue, pipe } from "effect/Function" +import * as HashSet from "effect/HashSet" +import * as Option from "effect/Option" +import * as Ref from "effect/Ref" + +describe("Effect", () => { + it.effect("andThen", () => + Effect.gen(function*() { + const a0 = Effect.andThen(Effect.succeed(0), Effect.succeed(1)) + const a1 = Effect.succeed(0).pipe(Effect.andThen(Effect.succeed(1))) + const a2 = Effect.andThen(Effect.succeed(0), (n) => Effect.succeed(n + 1)) + const a3 = Effect.succeed(0).pipe(Effect.andThen((n) => Effect.succeed(n + 1))) + const a4 = Effect.succeed(0).pipe(Effect.andThen("ok")) + const a5 = Effect.succeed(0).pipe(Effect.andThen(() => "ok")) + const a6 = Effect.andThen(Effect.succeed(0), () => "ok") + const a7 = Effect.andThen(Effect.succeed(0), "ok") + const a8 = Effect.andThen(Effect.succeed(0), () => Promise.resolve("ok")) + const a9 = Effect.andThen(Effect.succeed(0), Promise.resolve("ok")) + strictEqual(yield* a0, 1) + strictEqual(yield* a1, 1) + strictEqual(yield* a2, 1) + strictEqual(yield* a3, 1) + strictEqual(yield* a4, "ok") + strictEqual(yield* a5, "ok") + strictEqual(yield* a6, "ok") + strictEqual(yield* a7, "ok") + strictEqual(yield* a8, "ok") + strictEqual(yield* a9, "ok") + })) + it.effect("tap", () => + Effect.gen(function*() { + const a0 = Effect.tap(Effect.succeed(0), Effect.succeed(1)) + const a1 = Effect.succeed(0).pipe(Effect.tap(Effect.succeed(1))) + const a2 = Effect.succeed(0).pipe(Effect.tap(Effect.succeed(1), { onlyEffect: true })) + const a3 = Effect.tap(Effect.succeed(0), (n) => Effect.succeed(n + 1)) + const a4 = Effect.tap(Effect.succeed(0), (n) => Effect.succeed(n + 1), { onlyEffect: true }) + const a5 = Effect.succeed(0).pipe(Effect.tap((n) => Effect.succeed(n + 1))) + const a6 = Effect.succeed(0).pipe(Effect.tap((n) => Effect.succeed(n + 1), { onlyEffect: true })) + const a7 = Effect.succeed(0).pipe(Effect.tap("ok")) + const a8 = Effect.succeed(0).pipe(Effect.tap(() => "ok")) + const a9 = Effect.tap(Effect.succeed(0), () => "ok") + const a10 = Effect.tap(Effect.succeed(0), "ok") + const a11 = Effect.tap(Effect.succeed(0), () => Promise.resolve("ok")) + const a12 = Effect.tap(Effect.succeed(0), Promise.resolve("ok")) + strictEqual(yield* a0, 0) + strictEqual(yield* a1, 0) + strictEqual(yield* a2, 0) + strictEqual(yield* a3, 0) + strictEqual(yield* a4, 0) + strictEqual(yield* a5, 0) + strictEqual(yield* a6, 0) + strictEqual(yield* a7, 0) + strictEqual(yield* a8, 0) + strictEqual(yield* a9, 0) + strictEqual(yield* a10, 0) + strictEqual(yield* a11, 0) + strictEqual(yield* a12, 0) + })) + it.effect("flattens nested effects", () => + Effect.gen(function*() { + const effect = Effect.succeed(Effect.succeed("test")) + const flatten1 = yield* (Effect.flatten(effect)) + const flatten2 = yield* (Effect.flatten(effect)) + strictEqual(flatten1, "test") + strictEqual(flatten2, "test") + })) + it.effect("if - runs `onTrue` if result of `b` is `true`", () => + Effect.gen(function*() { + const result = yield* pipe( + true, + Effect.if({ + onTrue: () => Effect.succeed(true), + onFalse: () => Effect.succeed(false) + }) + ) + assertTrue(result) + })) + it.effect("if - runs `onFalse` if result of `b` is `false`", () => + Effect.gen(function*() { + const result = yield* pipe( + Effect.succeed(false), + Effect.if({ + onFalse: () => Effect.succeed(true), + onTrue: () => Effect.succeed(false) + }) + ) + assertTrue(result) + })) + describe("", () => { + it.effect("tapErrorCause - effectually peeks at the cause of the failure of this effect", () => + Effect.gen(function*() { + const ref = yield* (Ref.make(false)) + const result = yield* ( + pipe(Effect.dieMessage("die"), Effect.tapErrorCause(() => Ref.set(ref, true)), Effect.exit) + ) + const effect = yield* (Ref.get(ref)) + assertTrue(Exit.isFailure(result) && Option.isSome(Cause.dieOption(result.effect_instruction_i0))) + assertTrue(effect) + })) + }) + it.effect("tapDefect - effectually peeks at defects", () => + Effect.gen(function*() { + const ref = yield* (Ref.make(false)) + const result = yield* pipe( + Effect.dieMessage("die"), + Effect.tapDefect(() => Ref.set(ref, true)), + Effect.exit + ) + const effect = yield* (Ref.get(ref)) + assertTrue(Exit.isFailure(result) && Option.isSome(Cause.dieOption(result.effect_instruction_i0))) + assertTrue(effect) + })) + it.effect("tapDefect - leaves failures", () => + Effect.gen(function*() { + const ref = yield* (Ref.make(false)) + const result = yield* pipe( + Effect.fail("fail"), + Effect.tapDefect(() => Ref.set(ref, true)), + Effect.exit + ) + const effect = yield* (Ref.get(ref)) + deepStrictEqual(result, Exit.fail("fail")) + assertFalse(effect) + })) + it.effect("unless - executes correct branch only", () => + Effect.gen(function*() { + const ref = yield* (Ref.make(0)) + yield* pipe(Ref.set(ref, 1), Effect.unless(constTrue)) + const v1 = yield* (Ref.get(ref)) + yield* pipe(Ref.set(ref, 2), Effect.unless(constFalse)) + const v2 = yield* (Ref.get(ref)) + const failure = new Error("expected") + yield* pipe(Effect.fail(failure), Effect.unless(constTrue)) + const failed = yield* pipe(Effect.fail(failure), Effect.unless(constFalse), Effect.either) + strictEqual(v1, 0) + strictEqual(v2, 2) + assertLeft(failed, failure) + })) + it.effect("unlessEffect - executes condition effect and correct branch", () => + Effect.gen(function*() { + const effectRef = yield* (Ref.make(0)) + const conditionRef = yield* (Ref.make(0)) + const conditionTrue = pipe(Ref.update(conditionRef, (n) => n + 1), Effect.as(true)) + const conditionFalse = pipe(Ref.update(conditionRef, (n) => n + 1), Effect.as(false)) + yield* pipe(Ref.set(effectRef, 1), Effect.unlessEffect(conditionTrue)) + const v1 = yield* (Ref.get(effectRef)) + const c1 = yield* (Ref.get(conditionRef)) + yield* pipe(Ref.set(effectRef, 2), Effect.unlessEffect(conditionFalse)) + const v2 = yield* (Ref.get(effectRef)) + const c2 = yield* (Ref.get(conditionRef)) + const failure = new Error("expected") + yield* pipe(Effect.fail(failure), Effect.unlessEffect(conditionTrue)) + const failed = yield* pipe(Effect.fail(failure), Effect.unlessEffect(conditionFalse), Effect.either) + strictEqual(v1, 0) + strictEqual(c1, 1) + strictEqual(v2, 2) + strictEqual(c2, 2) + assertLeft(failed, failure) + })) + it.effect("when - executes correct branch only", () => + Effect.gen(function*() { + const ref = yield* (Ref.make(0)) + yield* pipe(Ref.set(ref, 1), Effect.when(constFalse)) + const v1 = yield* (Ref.get(ref)) + yield* pipe(Ref.set(ref, 2), Effect.when(constTrue)) + const v2 = yield* (Ref.get(ref)) + const failure = new Error("expected") + yield* pipe(Effect.fail(failure), Effect.when(constFalse)) + const failed = yield* pipe(Effect.fail(failure), Effect.when(constTrue), Effect.either) + strictEqual(v1, 0) + strictEqual(v2, 2) + assertLeft(failed, failure) + })) + it.effect("whenEffect - executes condition effect and correct branch", () => + Effect.gen(function*() { + const effectRef = yield* (Ref.make(0)) + const conditionRef = yield* (Ref.make(0)) + const conditionTrue = pipe(Ref.update(conditionRef, (n) => n + 1), Effect.as(true)) + const conditionFalse = pipe(Ref.update(conditionRef, (n) => n + 1), Effect.as(false)) + yield* pipe(Ref.set(effectRef, 1), Effect.whenEffect(conditionFalse)) + const v1 = yield* (Ref.get(effectRef)) + const c1 = yield* (Ref.get(conditionRef)) + yield* pipe(Ref.set(effectRef, 2), Effect.whenEffect(conditionTrue)) + const v2 = yield* (Ref.get(effectRef)) + const c2 = yield* (Ref.get(conditionRef)) + const failure = new Error("expected") + yield* pipe(Effect.fail(failure), Effect.whenEffect(conditionFalse)) + const failed = yield* pipe(Effect.fail(failure), Effect.whenEffect(conditionTrue), Effect.either) + strictEqual(v1, 0) + strictEqual(c1, 1) + strictEqual(v2, 2) + strictEqual(c2, 2) + assertLeft(failed, failure) + })) + it.effect("zip/parallel - combines results", () => + Effect.gen(function*() { + const result = yield* pipe( + Effect.succeed(1), + Effect.zip(Effect.succeed(2), { concurrent: true }), + Effect.flatMap((tuple) => Effect.succeed(tuple[0] + tuple[1])), + Effect.map((n) => n === 3) + ) + assertTrue(result) + })) + it.effect("zip/parallel - does not swallow exit causes of loser", () => + Effect.gen(function*() { + const result = yield* ( + pipe( + Effect.interrupt, + Effect.zip(Effect.interrupt, { concurrent: true }), + Effect.exit, + Effect.map((exit) => + pipe(Exit.causeOption(exit), Option.map(Cause.interruptors), Option.getOrElse(() => HashSet.empty())) + ) + ) + ) + assertTrue(HashSet.size(result) > 0) + })) + it.effect("zip/parallel - does not report failure when interrupting loser after it succeeded", () => + Effect.gen(function*() { + const result = yield* ( + pipe( + Effect.interrupt, + Effect.zip(Effect.succeed(1), { concurrent: true }), + Effect.sandbox, + Effect.either, + Effect.map(Either.mapLeft(Cause.isInterrupted)) + ) + ) + assertLeft(result, true) + })) + it.effect("zip/parallel - paralellizes simple success values", () => + Effect.gen(function*() { + const countdown = (n: number): Effect.Effect => { + return n === 0 + ? Effect.succeed(0) + : pipe( + Effect.succeed(1), + Effect.zip(Effect.succeed(2), { concurrent: true }), + Effect.flatMap((tuple) => pipe(countdown(n - 1), Effect.map((y) => tuple[0] + tuple[1] + y))) + ) + } + const result = yield* (countdown(50)) + strictEqual(result, 150) + })) + it.effect("zip/parallel - does not kill fiber when forked on parent scope", () => + Effect.gen(function*() { + const latch1 = yield* (Deferred.make()) + const latch2 = yield* (Deferred.make()) + const latch3 = yield* (Deferred.make()) + const ref = yield* (Ref.make(false)) + const left = Effect.uninterruptibleMask((restore) => + pipe( + Deferred.succeed(latch2, void 0), + Effect.zipRight(restore(pipe(Deferred.await(latch1), Effect.zipRight(Effect.succeed("foo"))))), + Effect.onInterrupt(() => Ref.set(ref, true)) + ) + ) + const right = pipe(Deferred.succeed(latch3, void 0), Effect.as(42)) + yield* pipe( + Deferred.await(latch2), + Effect.zipRight(Deferred.await(latch3)), + Effect.zipRight(Deferred.succeed(latch1, void 0)), + Effect.fork + ) + + const result = yield* pipe(Effect.fork(left), Effect.zip(right, { concurrent: true })) + const leftInnerFiber = result[0] + const rightResult = result[1] + const leftResult = yield* (Fiber.await(leftInnerFiber)) + const interrupted = yield* (Ref.get(ref)) + assertFalse(interrupted) + deepStrictEqual(leftResult, Exit.succeed("foo")) + strictEqual(rightResult, 42) + })) +}) diff --git a/repos/effect/packages/effect/test/Effect/service.test.ts b/repos/effect/packages/effect/test/Effect/service.test.ts new file mode 100644 index 0000000..dfc0739 --- /dev/null +++ b/repos/effect/packages/effect/test/Effect/service.test.ts @@ -0,0 +1,246 @@ +import { describe, it } from "@effect/vitest" +import { assertInstanceOf, assertTrue, deepStrictEqual, strictEqual } from "@effect/vitest/utils" +import { Cause } from "effect" +import * as Context from "effect/Context" +import * as Effect from "effect/Effect" +import { flow, pipe } from "effect/Function" +import * as Layer from "effect/Layer" +import * as Scope from "effect/Scope" + +class Prefix extends Effect.Service()("Prefix", { + sync: () => ({ + prefix: "PRE" + }) +}) {} + +class Postfix extends Effect.Service()("Postfix", { + sync: () => ({ + postfix: "POST" + }) +}) {} + +const messages: Array = [] + +class Logger extends Effect.Service()("Logger", { + accessors: true, + effect: Effect.gen(function*() { + const { prefix } = yield* Prefix + const { postfix } = yield* Postfix + return { + info: (message: string) => + Effect.sync(() => { + messages.push(`[${prefix}][${message}][${postfix}]`) + }) + } + }), + dependencies: [Prefix.Default, Postfix.Default] +}) { + static Test = Layer.succeed(this, new Logger({ info: () => Effect.void })) +} + +class Scoped extends Effect.Service()("Scoped", { + accessors: true, + scoped: Effect.gen(function*() { + const { prefix } = yield* Prefix + const { postfix } = yield* Postfix + yield* Scope.Scope + return { + info: (message: string) => + Effect.sync(() => { + messages.push(`[${prefix}][${message}][${postfix}]`) + }) + } + }), + dependencies: [Prefix.Default, Postfix.Default] +}) {} + +describe("Effect.Service", () => { + it("make is a function", () => { + assertTrue(pipe({ prefix: "OK" }, Prefix.make) instanceof Prefix) + }) + it("tags is a tag and default is a layer", () => { + assertTrue(Layer.isLayer(Logger.Default)) + assertTrue(Layer.isLayer(Logger.DefaultWithoutDependencies)) + assertTrue(Context.isTag(Logger)) + }) + + it.effect("correctly wires dependencies", () => + Effect.gen(function*() { + yield* Logger.info("Ok") + deepStrictEqual(messages, ["[PRE][Ok][POST]"]) + const { prefix } = yield* Prefix + strictEqual(prefix, "PRE") + const { postfix } = yield* Postfix + strictEqual(postfix, "POST") + strictEqual(yield* Prefix.use((_) => _._tag), "Prefix") + }).pipe( + Effect.provide([ + Logger.Default, + Prefix.Default, + Postfix.Default + ]) + )) + + it.effect("inherits prototype", () => { + class Time extends Effect.Service, + n: number, + worker: (a: A) => Effect.Effect + ): Effect.Effect => { + const worker1 = pipe( + Queue.take(queue), + Effect.flatMap((a) => Effect.uninterruptible(worker(a))), + Effect.forever + ) + return pipe( + Effect.forkAll(Array.makeBy(n, () => worker1)), + Effect.flatMap(Fiber.join), + Effect.zipRight(Effect.never) + ) + } + const worker = (n: number) => { + if (n === 100) { + return pipe(Queue.shutdown(queue), Effect.zipRight(Effect.fail("fail"))) + } + return pipe(Queue.offer(queue, n), Effect.asVoid) + } + const queue = yield* Queue.unbounded() + yield* Queue.offerAll(queue, Array.range(1, 100)) + const result = yield* Effect.exit(shard(queue, 4, worker)) + yield* Queue.shutdown(queue) + assertTrue(Exit.isFailure(result)) + })) + it.effect("child becoming interruptible is interrupted due to auto-supervision of uninterruptible parent", () => + Effect.gen(function*() { + const latch = yield* Deferred.make() + const child = pipe( + Effect.interruptible(Effect.never), + Effect.onInterrupt(() => Deferred.succeed(latch, void 0)), + Effect.fork + ) + yield* Effect.uninterruptible(Effect.fork(child)) + const result = yield* Deferred.await(latch) + strictEqual(result, undefined) + })) + it.effect("dual roots", () => + Effect.gen(function*() { + const rootContains = (fiber: Fiber.RuntimeFiber): Effect.Effect => { + return pipe(Fiber.roots, Effect.map(Chunk.unsafeFromArray), Effect.map(Array.contains(fiber))) + } + const fiber1 = yield* Effect.forkDaemon(Effect.never) + const fiber2 = yield* Effect.forkDaemon(Effect.never) + yield* pipe( + rootContains(fiber1), + Effect.flatMap((a) => a ? rootContains(fiber2) : Effect.succeed(false)), + Effect.repeat({ until: (_) => _ }) + ) + const result = yield* pipe(Fiber.interrupt(fiber1), Effect.zipRight(Fiber.interrupt(fiber2))) + assertTrue(Exit.isInterrupted(result)) + })) + it.effect("interruptAll interrupts fibers in parallel", () => + Effect.gen(function*() { + const deferred1 = yield* Deferred.make() + const deferred2 = yield* Deferred.make() + const fiber1 = yield* pipe(Deferred.succeed(deferred1, void 0), Effect.zipRight(Effect.never), Effect.forkDaemon) + const fiber2 = yield* pipe( + Deferred.succeed(deferred2, void 0), + Effect.zipRight(Fiber.await(fiber1)), + Effect.uninterruptible, + Effect.forkDaemon + ) + yield* Deferred.await(deferred1) + yield* Deferred.await(deferred2) + yield* Fiber.interruptAll([fiber2, fiber1]) + const result = yield* Fiber.await(fiber2) + assertTrue(Exit.isInterrupted(result)) + })) + it.effect("await does not return until all fibers have completed execution", () => + Effect.gen(function*() { + const ref = yield* Ref.make(0) + const fiber = yield* Effect.forkAll(Array.makeBy(100, () => Ref.set(ref, 10))) + yield* Fiber.interrupt(fiber) + yield* Ref.set(ref, -1) + const result = yield* Ref.get(ref) + strictEqual(result, -1) + })) + it.effect("awaitAll - stack safety", () => + Effect.gen(function*() { + const result = yield* Fiber.awaitAll(fibers) + assertTrue(Array.isArray(result)) + assertTrue(result.length === fibers.length) + result.forEach((_) => assertTrue(Exit.isSuccess(_) && _.value === undefined)) + }), 10000) + it.effect("joinAll - stack safety", () => + Effect.gen(function*() { + const result = yield* Fiber.joinAll(fibers) + assertTrue(Array.isArray(result)) + assertTrue(result.length === fibers.length) + result.forEach((x) => strictEqual(x, undefined)) + }), 10000) + it.effect("all - stack safety", () => + Effect.gen(function*() { + const result = yield* pipe(Fiber.join(Fiber.all(fibers)), Effect.asVoid) + strictEqual(result, undefined) + }), 10000) + it.effect("is subtype of Effect", () => + Effect.gen(function*() { + const fiber = yield* Effect.fork(Effect.succeed(1)) + const fiberResult = yield* fiber + assertTrue(1 === fiberResult) + })) +}) diff --git a/repos/effect/packages/effect/test/FiberHandle.test.ts b/repos/effect/packages/effect/test/FiberHandle.test.ts new file mode 100644 index 0000000..76f151b --- /dev/null +++ b/repos/effect/packages/effect/test/FiberHandle.test.ts @@ -0,0 +1,121 @@ +import { assert, describe, it } from "@effect/vitest" +import { assertFalse, assertTrue, strictEqual } from "@effect/vitest/utils" +import { Deferred, Effect, Exit, Fiber, FiberHandle, pipe, Ref, TestClock } from "effect" + +describe("FiberHandle", () => { + it.effect("interrupts fibers", () => + Effect.gen(function*() { + const ref = yield* (Ref.make(0)) + yield* pipe( + Effect.gen(function*() { + const handle = yield* (FiberHandle.make()) + yield* (FiberHandle.run(handle, Effect.onInterrupt(Effect.never, () => Ref.update(ref, (n) => n + 1)))) + yield* (Effect.yieldNow()) + }), + Effect.scoped + ) + + strictEqual(yield* (Ref.get(ref)), 1) + })) + + it.effect("runtime", () => + Effect.gen(function*() { + const ref = yield* (Ref.make(0)) + yield* pipe( + Effect.gen(function*() { + const handle = yield* (FiberHandle.make()) + const run = yield* (FiberHandle.runtime(handle)()) + run(Effect.onInterrupt(Effect.never, () => Ref.update(ref, (n) => n + 1))) + yield* (Effect.yieldNow()) + run(Effect.onInterrupt(Effect.never, () => Ref.update(ref, (n) => n + 1))) + yield* (Effect.yieldNow()) + run(Effect.onInterrupt(Effect.never, () => Ref.update(ref, (n) => n + 1)), { + onlyIfMissing: true + }) + yield* (Effect.yieldNow()) + strictEqual(yield* (Ref.get(ref)), 1) + }), + Effect.scoped + ) + + strictEqual(yield* (Ref.get(ref)), 2) + })) + + it.scoped("join", () => + Effect.gen(function*() { + const handle = yield* (FiberHandle.make()) + FiberHandle.unsafeSet(handle, Effect.runFork(Effect.void)) + FiberHandle.unsafeSet(handle, Effect.runFork(Effect.fail("fail"))) + const result = yield* pipe(FiberHandle.join(handle), Effect.flip) + strictEqual(result, "fail") + })) + + it.scoped("onlyIfMissing", () => + Effect.gen(function*() { + const handle = yield* (FiberHandle.make()) + const fiberA = yield* (FiberHandle.run(handle, Effect.never)) + const fiberB = yield* (FiberHandle.run(handle, Effect.never, { onlyIfMissing: true })) + const fiberC = yield* (FiberHandle.run(handle, Effect.never, { onlyIfMissing: true })) + yield* (Effect.yieldNow()) + assertTrue(Exit.isInterrupted(yield* (fiberB.await))) + assertTrue(Exit.isInterrupted(yield* (fiberC.await))) + strictEqual(fiberA.unsafePoll(), null) + })) + + it.scoped("runtime onlyIfMissing", () => + Effect.gen(function*() { + const run = yield* (FiberHandle.makeRuntime()) + const fiberA = run(Effect.never) + const fiberB = run(Effect.never, { onlyIfMissing: true }) + const fiberC = run(Effect.never, { onlyIfMissing: true }) + yield* (Effect.yieldNow()) + assertTrue(Exit.isInterrupted(yield* (fiberB.await))) + assertTrue(Exit.isInterrupted(yield* (fiberC.await))) + strictEqual(fiberA.unsafePoll(), null) + })) + + it.scoped("propagateInterruption: false", () => + Effect.gen(function*() { + const handle = yield* FiberHandle.make() + const fiber = yield* FiberHandle.run(handle, Effect.never, { + propagateInterruption: false + }) + yield* Effect.yieldNow() + yield* Fiber.interrupt(fiber) + assertFalse(yield* Deferred.isDone(handle.deferred)) + })) + + it.scoped("propagateInterruption: true", () => + Effect.gen(function*() { + const handle = yield* FiberHandle.make() + const fiber = yield* FiberHandle.run(handle, Effect.never, { + propagateInterruption: true + }) + yield* Effect.yieldNow() + yield* Fiber.interrupt(fiber) + assertTrue(Exit.isInterrupted( + yield* FiberHandle.join(handle).pipe( + Effect.exit + ) + )) + })) + + it.scoped("awaitEmpty", () => + Effect.gen(function*() { + const handle = yield* FiberHandle.make() + yield* FiberHandle.run(handle, Effect.sleep(1000)) + + const fiber = yield* Effect.fork(FiberHandle.awaitEmpty(handle)) + yield* TestClock.adjust(500) + assert.isNull(fiber.unsafePoll()) + yield* TestClock.adjust(500) + assert.isDefined(fiber.unsafePoll()) + })) + + it.scoped("makeRuntimePromise", () => + Effect.gen(function*() { + const run = yield* FiberHandle.makeRuntimePromise() + const result = yield* Effect.promise(() => run(Effect.succeed("done"))) + strictEqual(result, "done") + })) +}) diff --git a/repos/effect/packages/effect/test/FiberMap.test.ts b/repos/effect/packages/effect/test/FiberMap.test.ts new file mode 100644 index 0000000..0fbc276 --- /dev/null +++ b/repos/effect/packages/effect/test/FiberMap.test.ts @@ -0,0 +1,146 @@ +import { assert, describe, it } from "@effect/vitest" +import { assertFalse, assertTrue, strictEqual } from "@effect/vitest/utils" +import { Array, Deferred, Effect, Exit, Fiber, FiberMap, pipe, Ref, Scope, TestClock } from "effect" + +describe("FiberMap", () => { + it.effect("interrupts fibers", () => + Effect.gen(function*() { + const ref = yield* (Ref.make(0)) + yield* pipe( + Effect.gen(function*() { + const map = yield* (FiberMap.make()) + yield* ( + Effect.forEach(Array.range(1, 10), (i) => + Effect.onInterrupt( + Effect.never, + () => Ref.update(ref, (n) => n + 1) + ).pipe( + FiberMap.run(map, i) + )) + ) + yield* (Effect.yieldNow()) + }), + Effect.scoped + ) + + strictEqual(yield* (Ref.get(ref)), 10) + })) + + it.effect("runtime", () => + Effect.gen(function*() { + const ref = yield* (Ref.make(0)) + yield* pipe( + Effect.gen(function*() { + const map = yield* (FiberMap.make()) + const run = yield* (FiberMap.runtime(map)()) + Array.range(1, 10).forEach((i) => + run( + i, + Effect.onInterrupt( + Effect.never, + () => Ref.update(ref, (n) => n + 1) + ) + ) + ) + yield* (Effect.yieldNow()) + }), + Effect.scoped + ) + + strictEqual(yield* (Ref.get(ref)), 10) + })) + + it.scoped("join", () => + Effect.gen(function*() { + const map = yield* (FiberMap.make()) + FiberMap.unsafeSet(map, "a", Effect.runFork(Effect.void)) + FiberMap.unsafeSet(map, "b", Effect.runFork(Effect.void)) + FiberMap.unsafeSet(map, "c", Effect.runFork(Effect.fail("fail"))) + FiberMap.unsafeSet(map, "d", Effect.runFork(Effect.fail("ignored"))) + const result = yield* pipe(FiberMap.join(map), Effect.flip) + strictEqual(result, "fail") + })) + + it.effect("size", () => + Effect.gen(function*() { + const scope = yield* (Scope.make()) + const set = yield* pipe(FiberMap.make(), Scope.extend(scope)) + FiberMap.unsafeSet(set, "a", Effect.runFork(Effect.never)) + FiberMap.unsafeSet(set, "b", Effect.runFork(Effect.never)) + strictEqual(yield* (FiberMap.size(set)), 2) + yield* (Scope.close(scope, Exit.void)) + strictEqual(yield* (FiberMap.size(set)), 0) + })) + + it.scoped("onlyIfMissing", () => + Effect.gen(function*() { + const handle = yield* (FiberMap.make()) + const fiberA = yield* (FiberMap.run(handle, "a", Effect.never)) + const fiberB = yield* (FiberMap.run(handle, "a", Effect.never, { onlyIfMissing: true })) + const fiberC = yield* (FiberMap.run(handle, "a", Effect.never, { onlyIfMissing: true })) + yield* (Effect.yieldNow()) + assertTrue(Exit.isInterrupted(yield* (fiberB.await))) + assertTrue(Exit.isInterrupted(yield* (fiberC.await))) + strictEqual(fiberA.unsafePoll(), null) + })) + + it.scoped("runtime onlyIfMissing", () => + Effect.gen(function*() { + const run = yield* (FiberMap.makeRuntime()) + const fiberA = run("a", Effect.never) + const fiberB = run("a", Effect.never, { onlyIfMissing: true }) + const fiberC = run("a", Effect.never, { onlyIfMissing: true }) + yield* (Effect.yieldNow()) + assertTrue(Exit.isInterrupted(yield* (fiberB.await))) + assertTrue(Exit.isInterrupted(yield* (fiberC.await))) + strictEqual(fiberA.unsafePoll(), null) + })) + + it.scoped("propagateInterruption false", () => + Effect.gen(function*() { + const map = yield* FiberMap.make() + const fiber = yield* FiberMap.run(map, "a", Effect.never, { + propagateInterruption: false + }) + yield* Effect.yieldNow() + yield* Fiber.interrupt(fiber) + assertFalse(yield* Deferred.isDone(map.deferred)) + })) + + it.scoped("propagateInterruption true", () => + Effect.gen(function*() { + const map = yield* FiberMap.make() + const fiber = yield* FiberMap.run(map, "a", Effect.never, { + propagateInterruption: true + }) + yield* Effect.yieldNow() + yield* Fiber.interrupt(fiber) + assertTrue(Exit.isInterrupted( + yield* FiberMap.join(map).pipe( + Effect.exit + ) + )) + })) + + it.scoped("awaitEmpty", () => + Effect.gen(function*() { + const map = yield* FiberMap.make() + yield* FiberMap.run(map, "a", Effect.sleep(1000)) + yield* FiberMap.run(map, "b", Effect.sleep(1000)) + yield* FiberMap.run(map, "c", Effect.sleep(1000)) + yield* FiberMap.run(map, "d", Effect.sleep(1000)) + + const fiber = yield* Effect.fork(FiberMap.awaitEmpty(map)) + yield* TestClock.adjust(500) + assert.isNull(fiber.unsafePoll()) + yield* TestClock.adjust(500) + assert.isDefined(fiber.unsafePoll()) + })) + + it.scoped("makeRuntimePromise", () => + Effect.gen(function*() { + const run = yield* FiberMap.makeRuntimePromise() + const result = yield* Effect.promise(() => run("a", Effect.succeed("done"))) + strictEqual(result, "done") + })) +}) diff --git a/repos/effect/packages/effect/test/FiberRef.test.ts b/repos/effect/packages/effect/test/FiberRef.test.ts new file mode 100644 index 0000000..cf44f1d --- /dev/null +++ b/repos/effect/packages/effect/test/FiberRef.test.ts @@ -0,0 +1,367 @@ +import { describe, it } from "@effect/vitest" +import { assertInclude, assertTrue, strictEqual } from "@effect/vitest/utils" +import { + Chunk, + Clock, + Deferred, + Duration, + Effect, + Fiber, + FiberRef, + Function as Fun, + identity, + Option, + pipe, + Runtime +} from "effect" + +const initial = "initial" +const update = "update" +const update1 = "update1" +const update2 = "update2" + +const increment = (n: number): number => n + 1 + +const loseTimeAndCpu: Effect.Effect = Effect.yieldNow().pipe( + Effect.zipLeft(Clock.sleep(Duration.millis(1))), + Effect.repeatN(100) +) + +describe("FiberRef", () => { + it.scoped("get returns the current value", () => + Effect.gen(function*() { + const fiberRef = yield* FiberRef.make(initial) + const result = yield* FiberRef.get(fiberRef) + strictEqual(result, initial) + })) + it.scoped("get returns the correct value for a child", () => + Effect.gen(function*() { + const fiberRef = yield* FiberRef.make(initial) + const fiber = yield* Effect.fork(FiberRef.get(fiberRef)) + const result = yield* Fiber.join(fiber) + strictEqual(result, initial) + })) + it.scoped("getAndUpdate - changing the value", () => + Effect.gen(function*() { + const fiberRef = yield* FiberRef.make(initial) + const value1 = yield* FiberRef.getAndUpdate(fiberRef, () => update) + const value2 = yield* FiberRef.get(fiberRef) + strictEqual(value1, initial) + strictEqual(value2, update) + })) + it.scoped("getAndUpdateSome - changing the value", () => + Effect.gen(function*() { + const fiberRef = yield* FiberRef.make(initial) + const value1 = yield* FiberRef.getAndUpdateSome(fiberRef, () => Option.some(update)) + const value2 = yield* FiberRef.get(fiberRef) + strictEqual(value1, initial) + strictEqual(value2, update) + })) + it.scoped("getAndUpdateSome - not changing value", () => + Effect.gen(function*() { + const fiberRef = yield* FiberRef.make(initial) + const value1 = yield* FiberRef.getAndUpdateSome(fiberRef, () => Option.none()) + const value2 = yield* FiberRef.get(fiberRef) + strictEqual(value1, initial) + strictEqual(value2, initial) + })) + it.scoped("set updates the current value", () => + Effect.gen(function*() { + const fiberRef = yield* FiberRef.make(initial) + yield* FiberRef.set(fiberRef, update) + const result = yield* FiberRef.get(fiberRef) + strictEqual(result, update) + })) + it.scoped("set by a child doesn't update parent's value", () => + Effect.gen(function*() { + const fiberRef = yield* FiberRef.make(initial) + const deferred = yield* Deferred.make() + yield* FiberRef.set(fiberRef, update).pipe( + Effect.zipRight(Deferred.succeed(deferred, void 0)), + Effect.fork + ) + yield* Deferred.await(deferred) + const result = yield* FiberRef.get(fiberRef) + strictEqual(result, initial) + })) + it.scoped("modify - changing the value", () => + Effect.gen(function*() { + const fiberRef = yield* FiberRef.make(initial) + const value1 = yield* FiberRef.modify(fiberRef, () => [1, update]) + const value2 = yield* FiberRef.get(fiberRef) + strictEqual(value1, 1) + strictEqual(value2, update) + })) + it.scoped("modifySome - not changing the value", () => + Effect.gen(function*() { + const fiberRef = yield* FiberRef.make(initial) + const value1 = yield* FiberRef.modifySome(fiberRef, 2, () => Option.none()) + const value2 = yield* FiberRef.get(fiberRef) + strictEqual(value1, 2) + strictEqual(value2, initial) + })) + it.scoped("updateAndGet - changing the value", () => + Effect.gen(function*() { + const fiberRef = yield* FiberRef.make(initial) + const value1 = yield* FiberRef.updateAndGet(fiberRef, () => update) + const value2 = yield* FiberRef.get(fiberRef) + strictEqual(value1, update) + strictEqual(value2, update) + })) + it.scoped("updateSomeAndGet - changing the value", () => + Effect.gen(function*() { + const fiberRef = yield* FiberRef.make(initial) + const value1 = yield* FiberRef.updateSomeAndGet(fiberRef, () => Option.some(update)) + const value2 = yield* FiberRef.get(fiberRef) + strictEqual(value1, update) + strictEqual(value2, update) + })) + it.scoped("updateSomeAndGet - not changing the value", () => + Effect.gen(function*() { + const fiberRef = yield* FiberRef.make(initial) + const value1 = yield* FiberRef.updateSomeAndGet(fiberRef, () => Option.none()) + const value2 = yield* FiberRef.get(fiberRef) + strictEqual(value1, initial) + strictEqual(value2, initial) + })) + it.scoped("restores the original value", () => + Effect.gen(function*() { + const fiberRef = yield* FiberRef.make(initial) + yield* FiberRef.set(fiberRef, update) + yield* FiberRef.delete(fiberRef) + const result = yield* FiberRef.get(fiberRef) + strictEqual(result, initial) + })) + it.scoped("locally - restores original value", () => + Effect.gen(function*() { + const fiberRef = yield* FiberRef.make(initial) + const local = yield* Effect.locally(fiberRef, update)(FiberRef.get(fiberRef)) + const value = yield* FiberRef.get(fiberRef) + strictEqual(local, update) + strictEqual(value, initial) + })) + it.scoped("locally - restores parent's value", () => + Effect.gen(function*() { + const fiberRef = yield* FiberRef.make(initial) + const child = yield* Effect.locally(fiberRef, update)(FiberRef.get(fiberRef).pipe(Effect.fork)) + const local = yield* Fiber.join(child) + const value = yield* FiberRef.get(fiberRef) + strictEqual(local, update) + strictEqual(value, initial) + })) + it.scoped("locally - restores undefined value", () => + Effect.gen(function*() { + const child = yield* Effect.fork(FiberRef.make(initial)) // Don't use join as it inherits values from child + // Don't use join as it inherits values from child + const fiberRef = yield* pipe(Fiber.await(child), Effect.flatten) + const localValue = yield* Effect.locally(fiberRef, update)(FiberRef.get(fiberRef)) + const value = yield* FiberRef.get(fiberRef) + strictEqual(localValue, update) + strictEqual(value, initial) + })) + it.scoped("initial value is inherited on join", () => + Effect.gen(function*() { + const fiberRef = yield* FiberRef.make(initial) + const child = yield* Effect.fork(FiberRef.set(fiberRef, update)) + yield* Fiber.join(child) + const result = yield* FiberRef.get(fiberRef) + strictEqual(result, update) + })) + it.scoped("initial value is always available", () => + Effect.gen(function*() { + const child = yield* Effect.fork(FiberRef.make(initial)) + const fiberRef = yield* pipe(Fiber.await(child), Effect.flatten) + const result = yield* FiberRef.get(fiberRef) + strictEqual(result, initial) + })) + it.scoped("fork function is applied on fork - 1", () => + Effect.gen(function*() { + const fiberRef = yield* FiberRef.make(0, { fork: increment }) + const child = yield* Effect.fork(Effect.void) + yield* Fiber.join(child) + const result = yield* FiberRef.get(fiberRef) + strictEqual(result, 1) + })) + it.scoped("fork function is applied on fork - 2", () => + Effect.gen(function*() { + const fiberRef = yield* FiberRef.make(0, { fork: increment }) + const child = yield* pipe(Effect.void, Effect.fork, Effect.flatMap(Fiber.join), Effect.fork) + yield* Fiber.join(child) + const result = yield* FiberRef.get(fiberRef) + strictEqual(result, 2) + })) + it.scoped("join function is applied on join - 1", () => + Effect.gen(function*() { + const fiberRef = yield* FiberRef.make(0, { fork: identity, join: Math.max }) + const child = yield* Effect.fork(FiberRef.update(fiberRef, increment)) + yield* Fiber.join(child) + const result = yield* FiberRef.get(fiberRef) + strictEqual(result, 1) + })) + it.scoped("join function is applied on join - 2", () => + Effect.gen(function*() { + const fiberRef = yield* FiberRef.make(0, { fork: identity, join: Math.max }) + const child = yield* Effect.fork(FiberRef.update(fiberRef, increment)) + yield* FiberRef.update(fiberRef, (n) => n + 2) + yield* Fiber.join(child) + const result = yield* FiberRef.get(fiberRef) + strictEqual(result, 2) + })) + it.scopedLive("the value of the loser is inherited in zipPar", () => + Effect.gen(function*() { + const fiberRef = yield* FiberRef.make(initial) + const latch = yield* Deferred.make() + const winner = FiberRef.set(fiberRef, update1).pipe(Effect.zipRight(Deferred.succeed(latch, void 0))) + const loser = Deferred.await(latch).pipe( + Effect.zipRight(Clock.sleep(Duration.millis(1))), + Effect.zipRight(FiberRef.set(fiberRef, update2)) + ) + yield* pipe(winner, Effect.zip(loser, { concurrent: true })) + const result = yield* FiberRef.get(fiberRef) + strictEqual(result, update2) + })) + it.scoped("nothing gets inherited with a failure in zipPar", () => + Effect.gen(function*() { + const fiberRef = yield* FiberRef.make(initial) + const success = FiberRef.set(fiberRef, update) + const failure1 = FiberRef.set(fiberRef, update).pipe(Effect.zipRight(Effect.fail(":-("))) + const failure2 = FiberRef.set(fiberRef, update).pipe(Effect.zipRight(Effect.fail(":-O"))) + yield* pipe( + success, + Effect.zip(failure1.pipe(Effect.zip(failure2, { concurrent: true })), { concurrent: true }), + Effect.orElse(() => Effect.void) + ) + const result = yield* FiberRef.get(fiberRef) + assertInclude(result, initial) + })) + it.scoped("the value of all fibers in inherited when running many effects with collectAllPar", () => + Effect.gen(function*() { + const n = 1000 + const fiberRef = yield* FiberRef.make(0, { + fork: Fun.constant(0), + join: (a, b) => a + b + }) + yield* Effect.all(Array.from({ length: n }, () => FiberRef.update(fiberRef, (n) => n + 1)), { + concurrency: "unbounded", + discard: true + }) + const result = yield* FiberRef.get(fiberRef) + strictEqual(result, n) + })) + it.scoped("its value is inherited after simple race", () => + Effect.gen(function*() { + const fiberRef = yield* FiberRef.make(initial) + yield* pipe(FiberRef.set(fiberRef, update1), Effect.race(FiberRef.set(fiberRef, update2))) + const result = yield* FiberRef.get(fiberRef) + assertTrue(new RegExp(`${update1}|${update2}`).test(result)) + })) + it.scopedLive("its value is inherited after a race with a bad winner", () => + Effect.gen(function*() { + const fiberRef = yield* FiberRef.make(initial) + const latch = yield* Deferred.make() + const badWinner = FiberRef.set(fiberRef, update1).pipe( + Effect.zipRight(Effect.fail("ups").pipe(Effect.ensuring(Deferred.succeed(latch, void 0)))) + ) + const goodLoser = FiberRef.set(fiberRef, update2).pipe( + Effect.zipRight(Deferred.await(latch)), + Effect.zipRight(Effect.sleep(Duration.seconds(1))) + ) + yield* pipe(badWinner, Effect.race(goodLoser)) + const result = yield* FiberRef.get(fiberRef) + strictEqual(result, update2) + })) + it.scoped("its value is not inherited after a race of losers", () => + Effect.gen(function*() { + const fiberRef = yield* FiberRef.make(initial) + const loser1 = FiberRef.set(fiberRef, update1).pipe(Effect.zipRight(Effect.fail("ups1"))) + const loser2 = FiberRef.set(fiberRef, update2).pipe(Effect.zipRight(Effect.fail("ups2"))) + yield* pipe(loser1, Effect.race(loser2), Effect.ignore) + const result = yield* FiberRef.get(fiberRef) + strictEqual(result, initial) + })) + it.scoped("its value is inherited in a trivial race", () => + Effect.gen(function*() { + const fiberRef = yield* FiberRef.make(initial) + yield* Effect.raceAll([FiberRef.set(fiberRef, update)]) + const result = yield* FiberRef.get(fiberRef) + strictEqual(result, update) + })) + it.scoped("the value of the winner is inherited when racing two effects with raceAll", () => + Effect.gen(function*() { + const fiberRef = yield* FiberRef.make(initial) + const latch = yield* Deferred.make() + const winner1 = FiberRef.set(fiberRef, update1).pipe( + Effect.zipRight(Deferred.succeed(latch, void 0)) + ) + const loser1 = Deferred.await(latch).pipe( + Effect.zipRight(FiberRef.set(fiberRef, update2)), + Effect.zipRight(loseTimeAndCpu) + ) + yield* Effect.raceAll([loser1, winner1]) + const value1 = yield* pipe(FiberRef.get(fiberRef), Effect.zipLeft(FiberRef.set(fiberRef, initial))) + const winner2 = FiberRef.set(fiberRef, update1) + const loser2 = FiberRef.set(fiberRef, update2).pipe(Effect.zipRight(Effect.fail(":-O"))) + yield* Effect.raceAll([loser2, winner2]) + const value2 = yield* pipe(FiberRef.get(fiberRef), Effect.zipLeft(FiberRef.set(fiberRef, initial))) + strictEqual(value1, update1) + strictEqual(value2, update1) + })) + it.scoped("the value of the winner is inherited when racing many effects with raceAll", () => + Effect.gen(function*() { + const n = 63 + const fiberRef = yield* FiberRef.make(initial) + const latch = yield* Deferred.make() + const winner1 = FiberRef.set(fiberRef, update1).pipe( + Effect.zipRight(Deferred.succeed(latch, void 0)), + Effect.asVoid + ) + const losers1 = Deferred.await(latch).pipe( + Effect.zipRight(FiberRef.set(fiberRef, update2)), + Effect.zipRight(loseTimeAndCpu), + Effect.replicate(n) + ) + yield* pipe(Chunk.unsafeFromArray(losers1), Chunk.prepend(winner1), Effect.raceAll) + const value1 = yield* pipe(FiberRef.get(fiberRef), Effect.zipLeft(FiberRef.set(fiberRef, initial))) + const winner2 = FiberRef.set(fiberRef, update1) + const losers2 = FiberRef.set(fiberRef, update1).pipe(Effect.zipRight(Effect.fail(":-O")), Effect.replicate(n)) + yield* pipe(Chunk.unsafeFromArray(losers2), Chunk.prepend(winner2), Effect.raceAll) + const value2 = yield* pipe(FiberRef.get(fiberRef), Effect.zipLeft(FiberRef.set(fiberRef, initial))) + strictEqual(value1, update1) + strictEqual(value2, update1) + })) + it.scoped("nothing gets inherited when racing failures with raceAll", () => + Effect.gen(function*() { + const fiberRef = yield* FiberRef.make(initial) + const loser = FiberRef.set(fiberRef, update).pipe(Effect.zipRight(Effect.fail("darn"))) + yield* pipe(Effect.raceAll([loser, ...Array.from({ length: 63 }, () => loser)]), Effect.orElse(() => Effect.void)) + const result = yield* FiberRef.get(fiberRef) + strictEqual(result, initial) + })) + it.scoped("fork patch is applied when a fiber is unsafely run", () => + Effect.gen(function*() { + const fiberRef = yield* FiberRef.make(true, { fork: Fun.constTrue }) + const deferred = yield* Deferred.make() + const runtime: Runtime.Runtime = yield* Effect.runtime().pipe(Effect.locally(fiberRef, false)) + yield* Effect.sync(() => FiberRef.get(fiberRef).pipe(Effect.intoDeferred(deferred), Runtime.runCallback(runtime))) + const result = yield* Deferred.await(deferred) + assertTrue(result) + })) + it.scoped("fork patch is applied when a fiber is unsafely forked", () => + Effect.gen(function*() { + const fiberRef = yield* FiberRef.make(true, { fork: Fun.constTrue }) + const deferred = yield* Deferred.make() + const runtime: Runtime.Runtime = yield* Effect.locally(Effect.runtime(), fiberRef, false) + const fiber = yield* Effect.sync(() => + Runtime.runFork(runtime)(Effect.intoDeferred(FiberRef.get(fiberRef), deferred)) + ) + yield* Fiber.join(fiber) + const result = yield* Deferred.await(deferred) + assertTrue(result) + })) + it.scoped("is subtype of Effect", () => + Effect.gen(function*() { + const fiberRef = yield* FiberRef.make(initial) + const result = yield* fiberRef + strictEqual(result, initial) + })) +}) diff --git a/repos/effect/packages/effect/test/FiberRefs.test.ts b/repos/effect/packages/effect/test/FiberRefs.test.ts new file mode 100644 index 0000000..e6a7703 --- /dev/null +++ b/repos/effect/packages/effect/test/FiberRefs.test.ts @@ -0,0 +1,54 @@ +import { describe, it } from "@effect/vitest" +import { assertTrue, deepStrictEqual, strictEqual } from "@effect/vitest/utils" +import { Cause, Effect, Exit, Fiber, FiberId, FiberRef, FiberRefs, HashMap, Option, pipe, Queue, Scope } from "effect" + +describe("FiberRefs", () => { + it.scoped("propagate FiberRef values across fiber boundaries", () => + Effect.gen(function*() { + const fiberRef = yield* FiberRef.make(false) + const queue = yield* Queue.unbounded() + const producer = yield* FiberRef.set(fiberRef, true).pipe( + Effect.zipRight(Effect.getFiberRefs.pipe(Effect.flatMap((a) => Queue.offer(queue, a)))), + Effect.fork + ) + const consumer = yield* pipe( + Queue.take(queue), + Effect.flatMap((fiberRefs) => Effect.setFiberRefs(fiberRefs).pipe(Effect.zipRight(FiberRef.get(fiberRef)))), + Effect.fork + ) + yield* Fiber.join(producer) + const result = yield* Fiber.join(consumer) + assertTrue(result) + })) + it("interruptedCause", () => { + const parent = FiberId.make(1, Date.now()) as FiberId.Runtime + const child = FiberId.make(2, Date.now()) as FiberId.Runtime + const parentFiberRefs = FiberRefs.unsafeMake(new Map()) + const childFiberRefs = FiberRefs.updateAs(parentFiberRefs, { + fiberId: child, + fiberRef: FiberRef.interruptedCause, + value: Cause.interrupt(parent) + }) + const newParentFiberRefs = FiberRefs.joinAs(parentFiberRefs, parent, childFiberRefs) + deepStrictEqual(FiberRefs.get(newParentFiberRefs, FiberRef.interruptedCause), Option.some(Cause.empty)) + }) + + describe("currentLogAnnotations", () => { + it("doesnt leak", () => { + Effect.void.pipe(Effect.annotateLogs("test", "abc"), Effect.runSync) + strictEqual(FiberRef.currentLogAnnotations.pipe(FiberRef.get, Effect.map(HashMap.size), Effect.runSync), 0) + }) + + it.effect("annotateLogsScoped", () => + Effect.gen(function*() { + const scope = yield* Scope.make() + strictEqual(HashMap.size(yield* FiberRef.get(FiberRef.currentLogAnnotations)), 0) + yield* Effect.annotateLogsScoped({ + test: 123 + }).pipe(Scope.extend(scope)) + strictEqual(HashMap.size(yield* FiberRef.get(FiberRef.currentLogAnnotations)), 1) + yield* Scope.close(scope, Exit.void) + strictEqual(HashMap.size(yield* FiberRef.get(FiberRef.currentLogAnnotations)), 0) + })) + }) +}) diff --git a/repos/effect/packages/effect/test/FiberSet.test.ts b/repos/effect/packages/effect/test/FiberSet.test.ts new file mode 100644 index 0000000..29686aa --- /dev/null +++ b/repos/effect/packages/effect/test/FiberSet.test.ts @@ -0,0 +1,118 @@ +import { assert, describe, it } from "@effect/vitest" +import { assertFalse, assertTrue, strictEqual } from "@effect/vitest/utils" +import { Array, Deferred, Effect, Exit, Fiber, FiberSet, pipe, Ref, Scope, TestClock } from "effect" + +describe("FiberSet", () => { + it.effect("interrupts fibers", () => + Effect.gen(function*() { + const ref = yield* (Ref.make(0)) + yield* pipe( + Effect.gen(function*() { + const set = yield* (FiberSet.make()) + yield* pipe( + Effect.onInterrupt( + Effect.never, + () => Ref.update(ref, (n) => n + 1) + ).pipe(FiberSet.run(set)), + Effect.replicateEffect(10) + ) + yield* (Effect.yieldNow()) + }), + Effect.scoped + ) + + strictEqual(yield* (Ref.get(ref)), 10) + })) + + it.effect("runtime", () => + Effect.gen(function*() { + const ref = yield* (Ref.make(0)) + yield* pipe( + Effect.gen(function*() { + const set = yield* (FiberSet.make()) + const run = yield* (FiberSet.runtime(set)()) + Array.range(1, 10).forEach(() => + run( + Effect.onInterrupt( + Effect.never, + () => Ref.update(ref, (n) => n + 1) + ) + ) + ) + yield* (Effect.yieldNow()) + }), + Effect.scoped + ) + + strictEqual(yield* (Ref.get(ref)), 10) + })) + + it.scoped("join", () => + Effect.gen(function*() { + const set = yield* (FiberSet.make()) + FiberSet.unsafeAdd(set, Effect.runFork(Effect.void)) + FiberSet.unsafeAdd(set, Effect.runFork(Effect.void)) + FiberSet.unsafeAdd(set, Effect.runFork(Effect.fail("fail"))) + const result = yield* pipe(FiberSet.join(set), Effect.flip) + strictEqual(result, "fail") + })) + + it.effect("size", () => + Effect.gen(function*() { + const scope = yield* (Scope.make()) + const set = yield* pipe(FiberSet.make(), Scope.extend(scope)) + FiberSet.unsafeAdd(set, Effect.runFork(Effect.never)) + FiberSet.unsafeAdd(set, Effect.runFork(Effect.never)) + strictEqual(yield* (FiberSet.size(set)), 2) + yield* (Scope.close(scope, Exit.void)) + strictEqual(yield* (FiberSet.size(set)), 0) + })) + + it.scoped("propagateInterruption false", () => + Effect.gen(function*() { + const set = yield* FiberSet.make() + const fiber = yield* FiberSet.run(set, Effect.never, { + propagateInterruption: false + }) + yield* Effect.yieldNow() + yield* Fiber.interrupt(fiber) + assertFalse(yield* Deferred.isDone(set.deferred)) + })) + + it.scoped("propagateInterruption true", () => + Effect.gen(function*() { + const set = yield* FiberSet.make() + const fiber = yield* FiberSet.run(set, Effect.never, { + propagateInterruption: true + }) + yield* Effect.yieldNow() + yield* Fiber.interrupt(fiber) + assertTrue(Exit.isInterrupted( + yield* FiberSet.join(set).pipe( + Effect.exit + ) + )) + })) + + it.scoped("awaitEmpty", () => + Effect.gen(function*() { + const set = yield* FiberSet.make() + yield* FiberSet.run(set, Effect.sleep(1000)) + yield* FiberSet.run(set, Effect.sleep(1000)) + yield* FiberSet.run(set, Effect.sleep(1000)) + yield* FiberSet.run(set, Effect.sleep(1000)) + + const fiber = yield* Effect.fork(FiberSet.awaitEmpty(set)) + yield* TestClock.adjust(500) + assert.isNull(fiber.unsafePoll()) + yield* TestClock.adjust(500) + assert.isDefined(fiber.unsafePoll()) + })) + + it.scoped("makeRuntimePromise", () => + Effect.gen(function*() { + const run = yield* FiberSet.makeRuntimePromise() + const result = yield* Effect.promise(() => run(Effect.succeed("done"))) + strictEqual(result, "done") + })) +}) diff --git a/repos/effect/packages/effect/test/Function.test.ts b/repos/effect/packages/effect/test/Function.test.ts new file mode 100644 index 0000000..de67d89 --- /dev/null +++ b/repos/effect/packages/effect/test/Function.test.ts @@ -0,0 +1,198 @@ +import { describe, it } from "@effect/vitest" +import { deepStrictEqual, strictEqual, throws } from "@effect/vitest/utils" +import { Function, String } from "effect" + +const f = (n: number): number => n + 1 +const g = (n: number) => n * 2 + +describe("Function", () => { + it("apply", () => { + const countArgs = (...args: Array) => args.length + + deepStrictEqual(Function.pipe(countArgs, Function.apply("a")), 1) + deepStrictEqual(Function.pipe(countArgs, Function.apply("a", "b", "c")), 3) + }) + + it("compose", () => { + deepStrictEqual(Function.pipe(String.length, Function.compose((n) => n * 2))("aaa"), 6) + deepStrictEqual(Function.compose(String.length, (n) => n * 2)("aaa"), 6) + }) + + it("flip", () => { + const f = (a: number) => (b: string) => a - b.length + const g = (a: number, i = 0) => (b: number) => a ** b + i + + deepStrictEqual(Function.flip(f)("aaa")(2), -1) + deepStrictEqual(Function.flip(g)(2)(2, 1), 5) + }) + + it("unsafeCoerce", () => { + deepStrictEqual(Function.unsafeCoerce, Function.identity) + }) + + it("satisfies", () => { + deepStrictEqual(Function.satisfies()(5), 5) + // @ts-expect-error + deepStrictEqual(Function.satisfies()(5), 5) + }) + + it("constant", () => { + deepStrictEqual(Function.constant("a")(), "a") + }) + + it("constTrue", () => { + deepStrictEqual(Function.constTrue(), true) + }) + + it("constFalse", () => { + deepStrictEqual(Function.constFalse(), false) + }) + + it("constNull", () => { + deepStrictEqual(Function.constNull(), null) + }) + + it("constUndefined", () => { + deepStrictEqual(Function.constUndefined(), undefined) + }) + + it("constVoid", () => { + deepStrictEqual(Function.constVoid(), undefined) + }) + + it("absurd", () => { + throws(() => Function.absurd(null as any as never)) + }) + + it("hole", () => { + throws(() => Function.hole()) + }) + + it("SK", () => { + strictEqual(Function.SK(1, 2), 2) + }) + + it("tupled", () => { + const f1 = (a: number): number => a * 2 + const f2 = (a: number, b: number): number => a + b + const u1 = Function.tupled(f1) + const u2 = Function.tupled(f2) + deepStrictEqual(u1([1]), 2) + deepStrictEqual(u2([1, 2]), 3) + }) + + it("untupled", () => { + const f1 = (a: readonly [number]): number => a[0] * 2 + const f2 = (a: readonly [number, number]): number => a[0] + a[1] + const u1 = Function.untupled(f1) + const u2 = Function.untupled(f2) + deepStrictEqual(u1(1), 2) + deepStrictEqual(u2(1, 2), 3) + }) + + it("pipe()", () => { + const pipe = Function.pipe // this alias is required in order to exclude the `@effect/babel-plugin` compiler and get 100% coverage + // @effect-diagnostics-next-line unnecessaryPipe:off + deepStrictEqual(pipe(2), 2) + deepStrictEqual(pipe(2, f), 3) + deepStrictEqual(pipe(2, f, g), 6) + deepStrictEqual(pipe(2, f, g, f), 7) + deepStrictEqual(pipe(2, f, g, f, g), 14) + deepStrictEqual(pipe(2, f, g, f, g, f), 15) + deepStrictEqual(pipe(2, f, g, f, g, f, g), 30) + deepStrictEqual(pipe(2, f, g, f, g, f, g, f), 31) + deepStrictEqual(pipe(2, f, g, f, g, f, g, f, g), 62) + deepStrictEqual(pipe(2, f, g, f, g, f, g, f, g, f), 63) + deepStrictEqual(pipe(2, f, g, f, g, f, g, f, g, f, g), 126) + deepStrictEqual(pipe(2, f, g, f, g, f, g, f, g, f, g, f), 127) + deepStrictEqual(pipe(2, f, g, f, g, f, g, f, g, f, g, f, g), 254) + deepStrictEqual(pipe(2, f, g, f, g, f, g, f, g, f, g, f, g, f), 255) + deepStrictEqual(pipe(2, f, g, f, g, f, g, f, g, f, g, f, g, f, g), 510) + deepStrictEqual(pipe(2, f, g, f, g, f, g, f, g, f, g, f, g, f, g, f), 511) + deepStrictEqual(pipe(2, f, g, f, g, f, g, f, g, f, g, f, g, f, g, f, g), 1022) + deepStrictEqual(pipe(2, f, g, f, g, f, g, f, g, f, g, f, g, f, g, f, g, f), 1023) + deepStrictEqual(pipe(2, f, g, f, g, f, g, f, g, f, g, f, g, f, g, f, g, f, g), 2046) + deepStrictEqual(pipe(2, f, g, f, g, f, g, f, g, f, g, f, g, f, g, f, g, f, g, f), 2047) + deepStrictEqual( + (Function.pipe as any)(...[2, f, g, f, g, f, g, f, g, f, g, f, g, f, g, f, g, f, g, f, g]), + 4094 + ) + }) + + it("flow", () => { + const flow = Function.flow // this alias is required in order to exclude the `@effect/babel-plugin` compiler and get 100% coverage + deepStrictEqual(flow(f)(2), 3) + deepStrictEqual(flow(f, g)(2), 6) + deepStrictEqual(flow(f, g, f)(2), 7) + deepStrictEqual(flow(f, g, f, g)(2), 14) + deepStrictEqual(flow(f, g, f, g, f)(2), 15) + deepStrictEqual(flow(f, g, f, g, f, g)(2), 30) + deepStrictEqual(flow(f, g, f, g, f, g, f)(2), 31) + deepStrictEqual(flow(f, g, f, g, f, g, f, g)(2), 62) + deepStrictEqual(flow(f, g, f, g, f, g, f, g, f)(2), 63) + // this is just to satisfy noImplicitReturns and 100% coverage + deepStrictEqual((Function.flow as any)(...[f, g, f, g, f, g, f, g, f, g]), undefined) + }) + + describe("dual", () => { + it("arity as predicate", () => { + const f = Function.dual< + (that: number) => (self: number) => number, + (self: number, that: number) => number + >((args) => args.length >= 2, (a: number, b: number): number => a - b) + deepStrictEqual(f(3, 2), 1) + deepStrictEqual(Function.pipe(3, f(2)), 1) + // should ignore excess arguments + deepStrictEqual(f.apply(null, [3, 2, 100] as any), 1) + }) + + it("arity: 0", () => { + throws(() => + Function.dual< + () => () => number, + () => number + >(0, (): number => 2), new RangeError("Invalid arity 0")) + }) + + it("arity: 1", () => { + throws(() => + Function.dual< + () => (self: number) => number, + (self: number) => number + >(1, (self: number): number => self * 2), new RangeError("Invalid arity 1")) + }) + + it("arity: 2", () => { + const f = Function.dual< + (that: number) => (self: number) => number, + (self: number, that: number) => number + >(2, (a: number, b: number): number => a - b) + deepStrictEqual(f(3, 2), 1) + deepStrictEqual(Function.pipe(3, f(2)), 1) + // should ignore excess arguments + deepStrictEqual(f.apply(null, [3, 2, 100] as any), 1) + }) + + it("arity: 5", () => { + const f = Function.dual< + (a: string, b: string, c: string, d: string) => (self: string) => string, + (self: string, a: string, b: string, c: string, d: string) => string + >(5, (self: string, a: string, b: string, c: string, d: string): string => self + a + b + c + d) + deepStrictEqual(f("_", "a", "b", "c", "d"), "_abcd") + deepStrictEqual(Function.pipe("_", f("a", "b", "c", "d")), "_abcd") + // should ignore excess arguments + deepStrictEqual(f.apply(null, ["_", "a", "b", "c", "d", "e"] as any), "_abcd") + }) + + it("arity > 5", () => { + const f = Function.dual< + (a: string, b: string, c: string, d: string, e: string) => (self: string) => string, + (self: string, a: string, b: string, c: string, d: string, e: string) => string + >(6, (self: string, a: string, b: string, c: string, d: string, e: string): string => self + a + b + c + d + e) + deepStrictEqual(f("_", "a", "b", "c", "d", "e"), "_abcde") + deepStrictEqual(Function.pipe("_", f("a", "b", "c", "d", "e")), "_abcde") + // should ignore excess arguments + deepStrictEqual(f.apply(null, ["_", "a", "b", "c", "d", "e", "f"] as any), "_abcde") + }) + }) +}) diff --git a/repos/effect/packages/effect/test/GlobalValue.test.ts b/repos/effect/packages/effect/test/GlobalValue.test.ts new file mode 100644 index 0000000..7754133 --- /dev/null +++ b/repos/effect/packages/effect/test/GlobalValue.test.ts @@ -0,0 +1,12 @@ +import { describe, it } from "@effect/vitest" +import { strictEqual } from "@effect/vitest/utils" +import { GlobalValue as G } from "effect" + +const a = G.globalValue("id", () => ({})) +const b = G.globalValue("id", () => ({})) + +describe("GlobalValue", () => { + it("should give the same value when invoked with the same id", () => { + strictEqual(a, b) + }) +}) diff --git a/repos/effect/packages/effect/test/Graph.test.ts b/repos/effect/packages/effect/test/Graph.test.ts new file mode 100644 index 0000000..42d03a2 --- /dev/null +++ b/repos/effect/packages/effect/test/Graph.test.ts @@ -0,0 +1,3277 @@ +import { describe, expect, it } from "@effect/vitest" +import { Equal, Graph, Hash, Option } from "effect" + +describe("Graph", () => { + describe("constructors", () => { + it("should create empty directed graph", () => { + const graph = Graph.directed() + + expect(graph.type).toBe("directed") + expect(Graph.nodeCount(graph)).toBe(0) + expect(Graph.edgeCount(graph)).toBe(0) + }) + + it("should create empty undirected graph", () => { + const graph = Graph.undirected() + + expect(graph.type).toBe("undirected") + expect(Graph.nodeCount(graph)).toBe(0) + expect(Graph.edgeCount(graph)).toBe(0) + }) + }) + + describe("isGraph", () => { + it("should return true for graph instances", () => { + const directedGraph = Graph.directed() + const undirectedGraph = Graph.undirected() + + expect(Graph.isGraph(directedGraph)).toBe(true) + expect(Graph.isGraph(undirectedGraph)).toBe(true) + }) + + it("should return false for non-graph values", () => { + expect(Graph.isGraph({})).toBe(false) + expect(Graph.isGraph(null)).toBe(false) + expect(Graph.isGraph(undefined)).toBe(false) + expect(Graph.isGraph("string")).toBe(false) + expect(Graph.isGraph(42)).toBe(false) + expect(Graph.isGraph([])).toBe(false) + }) + + it("should be iterable using for...of syntax", () => { + const graph = Graph.directed((mutable) => { + Graph.addNode(mutable, "Node A") + Graph.addNode(mutable, "Node B") + Graph.addNode(mutable, "Node C") + }) + + const collected: Array = [] + for (const entry of graph) { + collected.push(entry) + } + + expect(collected).toHaveLength(3) + expect(collected).toEqual([ + [0, "Node A"], + [1, "Node B"], + [2, "Node C"] + ]) + }) + + it("should support manual iterator operations", () => { + const graph = Graph.directed((mutable) => { + Graph.addNode(mutable, "Node A") + Graph.addNode(mutable, "Node B") + }) + + const iterator = graph[Symbol.iterator]() + const first = iterator.next() + const second = iterator.next() + const third = iterator.next() + + expect(first.done).toBe(false) + expect(first.value).toEqual([0, "Node A"]) + expect(second.done).toBe(false) + expect(second.value).toEqual([1, "Node B"]) + expect(third.done).toBe(true) + }) + }) + + describe("undefined data handling", () => { + describe("undefined node data", () => { + it("should allow adding nodes with undefined data", () => { + const graph = Graph.directed((mutable) => { + const nodeA = Graph.addNode(mutable, undefined) + const nodeB = Graph.addNode(mutable, undefined) + Graph.addEdge(mutable, nodeA, nodeB, 1) + }) + + expect(Graph.nodeCount(graph)).toBe(2) + expect(Graph.edgeCount(graph)).toBe(1) + expect(Graph.getNode(graph, 0)).toEqual(Option.some(undefined)) + expect(Graph.getNode(graph, 1)).toEqual(Option.some(undefined)) + }) + + it("should correctly update nodes with undefined data", () => { + const graph = Graph.directed((mutable) => { + Graph.addNode(mutable, undefined) + Graph.addNode(mutable, "defined") + }) + + const updated = Graph.mutate(graph, (mutable) => { + Graph.updateNode(mutable, 0, () => "now defined") + Graph.updateNode(mutable, 1, () => undefined) + }) + + expect(Graph.getNode(updated, 0)).toEqual(Option.some("now defined")) + expect(Graph.getNode(updated, 1)).toEqual(Option.some(undefined)) + }) + + it("should correctly compare graphs with undefined node data", () => { + const graph1 = Graph.directed((mutable) => { + Graph.addNode(mutable, undefined) + Graph.addNode(mutable, undefined) + }) + + const graph2 = Graph.directed((mutable) => { + Graph.addNode(mutable, undefined) + Graph.addNode(mutable, undefined) + }) + + expect(Equal.equals(graph1, graph2)).toBe(true) + }) + + it("should find nodes with undefined data using predicates", () => { + const graph = Graph.directed((mutable) => { + Graph.addNode(mutable, undefined) + Graph.addNode(mutable, "defined") + Graph.addNode(mutable, undefined) + }) + + const undefinedNode = Graph.findNode(graph, (data) => data === undefined) + const undefinedNodes = Graph.findNodes(graph, (data) => data === undefined) + + expect(undefinedNode).toEqual(Option.some(0)) + expect(undefinedNodes).toEqual([0, 2]) + }) + + it("should iterate correctly over graphs with undefined node data", () => { + const graph = Graph.directed((mutable) => { + Graph.addNode(mutable, undefined) + Graph.addNode(mutable, undefined) + }) + + const collected: Array = [] + for (const entry of graph) { + collected.push(entry) + } + + expect(collected).toEqual([ + [0, undefined], + [1, undefined] + ]) + }) + }) + + describe("undefined edge data", () => { + it("should allow adding edges with undefined data", () => { + const graph = Graph.directed((mutable) => { + const nodeA = Graph.addNode(mutable, "A") + const nodeB = Graph.addNode(mutable, "B") + Graph.addEdge(mutable, nodeA, nodeB, undefined) + }) + + expect(Graph.edgeCount(graph)).toBe(1) + expect(Graph.getEdge(graph, 0)).toEqual(Option.some({ source: 0, target: 1, data: undefined })) + }) + + it("should correctly update edges with undefined data", () => { + const graph = Graph.directed((mutable) => { + const nodeA = Graph.addNode(mutable, "A") + const nodeB = Graph.addNode(mutable, "B") + Graph.addEdge(mutable, nodeA, nodeB, undefined) + Graph.addEdge(mutable, nodeB, nodeA, 42) + }) + + const updated = Graph.mutate(graph, (mutable) => { + Graph.updateEdge(mutable, 0, () => 100) + Graph.updateEdge(mutable, 1, () => undefined) + }) + + const edge0 = Graph.getEdge(updated, 0) + const edge1 = Graph.getEdge(updated, 1) + + expect(edge0).toEqual(Option.some({ source: 0, target: 1, data: 100 })) + expect(edge1).toEqual(Option.some({ source: 1, target: 0, data: undefined })) + }) + + it("should correctly compare graphs with undefined edge data", () => { + const graph1 = Graph.directed((mutable) => { + const a = Graph.addNode(mutable, "A") + const b = Graph.addNode(mutable, "B") + Graph.addEdge(mutable, a, b, undefined) + }) + + const graph2 = Graph.directed((mutable) => { + const a = Graph.addNode(mutable, "A") + const b = Graph.addNode(mutable, "B") + Graph.addEdge(mutable, a, b, undefined) + }) + + expect(Equal.equals(graph1, graph2)).toBe(true) + }) + + it("should find edges with undefined data using predicates", () => { + const graph = Graph.directed((mutable) => { + const a = Graph.addNode(mutable, "A") + const b = Graph.addNode(mutable, "B") + const c = Graph.addNode(mutable, "C") + Graph.addEdge(mutable, a, b, undefined) + Graph.addEdge(mutable, b, c, 42) + Graph.addEdge(mutable, c, a, undefined) + }) + + const undefinedEdge = Graph.findEdge(graph, (data) => data === undefined) + const undefinedEdges = Graph.findEdges(graph, (data) => data === undefined) + + expect(undefinedEdge).toEqual(Option.some(0)) + expect(undefinedEdges).toEqual([0, 2]) + }) + + it("should produce consistent hashes for graphs with undefined edge data", () => { + const graph1 = Graph.directed((mutable) => { + const a = Graph.addNode(mutable, "A") + const b = Graph.addNode(mutable, "B") + const c = Graph.addNode(mutable, "C") + Graph.addEdge(mutable, a, b, undefined) + Graph.addEdge(mutable, b, c, 42) + }) + + const graph2 = Graph.directed((mutable) => { + const a = Graph.addNode(mutable, "A") + const b = Graph.addNode(mutable, "B") + const c = Graph.addNode(mutable, "C") + Graph.addEdge(mutable, a, b, undefined) + Graph.addEdge(mutable, b, c, 42) + }) + + // Graphs with identical structure should have the same hash + expect(Hash.hash(graph1)).toBe(Hash.hash(graph2)) + + // Graph with different edge data should have different hash + const graph3 = Graph.directed((mutable) => { + const a = Graph.addNode(mutable, "A") + const b = Graph.addNode(mutable, "B") + const c = Graph.addNode(mutable, "C") + Graph.addEdge(mutable, a, b, 100) // Different data + Graph.addEdge(mutable, b, c, 42) + }) + + expect(Hash.hash(graph1)).not.toBe(Hash.hash(graph3)) + }) + + it("should correctly handle Equal.equals with graphs containing undefined edge data", () => { + const graph1 = Graph.directed((mutable) => { + const a = Graph.addNode(mutable, "A") + const b = Graph.addNode(mutable, "B") + Graph.addEdge(mutable, a, b, undefined) + }) + + const graph2 = Graph.directed((mutable) => { + const a = Graph.addNode(mutable, "A") + const b = Graph.addNode(mutable, "B") + Graph.addEdge(mutable, a, b, undefined) + }) + + const graph3 = Graph.directed((mutable) => { + const a = Graph.addNode(mutable, "A") + const b = Graph.addNode(mutable, "B") + Graph.addEdge(mutable, a, b, 42) + }) + + // Equal graphs with undefined edge data should be equal + expect(Equal.equals(graph1, graph2)).toBe(true) + + // Graphs with different edge data should not be equal + expect(Equal.equals(graph1, graph3)).toBe(false) + }) + }) + + describe("mixed undefined scenarios", () => { + it("should handle graphs with both undefined nodes and edges", () => { + const graph = Graph.directed((mutable) => { + const nodeA = Graph.addNode(mutable, undefined) + const nodeB = Graph.addNode(mutable, undefined) + Graph.addEdge(mutable, nodeA, nodeB, undefined) + }) + + expect(Graph.nodeCount(graph)).toBe(2) + expect(Graph.edgeCount(graph)).toBe(1) + expect(Graph.getNode(graph, 0)).toEqual(Option.some(undefined)) + expect(Graph.getEdge(graph, 0)).toEqual(Option.some({ source: 0, target: 1, data: undefined })) + }) + + it("should correctly handle graph operations with mixed undefined data", () => { + const graph = Graph.directed((mutable) => { + const a = Graph.addNode(mutable, undefined) + const b = Graph.addNode(mutable, "B") + const c = Graph.addNode(mutable, undefined) + Graph.addEdge(mutable, a, b, undefined) + Graph.addEdge(mutable, b, c, 42) + Graph.addEdge(mutable, c, a, undefined) + }) + + // Test neighbors + const neighborsOfA = Graph.neighbors(graph, 0) + const neighborsOfB = Graph.neighbors(graph, 1) + + expect(neighborsOfA).toEqual([1]) + expect(neighborsOfB).toEqual([2]) + + // Test filtering + const nodesWithUndefined = Graph.findNodes(graph, (data) => data === undefined) + const edgesWithUndefined = Graph.findEdges(graph, (data) => data === undefined) + + expect(nodesWithUndefined).toEqual([0, 2]) + expect(edgesWithUndefined).toEqual([0, 2]) + }) + }) + }) + + describe("beginMutation", () => { + it("should create a mutable graph from an immutable graph", () => { + const graph = Graph.directed() + const mutable = Graph.beginMutation(graph) + + expect(mutable.type).toBe("directed") + expect(Graph.nodeCount(mutable)).toBe(Graph.nodeCount(graph)) + expect(Graph.edgeCount(mutable)).toBe(Graph.edgeCount(graph)) + }) + }) + + describe("endMutation", () => { + it("should convert a mutable graph back to immutable", () => { + const graph = Graph.directed() + const mutable = Graph.beginMutation(graph) + const result = Graph.endMutation(mutable) + + expect(result.type).toBe("directed") + expect(Graph.nodeCount(result)).toBe(Graph.nodeCount(mutable)) + expect(Graph.edgeCount(result)).toBe(Graph.edgeCount(mutable)) + }) + }) + + describe("mutate", () => { + it("should create a new graph instance", () => { + const graph = Graph.directed() + + const result = Graph.mutate(graph, () => { + // No mutations performed + }) + + expect(result).not.toBe(graph) + expect(Equal.equals(result, graph)).toBe(true) // Structural equality + }) + + it("should handle empty mutation function", () => { + const graph = Graph.directed() + + const result = Graph.mutate(graph, () => { + // Do nothing + }) + + expect(Graph.nodeCount(result)).toBe(0) + expect(Graph.edgeCount(result)).toBe(0) + }) + }) + + describe("addNode", () => { + it("should add a node to a mutable graph and return its index", () => { + const graph = Graph.directed() + let nodeIndex: Graph.NodeIndex + + const result = Graph.mutate(graph, (mutable) => { + nodeIndex = Graph.addNode(mutable, "Node A") + }) + + expect(Graph.nodeCount(result)).toBe(1) + expect(Graph.getNode(result, nodeIndex!)).toEqual(Option.some("Node A")) + }) + }) + + describe("getNode", () => { + it("should return the node data for existing nodes", () => { + let nodeA: Graph.NodeIndex + let nodeB: Graph.NodeIndex + + const graph = Graph.directed((mutable) => { + nodeA = Graph.addNode(mutable, "Node A") + nodeB = Graph.addNode(mutable, "Node B") + }) + + expect(Graph.getNode(graph, nodeA!)).toEqual(Option.some("Node A")) + expect(Graph.getNode(graph, nodeB!)).toEqual(Option.some("Node B")) + }) + + it("should return None for non-existent nodes", () => { + const graph = Graph.directed((mutable) => { + Graph.addNode(mutable, "Node A") + }) + + const nonExistent = Graph.getNode(graph, 999) + expect(Option.isNone(nonExistent)).toBe(true) + }) + }) + + describe("hasNode", () => { + it("should return true for existing nodes", () => { + let nodeA: Graph.NodeIndex + let nodeB: Graph.NodeIndex + + const graph = Graph.directed((mutable) => { + nodeA = Graph.addNode(mutable, "Node A") + nodeB = Graph.addNode(mutable, "Node B") + }) + + expect(Graph.hasNode(graph, nodeA!)).toBe(true) + expect(Graph.hasNode(graph, nodeB!)).toBe(true) + }) + + it("should return false for non-existent nodes", () => { + const graph = Graph.directed((mutable) => { + Graph.addNode(mutable, "Node A") + }) + + expect(Graph.hasNode(graph, 999)).toBe(false) + expect(Graph.hasNode(graph, -1)).toBe(false) + }) + }) + + describe("nodeCount", () => { + it("should return 0 for empty graph", () => { + const graph = Graph.directed() + expect(Graph.nodeCount(graph)).toBe(0) + }) + + it("should return correct count after adding nodes", () => { + const graph = Graph.directed((mutable) => { + expect(Graph.nodeCount(mutable)).toBe(0) + Graph.addNode(mutable, "Node A") + expect(Graph.nodeCount(mutable)).toBe(1) + Graph.addNode(mutable, "Node B") + expect(Graph.nodeCount(mutable)).toBe(2) + Graph.addNode(mutable, "Node C") + expect(Graph.nodeCount(mutable)).toBe(3) + }) + + expect(Graph.nodeCount(graph)).toBe(3) + }) + }) + + describe("findNode", () => { + it("should find node by predicate", () => { + const graph = Graph.directed((mutable) => { + Graph.addNode(mutable, "Node A") + Graph.addNode(mutable, "Node B") + Graph.addNode(mutable, "Node C") + }) + + const result = Graph.findNode(graph, (data) => data === "Node B") + expect(Option.isSome(result)).toBe(true) + if (Option.isSome(result)) { + expect(result.value).toBe(1) + } + }) + + it("should return None when no node matches", () => { + const graph = Graph.directed((mutable) => { + Graph.addNode(mutable, "Node A") + Graph.addNode(mutable, "Node B") + }) + + const result = Graph.findNode(graph, (data) => data === "Node C") + expect(Option.isNone(result)).toBe(true) + }) + + it("should find first matching node when multiple match", () => { + const graph = Graph.directed((mutable) => { + Graph.addNode(mutable, "Start A") + Graph.addNode(mutable, "Start B") + Graph.addNode(mutable, "Start C") + }) + + const result = Graph.findNode(graph, (data) => data.startsWith("Start")) + expect(Option.isSome(result)).toBe(true) + if (Option.isSome(result)) { + expect(result.value).toBe(0) // First matching node + } + }) + }) + + describe("findNodes", () => { + it("should find all matching nodes", () => { + const graph = Graph.directed((mutable) => { + Graph.addNode(mutable, "Start A") + Graph.addNode(mutable, "Node B") + Graph.addNode(mutable, "Start C") + Graph.addNode(mutable, "Start D") + }) + + const result = Graph.findNodes(graph, (data) => data.startsWith("Start")) + expect(result).toEqual([0, 2, 3]) + }) + }) + + describe("findEdge", () => { + it("should find edge by predicate", () => { + const graph = Graph.directed((mutable) => { + const nodeA = Graph.addNode(mutable, "Node A") + const nodeB = Graph.addNode(mutable, "Node B") + const nodeC = Graph.addNode(mutable, "Node C") + Graph.addEdge(mutable, nodeA, nodeB, 10) + Graph.addEdge(mutable, nodeB, nodeC, 20) + }) + + const result = Graph.findEdge(graph, (data) => data === 20) + expect(Option.isSome(result)).toBe(true) + if (Option.isSome(result)) { + expect(result.value).toBe(1) + } + }) + + it("should return None when no edge matches", () => { + const graph = Graph.directed((mutable) => { + const nodeA = Graph.addNode(mutable, "Node A") + const nodeB = Graph.addNode(mutable, "Node B") + Graph.addEdge(mutable, nodeA, nodeB, 10) + }) + + const result = Graph.findEdge(graph, (data) => data === 99) + expect(Option.isNone(result)).toBe(true) + }) + + it("should find first matching edge when multiple match", () => { + const graph = Graph.directed((mutable) => { + const nodeA = Graph.addNode(mutable, "Node A") + const nodeB = Graph.addNode(mutable, "Node B") + const nodeC = Graph.addNode(mutable, "Node C") + Graph.addEdge(mutable, nodeA, nodeB, 15) + Graph.addEdge(mutable, nodeB, nodeC, 25) + Graph.addEdge(mutable, nodeC, nodeA, 35) + }) + + const result = Graph.findEdge(graph, (data) => data > 20) + expect(Option.isSome(result)).toBe(true) + if (Option.isSome(result)) { + expect(result.value).toBe(1) // First matching edge + } + }) + }) + + describe("findEdges", () => { + it("should find all matching edges", () => { + const graph = Graph.directed((mutable) => { + const nodeA = Graph.addNode(mutable, "Node A") + const nodeB = Graph.addNode(mutable, "Node B") + const nodeC = Graph.addNode(mutable, "Node C") + Graph.addEdge(mutable, nodeA, nodeB, 10) + Graph.addEdge(mutable, nodeB, nodeC, 20) + Graph.addEdge(mutable, nodeC, nodeA, 30) + Graph.addEdge(mutable, nodeA, nodeC, 25) + }) + + const result = Graph.findEdges(graph, (data) => data >= 20) + expect(result).toEqual([1, 2, 3]) + }) + }) + + describe("updateNode", () => { + it("should update node data", () => { + const updated = Graph.directed((mutable) => { + Graph.addNode(mutable, "Node A") + Graph.addNode(mutable, "Node B") + Graph.updateNode(mutable, 0, (data) => data.toUpperCase()) + }) + + const nodeData = Graph.getNode(updated, 0) + expect(Option.isSome(nodeData)).toBe(true) + if (Option.isSome(nodeData)) { + expect(nodeData.value).toBe("NODE A") + } + }) + + it("should do nothing if node doesn't exist", () => { + let nodeA: Graph.NodeIndex + + const graph = Graph.directed((mutable) => { + nodeA = Graph.addNode(mutable, "Node A") + Graph.updateNode(mutable, 999, (data) => data.toUpperCase()) + }) + + // Original node should be unchanged + const nodeData = Graph.getNode(graph, nodeA!) + expect(Option.isSome(nodeData)).toBe(true) + if (Option.isSome(nodeData)) { + expect(nodeData.value).toBe("Node A") + } + }) + }) + + describe("updateEdge", () => { + it("should update edge data", () => { + const result = Graph.mutate(Graph.directed(), (mutable) => { + const nodeA = Graph.addNode(mutable, "Node A") + const nodeB = Graph.addNode(mutable, "Node B") + const edgeIndex = Graph.addEdge(mutable, nodeA, nodeB, 10) + Graph.updateEdge(mutable, edgeIndex, (data) => data * 2) + }) + + const edge = Graph.getEdge(result, 0) + expect(Option.isSome(edge)).toBe(true) + if (Option.isSome(edge)) { + expect(edge.value.source).toBe(0) + expect(edge.value.target).toBe(1) + expect(edge.value.data).toBe(20) + } + }) + + it("should do nothing if edge doesn't exist", () => { + Graph.mutate(Graph.directed(), (mutable) => { + const nodeA = Graph.addNode(mutable, "Node A") + const nodeB = Graph.addNode(mutable, "Node B") + const edgeIndex = Graph.addEdge(mutable, nodeA, nodeB, 10) + + // Try to update non-existent edge + Graph.updateEdge(mutable, 999, (data) => data * 2) + + // Original edge should be unchanged + const edge = Graph.getEdge(mutable, edgeIndex) + expect(Option.isSome(edge)).toBe(true) + if (Option.isSome(edge)) { + expect(edge.value.data).toBe(10) + } + }) + }) + }) + + describe("mapNodes", () => { + it("should transform all node data", () => { + let nodeA: Graph.NodeIndex + let nodeB: Graph.NodeIndex + let nodeC: Graph.NodeIndex + + const graph = Graph.directed((mutable) => { + nodeA = Graph.addNode(mutable, "node a") + nodeB = Graph.addNode(mutable, "node b") + nodeC = Graph.addNode(mutable, "node c") + Graph.mapNodes(mutable, (data) => data.toUpperCase()) + }) + + expect(Graph.getNode(graph, nodeA!)).toEqual(Option.some("NODE A")) + expect(Graph.getNode(graph, nodeB!)).toEqual(Option.some("NODE B")) + expect(Graph.getNode(graph, nodeC!)).toEqual(Option.some("NODE C")) + }) + + it("should apply transformation to all nodes", () => { + let firstNode: Graph.NodeIndex + let secondNode: Graph.NodeIndex + let thirdNode: Graph.NodeIndex + + const graph = Graph.directed((mutable) => { + firstNode = Graph.addNode(mutable, "first") + secondNode = Graph.addNode(mutable, "second") + thirdNode = Graph.addNode(mutable, "third") + Graph.mapNodes(mutable, (data) => data + " (transformed)") + }) + + const node0 = Graph.getNode(graph, firstNode!) + const node1 = Graph.getNode(graph, secondNode!) + const node2 = Graph.getNode(graph, thirdNode!) + + expect(Option.isSome(node0)).toBe(true) + expect(Option.isSome(node1)).toBe(true) + expect(Option.isSome(node2)).toBe(true) + + if (Option.isSome(node0) && Option.isSome(node1) && Option.isSome(node2)) { + expect(node0.value).toBe("first (transformed)") + expect(node1.value).toBe("second (transformed)") + expect(node2.value).toBe("third (transformed)") + } + }) + + it("should modify graph in place during construction", () => { + let originalNode: Graph.NodeIndex + + const graph = Graph.directed((mutable) => { + originalNode = Graph.addNode(mutable, "original") + // Before transformation + const beforeData = Graph.getNode(mutable, originalNode!) + expect(Option.isSome(beforeData)).toBe(true) + if (Option.isSome(beforeData)) { + expect(beforeData.value).toBe("original") + } + + // Apply transformation + Graph.mapNodes(mutable, (data) => data.toUpperCase()) + }) + + // After transformation + const afterData = Graph.getNode(graph, originalNode!) + expect(Option.isSome(afterData)).toBe(true) + if (Option.isSome(afterData)) { + expect(afterData.value).toBe("ORIGINAL") + } + }) + }) + + describe("mapEdges", () => { + it("should transform all edge data", () => { + let edgeAB: Graph.EdgeIndex + let edgeBC: Graph.EdgeIndex + let edgeCA: Graph.EdgeIndex + + const graph = Graph.directed((mutable) => { + const a = Graph.addNode(mutable, "A") + const b = Graph.addNode(mutable, "B") + const c = Graph.addNode(mutable, "C") + edgeAB = Graph.addEdge(mutable, a, b, 10) + edgeBC = Graph.addEdge(mutable, b, c, 20) + edgeCA = Graph.addEdge(mutable, c, a, 30) + Graph.mapEdges(mutable, (data) => data * 2) + }) + + const edge0 = Graph.getEdge(graph, edgeAB!) + const edge1 = Graph.getEdge(graph, edgeBC!) + const edge2 = Graph.getEdge(graph, edgeCA!) + + expect(Option.isSome(edge0)).toBe(true) + expect(Option.isSome(edge1)).toBe(true) + expect(Option.isSome(edge2)).toBe(true) + + if (Option.isSome(edge0) && Option.isSome(edge1) && Option.isSome(edge2)) { + expect(edge0.value.data).toBe(20) + expect(edge1.value.data).toBe(40) + expect(edge2.value.data).toBe(60) + } + }) + + it("should modify graph in place during construction", () => { + let edgeAB: Graph.EdgeIndex + + const graph = Graph.directed((mutable) => { + const a = Graph.addNode(mutable, "A") + const b = Graph.addNode(mutable, "B") + edgeAB = Graph.addEdge(mutable, a, b, 10) + + // Before transformation + const beforeData = Graph.getEdge(mutable, edgeAB!) + expect(Option.isSome(beforeData)).toBe(true) + if (Option.isSome(beforeData)) { + expect(beforeData.value.data).toBe(10) + } + + // Apply transformation + Graph.mapEdges(mutable, (data) => data * 5) + }) + + // After transformation + const afterData = Graph.getEdge(graph, edgeAB!) + expect(Option.isSome(afterData)).toBe(true) + if (Option.isSome(afterData)) { + expect(afterData.value.data).toBe(50) + } + }) + }) + + describe("reverse", () => { + it("should reverse all edge directions", () => { + let nodeA: Graph.NodeIndex + let nodeB: Graph.NodeIndex + let nodeC: Graph.NodeIndex + let edgeAB: Graph.EdgeIndex + let edgeBC: Graph.EdgeIndex + let edgeCA: Graph.EdgeIndex + + const graph = Graph.directed((mutable) => { + nodeA = Graph.addNode(mutable, "A") + nodeB = Graph.addNode(mutable, "B") + nodeC = Graph.addNode(mutable, "C") + edgeAB = Graph.addEdge(mutable, nodeA, nodeB, 1) // A -> B + edgeBC = Graph.addEdge(mutable, nodeB, nodeC, 2) // B -> C + edgeCA = Graph.addEdge(mutable, nodeC, nodeA, 3) // C -> A + Graph.reverse(mutable) // Now B -> A, C -> B, A -> C + }) + + const edge0 = Graph.getEdge(graph, edgeAB!) + const edge1 = Graph.getEdge(graph, edgeBC!) + const edge2 = Graph.getEdge(graph, edgeCA!) + + expect(Option.isSome(edge0)).toBe(true) + expect(Option.isSome(edge1)).toBe(true) + expect(Option.isSome(edge2)).toBe(true) + + if (Option.isSome(edge0) && Option.isSome(edge1) && Option.isSome(edge2)) { + // Edge 0: was A -> B, now B -> A + expect(edge0.value.source).toBe(nodeB!) + expect(edge0.value.target).toBe(nodeA!) + expect(edge0.value.data).toBe(1) + + // Edge 1: was B -> C, now C -> B + expect(edge1.value.source).toBe(nodeC!) + expect(edge1.value.target).toBe(nodeB!) + expect(edge1.value.data).toBe(2) + + // Edge 2: was C -> A, now A -> C + expect(edge2.value.source).toBe(nodeA!) + expect(edge2.value.target).toBe(nodeC!) + expect(edge2.value.data).toBe(3) + } + }) + + it("should update adjacency lists correctly", () => { + const graph = Graph.directed((mutable) => { + const a = Graph.addNode(mutable, "A") + const b = Graph.addNode(mutable, "B") + const c = Graph.addNode(mutable, "C") + Graph.addEdge(mutable, a, b, 1) // A -> B + Graph.addEdge(mutable, a, c, 2) // A -> C + Graph.reverse(mutable) // Now B -> A, C -> A + }) + + // After reversal: + // - Node A should have no outgoing edges + // - Node B should have edge to A + // - Node C should have edge to A + + const neighborsA = Graph.neighbors(graph, 0) + const neighborsB = Graph.neighbors(graph, 1) + const neighborsC = Graph.neighbors(graph, 2) + + expect(Array.from(neighborsA)).toEqual([]) // A has no outgoing edges + expect(Array.from(neighborsB)).toEqual([0]) // B -> A + expect(Array.from(neighborsC)).toEqual([0]) // C -> A + }) + }) + + describe("filterMapNodes", () => { + it("should filter and transform nodes", () => { + const graph = Graph.directed((mutable) => { + Graph.addNode(mutable, "active") + Graph.addNode(mutable, "inactive") + Graph.addNode(mutable, "active") + Graph.addNode(mutable, "pending") + + // Keep only "active" nodes and transform to uppercase + Graph.filterMapNodes(mutable, (data) => data === "active" ? Option.some(data.toUpperCase()) : Option.none()) + }) + + // Should only have 2 nodes remaining (the "active" ones) + expect(Graph.nodeCount(graph)).toBe(2) + + // Check the remaining nodes have been transformed + const nodeData0 = Graph.getNode(graph, 0) + const nodeData2 = Graph.getNode(graph, 2) + + expect(Option.isSome(nodeData0)).toBe(true) + expect(Option.isSome(nodeData2)).toBe(true) + + if (Option.isSome(nodeData0) && Option.isSome(nodeData2)) { + expect(nodeData0.value).toBe("ACTIVE") + expect(nodeData2.value).toBe("ACTIVE") + } + + // Filtered out nodes should not exist + expect(Option.isNone(Graph.getNode(graph, 1))).toBe(true) + expect(Option.isNone(Graph.getNode(graph, 3))).toBe(true) + }) + + it("should remove edges connected to filtered nodes", () => { + const graph = Graph.directed((mutable) => { + const a = Graph.addNode(mutable, "keep") + const b = Graph.addNode(mutable, "remove") + const c = Graph.addNode(mutable, "keep") + + Graph.addEdge(mutable, a, b, 1) // keep -> remove + Graph.addEdge(mutable, b, c, 2) // remove -> keep + Graph.addEdge(mutable, a, c, 3) // keep -> keep + + // Filter out "remove" nodes + Graph.filterMapNodes(mutable, (data) => data === "keep" ? Option.some(data) : Option.none()) + }) + + // Should have 2 nodes and 1 edge remaining + expect(Graph.nodeCount(graph)).toBe(2) + expect(Graph.edgeCount(graph)).toBe(1) + + // Only the keep -> keep edge should remain + const remainingEdge = Graph.getEdge(graph, 2) + expect(Option.isSome(remainingEdge)).toBe(true) + if (Option.isSome(remainingEdge)) { + expect(remainingEdge.value.source).toBe(0) + expect(remainingEdge.value.target).toBe(2) + expect(remainingEdge.value.data).toBe(3) + } + + // Edges involving removed node should be gone + expect(Option.isNone(Graph.getEdge(graph, 0))).toBe(true) + expect(Option.isNone(Graph.getEdge(graph, 1))).toBe(true) + }) + + it("should handle transformation without filtering", () => { + const graph = Graph.directed((mutable) => { + Graph.addNode(mutable, 1) + Graph.addNode(mutable, 2) + Graph.addNode(mutable, 3) + + // Transform all nodes by doubling them + Graph.filterMapNodes(mutable, (data) => Option.some(data * 2)) + }) + + expect(Graph.nodeCount(graph)).toBe(3) + + const node0 = Graph.getNode(graph, 0) + const node1 = Graph.getNode(graph, 1) + const node2 = Graph.getNode(graph, 2) + + expect(Option.isSome(node0)).toBe(true) + expect(Option.isSome(node1)).toBe(true) + expect(Option.isSome(node2)).toBe(true) + + if (Option.isSome(node0) && Option.isSome(node1) && Option.isSome(node2)) { + expect(node0.value).toBe(2) + expect(node1.value).toBe(4) + expect(node2.value).toBe(6) + } + }) + + it("should handle filtering without transformation", () => { + const graph = Graph.directed((mutable) => { + Graph.addNode(mutable, 1) + Graph.addNode(mutable, 2) + Graph.addNode(mutable, 3) + Graph.addNode(mutable, 4) + + // Keep only even numbers + Graph.filterMapNodes(mutable, (data) => data % 2 === 0 ? Option.some(data) : Option.none()) + }) + + expect(Graph.nodeCount(graph)).toBe(2) + + const node1 = Graph.getNode(graph, 1) + const node3 = Graph.getNode(graph, 3) + + expect(Option.isSome(node1)).toBe(true) + expect(Option.isSome(node3)).toBe(true) + + if (Option.isSome(node1) && Option.isSome(node3)) { + expect(node1.value).toBe(2) + expect(node3.value).toBe(4) + } + + // Odd numbers should be removed + expect(Option.isNone(Graph.getNode(graph, 0))).toBe(true) + expect(Option.isNone(Graph.getNode(graph, 2))).toBe(true) + }) + }) + + describe("filterMapEdges", () => { + it("should filter and transform edges", () => { + const graph = Graph.directed((mutable) => { + const a = Graph.addNode(mutable, "A") + const b = Graph.addNode(mutable, "B") + const c = Graph.addNode(mutable, "C") + Graph.addEdge(mutable, a, b, 5) // Remove (< 10) + Graph.addEdge(mutable, b, c, 15) // Keep and double (30) + Graph.addEdge(mutable, c, a, 25) // Keep and double (50) + + // Keep only edges with weight >= 10 and double their weight + Graph.filterMapEdges(mutable, (data) => data >= 10 ? Option.some(data * 2) : Option.none()) + }) + + // Should have 2 edges remaining + expect(Graph.edgeCount(graph)).toBe(2) + expect(Graph.nodeCount(graph)).toBe(3) // All nodes should remain + + // Check that remaining edges have been transformed + const edge1 = Graph.getEdge(graph, 1) + const edge2 = Graph.getEdge(graph, 2) + + expect(Option.isSome(edge1)).toBe(true) + expect(Option.isSome(edge2)).toBe(true) + + if (Option.isSome(edge1) && Option.isSome(edge2)) { + expect(edge1.value.data).toBe(30) // 15 * 2 + expect(edge2.value.data).toBe(50) // 25 * 2 + } + + // Filtered out edge should not exist + expect(Option.isNone(Graph.getEdge(graph, 0))).toBe(true) + }) + + it("should update adjacency lists when removing edges", () => { + const graph = Graph.directed((mutable) => { + const a = Graph.addNode(mutable, "A") + const b = Graph.addNode(mutable, "B") + const c = Graph.addNode(mutable, "C") + + Graph.addEdge(mutable, a, b, 1) // Keep + Graph.addEdge(mutable, a, c, 2) // Remove + Graph.addEdge(mutable, b, c, 3) // Keep + + // Keep only odd numbers + Graph.filterMapEdges(mutable, (data) => data % 2 === 1 ? Option.some(data) : Option.none()) + }) + + // Should have 2 edges remaining (1 and 3) + expect(Graph.edgeCount(graph)).toBe(2) + + // Check adjacency: A should only connect to B now + const neighborsA = Array.from(Graph.neighbors(graph, 0)) + expect(neighborsA).toEqual([1]) // A -> B only + + // Check that B still connects to C + const neighborsB = Array.from(Graph.neighbors(graph, 1)) + expect(neighborsB).toEqual([2]) // B -> C + + // Check that C has no outgoing edges + const neighborsC = Array.from(Graph.neighbors(graph, 2)) + expect(neighborsC).toEqual([]) // C has no outgoing edges + }) + + it("should handle transformation without filtering", () => { + const graph = Graph.directed((mutable) => { + const a = Graph.addNode(mutable, "A") + const b = Graph.addNode(mutable, "B") + const c = Graph.addNode(mutable, "C") + Graph.addEdge(mutable, a, b, 10) + Graph.addEdge(mutable, b, c, 20) + Graph.addEdge(mutable, c, a, 30) + + // Transform all edges by adding 100 + Graph.filterMapEdges(mutable, (data) => Option.some(data + 100)) + }) + + expect(Graph.edgeCount(graph)).toBe(3) + + const edge0 = Graph.getEdge(graph, 0) + const edge1 = Graph.getEdge(graph, 1) + const edge2 = Graph.getEdge(graph, 2) + + expect(Option.isSome(edge0)).toBe(true) + expect(Option.isSome(edge1)).toBe(true) + expect(Option.isSome(edge2)).toBe(true) + + if (Option.isSome(edge0) && Option.isSome(edge1) && Option.isSome(edge2)) { + expect(edge0.value.data).toBe(110) + expect(edge1.value.data).toBe(120) + expect(edge2.value.data).toBe(130) + } + }) + + it("should handle filtering without transformation", () => { + const graph = Graph.directed((mutable) => { + const a = Graph.addNode(mutable, "A") + const b = Graph.addNode(mutable, "B") + const c = Graph.addNode(mutable, "C") + Graph.addEdge(mutable, a, b, { weight: 10, type: "primary" }) + Graph.addEdge(mutable, b, c, { weight: 20, type: "secondary" }) + Graph.addEdge(mutable, c, a, { weight: 30, type: "primary" }) + + // Keep only "primary" edges + Graph.filterMapEdges(mutable, (data) => data.type === "primary" ? Option.some(data) : Option.none()) + }) + + expect(Graph.edgeCount(graph)).toBe(2) + + const edge0 = Graph.getEdge(graph, 0) + const edge2 = Graph.getEdge(graph, 2) + + expect(Option.isSome(edge0)).toBe(true) + expect(Option.isSome(edge2)).toBe(true) + + if (Option.isSome(edge0) && Option.isSome(edge2)) { + expect(edge0.value.data.type).toBe("primary") + expect(edge2.value.data.type).toBe("primary") + } + + // Secondary edge should be removed + expect(Option.isNone(Graph.getEdge(graph, 1))).toBe(true) + }) + }) + + describe("filterNodes", () => { + it("should filter nodes by predicate", () => { + let activeNode1: Graph.NodeIndex + let inactiveNode: Graph.NodeIndex + let activeNode2: Graph.NodeIndex + let pendingNode: Graph.NodeIndex + + const graph = Graph.directed((mutable) => { + activeNode1 = Graph.addNode(mutable, "active") + inactiveNode = Graph.addNode(mutable, "inactive") + activeNode2 = Graph.addNode(mutable, "active") + pendingNode = Graph.addNode(mutable, "pending") + + // Keep only "active" nodes + Graph.filterNodes(mutable, (data) => data === "active") + }) + + expect(Graph.nodeCount(graph)).toBe(2) + + const node0 = Graph.getNode(graph, activeNode1!) + const node2 = Graph.getNode(graph, activeNode2!) + + expect(Option.isSome(node0)).toBe(true) + expect(Option.isSome(node2)).toBe(true) + + if (Option.isSome(node0) && Option.isSome(node2)) { + expect(node0.value).toBe("active") + expect(node2.value).toBe("active") + } + + // Filtered out nodes should be removed + expect(Option.isNone(Graph.getNode(graph, inactiveNode!))).toBe(true) // "inactive" + expect(Option.isNone(Graph.getNode(graph, pendingNode!))).toBe(true) // "pending" + }) + + it("should remove connected edges when filtering nodes", () => { + let edgeAB: Graph.EdgeIndex + let edgeBC: Graph.EdgeIndex + let edgeAC: Graph.EdgeIndex + + const graph = Graph.directed((mutable) => { + const a = Graph.addNode(mutable, "keep") + const b = Graph.addNode(mutable, "remove") + const c = Graph.addNode(mutable, "keep") + + edgeAB = Graph.addEdge(mutable, a, b, "A-B") + edgeBC = Graph.addEdge(mutable, b, c, "B-C") + edgeAC = Graph.addEdge(mutable, a, c, "A-C") + + // Remove node "remove" + Graph.filterNodes(mutable, (data) => data === "keep") + }) + + expect(Graph.nodeCount(graph)).toBe(2) // Only "keep" nodes remain + expect(Graph.edgeCount(graph)).toBe(1) // Only A-C edge remains + + // Check remaining edge + const edge2 = Graph.getEdge(graph, edgeAC!) + expect(Option.isSome(edge2)).toBe(true) + if (Option.isSome(edge2)) { + expect(edge2.value.data).toBe("A-C") + } + + // Check removed edges + expect(Option.isNone(Graph.getEdge(graph, edgeAB!))).toBe(true) // A-B removed + expect(Option.isNone(Graph.getEdge(graph, edgeBC!))).toBe(true) // B-C removed + }) + }) + + describe("filterEdges", () => { + it("should filter edges by predicate", () => { + let edgeAB: Graph.EdgeIndex + let edgeBC: Graph.EdgeIndex + let edgeCA: Graph.EdgeIndex + + const graph = Graph.directed((mutable) => { + const a = Graph.addNode(mutable, "A") + const b = Graph.addNode(mutable, "B") + const c = Graph.addNode(mutable, "C") + + edgeAB = Graph.addEdge(mutable, a, b, 5) + edgeBC = Graph.addEdge(mutable, b, c, 15) + edgeCA = Graph.addEdge(mutable, c, a, 25) + + // Keep only edges with weight >= 10 + Graph.filterEdges(mutable, (data) => data >= 10) + }) + + expect(Graph.nodeCount(graph)).toBe(3) // All nodes remain + expect(Graph.edgeCount(graph)).toBe(2) // Edge with weight 5 removed + + const edge1 = Graph.getEdge(graph, edgeBC!) + const edge2 = Graph.getEdge(graph, edgeCA!) + + expect(Option.isSome(edge1)).toBe(true) + expect(Option.isSome(edge2)).toBe(true) + + if (Option.isSome(edge1) && Option.isSome(edge2)) { + expect(edge1.value.data).toBe(15) + expect(edge2.value.data).toBe(25) + } + + // Edge with weight 5 should be removed + expect(Option.isNone(Graph.getEdge(graph, edgeAB!))).toBe(true) + }) + + it("should update adjacency lists when filtering edges", () => { + let nodeA: Graph.NodeIndex + let nodeB: Graph.NodeIndex + let nodeC: Graph.NodeIndex + + const graph = Graph.directed((mutable) => { + nodeA = Graph.addNode(mutable, "A") + nodeB = Graph.addNode(mutable, "B") + nodeC = Graph.addNode(mutable, "C") + + Graph.addEdge(mutable, nodeA, nodeB, "primary") + Graph.addEdge(mutable, nodeA, nodeC, "secondary") + Graph.addEdge(mutable, nodeB, nodeC, "primary") + + // Keep only "primary" edges + Graph.filterEdges(mutable, (data) => data === "primary") + }) + + expect(Graph.edgeCount(graph)).toBe(2) + + // Check adjacency - A should only connect to B now + const neighborsA = Array.from(Graph.neighbors(graph, nodeA!)) + expect(neighborsA).toEqual([nodeB!]) // A -> B only + + const neighborsB = Array.from(Graph.neighbors(graph, nodeB!)) + expect(neighborsB).toEqual([nodeC!]) // B -> C + + const neighborsC = Array.from(Graph.neighbors(graph, nodeC!)) + expect(neighborsC).toEqual([]) // C has no outgoing edges + }) + }) + + describe("addEdge", () => { + it("should add an edge between two existing nodes", () => { + let edgeIndex: Graph.EdgeIndex + + const result = Graph.directed((mutable) => { + const nodeA = Graph.addNode(mutable, "Node A") + const nodeB = Graph.addNode(mutable, "Node B") + edgeIndex = Graph.addEdge(mutable, nodeA, nodeB, 42) + }) + + expect(edgeIndex!).toBe(0) + expect(Graph.edgeCount(result)).toBe(1) + }) + + it("should add multiple edges with sequential indices", () => { + let edgeA: Graph.EdgeIndex + let edgeB: Graph.EdgeIndex + let edgeC: Graph.EdgeIndex + + const result = Graph.directed((mutable) => { + const nodeA = Graph.addNode(mutable, "Node A") + const nodeB = Graph.addNode(mutable, "Node B") + const nodeC = Graph.addNode(mutable, "Node C") + + edgeA = Graph.addEdge(mutable, nodeA, nodeB, 10) + edgeB = Graph.addEdge(mutable, nodeB, nodeC, 20) + edgeC = Graph.addEdge(mutable, nodeA, nodeC, 30) + }) + + expect(edgeA!).toBe(0) + expect(edgeB!).toBe(1) + expect(edgeC!).toBe(2) + expect(Graph.edgeCount(result)).toBe(3) + }) + + it("should throw error when source node doesn't exist", () => { + expect(() => { + Graph.directed((mutable) => { + const nodeB = Graph.addNode(mutable, "Node B") + const nonExistentNode = 999 + Graph.addEdge(mutable, nonExistentNode, nodeB, 42) + }) + }).toThrow("Node 999 does not exist") + }) + + it("should throw error when target node doesn't exist", () => { + expect(() => { + Graph.directed((mutable) => { + const nodeA = Graph.addNode(mutable, "Node A") + const nonExistentNode = 999 + Graph.addEdge(mutable, nodeA, nonExistentNode, 42) + }) + }).toThrow("Node 999 does not exist") + }) + }) + + describe("removeNode", () => { + it("should remove a node and all its incident edges", () => { + const result = Graph.directed((mutable) => { + const nodeA = Graph.addNode(mutable, "Node A") + const nodeB = Graph.addNode(mutable, "Node B") + const nodeC = Graph.addNode(mutable, "Node C") + + Graph.addEdge(mutable, nodeA, nodeB, 10) + Graph.addEdge(mutable, nodeB, nodeC, 20) + Graph.addEdge(mutable, nodeC, nodeA, 30) + + expect(Graph.nodeCount(mutable)).toBe(3) + expect(Graph.edgeCount(mutable)).toBe(3) + + // Remove nodeB which has 2 incident edges + Graph.removeNode(mutable, nodeB) + + expect(Graph.nodeCount(mutable)).toBe(2) + expect(Graph.edgeCount(mutable)).toBe(1) // Only nodeC -> nodeA edge remains + }) + + expect(Graph.nodeCount(result)).toBe(2) + expect(Graph.edgeCount(result)).toBe(1) + }) + + it("should handle removing non-existent node gracefully", () => { + const result = Graph.directed((mutable) => { + Graph.addNode(mutable, "Node A") // Just need one node for count + const nonExistentNode = 999 + + expect(Graph.nodeCount(mutable)).toBe(1) + Graph.removeNode(mutable, nonExistentNode) // Should not throw + expect(Graph.nodeCount(mutable)).toBe(1) // Should remain unchanged + }) + + expect(Graph.nodeCount(result)).toBe(1) + }) + + it("should handle isolated node removal", () => { + const result = Graph.directed((mutable) => { + Graph.addNode(mutable, "Node A") // Keep for final count + const nodeB = Graph.addNode(mutable, "Node B") // Isolated node to remove + + expect(Graph.nodeCount(mutable)).toBe(2) + expect(Graph.edgeCount(mutable)).toBe(0) + + Graph.removeNode(mutable, nodeB) + + expect(Graph.nodeCount(mutable)).toBe(1) + expect(Graph.edgeCount(mutable)).toBe(0) + }) + + expect(Graph.nodeCount(result)).toBe(1) + }) + }) + + describe("removeEdge", () => { + it("should remove an edge between two nodes", () => { + let edgeIndex: Graph.EdgeIndex + + const result = Graph.directed((mutable) => { + const nodeA = Graph.addNode(mutable, "Node A") + const nodeB = Graph.addNode(mutable, "Node B") + edgeIndex = Graph.addEdge(mutable, nodeA, nodeB, 42) + + expect(Graph.edgeCount(mutable)).toBe(1) + + Graph.removeEdge(mutable, edgeIndex) + + expect(Graph.edgeCount(mutable)).toBe(0) + }) + + expect(Graph.edgeCount(result)).toBe(0) + }) + + it("should handle removing non-existent edge gracefully", () => { + const result = Graph.directed((mutable) => { + const nodeA = Graph.addNode(mutable, "Node A") + const nodeB = Graph.addNode(mutable, "Node B") + Graph.addEdge(mutable, nodeA, nodeB, 42) + + const nonExistentEdge = 999 + + expect(Graph.edgeCount(mutable)).toBe(1) + Graph.removeEdge(mutable, nonExistentEdge) // Should not throw + expect(Graph.edgeCount(mutable)).toBe(1) // Should remain unchanged + }) + + expect(Graph.edgeCount(result)).toBe(1) + }) + + it("should handle multiple edges between same nodes", () => { + const result = Graph.directed((mutable) => { + const nodeA = Graph.addNode(mutable, "Node A") + const nodeB = Graph.addNode(mutable, "Node B") + + const edge1 = Graph.addEdge(mutable, nodeA, nodeB, 10) + const edge2 = Graph.addEdge(mutable, nodeA, nodeB, 20) + + expect(Graph.edgeCount(mutable)).toBe(2) + + Graph.removeEdge(mutable, edge1) + + expect(Graph.edgeCount(mutable)).toBe(1) + + // Verify second edge still exists + const edge2Data = mutable.edges.get(edge2) + expect(edge2Data).toBeDefined() + }) + + expect(Graph.edgeCount(result)).toBe(1) + }) + }) + + describe("Edge query operations", () => { + describe("getEdge", () => { + it("should return edge data for existing edge", () => { + const graph = Graph.directed((mutable) => { + const nodeA = Graph.addNode(mutable, "Node A") + const nodeB = Graph.addNode(mutable, "Node B") + Graph.addEdge(mutable, nodeA, nodeB, 42) + }) + + const edgeIndex = 0 + const edge = Graph.getEdge(graph, edgeIndex) + + expect(Option.isSome(edge)).toBe(true) + if (Option.isSome(edge)) { + expect(edge.value.source).toBe(0) + expect(edge.value.target).toBe(1) + expect(edge.value.data).toBe(42) + } + }) + + it("should return None for non-existent edge", () => { + const graph = Graph.directed() + const edgeIndex = 999 + const edge = Graph.getEdge(graph, edgeIndex) + + expect(Option.isNone(edge)).toBe(true) + }) + }) + + describe("hasEdge", () => { + it("should return true for existing edge", () => { + const graph = Graph.directed((mutable) => { + const nodeA = Graph.addNode(mutable, "Node A") + const nodeB = Graph.addNode(mutable, "Node B") + Graph.addEdge(mutable, nodeA, nodeB, 42) + }) + + const nodeA = 0 + const nodeB = 1 + + expect(Graph.hasEdge(graph, nodeA, nodeB)).toBe(true) + }) + + it("should return false for non-existent edge", () => { + const graph = Graph.directed((mutable) => { + const nodeA = Graph.addNode(mutable, "Node A") + const nodeB = Graph.addNode(mutable, "Node B") + Graph.addNode(mutable, "Node C") + Graph.addEdge(mutable, nodeA, nodeB, 42) + }) + + const nodeA = 0 + const nodeC = 2 + + expect(Graph.hasEdge(graph, nodeA, nodeC)).toBe(false) + }) + + it("should return false for non-existent source node", () => { + const graph = Graph.directed() + const nodeA = 0 + const nodeB = 1 + + expect(Graph.hasEdge(graph, nodeA, nodeB)).toBe(false) + }) + }) + + describe("edgeCount", () => { + it("should return 0 for empty graph", () => { + const graph = Graph.directed() + expect(Graph.edgeCount(graph)).toBe(0) + }) + + it("should return correct edge count", () => { + const graph = Graph.directed((mutable) => { + const nodeA = Graph.addNode(mutable, "Node A") + const nodeB = Graph.addNode(mutable, "Node B") + const nodeC = Graph.addNode(mutable, "Node C") + Graph.addEdge(mutable, nodeA, nodeB, 1) + Graph.addEdge(mutable, nodeB, nodeC, 2) + Graph.addEdge(mutable, nodeC, nodeA, 3) + }) + + expect(Graph.edgeCount(graph)).toBe(3) + }) + }) + + describe("neighbors", () => { + it("should return correct neighbors for directed graph", () => { + const graph = Graph.directed((mutable) => { + const nodeA = Graph.addNode(mutable, "Node A") + const nodeB = Graph.addNode(mutable, "Node B") + const nodeC = Graph.addNode(mutable, "Node C") + Graph.addEdge(mutable, nodeA, nodeB, 1) + Graph.addEdge(mutable, nodeA, nodeC, 2) + }) + + const nodeA = 0 + const nodeB = 1 + const nodeC = 2 + + const neighborsA = Graph.neighbors(graph, nodeA) + expect(neighborsA).toContain(nodeB) + expect(neighborsA).toContain(nodeC) + expect(neighborsA).toHaveLength(2) + + const neighborsB = Graph.neighbors(graph, nodeB) + expect(neighborsB).toEqual([]) + }) + }) + + describe("neighbors with undirected graphs", () => { + it("should return correct neighbors for single edge", () => { + const graph = Graph.undirected((mutable) => { + Graph.addNode(mutable, 0) + Graph.addNode(mutable, 1) + Graph.addEdge(mutable, 0, 1, undefined) + }) + + expect(Graph.neighbors(graph, 0)).toEqual([1]) + expect(Graph.neighbors(graph, 1)).toEqual([0]) + }) + + it("should return correct neighbors for linear graph", () => { + const graph = Graph.undirected((mutable) => { + Graph.addNode(mutable, 0) + Graph.addNode(mutable, 1) + Graph.addNode(mutable, 2) + Graph.addEdge(mutable, 0, 1, undefined) + Graph.addEdge(mutable, 1, 2, undefined) + }) + + expect(Graph.neighbors(graph, 0)).toEqual([1]) + expect(Graph.neighbors(graph, 1).sort()).toEqual([0, 2]) + expect(Graph.neighbors(graph, 2)).toEqual([1]) + }) + + it("should handle multiple edges between same nodes", () => { + const graph = Graph.undirected((mutable) => { + Graph.addNode(mutable, 0) + Graph.addNode(mutable, 1) + Graph.addEdge(mutable, 0, 1, undefined) + Graph.addEdge(mutable, 0, 1, undefined) + }) + + // Should deduplicate neighbors + expect(Graph.neighbors(graph, 0)).toEqual([1]) + expect(Graph.neighbors(graph, 1)).toEqual([0]) + }) + + it("should handle self-loops", () => { + const graph = Graph.undirected((mutable) => { + Graph.addNode(mutable, 0) + Graph.addEdge(mutable, 0, 0, undefined) + }) + + expect(Graph.neighbors(graph, 0)).toEqual([0]) + }) + + it("should handle node with no neighbors", () => { + const graph = Graph.undirected((mutable) => { + Graph.addNode(mutable, 0) + Graph.addNode(mutable, 1) + }) + + expect(Graph.neighbors(graph, 0)).toEqual([]) + expect(Graph.neighbors(graph, 1)).toEqual([]) + }) + }) + + describe("neighborsDirected", () => { + it("should return incoming neighbors", () => { + let nodeA: Graph.NodeIndex + let nodeB: Graph.NodeIndex + let nodeC: Graph.NodeIndex + + const graph = Graph.directed((mutable) => { + nodeA = Graph.addNode(mutable, "Node A") + nodeB = Graph.addNode(mutable, "Node B") + nodeC = Graph.addNode(mutable, "Node C") + Graph.addEdge(mutable, nodeA, nodeB, 1) + Graph.addEdge(mutable, nodeC, nodeB, 2) + }) + + const incomingB = Graph.neighborsDirected(graph, nodeB!, "incoming") + expect(incomingB.sort()).toEqual([nodeA!, nodeC!].sort()) + + const incomingA = Graph.neighborsDirected(graph, nodeA!, "incoming") + expect(incomingA).toEqual([]) + }) + + it("should return outgoing neighbors", () => { + let nodeA: Graph.NodeIndex + let nodeB: Graph.NodeIndex + let nodeC: Graph.NodeIndex + + const graph = Graph.directed((mutable) => { + nodeA = Graph.addNode(mutable, "Node A") + nodeB = Graph.addNode(mutable, "Node B") + nodeC = Graph.addNode(mutable, "Node C") + Graph.addEdge(mutable, nodeA, nodeB, 1) + Graph.addEdge(mutable, nodeA, nodeC, 2) + }) + + const outgoingA = Graph.neighborsDirected(graph, nodeA!, "outgoing") + expect(outgoingA.sort()).toEqual([nodeB!, nodeC!].sort()) + + const outgoingB = Graph.neighborsDirected(graph, nodeB!, "outgoing") + expect(outgoingB).toEqual([]) + }) + + it("should handle node with no connections", () => { + let nodeA: Graph.NodeIndex + + const graph = Graph.directed((mutable) => { + nodeA = Graph.addNode(mutable, "Node A") + }) + + expect(Graph.neighborsDirected(graph, nodeA!, "incoming")).toEqual([]) + expect(Graph.neighborsDirected(graph, nodeA!, "outgoing")).toEqual([]) + }) + }) + }) + + describe("GraphViz export", () => { + describe("toGraphViz", () => { + it("should export empty directed graph", () => { + const graph = Graph.directed() + const dot = Graph.toGraphViz(graph) + + expect(dot).toBe("digraph G {\n}") + }) + + it("should export empty undirected graph", () => { + const graph = Graph.undirected() + const dot = Graph.toGraphViz(graph) + + expect(dot).toBe("graph G {\n}") + }) + + it("should export directed graph with nodes and edges", () => { + const graph = Graph.directed((mutable) => { + const nodeA = Graph.addNode(mutable, "Node A") + const nodeB = Graph.addNode(mutable, "Node B") + const nodeC = Graph.addNode(mutable, "Node C") + Graph.addEdge(mutable, nodeA, nodeB, 1) + Graph.addEdge(mutable, nodeB, nodeC, 2) + Graph.addEdge(mutable, nodeC, nodeA, 3) + }) + + const dot = Graph.toGraphViz(graph) + + expect(dot).toContain("digraph G {") + expect(dot).toContain("\"0\" [label=\"Node A\"];") + expect(dot).toContain("\"1\" [label=\"Node B\"];") + expect(dot).toContain("\"2\" [label=\"Node C\"];") + expect(dot).toContain("\"0\" -> \"1\" [label=\"1\"];") + expect(dot).toContain("\"1\" -> \"2\" [label=\"2\"];") + expect(dot).toContain("\"2\" -> \"0\" [label=\"3\"];") + expect(dot).toContain("}") + }) + + it("should export undirected graph with correct edge format", () => { + const graph = Graph.undirected((mutable) => { + const nodeA = Graph.addNode(mutable, "A") + const nodeB = Graph.addNode(mutable, "B") + Graph.addEdge(mutable, nodeA, nodeB, 1) + }) + + const dot = Graph.toGraphViz(graph) + + expect(dot).toContain("graph G {") + expect(dot).toContain("\"0\" -- \"1\" [label=\"1\"];") + }) + + it("should support custom node and edge labels", () => { + const graph = Graph.directed<{ name: string }, { weight: number }>((mutable) => { + const nodeA = Graph.addNode(mutable, { name: "Alice" }) + const nodeB = Graph.addNode(mutable, { name: "Bob" }) + Graph.addEdge(mutable, nodeA, nodeB, { weight: 42 }) + }) + + const dot = Graph.toGraphViz(graph, { + nodeLabel: (data) => data.name, + edgeLabel: (data) => `weight: ${data.weight}`, + graphName: "MyGraph" + }) + + expect(dot).toContain("digraph MyGraph {") + expect(dot).toContain("\"0\" [label=\"Alice\"];") + expect(dot).toContain("\"1\" [label=\"Bob\"];") + expect(dot).toContain("\"0\" -> \"1\" [label=\"weight: 42\"];") + }) + + it("should escape quotes in labels", () => { + const graph = Graph.directed((mutable) => { + const nodeA = Graph.addNode(mutable, "Node \"A\"") + const nodeB = Graph.addNode(mutable, "Node \"B\"") + Graph.addEdge(mutable, nodeA, nodeB, "Edge \"1\"") + }) + + const dot = Graph.toGraphViz(graph) + + expect(dot).toContain("\"0\" [label=\"Node \\\"A\\\"\"];") + expect(dot).toContain("\"1\" [label=\"Node \\\"B\\\"\"];") + expect(dot).toContain("\"0\" -> \"1\" [label=\"Edge \\\"1\\\"\"];") + }) + + it("should demonstrate graph visualization", () => { + // Create a simple directed graph representing a dependency graph + const graph = Graph.directed((mutable) => { + const app = Graph.addNode(mutable, "App") + const auth = Graph.addNode(mutable, "Auth") + const db = Graph.addNode(mutable, "Database") + const cache = Graph.addNode(mutable, "Cache") + + Graph.addEdge(mutable, app, auth, "uses") + Graph.addEdge(mutable, app, db, "stores") + Graph.addEdge(mutable, auth, db, "validates") + Graph.addEdge(mutable, app, cache, "caches") + }) + + const dot = Graph.toGraphViz(graph, { + graphName: "DependencyGraph" + }) + + // Uncomment the next line to see the GraphViz output in test console + // console.log("\nDependency Graph DOT format:\n" + dot) + + expect(dot).toContain("digraph DependencyGraph {") + expect(dot).toContain("\"0\" [label=\"App\"];") + expect(dot).toContain("\"0\" -> \"1\" [label=\"uses\"];") + expect(dot).toContain("\"0\" -> \"2\" [label=\"stores\"];") + expect(dot).toContain("\"1\" -> \"2\" [label=\"validates\"];") + expect(dot).toContain("\"0\" -> \"3\" [label=\"caches\"];") + }) + + it("should demonstrate undirected graph visualization", () => { + // Create a simple social network graph + const graph = Graph.undirected((mutable) => { + const alice = Graph.addNode(mutable, "Alice") + const bob = Graph.addNode(mutable, "Bob") + const charlie = Graph.addNode(mutable, "Charlie") + const diana = Graph.addNode(mutable, "Diana") + + Graph.addEdge(mutable, alice, bob, "friends") + Graph.addEdge(mutable, bob, charlie, "friends") + Graph.addEdge(mutable, charlie, diana, "friends") + Graph.addEdge(mutable, alice, diana, "friends") + }) + + const dot = Graph.toGraphViz(graph, { + graphName: "SocialNetwork" + }) + + // Uncomment the next line to see the GraphViz output in test console + // console.log("\nSocial Network DOT format:\n" + dot) + + expect(dot).toContain("graph SocialNetwork {") + expect(dot).toContain("\"0\" [label=\"Alice\"];") + expect(dot).toContain("\"0\" -- \"1\" [label=\"friends\"];") + expect(dot).toContain("\"1\" -- \"2\" [label=\"friends\"];") + expect(dot).toContain("\"2\" -- \"3\" [label=\"friends\"];") + expect(dot).toContain("\"0\" -- \"3\" [label=\"friends\"];") + }) + }) + + describe("toMermaid", () => { + it("should export empty directed graph", () => { + const graph = Graph.directed() + const mermaid = Graph.toMermaid(graph) + expect(mermaid).toBe("flowchart TD") + }) + + it("should export empty undirected graph", () => { + const graph = Graph.undirected() + const mermaid = Graph.toMermaid(graph) + expect(mermaid).toBe("graph TD") + }) + + it("should export directed graph with nodes", () => { + const graph = Graph.mutate(Graph.directed(), (mutable) => { + Graph.addNode(mutable, "Node A") + Graph.addNode(mutable, "Node B") + Graph.addNode(mutable, "Node C") + }) + + const mermaid = Graph.toMermaid(graph) + expect(mermaid).toContain("flowchart TD") + expect(mermaid).toContain("0[\"Node A\"]") + expect(mermaid).toContain("1[\"Node B\"]") + expect(mermaid).toContain("2[\"Node C\"]") + }) + + it("should export undirected graph with nodes", () => { + const graph = Graph.mutate(Graph.undirected(), (mutable) => { + Graph.addNode(mutable, "Alice") + Graph.addNode(mutable, "Bob") + }) + + const mermaid = Graph.toMermaid(graph) + expect(mermaid).toContain("graph TD") + expect(mermaid).toContain("0[\"Alice\"]") + expect(mermaid).toContain("1[\"Bob\"]") + }) + + it("should support all node shapes", () => { + const shapes: Array<[string, Graph.MermaidNodeShape]> = [ + ["rectangle", "rectangle"], + ["rounded", "rounded"], + ["circle", "circle"], + ["diamond", "diamond"], + ["hexagon", "hexagon"], + ["stadium", "stadium"], + ["subroutine", "subroutine"], + ["cylindrical", "cylindrical"] + ] + + shapes.forEach(([shapeName, shapeValue]) => { + const graph = Graph.mutate(Graph.directed(), (mutable) => { + Graph.addNode(mutable, "Test") + }) + + const mermaid = Graph.toMermaid(graph, { + nodeShape: () => shapeValue + }) + + expect(mermaid).toContain("flowchart TD") + + // Test expected shape format + switch (shapeName) { + case "rectangle": + expect(mermaid).toContain("0[\"Test\"]") + break + case "rounded": + expect(mermaid).toContain("0(\"Test\")") + break + case "circle": + expect(mermaid).toContain("0((\"Test\"))") + break + case "diamond": + expect(mermaid).toContain("0{\"Test\"}") + break + case "hexagon": + expect(mermaid).toContain("0{{\"Test\"}}") + break + case "stadium": + expect(mermaid).toContain("0([\"Test\"])") + break + case "subroutine": + expect(mermaid).toContain("0[[\"Test\"]]") + break + case "cylindrical": + expect(mermaid).toContain("0[(\"Test\")]") + break + } + }) + }) + + it("should escape special characters in labels", () => { + const graph = Graph.mutate(Graph.directed(), (mutable) => { + Graph.addNode(mutable, "Node with \"quotes\"") + Graph.addNode(mutable, "Node with [brackets]") + Graph.addNode(mutable, "Node with | pipe") + Graph.addNode(mutable, "Node with \\ backslash") + Graph.addNode(mutable, "Node with \n newline") + }) + + const mermaid = Graph.toMermaid(graph) + + expect(mermaid).toContain("0[\"Node with #quot;quotes#quot;\"]") + expect(mermaid).toContain("1[\"Node with #91;brackets#93;\"]") + expect(mermaid).toContain("2[\"Node with #124; pipe\"]") + expect(mermaid).toContain("3[\"Node with #92; backslash\"]") + expect(mermaid).toContain("4[\"Node with
newline\"]") + }) + + it("should export directed graph with edges", () => { + const graph = Graph.mutate(Graph.directed(), (mutable) => { + const nodeA = Graph.addNode(mutable, "Node A") + const nodeB = Graph.addNode(mutable, "Node B") + const nodeC = Graph.addNode(mutable, "Node C") + Graph.addEdge(mutable, nodeA, nodeB, 1) + Graph.addEdge(mutable, nodeB, nodeC, 2) + Graph.addEdge(mutable, nodeC, nodeA, 3) + }) + + const mermaid = Graph.toMermaid(graph) + expect(mermaid).toContain("flowchart TD") + expect(mermaid).toContain("0[\"Node A\"]") + expect(mermaid).toContain("1[\"Node B\"]") + expect(mermaid).toContain("2[\"Node C\"]") + expect(mermaid).toContain("0 -->|\"1\"| 1") + expect(mermaid).toContain("1 -->|\"2\"| 2") + expect(mermaid).toContain("2 -->|\"3\"| 0") + }) + + it("should export undirected graph with edges", () => { + const graph = Graph.mutate(Graph.undirected(), (mutable) => { + const alice = Graph.addNode(mutable, "Alice") + const bob = Graph.addNode(mutable, "Bob") + const charlie = Graph.addNode(mutable, "Charlie") + Graph.addEdge(mutable, alice, bob, "friends") + Graph.addEdge(mutable, bob, charlie, "colleagues") + }) + + const mermaid = Graph.toMermaid(graph) + expect(mermaid).toContain("graph TD") + expect(mermaid).toContain("0[\"Alice\"]") + expect(mermaid).toContain("1[\"Bob\"]") + expect(mermaid).toContain("2[\"Charlie\"]") + expect(mermaid).toContain("0 ---|\"friends\"| 1") + expect(mermaid).toContain("1 ---|\"colleagues\"| 2") + }) + + it("should handle empty edge labels", () => { + const graph = Graph.mutate(Graph.directed(), (mutable) => { + const nodeA = Graph.addNode(mutable, "A") + const nodeB = Graph.addNode(mutable, "B") + Graph.addEdge(mutable, nodeA, nodeB, "") + }) + + const mermaid = Graph.toMermaid(graph) + expect(mermaid).toContain("0 --> 1") + }) + + it("should support all diagram directions", () => { + const directions: Array = ["TB", "TD", "BT", "RL", "LR"] + + directions.forEach((dir) => { + const graph = Graph.mutate(Graph.directed(), (mutable) => { + Graph.addNode(mutable, "A") + Graph.addNode(mutable, "B") + }) + + const mermaid = Graph.toMermaid(graph, { direction: dir }) + expect(mermaid).toContain(`flowchart ${dir}`) + expect(mermaid).toContain("0[\"A\"]") + expect(mermaid).toContain("1[\"B\"]") + }) + }) + + it("should auto-detect diagram type based on graph type", () => { + // Directed graph should auto-detect as flowchart + const directedGraph = Graph.mutate(Graph.directed(), (mutable) => { + Graph.addNode(mutable, "A") + }) + const directedMermaid = Graph.toMermaid(directedGraph) + expect(directedMermaid).toContain("flowchart TD") + + // Undirected graph should auto-detect as graph + const undirectedGraph = Graph.mutate(Graph.undirected(), (mutable) => { + Graph.addNode(mutable, "A") + }) + const undirectedMermaid = Graph.toMermaid(undirectedGraph) + expect(undirectedMermaid).toContain("graph TD") + }) + + it("should allow manual diagram type override", () => { + // Override directed graph to use 'graph' type + const directedGraph = Graph.mutate(Graph.directed(), (mutable) => { + Graph.addNode(mutable, "A") + }) + const overriddenMermaid = Graph.toMermaid(directedGraph, { + diagramType: "graph" + }) + expect(overriddenMermaid).toContain("graph TD") + + // Override undirected graph to use 'flowchart' type + const undirectedGraph = Graph.mutate(Graph.undirected(), (mutable) => { + Graph.addNode(mutable, "B") + }) + const overriddenFlowchart = Graph.toMermaid(undirectedGraph, { + diagramType: "flowchart" + }) + expect(overriddenFlowchart).toContain("flowchart TD") + }) + + it("should combine direction and diagram type options", () => { + const graph = Graph.mutate(Graph.directed(), (mutable) => { + Graph.addNode(mutable, "Test") + }) + + const mermaid = Graph.toMermaid(graph, { + direction: "LR", + diagramType: "graph" + }) + + expect(mermaid).toContain("graph LR") + expect(mermaid).toContain("0[\"Test\"]") + }) + + it("should handle self-loops correctly", () => { + const graph = Graph.mutate(Graph.directed(), (mutable) => { + const nodeA = Graph.addNode(mutable, "A") + Graph.addEdge(mutable, nodeA, nodeA, "self") + }) + + const mermaid = Graph.toMermaid(graph) + expect(mermaid).toContain("flowchart TD") + expect(mermaid).toContain("0[\"A\"]") + expect(mermaid).toContain("0 -->|\"self\"| 0") + }) + + it("should handle multi-edges correctly", () => { + const graph = Graph.mutate(Graph.directed(), (mutable) => { + const nodeA = Graph.addNode(mutable, "A") + const nodeB = Graph.addNode(mutable, "B") + Graph.addEdge(mutable, nodeA, nodeB, 1) + Graph.addEdge(mutable, nodeA, nodeB, 2) + Graph.addEdge(mutable, nodeA, nodeB, 3) + }) + + const mermaid = Graph.toMermaid(graph) + expect(mermaid).toContain("flowchart TD") + expect(mermaid).toContain("0[\"A\"]") + expect(mermaid).toContain("1[\"B\"]") + // Should contain all three edges + expect(mermaid).toContain("0 -->|\"1\"| 1") + expect(mermaid).toContain("0 -->|\"2\"| 1") + expect(mermaid).toContain("0 -->|\"3\"| 1") + }) + + it("should handle disconnected components", () => { + const graph = Graph.mutate(Graph.directed(), (mutable) => { + // Component 1: A -> B + const nodeA = Graph.addNode(mutable, "A") + const nodeB = Graph.addNode(mutable, "B") + Graph.addEdge(mutable, nodeA, nodeB, "A->B") + + // Component 2: C -> D (disconnected) + const nodeC = Graph.addNode(mutable, "C") + const nodeD = Graph.addNode(mutable, "D") + Graph.addEdge(mutable, nodeC, nodeD, "C->D") + + // Isolated node E + Graph.addNode(mutable, "E") + }) + + const mermaid = Graph.toMermaid(graph) + expect(mermaid).toContain("flowchart TD") + expect(mermaid).toContain("0[\"A\"]") + expect(mermaid).toContain("1[\"B\"]") + expect(mermaid).toContain("2[\"C\"]") + expect(mermaid).toContain("3[\"D\"]") + expect(mermaid).toContain("4[\"E\"]") + expect(mermaid).toContain("0 -->|\"A-#gt;B\"| 1") + expect(mermaid).toContain("2 -->|\"C-#gt;D\"| 3") + }) + + it("should handle custom labels with complex data", () => { + interface NodeData { + id: string + value: number + metadata: { type: string } + } + + interface EdgeData { + weight: number + type: string + } + + const graph = Graph.mutate(Graph.directed(), (mutable) => { + const node1 = Graph.addNode(mutable, { + id: "node1", + value: 42, + metadata: { type: "input" } + }) + const node2 = Graph.addNode(mutable, { + id: "node2", + value: 84, + metadata: { type: "processing" } + }) + Graph.addEdge(mutable, node1, node2, { weight: 1.5, type: "data" }) + }) + + const mermaid = Graph.toMermaid(graph, { + nodeLabel: (data) => `${data.id}:${data.value}`, + edgeLabel: (data) => `${data.type}(${data.weight})`, + direction: "LR" + }) + + expect(mermaid).toContain("flowchart LR") + expect(mermaid).toContain("0[\"node1:42\"]") + expect(mermaid).toContain("1[\"node2:84\"]") + expect(mermaid).toContain("0 -->|\"data#40;1.5#41;\"| 1") + }) + }) + }) + + describe("Graph Structure Analysis Algorithms (Phase 5A)", () => { + describe("isAcyclic", () => { + it("should detect acyclic directed graphs (DAGs)", () => { + const dag = Graph.directed((mutable) => { + const a = Graph.addNode(mutable, "A") + const b = Graph.addNode(mutable, "B") + const c = Graph.addNode(mutable, "C") + const d = Graph.addNode(mutable, "D") + Graph.addEdge(mutable, a, b, "A->B") + Graph.addEdge(mutable, a, c, "A->C") + Graph.addEdge(mutable, b, d, "B->D") + Graph.addEdge(mutable, c, d, "C->D") + }) + + expect(Graph.isAcyclic(dag)).toBe(true) + }) + + it("should detect cycles in directed graphs", () => { + const cyclic = Graph.directed((mutable) => { + const a = Graph.addNode(mutable, "A") + const b = Graph.addNode(mutable, "B") + const c = Graph.addNode(mutable, "C") + Graph.addEdge(mutable, a, b, "A->B") + Graph.addEdge(mutable, b, c, "B->C") + Graph.addEdge(mutable, c, a, "C->A") // Creates cycle + }) + + expect(Graph.isAcyclic(cyclic)).toBe(false) + }) + + it("should handle disconnected components", () => { + const disconnected = Graph.directed((mutable) => { + const a = Graph.addNode(mutable, "A") + const b = Graph.addNode(mutable, "B") + const c = Graph.addNode(mutable, "C") + const d = Graph.addNode(mutable, "D") + Graph.addEdge(mutable, a, b, "A->B") // Component 1: A->B (acyclic) + Graph.addEdge(mutable, c, d, "C->D") // Component 2: C->D (acyclic) + // No connections between components + }) + + expect(Graph.isAcyclic(disconnected)).toBe(true) + }) + + it("should detect cycles in one component of disconnected graph", () => { + const mixedComponents = Graph.directed((mutable) => { + const a = Graph.addNode(mutable, "A") + const b = Graph.addNode(mutable, "B") + const c = Graph.addNode(mutable, "C") + const d = Graph.addNode(mutable, "D") + Graph.addEdge(mutable, a, b, "A->B") // Component 1: A->B (acyclic) + Graph.addEdge(mutable, c, d, "C->D") // Component 2: C->D->C (cyclic) + Graph.addEdge(mutable, d, c, "D->C") + }) + + expect(Graph.isAcyclic(mixedComponents)).toBe(false) + }) + }) + + describe("isBipartite", () => { + it("should detect bipartite undirected graphs", () => { + const bipartite = Graph.undirected((mutable) => { + const a = Graph.addNode(mutable, "A") + const b = Graph.addNode(mutable, "B") + const c = Graph.addNode(mutable, "C") + const d = Graph.addNode(mutable, "D") + Graph.addEdge(mutable, a, b, "edge") // Set 1: {A, C}, Set 2: {B, D} + Graph.addEdge(mutable, b, c, "edge") + Graph.addEdge(mutable, c, d, "edge") + Graph.addEdge(mutable, d, a, "edge") + }) + + expect(Graph.isBipartite(bipartite)).toBe(true) + }) + + it("should detect non-bipartite graphs (odd cycles)", () => { + const triangle = Graph.undirected((mutable) => { + const a = Graph.addNode(mutable, "A") + const b = Graph.addNode(mutable, "B") + const c = Graph.addNode(mutable, "C") + Graph.addEdge(mutable, a, b, "edge") + Graph.addEdge(mutable, b, c, "edge") + Graph.addEdge(mutable, c, a, "edge") // Triangle (3-cycle) + }) + + expect(Graph.isBipartite(triangle)).toBe(false) + }) + + it("should handle path graphs (always bipartite)", () => { + const path = Graph.undirected((mutable) => { + const a = Graph.addNode(mutable, "A") + const b = Graph.addNode(mutable, "B") + const c = Graph.addNode(mutable, "C") + const d = Graph.addNode(mutable, "D") + Graph.addEdge(mutable, a, b, "edge") + Graph.addEdge(mutable, b, c, "edge") + Graph.addEdge(mutable, c, d, "edge") + }) + + expect(Graph.isBipartite(path)).toBe(true) + }) + + it("should handle disconnected components", () => { + const disconnected = Graph.undirected((mutable) => { + const a = Graph.addNode(mutable, "A") + const b = Graph.addNode(mutable, "B") + const c = Graph.addNode(mutable, "C") + const d = Graph.addNode(mutable, "D") + Graph.addEdge(mutable, a, b, "edge") // Component 1: A-B (bipartite) + Graph.addEdge(mutable, c, d, "edge") // Component 2: C-D (bipartite) + // No connections between components + }) + + expect(Graph.isBipartite(disconnected)).toBe(true) + }) + + it("should detect non-bipartite component in disconnected graph", () => { + const mixedComponents = Graph.undirected((mutable) => { + const a = Graph.addNode(mutable, "A") + const b = Graph.addNode(mutable, "B") + const c = Graph.addNode(mutable, "C") + const d = Graph.addNode(mutable, "D") + const e = Graph.addNode(mutable, "E") + Graph.addEdge(mutable, a, b, "edge") // Component 1: A-B (bipartite) + Graph.addEdge(mutable, c, d, "edge") // Component 2: triangle (non-bipartite) + Graph.addEdge(mutable, d, e, "edge") + Graph.addEdge(mutable, e, c, "edge") + }) + + expect(Graph.isBipartite(mixedComponents)).toBe(false) + }) + }) + + describe("connectedComponents", () => { + it("should find connected components in disconnected undirected graph", () => { + const graph = Graph.undirected((mutable) => { + const a = Graph.addNode(mutable, "A") + const b = Graph.addNode(mutable, "B") + const c = Graph.addNode(mutable, "C") + const d = Graph.addNode(mutable, "D") + Graph.addNode(mutable, "E") + Graph.addEdge(mutable, a, b, "edge") // Component 1: A-B + Graph.addEdge(mutable, c, d, "edge") // Component 2: C-D + // E is isolated - Component 3: E + }) + + const components = Graph.connectedComponents(graph) + expect(components).toHaveLength(3) + + // Sort components by size and first element for deterministic testing + components.sort((a, b) => a.length - b.length || a[0] - b[0]) + expect(components[0]).toEqual([4]) // E isolated + expect(components[1]).toHaveLength(2) // A-B or C-D + expect(components[2]).toHaveLength(2) // A-B or C-D + }) + + it("should handle fully connected component", () => { + const graph = Graph.undirected((mutable) => { + const a = Graph.addNode(mutable, "A") + const b = Graph.addNode(mutable, "B") + const c = Graph.addNode(mutable, "C") + Graph.addEdge(mutable, a, b, "edge") + Graph.addEdge(mutable, b, c, "edge") + Graph.addEdge(mutable, c, a, "edge") + }) + + const components = Graph.connectedComponents(graph) + expect(components).toHaveLength(1) + expect(components[0]).toHaveLength(3) + expect(components[0].sort()).toEqual([0, 1, 2]) + }) + }) + + describe("stronglyConnectedComponents", () => { + it("should find strongly connected components in directed graph", () => { + const graph = Graph.directed((mutable) => { + const a = Graph.addNode(mutable, "A") + const b = Graph.addNode(mutable, "B") + const c = Graph.addNode(mutable, "C") + const d = Graph.addNode(mutable, "D") + Graph.addEdge(mutable, a, b, "A->B") + Graph.addEdge(mutable, b, c, "B->C") + Graph.addEdge(mutable, c, a, "C->A") // SCC: A-B-C + Graph.addEdge(mutable, b, d, "B->D") // D is separate + }) + + const sccs = Graph.stronglyConnectedComponents(graph) + expect(sccs).toHaveLength(2) + + // Sort SCCs by size for deterministic testing + sccs.sort((a, b) => a.length - b.length) + expect(sccs[0]).toEqual([3]) // D is alone + expect(sccs[1]).toHaveLength(3) // A-B-C cycle + expect(sccs[1].sort()).toEqual([0, 1, 2]) + }) + + it("should handle acyclic directed graph (each node is its own SCC)", () => { + const dag = Graph.directed((mutable) => { + const a = Graph.addNode(mutable, "A") + const b = Graph.addNode(mutable, "B") + const c = Graph.addNode(mutable, "C") + Graph.addEdge(mutable, a, b, "A->B") + Graph.addEdge(mutable, b, c, "B->C") + }) + + const sccs = Graph.stronglyConnectedComponents(dag) + expect(sccs).toHaveLength(3) + // Each SCC should contain exactly one node + sccs.forEach((scc) => { + expect(scc).toHaveLength(1) + }) + }) + + it("should handle fully connected components", () => { + const graph = Graph.directed((mutable) => { + const a = Graph.addNode(mutable, "A") + const b = Graph.addNode(mutable, "B") + const c = Graph.addNode(mutable, "C") + // Create bidirectional edges (fully connected) + Graph.addEdge(mutable, a, b, "A->B") + Graph.addEdge(mutable, b, a, "B->A") + Graph.addEdge(mutable, b, c, "B->C") + Graph.addEdge(mutable, c, b, "C->B") + Graph.addEdge(mutable, a, c, "A->C") + Graph.addEdge(mutable, c, a, "C->A") + }) + + const sccs = Graph.stronglyConnectedComponents(graph) + expect(sccs).toHaveLength(1) + expect(sccs[0]).toHaveLength(3) + expect(sccs[0].sort()).toEqual([0, 1, 2]) + }) + + it("should handle disconnected components with cycles", () => { + const graph = Graph.directed((mutable) => { + const a = Graph.addNode(mutable, "A") + const b = Graph.addNode(mutable, "B") + const c = Graph.addNode(mutable, "C") + const d = Graph.addNode(mutable, "D") + // First SCC: A->B->A + Graph.addEdge(mutable, a, b, "A->B") + Graph.addEdge(mutable, b, a, "B->A") + // Second SCC: C->D->C + Graph.addEdge(mutable, c, d, "C->D") + Graph.addEdge(mutable, d, c, "D->C") + }) + + const sccs = Graph.stronglyConnectedComponents(graph) + expect(sccs).toHaveLength(2) + sccs.forEach((scc) => { + expect(scc).toHaveLength(2) + }) + }) + }) + + describe("dijkstra", () => { + it("should find shortest path in simple graph", () => { + let nodeA: Graph.NodeIndex + let nodeB: Graph.NodeIndex + let nodeC: Graph.NodeIndex + + const graph = Graph.directed((mutable) => { + nodeA = Graph.addNode(mutable, "A") + nodeB = Graph.addNode(mutable, "B") + nodeC = Graph.addNode(mutable, "C") + Graph.addEdge(mutable, nodeA, nodeB, 5) + Graph.addEdge(mutable, nodeA, nodeC, 10) + Graph.addEdge(mutable, nodeB, nodeC, 2) + }) + + const result = Graph.dijkstra(graph, { source: nodeA!, target: nodeC!, cost: (edge) => edge }) + expect(Option.isSome(result)).toBe(true) + if (Option.isSome(result)) { + expect(result.value.path).toEqual([nodeA!, nodeB!, nodeC!]) + expect(result.value.distance).toBe(7) + expect(result.value.costs).toEqual([5, 2]) + } + }) + + it("should return None for unreachable nodes", () => { + let nodeA: Graph.NodeIndex + let nodeB: Graph.NodeIndex + let nodeC: Graph.NodeIndex + + const graph = Graph.directed((mutable) => { + nodeA = Graph.addNode(mutable, "A") + nodeB = Graph.addNode(mutable, "B") + nodeC = Graph.addNode(mutable, "C") + Graph.addEdge(mutable, nodeA, nodeB, 1) + // No path from A to C + }) + + const result = Graph.dijkstra(graph, { source: nodeA!, target: nodeC!, cost: (edge) => edge }) + expect(Option.isNone(result)).toBe(true) + }) + + it("should handle same source and target", () => { + let nodeA: Graph.NodeIndex + + const graph = Graph.directed((mutable) => { + nodeA = Graph.addNode(mutable, "A") + }) + + const result = Graph.dijkstra(graph, { source: nodeA!, target: nodeA!, cost: (edge) => edge }) + expect(Option.isSome(result)).toBe(true) + if (Option.isSome(result)) { + expect(result.value.path).toEqual([nodeA!]) + expect(result.value.distance).toBe(0) + expect(result.value.costs).toEqual([]) + } + }) + + it("should throw for negative weights", () => { + let nodeA: Graph.NodeIndex + let nodeB: Graph.NodeIndex + + const graph = Graph.directed((mutable) => { + nodeA = Graph.addNode(mutable, "A") + nodeB = Graph.addNode(mutable, "B") + Graph.addEdge(mutable, nodeA, nodeB, -1) + }) + + expect(() => Graph.dijkstra(graph, { source: nodeA!, target: nodeB!, cost: (edge) => edge })).toThrow( + "Dijkstra's algorithm requires non-negative edge weights" + ) + }) + + it("should throw for non-existent nodes", () => { + const graph = Graph.directed() + + expect(() => Graph.dijkstra(graph, { source: 0, target: 1, cost: (edge) => edge })).toThrow( + "Node 0 does not exist" + ) + }) + }) + + describe("astar", () => { + it("should find shortest path with heuristic", () => { + let nodeA: Graph.NodeIndex + let nodeB: Graph.NodeIndex + let nodeC: Graph.NodeIndex + + const graph = Graph.directed<{ x: number; y: number }, number>((mutable) => { + nodeA = Graph.addNode(mutable, { x: 0, y: 0 }) + nodeB = Graph.addNode(mutable, { x: 1, y: 0 }) + nodeC = Graph.addNode(mutable, { x: 2, y: 0 }) + Graph.addEdge(mutable, nodeA, nodeB, 1) + Graph.addEdge(mutable, nodeB, nodeC, 1) + }) + + const heuristic = (source: { x: number; y: number }, target: { x: number; y: number }) => + Math.abs(source.x - target.x) + Math.abs(source.y - target.y) + + const result = Graph.astar(graph, { source: nodeA!, target: nodeC!, cost: (edge) => edge, heuristic }) + expect(Option.isSome(result)).toBe(true) + if (Option.isSome(result)) { + expect(result.value.path).toEqual([nodeA!, nodeB!, nodeC!]) + expect(result.value.distance).toBe(2) + expect(result.value.costs).toEqual([1, 1]) + } + }) + + it("should return None for unreachable nodes", () => { + const graph = Graph.directed<{ x: number; y: number }, number>((mutable) => { + const a = Graph.addNode(mutable, { x: 0, y: 0 }) + const b = Graph.addNode(mutable, { x: 1, y: 0 }) + Graph.addNode(mutable, { x: 2, y: 0 }) + Graph.addEdge(mutable, a, b, 1) + // No path from A to C + }) + + const heuristic = (source: { x: number; y: number }, target: { x: number; y: number }) => + Math.abs(source.x - target.x) + Math.abs(source.y - target.y) + + const result = Graph.astar(graph, { source: 0, target: 2, cost: (edge) => edge, heuristic }) + expect(Option.isNone(result)).toBe(true) + }) + + it("should handle same source and target", () => { + const graph = Graph.directed<{ x: number; y: number }, number>((mutable) => { + Graph.addNode(mutable, { x: 0, y: 0 }) + }) + + const heuristic = (source: { x: number; y: number }, target: { x: number; y: number }) => + Math.abs(source.x - target.x) + Math.abs(source.y - target.y) + + const result = Graph.astar(graph, { source: 0, target: 0, cost: (edge) => edge, heuristic }) + expect(Option.isSome(result)).toBe(true) + if (Option.isSome(result)) { + expect(result.value.path).toEqual([0]) + expect(result.value.distance).toBe(0) + expect(result.value.costs).toEqual([]) + } + }) + + it("should throw for negative weights", () => { + const graph = Graph.directed<{ x: number; y: number }, number>((mutable) => { + const a = Graph.addNode(mutable, { x: 0, y: 0 }) + const b = Graph.addNode(mutable, { x: 1, y: 0 }) + Graph.addEdge(mutable, a, b, -1) + }) + + const heuristic = (source: { x: number; y: number }, target: { x: number; y: number }) => + Math.abs(source.x - target.x) + Math.abs(source.y - target.y) + + expect(() => Graph.astar(graph, { source: 0, target: 1, cost: (edge) => edge, heuristic })).toThrow( + "A* algorithm requires non-negative edge weights" + ) + }) + }) + + describe("bellmanFord", () => { + it("should find shortest path with negative weights", () => { + const graph = Graph.directed((mutable) => { + const a = Graph.addNode(mutable, "A") + const b = Graph.addNode(mutable, "B") + const c = Graph.addNode(mutable, "C") + Graph.addEdge(mutable, a, b, -1) + Graph.addEdge(mutable, b, c, 3) + Graph.addEdge(mutable, a, c, 5) + }) + + const result = Graph.bellmanFord(graph, { source: 0, target: 2, cost: (edge) => edge }) + expect(Option.isSome(result)).toBe(true) + if (Option.isSome(result)) { + expect(result.value.path).toEqual([0, 1, 2]) + expect(result.value.distance).toBe(2) + expect(result.value.costs).toEqual([-1, 3]) + } + }) + + it("should return None for unreachable nodes", () => { + const graph = Graph.directed((mutable) => { + const a = Graph.addNode(mutable, "A") + const b = Graph.addNode(mutable, "B") + Graph.addNode(mutable, "C") + Graph.addEdge(mutable, a, b, 1) + // No path from A to C + }) + + const result = Graph.bellmanFord(graph, { source: 0, target: 2, cost: (edge) => edge }) + expect(Option.isNone(result)).toBe(true) + }) + + it("should handle same source and target", () => { + const graph = Graph.directed((mutable) => { + Graph.addNode(mutable, "A") + }) + + const result = Graph.bellmanFord(graph, { source: 0, target: 0, cost: (edge) => edge }) + expect(Option.isSome(result)).toBe(true) + if (Option.isSome(result)) { + expect(result.value.path).toEqual([0]) + expect(result.value.distance).toBe(0) + expect(result.value.costs).toEqual([]) + } + }) + + it("should detect negative cycles", () => { + const graph = Graph.directed((mutable) => { + const a = Graph.addNode(mutable, "A") + const b = Graph.addNode(mutable, "B") + const c = Graph.addNode(mutable, "C") + Graph.addEdge(mutable, a, b, 1) + Graph.addEdge(mutable, b, c, -3) + Graph.addEdge(mutable, c, a, 1) + }) + + const result = Graph.bellmanFord(graph, { source: 0, target: 2, cost: (edge) => edge }) + expect(Option.isNone(result)).toBe(true) + }) + }) + + describe("floydWarshall", () => { + it("should find all-pairs shortest paths", () => { + const graph = Graph.directed((mutable) => { + const a = Graph.addNode(mutable, "A") + const b = Graph.addNode(mutable, "B") + const c = Graph.addNode(mutable, "C") + Graph.addEdge(mutable, a, b, 3) + Graph.addEdge(mutable, b, c, 2) + Graph.addEdge(mutable, a, c, 7) + }) + + const result = Graph.floydWarshall(graph, (edge) => edge) + + // Check distance A to C (should be 5 via B, not 7 direct) + expect(result.distances.get(0)?.get(2)).toBe(5) + expect(result.paths.get(0)?.get(2)).toEqual([0, 1, 2]) + expect(result.costs.get(0)?.get(2)).toEqual([3, 2]) + + // Check distance A to B + expect(result.distances.get(0)?.get(1)).toBe(3) + expect(result.paths.get(0)?.get(1)).toEqual([0, 1]) + + // Check distance B to C + expect(result.distances.get(1)?.get(2)).toBe(2) + expect(result.paths.get(1)?.get(2)).toEqual([1, 2]) + }) + + it("should handle unreachable nodes", () => { + const graph = Graph.directed((mutable) => { + const a = Graph.addNode(mutable, "A") + const b = Graph.addNode(mutable, "B") + Graph.addNode(mutable, "C") + Graph.addEdge(mutable, a, b, 1) + // No path from A to C + }) + + const result = Graph.floydWarshall(graph, (edge) => edge) + + expect(result.distances.get(0)?.get(2)).toBe(Infinity) + expect(result.paths.get(0)?.get(2)).toBeNull() + }) + + it("should handle same source and target", () => { + const graph = Graph.directed((mutable) => { + Graph.addNode(mutable, "A") + }) + + const result = Graph.floydWarshall(graph, (edge) => edge) + + expect(result.distances.get(0)?.get(0)).toBe(0) + expect(result.paths.get(0)?.get(0)).toEqual([0]) + expect(result.costs.get(0)?.get(0)).toEqual([]) + }) + + it("should detect negative cycles", () => { + const graph = Graph.directed((mutable) => { + const a = Graph.addNode(mutable, "A") + const b = Graph.addNode(mutable, "B") + const c = Graph.addNode(mutable, "C") + Graph.addEdge(mutable, a, b, 1) + Graph.addEdge(mutable, b, c, -3) + Graph.addEdge(mutable, c, a, 1) + }) + + expect(() => Graph.floydWarshall(graph, (edge) => edge)).toThrow("Negative cycle detected") + }) + }) + + describe("Iterator Base Methods", () => { + it("should provide values() method for DFS iterator", () => { + const graph = Graph.directed((mutable) => { + const a = Graph.addNode(mutable, "A") + const b = Graph.addNode(mutable, "B") + const c = Graph.addNode(mutable, "C") + Graph.addEdge(mutable, a, b, 1) + Graph.addEdge(mutable, b, c, 2) + }) + + const dfsIterator = Graph.dfs(graph, { start: [0] }) + const values = Array.from(Graph.values(dfsIterator)) + + expect(values).toEqual(["A", "B", "C"]) + }) + + it("should provide entries() method for DFS iterator", () => { + const graph = Graph.directed((mutable) => { + const a = Graph.addNode(mutable, "A") + const b = Graph.addNode(mutable, "B") + const c = Graph.addNode(mutable, "C") + Graph.addEdge(mutable, a, b, 1) + Graph.addEdge(mutable, b, c, 2) + }) + + const dfsIterator = Graph.dfs(graph, { start: [0] }) + const entries = Array.from(Graph.entries(dfsIterator)) + + expect(entries).toEqual([[0, "A"], [1, "B"], [2, "C"]]) + }) + + it("should provide values() method for BFS iterator", () => { + const graph = Graph.directed((mutable) => { + const a = Graph.addNode(mutable, "A") + const b = Graph.addNode(mutable, "B") + const c = Graph.addNode(mutable, "C") + Graph.addEdge(mutable, a, b, 1) + Graph.addEdge(mutable, a, c, 2) + }) + + const bfsIterator = Graph.bfs(graph, { start: [0] }) + const values = Array.from(Graph.values(bfsIterator)) + + expect(values).toEqual(["A", "B", "C"]) + }) + + it("should provide entries() method for BFS iterator", () => { + const graph = Graph.directed((mutable) => { + const a = Graph.addNode(mutable, "A") + const b = Graph.addNode(mutable, "B") + const c = Graph.addNode(mutable, "C") + Graph.addEdge(mutable, a, b, 1) + Graph.addEdge(mutable, a, c, 2) + }) + + const bfsIterator = Graph.bfs(graph, { start: [0] }) + const entries = Array.from(Graph.entries(bfsIterator)) + + expect(entries).toEqual([[0, "A"], [1, "B"], [2, "C"]]) + }) + + it("should provide values() method for Topo iterator", () => { + const graph = Graph.directed((mutable) => { + const a = Graph.addNode(mutable, "A") + const b = Graph.addNode(mutable, "B") + const c = Graph.addNode(mutable, "C") + Graph.addEdge(mutable, a, b, 1) + Graph.addEdge(mutable, b, c, 2) + }) + + const topoIterator = Graph.topo(graph) + + const values = Array.from(Graph.values(topoIterator)) + expect(values).toEqual(["A", "B", "C"]) + }) + + it("should provide entries() method for Topo iterator", () => { + const graph = Graph.directed((mutable) => { + const a = Graph.addNode(mutable, "A") + const b = Graph.addNode(mutable, "B") + const c = Graph.addNode(mutable, "C") + Graph.addEdge(mutable, a, b, 1) + Graph.addEdge(mutable, b, c, 2) + }) + + const topoIterator = Graph.topo(graph) + + const entries = Array.from(Graph.entries(topoIterator)) + expect(entries).toEqual([[0, "A"], [1, "B"], [2, "C"]]) + }) + + it("should throw for cyclic graphs", () => { + const cyclicGraph = Graph.directed((mutable) => { + const a = Graph.addNode(mutable, "A") + const b = Graph.addNode(mutable, "B") + Graph.addEdge(mutable, a, b, 1) + Graph.addEdge(mutable, b, a, 2) // Creates cycle + }) + + expect(() => Graph.topo(cyclicGraph)).toThrow("Cannot perform topological sort on cyclic graph") + }) + + it("should handle corrupted graph state during topological sort", () => { + const graph = Graph.directed((mutable) => { + const a = Graph.addNode(mutable, "A") + const b = Graph.addNode(mutable, "B") + Graph.addEdge(mutable, a, b, 1) + }) + + // Test edge case by corrupting graph internals during iteration + const mutableGraph = graph as any + const originalGetNode = mutableGraph.nodes.get + + let callCount = 0 + // Mock getNode to return undefined for certain calls to trigger the recursive edge case + mutableGraph.nodes.get = function(key: any) { + callCount++ + // On specific call, return undefined to trigger the Option.isNone path + if (callCount === 2) { + return undefined + } + return originalGetNode.call(this, key) + } + + const iterator = Graph.topo(graph) + const results = Array.from(iterator) + + // Restore original method + mutableGraph.nodes.get = originalGetNode + + // Should complete without crashing + expect(results.length).toBeGreaterThanOrEqual(0) + }) + + it("should provide values() method for DfsPostOrder iterator", () => { + const graph = Graph.directed((mutable) => { + const a = Graph.addNode(mutable, "A") + const b = Graph.addNode(mutable, "B") + const c = Graph.addNode(mutable, "C") + Graph.addEdge(mutable, a, b, 1) + Graph.addEdge(mutable, b, c, 2) + }) + + const dfsPostIterator = Graph.dfsPostOrder(graph, { start: [0] }) + const values = Array.from(Graph.values(dfsPostIterator)) + + expect(values).toEqual(["C", "B", "A"]) // Postorder: children before parents + }) + + it("should provide entries() method for DfsPostOrder iterator", () => { + const graph = Graph.directed((mutable) => { + const a = Graph.addNode(mutable, "A") + const b = Graph.addNode(mutable, "B") + const c = Graph.addNode(mutable, "C") + Graph.addEdge(mutable, a, b, 1) + Graph.addEdge(mutable, b, c, 2) + }) + + const dfsPostIterator = Graph.dfsPostOrder(graph, { start: [0] }) + const entries = Array.from(Graph.entries(dfsPostIterator)) + + expect(entries).toEqual([[2, "C"], [1, "B"], [0, "A"]]) // Postorder: children before parents + }) + }) + + describe("DfsPostOrder Iterator", () => { + it("should traverse in postorder for simple chain", () => { + const graph = Graph.directed((mutable) => { + const a = Graph.addNode(mutable, "A") + const b = Graph.addNode(mutable, "B") + const c = Graph.addNode(mutable, "C") + Graph.addEdge(mutable, a, b, 1) + Graph.addEdge(mutable, b, c, 2) + }) + + const postOrder = Array.from(Graph.indices(Graph.dfsPostOrder(graph, { start: [0] }))) + expect(postOrder).toEqual([2, 1, 0]) // Children before parents + }) + + it("should traverse in postorder for branching tree", () => { + const graph = Graph.directed((mutable) => { + const root = Graph.addNode(mutable, "root") // 0 + const left = Graph.addNode(mutable, "left") // 1 + const right = Graph.addNode(mutable, "right") // 2 + const leaf1 = Graph.addNode(mutable, "leaf1") // 3 + const leaf2 = Graph.addNode(mutable, "leaf2") // 4 + + Graph.addEdge(mutable, root, left, 1) + Graph.addEdge(mutable, root, right, 2) + Graph.addEdge(mutable, left, leaf1, 3) + Graph.addEdge(mutable, right, leaf2, 4) + }) + + const postOrder = Array.from(Graph.indices(Graph.dfsPostOrder(graph, { start: [0] }))) + // Should visit leaves first, then parents + expect(postOrder).toEqual([3, 1, 4, 2, 0]) + }) + + it("should handle empty start nodes", () => { + const graph = Graph.directed((mutable) => { + Graph.addNode(mutable, "A") + }) + + const postOrder = Array.from(Graph.dfsPostOrder(graph, { start: [] })) + expect(postOrder).toEqual([]) + }) + + it("should handle disconnected components with multiple start nodes", () => { + const graph = Graph.directed((mutable) => { + const a = Graph.addNode(mutable, "A") + const b = Graph.addNode(mutable, "B") + const c = Graph.addNode(mutable, "C") + const d = Graph.addNode(mutable, "D") + + Graph.addEdge(mutable, a, b, 1) + Graph.addEdge(mutable, c, d, 2) + // No connection between (A,B) and (C,D) + }) + + const postOrder = Array.from(Graph.indices(Graph.dfsPostOrder(graph, { start: [0, 2] }))) + expect(postOrder).toEqual([1, 0, 3, 2]) // Each component in postorder + }) + + it("should support incoming direction", () => { + const graph = Graph.directed((mutable) => { + const a = Graph.addNode(mutable, "A") + const b = Graph.addNode(mutable, "B") + const c = Graph.addNode(mutable, "C") + Graph.addEdge(mutable, a, b, 1) + Graph.addEdge(mutable, b, c, 2) + }) + + // Starting from C, going backwards + const postOrder = Array.from( + Graph.indices(Graph.dfsPostOrder(graph, { + start: [2], + direction: "incoming" + })) + ) + expect(postOrder).toEqual([0, 1, 2]) // A, B, C in reverse postorder + }) + + it("should handle cycles correctly", () => { + const graph = Graph.directed((mutable) => { + const a = Graph.addNode(mutable, "A") + const b = Graph.addNode(mutable, "B") + const c = Graph.addNode(mutable, "C") + Graph.addEdge(mutable, a, b, 1) + Graph.addEdge(mutable, b, c, 2) + Graph.addEdge(mutable, c, a, 3) // Creates cycle + }) + + const postOrder = Array.from(Graph.indices(Graph.dfsPostOrder(graph, { start: [0] }))) + // Should handle cycle without infinite loop, visiting each node once + expect(postOrder.length).toBe(3) + expect(new Set(postOrder)).toEqual(new Set([0, 1, 2])) + }) + + it("should throw error for non-existent start node", () => { + const graph = Graph.directed((mutable) => { + Graph.addNode(mutable, "A") + }) + + expect(() => Graph.dfsPostOrder(graph, { start: [99] })) + .toThrow("Node 99 does not exist") + }) + + it("should be iterable multiple times with fresh state", () => { + const graph = Graph.directed((mutable) => { + const a = Graph.addNode(mutable, "A") + const b = Graph.addNode(mutable, "B") + Graph.addEdge(mutable, a, b, 1) + }) + + const iterator = Graph.dfsPostOrder(graph, { start: [0] }) + + const firstRun = Array.from(Graph.indices(iterator)) + const secondRun = Array.from(Graph.indices(iterator)) + + expect(firstRun).toEqual([1, 0]) + expect(secondRun).toEqual([1, 0]) + expect(firstRun).toEqual(secondRun) + }) + + it("should handle corrupted graph state during iteration", () => { + const graph = Graph.directed((mutable) => { + const a = Graph.addNode(mutable, "A") + const b = Graph.addNode(mutable, "B") + Graph.addEdge(mutable, a, b, 1) + }) + + // Test edge case by corrupting graph internals during iteration + const mutableGraph = graph as any + const originalGetNode = mutableGraph.nodes.get + + let callCount = 0 + // Mock getNode to return undefined for certain calls to trigger the recursive edge case + mutableGraph.nodes.get = function(key: any) { + callCount++ + // On specific call, return undefined to trigger the Option.isNone path + if (callCount === 3) { + return undefined + } + return originalGetNode.call(this, key) + } + + const iterator = Graph.dfsPostOrder(graph, { start: [0] }) + const results = Array.from(iterator) + + // Restore original method + mutableGraph.nodes.get = originalGetNode + + // Should complete without crashing + expect(results.length).toBeGreaterThanOrEqual(0) + }) + }) + + describe("Graph Element Iterators", () => { + describe("nodes", () => { + it("should iterate over all node indices", () => { + const graph = Graph.directed((mutable) => { + Graph.addNode(mutable, "A") + Graph.addNode(mutable, "B") + Graph.addNode(mutable, "C") + }) + + const indices = Array.from(Graph.indices(Graph.nodes(graph))) + expect(indices).toEqual([0, 1, 2]) + }) + + it("should work with manual iterator control", () => { + const graph = Graph.directed((mutable) => { + Graph.addNode(mutable, "A") + Graph.addNode(mutable, "B") + }) + + const iterator = Graph.indices(Graph.nodes(graph))[Symbol.iterator]() + expect(iterator.next().value).toBe(0) + expect(iterator.next().value).toBe(1) + expect(iterator.next().done).toBe(true) + }) + }) + + describe("edges", () => { + it("should iterate over all edge indices", () => { + const graph = Graph.directed((mutable) => { + const a = Graph.addNode(mutable, "A") + const b = Graph.addNode(mutable, "B") + const c = Graph.addNode(mutable, "C") + Graph.addEdge(mutable, a, b, 1) + Graph.addEdge(mutable, b, c, 2) + Graph.addEdge(mutable, c, a, 3) + }) + + const indices = Array.from(Graph.indices(Graph.edges(graph))) + expect(indices).toEqual([0, 1, 2]) + }) + + it("should handle graph with no edges", () => { + const graph = Graph.directed((mutable) => { + Graph.addNode(mutable, "A") + Graph.addNode(mutable, "B") + }) + + const indices = Array.from(Graph.indices(Graph.edges(graph))) + expect(indices).toEqual([]) + }) + }) + + describe("externals", () => { + it("should find nodes with no outgoing edges (sinks)", () => { + const graph = Graph.directed((mutable) => { + const source = Graph.addNode(mutable, "source") // 0 + const middle = Graph.addNode(mutable, "middle") // 1 + const sink = Graph.addNode(mutable, "sink") // 2 + Graph.addNode(mutable, "isolated") // 3 + + Graph.addEdge(mutable, source, middle, 1) + Graph.addEdge(mutable, middle, sink, 2) + // No outgoing edges from sink (2) or isolated (3) + }) + + const sinks = Array.from(Graph.indices(Graph.externals(graph, { direction: "outgoing" }))) + expect(sinks.sort()).toEqual([2, 3]) + }) + + it("should find nodes with no incoming edges (sources)", () => { + const graph = Graph.directed((mutable) => { + const source = Graph.addNode(mutable, "source") // 0 + const middle = Graph.addNode(mutable, "middle") // 1 + const sink = Graph.addNode(mutable, "sink") // 2 + Graph.addNode(mutable, "isolated") // 3 + + Graph.addEdge(mutable, source, middle, 1) + Graph.addEdge(mutable, middle, sink, 2) + // No incoming edges to source (0) or isolated (3) + }) + + const sources = Array.from(Graph.indices(Graph.externals(graph, { direction: "incoming" }))) + expect(sources.sort()).toEqual([0, 3]) + }) + + it("should default to outgoing direction", () => { + const graph = Graph.directed((mutable) => { + const a = Graph.addNode(mutable, "A") + const b = Graph.addNode(mutable, "B") + Graph.addEdge(mutable, a, b, 1) + // b has no outgoing edges + }) + + const externalsDefault = Array.from(Graph.indices(Graph.externals(graph))) + const externalsExplicit = Array.from(Graph.indices(Graph.externals(graph, { direction: "outgoing" }))) + + expect(externalsDefault).toEqual(externalsExplicit) + expect(externalsDefault).toEqual([1]) + }) + + it("should handle fully connected components", () => { + const graph = Graph.directed((mutable) => { + const a = Graph.addNode(mutable, "A") + const b = Graph.addNode(mutable, "B") + const c = Graph.addNode(mutable, "C") + Graph.addEdge(mutable, a, b, 1) + Graph.addEdge(mutable, b, c, 2) + Graph.addEdge(mutable, c, a, 3) // Creates cycle + }) + + const outgoingExternals = Array.from(Graph.indices(Graph.externals(graph, { direction: "outgoing" }))) + const incomingExternals = Array.from(Graph.indices(Graph.externals(graph, { direction: "incoming" }))) + + expect(outgoingExternals).toEqual([]) // All nodes have outgoing edges + expect(incomingExternals).toEqual([]) // All nodes have incoming edges + }) + + it("should work with manual iterator control", () => { + const graph = Graph.directed((mutable) => { + const a = Graph.addNode(mutable, "A") + const b = Graph.addNode(mutable, "B") + Graph.addNode(mutable, "C") + Graph.addEdge(mutable, a, b, 1) + // b and c have no outgoing edges + }) + + const iterator = Graph.indices(Graph.externals(graph, { direction: "outgoing" }))[Symbol.iterator]() + + const first = iterator.next().value + const second = iterator.next().value + const third = iterator.next() + + expect([first, second].sort()).toEqual([1, 2]) + expect(third.done).toBe(true) + }) + }) + + it("should allow combining different element iterators", () => { + const graph = Graph.directed((mutable) => { + const a = Graph.addNode(mutable, "A") + const b = Graph.addNode(mutable, "B") + Graph.addEdge(mutable, a, b, 100) + }) + + // Combine different iterators + const nodeCount = Array.from(Graph.indices(Graph.nodes(graph))).length + const edgeCount = Array.from(Graph.indices(Graph.edges(graph))).length + const nodeData = Array.from(Graph.values(Graph.nodes(graph))) + const edge = Array.from(Graph.values(Graph.edges(graph))) + + expect(nodeCount).toBe(2) + expect(edgeCount).toBe(1) + expect(nodeData).toEqual(["A", "B"]) + expect(edge).toEqual([{ source: 0, target: 1, data: 100 }]) + }) + }) + + describe("GraphIterable abstraction", () => { + it("should enable iteration over different types", () => { + const graph = Graph.directed((mutable) => { + const a = Graph.addNode(mutable, "A") + const b = Graph.addNode(mutable, "B") + const c = Graph.addNode(mutable, "C") + Graph.addEdge(mutable, a, b, 1) + Graph.addEdge(mutable, b, c, 2) + }) + + // Should work with different iterator types + const dfsIterable = Graph.dfs(graph, { start: [0] }) + const nodesIterable = Graph.nodes(graph) + const externalsIterable = Graph.externals(graph) + + // All should be iterable and have expected structure + expect(Array.from(dfsIterable)).toHaveLength(3) + expect(Array.from(nodesIterable)).toHaveLength(3) + expect(Array.from(externalsIterable)).toHaveLength(1) // Only one node with no outgoing edges + }) + }) + + describe("NodeIterable abstraction", () => { + it("should provide common interface for node index iterables", () => { + const graph = Graph.directed((mutable) => { + const a = Graph.addNode(mutable, "A") + const b = Graph.addNode(mutable, "B") + const c = Graph.addNode(mutable, "C") + Graph.addEdge(mutable, a, b, 1) + Graph.addEdge(mutable, b, c, 2) + }) + + // Utility function that works with any NodeWalker + function collectNodes( + nodeIterable: Graph.NodeWalker + ): Array { + return Array.from(Graph.indices(nodeIterable)).sort() + } + + // Both traversal and element iterators implement NodeWalker + const dfsNodes = Graph.dfs(graph, { start: [0] }) + const allNodes = Graph.nodes(graph) + const externalNodes = Graph.externals(graph, { direction: "outgoing" }) + + // All should work with the same utility function + expect(collectNodes(dfsNodes)).toEqual([0, 1, 2]) + expect(collectNodes(allNodes)).toEqual([0, 1, 2]) + expect(collectNodes(externalNodes)).toEqual([2]) // Only node 2 has no outgoing edges + }) + + it("should allow type-safe node iterable operations", () => { + const graph = Graph.directed((mutable) => { + const a = Graph.addNode(mutable, "A") + const b = Graph.addNode(mutable, "B") + Graph.addEdge(mutable, a, b, 1) + }) + + const nodeIterable: Graph.NodeWalker = Graph.nodes(graph) + const traversalIterable: Graph.NodeWalker = Graph.dfs(graph, { + start: [0] + }) + + expect(Array.from(Graph.indices(nodeIterable))).toEqual([0, 1]) + expect(Array.from(Graph.indices(traversalIterable))).toEqual([0, 1]) + }) + }) + + describe("Standalone utility functions", () => { + it("should work with values() function on any NodeIterable", () => { + const graph = Graph.directed((mutable) => { + const a = Graph.addNode(mutable, "A") + const b = Graph.addNode(mutable, "B") + const c = Graph.addNode(mutable, "C") + Graph.addEdge(mutable, a, b, 1) + Graph.addEdge(mutable, b, c, 2) + }) + + // Test with traversal iterators + const dfsIterable = Graph.dfs(graph, { start: [0] }) + const dfsValues = Array.from(Graph.values(dfsIterable)) + expect(dfsValues).toEqual(["A", "B", "C"]) + + // Test with element iterators + const nodesIterable = Graph.nodes(graph) + const nodeValues = Array.from(Graph.values(nodesIterable)) + expect(nodeValues.sort()).toEqual(["A", "B", "C"]) + + // Test with externals iterator + const externalsIterable = Graph.externals(graph, { direction: "outgoing" }) + const externalValues = Array.from(Graph.values(externalsIterable)) + expect(externalValues).toEqual(["C"]) // Only C has no outgoing edges + }) + + it("should work with entries() function on any NodeIterable", () => { + const graph = Graph.directed((mutable) => { + const a = Graph.addNode(mutable, "A") + const b = Graph.addNode(mutable, "B") + Graph.addEdge(mutable, a, b, 1) + }) + + // Test with traversal iterator + const dfsIterable = Graph.dfs(graph, { start: [0] }) + const dfsEntries = Array.from(Graph.entries(dfsIterable)) + expect(dfsEntries).toEqual([[0, "A"], [1, "B"]]) + + // Test with element iterator + const nodesIterable = Graph.nodes(graph) + const nodeEntries = Array.from(Graph.entries(nodesIterable)) + expect(nodeEntries.sort()).toEqual([[0, "A"], [1, "B"]]) + + // Test with externals iterator + const externalsIterable = Graph.externals(graph, { direction: "outgoing" }) + const externalEntries = Array.from(Graph.entries(externalsIterable)) + expect(externalEntries).toEqual([[1, "B"]]) // Only B has no outgoing edges + }) + + it("should work with instance methods", () => { + const graph = Graph.directed((mutable) => { + const a = Graph.addNode(mutable, "A") + const b = Graph.addNode(mutable, "B") + Graph.addEdge(mutable, a, b, 1) + }) + + const dfs = Graph.dfs(graph, { start: [0] }) + + // Instance methods should work + const instanceValues = Array.from(Graph.values(dfs)) + const instanceEntries = Array.from(Graph.entries(dfs)) + + expect(instanceValues).toEqual(["A", "B"]) + expect(instanceEntries).toEqual([[0, "A"], [1, "B"]]) + }) + + it("should work with mapEntry for NodeIterable", () => { + const graph = Graph.directed((mutable) => { + const a = Graph.addNode(mutable, "A") + const b = Graph.addNode(mutable, "B") + Graph.addEdge(mutable, a, b, 1) + }) + + const dfs = Graph.dfs(graph, { start: [0] }) + + // Test mapEntry with custom mapping + const custom = Array.from(dfs.visit((index, data) => ({ id: index, name: data }))) + expect(custom).toEqual([{ id: 0, name: "A" }, { id: 1, name: "B" }]) + + // Test that values() is implemented using mapEntry + const values = Array.from(Graph.values(dfs)) + expect(values).toEqual(["A", "B"]) + + // Test that entries() is implemented using mapEntry + const entries = Array.from(Graph.entries(dfs)) + expect(entries).toEqual([[0, "A"], [1, "B"]]) + }) + + it("should work with mapEntry for EdgeIterable", () => { + const graph = Graph.directed((mutable) => { + const a = Graph.addNode(mutable, "A") + const b = Graph.addNode(mutable, "B") + Graph.addEdge(mutable, a, b, 42) + }) + + const edgesIterable = Graph.edges(graph) + + // Test mapEntry with custom mapping + const connections = Array.from(edgesIterable.visit((index, edge) => ({ + id: index, + from: edge.source, + to: edge.target, + weight: edge.data + }))) + expect(connections).toEqual([{ id: 0, from: 0, to: 1, weight: 42 }]) + + // Test that values() is implemented using mapEntry + const weights = Array.from(edgesIterable.visit((_, edge) => edge.data)) + expect(weights).toEqual([42]) + + // Test that entries() is implemented using mapEntry + const entries = Array.from(Graph.entries(edgesIterable)) + expect(entries).toEqual([[0, { source: 0, target: 1, data: 42 }]]) + }) + }) + }) +}) diff --git a/repos/effect/packages/effect/test/Hash.test.ts b/repos/effect/packages/effect/test/Hash.test.ts new file mode 100644 index 0000000..155f408 --- /dev/null +++ b/repos/effect/packages/effect/test/Hash.test.ts @@ -0,0 +1,80 @@ +import { describe, expect, it } from "@effect/vitest" +import { assertFalse, assertTrue, strictEqual } from "@effect/vitest/utils" +import { absurd, Equal, Hash, HashSet, identity, Option, Utils } from "effect" + +describe("Hash", () => { + it("structural", () => { + const a = { foo: { bar: "ok", baz: { arr: [0, 1, 2] } } } + const b = { foo: { bar: "ok", baz: { arr: [0, 1, 2] } } } + assertTrue(Hash.hash(a) !== Hash.hash(b)) + assertFalse(Equal.equals(a, b)) + Utils.structuralRegion(() => { + strictEqual(Hash.hash(a), Hash.hash(b)) + assertTrue(Equal.equals(a, b)) + }) + assertTrue(Hash.hash(a) !== Hash.hash(b)) + assertFalse(Equal.equals(a, b)) + }) + it("structural cached", () => { + const a = Option.some({ foo: { bar: "ok", baz: { arr: [0, 1, 2] } } }) + const b = Option.some({ foo: { bar: "ok", baz: { arr: [0, 1, 2] } } }) + assertTrue(Hash.hash(a) !== Hash.hash(b)) + assertFalse(Equal.equals(a, b)) + Utils.structuralRegion(() => { + strictEqual(Hash.hash(a), Hash.hash(b)) + assertTrue(Equal.equals(a, b)) + }) + assertTrue(Hash.hash(a) !== Hash.hash(b)) + assertFalse(Equal.equals(a, b)) + }) + + it("number", () => { + const set: HashSet.HashSet = HashSet.make(Infinity) + assertTrue(HashSet.has(set, Infinity)) + assertFalse(HashSet.has(set, -Infinity)) + assertTrue(Hash.number(0.1) !== Hash.number(0)) + }) + + it("bigint", () => { + const set = HashSet.make(1n) + assertTrue(HashSet.has(set, 1n)) + assertFalse(HashSet.has(set, 2n)) + }) + + it("symbol", () => { + const a = Symbol.for("effect/test/Hash/a") + const b = Symbol.for("effect/test/Hash/b") + const set: HashSet.HashSet = HashSet.make(a) + assertTrue(HashSet.has(set, a)) + assertFalse(HashSet.has(set, b)) + }) + + it("undefined", () => { + const set: HashSet.HashSet = HashSet.make(1, undefined) + assertTrue(HashSet.has(set, undefined)) + assertFalse(HashSet.has(set, 2)) + }) + + it("null", () => { + const set: HashSet.HashSet = HashSet.make(1, null) + assertTrue(HashSet.has(set, null)) + assertFalse(HashSet.has(set, 2)) + }) + + it("function", () => { + const set: HashSet.HashSet = HashSet.make(identity) + assertTrue(HashSet.has(set, identity)) + assertFalse(HashSet.has(set, absurd)) + }) + + it("isHash", () => { + assertTrue(Hash.isHash(HashSet.empty())) + assertFalse(Hash.isHash(null)) + assertFalse(Hash.isHash({})) + }) + + it("invalid Date", () => { + const invalidDate = new Date("invalid") + expect(() => Hash.hash(invalidDate)).not.toThrow() + }) +}) diff --git a/repos/effect/packages/effect/test/HashMap.test.ts b/repos/effect/packages/effect/test/HashMap.test.ts new file mode 100644 index 0000000..983f15f --- /dev/null +++ b/repos/effect/packages/effect/test/HashMap.test.ts @@ -0,0 +1,477 @@ +import { describe, it } from "@effect/vitest" +import { + assertFalse, + assertNone, + assertSome, + assertTrue, + deepStrictEqual, + strictEqual, + throws +} from "@effect/vitest/utils" +import { Equal, Hash, HashMap as HM, Option, pipe } from "effect" + +class Key implements Equal.Equal { + constructor(readonly n: number) {} + + [Hash.symbol](): number { + return Hash.hash(this.n) + } + + [Equal.symbol](u: unknown): boolean { + return u instanceof Key && this.n === u.n + } +} + +class Value implements Equal.Equal { + constructor(readonly s: string) {} + + [Hash.symbol](): number { + return Hash.hash(this.s) + } + + [Equal.symbol](u: unknown): boolean { + return u instanceof Value && this.s === u.s + } +} + +describe("HashMap", () => { + function key(n: number): Key { + return new Key(n) + } + + function value(s: string): Value { + return new Value(s) + } + + it("option", () => { + const map = HM.make([Option.some(1), 0], [Option.none(), 1]) + assertTrue(pipe(map, HM.has(Option.none()))) + assertTrue(pipe(map, HM.has(Option.some(1)))) + assertFalse(pipe(map, HM.has(Option.some(2)))) + }) + + it("toString", () => { + const map = HM.make([0, "a"]) + strictEqual( + String(map), + `{ + "_id": "HashMap", + "values": [ + [ + 0, + "a" + ] + ] +}` + ) + }) + + it("toJSON", () => { + const map = HM.make([0, "a"]) + deepStrictEqual(map.toJSON(), { _id: "HashMap", values: [[0, "a"]] }) + }) + + it("inspect", () => { + if (typeof window === "undefined") { + // eslint-disable-next-line @typescript-eslint/no-require-imports + const { inspect } = require("node:util") + const map = HM.make([0, "a"]) + deepStrictEqual(inspect(map), inspect({ _id: "HashMap", values: [[0, "a"]] })) + } + }) + + it("has", () => { + const map = HM.make([key(0), value("a")]) + + assertTrue(HM.has(key(0))(map)) + assertFalse(HM.has(key(1))(map)) + }) + + it("hasHash", () => { + const map = HM.make([key(0), value("a")]) + + assertTrue(HM.hasHash(key(0), Hash.hash(key(0)))(map)) + assertFalse(HM.hasHash(key(1), Hash.hash(key(0)))(map)) + }) + + it("hasBy", () => { + const map = HM.make([key(0), value("a")]) + + assertTrue(HM.hasBy(map, (v) => Equal.equals(v, value("a")))) + assertTrue(HM.hasBy(map, (_, k) => Equal.equals(k, key(0)))) + assertTrue(pipe(map, HM.hasBy((v) => Equal.equals(v, value("a"))))) + assertFalse(HM.hasBy(map, (v) => Equal.equals(v, value("b")))) + assertFalse(HM.hasBy(map, (_, k) => Equal.equals(k, key(1)))) + assertFalse(pipe(map, HM.hasBy((v) => Equal.equals(v, value("b"))))) + }) + + it("get", () => { + const map = HM.make([key(0), value("a")]) + + assertSome(HM.get(key(0))(map), value("a")) + assertNone(HM.get(key(1))(map)) + }) + + it("getHash", () => { + const map = HM.make([key(0), value("a")]) + + assertSome(HM.getHash(key(0), Hash.hash(0))(map), value("a")) + assertNone(HM.getHash(key(1), Hash.hash(0))(map)) + }) + + it("set", () => { + const map = pipe(HM.empty(), HM.set(key(0), value("a"))) + + assertSome(HM.get(key(0))(map), value("a")) + }) + + it("mutation", () => { + let map: any = HM.empty() + + assertFalse(map._editable) + map = HM.beginMutation(map) + assertTrue(map._editable) + map = HM.endMutation(map) + assertFalse(map._editable) + }) + + it("mutate", () => { + const map = HM.empty() + const result = pipe( + map, + HM.mutate((map) => { + pipe(map, HM.set(0, "a")) + }) + ) + + assertSome(HM.get(0)(result), "a") + assertNone(HM.get(1)(result)) + }) + + it("flatMap", () => { + const map1 = HM.make([key(0), value("a")], [key(1), value("bb")]) + const result1 = pipe( + map1, + HM.flatMap(({ s }) => { + const newKey = key(s.length) + const newValue = value(s) + return pipe(HM.empty(), HM.set(newKey, newValue)) + }) + ) + + assertSome(HM.get(key(1))(result1), value("a")) + assertSome(HM.get(key(2))(result1), value("bb")) + assertNone(HM.get(key(3))(result1)) + + const map2 = HM.make([key(1), value("a")], [key(2), value("bb")]) + const result2 = pipe( + map2, + HM.flatMap(({ s }, { n }) => { + const newKey = key(s.length + n) + const newValue = value(s) + return pipe(HM.empty(), HM.set(newKey, newValue)) + }) + ) + + assertSome(HM.get(key(2))(result2), value("a")) + assertSome(HM.get(key(4))(result2), value("bb")) + assertNone(HM.get(key(6))(result2)) + }) + + it("filterMap", () => { + const map1 = HM.make([key(0), value("a")], [key(1), value("bb")]) + const result1 = pipe( + map1, + HM.filterMap(({ s }) => s.length > 1 ? Option.some(value(s)) : Option.none()) + ) + + assertNone(HM.get(key(0))(result1)) + assertSome(HM.get(key(1))(result1), value("bb")) + + const map2 = HM.make([key(0), value("a")], [key(1), value("bb")]) + const result2 = pipe( + map2, + HM.filterMap((v, { n }) => n > 0 ? Option.some(v) : Option.none()) + ) + + assertNone(HM.get(key(0))(result2)) + assertSome(HM.get(key(1))(result2), value("bb")) + }) + + it("compact", () => { + const map = HM.make([0, Option.some("a")], [1, Option.none()]) + const result = HM.compact(map) + + strictEqual(HM.unsafeGet(0)(result), "a") + throws(() => HM.unsafeGet(1)(result)) + }) + + it("filter", () => { + const map1 = HM.make([key(0), value("a")], [key(1), value("bb")]) + const result1 = pipe(map1, HM.filter(({ s }) => s.length > 1)) + + assertNone(HM.get(key(0))(result1)) + assertSome(HM.get(key(1))(result1), value("bb")) + + const map2 = HM.make([key(0), value("a")], [key(1), value("bb")]) + const result2 = pipe(map2, HM.filter(({ s }, { n }) => n > 0 && s.length > 0)) + + assertNone(HM.get(key(0))(result2)) + assertSome(HM.get(key(1))(result2), value("bb")) + }) + + it("forEach", () => { + const map1 = HM.make([key(0), value("a")], [key(1), value("b")]) + const result1: Array = [] + pipe( + map1, + HM.forEach((v) => { + result1.push(v.s) + }) + ) + + deepStrictEqual(result1, ["a", "b"]) + + const map2 = HM.make([key(0), value("a")], [key(1), value("b")]) + const result2: Array = [] + pipe( + map2, + HM.forEach(({ s }, { n }) => { + result2.push([n, s]) + }) + ) + + deepStrictEqual(result2, [[0, "a"], [1, "b"]]) + }) + + it("isEmpty", () => { + assertTrue(HM.isEmpty(HM.make())) + assertFalse(HM.isEmpty(HM.make([key(0), value("a")]))) + }) + + it("map", () => { + const map1 = HM.make([key(0), value("a")], [key(1), value("bb")]) + const result1 = pipe(map1, HM.map(({ s }) => s.length)) + + assertSome(HM.get(key(0))(result1), 1) + assertSome(HM.get(key(1))(result1), 2) + assertNone(HM.get(key(2))(result1)) + + const map2 = HM.make([key(0), value("a")], [key(1), value("bb")]) + const result2 = pipe(map2, HM.map(({ s }, { n }) => n + s.length)) + + assertSome(HM.get(key(0))(result2), 1) + assertSome(HM.get(key(1))(result2), 3) + assertNone(HM.get(key(2))(result2)) + }) + + it("modifyAt", () => { + const map = HM.make([key(0), value("a")], [key(1), value("b")]) + const result = pipe( + map, + HM.modifyAt(key(0), (maybe) => + Option.isSome(maybe) ? + Option.some(value("test")) : + Option.none()) + ) + + assertSome(HM.get(key(0))(result), value("test")) + assertSome(HM.get(key(1))(result), value("b")) + assertNone(HM.get(key(2))(result)) + + assertNone( + HM.get(key(0))(pipe( + map, + HM.modifyAt(key(0), (): Option.Option => Option.none()) + )) + ) + }) + + it("modifyHash", () => { + const map = HM.make([key(0), value("a")], [key(1), value("b")]) + const result = pipe( + map, + HM.modifyHash(key(0), Hash.hash(key(0)), (maybe) => + Option.isSome(maybe) ? + Option.some(value("test")) : + Option.none()) + ) + + assertSome(HM.get(key(0))(result), value("test")) + assertSome(HM.get(key(1))(result), value("b")) + assertNone(HM.get(key(2))(result)) + }) + + it("some", () => { + const mapWith3LettersMax = HM.make([0, "a"], [1, "bb"], [3, "ccc"]) + + deepStrictEqual(HM.some(mapWith3LettersMax, (value) => value.length > 3), false) + deepStrictEqual(pipe(mapWith3LettersMax, HM.some((value) => value.length > 3)), false) + + deepStrictEqual(HM.some(mapWith3LettersMax, (value) => value.length > 1), true) + + deepStrictEqual(HM.some(mapWith3LettersMax, (value, key) => value.length > 1 && key === 0), false) + + deepStrictEqual(HM.some(mapWith3LettersMax, (value, key) => value.length > 1 && key === 1), true) + }) + + it("every", () => { + const mapWith3LettersMax = HM.make([0, "a"], [1, "bb"], [3, "ccc"]) + + deepStrictEqual(HM.every(mapWith3LettersMax, (value) => value.length > 2), false) + deepStrictEqual(pipe(mapWith3LettersMax, HM.every((value) => value.length > 2)), false) + + deepStrictEqual(HM.every(mapWith3LettersMax, (value) => value.length >= 1), true) + + deepStrictEqual(HM.every(mapWith3LettersMax, (value, key) => value.length >= 1 && key === 0), false) + + deepStrictEqual(HM.every(mapWith3LettersMax, (value, key) => value.length >= 1 && key >= 0), true) + }) + + it("reduce", () => { + const map1 = HM.make([key(0), value("a")], [key(1), value("b")]) + const result1 = pipe(map1, HM.reduce("", (acc, { s }) => acc.length > 0 ? `${acc},${s}` : s)) + + strictEqual(result1, "a,b") + + const map2 = HM.make([key(0), value("a")], [key(1), value("b")]) + const result2 = pipe( + map2, + HM.reduce( + "", + (acc, { s }, { n }) => acc.length > 0 ? `${acc},${n}:${s}` : `${n}:${s}` + ) + ) + + strictEqual(result2, "0:a,1:b") + }) + + it("remove", () => { + const map = HM.make([key(0), value("a")], [key(1), value("b")]) + const result = pipe(map, HM.remove(key(0))) + + assertNone(HM.get(key(0))(result)) + assertSome(HM.get(key(1))(result), value("b")) + }) + + it("remove non existing key doesn't change the array", () => { + const map = HM.make([13, 95], [90, 4]) + const result = pipe(map, HM.remove(75)) + + deepStrictEqual(Array.from(HM.keySet(map)), Array.from(HM.keySet(result))) + }) + + it("removeMany", () => { + const map = HM.make([key(0), value("a")], [key(1), value("b")]) + const result = pipe(map, HM.removeMany([key(0), key(1)])) + + assertFalse(HM.isEmpty(map)) + assertTrue(HM.isEmpty(result)) + }) + + it("size", () => { + const map = HM.make([key(0), value("a")], [key(1), value("b")]) + const result = HM.size(map) + + strictEqual(result, 2) + }) + + it("union", () => { + const map1 = HM.make([0, "a"], [1, "b"]) + const map2 = HM.make(["foo", true], ["bar", false]) + const result = HM.union(map2)(map1) + + assertSome(pipe(result, HM.get(0)), "a") + assertSome(pipe(result, HM.get(1)), "b") + assertSome(pipe(result, HM.get("foo")), true) + assertSome(pipe(result, HM.get("bar")), false) + }) + + it("modify", () => { + const map = HM.make([key(0), value("a")], [key(1), value("b")]) + const result = pipe(map, HM.modify(key(0), ({ s }) => value(`${s}-${s}`))) + + assertSome(HM.get(key(0))(result), value("a-a")) + assertSome(HM.get(key(1))(result), value("b")) + assertNone(HM.get(key(2))(result)) + + assertNone( + HM.get(key(2))(pipe(map, HM.modify(key(2), ({ s }) => value(`${s}-${s}`)))) + ) + }) + + it("keys", () => { + const map = HM.make([0, "a"], [1, "b"]) + const result = Array.from(HM.keys(map)) + + deepStrictEqual(result, [0, 1]) + }) + + it("keySet", () => { + const hashMap = HM.make( + [key(0), value("a")], + [key(1), value("b")], + [key(1), value("c")] + ) + + const result = HM.keySet(hashMap) + + deepStrictEqual([...result], [key(0), key(1)]) + }) + + it("values", () => { + const map = HM.make([key(0), value("a")], [key(1), value("b")]) + const result = Array.from(HM.values(map)) + + deepStrictEqual(result, [value("a"), value("b")]) + }) + + it("toValues", () => { + const map = HM.make([key(0), value("a")], [key(1), value("b")]) + const result = HM.toValues(map) + + deepStrictEqual(result, [value("a"), value("b")]) + }) + + it("entries", () => { + const map = HM.make([key(0), value("a")], [key(1), value("b")]) + const result = Array.from(HM.entries(map)) + + deepStrictEqual(result, [[key(0), value("a")], [key(1), value("b")]]) + }) + + it("toEntries", () => { + const map = HM.make([key(0), value("a")], [key(1), value("b")]) + const result = HM.toEntries(map) + + deepStrictEqual(result, [[key(0), value("a")], [key(1), value("b")]]) + }) + + it("pipe()", () => { + strictEqual(HM.empty().pipe(HM.set("key", "value")).pipe(HM.size), HM.make(["key", "value"]).pipe(HM.size)) + }) + + it("isHashMap", () => { + assertTrue(HM.isHashMap(HM.empty())) + assertFalse(HM.isHashMap(null)) + assertFalse(HM.isHashMap({})) + }) + + it("findFirst", () => { + const map = HM.make([key(0), value("a")], [key(1), value("bb")]) + assertSome(HM.findFirst(map, (_v, k) => k.n === 0), [key(0), value("a")]) + assertSome(HM.findFirst(map, (v, _k) => v.s === "bb"), [key(1), value("bb")]) + assertNone(HM.findFirst(map, (v, k) => k.n === 0 && v.s === "bb")) + }) + + it("countBy", () => { + const map = HM.make([key(1), value("a")], [key(2), value("b")], [key(3), value("c")]) + strictEqual(HM.countBy(map, (_v, k) => k.n % 2 === 1), 2) + strictEqual(HM.countBy(map, (v, k) => k.n % 2 === 1 && v.s === "a"), 1) + strictEqual(HM.countBy(map, (v, k) => k.n % 2 === 1 && v.s === "b"), 0) + + strictEqual(pipe(map, HM.countBy((_v, k) => k.n % 2 === 1)), 2) + strictEqual(pipe(map, HM.countBy((v, k) => k.n % 2 === 1 && v.s === "a")), 1) + strictEqual(pipe(map, HM.countBy((v, k) => k.n % 2 === 1 && v.s === "b")), 0) + }) +}) diff --git a/repos/effect/packages/effect/test/HashSet.test.ts b/repos/effect/packages/effect/test/HashSet.test.ts new file mode 100644 index 0000000..153e907 --- /dev/null +++ b/repos/effect/packages/effect/test/HashSet.test.ts @@ -0,0 +1,243 @@ +import { describe, it } from "@effect/vitest" +import { assertFalse, assertTrue, deepStrictEqual, strictEqual } from "@effect/vitest/utils" +import { Equal, Hash, HashSet, pipe } from "effect" + +class Value implements Equal.Equal { + constructor(readonly n: number) {} + + [Hash.symbol](): number { + return Hash.hash(this.n) + } + + [Equal.symbol](u: unknown): boolean { + return u instanceof Value && this.n === u.n + } +} + +describe("HashSet", () => { + function value(n: number): Value { + return new Value(n) + } + + function makeTestHashSet(...values: Array): HashSet.HashSet { + return HashSet.mutate((set) => { + for (const _value of values) { + HashSet.add(value(_value))(set) + } + })(HashSet.empty()) + } + + it("toString", () => { + const map = HashSet.make(0, "a") + strictEqual( + String(map), + `{ + "_id": "HashSet", + "values": [ + 0, + "a" + ] +}` + ) + }) + + it("toJSON", () => { + const map = HashSet.make(0, "a") + deepStrictEqual(map.toJSON(), { _id: "HashSet", values: [0, "a"] }) + }) + + it("inspect", () => { + if (typeof window !== "undefined") { + return + } + // eslint-disable-next-line @typescript-eslint/no-require-imports + const { inspect } = require("node:util") + const map = HashSet.make(0, "a") + deepStrictEqual(inspect(map), inspect({ _id: "HashSet", values: [0, "a"] })) + }) + + it("add", () => { + const set = makeTestHashSet(0, 1, 2) + + deepStrictEqual(set, HashSet.make(value(0), value(1), value(2))) + }) + + it("mutation", () => { + let set: any = HashSet.empty() + + assertFalse(set._keyMap._editable) + set = HashSet.beginMutation(set) + assertTrue(set._keyMap._editable) + set = HashSet.endMutation(set) + assertFalse(set._keyMap._editable) + }) + + it("flatMap", () => { + const set = makeTestHashSet(0, 1, 2) + const result = pipe(set, HashSet.flatMap((v) => [`${v.n}`])) + + deepStrictEqual(result, HashSet.make("0", "1", "2")) + }) + + it("difference", () => { + const set1 = makeTestHashSet(0, 1, 2) + const set2 = makeTestHashSet(2, 3, 4) + const result = pipe(set1, HashSet.difference(set2)) + + assertTrue(Equal.equals(result, HashSet.make(value(0), value(1)))) + }) + + it("every", () => { + const set = makeTestHashSet(0, 1, 2) + + assertTrue(pipe(set, HashSet.every(({ n }) => n >= 0))) + assertFalse(pipe(set, HashSet.every(({ n }) => n > 0))) + }) + + it("filter", () => { + const set = makeTestHashSet(0, 1, 2) + const result = pipe(set, HashSet.filter(({ n }) => n > 0)) + + deepStrictEqual(result, HashSet.make(value(1), value(2))) + }) + + it("forEach", () => { + const set = makeTestHashSet(0, 1, 2) + const result: Array = [] + + pipe( + set, + HashSet.forEach(({ n }) => { + result.push(n) + }) + ) + + deepStrictEqual(result, [0, 1, 2]) + }) + + it("has", () => { + const set = makeTestHashSet(0, 1, 2) + + assertTrue(pipe(set, HashSet.has(value(0)))) + assertTrue(pipe(set, HashSet.has(value(1)))) + assertTrue(pipe(set, HashSet.has(value(2)))) + assertFalse(pipe(set, HashSet.has(value(3)))) + }) + + it("intersection", () => { + const set1 = makeTestHashSet(0, 1, 2) + const set2 = makeTestHashSet(2, 3, 4) + const result = pipe(set1, HashSet.intersection(set2)) + + deepStrictEqual(result, HashSet.make(value(2))) + }) + + it("isSubset", () => { + const set1 = makeTestHashSet(0, 1) + const set2 = makeTestHashSet(1, 2) + const set3 = makeTestHashSet(0, 1, 2) + + assertFalse(pipe(set1, HashSet.isSubset(set2))) + assertTrue(pipe(set1, HashSet.isSubset(set3))) + }) + + it("map", () => { + const set = makeTestHashSet(0, 1, 2) + const result = pipe(set, HashSet.map(({ n }) => value(n + 1))) + + deepStrictEqual(result, HashSet.make(value(1), value(2), value(3))) + }) + + it("mutate", () => { + const set = makeTestHashSet(0, 1, 2) + const result = pipe( + set, + HashSet.mutate((set) => { + pipe(set, HashSet.add(value(3))) + pipe(set, HashSet.remove(value(0))) + }) + ) + + assertFalse(pipe(result, HashSet.has(value(0)))) + assertTrue(pipe(result, HashSet.has(value(1)))) + assertTrue(pipe(result, HashSet.has(value(2)))) + assertTrue(pipe(result, HashSet.has(value(3)))) + }) + + it("partition", () => { + const set = makeTestHashSet(0, 1, 2, 3, 4, 5) + const result = pipe(set, HashSet.partition(({ n }) => n > 2)) + + deepStrictEqual(result[0], HashSet.make(value(0), value(1), value(2))) + deepStrictEqual(result[1], HashSet.make(value(3), value(4), value(5))) + }) + + it("remove", () => { + const set = makeTestHashSet(0, 1, 2) + const result = pipe(set, HashSet.remove(value(0))) + + assertFalse(pipe(result, HashSet.has(value(0)))) + assertTrue(pipe(result, HashSet.has(value(1)))) + assertTrue(pipe(result, HashSet.has(value(2)))) + }) + + it("size", () => { + const hashSet = makeTestHashSet(0, 1, 2) + const result = HashSet.size(hashSet) + + strictEqual(result, 3) + }) + + it("some", () => { + const set = makeTestHashSet(0, 1, 2) + + assertTrue(pipe(set, HashSet.some(({ n }) => n > 0))) + assertFalse(pipe(set, HashSet.some(({ n }) => n > 2))) + }) + + it("toggle", () => { + let set = makeTestHashSet(0, 1, 2) + assertTrue(pipe(set, HashSet.has(value(0)))) + set = pipe(set, HashSet.toggle(value(0))) + assertFalse(pipe(set, HashSet.has(value(0)))) + set = pipe(set, HashSet.toggle(value(0))) + assertTrue(pipe(set, HashSet.has(value(0)))) + }) + + it("union", () => { + const set1 = makeTestHashSet(0, 1, 2) + const set2 = makeTestHashSet(2, 3, 4) + const result = pipe(set1, HashSet.union(set2)) + + deepStrictEqual(result, HashSet.make(value(0), value(1), value(2), value(3), value(4))) + }) + + it("values", () => { + const hashSet = makeTestHashSet(0, 1, 2) + + const result = Array.from(HashSet.values(hashSet)) + + deepStrictEqual(result, [value(0), value(1), value(2)]) + }) + + it("toValues", () => { + const hashSet = makeTestHashSet(0, 1, 2) + + const result = HashSet.toValues(hashSet) + + deepStrictEqual(result, [value(0), value(1), value(2)]) + }) + + it("pipe()", () => { + strictEqual( + HashSet.empty().pipe(HashSet.add("value"), HashSet.size), + HashSet.make("value").pipe(HashSet.size) + ) + }) + + it("isHashSet", () => { + assertTrue(HashSet.isHashSet(HashSet.empty())) + assertFalse(HashSet.isHashSet(null)) + assertFalse(HashSet.isHashSet({})) + }) +}) diff --git a/repos/effect/packages/effect/test/Inspectable.test.ts b/repos/effect/packages/effect/test/Inspectable.test.ts new file mode 100644 index 0000000..2170415 --- /dev/null +++ b/repos/effect/packages/effect/test/Inspectable.test.ts @@ -0,0 +1,264 @@ +import { describe, it } from "@effect/vitest" +import { strictEqual } from "@effect/vitest/utils" +import { Inspectable, Redacted } from "effect" + +describe("Inspectable", () => { + describe("formatUnknown", () => { + const format = Inspectable.formatUnknown + it("null", () => { + strictEqual(format(null), `null`) + }) + + it("undefined", () => { + strictEqual(format(undefined), `undefined`) + }) + + it("string", () => { + strictEqual(format("a"), `"a"`) + }) + + it("number", () => { + strictEqual(format(123), `123`) + }) + + it("boolean", () => { + strictEqual(format(true), `true`) + }) + + it("symbol", () => { + strictEqual(format(Symbol("a")), `Symbol(a)`) + }) + + it("bigint", () => { + strictEqual(format(BigInt(123)), `123n`) + }) + + it("custom toString method", () => { + strictEqual(format({ toString: () => "custom" }), `custom`) + }) + + it("array", () => { + strictEqual(format([1, 2, 3n]), `[1,2,3n]`) + }) + + it("circular array", () => { + const arr: any = [1] + arr.push(arr) + strictEqual(format(arr), `[1,[Circular]]`) + }) + + it("Set", () => { + strictEqual(format(new Set([1, 2, 3])), `Set([1,2,3])`) + }) + + it("Map", () => { + strictEqual(format(new Map([["a", 1], ["b", 2]])), `Map([["a",1],["b",2]])`) + }) + + it("circular Map contents", () => { + const obj: any = { a: 1 } + const map = new Map([["obj", obj]]) + obj.map = map + strictEqual(format(map), `Map([["obj",{"a":1,"map":[Circular]}]])`) + }) + + it("circular Set contents", () => { + const obj: any = { a: 1 } + const set = new Set([obj]) + obj.set = set + strictEqual(format(set), `Set([{"a":1,"set":[Circular]}])`) + }) + + it("object", () => { + strictEqual(format({ a: 1 }), `{"a":1}`) + strictEqual(format({ a: 1, b: 2 }), `{"a":1,"b":2}`) + strictEqual(format({ [Symbol.for("a")]: 1 }), `{Symbol(a):1}`) + strictEqual(format({ a: 1, b: [1, 2, 3n] }), `{"a":1,"b":[1,2,3n]}`) + }) + + it("circular object", () => { + const obj: any = { a: 1 } + obj.b = obj + strictEqual(format(obj), `{"a":1,"b":[Circular]}`) + }) + + it("object with null prototype", () => { + strictEqual(format(Object.create(null)), `{}`) + strictEqual(format(Object.create(null, { a: { value: 1 } })), `{"a":1}`) + }) + + it("Error", () => { + strictEqual(format(new Error("a")), `Error: a`) + strictEqual(format(new Error("a", { cause: "b" })), `Error: a (cause: "b")`) + }) + + it("Date", () => { + strictEqual(format(new Date(0)), `1970-01-01T00:00:00.000Z`) + strictEqual(format(new Date("invalid")), `Invalid Date`) + }) + + it("RegExp", () => { + strictEqual(format(/a/), `/a/`) + }) + + it("Redacted", () => { + strictEqual(format(Redacted.make("a")), ``) + }) + + describe("whitespace", () => { + it("object", () => { + strictEqual(format({ a: 1 }, { space: 2 }), `{"a":1}`) + strictEqual( + format({ a: 1, b: 2 }, { space: 2 }), + `{ + "a": 1, + "b": 2 +}` + ) + strictEqual( + format({ a: 1, b: [1, 2, 3n] }, { space: 2 }), + `{ + "a": 1, + "b": [ + 1, + 2, + 3n + ] +}` + ) + strictEqual(format({ [Symbol.for("a")]: 1 }, { space: 2 }), `{Symbol(a):1}`) + }) + + it("circular object", () => { + const obj: any = { a: 1 } + obj.b = obj + strictEqual( + format(obj, { space: 2 }), + `{ + "a": 1, + "b": [Circular] +}` + ) + }) + + it("object with null prototype", () => { + strictEqual(format(Object.create(null), { space: 2 }), `{}`) + strictEqual( + format(Object.create(null, { a: { value: 1 } }), { space: 2 }), + `{"a":1}` + ) + }) + }) + }) + + describe("toString", () => { + it("primitives", () => { + strictEqual(Inspectable.format(null), "null") + strictEqual(Inspectable.format(undefined), undefined) + strictEqual(Inspectable.format(1), "1") + strictEqual(Inspectable.format("a"), `"a"`) + strictEqual(Inspectable.format(true), "true") + }) + + it("empty collections", () => { + strictEqual(Inspectable.format({}), "{}") + strictEqual(Inspectable.format([]), "[]") + }) + + it("objects", () => { + strictEqual( + Inspectable.format({ a: 1 }), + `{ + "a": 1 +}` + ) + strictEqual( + Inspectable.format({ a: 1, b: 2 }), + `{ + "a": 1, + "b": 2 +}` + ) + strictEqual( + Inspectable.format({ a: 1, b: { c: 2 } }), + `{ + "a": 1, + "b": { + "c": 2 + } +}` + ) + strictEqual(Inspectable.format({ a: undefined }), "{}") + }) + + it("arrays", () => { + strictEqual( + Inspectable.format([1, 2, 3]), + `[ + 1, + 2, + 3 +]` + ) + strictEqual( + Inspectable.format([1, [2, 3], 4]), + `[ + 1, + [ + 2, + 3 + ], + 4 +]` + ) + }) + + it("mixed", () => { + strictEqual( + Inspectable.format({ "a": [] }), + `{ + "a": [] +}` + ) + strictEqual( + Inspectable.format({ + "_id": "Cause", + "_tag": "Fail", + "errors": [ + { + "value": { "_id": "Chunk", "values": [0, 1, 2] } + }, + { + "value": { "_id": "Chunk", "values": ["a", "b"] } + } + ] + }), + `{ + "_id": "Cause", + "_tag": "Fail", + "errors": [ + { + "value": { + "_id": "Chunk", + "values": [ + 0, + 1, + 2 + ] + } + }, + { + "value": { + "_id": "Chunk", + "values": [ + "a", + "b" + ] + } + } + ] +}` + ) + }) + }) +}) diff --git a/repos/effect/packages/effect/test/Iterable.test.ts b/repos/effect/packages/effect/test/Iterable.test.ts new file mode 100644 index 0000000..4b54c85 --- /dev/null +++ b/repos/effect/packages/effect/test/Iterable.test.ts @@ -0,0 +1,461 @@ +import { describe, it } from "@effect/vitest" +import { assertFalse, assertNone, assertSome, assertTrue, deepStrictEqual, strictEqual } from "@effect/vitest/utils" +import { Iterable as Iter, Number, Option, pipe } from "effect" +import type { Predicate } from "effect/Predicate" + +const symA = Symbol.for("a") +const symB = Symbol.for("b") +const symC = Symbol.for("c") + +const toArray =
(i: Iterable) => { + if (Array.isArray(i)) { + throw new Error("not an iterable") + } + return Array.from(i) +} + +describe("Iterable", () => { + it("of", () => { + deepStrictEqual(Array.from(Iter.of(1)), [1]) + }) + + describe("iterable inputs", () => { + it("prepend", () => { + deepStrictEqual(pipe([1, 2, 3], Iter.prepend(0), toArray), [0, 1, 2, 3]) + deepStrictEqual(pipe([[2]], Iter.prepend([1]), toArray), [[1], [2]]) + + deepStrictEqual(pipe(new Set([1, 2, 3]), Iter.prepend(0), toArray), [0, 1, 2, 3]) + deepStrictEqual(pipe(new Set([[2]]), Iter.prepend([1]), toArray), [[1], [2]]) + }) + + it("prependAll", () => { + deepStrictEqual(pipe([3, 4], Iter.prependAll([1, 2]), toArray), [1, 2, 3, 4]) + + deepStrictEqual(pipe([3, 4], Iter.prependAll(new Set([1, 2])), toArray), [1, 2, 3, 4]) + deepStrictEqual(pipe(new Set([3, 4]), Iter.prependAll([1, 2]), toArray), [1, 2, 3, 4]) + }) + + it("append", () => { + deepStrictEqual(pipe([1, 2, 3], Iter.append(4), toArray), [1, 2, 3, 4]) + deepStrictEqual(pipe([[1]], Iter.append([2]), toArray), [[1], [2]]) + + deepStrictEqual(pipe(new Set([1, 2, 3]), Iter.append(4), toArray), [1, 2, 3, 4]) + deepStrictEqual(pipe(new Set([[1]]), Iter.append([2]), toArray), [[1], [2]]) + }) + + it("appendAll", () => { + deepStrictEqual(pipe([1, 2], Iter.appendAll([3, 4]), toArray), [1, 2, 3, 4]) + + deepStrictEqual(pipe([1, 2], Iter.appendAll(new Set([3, 4])), toArray), [1, 2, 3, 4]) + deepStrictEqual(pipe(new Set([1, 2]), Iter.appendAll([3, 4]), toArray), [1, 2, 3, 4]) + }) + + it("scan", () => { + const f = (b: number, a: number) => b - a + deepStrictEqual(pipe([1, 2, 3], Iter.scan(10, f), toArray), [10, 9, 7, 4]) + deepStrictEqual(pipe([0], Iter.scan(10, f), toArray), [10, 10]) + deepStrictEqual(pipe([], Iter.scan(10, f), toArray), [10]) + + deepStrictEqual(pipe(new Set([1, 2, 3]), Iter.scan(10, f), toArray), [10, 9, 7, 4]) + deepStrictEqual(pipe(new Set([0]), Iter.scan(10, f), toArray), [10, 10]) + deepStrictEqual(pipe(new Set([]), Iter.scan(10, f), toArray), [10]) + }) + + it("take", () => { + deepStrictEqual(pipe([1, 2, 3, 4], Iter.take(2), toArray), [1, 2]) + deepStrictEqual(pipe([1, 2, 3, 4], Iter.take(0), toArray), []) + // out of bounds + deepStrictEqual(pipe([1, 2, 3, 4], Iter.take(-10), toArray), []) + deepStrictEqual(pipe([1, 2, 3, 4], Iter.take(10), toArray), [1, 2, 3, 4]) + + deepStrictEqual(pipe(new Set([1, 2, 3, 4]), Iter.take(2), toArray), [1, 2]) + deepStrictEqual(pipe(new Set([1, 2, 3, 4]), Iter.take(0), toArray), []) + // out of bounds + deepStrictEqual(pipe(new Set([1, 2, 3, 4]), Iter.take(-10), toArray), []) + deepStrictEqual(pipe(new Set([1, 2, 3, 4]), Iter.take(10), toArray), [1, 2, 3, 4]) + }) + + it("takeWhile", () => { + const f = (n: number) => n % 2 === 0 + deepStrictEqual(pipe([2, 4, 3, 6], Iter.takeWhile(f), toArray), [2, 4]) + deepStrictEqual(pipe(Iter.empty(), Iter.takeWhile(f), toArray), []) + deepStrictEqual(pipe([1, 2, 4], Iter.takeWhile(f), toArray), []) + deepStrictEqual(pipe([2, 4], Iter.takeWhile(f), toArray), [2, 4]) + + deepStrictEqual(pipe(new Set([2, 4, 3, 6]), Iter.takeWhile(f), toArray), [2, 4]) + deepStrictEqual(pipe(new Set(), Iter.takeWhile(f), toArray), []) + deepStrictEqual(pipe(new Set([1, 2, 4]), Iter.takeWhile(f), toArray), []) + deepStrictEqual(pipe(new Set([2, 4]), Iter.takeWhile(f), toArray), [2, 4]) + }) + + it("drop", () => { + deepStrictEqual(pipe(Iter.empty(), Iter.drop(0), toArray), []) + deepStrictEqual(pipe([1, 2], Iter.drop(0), toArray), [1, 2]) + deepStrictEqual(pipe([1, 2], Iter.drop(1), toArray), [2]) + deepStrictEqual(pipe([1, 2], Iter.drop(2), toArray), []) + // out of bound + deepStrictEqual(pipe(Iter.empty(), Iter.drop(1), toArray), []) + deepStrictEqual(pipe(Iter.empty(), Iter.drop(-1), toArray), []) + deepStrictEqual(pipe([1, 2], Iter.drop(3), toArray), []) + deepStrictEqual(pipe([1, 2], Iter.drop(-1), toArray), [1, 2]) + + deepStrictEqual(pipe(new Set(), Iter.drop(0), toArray), []) + deepStrictEqual(pipe(new Set([1, 2]), Iter.drop(0), toArray), [1, 2]) + deepStrictEqual(pipe(new Set([1, 2]), Iter.drop(1), toArray), [2]) + deepStrictEqual(pipe(new Set([1, 2]), Iter.drop(2), toArray), []) + // out of bound + deepStrictEqual(pipe(new Set(), Iter.drop(1), toArray), []) + deepStrictEqual(pipe(new Set(), Iter.drop(-1), toArray), []) + deepStrictEqual(pipe(new Set([1, 2]), Iter.drop(3), toArray), []) + deepStrictEqual(pipe(new Set([1, 2]), Iter.drop(-1), toArray), [1, 2]) + }) + + describe("findFirst", () => { + it("boolean-returning overloads", () => { + assertNone(pipe([], Iter.findFirst((n) => n % 2 === 0))) + assertSome(pipe([1, 2, 3], Iter.findFirst((n) => n % 2 === 0)), 2) + assertSome(pipe([1, 2, 3, 4], Iter.findFirst((n) => n % 2 === 0)), 2) + + assertNone(pipe(new Set(), Iter.findFirst((n) => n % 2 === 0))) + assertSome(pipe(new Set([1, 2, 3]), Iter.findFirst((n) => n % 2 === 0)), 2) + assertSome(pipe(new Set([1, 2, 3, 4]), Iter.findFirst((n) => n % 2 === 0)), 2) + }) + + it("Option-returning overloads", () => { + assertNone(pipe([], Iter.findFirst((n, i) => n % 2 === 0 ? Option.some([n, i]) : Option.none()))) + assertSome( + pipe([1, 2, 3], Iter.findFirst((n, i) => n % 2 === 0 ? Option.some([n, i]) : Option.none())), + [2, 1] + ) + assertSome( + pipe([1, 2, 3, 4], Iter.findFirst((n, i) => n % 2 === 0 ? Option.some([n, i]) : Option.none())), + [2, 1] + ) + + assertNone( + pipe(new Set(), Iter.findFirst((n, i) => n % 2 === 0 ? Option.some([n, i]) : Option.none())) + ) + assertSome( + pipe(new Set([1, 2, 3]), Iter.findFirst((n, i) => n % 2 === 0 ? Option.some([n, i]) : Option.none())), + [2, 1] + ) + assertSome( + pipe(new Set([1, 2, 3, 4]), Iter.findFirst((n, i) => n % 2 === 0 ? Option.some([n, i]) : Option.none())), + [2, 1] + ) + }) + }) + + describe("findLast", () => { + it("boolean-returning overloads", () => { + assertNone(pipe([], Iter.findLast((n) => n % 2 === 0))) + assertSome(pipe([1, 2, 3], Iter.findLast((n) => n % 2 === 0)), 2) + assertSome(pipe([1, 2, 3, 4], Iter.findLast((n) => n % 2 === 0)), 4) + + assertNone(pipe(new Set(), Iter.findLast((n) => n % 2 === 0))) + assertSome(pipe(new Set([1, 2, 3]), Iter.findLast((n) => n % 2 === 0)), 2) + assertSome(pipe(new Set([1, 2, 3, 4]), Iter.findLast((n) => n % 2 === 0)), 4) + }) + + it("Option-returning overloads", () => { + assertNone(pipe([], Iter.findLast((n, i) => n % 2 === 0 ? Option.some([n, i]) : Option.none()))) + assertSome( + pipe([1, 2, 3], Iter.findLast((n, i) => n % 2 === 0 ? Option.some([n, i]) : Option.none())), + [2, 1] + ) + assertSome( + pipe([1, 2, 3, 4], Iter.findLast((n, i) => n % 2 === 0 ? Option.some([n, i]) : Option.none())), + [4, 3] + ) + + assertNone( + pipe(new Set(), Iter.findLast((n, i) => n % 2 === 0 ? Option.some([n, i]) : Option.none())) + ) + assertSome( + pipe(new Set([1, 2, 3]), Iter.findLast((n, i) => n % 2 === 0 ? Option.some([n, i]) : Option.none())), + [2, 1] + ) + assertSome( + pipe(new Set([1, 2, 3, 4]), Iter.findLast((n, i) => n % 2 === 0 ? Option.some([n, i]) : Option.none())), + [4, 3] + ) + }) + }) + + it("zip", () => { + deepStrictEqual(pipe(new Set([]), Iter.zip(new Set(["a", "b", "c", "d"])), toArray), []) + deepStrictEqual(pipe(new Set([1, 2, 3]), Iter.zip(new Set([])), toArray), []) + deepStrictEqual(pipe(new Set([1, 2, 3]), Iter.zip(new Set(["a", "b", "c", "d"])), toArray), [ + [1, "a"], + [2, "b"], + [3, "c"] + ]) + deepStrictEqual(pipe(new Set([1, 2, 3]), Iter.zip(new Set(["a", "b", "c", "d"])), toArray), [ + [1, "a"], + [2, "b"], + [3, "c"] + ]) + }) + + it("zipWith", () => { + deepStrictEqual( + pipe(new Set([1, 2, 3]), Iter.zipWith(new Set([]), (n, s) => s + n), toArray), + [] + ) + deepStrictEqual( + pipe(new Set([]), Iter.zipWith(new Set(["a", "b", "c", "d"]), (n, s) => s + n), toArray), + [] + ) + deepStrictEqual( + pipe(new Set([]), Iter.zipWith(new Set([]), (n, s) => s + n), toArray), + [] + ) + deepStrictEqual( + pipe(new Set([1, 2, 3]), Iter.zipWith(new Set(["a", "b", "c", "d"]), (n, s) => s + n), toArray), + ["a1", "b2", "c3"] + ) + }) + + it("intersperse", () => { + deepStrictEqual(pipe([], Iter.intersperse(0), toArray), []) + deepStrictEqual(pipe([1], Iter.intersperse(0), toArray), [1]) + deepStrictEqual(pipe([1, 2, 3], Iter.intersperse(0), toArray), [1, 0, 2, 0, 3]) + deepStrictEqual(pipe([1, 2], Iter.intersperse(0), toArray), [1, 0, 2]) + deepStrictEqual(pipe([1, 2, 3, 4], Iter.intersperse(0), toArray), [1, 0, 2, 0, 3, 0, 4]) + + deepStrictEqual(pipe(new Set([]), Iter.intersperse(0), toArray), []) + deepStrictEqual(pipe(new Set([1]), Iter.intersperse(0), toArray), [1]) + deepStrictEqual(pipe(new Set([1, 2, 3]), Iter.intersperse(0), toArray), [1, 0, 2, 0, 3]) + deepStrictEqual(pipe(new Set([1, 2]), Iter.intersperse(0), toArray), [1, 0, 2]) + deepStrictEqual(pipe(new Set([1, 2, 3, 4]), Iter.intersperse(0), toArray), [1, 0, 2, 0, 3, 0, 4]) + }) + + it("containsWith", () => { + const contains = Iter.containsWith(Number.Equivalence) + assertTrue(pipe([1, 2, 3], contains(2))) + assertFalse(pipe([1, 2, 3], contains(0))) + + assertTrue(pipe(new Set([1, 2, 3]), contains(2))) + assertFalse(pipe(new Set([1, 2, 3]), contains(0))) + }) + + it("contains", () => { + const contains = Iter.contains + assertTrue(pipe([1, 2, 3], contains(2))) + assertFalse(pipe([1, 2, 3], contains(0))) + + assertTrue(pipe(new Set([1, 2, 3]), contains(2))) + assertFalse(pipe(new Set([1, 2, 3]), contains(0))) + }) + + it("dedupeAdjacentWith", () => { + const dedupeAdjacent = Iter.dedupeAdjacentWith(Number.Equivalence) + deepStrictEqual(toArray(dedupeAdjacent([])), []) + deepStrictEqual(toArray(dedupeAdjacent([1, 2, 3])), [1, 2, 3]) + deepStrictEqual(toArray(dedupeAdjacent([1, 2, 2, 3, 3])), [1, 2, 3]) + }) + }) + + it("flatMapNullable", () => { + const f = Iter.flatMapNullable((n: number) => (n > 0 ? n : null)) + deepStrictEqual(pipe([], f, toArray), []) + deepStrictEqual(pipe([1], f, toArray), [1]) + deepStrictEqual(pipe([-1], f, toArray), []) + }) + + it("unfold", () => { + const as = Iter.unfold(5, (n) => (n > 0 ? Option.some([n, n - 1]) : Option.none())) + deepStrictEqual(toArray(as), [5, 4, 3, 2, 1]) + }) + + it("map", () => { + deepStrictEqual( + pipe([1, 2, 3], Iter.map((n) => n * 2), toArray), + [2, 4, 6] + ) + deepStrictEqual( + pipe(["a", "b"], Iter.map((s, i) => s + i), toArray), + ["a0", "b1"] + ) + }) + + it("flatMap", () => { + deepStrictEqual( + pipe([1, 2, 3], Iter.flatMap((n) => [n, n + 1]), toArray), + [1, 2, 2, 3, 3, 4] + ) + const f = (n: number, i: number) => [n + i] + deepStrictEqual(pipe([], Iter.flatMap(f), toArray), []) + deepStrictEqual(pipe([1, 2, 3], Iter.flatMap(f), toArray), [1, 3, 5]) + }) + + it("getSomes", () => { + deepStrictEqual(toArray(Iter.getSomes([])), []) + deepStrictEqual(toArray(Iter.getSomes([Option.some(1), Option.some(2), Option.some(3)])), [ + 1, + 2, + 3 + ]) + deepStrictEqual(toArray(Iter.getSomes([Option.some(1), Option.none(), Option.some(3)])), [ + 1, + 3 + ]) + }) + + it("filter", () => { + deepStrictEqual(toArray(Iter.filter([1, 2, 3], (n) => n % 2 === 1)), [1, 3]) + deepStrictEqual(toArray(Iter.filter([Option.some(3), Option.some(2), Option.some(1)], Option.isSome)), [ + Option.some(3), + Option.some(2), + Option.some(1) + ]) + deepStrictEqual(toArray(Iter.filter([Option.some(3), Option.none(), Option.some(1)], Option.isSome)), [ + Option.some(3), + Option.some(1) + ]) + deepStrictEqual(toArray(Iter.filter(["a", "b", "c"], (_, i) => i % 2 === 0)), ["a", "c"]) + }) + + it("filterMap", () => { + const f = (n: number) => (n % 2 === 0 ? Option.none() : Option.some(n)) + deepStrictEqual(pipe([1, 2, 3], Iter.filterMap(f), toArray), [1, 3]) + deepStrictEqual(pipe([], Iter.filterMap(f), toArray), []) + const g = (n: number, i: number) => ((i + n) % 2 === 0 ? Option.none() : Option.some(n)) + deepStrictEqual(pipe([1, 2, 4], Iter.filterMap(g), toArray), [1, 2]) + deepStrictEqual(pipe([], Iter.filterMap(g), toArray), []) + }) + + it("isEmpty", () => { + assertFalse(Iter.isEmpty([1, 2, 3])) + assertTrue(Iter.isEmpty([])) + }) + + it("head", () => { + const as: ReadonlyArray = [1, 2, 3] + assertSome(Iter.head(as), 1) + assertNone(Iter.head([])) + }) + + it("chunksOf", () => { + deepStrictEqual(toArray(Iter.chunksOf(2)([1, 2, 3, 4, 5])), [ + [1, 2], + [3, 4], + [5] + ]) + deepStrictEqual(toArray(Iter.chunksOf(2)([1, 2, 3, 4, 5, 6])), [ + [1, 2], + [3, 4], + [5, 6] + ]) + deepStrictEqual(toArray(Iter.chunksOf(1)([1, 2, 3, 4, 5])), [[1], [2], [3], [4], [5]]) + deepStrictEqual(toArray(Iter.chunksOf(5)([1, 2, 3, 4, 5])), [[1, 2, 3, 4, 5]]) + // out of bounds + deepStrictEqual(toArray(Iter.chunksOf(0)([1, 2, 3, 4, 5])), [[1], [2], [3], [4], [5]]) + deepStrictEqual(toArray(Iter.chunksOf(-1)([1, 2, 3, 4, 5])), [[1], [2], [3], [4], [5]]) + + const assertSingleChunk = ( + input: Iterable, + n: number + ) => { + const chunks = toArray(Iter.chunksOf(n)(input)) + strictEqual(chunks.length, 1) + deepStrictEqual(chunks[0], input) + } + // n = length + assertSingleChunk([1, 2], 2) + // n out of bounds + assertSingleChunk([1, 2], 3) + }) + + it("flatten", () => { + deepStrictEqual(toArray(Iter.flatten([[1], [2], [3]])), [1, 2, 3]) + }) + + it("groupWith", () => { + const groupWith = Iter.groupWith(Number.Equivalence) + deepStrictEqual(toArray(groupWith([1, 2, 1, 1])), [[1], [2], [1, 1]]) + deepStrictEqual(toArray(groupWith([1, 2, 1, 1, 3])), [[1], [2], [1, 1], [3]]) + }) + + it("groupBy", () => { + deepStrictEqual(Iter.groupBy((_) => "")([]), {}) + deepStrictEqual(Iter.groupBy((a) => `${a}`)([1]), { "1": [1] }) + deepStrictEqual( + Iter.groupBy((s: string) => `${s.length}`)(["foo", "bar", "foobar"]), + { + "3": ["foo", "bar"], + "6": ["foobar"] + } + ) + deepStrictEqual(Iter.groupBy(["a", "b"], (s) => s === "a" ? symA : s === "b" ? symB : symC), { + [symA]: ["a"], + [symB]: ["b"] + }) + deepStrictEqual(Iter.groupBy(["a", "b", "c", "d"], (s) => s === "a" ? symA : s === "b" ? symB : symC), { + [symA]: ["a"], + [symB]: ["b"], + [symC]: ["c", "d"] + }) + }) + + it("makeBy", () => { + deepStrictEqual( + pipe( + Iter.makeBy((n) => n * 2), + Iter.take(5), + toArray + ), + [0, 2, 4, 6, 8] + ) + deepStrictEqual(toArray(Iter.makeBy((n) => n * 2, { length: 5 })), [0, 2, 4, 6, 8]) + deepStrictEqual(toArray(Iter.makeBy((n) => n * 2, { length: 2.2 })), [0, 2]) + }) + + it("replicate", () => { + deepStrictEqual(toArray(Iter.replicate("a", 0)), ["a"]) + deepStrictEqual(toArray(Iter.replicate("a", -1)), ["a"]) + deepStrictEqual(toArray(Iter.replicate("a", 3)), ["a", "a", "a"]) + deepStrictEqual(toArray(Iter.replicate("a", 2.2)), ["a", "a"]) + }) + + it("range", () => { + deepStrictEqual(toArray(Iter.range(0, 0)), [0]) + deepStrictEqual(toArray(Iter.range(0, 1)), [0, 1]) + deepStrictEqual(toArray(Iter.range(1, 5)), [1, 2, 3, 4, 5]) + deepStrictEqual(toArray(Iter.range(10, 15)), [10, 11, 12, 13, 14, 15]) + deepStrictEqual(toArray(Iter.range(-1, 0)), [-1, 0]) + deepStrictEqual(toArray(Iter.range(-5, -1)), [-5, -4, -3, -2, -1]) + // out of bound + deepStrictEqual(Array.from(Iter.range(2, 1)), [2]) + deepStrictEqual(Array.from(Iter.range(-1, -2)), [-1]) + }) + + it("empty", () => { + deepStrictEqual(toArray(Iter.empty()).length, 0) + }) + + it("some", () => { + const isPositive: Predicate = (n) => n > 0 + assertTrue(Iter.some([-1, -2, 3], isPositive)) + assertFalse(Iter.some([-1, -2, -3], isPositive)) + }) + + it("size", () => { + strictEqual(Iter.size(Iter.empty()), 0) + strictEqual(Iter.size([]), 0) + strictEqual(Iter.size(["a"]), 1) + }) + + it("forEach", () => { + const log: Array = [] + Iter.forEach(["a", "b", "c"], (a, i) => log.push(`${a}-${i}`)) + deepStrictEqual(log, ["a-0", "b-1", "c-2"]) + }) + + it("countBy", () => { + deepStrictEqual(Iter.countBy([1, 2, 3, 4, 5], (n) => n % 2 === 0), 2) + deepStrictEqual(pipe([1, 2, 3, 4, 5], Iter.countBy((n) => n % 2 === 0)), 2) + + deepStrictEqual(Iter.countBy(new Map([["a", 1], ["b", 2], ["c", 3]]), ([key, n]) => n % 2 === 1 && key !== "c"), 1) + }) +}) diff --git a/repos/effect/packages/effect/test/KeyedPool.test.ts b/repos/effect/packages/effect/test/KeyedPool.test.ts new file mode 100644 index 0000000..c17837b --- /dev/null +++ b/repos/effect/packages/effect/test/KeyedPool.test.ts @@ -0,0 +1,71 @@ +import { describe, it } from "@effect/vitest" +import { strictEqual } from "@effect/vitest/utils" +import { Array, Duration, Effect, Fiber, KeyedPool, pipe, Random, Ref } from "effect" +import * as TestClock from "effect/TestClock" + +describe("KeyedPool", () => { + it.scoped("acquire release many successfully while other key is blocked", () => + Effect.gen(function*() { + const N = 10 + const pool = yield* KeyedPool.make({ + acquire: (key: string) => Effect.succeed(key), + size: 4 + }) + yield* pool.pipe( + KeyedPool.get("key1"), + Effect.repeatN(3), + Effect.asVoid + ) + const fiber = yield* Effect.fork( + Effect.forEach( + Array.range(1, N), + () => + Effect.scoped( + Effect.zipRight( + KeyedPool.get(pool, "key2"), + Effect.sleep(Duration.millis(10)) + ) + ), + { concurrency: "unbounded", discard: true } + ) + ) + yield* TestClock.adjust(Duration.millis(10 * N)) + const result = yield* Fiber.join(fiber) + strictEqual(result, undefined) + })) + + it.scoped("acquire release many with invalidates", () => + Effect.gen(function*() { + const N = 10 + const counter = yield* Ref.make(0) + const pool = yield* KeyedPool.make({ + acquire: (key) => Ref.modify(counter, (n) => [`${key}-${n}`, n + 1] as const), + size: 4 + }) + const fiber = yield* Effect.fork( + Effect.forEach( + Array.range(1, N), + () => + Effect.scoped(pipe( + KeyedPool.get(pool, "key1"), + Effect.flatMap((value) => + Effect.zipRight( + Effect.whenEffect( + KeyedPool.invalidate(pool, value), + Random.nextBoolean + ), + Effect.flatMap( + Random.nextIntBetween(0, 15 + 1), + (n) => Effect.sleep(Duration.millis(n)) + ) + ) + ) + )), + { concurrency: "unbounded", discard: true } + ) + ) + yield* TestClock.adjust(Duration.millis(15 * N)) + const result = yield* Fiber.join(fiber) + strictEqual(result, undefined) + })) +}) diff --git a/repos/effect/packages/effect/test/Layer.test.ts b/repos/effect/packages/effect/test/Layer.test.ts new file mode 100644 index 0000000..5e1588f --- /dev/null +++ b/repos/effect/packages/effect/test/Layer.test.ts @@ -0,0 +1,802 @@ +import { describe, it } from "@effect/vitest" +import { assertTrue, deepStrictEqual, strictEqual } from "@effect/vitest/utils" +import { + Chunk, + Context, + Deferred, + Duration, + Effect, + Exit, + Fiber, + FiberRef, + identity, + Layer, + pipe, + Ref, + Schedule, + Scope, + Stream +} from "effect" + +const acquire1 = "Acquiring Module 1" +const acquire2 = "Acquiring Module 2" +const acquire3 = "Acquiring Module 3" +const release1 = "Releasing Module 1" +const release2 = "Releasing Module 2" +const release3 = "Releasing Module 3" + +describe("Layer", () => { + it.effect("layers can be acquired in parallel", () => + Effect.gen(function*() { + const BoolTag = Context.GenericTag("boolean") + const deferred = yield* Deferred.make() + const layer1 = Layer.effectContext(Effect.never) + const layer2 = Layer.scopedContext( + Effect.acquireRelease( + Deferred.succeed(deferred, void 0).pipe( + Effect.map((bool) => Context.make(BoolTag, bool)) + ), + () => Effect.void + ) + ) + const env = layer1.pipe(Layer.merge(layer2), Layer.build) + const fiber = yield* pipe(Effect.scoped(env), Effect.forkDaemon) + yield* Deferred.await(deferred) + const result = yield* pipe(Fiber.interrupt(fiber), Effect.asVoid) + strictEqual(result, undefined) + })) + it.effect("preserves identity of acquired resources", () => + Effect.gen(function*() { + const ChunkTag = Context.GenericTag>>("Ref.Ref>") + const testRef = yield* Ref.make>(Chunk.empty()) + const layer = Layer.scoped( + ChunkTag, + Effect.acquireRelease( + Ref.make>(Chunk.empty()), + (ref) => + Ref.get(ref).pipe( + Effect.flatMap((chunk) => Ref.set(testRef, chunk)) + ) + ).pipe( + Effect.tap(() => Effect.void) + ) + ) + yield* pipe( + Layer.build(layer), + Effect.flatMap((context) => + Ref.update( + context.pipe(Context.get(ChunkTag)), + Chunk.append("test") + ) + ), + Effect.scoped + ) + const result = yield* Ref.get(testRef) + deepStrictEqual(Array.from(result), ["test"]) + })) + it.effect("sharing with merge", () => + Effect.gen(function*() { + const ref = yield* makeRef() + const layer = makeLayer1(ref) + const env = layer.pipe(Layer.merge(layer), Layer.build) + yield* Effect.scoped(env) + const result = yield* Ref.get(ref) + deepStrictEqual(Array.from(result), [acquire1, release1]) + })) + it.scoped("sharing itself with merge", () => + Effect.gen(function*() { + const service1 = new Service1() + const layer = Layer.succeed(Service1Tag, service1) + const env = layer.pipe(Layer.merge(layer), Layer.merge(layer), Layer.build) + const result = yield* env.pipe( + Effect.flatMap((context) => Effect.try(() => context.pipe(Context.get(Service1Tag)))) + ) + strictEqual(result, service1) + })) + it.effect("finalizers", () => + Effect.gen(function*() { + const ref = yield* makeRef() + const layer1 = makeLayer1(ref) + const layer2 = makeLayer2(ref) + const env = layer1.pipe(Layer.merge(layer2), Layer.build) + yield* Effect.scoped(env) + const result = yield* Ref.get(ref) + assertTrue(Array.from(result).slice(0, 2).find((s) => s === acquire1) !== undefined) + assertTrue(Array.from(result).slice(0, 2).find((s) => s === acquire2) !== undefined) + assertTrue(Array.from(result).slice(2, 4).find((s) => s === release1) !== undefined) + assertTrue(Array.from(result).slice(2, 4).find((s) => s === release2) !== undefined) + })) + it.effect("caching values in dependencies", () => + Effect.gen(function*() { + class Config { + constructor(readonly value: number) {} + } + const ConfigTag = Context.GenericTag("Config") + class A { + constructor(readonly value: number) {} + } + const ATag = Context.GenericTag("A") + const aLayer = Layer.function(ConfigTag, ATag, (config) => new A(config.value)) + class B { + constructor(readonly value: number) {} + } + const BTag = Context.GenericTag("B") + const bLayer = Layer.function(ATag, BTag, (_: A) => new B(_.value)) + class C { + constructor(readonly value: number) {} + } + const CTag = Context.GenericTag("C") + const cLayer = Layer.function(ATag, CTag, (_: A) => new C(_.value)) + const fedB = bLayer.pipe( + Layer.provideMerge(aLayer), + Layer.provideMerge(Layer.succeed(ConfigTag, new Config(1))) + ) + const fedC = cLayer.pipe( + Layer.provideMerge(aLayer), + Layer.provide(Layer.succeed(ConfigTag, new Config(2))) + ) + const result = yield* pipe( + fedB, + Layer.merge(fedC), + Layer.build, + Effect.map((context) => + [ + context.pipe(Context.get(BTag)), + context.pipe(Context.get(CTag)) + ] as const + ), + Effect.scoped + ) + strictEqual(result[0].value, 1) + strictEqual(result[1].value, 1) + })) + it.effect("orElse - uses an alternative layer", () => + Effect.gen(function*() { + const ref = yield* makeRef() + const layer1 = makeLayer1(ref) + const layer2 = makeLayer2(ref) + const env = Layer.fail("failed!").pipe(Layer.provideMerge(layer1), Layer.orElse(() => layer2), Layer.build) + yield* Effect.scoped(env) + const result = yield* Ref.get(ref) + deepStrictEqual(Array.from(result), [acquire1, release1, acquire2, release2]) + })) + it.effect("handles errors gracefully", () => + Effect.gen(function*() { + interface Bar { + readonly bar: string + } + const BarTag = Context.GenericTag("Bar") + interface Baz { + readonly baz: string + } + const BazTag = Context.GenericTag("Baz") + const ScopedTag = Context.GenericTag("void") + const sleep = Effect.sleep(Duration.millis(100)) + const layer1 = Layer.fail("foo") + const layer2 = Layer.succeed(BarTag, { bar: "bar" }) + const layer3 = Layer.succeed(BazTag, { baz: "baz" }) + const layer4 = Layer.scoped( + ScopedTag, + Effect.scoped(Effect.acquireRelease(sleep, () => sleep)) + ) + + const layer = Layer.merge( + layer1, + layer4.pipe( + Layer.provide(Layer.merge(layer2, layer3)) + ) + ) + const result = yield* pipe(Effect.void, Effect.provide(layer), Effect.exit) + assertTrue(Exit.isFailure(result)) + })) + it.effect("fresh with merge", () => + Effect.gen(function*() { + const ref = yield* makeRef() + const layer = makeLayer1(ref) + const env = layer.pipe(Layer.merge(Layer.fresh(layer)), Layer.build) + yield* Effect.scoped(env) + const result = yield* Ref.get(ref) + deepStrictEqual(Array.from(result), [acquire1, acquire1, release1, release1]) + })) + it.effect("fresh with to provideTo", () => + Effect.gen(function*() { + const ref = yield* makeRef() + const layer = makeLayer1(ref) + const env = Layer.fresh(layer).pipe( + Layer.provide(layer), + Layer.build + ) + yield* Effect.scoped(env) + const result = yield* Ref.get(ref) + deepStrictEqual(Array.from(result), [acquire1, acquire1, release1, release1]) + })) + it.effect("with multiple layers", () => + Effect.gen(function*() { + const ref = yield* makeRef() + const layer = makeLayer1(ref) + const env = layer.pipe( + Layer.merge(layer), + Layer.merge(layer.pipe(Layer.merge(layer), Layer.fresh)), + Layer.build + ) + yield* Effect.scoped(env) + const result = yield* Ref.get(ref) + deepStrictEqual(Array.from(result), [acquire1, acquire1, release1, release1]) + })) + it.effect("with identical fresh layers", () => + Effect.gen(function*() { + const ref = yield* makeRef() + const layer1 = makeLayer1(ref) + const layer2 = makeLayer2(ref) + const layer3 = makeLayer3(ref) + const env = layer2.pipe( + Layer.merge( + layer3.pipe( + Layer.provide(layer1), + Layer.fresh + ) + ), + Layer.provide(Layer.fresh(layer1)), + Layer.build + ) + yield* Effect.scoped(env) + const result = yield* Ref.get(ref) + deepStrictEqual(Array.from(result), [ + acquire1, + acquire2, + acquire1, + acquire3, + release3, + release1, + release2, + release1 + ]) + })) + it.effect("interruption with merge", () => + Effect.gen(function*() { + const ref = yield* makeRef() + const layer1 = makeLayer1(ref) + const layer2 = makeLayer2(ref) + const env = layer1.pipe(Layer.merge(layer2), Layer.build) + const fiber = yield* pipe(Effect.scoped(env), Effect.fork) + yield* Fiber.interrupt(fiber) + const result = yield* pipe(Ref.get(ref), Effect.map((chunk) => Array.from(chunk))) + if (result.find((s) => s === acquire1) !== undefined) { + assertTrue(result.some((s) => s === release1)) + } + if (result.find((s) => s === acquire2) !== undefined) { + assertTrue(result.some((s) => s === release2)) + } + })) + it.effect("interruption with provideTo", () => + Effect.gen(function*() { + const ref = yield* makeRef() + const layer1 = makeLayer1(ref) + const layer2 = makeLayer2(ref) + const env = layer2.pipe(Layer.provide(layer1), Layer.build) + const fiber = yield* pipe(Effect.scoped(env), Effect.fork) + yield* Fiber.interrupt(fiber) + const result = yield* pipe(Ref.get(ref), Effect.map((chunk) => Array.from(chunk))) + if (result.find((s) => s === acquire1) !== undefined) { + assertTrue(result.some((s) => s === release1)) + } + if (result.find((s) => s === acquire2) !== undefined) { + assertTrue(result.some((s) => s === release2)) + } + })) + it.effect("interruption with multiple layers", () => + Effect.gen(function*() { + const ref = yield* makeRef() + const layer1 = makeLayer1(ref) + const layer2 = makeLayer2(ref) + const layer3 = makeLayer3(ref) + const env = layer3.pipe( + Layer.provide(layer1), + Layer.merge(layer2), + Layer.provide(layer1), + Layer.build + ) + const fiber = yield* pipe(Effect.scoped(env), Effect.fork) + yield* Fiber.interrupt(fiber) + const result = yield* pipe(Ref.get(ref), Effect.map((chunk) => Array.from(chunk))) + if (result.find((s) => s === acquire1) !== undefined) { + assertTrue(result.some((s) => s === release1)) + } + if (result.find((s) => s === acquire2) !== undefined) { + assertTrue(result.some((s) => s === release2)) + } + if (result.find((s) => s === acquire3) !== undefined) { + assertTrue(result.some((s) => s === release3)) + } + })) + it.effect("can map a layer to an unrelated type", () => + Effect.gen(function*() { + interface ServiceA { + readonly name: string + readonly value: number + } + const ServiceATag = Context.GenericTag("ServiceA") + interface ServiceB { + readonly name: string + } + const ServiceBTag = Context.GenericTag("ServiceB") + const StringTag = Context.GenericTag("string") + const layer1 = Layer.succeed(ServiceATag, { name: "name", value: 1 }) + const layer2 = Layer.function(StringTag, ServiceBTag, (name) => ({ name })) + const live = layer2.pipe( + Layer.provide( + Layer.map(layer1, (context) => Context.make(StringTag, context.pipe(Context.get(ServiceATag)).name)) + ) + ) + const result = yield* pipe(ServiceBTag, Effect.provide(live)) + strictEqual(result.name, "name") + })) + it.effect("memoizes acquisition of resources", () => + Effect.gen(function*() { + const ref = yield* makeRef() + const memoized = Layer.memoize(makeLayer1(ref)) + yield* pipe( + memoized, + Effect.flatMap((layer) => + Effect.context().pipe( + Effect.provide(layer), + Effect.flatMap(() => Effect.context().pipe(Effect.provide(layer))) + ) + ), + Effect.scoped + ) + const result = yield* Ref.get(ref) + deepStrictEqual(Array.from(result), [acquire1, release1]) + })) + it.scoped("fiberRef changes are memoized", () => + Effect.gen(function*() { + const fiberRef = yield* FiberRef.make(false) + const tag = Context.GenericTag("boolean") + const layer1 = Layer.scopedDiscard(Effect.locallyScoped(fiberRef, true)) + const layer2 = Layer.effect(tag, FiberRef.get(fiberRef)) + const layer3 = layer2.pipe( + Layer.provide(layer1), + Layer.merge(layer1) + ) + const result = yield* Layer.build(layer3) + assertTrue(result.pipe(Context.unsafeGet(tag))) + })) + it.effect("provides a partial environment to an effect", () => + Effect.gen(function*() { + const NumberTag = Context.GenericTag("number") + const StringTag = Context.GenericTag("string") + const needsNumberAndString = Effect.all([NumberTag, StringTag]) + const providesNumber = Layer.succeed(NumberTag, 10) + const providesString = Layer.succeed(StringTag, "hi") + const needsString = needsNumberAndString.pipe(Effect.provide(providesNumber)) + const result = yield* pipe(needsString, Effect.provide(providesString)) + strictEqual(result[0], 10) + strictEqual(result[1], "hi") + })) + it.effect("to provides a partial environment to another layer", () => + Effect.gen(function*() { + const StringTag = Context.GenericTag("string") + const NumberRefTag = Context.GenericTag>("Ref.Ref") + interface FooService { + readonly ref: Ref.Ref + readonly string: string + readonly get: Effect.Effect< + readonly [ + number, + string + ] + > + } + const FooTag = Context.GenericTag("FooService") + const fooBuilder = Layer.context>().pipe( + Layer.map((context) => { + const s = Context.get(context, StringTag) + const ref = Context.get(context, NumberRefTag) + return Context.make(FooTag, { + ref, + string: s, + get: Ref.get(ref).pipe(Effect.map((i) => [i, s] as const)) + }) + }) + ) + const provideNumberRef = Layer.effect(NumberRefTag)(Ref.make(10)) + const provideString = Layer.succeed(StringTag, "hi") + const needsString = fooBuilder.pipe(Layer.provide(provideNumberRef)) + const layer = needsString.pipe(Layer.provide(provideString)) + const result = yield* pipe(Effect.flatMap(FooTag, (_) => _.get), Effect.provide(layer)) + strictEqual(result[0], 10) + strictEqual(result[1], "hi") + })) + it.effect("andTo provides a partial environment to another layer", () => + Effect.gen(function*() { + const StringTag = Context.GenericTag("string") + const NumberRefTag = Context.GenericTag>("Ref.Ref") + interface FooService { + readonly ref: Ref.Ref + readonly string: string + readonly get: Effect.Effect< + readonly [ + number, + string + ] + > + } + const FooTag = Context.GenericTag("FooService") + const fooBuilder = Layer.context>().pipe( + Layer.map((context) => { + const s = Context.get(context, StringTag) + const ref = Context.get(context, NumberRefTag) + return Context.make(FooTag, { + ref, + string: s, + get: Ref.get(ref).pipe(Effect.map((i) => [i, s] as const)) + }) + }) + ) + const provideNumberRef = Layer.effect(NumberRefTag, Ref.make(10)) + const provideString = Layer.succeed(StringTag, "hi") + const needsString = fooBuilder.pipe(Layer.provideMerge(provideNumberRef)) + const layer = needsString.pipe(Layer.provideMerge(provideString)) + const result = yield* pipe( + Effect.flatMap(FooTag, (foo) => foo.get), + Effect.flatMap(([i1, s]) => + NumberRefTag.pipe(Effect.flatMap(Ref.get), Effect.map((i2) => [i1, i2, s] as const)) + ), + Effect.provide(layer) + ) + strictEqual(result[0], 10) + strictEqual(result[1], 10) + strictEqual(result[2], "hi") + })) + it.effect("passthrough passes the inputs through to the next layer", () => + Effect.gen(function*() { + interface NumberService { + readonly value: number + } + const NumberTag = Context.GenericTag("NumberService") + interface ToStringService { + readonly value: string + } + const ToStringTag = Context.GenericTag("ToStringService") + const layer = Layer.function(NumberTag, ToStringTag, (numberService) => ({ + value: numberService.value.toString() + })) + const live = Layer.passthrough(layer).pipe(Layer.provide(Layer.succeed(NumberTag, { value: 1 }))) + const { i, s } = yield* pipe( + Effect.all({ + i: NumberTag, + s: ToStringTag + }), + Effect.provide(live) + ) + strictEqual(i.value, 1) + strictEqual(s.value, "1") + })) + it.effect("project", () => + Effect.gen(function*() { + interface PersonService { + readonly name: string + readonly age: number + } + interface AgeService extends Pick { + } + const PersonTag = Context.GenericTag("PersonService") + const AgeTag = Context.GenericTag("AgeService") + const personLayer = Layer.succeed(PersonTag, { name: "User", age: 42 }) + const ageLayer = personLayer.pipe(Layer.project(PersonTag, AgeTag, (_) => ({ age: _.age }))) + const { age } = yield* pipe(AgeTag, Effect.provide(ageLayer)) + strictEqual(age, 42) + })) + it.effect("sharing with provideTo", () => + Effect.gen(function*() { + const ref = yield* makeRef() + const layer = makeLayer1(ref) + const env = layer.pipe(Layer.provide(layer), Layer.build) + yield* Effect.scoped(env) + const result = yield* Ref.get(ref) + deepStrictEqual(Array.from(result), [acquire1, release1]) + })) + it.effect("sharing with multiple layers with provideTo", () => + Effect.gen(function*() { + const ref = yield* makeRef() + const layer1 = makeLayer1(ref) + const layer2 = makeLayer2(ref) + const layer3 = makeLayer3(ref) + const env = layer3.pipe( + Layer.provide(layer1), + Layer.merge(layer2.pipe(Layer.provide(layer1))), + Layer.build + ) + yield* Effect.scoped(env) + const result = yield* pipe(Ref.get(ref), Effect.map((chunk) => Array.from(chunk))) + strictEqual(result[0], acquire1) + assertTrue(result.slice(1, 3).some((s) => s === acquire2)) + assertTrue(result.slice(1, 3).some((s) => s === acquire3)) + assertTrue(result.slice(3, 5).some((s) => s === release3)) + assertTrue(result.slice(3, 5).some((s) => s === release2)) + strictEqual(result[5], release1) + })) + it.effect("finalizers with provideTo", () => + Effect.gen(function*() { + const ref = yield* makeRef() + const layer1 = makeLayer1(ref) + const layer2 = makeLayer2(ref) + const env = layer2.pipe(Layer.provide(layer1), Layer.build) + yield* Effect.scoped(env) + const result = yield* Ref.get(ref) + deepStrictEqual(Array.from(result), [acquire1, acquire2, release2, release1]) + })) + it.effect("finalizers with multiple layers with provideTo", () => + Effect.gen(function*() { + const ref = yield* makeRef() + const layer1 = makeLayer1(ref) + const layer2 = makeLayer2(ref) + const layer3 = makeLayer3(ref) + const env = layer3.pipe(Layer.provide(layer2), Layer.provide(layer1), Layer.build) + yield* Effect.scoped(env) + const result = yield* Ref.get(ref) + deepStrictEqual(Array.from(result), [acquire1, acquire2, acquire3, release3, release2, release1]) + })) + it.effect("retry", () => + Effect.gen(function*() { + const ref = yield* Ref.make(0) + const effect = ref.pipe(Ref.update((n) => n + 1), Effect.zipRight(Effect.fail("fail"))) + const layer = Layer.effectContext(effect).pipe(Layer.retry(Schedule.recurs(3))) + yield* Effect.ignore(Effect.scoped(Layer.build(layer))) + const result = yield* Ref.get(ref) + strictEqual(result, 4) + })) + it.effect("map does not interfere with sharing", () => + Effect.gen(function*() { + const ref = yield* makeRef() + const layer1 = makeLayer1(ref) + const layer2 = makeLayer2(ref) + const layer3 = makeLayer3(ref) + const env = layer3.pipe( + Layer.provide(layer1), + Layer.provide(layer2), + Layer.provide(Layer.map(layer1, identity)), + Layer.build + ) + yield* Effect.scoped(env) + const result = yield* pipe(Ref.get(ref), Effect.map((chunk) => Array.from(chunk))) + strictEqual(result[0], acquire1) + assertTrue(result.slice(1, 3).some((s) => s === acquire2)) + assertTrue(result.slice(1, 3).some((s) => s === acquire3)) + assertTrue(result.slice(3, 5).some((s) => s === release3)) + assertTrue(result.slice(3, 5).some((s) => s === release2)) + strictEqual(result[5], release1) + })) + it.effect("mapError does not interfere with sharing", () => + Effect.gen(function*() { + const ref = yield* makeRef() + const layer1 = makeLayer1(ref) + const layer2 = makeLayer2(ref) + const layer3 = makeLayer3(ref) + const env = layer3.pipe( + Layer.provide(layer1), + Layer.provide(layer2), + Layer.provide(Layer.mapError(layer1, identity)), + Layer.build + ) + yield* Effect.scoped(env) + const result = yield* pipe(Ref.get(ref), Effect.map((chunk) => Array.from(chunk))) + strictEqual(result[0], acquire1) + assertTrue(result.slice(1, 3).some((s) => s === acquire2)) + assertTrue(result.slice(1, 3).some((s) => s === acquire3)) + assertTrue(result.slice(3, 5).some((s) => s === release3)) + assertTrue(result.slice(3, 5).some((s) => s === release2)) + strictEqual(result[5], release1) + })) + it.effect("orDie does not interfere with sharing", () => + Effect.gen(function*() { + const ref = yield* makeRef() + const layer1 = makeLayer1(ref) + const layer2 = makeLayer2(ref) + const layer3 = makeLayer3(ref) + const env = layer3.pipe( + Layer.provide(layer1), + Layer.provide(layer2), + Layer.provide(Layer.orDie(layer1)), + Layer.build + ) + yield* Effect.scoped(env) + const result = yield* pipe(Ref.get(ref), Effect.map((chunk) => Array.from(chunk))) + strictEqual(result[0], acquire1) + assertTrue(result.slice(1, 3).some((s) => s === acquire2)) + assertTrue(result.slice(1, 3).some((s) => s === acquire3)) + assertTrue(result.slice(3, 5).some((s) => s === release3)) + assertTrue(result.slice(3, 5).some((s) => s === release2)) + strictEqual(result[5], release1) + })) + it.effect("tap peeks at an acquired resource", () => + Effect.gen(function*() { + interface BarService { + readonly bar: string + } + const BarTag = Context.GenericTag("BarService") + const ref: Ref.Ref = yield* Ref.make("foo") + const layer = Layer.succeed(BarTag, { bar: "bar" }).pipe( + Layer.tap((context) => Ref.set(ref, context.pipe(Context.get(BarTag)).bar)) + ) + yield* Effect.scoped(Layer.build(layer)) + const result = yield* Ref.get(ref) + strictEqual(result, "bar") + })) + it.effect("locally", () => + Effect.gen(function*() { + interface BarService { + readonly bar: string + } + const BarTag = Context.GenericTag("BarService") + const fiberRef = FiberRef.unsafeMake(0) + const layer = Layer.locally(fiberRef, 100)( + Layer.effect( + BarTag, + Effect.map( + FiberRef.get(fiberRef), + (n): BarService => ({ bar: `bar: ${n}` }) + ) + ) + ) + const env = yield* Effect.scoped(Layer.build(layer)) + const result = Context.get(env, BarTag) + strictEqual(result.bar, "bar: 100") + })) + it.effect("locallyWith", () => + Effect.gen(function*() { + interface BarService { + readonly bar: string + } + const BarTag = Context.GenericTag("BarService") + const fiberRef = FiberRef.unsafeMake(0) + const layer = Layer.locallyWith(fiberRef, (n) => n + 1)( + Layer.effect( + BarTag, + Effect.map( + FiberRef.get(fiberRef), + (n): BarService => ({ bar: `bar: ${n}` }) + ) + ) + ) + const env = yield* Effect.scoped(Layer.build(layer)) + const result = Context.get(env, BarTag) + strictEqual(result.bar, "bar: 1") + })) + + it.effect("Updates service via updateService", () => + Effect.gen(function*() { + const Foo = Context.GenericTag<"Foo", string>("Foo") + const FooDefault = Layer.succeed(Foo, "Foo") + const Bar = Context.GenericTag<"Bar", string>("Bar") + const BarDefault = Layer.effect(Bar, Foo).pipe( + Layer.updateService(Foo, (x) => `Bar: ${x}`), + Layer.provide(FooDefault) + ) + const result = yield* Bar.pipe(Effect.provide(BarDefault)) + deepStrictEqual(result, "Bar: Foo") + })) + + it.effect("allows passing partial service", () => + Effect.gen(function*() { + class Service1 extends Effect.Service()("Service1", { + succeed: { + one: Effect.succeed(123), + two: () => Effect.succeed(2), + stream: Stream.succeed(3) + } + }) {} + + yield* Effect.gen(function*() { + const service = yield* Service1 + + deepStrictEqual(yield* service.one, 123) + + yield* service.two().pipe( + Effect.catchAllDefect(Effect.fail), + Effect.flip + ) + yield* service.stream.pipe( + Stream.runDrain, + Effect.catchAllDefect(Effect.fail), + Effect.flip + ) + }).pipe( + Effect.provide(Layer.mock(Service1, { + _tag: "Service1", + one: Effect.succeed(123) + })) + ) + })) + + describe("MemoMap", () => { + it.effect("memoizes layer across builds", () => + Effect.gen(function*() { + const ref = yield* makeRef() + const layer1 = makeLayer1(ref) + const layer2 = makeLayer2(ref).pipe( + Layer.provide(layer1) + ) + const memoMap = yield* Layer.makeMemoMap + const scope1 = yield* Scope.make() + const scope2 = yield* Scope.make() + + yield* Layer.buildWithMemoMap(layer1, memoMap, scope1) + yield* Layer.buildWithMemoMap(layer2, memoMap, scope2) + yield* Scope.close(scope2, Exit.void) + yield* Layer.buildWithMemoMap(layer2, memoMap, scope1) + yield* Scope.close(scope1, Exit.void) + + const result = yield* Ref.get(ref) + deepStrictEqual(Array.from(result), [acquire1, acquire2, release2, acquire2, release2, release1]) + })) + + it.effect("layers are not released early", () => + Effect.gen(function*() { + const ref = yield* makeRef() + const layer1 = makeLayer1(ref) + const layer2 = makeLayer2(ref).pipe( + Layer.provide(layer1) + ) + const memoMap = yield* Layer.makeMemoMap + const scope1 = yield* Scope.make() + const scope2 = yield* Scope.make() + + yield* Layer.buildWithMemoMap(layer1, memoMap, scope1) + yield* Layer.buildWithMemoMap(layer2, memoMap, scope2) + yield* Scope.close(scope1, Exit.void) + yield* Scope.close(scope2, Exit.void) + + const result = yield* Ref.get(ref) + deepStrictEqual(Array.from(result), [acquire1, acquire2, release2, release1]) + })) + }) +}) +export const makeRef = (): Effect.Effect>> => { + return Ref.make(Chunk.empty()) +} +export class Service1 { + one(): Effect.Effect { + return Effect.succeed(1) + } +} +export const Service1Tag = Context.GenericTag("Service1") +export const makeLayer1 = (ref: Ref.Ref>): Layer.Layer => { + return Layer.scoped( + Service1Tag, + Effect.acquireRelease( + ref.pipe(Ref.update(Chunk.append(acquire1)), Effect.as(new Service1())), + () => Ref.update(ref, Chunk.append(release1)) + ) + ) +} +export class Service2 { + two(): Effect.Effect { + return Effect.succeed(2) + } +} +export const Service2Tag = Context.GenericTag("Service2") +export const makeLayer2 = (ref: Ref.Ref>): Layer.Layer => { + return Layer.scoped( + Service2Tag, + Effect.acquireRelease( + ref.pipe(Ref.update(Chunk.append(acquire2)), Effect.as(new Service2())), + () => Ref.update(ref, Chunk.append(release2)) + ) + ) +} +export class Service3 { + three(): Effect.Effect { + return Effect.succeed(3) + } +} +export const Service3Tag = Context.GenericTag("Service3") +export const makeLayer3 = (ref: Ref.Ref>): Layer.Layer => { + return Layer.scoped( + Service3Tag, + Effect.acquireRelease( + ref.pipe(Ref.update(Chunk.append(acquire3)), Effect.as(new Service3())), + () => Ref.update(ref, Chunk.append(release3)) + ) + ) +} diff --git a/repos/effect/packages/effect/test/List.test.ts b/repos/effect/packages/effect/test/List.test.ts new file mode 100644 index 0000000..b1b4683 --- /dev/null +++ b/repos/effect/packages/effect/test/List.test.ts @@ -0,0 +1,323 @@ +import { describe, it } from "@effect/vitest" +import { + assertFalse, + assertNone, + assertSome, + assertTrue, + deepStrictEqual, + strictEqual, + throws +} from "@effect/vitest/utils" +import { Array, Chunk, Duration, Either, List, Option } from "effect" +import { equals, symbol } from "effect/Equal" + +const testStructuralSharing = (a: List.List, b: List.List, n = 0): number | undefined => { + if (a === b) { + return n + } + if (List.isCons(a)) { + return testStructuralSharing(a.tail, b, n + 1) + } +} + +describe("List", () => { + it("is an iterable", () => { + deepStrictEqual(Array.fromIterable(List.make(0, 1, 2, 3)), [0, 1, 2, 3]) + }) + + it("isList", () => { + assertTrue(List.isList(List.empty())) + assertTrue(List.isList(List.make(1))) + assertFalse(List.isList(null)) + assertFalse(List.isList({})) + }) + + it("append", () => { + deepStrictEqual(List.append(List.make(1, 2), 3), List.make(1, 2, 3)) + }) + + it("appendAll", () => { + deepStrictEqual(List.appendAll(List.make(1, 2), List.make(3, 4)), List.make(1, 2, 3, 4)) + }) + + it("drop", () => { + deepStrictEqual(List.drop(List.make(1, 2, 3, 4), 2), List.make(3, 4)) + // out of bound + deepStrictEqual(List.drop(List.make(1, 2), -2), List.make(1, 2)) + deepStrictEqual(List.drop(List.make(1, 2), 3), List.empty()) + }) + + it("every", () => { + assertTrue(List.every(List.empty(), (n) => n > 2)) + assertFalse(List.every(List.make(1, 2), (n) => n > 2)) + assertFalse(List.every(List.make(2, 3), (n) => n > 2)) + assertTrue(List.every(List.make(3, 4), (n) => n > 2)) + }) + + it("findFirst", () => { + const item = (a: string, b: string) => ({ a, b }) + const list = List.make(item("a1", "b1"), item("a2", "b2"), item("a3", "b2")) + assertSome(List.findFirst(list, ({ b }) => b === "b2"), item("a2", "b2")) + assertNone(List.findFirst(list, ({ b }) => b === "-")) + }) + + it("flatMap", () => { + deepStrictEqual(List.flatMap(List.empty(), (n) => List.make(n - 1, n + 1)), List.empty()) + deepStrictEqual( + List.flatMap(List.make(1, 2, 3, 4), (n) => List.make(n - 1, n + 1)), + List.make(0, 2, 1, 3, 2, 4, 3, 5) + ) + deepStrictEqual(List.flatMap(List.make(1, 2, 3, 4), () => List.empty()), List.empty()) + }) + + it("forEach", () => { + const as: Array = [] + List.forEach(List.make(1, 2, 3, 4), (n) => as.push(n)) + deepStrictEqual(as, [1, 2, 3, 4]) + }) + + it("head", () => { + assertNone(List.head(List.empty())) + assertSome(List.head(List.make(1, 2, 3)), 1) + }) + + it("isCons", () => { + assertFalse(List.isCons(List.empty())) + assertTrue(List.isCons(List.make(1))) + }) + + it("isNil", () => { + assertTrue(List.isNil(List.nil())) + assertFalse(List.isNil(List.make(1))) + }) + + it("map", () => { + deepStrictEqual(List.map(List.empty(), (n) => n + 1), List.empty()) + deepStrictEqual(List.map(List.make(1, 2, 3, 4), (n) => n + 1), List.make(2, 3, 4, 5)) + }) + + it("mapWithIndex", () => { + deepStrictEqual(List.map(List.empty(), (n, i) => [i, n + 1]), List.empty()) + deepStrictEqual(List.map(List.make(1, 2, 3, 4), (n, i) => [i, n ** 2]), List.make([0, 1], [1, 4], [2, 9], [3, 16])) + }) + + it("partition", () => { + deepStrictEqual(List.partition(List.make(1, 2, 3, 4), (n) => n > 2), [ + List.make(1, 2), + List.make(3, 4) + ]) + }) + + it("partitionMap", () => { + deepStrictEqual( + List.partitionMap(List.make(1, 2, 3, 4), (n) => + n > 2 ? + Either.right(n) : + Either.left(n)), + [List.make(1, 2), List.make(3, 4)] + ) + }) + + it("prependAll", () => { + deepStrictEqual(List.prependAll(List.empty(), List.make(1, 2)), List.make(1, 2)) + deepStrictEqual(List.prependAll(List.make(1, 2), List.empty()), List.make(1, 2)) + deepStrictEqual(List.prependAll(List.make(3), List.make(1, 2)), List.make(1, 2, 3)) + }) + + it("prependAllReversed", () => { + deepStrictEqual(List.prependAllReversed(List.empty(), List.make(1, 2)), List.make(2, 1)) + deepStrictEqual(List.prependAllReversed(List.make(1, 2), List.empty()), List.make(1, 2)) + deepStrictEqual(List.prependAllReversed(List.make(3), List.make(1, 2)), List.make(2, 1, 3)) + }) + + it("reduce", () => { + deepStrictEqual(List.reduce(List.empty(), "-", (b, a) => b + a), "-") + deepStrictEqual(List.reduce(List.make("a", "b", "c"), "-", (b, a) => b + a), "-abc") + }) + + it("reduceRight", () => { + const f = (b: string, a: string) => b + a + deepStrictEqual(List.reduceRight(List.empty(), "", f), "") + deepStrictEqual(List.reduceRight(List.make("a", "b", "c"), "", f), "cba") + }) + + it("reverse", () => { + deepStrictEqual(List.reverse(List.empty()), List.empty()) + deepStrictEqual(List.reverse(List.make(1, 2, 3)), List.make(3, 2, 1)) + }) + + it("toChunk", () => { + deepStrictEqual(List.toChunk(List.empty()), Chunk.empty()) + deepStrictEqual(List.toChunk(List.make(1, 2, 3)), Chunk.make(1, 2, 3)) + }) + + it("toChunk", () => { + throws(() => List.unsafeHead(List.empty()), new Error("Expected List to be non-empty")) + deepStrictEqual(List.unsafeHead(List.make(1, 2, 3)), 1) + }) + + it("some", () => { + assertFalse(List.some(List.empty(), (n) => n > 2)) + assertFalse(List.some(List.make(1, 2), (n) => n > 2)) + assertTrue(List.some(List.make(2, 3), (n) => n > 2)) + assertTrue(List.some(List.make(3, 4), (n) => n > 2)) + }) + + it("splitAt", () => { + deepStrictEqual(List.splitAt(List.make(1, 2, 3, 4), 2), [List.make(1, 2), List.make(3, 4)]) + }) + + it("take", () => { + deepStrictEqual(List.take(List.make(1, 2, 3, 4), 2), List.make(1, 2)) + deepStrictEqual(List.take(List.make(1, 2, 3, 4), 0), List.nil()) + deepStrictEqual(List.take(List.make(1, 2, 3, 4), -10), List.nil()) + deepStrictEqual(List.take(List.make(1, 2, 3, 4), 10), List.make(1, 2, 3, 4)) + }) + + it("tail", () => { + assertNone(List.tail(List.empty())) + assertSome(List.tail(List.make(1, 2, 3)), List.make(2, 3)) + }) + + it("unsafeLast", () => { + throws(() => List.unsafeLast(List.empty()), new Error("Expected List to be non-empty")) + strictEqual(List.unsafeLast(List.make(1, 2, 3, 4)), 4) + }) + + it("unsafeTail", () => { + throws(() => List.unsafeTail(List.empty()), new Error("Expected List to be non-empty")) + deepStrictEqual(List.unsafeTail(List.make(1, 2, 3, 4)), List.make(2, 3, 4)) + }) + + it("pipe()", () => { + deepStrictEqual(List.empty().pipe(List.prepend("a")), List.make("a")) + }) + + it("toString", () => { + strictEqual( + String(List.empty()), + `{ + "_id": "List", + "_tag": "Nil" +}` + ) + strictEqual( + String(List.make(0, 1, 2)), + `{ + "_id": "List", + "_tag": "Cons", + "values": [ + 0, + 1, + 2 + ] +}` + ) + }) + + it("toJSON", () => { + deepStrictEqual(List.empty().toJSON(), { _id: "List", _tag: "Nil" }) + deepStrictEqual(List.make(0, 1, 2).toJSON(), { _id: "List", _tag: "Cons", values: [0, 1, 2] }) + deepStrictEqual(List.make(0, 1, List.empty()).toJSON(), { + _id: "List", + _tag: "Cons", + values: [0, 1, { _id: "List", _tag: "Nil" }] + }) + }) + + it("inspect", () => { + if (typeof window !== "undefined") { + return + } + // eslint-disable-next-line @typescript-eslint/no-require-imports + const { inspect } = require("node:util") + deepStrictEqual(inspect(List.empty()), inspect({ _id: "List", _tag: "Nil" })) + deepStrictEqual(inspect(List.make(0, 1, 2)), inspect({ _id: "List", _tag: "Cons", values: [0, 1, 2] })) + }) + + it("equals", () => { + assertTrue(List.empty()[symbol](List.empty())) + assertTrue(List.make(0)[symbol](List.make(0))) + assertFalse(List.empty()[symbol](Duration.millis(1))) + assertFalse(List.make(0)[symbol](Duration.millis(1))) + + assertTrue(equals(List.empty(), List.empty())) + assertTrue(equals(List.make(0), List.make(0))) + assertFalse(equals(List.empty(), Duration.millis(1))) + assertFalse(equals(List.make(0), Duration.millis(1))) + }) + + it("to iterable", () => { + deepStrictEqual(Array.fromIterable(List.empty()), []) + deepStrictEqual(Array.fromIterable(List.make(1, 2, 3)), [1, 2, 3]) + }) + + it("fromIterable", () => { + deepStrictEqual(List.fromIterable([]), List.empty()) + deepStrictEqual(List.fromIterable([1, 2, 3]), List.make(1, 2, 3)) + }) + + it(".pipe", () => { + deepStrictEqual(List.empty().pipe(List.prepend(1)), List.make(1)) + deepStrictEqual(List.make(2).pipe(List.prepend(1)), List.make(1, 2)) + }) + + it("getEquivalence", () => { + const equivalence = List.getEquivalence(equals) + assertTrue(equivalence(List.empty(), List.empty())) + assertFalse(equivalence(List.empty(), List.of(1))) + assertFalse(equivalence(List.of(1), List.empty())) + assertFalse(equivalence(List.of(1), List.of("a"))) + assertFalse(equivalence(List.make(1, 2, 3), List.make(1, 2))) + assertFalse(equivalence(List.make(1, 2), List.make(1, 2, 3))) + }) + + it("compact", () => { + deepStrictEqual(List.compact(List.empty()), List.empty()) + deepStrictEqual(List.compact(List.make(Option.some(1), Option.some(2), Option.some(3))), List.make(1, 2, 3)) + deepStrictEqual(List.compact(List.make(Option.some(1), Option.none(), Option.some(3))), List.make(1, 3)) + }) + + it("last", () => { + assertNone(List.last(List.empty())) + assertSome(List.last(List.make(1, 2, 3)), 3) + }) + + it("filter", () => { + const isEven = (n: number) => n % 2 === 0 + strictEqual(testStructuralSharing(List.filter(List.empty(), isEven), List.empty()), 0) + + const share1 = List.of(2) + const input1 = List.cons(1, share1) // 1, 2 + const r1 = List.filter(input1, isEven) + deepStrictEqual(r1, List.make(2)) + strictEqual(testStructuralSharing(r1, share1), 0) + + const share2 = List.make(2, 4) + const input2 = List.cons(1, share2) // 1, 2, 4 + const r2 = List.filter(input2, isEven) + deepStrictEqual(r2, List.make(2, 4)) + strictEqual(testStructuralSharing(r2, share2), 0) + + const input3 = List.cons(4, List.cons(3, share1)) // 4, 3, 2 + const r3 = List.filter(input3, isEven) + deepStrictEqual(r3, List.make(4, 2)) + strictEqual(testStructuralSharing(r3, share1), 1) + + deepStrictEqual(List.filter(List.make(2, 4, 1), isEven), List.make(2, 4)) + deepStrictEqual(List.filter(List.make(2, 4, 1, 3), isEven), List.make(2, 4)) + deepStrictEqual(List.filter(List.make(2, 4, 1, 6, 3), isEven), List.make(2, 4, 6)) + const share3 = List.of(6) + const r4 = List.filter(List.appendAll(List.make(2, 4, 1, 3), share3), isEven) + deepStrictEqual(r4, List.make(2, 4, 6)) + strictEqual(testStructuralSharing(r4, share3), 2) + const r5 = List.filter(List.appendAll(List.make(2, 4, 1), share3), isEven) + deepStrictEqual(r5, List.make(2, 4, 6)) + strictEqual(testStructuralSharing(r5, share3), 2) + }) + + it("toArray", () => { + deepStrictEqual(List.toArray(List.empty()), []) + deepStrictEqual(List.toArray(List.make(1, 2, 3)), [1, 2, 3]) + }) +}) diff --git a/repos/effect/packages/effect/test/LogLevel.test.ts b/repos/effect/packages/effect/test/LogLevel.test.ts new file mode 100644 index 0000000..1236fb4 --- /dev/null +++ b/repos/effect/packages/effect/test/LogLevel.test.ts @@ -0,0 +1,16 @@ +import { describe, it } from "@effect/vitest" +import { strictEqual } from "@effect/vitest/utils" +import { LogLevel } from "effect" + +describe("LogLevel", () => { + it("fromLiteral", () => { + strictEqual(LogLevel.fromLiteral("All"), LogLevel.All) + strictEqual(LogLevel.fromLiteral("Debug"), LogLevel.Debug) + strictEqual(LogLevel.fromLiteral("Error"), LogLevel.Error) + strictEqual(LogLevel.fromLiteral("Fatal"), LogLevel.Fatal) + strictEqual(LogLevel.fromLiteral("Info"), LogLevel.Info) + strictEqual(LogLevel.fromLiteral("None"), LogLevel.None) + strictEqual(LogLevel.fromLiteral("Trace"), LogLevel.Trace) + strictEqual(LogLevel.fromLiteral("Warning"), LogLevel.Warning) + }) +}) diff --git a/repos/effect/packages/effect/test/Logger.test.ts b/repos/effect/packages/effect/test/Logger.test.ts new file mode 100644 index 0000000..57c3149 --- /dev/null +++ b/repos/effect/packages/effect/test/Logger.test.ts @@ -0,0 +1,566 @@ +import { afterEach, beforeEach, describe, it, vi } from "@effect/vitest" +import { assertFalse, assertTrue, deepStrictEqual, strictEqual } from "@effect/vitest/utils" +import { + Cause, + Chunk, + Effect, + FiberId, + FiberRefs, + HashMap, + identity, + List, + Logger, + LogLevel, + LogSpan, + pipe +} from "effect" +import { logLevelInfo } from "../src/internal/core.js" + +describe("Logger", () => { + it("isLogger", () => { + assertTrue(Logger.isLogger(Logger.stringLogger)) + assertTrue(Logger.isLogger(Logger.logfmtLogger)) + assertFalse(Logger.isLogger({})) + assertFalse(Logger.isLogger(null)) + assertFalse(Logger.isLogger(undefined)) + }) + + it(".pipe", () => { + strictEqual(Logger.stringLogger.pipe(identity), Logger.stringLogger) + strictEqual(logLevelInfo.pipe(identity), logLevelInfo) + }) +}) + +describe("withLeveledConsole", () => { + it.effect("calls the respective Console functions on a given level", () => + Effect.gen(function*() { + const c = yield* Effect.console + const logs: Array<{ level: string; value: unknown }> = [] + const pusher = (level: string) => (value: unknown) => { + logs.push({ level, value }) + } + const newConsole: typeof c = { + ...c, + unsafe: { + ...c.unsafe, + log: pusher("log"), + warn: pusher("warn"), + error: pusher("error"), + info: pusher("info"), + debug: pusher("debug"), + trace: pusher("trace") + } + } + + const logger = Logger.make((o) => String(o.message)).pipe(Logger.withLeveledConsole) + yield* Effect.gen(function*() { + yield* Effect.log("log plain") + yield* Effect.logInfo("log info") + yield* Effect.logWarning("log warn") + yield* Effect.logError("log err") + yield* Effect.logFatal("log fatal") + yield* Effect.logDebug("log debug") + yield* Effect.logTrace("log trace") + }).pipe( + Effect.provide(Logger.replace(Logger.defaultLogger, logger)), + Logger.withMinimumLogLevel(LogLevel.Trace), + Effect.withConsole(newConsole) + ) + + deepStrictEqual(logs, [ + { level: "info", value: "log plain" }, + { level: "info", value: "log info" }, + { level: "warn", value: "log warn" }, + { level: "error", value: "log err" }, + { level: "error", value: "log fatal" }, + { level: "debug", value: "log debug" }, + { level: "trace", value: "log trace" } + ]) + })) +}) + +describe("stringLogger", () => { + beforeEach(() => { + vi.useFakeTimers() + }) + afterEach(() => { + vi.useRealTimers() + }) + + it("keys with special chars", () => { + const date = new Date() + vi.setSystemTime(date) + const spans = List.make(LogSpan.make("imma span=\"", date.getTime() - 7)) + const annotations = HashMap.make( + ["just_a_key", "just_a_value"], + ["I am bad key name", { coolValue: "cool value" }], + ["good_key", "I am a good value"], + ["good_bool", true], + ["good_number", 123] + ) + + const result = Logger.stringLogger.log({ + fiberId: FiberId.none, + logLevel: logLevelInfo, + message: "My message", + cause: Cause.empty, + context: FiberRefs.unsafeMake(new Map()), + spans, + annotations, + date + }) + + strictEqual( + result, + `timestamp=${date.toJSON()} level=INFO fiber= message="My message" imma_span__=7ms just_a_key=just_a_value good_key="I am a good value" good_bool=true I_am_bad_key_name="{ + \\"coolValue\\": \\"cool value\\" +}" good_number=123` + ) + }) + + it("with linebreaks", () => { + const date = new Date() + vi.setSystemTime(date) + const spans = List.make(LogSpan.make("imma\nspan=\"", date.getTime() - 7)) + const annotations = HashMap.make( + ["I am also\na bad key name", { return: "cool\nvalue" }], + ["good_key", { returnWithSpace: "cool\nvalue or not" }], + ["good_key2", "I am a good value\nwith line breaks"], + ["good_key3", "I_have=a"] + ) + + const result = Logger.stringLogger.log({ + fiberId: FiberId.none, + logLevel: logLevelInfo, + message: "My\nmessage", + cause: Cause.empty, + context: FiberRefs.unsafeMake(new Map()), + spans, + annotations, + date + }) + + strictEqual( + result, + `timestamp=${date.toJSON()} level=INFO fiber= message="My +message" imma_span__=7ms I_am_also_a_bad_key_name="{ + \\"return\\": \\"cool\\nvalue\\" +}" good_key="{ + \\"returnWithSpace\\": \\"cool\\nvalue or not\\" +}" good_key2="I am a good value +with line breaks" good_key3="I_have=a"` + ) + }) + + it("multiple messages", () => { + const date = new Date() + vi.setSystemTime(date) + + const result = Logger.stringLogger.log({ + fiberId: FiberId.none, + logLevel: logLevelInfo, + message: ["a", "b", "c"], + cause: Cause.empty, + context: FiberRefs.unsafeMake(new Map()), + spans: List.empty(), + annotations: HashMap.empty(), + date + }) + + strictEqual(result, `timestamp=${date.toJSON()} level=INFO fiber= message=a message=b message=c`) + }) +}) + +// Adding sequential to the describe block because otherwise the "batched" test fails locally +describe.sequential("logfmtLogger", () => { + beforeEach(() => { + vi.useFakeTimers() + }) + afterEach(() => { + vi.useRealTimers() + }) + + it("keys with special chars", () => { + const date = new Date() + vi.setSystemTime(date) + const spans = List.make(LogSpan.make("imma span=\"", date.getTime() - 7)) + const annotations = HashMap.make( + ["just_a_key", "just_a_value"], + ["I am bad key name", { coolValue: "cool value" }], + ["good_key", "I am a good value"] + ) + + const result = Logger.logfmtLogger.log({ + fiberId: FiberId.none, + logLevel: logLevelInfo, + message: "My message", + cause: Cause.empty, + context: FiberRefs.unsafeMake(new Map()), + spans, + annotations, + date + }) + + strictEqual( + result, + `timestamp=${date.toJSON()} level=INFO fiber= message="My message" imma_span__=7ms just_a_key=just_a_value good_key="I am a good value" I_am_bad_key_name="{\\"coolValue\\":\\"cool value\\"}"` + ) + }) + + it("with linebreaks", () => { + const date = new Date() + vi.setSystemTime(date) + const spans = List.make(LogSpan.make("imma\nspan=\"", date.getTime() - 7)) + const annotations = HashMap.make( + ["I am also\na bad key name", { return: "cool\nvalue" }], + ["good_key", { returnWithSpace: "cool\nvalue or not" }], + ["good_key2", "I am a good value\nwith line breaks"], + ["good_key3", "I_have=a"], + ["good_bool", true], + ["good_number", 123] + ) + + const result = Logger.logfmtLogger.log({ + fiberId: FiberId.none, + logLevel: logLevelInfo, + message: "My\nmessage", + cause: Cause.empty, + context: FiberRefs.unsafeMake(new Map()), + spans, + annotations, + date + }) + + strictEqual( + result, + `timestamp=${date.toJSON()} level=INFO fiber= message="My\\nmessage" imma_span__=7ms I_am_also_a_bad_key_name="{\\"return\\":\\"cool\\\\nvalue\\"}" good_key="{\\"returnWithSpace\\":\\"cool\\\\nvalue or not\\"}" good_bool=true good_number=123 good_key2="I am a good value\\nwith line breaks" good_key3="I_have=a"` + ) + }) + + it("objects", () => { + const date = new Date() + vi.setSystemTime(date) + + const result = Logger.logfmtLogger.log({ + fiberId: FiberId.none, + logLevel: logLevelInfo, + message: { hello: "world" }, + cause: Cause.empty, + context: FiberRefs.unsafeMake(new Map()), + spans: List.empty(), + annotations: HashMap.empty(), + date + }) + + strictEqual(result, `timestamp=${date.toJSON()} level=INFO fiber= message="{\\"hello\\":\\"world\\"}"`) + }) + + it("circular objects", () => { + const date = new Date() + vi.setSystemTime(date) + + const msg: Record = { hello: "world" } + msg.msg = msg + + const result = Logger.logfmtLogger.log({ + fiberId: FiberId.none, + logLevel: logLevelInfo, + message: msg, + cause: Cause.empty, + context: FiberRefs.unsafeMake(new Map()), + spans: List.empty(), + annotations: HashMap.empty(), + date + }) + + strictEqual(result, `timestamp=${date.toJSON()} level=INFO fiber= message="{\\"hello\\":\\"world\\"}"`) + }) + + it("symbols", () => { + const date = new Date() + vi.setSystemTime(date) + + const result = Logger.logfmtLogger.log({ + fiberId: FiberId.none, + logLevel: logLevelInfo, + message: Symbol.for("effect/Logger/test"), + cause: Cause.empty, + context: FiberRefs.unsafeMake(new Map()), + spans: List.empty(), + annotations: HashMap.empty(), + date + }) + + strictEqual(result, `timestamp=${date.toJSON()} level=INFO fiber= message=Symbol(effect/Logger/test)`) + }) + + it("functions", () => { + const date = new Date() + vi.setSystemTime(date) + + const result = Logger.logfmtLogger.log({ + fiberId: FiberId.none, + logLevel: logLevelInfo, + message: () => "hello world", + cause: Cause.empty, + context: FiberRefs.unsafeMake(new Map()), + spans: List.empty(), + annotations: HashMap.empty(), + date + }) + + strictEqual(result, `timestamp=${date.toJSON()} level=INFO fiber= message="() => \\"hello world\\""`) + }) + + it("annotations", () => { + const date = new Date() + vi.setSystemTime(date) + + const annotations = HashMap.make(["hashmap", HashMap.make(["key", 2])], ["chunk", Chunk.make(1, 2)]) + + const result = Logger.logfmtLogger.log({ + fiberId: FiberId.none, + logLevel: logLevelInfo, + message: "hello world", + cause: Cause.empty, + context: FiberRefs.unsafeMake(new Map()), + spans: List.empty(), + annotations, + date + }) + + strictEqual( + result, + `timestamp=${date.toJSON()} level=INFO fiber= message="hello world" hashmap="{\\"_id\\":\\"HashMap\\",\\"values\\":[[\\"key\\",2]]}" chunk="{\\"_id\\":\\"Chunk\\",\\"values\\":[1,2]}"` + ) + }) + + it("batched", () => + Effect.gen(function*() { + const state: Array> = [] + const date = new Date() + vi.setSystemTime(date) + const logger = yield* pipe( + Logger.logfmtLogger, + Logger.batched("100 millis", (strings) => + Effect.sync(() => { + state.push(strings) + })) + ) + const log = (message: string) => + logger.log({ + fiberId: FiberId.none, + logLevel: logLevelInfo, + message, + cause: Cause.empty, + context: FiberRefs.unsafeMake(new Map()), + spans: List.empty(), + annotations: HashMap.empty(), + date + }) + + log("a") + log("b") + log("c") + yield* Effect.promise(() => vi.advanceTimersByTimeAsync(100)) + log("d") + log("e") + yield* Effect.promise(() => vi.advanceTimersByTimeAsync(100)) + + deepStrictEqual(state, [ + [ + `timestamp=${date.toISOString()} level=INFO fiber= message=a`, + `timestamp=${date.toISOString()} level=INFO fiber= message=b`, + `timestamp=${date.toISOString()} level=INFO fiber= message=c` + ], + [ + `timestamp=${date.toISOString()} level=INFO fiber= message=d`, + `timestamp=${date.toISOString()} level=INFO fiber= message=e` + ] + ]) + }).pipe(Effect.scoped, Effect.runPromise)) + + it("multiple messages", () => { + const date = new Date() + vi.setSystemTime(date) + + const result = Logger.logfmtLogger.log({ + fiberId: FiberId.none, + logLevel: logLevelInfo, + message: ["a", "b", "c"], + cause: Cause.empty, + context: FiberRefs.unsafeMake(new Map()), + spans: List.empty(), + annotations: HashMap.empty(), + date + }) + + strictEqual(result, `timestamp=${date.toJSON()} level=INFO fiber= message=a message=b message=c`) + }) +}) + +describe("jsonLogger", () => { + beforeEach(() => { + vi.useFakeTimers() + }) + afterEach(() => { + vi.useRealTimers() + }) + + it("keys with special chars", () => { + const date = new Date() + vi.setSystemTime(date) + const spans = List.make(LogSpan.make("imma span=\"", date.getTime() - 7)) + const annotations = HashMap.make( + ["just_a_key", "just_a_value"], + ["I am bad key name", { coolValue: "cool value" }], + ["good_key", "I am a good value"], + ["good_bool", true], + ["good_number", 123] + ) + + const result = Logger.jsonLogger.log({ + fiberId: FiberId.none, + logLevel: logLevelInfo, + message: "My message", + cause: Cause.empty, + context: FiberRefs.unsafeMake(new Map()), + spans, + annotations, + date + }) + + strictEqual( + result, + JSON.stringify({ + message: "My message", + logLevel: "INFO", + timestamp: date.toJSON(), + annotations: { + just_a_key: "just_a_value", + good_key: "I am a good value", + good_bool: true, + "I am bad key name": { coolValue: "cool value" }, + good_number: 123 + }, + spans: { "imma span=\"": 7 }, + fiberId: "" + }) + ) + }) + + it("objects", () => { + const date = new Date() + vi.setSystemTime(date) + + const result = Logger.jsonLogger.log({ + fiberId: FiberId.none, + logLevel: logLevelInfo, + message: { hello: "world" }, + cause: Cause.empty, + context: FiberRefs.unsafeMake(new Map()), + spans: List.empty(), + annotations: HashMap.empty(), + date + }) + + strictEqual( + result, + JSON.stringify({ + message: { hello: "world" }, + logLevel: "INFO", + timestamp: date.toJSON(), + annotations: {}, + spans: {}, + fiberId: "" + }) + ) + }) + + it("circular objects", () => { + const date = new Date() + vi.setSystemTime(date) + + const msg: Record = { hello: "world" } + msg.msg = msg + + const result = Logger.jsonLogger.log({ + fiberId: FiberId.none, + logLevel: logLevelInfo, + message: msg, + cause: Cause.empty, + context: FiberRefs.unsafeMake(new Map()), + spans: List.empty(), + annotations: HashMap.empty(), + date + }) + + strictEqual( + result, + JSON.stringify({ + message: { hello: "world" }, + logLevel: "INFO", + timestamp: date.toJSON(), + annotations: {}, + spans: {}, + fiberId: "" + }) + ) + }) + + it("symbols", () => { + const date = new Date() + vi.setSystemTime(date) + + const result = Logger.jsonLogger.log({ + fiberId: FiberId.none, + logLevel: logLevelInfo, + message: Symbol.for("effect/Logger/test"), + cause: Cause.empty, + context: FiberRefs.unsafeMake(new Map()), + spans: List.empty(), + annotations: HashMap.empty(), + date + }) + + strictEqual( + result, + JSON.stringify({ + message: Symbol.for("effect/Logger/test").toString(), + logLevel: "INFO", + timestamp: date.toJSON(), + annotations: {}, + spans: {}, + fiberId: "" + }) + ) + }) + + it("functions", () => { + const date = new Date() + vi.setSystemTime(date) + + const result = Logger.jsonLogger.log({ + fiberId: FiberId.none, + logLevel: logLevelInfo, + message: () => "hello world", + cause: Cause.empty, + context: FiberRefs.unsafeMake(new Map()), + spans: List.empty(), + annotations: HashMap.empty(), + date + }) + + strictEqual( + result, + JSON.stringify({ + message: "() => \"hello world\"", + logLevel: "INFO", + timestamp: date.toJSON(), + annotations: {}, + spans: {}, + fiberId: "" + }) + ) + }) +}) diff --git a/repos/effect/packages/effect/test/Mailbox.test.ts b/repos/effect/packages/effect/test/Mailbox.test.ts new file mode 100644 index 0000000..6459906 --- /dev/null +++ b/repos/effect/packages/effect/test/Mailbox.test.ts @@ -0,0 +1,185 @@ +import { describe, it } from "@effect/vitest" +import { assertFalse, assertNone, assertTrue, deepStrictEqual, strictEqual } from "@effect/vitest/utils" +import { Chunk, Effect, Exit, Fiber, Mailbox, Stream } from "effect" + +describe("Mailbox", () => { + it.effect("offerAll with capacity", () => + Effect.gen(function*() { + const mailbox = yield* Mailbox.make(2) + const fiber = yield* mailbox.offerAll([1, 2, 3, 4]).pipe( + Effect.fork + ) + yield* Effect.yieldNow({ priority: 1 }) + assertTrue(fiber.unsafePoll() === null) + + let result = yield* mailbox + deepStrictEqual(Chunk.toReadonlyArray(result[0]), [1, 2]) + assertFalse(result[1]) + + yield* Effect.yieldNow({ priority: 1 }) + assertTrue(fiber.unsafePoll() !== null) + + result = yield* mailbox.takeAll + deepStrictEqual(Chunk.toReadonlyArray(result[0]), [3, 4]) + assertFalse(result[1]) + + yield* Effect.yieldNow({ priority: 1 }) + deepStrictEqual(fiber.unsafePoll(), Exit.succeed(Chunk.empty())) + })) + + it.effect("offer dropping", () => + Effect.gen(function*() { + const mailbox = yield* Mailbox.make({ capacity: 2, strategy: "dropping" }) + const remaining = yield* mailbox.offerAll([1, 2, 3, 4]) + deepStrictEqual(Chunk.toReadonlyArray(remaining), [3, 4]) + const result = yield* mailbox.offer(5) + assertFalse(result) + deepStrictEqual(Chunk.toReadonlyArray((yield* mailbox.takeAll)[0]), [1, 2]) + })) + + it.effect("offer sliding", () => + Effect.gen(function*() { + const mailbox = yield* Mailbox.make({ capacity: 2, strategy: "sliding" }) + const remaining = yield* mailbox.offerAll([1, 2, 3, 4]) + deepStrictEqual(Chunk.toReadonlyArray(remaining), []) + const result = yield* mailbox.offer(5) + assertTrue(result) + deepStrictEqual(Chunk.toReadonlyArray((yield* mailbox.takeAll)[0]), [4, 5]) + })) + + it.effect("offerAll can be interrupted", () => + Effect.gen(function*() { + const mailbox = yield* Mailbox.make(2) + const fiber = yield* mailbox.offerAll([1, 2, 3, 4]).pipe( + Effect.fork + ) + + yield* Effect.yieldNow({ priority: 1 }) + yield* Fiber.interrupt(fiber) + yield* Effect.yieldNow({ priority: 1 }) + + let result = yield* mailbox.takeAll + deepStrictEqual(Chunk.toReadonlyArray(result[0]), [1, 2]) + assertFalse(result[1]) + + yield* mailbox.offer(5) + yield* Effect.yieldNow({ priority: 1 }) + + result = yield* mailbox.takeAll + deepStrictEqual(Chunk.toReadonlyArray(result[0]), [5]) + assertFalse(result[1]) + })) + + it.effect("done completes takes", () => + Effect.gen(function*() { + const mailbox = yield* Mailbox.make(2) + const fiber = yield* mailbox.takeAll.pipe( + Effect.fork + ) + yield* Effect.yieldNow() + yield* mailbox.done(Exit.void) + deepStrictEqual(yield* fiber.await, Exit.succeed([Chunk.empty(), true] as const)) + })) + + it.effect("end", () => + Effect.gen(function*() { + const mailbox = yield* Mailbox.make(2) + yield* Effect.fork(mailbox.offerAll([1, 2, 3, 4])) + yield* Effect.fork(mailbox.offerAll([5, 6, 7, 8])) + yield* Effect.fork(mailbox.offer(9)) + yield* Effect.fork(mailbox.end) + const items = yield* Stream.runCollect(Mailbox.toStream(mailbox)) + deepStrictEqual(Chunk.toReadonlyArray(items), [1, 2, 3, 4, 5, 6, 7, 8, 9]) + strictEqual(yield* mailbox.await, void 0) + strictEqual(yield* mailbox.offer(10), false) + })) + + it.effect("end with take", () => + Effect.gen(function*() { + const mailbox = yield* Mailbox.make(2) + yield* Effect.fork(mailbox.offerAll([1, 2])) + yield* Effect.fork(mailbox.offer(3)) + yield* Effect.fork(mailbox.end) + strictEqual(yield* mailbox.take, 1) + strictEqual(yield* mailbox.take, 2) + strictEqual(yield* mailbox.take, 3) + assertNone(yield* mailbox.take.pipe(Effect.optionFromOptional)) + strictEqual(yield* mailbox.await, void 0) + strictEqual(yield* mailbox.offer(10), false) + })) + + it.effect("fail", () => + Effect.gen(function*() { + const mailbox = yield* Mailbox.make(2) + yield* Effect.fork(mailbox.offerAll([1, 2, 3, 4])) + yield* Effect.fork(mailbox.offer(5)) + yield* Effect.fork(mailbox.fail("boom")) + const takeArr = Effect.map(mailbox.takeAll, ([_]) => Chunk.toReadonlyArray(_)) + deepStrictEqual(yield* takeArr, [1, 2]) + deepStrictEqual(yield* takeArr, [3, 4]) + const [items, done] = yield* mailbox.takeAll + deepStrictEqual(Chunk.toReadonlyArray(items), [5]) + strictEqual(done, false) + const error = yield* mailbox.takeAll.pipe(Effect.flip) + deepStrictEqual(error, "boom") + strictEqual(yield* mailbox.await.pipe(Effect.flip), "boom") + strictEqual(yield* mailbox.offer(6), false) + })) + + it.effect("shutdown", () => + Effect.gen(function*() { + const mailbox = yield* Mailbox.make(2) + yield* Effect.fork(mailbox.offerAll([1, 2, 3, 4])) + yield* Effect.fork(mailbox.offerAll([5, 6, 7, 8])) + yield* Effect.fork(mailbox.shutdown) + const items = yield* Stream.runCollect(Mailbox.toStream(mailbox)) + deepStrictEqual(Chunk.toReadonlyArray(items), []) + strictEqual(yield* mailbox.await, void 0) + strictEqual(yield* mailbox.offer(10), false) + })) + + it.effect("fail doesnt drop items", () => + Effect.gen(function*() { + const mailbox = yield* Mailbox.make(2) + yield* Effect.fork(mailbox.offerAll([1, 2, 3, 4])) + yield* Effect.fork(mailbox.offer(5)) + yield* Effect.fork(mailbox.fail("boom")) + const items: Array = [] + const error = yield* Mailbox.toStream(mailbox).pipe( + Stream.runForEach((item) => Effect.sync(() => items.push(item))), + Effect.flip + ) + deepStrictEqual(items, [1, 2, 3, 4, 5]) + strictEqual(error, "boom") + })) + + it.effect("await waits for no items", () => + Effect.gen(function*() { + const mailbox = yield* Mailbox.make() + const fiber = yield* mailbox.await.pipe(Effect.fork) + yield* Effect.yieldNow() + yield* mailbox.offer(1) + yield* mailbox.end + + yield* Effect.yieldNow() + assertTrue(fiber.unsafePoll() === null) + const [result, done] = yield* mailbox.takeAll + deepStrictEqual(Chunk.toReadonlyArray(result), [1]) + assertTrue(done) + yield* Effect.yieldNow() + assertTrue(fiber.unsafePoll() !== null) + })) + + it.effect("bounded 0 capacity", () => + Effect.gen(function*() { + const mailbox = yield* Mailbox.make(0) + yield* mailbox.offer(1).pipe(Effect.fork) + let result = yield* mailbox.take + strictEqual(result, 1) + + const fiber = yield* mailbox.take.pipe(Effect.fork) + yield* mailbox.offer(2) + result = yield* Fiber.join(fiber) + strictEqual(result, 2) + })) +}) diff --git a/repos/effect/packages/effect/test/ManagedRuntime.test.ts b/repos/effect/packages/effect/test/ManagedRuntime.test.ts new file mode 100644 index 0000000..4b3fee5 --- /dev/null +++ b/repos/effect/packages/effect/test/ManagedRuntime.test.ts @@ -0,0 +1,85 @@ +import { describe, it, test } from "@effect/vitest" +import { deepStrictEqual, strictEqual } from "@effect/vitest/utils" +import { Context, Effect, FiberRef, FiberRefs, Layer, List, ManagedRuntime } from "effect" + +describe("ManagedRuntime", () => { + test("memoizes the layer build", async () => { + let count = 0 + const layer = Layer.effectDiscard(Effect.sync(() => { + count++ + })) + const runtime = ManagedRuntime.make(layer) + await runtime.runPromise(Effect.void) + await runtime.runPromise(Effect.void) + await runtime.dispose() + strictEqual(count, 1) + }) + + test("provides context", async () => { + const tag = Context.GenericTag("string") + const layer = Layer.succeed(tag, "test") + const runtime = ManagedRuntime.make(layer) + const result = await runtime.runPromise(tag) + await runtime.dispose() + strictEqual(result, "test") + }) + + test("provides fiberRefs", async () => { + const layer = Layer.setRequestCaching(true) + const runtime = ManagedRuntime.make(layer) + const result = await runtime.runPromise(FiberRef.get(FiberRef.currentRequestCacheEnabled)) + await runtime.dispose() + strictEqual(result, true) + }) + + test("allows sharing a MemoMap", async () => { + let count = 0 + const layer = Layer.effectDiscard(Effect.sync(() => { + count++ + })) + const runtimeA = ManagedRuntime.make(layer) + const runtimeB = ManagedRuntime.make(layer, runtimeA.memoMap) + await runtimeA.runPromise(Effect.void) + await runtimeB.runPromise(Effect.void) + await runtimeA.dispose() + await runtimeB.dispose() + strictEqual(count, 1) + }) + + it.effect("is subtype of effect", () => + Effect.gen(function*() { + const tag = Context.GenericTag("string") + const layer = Layer.succeed(tag, "test") + const managedRuntime = ManagedRuntime.make(layer) + const runtime = yield* managedRuntime + const result = Context.get(runtime.context, tag) + strictEqual(result, "test") + })) + + it.effect("does not inherit fiber refs", () => + Effect.gen(function*() { + const tag = Context.GenericTag("string") + const layer = Layer.succeed(tag, "test") + const managedRuntime = ManagedRuntime.make(layer) + const runtime = yield* managedRuntime.runtimeEffect.pipe( + Effect.withLogSpan("test") + ) + const result = FiberRefs.getOrDefault(runtime.fiberRefs, FiberRef.currentLogSpan) + deepStrictEqual(result, List.empty()) + })) + + it("can be build synchronously", () => { + const tag = Context.GenericTag("string") + const layer = Layer.succeed(tag, "test") + const managedRuntime = ManagedRuntime.make(layer) + const runtime = Effect.runSync(managedRuntime.runtimeEffect) + const result = Context.get(runtime.context, tag) + strictEqual(result, "test") + }) + + it("is built synchronously with runFork", () => { + const runtime = ManagedRuntime.make(Layer.empty) + runtime.runFork(Effect.void) + runtime.runSync(Effect.void) + }) +}) diff --git a/repos/effect/packages/effect/test/Match.test.ts b/repos/effect/packages/effect/test/Match.test.ts new file mode 100644 index 0000000..a631c67 --- /dev/null +++ b/repos/effect/packages/effect/test/Match.test.ts @@ -0,0 +1,893 @@ +import { describe, it } from "@effect/vitest" +import { + assertFalse, + assertLeft, + assertNone, + assertRight, + assertSome, + assertTrue, + doesNotThrow, + strictEqual, + throws +} from "@effect/vitest/utils" +import { Either, Match as M, Option, pipe, Predicate } from "effect" + +describe("Match", () => { + it("TypeMatcher.pipe() method", () => { + const match = M.type().pipe( + M.when(M.number, (n) => `number: ${n}`), + M.when(M.string, (s) => `string: ${s}`), + M.exhaustive + ) + + strictEqual(match(123), "number: 123") + strictEqual(match("hello"), "string: hello") + }) + + it("ValueMatcher.pipe() method", () => { + const input: string | number = 123 + const match = M.value(input).pipe( + M.when(M.number, (n) => `number: ${n}`), + M.when(M.string, (s) => `string: ${s}`), + M.exhaustive + ) + + strictEqual(match, "number: 123") + }) + + it("exhaustive", () => { + const match = pipe( + M.type<{ a: number } | { b: number }>(), + M.when({ a: M.number }, (_) => _.a), + M.when({ b: M.number }, (_) => _.b), + M.exhaustive + ) + strictEqual(match({ a: 0 }), 0) + strictEqual(match({ b: 1 }), 1) + }) + + it("exhaustive-literal", () => { + const match = pipe( + M.type<{ _tag: "A"; a: number } | { _tag: "B"; b: number }>(), + M.when({ _tag: "A" }, (_) => Either.right(_.a)), + M.when({ _tag: "B" }, (_) => Either.right(_.b)), + M.exhaustive + ) + assertRight(match({ _tag: "A", a: 0 }), 0) + assertRight(match({ _tag: "B", b: 1 }), 1) + }) + + it("schema exhaustive-literal", () => { + const match = pipe( + M.type<{ _tag: "A"; a: number | string } | { _tag: "B"; b: number }>(), + M.when({ _tag: M.is("A", "B"), a: M.number }, (_) => { + return Either.right(_._tag) + }), + M.when({ _tag: M.string, a: M.string }, (_) => { + return Either.right(_._tag) + }), + M.when({ b: M.number }, (_) => Either.left(_._tag)), + M.orElse((_) => { + throw "absurd" + }) + ) + assertRight(match({ _tag: "A", a: 0 }), "A") + assertRight(match({ _tag: "A", a: "hi" }), "A") + assertLeft(match({ _tag: "B", b: 1 }), "B") + }) + + it("exhaustive literal with not", () => { + const match = pipe( + M.type(), + M.when(1, (_) => true), + M.not(1, (_) => false), + M.exhaustive + ) + assertTrue(match(1)) + assertFalse(match(2)) + }) + + it("inline", () => { + const result = pipe( + M.value(Either.right(0)), + M.tag("Right", (_) => _.right), + M.tag("Left", (_) => _.left), + M.exhaustive + ) + strictEqual(result, 0) + }) + + it("piped", () => { + const result = pipe( + Either.right(0), + M.value, + M.when({ _tag: "Right" }, (_) => _.right), + M.option + ) + assertSome(result, 0) + }) + + it("tuples", () => { + const match = pipe( + M.type<[string, string]>(), + M.when(["yeah"], (_) => { + return true + }), + M.option + ) + + assertNone(match({ length: 2 } as any)) + assertNone(match(["a", "b"])) + assertSome(match(["yeah", "a"]), true) + }) + + it("literals", () => { + const match = pipe( + M.type(), + M.when("yeah", (_) => _ === "yeah"), + M.orElse(() => "nah") + ) + + strictEqual(match("yeah"), true) + strictEqual(match("a"), "nah") + }) + + it("piped", () => { + const result = pipe( + Either.right(0), + M.value, + M.when({ _tag: "Right" }, (_) => _.right), + M.option + ) + assertSome(result, 0) + }) + + it("not schema", () => { + const match = pipe( + M.type(), + M.not(M.number, (_) => "a"), + M.when(M.number, (_) => "b"), + M.exhaustive + ) + strictEqual(match("hi"), "a") + strictEqual(match(123), "b") + }) + + it("not literal", () => { + const match = pipe( + M.type(), + M.not("hi", (_) => { + return "a" + }), + M.orElse((_) => "b") + ) + strictEqual(match("hello"), "a") + strictEqual(match("hi"), "b") + }) + + it("literals", () => { + const match = pipe( + M.type(), + M.when("yeah", (_) => { + return _ === "yeah" + }), + M.orElse(() => "nah") + ) + + strictEqual(match("yeah"), true) + strictEqual(match("a"), "nah") + }) + + it("literals duplicate", () => { + const result = pipe( + M.value("yeah" as string), + M.when("yeah", (_) => _ === "yeah"), + M.when("yeah", (_) => "dupe"), + M.orElse((_) => "nah") + ) + + strictEqual(result, true) + }) + + it("discriminator", () => { + const match = pipe( + M.type<{ type: "A" } | { type: "B" }>(), + M.discriminator("type")("A", (_) => _.type), + M.discriminator("type")("B", (_) => _.type), + M.exhaustive + ) + strictEqual(match({ type: "B" }), "B") + }) + + it("discriminator with nullables", () => { + const match = M.type<{ _tag: "A" } | undefined>().pipe( + M.tags({ A: (x) => x._tag }), + M.orElse(() => null) + ) + doesNotThrow(() => match(undefined)) + }) + + it("Match.tag with nullable union", () => { + const match = M.type<{ _tag: "A" } | undefined>().pipe( + M.tag("A", () => "matched A"), + M.when(undefined, () => "matched undefined"), + M.exhaustive + ) + strictEqual(match({ _tag: "A" }), "matched A") + strictEqual(match(undefined), "matched undefined") + }) + + it("Match.tag with null union", () => { + const match = M.type<{ _tag: "A" } | null>().pipe( + M.tag("A", () => "matched A"), + M.when(null, () => "matched null"), + M.exhaustive + ) + strictEqual(match({ _tag: "A" }), "matched A") + strictEqual(match(null), "matched null") + }) + + it("Match.tagStartsWith with nullable union", () => { + const match = M.type<{ _tag: "A.B" } | undefined>().pipe( + M.tagStartsWith("A", () => "matched A prefix"), + M.when(undefined, () => "matched undefined"), + M.exhaustive + ) + strictEqual(match({ _tag: "A.B" }), "matched A prefix") + strictEqual(match(undefined), "matched undefined") + }) + + it("discriminator multiple", () => { + const result = pipe( + M.value(Either.right(0)), + M.discriminator("_tag")("Right", "Left", (_) => "match"), + M.exhaustive + ) + strictEqual(result, "match") + }) + + it("nested", () => { + const match = pipe( + M.type< + | { foo: { bar: { baz: { qux: string } } } } + | { foo: { bar: { baz: { qux: number } } } } + | { foo: { bar: null } } + >(), + M.when({ foo: { bar: { baz: { qux: 2 } } } }, (_) => { + return `literal ${_.foo.bar.baz.qux}` + }), + M.when({ foo: { bar: { baz: { qux: "b" } } } }, (_) => { + return `literal ${_.foo.bar.baz.qux}` + }), + M.when( + { foo: { bar: { baz: { qux: M.number } } } }, + (_) => _.foo.bar.baz.qux + ), + M.when( + { foo: { bar: { baz: { qux: M.string } } } }, + (_) => _.foo.bar.baz.qux + ), + M.when({ foo: { bar: null } }, (_) => _.foo.bar), + M.exhaustive + ) + + strictEqual(match({ foo: { bar: { baz: { qux: 1 } } } }), 1) + strictEqual(match({ foo: { bar: { baz: { qux: 2 } } } }), "literal 2") + strictEqual(match({ foo: { bar: { baz: { qux: "a" } } } }), "a") + strictEqual(match({ foo: { bar: { baz: { qux: "b" } } } }), "literal b") + strictEqual(match({ foo: { bar: null } }), null) + }) + + it("nested Option", () => { + const match = pipe( + M.type<{ user: Option.Option<{ readonly name: string }> }>(), + M.when({ user: { _tag: "Some" } }, (_) => _.user.value.name), + M.orElse((_) => "fail") + ) + + strictEqual(match({ user: Option.some({ name: "a" }) }), "a") + strictEqual(match({ user: Option.none() }), "fail") + }) + + it("predicate", () => { + const match = pipe( + M.type<{ age: number }>(), + M.when({ age: (a) => a >= 5 }, (_) => `Age: ${_.age}`), + M.orElse((_) => `${_.age} is too young`) + ) + + strictEqual(match({ age: 5 }), "Age: 5") + strictEqual(match({ age: 4 }), "4 is too young") + }) + + it("predicate not", () => { + const match = pipe( + M.type<{ age: number }>(), + M.not({ age: (a) => a >= 5 }, (_) => `Age: ${_.age}`), + M.orElse((_) => `${_.age} is too old`) + ) + + strictEqual(match({ age: 4 }), "Age: 4") + strictEqual(match({ age: 5 }), "5 is too old") + + const result = pipe( + M.value({ age: 4 }), + M.not({ age: (a) => a >= 5 }, (_) => `Age: ${_.age}`), + M.orElse((_) => `${_.age} is too old`) + ) + strictEqual(result, "Age: 4") + }) + + it("predicate with functions", () => { + const match = pipe( + M.type<{ + a: number + b: { + c: string + f?: (status: number) => Promise + } + }>(), + M.when({ a: 400 }, (_) => "400"), + M.when({ b: (b) => b.c === "nested" }, (_) => _.b.c), + M.orElse(() => "fail") + ) + + strictEqual(match({ b: { c: "nested" }, a: 200 }), "nested") + strictEqual(match({ b: { c: "nested" }, a: 400 }), "400") + }) + + it("predicate at root level", () => { + const match = pipe( + M.type<{ + a: number + b: { + c: string + f?: (status: number) => Promise + } + }>(), + M.when( + (_) => _.a === 400, + (_) => "400" + ), + M.when({ b: (b) => b.c === "nested" }, (_) => _.b.c), + M.orElse(() => "fail") + ) + + strictEqual(match({ b: { c: "nested" }, a: 200 }), "nested") + strictEqual(match({ b: { c: "nested" }, a: 400 }), "400") + }) + + it("symbols", () => { + const thing = { + symbol: Symbol(), + name: "thing" + } as const + + const match = pipe( + M.value(thing), + M.when({ name: "thing" }, (_) => _.name), + M.exhaustive + ) + + strictEqual(match, "thing") + }) + + it("unify", () => { + const match = pipe( + M.type<{ readonly _tag: "A" } | { readonly _tag: "B" }>(), + M.tag("A", () => Either.right("a") as Either.Either), + M.tag("B", () => Either.right(123) as Either.Either), + M.exhaustive + ) + + assertRight(match({ _tag: "B" }), 123) + }) + + it("optional props", () => { + const match = pipe( + M.type<{ readonly user?: { readonly name: string } | undefined }>(), + M.when({ user: M.any }, (_) => _.user?.name), + M.orElse(() => "no user") + ) + + strictEqual(match({}), "no user") + strictEqual(match({ user: undefined }), undefined) + strictEqual(match({ user: { name: "Tim" } }), "Tim") + }) + + it("optional props defined", () => { + const match = pipe( + M.type<{ readonly user?: { readonly name: string } | null | undefined }>(), + M.when({ user: M.defined }, (_) => _.user.name), + M.orElse(() => "no user") + ) + + strictEqual(match({}), "no user") + strictEqual(match({ user: undefined }), "no user") + strictEqual(match({ user: null }), "no user") + strictEqual(match({ user: { name: "Tim" } }), "Tim") + }) + + it("deep recursive", () => { + type A = + | null + | string + | number + | { [K in string]: A } + + const match = pipe( + M.type(), + M.when(Predicate.isNull, (_) => { + return "null" + }), + M.when(Predicate.isBoolean, (_) => { + return "boolean" + }), + M.when(Predicate.isNumber, (_) => { + return "number" + }), + M.when(Predicate.isString, (_) => { + return "string" + }), + M.when(M.record, (_) => { + return "record" + }), + M.when(Predicate.isSymbol, (_) => { + return "symbol" + }), + M.when(Predicate.isReadonlyRecord, (_) => { + return "readonlyrecord" + }), + M.exhaustive + ) + + strictEqual(match(null), "null") + strictEqual(match(123), "number") + strictEqual(match("hi"), "string") + strictEqual(match({}), "record") + }) + + it("nested option", () => { + type ABC = + | { readonly _tag: "A" } + | { readonly _tag: "B" } + | { readonly _tag: "C" } + + const match = pipe( + M.type<{ readonly abc: Option.Option }>(), + M.when({ abc: { value: { _tag: "A" } } }, (_) => _.abc.value._tag), + M.orElse((_) => "no match") + ) + + strictEqual(match({ abc: Option.some({ _tag: "A" }) }), "A") + strictEqual(match({ abc: Option.some({ _tag: "B" }) }), "no match") + strictEqual(match({ abc: Option.none() }), "no match") + }) + + it("getters", () => { + class Thing { + get name() { + return "thing" + } + } + + const match = pipe( + M.value(new Thing()), + M.when({ name: "thing" }, (_) => _.name), + M.orElse(() => "fail") + ) + + strictEqual(match, "thing") + }) + + it("whenOr", () => { + const match = pipe( + M.type< + { _tag: "A"; a: number } | { _tag: "B"; b: number } | { _tag: "C" } + >(), + M.whenOr({ _tag: "A" }, { _tag: "B" }, (_) => "A or B"), + M.when({ _tag: "C" }, (_) => "C"), + M.exhaustive + ) + strictEqual(match({ _tag: "A", a: 0 }), "A or B") + strictEqual(match({ _tag: "B", b: 1 }), "A or B") + strictEqual(match({ _tag: "C" }), "C") + }) + + it("optional array", () => { + const match = pipe( + M.type<{ a?: ReadonlyArray<{ name: string }> }>(), + M.when({ a: (_) => _.length > 0 }, (_) => `match ${_.a.length}`), + M.orElse(() => "no match") + ) + + strictEqual(match({ a: [{ name: "Tim" }] }), "match 1") + strictEqual(match({ a: [] }), "no match") + strictEqual(match({}), "no match") + }) + + it("whenAnd", () => { + const match = pipe( + M.type< + { _tag: "A"; a: number } | { _tag: "B"; b: number } | { _tag: "C" } + >(), + M.whenAnd({ _tag: "A" }, { a: M.number }, (_) => "A"), + M.whenAnd({ _tag: "B" }, { b: M.number }, (_) => "B"), + M.when({ _tag: "C" }, (_) => "C"), + M.exhaustive + ) + strictEqual(match({ _tag: "A", a: 0 }), "A") + strictEqual(match({ _tag: "B", b: 1 }), "B") + strictEqual(match({ _tag: "C" }), "C") + }) + + it("whenAnd nested", () => { + const match = pipe( + M.type<{ + status: number + user?: { + name: string + manager?: { + name: string + } + } + company?: { + name: string + } + }>(), + M.whenAnd( + { status: 200 }, + { user: { name: M.string } }, + { user: { manager: { name: M.string } } }, + { company: { name: M.string } }, + (_) => + [_.status, _.user.name, _.user.manager.name, _.company.name].join( + ", " + ) + ), + M.whenAnd( + { status: 200 }, + { user: { name: M.string } }, + { company: { name: M.string } }, + (_) => [_.status, _.user.name, _.company.name].join(", ") + ), + M.whenAnd( + { status: 200 }, + { user: { name: M.string } }, + (_) => [_.status, _.user.name].join(", ") + ), + M.whenAnd( + { status: M.number }, + { user: { name: M.string } }, + (_) => ["number", _.user.name].join(", ") + ), + M.when({ status: M.number }, (_) => "number"), + M.exhaustive + ) + strictEqual( + match({ + status: 200, + user: { name: "Tim", manager: { name: "Joe" } }, + company: { name: "Apple" } + }), + "200, Tim, Joe, Apple" + ) + strictEqual( + match({ + status: 200, + user: { name: "Tim" }, + company: { name: "Apple" } + }), + "200, Tim, Apple" + ) + strictEqual( + match({ + status: 200, + user: { name: "Tim" }, + company: { name: "Apple" } + }), + "200, Tim, Apple" + ) + strictEqual( + match({ + status: 200, + user: { name: "Tim" } + }), + "200, Tim" + ) + strictEqual(match({ status: 100, user: { name: "Tim" } }), "number, Tim") + strictEqual(match({ status: 100 }), "number") + }) + + it("instanceOf", () => { + const match = pipe( + M.type(), + M.when(M.instanceOf(Uint8Array), (_) => { + return "uint8" + }), + M.when(M.instanceOf(Uint16Array), (_) => { + return "uint16" + }), + M.orElse((_) => { + throw "absurd" + }) + ) + + strictEqual(match(new Uint8Array([1, 2, 3])), "uint8") + strictEqual(match(new Uint16Array([1, 2, 3])), "uint16") + }) + + it("tags", () => { + const match = pipe( + M.type<{ _tag: "A"; a: number } | { _tag: "B"; b: number }>(), + M.tags({ + A: (_) => _.a, + B: (_) => "B" + }), + M.exhaustive + ) + + strictEqual(match({ _tag: "A", a: 1 }), 1) + strictEqual(match({ _tag: "B", b: 1 }), "B") + }) + + it("tagsExhaustive", () => { + const match = pipe( + M.type<{ _tag: "A"; a: number } | { _tag: "B"; b: number }>(), + M.tagsExhaustive({ + A: (_) => _.a, + B: (_) => "B" + }) + ) + + strictEqual(match({ _tag: "A", a: 1 }), 1) + strictEqual(match({ _tag: "B", b: 1 }), "B") + }) + + it("valueTags", () => { + type Value = { _tag: "A"; a: number } | { _tag: "B"; b: number } + const match = pipe( + { _tag: "A", a: 123 } as Value, + M.valueTags({ + A: (_) => _.a, + B: (_) => "B" + }) + ) + + strictEqual(match, 123) + }) + + it("typeTags", () => { + type Value = { _tag: "A"; a: number } | { _tag: "B"; b: number } + const matcher = M.typeTags() + + strictEqual( + matcher({ + A: (_) => _.a, + B: (_) => "fail" + })({ _tag: "A", a: 123 }), + 123 + ) + + strictEqual( + matcher({ + A: (_) => _.a, + B: (_) => "B" + })({ _tag: "B", b: 123 }), + "B" + ) + }) + + it("refinement - with unknown", () => { + const isArray = (_: unknown): _ is ReadonlyArray => Array.isArray(_) + + const match = pipe( + M.type>(), + M.when(isArray, (_) => { + return "array" + }), + M.when(Predicate.isString, () => "string"), + M.exhaustive + ) + + strictEqual(match([]), "array") + strictEqual(match("fail"), "string") + }) + + it("refinement nested - with unknown", () => { + const isArray = (_: unknown): _ is ReadonlyArray => Array.isArray(_) + + const match = pipe( + M.type<{ readonly a: string | Array }>(), + M.when({ a: isArray }, (_) => "array"), + M.orElse(() => "fail") + ) + + strictEqual(match({ a: [123] }), "array") + strictEqual(match({ a: "fail" }), "fail") + }) + + it("unknown - refinement", () => { + const match = pipe( + M.type(), + M.when(Predicate.isReadonlyRecord, (_) => "record"), + M.orElse(() => "unknown") + ) + + strictEqual(match({}), "record") + strictEqual(match([]), "unknown") + }) + + it("any - refinement", () => { + const match = pipe( + M.type(), + M.when(Predicate.isReadonlyRecord, (_) => "record"), + M.orElse(() => "unknown") + ) + + strictEqual(match({}), "record") + strictEqual(match([]), "unknown") + }) + + it("discriminatorStartsWith", () => { + const match = pipe( + M.type<{ type: "A" } | { type: "B" } | { type: "A.A" } | {}>(), + M.discriminatorStartsWith("type")("A", (_) => 1 as const), + M.discriminatorStartsWith("type")("B", (_) => 2 as const), + M.orElse((_) => 3 as const) + ) + strictEqual(match({ type: "A" }), 1) + strictEqual(match({ type: "A.A" }), 1) + strictEqual(match({ type: "B" }), 2) + strictEqual(match({}), 3) + }) + + it("symbol", () => { + const match = pipe( + M.type(), + M.when(M.symbol, (_) => "symbol"), + M.orElse(() => "else") + ) + strictEqual(match(Symbol.for("a")), "symbol") + strictEqual(match(123), "else") + }) + + it("withReturnType", () => { + const match = pipe( + M.type(), + M.withReturnType(), + M.when("A", (_) => "A"), + M.orElse(() => "else") + ) + strictEqual(match("A"), "A") + strictEqual(match("a"), "else") + }) + + it("withReturnType after predicate", () => { + const match = pipe( + M.type(), + M.when("A", (_) => "A"), + M.withReturnType(), + M.orElse(() => "else") + ) + strictEqual(match("A"), "A") + strictEqual(match("a"), "else") + }) + + it("withReturnType mismatch", () => { + const match = pipe( + M.type(), + M.withReturnType(), + // @ts-expect-error + M.when("A", (_) => 123), + M.orElse(() => "else") + ) + // @ts-expect-error + strictEqual(match("A"), 123) + strictEqual(match("a"), "else") + }) + + it("withReturnType constraint mismatch", () => { + pipe( + M.type(), + M.when("A", (_) => 123), + M.withReturnType(), + // @ts-expect-error + M.orElse(() => "else") + ) + }) + + it("withReturnType union", () => { + const match = pipe( + M.type(), + M.withReturnType<"a" | "b">(), + M.when("A", (_) => "a"), + M.orElse((_) => "b") + ) + strictEqual(match("A"), "a") + strictEqual(match("a"), "b") + }) + + it("withReturnType union mismatch", () => { + pipe( + M.type(), + M.withReturnType<"a" | "b">(), + M.when("A", (_) => "a"), + // @ts-expect-error + M.orElse((_) => "c") + ) + }) + + it("nonEmptyString", () => { + const match = M.type().pipe( + M.when(M.nonEmptyString, () => "ok"), + M.orElse(() => "empty") + ) + + strictEqual(match("hello"), "ok") + strictEqual(match(""), "empty") + }) + + it("is", () => { + const match = M.type().pipe( + M.when(M.is("A"), () => "ok"), + M.orElse(() => "ko") + ) + + strictEqual(match("A"), "ok") + strictEqual(match("C"), "ko") + }) + + it("orElseAbsurd should throw if a match is not found", () => { + const match = M.type().pipe( + M.when(M.is("A", "B"), () => "ok"), + M.orElseAbsurd + ) + strictEqual(match("A"), "ok") + strictEqual(match("B"), "ok") + throws(() => match("C"), new Error("effect/Match/orElseAbsurd: absurd")) + }) + + it("option (with M.value) should return None if a match is not found", () => { + const result = M.value("C").pipe( + M.when(M.is("A", "B"), () => "ok"), + M.option + ) + assertNone(result) + }) + + it("exhaustive should throw on invalid inputs", () => { + const match = M.type<"A">().pipe( + M.when(M.is("A"), () => "ok"), + M.exhaustive + ) + + throws(() => match("C" as "A")) + + throws(() => + M.value("C" as "A").pipe( + M.when(M.is("A"), () => "ok"), + M.exhaustive + ) + ) + }) + + it("orElse (with M.value) should return the default if a match is not found", () => { + const result = M.value("C").pipe( + M.when(M.is("A", "B"), () => "ok"), + M.orElse(() => "default") + ) + strictEqual(result, "default") + }) + + it("tag + withReturnType doesn't need as const for string literals", () => { + type Value = { _tag: "A"; a: number } | { _tag: "B"; b: number } + const result = M.value({ _tag: "A", a: 1 }).pipe( + M.withReturnType<"a" | "b">(), + M.tag("A", () => "a"), + M.tag("B", () => "b"), + M.exhaustive + ) + strictEqual(result, "a") + }) +}) diff --git a/repos/effect/packages/effect/test/Metric.test.ts b/repos/effect/packages/effect/test/Metric.test.ts new file mode 100644 index 0000000..cd636e9 --- /dev/null +++ b/repos/effect/packages/effect/test/Metric.test.ts @@ -0,0 +1,829 @@ +import { describe, it } from "@effect/vitest" +import { assertTrue, deepStrictEqual, strictEqual } from "@effect/vitest/utils" +import { + Array, + Clock, + Duration, + Effect, + Equal, + Fiber, + Metric, + MetricBoundaries, + MetricKey, + MetricLabel, + MetricPolling, + MetricState, + Option, + pipe, + Schedule +} from "effect" + +const labels = [MetricLabel.make("x", "a"), MetricLabel.make("y", "b")] + +const makePollingGauge = (name: string, increment: number) => { + const gauge = Metric.gauge(name) + const metric = MetricPolling.make(gauge, Metric.value(gauge).pipe(Effect.map((gauge) => gauge.value + increment))) + return [gauge, metric] as const +} + +let nameCount = 0 +const nextName = () => `m${++nameCount}` + +describe("Metric", () => { + describe("Counter", () => { + it.effect("custom increment as aspect", () => + Effect.gen(function*() { + const id = nextName() + const counter = Metric.counter(id).pipe(Metric.taggedWithLabels(labels), Metric.withConstantInput(1)) + const result = yield* counter(Effect.void).pipe( + Effect.zipRight(counter(Effect.void)), + Effect.zipRight(Metric.value(counter)) + ) + deepStrictEqual(result, MetricState.counter(2)) + })) + it.effect("direct increment", () => + Effect.gen(function*() { + const id = nextName() + const counter = Metric.counter(id).pipe(Metric.taggedWithLabels(labels)) + const result = yield* Metric.increment(counter).pipe( + Effect.zipRight(Metric.increment(counter)), + Effect.zipRight(Metric.value(counter)) + ) + deepStrictEqual(result, MetricState.counter(2)) + })) + + it.effect("direct increment bigint", () => + Effect.gen(function*() { + const name = nextName() + const counter = Metric.counter(name, { + bigint: true + }).pipe(Metric.taggedWithLabels(labels)) + const result = yield* Metric.increment(counter).pipe( + Effect.zipRight(Metric.increment(counter)), + Effect.zipRight(Metric.value(counter)) + ) + deepStrictEqual(result, MetricState.counter(BigInt(2))) + })) + + it.effect("cannot decrement incremental", () => + Effect.gen(function*() { + const name = nextName() + const counter = Metric.counter(name, { incremental: true }).pipe(Metric.taggedWithLabels(labels)) + const result = yield* Metric.increment(counter).pipe( + Effect.zipRight(Metric.increment(counter)), + Effect.zipRight(Metric.incrementBy(counter, -1)), + Effect.zipRight(Metric.value(counter)) + ) + deepStrictEqual(result, MetricState.counter(2)) + })) + + it.effect("cannot decrement incremental bigint", () => + Effect.gen(function*() { + const name = nextName() + const counter = Metric.counter(name, { + incremental: true, + bigint: true + }).pipe(Metric.taggedWithLabels(labels)) + const result = yield* Metric.increment(counter).pipe( + Effect.zipRight(Metric.increment(counter)), + Effect.zipRight(Metric.incrementBy(counter, BigInt(-1))), + Effect.zipRight(Metric.value(counter)) + ) + deepStrictEqual(result, MetricState.counter(BigInt(2))) + })) + + it.effect("custom increment by value as aspect", () => + Effect.gen(function*() { + const name = nextName() + const counter = Metric.counter(name).pipe(Metric.taggedWithLabels(labels)) + const result = yield* counter(Effect.succeed(10)).pipe( + Effect.zipRight(counter(Effect.succeed(5))), + Effect.zipRight(Metric.value(counter)) + ) + deepStrictEqual(result, MetricState.counter(15)) + })) + + it.effect("custom increment by bigint value as aspect", () => + Effect.gen(function*() { + const name = nextName() + const counter = Metric.counter(name, { bigint: true }).pipe(Metric.taggedWithLabels(labels)) + const result = yield* counter(Effect.succeed(BigInt(10))).pipe( + Effect.zipRight(counter(Effect.succeed(BigInt(5)))), + Effect.zipRight(Metric.value(counter)) + ) + deepStrictEqual(result, MetricState.counter(BigInt(15))) + })) + + it.effect("direct increment referential transparency", () => + Effect.gen(function*() { + const name = nextName() + const result = yield* pipe( + Effect.void, + Effect.withMetric( + Metric.counter(name).pipe( + Metric.taggedWithLabels(labels), + Metric.withConstantInput(1) + ) + ), + Effect.zipRight( + pipe( + Effect.void, + Effect.withMetric(pipe( + Metric.counter(name), + Metric.taggedWithLabels(labels), + Metric.withConstantInput(1) + )) + ) + ), + Effect.zipRight(pipe( + Metric.counter(name), + Metric.taggedWithLabels(labels), + Metric.withConstantInput(1), + Metric.value + )) + ) + deepStrictEqual(result, MetricState.counter(2)) + })) + it.effect("custom increment referential transparency", () => + Effect.gen(function*() { + const name = nextName() + const result = yield* pipe( + Effect.succeed(10), + Effect.withMetric(pipe(Metric.counter(name), Metric.taggedWithLabels(labels))), + Effect.zipRight( + pipe(Effect.succeed(5), Effect.withMetric(pipe(Metric.counter(name), Metric.taggedWithLabels(labels)))) + ), + Effect.zipRight(pipe(Metric.counter(name), Metric.taggedWithLabels(labels), Metric.value)) + ) + deepStrictEqual(result, MetricState.counter(15)) + })) + it.effect("custom increment with mapInput", () => + Effect.gen(function*() { + const name = nextName() + const result = yield* pipe( + Effect.succeed("hello"), + Effect.withMetric( + pipe( + Metric.counter(name), + Metric.taggedWithLabels(labels), + Metric.mapInput((input: string) => input.length) + ) + ), + Effect.zipRight( + pipe( + Effect.succeed("!"), + Effect.withMetric( + pipe( + Metric.counter(name), + Metric.taggedWithLabels(labels), + Metric.mapInput((input: string) => input.length) + ) + ) + ) + ), + Effect.zipRight(pipe(Metric.counter(name), Metric.taggedWithLabels(labels), Metric.value)) + ) + deepStrictEqual(result, MetricState.counter(6)) + })) + it.effect("does not count errors", () => + Effect.gen(function*() { + const name = nextName() + const counter = pipe(Metric.counter(name), Metric.withConstantInput(1)) + const result = yield* pipe( + Effect.void, + Effect.withMetric(counter), + Effect.zipRight(pipe(Effect.fail("error"), Effect.withMetric(counter), Effect.ignore)), + Effect.zipRight(Metric.value(counter)) + ) + deepStrictEqual(result, MetricState.counter(1)) + })) + it.effect("count + taggedWith", () => + Effect.gen(function*() { + const name = nextName() + const base = pipe(Metric.counter(name), Metric.tagged("static", "0"), Metric.withConstantInput(1)) + const counter = pipe( + base, + Metric.taggedWithLabelsInput((input: string) => [MetricLabel.make("dyn", input)]) + ) + const result = yield* pipe( + Effect.succeed("hello"), + Effect.withMetric(counter), + Effect.zipRight(pipe(Effect.succeed("!"), Effect.withMetric(counter))), + Effect.zipRight(pipe(Effect.succeed("!"), Effect.withMetric(counter))), + Effect.zipRight(pipe(base, Metric.tagged("dyn", "!"), Metric.value)) + ) + deepStrictEqual(result, MetricState.counter(2)) + })) + it.effect("tags are a region setting", () => + Effect.gen(function*() { + const name = nextName() + const counter = Metric.counter(name) + const result = yield* pipe( + Metric.increment(counter), + Effect.tagMetrics({ key: "value" }), + Effect.zipRight( + pipe( + counter, + Metric.tagged("key", "value"), + Metric.value + ) + ) + ) + deepStrictEqual(result, MetricState.counter(1)) + })) + }) + describe("Frequency", () => { + it.effect("custom occurrences as aspect", () => + Effect.gen(function*() { + const name = nextName() + const frequency = pipe(Metric.frequency(name), Metric.taggedWithLabels(labels)) + const result = yield* pipe( + Effect.succeed("hello"), + Effect.withMetric(frequency), + Effect.zipRight(pipe(Effect.succeed("hello"), Effect.withMetric(frequency))), + Effect.zipRight(pipe(Effect.succeed("world"), Effect.withMetric(frequency))), + Effect.zipRight(Metric.value(frequency)) + ) + deepStrictEqual(result.occurrences, new Map([["hello", 2] as const, ["world", 1] as const])) + })) + it.effect("direct occurrences", () => + Effect.gen(function*() { + const name = nextName() + const frequency = pipe(Metric.frequency(name), Metric.taggedWithLabels(labels)) + const result = yield* pipe( + frequency, + Metric.update("hello"), + Effect.zipRight(pipe(frequency, Metric.update("hello"))), + Effect.zipRight(pipe(frequency, Metric.update("world"))), + Effect.zipRight(Metric.value(frequency)) + ) + deepStrictEqual(result.occurrences, new Map([["hello", 2] as const, ["world", 1] as const])) + })) + it.effect("custom occurrences with mapInput", () => + Effect.gen(function*() { + const name = nextName() + const frequency = pipe( + Metric.frequency(name), + Metric.taggedWithLabels(labels), + Metric.mapInput((n: number) => `${n}`) + ) + const result = yield* pipe( + Effect.succeed(1), + Effect.withMetric(frequency), + Effect.zipRight(pipe(Effect.succeed(1), Effect.withMetric(frequency))), + Effect.zipRight(pipe(Effect.succeed(2), Effect.withMetric(frequency))), + Effect.zipRight(Metric.value(frequency)) + ) + deepStrictEqual(result.occurrences, new Map([["1", 2] as const, ["2", 1] as const])) + })) + it.effect("occurences + taggedWith", () => + Effect.gen(function*() { + const name = nextName() + const base = pipe(Metric.frequency(name), Metric.taggedWithLabels(labels)) + const frequency = pipe( + base, + Metric.taggedWithLabelsInput((s: string) => [MetricLabel.make("dyn", s)]) + ) + const { result1, result2, result3 } = yield* pipe( + Effect.succeed("hello"), + Effect.withMetric(frequency), + Effect.zipRight(pipe(Effect.succeed("hello"), Effect.withMetric(frequency))), + Effect.zipRight(pipe(Effect.succeed("world"), Effect.withMetric(frequency))), + Effect.zipRight(Effect.all({ + result1: Metric.value(base), + result2: pipe(base, Metric.tagged("dyn", "hello"), Metric.value), + result3: pipe(base, Metric.tagged("dyn", "world"), Metric.value) + })) + ) + strictEqual(result1.occurrences.size, 0) + deepStrictEqual(result2.occurrences, new Map([["hello", 2] as const])) + deepStrictEqual(result3.occurrences, new Map([["world", 1] as const])) + })) + }) + describe("Gauge", () => { + it.effect("custom set as aspect", () => + Effect.gen(function*() { + const name = nextName() + const gauge = pipe(Metric.gauge(name), Metric.taggedWithLabels(labels)) + const result = yield* pipe( + Effect.succeed(1), + Effect.withMetric(gauge), + Effect.zipRight(pipe(Effect.succeed(3), Effect.withMetric(gauge))), + Effect.zipRight(Metric.value(gauge)) + ) + deepStrictEqual(result, MetricState.gauge(3)) + })) + it.effect("direct set", () => + Effect.gen(function*() { + const name = nextName() + const gauge = pipe(Metric.gauge(name), Metric.taggedWithLabels(labels)) + const result = yield* pipe( + gauge, + Metric.set(1), + Effect.zipRight(pipe(gauge, Metric.set(3))), + Effect.zipRight(Metric.value(gauge)) + ) + deepStrictEqual(result, MetricState.gauge(3)) + })) + it.effect("increment", () => + Effect.gen(function*() { + const name = nextName() + const gauge = pipe(Metric.gauge(name), Metric.taggedWithLabels(labels)) + yield* Effect.forEach(Array.range(0, 99), () => Metric.increment(gauge), { concurrency: "unbounded" }) + const result = yield* Metric.value(gauge) + deepStrictEqual(result, MetricState.gauge(100)) + })) + it.effect("custom set with mapInput", () => + Effect.gen(function*() { + const name = nextName() + const gauge = pipe(Metric.gauge(name), Metric.taggedWithLabels(labels), Metric.mapInput((n: number) => n * 2)) + const result = yield* pipe( + Effect.succeed(1), + Effect.withMetric(gauge), + Effect.zipRight(pipe(Effect.succeed(3), Effect.withMetric(gauge))), + Effect.zipRight(Metric.value(gauge)) + ) + deepStrictEqual(result, MetricState.gauge(6)) + })) + it.effect("gauge + taggedWith", () => + Effect.gen(function*() { + const name = nextName() + const base = pipe(Metric.gauge(name), Metric.tagged("static", "0"), Metric.mapInput((s: string) => s.length)) + const gauge = pipe( + base, + Metric.taggedWithLabelsInput((input: string) => [MetricLabel.make("dyn", input)]) + ) + const result = yield* pipe( + Effect.succeed("hello"), + Effect.withMetric(gauge), + Effect.zipRight(pipe(Effect.succeed("!"), Effect.withMetric(gauge))), + Effect.zipRight(pipe(Effect.succeed("!"), Effect.withMetric(gauge))), + Effect.zipRight(pipe(base, Metric.tagged("dyn", "!"), Metric.value)) + ) + deepStrictEqual(result, MetricState.gauge(1)) + })) + }) + describe("Histogram", () => { + it.effect("custom observe as aspect", () => + Effect.gen(function*() { + const name = nextName() + const boundaries = MetricBoundaries.linear({ start: 0, width: 1, count: 10 }) + const histogram = pipe(Metric.histogram(name, boundaries), Metric.taggedWithLabels(labels)) + const result = yield* pipe( + Effect.succeed(1), + Effect.withMetric(histogram), + Effect.zipRight(pipe(Effect.succeed(3), Effect.withMetric(histogram))), + Effect.zipRight(Metric.value(histogram)) + ) + strictEqual(result.count, 2) + strictEqual(result.sum, 4) + strictEqual(result.min, 1) + strictEqual(result.max, 3) + })) + + it.effect("direct observe", () => + Effect.gen(function*() { + const name = nextName() + const boundaries = MetricBoundaries.linear({ start: 0, width: 1, count: 10 }) + const histogram = pipe(Metric.histogram(name, boundaries), Metric.taggedWithLabels(labels)) + const result = yield* pipe( + histogram, + Metric.update(1), + Effect.zipRight(pipe(histogram, Metric.update(3))), + Effect.zipRight(Metric.value(histogram)) + ) + strictEqual(result.count, 2) + strictEqual(result.sum, 4) + strictEqual(result.min, 1) + strictEqual(result.max, 3) + })) + + it.live("histogram with sleeps", () => + it.flakyTest( + Effect.gen(function*() { + const name = nextName() + const boundaries = MetricBoundaries.linear({ start: 0, width: 1, count: 10 }) + const histogram = pipe( + Metric.histogram(name, boundaries), + Metric.taggedWithLabels(labels), + Metric.mapInput((duration: Duration.Duration) => Duration.toMillis(duration) / 1000) + ) + // NOTE: trackDuration always uses the **real** Clock + const start = yield* Effect.sync(() => Date.now()) + yield* pipe(Effect.sleep(Duration.millis(100)), Metric.trackDuration(histogram)) + yield* pipe(Effect.sleep(Duration.millis(300)), Metric.trackDuration(histogram)) + const end = yield* Effect.sync(() => Date.now()) + const elapsed = end - start + const result = yield* Metric.value(histogram) + strictEqual(result.count, 2) + assertTrue(result.sum > 0.39) + assertTrue(result.sum <= elapsed) + assertTrue(result.min >= 0.1) + assertTrue(result.min < result.max) + assertTrue(result.max >= 0.3) + assertTrue(result.max < elapsed) + }) + )) + + it.effect("custom observe with mapInput", () => + Effect.gen(function*() { + const name = nextName() + const boundaries = MetricBoundaries.linear({ start: 0, width: 1, count: 10 }) + const histogram = pipe( + Metric.histogram(name, boundaries), + Metric.taggedWithLabels(labels), + Metric.mapInput((s: string) => s.length) + ) + const result = yield* pipe( + Effect.succeed("x"), + Effect.withMetric(histogram), + Effect.zipRight(pipe(Effect.succeed("xyz"), Effect.withMetric(histogram))), + Effect.zipRight(Metric.value(histogram)) + ) + strictEqual(result.count, 2) + strictEqual(result.sum, 4) + strictEqual(result.min, 1) + strictEqual(result.max, 3) + })) + + it.effect("observe + taggedWith", () => + Effect.gen(function*() { + const name = nextName() + const boundaries = MetricBoundaries.linear({ start: 0, width: 1, count: 10 }) + const base = pipe( + Metric.histogram(name, boundaries), + Metric.taggedWithLabels(labels), + Metric.mapInput((s: string) => s.length) + ) + const histogram = base.pipe( + Metric.taggedWithLabelsInput((input: string) => [MetricLabel.make("dyn", input)]) + ) + const { result1, result2, result3 } = yield* pipe( + Effect.succeed("x"), + Effect.withMetric(histogram), + Effect.zipRight(pipe(Effect.succeed("xyz"), Effect.withMetric(histogram))), + Effect.zipRight(Effect.all({ + result1: Metric.value(base), + result2: pipe(base, Metric.tagged("dyn", "x"), Metric.value), + result3: pipe(base, Metric.tagged("dyn", "xyz"), Metric.value) + })) + ) + strictEqual(result1.count, 0) + strictEqual(result2.count, 1) + strictEqual(result3.count, 1) + })) + + it.effect("preserves precision of boundary values", () => + Effect.gen(function*() { + const preciseBoundaries = [0.005, 0.01, 0.025, 0.05, 0.075, 0.1] + + const histogram = Metric.histogram( + "precision_test", + MetricBoundaries.fromIterable(preciseBoundaries) + ) + + const result = yield* Metric.value(histogram) + + result.buckets.forEach(([boundary], index) => { + if (index < preciseBoundaries.length) { + strictEqual(boundary, preciseBoundaries[index]) + } + }) + })) + }) + + describe("Summary", () => { + it.effect("custom observe as aspect", () => + Effect.gen(function*() { + const name = nextName() + const summary = Metric.summary({ + name, + maxAge: Duration.minutes(1), + maxSize: 10, + error: 0, + quantiles: [0.25, 0.5, 0.75] + }).pipe( + Metric.taggedWithLabels(labels) + ) + const result = yield* pipe( + Effect.succeed(1), + Effect.withMetric(summary), + Effect.zipRight(pipe(Effect.succeed(3), Effect.withMetric(summary))), + Effect.zipRight(Metric.value(summary)) + ) + strictEqual(result.count, 2) + strictEqual(result.sum, 4) + strictEqual(result.min, 1) + strictEqual(result.max, 3) + const medianQuantileValue = result.quantiles[1][1] + strictEqual(Option.getOrNull(medianQuantileValue), 1) + })) + it.effect("direct observe", () => + Effect.gen(function*() { + const name = nextName() + const summary = Metric.summary({ + name, + maxAge: Duration.minutes(1), + maxSize: 10, + error: 0, + quantiles: [0.25, 0.5, 0.75] + }).pipe( + Metric.taggedWithLabels(labels) + ) + const result = yield* pipe( + summary, + Metric.update(1), + Effect.zipRight(pipe(summary, Metric.update(3))), + Effect.zipRight(Metric.value(summary)) + ) + strictEqual(result.count, 2) + strictEqual(result.sum, 4) + strictEqual(result.min, 1) + strictEqual(result.max, 3) + const medianQuantileValue = result.quantiles[1][1] + strictEqual(Option.getOrNull(medianQuantileValue), 1) + })) + it.effect("custom observe with mapInput", () => + Effect.gen(function*() { + const name = nextName() + const summary = Metric.summary({ + name, + maxAge: Duration.minutes(1), + maxSize: 10, + error: 0, + quantiles: [0.25, 0.5, 0.75] + }).pipe( + Metric.taggedWithLabels(labels), + Metric.mapInput((s: string) => s.length) + ) + const result = yield* pipe( + Effect.succeed("x"), + Effect.withMetric(summary), + Effect.zipRight(pipe(Effect.succeed("xyz"), Effect.withMetric(summary))), + Effect.zipRight(Metric.value(summary)) + ) + strictEqual(result.count, 2) + strictEqual(result.sum, 4) + strictEqual(result.min, 1) + strictEqual(result.max, 3) + const medianQuantileValue = result.quantiles[1][1] + strictEqual(Option.getOrNull(medianQuantileValue), 1) + })) + it.effect("observeSummaryWith + taggedWith", () => + Effect.gen(function*() { + const name = nextName() + const base = Metric.summary({ + name, + maxAge: Duration.minutes(1), + maxSize: 10, + error: 0, + quantiles: [0.25, 0.5, 0.75] + }).pipe( + Metric.taggedWithLabels(labels), + Metric.mapInput((s: string) => s.length) + ) + const summary = base.pipe( + Metric.taggedWithLabelsInput((input: string) => [MetricLabel.make("dyn", input)]) + ) + const { result1, result2, result3 } = yield* pipe( + Effect.succeed("x"), + Effect.withMetric(summary), + Effect.zipRight(pipe(Effect.succeed("xyz"), Effect.withMetric(summary))), + Effect.zipRight(Effect.all({ + result1: Metric.value(base), + result2: pipe(base, Metric.tagged("dyn", "x"), Metric.value), + result3: pipe(base, Metric.tagged("dyn", "xyz"), Metric.value) + })) + ) + strictEqual(result1.count, 0) + strictEqual(result2.count, 1) + strictEqual(result3.count, 1) + })) + it.effect("should return correct quantile when first chunk overshoots", () => + Effect.gen(function*() { + const name = nextName() + // Samples: [10 (x6), 20, 30, 40, 50] (10 samples) + // Target rank for 0.5 quantile = 0.5 * 10 = 5 + // Allowed error = (0.01 / 2) * 5 = 0.025. Range [4.975, 5.025] + // First chunk: 6 * 10. candConsumed = 6. 6 > 5.025 + const samples = [10, 10, 10, 10, 10, 10, 20, 30, 40, 50] + const summary = Metric.summary({ + name, + maxAge: Duration.minutes(1), + maxSize: 15, + error: 0.01, + quantiles: [0.5] + }) + + yield* Effect.forEach(samples, (value) => Metric.update(summary, value), { discard: true }) + + const result = yield* Metric.value(summary) + + const medianQuantileValue = result.quantiles[0][1] + + strictEqual(Option.getOrNull(medianQuantileValue), 10) + })) + it.effect("should return no values when no samples are present", () => + Effect.gen(function*() { + const name = nextName() + const summary = Metric.summary({ + name, + maxAge: Duration.minutes(1), + maxSize: 15, + error: 0.01, + quantiles: [0.5] + }) + + const result = yield* Metric.value(summary) + + const medianQuantileValue = result.quantiles[0][1] + const minValue = result.min + const maxValue = result.max + const countValue = result.count + const sumValue = result.sum + + strictEqual(Option.isNone(medianQuantileValue), true) + strictEqual(minValue, 0) + strictEqual(maxValue, 0) + strictEqual(countValue, 0) + strictEqual(sumValue, 0) + })) + }) + describe("Polling", () => { + it.scopedLive("launch should be interruptible", () => + Effect.gen(function*() { + const name = yield* pipe(Clock.currentTimeMillis, Effect.map((now) => `gauge-${now}`)) + const [gauge, metric] = makePollingGauge(name, 1) + const schedule = pipe(Schedule.forever, Schedule.delayed(() => Duration.millis(250))) + const fiber = yield* pipe(metric, MetricPolling.launch(schedule)) + yield* Fiber.interrupt(fiber) + const result = yield* Metric.value(gauge) + strictEqual(result.value, 0) + })) + it.scoped("launch should update the internal metric using the provided Schedule", () => + Effect.gen(function*() { + const name = yield* pipe(Clock.currentTimeMillis, Effect.map((now) => `gauge-${now}`)) + const [gauge, metric] = makePollingGauge(name, 1) + const fiber = yield* pipe(metric, MetricPolling.launch(Schedule.once)) + yield* Fiber.join(fiber) + const result = yield* Metric.value(gauge) + strictEqual(result.value, 1) + })) + it.scoped("collectAll should generate a metric that polls all the provided metrics", () => + Effect.gen(function*() { + const gaugeIncrement1 = 1 + const gaugeIncrement2 = 2 + const pollingCount = 2 + const name1 = yield* pipe(Clock.currentTimeMillis, Effect.map((now) => `gauge1-${now}`)) + const name2 = yield* pipe(Clock.currentTimeMillis, Effect.map((now) => `gauge2-${now}`)) + const [gauge1, metric1] = makePollingGauge(name1, gaugeIncrement1) + const [gauge2, metric2] = makePollingGauge(name2, gaugeIncrement2) + const metric = MetricPolling.collectAll([metric1, metric2]) + const fiber = yield* pipe(metric, MetricPolling.launch(Schedule.recurs(pollingCount))) + yield* Fiber.join(fiber) + const result1 = yield* Metric.value(gauge1) + const result2 = yield* Metric.value(gauge2) + strictEqual(result1.value, gaugeIncrement1 * pollingCount) + strictEqual(result2.value, gaugeIncrement2 * pollingCount) + })) + }) + + it.effect("with a description", () => + Effect.gen(function*() { + const name = "counterName" + const counter1 = Metric.counter(name) + const counter2 = Metric.counter(name, { description: "description1" }) + const counter3 = Metric.counter(name, { description: "description2" }) + + yield* (Metric.update(counter1, 1)) + yield* (Metric.update(counter2, 1)) + yield* (Metric.update(counter3, 1)) + + const result1 = yield* (Metric.value(counter1)) + const result2 = yield* (Metric.value(counter2)) + const result3 = yield* (Metric.value(counter3)) + + const snapshot = yield* (Metric.snapshot) + const pair1 = yield* ( + Array.findFirst(snapshot, (key) => Equal.equals(key.metricKey, MetricKey.counter(name))) + ) + const pair2 = yield* ( + Array.findFirst(snapshot, (key) => + Equal.equals( + key.metricKey, + MetricKey.counter(name, { + description: "description1" + }) + )) + ) + const pair3 = yield* ( + Array.findFirst(snapshot, (key) => + Equal.equals( + key.metricKey, + MetricKey.counter(name, { + description: "description2" + }) + )) + ) + + assertTrue(Equal.equals(result1, MetricState.counter(1))) + assertTrue(Equal.equals(result2, MetricState.counter(1))) + assertTrue(Equal.equals(result3, MetricState.counter(1))) + assertTrue(Equal.equals(pair1.metricState, MetricState.counter(1))) + assertTrue(Option.isNone(pair1.metricKey.description)) + assertTrue(Equal.equals(pair2.metricState, MetricState.counter(1))) + assertTrue(Equal.equals( + pair2.metricKey, + MetricKey.counter(name, { + description: "description1" + }) + )) + assertTrue(Equal.equals(pair3.metricState, MetricState.counter(1))) + assertTrue(Equal.equals( + pair3.metricKey, + MetricKey.counter(name, { + description: "description2" + }) + )) + })) + + it.effect(".register()", () => + Effect.gen(function*() { + const id = nextName() + Metric.counter(id).register() + const snapshot = yield* (Metric.snapshot) + const value = pipe( + Array.fromIterable(snapshot), + Array.findFirst((_) => _.metricKey.name === id) + ) + strictEqual(value._tag, "Some") + })) + describe("trackSuccessWith", () => { + it.effect("infers types in Effectful pipes", () => { + const counter = Metric.counter("counter") + const frequency = Metric.frequency("frequency") + const gauge = Metric.gauge("gauge") + const histogram = Metric.histogram( + "histogram", + MetricBoundaries.linear({ start: 0, width: 10, count: 11 }) + ) + const summary = Metric.summary({ + name: "summary", + maxAge: Duration.minutes(1), + maxSize: 10, + error: 0, + quantiles: [0.25, 0.5, 1] + }) + return Effect.Do.pipe( + Effect.let("step1", () => 1), + Metric.trackSuccessWith(counter, ({ step1 }) => step1), + Effect.let("someThingElse", () => ({ a: 3 })), + Metric.trackSuccessWith(gauge, ({ step1 }) => step1), + Metric.trackSuccessWith(histogram, ({ step1 }) => step1), + Effect.let("anotherPartOfTheState", () => ({ b: "seven" })), + Metric.trackSuccessWith(summary, ({ step1 }) => step1), + Effect.let("step2", () => 4), + Effect.let("step3", () => "foo"), + Metric.trackSuccessWith(counter, ({ step2 }) => step2), + Effect.let("irrelevant", () => "irrelevant"), + Metric.trackSuccessWith(gauge, ({ step2 }) => step2), + Metric.trackSuccessWith(histogram, ({ step2 }) => step2), + Effect.let("moreIrrelevant", () => "moreIrrelevant"), + Metric.trackSuccessWith(summary, ({ step2 }) => step2), + Effect.let("otherStuff", () => ({ x: "otherStuff" })), + Metric.trackSuccessWith(frequency, ({ step3 }) => step3), + Effect.let("step4", () => "bar"), + Metric.trackSuccessWith(frequency, ({ step4 }) => step4), + Effect.bind("results", () => + Effect.all({ + counter: Metric.value(counter), + gauge: Metric.value(gauge), + histogram: Metric.value(histogram), + summary: Metric.value(summary), + frequency: Metric.value(frequency) + })) + ).pipe( + Effect.map(({ results }) => { + deepStrictEqual(results.counter, MetricState.counter(5)) + deepStrictEqual(results.gauge, MetricState.gauge(4)) + strictEqual(results.histogram.count, 2) + strictEqual(results.histogram.sum, 5) + strictEqual(results.histogram.min, 1) + strictEqual(results.histogram.max, 4) + strictEqual(results.summary.count, 2) + strictEqual(results.summary.sum, 5) + strictEqual(results.summary.min, 1) + strictEqual(results.summary.max, 4) + deepStrictEqual(results.summary.quantiles.map((x) => x[1]).map(Option.getOrNull), [1, 1, 4]) + deepStrictEqual( + results.frequency.occurrences, + new Map([ + ["bar", 1], + ["foo", 1] + ]) + ) + }) + ) + }) + }) +}) diff --git a/repos/effect/packages/effect/test/Micro.test.ts b/repos/effect/packages/effect/test/Micro.test.ts new file mode 100644 index 0000000..f65f255 --- /dev/null +++ b/repos/effect/packages/effect/test/Micro.test.ts @@ -0,0 +1,1259 @@ +import { describe, it } from "@effect/vitest" +import { + assertFalse, + assertInclude, + assertInstanceOf, + assertTrue, + deepStrictEqual, + strictEqual +} from "@effect/vitest/utils" +import { Cause, Context, Effect, Either, Exit, Fiber, Micro, Option, pipe } from "effect" + +class ATag extends Context.Tag("ATag")() {} +class TestError extends Micro.TaggedError("TestError") {} + +describe.concurrent("Micro", () => { + describe("tracing", () => { + it.effect("Micro.TaggedError", () => + Micro.gen(function*() { + // Referenced line to be included in the string output + const error = yield* new TestError().pipe(Micro.flip) + deepStrictEqual(error, new TestError()) + assertInclude(error.stack, "Micro.test.ts:20") // <= reference to the line above + })) + + it.effect("withTrace", () => + Micro.gen(function*() { + const error = yield* Micro.fail("boom").pipe( + // Referenced line to be included in the string output + Micro.withTrace("test trace"), + Micro.sandbox, + Micro.flip + ) + assertInclude(error.stack, "at test trace") + assertInclude(error.stack, "Micro.test.ts:29") // <= reference to the line above + })) + }) + + it("runPromise", async () => { + const result = await Micro.runPromise(Micro.succeed(1)) + strictEqual(result, 1) + }) + + it("acquireUseRelease interrupt", async () => { + let acquire = false + let use = false + let release = false + const fiber = Micro.acquireUseRelease( + Micro.sync(() => { + acquire = true + return 123 + }).pipe(Micro.delay(100)), + () => + Micro.sync(() => { + use = true + }), + (_) => + Micro.sync(() => { + strictEqual(_, 123) + release = true + }) + ).pipe(Micro.runFork) + fiber.unsafeInterrupt() + const result = await Micro.runPromise(Micro.fiberAwait(fiber)) + deepStrictEqual(result, Micro.exitInterrupt) + assertTrue(acquire) + assertFalse(use) + assertTrue(release) + }) + + it("acquireUseRelease uninterruptible", async () => { + let acquire = false + let use = false + let release = false + const fiber = Micro.acquireUseRelease( + Micro.sync(() => { + acquire = true + return 123 + }).pipe(Micro.delay(100)), + (_) => + Micro.sync(() => { + use = true + return _ + }), + (_) => + Micro.sync(() => { + strictEqual(_, 123) + release = true + }) + ).pipe(Micro.uninterruptible, Micro.runFork) + fiber.unsafeInterrupt() + const result = await Micro.runPromise(Micro.fiberAwait(fiber)) + deepStrictEqual(result, Micro.exitInterrupt) + assertTrue(acquire) + assertTrue(use) + assertTrue(release) + }) + + it("Context.Tag", () => + Micro.service(ATag).pipe( + Micro.tap((_) => Micro.sync(() => strictEqual(_, "A"))), + Micro.provideService(ATag, "A"), + Micro.runPromise + )) + + describe("fromOption", () => { + it("from a some", () => + Option.some("A").pipe( + Micro.fromOption, + Micro.tap((_) => strictEqual(_, "A")), + Micro.runPromise + )) + + it("from a none", () => + Option.none().pipe( + Micro.fromOption, + Micro.flip, + Micro.tap((error) => assertInstanceOf(error, Micro.NoSuchElementException)), + Micro.runPromise + )) + }) + + describe("fromEither", () => { + it("from a right", () => + Either.right("A").pipe( + Micro.fromEither, + Micro.tap((_) => Micro.sync(() => strictEqual(_, "A"))), + Micro.runPromise + )) + + it("from a left", () => + Either.left("error").pipe( + Micro.fromEither, + Micro.flip, + Micro.tap((error) => Micro.sync(() => strictEqual(error, "error"))), + Micro.runPromise + )) + }) + + describe("gen", () => { + it("gen", () => + Micro.gen(function*() { + const result = yield* Micro.succeed(1) + strictEqual(result, 1) + return result + }).pipe(Micro.runPromise).then((_) => deepStrictEqual(_, 1))) + + it("gen with context", () => + Micro.gen({ a: 1, b: 2 }, function*() { + const result = yield* Micro.succeed(this.a) + strictEqual(result, 1) + return result + this.b + }).pipe(Micro.runPromise).then((_) => deepStrictEqual(_, 3))) + }) + + describe("forEach", () => { + it("sequential", () => + Micro.gen(function*() { + const results = yield* Micro.forEach([1, 2, 3], (_) => Micro.succeed(_)) + deepStrictEqual(results, [1, 2, 3]) + }).pipe(Micro.runPromise)) + + it("unbounded", () => + Micro.gen(function*() { + const results = yield* Micro.forEach([1, 2, 3], (_) => Micro.succeed(_), { concurrency: "unbounded" }) + deepStrictEqual(results, [1, 2, 3]) + }).pipe(Micro.runPromise)) + + it("bounded", () => + Micro.gen(function*() { + const results = yield* Micro.forEach([1, 2, 3, 4, 5], (_) => Micro.succeed(_), { concurrency: 2 }) + deepStrictEqual(results, [1, 2, 3, 4, 5]) + }).pipe(Micro.runPromise)) + + it("inherit unbounded", () => + Micro.gen(function*() { + const handle = yield* Micro.forEach([1, 2, 3], (_) => Micro.succeed(_).pipe(Micro.delay(50)), { + concurrency: "inherit" + }).pipe( + Micro.withConcurrency("unbounded"), + Micro.fork + ) + yield* Micro.sleep(90) + deepStrictEqual(handle.unsafePoll(), Micro.exitSucceed([1, 2, 3])) + }).pipe(Micro.runPromise)) + + it("sequential interrupt", () => + Micro.gen(function*() { + const done: Array = [] + const fiber = yield* Micro.forEach([1, 2, 3, 4, 5, 6], (i) => + Micro.sync(() => { + done.push(i) + return i + }).pipe(Micro.delay(300))).pipe(Micro.fork) + yield* Micro.sleep(800) + yield* Micro.fiberInterrupt(fiber) + const result = yield* Micro.fiberAwait(fiber) + deepStrictEqual(result, Micro.exitInterrupt) + deepStrictEqual(done, [1, 2]) + }).pipe(Micro.runPromise)) + + it("unbounded interrupt", () => + Micro.gen(function*() { + const done: Array = [] + const fiber = yield* Micro.forEach([1, 2, 3], (i) => + Micro.sync(() => { + done.push(i) + return i + }).pipe(Micro.delay(150)), { concurrency: "unbounded" }).pipe(Micro.fork) + yield* Micro.sleep(50) + yield* Micro.fiberInterrupt(fiber) + const result = yield* Micro.fiberAwait(fiber) + deepStrictEqual(result, Micro.exitInterrupt) + deepStrictEqual(done, []) + }).pipe(Micro.runPromise)) + + it("bounded interrupt", () => + Micro.gen(function*() { + const done: Array = [] + const fiber = yield* Micro.forEach([1, 2, 3, 4, 5, 6], (i) => + Micro.sync(() => { + done.push(i) + return i + }).pipe(Micro.delay(200)), { concurrency: 2 }).pipe(Micro.fork) + yield* Micro.sleep(350) + yield* Micro.fiberInterrupt(fiber) + const result = yield* Micro.fiberAwait(fiber) + deepStrictEqual(result, Micro.exitInterrupt) + deepStrictEqual(done, [1, 2]) + }).pipe(Micro.runPromise)) + + // TODO: mark this test as flaky, unfortunately I was not able to reproduce it locally + it("unbounded fail", { retry: 3 }, () => + Micro.gen(function*() { + const done: Array = [] + const handle = yield* Micro.forEach([1, 2, 3, 4, 5], (i) => + Micro.suspend(() => { + done.push(i) + return i === 3 ? Micro.fail("error") : Micro.succeed(i) + }).pipe(Micro.delay(i * 100)), { + concurrency: "unbounded" + }).pipe(Micro.fork) + const result = yield* Micro.fiberAwait(handle) + deepStrictEqual(result, Micro.exitFail("error")) + deepStrictEqual(done, [1, 2, 3]) + }).pipe(Micro.runPromise)) + + it("length = 0", () => + Micro.gen(function*() { + const results = yield* Micro.forEach([], (_) => Micro.succeed(_)) + deepStrictEqual(results, []) + }).pipe(Micro.runPromise)) + }) + + describe("all", () => { + it("tuple", () => + Micro.gen(function*() { + const results = (yield* Micro.all([ + Micro.succeed(1), + Micro.succeed(2), + Micro.succeed(3) + ])) satisfies [ + number, + number, + number + ] + deepStrictEqual(results, [1, 2, 3]) + }).pipe(Micro.runPromise)) + + it("record", () => + Micro.gen(function*() { + const results = (yield* Micro.all({ + a: Micro.succeed(1), + b: Micro.succeed("2"), + c: Micro.succeed(true) + })) satisfies { + a: number + b: string + c: boolean + } + deepStrictEqual(results, { + a: 1, + b: "2", + c: true + }) + }).pipe(Micro.runPromise)) + + it.effect("record discard", () => + Micro.gen(function*() { + const results = (yield* Micro.all({ + a: Micro.succeed(1), + b: Micro.succeed("2"), + c: Micro.succeed(true) + }, { discard: true })) satisfies void + deepStrictEqual(results, void 0) + })) + + it.effect("iterable", () => + Micro.gen(function*() { + const results = (yield* Micro.all( + new Set([ + Micro.succeed(1), + Micro.succeed(2), + Micro.succeed(3) + ]) + )) satisfies Array + deepStrictEqual(results, [1, 2, 3]) + })) + }) + + describe("filter", () => { + it.live("odd numbers", () => + Micro.gen(function*() { + const results = yield* Micro.filter([1, 2, 3, 4, 5], (_) => Micro.succeed(_ % 2 === 1)) + deepStrictEqual(results, [1, 3, 5]) + })) + + it.live("iterable", () => + Micro.gen(function*() { + const results = yield* Micro.filter(new Set([1, 2, 3, 4, 5]), (_) => Micro.succeed(_ % 2 === 1)) + deepStrictEqual(results, [1, 3, 5]) + })) + }) + + describe("acquireRelease", () => { + it("releases on interrupt", () => + Micro.gen(function*() { + let release = false + const fiber = yield* Micro.acquireRelease( + Micro.delay(Micro.succeed("foo"), 100), + () => + Micro.sync(() => { + release = true + }) + ).pipe(Micro.scoped, Micro.fork) + yield* Micro.yieldFlush + fiber.unsafeInterrupt() + yield* Micro.fiberAwait(fiber) + strictEqual(release, true) + }).pipe(Micro.runPromise)) + }) + + it.effect("raceAll", () => + Micro.gen(function*() { + const interrupted: Array = [] + const result = yield* Micro.raceAll([500, 300, 200, 0, 100].map((ms) => + (ms === 0 ? Micro.fail("boom") : Micro.succeed(ms)).pipe( + Micro.delay(ms), + Micro.onInterrupt( + Micro.sync(() => { + interrupted.push(ms) + }) + ) + ) + )) + strictEqual(result, 100) + deepStrictEqual(interrupted, [500, 300, 200]) + })) + + it("raceAllFirst", () => + Micro.gen(function*() { + const interrupted: Array = [] + const result = yield* Micro.raceAllFirst([500, 300, 200, 0, 100].map((ms) => + (ms === 0 ? Micro.fail("boom") : Micro.succeed(ms)).pipe( + Micro.delay(ms), + Micro.onInterrupt( + Micro.sync(() => { + interrupted.push(ms) + }) + ) + ) + )).pipe(Micro.exit) + deepStrictEqual(result, Micro.exitFail("boom")) + deepStrictEqual(interrupted, [500, 300, 200, 100]) + }).pipe(Micro.runPromise)) + + describe("valid Effect", () => { + it.effect("success", () => + Effect.gen(function*() { + const result = yield* Micro.succeed(123) + strictEqual(result, 123) + })) + + it.effect("failure", () => + Effect.gen(function*() { + const result = yield* Micro.fail("boom").pipe( + Effect.sandbox, + Effect.flip + ) + deepStrictEqual(result, Cause.fail("boom")) + })) + + it.effect("defects", () => + Effect.gen(function*() { + const result = yield* Micro.die("boom").pipe( + Effect.sandbox, + Effect.flip + ) + deepStrictEqual(result, Cause.die("boom")) + })) + + it.effect("context", () => + Effect.gen(function*() { + const result = yield* Micro.service(ATag).pipe( + Micro.map((_) => _) + ) + deepStrictEqual(result, "A") + }).pipe(Effect.provideService(ATag, "A"))) + + it.effect("interruption", () => + Effect.gen(function*() { + const fiber = yield* Micro.never.pipe( + Effect.fork + ) + yield* Effect.yieldNow() + yield* Fiber.interrupt(fiber) + const exit = yield* fiber.await + assertTrue(Exit.isInterrupted(exit)) + })) + }) + + describe("repeat", () => { + it.effect("is stack safe", () => + Micro.void.pipe( + Micro.repeat({ times: 10000 }) + )) + + it.effect("is interruptible", () => + Micro.void.pipe( + Micro.forever, + Micro.timeoutOption(50) + )) + + it("works with runSync", () => { + const result = Micro.succeed(123).pipe( + Micro.repeat({ times: 1000 }), + Micro.runSync + ) + deepStrictEqual(result, 123) + }) + + it.effect("scheduleRecurs", () => + Micro.gen(function*() { + let count = 0 + yield* Micro.sync(() => count++).pipe( + Micro.repeat({ + schedule: Micro.scheduleRecurs(3) + }) + ) + deepStrictEqual(count, 4) + })) + }) + + describe("retry", () => { + it.live("nothing on success", () => + Micro.gen(function*() { + let count = 0 + yield* Micro.sync(() => count++).pipe( + Micro.retry({ times: 10000 }) + ) + strictEqual(count, 1) + })) + + it.effect("initial + retries", () => + Micro.gen(function*() { + let count = 0 + const error = yield* Micro.failSync(() => ++count).pipe( + Micro.retry({ times: 2 }), + Micro.flip + ) + strictEqual(error, 3) + })) + + it.effect("predicate", () => + Micro.gen(function*() { + let count = 0 + const error = yield* Micro.failSync(() => ++count).pipe( + Micro.retry({ while: (i) => i < 3 }), + Micro.flip + ) + strictEqual(error, 3) + })) + }) + + describe("timeoutOption", () => { + it.live("timeout a long computation", () => + Micro.gen(function*() { + const result = yield* pipe( + Micro.sleep(60_000), + Micro.andThen(Micro.succeed(true)), + Micro.timeoutOption(10) + ) + deepStrictEqual(result, Option.none()) + })) + it.live("timeout a long computation with a failure", () => + Micro.gen(function*() { + const error = new Error("boom") + const result = yield* pipe( + Micro.sleep(5000), + Micro.andThen(Micro.succeed(true)), + Micro.timeoutOrElse({ + onTimeout: () => Micro.die(error), + duration: 10 + }), + Micro.sandbox, + Micro.flip + ) + deepStrictEqual(result, Micro.causeDie(error)) + })) + it.effect("timeout repetition of uninterruptible effect", () => + Micro.gen(function*() { + const result = yield* pipe( + Micro.void, + Micro.uninterruptible, + Micro.forever, + Micro.timeoutOption(10) + ) + deepStrictEqual(result, Option.none()) + })) + it.effect("timeout in uninterruptible region", () => + Micro.gen(function*() { + yield* Micro.void.pipe(Micro.timeoutOption(20_000), Micro.uninterruptible) + }), { timeout: 1000 }) + }) + + describe("timeout", () => { + it.live("timeout a long computation", () => + Micro.gen(function*() { + const result = yield* pipe( + Micro.sleep(60_000), + Micro.andThen(Micro.succeed(true)), + Micro.timeout(10), + Micro.flip + ) + deepStrictEqual(result, new Micro.TimeoutException()) + })) + }) + + describe("Error", () => { + class TestError extends Micro.Error {} + + it.effect("is yieldable", () => + Micro.gen(function*() { + const error = yield* new TestError().pipe(Micro.flip) + deepStrictEqual(error, new TestError()) + })) + + it.effect("is a valid Effect", () => + Effect.gen(function*() { + const error = yield* new TestError().pipe(Effect.flip) + deepStrictEqual(error, new TestError()) + })) + }) + + describe("TaggedError", () => { + it.effect("is a valid Effect", () => + Effect.gen(function*() { + const error = yield* new TestError().pipe(Effect.flip) + deepStrictEqual(error, new TestError()) + })) + + it.effect("has a _tag", () => + Micro.gen(function*() { + const result = yield* new TestError().pipe( + Micro.catchTag("TestError", (_) => Micro.succeed(true)) + ) + strictEqual(result, true) + })) + }) + + describe("failure rendering", () => { + it.effect("renders non-error defects", () => + Micro.gen(function*() { + const failure = yield* Micro.die({ some: "error" }).pipe( + Micro.withTrace("test trace"), + Micro.sandbox, + Micro.flip + ) + strictEqual(failure.name, "MicroCause.Die") + strictEqual(failure.message, JSON.stringify({ some: "error" })) + assertInclude(failure.stack, `MicroCause.Die: ${JSON.stringify({ some: "error" })}`) + assertInclude(failure.stack, "at test trace (") + })) + + it.effect("renders non-errors", () => + Micro.gen(function*() { + const failure = yield* Micro.fail({ some: "error" }).pipe( + Micro.withTrace("test trace"), + Micro.sandbox, + Micro.flip + ) + strictEqual(failure.name, "MicroCause.Fail") + strictEqual(failure.message, JSON.stringify({ some: "error" })) + assertInclude(failure.stack, `MicroCause.Fail: ${JSON.stringify({ some: "error" })}`) + assertInclude(failure.stack, "at test trace (") + })) + + it.effect("renders errors", () => + Micro.gen(function*() { + const failure = yield* Micro.fail(new Error("boom")).pipe( + Micro.withTrace("test trace"), + Micro.sandbox, + Micro.flip + ) + strictEqual(failure.name, "(MicroCause.Fail) Error") + strictEqual(failure.message, "boom") + assertInclude(failure.stack, `(MicroCause.Fail) Error: boom`) + assertInclude(failure.stack, "at test trace (") + })) + }) + + describe("interruption", () => { + it.effect("sync forever is interruptible", () => + Micro.gen(function*() { + const fiber = yield* pipe(Micro.succeed(1), Micro.forever, Micro.fork) + yield* Micro.fiberInterrupt(fiber) + deepStrictEqual(fiber.unsafePoll(), Micro.exitInterrupt) + })) + + it.effect("interrupt of never is interrupted with cause", () => + Micro.gen(function*() { + const fiber = yield* Micro.fork(Micro.never) + yield* Micro.fiberInterrupt(fiber) + deepStrictEqual(fiber.unsafePoll(), Micro.exitInterrupt) + })) + + it.effect("catchAll + ensuring + interrupt", () => + Micro.gen(function*() { + let catchFailure = false + let ensuring = false + const handle = yield* Micro.never.pipe( + Micro.catchAllCause((_) => + Micro.sync(() => { + catchFailure = true + }) + ), + Micro.ensuring(Micro.sync(() => { + ensuring = true + })), + Micro.fork + ) + yield* Micro.yieldFlush + yield* Micro.fiberInterrupt(handle) + assertFalse(catchFailure) + assertTrue(ensuring) + })) + + it.effect("run of interruptible", () => + Micro.gen(function*() { + let recovered = false + const fiber = yield* Micro.never.pipe( + Micro.interruptible, + Micro.exit, + Micro.flatMap((result) => + Micro.sync(() => { + recovered = result._tag === "Failure" && result.cause._tag === "Interrupt" + }) + ), + Micro.uninterruptible, + Micro.fork + ) + yield* Micro.yieldFlush + yield* Micro.fiberInterrupt(fiber) + assertTrue(recovered) + })) + + it.effect("alternating interruptibility", () => + Micro.gen(function*() { + let counter = 0 + const fiber = yield* Micro.never.pipe( + Micro.interruptible, + Micro.exit, + Micro.andThen(Micro.sync(() => { + counter++ + })), + Micro.uninterruptible, + Micro.interruptible, + Micro.exit, + Micro.andThen(Micro.sync(() => { + counter++ + })), + Micro.uninterruptible, + Micro.fork + ) + yield* Micro.yieldFlush + yield* Micro.fiberInterrupt(fiber) + strictEqual(counter, 2) + })) + + it.live("acquireUseRelease use inherits interrupt status", () => + Micro.gen(function*() { + let ref = false + const fiber = yield* Micro.acquireUseRelease( + Micro.succeed(123), + (_) => + Micro.sync(() => { + ref = true + }).pipe( + Micro.delay(10) + ), + () => Micro.void + ).pipe( + Micro.uninterruptible, + Micro.fork + ) + yield* Micro.yieldFlush + yield* Micro.fiberInterrupt(fiber) + assertTrue(ref) + })) + + it.live("async can be uninterruptible", () => + Micro.gen(function*() { + let ref = false + const fiber = yield* Micro.sleep(10).pipe( + Micro.andThen(() => { + ref = true + }), + Micro.uninterruptible, + Micro.fork + ) + yield* Micro.yieldFlush + yield* Micro.fiberInterrupt(fiber) + assertTrue(ref) + })) + + it.live("async cannot resume on interrupt", () => + Micro.gen(function*() { + const fiber = yield* Micro.async((resume) => { + setTimeout(() => { + resume(Micro.succeed("foo")) + }, 10) + }).pipe( + Micro.onInterrupt(Micro.sleep(30)), + Micro.fork + ) + yield* Micro.yieldFlush + yield* Micro.fiberInterrupt(fiber) + deepStrictEqual(fiber.unsafePoll(), Micro.exitInterrupt) + })) + + it.live("closing scope is uninterruptible", () => + Micro.gen(function*() { + let ref = false + const child = pipe( + Micro.sleep(10), + Micro.andThen(() => { + ref = true + }) + ) + const fiber = yield* child.pipe(Micro.uninterruptible, Micro.fork) + yield* Micro.yieldFlush + yield* Micro.fiberInterrupt(fiber) + assertTrue(ref) + })) + + it.effect("AbortSignal is aborted", () => + Micro.gen(function*() { + let signal: AbortSignal + const fiber = yield* Micro.async((_cb, signal_) => { + signal = signal_ + }).pipe(Micro.fork) + yield* Micro.yieldFlush + yield* Micro.fiberInterrupt(fiber) + strictEqual(signal!.aborted, true) + })) + }) + + describe("fork", () => { + it.effect("is interrupted with parent", () => + Micro.gen(function*() { + let child = false + let parent = false + const fiber = yield* Micro.never.pipe( + Micro.onInterrupt(Micro.sync(() => { + child = true + })), + Micro.fork, + Micro.andThen(Micro.never), + Micro.onInterrupt(Micro.sync(() => { + parent = true + })), + Micro.fork + ) + yield* Micro.yieldFlush + yield* Micro.fiberInterrupt(fiber) + yield* Micro.yieldFlush + assertTrue(child) + assertTrue(parent) + })) + }) + + describe("forkDaemon", () => { + it.effect("is not interrupted with parent", () => + Micro.gen(function*() { + let child = false + let parent = false + const handle = yield* Micro.never.pipe( + Micro.onInterrupt(Micro.sync(() => { + child = true + })), + Micro.forkDaemon, + Micro.andThen(Micro.never), + Micro.onInterrupt(Micro.sync(() => { + parent = true + })), + Micro.fork + ) + yield* Micro.yieldFlush + yield* Micro.fiberInterrupt(handle) + assertFalse(child) + assertTrue(parent) + })) + }) + + describe("forkIn", () => { + it.effect("is interrupted when scope is closed", () => + Micro.gen(function*() { + let interrupted = false + const scope = yield* Micro.scopeMake + yield* Micro.never.pipe( + Micro.onInterrupt(Micro.sync(() => { + interrupted = true + })), + Micro.forkIn(scope) + ) + yield* Micro.yieldFlush + yield* scope.close(Micro.exitVoid) + assertTrue(interrupted) + })) + }) + + describe("forkScoped", () => { + it.effect("is interrupted when scope is closed", () => + Micro.gen(function*() { + let interrupted = false + const scope = yield* Micro.scopeMake + yield* Micro.never.pipe( + Micro.onInterrupt(Micro.sync(() => { + interrupted = true + })), + Micro.forkScoped, + Micro.provideScope(scope) + ) + yield* Micro.yieldFlush + yield* scope.close(Micro.exitVoid) + assertTrue(interrupted) + })) + }) + + describe("do notation", () => { + it.effect("works", () => + Micro.succeed(1).pipe( + Micro.bindTo("a"), + Micro.let("b", ({ a }) => a + 1), + Micro.bind("b", ({ b }) => Micro.succeed(b.toString())), + Micro.tap((_) => { + deepStrictEqual(_, { + a: 1, + b: "2" + }) + }) + )) + it.effect("does not bindTo __proto__", () => + pipe( + Micro.succeed(1), + Micro.bindTo("__proto__"), + Micro.bind("x", () => Micro.succeed(2)), + Micro.tap((_) => { + deepStrictEqual(_, { + x: 2, + ["__proto__"]: 1 + }) + }) + )) + it.effect("does not let __proto__", () => + pipe( + Micro.Do, + Micro.let("__proto__", () => 1), + Micro.bind("x", () => Micro.succeed(2)), + Micro.tap((_) => { + deepStrictEqual(_, { + x: 2, + ["__proto__"]: 1 + }) + }) + )) + it.effect("does not bind __proto__", () => + pipe( + Micro.Do, + Micro.bind("__proto__", () => Micro.succeed(1)), + Micro.bind("x", () => Micro.succeed(2)), + Micro.tap((_) => { + deepStrictEqual(_, { + x: 2, + ["__proto__"]: 1 + }) + }) + )) + }) + + describe("stack safety", () => { + it.effect("recursion", () => { + const loop: Micro.Micro = Micro.void.pipe( + Micro.flatMap((_) => loop) + ) + return loop.pipe( + Micro.timeoutOption(50) + ) + }) + }) + + describe("finalization", () => { + const ExampleError = new Error("Oh noes!") + + it.effect("fail ensuring", () => + Micro.gen(function*() { + let finalized = false + const result = yield* Micro.fail(ExampleError).pipe( + Micro.ensuring(Micro.sync(() => { + finalized = true + })), + Micro.exit + ) + deepStrictEqual(result, Micro.exitFail(ExampleError)) + assertTrue(finalized) + })) + + it.effect("fail on error", () => + Micro.gen(function*() { + let finalized = false + const result = yield* Micro.fail(ExampleError).pipe( + Micro.onError(() => + Micro.sync(() => { + finalized = true + }) + ), + Micro.exit + ) + deepStrictEqual(result, Micro.exitFail(ExampleError)) + assertTrue(finalized) + })) + + it.effect("finalizer errors not caught", () => + Micro.gen(function*() { + const e2 = new Error("e2") + const e3 = new Error("e3") + const result = yield* pipe( + Micro.fail(ExampleError), + Micro.ensuring(Micro.die(e2)), + Micro.ensuring(Micro.die(e3)), + Micro.sandbox, + Micro.flip, + Micro.map((cause) => cause) + ) + deepStrictEqual(result, Micro.causeDie(e3)) + })) + + it.effect("finalizer errors reported", () => + Micro.gen(function*() { + let reported: Micro.MicroExit | undefined + const result = yield* pipe( + Micro.succeed(42), + Micro.ensuring(Micro.die(ExampleError)), + Micro.fork, + Micro.flatMap((fiber) => + pipe( + Micro.fiberAwait(fiber), + Micro.flatMap((e) => + Micro.sync(() => { + reported = e + }) + ) + ) + ) + ) + strictEqual(result, undefined) + assertFalse(reported !== undefined && Micro.exitIsSuccess(reported)) + })) + + it.effect("acquireUseRelease usage result", () => + Micro.gen(function*() { + const result = yield* Micro.acquireUseRelease( + Micro.void, + () => Micro.succeed(42), + () => Micro.void + ) + strictEqual(result, 42) + })) + + it.effect("error in just acquisition", () => + Micro.gen(function*() { + const result = yield* pipe( + Micro.acquireUseRelease( + Micro.fail(ExampleError), + () => Micro.void, + () => Micro.void + ), + Micro.exit + ) + deepStrictEqual(result, Micro.exitFail(ExampleError)) + })) + + it.effect("error in just release", () => + Micro.gen(function*() { + const result = yield* pipe( + Micro.acquireUseRelease( + Micro.void, + () => Micro.void, + () => Micro.die(ExampleError) + ), + Micro.exit + ) + deepStrictEqual(result, Micro.exitDie(ExampleError)) + })) + + it.effect("error in just usage", () => + Micro.gen(function*() { + const result = yield* pipe( + Micro.acquireUseRelease( + Micro.void, + () => Micro.fail(ExampleError), + () => Micro.void + ), + Micro.exit + ) + deepStrictEqual(result, Micro.exitFail(ExampleError)) + })) + + it.effect("rethrown caught error in acquisition", () => + Micro.gen(function*() { + const result = yield* Micro.acquireUseRelease( + Micro.fail(ExampleError), + () => Micro.void, + () => Micro.void + ).pipe(Micro.flip) + deepStrictEqual(result, ExampleError) + })) + + it.effect("rethrown caught error in release", () => + Micro.gen(function*() { + const result = yield* pipe( + Micro.acquireUseRelease( + Micro.void, + () => Micro.void, + () => Micro.die(ExampleError) + ), + Micro.exit + ) + deepStrictEqual(result, Micro.exitDie(ExampleError)) + })) + + it.effect("rethrown caught error in usage", () => + Micro.gen(function*() { + const result = yield* Micro.acquireUseRelease( + Micro.void, + () => Micro.fail(ExampleError), + () => Micro.void + ).pipe(Micro.exit) + deepStrictEqual(result, Micro.exitFail(ExampleError)) + })) + + it.effect("onResult - ensures that a cleanup function runs when an effect fails", () => + Micro.gen(function*() { + let ref = false + yield* Micro.die("boom").pipe( + Micro.onExit((result) => + Micro.exitIsDie(result) ? + Micro.sync(() => { + ref = true + }) : + Micro.void + ), + Micro.sandbox, + Micro.ignore + ) + assertTrue(ref) + })) + }) + + describe("error handling", () => { + class ErrorA extends Micro.TaggedError("A") {} + class ErrorB extends Micro.TaggedError("B") {} + class ErrorC extends Micro.Error {} + + it.effect("catchTag", () => + Micro.gen(function*() { + let error: ErrorA | ErrorB | ErrorC = new ErrorA() + const effect = Micro.failSync(() => error).pipe( + Micro.catchTag("A", (_) => Micro.succeed(1)), + Micro.catchTag("B", (_) => Micro.succeed(2)), + Micro.orElseSucceed(() => 3) + ) + strictEqual(yield* effect, 1) + error = new ErrorB() + strictEqual(yield* effect, 2) + error = new ErrorC() + strictEqual(yield* effect, 3) + })) + }) + + describe("zip", () => { + it.effect("concurrent: false", () => { + const executionOrder: Array = [] + const task1 = Micro.succeed("a").pipe(Micro.delay(5), Micro.tap(() => executionOrder.push("task1"))) + const task2 = Micro.succeed(1).pipe(Micro.delay(1), Micro.tap(() => executionOrder.push("task2"))) + return Micro.gen(function*() { + const result = yield* Micro.zip(task1, task2) + deepStrictEqual(result, ["a", 1]) + deepStrictEqual(executionOrder, ["task1", "task2"]) + }) + }) + it.effect("concurrent: true", () => { + const executionOrder: Array = [] + const task1 = Micro.succeed("a").pipe(Micro.delay(50), Micro.tap(() => executionOrder.push("task1"))) + const task2 = Micro.succeed(1).pipe(Micro.delay(1), Micro.tap(() => executionOrder.push("task2"))) + return Micro.gen(function*() { + const result = yield* Micro.zip(task1, task2, { concurrent: true }) + deepStrictEqual(result, ["a", 1]) + deepStrictEqual(executionOrder, ["task2", "task1"]) + }) + }) + }) + + describe("zipWith", () => { + it.effect("concurrent: false", () => { + const executionOrder: Array = [] + const task1 = Micro.succeed("a").pipe(Micro.delay(50), Micro.tap(() => executionOrder.push("task1"))) + const task2 = Micro.succeed(1).pipe(Micro.delay(1), Micro.tap(() => executionOrder.push("task2"))) + return Micro.gen(function*() { + const result = yield* Micro.zipWith(task1, task2, (a, b) => a + b) + deepStrictEqual(result, "a1") + deepStrictEqual(executionOrder, ["task1", "task2"]) + }) + }) + it.effect("concurrent: true", () => { + const executionOrder: Array = [] + const task1 = Micro.succeed("a").pipe(Micro.delay(50), Micro.tap(() => executionOrder.push("task1"))) + const task2 = Micro.succeed(1).pipe(Micro.delay(1), Micro.tap(() => executionOrder.push("task2"))) + return Micro.gen(function*() { + const result = yield* Micro.zipWith(task1, task2, (a, b) => a + b, { concurrent: true }) + deepStrictEqual(result, "a1") + deepStrictEqual(executionOrder, ["task2", "task1"]) + }) + }) + }) + + describe("catchCauseIf", () => { + it.effect("first argument as success", () => + Micro.gen(function*() { + const result = yield* Micro.catchCauseIf(Micro.succeed(1), () => false, () => Micro.fail("e2")) + deepStrictEqual(result, 1) + })) + it.effect("first argument as failure and predicate return false", () => + Micro.gen(function*() { + const result = yield* Micro.flip( + Micro.catchCauseIf(Micro.fail("e1" as const), () => false, () => Micro.fail("e2" as const)) + ) + deepStrictEqual(result, "e1") + })) + it.effect("first argument as failure and predicate return true", () => + Micro.gen(function*() { + const result = yield* Micro.flip( + Micro.catchCauseIf(Micro.fail("e1" as const), () => true, () => Micro.fail("e2" as const)) + ) + deepStrictEqual(result, "e2") + })) + }) + + describe("catchAll", () => { + it.effect("first argument as success", () => + Micro.gen(function*() { + const result = yield* Micro.catchAll(Micro.succeed(1), () => Micro.fail("e2" as const)) + deepStrictEqual(result, 1) + })) + it.effect("first argument as failure", () => + Micro.gen(function*() { + const result = yield* Micro.flip(Micro.catchAll(Micro.fail("e1" as const), () => Micro.fail("e2" as const))) + deepStrictEqual(result, "e2") + })) + }) + + describe("catchAllCause", () => { + it.effect("first argument as success", () => + Micro.gen(function*() { + const result = yield* Micro.catchAllCause(Micro.succeed(1), () => Micro.fail("e2" as const)) + deepStrictEqual(result, 1) + })) + it.effect("first argument as failure", () => + Micro.gen(function*() { + const result = yield* Micro.flip( + Micro.catchAllCause(Micro.fail("e1" as const), () => Micro.fail("e2" as const)) + ) + deepStrictEqual(result, "e2") + })) + }) + + describe("schedules", () => { + // returns an array of delays, an item for each attempt + const dryRun = (schedule: Micro.MicroSchedule, maxAttempt: number = 7): Array => { + let attempt = 1 + let elapsed = 0 + let duration = schedule(attempt, elapsed) + const out: Array = [] + while (Option.isSome(duration) && attempt <= maxAttempt) { + const value = duration.value + attempt++ + elapsed += value + out.push(value) + duration = schedule(attempt, elapsed) + } + return out + } + + it("scheduleRecurs", () => { + const out = dryRun(Micro.scheduleRecurs(5)) + deepStrictEqual(out, [0, 0, 0, 0, 0]) + }) + + it("scheduleSpaced", () => { + const out = dryRun(Micro.scheduleSpaced(10)) + deepStrictEqual(out, [10, 10, 10, 10, 10, 10, 10]) + }) + + it("scheduleExponential", () => { + const out = dryRun(Micro.scheduleExponential(10)) + deepStrictEqual(out, [20, 40, 80, 160, 320, 640, 1280]) + }) + + it("scheduleAddDelay", () => { + const out = dryRun(Micro.scheduleAddDelay(Micro.scheduleRecurs(5), () => 10)) + deepStrictEqual(out, [10, 10, 10, 10, 10]) + }) + + it("scheduleWithMaxDelay", () => { + const out = dryRun(Micro.scheduleWithMaxDelay(Micro.scheduleExponential(10), 400)) + deepStrictEqual(out, [20, 40, 80, 160, 320, 400, 400]) + }) + + it("scheduleWithMaxElapsed", () => { + const out = dryRun(Micro.scheduleWithMaxElapsed(Micro.scheduleExponential(10), 400)) + deepStrictEqual(out, [20, 40, 80, 160, 320]) + }) + + it("scheduleUnion", () => { + const out = dryRun(Micro.scheduleUnion( + Micro.scheduleExponential(10), + Micro.scheduleSpaced(100) + )) + deepStrictEqual(out, [20, 40, 80, 100, 100, 100, 100]) + }) + + it("scheduleIntersect", () => { + const out = dryRun(Micro.scheduleIntersect( + Micro.scheduleExponential(10), + Micro.scheduleSpaced(100) + )) + deepStrictEqual(out, [100, 100, 100, 160, 320, 640, 1280]) + }) + }) +}) diff --git a/repos/effect/packages/effect/test/MutableHashMap.test.ts b/repos/effect/packages/effect/test/MutableHashMap.test.ts new file mode 100644 index 0000000..e26d666 --- /dev/null +++ b/repos/effect/packages/effect/test/MutableHashMap.test.ts @@ -0,0 +1,316 @@ +import { describe, it } from "@effect/vitest" +import { assertFalse, assertNone, assertSome, assertTrue, deepStrictEqual, strictEqual } from "@effect/vitest/utils" +import { Equal, Hash, MutableHashMap as HM, Option, pipe } from "effect" + +class Key implements Equal.Equal { + constructor(readonly a: number, readonly b: number) {} + + [Hash.symbol]() { + return Hash.hash(`${this.a}-${this.b}`) + } + + [Equal.symbol](that: unknown): boolean { + return that instanceof Key && this.a === that.a && this.b === that.b + } +} + +class Value implements Equal.Equal { + constructor(readonly c: number, readonly d: number) {} + + [Hash.symbol]() { + return Hash.hash(`${this.c}-${this.d}`) + } + + [Equal.symbol](that: unknown): boolean { + return that instanceof Value && this.c === that.c && this.d === that.d + } +} + +function key(a: number, b: number): Key { + return new Key(a, b) +} + +function value(c: number, d: number): Value { + return new Value(c, d) +} + +describe("MutableHashMap", () => { + it("toString", () => { + const map = HM.make( + [0, "a"], + [1, "b"] + ) + + strictEqual( + String(map), + `{ + "_id": "MutableHashMap", + "values": [ + [ + 0, + "a" + ], + [ + 1, + "b" + ] + ] +}` + ) + }) + + it("toJSON", () => { + const map = HM.make( + [0, "a"], + [1, "b"] + ) + + deepStrictEqual(map.toJSON(), { _id: "MutableHashMap", values: [[0, "a"], [1, "b"]] }) + }) + + it("inspect", () => { + if (typeof window !== "undefined") { + return + } + // eslint-disable-next-line @typescript-eslint/no-require-imports + const { inspect } = require("node:util") + + const map = HM.make( + [0, "a"], + [1, "b"] + ) + + deepStrictEqual(inspect(map), inspect({ _id: "MutableHashMap", values: [[0, "a"], [1, "b"]] })) + }) + + it("make", () => { + const map = HM.make( + [key(0, 0), value(0, 0)], + [key(1, 1), value(1, 1)] + ) + + strictEqual(HM.size(map), 2) + assertTrue(pipe(map, HM.has(key(0, 0)))) + assertTrue(pipe(map, HM.has(key(1, 1)))) + }) + + it("fromIterable", () => { + const map = HM.fromIterable([ + [key(0, 0), value(0, 0)], + [key(1, 1), value(1, 1)] + ]) + + strictEqual(HM.size(map), 2) + assertTrue(pipe(map, HM.has(key(0, 0)))) + assertTrue(pipe(map, HM.has(key(1, 1)))) + }) + + it("iterate", () => { + class Hello { + [Hash.symbol]() { + return 0 + } + + [Equal.symbol](that: unknown) { + return this === that + } + } + + const a = new Hello() + const b = new Hello() + + const map = HM.make( + [a, 0], + [b, 0] + ) + + strictEqual(Array.from(map).length, 2) + }) + + it("get", () => { + const map = pipe( + HM.empty(), + HM.set(key(0, 0), value(0, 0)), + HM.set(key(0, 0), value(1, 1)) + ) + + const result = pipe( + map, + HM.get(key(0, 0)) + ) + + assertSome(result, value(1, 1)) + }) + + it("has", () => { + const map = HM.make( + [key(0, 0), value(0, 0)], + [key(0, 0), value(1, 1)], + [key(1, 1), value(2, 2)], + [key(1, 1), value(3, 3)], + [key(0, 0), value(4, 4)] + ) + + pipe( + map, + HM.has(key(0, 0)), + assertTrue + ) + + pipe( + map, + HM.has(key(1, 1)), + assertTrue + ) + + pipe( + map, + HM.has(key(4, 4)), + assertFalse + ) + }) + + it("keys", () => { + const map = pipe( + HM.empty(), + HM.set(key(0, 0), value(0, 0)), + HM.set(key(1, 1), value(1, 1)) + ) + + deepStrictEqual(HM.keys(map), [ + key(0, 0), + key(1, 1) + ]) + }) + + it("modifyAt", () => { + const map = pipe( + HM.empty(), + HM.set(key(0, 0), value(0, 0)), + HM.set(key(1, 1), value(1, 1)) + ) + + pipe( + map, + HM.modifyAt( + key(0, 0), + () => Option.some(value(0, 1)) + ) + ) + + strictEqual(HM.size(map), 2) + assertSome(pipe(map, HM.get(key(0, 0))), value(0, 1)) + + pipe( + map, + HM.modifyAt( + key(2, 2), + Option.match({ + onNone: () => Option.some(value(2, 2)), + onSome: Option.some + }) + ) + ) + + strictEqual(HM.size(map), 3) + assertSome(pipe(map, HM.get(key(2, 2))), value(2, 2)) + + pipe( + map, + HM.modifyAt( + key(2, 2), + () => Option.none() + ) + ) + + strictEqual(HM.size(map), 2) + }) + + it("remove", () => { + const map = pipe( + HM.empty(), + HM.set(key(0, 0), value(0, 0)), + HM.set(key(1, 1), value(1, 1)) + ) + + strictEqual(HM.size(map), 2) + + pipe( + map, + HM.has(key(1, 1)), + assertTrue + ) + + pipe( + map, + HM.remove(key(1, 1)) + ) + + strictEqual(HM.size(map), 1) + + pipe( + map, + HM.has(key(1, 1)), + assertFalse + ) + }) + + it("set", () => { + const map = pipe( + HM.empty(), + HM.set(key(0, 0), value(0, 0)), + HM.set(key(0, 0), value(1, 1)), + HM.set(key(1, 1), value(2, 2)), + HM.set(key(1, 1), value(3, 3)), + HM.set(key(0, 0), value(4, 4)) + ) + + deepStrictEqual(Array.from(map), [ + [key(0, 0), value(4, 4)], + [key(1, 1), value(3, 3)] + ]) + }) + + it("size", () => { + const map = pipe( + HM.empty(), + HM.set(key(0, 0), value(0, 0)), + HM.set(key(0, 0), value(1, 1)), + HM.set(key(1, 1), value(2, 2)), + HM.set(key(1, 1), value(3, 3)), + HM.set(key(0, 0), value(4, 4)) + ) + + strictEqual(HM.size(map), 2) + }) + + it("modify", () => { + const map = pipe( + HM.empty(), + HM.set(key(0, 0), value(0, 0)), + HM.set(key(1, 1), value(1, 1)) + ) + + pipe( + map, + HM.modify(key(0, 0), (v) => value(v.c + 1, v.d + 1)) + ) + + assertSome(pipe(map, HM.get(key(0, 0))), value(1, 1)) + + pipe( + map, + HM.modify(key(1, 1), (v) => value(v.c + 1, v.d + 1)) + ) + + assertNone(pipe( + map, + HM.remove(key(0, 0)), + HM.get(key(0, 0)) + )) + }) + + it("pipe()", () => { + deepStrictEqual(HM.empty().pipe(HM.set("key", "value")), HM.make(["key", "value"])) + }) +}) diff --git a/repos/effect/packages/effect/test/MutableHashSet.test.ts b/repos/effect/packages/effect/test/MutableHashSet.test.ts new file mode 100644 index 0000000..73b25ca --- /dev/null +++ b/repos/effect/packages/effect/test/MutableHashSet.test.ts @@ -0,0 +1,77 @@ +import { describe, it } from "@effect/vitest" +import { deepStrictEqual, strictEqual } from "@effect/vitest/utils" +import { Equal, Hash, MutableHashSet } from "effect" + +class Value implements Equal.Equal { + constructor(readonly a: number, readonly b: number) {} + + [Hash.symbol]() { + return Hash.hash(`${this.a}-${this.b}`) + } + + [Equal.symbol](that: unknown): boolean { + return that instanceof Value && this.a === that.a && this.b === that.b + } + + toJSON() { + return { _id: "Value", a: this.a, b: this.b } + } +} + +describe("MutableHashSet", () => { + it("toString", () => { + const set = MutableHashSet.make( + new Value(0, 1), + new Value(2, 3) + ) + + strictEqual( + String(set), + `{ + "_id": "MutableHashSet", + "values": [ + { + "_id": "Value", + "a": 0, + "b": 1 + }, + { + "_id": "Value", + "a": 2, + "b": 3 + } + ] +}` + ) + }) + + it("toJSON", () => { + const set = MutableHashSet.make( + new Value(0, 1), + new Value(2, 3) + ) + + deepStrictEqual(set.toJSON(), { + _id: "MutableHashSet", + values: [{ _id: "Value", a: 0, b: 1 }, { _id: "Value", a: 2, b: 3 }] + }) + }) + + it("inspect", () => { + if (typeof window !== "undefined") { + return + } + // eslint-disable-next-line @typescript-eslint/no-require-imports + const { inspect } = require("node:util") + + const set = MutableHashSet.make( + new Value(0, 1), + new Value(2, 3) + ) + + deepStrictEqual( + inspect(set), + inspect({ _id: "MutableHashSet", values: [{ _id: "Value", a: 0, b: 1 }, { _id: "Value", a: 2, b: 3 }] }) + ) + }) +}) diff --git a/repos/effect/packages/effect/test/MutableList.test.ts b/repos/effect/packages/effect/test/MutableList.test.ts new file mode 100644 index 0000000..5474919 --- /dev/null +++ b/repos/effect/packages/effect/test/MutableList.test.ts @@ -0,0 +1,128 @@ +import { describe, it } from "@effect/vitest" +import { assertFalse, assertTrue, deepStrictEqual, strictEqual } from "@effect/vitest/utils" +import { MutableList, pipe } from "effect" + +describe("MutableList", () => { + it("toString", () => { + strictEqual( + String(MutableList.make(0, 1, 2)), + `{ + "_id": "MutableList", + "values": [ + 0, + 1, + 2 + ] +}` + ) + }) + + it("toJSON", () => { + deepStrictEqual(MutableList.make(0, 1, 2).toJSON(), { _id: "MutableList", values: [0, 1, 2] }) + }) + + it("inspect", () => { + if (typeof window !== "undefined") { + return + } + // eslint-disable-next-line @typescript-eslint/no-require-imports + const { inspect } = require("node:util") + deepStrictEqual(inspect(MutableList.make(0, 1, 2)), inspect({ _id: "MutableList", values: [0, 1, 2] })) + }) + + it("pipe()", () => { + deepStrictEqual(MutableList.empty().pipe(MutableList.prepend("a")), MutableList.make("a")) + }) + + it("empty", () => { + deepStrictEqual(Array.from(MutableList.empty()), []) + }) + + it("fromIterable", () => { + deepStrictEqual(Array.from(MutableList.fromIterable([])), []) + deepStrictEqual(Array.from(MutableList.fromIterable([1, 2, 3])), [1, 2, 3]) + }) + + it("make", () => { + deepStrictEqual(Array.from(MutableList.make()), []) + deepStrictEqual(Array.from(MutableList.make(1, 2, 3)), [1, 2, 3]) + }) + + it("isEmpty", () => { + assertTrue(MutableList.isEmpty(MutableList.empty())) + assertFalse(MutableList.isEmpty(MutableList.make(1, 2, 3))) + }) + + it("length", () => { + strictEqual(MutableList.length(MutableList.empty()), 0) + strictEqual(MutableList.length(MutableList.make(1, 2, 3)), 3) + }) + + it("tail", () => { + strictEqual(MutableList.tail(MutableList.make()), undefined) + deepStrictEqual(MutableList.tail(MutableList.make(1, 2, 3)), 3) + }) + + it("head", () => { + strictEqual(MutableList.head(MutableList.make()), undefined) + deepStrictEqual(MutableList.head(MutableList.make(1, 2, 3)), 1) + }) + + it("forEach", () => { + const accumulator: Array = [] + const list = MutableList.make(1, 2, 3) + pipe( + list, + MutableList.forEach((n) => { + accumulator.push(n * 2) + }) + ) + + deepStrictEqual(Array.from(list), [1, 2, 3]) + deepStrictEqual(accumulator, [2, 4, 6]) + }) + + it("reset", () => { + const list = MutableList.make(1, 2, 3) + deepStrictEqual(Array.from(list), [1, 2, 3]) + deepStrictEqual(Array.from(MutableList.reset(list)), []) + }) + + it("append", () => { + const list = pipe( + MutableList.empty(), + MutableList.append(1), + MutableList.append(2), + MutableList.append(3) + ) + + deepStrictEqual(Array.from(list), [1, 2, 3]) + }) + + it("shift", () => { + const list = MutableList.make(1, 2, 3) + strictEqual(MutableList.shift(list), 1) + strictEqual(MutableList.shift(list), 2) + strictEqual(MutableList.shift(list), 3) + strictEqual(MutableList.shift(list), undefined) + }) + + it("pop", () => { + const list = MutableList.make(1, 2, 3) + strictEqual(MutableList.pop(list), 3) + strictEqual(MutableList.pop(list), 2) + strictEqual(MutableList.pop(list), 1) + strictEqual(MutableList.pop(list), undefined) + }) + + it("prepend", () => { + const list = pipe( + MutableList.empty(), + MutableList.prepend(1), + MutableList.prepend(2), + MutableList.prepend(3), + MutableList.append(4) + ) + deepStrictEqual(Array.from(list), [3, 2, 1, 4]) + }) +}) diff --git a/repos/effect/packages/effect/test/MutableQueue.test.ts b/repos/effect/packages/effect/test/MutableQueue.test.ts new file mode 100644 index 0000000..8b09715 --- /dev/null +++ b/repos/effect/packages/effect/test/MutableQueue.test.ts @@ -0,0 +1,165 @@ +import { describe, it } from "@effect/vitest" +import { assertFalse, assertTrue, deepStrictEqual, strictEqual } from "@effect/vitest/utils" +import { MutableQueue } from "effect" + +describe("MutableQueue", () => { + it("toString", () => { + const queue = MutableQueue.bounded(2) + MutableQueue.offerAll([0, 1, 2])(queue) + strictEqual( + String(queue), + `{ + "_id": "MutableQueue", + "values": [ + 0, + 1 + ] +}` + ) + }) + + it("toJSON", () => { + const queue = MutableQueue.bounded(2) + MutableQueue.offerAll([0, 1, 2])(queue) + deepStrictEqual(queue.toJSON(), { _id: "MutableQueue", values: [0, 1] }) + }) + + it("inspect", () => { + if (typeof window !== "undefined") { + return + } + // eslint-disable-next-line @typescript-eslint/no-require-imports + const { inspect } = require("node:util") + const queue = MutableQueue.bounded(2) + MutableQueue.offerAll([0, 1, 2])(queue) + deepStrictEqual(inspect(queue), inspect({ _id: "MutableQueue", values: [0, 1] })) + }) + + describe("bounded", () => { + it("length", () => { + const queue = MutableQueue.bounded(2) + strictEqual(MutableQueue.length(queue), 0) + MutableQueue.offerAll([0, 1, 2, 3, 4, 5])(queue) + strictEqual(MutableQueue.length(queue), 2) + }) + + it("isEmpty", () => { + const queue = MutableQueue.bounded(2) + assertTrue(MutableQueue.isEmpty(queue)) + MutableQueue.offerAll([1, 2, 3])(queue) + assertFalse(MutableQueue.isEmpty(queue)) + }) + + it("isFull", () => { + const queue = MutableQueue.bounded(2) + assertFalse(MutableQueue.isFull(queue)) + MutableQueue.offer(0)(queue) + assertFalse(MutableQueue.isFull(queue)) + MutableQueue.offer(1)(queue) + assertTrue(MutableQueue.isFull(queue)) + }) + + it("offer", () => { + const queue = MutableQueue.bounded(2) + MutableQueue.offer(0)(queue) + MutableQueue.offer(1)(queue) + MutableQueue.offer(2)(queue) + + deepStrictEqual(Array.from(queue), [0, 1]) + }) + + it("offerAll", () => { + const queue = MutableQueue.bounded(2) + const remainder = MutableQueue.offerAll([0, 1, 2, 3, 4, 5])(queue) + + deepStrictEqual(Array.from(queue), [0, 1]) + deepStrictEqual(Array.from(remainder), [2, 3, 4, 5]) + }) + + it("poll", () => { + const queue = MutableQueue.bounded(2) + strictEqual( + MutableQueue.poll(MutableQueue.EmptyMutableQueue)(queue), + MutableQueue.EmptyMutableQueue + ) + MutableQueue.offer(0)(queue) + strictEqual(MutableQueue.poll(MutableQueue.EmptyMutableQueue)(queue), 0) + }) + + it("pollUpTo", () => { + const queue = MutableQueue.bounded(5) + deepStrictEqual(Array.from(MutableQueue.pollUpTo(2)(queue)), []) + MutableQueue.offerAll([1, 2, 3, 4, 5])(queue) + strictEqual(MutableQueue.length(queue), 5) + deepStrictEqual(Array.from(MutableQueue.pollUpTo(2)(queue)), [1, 2]) + strictEqual(MutableQueue.length(queue), 3) + }) + }) + + describe("unbounded", () => { + it("capacity", () => { + const queue = MutableQueue.unbounded() + + strictEqual(MutableQueue.capacity(queue), Infinity) + }) + + it("length", () => { + const queue = MutableQueue.unbounded() + strictEqual(MutableQueue.length(queue), 0) + MutableQueue.offerAll([0, 1, 2, 3, 4, 5])(queue) + strictEqual(MutableQueue.length(queue), 6) + }) + + it("isEmpty", () => { + const queue = MutableQueue.unbounded() + assertTrue(MutableQueue.isEmpty(queue)) + MutableQueue.offerAll([1, 2, 3])(queue) + assertFalse(MutableQueue.isEmpty(queue)) + }) + + it("isFull", () => { + const queue = MutableQueue.unbounded() + assertFalse(MutableQueue.isFull(queue)) + MutableQueue.offer(0)(queue) + assertFalse(MutableQueue.isFull(queue)) + MutableQueue.offer(1)(queue) + assertFalse(MutableQueue.isFull(queue)) + }) + + it("offer", () => { + const queue = MutableQueue.unbounded() + MutableQueue.offer(0)(queue) + MutableQueue.offer(1)(queue) + MutableQueue.offer(2)(queue) + + deepStrictEqual(Array.from(queue), [0, 1, 2]) + }) + + it("offerAll", () => { + const queue = MutableQueue.unbounded() + const remainder = MutableQueue.offerAll([0, 1, 2, 3, 4, 5])(queue) + + deepStrictEqual(Array.from(queue), [0, 1, 2, 3, 4, 5]) + deepStrictEqual(Array.from(remainder), []) + }) + + it("poll", () => { + const queue = MutableQueue.unbounded() + strictEqual( + MutableQueue.poll(MutableQueue.EmptyMutableQueue)(queue), + MutableQueue.EmptyMutableQueue + ) + MutableQueue.offer(0)(queue) + strictEqual(MutableQueue.poll(MutableQueue.EmptyMutableQueue)(queue), 0) + }) + + it("pollUpTo", () => { + const queue = MutableQueue.unbounded() + deepStrictEqual(Array.from(MutableQueue.pollUpTo(2)(queue)), []) + MutableQueue.offerAll([1, 2, 3, 4, 5])(queue) + strictEqual(MutableQueue.length(queue), 5) + deepStrictEqual(Array.from(MutableQueue.pollUpTo(2)(queue)), [1, 2]) + strictEqual(MutableQueue.length(queue), 3) + }) + }) +}) diff --git a/repos/effect/packages/effect/test/MutableRef.test.ts b/repos/effect/packages/effect/test/MutableRef.test.ts new file mode 100644 index 0000000..388ea85 --- /dev/null +++ b/repos/effect/packages/effect/test/MutableRef.test.ts @@ -0,0 +1,41 @@ +import { describe, it } from "@effect/vitest" +import { deepStrictEqual, strictEqual } from "@effect/vitest/utils" +import { Chunk, MutableRef } from "effect" + +describe("MutableRef", () => { + it("toString", () => { + strictEqual( + String(MutableRef.make(Chunk.make(1, 2, 3))), + `{ + "_id": "MutableRef", + "current": { + "_id": "Chunk", + "values": [ + 1, + 2, + 3 + ] + } +}` + ) + }) + + it("toJSON", () => { + deepStrictEqual(MutableRef.make(Chunk.make(1, 2, 3)).toJSON(), { + _id: "MutableRef", + current: { _id: "Chunk", values: [1, 2, 3] } + }) + }) + + it("inspect", () => { + if (typeof window !== "undefined") { + return + } + // eslint-disable-next-line @typescript-eslint/no-require-imports + const { inspect } = require("node:util") + deepStrictEqual( + inspect(MutableRef.make(Chunk.make(1, 2, 3))), + inspect({ _id: "MutableRef", current: { _id: "Chunk", values: [1, 2, 3] } }) + ) + }) +}) diff --git a/repos/effect/packages/effect/test/NonEmptyIterable.test.ts b/repos/effect/packages/effect/test/NonEmptyIterable.test.ts new file mode 100644 index 0000000..26ee92d --- /dev/null +++ b/repos/effect/packages/effect/test/NonEmptyIterable.test.ts @@ -0,0 +1,20 @@ +import { describe, it } from "@effect/vitest" +import { deepStrictEqual, strictEqual, throws } from "@effect/vitest/utils" +import { Chunk, NonEmptyIterable } from "effect" + +describe("NonEmptyIterable", () => { + it("should get head and rest", () => { + const [head, rest] = NonEmptyIterable.unprepend(Chunk.make(0, 1, 2)) + const restArray: Array = [] + let next = rest.next() + while (!next.done) { + restArray.push(next.value) + next = rest.next() + } + strictEqual(head, 0) + deepStrictEqual(restArray, [1, 2]) + }) + it("should throw", () => { + throws(() => NonEmptyIterable.unprepend(Chunk.empty as any)) + }) +}) diff --git a/repos/effect/packages/effect/test/Number.test.ts b/repos/effect/packages/effect/test/Number.test.ts new file mode 100644 index 0000000..c193ebc --- /dev/null +++ b/repos/effect/packages/effect/test/Number.test.ts @@ -0,0 +1,262 @@ +import { describe, it } from "@effect/vitest" +import { assertFalse, assertNone, assertSome, assertTrue, strictEqual } from "@effect/vitest/utils" +import { Number, pipe } from "effect" + +describe("Number", () => { + it("isNumber", () => { + assertTrue(Number.isNumber(1)) + assertFalse(Number.isNumber("a")) + assertFalse(Number.isNumber(true)) + }) + + it("sum", () => { + strictEqual(pipe(1, Number.sum(2)), 3) + }) + + it("multiply", () => { + strictEqual(pipe(2, Number.multiply(3)), 6) + }) + + it("subtract", () => { + strictEqual(pipe(3, Number.subtract(1)), 2) + }) + + it("divide", () => { + assertSome(pipe(6, Number.divide(2)), 3) + assertNone(pipe(6, Number.divide(0))) + }) + + it("unsafeDivide", () => { + const six = 6 as const + const two = 2 as const + + strictEqual(pipe(six, Number.unsafeDivide(two)), Number.unsafeDivide(six, two)) + + strictEqual(pipe(six, Number.unsafeDivide(two)), 3) + strictEqual(pipe(six, Number.unsafeDivide(two)), 3) + + strictEqual(Number.unsafeDivide(0, six), 0) + strictEqual( + Number.unsafeDivide(six, 0), + Infinity + ) + strictEqual( + Number.unsafeDivide(0, 0), + NaN + ) + }) + + it("decrement", () => { + strictEqual(Number.decrement(3.14), 2.14) + + strictEqual(Number.decrement(-0.69314), -1.69314) + + strictEqual( + pipe( + 100, + Number.decrement, + Number.decrement, + Number.decrement, + Number.decrement, + Number.decrement, + Number.decrement, + Number.decrement + ), + 93 + ) + }) + + it("Equivalence", () => { + assertTrue(Number.Equivalence(1, 1)) + assertFalse(Number.Equivalence(1, 2)) + }) + + it("Order", () => { + strictEqual(Number.Order(1, 2), -1) + strictEqual(Number.Order(2, 1), 1) + strictEqual(Number.Order(2, 2), 0) + }) + + it("sign", () => { + strictEqual(Number.sign(0), 0) + strictEqual(Number.sign(0.0), 0) + strictEqual(Number.sign(-0.1), -1) + strictEqual(Number.sign(-10), -1) + strictEqual(Number.sign(10), 1) + strictEqual(Number.sign(0.1), 1) + }) + + it("remainder", () => { + strictEqual(Number.remainder(2, 2), 0) + strictEqual(Number.remainder(3, 2), 1) + strictEqual(Number.remainder(4, 2), 0) + strictEqual(Number.remainder(2.5, 2), 0.5) + strictEqual(Number.remainder(-2, 2), -0) + strictEqual(Number.remainder(-3, 2), -1) + strictEqual(Number.remainder(-4, 2), -0) + strictEqual(Number.remainder(-2.8, -.2), -0) + strictEqual(Number.remainder(-2, -.2), -0) + strictEqual(Number.remainder(-1.5, -.2), -0.1) + strictEqual(Number.remainder(0, -.2), 0) + strictEqual(Number.remainder(1, -.2), 0) + strictEqual(Number.remainder(2.6, -.2), 0) + strictEqual(Number.remainder(3.1, -.2), 0.1) + }) + + it("lessThan", () => { + assertTrue(Number.lessThan(2, 3)) + assertFalse(Number.lessThan(3, 3)) + assertFalse(Number.lessThan(4, 3)) + }) + + it("lessThanOrEqualTo", () => { + assertTrue(Number.lessThanOrEqualTo(2, 3)) + assertTrue(Number.lessThanOrEqualTo(3, 3)) + assertFalse(Number.lessThanOrEqualTo(4, 3)) + }) + + it("greaterThan", () => { + assertFalse(Number.greaterThan(2, 3)) + assertFalse(Number.greaterThan(3, 3)) + assertTrue(Number.greaterThan(4, 3)) + }) + + it("greaterThanOrEqualTo", () => { + assertFalse(Number.greaterThanOrEqualTo(2, 3)) + assertTrue(Number.greaterThanOrEqualTo(3, 3)) + assertTrue(Number.greaterThanOrEqualTo(4, 3)) + }) + + it("between", () => { + assertTrue(Number.between({ minimum: 0, maximum: 5 })(3)) + assertFalse(Number.between({ minimum: 0, maximum: 5 })(-1)) + assertFalse(Number.between({ minimum: 0, maximum: 5 })(6)) + + assertTrue(Number.between(3, { minimum: 0, maximum: 5 })) + }) + + it("clamp", () => { + strictEqual(Number.clamp({ minimum: 0, maximum: 5 })(3), 3) + strictEqual(Number.clamp({ minimum: 0, maximum: 5 })(-1), 0) + strictEqual(Number.clamp({ minimum: 0, maximum: 5 })(6), 5) + }) + + it("min", () => { + strictEqual(Number.min(2, 3), 2) + }) + + it("max", () => { + strictEqual(Number.max(2, 3), 3) + }) + + it("sumAll", () => { + strictEqual(Number.sumAll([2, 3, 4]), 9) + strictEqual(Number.sumAll([]), 0) + }) + + it("multiplyAll", () => { + strictEqual(Number.multiplyAll([2, 0, 4]), 0) + strictEqual(Number.multiplyAll([]), 1) + strictEqual(Number.multiplyAll([2, 3, 4]), 24) + }) + + describe("nextPow2", () => { + // Positive integers test cases + it("calculates the next power of 2 for positive integers", () => { + // Examples from documentation + strictEqual(Number.nextPow2(5), 2 ** 3) // 8 + strictEqual(Number.nextPow2(17), 2 ** 5) // 32 + + // Additional positive integer cases + strictEqual(Number.nextPow2(1), 2 ** 1) // 2 + strictEqual(Number.nextPow2(2), 2 ** 1) // 2 + strictEqual(Number.nextPow2(3), 2 ** 2) // 4 + strictEqual(Number.nextPow2(4), 2 ** 2) // 4 + strictEqual(Number.nextPow2(7), 2 ** 3) // 8 + strictEqual(Number.nextPow2(8), 2 ** 3) // 8 + strictEqual(Number.nextPow2(9), 2 ** 4) // 16 + strictEqual(Number.nextPow2(15), 2 ** 4) // 16 + strictEqual(Number.nextPow2(16), 2 ** 4) // 16 + strictEqual(Number.nextPow2(100), 2 ** 7) // 128 + strictEqual(Number.nextPow2(1000), 2 ** 10) // 1024 + }) + + // Positive non-integer test cases + it("calculates the next power of 2 for positive non-integers", () => { + strictEqual(Number.nextPow2(0.1), 2 ** 1) // 2 + strictEqual(Number.nextPow2(0.5), 2 ** 1) // 2 + strictEqual(Number.nextPow2(1.5), 2 ** 1) // 2 + strictEqual(Number.nextPow2(2.1), 2 ** 2) // 4 + strictEqual(Number.nextPow2(3.99), 2 ** 2) // 4 + strictEqual(Number.nextPow2(4.01), 2 ** 3) // 8 + }) + + // Zero test case + it("returns 2 for zero input", () => { + strictEqual(Number.nextPow2(0), 2 ** 1) // 2 + }) + + // Negative input test cases + it("returns NaN for negative inputs", () => { + // Document the current behavior: negative inputs produce NaN + strictEqual(isNaN(Number.nextPow2(-1)), true) + strictEqual(isNaN(Number.nextPow2(-5)), true) + strictEqual(isNaN(Number.nextPow2(-10)), true) + strictEqual(isNaN(Number.nextPow2(-100)), true) + }) + + // Special values test cases + it("handles special values as expected", () => { + // NaN input produces NaN output + strictEqual(isNaN(Number.nextPow2(NaN)), true) + + // Infinity input produces Infinity output + strictEqual(Number.nextPow2(Infinity), Infinity) + + // Negative Infinity produces NaN + strictEqual(isNaN(Number.nextPow2(-Infinity)), true) + }) + + // Mathematical property test + it("preserves mathematical properties for valid inputs", () => { + // Test on valid inputs (non-negative numbers) + const validInputs = [0, 0.3, 1, 2, 3.5, 7, 10, 15.9, 32.1, 100.1, 1000] + + for (const val of validInputs) { + const result = Number.nextPow2(val) + + // Result should be an integer + strictEqual(Math.floor(result), result, `Result for ${val} should be an integer`) + + // Result should be positive + strictEqual(result > 0, true, `Result for ${val} should be positive`) + + // Result should be a power of 2 (a number is a power of 2 if only one bit is set) + strictEqual((result & (result - 1)) === 0, true, `Result for ${val} should be a power of 2`) + + // Result should be at least 2 + strictEqual(result >= 2, true, `Result for ${val} should be at least 2`) + + // Result should be >= input (for all non-negative inputs) + strictEqual(result >= val, true, `Result for ${val} should be >= input`) + } + }) + }) + + it("parse", () => { + assertSome(Number.parse("NaN"), NaN) + assertSome(Number.parse("Infinity"), Infinity) + assertSome(Number.parse("-Infinity"), -Infinity) + assertSome(Number.parse("42"), 42) + assertNone(Number.parse("a")) + }) + + it("round", () => { + strictEqual(Number.round(1.1234, 2), 1.12) + strictEqual(Number.round(2)(1.1234), 1.12) + strictEqual(Number.round(0)(1.1234), 1) + strictEqual(Number.round(0)(1.1234), 1) + strictEqual(Number.round(1.567, 2), 1.57) + strictEqual(Number.round(2)(1.567), 1.57) + }) +}) diff --git a/repos/effect/packages/effect/test/Option.test.ts b/repos/effect/packages/effect/test/Option.test.ts new file mode 100644 index 0000000..f5db6b4 --- /dev/null +++ b/repos/effect/packages/effect/test/Option.test.ts @@ -0,0 +1,586 @@ +import { describe, it } from "@effect/vitest" +import { + assertFalse, + assertNone, + assertSome, + assertTrue, + deepStrictEqual, + strictEqual, + throws +} from "@effect/vitest/utils" +import { Chunk, Either, Equal, Hash, Number as N, Option, pipe, String as S } from "effect" + +const gt2 = (n: number): boolean => n > 2 + +describe("Option", () => { + it("gen", () => { + const a = Option.gen(function*() { + const x = yield* Option.some(1) + const y = yield* Option.some(2) + return x + y + }) + const b = Option.gen(function*() { + return 10 + }) + const c = Option.gen(function*() { + yield* Option.some(1) + yield* Option.some(2) + }) + const d = Option.gen(function*() { + yield* Option.some(1) + return yield* Option.some(2) + }) + const e = Option.gen(function*() { + yield* Option.some(1) + yield* Option.none() + return yield* Option.some(2) + }) + const f = Option.gen(function*() { + yield* Option.none() + }) + const g = Option.gen({ ctx: "testContext" as const }, function*() { + return yield* Option.some(this.ctx) + }) + // TODO(4.0) remove this test + // test adapter + const h = Option.gen(function*($) { + const x = yield* $(Option.some(1)) + const y = yield* $(Option.some(2)) + return x + y + }) + + assertSome(a, 3) + assertSome(b, 10) + assertSome(c, undefined) + assertSome(d, 2) + assertNone(e) + assertNone(f) + assertSome(g, "testContext") + assertSome(h, 3) + }) + + it("toString", () => { + strictEqual( + String(Option.none()), + `{ + "_id": "Option", + "_tag": "None" +}` + ) + strictEqual( + String(Option.some(1)), + `{ + "_id": "Option", + "_tag": "Some", + "value": 1 +}` + ) + strictEqual( + String(Option.some(Chunk.make(1, 2, 3))), + `{ + "_id": "Option", + "_tag": "Some", + "value": { + "_id": "Chunk", + "values": [ + 1, + 2, + 3 + ] + } +}` + ) + }) + + it("toJSON", () => { + deepStrictEqual(Option.none().toJSON(), { _id: "Option", _tag: "None" }) + deepStrictEqual(Option.some(1).toJSON(), { _id: "Option", _tag: "Some", value: 1 }) + }) + + it("inspect", () => { + if (typeof window !== "undefined") { + return + } + // eslint-disable-next-line @typescript-eslint/no-require-imports + const { inspect } = require("node:util") + deepStrictEqual(inspect(Option.none()), inspect({ _id: "Option", _tag: "None" })) + deepStrictEqual(inspect(Option.some(1)), inspect({ _id: "Option", _tag: "Some", value: 1 })) + }) + + it("Equal", () => { + assertTrue(Equal.equals(Option.some(1), Option.some(1))) + assertFalse(Equal.equals(Option.some(1), Option.some(2))) + assertTrue(Equal.equals(Option.none(), Option.none())) + }) + + it("Hash", () => { + strictEqual(Hash.hash(Option.some(1)), Hash.hash(Option.some(1))) + strictEqual(Hash.hash(Option.some(1)) === Hash.hash(Option.some(2)), false) + strictEqual(Hash.hash(Option.none()), Hash.hash(Option.none())) + }) + + it("getRight", () => { + assertSome(Option.getRight(Either.right(1)), 1) + assertNone(Option.getRight(Either.left("a"))) + }) + + it("getLeft", () => { + assertNone(Option.getLeft(Either.right(1))) + assertSome(Option.getLeft(Either.left("a")), "a") + }) + + it("toRefinement", () => { + const f = ( + s: string | number + ): Option.Option => (typeof s === "string" ? Option.some(s) : Option.none()) + const isString = Option.toRefinement(f) + + assertTrue(isString("s")) + assertFalse(isString(1)) + + type A = { readonly type: "A" } + type B = { readonly type: "B" } + type C = A | B + const isA = Option.toRefinement((c) => (c.type === "A" ? Option.some(c) : Option.none())) + + assertTrue(isA({ type: "A" })) + assertFalse(isA({ type: "B" })) + }) + + it("isOption", () => { + assertTrue(pipe(Option.some(1), Option.isOption)) + assertTrue(pipe(Option.none(), Option.isOption)) + assertFalse(pipe(Either.right(1), Option.isOption)) + }) + + it("firstSomeOf", () => { + assertNone(Option.firstSomeOf([])) + assertSome(Option.firstSomeOf([Option.some(1)]), 1) + assertNone(Option.firstSomeOf([Option.none()])) + assertSome( + Option.firstSomeOf([Option.none(), Option.none(), Option.none(), Option.none(), Option.some(1)]), + 1 + ) + assertNone( + Option.firstSomeOf([Option.none(), Option.none(), Option.none(), Option.none()]) + ) + }) + + it("orElseEither", () => { + assertSome(pipe(Option.some(1), Option.orElseEither(() => Option.some(2))), Either.left(1)) + assertSome(pipe(Option.some(1), Option.orElseEither(() => Option.none())), Either.left(1)) + assertSome(pipe(Option.none(), Option.orElseEither(() => Option.some(2))), Either.right(2)) + assertNone(pipe(Option.none(), Option.orElseEither(() => Option.none()))) + }) + + it("orElseSome", () => { + assertSome(pipe(Option.some(1), Option.orElseSome(() => 2)), 1) + assertSome(pipe(Option.none(), Option.orElseSome(() => 2)), 2) + }) + + it("getOrThrow", () => { + strictEqual(pipe(Option.some(1), Option.getOrThrow), 1) + throws(() => pipe(Option.none(), Option.getOrThrow), new Error("getOrThrow called on a None")) + }) + + it("getOrThrowWith", () => { + strictEqual(pipe(Option.some(1), Option.getOrThrowWith(() => new Error("Unexpected None"))), 1) + throws( + () => pipe(Option.none(), Option.getOrThrowWith(() => new Error("Unexpected None"))), + new Error("Unexpected None") + ) + }) + + it("unit", () => { + assertSome(Option.void, undefined) + }) + + it("product", () => { + const product = Option.product + assertNone(product(Option.none(), Option.none())) + assertNone(product(Option.some(1), Option.none())) + assertNone(product(Option.none(), Option.some("a"))) + assertSome(product(Option.some(1), Option.some("a")), [1, "a"]) + }) + + it("productMany", () => { + const productMany = Option.productMany + assertNone(productMany(Option.none(), [])) + assertSome(productMany(Option.some(1), []), [1]) + assertNone(productMany(Option.some(1), [Option.none()])) + assertSome(productMany(Option.some(1), [Option.some(2)]), [1, 2]) + }) + + it("fromIterable", () => { + assertNone(Option.fromIterable([])) + assertSome(Option.fromIterable(["a"]), "a") + }) + + it("map", () => { + assertSome(pipe(Option.some(2), Option.map((n) => n * 2)), 4) + assertNone(pipe(Option.none(), Option.map((n) => n * 2))) + }) + + it("flatMap", () => { + const f = (n: number) => Option.some(n * 2) + const g = () => Option.none() + assertSome(pipe(Option.some(1), Option.flatMap(f)), 2) + assertNone(pipe(Option.none(), Option.flatMap(f))) + assertNone(pipe(Option.some(1), Option.flatMap(g))) + assertNone(pipe(Option.none(), Option.flatMap(g))) + }) + + it("andThen", () => { + assertSome(pipe(Option.some(1), Option.andThen(() => Option.some(2))), 2) + assertSome(pipe(Option.some(1), Option.andThen(Option.some(2))), 2) + assertSome(pipe(Option.some(1), Option.andThen(2)), 2) + assertSome(pipe(Option.some(1), Option.andThen(() => 2)), 2) + assertSome(pipe(Option.some(1), Option.andThen((a) => a)), 1) + assertSome(Option.andThen(Option.some(1), () => Option.some(2)), 2) + assertSome(Option.andThen(Option.some(1), Option.some(2)), 2) + assertSome(Option.andThen(Option.some(1), 2), 2) + assertSome(Option.andThen(Option.some(1), () => 2), 2) + assertSome(Option.andThen(Option.some(1), (a) => a), 1) + }) + + it("orElse", () => { + const assertOrElse = ( + a: Option.Option, + b: Option.Option, + expected: Option.Option + ) => { + deepStrictEqual(pipe(a, Option.orElse(() => b)), expected) + } + assertOrElse(Option.some(1), Option.some(2), Option.some(1)) + assertOrElse(Option.some(1), Option.none(), Option.some(1)) + assertOrElse(Option.none(), Option.some(2), Option.some(2)) + assertOrElse(Option.none(), Option.none(), Option.none()) + }) + + it("partitionMap", () => { + const f = (n: number) => (gt2(n) ? Either.right(n + 1) : Either.left(n - 1)) + deepStrictEqual(pipe(Option.none(), Option.partitionMap(f)), [Option.none(), Option.none()]) + deepStrictEqual(pipe(Option.some(1), Option.partitionMap(f)), [Option.some(0), Option.none()]) + deepStrictEqual(pipe(Option.some(3), Option.partitionMap(f)), [Option.none(), Option.some(4)]) + }) + + it("filterMap", () => { + const f = (n: number) => (gt2(n) ? Option.some(n + 1) : Option.none()) + assertNone(pipe(Option.none(), Option.filterMap(f))) + assertNone(pipe(Option.some(1), Option.filterMap(f))) + assertSome(pipe(Option.some(3), Option.filterMap(f)), 4) + }) + + it("match", () => { + const onNone = () => "none" + const onSome = (s: string) => `some${s.length}` + const match = Option.match({ onNone, onSome }) + strictEqual(match(Option.none()), "none") + strictEqual(match(Option.some("abc")), "some3") + }) + + it("getOrElse", () => { + strictEqual(pipe(Option.some(1), Option.getOrElse(() => 0)), 1) + strictEqual(pipe(Option.none(), Option.getOrElse(() => 0)), 0) + }) + + it("getOrNull", () => { + strictEqual(Option.getOrNull(Option.none()), null) + strictEqual(Option.getOrNull(Option.some(1)), 1) + }) + + it("getOrUndefined", () => { + strictEqual(Option.getOrUndefined(Option.none()), undefined) + strictEqual(Option.getOrUndefined(Option.some(1)), 1) + }) + + it("getOrder", () => { + const OS = Option.getOrder(S.Order) + strictEqual(OS(Option.none(), Option.none()), 0) + strictEqual(OS(Option.some("a"), Option.none()), 1) + strictEqual(OS(Option.none(), Option.some("a")), -1) + strictEqual(OS(Option.some("a"), Option.some("a")), 0) + strictEqual(OS(Option.some("a"), Option.some("b")), -1) + strictEqual(OS(Option.some("b"), Option.some("a")), 1) + }) + + it("flatMapNullable", () => { + interface X { + readonly a?: { + readonly b?: { + readonly c?: { + readonly d: number + } + } + } + } + const x1: X = { a: {} } + const x2: X = { a: { b: {} } } + const x3: X = { a: { b: { c: { d: 1 } } } } + assertNone( + pipe( + Option.fromNullable(x1.a), + Option.flatMapNullable((x) => x.b), + Option.flatMapNullable((x) => x.c), + Option.flatMapNullable((x) => x.d) + ) + ) + assertNone( + pipe( + Option.fromNullable(x2.a), + Option.flatMapNullable((x) => x.b), + Option.flatMapNullable((x) => x.c), + Option.flatMapNullable((x) => x.d) + ) + ) + assertSome( + pipe( + Option.fromNullable(x3.a), + Option.flatMapNullable((x) => x.b), + Option.flatMapNullable((x) => x.c), + Option.flatMapNullable((x) => x.d) + ), + 1 + ) + }) + + it("fromNullable", () => { + assertSome(Option.fromNullable(2), 2) + assertNone(Option.fromNullable(null)) + assertNone(Option.fromNullable(undefined)) + }) + + it("liftPredicate", () => { + assertNone(pipe(1, Option.liftPredicate(gt2))) + assertSome(pipe(3, Option.liftPredicate(gt2)), 3) + assertNone(Option.liftPredicate(1, gt2)) + assertSome(Option.liftPredicate(3, gt2), 3) + + type Direction = "asc" | "desc" + const isDirection = (s: string): s is Direction => s === "asc" || s === "desc" + assertSome(pipe("asc", Option.liftPredicate(isDirection)), "asc") + assertNone(pipe("foo", Option.liftPredicate(isDirection))) + assertSome(Option.liftPredicate("asc", isDirection), "asc") + assertNone(Option.liftPredicate("foo", isDirection)) + }) + + it("containsWith", () => { + const containsWith = Option.containsWith((self, that) => self % 2 === that % 2) + assertTrue(pipe(Option.some(2), containsWith(2))) + assertTrue(pipe(Option.some(4), containsWith(4))) + assertTrue(pipe(Option.some(1), containsWith(3))) + + assertFalse(pipe(Option.none(), containsWith(2))) + assertFalse(pipe(Option.some(2), containsWith(1))) + }) + + it("contains", () => { + assertFalse(pipe(Option.none(), Option.contains(2))) + assertTrue(pipe(Option.some(2), Option.contains(2))) + assertFalse(pipe(Option.some(2), Option.contains(1))) + }) + + it("isNone", () => { + assertTrue(Option.isNone(Option.none())) + assertFalse(Option.isNone(Option.some(1))) + }) + + it("isSome", () => { + assertFalse(Option.isSome(Option.none())) + assertTrue(Option.isSome(Option.some(1))) + }) + + it("exists", () => { + const predicate = (a: number) => a === 2 + assertFalse(pipe(Option.none(), Option.exists(predicate))) + assertFalse(pipe(Option.some(1), Option.exists(predicate))) + assertTrue(pipe(Option.some(2), Option.exists(predicate))) + }) + + it("liftNullable", () => { + const f = Option.liftNullable((n: number) => (n > 0 ? n : null)) + assertSome(f(1), 1) + assertNone(f(-1)) + }) + + it("liftThrowable", () => { + const parse = Option.liftThrowable(JSON.parse) + assertSome(parse("1"), 1) + assertNone(parse("")) + }) + + it("tap", () => { + assertNone(Option.tap(Option.none(), () => Option.none())) + assertNone(Option.tap(Option.some(1), () => Option.none())) + assertNone(Option.tap(Option.none(), (n) => Option.some(n * 2))) + assertSome(Option.tap(Option.some(1), (n) => Option.some(n * 2)), 1) + }) + + it("guard", () => { + assertSome( + pipe( + Option.Do, + Option.bind("x", () => Option.some("a")), + Option.bind("y", () => Option.some("a")), + Option.filter(({ x, y }) => x === y) + ), + { x: "a", y: "a" } + ) + assertNone( + pipe( + Option.Do, + Option.bind("x", () => Option.some("a")), + Option.bind("y", () => Option.some("b")), + Option.filter(({ x, y }) => x === y) + ) + ) + }) + + it("zipWith", () => { + assertNone(pipe(Option.none(), Option.zipWith(Option.some(2), (a, b) => a + b))) + assertNone(pipe(Option.some(1), Option.zipWith(Option.none(), (a, b) => a + b))) + assertSome(pipe(Option.some(1), Option.zipWith(Option.some(2), (a, b) => a + b)), 3) + }) + + it("ap", () => { + assertNone( + pipe(Option.some((a: number) => (b: number) => a + b), Option.ap(Option.none()), Option.ap(Option.some(2))) + ) + assertNone( + pipe(Option.some((a: number) => (b: number) => a + b), Option.ap(Option.some(1)), Option.ap(Option.none())) + ) + assertSome( + pipe(Option.some((a: number) => (b: number) => a + b), Option.ap(Option.some(1)), Option.ap(Option.some(2))), + 3 + ) + }) + + it("reduceCompact", () => { + const sumCompact = Option.reduceCompact(0, N.sum) + strictEqual(sumCompact([]), 0) + strictEqual(sumCompact([Option.some(2), Option.some(3)]), 5) + strictEqual(sumCompact([Option.some(2), Option.none(), Option.some(3)]), 5) + }) + + it("getEquivalence", () => { + const isEquivalent = Option.getEquivalence(N.Equivalence) + assertTrue(isEquivalent(Option.none(), Option.none())) + assertFalse(isEquivalent(Option.none(), Option.some(1))) + assertFalse(isEquivalent(Option.some(1), Option.none())) + assertFalse(isEquivalent(Option.some(2), Option.some(1))) + assertFalse(isEquivalent(Option.some(1), Option.some(2))) + assertTrue(isEquivalent(Option.some(2), Option.some(2))) + }) + + it("all/ tuple", () => { + assertSome(Option.all([]), []) + assertSome(Option.all([Option.some(1), Option.some("hello")]), [1, "hello"]) + assertNone(Option.all([Option.some(1), Option.none()])) + }) + + it("all/ iterable", () => { + assertSome(Option.all([]), []) + assertNone(Option.all([Option.none()])) + assertSome(Option.all([Option.some(1), Option.some(2)]), [1, 2]) + assertSome(Option.all(new Set([Option.some(1), Option.some(2)])), [1, 2]) + assertNone(Option.all([Option.some(1), Option.none()])) + }) + + it("all/ struct", () => { + assertSome( + Option.all({ a: Option.some(1), b: Option.some("hello") }), + { a: 1, b: "hello" } + ) + assertNone(Option.all({ a: Option.some(1), b: Option.none() })) + }) + + it(".pipe()", () => { + assertSome(Option.some(1).pipe(Option.map((n) => n + 1)), 2) + }) + + it("lift2", () => { + const f = Option.lift2((a: number, b: number): number => a + b) + assertNone(f(Option.none(), Option.none())) + assertNone(f(Option.some(1), Option.none())) + assertNone(f(Option.none(), Option.some(2))) + assertSome(f(Option.some(1), Option.some(2)), 3) + }) + + describe("do notation", () => { + it("Do", () => { + assertSome(Option.Do, {}) + }) + + it("bindTo", () => { + assertSome(pipe(Option.some(1), Option.bindTo("a")), { a: 1 }) + assertNone(pipe(Option.none(), Option.bindTo("a"))) + assertSome( + pipe( + Option.some(1), + Option.bindTo("__proto__"), + Option.bind("x", () => Option.some(2)) + ), + { x: 2, ["__proto__"]: 1 } + ) + }) + + it("bind", () => { + assertSome(pipe(Option.some(1), Option.bindTo("a"), Option.bind("b", ({ a }) => Option.some(a + 1))), { + a: 1, + b: 2 + }) + assertNone( + pipe(Option.some(1), Option.bindTo("a"), Option.bind("b", () => Option.none())) + ) + assertNone( + pipe(Option.none(), Option.bindTo("a"), Option.bind("b", () => Option.some(2))) + ) + assertSome( + pipe( + Option.some(1), + Option.bindTo("a"), + Option.bind("__proto__", ({ a }) => Option.some(a + 1)), + Option.bind("b", ({ a }) => Option.some(a + 1)) + ), + { a: 1, b: 2, ["__proto__"]: 2 } + ) + }) + + it("let", () => { + assertSome(pipe(Option.some(1), Option.bindTo("a"), Option.let("b", ({ a }) => a + 1)), { a: 1, b: 2 }) + assertNone( + pipe(Option.none(), Option.bindTo("a"), Option.let("b", () => 2)) + ) + assertSome( + pipe( + Option.some(1), + Option.bindTo("a"), + Option.let("__proto__", ({ a }) => a + 1), + Option.let("b", ({ a }) => a + 1) + ), + { a: 1, b: 2, ["__proto__"]: 2 } + ) + }) + }) + + it("as", () => { + assertNone(Option.none().pipe(Option.as("a"))) + assertSome(Option.some(1).pipe(Option.as("a")), "a") + + assertNone(Option.as(Option.none(), "a")) + assertSome(Option.as(Option.some(1), "a"), "a") + }) + + it("asVoid", () => { + assertNone(Option.none().pipe(Option.asVoid)) + assertSome(Option.some(1).pipe(Option.asVoid), undefined) + }) + + it("[internal] mergeWith", () => { + const mergeWith = Option.mergeWith(N.sum) + assertNone(mergeWith(Option.none(), Option.none())) + assertSome(mergeWith(Option.some(1), Option.none()), 1) + assertSome(mergeWith(Option.none(), Option.some(2)), 2) + assertSome(mergeWith(Option.some(1), Option.some(2)), 3) + }) +}) diff --git a/repos/effect/packages/effect/test/Order.test.ts b/repos/effect/packages/effect/test/Order.test.ts new file mode 100644 index 0000000..a1b52ca --- /dev/null +++ b/repos/effect/packages/effect/test/Order.test.ts @@ -0,0 +1,190 @@ +import { describe, it } from "@effect/vitest" +import { assertFalse, assertTrue, deepStrictEqual, strictEqual } from "@effect/vitest/utils" +import { Array as Arr, Order, pipe } from "effect" + +describe("Order", () => { + it("struct", () => { + const O = Order.struct({ a: Order.string, b: Order.string }) + strictEqual(O({ a: "a", b: "b" }, { a: "a", b: "c" }), -1) + strictEqual(O({ a: "a", b: "b" }, { a: "a", b: "b" }), 0) + strictEqual(O({ a: "a", b: "c" }, { a: "a", b: "b" }), 1) + }) + + it("tuple", () => { + const O = Order.tuple(Order.string, Order.string) + strictEqual(O(["a", "b"], ["a", "c"]), -1) + strictEqual(O(["a", "b"], ["a", "b"]), 0) + strictEqual(O(["a", "b"], ["a", "a"]), 1) + strictEqual(O(["a", "b"], ["b", "a"]), -1) + }) + + it("all", () => { + const O = Order.all([Order.string, Order.string, Order.string]) + strictEqual(O([], []), 0) + strictEqual(O(["a", "b"], ["a"]), 0) + strictEqual(O(["a"], ["a", "c"]), 0) + strictEqual(O(["a", "b"], ["a", "c"]), -1) + strictEqual(O(["a", "b"], ["a", "b"]), 0) + strictEqual(O(["a", "b"], ["a", "a"]), 1) + strictEqual(O(["a", "b"], ["b", "a"]), -1) + }) + + it("mapInput", () => { + const O = Order.mapInput(Order.number, (s: string) => s.length) + strictEqual(O("a", "b"), 0) + strictEqual(O("a", "bb"), -1) + strictEqual(O("aa", "b"), 1) + }) + + it("Date", () => { + const O = Order.Date + strictEqual(O(new Date(0), new Date(1)), -1) + strictEqual(O(new Date(1), new Date(1)), 0) + strictEqual(O(new Date(1), new Date(0)), 1) + }) + + it("clamp", () => { + const clamp = Order.clamp(Order.number)({ minimum: 1, maximum: 10 }) + strictEqual(clamp(2), 2) + strictEqual(clamp(10), 10) + strictEqual(clamp(20), 10) + strictEqual(clamp(1), 1) + strictEqual(clamp(-10), 1) + + strictEqual(Order.clamp(Order.number)({ minimum: 1, maximum: 10 })(2), 2) + }) + + it("between", () => { + const between = Order.between(Order.number)({ minimum: 1, maximum: 10 }) + assertTrue(between(2)) + assertTrue(between(10)) + assertFalse(between(20)) + assertTrue(between(1)) + assertFalse(between(-10)) + + assertTrue(Order.between(Order.number)(2, { minimum: 1, maximum: 10 })) + }) + + it("reverse", () => { + const O = Order.reverse(Order.number) + strictEqual(O(1, 2), 1) + strictEqual(O(2, 1), -1) + strictEqual(O(2, 2), 0) + }) + + it("lessThan", () => { + const lessThan = Order.lessThan(Order.number) + assertTrue(lessThan(0, 1)) + assertFalse(lessThan(1, 1)) + assertFalse(lessThan(2, 1)) + }) + + it("lessThanOrEqualTo", () => { + const lessThanOrEqualTo = Order.lessThanOrEqualTo(Order.number) + assertTrue(lessThanOrEqualTo(0, 1)) + assertTrue(lessThanOrEqualTo(1, 1)) + assertFalse(lessThanOrEqualTo(2, 1)) + }) + + it("greaterThan", () => { + const greaterThan = Order.greaterThan(Order.number) + assertFalse(greaterThan(0, 1)) + assertFalse(greaterThan(1, 1)) + assertTrue(greaterThan(2, 1)) + }) + + it("greaterThanOrEqualTo", () => { + const greaterThanOrEqualTo = Order.greaterThanOrEqualTo(Order.number) + assertFalse(greaterThanOrEqualTo(0, 1)) + assertTrue(greaterThanOrEqualTo(1, 1)) + assertTrue(greaterThanOrEqualTo(2, 1)) + }) + + it("min", () => { + type A = { a: number } + const min = Order.min( + pipe( + Order.number, + Order.mapInput((a: A) => a.a) + ) + ) + deepStrictEqual(min({ a: 1 }, { a: 2 }), { a: 1 }) + deepStrictEqual(min({ a: 2 }, { a: 1 }), { a: 1 }) + const first = { a: 1 } + const second = { a: 1 } + deepStrictEqual(min(first, second), first) + }) + + it("max", () => { + type A = { a: number } + const max = Order.max( + pipe( + Order.number, + Order.mapInput((a: A) => a.a) + ) + ) + deepStrictEqual(max({ a: 1 }, { a: 2 }), { a: 2 }) + deepStrictEqual(max({ a: 2 }, { a: 1 }), { a: 2 }) + const first = { a: 1 } + const second = { a: 1 } + deepStrictEqual(max(first, second), first) + }) + + it("product", () => { + const O = Order.product(Order.string, Order.number) + strictEqual(O(["a", 1], ["a", 2]), -1) + strictEqual(O(["a", 1], ["a", 1]), 0) + strictEqual(O(["a", 1], ["a", 0]), 1) + strictEqual(O(["a", 1], ["b", 1]), -1) + }) + + it("productMany", () => { + const O = Order.productMany(Order.string, [Order.string, Order.string]) + strictEqual(O(["a", "b"], ["a", "c"]), -1) + strictEqual(O(["a", "b"], ["a", "b"]), 0) + strictEqual(O(["a", "b"], ["a", "a"]), 1) + strictEqual(O(["a", "b"], ["b", "a"]), -1) + }) + + it("combine / combineMany", () => { + type T = [number, string] + const tuples: Array = [ + [2, "c"], + [1, "b"], + [2, "a"], + [1, "c"] + ] + const sortByFst = pipe( + Order.number, + Order.mapInput((x: T) => x[0]) + ) + const sortBySnd = pipe( + Order.string, + Order.mapInput((x: T) => x[1]) + ) + deepStrictEqual(Arr.sort(Order.combine(sortByFst, sortBySnd))(tuples), [ + [1, "b"], + [1, "c"], + [2, "a"], + [2, "c"] + ]) + deepStrictEqual(Arr.sort(Order.combine(sortBySnd, sortByFst))(tuples), [ + [2, "a"], + [1, "b"], + [1, "c"], + [2, "c"] + ]) + deepStrictEqual(Arr.sort(Order.combineMany(sortBySnd, []))(tuples), [ + [2, "a"], + [1, "b"], + [2, "c"], + [1, "c"] + ]) + deepStrictEqual(Arr.sort(Order.combineMany(sortBySnd, [sortByFst]))(tuples), [ + [2, "a"], + [1, "b"], + [1, "c"], + [2, "c"] + ]) + }) +}) diff --git a/repos/effect/packages/effect/test/Ordering.test.ts b/repos/effect/packages/effect/test/Ordering.test.ts new file mode 100644 index 0000000..daefb21 --- /dev/null +++ b/repos/effect/packages/effect/test/Ordering.test.ts @@ -0,0 +1,39 @@ +import { describe, it } from "@effect/vitest" +import { strictEqual } from "@effect/vitest/utils" +import { Ordering } from "effect" + +describe("Ordering", () => { + it("match", () => { + const f = Ordering.match({ + onLessThan: () => "lt", + onEqual: () => "eq", + onGreaterThan: () => "gt" + }) + strictEqual(f(-1), "lt") + strictEqual(f(0), "eq") + strictEqual(f(1), "gt") + }) + + it("reverse", () => { + strictEqual(Ordering.reverse(-1), 1) + strictEqual(Ordering.reverse(0), 0) + strictEqual(Ordering.reverse(1), -1) + }) + + it("combine", () => { + strictEqual(Ordering.combine(0, 0), 0) + strictEqual(Ordering.combine(0, 1), 1) + strictEqual(Ordering.combine(1, -1), 1) + strictEqual(Ordering.combine(-1, 1), -1) + }) + + it("combineMany", () => { + strictEqual(Ordering.combineMany(0, []), 0) + strictEqual(Ordering.combineMany(1, []), 1) + strictEqual(Ordering.combineMany(-1, []), -1) + strictEqual(Ordering.combineMany(0, [0, 0, 0]), 0) + strictEqual(Ordering.combineMany(0, [0, 0, 1]), 1) + strictEqual(Ordering.combineMany(1, [0, 0, -1]), 1) + strictEqual(Ordering.combineMany(-1, [0, 0, 1]), -1) + }) +}) diff --git a/repos/effect/packages/effect/test/PartitionedSemaphore.test.ts b/repos/effect/packages/effect/test/PartitionedSemaphore.test.ts new file mode 100644 index 0000000..c5d14e5 --- /dev/null +++ b/repos/effect/packages/effect/test/PartitionedSemaphore.test.ts @@ -0,0 +1,359 @@ +import { assert, describe, it } from "@effect/vitest" +import { deepStrictEqual, strictEqual } from "@effect/vitest/utils" +import { Duration, Effect, Fiber, PartitionedSemaphore, TestClock } from "effect" + +describe("PartitionedSemaphore", () => { + it.effect("basic single partition operation", () => + Effect.gen(function*() { + const sem = yield* PartitionedSemaphore.make({ permits: 4 }) + const messages: Array = [] + + yield* Effect.fork(Effect.all( + [0, 1, 2, 3].map((n) => + sem.withPermits("partition-1", 2)( + Effect.delay(Duration.seconds(2))(Effect.sync(() => messages.push(`process: ${n}`))) + ) + ), + { concurrency: "unbounded", discard: true } + )) + + yield* TestClock.adjust(Duration.seconds(3)) + strictEqual(messages.length, 2) + + yield* TestClock.adjust(Duration.seconds(3)) + strictEqual(messages.length, 4) + })) + + it.effect("multiple partitions share total permits", () => + Effect.gen(function*() { + const sem = yield* PartitionedSemaphore.make({ permits: 4 }) + const messages: Array = [] + + yield* Effect.fork(Effect.all([ + sem.withPermits("partition-1", 2)( + Effect.delay(Duration.seconds(2))(Effect.sync(() => messages.push("p1-task1"))) + ), + sem.withPermits("partition-2", 2)( + Effect.delay(Duration.seconds(2))(Effect.sync(() => messages.push("p2-task1"))) + ), + sem.withPermits("partition-1", 2)( + Effect.delay(Duration.seconds(2))(Effect.sync(() => messages.push("p1-task2"))) + ) + ], { concurrency: "unbounded", discard: true })) + + yield* TestClock.adjust(Duration.seconds(3)) + // Only 2 tasks can run simultaneously (4 permits / 2 permits each) + strictEqual(messages.length, 2) + + yield* TestClock.adjust(Duration.seconds(3)) + strictEqual(messages.length, 3) + })) + + it.effect("round-robin fairness across partitions", () => + Effect.gen(function*() { + const sem = yield* PartitionedSemaphore.make({ permits: 2 }) + const messages: Array = [] + + // Start with 2 permits taken + yield* Effect.fork( + sem.withPermits("partition-1", 2)( + Effect.delay(Duration.seconds(2))(Effect.sync(() => messages.push("p1-initial"))) + ) + ) + + yield* TestClock.adjust(1) + + // Queue 3 tasks from partition-1 (needs 3 permits total) + yield* Effect.fork(Effect.all([ + sem.withPermits("partition-1", 1)(Effect.sync(() => messages.push("p1-task1"))), + sem.withPermits("partition-1", 1)(Effect.sync(() => messages.push("p1-task2"))), + sem.withPermits("partition-1", 1)(Effect.sync(() => messages.push("p1-task3"))) + ], { concurrency: "unbounded", discard: true })) + + yield* TestClock.adjust(1) + + // Queue 3 tasks from partition-2 (needs 3 permits total) + yield* Effect.fork(Effect.all([ + sem.withPermits("partition-2", 1)(Effect.sync(() => messages.push("p2-task1"))), + sem.withPermits("partition-2", 1)(Effect.sync(() => messages.push("p2-task2"))), + sem.withPermits("partition-2", 1)(Effect.sync(() => messages.push("p2-task3"))) + ], { concurrency: "unbounded", discard: true })) + + yield* TestClock.adjust(Duration.seconds(3)) + + // After initial task completes, permits should be distributed in round-robin fashion + // Initial task completes first, then permits alternate between partitions + deepStrictEqual(messages, [ + "p1-initial", + "p1-task1", + "p2-task1", + "p1-task2", + "p2-task2", + "p1-task3", + "p2-task3" + ]) + })) + + it.effect("requesting more permits than total returns never", () => + Effect.gen(function*() { + const sem = yield* PartitionedSemaphore.make({ permits: 4 }) + + const fiber = yield* Effect.fork( + sem.withPermits("partition-1", 5)(Effect.succeed(42)) + ) + + yield* TestClock.adjust(Duration.seconds(10)) + + // Should still be running (never completes) + assert.isNull(fiber.unsafePoll()) + })) + + it.effect("single permit operations", () => + Effect.gen(function*() { + const sem = yield* PartitionedSemaphore.make({ permits: 1 }) + const messages: Array = [] + + yield* Effect.fork(Effect.all( + [0, 1, 2].map((n) => + sem.withPermits("partition-1", 1)( + Effect.delay(Duration.seconds(1))(Effect.sync(() => messages.push(`task: ${n}`))) + ) + ), + { concurrency: "unbounded", discard: true } + )) + + yield* TestClock.adjust(Duration.seconds(1.5)) + strictEqual(messages.length, 1) + + yield* TestClock.adjust(Duration.seconds(1)) + strictEqual(messages.length, 2) + + yield* TestClock.adjust(Duration.seconds(1)) + strictEqual(messages.length, 3) + })) + + it.effect("different permit sizes on same partition", () => + Effect.gen(function*() { + const sem = yield* PartitionedSemaphore.make({ permits: 5 }) + const messages: Array = [] + + yield* Effect.fork(Effect.all([ + sem.withPermits("partition-1", 3)( + Effect.delay(Duration.seconds(2))(Effect.sync(() => messages.push("large"))) + ), + sem.withPermits("partition-1", 1)( + Effect.delay(Duration.seconds(2))(Effect.sync(() => messages.push("small-1"))) + ), + sem.withPermits("partition-1", 1)( + Effect.delay(Duration.seconds(2))(Effect.sync(() => messages.push("small-2"))) + ) + ], { concurrency: "unbounded", discard: true })) + + yield* TestClock.adjust(Duration.seconds(3)) + // All can run simultaneously (3 + 1 + 1 = 5 permits) + strictEqual(messages.length, 3) + })) + + it.effect("interruption releases permits", () => + Effect.gen(function*() { + const sem = yield* PartitionedSemaphore.make({ permits: 2 }) + const messages: Array = [] + + const fiber = yield* Effect.fork( + sem.withPermits("partition-1", 2)( + Effect.delay(Duration.seconds(10))(Effect.sync(() => messages.push("long-task"))) + ) + ) + + yield* TestClock.adjust(Duration.seconds(1)) + + // Interrupt the long-running task + yield* Fiber.interrupt(fiber) + + // Now we should be able to acquire permits again + yield* sem.withPermits("partition-1", 2)( + Effect.sync(() => messages.push("after-interrupt")) + ) + + strictEqual(messages.length, 1) + strictEqual(messages[0], "after-interrupt") + })) + + it.effect("interruption with partial permit acquisition releases all taken permits", () => + Effect.gen(function*() { + const sem = yield* PartitionedSemaphore.make({ permits: 3 }) + const messages: Array = [] + + // First task takes 2 permits, leaving 1 available + const fiber1 = yield* Effect.fork( + sem.withPermits("partition-1", 2)( + Effect.delay(Duration.seconds(5))(Effect.sync(() => messages.push("first"))) + ) + ) + + yield* TestClock.adjust(Duration.millis(1)) + + // Second task requests 3 permits + // It should take the 1 available permit and wait for 2 more + const fiber2 = yield* Effect.fork( + sem.withPermits("partition-2", 3)( + Effect.sync(() => messages.push("second")) + ) + ) + + yield* TestClock.adjust(Duration.millis(1)) + + // At this point: + // - Task 1 has taken 2 permits (running) + // - Task 2 has taken 1 permit and is waiting for 2 more + // - Total available: 0 + + // Interrupt the second task while it's waiting with 1 permit taken + yield* Fiber.interrupt(fiber2) + + // The interrupted task should release its partially taken permit (1) + // Total available should now be 1 + + // Verify we can acquire 1 permit now (proves the partial permit was released) + yield* sem.withPermits("partition-3", 1)( + Effect.sync(() => messages.push("can-acquire-one")) + ) + + strictEqual(messages.length, 1) + deepStrictEqual(messages, ["can-acquire-one"]) + + yield* TestClock.adjust(Duration.seconds(5)) + + // Wait for first task to complete + yield* Fiber.join(fiber1) + + deepStrictEqual(messages, ["can-acquire-one", "first"]) + + // Now all 3 permits should be available again + yield* sem.withPermits("partition-4", 3)( + Effect.sync(() => messages.push("all-three")) + ) + + deepStrictEqual(messages, ["can-acquire-one", "first", "all-three"]) + })) + + it.effect("exact permit match", () => + Effect.gen(function*() { + const sem = yield* PartitionedSemaphore.make({ permits: 4 }) + const messages: Array = [] + + yield* Effect.fork(Effect.all([ + sem.withPermits("partition-1", 4)( + Effect.delay(Duration.seconds(2))(Effect.sync(() => messages.push("exact-match"))) + ), + sem.withPermits("partition-2", 1)( + Effect.delay(Duration.seconds(2))(Effect.sync(() => messages.push("waiting"))) + ) + ], { concurrency: "unbounded", discard: true })) + + yield* TestClock.adjust(Duration.seconds(3)) + // First task takes all permits + strictEqual(messages.length, 1) + strictEqual(messages[0], "exact-match") + + yield* TestClock.adjust(Duration.seconds(3)) + // Second task runs after first completes + strictEqual(messages.length, 2) + })) + + it.effect("many partitions", () => + Effect.gen(function*() { + const sem = yield* PartitionedSemaphore.make({ permits: 3 }) + const messages: Array = [] + + yield* Effect.fork(Effect.all( + Array.from({ length: 10 }, (_, i) => + sem.withPermits(`partition-${i % 5}`, 1)( + Effect.delay(Duration.seconds(1))(Effect.sync(() => messages.push(`p${i % 5}-task`))) + )), + { concurrency: "unbounded", discard: true } + )) + + yield* TestClock.adjust(Duration.seconds(1.5)) + // 3 permits allow 3 concurrent tasks + strictEqual(messages.length, 3) + + yield* TestClock.adjust(Duration.seconds(1)) + strictEqual(messages.length, 6) + + yield* TestClock.adjust(Duration.seconds(1)) + strictEqual(messages.length, 9) + + yield* TestClock.adjust(Duration.seconds(1)) + strictEqual(messages.length, 10) + })) + + it.effect("partial permit allocation", () => + Effect.gen(function*() { + const sem = yield* PartitionedSemaphore.make({ permits: 3 }) + const messages: Array = [] + + // First task takes 2 permits + yield* Effect.fork( + sem.withPermits("partition-1", 2)( + Effect.delay(Duration.seconds(2))(Effect.sync(() => messages.push("first"))) + ) + ) + + yield* TestClock.adjust(Duration.millis(100)) + + // Second task needs 2 permits, but only 1 available + // It should take the 1 available and wait for 1 more + const fiber = yield* Effect.fork( + sem.withPermits("partition-2", 2)( + Effect.sync(() => messages.push("second")) + ) + ) + + yield* TestClock.adjust(Duration.millis(100)) + + // Second task should not have completed yet + assert.isNull(fiber.unsafePoll()) + strictEqual(messages.length, 0) + + yield* TestClock.adjust(Duration.seconds(3)) + + // After first task completes, second should run + strictEqual(messages.length, 2) + deepStrictEqual(messages, ["first", "second"]) + })) + + it.effect("zero permits requested", () => + Effect.gen(function*() { + const sem = yield* PartitionedSemaphore.make({ permits: 2 }) + let executed = false + + yield* sem.withPermits("partition-1", 0)( + Effect.sync(() => { + executed = true + }) + ) + + assert.isTrue(executed) + })) + + it.effect("sequential tasks in same partition", () => + Effect.gen(function*() { + const sem = yield* PartitionedSemaphore.make({ permits: 2 }) + const messages: Array = [] + + yield* sem.withPermits("partition-1", 2)( + Effect.sync(() => messages.push("task-1")) + ) + + yield* sem.withPermits("partition-1", 2)( + Effect.sync(() => messages.push("task-2")) + ) + + yield* sem.withPermits("partition-1", 2)( + Effect.sync(() => messages.push("task-3")) + ) + + deepStrictEqual(messages, ["task-1", "task-2", "task-3"]) + })) +}) diff --git a/repos/effect/packages/effect/test/Pipeable.test.ts b/repos/effect/packages/effect/test/Pipeable.test.ts new file mode 100644 index 0000000..4fd08b0 --- /dev/null +++ b/repos/effect/packages/effect/test/Pipeable.test.ts @@ -0,0 +1,111 @@ +import { describe, it } from "@effect/vitest" +import { assertInstanceOf, assertSome, deepStrictEqual } from "@effect/vitest/utils" +import { Option, Pipeable } from "effect" + +describe("Pipeable", () => { + it("pipeArguments", () => { + const f = (n: number): number => n + 1 + const g = (n: number): number => n * 2 + assertSome(Option.some(2).pipe(Option.map(f)), 3) + assertSome(Option.some(2).pipe(Option.map(f), Option.map(g)), 6) + assertSome(Option.some(2).pipe(Option.map(f), Option.map(g), Option.map(f)), 7) + assertSome(Option.some(2).pipe(Option.map(f), Option.map(g), Option.map(f), Option.map(g)), 14) + assertSome(Option.some(2).pipe(Option.map(f), Option.map(g), Option.map(f), Option.map(g), Option.map(f)), 15) + assertSome( + Option.some(2).pipe(Option.map(f), Option.map(g), Option.map(f), Option.map(g), Option.map(f), Option.map(g)), + 30 + ) + assertSome( + Option.some(2).pipe( + Option.map(f), + Option.map(g), + Option.map(f), + Option.map(g), + Option.map(f), + Option.map(g), + Option.map(f) + ), + 31 + ) + assertSome( + Option.some(2).pipe( + Option.map(f), + Option.map(g), + Option.map(f), + Option.map(g), + Option.map(f), + Option.map(g), + Option.map(f), + Option.map(g) + ), + 62 + ) + assertSome( + Option.some(2).pipe( + Option.map(f), + Option.map(g), + Option.map(f), + Option.map(g), + Option.map(f), + Option.map(g), + Option.map(f), + Option.map(g), + Option.map(f) + ), + 63 + ) + assertSome( + Option.some(2).pipe( + Option.map(f), + Option.map(g), + Option.map(f), + Option.map(g), + Option.map(f), + Option.map(g), + Option.map(f), + Option.map(g), + Option.map(f), + Option.map(g) + ), + 126 + ) + }) + it("pipeable", () => { + class A { + constructor(public a: number) {} + methodA() { + return this.a + } + } + class B extends Pipeable.Class(A) { + constructor(private b: string) { + super(b.length) + } + methodB() { + return [this.b, this.methodA()] + } + } + const b = new B("bb") + + assertInstanceOf(b, A) + assertInstanceOf(b, B) + deepStrictEqual(b.methodB(), ["bb", 2]) + deepStrictEqual(b.pipe((x) => x.methodB()), ["bb", 2]) + }) + it("Class", () => { + class A extends Pipeable.Class() { + constructor(public a: number) { + super() + } + methodA() { + return this.a + } + } + const a = new A(2) + + assertInstanceOf(a, A) + assertInstanceOf(a, Pipeable.Class()) + deepStrictEqual(a.methodA(), 2) + deepStrictEqual(a.pipe((x) => x.methodA()), 2) + }) +}) diff --git a/repos/effect/packages/effect/test/Pool.test.ts b/repos/effect/packages/effect/test/Pool.test.ts new file mode 100644 index 0000000..406ab5c --- /dev/null +++ b/repos/effect/packages/effect/test/Pool.test.ts @@ -0,0 +1,435 @@ +import { describe, it } from "@effect/vitest" +import { assertNone, deepStrictEqual, strictEqual } from "@effect/vitest/utils" +import { Deferred, Duration, Effect, Exit, Fiber, pipe, Pool, Ref, Scope, TestClock, TestServices } from "effect" + +describe("Pool", () => { + it.scoped("preallocates pool items", () => + Effect.gen(function*() { + const count = yield* Ref.make(0) + const get = Effect.acquireRelease( + Ref.updateAndGet(count, (n) => n + 1), + () => Ref.update(count, (n) => n - 1) + ) + yield* Pool.make({ acquire: get, size: 10 }) + yield* Effect.repeat(Ref.get(count), { until: (n) => n === 10 }) + const result = yield* Ref.get(count) + strictEqual(result, 10) + })) + + // it.scoped("benchmark", () => + // Effect.gen(function*() { + // const get = Effect.succeed("resource") + // const pool = yield* Pool.make({ acquire: get, size: 10 }) + // yield* Pool.get(pool).pipe( + // Effect.scoped, + // Effect.repeatN(10000), + // Console.withTime("Pool.get") + // ) + // })) + + it.scoped("cleans up items when shut down", () => + Effect.gen(function*() { + const count = yield* Ref.make(0) + const get = Effect.acquireRelease( + Ref.updateAndGet(count, (n) => n + 1), + () => Ref.update(count, (n) => n - 1) + ) + const scope = yield* Scope.make() + yield* Scope.extend(Pool.make({ acquire: get, size: 10 }), scope) + yield* Effect.repeat(Ref.get(count), { until: (n) => n === 10 }) + yield* Scope.close(scope, Exit.succeed(void 0)) + const result = yield* Ref.get(count) + strictEqual(result, 0) + })) + + it.scoped("defects don't prevent cleanup", () => + Effect.gen(function*() { + const count = yield* Ref.make(0) + const get = Effect.acquireRelease( + Ref.updateAndGet(count, (n) => n + 1), + () => Effect.zipRight(Ref.update(count, (n) => n - 1), Effect.die("boom")) + ) + const scope = yield* Scope.make() + yield* Scope.extend(Pool.make({ acquire: get, size: 10 }), scope) + yield* Effect.repeat(Ref.get(count), { until: (n) => n === 10 }) + yield* Scope.close(scope, Exit.succeed(void 0)) + const result = yield* Ref.get(count) + strictEqual(result, 0) + })) + + it.scoped("acquire one item", () => + Effect.gen(function*() { + const count = yield* Ref.make(0) + const get = Effect.acquireRelease( + Ref.updateAndGet(count, (n) => n + 1), + () => Ref.update(count, (n) => n - 1) + ) + const pool = yield* Pool.make({ acquire: get, size: 10 }) + yield* Effect.repeat(Ref.get(count), { until: (n) => n === 10 }) + const item = yield* Pool.get(pool) + strictEqual(item, 1) + })) + + it.scoped("reports failures via get", () => + Effect.gen(function*() { + const count = yield* Ref.make(0) + const get = Effect.acquireRelease( + Effect.flatMap( + Ref.updateAndGet(count, (n) => n + 1), + Effect.fail + ), + () => Ref.update(count, (n) => n - 1) + ) + const pool = yield* Pool.make({ acquire: get, size: 10 }) + const values = yield* Effect.all(Effect.replicate(9)(Effect.flip(Pool.get(pool)))) + deepStrictEqual(Array.from(values), [1, 2, 3, 4, 5, 6, 7, 8, 9]) + })) + + it.scoped("blocks when item not available", () => + Effect.gen(function*() { + const count = yield* Ref.make(0) + const get = Effect.acquireRelease( + Ref.updateAndGet(count, (n) => n + 1), + () => Ref.update(count, (n) => n - 1) + ) + const pool = yield* Pool.make({ acquire: get, size: 10 }) + yield* Effect.repeat(Ref.get(count), { until: (n) => n === 10 }) + yield* Effect.all(Effect.replicate(10)(Pool.get(pool))) + const result = yield* TestServices.provideLive( + Effect.scoped(Pool.get(pool)).pipe( + Effect.disconnect, + Effect.timeout(Duration.millis(1)), + Effect.option + ) + ) + assertNone(result) + })) + + it.scoped("reuse released items", () => + Effect.gen(function*() { + const count = yield* Ref.make(0) + const get = Effect.acquireRelease( + Ref.updateAndGet(count, (n) => n + 1), + () => Ref.update(count, (n) => n - 1) + ) + const pool = yield* Pool.make({ acquire: get, size: 10 }) + yield* Effect.repeatN(99)(Effect.scoped(Pool.get(pool))) + const result = yield* Ref.get(count) + strictEqual(result, 10) + })) + + it.scoped("invalidate item", () => + Effect.gen(function*() { + const count = yield* Ref.make(0) + const get = Effect.acquireRelease( + Ref.updateAndGet(count, (n) => n + 1), + () => Ref.update(count, (n) => n - 1) + ) + const pool = yield* Pool.make({ acquire: get, size: 10 }) + yield* Effect.repeat(Ref.get(count), { until: (n) => n === 10 }) + yield* Pool.invalidate(pool, 1) + yield* Effect.repeat(Ref.get(count), { until: (n) => n === 10 }) + const result = yield* Effect.scoped(Pool.get(pool)) + const value = yield* Ref.get(count) + strictEqual(result, 2) + strictEqual(value, 10) + })) + + it.scoped("invalidate all items in pool and check that pool.get doesn't hang forever", () => + Effect.gen(function*() { + const allocated = yield* Ref.make(0) + const finalized = yield* Ref.make(0) + const get = Effect.acquireRelease( + Ref.updateAndGet(allocated, (n) => n + 1), + () => Ref.update(finalized, (n) => n + 1) + ) + const pool = yield* Pool.make({ acquire: get, size: 2 }) + yield* Effect.repeat(Ref.get(allocated), { until: (n) => n === 2 }) + yield* Pool.invalidate(pool, 1) + yield* Pool.invalidate(pool, 2) + const result = yield* Effect.scoped(Pool.get(pool)) + const allocatedCount = yield* Ref.get(allocated) + const finalizedCount = yield* Ref.get(finalized) + strictEqual(result, 3) + strictEqual(allocatedCount, 4) + strictEqual(finalizedCount, 2) + })) + + it.scoped("retry on failed acquire should not exhaust pool", () => + Effect.gen(function*() { + const acquire = Effect.as(Effect.fail("error"), 1) + const pool = yield* Pool.makeWithTTL({ acquire, min: 0, max: 1, timeToLive: Duration.infinity }) + const result = yield* pipe( + Effect.scoped(Effect.retry(Pool.get(pool), { times: 5 })), + Effect.timeoutFail({ + onTimeout: () => "timeout", + duration: Duration.seconds(1) + }), + Effect.flip, + TestServices.provideLive + ) + strictEqual(result, "error") + })) + + it.scoped("compositional retry", () => + Effect.gen(function*() { + const cond = (i: number) => (i <= 10 ? Effect.fail(i) : Effect.succeed(i)) + const count = yield* Ref.make(0) + const get = Effect.acquireRelease( + Ref.updateAndGet(count, (n) => n + 1).pipe( + Effect.flatMap(cond) + ), + () => Ref.update(count, (n) => n - 1) + ) + const pool = yield* Pool.make({ acquire: get, size: 10 }) + const result = yield* Effect.eventually(Effect.scoped(Pool.get(pool))) + strictEqual(result, 11) + })) + + it.scoped("max pool size", () => + Effect.gen(function*() { + const deferred = yield* Deferred.make() + const count = yield* Ref.make(0) + const acquire = Effect.acquireRelease( + Ref.updateAndGet(count, (n) => n + 1), + () => Ref.update(count, (n) => n - 1) + ) + const pool = yield* Pool.makeWithTTL({ + acquire, + min: 10, + max: 15, + timeToLive: Duration.seconds(60) + }) + yield* pipe( + Effect.scoped(Effect.zipRight( + Pool.get(pool), + Deferred.await(deferred) + )), + Effect.fork, + Effect.repeatN(14) + ) + yield* Effect.repeat(Ref.get(count), { until: (n) => n === 15 }) + yield* Deferred.succeed(deferred, void 0) + const max = yield* Ref.get(count) + yield* TestClock.adjust(Duration.seconds(60)) + const min = yield* Ref.get(count) + strictEqual(min, 10) + strictEqual(max, 15) + })) + + it.scoped("max pool size with concurrency: 3", () => + Effect.gen(function*() { + const deferred = yield* Deferred.make() + const count = yield* Ref.make(0) + const acquire = Effect.acquireRelease( + Ref.updateAndGet(count, (n) => n + 1), + () => Ref.update(count, (n) => n - 1) + ) + const pool = yield* Pool.makeWithTTL({ + acquire, + min: 10, + max: 15, + concurrency: 3, + timeToLive: Duration.seconds(60) + }) + yield* pipe( + Effect.scoped(Effect.zipRight( + Pool.get(pool), + Deferred.await(deferred) + )), + Effect.fork, + Effect.repeatN(14 * 3) + ) + yield* Effect.repeat(Ref.get(count), { until: (n) => n === 15 }) + yield* Deferred.succeed(deferred, void 0) + const max = yield* Ref.get(count) + yield* TestClock.adjust(Duration.seconds(60)) + const min = yield* Ref.get(count) + strictEqual(min, 10) + strictEqual(max, 15) + })) + + it.scoped("concurrency reclaim", () => + Effect.gen(function*() { + const count = yield* Ref.make(0) + const acquire = Effect.acquireRelease( + Ref.updateAndGet(count, (n) => n + 1), + () => Ref.update(count, (n) => n - 1) + ) + const pool = yield* Pool.makeWithTTL({ + acquire, + min: 0, + max: 2, + concurrency: 2, + timeToLive: Duration.seconds(60) + }) + + const scope1 = yield* Scope.make() + yield* Scope.extend(Pool.get(pool), scope1) + yield* Pool.get(pool) + yield* Effect.scoped(Pool.get(pool)) + yield* TestClock.adjust(Duration.seconds(60)) + yield* Scope.close(scope1, Exit.void) + yield* Pool.get(pool) + yield* Pool.get(pool) + strictEqual(yield* Pool.get(pool), 1) + strictEqual(yield* Ref.get(count), 2) + })) + + it.scoped("scale to zero", () => + Effect.gen(function*() { + const deferred = yield* Deferred.make() + const count = yield* Ref.make(0) + const acquire = Effect.acquireRelease( + Ref.updateAndGet(count, (n) => n + 1), + () => Ref.update(count, (n) => n - 1) + ) + const pool = yield* Pool.makeWithTTL({ + acquire, + min: 0, + max: 10, + concurrency: 3, + timeToLive: Duration.seconds(60) + }) + yield* pipe( + Effect.scoped(Effect.zipRight( + Pool.get(pool), + Deferred.await(deferred) + )), + Effect.fork, + Effect.repeatN(29) + ) + yield* Effect.repeat(Ref.get(count), { until: (n) => n === 10 }) + yield* Deferred.succeed(deferred, void 0) + const max = yield* Ref.get(count) + yield* TestClock.adjust(Duration.seconds(60)) + const min = yield* Ref.get(count) + strictEqual(min, 0) + strictEqual(max, 10) + })) + + it.scoped("max pool size creation strategy", () => + Effect.gen(function*() { + const invalidated = yield* Ref.make(0) + const acquire = Effect.acquireRelease( + Effect.succeed("resource"), + () => Ref.update(invalidated, (n) => n + 1) + ) + const pool = yield* Pool.makeWithTTL({ + acquire, + min: 10, + max: 15, + timeToLive: Duration.seconds(60), + timeToLiveStrategy: "creation" + }) + const scope = yield* Scope.make() + yield* Pool.get(pool).pipe( + Effect.repeatN(14), + Scope.extend(scope) + ) + const one = yield* Ref.get(invalidated) + yield* TestClock.adjust(Duration.seconds(60)) + const two = yield* Ref.get(invalidated) + yield* Scope.close(scope, Exit.void) + const three = yield* Ref.get(invalidated) + strictEqual(one, 0) + strictEqual(two, 0) + strictEqual(three, 15) + })) + + it.scoped("shutdown robustness", () => + Effect.gen(function*() { + const count = yield* Ref.make(0) + const get = Effect.acquireRelease( + Ref.updateAndGet(count, (n) => n + 1), + () => Ref.update(count, (n) => n - 1) + ) + const scope = yield* Scope.make() + const pool = yield* Scope.extend(Pool.make({ acquire: get, size: 10 }), scope) + yield* pipe( + Effect.scoped(Pool.get(pool)), + Effect.fork, + Effect.repeatN(99) + ) + yield* Scope.close(scope, Exit.succeed(void 0)) + const result = yield* Effect.repeat(Ref.get(count), { until: (n) => n === 0 }) + strictEqual(result, 0) + })) + + it.scoped("shutdown with pending takers", () => + Effect.gen(function*() { + const count = yield* Ref.make(0) + const get = Effect.acquireRelease( + Ref.updateAndGet(count, (n) => n + 1), + () => Ref.update(count, (n) => n - 1) + ) + const scope = yield* Scope.make() + const pool = yield* Scope.extend(Pool.make({ acquire: get, size: 10 }), scope) + yield* pipe( + Pool.get(pool), + Scope.extend(scope), + Effect.fork, + Effect.repeatN(99) + ) + yield* Scope.close(scope, Exit.succeed(void 0)) + const result = yield* Effect.repeat(Ref.get(count), { until: (n) => n === 0 }) + strictEqual(result, 0) + })) + + it.scoped("get is interruptible", () => + Effect.gen(function*() { + const count = yield* Ref.make(0) + const get = Effect.acquireRelease( + Ref.updateAndGet(count, (n) => n + 1), + () => Ref.update(count, (n) => n - 1) + ) + const fiberId = yield* Effect.fiberId + const pool = yield* Pool.make({ acquire: get, size: 10 }) + yield* Effect.repeatN(Pool.get(pool), 9) + const fiber = yield* Effect.fork(Pool.get(pool)) + const result = yield* Fiber.interrupt(fiber) + deepStrictEqual(result, Exit.interrupt(fiberId)) + })) + + it.scoped("get is interruptible with dynamic size", () => + Effect.gen(function*() { + const get = Effect.never.pipe(Effect.forkScoped) + const fiberId = yield* Effect.fiberId + const pool = yield* Pool.makeWithTTL({ acquire: get, min: 0, max: 10, timeToLive: Duration.infinity }) + yield* Effect.repeatN(Pool.get(pool), 9) + const fiber = yield* Effect.fork(Pool.get(pool)) + const result = yield* Fiber.interrupt(fiber) + deepStrictEqual(result, Exit.interrupt(fiberId)) + })) + + it.scoped("finalizer is called for failed allocations", () => + Effect.gen(function*() { + const scope = yield* Scope.make() + const allocations = yield* Ref.make(0) + const released = yield* Ref.make(0) + const get = Effect.acquireRelease( + Ref.updateAndGet(allocations, (n) => n + 1), + () => Ref.update(released, (n) => n + 1) + ).pipe( + Effect.andThen(Effect.fail("boom")) + ) + const pool = yield* Pool.make({ acquire: get, size: 10 }).pipe( + Scope.extend(scope) + ) + yield* Effect.scoped(pool.get).pipe( + Effect.ignore + ) + strictEqual(yield* Ref.get(allocations), 10) + strictEqual(yield* Ref.get(released), 10) + })) + + it.scoped("is subtype of Effect", () => + Effect.gen(function*() { + const pool = yield* Pool.make({ + acquire: Effect.succeed(1), + size: 1 + }) + const item = yield* pool + strictEqual(item, 1) + })) +}) diff --git a/repos/effect/packages/effect/test/Predicate.test.ts b/repos/effect/packages/effect/test/Predicate.test.ts new file mode 100644 index 0000000..b2162a3 --- /dev/null +++ b/repos/effect/packages/effect/test/Predicate.test.ts @@ -0,0 +1,342 @@ +import { describe, it } from "@effect/vitest" +import { assertFalse, assertTrue } from "@effect/vitest/utils" +import { Function as Fun, pipe, Predicate } from "effect" + +const isPositive: Predicate.Predicate = (n) => n > 0 +const isNegative: Predicate.Predicate = (n) => n < 0 +const isLessThan2: Predicate.Predicate = (n) => n < 2 +const isString: Predicate.Refinement = (u: unknown): u is string => typeof u === "string" + +interface NonEmptyStringBrand { + readonly NonEmptyString: unique symbol +} + +type NonEmptyString = string & NonEmptyStringBrand + +const isNonEmptyString: Predicate.Refinement = (s): s is NonEmptyString => s.length > 0 + +describe("Predicate", () => { + it("compose", () => { + const refinement = pipe(isString, Predicate.compose(isNonEmptyString)) + assertTrue(refinement("a")) + assertFalse(refinement(null)) + assertFalse(refinement("")) + }) + + it("mapInput", () => { + type A = { + readonly a: number + } + const predicate = pipe( + isPositive, + Predicate.mapInput((a: A) => a.a) + ) + assertFalse(predicate({ a: -1 })) + assertFalse(predicate({ a: 0 })) + assertTrue(predicate({ a: 1 })) + }) + + it("product", () => { + const product = Predicate.product + const p = product(isPositive, isNegative) + assertTrue(p([1, -1])) + assertFalse(p([1, 1])) + assertFalse(p([-1, -1])) + assertFalse(p([-1, 1])) + }) + + it("productMany", () => { + const productMany = Predicate.productMany + const p = productMany(isPositive, [isNegative]) + assertTrue(p([1, -1])) + assertFalse(p([1, 1])) + assertFalse(p([-1, -1])) + assertFalse(p([-1, 1])) + }) + + it("tuple", () => { + const p = Predicate.tuple(isPositive, isNegative) + assertTrue(p([1, -1])) + assertFalse(p([1, 1])) + assertFalse(p([-1, -1])) + assertFalse(p([-1, 1])) + }) + + it("struct", () => { + const p = Predicate.struct({ a: isPositive, b: isNegative }) + assertTrue(p({ a: 1, b: -1 })) + assertFalse(p({ a: 1, b: 1 })) + assertFalse(p({ a: -1, b: -1 })) + assertFalse(p({ a: -1, b: 1 })) + }) + + it("all", () => { + const p = Predicate.all([isPositive, isNegative]) + assertTrue(p([1])) + assertTrue(p([1, -1])) + assertFalse(p([1, 1])) + assertFalse(p([-1, -1])) + assertFalse(p([-1, 1])) + }) + + it("not", () => { + const p = Predicate.not(isPositive) + assertFalse(p(1)) + assertTrue(p(0)) + assertTrue(p(-1)) + }) + + it("or", () => { + const p = pipe(isPositive, Predicate.or(isNegative)) + assertTrue(p(-1)) + assertTrue(p(1)) + assertFalse(p(0)) + }) + + it("and", () => { + const p = pipe(isPositive, Predicate.and(isLessThan2)) + assertTrue(p(1)) + assertFalse(p(-1)) + assertFalse(p(3)) + }) + + it("xor", () => { + assertFalse(pipe(Fun.constTrue, Predicate.xor(Fun.constTrue))(null)) // true xor true = false + assertTrue(pipe(Fun.constTrue, Predicate.xor(Fun.constFalse))(null)) // true xor false = true + assertTrue(pipe(Fun.constFalse, Predicate.xor(Fun.constTrue))(null)) // false xor true = true + assertFalse(pipe(Fun.constFalse, Predicate.xor(Fun.constFalse))(null)) // false xor false = false + }) + + it("eqv", () => { + assertTrue(pipe(Fun.constTrue, Predicate.eqv(Fun.constTrue))(null)) // true eqv true = true + assertFalse(pipe(Fun.constTrue, Predicate.eqv(Fun.constFalse))(null)) // true eqv false = false + assertFalse(pipe(Fun.constFalse, Predicate.eqv(Fun.constTrue))(null)) // false eqv true = false + assertTrue(pipe(Fun.constFalse, Predicate.eqv(Fun.constFalse))(null)) // false eqv false = true + }) + + it("implies", () => { + assertTrue(pipe(Fun.constTrue, Predicate.implies(Fun.constTrue))(null)) // true implies true = true + assertFalse(pipe(Fun.constTrue, Predicate.implies(Fun.constFalse))(null)) // true implies false = false + assertTrue(pipe(Fun.constFalse, Predicate.implies(Fun.constTrue))(null)) // false implies true = true + assertTrue(pipe(Fun.constFalse, Predicate.implies(Fun.constFalse))(null)) // false implies false = true + }) + + it("nor", () => { + assertFalse(pipe(Fun.constTrue, Predicate.nor(Fun.constTrue))(null)) // true nor true = false + assertFalse(pipe(Fun.constTrue, Predicate.nor(Fun.constFalse))(null)) // true nor false = false + assertFalse(pipe(Fun.constFalse, Predicate.nor(Fun.constTrue))(null)) // false nor true = false + assertTrue(pipe(Fun.constFalse, Predicate.nor(Fun.constFalse))(null)) // false nor false = true + }) + + it("nand", () => { + assertFalse(pipe(Fun.constTrue, Predicate.nand(Fun.constTrue))(null)) // true nand true = false + assertTrue(pipe(Fun.constTrue, Predicate.nand(Fun.constFalse))(null)) // true nand false = true + assertTrue(pipe(Fun.constFalse, Predicate.nand(Fun.constTrue))(null)) // false nand true = true + assertTrue(pipe(Fun.constFalse, Predicate.nand(Fun.constFalse))(null)) // false nand false = true + }) + + it("some", () => { + const predicate = Predicate.some([isPositive, isNegative]) + assertFalse(predicate(0)) + assertTrue(predicate(-1)) + assertTrue(predicate(1)) + }) + + it("every", () => { + const predicate = Predicate.every([isPositive, isLessThan2]) + assertFalse(predicate(0)) + assertFalse(predicate(-2)) + assertTrue(predicate(1)) + }) + + it("isTruthy", () => { + assertTrue(Predicate.isTruthy(true)) + assertFalse(Predicate.isTruthy(false)) + assertTrue(Predicate.isTruthy("a")) + assertFalse(Predicate.isTruthy("")) + assertTrue(Predicate.isTruthy(1)) + assertFalse(Predicate.isTruthy(0)) + assertTrue(Predicate.isTruthy(1n)) + assertFalse(Predicate.isTruthy(0n)) + }) + + it("isFunction", () => { + assertTrue(Predicate.isFunction(Predicate.isFunction)) + assertFalse(Predicate.isFunction("function")) + }) + + it("isUndefined", () => { + assertTrue(Predicate.isUndefined(undefined)) + assertFalse(Predicate.isUndefined(null)) + assertFalse(Predicate.isUndefined("undefined")) + }) + + it("isNotUndefined", () => { + assertFalse(Predicate.isNotUndefined(undefined)) + assertTrue(Predicate.isNotUndefined(null)) + assertTrue(Predicate.isNotUndefined("undefined")) + }) + + it("isNull", () => { + assertTrue(Predicate.isNull(null)) + assertFalse(Predicate.isNull(undefined)) + assertFalse(Predicate.isNull("null")) + }) + + it("isNotNull", () => { + assertFalse(Predicate.isNotNull(null)) + assertTrue(Predicate.isNotNull(undefined)) + assertTrue(Predicate.isNotNull("null")) + }) + + it("isNever", () => { + assertFalse(Predicate.isNever(null)) + assertFalse(Predicate.isNever(undefined)) + assertFalse(Predicate.isNever({})) + assertFalse(Predicate.isNever([])) + }) + + it("isUnknown", () => { + assertTrue(Predicate.isUnknown(null)) + assertTrue(Predicate.isUnknown(undefined)) + assertTrue(Predicate.isUnknown({})) + assertTrue(Predicate.isUnknown([])) + }) + + it("isObject", () => { + assertTrue(Predicate.isObject({})) + assertTrue(Predicate.isObject([])) + assertTrue(Predicate.isObject(() => 1)) + assertFalse(Predicate.isObject(null)) + assertFalse(Predicate.isObject(undefined)) + assertFalse(Predicate.isObject("a")) + assertFalse(Predicate.isObject(1)) + assertFalse(Predicate.isObject(true)) + assertFalse(Predicate.isObject(1n)) + assertFalse(Predicate.isObject(Symbol.for("a"))) + }) + + it("isSet", () => { + assertTrue(Predicate.isSet(new Set([1, 2]))) + assertTrue(Predicate.isSet(new Set())) + assertFalse(Predicate.isSet({})) + assertFalse(Predicate.isSet(null)) + assertFalse(Predicate.isSet(undefined)) + }) + + it("isMap", () => { + assertTrue(Predicate.isMap(new Map())) + assertFalse(Predicate.isMap({})) + assertFalse(Predicate.isMap(null)) + assertFalse(Predicate.isMap(undefined)) + }) + + it("hasProperty", () => { + const a = Symbol.for("effect/test/a") + + assertTrue(Predicate.hasProperty({ a: 1 }, "a")) + assertTrue(Predicate.hasProperty("a")({ a: 1 })) + assertTrue(Predicate.hasProperty({ [a]: 1 }, a)) + assertTrue(Predicate.hasProperty(a)({ [a]: 1 })) + + assertFalse(Predicate.hasProperty({}, "a")) + assertFalse(Predicate.hasProperty(null, "a")) + assertFalse(Predicate.hasProperty(undefined, "a")) + assertFalse(Predicate.hasProperty({}, "a")) + assertFalse(Predicate.hasProperty(() => {}, "a")) + + assertFalse(Predicate.hasProperty({}, a)) + assertFalse(Predicate.hasProperty(null, a)) + assertFalse(Predicate.hasProperty(undefined, a)) + assertFalse(Predicate.hasProperty({}, a)) + assertFalse(Predicate.hasProperty(() => {}, a)) + }) + + it("isTagged", () => { + assertFalse(Predicate.isTagged(1, "a")) + assertFalse(Predicate.isTagged("", "a")) + assertFalse(Predicate.isTagged({}, "a")) + assertFalse(Predicate.isTagged("a")({})) + assertFalse(Predicate.isTagged({ a: "a" }, "a")) + assertTrue(Predicate.isTagged({ _tag: "a" }, "a")) + assertTrue(Predicate.isTagged("a")({ _tag: "a" })) + }) + + it("isNullable", () => { + assertTrue(Predicate.isNullable(null)) + assertTrue(Predicate.isNullable(undefined)) + assertFalse(Predicate.isNullable({})) + assertFalse(Predicate.isNullable([])) + }) + + it("isNotNullable", () => { + assertTrue(Predicate.isNotNullable({})) + assertTrue(Predicate.isNotNullable([])) + assertFalse(Predicate.isNotNullable(null)) + assertFalse(Predicate.isNotNullable(undefined)) + }) + + it("isError", () => { + assertTrue(Predicate.isError(new Error())) + assertFalse(Predicate.isError(null)) + assertFalse(Predicate.isError({})) + }) + + it("isUint8Array", () => { + assertTrue(Predicate.isUint8Array(new Uint8Array())) + assertFalse(Predicate.isUint8Array(null)) + assertFalse(Predicate.isUint8Array({})) + }) + + it("isDate", () => { + assertTrue(Predicate.isDate(new Date())) + assertFalse(Predicate.isDate(null)) + assertFalse(Predicate.isDate({})) + }) + + it("isIterable", () => { + assertTrue(Predicate.isIterable([])) + assertTrue(Predicate.isIterable(new Set())) + assertFalse(Predicate.isIterable(null)) + assertFalse(Predicate.isIterable({})) + }) + + it("isRecord", () => { + assertTrue(Predicate.isRecord({})) + assertTrue(Predicate.isRecord({ a: 1 })) + + assertFalse(Predicate.isRecord([])) + assertFalse(Predicate.isRecord([1, 2, 3])) + assertFalse(Predicate.isRecord(null)) + assertFalse(Predicate.isRecord(undefined)) + assertFalse(Predicate.isRecord(() => null)) + }) + + it("isReadonlyRecord", () => { + assertTrue(Predicate.isReadonlyRecord({})) + assertTrue(Predicate.isReadonlyRecord({ a: 1 })) + + assertFalse(Predicate.isReadonlyRecord([])) + assertFalse(Predicate.isReadonlyRecord([1, 2, 3])) + assertFalse(Predicate.isReadonlyRecord(null)) + assertFalse(Predicate.isReadonlyRecord(undefined)) + }) + + it("isTupleOf", () => { + assertTrue(Predicate.isTupleOf([1, 2, 3], 3)) + assertFalse(Predicate.isTupleOf([1, 2, 3], 4)) + assertFalse(Predicate.isTupleOf([1, 2, 3], 2)) + }) + + it("isTupleOfAtLeast", () => { + assertTrue(Predicate.isTupleOfAtLeast([1, 2, 3], 3)) + assertTrue(Predicate.isTupleOfAtLeast([1, 2, 3], 2)) + assertFalse(Predicate.isTupleOfAtLeast([1, 2, 3], 4)) + }) + + it("isRegExp", () => { + assertTrue(Predicate.isRegExp(/a/)) + assertFalse(Predicate.isRegExp(null)) + assertFalse(Predicate.isRegExp("a")) + }) +}) diff --git a/repos/effect/packages/effect/test/PubSub.test.ts b/repos/effect/packages/effect/test/PubSub.test.ts new file mode 100644 index 0000000..2095149 --- /dev/null +++ b/repos/effect/packages/effect/test/PubSub.test.ts @@ -0,0 +1,729 @@ +import { describe, it } from "@effect/vitest" +import { assertSome, deepStrictEqual, strictEqual } from "@effect/vitest/utils" +import { Array, Chunk, Deferred, Effect, Fiber, pipe, PubSub, Queue } from "effect" + +describe("PubSub", () => { + it.effect("publishAll - capacity 2 (BoundedPubSubPow2)", () => { + const messages = [1, 2] + return PubSub.bounded(2).pipe( + Effect.flatMap((pubsub) => + Effect.scoped( + Effect.gen(function*() { + const dequeue1 = yield* (PubSub.subscribe(pubsub)) + const dequeue2 = yield* (PubSub.subscribe(pubsub)) + yield* (PubSub.publishAll(pubsub, messages)) + const takes1 = yield* (Queue.takeAll(dequeue1)) + const takes2 = yield* (Queue.takeAll(dequeue2)) + deepStrictEqual([...takes1], messages) + deepStrictEqual([...takes2], messages) + }) + ) + ) + ) + }) + it.effect("publishAll - capacity 4 (BoundedPubSubPow2)", () => { + const messages = [1, 2] + return PubSub.bounded(4).pipe( + Effect.flatMap((pubsub) => + Effect.scoped( + Effect.gen(function*() { + const dequeue1 = yield* (PubSub.subscribe(pubsub)) + const dequeue2 = yield* (PubSub.subscribe(pubsub)) + yield* (PubSub.publishAll(pubsub, messages)) + const takes1 = yield* (Queue.takeAll(dequeue1)) + const takes2 = yield* (Queue.takeAll(dequeue2)) + deepStrictEqual([...takes1], messages) + deepStrictEqual([...takes2], messages) + }) + ) + ) + ) + }) + it.effect("publishAll - capacity 3 (BoundedPubSubArb)", () => { + const messages = [1, 2] + return PubSub.bounded(3).pipe( + Effect.flatMap((pubsub) => + Effect.scoped( + Effect.gen(function*() { + const dequeue1 = yield* (PubSub.subscribe(pubsub)) + const dequeue2 = yield* (PubSub.subscribe(pubsub)) + yield* (PubSub.publishAll(pubsub, messages)) + const takes1 = yield* (Queue.takeAll(dequeue1)) + const takes2 = yield* (Queue.takeAll(dequeue2)) + deepStrictEqual([...takes1], messages) + deepStrictEqual([...takes2], messages) + }) + ) + ) + ) + }) + it.effect("sequential publishers and subscribers with one publisher and one subscriber", () => + Effect.gen(function*() { + const values = Array.range(0, 9) + const deferred1 = yield* Deferred.make() + const deferred2 = yield* Deferred.make() + const pubsub = yield* PubSub.bounded(10) + const subscriber = yield* pipe( + PubSub.subscribe(pubsub), + Effect.flatMap((subscription) => + pipe( + Deferred.succeed(deferred1, void 0), + Effect.zipRight(Deferred.await(deferred2)), + Effect.zipRight(pipe(values, Effect.forEach(() => Queue.take(subscription)))) + ) + ), + Effect.scoped, + Effect.fork + ) + yield* Deferred.await(deferred1) + yield* pipe(values, Effect.forEach((n) => PubSub.publish(pubsub, n))) + yield* Deferred.succeed(deferred2, void 0) + const result = yield* Fiber.join(subscriber) + deepStrictEqual(result, values) + })) + it.effect("sequential publishers and subscribers with one publisher and two subscribers", () => + Effect.gen(function*() { + const values = Array.range(0, 9) + const deferred1 = yield* Deferred.make() + const deferred2 = yield* Deferred.make() + const deferred3 = yield* Deferred.make() + const pubsub = yield* PubSub.bounded(10) + const subscriber1 = yield* pubsub.pipe( + PubSub.subscribe, + Effect.flatMap((subscription) => + pipe( + Deferred.succeed(deferred1, void 0), + Effect.zipRight(Deferred.await(deferred3)), + Effect.zipRight(pipe(values, Effect.forEach(() => Queue.take(subscription)))) + ) + ), + Effect.scoped, + Effect.fork + ) + const subscriber2 = yield* pubsub.pipe( + PubSub.subscribe, + Effect.flatMap((subscription) => + pipe( + Deferred.succeed(deferred2, void 0), + Effect.zipRight(Deferred.await(deferred3)), + Effect.zipRight(pipe(values, Effect.forEach(() => Queue.take(subscription)))) + ) + ), + Effect.scoped, + Effect.fork + ) + yield* Deferred.await(deferred1) + yield* Deferred.await(deferred2) + yield* pipe(values, Effect.forEach((n) => PubSub.publish(pubsub, n))) + yield* Deferred.succeed(deferred3, undefined) + const result1 = yield* Fiber.join(subscriber1) + const result2 = yield* Fiber.join(subscriber2) + deepStrictEqual(result1, values) + deepStrictEqual(result2, values) + })) + it.effect("backpressured concurrent publishers and subscribers - one to one", () => + Effect.gen(function*() { + const values = Array.range(0, 64) + const deferred = yield* Deferred.make() + const pubsub = yield* PubSub.bounded(64) + const subscriber = yield* pipe( + PubSub.subscribe(pubsub), + Effect.flatMap((subscription) => + pipe( + Deferred.succeed(deferred, void 0), + Effect.zipRight(pipe(values, Effect.forEach((_) => Queue.take(subscription)))) + ) + ), + Effect.scoped, + Effect.fork + ) + yield* Deferred.await(deferred) + yield* pipe( + values, + Effect.forEach((n) => PubSub.publish(pubsub, n)), + Effect.fork + ) + const result = yield* Fiber.join(subscriber) + deepStrictEqual(result, values) + })) + it.effect("backpressured concurrent publishers and subscribers - one to many", () => + Effect.gen(function*() { + const values = Array.range(0, 64) + const deferred1 = yield* Deferred.make() + const deferred2 = yield* Deferred.make() + const pubsub = yield* PubSub.bounded(64) + const subscriber1 = yield* pipe( + PubSub.subscribe(pubsub), + Effect.flatMap((subscription) => + pipe( + Deferred.succeed(deferred1, void 0), + Effect.zipRight(pipe(values, Effect.forEach((_) => Queue.take(subscription)))) + ) + ), + Effect.scoped, + Effect.fork + ) + const subscriber2 = yield* pipe( + PubSub.subscribe(pubsub), + Effect.flatMap((subscription) => + pipe( + Deferred.succeed(deferred2, void 0), + Effect.zipRight(pipe(values, Effect.forEach((_) => Queue.take(subscription)))) + ) + ), + Effect.scoped, + Effect.fork + ) + yield* Deferred.await(deferred1) + yield* Deferred.await(deferred2) + yield* pipe( + values, + Effect.forEach((n) => PubSub.publish(pubsub, n)), + Effect.fork + ) + const result1 = yield* Fiber.join(subscriber1) + const result2 = yield* Fiber.join(subscriber2) + deepStrictEqual(result1, values) + deepStrictEqual(result2, values) + })) + it.effect("backpressured concurrent publishers and subscribers - many to many", () => + Effect.gen(function*() { + const values = Array.range(1, 64) + const deferred1 = yield* Deferred.make() + const deferred2 = yield* Deferred.make() + const pubsub = yield* PubSub.bounded(64 * 2) + const subscriber1 = yield* pipe( + PubSub.subscribe(pubsub), + Effect.flatMap((subscription) => + pipe( + Deferred.succeed(deferred1, void 0), + Effect.zipRight(pipe( + values, + Array.appendAll(values), + Effect.forEach((_) => Queue.take(subscription)) + )) + ) + ), + Effect.scoped, + Effect.fork + ) + const subscriber2 = yield* pipe( + PubSub.subscribe(pubsub), + Effect.flatMap((subscription) => + pipe( + Deferred.succeed(deferred2, void 0), + Effect.zipRight(pipe( + values, + Array.appendAll(values), + Effect.forEach((_) => Queue.take(subscription)) + )) + ) + ), + Effect.scoped, + Effect.fork + ) + yield* Deferred.await(deferred1) + yield* Deferred.await(deferred2) + const fiber = yield* pipe( + values, + Effect.forEach((n) => PubSub.publish(pubsub, n)), + Effect.fork + ) + yield* pipe(values, Array.map((n) => -n), Effect.forEach((n) => PubSub.publish(pubsub, n)), Effect.fork) + const result1 = yield* Fiber.join(subscriber1) + const result2 = yield* Fiber.join(subscriber2) + yield* Fiber.join(fiber) + deepStrictEqual(pipe(result1, Array.filter((n) => n > 0)), values) + deepStrictEqual( + pipe(result1, Array.filter((n) => n < 0)), + pipe(values, Array.map((n) => -n)) + ) + deepStrictEqual(pipe(result2, Array.filter((n) => n > 0)), values) + deepStrictEqual( + pipe(result2, Array.filter((n) => n < 0)), + pipe(values, Array.map((n) => -n)) + ) + })) + it.effect("dropping concurrent publishers and subscribers - one to one", () => + Effect.gen(function*() { + const values = Array.range(0, 64) + const deferred = yield* Deferred.make() + const pubsub = yield* PubSub.dropping(64) + const subscriber = yield* pipe( + PubSub.subscribe(pubsub), + Effect.flatMap((subscription) => + pipe( + Deferred.succeed(deferred, void 0), + Effect.zipRight(pipe(values, Effect.forEach((_) => Queue.take(subscription)))) + ) + ), + Effect.scoped, + Effect.fork + ) + yield* Deferred.await(deferred) + yield* pipe( + values, + Effect.forEach((n) => PubSub.publish(pubsub, n)), + Effect.fork + ) + const result = yield* Fiber.join(subscriber) + deepStrictEqual(result, values) + })) + it.effect("dropping concurrent publishers and subscribers - one to many", () => + Effect.gen(function*() { + const values = Array.range(0, 64) + const deferred1 = yield* Deferred.make() + const deferred2 = yield* Deferred.make() + const pubsub = yield* PubSub.dropping(64) + const subscriber1 = yield* pipe( + PubSub.subscribe(pubsub), + Effect.flatMap((subscription) => + pipe( + Deferred.succeed(deferred1, void 0), + Effect.zipRight(pipe(values, Effect.forEach((_) => Queue.take(subscription)))) + ) + ), + Effect.scoped, + Effect.fork + ) + const subscriber2 = yield* pipe( + PubSub.subscribe(pubsub), + Effect.flatMap((subscription) => + pipe( + Deferred.succeed(deferred2, void 0), + Effect.zipRight(pipe(values, Effect.forEach((_) => Queue.take(subscription)))) + ) + ), + Effect.scoped, + Effect.fork + ) + yield* Deferred.await(deferred1) + yield* Deferred.await(deferred2) + yield* pipe( + values, + Effect.forEach((n) => PubSub.publish(pubsub, n)), + Effect.fork + ) + const result1 = yield* Fiber.join(subscriber1) + const result2 = yield* Fiber.join(subscriber2) + deepStrictEqual(result1, values) + deepStrictEqual(result2, values) + })) + it.effect("dropping concurrent publishers and subscribers - many to many", () => + Effect.gen(function*() { + const values = Array.range(1, 64) + const deferred1 = yield* Deferred.make() + const deferred2 = yield* Deferred.make() + const pubsub = yield* PubSub.dropping(64 * 2) + const subscriber1 = yield* pipe( + PubSub.subscribe(pubsub), + Effect.flatMap((subscription) => + pipe( + Deferred.succeed(deferred1, void 0), + Effect.zipRight(pipe( + values, + Array.appendAll(values), + Effect.forEach((_) => Queue.take(subscription)) + )) + ) + ), + Effect.scoped, + Effect.fork + ) + const subscriber2 = yield* pipe( + PubSub.subscribe(pubsub), + Effect.flatMap((subscription) => + pipe( + Deferred.succeed(deferred2, void 0), + Effect.zipRight(pipe( + values, + Array.appendAll(values), + Effect.forEach((_) => Queue.take(subscription)) + )) + ) + ), + Effect.scoped, + Effect.fork + ) + yield* Deferred.await(deferred1) + yield* Deferred.await(deferred2) + const fiber = yield* pipe( + values, + Effect.forEach((n) => PubSub.publish(pubsub, n)), + Effect.fork + ) + yield* pipe(values, Array.map((n) => -n), Effect.forEach((n) => PubSub.publish(pubsub, n)), Effect.fork) + const result1 = yield* Fiber.join(subscriber1) + const result2 = yield* Fiber.join(subscriber2) + yield* Fiber.join(fiber) + deepStrictEqual(pipe(result1, Array.filter((n) => n > 0)), values) + deepStrictEqual( + pipe(result1, Array.filter((n) => n < 0)), + pipe(values, Array.map((n) => -n)) + ) + deepStrictEqual(pipe(result2, Array.filter((n) => n > 0)), values) + deepStrictEqual( + pipe(result2, Array.filter((n) => n < 0)), + pipe(values, Array.map((n) => -n)) + ) + })) + it.effect("sliding concurrent publishers and subscribers - one to one", () => + Effect.gen(function*() { + const values = Array.range(0, 64) + const deferred = yield* Deferred.make() + const pubsub = yield* PubSub.sliding(64) + const subscriber = yield* pipe( + PubSub.subscribe(pubsub), + Effect.flatMap((subscription) => + pipe( + Deferred.succeed(deferred, void 0), + Effect.zipRight(pipe(values, Effect.forEach((_) => Queue.take(subscription)))) + ) + ), + Effect.scoped, + Effect.fork + ) + yield* Deferred.await(deferred) + yield* pipe( + values, + Effect.forEach((n) => PubSub.publish(pubsub, n)), + Effect.fork + ) + const result = yield* Fiber.join(subscriber) + deepStrictEqual(result, values) + })) + it.effect("sliding concurrent publishers and subscribers - one to many", () => + Effect.gen(function*() { + const values = Array.range(0, 64) + const deferred1 = yield* Deferred.make() + const deferred2 = yield* Deferred.make() + const pubsub = yield* PubSub.sliding(64) + const subscriber1 = yield* pipe( + PubSub.subscribe(pubsub), + Effect.flatMap((subscription) => + pipe( + Deferred.succeed(deferred1, void 0), + Effect.zipRight(pipe(values, Effect.forEach((_) => Queue.take(subscription)))) + ) + ), + Effect.scoped, + Effect.fork + ) + const subscriber2 = yield* pipe( + PubSub.subscribe(pubsub), + Effect.flatMap((subscription) => + pipe( + Deferred.succeed(deferred2, void 0), + Effect.zipRight(pipe(values, Effect.forEach((_) => Queue.take(subscription)))) + ) + ), + Effect.scoped, + Effect.fork + ) + yield* Deferred.await(deferred1) + yield* Deferred.await(deferred2) + yield* pipe( + values, + Effect.forEach((n) => PubSub.publish(pubsub, n)), + Effect.fork + ) + const result1 = yield* Fiber.join(subscriber1) + const result2 = yield* Fiber.join(subscriber2) + deepStrictEqual(result1, values) + deepStrictEqual(result2, values) + })) + it.effect("sliding concurrent publishers and subscribers - many to many", () => + Effect.gen(function*() { + const values = Array.range(1, 64) + const deferred1 = yield* Deferred.make() + const deferred2 = yield* Deferred.make() + const pubsub = yield* PubSub.sliding(64 * 2) + const subscriber1 = yield* pipe( + PubSub.subscribe(pubsub), + Effect.flatMap((subscription) => + pipe( + Deferred.succeed(deferred1, void 0), + Effect.zipRight(pipe( + values, + Array.appendAll(values), + Effect.forEach((_) => Queue.take(subscription)) + )) + ) + ), + Effect.scoped, + Effect.fork + ) + const subscriber2 = yield* pipe( + PubSub.subscribe(pubsub), + Effect.flatMap((subscription) => + pipe( + Deferred.succeed(deferred2, void 0), + Effect.zipRight(pipe( + values, + Array.appendAll(values), + Effect.forEach((_) => Queue.take(subscription)) + )) + ) + ), + Effect.scoped, + Effect.fork + ) + yield* Deferred.await(deferred1) + yield* Deferred.await(deferred2) + const fiber = yield* pipe( + values, + Effect.forEach((n) => PubSub.publish(pubsub, n)), + Effect.fork + ) + yield* pipe(values, Array.map((n) => -n), Effect.forEach((n) => PubSub.publish(pubsub, n)), Effect.fork) + const result1 = yield* Fiber.join(subscriber1) + const result2 = yield* Fiber.join(subscriber2) + yield* Fiber.join(fiber) + deepStrictEqual(pipe(result1, Array.filter((n) => n > 0)), values) + deepStrictEqual( + pipe(result1, Array.filter((n) => n < 0)), + pipe(values, Array.map((n) => -n)) + ) + deepStrictEqual(pipe(result2, Array.filter((n) => n > 0)), values) + deepStrictEqual( + pipe(result2, Array.filter((n) => n < 0)), + pipe(values, Array.map((n) => -n)) + ) + })) + it.effect("unbounded concurrent publishers and subscribers - one to one", () => + Effect.gen(function*() { + const values = Array.range(0, 64) + const deferred = yield* Deferred.make() + const pubsub = yield* PubSub.unbounded() + const subscriber = yield* pipe( + PubSub.subscribe(pubsub), + Effect.flatMap((subscription) => + pipe( + Deferred.succeed(deferred, void 0), + Effect.zipRight(pipe(values, Effect.forEach((_) => Queue.take(subscription)))) + ) + ), + Effect.scoped, + Effect.fork + ) + yield* Deferred.await(deferred) + yield* pipe( + values, + Effect.forEach((n) => PubSub.publish(pubsub, n)), + Effect.fork + ) + + const result = yield* Fiber.join(subscriber) + deepStrictEqual(result, values) + })) + it.effect("unbounded concurrent publishers and subscribers - one to many", () => + Effect.gen(function*() { + const values = Array.range(0, 64) + const deferred1 = yield* Deferred.make() + const deferred2 = yield* Deferred.make() + const pubsub = yield* PubSub.unbounded() + const subscriber1 = yield* pipe( + PubSub.subscribe(pubsub), + Effect.flatMap((subscription) => + pipe( + Deferred.succeed(deferred1, void 0), + Effect.zipRight(pipe(values, Effect.forEach((_) => Queue.take(subscription)))) + ) + ), + Effect.scoped, + Effect.fork + ) + const subscriber2 = yield* pipe( + PubSub.subscribe(pubsub), + Effect.flatMap((subscription) => + pipe( + Deferred.succeed(deferred2, void 0), + Effect.zipRight(pipe(values, Effect.forEach((_) => Queue.take(subscription)))) + ) + ), + Effect.scoped, + Effect.fork + ) + yield* Deferred.await(deferred1) + yield* Deferred.await(deferred2) + yield* pipe( + values, + Effect.forEach((n) => PubSub.publish(pubsub, n)), + Effect.fork + ) + const result1 = yield* Fiber.join(subscriber1) + const result2 = yield* Fiber.join(subscriber2) + deepStrictEqual(result1, values) + deepStrictEqual(result2, values) + })) + it.effect("unbounded concurrent publishers and subscribers - many to many", () => + Effect.gen(function*() { + const values = Array.range(1, 64) + const deferred1 = yield* Deferred.make() + const deferred2 = yield* Deferred.make() + const pubsub = yield* PubSub.unbounded() + const subscriber1 = yield* pipe( + PubSub.subscribe(pubsub), + Effect.flatMap((subscription) => + pipe( + Deferred.succeed(deferred1, void 0), + Effect.zipRight(pipe( + values, + Array.appendAll(values), + Effect.forEach((_) => Queue.take(subscription)) + )) + ) + ), + Effect.scoped, + Effect.fork + ) + + const subscriber2 = yield* pipe( + PubSub.subscribe(pubsub), + Effect.flatMap((subscription) => + pipe( + Deferred.succeed(deferred2, void 0), + Effect.zipRight(pipe( + values, + Array.appendAll(values), + Effect.forEach((_) => Queue.take(subscription)) + )) + ) + ), + Effect.scoped, + Effect.fork + ) + yield* Deferred.await(deferred1) + yield* Deferred.await(deferred2) + const fiber = yield* pipe( + values, + Effect.forEach((n) => PubSub.publish(pubsub, n)), + Effect.fork + ) + yield* pipe(values, Array.map((n) => -n), Effect.forEach((n) => PubSub.publish(pubsub, n)), Effect.fork) + const result1 = yield* Fiber.join(subscriber1) + const result2 = yield* Fiber.join(subscriber2) + yield* Fiber.join(fiber) + deepStrictEqual(Array.filter(result1, (n) => n > 0), values) + deepStrictEqual( + Array.filter(result1, (n) => n < 0), + Array.map(values, (n) => -n) + ) + deepStrictEqual(Array.filter(result2, (n) => n > 0), values) + deepStrictEqual( + Array.filter(result2, (n) => n < 0), + Array.map(values, (n) => -n) + ) + })) + it.effect("null values", () => { + const messages = [1, null] + return PubSub.unbounded().pipe( + Effect.flatMap((pubsub) => + Effect.scoped( + Effect.gen(function*() { + const dequeue1 = yield* PubSub.subscribe(pubsub) + const dequeue2 = yield* PubSub.subscribe(pubsub) + yield* PubSub.publishAll(pubsub, messages) + const takes1 = yield* Queue.takeAll(dequeue1) + const takes2 = yield* Queue.takeAll(dequeue2) + deepStrictEqual([...takes1], messages) + deepStrictEqual([...takes2], messages) + }) + ) + ) + ) + }) + + it.scoped("publish does not increase size while no subscribers", () => + Effect.gen(function*() { + const pubsub = yield* PubSub.dropping(2) + yield* PubSub.publish(pubsub, 1) + yield* PubSub.publish(pubsub, 2) + assertSome(pubsub.unsafeSize(), 0) + })) + + it.scoped("publishAll does not increase size while no subscribers", () => + Effect.gen(function*() { + const pubsub = yield* PubSub.dropping(2) + yield* PubSub.publishAll(pubsub, [1, 2]) + assertSome(pubsub.unsafeSize(), 0) + })) + + describe("replay", () => { + it.scoped("unbounded", () => + Effect.gen(function*() { + const messages = [1, 2, 3, 4, 5] + const pubsub = yield* PubSub.unbounded({ replay: 3 }) + yield* PubSub.publishAll(pubsub, messages) + const sub = yield* PubSub.subscribe(pubsub) + deepStrictEqual(Chunk.toReadonlyArray(yield* Queue.takeAll(sub)), [3, 4, 5]) + })) + + it.effect("unbounded takeUpTo", () => { + const messages = [1, 2, 3, 4, 5] + return PubSub.unbounded({ replay: 3 }).pipe( + Effect.flatMap((pubsub) => + Effect.scoped( + Effect.gen(function*() { + yield* PubSub.publishAll(pubsub, messages) + + const dequeue1 = yield* PubSub.subscribe(pubsub) + yield* PubSub.publish(pubsub, 6) + const dequeue2 = yield* PubSub.subscribe(pubsub) + + strictEqual(yield* Queue.size(dequeue1), 4) + strictEqual(yield* Queue.size(dequeue2), 3) + deepStrictEqual(Chunk.toReadonlyArray(yield* Queue.takeUpTo(dequeue1, 2)), [3, 4]) + deepStrictEqual(Chunk.toReadonlyArray(yield* Queue.takeUpTo(dequeue1, 2)), [5, 6]) + deepStrictEqual(Chunk.toReadonlyArray(yield* Queue.takeUpTo(dequeue2, 3)), [4, 5, 6]) + }) + ) + ) + ) + }) + + it.scoped("dropping", () => + Effect.gen(function*() { + const messages = [1, 2, 3, 4, 5] + const pubsub = yield* PubSub.dropping({ capacity: 2, replay: 3 }) + + yield* PubSub.publishAll(pubsub, messages) + const sub = yield* PubSub.subscribe(pubsub) + deepStrictEqual(Chunk.toReadonlyArray(yield* Queue.takeAll(sub)), [3, 4, 5]) + yield* PubSub.publishAll(pubsub, [6, 7]) + deepStrictEqual(Chunk.toReadonlyArray(yield* Queue.takeAll(sub)), [6, 7]) + + const sub2 = yield* PubSub.subscribe(pubsub) + deepStrictEqual(Chunk.toReadonlyArray(yield* Queue.takeAll(sub2)), [5, 6, 7]) + + yield* PubSub.publishAll(pubsub, [8, 9, 10, 11]) + deepStrictEqual(Chunk.toReadonlyArray(yield* Queue.takeAll(sub)), [8, 9]) + deepStrictEqual(Chunk.toReadonlyArray(yield* Queue.takeAll(sub2)), [8, 9]) + + const sub3 = yield* PubSub.subscribe(pubsub) + deepStrictEqual(Chunk.toReadonlyArray(yield* Queue.takeAll(sub3)), [7, 8, 9]) + })) + + it.scoped("sliding", () => + Effect.gen(function*() { + const messages = [1, 2, 3, 4, 5] + const pubsub = yield* PubSub.sliding({ capacity: 4, replay: 3 }) + + yield* PubSub.publishAll(pubsub, messages) + const sub = yield* PubSub.subscribe(pubsub) + deepStrictEqual(yield* Queue.take(sub), 3) + yield* PubSub.publishAll(pubsub, [6, 7, 8, 9, 10]) + deepStrictEqual(Chunk.toReadonlyArray(yield* Queue.takeAll(sub)), [5, 6, 7, 8, 9, 10]) + + const sub2 = yield* PubSub.subscribe(pubsub) + deepStrictEqual(Chunk.toReadonlyArray(yield* Queue.takeAll(sub2)), [8, 9, 10]) + + yield* PubSub.publishAll(pubsub, [11, 12, 13, 14, 15, 16]) + deepStrictEqual(Chunk.toReadonlyArray(yield* Queue.takeAll(sub)), [13, 14, 15, 16]) + deepStrictEqual(Chunk.toReadonlyArray(yield* Queue.takeAll(sub2)), [13, 14, 15, 16]) + + const sub3 = yield* PubSub.subscribe(pubsub) + deepStrictEqual(Chunk.toReadonlyArray(yield* Queue.takeAll(sub3)), [14, 15, 16]) + })) + }) +}) diff --git a/repos/effect/packages/effect/test/Queue.test.ts b/repos/effect/packages/effect/test/Queue.test.ts new file mode 100644 index 0000000..30d905c --- /dev/null +++ b/repos/effect/packages/effect/test/Queue.test.ts @@ -0,0 +1,779 @@ +import { describe, it } from "@effect/vitest" +import { + assertFalse, + assertLeft, + assertNone, + assertSome, + assertTrue, + deepStrictEqual, + strictEqual +} from "@effect/vitest/utils" +import { Array, Cause, Chunk, Deferred, Effect, Exit, Fiber, identity, pipe, Queue, Ref } from "effect" + +export const waitForValue = (ref: Effect.Effect, value: A): Effect.Effect => { + return ref.pipe(Effect.zipLeft(Effect.yieldNow()), Effect.repeat({ until: (a) => value === a })) +} + +export const waitForSize = (queue: Queue.Queue, size: number): Effect.Effect => { + return waitForValue(Queue.size(queue), size) +} + +describe("Queue", () => { + it.effect("bounded - offerAll returns true when there is enough space", () => + Effect.gen(function*() { + const queue = yield* Queue.bounded(5) + const result = yield* Queue.offerAll(queue, [1, 2, 3]) + assertTrue(result) + })) + it.effect("dropping - with offerAll", () => + Effect.gen(function*() { + const queue = yield* Queue.dropping(4) + const result1 = yield* Queue.offerAll(queue, [1, 2, 3, 4, 5]) + const result2 = yield* Queue.takeAll(queue) + assertFalse(result1) + deepStrictEqual(Chunk.toReadonlyArray(result2), [1, 2, 3, 4]) + })) + it.effect("dropping - with offerAll, check offer returns false", () => + Effect.gen(function*() { + const queue = yield* Queue.dropping(2) + const result1 = yield* Queue.offerAll(queue, [1, 2, 3, 4, 5, 6]) + const result2 = yield* Queue.takeAll(queue) + assertFalse(result1) + deepStrictEqual(Chunk.toReadonlyArray(result2), [1, 2]) + })) + it.effect("dropping - with offerAll, check ordering", () => + Effect.gen(function*() { + const queue = yield* Queue.dropping(128) + const result1 = yield* Queue.offerAll(queue, Array.makeBy(256, (i) => i + 1)) + const result2 = yield* Queue.takeAll(queue) + assertFalse(result1) + deepStrictEqual(Chunk.toReadonlyArray(result2), Array.makeBy(128, (i) => i + 1)) + })) + it.effect("dropping - with pending taker", () => + Effect.gen(function*() { + const queue = yield* Queue.dropping(2) + const fiber = yield* Effect.fork(Queue.take(queue)) + yield* waitForSize(queue, -1) + const result1 = yield* Queue.offerAll(queue, [1, 2, 3, 4]) + const result2 = yield* Fiber.join(fiber) + assertFalse(result1) + strictEqual(result2, 1) + })) + it.effect("sliding - with offer", () => + Effect.gen(function*() { + const queue = yield* Queue.sliding(2) + yield* Queue.offer(queue, 1) + const result1 = yield* Queue.offer(queue, 2) + const result2 = yield* Queue.offer(queue, 3) + const result3 = yield* Queue.takeAll(queue) + assertTrue(result1) + assertTrue(result2) + deepStrictEqual(Chunk.toReadonlyArray(result3), [2, 3]) + })) + it.effect("sliding - with offerAll", () => + Effect.gen(function*() { + const queue = yield* Queue.sliding(2) + const result1 = yield* Queue.offerAll(queue, [1, 2, 3]) + const result2 = yield* Queue.size(queue) + assertTrue(result1) + strictEqual(result2, 2) + })) + it.effect("sliding - with enough capacity", () => + Effect.gen(function*() { + const queue = yield* Queue.sliding(100) + yield* Queue.offer(queue, 1) + yield* Queue.offer(queue, 2) + yield* Queue.offer(queue, 3) + const result = yield* Queue.takeAll(queue) + deepStrictEqual(Chunk.toReadonlyArray(result), [1, 2, 3]) + })) + it.effect("sliding - with offerAll and takeAll", () => + Effect.gen(function*() { + const queue = yield* Queue.sliding(2) + const result1 = yield* Queue.offerAll(queue, [1, 2, 3, 4, 5, 6]) + const result2 = yield* Queue.takeAll(queue) + assertTrue(result1) + deepStrictEqual(Chunk.toReadonlyArray(result2), [5, 6]) + })) + it.effect("sliding - with pending taker", () => + Effect.gen(function*() { + const queue = yield* Queue.sliding(2) + yield* Effect.fork(Queue.take(queue)) + yield* waitForSize(queue, -1) + const result1 = yield* Queue.offerAll(queue, [1, 2, 3, 4]) + const result2 = yield* Queue.take(queue) + assertTrue(result1) + strictEqual(result2, 3) + })) + it.effect("sliding - check offerAll returns true", () => + Effect.gen(function*() { + const queue = yield* Queue.sliding(5) + const result = yield* Queue.offerAll(queue, [1, 2, 3]) + assertTrue(result) + })) + it.effect("awaitShutdown - once", () => + Effect.gen(function*() { + const queue = yield* Queue.bounded(3) + const deferred = yield* Deferred.make() + yield* pipe(Queue.awaitShutdown(queue), Effect.zipRight(Deferred.succeed(deferred, true)), Effect.fork) + yield* Queue.shutdown(queue) + const result = yield* Deferred.await(deferred) + assertTrue(result) + })) + it.effect("awaitShutdown - multiple", () => + Effect.gen(function*() { + const queue = yield* Queue.bounded(3) + const deferred1 = yield* Deferred.make() + const deferred2 = yield* Deferred.make() + yield* pipe(Queue.awaitShutdown(queue), Effect.zipRight(Deferred.succeed(deferred1, true)), Effect.fork) + yield* pipe(Queue.awaitShutdown(queue), Effect.zipRight(Deferred.succeed(deferred2, true)), Effect.fork) + yield* Queue.shutdown(queue) + const result1 = yield* Deferred.await(deferred1) + const result2 = yield* Deferred.await(deferred2) + assertTrue(result1) + assertTrue(result2) + })) + it.effect("offers are suspended by back pressure", () => + Effect.gen(function*() { + const queue = yield* Queue.bounded(10) + const ref = yield* Ref.make(true) + yield* pipe(queue.offer(1), Effect.repeatN(9)) + const fiber = yield* pipe(Queue.offer(queue, 2), Effect.zipRight(Ref.set(ref, false)), Effect.fork) + yield* waitForSize(queue, 11) + const result = yield* Ref.get(ref) + yield* Fiber.interrupt(fiber) + assertTrue(result) + })) + it.effect("back pressured offers are retrieved", () => + Effect.gen(function*() { + const queue = yield* Queue.bounded(10) + const ref = yield* Ref.make>([]) + const values = Array.makeBy(10, (i) => i + 1) + const fiber = yield* Effect.forkAll(values.map((n) => Queue.offer(queue, n))) + yield* waitForSize(queue, 10) + yield* pipe( + Queue.take(queue), + Effect.flatMap((n) => Ref.update(ref, (values) => [...values, n])), + Effect.repeatN(9) + ) + const result = yield* Ref.get(ref) + yield* Fiber.join(fiber) + deepStrictEqual(result, values) + })) + it.effect("back-pressured offer completes after take", () => + Effect.gen(function*() { + const queue = yield* Queue.bounded(2) + yield* Queue.offerAll(queue, [1, 2]) + const fiber = yield* pipe(Queue.offer(queue, 3), Effect.fork) + yield* waitForSize(queue, 3) + const result1 = yield* Queue.take(queue) + const result2 = yield* Queue.take(queue) + yield* Fiber.join(fiber) + strictEqual(result1, 1) + strictEqual(result2, 2) + })) + it.effect("back-pressured offer completes after takeAll", () => + Effect.gen(function*() { + const queue = yield* Queue.bounded(2) + yield* Queue.offerAll(queue, [1, 2]) + const fiber = yield* pipe(Queue.offer(queue, 3), Effect.fork) + yield* waitForSize(queue, 3) + const result = yield* Queue.takeAll(queue) + yield* Fiber.join(fiber) + deepStrictEqual(Chunk.toReadonlyArray(result), [1, 2]) + })) + it.effect("back-pressured offer completes after takeUpTo", () => + Effect.gen(function*() { + const queue = yield* Queue.bounded(2) + yield* Queue.offerAll(queue, [1, 2]) + const fiber = yield* pipe(Queue.offer(queue, 3), Effect.fork) + yield* waitForSize(queue, 3) + const result = yield* Queue.takeUpTo(queue, 2) + yield* Fiber.join(fiber) + deepStrictEqual(Chunk.toReadonlyArray(result), [1, 2]) + })) + it.effect("back-pressured offerAll completes after takeAll", () => + Effect.gen(function*() { + const queue = yield* Queue.bounded(2) + yield* Queue.offerAll(queue, [1, 2]) + const fiber = yield* pipe(Queue.offerAll(queue, [3, 4, 5]), Effect.fork) + yield* waitForSize(queue, 5) + const result1 = yield* Queue.takeAll(queue) + const result2 = yield* Queue.takeAll(queue) + const result3 = yield* Queue.takeAll(queue) + yield* Fiber.join(fiber) + deepStrictEqual(Chunk.toReadonlyArray(result1), [1, 2]) + deepStrictEqual(Chunk.toReadonlyArray(result2), [3, 4]) + deepStrictEqual(Chunk.toReadonlyArray(result3), [5]) + })) + it.effect("take interruption", () => + Effect.gen(function*() { + const queue = yield* Queue.bounded(100) + const fiber = yield* Effect.fork(Queue.take(queue)) + yield* waitForSize(queue, -1) + yield* Fiber.interrupt(fiber) + const result = yield* Queue.size(queue) + strictEqual(result, 0) + })) + it.effect("offer interruption", () => + Effect.gen(function*() { + const queue = yield* Queue.bounded(2) + yield* Queue.offer(queue, 1) + yield* Queue.offer(queue, 1) + const fiber = yield* pipe(Queue.offer(queue, 1), Effect.fork) + yield* waitForSize(queue, 3) + yield* Fiber.interrupt(fiber) + const result = yield* Queue.size(queue) + strictEqual(result, 2) + })) + it.effect("offerAll with takeAll", () => + Effect.gen(function*() { + const queue = yield* Queue.bounded(10) + const values = Array.range(1, 10) + yield* Queue.offerAll(queue, values) + yield* waitForSize(queue, 10) + const result = yield* Queue.takeAll(queue) + deepStrictEqual(Chunk.toReadonlyArray(result), Array.range(1, 10)) + })) + it.effect("offerAll with takeAll and back pressure", () => + Effect.gen(function*() { + const queue = yield* Queue.bounded(2) + const values = Array.range(1, 3) + const fiber = yield* pipe(Queue.offerAll(queue, values), Effect.fork) + const size = yield* waitForSize(queue, 3) + const result = yield* Queue.takeAll(queue) + yield* Fiber.interrupt(fiber) + strictEqual(size, 3) + deepStrictEqual(Chunk.toReadonlyArray(result), [1, 2]) + })) + it.effect("offerAll with takeAll and back pressure + interruption", () => + Effect.gen(function*() { + const queue = yield* Queue.bounded(2) + const values1 = Array.range(1, 2) + const values2 = Array.range(3, 4) + yield* Queue.offerAll(queue, values1) + const fiber = yield* pipe(Queue.offerAll(queue, values2), Effect.fork) + yield* waitForSize(queue, 4) + yield* Fiber.interrupt(fiber) + const result1 = yield* Queue.takeAll(queue) + const result2 = yield* Queue.takeAll(queue) + deepStrictEqual(Chunk.toReadonlyArray(result1), values1) + assertTrue(Chunk.isEmpty(result2)) + })) + it.effect("offerAll with takeAll and back pressure, check ordering", () => + Effect.gen(function*() { + const queue = yield* Queue.bounded(64) + const fiber = yield* pipe(Queue.offerAll(queue, Array.makeBy(128, (i) => i + 1)), Effect.fork) + yield* waitForSize(queue, 128) + const result = yield* Queue.takeAll(queue) + yield* Fiber.interrupt(fiber) + deepStrictEqual(Chunk.toReadonlyArray(result), Array.range(1, 64)) + })) + it.effect("offerAll with pending takers", () => + Effect.gen(function*() { + const queue = yield* Queue.bounded(50) + const takers = yield* Effect.forkAll(Array.makeBy(100, () => Queue.take(queue))) + yield* waitForSize(queue, -100) + yield* queue.offerAll(Array.makeBy(100, (i) => i + 1)) + const result = yield* Fiber.join(takers) + const size = yield* Queue.size(queue) + strictEqual(size, 0) + deepStrictEqual(result, Array.range(1, 100)) + })) + it.effect("offerAll with pending takers, check ordering", () => + Effect.gen(function*() { + const queue = yield* Queue.bounded(256) + const takers = yield* Effect.forkAll(Array.makeBy(64, () => Queue.take(queue))) + yield* waitForSize(queue, -64) + yield* Queue.offerAll(queue, Array.makeBy(128, (i) => i + 1)) + const result = yield* Fiber.join(takers) + const size = yield* Queue.size(queue) + strictEqual(size, 64) + deepStrictEqual(result, Array.range(1, 64)) + })) + it.effect("offerAll with pending takers, check ordering of taker resolution", () => + Effect.gen(function*() { + const queue = yield* Queue.bounded(200) + const takers = yield* Effect.forkAll(Array.makeBy(100, () => Queue.take(queue))) + yield* waitForSize(queue, -100) + const fiber = yield* Effect.forkAll(Array.makeBy(100, () => Queue.take(queue))) + yield* waitForSize(queue, -200) + yield* Queue.offerAll(queue, Array.makeBy(100, (i) => i + 1)) + const result = yield* Fiber.join(takers) + const size = yield* Queue.size(queue) + yield* Fiber.interrupt(fiber) + strictEqual(size, -100) + deepStrictEqual(result, Array.range(1, 100)) + })) + it.effect("offerAll with take and back pressure", () => + Effect.gen(function*() { + const queue = yield* Queue.bounded(2) + yield* pipe(Queue.offerAll(queue, [1, 2, 3]), Effect.fork) + yield* waitForSize(queue, 3) + const result1 = yield* Queue.take(queue) + const result2 = yield* Queue.take(queue) + const result3 = yield* Queue.take(queue) + strictEqual(result1, 1) + strictEqual(result2, 2) + strictEqual(result3, 3) + })) + it.effect("offerAll multiple with back pressure", () => + Effect.gen(function*() { + const queue = yield* Queue.bounded(2) + yield* pipe(Queue.offerAll(queue, [1, 2, 3]), Effect.fork) + yield* waitForSize(queue, 3) + yield* pipe(Queue.offerAll(queue, [4, 5]), Effect.fork) + yield* waitForSize(queue, 5) + const result1 = yield* Queue.take(queue) + const result2 = yield* Queue.take(queue) + const result3 = yield* Queue.take(queue) + const result4 = yield* Queue.take(queue) + const result5 = yield* Queue.take(queue) + strictEqual(result1, 1) + strictEqual(result2, 2) + strictEqual(result3, 3) + strictEqual(result4, 4) + strictEqual(result5, 5) + })) + it.effect("offerAll with takeAll, check ordering", () => + Effect.gen(function*() { + const queue = yield* Queue.bounded(1000) + yield* Queue.offer(queue, 1) + yield* Queue.offerAll(queue, Array.range(2, 1000)) + yield* waitForSize(queue, 1000) + const result = yield* Queue.takeAll(queue) + deepStrictEqual(Chunk.toReadonlyArray(result), Array.range(1, 1000)) + })) + it.effect("offerAll combination of offer, offerAll, take, takeAll", () => + Effect.gen(function*() { + const queue = yield* Queue.bounded(32) + yield* Queue.offer(queue, 1) + yield* Queue.offer(queue, 2) + yield* pipe(Queue.offerAll(queue, Array.range(3, 35)), Effect.fork) + yield* waitForSize(queue, 35) + const result1 = yield* Queue.takeAll(queue) + const result2 = yield* Queue.take(queue) + const result3 = yield* Queue.take(queue) + const result4 = yield* Queue.take(queue) + deepStrictEqual(Chunk.toReadonlyArray(result1), Array.range(1, 32)) + strictEqual(result2, 33) + strictEqual(result3, 34) + strictEqual(result4, 35) + })) + it.effect("parallel takes and sequential offers", () => + Effect.gen(function*() { + const queue = yield* Queue.bounded(100) + const fiber = yield* Effect.forkAll(Array.makeBy(10, () => Queue.take(queue))) + yield* Array.makeBy(10, (i) => Queue.offer(queue, i + 1)) + .reduce((acc, curr) => pipe(acc, Effect.zipRight(curr)), Effect.succeed(false)) + const result = yield* Fiber.join(fiber) + deepStrictEqual(result, Array.range(1, 10)) + })) + it.effect("parallel offers and sequential takes", () => + Effect.gen(function*() { + const queue = yield* Queue.bounded(10) + const fiber = yield* Effect.forkAll(Array.makeBy(10, (i) => Queue.offer(queue, i + 1))) + yield* waitForSize(queue, 10) + const ref = yield* Ref.make>([]) + yield* pipe( + Queue.take(queue), + Effect.flatMap((n) => Ref.update(ref, (ns) => [...ns, n])), + Effect.repeatN(9) + ) + const result = yield* Ref.get(ref) + yield* Fiber.join(fiber) + deepStrictEqual(result, Array.makeBy(10, (i) => i + 1)) + })) + it.effect("sequential offer and take", () => + Effect.gen(function*() { + const queue = yield* Queue.bounded(100) + const offer1 = yield* Queue.offer(queue, 10) + const result1 = yield* Queue.take(queue) + const offer2 = yield* Queue.offer(queue, 20) + const result2 = yield* Queue.take(queue) + assertTrue(offer1) + strictEqual(result1, 10) + assertTrue(offer2) + strictEqual(result2, 20) + })) + it.effect("sequential take and offer", () => + Effect.gen(function*() { + const queue = yield* Queue.bounded(100) + const fiber = yield* pipe(Queue.take(queue), Effect.zipWith(Queue.take(queue), (a, b) => a + b), Effect.fork) + yield* pipe(Queue.offer(queue, "don't "), Effect.zipRight(Queue.offer(queue, "give up :D"))) + const result = yield* Fiber.join(fiber) + strictEqual(result, "don't give up :D") + })) + it.effect("poll on empty queue", () => + Effect.gen(function*() { + const queue = yield* Queue.bounded(5) + const result = yield* Queue.poll(queue) + assertNone(result) + })) + it.effect("poll on queue just emptied", () => + Effect.gen(function*() { + const queue = yield* Queue.bounded(5) + yield* Queue.offerAll(queue, [1, 2, 3, 4]) + yield* Queue.takeAll(queue) + const result = yield* Queue.poll(queue) + assertNone(result) + })) + it.effect("multiple polls", () => + Effect.gen(function*() { + const queue = yield* Queue.bounded(5) + yield* Queue.offerAll(queue, [1, 2]) + const result1 = yield* Queue.poll(queue) + const result2 = yield* Queue.poll(queue) + const result3 = yield* Queue.poll(queue) + const result4 = yield* Queue.poll(queue) + assertSome(result1, 1) + assertSome(result2, 2) + assertNone(result3) + assertNone(result4) + })) + it.effect("shutdown with take fiber", () => + Effect.gen(function*() { + const fiberId = yield* Effect.fiberId + const queue = yield* Queue.bounded(3) + const fiber = yield* Effect.fork(Queue.take(queue)) + yield* waitForSize(queue, -1) + yield* Queue.shutdown(queue) + const result = yield* Effect.either(Effect.sandbox(Fiber.join(fiber))) + assertLeft(result, Cause.interrupt(fiberId)) + })) + it.effect("shutdown with offer fiber", () => + Effect.gen(function*() { + const fiberId = yield* Effect.fiberId + const queue = yield* Queue.bounded(2) + yield* Queue.offer(queue, 1) + yield* Queue.offer(queue, 1) + const fiber = yield* pipe(Queue.offer(queue, 1), Effect.fork) + yield* waitForSize(queue, 3) + yield* Queue.shutdown(queue) + const result = yield* Effect.either(Effect.sandbox(Fiber.join(fiber))) + assertLeft(result, Cause.interrupt(fiberId)) + })) + it.effect("shutdown with offer", () => + Effect.gen(function*() { + const fiberId = yield* Effect.fiberId + const queue = yield* Queue.bounded(1) + yield* Queue.shutdown(queue) + const result = yield* pipe(Queue.offer(queue, 1), Effect.sandbox, Effect.either) + assertLeft(result, Cause.interrupt(fiberId)) + })) + it.effect("shutdown with take", () => + Effect.gen(function*() { + const fiberId = yield* Effect.fiberId + const queue = yield* Queue.bounded(1) + yield* Queue.shutdown(queue) + const result = yield* pipe(Queue.take(queue), Effect.sandbox, Effect.either) + assertLeft(result, Cause.interrupt(fiberId)) + })) + it.effect("shutdown with takeAll", () => + Effect.gen(function*() { + const fiberId = yield* Effect.fiberId + const queue = yield* Queue.bounded(1) + yield* Queue.shutdown(queue) + const result = yield* pipe(Queue.takeAll(queue), Effect.sandbox, Effect.either) + assertLeft(result, Cause.interrupt(fiberId)) + })) + it.effect("shutdown with takeUpTo", () => + Effect.gen(function*() { + const fiberId = yield* Effect.fiberId + const queue = yield* Queue.bounded(1) + yield* Queue.shutdown(queue) + const result = yield* pipe(Queue.takeUpTo(queue, 1), Effect.sandbox, Effect.either) + assertLeft(result, Cause.interrupt(fiberId)) + })) + it.effect("shutdown with size", () => + Effect.gen(function*() { + const fiberId = yield* Effect.fiberId + const queue = yield* Queue.bounded(1) + yield* Queue.shutdown(queue) + const result = yield* pipe(Queue.size(queue), Effect.sandbox, Effect.either) + assertLeft(result, Cause.interrupt(fiberId)) + })) + it.effect("shutdown race condition with offer", () => + Effect.gen(function*() { + const queue = yield* Queue.bounded(2) + const fiber = yield* pipe(Queue.offer(queue, 1), Effect.forever, Effect.fork) + yield* Queue.shutdown(queue) + const result = yield* Fiber.await(fiber) + assertTrue(Exit.isFailure(result)) + })) + it.effect("shutdown race condition with take", () => + Effect.gen(function*() { + const queue = yield* Queue.bounded(2) + yield* Queue.offer(queue, 1) + yield* Queue.offer(queue, 1) + const fiber = yield* pipe(Queue.take(queue), Effect.forever, Effect.fork) + yield* Queue.shutdown(queue) + const result = yield* Fiber.await(fiber) + assertTrue(Exit.isFailure(result)) + })) + it.effect("isShutdown indicates shutdown status", () => + Effect.gen(function*() { + const queue = yield* Queue.bounded(5) + const result1 = yield* Queue.isShutdown(queue) + yield* Queue.offer(queue, 1) + const result2 = yield* Queue.isShutdown(queue) + yield* Queue.takeAll(queue) + const result3 = yield* Queue.isShutdown(queue) + yield* Queue.shutdown(queue) + const result4 = yield* Queue.isShutdown(queue) + assertFalse(result1) + assertFalse(result2) + assertFalse(result3) + assertTrue(result4) + })) + it.effect("takeAll returns all values from a non-empty queue", () => + Effect.gen(function*() { + const queue = yield* Queue.unbounded() + yield* Queue.offer(queue, 1) + yield* Queue.offer(queue, 2) + yield* Queue.offer(queue, 3) + const result = yield* Queue.takeAll(queue) + deepStrictEqual(Chunk.toReadonlyArray(result), Array.range(1, 3)) + })) + it.effect("elements can be enqueued syncroniously when there is space", () => + Effect.gen(function*() { + const queue = yield* Queue.unbounded() + Queue.unsafeOffer(queue, 1) + Queue.unsafeOffer(queue, 2) + Queue.unsafeOffer(queue, 3) + const result = yield* Queue.takeAll(queue) + deepStrictEqual(Chunk.toReadonlyArray(result), Array.range(1, 3)) + })) + it.effect("takeAll returns all values from an empty queue", () => + Effect.gen(function*() { + const queue = yield* Queue.unbounded() + const result1 = yield* Queue.takeAll(queue) + yield* Queue.offer(queue, 1) + yield* Queue.take(queue) + const result2 = yield* Queue.takeAll(queue) + assertTrue(Chunk.isEmpty(result1)) + assertTrue(Chunk.isEmpty(result2)) + })) + it.effect("takeAll does not return more than the queue size", () => + Effect.gen(function*() { + const queue = yield* Queue.bounded(4) + yield* [1, 2, 3, 4] + .map((n) => Queue.offer(queue, n)) + .reduce((acc, curr) => pipe(acc, Effect.zipRight(curr)), Effect.succeed(false)) + yield* pipe(Queue.offer(queue, 5), Effect.fork) + yield* waitForSize(queue, 5) + const result1 = yield* Queue.takeAll(queue) + const result2 = yield* Queue.take(queue) + deepStrictEqual(Chunk.toReadonlyArray(result1), Array.range(1, 4)) + strictEqual(result2, 5) + })) + it.effect("takeBetween returns immediately if there is enough elements", () => + Effect.gen(function*() { + const queue = yield* Queue.bounded(100) + yield* Queue.offer(queue, 1) + yield* Queue.offer(queue, 2) + yield* Queue.offer(queue, 3) + const result = yield* Queue.takeBetween(queue, 2, 5) + deepStrictEqual(Chunk.toReadonlyArray(result), Array.range(1, 3)) + })) + it.effect("takeBetween returns an empty list if boundaries are inverted", () => + Effect.gen(function*() { + const queue = yield* Queue.bounded(100) + yield* Queue.offer(queue, 1) + yield* Queue.offer(queue, 2) + yield* Queue.offer(queue, 3) + const result = yield* Queue.takeBetween(queue, 5, 2) + assertTrue(Chunk.isEmpty(result)) + })) + it.effect("takeBetween returns an empty list if boundaries are negative", () => + Effect.gen(function*() { + const queue = yield* Queue.bounded(100) + yield* Queue.offer(queue, 1) + yield* Queue.offer(queue, 2) + yield* Queue.offer(queue, 3) + const result = yield* Queue.takeBetween(queue, -5, -2) + assertTrue(Chunk.isEmpty(result)) + })) + it.effect("takeBetween blocks until a required minimum of elements is collected", () => + Effect.gen(function*() { + const queue = yield* Queue.bounded(100) + const updater = pipe(Queue.offer(queue, 10), Effect.forever) + const getter = Queue.takeBetween(queue, 5, 10) + const result = yield* pipe(getter, Effect.race(updater)) + assertTrue(result.length >= 5) + })) + it.effect("takeBetween returns elements in the correct order", () => + Effect.gen(function*() { + const values = [-10, -7, -4, -1, 5, 10] + const queue = yield* Queue.bounded(100) + const fiber = yield* pipe(values, Effect.forEach((n) => Queue.offer(queue, n)), Effect.fork) + const result = yield* Queue.takeBetween(queue, values.length, values.length) + yield* Fiber.interrupt(fiber) + deepStrictEqual(Array.fromIterable(result), values) + })) + it.effect("takeN returns immediately if there is enough elements", () => + Effect.gen(function*() { + const queue = yield* Queue.bounded(100) + yield* Queue.offerAll(queue, [1, 2, 3, 4, 5]) + const result = yield* Queue.takeN(queue, 3) + deepStrictEqual(Chunk.toReadonlyArray(result), Array.range(1, 3)) + })) + it.effect("takeN returns an empty list if a negative number or zero is specified", () => + Effect.gen(function*() { + const queue = yield* Queue.bounded(100) + yield* Queue.offerAll(queue, [1, 2, 3]) + const result1 = yield* Queue.takeN(queue, -3) + const result2 = yield* Queue.takeN(queue, 0) + assertTrue(Chunk.isEmpty(result1)) + assertTrue(Chunk.isEmpty(result2)) + })) + it.effect("takeN blocks until the required number of elements is available", () => + Effect.gen(function*() { + const queue = yield* Queue.bounded(100) + const updater = pipe(Queue.offer(queue, 10), Effect.forever) + const getter = Queue.takeN(queue, 5) + const result = yield* pipe(getter, Effect.race(updater)) + strictEqual(result.length, 5) + })) + it.effect("should return the specified number of elements from a non-empty queue", () => + Effect.gen(function*() { + const queue = yield* Queue.bounded(100) + yield* Queue.offer(queue, 1) + yield* Queue.offer(queue, 2) + const result = yield* Queue.takeUpTo(queue, 2) + deepStrictEqual(Chunk.toReadonlyArray(result), Array.range(1, 2)) + })) + it.effect("should return an empty collection from an empty queue", () => + Effect.gen(function*() { + const queue = yield* Queue.bounded(100) + const result = yield* Queue.takeUpTo(queue, 2) + assertTrue(Chunk.isEmpty(result)) + })) + it.effect("should handle an empty queue with max higher than queue size", () => + Effect.gen(function*() { + const queue = yield* Queue.bounded(100) + const result = yield* Queue.takeUpTo(queue, 101) + assertTrue(Chunk.isEmpty(result)) + })) + it.effect("should leave behind elements if necessary", () => + Effect.gen(function*() { + const queue = yield* Queue.bounded(100) + yield* Queue.offer(queue, 1) + yield* Queue.offer(queue, 2) + yield* Queue.offer(queue, 3) + yield* Queue.offer(queue, 4) + const result = yield* Queue.takeUpTo(queue, 2) + deepStrictEqual(Chunk.toReadonlyArray(result), Array.range(1, 2)) + })) + it.effect("should handle not enough items", () => + Effect.gen(function*() { + const queue = yield* Queue.bounded(100) + yield* Queue.offer(queue, 1) + yield* Queue.offer(queue, 2) + yield* Queue.offer(queue, 3) + yield* Queue.offer(queue, 4) + const result = yield* Queue.takeUpTo(queue, 10) + deepStrictEqual(Chunk.toReadonlyArray(result), Array.range(1, 4)) + })) + it.effect("should handle taking up to 0 items", () => + Effect.gen(function*() { + const queue = yield* Queue.bounded(100) + yield* Queue.offer(queue, 1) + yield* Queue.offer(queue, 2) + yield* Queue.offer(queue, 3) + yield* Queue.offer(queue, 4) + const result = yield* Queue.takeUpTo(queue, 0) + assertTrue(Chunk.isEmpty(result)) + })) + it.effect("should handle taking up to -1 items", () => + Effect.gen(function*() { + const queue = yield* Queue.bounded(100) + yield* Queue.offer(queue, 1) + yield* Queue.offer(queue, 2) + yield* Queue.offer(queue, 3) + yield* Queue.offer(queue, 4) + const result = yield* Queue.takeUpTo(queue, -1) + assertTrue(Chunk.isEmpty(result)) + })) + it.effect("should handle taking up to Number.POSITIVE_INFINITY items", () => + Effect.gen(function*() { + const queue = yield* Queue.bounded(100) + yield* Queue.offer(queue, 1) + const result = yield* Queue.takeUpTo(queue, Number.POSITIVE_INFINITY) + deepStrictEqual(Chunk.toReadonlyArray(result), [1]) + })) + it.effect("multiple take up to calls", () => + Effect.gen(function*() { + const queue = yield* Queue.bounded(100) + yield* Queue.offer(queue, 1) + yield* Queue.offer(queue, 2) + const result1 = yield* Queue.takeUpTo(queue, 2) + yield* Queue.offer(queue, 3) + yield* Queue.offer(queue, 4) + const result2 = yield* Queue.takeUpTo(queue, 2) + deepStrictEqual(Chunk.toReadonlyArray(result1), Array.range(1, 2)) + deepStrictEqual(Chunk.toReadonlyArray(result2), Array.range(3, 4)) + })) + it.effect("consecutive take up to calls", () => + Effect.gen(function*() { + const queue = yield* Queue.bounded(100) + yield* Queue.offer(queue, 1) + yield* Queue.offer(queue, 2) + yield* Queue.offer(queue, 3) + yield* Queue.offer(queue, 4) + const result1 = yield* Queue.takeUpTo(queue, 2) + const result2 = yield* Queue.takeUpTo(queue, 2) + deepStrictEqual(Chunk.toReadonlyArray(result1), Array.range(1, 2)) + deepStrictEqual(Chunk.toReadonlyArray(result2), Array.range(3, 4)) + })) + it.effect("does not return back-pressured offers", () => + Effect.gen(function*() { + const queue = yield* Queue.bounded(4) + yield* [1, 2, 3, 4] + .map((n) => Queue.offer(queue, n)) + .reduce((acc, curr) => pipe(acc, Effect.zipRight(curr)), Effect.succeed(false)) + const fiber = yield* pipe(Queue.offer(queue, 5), Effect.fork) + yield* waitForSize(queue, 5) + const result = yield* Queue.takeUpTo(queue, 5) + yield* Fiber.interrupt(fiber) + deepStrictEqual(Chunk.toReadonlyArray(result), Array.range(1, 4)) + })) + it.effect("rts - handles falsy values", () => + Effect.gen(function*() { + const queue = yield* Queue.unbounded() + yield* Queue.offer(queue, 0) + const result = yield* Queue.take(queue) + strictEqual(result, 0) + })) + it.effect("rts - queue is ordered", () => + Effect.gen(function*() { + const queue = yield* Queue.unbounded() + yield* Queue.offer(queue, 1) + yield* Queue.offer(queue, 2) + yield* Queue.offer(queue, 3) + const result1 = yield* Queue.take(queue) + const result2 = yield* Queue.take(queue) + const result3 = yield* Queue.take(queue) + strictEqual(result1, 1) + strictEqual(result2, 2) + strictEqual(result3, 3) + })) + it.effect( + ".pipe", + () => + Effect.gen(function*() { + const queue = yield* Queue.unbounded() + strictEqual(queue.pipe(identity), queue) + }) + ) + it.effect( + "is subtype of Effect", + () => + Effect.gen(function*() { + const queue = yield* Queue.unbounded() + yield* Queue.offer(queue, 1) + const result1 = yield* queue + strictEqual(result1, 1) + }) + ) +}) diff --git a/repos/effect/packages/effect/test/Random.test.ts b/repos/effect/packages/effect/test/Random.test.ts new file mode 100644 index 0000000..d93e2da --- /dev/null +++ b/repos/effect/packages/effect/test/Random.test.ts @@ -0,0 +1,130 @@ +import { describe, it } from "@effect/vitest" +import { assertTrue, deepStrictEqual, strictEqual } from "@effect/vitest/utils" +import { Array, Cause, Chunk, Data, Effect, Random } from "effect" + +describe("Random", () => { + it.effect("integer is correctly distributed", () => + Effect.gen(function*() { + const tenYearsMillis = 10 * 365 * 24 * 60 * 60 * 1000 + let lastRandom = 0 + while (lastRandom < tenYearsMillis / 2) { + lastRandom = yield* Random.nextIntBetween(0, tenYearsMillis) + } + assertTrue(lastRandom >= tenYearsMillis / 2) + })) + it.effect("shuffle", () => + Effect.gen(function*() { + const start = Array.range(0, 100) + const end = yield* Random.shuffle(start) + assertTrue(Chunk.every(end, (n) => n !== undefined)) + deepStrictEqual(start.sort(), Array.fromIterable(end).sort()) + }).pipe(Effect.repeatN(100))) + + it.effect("make", () => + Effect.gen(function*() { + const random0 = Random.make("foo") + const random1 = Random.make("foo") + const random2 = Random.make(Data.struct({ foo: "bar" })) + const random3 = Random.make(Data.struct({ foo: "bar" })) + const n0 = yield* random0.next + const n1 = yield* random1.next + const n2 = yield* random2.next + const n3 = yield* random3.next + strictEqual(n0, n1) + strictEqual(n2, n3) + assertTrue(n0 !== n2) + })) + + it.live("choice", () => + Effect.gen(function*() { + deepStrictEqual( + yield* Random.choice([]).pipe(Effect.flip), + new Cause.NoSuchElementException("Cannot select a random element from an empty array") + ) + strictEqual(yield* Random.choice([1]), 1) + + const randomItems = yield* Random.choice([1, 2, 3]).pipe(Array.replicate(100), Effect.all) + strictEqual(Array.intersection(randomItems, [1, 2, 3]).length, randomItems.length) + + assertTrue([1, 2, 3].includes(yield* Random.choice(Chunk.fromIterable([1, 2, 3])))) + })) + + describe("fixed", () => { + it.effect("cycles through numeric values", () => + Effect.gen(function*() { + strictEqual(yield* Random.next, 0.2) + strictEqual(yield* Random.next, 0.5) + strictEqual(yield* Random.next, 0.8) + strictEqual(yield* Random.next, 0.2) + strictEqual(yield* Random.next, 0.5) + }).pipe(Effect.withRandomFixed([0.2, 0.5, 0.8]))) + + it.effect("cycles through boolean values", () => + Effect.gen(function*() { + strictEqual(yield* Random.nextBoolean, true) + strictEqual(yield* Random.nextBoolean, false) + strictEqual(yield* Random.nextBoolean, true) + strictEqual(yield* Random.nextBoolean, true) + }).pipe(Effect.withRandom(Random.fixed([true, false, true])))) + + it.effect("cycles through integer values", () => + Effect.gen(function*() { + strictEqual(yield* Random.nextInt, 10) + strictEqual(yield* Random.nextInt, 20) + strictEqual(yield* Random.nextInt, 30) + strictEqual(yield* Random.nextInt, 10) + }).pipe(Effect.withRandom(Random.fixed([10, 20, 30])))) + + it.effect("handles mixed value types", () => + Effect.gen(function*() { + strictEqual(yield* Random.next, 0.5) + strictEqual(yield* Random.nextBoolean, true) + const next = yield* Random.next + assertTrue(next >= 0 && next <= 1) + strictEqual(yield* Random.nextInt, 4) + }).pipe(Effect.withRandom(Random.fixed([0.5, true, "hello", 4.2])))) + + it.effect("nextRange works correctly", () => + Effect.gen(function*() { + const value1 = yield* Random.nextRange(10, 20) + const value2 = yield* Random.nextRange(10, 20) + const value3 = yield* Random.nextRange(10, 20) + strictEqual(value1, 12) + strictEqual(value2, 15) + strictEqual(value3, 18) + }).pipe(Effect.withRandom(Random.fixed([0.2, 0.5, 0.8])))) + + it.effect("nextIntBetween works correctly", () => + Effect.gen(function*() { + strictEqual(yield* Random.nextIntBetween(10, 20), 15) + strictEqual(yield* Random.nextIntBetween(20, 30), 25) + strictEqual(yield* Random.nextIntBetween(30, 40), 35) + strictEqual(yield* Random.nextIntBetween(10, 20), 15) + }).pipe(Effect.withRandom(Random.fixed([15, 25, 35])))) + + it.effect("clamps numeric values to valid range", () => + Effect.gen(function*() { + strictEqual(yield* Random.next, 0) + strictEqual(yield* Random.next, 1) + strictEqual(yield* Random.next, 0.5) + }).pipe(Effect.withRandom(Random.fixed([-1, 2, 0.5])))) + + it.effect("handles non-numeric values by hashing", () => + Effect.gen(function*() { + const value1 = yield* Random.next + const value2 = yield* Random.next + const value3 = yield* Random.next + assertTrue(value1 >= 0 && value1 <= 1) + assertTrue(value2 >= 0 && value2 <= 1) + assertTrue(value3 >= 0 && value3 <= 1) + assertTrue(value1 !== value2) + assertTrue(value2 !== value3) + }).pipe(Effect.withRandom(Random.fixed(["a", "b", "c"])))) + + it.effect("shuffle works with array values", () => + Effect.gen(function*() { + const shuffled = yield* Random.shuffle([1, 2, 3, 4, 5]) + deepStrictEqual(Array.fromIterable(shuffled).sort(), [1, 2, 3, 4, 5]) + }).pipe(Effect.withRandom(Random.fixed([1, 2, 3, 4, 5])))) + }) +}) diff --git a/repos/effect/packages/effect/test/RateLimiter.test.ts b/repos/effect/packages/effect/test/RateLimiter.test.ts new file mode 100644 index 0000000..4e69e8b --- /dev/null +++ b/repos/effect/packages/effect/test/RateLimiter.test.ts @@ -0,0 +1,377 @@ +import { describe, it } from "@effect/vitest" +import { assertFalse, assertLeft, assertTrue, deepStrictEqual, strictEqual } from "@effect/vitest/utils" +import { Array, Clock, Deferred, Effect, Fiber, Function, Option, pipe, RateLimiter, Ref, TestClock } from "effect" + +describe("RateLimiter", () => { + describe.concurrent("fixed-window", () => { + RateLimiterTestSuite("fixed-window") + + it.scoped("will use the provided cost", () => + Effect.gen(function*() { + const rl = yield* (RateLimiter.make({ + limit: 100, + interval: "1 seconds", + algorithm: "fixed-window" + })) + + const now = yield* (Clock.currentTimeMillis) + const fib = yield* ( + Effect.replicateEffect(rl(Clock.currentTimeMillis).pipe(RateLimiter.withCost(10)), 20).pipe( + Effect.fork + ) + ) + + yield* (TestClock.adjust("1 seconds")) + const nowAfter1Second = yield* (Clock.currentTimeMillis) + + const times = yield* (Fiber.join(fib)) + assertTrue(times.slice(0, 10).every((t) => t === now)) + assertTrue(times.slice(10).every((t) => t === nowAfter1Second)) + })) + + it.scoped("will respect different costs per effect and interleave them.", () => + Effect.gen(function*() { + const rl = yield* (RateLimiter.make({ + limit: 10, + interval: "1 seconds", + algorithm: "fixed-window" + })) + const rl1 = Function.compose(rl, RateLimiter.withCost(7)) + const rl2 = Function.compose(rl, RateLimiter.withCost(3)) + + const start = yield* (Clock.currentTimeMillis) + + const tasks = [ + rl1(Clock.currentTimeMillis).pipe(Effect.map((x) => ["rl1", x] as const)), + rl1(Clock.currentTimeMillis).pipe(Effect.map((x) => ["rl1", x] as const)), + rl2(Clock.currentTimeMillis).pipe(Effect.map((x) => ["rl2", x] as const)), + rl2(Clock.currentTimeMillis).pipe(Effect.map((x) => ["rl2", x] as const)) + ] + + const fib = yield* ( + Effect.all(tasks, { concurrency: "unbounded" }).pipe(Effect.fork) + ) + + yield* (TestClock.adjust("1 seconds")) + const after1Second = yield* (Clock.currentTimeMillis) + + const times = yield* (Fiber.join(fib)) + + deepStrictEqual( + times, + [ + ["rl1", start], + ["rl1", after1Second], + ["rl2", start], + ["rl2", after1Second] + ] + ) + })) + + it.scoped("will be composable with other `RateLimiters`", () => + Effect.gen(function*() { + // Max 30 calls per minute + const rl1 = yield* (RateLimiter.make({ + limit: 30, + interval: "1 minutes", + algorithm: "fixed-window" + })) + // Max 2 calls per second + const rl2 = yield* (RateLimiter.make({ + limit: 2, + interval: "1 seconds", + algorithm: "fixed-window" + })) + // This rate limiter respects both the 30 calls per minute + // and the 2 calls per second constraints. + const rl = Function.compose(rl1, rl2) + + const now = yield* (Clock.currentTimeMillis) + + // 32 calls should take 1 minute to complete based on the constraints + // of the rate limiter defined above. + // First 30 calls should trigger in the first 15 seconds + // and the next 2 calls should trigger at the 1 minute mark. + const fib = yield* ( + Effect.replicateEffect(rl(Clock.currentTimeMillis), 32).pipe(Effect.fork) + ) + + const timestamps = yield* ( + Effect.replicateEffect( + Effect.zipRight(TestClock.adjust("1 seconds"), Clock.currentTimeMillis), + 60 + ) + ) + + const times = yield* (Fiber.join(fib)) + + assertTrue(timestamps.length === 60) + assertTrue(times.length === 32) + + const resultTimes = [ + now, + now, + ...timestamps.slice(0, 14).flatMap((x) => [x, x]), + ...timestamps.slice(59).flatMap((x) => [x, x]) + ] + + deepStrictEqual(times, resultTimes) + }), 10_000) + }) + + describe.concurrent("token-bucket", () => { + RateLimiterTestSuite("token-bucket") + + it.scoped("uses the token-bucket algorithm for token replenishment", () => + Effect.scoped(Effect.gen(function*() { + // The limiter below should allow be to execute 10 requests immediately, + // prevent further requests from being executed, and then after 100 ms + // allow execution of another request. + const limit = yield* (RateLimiter.make({ + limit: 10, + interval: "1 seconds", + algorithm: "token-bucket" + })) + const deferred = yield* (Deferred.make()) + + // Use up all of the available tokens + yield* (Effect.forEach(Array.range(1, 10), () => limit(Effect.void))) + + // Make an additional request when there are no tokens available + yield* pipe( + limit(Effect.void), + Effect.zipRight(Deferred.succeed(deferred, void 0)), + Effect.fork + ) + assertFalse(yield* (Deferred.isDone(deferred))) + + // Ensure that the request is successful once a token is replenished + yield* (TestClock.adjust("100 millis")) + yield* (Effect.yieldNow()) + + assertTrue(yield* (Deferred.isDone(deferred))) + }))) + }) +}) + +const RateLimiterTestSuite = (algorithm: "fixed-window" | "token-bucket") => { + it.scoped(`${algorithm} - execute up to max calls immediately`, () => + Effect.gen(function*() { + const limit = yield* (RateLimiter.make({ + limit: 10, + interval: "1 seconds", + algorithm + })) + const now = yield* (Clock.currentTimeMillis) + const times = yield* (Effect.forEach( + Array.range(1, 10), + () => limit(Clock.currentTimeMillis) + )) + const result = Array.every(times, (time) => time === now) + assertTrue(result) + })) + + it.scoped(`${algorithm} - is not affected by stream chunk size`, () => + Effect.gen(function*() { + const limiter = yield* (RateLimiter.make({ + limit: 10, + interval: "1 seconds", + algorithm + })) + const now = yield* (Clock.currentTimeMillis) + const times1 = yield* (Effect.forEach( + Array.range(1, 5), + () => limiter(Clock.currentTimeMillis), + { concurrency: "unbounded" } + )) + const fibers = yield* (Effect.forEach( + Array.range(1, 15), + () => Effect.fork(limiter(Clock.currentTimeMillis)), + { concurrency: "unbounded" } + )) + yield* (TestClock.adjust("1 seconds")) + const times2 = yield* (Effect.forEach(fibers, Fiber.join, { concurrency: "unbounded" })) + const times = Array.appendAll(times1, times2) + const result = Array.filter(times, (time) => time === now) + strictEqual(result.length, 10) + })) + + it.scoped(`${algorithm} - succeed with the result of the call`, () => + Effect.gen(function*() { + const limit = yield* (RateLimiter.make({ + limit: 10, + interval: "1 seconds", + algorithm + })) + const result = yield* (limit(Effect.succeed(3))) + strictEqual(result, 3) + })) + + it.scoped(`${algorithm} - fail with the result of a failed call`, () => + Effect.gen(function*() { + const limit = yield* (RateLimiter.make({ + limit: 10, + interval: "1 seconds", + algorithm + })) + const result = yield* (limit(Effect.either(Effect.fail(Option.none())))) + assertLeft(result, Option.none()) + })) + + it.scoped(`${algorithm} - continue after a failed call`, () => + Effect.gen(function*() { + const limit = yield* RateLimiter.make({ + limit: 10, + interval: "1 seconds", + algorithm + }) + yield* limit(Effect.either(Effect.fail(Option.none()))) + yield* limit(Effect.succeed(3)) + })) + + it.scoped(`${algorithm} - holds back up calls after the max`, () => + Effect.gen(function*() { + const limit = yield* (RateLimiter.make({ + limit: 10, + interval: "1 seconds", + algorithm + })) + + const now = yield* (Clock.currentTimeMillis) + + const fiber = yield* pipe( + Effect.replicateEffect( + limit(Clock.currentTimeMillis), + 20 + ), + Effect.fork + ) + + yield* (TestClock.adjust("1 seconds")) + + const times = yield* (Fiber.join(fiber)) + const later = yield* (Clock.currentTimeMillis) + + assertTrue(times.slice(0, 10).every((x) => x === now)) + assertTrue(times.slice(10).every((x) => x > now && x <= later)) + })) + + it.scoped(`${algorithm} - will interrupt the effect when a call is interrupted`, () => + Effect.gen(function*() { + const limit = yield* (RateLimiter.make({ + limit: 10, + interval: "1 seconds", + algorithm + })) + const latch = yield* (Deferred.make()) + const interrupted = yield* (Deferred.make()) + const fib = yield* pipe( + Deferred.succeed(latch, void 0), + Effect.zipRight(Effect.never), + Effect.onInterrupt(() => Deferred.succeed(interrupted, void 0)), + limit, + Effect.fork + ) + yield* (Deferred.await(latch)) + yield* (Fiber.interrupt(fib)) + yield* (Deferred.await(interrupted)) + })) + + it.scoped(`${algorithm} - will not start execution of an effect when it is interrupted before getting its turn to execute`, () => + Effect.gen(function*() { + const count = yield* (Ref.make(0)) + const limit = yield* (RateLimiter.make({ + limit: 1, + interval: "1 seconds", + algorithm + })) + yield* (limit(Effect.void)) + const fiber = yield* (Effect.fork(limit(Ref.set(count, 1)))) + const interruption = yield* (Effect.fork(Fiber.interrupt(fiber))) + yield* (Fiber.join(interruption)) + strictEqual(yield* (Ref.get(count)), 0) + })) + + it.scoped(`${algorithm} - will wait for interruption to complete of an effect that is already executing`, () => + Effect.gen(function*() { + const limit = yield* (RateLimiter.make({ + limit: 1, + interval: "1 seconds", + algorithm + })) + const latch = yield* (Deferred.make()) + const effectInterrupted = yield* (Ref.make(0)) + const fiber = yield* pipe( + Deferred.succeed(latch, void 0), + Effect.zipRight(Effect.never), + Effect.onInterrupt(() => Ref.set(effectInterrupted, 1)), + limit, + Effect.fork + ) + yield* (Deferred.await(latch)) + yield* (Fiber.interrupt(fiber)) + const interruptions = yield* (Ref.get(effectInterrupted)) + strictEqual(interruptions, 1) + })) + + it.scoped(`${algorithm} - will make effects wait for interrupted effects to pass through the rate limiter`, () => + Effect.gen(function*() { + const limit = yield* (RateLimiter.make({ + limit: 1, + interval: "1 seconds", + algorithm + })) + yield* (limit(Effect.void)) + const fiber1 = yield* (Effect.fork(limit(Effect.void))) + yield* (TestClock.adjust("1 seconds")) + yield* (Fiber.interrupt(fiber1)) + const fiber2 = yield* (Effect.fork(limit(Clock.currentTimeMillis))) + yield* (TestClock.adjust("1 seconds")) + const lastExecutionTime = yield* (Fiber.join(fiber2)) + strictEqual(lastExecutionTime, 2000) + })) + + it.scoped("will not include interrupted effects in the throttling", () => + Effect.gen(function*() { + const rate = 10 + const limit = yield* (RateLimiter.make({ limit: rate, interval: "1 seconds", algorithm })) + const latch = yield* (Deferred.make()) + const latched = yield* (Ref.make(0)) + const wait = yield* (Deferred.make()) + yield* pipe( + Deferred.succeed(latch, void 0), + Effect.whenEffect(latched.pipe( + Ref.updateAndGet((x) => x + 1), + Effect.map((x) => x === rate) + )), + Effect.zipRight(Deferred.await(wait)), + limit, + Effect.fork, + Effect.replicateEffect(rate) + ) + yield* (Deferred.await(latch)) + const fibers = yield* pipe( + Effect.fork(limit(Effect.void)), + Effect.replicateEffect(1000) + ) + yield* (Fiber.interruptAll(fibers)) + const fiber = yield* (Effect.fork(limit(Effect.void))) + yield* (TestClock.adjust("1 seconds")) + yield* (Fiber.join(fiber)) + }), 10_000) + + it.scoped(`${algorithm} - will not drop tokens if interrupted`, () => + Effect.gen(function*() { + const limit = yield* (RateLimiter.make({ + limit: 10, + interval: "1 seconds", + algorithm + })) + + yield* (limit(Effect.void)) + const fiber = yield* pipe(limit(Effect.void), RateLimiter.withCost(10), Effect.fork) + yield* (Effect.yieldNow()) + yield* (Fiber.interrupt(fiber)) + yield* pipe(limit(Effect.void), RateLimiter.withCost(9)) + })) +} diff --git a/repos/effect/packages/effect/test/RcMap.test.ts b/repos/effect/packages/effect/test/RcMap.test.ts new file mode 100644 index 0000000..5d5ec2b --- /dev/null +++ b/repos/effect/packages/effect/test/RcMap.test.ts @@ -0,0 +1,237 @@ +import { describe, it } from "@effect/vitest" +import { assertTrue, deepStrictEqual, strictEqual } from "@effect/vitest/utils" +import { Cause, Data, Effect, Exit, RcMap, Scope, TestClock } from "effect" + +describe("RcMap", () => { + it.effect("deallocation", () => + Effect.gen(function*() { + const acquired: Array = [] + const released: Array = [] + const mapScope = yield* Scope.make() + const map = yield* RcMap.make({ + lookup: (key: string) => + Effect.acquireRelease( + Effect.sync(() => { + acquired.push(key) + return key + }), + () => Effect.sync(() => released.push(key)) + ) + }).pipe( + Scope.extend(mapScope) + ) + + deepStrictEqual(acquired, []) + strictEqual(yield* Effect.scoped(RcMap.get(map, "foo")), "foo") + deepStrictEqual(acquired, ["foo"]) + deepStrictEqual(released, ["foo"]) + + const scopeA = yield* Scope.make() + const scopeB = yield* Scope.make() + yield* RcMap.get(map, "bar").pipe(Scope.extend(scopeA)) + yield* Effect.scoped(RcMap.get(map, "bar")) + yield* RcMap.get(map, "baz").pipe(Scope.extend(scopeB)) + yield* Effect.scoped(RcMap.get(map, "baz")) + deepStrictEqual(acquired, ["foo", "bar", "baz"]) + deepStrictEqual(released, ["foo"]) + yield* Scope.close(scopeB, Exit.void) + deepStrictEqual(acquired, ["foo", "bar", "baz"]) + deepStrictEqual(released, ["foo", "baz"]) + yield* Scope.close(scopeA, Exit.void) + deepStrictEqual(acquired, ["foo", "bar", "baz"]) + deepStrictEqual(released, ["foo", "baz", "bar"]) + + const scopeC = yield* Scope.make() + yield* RcMap.get(map, "qux").pipe(Scope.extend(scopeC)) + deepStrictEqual(acquired, ["foo", "bar", "baz", "qux"]) + deepStrictEqual(released, ["foo", "baz", "bar"]) + + yield* Scope.close(mapScope, Exit.void) + deepStrictEqual(acquired, ["foo", "bar", "baz", "qux"]) + deepStrictEqual(released, ["foo", "baz", "bar", "qux"]) + + const exit = yield* RcMap.get(map, "boom").pipe(Effect.scoped, Effect.exit) + assertTrue(Exit.isInterrupted(exit)) + })) + + it.scoped("idleTimeToLive", () => + Effect.gen(function*() { + const acquired: Array = [] + const released: Array = [] + const map = yield* RcMap.make({ + lookup: (key: string) => + Effect.acquireRelease( + Effect.sync(() => { + acquired.push(key) + return key + }), + () => Effect.sync(() => released.push(key)) + ), + idleTimeToLive: 1000 + }) + + deepStrictEqual(acquired, []) + strictEqual(yield* Effect.scoped(RcMap.get(map, "foo")), "foo") + deepStrictEqual(acquired, ["foo"]) + deepStrictEqual(released, []) + + yield* TestClock.adjust(1000) + deepStrictEqual(released, ["foo"]) + + strictEqual(yield* Effect.scoped(RcMap.get(map, "bar")), "bar") + deepStrictEqual(acquired, ["foo", "bar"]) + deepStrictEqual(released, ["foo"]) + + yield* TestClock.adjust(500) + strictEqual(yield* Effect.scoped(RcMap.get(map, "bar")), "bar") + deepStrictEqual(acquired, ["foo", "bar"]) + deepStrictEqual(released, ["foo"]) + + yield* TestClock.adjust(1000) + deepStrictEqual(released, ["foo", "bar"]) + + yield* Effect.scoped(RcMap.get(map, "baz")) + deepStrictEqual(acquired, ["foo", "bar", "baz"]) + yield* RcMap.invalidate(map, "baz") + deepStrictEqual(acquired, ["foo", "bar", "baz"]) + deepStrictEqual(released, ["foo", "bar", "baz"]) + })) + + it.scoped(".touch", () => + Effect.gen(function*() { + const acquired: Array = [] + const released: Array = [] + const map = yield* RcMap.make({ + lookup: (key: string) => + Effect.acquireRelease( + Effect.sync(() => { + acquired.push(key) + return key + }), + () => Effect.sync(() => released.push(key)) + ), + idleTimeToLive: 1000 + }) + + deepStrictEqual(acquired, []) + strictEqual(yield* Effect.scoped(RcMap.get(map, "foo")), "foo") + deepStrictEqual(acquired, ["foo"]) + deepStrictEqual(released, []) + + yield* TestClock.adjust(500) + deepStrictEqual(released, []) + + yield* RcMap.touch(map, "foo") + yield* TestClock.adjust(500) + deepStrictEqual(released, []) + yield* TestClock.adjust(500) + deepStrictEqual(released, ["foo"]) + })) + + it.scoped("capacity", () => + Effect.gen(function*() { + const map = yield* RcMap.make({ + lookup: (key: string) => Effect.succeed(key), + capacity: 2, + idleTimeToLive: 1000 + }) + + strictEqual(yield* Effect.scoped(RcMap.get(map, "foo")), "foo") + strictEqual(yield* Effect.scoped(RcMap.get(map, "foo")), "foo") + strictEqual(yield* Effect.scoped(RcMap.get(map, "bar")), "bar") + + const exit = yield* RcMap.get(map, "baz").pipe(Effect.scoped, Effect.exit) + deepStrictEqual( + exit, + Exit.fail(new Cause.ExceededCapacityException(`RcMap attempted to exceed capacity of 2`)) + ) + + yield* TestClock.adjust(1000) + strictEqual(yield* Effect.scoped(RcMap.get(map, "baz")), "baz") + })) + + it.scoped("complex key", () => + Effect.gen(function*() { + class Key extends Data.Class<{ readonly id: number }> {} + const map = yield* RcMap.make({ + lookup: (key: Key) => Effect.succeed(key.id), + capacity: 1 + }) + + strictEqual(yield* RcMap.get(map, new Key({ id: 1 })), 1) + // no failure means a hit + strictEqual(yield* RcMap.get(map, new Key({ id: 1 })), 1) + })) + + it.scoped("keys lookup", () => + Effect.gen(function*() { + const map = yield* RcMap.make({ + lookup: (key: string) => Effect.succeed(key) + }) + + yield* RcMap.get(map, "foo") + yield* RcMap.get(map, "bar") + yield* RcMap.get(map, "baz") + + deepStrictEqual(yield* RcMap.keys(map), ["foo", "bar", "baz"]) + })) + + it.scoped("dynamic idleTimeToLive", () => + Effect.gen(function*() { + const acquired: Array = [] + const released: Array = [] + const map = yield* RcMap.make({ + lookup: (key: string) => + Effect.acquireRelease( + Effect.sync(() => { + acquired.push(key) + return key + }), + () => Effect.sync(() => released.push(key)) + ), + idleTimeToLive: (key: string) => key.startsWith("short:") ? 500 : 2000 + }) + + deepStrictEqual(acquired, []) + + yield* Effect.scoped(RcMap.get(map, "short:a")) + yield* Effect.scoped(RcMap.get(map, "long:b")) + deepStrictEqual(acquired, ["short:a", "long:b"]) + deepStrictEqual(released, []) + + yield* TestClock.adjust(500) + deepStrictEqual(released, ["short:a"]) + + yield* TestClock.adjust(1500) + deepStrictEqual(released, ["short:a", "long:b"]) + })) + + it.scoped("dynamic idleTimeToLive with touch", () => + Effect.gen(function*() { + const acquired: Array = [] + const released: Array = [] + const map = yield* RcMap.make({ + lookup: (key: string) => + Effect.acquireRelease( + Effect.sync(() => { + acquired.push(key) + return key + }), + () => Effect.sync(() => released.push(key)) + ), + idleTimeToLive: (key: string) => key.startsWith("short:") ? 500 : 2000 + }) + + yield* Effect.scoped(RcMap.get(map, "short:a")) + deepStrictEqual(acquired, ["short:a"]) + deepStrictEqual(released, []) + + yield* TestClock.adjust(250) + yield* RcMap.touch(map, "short:a") + yield* TestClock.adjust(250) + deepStrictEqual(released, []) + + yield* TestClock.adjust(250) + deepStrictEqual(released, ["short:a"]) + })) +}) diff --git a/repos/effect/packages/effect/test/RcRef.test.ts b/repos/effect/packages/effect/test/RcRef.test.ts new file mode 100644 index 0000000..a92bb20 --- /dev/null +++ b/repos/effect/packages/effect/test/RcRef.test.ts @@ -0,0 +1,95 @@ +import { describe, it } from "@effect/vitest" +import { assertTrue, strictEqual } from "@effect/vitest/utils" +import { Effect, Exit, RcRef, Scope, TestClock } from "effect" + +describe("RcRef", () => { + it.effect("deallocation", () => + Effect.gen(function*() { + let acquired = 0 + let released = 0 + const refScope = yield* Scope.make() + const ref = yield* RcRef.make({ + acquire: Effect.acquireRelease( + Effect.sync(() => { + acquired++ + return "foo" + }), + () => + Effect.sync(() => { + released++ + }) + ) + }).pipe( + Scope.extend(refScope) + ) + + strictEqual(acquired, 0) + strictEqual(yield* Effect.scoped(ref), "foo") + strictEqual(acquired, 1) + strictEqual(released, 1) + + const scopeA = yield* Scope.make() + const scopeB = yield* Scope.make() + yield* ref.pipe(Scope.extend(scopeA)) + yield* ref.pipe(Scope.extend(scopeB)) + strictEqual(acquired, 2) + strictEqual(released, 1) + yield* Scope.close(scopeB, Exit.void) + strictEqual(acquired, 2) + strictEqual(released, 1) + yield* Scope.close(scopeA, Exit.void) + strictEqual(acquired, 2) + strictEqual(released, 2) + + const scopeC = yield* Scope.make() + yield* ref.pipe(Scope.extend(scopeC)) + strictEqual(acquired, 3) + strictEqual(released, 2) + + yield* Scope.close(refScope, Exit.void) + strictEqual(acquired, 3) + strictEqual(released, 3) + + const exit = yield* ref.get.pipe(Effect.scoped, Effect.exit) + assertTrue(Exit.isInterrupted(exit)) + })) + + it.scoped("idleTimeToLive", () => + Effect.gen(function*() { + let acquired = 0 + let released = 0 + const ref = yield* RcRef.make({ + acquire: Effect.acquireRelease( + Effect.sync(() => { + acquired++ + return "foo" + }), + () => + Effect.sync(() => { + released++ + }) + ), + idleTimeToLive: 1000 + }) + + strictEqual(acquired, 0) + strictEqual(yield* Effect.scoped(RcRef.get(ref)), "foo") + strictEqual(acquired, 1) + strictEqual(released, 0) + + yield* TestClock.adjust(1000) + strictEqual(released, 1) + + strictEqual(yield* Effect.scoped(RcRef.get(ref)), "foo") + strictEqual(acquired, 2) + strictEqual(released, 1) + + yield* TestClock.adjust(500) + strictEqual(yield* Effect.scoped(RcRef.get(ref)), "foo") + strictEqual(acquired, 2) + strictEqual(released, 1) + + yield* TestClock.adjust(1000) + strictEqual(released, 2) + })) +}) diff --git a/repos/effect/packages/effect/test/Record.test.ts b/repos/effect/packages/effect/test/Record.test.ts new file mode 100644 index 0000000..c9a3e6f --- /dev/null +++ b/repos/effect/packages/effect/test/Record.test.ts @@ -0,0 +1,403 @@ +import { describe, it } from "@effect/vitest" +import { assertFalse, assertNone, assertSome, assertTrue, deepStrictEqual } from "@effect/vitest/utils" +import { Either, Number as Num, Option, pipe, Record } from "effect" + +const symA = Symbol.for("a") +const symB = Symbol.for("b") +const symC = Symbol.for("c") + +const stringRecord: Record = { a: 1, [symA]: null } +const symbolRecord: Record = { [symA]: 1, [symB]: 2 } + +describe("Record", () => { + describe("string | symbol APIs", () => { + it("empty", () => { + deepStrictEqual(Record.empty(), {}) + }) + + it("fromIterableWith", () => { + deepStrictEqual(Record.fromIterableWith([1, 2, 3, 4], (a) => [a === 3 ? "a" : String(a), a * 2]), { + "1": 2, + "2": 4, + a: 6, + "4": 8 + }) + deepStrictEqual(Record.fromIterableWith([1, 2, 3, 4], (a) => [a === 3 ? symA : String(a), a * 2]), { + "1": 2, + "2": 4, + [symA]: 6, + "4": 8 + }) + }) + + it("fromIterableBy", () => { + const users = [ + { id: "2", name: "name2" }, + { id: "1", name: "name1" } + ] + deepStrictEqual(Record.fromIterableBy(users, (user) => user.id), { + "2": { id: "2", name: "name2" }, + "1": { id: "1", name: "name1" } + }) + + deepStrictEqual(Record.fromIterableBy(["a", symA], (s) => s), { a: "a", [symA]: symA }) + }) + + it("fromEntries", () => { + deepStrictEqual(Record.fromEntries([["1", 2], ["2", 4], ["3", 6], ["4", 8]]), { + "1": 2, + "2": 4, + "3": 6, + "4": 8 + }) + deepStrictEqual(Record.fromEntries([["1", 2], ["2", 4], ["3", 6], ["4", 8], [symA, 10], [symB, 12]]), { + "1": 2, + "2": 4, + "3": 6, + "4": 8, + [symA]: 10, + [symB]: 12 + }) + }) + + it("has", () => { + assertTrue(Record.has(stringRecord, "a")) + assertFalse(Record.has(stringRecord, "c")) + + assertTrue(Record.has(symbolRecord, symA)) + assertFalse(Record.has(symbolRecord, symC)) + }) + + it("get", () => { + assertNone(pipe(Record.empty(), Record.get("a"))) + assertSome(pipe(stringRecord, Record.get("a")), 1) + + assertNone(pipe(Record.empty(), Record.get(symA))) + assertSome(pipe(symbolRecord, Record.get(symA)), 1) + }) + + it("modify", () => { + deepStrictEqual(pipe(Record.empty(), Record.modify("a", (n: number) => n + 1)), {}) + deepStrictEqual(pipe(stringRecord, Record.modify("a", (n: number) => n + 1)), { a: 2, [symA]: null }) + deepStrictEqual(pipe(stringRecord, Record.modify("a", (n: number) => String(n))), { a: "1", [symA]: null }) + + deepStrictEqual(pipe(Record.empty(), Record.modify(symA, (n: number) => n + 1)), {}) + deepStrictEqual(pipe(symbolRecord, Record.modify(symA, (n: number) => n + 1)), { + [symA]: 2, + [symB]: 2 + }) + deepStrictEqual(pipe(symbolRecord, Record.modify(symA, (n: number) => String(n))), { [symA]: "1", [symB]: 2 }) + }) + + it("modifyOption", () => { + assertNone(pipe(Record.empty(), Record.modifyOption("a", (n) => n + 1))) + assertSome(pipe(stringRecord, Record.modifyOption("a", (n: number) => n + 1)), { a: 2, [symA]: null }) + assertSome(pipe(stringRecord, Record.modifyOption("a", (n: number) => String(n))), { a: "1", [symA]: null }) + + assertNone(pipe(Record.empty(), Record.modifyOption(symA, (n) => n + 1))) + assertSome(pipe(symbolRecord, Record.modifyOption(symA, (n: number) => n + 1)), { [symA]: 2, [symB]: 2 }) + assertSome( + pipe(symbolRecord, Record.modifyOption(symA, (n: number) => String(n))), + { [symA]: "1", [symB]: 2 } + ) + }) + + it("replaceOption", () => { + assertNone(pipe(Record.empty(), Record.replaceOption("a", 2))) + assertSome(pipe(stringRecord, Record.replaceOption("a", 2)), { a: 2, [symA]: null }) + assertSome(pipe(stringRecord, Record.replaceOption("a", true)), { a: true, [symA]: null }) + + assertNone(pipe(Record.empty(), Record.replaceOption(symA, 2))) + assertSome(pipe(symbolRecord, Record.replaceOption(symA, 2)), { [symA]: 2, [symB]: 2 }) + assertSome(pipe(symbolRecord, Record.replaceOption(symA, true)), { [symA]: true, [symB]: 2 }) + }) + + it("remove", () => { + deepStrictEqual(Record.remove(stringRecord, "a"), { [symA]: null }) + deepStrictEqual(Record.remove(stringRecord, "c"), stringRecord) + + deepStrictEqual(Record.remove(symbolRecord, symA), { [symB]: 2 }) + deepStrictEqual(Record.remove(symbolRecord, symC), symbolRecord) + }) + + describe("pop", () => { + it("should return the value associated with the given key, if the key is present in the record", () => { + const result1 = Record.pop(stringRecord, "a") + assertSome(result1, [1, { [symA]: null }]) + + const result2 = Record.pop(symbolRecord, symA) + assertSome(result2, [1, { [symB]: 2 }]) + }) + + it("should return none if the key is not present in the record", () => { + const result1 = Record.pop(stringRecord, "c") + assertNone(result1) + + const result2 = Record.pop(symbolRecord, symC) + assertNone(result2) + }) + }) + + describe("set", () => { + it("should replace an existing value", () => { + deepStrictEqual(Record.set(stringRecord, "a", 2), { a: 2, [symA]: null }) + + deepStrictEqual(Record.set(symbolRecord, symA, 2), { [symA]: 2, [symB]: 2 }) + }) + + it("should add the key / value pair", () => { + deepStrictEqual(Record.set(stringRecord, "c", 3), { a: 1, [symA]: null, c: 3 }) + + deepStrictEqual(Record.set(symbolRecord, symC, 3), { [symA]: 1, [symB]: 2, [symC]: 3 }) + }) + }) + + it("replace", () => { + deepStrictEqual(Record.replace(stringRecord, "c", 3), stringRecord) + deepStrictEqual(Record.replace(stringRecord, "a", 2), { a: 2, [symA]: null }) + + deepStrictEqual(Record.replace(symbolRecord, symC, 3), symbolRecord) + deepStrictEqual(Record.replace(symbolRecord, symA, 2), { [symA]: 2, [symB]: 2 }) + }) + + it("singleton", () => { + deepStrictEqual(Record.singleton("a", 1), { a: 1 }) + + deepStrictEqual(Record.singleton(symA, 1), { [symA]: 1 }) + }) + }) + + describe("string only APIs", () => { + it("map", () => { + deepStrictEqual(pipe(stringRecord, Record.map((n) => n * 2)), { a: 2, [symA]: null }) + deepStrictEqual(pipe(stringRecord, Record.map((n, k) => `${k}-${n}`)), { a: "a-1", [symA]: null }) + }) + + it("collect", () => { + const x = { a: 1, b: 2, c: 3, [symA]: null } + deepStrictEqual(Record.collect(x, (key, n) => [key, n]), [["a", 1], ["b", 2], ["c", 3]]) + }) + + it("toEntries", () => { + const x = { a: 1, b: 2, c: 3, [symA]: null } + deepStrictEqual(Record.toEntries(x), [["a", 1], ["b", 2], ["c", 3]]) + }) + + it("filterMap", () => { + const x: Record = { a: 1, b: 2, c: 3, [symA]: null } + const filtered = Record.filterMap(x, (value, key) => (value > 2 ? Option.some(key) : Option.none())) + deepStrictEqual(filtered, { c: "c" }) + }) + + it("getSomes", () => { + const x = { a: Option.some(1), b: Option.none(), c: Option.some(2), [symA]: null } + deepStrictEqual(Record.getSomes(x), { a: 1, c: 2 }) + }) + + it("filter", () => { + const x: Record = { a: 1, b: 2, c: 3, d: 4, [symA]: null } + deepStrictEqual(Record.filter(x, (value) => value > 2), { c: 3, d: 4 }) + }) + + it("partitionMap", () => { + const f = (n: number) => (n > 2 ? Either.right(n + 1) : Either.left(n - 1)) + deepStrictEqual(Record.partitionMap({}, f), [{}, {}]) + deepStrictEqual(Record.partitionMap({ a: 1, b: 3, [symA]: null }, f), [{ a: 0 }, { b: 4 }]) + }) + + it("partition", () => { + const f = (n: number) => n > 2 + deepStrictEqual(Record.partition({}, f), [{}, {}]) + deepStrictEqual(Record.partition({ a: 1, b: 3, [symA]: null }, f), [{ a: 1 }, { b: 3 }]) + }) + + it("separate", () => { + deepStrictEqual( + Record.separate({ a: Either.left("e"), b: Either.right(1), [symA]: null }), + [{ a: "e" }, { b: 1 }] + ) + // should ignore non own properties + const o: Record.ReadonlyRecord<"a", Either.Either> = Object.create({ a: 1 }) + deepStrictEqual(pipe(o, Record.separate), [{}, {}]) + }) + + it("isEmptyRecord", () => { + deepStrictEqual(Record.isEmptyRecord({}), true) + deepStrictEqual(Record.isEmptyRecord({ [symA]: null }), true) + deepStrictEqual(Record.isEmptyRecord({ a: 3 }), false) + }) + + it("isEmptyReadonlyRecord", () => { + deepStrictEqual(Record.isEmptyReadonlyRecord({}), true) + deepStrictEqual(Record.isEmptyReadonlyRecord({ [symA]: null }), true) + deepStrictEqual(Record.isEmptyReadonlyRecord({ a: 3 }), false) + }) + + it("size", () => { + deepStrictEqual(Record.size({ a: "a", b: 1, c: true, [symA]: null }), 3) + }) + + it("keys", () => { + deepStrictEqual(Record.keys({ a: 1, b: 2, [symA]: null }), ["a", "b"]) + }) + + it("values", () => { + deepStrictEqual(Record.values({ a: 1, b: 2, [symA]: null }), [1, 2]) + }) + + it("isSubrecord", () => { + assertTrue(Record.isSubrecord(Record.empty(), {})) + assertTrue(Record.isSubrecord(Record.empty(), { a: 1 })) + assertTrue(Record.isSubrecord({ a: 1 }, { a: 1 })) + assertTrue(Record.isSubrecord(stringRecord, { a: 1 })) + assertTrue(Record.isSubrecord({ a: 1 }, stringRecord)) + assertTrue(Record.isSubrecord({ a: 1 } as Record, { a: 1, b: 2 })) + assertTrue(Record.isSubrecord({ b: 2, a: 1 }, { a: 1, b: 2 })) + assertFalse(Record.isSubrecord({ a: 1 }, { a: 2 })) + assertFalse(Record.isSubrecord({ b: 2 } as Record, { a: 1 })) + }) + + it("reduce", () => { + // data-first + deepStrictEqual( + Record.reduce({ k1: "a", k2: "b", [symA]: null }, "-", (accumulator, value, key) => accumulator + key + value), + "-k1ak2b" + ) + // data-last + deepStrictEqual( + pipe( + { k1: "a", k2: "b", [symA]: null }, + Record.reduce("-", (accumulator, value, key) => accumulator + key + value) + ), + "-k1ak2b" + ) + }) + + it("every", () => { + assertTrue(Record.every((n: number) => n <= 2)({ a: 1, b: 2, [symA]: null })) + assertFalse(Record.every((n: number) => n <= 1)({ a: 1, b: 2, [symA]: null })) + }) + + it("some", () => { + assertTrue(Record.some((n: number) => n <= 1)({ a: 1, b: 2, [symA]: null })) + assertFalse(Record.some((n: number) => n <= 0)({ a: 1, b: 2, [symA]: null })) + }) + + it("union", () => { + const combine = (s1: string, s2: string) => s1 + s2 + const x: Record.ReadonlyRecord = { + a: "a1", + b: "b1", + c: "c1", + [symA]: null + } + const y: Record.ReadonlyRecord = { + b: "b2", + c: "c2", + d: "d2", + [symA]: null + } + deepStrictEqual(Record.union(x, {}, combine), x) + deepStrictEqual(Record.union({}, x, combine), x) + deepStrictEqual(Record.union(x, {}, combine), x) + deepStrictEqual(Record.union({}, x, combine), x) + deepStrictEqual(Record.union(x, y, combine), { + a: "a1", + b: "b1b2", + c: "c1c2", + d: "d2" + }) + }) + + it("intersection", () => { + const combine = (s1: string, s2: string) => s1 + s2 + const x: Record.ReadonlyRecord = { + a: "a1", + b: "b1", + c: "c1", + [symA]: null + } + const y: Record.ReadonlyRecord = { + b: "b2", + c: "c2", + d: "d2", + [symA]: null + } + deepStrictEqual(Record.intersection(x, {}, combine), {}) + deepStrictEqual(Record.intersection({}, y, combine), {}) + deepStrictEqual(Record.intersection(x, y, combine), { + b: "b1b2", + c: "c1c2" + }) + }) + + it("difference", () => { + const x: Record.ReadonlyRecord = { + a: "a1", + b: "b1", + c: "c1", + [symA]: null + } + const y: Record.ReadonlyRecord = { + b: "b2", + c: "c2", + d: "d2", + [symA]: null + } + deepStrictEqual(Record.difference({}, x), x) + deepStrictEqual(Record.difference(x, {}), x) + deepStrictEqual(Record.difference({}, x), x) + deepStrictEqual(Record.difference(x, {}), x) + deepStrictEqual(Record.difference(x, y), { + a: "a1", + d: "d2" + }) + }) + + it("getEquivalence", () => { + deepStrictEqual(Record.getEquivalence(Num.Equivalence)({ a: 1 }, { a: 1 }), true) + deepStrictEqual(Record.getEquivalence(Num.Equivalence)({ a: 1 }, stringRecord), true) + deepStrictEqual(Record.getEquivalence(Num.Equivalence)({ a: 1 }, { a: 2 }), false) + deepStrictEqual(Record.getEquivalence(Num.Equivalence)({ a: 1 }, { b: 1 }), false) + const noPrototype = Object.create(null) + deepStrictEqual(Record.getEquivalence(Num.Equivalence)(noPrototype, { b: 1 }), false) + }) + + it("mapKeys", () => { + deepStrictEqual(pipe({ a: 1, b: 2, [symA]: null }, Record.mapKeys((key) => key.toUpperCase())), { + A: 1, + B: 2 + }) + }) + + it("mapEntries", () => { + deepStrictEqual(pipe(stringRecord, Record.mapEntries((a, key) => [key.toUpperCase(), a + 1])), { A: 2 }) + }) + + describe("findFirst", () => { + it("refinement/predicate", () => { + const record = { + a: 1, + b: 2, + c: 1 + } + deepStrictEqual( + pipe(record, Record.findFirst((v) => v < 2)), + Option.some(["a", 1]) + ) + deepStrictEqual( + pipe(record, Record.findFirst((v, k) => v < 2 && k !== "a")), + Option.some(["c", 1]) + ) + deepStrictEqual( + pipe(record, Record.findFirst((v) => v > 2)), + Option.none() + ) + deepStrictEqual( + Record.findFirst(record, (v) => v < 2), + Option.some(["a", 1]) + ) + }) + }) + }) +}) diff --git a/repos/effect/packages/effect/test/RedBlackTree.test.ts b/repos/effect/packages/effect/test/RedBlackTree.test.ts new file mode 100644 index 0000000..c908d83 --- /dev/null +++ b/repos/effect/packages/effect/test/RedBlackTree.test.ts @@ -0,0 +1,420 @@ +import { describe, it } from "@effect/vitest" +import { assertTrue, deepStrictEqual, strictEqual } from "@effect/vitest/utils" +import { Equal, Hash, Number as Num, Option, Order, pipe, RedBlackTree } from "effect" + +describe("RedBlackTree", () => { + it("toString", () => { + const tree = pipe( + RedBlackTree.empty(Num.Order), + RedBlackTree.insert(1, "a"), + RedBlackTree.insert(0, "b") + ) + + strictEqual( + String(tree), + `{ + "_id": "RedBlackTree", + "values": [ + [ + 0, + "b" + ], + [ + 1, + "a" + ] + ] +}` + ) + }) + + it("toJSON", () => { + const tree = pipe( + RedBlackTree.empty(Num.Order), + RedBlackTree.insert(1, "a"), + RedBlackTree.insert(0, "b") + ) + + deepStrictEqual(tree.toJSON(), { _id: "RedBlackTree", values: [[0, "b"], [1, "a"]] }) + }) + + it("inspect", () => { + if (typeof window !== "undefined") { + return + } + // eslint-disable-next-line @typescript-eslint/no-require-imports + const { inspect } = require("node:util") + + const tree = pipe( + RedBlackTree.empty(Num.Order), + RedBlackTree.insert(1, "a"), + RedBlackTree.insert(0, "b") + ) + + deepStrictEqual(inspect(tree), inspect({ _id: "RedBlackTree", values: [[0, "b"], [1, "a"]] })) + }) + + it("forEach", () => { + const ordered: Array<[number, string]> = [] + pipe( + RedBlackTree.empty(Num.Order), + RedBlackTree.insert(1, "a"), + RedBlackTree.insert(0, "b"), + RedBlackTree.insert(-1, "c"), + RedBlackTree.insert(-2, "d"), + RedBlackTree.insert(3, "e"), + RedBlackTree.forEach((n, s) => { + ordered.push([n, s]) + }) + ) + + deepStrictEqual(ordered, [ + [-2, "d"], + [-1, "c"], + [0, "b"], + [1, "a"], + [3, "e"] + ]) + }) + + it("iterable", () => { + const tree = pipe( + RedBlackTree.empty(Num.Order), + RedBlackTree.insert(1, "a"), + RedBlackTree.insert(0, "b"), + RedBlackTree.insert(-1, "c"), + RedBlackTree.insert(-2, "d"), + RedBlackTree.insert(3, "e") + ) + + strictEqual(RedBlackTree.size(tree), 5) + deepStrictEqual(Array.from(tree), [ + [-2, "d"], + [-1, "c"], + [0, "b"], + [1, "a"], + [3, "e"] + ]) + }) + + it("iterable empty", () => { + const tree = RedBlackTree.empty(Num.Order) + + strictEqual(RedBlackTree.size(tree), 0) + deepStrictEqual(Array.from(tree), []) + }) + + it("backwards", () => { + const tree = pipe( + RedBlackTree.empty(Num.Order), + RedBlackTree.insert(1, "a"), + RedBlackTree.insert(0, "b"), + RedBlackTree.insert(-1, "c"), + RedBlackTree.insert(-2, "d"), + RedBlackTree.insert(3, "e") + ) + + strictEqual(RedBlackTree.size(tree), 5) + deepStrictEqual(Array.from(RedBlackTree.reversed(tree)), [ + [3, "e"], + [1, "a"], + [0, "b"], + [-1, "c"], + [-2, "d"] + ]) + }) + + it("backwards empty", () => { + const tree = RedBlackTree.empty(Num.Order) + + strictEqual(RedBlackTree.size(tree), 0) + deepStrictEqual(Array.from(RedBlackTree.reversed(tree)), []) + }) + + it("values", () => { + const tree = pipe( + RedBlackTree.empty(Num.Order), + RedBlackTree.insert(1, "a"), + RedBlackTree.insert(0, "b"), + RedBlackTree.insert(-1, "c"), + RedBlackTree.insert(-2, "d"), + RedBlackTree.insert(3, "e") + ) + + strictEqual(RedBlackTree.size(tree), 5) + deepStrictEqual(Array.from(RedBlackTree.values(tree)), ["d", "c", "b", "a", "e"]) + }) + + it("keys", () => { + const tree = pipe( + RedBlackTree.empty(Num.Order), + RedBlackTree.insert(1, "a"), + RedBlackTree.insert(0, "b"), + RedBlackTree.insert(-1, "c"), + RedBlackTree.insert(-2, "d"), + RedBlackTree.insert(3, "e") + ) + + strictEqual(RedBlackTree.size(tree), 5) + deepStrictEqual(Array.from(RedBlackTree.keys(tree)), [-2, -1, 0, 1, 3]) + }) + + it("begin/end", () => { + const tree = pipe( + RedBlackTree.empty(Num.Order), + RedBlackTree.insert(1, "a"), + RedBlackTree.insert(0, "b"), + RedBlackTree.insert(-1, "c"), + RedBlackTree.insert(-2, "d"), + RedBlackTree.insert(3, "e") + ) + + deepStrictEqual(RedBlackTree.first(tree), Option.some([-2, "d"])) + deepStrictEqual(RedBlackTree.last(tree), Option.some([3, "e"])) + deepStrictEqual(RedBlackTree.getAt(1)(tree), Option.some([-1, "c"])) + }) + + it("forEachGreaterThanEqual", () => { + const ordered: Array<[number, string]> = [] + pipe( + RedBlackTree.empty(Num.Order), + RedBlackTree.insert(1, "a"), + RedBlackTree.insert(0, "b"), + RedBlackTree.insert(-1, "c"), + RedBlackTree.insert(-2, "d"), + RedBlackTree.insert(3, "e"), + RedBlackTree.forEachGreaterThanEqual(0, (k, v) => { + ordered.push([k, v]) + }) + ) + + deepStrictEqual(ordered, [[0, "b"], [1, "a"], [3, "e"]]) + }) + + it("forEachLessThan", () => { + const ordered: Array<[number, string]> = [] + pipe( + RedBlackTree.empty(Num.Order), + RedBlackTree.insert(1, "a"), + RedBlackTree.insert(0, "b"), + RedBlackTree.insert(-1, "c"), + RedBlackTree.insert(-2, "d"), + RedBlackTree.insert(3, "e"), + RedBlackTree.forEachLessThan(0, (k, v) => { + ordered.push([k, v]) + }) + ) + + deepStrictEqual(ordered, [[-2, "d"], [-1, "c"]]) + }) + + it("forEachBetween", () => { + const ordered: Array<[number, string]> = [] + pipe( + RedBlackTree.empty(Num.Order), + RedBlackTree.insert(1, "a"), + RedBlackTree.insert(0, "b"), + RedBlackTree.insert(-1, "c"), + RedBlackTree.insert(-2, "d"), + RedBlackTree.insert(3, "e"), + RedBlackTree.forEachBetween({ + min: -1, + max: 2, + body: (k, v) => { + ordered.push([k, v]) + } + }) + ) + + deepStrictEqual(ordered, [[-1, "c"], [0, "b"], [1, "a"]]) + }) + + it("greaterThan", () => { + const tree = pipe( + RedBlackTree.empty(Num.Order), + RedBlackTree.insert(1, "a"), + RedBlackTree.insert(0, "b"), + RedBlackTree.insert(-1, "c"), + RedBlackTree.insert(-2, "d"), + RedBlackTree.insert(3, "e") + ) + + deepStrictEqual(Array.from(RedBlackTree.greaterThan(0)(tree)), [ + [1, "a"], + [3, "e"] + ]) + deepStrictEqual( + Array.from(RedBlackTree.greaterThanReversed(0)(tree)), + [ + [1, "a"], + [0, "b"], + [-1, "c"], + [-2, "d"] + ] + ) + }) + + it("greaterThanEqual", () => { + const tree = pipe( + RedBlackTree.empty(Num.Order), + RedBlackTree.insert(1, "a"), + RedBlackTree.insert(0, "b"), + RedBlackTree.insert(-1, "c"), + RedBlackTree.insert(-2, "d"), + RedBlackTree.insert(3, "e") + ) + + deepStrictEqual(Array.from(RedBlackTree.greaterThanEqual(0)(tree)), [ + [0, "b"], + [1, "a"], + [3, "e"] + ]) + deepStrictEqual( + Array.from(RedBlackTree.greaterThanEqualReversed(0)(tree)), + [ + [0, "b"], + [-1, "c"], + [-2, "d"] + ] + ) + }) + + it("lessThan", () => { + const tree = pipe( + RedBlackTree.empty(Num.Order), + RedBlackTree.insert(1, "a"), + RedBlackTree.insert(0, "b"), + RedBlackTree.insert(-1, "c"), + RedBlackTree.insert(-2, "d"), + RedBlackTree.insert(3, "e") + ) + + deepStrictEqual(Array.from(RedBlackTree.lessThan(0)(tree)), [ + [-1, "c"], + [0, "b"], + [1, "a"], + [3, "e"] + ]) + deepStrictEqual( + Array.from(RedBlackTree.lessThanReversed(0)(tree)), + [ + [-1, "c"], + [-2, "d"] + ] + ) + }) + + it("lessThanEqual", () => { + const tree = pipe( + RedBlackTree.empty(Num.Order), + RedBlackTree.insert(1, "a"), + RedBlackTree.insert(0, "b"), + RedBlackTree.insert(-1, "c"), + RedBlackTree.insert(-2, "d"), + RedBlackTree.insert(3, "e") + ) + + deepStrictEqual(Array.from(RedBlackTree.lessThanEqual(0)(tree)), [ + [0, "b"], + [1, "a"], + [3, "e"] + ]) + deepStrictEqual( + Array.from(RedBlackTree.lessThanEqualReversed(0)(tree)), + [ + [0, "b"], + [-1, "c"], + [-2, "d"] + ] + ) + }) + + it("findAll", () => { + const tree = pipe( + RedBlackTree.empty(Num.Order), + RedBlackTree.insert(1, "a"), + RedBlackTree.insert(2, "c"), + RedBlackTree.insert(1, "b"), + RedBlackTree.insert(3, "d"), + RedBlackTree.insert(1, "e") + ) + + deepStrictEqual(Array.from(RedBlackTree.findAll(1)(tree)), ["a", "b", "e"]) + + const bigintTree = pipe( + RedBlackTree.empty(Order.bigint), + RedBlackTree.insert(1n, 1), + RedBlackTree.insert(1n, 2), + RedBlackTree.insert(1n, 3), + RedBlackTree.insert(1n, 4), + RedBlackTree.insert(1n, 5), + RedBlackTree.insert(2n, 6) + ) + + deepStrictEqual(Array.from(RedBlackTree.findAll(1n)(bigintTree)), [1, 2, 3, 4, 5]) + }) + + it("findAll Eq/Ord", () => { + class Key { + constructor(readonly n: number, readonly s: string) {} + + [Hash.symbol](): number { + return Hash.combine(Hash.hash(this.n))(Hash.hash(this.s)) + } + + [Equal.symbol](that: unknown): boolean { + return that instanceof Key && this.n === that.n && this.s === that.s + } + } + + const ord = pipe(Num.Order, Order.mapInput((key: Key) => key.n)) + + const tree = pipe( + RedBlackTree.empty(ord), + RedBlackTree.insert(new Key(1, "0"), "a"), + RedBlackTree.insert(new Key(2, "0"), "c"), + RedBlackTree.insert(new Key(1, "1"), "b"), + RedBlackTree.insert(new Key(3, "0"), "d"), + RedBlackTree.insert(new Key(1, "0"), "e"), + RedBlackTree.insert(new Key(1, "0"), "f"), + RedBlackTree.insert(new Key(1, "1"), "g") + ) + + deepStrictEqual(Array.from(RedBlackTree.values(tree)), ["g", "f", "e", "b", "a", "c", "d"]) + deepStrictEqual(Array.from(RedBlackTree.findAll(new Key(1, "0"))(tree)), ["a", "e", "f"]) + deepStrictEqual( + Array.from(RedBlackTree.values(RedBlackTree.removeFirst(new Key(1, "1"))(tree))), + [ + "f", + "e", + "b", + "a", + "c", + "d" + ] + ) + deepStrictEqual( + Array.from(RedBlackTree.values(RedBlackTree.removeFirst(new Key(1, "0"))(tree))), + [ + "g", + "f", + "e", + "b", + "c", + "d" + ] + ) + }) + + it("Equal.symbol", () => { + assertTrue( + Equal.equals(RedBlackTree.empty(Num.Order), RedBlackTree.empty(Num.Order)) + ) + assertTrue( + Equal.equals( + RedBlackTree.make(Num.Order)([1, true], [2, true]), + RedBlackTree.make(Num.Order)([1, true], [2, true]) + ) + ) + }) +}) diff --git a/repos/effect/packages/effect/test/Redacted.test.ts b/repos/effect/packages/effect/test/Redacted.test.ts new file mode 100644 index 0000000..859451f --- /dev/null +++ b/repos/effect/packages/effect/test/Redacted.test.ts @@ -0,0 +1,67 @@ +import { describe, it } from "@effect/vitest" +import { assertFalse, assertTrue, strictEqual, throws } from "@effect/vitest/utils" +import { Chunk, Equal, Hash, Redacted, Secret } from "effect" + +describe("Redacted", () => { + it("chunk constructor", () => { + const redacted = Redacted.make(Chunk.fromIterable("redacted".split(""))) + assertTrue(Equal.equals(redacted, Redacted.make(Chunk.fromIterable("redacted".split(""))))) + }) + + it("value", () => { + const redacted = Redacted.make(Chunk.fromIterable("redacted".split(""))) + const value = Redacted.value(redacted) + assertTrue(Equal.equals(value, Chunk.fromIterable("redacted".split("")))) + }) + + it("pipe", () => { + const value = { asd: 123 } + const redacted = Redacted.make(value) + const extractedValue = redacted.pipe(Redacted.value) + strictEqual(value, extractedValue) + }) + + it("toString", () => { + const redacted = Redacted.make("redacted") + strictEqual(`${redacted}`, "") + }) + + it("toJSON", () => { + const redacted = Redacted.make("redacted") + strictEqual(JSON.stringify(redacted), "\"\"") + }) + + it("unsafeWipe", () => { + const redacted = Redacted.make("redacted") + assertTrue(Redacted.unsafeWipe(redacted)) + throws(() => Redacted.value(redacted), new Error("Unable to get redacted value")) + }) + + it("Equal", () => { + assertTrue(Equal.equals(Redacted.make(1), Redacted.make(1))) + assertFalse(Equal.equals(Redacted.make(1), Redacted.make(2))) + }) + + it("Hash", () => { + strictEqual(Hash.hash(Redacted.make(1)), Hash.hash(Redacted.make(1))) + assertTrue(Hash.hash(Redacted.make(1)) !== Hash.hash(Redacted.make(2))) + }) + + describe("Secret extends Redacted", () => { + it("Redacted.isRedacted", () => { + const secret = Secret.fromString("test") + assertTrue( + Redacted.isRedacted(secret) + ) + }) + it("Redacted.unsafeWipe", () => { + const secret = Secret.fromString("test") + assertTrue(Redacted.unsafeWipe(secret)) + }) + it("Redacted.value", () => { + const value = "test" + const secret = Secret.fromString(value) + strictEqual(value, Redacted.value(secret)) + }) + }) +}) diff --git a/repos/effect/packages/effect/test/Ref.test.ts b/repos/effect/packages/effect/test/Ref.test.ts new file mode 100644 index 0000000..2bf3314 --- /dev/null +++ b/repos/effect/packages/effect/test/Ref.test.ts @@ -0,0 +1,187 @@ +import { describe, it } from "@effect/vitest" +import { assertTrue, deepStrictEqual, strictEqual } from "@effect/vitest/utils" +import { Effect, Option, pipe, Readable, Ref } from "effect" + +const current = "value" +const update = "new value" + +type State = Active | Changed | Closed + +interface Active { + readonly _tag: "Active" +} + +interface Changed { + readonly _tag: "Changed" +} + +interface Closed { + readonly _tag: "Closed" +} + +export const Active: State = { _tag: "Active" } +export const Changed: State = { _tag: "Changed" } +export const Closed: State = { _tag: "Closed" } + +const isActive = (self: State): boolean => self._tag === "Active" +const isChanged = (self: State): boolean => self._tag === "Changed" +const isClosed = (self: State): boolean => self._tag === "Closed" + +describe("Ref", () => { + it.effect("implements Readable", () => + Effect.gen(function*() { + const ref = yield* Ref.make(123) + assertTrue(Readable.isReadable(ref)) + strictEqual(yield* ref, 123) + })) + + it.effect("get", () => + Effect.gen(function*() { + const result = yield* pipe(Ref.make(current), Effect.flatMap(Ref.get)) + strictEqual(result, current) + })) + it.effect("getAndSet", () => + Effect.gen(function*() { + const ref = yield* Ref.make(current) + const result1 = yield* Ref.getAndSet(ref, update) + + const result2 = yield* ref + strictEqual(result1, current) + strictEqual(result2, update) + })) + it.effect("getAndUpdate", () => + Effect.gen(function*() { + const ref = yield* Ref.make(current) + const result1 = yield* Ref.getAndUpdate(ref, () => update) + const result2 = yield* ref + strictEqual(result1, current) + strictEqual(result2, update) + })) + it.effect("getAndUpdateSome - once", () => + Effect.gen(function*() { + const ref = yield* Ref.make(Active) + const result1 = yield* Ref.getAndUpdateSome( + ref, + (state) => isClosed(state) ? Option.some(Changed) : Option.none() + ) + const result2 = yield* Ref.get(ref) + strictEqual(result1, Active) + strictEqual(result2, Active) + })) + it.effect("getAndUpdateSome - twice", () => + Effect.gen(function*() { + const ref = yield* Ref.make(Active) + const result1 = yield* Ref.getAndUpdateSome( + ref, + (state) => isActive(state) ? Option.some(Changed) : Option.none() + ) + const result2 = yield* Ref.getAndUpdateSome(ref, (state) => + isActive(state) ? + Option.some(Changed) : + isChanged(state) ? + Option.some(Closed) : + Option.none()) + const result3 = yield* Ref.get(ref) + strictEqual(result1, Active) + strictEqual(result2, Changed) + strictEqual(result3, Closed) + })) + it.effect("set", () => + Effect.gen(function*() { + const ref = yield* Ref.make(current) + yield* Ref.set(ref, update) + const result = yield* Ref.get(ref) + strictEqual(result, update) + })) + it.effect("update", () => + Effect.gen(function*() { + const ref = yield* Ref.make(current) + yield* Ref.update(ref, () => update) + const result = yield* Ref.get(ref) + strictEqual(result, update) + })) + it.effect("updateAndGet", () => + Effect.gen(function*() { + const ref = yield* Ref.make(current) + const result = yield* Ref.updateAndGet(ref, () => update) + strictEqual(result, update) + })) + it.effect("updateSome - once", () => + Effect.gen(function*() { + const ref = yield* Ref.make(Active) + yield* Ref.updateSome(ref, (state) => isClosed(state) ? Option.some(Changed) : Option.none()) + const result = yield* Ref.get(ref) + deepStrictEqual(result, Active) + })) + it.effect("updateSome - twice", () => + Effect.gen(function*() { + const ref = yield* Ref.make(Active) + yield* Ref.updateSome(ref, (state) => isActive(state) ? Option.some(Changed) : Option.none()) + const result1 = yield* Ref.get(ref) + yield* Ref.updateSome(ref, (state) => + isActive(state) ? + Option.some(Changed) : + isChanged(state) ? + Option.some(Closed) : + Option.none()) + const result2 = yield* Ref.get(ref) + deepStrictEqual(result1, Changed) + deepStrictEqual(result2, Closed) + })) + it.effect("updateSomeAndGet - once", () => + Effect.gen(function*() { + const ref = yield* Ref.make(Active) + const result = yield* Ref.updateSomeAndGet(ref, (state) => isClosed(state) ? Option.some(Changed) : Option.none()) + strictEqual(result, Active) + })) + it.effect("updateSomeAndGet - twice", () => + Effect.gen(function*() { + const ref = yield* Ref.make(Active) + const result1 = yield* Ref.updateSomeAndGet( + ref, + (state) => isActive(state) ? Option.some(Changed) : Option.none() + ) + const result2 = yield* Ref.updateSomeAndGet(ref, (state): Option.Option => { + return isActive(state) ? + Option.some(Changed) : + isChanged(state) ? + Option.some(Closed) : + Option.none() + }) + deepStrictEqual(result1, Changed) + deepStrictEqual(result2, Closed) + })) + it.effect("modify", () => + Effect.gen(function*() { + const ref = yield* Ref.make(current) + const result1 = yield* Ref.modify(ref, () => ["hello", update]) + const result2 = yield* Ref.get(ref) + strictEqual(result1, "hello") + strictEqual(result2, update) + })) + it.effect("modifySome - once", () => + Effect.gen(function*() { + const ref = yield* Ref.make(Active) + const result = yield* Ref.modifySome(ref, "state does not change", (state) => + isClosed(state) ? + Option.some(["active", Active]) : + Option.none()) + strictEqual(result, "state does not change") + })) + it.effect("modifySome - twice", () => + Effect.gen(function*() { + const ref = yield* Ref.make(Active) + const result1 = yield* Ref.modifySome(ref, "state does not change", (state) => + isActive(state) ? + Option.some(["changed", Changed]) : + Option.none()) + const result2 = yield* Ref.modifySome(ref, "state does not change", (state) => + isActive(state) ? + Option.some(["changed", Changed]) : + isChanged(state) ? + Option.some(["closed", Closed]) : + Option.none()) + strictEqual(result1, "changed") + strictEqual(result2, "closed") + })) +}) diff --git a/repos/effect/packages/effect/test/RegExp.test.ts b/repos/effect/packages/effect/test/RegExp.test.ts new file mode 100644 index 0000000..1554441 --- /dev/null +++ b/repos/effect/packages/effect/test/RegExp.test.ts @@ -0,0 +1,39 @@ +import { describe, it } from "@effect/vitest" +import { assertFalse, assertTrue, strictEqual } from "@effect/vitest/utils" +import { RegExp } from "effect" + +describe("RegExp", () => { + it("isRegExp", () => { + assertTrue(RegExp.isRegExp(/a/)) + assertFalse(RegExp.isRegExp(null)) + assertFalse(RegExp.isRegExp("a")) + }) + + describe("escape", () => { + it("should escape special characters correctly", () => { + const testCases: Array<[string, string]> = [ + ["abc", "abc"], + ["a*b", "a\\*b"], + ["a.b", "a\\.b"], + ["a|b", "a\\|b"], + ["a?b", "a\\?b"], + ["a+b", "a\\+b"], + ["a(b", "a\\(b"], + ["a)b", "a\\)b"], + ["a[b", "a\\[b"], + ["a]b", "a\\]b"], + ["a{b", "a\\{b"], + ["a}b", "a\\}b"], + ["a^b", "a\\^b"], + ["a$b", "a\\$b"], + ["a\\b", "a\\\\b"], + ["a/b", "a\\/b"] + ] + + testCases.forEach(([input, expected]) => { + const result = RegExp.escape(input) + strictEqual(result, expected) + }) + }) + }) +}) diff --git a/repos/effect/packages/effect/test/Reloadable.test.ts b/repos/effect/packages/effect/test/Reloadable.test.ts new file mode 100644 index 0000000..a3a256b --- /dev/null +++ b/repos/effect/packages/effect/test/Reloadable.test.ts @@ -0,0 +1,40 @@ +import { describe, it } from "@effect/vitest" +import { strictEqual } from "@effect/vitest/utils" +import { Context, Effect, Layer, pipe, Reloadable } from "effect" +import * as Counter from "./utils/counter.js" + +const DummyServiceTypeId = Symbol.for("effect/test/Reloadable/DummyService") +type DummyServiceTypeId = typeof DummyServiceTypeId + +interface DummyService { + readonly [DummyServiceTypeId]: DummyServiceTypeId +} + +const DummyService: DummyService = { + [DummyServiceTypeId]: DummyServiceTypeId +} + +const Tag = Context.GenericTag("DummyService") + +describe("Reloadable", () => { + it.effect("initialization", () => + Effect.gen(function*() { + const counter = yield* Counter.make() + const layer = Reloadable.manual(Tag, { + layer: Layer.scoped(Tag, pipe(counter.acquire(), Effect.as(DummyService))) + }) + yield* pipe(Reloadable.get(Tag), Effect.provide(layer)) + const acquired = yield* counter.acquired() + strictEqual(acquired, 1) + })) + it.effect("reload", () => + Effect.gen(function*() { + const counter = yield* Counter.make() + const layer = Reloadable.manual(Tag, { + layer: Layer.scoped(Tag, pipe(counter.acquire(), Effect.as(DummyService))) + }) + yield* pipe(Reloadable.reload(Tag), Effect.provide(layer)) + const acquired = yield* counter.acquired() + strictEqual(acquired, 2) + })) +}) diff --git a/repos/effect/packages/effect/test/Resource.test.ts b/repos/effect/packages/effect/test/Resource.test.ts new file mode 100644 index 0000000..12f5257 --- /dev/null +++ b/repos/effect/packages/effect/test/Resource.test.ts @@ -0,0 +1,54 @@ +import { describe, it } from "@effect/vitest" +import { strictEqual } from "@effect/vitest/utils" +import { Duration, Effect, Either, identity, pipe, Ref, Resource, Schedule } from "effect" +import * as TestClock from "effect/TestClock" + +describe("Resource", () => { + it.scoped("manual", () => + Effect.gen(function*() { + const ref = yield* Ref.make(0) + const cached = yield* Resource.manual(Ref.get(ref)) + const resul1 = yield* Resource.get(cached) + const result2 = yield* pipe( + Ref.set(ref, 1), + Effect.zipRight(Resource.refresh(cached)), + Effect.zipRight(Resource.get(cached)) + ) + strictEqual(resul1, 0) + strictEqual(result2, 1) + })) + it.scoped("auto", () => + Effect.gen(function*() { + const ref = yield* Ref.make(0) + const cached = yield* Resource.auto(Ref.get(ref), Schedule.spaced(Duration.millis(4))) + const result1 = yield* Resource.get(cached) + const result2 = yield* pipe( + Ref.set(ref, 1), + Effect.zipRight(TestClock.adjust(Duration.millis(5))), + Effect.zipRight(Resource.get(cached)) + ) + strictEqual(result1, 0) + strictEqual(result2, 1) + })) + it.scopedLive("failed refresh doesn't affect cached value", () => + Effect.gen(function*() { + const ref = yield* Ref.make>(Either.right(0)) + const cached = yield* Resource.auto(Effect.flatMap(Ref.get(ref), identity), Schedule.spaced(Duration.millis(4))) + const result1 = yield* Resource.get(cached) + const result2 = yield* pipe( + Ref.set(ref, Either.left("Uh oh!")), + Effect.zipRight(Effect.sleep(Duration.millis(5))), + Effect.zipRight(Resource.get(cached)) + ) + strictEqual(result1, 0) + strictEqual(result2, 0) + })) + it.scoped("subtype of Effect", () => + Effect.gen(function*() { + const ref = yield* Ref.make(0) + const cached = yield* Resource.manual(ref) + const resul1 = yield* cached + + strictEqual(resul1, 0) + })) +}) diff --git a/repos/effect/packages/effect/test/Runtime.test.ts b/repos/effect/packages/effect/test/Runtime.test.ts new file mode 100644 index 0000000..196a5ce --- /dev/null +++ b/repos/effect/packages/effect/test/Runtime.test.ts @@ -0,0 +1,175 @@ +import { AsyncLocalStorage } from "node:async_hooks" + +import { describe, it } from "@effect/vitest" +import { assertTrue, deepStrictEqual, strictEqual, throwsAsync } from "@effect/vitest/utils" +import { Effect, Exit, FiberRef, Layer, pipe, Runtime } from "effect" + +describe("Runtime", () => { + it.effect("setFiberRef", () => + Effect.gen(function*() { + const ref = FiberRef.unsafeMake(0) + const runtime = Runtime.defaultRuntime.pipe( + Runtime.setFiberRef(ref, 1) + ) + let result = Runtime.runSync(runtime)(FiberRef.get(ref)) + strictEqual(result, 1) + + result = yield* pipe(FiberRef.get(ref), Effect.provide(runtime)) + strictEqual(result, 1) + })) + + it.scoped("deleteFiberRef", () => + Effect.gen(function*() { + const ref = FiberRef.unsafeMake({ value: 0 }) + const runtime = yield* (Layer.toRuntime(Layer.effectDiscard(FiberRef.set(ref, { value: 1 })))) + + let result = Runtime.runSync(runtime)(FiberRef.get(ref)) + deepStrictEqual(result, { value: 1 }) + + result = Runtime.runSync(Runtime.deleteFiberRef(runtime, ref))(FiberRef.get(ref)) + deepStrictEqual(result, { value: 0 }) + })) + + it("runSync", () => { + deepStrictEqual(Runtime.runSync(Runtime.defaultRuntime)(Effect.succeed(1)), 1) + deepStrictEqual(Runtime.runSync(Runtime.defaultRuntime, Effect.succeed(1)), 1) + }) + + it("runSyncExit", () => { + deepStrictEqual(Runtime.runSyncExit(Runtime.defaultRuntime)(Effect.succeed(1)), Exit.succeed(1)) + deepStrictEqual(Runtime.runSyncExit(Runtime.defaultRuntime, Effect.succeed(1)), Exit.succeed(1)) + + deepStrictEqual(Runtime.runSyncExit(Runtime.defaultRuntime)(Effect.fail(1)), Exit.fail(1)) + deepStrictEqual(Runtime.runSyncExit(Runtime.defaultRuntime, Effect.fail(1)), Exit.fail(1)) + }) + + it("runPromise", async () => { + deepStrictEqual( + await Runtime.runPromise(Runtime.defaultRuntime)(Effect.promise(async () => 1)), + 1 + ) + throwsAsync( + async () => { + await Runtime.runPromise(Runtime.defaultRuntime)( + Effect.tryPromise({ try: () => new Promise((_, reject) => reject(1)), catch: () => "error" }) + ) + } + ) + + deepStrictEqual( + await Runtime.runPromise(Runtime.defaultRuntime, Effect.promise(async () => 1)), + 1 + ) + throwsAsync( + async () => { + await Runtime.runPromise( + Runtime.defaultRuntime, + Effect.tryPromise({ try: () => new Promise((_, reject) => reject(1)), catch: () => "error" }) + ) + } + ) + }) + + it.effect("runPromise isolates AsyncLocalStorage across concurrent calls", () => + Effect.gen(function*() { + type RequestStore = { + readonly userId: string + } + + type Result = { + readonly expected: string + readonly observed: string + } + + const requestStore = new AsyncLocalStorage() + + const readAlsOnScheduledContinuation = Effect.withFiberRuntime((fiber) => + Effect.sync(() => requestStore.getStore()?.userId ?? "NONE").pipe( + Effect.flatMap((expected) => + Effect.async((resume) => { + fiber.currentScheduler.scheduleTask( + () => { + resume( + Effect.succeed({ + expected, + observed: requestStore.getStore()?.userId ?? "NONE" + }) + ) + }, + 0, + fiber + ) + }) + ) + ) + ) + + const runtime = yield* Effect.runtime() + + const runRequest = (userId: string): Promise => + requestStore.run( + { userId }, + () => Runtime.runPromise(runtime)(readAlsOnScheduledContinuation) + ) + + const results = yield* Effect.promise(() => + Promise.all([ + runRequest("user-A"), + runRequest("user-B") + ]) + ) + + deepStrictEqual(results, [ + { expected: "user-A", observed: "user-A" }, + { expected: "user-B", observed: "user-B" } + ]) + })) + + it("runPromiseExit", async () => { + deepStrictEqual( + await Runtime.runPromiseExit(Runtime.defaultRuntime)(Effect.promise(async () => 1)), + Exit.succeed(1) + ) + deepStrictEqual( + await Runtime.runPromiseExit(Runtime.defaultRuntime)( + Effect.tryPromise({ try: () => new Promise((_, reject) => reject(1)), catch: () => "error" }) + ), + Exit.fail("error") + ) + + deepStrictEqual( + await Runtime.runPromiseExit(Runtime.defaultRuntime, Effect.promise(async () => 1)), + Exit.succeed(1) + ) + deepStrictEqual( + await Runtime.runPromiseExit( + Runtime.defaultRuntime, + Effect.tryPromise({ try: () => new Promise((_, reject) => reject(1)), catch: () => "error" }) + ), + Exit.fail("error") + ) + }) + + it("runPromiseExit/signal", async () => { + const aborted = AbortSignal.abort() + assertTrue( + Exit.isInterrupted(await Runtime.runPromiseExit(Runtime.defaultRuntime)(Effect.never, { signal: aborted })) + ) + assertTrue( + Exit.isInterrupted(await Runtime.runPromiseExit(Runtime.defaultRuntime, Effect.never, { signal: aborted })) + ) + + const controller = new AbortController() + setTimeout(() => controller.abort(), 10) + assertTrue( + Exit.isInterrupted( + await Runtime.runPromiseExit(Runtime.defaultRuntime)(Effect.never, { signal: controller.signal }) + ) + ) + assertTrue( + Exit.isInterrupted( + await Runtime.runPromiseExit(Runtime.defaultRuntime, Effect.never, { signal: controller.signal }) + ) + ) + }) +}) diff --git a/repos/effect/packages/effect/test/RuntimeFlags.test.ts b/repos/effect/packages/effect/test/RuntimeFlags.test.ts new file mode 100644 index 0000000..5f51d88 --- /dev/null +++ b/repos/effect/packages/effect/test/RuntimeFlags.test.ts @@ -0,0 +1,110 @@ +import { describe, it } from "@effect/vitest" +import { assertFalse, assertTrue, strictEqual } from "@effect/vitest/utils" +import { FastCheck as fc, pipe, RuntimeFlags, RuntimeFlagsPatch } from "effect" + +const arbRuntimeFlag = fc.constantFrom( + RuntimeFlags.None, + RuntimeFlags.Interruption, + RuntimeFlags.OpSupervision, + RuntimeFlags.RuntimeMetrics, + RuntimeFlags.WindDown, + RuntimeFlags.CooperativeYielding +) + +const arbRuntimeFlags = fc.uniqueArray(arbRuntimeFlag).map( + (flags) => RuntimeFlags.make(...flags) +) + +describe("RuntimeFlags", () => { + it("isDisabled & isEnabled", () => { + const flags = RuntimeFlags.make( + RuntimeFlags.RuntimeMetrics, + RuntimeFlags.Interruption + ) + assertTrue(RuntimeFlags.isEnabled(flags, RuntimeFlags.RuntimeMetrics)) + assertTrue(RuntimeFlags.isEnabled(flags, RuntimeFlags.Interruption)) + assertFalse(RuntimeFlags.isEnabled(flags, RuntimeFlags.CooperativeYielding)) + assertFalse(RuntimeFlags.isEnabled(flags, RuntimeFlags.OpSupervision)) + assertFalse(RuntimeFlags.isEnabled(flags, RuntimeFlags.WindDown)) + }) + + it("enabled patching", () => { + const patch = pipe( + RuntimeFlagsPatch.enable(RuntimeFlags.RuntimeMetrics), + RuntimeFlagsPatch.andThen(RuntimeFlagsPatch.enable(RuntimeFlags.OpSupervision)) + ) + const result = RuntimeFlags.patch(RuntimeFlags.none, patch) + + const expected = RuntimeFlags.make( + RuntimeFlags.RuntimeMetrics, + RuntimeFlags.OpSupervision + ) + strictEqual(result, expected) + }) + + it("inverse patching", () => { + const flags = RuntimeFlags.make( + RuntimeFlags.RuntimeMetrics, + RuntimeFlags.OpSupervision + ) + const patch1 = pipe( + RuntimeFlagsPatch.enable(RuntimeFlags.RuntimeMetrics), + RuntimeFlagsPatch.inverse + ) + const patch2 = pipe( + RuntimeFlagsPatch.enable(RuntimeFlags.RuntimeMetrics), + RuntimeFlagsPatch.andThen(RuntimeFlagsPatch.enable(RuntimeFlags.OpSupervision)), + RuntimeFlagsPatch.inverse + ) + strictEqual( + RuntimeFlags.patch(flags, patch1), + RuntimeFlags.make(RuntimeFlags.OpSupervision) + ) + strictEqual( + RuntimeFlags.patch(flags, patch2), + RuntimeFlags.none + ) + }) + + it("diff", () => { + const flags1 = RuntimeFlags.make(RuntimeFlags.RuntimeMetrics) + const flags2 = RuntimeFlags.make(RuntimeFlags.RuntimeMetrics, RuntimeFlags.OpSupervision) + strictEqual( + RuntimeFlags.diff(flags1, flags2), + RuntimeFlagsPatch.enable(RuntimeFlags.OpSupervision) + ) + }) + + it("flags within a set of RuntimeFlags is enabled", () => { + fc.assert(fc.property(arbRuntimeFlags, (flags) => { + const result = Array.from(RuntimeFlags.toSet(flags)).every( + (flag) => RuntimeFlags.isEnabled(flags, flag) + ) + assertTrue(result) + })) + }) + + it("patching a diff between `none` and a set of flags is an identity", () => { + fc.assert(fc.property(arbRuntimeFlags, (flags) => { + const diff = RuntimeFlags.diff(RuntimeFlags.none, flags) + strictEqual( + RuntimeFlags.patch(RuntimeFlags.none, diff), + flags + ) + })) + }) + + it("patching the inverse diff between `non` and a set of flags is `none`", () => { + fc.assert(fc.property(arbRuntimeFlags, (flags) => { + const diff = RuntimeFlags.diff(RuntimeFlags.none, flags) + strictEqual( + RuntimeFlags.patch(flags, RuntimeFlagsPatch.inverse(diff)), + RuntimeFlags.none + ) + strictEqual( + RuntimeFlags.patch(flags, RuntimeFlagsPatch.inverse(RuntimeFlagsPatch.inverse(diff))), + flags + ) + })) + }) +}) diff --git a/repos/effect/packages/effect/test/STM.test.ts b/repos/effect/packages/effect/test/STM.test.ts new file mode 100644 index 0000000..d316b1a --- /dev/null +++ b/repos/effect/packages/effect/test/STM.test.ts @@ -0,0 +1,1585 @@ +import { describe, it } from "@effect/vitest" +import { + assertFailure, + assertFalse, + assertLeft, + assertNone, + assertRight, + assertSome, + assertTrue, + deepStrictEqual, + strictEqual +} from "@effect/vitest/utils" +import { + Cause, + Chunk, + Context, + Deferred, + Effect, + Either, + Exit, + FastCheck as fc, + Fiber, + Option, + pipe, + STM, + TDeferred, + TQueue, + TRef +} from "effect" +import { constFalse, constTrue, constVoid } from "effect/Function" + +interface STMEnv { + readonly ref: TRef.TRef +} + +const STMEnv = Context.GenericTag("STMEnv") + +const makeSTMEnv = (n: number): Effect.Effect => + pipe( + TRef.make(n), + Effect.map((ref) => ({ ref })) + ) + +class UnpureBarrier { + #isOpen = false + open(): void { + this.#isOpen = true + } + await(): Effect.Effect { + return Effect.async((cb) => { + const check = () => { + if (this.#isOpen) { + cb(Effect.void) + } else { + setTimeout(() => { + check() + }, 100) + } + } + setTimeout(check, 100) + }) + } +} + +const chain = (depth: number) => +( + next: (stm: STM.STM) => STM.STM +): Effect.Effect => { + const loop = (_n: number, _acc: STM.STM): Effect.Effect => { + let n = _n + let acc = _acc + while (n > 0) { + acc = next(acc) + n = n - 1 + } + return STM.commit(acc) + } + return loop(depth, STM.succeed(0)) +} + +const chainError = (depth: number): Effect.Effect => { + const loop = (_n: number, _acc: STM.STM): Effect.Effect => { + let n = _n + let acc = _acc + while (n > 0) { + acc = pipe(acc, STM.mapError((n) => n + 1)) + n = n - 1 + } + return STM.commit(acc) + } + return loop(depth, STM.fail(0)) +} + +const incrementTRefN = (n: number, ref: TRef.TRef): Effect.Effect => + pipe( + TRef.get(ref), + STM.tap((n) => pipe(ref, TRef.set(n + 1))), + STM.zipRight(TRef.get(ref)), + STM.commit, + Effect.repeatN(n) + ) + +const transfer = ( + receiver: TRef.TRef, + sender: TRef.TRef, + much: number +): Effect.Effect => + pipe( + TRef.get(sender), + STM.tap((balance) => STM.check(() => balance >= much)), + STM.tap(() => pipe(receiver, TRef.update((n) => n + much))), + STM.tap(() => pipe(sender, TRef.update((n) => n - much))), + STM.zipRight(TRef.get(receiver)), + STM.commit + ) + +const compute3TRefN = ( + n: number, + ref1: TRef.TRef, + ref2: TRef.TRef, + ref3: TRef.TRef +): Effect.Effect => + pipe( + STM.all([TRef.get(ref1), TRef.get(ref2)]), + STM.tap(([v1, v2]) => pipe(ref3, TRef.set(v1 + v2))), + STM.flatMap(([v1, v2]) => + pipe( + TRef.get(ref3), + STM.flatMap((v3) => + pipe( + ref1, + TRef.set(v1 - 1), + STM.zipRight(pipe(ref2, TRef.set(v2 + 1))), + STM.as(v3) + ) + ) + ) + ), + STM.commit, + Effect.repeatN(n) + ) + +const permutation = (ref1: TRef.TRef, ref2: TRef.TRef): STM.STM => + pipe( + STM.all([TRef.get(ref1), TRef.get(ref2)]), + STM.flatMap(([a, b]) => + pipe( + ref1, + TRef.set(b), + STM.tap(() => pipe(ref2, TRef.set(a))), + STM.asVoid + ) + ) + ) + +describe("STM", () => { + it.effect("catchAll", () => + Effect.gen(function*() { + const transaction = pipe( + STM.fail("Ouch!"), + STM.tap(() => STM.succeed("everything is fine")), + STM.catchAll((s) => STM.succeed(`${s} phew`)) + ) + const result = yield* STM.commit(transaction) + deepStrictEqual(result, "Ouch! phew") + })) + + it.effect("collectAll - ordering", () => + Effect.gen(function*() { + const transaction = pipe( + TQueue.bounded(3), + STM.tap((queue) => pipe(queue, TQueue.offer(1))), + STM.tap((queue) => pipe(queue, TQueue.offer(2))), + STM.tap((queue) => pipe(queue, TQueue.offer(3))), + STM.flatMap((queue) => STM.all(Array.from({ length: 3 }, () => TQueue.take(queue)))) + ) + const result = yield* (STM.commit(transaction)) + deepStrictEqual(Array.from(result), [1, 2, 3]) + })) + + it.effect("catchSome - catches matched errors", () => + Effect.gen(function*() { + const transaction = pipe( + STM.fail(new Cause.RuntimeException("Ouch")), + STM.tap(() => STM.succeed("everything is fine")), + STM.catchSome((e) => + Cause.isRuntimeException(e) ? + Option.some(STM.succeed("gotcha")) : + Option.none() + ) + ) + const result = yield* (STM.commit(transaction)) + deepStrictEqual(result, "gotcha") + })) + + it.effect("catchSome - lets the error pass", () => + Effect.gen(function*() { + const error = new Cause.RuntimeException("Ouch") + const transaction = pipe( + STM.fail(error), + STM.tap(() => STM.succeed("everything is fine")), + STM.catchSome((e) => + Cause.isIllegalArgumentException(e) ? + Option.some(STM.succeed("gotcha")) : + Option.none() + ) + ) + const result = yield* (Effect.exit(STM.commit(transaction))) + deepStrictEqual(result, Exit.fail(error)) + })) + + it.effect("collectAll - collects a list of transactional effects to a single transaction", () => + Effect.gen(function*() { + const chunk: Chunk.Chunk = Chunk.range(1, 100) + const iterable = yield* (Effect.succeed(pipe(chunk, Chunk.map(TRef.make)))) + const refs = yield* (STM.all(iterable)) + const result = yield* ( + Effect.forEach(refs, TRef.get, { + concurrency: "unbounded" + }) + ) + deepStrictEqual(Array.from(result), Array.from(chunk)) + })) + + it.effect("either - convert a successful computation into a Right", () => + Effect.gen(function*() { + const transaction = STM.either(STM.succeed(42)) + const result = yield* (STM.commit(transaction)) + deepStrictEqual(result, Either.right(42)) + })) + + it.effect("either - convert a failed computation into a Left", () => + Effect.gen(function*() { + const transaction = STM.either(STM.fail("Ouch")) + const result = yield* (STM.commit(transaction)) + deepStrictEqual(result, Either.left("Ouch")) + })) + + it.effect("environment - access and provide outside transaction", () => + Effect.gen(function*() { + const result = yield* (pipe( + makeSTMEnv(0), + Effect.flatMap((env) => + pipe( + STM.flatMap(STMEnv, (env) => pipe(env.ref, TRef.update((n) => n + 1))), + STM.provideContext(Context.make(STMEnv, env)), + STM.commit, + Effect.zipRight(TRef.get(env.ref)) + ) + ) + )) + deepStrictEqual(result, 1) + })) + + it.effect("environment - access and provide inside transaction", () => + Effect.gen(function*() { + const result = yield* (pipe( + makeSTMEnv(0), + Effect.flatMap((env) => + pipe( + STM.flatMap(STMEnv, (env) => pipe(env.ref, TRef.update((n) => n + 1))), + STM.provideContext(Context.make(STMEnv, env)), + STM.zipRight(TRef.get(env.ref)) + ) + ) + )) + deepStrictEqual(result, 1) + })) + + it.effect("eventually - succeeds", () => + Effect.gen(function*() { + const f = (ref: TRef.TRef) => + STM.gen(function*() { + const n = yield* TRef.get(ref) + return yield* n < 10 ? + pipe(ref, TRef.update((n) => n + 1), STM.zipRight(STM.fail("Ouch"))) : + STM.succeed(n) + }) + const transaction = pipe( + TRef.make(0), + STM.flatMap((ref) => STM.eventually(f(ref))) + ) + const result = yield* (STM.commit(transaction)) + strictEqual(result, 10) + })) + + it.effect("fail", () => + Effect.gen(function*() { + const result = yield* (Effect.exit(STM.commit(STM.fail("Ouch")))) + deepStrictEqual(result, Exit.fail("Ouch")) + })) + + it.effect("filter - filters a collection using an effectual predicate", () => + Effect.gen(function*() { + const array = [2, 4, 6, 3, 5, 6] + const transaction = STM.gen(function*() { + const ref = yield* (TRef.make(Chunk.empty())) + const results = yield* (pipe( + array, + STM.filter((n) => pipe(ref, TRef.update(Chunk.append(n)), STM.as(n % 2 === 0))) + )) + const effects = yield* (TRef.get(ref)) + return { results, effects } + }) + const { effects, results } = yield* (STM.commit(transaction)) + deepStrictEqual(Array.from(results), [2, 4, 6, 6]) + deepStrictEqual(Array.from(effects), array) + })) + + it.effect("filterOrDie - dies when predicate fails", () => + Effect.gen(function*() { + const error = new Cause.RuntimeException("Ouch") + const transaction = pipe( + STM.succeed(1), + STM.filterOrDie((n) => n !== 1, () => error) + ) + const result = yield* (Effect.exit(STM.commit(transaction))) + deepStrictEqual(result, Exit.die(error)) + })) + + it.effect("filterOrDieMessage - dies with message when predicate fails", () => + Effect.gen(function*() { + const transaction = pipe( + STM.succeed(1), + STM.filterOrDieMessage((n) => n !== 1, "Ouch") + ) + const result = yield* (Effect.exit(STM.commit(transaction))) + deepStrictEqual(result, Exit.die(new Cause.RuntimeException("Ouch"))) + })) + + it.effect("filterOrElse - returns checked failure", () => + Effect.gen(function*() { + const transaction = pipe( + STM.succeed(1), + STM.filterOrElse((n) => n === 1, () => STM.succeed(2)) + ) + const result = yield* (STM.commit(transaction)) + strictEqual(result, 1) + })) + + it.effect("filterOrElse - returns held value", () => + Effect.gen(function*() { + const transaction = pipe( + STM.succeed(1), + STM.filterOrElse((n) => n !== 1, () => STM.succeed(2)) + ) + const result = yield* (STM.commit(transaction)) + strictEqual(result, 2) + })) + + it.effect("filterOrElse - returns checked failure", () => + Effect.gen(function*() { + const transaction = pipe( + STM.succeed(1), + STM.filterOrElse((n) => n === 1, (n) => STM.succeed(n + 1)) + ) + const result = yield* (STM.commit(transaction)) + strictEqual(result, 1) + })) + + it.effect("filterOrElse - returns held value", () => + Effect.gen(function*() { + const transaction = pipe( + STM.succeed(1), + STM.filterOrElse((n) => n !== 1, (n) => STM.succeed(n + 1)) + ) + const result = yield* (STM.commit(transaction)) + strictEqual(result, 2) + })) + + it.effect("filterOrElse - returns error", () => + Effect.gen(function*() { + const error = new Cause.RuntimeException("Ouch") + const transaction = pipe( + STM.fail(error), + STM.zipRight(STM.succeed(1)), + STM.filterOrElse((n) => n === 1, (n) => STM.succeed(n + 1)) + ) + const result = yield* (Effect.exit(STM.commit(transaction))) + deepStrictEqual(result, Exit.fail(error)) + })) + + it.effect("filterOrFail - returns failure when predicate fails", () => + Effect.gen(function*() { + const error = new Cause.RuntimeException("Ouch") + const transaction = pipe( + STM.succeed(1), + STM.filterOrFail((n) => n !== 1, () => error) + ) + const result = yield* (Effect.exit(STM.commit(transaction))) + deepStrictEqual(result, Exit.fail(error)) + })) + + it.effect("flatten", () => + Effect.gen(function*() { + const transaction = STM.flatten(STM.succeed(STM.succeed("test"))) + const result = yield* (STM.commit(transaction)) + strictEqual(result, "test") + })) + + it.effect("forEach - performs an action on each chunk element and return a single transaction", () => + Effect.gen(function*() { + const ref = yield* (TRef.make(0)) + const chunk = Chunk.range(1, 5) + yield* (pipe(chunk, STM.forEach((n) => pipe(ref, TRef.update((i) => i + n))))) + const expected = pipe(chunk, Chunk.reduceRight(0, (acc, curr) => acc + curr)) + const result = yield* (TRef.get(ref)) + strictEqual(result, expected) + })) + + it.effect("forEach - performs an action on each chunk element", () => + Effect.gen(function*() { + const ref = yield* (TRef.make(0)) + const chunk = Chunk.range(1, 5) + yield* (STM.forEach(chunk, (n) => pipe(ref, TRef.update((i) => i + n)), { discard: true })) + const expected = pipe(chunk, Chunk.reduceRight(0, (acc, curr) => acc + curr)) + const result = yield* (TRef.get(ref)) + strictEqual(result, expected) + })) + + it.effect("fold - handles both failure and success", () => + Effect.gen(function*() { + const transaction = STM.all({ + success: pipe(STM.succeed("yes"), STM.match({ onFailure: () => -1, onSuccess: () => 1 })), + failure: pipe(STM.fail("no"), STM.match({ onFailure: () => -1, onSuccess: () => 1 })) + }) + const { failure, success } = yield* (STM.commit(transaction)) + strictEqual(success, 1) + strictEqual(failure, -1) + })) + + it.effect("foldSTM - folds over the `STM` effect, and handle failure and success", () => + Effect.gen(function*() { + const transaction = STM.all({ + success: pipe(STM.succeed("yes"), STM.matchSTM({ onFailure: () => STM.succeed("no"), onSuccess: STM.succeed })), + failure: pipe(STM.fail("no"), STM.matchSTM({ onFailure: STM.succeed, onSuccess: () => STM.succeed("yes") })) + }) + const { failure, success } = yield* (STM.commit(transaction)) + strictEqual(failure, "no") + strictEqual(success, "yes") + })) + + it.effect("head - extracts the first value from an iterable", () => + Effect.gen(function*() { + const transaction = STM.head(STM.succeed([1, 2])) + const result = yield* (STM.commit(transaction)) + strictEqual(result, 1) + })) + + it.effect("head - returns None if the iterable is empty", () => + Effect.gen(function*() { + const transaction = STM.head(STM.succeed([])) + const result = yield* (Effect.exit(STM.commit(transaction))) + assertFailure(result, Cause.fail(Option.none())) + })) + + it.effect("head - returns Some if there is an error", () => + Effect.gen(function*() { + const transaction = STM.head(STM.fail("Ouch")) + const result = yield* (Effect.exit(STM.commit(transaction))) + assertFailure(result, Cause.fail(Option.some("Ouch"))) + })) + + it.effect("if - runs `onTrue` if result is `true`", () => + Effect.gen(function*() { + const transaction = pipe( + STM.succeed(true), + STM.if({ + onFalse: STM.succeed(-1), + onTrue: STM.succeed(1) + }) + ) + const result = yield* (STM.commit(transaction)) + strictEqual(result, 1) + })) + + it.effect("if - runs `onFalse` if result is `false`", () => + Effect.gen(function*() { + const transaction = pipe( + STM.succeed(false), + STM.if({ + onFalse: STM.succeed(-1), + onTrue: STM.succeed(1) + }) + ) + const result = yield* (STM.commit(transaction)) + strictEqual(result, -1) + })) + + it.effect("mapBoth - success value", () => + Effect.gen(function*() { + const transaction = pipe(STM.succeed(1), STM.mapBoth({ onFailure: () => -1, onSuccess: (n) => `${n} as string` })) + const result = yield* (STM.commit(transaction)) + strictEqual(result, "1 as string") + })) + + it.effect("mapBoth - success value", () => + Effect.gen(function*() { + const transaction = pipe(STM.fail(-1), STM.mapBoth({ onFailure: (n) => `${n} as string`, onSuccess: () => 0 })) + const result = yield* (Effect.exit(STM.commit(transaction))) + deepStrictEqual(result, Exit.fail("-1 as string")) + })) + + it.effect("mapError - map from one error to another", () => + Effect.gen(function*() { + const transaction = pipe(STM.fail(-1), STM.mapError(() => "Ouch")) + const result = yield* (Effect.exit(STM.commit(transaction))) + deepStrictEqual(result, Exit.fail("Ouch")) + })) + + it.effect("merge - on error", () => + Effect.gen(function*() { + const transaction = STM.merge(STM.fromEither(Either.left(1))) + const result = yield* (STM.commit(transaction)) + strictEqual(result, 1) + })) + + it.effect("merge - on success", () => + Effect.gen(function*() { + const transaction = STM.merge(STM.fromEither(Either.right(1))) + const result = yield* (STM.commit(transaction)) + strictEqual(result, 1) + })) + + it.effect("mergeAll - return zero element on empty input", () => + Effect.gen(function*() { + const transaction = pipe( + Chunk.empty>(), + STM.mergeAll(42, () => 43) + ) + const result = yield* (STM.commit(transaction)) + strictEqual(result, 42) + })) + + it.effect("mergeAll - merge iterable using function", () => + Effect.gen(function*() { + const transaction = pipe( + [3, 5, 7].map((n) => STM.succeed(n)), + STM.mergeAll(1, (acc, curr) => acc + curr) + ) + const result = yield* (STM.commit(transaction)) + strictEqual(result, 1 + 3 + 5 + 7) + })) + + it.effect("mergeAll - return error if it exists in list", () => + Effect.gen(function*() { + const transaction = pipe( + [STM.void, STM.fail(1)] as Array>, + STM.mergeAll(void 0 as void, constVoid) + ) + const result = yield* (Effect.exit(STM.commit(transaction))) + deepStrictEqual(result, Exit.fail(1)) + })) + + it.effect("none - on None", () => + Effect.gen(function*() { + const transaction = STM.none(STM.succeed(Option.none())) + const result = yield* (STM.commit(transaction)) + strictEqual(result, undefined) + })) + + it.effect("none - on Some", () => + Effect.gen(function*() { + const transaction = STM.none(STM.succeed(Option.some(1))) + const result = yield* (Effect.exit(STM.commit(transaction))) + assertFailure(result, Cause.fail(Option.none())) + })) + + it.effect("none - on error", () => + Effect.gen(function*() { + const error = new Cause.RuntimeException("Ouch") + const transaction = STM.none(STM.fail(error)) + const result = yield* (Effect.exit(STM.commit(transaction))) + assertFailure(result, Cause.fail(Option.some(error))) + })) + + it.effect("option - success converts to Some", () => + Effect.gen(function*() { + const transaction = STM.option(STM.succeed(42)) + const result = yield* (STM.commit(transaction)) + assertSome(result, 42) + })) + + it.effect("option - failure converts to None", () => + Effect.gen(function*() { + const transaction = STM.option(STM.fail("Ouch")) + const result = yield* (STM.commit(transaction)) + assertNone(result) + })) + + it.effect("orElse - succeeds if left succeeds", () => + Effect.gen(function*() { + const left = STM.succeed("left") + const right = STM.succeed("right") + const result = yield* (STM.commit(pipe(left, STM.orElse(() => right)))) + strictEqual(result, "left") + })) + + it.effect("orElse - succeeds if right succeeds", () => + Effect.gen(function*() { + const left = STM.retry + const right = STM.succeed("right") + const result = yield* (STM.commit(pipe(left, STM.orElse(() => right)))) + strictEqual(result, "right") + })) + + it.effect("orElse - tries alternative once left retries", () => + Effect.gen(function*() { + const ref = yield* (TRef.make(0)) + const left = pipe(ref, TRef.update((n) => n + 100), STM.zipRight(STM.retry)) + const right = pipe(ref, TRef.update((n) => n + 200)) + yield* (pipe(left, STM.orElse(() => right))) + const result = yield* (TRef.get(ref)) + strictEqual(result, 200) + })) + + it.effect("orElse - tries alternative once left fails", () => + Effect.gen(function*() { + const ref = yield* (TRef.make(0)) + const left = pipe(ref, TRef.update((n) => n + 100), STM.zipRight(STM.fail("boom"))) + const right = pipe(ref, TRef.update((n) => n + 200)) + yield* (pipe(left, STM.orElse(() => right))) + const result = yield* (TRef.get(ref)) + strictEqual(result, 200) + })) + + it.effect("orElse - fail if alternative fails", () => + Effect.gen(function*() { + const left = STM.fail("left") + const right = STM.fail("right") + const result = yield* (pipe(left, STM.orElse(() => right), Effect.exit)) + assertFailure(result, Cause.fail("right")) + })) + + it.effect("orElseEither - orElseEither returns result of the first successful transaction", () => + Effect.gen(function*() { + const result1 = yield* (pipe(STM.retry, STM.orElseEither(() => STM.succeed(42)))) + const result2 = yield* (pipe(STM.succeed(1), STM.orElseEither(() => STM.succeed("no")))) + const result3 = yield* (pipe(STM.succeed(2), STM.orElseEither(() => STM.retry))) + assertRight(result1, 42) + assertLeft(result2, 1) + assertLeft(result3, 2) + })) + + it.effect("orElseFail - tries left first", () => + Effect.gen(function*() { + const transaction = pipe(STM.succeed(true), STM.orElseFail(() => false)) + const result = yield* (STM.commit(transaction)) + assertTrue(result) + })) + + it.effect("orElseFail - fails with the specified error once left retries", () => + Effect.gen(function*() { + const transaction = pipe(STM.retry, STM.orElseFail(() => false), STM.either) + const result = yield* (STM.commit(transaction)) + assertLeft(result, false) + })) + + it.effect("orElseFail - fails with the specified error once left fails", () => + Effect.gen(function*() { + const transaction = pipe(STM.fail(true), STM.orElseFail(() => false), STM.either) + const result = yield* (STM.commit(transaction)) + assertLeft(result, false) + })) + + it.effect("orElseSucceed - tries left first", () => + Effect.gen(function*() { + const transaction = pipe(STM.succeed(true), STM.orElseSucceed(() => false)) + const result = yield* (STM.commit(transaction)) + assertTrue(result) + })) + + it.effect("orElseSucceed - succeeds with the specified error once left retries", () => + Effect.gen(function*() { + const transaction = pipe(STM.retry, STM.orElseSucceed(() => false)) + const result = yield* (STM.commit(transaction)) + assertFalse(result) + })) + + it.effect("orElseSucceed - succeeds with the specified error once left fails", () => + Effect.gen(function*() { + const transaction = pipe(STM.fail(true), STM.orElseSucceed(() => false)) + const result = yield* (STM.commit(transaction)) + assertFalse(result) + })) + + it.effect("unsome - converts Some in E to error in E", () => + Effect.gen(function*() { + const transaction = STM.unsome(STM.fromEither(Either.left(Option.some("Ouch")))) + const result = yield* (Effect.exit(STM.commit(transaction))) + deepStrictEqual(result, Exit.fail("Ouch")) + })) + + it.effect("unsome - converts None in E to None in A", () => + Effect.gen(function*() { + const transaction = STM.unsome(STM.fromEither(Either.left(Option.none()))) + const result = yield* (STM.commit(transaction)) + assertNone(result) + })) + + it.effect("unsome - no error", () => + Effect.gen(function*() { + const transaction = STM.unsome(STM.fromEither(Either.right(42))) + const result = yield* (STM.commit(transaction)) + assertSome(result, 42) + })) + + it.effect("orDie - when failure should die", () => + Effect.gen(function*() { + const error = new Cause.RuntimeException("Ouch") + const transaction = STM.orDie(STM.fail(error)) + const result = yield* (Effect.exit(STM.commit(transaction))) + deepStrictEqual(result, Exit.die(error)) + })) + + it.effect("orDie - when succeed should keep going", () => + Effect.gen(function*() { + const transaction = STM.orDie(STM.succeed(1)) + const result = yield* (STM.commit(transaction)) + strictEqual(result, 1) + })) + + it.effect("orDieWith - when failure should die", () => + Effect.gen(function*() { + const error = new Cause.RuntimeException("Ouch") + const transaction = pipe(STM.fail("-1"), STM.orDieWith(() => error)) + const result = yield* (Effect.exit(STM.commit(transaction))) + deepStrictEqual(result, Exit.die(error)) + })) + + it.effect("orDieWith - when succeed should keep going", () => + Effect.gen(function*() { + const error = new Cause.RuntimeException("Ouch") + const transaction = pipe(STM.succeed(1), STM.orDieWith(() => error)) + const result = yield* (STM.commit(transaction)) + strictEqual(result, 1) + })) + + it.effect("partition - collects only successes", () => + Effect.gen(function*() { + const input = Chunk.range(0, 9) + const transaction = pipe(input, STM.partition(STM.succeed)) + const [left, right] = yield* (STM.commit(transaction)) + assertTrue(left.length === 0) + deepStrictEqual(Array.from(right), Array.from(input)) + })) + + it.effect("partition - collects only failures", () => + Effect.gen(function*() { + const input = Chunk.range(0, 9) + const transaction = pipe(input, STM.partition(STM.fail)) + const [left, right] = yield* (STM.commit(transaction)) + assertTrue(right.length === 0) + deepStrictEqual(Array.from(left), Array.from(input)) + })) + + it.effect("partition - collects successes and failures", () => + Effect.gen(function*() { + const input = Chunk.range(0, 9) + const transaction = pipe(input, STM.partition((n) => n % 2 === 0 ? STM.fail(n) : STM.succeed(n))) + const [left, right] = yield* (STM.commit(transaction)) + deepStrictEqual(Array.from(left), [0, 2, 4, 6, 8]) + deepStrictEqual(Array.from(right), [1, 3, 5, 7, 9]) + })) + + it.effect("partition - evaluates effects in the correct order", () => + Effect.gen(function*() { + const input = [2, 4, 6, 3, 5, 6] + const transaction = STM.gen(function*() { + const ref = yield* (TRef.make(Chunk.empty())) + yield* (pipe(input, STM.partition((n) => pipe(ref, TRef.update(Chunk.append(n)))))) + return yield* (TRef.get(ref)) + }) + const result = yield* (STM.commit(transaction)) + deepStrictEqual(Array.from(result), input) + })) + + it("reduce - with a successful step function sums the list properly", () => + fc.assert(fc.asyncProperty(fc.array(fc.integer()), async (array) => { + const transaction = pipe(array, STM.reduce(0, (acc, curr) => STM.succeed(acc + curr))) + const result = await Effect.runPromise(STM.commit(transaction)) + strictEqual(result, array.reduce((acc, curr) => acc + curr, 0)) + }))) + + it("reduce - with a failing step function returns a failed transaction", () => + fc.assert(fc.asyncProperty(fc.array(fc.integer(), { minLength: 1 }), async (array) => { + const transaction = pipe(array, STM.reduce(0, () => STM.fail("Ouch"))) + const result = await Effect.runPromise(Effect.exit(STM.commit(transaction))) + deepStrictEqual(result, Exit.fail("Ouch")) + }))) + + it("reduce - run sequentially from left to right", () => + fc.assert(fc.asyncProperty(fc.array(fc.integer(), { minLength: 1 }), async (array) => { + const transaction = pipe( + array, + STM.reduce( + Chunk.empty(), + (acc, curr) => STM.succeed(pipe(acc, Chunk.append(curr))) + ) + ) + const result = await Effect.runPromise(STM.commit(transaction)) + deepStrictEqual(Array.from(result), array) + }))) + + it.effect("reduceAll", () => + Effect.gen(function*() { + const transaction = pipe( + [2, 3, 4].map((n) => STM.succeed(n)), + STM.reduceAll(STM.succeed(1), (a, b) => a + b) + ) + const result = yield* (STM.commit(transaction)) + strictEqual(result, 10) + })) + + it.effect("reduceAll - empty iterable", () => + Effect.gen(function*() { + const transaction = pipe( + Chunk.empty>(), + STM.reduceAll(STM.succeed(1), (a, b) => a + b) + ) + const result = yield* (STM.commit(transaction)) + strictEqual(result, 1) + })) + + it("reduceRight - with a successful step function sums the list properly", () => + fc.assert(fc.asyncProperty(fc.array(fc.integer()), async (array) => { + const transaction = pipe(array, STM.reduceRight(0, (acc, curr) => STM.succeed(acc + curr))) + const result = await Effect.runPromise(STM.commit(transaction)) + strictEqual(result, array.reduce((acc, curr) => acc + curr, 0)) + }))) + + it("reduceRight - with a failing step function returns a failed transaction", () => + fc.assert(fc.asyncProperty(fc.array(fc.integer(), { minLength: 1 }), async (array) => { + const transaction = pipe(array, STM.reduceRight(0, () => STM.fail("Ouch"))) + const result = await Effect.runPromise(Effect.exit(STM.commit(transaction))) + deepStrictEqual(result, Exit.fail("Ouch")) + }))) + + it("reduceRight - run sequentially from right to left", () => + fc.assert(fc.asyncProperty(fc.array(fc.integer(), { minLength: 1 }), async (array) => { + const transaction = pipe( + array, + STM.reduceRight( + Chunk.empty(), + (acc, curr) => STM.succeed(pipe(acc, Chunk.prepend(curr))) + ) + ) + const result = await Effect.runPromise(STM.commit(transaction)) + deepStrictEqual(Array.from(result), array) + }))) + + it.effect("reject - doesnt collect value", () => + Effect.gen(function*() { + const transaction = pipe( + STM.succeed(0), + STM.reject((n) => n !== 0 ? Option.some("Ouch") : Option.none()) + ) + const result = yield* (STM.commit(transaction)) + strictEqual(result, 0) + })) + + it.effect("reject - returns failure ignoring value", () => + Effect.gen(function*() { + const transaction = pipe( + STM.succeed(1), + STM.reject((n) => n !== 0 ? Option.some("Ouch") : Option.none()) + ) + const result = yield* (Effect.exit(STM.commit(transaction))) + deepStrictEqual(result, Exit.fail("Ouch")) + })) + + it.effect("rejectSTM - doesnt collect value", () => + Effect.gen(function*() { + const transaction = pipe( + STM.succeed(0), + STM.rejectSTM((n) => n !== 0 ? Option.some(STM.succeed("Ouch")) : Option.none()) + ) + const result = yield* (STM.commit(transaction)) + strictEqual(result, 0) + })) + + it.effect("rejectSTM - returns failure ignoring value", () => + Effect.gen(function*() { + const transaction = pipe( + STM.succeed(1), + STM.rejectSTM((n) => n !== 0 ? Option.some(STM.succeed("Ouch")) : Option.none()) + ) + const result = yield* (Effect.exit(STM.commit(transaction))) + deepStrictEqual(result, Exit.fail("Ouch")) + })) + + it.effect("repeatWhile - runs effect while it satisfies predicate", () => + Effect.gen(function*() { + const transaction = pipe( + TQueue.bounded(5), + STM.tap((queue) => pipe(queue, TQueue.offerAll([0, 0, 0, 1, 2]))), + STM.flatMap((queue) => + pipe( + TQueue.take(queue), + STM.repeatWhile((n) => n === 0) + ) + ) + ) + const result = yield* (STM.commit(transaction)) + strictEqual(result, 1) + })) + + it.effect("repeatUntil - runs effect until it satisfies predicate", () => + Effect.gen(function*() { + const transaction = pipe( + TQueue.bounded(5), + STM.tap((queue) => pipe(queue, TQueue.offerAll([0, 0, 0, 1, 2]))), + STM.flatMap((queue) => + pipe( + TQueue.take(queue), + STM.repeatUntil((n) => n === 1) + ) + ) + ) + const result = yield* (STM.commit(transaction)) + strictEqual(result, 1) + })) + + it.effect("replicate - zero", () => + Effect.gen(function*() { + const transaction = STM.all(STM.replicate(STM.succeed(12), 0)) + const result = yield* (STM.commit(transaction)) + deepStrictEqual(Array.from(result), []) + })) + + it.effect("replicate - negative", () => + Effect.gen(function*() { + const transaction = STM.all(STM.replicate(STM.succeed(12), -1)) + const result = yield* (STM.commit(transaction)) + deepStrictEqual(Array.from(result), []) + })) + + it.effect("replicate - positive", () => + Effect.gen(function*() { + const transaction = STM.all(STM.replicate(STM.succeed(12), 2)) + const result = yield* (STM.commit(transaction)) + deepStrictEqual(Array.from(result), [12, 12]) + })) + + it.effect("some - extracts the value from Some", () => + Effect.gen(function*() { + const transaction = STM.some(STM.succeed(Option.some(1))) + const result = yield* (STM.commit(transaction)) + strictEqual(result, 1) + })) + + it.effect("some - fails on None", () => + Effect.gen(function*() { + const transaction = STM.some(STM.succeed(Option.none())) + const result = yield* (Effect.exit(STM.commit(transaction))) + assertFailure(result, Cause.fail(Option.none())) + })) + + it.effect("some - fails on error", () => + Effect.gen(function*() { + const error = new Cause.RuntimeException("Ouch") + const transaction = STM.some(STM.fail(error)) + const result = yield* (Effect.exit(STM.commit(transaction))) + assertFailure(result, Cause.fail(Option.some(error))) + })) + + it.effect("succeed", () => + Effect.gen(function*() { + const result = yield* (STM.commit(STM.succeed("test"))) + strictEqual(result, "test") + })) + + it.effect("gen with context", () => + STM.gen({ context: "Context" as const }, function*() { + const result = yield* STM.succeed(this.context) + strictEqual(result, "Context") + })) + + it.effect("summarized - returns summary and value", () => + Effect.gen(function*() { + const transaction = STM.gen(function*() { + const ref = yield* (TRef.make(0)) + const increment = pipe(ref, TRef.updateAndGet((n) => n + 1)) + const result = yield* (pipe( + increment, + STM.summarized(increment, (before, after) => [before, after] as const) + )) + return [result[0][0], result[1], result[0][1]] as const + }) + const result = yield* (STM.commit(transaction)) + deepStrictEqual(result, [1, 2, 3]) + })) + + it.effect("tap - applies the function to the result preserving the original result", () => + Effect.gen(function*() { + const transaction = pipe( + STM.all([TRef.make(10), TRef.make(0)]), + STM.flatMap(([refA, refB]) => + STM.all({ + result1: pipe(TRef.get(refA), STM.tap((n) => pipe(refB, TRef.set(n + 1)))), + result2: TRef.get(refB) + }) + ) + ) + const { result1, result2 } = yield* (STM.commit(transaction)) + strictEqual(result1, 10) + strictEqual(result2, 11) + })) + + it.effect("tapBoth - applies the function to success values preserving the original result", () => + Effect.gen(function*() { + const transaction = STM.gen(function*() { + const tapSuccess = yield* (TDeferred.make()) + const tapError = yield* (TDeferred.make()) + const result = yield* (pipe( + STM.succeed(42), + STM.tapBoth({ + onFailure: (e: string) => pipe(tapError, TDeferred.succeed(e)), + onSuccess: (n) => pipe(tapSuccess, TDeferred.succeed(n)) + }) + )) + const success = yield* (TDeferred.await(tapSuccess)) + return [result, success] as const + }) + const result = yield* (STM.commit(transaction)) + deepStrictEqual(result, [42, 42]) + })) + + it.effect("tapBoth - applies the function to error values preserving the original error", () => + Effect.gen(function*() { + const transaction = STM.gen(function*() { + const tapSuccess = yield* (TDeferred.make()) + const tapError = yield* (TDeferred.make()) + const result = yield* (pipe( + STM.fail("error"), + STM.tapBoth({ + onFailure: (e) => pipe(tapError, TDeferred.succeed(e)), + onSuccess: (n: number) => pipe(tapSuccess, TDeferred.succeed(n)) + }), + STM.either + )) + const error = yield* (TDeferred.await(tapError)) + return [result, error] as const + }) + const result = yield* (STM.commit(transaction)) + deepStrictEqual(result, [Either.left("error"), "error"]) + })) + + it.effect("tapError - should apply the function to the error result preserving the original error", () => + Effect.gen(function*() { + const transaction = STM.gen(function*() { + const errorRef = yield* (TDeferred.make()) + const result = yield* (pipe( + STM.fail("error"), + STM.zipRight(STM.succeed(0)), + STM.tapError((e) => pipe(errorRef, TDeferred.succeed(e))), + STM.either + )) + const error = yield* (TDeferred.await(errorRef)) + return [result, error] + }) + const result = yield* (STM.commit(transaction)) + deepStrictEqual(result, [Either.left("error"), "error"]) + })) + + it.effect("validateAll - returns all errors if never valid", () => + Effect.gen(function*() { + const input = Array.from({ length: 10 }, () => 0) + const transaction = pipe(input, STM.validateAll(STM.fail)) + const result = yield* (pipe( + STM.commit(transaction), + Effect.mapError((chunk) => Array.from(chunk)), + Effect.exit + )) + deepStrictEqual(result, Exit.fail(input)) + })) + + it.effect("validateAll - accumulate errors and ignore successes", () => + Effect.gen(function*() { + const input = Chunk.range(0, 9) + const transaction = pipe(input, STM.validateAll((n) => n % 2 === 0 ? STM.succeed(n) : STM.fail(n))) + const result = yield* (pipe( + STM.commit(transaction), + Effect.mapError((chunk) => Array.from(chunk)), + Effect.exit + )) + deepStrictEqual(result, Exit.fail([1, 3, 5, 7, 9])) + })) + + it.effect("validateAll - accumulate successes", () => + Effect.gen(function*() { + const input = Array.from({ length: 10 }, () => 0) + const transaction = pipe(input, STM.validateAll(STM.succeed)) + const result = yield* (pipe( + STM.commit(transaction), + Effect.map((chunk) => Array.from(chunk)) + )) + deepStrictEqual(result, input) + })) + + it.effect("validateFirst - returns all errors if never valid", () => + Effect.gen(function*() { + const input = Array.from({ length: 10 }, () => 0) + const transaction = pipe(input, STM.validateFirst(STM.fail)) + const result = yield* (pipe( + STM.commit(transaction), + Effect.mapError((chunk) => Array.from(chunk)), + Effect.exit + )) + deepStrictEqual(result, Exit.fail(input)) + })) + + it.effect("validateFirst - runs sequentially and short circuits on first success validation", () => + Effect.gen(function*() { + const input = Chunk.range(1, 9) + const f = (n: number) => n === 6 ? STM.succeed(n) : STM.fail(n) + const transaction = STM.gen(function*() { + const ref = yield* (TRef.make(0)) + const result = yield* (pipe( + input, + STM.validateFirst((n) => pipe(ref, TRef.update((n) => n + 1), STM.zipRight(f(n)))) + )) + const counter = yield* (TRef.get(ref)) + return [result, counter] as const + }) + const result = yield* (STM.commit(transaction)) + deepStrictEqual(result, [6, 6]) + })) + + it.effect("validateFirst - returns errors in correct order", () => + Effect.gen(function*() { + const input = [2, 4, 6, 3, 5, 6] + const transaction = pipe(input, STM.validateFirst(STM.fail)) + const result = yield* (pipe( + STM.commit(transaction), + Effect.mapError((chunk) => Array.from(chunk)), + Effect.exit + )) + deepStrictEqual(result, Exit.fail(input)) + })) + + it.effect("when - true", () => + Effect.gen(function*() { + const ref = yield* (TRef.make(false)) + const transaction = pipe( + ref, + TRef.set(true), + STM.when(constTrue), + STM.zipRight(TRef.get(ref)) + ) + const result = yield* (STM.commit(transaction)) + assertTrue(result) + })) + + it.effect("when - false", () => + Effect.gen(function*() { + const ref = yield* (TRef.make(false)) + const transaction = pipe( + ref, + TRef.set(true), + STM.when(constFalse), + STM.zipRight(TRef.get(ref)) + ) + const result = yield* (STM.commit(transaction)) + assertFalse(result) + })) + + it.effect("whenSTM - true", () => + Effect.gen(function*() { + const ref = yield* (TRef.make(0)) + const isZero = pipe(TRef.get(ref), STM.map((n) => n === 0)) + const transaction = pipe( + ref, + TRef.update((n) => n + 1), + STM.whenSTM(isZero), + STM.zipRight(TRef.get(ref)) + ) + const result = yield* (STM.commit(transaction)) + strictEqual(result, 1) + })) + + it.effect("whenSTM - false", () => + Effect.gen(function*() { + const ref = yield* (TRef.make(0)) + const isZero = pipe(TRef.get(ref), STM.map((n) => n !== 0)) + const transaction = pipe( + ref, + TRef.update((n) => n + 1), + STM.whenSTM(isZero), + STM.zipRight(TRef.get(ref)) + ) + const result = yield* (STM.commit(transaction)) + strictEqual(result, 0) + })) + + it.effect("zip - return a tuple of two computations", () => + Effect.gen(function*() { + const transaction = pipe(STM.succeed(1), STM.zip(STM.succeed("A"))) + const result = yield* (STM.commit(transaction)) + deepStrictEqual(result, [1, "A"]) + })) + + it.effect("zipWith - perform an action on two computations", () => + Effect.gen(function*() { + const transaction = pipe(STM.succeed(578), STM.zipWith(STM.succeed(2), (a, b) => a + b)) + const result = yield* (STM.commit(transaction)) + strictEqual(result, 580) + })) + + it.effect("stack-safety - long orElse chains", () => + Effect.gen(function*() { + const transaction = STM.gen(function*() { + const ref = yield* (TRef.make(0)) + const value = yield* (STM.loop(10_000, { + while: (n) => n > 0, + step: (n) => n - 1, + body: () => + pipe( + STM.retry, + STM.orTry(() => pipe(ref, TRef.getAndUpdate((n) => n + 1))) + ), + discard: true + })) + strictEqual(value, void 0) + return yield* (TRef.get(ref)) + }) + const result = yield* (STM.commit(transaction)) + strictEqual(result, 10_000) + })) + + it.effect("stack-safety - long map chains", () => + Effect.gen(function*() { + const result = yield* (chain(10_000)(STM.map((n) => n + 1))) + strictEqual(result, 10_000) + })) + + it.effect("stack-safety - long collect chains", () => + Effect.gen(function*() { + const result = yield* (chain(10_000)(STM.collect((n) => Option.some(n + 1)))) + strictEqual(result, 10_000) + })) + + it.effect("stack-safety - long collectSTM chains", () => + Effect.gen(function*() { + const result = yield* (chain(10_000)(STM.collectSTM((n) => Option.some(STM.succeed(n + 1))))) + strictEqual(result, 10_000) + })) + + it.effect("stack-safety - long flatMap chains", () => + Effect.gen(function*() { + const result = yield* (chain(10_000)(STM.flatMap((n) => STM.succeed(n + 1)))) + strictEqual(result, 10_000) + })) + + it.effect("stack-safety - long fold chains", () => + Effect.gen(function*() { + const result = yield* (chain(10_000)(STM.match({ onFailure: () => 0, onSuccess: (n) => n + 1 }))) + strictEqual(result, 10_000) + })) + + it.effect("stack-safety - long foldSTM chains", () => + Effect.gen(function*() { + const result = yield* ( + chain(10_000)(STM.matchSTM({ onFailure: () => STM.succeed(0), onSuccess: (n) => STM.succeed(n + 1) })) + ) + strictEqual(result, 10_000) + })) + + it.effect("stack-safety - long mapError chains", () => + Effect.gen(function*() { + const result = yield* (Effect.exit(chainError(10_000))) + deepStrictEqual(result, Exit.fail(10_000)) + })) + + it.effect("stack-safety - long orElse chains", () => + Effect.gen(function*() { + const transaction = pipe( + TRef.make(0), + STM.tap((ref) => + STM.loop(10_000, { + while: (n) => n > 0, + step: (n) => n - 1, + body: () => pipe(STM.retry, STM.orElse(() => pipe(ref, TRef.getAndUpdate((n) => n + 1)))), + discard: true + }) + ), + STM.flatMap(TRef.get) + ) + const result = yield* (STM.commit(transaction)) + strictEqual(result, 10_000) + })) + + it.effect("stack-safety - long provideEnvironment chains", () => + Effect.gen(function*() { + const result = yield* (chain(10_000)(STM.provideContext(Context.empty()))) + strictEqual(result, 0) + })) + + it.effect("ZIO STM (Issue #2073)", () => + Effect.gen(function*() { + const ref0 = yield* (TRef.make(0)) + const ref1 = yield* (TRef.make(0)) + const fiber = yield* (pipe( + TRef.get(ref0), + STM.flatMap((value0) => + pipe( + TRef.get(ref1), + STM.map((value1) => value0 + value1) + ) + ), + STM.commit, + Effect.fork + )) + yield* (pipe( + ref0, + TRef.update((n) => n + 1), + STM.flatMap(() => pipe(ref1, TRef.update((n) => n + 1))), + STM.commit + )) + const result = yield* (Fiber.join(fiber)) + assertTrue(result === 0 || result === 2) + })) + + describe("concurrent computations", () => { + it.effect("increment `TRef` 100 times in 100 fibers", () => + Effect.gen(function*() { + const ref = yield* (TRef.make(0)) + const fiber = yield* (Effect.forkAll( + Array.from({ length: 10 }, () => incrementTRefN(99, ref)) + )) + yield* (Fiber.join(fiber)) + const result = yield* (TRef.get(ref)) + strictEqual(result, 1_000) + })) + + it.effect("compute a `TRef` from 2 variables, increment the first `TRef` and decrement the second `TRef` in different fibers", () => + Effect.gen(function*() { + const refs = yield* (STM.all([TRef.make(10_000), TRef.make(0), TRef.make(0)])) + const fiber = yield* (Effect.forkAll( + Array.from({ length: 10 }, () => compute3TRefN(99, refs[0], refs[1], refs[2])) + )) + yield* (Fiber.join(fiber)) + const result = yield* (TRef.get(refs[2])) + strictEqual(result, 10_000) + })) + }) + + it.effect("condition locks - resume directly when the condition is already satisfied", () => + Effect.gen(function*() { + const ref1 = yield* (TRef.make(10)) + const ref2 = yield* (TRef.make("failed")) + const result = yield* (pipe( + TRef.get(ref1), + STM.tap((n) => STM.check(() => n > 0)), + STM.tap(() => pipe(ref2, TRef.set("success"))), + STM.zipRight(TRef.get(ref2)), + STM.commit + )) + strictEqual(result, "success") + })) + + it.effect("condition locks - resume directly when the condition is already satisfied and change the ref with non-satisfying value", () => + Effect.gen(function*() { + const ref = yield* (TRef.make(42)) + const result = yield* (pipe( + TRef.get(ref), + STM.retryUntil((n) => n === 42), + STM.commit + )) + yield* (pipe(ref, TRef.set(9), STM.commit)) + const value = yield* (TRef.get(ref)) + strictEqual(result, 42) + strictEqual(value, 9) + })) + + it.effect("condition locks - resume after satisfying the condition", () => { + const barrier = new UnpureBarrier() + return Effect.gen(function*() { + const done = yield* (Deferred.make()) + const ref1 = yield* (TRef.make(0)) + const ref2 = yield* (TRef.make("failed")) + const transaction = pipe( + TRef.get(ref1), + STM.tap(() => STM.sync(() => barrier.open())), + STM.tap((n) => STM.check(() => n > 42)), + STM.tap(() => pipe(ref2, TRef.set("success"))), + STM.zipRight(TRef.get(ref2)) + ) + const fiber = yield* (pipe( + STM.commit(transaction), + Effect.zipLeft(pipe(done, Deferred.succeed(void 0))), + Effect.fork + )) + yield* (barrier.await()) + const oldValue = yield* (TRef.get(ref2)) + yield* (pipe(ref1, TRef.set(43))) + yield* (Deferred.await(done)) + const newValue = yield* (TRef.get(ref2)) + const result = yield* (Fiber.join(fiber)) + strictEqual(oldValue, "failed") + strictEqual(newValue, result) + }) + }) + + it.effect("condition locks - resume directly when the condition is already satisfied", () => + Effect.gen(function*() { + const sender = yield* (TRef.make(100)) + const receiver = yield* (TRef.make(0)) + yield* (Effect.fork(transfer(receiver, sender, 150))) + yield* (pipe(sender, TRef.update((n) => n + 100))) + yield* (pipe(TRef.get(sender), STM.retryUntil((n) => n === 50))) + const senderValue = yield* (TRef.get(sender)) + const receiverValue = yield* (TRef.get(receiver)) + strictEqual(senderValue, 50) + strictEqual(receiverValue, 150) + })) + + it.effect("condition locks - run both transactions sequentially", () => + Effect.gen(function*() { + const sender = yield* (TRef.make(100)) + const receiver = yield* (TRef.make(0)) + const toReceiver = transfer(receiver, sender, 150) + const toSender = transfer(sender, receiver, 150) + const fiber = yield* (pipe( + Array.from({ length: 10 }, () => pipe(toReceiver, Effect.zipRight(toSender))), + Effect.forkAll() + )) + yield* (pipe(sender, TRef.update((n) => n + 50))) + yield* (Fiber.join(fiber)) + const senderValue = yield* (TRef.get(sender)) + const receiverValue = yield* (TRef.get(receiver)) + strictEqual(senderValue, 150) + strictEqual(receiverValue, 0) + })) + + it.effect("condition locks - run both transactions concurrently #1", () => + Effect.gen(function*() { + const sender = yield* (TRef.make(50)) + const receiver = yield* (TRef.make(0)) + const toReceiver = transfer(receiver, sender, 100) + const toSender = transfer(sender, receiver, 100) + const fiber1 = yield* (pipe( + Array.from({ length: 10 }, () => toReceiver), + Effect.forkAll() + )) + const fiber2 = yield* (pipe( + Array.from({ length: 10 }, () => toSender), + Effect.forkAll() + )) + yield* (pipe(sender, TRef.update((n) => n + 50))) + yield* (Fiber.join(fiber1)) + yield* (Fiber.join(fiber2)) + const senderValue = yield* (TRef.get(sender)) + const receiverValue = yield* (TRef.get(receiver)) + strictEqual(senderValue, 100) + strictEqual(receiverValue, 0) + })) + + it.effect("condition locks - run both transactions concurrently #2", () => + Effect.gen(function*() { + const sender = yield* (TRef.make(50)) + const receiver = yield* (TRef.make(0)) + const toReceiver = pipe(transfer(receiver, sender, 100), Effect.repeatN(9)) + const toSender = pipe(transfer(sender, receiver, 100), Effect.repeatN(9)) + const fiber = yield* (pipe(toReceiver, Effect.zip(toSender, { concurrent: true }), Effect.fork)) + yield* (pipe(sender, TRef.update((n) => n + 50))) + yield* (Fiber.join(fiber)) + const senderValue = yield* (TRef.get(sender)) + const receiverValue = yield* (TRef.get(receiver)) + strictEqual(senderValue, 100) + strictEqual(receiverValue, 0) + })) + + it.effect("condition locks - atomically run a transaction with a TRef for 20 fibers, each one checking and incrementing the value", () => + Effect.gen(function*() { + const ref = yield* (TRef.make(1)) + const fiber = yield* (pipe( + Chunk.range(1, 20), + Chunk.map((i) => + pipe( + TRef.get(ref), + STM.tap((n) => STM.check(() => n === i)), + STM.tap(() => pipe(ref, TRef.update((n) => n + 1))), + STM.commit + ) + ), + Effect.forkAll() + )) + yield* (Fiber.join(fiber)) + const result = yield* (TRef.get(ref)) + strictEqual(result, 21) + })) + + it.effect("condition locks - atomically run a transaction that could not be satisfied", () => { + const barrier = new UnpureBarrier() + return Effect.gen(function*() { + const ref = yield* (TRef.make(0)) + const fiber = yield* (pipe( + TRef.get(ref), + STM.tap(() => STM.sync(() => barrier.open())), + STM.tap((n) => STM.check(() => n > 0)), + STM.tap(() => pipe(ref, TRef.update((n) => Math.floor(10 / n)))), + STM.commit, + Effect.fork + )) + yield* (barrier.await()) + yield* (Fiber.interrupt(fiber)) + yield* (pipe(ref, TRef.set(10))) + const result = yield* (pipe(Effect.yieldNow(), Effect.zipRight(TRef.get(ref)))) + strictEqual(result, 10) + }) + }) + + it.effect("condition locks - interrupt one fiber executing a transaction should terminate all transactions", () => { + const barrier = new UnpureBarrier() + return Effect.gen(function*() { + const ref = yield* (TRef.make(0)) + const fiber = yield* (pipe( + Array.from({ length: 100 }, () => + pipe( + TRef.get(ref), + STM.tap(() => STM.sync(() => barrier.open())), + STM.tap((n) => STM.check(() => n < 0)), + STM.tap(() => pipe(ref, TRef.set(10))), + STM.commit + )), + Effect.forkAll() + )) + yield* (barrier.await()) + yield* (Fiber.interrupt(fiber)) + yield* (pipe(ref, TRef.set(-1))) + const result = yield* (pipe(Effect.yieldNow(), Effect.zipRight(TRef.get(ref)))) + strictEqual(result, -1) + }) + }) + + it.effect("condition locks - interrupt fiber and observe it", () => + Effect.gen(function*() { + const fiberId = yield* (Effect.fiberId) + const ref = yield* (TRef.make(1)) + const fiber = yield* (pipe( + TRef.get(ref), + STM.flatMap((n) => STM.check(() => n === 0)), + STM.commit, + Effect.fork + )) + yield* (Fiber.interrupt(fiber)) + const result = yield* (pipe(Fiber.join(fiber), Effect.sandbox, Effect.either)) + assertTrue( + Either.isLeft(result) && + pipe( + result.left, + Cause.contains(Cause.interrupt(fiberId)) + ) + ) + })) + + it.effect("permutations - permutes two variables", () => + Effect.gen(function*() { + const ref1 = yield* (TRef.make(1)) + const ref2 = yield* (TRef.make(2)) + yield* (permutation(ref1, ref2)) + const result1 = yield* (TRef.get(ref1)) + const result2 = yield* (TRef.get(ref2)) + strictEqual(result1, 2) + strictEqual(result2, 1) + })) + + it.effect("permutations - permutes two variables in 100 fibers", () => + Effect.gen(function*() { + const ref1 = yield* (TRef.make(1)) + const ref2 = yield* (TRef.make(2)) + const oldValue1 = yield* (TRef.get(ref1)) + const oldValue2 = yield* (TRef.get(ref2)) + const fiber = yield* (pipe( + Array.from({ length: 100 }, () => STM.commit(permutation(ref1, ref2))), + Effect.forkAll() + )) + yield* (Fiber.join(fiber)) + const result1 = yield* (TRef.get(ref1)) + const result2 = yield* (TRef.get(ref2)) + strictEqual(result1, oldValue1) + strictEqual(result2, oldValue2) + })) +}) diff --git a/repos/effect/packages/effect/test/Schedule.test.ts b/repos/effect/packages/effect/test/Schedule.test.ts new file mode 100644 index 0000000..3a0866d --- /dev/null +++ b/repos/effect/packages/effect/test/Schedule.test.ts @@ -0,0 +1,1017 @@ +import { describe, it } from "@effect/vitest" +import { assertTrue, deepStrictEqual, strictEqual } from "@effect/vitest/utils" +import { + Array, + Cause, + Chunk, + Clock, + Deferred, + Duration, + Effect, + Exit, + Fiber, + Option, + pipe, + Ref, + Schedule, + ScheduleDecision, + ScheduleIntervals +} from "effect" +import { constVoid } from "effect/Function" +import * as TestClock from "effect/TestClock" + +describe("Schedule", () => { + it.effect("collect all inputs into a list as long as the condition f holds", () => + Effect.gen(function*() { + const result = yield* repeat(Schedule.collectWhile((n) => n < 10)) + deepStrictEqual(Chunk.toReadonlyArray(result), Array.range(1, 9)) + })) + it.effect("collect all inputs into a list as long as the effectful condition f holds", () => + Effect.gen(function*() { + const result = yield* repeat(Schedule.collectWhileEffect((n) => Effect.succeed(n > 10))) + assertTrue(Chunk.isEmpty(result)) + })) + it.effect("collect all inputs into a list until the effectful condition f fails", () => + Effect.gen(function*() { + const result = yield* repeat(Schedule.collectUntil((n) => n < 10 && n > 1)) + deepStrictEqual(Chunk.toReadonlyArray(result), [1]) + })) + it.effect("collect all inputs into a list until the effectful condition f fails", () => + Effect.gen(function*() { + const result = yield* repeat(Schedule.collectUntilEffect((n) => Effect.succeed(n > 10))) + deepStrictEqual(Chunk.toReadonlyArray(result), Array.range(1, 10)) + })) + it.effect("union composes", () => + Effect.gen(function*() { + const monday = Schedule.dayOfMonth(1) + const wednesday = Schedule.dayOfMonth(3) + const friday = Schedule.dayOfMonth(5) + const mondayOrWednesday = monday.pipe(Schedule.union(wednesday)) + const wednesdayOrFriday = wednesday.pipe(Schedule.union(friday)) + const alsoWednesday = mondayOrWednesday.pipe(Schedule.intersect(wednesdayOrFriday)) + const now = yield* Effect.sync(() => Date.now()) + const input = Array.range(1, 5) + const actual = yield* pipe(alsoWednesday, Schedule.delays, Schedule.run(now, input)) + const expected = yield* pipe(wednesday, Schedule.delays, Schedule.run(now, input)) + deepStrictEqual(Chunk.toReadonlyArray(actual), Chunk.toReadonlyArray(expected)) + })) + it.effect("either should not wait if neither schedule wants to continue", () => + Effect.gen(function*() { + const schedule = Schedule.stop.pipe( + Schedule.union(Schedule.spaced("2 seconds").pipe(Schedule.intersect(Schedule.stop))), + Schedule.compose(Schedule.elapsed) + ) + const input = Array.makeBy(4, constVoid) + const result = yield* runCollect(schedule, input) + deepStrictEqual(Chunk.toReadonlyArray(result), [Duration.zero]) + })) + it.effect("perform log for each recurrence of effect", () => + Effect.gen(function*() { + const schedule = (ref: Ref.Ref) => { + return Schedule.recurs(3).pipe(Schedule.onDecision(() => Ref.update(ref, (n) => n + 1))) + } + const ref = yield* Ref.make(0) + yield* pipe(Ref.getAndUpdate(ref, (n) => n + 1), Effect.repeat(schedule(ref))) + const result = yield* Ref.get(ref) + strictEqual(result, 8) + })) + it.effect("reset after some inactivity", () => + Effect.gen(function*() { + const io = (ref: Ref.Ref, latch: Deferred.Deferred): Effect.Effect => { + return Ref.updateAndGet(ref, (n) => n + 1).pipe( + Effect.flatMap((retries) => { + // The 5th retry will fail after 10 seconds to let the schedule reset + if (retries == 5) { + return Deferred.succeed(latch, void 0).pipe( + Effect.zipRight(io(ref, latch).pipe(Effect.delay("10 seconds"))) + ) + } + // The 10th retry will succeed, which is only possible if the schedule was reset + if (retries == 10) { + return Effect.void + } + return Effect.fail("Boom") + }) + ) + } + const schedule = Schedule.recurs(5).pipe(Schedule.resetAfter("5 seconds")) + const retriesCounter = yield* Ref.make(-1) + const latch = yield* Deferred.make() + const fiber = yield* pipe(io(retriesCounter, latch), Effect.retry(schedule), Effect.fork) + yield* Deferred.await(latch) + yield* TestClock.adjust("10 seconds") + yield* Fiber.join(fiber) + const retries = yield* Ref.get(retriesCounter) + strictEqual(retries, 10) + })) + it.effect("union of two schedules should continue as long as either wants to continue", () => + Effect.gen(function*() { + const schedule = Schedule.recurWhile((b: boolean) => b).pipe(Schedule.union(Schedule.fixed("1 seconds"))) + const input = Chunk.make(true, false, false, false, false) + const result = yield* runCollect(schedule.pipe(Schedule.compose(Schedule.elapsed)), input) + const expected = [0, 0, 1, 2, 3].map(Duration.seconds) + deepStrictEqual(Chunk.toReadonlyArray(result), expected) + })) + it.effect("Schedule.fixed should compute delays correctly", () => + Effect.gen(function*() { + const inputs = Chunk.make([0, undefined] as const, [6500, undefined] as const) + const result = yield* pipe( + runManually(Schedule.fixed("5 seconds"), inputs), + Effect.map((output) => output[0].pipe(Chunk.map((tuple) => tuple[0]))) + ) + deepStrictEqual(result, Chunk.make(5000, 10000)) + })) + it.effect("intersection of schedules recurring in bounded intervals", () => + Effect.gen(function*() { + const schedule = Schedule.hourOfDay(4).pipe(Schedule.intersect(Schedule.minuteOfHour(20))) + const now = yield* Effect.sync(() => Date.now()) + const input = Array.range(1, 5) + const delays = yield* pipe(Schedule.delays(schedule), Schedule.run(now, input)) + const actual = Chunk.toReadonlyArray(scanLeft(delays, now, (now, delay) => now + Duration.toMillis(delay))).slice( + 1 + ) + assertTrue(actual.map((n) => new Date(n).getHours()).every((n) => n === 4)) + assertTrue(actual.map((n) => new Date(n).getMinutes()).every((n) => n === 20)) + })) + it.effect("passthrough", () => + Effect.gen(function*() { + const ref = yield* Ref.make(0) + const result = yield* Ref.getAndUpdate(ref, (n) => n + 1).pipe( + Effect.repeat(Schedule.recurs(10).pipe(Schedule.passthrough)) + ) + strictEqual(result, 10) + })) + describe("simulate a schedule", () => { + it.effect("without timing out", () => + Effect.gen(function*() { + const schedule = Schedule.exponential("1 minutes") + const result = yield* pipe( + Clock.currentTimeMillis, + Effect.flatMap((now) => schedule.pipe(Schedule.run(now, Array.makeBy(5, constVoid)))) + ) + deepStrictEqual(Chunk.toReadonlyArray(result), [ + Duration.minutes(1), + Duration.minutes(2), + Duration.minutes(4), + Duration.minutes(8), + Duration.minutes(16) + ]) + })) + it.effect("respect Schedule.recurs even if more input is provided than needed", () => + Effect.gen(function*() { + const schedule = Schedule.recurs(2).pipe(Schedule.intersect(Schedule.exponential("1 minutes"))) + const result = yield* Clock.currentTimeMillis.pipe( + Effect.flatMap((now) => schedule.pipe(Schedule.run(now, Array.range(1, 10)))) + ) + deepStrictEqual(Chunk.toReadonlyArray(result), [ + [0, Duration.minutes(1)], + [1, Duration.minutes(2)], + [2, Duration.minutes(4)] + ]) + })) + it.effect("respect Schedule.upTo even if more input is provided than needed", () => + Effect.gen(function*() { + const schedule = Schedule.spaced("1 seconds").pipe(Schedule.upTo("5 seconds")) + const result = yield* Clock.currentTimeMillis.pipe( + Effect.flatMap((now) => schedule.pipe(Schedule.run(now, Array.range(1, 10)))) + ) + deepStrictEqual(Chunk.toReadonlyArray(result), [0, 1, 2, 3, 4, 5]) + })) + }) + describe("repeat an action a single time", () => { + it.effect("repeat on failure does not actually repeat", () => + Effect.gen(function*() { + const ref = yield* Ref.make(0) + const result = yield* Effect.flip(alwaysFail(ref)) + strictEqual(result, "Error: 1") + })) + it.effect("repeat a scheduled repeat repeats the whole number", () => + Effect.gen(function*() { + const n = 42 + const ref = yield* Ref.make(0) + const effect = ref.pipe(Ref.update((n) => n + 1), Effect.repeat(Schedule.recurs(n))) + yield* pipe(effect, Effect.repeat(Schedule.recurs(1))) + const result = yield* Ref.get(ref) + strictEqual(result, (n + 1) * 2) + })) + }) + describe("repeat an action two times and call ensuring should", () => { + it.effect("run the specified finalizer as soon as the schedule is complete", () => + Effect.gen(function*() { + const deferred = yield* Deferred.make() + const ref = yield* Ref.make(0) + yield* pipe( + Ref.update(ref, (n) => n + 2), + Effect.repeat(Schedule.recurs(2)), + Effect.ensuring(Deferred.succeed(deferred, void 0)) + ) + const value = yield* Ref.get(ref) + const finalizerValue = yield* Deferred.poll(deferred) + strictEqual(value, 6) + assertTrue(Option.isSome(finalizerValue)) + })) + }) + describe("repeat on success according to a provided strategy", () => { + it.effect("for 'recurs(a negative number)' repeats 0 additional time", () => + Effect.gen(function*() { + // A repeat with a negative number of times should not repeat the action at all + const result = yield* repeat(Schedule.recurs(-5)) + strictEqual(result, 0) + })) + it.effect("for 'recurs(0)' does repeat 0 additional time", () => + Effect.gen(function*() { + // A repeat with 0 number of times should not repeat the action at all + const result = yield* repeat(Schedule.recurs(0)) + strictEqual(result, 0) + })) + it.effect("for 'recurs(1)' does repeat 1 additional time", () => + Effect.gen(function*() { + const result = yield* repeat(Schedule.recurs(1)) + strictEqual(result, 1) + })) + it.effect("for 'once' will repeat 1 additional time", () => + Effect.gen(function*() { + const ref = yield* Ref.make(0) + yield* pipe(Ref.update(ref, (n) => n + 1), Effect.repeat(Schedule.once)) + const result = yield* Ref.get(ref) + strictEqual(result, 2) + })) + it.effect("for 'recurs(a positive given number)' repeats that additional number of time", () => + Effect.gen(function*() { + const result = yield* repeat(Schedule.recurs(42)) + strictEqual(result, 42) + })) + it.effect("for 'recurWhile(cond)' repeats while the cond still holds", () => + Effect.gen(function*() { + const result = yield* repeat(Schedule.recurWhile((n) => n < 10)) + strictEqual(result, 10) + })) + it.effect("for 'recurWhileEffect(cond)' repeats while the effectful cond still holds", () => + Effect.gen(function*() { + const result = yield* repeat(Schedule.recurWhileEffect((n) => Effect.succeed(n > 10))) + strictEqual(result, 1) + })) + it.effect("for 'recurUntil(cond)' repeats until the cond is satisfied", () => + Effect.gen(function*() { + const result = yield* repeat(Schedule.recurUntil((n) => n < 10)) + strictEqual(result, 1) + })) + it.effect("for 'recurUntilEffect(cond)' repeats until the effectful cond is satisfied", () => + Effect.gen(function*() { + const result = yield* repeat(Schedule.recurUntilEffect((n) => Effect.succeed(n > 10))) + strictEqual(result, 11) + })) + }) + describe("delays", () => { + it.effect("duration", () => + Effect.gen(function*() { + const [actual, expected] = yield* checkDelays(Schedule.duration("1 seconds")) + deepStrictEqual(actual, expected) + })) + it.effect("exponential", () => + Effect.gen(function*() { + const [actual, expected] = yield* checkDelays(Schedule.exponential("1 seconds")) + deepStrictEqual(actual, expected) + })) + it.effect("fibonacci", () => + Effect.gen(function*() { + const [actual, expected] = yield* checkDelays(Schedule.fibonacci("1 seconds")) + deepStrictEqual(actual, expected) + })) + it.effect("fromDelay", () => + Effect.gen(function*() { + const [actual, expected] = yield* checkDelays(Schedule.fromDelay("1 seconds")) + deepStrictEqual(actual, expected) + })) + it.effect("fromDelays", () => + Effect.gen(function*() { + const [actual, expected] = yield* checkDelays( + Schedule.fromDelays("1 seconds", "2 seconds", "3 seconds", "4 seconds") + ) + deepStrictEqual(actual, expected) + })) + it.effect("linear", () => + Effect.gen(function*() { + const [actual, expected] = yield* checkDelays(Schedule.linear("1 seconds")) + deepStrictEqual(actual, expected) + })) + }) + describe("repetitions", () => { + it.effect("forever", () => + Effect.gen(function*() { + const [actual, expected] = yield* checkRepetitions(Schedule.repeatForever) + deepStrictEqual(actual, expected) + })) + it.effect("count", () => + Effect.gen(function*() { + const [actual, expected] = yield* checkRepetitions(Schedule.count) + deepStrictEqual(actual, expected) + })) + it.effect("dayOfMonth", () => + Effect.gen(function*() { + const [actual, expected] = yield* checkRepetitions(Schedule.dayOfMonth(1)) + deepStrictEqual(actual, expected) + })) + it.effect("dayOfWeek", () => + Effect.gen(function*() { + const [actual, expected] = yield* checkRepetitions(Schedule.dayOfWeek(1)) + deepStrictEqual(actual, expected) + })) + it.effect("hourOfDay", () => + Effect.gen(function*() { + const [actual, expected] = yield* checkRepetitions(Schedule.hourOfDay(1)) + deepStrictEqual(actual, expected) + })) + it.effect("minuteOfHour", () => + Effect.gen(function*() { + const [actual, expected] = yield* checkRepetitions(Schedule.minuteOfHour(1)) + deepStrictEqual(actual, expected) + })) + it.effect("secondOfMinute", () => + Effect.gen(function*() { + const [actual, expected] = yield* checkRepetitions(Schedule.secondOfMinute(1)) + deepStrictEqual(actual, expected) + })) + it.effect("fixed", () => + Effect.gen(function*() { + const [actual, expected] = yield* checkRepetitions(Schedule.fixed("1 seconds")) + deepStrictEqual(actual, expected) + })) + it.effect("repeatForever", () => + Effect.gen(function*() { + const [actual, expected] = yield* checkRepetitions(Schedule.repeatForever) + deepStrictEqual(actual, expected) + })) + it.effect("recurs", () => + Effect.gen(function*() { + const [actual, expected] = yield* checkRepetitions(Schedule.recurs(2)) + deepStrictEqual(actual, expected) + })) + it.effect("spaced", () => + Effect.gen(function*() { + const [actual, expected] = yield* checkRepetitions(Schedule.spaced("1 seconds")) + deepStrictEqual(actual, expected) + })) + it.effect("windowed", () => + Effect.gen(function*() { + const [actual, expected] = yield* checkRepetitions(Schedule.windowed("1 seconds")) + deepStrictEqual(actual, expected) + })) + }) + describe("retries", () => { + it.effect("for up to 10 times", () => + Effect.gen(function*() { + let i = 0 + const strategy = Schedule.recurs(10) + const io = Effect.sync(() => { + i = i + 1 + }).pipe( + Effect.flatMap(() => i < 5 ? Effect.fail("KeepTryingError") : Effect.succeed(i)) + ) + const result = yield* pipe(io, Effect.retry(strategy)) + strictEqual(result, 5) + })) + it.effect("retry exactly one time for `once` when second time succeeds - retryOrElse", () => + Effect.gen(function*() { + const ref = yield* Ref.make(0) + const result = yield* pipe(failOn0(ref), Effect.retryOrElse(Schedule.once, ioFail)) + strictEqual(result, 2) + })) + it.effect("if fallback succeeded - retryOrElse", () => + Effect.gen(function*() { + const ref = yield* Ref.make(0) + const result = yield* pipe(alwaysFail(ref), Effect.retryOrElse(Schedule.once, ioSucceed)) + strictEqual(result, "OrElse") + })) + it.effect("if fallback failed - retryOrElse", () => + Effect.gen(function*() { + const ref = yield* Ref.make(0) + const result = yield* alwaysFail(ref).pipe( + Effect.retryOrElse(Schedule.once, ioFail), + Effect.flip + ) + strictEqual(result, "OrElseFailed") + })) + it.effect("retry 0 time for `once` when first time succeeds", () => + Effect.gen(function*() { + const ref = yield* Ref.make(0) + yield* pipe(Ref.update(ref, (n) => n + 1), Effect.retry(Schedule.once)) + const result = yield* Ref.get(ref) + strictEqual(result, 1) + })) + it.effect("retry 0 time for `recurs(0)`", () => + Effect.gen(function*() { + const ref = yield* Ref.make(0) + const result = yield* alwaysFail(ref).pipe( + Effect.retry(Schedule.recurs(0)), + Effect.flip + ) + strictEqual(result, "Error: 1") + })) + it.effect("retry exactly one time for `once` when second time succeeds", () => + Effect.gen(function*() { + const ref = yield* Ref.make(0) // One retry on failure + // One retry on failure + yield* pipe(failOn0(ref), Effect.retry(Schedule.once)) + const result = yield* Ref.get(ref) + strictEqual(result, 2) + })) + it.effect("retry exactly one time for `once` even if still in error", () => + Effect.gen(function*() { + const ref = yield* Ref.make(0) // No more than one retry on retry `once` + // No more than one retry on retry `once` + const result = yield* alwaysFail(ref).pipe( + Effect.retry(Schedule.once), + Effect.flip + ) + strictEqual(result, "Error: 2") + })) + it.effect("retry exactly 'n' times after failure", () => + Effect.gen(function*() { + const ref = yield* Ref.make(0) + const result = yield* pipe(alwaysFail(ref), Effect.retry({ times: 3 }), Effect.flip) + strictEqual(result, "Error: 4") + })) + // TODO(Max): after TestRandom + // it.skip("for a given number of times with random jitter in (0, 1)") + // Effect.gen(function*(){ + // const schedule = Schedule.spaced((500).millis).jittered(0, 1) + // const result = $(runCollect(schedule.compose(Schedule.elapsed), Chunk.fill(5, constVoid))) + // const expected = Chunk((0).millis, (250).millis, (500).millis, (750).millis, (1000).millis) + // assertTrue() + // }).unsafeRunPromise() + // TODO(Max): after TestRandom + // it.skip("for a given number of times with random jitter in custom interval") + // Effect.gen(function*(){ + // const schedule = Schedule.spaced((500).millis).jittered(2, 4) + // const result = $(runCollect(schedule.compose(Schedule.elapsed), Chunk.fill(5, constVoid))) + // const expected = Chunk((0).millis, (1500).millis, (3000).millis, (5000).millis, (7000).millis) + // assertTrue() + // }).unsafeRunPromise() + it.effect("fixed delay with error predicate", () => + Effect.gen(function*() { + let i = 0 + const effect = Effect.sync(() => { + i = i + 1 + }).pipe( + Effect.flatMap(() => i < 5 ? Effect.fail("KeepTryingError") : Effect.fail("GiveUpError")) + ) + const strategy = Schedule.spaced("200 millis").pipe( + Schedule.whileInput((s: string) => s === "KeepTryingError") + ) + const program = effect.pipe( + Effect.retryOrElse(strategy, (s, n) => + TestClock.currentTimeMillis.pipe(Effect.map((now) => [Duration.millis(now), s, n] as const))) + ) + const result = yield* run(program) + deepStrictEqual(result, [Duration.millis(800), "GiveUpError", 4] as const) + })) + it.effect("fibonacci delay", () => + Effect.gen(function*() { + const schedule = Schedule.fibonacci("100 millis").pipe(Schedule.compose(Schedule.elapsed)) + const result = yield* runCollect(schedule, Array.makeBy(5, constVoid)) + const expected = [0, 100, 200, 400, 700].map(Duration.millis) + deepStrictEqual(Chunk.toReadonlyArray(result), expected) + })) + it.effect("linear delay", () => + Effect.gen(function*() { + const schedule = Schedule.linear("100 millis").pipe(Schedule.compose(Schedule.elapsed)) + const result = yield* runCollect(schedule, Array.makeBy(5, constVoid)) + const expected = [0, 100, 300, 600, 1000].map(Duration.millis) + deepStrictEqual(Chunk.toReadonlyArray(result), expected) + })) + it.effect("spaced delay", () => + Effect.gen(function*() { + const schedule = Schedule.spaced("100 millis").pipe(Schedule.compose(Schedule.elapsed)) + const result = yield* runCollect(schedule, Array.makeBy(5, constVoid)) + const expected = [0, 100, 200, 300, 400].map(Duration.millis) + deepStrictEqual(Chunk.toReadonlyArray(result), expected) + })) + it.effect("fixed delay", () => + Effect.gen(function*() { + const schedule = Schedule.fixed("100 millis").pipe(Schedule.compose(Schedule.elapsed)) + const result = yield* runCollect(schedule, Array.makeBy(5, constVoid)) + const expected = [0, 100, 200, 300, 400].map(Duration.millis) + deepStrictEqual(Chunk.toReadonlyArray(result), expected) + })) + it.effect("fixed delay with zero delay", () => + Effect.gen(function*() { + const schedule = Schedule.fixed(Duration.zero).pipe(Schedule.compose(Schedule.elapsed)) + const result = yield* runCollect(schedule, Array.makeBy(5, constVoid)) + const expected = Array.makeBy(5, () => Duration.zero) + deepStrictEqual(Chunk.toReadonlyArray(result), expected) + })) + it.effect("windowed", () => + Effect.gen(function*() { + const schedule = Schedule.windowed("100 millis").pipe(Schedule.compose(Schedule.elapsed)) + const result = yield* runCollect(schedule, Array.makeBy(5, constVoid)) + const expected = [0, 100, 200, 300, 400].map(Duration.millis) + deepStrictEqual(Chunk.toReadonlyArray(result), expected) + })) + it.effect("modified linear delay", () => + Effect.gen(function*() { + const schedule = Schedule.linear("100 millis").pipe( + Schedule.modifyDelayEffect((_, duration) => Effect.succeed(duration.pipe(Duration.times(2)))), + Schedule.compose(Schedule.elapsed) + ) + const result = yield* runCollect(schedule, Array.makeBy(5, constVoid)) + const expected = [0, 200, 600, 1200, 2000].map(Duration.millis) + deepStrictEqual(Chunk.toReadonlyArray(result), expected) + })) + it.effect("exponential delay with default factor", () => + Effect.gen(function*() { + const schedule = Schedule.exponential("100 millis").pipe(Schedule.compose(Schedule.elapsed)) + const result = yield* runCollect(schedule, Array.makeBy(5, constVoid)) + const expected = [0, 100, 300, 700, 1500].map(Duration.millis) + deepStrictEqual(Chunk.toReadonlyArray(result), expected) + })) + it.effect("exponential delay with other factor", () => + Effect.gen(function*() { + const schedule = Schedule.exponential("100 millis", 3).pipe(Schedule.compose(Schedule.elapsed)) + const result = yield* runCollect(schedule, Array.makeBy(5, constVoid)) + const expected = [0, 100, 400, 1300, 4000].map(Duration.millis) + deepStrictEqual(Chunk.toReadonlyArray(result), expected) + })) + it.effect("fromDelays", () => + Effect.gen(function*() { + const delays = Schedule.fromDelays( + "4 seconds", + "7 seconds", + "12 seconds", + "19 seconds" + ) + const schedule = delays.pipe(Schedule.compose(Schedule.elapsed)) + const result = yield* runCollect(schedule, Array.makeBy(5, constVoid)) + const expected = [0, 4, 11, 23, 42].map(Duration.seconds) + deepStrictEqual(Chunk.toReadonlyArray(result), expected) + })) + it.effect("retry a failed action 2 times and call `ensuring` should run the specified finalizer as soon as the schedule is complete", () => + Effect.gen(function*() { + const deferred = yield* Deferred.make() + const value = yield* Effect.fail("oh no").pipe( + Effect.retry(Schedule.recurs(2)), + Effect.ensuring(Deferred.succeed(deferred, void 0)), + Effect.option + ) + const finalizerValue = yield* Deferred.poll(deferred) + assertTrue(Option.isNone(value)) + assertTrue(Option.isSome(finalizerValue)) + })) + }) + describe("cron-like scheduling - repeats at point of time (minute of hour, day of week, ...)", () => { + it.effect("recur every second minute using cron", () => + Effect.gen(function*() { + const ref = yield* Ref.make>([]) + yield* TestClock.setTime(new Date(2024, 0, 1, 0, 0, 35).getTime()) + const schedule = Schedule.cron("*/2 * * * *") + yield* pipe( + TestClock.currentTimeMillis, + Effect.tap((instant) => Ref.update(ref, Array.append(format(instant)))), + Effect.repeat(schedule), + Effect.fork + ) + yield* TestClock.adjust("8 minutes") + const result = yield* Ref.get(ref) + const expected = [ + "Mon Jan 01 2024 00:00:35", + "Mon Jan 01 2024 00:02:00", + "Mon Jan 01 2024 00:04:00", + "Mon Jan 01 2024 00:06:00", + "Mon Jan 01 2024 00:08:00" + ] + deepStrictEqual(result, expected) + })) + it.effect("recur at time matching cron expression", () => + Effect.gen(function*() { + const ref = yield* Ref.make>([]) + yield* TestClock.setTime(new Date(2024, 0, 1, 0, 0, 0).getTime()) + // At 04:30 on day-of-month 5 and 15 and on Wednesday. + const schedule = Schedule.cron("30 4 5,15 * WED") + yield* pipe( + TestClock.currentTimeMillis, + Effect.tap((instant) => Ref.update(ref, Array.append(format(instant)))), + Effect.repeat(schedule), + Effect.fork + ) + yield* TestClock.adjust("4 weeks") + const result = yield* Ref.get(ref) + const expected = [ + "Mon Jan 01 2024 00:00:00", + "Wed Jan 03 2024 04:30:00", + "Fri Jan 05 2024 04:30:00", + "Wed Jan 10 2024 04:30:00", + "Mon Jan 15 2024 04:30:00", + "Wed Jan 17 2024 04:30:00", + "Wed Jan 24 2024 04:30:00" + ] + deepStrictEqual(result, expected) + })) + it.effect("recur at time matching cron expression (second granularity)", () => + Effect.gen(function*() { + const ref = yield* Ref.make>([]) + yield* TestClock.setTime(new Date(2024, 0, 1, 0, 0, 0).getTime()) + const schedule = Schedule.cron("*/3 * * * * *") + yield* pipe( + TestClock.currentTimeMillis, + Effect.tap((instant) => Ref.update(ref, Array.append(format(instant)))), + Effect.repeat(schedule), + Effect.fork + ) + yield* TestClock.adjust("30 seconds") + const result = yield* Ref.get(ref) + const expected = [ + "Mon Jan 01 2024 00:00:00", + "Mon Jan 01 2024 00:00:03", + "Mon Jan 01 2024 00:00:06", + "Mon Jan 01 2024 00:00:09", + "Mon Jan 01 2024 00:00:12", + "Mon Jan 01 2024 00:00:15", + "Mon Jan 01 2024 00:00:18", + "Mon Jan 01 2024 00:00:21", + "Mon Jan 01 2024 00:00:24", + "Mon Jan 01 2024 00:00:27", + "Mon Jan 01 2024 00:00:30" + ] + deepStrictEqual(result, expected) + })) + it.effect("recur at 01 second of each minute", () => + Effect.gen(function*() { + const originOffset = new Date(new Date(new Date().setMinutes(0)).setSeconds(0)).setMilliseconds(0) + const inTimeSecondMillis = new Date(new Date(originOffset).setSeconds(1)).setMilliseconds(1) + const inTimeSecond = new Date(originOffset).setSeconds(1) + const beforeTime = new Date(originOffset).setSeconds(0) + const afterTime = new Date(originOffset).setSeconds(3) + const input = Chunk.make(inTimeSecondMillis, inTimeSecond, beforeTime, afterTime).pipe( + Chunk.map((n) => [n, void 0] as const) + ) + const result = yield* pipe( + runManually(Schedule.secondOfMinute(1), input), + Effect.map((output) => output[0].pipe(Chunk.map((tuple) => tuple[0]))) + ) + const expectedDate = new Date(new Date(originOffset).setSeconds(1)) + const expected = expectedDate.getTime() + const afterTimeExpected = new Date(expectedDate).setMinutes(expectedDate.getMinutes() + 1) + const expectedOutput = Chunk.make(expected, afterTimeExpected, expected, afterTimeExpected) + deepStrictEqual(result, expectedOutput) + })) + it.effect("recur at 01 minute of each hour", () => + Effect.gen(function*() { + const originOffset = new Date(new Date(new Date().setHours(0)).setSeconds(0)).setMilliseconds(0) + const inTimeMinuteMillis = new Date(new Date(originOffset).setMinutes(1)).setMilliseconds(1) + const inTimeMinute = new Date(originOffset).setMinutes(1) + const beforeTime = new Date(originOffset).setMinutes(0) + const afterTime = new Date(originOffset).setMinutes(3) + const input = Chunk.make(inTimeMinuteMillis, inTimeMinute, beforeTime, afterTime).pipe( + Chunk.map((n) => [n, void 0] as const) + ) + const result = yield* pipe( + runManually(Schedule.minuteOfHour(1), input), + Effect.map((output) => output[0].pipe(Chunk.map((tuple) => tuple[0]))) + ) + const expected = new Date(new Date(originOffset).setMinutes(1)) + const afterTimeExpected = new Date(expected).setHours(expected.getHours() + 1) + const expectedOutput = Chunk.make(expected.getTime(), afterTimeExpected, expected.getTime(), afterTimeExpected) + deepStrictEqual(result, expectedOutput) + })) + it.effect("recur at 01 hour of each day", () => + Effect.gen(function*() { + const originOffset = roundToNearestHour(new Date()) + const inTimeHourSecond = new Date(new Date(originOffset).setHours(1)).setSeconds(1) + const inTimeHour = new Date(originOffset).setHours(1) + const beforeTime = new Date(originOffset).setHours(0) + const afterTime = new Date(originOffset).setHours(3) + const input = Chunk.make(inTimeHourSecond, inTimeHour, beforeTime, afterTime).pipe( + Chunk.map((n) => [n, void 0] as const) + ) + const result = yield* pipe( + runManually(Schedule.hourOfDay(1), input), + Effect.map((output) => output[0].pipe(Chunk.map((tuple) => tuple[0]))) + ) + const expectedDate = new Date(new Date(originOffset).setHours(1)) + const expected = expectedDate.getTime() + const afterTimeExpected = new Date(expectedDate).setDate(expectedDate.getDate() + 1) + const expectedOutput = Chunk.make(expected, afterTimeExpected, expected, afterTimeExpected) + deepStrictEqual(result, expectedOutput) + })) + it.effect("recur at Tuesday of each week", () => + Effect.gen(function*() { + const withDayOfWeek = (now: number, dayOfWeek: number): number => { + const date = new Date(now) + return date.setDate(date.getDate() + (7 + dayOfWeek - date.getDay()) % 7) + } + const originOffset = new Date().setHours(0, 0, 0, 0) + const tuesday = new Date(withDayOfWeek(originOffset, 2)) + const tuesdayHour = new Date(tuesday).setHours(1) + const monday = new Date(tuesday).setDate(tuesday.getDate() - 1) + const wednesday = new Date(tuesday).setDate(tuesday.getDate() + 1) + const input = Chunk.make(tuesdayHour, tuesday.getTime(), monday, wednesday).pipe( + Chunk.map((n) => [n, void 0] as const) + ) + const result = yield* pipe( + runManually(Schedule.dayOfWeek(2), input), + Effect.map((output) => output[0].pipe(Chunk.map((tuple) => tuple[0]))) + ) + const expectedTuesday = new Date(tuesday) + const nextTuesday = new Date(expectedTuesday).setDate(expectedTuesday.getDate() + 7) + const expectedOutput = Chunk.make( + expectedTuesday.getTime(), + nextTuesday, + expectedTuesday.getTime(), + nextTuesday + ) + deepStrictEqual(result, expectedOutput) + })) + it.effect("recur in the 2nd day of each month", () => + Effect.gen(function*() { + const originOffset = new Date(2020, 0, 1, 0, 0, 0).getTime() + const inTimeDate1 = new Date(new Date(originOffset).setDate(2)).setHours(1) + const inTimeDate2 = new Date(originOffset).setDate(2) + const before = new Date(originOffset).setDate(1) + const after = new Date(originOffset).setDate(2) + const input = Chunk.make(inTimeDate1, inTimeDate2, before, after).pipe(Chunk.map((n) => [n, void 0] as const)) + const result = yield* pipe( + runManually(Schedule.dayOfMonth(2), input), + Effect.map((output) => output[0].pipe(Chunk.map((tuple) => tuple[0]))) + ) + const expectedFirstInTime = new Date(new Date(originOffset).setDate(2)) + const expectedSecondInTime = new Date(expectedFirstInTime).setMonth(expectedFirstInTime.getMonth() + 1) + const expectedBefore = new Date(originOffset).setDate(2) + const expectedAfter = new Date(new Date(expectedBefore).setDate(2)).setMonth( + new Date(expectedBefore).getMonth() + 1 + ) + const expected = Chunk.make(expectedFirstInTime.getTime(), expectedSecondInTime, expectedBefore, expectedAfter) + deepStrictEqual(result, expected) + })) + it.effect("recur only in months containing valid number of days", () => + Effect.gen(function*() { + const originOffset = new Date(2020, 0, 31, 0, 0, 0).getTime() + const input = Chunk.of([originOffset, void 0] as const) + const result = yield* pipe( + runManually(Schedule.dayOfMonth(30), input), + Effect.map((output) => output[0].pipe(Chunk.map((tuple) => tuple[0]))) + ) + const expected = Chunk.make(new Date(originOffset).setMonth(2, 30)) + deepStrictEqual(result, expected) + })) + it.effect("union with cron like schedules", () => + Effect.gen(function*() { + const ref = yield* Ref.make>([]) + yield* TestClock.adjust("5 seconds") + const schedule = Schedule.spaced("20 seconds").pipe(Schedule.union(Schedule.secondOfMinute(30))) + yield* pipe( + TestClock.currentTimeMillis, + Effect.tap((instant) => Ref.update(ref, (seconds) => [...seconds, instant / 1000])), + Effect.repeat(schedule), + Effect.fork + ) + yield* TestClock.adjust("2 minutes") + const result = yield* Ref.get(ref) + const expected = [5, 25, 30, 50, 70, 90, 110] + deepStrictEqual(result, expected) + })) + it.effect("throw IllegalArgumentException on invalid `second` argument of `secondOfMinute`", () => + Effect.gen(function*() { + const input = Chunk.of(Date.now()) + const exit = yield* Effect.exit(runCollect(Schedule.secondOfMinute(60), input)) + const exception = new Cause.IllegalArgumentException( + "Invalid argument in: secondOfMinute(60). Must be in range 0...59" + ) + deepStrictEqual(exit, Exit.die(exception)) + })) + it.effect("throw IllegalArgumentException on invalid `minute` argument of `minuteOfHour`", () => + Effect.gen(function*() { + const input = Chunk.of(Date.now()) + const exit = yield* Effect.exit(runCollect(Schedule.minuteOfHour(60), input)) + const exception = new Cause.IllegalArgumentException( + "Invalid argument in: minuteOfHour(60). Must be in range 0...59" + ) + deepStrictEqual(exit, Exit.die(exception)) + })) + it.effect("throw IllegalArgumentException on invalid `hour` argument of `hourOfDay`", () => + Effect.gen(function*() { + const input = Chunk.of(Date.now()) + const exit = yield* Effect.exit(runCollect(Schedule.hourOfDay(24), input)) + const exception = new Cause.IllegalArgumentException( + "Invalid argument in: hourOfDay(24). Must be in range 0...23" + ) + deepStrictEqual(exit, Exit.die(exception)) + })) + it.effect("throw IllegalArgumentException on invalid `day` argument of `dayOfWeek`", () => + Effect.gen(function*() { + const input = Chunk.of(Date.now()) + const exit = yield* Effect.exit(runCollect(Schedule.dayOfWeek(8), input)) + const exception = new Cause.IllegalArgumentException( + "Invalid argument in: dayOfWeek(8). Must be in range 1 (Monday)...7 (Sunday)" + ) + deepStrictEqual(exit, Exit.die(exception)) + })) + it.effect("throw IllegalArgumentException on invalid `day` argument of `dayOfMonth`", () => + Effect.gen(function*() { + const input = Chunk.of(Date.now()) + const exit = yield* Effect.exit(runCollect(Schedule.dayOfMonth(32), input)) + const exception = new Cause.IllegalArgumentException( + "Invalid argument in: dayOfMonth(32). Must be in range 1...31" + ) + deepStrictEqual(exit, Exit.die(exception)) + })) + it.effect("tapOutput", () => + Effect.gen(function*() { + const log: Array = [] + const schedule = Schedule.once.pipe( + Schedule.as(1), + Schedule.tapOutput((x) => + Effect.sync(() => { + log.push(x) + }) + ) + ) + yield* Effect.void.pipe(Effect.schedule(schedule)) + deepStrictEqual(log, [1, 1]) + })) + }) +}) + +const format = (value: number): string => { + const date = new Date(value) + const hours = `0${date.getHours()}`.slice(-2) + const minutes = `0${date.getMinutes()}`.slice(-2) + const seconds = `0${date.getSeconds()}`.slice(-2) + return `${date.toDateString()} ${hours}:${minutes}:${seconds}` +} + +const ioSucceed = () => Effect.succeed("OrElse") +const ioFail = () => Effect.fail("OrElseFailed") +const failOn0 = (ref: Ref.Ref): Effect.Effect => { + return Effect.gen(function*() { + const i = yield* Ref.updateAndGet(ref, (n) => n + 1) + return yield* i <= 1 ? Effect.fail(`Error: ${i}`) : Effect.succeed(i) + }) +} +const alwaysFail = (ref: Ref.Ref): Effect.Effect => { + return Ref.updateAndGet(ref, (n) => n + 1).pipe(Effect.flatMap((n) => Effect.fail(`Error: ${n}`))) +} +const repeat = (schedule: Schedule.Schedule): Effect.Effect => { + return Ref.make(0).pipe( + Effect.flatMap((ref) => ref.pipe(Ref.updateAndGet((n) => n + 1), Effect.repeat(schedule))) + ) +} +const roundToNearestHour = (date: Date): number => { + date.setMinutes(date.getMinutes() + 30) + date.setMinutes(0, 0, 0) + return date.getMilliseconds() +} +const checkDelays = ( + schedule: Schedule.Schedule +): Effect.Effect< + readonly [ + Chunk.Chunk, + Chunk.Chunk + ], + never, + Env +> => { + return Effect.gen(function*() { + const now = yield* Effect.sync(() => Date.now()) + const input = Array.range(1, 5) + const actual = yield* pipe(schedule, Schedule.run(now, input)) + const expected = yield* pipe(Schedule.delays(schedule), Schedule.run(now, input)) + return [actual, expected] as const + }) +} +const checkRepetitions = (schedule: Schedule.Schedule): Effect.Effect< + readonly [ + Chunk.Chunk, + Chunk.Chunk + ], + never, + Env +> => { + return Effect.gen(function*() { + const now = yield* Effect.sync(() => Date.now()) + const input = Array.range(1, 5) + const actual = yield* pipe(schedule, Schedule.run(now, input)) + const expected = yield* pipe(Schedule.repetitions(schedule), Schedule.run(now, input)) + return [actual, expected] as const + }) +} +export const run = ( + effect: Effect.Effect +): Effect.Effect => { + return Effect.fork(effect).pipe( + Effect.tap(() => TestClock.setTime(Number.POSITIVE_INFINITY)), + Effect.flatMap(Fiber.join) + ) +} +export const runCollect = ( + schedule: Schedule.Schedule, + input: Iterable +): Effect.Effect, never, Env> => { + return run( + Schedule.driver(schedule).pipe( + Effect.flatMap((driver) => runCollectLoop(driver, Chunk.fromIterable(input), Chunk.empty())) + ) + ) +} +const runCollectLoop = ( + driver: Schedule.ScheduleDriver, + input: Chunk.Chunk, + acc: Chunk.Chunk +): Effect.Effect, never, Env> => { + if (!Chunk.isNonEmpty(input)) { + return Effect.succeed(acc) + } + const head = Chunk.headNonEmpty(input) + const tail = Chunk.tailNonEmpty(input) + return driver.next(head).pipe( + Effect.matchEffect({ + onFailure: () => + driver.last.pipe( + Effect.match({ + onFailure: () => acc, + onSuccess: (b) => Chunk.append(acc, b) + }) + ), + onSuccess: (b) => runCollectLoop(driver, tail, acc.pipe(Chunk.append(b))) + }) + ) +} +const runManually = ( + schedule: Schedule.Schedule, + inputs: Iterable< + readonly [ + number, + In + ] + > +): Effect.Effect< + readonly [ + Chunk.Chunk< + readonly [ + number, + Out + ] + >, + Option.Option + ], + never, + Env +> => { + return runManuallyLoop(schedule, schedule.initial, Chunk.fromIterable(inputs), Chunk.empty()) +} +const runManuallyLoop = ( + schedule: Schedule.Schedule, + state: unknown, + inputs: Chunk.Chunk< + readonly [ + number, + In + ] + >, + acc: Chunk.Chunk< + readonly [ + number, + Out + ] + > +): Effect.Effect< + readonly [ + Chunk.Chunk< + readonly [ + number, + Out + ] + >, + Option.Option + ], + never, + Env +> => { + if (!Chunk.isNonEmpty(inputs)) { + return Effect.succeed([Chunk.reverse(acc), Option.none()] as const) + } + const [offset, input] = Chunk.headNonEmpty(inputs) + const rest = Chunk.tailNonEmpty(inputs) + return schedule.step(offset, input, state).pipe( + Effect.flatMap(([state, out, decision]) => { + if (ScheduleDecision.isDone(decision)) { + return Effect.succeed([Chunk.reverse(acc), Option.some(out)] as const) + } + return runManuallyLoop( + schedule, + state, + rest, + acc.pipe(Chunk.prepend([ScheduleIntervals.start(decision.intervals), out] as const)) + ) + }) + ) +} +// TODO(Mike/Max): remove if added to `effect` +const scanLeft = (self: Chunk.Chunk, b: B, f: (b: B, a: A) => B): Chunk.Chunk => { + const len = self.length + const out = Array.allocate(len + 1) as Array + out[0] = b + for (let i = 0; i < len; i++) { + out[i + 1] = f(out[i], self.pipe(Chunk.unsafeGet(i))) + } + return Chunk.unsafeFromArray(out) +} diff --git a/repos/effect/packages/effect/test/Schema/Arbitrary/Arbitrary.test.ts b/repos/effect/packages/effect/test/Schema/Arbitrary/Arbitrary.test.ts new file mode 100644 index 0000000..08a67bf --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Arbitrary/Arbitrary.test.ts @@ -0,0 +1,1133 @@ +import { describe, it } from "@effect/vitest" +import { assertTrue, deepStrictEqual, strictEqual, throws } from "@effect/vitest/utils" +import type { FastCheck } from "effect" +import { Arbitrary, FastCheck as fc, Order, Predicate, Schema as S, SchemaAST } from "effect" +import * as Util from "../TestUtils.js" + +describe("Arb", () => { + describe("getDescription", () => { + describe("String", () => { + it("String", () => { + const schema = S.String + deepStrictEqual(Arbitrary.getDescription(schema.ast, []), { + _tag: "StringKeyword", + constraints: [], + path: [], + refinements: [], + annotations: [] + }) + }) + + it("String & minLength(2) & maxLength(5)", () => { + const schema = S.String.pipe(S.minLength(2), S.maxLength(5)) + const ast = schema.ast + assertTrue(SchemaAST.isRefinement(ast)) + assertTrue(SchemaAST.isRefinement(ast.from)) + deepStrictEqual(Arbitrary.getDescription(ast, []), { + _tag: "StringKeyword", + constraints: [ + { + _tag: "StringConstraints", + constraints: { + "minLength": 2 + } + }, + { + _tag: "StringConstraints", + constraints: { + maxLength: 5 + } + } + ], + path: [], + refinements: [ + ast.from, + ast + ], + annotations: [] + }) + }) + + it("String & annotation", () => { + const f = () => (fc: typeof FastCheck) => fc.constant("a") + const schema = S.String.annotations({ arbitrary: f }) + deepStrictEqual(Arbitrary.getDescription(schema.ast, []), { + _tag: "StringKeyword", + constraints: [], + path: [], + refinements: [], + annotations: [f] + }) + }) + + it("String & annotation & minLength(2)", () => { + const f = () => (fc: typeof FastCheck) => fc.constant("a") + const schema = S.String.annotations({ arbitrary: f }).pipe(S.minLength(2)) + const ast = schema.ast + assertTrue(SchemaAST.isRefinement(ast)) + deepStrictEqual(Arbitrary.getDescription(ast, []), { + _tag: "StringKeyword", + constraints: [ + { + _tag: "StringConstraints", + constraints: { + minLength: 2 + } + } + ], + path: [], + refinements: [ast], + annotations: [f] + }) + }) + + it("String & minLength(2) & annotation", () => { + const f = () => (fc: typeof FastCheck) => fc.constant("a") + const schema = S.String.pipe(S.minLength(2, { arbitrary: f })) + const ast = schema.ast + assertTrue(SchemaAST.isRefinement(ast)) + deepStrictEqual(Arbitrary.getDescription(ast, []), { + _tag: "StringKeyword", + constraints: [ + { + _tag: "StringConstraints", + constraints: { + minLength: 2 + } + } + ], + path: [], + refinements: [ast], + annotations: [f] + }) + }) + }) + }) + + describe("makeLazy", () => { + describe("Errors", () => { + it("should throw on `Declaration`s without annotations", () => { + const schema = S.declare(Predicate.isUnknown) + throws( + () => Arbitrary.makeLazy(schema), + new Error(`Missing annotation +details: Generating an Arbitrary for this schema requires an "arbitrary" annotation +schema (Declaration): `) + ) + throws( + () => Arbitrary.makeLazy(S.Tuple(S.declare(Predicate.isUnknown))), + new Error(`Missing annotation +at path: [0] +details: Generating an Arbitrary for this schema requires an "arbitrary" annotation +schema (Declaration): `) + ) + throws( + () => Arbitrary.makeLazy(S.Struct({ a: S.declare(Predicate.isUnknown) })), + new Error(`Missing annotation +at path: ["a"] +details: Generating an Arbitrary for this schema requires an "arbitrary" annotation +schema (Declaration): `) + ) + throws( + () => + Arbitrary.makeLazy( + S.Record({ key: S.String, value: S.declare(Predicate.isUnknown) }) + ), + new Error(`Missing annotation +details: Generating an Arbitrary for this schema requires an "arbitrary" annotation +schema (Declaration): `) + ) + }) + + it("should throw on `NeverKeyword`s without annotations", () => { + throws( + () => Arbitrary.makeLazy(S.Never), + new Error(`Missing annotation +details: Generating an Arbitrary for this schema requires an "arbitrary" annotation +schema (NeverKeyword): never`) + ) + }) + + it("should throw on `Enums`s with no enums", () => { + enum Fruits {} + const schema = S.Enums(Fruits) + throws( + () => Arbitrary.makeLazy(schema)(fc), + new Error(`Empty Enums schema +details: Generating an Arbitrary for this schema requires at least one enum`) + ) + }) + }) + + describe("Unrefined Primitives", () => { + it("Void", () => { + Util.assertions.arbitrary.validateGeneratedValues(S.Void) + }) + + it("String", () => { + const schema = S.String + Util.assertions.arbitrary.validateGeneratedValues(schema) + }) + + it("Number", () => { + Util.assertions.arbitrary.validateGeneratedValues(S.Number) + }) + + it("Boolean", () => { + Util.assertions.arbitrary.validateGeneratedValues(S.Boolean) + }) + + it("BigIntFromSelf", () => { + Util.assertions.arbitrary.validateGeneratedValues(S.BigIntFromSelf) + }) + + it("SymbolFromSelf", () => { + Util.assertions.arbitrary.validateGeneratedValues(S.SymbolFromSelf) + }) + + it("Object", () => { + Util.assertions.arbitrary.validateGeneratedValues(S.Object) + }) + + it("Any", () => { + Util.assertions.arbitrary.validateGeneratedValues(S.Any) + }) + + it("Unknown", () => { + Util.assertions.arbitrary.validateGeneratedValues(S.Unknown) + }) + + it("UniqueSymbolFromSelf", () => { + const a = Symbol.for("effect/Schema/test/a") + const schema = S.UniqueSymbolFromSelf(a) + Util.assertions.arbitrary.validateGeneratedValues(schema) + }) + + describe("Literal", () => { + it("single literal", () => { + const schema = S.Literal(1) + Util.assertions.arbitrary.validateGeneratedValues(schema) + }) + + it("multiple literals", () => { + const schema = S.Literal(1, "a") + Util.assertions.arbitrary.validateGeneratedValues(schema) + }) + }) + + describe("TemplateLiteral", () => { + it("a", () => { + const schema = S.TemplateLiteral(S.Literal("a")) + Util.assertions.arbitrary.validateGeneratedValues(schema) + }) + + it("a b", () => { + const schema = S.TemplateLiteral(S.Literal("a"), S.Literal(" "), S.Literal("b")) + Util.assertions.arbitrary.validateGeneratedValues(schema) + }) + + it("a${string}", () => { + const schema = S.TemplateLiteral(S.Literal("a"), S.String) + Util.assertions.arbitrary.validateGeneratedValues(schema) + }) + + it("a${number}", () => { + const schema = S.TemplateLiteral(S.Literal("a"), S.Number) + Util.assertions.arbitrary.validateGeneratedValues(schema) + }) + + it("a", () => { + const schema = S.TemplateLiteral(S.Literal("a")) + Util.assertions.arbitrary.validateGeneratedValues(schema) + }) + + it("${string}", () => { + const schema = S.TemplateLiteral(S.String) + Util.assertions.arbitrary.validateGeneratedValues(schema) + }) + + it("a${string}b", () => { + const schema = S.TemplateLiteral(S.Literal("a"), S.String, S.Literal("b")) + Util.assertions.arbitrary.validateGeneratedValues(schema) + }) + + it("https://www.typescriptlang.org/docs/handbook/2/template-literal-types.html", async () => { + const EmailLocaleIDs = S.Literal("welcome_email", "email_heading") + const FooterLocaleIDs = S.Literal("footer_title", "footer_sendoff") + const schema = S.TemplateLiteral(S.Union(EmailLocaleIDs, FooterLocaleIDs), "_id") + Util.assertions.arbitrary.validateGeneratedValues(schema) + }) + + it("< + h + (1|2) + >", async () => { + const schema = S.TemplateLiteral("<", S.TemplateLiteral("h", S.Literal(1, 2)), ">") + Util.assertions.arbitrary.validateGeneratedValues(schema) + }) + }) + + describe("Enums", () => { + it("Numeric enums", () => { + enum Fruits { + Apple, + Banana + } + const schema = S.Enums(Fruits) + Util.assertions.arbitrary.validateGeneratedValues(schema) + }) + + it("String enums", () => { + enum Fruits { + Apple = "apple", + Banana = "banana", + Cantaloupe = 0 + } + const schema = S.Enums(Fruits) + Util.assertions.arbitrary.validateGeneratedValues(schema) + }) + + it("Const enums", () => { + const Fruits = { + Apple: "apple", + Banana: "banana", + Cantaloupe: 3 + } as const + const schema = S.Enums(Fruits) + Util.assertions.arbitrary.validateGeneratedValues(schema) + }) + }) + + describe("Struct", () => { + it("fields", () => { + const schema = S.Struct({ a: S.String, b: S.Number }) + Util.assertions.arbitrary.validateGeneratedValues(schema) + }) + + it("fields + record", () => { + const schema = S.Struct({ a: S.String }, S.Record({ key: S.String, value: S.Union(S.String, S.Number) })) + Util.assertions.arbitrary.validateGeneratedValues(schema) + }) + + it("required property signature", () => { + const schema = S.Struct({ a: S.Number }) + Util.assertions.arbitrary.validateGeneratedValues(schema) + }) + + it("required property signature with undefined", () => { + const schema = S.Struct({ a: S.Union(S.Number, S.Undefined) }) + Util.assertions.arbitrary.validateGeneratedValues(schema) + }) + + it("optional property signature", () => { + const schema = S.Struct({ a: S.optionalWith(S.Number, { exact: true }) }) + Util.assertions.arbitrary.validateGeneratedValues(schema) + }) + + it("optional property signature with undefined", () => { + const schema = S.Struct({ + a: S.optionalWith(S.Union(S.Number, S.Undefined), { exact: true }) + }) + Util.assertions.arbitrary.validateGeneratedValues(schema) + }) + }) + + describe("Record", () => { + it("Record(string, string)", () => { + const schema = S.Record({ key: S.String, value: S.String }) + Util.assertions.arbitrary.validateGeneratedValues(schema) + }) + + it("Record(symbol, string)", () => { + const schema = S.Record({ key: S.SymbolFromSelf, value: S.String }) + Util.assertions.arbitrary.validateGeneratedValues(schema) + }) + }) + + it("Union", () => { + const schema = S.Union(S.String, S.Number) + Util.assertions.arbitrary.validateGeneratedValues(schema) + }) + + describe("Tuple", () => { + it("empty", () => { + const schema = S.Tuple() + Util.assertions.arbitrary.validateGeneratedValues(schema) + }) + + it("required element", () => { + const schema = S.Tuple(S.Number) + Util.assertions.arbitrary.validateGeneratedValues(schema) + }) + + it("required element with undefined", () => { + const schema = S.Tuple(S.Union(S.Number, S.Undefined)) + Util.assertions.arbitrary.validateGeneratedValues(schema) + }) + + it("optional element", () => { + const schema = S.Tuple(S.optionalElement(S.Number)) + Util.assertions.arbitrary.validateGeneratedValues(schema) + }) + + it("optional element with undefined", () => { + const schema = S.Tuple(S.optionalElement(S.Union(S.Number, S.Undefined))) + Util.assertions.arbitrary.validateGeneratedValues(schema) + }) + + it("e e?", () => { + const schema = S.Tuple(S.String, S.optionalElement(S.Number)) + Util.assertions.arbitrary.validateGeneratedValues(schema) + }) + + it("e r", () => { + const schema = S.Tuple([S.String], S.Number) + Util.assertions.arbitrary.validateGeneratedValues(schema) + }) + + it("e? r", () => { + const schema = S.Tuple([S.optionalElement(S.String)], S.Number) + Util.assertions.arbitrary.validateGeneratedValues(schema) + }) + + it("r", () => { + const schema = S.Array(S.Number) + Util.assertions.arbitrary.validateGeneratedValues(schema) + }) + + it("r e", () => { + const schema = S.Tuple([], S.String, S.Number) + Util.assertions.arbitrary.validateGeneratedValues(schema) + }) + + it("e r e", () => { + const schema = S.Tuple([S.String], S.Number, S.Boolean) + Util.assertions.arbitrary.validateGeneratedValues(schema) + }) + }) + + describe("suspend", () => { + it("should support an arbitrary annotation", () => { + interface A { + readonly a: string + readonly as: ReadonlyArray + } + const arb: fc.Arbitrary = fc.letrec((tie) => ({ + root: fc.record({ + a: fc.string(), + as: fc.oneof( + { depthSize: "small" }, + fc.constant([]), + fc.array(tie("root")) + ) + }) + })).root + const schema = S.Struct({ + a: S.String, + as: S.Array( + S.suspend((): S.Schema => schema).annotations({ arbitrary: () => () => arb }) + ) + }) + Util.assertions.arbitrary.validateGeneratedValues(schema) + }) + + it("make(S.encodedSchema(schema))", () => { + const NumberFromString = S.NumberFromString + interface I { + readonly a: string | I + } + interface A { + readonly a: number | A + } + const schema = S.Struct({ + a: S.Union(NumberFromString, S.suspend((): S.Schema => schema)) + }) + + Util.assertions.arbitrary.validateGeneratedValues(S.encodedSchema(schema)) + }) + + it("Tuple", () => { + type A = readonly [number, A | null] + const schema = S.Tuple( + S.Number, + S.NullOr(S.suspend((): S.Schema => schema)) + ) + Util.assertions.arbitrary.validateGeneratedValues(schema) + }) + + it("Array", () => { + const Rec = S.suspend((): any => schema) + const schema: any = S.Array(S.Union(S.String, Rec)) + Util.assertions.arbitrary.validateGeneratedValues(schema) + }) + + it("Struct", () => { + interface A { + readonly a: string + readonly as: ReadonlyArray + } + const schema = S.Struct({ + a: S.String, + as: S.Array(S.suspend((): S.Schema => schema)) + }) + Util.assertions.arbitrary.validateGeneratedValues(schema) + }) + + it("Record", () => { + type A = { + [_: string]: A + } + const schema = S.Record({ key: S.String, value: S.suspend((): S.Schema => schema) }) + Util.assertions.arbitrary.validateGeneratedValues(schema) + }) + + it("optional", () => { + const Rec = S.suspend((): any => schema) + const schema: any = S.Struct({ + a: S.optional(Rec) + }) + Util.assertions.arbitrary.validateGeneratedValues(schema) + }) + + it("Array + Array", () => { + const Rec = S.suspend((): any => schema) + const schema: any = S.Struct({ + a: S.Array(Rec), + b: S.Array(Rec) + }) + Util.assertions.arbitrary.validateGeneratedValues(schema) + }) + + it("optional + Array", () => { + const Rec = S.suspend((): any => schema) + const schema: any = S.Struct({ + a: S.optional(Rec), + b: S.Array(Rec) + }) + Util.assertions.arbitrary.validateGeneratedValues(schema) + }) + + it.skip("mutually suspended schemas", { retry: 5 }, () => { + interface Expression { + readonly type: "expression" + readonly value: number | Operation + } + + interface Operation { + readonly type: "operation" + readonly operator: "+" | "-" + readonly left: Expression + readonly right: Expression + } + + const Expression = S.Struct({ + type: S.Literal("expression"), + value: S.Union(S.JsonNumber, S.suspend((): S.Schema => Operation)) + }) + + const Operation = S.Struct({ + type: S.Literal("operation"), + operator: S.Union(S.Literal("+"), S.Literal("-")), + left: Expression, + right: Expression + }) + Util.assertions.arbitrary.validateGeneratedValues(Operation) + }) + }) + + describe("Transformation", () => { + it("clamp", () => { + const schema = S.Number.pipe(S.clamp(1.3, 3.1)) + Util.assertions.arbitrary.validateGeneratedValues(schema) + }) + }) + }) + + describe("Data Types", () => { + describe("Unrefined", () => { + it("DateFromSelf", () => { + const schema = S.DateFromSelf + Util.assertions.arbitrary.validateGeneratedValues(schema) + }) + + it("DurationFromSelf", () => { + const schema = S.DurationFromSelf + Util.assertions.arbitrary.validateGeneratedValues(schema) + }) + }) + + describe("Suspend", () => { + it("RedactedFromSelf", () => { + const Rec = S.suspend((): any => schema) + const schema: any = S.RedactedFromSelf(S.NullOr(Rec)) + Util.assertions.arbitrary.validateGeneratedValues(schema) + }) + + it("OptionFromSelf", () => { + const Rec = S.suspend((): any => schema) + const schema: any = S.OptionFromSelf(Rec) + Util.assertions.arbitrary.validateGeneratedValues(schema) + }) + + it("EitherFromSelf", () => { + const Rec = S.suspend((): any => schema) + const schema: any = S.EitherFromSelf({ left: S.String, right: Rec }) + Util.assertions.arbitrary.validateGeneratedValues(schema) + }) + + it("MapFromSelf", () => { + const Rec = S.suspend((): any => schema) + const schema: any = S.MapFromSelf({ key: S.String, value: Rec }) + Util.assertions.arbitrary.validateGeneratedValues(schema) + }) + + it("SetFromSelf", () => { + const Rec = S.suspend((): any => schema) + const schema: any = S.SetFromSelf(Rec) + Util.assertions.arbitrary.validateGeneratedValues(schema) + }) + + it("ChunkFromSelf", () => { + const Rec = S.suspend((): any => schema) + const schema: any = S.ChunkFromSelf(Rec) + Util.assertions.arbitrary.validateGeneratedValues(schema) + }) + + it("HashSetFromSelf", () => { + const Rec = S.suspend((): any => schema) + const schema: any = S.HashSetFromSelf(Rec) + Util.assertions.arbitrary.validateGeneratedValues(schema) + }) + + it("HashMapFromSelf", () => { + const Rec = S.suspend((): any => schema) + const schema: any = S.HashMapFromSelf({ key: S.String, value: Rec }) + Util.assertions.arbitrary.validateGeneratedValues(schema) + }) + + it("ListFromSelf", () => { + const Rec = S.suspend((): any => schema) + const schema: any = S.ListFromSelf(Rec) + Util.assertions.arbitrary.validateGeneratedValues(schema) + }) + + it("SortedSetFromSelf", () => { + const Rec = S.suspend((): any => schema) + const schema: any = S.SortedSetFromSelf(Rec, Order.empty(), Order.empty()) + Util.assertions.arbitrary.validateGeneratedValues(schema) + }) + + describe("DateTime", () => { + it("DateTimeUtcFromSelf", () => { + Util.assertions.arbitrary.validateGeneratedValues(S.DateTimeUtcFromSelf) + }) + + it("TimeZoneOffsetFromSelf", () => { + Util.assertions.arbitrary.validateGeneratedValues(S.TimeZoneOffsetFromSelf) + }) + + it("TimeZoneNamedFromSelf", () => { + Util.assertions.arbitrary.validateGeneratedValues(S.TimeZoneNamedFromSelf) + }) + + it("DateTimeZonedFromSelf", () => { + Util.assertions.arbitrary.validateGeneratedValues(S.DateTimeZonedFromSelf) + }) + }) + }) + }) + + describe("Refinement", () => { + const assertConstraints = ( + schema: S.Schema, + constraints: ReadonlyArray< + | ReturnType + | ReturnType + | ReturnType + | ReturnType + | ReturnType + > + ) => { + const description = Arbitrary.getDescription(schema.ast, []) + switch (description._tag) { + case "StringKeyword": { + assertTrue(constraints.every((c) => c._tag === "StringConstraints")) + deepStrictEqual(description.constraints, constraints) + break + } + case "NumberKeyword": { + assertTrue(constraints.every((c) => c._tag === "NumberConstraints")) + deepStrictEqual(description.constraints, constraints) + break + } + case "BigIntKeyword": { + assertTrue(constraints.every((c) => c._tag === "BigIntConstraints")) + deepStrictEqual(description.constraints, constraints) + break + } + case "DateFromSelf": { + assertTrue(constraints.every((c) => c._tag === "DateConstraints")) + deepStrictEqual(description.constraints, constraints) + break + } + case "TupleType": { + assertTrue(constraints.every((c) => c._tag === "ArrayConstraints")) + deepStrictEqual(description.constraints, constraints) + break + } + } + } + + describe("array filters", () => { + it("Array", () => { + const schema = S.Array(S.String).pipe(S.filter(() => true)) + assertConstraints(schema, [Arbitrary.makeArrayConstraints({})]) + Util.assertions.arbitrary.validateGeneratedValues(schema) + }) + + it("minItems (Array)", () => { + const schema = S.Array(S.String).pipe(S.minItems(2)) + assertConstraints(schema, [Arbitrary.makeArrayConstraints({ minLength: 2 })]) + Util.assertions.arbitrary.validateGeneratedValues(schema) + }) + + it("minItems (NonEmptyArray)", () => { + const schema = S.NonEmptyArray(S.String).pipe(S.minItems(2)) + assertConstraints(schema, [Arbitrary.makeArrayConstraints({ minLength: 2 })]) + Util.assertions.arbitrary.validateGeneratedValues(schema) + }) + + it("maxItems (Array)", () => { + const schema = S.Array(S.String).pipe(S.maxItems(5)) + assertConstraints(schema, [Arbitrary.makeArrayConstraints({ maxLength: 5 })]) + Util.assertions.arbitrary.validateGeneratedValues(schema) + }) + + it("maxItems (NonEmptyArray)", () => { + const schema = S.NonEmptyArray(S.String).pipe(S.maxItems(5)) + assertConstraints(schema, [Arbitrary.makeArrayConstraints({ maxLength: 5 })]) + Util.assertions.arbitrary.validateGeneratedValues(schema) + }) + + it("itemsCount (Array)", () => { + const schema = S.Array(S.String).pipe(S.itemsCount(3)) + assertConstraints(schema, [Arbitrary.makeArrayConstraints({ minLength: 3, maxLength: 3 })]) + Util.assertions.arbitrary.validateGeneratedValues(schema) + }) + + it("itemsCount (NonEmptyArray)", () => { + const schema = S.NonEmptyArray(S.String).pipe(S.itemsCount(3)) + assertConstraints(schema, [Arbitrary.makeArrayConstraints({ minLength: 3, maxLength: 3 })]) + Util.assertions.arbitrary.validateGeneratedValues(schema) + }) + }) + + describe("string filters", () => { + it("String", () => { + const schema = S.String.pipe(S.filter(() => true)) + assertConstraints(schema, [Arbitrary.makeStringConstraints({})]) + Util.assertions.arbitrary.validateGeneratedValues(schema) + }) + + it("minLength", () => { + const schema = S.String.pipe(S.minLength(2)) + assertConstraints(schema, [Arbitrary.makeStringConstraints({ minLength: 2 })]) + Util.assertions.arbitrary.validateGeneratedValues(schema) + }) + + it("maxLength", () => { + const schema = S.String.pipe(S.maxLength(5)) + assertConstraints(schema, [Arbitrary.makeStringConstraints({ maxLength: 5 })]) + Util.assertions.arbitrary.validateGeneratedValues(schema) + }) + + it("length: number", () => { + const schema = S.String.pipe(S.length(10)) + assertConstraints(schema, [Arbitrary.makeStringConstraints({ minLength: 10, maxLength: 10 })]) + Util.assertions.arbitrary.validateGeneratedValues(schema) + }) + + it("length: { min, max }", () => { + const schema = S.String.pipe(S.length({ min: 2, max: 5 })) + assertConstraints(schema, [Arbitrary.makeStringConstraints({ minLength: 2, maxLength: 5 })]) + Util.assertions.arbitrary.validateGeneratedValues(schema) + }) + + it("minLength + maxLength", () => { + const schema = S.String.pipe(S.minLength(2), S.maxLength(5)) + assertConstraints(schema, [ + Arbitrary.makeStringConstraints({ minLength: 2 }), + Arbitrary.makeStringConstraints({ maxLength: 5 }) + ]) + Util.assertions.arbitrary.validateGeneratedValues(schema) + }) + + it("annotation + minLength + maxLength", () => { + const schema = S.String.annotations({ arbitrary: () => (fc) => fc.string() }).pipe( + S.minLength(2), + S.maxLength(5) + ) + assertConstraints(schema, [ + Arbitrary.makeStringConstraints({ minLength: 2 }), + Arbitrary.makeStringConstraints({ maxLength: 5 }) + ]) + Util.assertions.arbitrary.validateGeneratedValues(schema) + }) + + it("minLength + maxLength + annotation", () => { + const schema = S.String.pipe( + S.minLength(2), + S.maxLength(5) + ).annotations({ arbitrary: () => (fc) => fc.string() }) + assertConstraints(schema, [ + Arbitrary.makeStringConstraints({ minLength: 2 }), + Arbitrary.makeStringConstraints({ maxLength: 5 }) + ]) + Util.assertions.arbitrary.validateGeneratedValues(schema) + }) + + it("startsWith", () => { + const schema = S.String.pipe(S.startsWith("a")) + assertConstraints(schema, [Arbitrary.makeStringConstraints({ pattern: "^a" })]) + Util.assertions.arbitrary.validateGeneratedValues(schema) + }) + + it("endsWith", () => { + const schema = S.String.pipe(S.endsWith("a")) + assertConstraints(schema, [Arbitrary.makeStringConstraints({ pattern: "^.*a$" })]) + Util.assertions.arbitrary.validateGeneratedValues(schema) + }) + + it("pattern", () => { + const regex = /^[A-Z]{3}[0-9]{3}$/ + const schema = S.String.pipe(S.pattern(regex)) + assertConstraints(schema, [Arbitrary.makeStringConstraints({ pattern: regex.source })]) + Util.assertions.arbitrary.validateGeneratedValues(schema) + }) + + it("nonEmptyString + pattern", () => { + const regex = /^[-]*$/ + const schema = S.String.pipe(S.nonEmptyString(), S.pattern(regex)) + assertConstraints(schema, [ + Arbitrary.makeStringConstraints({ minLength: 1 }), + Arbitrary.makeStringConstraints({ pattern: regex.source }) + ]) + Util.assertions.arbitrary.validateGeneratedValues(schema) + }) + + it("pattern + pattern", () => { + const regexp1 = /^[^A-Z]*$/ + const regexp2 = /^0x[0-9a-f]{40}$/ + const schema = S.String.pipe(S.pattern(regexp1), S.pattern(regexp2)) + assertConstraints( + schema, + [ + Arbitrary.makeStringConstraints({ pattern: regexp1.source }), + Arbitrary.makeStringConstraints({ pattern: regexp2.source }) + ] + ) + Util.assertions.arbitrary.validateGeneratedValues(schema) + }) + }) + + describe("number filters", () => { + it("Number", () => { + const schema = S.Number.pipe(S.filter(() => true)) + assertConstraints(schema, [Arbitrary.makeNumberConstraints({})]) + Util.assertions.arbitrary.validateGeneratedValues(schema) + }) + + it("nonNaN", () => { + const schema = S.Number.pipe(S.nonNaN()) + assertConstraints(schema, [Arbitrary.makeNumberConstraints({ noNaN: true })]) + Util.assertions.arbitrary.validateGeneratedValues(schema) + }) + + it("finite", () => { + const schema = S.Number.pipe(S.finite()) + assertConstraints(schema, [Arbitrary.makeNumberConstraints({ noNaN: true, noDefaultInfinity: true })]) + Util.assertions.arbitrary.validateGeneratedValues(schema) + }) + + it("JsonNumber", () => { + const schema = S.JsonNumber + assertConstraints(schema, [Arbitrary.makeNumberConstraints({ noDefaultInfinity: true, noNaN: true })]) + Util.assertions.arbitrary.validateGeneratedValues(schema) + }) + + it("int", () => { + const schema = S.Number.pipe(S.int()) + assertConstraints(schema, [Arbitrary.makeNumberConstraints({ isInteger: true })]) + Util.assertions.arbitrary.validateGeneratedValues(schema) + }) + + it("between int", () => { + const schema = S.Number.pipe(S.between(2, 5), S.int()) + assertConstraints(schema, [ + Arbitrary.makeNumberConstraints({ min: 2, max: 5 }), + Arbitrary.makeNumberConstraints({ isInteger: true }) + ]) + Util.assertions.arbitrary.validateGeneratedValues(schema) + }) + + it("int between", () => { + const schema = S.Number.pipe(S.int(), S.between(2, 5)) + assertConstraints(schema, [ + Arbitrary.makeNumberConstraints({ isInteger: true }), + Arbitrary.makeNumberConstraints({ min: 2, max: 5 }) + ]) + Util.assertions.arbitrary.validateGeneratedValues(schema) + }) + + it("lessThanOrEqualTo", () => { + const schema = S.Number.pipe(S.lessThanOrEqualTo(5)) + assertConstraints(schema, [Arbitrary.makeNumberConstraints({ max: 5 })]) + Util.assertions.arbitrary.validateGeneratedValues(schema) + }) + + it("greaterThanOrEqualTo", () => { + const schema = S.Number.pipe(S.greaterThanOrEqualTo(2)) + assertConstraints(schema, [Arbitrary.makeNumberConstraints({ min: 2 })]) + Util.assertions.arbitrary.validateGeneratedValues(schema) + }) + + it("lessThan", () => { + const schema = S.Number.pipe(S.lessThan(5)) + assertConstraints(schema, [Arbitrary.makeNumberConstraints({ max: 5, maxExcluded: true })]) + Util.assertions.arbitrary.validateGeneratedValues(schema) + }) + + it("greaterThan", () => { + const schema = S.Number.pipe(S.greaterThan(2)) + assertConstraints(schema, [Arbitrary.makeNumberConstraints({ min: 2, minExcluded: true })]) + Util.assertions.arbitrary.validateGeneratedValues(schema) + }) + + it("between", () => { + const schema = S.Number.pipe(S.between(2, 5)) + assertConstraints(schema, [Arbitrary.makeNumberConstraints({ min: 2, max: 5 })]) + Util.assertions.arbitrary.validateGeneratedValues(schema) + }) + }) + + describe("bigint filters", () => { + it("BigIntFromSelf", () => { + const schema = S.BigIntFromSelf.pipe(S.filter(() => true)) + assertConstraints(schema, []) + Util.assertions.arbitrary.validateGeneratedValues(schema) + }) + + it("lessThanOrEqualTo", () => { + const schema = S.BigIntFromSelf.pipe(S.lessThanOrEqualToBigInt(BigInt(5))) + assertConstraints(schema, [Arbitrary.makeBigIntConstraints({ max: BigInt(5) })]) + Util.assertions.arbitrary.validateGeneratedValues(schema) + }) + + it("greaterThanOrEqualTo", () => { + const schema = S.BigIntFromSelf.pipe(S.greaterThanOrEqualToBigInt(BigInt(2))) + assertConstraints(schema, [Arbitrary.makeBigIntConstraints({ min: BigInt(2) })]) + Util.assertions.arbitrary.validateGeneratedValues(schema) + }) + + it("lessThan", () => { + const schema = S.BigIntFromSelf.pipe(S.lessThanBigInt(BigInt(5))) + assertConstraints(schema, [Arbitrary.makeBigIntConstraints({ max: BigInt(5) })]) + Util.assertions.arbitrary.validateGeneratedValues(schema) + }) + + it("greaterThan", () => { + const schema = S.BigIntFromSelf.pipe(S.greaterThanBigInt(BigInt(2))) + assertConstraints(schema, [Arbitrary.makeBigIntConstraints({ min: BigInt(2) })]) + Util.assertions.arbitrary.validateGeneratedValues(schema) + }) + + it("between", () => { + const schema = S.BigIntFromSelf.pipe(S.betweenBigInt(BigInt(2), BigInt(5))) + assertConstraints(schema, [Arbitrary.makeBigIntConstraints({ min: BigInt(2), max: BigInt(5) })]) + Util.assertions.arbitrary.validateGeneratedValues(schema) + }) + }) + + describe("date filters", () => { + it("DateFromSelf", () => { + const schema = S.DateFromSelf + assertConstraints(schema, [ + Arbitrary.makeDateConstraints({ noInvalidDate: false }) + ]) + Util.assertions.arbitrary.validateGeneratedValues(schema) + }) + + it("ValidDateFromSelf", () => { + const schema = S.ValidDateFromSelf + assertConstraints(schema, [ + Arbitrary.makeDateConstraints({ noInvalidDate: false }), + Arbitrary.makeDateConstraints({ noInvalidDate: true }) + ]) + Util.assertions.arbitrary.validateGeneratedValues(schema) + }) + + it("lessThanOrEqualTo", () => { + const schema = S.DateFromSelf.pipe(S.lessThanOrEqualToDate(new Date(5))) + assertConstraints(schema, [ + Arbitrary.makeDateConstraints({ noInvalidDate: false }), + Arbitrary.makeDateConstraints({ max: new Date(5) }) + ]) + Util.assertions.arbitrary.validateGeneratedValues(schema) + }) + + it("greaterThanOrEqualTo", () => { + const schema = S.DateFromSelf.pipe(S.greaterThanOrEqualToDate(new Date(2))) + assertConstraints(schema, [ + Arbitrary.makeDateConstraints({ noInvalidDate: false }), + Arbitrary.makeDateConstraints({ min: new Date(2) }) + ]) + Util.assertions.arbitrary.validateGeneratedValues(schema) + }) + + it("lessThan", () => { + const schema = S.DateFromSelf.pipe(S.lessThanDate(new Date(5))) + assertConstraints(schema, [ + Arbitrary.makeDateConstraints({ noInvalidDate: false }), + Arbitrary.makeDateConstraints({ max: new Date(5) }) + ]) + Util.assertions.arbitrary.validateGeneratedValues(schema) + }) + + it("greaterThan", () => { + const schema = S.DateFromSelf.pipe(S.greaterThanDate(new Date(2))) + assertConstraints(schema, [ + Arbitrary.makeDateConstraints({ noInvalidDate: false }), + Arbitrary.makeDateConstraints({ min: new Date(2) }) + ]) + Util.assertions.arbitrary.validateGeneratedValues(schema) + }) + + it("between", () => { + const schema = S.DateFromSelf.pipe(S.betweenDate(new Date(2), new Date(5))) + assertConstraints( + schema, + [ + Arbitrary.makeDateConstraints({ noInvalidDate: false }), + Arbitrary.makeDateConstraints({ min: new Date(2), max: new Date(5) }) + ] + ) + Util.assertions.arbitrary.validateGeneratedValues(schema) + }) + }) + }) + + describe("Annotations", () => { + const assertAnnotation = (source: S.Schema) => { + const schema = source.annotations({ arbitrary: () => (fc) => fc.constant("custom arbitrary") as any }) + const arb = Arbitrary.make(schema) + strictEqual(fc.sample(arb, 1)[0], "custom arbitrary" as any) + } + + it("Never", () => { + assertAnnotation(S.Never) + }) + + it("Void", () => { + assertAnnotation(S.Void) + }) + + it("Literal", () => { + assertAnnotation(S.Literal("a")) + }) + + it("Symbol", () => { + assertAnnotation(S.Symbol) + }) + + it("UniqueSymbolFromSelf", () => { + assertAnnotation(S.UniqueSymbolFromSelf(Symbol.for("effect/schema/test/a"))) + }) + + it("TemplateLiteral", () => { + assertAnnotation(S.TemplateLiteral(S.Literal("a"), S.String, S.Literal("b"))) + }) + + it("Undefined", () => { + assertAnnotation(S.Undefined) + }) + + it("Unknown", () => { + assertAnnotation(S.Unknown) + }) + + it("Any", () => { + assertAnnotation(S.Any) + }) + + it("Object", () => { + assertAnnotation(S.Object) + }) + + it("String", () => { + assertAnnotation(S.String) + }) + + it("Number", () => { + assertAnnotation(S.Number) + }) + + it("Boolean", () => { + assertAnnotation(S.Boolean) + }) + + it("BigIntFromSelf", () => { + assertAnnotation(S.BigIntFromSelf) + }) + + it("Enums", () => { + enum Fruits { + Apple, + Banana + } + assertAnnotation(S.Enums(Fruits)) + }) + + it("Tuple", () => { + assertAnnotation(S.Tuple(S.String, S.Number)) + }) + + it("Struct", () => { + assertAnnotation(S.Struct({ a: S.String, b: S.Number })) + }) + + it("Union", () => { + assertAnnotation(S.Union(S.String, S.Number)) + }) + + it("suspend", () => { + interface A { + readonly a: string + readonly as: ReadonlyArray + } + const schema = S.Struct({ + a: S.String, + as: S.Array(S.suspend((): S.Schema => schema)) + }) + assertAnnotation(schema) + }) + + describe("Refinement", () => { + it("should provide the `from` Arbitrary", () => { + const schema = S.String.pipe(S.filter((s) => s.length > 2, { + arbitrary: (from, ctx) => (fc) => { + assertTrue(Predicate.isFunction(from)) + assertTrue(Predicate.isObject(ctx)) + return from(fc) + } + })) + Util.assertions.arbitrary.validateGeneratedValues(schema) + }) + + it("NonEmptyString", () => { + assertAnnotation(S.NonEmptyString) + }) + }) + + it("Transformation", () => { + assertAnnotation(S.NumberFromString) + }) + }) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Arbitrary/Class.test.ts b/repos/effect/packages/effect/test/Schema/Arbitrary/Class.test.ts new file mode 100644 index 0000000..22857c1 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Arbitrary/Class.test.ts @@ -0,0 +1,51 @@ +import { describe, it } from "@effect/vitest" +import { Arbitrary, FastCheck, Schema as S } from "effect" +import * as Util from "../TestUtils.js" + +describe("Class", () => { + it("baseline", () => { + class Class extends S.Class("Class")({ + a: S.String, + b: S.NumberFromString + }) {} + Util.assertions.arbitrary.validateGeneratedValues(Class) + }) + + it("required property signature", () => { + class Class extends S.Class("Class")({ + a: S.Number + }) {} + Util.assertions.arbitrary.validateGeneratedValues(Class) + }) + + it("required property signature with undefined", () => { + class Class extends S.Class("Class")({ + a: S.Union(S.Number, S.Undefined) + }) {} + Util.assertions.arbitrary.validateGeneratedValues(Class) + }) + + it("exact optional property signature", () => { + class Class extends S.Class("Class")({ + a: S.optionalWith(S.Number, { exact: true }) + }) {} + Util.assertions.arbitrary.validateGeneratedValues(Class) + }) + + it("exact optional property signature with undefined", () => { + class Class extends S.Class("Class")({ + a: S.optionalWith(S.Union(S.Number, S.Undefined), { exact: true }) + }) {} + Util.assertions.arbitrary.validateGeneratedValues(Class) + }) + + it("transformation property signature with annotation (#4550)", () => { + class Class extends S.Class("Class")({ + a: S.NumberFromString.annotations({ + arbitrary: () => (fc) => fc.constant(1) + }) + }) {} + const arb = Arbitrary.make(Class) + FastCheck.assert(FastCheck.property(arb, (a) => a.a === 1)) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/JSONSchema.new.test.ts b/repos/effect/packages/effect/test/Schema/JSONSchema.new.test.ts new file mode 100644 index 0000000..55e9332 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/JSONSchema.new.test.ts @@ -0,0 +1,4637 @@ +import { assertFalse, assertTrue, deepStrictEqual, strictEqual, throws } from "@effect/vitest/utils" +import type { Options as AjvOptions } from "ajv" +import Ajv from "ajv" +import * as JSONSchema from "effect/JSONSchema" +import * as Schema from "effect/Schema" +import { describe, it } from "vitest" + +// eslint-disable-next-line @typescript-eslint/no-require-imports +const Ajv2020 = require("ajv/dist/2020") + +const ajvOptions: Ajv.Options = { + strictTuples: false, + allowMatchingProperties: true +} + +function getAjvValidate(jsonSchema: object): Ajv.ValidateFunction { + return new Ajv.default(ajvOptions).compile(jsonSchema) +} + +const baseAjvOptions: AjvOptions = { + allErrors: true, + strict: false, // warns/throws on unknown keywords depending on Ajv version + validateSchema: true, + code: { esm: true } // optional +} + +const ajvDraft7 = new Ajv.default(baseAjvOptions) +const ajv2020 = new Ajv2020.default(baseAjvOptions) + +const expectError = (schema: Schema.Schema, message: string) => { + throws(() => JSONSchema.make(schema), new Error(message)) +} + +async function assertDraft7(schema: Schema.Schema, expected: object) { + const jsonSchema = JSONSchema.make(schema) + deepStrictEqual(jsonSchema, { + "$schema": "http://json-schema.org/draft-07/schema#", + ...expected + } as any) + const valid = ajvDraft7.validateSchema(jsonSchema) + if (valid instanceof Promise) { + await valid + } + strictEqual(ajvDraft7.errors, null) + return jsonSchema +} + +async function assertDraft201909( + schema: S, + expected: object +) { + const definitions = {} + const jsonSchema = JSONSchema.fromAST(schema.ast, { + definitions, + target: "jsonSchema2019-09" + }) + deepStrictEqual(jsonSchema, expected) + const valid = ajv2020.validateSchema(jsonSchema) + if (valid instanceof Promise) { + await valid + } + strictEqual(ajv2020.errors, null) + return jsonSchema +} + +async function assertOpenApi3_1( + schema: S, + expected: object +) { + const definitions = {} + const jsonSchema = JSONSchema.fromAST(schema.ast, { + definitions, + target: "openApi3.1" + }) + deepStrictEqual(jsonSchema, expected) + const valid = ajv2020.validateSchema(jsonSchema) + if (valid instanceof Promise) { + await valid + } + strictEqual(ajvDraft7.errors, null) + return jsonSchema +} + +async function assertDraft2020_12( + schema: S, + expected: object +) { + const definitions = {} + const jsonSchema = JSONSchema.fromAST(schema.ast, { + definitions, + target: "jsonSchema2020-12" + }) + deepStrictEqual(jsonSchema, expected) + const valid = ajv2020.validateSchema(jsonSchema) + if (valid instanceof Promise) { + await valid + } + strictEqual(ajv2020.errors, null) + return jsonSchema +} + +function assertAjvDraft7Success( + schema: S, + input: S["Type"] +) { + const jsonSchema = JSONSchema.make(schema) + const validate = getAjvValidate(jsonSchema) + assertTrue(validate(input)) +} + +function assertAjvDraft7Failure( + schema: S, + input: unknown +) { + const jsonSchema = JSONSchema.make(schema) + const validate = getAjvValidate(jsonSchema) + assertFalse(validate(input)) +} + +describe("JSONSchema", () => { + describe("fromAST", () => { + it("definitionsPath", () => { + const schema = Schema.String.annotations({ identifier: "08368672-2c02-4d6d-92b0-dd0019b33a7b" }) + const definitions = {} + const jsonSchema = JSONSchema.fromAST(schema.ast, { + definitions, + definitionPath: "#/components/schemas/" + }) + deepStrictEqual(jsonSchema, { + "$ref": "#/components/schemas/08368672-2c02-4d6d-92b0-dd0019b33a7b" + }) + deepStrictEqual(definitions, { + "08368672-2c02-4d6d-92b0-dd0019b33a7b": { + "type": "string" + } + }) + }) + + describe("topLevelReferenceStrategy", () => { + describe(`"skip"`, () => { + it("top level identifier", () => { + const schema = Schema.String.annotations({ identifier: "1b205579-f159-48d4-a218-f09426bca040" }) + const definitions = {} + const jsonSchema = JSONSchema.fromAST(schema.ast, { + definitions, + topLevelReferenceStrategy: "skip" + }) + deepStrictEqual(jsonSchema, { + "type": "string" + }) + deepStrictEqual(definitions, {}) + }) + + it("nested identifiers", () => { + class A extends Schema.Class("A")({ a: Schema.String.annotations({ identifier: "ID4" }) }) {} + const schema = Schema.Struct({ + a: Schema.String.annotations({ identifier: "ID" }), + b: Schema.Date, + c: Schema.Struct({ + d: Schema.String.annotations({ identifier: "ID3" }) + }).annotations({ identifier: "ID2" }), + e: A + }) + const definitions = {} + const jsonSchema = JSONSchema.fromAST(schema.ast, { + definitions, + topLevelReferenceStrategy: "skip" + }) + deepStrictEqual(jsonSchema, { + "type": "object", + "properties": { + "a": { + "type": "string" + }, + "b": { + "type": "string", + "description": "a string to be decoded into a Date" + }, + "c": { + "type": "object", + "properties": { + "d": { "type": "string" } + }, + "required": ["d"], + "additionalProperties": false + }, + "e": { + "type": "object", + "properties": { + "a": { "type": "string" } + }, + "required": ["a"], + "additionalProperties": false + } + }, + "required": ["a", "b", "c", "e"], + "additionalProperties": false + }) + deepStrictEqual(definitions, {}) + }) + + it("suspended schema", () => { + interface A { + readonly a: string + readonly as: ReadonlyArray + } + const schema: Schema.Schema = Schema.suspend(() => + Schema.Struct({ + a: Schema.String.annotations({ identifier: "ID2" }), + as: Schema.Array(schema) + }) + ).annotations({ identifier: "ID" }) + const definitions = {} + const jsonSchema = JSONSchema.fromAST(schema.ast, { + definitions, + topLevelReferenceStrategy: "skip" + }) + deepStrictEqual(jsonSchema, { + "$ref": "#/$defs/ID" + }) + deepStrictEqual(definitions, { + "ID": { + "type": "object", + "properties": { + "a": { "type": "string" }, + "as": { "type": "array", "items": { "$ref": "#/$defs/ID" } } + }, + "required": ["a", "as"], + "additionalProperties": false + } + }) + }) + }) + }) + + describe("additionalPropertiesStrategy", () => { + it(`"allow"`, () => { + const schema = Schema.Struct({ + a: Schema.String, + b: Schema.Struct({ + c: Schema.String + }) + }) + const definitions = {} + const jsonSchema = JSONSchema.fromAST(schema.ast, { + definitions, + additionalPropertiesStrategy: "allow" + }) + deepStrictEqual(jsonSchema, { + "type": "object", + "properties": { + "a": { + "type": "string" + }, + "b": { + "type": "object", + "properties": { + "c": { "type": "string" } + }, + "required": ["c"], + "additionalProperties": true + } + }, + "required": ["a", "b"], + "additionalProperties": true + }) + deepStrictEqual(definitions, {}) + }) + }) + }) + + describe("Unsupported schemas", () => { + it("Tuple with an unsupported component", () => { + expectError( + Schema.Tuple(Schema.Undefined), + `Missing annotation +at path: [0] +details: Generating a JSON Schema for this schema requires a "jsonSchema" annotation +schema (UndefinedKeyword): undefined` + ) + }) + + it("Struct with an unsupported field", () => { + expectError( + Schema.Struct({ a: Schema.SymbolFromSelf }), + `Missing annotation +at path: ["a"] +details: Generating a JSON Schema for this schema requires a "jsonSchema" annotation +schema (SymbolKeyword): symbol` + ) + }) + + it("Declaration", async () => { + expectError( + Schema.ChunkFromSelf(Schema.String), + `Missing annotation +details: Generating a JSON Schema for this schema requires a "jsonSchema" annotation +schema (Declaration): Chunk` + ) + }) + + it("Undefined", async () => { + expectError( + Schema.Undefined, + `Missing annotation +details: Generating a JSON Schema for this schema requires a "jsonSchema" annotation +schema (UndefinedKeyword): undefined` + ) + }) + + it("BigIntFromSelf", async () => { + expectError( + Schema.BigIntFromSelf, + `Missing annotation +details: Generating a JSON Schema for this schema requires a "jsonSchema" annotation +schema (BigIntKeyword): bigint` + ) + }) + + it("UniqueSymbolFromSelf", async () => { + expectError( + Schema.UniqueSymbolFromSelf(Symbol.for("effect/Schema/test/a")), + `Missing annotation +details: Generating a JSON Schema for this schema requires a "jsonSchema" annotation +schema (UniqueSymbol): Symbol(effect/Schema/test/a)` + ) + }) + + it("SymbolFromSelf", async () => { + expectError( + Schema.SymbolFromSelf, + `Missing annotation +details: Generating a JSON Schema for this schema requires a "jsonSchema" annotation +schema (SymbolKeyword): symbol` + ) + }) + + it("Literal(bigint)", () => { + expectError( + Schema.Literal(1n), + `Missing annotation +details: Generating a JSON Schema for this schema requires a "jsonSchema" annotation +schema (Literal): 1n` + ) + }) + + it("Suspend", () => { + interface A { + readonly a: string + readonly as: ReadonlyArray + } + const schema = Schema.Struct({ + a: Schema.String, + as: Schema.Array(Schema.suspend((): Schema.Schema => schema)) + }) + expectError( + schema, + `Missing annotation +at path: ["as"] +details: Generating a JSON Schema for this schema requires an "identifier" annotation +schema (Suspend): ` + ) + }) + + it("Unsupported property signature key", () => { + const a = Symbol.for("effect/Schema/test/a") + expectError( + Schema.Struct({ [a]: Schema.String }), + `Unsupported key +details: Cannot encode Symbol(effect/Schema/test/a) key to JSON Schema` + ) + }) + + it("Unsupported index signature parameter", () => { + expectError( + Schema.Record({ key: Schema.SymbolFromSelf, value: Schema.Number }), + `Missing annotation +at path: ["[symbol]"] +details: Generating a JSON Schema for this schema requires a "jsonSchema" annotation +schema (SymbolKeyword): symbol` + ) + }) + + it("Unsupported post-rest elements", () => { + expectError( + Schema.Tuple([], Schema.Number, Schema.String), + "Generating a JSON Schema for post-rest elements is not currently supported. You're welcome to contribute by submitting a Pull Request" + ) + }) + }) + + describe("jsonSchema7", () => { + describe("nullable handling", () => { + it("Null", async () => { + const schema = Schema.Null + await assertDraft7(schema, { "type": "null" }) + }) + + it("NullOr(String)", async () => { + const schema = Schema.NullOr(Schema.String) + await assertDraft7(schema, { + "anyOf": [ + { "type": "string" }, + { "type": "null" } + ] + }) + }) + + it("NullOr(Any)", async () => { + const schema = Schema.NullOr(Schema.Any) + await assertDraft7(schema, { + "$id": "/schemas/any", + "title": "any" + }) + }) + + it("NullOr(Unknown)", async () => { + const schema = Schema.NullOr(Schema.Unknown) + await assertDraft7(schema, { + "$id": "/schemas/unknown", + "title": "unknown" + }) + }) + + it("NullOr(Void)", async () => { + const schema = Schema.NullOr(Schema.Void) + await assertDraft7(schema, { + "$id": "/schemas/void", + "title": "void" + }) + }) + + it("Literal | null", async () => { + const schema = Schema.Literal("a", null) + await assertDraft7(schema, { + "anyOf": [ + { + "type": "string", + "enum": ["a"] + }, + { "type": "null" } + ] + }) + }) + + it("Literal | null(with description)", async () => { + const schema = Schema.Union(Schema.Literal("a"), Schema.Null.annotations({ description: "mydescription" })) + await assertDraft7(schema, { + "anyOf": [ + { + "type": "string", + "enum": ["a"] + }, + { + "type": "null", + "description": "mydescription" + } + ] + }) + }) + + it("Nested nullable unions", async () => { + const schema = Schema.Union(Schema.NullOr(Schema.String), Schema.Literal("a", null)) + await assertDraft7(schema, { + "anyOf": [ + { + "anyOf": [ + { "type": "string" }, + { "type": "null" } + ] + }, + { + "anyOf": [ + { "type": "string", "enum": ["a"] }, + { "type": "null" } + ] + } + ] + }) + }) + }) + + it("parseJson handling", async () => { + const schema = Schema.parseJson(Schema.Struct({ + a: Schema.parseJson(Schema.NumberFromString) + })) + await assertDraft7( + schema, + { + "type": "object", + "required": ["a"], + "properties": { + "a": { + "type": "string", + "contentMediaType": "application/json" + } + }, + "additionalProperties": false + } + ) + }) + + describe("primitives", () => { + it("Never", async () => { + await assertDraft7(Schema.Never, { + "$id": "/schemas/never", + "not": {}, + "title": "never" + }) + await assertDraft7(Schema.Never.annotations({ description: "description" }), { + "$id": "/schemas/never", + "not": {}, + "title": "never", + "description": "description" + }) + }) + + it("Void", async () => { + await assertDraft7(Schema.Void, { + "$id": "/schemas/void", + "title": "void" + }) + await assertDraft7(Schema.Void.annotations({ description: "description" }), { + "$id": "/schemas/void", + "title": "void", + "description": "description" + }) + }) + + it("Unknown", async () => { + await assertDraft7(Schema.Unknown, { + "$id": "/schemas/unknown", + "title": "unknown" + }) + await assertDraft7(Schema.Unknown.annotations({ description: "description" }), { + "$id": "/schemas/unknown", + "title": "unknown", + "description": "description" + }) + }) + + it("Any", async () => { + await assertDraft7(Schema.Any, { + "$id": "/schemas/any", + "title": "any" + }) + await assertDraft7(Schema.Any.annotations({ description: "description" }), { + "$id": "/schemas/any", + "title": "any", + "description": "description" + }) + }) + + it("Object", async () => { + await assertDraft7(Schema.Object, { + "$id": "/schemas/object", + "anyOf": [ + { "type": "object" }, + { "type": "array" } + ], + "title": "object", + "description": "an object in the TypeScript meaning, i.e. the `object` type" + }) + await assertDraft7(Schema.Object.annotations({ description: "description" }), { + "$id": "/schemas/object", + "anyOf": [ + { "type": "object" }, + { "type": "array" } + ], + "title": "object", + "description": "description" + }) + }) + + it("String", async () => { + const schema = Schema.String + await assertDraft7(schema, { + "type": "string" + }) + await assertDraft7(schema.annotations({ description: "description" }), { + "type": "string", + "description": "description" + }) + assertAjvDraft7Success(schema, "a") + assertAjvDraft7Failure(schema, null) + }) + + it("Number", async () => { + await assertDraft7(Schema.Number, { + "type": "number" + }) + await assertDraft7(Schema.Number.annotations({ description: "description" }), { + "type": "number", + "description": "description" + }) + }) + + it("Boolean", async () => { + await assertDraft7(Schema.Boolean, { + "type": "boolean" + }) + await assertDraft7(Schema.Boolean.annotations({ description: "description" }), { + "type": "boolean", + "description": "description" + }) + }) + }) + + describe("Literal", () => { + const schema = Schema.Literal(null) + it("null literal", async () => { + await assertDraft7(schema, { + "type": "null" + }) + await assertDraft7(schema.annotations({ description: "description" }), { + "type": "null", + "description": "description" + }) + assertAjvDraft7Success(schema, null) + assertAjvDraft7Failure(schema, "a") + }) + + it("string literal", async () => { + await assertDraft7(Schema.Literal("a"), { + "type": "string", + "enum": ["a"] + }) + await assertDraft7(Schema.Literal("a").annotations({ description: "description" }), { + "type": "string", + "enum": ["a"], + "description": "description" + }) + }) + + it("number literal", async () => { + await assertDraft7(Schema.Literal(1), { + "type": "number", + "enum": [1] + }) + await assertDraft7(Schema.Literal(1).annotations({ description: "description" }), { + "type": "number", + "enum": [1], + "description": "description" + }) + }) + + it("boolean literal", async () => { + await assertDraft7(Schema.Literal(true), { + "type": "boolean", + "enum": [true] + }) + await assertDraft7(Schema.Literal(true).annotations({ description: "description" }), { + "type": "boolean", + "enum": [true], + "description": "description" + }) + }) + }) + + describe("Literals", () => { + it("string literals", async () => { + await assertDraft7(Schema.Literal("a", "b"), { + "type": "string", + "enum": ["a", "b"] + }) + }) + + it("number literals", async () => { + await assertDraft7(Schema.Literal(1, 2), { + "type": "number", + "enum": [1, 2] + }) + }) + + it("boolean literals", async () => { + await assertDraft7(Schema.Literal(true, false), { + "type": "boolean", + "enum": [true, false] + }) + }) + + it("mixed literals", async () => { + await assertDraft7(Schema.Literal(1, "a", true), { + "anyOf": [ + { "type": "number", "enum": [1] }, + { "type": "string", "enum": ["a"] }, + { "type": "boolean", "enum": [true] } + ] + }) + await assertDraft7(Schema.Literal("a", "b", 1), { + "anyOf": [ + { "type": "string", "enum": ["a", "b"] }, + { "type": "number", "enum": [1] } + ] + }) + await assertDraft7(Schema.Literal("a", 1, "b"), { + "anyOf": [ + { "type": "string", "enum": ["a"] }, + { "type": "number", "enum": [1] }, + { "type": "string", "enum": ["b"] } + ] + }) + }) + }) + + describe("Enums", () => { + it("empty enum", async () => { + enum Empty {} + await assertDraft7(Schema.Enums(Empty), { + "$id": "/schemas/never", + "not": {} + }) + await assertDraft7(Schema.Enums(Empty).annotations({ description: "description" }), { + "$id": "/schemas/never", + "not": {}, + "description": "description" + }) + }) + + it("single enum", async () => { + enum Fruits { + Apple + } + await assertDraft7(Schema.Enums(Fruits), { + "$comment": "/schemas/enums", + "anyOf": [ + { "type": "number", "title": "Apple", "enum": [0] } + ] + }) + await assertDraft7(Schema.Enums(Fruits).annotations({ description: "description" }), { + "$comment": "/schemas/enums", + "anyOf": [ + { "type": "number", "title": "Apple", "enum": [0] } + ], + "description": "description" + }) + }) + + it("numeric enums", async () => { + enum Fruits { + Apple, + Banana + } + await assertDraft7(Schema.Enums(Fruits), { + "$comment": "/schemas/enums", + "anyOf": [ + { "type": "number", "title": "Apple", "enum": [0] }, + { "type": "number", "title": "Banana", "enum": [1] } + ] + }) + }) + + it("string enums", async () => { + enum Fruits { + Apple = "apple", + Banana = "banana" + } + await assertDraft7(Schema.Enums(Fruits), { + "$comment": "/schemas/enums", + "anyOf": [ + { "type": "string", "title": "Apple", "enum": ["apple"] }, + { "type": "string", "title": "Banana", "enum": ["banana"] } + ] + }) + }) + + it("mix of string/number enums", async () => { + enum Fruits { + Apple = "apple", + Banana = "banana", + Cantaloupe = 0 + } + await assertDraft7(Schema.Enums(Fruits), { + "$comment": "/schemas/enums", + "anyOf": [ + { "type": "string", "title": "Apple", "enum": ["apple"] }, + { "type": "string", "title": "Banana", "enum": ["banana"] }, + { "type": "number", "title": "Cantaloupe", "enum": [0] } + ] + }) + }) + + it("const enums", async () => { + const Fruits = { + Apple: "apple", + Banana: "banana", + Cantaloupe: 3 + } as const + await assertDraft7(Schema.Enums(Fruits), { + "$comment": "/schemas/enums", + "anyOf": [ + { "type": "string", "title": "Apple", "enum": ["apple"] }, + { "type": "string", "title": "Banana", "enum": ["banana"] }, + { "type": "number", "title": "Cantaloupe", "enum": [3] } + ] + }) + }) + }) + + it("TemplateLiteral", async () => { + const schema = Schema.TemplateLiteral(Schema.Literal("a"), Schema.Number) + await assertDraft7(schema, { + "type": "string", + "pattern": "^a[+-]?\\d*\\.?\\d+(?:[Ee][+-]?\\d+)?$", + "title": "`a${number}`", + "description": "a template literal" + }) + }) + + describe("Refinement", () => { + it("itemsCount (Array)", async () => { + await assertDraft7(Schema.Array(Schema.String).pipe(Schema.itemsCount(2)), { + "type": "array", + "items": { + "type": "string" + }, + "description": "an array of exactly 2 item(s)", + "title": "itemsCount(2)", + "minItems": 2, + "maxItems": 2 + }) + }) + + it("itemsCount (NonEmptyArray)", async () => { + await assertDraft7(Schema.NonEmptyArray(Schema.String).pipe(Schema.itemsCount(2)), { + "type": "array", + "items": { + "type": "string" + }, + "description": "an array of exactly 2 item(s)", + "title": "itemsCount(2)", + "minItems": 2, + "maxItems": 2 + }) + }) + + it("minItems (Array)", async () => { + await assertDraft7(Schema.Array(Schema.String).pipe(Schema.minItems(2)), { + "type": "array", + "items": { + "type": "string" + }, + "description": "an array of at least 2 item(s)", + "title": "minItems(2)", + "minItems": 2 + }) + }) + + it("minItems (NonEmptyArray)", async () => { + await assertDraft7(Schema.NonEmptyArray(Schema.String).pipe(Schema.minItems(2)), { + "type": "array", + "items": { + "type": "string" + }, + "description": "an array of at least 2 item(s)", + "title": "minItems(2)", + "minItems": 2 + }) + }) + + it("maxItems (Array)", async () => { + await assertDraft7(Schema.Array(Schema.String).pipe(Schema.maxItems(2)), { + "type": "array", + "items": { + "type": "string" + }, + "description": "an array of at most 2 item(s)", + "title": "maxItems(2)", + "maxItems": 2 + }) + }) + + it("maxItems (NonEmptyArray)", async () => { + await assertDraft7(Schema.NonEmptyArray(Schema.String).pipe(Schema.maxItems(2)), { + "type": "array", + "items": { + "type": "string" + }, + "description": "an array of at most 2 item(s)", + "title": "maxItems(2)", + "minItems": 1, + "maxItems": 2 + }) + }) + + it("minLength", async () => { + await assertDraft7(Schema.String.pipe(Schema.minLength(1)), { + "type": "string", + "title": "minLength(1)", + "description": "a string at least 1 character(s) long", + "minLength": 1 + }) + }) + + it("maxLength", async () => { + await assertDraft7(Schema.String.pipe(Schema.maxLength(1)), { + "type": "string", + "title": "maxLength(1)", + "description": "a string at most 1 character(s) long", + "maxLength": 1 + }) + }) + + it("length: number", async () => { + await assertDraft7(Schema.String.pipe(Schema.length(1)), { + "type": "string", + "title": "length(1)", + "description": "a single character", + "maxLength": 1, + "minLength": 1 + }) + }) + + it("length: { min, max }", async () => { + await assertDraft7(Schema.String.pipe(Schema.length({ min: 2, max: 4 })), { + "type": "string", + "title": "length({ min: 2, max: 4)", + "description": "a string at least 2 character(s) and at most 4 character(s) long", + "maxLength": 4, + "minLength": 2 + }) + }) + + it("greaterThan", async () => { + await assertDraft7(Schema.Number.pipe(Schema.greaterThan(1)), { + "type": "number", + "title": "greaterThan(1)", + "description": "a number greater than 1", + "exclusiveMinimum": 1 + }) + }) + + it("greaterThanOrEqualTo", async () => { + await assertDraft7(Schema.Number.pipe(Schema.greaterThanOrEqualTo(1)), { + "type": "number", + "title": "greaterThanOrEqualTo(1)", + "description": "a number greater than or equal to 1", + "minimum": 1 + }) + }) + + it("lessThan", async () => { + await assertDraft7(Schema.Number.pipe(Schema.lessThan(1)), { + "type": "number", + "title": "lessThan(1)", + "description": "a number less than 1", + "exclusiveMaximum": 1 + }) + }) + + it("lessThanOrEqualTo", async () => { + await assertDraft7(Schema.Number.pipe(Schema.lessThanOrEqualTo(1)), { + "type": "number", + "title": "lessThanOrEqualTo(1)", + "description": "a number less than or equal to 1", + "maximum": 1 + }) + }) + + it("pattern", async () => { + await assertDraft7(Schema.String.pipe(Schema.pattern(/^abb+$/)), { + "type": "string", + "description": "a string matching the pattern ^abb+$", + "pattern": "^abb+$" + }) + }) + + it("int", async () => { + await assertDraft7(Schema.Number.pipe(Schema.int()), { + "type": "integer", + "title": "int", + "description": "an integer" + }) + }) + + it("Trimmed", async () => { + const schema = Schema.Trimmed + await assertDraft7(schema, { + "$defs": { + "Trimmed": { + "title": "trimmed", + "description": "a string with no leading or trailing whitespace", + "pattern": "^\\S[\\s\\S]*\\S$|^\\S$|^$", + "type": "string" + } + }, + "$ref": "#/$defs/Trimmed" + }) + }) + + it("Lowercased", async () => { + const schema = Schema.Lowercased + await assertDraft7(schema, { + "$defs": { + "Lowercased": { + "title": "lowercased", + "description": "a lowercase string", + "pattern": "^[^A-Z]*$", + "type": "string" + } + }, + "$ref": "#/$defs/Lowercased" + }) + }) + + it("Uppercased", async () => { + const schema = Schema.Uppercased + await assertDraft7(schema, { + "$defs": { + "Uppercased": { + "title": "uppercased", + "description": "an uppercase string", + "pattern": "^[^a-z]*$", + "type": "string" + } + }, + "$ref": "#/$defs/Uppercased" + }) + }) + + it("Capitalized", async () => { + const schema = Schema.Capitalized + await assertDraft7(schema, { + "$defs": { + "Capitalized": { + "title": "capitalized", + "description": "a capitalized string", + "pattern": "^[^a-z]?.*$", + "type": "string" + } + }, + "$ref": "#/$defs/Capitalized" + }) + }) + + it("Uncapitalized", async () => { + const schema = Schema.Uncapitalized + await assertDraft7(schema, { + "$defs": { + "Uncapitalized": { + "title": "uncapitalized", + "description": "a uncapitalized string", + "pattern": "^[^A-Z]?.*$", + "type": "string" + } + }, + "$ref": "#/$defs/Uncapitalized" + }) + }) + + describe("should handle merge conflicts", () => { + it("minLength + minLength", async () => { + await assertDraft7(Schema.String.pipe(Schema.minLength(1), Schema.minLength(2)), { + "type": "string", + "title": "minLength(2)", + "description": "a string at least 2 character(s) long", + "minLength": 2 + }) + await assertDraft7(Schema.String.pipe(Schema.minLength(2), Schema.minLength(1)), { + "type": "string", + "title": "minLength(1)", + "description": "a string at least 1 character(s) long", + "minLength": 1, + "allOf": [ + { "minLength": 2 } + ] + }) + await assertDraft7(Schema.String.pipe(Schema.minLength(2), Schema.minLength(1), Schema.minLength(2)), { + "type": "string", + "title": "minLength(2)", + "description": "a string at least 2 character(s) long", + "minLength": 2 + }) + }) + + it("maxLength + maxLength", async () => { + await assertDraft7(Schema.String.pipe(Schema.maxLength(1), Schema.maxLength(2)), { + "type": "string", + "title": "maxLength(2)", + "description": "a string at most 2 character(s) long", + "maxLength": 2, + "allOf": [ + { "maxLength": 1 } + ] + }) + await assertDraft7(Schema.String.pipe(Schema.maxLength(2), Schema.maxLength(1)), { + "type": "string", + "title": "maxLength(1)", + "description": "a string at most 1 character(s) long", + "maxLength": 1 + }) + await assertDraft7(Schema.String.pipe(Schema.maxLength(1), Schema.maxLength(2), Schema.maxLength(1)), { + "type": "string", + "title": "maxLength(1)", + "description": "a string at most 1 character(s) long", + "maxLength": 1 + }) + }) + + it("pattern + pattern", async () => { + await assertDraft7(Schema.String.pipe(Schema.startsWith("a"), Schema.endsWith("c")), { + "type": "string", + "title": "endsWith(\"c\")", + "description": "a string ending with \"c\"", + "pattern": "^.*c$", + "allOf": [ + { "pattern": "^a" } + ] + }) + await assertDraft7( + Schema.String.pipe(Schema.startsWith("a"), Schema.endsWith("c"), Schema.startsWith("a")), + { + "type": "string", + "title": "startsWith(\"a\")", + "description": "a string starting with \"a\"", + "pattern": "^a", + "allOf": [ + { "pattern": "^.*c$" } + ] + } + ) + await assertDraft7( + Schema.String.pipe(Schema.endsWith("c"), Schema.startsWith("a"), Schema.endsWith("c")), + { + "type": "string", + "title": "endsWith(\"c\")", + "description": "a string ending with \"c\"", + "pattern": "^.*c$", + "allOf": [ + { "pattern": "^a" } + ] + } + ) + }) + + it("minItems + minItems", async () => { + await assertDraft7(Schema.Array(Schema.String).pipe(Schema.minItems(1), Schema.minItems(2)), { + "type": "array", + "items": { + "type": "string" + }, + "description": "an array of at least 2 item(s)", + "title": "minItems(2)", + "minItems": 2 + }) + await assertDraft7(Schema.Array(Schema.String).pipe(Schema.minItems(2), Schema.minItems(1)), { + "type": "array", + "items": { + "type": "string" + }, + "title": "minItems(1)", + "description": "an array of at least 1 item(s)", + "minItems": 1, + "allOf": [ + { "minItems": 2 } + ] + }) + await assertDraft7( + Schema.Array(Schema.String).pipe(Schema.minItems(2), Schema.minItems(1), Schema.minItems(2)), + { + "type": "array", + "items": { + "type": "string" + }, + "description": "an array of at least 2 item(s)", + "title": "minItems(2)", + "minItems": 2 + } + ) + }) + + it("maxItems + maxItems", async () => { + await assertDraft7(Schema.Array(Schema.String).pipe(Schema.maxItems(1), Schema.maxItems(2)), { + "type": "array", + "items": { + "type": "string" + }, + "title": "maxItems(2)", + "description": "an array of at most 2 item(s)", + "maxItems": 2, + "allOf": [ + { "maxItems": 1 } + ] + }) + await assertDraft7(Schema.Array(Schema.String).pipe(Schema.maxItems(2), Schema.maxItems(1)), { + "type": "array", + "items": { + "type": "string" + }, + "title": "maxItems(1)", + "description": "an array of at most 1 item(s)", + "maxItems": 1 + }) + await assertDraft7( + Schema.Array(Schema.String).pipe(Schema.maxItems(1), Schema.maxItems(2), Schema.maxItems(1)), + { + "type": "array", + "items": { + "type": "string" + }, + "title": "maxItems(1)", + "description": "an array of at most 1 item(s)", + "maxItems": 1 + } + ) + }) + + it("minimum + minimum", async () => { + await assertDraft7(Schema.Number.pipe(Schema.greaterThanOrEqualTo(1), Schema.greaterThanOrEqualTo(2)), { + "type": "number", + "title": "greaterThanOrEqualTo(2)", + "description": "a number greater than or equal to 2", + "minimum": 2 + }) + await assertDraft7(Schema.Number.pipe(Schema.greaterThanOrEqualTo(2), Schema.greaterThanOrEqualTo(1)), { + "type": "number", + "minimum": 1, + "title": "greaterThanOrEqualTo(1)", + "description": "a number greater than or equal to 1", + "allOf": [ + { "minimum": 2 } + ] + }) + await assertDraft7( + Schema.Number.pipe( + Schema.greaterThanOrEqualTo(2), + Schema.greaterThanOrEqualTo(1), + Schema.greaterThanOrEqualTo(2) + ), + { + "type": "number", + "title": "greaterThanOrEqualTo(2)", + "description": "a number greater than or equal to 2", + "minimum": 2 + } + ) + }) + + it("maximum + maximum", async () => { + await assertDraft7(Schema.Number.pipe(Schema.lessThanOrEqualTo(1), Schema.lessThanOrEqualTo(2)), { + "type": "number", + "title": "lessThanOrEqualTo(2)", + "description": "a number less than or equal to 2", + "maximum": 2, + "allOf": [ + { "maximum": 1 } + ] + }) + await assertDraft7(Schema.Number.pipe(Schema.lessThanOrEqualTo(2), Schema.lessThanOrEqualTo(1)), { + "type": "number", + "title": "lessThanOrEqualTo(1)", + "description": "a number less than or equal to 1", + "maximum": 1 + }) + await assertDraft7( + Schema.Number.pipe(Schema.lessThanOrEqualTo(1), Schema.lessThanOrEqualTo(2), Schema.lessThanOrEqualTo(1)), + { + "type": "number", + "title": "lessThanOrEqualTo(1)", + "description": "a number less than or equal to 1", + "maximum": 1 + } + ) + }) + + it("exclusiveMinimum + exclusiveMinimum", async () => { + await assertDraft7(Schema.Number.pipe(Schema.greaterThan(1), Schema.greaterThan(2)), { + "type": "number", + "title": "greaterThan(2)", + "description": "a number greater than 2", + "exclusiveMinimum": 2 + }) + await assertDraft7(Schema.Number.pipe(Schema.greaterThan(2), Schema.greaterThan(1)), { + "type": "number", + "exclusiveMinimum": 1, + "title": "greaterThan(1)", + "description": "a number greater than 1", + "allOf": [ + { "exclusiveMinimum": 2 } + ] + }) + await assertDraft7( + Schema.Number.pipe( + Schema.greaterThan(2), + Schema.greaterThan(1), + Schema.greaterThan(2) + ), + { + "type": "number", + "title": "greaterThan(2)", + "description": "a number greater than 2", + "exclusiveMinimum": 2 + } + ) + }) + + it("exclusiveMaximum + exclusiveMaximum", async () => { + await assertDraft7(Schema.Number.pipe(Schema.lessThan(1), Schema.lessThan(2)), { + "type": "number", + "title": "lessThan(2)", + "description": "a number less than 2", + "exclusiveMaximum": 2, + "allOf": [ + { "exclusiveMaximum": 1 } + ] + }) + await assertDraft7(Schema.Number.pipe(Schema.lessThan(2), Schema.lessThan(1)), { + "type": "number", + "title": "lessThan(1)", + "description": "a number less than 1", + "exclusiveMaximum": 1 + }) + await assertDraft7( + Schema.Number.pipe(Schema.lessThan(1), Schema.lessThan(2), Schema.lessThan(1)), + { + "type": "number", + "title": "lessThan(1)", + "description": "a number less than 1", + "exclusiveMaximum": 1 + } + ) + }) + + it("multipleOf + multipleOf", async () => { + await assertDraft7(Schema.Number.pipe(Schema.multipleOf(2), Schema.multipleOf(3)), { + "type": "number", + "title": "multipleOf(3)", + "description": "a number divisible by 3", + "multipleOf": 3, + "allOf": [ + { "multipleOf": 2 } + ] + }) + await assertDraft7( + Schema.Number.pipe(Schema.multipleOf(2), Schema.multipleOf(3), Schema.multipleOf(3)), + { + "type": "number", + "title": "multipleOf(3)", + "description": "a number divisible by 3", + "multipleOf": 3, + "allOf": [ + { "multipleOf": 2 } + ] + } + ) + await assertDraft7( + Schema.Number.pipe(Schema.multipleOf(3), Schema.multipleOf(2), Schema.multipleOf(3)), + { + "type": "number", + "title": "multipleOf(3)", + "description": "a number divisible by 3", + "multipleOf": 3, + "allOf": [ + { "multipleOf": 2 } + ] + } + ) + }) + }) + }) + + describe("Tuple", () => { + it("empty tuple", async () => { + const schema = Schema.Tuple() + await assertDraft7(schema, { + "type": "array", + "maxItems": 0 + }) + }) + + it("element", async () => { + const schema = Schema.Tuple(Schema.Number) + await assertDraft7(schema, { + "type": "array", + "items": [{ + "type": "number" + }], + "minItems": 1, + "additionalItems": false + }) + }) + + it("element + inner annotations", async () => { + await assertDraft7( + Schema.Tuple(Schema.Number.annotations({ description: "inner" })), + { + "type": "array", + "items": [{ + "type": "number", + "description": "inner" + }], + "minItems": 1, + "additionalItems": false + } + ) + }) + + it("element + outer annotations should override inner annotations", async () => { + await assertDraft7( + Schema.Tuple( + Schema.element(Schema.Number.annotations({ description: "inner" })).annotations({ description: "outer" }) + ), + { + "type": "array", + "items": [{ + "type": "number", + "description": "outer" + }], + "minItems": 1, + "additionalItems": false + } + ) + }) + + it("optionalElement", async () => { + const schema = Schema.Tuple(Schema.optionalElement(Schema.Number)) + await assertDraft7(schema, { + "type": "array", + "minItems": 0, + "items": [ + { + "type": "number" + } + ], + "additionalItems": false + }) + }) + + it("optionalElement + inner annotations", async () => { + await assertDraft7( + Schema.Tuple(Schema.optionalElement(Schema.Number).annotations({ description: "inner" })), + { + "type": "array", + "minItems": 0, + "items": [ + { + "type": "number", + "description": "inner" + } + ], + "additionalItems": false + } + ) + }) + + it("optionalElement + outer annotations should override inner annotations", async () => { + await assertDraft7( + Schema.Tuple( + Schema.optionalElement(Schema.Number).annotations({ description: "inner" }).annotations({ + description: "outer" + }) + ), + { + "type": "array", + "minItems": 0, + "items": [ + { + "type": "number", + "description": "outer" + } + ], + "additionalItems": false + } + ) + }) + + it("element + optionalElement", async () => { + const schema = Schema.Tuple( + Schema.element(Schema.String.annotations({ description: "inner" })).annotations({ description: "outer" }), + Schema.optionalElement(Schema.Number.annotations({ description: "inner?" })).annotations({ + description: "outer?" + }) + ) + await assertDraft7(schema, { + "type": "array", + "minItems": 1, + "items": [ + { + "type": "string", + "description": "outer" + }, + { + "type": "number", + "description": "outer?" + } + ], + "additionalItems": false + }) + }) + + it("rest", async () => { + const schema = Schema.Array(Schema.Number) + await assertDraft7(schema, { + "type": "array", + "items": { + "type": "number" + } + }) + }) + + it("rest + inner annotations", async () => { + await assertDraft7(Schema.Array(Schema.Number.annotations({ description: "inner" })), { + "type": "array", + "items": { + "type": "number", + "description": "inner" + } + }) + }) + + it("optionalElement + rest + inner annotations", async () => { + const schema = Schema.Tuple( + [Schema.optionalElement(Schema.String)], + Schema.element(Schema.Number.annotations({ description: "inner" })) + ) + await assertDraft7(schema, { + "type": "array", + "minItems": 0, + "items": [ + { + "type": "string" + } + ], + "additionalItems": { + "type": "number", + "description": "inner" + } + }) + }) + + it("optionalElement + rest + outer annotations should override inner annotations", async () => { + await assertDraft7( + Schema.Tuple( + [Schema.optionalElement(Schema.String)], + Schema.element(Schema.Number.annotations({ description: "inner" })).annotations({ description: "outer" }) + ), + { + "type": "array", + "minItems": 0, + "items": [ + { + "type": "string" + } + ], + "additionalItems": { + "type": "number", + "description": "outer" + } + } + ) + }) + + it("element + rest", async () => { + const schema = Schema.Tuple([Schema.String], Schema.Number) + await assertDraft7(schema, { + "type": "array", + "items": [{ + "type": "string" + }], + "minItems": 1, + "additionalItems": { + "type": "number" + } + }) + }) + + it("NonEmptyArray", async () => { + await assertDraft7( + Schema.NonEmptyArray(Schema.String), + { + type: "array", + minItems: 1, + items: { type: "string" } + } + ) + }) + }) + + describe("Struct", () => { + it("empty struct: Schema.Struct({})", async () => { + const schema = Schema.Struct({}) + await assertDraft7(schema, { + "$id": "/schemas/%7B%7D", + "anyOf": [{ + "type": "object" + }, { + "type": "array" + }] + }) + }) + + it("required property signatures", async () => { + const schema = Schema.Struct({ + a: Schema.String, + b: Schema.String.annotations({ description: "b-inner" }), + c: Schema.propertySignature(Schema.String).annotations({ description: "c-outer" }), + d: Schema.propertySignature(Schema.String.annotations({ description: "d-inner" })).annotations({ + description: "d-outer" + }) + }) + await assertDraft7(schema, { + "type": "object", + "properties": { + "a": { "type": "string" }, + "b": { "type": "string", "description": "b-inner" }, + "c": { "type": "string", "description": "c-outer" }, + "d": { "type": "string", "description": "d-outer" } + }, + "required": ["a", "b", "c", "d"], + "additionalProperties": false + }) + }) + + it("optional", async () => { + const schema = Schema.Struct({ + a: Schema.optional(Schema.String), + b: Schema.optional(Schema.String.annotations({ description: "b-inner" })), + c: Schema.optional(Schema.String).annotations({ description: "c-outer" }), + d: Schema.optional(Schema.String.annotations({ description: "d-inner" })).annotations({ + description: "d-outer" + }), + e: Schema.optional(Schema.UndefinedOr(Schema.String)) + }) + await assertDraft7(schema, { + "type": "object", + "properties": { + "a": { "type": "string" }, + "b": { "type": "string", "description": "b-inner" }, + "c": { "type": "string", "description": "c-outer" }, + "d": { "type": "string", "description": "d-outer" }, + "e": { "type": "string" } + }, + "required": [], + "additionalProperties": false + }) + }) + + describe("optionalWith", () => { + it("{ nullable: true }", async () => { + const schema = Schema.Struct({ + a: Schema.optionalWith(Schema.String, { nullable: true }), + b: Schema.optionalWith(Schema.String.annotations({ description: "b-inner" }), { nullable: true }), + c: Schema.optionalWith(Schema.String, { nullable: true }).annotations({ description: "c-outer" }), + d: Schema.optionalWith(Schema.String.annotations({ description: "d-inner" }), { nullable: true }) + .annotations({ + description: "d-outer" + }), + e: Schema.optionalWith(Schema.UndefinedOr(Schema.String), { nullable: true }) + }) + await assertDraft7(schema, { + "type": "object", + "properties": { + "a": { "anyOf": [{ "type": "string" }, { "type": "null" }] }, + "b": { + "anyOf": [{ "type": "string", "description": "b-inner" }, { "type": "null" }], + "description": "b-inner" + }, + "c": { "anyOf": [{ "type": "string" }, { "type": "null" }], "description": "c-outer" }, + "d": { + "anyOf": [{ "type": "string", "description": "d-inner" }, { "type": "null" }], + "description": "d-outer" + }, + "e": { "anyOf": [{ "type": "string" }, { "type": "null" }] } + }, + "required": [], + "additionalProperties": false + }) + }) + + it("{ exact: true }", async () => { + const schema = Schema.Struct({ + a: Schema.optionalWith(Schema.String, { exact: true }), + b: Schema.optionalWith(Schema.String.annotations({ description: "b-inner" }), { exact: true }), + c: Schema.optionalWith(Schema.String, { exact: true }).annotations({ description: "c-outer" }), + d: Schema.optionalWith(Schema.String.annotations({ description: "d-inner" }), { exact: true }).annotations({ + description: "d-outer" + }), + e: Schema.optionalWith(Schema.UndefinedOr(Schema.String), { exact: true }) + }) + await assertDraft7(schema, { + "type": "object", + "properties": { + "a": { "type": "string" }, + "b": { "type": "string", "description": "b-inner" }, + "c": { "type": "string", "description": "c-outer" }, + "d": { "type": "string", "description": "d-outer" }, + "e": { "type": "string" } + }, + "required": [], + "additionalProperties": false + }) + }) + + it("{ exact: true, nullable: true }", async () => { + const schema = Schema.Struct({ + a: Schema.optionalWith(Schema.String, { exact: true, nullable: true }), + b: Schema.optionalWith(Schema.String.annotations({ description: "b-inner" }), { + exact: true, + nullable: true + }), + c: Schema.optionalWith(Schema.String, { exact: true, nullable: true }).annotations({ + description: "c-outer" + }), + d: Schema.optionalWith(Schema.String.annotations({ description: "d-inner" }), { + exact: true, + nullable: true + }).annotations({ + description: "d-outer" + }), + e: Schema.optionalWith(Schema.UndefinedOr(Schema.String), { exact: true, nullable: true }) + }) + await assertDraft7(schema, { + "type": "object", + "properties": { + "a": { "anyOf": [{ "type": "string" }, { "type": "null" }] }, + "b": { + "anyOf": [{ "type": "string", "description": "b-inner" }, { "type": "null" }], + "description": "b-inner" + }, + "c": { "anyOf": [{ "type": "string" }, { "type": "null" }], "description": "c-outer" }, + "d": { + "anyOf": [{ "type": "string", "description": "d-inner" }, { "type": "null" }], + "description": "d-outer" + }, + "e": { "anyOf": [{ "type": "string" }, { "type": "null" }] } + }, + "required": [], + "additionalProperties": false + }) + }) + + it("{ default }", async () => { + const schema = Schema.Struct({ + a: Schema.optionalWith(Schema.String, { default: () => "" }), + b: Schema.optionalWith(Schema.String.annotations({ description: "b-inner" }), { default: () => "" }), + c: Schema.optionalWith(Schema.String, { default: () => "" }).annotations({ description: "c-outer" }), + d: Schema.optionalWith(Schema.String.annotations({ description: "d-inner" }), { default: () => "" }) + .annotations({ description: "d-outer" }), + e: Schema.optionalWith(Schema.UndefinedOr(Schema.String), { default: () => "" }) + }) + await assertDraft7(schema, { + "type": "object", + "properties": { + "a": { "type": "string" }, + "b": { "type": "string", "description": "b-inner" }, + "c": { "type": "string", "description": "c-outer" }, + "d": { "type": "string", "description": "d-outer" }, + "e": { "type": "string" } + }, + "required": [], + "additionalProperties": false + }) + }) + + it("{ default, nullable: true }", async () => { + const schema = Schema.Struct({ + a: Schema.optionalWith(Schema.String, { default: () => "", nullable: true }), + b: Schema.optionalWith(Schema.String.annotations({ description: "b-inner" }), { + default: () => "", + nullable: true + }), + c: Schema.optionalWith(Schema.String, { default: () => "", nullable: true }).annotations({ + description: "c-outer" + }), + d: Schema.optionalWith(Schema.String.annotations({ description: "d-inner" }), { + default: () => "", + nullable: true + }) + .annotations({ description: "d-outer" }), + e: Schema.optionalWith(Schema.UndefinedOr(Schema.String), { default: () => "", nullable: true }) + }) + await assertDraft7(schema, { + "type": "object", + "properties": { + "a": { "anyOf": [{ "type": "string" }, { "type": "null" }] }, + "b": { + "anyOf": [{ "type": "string", "description": "b-inner" }, { "type": "null" }], + "description": "b-inner" + }, + "c": { "anyOf": [{ "type": "string" }, { "type": "null" }], "description": "c-outer" }, + "d": { + "anyOf": [{ "type": "string", "description": "d-inner" }, { "type": "null" }], + "description": "d-outer" + }, + "e": { "anyOf": [{ "type": "string" }, { "type": "null" }] } + }, + "required": [], + "additionalProperties": false + }) + }) + + it(`{ as: "Option" }`, async () => { + const schema = Schema.Struct({ + a: Schema.optionalWith(Schema.String, { as: "Option" }), + b: Schema.optionalWith(Schema.String.annotations({ description: "b-inner" }), { as: "Option" }), + c: Schema.optionalWith(Schema.String, { as: "Option" }).annotations({ description: "c-outer" }), + d: Schema.optionalWith(Schema.String.annotations({ description: "d-inner" }), { as: "Option" }) + .annotations({ description: "d-outer" }), + e: Schema.optionalWith(Schema.UndefinedOr(Schema.String), { as: "Option" }) + }) + await assertDraft7(schema, { + "type": "object", + "properties": { + "a": { "type": "string" }, + "b": { "type": "string", "description": "b-inner" }, + "c": { "type": "string", "description": "c-outer" }, + "d": { "type": "string", "description": "d-outer" }, + "e": { "type": "string" } + }, + "required": [], + "additionalProperties": false + }) + }) + + it(`{ as: "Option", nullable: true }`, async () => { + const schema = Schema.Struct({ + a: Schema.optionalWith(Schema.String, { as: "Option", nullable: true }), + b: Schema.optionalWith(Schema.String.annotations({ description: "b-inner" }), { + as: "Option", + nullable: true + }), + c: Schema.optionalWith(Schema.String, { as: "Option", nullable: true }).annotations({ + description: "c-outer" + }), + d: Schema.optionalWith(Schema.String.annotations({ description: "d-inner" }), { + as: "Option", + nullable: true + }).annotations({ + description: "d-outer" + }), + e: Schema.optionalWith(Schema.UndefinedOr(Schema.String), { as: "Option", nullable: true }) + }) + await assertDraft7(schema, { + "type": "object", + "properties": { + "a": { "anyOf": [{ "type": "string" }, { "type": "null" }] }, + "b": { + "anyOf": [{ "type": "string", "description": "b-inner" }, { "type": "null" }] + }, + "c": { "anyOf": [{ "type": "string" }, { "type": "null" }], "description": "c-outer" }, + "d": { + "anyOf": [{ "type": "string", "description": "d-inner" }, { "type": "null" }], + "description": "d-outer" + }, + "e": { "anyOf": [{ "type": "string" }, { "type": "null" }] } + }, + "required": [], + "additionalProperties": false + }) + }) + }) + + it("Struct + Record", async () => { + const schema = Schema.Struct({ + a: Schema.String + }, Schema.Record({ key: Schema.String, value: Schema.String })) + + await assertDraft7(schema, { + "type": "object", + "required": [ + "a" + ], + "properties": { + "a": { + "type": "string" + } + }, + "additionalProperties": { + "type": "string" + } + }) + }) + + describe("identifier annotation", () => { + it("should use the identifier annotation of the property signature values", async () => { + const schemaWithIdentifier = Schema.String.annotations({ + identifier: "my-id" + }) + + const schema = Schema.Struct({ + a: schemaWithIdentifier, + b: schemaWithIdentifier + }) + + await assertDraft7(schema, { + "$defs": { + "my-id": { + "type": "string" + } + }, + "type": "object", + "required": [ + "a", + "b" + ], + "properties": { + "a": { + "$ref": "#/$defs/my-id" + }, + "b": { + "$ref": "#/$defs/my-id" + } + }, + "additionalProperties": false + }) + }) + + it("should ignore the identifier annotation when annotating the value schema", async () => { + const schemaWithIdentifier = Schema.String.annotations({ + identifier: "my-id" + }) + + const schema = Schema.Struct({ + a: schemaWithIdentifier.annotations({ + description: "a-description" + }), + b: schemaWithIdentifier.annotations({ + description: "b-description" + }) + }) + + await assertDraft7(schema, { + "type": "object", + "required": [ + "a", + "b" + ], + "properties": { + "a": { + "type": "string", + "description": "a-description" + }, + "b": { + "type": "string", + "description": "b-description" + } + }, + "additionalProperties": false + }) + }) + + it("should use the identifier annotation when annotating the property signature", async () => { + const schemaWithIdentifier = Schema.String.annotations({ + identifier: "my-id" + }) + + const schema = Schema.Struct({ + a: Schema.propertySignature(schemaWithIdentifier).annotations({ + description: "a-description" + }), + b: Schema.propertySignature(schemaWithIdentifier).annotations({ + description: "b-description" + }) + }) + + await assertDraft7(schema, { + "$defs": { + "my-id": { + "type": "string" + } + }, + "type": "object", + "required": [ + "a", + "b" + ], + "properties": { + "a": { + "allOf": [ + { + "$ref": "#/$defs/my-id" + } + ], + "description": "a-description" + }, + "b": { + "allOf": [ + { + "$ref": "#/$defs/my-id" + } + ], + "description": "b-description" + } + }, + "additionalProperties": false + }) + }) + }) + }) + + describe("Record", () => { + it("Record(refinement, number)", async () => { + await assertDraft7( + Schema.Record({ key: Schema.String.pipe(Schema.minLength(1)), value: Schema.Number }), + { + "type": "object", + "required": [], + "properties": {}, + "patternProperties": { + "": { + "type": "number" + } + }, + "propertyNames": { + "type": "string", + "title": "minLength(1)", + "description": "a string at least 1 character(s) long", + "minLength": 1 + } + } + ) + }) + + it("Record(string, number)", async () => { + await assertDraft7(Schema.Record({ key: Schema.String, value: Schema.Number }), { + "type": "object", + "properties": {}, + "required": [], + "additionalProperties": { + "type": "number" + } + }) + }) + + it("Record('a' | 'b', number)", async () => { + await assertDraft7( + Schema.Record( + { key: Schema.Union(Schema.Literal("a"), Schema.Literal("b")), value: Schema.Number } + ), + { + "type": "object", + "properties": { + "a": { + "type": "number" + }, + "b": { + "type": "number" + } + }, + "required": ["a", "b"], + "additionalProperties": false + } + ) + }) + + it("Record(${string}-${string}, number)", async () => { + const schema = Schema.Record( + { key: Schema.TemplateLiteral(Schema.String, Schema.Literal("-"), Schema.String), value: Schema.Number } + ) + await assertDraft7(schema, { + "type": "object", + "required": [], + "properties": {}, + "patternProperties": { + "": { "type": "number" } + }, + "propertyNames": { + "pattern": "^[\\s\\S]*?-[\\s\\S]*?$", + "type": "string" + } + }) + }) + + it("Record(pattern, number)", async () => { + const schema = Schema.Record( + { key: Schema.String.pipe(Schema.pattern(new RegExp("^.*-.*$"))), value: Schema.Number } + ) + await assertDraft7(schema, { + "type": "object", + "required": [], + "properties": {}, + "patternProperties": { + "": { + "type": "number" + } + }, + "propertyNames": { + "description": "a string matching the pattern ^.*-.*$", + "pattern": "^.*-.*$", + "type": "string" + } + }) + }) + + it("Record(SymbolFromSelf & annotation, number)", async () => { + await assertDraft7( + Schema.Record({ + key: Schema.SymbolFromSelf.annotations({ jsonSchema: { "type": "string" } }), + value: Schema.Number + }), + { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "required": [], + "properties": {}, + "additionalProperties": { + "type": "number" + }, + "propertyNames": { + "type": "string" + } + } + ) + }) + + it("Record(string, UndefinedOr(number))", async () => { + await assertDraft7(Schema.Record({ key: Schema.String, value: Schema.UndefinedOr(Schema.Number) }), { + "type": "object", + "properties": {}, + "required": [], + "additionalProperties": { "type": "number" } + }) + }) + + it("partial(Struct + Record(string, number))", async () => { + const schema = Schema.partial( + Schema.Struct( + { foo: Schema.Number }, + { + key: Schema.String, + value: Schema.Number + } + ) + ) + + await assertDraft7(schema, { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "required": [], + "properties": { + "foo": { + "type": "number" + } + }, + "additionalProperties": { + "type": "number" + } + }) + }) + }) + + describe("Union", () => { + it("never members", async () => { + await assertDraft7(Schema.Union(Schema.String, Schema.Never), { + "type": "string" + }) + await assertDraft7(Schema.Union(Schema.String, Schema.Union(Schema.Never, Schema.Never)), { + "type": "string" + }) + }) + + it("String | Number", async () => { + await assertDraft7(Schema.Union(Schema.String, Schema.Number), { + "anyOf": [ + { "type": "string" }, + { "type": "number" } + ] + }) + }) + + describe("Union including literals", () => { + it(`1 | 2`, async () => { + await assertDraft7( + Schema.Union(Schema.Literal(1), Schema.Literal(2)), + { + "type": "number", + "enum": [1, 2] + } + ) + }) + + it(`1(with description) | 2`, async () => { + await assertDraft7( + Schema.Union( + Schema.Literal(1).annotations({ description: "1-description" }), + Schema.Literal(2) + ), + { + "anyOf": [ + { + "type": "number", + "enum": [1], + "description": "1-description" + }, + { "type": "number", "enum": [2] } + ] + } + ) + }) + + it(`1 | 2(with description)`, async () => { + await assertDraft7( + Schema.Union( + Schema.Literal(1), + Schema.Literal(2).annotations({ description: "2-description" }) + ), + { + "anyOf": [ + { "type": "number", "enum": [1] }, + { + "type": "number", + "enum": [2], + "description": "2-description" + } + ] + } + ) + }) + + it(`1 | 2 | string`, async () => { + await assertDraft7(Schema.Union(Schema.Literal(1), Schema.Literal(2), Schema.String), { + "anyOf": [ + { "type": "number", "enum": [1, 2] }, + { "type": "string" } + ] + }) + }) + + it(`(1 | 2) | string`, async () => { + await assertDraft7(Schema.Union(Schema.Literal(1, 2), Schema.String), { + "anyOf": [ + { "type": "number", "enum": [1, 2] }, + { "type": "string" } + ] + }) + }) + + it(`(1 | 2)(with description) | string`, async () => { + await assertDraft7( + Schema.Union( + Schema.Literal(1, 2).annotations({ description: "1-2-description" }), + Schema.String + ), + { + "anyOf": [ + { + "type": "number", + "enum": [1, 2], + "description": "1-2-description" + }, + { "type": "string" } + ] + } + ) + }) + + it(`(1 | 2)(with description) | 3 | string`, async () => { + await assertDraft7( + Schema.Union( + Schema.Literal(1, 2).annotations({ description: "1-2-description" }), + Schema.Literal(3), + Schema.String + ), + { + "anyOf": [ + { + "type": "number", + "enum": [1, 2], + "description": "1-2-description" + }, + { "enum": [3], "type": "number" }, + { + "type": "string" + } + ] + } + ) + }) + + it(`1(with description) | 2 | string`, async () => { + await assertDraft7( + Schema.Union( + Schema.Literal(1).annotations({ description: "1-description" }), + Schema.Literal(2), + Schema.String + ), + { + "anyOf": [ + { + "type": "number", + "description": "1-description", + "enum": [1] + }, + { "type": "number", "enum": [2] }, + { "type": "string" } + ] + } + ) + }) + + it(`1 | 2(with description) | string`, async () => { + await assertDraft7( + Schema.Union( + Schema.Literal(1), + Schema.Literal(2).annotations({ description: "2-description" }), + Schema.String + ), + { + "anyOf": [ + { "type": "number", "enum": [1] }, + { + "type": "number", + "description": "2-description", + "enum": [2] + }, + { "type": "string" } + ] + } + ) + }) + + it(`string | 1 | 2 `, async () => { + await assertDraft7(Schema.Union(Schema.String, Schema.Literal(1), Schema.Literal(2)), { + "anyOf": [ + { "type": "string" }, + { "type": "number", "enum": [1, 2] } + ] + }) + }) + + it(`string | (1 | 2) `, async () => { + await assertDraft7(Schema.Union(Schema.String, Schema.Literal(1, 2)), { + "anyOf": [ + { "type": "string" }, + { "type": "number", "enum": [1, 2] } + ] + }) + }) + + it(`string | 1(with description) | 2`, async () => { + await assertDraft7( + Schema.Union( + Schema.String, + Schema.Literal(1).annotations({ description: "1-description" }), + Schema.Literal(2) + ), + { + "anyOf": [ + { "type": "string" }, + { + "type": "number", + "description": "1-description", + "enum": [1] + }, + { "type": "number", "enum": [2] } + ] + } + ) + }) + + it(`string | 1 | 2(with description)`, async () => { + await assertDraft7( + Schema.Union( + Schema.String, + Schema.Literal(1), + Schema.Literal(2).annotations({ description: "2-description" }) + ), + { + "anyOf": [ + { "type": "string" }, + { "type": "number", "enum": [1] }, + { + "type": "number", + "description": "2-description", + "enum": [2] + } + ] + } + ) + }) + }) + }) + + describe("Suspend", () => { + it("suspend(() => schema).annotations({ identifier: '...' })", async () => { + interface A { + readonly a: string + readonly as: ReadonlyArray + } + const schema: Schema.Schema = Schema.suspend(() => + Schema.Struct({ + a: Schema.String, + as: Schema.Array(schema) + }) + ).annotations({ identifier: "ID" }) + await assertDraft7(schema, { + "$ref": "#/$defs/ID", + "$defs": { + "ID": { + "type": "object", + "required": [ + "a", + "as" + ], + "properties": { + "a": { + "type": "string" + }, + "as": { + "type": "array", + "items": { + "$ref": "#/$defs/ID" + } + } + }, + "additionalProperties": false + } + } + }) + }) + + it("suspend(() => schema.annotations({ identifier: '...' }))", async () => { + interface A { + readonly a: string + readonly as: ReadonlyArray + } + const schema: Schema.Schema = Schema.suspend(() => + Schema.Struct({ + a: Schema.String, + as: Schema.Array(schema) + }).annotations({ identifier: "ID" }) + ) + await assertDraft7(schema, { + "$ref": "#/$defs/ID", + "$defs": { + "ID": { + "type": "object", + "required": [ + "a", + "as" + ], + "properties": { + "a": { + "type": "string" + }, + "as": { + "type": "array", + "items": { + "$ref": "#/$defs/ID" + } + } + }, + "additionalProperties": false + } + } + }) + }) + + it("inner annotation", async () => { + interface A { + readonly a: string + readonly as: ReadonlyArray + } + const schema = Schema.Struct({ + a: Schema.String, + as: Schema.Array( + Schema.suspend((): Schema.Schema => schema).annotations({ + identifier: "ID" + }) + ) + }) + await assertDraft7(schema, { + "type": "object", + "required": [ + "a", + "as" + ], + "properties": { + "a": { + "type": "string" + }, + "as": { + "type": "array", + "items": { + "$ref": "#/$defs/ID" + } + } + }, + "additionalProperties": false, + "$defs": { + "ID": { + "type": "object", + "required": [ + "a", + "as" + ], + "properties": { + "a": { + "type": "string" + }, + "as": { + "type": "array", + "items": { + "$ref": "#/$defs/ID" + } + } + }, + "additionalProperties": false + } + } + }) + }) + + it("outer annotation", async () => { + interface A { + readonly a: string + readonly as: ReadonlyArray + } + const schema = Schema.Struct({ + a: Schema.String, + as: Schema.Array(Schema.suspend((): Schema.Schema => schema)) + }).annotations({ identifier: "ID" }) + await assertDraft7(schema, { + "$ref": "#/$defs/ID", + "$defs": { + "ID": { + "type": "object", + "required": [ + "a", + "as" + ], + "properties": { + "a": { + "type": "string" + }, + "as": { + "type": "array", + "items": { + "$ref": "#/$defs/ID" + } + } + }, + "additionalProperties": false + } + } + }) + }) + + it("should support mutually suspended schemas", async () => { + interface Expression { + readonly type: "expression" + readonly value: number | Operation + } + + interface Operation { + readonly type: "operation" + readonly operator: "+" | "-" + readonly left: Expression + readonly right: Expression + } + + // intended outer suspend + const Expression: Schema.Schema = Schema.suspend(() => + Schema.Struct({ + type: Schema.Literal("expression"), + value: Schema.Union(Schema.Number, Operation) + }) + ).annotations({ identifier: "2ad5683a-878f-4e4d-909c-496e59ce62e0" }) + + // intended outer suspend + const Operation: Schema.Schema = Schema.suspend(() => + Schema.Struct({ + type: Schema.Literal("operation"), + operator: Schema.Union(Schema.Literal("+"), Schema.Literal("-")), + left: Expression, + right: Expression + }) + ).annotations({ identifier: "e0f2ce47-eac7-4991-8730-90ebe4e0ffda" }) + + await assertDraft7(Operation, { + "$ref": "#/$defs/e0f2ce47-eac7-4991-8730-90ebe4e0ffda", + "$defs": { + "e0f2ce47-eac7-4991-8730-90ebe4e0ffda": { + "type": "object", + "required": [ + "type", + "operator", + "left", + "right" + ], + "properties": { + "type": { + "type": "string", + "enum": ["operation"] + }, + "operator": { + "type": "string", + "enum": ["+", "-"] + }, + "left": { + "$ref": "#/$defs/2ad5683a-878f-4e4d-909c-496e59ce62e0" + }, + "right": { + "$ref": "#/$defs/2ad5683a-878f-4e4d-909c-496e59ce62e0" + } + }, + "additionalProperties": false + }, + "2ad5683a-878f-4e4d-909c-496e59ce62e0": { + "type": "object", + "required": [ + "type", + "value" + ], + "properties": { + "type": { + "type": "string", + "enum": ["expression"] + }, + "value": { + "anyOf": [ + { + "type": "number" + }, + { + "$ref": "#/$defs/e0f2ce47-eac7-4991-8730-90ebe4e0ffda" + } + ] + } + }, + "additionalProperties": false + } + } + }) + }) + }) + + describe("Class", () => { + it("should use the identifier as JSON Schema identifier", async () => { + class A extends Schema.Class("A")(Schema.Struct({ a: Schema.String })) {} + await assertDraft7(A, { + "$defs": { + "A": { + "type": "object", + "required": [ + "a" + ], + "properties": { + "a": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "$ref": "#/$defs/A" + }) + }) + + it("type side json schema annotation", async () => { + class A extends Schema.Class("A")(Schema.Struct({ a: Schema.String }), { + identifier: "A2" + }) {} + await assertDraft7(A, { + "$defs": { + "A2": { + "type": "object", + "required": [ + "a" + ], + "properties": { + "a": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "$ref": "#/$defs/A2" + }) + }) + + it("transformation side json schema annotation", async () => { + class A extends Schema.Class("A")(Schema.Struct({ a: Schema.String }), [ + undefined, + { + identifier: "A2" + } + ]) {} + await assertDraft7(A, { + "$defs": { + "A2": { + "type": "object", + "required": [ + "a" + ], + "properties": { + "a": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "$ref": "#/$defs/A2" + }) + }) + + it("from side json schema annotation", async () => { + class A extends Schema.Class("A")(Schema.Struct({ a: Schema.String }), [ + undefined, + undefined, + { + identifier: "A2" + } + ]) {} + await assertDraft7(A, { + "$defs": { + "A": { + "type": "object", + "required": [ + "a" + ], + "properties": { + "a": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "$ref": "#/$defs/A" + }) + }) + + it("should escape special characters in the $ref", async () => { + class A extends Schema.Class("~package/name")(Schema.Struct({ a: Schema.String })) {} + await assertDraft7(A, { + "$defs": { + "~package/name": { + "type": "object", + "required": [ + "a" + ], + "properties": { + "a": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "$ref": "#/$defs/~0package~1name" + }) + }) + }) + + it("compose", async () => { + const schema = Schema.Struct({ + a: Schema.NonEmptyString.pipe(Schema.compose(Schema.NumberFromString)) + }) + await assertDraft7(schema, { + "$defs": { + "NonEmptyString": { + "type": "string", + "title": "nonEmptyString", + "description": "a non empty string", + "minLength": 1 + } + }, + "type": "object", + "required": [ + "a" + ], + "properties": { + "a": { + "$ref": "#/$defs/NonEmptyString" + } + }, + "additionalProperties": false + }) + }) + + it("should correctly generate JSON Schemas for a schema created by extending two refinements", async () => { + await assertDraft7( + Schema.Struct({ + a: Schema.String + }).pipe( + Schema.filter(() => true, { + jsonSchema: { "examples": ["c5052c04-d6c9-44f3-9c8f-ede707d6ce38"] } + }) + ).pipe(Schema.extend( + Schema.Struct({ + b: Schema.Number + }).pipe( + Schema.filter(() => true, { + jsonSchema: { "$comment": "940b4ea4-6313-4b59-9e64-ff7a41b8eb15" } + }) + ) + )), + { + "type": "object", + "required": ["a", "b"], + "properties": { + "a": { "type": "string" }, + "b": { "type": "number" } + }, + "examples": ["c5052c04-d6c9-44f3-9c8f-ede707d6ce38"], + "$comment": "940b4ea4-6313-4b59-9e64-ff7a41b8eb15", + "additionalProperties": false + } + ) + }) + + describe("identifier annotation support", () => { + it("String", async () => { + await assertDraft7(Schema.String.annotations({ identifier: "ID" }), { + "$defs": { + "ID": { + "type": "string" + } + }, + "$ref": "#/$defs/ID" + }) + await assertDraft7(Schema.String.annotations({ identifier: "ID", description: "description" }), { + "$defs": { + "ID": { + "type": "string", + "description": "description" + } + }, + "$ref": "#/$defs/ID" + }) + }) + + it("Refinement", async () => { + await assertDraft7( + Schema.String.pipe(Schema.minLength(2)).annotations({ identifier: "ID" }), + { + "$defs": { + "ID": { + "type": "string", + "title": "minLength(2)", + "description": "a string at least 2 character(s) long", + "minLength": 2 + } + }, + "$ref": "#/$defs/ID" + } + ) + }) + + describe("Struct", () => { + it("annotation", async () => { + await assertDraft7( + Schema.Struct({ + a: Schema.String + }).annotations({ identifier: "ID" }), + { + "$defs": { + "ID": { + "type": "object", + "required": ["a"], + "properties": { + "a": { "type": "string" } + }, + "additionalProperties": false + } + }, + "$ref": "#/$defs/ID" + } + ) + }) + + it("field annotations", async () => { + const Name = Schema.String.annotations({ + identifier: "ID", + description: "description" + }) + const schema = Schema.Struct({ + a: Name + }) + await assertDraft7(schema, { + "$defs": { + "ID": { + "type": "string", + "description": "description" + } + }, + "type": "object", + "required": ["a"], + "properties": { + "a": { + "$ref": "#/$defs/ID" + } + }, + "additionalProperties": false + }) + }) + + it("self annotation + field annotations", async () => { + const Name = Schema.String.annotations({ + identifier: "b49f125d-1646-4eb5-8120-9524ab6039de", + description: "703b7ff0-cb8d-49de-aeeb-05d92faa4599", + title: "4b6d9ea6-7c4d-4073-a427-8d1b82fd1677" + }) + await assertDraft7( + Schema.Struct({ + a: Name + }).annotations({ identifier: "7e559891-9143-4138-ae3e-81a5f0907380" }), + { + "$defs": { + "7e559891-9143-4138-ae3e-81a5f0907380": { + "type": "object", + "required": ["a"], + "properties": { + "a": { "$ref": "#/$defs/b49f125d-1646-4eb5-8120-9524ab6039de" } + }, + "additionalProperties": false + }, + "b49f125d-1646-4eb5-8120-9524ab6039de": { + "type": "string", + "description": "703b7ff0-cb8d-49de-aeeb-05d92faa4599", + "title": "4b6d9ea6-7c4d-4073-a427-8d1b82fd1677" + } + }, + "$ref": "#/$defs/7e559891-9143-4138-ae3e-81a5f0907380" + } + ) + }) + + it("deeply nested field annotations", async () => { + const Name = Schema.String.annotations({ + identifier: "434a08dd-3f8f-4de4-b91d-8846aab1fb05", + description: "eb183f5c-404c-4686-b78b-1bd00d18f8fd", + title: "c0cbd438-1fb5-47fe-bf81-1ff5527e779a" + }) + const schema = Schema.Struct({ a: Name, b: Schema.Struct({ c: Name }) }) + await assertDraft7(schema, { + "$defs": { + "434a08dd-3f8f-4de4-b91d-8846aab1fb05": { + "type": "string", + "description": "eb183f5c-404c-4686-b78b-1bd00d18f8fd", + "title": "c0cbd438-1fb5-47fe-bf81-1ff5527e779a" + } + }, + "type": "object", + "required": ["a", "b"], + "properties": { + "a": { + "$ref": "#/$defs/434a08dd-3f8f-4de4-b91d-8846aab1fb05" + }, + "b": { + "type": "object", + "required": ["c"], + "properties": { + "c": { "$ref": "#/$defs/434a08dd-3f8f-4de4-b91d-8846aab1fb05" } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }) + }) + }) + + describe("Union", () => { + it("Union of literals with identifiers", async () => { + await assertDraft7( + Schema.Union( + Schema.Literal("a").annotations({ + description: "ef296f1c-01fe-4a20-bd35-ed449c964c49", + identifier: "170d659f-112e-4e3b-85db-464b668f2aed" + }), + Schema.Literal("b").annotations({ + description: "effbf54b-a62d-455b-86fa-97a5af46c6f3", + identifier: "2a4e4f67-3732-4f7b-a505-856e51dd1578" + }) + ), + { + "$defs": { + "170d659f-112e-4e3b-85db-464b668f2aed": { + "type": "string", + "enum": ["a"], + "description": "ef296f1c-01fe-4a20-bd35-ed449c964c49" + }, + "2a4e4f67-3732-4f7b-a505-856e51dd1578": { + "type": "string", + "enum": ["b"], + "description": "effbf54b-a62d-455b-86fa-97a5af46c6f3" + } + }, + "anyOf": [ + { "$ref": "#/$defs/170d659f-112e-4e3b-85db-464b668f2aed" }, + { "$ref": "#/$defs/2a4e4f67-3732-4f7b-a505-856e51dd1578" } + ] + } + ) + }) + }) + }) + + it("should filter out invalid examples", async () => { + await assertDraft7(Schema.NonEmptyString.annotations({ examples: ["", "a"] }), { + "type": "string", + "title": "nonEmptyString", + "description": "a non empty string", + "minLength": 1, + "examples": ["a"] + }) + }) + + it("should filter out invalid defaults", async () => { + await assertDraft7(Schema.NonEmptyString.annotations({ default: "" }), { + "type": "string", + "title": "nonEmptyString", + "description": "a non empty string", + "minLength": 1 + }) + }) + + describe("should encode the examples", () => { + it("property signatures", async () => { + const schema = Schema.Struct({ + a: Schema.NumberFromString.pipe(Schema.propertySignature).annotations({ examples: [1, 2] }) + }) + await assertDraft7(schema, { + "$defs": { + "NumberFromString": { + "description": "a string to be decoded into a number", + "type": "string" + } + }, + "type": "object", + "required": [ + "a" + ], + "properties": { + "a": { + "allOf": [ + { + "$ref": "#/$defs/NumberFromString" + } + ], + "examples": ["1", "2"] + } + }, + "additionalProperties": false + }) + }) + + it("elements", async () => { + const schema = Schema.Tuple(Schema.NumberFromString.pipe(Schema.element).annotations({ examples: [1, 2] })) + await assertDraft7(schema, { + "$defs": { + "NumberFromString": { + "description": "a string to be decoded into a number", + "type": "string" + } + }, + "type": "array", + "items": [ + { + "allOf": [ + { + "$ref": "#/$defs/NumberFromString" + } + ], + "examples": ["1", "2"] + } + ], + "minItems": 1, + "additionalItems": false + }) + }) + }) + + it("Exit", async () => { + const schema = Schema.Exit({ + failure: Schema.String, + success: Schema.Number, + defect: Schema.Defect + }) + await assertDraft7(schema, { + "$schema": "http://json-schema.org/draft-07/schema#", + "$defs": { + "CauseEncoded0": { + "anyOf": [ + { + "type": "object", + "required": [ + "_tag" + ], + "properties": { + "_tag": { + "type": "string", + "enum": [ + "Empty" + ] + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "_tag", + "error" + ], + "properties": { + "_tag": { + "type": "string", + "enum": [ + "Fail" + ] + }, + "error": { + "type": "string" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "_tag", + "defect" + ], + "properties": { + "_tag": { + "type": "string", + "enum": [ + "Die" + ] + }, + "defect": { + "$ref": "#/$defs/Defect" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "_tag", + "fiberId" + ], + "properties": { + "_tag": { + "type": "string", + "enum": [ + "Interrupt" + ] + }, + "fiberId": { + "$ref": "#/$defs/FiberIdEncoded" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "_tag", + "left", + "right" + ], + "properties": { + "_tag": { + "type": "string", + "enum": [ + "Sequential" + ] + }, + "left": { + "$ref": "#/$defs/CauseEncoded0" + }, + "right": { + "$ref": "#/$defs/CauseEncoded0" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "_tag", + "left", + "right" + ], + "properties": { + "_tag": { + "type": "string", + "enum": [ + "Parallel" + ] + }, + "left": { + "$ref": "#/$defs/CauseEncoded0" + }, + "right": { + "$ref": "#/$defs/CauseEncoded0" + } + }, + "additionalProperties": false + } + ], + "title": "CauseEncoded" + }, + "Defect": { + "$id": "/schemas/unknown", + "title": "unknown" + }, + "FiberIdEncoded": { + "anyOf": [ + { + "$ref": "#/$defs/FiberIdNoneEncoded" + }, + { + "$ref": "#/$defs/FiberIdRuntimeEncoded" + }, + { + "$ref": "#/$defs/FiberIdCompositeEncoded" + } + ] + }, + "FiberIdNoneEncoded": { + "type": "object", + "required": [ + "_tag" + ], + "properties": { + "_tag": { + "type": "string", + "enum": [ + "None" + ] + } + }, + "additionalProperties": false + }, + "FiberIdRuntimeEncoded": { + "type": "object", + "required": [ + "_tag", + "id", + "startTimeMillis" + ], + "properties": { + "_tag": { + "type": "string", + "enum": [ + "Runtime" + ] + }, + "id": { + "$ref": "#/$defs/Int" + }, + "startTimeMillis": { + "$ref": "#/$defs/Int" + } + }, + "additionalProperties": false + }, + "Int": { + "type": "integer", + "description": "an integer", + "title": "int" + }, + "FiberIdCompositeEncoded": { + "type": "object", + "required": [ + "_tag", + "left", + "right" + ], + "properties": { + "_tag": { + "type": "string", + "enum": [ + "Composite" + ] + }, + "left": { + "$ref": "#/$defs/FiberIdEncoded" + }, + "right": { + "$ref": "#/$defs/FiberIdEncoded" + } + }, + "additionalProperties": false + } + }, + "anyOf": [ + { + "type": "object", + "required": [ + "_tag", + "cause" + ], + "properties": { + "_tag": { + "type": "string", + "enum": [ + "Failure" + ] + }, + "cause": { + "$ref": "#/$defs/CauseEncoded0" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "_tag", + "value" + ], + "properties": { + "_tag": { + "type": "string", + "enum": [ + "Success" + ] + }, + "value": { + "type": "number" + } + }, + "additionalProperties": false + } + ], + "title": "ExitEncoded" + }) + }) + + describe("Schema.encodedBoundSchema / Schema.encodedSchema", () => { + describe("Suspend", () => { + it("without inner transformations", async () => { + interface Category { + readonly name: string + readonly categories: ReadonlyArray + } + + const schema: Schema.Schema = Schema.Struct({ + name: Schema.String, + categories: Schema.Array( + Schema.suspend(() => schema).annotations({ identifier: "ID" }) + ) + }) + + await assertDraft7(Schema.encodedBoundSchema(schema), { + "type": "object", + "required": [ + "name", + "categories" + ], + "properties": { + "name": { + "type": "string" + }, + "categories": { + "type": "array", + "items": { + "$ref": "#/$defs/IDEncodedBound" + } + } + }, + "additionalProperties": false, + "$defs": { + "IDEncodedBound": { + "type": "object", + "required": [ + "name", + "categories" + ], + "properties": { + "name": { + "type": "string" + }, + "categories": { + "type": "array", + "items": { + "$ref": "#/$defs/IDEncodedBound" + } + } + }, + "additionalProperties": false + } + } + }) + await assertDraft7(Schema.encodedSchema(schema), { + "type": "object", + "required": [ + "name", + "categories" + ], + "properties": { + "name": { + "type": "string" + }, + "categories": { + "type": "array", + "items": { + "$ref": "#/$defs/IDEncoded" + } + } + }, + "additionalProperties": false, + "$defs": { + "IDEncoded": { + "type": "object", + "required": [ + "name", + "categories" + ], + "properties": { + "name": { + "type": "string" + }, + "categories": { + "type": "array", + "items": { + "$ref": "#/$defs/IDEncoded" + } + } + }, + "additionalProperties": false + } + } + }) + }) + + it("with inner transformations", async () => { + interface Category { + readonly name: number + readonly categories: ReadonlyArray + } + interface CategoryEncoded { + readonly name: string + readonly categories: ReadonlyArray + } + + const schema: Schema.Schema = Schema.Struct({ + name: Schema.NumberFromString, + categories: Schema.Array( + Schema.suspend(() => schema).annotations({ identifier: "ID" }) + ) + }) + + await assertDraft7(Schema.encodedBoundSchema(schema), { + "type": "object", + "required": [ + "name", + "categories" + ], + "properties": { + "name": { + "type": "string", + "description": "a string to be decoded into a number" + }, + "categories": { + "type": "array", + "items": { + "$ref": "#/$defs/IDEncodedBound" + } + } + }, + "additionalProperties": false, + "$defs": { + "IDEncodedBound": { + "type": "object", + "required": [ + "name", + "categories" + ], + "properties": { + "name": { + "type": "string", + "description": "a string to be decoded into a number" + }, + "categories": { + "type": "array", + "items": { + "$ref": "#/$defs/IDEncodedBound" + } + } + }, + "additionalProperties": false + } + } + }) + await assertDraft7(Schema.encodedSchema(schema), { + "type": "object", + "required": [ + "name", + "categories" + ], + "properties": { + "name": { + "type": "string", + "description": "a string to be decoded into a number" + }, + "categories": { + "type": "array", + "items": { + "$ref": "#/$defs/IDEncoded" + } + } + }, + "additionalProperties": false, + "$defs": { + "IDEncoded": { + "type": "object", + "required": [ + "name", + "categories" + ], + "properties": { + "name": { + "type": "string", + "description": "a string to be decoded into a number" + }, + "categories": { + "type": "array", + "items": { + "$ref": "#/$defs/IDEncoded" + } + } + }, + "additionalProperties": false + } + } + }) + }) + }) + }) + + describe("jsonSchema annotation support", () => { + describe("Class", () => { + it("custom annotation", async () => { + class A extends Schema.Class("A")({ a: Schema.String }, { + jsonSchema: { "type": "string" } + }) {} + await assertDraft7(A, { + "$defs": { + "A": { + "type": "string" + } + }, + "$ref": "#/$defs/A" + }) + }) + + it("should support typeSchema(Class) with custom annotation", async () => { + class A extends Schema.Class("A")({ a: Schema.String }, { + jsonSchema: { "type": "string" } + }) {} + await assertDraft7(Schema.typeSchema(A), { + "$defs": { + "A": { + "type": "string" + } + }, + "$ref": "#/$defs/A" + }) + }) + }) + + it("Declaration", async () => { + class MyType {} + const schema = Schema.declare((x) => x instanceof MyType, { + jsonSchema: { + type: "string", + description: "default-description" + } + }) + await assertDraft7(schema, { + "type": "string", + "description": "default-description" + }) + await assertDraft7( + schema.annotations({ + description: "description" + }), + { + "type": "string", + "description": "description" + } + ) + }) + + it("Void", async () => { + await assertDraft7(Schema.Void.annotations({ jsonSchema: { "type": "string" } }), { + "type": "string" + }) + }) + + it("Never", async () => { + await assertDraft7(Schema.Never.annotations({ jsonSchema: { "type": "string" } }), { + "type": "string" + }) + }) + + it("Literal", async () => { + await assertDraft7(Schema.Literal("a").annotations({ jsonSchema: { "type": "string" } }), { + "type": "string" + }) + }) + + it("SymbolFromSelf", async () => { + await assertDraft7(Schema.SymbolFromSelf.annotations({ jsonSchema: { "type": "string" } }), { + "type": "string" + }) + }) + + it("UniqueSymbolFromSelf", async () => { + await assertDraft7( + Schema.UniqueSymbolFromSelf(Symbol.for("effect/schema/test/a")).annotations({ + jsonSchema: { "type": "string" } + }), + { "type": "string" } + ) + }) + + it("TemplateLiteral", async () => { + await assertDraft7( + Schema.TemplateLiteral(Schema.Literal("a"), Schema.String, Schema.Literal("b")).annotations({ + jsonSchema: { "type": "string" } + }), + { "type": "string" } + ) + }) + + it("Undefined", async () => { + await assertDraft7(Schema.Undefined.annotations({ jsonSchema: { "type": "string" } }), { + "type": "string" + }) + }) + + it("Unknown", async () => { + await assertDraft7(Schema.Unknown.annotations({ jsonSchema: { "type": "string" } }), { + "type": "string" + }) + }) + + it("Any", async () => { + await assertDraft7(Schema.Any.annotations({ jsonSchema: { "type": "string" } }), { + "type": "string" + }) + }) + + it("Object", async () => { + await assertDraft7(Schema.Object.annotations({ jsonSchema: { "type": "string" } }), { + "type": "string" + }) + }) + + it("String", async () => { + await assertDraft7( + Schema.String.annotations({ + jsonSchema: { + "type": "string", + "description": "description", + "format": "uuid" + } + }), + { + "type": "string", + "description": "description", + "format": "uuid" + } + ) + await assertDraft7( + Schema.String.annotations({ + identifier: "630d10c4-7030-45e7-894d-2c0bf5acadcf", + jsonSchema: { "type": "string", "description": "description" } + }), + { + "$defs": { + "630d10c4-7030-45e7-894d-2c0bf5acadcf": { + "type": "string", + "description": "description" + } + }, + "$ref": "#/$defs/630d10c4-7030-45e7-894d-2c0bf5acadcf" + } + ) + }) + + it("Number", async () => { + await assertDraft7(Schema.Number.annotations({ jsonSchema: { "type": "string" } }), { + "type": "string" + }) + }) + + it("BigintFromSelf", async () => { + await assertDraft7(Schema.BigIntFromSelf.annotations({ jsonSchema: { "type": "string" } }), { + "type": "string" + }) + }) + + it("Boolean", async () => { + await assertDraft7(Schema.Boolean.annotations({ jsonSchema: { "type": "string" } }), { + "type": "string" + }) + }) + + it("Enums", async () => { + enum Fruits { + Apple, + Banana + } + await assertDraft7(Schema.Enums(Fruits).annotations({ jsonSchema: { "type": "string" } }), { + "type": "string" + }) + }) + + it("Tuple", async () => { + await assertDraft7( + Schema.Tuple(Schema.String, Schema.Number).annotations({ jsonSchema: { "type": "string" } }), + { "type": "string" } + ) + }) + + it("Struct", async () => { + await assertDraft7( + Schema.Struct({ a: Schema.String, b: Schema.Number }).annotations({ + jsonSchema: { "type": "string" } + }), + { "type": "string" } + ) + }) + + it("Union", async () => { + await assertDraft7( + Schema.Union(Schema.String, Schema.Number).annotations({ jsonSchema: { "type": "string" } }), + { "type": "string" } + ) + }) + + it("UUID", async () => { + await assertDraft7( + Schema.UUID, + { + "$defs": { + "UUID": { + "description": "a Universally Unique Identifier", + "pattern": "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$", + "type": "string", + "format": "uuid" + } + }, + "$ref": "#/$defs/UUID" + } + ) + }) + + it("Suspend", async () => { + interface A { + readonly a: string + readonly as: ReadonlyArray + } + const schema = Schema.Struct({ + a: Schema.String, + as: Schema.Array( + Schema.suspend((): Schema.Schema => schema).annotations({ jsonSchema: { "type": "string" } }) + ) + }) + + await assertDraft7(schema, { + "type": "object", + "required": [ + "a", + "as" + ], + "properties": { + "a": { + "type": "string" + }, + "as": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + }) + }) + + describe("Refinement", () => { + it("Int", async () => { + await assertDraft7(Schema.Int.annotations({ jsonSchema: { "type": "string" } }), { + "type": "string" + }) + }) + + it("custom", async () => { + await assertDraft7( + Schema.String.pipe(Schema.filter(() => true, { jsonSchema: {} })).annotations({ + identifier: "ID" + }), + { + "$ref": "#/$defs/ID", + "$defs": { + "ID": { + "type": "string" + } + } + } + ) + }) + }) + + it("Transformation", async () => { + await assertDraft7(Schema.NumberFromString.annotations({ jsonSchema: { "type": "string" } }), { + "type": "string" + }) + }) + + it("refinement of a transformation with an override annotation", async () => { + await assertDraft7(Schema.Date.annotations({ jsonSchema: { type: "string", format: "date-time" } }), { + "format": "date-time", + "type": "string" + }) + await assertDraft7( + Schema.Date.annotations({ + jsonSchema: { anyOf: [{ type: "object" }, { type: "array" }] } + }), + { + "anyOf": [{ "type": "object" }, { "type": "array" }] + } + ) + await assertDraft7(Schema.Date.annotations({ jsonSchema: { "$ref": "x" } }), { + "$ref": "x" + }) + await assertDraft7(Schema.Date.annotations({ jsonSchema: { "type": "number", "const": 1 } }), { + "type": "number", + "const": 1 + }) + await assertDraft7(Schema.Date.annotations({ jsonSchema: { "type": "number", "enum": [1] } }), { + "type": "number", + "enum": [1] + }) + }) + + it("refinement of a transformation without an override annotation", async () => { + await assertDraft7(Schema.Trim.pipe(Schema.nonEmptyString()), { + "type": "string", + "description": "a string that will be trimmed" + }) + await assertDraft7( + Schema.Trim.pipe(Schema.nonEmptyString({ jsonSchema: { title: "a0ba6c10-091e-4ceb-9773-25fb1466fb1b" } })), + { + "type": "string", + "description": "a string that will be trimmed" + } + ) + await assertDraft7( + Schema.Trim.pipe(Schema.nonEmptyString()).annotations({ + jsonSchema: { title: "75f7eb4f-626d-4dc6-af48-c17094418d85" } + }), + { + "type": "string", + "description": "a string that will be trimmed" + } + ) + }) + + it("should detect a fragment on a non-refinement schema", async () => { + const schema = Schema.UUID.pipe( + Schema.compose(Schema.String), + Schema.annotations({ + identifier: "UUID", + title: "title", + description: "description", + jsonSchema: { + format: "uuid" // fragment + } + }) + ) + await assertDraft7( + schema, + { + "$defs": { + "UUID": { + "type": "string", + "description": "description", + "title": "title", + "format": "uuid", + "pattern": "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$" + } + }, + "$ref": "#/$defs/UUID" + } + ) + }) + }) + + describe("Pruning `undefined` and make the property optional by default", () => { + it("Undefined", async () => { + await assertDraft7( + Schema.Struct({ + a: Schema.Undefined + }), + { + "type": "object", + "required": [], + "properties": { + "a": { + "$id": "/schemas/never", + "not": {}, + "title": "never" + } + }, + "additionalProperties": false + } + ) + }) + + it("UndefinedOr(Undefined)", async () => { + await assertDraft7( + Schema.Struct({ + a: Schema.UndefinedOr(Schema.Undefined) + }), + { + "type": "object", + "required": [], + "properties": { + "a": { + "$id": "/schemas/never", + "not": {}, + "title": "never" + } + }, + "additionalProperties": false + } + ) + }) + + it("Nested `Undefined`s", async () => { + await assertDraft7( + Schema.Struct({ + a: Schema.UndefinedOr(Schema.UndefinedOr(Schema.Undefined)) + }), + { + "type": "object", + "required": [], + "properties": { + "a": { + "$id": "/schemas/never", + "not": {}, + "title": "never" + } + }, + "additionalProperties": false + } + ) + }) + + it("Schema.optional", async () => { + await assertDraft7( + Schema.Struct({ + a: Schema.optional(Schema.String) + }), + { + "type": "object", + "required": [], + "properties": { + "a": { "type": "string" } + }, + "additionalProperties": false + } + ) + }) + + it("Schema.optional + inner annotation", async () => { + await assertDraft7( + Schema.Struct({ + a: Schema.optional(Schema.String.annotations({ description: "inner" })) + }), + { + "type": "object", + "required": [], + "properties": { + "a": { + "type": "string", + "description": "inner" + } + }, + "additionalProperties": false + } + ) + }) + + it("Schema.optional + outer annotation should override inner annotation", async () => { + await assertDraft7( + Schema.Struct({ + a: Schema.optional(Schema.String.annotations({ description: "inner" })).annotations({ + description: "outer" + }) + }), + { + "type": "object", + "required": [], + "properties": { + "a": { + "type": "string", + "description": "outer" + } + }, + "additionalProperties": false + } + ) + }) + + it("UndefinedOr", async () => { + await assertDraft7( + Schema.Struct({ + a: Schema.UndefinedOr(Schema.String) + }), + { + "type": "object", + "required": [], + "properties": { + "a": { + "type": "string" + } + }, + "additionalProperties": false + } + ) + }) + + it("UndefinedOr + inner annotation", async () => { + await assertDraft7( + Schema.Struct({ + a: Schema.UndefinedOr(Schema.String.annotations({ description: "inner" })) + }), + { + "type": "object", + "required": [], + "properties": { + "a": { + "type": "string", + "description": "inner" + } + }, + "additionalProperties": false + } + ) + }) + + it("UndefinedOr + annotation should not override inner annotations", async () => { + await assertDraft7( + Schema.Struct({ + a: Schema.UndefinedOr(Schema.String.annotations({ description: "inner" })).annotations({ + description: "middle" + }) + }), + { + "type": "object", + "required": [], + "properties": { + "a": { + "type": "string", + "description": "inner" + } + }, + "additionalProperties": false + } + ) + }) + + it("UndefinedOr + propertySignature annotation should override inner and middle annotations", async () => { + await assertDraft7( + Schema.Struct({ + a: Schema.propertySignature( + Schema.UndefinedOr(Schema.String.annotations({ description: "inner" })).annotations({ + description: "middle" + }) + ).annotations({ description: "outer" }) + }), + { + "type": "object", + "required": [], + "properties": { + "a": { + "type": "string", + "description": "outer" + } + }, + "additionalProperties": false + } + ) + }) + + it("UndefinedOr + jsonSchema annotation should keep the property required", async () => { + await assertDraft7( + Schema.Struct({ + a: Schema.UndefinedOr(Schema.String).annotations({ jsonSchema: { "type": "string" } }) + }), + { + "type": "object", + "required": ["a"], + "properties": { + "a": { "type": "string" } + }, + "additionalProperties": false + } + ) + }) + + it("Transformation: OptionFromUndefinedOr", async () => { + await assertDraft7( + Schema.Struct({ + a: Schema.OptionFromUndefinedOr(Schema.String) + }), + { + "type": "object", + "required": [], + "properties": { + "a": { + "type": "string" + } + }, + "additionalProperties": false + } + ) + }) + + it("Suspend", async () => { + await assertDraft7( + Schema.Struct({ + a: Schema.suspend(() => Schema.UndefinedOr(Schema.String)) + }), + { + "type": "object", + "required": [], + "properties": { + "a": { + "type": "string" + } + }, + "additionalProperties": false + } + ) + }) + }) + + describe("fromKey", () => { + it("with transformation identifier annotation", async () => { + await assertDraft7( + Schema.Struct({ + a: Schema.NonEmptyString.pipe(Schema.propertySignature, Schema.fromKey("b")) + }).annotations({ + identifier: "ID", + description: "struct-description" + }), + { + "$ref": "#/$defs/ID", + "$defs": { + "NonEmptyString": { + "type": "string", + "title": "nonEmptyString", + "description": "a non empty string", + "minLength": 1 + }, + "ID": { + "type": "object", + "required": [ + "b" + ], + "properties": { + "b": { + "$ref": "#/$defs/NonEmptyString" + } + }, + "additionalProperties": false, + "description": "struct-description" + } + } + } + ) + }) + }) + }) + + describe("jsonSchema2019-09", () => { + describe("nullable handling", () => { + it("Null", async () => { + const schema = Schema.Null + await assertDraft201909(schema, { "type": "null" }) + }) + + it("NullOr(String)", async () => { + const schema = Schema.NullOr(Schema.String) + await assertDraft201909(schema, { + "anyOf": [ + { "type": "string" }, + { "type": "null" } + ] + }) + }) + + it("NullOr(Any)", async () => { + const schema = Schema.NullOr(Schema.Any) + await assertDraft201909(schema, { + "$id": "/schemas/any", + "title": "any" + }) + }) + + it("NullOr(Unknown)", async () => { + const schema = Schema.NullOr(Schema.Unknown) + await assertDraft201909(schema, { + "$id": "/schemas/unknown", + "title": "unknown" + }) + }) + + it("NullOr(Void)", async () => { + const schema = Schema.NullOr(Schema.Void) + await assertDraft201909(schema, { + "$id": "/schemas/void", + "title": "void" + }) + }) + + it("Literal | null", async () => { + const schema = Schema.Literal("a", null) + await assertDraft201909(schema, { + "anyOf": [ + { + "type": "string", + "enum": ["a"] + }, + { "type": "null" } + ] + }) + }) + + it("Literal | null(with description)", async () => { + const schema = Schema.Union(Schema.Literal("a"), Schema.Null.annotations({ description: "mydescription" })) + await assertDraft201909(schema, { + "anyOf": [ + { + "type": "string", + "enum": ["a"] + }, + { + "type": "null", + "description": "mydescription" + } + ] + }) + }) + + it("Nested nullable unions", async () => { + const schema = Schema.Union(Schema.NullOr(Schema.String), Schema.Literal("a", null)) + await assertDraft201909(schema, { + "anyOf": [ + { + "anyOf": [ + { "type": "string" }, + { "type": "null" } + ] + }, + { + "anyOf": [ + { "type": "string", "enum": ["a"] }, + { "type": "null" } + ] + } + ] + }) + }) + }) + + it("parseJson handling", async () => { + const schema = Schema.parseJson(Schema.Struct({ + a: Schema.parseJson(Schema.NumberFromString) + })) + await assertDraft201909( + schema, + { + "type": "string", + "contentMediaType": "application/json", + "contentSchema": { + "type": "object", + "required": ["a"], + "properties": { + "a": { + "type": "string", + "contentMediaType": "application/json", + "contentSchema": { + "$ref": "#/$defs/NumberFromString" + } + } + }, + "additionalProperties": false + } + } + ) + }) + }) + + describe("openApi3.1", () => { + describe("nullable handling", () => { + it("Null", async () => { + const schema = Schema.Null + await assertOpenApi3_1(schema, { "type": "null" }) + }) + + it("NullOr(String)", async () => { + const schema = Schema.NullOr(Schema.String) + await assertOpenApi3_1(schema, { + "anyOf": [ + { "type": "string" }, + { "type": "null" } + ] + }) + }) + + it("NullOr(Any)", async () => { + const schema = Schema.NullOr(Schema.Any) + await assertOpenApi3_1(schema, { + "$id": "/schemas/any", + "title": "any" + }) + }) + + it("NullOr(Unknown)", async () => { + const schema = Schema.NullOr(Schema.Unknown) + await assertOpenApi3_1(schema, { + "$id": "/schemas/unknown", + "title": "unknown" + }) + }) + + it("NullOr(Void)", async () => { + const schema = Schema.NullOr(Schema.Void) + await assertOpenApi3_1(schema, { + "$id": "/schemas/void", + "title": "void" + }) + }) + + it("Literal | null", async () => { + const schema = Schema.Literal("a", null) + await assertOpenApi3_1(schema, { + "anyOf": [ + { + "type": "string", + "enum": ["a"] + }, + { "type": "null" } + ] + }) + }) + + it("Literal | null(with description)", async () => { + const schema = Schema.Union(Schema.Literal("a"), Schema.Null.annotations({ description: "mydescription" })) + await assertOpenApi3_1(schema, { + "anyOf": [ + { + "type": "string", + "enum": ["a"] + }, + { + "type": "null", + "description": "mydescription" + } + ] + }) + }) + + it("Nested nullable unions", async () => { + const schema = Schema.Union(Schema.NullOr(Schema.String), Schema.Literal("a", null)) + await assertOpenApi3_1(schema, { + "anyOf": [ + { + "anyOf": [ + { "type": "string" }, + { "type": "null" } + ] + }, + { + "anyOf": [ + { "type": "string", "enum": ["a"] }, + { "type": "null" } + ] + } + ] + }) + }) + }) + + it("parseJson handling", async () => { + const schema = Schema.parseJson(Schema.Struct({ + a: Schema.parseJson(Schema.NumberFromString) + })) + await assertOpenApi3_1( + schema, + { + "type": "string", + "contentMediaType": "application/json", + "contentSchema": { + "type": "object", + "required": ["a"], + "properties": { + "a": { + "type": "string", + "contentMediaType": "application/json", + "contentSchema": { + "$ref": "#/$defs/NumberFromString" + } + } + }, + "additionalProperties": false + } + } + ) + }) + }) +}) + +describe("jsonSchema2020-12", () => { + describe("Tuple", () => { + it("empty tuple", async () => { + const schema = Schema.Tuple() + await assertDraft2020_12(schema, { + "type": "array", + "maxItems": 0 + }) + }) + + it("element", async () => { + const schema = Schema.Tuple(Schema.Number) + await assertDraft2020_12(schema, { + "type": "array", + "prefixItems": [{ + "type": "number" + }], + "minItems": 1, + "items": false + }) + }) + + it("element + inner annotations", async () => { + await assertDraft2020_12( + Schema.Tuple(Schema.Number.annotations({ description: "inner" })), + { + "type": "array", + "prefixItems": [{ + "type": "number", + "description": "inner" + }], + "minItems": 1, + "items": false + } + ) + }) + + it("element + outer annotations should override inner annotations", async () => { + await assertDraft2020_12( + Schema.Tuple( + Schema.element(Schema.Number.annotations({ description: "inner" })).annotations({ description: "outer" }) + ), + { + "type": "array", + "prefixItems": [{ + "type": "number", + "description": "outer" + }], + "minItems": 1, + "items": false + } + ) + }) + + it("optionalElement", async () => { + const schema = Schema.Tuple(Schema.optionalElement(Schema.Number)) + await assertDraft2020_12(schema, { + "type": "array", + "minItems": 0, + "prefixItems": [ + { + "type": "number" + } + ], + "items": false + }) + }) + + it("optionalElement + inner annotations", async () => { + await assertDraft2020_12( + Schema.Tuple(Schema.optionalElement(Schema.Number).annotations({ description: "inner" })), + { + "type": "array", + "minItems": 0, + "prefixItems": [ + { + "type": "number", + "description": "inner" + } + ], + "items": false + } + ) + }) + + it("optionalElement + outer annotations should override inner annotations", async () => { + await assertDraft2020_12( + Schema.Tuple( + Schema.optionalElement(Schema.Number).annotations({ description: "inner" }).annotations({ + description: "outer" + }) + ), + { + "type": "array", + "minItems": 0, + "prefixItems": [ + { + "type": "number", + "description": "outer" + } + ], + "items": false + } + ) + }) + + it("element + optionalElement", async () => { + const schema = Schema.Tuple( + Schema.element(Schema.String.annotations({ description: "inner" })).annotations({ description: "outer" }), + Schema.optionalElement(Schema.Number.annotations({ description: "inner?" })).annotations({ + description: "outer?" + }) + ) + await assertDraft2020_12(schema, { + "type": "array", + "minItems": 1, + "prefixItems": [ + { + "type": "string", + "description": "outer" + }, + { + "type": "number", + "description": "outer?" + } + ], + "items": false + }) + }) + + it("rest", async () => { + const schema = Schema.Array(Schema.Number) + await assertDraft2020_12(schema, { + "type": "array", + "items": { + "type": "number" + } + }) + }) + + it("rest + inner annotations", async () => { + await assertDraft2020_12(Schema.Array(Schema.Number.annotations({ description: "inner" })), { + "type": "array", + "items": { + "type": "number", + "description": "inner" + } + }) + }) + + it("optionalElement + rest + inner annotations", async () => { + const schema = Schema.Tuple( + [Schema.optionalElement(Schema.String)], + Schema.element(Schema.Number.annotations({ description: "inner" })) + ) + await assertDraft2020_12(schema, { + "type": "array", + "minItems": 0, + "prefixItems": [ + { + "type": "string" + } + ], + "items": { + "type": "number", + "description": "inner" + } + }) + }) + + it("optionalElement + rest + outer annotations should override inner annotations", async () => { + await assertDraft2020_12( + Schema.Tuple( + [Schema.optionalElement(Schema.String)], + Schema.element(Schema.Number.annotations({ description: "inner" })).annotations({ description: "outer" }) + ), + { + "type": "array", + "minItems": 0, + "prefixItems": [ + { + "type": "string" + } + ], + "items": { + "type": "number", + "description": "outer" + } + } + ) + }) + + it("element + rest", async () => { + const schema = Schema.Tuple([Schema.String], Schema.Number) + await assertDraft2020_12(schema, { + "type": "array", + "prefixItems": [{ + "type": "string" + }], + "minItems": 1, + "items": { + "type": "number" + } + }) + }) + + it("NonEmptyArray", async () => { + await assertDraft2020_12( + Schema.NonEmptyArray(Schema.String), + { + type: "array", + minItems: 1, + items: { type: "string" } + } + ) + }) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/JSONSchema.test.ts b/repos/effect/packages/effect/test/Schema/JSONSchema.test.ts new file mode 100644 index 0000000..e3eaec5 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/JSONSchema.test.ts @@ -0,0 +1,4998 @@ +import { describe, it } from "@effect/vitest" +import { assertFalse, assertTrue, deepStrictEqual, throws } from "@effect/vitest/utils" +import Ajv from "ajv" +import * as A from "effect/Arbitrary" +import * as fc from "effect/FastCheck" +import * as JSONSchema from "effect/JSONSchema" +import * as Schema from "effect/Schema" +import * as AST from "effect/SchemaAST" + +type Root = JSONSchema.JsonSchema7Root + +const ajvOptions: Ajv.Options = { + strictTuples: false, + allowMatchingProperties: true +} + +const getAjvValidate = (jsonSchema: Root): Ajv.ValidateFunction => + // new instance of Ajv is created for each schema to avoid error: "schema with key or id "/schemas/any" already exists" + new Ajv.default(ajvOptions).compile(jsonSchema) + +const expectProperty = ( + schema: Schema.Schema, + jsonSchema: JSONSchema.JsonSchema7, + params?: fc.Parameters<[I]> +) => { + if (false as boolean) { + const encodedBoundSchema = Schema.encodedBoundSchema(schema) + const arb = A.make(encodedBoundSchema) + const is = Schema.is(encodedBoundSchema) + const validate = getAjvValidate(jsonSchema) + fc.assert(fc.property(arb, (i) => is(i) && validate(i)), params) + } +} + +const expectJSONSchema = ( + schema: Schema.Schema, + expectedJsonSchema: object +) => { + const jsonSchema = JSONSchema.make(schema) + deepStrictEqual(jsonSchema, { + "$schema": "http://json-schema.org/draft-07/schema#", + ...expectedJsonSchema + } as any) + return jsonSchema +} + +const expectJSONSchema2019 = ( + schema: Schema.Schema, + expectedJsonSchema: object, + expectedDefinitions: object +) => { + const definitions = {} + const jsonSchema = JSONSchema.fromAST(schema.ast, { + definitions, + target: "jsonSchema2019-09" + }) + deepStrictEqual(jsonSchema, expectedJsonSchema) + deepStrictEqual(definitions, expectedDefinitions) + return jsonSchema +} + +const expectJSONSchemaOpenApi31 = ( + schema: Schema.Schema, + expectedJsonSchema: object, + expectedDefinitions: object +) => { + const definitions = {} + const jsonSchema = JSONSchema.fromAST(schema.ast, { + definitions, + target: "openApi3.1" + }) + deepStrictEqual(jsonSchema, expectedJsonSchema) + deepStrictEqual(definitions, expectedDefinitions) + return jsonSchema +} + +const expectJSONSchemaProperty = ( + schema: Schema.Schema, + expected: object, + params?: fc.Parameters<[I]> +) => { + const jsonSchema = expectJSONSchema(schema, expected) + expectProperty(schema, jsonSchema, params) +} + +const expectJSONSchemaAnnotations = ( + schema: Schema.Schema, + expected: object, + params?: fc.Parameters<[I]> +) => { + expectJSONSchemaProperty(schema, expected, params) + const jsonSchemaAnnotations = { + description: "269d3e58-8fb2-43cb-a389-8146c353fdd5", + title: "5401c637-61f2-49b8-b74d-17f058c2670f" + } + expectJSONSchemaProperty(schema.annotations(jsonSchemaAnnotations), { ...expected, ...jsonSchemaAnnotations }, params) +} + +const expectError = (schema: Schema.Schema, message: string) => { + throws(() => JSONSchema.make(schema), new Error(message)) +} + +// Using this instead of Schema.JsonNumber to avoid cluttering the output with unnecessary description and title +const JsonNumber = Schema.Number.pipe(Schema.filter((n) => Number.isFinite(n), { jsonSchema: {} })) + +describe("fromAST", () => { + it("definitionsPath", () => { + const schema = Schema.String.annotations({ identifier: "08368672-2c02-4d6d-92b0-dd0019b33a7b" }) + const definitions = {} + const jsonSchema = JSONSchema.fromAST(schema.ast, { + definitions, + definitionPath: "#/components/schemas/" + }) + deepStrictEqual(jsonSchema, { + "$ref": "#/components/schemas/08368672-2c02-4d6d-92b0-dd0019b33a7b" + }) + deepStrictEqual(definitions, { + "08368672-2c02-4d6d-92b0-dd0019b33a7b": { + "type": "string" + } + }) + }) + + describe("target", () => { + describe("jsonSchema7", () => { + describe("nullable handling", () => { + it("Null", () => { + const schema = Schema.Null + expectJSONSchemaAnnotations(schema, { "type": "null" }) + }) + + it("NullOr(String)", () => { + const schema = Schema.NullOr(Schema.String) + expectJSONSchemaAnnotations(schema, { + "anyOf": [ + { "type": "string" }, + { "type": "null" } + ] + }) + }) + + it("NullOr(Any)", () => { + const schema = Schema.NullOr(Schema.Any) + expectJSONSchemaAnnotations(schema, { + "$id": "/schemas/any", + "title": "any" + }) + }) + + it("NullOr(Unknown)", () => { + const schema = Schema.NullOr(Schema.Unknown) + expectJSONSchemaAnnotations(schema, { + "$id": "/schemas/unknown", + "title": "unknown" + }) + }) + + it("NullOr(Void)", () => { + const schema = Schema.NullOr(Schema.Void) + expectJSONSchemaAnnotations(schema, { + "$id": "/schemas/void", + "title": "void" + }) + }) + + it("Literal | null", () => { + const schema = Schema.Literal("a", null) + expectJSONSchemaAnnotations(schema, { + "anyOf": [ + { + "type": "string", + "enum": ["a"] + }, + { "type": "null" } + ] + }) + }) + + it("Literal | null(with description)", () => { + const schema = Schema.Union(Schema.Literal("a"), Schema.Null.annotations({ description: "mydescription" })) + expectJSONSchemaAnnotations(schema, { + "anyOf": [ + { + "type": "string", + "enum": ["a"] + }, + { + "type": "null", + "description": "mydescription" + } + ] + }) + }) + + it("Nested nullable unions", () => { + const schema = Schema.Union(Schema.NullOr(Schema.String), Schema.Literal("a", null)) + expectJSONSchemaAnnotations(schema, { + "anyOf": [ + { + "anyOf": [ + { "type": "string" }, + { "type": "null" } + ] + }, + { + "anyOf": [ + { "type": "string", "enum": ["a"] }, + { "type": "null" } + ] + } + ] + }) + }) + }) + + it("parseJson handling", () => { + const schema = Schema.parseJson(Schema.Struct({ + a: Schema.parseJson(Schema.NumberFromString) + })) + const definitions = {} + const jsonSchema = JSONSchema.fromAST(schema.ast, { + definitions + }) + deepStrictEqual(jsonSchema, { + "type": "string", + "contentMediaType": "application/json" + }) + deepStrictEqual(definitions, {}) + }) + }) + + describe("jsonSchema2019-09", () => { + describe("nullable handling", () => { + it("Null", () => { + const schema = Schema.Null + expectJSONSchema2019(schema, { "type": "null" }, {}) + }) + + it("NullOr(String)", () => { + const schema = Schema.NullOr(Schema.String) + expectJSONSchema2019(schema, { + "anyOf": [ + { "type": "string" }, + { "type": "null" } + ] + }, {}) + }) + + it("NullOr(Any)", () => { + const schema = Schema.NullOr(Schema.Any) + expectJSONSchema2019(schema, { + "$id": "/schemas/any", + "title": "any" + }, {}) + }) + + it("NullOr(Unknown)", () => { + const schema = Schema.NullOr(Schema.Unknown) + expectJSONSchema2019(schema, { + "$id": "/schemas/unknown", + "title": "unknown" + }, {}) + }) + + it("NullOr(Void)", () => { + const schema = Schema.NullOr(Schema.Void) + expectJSONSchema2019(schema, { + "$id": "/schemas/void", + "title": "void" + }, {}) + }) + + it("Literal | null", () => { + const schema = Schema.Literal("a", null) + expectJSONSchema2019(schema, { + "anyOf": [ + { + "type": "string", + "enum": ["a"] + }, + { "type": "null" } + ] + }, {}) + }) + + it("Literal | null(with description)", () => { + const schema = Schema.Union(Schema.Literal("a"), Schema.Null.annotations({ description: "mydescription" })) + expectJSONSchema2019(schema, { + "anyOf": [ + { + "type": "string", + "enum": ["a"] + }, + { + "type": "null", + "description": "mydescription" + } + ] + }, {}) + }) + + it("Nested nullable unions", () => { + const schema = Schema.Union(Schema.NullOr(Schema.String), Schema.Literal("a", null)) + expectJSONSchema2019(schema, { + "anyOf": [ + { + "anyOf": [ + { "type": "string" }, + { "type": "null" } + ] + }, + { + "anyOf": [ + { "type": "string", "enum": ["a"] }, + { "type": "null" } + ] + } + ] + }, {}) + }) + }) + + it("parseJson handling", () => { + const schema = Schema.parseJson(Schema.Struct({ + a: Schema.parseJson(Schema.NumberFromString) + })) + expectJSONSchema2019(schema, { + "type": "string", + "contentMediaType": "application/json", + "contentSchema": { + "type": "object", + "required": ["a"], + "properties": { + "a": { + "type": "string", + "contentMediaType": "application/json", + "contentSchema": { + "$ref": "#/$defs/NumberFromString" + } + } + }, + "additionalProperties": false + } + }, { + "NumberFromString": { + "description": "a string to be decoded into a number", + "type": "string" + } + }) + }) + }) + + describe("openApi3.1", () => { + describe("nullable handling", () => { + it("Null", () => { + const schema = Schema.Null + expectJSONSchemaOpenApi31(schema, { "type": "null" }, {}) + }) + + it("NullOr(String)", () => { + const schema = Schema.NullOr(Schema.String) + expectJSONSchemaOpenApi31(schema, { + "anyOf": [ + { "type": "string" }, + { "type": "null" } + ] + }, {}) + }) + + it("NullOr(Any)", () => { + const schema = Schema.NullOr(Schema.Any) + expectJSONSchemaOpenApi31(schema, { + "$id": "/schemas/any", + "title": "any" + }, {}) + }) + + it("NullOr(Unknown)", () => { + const schema = Schema.NullOr(Schema.Unknown) + expectJSONSchemaOpenApi31(schema, { + "$id": "/schemas/unknown", + "title": "unknown" + }, {}) + }) + + it("NullOr(Void)", () => { + const schema = Schema.NullOr(Schema.Void) + expectJSONSchemaOpenApi31(schema, { + "$id": "/schemas/void", + "title": "void" + }, {}) + }) + + it("NullOr(Object)", () => { + const schema = Schema.NullOr(Schema.Object) + expectJSONSchemaOpenApi31(schema, { + "anyOf": [ + { + "$id": "/schemas/object", + "anyOf": [ + { "type": "object" }, + { "type": "array" } + ], + "description": "an object in the TypeScript meaning, i.e. the `object` type", + "title": "object" + }, + { "type": "null" } + ] + }, {}) + }) + + it("NullOr(Struct({}))", () => { + const schema = Schema.NullOr(Schema.Struct({})) + expectJSONSchemaOpenApi31(schema, { + "anyOf": [ + { + "$id": "/schemas/%7B%7D", + "anyOf": [ + { "type": "object" }, + { "type": "array" } + ] + }, + { "type": "null" } + ] + }, {}) + }) + + it("NullOr(Ref)", () => { + const schema = Schema.NullOr( + Schema.String.annotations({ identifier: "b812aaa1-cfe1-4dda-8c9c-360bfa6cb855" }) + ) + expectJSONSchemaOpenApi31(schema, { + "anyOf": [ + { + "$ref": "#/$defs/b812aaa1-cfe1-4dda-8c9c-360bfa6cb855" + }, + { "type": "null" } + ] + }, { + "b812aaa1-cfe1-4dda-8c9c-360bfa6cb855": { + "type": "string" + } + }) + }) + + it("NullOr(Number)", () => { + const schema = Schema.NullOr(Schema.Number) + expectJSONSchemaOpenApi31(schema, { + "anyOf": [ + { "type": "number" }, + { "type": "null" } + ] + }, {}) + }) + + it("NullOr(Int)", () => { + const schema = Schema.NullOr(Schema.Int) + expectJSONSchemaOpenApi31(schema, { + "anyOf": [ + { + "$ref": "#/$defs/Int" + }, + { "type": "null" } + ] + }, { + "Int": { + "title": "int", + "description": "an integer", + "type": "integer" + } + }) + }) + + it("NullOr(Boolean)", () => { + const schema = Schema.NullOr(Schema.Boolean) + expectJSONSchemaOpenApi31(schema, { + "anyOf": [ + { "type": "boolean" }, + { "type": "null" } + ] + }, {}) + }) + + it("NullOr(Array)", () => { + const schema = Schema.NullOr(Schema.Array(Schema.String)) + expectJSONSchemaOpenApi31(schema, { + "anyOf": [ + { + "items": { "type": "string" }, + "type": "array" + }, + { "type": "null" } + ] + }, {}) + }) + + it("NullOr(Enum)", () => { + enum Fruits { + Apple, + Banana + } + const schema = Schema.NullOr(Schema.Enums(Fruits)) + expectJSONSchemaOpenApi31(schema, { + "anyOf": [ + { + "$comment": "/schemas/enums", + "anyOf": [ + { + "type": "number", + "title": "Apple", + "enum": [0] + }, + { + "type": "number", + "title": "Banana", + "enum": [1] + } + ] + }, + { "type": "null" } + ] + }, {}) + }) + + it("NullOr(Literal)", () => { + const schema = Schema.NullOr(Schema.Literal("a")) + expectJSONSchemaOpenApi31(schema, { + "anyOf": [ + { + "type": "string", + "enum": ["a"] + }, + { "type": "null" } + ] + }, {}) + }) + + it("Literal | null", () => { + const schema = Schema.Literal("a", null) + expectJSONSchemaOpenApi31(schema, { + "anyOf": [ + { + "type": "string", + "enum": ["a"] + }, + { "type": "null" } + ] + }, {}) + }) + + it("Literal | null(with description)", () => { + const schema = Schema.Union(Schema.Literal("a"), Schema.Null.annotations({ description: "mydescription" })) + expectJSONSchemaOpenApi31(schema, { + "anyOf": [ + { + "type": "string", + "enum": ["a"] + }, + { + "description": "mydescription", + "type": "null" + } + ] + }, {}) + }) + + it("Nested nullable unions", () => { + const schema = Schema.Union(Schema.NullOr(Schema.String), Schema.Literal("a", null)) + expectJSONSchemaOpenApi31(schema, { + "anyOf": [ + { + "anyOf": [ + { "type": "string" }, + { "type": "null" } + ] + }, + { + "anyOf": [ + { "type": "string", "enum": ["a"] }, + { "type": "null" } + ] + } + ] + }, {}) + }) + + it("NullOr(Struct({ a: String }))", () => { + const schema = Schema.NullOr(Schema.Struct({ a: Schema.String })) + expectJSONSchemaOpenApi31(schema, { + "anyOf": [ + { + "additionalProperties": false, + "properties": { "a": { "type": "string" } }, + "required": ["a"], + "type": "object" + }, + { "type": "null" } + ] + }, {}) + }) + }) + + it("parseJson handling", () => { + const schema = Schema.parseJson(Schema.Struct({ + a: Schema.parseJson(Schema.NumberFromString) + })) + expectJSONSchemaOpenApi31(schema, { + "type": "string", + "contentMediaType": "application/json", + "contentSchema": { + "type": "object", + "required": ["a"], + "properties": { + "a": { + "type": "string", + "contentMediaType": "application/json", + "contentSchema": { + "$ref": "#/$defs/NumberFromString" + } + } + }, + "additionalProperties": false + } + }, { + "NumberFromString": { + "description": "a string to be decoded into a number", + "type": "string" + } + }) + }) + }) + }) + + describe("topLevelReferenceStrategy", () => { + it(`"skip"`, () => { + const schema = Schema.String.annotations({ identifier: "1b205579-f159-48d4-a218-f09426bca040" }) + const definitions = {} + const jsonSchema = JSONSchema.fromAST(schema.ast, { + definitions, + topLevelReferenceStrategy: "skip" + }) + deepStrictEqual(jsonSchema, { + "type": "string" + }) + deepStrictEqual(definitions, {}) + }) + }) + + describe("additionalPropertiesStrategy", () => { + it(`"allow"`, () => { + const schema = Schema.Struct({ a: Schema.String }) + const definitions = {} + const jsonSchema = JSONSchema.fromAST(schema.ast, { + definitions, + additionalPropertiesStrategy: "allow" + }) + deepStrictEqual(jsonSchema, { + "type": "object", + "properties": { + "a": { + "type": "string" + } + }, + "required": ["a"], + "additionalProperties": true + }) + deepStrictEqual(definitions, {}) + }) + }) +}) + +describe("make", () => { + it("should filter out non-JSON values and cyclic references from default and examples", () => { + const cyclic: any = { value: "test" } + cyclic.self = cyclic + const schema = Schema.String.annotations({ default: 1n as any, examples: ["a", 1n as any, cyclic, "b"] }) + expectJSONSchemaAnnotations(schema, { + "type": "string", + "examples": ["a", "b"] + }) + }) + + it("handling of a top level `parseJson` should targeting the \"to\" side", () => { + const schema = Schema.parseJson(Schema.Struct({ + a: Schema.parseJson(Schema.NumberFromString) + })) + expectJSONSchema( + schema, + { + "type": "object", + "required": ["a"], + "properties": { + "a": { + "type": "string", + "contentMediaType": "application/json" + } + }, + "additionalProperties": false + } + ) + }) + + describe("Unsupported schemas", () => { + describe("Missing jsonSchema annotation Error", () => { + it("Declaration", () => { + expectError( + Schema.ChunkFromSelf(JsonNumber), + `Missing annotation +details: Generating a JSON Schema for this schema requires a "jsonSchema" annotation +schema (Declaration): Chunk<{ number | filter }>` + ) + }) + + it("BigIntFromSelf", () => { + expectError( + Schema.BigIntFromSelf, + `Missing annotation +details: Generating a JSON Schema for this schema requires a "jsonSchema" annotation +schema (BigIntKeyword): bigint` + ) + }) + + it("SymbolFromSelf", () => { + expectError( + Schema.SymbolFromSelf, + `Missing annotation +details: Generating a JSON Schema for this schema requires a "jsonSchema" annotation +schema (SymbolKeyword): symbol` + ) + }) + + it("UniqueSymbolFromSelf", () => { + expectError( + Schema.UniqueSymbolFromSelf(Symbol.for("effect/Schema/test/a")), + `Missing annotation +details: Generating a JSON Schema for this schema requires a "jsonSchema" annotation +schema (UniqueSymbol): Symbol(effect/Schema/test/a)` + ) + }) + + it("Undefined", () => { + expectError( + Schema.Undefined, + `Missing annotation +details: Generating a JSON Schema for this schema requires a "jsonSchema" annotation +schema (UndefinedKeyword): undefined` + ) + }) + + it("Schema.Literal with a bigint literal", () => { + expectError( + Schema.Literal(1n), + `Missing annotation +details: Generating a JSON Schema for this schema requires a "jsonSchema" annotation +schema (Literal): 1n` + ) + }) + + it("Tuple with an unsupported component", () => { + expectError( + Schema.Tuple(Schema.Undefined), + `Missing annotation +at path: [0] +details: Generating a JSON Schema for this schema requires a "jsonSchema" annotation +schema (UndefinedKeyword): undefined` + ) + }) + + it("Struct with an unsupported field", () => { + expectError( + Schema.Struct({ a: Schema.SymbolFromSelf }), + `Missing annotation +at path: ["a"] +details: Generating a JSON Schema for this schema requires a "jsonSchema" annotation +schema (SymbolKeyword): symbol` + ) + }) + }) + + describe("Missing identifier annotation Error", () => { + it("Suspend", () => { + interface A { + readonly a: string + readonly as: ReadonlyArray + } + const schema = Schema.Struct({ + a: Schema.String, + as: Schema.Array(Schema.suspend((): Schema.Schema => schema)) + }) + expectError( + schema, + `Missing annotation +at path: ["as"] +details: Generating a JSON Schema for this schema requires an "identifier" annotation +schema (Suspend): ` + ) + }) + }) + + describe("Unsupported index signature parameter", () => { + it("Record(SymbolFromSelf, number)", () => { + expectError( + Schema.Record({ key: Schema.SymbolFromSelf, value: JsonNumber }), + `Missing annotation +at path: ["[symbol]"] +details: Generating a JSON Schema for this schema requires a "jsonSchema" annotation +schema (SymbolKeyword): symbol` + ) + }) + }) + + describe("Unsupported key", () => { + it("should raise an error if there is a property named with a symbol", () => { + const a = Symbol.for("effect/Schema/test/a") + expectError( + Schema.Struct({ [a]: Schema.String }), + `Unsupported key +details: Cannot encode Symbol(effect/Schema/test/a) key to JSON Schema` + ) + }) + }) + + describe("Unsupported post-rest elements", () => { + it("r e should raise an error", () => { + expectError( + Schema.Tuple([], JsonNumber, Schema.String), + "Generating a JSON Schema for post-rest elements is not currently supported. You're welcome to contribute by submitting a Pull Request" + ) + }) + }) + }) + + it("Never", () => { + const jsonSchema: Root = { + "$id": "/schemas/never", + "not": {}, + "title": "never" + } + expectJSONSchema(Schema.Never, jsonSchema) + const validate = getAjvValidate(jsonSchema) + assertFalse(validate(null)) + }) + + it("Any", () => { + expectJSONSchemaAnnotations(Schema.Any, { + "$id": "/schemas/any", + "title": "any" + }) + }) + + it("Unknown", () => { + expectJSONSchemaAnnotations(Schema.Unknown, { + "$id": "/schemas/unknown", + "title": "unknown" + }) + }) + + it("Object", () => { + const jsonSchema: Root = { + "$id": "/schemas/object", + "anyOf": [ + { "type": "object" }, + { "type": "array" } + ], + "description": "an object in the TypeScript meaning, i.e. the `object` type", + "title": "object" + } + expectJSONSchemaAnnotations(Schema.Object, jsonSchema) + + const validate = getAjvValidate(jsonSchema) + assertTrue(validate({})) + assertTrue(validate({ a: 1 })) + assertTrue(validate([])) + assertFalse(validate("a")) + assertFalse(validate(1)) + assertFalse(validate(true)) + }) + + it("empty struct: Schema.Struct({})", () => { + const schema = Schema.Struct({}) + const jsonSchema: Root = { + "$id": "/schemas/%7B%7D", + "anyOf": [{ + "type": "object" + }, { + "type": "array" + }] + } + expectJSONSchemaAnnotations(schema, jsonSchema) + const validate = getAjvValidate(jsonSchema) + assertTrue(validate({})) + assertTrue(validate({ a: 1 })) + assertTrue(validate([])) + assertFalse(validate(null)) + assertFalse(validate(1)) + assertFalse(validate(true)) + }) + + it("Void", () => { + expectJSONSchemaAnnotations(Schema.Void, { + "$id": "/schemas/void", + "title": "void" + }) + }) + + it("String", () => { + expectJSONSchemaAnnotations(Schema.String, { + "type": "string" + }) + }) + + it("Number", () => { + expectJSONSchema(Schema.Number, { + "type": "number" + }) + }) + + it("JsonNumber", () => { + expectJSONSchemaProperty(Schema.JsonNumber, { + "$defs": { + "JsonNumber": { + "type": "number", + "title": "finite", + "description": "a finite number" + } + }, + "$ref": "#/$defs/JsonNumber" + }) + }) + + it("Boolean", () => { + expectJSONSchemaAnnotations(Schema.Boolean, { + "type": "boolean" + }) + }) + + it("TemplateLiteral", () => { + const schema = Schema.TemplateLiteral(Schema.Literal("a"), Schema.Number) + const jsonSchema: Root = { + "type": "string", + "pattern": "^a[+-]?\\d*\\.?\\d+(?:[Ee][+-]?\\d+)?$", + "title": "`a${number}`", + "description": "a template literal" + } + expectJSONSchemaAnnotations(schema, jsonSchema) + const validate = getAjvValidate(jsonSchema) + assertTrue(validate("a1")) + assertTrue(validate("a12")) + assertFalse(validate("a")) + assertFalse(validate("aa")) + }) + + describe("Literal", () => { + it("null literal", () => { + expectJSONSchemaAnnotations(Schema.Null, { + "type": "null" + }) + expectJSONSchemaProperty(Schema.Null.annotations({ identifier: "9b7d3b2b-3b3a-4741-8c4c-9cae776c47f6" }), { + "$defs": { + "9b7d3b2b-3b3a-4741-8c4c-9cae776c47f6": { + "type": "null" + } + }, + "$ref": "#/$defs/9b7d3b2b-3b3a-4741-8c4c-9cae776c47f6" + }) + }) + + it("string literals", () => { + expectJSONSchemaAnnotations(Schema.Literal("a"), { + "type": "string", + "enum": ["a"] + }) + expectJSONSchemaAnnotations(Schema.Literal("a", "b"), { + "type": "string", + "enum": ["a", "b"] + }) + }) + + it("number literals", () => { + expectJSONSchemaAnnotations(Schema.Literal(1), { + "type": "number", + "enum": [1] + }) + expectJSONSchemaAnnotations(Schema.Literal(1, 2), { + "type": "number", + "enum": [1, 2] + }) + }) + + it("boolean literals", () => { + expectJSONSchemaAnnotations(Schema.Literal(true), { + "type": "boolean", + "enum": [true] + }) + expectJSONSchemaAnnotations(Schema.Literal(false), { + "type": "boolean", + "enum": [false] + }) + expectJSONSchemaAnnotations(Schema.Literal(true, false), { + "type": "boolean", + "enum": [true, false] + }) + }) + + it("union of literals", () => { + expectJSONSchemaAnnotations(Schema.Literal(1, true), { + "anyOf": [ + { "type": "number", "enum": [1] }, + { "type": "boolean", "enum": [true] } + ] + }) + }) + }) + + describe("Enums", () => { + it("empty enum", () => { + enum Empty {} + const jsonSchema = expectJSONSchema(Schema.Enums(Empty), { + "$id": "/schemas/never", + "not": {} + }) + const validate = getAjvValidate(jsonSchema) + assertFalse(validate(1)) + }) + + it("single enum", () => { + enum Fruits { + Apple + } + expectJSONSchemaAnnotations(Schema.Enums(Fruits), { + "$comment": "/schemas/enums", + "anyOf": [ + { + "type": "number", + "title": "Apple", + "enum": [0] + } + ] + }) + }) + + it("numeric enums", () => { + enum Fruits { + Apple, + Banana + } + expectJSONSchemaAnnotations(Schema.Enums(Fruits), { + "$comment": "/schemas/enums", + "anyOf": [ + { + "type": "number", + "title": "Apple", + "enum": [0] + }, + { + "type": "number", + "title": "Banana", + "enum": [1] + } + ] + }) + }) + + it("string enums", () => { + enum Fruits { + Apple = "apple", + Banana = "banana" + } + expectJSONSchemaAnnotations(Schema.Enums(Fruits), { + "$comment": "/schemas/enums", + "anyOf": [ + { + "type": "string", + "title": "Apple", + "enum": ["apple"] + }, + { + "type": "string", + "title": "Banana", + "enum": ["banana"] + } + ] + }) + }) + + it("mix of string/number enums", () => { + enum Fruits { + Apple = "apple", + Banana = "banana", + Cantaloupe = 0 + } + expectJSONSchemaAnnotations(Schema.Enums(Fruits), { + "$comment": "/schemas/enums", + "anyOf": [ + { + "type": "string", + "title": "Apple", + "enum": ["apple"] + }, + { + "type": "string", + "title": "Banana", + "enum": ["banana"] + }, + { + "type": "number", + "title": "Cantaloupe", + "enum": [0] + } + ] + }) + }) + + it("const enums", () => { + const Fruits = { + Apple: "apple", + Banana: "banana", + Cantaloupe: 3 + } as const + expectJSONSchemaAnnotations(Schema.Enums(Fruits), { + "$comment": "/schemas/enums", + "anyOf": [ + { + "type": "string", + "title": "Apple", + "enum": ["apple"] + }, + { + "type": "string", + "title": "Banana", + "enum": ["banana"] + }, + { + "type": "number", + "title": "Cantaloupe", + "enum": [3] + } + ] + }) + }) + }) + + describe("Refinement", () => { + it("itemsCount (Array)", () => { + expectJSONSchemaAnnotations(Schema.Array(Schema.String).pipe(Schema.itemsCount(2)), { + "type": "array", + "items": { + "type": "string" + }, + "description": "an array of exactly 2 item(s)", + "title": "itemsCount(2)", + "minItems": 2, + "maxItems": 2 + }) + }) + + it("itemsCount (NonEmptyArray)", () => { + expectJSONSchemaAnnotations(Schema.NonEmptyArray(Schema.String).pipe(Schema.itemsCount(2)), { + "type": "array", + "items": { + "type": "string" + }, + "description": "an array of exactly 2 item(s)", + "title": "itemsCount(2)", + "minItems": 2, + "maxItems": 2 + }) + }) + + it("minItems (Array)", () => { + expectJSONSchemaAnnotations(Schema.Array(Schema.String).pipe(Schema.minItems(2)), { + "type": "array", + "items": { + "type": "string" + }, + "description": "an array of at least 2 item(s)", + "title": "minItems(2)", + "minItems": 2 + }) + }) + + it("minItems (NonEmptyArray)", () => { + expectJSONSchemaAnnotations(Schema.NonEmptyArray(Schema.String).pipe(Schema.minItems(2)), { + "type": "array", + "items": { + "type": "string" + }, + "description": "an array of at least 2 item(s)", + "title": "minItems(2)", + "minItems": 2 + }) + }) + + it("maxItems (Array)", () => { + expectJSONSchemaAnnotations(Schema.Array(Schema.String).pipe(Schema.maxItems(2)), { + "type": "array", + "items": { + "type": "string" + }, + "description": "an array of at most 2 item(s)", + "title": "maxItems(2)", + "maxItems": 2 + }) + }) + + it("maxItems (NonEmptyArray)", () => { + expectJSONSchemaAnnotations(Schema.NonEmptyArray(Schema.String).pipe(Schema.maxItems(2)), { + "type": "array", + "items": { + "type": "string" + }, + "description": "an array of at most 2 item(s)", + "title": "maxItems(2)", + "minItems": 1, + "maxItems": 2 + }) + }) + + it("minLength", () => { + expectJSONSchemaAnnotations(Schema.String.pipe(Schema.minLength(1)), { + "type": "string", + "title": "minLength(1)", + "description": "a string at least 1 character(s) long", + "minLength": 1 + }) + }) + + it("maxLength", () => { + expectJSONSchemaAnnotations(Schema.String.pipe(Schema.maxLength(1)), { + "type": "string", + "title": "maxLength(1)", + "description": "a string at most 1 character(s) long", + "maxLength": 1 + }) + }) + + it("length: number", () => { + expectJSONSchemaAnnotations(Schema.String.pipe(Schema.length(1)), { + "type": "string", + "title": "length(1)", + "description": "a single character", + "maxLength": 1, + "minLength": 1 + }) + }) + + it("length: { min, max }", () => { + expectJSONSchemaAnnotations(Schema.String.pipe(Schema.length({ min: 2, max: 4 })), { + "type": "string", + "title": "length({ min: 2, max: 4)", + "description": "a string at least 2 character(s) and at most 4 character(s) long", + "maxLength": 4, + "minLength": 2 + }) + }) + + it("greaterThan", () => { + expectJSONSchemaAnnotations(JsonNumber.pipe(Schema.greaterThan(1)), { + "type": "number", + "title": "greaterThan(1)", + "description": "a number greater than 1", + "exclusiveMinimum": 1 + }) + }) + + it("greaterThanOrEqualTo", () => { + expectJSONSchemaAnnotations(JsonNumber.pipe(Schema.greaterThanOrEqualTo(1)), { + "type": "number", + "title": "greaterThanOrEqualTo(1)", + "description": "a number greater than or equal to 1", + "minimum": 1 + }) + }) + + it("lessThan", () => { + expectJSONSchemaAnnotations(JsonNumber.pipe(Schema.lessThan(1)), { + "type": "number", + "title": "lessThan(1)", + "description": "a number less than 1", + "exclusiveMaximum": 1 + }) + }) + + it("lessThanOrEqualTo", () => { + expectJSONSchemaAnnotations(JsonNumber.pipe(Schema.lessThanOrEqualTo(1)), { + "type": "number", + "title": "lessThanOrEqualTo(1)", + "description": "a number less than or equal to 1", + "maximum": 1 + }) + }) + + it("pattern", () => { + expectJSONSchemaAnnotations(Schema.String.pipe(Schema.pattern(/^abb+$/)), { + "type": "string", + "description": "a string matching the pattern ^abb+$", + "pattern": "^abb+$" + }) + }) + + it("int", () => { + expectJSONSchemaAnnotations(JsonNumber.pipe(Schema.int()), { + "type": "integer", + "title": "int", + "description": "an integer" + }) + }) + + it("Trimmed", () => { + const schema = Schema.Trimmed + expectJSONSchemaProperty(schema, { + "$defs": { + "Trimmed": { + "title": "trimmed", + "description": "a string with no leading or trailing whitespace", + "pattern": "^\\S[\\s\\S]*\\S$|^\\S$|^$", + "type": "string" + } + }, + "$ref": "#/$defs/Trimmed" + }) + }) + + it("Lowercased", () => { + const schema = Schema.Lowercased + expectJSONSchemaProperty(schema, { + "$defs": { + "Lowercased": { + "title": "lowercased", + "description": "a lowercase string", + "pattern": "^[^A-Z]*$", + "type": "string" + } + }, + "$ref": "#/$defs/Lowercased" + }) + }) + + it("Uppercased", () => { + const schema = Schema.Uppercased + expectJSONSchemaProperty(schema, { + "$defs": { + "Uppercased": { + "title": "uppercased", + "description": "an uppercase string", + "pattern": "^[^a-z]*$", + "type": "string" + } + }, + "$ref": "#/$defs/Uppercased" + }) + }) + + it("Capitalized", () => { + const schema = Schema.Capitalized + expectJSONSchemaProperty(schema, { + "$defs": { + "Capitalized": { + "title": "capitalized", + "description": "a capitalized string", + "pattern": "^[^a-z]?.*$", + "type": "string" + } + }, + "$ref": "#/$defs/Capitalized" + }) + }) + + it("Uncapitalized", () => { + const schema = Schema.Uncapitalized + expectJSONSchemaProperty(schema, { + "$defs": { + "Uncapitalized": { + "title": "uncapitalized", + "description": "a uncapitalized string", + "pattern": "^[^A-Z]?.*$", + "type": "string" + } + }, + "$ref": "#/$defs/Uncapitalized" + }) + }) + + describe("should handle merge conflicts", () => { + it("minLength + minLength", () => { + expectJSONSchemaProperty(Schema.String.pipe(Schema.minLength(1), Schema.minLength(2)), { + "type": "string", + "title": "minLength(2)", + "description": "a string at least 2 character(s) long", + "minLength": 2 + }) + expectJSONSchemaProperty(Schema.String.pipe(Schema.minLength(2), Schema.minLength(1)), { + "type": "string", + "title": "minLength(1)", + "description": "a string at least 1 character(s) long", + "minLength": 1, + "allOf": [ + { "minLength": 2 } + ] + }) + expectJSONSchemaProperty(Schema.String.pipe(Schema.minLength(2), Schema.minLength(1), Schema.minLength(2)), { + "type": "string", + "title": "minLength(2)", + "description": "a string at least 2 character(s) long", + "minLength": 2 + }) + }) + + it("maxLength + maxLength", () => { + expectJSONSchemaProperty(Schema.String.pipe(Schema.maxLength(1), Schema.maxLength(2)), { + "type": "string", + "title": "maxLength(2)", + "description": "a string at most 2 character(s) long", + "maxLength": 2, + "allOf": [ + { "maxLength": 1 } + ] + }) + expectJSONSchemaProperty(Schema.String.pipe(Schema.maxLength(2), Schema.maxLength(1)), { + "type": "string", + "title": "maxLength(1)", + "description": "a string at most 1 character(s) long", + "maxLength": 1 + }) + expectJSONSchemaProperty(Schema.String.pipe(Schema.maxLength(1), Schema.maxLength(2), Schema.maxLength(1)), { + "type": "string", + "title": "maxLength(1)", + "description": "a string at most 1 character(s) long", + "maxLength": 1 + }) + }) + + it("pattern + pattern", () => { + expectJSONSchemaProperty(Schema.String.pipe(Schema.startsWith("a"), Schema.endsWith("c")), { + "type": "string", + "title": "endsWith(\"c\")", + "description": "a string ending with \"c\"", + "pattern": "^.*c$", + "allOf": [ + { "pattern": "^a" } + ] + }) + expectJSONSchemaProperty( + Schema.String.pipe(Schema.startsWith("a"), Schema.endsWith("c"), Schema.startsWith("a")), + { + "type": "string", + "title": "startsWith(\"a\")", + "description": "a string starting with \"a\"", + "pattern": "^a", + "allOf": [ + { "pattern": "^.*c$" } + ] + } + ) + expectJSONSchemaProperty( + Schema.String.pipe(Schema.endsWith("c"), Schema.startsWith("a"), Schema.endsWith("c")), + { + "type": "string", + "title": "endsWith(\"c\")", + "description": "a string ending with \"c\"", + "pattern": "^.*c$", + "allOf": [ + { "pattern": "^a" } + ] + } + ) + }) + + it("minItems + minItems", () => { + expectJSONSchemaProperty(Schema.Array(Schema.String).pipe(Schema.minItems(1), Schema.minItems(2)), { + "type": "array", + "items": { + "type": "string" + }, + "description": "an array of at least 2 item(s)", + "title": "minItems(2)", + "minItems": 2 + }) + expectJSONSchemaProperty(Schema.Array(Schema.String).pipe(Schema.minItems(2), Schema.minItems(1)), { + "type": "array", + "items": { + "type": "string" + }, + "title": "minItems(1)", + "description": "an array of at least 1 item(s)", + "minItems": 1, + "allOf": [ + { "minItems": 2 } + ] + }) + expectJSONSchemaProperty( + Schema.Array(Schema.String).pipe(Schema.minItems(2), Schema.minItems(1), Schema.minItems(2)), + { + "type": "array", + "items": { + "type": "string" + }, + "description": "an array of at least 2 item(s)", + "title": "minItems(2)", + "minItems": 2 + } + ) + }) + + it("maxItems + maxItems", () => { + expectJSONSchemaProperty(Schema.Array(Schema.String).pipe(Schema.maxItems(1), Schema.maxItems(2)), { + "type": "array", + "items": { + "type": "string" + }, + "title": "maxItems(2)", + "description": "an array of at most 2 item(s)", + "maxItems": 2, + "allOf": [ + { "maxItems": 1 } + ] + }) + expectJSONSchemaProperty(Schema.Array(Schema.String).pipe(Schema.maxItems(2), Schema.maxItems(1)), { + "type": "array", + "items": { + "type": "string" + }, + "title": "maxItems(1)", + "description": "an array of at most 1 item(s)", + "maxItems": 1 + }) + expectJSONSchemaProperty( + Schema.Array(Schema.String).pipe(Schema.maxItems(1), Schema.maxItems(2), Schema.maxItems(1)), + { + "type": "array", + "items": { + "type": "string" + }, + "title": "maxItems(1)", + "description": "an array of at most 1 item(s)", + "maxItems": 1 + } + ) + }) + + it("minimum + minimum", () => { + expectJSONSchemaProperty(JsonNumber.pipe(Schema.greaterThanOrEqualTo(1), Schema.greaterThanOrEqualTo(2)), { + "type": "number", + "title": "greaterThanOrEqualTo(2)", + "description": "a number greater than or equal to 2", + "minimum": 2 + }) + expectJSONSchemaProperty(JsonNumber.pipe(Schema.greaterThanOrEqualTo(2), Schema.greaterThanOrEqualTo(1)), { + "type": "number", + "minimum": 1, + "title": "greaterThanOrEqualTo(1)", + "description": "a number greater than or equal to 1", + "allOf": [ + { "minimum": 2 } + ] + }) + expectJSONSchemaProperty( + JsonNumber.pipe( + Schema.greaterThanOrEqualTo(2), + Schema.greaterThanOrEqualTo(1), + Schema.greaterThanOrEqualTo(2) + ), + { + "type": "number", + "title": "greaterThanOrEqualTo(2)", + "description": "a number greater than or equal to 2", + "minimum": 2 + } + ) + }) + + it("maximum + maximum", () => { + expectJSONSchemaProperty(JsonNumber.pipe(Schema.lessThanOrEqualTo(1), Schema.lessThanOrEqualTo(2)), { + "type": "number", + "title": "lessThanOrEqualTo(2)", + "description": "a number less than or equal to 2", + "maximum": 2, + "allOf": [ + { "maximum": 1 } + ] + }) + expectJSONSchemaProperty(JsonNumber.pipe(Schema.lessThanOrEqualTo(2), Schema.lessThanOrEqualTo(1)), { + "type": "number", + "title": "lessThanOrEqualTo(1)", + "description": "a number less than or equal to 1", + "maximum": 1 + }) + expectJSONSchemaProperty( + JsonNumber.pipe(Schema.lessThanOrEqualTo(1), Schema.lessThanOrEqualTo(2), Schema.lessThanOrEqualTo(1)), + { + "type": "number", + "title": "lessThanOrEqualTo(1)", + "description": "a number less than or equal to 1", + "maximum": 1 + } + ) + }) + + it("exclusiveMinimum + exclusiveMinimum", () => { + expectJSONSchemaProperty(JsonNumber.pipe(Schema.greaterThan(1), Schema.greaterThan(2)), { + "type": "number", + "title": "greaterThan(2)", + "description": "a number greater than 2", + "exclusiveMinimum": 2 + }) + expectJSONSchemaProperty(JsonNumber.pipe(Schema.greaterThan(2), Schema.greaterThan(1)), { + "type": "number", + "exclusiveMinimum": 1, + "title": "greaterThan(1)", + "description": "a number greater than 1", + "allOf": [ + { "exclusiveMinimum": 2 } + ] + }) + expectJSONSchemaProperty( + JsonNumber.pipe( + Schema.greaterThan(2), + Schema.greaterThan(1), + Schema.greaterThan(2) + ), + { + "type": "number", + "title": "greaterThan(2)", + "description": "a number greater than 2", + "exclusiveMinimum": 2 + } + ) + }) + + it("exclusiveMaximum + exclusiveMaximum", () => { + expectJSONSchemaProperty(JsonNumber.pipe(Schema.lessThan(1), Schema.lessThan(2)), { + "type": "number", + "title": "lessThan(2)", + "description": "a number less than 2", + "exclusiveMaximum": 2, + "allOf": [ + { "exclusiveMaximum": 1 } + ] + }) + expectJSONSchemaProperty(JsonNumber.pipe(Schema.lessThan(2), Schema.lessThan(1)), { + "type": "number", + "title": "lessThan(1)", + "description": "a number less than 1", + "exclusiveMaximum": 1 + }) + expectJSONSchemaProperty( + JsonNumber.pipe(Schema.lessThan(1), Schema.lessThan(2), Schema.lessThan(1)), + { + "type": "number", + "title": "lessThan(1)", + "description": "a number less than 1", + "exclusiveMaximum": 1 + } + ) + }) + + it("multipleOf + multipleOf", () => { + expectJSONSchema(JsonNumber.pipe(Schema.multipleOf(2), Schema.multipleOf(3)), { + "type": "number", + "title": "multipleOf(3)", + "description": "a number divisible by 3", + "multipleOf": 3, + "allOf": [ + { "multipleOf": 2 } + ] + }) + expectJSONSchema( + JsonNumber.pipe(Schema.multipleOf(2), Schema.multipleOf(3), Schema.multipleOf(3)), + { + "type": "number", + "title": "multipleOf(3)", + "description": "a number divisible by 3", + "multipleOf": 3, + "allOf": [ + { "multipleOf": 2 } + ] + } + ) + expectJSONSchema( + JsonNumber.pipe(Schema.multipleOf(3), Schema.multipleOf(2), Schema.multipleOf(3)), + { + "type": "number", + "title": "multipleOf(3)", + "description": "a number divisible by 3", + "multipleOf": 3, + "allOf": [ + { "multipleOf": 2 } + ] + } + ) + }) + }) + }) + + describe("Tuple", () => { + it("empty tuple", () => { + const schema = Schema.Tuple() + const jsonSchema: Root = { + "type": "array", + "maxItems": 0 + } + expectJSONSchemaAnnotations(schema, jsonSchema) + const validate = getAjvValidate(jsonSchema) + assertTrue(validate([])) + assertFalse(validate([1])) + }) + + it("element", () => { + const schema = Schema.Tuple(JsonNumber) + const jsonSchema: Root = { + "type": "array", + "items": [{ + "type": "number" + }], + "minItems": 1, + "additionalItems": false + } + expectJSONSchemaAnnotations(schema, jsonSchema) + const validate = getAjvValidate(jsonSchema) + assertTrue(validate([1])) + assertFalse(validate([])) + assertFalse(validate(["a"])) + assertFalse(validate([1, "a"])) + }) + + it("element + inner annotations", () => { + expectJSONSchemaAnnotations( + Schema.Tuple(JsonNumber.annotations({ description: "inner" })), + { + "type": "array", + "items": [{ + "type": "number", + "description": "inner" + }], + "minItems": 1, + "additionalItems": false + } + ) + }) + + it("element + outer annotations should override inner annotations", () => { + expectJSONSchemaAnnotations( + Schema.Tuple( + Schema.element(JsonNumber.annotations({ description: "inner" })).annotations({ description: "outer" }) + ), + { + "type": "array", + "items": [{ + "type": "number", + "description": "outer" + }], + "minItems": 1, + "additionalItems": false + } + ) + }) + + it("optionalElement", () => { + const schema = Schema.Tuple(Schema.optionalElement(JsonNumber)) + const jsonSchema: Root = { + "type": "array", + "minItems": 0, + "items": [ + { + "type": "number" + } + ], + "additionalItems": false + } + expectJSONSchemaAnnotations(schema, jsonSchema) + const validate = getAjvValidate(jsonSchema) + assertTrue(validate([])) + assertTrue(validate([1])) + assertFalse(validate(["a"])) + assertFalse(validate([1, 2])) + }) + + it("optionalElement + inner annotations", () => { + expectJSONSchemaAnnotations( + Schema.Tuple(Schema.optionalElement(JsonNumber).annotations({ description: "inner" })), + { + "type": "array", + "minItems": 0, + "items": [ + { + "type": "number", + "description": "inner" + } + ], + "additionalItems": false + } + ) + }) + + it("optionalElement + outer annotations should override inner annotations", () => { + expectJSONSchemaAnnotations( + Schema.Tuple( + Schema.optionalElement(JsonNumber).annotations({ description: "inner" }).annotations({ description: "outer" }) + ), + { + "type": "array", + "minItems": 0, + "items": [ + { + "type": "number", + "description": "outer" + } + ], + "additionalItems": false + } + ) + }) + + it("element + optionalElement", () => { + const schema = Schema.Tuple( + Schema.element(Schema.String.annotations({ description: "inner" })).annotations({ description: "outer" }), + Schema.optionalElement(JsonNumber.annotations({ description: "inner?" })).annotations({ + description: "outer?" + }) + ) + const jsonSchema: Root = { + "type": "array", + "minItems": 1, + "items": [ + { + "type": "string", + "description": "outer" + }, + { + "type": "number", + "description": "outer?" + } + ], + "additionalItems": false + } + expectJSONSchemaAnnotations(schema, jsonSchema) + const validate = getAjvValidate(jsonSchema) + assertTrue(validate(["a"])) + assertTrue(validate(["a", 1])) + assertFalse(validate([])) + assertFalse(validate([1])) + assertFalse(validate([1, 2])) + }) + + it("rest", () => { + const schema = Schema.Array(JsonNumber) + const jsonSchema: Root = { + "type": "array", + "items": { + "type": "number" + } + } + expectJSONSchemaAnnotations(schema, jsonSchema) + const validate = getAjvValidate(jsonSchema) + assertTrue(validate([])) + assertTrue(validate([1])) + assertTrue(validate([1, 2])) + assertTrue(validate([1, 2, 3])) + assertFalse(validate(["a"])) + assertFalse(validate([1, 2, 3, "a"])) + }) + + it("rest + inner annotations", () => { + expectJSONSchemaAnnotations(Schema.Array(JsonNumber.annotations({ description: "inner" })), { + "type": "array", + "items": { + "type": "number", + "description": "inner" + } + }) + }) + + it("optionalElement + rest + inner annotations", () => { + const schema = Schema.Tuple( + [Schema.optionalElement(Schema.String)], + Schema.element(JsonNumber.annotations({ description: "inner" })) + ) + const jsonSchema: Root = { + "type": "array", + "minItems": 0, + "items": [ + { + "type": "string" + } + ], + "additionalItems": { + "type": "number", + "description": "inner" + } + } + expectJSONSchemaAnnotations(schema, jsonSchema) + const validate = getAjvValidate(jsonSchema) + assertTrue(validate([])) + assertTrue(validate(["a"])) + assertTrue(validate(["a", 1])) + assertFalse(validate([1])) + assertFalse(validate([1, 2])) + assertFalse(validate(["a", "b", 1])) + }) + + it("optionalElement + rest + outer annotations should override inner annotations", () => { + expectJSONSchemaAnnotations( + Schema.Tuple( + [Schema.optionalElement(Schema.String)], + Schema.element(JsonNumber.annotations({ description: "inner" })).annotations({ description: "outer" }) + ), + { + "type": "array", + "minItems": 0, + "items": [ + { + "type": "string" + } + ], + "additionalItems": { + "type": "number", + "description": "outer" + } + } + ) + }) + + it("element + rest", () => { + const schema = Schema.Tuple([Schema.String], JsonNumber) + const jsonSchema: Root = { + "type": "array", + "items": [{ + "type": "string" + }], + "minItems": 1, + "additionalItems": { + "type": "number" + } + } + expectJSONSchemaAnnotations(schema, jsonSchema) + const validate = getAjvValidate(jsonSchema) + assertTrue(validate(["a"])) + assertTrue(validate(["a", 1])) + assertTrue(validate(["a", 1, 2])) + assertTrue(validate(["a", 1, 2, 3])) + assertFalse(validate([])) + assertFalse(validate([1])) + assertFalse(validate(["a", "b"])) + }) + + it("NonEmptyArray", () => { + expectJSONSchemaProperty( + Schema.NonEmptyArray(Schema.String), + { + type: "array", + minItems: 1, + items: { type: "string" } + } + ) + }) + }) + + describe("Struct", () => { + it("Baseline", () => { + const schema = Schema.Struct({ + a: Schema.String, + b: JsonNumber + }) + const jsonSchema: Root = { + "type": "object", + "properties": { + "a": { "type": "string" }, + "b": { "type": "number" } + }, + "required": ["a", "b"], + "additionalProperties": false + } + expectJSONSchemaAnnotations(schema, jsonSchema) + const validate = getAjvValidate(jsonSchema) + assertTrue(validate({ a: "a", b: 1 })) + assertFalse(validate({})) + assertFalse(validate({ a: "a" })) + assertFalse(validate({ b: 1 })) + assertFalse(validate({ a: "a", b: 1, c: true })) + }) + + it("field + inner annotation", () => { + expectJSONSchemaAnnotations( + Schema.Struct({ + a: Schema.String.annotations({ description: "inner" }) + }), + { + "type": "object", + "properties": { + "a": { + "type": "string", + "description": "inner" + } + }, + "required": ["a"], + "additionalProperties": false + } + ) + }) + + it("field + outer annotation should override inner annotation", () => { + expectJSONSchemaAnnotations( + Schema.Struct({ + a: Schema.propertySignature(Schema.String.annotations({ description: "inner" })).annotations({ + description: "outer" + }) + }), + { + "type": "object", + "properties": { + "a": { + "type": "string", + "description": "outer" + } + }, + "required": ["a"], + "additionalProperties": false + } + ) + }) + + it("Struct + Record", () => { + const schema = Schema.Struct({ + a: Schema.String + }, Schema.Record({ key: Schema.String, value: Schema.String })) + const jsonSchema: Root = { + "type": "object", + "required": [ + "a" + ], + "properties": { + "a": { + "type": "string" + } + }, + "additionalProperties": { + "type": "string" + } + } + expectJSONSchemaAnnotations(schema, jsonSchema) + const validate = getAjvValidate(jsonSchema) + assertTrue(validate({ a: "a" })) + assertTrue(validate({ a: "a", b: "b" })) + assertFalse(validate({})) + assertFalse(validate({ b: "b" })) + assertFalse(validate({ a: 1 })) + assertFalse(validate({ a: "a", b: 1 })) + }) + + it("exact optional field", () => { + const schema = Schema.Struct({ + a: Schema.String, + b: Schema.optionalWith(JsonNumber, { exact: true }) + }) + const jsonSchema: Root = { + "type": "object", + "properties": { + "a": { + "type": "string" + }, + "b": { + "type": "number" + } + }, + "required": ["a"], + "additionalProperties": false + } + expectJSONSchemaAnnotations(schema, jsonSchema) + const validate = getAjvValidate(jsonSchema) + assertTrue(validate({ a: "a", b: 1 })) + assertTrue(validate({ a: "a" })) + assertFalse(validate({})) + assertFalse(validate({ b: 1 })) + assertFalse(validate({ a: "a", b: 1, c: true })) + }) + + it("exact optional field + inner annotation", () => { + expectJSONSchemaAnnotations( + Schema.Struct({ + a: Schema.optionalWith(Schema.String.annotations({ description: "inner" }), { exact: true }) + }), + { + "type": "object", + "properties": { + "a": { + "type": "string", + "description": "inner" + } + }, + "required": [], + "additionalProperties": false + } + ) + }) + + it("exact optional field + outer annotation should override inner annotations", () => { + expectJSONSchemaAnnotations( + Schema.Struct({ + a: Schema.optionalWith(Schema.String.annotations({ description: "inner" }), { exact: true }).annotations({ + description: "outer" + }) + }), + { + "type": "object", + "properties": { + "a": { + "type": "string", + "description": "outer" + } + }, + "required": [], + "additionalProperties": false + } + ) + }) + }) + + describe("Record", () => { + it("Record(refinement, number)", () => { + expectJSONSchemaAnnotations( + Schema.Record({ key: Schema.String.pipe(Schema.minLength(1)), value: JsonNumber }), + { + "type": "object", + "required": [], + "properties": {}, + "patternProperties": { + "": { + "type": "number" + } + }, + "propertyNames": { + "type": "string", + "title": "minLength(1)", + "description": "a string at least 1 character(s) long", + "minLength": 1 + } + } + ) + }) + + it("Record(string, number)", () => { + expectJSONSchemaAnnotations(Schema.Record({ key: Schema.String, value: JsonNumber }), { + "type": "object", + "properties": {}, + "required": [], + "additionalProperties": { + "type": "number" + } + }) + }) + + it("Record('a' | 'b', number)", () => { + expectJSONSchemaAnnotations( + Schema.Record( + { key: Schema.Union(Schema.Literal("a"), Schema.Literal("b")), value: JsonNumber } + ), + { + "type": "object", + "properties": { + "a": { + "type": "number" + }, + "b": { + "type": "number" + } + }, + "required": ["a", "b"], + "additionalProperties": false + } + ) + }) + + it("Record(${string}-${string}, number)", () => { + const schema = Schema.Record( + { key: Schema.TemplateLiteral(Schema.String, Schema.Literal("-"), Schema.String), value: JsonNumber } + ) + const jsonSchema: Root = { + "type": "object", + "required": [], + "properties": {}, + "patternProperties": { + "": { "type": "number" } + }, + "propertyNames": { + "pattern": "^[\\s\\S]*?-[\\s\\S]*?$", + "type": "string" + } + } + expectJSONSchemaAnnotations(schema, jsonSchema) + const validate = getAjvValidate(jsonSchema) + assertTrue(validate({})) + assertTrue(validate({ "-": 1 })) + assertTrue(validate({ "a-": 1 })) + assertTrue(validate({ "-b": 1 })) + assertTrue(validate({ "a-b": 1 })) + assertFalse(validate({ "": 1 })) + assertFalse(validate({ "-": "a" })) + }) + + it("Record(pattern, number)", () => { + const schema = Schema.Record( + { key: Schema.String.pipe(Schema.pattern(new RegExp("^.*-.*$"))), value: JsonNumber } + ) + const jsonSchema: Root = { + "type": "object", + "required": [], + "properties": {}, + "patternProperties": { + "": { + "type": "number" + } + }, + "propertyNames": { + "description": "a string matching the pattern ^.*-.*$", + "pattern": "^.*-.*$", + "type": "string" + } + } + expectJSONSchemaAnnotations(schema, jsonSchema) + const validate = getAjvValidate(jsonSchema) + assertTrue(validate({})) + assertTrue(validate({ "-": 1 })) + assertTrue(validate({ "a-": 1 })) + assertTrue(validate({ "-b": 1 })) + assertTrue(validate({ "a-b": 1 })) + assertFalse(validate({ "": 1 })) + assertFalse(validate({ "-": "a" })) + }) + + it("Record(SymbolFromSelf & annotation, number)", () => { + expectJSONSchemaAnnotations( + Schema.Record({ + key: Schema.SymbolFromSelf.annotations({ jsonSchema: { "type": "string" } }), + value: JsonNumber + }), + { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "required": [], + "properties": {}, + "additionalProperties": { + "type": "number" + }, + "propertyNames": { + "type": "string" + } + } + ) + }) + + it("Record(string, UndefinedOr(number))", () => { + expectJSONSchemaAnnotations(Schema.Record({ key: Schema.String, value: Schema.UndefinedOr(JsonNumber) }), { + "type": "object", + "properties": {}, + "required": [], + "additionalProperties": { "type": "number" } + }) + }) + + it("partial(Struct + Record(string, number))", () => { + const schema = Schema.partial( + Schema.Struct( + { foo: Schema.Number }, + { + key: Schema.String, + value: Schema.Number + } + ) + ) + + expectJSONSchemaAnnotations(schema, { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "required": [], + "properties": { + "foo": { + "type": "number" + } + }, + "additionalProperties": { + "type": "number" + } + }) + }) + }) + + describe("Union", () => { + it("should ignore never members", () => { + expectJSONSchema(Schema.Union(Schema.String, Schema.Never), { + "type": "string" + }) + expectJSONSchema(Schema.Union(Schema.String, Schema.Union(Schema.Never, Schema.Never)), { + "type": "string" + }) + }) + + it("string | JsonNumber", () => { + expectJSONSchemaAnnotations(Schema.Union(Schema.String, JsonNumber), { + "anyOf": [ + { "type": "string" }, + { "type": "number" } + ] + }) + }) + + describe("Union including literals", () => { + it(`1 | 2`, () => { + expectJSONSchemaAnnotations(Schema.Union(Schema.Literal(1), Schema.Literal(2)), { + "type": "number", + "enum": [1, 2] + }) + }) + + it(`1(with description) | 2`, () => { + expectJSONSchemaAnnotations( + Schema.Union( + Schema.Literal(1).annotations({ description: "43d87cd1-df64-457f-8119-0401ecd1399e" }), + Schema.Literal(2) + ), + { + "anyOf": [ + { + "type": "number", + "enum": [1], + "description": "43d87cd1-df64-457f-8119-0401ecd1399e" + }, + { + "type": "number", + "enum": [2] + } + ] + } + ) + }) + + it(`1 | 2(with description)`, () => { + expectJSONSchemaAnnotations( + Schema.Union( + Schema.Literal(1), + Schema.Literal(2).annotations({ description: "28e1ba58-7c13-4667-88cb-2baa1ac31a0f" }) + ), + { + "anyOf": [ + { + "type": "number", + "enum": [1] + }, + { + "type": "number", + "enum": [2], + "description": "28e1ba58-7c13-4667-88cb-2baa1ac31a0f" + } + ] + } + ) + }) + + it(`1 | 2 | string`, () => { + expectJSONSchemaAnnotations(Schema.Union(Schema.Literal(1), Schema.Literal(2), Schema.String), { + "anyOf": [ + { + "type": "number", + "enum": [1, 2] + }, + { "type": "string" } + ] + }) + }) + + it(`(1 | 2) | string`, () => { + expectJSONSchemaAnnotations(Schema.Union(Schema.Literal(1, 2), Schema.String), { + "anyOf": [ + { + "type": "number", + "enum": [1, 2] + }, + { "type": "string" } + ] + }) + }) + + it(`(1 | 2)(with description) | string`, () => { + expectJSONSchemaAnnotations( + Schema.Union( + Schema.Literal(1, 2).annotations({ description: "d0121d0e-8b56-4a2e-9963-47a0965d6a3c" }), + Schema.String + ), + { + "anyOf": [ + { + "type": "number", + "description": "d0121d0e-8b56-4a2e-9963-47a0965d6a3c", + "enum": [1, 2] + }, + { "type": "string" } + ] + } + ) + }) + + it(`(1 | 2)(with description) | 3 | string`, () => { + expectJSONSchemaAnnotations( + Schema.Union( + Schema.Literal(1, 2).annotations({ description: "eca4431f-c97c-454f-8167-6c2e81430c6b" }), + Schema.Literal(3), + Schema.String + ), + { + "anyOf": [ + { + "type": "number", + "description": "eca4431f-c97c-454f-8167-6c2e81430c6b", + "enum": [1, 2] + }, + { + "type": "number", + "enum": [3] + }, + { "type": "string" } + ] + } + ) + }) + + it(`1(with description) | 2 | string`, () => { + expectJSONSchemaAnnotations( + Schema.Union( + Schema.Literal(1).annotations({ description: "867c07f5-5710-477c-8296-239694e86562" }), + Schema.Literal(2), + Schema.String + ), + { + "anyOf": [ + { + "type": "number", + "description": "867c07f5-5710-477c-8296-239694e86562", + "enum": [1] + }, + { + "type": "number", + "enum": [2] + }, + { "type": "string" } + ] + } + ) + }) + + it(`1 | 2(with description) | string`, () => { + expectJSONSchemaAnnotations( + Schema.Union( + Schema.Literal(1), + Schema.Literal(2).annotations({ description: "4e49a840-5fb8-43f6-916f-565cbf532db4" }), + Schema.String + ), + { + "anyOf": [ + { + "type": "number", + "enum": [1] + }, + { + "type": "number", + "description": "4e49a840-5fb8-43f6-916f-565cbf532db4", + "enum": [2] + }, + { "type": "string" } + ] + } + ) + }) + + it(`string | 1 | 2 `, () => { + expectJSONSchemaAnnotations(Schema.Union(Schema.String, Schema.Literal(1), Schema.Literal(2)), { + "anyOf": [ + { "type": "string" }, + { + "type": "number", + "enum": [1, 2] + } + ] + }) + }) + + it(`string | (1 | 2) `, () => { + expectJSONSchemaAnnotations(Schema.Union(Schema.String, Schema.Literal(1, 2)), { + "anyOf": [ + { "type": "string" }, + { + "type": "number", + "enum": [1, 2] + } + ] + }) + }) + + it(`string | 1(with description) | 2`, () => { + expectJSONSchemaAnnotations( + Schema.Union( + Schema.String, + Schema.Literal(1).annotations({ description: "26521e57-cfb6-4563-abe2-2fe920398e16" }), + Schema.Literal(2) + ), + { + "anyOf": [ + { "type": "string" }, + { + "type": "number", + "description": "26521e57-cfb6-4563-abe2-2fe920398e16", + "enum": [1] + }, + { + "type": "number", + "enum": [2] + } + ] + } + ) + }) + + it(`string | 1 | 2(with description)`, () => { + expectJSONSchemaAnnotations( + Schema.Union( + Schema.String, + Schema.Literal(1), + Schema.Literal(2).annotations({ description: "c4fb2a01-68ff-43d2-81d0-de799c06e9c0" }) + ), + { + "anyOf": [ + { "type": "string" }, + { + "type": "number", + "enum": [1] + }, + { + "type": "number", + "description": "c4fb2a01-68ff-43d2-81d0-de799c06e9c0", + "enum": [2] + } + ] + } + ) + }) + }) + }) + + describe("Transformation", () => { + it("NumberFromString", () => { + expectJSONSchemaProperty(Schema.NumberFromString, { + "$defs": { + "NumberFromString": { + "type": "string", + "description": "a string to be decoded into a number" + } + }, + "$ref": "#/$defs/NumberFromString" + }) + }) + + it("DateFromString", () => { + expectJSONSchemaProperty( + Schema.DateFromString, + { + "$defs": { + "DateFromString": { + "type": "string", + "description": "a string to be decoded into a Date" + } + }, + "$ref": "#/$defs/DateFromString" + } + ) + }) + + it("Date", () => { + expectJSONSchemaProperty( + Schema.Date, + { + "$defs": { + "Date": { + "type": "string", + "description": "a string to be decoded into a Date" + } + }, + "$ref": "#/$defs/Date" + } + ) + }) + + it("OptionFromNullOr", () => { + expectJSONSchemaAnnotations( + Schema.Struct({ + a: Schema.OptionFromNullOr(Schema.NonEmptyString) + }), + { + "$defs": { + "NonEmptyString": { + "type": "string", + "title": "nonEmptyString", + "description": "a non empty string", + "minLength": 1 + } + }, + "type": "object", + "required": [ + "a" + ], + "properties": { + "a": { + "anyOf": [ + { "$ref": "#/$defs/NonEmptyString" }, + { "type": "null" } + ] + } + }, + "additionalProperties": false + } + ) + }) + + it("ReadonlyMapFromRecord", () => { + expectJSONSchemaProperty( + Schema.ReadonlyMapFromRecord({ + key: Schema.String.pipe(Schema.minLength(2)), + value: Schema.NumberFromString + }), + { + "$defs": { + "NumberFromString": { + "type": "string", + "description": "a string to be decoded into a number" + } + }, + "type": "object", + "description": "a record to be decoded into a ReadonlyMap", + "required": [], + "properties": {}, + "patternProperties": { + "": { + "$ref": "#/$defs/NumberFromString" + } + }, + "propertyNames": { + "title": "minLength(2)", + "description": "a string at least 2 character(s) long", + "minLength": 2, + "type": "string" + } + } + ) + }) + + it("MapFromRecord", () => { + expectJSONSchemaProperty( + Schema.MapFromRecord({ + key: Schema.String.pipe(Schema.minLength(2)), + value: Schema.NumberFromString + }), + { + "$defs": { + "NumberFromString": { + "type": "string", + "description": "a string to be decoded into a number" + } + }, + "type": "object", + "description": "a record to be decoded into a Map", + "required": [], + "properties": {}, + "patternProperties": { + "": { + "$ref": "#/$defs/NumberFromString" + } + }, + "propertyNames": { + "title": "minLength(2)", + "description": "a string at least 2 character(s) long", + "minLength": 2, + "type": "string" + } + } + ) + }) + + describe("TypeLiteralTransformation", () => { + // not sure if this is a bug or not + it.skip("a title annotation on the transformation should not overwrite an annotation set on the from part", () => { + const schema = Schema.make( + new AST.Transformation( + new AST.TypeLiteral([new AST.PropertySignature("a", Schema.String.ast, false, true)], [], { + [AST.TitleAnnotationId]: "from-title" + }), + new AST.TypeLiteral([new AST.PropertySignature("a", Schema.String.ast, false, true)], [], { + [AST.TitleAnnotationId]: "to-title" + }), + new AST.TypeLiteralTransformation([]), + { [AST.TitleAnnotationId]: "transformation-title" } + ) + ) + expectJSONSchemaProperty(schema, { + "type": "object", + "required": ["a"], + "properties": { + "a": { "type": "string" } + }, + "additionalProperties": false, + "title": "from-title" + }) + }) + + // not sure if this is a bug or not + it.skip("a description annotation on the transformation should not overwrite an annotation set on the from part", () => { + const schema = Schema.make( + new AST.Transformation( + new AST.TypeLiteral([new AST.PropertySignature("a", Schema.String.ast, false, true)], [], { + [AST.DescriptionAnnotationId]: "from-description" + }), + new AST.TypeLiteral([new AST.PropertySignature("a", Schema.String.ast, false, true)], [], { + [AST.DescriptionAnnotationId]: "to-description" + }), + new AST.TypeLiteralTransformation([]), + { [AST.DescriptionAnnotationId]: "transformation-description" } + ) + ) + expectJSONSchemaProperty(schema, { + "type": "object", + "required": ["a"], + "properties": { + "a": { "type": "string" } + }, + "additionalProperties": false, + "description": "from-description" + }) + }) + + describe("optionalWith", () => { + describe(`{ default: () => ... } option`, () => { + it("with transformation description and title", () => { + expectJSONSchemaProperty( + Schema.Struct({ + a: Schema.optionalWith( + Schema.NonEmptyString.annotations({ + description: "inner-description", + title: "inner-title" + }), + { default: () => "" } + ).annotations({ + description: "middle-description", + title: "middle-title" + }) + }).annotations({ + description: "outer-description", + title: "outer-title" + }), + { + "type": "object", + "description": "outer-description", + "title": "outer-title", + "required": [], + "properties": { + "a": { + "description": "middle-description", + "minLength": 1, + "title": "middle-title", + "type": "string" + } + }, + "additionalProperties": false + } + ) + }) + }) + + describe(`{ as: "Option" } option`, () => { + it("base", () => { + expectJSONSchemaProperty( + Schema.Struct({ + a: Schema.optionalWith(Schema.NonEmptyString, { as: "Option" }) + }), + { + "$defs": { + "NonEmptyString": { + "type": "string", + "title": "nonEmptyString", + "description": "a non empty string", + "minLength": 1 + } + }, + "type": "object", + "required": [], + "properties": { + "a": { + "$ref": "#/$defs/NonEmptyString" + } + }, + "additionalProperties": false + } + ) + }) + + it("with transformation identifier annotation", () => { + expectJSONSchemaProperty( + Schema.Struct({ + a: Schema.optionalWith(Schema.NonEmptyString, { as: "Option" }) + }).annotations({ + identifier: "aa6f48cd-03e4-470a-beb7-5f7cc532c676", + description: "b964b873-0266-446b-acf4-97dc125e7553", + title: "aa67b73c-3161-4640-b1e1-5b5830cfb173" + }), + { + "$ref": "#/$defs/aa6f48cd-03e4-470a-beb7-5f7cc532c676", + "$defs": { + "NonEmptyString": { + "type": "string", + "title": "nonEmptyString", + "description": "a non empty string", + "minLength": 1 + }, + "aa6f48cd-03e4-470a-beb7-5f7cc532c676": { + "type": "object", + "required": [], + "properties": { + "a": { + "$ref": "#/$defs/NonEmptyString" + } + }, + "additionalProperties": false, + "description": "b964b873-0266-446b-acf4-97dc125e7553", + "title": "aa67b73c-3161-4640-b1e1-5b5830cfb173" + } + } + } + ) + }) + }) + }) + + describe("fromKey", () => { + it("a <- b", () => { + expectJSONSchemaProperty( + Schema.Struct({ + a: Schema.NonEmptyString.pipe(Schema.propertySignature, Schema.fromKey("b")) + }), + { + "$defs": { + "NonEmptyString": { + "type": "string", + "title": "nonEmptyString", + "description": "a non empty string", + "minLength": 1 + } + }, + "type": "object", + "required": [ + "b" + ], + "properties": { + "b": { + "$ref": "#/$defs/NonEmptyString" + } + }, + "additionalProperties": false + } + ) + }) + + it("a <- b & annotations", () => { + expectJSONSchemaProperty( + Schema.Struct({ + a: Schema.NonEmptyString.pipe(Schema.propertySignature, Schema.fromKey("b")).annotations({}) + }), + { + "$defs": { + "NonEmptyString": { + "type": "string", + "title": "nonEmptyString", + "description": "a non empty string", + "minLength": 1 + } + }, + "type": "object", + "required": [ + "b" + ], + "properties": { + "b": { + "$ref": "#/$defs/NonEmptyString" + } + }, + "additionalProperties": false + } + ) + }) + + it("with transformation identifier annotation", () => { + expectJSONSchemaProperty( + Schema.Struct({ + a: Schema.NonEmptyString.pipe(Schema.propertySignature, Schema.fromKey("b")) + }).annotations({ + identifier: "d5ff7bc8-1bd5-42a7-8186-e29fd4c217ea", + description: "5f7bc5b8-dd68-4ec5-b9e9-64df74bd3c45", + title: "119da226-70aa-4ae6-ab63-7db10c7e9dde" + }), + { + "$ref": "#/$defs/d5ff7bc8-1bd5-42a7-8186-e29fd4c217ea", + "$defs": { + "NonEmptyString": { + "type": "string", + "title": "nonEmptyString", + "description": "a non empty string", + "minLength": 1 + }, + "d5ff7bc8-1bd5-42a7-8186-e29fd4c217ea": { + "type": "object", + "required": [ + "b" + ], + "properties": { + "b": { + "$ref": "#/$defs/NonEmptyString" + } + }, + "additionalProperties": false, + "description": "5f7bc5b8-dd68-4ec5-b9e9-64df74bd3c45", + "title": "119da226-70aa-4ae6-ab63-7db10c7e9dde" + } + } + } + ) + }) + }) + }) + }) + + describe("Suspend", () => { + it("should support outer suspended schemas", () => { + interface A { + readonly a: string + readonly as: ReadonlyArray + } + const schema: Schema.Schema = Schema.suspend(() => + // intended outer suspend + Schema.Struct({ + a: Schema.String, + as: Schema.Array(schema) + }) + ).annotations({ identifier: "cdb51157-6f4a-42c1-9075-5b9af3a1448c" }) + const jsonSchema: Root = { + "$ref": "#/$defs/cdb51157-6f4a-42c1-9075-5b9af3a1448c", + "$defs": { + "cdb51157-6f4a-42c1-9075-5b9af3a1448c": { + "type": "object", + "required": [ + "a", + "as" + ], + "properties": { + "a": { + "type": "string" + }, + "as": { + "type": "array", + "items": { + "$ref": "#/$defs/cdb51157-6f4a-42c1-9075-5b9af3a1448c" + } + } + }, + "additionalProperties": false + } + } + } + expectJSONSchemaProperty(schema, jsonSchema) + const validate = getAjvValidate(jsonSchema) + assertTrue(validate({ a: "a1", as: [] })) + assertTrue(validate({ a: "a1", as: [{ a: "a2", as: [] }] })) + assertTrue(validate({ a: "a1", as: [{ a: "a2", as: [] }, { a: "a3", as: [] }] })) + assertTrue( + validate({ a: "a1", as: [{ a: "a2", as: [] }, { a: "a3", as: [{ a: "a4", as: [] }] }] }) + ) + assertFalse( + validate({ a: "a1", as: [{ a: "a2", as: [] }, { a: "a3", as: [{ a: "a4", as: [1] }] }] }) + ) + }) + + it("should support inner suspended schemas with inner identifier annotation", () => { + interface A { + readonly a: string + readonly as: ReadonlyArray + } + const schema = Schema.Struct({ + a: Schema.String, + as: Schema.Array( + Schema.suspend((): Schema.Schema => schema).annotations({ + identifier: "c4588a13-c003-4b8d-930f-d3469925ec1b" + }) + ) + }) + const jsonSchema: Root = { + "type": "object", + "required": [ + "a", + "as" + ], + "properties": { + "a": { + "type": "string" + }, + "as": { + "type": "array", + "items": { + "$ref": "#/$defs/c4588a13-c003-4b8d-930f-d3469925ec1b" + } + } + }, + "additionalProperties": false, + "$defs": { + "c4588a13-c003-4b8d-930f-d3469925ec1b": { + "type": "object", + "required": [ + "a", + "as" + ], + "properties": { + "a": { + "type": "string" + }, + "as": { + "type": "array", + "items": { + "$ref": "#/$defs/c4588a13-c003-4b8d-930f-d3469925ec1b" + } + } + }, + "additionalProperties": false + } + } + } + expectJSONSchemaProperty(schema, jsonSchema) + const validate = getAjvValidate(jsonSchema) + assertTrue(validate({ a: "a1", as: [] })) + assertTrue(validate({ a: "a1", as: [{ a: "a2", as: [] }] })) + assertTrue(validate({ a: "a1", as: [{ a: "a2", as: [] }, { a: "a3", as: [] }] })) + assertTrue( + validate({ a: "a1", as: [{ a: "a2", as: [] }, { a: "a3", as: [{ a: "a4", as: [] }] }] }) + ) + assertFalse( + validate({ a: "a1", as: [{ a: "a2", as: [] }, { a: "a3", as: [{ a: "a4", as: [1] }] }] }) + ) + }) + + it("should support inner suspended schemas with outer identifier annotation", () => { + interface Category { + readonly name: string + readonly categories: ReadonlyArray + } + const schema = Schema.Struct({ + name: Schema.String, + categories: Schema.Array(Schema.suspend((): Schema.Schema => schema)) + }).annotations({ identifier: "5c2a4755-f8f2-4290-a40f-ed247803a1a0" }) + const jsonSchema: Root = { + "$ref": "#/$defs/5c2a4755-f8f2-4290-a40f-ed247803a1a0", + "$defs": { + "5c2a4755-f8f2-4290-a40f-ed247803a1a0": { + "type": "object", + "required": [ + "name", + "categories" + ], + "properties": { + "name": { + "type": "string" + }, + "categories": { + "type": "array", + "items": { + "$ref": "#/$defs/5c2a4755-f8f2-4290-a40f-ed247803a1a0" + } + } + }, + "additionalProperties": false + } + } + } + expectJSONSchemaProperty(schema, jsonSchema) + const validate = getAjvValidate(jsonSchema) + assertTrue(validate({ name: "a1", categories: [] })) + assertTrue(validate({ name: "a1", categories: [{ name: "a2", categories: [] }] })) + assertTrue(validate({ name: "a1", categories: [{ name: "a2", categories: [] }, { name: "a3", categories: [] }] })) + + assertTrue( + validate({ + name: "a1", + categories: [{ name: "a2", categories: [] }, { name: "a3", categories: [{ name: "a4", categories: [] }] }] + }) + ) + assertFalse( + validate({ + name: "a1", + categories: [{ name: "a2", categories: [] }, { name: "a3", categories: [{ name: "a4", categories: [1] }] }] + }) + ) + }) + + it("should support mutually suspended schemas", () => { + interface Expression { + readonly type: "expression" + readonly value: number | Operation + } + + interface Operation { + readonly type: "operation" + readonly operator: "+" | "-" + readonly left: Expression + readonly right: Expression + } + + // intended outer suspend + const Expression: Schema.Schema = Schema.suspend(() => + Schema.Struct({ + type: Schema.Literal("expression"), + value: Schema.Union(JsonNumber, Operation) + }) + ).annotations({ identifier: "2ad5683a-878f-4e4d-909c-496e59ce62e0" }) + + // intended outer suspend + const Operation: Schema.Schema = Schema.suspend(() => + Schema.Struct({ + type: Schema.Literal("operation"), + operator: Schema.Union(Schema.Literal("+"), Schema.Literal("-")), + left: Expression, + right: Expression + }) + ).annotations({ identifier: "e0f2ce47-eac7-4991-8730-90ebe4e0ffda" }) + + const jsonSchema: Root = { + "$ref": "#/$defs/e0f2ce47-eac7-4991-8730-90ebe4e0ffda", + "$defs": { + "e0f2ce47-eac7-4991-8730-90ebe4e0ffda": { + "type": "object", + "required": [ + "type", + "operator", + "left", + "right" + ], + "properties": { + "type": { + "type": "string", + "enum": ["operation"] + }, + "operator": { + "type": "string", + "enum": ["+", "-"] + }, + "left": { + "$ref": "#/$defs/2ad5683a-878f-4e4d-909c-496e59ce62e0" + }, + "right": { + "$ref": "#/$defs/2ad5683a-878f-4e4d-909c-496e59ce62e0" + } + }, + "additionalProperties": false + }, + "2ad5683a-878f-4e4d-909c-496e59ce62e0": { + "type": "object", + "required": [ + "type", + "value" + ], + "properties": { + "type": { + "type": "string", + "enum": ["expression"] + }, + "value": { + "anyOf": [ + { + "type": "number" + }, + { + "$ref": "#/$defs/e0f2ce47-eac7-4991-8730-90ebe4e0ffda" + } + ] + } + }, + "additionalProperties": false + } + } + } + expectJSONSchemaProperty(Operation, jsonSchema, { numRuns: 5 }) + const validate = getAjvValidate(jsonSchema) + assertTrue(validate({ + type: "operation", + operator: "+", + left: { + type: "expression", + value: 1 + }, + right: { + type: "expression", + value: { + type: "operation", + operator: "-", + left: { + type: "expression", + value: 3 + }, + right: { + type: "expression", + value: 2 + } + } + } + })) + }) + }) + + it("examples JSON Schema annotation support", () => { + expectJSONSchemaAnnotations(Schema.String.annotations({ examples: ["a", "b"] }), { + "type": "string", + "examples": ["a", "b"] + }) + expectJSONSchemaProperty(Schema.BigInt.annotations({ examples: [1n, 2n] }), { + "description": "a string to be decoded into a bigint", + "examples": [ + "1", + "2" + ], + "type": "string" + }) + expectJSONSchemaProperty( + Schema.Struct({ + a: Schema.propertySignature(Schema.BigInt).annotations({ examples: [1n, 2n] }) + }), + { + "$defs": { + "BigInt": { + "type": "string", + "description": "a string to be decoded into a bigint" + } + }, + "type": "object", + "required": [ + "a" + ], + "properties": { + "a": { + "allOf": [ + { "$ref": "#/$defs/BigInt" } + ], + "examples": ["1", "2"] + } + }, + "additionalProperties": false + } + ) + }) + + it("default JSON Schema annotation support", () => { + expectJSONSchemaAnnotations(Schema.String.annotations({ default: "" }), { + "type": "string", + "default": "" + }) + }) + + describe("Class", () => { + it("should use the identifier as JSON Schema identifier", () => { + const input = Schema.Struct({ a: Schema.String }) + class A extends Schema.Class("7a8b06e3-ebc1-4bdd-ab0d-3ec493d96d95")(input) {} + expectJSONSchemaProperty(A, { + "$defs": { + "7a8b06e3-ebc1-4bdd-ab0d-3ec493d96d95": { + "type": "object", + "required": [ + "a" + ], + "properties": { + "a": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "$ref": "#/$defs/7a8b06e3-ebc1-4bdd-ab0d-3ec493d96d95" + }) + }) + + it("should escape special characters in the $ref", () => { + const input = Schema.Struct({ a: Schema.String }) + class A extends Schema.Class("~package/name")(input) {} + expectJSONSchemaProperty(A, { + "$defs": { + "~package/name": { + "type": "object", + "required": [ + "a" + ], + "properties": { + "a": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "$ref": "#/$defs/~0package~1name" + }) + }) + }) + + it("compose", () => { + expectJSONSchemaAnnotations( + Schema.Struct({ + a: Schema.NonEmptyString.pipe(Schema.compose(Schema.NumberFromString)) + }), + { + "$defs": { + "NonEmptyString": { + "type": "string", + "title": "nonEmptyString", + "description": "a non empty string", + "minLength": 1 + } + }, + "type": "object", + "required": [ + "a" + ], + "properties": { + "a": { + "$ref": "#/$defs/NonEmptyString" + } + }, + "additionalProperties": false + } + ) + }) + + describe("extend", () => { + it("should correctly generate JSON Schemas for a schema created by extending two refinements", () => { + expectJSONSchemaAnnotations( + Schema.Struct({ + a: Schema.String + }).pipe( + Schema.filter(() => true, { + jsonSchema: { "examples": ["c5052c04-d6c9-44f3-9c8f-ede707d6ce38"] } + }) + ).pipe(Schema.extend( + Schema.Struct({ + b: JsonNumber + }).pipe( + Schema.filter(() => true, { + jsonSchema: { "$comment": "940b4ea4-6313-4b59-9e64-ff7a41b8eb15" } + }) + ) + )), + { + "type": "object", + "required": ["a", "b"], + "properties": { + "a": { "type": "string" }, + "b": { "type": "number" } + }, + "examples": ["c5052c04-d6c9-44f3-9c8f-ede707d6ce38"], + "$comment": "940b4ea4-6313-4b59-9e64-ff7a41b8eb15", + "additionalProperties": false + } + ) + }) + }) + + describe("identifier annotation support", () => { + it("String", () => { + expectJSONSchemaProperty(Schema.String.annotations({ identifier: "6f274f5e-be19-48e6-8f33-16e9789b2731" }), { + "$defs": { + "6f274f5e-be19-48e6-8f33-16e9789b2731": { + "type": "string" + } + }, + "$ref": "#/$defs/6f274f5e-be19-48e6-8f33-16e9789b2731" + }) + }) + + it("Refinement", () => { + expectJSONSchemaProperty( + Schema.String.pipe(Schema.minLength(2)).annotations({ identifier: "cd6647a4-dc64-40a7-a031-61d35ed904ca" }), + { + "$defs": { + "cd6647a4-dc64-40a7-a031-61d35ed904ca": { + "type": "string", + "title": "minLength(2)", + "description": "a string at least 2 character(s) long", + "minLength": 2 + } + }, + "$ref": "#/$defs/cd6647a4-dc64-40a7-a031-61d35ed904ca" + } + ) + }) + + describe("Struct", () => { + it("self annotation", () => { + expectJSONSchemaProperty( + Schema.Struct({ + a: Schema.String + }).annotations({ identifier: "0df962f3-f649-4ffc-a3ec-a8b5344dd7de" }), + { + "$defs": { + "0df962f3-f649-4ffc-a3ec-a8b5344dd7de": { + "type": "object", + "required": ["a"], + "properties": { + "a": { "type": "string" } + }, + "additionalProperties": false + } + }, + "$ref": "#/$defs/0df962f3-f649-4ffc-a3ec-a8b5344dd7de" + } + ) + }) + + it("field annotations", () => { + const Name = Schema.String.annotations({ + identifier: "44873d66-d138-4e2a-9782-5982a29f6ea8", + description: "e5d30f53-b2df-4fa3-b151-9fc3a47d258e", + title: "0115ccbf-5d27-41ed-a658-83c5f4a8805f" + }) + const schema = Schema.Struct({ + a: Name + }) + expectJSONSchemaProperty(schema, { + "$defs": { + "44873d66-d138-4e2a-9782-5982a29f6ea8": { + "type": "string", + "description": "e5d30f53-b2df-4fa3-b151-9fc3a47d258e", + "title": "0115ccbf-5d27-41ed-a658-83c5f4a8805f" + } + }, + "type": "object", + "required": ["a"], + "properties": { + "a": { + "$ref": "#/$defs/44873d66-d138-4e2a-9782-5982a29f6ea8" + } + }, + "additionalProperties": false + }) + }) + + it("self annotation + field annotations", () => { + const Name = Schema.String.annotations({ + identifier: "b49f125d-1646-4eb5-8120-9524ab6039de", + description: "703b7ff0-cb8d-49de-aeeb-05d92faa4599", + title: "4b6d9ea6-7c4d-4073-a427-8d1b82fd1677" + }) + expectJSONSchemaProperty( + Schema.Struct({ + a: Name + }).annotations({ identifier: "7e559891-9143-4138-ae3e-81a5f0907380" }), + { + "$defs": { + "7e559891-9143-4138-ae3e-81a5f0907380": { + "type": "object", + "required": ["a"], + "properties": { + "a": { "$ref": "#/$defs/b49f125d-1646-4eb5-8120-9524ab6039de" } + }, + "additionalProperties": false + }, + "b49f125d-1646-4eb5-8120-9524ab6039de": { + "type": "string", + "description": "703b7ff0-cb8d-49de-aeeb-05d92faa4599", + "title": "4b6d9ea6-7c4d-4073-a427-8d1b82fd1677" + } + }, + "$ref": "#/$defs/7e559891-9143-4138-ae3e-81a5f0907380" + } + ) + }) + + it("deeply nested field annotations", () => { + const Name = Schema.String.annotations({ + identifier: "434a08dd-3f8f-4de4-b91d-8846aab1fb05", + description: "eb183f5c-404c-4686-b78b-1bd00d18f8fd", + title: "c0cbd438-1fb5-47fe-bf81-1ff5527e779a" + }) + const schema = Schema.Struct({ a: Name, b: Schema.Struct({ c: Name }) }) + expectJSONSchemaProperty(schema, { + "$defs": { + "434a08dd-3f8f-4de4-b91d-8846aab1fb05": { + "type": "string", + "description": "eb183f5c-404c-4686-b78b-1bd00d18f8fd", + "title": "c0cbd438-1fb5-47fe-bf81-1ff5527e779a" + } + }, + "type": "object", + "required": ["a", "b"], + "properties": { + "a": { + "$ref": "#/$defs/434a08dd-3f8f-4de4-b91d-8846aab1fb05" + }, + "b": { + "type": "object", + "required": ["c"], + "properties": { + "c": { "$ref": "#/$defs/434a08dd-3f8f-4de4-b91d-8846aab1fb05" } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }) + }) + }) + + describe("Union", () => { + it("Union of literals with identifiers", () => { + expectJSONSchemaAnnotations( + Schema.Union( + Schema.Literal("a").annotations({ + description: "ef296f1c-01fe-4a20-bd35-ed449c964c49", + identifier: "170d659f-112e-4e3b-85db-464b668f2aed" + }), + Schema.Literal("b").annotations({ + description: "effbf54b-a62d-455b-86fa-97a5af46c6f3", + identifier: "2a4e4f67-3732-4f7b-a505-856e51dd1578" + }) + ), + { + "$defs": { + "170d659f-112e-4e3b-85db-464b668f2aed": { + "type": "string", + "enum": ["a"], + "description": "ef296f1c-01fe-4a20-bd35-ed449c964c49" + }, + "2a4e4f67-3732-4f7b-a505-856e51dd1578": { + "type": "string", + "enum": ["b"], + "description": "effbf54b-a62d-455b-86fa-97a5af46c6f3" + } + }, + "anyOf": [ + { "$ref": "#/$defs/170d659f-112e-4e3b-85db-464b668f2aed" }, + { "$ref": "#/$defs/2a4e4f67-3732-4f7b-a505-856e51dd1578" } + ] + } + ) + }) + }) + + describe("Transformation", () => { + describe("TypeLiteralTransformation", () => { + it("an identifier annotation on the transformation should overwrite an annotation set on the from part", () => { + const schema = Schema.make( + new AST.Transformation( + new AST.TypeLiteral([new AST.PropertySignature("a", Schema.String.ast, false, true)], [], { + [AST.IdentifierAnnotationId]: "0f70b90b-b268-46c8-a5a3-035139ad9126" + }), + new AST.TypeLiteral([new AST.PropertySignature("a", Schema.String.ast, false, true)], [], { + [AST.IdentifierAnnotationId]: "77bb2410-9cf3-47cf-af76-fa3be1a3c626" + }), + new AST.TypeLiteralTransformation([]), + { [AST.IdentifierAnnotationId]: "18e1de28-a15e-4373-bd2f-d53903942656" } + ) + ) + expectJSONSchemaProperty(schema, { + "$defs": { + "18e1de28-a15e-4373-bd2f-d53903942656": { + "type": "object", + "required": ["a"], + "properties": { + "a": { "type": "string" } + }, + "additionalProperties": false + } + }, + "$ref": "#/$defs/18e1de28-a15e-4373-bd2f-d53903942656" + }) + }) + + it("with transformation description, title and identifier", () => { + expectJSONSchemaProperty( + Schema.Struct({ + a: Schema.optionalWith( + Schema.NonEmptyString.annotations({ + description: "inner-description", + title: "inner-title" + }), + { default: () => "" } + ).annotations({ + description: "middle-description", + title: "middle-title" + }) + }).annotations({ + description: "outer-description", + title: "outer-title", + identifier: "75d9b539-eb6b-48d3-81dd-61176a9bce78" + }), + { + "$defs": { + "75d9b539-eb6b-48d3-81dd-61176a9bce78": { + "type": "object", + "description": "outer-description", + "title": "outer-title", + "required": [], + "properties": { + "a": { + "type": "string", + "description": "middle-description", + "title": "middle-title", + "minLength": 1 + } + }, + "additionalProperties": false + } + }, + "$ref": "#/$defs/75d9b539-eb6b-48d3-81dd-61176a9bce78" + } + ) + }) + }) + }) + }) + + describe("surrogate annotation support", () => { + describe("Class", () => { + it("should support typeSchema(Class)", () => { + class A extends Schema.Class("A")({ a: Schema.String }) {} + expectJSONSchemaProperty(Schema.typeSchema(A), { + "$defs": { + "A": { + "type": "object", + "required": ["a"], + "properties": { + "a": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "$ref": "#/$defs/A" + }) + expectJSONSchemaProperty( + Schema.typeSchema(A).annotations({ + description: "description", + title: "title" + }), + { + "type": "object", + "required": ["a"], + "properties": { + "a": { + "type": "string" + } + }, + "additionalProperties": false, + "description": "description", + "title": "title" + } + ) + }) + + it("with identifier annotation", () => { + class A extends Schema.Class("A")({ a: Schema.String }, { + identifier: "ID", + description: "description", + title: "title" + }) {} + expectJSONSchemaProperty(Schema.typeSchema(A), { + "$defs": { + "ID": { + "type": "object", + "required": ["a"], + "properties": { + "a": { + "type": "string" + } + }, + "additionalProperties": false, + "description": "description", + "title": "title" + } + }, + "$ref": "#/$defs/ID" + }) + expectJSONSchemaProperty( + Schema.typeSchema(A).annotations({ + description: "description", + title: "title" + }), + { + "type": "object", + "required": ["a"], + "properties": { + "a": { + "type": "string" + } + }, + "additionalProperties": false, + "description": "description", + "title": "title" + } + ) + }) + }) + }) + + describe("jsonSchema annotation support", () => { + it("refinements without a jsonSchema annotation should be ignored rather than raising an error", () => { + const schema = Schema.String.pipe(Schema.filter(() => true)) + expectJSONSchema(schema, { + "type": "string" + }) + }) + + it("should have higher priority than surrogate annotation", () => { + expectJSONSchema( + Schema.String.annotations({ + [AST.SurrogateAnnotationId]: Schema.Number.ast, + jsonSchema: { "type": "custom" } + }), + { + "type": "custom" + } + ) + }) + + describe("Class", () => { + it("should support typeSchema(Class) with custom annotation", () => { + class A extends Schema.Class("3c9977ee-0e9b-4471-99af-c6c73340f9ed")({ a: Schema.String }, { + jsonSchema: { "type": "custom" } + }) {} + expectJSONSchema(Schema.typeSchema(A), { + "$defs": { + "3c9977ee-0e9b-4471-99af-c6c73340f9ed": { + "type": "custom" + } + }, + "$ref": "#/$defs/3c9977ee-0e9b-4471-99af-c6c73340f9ed" + }) + }) + }) + + it("Declaration", () => { + class MyType {} + const schema = Schema.declare((x) => x instanceof MyType, { + jsonSchema: { + type: "my-type", + title: "default-title", + description: "default-description" + } + }).annotations({ + title: "My Title", + description: "My Description" + }) + expectJSONSchema(schema, { + "type": "my-type", + "title": "My Title", + "description": "My Description" + }) + }) + + it("Void", () => { + expectJSONSchema(Schema.Void.annotations({ jsonSchema: { "type": "custom" } }), { + "type": "custom" + }) + }) + + it("Never", () => { + expectJSONSchema(Schema.Never.annotations({ jsonSchema: { "type": "custom" } }), { + "type": "custom" + }) + }) + + it("Literal", () => { + expectJSONSchema(Schema.Literal("a").annotations({ jsonSchema: { "type": "custom" } }), { + "type": "custom" + }) + }) + + it("SymbolFromSelf", () => { + expectJSONSchema(Schema.SymbolFromSelf.annotations({ jsonSchema: { "type": "custom" } }), { + "type": "custom" + }) + }) + + it("UniqueSymbolFromSelf", () => { + expectJSONSchema( + Schema.UniqueSymbolFromSelf(Symbol.for("effect/schema/test/a")).annotations({ + jsonSchema: { "type": "custom" } + }), + { "type": "custom" } + ) + }) + + it("TemplateLiteral", () => { + expectJSONSchema( + Schema.TemplateLiteral(Schema.Literal("a"), Schema.String, Schema.Literal("b")).annotations({ + jsonSchema: { "type": "custom" } + }), + { "type": "custom" } + ) + }) + + it("Undefined", () => { + expectJSONSchema(Schema.Undefined.annotations({ jsonSchema: { "type": "custom" } }), { + "type": "custom" + }) + }) + + it("Unknown", () => { + expectJSONSchema(Schema.Unknown.annotations({ jsonSchema: { "type": "custom" } }), { + "type": "custom" + }) + }) + + it("Any", () => { + expectJSONSchema(Schema.Any.annotations({ jsonSchema: { "type": "custom" } }), { + "type": "custom" + }) + }) + + it("Object", () => { + expectJSONSchema(Schema.Object.annotations({ jsonSchema: { "type": "custom" } }), { + "type": "custom" + }) + }) + + it("String", () => { + expectJSONSchema( + Schema.String.annotations({ + jsonSchema: { + "type": "custom", + "description": "description", + "format": "uuid" + } + }), + { + "type": "custom", + "description": "description", + "format": "uuid" + } + ) + expectJSONSchema( + Schema.String.annotations({ + identifier: "630d10c4-7030-45e7-894d-2c0bf5acadcf", + jsonSchema: { "type": "custom", "description": "description" } + }), + { + "$defs": { + "630d10c4-7030-45e7-894d-2c0bf5acadcf": { + "type": "custom", + "description": "description" + } + }, + "$ref": "#/$defs/630d10c4-7030-45e7-894d-2c0bf5acadcf" + } + ) + }) + + it("Number", () => { + expectJSONSchema(Schema.Number.annotations({ jsonSchema: { "type": "custom" } }), { + "type": "custom" + }) + }) + + it("BigintFromSelf", () => { + expectJSONSchema(Schema.BigIntFromSelf.annotations({ jsonSchema: { "type": "custom" } }), { + "type": "custom" + }) + }) + + it("Boolean", () => { + expectJSONSchema(Schema.Boolean.annotations({ jsonSchema: { "type": "custom" } }), { + "type": "custom" + }) + }) + + it("Enums", () => { + enum Fruits { + Apple, + Banana + } + expectJSONSchema(Schema.Enums(Fruits).annotations({ jsonSchema: { "type": "custom" } }), { + "type": "custom" + }) + }) + + it("Tuple", () => { + expectJSONSchema( + Schema.Tuple(Schema.String, JsonNumber).annotations({ jsonSchema: { "type": "custom" } }), + { "type": "custom" } + ) + }) + + it("Struct", () => { + expectJSONSchema( + Schema.Struct({ a: Schema.String, b: JsonNumber }).annotations({ + jsonSchema: { "type": "custom" } + }), + { "type": "custom" } + ) + }) + + it("Union", () => { + expectJSONSchema( + Schema.Union(Schema.String, JsonNumber).annotations({ jsonSchema: { "type": "custom" } }), + { "type": "custom" } + ) + }) + + it("UUID", () => { + expectJSONSchema( + Schema.UUID, + { + "$defs": { + "UUID": { + "description": "a Universally Unique Identifier", + "pattern": "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$", + "type": "string", + "format": "uuid" + } + }, + "$ref": "#/$defs/UUID" + } + ) + }) + + it("Suspend", () => { + interface A { + readonly a: string + readonly as: ReadonlyArray + } + const schema = Schema.Struct({ + a: Schema.String, + as: Schema.Array( + Schema.suspend((): Schema.Schema => schema).annotations({ jsonSchema: { "type": "custom" } }) + ) + }) + + expectJSONSchema(schema, { + "type": "object", + "required": [ + "a", + "as" + ], + "properties": { + "a": { + "type": "string" + }, + "as": { + "type": "array", + "items": { + "type": "custom" + } + } + }, + "additionalProperties": false + }) + }) + + describe("Refinement", () => { + it("Int", () => { + expectJSONSchema(Schema.Int.annotations({ jsonSchema: { "type": "custom" } }), { + "type": "custom" + }) + }) + + it("custom", () => { + expectJSONSchemaProperty( + Schema.String.pipe(Schema.filter(() => true, { jsonSchema: {} })).annotations({ + identifier: "230acf3d-b3b0-4c3e-8ccc-5ca089c80014" + }), + { + "$ref": "#/$defs/230acf3d-b3b0-4c3e-8ccc-5ca089c80014", + "$defs": { + "230acf3d-b3b0-4c3e-8ccc-5ca089c80014": { + "type": "string" + } + } + } + ) + }) + }) + + it("Transformation", () => { + expectJSONSchema(Schema.NumberFromString.annotations({ jsonSchema: { "type": "custom" } }), { + "type": "custom" + }) + }) + + it("refinement of a transformation with an override annotation", () => { + expectJSONSchema(Schema.Date.annotations({ jsonSchema: { type: "string", format: "date-time" } }), { + "format": "date-time", + "type": "string" + }) + expectJSONSchema( + Schema.Date.annotations({ + jsonSchema: { anyOf: [{ type: "object" }, { type: "array" }] } + }), + { + "anyOf": [{ "type": "object" }, { "type": "array" }] + } + ) + expectJSONSchema( + Schema.Date.annotations({ + jsonSchema: { anyOf: [{ type: "object" }, { type: "array" }] } + }), + { + "anyOf": [{ "type": "object" }, { "type": "array" }] + } + ) + expectJSONSchema(Schema.Date.annotations({ jsonSchema: { "$ref": "x" } }), { + "$ref": "x" + }) + expectJSONSchema(Schema.Date.annotations({ jsonSchema: { "type": "number", "const": 1 } }), { + "type": "number", + "const": 1 + }) + expectJSONSchema(Schema.Date.annotations({ jsonSchema: { "type": "number", "enum": [1] } }), { + "type": "number", + "enum": [1] + }) + }) + + it("refinement of a transformation without an override annotation", () => { + expectJSONSchema(Schema.Trim.pipe(Schema.nonEmptyString()), { + "type": "string", + "description": "a string that will be trimmed" + }) + expectJSONSchema( + Schema.Trim.pipe(Schema.nonEmptyString({ jsonSchema: { title: "a0ba6c10-091e-4ceb-9773-25fb1466fb1b" } })), + { + "type": "string", + "description": "a string that will be trimmed" + } + ) + expectJSONSchema( + Schema.Trim.pipe(Schema.nonEmptyString()).annotations({ + jsonSchema: { title: "75f7eb4f-626d-4dc6-af48-c17094418d85" } + }), + { + "type": "string", + "description": "a string that will be trimmed" + } + ) + }) + }) + + describe("Pruning `undefined` and make the property optional by default", () => { + it("Undefined", () => { + expectJSONSchemaAnnotations( + Schema.Struct({ + a: Schema.Undefined + }), + { + "type": "object", + "required": [], + "properties": { + "a": { + "$id": "/schemas/never", + "not": {}, + "title": "never" + } + }, + "additionalProperties": false + } + ) + }) + + it("UndefinedOr(Undefined)", () => { + expectJSONSchemaAnnotations( + Schema.Struct({ + a: Schema.UndefinedOr(Schema.Undefined) + }), + { + "type": "object", + "required": [], + "properties": { + "a": { + "$id": "/schemas/never", + "not": {}, + "title": "never" + } + }, + "additionalProperties": false + } + ) + }) + + it("Nested `Undefined`s", () => { + expectJSONSchemaAnnotations( + Schema.Struct({ + a: Schema.UndefinedOr(Schema.UndefinedOr(Schema.Undefined)) + }), + { + "type": "object", + "required": [], + "properties": { + "a": { + "$id": "/schemas/never", + "not": {}, + "title": "never" + } + }, + "additionalProperties": false + } + ) + }) + + it("Schema.optional", () => { + expectJSONSchemaAnnotations( + Schema.Struct({ + a: Schema.optional(Schema.String) + }), + { + "type": "object", + "required": [], + "properties": { + "a": { "type": "string" } + }, + "additionalProperties": false + } + ) + }) + + it("Schema.optional + inner annotation", () => { + expectJSONSchemaAnnotations( + Schema.Struct({ + a: Schema.optional(Schema.String.annotations({ description: "inner" })) + }), + { + "type": "object", + "required": [], + "properties": { + "a": { + "type": "string", + "description": "inner" + } + }, + "additionalProperties": false + } + ) + }) + + it("Schema.optional + outer annotation should override inner annotation", () => { + expectJSONSchemaAnnotations( + Schema.Struct({ + a: Schema.optional(Schema.String.annotations({ description: "inner" })).annotations({ + description: "outer" + }) + }), + { + "type": "object", + "required": [], + "properties": { + "a": { + "type": "string", + "description": "outer" + } + }, + "additionalProperties": false + } + ) + }) + + it("UndefinedOr", () => { + expectJSONSchemaAnnotations( + Schema.Struct({ + a: Schema.UndefinedOr(Schema.String) + }), + { + "type": "object", + "required": [], + "properties": { + "a": { + "type": "string" + } + }, + "additionalProperties": false + } + ) + }) + + it("UndefinedOr + inner annotation", () => { + expectJSONSchemaAnnotations( + Schema.Struct({ + a: Schema.UndefinedOr(Schema.String.annotations({ description: "inner" })) + }), + { + "type": "object", + "required": [], + "properties": { + "a": { + "type": "string", + "description": "inner" + } + }, + "additionalProperties": false + } + ) + }) + + it("UndefinedOr + annotation should not override inner annotations", () => { + expectJSONSchemaAnnotations( + Schema.Struct({ + a: Schema.UndefinedOr(Schema.String.annotations({ description: "inner" })).annotations({ + description: "middle" + }) + }), + { + "type": "object", + "required": [], + "properties": { + "a": { + "type": "string", + "description": "inner" + } + }, + "additionalProperties": false + } + ) + }) + + it("UndefinedOr + propertySignature annotation should override inner and middle annotations", () => { + expectJSONSchemaAnnotations( + Schema.Struct({ + a: Schema.propertySignature( + Schema.UndefinedOr(Schema.String.annotations({ description: "inner" })).annotations({ + description: "middle" + }) + ).annotations({ description: "outer" }) + }), + { + "type": "object", + "required": [], + "properties": { + "a": { + "type": "string", + "description": "outer" + } + }, + "additionalProperties": false + } + ) + }) + + it("UndefinedOr + jsonSchema annotation should keep the property required", () => { + expectJSONSchema( + Schema.Struct({ + a: Schema.UndefinedOr(Schema.String).annotations({ jsonSchema: { "type": "string" } }) + }), + { + "type": "object", + "required": ["a"], + "properties": { + "a": { "type": "string" } + }, + "additionalProperties": false + } + ) + }) + + it("Transformation: OptionFromUndefinedOr", () => { + expectJSONSchemaAnnotations( + Schema.Struct({ + a: Schema.OptionFromUndefinedOr(Schema.String) + }), + { + "type": "object", + "required": [], + "properties": { + "a": { + "type": "string" + } + }, + "additionalProperties": false + } + ) + }) + + it("Suspend", () => { + expectJSONSchemaAnnotations( + Schema.Struct({ + a: Schema.suspend(() => Schema.UndefinedOr(Schema.String)) + }), + { + "type": "object", + "required": [], + "properties": { + "a": { + "type": "string" + } + }, + "additionalProperties": false + } + ) + }) + }) + + describe("Schema.encodedBoundSchema / Schema.encodedSchema", () => { + describe("borrowing the identifier", () => { + describe("Declaration", () => { + it("without inner transformation", () => { + const schema = Schema.Chunk(Schema.String).annotations({ identifier: "ID" }) + const expected = { + "items": { + "type": "string" + }, + "type": "array" + } + expectJSONSchemaProperty(Schema.encodedBoundSchema(schema), expected) + expectJSONSchemaProperty(Schema.encodedSchema(schema), expected) + }) + + it("with inner transformation", () => { + const schema = Schema.Chunk(Schema.NumberFromString).annotations({ + identifier: "ID" + }) + const expected = { + "items": { + "description": "a string to be decoded into a number", + "type": "string" + }, + "type": "array" + } + expectJSONSchemaProperty(Schema.encodedBoundSchema(schema), expected) + expectJSONSchemaProperty(Schema.encodedSchema(schema), expected) + }) + }) + + describe("Refinement", () => { + it("without from transformation", () => { + const schema = Schema.Trimmed + expectJSONSchemaProperty(Schema.encodedBoundSchema(schema), { + "$defs": { + "Trimmed": { + "title": "trimmed", + "description": "a string with no leading or trailing whitespace", + "pattern": "^\\S[\\s\\S]*\\S$|^\\S$|^$", + "type": "string" + } + }, + "$ref": "#/$defs/Trimmed" + }) + expectJSONSchemaProperty(Schema.encodedSchema(schema), { + "type": "string" + }) + }) + + it("with from transformation", () => { + const schema = Schema.compose(Schema.String, Schema.Trimmed).annotations({ + identifier: "ID" + }) + const expected = { + "type": "string" + } + expectJSONSchemaProperty(Schema.encodedBoundSchema(schema), expected) + expectJSONSchemaProperty(Schema.encodedSchema(schema), expected) + }) + + it("a stable filter without inner transformations", () => { + const schema = Schema.Array(Schema.NumberFromString).pipe(Schema.minItems(2)).annotations( + { identifier: "ID" } + ) + expectJSONSchemaProperty(Schema.encodedBoundSchema(schema), { + "$defs": { + "ID": { + "description": "an array of at least 2 item(s)", + "title": "minItems(2)", + "items": { + "description": "a string to be decoded into a number", + "type": "string" + }, + "minItems": 2, + "type": "array" + } + }, + "$ref": "#/$defs/ID" + }) + expectJSONSchemaProperty(Schema.encodedSchema(schema), { + "items": { + "description": "a string to be decoded into a number", + "type": "string" + }, + "type": "array" + }) + }) + + it("a stable filter with inner transformations SHOULD NOT borrow the annotations, identifier included", () => { + const schema = Schema.compose(Schema.Unknown, Schema.Array(Schema.String)).pipe(Schema.minItems(1)) + const expected = { + "$id": "/schemas/unknown", + "title": "unknown" + } + expectJSONSchemaProperty(Schema.encodedBoundSchema(schema), expected) + expectJSONSchemaProperty(Schema.encodedSchema(schema), expected) + }) + }) + + describe("Tuple", () => { + it("without inner transformations", () => { + const schema = Schema.Tuple(Schema.String).annotations({ + identifier: "4d8bbca3-9462-4679-8ee6-e4e718711552" + }) + const expected = { + "$defs": { + "4d8bbca3-9462-4679-8ee6-e4e718711552": { + "additionalItems": false, + "items": [{ "type": "string" }], + "minItems": 1, + "type": "array" + } + }, + "$ref": "#/$defs/4d8bbca3-9462-4679-8ee6-e4e718711552" + } + expectJSONSchemaProperty(Schema.encodedBoundSchema(schema), expected) + expectJSONSchemaProperty(Schema.encodedSchema(schema), expected) + }) + + it("with inner transformations", () => { + const schema = Schema.Tuple(Schema.NumberFromString).annotations({ + identifier: "ID" + }) + const expected = { + "additionalItems": false, + "items": [{ + "description": "a string to be decoded into a number", + "type": "string" + }], + "minItems": 1, + "type": "array" + } + expectJSONSchemaProperty(Schema.encodedBoundSchema(schema), expected) + expectJSONSchemaProperty(Schema.encodedSchema(schema), expected) + }) + }) + + describe("Struct", () => { + it("without inner transformations", () => { + const schema = Schema.Struct({ a: Schema.String }).annotations({ + identifier: "c8d0663b-c41b-4b6f-8b6e-bff59afc87c3" + }) + const expected = { + "$defs": { + "c8d0663b-c41b-4b6f-8b6e-bff59afc87c3": { + "additionalProperties": false, + "properties": { + "a": { "type": "string" } + }, + "required": ["a"], + "type": "object" + } + }, + "$ref": "#/$defs/c8d0663b-c41b-4b6f-8b6e-bff59afc87c3" + } + expectJSONSchemaProperty(Schema.encodedBoundSchema(schema), expected) + expectJSONSchemaProperty(Schema.encodedSchema(schema), expected) + }) + + it("with inner transformations", () => { + const schema = Schema.Struct({ a: Schema.NumberFromString }).annotations({ + identifier: "ID" + }) + const expected = { + "additionalProperties": false, + "properties": { + "a": { + "description": "a string to be decoded into a number", + "type": "string" + } + }, + "required": ["a"], + "type": "object" + } + expectJSONSchemaProperty(Schema.encodedBoundSchema(schema), expected) + expectJSONSchemaProperty(Schema.encodedSchema(schema), expected) + }) + }) + + describe("Union", () => { + it("without inner transformations", () => { + const schema = Schema.Union(Schema.String, Schema.JsonNumber).annotations({ + identifier: "ID" + }) + expectJSONSchemaProperty(Schema.encodedBoundSchema(schema), { + "$defs": { + "JsonNumber": { + "description": "a finite number", + "title": "finite", + "type": "number" + }, + "ID": { + "anyOf": [ + { "type": "string" }, + { "$ref": "#/$defs/JsonNumber" } + ] + } + }, + "$ref": "#/$defs/ID" + }) + expectJSONSchema(Schema.encodedSchema(schema), { + "anyOf": [ + { "type": "string" }, + { "type": "number" } + ] + }) + }) + + it("with inner transformations", () => { + const schema = Schema.Union(Schema.String, Schema.NumberFromString).annotations({ + identifier: "ID" + }) + const expected = { + "anyOf": [ + { "type": "string" }, + { "description": "a string to be decoded into a number", "type": "string" } + ] + } + expectJSONSchemaProperty(Schema.encodedBoundSchema(schema), expected) + expectJSONSchemaProperty(Schema.encodedSchema(schema), expected) + }) + }) + + describe("Suspend", () => { + it("without inner transformations", () => { + interface Category { + readonly name: string + readonly categories: ReadonlyArray + } + + const schema: Schema.Schema = Schema.Struct({ + name: Schema.String, + categories: Schema.Array( + Schema.suspend(() => schema).annotations({ identifier: "ID" }) + ) + }) + + expectJSONSchemaProperty(Schema.encodedBoundSchema(schema), { + "type": "object", + "required": [ + "name", + "categories" + ], + "properties": { + "name": { + "type": "string" + }, + "categories": { + "type": "array", + "items": { + "$ref": "#/$defs/IDEncodedBound" + } + } + }, + "additionalProperties": false, + "$defs": { + "IDEncodedBound": { + "type": "object", + "required": [ + "name", + "categories" + ], + "properties": { + "name": { + "type": "string" + }, + "categories": { + "type": "array", + "items": { + "$ref": "#/$defs/IDEncodedBound" + } + } + }, + "additionalProperties": false + } + } + }) + expectJSONSchemaProperty(Schema.encodedSchema(schema), { + "type": "object", + "required": [ + "name", + "categories" + ], + "properties": { + "name": { + "type": "string" + }, + "categories": { + "type": "array", + "items": { + "$ref": "#/$defs/IDEncoded" + } + } + }, + "additionalProperties": false, + "$defs": { + "IDEncoded": { + "type": "object", + "required": [ + "name", + "categories" + ], + "properties": { + "name": { + "type": "string" + }, + "categories": { + "type": "array", + "items": { + "$ref": "#/$defs/IDEncoded" + } + } + }, + "additionalProperties": false + } + } + }) + }) + + it("with inner transformations", () => { + interface Category { + readonly name: number + readonly categories: ReadonlyArray + } + interface CategoryEncoded { + readonly name: string + readonly categories: ReadonlyArray + } + + const schema: Schema.Schema = Schema.Struct({ + name: Schema.NumberFromString, + categories: Schema.Array( + Schema.suspend(() => schema).annotations({ identifier: "ID" }) + ) + }) + + expectJSONSchemaProperty(Schema.encodedBoundSchema(schema), { + "type": "object", + "required": [ + "name", + "categories" + ], + "properties": { + "name": { + "description": "a string to be decoded into a number", + "type": "string" + }, + "categories": { + "type": "array", + "items": { + "$ref": "#/$defs/IDEncodedBound" + } + } + }, + "additionalProperties": false, + "$defs": { + "IDEncodedBound": { + "type": "object", + "required": [ + "name", + "categories" + ], + "properties": { + "name": { + "description": "a string to be decoded into a number", + "type": "string" + }, + "categories": { + "type": "array", + "items": { + "$ref": "#/$defs/IDEncodedBound" + } + } + }, + "additionalProperties": false + } + } + }) + expectJSONSchemaProperty(Schema.encodedSchema(schema), { + "type": "object", + "required": [ + "name", + "categories" + ], + "properties": { + "name": { + "description": "a string to be decoded into a number", + "type": "string" + }, + "categories": { + "type": "array", + "items": { + "$ref": "#/$defs/IDEncoded" + } + } + }, + "additionalProperties": false, + "$defs": { + "IDEncoded": { + "type": "object", + "required": [ + "name", + "categories" + ], + "properties": { + "name": { + "description": "a string to be decoded into a number", + "type": "string" + }, + "categories": { + "type": "array", + "items": { + "$ref": "#/$defs/IDEncoded" + } + } + }, + "additionalProperties": false + } + } + }) + }) + }) + + it("Transformation", () => { + const expected = { + "type": "string", + "description": "a string to be decoded into a number" + } + expectJSONSchemaProperty(Schema.encodedBoundSchema(Schema.NumberFromString), expected) + expectJSONSchemaProperty(Schema.encodedSchema(Schema.NumberFromString), expected) + }) + }) + }) + + it("Exit", () => { + const schema = Schema.Exit({ + failure: Schema.String, + success: Schema.Number, + defect: Schema.Defect + }) + expectJSONSchemaProperty(schema, { + "$schema": "http://json-schema.org/draft-07/schema#", + "$defs": { + "CauseEncoded0": { + "anyOf": [ + { + "type": "object", + "required": [ + "_tag" + ], + "properties": { + "_tag": { + "type": "string", + "enum": [ + "Empty" + ] + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "_tag", + "error" + ], + "properties": { + "_tag": { + "type": "string", + "enum": [ + "Fail" + ] + }, + "error": { + "type": "string" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "_tag", + "defect" + ], + "properties": { + "_tag": { + "type": "string", + "enum": [ + "Die" + ] + }, + "defect": { + "$ref": "#/$defs/Defect" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "_tag", + "fiberId" + ], + "properties": { + "_tag": { + "type": "string", + "enum": [ + "Interrupt" + ] + }, + "fiberId": { + "$ref": "#/$defs/FiberIdEncoded" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "_tag", + "left", + "right" + ], + "properties": { + "_tag": { + "type": "string", + "enum": [ + "Sequential" + ] + }, + "left": { + "$ref": "#/$defs/CauseEncoded0" + }, + "right": { + "$ref": "#/$defs/CauseEncoded0" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "_tag", + "left", + "right" + ], + "properties": { + "_tag": { + "type": "string", + "enum": [ + "Parallel" + ] + }, + "left": { + "$ref": "#/$defs/CauseEncoded0" + }, + "right": { + "$ref": "#/$defs/CauseEncoded0" + } + }, + "additionalProperties": false + } + ], + "title": "CauseEncoded" + }, + "Defect": { + "$id": "/schemas/unknown", + "title": "unknown" + }, + "FiberIdEncoded": { + "anyOf": [ + { + "$ref": "#/$defs/FiberIdNoneEncoded" + }, + { + "$ref": "#/$defs/FiberIdRuntimeEncoded" + }, + { + "$ref": "#/$defs/FiberIdCompositeEncoded" + } + ] + }, + "FiberIdNoneEncoded": { + "type": "object", + "required": [ + "_tag" + ], + "properties": { + "_tag": { + "type": "string", + "enum": [ + "None" + ] + } + }, + "additionalProperties": false + }, + "FiberIdRuntimeEncoded": { + "type": "object", + "required": [ + "_tag", + "id", + "startTimeMillis" + ], + "properties": { + "_tag": { + "type": "string", + "enum": [ + "Runtime" + ] + }, + "id": { + "$ref": "#/$defs/Int" + }, + "startTimeMillis": { + "$ref": "#/$defs/Int" + } + }, + "additionalProperties": false + }, + "Int": { + "type": "integer", + "description": "an integer", + "title": "int" + }, + "FiberIdCompositeEncoded": { + "type": "object", + "required": [ + "_tag", + "left", + "right" + ], + "properties": { + "_tag": { + "type": "string", + "enum": [ + "Composite" + ] + }, + "left": { + "$ref": "#/$defs/FiberIdEncoded" + }, + "right": { + "$ref": "#/$defs/FiberIdEncoded" + } + }, + "additionalProperties": false + } + }, + "anyOf": [ + { + "type": "object", + "required": [ + "_tag", + "cause" + ], + "properties": { + "_tag": { + "type": "string", + "enum": [ + "Failure" + ] + }, + "cause": { + "$ref": "#/$defs/CauseEncoded0" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "_tag", + "value" + ], + "properties": { + "_tag": { + "type": "string", + "enum": [ + "Success" + ] + }, + "value": { + "type": "number" + } + }, + "additionalProperties": false + } + ], + "title": "ExitEncoded" + }) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/ParseResult.test.ts b/repos/effect/packages/effect/test/Schema/ParseResult.test.ts new file mode 100644 index 0000000..59e5732 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/ParseResult.test.ts @@ -0,0 +1,644 @@ +import { describe, it } from "@effect/vitest" +import { + assertFailure, + assertLeft, + assertSuccess, + assertTrue, + deepStrictEqual, + strictEqual, + throws +} from "@effect/vitest/utils" +import { Cause, Effect, Either, ParseResult } from "effect" +import * as S from "effect/Schema" +import * as AST from "effect/SchemaAST" +import { inspect } from "node:util" + +const asEffect = (either: Either.Either): Effect.Effect => either + +const expectGetRefinementExpected = (schema: S.Schema.Any, expected: string) => { + if (AST.isRefinement(schema.ast)) { + strictEqual(ParseResult.getRefinementExpected(schema.ast), expected) + } else { + // eslint-disable-next-line no-console + console.log(schema.ast) + throw new Error(`expected a Refinement`) + } +} + +describe("ParseResult", () => { + const typeParseError1 = ParseResult.parseError(new ParseResult.Type(S.String.ast, null)) + const typeParseError2 = ParseResult.parseError(new ParseResult.Type(S.Number.ast, null)) + + it("getRefinementExpected", () => { + expectGetRefinementExpected(S.Number.pipe(S.filter(() => true)), "{ number | filter }") + expectGetRefinementExpected(S.Number.pipe(S.int()), "an integer") + expectGetRefinementExpected(S.Number.pipe(S.int(), S.positive()), "a positive number") + expectGetRefinementExpected(S.Int.pipe(S.positive()), "a positive number") + }) + + describe("ParseError", () => { + it("toString()", () => { + const schema = S.Struct({ a: S.String }) + assertLeft( + S.decodeUnknownEither(schema)({}).pipe(Either.mapLeft((e) => e.toString())), + `{ readonly a: string } +└─ ["a"] + └─ is missing` + ) + }) + + it("toJSON()", () => { + const schema = S.Struct({ a: S.String }) + assertLeft(S.decodeUnknownEither(schema)({}).pipe(Either.mapLeft((e) => (e as any).toJSON())), { + _id: "ParseError", + message: `{ readonly a: string } +└─ ["a"] + └─ is missing` + }) + }) + + it("[NodeInspectSymbol]", () => { + const schema = S.Struct({ a: S.String }) + assertLeft( + S.decodeUnknownEither(schema)({}).pipe(Either.mapLeft((e) => inspect(e))), + inspect({ + _id: "ParseError", + message: `{ readonly a: string } +└─ ["a"] + └─ is missing` + }) + ) + }) + + it("Error.stack", () => { + assertTrue( + ParseResult.parseError(new ParseResult.Type(S.String.ast, 1)).stack?.startsWith( + `ParseError: Expected string, actual 1` + ) + ) + }) + + it("Effect.catchTag can be used to catch ParseError", () => { + const program = Effect.fail(typeParseError1).pipe( + Effect.catchTag("ParseError", () => Effect.succeed(1)) + ) + strictEqual(Effect.runSync(program), 1) + }) + }) + + it("eitherOrUndefined", () => { + deepStrictEqual(ParseResult.eitherOrUndefined(Either.right(1)), Either.right(1)) + deepStrictEqual(ParseResult.eitherOrUndefined(Either.left("err")), Either.left("err")) + strictEqual(ParseResult.eitherOrUndefined(Effect.succeed(1)), undefined) + strictEqual(ParseResult.eitherOrUndefined(Effect.fail("err")), undefined) + }) + + it("flatMap", () => { + deepStrictEqual( + ParseResult.flatMap(Either.right(1), (a) => Either.right(a)), + Either.right(1) as Effect.Effect + ) + deepStrictEqual( + ParseResult.flatMap(Either.right(1), () => Either.left("err")), + Either.left("err") as Effect.Effect + ) + assertSuccess( + Effect.runSyncExit( + ParseResult.flatMap(Either.right(1), (a) => Either.right(a)) + ), + 1 + ) + assertSuccess( + Effect.runSyncExit( + ParseResult.flatMap(Either.right(1), (a) => Effect.succeed(a)) + ), + 1 + ) + assertFailure( + Effect.runSyncExit(ParseResult.flatMap(Either.right(1), () => Either.left("err"))), + Cause.fail("err") + ) + assertFailure( + Effect.runSyncExit(ParseResult.flatMap(Either.right(1), () => Effect.fail("err"))), + Cause.fail("err") + ) + }) + + it("map", () => { + deepStrictEqual(ParseResult.map(Either.right(1), (n) => n + 1), asEffect(Either.right(2))) + deepStrictEqual(ParseResult.map(Either.left(typeParseError1), (n) => n + 1), asEffect(Either.left(typeParseError1))) + deepStrictEqual(Either.right(1).pipe(ParseResult.map((n) => n + 1)), Either.right(2)) + assertSuccess(Effect.runSyncExit(ParseResult.map(Effect.succeed(1), (n) => n + 1)), 2) + assertFailure( + Effect.runSyncExit(ParseResult.map(Effect.fail(typeParseError1), (n) => n + 1)), + Cause.fail(typeParseError1) + ) + }) + + it("mapError", () => { + deepStrictEqual(ParseResult.mapError(Either.right(1), () => typeParseError2), asEffect(Either.right(1))) + deepStrictEqual( + ParseResult.mapError(Either.left(typeParseError1), () => typeParseError2), + asEffect(Either.left(typeParseError2)) + ) + // pipeable + deepStrictEqual(Either.right(1).pipe(ParseResult.mapError(() => typeParseError2)), Either.right(1)) + assertSuccess(Effect.runSyncExit(ParseResult.mapError(Effect.succeed(1), () => typeParseError2)), 1) + assertFailure( + Effect.runSyncExit( + ParseResult.mapError(Effect.fail(typeParseError1), () => typeParseError2) + ), + Cause.fail(typeParseError2) + ) + }) + + it("mapBoth", () => { + deepStrictEqual( + ParseResult.mapBoth(Either.right(1), { onFailure: () => typeParseError2, onSuccess: (n) => n + 1 }), + asEffect(Either.right(2)) + ) + deepStrictEqual( + ParseResult.mapBoth(Either.left(typeParseError1), { + onFailure: () => typeParseError2, + onSuccess: (n) => n + 1 + }), + asEffect(Either.left(typeParseError2)) + ) + // pipeable + deepStrictEqual( + Either.right(1).pipe(ParseResult.mapBoth({ onFailure: () => typeParseError2, onSuccess: (n) => n + 1 })), + Either.right(2) + ) + assertSuccess( + Effect.runSyncExit( + ParseResult.mapBoth(Effect.succeed(1), { onFailure: () => typeParseError2, onSuccess: (n) => n + 1 }) + ), + 2 + ) + assertFailure( + Effect.runSyncExit( + ParseResult.mapBoth(Effect.fail(typeParseError1), { + onFailure: () => typeParseError2, + onSuccess: (n) => n + 1 + }) + ), + Cause.fail(typeParseError2) + ) + }) + + it("orElse", () => { + deepStrictEqual(ParseResult.orElse(Either.right(1), () => Either.right(2)), asEffect(Either.right(1))) + deepStrictEqual(ParseResult.orElse(Either.left(typeParseError1), () => Either.right(2)), asEffect(Either.right(2))) + // pipeable + deepStrictEqual(Either.right(1).pipe(ParseResult.orElse(() => Either.right(2))), Either.right(1)) + assertSuccess(Effect.runSyncExit(ParseResult.orElse(Effect.succeed(1), () => Either.right(2))), 1) + assertSuccess( + Effect.runSyncExit( + ParseResult.orElse(Effect.fail(typeParseError1), () => Either.right(2)) + ), + 2 + ) + }) +}) + +describe("ParseIssue.actual", () => { + it("transform decode", () => { + const result = S.decodeEither(S.transformOrFail( + S.NumberFromString, + S.Boolean, + { + strict: true, + decode: (n, _, ast) => ParseResult.fail(new ParseResult.Type(ast, n)), + encode: (b, _, ast) => ParseResult.fail(new ParseResult.Type(ast, b)) + } + ))("1") + if (Either.isRight(result)) throw new Error("Expected failure") + strictEqual(result.left.issue.actual, "1") + strictEqual((result.left.issue as ParseResult.Transformation).issue.actual, 1) + }) + + it("transform encode", () => { + const result = S.encodeEither(S.transformOrFail( + S.Boolean, + S.NumberFromString, + { + strict: true, + decode: (n, _, ast) => ParseResult.fail(new ParseResult.Type(ast, n)), + encode: (b, _, ast) => ParseResult.fail(new ParseResult.Type(ast, b)) + } + ))(1) + if (Either.isRight(result)) throw new Error("Expected failure") + strictEqual(result.left.issue.actual, 1) + strictEqual((result.left.issue as ParseResult.Transformation).issue.actual, "1") + }) + + it("compose decode", () => { + const result = S.decodeEither(S.compose(S.NumberFromString, S.Number.pipe(S.negative())))("1") + if (Either.isRight(result)) throw new Error("Expected failure") + strictEqual(result.left.issue.actual, "1") + strictEqual((result.left.issue as ParseResult.Transformation).issue.actual, 1) + }) + + it("compose encode", () => { + const result = S.encodeEither(S.compose(S.String.pipe(S.length(5)), S.NumberFromString))(1) + if (Either.isRight(result)) throw new Error("Expected failure") + strictEqual(result.left.issue.actual, 1) + strictEqual((result.left.issue as ParseResult.Transformation).issue.actual, "1") + }) + + it("decode", () => { + assertTrue(Either.isEither(ParseResult.decode(S.String)("a"))) + }) + + it("encode", () => { + assertTrue(Either.isEither(ParseResult.encode(S.String)("a"))) + }) + + it("mergeInternalOptions", () => { + strictEqual(ParseResult.mergeInternalOptions(undefined, undefined), undefined) + deepStrictEqual(ParseResult.mergeInternalOptions({}, undefined), {}) + deepStrictEqual(ParseResult.mergeInternalOptions(undefined, {}), {}) + deepStrictEqual(ParseResult.mergeInternalOptions({ errors: undefined }, undefined), { errors: undefined }) + deepStrictEqual(ParseResult.mergeInternalOptions(undefined, { errors: undefined }), { errors: undefined }) + deepStrictEqual(ParseResult.mergeInternalOptions({ errors: "all" }, { errors: "first" }), { + errors: "first" + }) + deepStrictEqual(ParseResult.mergeInternalOptions({ onExcessProperty: "ignore" }, { onExcessProperty: "error" }), { + onExcessProperty: "error" + }) + deepStrictEqual(ParseResult.mergeInternalOptions({}, { exact: false }), { exact: false }) + deepStrictEqual(ParseResult.mergeInternalOptions({ exact: true }, { exact: false }), { exact: false }) + + deepStrictEqual(ParseResult.mergeInternalOptions({ isEffectAllowed: true }, {}), { isEffectAllowed: true }) + deepStrictEqual(ParseResult.mergeInternalOptions({}, { isEffectAllowed: true }), { isEffectAllowed: true }) + deepStrictEqual(ParseResult.mergeInternalOptions({ isEffectAllowed: false }, { isEffectAllowed: true }), { + isEffectAllowed: true + }) + }) + + it("asserts", () => { + const schema = S.String + strictEqual(ParseResult.asserts(schema)("a"), undefined) + throws( + () => ParseResult.asserts(schema)(1), + new ParseResult.ParseError({ issue: new ParseResult.Type(schema.ast, 1) }) + ) + }) + + describe("getLiterals", () => { + it("StringKeyword", () => { + deepStrictEqual(ParseResult.getLiterals(S.String.ast, true), []) + }) + + it("Struct", () => { + deepStrictEqual(ParseResult.getLiterals(S.Struct({ _tag: S.Literal("a") }).ast, true), [[ + "_tag", + new AST.Literal("a") + ]]) + }) + + it("Tuple", () => { + deepStrictEqual(ParseResult.getLiterals(S.Tuple(S.Literal("a"), S.String).ast, true), [[0, new AST.Literal("a")]]) + }) + + it("Refinement", () => { + deepStrictEqual( + ParseResult.getLiterals( + S.Struct({ _tag: S.Literal("a") }).pipe( + S.filter(() => true) + ).ast, + true + ), + [["_tag", new AST.Literal("a")]] + ) + }) + + it("Transform (decode)", () => { + deepStrictEqual( + ParseResult.getLiterals( + S.Struct({ radius: S.Number }).pipe(S.attachPropertySignature("kind", "circle")).ast, + true + ), + [] + ) + }) + + it("Transform (encode)", () => { + deepStrictEqual( + ParseResult.getLiterals( + S.Struct({ radius: S.Number }).pipe(S.attachPropertySignature("kind", "circle")).ast, + false + ), + [["kind", new AST.Literal("circle")]] + ) + }) + + it("property Transform (encode)", () => { + deepStrictEqual( + ParseResult.getLiterals( + S.Struct({ + _tag: S.transform( + S.Literal("a"), + S.Literal("b"), + { strict: true, decode: () => "b" as const, encode: () => "a" as const } + ) + }) + .ast, + false + ), + [["_tag", new AST.Literal("b")]] + ) + }) + + it("Class (decode)", () => { + class A extends S.Class("A")({ _tag: S.Literal("a") }) {} + deepStrictEqual(ParseResult.getLiterals(A.ast, true), [["_tag", new AST.Literal("a")]]) + }) + + it("Class (encode)", () => { + class A extends S.Class("A")({ _tag: S.Literal("a") }) {} + deepStrictEqual(ParseResult.getLiterals(A.ast, false), [["_tag", new AST.Literal("a")]]) + }) + }) + + describe("getSearchTree", () => { + it("primitive + primitive", () => { + deepStrictEqual(ParseResult.getSearchTree([S.String.ast, S.Number.ast], true), { + keys: {}, + otherwise: [S.String.ast, S.Number.ast], + candidates: [] + }) + }) + + it("struct + primitive", () => { + const a = S.Struct({ _tag: S.Literal("a") }) + deepStrictEqual(ParseResult.getSearchTree([a.ast, S.Number.ast], true), { + keys: { + _tag: { + buckets: { + a: [a.ast] + }, + literals: [new AST.Literal("a")], + candidates: [a.ast] + } + }, + otherwise: [S.Number.ast], + candidates: [a.ast] + }) + }) + + it("struct + struct (same tag key)", () => { + const a = S.Struct({ _tag: S.Literal("a") }) + const b = S.Struct({ _tag: S.Literal("b") }) + deepStrictEqual(ParseResult.getSearchTree([a.ast, b.ast], true), { + keys: { + _tag: { + buckets: { + a: [a.ast], + b: [b.ast] + }, + literals: [new AST.Literal("a"), new AST.Literal("b")], + candidates: [a.ast, b.ast] + } + }, + otherwise: [], + candidates: [a.ast, b.ast] + }) + }) + + it("struct + struct (different tag key)", () => { + const A = S.Struct({ a: S.Literal("A"), c: S.String }) + const B = S.Struct({ b: S.Literal("B"), d: S.Number }) + deepStrictEqual( + ParseResult.getSearchTree([A.ast, B.ast], true), + { + keys: { + a: { + buckets: { + A: [A.ast] + }, + literals: [new AST.Literal("A")], + candidates: [A.ast] + }, + b: { + buckets: { + B: [B.ast] + }, + literals: [new AST.Literal("B")], + candidates: [B.ast] + } + }, + otherwise: [], + candidates: [A.ast, B.ast] + } + ) + }) + + it("struct + struct (multiple tags)", () => { + const A = S.Struct({ _tag: S.Literal("A"), _tag2: S.Literal("A1"), c: S.String }) + const B = S.Struct({ _tag: S.Literal("A"), _tag2: S.Literal("A2"), d: S.Number }) + deepStrictEqual( + ParseResult.getSearchTree([A.ast, B.ast], true), + { + keys: { + _tag: { + buckets: { + A: [A.ast] + }, + literals: [new AST.Literal("A")], + candidates: [A.ast] + }, + _tag2: { + buckets: { + A2: [B.ast] + }, + literals: [new AST.Literal("A2")], + candidates: [B.ast] + } + }, + otherwise: [], + candidates: [A.ast, B.ast] + } + ) + }) + + it("tuple + tuple (same tag key)", () => { + const a = S.Tuple(S.Literal("a"), S.String) + const b = S.Tuple(S.Literal("b"), S.Number) + deepStrictEqual( + ParseResult.getSearchTree([a.ast, b.ast], true), + { + keys: { + 0: { + buckets: { + a: [a.ast], + b: [b.ast] + }, + literals: [new AST.Literal("a"), new AST.Literal("b")], + candidates: [a.ast, b.ast] + } + }, + otherwise: [], + candidates: [a.ast, b.ast] + } + ) + }) + + it("tuple + tuple (different tag key)", () => { + const a = S.Tuple(S.Literal("a"), S.String) + const b = S.Tuple(S.Number, S.Literal("b")) + deepStrictEqual( + ParseResult.getSearchTree([a.ast, b.ast], true), + { + keys: { + 0: { + buckets: { + a: [a.ast] + }, + literals: [new AST.Literal("a")], + candidates: [a.ast] + }, + 1: { + buckets: { + b: [b.ast] + }, + literals: [new AST.Literal("b")], + candidates: [b.ast] + } + }, + otherwise: [], + candidates: [a.ast, b.ast] + } + ) + }) + + it("tuple + tuple (multiple tags)", () => { + const a = S.Tuple(S.Literal("a"), S.Literal("b"), S.String) + const b = S.Tuple(S.Literal("a"), S.Literal("c"), S.Number) + deepStrictEqual( + ParseResult.getSearchTree([a.ast, b.ast], true), + { + keys: { + 0: { + buckets: { + a: [a.ast] + }, + literals: [new AST.Literal("a")], + candidates: [a.ast] + }, + 1: { + buckets: { + c: [b.ast] + }, + literals: [new AST.Literal("c")], + candidates: [b.ast] + } + }, + otherwise: [], + candidates: [a.ast, b.ast] + } + ) + }) + + it("should handle multiple tags", () => { + const a = S.Struct({ category: S.Literal("catA"), tag: S.Literal("a") }) + const b = S.Struct({ category: S.Literal("catA"), tag: S.Literal("b") }) + const c = S.Struct({ category: S.Literal("catA"), tag: S.Literal("c") }) + deepStrictEqual( + ParseResult.getSearchTree([ + a.ast, + b.ast, + c.ast + ], true), + { + keys: { + category: { + buckets: { + catA: [a.ast] + }, + literals: [new AST.Literal("catA")], + candidates: [a.ast] + }, + tag: { + buckets: { + b: [b.ast], + c: [c.ast] + }, + literals: [new AST.Literal("b"), new AST.Literal("c")], + candidates: [b.ast, c.ast] + } + }, + otherwise: [], + candidates: [a.ast, b.ast, c.ast] + } + ) + }) + + it("big union", () => { + const a = S.Struct({ type: S.Literal("a"), value: S.String }) + const b = S.Struct({ type: S.Literal("b"), value: S.String }) + const c = S.Struct({ type: S.Literal("c"), value: S.String }) + const n = S.Struct({ type: S.Literal(null), value: S.String }) + const schema = S.Union( + a, + b, + c, + S.Struct({ type: S.String, value: S.String }), + n, + S.Struct({ type: S.Undefined, value: S.String }), + S.Struct({ type: S.Literal("d", "e"), value: S.String }), + S.Struct({ type: S.Struct({ nested: S.String }), value: S.String }), + S.Struct({ type: S.Array(S.Number), value: S.String }) + ) + const types = (schema.ast as AST.Union).types + deepStrictEqual(ParseResult.getSearchTree(types, true), { + keys: { + type: { + buckets: { + a: [a.ast], + b: [b.ast], + c: [c.ast], + null: [n.ast] + }, + literals: [ + new AST.Literal("a"), + new AST.Literal("b"), + new AST.Literal("c"), + new AST.Literal(null) + ], + candidates: [a.ast, b.ast, c.ast, n.ast] + } + }, + otherwise: [ + S.Struct({ type: S.String, value: S.String }).ast, + S.Struct({ type: S.Undefined, value: S.String }).ast, + S.Struct({ type: S.Literal("d", "e"), value: S.String }).ast, + S.Struct({ type: S.Struct({ nested: S.String }), value: S.String }).ast, + S.Struct({ type: S.Array(S.Number), value: S.String }).ast + ], + candidates: [ + a.ast, + b.ast, + c.ast, + n.ast + ] + }) + }) + + it("nested unions", () => { + const a = S.Struct({ _tag: S.Literal("a") }) + const b = S.Struct({ _tag: S.Literal("b") }) + const A = S.Struct({ a: S.Literal("A"), c: S.String }) + const B = S.Struct({ b: S.Literal("B"), d: S.Number }) + const ab = S.Union(a, b) + const AB = S.Union(A, B) + const schema = S.Union(ab, AB) + const types = (schema.ast as AST.Union).types + deepStrictEqual(ParseResult.getSearchTree(types, true), { + keys: {}, + otherwise: [ab.ast, AB.ast], + candidates: [] + }) + }) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/ParseResultEffectful.test.ts b/repos/effect/packages/effect/test/Schema/ParseResultEffectful.test.ts new file mode 100644 index 0000000..fb3877a --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/ParseResultEffectful.test.ts @@ -0,0 +1,183 @@ +import { describe, it } from "@effect/vitest" +import { Effect, ParseResult, Schema } from "effect" +import * as Util from "./TestUtils.js" + +const EffectfulStringFailure = Schema.transformOrFail(Schema.String, Schema.String, { + strict: true, + decode: (actual, _, ast) => + actual === "" + ? Effect.fail(new ParseResult.Type(ast, actual, "Empty String")) + : Effect.succeed(actual), + encode: Effect.succeed +}).annotations({ identifier: "EffectfulStringFailure" }) + +describe("Effectful Schemas", () => { + describe("TupleType", () => { + it("elements", async () => { + const schema = Schema.Tuple(EffectfulStringFailure, Schema.String) + + await Util.assertions.decoding.succeed(schema, ["a", "b"]) + + await Util.assertions.decoding.fail( + schema, + ["", "b"], + `readonly [EffectfulStringFailure, string] +└─ [0] + └─ EffectfulStringFailure + └─ Transformation process failure + └─ Empty String` + ) + await Util.assertions.decoding.fail( + schema, + ["", null], + `readonly [EffectfulStringFailure, string] +├─ [0] +│ └─ EffectfulStringFailure +│ └─ Transformation process failure +│ └─ Empty String +└─ [1] + └─ Expected string, actual null`, + { parseOptions: Util.ErrorsAll } + ) + }) + + it("rest", async () => { + const schema = Schema.Array(EffectfulStringFailure) + + await Util.assertions.decoding.succeed(schema, ["a", "b"]) + + await Util.assertions.decoding.fail( + schema, + ["", "b"], + `ReadonlyArray +└─ [0] + └─ EffectfulStringFailure + └─ Transformation process failure + └─ Empty String` + ) + await Util.assertions.decoding.fail( + schema, + ["", ""], + `ReadonlyArray +├─ [0] +│ └─ EffectfulStringFailure +│ └─ Transformation process failure +│ └─ Empty String +└─ [1] + └─ EffectfulStringFailure + └─ Transformation process failure + └─ Empty String`, + { parseOptions: Util.ErrorsAll } + ) + }) + + it("Rest & post rest elements", async () => { + const schema = Schema.Tuple([], Schema.String, EffectfulStringFailure, EffectfulStringFailure) + + await Util.assertions.decoding.succeed(schema, ["a", "b", "c"]) + + await Util.assertions.decoding.fail( + schema, + ["a", "", ""], + `readonly [...string[], EffectfulStringFailure, EffectfulStringFailure] +└─ [1] + └─ EffectfulStringFailure + └─ Transformation process failure + └─ Empty String` + ) + await Util.assertions.decoding.fail( + schema, + ["a", "", ""], + `readonly [...string[], EffectfulStringFailure, EffectfulStringFailure] +├─ [1] +│ └─ EffectfulStringFailure +│ └─ Transformation process failure +│ └─ Empty String +└─ [2] + └─ EffectfulStringFailure + └─ Transformation process failure + └─ Empty String`, + { parseOptions: Util.ErrorsAll } + ) + }) + }) + + describe("TypeLiteral", () => { + it("property signatures", async () => { + const schema = Schema.Struct({ + a: EffectfulStringFailure, + b: Schema.String + }) + + await Util.assertions.decoding.succeed(schema, { a: "a", b: "b" }) + + await Util.assertions.decoding.fail( + schema, + {}, + `{ readonly a: EffectfulStringFailure; readonly b: string } +└─ ["a"] + └─ is missing` + ) + await Util.assertions.decoding.fail( + schema, + { a: undefined }, + `{ readonly a: EffectfulStringFailure; readonly b: string } +└─ ["a"] + └─ EffectfulStringFailure + └─ Encoded side transformation failure + └─ Expected string, actual undefined` + ) + await Util.assertions.decoding.fail( + schema, + { a: "", b: "b" }, + `{ readonly a: EffectfulStringFailure; readonly b: string } +└─ ["a"] + └─ EffectfulStringFailure + └─ Transformation process failure + └─ Empty String` + ) + await Util.assertions.decoding.fail( + schema, + { a: "", b: null }, + `{ readonly a: EffectfulStringFailure; readonly b: string } +├─ ["a"] +│ └─ EffectfulStringFailure +│ └─ Transformation process failure +│ └─ Empty String +└─ ["b"] + └─ Expected string, actual null`, + { parseOptions: Util.ErrorsAll } + ) + }) + + it("index signatures", async () => { + const schema = Schema.Record({ key: Schema.String, value: EffectfulStringFailure }) + + await Util.assertions.decoding.succeed(schema, { a: "a", b: "b" }) + + await Util.assertions.decoding.fail( + schema, + { a: "" }, + `{ readonly [x: string]: EffectfulStringFailure } +└─ ["a"] + └─ EffectfulStringFailure + └─ Transformation process failure + └─ Empty String` + ) + await Util.assertions.decoding.fail( + schema, + { a: "", b: "" }, + `{ readonly [x: string]: EffectfulStringFailure } +├─ ["a"] +│ └─ EffectfulStringFailure +│ └─ Transformation process failure +│ └─ Empty String +└─ ["b"] + └─ EffectfulStringFailure + └─ Transformation process failure + └─ Empty String`, + { parseOptions: Util.ErrorsAll } + ) + }) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/ParseResultFormatter.test.ts b/repos/effect/packages/effect/test/Schema/ParseResultFormatter.test.ts new file mode 100644 index 0000000..9b2dac8 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/ParseResultFormatter.test.ts @@ -0,0 +1,1465 @@ +import { describe, it } from "@effect/vitest" +import { assertLeft, assertTrue, deepStrictEqual, strictEqual, throws } from "@effect/vitest/utils" +import * as Context from "effect/Context" +import * as Effect from "effect/Effect" +import * as Either from "effect/Either" +import { identity, pipe } from "effect/Function" +import * as Option from "effect/Option" +import * as ParseResult from "effect/ParseResult" +import * as S from "effect/Schema" +import type { ParseOptions } from "effect/SchemaAST" +import * as AST from "effect/SchemaAST" +import * as Util from "./TestUtils.js" + +const expectSyncTree = ( + schema: S.Schema, + input: unknown, + expected: string, + options?: { + readonly parseOptions?: AST.ParseOptions | undefined + } | undefined +) => { + const actual = S.decodeUnknownEither(schema)(input, options?.parseOptions).pipe( + Either.mapLeft((e) => ParseResult.TreeFormatter.formatIssueSync(e.issue)) + ) + assertLeft(actual, expected) +} + +const expectSyncIssues = ( + schema: S.Schema, + input: unknown, + expected: ReadonlyArray +) => { + const options: ParseOptions = { errors: "all", onExcessProperty: "error" } + const actual = S.decodeUnknownEither(schema)(input, options).pipe( + Either.mapLeft((e) => ParseResult.ArrayFormatter.formatIssueSync(e.issue)) + ) + assertLeft(actual, expected) +} + +const expectAsyncTree = async ( + schema: S.Schema, + input: unknown, + expected: string, + options?: { + readonly parseOptions?: AST.ParseOptions | undefined + } | undefined +) => { + const result = S.decodeUnknownEither(schema)(input, options?.parseOptions) + assertTrue(Either.isLeft(result)) + const actualEffect = ParseResult.TreeFormatter.formatIssue(result.left.issue) + assertTrue(Effect.isEffect(actualEffect)) + throws(() => Effect.runSync(actualEffect)) + await Effect.runPromise(actualEffect).then((actual) => { + strictEqual(actual, expected) + }) +} + +const expectAsyncIssues = async ( + schema: S.Schema, + input: unknown, + expected: ReadonlyArray +) => { + const options: ParseOptions = { errors: "all", onExcessProperty: "error" } + const result = S.decodeUnknownEither(schema)(input, options) + assertTrue(Either.isLeft(result)) + const actualEffect = ParseResult.ArrayFormatter.formatIssue(result.left.issue) + assertTrue(Effect.isEffect(actualEffect)) + throws(() => Effect.runSync(actualEffect)) + await Effect.runPromise(actualEffect).then((actual) => { + deepStrictEqual(actual, expected) + }) +} + +describe("Formatters output", () => { + it("Effect async message", async () => { + const EffectAsyncMessage = S.String.annotations({ + message: () => + Effect.gen(function*() { + yield* Effect.sleep("10 millis") + return "custom message" + }) + }) + const schema = EffectAsyncMessage + const input = null + await expectAsyncTree( + schema, + input, + "custom message" + ) + await expectAsyncIssues( + schema, + input, + [{ + _tag: "Type", + path: [], + message: "custom message" + }] + ) + }) + + it("Effect sync messages", () => { + const EffectSyncMessage = S.String.annotations({ + message: () => Effect.succeed(1).pipe(Effect.as("custom message")) + }) + const schema = EffectSyncMessage + const input = null + expectSyncTree( + schema, + input, + "custom message" + ) + expectSyncIssues( + schema, + input, + [{ + _tag: "Type", + path: [], + message: "custom message" + }] + ) + }) + + describe("Forbidden", () => { + it("default message", () => { + const schema = Util.AsyncStringWithoutIdentifier + const input = "" + expectSyncTree( + schema, + input, + `(string <-> string) +└─ cannot be be resolved synchronously, this is caused by using runSync on an effect that performs async work` + ) + expectSyncIssues(schema, input, [{ + _tag: "Forbidden", + path: [], + message: + `cannot be be resolved synchronously, this is caused by using runSync on an effect that performs async work` + }]) + }) + + it("default message with identifier", () => { + const schema = Util.AsyncString + const input = "" + expectSyncTree( + schema, + input, + `AsyncString +└─ cannot be be resolved synchronously, this is caused by using runSync on an effect that performs async work` + ) + expectSyncIssues(schema, input, [{ + _tag: "Forbidden", + path: [], + message: + `cannot be be resolved synchronously, this is caused by using runSync on an effect that performs async work` + }]) + }) + + it("custom message (override=false)", () => { + const schema = Util.AsyncString.annotations({ message: () => "custom message" }) + const input = "" + expectSyncTree( + schema, + input, + `(string <-> string) +└─ cannot be be resolved synchronously, this is caused by using runSync on an effect that performs async work` + ) + expectSyncIssues(schema, input, [{ + _tag: "Forbidden", + path: [], + message: + `cannot be be resolved synchronously, this is caused by using runSync on an effect that performs async work` + }]) + }) + + it("custom message (override=true)", () => { + const schema = Util.AsyncString.annotations({ + message: () => ({ message: "custom message", override: true }) + }) + const input = "" + expectSyncTree( + schema, + input, + `(string <-> string) +└─ cannot be be resolved synchronously, this is caused by using runSync on an effect that performs async work` + ) + expectSyncIssues(schema, input, [{ + _tag: "Forbidden", + path: [], + message: + `cannot be be resolved synchronously, this is caused by using runSync on an effect that performs async work` + }]) + }) + }) + + describe("sync messages", () => { + describe("Missing", () => { + it("default message", () => { + const schema = S.Struct({ a: S.String }) + const input = {} + expectSyncTree( + schema, + input, + `{ readonly a: string } +└─ ["a"] + └─ is missing` + ) + expectSyncIssues(schema, input, [{ + _tag: "Missing", + path: ["a"], + message: "is missing" + }]) + }) + + it("default message with parent identifier", () => { + const schema = S.Struct({ a: S.String }).annotations({ identifier: "identifier" }) + const input = {} + expectSyncTree( + schema, + input, + `identifier +└─ ["a"] + └─ is missing` + ) + expectSyncIssues(schema, input, [{ + _tag: "Missing", + path: ["a"], + message: "is missing" + }]) + }) + + it("parent custom message with override=true", () => { + const schema = S.Struct({ a: S.String }).annotations({ + message: () => ({ message: "custom message", override: true }) + }) + const input = {} + expectSyncTree( + schema, + input, + "custom message" + ) + expectSyncIssues(schema, input, [{ + _tag: "Composite", + path: [], + message: "custom message" + }]) + }) + + describe("missing message", () => { + describe("Struct", () => { + it("PropertySignatureDeclaration", () => { + const schema = S.Struct({ + a: S.propertySignature(S.String).annotations({ + missingMessage: () => "a80b642a-729f-4676-ba6a-235964afd52b" + }) + }) + const input = {} + expectSyncTree( + schema, + input, + `{ readonly a: string } +└─ ["a"] + └─ a80b642a-729f-4676-ba6a-235964afd52b` + ) + expectSyncIssues(schema, input, [{ + _tag: "Missing", + path: ["a"], + message: "a80b642a-729f-4676-ba6a-235964afd52b" + }]) + }) + + it("PropertySignatureDeclaration + PropertySignatureTransformation", () => { + const schema = S.Struct({ + a: S.propertySignature(S.String).annotations({ + missingMessage: () => "1ff9f37a-1f50-4ee2-906d-e824067d4cf7" + }), + b: S.propertySignature(S.String).annotations({ + missingMessage: () => "132f0e48-ae12-4bbb-8473-3dd433de2eb0" + }).pipe(S.fromKey("c")) + }) + const input = {} + expectSyncTree( + schema, + input, + `(Struct (Encoded side) <-> Struct (Type side)) +└─ Encoded side transformation failure + └─ Struct (Encoded side) + ├─ ["a"] + │ └─ 1ff9f37a-1f50-4ee2-906d-e824067d4cf7 + └─ ["c"] + └─ 132f0e48-ae12-4bbb-8473-3dd433de2eb0`, + { parseOptions: Util.ErrorsAll } + ) + expectSyncIssues(schema, input, [{ + _tag: "Missing", + path: ["a"], + message: "1ff9f37a-1f50-4ee2-906d-e824067d4cf7" + }, { + _tag: "Missing", + path: ["c"], + message: "132f0e48-ae12-4bbb-8473-3dd433de2eb0" + }]) + }) + }) + + describe("Tuple", () => { + it("e", () => { + const schema = S.make( + new AST.TupleType( + [ + new AST.OptionalType(AST.stringKeyword, false, { + [AST.MissingMessageAnnotationId]: () => "my missing message" + }) + ], + [], + true + ) + ) + const input: Array = [] + expectSyncTree( + schema, + input, + `readonly [string] +└─ [0] + └─ my missing message` + ) + expectSyncIssues(schema, input, [{ + _tag: "Missing", + path: [0], + message: "my missing message" + }]) + }) + + it("r + e", () => { + const schema = S.Tuple( + [], + S.String, + S.element(S.String).annotations({ [AST.MissingMessageAnnotationId]: () => "my missing message" }) + ) + const input: Array = [] + expectSyncTree( + schema, + input, + `readonly [...string[], string] +└─ [0] + └─ my missing message` + ) + expectSyncIssues(schema, input, [{ + _tag: "Missing", + path: [0], + message: "my missing message" + }]) + }) + }) + }) + }) + + describe("Unexpected", () => { + it("default message", () => { + const schema = S.Struct({ a: S.String }) + const input = { a: "a", b: 1 } + expectSyncTree( + schema, + input, + `{ readonly a: string } +└─ ["b"] + └─ is unexpected, expected: "a"`, + { parseOptions: Util.onExcessPropertyError } + ) + expectSyncIssues(schema, input, [{ + _tag: "Unexpected", + path: ["b"], + message: `is unexpected, expected: "a"` + }]) + }) + + it("default message with parent identifier", () => { + const schema = S.Struct({ a: S.String }).annotations({ identifier: "identifier" }) + const input = { a: "a", b: 1 } + expectSyncTree( + schema, + input, + `identifier +└─ ["b"] + └─ is unexpected, expected: "a"`, + { parseOptions: Util.onExcessPropertyError } + ) + expectSyncIssues(schema, input, [{ + _tag: "Unexpected", + path: ["b"], + message: `is unexpected, expected: "a"` + }]) + }) + + it("parent custom message with override=true", () => { + const schema = S.Struct({ a: S.String }).annotations({ + message: () => ({ message: "custom message", override: true }) + }) + const input = { a: "a", b: 1 } + expectSyncTree( + schema, + input, + "custom message", + { parseOptions: Util.onExcessPropertyError } + ) + expectSyncIssues(schema, input, [{ + _tag: "Composite", + path: [], + message: "custom message" + }]) + }) + }) + + describe("Declaration", () => { + it("default message", () => { + const schema = S.OptionFromSelf(S.String) + const input = null + expectSyncTree( + schema, + input, + "Expected Option, actual null" + ) + expectSyncIssues(schema, input, [{ + _tag: "Type", + path: [], + message: "Expected Option, actual null" + }]) + }) + + it("default message with identifier", () => { + const schema = S.OptionFromSelf(S.String).annotations({ identifier: "identifier" }) + const input = null + expectSyncTree( + schema, + input, + "Expected identifier, actual null" + ) + expectSyncIssues(schema, input, [{ + _tag: "Type", + path: [], + message: "Expected identifier, actual null" + }]) + }) + + it("custom message (override=false)", () => { + const schema = S.OptionFromSelf(S.String).annotations({ message: () => "custom message" }) + const input = Option.some(1) + expectSyncTree( + schema, + input, + `Option +└─ Expected string, actual 1` + ) + expectSyncIssues(schema, input, [{ + _tag: "Type", + path: [], + message: "Expected string, actual 1" + }]) + }) + + it("custom message (override=true)", () => { + const schema = S.OptionFromSelf(S.String).annotations({ + message: () => ({ message: "custom message", override: true }) + }) + const input = Option.some(1) + expectSyncTree( + schema, + input, + "custom message" + ) + expectSyncIssues(schema, input, [{ + _tag: "Composite", + path: [], + message: "custom message" + }]) + }) + }) + + describe("String", () => { + it("default message", () => { + const schema = S.String + const input = null + expectSyncTree( + schema, + input, + "Expected string, actual null" + ) + expectSyncIssues(schema, input, [{ + _tag: "Type", + path: [], + message: "Expected string, actual null" + }]) + }) + + it("default message with identifier", () => { + const schema = S.String.annotations({ identifier: "ID" }) + const input = null + expectSyncTree( + schema, + input, + "Expected ID, actual null" + ) + expectSyncIssues(schema, input, [{ + _tag: "Type", + path: [], + message: "Expected ID, actual null" + }]) + }) + + it("custom message", () => { + const schema = S.String.annotations({ message: () => "custom message" }) + const input = null + expectSyncTree( + schema, + input, + "custom message" + ) + expectSyncIssues(schema, input, [{ + _tag: "Type", + path: [], + message: "custom message" + }]) + }) + }) + + describe("Transformation", () => { + it("default message", () => { + const schema = S.transformOrFail( + S.String, + S.String, + { + strict: true, + decode: (s, _, ast) => ParseResult.fail(new ParseResult.Type(ast, s)), + encode: ParseResult.succeed + } + ) + const input = null + expectSyncTree( + schema, + input, + `(string <-> string) +└─ Encoded side transformation failure + └─ Expected string, actual null` + ) + expectSyncIssues(schema, input, [{ + _tag: "Type", + path: [], + message: "Expected string, actual null" + }]) + }) + + it("default message with identifier", () => { + const schema = S.transformOrFail( + S.String, + S.String, + { + strict: true, + decode: (s, _, ast) => ParseResult.fail(new ParseResult.Type(ast, s)), + encode: ParseResult.succeed + } + ).annotations({ identifier: "identifier" }) + const input = null + expectSyncTree( + schema, + input, + `identifier +└─ Encoded side transformation failure + └─ Expected string, actual null` + ) + expectSyncIssues(schema, input, [{ + _tag: "Type", + path: [], + message: "Expected string, actual null" + }]) + }) + + it("default message with message field (kind=Transformation)", () => { + const schema = S.transformOrFail( + S.String, + S.String, + { + strict: true, + decode: (s, _, ast) => ParseResult.fail(new ParseResult.Type(ast, s, "transformation failure")), + encode: ParseResult.succeed + } + ) + const input = "" + expectSyncTree( + schema, + input, + `(string <-> string) +└─ Transformation process failure + └─ transformation failure` + ) + expectSyncIssues(schema, input, [{ + _tag: "Transformation", + path: [], + message: "transformation failure" + }]) + }) + + it("custom message (kind=From, override=false)", () => { + const schema = S.transform( + S.String, + S.String, + { + strict: true, + decode: identity, + encode: identity + } + ).annotations({ message: () => "custom message" }) + const input = null + expectSyncTree( + schema, + input, + `(string <-> string) +└─ Encoded side transformation failure + └─ Expected string, actual null` + ) + expectSyncIssues(schema, input, [{ + _tag: "Type", + path: [], + message: "Expected string, actual null" + }]) + }) + + it("custom message (kind=From, override=true)", () => { + const schema = S.transform( + S.String, + S.String, + { + strict: true, + decode: identity, + encode: identity + } + ).annotations({ message: () => ({ message: "custom message", override: true }) }) + const input = null + expectSyncTree( + schema, + input, + "custom message" + ) + expectSyncIssues(schema, input, [{ + _tag: "Transformation", + path: [], + message: "custom message" + }]) + }) + + it("custom message with inner custom message (kind=From, override=false)", () => { + const schema = S.transform( + S.String.annotations({ message: () => "inner custom message" }), + S.String, + { + strict: true, + decode: identity, + encode: identity + } + ).annotations({ message: () => "custom message" }) + const input = null + expectSyncTree( + schema, + input, + "inner custom message" + ) + expectSyncIssues(schema, input, [{ + _tag: "Transformation", + path: [], + message: "inner custom message" + }]) + }) + + it("custom message with inner custom message (kind=From, override=true)", () => { + const schema = S.transform( + S.String.annotations({ message: () => "inner custom message" }), + S.String, + { + strict: true, + decode: identity, + encode: identity + } + ).annotations({ message: () => ({ message: "custom message", override: true }) }) + const input = null + expectSyncTree( + schema, + input, + "custom message" + ) + expectSyncIssues(schema, input, [{ + _tag: "Transformation", + path: [], + message: "custom message" + }]) + }) + + it("custom message (kind=To, override=false)", () => { + const schema = S.transform( + S.String, + S.NonEmptyString, + { + strict: true, + decode: identity, + encode: identity + } + ).annotations({ message: () => "custom message" }) + const input = "" + expectSyncTree( + schema, + input, + `(string <-> NonEmptyString) +└─ Type side transformation failure + └─ NonEmptyString + └─ Predicate refinement failure + └─ Expected a non empty string, actual ""` + ) + expectSyncIssues(schema, input, [{ + _tag: "Refinement", + path: [], + message: `Expected a non empty string, actual ""` + }]) + }) + + it("custom message (kind=To, override=true)", () => { + const schema = S.transform( + S.String, + S.NonEmptyString, + { + strict: true, + decode: identity, + encode: identity + } + ).annotations({ message: () => ({ message: "custom message", override: true }) }) + const input = "" + expectSyncTree( + schema, + input, + "custom message" + ) + expectSyncIssues(schema, input, [{ + _tag: "Transformation", + path: [], + message: "custom message" + }]) + }) + + it("custom message with inner custom message (kind=To, override=false)", () => { + const schema = S.transform( + S.String, + S.NonEmptyString.annotations({ message: () => "inner custom message" }), + { + strict: true, + decode: identity, + encode: identity + } + ).annotations({ message: () => "custom message" }) + const input = "" + expectSyncTree( + schema, + input, + "inner custom message" + ) + expectSyncIssues(schema, input, [{ + _tag: "Transformation", + path: [], + message: "inner custom message" + }]) + }) + + it("custom message with inner custom message (kind=To, override=true)", () => { + const schema = S.transform( + S.String, + S.NonEmptyString.annotations({ message: () => "inner custom message" }), + { + strict: true, + decode: identity, + encode: identity + } + ).annotations({ message: () => ({ message: "custom message", override: true }) }) + const input = "" + expectSyncTree( + schema, + input, + "custom message" + ) + expectSyncIssues(schema, input, [{ + _tag: "Transformation", + path: [], + message: "custom message" + }]) + }) + + it("custom message (kind=Transformation, override=false)", () => { + const schema = S.transformOrFail( + S.String, + S.String, + { + strict: true, + decode: (s, _, ast) => ParseResult.fail(new ParseResult.Type(ast, s, "message field")), + encode: ParseResult.succeed + } + ).annotations({ message: () => "custom message" }) + const input = "" + expectSyncTree( + schema, + input, + "custom message" + ) + expectSyncIssues(schema, input, [{ + _tag: "Transformation", + path: [], + message: "custom message" + }]) + }) + + it("custom message (kind=Transformation, override=true)", () => { + const schema = S.transformOrFail( + S.String, + S.String, + { + strict: true, + decode: (s, _, ast) => ParseResult.fail(new ParseResult.Type(ast, s, "message field")), + encode: ParseResult.succeed + } + ).annotations({ message: () => ({ message: "custom message", override: true }) }) + const input = "" + expectSyncTree( + schema, + input, + "custom message" + ) + expectSyncIssues(schema, input, [{ + _tag: "Transformation", + path: [], + message: "custom message" + }]) + }) + }) + + describe("Refinement", () => { + it("default message (kind=From)", () => { + const schema = S.String.pipe(S.minLength(1)) + const input = null + expectSyncTree( + schema, + input, + `minLength(1) +└─ From side refinement failure + └─ Expected string, actual null` + ) + expectSyncIssues(schema, input, [{ + _tag: "Type", + path: [], + message: "Expected string, actual null" + }]) + }) + + it("default message with identifier (kind=From)", () => { + const schema = S.String.pipe(S.minLength(1)).annotations({ identifier: "identifier" }) + const input = null + expectSyncTree( + schema, + input, + `identifier +└─ From side refinement failure + └─ Expected string, actual null` + ) + expectSyncIssues(schema, input, [{ + _tag: "Type", + path: [], + message: "Expected string, actual null" + }]) + }) + + it("default message (kind=Predicate)", () => { + const schema = S.String.pipe(S.minLength(1)) + const input = "" + expectSyncTree( + schema, + input, + `minLength(1) +└─ Predicate refinement failure + └─ Expected a string at least 1 character(s) long, actual ""` + ) + expectSyncIssues(schema, input, [{ + _tag: "Refinement", + path: [], + message: `Expected a string at least 1 character(s) long, actual ""` + }]) + }) + + it("default message with identifier (kind=Predicate)", () => { + const schema = S.String.pipe(S.minLength(1)).annotations({ identifier: "identifier" }) + const input = "" + expectSyncTree( + schema, + input, + `identifier +└─ Predicate refinement failure + └─ Expected a string at least 1 character(s) long, actual ""` + ) + expectSyncIssues(schema, input, [{ + _tag: "Refinement", + path: [], + message: `Expected a string at least 1 character(s) long, actual ""` + }]) + }) + + it("custom message (kind=From, override=false)", () => { + const schema = S.String.pipe(S.minLength(1)).annotations({ message: () => "custom message" }) + const input = null + expectSyncTree( + schema, + input, + `minLength(1) +└─ From side refinement failure + └─ Expected string, actual null` + ) + expectSyncIssues(schema, input, [{ + _tag: "Type", + path: [], + message: "Expected string, actual null" + }]) + }) + + it("custom message (kind=From, override=true)", () => { + const schema = S.String.pipe(S.minLength(1)).annotations({ + message: () => ({ message: "custom message", override: true }) + }) + const input = null + expectSyncTree( + schema, + input, + "custom message" + ) + expectSyncIssues(schema, input, [{ + _tag: "Refinement", + path: [], + message: "custom message" + }]) + }) + + it("custom message (kind=Predicate, override=false)", () => { + const schema = S.String.pipe(S.minLength(1)).annotations({ message: () => "custom message" }) + const input = "" + expectSyncTree( + schema, + input, + "custom message" + ) + expectSyncIssues(schema, input, [{ + _tag: "Refinement", + path: [], + message: "custom message" + }]) + }) + + it("custom message (kind=Predicate, override=true)", () => { + const schema = S.String.pipe(S.minLength(1)).annotations({ + message: () => ({ message: "custom message", override: true }) + }) + const input = "" + expectSyncTree( + schema, + input, + "custom message" + ) + expectSyncIssues(schema, input, [{ + _tag: "Refinement", + path: [], + message: "custom message" + }]) + }) + + it("custom message with inner custom message (kind=From, override=false)", () => { + const schema = S.String.pipe(S.minLength(1, { message: () => "inner custom message" }), S.maxLength(2)) + .annotations({ message: () => "custom message" }) + const input = "" + expectSyncTree( + schema, + input, + "inner custom message" + ) + expectSyncIssues(schema, input, [{ + _tag: "Refinement", + path: [], + message: "inner custom message" + }]) + }) + + it("custom message with inner custom message (kind=From, override=true)", () => { + const schema = S.String.pipe(S.minLength(1, { message: () => "inner custom message" }), S.maxLength(2)) + .annotations({ message: () => ({ message: "custom message", override: true }) }) + const input = "" + expectSyncTree( + schema, + input, + "custom message" + ) + expectSyncIssues(schema, input, [{ + _tag: "Refinement", + path: [], + message: "custom message" + }]) + }) + }) + + describe("Suspend", () => { + it("outer", () => { + type A = readonly [number, A | null] + const schema: S.Schema = S.suspend( // intended outer suspend + () => S.Tuple(S.Number, S.Union(schema, S.Literal(null))) + ) + + expectSyncTree( + schema, + null, + `Expected readonly [number, | null], actual null` + ) + expectSyncTree( + schema, + [1, undefined], + `readonly [number, | null] +└─ [1] + └─ | null + ├─ Expected readonly [number, | null], actual undefined + └─ Expected null, actual undefined` + ) + }) + + it("inner", () => { + type A = readonly [number, A | null] + const schema: S.Schema = S.Tuple( + S.Number, + S.Union(S.suspend(() => schema), S.Literal(null)) + ) + + expectSyncTree( + schema, + null, + `Expected readonly [number, | null], actual null` + ) + expectSyncTree( + schema, + [1, undefined], + `readonly [number, | null] +└─ [1] + └─ | null + ├─ Expected readonly [number, | null], actual undefined + └─ Expected null, actual undefined` + ) + }) + }) + + describe("Union", () => { + it("default message", () => { + const schema = S.Union(S.String, S.Number) + const input = null + expectSyncTree( + schema, + input, + `string | number +├─ Expected string, actual null +└─ Expected number, actual null` + ) + expectSyncIssues(schema, input, [{ + _tag: "Type", + path: [], + message: "Expected string, actual null" + }, { + _tag: "Type", + path: [], + message: "Expected number, actual null" + }]) + }) + + it("default message with identifier", () => { + const schema = S.Union(S.String, S.Number).annotations({ identifier: "identifier" }) + const input = null + expectSyncTree( + schema, + input, + `identifier +├─ Expected string, actual null +└─ Expected number, actual null` + ) + expectSyncIssues(schema, input, [{ + _tag: "Type", + path: [], + message: "Expected string, actual null" + }, { + _tag: "Type", + path: [], + message: "Expected number, actual null" + }]) + }) + + it("parent custom message with override=false", () => { + const schema = S.Union(S.String, S.Number).annotations({ + message: () => "custom message" + }) + const input = null + expectSyncTree( + schema, + input, + `string | number +├─ Expected string, actual null +└─ Expected number, actual null` + ) + expectSyncIssues(schema, input, [{ + _tag: "Type", + path: [], + message: "Expected string, actual null" + }, { + _tag: "Type", + path: [], + message: "Expected number, actual null" + }]) + }) + + it("parent custom message with override=true", () => { + const schema = S.Union(S.String, S.Number).annotations({ + message: () => ({ message: "custom message", override: true }) + }) + const input = null + expectSyncTree( + schema, + input, + "custom message" + ) + expectSyncIssues(schema, input, [{ + _tag: "Composite", + path: [], + message: "custom message" + }]) + }) + }) + + describe("Tuple", () => { + it("parent custom message with override=false", () => { + const schema = S.Tuple(S.String).annotations({ message: () => "custom message" }) + const input1 = [1] + expectSyncTree( + schema, + input1, + `readonly [string] +└─ [0] + └─ Expected string, actual 1` + ) + expectSyncIssues(schema, input1, [{ + _tag: "Type", + path: [0], + message: "Expected string, actual 1" + }]) + }) + + it("parent custom message with override=true", () => { + const schema = S.Tuple(S.String).annotations({ message: () => ({ message: "custom message", override: true }) }) + const input1 = [1] + expectSyncTree( + schema, + input1, + "custom message" + ) + expectSyncIssues(schema, input1, [{ + _tag: "Composite", + path: [], + message: "custom message" + }]) + }) + }) + + describe("Struct", () => { + it("parent custom message with override=false", () => { + const schema = S.Struct({ + as: pipe( + S.Array( + S.Struct({ + b: pipe( + S.String.annotations({ message: () => "type" }), + S.minLength(1, { message: () => "minLength" }), + S.maxLength(2, { message: () => "maxLength" }) + ) + }) + ).annotations({ identifier: "C" }), + S.minItems(1, { message: () => "minItems" }) + ).annotations({ identifier: "B" }) + }).annotations({ identifier: "A", message: () => "custom message" }) + const input1 = null + expectSyncTree( + schema, + input1, + "custom message" + ) + expectSyncIssues(schema, input1, [{ + _tag: "Type", + path: [], + message: "custom message" + }]) + + const input2 = { as: [] } + expectSyncTree( + schema, + input2, + `A +└─ ["as"] + └─ minItems` + ) + expectSyncIssues(schema, input2, [{ + _tag: "Refinement", + path: ["as"], + message: "minItems" + }]) + + const input3 = { as: [{ b: null }] } + expectSyncTree( + schema, + input3, + `A +└─ ["as"] + └─ B + └─ From side refinement failure + └─ C + └─ [0] + └─ { readonly b: minLength(1) & maxLength(2) } + └─ ["b"] + └─ type` + ) + expectSyncIssues(schema, input3, [{ + _tag: "Refinement", + path: ["as", 0, "b"], + message: "type" + }]) + + const input4 = { as: [{ b: "" }] } + expectSyncTree( + schema, + input4, + `A +└─ ["as"] + └─ B + └─ From side refinement failure + └─ C + └─ [0] + └─ { readonly b: minLength(1) & maxLength(2) } + └─ ["b"] + └─ minLength` + ) + expectSyncIssues(schema, input4, [{ + _tag: "Refinement", + path: ["as", 0, "b"], + message: "minLength" + }]) + + const input5 = { as: [{ b: "---" }] } + expectSyncTree( + schema, + input5, + `A +└─ ["as"] + └─ B + └─ From side refinement failure + └─ C + └─ [0] + └─ { readonly b: minLength(1) & maxLength(2) } + └─ ["b"] + └─ maxLength` + ) + expectSyncIssues(schema, input5, [{ + _tag: "Refinement", + path: ["as", 0, "b"], + message: "maxLength" + }]) + }) + + it("parent custom message with override=true", () => { + const schema = S.Struct({ + as: pipe( + S.Array( + S.Struct({ + b: pipe( + S.String.annotations({ message: () => "type" }), + S.minLength(1, { message: () => "minLength" }), + S.maxLength(2, { message: () => "maxLength" }) + ) + }) + ).annotations({ identifier: "C" }), + S.minItems(1, { message: () => "minItems" }) + ).annotations({ identifier: "B" }) + }).annotations({ identifier: "A", message: () => ({ message: "custom message", override: true }) }) + const input1 = null + expectSyncTree( + schema, + input1, + "custom message" + ) + expectSyncIssues(schema, input1, [{ + _tag: "Type", + path: [], + message: "custom message" + }]) + const input2 = { as: [] } + expectSyncTree( + schema, + input2, + "custom message" + ) + expectSyncIssues(schema, input2, [{ + _tag: "Composite", + path: [], + message: "custom message" + }]) + }) + }) + }) + + describe("handle identifiers", () => { + it("Struct", () => { + const schema = S.Struct({ + a: S.String.annotations({ identifier: "MyString1" }), + b: S.String.annotations({ identifier: "MyString2" }) + }).annotations({ identifier: "MySchema" }) + + expectSyncTree( + schema, + { a: 1, b: 2 }, + `MySchema +├─ ["a"] +│ └─ Expected MyString1, actual 1 +└─ ["b"] + └─ Expected MyString2, actual 2`, + { parseOptions: Util.ErrorsAll } + ) + }) + + describe("Suspend", () => { + it("outer", () => { + type A = readonly [number, A | null] + const schema: S.Schema = S.suspend( // intended outer suspend + () => S.Tuple(S.Number, S.Union(schema, S.Literal(null))) + ).annotations({ identifier: "A" }) + + expectSyncTree( + schema, + null, + `Expected readonly [number, A | null], actual null` + ) + expectSyncTree( + schema, + [1, undefined], + `readonly [number, A | null] +└─ [1] + └─ A | null + ├─ Expected readonly [number, A | null], actual undefined + └─ Expected null, actual undefined` + ) + }) + + it("inner/outer", () => { + type A = readonly [number, A | null] + const schema = S.Tuple( + S.Number, + S.Union(S.suspend((): S.Schema => schema), S.Literal(null)) + ).annotations({ identifier: "A" }) + + expectSyncTree( + schema, + null, + `Expected A, actual null` + ) + expectSyncTree( + schema, + [1, undefined], + `A +└─ [1] + └─ A | null + ├─ Expected A, actual undefined + └─ Expected null, actual undefined` + ) + }) + + it("inner/inner", () => { + type A = readonly [number, A | null] + const schema = S.Tuple( + S.Number, + S.Union(S.suspend((): S.Schema => schema).annotations({ identifier: "A" }), S.Literal(null)) + ) + + expectSyncTree( + schema, + null, + `Expected readonly [number, A | null], actual null` + ) + expectSyncTree( + schema, + [1, undefined], + `readonly [number, A | null] +└─ [1] + └─ A | null + ├─ Expected readonly [number, A | null], actual undefined + └─ Expected null, actual undefined` + ) + }) + }) + }) + + it("Effect as message", () => { + const translations = { + it: "Nome non valido", + en: "Invalid name" + } + + class Translator extends Context.Tag("Translator")() {} + + const Name = S.NonEmptyString.annotations({ + message: () => + Effect.gen(function*() { + const service = yield* Effect.serviceOption(Translator) + return Option.match(service, { + onNone: () => "Invalid string", + onSome: (translator) => translator.translations[translator.locale] + }) + }) + }) + + const result = S.decodeUnknownEither(Name)("") + + // no service + assertLeft( + Either.mapLeft(result, (error) => Effect.runSync(ParseResult.TreeFormatter.formatError(error))), + "Invalid string" + ) + + // it locale + assertLeft( + Either.mapLeft( + result, + (error) => + Effect.runSync( + ParseResult.TreeFormatter.formatError(error).pipe(Effect.provideService(Translator, { + locale: "it", + translations + })) + ) + ), + "Nome non valido" + ) + + // en locale + assertLeft( + Either.mapLeft( + result, + (error) => + Effect.runSync( + ParseResult.TreeFormatter.formatError(error).pipe(Effect.provideService(Translator, { + locale: "en", + translations + })) + ) + ), + "Invalid name" + ) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Pretty.test.ts b/repos/effect/packages/effect/test/Schema/Pretty.test.ts new file mode 100644 index 0000000..897dd9f --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Pretty.test.ts @@ -0,0 +1,495 @@ +import { describe, it } from "@effect/vitest" +import { strictEqual, throws } from "@effect/vitest/utils" +import { isUnknown } from "effect/Predicate" +import * as Pretty from "effect/Pretty" +import * as S from "effect/Schema" +import * as AST from "effect/SchemaAST" +import * as Util from "./TestUtils.js" + +describe("Pretty", () => { + it("make", () => { + const schema = S.NumberFromString + Util.assertions.pretty(schema, 1, "1") + }) + + it("make(S.encodedSchema(schema))", () => { + const schema = S.encodedSchema(S.NumberFromString) + Util.assertions.pretty(schema, "a", `"a"`) + }) + + it("should throw on declarations without annotations", () => { + const schema = S.declare(isUnknown) + throws( + () => Pretty.make(schema), + new Error(`Missing annotation +details: Generating a Pretty for this schema requires a "pretty" annotation +schema (Declaration): `) + ) + }) + + it("should throw on never", () => { + const schema = S.Never + const pretty = Pretty.make(schema) + throws(() => pretty("a" as any as never), new Error("Cannot pretty print a `never` value")) + }) + + it("the errors should disply a path", () => { + throws( + () => Pretty.make(S.Tuple(S.declare(isUnknown))), + new Error(`Missing annotation +at path: [0] +details: Generating a Pretty for this schema requires a "pretty" annotation +schema (Declaration): `) + ) + throws( + () => Pretty.make(S.Struct({ a: S.declare(isUnknown) })), + new Error(`Missing annotation +at path: ["a"] +details: Generating a Pretty for this schema requires a "pretty" annotation +schema (Declaration): `) + ) + }) + + it("should allow for custom compilers", () => { + const match: typeof Pretty.match = { + ...Pretty.match, + "BooleanKeyword": () => (b: boolean) => b ? "True" : "False" + } + const go = AST.getCompiler(match) + const pretty = (schema: S.Schema) => (a: A): string => go(schema.ast, [])(a) + strictEqual(pretty(S.Boolean)(true), `True`) + const schema = S.Tuple(S.String, S.Boolean) + strictEqual(pretty(schema)(["a", true]), `["a", True]`) + }) + + describe("templateLiteral", () => { + it("a${string}b", () => { + const schema = S.TemplateLiteral(S.Literal("a"), S.String, S.Literal("b")) + Util.assertions.pretty(schema, "acb", `"acb"`) + }) + }) + + it("unknown", () => { + const schema = S.Unknown + Util.assertions.pretty(schema, "a", `"a"`) + Util.assertions.pretty(schema, 1n, "1n") + }) + + it("string", () => { + const schema = S.String + Util.assertions.pretty(schema, "a", `"a"`) + }) + + it("number", () => { + const schema = S.Number + Util.assertions.pretty(schema, 1, "1") + Util.assertions.pretty(schema, NaN, "NaN") + Util.assertions.pretty(schema, Infinity, "Infinity") + Util.assertions.pretty(schema, -Infinity, "-Infinity") + }) + + it("boolean", () => { + const schema = S.Boolean + Util.assertions.pretty(schema, true, "true") + Util.assertions.pretty(schema, false, "false") + }) + + it("bigint", () => { + const schema = S.BigIntFromSelf + Util.assertions.pretty(schema, 1n, "1n") + }) + + it("symbol", () => { + const schema = S.SymbolFromSelf + Util.assertions.pretty(schema, Symbol.for("effect/test/a"), "Symbol(effect/test/a)") + }) + + it("void", () => { + const schema = S.Void + Util.assertions.pretty(schema, undefined, "void(0)") + }) + + describe("literal", () => { + it("null", () => { + const schema = S.Literal(null) + Util.assertions.pretty(schema, null, "null") + }) + + it("bigint", () => { + const schema = S.Literal(1n) + Util.assertions.pretty(schema, 1n, "1n") + }) + }) + + it("uniqueSymbolFromSelf", () => { + const a = Symbol.for("effect/Schema/test/a") + const schema = S.UniqueSymbolFromSelf(a) + Util.assertions.pretty(schema, a, "Symbol(effect/Schema/test/a)") + }) + + describe("enums", () => { + it("Numeric enums", () => { + enum Fruits { + Apple, + Banana + } + const schema = S.Enums(Fruits) + Util.assertions.pretty(schema, Fruits.Apple, "0") + Util.assertions.pretty(schema, Fruits.Banana, "1") + }) + + it("String enums", () => { + enum Fruits { + Apple = "apple", + Banana = "banana", + Cantaloupe = 0 + } + const schema = S.Enums(Fruits) + Util.assertions.pretty(schema, Fruits.Apple, `"apple"`) + Util.assertions.pretty(schema, Fruits.Banana, `"banana"`) + Util.assertions.pretty(schema, Fruits.Cantaloupe, "0") + }) + + it("Const enums", () => { + const Fruits = { + Apple: "apple", + Banana: "banana", + Cantaloupe: 3 + } as const + const schema = S.Enums(Fruits) + Util.assertions.pretty(schema, Fruits.Apple, `"apple"`) + Util.assertions.pretty(schema, Fruits.Banana, `"banana"`) + Util.assertions.pretty(schema, Fruits.Cantaloupe, "3") + }) + }) + + describe("struct", () => { + it("empty", () => { + const schema = S.Struct({}) + Util.assertions.pretty(schema, {}, "{}") + }) + + it("required fields", () => { + const schema = S.Struct({ a: S.String, b: S.Number }) + Util.assertions.pretty(schema, { a: "a", b: 1 }, `{ "a": "a", "b": 1 }`) + }) + + it("should not output exact optional property signatures", () => { + const schema = S.Struct({ a: S.optionalWith(S.Number, { exact: true }) }) + Util.assertions.pretty(schema, {}, "{}") + Util.assertions.pretty(schema, { a: 1 }, `{ "a": 1 }`) + }) + + it("should escape keys", () => { + const schema = S.Struct({ "-": S.Number }) + Util.assertions.pretty(schema, { "-": 1 }, `{ "-": 1 }`) + }) + + it("required property signature", () => { + const schema = S.Struct({ a: S.Number }) + Util.assertions.pretty(schema, { a: 1 }, `{ "a": 1 }`) + const x = { a: 1, b: "b" } + Util.assertions.pretty(schema, x, `{ "a": 1 }`) + }) + + it("required property signature with undefined", () => { + const schema = S.Struct({ a: S.Union(S.Number, S.Undefined) }) + Util.assertions.pretty(schema, { a: 1 }, `{ "a": 1 }`) + Util.assertions.pretty(schema, { a: undefined }, `{ "a": undefined }`) + const x = { a: 1, b: "b" } + Util.assertions.pretty(schema, x, `{ "a": 1 }`) + }) + + it("exact optional property signature", () => { + const schema = S.Struct({ a: S.optionalWith(S.Number, { exact: true }) }) + Util.assertions.pretty(schema, {}, "{}") + Util.assertions.pretty(schema, { a: 1 }, `{ "a": 1 }`) + const x = { a: 1, b: "b" } + Util.assertions.pretty(schema, x, `{ "a": 1 }`) + }) + + it("exact optional property signature with undefined", () => { + const schema = S.Struct({ a: S.optionalWith(S.Union(S.Number, S.Undefined), { exact: true }) }) + Util.assertions.pretty(schema, {}, "{}") + Util.assertions.pretty(schema, { a: 1 }, `{ "a": 1 }`) + const x = { a: 1, b: "b" } + Util.assertions.pretty(schema, x, `{ "a": 1 }`) + Util.assertions.pretty(schema, { a: undefined }, `{ "a": undefined }`) + }) + + it("extend: struct and record", () => { + const schema = S.Struct({ a: S.String }, S.Record({ key: S.String, value: S.Union(S.String, S.Number) })) + Util.assertions.pretty(schema, { a: "a" }, `{ "a": "a" }`) + Util.assertions.pretty(schema, { a: "a", b: "b", c: 1 }, `{ "a": "a", "b": "b", "c": 1 }`) + }) + }) + + describe("record", () => { + it("record(string, string)", () => { + const schema = S.Record({ key: S.String, value: S.String }) + Util.assertions.pretty(schema, { a: "a", b: "b" }, `{ "a": "a", "b": "b" }`) + }) + + it("record(symbol, string)", () => { + const a = Symbol.for("effect/Schema/test/a") + const schema = S.Record({ key: S.SymbolFromSelf, value: S.String }) + Util.assertions.pretty(schema, { [a]: "a" }, `{ Symbol(effect/Schema/test/a): "a" }`) + }) + }) + + describe("tuple", () => { + it("required element", () => { + const schema = S.Tuple(S.Number) + Util.assertions.pretty(schema, [1], `[1]`) + const x = [1, "b"] as any + Util.assertions.pretty(schema, x, `[1]`) + }) + + it("required element with undefined", () => { + const schema = S.Tuple(S.Union(S.Number, S.Undefined)) + Util.assertions.pretty(schema, [1], `[1]`) + Util.assertions.pretty(schema, [undefined], `[undefined]`) + const x = [1, "b"] as any + Util.assertions.pretty(schema, x, `[1]`) + }) + + it("optional element", () => { + const schema = S.Tuple(S.optionalElement(S.Number)) + Util.assertions.pretty(schema, [], `[]`) + Util.assertions.pretty(schema, [1], `[1]`) + const x = [1, "b"] as any + Util.assertions.pretty(schema, x, `[1]`) + }) + + it("optional element with undefined", () => { + const schema = S.Tuple(S.optionalElement(S.Union(S.Number, S.Undefined))) + Util.assertions.pretty(schema, [], `[]`) + Util.assertions.pretty(schema, [1], `[1]`) + const x = [1, "b"] as any + Util.assertions.pretty(schema, x, `[1]`) + Util.assertions.pretty(schema, [undefined], `[undefined]`) + }) + + it("baseline", () => { + const schema = S.Tuple(S.String, S.Number) + Util.assertions.pretty(schema, ["a", 1], `["a", 1]`) + }) + + it("empty tuple", () => { + const schema = S.Tuple() + Util.assertions.pretty(schema, [], `[]`) + }) + + it("optional elements", () => { + const schema = S.Tuple(S.optionalElement(S.String), S.optionalElement(S.Number)) + Util.assertions.pretty(schema, [], `[]`) + Util.assertions.pretty(schema, ["a"], `["a"]`) + Util.assertions.pretty(schema, ["a", 1], `["a", 1]`) + }) + + it("array", () => { + const schema = S.Array(S.String) + Util.assertions.pretty(schema, [], `[]`) + Util.assertions.pretty(schema, ["a"], `["a"]`) + }) + + it("post rest element", () => { + const schema = S.Tuple([], S.Number, S.Boolean) + Util.assertions.pretty(schema, [true], `[true]`) + Util.assertions.pretty(schema, [1, true], `[1, true]`) + Util.assertions.pretty(schema, [1, 2, true], `[1, 2, true]`) + Util.assertions.pretty(schema, [1, 2, 3, true], `[1, 2, 3, true]`) + }) + + it("post rest elements", () => { + const schema = S.Tuple([], S.Number, S.Boolean, S.Union(S.String, S.Undefined)) + Util.assertions.pretty(schema, [true, "c"], `[true, "c"]`) + Util.assertions.pretty(schema, [1, true, "c"], `[1, true, "c"]`) + Util.assertions.pretty(schema, [1, 2, true, "c"], `[1, 2, true, "c"]`) + Util.assertions.pretty(schema, [1, 2, 3, true, "c"], `[1, 2, 3, true, "c"]`) + Util.assertions.pretty(schema, [1, 2, 3, true, undefined], `[1, 2, 3, true, undefined]`) + }) + + it("post rest elements when rest is unknown", () => { + const schema = S.Tuple([], S.Unknown, S.Boolean) + Util.assertions.pretty(schema, [1, "a", 2, "b", true], `[1, "a", 2, "b", true]`) + Util.assertions.pretty(schema, [true], `[true]`) + }) + + it("all", () => { + const schema = S.Tuple([S.String], S.Number, S.Boolean) + Util.assertions.pretty(schema, ["a", true], `["a", true]`) + Util.assertions.pretty(schema, ["a", 1, true], `["a", 1, true]`) + Util.assertions.pretty(schema, ["a", 1, 2, true], `["a", 1, 2, true]`) + }) + + it("nonEmptyArray", () => { + const schema = S.NonEmptyArray(S.Number) + Util.assertions.pretty(schema, [1], `[1]`) + Util.assertions.pretty(schema, [1, 2], `[1, 2]`) + }) + + it("ReadonlyArray", () => { + const schema = S.Array(S.Unknown) + Util.assertions.pretty(schema, [], `[]`) + Util.assertions.pretty(schema, ["a", 1, true], `["a", 1, true]`) + }) + + it("ReadonlyArray", () => { + const schema = S.Array(S.Any) + Util.assertions.pretty(schema, [], `[]`) + Util.assertions.pretty(schema, ["a", 1, true], `["a", 1, true]`) + }) + }) + + describe("union", () => { + it("primitives", () => { + const schema = S.Union(S.String, S.Number) + Util.assertions.pretty(schema, "a", `"a"`) + Util.assertions.pretty(schema, 1, "1") + }) + + it("discriminated", () => { + const schema = S.Union( + S.Struct({ tag: S.Literal("a"), a: S.String }), + S.Struct({ tag: S.Literal("b"), b: S.Number }) + ) + Util.assertions.pretty(schema, { tag: "a", a: "-" }, `{ "tag": "a", "a": "-" }`) + Util.assertions.pretty(schema, { tag: "b", b: 1 }, `{ "tag": "b", "b": 1 }`) + }) + }) + + it("suspend", () => { + interface A { + readonly a: string + readonly as: ReadonlyArray + } + const A = S.Struct({ + a: S.String, + as: S.Array(S.suspend((): S.Schema => A)) + }) + const schema = A + Util.assertions.pretty(schema, { a: "a", as: [] }, `{ "a": "a", "as": [] }`) + }) + + it("transformation", () => { + const schema = S.Trim + Util.assertions.pretty(schema, "a", `"a"`) + }) + + describe("should handle annotations", () => { + const expectHook = (source: S.Schema) => { + const schema = source.annotations({ pretty: () => () => "custom pretty" }) + Util.assertions.pretty(schema, null as any, "custom pretty") + } + + it("void", () => { + expectHook(S.Void) + }) + + it("never", () => { + expectHook(S.Never) + }) + + it("literal", () => { + expectHook(S.Literal("a")) + }) + + it("symbol", () => { + expectHook(S.Symbol) + }) + + it("uniqueSymbolFromSelf", () => { + expectHook(S.UniqueSymbolFromSelf(Symbol.for("effect/schema/test/a"))) + }) + + it("templateLiteral", () => { + expectHook(S.TemplateLiteral(S.Literal("a"), S.String, S.Literal("b"))) + }) + + it("undefined", () => { + expectHook(S.Undefined) + }) + + it("unknown", () => { + expectHook(S.Unknown) + }) + + it("any", () => { + expectHook(S.Any) + }) + + it("object", () => { + expectHook(S.Object) + }) + + it("string", () => { + expectHook(S.String) + }) + + it("number", () => { + expectHook(S.Number) + }) + + it("bigintFromSelf", () => { + expectHook(S.BigIntFromSelf) + }) + + it("boolean", () => { + expectHook(S.Boolean) + }) + + it("enums", () => { + enum Fruits { + Apple, + Banana + } + expectHook(S.Enums(Fruits)) + }) + + it("tuple", () => { + expectHook(S.Tuple(S.String, S.Number)) + }) + + it("struct", () => { + expectHook(S.Struct({ a: S.String, b: S.Number })) + }) + + it("union", () => { + expectHook(S.Union(S.String, S.Number)) + }) + + it("suspend", () => { + interface A { + readonly a: string + readonly as: ReadonlyArray + } + const schema = S.Struct({ + a: S.String, + as: S.Array(S.suspend((): S.Schema => schema)) + }) + expectHook(schema) + }) + + it("refinement", () => { + expectHook(S.Int) + }) + + it("transformation", () => { + expectHook(S.NumberFromString) + }) + }) + + it("no matching schema error", () => { + const A = S.Struct({ a: S.optionalWith(S.String, { exact: true }) }) + const schema = S.Union(A, S.Number) + const x: {} = { a: undefined } + const input: typeof A.Type = x + throws( + () => Pretty.make(schema)(input), + new Error(`Unexpected Error +details: Cannot find a matching schema for {"a":undefined} +schema (Union): { readonly a?: string } | number`) + ) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/Any/Any.test.ts b/repos/effect/packages/effect/test/Schema/Schema/Any/Any.test.ts new file mode 100644 index 0000000..780b26f --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/Any/Any.test.ts @@ -0,0 +1,21 @@ +import { describe, it } from "@effect/vitest" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("Any", () => { + const schema = S.Any + + it("test roundtrip consistency", () => { + Util.assertions.testRoundtripConsistency(schema) + }) + + it("decoding", async () => { + await Util.assertions.decoding.succeed(schema, undefined) + await Util.assertions.decoding.succeed(schema, null) + await Util.assertions.decoding.succeed(schema, "a") + await Util.assertions.decoding.succeed(schema, 1) + await Util.assertions.decoding.succeed(schema, true) + await Util.assertions.decoding.succeed(schema, []) + await Util.assertions.decoding.succeed(schema, {}) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/Array/Array.test.ts b/repos/effect/packages/effect/test/Schema/Schema/Array/Array.test.ts new file mode 100644 index 0000000..34af326 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/Array/Array.test.ts @@ -0,0 +1,38 @@ +import { describe, it } from "@effect/vitest" +import { deepStrictEqual, strictEqual } from "@effect/vitest/utils" +import * as Either from "effect/Either" +import * as ParseResult from "effect/ParseResult" +import * as S from "effect/Schema" + +describe("Array", () => { + it("should expose the value", () => { + const schema = S.Array(S.String) + strictEqual(schema.value, S.String) + }) + + it("should compute the partial result", () => { + const schema = S.Array(S.Number) + const all = S.decodeUnknownEither(schema)([1, "a", 2, "b"], { errors: "all" }) + if (Either.isLeft(all)) { + const issue = all.left.issue + if (ParseResult.isComposite(issue)) { + deepStrictEqual(issue.output, [1, 2]) + } else { + throw new Error("expected an And") + } + } else { + throw new Error("expected a Left") + } + const first = S.decodeUnknownEither(schema)([1, "a", 2, "b"], { errors: "first" }) + if (Either.isLeft(first)) { + const issue = first.left.issue + if (ParseResult.isComposite(issue)) { + deepStrictEqual(issue.output, [1]) + } else { + throw new Error("expected an And") + } + } else { + throw new Error("expected a Left") + } + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/Array/head.test.ts b/repos/effect/packages/effect/test/Schema/Schema/Array/head.test.ts new file mode 100644 index 0000000..af9469a --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/Array/head.test.ts @@ -0,0 +1,29 @@ +import { describe, it } from "@effect/vitest" +import * as Option from "effect/Option" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("head", () => { + it("decoding", async () => { + const schema = S.head(S.Array(S.NumberFromString)) + await Util.assertions.decoding.succeed(schema, [], Option.none()) + await Util.assertions.decoding.succeed(schema, ["1"], Option.some(1)) + await Util.assertions.decoding.fail( + schema, + ["a"], + `(ReadonlyArray <-> Option) +└─ Encoded side transformation failure + └─ ReadonlyArray + └─ [0] + └─ NumberFromString + └─ Transformation process failure + └─ Unable to decode "a" into a number` + ) + }) + + it("encoding", async () => { + const schema = S.head(S.Array(S.NumberFromString)) + await Util.assertions.encoding.succeed(schema, Option.none(), []) + await Util.assertions.encoding.succeed(schema, Option.some(1), ["1"]) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/Array/headNonEmpty.test.ts b/repos/effect/packages/effect/test/Schema/Schema/Array/headNonEmpty.test.ts new file mode 100644 index 0000000..c49b292 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/Array/headNonEmpty.test.ts @@ -0,0 +1,26 @@ +import { describe, it } from "@effect/vitest" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("headNonEmpty", () => { + it("decoding", async () => { + const schema = S.headNonEmpty(S.NonEmptyArray(S.NumberFromString)) + await Util.assertions.decoding.succeed(schema, ["1"], 1) + await Util.assertions.decoding.fail( + schema, + ["a"], + `(readonly [NumberFromString, ...NumberFromString[]] <-> number | number) +└─ Encoded side transformation failure + └─ readonly [NumberFromString, ...NumberFromString[]] + └─ [0] + └─ NumberFromString + └─ Transformation process failure + └─ Unable to decode "a" into a number` + ) + }) + + it("encoding", async () => { + const schema = S.headNonEmpty(S.NonEmptyArray(S.NumberFromString)) + await Util.assertions.encoding.succeed(schema, 1, ["1"]) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/Array/headOrElse.test.ts b/repos/effect/packages/effect/test/Schema/Schema/Array/headOrElse.test.ts new file mode 100644 index 0000000..e444ac7 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/Array/headOrElse.test.ts @@ -0,0 +1,71 @@ +import { describe, it } from "@effect/vitest" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("headOrElse", () => { + it("decoding (without fallback)", async () => { + const schema = S.headOrElse(S.Array(S.NumberFromString)) + await Util.assertions.decoding.succeed(schema, ["1"], 1) + await Util.assertions.decoding.fail( + schema, + [], + `(ReadonlyArray <-> number) +└─ Transformation process failure + └─ Unable to retrieve the first element of an empty array` + ) + await Util.assertions.decoding.fail( + schema, + ["a"], + `(ReadonlyArray <-> number) +└─ Encoded side transformation failure + └─ ReadonlyArray + └─ [0] + └─ NumberFromString + └─ Transformation process failure + └─ Unable to decode "a" into a number` + ) + }) + + it("decoding (with fallback)", async () => { + const schema = S.headOrElse(S.Array(S.NumberFromString), () => 0) + await Util.assertions.decoding.succeed(schema, ["1"], 1) + await Util.assertions.decoding.succeed(schema, [], 0) + await Util.assertions.decoding.fail( + schema, + ["a"], + `(ReadonlyArray <-> number) +└─ Encoded side transformation failure + └─ ReadonlyArray + └─ [0] + └─ NumberFromString + └─ Transformation process failure + └─ Unable to decode "a" into a number` + ) + + const schema2 = S.Array(S.NumberFromString).pipe(S.headOrElse(() => 0)) + await Util.assertions.decoding.succeed(schema2, ["1"], 1) + await Util.assertions.decoding.succeed(schema2, [], 0) + }) + + it("decoding (struct)", async () => { + const schema = S.headOrElse( + S.Array( + S.Struct({ + id: S.String, + data: S.parseJson() + }) + ) + ) + await Util.assertions.decoding.succeed(schema, [ + { + id: "1", + data: "{\"a\":\"a\"}" + } + ], { id: "1", data: { a: "a" } }) + }) + + it("encoding", async () => { + const schema = S.headOrElse(S.Array(S.Number)) + await Util.assertions.encoding.succeed(schema, 1, [1]) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/Array/itemsCount.test.ts b/repos/effect/packages/effect/test/Schema/Schema/Array/itemsCount.test.ts new file mode 100644 index 0000000..f75c3c5 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/Array/itemsCount.test.ts @@ -0,0 +1,84 @@ +import { describe, it } from "@effect/vitest" +import { throws } from "@effect/vitest/utils" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("itemsCount", () => { + it("should throw for invalid argument", () => { + throws( + () => S.Array(S.Number).pipe(S.itemsCount(-1)), + new Error(`Invalid Argument +details: Expected an integer greater than or equal to 0, actual -1`) + ) + }) + + it("should allow 0 as a valid argument", async () => { + const schema = S.Array(S.Number).pipe(S.itemsCount(0)) + await Util.assertions.decoding.succeed(schema, []) + await Util.assertions.decoding.fail( + schema, + [1], + `itemsCount(0) +└─ Predicate refinement failure + └─ Expected an array of exactly 0 item(s), actual [1]` + ) + }) + + describe("decoding", () => { + it("Array", async () => { + const schema = S.Array(S.Number).pipe(S.itemsCount(2)) + + await Util.assertions.decoding.succeed(schema, [1, 2]) + await Util.assertions.decoding.fail( + schema, + [], + `itemsCount(2) +└─ Predicate refinement failure + └─ Expected an array of exactly 2 item(s), actual []` + ) + await Util.assertions.decoding.fail( + schema, + [1], + `itemsCount(2) +└─ Predicate refinement failure + └─ Expected an array of exactly 2 item(s), actual [1]` + ) + await Util.assertions.decoding.fail( + schema, + [1, 2, 3], + `itemsCount(2) +└─ Predicate refinement failure + └─ Expected an array of exactly 2 item(s), actual [1,2,3]` + ) + }) + + it("NonEmptyArray", async () => { + const schema = S.NonEmptyArray(S.Number).pipe(S.itemsCount(2)) + + await Util.assertions.decoding.succeed(schema, [1, 2]) + await Util.assertions.decoding.fail( + schema, + [], + `itemsCount(2) +└─ From side refinement failure + └─ readonly [number, ...number[]] + └─ [0] + └─ is missing` + ) + await Util.assertions.decoding.fail( + schema, + [1], + `itemsCount(2) +└─ Predicate refinement failure + └─ Expected an array of exactly 2 item(s), actual [1]` + ) + await Util.assertions.decoding.fail( + schema, + [1, 2, 3], + `itemsCount(2) +└─ Predicate refinement failure + └─ Expected an array of exactly 2 item(s), actual [1,2,3]` + ) + }) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/Array/maxItems.test.ts b/repos/effect/packages/effect/test/Schema/Schema/Array/maxItems.test.ts new file mode 100644 index 0000000..b609bea --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/Array/maxItems.test.ts @@ -0,0 +1,54 @@ +import { describe, it } from "@effect/vitest" +import { throws } from "@effect/vitest/utils" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("maxItems", () => { + it("should throw for invalid argument", () => { + throws( + () => S.Array(S.Number).pipe(S.maxItems(-1)), + new Error(`Invalid Argument +details: Expected an integer greater than or equal to 1, actual -1`) + ) + }) + + describe("decoding", () => { + it("Array", async () => { + const schema = S.Array(S.Number).pipe(S.maxItems(2)) + + await Util.assertions.decoding.succeed(schema, []) + await Util.assertions.decoding.succeed(schema, [1]) + await Util.assertions.decoding.succeed(schema, [1, 2]) + await Util.assertions.decoding.fail( + schema, + [1, 2, 3], + `maxItems(2) +└─ Predicate refinement failure + └─ Expected an array of at most 2 item(s), actual [1,2,3]` + ) + }) + + it("NonEmptyArray", async () => { + const schema = S.NonEmptyArray(S.Number).pipe(S.maxItems(2)) + + await Util.assertions.decoding.succeed(schema, [1]) + await Util.assertions.decoding.succeed(schema, [1, 2]) + await Util.assertions.decoding.fail( + schema, + [], + `maxItems(2) +└─ From side refinement failure + └─ readonly [number, ...number[]] + └─ [0] + └─ is missing` + ) + await Util.assertions.decoding.fail( + schema, + [1, 2, 3], + `maxItems(2) +└─ Predicate refinement failure + └─ Expected an array of at most 2 item(s), actual [1,2,3]` + ) + }) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/Array/minItems.test.ts b/repos/effect/packages/effect/test/Schema/Schema/Array/minItems.test.ts new file mode 100644 index 0000000..c1b8caf --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/Array/minItems.test.ts @@ -0,0 +1,60 @@ +import { describe, it } from "@effect/vitest" +import { throws } from "@effect/vitest/utils" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("minItems", () => { + it("should throw for invalid argument", () => { + throws( + () => S.Array(S.Number).pipe(S.minItems(-1)), + new Error(`Invalid Argument +details: Expected an integer greater than or equal to 1, actual -1`) + ) + }) + + describe("decoding", () => { + it("Array", async () => { + const schema = S.Array(S.Number).pipe(S.minItems(2)) + + await Util.assertions.decoding.succeed(schema, [1, 2]) + await Util.assertions.decoding.succeed(schema, [1, 2, 3]) + await Util.assertions.decoding.fail( + schema, + [], + `minItems(2) +└─ Predicate refinement failure + └─ Expected an array of at least 2 item(s), actual []` + ) + await Util.assertions.decoding.fail( + schema, + [1], + `minItems(2) +└─ Predicate refinement failure + └─ Expected an array of at least 2 item(s), actual [1]` + ) + }) + + it("NonEmptyArray", async () => { + const schema = S.NonEmptyArray(S.Number).pipe(S.minItems(2)) + + await Util.assertions.decoding.succeed(schema, [1, 2]) + await Util.assertions.decoding.succeed(schema, [1, 2, 3]) + await Util.assertions.decoding.fail( + schema, + [], + `minItems(2) +└─ From side refinement failure + └─ readonly [number, ...number[]] + └─ [0] + └─ is missing` + ) + await Util.assertions.decoding.fail( + schema, + [1], + `minItems(2) +└─ Predicate refinement failure + └─ Expected an array of at least 2 item(s), actual [1]` + ) + }) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/ArrayEnsure.test.ts b/repos/effect/packages/effect/test/Schema/Schema/ArrayEnsure.test.ts new file mode 100644 index 0000000..8f24c26 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/ArrayEnsure.test.ts @@ -0,0 +1,53 @@ +import { describe, it } from "@effect/vitest" +import * as S from "effect/Schema" +import * as Util from "../TestUtils.js" + +describe("ArrayEnsure", () => { + it("decode non-array", async () => { + const schema = S.ArrayEnsure(S.NumberFromString) + await Util.assertions.decoding.succeed(schema, "123", [123]) + await Util.assertions.decoding.fail( + schema, + null, + `(NumberFromString | ReadonlyArray <-> ReadonlyArray) +└─ Encoded side transformation failure + └─ NumberFromString | ReadonlyArray + ├─ NumberFromString + │ └─ Encoded side transformation failure + │ └─ Expected string, actual null + └─ Expected ReadonlyArray, actual null` + ) + }) + + it("decode empty array", async () => { + const schema = S.ArrayEnsure(S.NumberFromString) + await Util.assertions.decoding.succeed(schema, [], []) + }) + + it("decode array", async () => { + const schema = S.ArrayEnsure(S.NumberFromString) + await Util.assertions.decoding.succeed(schema, ["123"], [123]) + await Util.assertions.decoding.fail( + schema, + [null], + `(NumberFromString | ReadonlyArray <-> ReadonlyArray) +└─ Encoded side transformation failure + └─ NumberFromString | ReadonlyArray + ├─ NumberFromString + │ └─ Encoded side transformation failure + │ └─ Expected string, actual [null] + └─ ReadonlyArray + └─ [0] + └─ NumberFromString + └─ Encoded side transformation failure + └─ Expected string, actual null` + ) + }) + + it("encode", async () => { + const schema = S.ArrayEnsure(S.NumberFromString) + await Util.assertions.encoding.succeed(schema, [], []) + await Util.assertions.encoding.succeed(schema, [123], "123") + await Util.assertions.encoding.succeed(schema, [1, 2, 3], ["1", "2", "3"]) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/ArrayFormatterIssue/ArrayFormatterIssue.test.ts b/repos/effect/packages/effect/test/Schema/Schema/ArrayFormatterIssue/ArrayFormatterIssue.test.ts new file mode 100644 index 0000000..e9284bb --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/ArrayFormatterIssue/ArrayFormatterIssue.test.ts @@ -0,0 +1,11 @@ +import { describe, it } from "@effect/vitest" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("ArrayFormatterIssue", () => { + const schema = S.ArrayFormatterIssue + + it("test roundtrip consistency", () => { + Util.assertions.testRoundtripConsistency(schema) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/BigDecimal/BigDecimal.test.ts b/repos/effect/packages/effect/test/Schema/Schema/BigDecimal/BigDecimal.test.ts new file mode 100644 index 0000000..a1ca328 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/BigDecimal/BigDecimal.test.ts @@ -0,0 +1,51 @@ +import { describe, it } from "@effect/vitest" +import { BigDecimal } from "effect" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("BigDecimal", () => { + const schema = S.BigDecimal + + it("decoding", async () => { + await Util.assertions.decoding.succeed( + schema, + "2", + BigDecimal.normalize(BigDecimal.make(2n, 0)) + ) + await Util.assertions.decoding.succeed( + schema, + "0.123", + BigDecimal.normalize(BigDecimal.make(123n, 3)) + ) + await Util.assertions.decoding.succeed( + schema, + "", + BigDecimal.normalize(BigDecimal.make(0n, 0)) + ) + await Util.assertions.decoding.fail( + schema, + "abc", + `BigDecimal +└─ Transformation process failure + └─ Unable to decode "abc" into a BigDecimal` + ) + }) + + it("encoding", async () => { + await Util.assertions.encoding.succeed( + schema, + BigDecimal.make(2n, 0), + "2" + ) + await Util.assertions.encoding.succeed( + schema, + BigDecimal.make(123n, 3), + "0.123" + ) + await Util.assertions.encoding.succeed( + schema, + BigDecimal.make(0n, 0), + "0" + ) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/BigDecimal/BigDecimalFromNumber.test.ts b/repos/effect/packages/effect/test/Schema/Schema/BigDecimal/BigDecimalFromNumber.test.ts new file mode 100644 index 0000000..f40d68d --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/BigDecimal/BigDecimalFromNumber.test.ts @@ -0,0 +1,51 @@ +import { describe, it } from "@effect/vitest" +import { BigDecimal } from "effect" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("BigDecimalFromNumber", () => { + const schema = S.BigDecimalFromNumber + + it("decoding", async () => { + await Util.assertions.decoding.succeed( + schema, + 2, + BigDecimal.make(2n, 0) + ) + await Util.assertions.decoding.succeed( + schema, + 0.123, + BigDecimal.make(123n, 3) + ) + await Util.assertions.decoding.succeed( + schema, + 0, + BigDecimal.make(0n, 0) + ) + await Util.assertions.decoding.fail( + schema, + "abc", + `BigDecimalFromNumber +└─ Encoded side transformation failure + └─ Expected number, actual "abc"` + ) + }) + + it("encoding", async () => { + await Util.assertions.encoding.succeed( + schema, + BigDecimal.make(2n, 0), + 2 + ) + await Util.assertions.encoding.succeed( + schema, + BigDecimal.make(123n, 3), + 0.123 + ) + await Util.assertions.encoding.succeed( + schema, + BigDecimal.make(0n, 0), + 0 + ) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/BigDecimal/BigDecimalFromSelf.test.ts b/repos/effect/packages/effect/test/Schema/Schema/BigDecimal/BigDecimalFromSelf.test.ts new file mode 100644 index 0000000..f3506fa --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/BigDecimal/BigDecimalFromSelf.test.ts @@ -0,0 +1,50 @@ +import { describe, it } from "@effect/vitest" +import { assertFalse, assertTrue } from "@effect/vitest/utils" +import { BigDecimal } from "effect" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("BigDecimalFromSelf", () => { + const schema = S.BigDecimalFromSelf + + it("test roundtrip consistency", () => { + Util.assertions.testRoundtripConsistency(schema) + }) + + it("decoding", async () => { + await Util.assertions.decoding.succeed(schema, BigDecimal.make(0n, 0), BigDecimal.make(0n, 0)) + await Util.assertions.decoding.succeed(schema, BigDecimal.make(123n, 5), BigDecimal.make(123n, 5)) + await Util.assertions.decoding.succeed( + schema, + BigDecimal.make(-20000000n, 0), + BigDecimal.make(-20000000n, 0) + ) + }) + + it("encoding", async () => { + await Util.assertions.encoding.succeed(schema, BigDecimal.make(0n, 0), BigDecimal.make(0n, 0)) + await Util.assertions.encoding.succeed(schema, BigDecimal.make(123n, 5), BigDecimal.make(123n, 5)) + await Util.assertions.encoding.succeed( + schema, + BigDecimal.make(-20000000n, 0), + BigDecimal.make(-20000000n, 0) + ) + }) + + it("pretty", () => { + const schema = S.BigDecimalFromSelf + + Util.assertions.pretty(schema, BigDecimal.fromNumber(123), "BigDecimal(123)") + Util.assertions.pretty(schema, BigDecimal.unsafeFromString("123.100"), "BigDecimal(123.1)") + Util.assertions.pretty(schema, BigDecimal.unsafeFromString(""), "BigDecimal(0)") + }) + + it("equivalence", () => { + const schema = S.BigDecimalFromSelf + const equivalence = S.equivalence(schema) + + assertTrue(equivalence(BigDecimal.fromNumber(1), BigDecimal.unsafeFromString("1"))) + assertFalse(equivalence(BigDecimal.fromNumber(2), BigDecimal.unsafeFromString("1"))) + assertFalse(equivalence(BigDecimal.fromNumber(1), BigDecimal.unsafeFromString("2"))) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/BigDecimal/NegativeBigDecimalFromSelf.test.ts b/repos/effect/packages/effect/test/Schema/Schema/BigDecimal/NegativeBigDecimalFromSelf.test.ts new file mode 100644 index 0000000..b20fe64 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/BigDecimal/NegativeBigDecimalFromSelf.test.ts @@ -0,0 +1,34 @@ +import { describe, it } from "@effect/vitest" +import { BigDecimal } from "effect" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("NegativeBigDecimalFromSelf", () => { + const schema = S.NegativeBigDecimalFromSelf + + it("decoding", async () => { + await Util.assertions.decoding.fail( + schema, + BigDecimal.make(0n, 0), + `NegativeBigDecimalFromSelf +└─ Predicate refinement failure + └─ Expected a negative BigDecimal, actual BigDecimal(0)` + ) + await Util.assertions.decoding.fail( + schema, + BigDecimal.make(2n, 0), + `NegativeBigDecimalFromSelf +└─ Predicate refinement failure + └─ Expected a negative BigDecimal, actual BigDecimal(2)` + ) + await Util.assertions.decoding.succeed( + schema, + BigDecimal.make(-2n, 0), + BigDecimal.make(-2n, 0) + ) + }) + + it("encoding", async () => { + await Util.assertions.encoding.succeed(schema, BigDecimal.make(-1n, 0), BigDecimal.make(-1n, 0)) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/BigDecimal/NonNegativeBigDecimalFromSelf.test.ts b/repos/effect/packages/effect/test/Schema/Schema/BigDecimal/NonNegativeBigDecimalFromSelf.test.ts new file mode 100644 index 0000000..f139cfa --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/BigDecimal/NonNegativeBigDecimalFromSelf.test.ts @@ -0,0 +1,32 @@ +import { describe, it } from "@effect/vitest" +import { BigDecimal } from "effect" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("NonNegativeBigDecimalFromSelf", () => { + const schema = S.NonNegativeBigDecimalFromSelf + + it("decoding", async () => { + await Util.assertions.decoding.succeed( + schema, + BigDecimal.make(0n, 0), + BigDecimal.make(0n, 0) + ) + await Util.assertions.decoding.fail( + schema, + BigDecimal.make(-2n, 0), + `NonNegativeBigDecimalFromSelf +└─ Predicate refinement failure + └─ Expected a non-negative BigDecimal, actual BigDecimal(-2)` + ) + await Util.assertions.decoding.succeed( + schema, + BigDecimal.make(2n, 0), + BigDecimal.make(2n, 0) + ) + }) + + it("encoding", async () => { + await Util.assertions.encoding.succeed(schema, BigDecimal.make(1n, 0), BigDecimal.make(1n, 0)) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/BigDecimal/NonPositiveBigDecimalFromSelf.test.ts b/repos/effect/packages/effect/test/Schema/Schema/BigDecimal/NonPositiveBigDecimalFromSelf.test.ts new file mode 100644 index 0000000..def0b4d --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/BigDecimal/NonPositiveBigDecimalFromSelf.test.ts @@ -0,0 +1,32 @@ +import { describe, it } from "@effect/vitest" +import { BigDecimal } from "effect" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("NonPositiveBigDecimalFromSelf", () => { + const schema = S.NonPositiveBigDecimalFromSelf + + it("decoding", async () => { + await Util.assertions.decoding.succeed( + schema, + BigDecimal.make(0n, 0), + BigDecimal.make(0n, 0) + ) + await Util.assertions.decoding.fail( + schema, + BigDecimal.make(2n, 0), + `NonPositiveBigDecimalFromSelf +└─ Predicate refinement failure + └─ Expected a non-positive BigDecimal, actual BigDecimal(2)` + ) + await Util.assertions.decoding.succeed( + schema, + BigDecimal.make(-2n, 0), + BigDecimal.make(-2n, 0) + ) + }) + + it("encoding", async () => { + await Util.assertions.encoding.succeed(schema, BigDecimal.make(-1n, 0), BigDecimal.make(-1n, 0)) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/BigDecimal/PositiveBigDecimalFromSelf.test.ts b/repos/effect/packages/effect/test/Schema/Schema/BigDecimal/PositiveBigDecimalFromSelf.test.ts new file mode 100644 index 0000000..a2f39f8 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/BigDecimal/PositiveBigDecimalFromSelf.test.ts @@ -0,0 +1,34 @@ +import { describe, it } from "@effect/vitest" +import { BigDecimal } from "effect" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("PositiveBigDecimalFromSelf", () => { + const schema = S.PositiveBigDecimalFromSelf + + it("decoding", async () => { + await Util.assertions.decoding.fail( + schema, + BigDecimal.make(0n, 0), + `PositiveBigDecimalFromSelf +└─ Predicate refinement failure + └─ Expected a positive BigDecimal, actual BigDecimal(0)` + ) + await Util.assertions.decoding.fail( + schema, + BigDecimal.make(-2n, 0), + `PositiveBigDecimalFromSelf +└─ Predicate refinement failure + └─ Expected a positive BigDecimal, actual BigDecimal(-2)` + ) + await Util.assertions.decoding.succeed( + schema, + BigDecimal.make(2n, 0), + BigDecimal.make(2n, 0) + ) + }) + + it("encoding", async () => { + await Util.assertions.encoding.succeed(schema, BigDecimal.make(1n, 0), BigDecimal.make(1n, 0)) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/BigDecimal/betweenBigDecimal.test.ts b/repos/effect/packages/effect/test/Schema/Schema/BigDecimal/betweenBigDecimal.test.ts new file mode 100644 index 0000000..da62d0f --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/BigDecimal/betweenBigDecimal.test.ts @@ -0,0 +1,42 @@ +import { describe, it } from "@effect/vitest" +import { BigDecimal } from "effect" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +const max = BigDecimal.make(1n, 0) +const min = BigDecimal.make(-1n, 0) + +describe("betweenBigDecimal", () => { + const schema = S.BigDecimal.pipe(S.betweenBigDecimal(min, max)) + + it("make", () => { + Util.assertions.make.succeed(schema, BigDecimal.make(0n, 0)) + Util.assertions.make.fail( + schema, + BigDecimal.make(-2n, 0), + `betweenBigDecimal(-1, 1) +└─ Predicate refinement failure + └─ Expected a BigDecimal between -1 and 1, actual BigDecimal(-2)` + ) + }) + + it("decoding", async () => { + await Util.assertions.decoding.fail( + schema, + "2", + `betweenBigDecimal(-1, 1) +└─ Predicate refinement failure + └─ Expected a BigDecimal between -1 and 1, actual BigDecimal(2)` + ) + await Util.assertions.decoding.succeed(schema, "0", BigDecimal.normalize(BigDecimal.make(0n, 0))) + await Util.assertions.decoding.succeed( + schema, + "0.2", + BigDecimal.normalize(BigDecimal.make(2n, 1)) + ) + }) + + it("encoding", async () => { + await Util.assertions.encoding.succeed(schema, BigDecimal.make(0n, 0), "0") + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/BigDecimal/clampBigDecimal.test.ts b/repos/effect/packages/effect/test/Schema/Schema/BigDecimal/clampBigDecimal.test.ts new file mode 100644 index 0000000..f093c8f --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/BigDecimal/clampBigDecimal.test.ts @@ -0,0 +1,20 @@ +import { describe, it } from "@effect/vitest" +import { BigDecimal } from "effect" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("clampBigDecimal", () => { + it("decoding", async () => { + const min = BigDecimal.make(-1n, 0) + const max = BigDecimal.make(1n, 0) + const schema = S.BigDecimalFromSelf.pipe(S.clampBigDecimal(min, max)) // [-1, 1] + + await Util.assertions.decoding.succeed(schema, BigDecimal.make(3n, 0), BigDecimal.normalize(BigDecimal.make(1n, 0))) + await Util.assertions.decoding.succeed(schema, BigDecimal.make(0n, 0), BigDecimal.make(0n, 0)) + await Util.assertions.decoding.succeed( + schema, + BigDecimal.make(-3n, 0), + BigDecimal.normalize(BigDecimal.make(-1n, 0)) + ) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/BigDecimal/greaterThanBigDecimal.test.ts b/repos/effect/packages/effect/test/Schema/Schema/BigDecimal/greaterThanBigDecimal.test.ts new file mode 100644 index 0000000..bd3194e --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/BigDecimal/greaterThanBigDecimal.test.ts @@ -0,0 +1,30 @@ +import { describe, it } from "@effect/vitest" +import { BigDecimal } from "effect" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("greaterThanBigDecimal", () => { + const min = BigDecimal.fromNumber(10) + const schema = S.BigDecimal.pipe(S.greaterThanBigDecimal(min)) + + it("decoding", async () => { + await Util.assertions.decoding.fail( + schema, + "0", + `greaterThanBigDecimal(10) +└─ Predicate refinement failure + └─ Expected a BigDecimal greater than 10, actual BigDecimal(0)` + ) + await Util.assertions.decoding.fail( + schema, + "10", + `greaterThanBigDecimal(10) +└─ Predicate refinement failure + └─ Expected a BigDecimal greater than 10, actual BigDecimal(10)` + ) + }) + + it("encoding", async () => { + await Util.assertions.encoding.succeed(schema, BigDecimal.fromNumber(11), "11") + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/BigDecimal/greaterThanOrEqualToBigDecimal.test.ts b/repos/effect/packages/effect/test/Schema/Schema/BigDecimal/greaterThanOrEqualToBigDecimal.test.ts new file mode 100644 index 0000000..ed2467e --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/BigDecimal/greaterThanOrEqualToBigDecimal.test.ts @@ -0,0 +1,28 @@ +import { describe, it } from "@effect/vitest" +import { BigDecimal } from "effect" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("greaterThanOrEqualToBigDecimal", () => { + const min = BigDecimal.fromNumber(10) + const schema = S.BigDecimal.pipe(S.greaterThanOrEqualToBigDecimal(min)) + + it("decoding", async () => { + await Util.assertions.decoding.fail( + schema, + "0", + `greaterThanOrEqualToBigDecimal(10) +└─ Predicate refinement failure + └─ Expected a BigDecimal greater than or equal to 10, actual BigDecimal(0)` + ) + await Util.assertions.decoding.succeed( + schema, + "10", + BigDecimal.normalize(BigDecimal.fromNumber(10)) + ) + }) + + it("encoding", async () => { + await Util.assertions.encoding.succeed(schema, BigDecimal.fromNumber(11), "11") + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/BigDecimal/lessThanBigDecimal.test.ts b/repos/effect/packages/effect/test/Schema/Schema/BigDecimal/lessThanBigDecimal.test.ts new file mode 100644 index 0000000..4fa733a --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/BigDecimal/lessThanBigDecimal.test.ts @@ -0,0 +1,30 @@ +import { describe, it } from "@effect/vitest" +import { BigDecimal } from "effect" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("lessThanBigDecimal", () => { + const max = BigDecimal.fromNumber(5) + const schema = S.BigDecimal.pipe(S.lessThanBigDecimal(max)) + + it("decoding", async () => { + await Util.assertions.decoding.fail( + schema, + "5", + `lessThanBigDecimal(5) +└─ Predicate refinement failure + └─ Expected a BigDecimal less than 5, actual BigDecimal(5)` + ) + await Util.assertions.decoding.fail( + schema, + "6", + `lessThanBigDecimal(5) +└─ Predicate refinement failure + └─ Expected a BigDecimal less than 5, actual BigDecimal(6)` + ) + }) + + it("encoding", async () => { + await Util.assertions.encoding.succeed(schema, BigDecimal.fromNumber(4.5), "4.5") + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/BigDecimal/lessThanOrEqualToBigDecimal.test.ts b/repos/effect/packages/effect/test/Schema/Schema/BigDecimal/lessThanOrEqualToBigDecimal.test.ts new file mode 100644 index 0000000..dd0d1c1 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/BigDecimal/lessThanOrEqualToBigDecimal.test.ts @@ -0,0 +1,28 @@ +import { describe, it } from "@effect/vitest" +import { BigDecimal } from "effect" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("lessThanOrEqualToBigDecimal", () => { + const max = BigDecimal.unsafeFromNumber(5) + const schema = S.BigDecimal.pipe(S.lessThanOrEqualToBigDecimal(max)) + + it("decoding", async () => { + await Util.assertions.decoding.succeed( + schema, + "5", + BigDecimal.normalize(BigDecimal.unsafeFromNumber(5)) + ) + await Util.assertions.decoding.fail( + schema, + "6", + `lessThanOrEqualToBigDecimal(5) +└─ Predicate refinement failure + └─ Expected a BigDecimal less than or equal to 5, actual BigDecimal(6)` + ) + }) + + it("encoding", async () => { + await Util.assertions.encoding.succeed(schema, BigDecimal.fromNumber(4.5), "4.5") + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/BigInt/BigInt.test.ts b/repos/effect/packages/effect/test/Schema/Schema/BigInt/BigInt.test.ts new file mode 100644 index 0000000..67cd3bd --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/BigInt/BigInt.test.ts @@ -0,0 +1,71 @@ +import { describe, it } from "@effect/vitest" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("BigInt", () => { + const schema = S.BigInt + + it("test roundtrip consistency", () => { + Util.assertions.testRoundtripConsistency(schema) + }) + + it("decoding", async () => { + await Util.assertions.decoding.succeed(schema, "0", 0n) + await Util.assertions.decoding.succeed(schema, "-0", -0n) + await Util.assertions.decoding.succeed(schema, "1", 1n) + + await Util.assertions.decoding.fail( + schema, + "", + `BigInt +└─ Transformation process failure + └─ Unable to decode "" into a bigint` + ) + await Util.assertions.decoding.fail( + schema, + " ", + `BigInt +└─ Transformation process failure + └─ Unable to decode " " into a bigint` + ) + await Util.assertions.decoding.fail( + schema, + "1.2", + `BigInt +└─ Transformation process failure + └─ Unable to decode "1.2" into a bigint` + ) + await Util.assertions.decoding.fail( + schema, + "1AB", + `BigInt +└─ Transformation process failure + └─ Unable to decode "1AB" into a bigint` + ) + await Util.assertions.decoding.fail( + schema, + "AB1", + `BigInt +└─ Transformation process failure + └─ Unable to decode "AB1" into a bigint` + ) + await Util.assertions.decoding.fail( + schema, + "a", + `BigInt +└─ Transformation process failure + └─ Unable to decode "a" into a bigint` + ) + await Util.assertions.decoding.fail( + schema, + "a1", + `BigInt +└─ Transformation process failure + └─ Unable to decode "a1" into a bigint` + ) + }) + + it("encoding", async () => { + await Util.assertions.encoding.succeed(schema, 1n, "1") + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/BigInt/BigIntFromNumber.test.ts b/repos/effect/packages/effect/test/Schema/Schema/BigInt/BigIntFromNumber.test.ts new file mode 100644 index 0000000..4e9342a --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/BigInt/BigIntFromNumber.test.ts @@ -0,0 +1,69 @@ +import { describe, it } from "@effect/vitest" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("BigIntFromNumber", () => { + const schema = S.BigIntFromNumber + + it("test roundtrip consistency", () => { + Util.assertions.testRoundtripConsistency(schema) + }) + + it("decoding", async () => { + await Util.assertions.decoding.succeed(schema, 0, 0n) + await Util.assertions.decoding.succeed(schema, -0, -0n) + await Util.assertions.decoding.succeed(schema, 1, 1n) + + await Util.assertions.decoding.fail( + schema, + 1.2, + `BigIntFromNumber +└─ Transformation process failure + └─ Unable to decode 1.2 into a bigint` + ) + await Util.assertions.decoding.fail( + schema, + NaN, + `BigIntFromNumber +└─ Transformation process failure + └─ Unable to decode NaN into a bigint` + ) + await Util.assertions.decoding.fail( + schema, + Infinity, + `BigIntFromNumber +└─ Transformation process failure + └─ Unable to decode Infinity into a bigint` + ) + await Util.assertions.decoding.fail( + schema, + -Infinity, + `BigIntFromNumber +└─ Transformation process failure + └─ Unable to decode -Infinity into a bigint` + ) + }) + + it("encoding", async () => { + await Util.assertions.encoding.succeed(schema, 1n, 1) + + await Util.assertions.encoding.fail( + schema, + BigInt(Number.MAX_SAFE_INTEGER) + 1n, + `BigIntFromNumber +└─ Type side transformation failure + └─ betweenBigInt(-9007199254740991, 9007199254740991) + └─ Predicate refinement failure + └─ Expected a bigint between -9007199254740991n and 9007199254740991n, actual 9007199254740992n` + ) + await Util.assertions.encoding.fail( + schema, + BigInt(Number.MIN_SAFE_INTEGER) - 1n, + `BigIntFromNumber +└─ Type side transformation failure + └─ betweenBigInt(-9007199254740991, 9007199254740991) + └─ Predicate refinement failure + └─ Expected a bigint between -9007199254740991n and 9007199254740991n, actual -9007199254740992n` + ) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/BigInt/BigIntFromSelf.test.ts b/repos/effect/packages/effect/test/Schema/Schema/BigInt/BigIntFromSelf.test.ts new file mode 100644 index 0000000..f6f838d --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/BigInt/BigIntFromSelf.test.ts @@ -0,0 +1,26 @@ +import { describe, it } from "@effect/vitest" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("BigIntFromSelf", () => { + const schema = S.BigIntFromSelf + it("decoding", async () => { + await Util.assertions.decoding.succeed(schema, 0n, 0n) + await Util.assertions.decoding.succeed(schema, 1n, 1n) + + await Util.assertions.decoding.fail( + schema, + null, + `Expected bigint, actual null` + ) + await Util.assertions.decoding.fail( + schema, + 1.2, + `Expected bigint, actual 1.2` + ) + }) + + it("encoding", async () => { + await Util.assertions.encoding.succeed(schema, 1n, 1n) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/BigInt/NegativeBigIntFromSelf.test.ts b/repos/effect/packages/effect/test/Schema/Schema/BigInt/NegativeBigIntFromSelf.test.ts new file mode 100644 index 0000000..50ed96f --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/BigInt/NegativeBigIntFromSelf.test.ts @@ -0,0 +1,28 @@ +import { describe, it } from "@effect/vitest" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("NegativeBigIntFromSelf", () => { + const schema = S.NegativeBigIntFromSelf + + it("decoding", async () => { + await Util.assertions.decoding.fail( + schema, + 0n, + `NegativeBigintFromSelf +└─ Predicate refinement failure + └─ Expected a negative bigint, actual 0n` + ) + await Util.assertions.decoding.fail( + schema, + 1n, + `NegativeBigintFromSelf +└─ Predicate refinement failure + └─ Expected a negative bigint, actual 1n` + ) + }) + + it("encoding", async () => { + await Util.assertions.encoding.succeed(schema, -1n, -1n) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/BigInt/NonNegativeBigIntFromSelf.test.ts b/repos/effect/packages/effect/test/Schema/Schema/BigInt/NonNegativeBigIntFromSelf.test.ts new file mode 100644 index 0000000..7d2a9e3 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/BigInt/NonNegativeBigIntFromSelf.test.ts @@ -0,0 +1,22 @@ +import { describe, it } from "@effect/vitest" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("NonNegativeBigIntFromSelf", () => { + const schema = S.NonNegativeBigIntFromSelf + + it("decoding", async () => { + await Util.assertions.decoding.succeed(schema, 0n) + await Util.assertions.decoding.succeed(schema, 1n) + }) + + it("encoding", async () => { + await Util.assertions.encoding.fail( + schema, + -1n, + `NonNegativeBigintFromSelf +└─ Predicate refinement failure + └─ Expected a non-negative bigint, actual -1n` + ) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/BigInt/NonPositiveBigIntFromSelf.test.ts b/repos/effect/packages/effect/test/Schema/Schema/BigInt/NonPositiveBigIntFromSelf.test.ts new file mode 100644 index 0000000..245a9ad --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/BigInt/NonPositiveBigIntFromSelf.test.ts @@ -0,0 +1,22 @@ +import { describe, it } from "@effect/vitest" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("NonPositiveBigIntFromSelf", () => { + const schema = S.NonPositiveBigIntFromSelf + + it("decoding", async () => { + await Util.assertions.decoding.succeed(schema, 0n) + await Util.assertions.decoding.fail( + schema, + 1n, + `NonPositiveBigintFromSelf +└─ Predicate refinement failure + └─ Expected a non-positive bigint, actual 1n` + ) + }) + + it("encoding", async () => { + await Util.assertions.encoding.succeed(schema, -1n, -1n) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/BigInt/PositiveBigIntFromSelf.test.ts b/repos/effect/packages/effect/test/Schema/Schema/BigInt/PositiveBigIntFromSelf.test.ts new file mode 100644 index 0000000..eff17d2 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/BigInt/PositiveBigIntFromSelf.test.ts @@ -0,0 +1,28 @@ +import { describe, it } from "@effect/vitest" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("PositiveBigIntFromSelf", () => { + const schema = S.PositiveBigIntFromSelf + + it("decoding", async () => { + await Util.assertions.decoding.fail( + schema, + -1n, + `PositiveBigintFromSelf +└─ Predicate refinement failure + └─ Expected a positive bigint, actual -1n` + ) + await Util.assertions.decoding.fail( + schema, + 0n, + `PositiveBigintFromSelf +└─ Predicate refinement failure + └─ Expected a positive bigint, actual 0n` + ) + }) + + it("encoding", async () => { + await Util.assertions.encoding.succeed(schema, 1n, 1n) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/BigInt/betweenBigInt.test.ts b/repos/effect/packages/effect/test/Schema/Schema/BigInt/betweenBigInt.test.ts new file mode 100644 index 0000000..793cf02 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/BigInt/betweenBigInt.test.ts @@ -0,0 +1,31 @@ +import { describe, it } from "@effect/vitest" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("betweenBigInt", () => { + const schema = S.BigIntFromSelf.pipe(S.betweenBigInt(-1n, 1n)).annotations({ + title: "[-1n, 1n] interval" + }) + + it("decoding", async () => { + await Util.assertions.decoding.succeed(schema, 0n, 0n) + await Util.assertions.decoding.fail( + schema, + -2n, + `[-1n, 1n] interval +└─ Predicate refinement failure + └─ Expected a bigint between -1n and 1n, actual -2n` + ) + await Util.assertions.decoding.fail( + schema, + 2n, + `[-1n, 1n] interval +└─ Predicate refinement failure + └─ Expected a bigint between -1n and 1n, actual 2n` + ) + }) + + it("encoding", async () => { + await Util.assertions.encoding.succeed(schema, 1n, 1n) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/BigInt/clampBigInt.test.ts b/repos/effect/packages/effect/test/Schema/Schema/BigInt/clampBigInt.test.ts new file mode 100644 index 0000000..7eba318 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/BigInt/clampBigInt.test.ts @@ -0,0 +1,13 @@ +import { describe, it } from "@effect/vitest" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("clampBigInt", () => { + it("decoding", async () => { + const schema = S.BigIntFromSelf.pipe(S.clampBigInt(-1n, 1n)) + + await Util.assertions.decoding.succeed(schema, 3n, 1n) + await Util.assertions.decoding.succeed(schema, 0n, 0n) + await Util.assertions.decoding.succeed(schema, -3n, -1n) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/BigInt/greaterThanBigInt.test.ts b/repos/effect/packages/effect/test/Schema/Schema/BigInt/greaterThanBigInt.test.ts new file mode 100644 index 0000000..acba1fe --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/BigInt/greaterThanBigInt.test.ts @@ -0,0 +1,28 @@ +import { describe, it } from "@effect/vitest" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("greaterThanBigInt", () => { + const schema = S.BigIntFromSelf.pipe(S.greaterThanBigInt(0n)) + + it("decoding", async () => { + await Util.assertions.decoding.fail( + schema, + -1n, + `greaterThanBigInt(0) +└─ Predicate refinement failure + └─ Expected a positive bigint, actual -1n` + ) + await Util.assertions.decoding.fail( + schema, + 0n, + `greaterThanBigInt(0) +└─ Predicate refinement failure + └─ Expected a positive bigint, actual 0n` + ) + }) + + it("encoding", async () => { + await Util.assertions.encoding.succeed(schema, 1n, 1n) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/BigInt/greaterThanOrEqualToBigInt.test.ts b/repos/effect/packages/effect/test/Schema/Schema/BigInt/greaterThanOrEqualToBigInt.test.ts new file mode 100644 index 0000000..e1d6a5a --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/BigInt/greaterThanOrEqualToBigInt.test.ts @@ -0,0 +1,21 @@ +import { describe, it } from "@effect/vitest" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("greaterThanOrEqualToBigInt", () => { + const schema = S.BigIntFromSelf.pipe(S.greaterThanOrEqualToBigInt(0n)) + it("decoding", async () => { + await Util.assertions.decoding.fail( + schema, + -1n, + `greaterThanOrEqualToBigInt(0) +└─ Predicate refinement failure + └─ Expected a non-negative bigint, actual -1n` + ) + await Util.assertions.decoding.succeed(schema, 0n, 0n) + }) + + it("encoding", async () => { + await Util.assertions.encoding.succeed(schema, 1n, 1n) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/BigInt/lessThanBigInt.test.ts b/repos/effect/packages/effect/test/Schema/Schema/BigInt/lessThanBigInt.test.ts new file mode 100644 index 0000000..5c037f9 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/BigInt/lessThanBigInt.test.ts @@ -0,0 +1,28 @@ +import { describe, it } from "@effect/vitest" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("lessThanBigInt", () => { + const schema = S.BigIntFromSelf.pipe(S.lessThanBigInt(0n)) + + it("decoding", async () => { + await Util.assertions.decoding.fail( + schema, + 0n, + `lessThanBigInt(0) +└─ Predicate refinement failure + └─ Expected a negative bigint, actual 0n` + ) + await Util.assertions.decoding.fail( + schema, + 1n, + `lessThanBigInt(0) +└─ Predicate refinement failure + └─ Expected a negative bigint, actual 1n` + ) + }) + + it("encoding", async () => { + await Util.assertions.encoding.succeed(schema, -1n, -1n) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/BigInt/lessThanOrEqualToBigInt.test.ts b/repos/effect/packages/effect/test/Schema/Schema/BigInt/lessThanOrEqualToBigInt.test.ts new file mode 100644 index 0000000..3295481 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/BigInt/lessThanOrEqualToBigInt.test.ts @@ -0,0 +1,22 @@ +import { describe, it } from "@effect/vitest" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("lessThanOrEqualToBigInt", () => { + const schema = S.BigIntFromSelf.pipe(S.lessThanOrEqualToBigInt(0n)) + + it("decoding", async () => { + await Util.assertions.decoding.succeed(schema, 0n, 0n) + await Util.assertions.decoding.fail( + schema, + 1n, + `lessThanOrEqualToBigInt(0) +└─ Predicate refinement failure + └─ Expected a non-positive bigint, actual 1n` + ) + }) + + it("encoding", async () => { + await Util.assertions.encoding.succeed(schema, -1n, -1n) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/Boolean/Boolean.test.ts b/repos/effect/packages/effect/test/Schema/Schema/Boolean/Boolean.test.ts new file mode 100644 index 0000000..44bebb5 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/Boolean/Boolean.test.ts @@ -0,0 +1,17 @@ +import { describe, it } from "@effect/vitest" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("Boolean", () => { + const schema = S.Boolean + it("decoding", async () => { + await Util.assertions.decoding.succeed(schema, true, true) + await Util.assertions.decoding.succeed(schema, false, false) + await Util.assertions.decoding.fail(schema, 1, `Expected boolean, actual 1`) + }) + + it("encoding", async () => { + await Util.assertions.encoding.succeed(schema, true, true) + await Util.assertions.encoding.succeed(schema, false, false) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/Boolean/BooleanFromString.test.ts b/repos/effect/packages/effect/test/Schema/Schema/Boolean/BooleanFromString.test.ts new file mode 100644 index 0000000..632dc03 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/Boolean/BooleanFromString.test.ts @@ -0,0 +1,25 @@ +import { describe, it } from "@effect/vitest" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("BooleanFromString", () => { + const schema = S.BooleanFromString + it("decoding", async () => { + await Util.assertions.decoding.succeed(schema, "true", true) + await Util.assertions.decoding.succeed(schema, "false", false) + await Util.assertions.decoding.fail( + schema, + "a", + `BooleanFromString +└─ Encoded side transformation failure + └─ a string to be decoded into a boolean + ├─ Expected "true", actual "a" + └─ Expected "false", actual "a"` + ) + }) + + it("encoding", async () => { + await Util.assertions.encoding.succeed(schema, true, "true") + await Util.assertions.encoding.succeed(schema, false, "false") + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/Boolean/BooleanFromUnknown.test.ts b/repos/effect/packages/effect/test/Schema/Schema/Boolean/BooleanFromUnknown.test.ts new file mode 100644 index 0000000..a81bfcf --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/Boolean/BooleanFromUnknown.test.ts @@ -0,0 +1,24 @@ +import { describe, it } from "@effect/vitest" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("BooleanFromUnknown", () => { + const schema = S.BooleanFromUnknown + it("decoding", async () => { + await Util.assertions.decoding.succeed(schema, true, true) + await Util.assertions.decoding.succeed(schema, 1, true) + await Util.assertions.decoding.succeed(schema, 1n, true) + await Util.assertions.decoding.succeed(schema, "a", true) + + await Util.assertions.decoding.succeed(schema, false, false) + await Util.assertions.decoding.succeed(schema, 0, false) + await Util.assertions.decoding.succeed(schema, 0n, false) + await Util.assertions.decoding.succeed(schema, null, false) + await Util.assertions.decoding.succeed(schema, "", false) + }) + + it("encoding", async () => { + await Util.assertions.encoding.succeed(schema, true, true) + await Util.assertions.encoding.succeed(schema, false, false) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/Boolean/Not.test.ts b/repos/effect/packages/effect/test/Schema/Schema/Boolean/Not.test.ts new file mode 100644 index 0000000..a87e418 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/Boolean/Not.test.ts @@ -0,0 +1,16 @@ +import { describe, it } from "@effect/vitest" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("Not", () => { + const schema = S.Not + it("decoding", async () => { + await Util.assertions.decoding.succeed(schema, true, false) + await Util.assertions.decoding.succeed(schema, false, true) + }) + + it("encoding", async () => { + await Util.assertions.encoding.succeed(schema, true, false) + await Util.assertions.encoding.succeed(schema, false, true) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/Cause/Cause.test.ts b/repos/effect/packages/effect/test/Schema/Schema/Cause/Cause.test.ts new file mode 100644 index 0000000..8971585 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/Cause/Cause.test.ts @@ -0,0 +1,176 @@ +import { describe, it } from "@effect/vitest" +import { assertTrue, deepStrictEqual, strictEqual } from "@effect/vitest/utils" +import { Cause, FiberId } from "effect" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("Cause", () => { + it("test roundtrip consistency", () => { + Util.assertions.testRoundtripConsistency(S.Cause({ error: S.NumberFromString, defect: S.Defect })) + }) + + it("decoding", async () => { + const schema = S.Cause({ error: S.NumberFromString, defect: S.Defect }) + await Util.assertions.decoding.succeed( + schema, + { _tag: "Fail", error: "1" }, + Cause.fail(1) + ) + await Util.assertions.decoding.succeed( + schema, + { _tag: "Empty" }, + Cause.empty + ) + await Util.assertions.decoding.succeed( + schema, + { + _tag: "Parallel", + left: { _tag: "Fail", error: "1" }, + right: { _tag: "Empty" } + }, + Cause.parallel(Cause.fail(1), Cause.empty) + ) + await Util.assertions.decoding.succeed( + schema, + { + _tag: "Sequential", + left: { _tag: "Fail", error: "1" }, + right: { _tag: "Empty" } + }, + Cause.sequential(Cause.fail(1), Cause.empty) + ) + await Util.assertions.decoding.succeed( + schema, + { + _tag: "Die", + defect: { stack: "fail", message: "error" } + }, + Cause.die(new Error("error", { cause: { stack: "fail", message: "error" } })) + ) + await Util.assertions.decoding.succeed( + schema, + { + _tag: "Interrupt", + fiberId: { + _tag: "Composite", + left: { + _tag: "Runtime", + id: 1, + startTimeMillis: 1000 + }, + right: { + _tag: "None" + } + } + }, + Cause.interrupt(FiberId.composite(FiberId.runtime(1, 1000), FiberId.none)) + ) + + await Util.assertions.decoding.fail( + schema, + null, + `(CauseEncoded <-> Cause) +└─ Encoded side transformation failure + └─ Expected CauseEncoded, actual null` + ) + await Util.assertions.decoding.fail( + schema, + {}, + `(CauseEncoded <-> Cause) +└─ Encoded side transformation failure + └─ CauseEncoded + └─ { readonly _tag: "Empty" | "Fail" | "Die" | "Interrupt" | "Sequential" | "Parallel" } + └─ ["_tag"] + └─ is missing` + ) + await Util.assertions.decoding.fail( + schema, + { _tag: "Parallel", left: { _tag: "Fail" }, right: { _tag: "Interrupt" } }, + `(CauseEncoded <-> Cause) +└─ Encoded side transformation failure + └─ CauseEncoded + └─ { readonly _tag: "Parallel"; readonly left: CauseEncoded; readonly right: CauseEncoded } + └─ ["left"] + └─ CauseEncoded + └─ { readonly _tag: "Fail"; readonly error: NumberFromString } + └─ ["error"] + └─ is missing` + ) + }) + + describe("encoding", () => { + it("handles array-based defects without throwing", async () => { + const schema = S.Cause({ error: S.String, defect: S.Defect }) + await Util.assertions.encoding.succeed(schema, Cause.die([{ toString: "" }]), { + _tag: "Die", + defect: "[{\"toString\":\"\"}]" + }) + }) + + it("should raise an error when a non-encodable Cause is passed", async () => { + const schema = S.Cause({ error: S.String, defect: Util.Defect }) + await Util.assertions.encoding.fail( + schema, + Cause.die(null), + `(CauseEncoded <-> Cause) +└─ Type side transformation failure + └─ Cause + └─ CauseEncoded + └─ { readonly _tag: "Die"; readonly defect: object } + └─ ["defect"] + └─ Expected object, actual null` + ) + }) + + it("using the built-in Defect schema as defect argument", async () => { + const schema = S.Cause({ error: S.NumberFromString, defect: S.Defect }) + const schemaUnknown = S.Cause({ error: S.NumberFromString, defect: S.Unknown }) + + await Util.assertions.encoding.succeed(schema, Cause.fail(1), { _tag: "Fail", error: "1" }) + await Util.assertions.encoding.succeed(schema, Cause.empty, { _tag: "Empty" }) + await Util.assertions.encoding.succeed(schema, Cause.parallel(Cause.fail(1), Cause.empty), { + _tag: "Parallel", + left: { _tag: "Fail", error: "1" }, + right: { _tag: "Empty" } + }) + await Util.assertions.encoding.succeed(schema, Cause.sequential(Cause.fail(1), Cause.empty), { + _tag: "Sequential", + left: { _tag: "Fail", error: "1" }, + right: { _tag: "Empty" } + }) + await Util.assertions.encoding.succeed(schema, Cause.die("fail"), { + _tag: "Die", + defect: "fail" + }) + await Util.assertions.encoding.succeed( + schema, + Cause.interrupt(FiberId.composite(FiberId.runtime(1, 1000), FiberId.none)), + { + _tag: "Interrupt", + fiberId: { + _tag: "Composite", + left: { + _tag: "Runtime", + id: 1, + startTimeMillis: 1000 + }, + right: { + _tag: "None" + } + } + } + ) + + let failWithStack = S.encodeSync(schema)(Cause.die(new Error("fail"))) + assertTrue(failWithStack._tag === "Die") + deepStrictEqual(failWithStack.defect, { + name: "Error", + message: "fail" + }) + + failWithStack = S.encodeSync(schemaUnknown)(Cause.die(new Error("fail"))) + assertTrue(failWithStack._tag === "Die") + strictEqual((failWithStack.defect as Error).message, "fail") + }) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/Cause/CauseFromSelf.test.ts b/repos/effect/packages/effect/test/Schema/Schema/Cause/CauseFromSelf.test.ts new file mode 100644 index 0000000..0f4ec0a --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/Cause/CauseFromSelf.test.ts @@ -0,0 +1,80 @@ +import { describe, it } from "@effect/vitest" +import * as Cause from "effect/Cause" +import * as FiberId from "effect/FiberId" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("CauseFromSelf", () => { + it("arbitrary", () => { + Util.assertions.arbitrary.validateGeneratedValues(S.CauseFromSelf({ error: S.NumberFromString, defect: S.Unknown })) + }) + + it("test roundtrip consistency", () => { + Util.assertions.testRoundtripConsistency(S.CauseFromSelf({ error: S.NumberFromString, defect: S.Unknown })) + }) + + it("decoding", async () => { + const schema = S.CauseFromSelf({ error: S.NumberFromString, defect: S.Unknown }) + + await Util.assertions.decoding.succeed(schema, Cause.fail("1"), Cause.fail(1)) + + await Util.assertions.decoding.fail( + schema, + null, + `Expected Cause, actual null` + ) + await Util.assertions.decoding.fail( + schema, + Cause.fail("a"), + `Cause +└─ CauseEncoded + └─ { readonly _tag: "Fail"; readonly error: NumberFromString } + └─ ["error"] + └─ NumberFromString + └─ Transformation process failure + └─ Unable to decode "a" into a number` + ) + await Util.assertions.decoding.fail( + schema, + Cause.parallel(Cause.die("error"), Cause.fail("a")), + `Cause +└─ CauseEncoded + └─ { readonly _tag: "Parallel"; readonly left: CauseEncoded; readonly right: CauseEncoded } + └─ ["right"] + └─ CauseEncoded + └─ { readonly _tag: "Fail"; readonly error: NumberFromString } + └─ ["error"] + └─ NumberFromString + └─ Transformation process failure + └─ Unable to decode "a" into a number` + ) + }) + + it("encoding", async () => { + const schema = S.CauseFromSelf({ error: S.NumberFromString, defect: S.Unknown }) + + await Util.assertions.encoding.succeed(schema, Cause.fail(1), Cause.fail("1")) + }) + + it("pretty", () => { + const schema = S.CauseFromSelf({ error: S.String, defect: S.Unknown }) + Util.assertions.pretty(schema, Cause.die("error"), `Cause.die(Error: error)`) + Util.assertions.pretty(schema, Cause.empty, `Cause.empty`) + Util.assertions.pretty(schema, Cause.fail("error"), `Cause.fail("error")`) + Util.assertions.pretty( + schema, + Cause.interrupt(FiberId.composite(FiberId.none, FiberId.none)), + `Cause.interrupt(FiberId.composite(FiberId.none, FiberId.none))` + ) + Util.assertions.pretty( + schema, + Cause.parallel(Cause.die("error"), Cause.fail("error")), + `Cause.parallel(Cause.die(Error: error), Cause.fail("error"))` + ) + Util.assertions.pretty( + schema, + Cause.sequential(Cause.die("error"), Cause.fail("error")), + `Cause.sequential(Cause.die(Error: error), Cause.fail("error"))` + ) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/Chunk/Chunk.test.ts b/repos/effect/packages/effect/test/Schema/Schema/Chunk/Chunk.test.ts new file mode 100644 index 0000000..affd53c --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/Chunk/Chunk.test.ts @@ -0,0 +1,39 @@ +import { describe, it } from "@effect/vitest" +import * as C from "effect/Chunk" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("Chunk", () => { + it("test roundtrip consistency", () => { + Util.assertions.testRoundtripConsistency(S.Chunk(S.Number)) + }) + + it("decoding", async () => { + const schema = S.Chunk(S.Number) + await Util.assertions.decoding.succeed(schema, [], C.empty()) + await Util.assertions.decoding.succeed(schema, [1, 2, 3], C.fromIterable([1, 2, 3])) + + await Util.assertions.decoding.fail( + schema, + null, + `(ReadonlyArray <-> Chunk) +└─ Encoded side transformation failure + └─ Expected ReadonlyArray, actual null` + ) + await Util.assertions.decoding.fail( + schema, + [1, "a"], + `(ReadonlyArray <-> Chunk) +└─ Encoded side transformation failure + └─ ReadonlyArray + └─ [1] + └─ Expected number, actual "a"` + ) + }) + + it("encoding", async () => { + const schema = S.Chunk(S.Number) + await Util.assertions.encoding.succeed(schema, C.empty(), []) + await Util.assertions.encoding.succeed(schema, C.fromIterable([1, 2, 3]), [1, 2, 3]) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/Chunk/ChunkFromSelf.test.ts b/repos/effect/packages/effect/test/Schema/Schema/Chunk/ChunkFromSelf.test.ts new file mode 100644 index 0000000..91f91d5 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/Chunk/ChunkFromSelf.test.ts @@ -0,0 +1,64 @@ +import { describe, it } from "@effect/vitest" +import { assertFalse, assertTrue } from "@effect/vitest/utils" +import * as C from "effect/Chunk" +import * as P from "effect/ParseResult" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("ChunkFromSelf", () => { + it("test roundtrip consistency", () => { + Util.assertions.testRoundtripConsistency(S.ChunkFromSelf(S.Number)) + }) + + it("decoding", async () => { + const schema = S.ChunkFromSelf(S.NumberFromString) + await Util.assertions.decoding.succeed(schema, C.empty(), C.empty()) + await Util.assertions.decoding.succeed( + schema, + C.fromIterable(["1", "2", "3"]), + C.fromIterable([1, 2, 3]) + ) + + await Util.assertions.decoding.fail( + schema, + null, + `Expected Chunk, actual null` + ) + await Util.assertions.decoding.fail( + schema, + C.fromIterable(["1", "a", "3"]), + `Chunk +└─ ReadonlyArray + └─ [1] + └─ NumberFromString + └─ Transformation process failure + └─ Unable to decode "a" into a number` + ) + }) + + it("encoding", async () => { + const schema = S.ChunkFromSelf(S.NumberFromString) + await Util.assertions.encoding.succeed(schema, C.empty(), C.empty()) + await Util.assertions.encoding.succeed( + schema, + C.fromIterable([1, 2, 3]), + C.fromIterable(["1", "2", "3"]) + ) + }) + + it("is", () => { + const schema = S.ChunkFromSelf(S.String) + const is = P.is(schema) + assertTrue(is(C.empty())) + assertTrue(is(C.fromIterable(["a", "b", "c"]))) + + assertFalse(is(C.fromIterable(["a", "b", 1]))) + assertFalse(is({ _id: Symbol.for("effect/Schema/test/FakeChunk") })) + }) + + it("pretty", () => { + const schema = S.ChunkFromSelf(S.String) + Util.assertions.pretty(schema, C.empty(), "Chunk()") + Util.assertions.pretty(schema, C.fromIterable(["a", "b"]), `Chunk("a", "b")`) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/Chunk/NonEmptyChunk.test.ts b/repos/effect/packages/effect/test/Schema/Schema/Chunk/NonEmptyChunk.test.ts new file mode 100644 index 0000000..3db993b --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/Chunk/NonEmptyChunk.test.ts @@ -0,0 +1,37 @@ +import { describe, it } from "@effect/vitest" +import * as C from "effect/Chunk" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("NonEmptyChunk", () => { + it("test roundtrip consistency", () => { + Util.assertions.testRoundtripConsistency(S.NonEmptyChunk(S.Number)) + }) + + it("decoding", async () => { + const schema = S.NonEmptyChunk(S.Number) + await Util.assertions.decoding.succeed(schema, [1, 2, 3], C.make(1, 2, 3)) + + await Util.assertions.decoding.fail( + schema, + null, + `(readonly [number, ...number[]] <-> NonEmptyChunk) +└─ Encoded side transformation failure + └─ Expected readonly [number, ...number[]], actual null` + ) + await Util.assertions.decoding.fail( + schema, + [1, "a"], + `(readonly [number, ...number[]] <-> NonEmptyChunk) +└─ Encoded side transformation failure + └─ readonly [number, ...number[]] + └─ [1] + └─ Expected number, actual "a"` + ) + }) + + it("encoding", async () => { + const schema = S.NonEmptyChunk(S.Number) + await Util.assertions.encoding.succeed(schema, C.make(1, 2, 3), [1, 2, 3]) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/Chunk/NonEmptyChunkFromSelf.test.ts b/repos/effect/packages/effect/test/Schema/Schema/Chunk/NonEmptyChunkFromSelf.test.ts new file mode 100644 index 0000000..f592461 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/Chunk/NonEmptyChunkFromSelf.test.ts @@ -0,0 +1,75 @@ +import { describe, it } from "@effect/vitest" +import { assertFalse, assertTrue } from "@effect/vitest/utils" +import * as Arbitrary from "effect/Arbitrary" +import * as C from "effect/Chunk" +import * as FastCheck from "effect/FastCheck" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("NonEmptyChunkFromSelf", () => { + it("test roundtrip consistency", () => { + Util.assertions.testRoundtripConsistency(S.NonEmptyChunkFromSelf(S.Number)) + }) + + it("decoding", async () => { + const schema = S.NonEmptyChunkFromSelf(S.NumberFromString) + await Util.assertions.decoding.succeed( + schema, + C.make("1", "2", "3"), + C.make(1, 2, 3) + ) + + await Util.assertions.decoding.fail( + schema, + null, + `Expected NonEmptyChunk, actual null` + ) + await Util.assertions.decoding.fail( + schema, + C.empty(), + `Expected NonEmptyChunk, actual { + "_id": "Chunk", + "values": [] +}` + ) + await Util.assertions.decoding.fail( + schema, + C.make("1", "a", "3"), + `NonEmptyChunk +└─ readonly [NumberFromString, ...NumberFromString[]] + └─ [1] + └─ NumberFromString + └─ Transformation process failure + └─ Unable to decode "a" into a number` + ) + }) + + it("encoding", async () => { + const schema = S.NonEmptyChunkFromSelf(S.NumberFromString) + await Util.assertions.encoding.succeed( + schema, + C.make(1, 2, 3), + C.make("1", "2", "3") + ) + }) + + it("pretty", () => { + const schema = S.NonEmptyChunkFromSelf(S.String) + Util.assertions.pretty(schema, C.make("a", "b"), `NonEmptyChunk("a", "b")`) + }) + + it("equivalence", () => { + const schema = S.NonEmptyChunkFromSelf(S.String) + const equivalence = S.equivalence(schema) + assertTrue(equivalence(C.make("a", "b"), C.make("a", "b"))) + assertFalse(equivalence(C.make("a", "b"), C.make("a", "c"))) + assertFalse(equivalence(C.make("a", "b"), C.make("a"))) + }) + + it("arbitrary", () => { + const schema = S.NonEmptyChunkFromSelf(S.String) + const arb = Arbitrary.make(schema) + FastCheck.assert(FastCheck.property(arb, C.isNonEmpty)) + assertTrue(FastCheck.sample(arb, 10).every(C.isNonEmpty)) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/Class/Class.test.ts b/repos/effect/packages/effect/test/Schema/Schema/Class/Class.test.ts new file mode 100644 index 0000000..f6c3926 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/Class/Class.test.ts @@ -0,0 +1,616 @@ +import { describe, it } from "@effect/vitest" +import { + assertFalse, + assertInstanceOf, + assertSome, + assertTrue, + deepStrictEqual, + strictEqual, + throws +} from "@effect/vitest/utils" +import { Context, Data, Effect, Equal, JSONSchema, ParseResult, Schema as S, SchemaAST as AST } from "effect" +import * as Util from "../../TestUtils.js" + +class Person extends S.Class("Person")({ + id: S.Number, + name: S.String.pipe(S.nonEmptyString()) +}) { + get upperName() { + return this.name.toUpperCase() + } +} + +const Name = Context.GenericTag<"Name", string>("Name") +const NameString = S.String.pipe( + S.nonEmptyString(), + S.transformOrFail( + S.String, + { + strict: true, + decode: (_, _opts, ast) => + Name.pipe( + Effect.filterOrFail( + (name) => _ === name, + () => new ParseResult.Type(ast, _, "Does not match Name") + ) + ), + encode: (_) => ParseResult.succeed(_) + } + ) +) + +class PersonWithAge extends Person.extend("PersonWithAge")({ + age: S.Number +}) { + get isAdult() { + return this.age >= 18 + } +} + +describe("Class", () => { + it("suspend before initialization", async () => { + const schema = S.suspend(() => string) + class A extends S.Class("A")({ a: S.optional(schema) }) {} + const string = S.String + await Util.assertions.decoding.succeed(A, new A({ a: "a" })) + }) + + it("should be a Schema", () => { + class A extends S.Class("A")({ a: S.String }) {} + assertTrue(S.isSchema(A)) + strictEqual(String(A), "(A (Encoded side) <-> A)") + strictEqual(S.format(A), "(A (Encoded side) <-> A)") + }) + + it("should expose the fields", () => { + class A extends S.Class("A")({ a: S.String }) {} + deepStrictEqual(A.fields, { a: S.String }) + }) + + it("should expose the identifier", () => { + class A extends S.Class("A")({ a: S.String }) {} + strictEqual(A.identifier, "A") + }) + + it("should add an identifier annotation", () => { + class A extends S.Class("MyName")({ a: S.String }) {} + strictEqual(A.ast.to.annotations[AST.IdentifierAnnotationId], "MyName") + }) + + describe("constructor", () => { + it("should be a constructor", () => { + class A extends S.Class("A")({ a: S.String }) {} + const instance = new A({ a: "a" }) + strictEqual(instance.a, "a") + assertInstanceOf(instance, A) + }) + + it("should validate the input by default", () => { + class A extends S.Class("A")({ a: S.NonEmptyString }) {} + Util.assertions.parseError( + () => new A({ a: "" }), + `A (Constructor) +└─ ["a"] + └─ NonEmptyString + └─ Predicate refinement failure + └─ Expected a non empty string, actual ""` + ) + Util.assertions.parseError( + () => A.make({ a: "" }), + `A (Constructor) +└─ ["a"] + └─ NonEmptyString + └─ Predicate refinement failure + └─ Expected a non empty string, actual ""` + ) + }) + + it("validation can be disabled", () => { + class A extends S.Class("A")({ a: S.NonEmptyString }) {} + strictEqual(new A({ a: "" }, true).a, "") + strictEqual(new A({ a: "" }, { disableValidation: true }).a, "") + + strictEqual(A.make({ a: "" }, true).a, "") + strictEqual(A.make({ a: "" }, { disableValidation: true }).a, "") + }) + + it("should support defaults", () => { + const b = Symbol.for("b") + class A extends S.Class("A")({ + a: S.propertySignature(S.String).pipe(S.withConstructorDefault(() => "")), + [b]: S.propertySignature(S.Number).pipe(S.withConstructorDefault(() => 1)) + }) {} + deepStrictEqual({ ...new A({ a: "a", [b]: 2 }) }, { a: "a", [b]: 2 }) + deepStrictEqual({ ...new A({ a: "a" }) }, { a: "a", [b]: 1 }) + deepStrictEqual({ ...new A({ [b]: 2 }) }, { a: "", [b]: 2 }) + deepStrictEqual({ ...new A({}) }, { a: "", [b]: 1 }) + + deepStrictEqual({ ...A.make({ a: "a", [b]: 2 }) }, { a: "a", [b]: 2 }) + deepStrictEqual({ ...A.make({ a: "a" }) }, { a: "a", [b]: 1 }) + deepStrictEqual({ ...A.make({ [b]: 2 }) }, { a: "", [b]: 2 }) + deepStrictEqual({ ...A.make({}) }, { a: "", [b]: 1 }) + }) + + it("should support lazy defaults", () => { + let i = 0 + class A extends S.Class("A")({ + a: S.propertySignature(S.Number).pipe(S.withConstructorDefault(() => ++i)) + }) {} + deepStrictEqual({ ...new A({}) }, { a: 1 }) + deepStrictEqual({ ...new A({}) }, { a: 2 }) + new A({ a: 10 }) + deepStrictEqual({ ...new A({}) }, { a: 3 }) + + deepStrictEqual({ ...A.make({}) }, { a: 4 }) + deepStrictEqual({ ...A.make({}) }, { a: 5 }) + new A({ a: 10 }) + deepStrictEqual({ ...A.make({}) }, { a: 6 }) + }) + + it("should treat `undefined` as missing field", () => { + class A extends S.Class("A")({ + a: S.propertySignature(S.UndefinedOr(S.String)).pipe(S.withConstructorDefault(() => "")) + }) {} + deepStrictEqual({ ...new A({}) }, { a: "" }) + deepStrictEqual({ ...new A({ a: undefined }) }, { a: "" }) + + deepStrictEqual({ ...A.make({}) }, { a: "" }) + deepStrictEqual({ ...A.make({ a: undefined }) }, { a: "" }) + }) + + it("should accept void if the Class has no fields", () => { + class A extends S.Class("A")({}) {} + deepStrictEqual({ ...new A() }, {}) + deepStrictEqual({ ...new A(undefined) }, {}) + deepStrictEqual({ ...new A(undefined, true) }, {}) + deepStrictEqual({ ...new A(undefined, false) }, {}) + deepStrictEqual({ ...new A({}) }, {}) + + deepStrictEqual({ ...A.make() }, {}) + deepStrictEqual({ ...A.make(undefined) }, {}) + deepStrictEqual({ ...A.make(undefined, true) }, {}) + deepStrictEqual({ ...A.make(undefined, false) }, {}) + deepStrictEqual({ ...A.make({}) }, {}) + }) + + it("should accept void if the Class has all the fields with a default", () => { + class A extends S.Class("A")({ + a: S.String.pipe(S.propertySignature, S.withConstructorDefault(() => "")) + }) {} + deepStrictEqual({ ...new A() }, { a: "" }) + deepStrictEqual({ ...new A(undefined) }, { a: "" }) + deepStrictEqual({ ...new A(undefined, true) }, { a: "" }) + deepStrictEqual({ ...new A(undefined, false) }, { a: "" }) + deepStrictEqual({ ...new A({}) }, { a: "" }) + + deepStrictEqual({ ...A.make() }, { a: "" }) + deepStrictEqual({ ...A.make(undefined) }, { a: "" }) + deepStrictEqual({ ...A.make(undefined, true) }, { a: "" }) + deepStrictEqual({ ...A.make(undefined, false) }, { a: "" }) + deepStrictEqual({ ...A.make({}) }, { a: "" }) + }) + }) + + it("should support methods", () => { + class A extends S.Class("A")({ a: S.String }) { + method(b: string) { + return `method: ${this.a} ${b}` + } + } + strictEqual(new A({ a: "a" }).method("b"), "method: a b") + }) + + it("should support getters", () => { + class A extends S.Class("A")({ a: S.String }) { + get getter() { + return `getter: ${this.a}` + } + } + strictEqual(new A({ a: "a" }).getter, "getter: a") + }) + + it("using S.annotations() on a Class should return a Schema", () => { + class A extends S.Class("A")({ a: S.String }) {} + const schema = A.pipe(S.annotations({ title: "X" })) + assertTrue(S.isSchema(schema)) + strictEqual(schema.ast._tag, "Transformation") + strictEqual(schema.ast.annotations[AST.TitleAnnotationId], "X") + }) + + it("using the .annotations() method of a Class should return a Schema", () => { + class A extends S.Class("A")({ a: S.String }) {} + const schema = A.annotations({ title: "X" }) + assertTrue(S.isSchema(schema)) + strictEqual(schema.ast._tag, "Transformation") + strictEqual(schema.ast.annotations[AST.TitleAnnotationId], "X") + }) + + it("default toString()", () => { + const b = Symbol.for("b") + class A extends S.Class("A")({ a: S.String, [b]: S.Number }) {} + strictEqual(String(new A({ a: "a", [b]: 1 })), `A({ "a": "a", Symbol(b): 1 })`) + }) + + it("decoding", async () => { + class A extends S.Class("A")({ a: S.NonEmptyString }) {} + await Util.assertions.decoding.succeed(A, { a: "a" }, new A({ a: "a" })) + await Util.assertions.decoding.fail( + A, + { a: "" }, + `(A (Encoded side) <-> A) +└─ Encoded side transformation failure + └─ A (Encoded side) + └─ ["a"] + └─ NonEmptyString + └─ Predicate refinement failure + └─ Expected a non empty string, actual ""` + ) + }) + + it("encoding", async () => { + class A extends S.Class("A")({ a: S.NonEmptyString }) {} + await Util.assertions.encoding.succeed(A, new A({ a: "a" }), { a: "a" }) + await Util.assertions.encoding.succeed(A, { a: "a" }, { a: "a" }) + await Util.assertions.encoding.fail( + A, + new A({ a: "" }, true), + `(A (Encoded side) <-> A) +└─ Encoded side transformation failure + └─ A (Encoded side) + └─ ["a"] + └─ NonEmptyString + └─ Predicate refinement failure + └─ Expected a non empty string, actual ""` + ) + }) + + it("a custom _tag field should be allowed", () => { + class A extends S.Class("A")({ _tag: S.Literal("a", "b") }) {} + Util.expectFields(A.fields, { + _tag: S.Literal("a", "b") + }) + }) + + it("duplicated fields should not be allowed when extending with extend()", () => { + class A extends S.Class("A")({ a: S.String }) {} + throws( + () => { + class A2 extends A.extend("A2")({ a: S.String }) {} + // eslint-disable-next-line @typescript-eslint/no-unused-expressions + A2 + }, + new Error(`Duplicate property signature +details: Duplicate key "a"`) + ) + }) + + it("can be extended with Class fields", () => { + class AB extends S.Class("AB")({ a: S.String, b: S.Number }) {} + class C extends S.Class("C")({ + ...AB.fields, + b: S.String, + c: S.Boolean + }) {} + Util.expectFields(C.fields, { + a: S.String, + b: S.String, + c: S.Boolean + }) + deepStrictEqual({ ...new C({ a: "a", b: "b", c: true }) }, { a: "a", b: "b", c: true }) + }) + + it("can be extended with TaggedClass fields", () => { + class AB extends S.Class("AB")({ a: S.String, b: S.Number }) {} + class D extends S.TaggedClass()("D", { + ...AB.fields, + b: S.String, + c: S.Boolean + }) {} + Util.expectFields(D.fields, { + _tag: S.getClassTag("D"), + a: S.String, + b: S.String, + c: S.Boolean + }) + deepStrictEqual({ ...new D({ a: "a", b: "b", c: true }) }, { _tag: "D", a: "a", b: "b", c: true }) + }) + + it("S.typeSchema(Class)", async () => { + const PersonFromSelf = S.typeSchema(Person) + await Util.assertions.decoding.succeed(PersonFromSelf, new Person({ id: 1, name: "John" })) + await Util.assertions.decoding.fail( + PersonFromSelf, + { id: 1, name: "John" }, + `Expected Person, actual {"id":1,"name":"John"}` + ) + }) + + it("is", () => { + const is = S.is(S.typeSchema(Person)) + assertTrue(is(new Person({ id: 1, name: "name" }))) + assertFalse(is({ id: 1, name: "name" })) + }) + + it("with a field with a context !== never", async () => { + class PersonContext extends S.Class("PersonContext")({ + ...Person.fields, + name: NameString + }) {} + + const person = S.decodeUnknown(PersonContext)({ id: 1, name: "John" }).pipe( + Effect.provideService(Name, "John"), + Effect.runSync + ) + strictEqual(person.name, "John") + + const PersonFromSelf = S.typeSchema(Person) + await Util.assertions.decoding.succeed(PersonFromSelf, new Person({ id: 1, name: "John" })) + await Util.assertions.decoding.fail( + PersonFromSelf, + { id: 1, name: "John" }, + `Expected Person, actual {"id":1,"name":"John"}` + ) + }) + + it("should accept a Struct as argument", () => { + const fields = { a: S.String, b: S.Number } + class A extends S.Class("A")(S.Struct(fields)) {} + Util.expectFields(A.fields, fields) + }) + + it("should accept a refinement of a Struct as argument", async () => { + const fields = { a: S.Number, b: S.Number } + class A extends S.Class("A")( + S.Struct(fields).pipe(S.filter(({ a, b }) => a === b ? undefined : "a should be equal to b")) + ) {} + Util.expectFields(A.fields, fields) + await Util.assertions.decoding.succeed(A, new A({ a: 1, b: 1 })) + await Util.assertions.decoding.fail( + A, + { a: 1, b: 2 }, + `(A (Encoded side) <-> A) +└─ Encoded side transformation failure + └─ A (Encoded side) + └─ Predicate refinement failure + └─ a should be equal to b` + ) + Util.assertions.parseError( + () => new A({ a: 1, b: 2 }), + `A (Constructor) +└─ Predicate refinement failure + └─ a should be equal to b` + ) + }) + + it("Data.Class", () => { + const person = new Person({ id: 1, name: "John" }) + const personAge = new PersonWithAge({ id: 1, name: "John", age: 30 }) + + strictEqual(String(person), `Person({ "id": 1, "name": "John" })`) + strictEqual(String(personAge), `PersonWithAge({ "id": 1, "name": "John", "age": 30 })`) + + assertInstanceOf(person, Data.Class) + assertInstanceOf(personAge, Data.Class) + + const person2 = new Person({ id: 1, name: "John" }) + assertTrue(Equal.equals(person, person2)) + + const person3 = new Person({ id: 2, name: "John" }) + assertFalse(Equal.equals(person, person3)) + }) + + it("pretty", () => { + const schema = Person + Util.assertions.pretty(schema, new Person({ id: 1, name: "John" }), `Person({ "id": 1, "name": "John" })`) + }) + + describe("encode", () => { + it("struct a class without methods nor getters", async () => { + class A extends S.Class("A")({ + n: S.NumberFromString + }) {} + await Util.assertions.encoding.succeed(A, { n: 1 }, { n: "1" }) + }) + + it("struct a class with a getter", async () => { + class A extends S.Class("A")({ + n: S.NumberFromString + }) { + get s() { + return "s" + } + } + await Util.assertions.encoding.succeed(A, { n: 1 } as any, { n: "1" }) + }) + + it("struct nested classes", async () => { + class A extends S.Class("A")({ + n: S.NumberFromString + }) {} + class B extends S.Class("B")({ + a: A + }) {} + await Util.assertions.encoding.succeed(S.Union(B, S.NumberFromString), 1, "1") + await Util.assertions.encoding.succeed(B, { a: { n: 1 } }, { a: { n: "1" } }) + }) + + it("class a class with a getter", async () => { + class A extends S.Class("A")({ + n: S.NumberFromString + }) { + get s() { + return "s" + } + } + class B extends S.Class("B")({ + n: S.NumberFromString, + s: S.String + }) {} + + await Util.assertions.encoding.succeed(B, new A({ n: 1 }), { n: "1", s: "s" }) + }) + + describe("encode(S.typeSchema(Class))", () => { + it("should always return an instance", async () => { + class A extends S.Class("A")({ + n: S.NumberFromString + }) {} + const schema = S.typeSchema(A) + await Util.assertions.encoding.succeed(schema, new A({ n: 1 }), new A({ n: 1 })) + await Util.assertions.encoding.succeed(schema, { n: 1 }, new A({ n: 1 })) + }) + + it("should fail on bad values", async () => { + class A extends S.Class("A")({ + n: S.NumberFromString + }) {} + const schema = S.typeSchema(A) + await Util.assertions.encoding.fail( + schema, + null as any, + `Expected A (Type side), actual null` + ) + }) + }) + }) + + it("arbitrary", () => { + class A extends S.Class("A")({ a: S.String }) {} + Util.assertions.arbitrary.validateGeneratedValues(A) + }) + + it("should expose a make constructor", () => { + class A extends S.Class("A")({ + n: S.NumberFromString + }) { + a() { + return this.n + "a" + } + } + const a = A.make({ n: 1 }) + assertInstanceOf(a, A) + strictEqual(a.a(), "1a") + }) + + describe("should support annotations when declaring the Class", () => { + it("single argument", async () => { + class A extends S.Class("A")({ + a: S.NonEmptyString + }, { title: "mytitle" }) {} + + strictEqual(A.ast.to.annotations[AST.TitleAnnotationId], "mytitle") + + await Util.assertions.encoding.fail( + A, + { a: "" }, + `(A (Encoded side) <-> A) +└─ Type side transformation failure + └─ mytitle + └─ ["a"] + └─ NonEmptyString + └─ Predicate refinement failure + └─ Expected a non empty string, actual ""` + ) + }) + + it("tuple argument", async () => { + class A extends S.Class("A")( + { + a: S.NonEmptyString + }, + [ + { identifier: "TypeID", description: "TypeDescription" }, + { identifier: "TransformationID" }, + { identifier: "EncodedID" } + ] + ) {} + assertSome(AST.getIdentifierAnnotation(A.ast.to), "TypeID") + assertSome(AST.getIdentifierAnnotation(A.ast), "TransformationID") + assertSome(AST.getIdentifierAnnotation(A.ast.from), "EncodedID") + + await Util.assertions.decoding.fail( + A, + {}, + `TransformationID +└─ Encoded side transformation failure + └─ EncodedID + └─ ["a"] + └─ is missing` + ) + + await Util.assertions.encoding.fail( + A, + { a: "" }, + `TransformationID +└─ Type side transformation failure + └─ TypeID + └─ ["a"] + └─ NonEmptyString + └─ Predicate refinement failure + └─ Expected a non empty string, actual ""` + ) + + const ctor = { make: A.make.bind(A) } + + Util.assertions.make.fail( + ctor, + null as any, + `TypeID +└─ ["a"] + └─ is missing` + ) + + deepStrictEqual(JSONSchema.make(S.typeSchema(A)), { + "$defs": { + "NonEmptyString": { + "title": "nonEmptyString", + "description": "a non empty string", + "minLength": 1, + "type": "string" + }, + "TypeID": { + "additionalProperties": false, + "description": "TypeDescription", + "properties": { + "a": { + "$ref": "#/$defs/NonEmptyString" + } + }, + "required": [ + "a" + ], + "type": "object" + } + }, + "$ref": "#/$defs/TypeID", + "$schema": "http://json-schema.org/draft-07/schema#" + }) + + deepStrictEqual(JSONSchema.make(A), { + "$defs": { + "NonEmptyString": { + "title": "nonEmptyString", + "description": "a non empty string", + "minLength": 1, + "type": "string" + }, + "TransformationID": { + "additionalProperties": false, + "description": "TypeDescription", + "properties": { + "a": { + "$ref": "#/$defs/NonEmptyString" + } + }, + "required": [ + "a" + ], + "type": "object" + } + }, + "$ref": "#/$defs/TransformationID", + "$schema": "http://json-schema.org/draft-07/schema#" + }) + }) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/Class/TaggedClass.test.ts b/repos/effect/packages/effect/test/Schema/Schema/Class/TaggedClass.test.ts new file mode 100644 index 0000000..edf986a --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/Class/TaggedClass.test.ts @@ -0,0 +1,363 @@ +import { describe, it } from "@effect/vitest" +import { + assertFalse, + assertInstanceOf, + assertSome, + assertTrue, + deepStrictEqual, + strictEqual, + throws +} from "@effect/vitest/utils" +import { JSONSchema, pipe, Schema as S, SchemaAST as AST, Struct } from "effect" +import * as Util from "../../TestUtils.js" + +describe("TaggedClass", () => { + it("the constructor should add a `_tag` field", () => { + class TA extends S.TaggedClass()("TA", { a: S.String }) {} + deepStrictEqual({ ...new TA({ a: "a" }) }, { _tag: "TA", a: "a" }) + }) + + it("should expose the fields and the tag", () => { + class TA extends S.TaggedClass()("TA", { a: S.String }) {} + Util.expectFields(TA.fields, { _tag: S.getClassTag("TA"), a: S.String }) + deepStrictEqual(S.Struct(TA.fields).make({ a: "a" }), { _tag: "TA", a: "a" }) + strictEqual(TA._tag, "TA") + }) + + it("should expose the identifier", () => { + class TA extends S.TaggedClass()("TA", { a: S.String }) {} + strictEqual(TA.identifier, "TA") + class TB extends S.TaggedClass("id")("TB", { a: S.String }) {} + strictEqual(TB.identifier, "id") + }) + + it("constructor parameters should not overwrite the tag", async () => { + class A extends S.TaggedClass()("A", { + a: S.String + }) {} + strictEqual(new A({ ...{ _tag: "B", a: "a" } })._tag, "A") + strictEqual(new A({ ...{ _tag: "B", a: "a" } }, true)._tag, "A") + }) + + it("a TaggedClass with no fields should have a void constructor", () => { + class TA extends S.TaggedClass()("TA", {}) {} + deepStrictEqual({ ...new TA() }, { _tag: "TA" }) + deepStrictEqual({ ...new TA(undefined) }, { _tag: "TA" }) + deepStrictEqual({ ...new TA(undefined, true) }, { _tag: "TA" }) + }) + + it("a custom _tag field should be not allowed", () => { + throws( + () => { + class _TA extends S.TaggedClass<_TA>()("TA", { _tag: S.Literal("X"), a: S.String }) {} + // eslint-disable-next-line @typescript-eslint/no-unused-expressions + _TA + }, + new Error(`Duplicate property signature +details: Duplicate key "_tag"`) + ) + }) + + it("should accept a Struct as argument", () => { + const fields = { a: S.String, b: S.Number } + class A extends S.TaggedClass()("A", S.Struct(fields)) {} + Util.expectFields(A.fields, { _tag: S.getClassTag("A"), ...fields }) + }) + + it("should accept a refinement of a Struct as argument", async () => { + const fields = { a: S.Number, b: S.Number } + class A extends S.TaggedClass()( + "A", + S.Struct(fields).pipe(S.filter(({ a, b }) => a === b ? undefined : "a should be equal to b")) + ) {} + Util.expectFields(A.fields, { _tag: S.getClassTag("A"), ...fields }) + await Util.assertions.decoding.succeed(A, new A({ a: 1, b: 1 })) + await Util.assertions.decoding.fail( + A, + { _tag: "A", a: 1, b: 2 }, + `(A (Encoded side) <-> A) +└─ Encoded side transformation failure + └─ A (Encoded side) + └─ Predicate refinement failure + └─ a should be equal to b` + ) + Util.assertions.parseError( + () => new A({ a: 1, b: 2 }), + `A (Constructor) +└─ Predicate refinement failure + └─ a should be equal to b` + ) + }) + + it("decoding", async () => { + class TA extends S.TaggedClass()("TA", { a: S.NonEmptyString }) {} + await Util.assertions.decoding.succeed(TA, { _tag: "TA", a: "a" }, new TA({ a: "a" })) + await Util.assertions.decoding.fail( + TA, + { a: "a" }, + `(TA (Encoded side) <-> TA) +└─ Encoded side transformation failure + └─ TA (Encoded side) + └─ ["_tag"] + └─ is missing` + ) + await Util.assertions.decoding.fail( + TA, + { _tag: "TA", a: "" }, + `(TA (Encoded side) <-> TA) +└─ Encoded side transformation failure + └─ TA (Encoded side) + └─ ["a"] + └─ NonEmptyString + └─ Predicate refinement failure + └─ Expected a non empty string, actual ""` + ) + }) + + it("encoding", async () => { + class TA extends S.TaggedClass()("TA", { a: S.NonEmptyString }) {} + await Util.assertions.encoding.succeed(TA, new TA({ a: "a" }), { _tag: "TA", a: "a" }) + await Util.assertions.encoding.succeed(TA, { _tag: "TA", a: "a" } as any, { _tag: "TA", a: "a" }) + await Util.assertions.encoding.fail( + TA, + new TA({ a: "" }, true), + `(TA (Encoded side) <-> TA) +└─ Encoded side transformation failure + └─ TA (Encoded side) + └─ ["a"] + └─ NonEmptyString + └─ Predicate refinement failure + └─ Expected a non empty string, actual ""` + ) + }) + + it("can be extended with Class fields", () => { + class TA extends S.TaggedClass()("TA", { a: S.String }) {} + class B extends S.Class("B")({ + b: S.Number, + ...TA.fields + }) {} + Util.expectFields(B.fields, { + _tag: S.getClassTag("TA"), + a: S.String, + b: S.Number + }) + deepStrictEqual({ ...new B({ _tag: "TA", a: "a", b: 1 }) }, { _tag: "TA", a: "a", b: 1 }) + }) + + it("can be extended with TaggedClass fields", () => { + class TA extends S.TaggedClass()("TA", { a: S.String }) {} + class TB extends S.TaggedClass()("TB", { + b: S.Number, + ...pipe(TA.fields, Struct.omit("_tag")) + }) {} + Util.expectFields(TB.fields, { + _tag: S.getClassTag("TB"), + a: S.String, + b: S.Number + }) + deepStrictEqual({ ...new TB({ a: "a", b: 1 }) }, { _tag: "TB", a: "a", b: 1 }) + }) + + it("equivalence", () => { + class A extends S.TaggedClass()("A", { + a: S.String + }) {} + const eqA = S.equivalence(A) + assertTrue(eqA(new A({ a: "a" }), new A({ a: "a" }))) + assertFalse(eqA(new A({ a: "a" }), new A({ a: "b" }))) + + class B extends S.TaggedClass()("B", { + b: S.Number, + as: S.Array(A) + }) {} + const eqB = S.equivalence(B) + assertTrue(eqB(new B({ b: 1, as: [] }), new B({ b: 1, as: [] }))) + assertFalse(eqB(new B({ b: 1, as: [] }), new B({ b: 2, as: [] }))) + assertTrue(eqB(new B({ b: 1, as: [new A({ a: "a" })] }), new B({ b: 1, as: [new A({ a: "a" })] }))) + assertFalse(eqB(new B({ b: 1, as: [new A({ a: "a" })] }), new B({ b: 1, as: [new A({ a: "b" })] }))) + }) + + it("baseline", () => { + class TaggedPerson extends S.TaggedClass()("TaggedPerson", { + id: S.Number, + name: S.String.pipe(S.nonEmptyString()) + }) { + get upperName() { + return this.name.toUpperCase() + } + } + + class TaggedPersonWithAge extends TaggedPerson.extend("TaggedPersonWithAge")({ + age: S.Number + }) { + get isAdult() { + return this.age >= 18 + } + } + + let person = new TaggedPersonWithAge({ id: 1, name: "John", age: 30 }) + + strictEqual(String(person), `TaggedPersonWithAge({ "_tag": "TaggedPerson", "id": 1, "name": "John", "age": 30 })`) + strictEqual(person._tag, "TaggedPerson") + strictEqual(person.upperName, "JOHN") + + Util.assertions.parseError( + () => S.decodeUnknownSync(TaggedPersonWithAge)({ id: 1, name: "John", age: 30 }), + `(TaggedPersonWithAge (Encoded side) <-> TaggedPersonWithAge) +└─ Encoded side transformation failure + └─ TaggedPersonWithAge (Encoded side) + └─ ["_tag"] + └─ is missing` + ) + person = S.decodeUnknownSync(TaggedPersonWithAge)({ + _tag: "TaggedPerson", + id: 1, + name: "John", + age: 30 + }) + strictEqual(person._tag, "TaggedPerson") + strictEqual(person.upperName, "JOHN") + }) + + it("should expose a make constructor", () => { + class TA extends S.TaggedClass()("TA", { + n: S.NumberFromString + }) { + a() { + return this.n + "a" + } + } + const ta = TA.make({ n: 1 }) + assertInstanceOf(ta, TA) + strictEqual(ta._tag, "TA") + strictEqual(ta.a(), "1a") + }) + + describe("should support annotations when declaring the Class", () => { + it("single argument", async () => { + class A extends S.TaggedClass()("A", { + a: S.NonEmptyString + }, { title: "mytitle" }) {} + + strictEqual(A.ast.to.annotations[AST.TitleAnnotationId], "mytitle") + + await Util.assertions.encoding.fail( + A, + { _tag: "A", a: "" }, + `(A (Encoded side) <-> A) +└─ Type side transformation failure + └─ mytitle + └─ ["a"] + └─ NonEmptyString + └─ Predicate refinement failure + └─ Expected a non empty string, actual ""` + ) + }) + + it("tuple argument", async () => { + class A extends S.TaggedClass()("A", { + a: S.NonEmptyString + }, [ + { identifier: "TypeID", description: "TypeDescription" }, + { identifier: "TransformationID" }, + { identifier: "EncodedID" } + ]) {} + assertSome(AST.getIdentifierAnnotation(A.ast.to), "TypeID") + assertSome(AST.getIdentifierAnnotation(A.ast), "TransformationID") + assertSome(AST.getIdentifierAnnotation(A.ast.from), "EncodedID") + + await Util.assertions.decoding.fail( + A, + {}, + `TransformationID +└─ Encoded side transformation failure + └─ EncodedID + └─ ["a"] + └─ is missing` + ) + + await Util.assertions.encoding.fail( + A, + { _tag: "A", a: "" }, + `TransformationID +└─ Type side transformation failure + └─ TypeID + └─ ["a"] + └─ NonEmptyString + └─ Predicate refinement failure + └─ Expected a non empty string, actual ""` + ) + + const ctor = { make: A.make.bind(A) } + + Util.assertions.make.fail( + ctor, + null as any, + `TypeID +└─ ["a"] + └─ is missing` + ) + + deepStrictEqual(JSONSchema.make(S.typeSchema(A)), { + "$defs": { + "NonEmptyString": { + "title": "nonEmptyString", + "description": "a non empty string", + "minLength": 1, + "type": "string" + }, + "TypeID": { + "additionalProperties": false, + "description": "TypeDescription", + "properties": { + "_tag": { + "enum": [ + "A" + ], + "type": "string" + }, + "a": { + "$ref": "#/$defs/NonEmptyString" + } + }, + "required": ["a", "_tag"], + "type": "object" + } + }, + "$ref": "#/$defs/TypeID", + "$schema": "http://json-schema.org/draft-07/schema#" + }) + + deepStrictEqual(JSONSchema.make(A), { + "$defs": { + "NonEmptyString": { + "title": "nonEmptyString", + "description": "a non empty string", + "minLength": 1, + "type": "string" + }, + "TransformationID": { + "additionalProperties": false, + "description": "TypeDescription", + "properties": { + "_tag": { + "enum": [ + "A" + ], + "type": "string" + }, + "a": { + "$ref": "#/$defs/NonEmptyString" + } + }, + "required": ["a", "_tag"], + "type": "object" + } + }, + "$ref": "#/$defs/TransformationID", + "$schema": "http://json-schema.org/draft-07/schema#" + }) + }) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/Class/TaggedError.test.ts b/repos/effect/packages/effect/test/Schema/Schema/Class/TaggedError.test.ts new file mode 100644 index 0000000..ec7a8e2 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/Class/TaggedError.test.ts @@ -0,0 +1,273 @@ +import { describe, it } from "@effect/vitest" +import { assertInclude, assertInstanceOf, assertSome, deepStrictEqual, strictEqual } from "@effect/vitest/utils" +import { Cause, Effect, Inspectable, JSONSchema, Schema, SchemaAST as AST } from "effect" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("TaggedError", () => { + it("should expose the fields and the tag", () => { + class TE extends S.TaggedError()("TE", { a: S.String }) {} + Util.expectFields(TE.fields, { _tag: S.getClassTag("TE"), a: S.String }) + deepStrictEqual(S.Struct(TE.fields).make({ a: "a" }), { _tag: "TE", a: "a" }) + strictEqual(TE._tag, "TE") + }) + + it("make should respect custom constructors", () => { + class MyError extends Schema.TaggedError()( + "MyError", + { message: Schema.String } + ) { + constructor({ a, b }: { a: string; b: string }) { + super({ message: `${a}:${b}` }) + } + } + + strictEqual(MyError.make({ a: "a", b: "b" }).message, "a:b") + strictEqual(new MyError({ a: "a", b: "b" }).message, "a:b") + }) + + it("should accept a Struct as argument", () => { + const fields = { a: S.String, b: S.Number } + class A extends S.TaggedError()("A", S.Struct(fields)) {} + Util.expectFields(A.fields, { _tag: S.getClassTag("A"), ...fields }) + }) + + it("should accept a refinement of a Struct as argument", async () => { + const fields = { a: S.Number, b: S.Number } + class A extends S.TaggedError()( + "A", + S.Struct(fields).pipe(S.filter(({ a, b }) => a === b ? undefined : "a should be equal to b")) + ) {} + Util.expectFields(A.fields, { _tag: S.getClassTag("A"), ...fields }) + await Util.assertions.decoding.succeed(A, new A({ a: 1, b: 1 })) + await Util.assertions.decoding.fail( + A, + { _tag: "A", a: 1, b: 2 }, + `(A (Encoded side) <-> A) +└─ Encoded side transformation failure + └─ A (Encoded side) + └─ Predicate refinement failure + └─ a should be equal to b` + ) + Util.assertions.parseError( + () => new A({ a: 1, b: 2 }), + `A (Constructor) +└─ Predicate refinement failure + └─ a should be equal to b` + ) + }) + + it("baseline", () => { + class MyError extends S.TaggedError()("MyError", { + id: S.Number + }) {} + + let err = new MyError({ id: 1 }) + + strictEqual(String(err), `MyError: { "id": 1 }`) + assertInclude(err.stack, "TaggedError.test.ts:") + strictEqual(err._tag, "MyError") + strictEqual(err.id, 1) + + err = Effect.runSync(Effect.flip(err)) + strictEqual(err._tag, "MyError") + strictEqual(err.id, 1) + + err = S.decodeUnknownSync(MyError)({ _tag: "MyError", id: 1 }) + strictEqual(err._tag, "MyError") + strictEqual(err.id, 1) + }) + + it("message", () => { + class MyError extends S.TaggedError()("MyError", { + id: S.Number + }) { + get message() { + return `bad id: ${this.id}` + } + } + + const err = new MyError({ id: 1 }) + + assertInclude(String(err), `MyError: bad id: 1`) + assertInclude(err.stack, "TaggedError.test.ts:") + strictEqual(err._tag, "MyError") + strictEqual(err.id, 1) + }) + + it("message field", () => { + class MyError extends S.TaggedError()("MyError", { + id: S.Number, + message: S.String + }) { + } + + const err = new MyError({ id: 1, message: "boom" }) + + assertInclude(String(err), `MyError: boom`) + assertInclude(err.stack, "TaggedError.test.ts:") + strictEqual(err._tag, "MyError") + strictEqual(err.id, 1) + }) + + it("should expose a make constructor", () => { + class A extends S.TaggedError()("A", { + n: S.NumberFromString + }) { + a() { + return this.n + "a" + } + } + const a = A.make({ n: 1 }) + assertInstanceOf(a, A) + strictEqual(a._tag, "A") + strictEqual(a.a(), "1a") + }) + + it("cause", () => { + class MyError extends S.TaggedError()("MyError", { + cause: Schema.Defect + }) {} + + const err = new MyError({ cause: new Error("child") }) + assertInclude(Cause.pretty(Cause.fail(err), { renderErrorCause: true }), "[cause]: Error: child") + // ensure node renders the error directly + deepStrictEqual(err[Inspectable.NodeInspectSymbol](), err) + }) + + describe("should support annotations when declaring the Class", () => { + it("single argument", async () => { + class A extends S.TaggedError()("A", { + a: S.NonEmptyString + }, { title: "mytitle" }) {} + + strictEqual(A.ast.to.annotations[AST.TitleAnnotationId], "mytitle") + + await Util.assertions.encoding.fail( + A, + { _tag: "A", a: "" } as any, + `(A (Encoded side) <-> A) +└─ Type side transformation failure + └─ mytitle + └─ ["a"] + └─ NonEmptyString + └─ Predicate refinement failure + └─ Expected a non empty string, actual ""` + ) + }) + + it("tuple argument", async () => { + class A extends S.TaggedError()("A", { + a: S.NonEmptyString + }, [ + { identifier: "TypeID", description: "TypeDescription" }, + { identifier: "TransformationID" }, + { identifier: "EncodedID" } + ]) {} + assertSome(AST.getIdentifierAnnotation(A.ast.to), "TypeID") + assertSome(AST.getIdentifierAnnotation(A.ast), "TransformationID") + assertSome(AST.getIdentifierAnnotation(A.ast.from), "EncodedID") + + await Util.assertions.decoding.fail( + A, + {}, + `TransformationID +└─ Encoded side transformation failure + └─ EncodedID + └─ ["a"] + └─ is missing` + ) + + await Util.assertions.encoding.fail( + A, + { _tag: "A", a: "" } as any, + `TransformationID +└─ Type side transformation failure + └─ TypeID + └─ ["a"] + └─ NonEmptyString + └─ Predicate refinement failure + └─ Expected a non empty string, actual ""` + ) + + const ctor = { make: A.make.bind(A) } + + Util.assertions.make.fail( + ctor, + null as any, + `TypeID +└─ ["a"] + └─ is missing` + ) + + deepStrictEqual(JSONSchema.make(S.typeSchema(A)), { + "$defs": { + "NonEmptyString": { + "title": "nonEmptyString", + "description": "a non empty string", + "minLength": 1, + "type": "string" + }, + "TypeID": { + "additionalProperties": false, + "description": "TypeDescription", + "properties": { + "_tag": { + "enum": [ + "A" + ], + "type": "string" + }, + "a": { + "$ref": "#/$defs/NonEmptyString" + } + }, + "required": ["a", "_tag"], + "type": "object" + } + }, + "$ref": "#/$defs/TypeID", + "$schema": "http://json-schema.org/draft-07/schema#" + }) + + deepStrictEqual(JSONSchema.make(A), { + "$defs": { + "NonEmptyString": { + "title": "nonEmptyString", + "description": "a non empty string", + "minLength": 1, + "type": "string" + }, + "TransformationID": { + "additionalProperties": false, + "description": "TypeDescription", + "properties": { + "_tag": { + "enum": [ + "A" + ], + "type": "string" + }, + "a": { + "$ref": "#/$defs/NonEmptyString" + } + }, + "required": ["a", "_tag"], + "type": "object" + } + }, + "$ref": "#/$defs/TransformationID", + "$schema": "http://json-schema.org/draft-07/schema#" + }) + }) + }) + + it("should allow an optional `message` field", () => { + class MyError extends S.TaggedError()("MyError", { + message: S.optional(S.String) + }) {} + + const err = new MyError({}) + strictEqual(err.message, "") + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/Class/TaggedRequest.test.ts b/repos/effect/packages/effect/test/Schema/Schema/Class/TaggedRequest.test.ts new file mode 100644 index 0000000..747374b --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/Class/TaggedRequest.test.ts @@ -0,0 +1,338 @@ +import { describe, it } from "@effect/vitest" +import { assertInstanceOf, assertSome, assertTrue, deepStrictEqual, strictEqual } from "@effect/vitest/utils" +import { Context, Effect, Equal, Exit, JSONSchema, ParseResult, Request, Schema as S, SchemaAST as AST } from "effect" +import * as Util from "../../TestUtils.js" + +const Name = Context.GenericTag<"Name", string>("Name") +const NameString = S.String.pipe( + S.nonEmptyString(), + S.transformOrFail( + S.String, + { + decode: (_, _opts, ast) => + Name.pipe( + Effect.filterOrFail( + (name) => _ === name, + () => new ParseResult.Type(ast, _, "Does not match Name") + ) + ), + encode: (_) => ParseResult.succeed(_) + } + ) +) + +const Id = Context.GenericTag<"Id", number>("Name") +const IdNumber = S.Number.pipe( + S.transformOrFail( + S.Number, + { + strict: true, + decode: (_, _opts, ast) => + Effect.filterOrFail( + Id, + (id) => _ === id, + () => new ParseResult.Type(ast, _, "Does not match Id") + ), + encode: (_) => ParseResult.succeed(_) + } + ) +) + +describe("TaggedRequest", () => { + it("should expose the fields, the tag, the success and the failure schema", () => { + class TRA extends S.TaggedRequest()("TRA", { + failure: S.String, + success: S.Number, + payload: { + id: S.Number + } + }) {} + Util.expectFields(TRA.fields, { + _tag: S.getClassTag("TRA"), + id: S.Number + }) + strictEqual(TRA._tag, "TRA") + strictEqual(TRA.success, S.Number) + strictEqual(TRA.failure, S.String) + }) + + it("should expose the identifier", () => { + class TRA extends S.TaggedRequest()("TRA", { + failure: S.String, + success: S.Number, + payload: { + id: S.Number + } + }) {} + strictEqual(TRA.identifier, "TRA") + class TRB extends S.TaggedRequest("id")("TRB", { + failure: S.String, + success: S.Number, + payload: { + id: S.Number + } + }) {} + strictEqual(TRB.identifier, "id") + }) + + it("baseline", () => { + class MyRequest extends S.TaggedRequest()("MyRequest", { + failure: S.String, + success: S.Number, + payload: { + id: S.Number + } + }) {} + + let req = new MyRequest({ id: 1 }) + + strictEqual(String(req), `MyRequest({ "_tag": "MyRequest", "id": 1 })`) + strictEqual(req._tag, "MyRequest") + strictEqual(req.id, 1) + assertTrue(Request.isRequest(req)) + + req = S.decodeSync(MyRequest)({ _tag: "MyRequest", id: 1 }) + strictEqual(req._tag, "MyRequest") + strictEqual(req.id, 1) + assertTrue(Request.isRequest(req)) + }) + + it("TaggedRequest extends SerializableWithExit", () => { + class MyRequest extends S.TaggedRequest()("MyRequest", { + failure: S.String, + success: S.NumberFromString, + payload: { + id: S.Number + } + }) {} + + const req = new MyRequest({ id: 1 }) + deepStrictEqual( + S.serialize(req).pipe(Effect.runSync), + { _tag: "MyRequest", id: 1 } + ) + assertTrue(Equal.equals( + S.deserialize(req, { _tag: "MyRequest", id: 1 }).pipe(Effect.runSync), + req + )) + deepStrictEqual( + S.serializeExit(req, Exit.fail("fail")).pipe(Effect.runSync), + { _tag: "Failure", cause: { _tag: "Fail", error: "fail" } } + ) + deepStrictEqual( + S.deserializeExit(req, { _tag: "Failure", cause: { _tag: "Fail", error: "fail" } }) + .pipe(Effect.runSync), + Exit.fail("fail") + ) + deepStrictEqual( + S.serializeExit(req, Exit.succeed(123)).pipe(Effect.runSync), + { _tag: "Success", value: "123" } + ) + deepStrictEqual( + S.deserializeExit(req, { _tag: "Success", value: "123" }).pipe(Effect.runSync), + Exit.succeed(123) + ) + }) + + it("TaggedRequest context", () => { + class MyRequest extends S.TaggedRequest()("MyRequest", { + failure: NameString, + success: S.Number, + payload: { + id: IdNumber + } + }) {} + + let req = new MyRequest({ id: 1 }, true) + strictEqual(String(req), `MyRequest({ "_tag": "MyRequest", "id": 1 })`) + + req = S.decode(MyRequest)({ _tag: "MyRequest", id: 1 }).pipe( + Effect.provideService(Id, 1), + Effect.runSync + ) + strictEqual(String(req), `MyRequest({ "_tag": "MyRequest", "id": 1 })`) + + deepStrictEqual( + S.serialize(req).pipe( + Effect.provideService(Id, 1), + Effect.runSync + ), + { _tag: "MyRequest", id: 1 } + ) + deepStrictEqual( + S.deserialize(req, { _tag: "MyRequest", id: 1 }).pipe( + Effect.provideService(Id, 1), + Effect.runSync + ), + req + ) + deepStrictEqual( + S.serializeExit(req, Exit.fail("fail")).pipe( + Effect.provideService(Name, "fail"), + Effect.runSync + ), + { _tag: "Failure", cause: { _tag: "Fail", error: "fail" } } + ) + deepStrictEqual( + S.deserializeExit(req, { _tag: "Failure", cause: { _tag: "Fail", error: "fail" } }) + .pipe( + Effect.provideService(Name, "fail"), + Effect.runSync + ), + Exit.fail("fail") + ) + }) + + it("should expose a make constructor", () => { + class TRA extends S.TaggedRequest()("TRA", { + failure: S.String, + success: S.Number, + payload: { + n: S.NumberFromString + } + }) { + a() { + return this.n + "a" + } + } + const tra = TRA.make({ n: 1 }) + assertInstanceOf(tra, TRA) + strictEqual(tra._tag, "TRA") + strictEqual(tra.a(), "1a") + }) + + describe("should support annotations when declaring the Class", () => { + it("single argument", async () => { + class A extends S.TaggedRequest()("A", { + failure: S.String, + success: S.Number, + payload: { + a: S.NonEmptyString + } + }, { title: "mytitle" }) {} + + strictEqual(A.ast.to.annotations[AST.TitleAnnotationId], "mytitle") + + await Util.assertions.encoding.fail( + A, + { _tag: "A", a: "" } as any, + `(A (Encoded side) <-> A) +└─ Type side transformation failure + └─ mytitle + └─ ["a"] + └─ NonEmptyString + └─ Predicate refinement failure + └─ Expected a non empty string, actual ""` + ) + }) + + it("tuple argument", async () => { + class A extends S.TaggedRequest()("A", { + failure: S.String, + success: S.Number, + payload: { + a: S.NonEmptyString + } + }, [ + { identifier: "TypeID", description: "TypeDescription" }, + { identifier: "TransformationID" }, + { identifier: "EncodedID" } + ]) {} + assertSome(AST.getIdentifierAnnotation(A.ast.to), "TypeID") + assertSome(AST.getIdentifierAnnotation(A.ast), "TransformationID") + assertSome(AST.getIdentifierAnnotation(A.ast.from), "EncodedID") + + await Util.assertions.decoding.fail( + A, + {}, + `TransformationID +└─ Encoded side transformation failure + └─ EncodedID + └─ ["_tag"] + └─ is missing` + ) + + await Util.assertions.encoding.fail( + A, + { _tag: "A", a: "" } as any, + `TransformationID +└─ Type side transformation failure + └─ TypeID + └─ ["a"] + └─ NonEmptyString + └─ Predicate refinement failure + └─ Expected a non empty string, actual ""` + ) + + const ctor = { make: A.make.bind(A) } + + Util.assertions.make.fail( + ctor, + null as any, + `TypeID +└─ ["a"] + └─ is missing` + ) + + deepStrictEqual(JSONSchema.make(S.typeSchema(A)), { + "$defs": { + "NonEmptyString": { + "title": "nonEmptyString", + "description": "a non empty string", + "minLength": 1, + "type": "string" + }, + "TypeID": { + "additionalProperties": false, + "description": "TypeDescription", + "properties": { + "_tag": { + "enum": [ + "A" + ], + "type": "string" + }, + "a": { + "$ref": "#/$defs/NonEmptyString" + } + }, + "required": ["_tag", "a"], + "type": "object" + } + }, + "$ref": "#/$defs/TypeID", + "$schema": "http://json-schema.org/draft-07/schema#" + }) + + deepStrictEqual(JSONSchema.make(A), { + "$defs": { + "NonEmptyString": { + "title": "nonEmptyString", + "description": "a non empty string", + "minLength": 1, + "type": "string" + }, + "TransformationID": { + "additionalProperties": false, + "description": "TypeDescription", + "properties": { + "_tag": { + "enum": [ + "A" + ], + "type": "string" + }, + "a": { + "$ref": "#/$defs/NonEmptyString" + } + }, + "required": ["_tag", "a"], + "type": "object" + } + }, + "$ref": "#/$defs/TransformationID", + "$schema": "http://json-schema.org/draft-07/schema#" + }) + }) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/Class/extend.test.ts b/repos/effect/packages/effect/test/Schema/Schema/Class/extend.test.ts new file mode 100644 index 0000000..2add95d --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/Class/extend.test.ts @@ -0,0 +1,322 @@ +import { describe, it } from "@effect/vitest" +import { assertInstanceOf, assertSome, deepStrictEqual, strictEqual } from "@effect/vitest/utils" +import { JSONSchema, Schema as S, SchemaAST as AST } from "effect" +import * as Util from "../../TestUtils.js" + +class Person extends S.Class("Person")({ + id: S.Number, + name: S.String.pipe(S.nonEmptyString()) +}) { + get upperName() { + return this.name.toUpperCase() + } +} + +class PersonWithAge extends Person.extend("PersonWithAge")({ + age: S.Number +}) { + get isAdult() { + return this.age >= 18 + } +} + +class PersonWithNick extends PersonWithAge.extend("PersonWithNick")({ + nick: S.String +}) {} + +describe("extend", () => { + it("1 extend", () => { + const person = S.decodeUnknownSync(PersonWithAge)({ + id: 1, + name: "John", + age: 30 + }) + deepStrictEqual(PersonWithAge.fields, { + ...Person.fields, + age: S.Number + }) + strictEqual(PersonWithAge.identifier, "PersonWithAge") + strictEqual(person.name, "John") + strictEqual(person.age, 30) + strictEqual(person.isAdult, true) + strictEqual(person.upperName, "JOHN") + strictEqual(typeof person.upperName, "string") + }) + + it("2 extend", () => { + const person = S.decodeUnknownSync(PersonWithNick)({ + id: 1, + name: "John", + age: 30, + nick: "Joe" + }) + strictEqual(person.age, 30) + strictEqual(person.nick, "Joe") + }) + + it("should accept a Struct as argument", () => { + const baseFields = { base: S.String } + class Base extends S.Class("Base")(baseFields) {} + const fields = { a: S.String, b: S.Number } + class A extends Base.extend("A")(S.Struct(fields)) {} + Util.expectFields(A.fields, { ...baseFields, ...fields }) + }) + + it("should accept a refinement of a Struct as argument", async () => { + const baseFields = { base: S.String } + class Base extends S.Class("Base")(baseFields) {} + const fields = { a: S.Number, b: S.Number } + class A extends Base.extend("A")( + S.Struct(fields).pipe(S.filter(({ a, b }) => a === b ? undefined : "a should be equal to b")) + ) {} + Util.expectFields(A.fields, { ...baseFields, ...fields }) + await Util.assertions.decoding.succeed(A, new A({ base: "base", a: 1, b: 1 })) + await Util.assertions.decoding.fail( + A, + { base: "base", a: 1, b: 2 }, + `(A (Encoded side) <-> A) +└─ Encoded side transformation failure + └─ A (Encoded side) + └─ Predicate refinement failure + └─ a should be equal to b` + ) + Util.assertions.parseError( + () => new A({ base: "base", a: 1, b: 2 }), + `A (Constructor) +└─ Predicate refinement failure + └─ a should be equal to b` + ) + }) + + it("decoding", async () => { + await Util.assertions.decoding.fail( + PersonWithAge, + { id: 1, name: "John" }, + `(PersonWithAge (Encoded side) <-> PersonWithAge) +└─ Encoded side transformation failure + └─ PersonWithAge (Encoded side) + └─ ["age"] + └─ is missing` + ) + }) + + it("should expose a make constructor", () => { + class A extends S.Class("A")({ + n: S.NumberFromString + }) { + a() { + return this.n + "a" + } + } + class B extends A.extend("B")({ + c: S.String + }) { + b() { + return this.n + "b" + } + } + const b = B.make({ n: 1, c: "c" }) + assertInstanceOf(b, B) + strictEqual(b.a(), "1a") + strictEqual(b.b(), "1b") + }) + + it("users can override an instance member property", () => { + class OverrideBase1 extends S.Class("OverrideBase1")(S.Struct({ + a: S.String + })) { + readonly b: number = 1 + } + + class OverrideExtended1 extends OverrideBase1.extend( + "OverrideExtended1" + )({ + c: S.String + }) { + override readonly b = 2 + } + + strictEqual(new OverrideExtended1({ a: "a", c: "c" }).b, 2) + }) + + it("users can override an instance member function", () => { + class OverrideBase2 extends S.Class("OverrideBase2")(S.Struct({ + a: S.String + })) { + b(): number { + return 1 + } + } + + class OverrideExtended2 extends OverrideBase2.extend( + "OverrideExtended2" + )({ + c: S.String + }) { + override b(): 2 { + return 2 + } + } + + strictEqual(new OverrideExtended2({ a: "a", c: "c" }).b(), 2) + }) + + it("users can override a field with an instance member property", () => { + class OverrideBase3 extends S.Class("OverrideBase3")(S.Struct({ + a: S.String + })) {} + + class OverrideExtended3 extends OverrideBase3.extend( + "OverrideExtended3" + )({ + c: S.String + }) { + override readonly a = "default" + } + + strictEqual(new OverrideExtended3({ a: "a", c: "c" }).a, "default") + }) + + it("users can't override an instance member property with a field", () => { + class OverrideBase4 extends S.Class("OverrideBase4")(S.Struct({ + a: S.String + })) { + readonly b = 1 + } + + class OverrideExtended4 extends OverrideBase4.extend( + "OverrideExtended4" + )({ + b: S.Number + }) {} + + strictEqual(new OverrideExtended4({ a: "a", b: 2 }).b, 1) + }) + + describe("should support annotations when declaring the Class", () => { + it("single argument", async () => { + class A extends S.Class("A")({ + a: S.NonEmptyString + }) {} + class B extends A.extend("B")({ + b: S.NonEmptyString + }, { title: "mytitle" }) {} + + strictEqual(B.ast.to.annotations[AST.TitleAnnotationId], "mytitle") + + await Util.assertions.encoding.fail( + B, + { a: "a", b: "" }, + `(B (Encoded side) <-> B) +└─ Type side transformation failure + └─ mytitle + └─ ["b"] + └─ NonEmptyString + └─ Predicate refinement failure + └─ Expected a non empty string, actual ""` + ) + }) + + it("tuple argument", async () => { + class A extends S.Class("A")({ + a: S.NonEmptyString + }) {} + class B extends A.extend("B")({ + b: S.NonEmptyString + }, [ + { identifier: "TypeID", description: "TypeDescription" }, + { identifier: "TransformationID" }, + { identifier: "EncodedID" } + ]) {} + assertSome(AST.getIdentifierAnnotation(B.ast.to), "TypeID") + assertSome(AST.getIdentifierAnnotation(B.ast), "TransformationID") + assertSome(AST.getIdentifierAnnotation(B.ast.from), "EncodedID") + + await Util.assertions.decoding.fail( + B, + {}, + `TransformationID +└─ Encoded side transformation failure + └─ EncodedID + └─ ["a"] + └─ is missing` + ) + + await Util.assertions.encoding.fail( + B, + { a: "a", b: "" }, + `TransformationID +└─ Type side transformation failure + └─ TypeID + └─ ["b"] + └─ NonEmptyString + └─ Predicate refinement failure + └─ Expected a non empty string, actual ""` + ) + + const ctor = { make: B.make.bind(B) } + + Util.assertions.make.fail( + ctor, + null as any, + `TypeID +└─ ["a"] + └─ is missing` + ) + + deepStrictEqual(JSONSchema.make(S.typeSchema(B)), { + "$defs": { + "NonEmptyString": { + "title": "nonEmptyString", + "description": "a non empty string", + "minLength": 1, + "type": "string" + }, + "TypeID": { + "additionalProperties": false, + "description": "TypeDescription", + "properties": { + "a": { + "$ref": "#/$defs/NonEmptyString" + }, + "b": { + "$ref": "#/$defs/NonEmptyString" + } + }, + "required": ["a", "b"], + "type": "object" + } + }, + "$ref": "#/$defs/TypeID", + "$schema": "http://json-schema.org/draft-07/schema#" + }) + + deepStrictEqual(JSONSchema.make(B), { + "$defs": { + "NonEmptyString": { + "title": "nonEmptyString", + "description": "a non empty string", + "minLength": 1, + "type": "string" + }, + "TransformationID": { + "additionalProperties": false, + "description": "TypeDescription", + "properties": { + "a": { + "$ref": "#/$defs/NonEmptyString" + }, + "b": { + "$ref": "#/$defs/NonEmptyString" + } + }, + "required": ["a", "b"], + "type": "object" + } + }, + "$ref": "#/$defs/TransformationID", + "$schema": "http://json-schema.org/draft-07/schema#" + }) + }) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/Class/transformOrFail.test.ts b/repos/effect/packages/effect/test/Schema/Schema/Class/transformOrFail.test.ts new file mode 100644 index 0000000..1e2a0d0 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/Class/transformOrFail.test.ts @@ -0,0 +1,94 @@ +import { describe, it } from "@effect/vitest" +import { assertInstanceOf, assertTrue, deepStrictEqual, strictEqual } from "@effect/vitest/utils" +import * as Option from "effect/Option" +import * as ParseResult from "effect/ParseResult" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +class Person extends S.Class("Person")({ + id: S.Number, + name: S.String.pipe(S.nonEmptyString()) +}) { + get upperName() { + return this.name.toUpperCase() + } +} + +const Thing = S.optionalWith(S.Struct({ id: S.Number }), { exact: true, as: "Option" }) + +class PersonWithTransform extends Person.transformOrFail("PersonWithTransform")( + { + thing: Thing + }, + { + decode: (input, _, ast) => + input.id === 2 ? + ParseResult.fail(new ParseResult.Type(ast, input)) : + ParseResult.succeed({ + ...input, + thing: Option.some({ id: 123 }) + }), + encode: (input, _, ast) => + input.id === 2 ? + ParseResult.fail(new ParseResult.Type(ast, input)) : + ParseResult.succeed(input) + } +) { + a() { + return this.id + "a" + } +} + +describe("transformOrFail", () => { + it("transformOrFail", async () => { + const decode = S.decodeSync(PersonWithTransform) + const person = decode({ + id: 1, + name: "John" + }) + deepStrictEqual(PersonWithTransform.fields, { + ...Person.fields, + thing: Thing + }) + strictEqual(PersonWithTransform.identifier, "PersonWithTransform") + strictEqual(person.id, 1) + strictEqual(person.name, "John") + assertTrue(Option.isSome(person.thing) && person.thing.value.id === 123) + strictEqual(person.upperName, "JOHN") + strictEqual(typeof person.upperName, "string") + + await Util.assertions.decoding.fail( + PersonWithTransform, + { + id: 2, + name: "John" + }, + `(PersonWithTransform (Encoded side) <-> PersonWithTransform) +└─ Encoded side transformation failure + └─ PersonWithTransform (Encoded side) + └─ Transformation process failure + └─ Expected PersonWithTransform (Encoded side), actual {"id":2,"name":"John"}` + ) + await Util.assertions.encoding.fail( + PersonWithTransform, + new PersonWithTransform({ id: 2, name: "John", thing: Option.some({ id: 1 }) }), + `(PersonWithTransform (Encoded side) <-> PersonWithTransform) +└─ Encoded side transformation failure + └─ PersonWithTransform (Encoded side) + └─ Transformation process failure + └─ Expected PersonWithTransform (Encoded side), actual {"id":2,"name":"John","thing":{ + "_id": "Option", + "_tag": "Some", + "value": { + "id": 1 + } +}}` + ) + }) + + it("should expose a make constructor", () => { + const instance = PersonWithTransform.make({ id: 2, name: "John", thing: Option.some({ id: 1 }) }) + assertInstanceOf(instance, PersonWithTransform) + strictEqual(instance.a(), "2a") + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/Class/transformOrFailFrom.test.ts b/repos/effect/packages/effect/test/Schema/Schema/Class/transformOrFailFrom.test.ts new file mode 100644 index 0000000..104fcca --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/Class/transformOrFailFrom.test.ts @@ -0,0 +1,88 @@ +import { describe, it } from "@effect/vitest" +import { assertInstanceOf, assertTrue, deepStrictEqual, strictEqual } from "@effect/vitest/utils" +import * as Option from "effect/Option" +import * as ParseResult from "effect/ParseResult" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +class Person extends S.Class("Person")({ + id: S.Number, + name: S.String.pipe(S.nonEmptyString()) +}) { + get upperName() { + return this.name.toUpperCase() + } +} + +const Thing = S.optionalWith(S.Struct({ id: S.Number }), { exact: true, as: "Option" }) + +class PersonWithTransformFrom extends Person.transformOrFailFrom("PersonWithTransformFrom")( + { + thing: Thing + }, + { + decode: (input, _, ast) => + input.id === 2 ? + ParseResult.fail(new ParseResult.Type(ast, input)) : + ParseResult.succeed({ + ...input, + thing: { id: 123 } + }), + encode: (input, _, ast) => + input.id === 2 ? + ParseResult.fail(new ParseResult.Type(ast, input)) : + ParseResult.succeed(input) + } +) { + a() { + return this.id + "a" + } +} + +describe("", () => { + it("transformOrFailFrom", async () => { + const decode = S.decodeSync(PersonWithTransformFrom) + const person = decode({ + id: 1, + name: "John" + }) + deepStrictEqual(PersonWithTransformFrom.fields, { + ...Person.fields, + thing: Thing + }) + strictEqual(PersonWithTransformFrom.identifier, "PersonWithTransformFrom") + strictEqual(person.id, 1) + strictEqual(person.name, "John") + assertTrue(Option.isSome(person.thing) && person.thing.value.id === 123) + strictEqual(person.upperName, "JOHN") + strictEqual(typeof person.upperName, "string") + + await Util.assertions.decoding.fail( + PersonWithTransformFrom, + { + id: 2, + name: "John" + }, + `(PersonWithTransformFrom (Encoded side) <-> PersonWithTransformFrom) +└─ Encoded side transformation failure + └─ PersonWithTransformFrom (Encoded side) + └─ Transformation process failure + └─ Expected PersonWithTransformFrom (Encoded side), actual {"id":2,"name":"John"}` + ) + await Util.assertions.encoding.fail( + PersonWithTransformFrom, + new PersonWithTransformFrom({ id: 2, name: "John", thing: Option.some({ id: 1 }) }), + `(PersonWithTransformFrom (Encoded side) <-> PersonWithTransformFrom) +└─ Encoded side transformation failure + └─ PersonWithTransformFrom (Encoded side) + └─ Transformation process failure + └─ Expected PersonWithTransformFrom (Encoded side), actual {"id":2,"name":"John","thing":{"id":1}}` + ) + }) + + it("should expose a make constructor", () => { + const instance = PersonWithTransformFrom.make({ id: 2, name: "John", thing: Option.some({ id: 1 }) }) + assertInstanceOf(instance, PersonWithTransformFrom) + strictEqual(instance.a(), "2a") + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/Config/Config.test.ts b/repos/effect/packages/effect/test/Schema/Schema/Config/Config.test.ts new file mode 100644 index 0000000..f063c30 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/Config/Config.test.ts @@ -0,0 +1,61 @@ +import { describe, it } from "@effect/vitest" +import { assertFailure, assertSuccess } from "@effect/vitest/utils" +import type { Config } from "effect" +import { Cause, ConfigError, ConfigProvider, Effect, Schema } from "effect" + +/** + * Asserts that loading a configuration with invalid data fails with the expected error. + */ +const assertConfigFailure = ( + config: Config.Config, + map: ReadonlyArray, + error: ConfigError.ConfigError +) => { + const configProvider = ConfigProvider.fromMap(new Map(map)) + const result = Effect.runSync(Effect.exit(configProvider.load(config))) + assertFailure(result, Cause.fail(error)) +} + +/** + * Asserts that loading a configuration with valid data succeeds and returns the expected value. + */ +const assertConfigSuccess = ( + config: Config.Config, + map: ReadonlyArray, + a: A +) => { + const configProvider = ConfigProvider.fromMap(new Map(map)) + const result = Effect.runSync(Effect.exit(configProvider.load(config))) + assertSuccess(result, a) +} + +describe("Config", () => { + it("should validate the configuration schema correctly", () => { + const config = Schema.Config("A", Schema.NonEmptyString) + assertConfigSuccess(config, [["A", "a"]], "a") + assertConfigFailure(config, [], ConfigError.MissingData(["A"], `Expected A to exist in the provided map`)) + assertConfigFailure( + config, + [["A", ""]], + ConfigError.InvalidData( + ["A"], + `NonEmptyString +└─ Predicate refinement failure + └─ Expected a non empty string, actual ""` + ) + ) + }) + + it("should work with a template literal", () => { + const config = Schema.Config("A", Schema.TemplateLiteral("a", Schema.Number)) + assertConfigSuccess(config, [["A", "a1"]], "a1") + assertConfigFailure( + config, + [["A", "ab"]], + ConfigError.InvalidData( + ["A"], + `Expected \`a$\{number}\`, actual "ab"` + ) + ) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/Data/Data.test.ts b/repos/effect/packages/effect/test/Schema/Schema/Data/Data.test.ts new file mode 100644 index 0000000..bafa60f --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/Data/Data.test.ts @@ -0,0 +1,34 @@ +import { describe, it } from "@effect/vitest" +import * as Data from "effect/Data" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("Data", () => { + it("test roundtrip consistency", () => { + Util.assertions.testRoundtripConsistency(S.Data(S.Struct({ a: S.String, b: S.Number }))) + Util.assertions.testRoundtripConsistency(S.Data(S.Array(S.Number))) + }) + + it("decoding", async () => { + const schema = S.Data(S.Struct({ a: S.String, b: S.Number })) + await Util.assertions.decoding.succeed( + schema, + { a: "ok", b: 0 }, + Data.struct({ a: "ok", b: 0 }) + ) + await Util.assertions.decoding.fail( + schema, + { a: "ok", b: "0" }, + `({ readonly a: string; readonly b: number } <-> Data<{ readonly a: string; readonly b: number }>) +└─ Encoded side transformation failure + └─ { readonly a: string; readonly b: number } + └─ ["b"] + └─ Expected number, actual "0"` + ) + }) + + it("encoding", async () => { + const schema = S.Data(S.Struct({ a: S.String, b: S.Number })) + await Util.assertions.encoding.succeed(schema, Data.struct({ a: "ok", b: 0 }), { a: "ok", b: 0 }) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/Data/DataFromSelf.test.ts b/repos/effect/packages/effect/test/Schema/Schema/Data/DataFromSelf.test.ts new file mode 100644 index 0000000..389c2a0 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/Data/DataFromSelf.test.ts @@ -0,0 +1,57 @@ +import { describe, it } from "@effect/vitest" +import { assertFalse, assertTrue } from "@effect/vitest/utils" +import * as Data from "effect/Data" +import * as P from "effect/ParseResult" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("DataFromSelf", () => { + it("test roundtrip consistency", () => { + Util.assertions.testRoundtripConsistency(S.DataFromSelf(S.Struct({ a: S.String, b: S.Number }))) + Util.assertions.testRoundtripConsistency(S.DataFromSelf(S.Array(S.Number))) + }) + + it("decoding", async () => { + const schema = S.DataFromSelf(S.Struct({ a: S.String, b: S.Number })) + await Util.assertions.decoding.succeed( + schema, + Data.struct({ a: "ok", b: 0 }), + Data.struct({ a: "ok", b: 0 }) + ) + await Util.assertions.decoding.fail( + schema, + { a: "ok", b: 0 }, + `Expected Data<{ readonly a: string; readonly b: number }>, actual {"a":"ok","b":0}` + ) + await Util.assertions.decoding.fail( + schema, + Data.struct({ a: "ok", b: "0" }), + `Data<{ readonly a: string; readonly b: number }> +└─ { readonly a: string; readonly b: number } + └─ ["b"] + └─ Expected number, actual "0"` + ) + }) + + it("encoding", async () => { + const schema = S.DataFromSelf(S.Struct({ a: S.String, b: S.Number })) + await Util.assertions.encoding.succeed( + schema, + Data.struct({ a: "ok", b: 0 }), + Data.struct({ a: "ok", b: 0 }) + ) + }) + + it("is", () => { + const schema = S.DataFromSelf(S.Struct({ a: S.String, b: S.Number })) + const is = P.is(schema) + assertTrue(is(Data.struct({ a: "ok", b: 0 }))) + assertFalse(is({ a: "ok", b: 0 })) + assertFalse(is(Data.struct({ a: "ok", b: "no" }))) + }) + + it("pretty", () => { + const schema = S.DataFromSelf(S.Struct({ a: S.String, b: S.Number })) + Util.assertions.pretty(schema, Data.struct({ a: "ok", b: 0 }), `Data({ "a": "ok", "b": 0 })`) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/Date/Date.test.ts b/repos/effect/packages/effect/test/Schema/Schema/Date/Date.test.ts new file mode 100644 index 0000000..3befdb6 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/Date/Date.test.ts @@ -0,0 +1,37 @@ +import { describe, it } from "@effect/vitest" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("Date", () => { + const schema = S.Date + + it("test roundtrip consistency", () => { + Util.assertions.testRoundtripConsistency(schema) + }) + + it("decoding", async () => { + await Util.assertions.decoding.succeed( + schema, + "1970-01-01T00:00:00.000Z", + new Date(0) + ) + await Util.assertions.decoding.fail( + schema, + "a", + `Date +└─ Predicate refinement failure + └─ Expected a valid Date, actual Invalid Date` + ) + }) + + it("encoding", async () => { + await Util.assertions.encoding.succeed(schema, new Date(0), "1970-01-01T00:00:00.000Z") + await Util.assertions.encoding.fail( + schema, + new Date("fail"), + `Date +└─ Predicate refinement failure + └─ Expected a valid Date, actual Invalid Date` + ) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/Date/DateFromNumber.test.ts b/repos/effect/packages/effect/test/Schema/Schema/Date/DateFromNumber.test.ts new file mode 100644 index 0000000..ca0a367 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/Date/DateFromNumber.test.ts @@ -0,0 +1,29 @@ +import { describe, it } from "@effect/vitest" +import { assertTrue, strictEqual } from "@effect/vitest/utils" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("DateFromNumber", () => { + it("decoding", async () => { + await Util.assertions.decoding.succeed(S.DateFromNumber, 0, new Date(0)) + assertTrue(S.decodeSync(S.DateFromNumber)(NaN) instanceof Date) + assertTrue(S.decodeSync(S.DateFromNumber)(Infinity) instanceof Date) + assertTrue(S.decodeSync(S.DateFromNumber)(-Infinity) instanceof Date) + + await Util.assertions.decoding.fail( + S.DateFromNumber, + null, + `DateFromNumber +└─ Encoded side transformation failure + └─ Expected number, actual null` + ) + }) + + it("encoding", async () => { + await Util.assertions.encoding.succeed(S.DateFromNumber, new Date(0), 0) + strictEqual(S.encodeSync(S.DateFromNumber)(new Date("invalid")), NaN) + strictEqual(S.encodeSync(S.DateFromNumber)(new Date(NaN)), NaN) + strictEqual(S.encodeSync(S.DateFromNumber)(new Date(Infinity)), NaN) + strictEqual(S.encodeSync(S.DateFromNumber)(new Date(-Infinity)), NaN) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/Date/DateFromSelf.test.ts b/repos/effect/packages/effect/test/Schema/Schema/Date/DateFromSelf.test.ts new file mode 100644 index 0000000..c1343b9 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/Date/DateFromSelf.test.ts @@ -0,0 +1,32 @@ +import { describe, it } from "@effect/vitest" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("DateFromSelf", () => { + it("test roundtrip consistency", () => { + Util.assertions.testRoundtripConsistency(S.DateFromSelf) + }) + + it("decoding", async () => { + await Util.assertions.decoding.succeed(S.DateFromSelf, new Date(0), new Date(0)) + await Util.assertions.decoding.succeed(S.DateFromSelf, new Date("invalid")) + + await Util.assertions.decoding.fail( + S.DateFromSelf, + null, + `Expected DateFromSelf, actual null` + ) + }) + + it("encoding", async () => { + const now = new Date() + await Util.assertions.encoding.succeed(S.DateFromSelf, now, now) + const invalid = new Date("invalid") + await Util.assertions.encoding.succeed(S.DateFromSelf, invalid, invalid) + }) + + it("pretty", () => { + const schema = S.DateFromSelf + Util.assertions.pretty(schema, new Date(0), `new Date("1970-01-01T00:00:00.000Z")`) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/Date/betweenDate.test.ts b/repos/effect/packages/effect/test/Schema/Schema/Date/betweenDate.test.ts new file mode 100644 index 0000000..01cb84b --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/Date/betweenDate.test.ts @@ -0,0 +1,37 @@ +import { describe, it } from "@effect/vitest" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("betweenDate", () => { + const schema = S.DateFromSelf.pipe(S.betweenDate(new Date(-1), new Date(1))) + + it("decoding", async () => { + await Util.assertions.decoding.succeed( + schema, + new Date(-1) + ) + await Util.assertions.decoding.succeed( + schema, + new Date(0) + ) + await Util.assertions.decoding.succeed( + schema, + new Date(1) + ) + + await Util.assertions.decoding.fail( + schema, + new Date(-2), + `betweenDate(1969-12-31T23:59:59.999Z, 1970-01-01T00:00:00.001Z) +└─ Predicate refinement failure + └─ Expected a date between 1969-12-31T23:59:59.999Z and 1970-01-01T00:00:00.001Z, actual 1969-12-31T23:59:59.998Z` + ) + await Util.assertions.decoding.fail( + schema, + new Date(2), + `betweenDate(1969-12-31T23:59:59.999Z, 1970-01-01T00:00:00.001Z) +└─ Predicate refinement failure + └─ Expected a date between 1969-12-31T23:59:59.999Z and 1970-01-01T00:00:00.001Z, actual 1970-01-01T00:00:00.002Z` + ) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/Date/greaterThanDate.test.ts b/repos/effect/packages/effect/test/Schema/Schema/Date/greaterThanDate.test.ts new file mode 100644 index 0000000..40a75bc --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/Date/greaterThanDate.test.ts @@ -0,0 +1,29 @@ +import { describe, it } from "@effect/vitest" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("greaterThanDate", () => { + const schema = S.DateFromSelf.pipe(S.greaterThanDate(new Date(0))) + + it("decoding", async () => { + await Util.assertions.decoding.succeed( + schema, + new Date(1) + ) + + await Util.assertions.decoding.fail( + schema, + new Date(0), + `greaterThanDate(1970-01-01T00:00:00.000Z) +└─ Predicate refinement failure + └─ Expected a date after 1970-01-01T00:00:00.000Z, actual 1970-01-01T00:00:00.000Z` + ) + await Util.assertions.decoding.fail( + schema, + new Date(-1), + `greaterThanDate(1970-01-01T00:00:00.000Z) +└─ Predicate refinement failure + └─ Expected a date after 1970-01-01T00:00:00.000Z, actual 1969-12-31T23:59:59.999Z` + ) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/Date/greaterThanOrEqualToDate.test.ts b/repos/effect/packages/effect/test/Schema/Schema/Date/greaterThanOrEqualToDate.test.ts new file mode 100644 index 0000000..ac82fc3 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/Date/greaterThanOrEqualToDate.test.ts @@ -0,0 +1,26 @@ +import { describe, it } from "@effect/vitest" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("greaterThanOrEqualToDate", () => { + const schema = S.DateFromSelf.pipe(S.greaterThanOrEqualToDate(new Date(0))) + + it("decoding", async () => { + await Util.assertions.decoding.succeed( + schema, + new Date(1) + ) + await Util.assertions.decoding.succeed( + schema, + new Date(0) + ) + + await Util.assertions.decoding.fail( + schema, + new Date(-1), + `greaterThanOrEqualToDate(1970-01-01T00:00:00.000Z) +└─ Predicate refinement failure + └─ Expected a date after or equal to 1970-01-01T00:00:00.000Z, actual 1969-12-31T23:59:59.999Z` + ) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/Date/lessThanDate.test.ts b/repos/effect/packages/effect/test/Schema/Schema/Date/lessThanDate.test.ts new file mode 100644 index 0000000..4d8aa2d --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/Date/lessThanDate.test.ts @@ -0,0 +1,29 @@ +import { describe, it } from "@effect/vitest" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("lessThanDate", () => { + const schema = S.DateFromSelf.pipe(S.lessThanDate(new Date(0))) + + it("decoding", async () => { + await Util.assertions.decoding.succeed( + schema, + new Date(-1) + ) + + await Util.assertions.decoding.fail( + schema, + new Date(0), + `lessThanDate(1970-01-01T00:00:00.000Z) +└─ Predicate refinement failure + └─ Expected a date before 1970-01-01T00:00:00.000Z, actual 1970-01-01T00:00:00.000Z` + ) + await Util.assertions.decoding.fail( + schema, + new Date(1), + `lessThanDate(1970-01-01T00:00:00.000Z) +└─ Predicate refinement failure + └─ Expected a date before 1970-01-01T00:00:00.000Z, actual 1970-01-01T00:00:00.001Z` + ) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/Date/lessThanOrEqualToDate.test.ts b/repos/effect/packages/effect/test/Schema/Schema/Date/lessThanOrEqualToDate.test.ts new file mode 100644 index 0000000..c206a2b --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/Date/lessThanOrEqualToDate.test.ts @@ -0,0 +1,26 @@ +import { describe, it } from "@effect/vitest" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("lessThanOrEqualToDate", () => { + const schema = S.DateFromSelf.pipe(S.lessThanOrEqualToDate(new Date(0))) + + it("decoding", async () => { + await Util.assertions.decoding.succeed( + schema, + new Date(-1) + ) + await Util.assertions.decoding.succeed( + schema, + new Date(0) + ) + + await Util.assertions.decoding.fail( + schema, + new Date(1), + `lessThanOrEqualToDate(1970-01-01T00:00:00.000Z) +└─ Predicate refinement failure + └─ Expected a date before or equal to 1970-01-01T00:00:00.000Z, actual 1970-01-01T00:00:00.001Z` + ) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/DateTime/DateTimeUtc.test.ts b/repos/effect/packages/effect/test/Schema/Schema/DateTime/DateTimeUtc.test.ts new file mode 100644 index 0000000..947fdd4 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/DateTime/DateTimeUtc.test.ts @@ -0,0 +1,31 @@ +import { describe, it } from "@effect/vitest" +import { DateTime } from "effect" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("DateTimeUtc", () => { + const schema = S.DateTimeUtc + + it("test roundtrip consistency", () => { + Util.assertions.testRoundtripConsistency(schema) + }) + + it("decoding", async () => { + await Util.assertions.decoding.succeed( + schema, + "1970-01-01T00:00:00.000Z", + DateTime.unsafeMake(0) + ) + await Util.assertions.decoding.fail( + schema, + "a", + `DateTimeUtc +└─ Transformation process failure + └─ Unable to decode "a" into a DateTime.Utc` + ) + }) + + it("encoding", async () => { + await Util.assertions.encoding.succeed(schema, DateTime.unsafeMake(0), "1970-01-01T00:00:00.000Z") + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/DateTime/DateTimeUtcFromDate.test.ts b/repos/effect/packages/effect/test/Schema/Schema/DateTime/DateTimeUtcFromDate.test.ts new file mode 100644 index 0000000..bc7d3c6 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/DateTime/DateTimeUtcFromDate.test.ts @@ -0,0 +1,47 @@ +import { describe, it } from "@effect/vitest" +import { deepStrictEqual } from "@effect/vitest/utils" +import * as DateTime from "effect/DateTime" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("DateTimeUtcFromDate", () => { + const schema = S.DateTimeUtcFromDate + + it("test roundtrip consistency", () => { + Util.assertions.testRoundtripConsistency(schema) + }) + + it("decoding", async () => { + await Util.assertions.decoding.succeed(schema, new Date(0), DateTime.unsafeMake(0)) + await Util.assertions.decoding.succeed( + schema, + new Date("2024-12-06T00:00:00Z"), + DateTime.unsafeMake({ day: 6, month: 12, year: 2024, hour: 0, minute: 0, second: 0, millisecond: 0 }) + ) + + await Util.assertions.decoding.fail( + schema, + null, + `DateTimeUtcFromDate +└─ Encoded side transformation failure + └─ Expected a Date to be decoded into a DateTime.Utc, actual null` + ) + await Util.assertions.decoding.fail( + schema, + new Date(NaN), + `DateTimeUtcFromDate +└─ Transformation process failure + └─ Unable to decode Invalid Date into a DateTime.Utc` + ) + }) + + it("encoding", async () => { + await Util.assertions.encoding.succeed(schema, DateTime.unsafeMake(0), new Date(0)) + deepStrictEqual( + S.encodeSync(schema)( + DateTime.unsafeMake({ day: 6, month: 12, year: 2024, hour: 0, minute: 0, second: 0, millisecond: 0 }) + ), + new Date("2024-12-06T00:00:00Z") + ) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/DateTime/DateTimeUtcFromNumber.test.ts b/repos/effect/packages/effect/test/Schema/Schema/DateTime/DateTimeUtcFromNumber.test.ts new file mode 100644 index 0000000..d99175f --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/DateTime/DateTimeUtcFromNumber.test.ts @@ -0,0 +1,11 @@ +import { describe, it } from "@effect/vitest" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("DateTimeUtcFromNumber", () => { + const schema = S.DateTimeUtcFromNumber + + it("test roundtrip consistency", () => { + Util.assertions.testRoundtripConsistency(schema) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/DateTime/DateTimeUtcFromSelf.test.ts b/repos/effect/packages/effect/test/Schema/Schema/DateTime/DateTimeUtcFromSelf.test.ts new file mode 100644 index 0000000..7169e06 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/DateTime/DateTimeUtcFromSelf.test.ts @@ -0,0 +1,11 @@ +import { describe, it } from "@effect/vitest" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("DateTimeUtcFromSelf", () => { + const schema = S.DateTimeUtcFromSelf + + it("test roundtrip consistency", () => { + Util.assertions.testRoundtripConsistency(schema) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/DateTime/DateTimeZoned.test.ts b/repos/effect/packages/effect/test/Schema/Schema/DateTime/DateTimeZoned.test.ts new file mode 100644 index 0000000..cb5af8e --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/DateTime/DateTimeZoned.test.ts @@ -0,0 +1,35 @@ +import { describe, it } from "@effect/vitest" +import { DateTime } from "effect" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("DateTimeZoned", () => { + const schema = S.DateTimeZoned + const dt = DateTime.unsafeMakeZoned(0, { timeZone: "Europe/London" }) + + it("test roundtrip consistency", () => { + Util.assertions.testRoundtripConsistency(schema) + }) + + it("decoding", async () => { + await Util.assertions.decoding.succeed(schema, "1970-01-01T01:00:00.000+01:00[Europe/London]", dt) + await Util.assertions.decoding.fail( + schema, + "1970-01-01T00:00:00.000Z", + `DateTimeZoned +└─ Transformation process failure + └─ Unable to decode "1970-01-01T00:00:00.000Z" into a DateTime.Zoned` + ) + await Util.assertions.decoding.fail( + schema, + "a", + `DateTimeZoned +└─ Transformation process failure + └─ Unable to decode "a" into a DateTime.Zoned` + ) + }) + + it("encoding", async () => { + await Util.assertions.encoding.succeed(schema, dt, "1970-01-01T01:00:00.000+01:00[Europe/London]") + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/DateTime/DateTimeZonedFromSelf.test.ts b/repos/effect/packages/effect/test/Schema/Schema/DateTime/DateTimeZonedFromSelf.test.ts new file mode 100644 index 0000000..00bc8be --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/DateTime/DateTimeZonedFromSelf.test.ts @@ -0,0 +1,11 @@ +import { describe, it } from "@effect/vitest" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("DateTimeZonedFromSelf", () => { + const schema = S.DateTimeZonedFromSelf + + it("test roundtrip consistency", () => { + Util.assertions.testRoundtripConsistency(schema) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/DateTime/TimeZone.test.ts b/repos/effect/packages/effect/test/Schema/Schema/DateTime/TimeZone.test.ts new file mode 100644 index 0000000..9898d3a --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/DateTime/TimeZone.test.ts @@ -0,0 +1,11 @@ +import { describe, it } from "@effect/vitest" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("TimeZone", () => { + const schema = S.TimeZone + + it("test roundtrip consistency", () => { + Util.assertions.testRoundtripConsistency(schema) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/DateTime/TimeZoneFromSelf.test.ts b/repos/effect/packages/effect/test/Schema/Schema/DateTime/TimeZoneFromSelf.test.ts new file mode 100644 index 0000000..d2dfe80 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/DateTime/TimeZoneFromSelf.test.ts @@ -0,0 +1,11 @@ +import { describe, it } from "@effect/vitest" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("TimeZoneFromSelf", () => { + const schema = S.TimeZoneFromSelf + + it("test roundtrip consistency", () => { + Util.assertions.testRoundtripConsistency(schema) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/DateTime/TimeZoneNamed.test.ts b/repos/effect/packages/effect/test/Schema/Schema/DateTime/TimeZoneNamed.test.ts new file mode 100644 index 0000000..aab54a1 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/DateTime/TimeZoneNamed.test.ts @@ -0,0 +1,11 @@ +import { describe, it } from "@effect/vitest" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("TimeZoneNamed", () => { + const schema = S.TimeZoneNamed + + it("test roundtrip consistency", () => { + Util.assertions.testRoundtripConsistency(schema) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/DateTime/TimeZoneNamedFromSelf.test.ts b/repos/effect/packages/effect/test/Schema/Schema/DateTime/TimeZoneNamedFromSelf.test.ts new file mode 100644 index 0000000..64e610b --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/DateTime/TimeZoneNamedFromSelf.test.ts @@ -0,0 +1,11 @@ +import { describe, it } from "@effect/vitest" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("TimeZoneNamedFromSelf", () => { + const schema = S.TimeZoneNamedFromSelf + + it("test roundtrip consistency", () => { + Util.assertions.testRoundtripConsistency(schema) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/DateTime/TimeZoneOffset.test.ts b/repos/effect/packages/effect/test/Schema/Schema/DateTime/TimeZoneOffset.test.ts new file mode 100644 index 0000000..a5e1d01 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/DateTime/TimeZoneOffset.test.ts @@ -0,0 +1,11 @@ +import { describe, it } from "@effect/vitest" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("TimeZoneOffset", () => { + const schema = S.TimeZoneOffset + + it("test roundtrip consistency", () => { + Util.assertions.testRoundtripConsistency(schema) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/DateTime/TimeZoneOffsetFromSelf.test.ts b/repos/effect/packages/effect/test/Schema/Schema/DateTime/TimeZoneOffsetFromSelf.test.ts new file mode 100644 index 0000000..cee19bf --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/DateTime/TimeZoneOffsetFromSelf.test.ts @@ -0,0 +1,11 @@ +import { describe, it } from "@effect/vitest" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("TimeZoneOffsetFromSelf", () => { + const schema = S.TimeZoneOffsetFromSelf + + it("test roundtrip consistency", () => { + Util.assertions.testRoundtripConsistency(schema) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/DecodingFallbackAnnotation.test.ts b/repos/effect/packages/effect/test/Schema/Schema/DecodingFallbackAnnotation.test.ts new file mode 100644 index 0000000..23c393d --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/DecodingFallbackAnnotation.test.ts @@ -0,0 +1,68 @@ +import { describe, it } from "@effect/vitest" +import { deepStrictEqual } from "@effect/vitest/utils" +import { Effect, Either } from "effect" +import * as S from "effect/Schema" +import * as Util from "../TestUtils.js" + +describe("DecodingFallbackAnnotation", () => { + it("using Either", async () => { + const schema = S.String.annotations({ decodingFallback: () => Either.right("") }) + await Util.assertions.decoding.succeed( + schema, + null, + "" + ) + }) + + it("using a sync Effect", async () => { + const log: Array = [] + const schema = S.String.annotations({ + decodingFallback: (issue) => + Effect.gen(function*() { + log.push(issue.actual) + return yield* Effect.succeed("") + }) + }) + await Util.assertions.decoding.succeed( + schema, + null, + "" + ) + deepStrictEqual(log, [null]) + }) + + it("using an async Effect", async () => { + const log: Array = [] + const schema = S.String.annotations({ + decodingFallback: (issue) => + Effect.gen(function*() { + log.push(issue.actual) + yield* Effect.sleep(10) + return yield* Effect.succeed("") + }) + }) + await Util.assertions.decoding.succeed( + schema, + null, + "" + ) + deepStrictEqual(log, [null]) + Util.assertions.parseError( + () => S.decodeUnknownSync(schema)(null), + `string +└─ cannot be be resolved synchronously, this is caused by using runSync on an effect that performs async work` + ) + }) + + it("nested Struct", async () => { + const schema = S.Struct({ + name: S.String.annotations({ decodingFallback: () => Either.right("John") }), + age: S.Number.annotations({ decodingFallback: () => Either.right(18) }) + }) + await Util.assertions.decoding.succeed( + schema, + {}, + { name: "John", age: 18 } + ) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/Defect/Defect.test.ts b/repos/effect/packages/effect/test/Schema/Schema/Defect/Defect.test.ts new file mode 100644 index 0000000..a9db5e9 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/Defect/Defect.test.ts @@ -0,0 +1,94 @@ +import { describe, it } from "@effect/vitest" +import { assertInstanceOf, deepStrictEqual, strictEqual } from "@effect/vitest/utils" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("Defect", () => { + describe("decoding", () => { + it("a string", async () => { + await Util.assertions.decoding.succeed( + S.Defect, + "error", + "error" + ) + }) + + it("an object with a message", () => { + const err = S.decodeUnknownSync(S.Defect)({ message: "message" }) + deepStrictEqual(err, new Error("message", { cause: { message: "message" } })) + }) + + it("a null object with a message", async () => { + const defect = Object.create(null) + defect.message = "message" + + const err = S.decodeUnknownSync(S.Defect)(defect) + deepStrictEqual(err, new Error("message", { cause: defect })) + }) + + it("an object with a message and a name", () => { + const err = S.decodeUnknownSync(S.Defect)({ message: "message", name: "name" }) + assertInstanceOf(err, Error) + strictEqual(err.message, "message") + strictEqual(err.name, "name") + }) + + it("an object with a message and a stack", () => { + const err = S.decodeUnknownSync(S.Defect)({ message: "message", stack: "stack" }) + assertInstanceOf(err, Error) + strictEqual(err.message, "message") + strictEqual(err.stack, "stack") + }) + + it("a null object without a message", async () => { + const defect = Object.create(null) + defect.a = 1 + + await Util.assertions.decoding.succeed( + S.Defect, + defect, + "{\"a\":1}" + ) + }) + }) + + describe("encoding", () => { + it("a string", async () => { + await Util.assertions.encoding.succeed( + S.Defect, + "error", + "error" + ) + }) + + it("an object", async () => { + await Util.assertions.encoding.succeed( + S.Defect, + { a: 1 }, + "{\"a\":1}" + ) + }) + + it("a null object", async () => { + const defect = Object.create(null) + defect.a = 1 + + await Util.assertions.encoding.succeed( + S.Defect, + defect, + "{\"a\":1}" + ) + }) + + it("an Error", async () => { + await Util.assertions.encoding.succeed( + S.Defect, + new Error("message"), + { + "message": "message", + "name": "Error" + } + ) + }) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/Duration/Duration.test.ts b/repos/effect/packages/effect/test/Schema/Schema/Duration/Duration.test.ts new file mode 100644 index 0000000..1486331 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/Duration/Duration.test.ts @@ -0,0 +1,168 @@ +import { describe, it } from "@effect/vitest" +import { Duration } from "effect" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("Duration", () => { + const schema = S.Duration + + it("test roundtrip consistency", () => { + Util.assertions.testRoundtripConsistency(schema) + }) + + it("decoding", async () => { + await Util.assertions.decoding.succeed(schema, { _tag: "Infinity" }, Duration.infinity) + await Util.assertions.decoding.succeed(schema, { _tag: "Millis", millis: 12345 }, Duration.millis(12345)) + await Util.assertions.decoding.succeed(schema, { _tag: "Nanos", nanos: "54321" }, Duration.nanos(54321n)) + + await Util.assertions.decoding.fail( + schema, + null, + `Duration +└─ Encoded side transformation failure + └─ DurationValue | HRTime + ├─ Expected DurationValue, actual null + └─ HRTime + ├─ Expected InfiniteHRTime, actual null + └─ Expected FiniteHRTime, actual null` + ) + + await Util.assertions.decoding.fail( + schema, + {}, + `Duration +└─ Encoded side transformation failure + └─ DurationValue | HRTime + ├─ DurationValue + │ └─ { readonly _tag: "Millis" | "Nanos" | "Infinity" } + │ └─ ["_tag"] + │ └─ is missing + └─ HRTime + ├─ InfiniteHRTime + │ └─ ["0"] + │ └─ is missing + └─ Expected FiniteHRTime, actual {}` + ) + + await Util.assertions.decoding.fail( + schema, + { _tag: "Millis", millis: -1 }, + `Duration +└─ Encoded side transformation failure + └─ DurationValue | HRTime + ├─ DurationValue + │ └─ { readonly _tag: "Millis"; readonly millis: NonNegativeInt } + │ └─ ["millis"] + │ └─ NonNegativeInt + │ └─ From side refinement failure + │ └─ NonNegative + │ └─ Predicate refinement failure + │ └─ Expected a non-negative number, actual -1 + └─ HRTime + ├─ InfiniteHRTime + │ └─ ["0"] + │ └─ is missing + └─ Expected FiniteHRTime, actual {"_tag":"Millis","millis":-1}` + ) + + await Util.assertions.decoding.fail( + schema, + { _tag: "Nanos", nanos: null }, + `Duration +└─ Encoded side transformation failure + └─ DurationValue | HRTime + ├─ DurationValue + │ └─ { readonly _tag: "Nanos"; readonly nanos: BigInt } + │ └─ ["nanos"] + │ └─ BigInt + │ └─ Encoded side transformation failure + │ └─ Expected string, actual null + └─ HRTime + ├─ InfiniteHRTime + │ └─ ["0"] + │ └─ is missing + └─ Expected FiniteHRTime, actual {"_tag":"Nanos","nanos":null}` + ) + }) + + it("HRTime backward compatible encoding", async () => { + await Util.assertions.decoding.succeed(schema, [-1, 0], Duration.infinity) + await Util.assertions.decoding.succeed(schema, [555, 123456789], Duration.nanos(555123456789n)) + await Util.assertions.decoding.fail( + schema, + [-500, 0], + `Duration +└─ Encoded side transformation failure + └─ DurationValue | HRTime + ├─ DurationValue + │ └─ { readonly _tag: "Millis" | "Nanos" | "Infinity" } + │ └─ ["_tag"] + │ └─ is missing + └─ HRTime + ├─ InfiniteHRTime + │ └─ ["0"] + │ └─ Expected -1, actual -500 + └─ FiniteHRTime + └─ [0] + └─ NonNegativeInt + └─ From side refinement failure + └─ NonNegative + └─ Predicate refinement failure + └─ Expected a non-negative number, actual -500` + ) + await Util.assertions.decoding.fail( + schema, + [0, -123], + `Duration +└─ Encoded side transformation failure + └─ DurationValue | HRTime + ├─ DurationValue + │ └─ { readonly _tag: "Millis" | "Nanos" | "Infinity" } + │ └─ ["_tag"] + │ └─ is missing + └─ HRTime + ├─ InfiniteHRTime + │ └─ ["0"] + │ └─ Expected -1, actual 0 + └─ FiniteHRTime + └─ [1] + └─ NonNegativeInt + └─ From side refinement failure + └─ NonNegative + └─ Predicate refinement failure + └─ Expected a non-negative number, actual -123` + ) + await Util.assertions.decoding.fail( + schema, + 123, + `Duration +└─ Encoded side transformation failure + └─ DurationValue | HRTime + ├─ Expected DurationValue, actual 123 + └─ HRTime + ├─ Expected InfiniteHRTime, actual 123 + └─ Expected FiniteHRTime, actual 123` + ) + await Util.assertions.decoding.fail( + schema, + 123n, + `Duration +└─ Encoded side transformation failure + └─ DurationValue | HRTime + ├─ Expected DurationValue, actual 123n + └─ HRTime + ├─ Expected InfiniteHRTime, actual 123n + └─ Expected FiniteHRTime, actual 123n` + ) + }) + + it("encoding", async () => { + await Util.assertions.encoding.succeed(schema, Duration.infinity, { _tag: "Infinity" }) + await Util.assertions.encoding.succeed(schema, Duration.seconds(5), { _tag: "Millis", millis: 5000 }) + await Util.assertions.encoding.succeed(schema, Duration.millis(123456789), { _tag: "Millis", millis: 123456789 }) + await Util.assertions.encoding.succeed(schema, Duration.nanos(555123456789n), { + _tag: "Nanos", + nanos: "555123456789" + }) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/Duration/DurationFromMillis.test.ts b/repos/effect/packages/effect/test/Schema/Schema/Duration/DurationFromMillis.test.ts new file mode 100644 index 0000000..436527c --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/Duration/DurationFromMillis.test.ts @@ -0,0 +1,47 @@ +import { describe, it } from "@effect/vitest" +import { Duration } from "effect" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("DurationFromMillis", () => { + const schema = S.DurationFromMillis + + it("test roundtrip consistency", () => { + Util.assertions.testRoundtripConsistency(schema) + }) + + it("decoding", async () => { + await Util.assertions.decoding.succeed(schema, Infinity, Duration.infinity) + await Util.assertions.decoding.succeed(schema, 0, Duration.millis(0)) + await Util.assertions.decoding.succeed(schema, 1000, Duration.seconds(1)) + await Util.assertions.decoding.succeed(schema, 60 * 1000, Duration.minutes(1)) + await Util.assertions.decoding.succeed(schema, 0.1, Duration.millis(0.1)) + + await Util.assertions.decoding.fail( + schema, + -1, + `DurationFromMillis +└─ Encoded side transformation failure + └─ nonNegative + └─ Predicate refinement failure + └─ Expected a non-negative number to be decoded into a Duration, actual -1` + ) + await Util.assertions.decoding.fail( + schema, + NaN, + `DurationFromMillis +└─ Encoded side transformation failure + └─ nonNegative + └─ Predicate refinement failure + └─ Expected a non-negative number to be decoded into a Duration, actual NaN` + ) + }) + + it("encoding", async () => { + await Util.assertions.encoding.succeed(schema, Duration.infinity, Infinity) + await Util.assertions.encoding.succeed(schema, Duration.seconds(5), 5000) + await Util.assertions.encoding.succeed(schema, Duration.millis(5000), 5000) + await Util.assertions.encoding.succeed(schema, Duration.millis(0.1), 0.1) + await Util.assertions.encoding.succeed(schema, Duration.nanos(5000n), 0.005) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/Duration/DurationFromNanos.test.ts b/repos/effect/packages/effect/test/Schema/Schema/Duration/DurationFromNanos.test.ts new file mode 100644 index 0000000..059bcd0 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/Duration/DurationFromNanos.test.ts @@ -0,0 +1,31 @@ +import { describe, it } from "@effect/vitest" +import { Duration } from "effect" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("DurationFromNanos", () => { + const schema = S.DurationFromNanos + + it("test roundtrip consistency", () => { + Util.assertions.testRoundtripConsistency(schema) + }) + + it("decoding", async () => { + await Util.assertions.decoding.succeed(schema, 0n, Duration.nanos(0n)) + await Util.assertions.decoding.succeed(schema, 1000n, Duration.nanos(1000n)) + }) + + it("encoding", async () => { + await Util.assertions.encoding.succeed(schema, Duration.millis(5), 5000000n) + await Util.assertions.encoding.succeed(schema, Duration.nanos(5000n), 5000n) + await Util.assertions.encoding.fail( + schema, + Duration.infinity, + `DurationFromNanos +└─ Type side transformation failure + └─ a finite duration + └─ Predicate refinement failure + └─ Expected a finite duration, actual Duration(Infinity)` + ) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/Duration/DurationFromSelf.test.ts b/repos/effect/packages/effect/test/Schema/Schema/Duration/DurationFromSelf.test.ts new file mode 100644 index 0000000..2db3342 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/Duration/DurationFromSelf.test.ts @@ -0,0 +1,37 @@ +import { describe, it } from "@effect/vitest" +import { Duration } from "effect" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("DurationFromSelf", () => { + const schema = S.DurationFromSelf + + it("test roundtrip consistency", () => { + Util.assertions.testRoundtripConsistency(schema) + }) + + it("decoding", async () => { + await Util.assertions.decoding.succeed(schema, Duration.nanos(123n), Duration.nanos(123n)) + await Util.assertions.decoding.succeed(schema, Duration.millis(0), Duration.millis(0)) + await Util.assertions.decoding.fail( + schema, + 123, + `Expected DurationFromSelf, actual 123` + ) + await Util.assertions.decoding.fail( + schema, + 123n, + `Expected DurationFromSelf, actual 123n` + ) + }) + + it("encoding", async () => { + await Util.assertions.encoding.succeed(schema, Duration.seconds(5), Duration.seconds(5)) + }) + + it("pretty", () => { + Util.assertions.pretty(schema, Duration.millis(500), "Duration(500ms)") + Util.assertions.pretty(schema, Duration.seconds(30), "Duration(30s)") + Util.assertions.pretty(schema, Duration.minutes(5.25), "Duration(5m 15s)") + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/Duration/betweenDuration.test.ts b/repos/effect/packages/effect/test/Schema/Schema/Duration/betweenDuration.test.ts new file mode 100644 index 0000000..3564e31 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/Duration/betweenDuration.test.ts @@ -0,0 +1,42 @@ +import { describe, it } from "@effect/vitest" +import { Duration } from "effect" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("betweenDuration", () => { + const schema = S.DurationFromSelf.pipe( + S.betweenDuration("5 seconds", "10 seconds") + ).annotations({ title: "[5 seconds, 10 seconds] interval" }) + + it("decoding", async () => { + await Util.assertions.decoding.fail( + schema, + Duration.decode("4 seconds"), + `[5 seconds, 10 seconds] interval +└─ Predicate refinement failure + └─ Expected a Duration between Duration(5s) and Duration(10s), actual Duration(4s)` + ) + + await Util.assertions.decoding.succeed( + schema, + Duration.decode("7 seconds"), + Duration.decode("7 seconds") + ) + + await Util.assertions.decoding.fail( + schema, + Duration.decode("11 seconds"), + `[5 seconds, 10 seconds] interval +└─ Predicate refinement failure + └─ Expected a Duration between Duration(5s) and Duration(10s), actual Duration(11s)` + ) + }) + + it("encoding", async () => { + await Util.assertions.encoding.succeed( + schema, + Duration.decode("7 seconds"), + Duration.decode("7 seconds") + ) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/Duration/clampDuration.test.ts b/repos/effect/packages/effect/test/Schema/Schema/Duration/clampDuration.test.ts new file mode 100644 index 0000000..5f29d57 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/Duration/clampDuration.test.ts @@ -0,0 +1,28 @@ +import { describe, it } from "@effect/vitest" +import { Duration } from "effect" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("clampDuration", () => { + it("decoding", async () => { + const schema = S.DurationFromSelf.pipe(S.clampDuration("5 seconds", "10 seconds")) + + await Util.assertions.decoding.succeed( + schema, + Duration.decode("1 seconds"), + Duration.decode("5 seconds") + ) + + await Util.assertions.decoding.succeed( + schema, + Duration.decode("6 seconds"), + Duration.decode("6 seconds") + ) + + await Util.assertions.decoding.succeed( + schema, + Duration.decode("11 seconds"), + Duration.decode("10 seconds") + ) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/Duration/greaterThanDuration.test.ts b/repos/effect/packages/effect/test/Schema/Schema/Duration/greaterThanDuration.test.ts new file mode 100644 index 0000000..dd60d0d --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/Duration/greaterThanDuration.test.ts @@ -0,0 +1,40 @@ +import { describe, it } from "@effect/vitest" +import { Duration } from "effect" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("greaterThanDuration", () => { + const schema = S.DurationFromSelf.pipe(S.greaterThanDuration("5 seconds")) + + it("decoding", async () => { + await Util.assertions.decoding.succeed( + schema, + Duration.decode("6 seconds"), + Duration.decode("6 seconds") + ) + + await Util.assertions.decoding.fail( + schema, + Duration.decode("5 seconds"), + `greaterThanDuration(5 seconds) +└─ Predicate refinement failure + └─ Expected a Duration greater than Duration(5s), actual Duration(5s)` + ) + + await Util.assertions.decoding.fail( + schema, + Duration.decode("4 seconds"), + `greaterThanDuration(5 seconds) +└─ Predicate refinement failure + └─ Expected a Duration greater than Duration(5s), actual Duration(4s)` + ) + }) + + it("encoding", async () => { + await Util.assertions.encoding.succeed( + schema, + Duration.decode("6 seconds"), + Duration.decode("6 seconds") + ) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/Duration/greaterThanOrEqualToDuration.test.ts b/repos/effect/packages/effect/test/Schema/Schema/Duration/greaterThanOrEqualToDuration.test.ts new file mode 100644 index 0000000..4b9e612 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/Duration/greaterThanOrEqualToDuration.test.ts @@ -0,0 +1,38 @@ +import { describe, it } from "@effect/vitest" +import { Duration } from "effect" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("greaterThanOrEqualToDuration", () => { + const schema = S.DurationFromSelf.pipe(S.greaterThanOrEqualToDuration("5 seconds")) + + it("decoding", async () => { + await Util.assertions.decoding.succeed( + schema, + Duration.decode("6 seconds"), + Duration.decode("6 seconds") + ) + + await Util.assertions.decoding.succeed( + schema, + Duration.decode("5 seconds"), + Duration.decode("5 seconds") + ) + + await Util.assertions.decoding.fail( + schema, + Duration.decode("4 seconds"), + `greaterThanOrEqualToDuration(5 seconds) +└─ Predicate refinement failure + └─ Expected a Duration greater than or equal to Duration(5s), actual Duration(4s)` + ) + }) + + it("encoding", async () => { + await Util.assertions.encoding.succeed( + schema, + Duration.decode("5 seconds"), + Duration.decode("5 seconds") + ) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/Duration/lessThanDuration.test.ts b/repos/effect/packages/effect/test/Schema/Schema/Duration/lessThanDuration.test.ts new file mode 100644 index 0000000..868746b --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/Duration/lessThanDuration.test.ts @@ -0,0 +1,40 @@ +import { describe, it } from "@effect/vitest" +import { Duration } from "effect" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("lessThanDuration", () => { + const schema = S.DurationFromSelf.pipe(S.lessThanDuration("5 seconds")) + + it("decoding", async () => { + await Util.assertions.decoding.succeed( + schema, + Duration.decode("4 seconds"), + Duration.decode("4 seconds") + ) + + await Util.assertions.decoding.fail( + schema, + Duration.decode("5 seconds"), + `lessThanDuration(5 seconds) +└─ Predicate refinement failure + └─ Expected a Duration less than Duration(5s), actual Duration(5s)` + ) + + await Util.assertions.decoding.fail( + schema, + Duration.decode("6 seconds"), + `lessThanDuration(5 seconds) +└─ Predicate refinement failure + └─ Expected a Duration less than Duration(5s), actual Duration(6s)` + ) + }) + + it("encoding", async () => { + await Util.assertions.encoding.succeed( + schema, + Duration.decode("4 seconds"), + Duration.decode("4 seconds") + ) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/Duration/lessThanOrEqualToDuration.test.ts b/repos/effect/packages/effect/test/Schema/Schema/Duration/lessThanOrEqualToDuration.test.ts new file mode 100644 index 0000000..1df7a55 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/Duration/lessThanOrEqualToDuration.test.ts @@ -0,0 +1,38 @@ +import { describe, it } from "@effect/vitest" +import { Duration } from "effect" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("lessThanOrEqualToDuration", () => { + const schema = S.DurationFromSelf.pipe(S.lessThanOrEqualToDuration("5 seconds")) + + it("decoding", async () => { + await Util.assertions.decoding.succeed( + schema, + Duration.decode("4 seconds"), + Duration.decode("4 seconds") + ) + + await Util.assertions.decoding.succeed( + schema, + Duration.decode("5 seconds"), + Duration.decode("5 seconds") + ) + + await Util.assertions.decoding.fail( + schema, + Duration.decode("6 seconds"), + `lessThanOrEqualToDuration(5 seconds) +└─ Predicate refinement failure + └─ Expected a Duration less than or equal to Duration(5s), actual Duration(6s)` + ) + }) + + it("encoding", async () => { + await Util.assertions.encoding.succeed( + schema, + Duration.decode("5 seconds"), + Duration.decode("5 seconds") + ) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/Either/Either.test.ts b/repos/effect/packages/effect/test/Schema/Schema/Either/Either.test.ts new file mode 100644 index 0000000..7b7e28f --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/Either/Either.test.ts @@ -0,0 +1,30 @@ +import { describe, it } from "@effect/vitest" +import * as E from "effect/Either" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("Either", () => { + it("test roundtrip consistency", () => { + Util.assertions.testRoundtripConsistency(S.Either({ left: S.String, right: S.Number })) + }) + + it("decoding", async () => { + const schema = S.Either({ left: S.String, right: S.NumberFromString }) + await Util.assertions.decoding.succeed( + schema, + JSON.parse(JSON.stringify(E.left("a"))), + E.left("a") + ) + await Util.assertions.decoding.succeed( + schema, + JSON.parse(JSON.stringify(E.right("1"))), + E.right(1) + ) + }) + + it("encoding", async () => { + const schema = S.Either({ left: S.String, right: S.NumberFromString }) + await Util.assertions.encoding.succeed(schema, E.left("a"), { _tag: "Left", left: "a" }) + await Util.assertions.encoding.succeed(schema, E.right(1), { _tag: "Right", right: "1" }) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/Either/EitherFromSelf.test.ts b/repos/effect/packages/effect/test/Schema/Schema/Either/EitherFromSelf.test.ts new file mode 100644 index 0000000..5cb672c --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/Either/EitherFromSelf.test.ts @@ -0,0 +1,65 @@ +import { describe, it } from "@effect/vitest" +import { assertFalse, assertTrue } from "@effect/vitest/utils" +import * as E from "effect/Either" +import * as P from "effect/ParseResult" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("EitherFromSelf", () => { + it("arbitrary", () => { + Util.assertions.arbitrary.validateGeneratedValues(S.EitherFromSelf({ left: S.String, right: S.Number })) + }) + + it("test roundtrip consistency", () => { + Util.assertions.testRoundtripConsistency(S.EitherFromSelf({ left: S.String, right: S.Number })) + }) + + it("is", () => { + const schema = S.EitherFromSelf({ left: S.String, right: S.Number }) + const is = P.is(schema) + assertTrue(is(E.left("a"))) + assertTrue(is(E.right(1))) + assertFalse(is(null)) + assertFalse(is(E.right("a"))) + assertFalse(is(E.left(1))) + + assertFalse(is({ _tag: "Right", right: 1 })) + assertFalse(is({ _tag: "Left", left: "a" })) + }) + + it("decoding", async () => { + const schema = S.EitherFromSelf({ left: S.NumberFromString, right: S.BooleanFromString }) + await Util.assertions.decoding.succeed(schema, E.left("1"), E.left(1)) + await Util.assertions.decoding.succeed(schema, E.right("true"), E.right(true)) + + await Util.assertions.decoding.fail( + schema, + null, + `Expected Either, actual null` + ) + await Util.assertions.decoding.fail( + schema, + E.right(""), + `Either +└─ BooleanFromString + └─ Encoded side transformation failure + └─ a string to be decoded into a boolean + ├─ Expected "true", actual "" + └─ Expected "false", actual ""` + ) + await Util.assertions.decoding.fail( + schema, + E.left("a"), + `Either +└─ NumberFromString + └─ Transformation process failure + └─ Unable to decode "a" into a number` + ) + }) + + it("pretty", () => { + const schema = S.EitherFromSelf({ left: S.String, right: S.Number }) + Util.assertions.pretty(schema, E.left("a"), `left("a")`) + Util.assertions.pretty(schema, E.right(1), "right(1)") + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/Either/EitherFromUnion.test.ts b/repos/effect/packages/effect/test/Schema/Schema/Either/EitherFromUnion.test.ts new file mode 100644 index 0000000..1150a3e --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/Either/EitherFromUnion.test.ts @@ -0,0 +1,137 @@ +import { describe, it } from "@effect/vitest" +import { assertTrue } from "@effect/vitest/utils" +import * as E from "effect/Either" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("EitherFromUnion", () => { + it("test roundtrip consistency", () => { + Util.assertions.testRoundtripConsistency(S.EitherFromUnion({ left: S.String, right: S.Number })) + }) + + it("decoding success", async () => { + const schema = S.EitherFromUnion({ left: S.DateFromString, right: S.NumberFromString }) + await Util.assertions.decoding.succeed(schema, "1970-01-01T00:00:00.000Z", E.left(new Date(0))) + await Util.assertions.decoding.succeed(schema, "1", E.right(1)) + + assertTrue(E.isEither(S.decodeSync(schema)("1970-01-01T00:00:00.000Z"))) + assertTrue(E.isEither(S.decodeSync(schema)("1"))) + }) + + it("decoding error (Encoded side transformation failure)", async () => { + const schema = S.EitherFromUnion({ left: S.Number, right: S.String }) + await Util.assertions.decoding.fail( + schema, + undefined, + `((string <-> RightEncoded) | (number <-> LeftEncoded) <-> Either) +└─ Encoded side transformation failure + └─ (string <-> RightEncoded) | (number <-> LeftEncoded) + ├─ (string <-> RightEncoded) + │ └─ Encoded side transformation failure + │ └─ Expected string, actual undefined + └─ (number <-> LeftEncoded) + └─ Encoded side transformation failure + └─ Expected number, actual undefined` + ) + }) + + it("decoding error (Transformation process failure)", async () => { + const schema = S.EitherFromUnion({ left: S.Number, right: S.compose(S.Boolean, S.String, { strict: false }) }) + await Util.assertions.decoding.fail( + schema, + true, + `(((boolean <-> string) <-> RightEncoded) | (number <-> LeftEncoded) <-> Either) +└─ Encoded side transformation failure + └─ ((boolean <-> string) <-> RightEncoded) | (number <-> LeftEncoded) + ├─ ((boolean <-> string) <-> RightEncoded) + │ └─ Encoded side transformation failure + │ └─ (boolean <-> string) + │ └─ Type side transformation failure + │ └─ Expected string, actual true + └─ (number <-> LeftEncoded) + └─ Encoded side transformation failure + └─ Expected number, actual true` + ) + }) + + it("decoding prefer right", async () => { + const schema = S.EitherFromUnion({ left: S.NumberFromString, right: S.NumberFromString }) + await Util.assertions.decoding.succeed(schema, "1", E.right(1)) + }) + + it("encoding success", async () => { + const schema = S.EitherFromUnion({ left: S.DateFromString, right: S.NumberFromString }) + await Util.assertions.encoding.succeed(schema, E.left(new Date(0)), "1970-01-01T00:00:00.000Z") + await Util.assertions.encoding.succeed(schema, E.right(1), "1") + }) + + it("encoding error", async () => { + const schema = S.EitherFromUnion({ + left: S.compose(S.DateFromString, S.Unknown, { strict: false }), + right: S.compose(S.NumberFromString, S.Unknown, { strict: false }) + }) + await Util.assertions.encoding.fail( + schema, + E.left(undefined), + `(((NumberFromString <-> unknown) <-> RightEncoded) | ((DateFromString <-> unknown) <-> LeftEncoded) <-> Either) +└─ Encoded side transformation failure + └─ ((NumberFromString <-> unknown) <-> RightEncoded) | ((DateFromString <-> unknown) <-> LeftEncoded) + └─ ((DateFromString <-> unknown) <-> LeftEncoded) + └─ Encoded side transformation failure + └─ (DateFromString <-> unknown) + └─ Encoded side transformation failure + └─ DateFromString + └─ Type side transformation failure + └─ Expected DateFromSelf, actual undefined` + ) + await Util.assertions.encoding.fail( + schema, + E.right(undefined), + `(((NumberFromString <-> unknown) <-> RightEncoded) | ((DateFromString <-> unknown) <-> LeftEncoded) <-> Either) +└─ Encoded side transformation failure + └─ ((NumberFromString <-> unknown) <-> RightEncoded) | ((DateFromString <-> unknown) <-> LeftEncoded) + └─ ((NumberFromString <-> unknown) <-> RightEncoded) + └─ Encoded side transformation failure + └─ (NumberFromString <-> unknown) + └─ Encoded side transformation failure + └─ NumberFromString + └─ Type side transformation failure + └─ Expected number, actual undefined` + ) + }) + + it("encoding don't overlap", async () => { + const schema = S.EitherFromUnion({ + left: S.compose(S.DateFromString, S.Unknown, { strict: false }), + right: S.compose(S.NumberFromString, S.Unknown, { strict: false }) + }) + await Util.assertions.encoding.fail( + schema, + E.left(1), + `(((NumberFromString <-> unknown) <-> RightEncoded) | ((DateFromString <-> unknown) <-> LeftEncoded) <-> Either) +└─ Encoded side transformation failure + └─ ((NumberFromString <-> unknown) <-> RightEncoded) | ((DateFromString <-> unknown) <-> LeftEncoded) + └─ ((DateFromString <-> unknown) <-> LeftEncoded) + └─ Encoded side transformation failure + └─ (DateFromString <-> unknown) + └─ Encoded side transformation failure + └─ DateFromString + └─ Type side transformation failure + └─ Expected DateFromSelf, actual 1` + ) + await Util.assertions.encoding.fail( + schema, + E.right(new Date(0)), + `(((NumberFromString <-> unknown) <-> RightEncoded) | ((DateFromString <-> unknown) <-> LeftEncoded) <-> Either) +└─ Encoded side transformation failure + └─ ((NumberFromString <-> unknown) <-> RightEncoded) | ((DateFromString <-> unknown) <-> LeftEncoded) + └─ ((NumberFromString <-> unknown) <-> RightEncoded) + └─ Encoded side transformation failure + └─ (NumberFromString <-> unknown) + └─ Encoded side transformation failure + └─ NumberFromString + └─ Type side transformation failure + └─ Expected number, actual ${new Date(0).toISOString()}` + ) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/Enums/Enums.test.ts b/repos/effect/packages/effect/test/Schema/Schema/Enums/Enums.test.ts new file mode 100644 index 0000000..bf8d433 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/Enums/Enums.test.ts @@ -0,0 +1,98 @@ +import { describe, it } from "@effect/vitest" +import { strictEqual } from "@effect/vitest/utils" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("Enums", () => { + it("enums should be exposed", () => { + enum Fruits { + Apple, + Banana + } + const schema = S.Enums(Fruits).annotations({ identifier: "Fruits" }) + strictEqual(schema.enums.Apple, 0) + strictEqual(schema.enums.Banana, 1) + }) + + describe("Numeric enums", () => { + enum Fruits { + Apple, + Banana + } + const schema = S.Enums(Fruits) + + it("decoding", async () => { + await Util.assertions.decoding.succeed(schema, Fruits.Apple) + await Util.assertions.decoding.succeed(schema, Fruits.Banana) + await Util.assertions.decoding.succeed(schema, 0) + await Util.assertions.decoding.succeed(schema, 1) + + await Util.assertions.decoding.fail( + schema, + 3, + `Expected , actual 3` + ) + }) + + it("encoding", async () => { + await Util.assertions.encoding.succeed(schema, Fruits.Apple, 0) + await Util.assertions.encoding.succeed(schema, Fruits.Banana, 1) + }) + }) + + describe("String enums", () => { + enum Fruits { + Apple = "apple", + Banana = "banana", + Cantaloupe = 0 + } + const schema = S.Enums(Fruits) + + it("decoding", async () => { + await Util.assertions.decoding.succeed(schema, Fruits.Apple) + await Util.assertions.decoding.succeed(schema, Fruits.Cantaloupe) + await Util.assertions.decoding.succeed(schema, "apple") + await Util.assertions.decoding.succeed(schema, "banana") + await Util.assertions.decoding.succeed(schema, 0) + + await Util.assertions.decoding.fail( + schema, + "Cantaloupe", + `Expected , actual "Cantaloupe"` + ) + }) + + it("encoding", async () => { + await Util.assertions.encoding.succeed(schema, Fruits.Apple) + await Util.assertions.encoding.succeed(schema, Fruits.Banana) + await Util.assertions.encoding.succeed(schema, Fruits.Cantaloupe) + }) + }) + + describe("Const enums", () => { + const Fruits = { + Apple: "apple", + Banana: "banana", + Cantaloupe: 3 + } as const + const schema = S.Enums(Fruits) + + it("decoding", async () => { + await Util.assertions.decoding.succeed(schema, "apple") + await Util.assertions.decoding.succeed(schema, "banana") + await Util.assertions.decoding.succeed(schema, 3) + + await Util.assertions.decoding.fail( + schema, + "Cantaloupe", + `Expected , actual "Cantaloupe"` + ) + }) + + it("encoding", async () => { + await Util.assertions.encoding.succeed(schema, Fruits.Apple, "apple") + await Util.assertions.encoding.succeed(schema, Fruits.Banana, "banana") + await Util.assertions.encoding.succeed(schema, Fruits.Cantaloupe, 3) + }) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/Exit/Exit.test.ts b/repos/effect/packages/effect/test/Schema/Schema/Exit/Exit.test.ts new file mode 100644 index 0000000..0ff8095 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/Exit/Exit.test.ts @@ -0,0 +1,70 @@ +import { describe, it } from "@effect/vitest" +import { Cause, Exit, Schema as S } from "effect" +import * as Util from "../../TestUtils.js" + +describe("Exit", () => { + it("test roundtrip consistency", () => { + Util.assertions.testRoundtripConsistency(S.Exit({ failure: S.String, success: S.Number, defect: S.Defect })) + }) + + it("decoding", async () => { + const schema = S.Exit({ failure: S.String, success: S.Number, defect: S.Defect }) + await Util.assertions.decoding.succeed( + schema, + { _tag: "Failure", cause: { _tag: "Fail", error: "error" } }, + Exit.fail("error") + ) + await Util.assertions.decoding.succeed( + schema, + { _tag: "Success", value: 123 }, + Exit.succeed(123) + ) + await Util.assertions.decoding.fail( + schema, + { _tag: "Success", value: null }, + `(ExitEncoded <-> Exit) +└─ Encoded side transformation failure + └─ ExitEncoded + └─ { readonly _tag: "Success"; readonly value: number } + └─ ["value"] + └─ Expected number, actual null` + ) + await Util.assertions.decoding.fail( + schema, + { _tag: "Failure", cause: null }, + `(ExitEncoded <-> Exit) +└─ Encoded side transformation failure + └─ ExitEncoded + └─ { readonly _tag: "Failure"; readonly cause: CauseEncoded } + └─ ["cause"] + └─ Expected CauseEncoded, actual null` + ) + }) + + describe("encoding", async () => { + it("should raise an error when a non-encodable Cause is passed", async () => { + const schema = S.Exit({ failure: S.String, success: S.Number, defect: Util.Defect }) + await Util.assertions.encoding.fail( + schema, + Exit.failCause(Cause.die(null)), + `(ExitEncoded object)> <-> Exit) +└─ Type side transformation failure + └─ Exit + └─ Cause + └─ CauseEncoded + └─ { readonly _tag: "Die"; readonly defect: object } + └─ ["defect"] + └─ Expected object, actual null` + ) + }) + }) + + it("using the built-in Defect schema as defect argument", async () => { + const schema = S.Exit({ failure: S.String, success: S.Number, defect: S.Defect }) + await Util.assertions.encoding.succeed(schema, Exit.fail("error"), { + _tag: "Failure", + cause: { _tag: "Fail", error: "error" } + }) + await Util.assertions.encoding.succeed(schema, Exit.succeed(123), { _tag: "Success", value: 123 }) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/Exit/ExitFromSelf.test.ts b/repos/effect/packages/effect/test/Schema/Schema/Exit/ExitFromSelf.test.ts new file mode 100644 index 0000000..0521323 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/Exit/ExitFromSelf.test.ts @@ -0,0 +1,57 @@ +import { describe, it } from "@effect/vitest" +import * as Exit from "effect/Exit" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("ExitFromSelf", () => { + it("arbitrary", () => { + Util.assertions.arbitrary.validateGeneratedValues( + S.ExitFromSelf({ failure: S.String, success: S.Number, defect: S.Unknown }) + ) + }) + + it("decoding", async () => { + const schema = S.ExitFromSelf({ failure: S.NumberFromString, success: S.BooleanFromString, defect: S.Unknown }) + await Util.assertions.decoding.succeed(schema, Exit.fail("1"), Exit.fail(1)) + await Util.assertions.decoding.succeed(schema, Exit.succeed("true"), Exit.succeed(true)) + + await Util.assertions.decoding.fail( + schema, + null, + `Expected Exit, actual null` + ) + await Util.assertions.decoding.fail( + schema, + Exit.succeed(""), + `Exit +└─ BooleanFromString + └─ Encoded side transformation failure + └─ a string to be decoded into a boolean + ├─ Expected "true", actual "" + └─ Expected "false", actual ""` + ) + await Util.assertions.decoding.fail( + schema, + Exit.fail("a"), + `Exit +└─ Cause + └─ CauseEncoded + └─ { readonly _tag: "Fail"; readonly error: NumberFromString } + └─ ["error"] + └─ NumberFromString + └─ Transformation process failure + └─ Unable to decode "a" into a number` + ) + }) + + describe("encoding", async () => { + it("should handle a defect schema", async () => { + const schema = S.ExitFromSelf({ + success: S.Number, + failure: S.String, + defect: Util.Defect + }) + await Util.assertions.encoding.succeed(schema, Exit.die({ a: 1 }), Exit.die(`{"a":1}`)) + }) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/FiberId/FiberId.test.ts b/repos/effect/packages/effect/test/Schema/Schema/FiberId/FiberId.test.ts new file mode 100644 index 0000000..1d40aad --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/FiberId/FiberId.test.ts @@ -0,0 +1,40 @@ +import { describe, it } from "@effect/vitest" +import * as FiberId from "effect/FiberId" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("FiberId", () => { + it("test roundtrip consistency", () => { + Util.assertions.testRoundtripConsistency(S.FiberId) + }) + + it("decoding", async () => { + const schema = S.FiberId + + await Util.assertions.decoding.succeed(schema, { _tag: "None" }, FiberId.none) + await Util.assertions.decoding.succeed( + schema, + { _tag: "Runtime", id: 1, startTimeMillis: 100 }, + FiberId.runtime(1, 100) + ) + await Util.assertions.decoding.succeed( + schema, + { _tag: "Composite", left: { _tag: "None" }, right: { _tag: "None" } }, + FiberId.composite(FiberId.none, FiberId.none) + ) + + await Util.assertions.decoding.fail( + schema, + { _tag: "Composite", left: { _tag: "None" }, right: { _tag: "-" } }, + `FiberId +└─ Encoded side transformation failure + └─ FiberIdEncoded + └─ FiberIdCompositeEncoded + └─ ["right"] + └─ FiberIdEncoded + └─ { readonly _tag: "None" | "Runtime" | "Composite" } + └─ ["_tag"] + └─ Expected "None" | "Runtime" | "Composite", actual "-"` + ) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/FiberId/FiberIdFromSelf.test.ts b/repos/effect/packages/effect/test/Schema/Schema/FiberId/FiberIdFromSelf.test.ts new file mode 100644 index 0000000..87a1edc --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/FiberId/FiberIdFromSelf.test.ts @@ -0,0 +1,39 @@ +import { describe, it } from "@effect/vitest" +import * as FiberId from "effect/FiberId" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("FiberIdFromSelf", () => { + it("arbitrary", () => { + Util.assertions.arbitrary.validateGeneratedValues(S.FiberIdFromSelf) + }) + + it("test roundtrip consistency", () => { + Util.assertions.testRoundtripConsistency(S.FiberIdFromSelf) + }) + + it("decoding", async () => { + const schema = S.FiberIdFromSelf + + await Util.assertions.decoding.succeed(schema, FiberId.none) + await Util.assertions.decoding.succeed(schema, FiberId.runtime(1, 100)) + await Util.assertions.decoding.succeed(schema, FiberId.composite(FiberId.none, FiberId.none)) + + await Util.assertions.decoding.fail( + schema, + null, + `Expected FiberIdFromSelf, actual null` + ) + }) + + it("pretty", () => { + const schema = S.FiberIdFromSelf + Util.assertions.pretty(schema, FiberId.none, `FiberId.none`) + Util.assertions.pretty(schema, FiberId.runtime(1, 100), `FiberId.runtime(1, 100)`) + Util.assertions.pretty( + schema, + FiberId.composite(FiberId.none, FiberId.none), + `FiberId.composite(FiberId.none, FiberId.none)` + ) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/HashMap/HashMap.test.ts b/repos/effect/packages/effect/test/Schema/Schema/HashMap/HashMap.test.ts new file mode 100644 index 0000000..0cc2e04 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/HashMap/HashMap.test.ts @@ -0,0 +1,48 @@ +import { describe, it } from "@effect/vitest" +import * as HashMap from "effect/HashMap" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("HashMap", () => { + it("test roundtrip consistency", () => { + Util.assertions.testRoundtripConsistency(S.HashMap({ key: S.Number, value: S.String })) + }) + + it("decoding", async () => { + const schema = S.HashMap({ key: S.Number, value: S.String }) + await Util.assertions.decoding.succeed(schema, [], HashMap.fromIterable([])) + await Util.assertions.decoding.succeed( + schema, + [[1, "a"], [2, "b"], [3, "c"]], + HashMap.fromIterable([[1, "a"], [2, "b"], [3, "c"]]) + ) + + await Util.assertions.decoding.fail( + schema, + null, + `(ReadonlyArray <-> HashMap) +└─ Encoded side transformation failure + └─ Expected ReadonlyArray, actual null` + ) + await Util.assertions.decoding.fail( + schema, + [[1, "a"], [2, 1]], + `(ReadonlyArray <-> HashMap) +└─ Encoded side transformation failure + └─ ReadonlyArray + └─ [1] + └─ readonly [number, string] + └─ [1] + └─ Expected string, actual 1` + ) + }) + + it("encoding", async () => { + const schema = S.HashMap({ key: S.Number, value: S.String }) + await Util.assertions.encoding.succeed(schema, HashMap.fromIterable([]), []) + await Util.assertions.encoding.succeed(schema, HashMap.fromIterable([[1, "a"], [2, "b"], [3, "c"]]), [[1, "a"], [ + 2, + "b" + ], [3, "c"]]) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/HashMap/HashMapFromSelf.test.ts b/repos/effect/packages/effect/test/Schema/Schema/HashMap/HashMapFromSelf.test.ts new file mode 100644 index 0000000..94307e3 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/HashMap/HashMapFromSelf.test.ts @@ -0,0 +1,71 @@ +import { describe, it } from "@effect/vitest" +import { assertFalse, assertTrue } from "@effect/vitest/utils" +import * as HashMap from "effect/HashMap" +import * as P from "effect/ParseResult" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("HashMapFromSelf", () => { + it("test roundtrip consistency", () => { + Util.assertions.testRoundtripConsistency(S.HashMapFromSelf({ key: S.Number, value: S.String })) + }) + + it("decoding", async () => { + const schema = S.HashMapFromSelf({ key: S.NumberFromString, value: S.String }) + await Util.assertions.decoding.succeed(schema, HashMap.fromIterable([])) + await Util.assertions.decoding.succeed( + schema, + HashMap.fromIterable([["1", "a"], ["2", "b"], ["3", "c"]]), + HashMap.fromIterable([[1, "a"], [2, "b"], [3, "c"]]) + ) + + await Util.assertions.decoding.fail( + schema, + null, + `Expected HashMap, actual null` + ) + await Util.assertions.decoding.fail( + schema, + HashMap.fromIterable([["1", "a"], ["a", "b"]]), + `HashMap +└─ ReadonlyArray + └─ [0] + └─ readonly [NumberFromString, string] + └─ [0] + └─ NumberFromString + └─ Transformation process failure + └─ Unable to decode "a" into a number` + ) + }) + + it("encoding", async () => { + const schema = S.HashMapFromSelf({ key: S.NumberFromString, value: S.String }) + await Util.assertions.encoding.succeed(schema, HashMap.fromIterable([]), HashMap.fromIterable([])) + await Util.assertions.encoding.succeed( + schema, + HashMap.fromIterable([[1, "a"], [2, "b"], [3, "c"]]), + HashMap.fromIterable([["1", "a"], ["2", "b"], ["3", "c"]]) + ) + }) + + it("is", () => { + const schema = S.HashMapFromSelf({ key: S.Number, value: S.String }) + const is = P.is(schema) + assertTrue(is(HashMap.fromIterable([]))) + assertTrue(is(HashMap.fromIterable([[1, "a"], [2, "b"], [3, "c"]]))) + + assertFalse(is(null)) + assertFalse(is(undefined)) + assertFalse(is(HashMap.fromIterable([[1, "a"], [2, 1]]))) + assertFalse(is(HashMap.fromIterable([[1, 1], [2, "b"]]))) + assertFalse(is(HashMap.fromIterable([[1, 1], [2, 2]]))) + assertFalse(is(HashMap.fromIterable([["a", 1], ["b", 2], [3, 1]]))) + assertFalse(is(HashMap.fromIterable([[1, "a"], [2, "b"], [3, 1]]))) + }) + + it("pretty", () => { + const schema = S.HashMapFromSelf({ key: S.Number, value: S.String }) + Util.assertions.pretty(schema, HashMap.fromIterable([]), "HashMap([])") + Util.assertions.pretty(schema, HashMap.fromIterable([[1, "a"], [2, "b"]]), `HashMap([[1, "a"], [2, "b"]])`) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/HashSet/HashSet.test.ts b/repos/effect/packages/effect/test/Schema/Schema/HashSet/HashSet.test.ts new file mode 100644 index 0000000..7e302f8 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/HashSet/HashSet.test.ts @@ -0,0 +1,39 @@ +import { describe, it } from "@effect/vitest" +import * as HashSet from "effect/HashSet" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("HashSet", () => { + it("test roundtrip consistency", () => { + Util.assertions.testRoundtripConsistency(S.HashSet(S.Number)) + }) + + it("decoding", async () => { + const schema = S.HashSet(S.Number) + await Util.assertions.decoding.succeed(schema, [], HashSet.fromIterable([])) + await Util.assertions.decoding.succeed(schema, [1, 2, 3], HashSet.fromIterable([1, 2, 3])) + + await Util.assertions.decoding.fail( + schema, + null, + `(ReadonlyArray <-> HashSet) +└─ Encoded side transformation failure + └─ Expected ReadonlyArray, actual null` + ) + await Util.assertions.decoding.fail( + schema, + [1, "a"], + `(ReadonlyArray <-> HashSet) +└─ Encoded side transformation failure + └─ ReadonlyArray + └─ [1] + └─ Expected number, actual "a"` + ) + }) + + it("encoding", async () => { + const schema = S.HashSet(S.Number) + await Util.assertions.encoding.succeed(schema, HashSet.empty(), []) + await Util.assertions.encoding.succeed(schema, HashSet.fromIterable([1, 2, 3]), [1, 2, 3]) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/HashSet/HashSetFromSelf.test.ts b/repos/effect/packages/effect/test/Schema/Schema/HashSet/HashSetFromSelf.test.ts new file mode 100644 index 0000000..f806295 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/HashSet/HashSetFromSelf.test.ts @@ -0,0 +1,64 @@ +import { describe, it } from "@effect/vitest" +import { assertFalse, assertTrue } from "@effect/vitest/utils" +import * as HashSet from "effect/HashSet" +import * as P from "effect/ParseResult" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("HashSetFromSelf", () => { + it("test roundtrip consistency", () => { + Util.assertions.testRoundtripConsistency(S.HashSetFromSelf(S.Number)) + }) + + it("decoding", async () => { + const schema = S.HashSetFromSelf(S.NumberFromString) + await Util.assertions.decoding.succeed(schema, HashSet.empty(), HashSet.fromIterable([])) + await Util.assertions.decoding.succeed( + schema, + HashSet.fromIterable(["1", "2", "3"]), + HashSet.fromIterable([1, 2, 3]) + ) + + await Util.assertions.decoding.fail( + schema, + null, + `Expected HashSet, actual null` + ) + await Util.assertions.decoding.fail( + schema, + HashSet.fromIterable(["1", "a", "3"]), + `HashSet +└─ ReadonlyArray + └─ [0] + └─ NumberFromString + └─ Transformation process failure + └─ Unable to decode "a" into a number` + ) + }) + + it("encoding", async () => { + const schema = S.HashSetFromSelf(S.NumberFromString) + await Util.assertions.encoding.succeed(schema, HashSet.empty(), HashSet.fromIterable([])) + await Util.assertions.encoding.succeed( + schema, + HashSet.fromIterable([1, 2, 3]), + HashSet.fromIterable(["1", "2", "3"]) + ) + }) + + it("is", () => { + const schema = S.HashSetFromSelf(S.String) + const is = P.is(schema) + assertTrue(is(HashSet.empty())) + assertTrue(is(HashSet.fromIterable(["a", "b", "c"]))) + + assertFalse(is(HashSet.fromIterable(["a", "b", 1]))) + assertFalse(is({ _id: Symbol.for("effect/Schema/test/FakeHashSet") })) + }) + + it("pretty", () => { + const schema = S.HashSetFromSelf(S.String) + Util.assertions.pretty(schema, HashSet.empty(), "HashSet()") + Util.assertions.pretty(schema, HashSet.fromIterable(["a", "b"]), `HashSet("a", "b")`) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/List/List.test.ts b/repos/effect/packages/effect/test/Schema/Schema/List/List.test.ts new file mode 100644 index 0000000..f32f223 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/List/List.test.ts @@ -0,0 +1,39 @@ +import { describe, it } from "@effect/vitest" +import * as List from "effect/List" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("List", () => { + it("test roundtrip consistency", () => { + Util.assertions.testRoundtripConsistency(S.List(S.Number)) + }) + + it("decoding", async () => { + const schema = S.List(S.Number) + await Util.assertions.decoding.succeed(schema, [], List.empty()) + await Util.assertions.decoding.succeed(schema, [1, 2, 3], List.fromIterable([1, 2, 3])) + + await Util.assertions.decoding.fail( + schema, + null, + `(ReadonlyArray <-> List) +└─ Encoded side transformation failure + └─ Expected ReadonlyArray, actual null` + ) + await Util.assertions.decoding.fail( + schema, + [1, "a"], + `(ReadonlyArray <-> List) +└─ Encoded side transformation failure + └─ ReadonlyArray + └─ [1] + └─ Expected number, actual "a"` + ) + }) + + it("encoding", async () => { + const schema = S.List(S.Number) + await Util.assertions.encoding.succeed(schema, List.empty(), []) + await Util.assertions.encoding.succeed(schema, List.fromIterable([1, 2, 3]), [1, 2, 3]) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/List/ListFromSelf.test.ts b/repos/effect/packages/effect/test/Schema/Schema/List/ListFromSelf.test.ts new file mode 100644 index 0000000..24a2202 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/List/ListFromSelf.test.ts @@ -0,0 +1,64 @@ +import { describe, it } from "@effect/vitest" +import { assertFalse, assertTrue } from "@effect/vitest/utils" +import * as List from "effect/List" +import * as P from "effect/ParseResult" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("ListFromSelf", () => { + it("test roundtrip consistency", () => { + Util.assertions.testRoundtripConsistency(S.ListFromSelf(S.Number)) + }) + + it("decoding", async () => { + const schema = S.ListFromSelf(S.NumberFromString) + await Util.assertions.decoding.succeed(schema, List.empty(), List.empty()) + await Util.assertions.decoding.succeed( + schema, + List.fromIterable(["1", "2", "3"]), + List.fromIterable([1, 2, 3]) + ) + + await Util.assertions.decoding.fail( + schema, + null, + `Expected List, actual null` + ) + await Util.assertions.decoding.fail( + schema, + List.fromIterable(["1", "a", "3"]), + `List +└─ ReadonlyArray + └─ [1] + └─ NumberFromString + └─ Transformation process failure + └─ Unable to decode "a" into a number` + ) + }) + + it("encoding", async () => { + const schema = S.ListFromSelf(S.NumberFromString) + await Util.assertions.encoding.succeed(schema, List.empty(), List.empty()) + await Util.assertions.encoding.succeed( + schema, + List.fromIterable([1, 2, 3]), + List.fromIterable(["1", "2", "3"]) + ) + }) + + it("is", () => { + const schema = S.ListFromSelf(S.String) + const is = P.is(schema) + assertTrue(is(List.empty())) + assertTrue(is(List.fromIterable(["a", "b", "c"]))) + + assertFalse(is(List.fromIterable(["a", "b", 1]))) + assertFalse(is({ _id: Symbol.for("effect/Schema/test/FakeList") })) + }) + + it("pretty", () => { + const schema = S.ListFromSelf(S.String) + Util.assertions.pretty(schema, List.empty(), "List()") + Util.assertions.pretty(schema, List.fromIterable(["a", "b"]), `List("a", "b")`) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/Literal/Literal.test.ts b/repos/effect/packages/effect/test/Schema/Schema/Literal/Literal.test.ts new file mode 100644 index 0000000..c847ff2 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/Literal/Literal.test.ts @@ -0,0 +1,60 @@ +import { describe, it } from "@effect/vitest" +import { deepStrictEqual, strictEqual } from "@effect/vitest/utils" +import * as S from "effect/Schema" +import * as AST from "effect/SchemaAST" +import * as Util from "../../TestUtils.js" + +describe("Literal", () => { + it("should expose the literals", () => { + const schema = S.Literal("a", "b") + deepStrictEqual(schema.literals, ["a", "b"]) + }) + + it("should return Never when no literals are provided", () => { + strictEqual(S.Literal(), S.Never) + strictEqual(S.Literal(...[]), S.Never) + }) + + it("should return an unwrapped AST with exactly one literal", () => { + deepStrictEqual(S.Literal(1).ast, new AST.Literal(1)) + }) + + it("should return a union with more than one literal", () => { + deepStrictEqual(S.Literal(1, 2).ast, AST.Union.make([new AST.Literal(1), new AST.Literal(2)])) + }) + + it("should return the literal interface when using the .annotations() method", () => { + const schema = S.Literal("a", "b").annotations({ identifier: "literal test" }) + deepStrictEqual(schema.ast.annotations, { [AST.IdentifierAnnotationId]: "literal test" }) + deepStrictEqual(schema.literals, ["a", "b"]) + }) + + describe("decoding", () => { + it("1 member", async () => { + const schema = S.Literal(1) + await Util.assertions.decoding.succeed(schema, 1) + + await Util.assertions.decoding.fail(schema, "a", `Expected 1, actual "a"`) + await Util.assertions.decoding.fail(schema, null, `Expected 1, actual null`) + }) + + it("2 members", async () => { + const schema = S.Literal(1, "a") + await Util.assertions.decoding.succeed(schema, 1) + await Util.assertions.decoding.succeed(schema, "a") + + await Util.assertions.decoding.fail( + schema, + null, + `1 | "a" +├─ Expected 1, actual null +└─ Expected "a", actual null` + ) + }) + }) + + it("encoding", async () => { + const schema = S.Literal(null) + await Util.assertions.encoding.succeed(schema, null, null) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/Map/Map.test.ts b/repos/effect/packages/effect/test/Schema/Schema/Map/Map.test.ts new file mode 100644 index 0000000..d008bf7 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/Map/Map.test.ts @@ -0,0 +1,12 @@ +import { describe, it } from "@effect/vitest" +import { strictEqual } from "@effect/vitest/utils" +import * as S from "effect/Schema" + +describe("Map", () => { + it("description", () => { + strictEqual( + String(S.Map({ key: S.String, value: S.Number })), + "(ReadonlyArray <-> Map)" + ) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/Map/MapFromRecord.test.ts b/repos/effect/packages/effect/test/Schema/Schema/Map/MapFromRecord.test.ts new file mode 100644 index 0000000..dcc290a --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/Map/MapFromRecord.test.ts @@ -0,0 +1,54 @@ +import { describe, it } from "@effect/vitest" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("MapFromRecord", () => { + it("decoding", async () => { + const schema = S.MapFromRecord({ key: S.NumberFromString, value: S.NumberFromString }) + await Util.assertions.decoding.succeed(schema, {}, new Map()) + await Util.assertions.decoding.succeed( + schema, + { 1: "2", 3: "4", 5: "6" }, + new Map([[1, 2], [3, 4], [5, 6]]) + ) + + await Util.assertions.decoding.fail( + schema, + null, + `(a record to be decoded into a Map <-> Map) +└─ Encoded side transformation failure + └─ Expected a record to be decoded into a Map, actual null` + ) + await Util.assertions.decoding.fail( + schema, + { a: "1" }, + `(a record to be decoded into a Map <-> Map) +└─ Type side transformation failure + └─ Map + └─ ReadonlyArray + └─ [0] + └─ readonly [NumberFromString, number] + └─ [0] + └─ NumberFromString + └─ Transformation process failure + └─ Unable to decode "a" into a number` + ) + await Util.assertions.decoding.fail( + schema, + { 1: "a" }, + `(a record to be decoded into a Map <-> Map) +└─ Encoded side transformation failure + └─ a record to be decoded into a Map + └─ ["1"] + └─ NumberFromString + └─ Transformation process failure + └─ Unable to decode "a" into a number` + ) + }) + + it("encoding", async () => { + const schema = S.MapFromRecord({ key: S.NumberFromString, value: S.NumberFromString }) + await Util.assertions.encoding.succeed(schema, new Map(), {}) + await Util.assertions.encoding.succeed(schema, new Map([[1, 2], [3, 4], [5, 6]]), { 1: "2", 3: "4", 5: "6" }) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/Map/MapFromSelf.test.ts b/repos/effect/packages/effect/test/Schema/Schema/Map/MapFromSelf.test.ts new file mode 100644 index 0000000..e7ec9ff --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/Map/MapFromSelf.test.ts @@ -0,0 +1,9 @@ +import { describe, it } from "@effect/vitest" +import { strictEqual } from "@effect/vitest/utils" +import * as S from "effect/Schema" + +describe("MapFromSelf", () => { + it("description", () => { + strictEqual(String(S.MapFromSelf({ key: S.String, value: S.Number })), "Map") + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/Never/Never.test.ts b/repos/effect/packages/effect/test/Schema/Schema/Never/Never.test.ts new file mode 100644 index 0000000..ab95069 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/Never/Never.test.ts @@ -0,0 +1,14 @@ +import { describe, it } from "@effect/vitest" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("Never", () => { + const schema = S.Never + it("decoding", async () => { + await Util.assertions.decoding.fail(schema, 1, "Expected never, actual 1") + }) + + it("encoding", async () => { + await Util.assertions.encoding.fail(schema, 1 as any as never, "Expected never, actual 1") + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/NonEmptyArrayEnsure.test.ts b/repos/effect/packages/effect/test/Schema/Schema/NonEmptyArrayEnsure.test.ts new file mode 100644 index 0000000..ec26ef4 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/NonEmptyArrayEnsure.test.ts @@ -0,0 +1,64 @@ +import { describe, it } from "@effect/vitest" +import * as S from "effect/Schema" +import * as Util from "../TestUtils.js" + +describe("NonEmptyArrayEnsure", () => { + it("decode non-array", async () => { + const schema = S.NonEmptyArrayEnsure(S.NumberFromString) + await Util.assertions.decoding.succeed(schema, "123", [123]) + await Util.assertions.decoding.fail( + schema, + null, + `(NumberFromString | readonly [NumberFromString, ...NumberFromString[]] <-> readonly [number, ...number[]]) +└─ Encoded side transformation failure + └─ NumberFromString | readonly [NumberFromString, ...NumberFromString[]] + ├─ NumberFromString + │ └─ Encoded side transformation failure + │ └─ Expected string, actual null + └─ Expected readonly [NumberFromString, ...NumberFromString[]], actual null` + ) + }) + + it("decode empty array", async () => { + const schema = S.NonEmptyArrayEnsure(S.NumberFromString) + await Util.assertions.decoding.fail( + schema, + [], + `(NumberFromString | readonly [NumberFromString, ...NumberFromString[]] <-> readonly [number, ...number[]]) +└─ Encoded side transformation failure + └─ NumberFromString | readonly [NumberFromString, ...NumberFromString[]] + ├─ NumberFromString + │ └─ Encoded side transformation failure + │ └─ Expected string, actual [] + └─ readonly [NumberFromString, ...NumberFromString[]] + └─ [0] + └─ is missing` + ) + }) + + it("decode array", async () => { + const schema = S.NonEmptyArrayEnsure(S.NumberFromString) + await Util.assertions.decoding.succeed(schema, ["123"], [123]) + await Util.assertions.decoding.fail( + schema, + [null], + `(NumberFromString | readonly [NumberFromString, ...NumberFromString[]] <-> readonly [number, ...number[]]) +└─ Encoded side transformation failure + └─ NumberFromString | readonly [NumberFromString, ...NumberFromString[]] + ├─ NumberFromString + │ └─ Encoded side transformation failure + │ └─ Expected string, actual [null] + └─ readonly [NumberFromString, ...NumberFromString[]] + └─ [0] + └─ NumberFromString + └─ Encoded side transformation failure + └─ Expected string, actual null` + ) + }) + + it("encode", async () => { + const schema = S.NonEmptyArrayEnsure(S.NumberFromString) + await Util.assertions.encoding.succeed(schema, [123], "123") + await Util.assertions.encoding.succeed(schema, [1, 2, 3], ["1", "2", "3"]) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/Number/JsonNumber.test.ts b/repos/effect/packages/effect/test/Schema/Schema/Number/JsonNumber.test.ts new file mode 100644 index 0000000..c73f2dc --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/Number/JsonNumber.test.ts @@ -0,0 +1,57 @@ +import { describe, it } from "@effect/vitest" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("JsonNumber", () => { + it("test roundtrip consistency", () => { + Util.assertions.testRoundtripConsistency(S.JsonNumber) + }) + + it("should exclude NaN from decoding", async () => { + await Util.assertions.decoding.fail( + S.JsonNumber, + NaN, + `JsonNumber +└─ Predicate refinement failure + └─ Expected a finite number, actual NaN` + ) + await Util.assertions.decoding.fail( + S.JsonNumber, + Number.NaN, + `JsonNumber +└─ Predicate refinement failure + └─ Expected a finite number, actual NaN` + ) + }) + + it("should exclude +/- Infinity from decoding", async () => { + await Util.assertions.decoding.fail( + S.JsonNumber, + Infinity, + `JsonNumber +└─ Predicate refinement failure + └─ Expected a finite number, actual Infinity` + ) + await Util.assertions.decoding.fail( + S.JsonNumber, + -Infinity, + `JsonNumber +└─ Predicate refinement failure + └─ Expected a finite number, actual -Infinity` + ) + await Util.assertions.decoding.fail( + S.JsonNumber, + Number.POSITIVE_INFINITY, + `JsonNumber +└─ Predicate refinement failure + └─ Expected a finite number, actual Infinity` + ) + await Util.assertions.decoding.fail( + S.JsonNumber, + Number.NEGATIVE_INFINITY, + `JsonNumber +└─ Predicate refinement failure + └─ Expected a finite number, actual -Infinity` + ) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/Number/Number.test.ts b/repos/effect/packages/effect/test/Schema/Schema/Number/Number.test.ts new file mode 100644 index 0000000..03423ea --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/Number/Number.test.ts @@ -0,0 +1,18 @@ +import { describe, it } from "@effect/vitest" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("Number", () => { + const schema = S.Number + it("decoding", async () => { + await Util.assertions.decoding.succeed(schema, 1, 1) + await Util.assertions.decoding.succeed(schema, NaN, NaN) + await Util.assertions.decoding.succeed(schema, Infinity, Infinity) + await Util.assertions.decoding.succeed(schema, -Infinity, -Infinity) + await Util.assertions.decoding.fail(schema, "a", `Expected number, actual "a"`) + }) + + it("encoding", async () => { + await Util.assertions.encoding.succeed(schema, 1, 1) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/Number/between.test.ts b/repos/effect/packages/effect/test/Schema/Schema/Number/between.test.ts new file mode 100644 index 0000000..fcfeca2 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/Number/between.test.ts @@ -0,0 +1,30 @@ +import { describe, it } from "@effect/vitest" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("between", () => { + const schema = S.Number.pipe(S.between(-1, 1)).annotations({ + title: "[-1, -1] interval" + }) + it("decoding", async () => { + await Util.assertions.decoding.fail( + schema, + -2, + `[-1, -1] interval +└─ Predicate refinement failure + └─ Expected a number between -1 and 1, actual -2` + ) + await Util.assertions.decoding.succeed(schema, 0, 0) + await Util.assertions.decoding.fail( + schema, + 2, + `[-1, -1] interval +└─ Predicate refinement failure + └─ Expected a number between -1 and 1, actual 2` + ) + }) + + it("encoding", async () => { + await Util.assertions.encoding.succeed(schema, 1, 1) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/Number/clamp.test.ts b/repos/effect/packages/effect/test/Schema/Schema/Number/clamp.test.ts new file mode 100644 index 0000000..86f7aa1 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/Number/clamp.test.ts @@ -0,0 +1,19 @@ +import { describe, it } from "@effect/vitest" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("clamp", () => { + it("decoding", async () => { + const schema = S.Number.pipe(S.clamp(-1, 1)) + await Util.assertions.decoding.succeed(schema, 3, 1) + await Util.assertions.decoding.succeed(schema, 0, 0) + await Util.assertions.decoding.succeed(schema, -3, -1) + }) + + it("should support doubles as constraints", async () => { + const schema = S.Number.pipe(S.clamp(1.3, 3.1)) + await Util.assertions.decoding.succeed(schema, 4, 3.1) + await Util.assertions.decoding.succeed(schema, 2, 2) + await Util.assertions.decoding.succeed(schema, 1, 1.3) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/Number/finite.test.ts b/repos/effect/packages/effect/test/Schema/Schema/Number/finite.test.ts new file mode 100644 index 0000000..cebc466 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/Number/finite.test.ts @@ -0,0 +1,36 @@ +import { describe, it } from "@effect/vitest" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("Finite", () => { + const schema = S.Finite + + it("test roundtrip consistency", () => { + Util.assertions.testRoundtripConsistency(schema) + }) + + it("decoding", async () => { + await Util.assertions.decoding.succeed(schema, 1) + await Util.assertions.decoding.fail( + schema, + NaN, + `Finite +└─ Predicate refinement failure + └─ Expected a finite number, actual NaN` + ) + await Util.assertions.decoding.fail( + schema, + Infinity, + `Finite +└─ Predicate refinement failure + └─ Expected a finite number, actual Infinity` + ) + await Util.assertions.decoding.fail( + schema, + -Infinity, + `Finite +└─ Predicate refinement failure + └─ Expected a finite number, actual -Infinity` + ) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/Number/greaterThan.test.ts b/repos/effect/packages/effect/test/Schema/Schema/Number/greaterThan.test.ts new file mode 100644 index 0000000..3cc0368 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/Number/greaterThan.test.ts @@ -0,0 +1,42 @@ +import { describe, it } from "@effect/vitest" +import { assertFalse, assertTrue } from "@effect/vitest/utils" +import * as P from "effect/ParseResult" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("greaterThan", () => { + const schema = S.Number.pipe(S.greaterThan(0)) + + it("test roundtrip consistency", () => { + Util.assertions.testRoundtripConsistency(schema) + }) + + it("is", () => { + const is = P.is(schema) + assertFalse(is(0)) + assertTrue(is(1)) + assertFalse(is(-1)) + }) + + it("decoding", async () => { + await Util.assertions.decoding.succeed(schema, 1) + await Util.assertions.decoding.fail( + schema, + 0, + `greaterThan(0) +└─ Predicate refinement failure + └─ Expected a positive number, actual 0` + ) + await Util.assertions.decoding.fail( + schema, + -1, + `greaterThan(0) +└─ Predicate refinement failure + └─ Expected a positive number, actual -1` + ) + }) + + it("pretty", () => { + Util.assertions.pretty(schema, 1, "1") + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/Number/greaterThanOrEqualTo.test.ts b/repos/effect/packages/effect/test/Schema/Schema/Number/greaterThanOrEqualTo.test.ts new file mode 100644 index 0000000..367d9b0 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/Number/greaterThanOrEqualTo.test.ts @@ -0,0 +1,36 @@ +import { describe, it } from "@effect/vitest" +import { assertFalse, assertTrue } from "@effect/vitest/utils" +import * as P from "effect/ParseResult" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("greaterThanOrEqualTo", () => { + const schema = S.Number.pipe(S.greaterThanOrEqualTo(0)) + + it("test roundtrip consistency", () => { + Util.assertions.testRoundtripConsistency(schema) + }) + + it("is", () => { + const is = P.is(schema) + assertTrue(is(0)) + assertTrue(is(1)) + assertFalse(is(-1)) + }) + + it("decoding", async () => { + await Util.assertions.decoding.succeed(schema, 0) + await Util.assertions.decoding.succeed(schema, 1) + await Util.assertions.decoding.fail( + schema, + -1, + `greaterThanOrEqualTo(0) +└─ Predicate refinement failure + └─ Expected a non-negative number, actual -1` + ) + }) + + it("pretty", () => { + Util.assertions.pretty(schema, 1, "1") + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/Number/int.test.ts b/repos/effect/packages/effect/test/Schema/Schema/Number/int.test.ts new file mode 100644 index 0000000..cb97ce2 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/Number/int.test.ts @@ -0,0 +1,38 @@ +import { describe, it } from "@effect/vitest" +import { assertFalse, assertTrue } from "@effect/vitest/utils" +import * as P from "effect/ParseResult" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("Int", () => { + const schema = S.Int + + it("test roundtrip consistency", () => { + Util.assertions.testRoundtripConsistency(schema) + }) + + it("is", () => { + const is = P.is(schema) + assertTrue(is(0)) + assertTrue(is(1)) + assertFalse(is(0.5)) + assertFalse(is(Number.MAX_SAFE_INTEGER + 1)) + assertFalse(is(Number.MIN_SAFE_INTEGER - 1)) + }) + + it("decoding", async () => { + await Util.assertions.decoding.succeed(schema, 0) + await Util.assertions.decoding.succeed(schema, 1) + await Util.assertions.decoding.fail( + schema, + 0.5, + `Int +└─ Predicate refinement failure + └─ Expected an integer, actual 0.5` + ) + }) + + it("pretty", () => { + Util.assertions.pretty(schema, 1, "1") + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/Number/lessThan.test.ts b/repos/effect/packages/effect/test/Schema/Schema/Number/lessThan.test.ts new file mode 100644 index 0000000..cd92e5b --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/Number/lessThan.test.ts @@ -0,0 +1,42 @@ +import { describe, it } from "@effect/vitest" +import { assertFalse, assertTrue } from "@effect/vitest/utils" +import * as P from "effect/ParseResult" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("lessThan", () => { + it("test roundtrip consistency", () => { + Util.assertions.testRoundtripConsistency(S.Number.pipe(S.lessThan(0))) + }) + + it("is", () => { + const is = P.is(S.Number.pipe(S.lessThan(0))) + assertFalse(is(0)) + assertFalse(is(1)) + assertTrue(is(-1)) + }) + + it("decoding", async () => { + const schema = S.Number.pipe(S.lessThan(0)) + await Util.assertions.decoding.succeed(schema, -1) + await Util.assertions.decoding.fail( + schema, + 0, + `lessThan(0) +└─ Predicate refinement failure + └─ Expected a negative number, actual 0` + ) + await Util.assertions.decoding.fail( + schema, + 1, + `lessThan(0) +└─ Predicate refinement failure + └─ Expected a negative number, actual 1` + ) + }) + + it("pretty", () => { + const schema = S.Number.pipe(S.lessThan(0)) + Util.assertions.pretty(schema, 1, "1") + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/Number/lessThanOrEqualTo.test.ts b/repos/effect/packages/effect/test/Schema/Schema/Number/lessThanOrEqualTo.test.ts new file mode 100644 index 0000000..0fb425e --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/Number/lessThanOrEqualTo.test.ts @@ -0,0 +1,36 @@ +import { describe, it } from "@effect/vitest" +import { assertFalse, assertTrue } from "@effect/vitest/utils" +import * as P from "effect/ParseResult" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("lessThanOrEqualTo", () => { + it("test roundtrip consistency", () => { + Util.assertions.testRoundtripConsistency(S.Number.pipe(S.lessThanOrEqualTo(0))) + }) + + it("is", () => { + const is = P.is(S.Number.pipe(S.lessThanOrEqualTo(0))) + assertTrue(is(0)) + assertFalse(is(1)) + assertTrue(is(-1)) + }) + + it("decoding", async () => { + const schema = S.Number.pipe(S.lessThanOrEqualTo(0)) + await Util.assertions.decoding.succeed(schema, 0) + await Util.assertions.decoding.succeed(schema, -1) + await Util.assertions.decoding.fail( + schema, + 1, + `lessThanOrEqualTo(0) +└─ Predicate refinement failure + └─ Expected a non-positive number, actual 1` + ) + }) + + it("pretty", () => { + const schema = S.Number.pipe(S.lessThanOrEqualTo(0)) + Util.assertions.pretty(schema, 1, "1") + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/Number/multipleOf.test.ts b/repos/effect/packages/effect/test/Schema/Schema/Number/multipleOf.test.ts new file mode 100644 index 0000000..3993bc5 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/Number/multipleOf.test.ts @@ -0,0 +1,51 @@ +import { describe, it } from "@effect/vitest" +import { assertFalse, assertTrue } from "@effect/vitest/utils" +import * as P from "effect/ParseResult" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("multipleOf", () => { + it("test roundtrip consistency", () => { + Util.assertions.testRoundtripConsistency(S.Number.pipe(S.multipleOf(2))) + }) + + it("is", () => { + const schema = S.Number.pipe(S.multipleOf(-.2)) + const is = P.is(schema) + assertTrue(is(-2.8)) + assertTrue(is(-2)) + assertFalse(is(-1.5)) + assertTrue(is(0)) + assertTrue(is(1)) + assertTrue(is(2.6)) + assertFalse(is(3.1)) + }) + + it("decoding", async () => { + const schema = S.Number.pipe(S.multipleOf(2)).annotations({ identifier: "Even" }) + await Util.assertions.decoding.succeed(schema, -4) + await Util.assertions.decoding.fail( + schema, + -3, + `Even +└─ Predicate refinement failure + └─ Expected a number divisible by 2, actual -3` + ) + await Util.assertions.decoding.succeed(schema, 0) + await Util.assertions.decoding.succeed(schema, 2) + await Util.assertions.decoding.fail( + schema, + 2.5, + `Even +└─ Predicate refinement failure + └─ Expected a number divisible by 2, actual 2.5` + ) + await Util.assertions.decoding.fail( + schema, + "", + `Even +└─ From side refinement failure + └─ Expected number, actual ""` + ) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/Number/negative.test.ts b/repos/effect/packages/effect/test/Schema/Schema/Number/negative.test.ts new file mode 100644 index 0000000..34440d5 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/Number/negative.test.ts @@ -0,0 +1,27 @@ +import { describe, it } from "@effect/vitest" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("Negative", () => { + const schema = S.Negative + it("decoding", async () => { + await Util.assertions.decoding.fail( + schema, + 0, + `Negative +└─ Predicate refinement failure + └─ Expected a negative number, actual 0` + ) + await Util.assertions.decoding.fail( + schema, + 1, + `Negative +└─ Predicate refinement failure + └─ Expected a negative number, actual 1` + ) + }) + + it("encoding", async () => { + await Util.assertions.encoding.succeed(schema, -1, -1) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/Number/nonNaN.test.ts b/repos/effect/packages/effect/test/Schema/Schema/Number/nonNaN.test.ts new file mode 100644 index 0000000..e38c20d --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/Number/nonNaN.test.ts @@ -0,0 +1,35 @@ +import { describe, it } from "@effect/vitest" +import { assertFalse, assertTrue } from "@effect/vitest/utils" +import * as P from "effect/ParseResult" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("NonNaN", () => { + const schema = S.NonNaN + + it("test roundtrip consistency", () => { + Util.assertions.testRoundtripConsistency(schema) + }) + + it("is", () => { + const is = P.is(schema) + assertTrue(is(1)) + assertFalse(is(NaN)) + }) + + it("decoding", async () => { + await Util.assertions.decoding.succeed(schema, 1) + await Util.assertions.decoding.fail( + schema, + NaN, + `NonNaN +└─ Predicate refinement failure + └─ Expected a number excluding NaN, actual NaN` + ) + }) + + it("pretty", () => { + Util.assertions.pretty(schema, 1, "1") + Util.assertions.pretty(schema, NaN, "NaN") + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/Number/nonNegative.test.ts b/repos/effect/packages/effect/test/Schema/Schema/Number/nonNegative.test.ts new file mode 100644 index 0000000..4028b3a --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/Number/nonNegative.test.ts @@ -0,0 +1,21 @@ +import { describe, it } from "@effect/vitest" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("NonNegative", () => { + const schema = S.NonNegative + it("decoding", async () => { + await Util.assertions.decoding.succeed(schema, 0, 0) + await Util.assertions.decoding.succeed(schema, 1, 1) + }) + + it("encoding", async () => { + await Util.assertions.encoding.fail( + schema, + -1, + `NonNegative +└─ Predicate refinement failure + └─ Expected a non-negative number, actual -1` + ) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/Number/nonPositive.test.ts b/repos/effect/packages/effect/test/Schema/Schema/Number/nonPositive.test.ts new file mode 100644 index 0000000..9bf4d8a --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/Number/nonPositive.test.ts @@ -0,0 +1,21 @@ +import { describe, it } from "@effect/vitest" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("NonPositive", () => { + const schema = S.NonPositive + it("decoding", async () => { + await Util.assertions.decoding.succeed(schema, 0, 0) + await Util.assertions.decoding.fail( + schema, + 1, + `NonPositive +└─ Predicate refinement failure + └─ Expected a non-positive number, actual 1` + ) + }) + + it("encoding", async () => { + await Util.assertions.encoding.succeed(schema, -1, -1) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/Number/numberFromString.test.ts b/repos/effect/packages/effect/test/Schema/Schema/Number/numberFromString.test.ts new file mode 100644 index 0000000..421f9a3 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/Number/numberFromString.test.ts @@ -0,0 +1,69 @@ +import { describe, it } from "@effect/vitest" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("NumberFromString", () => { + const schema = S.NumberFromString + + it("test roundtrip consistency", () => { + Util.assertions.testRoundtripConsistency(schema) + }) + + it("decoding", async () => { + await Util.assertions.decoding.succeed(schema, "0", 0) + await Util.assertions.decoding.succeed(schema, "-0", -0) + await Util.assertions.decoding.succeed(schema, "1", 1) + await Util.assertions.decoding.succeed(schema, "1.2", 1.2) + + await Util.assertions.decoding.succeed(schema, "NaN", NaN) + await Util.assertions.decoding.succeed(schema, "Infinity", Infinity) + await Util.assertions.decoding.succeed(schema, "-Infinity", -Infinity) + + await Util.assertions.decoding.fail( + schema, + "", + `NumberFromString +└─ Transformation process failure + └─ Unable to decode "" into a number` + ) + await Util.assertions.decoding.fail( + schema, + " ", + `NumberFromString +└─ Transformation process failure + └─ Unable to decode " " into a number` + ) + await Util.assertions.decoding.fail( + schema, + "1AB", + `NumberFromString +└─ Transformation process failure + └─ Unable to decode "1AB" into a number` + ) + await Util.assertions.decoding.fail( + schema, + "AB1", + `NumberFromString +└─ Transformation process failure + └─ Unable to decode "AB1" into a number` + ) + await Util.assertions.decoding.fail( + schema, + "a", + `NumberFromString +└─ Transformation process failure + └─ Unable to decode "a" into a number` + ) + await Util.assertions.decoding.fail( + schema, + "a1", + `NumberFromString +└─ Transformation process failure + └─ Unable to decode "a1" into a number` + ) + }) + + it("encoding", async () => { + await Util.assertions.encoding.succeed(schema, 1, "1") + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/Number/positive.test.ts b/repos/effect/packages/effect/test/Schema/Schema/Number/positive.test.ts new file mode 100644 index 0000000..e06326a --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/Number/positive.test.ts @@ -0,0 +1,27 @@ +import { describe, it } from "@effect/vitest" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("Positive", () => { + const schema = S.Positive + it("decoding", async () => { + await Util.assertions.decoding.fail( + schema, + -1, + `Positive +└─ Predicate refinement failure + └─ Expected a positive number, actual -1` + ) + await Util.assertions.decoding.fail( + schema, + 0, + `Positive +└─ Predicate refinement failure + └─ Expected a positive number, actual 0` + ) + }) + + it("encoding", async () => { + await Util.assertions.encoding.succeed(schema, 1, 1) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/Object/Object.test.ts b/repos/effect/packages/effect/test/Schema/Schema/Object/Object.test.ts new file mode 100644 index 0000000..9959365 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/Object/Object.test.ts @@ -0,0 +1,37 @@ +import { describe, it } from "@effect/vitest" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("object", () => { + const schema = S.Object + it("decoding", async () => { + await Util.assertions.decoding.succeed(schema, {}) + await Util.assertions.decoding.succeed(schema, []) + await Util.assertions.decoding.fail( + schema, + null, + `Expected object, actual null` + ) + await Util.assertions.decoding.fail( + schema, + "a", + `Expected object, actual "a"` + ) + await Util.assertions.decoding.fail( + schema, + 1, + `Expected object, actual 1` + ) + await Util.assertions.decoding.fail( + schema, + true, + `Expected object, actual true` + ) + }) + + it("encoding", async () => { + await Util.assertions.encoding.succeed(schema, {}, {}) + await Util.assertions.encoding.succeed(schema, [], []) + await Util.assertions.encoding.succeed(schema, [1, 2, 3], [1, 2, 3]) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/Option/Option.test.ts b/repos/effect/packages/effect/test/Schema/Schema/Option/Option.test.ts new file mode 100644 index 0000000..963735f --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/Option/Option.test.ts @@ -0,0 +1,22 @@ +import { describe, it } from "@effect/vitest" +import * as O from "effect/Option" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("Option", () => { + it("test roundtrip consistency", () => { + Util.assertions.testRoundtripConsistency(S.Option(S.Number)) + }) + + it("decoding", async () => { + const schema = S.Option(S.NumberFromString) + await Util.assertions.decoding.succeed(schema, JSON.parse(JSON.stringify(O.none())), O.none()) + await Util.assertions.decoding.succeed(schema, JSON.parse(JSON.stringify(O.some("1"))), O.some(1)) + }) + + it("encoding", async () => { + const schema = S.Option(S.NumberFromString) + await Util.assertions.encoding.succeed(schema, O.none(), { _tag: "None" }) + await Util.assertions.encoding.succeed(schema, O.some(1), { _tag: "Some", value: "1" }) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/Option/OptionFromNonEmptyTrimmedString.test.ts b/repos/effect/packages/effect/test/Schema/Schema/Option/OptionFromNonEmptyTrimmedString.test.ts new file mode 100644 index 0000000..3f3cdd6 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/Option/OptionFromNonEmptyTrimmedString.test.ts @@ -0,0 +1,43 @@ +import { describe, it } from "@effect/vitest" +import * as O from "effect/Option" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("OptionFromNonEmptyTrimmedString", () => { + it("test roundtrip consistency", () => { + Util.assertions.testRoundtripConsistency(S.OptionFromNonEmptyTrimmedString) + }) + + it("decoding", async () => { + const schema = S.OptionFromNonEmptyTrimmedString + await Util.assertions.decoding.succeed(schema, "", O.none()) + await Util.assertions.decoding.succeed(schema, "a", O.some("a")) + await Util.assertions.decoding.succeed(schema, " ", O.none()) + await Util.assertions.decoding.succeed(schema, " a ", O.some("a")) + + await Util.assertions.decoding.fail( + schema, + null, + `(string <-> Option) +└─ Encoded side transformation failure + └─ Expected string, actual null` + ) + }) + + it("encoding", async () => { + const schema = S.OptionFromNonEmptyTrimmedString + await Util.assertions.encoding.succeed(schema, O.none(), "") + await Util.assertions.encoding.succeed(schema, O.some("a"), "a") + + await Util.assertions.encoding.fail( + schema, + O.some(""), + `(string <-> Option) +└─ Type side transformation failure + └─ Option + └─ NonEmptyTrimmedString + └─ Predicate refinement failure + └─ Expected a non empty string, actual ""` + ) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/Option/OptionFromNullOr.test.ts b/repos/effect/packages/effect/test/Schema/Schema/Option/OptionFromNullOr.test.ts new file mode 100644 index 0000000..baecc15 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/Option/OptionFromNullOr.test.ts @@ -0,0 +1,49 @@ +import { describe, it } from "@effect/vitest" +import { assertTrue } from "@effect/vitest/utils" +import * as O from "effect/Option" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("OptionFromNullOr", () => { + it("test roundtrip consistency", () => { + Util.assertions.testRoundtripConsistency(S.OptionFromNullOr(S.Number)) + }) + + it("decoding", async () => { + const schema = S.OptionFromNullOr(S.NumberFromString) + await Util.assertions.decoding.succeed(schema, null, O.none()) + await Util.assertions.decoding.succeed(schema, "1", O.some(1)) + + assertTrue(O.isOption(S.decodeSync(schema)(null))) + assertTrue(O.isOption(S.decodeSync(schema)("1"))) + + await Util.assertions.decoding.fail( + schema, + undefined, + `(NumberFromString | null <-> Option) +└─ Encoded side transformation failure + └─ NumberFromString | null + ├─ NumberFromString + │ └─ Encoded side transformation failure + │ └─ Expected string, actual undefined + └─ Expected null, actual undefined` + ) + await Util.assertions.decoding.fail( + schema, + {}, + `(NumberFromString | null <-> Option) +└─ Encoded side transformation failure + └─ NumberFromString | null + ├─ NumberFromString + │ └─ Encoded side transformation failure + │ └─ Expected string, actual {} + └─ Expected null, actual {}` + ) + }) + + it("encoding", async () => { + const schema = S.OptionFromNullOr(S.NumberFromString) + await Util.assertions.encoding.succeed(schema, O.none(), null) + await Util.assertions.encoding.succeed(schema, O.some(1), "1") + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/Option/OptionFromNullishOr.test.ts b/repos/effect/packages/effect/test/Schema/Schema/Option/OptionFromNullishOr.test.ts new file mode 100644 index 0000000..f5e25d6 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/Option/OptionFromNullishOr.test.ts @@ -0,0 +1,48 @@ +import { describe, it } from "@effect/vitest" +import { assertTrue } from "@effect/vitest/utils" +import * as O from "effect/Option" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("OptionFromNullishOr", () => { + it("test roundtrip consistency", () => { + Util.assertions.testRoundtripConsistency(S.OptionFromNullishOr(S.Number, null)) + Util.assertions.testRoundtripConsistency(S.OptionFromNullishOr(S.Number, undefined)) + }) + + it("decoding", async () => { + const schema = S.OptionFromNullishOr(S.NumberFromString, undefined) + await Util.assertions.decoding.succeed(schema, null, O.none()) + await Util.assertions.decoding.succeed(schema, undefined, O.none()) + await Util.assertions.decoding.succeed(schema, "1", O.some(1)) + + assertTrue(O.isOption(S.decodeSync(schema)(null))) + assertTrue(O.isOption(S.decodeSync(schema)(undefined))) + assertTrue(O.isOption(S.decodeSync(schema)("1"))) + + await Util.assertions.decoding.fail( + schema, + {}, + `(NumberFromString | null | undefined <-> Option) +└─ Encoded side transformation failure + └─ NumberFromString | null | undefined + ├─ NumberFromString + │ └─ Encoded side transformation failure + │ └─ Expected string, actual {} + ├─ Expected null, actual {} + └─ Expected undefined, actual {}` + ) + }) + + it("encoding null", async () => { + const schema = S.OptionFromNullishOr(S.NumberFromString, null) + await Util.assertions.encoding.succeed(schema, O.none(), null) + await Util.assertions.encoding.succeed(schema, O.some(1), "1") + }) + + it("encoding undefined", async () => { + const schema = S.OptionFromNullishOr(S.NumberFromString, undefined) + await Util.assertions.encoding.succeed(schema, O.none(), undefined) + await Util.assertions.encoding.succeed(schema, O.some(1), "1") + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/Option/OptionFromSelf.test.ts b/repos/effect/packages/effect/test/Schema/Schema/Option/OptionFromSelf.test.ts new file mode 100644 index 0000000..9838771 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/Option/OptionFromSelf.test.ts @@ -0,0 +1,46 @@ +import { describe, it } from "@effect/vitest" +import { assertFalse, assertTrue } from "@effect/vitest/utils" +import * as O from "effect/Option" +import * as P from "effect/ParseResult" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("OptionFromSelf", () => { + it("arbitrary", () => { + Util.assertions.arbitrary.validateGeneratedValues(S.OptionFromSelf(S.Number)) + }) + + it("test roundtrip consistency", () => { + Util.assertions.testRoundtripConsistency(S.OptionFromSelf(S.NumberFromString)) + }) + + it("is", () => { + const schema = S.OptionFromSelf(S.Number) + const is = P.is(schema) + assertTrue(is(O.none())) + assertTrue(is(O.some(1))) + assertFalse(is(null)) + assertFalse(is(O.some("a"))) + + assertFalse(is({ _tag: "None" })) + assertFalse(is({ _tag: "Some", value: 1 })) + }) + + it("decoding", async () => { + const schema = S.OptionFromSelf(S.NumberFromString) + await Util.assertions.decoding.succeed(schema, O.none(), O.none()) + await Util.assertions.decoding.succeed(schema, O.some("1"), O.some(1)) + + await Util.assertions.decoding.fail( + schema, + null, + `Expected Option, actual null` + ) + }) + + it("pretty", () => { + const schema = S.OptionFromSelf(S.Number) + Util.assertions.pretty(schema, O.none(), "none()") + Util.assertions.pretty(schema, O.some(1), "some(1)") + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/Option/OptionFromUndefinedOr.test.ts b/repos/effect/packages/effect/test/Schema/Schema/Option/OptionFromUndefinedOr.test.ts new file mode 100644 index 0000000..908abce --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/Option/OptionFromUndefinedOr.test.ts @@ -0,0 +1,49 @@ +import { describe, it } from "@effect/vitest" +import { assertTrue } from "@effect/vitest/utils" +import * as O from "effect/Option" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("OptionFromUndefinedOr", () => { + it("test roundtrip consistency", () => { + Util.assertions.testRoundtripConsistency(S.OptionFromUndefinedOr(S.Number)) + }) + + it("decoding", async () => { + const schema = S.OptionFromUndefinedOr(S.NumberFromString) + await Util.assertions.decoding.succeed(schema, undefined, O.none()) + await Util.assertions.decoding.succeed(schema, "1", O.some(1)) + + assertTrue(O.isOption(S.decodeSync(schema)(undefined))) + assertTrue(O.isOption(S.decodeSync(schema)("1"))) + + await Util.assertions.decoding.fail( + schema, + null, + `(NumberFromString | undefined <-> Option) +└─ Encoded side transformation failure + └─ NumberFromString | undefined + ├─ NumberFromString + │ └─ Encoded side transformation failure + │ └─ Expected string, actual null + └─ Expected undefined, actual null` + ) + await Util.assertions.decoding.fail( + schema, + {}, + `(NumberFromString | undefined <-> Option) +└─ Encoded side transformation failure + └─ NumberFromString | undefined + ├─ NumberFromString + │ └─ Encoded side transformation failure + │ └─ Expected string, actual {} + └─ Expected undefined, actual {}` + ) + }) + + it("encoding", async () => { + const schema = S.OptionFromUndefinedOr(S.NumberFromString) + await Util.assertions.encoding.succeed(schema, O.none(), undefined) + await Util.assertions.encoding.succeed(schema, O.some(1), "1") + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/ParseOptions-errors.test.ts b/repos/effect/packages/effect/test/Schema/Schema/ParseOptions-errors.test.ts new file mode 100644 index 0000000..474154f --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/ParseOptions-errors.test.ts @@ -0,0 +1,309 @@ +import { describe, it } from "@effect/vitest" +import * as S from "effect/Schema" +import * as Util from "../TestUtils.js" + +describe("`errors` option", () => { + describe("decoding", () => { + describe("tuple", () => { + it("e r e", async () => { + const schema = S.Tuple([S.String], S.Number, S.Boolean) + await Util.assertions.decoding.fail( + schema, + [true], + `readonly [string, ...number[], boolean] +├─ [1] +│ └─ is missing +└─ [0] + └─ Expected string, actual true`, + { parseOptions: Util.ErrorsAll } + ) + }) + + it("missing element", async () => { + const schema = S.Tuple(S.String, S.Number) + await Util.assertions.decoding.fail( + schema, + [], + `readonly [string, number] +├─ [0] +│ └─ is missing +└─ [1] + └─ is missing`, + { parseOptions: Util.ErrorsAll } + ) + }) + + it("unexpected indexes", async () => { + const schema = S.Tuple() + await Util.assertions.decoding.fail( + schema, + ["a", "b"], + `readonly [] +├─ [0] +│ └─ is unexpected, expected: never +└─ [1] + └─ is unexpected, expected: never`, + { parseOptions: Util.ErrorsAll } + ) + }) + + it("wrong type for elements", async () => { + const schema = S.Tuple(S.String, S.Number) + await Util.assertions.decoding.fail( + schema, + [1, "b"], + `readonly [string, number] +├─ [0] +│ └─ Expected string, actual 1 +└─ [1] + └─ Expected number, actual "b"`, + { parseOptions: Util.ErrorsAll } + ) + }) + + it("wrong type for rest", async () => { + const schema = S.Tuple([S.String], S.Number) + await Util.assertions.decoding.fail( + schema, + ["a", "b", "c"], + `readonly [string, ...number[]] +├─ [1] +│ └─ Expected number, actual "b" +└─ [2] + └─ Expected number, actual "c"`, + { parseOptions: Util.ErrorsAll } + ) + }) + + it("wrong type for post rest elements", async () => { + const schema = S.Tuple([], S.Boolean, S.Number, S.Number) + await Util.assertions.decoding.fail( + schema, + ["a", "b"], + `readonly [...boolean[], number, number] +├─ [0] +│ └─ Expected number, actual "a" +└─ [1] + └─ Expected number, actual "b"`, + { parseOptions: Util.ErrorsAll } + ) + }) + }) + + describe("struct", () => { + it("missing keys", async () => { + const schema = S.Struct({ a: S.String, b: S.Number }) + await Util.assertions.decoding.fail( + schema, + {}, + `{ readonly a: string; readonly b: number } +├─ ["a"] +│ └─ is missing +└─ ["b"] + └─ is missing`, + { parseOptions: Util.ErrorsAll } + ) + }) + + it("wrong type for values", async () => { + const schema = S.Struct({ a: S.String, b: S.Number }) + await Util.assertions.decoding.fail( + schema, + { a: 1, b: "b" }, + `{ readonly a: string; readonly b: number } +├─ ["a"] +│ └─ Expected string, actual 1 +└─ ["b"] + └─ Expected number, actual "b"`, + { parseOptions: Util.ErrorsAll } + ) + }) + + it("unexpected keys", async () => { + const schema = S.Struct({ a: S.Number }) + await Util.assertions.decoding.fail( + schema, + { a: 1, b: "b", c: "c" }, + `{ readonly a: number } +├─ ["b"] +│ └─ is unexpected, expected: "a" +└─ ["c"] + └─ is unexpected, expected: "a"`, + { parseOptions: { ...Util.ErrorsAll, ...Util.onExcessPropertyError } } + ) + }) + }) + + describe("record", () => { + it("all key errors", async () => { + const schema = S.Record({ key: S.String.pipe(S.minLength(2)), value: S.Number }) + await Util.assertions.decoding.fail( + schema, + { a: 1, b: 2 }, + `{ readonly [x: minLength(2)]: number } +├─ ["a"] +│ └─ is unexpected, expected: minLength(2) +└─ ["b"] + └─ is unexpected, expected: minLength(2)`, + { parseOptions: { ...Util.ErrorsAll, ...Util.onExcessPropertyError } } + ) + }) + + it("all value errors", async () => { + const schema = S.Record({ key: S.String, value: S.Number }) + await Util.assertions.decoding.fail( + schema, + { a: "a", b: "b" }, + `{ readonly [x: string]: number } +├─ ["a"] +│ └─ Expected number, actual "a" +└─ ["b"] + └─ Expected number, actual "b"`, + { parseOptions: Util.ErrorsAll } + ) + }) + }) + }) + + describe("encoding", () => { + describe("tuple", () => { + it("unexpected indexes", async () => { + const schema = S.Tuple() + await Util.assertions.encoding.fail( + schema, + [1, 1] as any, + `readonly [] +├─ [0] +│ └─ is unexpected, expected: never +└─ [1] + └─ is unexpected, expected: never`, + { parseOptions: Util.ErrorsAll } + ) + }) + + it("wrong type for elements", async () => { + const schema = S.Tuple(Util.NumberFromChar, Util.NumberFromChar) + await Util.assertions.encoding.fail( + schema, + [10, 10], + `readonly [NumberFromChar, NumberFromChar] +├─ [0] +│ └─ NumberFromChar +│ └─ Encoded side transformation failure +│ └─ Char +│ └─ Predicate refinement failure +│ └─ Expected a single character, actual "10" +└─ [1] + └─ NumberFromChar + └─ Encoded side transformation failure + └─ Char + └─ Predicate refinement failure + └─ Expected a single character, actual "10"`, + { parseOptions: Util.ErrorsAll } + ) + }) + + it("wrong type for rest", async () => { + const schema = S.Array(Util.NumberFromChar) + await Util.assertions.encoding.fail( + schema, + [10, 10], + `ReadonlyArray +├─ [0] +│ └─ NumberFromChar +│ └─ Encoded side transformation failure +│ └─ Char +│ └─ Predicate refinement failure +│ └─ Expected a single character, actual "10" +└─ [1] + └─ NumberFromChar + └─ Encoded side transformation failure + └─ Char + └─ Predicate refinement failure + └─ Expected a single character, actual "10"`, + { parseOptions: Util.ErrorsAll } + ) + }) + + it("wrong type for values post rest elements", async () => { + const schema = S.Tuple([], S.String, Util.NumberFromChar, Util.NumberFromChar) + await Util.assertions.encoding.fail( + schema, + [10, 10], + `readonly [...string[], NumberFromChar, NumberFromChar] +├─ [0] +│ └─ NumberFromChar +│ └─ Encoded side transformation failure +│ └─ Char +│ └─ Predicate refinement failure +│ └─ Expected a single character, actual "10" +└─ [1] + └─ NumberFromChar + └─ Encoded side transformation failure + └─ Char + └─ Predicate refinement failure + └─ Expected a single character, actual "10"`, + { parseOptions: Util.ErrorsAll } + ) + }) + }) + + describe("struct", () => { + it("wrong type for values", async () => { + const schema = S.Struct({ a: Util.NumberFromChar, b: Util.NumberFromChar }) + await Util.assertions.encoding.fail( + schema, + { a: 10, b: 10 }, + `{ readonly a: NumberFromChar; readonly b: NumberFromChar } +├─ ["a"] +│ └─ NumberFromChar +│ └─ Encoded side transformation failure +│ └─ Char +│ └─ Predicate refinement failure +│ └─ Expected a single character, actual "10" +└─ ["b"] + └─ NumberFromChar + └─ Encoded side transformation failure + └─ Char + └─ Predicate refinement failure + └─ Expected a single character, actual "10"`, + { parseOptions: Util.ErrorsAll } + ) + }) + }) + + describe("record", () => { + it("all key errors", async () => { + const schema = S.Record({ key: S.Char, value: S.String }) + await Util.assertions.encoding.fail( + schema, + { aa: "a", bb: "bb" }, + `{ readonly [x: Char]: string } +├─ ["aa"] +│ └─ is unexpected, expected: Char +└─ ["bb"] + └─ is unexpected, expected: Char`, + { parseOptions: { ...Util.ErrorsAll, ...Util.onExcessPropertyError } } + ) + }) + + it("all value errors", async () => { + const schema = S.Record({ key: S.String, value: S.Char }) + await Util.assertions.encoding.fail( + schema, + { a: "aa", b: "bb" }, + `{ readonly [x: string]: Char } +├─ ["a"] +│ └─ Char +│ └─ Predicate refinement failure +│ └─ Expected a single character, actual "aa" +└─ ["b"] + └─ Char + └─ Predicate refinement failure + └─ Expected a single character, actual "bb"`, + { parseOptions: Util.ErrorsAll } + ) + }) + }) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/ParseOptions-exact.test.ts b/repos/effect/packages/effect/test/Schema/Schema/ParseOptions-exact.test.ts new file mode 100644 index 0000000..f4273ed --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/ParseOptions-exact.test.ts @@ -0,0 +1,65 @@ +import { describe, it } from "@effect/vitest" +import { assertFalse, assertTrue } from "@effect/vitest/utils" +import * as S from "effect/Schema" +import * as Util from "../TestUtils.js" + +describe("`exact` option", () => { + describe("decoding", () => { + it("false (default)", async () => { + const schema = S.Struct({ a: S.Unknown }) + await Util.assertions.decoding.succeed(schema, {}, { a: undefined }) + }) + + it("true", async () => { + const schema = S.Struct({ a: S.Unknown, b: S.Unknown }) + await Util.assertions.decoding.fail( + schema, + {}, + `{ readonly a: unknown; readonly b: unknown } +└─ ["a"] + └─ is missing`, + { parseOptions: { exact: true } } + ) + await Util.assertions.decoding.fail( + schema, + {}, + `{ readonly a: unknown; readonly b: unknown } +├─ ["a"] +│ └─ is missing +└─ ["b"] + └─ is missing`, + { parseOptions: { exact: true, errors: "all" } } + ) + }) + }) + + describe("is", () => { + it("true (default)", async () => { + const schema = S.Struct({ a: S.Unknown }) + assertFalse(S.is(schema)({})) + }) + + it("false", async () => { + const schema = S.Struct({ a: S.Unknown }) + assertTrue(S.is(schema)({}, { exact: false })) + }) + }) + + describe("asserts", () => { + it("true (default)", async () => { + const schema = S.Struct({ a: S.Unknown }) + Util.assertions.asserts.fail( + schema, + {}, + `{ readonly a: unknown } +└─ ["a"] + └─ is missing` + ) + }) + + it("false", async () => { + const schema = S.Struct({ a: S.Unknown }) + Util.assertions.asserts.succeed(schema, {}, { parseOptions: { exact: false } }) + }) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/ParseOptions-onExcessProperty.test.ts b/repos/effect/packages/effect/test/Schema/Schema/ParseOptions-onExcessProperty.test.ts new file mode 100644 index 0000000..4a64ab1 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/ParseOptions-onExcessProperty.test.ts @@ -0,0 +1,235 @@ +import { describe, it } from "@effect/vitest" +import { assertLeft } from "@effect/vitest/utils" +import * as ParseResult from "effect/ParseResult" +import * as S from "effect/Schema" +import * as Util from "../TestUtils.js" + +describe("`onExcessProperty` option", () => { + describe("`ignore` option", () => { + it("should not change tuple behaviour", async () => { + const schema = S.Tuple(S.Number) + await Util.assertions.decoding.fail( + schema, + [1, "b"], + `readonly [number] +└─ [1] + └─ is unexpected, expected: 0` + ) + await Util.assertions.encoding.fail( + schema, + [1, "b"] as any, + `readonly [number] +└─ [1] + └─ is unexpected, expected: 0` + ) + }) + + it("tuple of a struct", async () => { + const schema = S.Tuple(S.Struct({ b: S.Number })) + await Util.assertions.decoding.succeed( + schema, + [{ b: 1, c: "c" }], + [{ b: 1 }] + ) + }) + + it("tuple rest element of a struct", async () => { + const schema = S.Array(S.Struct({ b: S.Number })) + await Util.assertions.decoding.succeed( + schema, + [{ b: 1, c: "c" }], + [{ b: 1 }] + ) + }) + + it("tuple. post rest elements of a struct", async () => { + const schema = S.Tuple([], S.String, S.Struct({ b: S.Number })) + await Util.assertions.decoding.succeed(schema, [{ b: 1 }]) + await Util.assertions.decoding.succeed( + schema, + [{ b: 1, c: "c" }], + [{ b: 1 }] + ) + }) + + it("struct excess property signatures", async () => { + const schema = S.Struct({ a: S.Number }) + await Util.assertions.decoding.succeed( + schema, + { a: 1, b: "b" }, + { a: 1 } + ) + }) + + it("struct nested struct", async () => { + const schema = S.Struct({ a: S.Struct({ b: S.Number }) }) + await Util.assertions.decoding.succeed( + schema, + { a: { b: 1, c: "c" } }, + { + a: { b: 1 } + } + ) + }) + + it("record of struct", async () => { + const schema = S.Record({ key: S.String, value: S.Struct({ b: S.Number }) }) + await Util.assertions.decoding.succeed( + schema, + { a: { b: 1, c: "c" } }, + { a: { b: 1 } } + ) + }) + }) + + describe("`error` option", () => { + describe("should register the actual value", () => { + it("struct", () => { + const schema = S.Struct({ a: S.String }) + const input = { a: "a", b: 1 } + const e = ParseResult.decodeUnknownEither(schema)(input, Util.onExcessPropertyError) + assertLeft( + e, + new ParseResult.Composite( + schema.ast, + input, + new ParseResult.Pointer("b", input, new ParseResult.Unexpected(1, `is unexpected, expected: "a"`)), + {} + ) + ) + }) + + it("tuple", () => { + const schema = S.Tuple(S.String) + const input = ["a", 1] + const e = ParseResult.decodeUnknownEither(schema)(input, Util.onExcessPropertyError) + assertLeft( + e, + new ParseResult.Composite( + schema.ast, + input, + new ParseResult.Pointer(1, input, new ParseResult.Unexpected(1, `is unexpected, expected: 0`)), + [] + ) + ) + }) + }) + + it("structs", async () => { + const a = S.Struct({ + a: S.optionalWith(S.Number, { exact: true }), + b: S.optionalWith(S.String, { exact: true }) + }) + const b = S.Struct({ a: S.optionalWith(S.Number, { exact: true }) }) + const schema = S.Union(a, b) + await Util.assertions.decoding.fail( + schema, + { a: 1, b: "b", c: true }, + `{ readonly a?: number; readonly b?: string } | { readonly a?: number } +├─ { readonly a?: number; readonly b?: string } +│ └─ ["c"] +│ └─ is unexpected, expected: "a" | "b" +└─ { readonly a?: number } + └─ ["b"] + └─ is unexpected, expected: "a"`, + { parseOptions: Util.onExcessPropertyError } + ) + }) + + it("tuples", async () => { + const a = S.Tuple(S.Number, S.optionalElement(S.String)) + const b = S.Tuple(S.Number) + const schema = S.Union(a, b) + await Util.assertions.decoding.fail( + schema, + [1, "b", true], + `readonly [number, string?] | readonly [number] +├─ readonly [number, string?] +│ └─ [2] +│ └─ is unexpected, expected: 0 | 1 +└─ readonly [number] + └─ [1] + └─ is unexpected, expected: 0` + ) + await Util.assertions.decoding.fail( + schema, + [1, "b", true], + `readonly [number, string?] | readonly [number] +├─ readonly [number, string?] +│ └─ [2] +│ └─ is unexpected, expected: 0 | 1 +└─ readonly [number] + └─ [1] + └─ is unexpected, expected: 0`, + { parseOptions: Util.onExcessPropertyError } + ) + }) + }) + + describe("`preserve` option", () => { + it("should not change tuple behaviour", async () => { + const schema = S.Tuple(S.Number) + await Util.assertions.decoding.fail( + schema, + [1, "b"], + `readonly [number] +└─ [1] + └─ is unexpected, expected: 0`, + { parseOptions: Util.onExcessPropertyPreserve } + ) + await Util.assertions.encoding.fail( + schema, + [1, "b"] as any, + `readonly [number] +└─ [1] + └─ is unexpected, expected: 0`, + { parseOptions: Util.onExcessPropertyPreserve } + ) + }) + + it("struct with string excess keys", async () => { + const c = Symbol.for("effect/Schema/test/c") + const schema = S.Struct({ a: S.String }) + const input = { a: "a", b: 1, [c]: true } + await Util.assertions.decoding.succeed(schema, input, input, { + parseOptions: Util.onExcessPropertyPreserve + }) + }) + + it("struct with symbol excess keys", async () => { + const c = Symbol.for("effect/Schema/test/c") + const schema = S.Struct({ [c]: S.Boolean }) + const input = { a: "a", b: 1, [c]: true } + await Util.assertions.decoding.succeed(schema, input, input, { + parseOptions: Util.onExcessPropertyPreserve + }) + }) + + it("struct with both string and symbol excess keys", async () => { + const c = Symbol.for("effect/Schema/test/c") + const schema = S.Struct({ a: S.String, [c]: S.Boolean }) + const input = { a: "a", b: 1, [c]: true } + await Util.assertions.decoding.succeed(schema, input, input, { + parseOptions: Util.onExcessPropertyPreserve + }) + }) + + it("record(string, string)", async () => { + const c = Symbol.for("effect/Schema/test/c") + const schema = S.Struct({ a: S.String }) + const input = { a: "a", [c]: true } + await Util.assertions.decoding.succeed(schema, input, input, { + parseOptions: Util.onExcessPropertyPreserve + }) + }) + + it("record(symbol, boolean)", async () => { + const c = Symbol.for("effect/Schema/test/c") + const schema = S.Struct({ [c]: S.Boolean }) + const input = { a: "a", [c]: true } + await Util.assertions.decoding.succeed(schema, input, input, { + parseOptions: Util.onExcessPropertyPreserve + }) + }) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/ParseOptions-preserveKeyOrder.test.ts b/repos/effect/packages/effect/test/Schema/Schema/ParseOptions-preserveKeyOrder.test.ts new file mode 100644 index 0000000..86d0029 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/ParseOptions-preserveKeyOrder.test.ts @@ -0,0 +1,74 @@ +import { describe, it } from "@effect/vitest" +import { deepStrictEqual } from "@effect/vitest/utils" +import type { Duration } from "effect" +import * as Effect from "effect/Effect" +import * as ParseResult from "effect/ParseResult" +import * as S from "effect/Schema" + +describe("`preserveKeyOrder` option", () => { + const b = Symbol.for("effect/Schema/test/b") + const Sync = S.Struct({ + a: S.Literal("a"), + [b]: S.Array(S.String), + c: S.Record({ key: S.String, value: S.Number }), + d: S.NumberFromString, + e: S.Boolean, + f: S.optional(S.String), + g: S.optional(S.String) + }) + + const effectify = (duration: Duration.DurationInput) => + S.NumberFromString.pipe( + S.transformOrFail(S.Number, { + strict: true, + decode: (x) => Effect.sleep(duration).pipe(Effect.andThen(ParseResult.succeed(x))), + encode: ParseResult.succeed + }) + ) + + const Async = S.Struct({ + a: effectify("20 millis"), + [b]: effectify("30 millis"), + c: effectify("10 millis") + }).annotations({ concurrency: 3 }) + + describe("decoding", () => { + it("should preserve the order of input properties (sync)", () => { + const input = { [b]: ["b"], c: { c: 1 }, d: "1", e: true, a: "a", other: 1, f: undefined } + const output = S.decodeUnknownSync(Sync)(input, { propertyOrder: "original", onExcessProperty: "preserve" }) + const expectedOutput = { [b]: ["b"], c: { c: 1 }, d: 1, e: true, a: "a", other: 1, f: undefined } as const + deepStrictEqual(output, expectedOutput) + deepStrictEqual(Reflect.ownKeys(output), Reflect.ownKeys(expectedOutput)) + }) + + it("should preserve the order of input properties (async)", async () => { + const input = { a: "1", c: "3", [b]: "2", other: 1 } + const output = await Effect.runPromise( + S.decodeUnknown(Async)(input, { propertyOrder: "original", onExcessProperty: "preserve" }) + ) + const expectedOutput = { a: 1, c: 3, [b]: 2, other: 1 } + deepStrictEqual(output, expectedOutput) + deepStrictEqual(Reflect.ownKeys(output), Reflect.ownKeys(expectedOutput)) + }) + }) + + describe("encoding", () => { + it("should preserve the order of input properties (sync)", () => { + const input = { [b]: ["b"], c: { c: 1 }, d: 1, e: true, a: "a", other: 1, f: undefined } + const output = S.encodeUnknownSync(Sync)(input, { propertyOrder: "original", onExcessProperty: "preserve" }) + const expectedOutput = { [b]: ["b"], c: { c: 1 }, d: "1", e: true, a: "a", other: 1, f: undefined } as const + deepStrictEqual(output, expectedOutput) + deepStrictEqual(Reflect.ownKeys(output), Reflect.ownKeys(expectedOutput)) + }) + + it("should preserve the order of input properties (async)", async () => { + const input = { a: 1, c: 3, [b]: 2, other: 1 } + const output = await Effect.runPromise( + S.encodeUnknown(Async)(input, { propertyOrder: "original", onExcessProperty: "preserve" }) + ) + const expectedOutput = { a: "1", c: "3", [b]: "2", other: 1 } + deepStrictEqual(output, expectedOutput) + deepStrictEqual(Reflect.ownKeys(output), Reflect.ownKeys(expectedOutput)) + }) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/ParseOptionsAnnotation.test.ts b/repos/effect/packages/effect/test/Schema/Schema/ParseOptionsAnnotation.test.ts new file mode 100644 index 0000000..4f399c2 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/ParseOptionsAnnotation.test.ts @@ -0,0 +1,53 @@ +import { describe, it } from "@effect/vitest" +import * as S from "effect/Schema" +import * as Util from "../TestUtils.js" + +const String = S.transform(S.NonEmptyString, S.String, { strict: true, decode: (s) => s, encode: (s) => s }) + .annotations({ + identifier: "string" + }) + +describe("ParseOptionsAnnotation", () => { + it("nested structs", async () => { + const schema = S.Struct({ + a: S.Struct({ + b: String, + c: String + }).annotations({ parseOptions: { errors: "first" } }), + d: String + }).annotations({ parseOptions: { errors: "all" } }) + await Util.assertions.decoding.fail( + schema, + { a: {} }, + `{ readonly a: { readonly b: string; readonly c: string }; readonly d: string } +├─ ["a"] +│ └─ { readonly b: string; readonly c: string } +│ └─ ["b"] +│ └─ is missing +└─ ["d"] + └─ is missing`, + { parseOptions: { errors: "first" } } + ) + + await Util.assertions.encoding.fail( + schema, + { a: { b: "", c: "" }, d: "" }, + `{ readonly a: { readonly b: string; readonly c: string }; readonly d: string } +├─ ["a"] +│ └─ { readonly b: string; readonly c: string } +│ └─ ["b"] +│ └─ string +│ └─ Encoded side transformation failure +│ └─ NonEmptyString +│ └─ Predicate refinement failure +│ └─ Expected a non empty string, actual "" +└─ ["d"] + └─ string + └─ Encoded side transformation failure + └─ NonEmptyString + └─ Predicate refinement failure + └─ Expected a non empty string, actual ""`, + { parseOptions: { errors: "first" } } + ) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/PropertyKey/PropertyKey.test.ts b/repos/effect/packages/effect/test/Schema/Schema/PropertyKey/PropertyKey.test.ts new file mode 100644 index 0000000..1a8ca21 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/PropertyKey/PropertyKey.test.ts @@ -0,0 +1,17 @@ +import { describe, it } from "@effect/vitest" +import { strictEqual } from "@effect/vitest/utils" +import * as Schema from "effect/Schema" + +describe("PropertyKey", () => { + it("should handle symbol, string, and number", () => { + const encodeSync = Schema.encodeSync(Schema.PropertyKey) + const decodeSync = Schema.decodeSync(Schema.PropertyKey) + const expectRoundtrip = (pk: PropertyKey) => { + strictEqual(decodeSync(encodeSync(pk)), pk) + } + + expectRoundtrip("path") + expectRoundtrip(1) + expectRoundtrip(Symbol.for("symbol")) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/PropertySignature.test.ts b/repos/effect/packages/effect/test/Schema/Schema/PropertySignature.test.ts new file mode 100644 index 0000000..e594d8d --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/PropertySignature.test.ts @@ -0,0 +1,222 @@ +import { describe, it } from "@effect/vitest" +import { deepStrictEqual, strictEqual } from "@effect/vitest/utils" +import { identity } from "effect/Function" +import * as Option from "effect/Option" +import * as S from "effect/Schema" +import * as AST from "effect/SchemaAST" +import * as Util from "../TestUtils.js" + +describe("PropertySignature", () => { + it("should expose a from property", () => { + const schema = S.propertySignature(S.String) + strictEqual(schema.from, S.String) + }) + + it("should expose a from property after an annotations call", () => { + const schema = S.propertySignature(S.String).annotations({}) + strictEqual(schema.from, S.String) + }) + + it("toString", () => { + strictEqual( + String(S.optional(S.String)), + `PropertySignature<"?:", string | undefined, never, "?:", string | undefined>` + ) + strictEqual( + String(S.optional(S.String).pipe(S.fromKey("a"))), + `PropertySignature<"?:", string | undefined, "a", "?:", string | undefined>` + ) + }) + + describe("annotations", () => { + it("propertySignature(S.string)", () => { + const schema = S.Struct({ + a: S.propertySignature(S.String).annotations({ description: "a description" }).annotations({ title: "a title" }) + }) + const ast = schema.ast as AST.TypeLiteral + deepStrictEqual(ast.propertySignatures[0].annotations, { + [AST.DescriptionAnnotationId]: "a description", + [AST.TitleAnnotationId]: "a title" + }) + }) + + it("propertySignature(S.NumberFromString)", () => { + const schema = S.Struct({ + a: S.propertySignature(S.NumberFromString).annotations({ description: "a description" }).annotations({ + title: "a title" + }) + }) + const ast = schema.ast as AST.TypeLiteral + deepStrictEqual(ast.propertySignatures[0].annotations, { + [AST.DescriptionAnnotationId]: "a description", + [AST.TitleAnnotationId]: "a title" + }) + }) + + it("optional(S.string)", () => { + const schema = S.Struct({ + a: S.optional(S.String).annotations({ description: "a description" }).annotations({ title: "a title" }) + }) + const ast = schema.ast as AST.TypeLiteral + deepStrictEqual(ast.propertySignatures[0].annotations, { + [AST.DescriptionAnnotationId]: "a description", + [AST.TitleAnnotationId]: "a title" + }) + }) + + it("optional(S.NumberFromString)", () => { + const schema = S.Struct({ + a: S.optional(S.NumberFromString).annotations({ description: "a description" }).annotations({ + title: "a title" + }) + }) + const ast = schema.ast as AST.TypeLiteral + deepStrictEqual(ast.propertySignatures[0].annotations, { + [AST.DescriptionAnnotationId]: "a description", + [AST.TitleAnnotationId]: "a title" + }) + }) + + it("optionalWith(S.string, { default })", () => { + const schema = S.Struct({ + a: S.optionalWith(S.String, { default: () => "" }).annotations({ description: "a description" }).annotations({ + title: "a title" + }) + }) + const ast = schema.ast as AST.Transformation + const to = ast.to as AST.TypeLiteral + deepStrictEqual(to.propertySignatures[0].annotations, { + [AST.DescriptionAnnotationId]: "a description", + [AST.TitleAnnotationId]: "a title" + }) + }) + + it("optionalWith(S.NumberFromString, { default })", () => { + const schema = S.Struct({ + a: S.optionalWith(S.NumberFromString, { default: () => 0 }).annotations({ description: "a description" }) + .annotations({ title: "a title" }) + }) + const ast = schema.ast as AST.Transformation + const to = ast.to as AST.TypeLiteral + deepStrictEqual(to.propertySignatures[0].annotations, { + [AST.DescriptionAnnotationId]: "a description", + [AST.TitleAnnotationId]: "a title" + }) + }) + }) + + it("add a decoding default to an optional field", async () => { + const ps: S.PropertySignature<":", number, never, "?:", string, never> = S.makePropertySignature( + new S.PropertySignatureTransformation( + new S.FromPropertySignature(S.NumberFromString.ast, true, true, {}, undefined), + new S.ToPropertySignature(S.Number.ast, false, true, {}, undefined), + Option.orElse(() => Option.some(0)), + identity + ) + ) + const transform = S.Struct({ a: ps }) + const schema = S.asSchema(transform) + await Util.assertions.decoding.succeed(schema, {}, { a: 0 }) + await Util.assertions.decoding.succeed(schema, { a: "1" }, { a: 1 }) + await Util.assertions.decoding.fail( + schema, + { a: "a" }, + `(Struct (Encoded side) <-> Struct (Type side)) +└─ Encoded side transformation failure + └─ Struct (Encoded side) + └─ ["a"] + └─ NumberFromString + └─ Transformation process failure + └─ Unable to decode "a" into a number` + ) + + await Util.assertions.encoding.succeed(schema, { a: 1 }, { a: "1" }) + await Util.assertions.encoding.succeed(schema, { a: 0 }, { a: "0" }) + }) + + it("add a bidirectional (decoding/encoding) default to an optional field", async () => { + const ps: S.PropertySignature<":", number, never, "?:", string, never> = S.makePropertySignature( + new S.PropertySignatureTransformation( + new S.FromPropertySignature(S.NumberFromString.ast, true, true, {}, undefined), + new S.ToPropertySignature(S.Number.ast, false, true, {}, undefined), + Option.orElse(() => Option.some(0)), + (o) => Option.flatMap(o, Option.liftPredicate((v) => v !== 0)) + ) + ) + const transform = S.Struct({ a: ps }) + const schema = S.asSchema(transform) + await Util.assertions.decoding.succeed(schema, {}, { a: 0 }) + await Util.assertions.decoding.succeed(schema, { a: "1" }, { a: 1 }) + await Util.assertions.decoding.fail( + schema, + { a: "a" }, + `(Struct (Encoded side) <-> Struct (Type side)) +└─ Encoded side transformation failure + └─ Struct (Encoded side) + └─ ["a"] + └─ NumberFromString + └─ Transformation process failure + └─ Unable to decode "a" into a number` + ) + + await Util.assertions.encoding.succeed(schema, { a: 1 }, { a: "1" }) + await Util.assertions.encoding.succeed(schema, { a: 0 }, {}) + }) + + it("empty string as optional", async () => { + const ps: S.PropertySignature<"?:", string, never, ":", string, never> = S.makePropertySignature( + new S.PropertySignatureTransformation( + new S.FromPropertySignature(S.String.ast, false, true, {}, undefined), + new S.ToPropertySignature(S.String.ast, true, true, {}, undefined), + Option.flatMap(Option.liftPredicate((v) => v !== "")), + identity + ) + ) + const transform = S.Struct({ a: ps }) + const schema = S.asSchema(transform) + await Util.assertions.decoding.succeed(schema, { a: "" }, {}) + await Util.assertions.decoding.succeed(schema, { a: "a" }, { a: "a" }) + + await Util.assertions.encoding.succeed(schema, { a: "a" }, { a: "a" }) + }) + + it("encoding default", async () => { + const ps: S.PropertySignature<"?:", number, never, ":", number, never> = S.makePropertySignature( + new S.PropertySignatureTransformation( + new S.FromPropertySignature(S.Number.ast, false, true, {}, undefined), + new S.ToPropertySignature(S.Number.ast, true, true, {}, undefined), + identity, + Option.orElse(() => Option.some(0)) + ) + ) + const transform = S.Struct({ a: ps }) + const schema = S.asSchema(transform) + await Util.assertions.decoding.succeed(schema, { a: 1 }, { a: 1 }) + await Util.assertions.decoding.succeed(schema, { a: 0 }, { a: 0 }) + + await Util.assertions.encoding.succeed(schema, {}, { a: 0 }) + await Util.assertions.encoding.succeed(schema, { a: 1 }, { a: 1 }) + }) + + describe("fromKey", () => { + it("string key", async () => { + const ps = S.propertySignature(S.Number).pipe(S.fromKey("b")) + const transform = S.Struct({ a: ps }) + const schema = S.asSchema(transform) + await Util.assertions.decoding.succeed(schema, { b: 1 }, { a: 1 }, { parseOptions: Util.onExcessPropertyError }) + + await Util.assertions.encoding.succeed(schema, { a: 1 }, { b: 1 }, { parseOptions: Util.onExcessPropertyError }) + }) + + it("symbol key", async () => { + const a = Symbol.for("effect/Schema/test/a") + const ps = S.propertySignature(S.Symbol).pipe(S.fromKey(a)) + const transform = S.Struct({ a: ps }) + const rename = S.asSchema(transform) + const schema = S.Struct({ b: S.Number }).pipe(S.extend(rename)) + + await Util.assertions.decoding.succeed(schema, { [a]: "effect/Schema/test/a", b: 1 }, { a, b: 1 }) + await Util.assertions.encoding.succeed(schema, { a, b: 1 }, { [a]: "effect/Schema/test/a", b: 1 }) + }) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/ReadonlyMap/ReadonlyMap.test.ts b/repos/effect/packages/effect/test/Schema/Schema/ReadonlyMap/ReadonlyMap.test.ts new file mode 100644 index 0000000..7a9deb6 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/ReadonlyMap/ReadonlyMap.test.ts @@ -0,0 +1,47 @@ +import { describe, it } from "@effect/vitest" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("ReadonlyMap", () => { + it("test roundtrip consistency", () => { + Util.assertions.testRoundtripConsistency(S.ReadonlyMap({ key: S.Number, value: S.String })) + }) + + it("decoding", async () => { + const schema = S.ReadonlyMap({ key: S.Number, value: S.String }) + await Util.assertions.decoding.succeed(schema, [], new Map()) + await Util.assertions.decoding.succeed( + schema, + [[1, "a"], [2, "b"], [3, "c"]], + new Map([[1, "a"], [2, "b"], [3, "c"]]) + ) + + await Util.assertions.decoding.fail( + schema, + null, + `(ReadonlyArray <-> ReadonlyMap) +└─ Encoded side transformation failure + └─ Expected ReadonlyArray, actual null` + ) + await Util.assertions.decoding.fail( + schema, + [[1, "a"], [2, 1]], + `(ReadonlyArray <-> ReadonlyMap) +└─ Encoded side transformation failure + └─ ReadonlyArray + └─ [1] + └─ readonly [number, string] + └─ [1] + └─ Expected string, actual 1` + ) + }) + + it("encoding", async () => { + const schema = S.ReadonlyMap({ key: S.Number, value: S.String }) + await Util.assertions.encoding.succeed(schema, new Map(), []) + await Util.assertions.encoding.succeed(schema, new Map([[1, "a"], [2, "b"], [3, "c"]]), [[1, "a"], [ + 2, + "b" + ], [3, "c"]]) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/ReadonlyMap/ReadonlyMapFromRecord.test.ts b/repos/effect/packages/effect/test/Schema/Schema/ReadonlyMap/ReadonlyMapFromRecord.test.ts new file mode 100644 index 0000000..be7faf3 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/ReadonlyMap/ReadonlyMapFromRecord.test.ts @@ -0,0 +1,54 @@ +import { describe, it } from "@effect/vitest" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("ReadonlyMapFromRecord", () => { + it("decoding", async () => { + const schema = S.ReadonlyMapFromRecord({ key: S.NumberFromString, value: S.NumberFromString }) + await Util.assertions.decoding.succeed(schema, {}, new Map()) + await Util.assertions.decoding.succeed( + schema, + { 1: "2", 3: "4", 5: "6" }, + new Map([[1, 2], [3, 4], [5, 6]]) + ) + + await Util.assertions.decoding.fail( + schema, + null, + `(a record to be decoded into a ReadonlyMap <-> ReadonlyMap) +└─ Encoded side transformation failure + └─ Expected a record to be decoded into a ReadonlyMap, actual null` + ) + await Util.assertions.decoding.fail( + schema, + { a: "1" }, + `(a record to be decoded into a ReadonlyMap <-> ReadonlyMap) +└─ Type side transformation failure + └─ ReadonlyMap + └─ ReadonlyArray + └─ [0] + └─ readonly [NumberFromString, number] + └─ [0] + └─ NumberFromString + └─ Transformation process failure + └─ Unable to decode "a" into a number` + ) + await Util.assertions.decoding.fail( + schema, + { 1: "a" }, + `(a record to be decoded into a ReadonlyMap <-> ReadonlyMap) +└─ Encoded side transformation failure + └─ a record to be decoded into a ReadonlyMap + └─ ["1"] + └─ NumberFromString + └─ Transformation process failure + └─ Unable to decode "a" into a number` + ) + }) + + it("encoding", async () => { + const schema = S.ReadonlyMapFromRecord({ key: S.NumberFromString, value: S.NumberFromString }) + await Util.assertions.encoding.succeed(schema, new Map(), {}) + await Util.assertions.encoding.succeed(schema, new Map([[1, 2], [3, 4], [5, 6]]), { 1: "2", 3: "4", 5: "6" }) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/ReadonlyMap/ReadonlyMapFromSelf.test.ts b/repos/effect/packages/effect/test/Schema/Schema/ReadonlyMap/ReadonlyMapFromSelf.test.ts new file mode 100644 index 0000000..c40c64e --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/ReadonlyMap/ReadonlyMapFromSelf.test.ts @@ -0,0 +1,70 @@ +import { describe, it } from "@effect/vitest" +import { assertFalse, assertTrue } from "@effect/vitest/utils" +import * as P from "effect/ParseResult" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("ReadonlyMapFromSelf", () => { + it("test roundtrip consistency", () => { + Util.assertions.testRoundtripConsistency(S.ReadonlyMapFromSelf({ key: S.Number, value: S.String })) + }) + + it("decoding", async () => { + const schema = S.ReadonlyMapFromSelf({ key: S.NumberFromString, value: S.String }) + await Util.assertions.decoding.succeed(schema, new Map(), new Map()) + await Util.assertions.decoding.succeed( + schema, + new Map([["1", "a"], ["2", "b"], ["3", "c"]]), + new Map([[1, "a"], [2, "b"], [3, "c"]]) + ) + + await Util.assertions.decoding.fail( + schema, + null, + `Expected ReadonlyMap, actual null` + ) + await Util.assertions.decoding.fail( + schema, + new Map([["1", "a"], ["a", "b"]]), + `ReadonlyMap +└─ ReadonlyArray + └─ [1] + └─ readonly [NumberFromString, string] + └─ [0] + └─ NumberFromString + └─ Transformation process failure + └─ Unable to decode "a" into a number` + ) + }) + + it("encoding", async () => { + const schema = S.ReadonlyMapFromSelf({ key: S.NumberFromString, value: S.String }) + await Util.assertions.encoding.succeed(schema, new Map(), new Map()) + await Util.assertions.encoding.succeed( + schema, + new Map([[1, "a"], [2, "b"], [3, "c"]]), + new Map([["1", "a"], ["2", "b"], ["3", "c"]]) + ) + }) + + it("is", () => { + const schema = S.ReadonlyMapFromSelf({ key: S.Number, value: S.String }) + const is = P.is(schema) + assertTrue(is(new Map())) + assertTrue(is(new Map([[1, "a"], [2, "b"], [3, "c"]]))) + + assertFalse(is(null)) + assertFalse(is(undefined)) + assertFalse(is(new Map([[1, "a"], [2, 1]]))) + assertFalse(is(new Map([[1, 1], [2, "b"]]))) + assertFalse(is(new Map([[1, 1], [2, 2]]))) + assertFalse(is(new Map([["a", 1], ["b", 2], [3, 1]]))) + assertFalse(is(new Map([[1, "a"], [2, "b"], [3, 1]]))) + }) + + it("pretty", () => { + const schema = S.ReadonlyMapFromSelf({ key: S.Number, value: S.String }) + Util.assertions.pretty(schema, new Map(), "new Map([])") + Util.assertions.pretty(schema, new Map([[1, "a"], [2, "b"]]), `new Map([[1, "a"], [2, "b"]])`) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/ReadonlySet/ReadonlySet.test.ts b/repos/effect/packages/effect/test/Schema/Schema/ReadonlySet/ReadonlySet.test.ts new file mode 100644 index 0000000..df79b7d --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/ReadonlySet/ReadonlySet.test.ts @@ -0,0 +1,38 @@ +import { describe, it } from "@effect/vitest" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("ReadonlySet", () => { + it("test roundtrip consistency", () => { + Util.assertions.testRoundtripConsistency(S.ReadonlySet(S.Number)) + }) + + it("decoding", async () => { + const schema = S.ReadonlySet(S.Number) + await Util.assertions.decoding.succeed(schema, [], new Set([])) + await Util.assertions.decoding.succeed(schema, [1, 2, 3], new Set([1, 2, 3])) + + await Util.assertions.decoding.fail( + schema, + null, + `(ReadonlyArray <-> ReadonlySet) +└─ Encoded side transformation failure + └─ Expected ReadonlyArray, actual null` + ) + await Util.assertions.decoding.fail( + schema, + [1, "a"], + `(ReadonlyArray <-> ReadonlySet) +└─ Encoded side transformation failure + └─ ReadonlyArray + └─ [1] + └─ Expected number, actual "a"` + ) + }) + + it("encoding", async () => { + const schema = S.ReadonlySet(S.Number) + await Util.assertions.encoding.succeed(schema, new Set(), []) + await Util.assertions.encoding.succeed(schema, new Set([1, 2, 3]), [1, 2, 3]) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/ReadonlySet/ReadonlySetFromSelf.test.ts b/repos/effect/packages/effect/test/Schema/Schema/ReadonlySet/ReadonlySetFromSelf.test.ts new file mode 100644 index 0000000..1cb9fa6 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/ReadonlySet/ReadonlySetFromSelf.test.ts @@ -0,0 +1,56 @@ +import { describe, it } from "@effect/vitest" +import { assertFalse, assertTrue } from "@effect/vitest/utils" +import * as P from "effect/ParseResult" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("ReadonlySetFromSelf", () => { + it("test roundtrip consistency", () => { + Util.assertions.testRoundtripConsistency(S.ReadonlySetFromSelf(S.Number)) + }) + + it("decoding", async () => { + const schema = S.ReadonlySetFromSelf(S.NumberFromString) + await Util.assertions.decoding.succeed(schema, new Set(), new Set()) + await Util.assertions.decoding.succeed(schema, new Set(["1", "2", "3"]), new Set([1, 2, 3])) + + await Util.assertions.decoding.fail( + schema, + null, + `Expected ReadonlySet, actual null` + ) + await Util.assertions.decoding.fail( + schema, + new Set(["1", "a", "3"]), + `ReadonlySet +└─ ReadonlyArray + └─ [1] + └─ NumberFromString + └─ Transformation process failure + └─ Unable to decode "a" into a number` + ) + }) + + it("encoding", async () => { + const schema = S.ReadonlySetFromSelf(S.NumberFromString) + await Util.assertions.encoding.succeed(schema, new Set(), new Set()) + await Util.assertions.encoding.succeed(schema, new Set([1, 2, 3]), new Set(["1", "2", "3"])) + }) + + it("is", () => { + const schema = S.ReadonlySetFromSelf(S.String) + const is = P.is(schema) + assertTrue(is(new Set())) + assertTrue(is(new Set(["a", "b", "c"]))) + + assertFalse(is(new Set(["a", "b", 1]))) + assertFalse(is(null)) + assertFalse(is(undefined)) + }) + + it("pretty", () => { + const schema = S.ReadonlySetFromSelf(S.String) + Util.assertions.pretty(schema, new Set(), "new Set([])") + Util.assertions.pretty(schema, new Set(["a", "b"]), `new Set(["a", "b"])`) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/Record/Record.test.ts b/repos/effect/packages/effect/test/Schema/Schema/Record/Record.test.ts new file mode 100644 index 0000000..6fe9d7e --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/Record/Record.test.ts @@ -0,0 +1,405 @@ +import { describe, it } from "@effect/vitest" +import { deepStrictEqual, strictEqual } from "@effect/vitest/utils" +import * as Either from "effect/Either" +import * as ParseResult from "effect/ParseResult" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("record", () => { + it("should expose the key and the value", () => { + const schema = S.Record({ key: S.String, value: S.Number }) + strictEqual(schema.key, S.String) + strictEqual(schema.value, S.Number) + }) + + it("should compute the partial result", () => { + const schema = S.Record({ key: S.String, value: S.Number }) + const all = S.decodeUnknownEither(schema)({ a: 1, b: "b", c: 2, d: "d" }, { errors: "all" }) + if (Either.isLeft(all)) { + const issue = all.left.issue + if (ParseResult.isComposite(issue)) { + deepStrictEqual(issue.output, { a: 1, c: 2 }) + } else { + throw new Error("expected an And") + } + } else { + throw new Error("expected a Left") + } + const first = S.decodeUnknownEither(schema)({ a: 1, b: "b", c: 2, d: "d" }, { errors: "first" }) + if (Either.isLeft(first)) { + const issue = first.left.issue + if (ParseResult.isComposite(issue)) { + deepStrictEqual(issue.output, { a: 1 }) + } else { + throw new Error("expected an And") + } + } else { + throw new Error("expected a Left") + } + }) + + describe("decoding", () => { + it("Record(enum, number)", async () => { + enum Abc { + A = 1, + B = "b", + C = "c" + } + const AbcSchema = S.Enums(Abc) + const schema = S.Record({ key: AbcSchema, value: S.String }) + await Util.assertions.decoding.succeed(schema, { [Abc.A]: "A", [Abc.B]: "B", [Abc.C]: "C" }) + await Util.assertions.decoding.succeed(schema, { [1]: "A", b: "B", c: "C" }) + await Util.assertions.decoding.succeed(schema, { "1": "A", b: "B", c: "C" }) + + await Util.assertions.decoding.fail( + schema, + { [Abc.B]: "B", [Abc.C]: "C" }, + `{ readonly 1: string; readonly b: string; readonly c: string } +└─ [1] + └─ is missing` + ) + await Util.assertions.decoding.fail( + schema, + { [Abc.A]: "A", [Abc.B]: "B" }, + `{ readonly 1: string; readonly b: string; readonly c: string } +└─ ["c"] + └─ is missing` + ) + await Util.assertions.decoding.fail( + schema, + { [Abc.A]: null, [Abc.B]: "B", [Abc.C]: "C" }, + `{ readonly 1: string; readonly b: string; readonly c: string } +└─ [1] + └─ Expected string, actual null` + ) + }) + + it("Record(never, number)", async () => { + const schema = S.Record({ key: S.Never, value: S.Number }) + await Util.assertions.decoding.succeed(schema, {}) + await Util.assertions.decoding.succeed(schema, { a: 1 }) + }) + + it("Record(string, number)", async () => { + const schema = S.Record({ key: S.String, value: S.Number }) + await Util.assertions.decoding.succeed(schema, {}) + await Util.assertions.decoding.succeed(schema, { a: 1 }) + + await Util.assertions.decoding.fail( + schema, + [], + "Expected { readonly [x: string]: number }, actual []" + ) + await Util.assertions.decoding.fail( + schema, + { a: "a" }, + `{ readonly [x: string]: number } +└─ ["a"] + └─ Expected number, actual "a"` + ) + const b = Symbol.for("effect/Schema/test/b") + await Util.assertions.decoding.succeed(schema, { a: 1, [b]: "b" }, { a: 1 }) + await Util.assertions.decoding.fail( + schema, + { a: 1, [b]: "b" }, + `{ readonly [x: string]: number } +└─ [Symbol(effect/Schema/test/b)] + └─ is unexpected, expected: string`, + { parseOptions: Util.onExcessPropertyError } + ) + }) + + it("Record(symbol, number)", async () => { + const a = Symbol.for("effect/Schema/test/a") + const schema = S.Record({ key: S.SymbolFromSelf, value: S.Number }) + await Util.assertions.decoding.succeed(schema, {}) + await Util.assertions.decoding.succeed(schema, { [a]: 1 }) + + await Util.assertions.decoding.fail( + schema, + [], + "Expected { readonly [x: symbol]: number }, actual []" + ) + await Util.assertions.decoding.fail( + schema, + { [a]: "a" }, + `{ readonly [x: symbol]: number } +└─ [Symbol(effect/Schema/test/a)] + └─ Expected number, actual "a"` + ) + await Util.assertions.decoding.succeed( + schema, + { [a]: 1, b: "b" }, + { [a]: 1 } + ) + await Util.assertions.decoding.fail( + schema, + { [a]: 1, b: "b" }, + `{ readonly [x: symbol]: number } +└─ ["b"] + └─ is unexpected, expected: symbol`, + { parseOptions: Util.onExcessPropertyError } + ) + }) + + it("Record('a' | 'b', number)", async () => { + const schema = S.Record({ key: S.Union(S.Literal("a"), S.Literal("b")), value: S.Number }) + await Util.assertions.decoding.succeed(schema, { a: 1, b: 2 }) + + await Util.assertions.decoding.fail( + schema, + {}, + `{ readonly a: number; readonly b: number } +└─ ["a"] + └─ is missing` + ) + await Util.assertions.decoding.fail( + schema, + { a: 1 }, + `{ readonly a: number; readonly b: number } +└─ ["b"] + └─ is missing` + ) + await Util.assertions.decoding.fail( + schema, + { b: 2 }, + `{ readonly a: number; readonly b: number } +└─ ["a"] + └─ is missing` + ) + }) + + it("Record('a' | `prefix-${string}`, number)", async () => { + const schema = S.Record( + { key: S.Union(S.Literal("a"), S.TemplateLiteral(S.Literal("prefix-"), S.String)), value: S.Number } + ) + await Util.assertions.decoding.succeed(schema, { a: 1 }) + await Util.assertions.decoding.succeed(schema, { a: 1, "prefix-b": 2 }) + + await Util.assertions.decoding.fail( + schema, + {}, + `{ readonly a: number; readonly [x: \`prefix-\${string}\`]: number } +└─ ["a"] + └─ is missing` + ) + await Util.assertions.decoding.fail( + schema, + { a: 1, "prefix-b": "b" }, + `{ readonly a: number; readonly [x: \`prefix-\${string}\`]: number } +└─ ["prefix-b"] + └─ Expected number, actual "b"` + ) + }) + + it("Record(keyof struct({ a, b }), number)", async () => { + const schema = S.Record({ key: S.keyof(S.Struct({ a: S.String, b: S.String })), value: S.Number }) + await Util.assertions.decoding.succeed(schema, { a: 1, b: 2 }) + + await Util.assertions.decoding.fail( + schema, + {}, + `{ readonly a: number; readonly b: number } +└─ ["a"] + └─ is missing` + ) + await Util.assertions.decoding.fail( + schema, + { a: 1 }, + `{ readonly a: number; readonly b: number } +└─ ["b"] + └─ is missing` + ) + await Util.assertions.decoding.fail( + schema, + { b: 2 }, + `{ readonly a: number; readonly b: number } +└─ ["a"] + └─ is missing` + ) + await Util.assertions.decoding.fail( + schema, + { a: "a" }, + `{ readonly a: number; readonly b: number } +└─ ["a"] + └─ Expected number, actual "a"` + ) + }) + + it("Record(Symbol('a') | Symbol('b'), number)", async () => { + const a = Symbol.for("effect/Schema/test/a") + const b = Symbol.for("effect/Schema/test/b") + const schema = S.Record({ key: S.Union(S.UniqueSymbolFromSelf(a), S.UniqueSymbolFromSelf(b)), value: S.Number }) + await Util.assertions.decoding.succeed(schema, { [a]: 1, [b]: 2 }) + + await Util.assertions.decoding.fail( + schema, + {}, + `{ readonly Symbol(effect/Schema/test/a): number; readonly Symbol(effect/Schema/test/b): number } +└─ [Symbol(effect/Schema/test/a)] + └─ is missing` + ) + await Util.assertions.decoding.fail( + schema, + { [a]: 1 }, + `{ readonly Symbol(effect/Schema/test/a): number; readonly Symbol(effect/Schema/test/b): number } +└─ [Symbol(effect/Schema/test/b)] + └─ is missing` + ) + await Util.assertions.decoding.fail( + schema, + { [b]: 2 }, + `{ readonly Symbol(effect/Schema/test/a): number; readonly Symbol(effect/Schema/test/b): number } +└─ [Symbol(effect/Schema/test/a)] + └─ is missing` + ) + }) + + it("Record(${string}-${string}, number)", async () => { + const schema = S.Record({ key: S.TemplateLiteral(S.String, S.Literal("-"), S.String), value: S.Number }) + await Util.assertions.decoding.succeed(schema, {}) + await Util.assertions.decoding.succeed(schema, { "-": 1 }) + await Util.assertions.decoding.succeed(schema, { "a-": 1 }) + await Util.assertions.decoding.succeed(schema, { "-b": 1 }) + await Util.assertions.decoding.succeed(schema, { "a-b": 1 }) + await Util.assertions.decoding.succeed(schema, { "": 1 }, {}) + await Util.assertions.decoding.succeed(schema, { "a": 1 }, {}) + await Util.assertions.decoding.succeed(schema, { "a": "a" }, {}) + + await Util.assertions.decoding.fail( + schema, + { "-": "a" }, + `{ readonly [x: \`\${string}-\${string}\`]: number } +└─ ["-"] + └─ Expected number, actual "a"` + ) + await Util.assertions.decoding.fail( + schema, + { "a-": "a" }, + `{ readonly [x: \`\${string}-\${string}\`]: number } +└─ ["a-"] + └─ Expected number, actual "a"` + ) + await Util.assertions.decoding.fail( + schema, + { "-b": "b" }, + `{ readonly [x: \`\${string}-\${string}\`]: number } +└─ ["-b"] + └─ Expected number, actual "b"` + ) + await Util.assertions.decoding.fail( + schema, + { "a-b": "ab" }, + `{ readonly [x: \`\${string}-\${string}\`]: number } +└─ ["a-b"] + └─ Expected number, actual "ab"` + ) + + await Util.assertions.decoding.fail( + schema, + { "a": 1 }, + `{ readonly [x: \`\${string}-\${string}\`]: number } +└─ ["a"] + └─ is unexpected, expected: \`\${string}-\${string}\``, + { parseOptions: Util.onExcessPropertyError } + ) + }) + + it("Record(minLength(2), number)", async () => { + const schema = S.Record({ key: S.String.pipe(S.minLength(2)), value: S.Number }) + await Util.assertions.decoding.succeed(schema, {}) + await Util.assertions.decoding.succeed(schema, { "a": 1 }, {}) + await Util.assertions.decoding.succeed(schema, { "a": "a" }, {}) + await Util.assertions.decoding.succeed(schema, { "aa": 1 }) + await Util.assertions.decoding.succeed(schema, { "aaa": 1 }) + + await Util.assertions.decoding.fail( + schema, + { "aa": "aa" }, + `{ readonly [x: minLength(2)]: number } +└─ ["aa"] + └─ Expected number, actual "aa"` + ) + await Util.assertions.decoding.fail( + schema, + { "a": 1 }, + `{ readonly [x: minLength(2)]: number } +└─ ["a"] + └─ is unexpected, expected: minLength(2)`, + { parseOptions: Util.onExcessPropertyError } + ) + }) + + it("Record(${string}-${string}, number) & record(string, string | number)", async () => { + const schema = S.Struct( + {}, + S.Record({ key: S.TemplateLiteral(S.String, S.Literal("-"), S.String), value: S.Number }), + S.Record({ key: S.String, value: S.Union(S.String, S.Number) }) + ) + await Util.assertions.decoding.succeed(schema, {}) + await Util.assertions.decoding.succeed(schema, { "a": "a" }) + await Util.assertions.decoding.succeed(schema, { "a-": 1 }) + + await Util.assertions.decoding.fail( + schema, + { "a-": "a" }, + `{ readonly [x: \`\${string}-\${string}\`]: number; readonly [x: string]: string | number } +└─ ["a-"] + └─ Expected number, actual "a"` + ) + await Util.assertions.decoding.fail( + schema, + { "a": true }, + `{ readonly [x: \`\${string}-\${string}\`]: number; readonly [x: string]: string | number } +└─ ["a"] + └─ string | number + ├─ Expected string, actual true + └─ Expected number, actual true` + ) + }) + + it("should support branded keys", async () => { + const schema = S.Record({ key: S.NonEmptyString.pipe(S.brand("UserId")), value: S.Number }) + await Util.assertions.decoding.succeed(schema, {}) + await Util.assertions.decoding.succeed(schema, { "a": 1 }) + await Util.assertions.decoding.succeed(schema, { "": 1 }, {}) + await Util.assertions.decoding.succeed(schema, { "": "" }, {}) + + await Util.assertions.decoding.fail( + schema, + { "": 1 }, + `{ readonly [x: nonEmptyString & Brand<"UserId">]: number } +└─ [""] + └─ is unexpected, expected: nonEmptyString & Brand<"UserId">`, + { parseOptions: Util.onExcessPropertyError } + ) + }) + }) + + describe("encoding", () => { + it("key error", async () => { + const schema = S.Record({ key: S.Char, value: S.String }) + await Util.assertions.encoding.fail( + schema, + { aa: "a" }, + `{ readonly [x: Char]: string } +└─ ["aa"] + └─ is unexpected, expected: Char`, + { parseOptions: Util.onExcessPropertyError } + ) + }) + + it("value error", async () => { + const schema = S.Record({ key: S.String, value: S.Char }) + await Util.assertions.encoding.fail( + schema, + { a: "aa" }, + `{ readonly [x: string]: Char } +└─ ["a"] + └─ Char + └─ Predicate refinement failure + └─ Expected a single character, actual "aa"` + ) + }) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/Redacted/Redacted.test.ts b/repos/effect/packages/effect/test/Schema/Schema/Redacted/Redacted.test.ts new file mode 100644 index 0000000..5e2575a --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/Redacted/Redacted.test.ts @@ -0,0 +1,43 @@ +import { describe, it } from "@effect/vitest" +import { Redacted } from "effect" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("Redacted", () => { + const schema = S.Redacted(S.String) + + it("test roundtrip consistency", () => { + Util.assertions.testRoundtripConsistency(schema) + }) + + it("arbitrary", () => { + Util.assertions.arbitrary.validateGeneratedValues(S.RedactedFromSelf(S.Number)) + }) + + it("decoding", async () => { + await Util.assertions.decoding.succeed( + schema, + "keep me safe", + Redacted.make("keep me safe") + ) + await Util.assertions.decoding.fail( + schema, + Redacted.make(123), + `(string <-> Redacted()) +└─ Encoded side transformation failure + └─ Expected string, actual ` + ) + }) + + it("encoding", async () => { + await Util.assertions.encoding.succeed( + schema, + Redacted.make("keep me safe"), + "keep me safe" + ) + }) + + it("Pretty", () => { + Util.assertions.pretty(schema, Redacted.make("keep me safe"), `Redacted()`) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/Redacted/RedactedFromSelf.test.ts b/repos/effect/packages/effect/test/Schema/Schema/Redacted/RedactedFromSelf.test.ts new file mode 100644 index 0000000..b7aed80 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/Redacted/RedactedFromSelf.test.ts @@ -0,0 +1,42 @@ +import { describe, it } from "@effect/vitest" +import { Redacted } from "effect" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("RedactedFromSelf", () => { + const schema = S.RedactedFromSelf(S.String) + + it("test roundtrip consistency", () => { + Util.assertions.testRoundtripConsistency(schema) + }) + + it("arbitrary", () => { + Util.assertions.arbitrary.validateGeneratedValues(S.RedactedFromSelf(S.Number)) + }) + + it("decoding", async () => { + await Util.assertions.decoding.succeed( + schema, + Redacted.make("keep me safe"), + Redacted.make("keep me safe") + ) + await Util.assertions.decoding.fail( + schema, + Redacted.make(123), + `Redacted() +└─ Expected string, actual 123` + ) + }) + + it("encoding", async () => { + await Util.assertions.encoding.succeed( + schema, + Redacted.make("keep me safe"), + Redacted.make("keep me safe") + ) + }) + + it("Pretty", () => { + Util.assertions.pretty(schema, Redacted.make("keep me safe"), `Redacted()`) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/Set/Set.test.ts b/repos/effect/packages/effect/test/Schema/Schema/Set/Set.test.ts new file mode 100644 index 0000000..18c8fbd --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/Set/Set.test.ts @@ -0,0 +1,9 @@ +import { describe, it } from "@effect/vitest" +import { strictEqual } from "@effect/vitest/utils" +import * as S from "effect/Schema" + +describe("Set", () => { + it("description", () => { + strictEqual(String(S.Set(S.Number)), "(ReadonlyArray <-> Set)") + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/Set/SetFromSelf.test.ts b/repos/effect/packages/effect/test/Schema/Schema/Set/SetFromSelf.test.ts new file mode 100644 index 0000000..0799a93 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/Set/SetFromSelf.test.ts @@ -0,0 +1,9 @@ +import { describe, it } from "@effect/vitest" +import { strictEqual } from "@effect/vitest/utils" +import * as S from "effect/Schema" + +describe("SetFromSelf", () => { + it("description", () => { + strictEqual(String(S.SetFromSelf(S.Number)), "Set") + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/SortedSet/SortedSet.test.ts b/repos/effect/packages/effect/test/Schema/Schema/SortedSet/SortedSet.test.ts new file mode 100644 index 0000000..739783b --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/SortedSet/SortedSet.test.ts @@ -0,0 +1,48 @@ +import { describe, it } from "@effect/vitest" +import * as N from "effect/Number" +import * as S from "effect/Schema" +import * as SortedSet from "effect/SortedSet" +import * as Util from "../../TestUtils.js" + +describe("SortedSet", () => { + it("test roundtrip consistency", () => { + Util.assertions.testRoundtripConsistency(S.SortedSet(S.Number, N.Order)) + }) + + it("decoding", async () => { + const schema = S.SortedSet(S.Number, N.Order) + await Util.assertions.decoding.succeed(schema, [], SortedSet.fromIterable([] as Array, N.Order)) + await Util.assertions.decoding.succeed( + schema, + [1, 2, 3], + SortedSet.fromIterable([1, 2, 3] as Array, N.Order) + ) + + await Util.assertions.decoding.fail( + schema, + null, + `(ReadonlyArray <-> SortedSet) +└─ Encoded side transformation failure + └─ Expected ReadonlyArray, actual null` + ) + await Util.assertions.decoding.fail( + schema, + [1, "a"], + `(ReadonlyArray <-> SortedSet) +└─ Encoded side transformation failure + └─ ReadonlyArray + └─ [1] + └─ Expected number, actual "a"` + ) + }) + + it("encoding", async () => { + const schema = S.SortedSet(S.Number, N.Order) + await Util.assertions.encoding.succeed(schema, SortedSet.fromIterable([] as Array, N.Order), []) + await Util.assertions.encoding.succeed(schema, SortedSet.fromIterable([1, 2, 3] as Array, N.Order), [ + 1, + 2, + 3 + ]) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/SortedSet/SortedSetFromSelf.test.ts b/repos/effect/packages/effect/test/Schema/Schema/SortedSet/SortedSetFromSelf.test.ts new file mode 100644 index 0000000..91dcb70 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/SortedSet/SortedSetFromSelf.test.ts @@ -0,0 +1,88 @@ +import { describe, it } from "@effect/vitest" +import { assertFalse, assertTrue } from "@effect/vitest/utils" +import * as N from "effect/Number" +import * as P from "effect/ParseResult" +import * as Schema from "effect/Schema" +import * as SortedSet from "effect/SortedSet" +import * as S from "effect/String" +import * as Util from "../../TestUtils.js" + +describe("SortedSetFromSelf", () => { + it("test roundtrip consistency", () => { + Util.assertions.testRoundtripConsistency(Schema.SortedSetFromSelf(Schema.Number, N.Order, N.Order)) + }) + + it("decoding", async () => { + const schema = Schema.SortedSetFromSelf(Schema.NumberFromString, N.Order, S.Order) + await Util.assertions.decoding.succeed( + schema, + SortedSet.fromIterable([], S.Order), + SortedSet.fromIterable([] as Array, N.Order) + ) + await Util.assertions.decoding.succeed( + schema, + SortedSet.fromIterable(["1", "2", "3"], S.Order), + SortedSet.fromIterable([1, 2, 3], N.Order) + ) + + await Util.assertions.decoding.fail( + schema, + null, + `Expected SortedSet, actual null` + ) + await Util.assertions.decoding.fail( + schema, + SortedSet.fromIterable(["1", "a", "3"], S.Order), + `SortedSet +└─ ReadonlyArray + └─ [2] + └─ NumberFromString + └─ Transformation process failure + └─ Unable to decode "a" into a number` + ) + }) + + it("encoding", async () => { + const schema = Schema.SortedSetFromSelf(Schema.NumberFromString, N.Order, S.Order) + await Util.assertions.encoding.succeed( + schema, + SortedSet.fromIterable([] as Array, N.Order), + SortedSet.fromIterable([] as Array, S.Order) + ) + await Util.assertions.encoding.succeed( + schema, + SortedSet.fromIterable([1, 2, 3], N.Order), + SortedSet.fromIterable(["1", "2", "3"], S.Order) + ) + }) + + it("is", () => { + const schema = Schema.SortedSetFromSelf(Schema.String, S.Order, S.Order) + const is = P.is(schema) + assertTrue(is(SortedSet.fromIterable([], S.Order))) + assertTrue(is(SortedSet.fromIterable(["a", "b", "c"], S.Order))) + + assertFalse(is(new Set(["a", "b", 1]))) + assertFalse(is(null)) + assertFalse(is(undefined)) + }) + + it("pretty", () => { + const schema = Schema.SortedSetFromSelf(Schema.String, S.Order, S.Order) + Util.assertions.pretty(schema, SortedSet.fromIterable([] as Array, S.Order), "new SortedSet([])") + Util.assertions.pretty(schema, SortedSet.fromIterable(["a", "b"], S.Order), `new SortedSet(["a", "b"])`) + }) + + it("equivalence", () => { + const schema = Schema.SortedSetFromSelf(Schema.String, S.Order, S.Order) + const eq = Schema.equivalence(schema) + + const a = SortedSet.fromIterable([] as Array, S.Order) + const b = SortedSet.fromIterable(["a"] as Array, S.Order) + + assertTrue(eq(a, a)) + assertFalse(eq(a, b)) + assertFalse(eq(b, a)) + assertTrue(eq(b, b)) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/String/NonEmptyTrimmedString.test.ts b/repos/effect/packages/effect/test/Schema/Schema/String/NonEmptyTrimmedString.test.ts new file mode 100644 index 0000000..bfbabfe --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/String/NonEmptyTrimmedString.test.ts @@ -0,0 +1,58 @@ +import { describe, it } from "@effect/vitest" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("NonEmptyTrimmedString", () => { + it("test roundtrip consistency", () => { + const schema = S.NonEmptyTrimmedString + Util.assertions.testRoundtripConsistency(schema) + }) + + it("decoding", async () => { + const schema = S.NonEmptyTrimmedString + await Util.assertions.decoding.succeed(schema, "a", "a") + + await Util.assertions.decoding.fail( + schema, + " ", + `NonEmptyTrimmedString +└─ From side refinement failure + └─ Trimmed + └─ Predicate refinement failure + └─ Expected a string with no leading or trailing whitespace, actual " "` + ) + await Util.assertions.decoding.fail( + schema, + " a ", + `NonEmptyTrimmedString +└─ From side refinement failure + └─ Trimmed + └─ Predicate refinement failure + └─ Expected a string with no leading or trailing whitespace, actual " a "` + ) + }) + + it("encoding", async () => { + const schema = S.NonEmptyTrimmedString + await Util.assertions.encoding.succeed(schema, "a", "a") + + await Util.assertions.encoding.fail( + schema, + " ", + `NonEmptyTrimmedString +└─ From side refinement failure + └─ Trimmed + └─ Predicate refinement failure + └─ Expected a string with no leading or trailing whitespace, actual " "` + ) + await Util.assertions.encoding.fail( + schema, + " a ", + `NonEmptyTrimmedString +└─ From side refinement failure + └─ Trimmed + └─ Predicate refinement failure + └─ Expected a string with no leading or trailing whitespace, actual " a "` + ) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/String/String.test.ts b/repos/effect/packages/effect/test/Schema/Schema/String/String.test.ts new file mode 100644 index 0000000..487b229 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/String/String.test.ts @@ -0,0 +1,15 @@ +import { describe, it } from "@effect/vitest" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("String", () => { + const schema = S.String + it("decoding", async () => { + await Util.assertions.decoding.succeed(schema, "a", "a") + await Util.assertions.decoding.fail(schema, 1, "Expected string, actual 1") + }) + + it("encoding", async () => { + await Util.assertions.encoding.succeed(schema, "a", "a") + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/String/StringFromBase64.test.ts b/repos/effect/packages/effect/test/Schema/Schema/String/StringFromBase64.test.ts new file mode 100644 index 0000000..f98e95b --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/String/StringFromBase64.test.ts @@ -0,0 +1,48 @@ +import { describe, it } from "@effect/vitest" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("StringFromBase64", () => { + const schema = S.StringFromBase64 + + it("test roundtrip consistency", () => { + Util.assertions.testRoundtripConsistency(schema) + }) + + it("decoding", async () => { + await Util.assertions.decoding.succeed( + schema, + "Zm9vYmFy", + "foobar" + ) + await Util.assertions.decoding.fail( + schema, + "Zm9vY", + `StringFromBase64 +└─ Transformation process failure + └─ Length must be a multiple of 4, but is 5` + ) + await Util.assertions.decoding.fail( + schema, + "Zm9vYmF-", + `StringFromBase64 +└─ Transformation process failure + └─ Invalid character -` + ) + await Util.assertions.decoding.fail( + schema, + "=Zm9vYmF", + `StringFromBase64 +└─ Transformation process failure + └─ Found a '=' character, but it is not at the end` + ) + }) + + it("encoding", async () => { + await Util.assertions.encoding.succeed( + schema, + "foobar", + "Zm9vYmFy" + ) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/String/StringFromBase64Url.test.ts b/repos/effect/packages/effect/test/Schema/Schema/String/StringFromBase64Url.test.ts new file mode 100644 index 0000000..7f92f26 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/String/StringFromBase64Url.test.ts @@ -0,0 +1,51 @@ +import { describe, it } from "@effect/vitest" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("StringFromBase64Url", () => { + const schema = S.StringFromBase64Url + + it("test roundtrip consistency", () => { + Util.assertions.testRoundtripConsistency(schema) + }) + + it("decoding", async () => { + await Util.assertions.decoding.succeed( + schema, + "Zm9vYmFy", + "foobar" + ) + await Util.assertions.decoding.succeed( + schema, + "Pj8-ZD_Dnw", + ">?>d?ß" + ) + await Util.assertions.decoding.fail( + schema, + "Zm9vY", + `StringFromBase64Url +└─ Transformation process failure + └─ Length should be a multiple of 4, but is 5` + ) + await Util.assertions.decoding.fail( + schema, + "Pj8/ZD+Dnw", + `StringFromBase64Url +└─ Transformation process failure + └─ Invalid input` + ) + }) + + it("encoding", async () => { + await Util.assertions.encoding.succeed( + schema, + "foobar", + "Zm9vYmFy" + ) + await Util.assertions.encoding.succeed( + schema, + ">?>d?ß", + "Pj8-ZD_Dnw" + ) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/String/StringFromHex.test.ts b/repos/effect/packages/effect/test/Schema/Schema/String/StringFromHex.test.ts new file mode 100644 index 0000000..00e8a68 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/String/StringFromHex.test.ts @@ -0,0 +1,59 @@ +import { describe, it } from "@effect/vitest" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("StringFromHex", () => { + const schema = S.StringFromHex + const decoder = new TextDecoder("utf-8") + + it("test roundtrip consistency", () => { + Util.assertions.testRoundtripConsistency(schema) + }) + + it("decoding", async () => { + await Util.assertions.decoding.succeed( + schema, + "0001020304050607", + decoder.decode(Uint8Array.from([0, 1, 2, 3, 4, 5, 6, 7])) + ) + await Util.assertions.decoding.succeed( + schema, + "f0f1f2f3f4f5f6f7", + decoder.decode(Uint8Array.from([0xf0, 0xf1, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7])) + ) + await Util.assertions.decoding.succeed( + schema, + "67", + "g" + ) + await Util.assertions.decoding.fail( + schema, + "0", + `StringFromHex +└─ Transformation process failure + └─ Length must be a multiple of 2, but is 1` + ) + await Util.assertions.decoding.fail( + schema, + "zd4aa", + `StringFromHex +└─ Transformation process failure + └─ Length must be a multiple of 2, but is 5` + ) + await Util.assertions.decoding.fail( + schema, + "0\x01", + `StringFromHex +└─ Transformation process failure + └─ Invalid input` + ) + }) + + it("encoding", async () => { + await Util.assertions.encoding.succeed( + schema, + decoder.decode(Uint8Array.from([0, 1, 2, 3, 4, 5, 6, 7])), + "0001020304050607" + ) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/String/StringFromUriComponent.test.ts b/repos/effect/packages/effect/test/Schema/Schema/String/StringFromUriComponent.test.ts new file mode 100644 index 0000000..272f71c --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/String/StringFromUriComponent.test.ts @@ -0,0 +1,32 @@ +import { describe, it } from "@effect/vitest" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("StringFromUriComponent", () => { + const schema = S.StringFromUriComponent + + it("encoding", async () => { + await Util.assertions.encoding.succeed(schema, "шеллы", "%D1%88%D0%B5%D0%BB%D0%BB%D1%8B") + await Util.assertions.encoding.fail( + schema, + "Hello\uD800", + `StringFromUriComponent +└─ Transformation process failure + └─ URI malformed` + ) + }) + + it("decoding", async () => { + await Util.assertions.decoding.succeed(schema, "%D1%88%D0%B5%D0%BB%D0%BB%D1%8B", "шеллы") + await Util.assertions.decoding.succeed(schema, "hello", "hello") + await Util.assertions.decoding.succeed(schema, "hello%20world", "hello world") + + await Util.assertions.decoding.fail( + schema, + "Hello%2world", + `StringFromUriComponent +└─ Transformation process failure + └─ URI malformed` + ) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/String/capitalize.test.ts b/repos/effect/packages/effect/test/Schema/Schema/String/capitalize.test.ts new file mode 100644 index 0000000..a5ccfc0 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/String/capitalize.test.ts @@ -0,0 +1,34 @@ +import { describe, it } from "@effect/vitest" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("Capitalize", () => { + it("test roundtrip consistency", () => { + const schema = S.Capitalize + Util.assertions.testRoundtripConsistency(schema) + }) + + it("decoding", async () => { + const schema = S.Capitalize + await Util.assertions.decoding.succeed(schema, "aa", "Aa") + await Util.assertions.decoding.succeed(schema, "aa ", "Aa ") + await Util.assertions.decoding.succeed(schema, " aa ", " aa ") + await Util.assertions.decoding.succeed(schema, "", "") + }) + + it("encoding", async () => { + const schema = S.Capitalize + await Util.assertions.encoding.succeed(schema, "", "") + await Util.assertions.encoding.succeed(schema, "Aa", "Aa") + + await Util.assertions.encoding.fail( + schema, + "aa", + `Capitalize +└─ Type side transformation failure + └─ Capitalized + └─ Predicate refinement failure + └─ Expected a capitalized string, actual "aa"` + ) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/String/endsWith.test.ts b/repos/effect/packages/effect/test/Schema/Schema/String/endsWith.test.ts new file mode 100644 index 0000000..2475ba1 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/String/endsWith.test.ts @@ -0,0 +1,38 @@ +import { describe, it } from "@effect/vitest" +import { assertFalse, assertTrue } from "@effect/vitest/utils" +import * as P from "effect/ParseResult" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("endsWith", () => { + it("is", () => { + const schema = S.String.pipe(S.endsWith("a")) + const is = P.is(schema) + assertTrue(is("a")) + assertTrue(is("ba")) + + assertFalse(is("")) + assertFalse(is("b")) + }) + + it("decoding", async () => { + const schema = S.String.pipe(S.endsWith("a")) + await Util.assertions.decoding.succeed(schema, "a") + await Util.assertions.decoding.succeed(schema, "ba") + + await Util.assertions.decoding.fail( + schema, + "", + `endsWith("a") +└─ Predicate refinement failure + └─ Expected a string ending with "a", actual ""` + ) + await Util.assertions.decoding.fail( + schema, + "b", + `endsWith("a") +└─ Predicate refinement failure + └─ Expected a string ending with "a", actual "b"` + ) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/String/includes.test.ts b/repos/effect/packages/effect/test/Schema/Schema/String/includes.test.ts new file mode 100644 index 0000000..30b1c72 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/String/includes.test.ts @@ -0,0 +1,40 @@ +import { describe, it } from "@effect/vitest" +import { assertFalse, assertTrue } from "@effect/vitest/utils" +import * as P from "effect/ParseResult" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("includes", () => { + const schema = S.String.pipe(S.includes("a")) + + it("test roundtrip consistency", () => { + Util.assertions.testRoundtripConsistency(schema) + }) + + it("is", () => { + const is = P.is(schema) + assertFalse(is("")) + assertTrue(is("a")) + assertTrue(is("aa")) + assertTrue(is("bac")) + assertTrue(is("ba")) + }) + + it("decoding", async () => { + await Util.assertions.decoding.succeed(schema, "a") + await Util.assertions.decoding.succeed(schema, "aa") + await Util.assertions.decoding.succeed(schema, "bac") + await Util.assertions.decoding.succeed(schema, "ba") + await Util.assertions.decoding.fail( + schema, + "", + `includes("a") +└─ Predicate refinement failure + └─ Expected a string including "a", actual ""` + ) + }) + + it("Pretty", () => { + Util.assertions.pretty(schema, "a", `"a"`) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/String/length.test.ts b/repos/effect/packages/effect/test/Schema/Schema/String/length.test.ts new file mode 100644 index 0000000..350420e --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/String/length.test.ts @@ -0,0 +1,88 @@ +import { describe, it } from "@effect/vitest" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("length", () => { + describe("decoding", () => { + it("length: 1", async () => { + const schema = S.String.pipe(S.length(1, { identifier: "Char" })) + await Util.assertions.decoding.succeed(schema, "a") + + await Util.assertions.decoding.fail( + schema, + "", + `Char +└─ Predicate refinement failure + └─ Expected a single character, actual ""` + ) + await Util.assertions.decoding.fail( + schema, + "aa", + `Char +└─ Predicate refinement failure + └─ Expected a single character, actual "aa"` + ) + }) + + it("length > 1", async () => { + const schema = S.String.pipe(S.length(2, { identifier: "Char2" })) + await Util.assertions.decoding.succeed(schema, "aa") + + await Util.assertions.decoding.fail( + schema, + "", + `Char2 +└─ Predicate refinement failure + └─ Expected a string 2 character(s) long, actual ""` + ) + }) + + it("length : { min > max }", async () => { + const schema = S.String.pipe(S.length({ min: 2, max: 4 }, { identifier: "Char(2-4)" })) + await Util.assertions.decoding.succeed(schema, "aa") + await Util.assertions.decoding.succeed(schema, "aaa") + await Util.assertions.decoding.succeed(schema, "aaaa") + + await Util.assertions.decoding.fail( + schema, + "", + `Char(2-4) +└─ Predicate refinement failure + └─ Expected a string at least 2 character(s) and at most 4 character(s) long, actual ""` + ) + await Util.assertions.decoding.fail( + schema, + "aaaaa", + `Char(2-4) +└─ Predicate refinement failure + └─ Expected a string at least 2 character(s) and at most 4 character(s) long, actual "aaaaa"` + ) + }) + + it("length : { min = max }", async () => { + const schema = S.String.pipe(S.length({ min: 2, max: 2 }, { identifier: "Char2" })) + await Util.assertions.decoding.succeed(schema, "aa") + + await Util.assertions.decoding.fail( + schema, + "", + `Char2 +└─ Predicate refinement failure + └─ Expected a string 2 character(s) long, actual ""` + ) + }) + + it("length : { min < max }", async () => { + const schema = S.String.pipe(S.length({ min: 2, max: 1 }, { identifier: "Char2" })) + await Util.assertions.decoding.succeed(schema, "aa") + + await Util.assertions.decoding.fail( + schema, + "", + `Char2 +└─ Predicate refinement failure + └─ Expected a string 2 character(s) long, actual ""` + ) + }) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/String/lowercase.test.ts b/repos/effect/packages/effect/test/Schema/Schema/String/lowercase.test.ts new file mode 100644 index 0000000..fc4d40a --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/String/lowercase.test.ts @@ -0,0 +1,33 @@ +import { describe, it } from "@effect/vitest" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("Lowercase", () => { + it("test roundtrip consistency", () => { + const schema = S.Lowercase + Util.assertions.testRoundtripConsistency(schema) + }) + + it("decoding", async () => { + const schema = S.Lowercase + await Util.assertions.decoding.succeed(schema, "a", "a") + await Util.assertions.decoding.succeed(schema, "A ", "a ") + await Util.assertions.decoding.succeed(schema, " A ", " a ") + }) + + it("encoding", async () => { + const schema = S.Lowercase + await Util.assertions.encoding.succeed(schema, "", "") + await Util.assertions.encoding.succeed(schema, "a", "a") + + await Util.assertions.encoding.fail( + schema, + "A", + `Lowercase +└─ Type side transformation failure + └─ Lowercased + └─ Predicate refinement failure + └─ Expected a lowercase string, actual "A"` + ) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/String/maxLength.test.ts b/repos/effect/packages/effect/test/Schema/Schema/String/maxLength.test.ts new file mode 100644 index 0000000..a4e395b --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/String/maxLength.test.ts @@ -0,0 +1,36 @@ +import { describe, it } from "@effect/vitest" +import { assertFalse, assertTrue } from "@effect/vitest/utils" +import * as P from "effect/ParseResult" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("maxLength", () => { + it("test roundtrip consistency", () => { + Util.assertions.testRoundtripConsistency(S.String.pipe(S.maxLength(1))) + }) + + it("is", () => { + const is = P.is(S.String.pipe(S.maxLength(1))) + assertTrue(is("")) + assertTrue(is("a")) + assertFalse(is("aa")) + }) + + it("decoding", async () => { + const schema = S.String.pipe(S.maxLength(1)) + await Util.assertions.decoding.succeed(schema, "") + await Util.assertions.decoding.succeed(schema, "a") + await Util.assertions.decoding.fail( + schema, + "aa", + `maxLength(1) +└─ Predicate refinement failure + └─ Expected a string at most 1 character(s) long, actual "aa"` + ) + }) + + it("pretty", () => { + const schema = S.String.pipe(S.maxLength(1)) + Util.assertions.pretty(schema, "a", `"a"`) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/String/minLength.test.ts b/repos/effect/packages/effect/test/Schema/Schema/String/minLength.test.ts new file mode 100644 index 0000000..dfa08d4 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/String/minLength.test.ts @@ -0,0 +1,36 @@ +import { describe, it } from "@effect/vitest" +import { assertFalse, assertTrue } from "@effect/vitest/utils" +import * as P from "effect/ParseResult" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("minLength", () => { + it("test roundtrip consistency", () => { + Util.assertions.testRoundtripConsistency(S.String.pipe(S.minLength(1))) + }) + + it("is", () => { + const is = P.is(S.String.pipe(S.minLength(1))) + assertFalse(is("")) + assertTrue(is("a")) + assertTrue(is("aa")) + }) + + it("decoding", async () => { + const schema = S.String.pipe(S.minLength(1)) + await Util.assertions.decoding.succeed(schema, "a") + await Util.assertions.decoding.succeed(schema, "aa") + await Util.assertions.decoding.fail( + schema, + "", + `minLength(1) +└─ Predicate refinement failure + └─ Expected a string at least 1 character(s) long, actual ""` + ) + }) + + it("pretty", () => { + const schema = S.String.pipe(S.minLength(1)) + Util.assertions.pretty(schema, "a", `"a"`) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/String/nonEmptyString.test.ts b/repos/effect/packages/effect/test/Schema/Schema/String/nonEmptyString.test.ts new file mode 100644 index 0000000..5702ff3 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/String/nonEmptyString.test.ts @@ -0,0 +1,31 @@ +import { describe, it } from "@effect/vitest" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("nonEmptyString", () => { + const schema = S.NonEmptyString + + it("make", () => { + Util.assertions.make.succeed(S.NonEmptyString, "a") + Util.assertions.make.fail( + S.NonEmptyString, + "", + `NonEmptyString +└─ Predicate refinement failure + └─ Expected a non empty string, actual ""` + ) + }) + + it("decoding", async () => { + await Util.assertions.decoding.succeed(schema, "a") + await Util.assertions.decoding.succeed(schema, "aa") + + await Util.assertions.decoding.fail( + schema, + "", + `NonEmptyString +└─ Predicate refinement failure + └─ Expected a non empty string, actual ""` + ) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/String/pattern.test.ts b/repos/effect/packages/effect/test/Schema/Schema/String/pattern.test.ts new file mode 100644 index 0000000..55abc35 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/String/pattern.test.ts @@ -0,0 +1,44 @@ +import { describe, it } from "@effect/vitest" +import { assertFalse, assertTrue, strictEqual } from "@effect/vitest/utils" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("pattern", () => { + it("is", () => { + const schema = S.String.pipe(S.pattern(/^abb+$/)) + const is = S.is(schema) + assertTrue(is("abb")) + assertTrue(is("abbb")) + + assertFalse(is("ab")) + assertFalse(is("a")) + }) + + it("should reset lastIndex to 0 before each `test` call (#88)", () => { + const regexp = /^(A|B)$/g + const schema = S.String.pipe(S.pattern(regexp)) + strictEqual(S.decodeSync(schema)("A"), "A") + strictEqual(S.decodeSync(schema)("A"), "A") + }) + + it("decoding", async () => { + const schema = S.String.pipe(S.pattern(/^abb+$/)) + await Util.assertions.decoding.succeed(schema, "abb") + await Util.assertions.decoding.succeed(schema, "abbb") + + await Util.assertions.decoding.fail( + schema, + "ab", + `a string matching the pattern ^abb+$ +└─ Predicate refinement failure + └─ Expected a string matching the pattern ^abb+$, actual "ab"` + ) + await Util.assertions.decoding.fail( + schema, + "a", + `a string matching the pattern ^abb+$ +└─ Predicate refinement failure + └─ Expected a string matching the pattern ^abb+$, actual "a"` + ) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/String/split.test.ts b/repos/effect/packages/effect/test/Schema/Schema/String/split.test.ts new file mode 100644 index 0000000..243d66a --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/String/split.test.ts @@ -0,0 +1,28 @@ +import { describe, it } from "@effect/vitest" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("string/split", () => { + it("split (data-last)", async () => { + const schema = S.split(",") + + Util.assertions.testRoundtripConsistency(schema) + + // Decoding + await Util.assertions.decoding.succeed(schema, "", [""]) + await Util.assertions.decoding.succeed(schema, ",", ["", ""]) + await Util.assertions.decoding.succeed(schema, "a", ["a"]) + await Util.assertions.decoding.succeed(schema, ",a", ["", "a"]) + await Util.assertions.decoding.succeed(schema, "a,", ["a", ""]) + await Util.assertions.decoding.succeed(schema, "a,b", ["a", "b"]) + + // Encoding + await Util.assertions.encoding.succeed(schema, [], "") + await Util.assertions.encoding.succeed(schema, [""], "") + await Util.assertions.encoding.succeed(schema, ["", ""], ",") + await Util.assertions.encoding.succeed(schema, ["a"], "a") + await Util.assertions.encoding.succeed(schema, ["", "a"], ",a") + await Util.assertions.encoding.succeed(schema, ["a", ""], "a,") + await Util.assertions.encoding.succeed(schema, ["a", "b"], "a,b") + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/String/startsWith.test.ts b/repos/effect/packages/effect/test/Schema/Schema/String/startsWith.test.ts new file mode 100644 index 0000000..228909b --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/String/startsWith.test.ts @@ -0,0 +1,38 @@ +import { describe, it } from "@effect/vitest" +import { assertFalse, assertTrue } from "@effect/vitest/utils" +import * as P from "effect/ParseResult" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("startsWith", () => { + it("is", () => { + const schema = S.String.pipe(S.startsWith("a")) + const is = P.is(schema) + assertTrue(is("a")) + assertTrue(is("ab")) + + assertFalse(is("")) + assertFalse(is("b")) + }) + + it("decoding", async () => { + const schema = S.String.pipe(S.startsWith("a")) + await Util.assertions.decoding.succeed(schema, "a") + await Util.assertions.decoding.succeed(schema, "ab") + + await Util.assertions.decoding.fail( + schema, + "", + `startsWith("a") +└─ Predicate refinement failure + └─ Expected a string starting with "a", actual ""` + ) + await Util.assertions.decoding.fail( + schema, + "b", + `startsWith("a") +└─ Predicate refinement failure + └─ Expected a string starting with "a", actual "b"` + ) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/String/trim.test.ts b/repos/effect/packages/effect/test/Schema/Schema/String/trim.test.ts new file mode 100644 index 0000000..0b051e5 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/String/trim.test.ts @@ -0,0 +1,87 @@ +import { describe, it } from "@effect/vitest" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("trim", () => { + it("test roundtrip consistency", () => { + const schema = S.Trim + Util.assertions.testRoundtripConsistency(schema) + }) + + it("decoding", async () => { + const schema = S.String.pipe(S.minLength(1), S.compose(S.Trim)).annotations({ identifier: "MySchema" }) + await Util.assertions.decoding.succeed(schema, "a", "a") + await Util.assertions.decoding.succeed(schema, "a ", "a") + await Util.assertions.decoding.succeed(schema, " a ", "a") + await Util.assertions.decoding.succeed(schema, " ", "") + + await Util.assertions.decoding.fail( + schema, + "", + `MySchema +└─ Encoded side transformation failure + └─ minLength(1) + └─ Predicate refinement failure + └─ Expected a string at least 1 character(s) long, actual ""` + ) + }) + + it("encoding", async () => { + const schema = S.String.pipe(S.minLength(1), S.compose(S.Trim)).annotations({ identifier: "MySchema" }) + await Util.assertions.encoding.succeed(schema, "a", "a") + + await Util.assertions.encoding.fail( + schema, + "", + `MySchema +└─ Encoded side transformation failure + └─ minLength(1) + └─ Predicate refinement failure + └─ Expected a string at least 1 character(s) long, actual ""` + ) + await Util.assertions.encoding.fail( + schema, + " a", + `MySchema +└─ Type side transformation failure + └─ Trim + └─ Type side transformation failure + └─ Trimmed + └─ Predicate refinement failure + └─ Expected a string with no leading or trailing whitespace, actual " a"` + ) + await Util.assertions.encoding.fail( + schema, + "a ", + `MySchema +└─ Type side transformation failure + └─ Trim + └─ Type side transformation failure + └─ Trimmed + └─ Predicate refinement failure + └─ Expected a string with no leading or trailing whitespace, actual "a "` + ) + await Util.assertions.encoding.fail( + schema, + " a ", + `MySchema +└─ Type side transformation failure + └─ Trim + └─ Type side transformation failure + └─ Trimmed + └─ Predicate refinement failure + └─ Expected a string with no leading or trailing whitespace, actual " a "` + ) + await Util.assertions.encoding.fail( + schema, + " ", + `MySchema +└─ Type side transformation failure + └─ Trim + └─ Type side transformation failure + └─ Trimmed + └─ Predicate refinement failure + └─ Expected a string with no leading or trailing whitespace, actual " "` + ) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/String/uncapitalize.test.ts b/repos/effect/packages/effect/test/Schema/Schema/String/uncapitalize.test.ts new file mode 100644 index 0000000..ffb8a37 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/String/uncapitalize.test.ts @@ -0,0 +1,34 @@ +import { describe, it } from "@effect/vitest" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("Uncapitalize", () => { + it("test roundtrip consistency", () => { + const schema = S.Uncapitalize + Util.assertions.testRoundtripConsistency(schema) + }) + + it("decoding", async () => { + const schema = S.Uncapitalize + await Util.assertions.decoding.succeed(schema, "AA", "aA") + await Util.assertions.decoding.succeed(schema, "AA ", "aA ") + await Util.assertions.decoding.succeed(schema, " aa ", " aa ") + await Util.assertions.decoding.succeed(schema, "", "") + }) + + it("encoding", async () => { + const schema = S.Uncapitalize + await Util.assertions.encoding.succeed(schema, "", "") + await Util.assertions.encoding.succeed(schema, "aA", "aA") + + await Util.assertions.encoding.fail( + schema, + "AA", + `Uncapitalize +└─ Type side transformation failure + └─ Uncapitalized + └─ Predicate refinement failure + └─ Expected a uncapitalized string, actual "AA"` + ) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/String/uppercase.test.ts b/repos/effect/packages/effect/test/Schema/Schema/String/uppercase.test.ts new file mode 100644 index 0000000..dd18a51 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/String/uppercase.test.ts @@ -0,0 +1,33 @@ +import { describe, it } from "@effect/vitest" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("Uppercase", () => { + it("test roundtrip consistency", () => { + const schema = S.Uppercase + Util.assertions.testRoundtripConsistency(schema) + }) + + it("decoding", async () => { + const schema = S.Uppercase + await Util.assertions.decoding.succeed(schema, "A", "A") + await Util.assertions.decoding.succeed(schema, "a ", "A ") + await Util.assertions.decoding.succeed(schema, " a ", " A ") + }) + + it("encoding", async () => { + const schema = S.Uppercase + await Util.assertions.encoding.succeed(schema, "", "") + await Util.assertions.encoding.succeed(schema, "A", "A") + + await Util.assertions.encoding.fail( + schema, + "a", + `Uppercase +└─ Type side transformation failure + └─ Uppercased + └─ Predicate refinement failure + └─ Expected an uppercase string, actual "a"` + ) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/Struct/Struct.test.ts b/repos/effect/packages/effect/test/Schema/Schema/Struct/Struct.test.ts new file mode 100644 index 0000000..0a42b8d --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/Struct/Struct.test.ts @@ -0,0 +1,271 @@ +import { describe, it } from "@effect/vitest" +import { deepStrictEqual, strictEqual } from "@effect/vitest/utils" +import * as S from "effect/Schema" +import * as AST from "effect/SchemaAST" +import * as Util from "../../TestUtils.js" + +describe("Struct", () => { + it("should expose the fields", () => { + const schema = S.Struct({ + a: S.String, + b: S.Number + }) + deepStrictEqual(schema.fields, { + a: S.String, + b: S.Number + }) + }) + + it("should return the literal interface when using the .annotations() method", () => { + const schema = S.Struct({ + a: S.String, + b: S.Number + }).annotations({ identifier: "struct test" }) + deepStrictEqual(schema.ast.annotations, { [AST.IdentifierAnnotationId]: "struct test" }) + deepStrictEqual(schema.fields, { + a: S.String, + b: S.Number + }) + }) + + it(`should allow a "constructor" field name`, () => { + const schema = S.Struct({ constructor: S.String }) + strictEqual(schema.ast._tag, "TypeLiteral") + }) + + describe("decoding", () => { + it("should use annotations to generate a more informative error message when an incorrect data type is provided", async () => { + const schema = S.Struct({}).annotations({ identifier: "MyDataType" }) + await Util.assertions.decoding.fail( + schema, + null, + `Expected MyDataType, actual null` + ) + }) + + it("empty", async () => { + const schema = S.Struct({}) + await Util.assertions.decoding.succeed(schema, {}) + await Util.assertions.decoding.succeed(schema, { a: 1 }) + await Util.assertions.decoding.succeed(schema, []) + + await Util.assertions.decoding.fail( + schema, + null, + `Expected {}, actual null` + ) + }) + + it("required property signature", async () => { + const schema = S.Struct({ a: S.Number }) + await Util.assertions.decoding.succeed(schema, { a: 1 }) + + await Util.assertions.decoding.fail( + schema, + null, + `Expected { readonly a: number }, actual null` + ) + await Util.assertions.decoding.fail( + schema, + {}, + `{ readonly a: number } +└─ ["a"] + └─ is missing` + ) + await Util.assertions.decoding.fail( + schema, + { a: undefined }, + `{ readonly a: number } +└─ ["a"] + └─ Expected number, actual undefined` + ) + await Util.assertions.decoding.fail( + schema, + { a: 1, b: "b" }, + `{ readonly a: number } +└─ ["b"] + └─ is unexpected, expected: "a"`, + { parseOptions: Util.onExcessPropertyError } + ) + }) + + it("required property signature with undefined", async () => { + const schema = S.Struct({ a: S.Union(S.Number, S.Undefined) }) + await Util.assertions.decoding.succeed(schema, { a: 1 }) + await Util.assertions.decoding.succeed(schema, { a: undefined }) + await Util.assertions.decoding.succeed(schema, {}, { a: undefined }) + + await Util.assertions.decoding.fail( + schema, + null, + `Expected { readonly a: number | undefined }, actual null` + ) + await Util.assertions.decoding.fail( + schema, + { a: "a" }, + `{ readonly a: number | undefined } +└─ ["a"] + └─ number | undefined + ├─ Expected number, actual "a" + └─ Expected undefined, actual "a"` + ) + await Util.assertions.decoding.fail( + schema, + { a: 1, b: "b" }, + `{ readonly a: number | undefined } +└─ ["b"] + └─ is unexpected, expected: "a"`, + { parseOptions: Util.onExcessPropertyError } + ) + }) + + it("exact optional property signature", async () => { + const schema = S.Struct({ a: S.optionalWith(S.Number, { exact: true }) }) + await Util.assertions.decoding.succeed(schema, {}) + await Util.assertions.decoding.succeed(schema, { a: 1 }) + + await Util.assertions.decoding.fail( + schema, + null, + `Expected { readonly a?: number }, actual null` + ) + await Util.assertions.decoding.fail( + schema, + { a: "a" }, + `{ readonly a?: number } +└─ ["a"] + └─ Expected number, actual "a"` + ) + await Util.assertions.decoding.fail( + schema, + { a: undefined }, + `{ readonly a?: number } +└─ ["a"] + └─ Expected number, actual undefined` + ) + await Util.assertions.decoding.fail( + schema, + { a: 1, b: "b" }, + `{ readonly a?: number } +└─ ["b"] + └─ is unexpected, expected: "a"`, + { parseOptions: Util.onExcessPropertyError } + ) + }) + + it("exact optional property signature with undefined", async () => { + const schema = S.Struct({ a: S.optionalWith(S.Union(S.Number, S.Undefined), { exact: true }) }) + await Util.assertions.decoding.succeed(schema, {}) + await Util.assertions.decoding.succeed(schema, { a: 1 }) + await Util.assertions.decoding.succeed(schema, { a: undefined }) + + await Util.assertions.decoding.fail( + schema, + null, + `Expected { readonly a?: number | undefined }, actual null` + ) + await Util.assertions.decoding.fail( + schema, + { a: "a" }, + `{ readonly a?: number | undefined } +└─ ["a"] + └─ number | undefined + ├─ Expected number, actual "a" + └─ Expected undefined, actual "a"` + ) + await Util.assertions.decoding.fail( + schema, + { a: 1, b: "b" }, + `{ readonly a?: number | undefined } +└─ ["b"] + └─ is unexpected, expected: "a"`, + { parseOptions: Util.onExcessPropertyError } + ) + }) + + it("should not add optional keys", async () => { + const schema = S.Struct({ + a: S.optionalWith(S.String, { exact: true }), + b: S.optionalWith(S.Number, { exact: true }) + }) + await Util.assertions.decoding.succeed(schema, {}) + }) + }) + + describe("encoding", () => { + it("empty", async () => { + const schema = S.Struct({}) + await Util.assertions.encoding.succeed(schema, {}, {}) + await Util.assertions.encoding.succeed(schema, { a: 1 }, { a: 1 }) + await Util.assertions.encoding.succeed(schema, [], []) + + await Util.assertions.encoding.fail( + schema, + null as any, + `Expected {}, actual null` + ) + }) + + it("required property signature", async () => { + const schema = S.Struct({ a: S.Number }) + await Util.assertions.encoding.succeed(schema, { a: 1 }, { a: 1 }) + await Util.assertions.encoding.fail( + schema, + { a: 1, b: "b" } as any, + `{ readonly a: number } +└─ ["b"] + └─ is unexpected, expected: "a"`, + { parseOptions: Util.onExcessPropertyError } + ) + }) + + it("required property signature with undefined", async () => { + const schema = S.Struct({ a: S.Union(S.Number, S.Undefined) }) + await Util.assertions.encoding.succeed(schema, { a: 1 }, { a: 1 }) + await Util.assertions.encoding.succeed(schema, { a: undefined }, { a: undefined }) + await Util.assertions.encoding.fail( + schema, + { a: 1, b: "b" } as any, + `{ readonly a: number | undefined } +└─ ["b"] + └─ is unexpected, expected: "a"`, + { parseOptions: Util.onExcessPropertyError } + ) + }) + + it("exact optional property signature", async () => { + const schema = S.Struct({ a: S.optionalWith(S.Number, { exact: true }) }) + await Util.assertions.encoding.succeed(schema, {}, {}) + await Util.assertions.encoding.succeed(schema, { a: 1 }, { a: 1 }) + await Util.assertions.encoding.fail( + schema, + { a: 1, b: "b" } as any, + `{ readonly a?: number } +└─ ["b"] + └─ is unexpected, expected: "a"`, + { parseOptions: Util.onExcessPropertyError } + ) + }) + + it("exact optional property signature with undefined", async () => { + const schema = S.Struct({ a: S.optionalWith(S.Union(S.Number, S.Undefined), { exact: true }) }) + await Util.assertions.encoding.succeed(schema, {}, {}) + await Util.assertions.encoding.succeed(schema, { a: 1 }, { a: 1 }) + await Util.assertions.encoding.succeed(schema, { a: undefined }, { a: undefined }) + await Util.assertions.encoding.fail( + schema, + { a: 1, b: "b" } as any, + `{ readonly a?: number | undefined } +└─ ["b"] + └─ is unexpected, expected: "a"`, + { parseOptions: Util.onExcessPropertyError } + ) + }) + + it("should handle symbols as keys", async () => { + const a = Symbol.for("effect/Schema/test/a") + const schema = S.Struct({ [a]: S.String }) + await Util.assertions.encoding.succeed(schema, { [a]: "a" }, { [a]: "a" }) + }) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/Struct/make.test.ts b/repos/effect/packages/effect/test/Schema/Schema/Struct/make.test.ts new file mode 100644 index 0000000..3247a8f --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/Struct/make.test.ts @@ -0,0 +1,126 @@ +import { describe, it } from "@effect/vitest" +import { deepStrictEqual } from "@effect/vitest/utils" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("make", () => { + it("required fields", () => { + const schema = S.Struct({ a: S.String }) + Util.assertions.make.succeed(schema, { a: "a" }) + }) + + it("optional fields", () => { + const schema = S.Struct({ a: S.optional(S.String) }) + Util.assertions.make.succeed(schema, { a: "a" }) + Util.assertions.make.succeed(schema, {}) + }) + + it("should validate the input by default", () => { + const schema = S.Struct({ a: S.NonEmptyString }) + Util.assertions.make.fail( + schema, + { a: "" }, + `{ readonly a: NonEmptyString } +└─ ["a"] + └─ NonEmptyString + └─ Predicate refinement failure + └─ Expected a non empty string, actual ""` + ) + }) + + it("validation can be disabled", () => { + const schema = S.Struct({ a: S.NonEmptyString }) + deepStrictEqual(schema.make({ a: "" }, true), { a: "" }) + deepStrictEqual(schema.make({ a: "" }, { disableValidation: true }), { a: "" }) + }) + + it("should support defaults", () => { + const schema = S.Struct({ + a: S.propertySignature(S.Number).pipe(S.withConstructorDefault(() => 0)) + }) + deepStrictEqual(schema.make({}), { a: 0 }) + deepStrictEqual(schema.make({ a: 1 }), { a: 1 }) + }) + + it("should support lazy defaults", () => { + let i = 0 + const schema = S.Struct({ + a: S.propertySignature(S.Number).pipe(S.withConstructorDefault(() => ++i)) + }) + deepStrictEqual(schema.make({}), { a: 1 }) + deepStrictEqual(schema.make({}), { a: 2 }) + schema.make({ a: 10 }) + deepStrictEqual(schema.make({}), { a: 3 }) + }) + + it("should treat `undefined` as missing field", () => { + const schema = S.Struct({ + a: S.propertySignature(S.UndefinedOr(S.Number)).pipe(S.withConstructorDefault(() => 0)) + }) + deepStrictEqual(schema.make({}), { a: 0 }) + deepStrictEqual(schema.make({ a: undefined }), { a: 0 }) + }) + + it("should accept void if the struct has no fields", () => { + const schema = S.Struct({}) + deepStrictEqual(schema.make({}), {}) + deepStrictEqual(schema.make(undefined), {}) + deepStrictEqual(schema.make(undefined, true), {}) + deepStrictEqual(schema.make(undefined, false), {}) + deepStrictEqual(schema.make(), {}) + }) + + it("should accept void if the Class has all the fields with a default", () => { + const schema = S.Struct({ + a: S.propertySignature(S.Number).pipe(S.withConstructorDefault(() => 0)) + }) + deepStrictEqual(schema.make({}), { a: 0 }) + deepStrictEqual(schema.make(undefined), { a: 0 }) + deepStrictEqual(schema.make(undefined, true), { a: 0 }) + deepStrictEqual(schema.make(undefined, false), { a: 0 }) + deepStrictEqual(schema.make(), { a: 0 }) + }) + + it("props declarations with defaults (data last)", () => { + const b = Symbol.for("b") + const schema = S.Struct({ + a: S.String.pipe(S.propertySignature, S.withConstructorDefault(() => "")), + [b]: S.Number.pipe(S.propertySignature, S.withConstructorDefault(() => 0)) + }) + Util.assertions.make.succeed(schema, { a: "a", [b]: 2 }) + Util.assertions.make.succeed(schema, { a: "a" }, { a: "a", [b]: 0 }) + Util.assertions.make.succeed(schema, { [b]: 2 }, { a: "", [b]: 2 }) + Util.assertions.make.succeed(schema, {}, { a: "", [b]: 0 }) + }) + + it("props declarations with defaults (data first)", () => { + const b = Symbol.for("b") + const schema = S.Struct({ + a: S.withConstructorDefault(S.propertySignature(S.String), () => ""), + [b]: S.withConstructorDefault(S.propertySignature(S.Number), () => 0) + }) + Util.assertions.make.succeed(schema, { a: "a", [b]: 2 }) + Util.assertions.make.succeed(schema, { a: "a" }, { a: "a", [b]: 0 }) + Util.assertions.make.succeed(schema, { [b]: 2 }, { a: "", [b]: 2 }) + Util.assertions.make.succeed(schema, {}, { a: "", [b]: 0 }) + }) + + it("props transformations with defaults (data last)", () => { + const b = Symbol.for("b") + const schema = S.Struct({ + a: S.String.pipe(S.optionalWith({ default: () => "-" }), S.withConstructorDefault(() => "")), + [b]: S.Number.pipe(S.optionalWith({ default: () => -1 }), S.withConstructorDefault(() => 0)) + }) + Util.assertions.make.succeed(schema, { a: "a", [b]: 2 }) + Util.assertions.make.succeed(schema, { a: "a" }, { a: "a", [b]: 0 }) + Util.assertions.make.succeed(schema, { [b]: 2 }, { a: "", [b]: 2 }) + Util.assertions.make.succeed(schema, {}, { a: "", [b]: 0 }) + }) + + it("withConstructorDefault + withDecodingDefault", () => { + const schema = S.Struct({ + a: S.optional(S.Number).pipe(S.withDecodingDefault(() => 1), S.withConstructorDefault(() => 0)) + }) + Util.assertions.make.succeed(schema, {}, { a: 0 }) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/Struct/omit.test.ts b/repos/effect/packages/effect/test/Schema/Schema/Struct/omit.test.ts new file mode 100644 index 0000000..da5f2b9 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/Struct/omit.test.ts @@ -0,0 +1,22 @@ +import { describe, it } from "@effect/vitest" +import { deepStrictEqual } from "@effect/vitest/utils" +import * as S from "effect/Schema" + +describe("omit", () => { + it("should work", () => { + const schema = S.Struct({ a: S.String, b: S.Number, c: S.Boolean }).omit("c") + deepStrictEqual(schema.fields, { a: S.String, b: S.Number }) + }) + + it("should preserve index signatures on Struct with optionalWith default", () => { + const schema = S.Struct( + { a: S.String, b: S.optionalWith(S.Number, { default: () => 0 }) }, + S.Record({ key: S.String, value: S.Boolean }) + ) + const plain = S.Struct( + { a: S.String, b: S.Number }, + S.Record({ key: S.String, value: S.Boolean }) + ) + deepStrictEqual(schema.pipe(S.omit("a")).ast, plain.pipe(S.omit("a")).ast) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/Struct/pick.test.ts b/repos/effect/packages/effect/test/Schema/Schema/Struct/pick.test.ts new file mode 100644 index 0000000..0ad252c --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/Struct/pick.test.ts @@ -0,0 +1,10 @@ +import { describe, it } from "@effect/vitest" +import { deepStrictEqual } from "@effect/vitest/utils" +import * as S from "effect/Schema" + +describe("pick", () => { + it("should work", () => { + const schema = S.Struct({ a: S.String, b: S.Number, c: S.Boolean }).pick("a", "b") + deepStrictEqual(schema.fields, { a: S.String, b: S.Number }) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/Symbol/Symbol.test.ts b/repos/effect/packages/effect/test/Schema/Schema/Symbol/Symbol.test.ts new file mode 100644 index 0000000..6e3e8eb --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/Symbol/Symbol.test.ts @@ -0,0 +1,33 @@ +import { describe, it } from "@effect/vitest" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("Symbol", () => { + const schema = S.Symbol + + it("test roundtrip consistency", () => { + Util.assertions.testRoundtripConsistency(schema) + }) + + it("decoding", async () => { + await Util.assertions.decoding.succeed(schema, "a", Symbol.for("a")) + await Util.assertions.decoding.fail( + schema, + null, + `Symbol +└─ Encoded side transformation failure + └─ Expected string, actual null` + ) + }) + + it("encoding", async () => { + await Util.assertions.encoding.succeed(schema, Symbol.for("a"), "a") + await Util.assertions.encoding.fail( + schema, + Symbol(), + `Symbol +└─ Transformation process failure + └─ Unable to encode a unique symbol Symbol() into a string` + ) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/Symbol/SymbolFromSelf.test.ts b/repos/effect/packages/effect/test/Schema/Schema/Symbol/SymbolFromSelf.test.ts new file mode 100644 index 0000000..2e23e9a --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/Symbol/SymbolFromSelf.test.ts @@ -0,0 +1,21 @@ +import { describe, it } from "@effect/vitest" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("SymbolFromSelf", () => { + const schema = S.SymbolFromSelf + it("decoding", async () => { + const a = Symbol.for("effect/Schema/test/a") + await Util.assertions.decoding.succeed(schema, a) + await Util.assertions.decoding.fail( + schema, + null, + `Expected symbol, actual null` + ) + }) + + it("encoding", async () => { + const a = Symbol.for("effect/Schema/test/a") + await Util.assertions.encoding.succeed(schema, a, a) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/TaggedStruct/make.test.ts b/repos/effect/packages/effect/test/Schema/Schema/TaggedStruct/make.test.ts new file mode 100644 index 0000000..dcfe484 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/TaggedStruct/make.test.ts @@ -0,0 +1,26 @@ +import { describe, it } from "@effect/vitest" +import { deepStrictEqual } from "@effect/vitest/utils" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("make", () => { + it("tag should be optional", () => { + const schema = S.TaggedStruct("A", { value: S.String }) + deepStrictEqual(schema.make({ value: "a" }), { _tag: "A", value: "a" }) + }) + + it("should support empty fields", () => { + const schema = S.TaggedStruct("A", {}) + deepStrictEqual(schema.make({}), { _tag: "A" }) + }) + + it("should expose the fields", () => { + const schema = S.TaggedStruct("A", { value: S.String }) + Util.expectFields(schema.fields, { _tag: S.tag("A"), value: S.String }) + }) + + it("should support multiple tags", () => { + const schema = S.TaggedStruct("A", { category: S.tag("B"), value: S.String }) + deepStrictEqual(schema.make({ value: "a" }), { _tag: "A", category: "B", value: "a" }) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/TemplateLiteral/TemplateLiteral.test.ts b/repos/effect/packages/effect/test/Schema/Schema/TemplateLiteral/TemplateLiteral.test.ts new file mode 100644 index 0000000..36b7974 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/TemplateLiteral/TemplateLiteral.test.ts @@ -0,0 +1,475 @@ +import { describe, it } from "@effect/vitest" +import { deepStrictEqual, strictEqual, throws } from "@effect/vitest/utils" +import * as A from "effect/Arbitrary" +import type * as array_ from "effect/Array" +import * as fc from "effect/FastCheck" +import * as S from "effect/Schema" +import * as AST from "effect/SchemaAST" +import * as Util from "../../TestUtils.js" + +type TemplateLiteralParameter = S.Schema.AnyNoContext | AST.LiteralValue + +const expectPattern = ( + params: array_.NonEmptyReadonlyArray, + expectedPattern: string +) => { + const ast = S.TemplateLiteral(...params).ast as AST.TemplateLiteral + strictEqual(AST.getTemplateLiteralRegExp(ast).source, expectedPattern) +} + +const expectAST = ( + params: array_.NonEmptyReadonlyArray, + expectedAST: AST.TemplateLiteral, + expectedString: string +) => { + const ast = S.TemplateLiteral(...params).ast + deepStrictEqual(ast, expectedAST) + strictEqual(String(ast), expectedString) +} + +const expectProperty = ( + schema: S.Schema, + params?: fc.Parameters<[A]> +) => { + if (false as boolean) { + const arb = A.make(schema) + const is = S.is(schema) + fc.assert(fc.property(arb, (i) => is(i)), params) + } +} + +describe("TemplateLiteral", () => { + it("should throw on unsupported template literal spans", () => { + throws( + () => S.TemplateLiteral(S.Boolean), + new Error(`Unsupported template literal span +schema (BooleanKeyword): boolean`) + ) + + throws( + () => S.TemplateLiteral(S.Union(S.Boolean, S.SymbolFromSelf)), + new Error(`Unsupported template literal span +schema (Union): boolean | symbol`) + ) + }) + + it("getTemplateLiteralRegExp", () => { + expectPattern(["a"], "^a$") + expectPattern(["a", "b"], "^ab$") + expectPattern([S.Literal("a", "b"), "c"], "^(a|b)c$") + expectPattern([S.Literal("a", "b"), "c", S.Literal("d", "e")], "^(a|b)c(d|e)$") + expectPattern([S.Literal("a", "b"), S.String, S.Literal("d", "e")], "^(a|b)[\\s\\S]*?(d|e)$") + expectPattern(["a", S.String], "^a[\\s\\S]*?$") + expectPattern(["a", S.String, "b"], "^a[\\s\\S]*?b$") + expectPattern( + ["a", S.String, "b", S.Number], + "^a[\\s\\S]*?b[+-]?\\d*\\.?\\d+(?:[Ee][+-]?\\d+)?$" + ) + expectPattern(["a", S.Number], "^a[+-]?\\d*\\.?\\d+(?:[Ee][+-]?\\d+)?$") + expectPattern([S.String, "a"], "^[\\s\\S]*?a$") + expectPattern([S.Number, "a"], "^[+-]?\\d*\\.?\\d+(?:[Ee][+-]?\\d+)?a$") + expectPattern( + [S.Union(S.String, S.Literal(1)), S.Union(S.Number, S.Literal(true))], + "^([\\s\\S]*?|1)([+-]?\\d*\\.?\\d+(?:[Ee][+-]?\\d+)?|true)$" + ) + expectPattern([S.Union(S.Literal("a", "b"), S.Literal(1, 2))], "^(a|b|1|2)$") + expectPattern(["c", S.Union(S.TemplateLiteral("a", S.String, "b"), S.Literal("e")), "d"], "^c(a[\\s\\S]*?b|e)d$") + expectPattern(["<", S.TemplateLiteral("h", S.Literal(1, 2)), ">"], "^$") + expectPattern( + ["-", S.Union(S.TemplateLiteral("a", S.Literal("b", "c")), S.TemplateLiteral("d", S.Literal("e", "f")))], + "^-(a(b|c)|d(e|f))$" + ) + }) + + describe("AST and toString", () => { + it(`"a"`, () => { + const expectedAST = new AST.TemplateLiteral("", [new AST.TemplateLiteralSpan(S.Literal("a").ast, "")]) + const expectedString = "`a`" + expectAST([S.Literal("a")], expectedAST, expectedString) + expectAST(["a"], expectedAST, expectedString) + }) + + it(`"a" + "b"`, () => { + const expectedAST = new AST.TemplateLiteral("a", [new AST.TemplateLiteralSpan(S.Literal("b").ast, "")]) + const expectedString = "`ab`" + expectAST([S.Literal("a"), S.Literal("b")], expectedAST, expectedString) + expectAST(["a", "b"], expectedAST, expectedString) + }) + + it(`("a" | "b") + "c"`, () => { + const expectedAST = new AST.TemplateLiteral("", [ + new AST.TemplateLiteralSpan(S.Literal("a", "b").ast, "c") + ]) + const expectedString = "`${\"a\" | \"b\"}c`" + expectAST([S.Literal("a", "b"), S.Literal("c")], expectedAST, expectedString) + expectAST([S.Literal("a", "b"), "c"], expectedAST, expectedString) + }) + + it(`("a" | "b) + "c" + ("d" | "e")`, () => { + const expectedAST = new AST.TemplateLiteral("", [ + new AST.TemplateLiteralSpan(S.Literal("a", "b").ast, "c"), + new AST.TemplateLiteralSpan(S.Literal("d", "e").ast, "") + ]) + const expectedString = "`${\"a\" | \"b\"}c${\"d\" | \"e\"}`" + expectAST([S.Literal("a", "b"), S.Literal("c"), S.Literal("d", "e")], expectedAST, expectedString) + expectAST([S.Literal("a", "b"), "c", S.Literal("d", "e")], expectedAST, expectedString) + }) + + it(`("a" | "b") + string + ("d" | "e")`, () => { + const expectedAST = new AST.TemplateLiteral("", [ + new AST.TemplateLiteralSpan(S.Literal("a", "b").ast, ""), + new AST.TemplateLiteralSpan(S.String.ast, ""), + new AST.TemplateLiteralSpan(S.Literal("d", "e").ast, "") + ]) + const expectedString = "`${\"a\" | \"b\"}${string}${\"d\" | \"e\"}`" + expectAST([S.Literal("a", "b"), S.String, S.Literal("d", "e")], expectedAST, expectedString) + }) + + it(`"a" + string`, () => { + const expectedAST = new AST.TemplateLiteral("a", [new AST.TemplateLiteralSpan(AST.stringKeyword, "")]) + const expectedString = "`a${string}`" + expectAST([S.Literal("a"), S.String], expectedAST, expectedString) + expectAST(["a", S.String], expectedAST, expectedString) + }) + + it(`"a" + string + "b"`, () => { + const expectedAST = new AST.TemplateLiteral("a", [ + new AST.TemplateLiteralSpan(AST.stringKeyword, "b") + ]) + const expectedString = "`a${string}b`" + expectAST([S.Literal("a"), S.String, S.Literal("b")], expectedAST, expectedString) + expectAST(["a", S.String, "b"], expectedAST, expectedString) + }) + + it(`"a" + string + "b" + number`, () => { + const expectedAST = new AST.TemplateLiteral("a", [ + new AST.TemplateLiteralSpan(AST.stringKeyword, "b"), + new AST.TemplateLiteralSpan(AST.numberKeyword, "") + ]) + const expectedString = "`a${string}b${number}`" + expectAST([S.Literal("a"), S.String, S.Literal("b"), S.Number], expectedAST, expectedString) + expectAST(["a", S.String, "b", S.Number], expectedAST, expectedString) + }) + + it(`"a" + number`, () => { + const expectedAST = new AST.TemplateLiteral("a", [new AST.TemplateLiteralSpan(AST.numberKeyword, "")]) + const expectedString = "`a${number}`" + expectAST([S.Literal("a"), S.Number], expectedAST, expectedString) + expectAST(["a", S.Number], expectedAST, expectedString) + }) + + it(`string + "a"`, () => { + const expectedAST = new AST.TemplateLiteral("", [new AST.TemplateLiteralSpan(AST.stringKeyword, "a")]) + const expectedString = "`${string}a`" + expectAST([S.String, S.Literal("a")], expectedAST, expectedString) + expectAST([S.String, "a"], expectedAST, expectedString) + }) + + it(`number + "a"`, () => { + const expectedAST = new AST.TemplateLiteral("", [new AST.TemplateLiteralSpan(AST.numberKeyword, "a")]) + const expectedString = "`${number}a`" + expectAST([S.Number, S.Literal("a")], expectedAST, expectedString) + expectAST([S.Number, "a"], expectedAST, expectedString) + }) + + it(`(string | 1) + (number | true)`, () => { + const expectedAST = new AST.TemplateLiteral("", [ + new AST.TemplateLiteralSpan(S.Union(S.String, S.Literal(1)).ast, ""), + new AST.TemplateLiteralSpan(S.Union(S.Number, S.Literal(true)).ast, "") + ]) + const expectedString = "`${string | \"1\"}${number | \"true\"}`" + expectAST([S.Union(S.String, S.Literal(1)), S.Union(S.Number, S.Literal(true))], expectedAST, expectedString) + }) + + it(`(("a" | "b") | "c")`, () => { + const expectedAST = new AST.TemplateLiteral("", [ + new AST.TemplateLiteralSpan(S.Union(S.Union(S.Literal("a"), S.Literal("b")), S.Literal("c")).ast, "") + ]) + const expectedString = "`${\"a\" | \"b\" | \"c\"}`" + expectAST([S.Union(S.Union(S.Literal("a"), S.Literal("b")), S.Literal("c"))], expectedAST, expectedString) + }) + + it("`${string}`", () => { + const expectedAST = new AST.TemplateLiteral("", [new AST.TemplateLiteralSpan(S.String.ast, "")]) + const expectedString = "`${string}`" + expectAST([S.String], expectedAST, expectedString) + }) + + it("`${number}`", () => { + const expectedAST = new AST.TemplateLiteral("", [new AST.TemplateLiteralSpan(S.Number.ast, "")]) + const expectedString = "`${number}`" + expectAST([S.Number], expectedAST, expectedString) + }) + + it("`${`${string}`}`", () => { + const schema = S.TemplateLiteral(S.String) + const expectedAST = new AST.TemplateLiteral("", [ + new AST.TemplateLiteralSpan(schema.ast, "") + ]) + const expectedString = "`${`${string}`}`" + expectAST([schema], expectedAST, expectedString) + }) + + it("`${`${string}` | \"a\"}`", () => { + const schema = S.Union(S.TemplateLiteral(S.String), S.Literal("a")) + const expectedAST = new AST.TemplateLiteral("", [ + new AST.TemplateLiteralSpan(schema.ast, "") + ]) + const expectedString = "`${`${string}` | \"a\"}`" + expectAST([schema], expectedAST, expectedString) + }) + }) + + describe("decoding", () => { + it(`"a"`, async () => { + const schema = S.TemplateLiteral("a") + expectProperty(schema) + await Util.assertions.decoding.succeed(schema, "a") + + await Util.assertions.decoding.fail(schema, "ab", `Expected \`a\`, actual "ab"`) + await Util.assertions.decoding.fail(schema, "", `Expected \`a\`, actual ""`) + await Util.assertions.decoding.fail(schema, null, `Expected \`a\`, actual null`) + }) + + it(`"a b"`, async () => { + const schema = S.TemplateLiteral("a", " ", "b") + expectProperty(schema) + await Util.assertions.decoding.succeed(schema, "a b") + + await Util.assertions.decoding.fail(schema, "a b", `Expected \`a b\`, actual "a b"`) + }) + + it(`"[" + string + "]"`, async () => { + const schema = S.TemplateLiteral("[", S.String, "]") + expectProperty(schema) + await Util.assertions.decoding.succeed(schema, "[a]") + + await Util.assertions.decoding.fail(schema, "a", "Expected `[${string}]`, actual \"a\"") + }) + + it(`"a" + string`, async () => { + const schema = S.TemplateLiteral("a", S.String) + expectProperty(schema) + await Util.assertions.decoding.succeed(schema, "a") + await Util.assertions.decoding.succeed(schema, "ab") + + await Util.assertions.decoding.fail( + schema, + null, + "Expected `a${string}`, actual null" + ) + await Util.assertions.decoding.fail( + schema, + "", + "Expected `a${string}`, actual \"\"" + ) + }) + + it(`"a" + number`, async () => { + const schema = S.TemplateLiteral("a", S.Number) + expectProperty(schema) + await Util.assertions.decoding.succeed(schema, "a1") + await Util.assertions.decoding.succeed(schema, "a1.2") + + await Util.assertions.decoding.succeed(schema, "a-1.401298464324817e-45") + await Util.assertions.decoding.succeed(schema, "a1.401298464324817e-45") + await Util.assertions.decoding.succeed(schema, "a+1.401298464324817e-45") + await Util.assertions.decoding.succeed(schema, "a-1.401298464324817e+45") + await Util.assertions.decoding.succeed(schema, "a1.401298464324817e+45") + await Util.assertions.decoding.succeed(schema, "a+1.401298464324817e+45") + + await Util.assertions.decoding.succeed(schema, "a-1.401298464324817E-45") + await Util.assertions.decoding.succeed(schema, "a1.401298464324817E-45") + await Util.assertions.decoding.succeed(schema, "a+1.401298464324817E-45") + await Util.assertions.decoding.succeed(schema, "a-1.401298464324817E+45") + await Util.assertions.decoding.succeed(schema, "a1.401298464324817E+45") + await Util.assertions.decoding.succeed(schema, "a+1.401298464324817E+45") + + await Util.assertions.decoding.fail( + schema, + null, + "Expected `a${number}`, actual null" + ) + await Util.assertions.decoding.fail( + schema, + "", + "Expected `a${number}`, actual \"\"" + ) + await Util.assertions.decoding.fail( + schema, + "aa", + "Expected `a${number}`, actual \"aa\"" + ) + }) + + it(`string`, async () => { + const schema = S.TemplateLiteral(S.String) + expectProperty(schema) + await Util.assertions.decoding.succeed(schema, "a") + await Util.assertions.decoding.succeed(schema, "ab") + await Util.assertions.decoding.succeed(schema, "") + await Util.assertions.decoding.succeed(schema, "\n") + await Util.assertions.decoding.succeed(schema, "\r") + await Util.assertions.decoding.succeed(schema, "\r\n") + await Util.assertions.decoding.succeed(schema, "\t") + }) + + it(`\\n + string`, async () => { + const schema = S.TemplateLiteral("\n", S.String) + expectProperty(schema) + await Util.assertions.decoding.succeed(schema, "\n") + await Util.assertions.decoding.succeed(schema, "\na") + await Util.assertions.decoding.fail( + schema, + "a", + "Expected `\n${string}`, actual \"a\"" + ) + }) + + it(`a\\nb + string`, async () => { + const schema = S.TemplateLiteral("a\nb ", S.String) + expectProperty(schema) + await Util.assertions.decoding.succeed(schema, "a\nb ") + await Util.assertions.decoding.succeed(schema, "a\nb c") + }) + + it(`"a" + string + "b"`, async () => { + const schema = S.TemplateLiteral("a", S.String, "b") + expectProperty(schema) + await Util.assertions.decoding.succeed(schema, "ab") + await Util.assertions.decoding.succeed(schema, "acb") + await Util.assertions.decoding.succeed(schema, "abb") + await Util.assertions.decoding.fail( + schema, + "", + "Expected `a${string}b`, actual \"\"" + ) + await Util.assertions.decoding.fail( + schema, + "a", + "Expected `a${string}b`, actual \"a\"" + ) + await Util.assertions.decoding.fail( + schema, + "b", + "Expected `a${string}b`, actual \"b\"" + ) + await Util.assertions.encoding.succeed(schema, "acb", "acb") + }) + + it(`"a" + string + "b" + string`, async () => { + const schema = S.TemplateLiteral("a", S.String, "b", S.String) + expectProperty(schema) + await Util.assertions.decoding.succeed(schema, "ab") + await Util.assertions.decoding.succeed(schema, "acb") + await Util.assertions.decoding.succeed(schema, "acbd") + + await Util.assertions.decoding.fail( + schema, + "a", + "Expected `a${string}b${string}`, actual \"a\"" + ) + await Util.assertions.decoding.fail( + schema, + "b", + "Expected `a${string}b${string}`, actual \"b\"" + ) + }) + + it("https://www.typescriptlang.org/docs/handbook/2/template-literal-types.html", async () => { + const EmailLocaleIDs = S.Literal("welcome_email", "email_heading") + const FooterLocaleIDs = S.Literal("footer_title", "footer_sendoff") + const schema = S.TemplateLiteral(S.Union(EmailLocaleIDs, FooterLocaleIDs), "_id") + expectProperty(schema) + await Util.assertions.decoding.succeed(schema, "welcome_email_id") + await Util.assertions.decoding.succeed(schema, "email_heading_id") + await Util.assertions.decoding.succeed(schema, "footer_title_id") + await Util.assertions.decoding.succeed(schema, "footer_sendoff_id") + + await Util.assertions.decoding.fail( + schema, + "_id", + `Expected \`\${"welcome_email" | "email_heading" | "footer_title" | "footer_sendoff"}_id\`, actual "_id"` + ) + }) + + it(`string + 0`, async () => { + const schema = S.TemplateLiteral(S.String, 0) + expectProperty(schema) + await Util.assertions.decoding.succeed(schema, "a0") + await Util.assertions.decoding.fail(schema, "a", "Expected `${string}0`, actual \"a\"") + }) + + it(`string + true`, async () => { + const schema = S.TemplateLiteral(S.String, true) + expectProperty(schema) + await Util.assertions.decoding.succeed(schema, "atrue") + await Util.assertions.decoding.fail(schema, "a", "Expected `${string}true`, actual \"a\"") + }) + + it(`string + null`, async () => { + const schema = S.TemplateLiteral(S.String, null) + expectProperty(schema) + await Util.assertions.decoding.succeed(schema, "anull") + await Util.assertions.decoding.fail(schema, "a", "Expected `${string}null`, actual \"a\"") + }) + + it(`string + 1n`, async () => { + const schema = S.TemplateLiteral(S.String, 1n) + expectProperty(schema) + await Util.assertions.decoding.succeed(schema, "a1") + await Util.assertions.decoding.fail(schema, "a", "Expected `${string}1`, actual \"a\"") + }) + + it(`string + ("a" | 0)`, async () => { + const schema = S.TemplateLiteral(S.String, S.Literal("a", 0)) + expectProperty(schema) + await Util.assertions.decoding.succeed(schema, "a0") + await Util.assertions.decoding.succeed(schema, "aa") + await Util.assertions.decoding.fail( + schema, + "b", + `Expected \`\${string}\${"a" | "0"}\`, actual "b"` + ) + }) + + it(`(string | 1) + (number | true)`, async () => { + const schema = S.TemplateLiteral(S.Union(S.String, S.Literal(1)), S.Union(S.Number, S.Literal(true))) + expectProperty(schema) + await Util.assertions.decoding.succeed(schema, "atrue") + await Util.assertions.decoding.succeed(schema, "-2") + await Util.assertions.decoding.succeed(schema, "10.1") + await Util.assertions.decoding.fail( + schema, + "", + `Expected \`\${string | "1"}\${number | "true"}\`, actual ""` + ) + }) + + it("`c${`a${string}b` | \"e\"}d`", async () => { + const schema = S.TemplateLiteral( + "c", + S.Union(S.TemplateLiteral("a", S.String, "b"), S.Literal("e")), + "d" + ) + expectProperty(schema) + await Util.assertions.decoding.succeed(schema, "ced") + await Util.assertions.decoding.succeed(schema, "cabd") + await Util.assertions.decoding.succeed(schema, "casbd") + await Util.assertions.decoding.succeed(schema, "ca bd") + await Util.assertions.decoding.fail( + schema, + "", + "Expected `c${`a${string}b` | \"e\"}d`, actual \"\"" + ) + }) + + it("< + h + (1|2) + >", async () => { + const schema = S.TemplateLiteral("<", S.TemplateLiteral("h", S.Literal(1, 2)), ">") + expectProperty(schema) + await Util.assertions.decoding.succeed(schema, "

") + await Util.assertions.decoding.succeed(schema, "

") + await Util.assertions.decoding.fail(schema, "

", "Expected `<${`h${\"1\" | \"2\"}`}>`, actual \"

\"") + }) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/TemplateLiteralParser.test.ts b/repos/effect/packages/effect/test/Schema/Schema/TemplateLiteralParser.test.ts new file mode 100644 index 0000000..5050694 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/TemplateLiteralParser.test.ts @@ -0,0 +1,394 @@ +import { describe, it } from "@effect/vitest" +import { deepStrictEqual, strictEqual, throws } from "@effect/vitest/utils" +import type * as array_ from "effect/Array" +import * as S from "effect/Schema" +import * as AST from "effect/SchemaAST" +import * as Util from "../TestUtils.js" + +type TemplateLiteralParameter = S.Schema.AnyNoContext | AST.LiteralValue + +const expectPattern = ( + params: array_.NonEmptyReadonlyArray, + expectedPattern: string +) => { + const ast = S.TemplateLiteral(...params).ast as AST.TemplateLiteral + strictEqual(AST.getTemplateLiteralCapturingRegExp(ast).source, expectedPattern) +} + +describe("TemplateLiteralParser", () => { + it("should throw on unsupported template literal spans", () => { + throws( + () => S.TemplateLiteralParser(S.Boolean), + new Error(`Unsupported template literal span +schema (BooleanKeyword): boolean`) + ) + + throws( + () => S.TemplateLiteralParser(S.Union(S.Boolean, S.SymbolFromSelf)), + new Error(`Unsupported template literal span +schema (Union): boolean | symbol`) + ) + }) + + it("getTemplateLiteralCapturingRegExp", () => { + expectPattern(["a"], "^(a)$") + expectPattern(["a", "b"], "^(a)(b)$") + expectPattern([S.Literal("a", "b"), "c"], "^(a|b)(c)$") + expectPattern([S.Literal("a", "b"), "c", S.Literal("d", "e")], "^(a|b)(c)(d|e)$") + expectPattern([S.Literal("a", "b"), S.String, S.Literal("d", "e")], "^(a|b)([\\s\\S]*?)(d|e)$") + expectPattern(["a", S.String], "^(a)([\\s\\S]*?)$") + expectPattern(["a", S.String, "b"], "^(a)([\\s\\S]*?)(b)$") + expectPattern( + ["a", S.String, "b", S.Number], + "^(a)([\\s\\S]*?)(b)([+-]?\\d*\\.?\\d+(?:[Ee][+-]?\\d+)?)$" + ) + expectPattern(["a", S.Number], "^(a)([+-]?\\d*\\.?\\d+(?:[Ee][+-]?\\d+)?)$") + expectPattern([S.String, "a"], "^([\\s\\S]*?)(a)$") + expectPattern([S.Number, "a"], "^([+-]?\\d*\\.?\\d+(?:[Ee][+-]?\\d+)?)(a)$") + expectPattern([ + S.Union(S.String, S.Literal(1)), + S.Union(S.Number, S.Literal(true)) + ], "^([\\s\\S]*?|1)([+-]?\\d*\\.?\\d+(?:[Ee][+-]?\\d+)?|true)$") + expectPattern([S.Union(S.Literal("a", "b"), S.Literal(1, 2))], "^(a|b|1|2)$") + expectPattern([ + "c", + S.Union(S.TemplateLiteral("a", S.String, "b"), S.Literal("e")), + "d" + ], "^(c)(a[\\s\\S]*?b|e)(d)$") + expectPattern(["<", S.TemplateLiteral("h", S.Literal(1, 2)), ">"], "^(<)(h(?:1|2))(>)$") + expectPattern( + ["-", S.Union(S.TemplateLiteral("a", S.Literal("b", "c")), S.TemplateLiteral("d", S.Literal("e", "f")))], + "^(-)(a(?:b|c)|d(?:e|f))$" + ) + }) + + it("should expose the params", () => { + const params = ["/", S.Int, "/", S.String] as const + const schema = S.TemplateLiteralParser(...params) + deepStrictEqual(schema.params, params) + }) + + describe("decoding", () => { + it(`"a"`, async () => { + const schema = S.TemplateLiteralParser("a") + + await Util.assertions.decoding.succeed(schema, "a", ["a"]) + await Util.assertions.decoding.fail( + schema, + "", + `(\`a\` <-> readonly ["a"]) +└─ Encoded side transformation failure + └─ Expected \`a\`, actual ""` + ) + + await Util.assertions.encoding.succeed(schema, ["a"], "a") + }) + + it(`"a" + "b"`, async () => { + const schema = S.TemplateLiteralParser("a", "b") + + await Util.assertions.decoding.succeed(schema, "ab", ["a", "b"]) + await Util.assertions.decoding.fail( + schema, + "a", + `(\`ab\` <-> readonly ["a", "b"]) +└─ Encoded side transformation failure + └─ Expected \`ab\`, actual "a"` + ) + + await Util.assertions.encoding.succeed(schema, ["a", "b"], "ab") + }) + + it(`Int + "a"`, async () => { + const schema = S.TemplateLiteralParser(S.Int, "a") + + await Util.assertions.decoding.succeed(schema, "1a", [1, "a"]) + await Util.assertions.decoding.fail( + schema, + "1.1a", + `(\`\${number}a\` <-> readonly [Int, "a"]) +└─ Type side transformation failure + └─ readonly [Int, "a"] + └─ [0] + └─ (NumberFromString <-> Int) + └─ Type side transformation failure + └─ Int + └─ Predicate refinement failure + └─ Expected an integer, actual 1.1` + ) + + await Util.assertions.encoding.succeed(schema, [1, "a"], "1a") + await Util.assertions.encoding.fail( + schema, + [1.1, "a"], + `(\`\${number}a\` <-> readonly [Int, "a"]) +└─ Type side transformation failure + └─ readonly [Int, "a"] + └─ [0] + └─ (NumberFromString <-> Int) + └─ Type side transformation failure + └─ Int + └─ Predicate refinement failure + └─ Expected an integer, actual 1.1` + ) + }) + + it(`NumberFromString + "a" + NonEmptyString`, async () => { + const schema = S.TemplateLiteralParser(S.NumberFromString, "a", S.NonEmptyString) + + await Util.assertions.decoding.succeed(schema, "100ab", [100, "a", "b"]) + await Util.assertions.decoding.succeed(schema, "100ab23a", [100, "a", "b23a"]) + await Util.assertions.decoding.fail( + schema, + "-ab", + `(\`\${string}a\${string}\` <-> readonly [NumberFromString, "a", NonEmptyString]) +└─ Type side transformation failure + └─ readonly [NumberFromString, "a", NonEmptyString] + └─ [0] + └─ NumberFromString + └─ Transformation process failure + └─ Unable to decode "-" into a number` + ) + + await Util.assertions.encoding.succeed(schema, [100, "a", "b"], "100ab") + await Util.assertions.encoding.fail( + schema, + [100, "a", ""], + `(\`\${string}a\${string}\` <-> readonly [NumberFromString, "a", NonEmptyString]) +└─ Type side transformation failure + └─ readonly [NumberFromString, "a", NonEmptyString] + └─ [2] + └─ NonEmptyString + └─ Predicate refinement failure + └─ Expected a non empty string, actual ""` + ) + }) + + it("1", async () => { + const schema = S.TemplateLiteralParser(1) + await Util.assertions.decoding.succeed(schema, "1", [1]) + await Util.assertions.decoding.fail( + schema, + "1a", + `(\`1\` <-> readonly [1]) +└─ Encoded side transformation failure + └─ Expected \`1\`, actual "1a"` + ) + }) + + it("Literal(1)", async () => { + const schema = S.TemplateLiteralParser(S.Literal(1)) + await Util.assertions.decoding.succeed(schema, "1", [1]) + await Util.assertions.decoding.fail( + schema, + "1a", + `(\`1\` <-> readonly [1]) +└─ Encoded side transformation failure + └─ Expected \`1\`, actual "1a"` + ) + }) + + it("1n", async () => { + const schema = S.TemplateLiteralParser(1n) + await Util.assertions.decoding.succeed(schema, "1", [1n]) + await Util.assertions.decoding.fail( + schema, + "1a", + `(\`1\` <-> readonly [1n]) +└─ Encoded side transformation failure + └─ Expected \`1\`, actual "1a"` + ) + }) + + it("Literal(1n)", async () => { + const schema = S.TemplateLiteralParser(S.Literal(1n)) + await Util.assertions.decoding.succeed(schema, "1", [1n]) + await Util.assertions.decoding.fail( + schema, + "1a", + `(\`1\` <-> readonly [1n]) +└─ Encoded side transformation failure + └─ Expected \`1\`, actual "1a"` + ) + }) + + it("true", async () => { + const schema = S.TemplateLiteralParser(true) + await Util.assertions.decoding.succeed(schema, "true", [true]) + await Util.assertions.decoding.fail( + schema, + "truea", + `(\`true\` <-> readonly [true]) +└─ Encoded side transformation failure + └─ Expected \`true\`, actual "truea"` + ) + }) + + it("Literal(true)", async () => { + const schema = S.TemplateLiteralParser(S.Literal(true)) + await Util.assertions.decoding.succeed(schema, "true", [true]) + await Util.assertions.decoding.fail( + schema, + "truea", + `(\`true\` <-> readonly [true]) +└─ Encoded side transformation failure + └─ Expected \`true\`, actual "truea"` + ) + }) + + it("false", async () => { + const schema = S.TemplateLiteralParser(false) + await Util.assertions.decoding.succeed(schema, "false", [false]) + await Util.assertions.decoding.fail( + schema, + "falsea", + `(\`false\` <-> readonly [false]) +└─ Encoded side transformation failure + └─ Expected \`false\`, actual "falsea"` + ) + }) + + it("Literal(false)", async () => { + const schema = S.TemplateLiteralParser(S.Literal(false)) + await Util.assertions.decoding.succeed(schema, "false", [false]) + await Util.assertions.decoding.fail( + schema, + "falsea", + `(\`false\` <-> readonly [false]) +└─ Encoded side transformation failure + └─ Expected \`false\`, actual "falsea"` + ) + }) + + it("null", async () => { + const schema = S.TemplateLiteralParser(null) + await Util.assertions.decoding.succeed(schema, "null", [null]) + await Util.assertions.decoding.fail( + schema, + "nulla", + `(\`null\` <-> readonly [null]) +└─ Encoded side transformation failure + └─ Expected \`null\`, actual "nulla"` + ) + }) + + it("Literal(null)", async () => { + const schema = S.TemplateLiteralParser(S.Literal(null)) + await Util.assertions.decoding.succeed(schema, "null", [null]) + await Util.assertions.decoding.fail( + schema, + "nulla", + `(\`null\` <-> readonly [null]) +└─ Encoded side transformation failure + └─ Expected \`null\`, actual "nulla"` + ) + }) + + it("1 | 2", async () => { + const schema = S.TemplateLiteralParser(S.Literal(1, 2)) + await Util.assertions.decoding.succeed(schema, "1", [1]) + await Util.assertions.decoding.succeed(schema, "2", [2]) + }) + + it(`"h" + (1 | 2 | 3)`, async () => { + const schema = S.TemplateLiteralParser("h", S.Literal(1, 2, 3)) + await Util.assertions.decoding.succeed(schema, "h1", ["h", 1]) + }) + + it(`"c" + (\`a\${string}b\`|"e") + "d"`, async () => { + const schema = S.TemplateLiteralParser( + "c", + S.Union(S.TemplateLiteralParser("a", S.NonEmptyString, "b"), S.Literal("e")), + "d" + ) + await Util.assertions.decoding.succeed(schema, "ca bd", ["c", ["a", " ", "b"], "d"]) + await Util.assertions.decoding.succeed(schema, "ced", ["c", "e", "d"]) + await Util.assertions.decoding.fail( + schema, + "cabd", + `(\`c\${\`a\${string}b\` | "e"}d\` <-> readonly ["c", (\`a\${string}b\` <-> readonly ["a", NonEmptyString, "b"]) | "e", "d"]) +└─ Type side transformation failure + └─ readonly ["c", (\`a\${string}b\` <-> readonly ["a", NonEmptyString, "b"]) | "e", "d"] + └─ [1] + └─ (\`a\${string}b\` <-> readonly ["a", NonEmptyString, "b"]) | "e" + ├─ (\`a\${string}b\` <-> readonly ["a", NonEmptyString, "b"]) + │ └─ Type side transformation failure + │ └─ readonly ["a", NonEmptyString, "b"] + │ └─ [1] + │ └─ NonEmptyString + │ └─ Predicate refinement failure + │ └─ Expected a non empty string, actual "" + └─ Expected "e", actual "ab"` + ) + await Util.assertions.decoding.fail( + schema, + "ed", + `(\`c\${\`a\${string}b\` | "e"}d\` <-> readonly ["c", (\`a\${string}b\` <-> readonly ["a", NonEmptyString, "b"]) | "e", "d"]) +└─ Encoded side transformation failure + └─ Expected \`c\${\`a\${string}b\` | "e"}d\`, actual "ed"` + ) + }) + + it(`"c" + (\`a\${number}b\`|"e") + "d"`, async () => { + const schema = S.TemplateLiteralParser( + "c", + S.Union(S.TemplateLiteralParser("a", S.Int, "b"), S.Literal("e")), + "d" + ) + await Util.assertions.decoding.succeed(schema, "ced", ["c", "e", "d"]) + await Util.assertions.decoding.succeed(schema, "ca1bd", ["c", ["a", 1, "b"], "d"]) + await Util.assertions.decoding.fail( + schema, + "ca1.1bd", + `(\`c\${\`a\${number}b\` | "e"}d\` <-> readonly ["c", (\`a\${number}b\` <-> readonly ["a", Int, "b"]) | "e", "d"]) +└─ Type side transformation failure + └─ readonly ["c", (\`a\${number}b\` <-> readonly ["a", Int, "b"]) | "e", "d"] + └─ [1] + └─ (\`a\${number}b\` <-> readonly ["a", Int, "b"]) | "e" + ├─ (\`a\${number}b\` <-> readonly ["a", Int, "b"]) + │ └─ Type side transformation failure + │ └─ readonly ["a", Int, "b"] + │ └─ [1] + │ └─ (NumberFromString <-> Int) + │ └─ Type side transformation failure + │ └─ Int + │ └─ Predicate refinement failure + │ └─ Expected an integer, actual 1.1 + └─ Expected "e", actual "a1.1b"` + ) + await Util.assertions.decoding.fail( + schema, + "ca-bd", + `(\`c\${\`a\${number}b\` | "e"}d\` <-> readonly ["c", (\`a\${number}b\` <-> readonly ["a", Int, "b"]) | "e", "d"]) +└─ Encoded side transformation failure + └─ Expected \`c\${\`a\${number}b\` | "e"}d\`, actual "ca-bd"` + ) + }) + + it("(`<${`h${\"1\" | \"2\"}`}>` <-> readonly [\"<\", `h${\"1\" | \"2\"}`, \">\"])", async () => { + const schema = S.TemplateLiteralParser("<", S.TemplateLiteral("h", S.Literal(1, 2)), ">") + await Util.assertions.decoding.succeed(schema, "

", ["<", "h1", ">"]) + await Util.assertions.decoding.succeed(schema, "

", ["<", "h2", ">"]) + await Util.assertions.decoding.fail( + schema, + "

", + `(\`<\${\`h\${"1" | "2"}\`}>\` <-> readonly ["<", \`h\${"1" | "2"}\`, ">"]) +└─ Encoded side transformation failure + └─ Expected \`<\${\`h\${"1" | "2"}\`}>\`, actual "

"` + ) + }) + + it("(`<${`h${\"1\" | \"2\"}`}>` <-> readonly [\"<\", `h${\"1\" | \"2\"}`, \">\"])", async () => { + const schema = S.TemplateLiteralParser("<", S.TemplateLiteralParser("h", S.Literal(1, 2)), ">") + await Util.assertions.decoding.succeed(schema, "

", ["<", ["h", 1], ">"]) + await Util.assertions.decoding.succeed(schema, "

", ["<", ["h", 2], ">"]) + await Util.assertions.decoding.fail( + schema, + "

", + `(\`<\${\`h\${"1" | "2"}\`}>\` <-> readonly ["<", (\`h\${"1" | "2"}\` <-> readonly ["h", 1 | 2]), ">"]) +└─ Encoded side transformation failure + └─ Expected \`<\${\`h\${"1" | "2"}\`}>\`, actual "

"` + ) + }) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/Trimmed/Trimmed.test.ts b/repos/effect/packages/effect/test/Schema/Schema/Trimmed/Trimmed.test.ts new file mode 100644 index 0000000..c29d69d --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/Trimmed/Trimmed.test.ts @@ -0,0 +1,108 @@ +import { describe, it } from "@effect/vitest" +import { assertFalse, assertTrue } from "@effect/vitest/utils" +import { Option, Predicate } from "effect" +import * as P from "effect/ParseResult" +import * as S from "effect/Schema" +import * as AST from "effect/SchemaAST" +import * as Util from "../../TestUtils.js" + +describe("Trimmed", () => { + const schema = S.Trimmed + + it("pattern in JSONSchemaAnnotation", () => { + const annotation = AST.getJSONSchemaAnnotation(schema.ast) + if (Option.isSome(annotation) && "pattern" in annotation.value && Predicate.isString(annotation.value.pattern)) { + const regexp = new RegExp(annotation.value.pattern) + const is = (s: string) => regexp.test(s) + assertTrue(is("hello")) + assertFalse(is(" hello")) + assertFalse(is("hello ")) + assertFalse(is(" hello ")) + assertTrue(is("h")) + assertFalse(is(" a b")) + assertFalse(is("a b ")) + assertTrue(is("a b")) + assertTrue(is("a b")) + assertTrue(is("")) + assertFalse(is("\n")) + assertTrue(is("a\nb")) + assertFalse(is("a\nb ")) + assertFalse(is(" a\nb")) + } + }) + + it("test roundtrip consistency", () => { + Util.assertions.testRoundtripConsistency(schema) + }) + + it("is", () => { + const is = P.is(schema) + assertTrue(is("a")) + assertTrue(is("")) + assertFalse(is("a ")) + assertFalse(is(" a")) + assertFalse(is(" a ")) + assertFalse(is(" ")) + assertFalse(is("\n")) + assertTrue(is("a\nb")) + assertFalse(is("a\nb ")) + assertFalse(is(" a\nb")) + }) + + it("decoding", async () => { + await Util.assertions.decoding.succeed(schema, "a") + await Util.assertions.decoding.succeed(schema, "") + await Util.assertions.decoding.fail( + schema, + "a ", + `Trimmed +└─ Predicate refinement failure + └─ Expected a string with no leading or trailing whitespace, actual "a "` + ) + await Util.assertions.decoding.fail( + schema, + " a", + `Trimmed +└─ Predicate refinement failure + └─ Expected a string with no leading or trailing whitespace, actual " a"` + ) + await Util.assertions.decoding.fail( + schema, + " a ", + `Trimmed +└─ Predicate refinement failure + └─ Expected a string with no leading or trailing whitespace, actual " a "` + ) + }) + + it("encoding", async () => { + await Util.assertions.encoding.succeed(schema, "a", "a") + await Util.assertions.encoding.succeed(schema, "", "") + await Util.assertions.encoding.fail( + schema, + "a ", + `Trimmed +└─ Predicate refinement failure + └─ Expected a string with no leading or trailing whitespace, actual "a "` + ) + await Util.assertions.encoding.fail( + schema, + " a", + `Trimmed +└─ Predicate refinement failure + └─ Expected a string with no leading or trailing whitespace, actual " a"` + ) + await Util.assertions.encoding.fail( + schema, + " a ", + `Trimmed +└─ Predicate refinement failure + └─ Expected a string with no leading or trailing whitespace, actual " a "` + ) + }) + + it("pretty", () => { + Util.assertions.pretty(schema, "a", `"a"`) + Util.assertions.pretty(schema, "", `""`) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/Tuple/Tuple.test.ts b/repos/effect/packages/effect/test/Schema/Schema/Tuple/Tuple.test.ts new file mode 100644 index 0000000..bc5a5f4 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/Tuple/Tuple.test.ts @@ -0,0 +1,494 @@ +import { describe, it } from "@effect/vitest" +import { deepStrictEqual } from "@effect/vitest/utils" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("Tuple", () => { + it("should expose the elements", () => { + const schema = S.Tuple(S.String, S.Number) + deepStrictEqual(schema.elements, [S.String, S.Number]) + }) + + describe("decoding", () => { + it("should use annotations to generate a more informative error message when an incorrect data type is provided", async () => { + const schema = S.Tuple().annotations({ identifier: "MyDataType" }) + await Util.assertions.decoding.fail( + schema, + null, + `Expected MyDataType, actual null` + ) + }) + + it("empty", async () => { + const schema = S.Tuple() + await Util.assertions.decoding.succeed(schema, []) + + await Util.assertions.decoding.fail( + schema, + null, + `Expected readonly [], actual null` + ) + await Util.assertions.decoding.fail( + schema, + {}, + `Expected readonly [], actual {}` + ) + await Util.assertions.decoding.fail( + schema, + [undefined], + `readonly [] +└─ [0] + └─ is unexpected, expected: never` + ) + await Util.assertions.decoding.fail( + schema, + [1], + `readonly [] +└─ [0] + └─ is unexpected, expected: never` + ) + }) + + it("element", async () => { + const schema = S.Tuple(S.Number) + await Util.assertions.decoding.succeed(schema, [1]) + + await Util.assertions.decoding.fail( + schema, + null, + `Expected readonly [number], actual null` + ) + await Util.assertions.decoding.fail( + schema, + [], + `readonly [number] +└─ [0] + └─ is missing` + ) + await Util.assertions.decoding.fail( + schema, + [undefined], + `readonly [number] +└─ [0] + └─ Expected number, actual undefined` + ) + await Util.assertions.decoding.fail( + schema, + ["a"], + `readonly [number] +└─ [0] + └─ Expected number, actual "a"` + ) + await Util.assertions.decoding.fail( + schema, + [1, "b"], + `readonly [number] +└─ [1] + └─ is unexpected, expected: 0` + ) + }) + + it("element with undefined", async () => { + const schema = S.Tuple(S.Union(S.Number, S.Undefined)) + await Util.assertions.decoding.succeed(schema, [1]) + await Util.assertions.decoding.succeed(schema, [undefined]) + + await Util.assertions.decoding.fail( + schema, + null, + `Expected readonly [number | undefined], actual null` + ) + await Util.assertions.decoding.fail( + schema, + [], + `readonly [number | undefined] +└─ [0] + └─ is missing` + ) + await Util.assertions.decoding.fail( + schema, + ["a"], + `readonly [number | undefined] +└─ [0] + └─ number | undefined + ├─ Expected number, actual "a" + └─ Expected undefined, actual "a"` + ) + await Util.assertions.decoding.fail( + schema, + [1, "b"], + `readonly [number | undefined] +└─ [1] + └─ is unexpected, expected: 0` + ) + }) + + it("optional element", async () => { + const schema = S.Tuple(S.optionalElement(S.Number)) + await Util.assertions.decoding.succeed(schema, []) + await Util.assertions.decoding.succeed(schema, [1]) + + await Util.assertions.decoding.fail( + schema, + null, + `Expected readonly [number?], actual null` + ) + await Util.assertions.decoding.fail( + schema, + ["a"], + `readonly [number?] +└─ [0] + └─ Expected number, actual "a"` + ) + await Util.assertions.decoding.fail( + schema, + [1, "b"], + `readonly [number?] +└─ [1] + └─ is unexpected, expected: 0` + ) + }) + + it("optional element with undefined", async () => { + const schema = S.Tuple(S.optionalElement(S.Union(S.Number, S.Undefined))) + await Util.assertions.decoding.succeed(schema, []) + await Util.assertions.decoding.succeed(schema, [1]) + await Util.assertions.decoding.succeed(schema, [undefined]) + + await Util.assertions.decoding.fail( + schema, + null, + `Expected readonly [number | undefined?], actual null` + ) + await Util.assertions.decoding.fail( + schema, + ["a"], + `readonly [number | undefined?] +└─ [0] + └─ number | undefined + ├─ Expected number, actual "a" + └─ Expected undefined, actual "a"` + ) + await Util.assertions.decoding.fail( + schema, + [1, "b"], + `readonly [number | undefined?] +└─ [1] + └─ is unexpected, expected: 0` + ) + }) + + it("element / optional element", async () => { + const schema = S.Tuple(S.String, S.optionalElement(S.Number)) + await Util.assertions.decoding.succeed(schema, ["a"]) + await Util.assertions.decoding.succeed(schema, ["a", 1]) + + await Util.assertions.decoding.fail( + schema, + [1], + `readonly [string, number?] +└─ [0] + └─ Expected string, actual 1` + ) + await Util.assertions.decoding.fail( + schema, + ["a", "b"], + `readonly [string, number?] +└─ [1] + └─ Expected number, actual "b"` + ) + }) + + it("e + r", async () => { + const schema = S.Tuple([S.String], S.Number) + await Util.assertions.decoding.succeed(schema, ["a"]) + await Util.assertions.decoding.succeed(schema, ["a", 1]) + await Util.assertions.decoding.succeed(schema, ["a", 1, 2]) + + await Util.assertions.decoding.fail( + schema, + [], + `readonly [string, ...number[]] +└─ [0] + └─ is missing` + ) + }) + + it("e? + r", async () => { + const schema = S.Tuple([S.optionalElement(S.String)], S.Number) + await Util.assertions.decoding.succeed(schema, []) + await Util.assertions.decoding.succeed(schema, ["a"]) + await Util.assertions.decoding.succeed(schema, ["a", 1]) + await Util.assertions.decoding.succeed(schema, ["a", 1, 2]) + + await Util.assertions.decoding.fail( + schema, + [1], + `readonly [string?, ...number[]] +└─ [0] + └─ Expected string, actual 1` + ) + }) + + it("rest", async () => { + const schema = S.Array(S.Number) + await Util.assertions.decoding.succeed(schema, []) + await Util.assertions.decoding.succeed(schema, [1]) + await Util.assertions.decoding.succeed(schema, [1, 2]) + + await Util.assertions.decoding.fail( + schema, + ["a"], + `ReadonlyArray +└─ [0] + └─ Expected number, actual "a"` + ) + await Util.assertions.decoding.fail( + schema, + [1, "a"], + `ReadonlyArray +└─ [1] + └─ Expected number, actual "a"` + ) + }) + + it("rest / element", async () => { + const schema = S.Tuple([], S.String, S.Number) + await Util.assertions.decoding.succeed(schema, [1]) + await Util.assertions.decoding.succeed(schema, ["a", 1]) + await Util.assertions.decoding.succeed(schema, ["a", "b", 1]) + + await Util.assertions.decoding.fail( + schema, + [], + `readonly [...string[], number] +└─ [0] + └─ is missing` + ) + await Util.assertions.decoding.fail( + schema, + ["a"], + `readonly [...string[], number] +└─ [0] + └─ Expected number, actual "a"` + ) + await Util.assertions.decoding.fail( + schema, + [1, 2], + `readonly [...string[], number] +└─ [0] + └─ Expected string, actual 1` + ) + }) + + it("element / rest / element", async () => { + const schema = S.Tuple([S.String], S.Number, S.Boolean) + await Util.assertions.decoding.succeed(schema, ["a", true]) + await Util.assertions.decoding.succeed(schema, ["a", 1, true]) + await Util.assertions.decoding.succeed(schema, ["a", 1, 2, true]) + + await Util.assertions.decoding.fail( + schema, + [], + `readonly [string, ...number[], boolean] +└─ [0] + └─ is missing` + ) + await Util.assertions.decoding.fail( + schema, + ["a"], + `readonly [string, ...number[], boolean] +└─ [1] + └─ is missing` + ) + await Util.assertions.decoding.fail( + schema, + ["a", 1], + `readonly [string, ...number[], boolean] +└─ [1] + └─ Expected boolean, actual 1` + ) + await Util.assertions.decoding.fail( + schema, + [1, true], + `readonly [string, ...number[], boolean] +└─ [0] + └─ Expected string, actual 1` + ) + await Util.assertions.decoding.fail( + schema, + [true], + `readonly [string, ...number[], boolean] +└─ [1] + └─ is missing` + ) + }) + + it("[String] + [Boolean, String, Number, Number] validates every post-rest index", async () => { + const schema = S.Tuple([S.String], S.Boolean, S.String, S.NumberFromString, S.NumberFromString) + + await Util.assertions.decoding.fail( + schema, + ["a", true, "b", "1", "x"], + `readonly [string, ...boolean[], string, NumberFromString, NumberFromString] +└─ [4] + └─ NumberFromString + └─ Transformation process failure + └─ Unable to decode "x" into a number` + ) + await Util.assertions.decoding.succeed(schema, ["a", true, "b", "1", "2"], ["a", true, "b", 1, 2]) + }) + }) + + describe("encoding", () => { + it("empty", async () => { + const schema = S.Tuple() + await Util.assertions.encoding.succeed(schema, [], []) + }) + + it("element", async () => { + const schema = S.Tuple(Util.NumberFromChar) + await Util.assertions.encoding.succeed(schema, [1], ["1"]) + await Util.assertions.encoding.fail( + schema, + [10], + `readonly [NumberFromChar] +└─ [0] + └─ NumberFromChar + └─ Encoded side transformation failure + └─ Char + └─ Predicate refinement failure + └─ Expected a single character, actual "10"` + ) + await Util.assertions.encoding.fail( + schema, + [1, "b"] as any, + `readonly [NumberFromChar] +└─ [1] + └─ is unexpected, expected: 0` + ) + }) + + it("element with undefined", async () => { + const schema = S.Tuple(S.Union(Util.NumberFromChar, S.Undefined)) + await Util.assertions.encoding.succeed(schema, [1], ["1"]) + await Util.assertions.encoding.succeed(schema, [undefined], [undefined]) + await Util.assertions.encoding.fail( + schema, + [1, "b"] as any, + `readonly [NumberFromChar | undefined] +└─ [1] + └─ is unexpected, expected: 0` + ) + }) + + it("optional element", async () => { + const schema = S.Tuple(S.optionalElement(Util.NumberFromChar)) + await Util.assertions.encoding.succeed(schema, [], []) + await Util.assertions.encoding.succeed(schema, [1], ["1"]) + await Util.assertions.encoding.fail( + schema, + [10], + `readonly [NumberFromChar?] +└─ [0] + └─ NumberFromChar + └─ Encoded side transformation failure + └─ Char + └─ Predicate refinement failure + └─ Expected a single character, actual "10"` + ) + await Util.assertions.encoding.fail( + schema, + [1, "b"] as any, + `readonly [NumberFromChar?] +└─ [1] + └─ is unexpected, expected: 0` + ) + }) + + it("optional element with undefined", async () => { + const schema = S.Tuple(S.optionalElement(S.Union(Util.NumberFromChar, S.Undefined))) + await Util.assertions.encoding.succeed(schema, [], []) + await Util.assertions.encoding.succeed(schema, [1], ["1"]) + await Util.assertions.encoding.succeed(schema, [undefined], [undefined]) + await Util.assertions.encoding.fail( + schema, + [1, "b"] as any, + `readonly [NumberFromChar | undefined?] +└─ [1] + └─ is unexpected, expected: 0` + ) + }) + + it("element / optional element", async () => { + const schema = S.Tuple(S.String, S.optionalElement(Util.NumberFromChar)) + await Util.assertions.encoding.succeed(schema, ["a"], ["a"]) + await Util.assertions.encoding.succeed(schema, ["a", 1], ["a", "1"]) + }) + + it("e + r", async () => { + const schema = S.Tuple([S.String], Util.NumberFromChar) + await Util.assertions.encoding.succeed(schema, ["a"], ["a"]) + await Util.assertions.encoding.succeed(schema, ["a", 1], ["a", "1"]) + await Util.assertions.encoding.succeed(schema, ["a", 1, 2], ["a", "1", "2"]) + }) + + it("e? + r", async () => { + const schema = S.Tuple([S.optionalElement(S.String)], Util.NumberFromChar) + await Util.assertions.encoding.succeed(schema, [], []) + await Util.assertions.encoding.succeed(schema, ["a"], ["a"]) + await Util.assertions.encoding.succeed(schema, ["a", 1], ["a", "1"]) + await Util.assertions.encoding.succeed(schema, ["a", 1, 2], ["a", "1", "2"]) + }) + + it("rest", async () => { + const schema = S.Array(Util.NumberFromChar) + await Util.assertions.encoding.succeed(schema, [], []) + await Util.assertions.encoding.succeed(schema, [1], ["1"]) + await Util.assertions.encoding.succeed(schema, [1, 2], ["1", "2"]) + await Util.assertions.encoding.fail( + schema, + [10], + `ReadonlyArray +└─ [0] + └─ NumberFromChar + └─ Encoded side transformation failure + └─ Char + └─ Predicate refinement failure + └─ Expected a single character, actual "10"` + ) + }) + + it("rest / element", async () => { + const schema = S.Tuple([], S.String, Util.NumberFromChar) + await Util.assertions.encoding.succeed(schema, [1], ["1"]) + await Util.assertions.encoding.succeed(schema, ["a", 1], ["a", "1"]) + await Util.assertions.encoding.succeed(schema, ["a", "b", 1], ["a", "b", "1"]) + await Util.assertions.encoding.fail( + schema, + [] as any, + `readonly [...string[], NumberFromChar] +└─ [0] + └─ is missing` + ) + await Util.assertions.encoding.fail( + schema, + [10], + `readonly [...string[], NumberFromChar] +└─ [0] + └─ NumberFromChar + └─ Encoded side transformation failure + └─ Char + └─ Predicate refinement failure + └─ Expected a single character, actual "10"` + ) + }) + + it("element / rest / element", async () => { + const schema = S.Tuple([S.String], Util.NumberFromChar, S.Boolean) + await Util.assertions.encoding.succeed(schema, ["a", true], ["a", true]) + await Util.assertions.encoding.succeed(schema, ["a", 1, true], ["a", "1", true]) + await Util.assertions.encoding.succeed(schema, ["a", 1, 2, true], ["a", "1", "2", true]) + }) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/ULID.test.ts b/repos/effect/packages/effect/test/Schema/Schema/ULID.test.ts new file mode 100644 index 0000000..292f62f --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/ULID.test.ts @@ -0,0 +1,21 @@ +import { describe, it } from "@effect/vitest" +import * as S from "effect/Schema" +import * as Util from "../TestUtils.js" + +describe("ULID", () => { + it("test roundtrip consistency", () => { + Util.assertions.testRoundtripConsistency(S.ULID) + }) + + it("Decoder", async () => { + const schema = S.ULID + await Util.assertions.decoding.succeed(schema, "01H4PGGGJVN2DKP2K1H7EH996V") + await Util.assertions.decoding.fail( + schema, + "", + `ULID +└─ Predicate refinement failure + └─ Expected a Universally Unique Lexicographically Sortable Identifier, actual ""` + ) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/URL/URL.test.ts b/repos/effect/packages/effect/test/Schema/Schema/URL/URL.test.ts new file mode 100644 index 0000000..1225cbe --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/URL/URL.test.ts @@ -0,0 +1,44 @@ +import { describe, it } from "@effect/vitest" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("URL", () => { + const schema = S.URL + + it("test roundtrip consistency", () => { + Util.assertions.testRoundtripConsistency(schema) + }) + + it("arbitrary", () => { + Util.assertions.arbitrary.validateGeneratedValues(S.URL) + }) + + it("decoding", async () => { + await Util.assertions.decoding.succeed( + schema, + "http://effect.website", + new URL("http://effect.website") + ) + await Util.assertions.decoding.fail( + schema, + "123", + `URL +└─ Transformation process failure + └─ Unable to decode "123" into a URL. Invalid URL` + ) + }) + + it("encoding", async () => { + await Util.assertions.encoding.succeed( + schema, + new URL("https://effecty.website"), + "https://effecty.website/" + ) + }) + + it("Pretty", () => { + const input = "https://effecty.website:443" + const prettified = "https://effecty.website/" + Util.assertions.pretty(schema, new URL(input), prettified) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/URL/URLFromSelf.test.ts b/repos/effect/packages/effect/test/Schema/Schema/URL/URLFromSelf.test.ts new file mode 100644 index 0000000..3db2fce --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/URL/URLFromSelf.test.ts @@ -0,0 +1,35 @@ +import { describe, it } from "@effect/vitest" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("URLFromSelf", () => { + const schema = S.URLFromSelf + + it("test roundtrip consistency", () => { + Util.assertions.testRoundtripConsistency(schema) + }) + + it("arbitrary", () => { + Util.assertions.arbitrary.validateGeneratedValues(S.URLFromSelf) + }) + + it("decoding", async () => { + await Util.assertions.decoding.succeed( + schema, + new URL("https://effect.website"), + new URL("https://effect.website") + ) + }) + + it("encoding", async () => { + await Util.assertions.encoding.succeed( + schema, + new URL("https://effect.website"), + new URL("https://effect.website") + ) + }) + + it("Pretty", () => { + Util.assertions.pretty(schema, new URL("https://effect.website"), `https://effect.website/`) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/UUID.test.ts b/repos/effect/packages/effect/test/Schema/Schema/UUID.test.ts new file mode 100644 index 0000000..5b193f3 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/UUID.test.ts @@ -0,0 +1,21 @@ +import { describe, it } from "@effect/vitest" +import * as S from "effect/Schema" +import * as Util from "../TestUtils.js" + +describe("string/UUID", () => { + it("test roundtrip consistency", () => { + Util.assertions.testRoundtripConsistency(S.UUID) + }) + + it("Decoder", async () => { + const schema = S.UUID + await Util.assertions.decoding.succeed(schema, "123e4567-e89b-12d3-a456-426614174000") + await Util.assertions.decoding.fail( + schema, + "", + `UUID +└─ Predicate refinement failure + └─ Expected a Universally Unique Identifier, actual ""` + ) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/Uint8Array/Uint8Array.test.ts b/repos/effect/packages/effect/test/Schema/Schema/Uint8Array/Uint8Array.test.ts new file mode 100644 index 0000000..02816d9 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/Uint8Array/Uint8Array.test.ts @@ -0,0 +1,35 @@ +import { describe, it } from "@effect/vitest" +import { assertTrue } from "@effect/vitest/utils" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("Uint8Array > Uint8Array", () => { + const schema = S.Uint8Array + + it("test roundtrip consistency", () => { + Util.assertions.testRoundtripConsistency(schema) + }) + + it("isSchema", () => { + assertTrue(S.isSchema(schema)) + }) + + it("decoding", async () => { + await Util.assertions.decoding.succeed(schema, [0, 1, 2, 3], Uint8Array.from([0, 1, 2, 3])) + await Util.assertions.decoding.fail( + schema, + [12354], + `Uint8Array +└─ Encoded side transformation failure + └─ an array of 8-bit unsigned integers to be decoded into a Uint8Array + └─ [0] + └─ Uint8 + └─ Predicate refinement failure + └─ Expected a 8-bit unsigned integer, actual 12354` + ) + }) + + it("encoding", async () => { + await Util.assertions.encoding.succeed(schema, Uint8Array.from([0, 1, 2, 3]), [0, 1, 2, 3]) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/Uint8Array/Uint8ArrayFromBase64.test.ts b/repos/effect/packages/effect/test/Schema/Schema/Uint8Array/Uint8ArrayFromBase64.test.ts new file mode 100644 index 0000000..725a940 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/Uint8Array/Uint8ArrayFromBase64.test.ts @@ -0,0 +1,49 @@ +import { describe, it } from "@effect/vitest" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("Uint8ArrayFromBase64", () => { + const schema = S.Uint8ArrayFromBase64 + const encoder = new TextEncoder() + + it("test roundtrip consistency", () => { + Util.assertions.testRoundtripConsistency(schema) + }) + + it("decoding", async () => { + await Util.assertions.decoding.succeed( + schema, + "Zm9vYmFy", + encoder.encode("foobar") + ) + await Util.assertions.decoding.fail( + schema, + "Zm9vY", + `Uint8ArrayFromBase64 +└─ Transformation process failure + └─ Length must be a multiple of 4, but is 5` + ) + await Util.assertions.decoding.fail( + schema, + "Zm9vYmF-", + `Uint8ArrayFromBase64 +└─ Transformation process failure + └─ Invalid character -` + ) + await Util.assertions.decoding.fail( + schema, + "=Zm9vYmF", + `Uint8ArrayFromBase64 +└─ Transformation process failure + └─ Found a '=' character, but it is not at the end` + ) + }) + + it("encoding", async () => { + await Util.assertions.encoding.succeed( + schema, + encoder.encode("foobar"), + "Zm9vYmFy" + ) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/Uint8Array/Uint8ArrayFromBase64Url.test.ts b/repos/effect/packages/effect/test/Schema/Schema/Uint8Array/Uint8ArrayFromBase64Url.test.ts new file mode 100644 index 0000000..167fe65 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/Uint8Array/Uint8ArrayFromBase64Url.test.ts @@ -0,0 +1,52 @@ +import { describe, it } from "@effect/vitest" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("Uint8ArrayFromBase64Url", () => { + const schema = S.Uint8ArrayFromBase64Url + const encoder = new TextEncoder() + + it("test roundtrip consistency", () => { + Util.assertions.testRoundtripConsistency(schema) + }) + + it("decoding", async () => { + await Util.assertions.decoding.succeed( + schema, + "Zm9vYmFy", + encoder.encode("foobar") + ) + await Util.assertions.decoding.succeed( + schema, + "Pj8-ZD_Dnw", + encoder.encode(">?>d?ß") + ) + await Util.assertions.decoding.fail( + schema, + "Zm9vY", + `Uint8ArrayFromBase64Url +└─ Transformation process failure + └─ Length should be a multiple of 4, but is 5` + ) + await Util.assertions.decoding.fail( + schema, + "Pj8/ZD+Dnw", + `Uint8ArrayFromBase64Url +└─ Transformation process failure + └─ Invalid input` + ) + }) + + it("encoding", async () => { + await Util.assertions.encoding.succeed( + schema, + encoder.encode("foobar"), + "Zm9vYmFy" + ) + await Util.assertions.encoding.succeed( + schema, + encoder.encode(">?>d?ß"), + "Pj8-ZD_Dnw" + ) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/Uint8Array/Uint8ArrayFromHex.test.ts b/repos/effect/packages/effect/test/Schema/Schema/Uint8Array/Uint8ArrayFromHex.test.ts new file mode 100644 index 0000000..d0975ef --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/Uint8Array/Uint8ArrayFromHex.test.ts @@ -0,0 +1,59 @@ +import { describe, it } from "@effect/vitest" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("Uint8ArrayFromHex", () => { + const schema = S.Uint8ArrayFromHex + const encoder = new TextEncoder() + + it("test roundtrip consistency", () => { + Util.assertions.testRoundtripConsistency(schema) + }) + + it("decoding", async () => { + await Util.assertions.decoding.succeed( + schema, + "0001020304050607", + Uint8Array.from([0, 1, 2, 3, 4, 5, 6, 7]) + ) + await Util.assertions.decoding.succeed( + schema, + "f0f1f2f3f4f5f6f7", + Uint8Array.from([0xf0, 0xf1, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7]) + ) + await Util.assertions.decoding.succeed( + schema, + "67", + encoder.encode("g") + ) + await Util.assertions.decoding.fail( + schema, + "0", + `Uint8ArrayFromHex +└─ Transformation process failure + └─ Length must be a multiple of 2, but is 1` + ) + await Util.assertions.decoding.fail( + schema, + "zd4aa", + `Uint8ArrayFromHex +└─ Transformation process failure + └─ Length must be a multiple of 2, but is 5` + ) + await Util.assertions.decoding.fail( + schema, + "0\x01", + `Uint8ArrayFromHex +└─ Transformation process failure + └─ Invalid input` + ) + }) + + it("encoding", async () => { + await Util.assertions.encoding.succeed( + schema, + Uint8Array.from([0, 1, 2, 3, 4, 5, 6, 7]), + "0001020304050607" + ) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/Uint8Array/Uint8ArrayFromSelf.test.ts b/repos/effect/packages/effect/test/Schema/Schema/Uint8Array/Uint8ArrayFromSelf.test.ts new file mode 100644 index 0000000..1e7cd07 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/Uint8Array/Uint8ArrayFromSelf.test.ts @@ -0,0 +1,28 @@ +import { describe, it } from "@effect/vitest" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("Uint8Array > Uint8ArrayFromSelf", () => { + it("test roundtrip consistency", () => { + Util.assertions.testRoundtripConsistency(S.Uint8ArrayFromSelf) + }) + + it("decoding", async () => { + await Util.assertions.decoding.succeed(S.Uint8ArrayFromSelf, new Uint8Array(), new Uint8Array()) + await Util.assertions.decoding.fail( + S.Uint8ArrayFromSelf, + null, + `Expected Uint8ArrayFromSelf, actual null` + ) + }) + + it("encoding", async () => { + const u8arr = Uint8Array.from([0, 1, 2, 3]) + await Util.assertions.encoding.succeed(S.Uint8ArrayFromSelf, u8arr, u8arr) + }) + + it("pretty", () => { + const schema = S.Uint8ArrayFromSelf + Util.assertions.pretty(schema, Uint8Array.from([0, 1, 2, 3]), "new Uint8Array([0,1,2,3])") + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/Union/Union.test.ts b/repos/effect/packages/effect/test/Schema/Schema/Union/Union.test.ts new file mode 100644 index 0000000..3576200 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/Union/Union.test.ts @@ -0,0 +1,313 @@ +import { describe, it } from "@effect/vitest" +import { deepStrictEqual } from "@effect/vitest/utils" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("Union", () => { + it("should expose the union members", () => { + const schema = S.Union(S.String, S.Number) + deepStrictEqual(schema.members, [S.String, S.Number]) + }) + + it("should return a `Never` schema when no members are provided", async () => { + const schema = S.Union() + await Util.assertions.decoding.fail(schema, 1, "Expected never, actual 1") + }) + + describe("decoding", () => { + it("should use identifier annotations to generate informative error messages", async () => { + const schema = S.Union( + S.Struct({ a: S.String }).annotations({ identifier: "ID1" }), + S.Struct({ a: S.String }).annotations({ identifier: "ID2" }) + ) + await Util.assertions.decoding.fail( + schema, + null, + `ID1 | ID2 +├─ Expected ID1, actual null +└─ Expected ID2, actual null` + ) + }) + + describe("discriminated unions", () => { + describe("structs", () => { + it("should handle discriminators for each struct", async () => { + const schema = S.Union( + S.Struct({ a: S.Literal(1), c: S.String }).annotations({ identifier: "ID1" }), + S.Struct({ b: S.Literal(2), d: S.Number }).annotations({ identifier: "ID2" }) + ) + await Util.assertions.decoding.fail( + schema, + null, + `Expected ID1 | ID2, actual null` + ) + await Util.assertions.decoding.fail( + schema, + {}, + `ID1 | ID2 +├─ ID1 +│ └─ ["a"] +│ └─ is missing +└─ ID2 + └─ ["b"] + └─ is missing` + ) + await Util.assertions.decoding.fail( + schema, + { a: null }, + `ID1 | ID2 +├─ ID1 +│ └─ ["a"] +│ └─ Expected 1, actual null +└─ ID2 + └─ ["b"] + └─ is missing` + ) + await Util.assertions.decoding.fail( + schema, + { b: 3 }, + `ID1 | ID2 +├─ ID1 +│ └─ ["a"] +│ └─ is missing +└─ ID2 + └─ ["b"] + └─ Expected 2, actual 3` + ) + }) + + it("should handle structs with multiple discriminators", async () => { + const schema = S.Union( + S.Struct({ category: S.Literal("catA"), tag: S.Literal("a") }).annotations({ identifier: "IDa" }), + S.Struct({ category: S.Literal("catA"), tag: S.Literal("b") }).annotations({ identifier: "IDb" }), + S.Struct({ category: S.Literal("catA"), tag: S.Literal("c") }).annotations({ identifier: "IDc" }) + ) + await Util.assertions.decoding.fail( + schema, + null, + `Expected IDa | IDb | IDc, actual null` + ) + await Util.assertions.decoding.fail( + schema, + {}, + `IDa | IDb | IDc +├─ IDa +│ └─ ["category"] +│ └─ is missing +└─ IDb | IDc + └─ ["tag"] + └─ is missing` + ) + await Util.assertions.decoding.fail( + schema, + { category: null }, + `IDa | IDb | IDc +├─ IDa +│ └─ ["category"] +│ └─ Expected "catA", actual null +└─ IDb | IDc + └─ ["tag"] + └─ is missing` + ) + await Util.assertions.decoding.fail( + schema, + { tag: "d" }, + `IDa | IDb | IDc +├─ IDa +│ └─ ["category"] +│ └─ is missing +└─ IDb | IDc + └─ ["tag"] + └─ Expected "b" | "c", actual "d"` + ) + }) + + it("should handle nested unions", async () => { + const a = S.Struct({ _tag: S.Literal("a") }).annotations({ identifier: "IDa" }) + const b = S.Struct({ _tag: S.Literal("b") }).annotations({ identifier: "IDb" }) + const A = S.Struct({ a: S.Literal("A"), c: S.String }).annotations({ identifier: "IDA" }) + const B = S.Struct({ b: S.Literal("B"), d: S.Number }).annotations({ identifier: "IDB" }) + const ab = S.Union(a, b).annotations({ identifier: "IDab" }) + const AB = S.Union(A, B).annotations({ identifier: "IDAB" }) + const schema = S.Union(ab, AB) + await Util.assertions.decoding.succeed(schema, { _tag: "a" }) + await Util.assertions.decoding.succeed(schema, { _tag: "b" }) + await Util.assertions.decoding.succeed(schema, { a: "A", c: "c" }) + await Util.assertions.decoding.succeed(schema, { b: "B", d: 1 }) + await Util.assertions.decoding.fail( + schema, + {}, + `IDab | IDAB +├─ IDab +│ └─ { readonly _tag: "a" | "b" } +│ └─ ["_tag"] +│ └─ is missing +└─ IDAB + ├─ IDA + │ └─ ["a"] + │ └─ is missing + └─ IDB + └─ ["b"] + └─ is missing` + ) + }) + }) + + describe("tuples", () => { + it("should handle discriminators for each tuple", async () => { + const schema = S.Union( + S.Tuple(S.Literal("a"), S.String), + S.Tuple(S.Literal("b"), S.Number) + ).annotations({ identifier: "ID" }) + + await Util.assertions.decoding.succeed(schema, ["a", "s"]) + await Util.assertions.decoding.succeed(schema, ["b", 1]) + + await Util.assertions.decoding.fail(schema, null, `Expected ID, actual null`) + await Util.assertions.decoding.fail( + schema, + [], + `ID +└─ { readonly 0: "a" | "b" } + └─ ["0"] + └─ is missing` + ) + await Util.assertions.decoding.fail( + schema, + ["c"], + `ID +└─ { readonly 0: "a" | "b" } + └─ ["0"] + └─ Expected "a" | "b", actual "c"` + ) + await Util.assertions.decoding.fail( + schema, + ["a", 0], + `ID +└─ readonly ["a", string] + └─ [1] + └─ Expected string, actual 0` + ) + }) + + it("should handle tuples with multiple discriminators", async () => { + const schema = S.Union( + S.Tuple(S.Literal("catA"), S.Literal("a"), S.String).annotations({ identifier: "IDa" }), + S.Tuple(S.Literal("catA"), S.Literal("b"), S.Number).annotations({ identifier: "IDb" }), + S.Tuple(S.Literal("catA"), S.Literal("c"), S.Boolean).annotations({ identifier: "IDc" }) + ).annotations({ identifier: "ID" }) + + await Util.assertions.decoding.succeed(schema, ["catA", "a", "s"]) + await Util.assertions.decoding.succeed(schema, ["catA", "b", 1]) + await Util.assertions.decoding.succeed(schema, ["catA", "c", true]) + + await Util.assertions.decoding.fail(schema, null, `Expected ID, actual null`) + await Util.assertions.decoding.fail( + schema, + [], + `ID +├─ IDa +│ └─ ["0"] +│ └─ is missing +└─ IDb | IDc + └─ ["1"] + └─ is missing` + ) + await Util.assertions.decoding.fail( + schema, + ["catB"], + `ID +├─ IDa +│ └─ ["0"] +│ └─ Expected "catA", actual "catB" +└─ IDb | IDc + └─ ["1"] + └─ is missing` + ) + await Util.assertions.decoding.fail( + schema, + ["catA", "c"], + `ID +├─ IDa +│ └─ [2] +│ └─ is missing +└─ IDc + └─ [2] + └─ is missing` + ) + await Util.assertions.decoding.fail( + schema, + ["catA", "a", 0], + `ID +├─ IDb | IDc +│ └─ ["1"] +│ └─ Expected "b" | "c", actual "a" +└─ IDa + └─ [2] + └─ Expected string, actual 0` + ) + }) + + it("should handle discriminated tuple + tuple", async () => { + const schema = S.Union( + S.Tuple(S.Literal(-1), S.Literal(0)).annotations({ identifier: "ID1" }), + S.Tuple(S.NonNegativeInt, S.NonNegativeInt).annotations({ identifier: "ID2" }) + ).annotations({ identifier: "ID" }) + + await Util.assertions.decoding.fail( + schema, + null, + `ID +├─ Expected ID1, actual null +└─ Expected ID2, actual null` + ) + }) + + it("should handle 2 discriminated tuples + a tuple", async () => { + const schema = S.Union( + S.Tuple(S.Literal(-1), S.Literal(0)).annotations({ identifier: "ID1" }), + S.Tuple(S.Literal(1), S.Literal(0)).annotations({ identifier: "ID2" }), + S.Tuple(S.NonNegativeInt, S.NonNegativeInt).annotations({ identifier: "ID3" }) + ).annotations({ identifier: "ID" }) + + await Util.assertions.decoding.fail( + schema, + [], + `ID +├─ ID1 | ID2 +│ └─ ["0"] +│ └─ is missing +└─ ID3 + └─ [0] + └─ is missing` + ) + }) + }) + }) + }) + + describe("encoding", () => { + it("should encode all members", async () => { + const schema = S.Union(S.String, Util.NumberFromChar) + await Util.assertions.encoding.succeed(schema, "a", "a") + await Util.assertions.encoding.succeed(schema, 1, "1") + }) + + it("should handle members with exact optional property signatures", async () => { + const ab = S.Struct({ a: S.String, b: S.optionalWith(S.Number, { exact: true }) }) + const ac = S.Struct({ a: S.String, c: S.optionalWith(S.Number, { exact: true }) }) + const schema = S.Union(ab, ac) + await Util.assertions.encoding.succeed( + schema, + { a: "a", c: 1 }, + { a: "a" } + ) + await Util.assertions.encoding.succeed( + schema, + { a: "a", c: 1 }, + { a: "a", c: 1 }, + { parseOptions: Util.onExcessPropertyError } + ) + }) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/UniqueSymbol/UniqueSymbolFromSelf.test.ts b/repos/effect/packages/effect/test/Schema/Schema/UniqueSymbol/UniqueSymbolFromSelf.test.ts new file mode 100644 index 0000000..88e4263 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/UniqueSymbol/UniqueSymbolFromSelf.test.ts @@ -0,0 +1,17 @@ +import { describe, it } from "@effect/vitest" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("UniqueSymbolFromSelf", () => { + const a = Symbol.for("effect/Schema/test/a") + const schema = S.UniqueSymbolFromSelf(a) + it("decoding", async () => { + await Util.assertions.decoding.succeed(schema, a) + await Util.assertions.decoding.succeed(schema, Symbol.for("effect/Schema/test/a")) + await Util.assertions.decoding.fail( + schema, + "Symbol(effect/Schema/test/a)", + `Expected Symbol(effect/Schema/test/a), actual "Symbol(effect/Schema/test/a)"` + ) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/Unknown/Unknown.test.ts b/repos/effect/packages/effect/test/Schema/Schema/Unknown/Unknown.test.ts new file mode 100644 index 0000000..9ff03a5 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/Unknown/Unknown.test.ts @@ -0,0 +1,16 @@ +import { describe, it } from "@effect/vitest" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("Unknown", () => { + const schema = S.Unknown + it("decoding", async () => { + await Util.assertions.decoding.succeed(schema, undefined) + await Util.assertions.decoding.succeed(schema, null) + await Util.assertions.decoding.succeed(schema, "a") + await Util.assertions.decoding.succeed(schema, 1) + await Util.assertions.decoding.succeed(schema, true) + await Util.assertions.decoding.succeed(schema, []) + await Util.assertions.decoding.succeed(schema, {}) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/Void/Void.test.ts b/repos/effect/packages/effect/test/Schema/Schema/Void/Void.test.ts new file mode 100644 index 0000000..2c1cd66 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/Void/Void.test.ts @@ -0,0 +1,16 @@ +import { describe, it } from "@effect/vitest" +import * as S from "effect/Schema" +import * as Util from "../../TestUtils.js" + +describe("Void", () => { + const schema = S.Void + it("decoding", async () => { + await Util.assertions.decoding.succeed(schema, undefined as any) + await Util.assertions.decoding.succeed(schema, null as any) + await Util.assertions.decoding.succeed(schema, "a" as any) + await Util.assertions.decoding.succeed(schema, 1 as any) + await Util.assertions.decoding.succeed(schema, true as any) + await Util.assertions.decoding.succeed(schema, [] as any) + await Util.assertions.decoding.succeed(schema, {} as any) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/annotations.test.ts b/repos/effect/packages/effect/test/Schema/Schema/annotations.test.ts new file mode 100644 index 0000000..e2ea167 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/annotations.test.ts @@ -0,0 +1,170 @@ +import { describe, it } from "@effect/vitest" +import { assertTrue, deepStrictEqual, strictEqual } from "@effect/vitest/utils" +import * as Option from "effect/Option" +import type * as ParseResult from "effect/ParseResult" +import type * as Pretty from "effect/Pretty" +import * as S from "effect/Schema" +import * as AST from "effect/SchemaAST" +import * as Util from "../TestUtils.js" + +describe(".annotations()", () => { + it("should return a Schema", () => { + const schema = S.String.annotations({ + [AST.TitleAnnotationId]: "MyString", + [AST.DescriptionAnnotationId]: "a string" + }) + deepStrictEqual(schema.ast.annotations, { + [AST.TitleAnnotationId]: "MyString", + [AST.DescriptionAnnotationId]: "a string" + }) + assertTrue(S.isSchema(schema)) + }) + + it("title", () => { + const schema = S.String.annotations({ title: "MyString" }) + deepStrictEqual(schema.ast.annotations, { + [AST.TitleAnnotationId]: "MyString", + [AST.DescriptionAnnotationId]: "a string" + }) + assertTrue(S.isSchema(schema)) + }) + + it("description", () => { + const schema = S.String.annotations({ description: "description" }) + deepStrictEqual(schema.ast.annotations, { + [AST.DescriptionAnnotationId]: "description", + [AST.TitleAnnotationId]: "string" + }) + assertTrue(S.isSchema(schema)) + }) + + it("examples", () => { + const schema = S.String.annotations({ examples: ["example"] }) + deepStrictEqual(schema.ast.annotations, { + [AST.ExamplesAnnotationId]: ["example"], + [AST.TitleAnnotationId]: "string", + [AST.DescriptionAnnotationId]: "a string" + }) + assertTrue(S.isSchema(schema)) + }) + + it("default", () => { + const schema = S.String.annotations({ default: "a" }) + deepStrictEqual(schema.ast.annotations, { + [AST.DefaultAnnotationId]: "a", + [AST.TitleAnnotationId]: "string", + [AST.DescriptionAnnotationId]: "a string" + }) + assertTrue(S.isSchema(schema)) + }) + + it("documentation", () => { + const schema = S.String.annotations({ documentation: "documentation" }) + deepStrictEqual(schema.ast.annotations, { + [AST.DocumentationAnnotationId]: "documentation", + [AST.TitleAnnotationId]: "string", + [AST.DescriptionAnnotationId]: "a string" + }) + assertTrue(S.isSchema(schema)) + }) + + it("concurrency", () => { + const schema = S.Struct({ a: S.String }).annotations({ concurrency: 1 }) + deepStrictEqual(schema.ast.annotations, { + [AST.ConcurrencyAnnotationId]: 1 + }) + }) + + it("batching", () => { + const schema = S.Struct({ a: S.String }).annotations({ batching: "inherit" }) + deepStrictEqual(schema.ast.annotations, { + [AST.BatchingAnnotationId]: "inherit" + }) + }) + + it("typeConstructor", () => { + const schema = S.Struct({ a: S.String }).annotations({ typeConstructor: { _tag: "MyTypeConstructor" } }) + deepStrictEqual(schema.ast.annotations, { + [AST.TypeConstructorAnnotationId]: { _tag: "MyTypeConstructor" } + }) + deepStrictEqual( + AST.getTypeConstructorAnnotation(schema.ast), + Option.some({ _tag: "MyTypeConstructor" }) + ) + }) + + it("parseIssueTitle", async () => { + const getOrderId = ({ actual }: ParseResult.ParseIssue) => { + if (S.is(S.Struct({ id: S.Number }))(actual)) { + return `Order with ID ${actual.id}` + } + } + + const Order = S.Struct({ + id: S.Number, + name: S.String, + totalPrice: S.Number + }).annotations({ + identifier: "Order", + parseIssueTitle: getOrderId + }) + + await Util.assertions.decoding.fail( + Order, + {}, + `Order +└─ ["id"] + └─ is missing` + ) + await Util.assertions.decoding.fail( + Order, + { id: 1 }, + `Order with ID 1 +└─ ["name"] + └─ is missing` + ) + }) + + it("message as annotation options", async () => { + const schema = + // initial schema, a string + S.String + // add an error message for non-string values + .annotations({ message: () => "not a string" }).pipe( + // add a constraint to the schema, only non-empty strings are valid + S.nonEmptyString({ message: () => "required" }), + // add a constraint to the schema, only strings with a length less or equal than 10 are valid + S.maxLength(10, { message: (issue) => `${issue.actual} is too long` }) + ) + + assertTrue(S.isSchema(schema)) + await Util.assertions.decoding.fail(schema, null, "not a string") + await Util.assertions.decoding.fail(schema, "", "required") + await Util.assertions.decoding.succeed(schema, "a", "a") + await Util.assertions.decoding.fail(schema, "aaaaaaaaaaaaaa", "aaaaaaaaaaaaaa is too long") + }) + + it("pretty", () => { + class A { + constructor(readonly a: string) {} + } + const prettyA = (): Pretty.Pretty => (instance) => `new A("${instance.a}")` + const schema = S.instanceOf(A, { + pretty: prettyA + }) + Util.assertions.pretty(schema, new A("value"), `new A("value")`) + }) +}) + +declare module "effect/Schema" { + namespace Annotations { + interface Schema extends Doc { + formName?: string + } + } +} + +it("should support custom annotations", () => { + const schema = S.String.annotations({ formName: "a" }) + strictEqual(schema.ast.annotations["formName"], "a") +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/asserts.test.ts b/repos/effect/packages/effect/test/Schema/Schema/asserts.test.ts new file mode 100644 index 0000000..8dda0cf --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/asserts.test.ts @@ -0,0 +1,73 @@ +import { describe, it } from "@effect/vitest" +import { assertTrue, strictEqual } from "@effect/vitest/utils" +import * as ParseResult from "effect/ParseResult" +import * as S from "effect/Schema" +import * as Util from "../TestUtils.js" + +describe("asserts", () => { + it("the returned error should be a ParseError", () => { + const asserts: (u: unknown) => asserts u is string = S.asserts(S.String) + try { + asserts(1) + } catch (e) { + assertTrue(ParseResult.isParseError(e)) + } + }) + + it("should respect outer/inner options", () => { + const schema = S.Struct({ a: Util.NumberFromChar }) + const input = { a: 1, b: "b" } + Util.assertions.parseError( + () => S.asserts(schema)(input, { onExcessProperty: "error" }), + `{ readonly a: number } +└─ ["b"] + └─ is unexpected, expected: "a"` + ) + Util.assertions.parseError( + () => S.asserts(schema, { onExcessProperty: "error" })(input), + `{ readonly a: number } +└─ ["b"] + └─ is unexpected, expected: "a"` + ) + strictEqual(S.asserts(schema, { onExcessProperty: "error" })(input, { onExcessProperty: "ignore" }), undefined) + }) + + describe("struct", () => { + it("required property signature", () => { + const schema = S.Struct({ a: Util.NumberFromChar }) + Util.assertions.asserts.succeed(schema, { a: 1 }) + Util.assertions.asserts.fail( + schema, + { a: null }, + `{ readonly a: number } +└─ ["a"] + └─ Expected number, actual null` + ) + }) + + it("required property signature with undefined", () => { + const schema = S.Struct({ a: S.Union(S.Number, S.Undefined) }) + Util.assertions.asserts.succeed(schema, { a: 1 }) + Util.assertions.asserts.succeed(schema, { a: undefined }) + Util.assertions.asserts.succeed(schema, { a: 1, b: "b" }) + + Util.assertions.asserts.fail( + schema, + {}, + `{ readonly a: number | undefined } +└─ ["a"] + └─ is missing` + ) + Util.assertions.asserts.fail(schema, null, `Expected { readonly a: number | undefined }, actual null`) + Util.assertions.asserts.fail( + schema, + { a: "a" }, + `{ readonly a: number | undefined } +└─ ["a"] + └─ number | undefined + ├─ Expected number, actual "a" + └─ Expected undefined, actual "a"` + ) + }) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/attachPropertySignature.test.ts b/repos/effect/packages/effect/test/Schema/Schema/attachPropertySignature.test.ts new file mode 100644 index 0000000..f784ac5 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/attachPropertySignature.test.ts @@ -0,0 +1,152 @@ +import { describe, it } from "@effect/vitest" +import * as ParseResult from "effect/ParseResult" +import * as S from "effect/Schema" +import * as Util from "../TestUtils.js" + +describe("attachPropertySignature", () => { + it("string keys literal values", async () => { + const Circle = S.Struct({ radius: S.Number }) + const Square = S.Struct({ sideLength: S.Number }) + const schema = S.Union( + Circle.pipe(S.attachPropertySignature("kind", "circle")), + Square.pipe(S.attachPropertySignature("kind", "square")) + ) + + await Util.assertions.decoding.succeed(schema, { radius: 10 }, { kind: "circle", radius: 10 }) + await Util.assertions.encoding.succeed(schema, { kind: "circle", radius: 10 }, { radius: 10 }) + await Util.assertions.decoding.succeed(schema, { sideLength: 10 }, { kind: "square", sideLength: 10 }) + await Util.assertions.encoding.succeed(schema, { kind: "square", sideLength: 10 }, { sideLength: 10 }) + }) + + it("symbol keys literal values", async () => { + const Circle = S.Struct({ radius: S.Number }) + const Square = S.Struct({ sideLength: S.Number }) + const kind = Symbol.for("effect/Schema/test/kind") + const schema = S.Union( + Circle.pipe(S.attachPropertySignature(kind, "circle")), + Square.pipe(S.attachPropertySignature(kind, "square")) + ) + + await Util.assertions.decoding.succeed(schema, { radius: 10 }, { [kind]: "circle", radius: 10 }) + await Util.assertions.encoding.succeed(schema, { [kind]: "circle", radius: 10 }, { radius: 10 }) + await Util.assertions.decoding.succeed(schema, { sideLength: 10 }, { [kind]: "square", sideLength: 10 }) + await Util.assertions.encoding.succeed(schema, { [kind]: "square", sideLength: 10 }, { sideLength: 10 }) + }) + + it("string keys unique symbols", async () => { + const Circle = S.Struct({ radius: S.Number }) + const Square = S.Struct({ sideLength: S.Number }) + const kind = Symbol.for("effect/Schema/test/kind") + const circle = Symbol.for("effect/Schema/test/circle") + const square = Symbol.for("effect/Schema/test/square") + const schema = S.Union( + Circle.pipe(S.attachPropertySignature(kind, circle)), + Square.pipe(S.attachPropertySignature(kind, square)) + ) + + await Util.assertions.decoding.succeed(schema, { radius: 10 }, { [kind]: circle, radius: 10 }) + await Util.assertions.encoding.succeed(schema, { [kind]: circle, radius: 10 }, { radius: 10 }) + await Util.assertions.decoding.succeed(schema, { sideLength: 10 }, { [kind]: square, sideLength: 10 }) + await Util.assertions.encoding.succeed(schema, { [kind]: square, sideLength: 10 }, { sideLength: 10 }) + }) + + it("symbol keys unique symbols", async () => { + const Circle = S.Struct({ radius: S.Number }) + const Square = S.Struct({ sideLength: S.Number }) + const circle = Symbol.for("effect/Schema/test/circle") + const square = Symbol.for("effect/Schema/test/square") + const schema = S.Union( + Circle.pipe(S.attachPropertySignature("kind", circle)), + Square.pipe(S.attachPropertySignature("kind", square)) + ) + + await Util.assertions.decoding.succeed(schema, { radius: 10 }, { kind: circle, radius: 10 }) + await Util.assertions.encoding.succeed(schema, { kind: circle, radius: 10 }, { radius: 10 }) + await Util.assertions.decoding.succeed(schema, { sideLength: 10 }, { kind: square, sideLength: 10 }) + await Util.assertions.encoding.succeed(schema, { kind: square, sideLength: 10 }, { sideLength: 10 }) + }) + + it("should be compatible with extend", async () => { + const schema = S.Struct({ a: S.String }).pipe( + S.attachPropertySignature("_tag", "b"), + S.extend(S.Struct({ c: S.Number })) + ) + await Util.assertions.decoding.succeed(schema, { a: "a", c: 1 }, { a: "a", c: 1, _tag: "b" as const }) + await Util.assertions.encoding.succeed(schema, { a: "a", c: 1, _tag: "b" as const }, { a: "a", c: 1 }) + }) + + it("with a transformation", async () => { + const From = S.Struct({ radius: S.Number, _isVisible: S.optionalWith(S.Boolean, { exact: true }) }) + const To = S.Struct({ radius: S.Number, _isVisible: S.Boolean }) + + const schema = S.transformOrFail( + From, + To, + { + strict: true, + decode: (input) => ParseResult.mapError(S.decodeUnknown(To)(input), (e) => e.issue), + encode: ({ _isVisible, ...rest }) => ParseResult.succeed(rest) + } + ).pipe( + S.attachPropertySignature("_tag", "Circle") + ) + + await Util.assertions.decoding.succeed(schema, { radius: 10, _isVisible: true }, { + _tag: "Circle" as const, + _isVisible: true, + radius: 10 + }) + await Util.assertions.encoding.succeed(schema, { + _tag: "Circle" as const, + radius: 10, + _isVisible: true + }, { + radius: 10 + }) + }) + + it("annotations", async () => { + const schema1 = S.Struct({ + a: S.String + }).pipe( + S.attachPropertySignature("_tag", "a", { identifier: "AttachedProperty" }) + ) + await Util.assertions.encoding.fail( + schema1, + null as any, + `({ readonly a: string } <-> AttachedProperty) +└─ Type side transformation failure + └─ Expected AttachedProperty, actual null` + ) + const schema2 = S.attachPropertySignature( + S.Struct({ + a: S.String + }), + "_tag", + "a", + { identifier: "AttachedProperty" } + ) + await Util.assertions.encoding.fail( + schema2, + null as any, + `({ readonly a: string } <-> AttachedProperty) +└─ Type side transformation failure + └─ Expected AttachedProperty, actual null` + ) + }) + + it("decoding error message", async () => { + const schema = S.Struct({ + a: S.String + }).pipe( + S.attachPropertySignature("_tag", "a") + ).annotations({ identifier: "AttachedProperty" }) + await Util.assertions.decoding.fail( + schema, + null, + `AttachedProperty +└─ Encoded side transformation failure + └─ Expected { readonly a: string }, actual null` + ) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/brand.test.ts b/repos/effect/packages/effect/test/Schema/Schema/brand.test.ts new file mode 100644 index 0000000..83f96bc --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/brand.test.ts @@ -0,0 +1,169 @@ +import { describe, it } from "@effect/vitest" +import { assertFalse, assertTrue, deepStrictEqual, strictEqual } from "@effect/vitest/utils" +import * as S from "effect/Schema" +import * as AST from "effect/SchemaAST" +import * as Util from "../TestUtils.js" + +describe("brand", () => { + it("toString", () => { + strictEqual(String(S.String.pipe(S.brand("my-brand"))), `string & Brand<"my-brand">`) + }) + + it("should expose the original schema as `from`", () => { + const schema = S.String.pipe(S.brand("my-brand")) + strictEqual(schema.from, S.String) + }) + + it("the constructor should validate the input by default", () => { + const schema = S.NonEmptyString.pipe(S.brand("A")) + Util.assertions.make.succeed(schema, "a") + Util.assertions.make.fail( + schema, + "", + `nonEmptyString & Brand<"A"> +└─ Predicate refinement failure + └─ Expected a non empty string, actual ""` + ) + }) + + it("the constructor validation can be disabled", () => { + const schema = S.NonEmptyString.pipe(S.brand("A")) + strictEqual(schema.make("", true), "") + strictEqual(schema.make("", { disableValidation: true }), "") + }) + + describe("annotations", () => { + it("using .annotations() twice", () => { + const schema = S.Number.pipe(S.brand("A")) + const annotatedSchema = schema.annotations({ + description: "description" + }).annotations({ title: "title" }) + deepStrictEqual(annotatedSchema.ast.annotations, { + [AST.BrandAnnotationId]: ["A"], + [AST.TitleAnnotationId]: "title", + [AST.DescriptionAnnotationId]: "description" + }) + }) + + it("using .annotations() on a BrandSchema should return a BrandSchema", () => { + const schema = S.Number.pipe( + S.int(), + S.brand("A") + ) + const annotatedSchema = schema.annotations({ + description: "description" + }).annotations({ title: "title" }) + strictEqual(typeof annotatedSchema.make, "function") + }) + + it("brand as string (1 brand)", () => { + const schema = S.Number.pipe( + S.int(), + S.brand("A", { + description: "description" + }) + ) + strictEqual(String(schema), `int & Brand<"A">`) + + deepStrictEqual(schema.ast.annotations, { + [AST.SchemaIdAnnotationId]: S.IntSchemaId, + [AST.BrandAnnotationId]: ["A"], + [AST.TitleAnnotationId]: "int", + [AST.DescriptionAnnotationId]: "description", + [AST.JSONSchemaAnnotationId]: { type: "integer" } + }) + }) + + it("brand as string (2 brands)", () => { + const schema = S.Number.pipe( + S.int(), + S.brand("A"), + S.brand("B", { + description: "description" + }) + ) + + strictEqual(String(schema), `int & Brand<"A"> & Brand<"B">`) + + deepStrictEqual(schema.ast.annotations, { + [AST.SchemaIdAnnotationId]: S.IntSchemaId, + [AST.BrandAnnotationId]: ["A", "B"], + [AST.TitleAnnotationId]: "int", + [AST.DescriptionAnnotationId]: "description", + [AST.JSONSchemaAnnotationId]: { type: "integer" } + }) + }) + + it("brand as symbol", () => { + const A = Symbol.for("A") + const B = Symbol.for("B") + const schema = S.Number.pipe( + S.int(), + S.brand(A), + S.brand(B, { + description: "description" + }) + ) + + strictEqual(String(schema), "int & Brand & Brand") + + deepStrictEqual(schema.ast.annotations, { + [AST.SchemaIdAnnotationId]: S.IntSchemaId, + [AST.BrandAnnotationId]: [A, B], + [AST.TitleAnnotationId]: "int", + [AST.DescriptionAnnotationId]: "description", + [AST.JSONSchemaAnnotationId]: { type: "integer" } + }) + }) + }) + + it("composition", () => { + const int = (self: S.Schema) => self.pipe(S.int(), S.brand("Int")) + + const positive = (self: S.Schema) => self.pipe(S.positive(), S.brand("Positive")) + + const PositiveInt = S.NumberFromString.pipe(int, positive) + + const is = S.is(PositiveInt) + assertTrue(is(1)) + assertFalse(is(-1)) + assertFalse(is(1.2)) + }) + + describe("decoding", () => { + it("string brand", async () => { + const schema = S.NumberFromString.pipe( + S.int(), + S.brand("Int") + ).annotations({ identifier: "IntegerFromString" }) + await Util.assertions.decoding.succeed(schema, "1", 1 as any) + await Util.assertions.decoding.fail( + schema, + null, + `IntegerFromString +└─ From side refinement failure + └─ NumberFromString + └─ Encoded side transformation failure + └─ Expected string, actual null` + ) + }) + + it("symbol brand", async () => { + const Int = Symbol.for("Int") + const schema = S.NumberFromString.pipe( + S.int(), + S.brand(Int) + ).annotations({ identifier: "IntegerFromString" }) + await Util.assertions.decoding.succeed(schema, "1", 1 as any) + await Util.assertions.decoding.fail( + schema, + null, + `IntegerFromString +└─ From side refinement failure + └─ NumberFromString + └─ Encoded side transformation failure + └─ Expected string, actual null` + ) + }) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/compose.test.ts b/repos/effect/packages/effect/test/Schema/Schema/compose.test.ts new file mode 100644 index 0000000..69c8cb1 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/compose.test.ts @@ -0,0 +1,84 @@ +import { describe, it } from "@effect/vitest" +import * as S from "effect/Schema" +import * as Util from "../TestUtils.js" + +describe("compose", async () => { + it("B = C", async () => { + const schema1 = S.compose(S.split(","), S.Array(S.NumberFromString)) + await Util.assertions.decoding.succeed(schema1, "1,2,3", [1, 2, 3]) + const schema2 = S.split(",").pipe(S.compose(S.Array(S.NumberFromString))) + await Util.assertions.decoding.succeed(schema2, "1,2,3", [1, 2, 3]) + }) + + it("force decoding: (A U B) compose (B -> C)", async () => { + const schema1 = S.compose(S.Union(S.String, S.Null), S.NumberFromString, { strict: false }) + await Util.assertions.decoding.succeed(schema1, "1", 1) + await Util.assertions.decoding.fail( + schema1, + "a", + `(string | null <-> NumberFromString) +└─ Type side transformation failure + └─ NumberFromString + └─ Transformation process failure + └─ Unable to decode "a" into a number` + ) + await Util.assertions.decoding.fail( + schema1, + null, + `(string | null <-> NumberFromString) +└─ Type side transformation failure + └─ NumberFromString + └─ Encoded side transformation failure + └─ Expected string, actual null` + ) + const schema2 = S.Union(S.String, S.Null).pipe( + S.compose(S.NumberFromString, { strict: false }) + ) + await Util.assertions.decoding.succeed(schema2, "1", 1) + await Util.assertions.decoding.fail( + schema2, + "a", + `(string | null <-> NumberFromString) +└─ Type side transformation failure + └─ NumberFromString + └─ Transformation process failure + └─ Unable to decode "a" into a number` + ) + await Util.assertions.decoding.fail( + schema2, + null, + `(string | null <-> NumberFromString) +└─ Type side transformation failure + └─ NumberFromString + └─ Encoded side transformation failure + └─ Expected string, actual null` + ) + }) + + it("force encoding: (A -> B) compose (C U B)", async () => { + const schema1 = S.compose(S.NumberFromString, S.Union(S.Number, S.Null), { strict: false }) + await Util.assertions.encoding.succeed(schema1, 1, "1") + await Util.assertions.encoding.fail( + schema1, + null, + `(NumberFromString <-> number | null) +└─ Encoded side transformation failure + └─ NumberFromString + └─ Type side transformation failure + └─ Expected number, actual null` + ) + const schema2 = S.NumberFromString.pipe( + S.compose(S.Union(S.Number, S.Null), { strict: false }) + ) + await Util.assertions.encoding.succeed(schema2, 1, "1") + await Util.assertions.encoding.fail( + schema2, + null, + `(NumberFromString <-> number | null) +└─ Encoded side transformation failure + └─ NumberFromString + └─ Type side transformation failure + └─ Expected number, actual null` + ) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/decode.test.ts b/repos/effect/packages/effect/test/Schema/Schema/decode.test.ts new file mode 100644 index 0000000..57fe04b --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/decode.test.ts @@ -0,0 +1,44 @@ +import { describe, it } from "@effect/vitest" +import * as Effect from "effect/Effect" +import * as S from "effect/Schema" +import * as Util from "../TestUtils.js" + +describe("decode", () => { + const schema = S.Struct({ a: Util.NumberFromChar }) + + it("should return an error on invalid values", async () => { + await Util.assertions.effect.succeed(S.decode(schema)({ a: "1" }), { a: 1 }) + await Util.assertions.effect.fail( + S.decode(schema)({ a: "10" }).pipe(Effect.mapError((e) => e.issue)), + `{ readonly a: NumberFromChar } +└─ ["a"] + └─ NumberFromChar + └─ Encoded side transformation failure + └─ Char + └─ Predicate refinement failure + └─ Expected a single character, actual "10"` + ) + }) + + it("should respect outer/inner options", async () => { + const input = { a: "1", b: "b" } + await Util.assertions.effect.fail( + S.decode(schema)(input, { onExcessProperty: "error" }).pipe(Effect.mapError((e) => e.issue)), + `{ readonly a: NumberFromChar } +└─ ["b"] + └─ is unexpected, expected: "a"` + ) + await Util.assertions.effect.fail( + S.decode(schema, { onExcessProperty: "error" })(input).pipe(Effect.mapError((e) => e.issue)), + `{ readonly a: NumberFromChar } +└─ ["b"] + └─ is unexpected, expected: "a"` + ) + await Util.assertions.effect.succeed( + S.decode(schema, { onExcessProperty: "error" })(input, { onExcessProperty: "ignore" }), + { + a: 1 + } + ) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/decodeEither.test.ts b/repos/effect/packages/effect/test/Schema/Schema/decodeEither.test.ts new file mode 100644 index 0000000..a7cd891 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/decodeEither.test.ts @@ -0,0 +1,52 @@ +import { describe, it } from "@effect/vitest" +import * as Either from "effect/Either" +import * as S from "effect/Schema" +import * as Util from "../TestUtils.js" + +describe("decodeEither", () => { + const schema = S.Struct({ a: Util.NumberFromChar }) + + it("should return an error on invalid values", async () => { + Util.assertions.either.right(S.decodeEither(schema)({ a: "1" }), { a: 1 }) + await Util.assertions.either.fail( + S.decodeEither(schema)({ a: "10" }).pipe(Either.mapLeft((e) => e.issue)), + `{ readonly a: NumberFromChar } +└─ ["a"] + └─ NumberFromChar + └─ Encoded side transformation failure + └─ Char + └─ Predicate refinement failure + └─ Expected a single character, actual "10"` + ) + }) + + it("should return an error on async", async () => { + await Util.assertions.either.fail( + S.decodeEither(Util.AsyncString)("a").pipe(Either.mapLeft((e) => e.issue)), + `AsyncString +└─ cannot be be resolved synchronously, this is caused by using runSync on an effect that performs async work` + ) + }) + + it("should respect outer/inner options", async () => { + const input = { a: "1", b: "b" } + await Util.assertions.either.fail( + S.decodeEither(schema)(input, { onExcessProperty: "error" }).pipe(Either.mapLeft((e) => e.issue)), + `{ readonly a: NumberFromChar } +└─ ["b"] + └─ is unexpected, expected: "a"` + ) + await Util.assertions.either.fail( + S.decodeEither(schema, { onExcessProperty: "error" })(input).pipe(Either.mapLeft((e) => e.issue)), + `{ readonly a: NumberFromChar } +└─ ["b"] + └─ is unexpected, expected: "a"` + ) + Util.assertions.either.right( + S.decodeEither(schema, { onExcessProperty: "error" })(input, { onExcessProperty: "ignore" }), + { + a: 1 + } + ) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/decodeOption.test.ts b/repos/effect/packages/effect/test/Schema/Schema/decodeOption.test.ts new file mode 100644 index 0000000..6127aea --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/decodeOption.test.ts @@ -0,0 +1,24 @@ +import { describe, it } from "@effect/vitest" +import { assertNone, assertSome } from "@effect/vitest/utils" +import { Schema as S } from "effect" +import * as Util from "../TestUtils.js" + +describe("decodeOption", () => { + it("should return none on async", () => { + assertNone(S.decodeOption(Util.AsyncString)("a")) + }) + + const schema = S.Struct({ a: Util.NumberFromChar }) + + it("should return None on invalid values", () => { + assertSome(S.decodeOption(schema)({ a: "1" }), { a: 1 }) + assertNone(S.decodeOption(schema)({ a: "10" })) + }) + + it("should respect outer/inner options", () => { + const input = { a: "1", b: "b" } + assertNone(S.decodeOption(schema)(input, { onExcessProperty: "error" })) + assertNone(S.decodeOption(schema, { onExcessProperty: "error" })(input)) + assertSome(S.decodeOption(schema, { onExcessProperty: "error" })(input, { onExcessProperty: "ignore" }), { a: 1 }) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/decodePromise.test.ts b/repos/effect/packages/effect/test/Schema/Schema/decodePromise.test.ts new file mode 100644 index 0000000..a760d74 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/decodePromise.test.ts @@ -0,0 +1,45 @@ +import { describe, it } from "@effect/vitest" +import { deepStrictEqual } from "@effect/vitest/utils" +import * as S from "effect/Schema" +import * as Util from "../TestUtils.js" + +describe("decodePromise", () => { + const schema = S.Struct({ a: Util.NumberFromChar }) + + it("should return None on invalid values", async () => { + deepStrictEqual(await S.decodePromise(schema)({ a: "1" }), { a: 1 }) + + await Util.assertions.promise.fail( + S.decodePromise(schema)({ a: "10" }), + `{ readonly a: NumberFromChar } +└─ ["a"] + └─ NumberFromChar + └─ Encoded side transformation failure + └─ Char + └─ Predicate refinement failure + └─ Expected a single character, actual "10"` + ) + }) + + it("should respect outer/inner options", async () => { + const input = { a: "1", b: "b" } + + deepStrictEqual( + await S.decodePromise(schema, { onExcessProperty: "error" })(input, { onExcessProperty: "ignore" }), + { a: 1 } + ) + + await Util.assertions.promise.fail( + S.decodePromise(schema)(input, { onExcessProperty: "error" }), + `{ readonly a: NumberFromChar } +└─ ["b"] + └─ is unexpected, expected: "a"` + ) + await Util.assertions.promise.fail( + S.decodePromise(schema, { onExcessProperty: "error" })(input), + `{ readonly a: NumberFromChar } +└─ ["b"] + └─ is unexpected, expected: "a"` + ) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/decodeSync.test.ts b/repos/effect/packages/effect/test/Schema/Schema/decodeSync.test.ts new file mode 100644 index 0000000..f143bd6 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/decodeSync.test.ts @@ -0,0 +1,49 @@ +import { describe, it } from "@effect/vitest" +import { deepStrictEqual } from "@effect/vitest/utils" +import * as S from "effect/Schema" +import * as Util from "../TestUtils.js" + +describe("decodeSync", () => { + const schema = S.Struct({ a: Util.NumberFromChar }) + + it("should throw on invalid values", () => { + deepStrictEqual(S.decodeSync(schema)({ a: "1" }), { a: 1 }) + Util.assertions.parseError( + () => S.decodeSync(schema)({ a: "10" }), + `{ readonly a: NumberFromChar } +└─ ["a"] + └─ NumberFromChar + └─ Encoded side transformation failure + └─ Char + └─ Predicate refinement failure + └─ Expected a single character, actual "10"` + ) + }) + + it("should throw on async", () => { + Util.assertions.parseError( + () => S.decodeSync(Util.AsyncString)("a"), + `AsyncString +└─ cannot be be resolved synchronously, this is caused by using runSync on an effect that performs async work` + ) + }) + + it("should respect outer/inner options", () => { + const input = { a: "1", b: "b" } + Util.assertions.parseError( + () => S.decodeSync(schema)(input, { onExcessProperty: "error" }), + `{ readonly a: NumberFromChar } +└─ ["b"] + └─ is unexpected, expected: "a"` + ) + Util.assertions.parseError( + () => S.decodeSync(schema, { onExcessProperty: "error" })(input), + `{ readonly a: NumberFromChar } +└─ ["b"] + └─ is unexpected, expected: "a"` + ) + deepStrictEqual(S.decodeSync(schema, { onExcessProperty: "error" })(input, { onExcessProperty: "ignore" }), { + a: 1 + }) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/decodeUnknownEither.test.ts b/repos/effect/packages/effect/test/Schema/Schema/decodeUnknownEither.test.ts new file mode 100644 index 0000000..c2d27ae --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/decodeUnknownEither.test.ts @@ -0,0 +1,14 @@ +import { describe, it } from "@effect/vitest" +import * as Either from "effect/Either" +import * as S from "effect/Schema" +import * as Util from "../TestUtils.js" + +describe("decodeUnknownEither", () => { + it("should return Left on async", async () => { + await Util.assertions.either.fail( + S.decodeUnknownEither(Util.AsyncString)("a").pipe(Either.mapLeft((e) => e.issue)), + `AsyncString +└─ cannot be be resolved synchronously, this is caused by using runSync on an effect that performs async work` + ) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/decodeUnknownOption.test.ts b/repos/effect/packages/effect/test/Schema/Schema/decodeUnknownOption.test.ts new file mode 100644 index 0000000..1a63ce6 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/decodeUnknownOption.test.ts @@ -0,0 +1,10 @@ +import { describe, it } from "@effect/vitest" +import { assertNone } from "@effect/vitest/utils" +import { Schema as S } from "effect" +import * as Util from "../TestUtils.js" + +describe("decodeUnknownOption", () => { + it("should return none on async", () => { + assertNone(S.decodeUnknownOption(Util.AsyncString)("a")) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/decodeUnknownPromise.test.ts b/repos/effect/packages/effect/test/Schema/Schema/decodeUnknownPromise.test.ts new file mode 100644 index 0000000..2b67bea --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/decodeUnknownPromise.test.ts @@ -0,0 +1,20 @@ +import { describe, it } from "@effect/vitest" +import { deepStrictEqual } from "@effect/vitest/utils" +import { ParseResult, Schema } from "effect" +import * as Util from "../TestUtils.js" + +describe("decodeUnknownPromise", () => { + const schema = Schema.String + + it("should resolve", async () => { + deepStrictEqual(await Schema.decodeUnknownPromise(schema)("a"), "a") + deepStrictEqual(await ParseResult.decodeUnknownPromise(schema)("a"), "a") + }) + + it("should reject on invalid values", async () => { + await Util.assertions.promise.fail( + Schema.decodeUnknownPromise(schema)(null), + `Expected string, actual null` + ) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/decodeUnknownSync.test.ts b/repos/effect/packages/effect/test/Schema/Schema/decodeUnknownSync.test.ts new file mode 100644 index 0000000..7e0182e --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/decodeUnknownSync.test.ts @@ -0,0 +1,39 @@ +import { describe, it } from "@effect/vitest" +import { assertInclude, assertInstanceOf, strictEqual, throws } from "@effect/vitest/utils" +import { Effect, ParseResult, Predicate, Schema as S } from "effect" +import * as Util from "../TestUtils.js" + +const SyncEffectfulString = S.declare([], { + decode: () => (u, _, ast) => + Predicate.isString(u) ? Effect.succeed(u) : Effect.fail(new ParseResult.Type(ast, u, "not a string")), + encode: () => (u, _, ast) => + Predicate.isString(u) ? Effect.succeed(u) : Effect.fail(new ParseResult.Type(ast, u, "not a string")) +}, { identifier: "SyncEffectfulString" }) + +describe("decodeUnknownSync", () => { + it("should return a ParseError when the input is invalid", () => { + Util.assertions.parseError(() => S.decodeUnknownSync(S.String)(1), "Expected string, actual 1") + }) + + it("should decode synchronously even when the schema uses Effects", () => { + strictEqual(S.decodeUnknownSync(SyncEffectfulString)("a"), "a") + Util.assertions.parseError(() => { + S.decodeUnknownSync(SyncEffectfulString)(null) + }, "not a string") + }) + + it("should throw an error when the schema performs asynchronous work", () => { + Util.assertions.parseError( + () => S.decodeUnknownSync(Util.AsyncString)("a"), + `AsyncString +└─ cannot be be resolved synchronously, this is caused by using runSync on an effect that performs async work` + ) + }) + + it("should throw an error when required dependencies are missing", () => { + throws(() => S.decodeUnknownSync(Util.DependencyString as any)("a"), (err) => { + assertInstanceOf(err, ParseResult.ParseError) + assertInclude(err.message, "Service not found: Name") + }) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/encode.test.ts b/repos/effect/packages/effect/test/Schema/Schema/encode.test.ts new file mode 100644 index 0000000..d0a4144 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/encode.test.ts @@ -0,0 +1,44 @@ +import { describe, it } from "@effect/vitest" +import * as Effect from "effect/Effect" +import * as S from "effect/Schema" +import * as Util from "../TestUtils.js" + +describe("encode", () => { + const schema = S.Struct({ a: Util.NumberFromChar }) + + it("should return an error on invalid values", async () => { + await Util.assertions.effect.succeed(S.encode(schema)({ a: 1 }), { a: "1" }) + await Util.assertions.effect.fail( + S.encode(schema)({ a: 10 }).pipe(Effect.mapError((e) => e.issue)), + `{ readonly a: NumberFromChar } +└─ ["a"] + └─ NumberFromChar + └─ Encoded side transformation failure + └─ Char + └─ Predicate refinement failure + └─ Expected a single character, actual "10"` + ) + }) + + it("should respect outer/inner options", async () => { + const input = { a: 1, b: "b" } + await Util.assertions.effect.fail( + S.encode(schema)(input, { onExcessProperty: "error" }).pipe(Effect.mapError((e) => e.issue)), + `{ readonly a: NumberFromChar } +└─ ["b"] + └─ is unexpected, expected: "a"` + ) + await Util.assertions.effect.fail( + S.encode(schema, { onExcessProperty: "error" })(input).pipe(Effect.mapError((e) => e.issue)), + `{ readonly a: NumberFromChar } +└─ ["b"] + └─ is unexpected, expected: "a"` + ) + await Util.assertions.effect.succeed( + S.encode(schema, { onExcessProperty: "error" })(input, { onExcessProperty: "ignore" }), + { + a: "1" + } + ) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/encodeEither.test.ts b/repos/effect/packages/effect/test/Schema/Schema/encodeEither.test.ts new file mode 100644 index 0000000..5908589 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/encodeEither.test.ts @@ -0,0 +1,50 @@ +import { describe, it } from "@effect/vitest" +import * as Either from "effect/Either" +import * as S from "effect/Schema" +import * as Util from "../TestUtils.js" + +describe("encodeEither", () => { + const schema = S.Struct({ a: Util.NumberFromChar }) + + it("should return an error on invalid values", async () => { + Util.assertions.either.right(S.encodeEither(schema)({ a: 1 }), { a: "1" }) + await Util.assertions.either.fail( + S.encodeEither(schema)({ a: 10 }).pipe(Either.mapLeft((e) => e.issue)), + `{ readonly a: NumberFromChar } +└─ ["a"] + └─ NumberFromChar + └─ Encoded side transformation failure + └─ Char + └─ Predicate refinement failure + └─ Expected a single character, actual "10"` + ) + }) + + it("should return an error on async", async () => { + await Util.assertions.either.fail( + S.encodeEither(Util.AsyncString)("a").pipe(Either.mapLeft((e) => e.issue)), + `AsyncString +└─ cannot be be resolved synchronously, this is caused by using runSync on an effect that performs async work` + ) + }) + + it("should respect outer/inner options", async () => { + const input = { a: 1, b: "b" } + await Util.assertions.either.fail( + S.encodeEither(schema)(input, { onExcessProperty: "error" }).pipe(Either.mapLeft((e) => e.issue)), + `{ readonly a: NumberFromChar } +└─ ["b"] + └─ is unexpected, expected: "a"` + ) + await Util.assertions.either.fail( + S.encodeEither(schema, { onExcessProperty: "error" })(input).pipe(Either.mapLeft((e) => e.issue)), + `{ readonly a: NumberFromChar } +└─ ["b"] + └─ is unexpected, expected: "a"` + ) + Util.assertions.either.right( + S.encodeEither(schema, { onExcessProperty: "error" })(input, { onExcessProperty: "ignore" }), + { a: "1" } + ) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/encodeOption.test.ts b/repos/effect/packages/effect/test/Schema/Schema/encodeOption.test.ts new file mode 100644 index 0000000..37204fc --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/encodeOption.test.ts @@ -0,0 +1,24 @@ +import { describe, it } from "@effect/vitest" +import { assertNone, assertSome } from "@effect/vitest/utils" +import { Schema as S } from "effect" +import * as Util from "../TestUtils.js" + +describe("encodeOption", () => { + it("should return none on async", () => { + assertNone(S.encodeOption(Util.AsyncString)("a")) + }) + + const schema = S.Struct({ a: Util.NumberFromChar }) + + it("should return None on invalid values", () => { + assertSome(S.encodeOption(schema)({ a: 1 }), { a: "1" }) + assertNone(S.encodeOption(schema)({ a: 10 })) + }) + + it("should respect outer/inner options", () => { + const input = { a: 1, b: "b" } + assertNone(S.encodeOption(schema)(input, { onExcessProperty: "error" })) + assertNone(S.encodeOption(schema, { onExcessProperty: "error" })(input)) + assertSome(S.encodeOption(schema, { onExcessProperty: "error" })(input, { onExcessProperty: "ignore" }), { a: "1" }) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/encodePromise.test.ts b/repos/effect/packages/effect/test/Schema/Schema/encodePromise.test.ts new file mode 100644 index 0000000..15cecef --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/encodePromise.test.ts @@ -0,0 +1,45 @@ +import { describe, it } from "@effect/vitest" +import { deepStrictEqual } from "@effect/vitest/utils" +import * as S from "effect/Schema" +import * as Util from "../TestUtils.js" + +describe("encodePromise", () => { + const schema = S.Struct({ a: Util.NumberFromChar }) + + it("should return None on invalid values", async () => { + deepStrictEqual(await S.encodePromise(schema)({ a: 1 }), { a: "1" }) + + await Util.assertions.promise.fail( + S.encodePromise(schema)({ a: 10 }), + `{ readonly a: NumberFromChar } +└─ ["a"] + └─ NumberFromChar + └─ Encoded side transformation failure + └─ Char + └─ Predicate refinement failure + └─ Expected a single character, actual "10"` + ) + }) + + it("should respect outer/inner options", async () => { + const input = { a: 1, b: "b" } + + deepStrictEqual( + await S.encodePromise(schema, { onExcessProperty: "error" })(input, { onExcessProperty: "ignore" }), + { a: "1" } + ) + + await Util.assertions.promise.fail( + S.encodePromise(schema)(input, { onExcessProperty: "error" }), + `{ readonly a: NumberFromChar } +└─ ["b"] + └─ is unexpected, expected: "a"` + ) + await Util.assertions.promise.fail( + S.encodePromise(schema, { onExcessProperty: "error" })(input), + `{ readonly a: NumberFromChar } +└─ ["b"] + └─ is unexpected, expected: "a"` + ) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/encodeSync.test.ts b/repos/effect/packages/effect/test/Schema/Schema/encodeSync.test.ts new file mode 100644 index 0000000..23132b1 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/encodeSync.test.ts @@ -0,0 +1,49 @@ +import { describe, it } from "@effect/vitest" +import { deepStrictEqual } from "@effect/vitest/utils" +import * as S from "effect/Schema" +import * as Util from "../TestUtils.js" + +describe("encodeSync", () => { + const schema = S.Struct({ a: Util.NumberFromChar }) + + it("should throw on invalid values", () => { + deepStrictEqual(S.encodeSync(schema)({ a: 1 }), { a: "1" }) + Util.assertions.parseError( + () => S.encodeSync(schema)({ a: 10 }), + `{ readonly a: NumberFromChar } +└─ ["a"] + └─ NumberFromChar + └─ Encoded side transformation failure + └─ Char + └─ Predicate refinement failure + └─ Expected a single character, actual "10"` + ) + }) + + it("should throw on async", () => { + Util.assertions.parseError( + () => S.encodeSync(Util.AsyncString)("a"), + `AsyncString +└─ cannot be be resolved synchronously, this is caused by using runSync on an effect that performs async work` + ) + }) + + it("should respect outer/inner options", () => { + const input = { a: 1, b: "b" } + Util.assertions.parseError( + () => S.encodeSync(schema)(input, { onExcessProperty: "error" }), + `{ readonly a: NumberFromChar } +└─ ["b"] + └─ is unexpected, expected: "a"` + ) + Util.assertions.parseError( + () => S.encodeSync(schema, { onExcessProperty: "error" })(input), + `{ readonly a: NumberFromChar } +└─ ["b"] + └─ is unexpected, expected: "a"` + ) + deepStrictEqual(S.encodeSync(schema, { onExcessProperty: "error" })(input, { onExcessProperty: "ignore" }), { + a: "1" + }) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/encodeUnknownEither.test.ts b/repos/effect/packages/effect/test/Schema/Schema/encodeUnknownEither.test.ts new file mode 100644 index 0000000..f318a8c --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/encodeUnknownEither.test.ts @@ -0,0 +1,14 @@ +import { describe, it } from "@effect/vitest" +import * as Either from "effect/Either" +import * as S from "effect/Schema" +import * as Util from "../TestUtils.js" + +describe("encodeUnknownEither", () => { + it("should return Left on async", async () => { + await Util.assertions.either.fail( + S.encodeUnknownEither(Util.AsyncString)("a").pipe(Either.mapLeft((e) => e.issue)), + `AsyncString +└─ cannot be be resolved synchronously, this is caused by using runSync on an effect that performs async work` + ) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/encodeUnknownOption.test.ts b/repos/effect/packages/effect/test/Schema/Schema/encodeUnknownOption.test.ts new file mode 100644 index 0000000..c57b262 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/encodeUnknownOption.test.ts @@ -0,0 +1,10 @@ +import { describe, it } from "@effect/vitest" +import { assertNone } from "@effect/vitest/utils" +import { Schema as S } from "effect" +import * as Util from "../TestUtils.js" + +describe("encodeUnknownOption", () => { + it("should return none on async", () => { + assertNone(S.encodeUnknownOption(Util.AsyncString)("a")) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/encodeUnknownPromise.test.ts b/repos/effect/packages/effect/test/Schema/Schema/encodeUnknownPromise.test.ts new file mode 100644 index 0000000..a765202 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/encodeUnknownPromise.test.ts @@ -0,0 +1,20 @@ +import { describe, it } from "@effect/vitest" +import { deepStrictEqual } from "@effect/vitest/utils" +import { ParseResult, Schema } from "effect" +import * as Util from "../TestUtils.js" + +describe("encodeUnknownPromise", () => { + const schema = Schema.String + + it("should resolve", async () => { + deepStrictEqual(await Schema.encodeUnknownPromise(schema)("a"), "a") + deepStrictEqual(await ParseResult.encodeUnknownPromise(schema)("a"), "a") + }) + + it("should reject on invalid values", async () => { + await Util.assertions.promise.fail( + Schema.encodeUnknownPromise(schema)(null), + `Expected string, actual null` + ) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/encodeUnknownSync.test.ts b/repos/effect/packages/effect/test/Schema/Schema/encodeUnknownSync.test.ts new file mode 100644 index 0000000..25ff6dd --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/encodeUnknownSync.test.ts @@ -0,0 +1,13 @@ +import { describe, it } from "@effect/vitest" +import * as S from "effect/Schema" +import * as Util from "../TestUtils.js" + +describe("encodeUnknownSync", () => { + it("should throw on async", () => { + Util.assertions.parseError( + () => S.encodeUnknownSync(Util.AsyncString)("a"), + `AsyncString +└─ cannot be be resolved synchronously, this is caused by using runSync on an effect that performs async work` + ) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/encodedBoundSchema.test.ts b/repos/effect/packages/effect/test/Schema/Schema/encodedBoundSchema.test.ts new file mode 100644 index 0000000..7fbbc65 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/encodedBoundSchema.test.ts @@ -0,0 +1,229 @@ +import { describe, it } from "@effect/vitest" +import * as S from "effect/Schema" +import * as Util from "../TestUtils.js" + +describe("encodedBoundSchema", () => { + const StringTransformation = S.transform( + S.String.pipe(S.minLength(2)).annotations({ identifier: "String2" }), + S.String, + { + strict: true, + encode: (s) => s, + decode: (s) => s + } + ).annotations({ identifier: "StringTransformation" }) + + it("struct", async () => { + const String3 = S.String.pipe(S.minLength(3)).annotations({ identifier: "String3" }) + + const schema = S.Struct({ + a: S.Array(StringTransformation), + b: String3 + }).annotations({ identifier: "FullSchema" }) + + const bound = S.encodedBoundSchema(schema) + + await Util.assertions.decoding.succeed(bound, { + a: ["ab"], + b: "abc" + }) + + await Util.assertions.decoding.fail( + bound, + { + a: ["a"], + b: "abc" + }, + `{ readonly a: ReadonlyArray; readonly b: String3 } +└─ ["a"] + └─ ReadonlyArray + └─ [0] + └─ String2 + └─ Predicate refinement failure + └─ Expected a string at least 2 character(s) long, actual "a"` + ) + + await Util.assertions.decoding.fail( + bound, + { + a: ["ab"], + b: "ab" + }, + `{ readonly a: ReadonlyArray; readonly b: String3 } +└─ ["b"] + └─ String3 + └─ Predicate refinement failure + └─ Expected a string at least 3 character(s) long, actual "ab"` + ) + }) + + describe("Stable filters", () => { + describe("Array", () => { + it("minItems", async () => { + const schema = S.Array(StringTransformation).pipe(S.minItems(2)) + const bound = S.encodedBoundSchema(schema) + + await Util.assertions.decoding.succeed(bound, ["ab", "cd"]) + await Util.assertions.decoding.fail( + bound, + ["a"], + `minItems(2) +└─ From side refinement failure + └─ ReadonlyArray + └─ [0] + └─ String2 + └─ Predicate refinement failure + └─ Expected a string at least 2 character(s) long, actual "a"` + ) + await Util.assertions.decoding.fail( + bound, + ["ab"], + `minItems(2) +└─ Predicate refinement failure + └─ Expected an array of at least 2 item(s), actual ["ab"]` + ) + }) + + it("maxItems", async () => { + const schema = S.Array(StringTransformation).pipe(S.maxItems(2)) + const bound = S.encodedBoundSchema(schema) + + await Util.assertions.decoding.succeed(bound, ["ab", "cd"]) + await Util.assertions.decoding.fail( + bound, + ["a"], + `maxItems(2) +└─ From side refinement failure + └─ ReadonlyArray + └─ [0] + └─ String2 + └─ Predicate refinement failure + └─ Expected a string at least 2 character(s) long, actual "a"` + ) + await Util.assertions.decoding.fail( + bound, + ["ab", "cd", "ef"], + `maxItems(2) +└─ Predicate refinement failure + └─ Expected an array of at most 2 item(s), actual ["ab","cd","ef"]` + ) + }) + + it("itemsCount", async () => { + const schema = S.Array(StringTransformation).pipe(S.itemsCount(2)) + const bound = S.encodedBoundSchema(schema) + + await Util.assertions.decoding.succeed(bound, ["ab", "cd"]) + await Util.assertions.decoding.fail( + bound, + ["a"], + `itemsCount(2) +└─ From side refinement failure + └─ ReadonlyArray + └─ [0] + └─ String2 + └─ Predicate refinement failure + └─ Expected a string at least 2 character(s) long, actual "a"` + ) + await Util.assertions.decoding.fail( + bound, + ["ab"], + `itemsCount(2) +└─ Predicate refinement failure + └─ Expected an array of exactly 2 item(s), actual ["ab"]` + ) + await Util.assertions.decoding.fail( + bound, + ["ab", "cd", "ef"], + `itemsCount(2) +└─ Predicate refinement failure + └─ Expected an array of exactly 2 item(s), actual ["ab","cd","ef"]` + ) + }) + }) + + describe("NonEmptyArray", () => { + it("minItems", async () => { + const schema = S.NonEmptyArray(StringTransformation).pipe(S.minItems(2)) + const bound = S.encodedBoundSchema(schema) + + await Util.assertions.decoding.succeed(bound, ["ab", "cd"]) + await Util.assertions.decoding.fail( + bound, + ["a"], + `minItems(2) +└─ From side refinement failure + └─ readonly [String2, ...String2[]] + └─ [0] + └─ String2 + └─ Predicate refinement failure + └─ Expected a string at least 2 character(s) long, actual "a"` + ) + await Util.assertions.decoding.fail( + bound, + ["ab"], + `minItems(2) +└─ Predicate refinement failure + └─ Expected an array of at least 2 item(s), actual ["ab"]` + ) + }) + + it("maxItems", async () => { + const schema = S.NonEmptyArray(StringTransformation).pipe(S.maxItems(2)) + const bound = S.encodedBoundSchema(schema) + + await Util.assertions.decoding.succeed(bound, ["ab", "cd"]) + await Util.assertions.decoding.fail( + bound, + ["a"], + `maxItems(2) +└─ From side refinement failure + └─ readonly [String2, ...String2[]] + └─ [0] + └─ String2 + └─ Predicate refinement failure + └─ Expected a string at least 2 character(s) long, actual "a"` + ) + await Util.assertions.decoding.fail( + bound, + ["ab", "cd", "ef"], + `maxItems(2) +└─ Predicate refinement failure + └─ Expected an array of at most 2 item(s), actual ["ab","cd","ef"]` + ) + }) + + it("itemsCount", async () => { + const schema = S.NonEmptyArray(StringTransformation).pipe(S.itemsCount(2)) + const bound = S.encodedBoundSchema(schema) + + await Util.assertions.decoding.succeed(bound, ["ab", "cd"]) + await Util.assertions.decoding.fail( + bound, + ["a"], + `itemsCount(2) +└─ From side refinement failure + └─ readonly [String2, ...String2[]] + └─ [0] + └─ String2 + └─ Predicate refinement failure + └─ Expected a string at least 2 character(s) long, actual "a"` + ) + await Util.assertions.decoding.fail( + bound, + ["ab"], + `itemsCount(2) +└─ Predicate refinement failure + └─ Expected an array of exactly 2 item(s), actual ["ab"]` + ) + await Util.assertions.decoding.fail( + bound, + ["ab", "cd", "ef"], + `itemsCount(2) +└─ Predicate refinement failure + └─ Expected an array of exactly 2 item(s), actual ["ab","cd","ef"]` + ) + }) + }) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/encodedSchema.test.ts b/repos/effect/packages/effect/test/Schema/Schema/encodedSchema.test.ts new file mode 100644 index 0000000..520fc5c --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/encodedSchema.test.ts @@ -0,0 +1,37 @@ +import { describe, it } from "@effect/vitest" +import * as S from "effect/Schema" +import * as Util from "../TestUtils.js" + +describe("encodedSchema", () => { + it("suspend", async () => { + interface I { + prop: I | string + } + interface A { + prop: A | number + } + const schema1 = S.Struct({ + prop: S.Union(S.NumberFromString, S.suspend((): S.Schema => schema1)) + }) + const from1 = S.encodedSchema(schema1) + await Util.assertions.decoding.succeed(from1, { prop: "a" }) + await Util.assertions.decoding.succeed(from1, { prop: { prop: "a" } }) + + const schema2: S.Schema = S.suspend( // intended outer suspend + () => + S.Struct({ + prop: S.Union(S.NumberFromString, schema1) + }) + ) + const from2 = S.encodedSchema(schema2) + await Util.assertions.decoding.succeed(from2, { prop: "a" }) + await Util.assertions.decoding.succeed(from2, { prop: { prop: "a" } }) + }) + + it("decoding", async () => { + const schema = S.encodedSchema(S.NumberFromString) + await Util.assertions.decoding.succeed(schema, "a") + await Util.assertions.decoding.fail(schema, null, "Expected string, actual null") + await Util.assertions.decoding.fail(schema, 1, "Expected string, actual 1") + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/equivalence.test.ts b/repos/effect/packages/effect/test/Schema/Schema/equivalence.test.ts new file mode 100644 index 0000000..488105f --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/equivalence.test.ts @@ -0,0 +1,844 @@ +import { describe, it } from "@effect/vitest" +import { assertFalse, assertTrue, strictEqual, throws } from "@effect/vitest/utils" +import * as A from "effect/Arbitrary" +import * as Chunk from "effect/Chunk" +import * as Data from "effect/Data" +import * as Either from "effect/Either" +import * as Equal from "effect/Equal" +import * as Equivalence from "effect/Equivalence" +import * as fc from "effect/FastCheck" +import * as Hash from "effect/Hash" +import * as Option from "effect/Option" +import { isUnknown } from "effect/Predicate" +import * as S from "effect/Schema" + +/** + * Tests that the generated Eq is a valid Eq + */ +export const propertyType = ( + schema: S.Schema, + params?: fc.Parameters<[A, ...Array]> +) => { + const arb = A.makeLazy(schema)(fc) + // console.log(fc.sample(arb, 10)) + const equivalence = S.equivalence(schema) + + const reflexivity = fc.property(arb, (a) => equivalence(a, a)) + const symmetry = fc.property(arb, arb, (a, b) => equivalence(a, b) === equivalence(b, a)) + const transitivity = fc.property( + arb, + arb, + arb, + (a, b, c) => + /* + A logical implication is a relationship between two propositions that states that if the first proposition is true, + then the second proposition must also be true. In terms of booleans, a logical implication can be translated as: + + (p → q) ≡ ¬p ∨ q + */ + !(equivalence(a, b) && equivalence(b, c)) || equivalence(a, c) + ) + + fc.assert(reflexivity, params) + fc.assert(symmetry, params) + fc.assert(transitivity, params) +} + +const MyString = S.String.annotations({ + equivalence: () => (a, b) => { + if (typeof a !== "string" || typeof b !== "string") { + throw new Error("invalid string provided to `string`") + } + return a === b + } +}) + +const MyNumber = S.JsonNumber.annotations({ + equivalence: () => (a, b) => { + if (typeof a !== "number" || typeof b !== "number") { + throw new Error("invalid number provided to `number`") + } + return a === b + } +}) + +const MySymbol = S.SymbolFromSelf.annotations({ + equivalence: () => (a, b) => { + if (typeof a !== "symbol" || typeof b !== "symbol") { + throw new Error("invalid symbol provided to `symbol`") + } + return a === b + } +}) + +describe("equivalence", () => { + it("the errors should display a path", () => { + throws( + () => S.equivalence(S.Tuple(S.Never as any)), + new Error(`Unsupported schema +at path: [0] +details: Cannot build an Equivalence +schema (NeverKeyword): never`) + ) + throws( + () => S.equivalence(S.Struct({ a: S.Never as any })), + new Error(`Unsupported schema +at path: ["a"] +details: Cannot build an Equivalence +schema (NeverKeyword): never`) + ) + }) + + it("transformation", () => { + const schema = S.NumberFromString + const equivalence = S.equivalence(schema) + + assertTrue(equivalence(1, 1)) + + assertFalse(equivalence(1, 2)) + }) + + it("S.equivalence(S.encodedSchema(schema))", () => { + const schema = S.NumberFromString + const equivalence = S.equivalence(S.encodedSchema(schema)) + + assertTrue(equivalence("a", "a")) + + assertFalse(equivalence("a", "b")) + }) + + it("never", () => { + throws( + () => S.equivalence(S.Never), + new Error(`Unsupported schema +details: Cannot build an Equivalence +schema (NeverKeyword): never`) + ) + }) + + it("string", () => { + const schema = MyString + const equivalence = S.equivalence(schema) + + assertTrue(equivalence("a", "a")) + + assertFalse(equivalence("a", "b")) + + // propertyType(schema) + }) + + it("Refinement", () => { + const schema = S.NonEmptyString + const equivalence = S.equivalence(schema) + + assertTrue(equivalence("a", "a")) + + assertFalse(equivalence("a", "b")) + + // propertyType(schema) + }) + + describe("declaration", () => { + it("should return Equal.equals when an annotation doesn't exist", () => { + const schema = S.declare(isUnknown) + const equivalence = S.equivalence(schema) + strictEqual(equivalence, Equal.equals) + + const make = (id: number, s: string) => { + return { + [Hash.symbol]() { + return 0 + }, + [Equal.symbol](that: any) { + return that.id === id + }, + id, + s + } + } + + assertTrue(equivalence(make(1, "a"), make(1, "a"))) + assertTrue(equivalence(make(1, "a"), make(1, "b"))) + assertFalse(equivalence(make(1, "a"), make(2, "a"))) + }) + + it("Chunk", () => { + const schema = S.ChunkFromSelf(MyNumber) + const equivalence = S.equivalence(schema) + + assertTrue(equivalence(Chunk.empty(), Chunk.empty())) + assertTrue(equivalence(Chunk.make(1, 2, 3), Chunk.make(1, 2, 3))) + + assertFalse(equivalence(Chunk.make(1, 2, 3), Chunk.make(1, 2))) + assertFalse(equivalence(Chunk.make(1, 2, 3), Chunk.make(1, 2, 4))) + + // propertyType(schema) + }) + + it("Date", () => { + const schema = S.DateFromSelf + const equivalence = S.equivalence(schema) + const now = new Date() + + assertTrue(equivalence(now, now)) + assertTrue(equivalence(new Date(0), new Date(0))) + + assertFalse(equivalence(new Date(0), new Date(1))) + + // propertyType(schema) + }) + + it("Data", () => { + const schema = S.DataFromSelf(S.Struct({ a: MyString, b: MyNumber })) + const equivalence = S.equivalence(schema) + + assertTrue(equivalence(Data.struct({ a: "ok", b: 0 }), Data.struct({ a: "ok", b: 0 }))) + + // propertyType(schema) + }) + + it("Either", () => { + const schema = S.EitherFromSelf({ left: MyString, right: MyNumber }) + const equivalence = S.equivalence(schema) + + assertTrue(equivalence(Either.right(1), Either.right(1))) + assertTrue(equivalence(Either.left("a"), Either.left("a"))) + + assertFalse(equivalence(Either.right(1), Either.right(2))) + assertFalse(equivalence(Either.left("a"), Either.left("b"))) + + // propertyType(schema) + }) + + it("Option", () => { + const schema = S.OptionFromSelf(MyNumber) + const equivalence = S.equivalence(schema) + + assertTrue(equivalence(Option.none(), Option.none())) + assertTrue(equivalence(Option.some(1), Option.some(1))) + + assertFalse(equivalence(Option.some(1), Option.some(2))) + + // propertyType(schema) + }) + + it("ReadonlySet", () => { + const schema = S.ReadonlySetFromSelf(MyNumber) + const equivalence = S.equivalence(schema) + + assertTrue(equivalence(new Set(), new Set())) + assertTrue(equivalence(new Set([1, 2, 3]), new Set([1, 2, 3]))) + + assertFalse(equivalence(new Set([1, 2, 3]), new Set([1, 2]))) + + // propertyType(schema) + }) + + it("ReadonlyMap", () => { + const schema = S.ReadonlyMapFromSelf({ key: MyString, value: MyNumber }) + const equivalence = S.equivalence(schema) + + assertTrue(equivalence(new Map(), new Map())) + assertTrue(equivalence(new Map([["a", 1], ["b", 2]]), new Map([["a", 1], ["b", 2]]))) + + assertFalse(equivalence(new Map([["a", 1], ["b", 2]]), new Map([["a", 3], ["b", 2]]))) + assertFalse(equivalence(new Map([["a", 1], ["b", 2]]), new Map([["a", 1], ["b", 4]]))) + + // propertyType(schema) + }) + + it("Uint8Array", () => { + const schema = S.Uint8ArrayFromSelf + const equivalence = S.equivalence(schema) + + assertTrue(equivalence(new Uint8Array(), new Uint8Array())) + assertTrue( + equivalence(new Uint8Array([10, 20, 30, 40, 50]), new Uint8Array([10, 20, 30, 40, 50])) + ) + + assertFalse( + equivalence(new Uint8Array([10, 20, 30, 40, 50]), new Uint8Array([10, 20, 30, 30, 50])) + ) + + // propertyType(schema) + }) + + it("instanceOf", () => { + const schema = S.instanceOf(URL, { + equivalence: () => Equivalence.make((a, b) => a.href === b.href) + }) + const equivalence = S.equivalence(schema) + + assertTrue(equivalence(new URL("https://example.com/page"), new URL("https://example.com/page"))) + + assertFalse(equivalence(new URL("https://example.com/page"), new URL("https://google.come"))) + }) + }) + + describe("union", () => { + it("primitives", () => { + const schema = S.Union(MyString, MyNumber) + const equivalence = S.equivalence(schema) + + assertTrue(equivalence("a", "a")) + assertTrue(equivalence(1, 1)) + + assertFalse(equivalence("a", "b")) + assertFalse(equivalence(1, 2)) + + // propertyType(schema) + }) + + it("should fallback on the less precise equivalence", () => { + const a = S.Struct({ a: MyString }) + const ab = S.Struct({ a: MyString, b: S.Number }) + const schema = S.Union(a, ab) + const equivalence = S.equivalence(schema) + + assertTrue(equivalence({ a: "a", b: 1 }, { a: "a", b: 1 })) + assertTrue(equivalence({ a: "a", b: 1 }, { a: "a", b: 2 })) + + assertFalse(equivalence({ a: "a", b: 1 }, { a: "c", b: 1 })) + + // propertyType(schema) + }) + + it("discriminated structs", () => { + const schema = S.Union( + S.Struct({ tag: S.Literal("a"), a: MyString }), + S.Struct({ tag: S.Literal("b"), b: S.Number }) + ) + const equivalence = S.equivalence(schema) + + assertTrue(equivalence({ tag: "a", a: "a" }, { tag: "a", a: "a" })) + assertTrue(equivalence({ tag: "b", b: 1 }, { tag: "b", b: 1 })) + + assertFalse(equivalence({ tag: "a", a: "a" }, { tag: "a", a: "b" })) + assertFalse(equivalence({ tag: "b", b: 1 }, { tag: "b", b: 2 })) + assertFalse(equivalence({ tag: "a", a: "a" }, { tag: "b", b: 1 })) + }) + + it("discriminated tuples", () => { + const schema = S.Union( + S.Tuple(S.Literal("a"), S.String), + S.Tuple(S.Literal("b"), S.Number) + ) + const equivalence = S.equivalence(schema) + + assertTrue(equivalence(["a", "x"], ["a", "x"])) + assertFalse(equivalence(["a", "x"], ["a", "y"])) + + assertTrue(equivalence(["b", 1], ["b", 1])) + assertFalse(equivalence(["b", 1], ["b", 2])) + + assertFalse(equivalence(["a", "x"], ["b", 1])) + }) + }) + + describe("tuple", () => { + it("empty", () => { + const schema = S.Tuple() + const equivalence = S.equivalence(schema) + + assertTrue(equivalence([], [])) + }) + + it("should fail on non-array inputs", () => { + const schema = S.Tuple(S.String, S.Number) + const equivalence = S.equivalence(schema) + assertFalse(equivalence(["a", 1], null as never)) + }) + + it("e", () => { + const schema = S.Tuple(MyString, MyNumber) + const equivalence = S.equivalence(schema) + + assertTrue(equivalence(["a", 1], ["a", 1])) + + assertFalse(equivalence(["a", 1], ["b", 1])) + assertFalse(equivalence(["a", 1], ["a", 2])) + + // propertyType(schema) + }) + + it("e r", () => { + const schema = S.Tuple([S.String], S.Number) + const equivalence = S.equivalence(schema) + + assertTrue(equivalence(["a"], ["a"])) + assertTrue(equivalence(["a", 1], ["a", 1])) + assertTrue(equivalence(["a", 1, 2], ["a", 1, 2])) + + assertFalse(equivalence(["a", 1], ["a", 2])) + assertFalse(equivalence(["a", 1, 2], ["a", 1, 3])) + + // propertyType(schema) + }) + + it("r", () => { + const schema = S.Array(MyNumber) + const equivalence = S.equivalence(schema) + + assertTrue(equivalence([], [])) + assertTrue(equivalence([1], [1])) + assertTrue(equivalence([1, 2], [1, 2])) + + assertFalse(equivalence([1, 2], [1, 2, 3])) + assertFalse(equivalence([1, 2, 3], [1, 2])) + + // propertyType(schema) + }) + + it("r e", () => { + const schema = S.Tuple([], MyString, MyNumber) + const equivalence = S.equivalence(schema) + + assertTrue(equivalence([1], [1])) + assertTrue(equivalence(["a", 1], ["a", 1])) + assertTrue(equivalence(["a", "b", 1], ["a", "b", 1])) + + assertFalse(equivalence([1], [2])) + assertFalse(equivalence([2], [1])) + assertFalse(equivalence(["a", "b", 1], ["a", "c", 1])) + + // propertyType(schema) + }) + + describe("optional element support", () => { + it("e?", () => { + const schema = S.Tuple(S.optionalElement(MyString)) + const equivalence = S.equivalence(schema) + + assertTrue(equivalence([], [])) + assertTrue(equivalence(["a"], ["a"])) + + assertFalse(equivalence(["a"], ["b"])) + assertFalse(equivalence([], ["a"])) + assertFalse(equivalence(["a"], [])) + + // propertyType(schema) + }) + + it("e? e?", () => { + const schema = S.Tuple(S.optionalElement(MyString), S.optionalElement(MyNumber)) + const equivalence = S.equivalence(schema) + + assertTrue(equivalence([], [])) + assertTrue(equivalence(["a"], ["a"])) + assertTrue(equivalence(["a"], ["a"])) + assertTrue(equivalence(["a", 1], ["a", 1])) + + assertFalse(equivalence(["a"], ["b"])) + assertFalse(equivalence(["a", 1], ["a", 2])) + assertFalse(equivalence(["a", 1], ["a"])) + assertFalse(equivalence([], ["a"])) + assertFalse(equivalence(["a"], [])) + + // propertyType(schema) + }) + + it("e e?", () => { + const schema = S.Tuple(MyString, S.optionalElement(MyNumber)) + const equivalence = S.equivalence(schema) + + assertTrue(equivalence(["a"], ["a"])) + assertTrue(equivalence(["a", 1], ["a", 1])) + + assertFalse(equivalence(["a", 1], ["a", 2])) + assertFalse(equivalence(["a"], ["a", 1])) + assertFalse(equivalence(["a", 1], ["a"])) + + // propertyType(schema) + }) + + it("e? r", () => { + const schema = S.Tuple([S.optionalElement(S.String)], S.Number) + const equivalence = S.equivalence(schema) + + assertTrue(equivalence([], [])) + assertTrue(equivalence(["a"], ["a"])) + assertTrue(equivalence(["a", 1], ["a", 1])) + + assertFalse(equivalence([], ["a"])) + assertFalse(equivalence(["a"], [])) + assertFalse(equivalence(["a"], ["b"])) + assertFalse(equivalence(["a", 1], ["a", 2])) + + // propertyType(schema) + }) + }) + }) + + describe("struct", () => { + it("empty", () => { + const schema = S.Struct({}) + const equivalence = S.equivalence(schema) + + assertFalse(equivalence({}, {})) + }) + + it("should fail on non-record inputs", () => { + const schema = S.Struct({ a: S.String }) + const equivalence = S.equivalence(schema) + assertFalse(equivalence({ a: "a" }, 1 as never)) + }) + + it("string keys", () => { + const schema = S.Struct({ a: MyString, b: MyNumber }) + const equivalence = S.equivalence(schema) + + assertTrue(equivalence({ a: "a", b: 1 }, { a: "a", b: 1 })) + // should ignore excess properties + const d = Symbol.for("effect/Schema/test/d") + const excess = { + a: "a", + b: 1, + c: true, + [d]: "d" + } + assertTrue(equivalence({ a: "a", b: 1 }, excess)) + + assertFalse(equivalence({ a: "a", b: 1 }, { a: "c", b: 1 })) + assertFalse(equivalence({ a: "a", b: 1 }, { a: "a", b: 2 })) + + // propertyType(schema) + }) + + it("symbol keys", () => { + const a = Symbol.for("effect/Schema/test/a") + const b = Symbol.for("effect/Schema/test/b") + const schema = S.Struct({ [a]: MyString, [b]: MyNumber }) + const equivalence = S.equivalence(schema) + + assertTrue(equivalence({ [a]: "a", [b]: 1 }, { [a]: "a", [b]: 1 })) + // should ignore excess properties + const d = Symbol.for("effect/Schema/test/d") + const excess = { + [a]: "a", + [b]: 1, + c: true, + [d]: "d" + } + assertTrue(equivalence({ [a]: "a", [b]: 1 }, excess)) + + assertFalse(equivalence({ [a]: "a", [b]: 1 }, { [a]: "c", [b]: 1 })) + assertFalse(equivalence({ [a]: "a", [b]: 1 }, { [a]: "a", [b]: 2 })) + + // propertyType(schema) + }) + + it("exact optional property signature", () => { + const schema = S.Struct({ + a: S.optionalWith(MyString, { exact: true }), + b: S.optionalWith(S.Union(MyNumber, S.Undefined), { exact: true }) + }) + const equivalence = S.equivalence(schema) + + assertTrue(equivalence({ a: "a", b: 1 }, { a: "a", b: 1 })) + assertTrue(equivalence({ b: 1 }, { b: 1 })) + assertTrue(equivalence({ a: "a" }, { a: "a" })) + assertTrue(equivalence({ a: "a", b: undefined }, { a: "a", b: undefined })) + + assertFalse(equivalence({ a: "a" }, { b: 1 })) + assertFalse(equivalence({ a: "a", b: 1 }, { a: "a" })) + assertFalse(equivalence({ a: "a", b: undefined }, { a: "a" })) + assertFalse(equivalence({ a: "a" }, { a: "a", b: 1 })) + assertFalse(equivalence({ a: "a" }, { a: "a", b: undefined })) + assertFalse(equivalence({ a: "a", b: 1 }, { a: "c", b: 1 })) + assertFalse(equivalence({ a: "a", b: 1 }, { a: "a", b: 2 })) + + // propertyType(schema) + }) + }) + + describe("record", () => { + it("record(never, number)", () => { + const schema = S.Record({ key: S.Never, value: MyNumber }) + const equivalence = S.equivalence(schema) + + const input = {} + assertTrue(equivalence(input, input)) + assertFalse(equivalence({}, {})) + }) + + it("record(string, number)", () => { + const schema = S.Record({ key: MyString, value: MyNumber }) + const equivalence = S.equivalence(schema) + + assertTrue(equivalence({}, {})) + assertTrue(equivalence({ a: 1 }, { a: 1 })) + assertTrue(equivalence({ a: 1, b: 2 }, { a: 1, b: 2 })) + // should ignore symbol excess properties + const d = Symbol.for("effect/Schema/test/d") + assertTrue(equivalence({ a: 1, b: 2 }, { a: 1, b: 2, [d]: "d" })) + + assertFalse(equivalence({ a: 1 }, { a: 2 })) + assertFalse(equivalence({ a: 1, b: 2 }, { a: 1 })) + assertFalse(equivalence({ a: 1 }, { a: 1, b: 2 })) + assertFalse(equivalence({ a: 1 }, { b: 1 })) + + // propertyType(schema) + }) + + it("record(symbol, number)", () => { + const schema = S.Record({ key: MySymbol, value: MyNumber }) + const equivalence = S.equivalence(schema) + + const a = Symbol.for("effect/Schema/test/a") + const b = Symbol.for("effect/Schema/test/b") + assertTrue(equivalence({}, {})) + assertTrue(equivalence({ [a]: 1 }, { [a]: 1 })) + assertTrue(equivalence({ [a]: 1, [b]: 2 }, { [a]: 1, [b]: 2 })) + // should ignore string excess properties + const excess = { [a]: 1, [b]: 2, c: "c" } + assertTrue(equivalence({ [a]: 1, [b]: 2 }, excess)) + + assertFalse(equivalence({ [a]: 1 }, { [a]: 2 })) + assertFalse(equivalence({ [a]: 1, [b]: 2 }, { [a]: 1 })) + assertFalse(equivalence({ [a]: 1 }, { [a]: 1, [b]: 2 })) + assertFalse(equivalence({ [a]: 1 }, { [b]: 1 })) + + // propertyType(schema) + }) + + it("struct record", () => { + const schema = S.Struct({ a: MyString, b: MyString }, S.Record({ key: MyString, value: MyString })) + const equivalence = S.equivalence(schema) + + assertTrue(equivalence({ a: "a", b: "b" }, { a: "a", b: "b" })) + assertTrue(equivalence({ a: "a", b: "b", c: "c" }, { a: "a", b: "b", c: "c" })) + + assertFalse(equivalence({ a: "a", b: "b" }, { a: "c", b: "b" })) + assertFalse(equivalence({ a: "a", b: "b" }, { a: "a", b: "c" })) + assertFalse(equivalence({ a: "a", b: "b", c: "c1" }, { a: "a", b: "b", c: "c2" })) + + // propertyType(schema) + }) + + it("custom equivalence", () => { + const schema = S.Struct({ a: MyString, b: MyString }).annotations({ + equivalence: () => Equivalence.make((x, y) => x.a === y.a) + }) + const equivalence = S.equivalence(schema) + + assertTrue(equivalence({ a: "a", b: "b" }, { a: "a", b: "b" })) + assertTrue(equivalence({ a: "a", b: "b" }, { a: "a", b: "c" })) + + assertFalse(equivalence({ a: "a", b: "b" }, { a: "c", b: "b" })) + + // propertyType(schema) + }) + }) + + describe("Suspend", () => { + it("should support suspended schemas", () => { + interface A { + readonly a: string + readonly as: ReadonlyArray + } + const schema = S.Struct({ + a: MyString, + as: S.Array(S.suspend((): S.Schema => schema)) + }) + + const equivalence = S.equivalence(schema) + + const a1: A = { a: "a1", as: [] } + assertTrue(equivalence(a1, a1)) + const a2: A = { a: "a1", as: [{ a: "a2", as: [] }] } + assertTrue(equivalence(a2, a2)) + const a3: A = { a: "a1", as: [{ a: "a2", as: [] }, { a: "a3", as: [{ a: "a4", as: [] }] }] } + assertTrue(equivalence(a3, a3)) + + const a4: A = { a: "a1", as: [{ a: "a2", as: [] }, { a: "a3", as: [{ a: "a4", as: [] }] }] } + const a5: A = { a: "a1", as: [{ a: "a2", as: [] }, { a: "a3", as: [{ a: "a5", as: [] }] }] } + assertFalse(equivalence(a4, a5)) + + // propertyType(schema, { numRuns: 5 }) + }) + + it("should support mutually suspended schemas", () => { + interface Expression { + readonly type: "expression" + readonly value: number | Operation + } + + interface Operation { + readonly type: "operation" + readonly operator: "+" | "-" + readonly left: Expression + readonly right: Expression + } + + const Expression = S.Struct({ + type: S.Literal("expression"), + value: S.Union(MyNumber, S.suspend((): S.Schema => Operation)) + }) + + const Operation = S.Struct({ + type: S.Literal("operation"), + operator: S.Union(S.Literal("+"), S.Literal("-")), + left: Expression, + right: Expression + }) + + const equivalence = S.equivalence(Operation) + + const a1: Operation = { + type: "operation", + operator: "+", + left: { + type: "expression", + value: 1 + }, + right: { + type: "expression", + value: { + type: "operation", + operator: "-", + left: { + type: "expression", + value: 3 + }, + right: { + type: "expression", + value: 2 + } + } + } + } + assertTrue(equivalence(a1, a1)) + + const a2: Operation = { + type: "operation", + operator: "+", + left: { + type: "expression", + value: 1 + }, + right: { + type: "expression", + value: { + type: "operation", + operator: "-", + left: { + type: "expression", + value: 3 + }, + right: { + type: "expression", + value: 4 + } + } + } + } + assertFalse(equivalence(a1, a2)) + + // propertyType(Operation, { numRuns: 5 }) + }) + }) + + describe("should handle annotations", () => { + const expectHook = (source: S.Schema) => { + const schema = source.annotations({ equivalence: () => () => true }) + const eq = S.equivalence(schema) + assertTrue(eq("a" as any, "b" as any)) + } + + it("void", () => { + expectHook(S.Void) + }) + + it("never", () => { + expectHook(S.Never) + }) + + it("literal", () => { + expectHook(S.Literal("a")) + }) + + it("symbol", () => { + expectHook(S.Symbol) + }) + + it("uniqueSymbolFromSelf", () => { + expectHook(S.UniqueSymbolFromSelf(Symbol.for("effect/schema/test/a"))) + }) + + it("templateLiteral", () => { + expectHook(S.TemplateLiteral(S.Literal("a"), S.String, S.Literal("b"))) + }) + + it("undefined", () => { + expectHook(S.Undefined) + }) + + it("unknown", () => { + expectHook(S.Unknown) + }) + + it("any", () => { + expectHook(S.Any) + }) + + it("object", () => { + expectHook(S.Object) + }) + + it("string", () => { + expectHook(S.String) + }) + + it("number", () => { + expectHook(S.Number) + }) + + it("bigintFromSelf", () => { + expectHook(S.BigIntFromSelf) + }) + + it("boolean", () => { + expectHook(S.Boolean) + }) + + it("enums", () => { + enum Fruits { + Apple, + Banana + } + expectHook(S.Enums(Fruits)) + }) + + it("tuple", () => { + expectHook(S.Tuple(S.String, S.Number)) + }) + + it("struct", () => { + expectHook(S.Struct({ a: S.String, b: S.Number })) + }) + + it("union", () => { + expectHook(S.Union(S.String, S.Number)) + }) + + it("suspend", () => { + interface A { + readonly a: string + readonly as: ReadonlyArray + } + const schema = S.Struct({ + a: S.String, + as: S.Array(S.suspend((): S.Schema => schema)) + }) + expectHook(schema) + }) + + it("refinement", () => { + expectHook(S.Int) + }) + + it("transformation", () => { + expectHook(S.NumberFromString) + }) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/extend.test.ts b/repos/effect/packages/effect/test/Schema/Schema/extend.test.ts new file mode 100644 index 0000000..638e2c1 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/extend.test.ts @@ -0,0 +1,769 @@ +import { describe, it } from "@effect/vitest" +import { assertTrue, deepStrictEqual, strictEqual, throws } from "@effect/vitest/utils" +import * as Arbitrary from "effect/Arbitrary" +import * as FastCheck from "effect/FastCheck" +import * as Schema from "effect/Schema" +import * as AST from "effect/SchemaAST" +import * as Util from "../TestUtils.js" + +const assertExtend = (A: Schema.Schema.Any, B: Schema.Schema.Any, expected: readonly [string, string], options?: { + readonly skipFastCheck?: boolean | undefined +}) => { + const AB = Schema.extend(A, B) + const BA = Schema.extend(B, A) + strictEqual(String(AB), expected[0], "AB") + strictEqual(String(BA), expected[1], "BA") + const arbAB = Arbitrary.make(AB) + const arbBA = Arbitrary.make(BA) + const isAB = Schema.is(AB) + const isBA = Schema.is(BA) + if (options?.skipFastCheck) { + return + } + FastCheck.assert( + FastCheck.property(arbAB, (ab) => isBA(ab)), + { numRuns: 10 } + ) + FastCheck.assert( + FastCheck.property(arbBA, (ba) => isAB(ba)), + { numRuns: 10 } + ) +} + +describe("extend", () => { + describe("String", () => { + it("String & String", () => { + const schema = Schema.extend(Schema.String, Schema.String) + deepStrictEqual(schema.ast, Schema.String.ast) + }) + + it("String & Literal", () => { + const literal = Schema.Literal("a") + const schema = Schema.extend(Schema.String, literal) + deepStrictEqual(schema.ast, literal.ast) + }) + + it("Literal & String", () => { + const literal = Schema.Literal("a") + const schema = Schema.extend(literal, Schema.String) + deepStrictEqual(schema.ast, literal.ast) + }) + + it("(String with annotations) & String", () => { + const A = Schema.String.annotations({ identifier: "A" }) + const schema = Schema.extend(A, Schema.String) + assertTrue(schema.ast === A.ast) + }) + + it("String & Refinement", () => { + const schema = Schema.extend( + Schema.String, + Schema.String.pipe(Schema.startsWith("start:")) + ) + strictEqual(String(schema), `startsWith("start:")`) + assertTrue(AST.isRefinement(schema.ast)) + assertTrue(schema.ast.from === AST.stringKeyword) + }) + + it("String Branded Refinement & String Branded Refinement", () => { + const startsWith = Schema.String.pipe(Schema.startsWith("start:"), Schema.brand("start:")) + const endsWith = Schema.String.pipe(Schema.endsWith(":end"), Schema.brand(":end")) + const schema = Schema.extend(startsWith, endsWith) + strictEqual(String(schema), `startsWith("start:") & Brand<"start:"> & endsWith(":end") & Brand<":end">`) + deepStrictEqual(schema.ast.annotations[AST.BrandAnnotationId], [":end"]) + assertTrue(AST.isRefinement(schema.ast)) + const from = schema.ast.from + deepStrictEqual(from.annotations[AST.BrandAnnotationId], ["start:"]) + assertTrue(AST.isRefinement(from)) + const fromfrom = from.from + assertTrue(fromfrom === AST.stringKeyword) + }) + }) + + describe("Number", () => { + it("Number & Number", () => { + const schema = Schema.extend(Schema.Number, Schema.Number) + deepStrictEqual(schema.ast, Schema.Number.ast) + }) + + it("Number & Literal", () => { + const literal = Schema.Literal(1) + const schema = Schema.extend(Schema.Number, literal) + deepStrictEqual(schema.ast, literal.ast) + }) + + it("Literal & Number", () => { + const literal = Schema.Literal(1) + const schema = Schema.extend(literal, Schema.Number) + deepStrictEqual(schema.ast, literal.ast) + }) + + it("(Number with annotations) & Number", () => { + const A = Schema.Number.annotations({ identifier: "A" }) + const schema = Schema.extend(A, Schema.Number) + assertTrue(schema.ast === A.ast) + }) + + it("Number & Refinement", () => { + const schema = Schema.extend( + Schema.Number, + Schema.Number.pipe(Schema.greaterThan(0)) + ) + assertTrue(AST.isRefinement(schema.ast)) + assertTrue(schema.ast.from === AST.numberKeyword) + }) + + it("Number Branded Refinement & Number Branded Refinement", () => { + const gt0 = Schema.Number.pipe(Schema.greaterThan(0), Schema.brand("> 0")) + const lt2 = Schema.Number.pipe(Schema.lessThan(2), Schema.brand("< 2")) + const schema = Schema.extend(gt0, lt2) + strictEqual(String(schema.ast), `greaterThan(0) & Brand<"> 0"> & lessThan(2) & Brand<"< 2">`) + deepStrictEqual(schema.ast.annotations[AST.BrandAnnotationId], ["< 2"]) + assertTrue(AST.isRefinement(schema.ast)) + const from = schema.ast.from + deepStrictEqual(from.annotations[AST.BrandAnnotationId], ["> 0"]) + assertTrue(AST.isRefinement(from)) + const fromfrom = from.from + assertTrue(fromfrom === AST.numberKeyword) + }) + }) + + describe("Boolean", () => { + it("Boolean & Boolean", () => { + const schema = Schema.extend(Schema.Boolean, Schema.Boolean) + deepStrictEqual(schema.ast, Schema.Boolean.ast) + }) + + it("Boolean & Literal", () => { + const literal = Schema.Literal(true) + const schema = Schema.extend(Schema.Boolean, literal) + deepStrictEqual(schema.ast, literal.ast) + }) + + it("Literal & Boolean", () => { + const literal = Schema.Literal(true) + const schema = Schema.extend(literal, Schema.Boolean) + deepStrictEqual(schema.ast, literal.ast) + }) + }) + + describe("Struct", () => { + it("Struct & Struct", async () => { + const A = Schema.Struct({ a: Schema.String }) + const B = Schema.Struct({ b: Schema.Number }) + assertExtend(A, B, [ + "{ readonly a: string; readonly b: number }", + "{ readonly b: number; readonly a: string }" + ]) + }) + + it("Struct $ TypeLiteralTransformation", async () => { + const A = Schema.Struct({ a: Schema.Number }) + const B = Schema.Struct({ + b: Schema.String, + c: Schema.optionalWith(Schema.String, { exact: true, default: () => "" }) + }) + assertExtend(A, B, [ + "({ readonly b: string; readonly c?: string; readonly a: number } <-> { readonly b: string; readonly c: string; readonly a: number })", + "({ readonly b: string; readonly c?: string; readonly a: number } <-> { readonly b: string; readonly c: string; readonly a: number })" + ]) + }) + + it("Struct & Union", () => { + const A = Schema.Struct({ b: Schema.Boolean }) + const B = Schema.Union( + Schema.Struct({ a: Schema.Literal("a") }), + Schema.Struct({ a: Schema.Literal("b") }) + ) + assertExtend(A, B, [ + `{ readonly b: boolean; readonly a: "a" } | { readonly b: boolean; readonly a: "b" }`, + `{ readonly a: "a"; readonly b: boolean } | { readonly a: "b"; readonly b: boolean }` + ]) + }) + + it("Struct & Record(string, string)", async () => { + const A = Schema.Struct({ a: Schema.String }) + const B = Schema.Record({ key: Schema.String, value: Schema.String }) + assertExtend(A, B, [ + `{ readonly a: string; readonly [x: string]: string }`, + `{ readonly a: string; readonly [x: string]: string }` + ]) + }) + + it("Struct & Record(templateLiteral, string)", async () => { + const A = Schema.Struct({ a: Schema.String }) + const B = Schema.Record( + { + key: Schema.TemplateLiteral( + Schema.String, + Schema.Literal("-"), + Schema.Number + ), + value: Schema.String + } + ) + assertExtend(A, B, [ + "{ readonly a: string; readonly [x: `${string}-${number}`]: string }", + "{ readonly a: string; readonly [x: `${string}-${number}`]: string }" + ]) + }) + + it("Struct & Record(string, NumberFromChar)", async () => { + const A = Schema.Struct({ a: Schema.Number }) + const B = Schema.Record({ key: Schema.String, value: Util.NumberFromChar }) + assertExtend(A, B, [ + `{ readonly a: number; readonly [x: string]: NumberFromChar }`, + `{ readonly a: number; readonly [x: string]: NumberFromChar }` + ]) + }) + + it("Struct & Record(symbol, NumberFromChar)", async () => { + const A = Schema.Struct({ a: Schema.Number }) + const B = Schema.Record({ key: Schema.SymbolFromSelf, value: Util.NumberFromChar }) + assertExtend(A, B, [ + `{ readonly a: number; readonly [x: symbol]: NumberFromChar }`, + `{ readonly a: number; readonly [x: symbol]: NumberFromChar }` + ]) + }) + + it("Nested Struct & Nested Struct", async () => { + const A = Schema.Struct({ a: Schema.Struct({ b: Schema.String }) }) + const B = Schema.Struct({ a: Schema.Struct({ c: Schema.Number }) }) + assertExtend(A, B, [ + `{ readonly a: { readonly b: string; readonly c: number } }`, + `{ readonly a: { readonly c: number; readonly b: string } }` + ]) + }) + + it("Nested Struct with refinements & Nested struct with refinements", async () => { + const A = Schema.Struct({ + nested: Schema.Struct({ + same: Schema.String.pipe(Schema.startsWith("start:")), + different1: Schema.String + }) + }) + const B = Schema.Struct({ + nested: Schema.Struct({ + same: Schema.String.pipe(Schema.endsWith(":end")), + different2: Schema.String + }) + }) + assertExtend(A, B, [ + `{ readonly nested: { readonly same: startsWith("start:") & endsWith(":end"); readonly different1: string; readonly different2: string } }`, + `{ readonly nested: { readonly same: endsWith(":end") & startsWith("start:"); readonly different2: string; readonly different1: string } }` + ], { skipFastCheck: true }) + const schema = Schema.extend(A, B) + await Util.assertions.decoding.succeed( + schema, + { + nested: { + same: "start:5:end", + different1: "", + different2: "" + } + } + ) + await Util.assertions.decoding.fail( + schema, + { + nested: { + same: "", + different1: "", + different2: "" + } + }, + `{ readonly nested: { readonly same: startsWith("start:") & endsWith(":end"); readonly different1: string; readonly different2: string } } +└─ ["nested"] + └─ { readonly same: startsWith("start:") & endsWith(":end"); readonly different1: string; readonly different2: string } + └─ ["same"] + └─ startsWith("start:") & endsWith(":end") + └─ From side refinement failure + └─ startsWith("start:") + └─ Predicate refinement failure + └─ Expected a string starting with "start:", actual ""` + ) + await Util.assertions.decoding.fail( + schema, + { + nested: { + same: "start:5", + different1: "", + different2: "" + } + }, + `{ readonly nested: { readonly same: startsWith("start:") & endsWith(":end"); readonly different1: string; readonly different2: string } } +└─ ["nested"] + └─ { readonly same: startsWith("start:") & endsWith(":end"); readonly different1: string; readonly different2: string } + └─ ["same"] + └─ startsWith("start:") & endsWith(":end") + └─ Predicate refinement failure + └─ Expected a string ending with ":end", actual "start:5"` + ) + }) + }) + + describe("TypeLiteralTransformation", () => { + it("TypeLiteralTransformation & Struct", async () => { + const A = Schema.Struct({ + a: Schema.optionalWith(Schema.String, { exact: true, default: () => "" }), + b: Schema.String + }) + const B = Schema.Struct({ c: Schema.Number }) + assertExtend(A, B, [ + "({ readonly a?: string; readonly b: string; readonly c: number } <-> { readonly a: string; readonly b: string; readonly c: number })", + "({ readonly a?: string; readonly b: string; readonly c: number } <-> { readonly a: string; readonly b: string; readonly c: number })" + ]) + }) + + it("TypeLiteralTransformation & Union", async () => { + const A = Schema.Struct({ + a: Schema.optionalWith(Schema.String, { default: () => "default" }) + }) + const B = Schema.Union( + Schema.Struct({ b: Schema.String }), + Schema.Struct({ c: Schema.String }) + ) + assertExtend(A, B, [ + "({ readonly a?: string | undefined; readonly b: string } <-> { readonly a: string; readonly b: string }) | ({ readonly a?: string | undefined; readonly c: string } <-> { readonly a: string; readonly c: string })", + "({ readonly a?: string | undefined; readonly b: string } <-> { readonly a: string; readonly b: string }) | ({ readonly a?: string | undefined; readonly c: string } <-> { readonly a: string; readonly c: string })" + ]) + }) + + it("TypeLiteralTransformation & Refinement", async () => { + const A = Schema.Struct({ + a: Schema.optionalWith(Schema.String, { default: () => "default" }) + }) + const B = Schema.Struct({ b: Schema.String }).pipe(Schema.filter(() => true)) + assertExtend(A, B, [ + "{ ({ readonly a?: string | undefined; readonly b: string } <-> { readonly a: string; readonly b: string }) | filter }", + "{ ({ readonly a?: string | undefined; readonly b: string } <-> { readonly a: string; readonly b: string }) | filter }" + ]) + }) + + it("TypeLiteralTransformation & Suspend", async () => { + const suspend = Schema.suspend(() => Schema.Struct({ b: Schema.String })) + const schema = Schema.extend( + Schema.Struct({ + a: Schema.optionalWith(Schema.String, { default: () => "default" }) + }), + suspend + ) + strictEqual( + String((schema.ast as AST.Suspend).f()), + "({ readonly a?: string | undefined; readonly b: string } <-> { readonly a: string; readonly b: string })" + ) + }) + + it("TypeLiteralTransformation & TypeLiteralTransformation", async () => { + const A = Schema.Struct({ + a: Schema.optionalWith(Schema.String, { exact: true, default: () => "" }), + b: Schema.String + }) + const B = Schema.Struct({ + c: Schema.optionalWith(Schema.Number, { exact: true, default: () => 0 }), + d: Schema.Boolean + }) + assertExtend(A, B, [ + "({ readonly a?: string; readonly b: string; readonly c?: number; readonly d: boolean } <-> { readonly a: string; readonly b: string; readonly c: number; readonly d: boolean })", + "({ readonly c?: number; readonly d: boolean; readonly a?: string; readonly b: string } <-> { readonly c: number; readonly d: boolean; readonly a: string; readonly b: string })" + ]) + }) + }) + + describe("FinalTransformation", () => { + it("FinalTransformation & Struct", async () => { + const A = Schema.Struct({ + a: Schema.String + }) + + const B = Schema.Struct({ + b: Schema.String + }) + + const C = Schema.Struct({ + c: Schema.String + }) + + const AB = Schema.transform(A, B, { + strict: true, + decode: (a) => ({ b: a.a }), + encode: (b) => ({ a: b.b }) + }) + + assertExtend(AB, C, [ + "({ readonly a: string; readonly c: string } <-> { readonly b: string; readonly c: string })", + "({ readonly a: string; readonly c: string } <-> { readonly b: string; readonly c: string })" + ]) + }) + }) + + describe("ComposeTransformation", () => { + it("ComposeTransformation & Struct", async () => { + const A = Schema.Struct({ + a: Schema.NumberFromString + }) + + const B = Schema.Struct({ + a: Schema.Number + }) + + const AB = Schema.compose(A, B) + + const C = Schema.Struct({ + c: Schema.String + }) + + assertExtend(AB, C, [ + "({ readonly a: NumberFromString; readonly c: string } <-> { readonly a: number; readonly c: string })", + "({ readonly a: NumberFromString; readonly c: string } <-> { readonly a: number; readonly c: string })" + ]) + + const schema = Schema.extend(AB, C) + await Util.assertions.decoding.succeed( + schema, + { a: "1", c: "c" }, + { a: 1, c: "c" } + ) + await Util.assertions.decoding.fail( + schema, + { a: "a", c: "c" }, + `({ readonly a: NumberFromString; readonly c: string } <-> { readonly a: number; readonly c: string }) +└─ Encoded side transformation failure + └─ { readonly a: NumberFromString; readonly c: string } + └─ ["a"] + └─ NumberFromString + └─ Transformation process failure + └─ Unable to decode "a" into a number` + ) + }) + }) + + describe("Union", () => { + it("Union & Struct", () => { + const A = Schema.Union( + Schema.Struct({ a: Schema.Literal("a") }), + Schema.Struct({ b: Schema.Literal("b") }) + ) + const B = Schema.Struct({ c: Schema.Boolean }) + assertExtend(A, B, [ + `{ readonly a: "a"; readonly c: boolean } | { readonly b: "b"; readonly c: boolean }`, + `{ readonly c: boolean; readonly a: "a" } | { readonly c: boolean; readonly b: "b" }` + ]) + }) + + it("Union of structs with defaults & Union of structs with defaults", async () => { + const A = Schema.Union( + Schema.Struct({ + a: Schema.optionalWith(Schema.String, { exact: true, default: () => "a" }), + b: Schema.String + }), + Schema.Struct({ + c: Schema.optionalWith(Schema.String, { exact: true, default: () => "c" }), + d: Schema.String + }) + ) + const B = Schema.Union( + Schema.Struct({ + e: Schema.optionalWith(Schema.String, { exact: true, default: () => "e" }), + f: Schema.String + }), + Schema.Struct({ + g: Schema.optionalWith(Schema.String, { exact: true, default: () => "g" }), + h: Schema.String + }) + ) + assertExtend(A, B, [ + "({ readonly a?: string; readonly b: string; readonly e?: string; readonly f: string } <-> { readonly a: string; readonly b: string; readonly e: string; readonly f: string }) | ({ readonly a?: string; readonly b: string; readonly g?: string; readonly h: string } <-> { readonly a: string; readonly b: string; readonly g: string; readonly h: string }) | ({ readonly c?: string; readonly d: string; readonly e?: string; readonly f: string } <-> { readonly c: string; readonly d: string; readonly e: string; readonly f: string }) | ({ readonly c?: string; readonly d: string; readonly g?: string; readonly h: string } <-> { readonly c: string; readonly d: string; readonly g: string; readonly h: string })", + "({ readonly e?: string; readonly f: string; readonly a?: string; readonly b: string } <-> { readonly e: string; readonly f: string; readonly a: string; readonly b: string }) | ({ readonly e?: string; readonly f: string; readonly c?: string; readonly d: string } <-> { readonly e: string; readonly f: string; readonly c: string; readonly d: string }) | ({ readonly g?: string; readonly h: string; readonly a?: string; readonly b: string } <-> { readonly g: string; readonly h: string; readonly a: string; readonly b: string }) | ({ readonly g?: string; readonly h: string; readonly c?: string; readonly d: string } <-> { readonly g: string; readonly h: string; readonly c: string; readonly d: string })" + ]) + }) + + it("Union & Union", () => { + const A = Schema.Union( + Schema.Struct({ a: Schema.Literal("a") }), + Schema.Struct({ a: Schema.Literal("b") }) + ) + const B = Schema.Union( + Schema.Struct({ c: Schema.Boolean }), + Schema.Struct({ d: Schema.Number }) + ) + assertExtend(A, B, [ + `{ readonly a: "a"; readonly c: boolean } | { readonly a: "a"; readonly d: number } | { readonly a: "b"; readonly c: boolean } | { readonly a: "b"; readonly d: number }`, + `{ readonly c: boolean; readonly a: "a" } | { readonly c: boolean; readonly a: "b" } | { readonly d: number; readonly a: "a" } | { readonly d: number; readonly a: "b" }` + ]) + }) + + it("Nested Union & Struct", () => { + const A = Schema.Union( + Schema.Union( + Schema.Struct({ a: Schema.Literal("a") }), + Schema.Struct({ a: Schema.Literal("b") }) + ), + Schema.Struct({ b: Schema.Literal("b") }) + ) + const B = Schema.Struct({ c: Schema.Boolean }) + assertExtend(A, B, [ + `{ readonly a: "a"; readonly c: boolean } | { readonly a: "b"; readonly c: boolean } | { readonly b: "b"; readonly c: boolean }`, + `{ readonly c: boolean; readonly a: "a" } | { readonly c: boolean; readonly a: "b" } | { readonly c: boolean; readonly b: "b" }` + ]) + }) + }) + + describe("Refinements", () => { + it("Struct & Refinement", async () => { + const A = Schema.Struct({ a: Schema.String }) + const B = Schema.Struct({ b: Schema.Number }).pipe( + Schema.filter((input) => input.b > 0, { message: () => "R filter" }) + ) + assertExtend(A, B, [ + `{ { readonly a: string; readonly b: number } | filter }`, + `{ { readonly b: number; readonly a: string } | filter }` + ]) + const schema = Schema.extend(A, B) + await Util.assertions.decoding.fail( + schema, + { a: "a", b: -1 }, + `R filter` + ) + }) + + it("Struct & Refinement (two filters)", async () => { + const A = Schema.Struct({ a: Schema.String }) + const B = Schema.Struct({ b: Schema.Number }).pipe( + Schema.filter((input) => input.b > 0, { message: () => "filter1" }), + Schema.filter((input) => input.b < 10, { message: () => "filter2" }) + ) + assertExtend(A, B, [ + `{ { { readonly a: string; readonly b: number } | filter } | filter }`, + `{ { { readonly b: number; readonly a: string } | filter } | filter }` + ]) + const schema = Schema.extend(A, B) + await Util.assertions.decoding.fail( + schema, + { a: "a", b: -1 }, + `filter1` + ) + await Util.assertions.decoding.fail( + schema, + { a: "a", b: 11 }, + `filter2` + ) + }) + + it("Refinement & Refinement", async () => { + const A = Schema.Struct({ a: Schema.String }).pipe( + Schema.filter((input) => input.a.length > 0, { message: () => "R1 filter" }) + ) + const B = Schema.Struct({ b: Schema.Number }).pipe( + Schema.filter((input) => input.b > 0, { message: () => "R2 filter" }) + ) + assertExtend(A, B, [ + `{ { { readonly a: string; readonly b: number } | filter } | filter }`, + `{ { { readonly b: number; readonly a: string } | filter } | filter }` + ]) + const schema = Schema.extend(A, B) + await Util.assertions.decoding.fail( + schema, + { a: "", b: 1 }, + `R1 filter` + ) + await Util.assertions.decoding.fail( + schema, + { a: "a", b: -1 }, + `R2 filter` + ) + }) + + it("Union of structs & Refinement", async () => { + const S1 = Schema.Struct({ a: Schema.String }) + const S2 = Schema.Struct({ b: Schema.Number }) + const B = Schema.Struct({ c: Schema.Boolean }).pipe( + Schema.filter((input) => input.c === true, { message: () => "R filter" }) + ) + const A = Schema.Union(S1, S2) + assertExtend(A, B, [ + `{ { readonly a: string; readonly c: boolean } | filter } | { { readonly b: number; readonly c: boolean } | filter }`, + `{ { readonly c: boolean; readonly a: string } | filter } | { { readonly c: boolean; readonly b: number } | filter }` + ]) + + const schema = Schema.extend(A, B) + await Util.assertions.decoding.fail( + schema, + { a: "a", c: false }, + `{ { readonly a: string; readonly c: boolean } | filter } | { { readonly b: number; readonly c: boolean } | filter } +├─ R filter +└─ { { readonly b: number; readonly c: boolean } | filter } + └─ From side refinement failure + └─ { readonly b: number; readonly c: boolean } + └─ ["b"] + └─ is missing` + ) + await Util.assertions.decoding.fail( + schema, + { b: 1, c: false }, + `{ { readonly a: string; readonly c: boolean } | filter } | { { readonly b: number; readonly c: boolean } | filter } +├─ { { readonly a: string; readonly c: boolean } | filter } +│ └─ From side refinement failure +│ └─ { readonly a: string; readonly c: boolean } +│ └─ ["a"] +│ └─ is missing +└─ R filter` + ) + }) + + it("Union of refinements & Refinement", async () => { + const R1 = Schema.Struct({ a: Schema.String }).pipe( + Schema.filter((input) => input.a.length > 0, { message: () => "R1 filter" }) + ) + const R2 = Schema.Struct({ b: Schema.Number }).pipe( + Schema.filter((input) => input.b > 0, { message: () => "R2 filter" }) + ) + const B = Schema.Struct({ c: Schema.Boolean }).pipe( + Schema.filter((input) => input.c === true, { message: () => "R3 filter" }) + ) + const A = Schema.Union(R1, R2) + assertExtend(A, B, [ + `{ { { readonly a: string; readonly c: boolean } | filter } | filter } | { { { readonly b: number; readonly c: boolean } | filter } | filter }`, + `{ { { readonly c: boolean; readonly a: string } | filter } | filter } | { { { readonly c: boolean; readonly b: number } | filter } | filter }` + ]) + const schema = Schema.extend(A, B) + await Util.assertions.decoding.fail( + schema, + { a: "", c: true }, + `{ { { readonly a: string; readonly c: boolean } | filter } | filter } | { { { readonly b: number; readonly c: boolean } | filter } | filter } +├─ R1 filter +└─ { { { readonly b: number; readonly c: boolean } | filter } | filter } + └─ From side refinement failure + └─ { { readonly b: number; readonly c: boolean } | filter } + └─ From side refinement failure + └─ { readonly b: number; readonly c: boolean } + └─ ["b"] + └─ is missing` + ) + await Util.assertions.decoding.fail( + schema, + { b: -1, c: true }, + `{ { { readonly a: string; readonly c: boolean } | filter } | filter } | { { { readonly b: number; readonly c: boolean } | filter } | filter } +├─ { { { readonly a: string; readonly c: boolean } | filter } | filter } +│ └─ From side refinement failure +│ └─ { { readonly a: string; readonly c: boolean } | filter } +│ └─ From side refinement failure +│ └─ { readonly a: string; readonly c: boolean } +│ └─ ["a"] +│ └─ is missing +└─ R2 filter` + ) + await Util.assertions.decoding.fail( + schema, + { a: "a", c: false }, + `{ { { readonly a: string; readonly c: boolean } | filter } | filter } | { { { readonly b: number; readonly c: boolean } | filter } | filter } +├─ R3 filter +└─ { { { readonly b: number; readonly c: boolean } | filter } | filter } + └─ From side refinement failure + └─ { { readonly b: number; readonly c: boolean } | filter } + └─ From side refinement failure + └─ { readonly b: number; readonly c: boolean } + └─ ["b"] + └─ is missing` + ) + await Util.assertions.decoding.fail( + schema, + { b: 1, c: false }, + `{ { { readonly a: string; readonly c: boolean } | filter } | filter } | { { { readonly b: number; readonly c: boolean } | filter } | filter } +├─ { { { readonly a: string; readonly c: boolean } | filter } | filter } +│ └─ From side refinement failure +│ └─ { { readonly a: string; readonly c: boolean } | filter } +│ └─ From side refinement failure +│ └─ { readonly a: string; readonly c: boolean } +│ └─ ["a"] +│ └─ is missing +└─ R3 filter` + ) + }) + }) + + describe("Suspend", () => { + it("List as union", async () => { + type List = { + readonly type: "nil" + } | { + readonly type: "cons" + readonly tail: { + readonly value: number + } & List + } + const List = Schema.Union( + Schema.Struct({ type: Schema.Literal("nil") }), + Schema.Struct({ + type: Schema.Literal("cons"), + tail: Schema.extend( + Schema.Struct({ value: Schema.Number }), + Schema.suspend((): Schema.Schema => List) + ) + }) + ) + strictEqual( + String(List), + `{ readonly type: "nil" } | { readonly type: "cons"; readonly tail: }` + ) + await Util.assertions.decoding.succeed(List, { type: "nil" }) + await Util.assertions.decoding.succeed(List, { type: "cons", tail: { value: 1, type: "nil" } }) + await Util.assertions.decoding.succeed(List, { + type: "cons", + tail: { value: 1, type: "cons", tail: { value: 2, type: "nil" } } + }) + const decodeUnknownSync = Schema.decodeUnknownSync(List) + const arb = Arbitrary.make(List) + FastCheck.assert( + FastCheck.property(arb, (a) => { + decodeUnknownSync(a) + }), + { numRuns: 10 } + ) + }) + }) + + it("Errors", () => { + throws( + () => Schema.String.pipe(Schema.extend(Schema.Number)), + new Error(`Unsupported schema or overlapping types +details: cannot extend string with number`) + ) + throws( + () => + Schema.Record({ key: Schema.String, value: Schema.Number }).pipe( + Schema.extend(Schema.Record({ key: Schema.String, value: Schema.Boolean })) + ), + new Error(`Duplicate index signature +details: string index signature`) + ) + throws( + () => + Schema.Record({ key: Schema.SymbolFromSelf, value: Schema.Number }).pipe( + Schema.extend(Schema.Record({ key: Schema.SymbolFromSelf, value: Schema.Boolean })) + ), + new Error(`Duplicate index signature +details: symbol index signature`) + ) + throws( + () => + Schema.Record({ key: Schema.String, value: Schema.Number }).pipe( + Schema.extend(Schema.Record({ key: Schema.String.pipe(Schema.minLength(2)), value: Schema.Boolean })) + ), + new Error(`Duplicate index signature +details: string index signature`) + ) + throws( + () => + Schema.extend( + Schema.Struct({ a: Schema.Struct({ b: Schema.String }) }), + Schema.Struct({ a: Schema.Struct({ b: Schema.Number }) }) + ), + new Error( + `Unsupported schema or overlapping types +at path: ["a"]["b"] +details: cannot extend string with number` + ) + ) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/filter.test.ts b/repos/effect/packages/effect/test/Schema/Schema/filter.test.ts new file mode 100644 index 0000000..3e5015e --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/filter.test.ts @@ -0,0 +1,419 @@ +import { describe, it } from "@effect/vitest" +import { deepStrictEqual, strictEqual } from "@effect/vitest/utils" +import * as ParseResult from "effect/ParseResult" +import * as S from "effect/Schema" +import * as AST from "effect/SchemaAST" +import * as Util from "../TestUtils.js" + +describe("filter", () => { + describe("error messages", () => { + it("single refinement", async () => { + const schema = S.Number.pipe(S.int()) + await Util.assertions.decoding.fail( + schema, + null, + `int +└─ From side refinement failure + └─ Expected number, actual null` + ) + await Util.assertions.decoding.fail( + schema, + 1.1, + `int +└─ Predicate refinement failure + └─ Expected an integer, actual 1.1` + ) + }) + + it("double refinement", async () => { + const schema = S.Number.pipe(S.int(), S.positive()) + await Util.assertions.decoding.fail( + schema, + null, + `int & positive +└─ From side refinement failure + └─ int + └─ From side refinement failure + └─ Expected number, actual null` + ) + await Util.assertions.decoding.fail( + schema, + 1.1, + `int & positive +└─ From side refinement failure + └─ int + └─ Predicate refinement failure + └─ Expected an integer, actual 1.1` + ) + await Util.assertions.decoding.fail( + schema, + -1, + `int & positive +└─ Predicate refinement failure + └─ Expected a positive number, actual -1` + ) + }) + + it("with an anonymous refinement", async () => { + const schema = S.Number.pipe(S.filter(() => false), S.positive()) + await Util.assertions.decoding.fail( + schema, + 1, + `{ number | filter } & positive +└─ From side refinement failure + └─ { number | filter } + └─ Predicate refinement failure + └─ Expected { number | filter }, actual 1` + ) + }) + }) + + it("annotation options", () => { + const schema = S.String.pipe( + S.filter((s): s is string => s.length === 1, { + schemaId: Symbol.for("Char"), + description: "description", + documentation: "documentation", + examples: ["examples"], + identifier: "identifier", + jsonSchema: { minLength: 1, maxLength: 1 }, + title: "title" + }) + ) + deepStrictEqual(schema.ast.annotations, { + [AST.SchemaIdAnnotationId]: Symbol.for("Char"), + [AST.DescriptionAnnotationId]: "description", + [AST.DocumentationAnnotationId]: "documentation", + [AST.ExamplesAnnotationId]: [ + "examples" + ], + [AST.IdentifierAnnotationId]: "identifier", + [AST.JSONSchemaAnnotationId]: { + "maxLength": 1, + "minLength": 1 + }, + [AST.TitleAnnotationId]: "title" + }) + }) + + it("the constructor should validate the input by default", () => { + const schema = S.NonEmptyString + Util.assertions.make.succeed(schema, "a") + Util.assertions.make.fail( + schema, + "", + `NonEmptyString +└─ Predicate refinement failure + └─ Expected a non empty string, actual ""` + ) + }) + + it("the constructor validation can be disabled", () => { + const schema = S.NonEmptyString + strictEqual(schema.make("", true), "") + strictEqual(schema.make("", { disableValidation: true }), "") + }) + + describe("ParseIssue overloading", () => { + it("return a Type", async () => { + const schema = S.Struct({ a: S.String, b: S.String }).pipe( + S.filter((o) => { + if (o.b !== o.a) { + return new ParseResult.Type(S.Literal(o.a).ast, o.b, `b should be equal to a's value ("${o.a}")`) + } + }) + ) + + await Util.assertions.decoding.succeed(schema, { a: "x", b: "x" }) + await Util.assertions.decoding.fail( + schema, + { a: "a", b: "b" }, + `{ { readonly a: string; readonly b: string } | filter } +└─ Predicate refinement failure + └─ b should be equal to a's value ("a")` + ) + }) + + const ValidString = S.Trim.pipe(S.minLength(1, { message: () => "ERROR_MIN_LENGTH" })) + const Test = S.Struct({ + a: S.Struct({ + b: S.String, + c: ValidString + }), + d: S.Tuple(S.String, ValidString) + }).annotations({ identifier: "Test" }) + + it("return a Pointer", async () => { + const schema = Test.pipe(S.filter((input) => { + if (input.a.b !== input.a.c) { + return new ParseResult.Pointer( + ["a", "c"], + input, + new ParseResult.Type(S.Literal(input.a.b).ast, input.a.c) + ) + } + if (input.d[0] !== input.d[1]) { + return new ParseResult.Pointer( + ["d", 1], + input, + new ParseResult.Type(S.Literal(input.d[0]).ast, input.d[1]) + ) + } + })) + await Util.assertions.decoding.fail( + schema, + { a: { b: "b", c: " " }, d: ["-", "-"] }, + `{ Test | filter } +└─ From side refinement failure + └─ Test + └─ ["a"] + └─ { readonly b: string; readonly c: minLength(1) } + └─ ["c"] + └─ ERROR_MIN_LENGTH` + ) + await Util.assertions.decoding.fail( + schema, + { a: { b: "b", c: "c" }, d: ["-", "-"] }, + `{ Test | filter } +└─ Predicate refinement failure + └─ ["a"]["c"] + └─ Expected "b", actual "c"` + ) + await Util.assertions.decoding.fail( + schema, + { a: { b: "-", c: "-" }, d: ["item0", "item1"] }, + `{ Test | filter } +└─ Predicate refinement failure + └─ ["d"][1] + └─ Expected "item0", actual "item1"` + ) + }) + + it("return a path and a message", async () => { + const schema = Test.pipe(S.filter((input) => { + if (input.a.b !== input.a.c) { + return { + path: ["a", "c"], + message: "FILTER1" + } + } + if (input.d[0] !== input.d[1]) { + return { + path: ["d", 1], + message: "FILTER2" + } + } + })) + await Util.assertions.decoding.fail( + schema, + { a: { b: "b", c: " " }, d: ["-", "-"] }, + `{ Test | filter } +└─ From side refinement failure + └─ Test + └─ ["a"] + └─ { readonly b: string; readonly c: minLength(1) } + └─ ["c"] + └─ ERROR_MIN_LENGTH` + ) + await Util.assertions.decoding.fail( + schema, + { a: { b: "b", c: "c" }, d: ["-", "-"] }, + `{ Test | filter } +└─ Predicate refinement failure + └─ ["a"]["c"] + └─ FILTER1` + ) + await Util.assertions.decoding.fail( + schema, + { a: { b: "-", c: "-" }, d: ["item0", "item1"] }, + `{ Test | filter } +└─ Predicate refinement failure + └─ ["d"][1] + └─ FILTER2` + ) + }) + + it("return many paths and messages", async () => { + const schema = Test.pipe(S.filter((input) => { + const issues: Array = [] + if (input.a.b !== input.a.c) { + issues.push({ + path: ["a", "c"], + message: "FILTER1" + }) + } + if (input.d[0] !== input.d[1]) { + issues.push({ + path: ["d", 1], + message: "FILTER2" + }) + } + return issues + })) + await Util.assertions.decoding.fail( + schema, + { a: { b: "b", c: " " }, d: ["-", "-"] }, + `{ Test | filter } +└─ From side refinement failure + └─ Test + └─ ["a"] + └─ { readonly b: string; readonly c: minLength(1) } + └─ ["c"] + └─ ERROR_MIN_LENGTH` + ) + await Util.assertions.decoding.fail( + schema, + { a: { b: "b", c: "c" }, d: ["-", "-"] }, + `{ Test | filter } +└─ Predicate refinement failure + └─ ["a"]["c"] + └─ FILTER1` + ) + await Util.assertions.decoding.fail( + schema, + { a: { b: "-", c: "-" }, d: ["item0", "item1"] }, + `{ Test | filter } +└─ Predicate refinement failure + └─ ["d"][1] + └─ FILTER2` + ) + await Util.assertions.decoding.fail( + schema, + { a: { b: "b", c: "c" }, d: ["item0", "item1"] }, + `{ Test | filter } +└─ Predicate refinement failure + └─ { Test | filter } + ├─ ["a"]["c"] + │ └─ FILTER1 + └─ ["d"][1] + └─ FILTER2` + ) + }) + }) + + describe("Stable Filters", () => { + describe("Array", () => { + it("when the 'errors' option is set to 'all', stable filters should generate multiple errors", async () => { + const schema = S.Struct({ + tags: S.Array(S.String.pipe(S.minLength(2))).pipe(S.minItems(3)) + }) + await Util.assertions.decoding.fail( + schema, + { tags: ["AB", "B"] }, + `{ readonly tags: minItems(3) } +└─ ["tags"] + └─ minItems(3) + ├─ minItems(3) + │ └─ From side refinement failure + │ └─ ReadonlyArray + │ └─ [1] + │ └─ minLength(2) + │ └─ Predicate refinement failure + │ └─ Expected a string at least 2 character(s) long, actual "B" + └─ minItems(3) + └─ Predicate refinement failure + └─ Expected an array of at least 3 item(s), actual ["AB","B"]`, + { parseOptions: Util.ErrorsAll } + ) + await Util.assertions.decoding.fail( + schema, + { tags: ["AB", "B"] }, + `{ readonly tags: minItems(3) } +└─ ["tags"] + └─ minItems(3) + └─ From side refinement failure + └─ ReadonlyArray + └─ [1] + └─ minLength(2) + └─ Predicate refinement failure + └─ Expected a string at least 2 character(s) long, actual "B"` + ) + }) + + it("when the 'errors' option is set to 'all', stable filters should be applied only if the from part fails with a `Composite` issue", async () => { + await Util.assertions.decoding.fail( + S.Struct({ + tags: S.Array(S.String).pipe(S.minItems(1)) + }), + {}, + `{ readonly tags: minItems(1) } +└─ ["tags"] + └─ is missing`, + { parseOptions: Util.ErrorsAll } + ) + await Util.assertions.decoding.fail( + S.Struct({ + tags: S.Array(S.String).pipe(S.minItems(1), S.maxItems(3)) + }), + {}, + `{ readonly tags: minItems(1) & maxItems(3) } +└─ ["tags"] + └─ is missing`, + { parseOptions: Util.ErrorsAll } + ) + }) + }) + + describe("NonEmptyArray", () => { + it("when the 'errors' option is set to 'all', stable filters should generate multiple errors", async () => { + const schema = S.Struct({ + tags: S.NonEmptyArray(S.String.pipe(S.minLength(2))).pipe(S.minItems(3)) + }) + await Util.assertions.decoding.fail( + schema, + { tags: ["AB", "B"] }, + `{ readonly tags: minItems(3) } +└─ ["tags"] + └─ minItems(3) + ├─ minItems(3) + │ └─ From side refinement failure + │ └─ readonly [minLength(2), ...minLength(2)[]] + │ └─ [1] + │ └─ minLength(2) + │ └─ Predicate refinement failure + │ └─ Expected a string at least 2 character(s) long, actual "B" + └─ minItems(3) + └─ Predicate refinement failure + └─ Expected an array of at least 3 item(s), actual ["AB","B"]`, + { parseOptions: Util.ErrorsAll } + ) + await Util.assertions.decoding.fail( + schema, + { tags: ["AB", "B"] }, + `{ readonly tags: minItems(3) } +└─ ["tags"] + └─ minItems(3) + └─ From side refinement failure + └─ readonly [minLength(2), ...minLength(2)[]] + └─ [1] + └─ minLength(2) + └─ Predicate refinement failure + └─ Expected a string at least 2 character(s) long, actual "B"` + ) + }) + + it("when the 'errors' option is set to 'all', stable filters should be applied only if the from part fails with a `Composite` issue", async () => { + await Util.assertions.decoding.fail( + S.Struct({ + tags: S.NonEmptyArray(S.String).pipe(S.minItems(1)) + }), + {}, + `{ readonly tags: minItems(1) } +└─ ["tags"] + └─ is missing`, + { parseOptions: Util.ErrorsAll } + ) + await Util.assertions.decoding.fail( + S.Struct({ + tags: S.NonEmptyArray(S.String).pipe(S.minItems(1), S.maxItems(3)) + }), + {}, + `{ readonly tags: minItems(1) & maxItems(3) } +└─ ["tags"] + └─ is missing`, + { parseOptions: Util.ErrorsAll } + ) + }) + }) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/filterEffect.test.ts b/repos/effect/packages/effect/test/Schema/Schema/filterEffect.test.ts new file mode 100644 index 0000000..69474be --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/filterEffect.test.ts @@ -0,0 +1,198 @@ +import { describe, it } from "@effect/vitest" +import { strictEqual } from "@effect/vitest/utils" +import * as Effect from "effect/Effect" +import * as ParseResult from "effect/ParseResult" +import * as S from "effect/Schema" +import * as Util from "../TestUtils.js" + +describe("filterEffect", () => { + it("should expose the original schema as `from`", () => { + const schema = S.filterEffect(S.String, () => Effect.succeed(true)) + strictEqual(schema.from, S.String) + strictEqual(schema.to.ast, S.String.ast) + }) + + describe("ParseIssue overloading", () => { + it("return a Type", async () => { + const schema = S.filterEffect(S.Struct({ a: S.String, b: S.String }), (o) => { + if (o.b !== o.a) { + return Effect.succeed( + new ParseResult.Type(S.Literal(o.a).ast, o.b, `b should be equal to a's value ("${o.a}")`) + ) + } + return Effect.succeed(true) + }) + + await Util.assertions.decoding.succeed(schema, { a: "x", b: "x" }) + await Util.assertions.decoding.fail( + schema, + { a: "a", b: "b" }, + `({ readonly a: string; readonly b: string } <-> { readonly a: string; readonly b: string }) +└─ Transformation process failure + └─ b should be equal to a's value ("a")` + ) + }) + + const ValidString = S.Trim.pipe(S.minLength(1, { message: () => "ERROR_MIN_LENGTH" })) + const Test = S.Struct({ + a: S.Struct({ + b: S.String, + c: ValidString + }), + d: S.Tuple(S.String, ValidString) + }).annotations({ identifier: "Test" }) + + it("return a Pointer", async () => { + const schema = Test.pipe(S.filterEffect((input) => { + if (input.a.b !== input.a.c) { + return Effect.succeed( + new ParseResult.Pointer( + ["a", "c"], + input, + new ParseResult.Type(S.Literal(input.a.b).ast, input.a.c) + ) + ) + } + if (input.d[0] !== input.d[1]) { + return Effect.succeed( + new ParseResult.Pointer( + ["d", 1], + input, + new ParseResult.Type(S.Literal(input.d[0]).ast, input.d[1]) + ) + ) + } + return Effect.succeed(true) + })) + await Util.assertions.decoding.fail( + schema, + { a: { b: "b", c: " " }, d: ["-", "-"] }, + `(Test <-> Test) +└─ Encoded side transformation failure + └─ Test + └─ ["a"] + └─ { readonly b: string; readonly c: minLength(1) } + └─ ["c"] + └─ ERROR_MIN_LENGTH` + ) + await Util.assertions.decoding.fail( + schema, + { a: { b: "b", c: "c" }, d: ["-", "-"] }, + `(Test <-> Test) +└─ Transformation process failure + └─ ["a"]["c"] + └─ Expected "b", actual "c"` + ) + await Util.assertions.decoding.fail( + schema, + { a: { b: "-", c: "-" }, d: ["item0", "item1"] }, + `(Test <-> Test) +└─ Transformation process failure + └─ ["d"][1] + └─ Expected "item0", actual "item1"` + ) + }) + + it("return a path and a message", async () => { + const schema = Test.pipe(S.filterEffect((input) => { + if (input.a.b !== input.a.c) { + return Effect.succeed({ + path: ["a", "c"], + message: "FILTER1" + }) + } + if (input.d[0] !== input.d[1]) { + return Effect.succeed({ + path: ["d", 1], + message: "FILTER2" + }) + } + return Effect.succeed(true) + })) + await Util.assertions.decoding.fail( + schema, + { a: { b: "b", c: " " }, d: ["-", "-"] }, + `(Test <-> Test) +└─ Encoded side transformation failure + └─ Test + └─ ["a"] + └─ { readonly b: string; readonly c: minLength(1) } + └─ ["c"] + └─ ERROR_MIN_LENGTH` + ) + await Util.assertions.decoding.fail( + schema, + { a: { b: "b", c: "c" }, d: ["-", "-"] }, + `(Test <-> Test) +└─ Transformation process failure + └─ ["a"]["c"] + └─ FILTER1` + ) + await Util.assertions.decoding.fail( + schema, + { a: { b: "-", c: "-" }, d: ["item0", "item1"] }, + `(Test <-> Test) +└─ Transformation process failure + └─ ["d"][1] + └─ FILTER2` + ) + }) + + it("return many paths and messages", async () => { + const schema = Test.pipe(S.filterEffect((input) => { + const issues: Array = [] + if (input.a.b !== input.a.c) { + issues.push({ + path: ["a", "c"], + message: "FILTER1" + }) + } + if (input.d[0] !== input.d[1]) { + issues.push({ + path: ["d", 1], + message: "FILTER2" + }) + } + return Effect.succeed(issues) + })) + await Util.assertions.decoding.fail( + schema, + { a: { b: "b", c: " " }, d: ["-", "-"] }, + `(Test <-> Test) +└─ Encoded side transformation failure + └─ Test + └─ ["a"] + └─ { readonly b: string; readonly c: minLength(1) } + └─ ["c"] + └─ ERROR_MIN_LENGTH` + ) + await Util.assertions.decoding.fail( + schema, + { a: { b: "b", c: "c" }, d: ["-", "-"] }, + `(Test <-> Test) +└─ Transformation process failure + └─ ["a"]["c"] + └─ FILTER1` + ) + await Util.assertions.decoding.fail( + schema, + { a: { b: "-", c: "-" }, d: ["item0", "item1"] }, + `(Test <-> Test) +└─ Transformation process failure + └─ ["d"][1] + └─ FILTER2` + ) + await Util.assertions.decoding.fail( + schema, + { a: { b: "b", c: "c" }, d: ["item0", "item1"] }, + `(Test <-> Test) +└─ Transformation process failure + └─ (Test <-> Test) + ├─ ["a"]["c"] + │ └─ FILTER1 + └─ ["d"][1] + └─ FILTER2` + ) + }) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/fromBrand.test.ts b/repos/effect/packages/effect/test/Schema/Schema/fromBrand.test.ts new file mode 100644 index 0000000..b28dc55 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/fromBrand.test.ts @@ -0,0 +1,89 @@ +import { describe, it } from "@effect/vitest" +import { strictEqual } from "@effect/vitest/utils" +import * as Brand from "effect/Brand" +import * as S from "effect/Schema" +import * as Util from "../TestUtils.js" + +type Int = number & Brand.Brand<"Int"> +const Int = Brand.refined( + (n) => Number.isSafeInteger(n), + (n) => Brand.error(`Expected ${n} to be an integer`) +) + +type Positive = number & Brand.Brand<"Positive"> +const Positive = Brand.refined( + (n) => n > 0, + (n) => Brand.error(`Expected ${n} to be positive`) +) + +type PositiveInt = Positive & Int +const PositiveInt = Brand.all(Int, Positive) + +type Eur = number & Brand.Brand<"Eur"> +const Eur = Brand.nominal() + +describe("fromBrand", () => { + it("make", () => { + const schema = S.NumberFromString.pipe(S.fromBrand(PositiveInt)).annotations({ identifier: "PositiveInt" }) + Util.assertions.make.succeed(schema, 1) + Util.assertions.make.fail( + schema, + -1, + `PositiveInt +└─ Predicate refinement failure + └─ Expected -1 to be positive` + ) + }) + + it("[internal] should expose the original schema as `from`", () => { + // the from property is not exposed in the public API + const schema: any = S.Number.pipe(S.fromBrand(PositiveInt)) + strictEqual(schema.from, S.Number) + }) + + it("test roundtrip consistency", () => { + Util.assertions.testRoundtripConsistency(S.Number.pipe(S.fromBrand(Int))) // refined + Util.assertions.testRoundtripConsistency(S.Number.pipe(S.fromBrand(Eur))) // nominal + }) + + it("refined", async () => { + const schema = S.Number.pipe(S.fromBrand(Brand.all(Positive, Int))) + + await Util.assertions.decoding.fail( + schema, + -0.5, + `{ number | filter } +└─ Predicate refinement failure + └─ Expected -0.5 to be positive, Expected -0.5 to be an integer` + ) + Util.assertions.parseError( + () => S.decodeUnknownSync(schema)(-0.5), + `{ number | filter } +└─ Predicate refinement failure + └─ Expected -0.5 to be positive, Expected -0.5 to be an integer` + ) + await Util.assertions.decoding.fail( + schema, + -1, + `{ number | filter } +└─ Predicate refinement failure + └─ Expected -1 to be positive` + ) + await Util.assertions.decoding.fail( + schema, + 0, + `{ number | filter } +└─ Predicate refinement failure + └─ Expected 0 to be positive` + ) + await Util.assertions.decoding.succeed(schema, 1, 1 as PositiveInt) + await Util.assertions.decoding.fail( + schema, + 1.5, + `{ number | filter } +└─ Predicate refinement failure + └─ Expected 1.5 to be an integer` + ) + await Util.assertions.decoding.succeed(schema, 2, 2 as PositiveInt) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/getNumberIndexedAccess.test.ts b/repos/effect/packages/effect/test/Schema/Schema/getNumberIndexedAccess.test.ts new file mode 100644 index 0000000..f5669b0 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/getNumberIndexedAccess.test.ts @@ -0,0 +1,40 @@ +import { describe, it } from "@effect/vitest" +import * as Duration from "effect/Duration" +import * as S from "effect/Schema" +import * as Util from "../TestUtils.js" + +describe("getNumberIndexedAccess", () => { + describe("Tuple", () => { + it("decodes and encodes required elements in a tuple", async () => { + const schema = S.getNumberIndexedAccess(S.Tuple(S.NumberFromString, S.DurationFromNanos)) + await Util.assertions.decoding.succeed(schema, "1", 1) + await Util.assertions.decoding.succeed(schema, 1n, Duration.nanos(1n)) + await Util.assertions.encoding.succeed(schema, 1, "1") + await Util.assertions.encoding.succeed(schema, Duration.nanos(1n), 1n) + }) + + it("decodes and encodes a tuple with an optional element", async () => { + const schema = S.getNumberIndexedAccess(S.Tuple(S.NumberFromString, S.optionalElement(S.DurationFromNanos))) + await Util.assertions.decoding.succeed(schema, undefined) + await Util.assertions.decoding.succeed(schema, "1", 1) + await Util.assertions.decoding.succeed(schema, 1n, Duration.nanos(1n)) + await Util.assertions.encoding.succeed(schema, undefined, undefined) + await Util.assertions.encoding.succeed(schema, 1, "1") + await Util.assertions.encoding.succeed(schema, Duration.nanos(1n), 1n) + }) + }) + + it("Array", async () => { + const schema = S.getNumberIndexedAccess(S.Array(S.NumberFromString)) + await Util.assertions.decoding.succeed(schema, "1", 1) + await Util.assertions.encoding.succeed(schema, 1, "1") + }) + + it("Union", async () => { + const schema = S.getNumberIndexedAccess(S.Union(S.Array(S.Number), S.Array(S.String))) + await Util.assertions.decoding.succeed(schema, "a") + await Util.assertions.decoding.succeed(schema, 1) + await Util.assertions.encoding.succeed(schema, "a", "a") + await Util.assertions.encoding.succeed(schema, 1, 1) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/instanceOf.test.ts b/repos/effect/packages/effect/test/Schema/Schema/instanceOf.test.ts new file mode 100644 index 0000000..72431d5 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/instanceOf.test.ts @@ -0,0 +1,51 @@ +import { describe, it } from "@effect/vitest" +import { assertFalse, assertTrue, deepStrictEqual, strictEqual } from "@effect/vitest/utils" +import * as P from "effect/ParseResult" +import * as S from "effect/Schema" +import * as AST from "effect/SchemaAST" +import * as Util from "../TestUtils.js" + +describe("instanceOf", () => { + it("is", () => { + const schema = S.instanceOf(Set) + const is = P.is(schema) + assertTrue(is(new Set())) + assertFalse(is(1)) + assertFalse(is({})) + }) + + it("annotations", () => { + const schema = S.instanceOf(Set, { description: "my description" }) + strictEqual(schema.ast.annotations[AST.DescriptionAnnotationId], "my description") + deepStrictEqual(schema.ast.annotations[S.InstanceOfSchemaId], { constructor: Set }) + }) + + it("decoding", async () => { + const schema = S.instanceOf(Set) + await Util.assertions.decoding.succeed(schema, new Set()) + await Util.assertions.decoding.fail( + schema, + 1, + `Expected Set, actual 1` + ) + await Util.assertions.decoding.fail( + schema, + {}, + `Expected Set, actual {}` + ) + }) + + describe("pretty", () => { + it("default", () => { + const schema = S.instanceOf(Set) + Util.assertions.pretty(schema, new Set(), "[object Set]") + }) + + it("override", () => { + const schema = S.instanceOf(Set, { + pretty: () => (set) => `new Set(${JSON.stringify(Array.from(set.values()))})` + }) + Util.assertions.pretty(schema, new Set([1, 2, 3]), "new Set([1,2,3])") + }) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/is.test.ts b/repos/effect/packages/effect/test/Schema/Schema/is.test.ts new file mode 100644 index 0000000..5be3520 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/is.test.ts @@ -0,0 +1,554 @@ +import { describe, it } from "@effect/vitest" +import { assertFalse, assertTrue } from "@effect/vitest/utils" +import * as P from "effect/ParseResult" +import * as S from "effect/Schema" +import * as Util from "../TestUtils.js" + +describe("is", () => { + it("never", () => { + const is = P.is(S.Never) + assertFalse(is(1)) + }) + + it("string", () => { + const is = P.is(S.String) + assertTrue(is("a")) + assertFalse(is(1)) + }) + + it("number", () => { + const is = P.is(S.Number) + assertTrue(is(1)) + assertTrue(is(NaN)) + assertTrue(is(Infinity)) + assertTrue(is(-Infinity)) + assertFalse(is("a")) + }) + + it("boolean", () => { + const is = P.is(S.Boolean) + assertTrue(is(true)) + assertTrue(is(false)) + assertFalse(is(1)) + }) + + it("bigint", () => { + const is = P.is(S.BigIntFromSelf) + assertTrue(is(0n)) + assertTrue(is(1n)) + assertTrue(is(BigInt("1"))) + assertFalse(is(null)) + assertFalse(is(1.2)) + }) + + it("symbol", () => { + const a = Symbol.for("effect/Schema/test/a") + const is = P.is(S.SymbolFromSelf) + assertTrue(is(a)) + assertFalse(is("effect/Schema/test/a")) + }) + + it("object", () => { + const is = P.is(S.Object) + assertTrue(is({})) + assertTrue(is([])) + assertFalse(is(null)) + assertFalse(is("a")) + assertFalse(is(1)) + assertFalse(is(true)) + }) + + it("literal 1 member", () => { + const schema = S.Literal(1) + const is = P.is(schema) + assertTrue(is(1)) + assertFalse(is("a")) + assertFalse(is(null)) + }) + + it("literal 2 members", () => { + const schema = S.Literal(1, "a") + const is = P.is(schema) + assertTrue(is(1)) + assertTrue(is("a")) + assertFalse(is(null)) + }) + + it("uniqueSymbolFromSelf", () => { + const a = Symbol.for("effect/Schema/test/a") + const schema = S.UniqueSymbolFromSelf(a) + const is = P.is(schema) + assertTrue(is(a)) + assertTrue(is(Symbol.for("effect/Schema/test/a"))) + assertFalse(is("Symbol(effect/Schema/test/a)")) + }) + + it("Numeric enums", () => { + enum Fruits { + Apple, + Banana + } + const schema = S.Enums(Fruits) + const is = P.is(schema) + assertTrue(is(Fruits.Apple)) + assertTrue(is(Fruits.Banana)) + assertTrue(is(0)) + assertTrue(is(1)) + assertFalse(is(3)) + }) + + it("String enums", () => { + enum Fruits { + Apple = "apple", + Banana = "banana", + Cantaloupe = 0 + } + const schema = S.Enums(Fruits) + const is = P.is(schema) + assertTrue(is(Fruits.Apple)) + assertTrue(is(Fruits.Cantaloupe)) + assertTrue(is("apple")) + assertTrue(is("banana")) + assertTrue(is(0)) + assertFalse(is("Cantaloupe")) + }) + + it("Const enums", () => { + const Fruits = { + Apple: "apple", + Banana: "banana", + Cantaloupe: 3 + } as const + const schema = S.Enums(Fruits) + const is = P.is(schema) + assertTrue(is("apple")) + assertTrue(is("banana")) + assertTrue(is(3)) + assertFalse(is("Cantaloupe")) + }) + + it("tuple. empty", () => { + const schema = S.Tuple() + const is = P.is(schema) + assertTrue(is([])) + + assertFalse(is(null)) + assertFalse(is([undefined])) + assertFalse(is([1])) + assertFalse(is({})) + }) + + it("tuple. required element", () => { + const schema = S.Tuple(S.Number) + const is = P.is(schema) + assertTrue(is([1])) + + assertFalse(is(null)) + assertFalse(is([])) + assertFalse(is([undefined])) + assertFalse(is(["a"])) + assertFalse(is([1, "b"])) + }) + + it("tuple. required element with undefined", () => { + const schema = S.Tuple(S.Union(S.Number, S.Undefined)) + const is = P.is(schema) + assertTrue(is([1])) + assertTrue(is([undefined])) + + assertFalse(is(null)) + assertFalse(is([])) + assertFalse(is(["a"])) + assertFalse(is([1, "b"])) + }) + + it("tuple. optional element", () => { + const schema = S.Tuple(S.optionalElement(S.Number)) + const is = P.is(schema) + assertTrue(is([])) + assertTrue(is([1])) + + assertFalse(is(null)) + assertFalse(is(["a"])) + assertFalse(is([undefined])) + assertFalse(is([1, "b"])) + }) + + it("tuple. optional element with undefined", () => { + const schema = S.Tuple(S.optionalElement(S.Union(S.Number, S.Undefined))) + const is = P.is(schema) + assertTrue(is([])) + assertTrue(is([1])) + assertTrue(is([undefined])) + + assertFalse(is(null)) + assertFalse(is(["a"])) + assertFalse(is([1, "b"])) + }) + + it("tuple. e + e?", () => { + const schema = S.Tuple(S.String, S.optionalElement(S.Number)) + const is = P.is(schema) + assertTrue(is(["a"])) + assertTrue(is(["a", 1])) + + assertFalse(is([1])) + assertFalse(is(["a", "b"])) + }) + + it("tuple. e + r", () => { + const schema = S.Tuple([S.String], S.Number) + const is = P.is(schema) + assertTrue(is(["a"])) + assertTrue(is(["a", 1])) + assertTrue(is(["a", 1, 2])) + + assertFalse(is([])) + }) + + it("tuple. e? + r", () => { + const schema = S.Tuple([S.optionalElement(S.String)], S.Number) + const is = P.is(schema) + assertTrue(is([])) + assertTrue(is(["a"])) + assertTrue(is(["a", 1])) + assertTrue(is(["a", 1, 2])) + + assertFalse(is([1])) + }) + + it("tuple. r", () => { + const schema = S.Array(S.Number) + const is = P.is(schema) + assertTrue(is([])) + assertTrue(is([1])) + assertTrue(is([1, 2])) + + assertFalse(is(["a"])) + assertFalse(is([1, "a"])) + }) + + it("tuple. r + e", () => { + const schema = S.Tuple([], S.String, S.Number) + const is = P.is(schema) + assertTrue(is([1])) + assertTrue(is(["a", 1])) + assertTrue(is(["a", "b", 1])) + + assertFalse(is([])) + assertFalse(is(["a"])) + assertFalse(is([1, 2])) + }) + + it("tuple. e + r + e", () => { + const schema = S.Tuple([S.String], S.Number, S.Boolean) + const is = P.is(schema) + assertTrue(is(["a", true])) + assertTrue(is(["a", 1, true])) + assertTrue(is(["a", 1, 2, true])) + + assertFalse(is([])) + assertFalse(is(["a"])) + assertFalse(is([true])) + assertFalse(is(["a", 1])) + assertFalse(is([1, true])) + }) + + it("struct. empty", () => { + const schema = S.Struct({}) + const is = P.is(schema) + assertTrue(is({})) + assertTrue(is({ a: 1 })) + assertTrue(is([])) + + assertFalse(is(null)) + assertFalse(is(undefined)) + }) + + describe("struct", () => { + it("required property signature", () => { + const schema = S.Struct({ a: S.Number }) + const is = P.is(schema) + assertTrue(is({ a: 1 })) + assertTrue(is({ a: 1, b: "b" })) + + assertFalse(is(null)) + assertFalse(is({})) + assertFalse(is({ a: undefined })) + assertFalse(is({ a: "a" })) + }) + + it("required property signature with undefined", () => { + const schema = S.Struct({ a: S.Union(S.Number, S.Undefined) }) + const is = P.is(schema) + assertTrue(is({ a: 1 })) + assertTrue(is({ a: undefined })) + assertTrue(is({ a: 1, b: "b" })) + + assertFalse(is({})) + assertFalse(is(null)) + assertFalse(is({ a: "a" })) + }) + + it("exact optional property signature", () => { + const schema = S.Struct({ a: S.optionalWith(S.Number, { exact: true }) }) + const is = P.is(schema) + assertTrue(is({})) + assertTrue(is({ a: 1 })) + assertTrue(is({ a: 1, b: "b" })) + + assertFalse(is(null)) + assertFalse(is({ a: "a" })) + assertFalse(is({ a: undefined })) + }) + + it("exact optional property signature with undefined", () => { + const schema = S.Struct({ a: S.optionalWith(S.Union(S.Number, S.Undefined), { exact: true }) }) + const is = P.is(schema) + assertTrue(is({})) + assertTrue(is({ a: 1 })) + assertTrue(is({ a: undefined })) + assertTrue(is({ a: 1, b: "b" })) + + assertFalse(is(null)) + assertFalse(is({ a: "a" })) + }) + }) + + it("record(string, string)", () => { + const a = Symbol.for("effect/Schema/test/a") + const schema = S.Record({ key: S.String, value: S.String }) + const is = P.is(schema) + assertFalse(is(null)) + assertTrue(is({})) + assertTrue(is({ a: "a" })) + assertFalse(is({ a: 1 })) + assertTrue(is({ [a]: 1 })) + assertTrue(is({ a: "a", b: "b" })) + assertFalse(is({ a: "a", b: 1 })) + assertTrue(is({ [a]: 1, b: "b" })) + }) + + it("record(symbol, string)", () => { + const a = Symbol.for("effect/Schema/test/a") + const b = Symbol.for("effect/Schema/test/b") + const schema = S.Record({ key: S.SymbolFromSelf, value: S.String }) + const is = P.is(schema) + assertFalse(is(null)) + assertTrue(is({})) + assertTrue(is({ [a]: "a" })) + assertFalse(is({ [a]: 1 })) + assertTrue(is({ a: 1 })) + assertTrue(is({ [a]: "a", [b]: "b" })) + assertFalse(is({ [a]: "a", [b]: 1 })) + assertTrue(is({ a: 1, [b]: "b" })) + }) + + it("record(never, number)", () => { + const schema = S.Record({ key: S.Never, value: S.Number }) + const is = P.is(schema) + assertTrue(is({})) + assertTrue(is({ a: 1 })) + }) + + it("record('a' | 'b', number)", () => { + const schema = S.Record({ key: S.Union(S.Literal("a"), S.Literal("b")), value: S.Number }) + const is = P.is(schema) + assertTrue(is({ a: 1, b: 2 })) + + assertFalse(is({})) + assertFalse(is({ a: 1 })) + assertFalse(is({ b: 2 })) + }) + + it("record(keyof struct({ a, b }), number)", () => { + const schema = S.Record({ key: S.keyof(S.Struct({ a: S.String, b: S.String })), value: S.Number }) + const is = P.is(schema) + assertTrue(is({ a: 1, b: 2 })) + + assertFalse(is({})) + assertFalse(is({ a: 1 })) + assertFalse(is({ b: 2 })) + assertFalse(is({ a: "a" })) + }) + + it("record(Symbol('a') | Symbol('b'), number)", () => { + const a = Symbol.for("effect/Schema/test/a") + const b = Symbol.for("effect/Schema/test/b") + const schema = S.Record({ key: S.Union(S.UniqueSymbolFromSelf(a), S.UniqueSymbolFromSelf(b)), value: S.Number }) + const is = P.is(schema) + assertTrue(is({ [a]: 1, [b]: 2 })) + + assertFalse(is({})) + assertFalse(is({ a: 1 })) + assertFalse(is({ b: 2 })) + }) + + it("record(${string}-${string}, number)", () => { + const schema = S.Record({ key: S.TemplateLiteral(S.String, S.Literal("-"), S.String), value: S.Number }) + const is = P.is(schema) + assertTrue(is({})) + assertTrue(is({ "-": 1 })) + assertTrue(is({ "a-": 1 })) + assertTrue(is({ "-b": 1 })) + assertTrue(is({ "a-b": 1 })) + assertTrue(is({ "": 1 })) + assertTrue(is({ "a": 1 })) + assertTrue(is({ "a": "a" })) + + assertFalse(is({ "-": "a" })) + assertFalse(is({ "a-": "a" })) + assertFalse(is({ "-b": "b" })) + assertFalse(is({ "a-b": "ab" })) + }) + + it("record(minLength(2), number)", () => { + const schema = S.Record({ key: S.String.pipe(S.minLength(2)), value: S.Number }) + const is = P.is(schema) + assertTrue(is({})) + assertTrue(is({ "a": 1 })) + assertTrue(is({ "a": "a" })) + assertTrue(is({ "aa": 1 })) + assertTrue(is({ "aaa": 1 })) + + assertFalse(is({ "aa": "aa" })) + }) + + it("record(${string}-${string}, number) & record(string, string | number)", () => { + const schema = S.Struct( + {}, + S.Record({ key: S.TemplateLiteral(S.String, S.Literal("-"), S.String), value: S.Number }), + S.Record({ key: S.String, value: S.Union(S.String, S.Number) }) + ) + const is = P.is(schema) + assertTrue(is({})) + assertTrue(is({ "a": "a" })) + assertTrue(is({ "a-": 1 })) + + assertFalse(is({ "a-": "a" })) + assertFalse(is({ "a": true })) + }) + + it("union", () => { + const schema = S.Union(S.String, S.Number) + const is = P.is(schema) + assertFalse(is(null)) + assertTrue(is(1)) + assertTrue(is("a")) + }) + + describe("suspend", () => { + it("baseline", () => { + interface Category { + readonly name: string + readonly categories: ReadonlyArray + } + const schema = S.Struct({ + name: S.String, + categories: S.Array(S.suspend((): S.Schema => schema)) + }) + const is = P.is(schema) + assertTrue(is({ name: "a", categories: [] })) + assertTrue( + is({ + name: "a", + categories: [{ + name: "b", + categories: [{ name: "c", categories: [] }] + }] + }) + ) + assertFalse(is({ name: "a", categories: [1] })) + }) + + it("mutually suspended", () => { + interface A { + readonly a: string + readonly bs: ReadonlyArray + } + interface B { + readonly b: number + readonly as: ReadonlyArray + } + const schemaA = S.Struct({ + a: S.String, + bs: S.Array(S.suspend((): S.Schema => schemaB)) + }) + const schemaB = S.Struct({ + b: S.Number, + as: S.Array(S.suspend(() => schemaA)) + }) + const isA = P.is(schemaA) + assertTrue(isA({ a: "a1", bs: [] })) + assertTrue(isA({ a: "a1", bs: [{ b: 1, as: [] }] })) + assertTrue( + isA({ a: "a1", bs: [{ b: 1, as: [{ a: "a2", bs: [] }] }] }) + ) + assertFalse( + isA({ a: "a1", bs: [{ b: 1, as: [{ a: "a2", bs: [null] }] }] }) + ) + }) + }) + + it("union", () => { + const schema = S.Union(S.String, S.Number) + const is = P.is(schema) + assertFalse(is(null)) + assertTrue(is(1)) + assertTrue(is("a")) + }) + + describe("rest", () => { + it("baseline", () => { + const schema = S.Tuple([S.String, S.Number], S.Boolean) + const is = P.is(schema) + assertTrue(is(["a", 1])) + assertTrue(is(["a", 1, true])) + assertTrue(is(["a", 1, true, false])) + assertFalse(is(["a", 1, true, "a"])) + assertFalse(is(["a", 1, true, "a", true])) + }) + }) + + describe("extend", () => { + it("struct", () => { + const schema = S.Struct({ a: S.String }).pipe( + S.extend(S.Struct({ b: S.Number })) + ) + const is = P.is(schema) + assertTrue(is({ a: "a", b: 1 })) + + assertFalse(is({})) + assertFalse(is({ a: "a" })) + }) + + it("record(string, string)", () => { + const schema = S.Struct({ a: S.String }, S.Record({ key: S.String, value: S.String })) + const is = P.is(schema) + assertTrue(is({ a: "a" })) + assertTrue(is({ a: "a", b: "b" })) + + assertFalse(is({})) + assertFalse(is({ b: "b" })) + assertFalse(is({ a: 1 })) + assertFalse(is({ a: "a", b: 2 })) + }) + }) + + it("nonEmptyString", () => { + const schema = S.String.pipe(S.nonEmptyString()) + const is = P.is(schema) + assertTrue(is("a")) + assertTrue(is("aa")) + + assertFalse(is("")) + }) + + it("should respect outer/inner options", () => { + const schema = S.Struct({ a: Util.NumberFromChar }) + const input = { a: 1, b: "b" } + assertFalse(S.is(schema)(input, { onExcessProperty: "error" })) + assertFalse(S.is(schema, { onExcessProperty: "error" })(input)) + assertTrue(S.is(schema, { onExcessProperty: "error" })(input, { onExcessProperty: "ignore" })) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/isSchema.test.ts b/repos/effect/packages/effect/test/Schema/Schema/isSchema.test.ts new file mode 100644 index 0000000..62d79dd --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/isSchema.test.ts @@ -0,0 +1,20 @@ +import { describe, it } from "@effect/vitest" +import { assertFalse, assertTrue } from "@effect/vitest/utils" +import * as S from "effect/Schema" + +describe("isSchema", () => { + it("Schema", () => { + assertTrue(S.isSchema(S.String)) + assertFalse(S.isSchema(S.parseJson)) + }) + + it("BrandSchema", () => { + assertTrue(S.isSchema(S.String.pipe(S.brand("my-brand")))) + }) + + it("PropertySignature", () => { + assertFalse(S.isSchema(S.propertySignature(S.String))) + assertFalse(S.isSchema(S.optionalWith(S.String, { exact: true }))) + assertFalse(S.isSchema(S.optionalWith(S.String, { default: () => "" }))) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/keyof.test.ts b/repos/effect/packages/effect/test/Schema/Schema/keyof.test.ts new file mode 100644 index 0000000..53beb43 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/keyof.test.ts @@ -0,0 +1,125 @@ +import { describe, it } from "@effect/vitest" +import { assertFalse, assertTrue, deepStrictEqual, throws } from "@effect/vitest/utils" +import * as P from "effect/ParseResult" +import * as S from "effect/Schema" +import * as AST from "effect/SchemaAST" + +describe("keyof", () => { + it("should unify string literals with string", () => { + const schema = S.Struct({ a: S.String }, S.Record({ key: S.String, value: S.String })) + const keyof = S.keyof(schema) + deepStrictEqual(keyof.ast, S.String.ast) + }) + + it("should unify symbol literals with symbol", () => { + const a = Symbol.for("effect/Schema/test/a") + const schema = S.Struct({ [a]: S.String }, S.Record({ key: S.SymbolFromSelf, value: S.String })) + const keyof = S.keyof(schema) + deepStrictEqual(keyof.ast, S.SymbolFromSelf.ast) + }) + + describe("struct", () => { + it("string keys", () => { + const schema = S.Struct({ + a: S.String, + b: S.Number + }) + // type K = keyof S.Schema.Type // "a" | "b" + const keyOf = S.keyof(schema) + const is = P.is(keyOf) + assertTrue(is("a")) + assertTrue(is("b")) + assertFalse(is("c")) + }) + + it("symbol keys", () => { + const a = Symbol.for("effect/Schema/test/a") + const b = Symbol.for("effect/Schema/test/b") + const schema = S.Struct({ + [a]: S.String, + [b]: S.Number + }) + const keyOf = S.keyof(schema) + const is = P.is(keyOf) + assertTrue(is(a)) + assertTrue(is(b)) + assertFalse(is("a")) + assertFalse(is("b")) + }) + }) + + describe("record", () => { + it("string", () => { + const schema = S.Record({ key: S.String, value: S.Number }) + // type K = keyof S.Schema.Type // string + deepStrictEqual(AST.keyof(schema.ast), S.String.ast) + }) + + it("symbol", () => { + const schema = S.Record({ key: S.SymbolFromSelf, value: S.Number }) + // type K = keyof S.Schema.Type // symbol + deepStrictEqual(AST.keyof(schema.ast), S.SymbolFromSelf.ast) + }) + + it("template literal", () => { + const schema = S.Record({ key: S.TemplateLiteral(S.Literal("a"), S.String), value: S.Number }) + // type K = keyof S.Schema.Type // `a${string}` + deepStrictEqual(AST.keyof(schema.ast), S.TemplateLiteral(S.Literal("a"), S.String).ast) + }) + }) + + it("suspend", () => { + interface Category { + readonly name: string + readonly categories: ReadonlyArray + } + const schema: S.Schema = S.suspend( // intended outer suspend + () => + S.Struct({ + name: S.String, + categories: S.Array(schema) + }) + ) + deepStrictEqual(AST.keyof(schema.ast), S.Literal("name", "categories").ast) + }) + + describe("union", () => { + it("union of structs", () => { + const schema = S.Union(S.Struct({ a: S.String }), S.Struct({ a: S.Number })) + // type K = keyof S.Schema.Type // "a" + deepStrictEqual(AST.keyof(schema.ast), S.Literal("a").ast) + }) + + it("union of records", () => { + const schema = S.Union( + S.Record({ key: S.String, value: S.Number }), + S.Record({ key: S.String, value: S.Boolean }) + ) + // type K = keyof S.Schema.Type // string + deepStrictEqual(AST.keyof(schema.ast), S.String.ast) + }) + + it("union of structs and records", () => { + const schema = S.Union( + S.Struct({ a: S.String }, S.Record({ key: S.String, value: S.Number })), + S.Struct({ a: S.Number }, S.Record({ key: S.String, value: S.Boolean })) + ) + // type K = keyof S.Schema.Type // string + deepStrictEqual(AST.keyof(schema.ast), S.String.ast) + }) + }) + + it("should support Class", () => { + class A extends S.Class("A")({ a: S.String }) {} + // type K = keyof S.Schema.Type // "a" + deepStrictEqual(AST.keyof(A.ast), S.Literal("a").ast) + }) + + it("should throw on unsupported schemas", () => { + throws( + () => S.keyof(S.Option(S.String)), + new Error(`Unsupported schema +schema (Declaration): Option`) + ) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/mutable.test.ts b/repos/effect/packages/effect/test/Schema/Schema/mutable.test.ts new file mode 100644 index 0000000..e25e7ea --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/mutable.test.ts @@ -0,0 +1,70 @@ +import { describe, it } from "@effect/vitest" +import { deepStrictEqual } from "@effect/vitest/utils" +import { identity } from "effect" +import * as S from "effect/Schema" +import * as AST from "effect/SchemaAST" + +describe("mutable", () => { + it("string", () => { + deepStrictEqual(S.mutable(S.String).ast, S.String.ast) + }) + + it("struct", () => { + const schema = S.mutable(S.Struct({ a: S.Number })) + deepStrictEqual( + schema.ast, + new AST.TypeLiteral([ + new AST.PropertySignature("a", S.Number.ast, false, false) + ], []) + ) + }) + + it("record", () => { + const schema = S.mutable(S.Record({ key: S.String, value: S.Number })) + deepStrictEqual(schema.ast, new AST.TypeLiteral([], [new AST.IndexSignature(S.String.ast, S.Number.ast, false)])) + }) + + it("array", () => { + const schema = S.mutable(S.Array(S.String)) + deepStrictEqual(schema.ast, new AST.TupleType([], [new AST.Type(S.String.ast)], false)) + }) + + it("union", () => { + const schema = S.mutable(S.Union(S.Struct({ a: S.Number }), S.Array(S.String))) + deepStrictEqual( + schema.ast, + AST.Union.make([ + new AST.TypeLiteral([ + new AST.PropertySignature("a", S.Number.ast, false, false) + ], []), + new AST.TupleType([], [new AST.Type(S.String.ast)], false) + ]) + ) + }) + + it("refinement", () => { + const schema = S.mutable(S.Array(S.String).pipe(S.maxItems(2))) + if (AST.isRefinement(schema.ast)) { + deepStrictEqual(schema.ast.from, new AST.TupleType([], [new AST.Type(S.String.ast)], false)) + } + }) + + it("suspend", () => { + const schema = S.mutable(S.suspend( // intended outer suspend + () => S.Array(S.String) + )) + if (AST.isSuspend(schema.ast)) { + deepStrictEqual(schema.ast.f(), new AST.TupleType([], [new AST.Type(S.String.ast)], false)) + } + }) + + it("transformation", () => { + const schema = S.mutable( + S.transform(S.Array(S.String), S.Array(S.String), { strict: true, decode: identity, encode: identity }) + ) + if (AST.isTransformation(schema.ast)) { + deepStrictEqual(schema.ast.from, new AST.TupleType([], [new AST.Type(S.String.ast)], false)) + deepStrictEqual(schema.ast.to, new AST.TupleType([], [new AST.Type(S.String.ast)], false)) + } + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/nonEmptyArray.test.ts b/repos/effect/packages/effect/test/Schema/Schema/nonEmptyArray.test.ts new file mode 100644 index 0000000..b17a5f3 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/nonEmptyArray.test.ts @@ -0,0 +1,10 @@ +import { describe, it } from "@effect/vitest" +import { strictEqual } from "@effect/vitest/utils" +import * as S from "effect/Schema" + +describe("nonEmptyArray", () => { + it("should expose the value", () => { + const schema = S.NonEmptyArray(S.String) + strictEqual(schema.value, S.String) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/omit.test.ts b/repos/effect/packages/effect/test/Schema/Schema/omit.test.ts new file mode 100644 index 0000000..07c80db --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/omit.test.ts @@ -0,0 +1,203 @@ +import { describe, it } from "@effect/vitest" +import { assertTrue, deepStrictEqual, doesNotThrow, strictEqual } from "@effect/vitest/utils" +import { Schema as S, SchemaAST as AST } from "effect" +import * as Util from "../TestUtils.js" + +describe("omit", () => { + describe("omit specific tests", () => { + it("Struct & Record", () => { + const record = S.Record({ key: S.String, value: S.Union(S.String, S.Number) }) + const schema = S.Struct( + { a: S.NumberFromString, b: S.Number }, + record + ) + const omitted = schema.pipe(S.omit("b")) + Util.assertions.ast.equals(omitted, record) + }) + + describe("Record", () => { + it("Record(string, number)", () => { + const schema = S.Record({ key: S.String, value: S.Number }) + const omitted = schema.pipe(S.omit("a")) + Util.assertions.ast.equals(omitted, schema) + }) + + it("Record(symbol, number)", () => { + const a = Symbol.for("effect/Schema/test/a") + const schema = S.Record({ key: S.SymbolFromSelf, value: S.Number }) + const omitted = schema.pipe(S.omit(a)) + Util.assertions.ast.equals(omitted, schema) + }) + + it("Record(string, string) & Record(`a${string}`, number)", async () => { + const schema = S.Struct( + {}, + S.Record({ key: S.String, value: S.Union(S.String, S.Number) }), + S.Record({ key: S.TemplateLiteral(S.Literal("a"), S.String), value: S.Number }) + ) + const omitted = schema.pipe(S.omit("a")) + Util.assertions.ast.equals(omitted, S.Record({ key: S.String, value: S.Union(S.String, S.Number) })) + }) + }) + + it("fromKey", () => { + const schema = S.Struct({ + a: S.String, + b: S.propertySignature(S.Number).pipe(S.fromKey("c")) + }) + const omitted = schema.pipe(S.omit("a")) + const expected = S.Struct({ + c: S.Number + }).pipe(S.rename({ c: "b" })) + Util.assertions.ast.equals(omitted, expected) + }) + + it("rename", () => { + const schema = S.Struct({ + a: S.String, + c: S.Number + }).pipe(S.rename({ c: "b" })) + const omitted = schema.pipe(S.omit("a")) + const expected = S.Struct({ + c: S.Number + }).pipe(S.rename({ c: "b" })) + Util.assertions.ast.equals(omitted, expected) + }) + }) + + it("Refinement", () => { + const schema = S.Struct({ a: S.NumberFromString, b: S.Number }).pipe(S.filter(() => true)) + const omitted = schema.pipe(S.omit("b")) + Util.assertions.ast.equals(omitted, S.Struct({ a: S.NumberFromString })) + }) + + describe("Struct", () => { + it("required properties", () => { + const a = Symbol.for("effect/Schema/test/a") + const schema = S.Struct({ [a]: S.String, b: S.NumberFromString, c: S.Boolean }) + const omitted = schema.pipe(S.omit("c")) + Util.assertions.ast.equals(omitted, S.Struct({ [a]: S.String, b: S.NumberFromString })) + }) + + it("optional property (exact)", () => { + const schema = S.Struct({ + a: S.optionalWith(S.String, { exact: true }), + b: S.NumberFromString, + c: S.Boolean + }) + const omitted = schema.pipe(S.omit("c")) + Util.assertions.ast.equals( + omitted, + S.Struct({ a: S.optionalWith(S.String, { exact: true }), b: S.NumberFromString }) + ) + }) + }) + + it("Union", () => { + const A = S.Struct({ a: S.String, b: S.Number }) + const B = S.Struct({ a: S.Number, b: S.String }) + const schema = S.Union(A, B) + const omitted = schema.pipe(S.omit("b")) + Util.assertions.ast.equals(omitted, S.Struct({ a: S.Union(S.String, S.Number) })) + }) + + it("suspend", async () => { + interface A { + readonly a: string + readonly as: ReadonlyArray + } + const schema: S.Schema = S.suspend( // intended outer suspend + () => + S.Struct({ + a: S.String, + as: S.Array(schema) + }).annotations({ identifier: "A" }) + ) + const omitted = schema.pipe(S.omit("a")) + strictEqual(String(omitted), "{ readonly as: ReadonlyArray }") + await Util.assertions.decoding.succeed(omitted, { as: [] }) + await Util.assertions.decoding.succeed(omitted, { as: [{ a: "a", as: [] }] }) + + await Util.assertions.decoding.fail( + omitted, + { as: [{ as: [] }] }, + `{ readonly as: ReadonlyArray } +└─ ["as"] + └─ ReadonlyArray + └─ [0] + └─ A + └─ ["a"] + └─ is missing` + ) + }) + + describe("Transformation", () => { + it("ComposeTransformation", () => { + const schema = S.compose( + S.Struct({ a: S.NumberFromString, b: S.Number }), + S.Struct({ a: S.Number, b: S.Number }) + ) + const omitted = schema.pipe(S.omit("b")) + Util.assertions.ast.equals(omitted, S.compose(S.Struct({ a: S.NumberFromString }), S.Struct({ a: S.Number }))) + }) + + describe("TypeLiteralTransformation", () => { + it("omitting keys without associated PropertySignatureTransformations", () => { + const schema = S.Struct({ a: S.optionalWith(S.NumberFromString, { default: () => 0 }), b: S.Number }) + const omitted = schema.pipe(S.omit("b")) + const ast = omitted.ast + assertTrue(AST.isTransformation(ast)) + deepStrictEqual(ast.from, S.Struct({ a: S.optional(S.NumberFromString) }).ast) + deepStrictEqual(ast.to, S.Struct({ a: S.Number }).ast) + assertTrue(AST.isTransformation(schema.ast)) + deepStrictEqual(ast.transformation, schema.ast.transformation) + doesNotThrow(() => omitted.pipe(S.extend(S.Struct({ c: S.Boolean })))) + }) + + it("omitting keys with associated PropertySignatureTransformations", () => { + const schema = S.Struct({ + a: S.optionalWith(S.NumberFromString, { default: () => 0 }), + b: S.NumberFromString + }) + const omitted = schema.pipe(S.omit("a")) + const ast = omitted.ast + assertTrue(AST.isTypeLiteral(ast)) + deepStrictEqual(ast.propertySignatures, [ + new AST.PropertySignature("b", S.NumberFromString.ast, false, true) + ]) + doesNotThrow(() => omitted.pipe(S.extend(S.Struct({ c: S.Boolean })))) + }) + }) + + describe("SurrogateAnnotation", () => { + it("a single Class", () => { + class A extends S.Class("A")({ a: S.NumberFromString, b: S.Number }) {} + const schema = A + const omitted = schema.pipe(S.omit("b")) + Util.assertions.ast.equals(omitted, S.Struct({ a: S.NumberFromString })) + }) + + it("a union of Classes", () => { + class A extends S.Class("A")({ a: S.Number, b: S.String }) {} + class B extends S.Class("B")({ a: S.String, b: S.Number }) {} + const schema = S.Union(A, B) + const omitted = schema.pipe(S.omit("b")) + Util.assertions.ast.equals(omitted, S.Struct({ a: S.Union(S.Number, S.String) })) + }) + }) + }) + + it("typeSchema(Class)", () => { + class A extends S.Class("A")({ a: S.String, b: S.NumberFromString }) {} + const schema = A + const omitted = schema.pipe(S.typeSchema, S.omit("a")) + Util.assertions.ast.equals(omitted, S.Struct({ b: S.Number })) + }) + + it("Class", () => { + class A extends S.Class("A")({ a: S.String, b: S.NumberFromString }) {} + const schema = A + const omitted = schema.pipe(S.omit("a")) + Util.assertions.ast.equals(omitted, S.Struct({ b: S.NumberFromString })) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/optional.test.ts b/repos/effect/packages/effect/test/Schema/Schema/optional.test.ts new file mode 100644 index 0000000..60a6243 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/optional.test.ts @@ -0,0 +1,68 @@ +import { describe, it } from "@effect/vitest" +import { strictEqual } from "@effect/vitest/utils" +import * as S from "effect/Schema" +import * as Util from "../TestUtils.js" + +describe("optional", () => { + it("should expose a from property", () => { + const schema = S.optional(S.String) + strictEqual(schema.from, S.String) + }) + + it("if the input is Schema.Undefined should not duplicate the schema", () => { + const schema = S.optional(S.Undefined) + strictEqual((schema.ast as any as S.PropertySignatureDeclaration).type, S.Undefined.ast) + }) + + it("if the input is Schema.Never should include the input in the schema", () => { + const schema = S.optional(S.Never) + strictEqual((schema.ast as any as S.PropertySignatureDeclaration).type, S.Undefined.ast) + }) + + it("should expose a from property after an annotations call", () => { + const schema = S.optional(S.String).annotations({}) + strictEqual(schema.from, S.String) + }) + + it("decoding / encoding", async () => { + const schema = S.Struct({ + a: S.optional(S.NumberFromString) + }) + await Util.assertions.decoding.succeed(schema, {}, {}) + await Util.assertions.decoding.succeed(schema, { a: undefined }, { a: undefined }) + await Util.assertions.decoding.succeed(schema, { a: "1" }, { a: 1 }) + await Util.assertions.decoding.fail( + schema, + { a: "a" }, + `{ readonly a?: NumberFromString | undefined } +└─ ["a"] + └─ NumberFromString | undefined + ├─ NumberFromString + │ └─ Transformation process failure + │ └─ Unable to decode "a" into a number + └─ Expected undefined, actual "a"` + ) + + await Util.assertions.encoding.succeed(schema, {}, {}) + await Util.assertions.encoding.succeed(schema, { a: undefined }, { a: undefined }) + await Util.assertions.encoding.succeed(schema, { a: 1 }, { a: "1" }) + }) + + it("Schema.Never as input", async () => { + const schema = S.Struct({ + a: S.optional(S.Never) + }) + await Util.assertions.decoding.succeed(schema, {}, {}) + await Util.assertions.decoding.succeed(schema, { a: undefined }, { a: undefined }) + await Util.assertions.decoding.fail( + schema, + { a: "a" }, + `{ readonly a?: undefined } +└─ ["a"] + └─ Expected undefined, actual "a"` + ) + + await Util.assertions.encoding.succeed(schema, {}, {}) + await Util.assertions.encoding.succeed(schema, { a: undefined }, { a: undefined }) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/optionalElement.test.ts b/repos/effect/packages/effect/test/Schema/Schema/optionalElement.test.ts new file mode 100644 index 0000000..34d9734 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/optionalElement.test.ts @@ -0,0 +1,9 @@ +import { describe, it } from "@effect/vitest" +import { strictEqual } from "@effect/vitest/utils" +import * as S from "effect/Schema" + +describe("optionalElement", () => { + it("toString", () => { + strictEqual(String(S.optionalElement(S.String)), "string?") + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/optionalToRequired.test.ts b/repos/effect/packages/effect/test/Schema/Schema/optionalToRequired.test.ts new file mode 100644 index 0000000..1fb41f8 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/optionalToRequired.test.ts @@ -0,0 +1,20 @@ +import { describe, it } from "@effect/vitest" +import * as Option from "effect/Option" +import * as S from "effect/Schema" +import * as Util from "../TestUtils.js" + +describe("optionalToRequired", () => { + it("two transformation schemas", async () => { + const ps = S.optionalToRequired( + S.NumberFromString, + S.BigIntFromNumber, + { decode: Option.getOrElse(() => 0), encode: Option.liftPredicate((n) => n !== 0) } + ) + const schema = S.Struct({ a: ps }) + await Util.assertions.decoding.succeed(schema, {}, { a: 0n }) + await Util.assertions.decoding.succeed(schema, { a: "1" }, { a: 1n }) + + await Util.assertions.encoding.succeed(schema, { a: 0n }, {}) + await Util.assertions.encoding.succeed(schema, { a: 1n }, { a: "1" }) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/optionalWith.test.ts b/repos/effect/packages/effect/test/Schema/Schema/optionalWith.test.ts new file mode 100644 index 0000000..a8276d0 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/optionalWith.test.ts @@ -0,0 +1,520 @@ +import { describe, it } from "@effect/vitest" +import { deepStrictEqual, strictEqual } from "@effect/vitest/utils" +import * as O from "effect/Option" +import * as S from "effect/Schema" +import * as AST from "effect/SchemaAST" +import * as Util from "../TestUtils.js" + +describe("optionalWith", () => { + it("annotations", async () => { + const schema = S.Struct({ + a: S.optionalWith(S.NumberFromString, { + exact: true + }).annotations({ description: "my description" }) + }) + deepStrictEqual((schema.ast as any).propertySignatures[0].annotations, { + [AST.DescriptionAnnotationId]: "my description" + }) + }) + + describe("{ exact: true }", () => { + it("should expose a from property", () => { + const schema = S.optionalWith(S.String, { exact: true }) + strictEqual(schema.from, S.String) + }) + + it("should expose a from property after an annotations call", () => { + const schema = S.optionalWith(S.String, { exact: true }).annotations({}) + strictEqual(schema.from, S.String) + }) + + it("decoding / encoding", async () => { + const schema = S.Struct({ + a: S.optionalWith(S.NumberFromString, { exact: true }) + }) + await Util.assertions.decoding.succeed(schema, {}, {}) + await Util.assertions.decoding.succeed(schema, { a: "1" }, { a: 1 }) + await Util.assertions.decoding.fail( + schema, + { a: "a" }, + `{ readonly a?: NumberFromString } +└─ ["a"] + └─ NumberFromString + └─ Transformation process failure + └─ Unable to decode "a" into a number` + ) + + await Util.assertions.encoding.succeed(schema, {}, {}) + await Util.assertions.encoding.succeed(schema, { a: 1 }, { a: "1" }) + }) + + it("never", async () => { + strictEqual(S.optionalWith(S.Never, { exact: true }).from.ast, AST.neverKeyword) + const schema = S.Struct({ a: S.optionalWith(S.Never, { exact: true }), b: S.Number }) + await Util.assertions.decoding.succeed(schema, { b: 1 }) + await Util.assertions.decoding.fail( + schema, + { a: "a", b: 1 }, + `{ readonly a?: never; readonly b: number } +└─ ["a"] + └─ Expected never, actual "a"` + ) + }) + }) + + describe("{ nullable: true }", () => { + it("decoding / encoding", async () => { + const schema = S.Struct({ + a: S.optionalWith(S.NumberFromString, { nullable: true }) + }) + await Util.assertions.decoding.succeed(schema, {}, {}) + await Util.assertions.decoding.succeed(schema, { a: undefined }, { a: undefined }) + await Util.assertions.decoding.succeed(schema, { a: "1" }, { a: 1 }) + await Util.assertions.decoding.succeed(schema, { a: null }, {}) + await Util.assertions.decoding.fail( + schema, + { a: "a" }, + `(Struct (Encoded side) <-> Struct (Type side)) +└─ Encoded side transformation failure + └─ Struct (Encoded side) + └─ ["a"] + └─ NumberFromString | null | undefined + ├─ NumberFromString + │ └─ Transformation process failure + │ └─ Unable to decode "a" into a number + ├─ Expected null, actual "a" + └─ Expected undefined, actual "a"` + ) + + await Util.assertions.encoding.succeed(schema, {}, {}) + await Util.assertions.encoding.succeed(schema, { a: undefined }, { a: undefined }) + await Util.assertions.encoding.succeed(schema, { a: 1 }, { a: "1" }) + }) + }) + + describe("{ exact: true, nullable: true }", () => { + it("decoding / encoding", async () => { + const schema = S.Struct({ + a: S.optionalWith(S.NumberFromString, { exact: true, nullable: true }) + }) + await Util.assertions.decoding.succeed(schema, {}, {}) + await Util.assertions.decoding.succeed(schema, { a: "1" }, { a: 1 }) + await Util.assertions.decoding.succeed(schema, { a: null }, {}) + await Util.assertions.decoding.fail( + schema, + { a: undefined }, + `(Struct (Encoded side) <-> Struct (Type side)) +└─ Encoded side transformation failure + └─ Struct (Encoded side) + └─ ["a"] + └─ NumberFromString | null + ├─ NumberFromString + │ └─ Encoded side transformation failure + │ └─ Expected string, actual undefined + └─ Expected null, actual undefined` + ) + await Util.assertions.decoding.fail( + schema, + { a: "a" }, + `(Struct (Encoded side) <-> Struct (Type side)) +└─ Encoded side transformation failure + └─ Struct (Encoded side) + └─ ["a"] + └─ NumberFromString | null + ├─ NumberFromString + │ └─ Transformation process failure + │ └─ Unable to decode "a" into a number + └─ Expected null, actual "a"` + ) + + await Util.assertions.encoding.succeed(schema, {}, {}) + await Util.assertions.encoding.succeed(schema, { a: 1 }, { a: "1" }) + }) + }) + + describe(`optionalWith > { exact: true, as: "Option" }`, () => { + it("decoding / encoding", async () => { + const schema = S.Struct({ + a: S.optionalWith(S.NumberFromString, { exact: true, as: "Option" }) + }) + await Util.assertions.decoding.succeed(schema, {}, { a: O.none() }) + await Util.assertions.decoding.succeed(schema, { a: "1" }, { a: O.some(1) }) + await Util.assertions.decoding.fail( + schema, + { + a: "a" + }, + `(Struct (Encoded side) <-> Struct (Type side)) +└─ Encoded side transformation failure + └─ Struct (Encoded side) + └─ ["a"] + └─ NumberFromString + └─ Transformation process failure + └─ Unable to decode "a" into a number` + ) + + await Util.assertions.encoding.succeed(schema, { a: O.some(1) }, { a: "1" }) + await Util.assertions.encoding.succeed(schema, { a: O.none() }, {}) + }) + }) + + describe(`optionalWith > { exact: true, nullable: true, as: "Option" }`, () => { + it("decoding / encoding", async () => { + const schema = S.Struct({ + a: S.optionalWith(S.NumberFromString, { exact: true, nullable: true, as: "Option" }) + }) + await Util.assertions.decoding.succeed(schema, {}, { a: O.none() }) + await Util.assertions.decoding.succeed(schema, { a: null }, { a: O.none() }) + await Util.assertions.decoding.succeed(schema, { a: "1" }, { a: O.some(1) }) + await Util.assertions.decoding.fail( + schema, + { + a: "a" + }, + `(Struct (Encoded side) <-> Struct (Type side)) +└─ Encoded side transformation failure + └─ Struct (Encoded side) + └─ ["a"] + └─ NumberFromString | null + ├─ NumberFromString + │ └─ Transformation process failure + │ └─ Unable to decode "a" into a number + └─ Expected null, actual "a"` + ) + + await Util.assertions.encoding.succeed(schema, { a: O.some(1) }, { a: "1" }) + await Util.assertions.encoding.succeed(schema, { a: O.none() }, {}) + }) + }) + + describe(`optionalWith > { exact: true, nullable: true, as: "Option", onNoneEncoding: () => O.some(null) }`, () => { + it("decoding / encoding", async () => { + const schema = S.Struct({ + a: S.optionalWith(S.NumberFromString, { + exact: true, + nullable: true, + as: "Option", + onNoneEncoding: () => O.some(null) + }) + }) + await Util.assertions.decoding.succeed(schema, {}, { a: O.none() }) + await Util.assertions.decoding.succeed(schema, { a: null }, { a: O.none() }) + await Util.assertions.decoding.succeed(schema, { a: "1" }, { a: O.some(1) }) + await Util.assertions.decoding.fail( + schema, + { + a: "a" + }, + `(Struct (Encoded side) <-> Struct (Type side)) +└─ Encoded side transformation failure + └─ Struct (Encoded side) + └─ ["a"] + └─ NumberFromString | null + ├─ NumberFromString + │ └─ Transformation process failure + │ └─ Unable to decode "a" into a number + └─ Expected null, actual "a"` + ) + + await Util.assertions.encoding.succeed(schema, { a: O.some(1) }, { a: "1" }) + await Util.assertions.encoding.succeed(schema, { a: O.none() }, { a: null }) + }) + }) + + describe(`optionalWith > { as: "Option" }`, () => { + it("decoding / encoding", async () => { + const schema = S.Struct({ a: S.optionalWith(S.NumberFromString, { as: "Option" }) }) + await Util.assertions.decoding.succeed(schema, {}, { a: O.none() }) + await Util.assertions.decoding.succeed(schema, { a: undefined }, { a: O.none() }) + await Util.assertions.decoding.succeed(schema, { a: "1" }, { a: O.some(1) }) + await Util.assertions.decoding.fail( + schema, + { + a: "a" + }, + `(Struct (Encoded side) <-> Struct (Type side)) +└─ Encoded side transformation failure + └─ Struct (Encoded side) + └─ ["a"] + └─ NumberFromString | undefined + ├─ NumberFromString + │ └─ Transformation process failure + │ └─ Unable to decode "a" into a number + └─ Expected undefined, actual "a"` + ) + + await Util.assertions.encoding.succeed(schema, { a: O.some(1) }, { a: "1" }) + await Util.assertions.encoding.succeed(schema, { a: O.none() }, {}) + }) + }) + + describe(`optionalWith > { nullable: true, as: "Option" }`, () => { + it("decoding / encoding", async () => { + const schema = S.Struct({ + a: S.optionalWith(S.NumberFromString, { nullable: true, as: "Option" }) + }) + await Util.assertions.decoding.succeed(schema, {}, { a: O.none() }) + await Util.assertions.decoding.succeed(schema, { a: undefined }, { a: O.none() }) + await Util.assertions.decoding.succeed(schema, { a: null }, { a: O.none() }) + await Util.assertions.decoding.succeed(schema, { a: "1" }, { a: O.some(1) }) + await Util.assertions.decoding.fail( + schema, + { + a: "a" + }, + `(Struct (Encoded side) <-> Struct (Type side)) +└─ Encoded side transformation failure + └─ Struct (Encoded side) + └─ ["a"] + └─ NumberFromString | null | undefined + ├─ NumberFromString + │ └─ Transformation process failure + │ └─ Unable to decode "a" into a number + ├─ Expected null, actual "a" + └─ Expected undefined, actual "a"` + ) + + await Util.assertions.encoding.succeed(schema, { a: O.some(1) }, { a: "1" }) + await Util.assertions.encoding.succeed(schema, { a: O.none() }, {}) + }) + }) + + describe(`optionalWith > { as: "Option", onNoneEncoding: () => O.some(undefined) }`, () => { + it("decoding / encoding", async () => { + const schema = S.Struct({ + a: S.optionalWith(S.NumberFromString, { as: "Option", onNoneEncoding: () => O.some(undefined) }) + }) + await Util.assertions.decoding.succeed(schema, {}, { a: O.none() }) + await Util.assertions.decoding.succeed(schema, { a: undefined }, { a: O.none() }) + await Util.assertions.decoding.succeed(schema, { a: "1" }, { a: O.some(1) }) + await Util.assertions.decoding.fail( + schema, + { a: null }, + `(Struct (Encoded side) <-> Struct (Type side)) +└─ Encoded side transformation failure + └─ Struct (Encoded side) + └─ ["a"] + └─ NumberFromString | undefined + ├─ NumberFromString + │ └─ Encoded side transformation failure + │ └─ Expected string, actual null + └─ Expected undefined, actual null` + ) + await Util.assertions.decoding.fail( + schema, + { + a: "a" + }, + `(Struct (Encoded side) <-> Struct (Type side)) +└─ Encoded side transformation failure + └─ Struct (Encoded side) + └─ ["a"] + └─ NumberFromString | undefined + ├─ NumberFromString + │ └─ Transformation process failure + │ └─ Unable to decode "a" into a number + └─ Expected undefined, actual "a"` + ) + + await Util.assertions.encoding.succeed(schema, { a: O.some(1) }, { a: "1" }) + await Util.assertions.encoding.succeed(schema, { a: O.none() }, { a: undefined }) + }) + }) + + describe(`optionalWith > { nullable: true, as: "Option", onNoneEncoding: () => O.some(undefined) }`, () => { + it("decoding / encoding", async () => { + const schema = S.Struct({ + a: S.optionalWith(S.NumberFromString, { nullable: true, as: "Option", onNoneEncoding: () => O.some(undefined) }) + }) + await Util.assertions.decoding.succeed(schema, {}, { a: O.none() }) + await Util.assertions.decoding.succeed(schema, { a: undefined }, { a: O.none() }) + await Util.assertions.decoding.succeed(schema, { a: null }, { a: O.none() }) + await Util.assertions.decoding.succeed(schema, { a: "1" }, { a: O.some(1) }) + await Util.assertions.decoding.fail( + schema, + { + a: "a" + }, + `(Struct (Encoded side) <-> Struct (Type side)) +└─ Encoded side transformation failure + └─ Struct (Encoded side) + └─ ["a"] + └─ NumberFromString | null | undefined + ├─ NumberFromString + │ └─ Transformation process failure + │ └─ Unable to decode "a" into a number + ├─ Expected null, actual "a" + └─ Expected undefined, actual "a"` + ) + + await Util.assertions.encoding.succeed(schema, { a: O.some(1) }, { a: "1" }) + await Util.assertions.encoding.succeed(schema, { a: O.none() }, { a: undefined }) + }) + }) + + describe(`optionalWith > { nullable: true, as: "Option", onNoneEncoding: () => O.some(null) }`, () => { + it("decoding / encoding", async () => { + const schema = S.Struct({ + a: S.optionalWith(S.NumberFromString, { nullable: true, as: "Option", onNoneEncoding: () => O.some(null) }) + }) + await Util.assertions.decoding.succeed(schema, {}, { a: O.none() }) + await Util.assertions.decoding.succeed(schema, { a: undefined }, { a: O.none() }) + await Util.assertions.decoding.succeed(schema, { a: null }, { a: O.none() }) + await Util.assertions.decoding.succeed(schema, { a: "1" }, { a: O.some(1) }) + await Util.assertions.decoding.fail( + schema, + { + a: "a" + }, + `(Struct (Encoded side) <-> Struct (Type side)) +└─ Encoded side transformation failure + └─ Struct (Encoded side) + └─ ["a"] + └─ NumberFromString | null | undefined + ├─ NumberFromString + │ └─ Transformation process failure + │ └─ Unable to decode "a" into a number + ├─ Expected null, actual "a" + └─ Expected undefined, actual "a"` + ) + + await Util.assertions.encoding.succeed(schema, { a: O.some(1) }, { a: "1" }) + await Util.assertions.encoding.succeed(schema, { a: O.none() }, { a: null }) + }) + }) + + describe("{ exact: true, default: () => A }", () => { + it("decoding / encoding", async () => { + const schema = S.Struct({ + a: S.optionalWith(S.NumberFromString, { exact: true, default: () => 0 }) + }) + await Util.assertions.decoding.succeed(schema, {}, { a: 0 }) + await Util.assertions.decoding.succeed(schema, { a: "1" }, { a: 1 }) + await Util.assertions.decoding.fail( + schema, + { a: "a" }, + `(Struct (Encoded side) <-> Struct (Type side)) +└─ Encoded side transformation failure + └─ Struct (Encoded side) + └─ ["a"] + └─ NumberFromString + └─ Transformation process failure + └─ Unable to decode "a" into a number` + ) + + await Util.assertions.encoding.succeed(schema, { a: 1 }, { a: "1" }) + await Util.assertions.encoding.succeed(schema, { a: 0 }, { a: "0" }) + }) + + it("should apply the default to the default constructor", () => { + const schema = S.Struct({ + a: S.optionalWith(S.NumberFromString, { exact: true, default: () => 0 }) + }) + deepStrictEqual(schema.make({}), { a: 0 }) + }) + }) + + describe("{ default: () => A }", () => { + it("decoding / encoding", async () => { + const schema = S.Struct({ + a: S.optionalWith(S.NumberFromString, { default: () => 0 }) + }) + await Util.assertions.decoding.succeed(schema, {}, { a: 0 }) + await Util.assertions.decoding.succeed(schema, { a: undefined }, { a: 0 }) + await Util.assertions.decoding.succeed(schema, { a: "1" }, { a: 1 }) + await Util.assertions.decoding.fail( + schema, + { a: "a" }, + `(Struct (Encoded side) <-> Struct (Type side)) +└─ Encoded side transformation failure + └─ Struct (Encoded side) + └─ ["a"] + └─ NumberFromString | undefined + ├─ NumberFromString + │ └─ Transformation process failure + │ └─ Unable to decode "a" into a number + └─ Expected undefined, actual "a"` + ) + + await Util.assertions.encoding.succeed(schema, { a: 1 }, { a: "1" }) + await Util.assertions.encoding.succeed(schema, { a: 0 }, { a: "0" }) + }) + + it("should apply the default to the default constructor", () => { + const schema = S.Struct({ + a: S.optionalWith(S.NumberFromString, { default: () => 0 }) + }) + deepStrictEqual(schema.make({}), { a: 0 }) + }) + }) + + describe("{ nullable: true, default: () => A }", () => { + it("decoding / encoding", async () => { + const schema = S.Struct({ + a: S.optionalWith(S.NumberFromString, { nullable: true, default: () => 0 }) + }) + await Util.assertions.decoding.succeed(schema, {}, { a: 0 }) + await Util.assertions.decoding.succeed(schema, { a: null }, { a: 0 }) + await Util.assertions.decoding.succeed(schema, { a: undefined }, { a: 0 }) + await Util.assertions.decoding.succeed(schema, { a: "1" }, { a: 1 }) + await Util.assertions.decoding.fail( + schema, + { a: "a" }, + `(Struct (Encoded side) <-> Struct (Type side)) +└─ Encoded side transformation failure + └─ Struct (Encoded side) + └─ ["a"] + └─ NumberFromString | null | undefined + ├─ NumberFromString + │ └─ Transformation process failure + │ └─ Unable to decode "a" into a number + ├─ Expected null, actual "a" + └─ Expected undefined, actual "a"` + ) + + await Util.assertions.encoding.succeed(schema, { a: 1 }, { a: "1" }) + await Util.assertions.encoding.succeed(schema, { a: 0 }, { a: "0" }) + }) + + it("should apply the default to the default constructor", () => { + const schema = S.Struct({ + a: S.optionalWith(S.NumberFromString, { nullable: true, default: () => 0 }) + }) + deepStrictEqual(schema.make({}), { a: 0 }) + }) + }) + + describe("{ exact: true, nullable: true, default: () => A }", () => { + it("decoding / encoding", async () => { + const schema = S.Struct({ + a: S.optionalWith(S.NumberFromString, { exact: true, nullable: true, default: () => 0 }) + }) + await Util.assertions.decoding.succeed(schema, {}, { a: 0 }) + await Util.assertions.decoding.succeed(schema, { a: null }, { a: 0 }) + await Util.assertions.decoding.succeed(schema, { a: "1" }, { a: 1 }) + await Util.assertions.decoding.fail( + schema, + { a: "a" }, + `(Struct (Encoded side) <-> Struct (Type side)) +└─ Encoded side transformation failure + └─ Struct (Encoded side) + └─ ["a"] + └─ NumberFromString | null + ├─ NumberFromString + │ └─ Transformation process failure + │ └─ Unable to decode "a" into a number + └─ Expected null, actual "a"` + ) + + await Util.assertions.encoding.succeed(schema, { a: 1 }, { a: "1" }) + await Util.assertions.encoding.succeed(schema, { a: 0 }, { a: "0" }) + }) + + it("should apply the default to the default constructor", () => { + const schema = S.Struct({ + a: S.optionalWith(S.NumberFromString, { exact: true, nullable: true, default: () => 0 }) + }) + deepStrictEqual(schema.make({}), { a: 0 }) + }) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/parseJson.test.ts b/repos/effect/packages/effect/test/Schema/Schema/parseJson.test.ts new file mode 100644 index 0000000..798be77 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/parseJson.test.ts @@ -0,0 +1,157 @@ +import { describe, it } from "@effect/vitest" +import * as Exit from "effect/Exit" +import * as S from "effect/Schema" +import * as Util from "../TestUtils.js" + +const isBun = "Bun" in globalThis + +describe("parseJson", () => { + describe("parseJson()", () => { + it("decoding", async () => { + const schema = S.parseJson() + await Util.assertions.decoding.succeed(schema, "{}", {}) + await Util.assertions.decoding.succeed(schema, `{"a":"b"}`, { "a": "b" }) + + await Util.assertions.decoding.fail( + schema, + null, + `parseJson +└─ Encoded side transformation failure + └─ Expected string, actual null` + ) + await Util.assertions.decoding.fail( + schema, + "", + isBun ? + `parseJson +└─ Transformation process failure + └─ JSON Parse error: Unexpected EOF` : + `parseJson +└─ Transformation process failure + └─ Unexpected end of JSON input` + ) + await Util.assertions.decoding.fail( + schema, + "a", + isBun + ? `parseJson +└─ Transformation process failure + └─ JSON Parse error: Unexpected identifier "a"` + : `parseJson +└─ Transformation process failure + └─ Unexpected token 'a', "a" is not valid JSON` + ) + await Util.assertions.decoding.fail( + schema, + "{", + isBun + ? `parseJson +└─ Transformation process failure + └─ JSON Parse error: Expected '}'` + : `parseJson +└─ Transformation process failure + └─ Expected property name or '}' in JSON at position 1 (line 1 column 2)` + ) + }) + + it("encoding", async () => { + const schema = S.parseJson() + await Util.assertions.encoding.succeed(schema, "a", `"a"`) + await Util.assertions.encoding.succeed(schema, { a: "b" }, `{"a":"b"}`) + + const bad: any = { a: 0 } + bad["a"] = bad + await Util.assertions.encoding.fail( + schema, + bad, + isBun ? + `parseJson +└─ Transformation process failure + └─ JSON.stringify cannot serialize cyclic structures.` : + `parseJson +└─ Transformation process failure + └─ Converting circular structure to JSON + --> starting at object with constructor 'Object' + --- property 'a' closes the circle` + ) + }) + }) + + describe("parseJson(schema)", () => { + it("decoding", async () => { + const schema = S.parseJson(S.Struct({ a: S.NumberFromString })) + await Util.assertions.decoding.succeed(schema, `{"a":"1"}`, { a: 1 }) + await Util.assertions.decoding.fail( + schema, + `{"a"}`, + isBun + ? `(parseJson <-> { readonly a: NumberFromString }) +└─ Encoded side transformation failure + └─ parseJson + └─ Transformation process failure + └─ JSON Parse error: Expected ':' before value in object property definition` + : `(parseJson <-> { readonly a: NumberFromString }) +└─ Encoded side transformation failure + └─ parseJson + └─ Transformation process failure + └─ Expected ':' after property name in JSON at position 4 (line 1 column 5)` + ) + await Util.assertions.decoding.fail( + schema, + `{"a":"b"}`, + `(parseJson <-> { readonly a: NumberFromString }) +└─ Type side transformation failure + └─ { readonly a: NumberFromString } + └─ ["a"] + └─ NumberFromString + └─ Transformation process failure + └─ Unable to decode "b" into a number` + ) + + await Util.assertions.decoding.succeed(schema.from, `{"a":"1"}`, { a: "1" }) + await Util.assertions.decoding.succeed(schema.to, { a: "1" }, { a: 1 }) + }) + + it("encoding", async () => { + const schema = S.parseJson(S.Struct({ a: S.NumberFromString })) + await Util.assertions.encoding.succeed(schema, { a: 1 }, `{"a":"1"}`) + }) + + describe("roundtrip", () => { + it("Exit", async () => { + const schema = S.parseJson(S.Exit({ failure: S.Never, success: S.Void, defect: S.Defect })) + const encoding = S.encodeSync(schema)(Exit.void) + await Util.assertions.decoding.succeed(schema, encoding, Exit.void) + }) + }) + }) + + describe("parseJson(schema, options)", () => { + it("reviver", async () => { + const schema = S.parseJson(S.Struct({ a: S.Number, b: S.String }), { + reviver: (key, value) => key === "a" ? value + 1 : value + }) + await Util.assertions.decoding.succeed(schema, `{"a":1,"b":"b"}`, { a: 2, b: "b" }) + }) + + it("replacer", async () => { + const schema = S.parseJson(S.Struct({ a: S.Number, b: S.String }), { replacer: ["b"] }) + await Util.assertions.encoding.succeed( + schema, + { a: 1, b: "b" }, + `{"b":"b"}` + ) + }) + + it("space", async () => { + const schema = S.parseJson(S.Struct({ a: S.Number }), { space: 2 }) + await Util.assertions.encoding.succeed( + schema, + { a: 1 }, + `{ + "a": 1 +}` + ) + }) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/partial.test.ts b/repos/effect/packages/effect/test/Schema/Schema/partial.test.ts new file mode 100644 index 0000000..1d540c3 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/partial.test.ts @@ -0,0 +1,106 @@ +import { describe, it } from "@effect/vitest" +import { strictEqual, throws } from "@effect/vitest/utils" +import * as S from "effect/Schema" +import * as Util from "../TestUtils.js" + +describe("partial", () => { + it("Struct", async () => { + const schema = S.partial(S.Struct({ a: S.Number })) + await Util.assertions.decoding.succeed(schema, {}) + await Util.assertions.decoding.succeed(schema, { a: 1 }) + await Util.assertions.decoding.succeed(schema, { a: undefined }) + + await Util.assertions.decoding.fail( + schema, + { a: null }, + `{ readonly a?: number | undefined } +└─ ["a"] + └─ number | undefined + ├─ Expected number, actual null + └─ Expected undefined, actual null` + ) + }) + + it("Record", async () => { + const schema = S.partial(S.Record({ key: S.String, value: S.NumberFromString })) + await Util.assertions.decoding.succeed(schema, {}, {}) + await Util.assertions.decoding.succeed(schema, { a: "1" }, { a: 1 }) + await Util.assertions.decoding.succeed(schema, { a: undefined }) + }) + + describe("Tuple", () => { + it("e", async () => { + const schema = S.partial(S.Tuple(S.NumberFromString)) + await Util.assertions.decoding.succeed(schema, ["1"], [1]) + await Util.assertions.decoding.succeed(schema, [], []) + await Util.assertions.decoding.succeed(schema, [undefined]) + }) + + it("e r", async () => { + const schema = S.partial(S.Tuple([S.NumberFromString], S.NumberFromString)) + await Util.assertions.decoding.succeed(schema, ["1"], [1]) + await Util.assertions.decoding.succeed(schema, [], []) + await Util.assertions.decoding.succeed(schema, ["1", "2"], [1, 2]) + await Util.assertions.decoding.succeed(schema, ["1", undefined], [1, undefined]) + await Util.assertions.decoding.succeed(schema, [undefined]) + }) + }) + + it("Array", async () => { + const schema = S.partial(S.Array(S.Number)) + await Util.assertions.decoding.succeed(schema, []) + await Util.assertions.decoding.succeed(schema, [1]) + await Util.assertions.decoding.succeed(schema, [undefined]) + + await Util.assertions.decoding.fail( + schema, + ["a"], + `ReadonlyArray +└─ [0] + └─ number | undefined + ├─ Expected number, actual "a" + └─ Expected undefined, actual "a"` + ) + }) + + describe("unsupported schemas", () => { + it("declarations should throw", () => { + throws( + () => S.partial(S.OptionFromSelf(S.String)), + new Error(`Unsupported schema +schema (Declaration): Option`) + ) + }) + + it("refinements should throw", () => { + throws( + () => S.partial(S.String.pipe(S.minLength(2))), + new Error(`Unsupported schema +schema (Refinement): minLength(2)`) + ) + }) + + describe("Transformation", () => { + it("should support property key renamings", async () => { + const original = S.Struct({ + a: S.String, + b: S.propertySignature(S.String).pipe(S.fromKey("c")) + }) + const schema = S.partial(original) + strictEqual( + S.format(schema), + "({ readonly a?: string | undefined; readonly c?: string | undefined } <-> { readonly a?: string | undefined; readonly b?: string | undefined })" + ) + await Util.assertions.decoding.succeed(schema, {}) + await Util.assertions.decoding.succeed(schema, { a: undefined }) + await Util.assertions.decoding.succeed(schema, { c: undefined }, { b: undefined }) + await Util.assertions.decoding.succeed(schema, { a: "a" }) + await Util.assertions.decoding.succeed(schema, { c: "b" }, { b: "b" }) + await Util.assertions.decoding.succeed(schema, { a: undefined, c: undefined }, { a: undefined, b: undefined }) + await Util.assertions.decoding.succeed(schema, { a: "a", c: undefined }, { a: "a", b: undefined }) + await Util.assertions.decoding.succeed(schema, { a: undefined, c: "b" }, { a: undefined, b: "b" }) + await Util.assertions.decoding.succeed(schema, { a: "a", c: "b" }, { a: "a", b: "b" }) + }) + }) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/partialWith.test.ts b/repos/effect/packages/effect/test/Schema/Schema/partialWith.test.ts new file mode 100644 index 0000000..add70e8 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/partialWith.test.ts @@ -0,0 +1,178 @@ +import { describe, it } from "@effect/vitest" +import { strictEqual, throws } from "@effect/vitest/utils" +import { identity } from "effect/Function" +import * as S from "effect/Schema" +import * as Util from "../TestUtils.js" + +describe("partialWith", () => { + describe("{ exact: true }", () => { + it("Struct", async () => { + const schema = S.partialWith(S.Struct({ a: S.Number }), { exact: true }) + await Util.assertions.decoding.succeed(schema, {}) + await Util.assertions.decoding.succeed(schema, { a: 1 }) + + await Util.assertions.decoding.fail( + schema, + { a: undefined }, + `{ readonly a?: number } +└─ ["a"] + └─ Expected number, actual undefined` + ) + }) + + it("Record", async () => { + const schema = S.partialWith(S.Record({ key: S.String, value: S.NumberFromString }), { exact: true }) + await Util.assertions.decoding.succeed(schema, {}, {}) + await Util.assertions.decoding.succeed(schema, { a: "1" }, { a: 1 }) + await Util.assertions.decoding.succeed(schema, { a: undefined }) + }) + + describe("Tuple", () => { + it("e", async () => { + const schema = S.partialWith(S.Tuple(S.NumberFromString), { exact: true }) + await Util.assertions.decoding.succeed(schema, ["1"], [1]) + await Util.assertions.decoding.succeed(schema, [], []) + + await Util.assertions.decoding.fail( + schema, + [undefined], + `readonly [NumberFromString?] +└─ [0] + └─ NumberFromString + └─ Encoded side transformation failure + └─ Expected string, actual undefined` + ) + }) + + it("e + r", async () => { + const schema = S.partialWith(S.Tuple([S.NumberFromString], S.NumberFromString), { exact: true }) + await Util.assertions.decoding.succeed(schema, ["1"], [1]) + await Util.assertions.decoding.succeed(schema, [], []) + await Util.assertions.decoding.succeed(schema, ["1", "2"], [1, 2]) + await Util.assertions.decoding.succeed(schema, ["1", undefined], [1, undefined]) + + await Util.assertions.decoding.fail( + schema, + [undefined], + `readonly [NumberFromString?, ...(NumberFromString | undefined)[]] +└─ [0] + └─ NumberFromString + └─ Encoded side transformation failure + └─ Expected string, actual undefined` + ) + }) + }) + + it("Array", async () => { + const schema = S.partialWith(S.Array(S.Number), { exact: true }) + await Util.assertions.decoding.succeed(schema, []) + await Util.assertions.decoding.succeed(schema, [1]) + await Util.assertions.decoding.succeed(schema, [undefined]) + + await Util.assertions.decoding.fail( + schema, + ["a"], + `ReadonlyArray +└─ [0] + └─ number | undefined + ├─ Expected number, actual "a" + └─ Expected undefined, actual "a"` + ) + }) + + it("Union", async () => { + const schema = S.partialWith(S.Union(S.Array(S.Number), S.String), { exact: true }) + await Util.assertions.decoding.succeed(schema, "a") + await Util.assertions.decoding.succeed(schema, []) + await Util.assertions.decoding.succeed(schema, [1]) + await Util.assertions.decoding.succeed(schema, [undefined]) + + await Util.assertions.decoding.fail( + schema, + ["a"], + `ReadonlyArray | string +├─ ReadonlyArray +│ └─ [0] +│ └─ number | undefined +│ ├─ Expected number, actual "a" +│ └─ Expected undefined, actual "a" +└─ Expected string, actual ["a"]` + ) + }) + + it("suspend", async () => { + interface A { + readonly a?: null | A + } + const schema: S.Schema = S.partialWith( + S.suspend( // intended outer suspend + () => + S.Struct({ + a: S.Union(schema, S.Null) + }) + ), + { exact: true } + ) + await Util.assertions.decoding.succeed(schema, {}) + await Util.assertions.decoding.succeed(schema, { a: null }) + await Util.assertions.decoding.succeed(schema, { a: {} }) + await Util.assertions.decoding.succeed(schema, { a: { a: null } }) + await Util.assertions.decoding.fail( + schema, + { a: 1 }, + `{ readonly a?: | null } +└─ ["a"] + └─ | null + ├─ Expected { readonly a?: | null }, actual 1 + └─ Expected null, actual 1` + ) + }) + }) + + describe("unsupported schemas", () => { + it("declarations should throw", () => { + throws( + () => S.partialWith(S.OptionFromSelf(S.String), { exact: true }), + new Error(`Unsupported schema +schema (Declaration): Option`) + ) + }) + + it("refinements should throw", () => { + throws( + () => S.partialWith(S.String.pipe(S.minLength(2)), { exact: true }), + new Error(`Unsupported schema +schema (Refinement): minLength(2)`) + ) + }) + + describe("Transformation", () => { + it("should support property key renamings", async () => { + const original = S.Struct({ + a: S.String, + b: S.propertySignature(S.String).pipe(S.fromKey("c")) + }) + const schema = S.partialWith(original, { exact: true }) + strictEqual( + S.format(schema), + "({ readonly a?: string; readonly c?: string } <-> { readonly a?: string; readonly b?: string })" + ) + await Util.assertions.decoding.succeed(schema, {}) + await Util.assertions.decoding.succeed(schema, { a: "a" }) + await Util.assertions.decoding.succeed(schema, { c: "b" }, { b: "b" }) + await Util.assertions.decoding.succeed(schema, { a: "a", c: "b" }, { a: "a", b: "b" }) + }) + + it("transformations should throw", () => { + throws( + () => + S.partialWith(S.transform(S.String, S.String, { strict: true, decode: identity, encode: identity }), { + exact: true + }), + new Error(`Unsupported schema +schema (Transformation): (string <-> string)`) + ) + }) + }) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/pick.test.ts b/repos/effect/packages/effect/test/Schema/Schema/pick.test.ts new file mode 100644 index 0000000..aef29a9 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/pick.test.ts @@ -0,0 +1,174 @@ +import { describe, it } from "@effect/vitest" +import { assertTrue, deepStrictEqual, doesNotThrow, strictEqual } from "@effect/vitest/utils" +import { Schema as S, SchemaAST as AST } from "effect" +import * as Util from "../TestUtils.js" + +describe("pick", () => { + it("Refinement", () => { + const schema = S.Struct({ a: S.NumberFromString, b: S.Number }).pipe(S.filter(() => true)) + const picked = schema.pipe(S.pick("a")) + Util.assertions.ast.equals(picked, S.Struct({ a: S.NumberFromString })) + }) + + describe("Struct", () => { + it("required properties", () => { + const a = Symbol.for("effect/Schema/test/a") + const schema = S.Struct({ [a]: S.String, b: S.NumberFromString, c: S.Boolean }) + const picked = schema.pipe(S.pick(a, "b")) + Util.assertions.ast.equals(picked, S.Struct({ [a]: S.String, b: S.NumberFromString })) + }) + + it("optional property (exact)", () => { + const schema = S.Struct({ + a: S.optionalWith(S.String, { exact: true }), + b: S.NumberFromString, + c: S.Boolean + }) + const picked = schema.pipe(S.pick("a", "b")) + Util.assertions.ast.equals( + picked, + S.Struct({ a: S.optionalWith(S.String, { exact: true }), b: S.NumberFromString }) + ) + }) + }) + + it("Struct & Record", () => { + const schema = S.Struct( + { a: S.NumberFromString, b: S.Number }, + S.Record({ key: S.String, value: S.Union(S.String, S.Number) }) + ) + const picked = schema.pipe(S.pick("a", "c")) + Util.assertions.ast.equals(picked, S.Struct({ a: S.NumberFromString, c: S.Union(S.String, S.Number) })) + }) + + describe("Record", () => { + it("Record(string, number)", () => { + const schema = S.Record({ key: S.String, value: S.Number }) + const picked = schema.pipe(S.pick("a", "b")) + Util.assertions.ast.equals(picked, S.Struct({ a: S.Number, b: S.Number })) + }) + + it("Record(symbol, number)", () => { + const a = Symbol.for("effect/Schema/test/a") + const b = Symbol.for("effect/Schema/test/b") + const schema = S.Record({ key: S.SymbolFromSelf, value: S.Number }) + const picked = schema.pipe(S.pick(a, b)) + Util.assertions.ast.equals(picked, S.Struct({ [a]: S.Number, [b]: S.Number })) + }) + + it("Record(string, string) & Record(`a${string}`, number)", async () => { + const schema = S.Struct( + {}, + S.Record({ key: S.String, value: S.Union(S.String, S.Number) }), + S.Record({ key: S.TemplateLiteral(S.Literal("a"), S.String), value: S.Number }) + ) + const picked = schema.pipe(S.pick("a", "b")) + Util.assertions.ast.equals(picked, S.Struct({ a: S.Number, b: S.Union(S.String, S.Number) })) + }) + }) + + it("Union", () => { + const A = S.Struct({ a: S.String, b: S.Number }) + const B = S.Struct({ a: S.Number, b: S.String }) + const schema = S.Union(A, B) + const picked = schema.pipe(S.pick("a")) + Util.assertions.ast.equals(picked, S.Struct({ a: S.Union(S.String, S.Number) })) + }) + + it("suspend", async () => { + interface A { + readonly a: string + readonly as: ReadonlyArray + } + const schema: S.Schema = S.suspend( // intended outer suspend + () => + S.Struct({ + a: S.String, + as: S.Array(schema) + }).annotations({ identifier: "A" }) + ) + const picked = schema.pipe(S.pick("as")) + strictEqual(String(picked), "{ readonly as: ReadonlyArray }") + await Util.assertions.decoding.succeed(picked, { as: [] }) + await Util.assertions.decoding.succeed(picked, { as: [{ a: "a", as: [] }] }) + + await Util.assertions.decoding.fail( + picked, + { as: [{ as: [] }] }, + `{ readonly as: ReadonlyArray } +└─ ["as"] + └─ ReadonlyArray + └─ [0] + └─ A + └─ ["a"] + └─ is missing` + ) + }) + + describe("Transformation", () => { + it("ComposeTransformation", () => { + const schema = S.compose( + S.Struct({ a: S.NumberFromString, b: S.Number }), + S.Struct({ a: S.Number, b: S.Number }) + ) + const picked = schema.pipe(S.pick("a")) + Util.assertions.ast.equals(picked, S.compose(S.Struct({ a: S.NumberFromString }), S.Struct({ a: S.Number }))) + }) + + describe("TypeLiteralTransformation", () => { + it("picking keys with associated PropertySignatureTransformations", () => { + const schema = S.Struct({ a: S.optionalWith(S.NumberFromString, { default: () => 0 }), b: S.Number }) + const picked = schema.pipe(S.pick("a")) + const ast = picked.ast + assertTrue(AST.isTransformation(ast)) + deepStrictEqual(ast.from, S.Struct({ a: S.optional(S.NumberFromString) }).ast) + deepStrictEqual(ast.to, S.Struct({ a: S.Number }).ast) + assertTrue(AST.isTransformation(schema.ast)) + deepStrictEqual(ast.transformation, schema.ast.transformation) + doesNotThrow(() => picked.pipe(S.extend(S.Struct({ c: S.Boolean })))) + }) + + it("picking keys without associated PropertySignatureTransformations", () => { + const schema = S.Struct({ a: S.optionalWith(S.NumberFromString, { default: () => 0 }), b: S.NumberFromString }) + const picked = schema.pipe(S.pick("b")) + const ast = picked.ast + assertTrue(AST.isTypeLiteral(ast)) + deepStrictEqual(ast.propertySignatures, [ + new AST.PropertySignature("b", S.NumberFromString.ast, false, true) + ]) + doesNotThrow(() => picked.pipe(S.extend(S.Struct({ c: S.Boolean })))) + }) + }) + + describe("SurrogateAnnotation", () => { + it("a single Class", () => { + class A extends S.Class("A")({ a: S.NumberFromString, b: S.Number }) {} + const schema = A + const picked = schema.pipe(S.pick("a")) + Util.assertions.ast.equals(picked, S.Struct({ a: S.NumberFromString })) + }) + + it("a union of Classes", () => { + class A extends S.Class("A")({ a: S.Number, b: S.String }) {} + class B extends S.Class("B")({ a: S.String, b: S.Number }) {} + const schema = S.Union(A, B) + const picked = schema.pipe(S.pick("a")) + Util.assertions.ast.equals(picked, S.Struct({ a: S.Union(S.Number, S.String) })) + }) + }) + }) + + it("typeSchema(Class)", () => { + class A extends S.Class("A")({ a: S.String, b: S.NumberFromString }) {} + const schema = A + const picked = schema.pipe(S.typeSchema, S.pick("b")) + Util.assertions.ast.equals(picked, S.Struct({ b: S.Number })) + }) + + it("Class", () => { + class A extends S.Class("A")({ a: S.String, b: S.NumberFromString }) {} + const schema = A + const picked = schema.pipe(S.pick("b")) + Util.assertions.ast.equals(picked, S.Struct({ b: S.NumberFromString })) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/pickLiteral.test.ts b/repos/effect/packages/effect/test/Schema/Schema/pickLiteral.test.ts new file mode 100644 index 0000000..f77133e --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/pickLiteral.test.ts @@ -0,0 +1,48 @@ +import { describe, it } from "@effect/vitest" +import { deepStrictEqual } from "@effect/vitest/utils" +import * as S from "effect/Schema" +import * as AST from "effect/SchemaAST" +import * as Util from "../TestUtils.js" + +describe("pickLiteral", () => { + it("should return an unwrapped AST with exactly one literal", () => { + deepStrictEqual(S.Literal("a").pipe(S.pickLiteral("a")).ast, new AST.Literal("a")) + }) + + it("should return a union with more than one literal", () => { + deepStrictEqual( + S.Literal("a", "b", "c").pipe(S.pickLiteral("a", "b")).ast, + AST.Union.make([new AST.Literal("a"), new AST.Literal("b")]) + ) + }) + + describe("decoding", () => { + it("1 member", async () => { + const schema = S.Literal("a").pipe(S.pickLiteral("a")) + await Util.assertions.decoding.succeed(schema, "a") + + await Util.assertions.decoding.fail(schema, 1, `Expected "a", actual 1`) + await Util.assertions.decoding.fail(schema, null, `Expected "a", actual null`) + }) + + it("2 members", async () => { + const schema = S.Literal("a", "b", "c").pipe(S.pickLiteral("a", "b")) + + await Util.assertions.decoding.succeed(schema, "a") + await Util.assertions.decoding.succeed(schema, "b") + + await Util.assertions.decoding.fail( + schema, + null, + `"a" | "b" +├─ Expected "a", actual null +└─ Expected "b", actual null` + ) + }) + }) + + it("encoding", async () => { + const schema = S.Literal(null).pipe(S.pickLiteral(null)) + await Util.assertions.encoding.succeed(schema, null, null) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/pipe.test.ts b/repos/effect/packages/effect/test/Schema/Schema/pipe.test.ts new file mode 100644 index 0000000..007dcee --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/pipe.test.ts @@ -0,0 +1,18 @@ +import { describe, it } from "@effect/vitest" +import { assertFalse, assertTrue } from "@effect/vitest/utils" +import * as S from "effect/Schema" + +describe("pipe", () => { + it("schemas should be pipeable", () => { + const int = (self: S.Schema) => self.pipe(S.int(), S.brand("Int")) + + const positive = (self: S.Schema) => self.pipe(S.positive(), S.brand("Positive")) + + const PositiveInt = S.NumberFromString.pipe(int, positive) + + const is = S.is(PositiveInt) + assertTrue(is(1)) + assertFalse(is(-1)) + assertFalse(is(1.2)) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/pluck.test.ts b/repos/effect/packages/effect/test/Schema/Schema/pluck.test.ts new file mode 100644 index 0000000..d23e91d --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/pluck.test.ts @@ -0,0 +1,110 @@ +import { describe, it } from "@effect/vitest" +import * as S from "effect/Schema" +import * as Util from "../TestUtils.js" + +describe("pluck", () => { + describe("decoding", () => { + it("struct (string keys)", async () => { + const origin = S.Struct({ a: S.String, b: S.NumberFromString }) + const schema = S.pluck(origin, "a") + await Util.assertions.decoding.succeed(schema, { a: "a", b: "2" }, "a") + await Util.assertions.decoding.fail( + schema, + { a: 1, b: "2" }, + `({ readonly a: string } <-> string) +└─ Encoded side transformation failure + └─ { readonly a: string } + └─ ["a"] + └─ Expected string, actual 1` + ) + }) + + it("struct (symbol keys)", async () => { + const a = Symbol.for("effect/schema/test/a") + const b = Symbol.for("effect/schema/test/b") + const origin = S.Struct({ [a]: S.String, [b]: S.NumberFromString }) + const schema = S.pluck(origin, a) + await Util.assertions.decoding.succeed(schema, { [a]: "a", [b]: "2" }, "a") + await Util.assertions.decoding.fail( + schema, + { [a]: 1, [b]: "2" }, + `({ readonly Symbol(effect/schema/test/a): string } <-> string) +└─ Encoded side transformation failure + └─ { readonly Symbol(effect/schema/test/a): string } + └─ [Symbol(effect/schema/test/a)] + └─ Expected string, actual 1` + ) + }) + + it("struct with optional key", async () => { + const origin = S.Struct({ a: S.optional(S.String), b: S.Number }) + const schema = S.pluck(origin, "a") + await Util.assertions.decoding.succeed(schema, { b: 2 }, undefined) + await Util.assertions.decoding.succeed(schema, { a: undefined, b: 2 }, undefined) + await Util.assertions.decoding.succeed(schema, { a: "a", b: 2 }, "a") + }) + + it("union", async () => { + const origin = S.Union(S.Struct({ _tag: S.Literal("A") }), S.Struct({ _tag: S.Literal("B") })) + const schema = S.pluck(origin, "_tag") + await Util.assertions.decoding.succeed(schema, { _tag: "A" }, "A") + await Util.assertions.decoding.succeed(schema, { _tag: "B" }, "B") + await Util.assertions.decoding.fail( + schema, + {}, + `({ readonly _tag: "A" | "B" } <-> "A" | "B") +└─ Encoded side transformation failure + └─ { readonly _tag: "A" | "B" } + └─ ["_tag"] + └─ is missing` + ) + }) + }) + + describe("encoding", () => { + it("struct (string keys)", async () => { + const origin = S.Struct({ a: S.NonEmptyString }) + const schema = S.pluck(origin, "a") + await Util.assertions.encoding.succeed(schema, "a", { a: "a" }) + await Util.assertions.encoding.fail( + schema, + "", + `({ readonly a: NonEmptyString } <-> NonEmptyString) +└─ Type side transformation failure + └─ NonEmptyString + └─ Predicate refinement failure + └─ Expected a non empty string, actual ""` + ) + }) + + it("struct (symbol keys)", async () => { + const a = Symbol.for("effect/schema/test/a") + const origin = S.Struct({ [a]: S.NonEmptyString }) + const schema = S.pluck(origin, a) + await Util.assertions.encoding.succeed(schema, "a", { [a]: "a" }) + await Util.assertions.encoding.fail( + schema, + "", + `({ readonly Symbol(effect/schema/test/a): NonEmptyString } <-> NonEmptyString) +└─ Type side transformation failure + └─ NonEmptyString + └─ Predicate refinement failure + └─ Expected a non empty string, actual ""` + ) + }) + }) + + it("struct with optional key", async () => { + const origin = S.Struct({ a: S.optional(S.String) }) + const schema = S.pluck(origin, "a") + await Util.assertions.encoding.succeed(schema, undefined, {}) + await Util.assertions.encoding.succeed(schema, "a", { a: "a" }) + }) + + it("struct with exact optional key", async () => { + const origin = S.Struct({ a: S.optionalWith(S.String, { exact: true }) }) + const schema = S.pluck(origin, "a") + await Util.assertions.encoding.succeed(schema, undefined, {}) + await Util.assertions.encoding.succeed(schema, "a", { a: "a" }) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/rename.test.ts b/repos/effect/packages/effect/test/Schema/Schema/rename.test.ts new file mode 100644 index 0000000..fa5c050 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/rename.test.ts @@ -0,0 +1,121 @@ +import { describe, it } from "@effect/vitest" +import { strictEqual } from "@effect/vitest/utils" +import * as S from "effect/Schema" +import * as Util from "../TestUtils.js" + +describe("rename", () => { + describe("Struct", () => { + it("from string key to string key", async () => { + const schema = S.Struct({ a: S.String, b: S.Number }) + const renamed = S.rename(schema, { a: "c" }) + + await Util.assertions.decoding.succeed(renamed, { a: "a", b: 1 }, { c: "a", b: 1 }) + await Util.assertions.encoding.succeed(renamed, { c: "a", b: 1 }, { a: "a", b: 1 }) + }) + + it("from string key to symbol key", async () => { + const c = Symbol.for("effect/Schema/test/c") + const schema = S.Struct({ a: S.String, b: S.Number }) + const renamed = S.rename(schema, { a: c }) + + await Util.assertions.decoding.succeed(renamed, { a: "a", b: 1 }, { [c]: "a", b: 1 }) + await Util.assertions.encoding.succeed(renamed, { [c]: "a", b: 1 }, { a: "a", b: 1 }) + }) + + it("from symbol key to string key", async () => { + const a = Symbol.for("effect/Schema/test/a") + const schema = S.Struct({ [a]: S.String, b: S.Number }) + const renamed = S.rename(schema, { [a]: "c" }) + + await Util.assertions.decoding.succeed(renamed, { [a]: "a", b: 1 }, { c: "a", b: 1 }) + await Util.assertions.encoding.succeed(renamed, { c: "a", b: 1 }, { [a]: "a", b: 1 }) + }) + + it("from symbol key to symbol key", async () => { + const a = Symbol.for("effect/Schema/test/a") + const c = Symbol.for("effect/Schema/test/c") + const schema = S.Struct({ [a]: S.String, b: S.Number }) + const renamed = S.rename(schema, { [a]: c }) + + await Util.assertions.decoding.succeed(renamed, { [a]: "a", b: 1 }, { [c]: "a", b: 1 }) + await Util.assertions.encoding.succeed(renamed, { [c]: "a", b: 1 }, { [a]: "a", b: 1 }) + }) + }) + + it("Transform (renaming twice)", async () => { + const schema = S.Struct({ a: S.String, b: S.Number }) + const renamed = S.rename(schema, { a: "c" }) + const renamed2 = S.rename(renamed, { c: "d" }) + + await Util.assertions.decoding.succeed(renamed2, { a: "a", b: 1 }, { d: "a", b: 1 }) + await Util.assertions.encoding.succeed(renamed2, { d: "a", b: 1 }, { a: "a", b: 1 }) + }) + + it("suspend", async () => { + interface A { + readonly a: string + readonly as: ReadonlyArray + } + const schema: S.Schema = S.suspend( // intended outer suspend + () => + S.Struct({ + a: S.String, + as: S.Array(schema) + }) + ) + const renamed = S.rename(schema, { a: "c" }) + + await Util.assertions.decoding.succeed(renamed, { a: "a1", as: [{ a: "a2", as: [] }] }, { + c: "a1", + as: [{ a: "a2", as: [] }] + }) + await Util.assertions.encoding.succeed(renamed, { + c: "a1", + as: [{ a: "a2", as: [] }] + }, { a: "a1", as: [{ a: "a2", as: [] }] }) + }) + + it("pipe", async () => { + const renamed = S.Struct({ a: S.String, b: S.Number }).pipe( + S.rename({ a: "c" }) + ) + + await Util.assertions.decoding.succeed(renamed, { a: "a", b: 1 }, { c: "a", b: 1 }) + await Util.assertions.encoding.succeed(renamed, { c: "a", b: 1 }, { a: "a", b: 1 }) + }) + + it("should return the same ast if there are no mappings", () => { + const schema = S.Struct({ a: S.String }) + const renamed = S.rename(schema, {}) + strictEqual(schema.ast, renamed.ast) + }) + + it("field transformation", async () => { + const schema = S.Struct({ a: S.String, b: S.NumberFromString }) + const renamed = S.rename(schema, { a: "c" }) + + await Util.assertions.decoding.succeed(renamed, { a: "a", b: "1" }, { c: "a", b: 1 }) + await Util.assertions.encoding.succeed(renamed, { c: "a", b: 1 }, { a: "a", b: "1" }) + }) + + it("union", async () => { + const A = S.Struct({ + ab: S.Number + }) + + const B = S.Struct({ + ab: S.Null + }) + + const schema = S.Union( + A.pipe(S.attachPropertySignature("kind", "A")), + B.pipe(S.attachPropertySignature("kind", "B")) + ) + const renamed = schema.pipe(S.rename({ ab: "c" })) + await Util.assertions.decoding.succeed(renamed, { ab: 1 }, { kind: "A", c: 1 }) + await Util.assertions.decoding.succeed(renamed, { ab: null }, { kind: "B", c: null }) + + await Util.assertions.encoding.succeed(renamed, { kind: "A", c: 1 }, { ab: 1 }) + await Util.assertions.encoding.succeed(renamed, { kind: "B", c: null }, { ab: null }) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/required.test.ts b/repos/effect/packages/effect/test/Schema/Schema/required.test.ts new file mode 100644 index 0000000..8aaa931 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/required.test.ts @@ -0,0 +1,230 @@ +import { describe, it } from "@effect/vitest" +import { strictEqual, throws } from "@effect/vitest/utils" +import { identity } from "effect/Function" +import * as S from "effect/Schema" +import * as Util from "../TestUtils.js" + +describe("required", () => { + it("string", () => { + strictEqual(S.required(S.String).ast, S.String.ast) + }) + + it("Struct", async () => { + const schema = S.required(S.Struct({ + a: S.optionalWith(S.NumberFromString.pipe(S.greaterThan(0)), { exact: true }) + })) + + await Util.assertions.decoding.succeed(schema, { a: "1" }, { a: 1 }) + await Util.assertions.decoding.fail( + schema, + {}, + `{ readonly a: greaterThan(0) } +└─ ["a"] + └─ is missing` + ) + await Util.assertions.decoding.fail( + schema, + { a: "-1" }, + `{ readonly a: greaterThan(0) } +└─ ["a"] + └─ greaterThan(0) + └─ Predicate refinement failure + └─ Expected a positive number, actual -1` + ) + }) + + describe("Tuple", () => { + it("e?", async () => { + // type A = readonly [string?] + // type B = Required + + const A = S.Tuple(S.optionalElement(S.NumberFromString)) + const B = S.required(A) + + await Util.assertions.decoding.succeed(B, ["1"], [1]) + await Util.assertions.decoding.fail( + B, + [], + `readonly [NumberFromString] +└─ [0] + └─ is missing` + ) + }) + + it("e e?", async () => { + // type A = readonly [number, string?] + // type B = Required + + const A = S.Tuple(S.NumberFromString, S.optionalElement(S.String)) + const B = S.required(A) + + await Util.assertions.decoding.succeed(B, ["0", ""], [0, ""]) + await Util.assertions.decoding.fail( + B, + ["0"], + `readonly [NumberFromString, string] +└─ [1] + └─ is missing` + ) + }) + + it("e r e", async () => { + // type A = readonly [string, ...Array, boolean] + // type B = Required // readonly [string, ...number[], boolean] + + const A = S.Tuple([S.String], S.Number, S.Boolean) + const B = S.required(A) + + await Util.assertions.decoding.succeed(B, ["", true], ["", true]) + await Util.assertions.decoding.succeed(B, ["", 0, true]) + await Util.assertions.decoding.succeed(B, ["", 0, 1, true]) + + await Util.assertions.decoding.fail( + B, + [], + `readonly [string, ...number[], boolean] +└─ [0] + └─ is missing` + ) + await Util.assertions.decoding.fail( + B, + [""], + `readonly [string, ...number[], boolean] +└─ [1] + └─ is missing` + ) + }) + + it("e r e e", async () => { + // type A = readonly [string, ...Array, boolean, boolean] + // type B = Required // readonly [string, ...number[], boolean, boolean] + + const A = S.Tuple([S.String], S.Number, S.Boolean, S.Boolean) + const B = S.required(A) + + await Util.assertions.decoding.succeed(B, ["", 0, true, false]) + await Util.assertions.decoding.succeed(B, ["", 0, 1, 2, 3, true, false]) + + await Util.assertions.decoding.fail( + B, + [], + `readonly [string, ...number[], boolean, boolean] +└─ [0] + └─ is missing` + ) + await Util.assertions.decoding.fail( + B, + [""], + `readonly [string, ...number[], boolean, boolean] +└─ [1] + └─ is missing` + ) + await Util.assertions.decoding.fail( + B, + ["", true], + `readonly [string, ...number[], boolean, boolean] +└─ [2] + └─ is missing` + ) + await Util.assertions.decoding.fail( + B, + ["", 0, true], + `readonly [string, ...number[], boolean, boolean] +└─ [1] + └─ Expected boolean, actual 0` + ) + }) + }) + + it("Union", async () => { + const schema = S.required(S.Union( + S.Struct({ a: S.optionalWith(S.String, { exact: true }) }), + S.Struct({ b: S.optionalWith(S.Number, { exact: true }) }) + )) + await Util.assertions.decoding.succeed(schema, { a: "a" }) + await Util.assertions.decoding.succeed(schema, { b: 1 }) + await Util.assertions.decoding.fail( + schema, + {}, + `{ readonly a: string } | { readonly b: number } +├─ { readonly a: string } +│ └─ ["a"] +│ └─ is missing +└─ { readonly b: number } + └─ ["b"] + └─ is missing` + ) + }) + + it("suspend", async () => { + interface A { + readonly a: null | A + } + const schema: S.Schema = S.required(S.suspend( // intended outer suspend + () => + S.Struct({ + a: S.optionalWith(S.Union(schema, S.Null), { exact: true }) + }) + )) + await Util.assertions.decoding.succeed(schema, { a: null }) + await Util.assertions.decoding.succeed(schema, { a: { a: null } }) + await Util.assertions.decoding.fail( + schema, + {}, + `{ readonly a: | null } +└─ ["a"] + └─ is missing` + ) + await Util.assertions.decoding.fail( + schema, + { a: {} }, + `{ readonly a: | null } +└─ ["a"] + └─ | null + ├─ { readonly a: | null } + │ └─ ["a"] + │ └─ is missing + └─ Expected null, actual {}` + ) + }) + + describe("unsupported schemas", () => { + it("declarations should throw", async () => { + throws( + () => S.required(S.OptionFromSelf(S.String)), + new Error(`Unsupported schema +schema (Declaration): Option`) + ) + }) + + it("refinements should throw", async () => { + throws( + () => S.required(S.String.pipe(S.minLength(2))), + new Error(`Unsupported schema +schema (Refinement): minLength(2)`) + ) + }) + + describe("Transformation", () => { + it("should support property key renamings", () => { + const original = S.Struct({ + a: S.String, + b: S.propertySignature(S.String).pipe(S.fromKey("c")) + }) + const schema = S.required(S.partial(original)) + strictEqual( + S.format(schema), + "({ readonly a: string | undefined; readonly c: string | undefined } <-> { readonly a: string | undefined; readonly b: string | undefined })" + ) + }) + + it("transformations should throw", async () => { + throws( + () => S.required(S.transform(S.String, S.String, { strict: true, decode: identity, encode: identity })), + new Error(`Unsupported schema +schema (Transformation): (string <-> string)`) + ) + }) + }) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/requiredToOptional.test.ts b/repos/effect/packages/effect/test/Schema/Schema/requiredToOptional.test.ts new file mode 100644 index 0000000..1375109 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/requiredToOptional.test.ts @@ -0,0 +1,20 @@ +import { describe, it } from "@effect/vitest" +import * as Option from "effect/Option" +import * as S from "effect/Schema" +import * as Util from "../TestUtils.js" + +describe("requiredToOptional", () => { + it("two transformation schemas", async () => { + const ps = S.requiredToOptional( + S.NumberFromString, + S.BigIntFromNumber, + { decode: Option.liftPredicate((n) => n !== 0), encode: Option.getOrElse(() => 0) } + ) + const schema = S.Struct({ a: ps }) + await Util.assertions.decoding.succeed(schema, { a: "0" }, {}) + await Util.assertions.decoding.succeed(schema, { a: "1" }, { a: 1n }) + + await Util.assertions.encoding.succeed(schema, {}, { a: "0" }) + await Util.assertions.encoding.succeed(schema, { a: 1n }, { a: "1" }) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/standardSchemaV1.test.ts b/repos/effect/packages/effect/test/Schema/Schema/standardSchemaV1.test.ts new file mode 100644 index 0000000..cf72f4f --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/standardSchemaV1.test.ts @@ -0,0 +1,366 @@ +import { assertTrue, deepStrictEqual, strictEqual } from "@effect/vitest/utils" +import type { StandardSchemaV1 } from "@standard-schema/spec" +import { Context, Effect, ParseResult, Predicate, Schema } from "effect" +import { describe, it } from "vitest" +import { AsyncString } from "../TestUtils.js" + +function validate( + schema: StandardSchemaV1, + input: unknown +): StandardSchemaV1.Result | Promise> { + return schema["~standard"].validate(input) +} + +const isPromise = (value: unknown): value is Promise => value instanceof Promise + +const expectSuccess = async ( + result: StandardSchemaV1.Result, + a: A +) => { + deepStrictEqual(result, { value: a }) +} + +const expectFailure = async ( + result: StandardSchemaV1.Result, + issues: ReadonlyArray | ((issues: ReadonlyArray) => void) +) => { + if (result.issues !== undefined) { + if (Predicate.isFunction(issues)) { + issues(result.issues) + } else { + deepStrictEqual(result.issues, issues) + } + } else { + throw new Error("Expected issues, got undefined") + } +} + +const expectSyncSuccess = ( + schema: StandardSchemaV1, + input: unknown, + a: A +) => { + const result = validate(schema, input) + if (isPromise(result)) { + throw new Error("Expected value, got promise") + } else { + expectSuccess(result, a) + } +} + +const expectAsyncSuccess = async ( + schema: StandardSchemaV1, + input: unknown, + a: A +) => { + const result = validate(schema, input) + if (isPromise(result)) { + expectSuccess(await result, a) + } else { + throw new Error("Expected promise, got value") + } +} + +const expectSyncFailure = ( + schema: StandardSchemaV1, + input: unknown, + issues: ReadonlyArray | ((issues: ReadonlyArray) => void) +) => { + const result = validate(schema, input) + if (isPromise(result)) { + throw new Error("Expected value, got promise") + } else { + expectFailure(result, issues) + } +} + +const expectAsyncFailure = async ( + schema: StandardSchemaV1, + input: unknown, + issues: ReadonlyArray | ((issues: ReadonlyArray) => void) +) => { + const result = validate(schema, input) + if (isPromise(result)) { + expectFailure(await result, issues) + } else { + throw new Error("Expected promise, got value") + } +} + +const AsyncNonEmptyString = AsyncString.pipe(Schema.minLength(1)) + +describe("standardSchemaV1", () => { + it("should return a schema", () => { + const schema = Schema.NumberFromString + const standardSchema = Schema.standardSchemaV1(schema) + assertTrue(Schema.isSchema(standardSchema)) + }) + + it("sync decoding + sync issue formatting", () => { + const schema = Schema.NonEmptyString + const standardSchema = Schema.standardSchemaV1(schema) + expectSyncSuccess(standardSchema, "a", "a") + expectSyncFailure(standardSchema, null, [ + { + message: "Expected string, actual null", + path: [] + } + ]) + expectSyncFailure(standardSchema, "", [ + { + message: `Expected a non empty string, actual ""`, + path: [] + } + ]) + }) + + it("sync decoding + sync custom message", () => { + const schema = Schema.NonEmptyString.annotations({ message: () => Effect.succeed("my message") }) + const standardSchema = Schema.standardSchemaV1(schema) + expectSyncSuccess(standardSchema, "a", "a") + expectSyncFailure(standardSchema, null, [ + { + message: "Expected string, actual null", + path: [] + } + ]) + expectSyncFailure(standardSchema, "", [ + { + message: "my message", + path: [] + } + ]) + }) + + it("sync decoding + async custom message", async () => { + const schema = Schema.NonEmptyString.annotations({ + message: () => Effect.succeed("my message").pipe(Effect.delay("10 millis")) + }) + const standardSchema = Schema.standardSchemaV1(schema) + expectSyncSuccess(standardSchema, "a", "a") + await expectAsyncFailure(standardSchema, null, [ + { + message: "Expected string, actual null", + path: [] + } + ]) + await expectAsyncFailure(standardSchema, "", [ + { + message: "my message", + path: [] + } + ]) + }) + + it("async decoding + sync issue formatting", async () => { + const schema = AsyncNonEmptyString + const standardSchema = Schema.standardSchemaV1(schema) + await expectAsyncSuccess(standardSchema, "a", "a") + expectSyncFailure(standardSchema, null, [ + { + message: "Expected string, actual null", + path: [] + } + ]) + await expectAsyncFailure(standardSchema, "", [ + { + message: `Expected a string at least 1 character(s) long, actual ""`, + path: [] + } + ]) + }) + + it("async decoding + sync custom message", async () => { + const schema = AsyncNonEmptyString.annotations({ message: () => Effect.succeed("my message") }) + const standardSchema = Schema.standardSchemaV1(schema) + await expectAsyncSuccess(standardSchema, "a", "a") + expectSyncFailure(standardSchema, null, [ + { + message: "Expected string, actual null", + path: [] + } + ]) + await expectAsyncFailure(standardSchema, "", [ + { + message: "my message", + path: [] + } + ]) + }) + + it("async decoding + async custom message", async () => { + const schema = AsyncNonEmptyString.annotations({ + message: () => Effect.succeed("my message").pipe(Effect.delay("10 millis")) + }) + const standardSchema = Schema.standardSchemaV1(schema) + await expectAsyncSuccess(standardSchema, "a", "a") + await expectAsyncFailure(standardSchema, null, [ + { + message: "Expected string, actual null", + path: [] + } + ]) + await expectAsyncFailure(standardSchema, "", [ + { + message: "my message", + path: [] + } + ]) + }) + + describe("missing dependencies", () => { + class MagicNumber extends Context.Tag("Min")() {} + + it("sync decoding should throw", () => { + const DepString = Schema.transformOrFail(Schema.Number, Schema.Number, { + strict: true, + decode: (n) => + Effect.gen(function*() { + const magicNumber = yield* MagicNumber + return n * magicNumber + }), + encode: ParseResult.succeed + }) + + const schema = DepString + const standardSchema = Schema.standardSchemaV1(schema as any) + expectSyncFailure(standardSchema, 1, (issues) => { + strictEqual(issues.length, 1) + deepStrictEqual(issues[0].path, undefined) + assertTrue(issues[0].message.includes("Service not found: Min")) + }) + }) + + it("async decoding should throw", () => { + const DepString = Schema.transformOrFail(Schema.Number, Schema.Number, { + strict: true, + decode: (n) => + Effect.gen(function*() { + const magicNumber = yield* MagicNumber + yield* Effect.sleep("10 millis") + return n * magicNumber + }), + encode: ParseResult.succeed + }) + + const schema = DepString + const standardSchema = Schema.standardSchemaV1(schema as any) + expectSyncFailure(standardSchema, 1, (issues) => { + strictEqual(issues.length, 1) + deepStrictEqual(issues[0].path, undefined) + assertTrue(issues[0].message.includes("Service not found: Min")) + }) + }) + }) + + it("sync decoding + sync all issues formatting", () => { + const schema = Schema.Struct({ + a: Schema.NonEmptyString, + b: Schema.NonEmptyString + }) + const standardSchema = Schema.standardSchemaV1(schema) + expectSyncSuccess(standardSchema, { + a: "a", + b: "b" + }, { + a: "a", + b: "b" + }) + expectSyncFailure(standardSchema, null, [ + { + message: "Expected { readonly a: NonEmptyString; readonly b: NonEmptyString }, actual null", + path: [] + } + ]) + expectSyncFailure(standardSchema, "", [ + { + message: `Expected { readonly a: NonEmptyString; readonly b: NonEmptyString }, actual ""`, + path: [] + } + ]) + expectSyncFailure(standardSchema, { + a: "", + b: "" + }, [ + { + message: `Expected a non empty string, actual ""`, + path: ["a"] + }, + { + message: `Expected a non empty string, actual ""`, + path: ["b"] + } + ]) + expectSyncFailure(standardSchema, { + a: "a", + b: "" + }, [ + { + message: `Expected a non empty string, actual ""`, + path: ["b"] + } + ]) + expectSyncFailure(standardSchema, { + a: "", + b: "b" + }, [ + { + message: `Expected a non empty string, actual ""`, + path: ["a"] + } + ]) + }) + it("sync decoding + sync first issue formatting", () => { + const schema = Schema.Struct({ + a: Schema.NonEmptyString, + b: Schema.NonEmptyString + }) + const standardSchema = Schema.standardSchemaV1(schema, { errors: "first" }) + expectSyncSuccess(standardSchema, { + a: "a", + b: "b" + }, { + a: "a", + b: "b" + }) + expectSyncFailure(standardSchema, null, [ + { + message: "Expected { readonly a: NonEmptyString; readonly b: NonEmptyString }, actual null", + path: [] + } + ]) + expectSyncFailure(standardSchema, "", [ + { + message: `Expected { readonly a: NonEmptyString; readonly b: NonEmptyString }, actual ""`, + path: [] + } + ]) + expectSyncFailure(standardSchema, { + a: "", + b: "" + }, [ + { + message: `Expected a non empty string, actual ""`, + path: ["a"] + } + ]) + expectSyncFailure(standardSchema, { + a: "a", + b: "" + }, [ + { + message: `Expected a non empty string, actual ""`, + path: ["b"] + } + ]) + expectSyncFailure(standardSchema, { + a: "", + b: "b" + }, [ + { + message: `Expected a non empty string, actual ""`, + path: ["a"] + } + ]) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/suspend.test.ts b/repos/effect/packages/effect/test/Schema/Schema/suspend.test.ts new file mode 100644 index 0000000..95a92b5 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/suspend.test.ts @@ -0,0 +1,138 @@ +import { describe, it } from "@effect/vitest" +import { strictEqual } from "@effect/vitest/utils" +import * as S from "effect/Schema" +import * as Util from "../TestUtils.js" + +describe("suspend", () => { + describe("toString", () => { + it("outer suspend", () => { + type A = readonly [number, A | null] + const schema: S.Schema = S.suspend( // intended outer suspend + () => S.Tuple(S.Number, S.Union(schema, S.Literal(null))) + ) + strictEqual(String(schema), "") + }) + + it("should handle before initialization error", () => { + const schema = S.suspend(() => string) + strictEqual(String(schema), "") + const string = S.String + }) + }) + + describe("decoding", () => { + it("baseline", async () => { + interface A { + readonly a: string + readonly as: ReadonlyArray + } + const schema = S.Struct({ + a: S.String, + as: S.Array(S.suspend((): S.Schema => schema)) + }) + + await Util.assertions.decoding.succeed(schema, { a: "a1", as: [] }) + await Util.assertions.decoding.succeed(schema, { a: "a1", as: [{ a: "a2", as: [] }] }) + + await Util.assertions.decoding.fail( + schema, + null, + `Expected { readonly a: string; readonly as: ReadonlyArray<> }, actual null` + ) + await Util.assertions.decoding.fail( + schema, + { a: "a1" }, + `{ readonly a: string; readonly as: ReadonlyArray<> } +└─ ["as"] + └─ is missing` + ) + await Util.assertions.decoding.fail( + schema, + { a: "a1", as: [{ a: "a2", as: [1] }] }, + `{ readonly a: string; readonly as: ReadonlyArray<> } +└─ ["as"] + └─ ReadonlyArray<> + └─ [0] + └─ { readonly a: string; readonly as: ReadonlyArray<> } + └─ ["as"] + └─ ReadonlyArray<> + └─ [0] + └─ Expected { readonly a: string; readonly as: ReadonlyArray<> }, actual 1` + ) + }) + + it("mutually suspended", async () => { + interface Expression { + readonly type: "expression" + readonly value: number | Operation + } + + interface Operation { + readonly type: "operation" + readonly operator: "+" | "-" + readonly left: Expression + readonly right: Expression + } + + const Expression = S.Struct({ + type: S.Literal("expression"), + value: S.Union(S.Number, S.suspend((): S.Schema => Operation)) + }) + + const Operation = S.Struct({ + type: S.Literal("operation"), + operator: S.Union(S.Literal("+"), S.Literal("-")), + left: Expression, + right: Expression + }) + + const input = { + type: "operation", + operator: "+", + left: { + type: "expression", + value: { + type: "operation", + operator: "-", + left: { + type: "expression", + value: 2 + }, + right: { + type: "expression", + value: 3 + } + } + }, + right: { + type: "expression", + value: 1 + } + } + + await Util.assertions.decoding.succeed(Operation, input) + }) + }) + + describe("encoding", () => { + it("suspend", async () => { + interface A { + readonly a: number + readonly as: ReadonlyArray + } + interface FromA { + readonly a: string + readonly as: ReadonlyArray + } + const schema = S.Struct({ + a: Util.NumberFromChar, + as: S.Array(S.suspend((): S.Schema => schema)) + }) + await Util.assertions.encoding.succeed(schema, { a: 1, as: [] }, { a: "1", as: [] }) + await Util.assertions.encoding.succeed(schema, { a: 1, as: [{ a: 2, as: [] }] }, { + a: "1", + as: [{ a: "2", as: [] }] + }) + }) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/transform.test.ts b/repos/effect/packages/effect/test/Schema/Schema/transform.test.ts new file mode 100644 index 0000000..2057dce --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/transform.test.ts @@ -0,0 +1,25 @@ +import { describe, it } from "@effect/vitest" +import * as Schema from "effect/Schema" +import * as Util from "../TestUtils.js" + +describe("transform", () => { + it("should receive the fromI value other than the fromA value", async () => { + const A = Schema.Struct({ + a: Schema.NumberFromString + }) + + const B = Schema.Struct({ + a: Schema.String, + b: Schema.NumberFromString + }) + + const AB = Schema.transform(B, A, { + strict: true, + decode: ({ a, b: _b }, i) => ({ a: a + i.b }), + encode: (i, a) => ({ ...i, b: a.a * 2 }) + }) + + await Util.assertions.decoding.succeed(AB, { a: "1", b: "2" }, { a: 12 }) + await Util.assertions.encoding.succeed(AB, { a: 2 }, { a: "2", b: "4" }) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/transformLiterals.test.ts b/repos/effect/packages/effect/test/Schema/Schema/transformLiterals.test.ts new file mode 100644 index 0000000..c107c9b --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/transformLiterals.test.ts @@ -0,0 +1,44 @@ +import { describe, it } from "@effect/vitest" +import * as S from "effect/Schema" +import * as Util from "../TestUtils.js" + +describe("transformLiteral", () => { + describe("Struct", () => { + it("simple", async () => { + const schema = S.transformLiteral(0, "a") + + await Util.assertions.decoding.succeed(schema, 0, "a") + await Util.assertions.encoding.succeed(schema, "a", 0) + }) + }) +}) + +describe("transformLiterals", () => { + describe("Struct", () => { + it("simple", async () => { + const schema = S.transformLiterals( + [0, "a"], + [1, "b"], + [2, "c"] + ) + + await Util.assertions.decoding.succeed(schema, 1, "b") + await Util.assertions.encoding.succeed(schema, "b", 1) + }) + + it("mixed types", async () => { + const schema = S.transformLiterals( + [0, BigInt(0)], + ["a", true], + [null, false] + ) + + await Util.assertions.decoding.succeed(schema, 0, BigInt(0)) + await Util.assertions.encoding.succeed(schema, BigInt(0), 0) + await Util.assertions.decoding.succeed(schema, "a", true) + await Util.assertions.encoding.succeed(schema, true, "a") + await Util.assertions.decoding.succeed(schema, null, false) + await Util.assertions.encoding.succeed(schema, false, null) + }) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/transformOrFail.test.ts b/repos/effect/packages/effect/test/Schema/Schema/transformOrFail.test.ts new file mode 100644 index 0000000..025db2c --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/transformOrFail.test.ts @@ -0,0 +1,26 @@ +import { describe, it } from "@effect/vitest" +import * as ParseResult from "effect/ParseResult" +import * as Schema from "effect/Schema" +import * as Util from "../TestUtils.js" + +describe("transformOrFail", () => { + it("should receive the fromI value other than the fromA value", async () => { + const A = Schema.Struct({ + a: Schema.NumberFromString + }) + + const B = Schema.Struct({ + a: Schema.String, + b: Schema.NumberFromString + }) + + const AB = Schema.transformOrFail(B, A, { + strict: true, + decode: ({ a, b: _b }, _options, _ast, i) => ParseResult.succeed({ a: a + i.b }), + encode: (i, _options, _ast, a) => ParseResult.succeed({ ...i, b: a.a * 2 }) + }) + + await Util.assertions.decoding.succeed(AB, { a: "1", b: "2" }, { a: 12 }) + await Util.assertions.encoding.succeed(AB, { a: 2 }, { a: "2", b: "4" }) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/typeSchema.test.ts b/repos/effect/packages/effect/test/Schema/Schema/typeSchema.test.ts new file mode 100644 index 0000000..5dce5b0 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/typeSchema.test.ts @@ -0,0 +1,54 @@ +import { describe, it } from "@effect/vitest" +import { assertFalse, assertTrue, deepStrictEqual } from "@effect/vitest/utils" +import * as S from "effect/Schema" +import * as Util from "../TestUtils.js" + +describe("typeSchema", () => { + it("transformation", () => { + const schema = S.String.pipe( + S.transform( + S.Tuple(S.NumberFromString, S.NumberFromString), + { strict: true, decode: (s) => [s, s] as const, encode: ([s]) => s } + ), + S.typeSchema + ) + deepStrictEqual(S.decodeUnknownSync(schema)([1, 2]), [1, 2]) + }) + + it("refinement", () => { + const schema = S.NumberFromString.pipe( + S.greaterThanOrEqualTo(1), + S.lessThanOrEqualTo(2), + S.typeSchema + ) + assertFalse(S.is(schema)(0)) + assertTrue(S.is(schema)(1)) + assertTrue(S.is(schema)(2)) + assertFalse(S.is(schema)(3)) + }) + + it("suspend", async () => { + interface I { + prop: I | string + } + interface A { + prop: A | number + } + const schema: S.Schema = S.suspend( // intended outer suspend + () => + S.Struct({ + prop: S.Union(S.NumberFromString, schema) + }) + ) + const to = S.typeSchema(schema) + await Util.assertions.decoding.succeed(to, { prop: 1 }) + await Util.assertions.decoding.succeed(to, { prop: { prop: 1 } }) + }) + + it("decoding", async () => { + const schema = S.typeSchema(S.NumberFromString) + await Util.assertions.decoding.succeed(schema, 1) + await Util.assertions.decoding.fail(schema, null, "Expected number, actual null") + await Util.assertions.decoding.fail(schema, "a", `Expected number, actual "a"`) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/validate.test.ts b/repos/effect/packages/effect/test/Schema/Schema/validate.test.ts new file mode 100644 index 0000000..1d47db4 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/validate.test.ts @@ -0,0 +1,40 @@ +import { describe, it } from "@effect/vitest" +import * as Effect from "effect/Effect" +import * as S from "effect/Schema" +import * as Util from "../TestUtils.js" + +describe("validate", () => { + const schema = S.Struct({ a: Util.NumberFromChar }) + + it("should return an error on invalid values", async () => { + await Util.assertions.effect.succeed(S.validate(schema)({ a: 1 }), { a: 1 }) + await Util.assertions.effect.fail( + S.validate(schema)({ a: null }).pipe(Effect.mapError((e) => e.issue)), + `{ readonly a: number } +└─ ["a"] + └─ Expected number, actual null` + ) + }) + + it("should respect outer/inner options", async () => { + const input = { a: 1, b: "b" } + await Util.assertions.effect.fail( + S.validate(schema)(input, { onExcessProperty: "error" }).pipe(Effect.mapError((e) => e.issue)), + `{ readonly a: number } +└─ ["b"] + └─ is unexpected, expected: "a"` + ) + await Util.assertions.effect.fail( + S.validate(schema, { onExcessProperty: "error" })(input).pipe(Effect.mapError((e) => e.issue)), + `{ readonly a: number } +└─ ["b"] + └─ is unexpected, expected: "a"` + ) + await Util.assertions.effect.succeed( + S.validate(schema, { onExcessProperty: "error" })(input, { onExcessProperty: "ignore" }), + { + a: 1 + } + ) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/validateEither.test.ts b/repos/effect/packages/effect/test/Schema/Schema/validateEither.test.ts new file mode 100644 index 0000000..02ba931 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/validateEither.test.ts @@ -0,0 +1,46 @@ +import { describe, it } from "@effect/vitest" +import * as Either from "effect/Either" +import * as S from "effect/Schema" +import * as Util from "../TestUtils.js" + +describe("validateEither", () => { + const schema = S.Struct({ a: Util.NumberFromChar }) + + it("should return an error on invalid values", async () => { + Util.assertions.either.right(S.validateEither(schema)({ a: 1 }), { a: 1 }) + await Util.assertions.either.fail( + S.validateEither(schema)({ a: null }).pipe(Either.mapLeft((e) => e.issue)), + `{ readonly a: number } +└─ ["a"] + └─ Expected number, actual null` + ) + }) + + it("should return an error on async", async () => { + await Util.assertions.either.fail( + S.encodeEither(Util.AsyncDeclaration)("a").pipe(Either.mapLeft((e) => e.issue)), + `AsyncDeclaration +└─ cannot be be resolved synchronously, this is caused by using runSync on an effect that performs async work` + ) + }) + + it("should respect outer/inner options", async () => { + const input = { a: 1, b: "b" } + await Util.assertions.either.fail( + S.validateEither(schema)(input, { onExcessProperty: "error" }).pipe(Either.mapLeft((e) => e.issue)), + `{ readonly a: number } +└─ ["b"] + └─ is unexpected, expected: "a"` + ) + await Util.assertions.either.fail( + S.validateEither(schema, { onExcessProperty: "error" })(input).pipe(Either.mapLeft((e) => e.issue)), + `{ readonly a: number } +└─ ["b"] + └─ is unexpected, expected: "a"` + ) + Util.assertions.either.right( + S.validateEither(schema, { onExcessProperty: "error" })(input, { onExcessProperty: "ignore" }), + { a: 1 } + ) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/validateOption.test.ts b/repos/effect/packages/effect/test/Schema/Schema/validateOption.test.ts new file mode 100644 index 0000000..34dd425 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/validateOption.test.ts @@ -0,0 +1,24 @@ +import { describe, it } from "@effect/vitest" +import { assertNone, assertSome } from "@effect/vitest/utils" +import { Schema as S } from "effect" +import * as Util from "../TestUtils.js" + +describe("validateOption", () => { + it("should return none on async", () => { + assertNone(S.validateOption(Util.AsyncDeclaration)("a")) + }) + + const schema = S.Struct({ a: Util.NumberFromChar }) + + it("should return None on invalid values", () => { + assertSome(S.validateOption(schema)({ a: 1 }), { a: 1 }) + assertNone(S.validateOption(schema)({ a: null })) + }) + + it("should respect outer/inner options", () => { + const input = { a: 1, b: "b" } + assertNone(S.validateOption(schema)(input, { onExcessProperty: "error" })) + assertNone(S.validateOption(schema, { onExcessProperty: "error" })(input)) + assertSome(S.validateOption(schema, { onExcessProperty: "error" })(input, { onExcessProperty: "ignore" }), { a: 1 }) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/validatePromise.test.ts b/repos/effect/packages/effect/test/Schema/Schema/validatePromise.test.ts new file mode 100644 index 0000000..bc77461 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/validatePromise.test.ts @@ -0,0 +1,42 @@ +import { describe, it } from "@effect/vitest" +import { deepStrictEqual } from "@effect/vitest/utils" +import { ParseResult, Schema as S } from "effect" +import * as Util from "../TestUtils.js" + +describe("validatePromise", () => { + const schema = S.Struct({ a: Util.NumberFromChar }) + + it("should reject on invalid values", async () => { + deepStrictEqual(await S.validatePromise(schema)({ a: 1 }), { a: 1 }) + deepStrictEqual(await ParseResult.validatePromise(schema)({ a: 1 }), { a: 1 }) + + await Util.assertions.promise.fail( + S.validatePromise(schema)({ a: null }), + `{ readonly a: number } +└─ ["a"] + └─ Expected number, actual null` + ) + }) + + it("should respect outer/inner options", async () => { + const input = { a: 1, b: "b" } + + deepStrictEqual( + await S.validatePromise(schema, { onExcessProperty: "error" })(input, { onExcessProperty: "ignore" }), + { a: 1 } + ) + + await Util.assertions.promise.fail( + S.validatePromise(schema)(input, { onExcessProperty: "error" }), + `{ readonly a: number } +└─ ["b"] + └─ is unexpected, expected: "a"` + ) + await Util.assertions.promise.fail( + S.validatePromise(schema, { onExcessProperty: "error" })(input), + `{ readonly a: number } +└─ ["b"] + └─ is unexpected, expected: "a"` + ) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/validateSync.test.ts b/repos/effect/packages/effect/test/Schema/Schema/validateSync.test.ts new file mode 100644 index 0000000..af195c5 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/validateSync.test.ts @@ -0,0 +1,45 @@ +import { describe, it } from "@effect/vitest" +import { deepStrictEqual } from "@effect/vitest/utils" +import * as S from "effect/Schema" +import * as Util from "../TestUtils.js" + +describe("validateSync", () => { + const schema = S.Struct({ a: Util.NumberFromChar }) + + it("should throw on invalid values", () => { + deepStrictEqual(S.validateSync(schema)({ a: 1 }), { a: 1 }) + Util.assertions.parseError( + () => S.validateSync(schema)({ a: null }), + `{ readonly a: number } +└─ ["a"] + └─ Expected number, actual null` + ) + }) + + it("should throw on async", () => { + Util.assertions.parseError( + () => S.validateSync(Util.AsyncDeclaration)("a"), + `AsyncDeclaration +└─ cannot be be resolved synchronously, this is caused by using runSync on an effect that performs async work` + ) + }) + + it("should respect outer/inner options", () => { + const input = { a: 1, b: "b" } + Util.assertions.parseError( + () => S.validateSync(schema)(input, { onExcessProperty: "error" }), + `{ readonly a: number } +└─ ["b"] + └─ is unexpected, expected: "a"` + ) + Util.assertions.parseError( + () => S.validateSync(schema, { onExcessProperty: "error" })(input), + `{ readonly a: number } +└─ ["b"] + └─ is unexpected, expected: "a"` + ) + deepStrictEqual(S.validateSync(schema, { onExcessProperty: "error" })(input, { onExcessProperty: "ignore" }), { + a: 1 + }) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/withConstructorDefault.test.ts b/repos/effect/packages/effect/test/Schema/Schema/withConstructorDefault.test.ts new file mode 100644 index 0000000..8258018 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/withConstructorDefault.test.ts @@ -0,0 +1,22 @@ +import { describe, it } from "@effect/vitest" +import { strictEqual } from "@effect/vitest/utils" +import * as S from "effect/Schema" + +describe("withConstructorDefault", () => { + it("annotating a PropertySignatureDeclaration should repect existing defaultValues", () => { + const prop: any = S.propertySignature(S.String).pipe(S.withConstructorDefault(() => "")).annotations({}) + strictEqual(prop.ast.defaultValue(), "") + }) + + it("annotating a PropertySignatureTransformation should repect existing defaultValues", () => { + const prop: any = S.optionalWith(S.String, { nullable: true }).pipe(S.withConstructorDefault(() => "")).annotations( + {} + ) + strictEqual(prop.ast.to.defaultValue(), "") + }) + + it("using fromKey should repect existing defaultValues", () => { + const prop: any = S.propertySignature(S.String).pipe(S.withConstructorDefault(() => ""), S.fromKey("a")) + strictEqual(prop.ast.to.defaultValue(), "") + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Schema/withDecodingDefault.test.ts b/repos/effect/packages/effect/test/Schema/Schema/withDecodingDefault.test.ts new file mode 100644 index 0000000..953d23f --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Schema/withDecodingDefault.test.ts @@ -0,0 +1,59 @@ +import { describe, it } from "@effect/vitest" +import { strictEqual } from "@effect/vitest/utils" +import * as S from "effect/Schema" +import * as Util from "../TestUtils.js" + +describe("withDecodingDefault", () => { + describe("PropertySignatureDeclaration", () => { + it("optional", async () => { + const prop = S.optional(S.String).pipe(S.withDecodingDefault(() => "")) + const schema = S.Struct({ a: prop }) + await Util.assertions.decoding.succeed(schema, {}, { a: "" }) + await Util.assertions.decoding.succeed(schema, { a: undefined }, { a: "" }) + await Util.assertions.decoding.succeed(schema, { a: "a" }) + }) + + it("optionalWith { exact: true }", async () => { + const prop = S.optionalWith(S.String, { exact: true }).pipe(S.withDecodingDefault(() => "")) + const schema = S.Struct({ a: prop }) + await Util.assertions.decoding.succeed(schema, {}, { a: "" }) + await Util.assertions.decoding.succeed(schema, { a: "a" }) + }) + + it("should prune undefined from the type", () => { + const prop1 = S.optional(S.String).pipe(S.withDecodingDefault(() => "")) + strictEqual(String(prop1), `PropertySignature<":", string, never, "?:", string | undefined>`) + + const prop2 = S.optional(S.NumberFromString).pipe(S.withDecodingDefault(() => 0)) + strictEqual(String(prop2), `PropertySignature<":", number, never, "?:", NumberFromString | undefined>`) + }) + }) + + describe("PropertySignatureTransformation", () => { + it("optional", async () => { + const prop = S.optional(S.String).pipe(S.fromKey("b"), S.withDecodingDefault(() => "")) + const schema = S.Struct({ a: prop }) + await Util.assertions.decoding.succeed(schema, {}, { a: "" }) + await Util.assertions.decoding.succeed(schema, { b: undefined }, { a: "" }) + await Util.assertions.decoding.succeed(schema, { b: "a" }, { a: "a" }) + }) + + it("optionalWith { exact: true }", async () => { + const prop = S.optionalWith(S.String, { exact: true }).pipe( + S.fromKey("b"), + S.withDecodingDefault(() => "") + ) + const schema = S.Struct({ a: prop }) + await Util.assertions.decoding.succeed(schema, {}, { a: "" }) + await Util.assertions.decoding.succeed(schema, { b: "a" }, { a: "a" }) + }) + + it("should prune undefined from the type", () => { + const prop1 = S.optional(S.String).pipe(S.fromKey("a"), S.withDecodingDefault(() => "")) + strictEqual(String(prop1), `PropertySignature<":", string, "a", "?:", string | undefined>`) + + const prop2 = S.optional(S.NumberFromString).pipe(S.fromKey("a"), S.withDecodingDefault(() => 0)) + strictEqual(String(prop2), `PropertySignature<":", number, "a", "?:", NumberFromString | undefined>`) + }) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/SchemaAST/IndexSignature.test.ts b/repos/effect/packages/effect/test/Schema/SchemaAST/IndexSignature.test.ts new file mode 100644 index 0000000..714ea41 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/SchemaAST/IndexSignature.test.ts @@ -0,0 +1,15 @@ +import { describe, it } from "@effect/vitest" +import { throws } from "@effect/vitest/utils" +import * as AST from "effect/SchemaAST" + +describe("AST.IndexSignature", () => { + it("new IndexSignature should throw on unsupported ASTs", () => { + throws( + () => new AST.IndexSignature(AST.booleanKeyword, AST.stringKeyword, true), + new Error( + `Unsupported index signature parameter +details: An index signature parameter type must be \`string\`, \`symbol\`, a template literal type or a refinement of the previous types` + ) + ) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/SchemaAST/Refinement.test.ts b/repos/effect/packages/effect/test/Schema/SchemaAST/Refinement.test.ts new file mode 100644 index 0000000..69efee4 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/SchemaAST/Refinement.test.ts @@ -0,0 +1,12 @@ +import { describe, it } from "@effect/vitest" +import { strictEqual } from "@effect/vitest/utils" +import * as S from "effect/Schema" + +describe("AST.Refinement", () => { + it("toString", () => { + strictEqual(String(S.Number.pipe(S.filter(() => true))), "{ number | filter }") + strictEqual(String(S.Number.pipe(S.int())), "int") + strictEqual(String(S.Number.pipe(S.int(), S.positive())), "int & positive") + strictEqual(String(S.Int.pipe(S.positive())), "Int & positive") + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/SchemaAST/Tuple.test.ts b/repos/effect/packages/effect/test/Schema/SchemaAST/Tuple.test.ts new file mode 100644 index 0000000..f2cec5f --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/SchemaAST/Tuple.test.ts @@ -0,0 +1,36 @@ +import { describe, it } from "@effect/vitest" +import { strictEqual, throws } from "@effect/vitest/utils" +import * as S from "effect/Schema" +import * as AST from "effect/SchemaAST" + +describe("AST.Tuple", () => { + it("toString", () => { + strictEqual(String(S.Tuple(S.String, S.optionalElement(S.Number))), "readonly [string, number?]") + }) + + it("A required element cannot follow an optional element", () => { + throws( + () => + new AST.TupleType( + [new AST.OptionalType(AST.stringKeyword, true), new AST.OptionalType(AST.stringKeyword, false)], + [], + true + ), + new Error(`Invalid element +details: A required element cannot follow an optional element. ts(1257)`) + ) + }) + + it("A required rest element cannot follow an optional element", () => { + throws( + () => + new AST.TupleType( + [new AST.OptionalType(AST.stringKeyword, true)], + [new AST.Type(AST.stringKeyword), new AST.Type(AST.stringKeyword)], + true + ), + new Error(`Invalid element +details: A required element cannot follow an optional element. ts(1257)`) + ) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/SchemaAST/TypeLiteral.test.ts b/repos/effect/packages/effect/test/Schema/SchemaAST/TypeLiteral.test.ts new file mode 100644 index 0000000..c0a9a5f --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/SchemaAST/TypeLiteral.test.ts @@ -0,0 +1,34 @@ +import { describe, it } from "@effect/vitest" +import { strictEqual, throws } from "@effect/vitest/utils" +import * as S from "effect/Schema" +import * as AST from "effect/SchemaAST" + +describe("AST.TypeLiteral", () => { + it("should throw on onvalid index signature parameters", () => { + throws( + () => new AST.IndexSignature(S.NumberFromString.ast, AST.stringKeyword, true), + new Error( + `Unsupported index signature parameter +details: An index signature parameter type must be \`string\`, \`symbol\`, a template literal type or a refinement of the previous types` + ) + ) + }) + + describe("toString", () => { + it("Struct (immutable)", () => { + strictEqual(S.Struct({ a: S.String, b: S.Number }).ast.toString(), `{ readonly a: string; readonly b: number }`) + }) + + it("Struct (mutable)", () => { + strictEqual(S.mutable(S.Struct({ a: S.String, b: S.Number })).ast.toString(), `{ a: string; b: number }`) + }) + + it("Record (immutable)", () => { + strictEqual(S.Record({ key: S.String, value: S.Number }).ast.toString(), `{ readonly [x: string]: number }`) + }) + + it("Record (mutable)", () => { + strictEqual(S.mutable(S.Record({ key: S.String, value: S.Number })).ast.toString(), `{ [x: string]: number }`) + }) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/SchemaAST/TypeLiteralTransformation.test.ts b/repos/effect/packages/effect/test/Schema/SchemaAST/TypeLiteralTransformation.test.ts new file mode 100644 index 0000000..3983377 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/SchemaAST/TypeLiteralTransformation.test.ts @@ -0,0 +1,27 @@ +import { describe, it } from "@effect/vitest" +import { throws } from "@effect/vitest/utils" +import { identity } from "effect/Function" +import * as AST from "effect/SchemaAST" + +describe("AST.TypeLiteralTransformation", () => { + it("Duplicate property signature transformation", () => { + throws( + () => + new AST.TypeLiteralTransformation([ + new AST.PropertySignatureTransformation("a", "b", identity, identity), + new AST.PropertySignatureTransformation("a", "c", identity, identity) + ]), + new Error(`Duplicate property signature transformation +details: Duplicate key "a"`) + ) + throws( + () => + new AST.TypeLiteralTransformation([ + new AST.PropertySignatureTransformation("a", "c", identity, identity), + new AST.PropertySignatureTransformation("b", "c", identity, identity) + ]), + new Error(`Duplicate property signature transformation +details: Duplicate key "c"`) + ) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/SchemaAST/Union.test.ts b/repos/effect/packages/effect/test/Schema/SchemaAST/Union.test.ts new file mode 100644 index 0000000..e69ddf9 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/SchemaAST/Union.test.ts @@ -0,0 +1,61 @@ +import { describe, it } from "@effect/vitest" +import { deepStrictEqual, strictEqual } from "@effect/vitest/utils" +import * as S from "effect/Schema" +import * as AST from "effect/SchemaAST" + +describe("AST.Union", () => { + it("flatten should un-nest union members", () => { + const asts = AST.flatten([S.Union(S.Literal("a", "b"), S.Literal("c", "d")).ast]) + strictEqual(asts.length, 4) + }) + + it("unify should remove never from members", () => { + strictEqual(AST.Union.unify([AST.neverKeyword, AST.neverKeyword]), AST.neverKeyword) + strictEqual(AST.Union.unify([AST.neverKeyword, AST.stringKeyword]), AST.stringKeyword) + strictEqual(AST.Union.unify([AST.stringKeyword, AST.neverKeyword]), AST.stringKeyword) + deepStrictEqual( + AST.Union.unify([ + AST.neverKeyword, + AST.stringKeyword, + AST.neverKeyword, + AST.numberKeyword + ]), + AST.Union.unify([AST.stringKeyword, AST.numberKeyword]) + ) + }) + + describe("toString", () => { + it("string | number", () => { + strictEqual(String(S.Union(S.String, S.Number)), "string | number") + }) + + it("should support suspended schemas", () => { + interface A { + readonly a?: null | A | undefined + } + // intended outer suspend + const schema: S.Schema = S.partial( + S.suspend( + () => + S.Struct({ + a: S.Union(S.Null, schema) + }) + ) + ) + strictEqual(String(schema), "") + }) + + it("descriptions of nested unions should be preserved", () => { + const u = S.Union(S.String, S.Number) + const nested1 = u.annotations({ identifier: "nested1" }) + const nested2 = u.annotations({ identifier: "nested2" }) + + strictEqual(String(u), "string | number") + strictEqual(String(S.Union(nested1, nested1)), "nested1 | nested1") + strictEqual(String(S.Union(nested1, S.String)), "nested1 | string") + strictEqual(String(S.Union(nested1, u)), "nested1 | string | number") + strictEqual(String(S.Union(nested1, nested2)), "nested1 | nested2") + strictEqual(String(S.Union(nested1, nested2, S.String)), "nested1 | nested2 | string") + }) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/SchemaAST/annotations.test.ts b/repos/effect/packages/effect/test/Schema/SchemaAST/annotations.test.ts new file mode 100644 index 0000000..52c8eb4 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/SchemaAST/annotations.test.ts @@ -0,0 +1,26 @@ +import { describe, it } from "@effect/vitest" +import { assertInstanceOf, deepStrictEqual } from "@effect/vitest/utils" +import * as AST from "effect/SchemaAST" + +describe("annotations", () => { + it("should add annotations", () => { + const symA = Symbol.for("a") + const ast = AST.annotations(AST.stringKeyword, { [symA]: "A" }) + assertInstanceOf(ast, AST.StringKeyword) + deepStrictEqual( + ast, + new AST.StringKeyword({ + [AST.TitleAnnotationId]: "string", + [AST.DescriptionAnnotationId]: "a string", + [symA]: "A" + }) + ) + deepStrictEqual( + AST.stringKeyword, + new AST.StringKeyword({ + [AST.TitleAnnotationId]: "string", + [AST.DescriptionAnnotationId]: "a string" + }) + ) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/SchemaAST/encodedAST.test.ts b/repos/effect/packages/effect/test/Schema/SchemaAST/encodedAST.test.ts new file mode 100644 index 0000000..8d51ef3 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/SchemaAST/encodedAST.test.ts @@ -0,0 +1,74 @@ +import { describe, it } from "@effect/vitest" +import { assertFalse, assertTrue, strictEqual } from "@effect/vitest/utils" +import * as S from "effect/Schema" +import * as AST from "effect/SchemaAST" + +describe("encodedAST", () => { + it("refinements", () => { + const ast = S.String.pipe(S.minLength(2)).ast + const encodedAST = AST.encodedAST(ast) + strictEqual(encodedAST, S.String.ast) + }) + + describe(`should return the same reference if the AST doesn't represent a transformation`, () => { + it("declaration (true)", () => { + const schema = S.OptionFromSelf(S.String) + assertTrue(AST.encodedAST(schema.ast) === schema.ast) + }) + + it("declaration (false)", () => { + const schema = S.OptionFromSelf(S.NumberFromString) + assertFalse(AST.encodedAST(schema.ast) === schema.ast) + }) + + it("tuple (true)", () => { + const schema = S.Tuple(S.String, S.Number) + assertTrue(AST.encodedAST(schema.ast) === schema.ast) + }) + + it("tuple (false)", () => { + const schema = S.Tuple(S.String, S.NumberFromString) + assertFalse(AST.encodedAST(schema.ast) === schema.ast) + }) + + it("array (true)", () => { + const schema = S.Array(S.Number) + assertTrue(AST.encodedAST(schema.ast) === schema.ast) + }) + + it("array (false)", () => { + const schema = S.Array(S.NumberFromString) + assertFalse(AST.encodedAST(schema.ast) === schema.ast) + }) + + it("union (true)", () => { + const schema = S.Union(S.String, S.Number) + assertTrue(AST.encodedAST(schema.ast) === schema.ast) + }) + + it("union (false)", () => { + const schema = S.Union(S.String, S.NumberFromString) + assertFalse(AST.encodedAST(schema.ast) === schema.ast) + }) + + it("struct (true)", () => { + const schema = S.Struct({ a: S.String, b: S.Number }) + assertTrue(AST.encodedAST(schema.ast) === schema.ast) + }) + + it("struct (false)", () => { + const schema = S.Struct({ a: S.String, b: S.NumberFromString }) + assertFalse(AST.encodedAST(schema.ast) === schema.ast) + }) + + it("record (true)", () => { + const schema = S.Record({ key: S.String, value: S.Number }) + assertTrue(AST.encodedAST(schema.ast) === schema.ast) + }) + + it("record (false)", () => { + const schema = S.Record({ key: S.String, value: S.NumberFromString }) + assertFalse(AST.encodedAST(schema.ast) === schema.ast) + }) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/SchemaAST/encodedBoundAST.test.ts b/repos/effect/packages/effect/test/Schema/SchemaAST/encodedBoundAST.test.ts new file mode 100644 index 0000000..be6b4c0 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/SchemaAST/encodedBoundAST.test.ts @@ -0,0 +1,74 @@ +import { describe, it } from "@effect/vitest" +import { assertFalse, assertTrue } from "@effect/vitest/utils" +import * as S from "effect/Schema" +import * as AST from "effect/SchemaAST" + +describe("encodedBoundAST", () => { + it("refinements", () => { + const ast = S.String.pipe(S.minLength(2)).ast + const encodedAST = AST.encodedBoundAST(ast) + assertTrue(encodedAST === ast) + }) + + describe(`should return the same reference if the AST doesn't represent a transformation`, () => { + it("declaration (true)", () => { + const schema = S.OptionFromSelf(S.String) + assertTrue(AST.encodedBoundAST(schema.ast) === schema.ast) + }) + + it("declaration (false)", () => { + const schema = S.OptionFromSelf(S.NumberFromString) + assertFalse(AST.encodedBoundAST(schema.ast) === schema.ast) + }) + + it("tuple (true)", () => { + const schema = S.Tuple(S.String, S.Number) + assertTrue(AST.encodedBoundAST(schema.ast) === schema.ast) + }) + + it("tuple (false)", () => { + const schema = S.Tuple(S.String, S.NumberFromString) + assertFalse(AST.encodedBoundAST(schema.ast) === schema.ast) + }) + + it("array (true)", () => { + const schema = S.Array(S.Number) + assertTrue(AST.encodedBoundAST(schema.ast) === schema.ast) + }) + + it("array (false)", () => { + const schema = S.Array(S.NumberFromString) + assertFalse(AST.encodedBoundAST(schema.ast) === schema.ast) + }) + + it("union (true)", () => { + const schema = S.Union(S.String, S.Number) + assertTrue(AST.encodedBoundAST(schema.ast) === schema.ast) + }) + + it("union (false)", () => { + const schema = S.Union(S.String, S.NumberFromString) + assertFalse(AST.encodedBoundAST(schema.ast) === schema.ast) + }) + + it("struct (true)", () => { + const schema = S.Struct({ a: S.String, b: S.Number }) + assertTrue(AST.encodedBoundAST(schema.ast) === schema.ast) + }) + + it("struct (false)", () => { + const schema = S.Struct({ a: S.String, b: S.NumberFromString }) + assertFalse(AST.encodedBoundAST(schema.ast) === schema.ast) + }) + + it("record (true)", () => { + const schema = S.Record({ key: S.String, value: S.Number }) + assertTrue(AST.encodedBoundAST(schema.ast) === schema.ast) + }) + + it("record (false)", () => { + const schema = S.Record({ key: S.String, value: S.NumberFromString }) + assertFalse(AST.encodedBoundAST(schema.ast) === schema.ast) + }) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/SchemaAST/equals.test.ts b/repos/effect/packages/effect/test/Schema/SchemaAST/equals.test.ts new file mode 100644 index 0000000..b80256a --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/SchemaAST/equals.test.ts @@ -0,0 +1,15 @@ +import { describe, it } from "@effect/vitest" +import { assertFalse, assertTrue } from "@effect/vitest/utils" +import * as S from "effect/Schema" +import * as AST from "effect/SchemaAST" + +describe("equals", () => { + describe("TemplateLiteral", () => { + it(`("a" | "b") + string + ("d" | "e")`, () => { + const schema1 = S.TemplateLiteral(S.Literal("a", "b"), S.String, S.Literal("d", "e")) + const schema2 = S.TemplateLiteral(S.Literal("a", "b"), S.String, S.Literal("d", "f")) + assertTrue(AST.equals(schema1.ast, schema1.ast)) + assertFalse(AST.equals(schema1.ast, schema2.ast)) + }) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/SchemaAST/getPropertySignatures.test.ts b/repos/effect/packages/effect/test/Schema/SchemaAST/getPropertySignatures.test.ts new file mode 100644 index 0000000..8e659fb --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/SchemaAST/getPropertySignatures.test.ts @@ -0,0 +1,72 @@ +import { describe, it } from "@effect/vitest" +import { deepStrictEqual } from "@effect/vitest/utils" +import * as S from "effect/Schema" +import * as AST from "effect/SchemaAST" + +describe("getPropertySignatures", () => { + it("Struct", () => { + const schema = S.Struct({ a: S.String, b: S.Number }) + deepStrictEqual(AST.getPropertySignatures(schema.ast), [ + new AST.PropertySignature("a", S.String.ast, false, true), + new AST.PropertySignature("b", S.Number.ast, false, true) + ]) + }) + + it("Refinement", () => { + const schema = S.Struct({ a: S.String, b: S.Number }).pipe(S.filter(() => true)) + deepStrictEqual(AST.getPropertySignatures(schema.ast), [ + new AST.PropertySignature("a", S.String.ast, false, true), + new AST.PropertySignature("b", S.Number.ast, false, true) + ]) + }) + + it("suspend", () => { + const schema = S.suspend(() => S.Struct({ a: S.String, b: S.Number })) + deepStrictEqual(AST.getPropertySignatures(schema.ast), [ + new AST.PropertySignature("a", S.String.ast, false, true), + new AST.PropertySignature("b", S.Number.ast, false, true) + ]) + }) + + it("Union", () => { + const schema = S.Union(S.Struct({ _tag: S.Literal("A") }), S.Struct({ _tag: S.Literal("B") })) + deepStrictEqual(AST.getPropertySignatures(schema.ast), [ + new AST.PropertySignature("_tag", S.Literal("A", "B").ast, false, true) + ]) + }) + + it("Class", () => { + class A extends S.Class("A")({ a: S.String, b: S.Number }) {} + const schema = A.pipe(S.typeSchema) + deepStrictEqual(AST.getPropertySignatures(schema.ast), [ + new AST.PropertySignature("a", S.String.ast, false, true), + new AST.PropertySignature("b", S.Number.ast, false, true) + ]) + }) + + it("Transformation (Struct with optionalWith default)", () => { + const schema = S.Struct({ + a: S.String, + b: S.optionalWith(S.Number, { default: () => 0 }) + }) + deepStrictEqual(AST.getPropertySignatures(schema.ast), [ + new AST.PropertySignature("a", S.String.ast, false, true), + new AST.PropertySignature("b", S.Number.ast, false, true) + ]) + }) + + it("Transformation (Struct with optionalWith as Option)", () => { + const schema = S.Struct({ + a: S.String, + b: S.optionalWith(S.Number, { as: "Option" }) + }) + const signatures = AST.getPropertySignatures(schema.ast) + deepStrictEqual(signatures.length, 2) + deepStrictEqual(signatures[0], new AST.PropertySignature("a", S.String.ast, false, true)) + deepStrictEqual(signatures[1].name, "b") + deepStrictEqual(signatures[1].isOptional, false) + deepStrictEqual(signatures[1].isReadonly, true) + // b's type on the decoded side is Option (a Declaration AST) + deepStrictEqual(signatures[1].type._tag, "Declaration") + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/SchemaAST/guards.test.ts b/repos/effect/packages/effect/test/Schema/SchemaAST/guards.test.ts new file mode 100644 index 0000000..b21692c --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/SchemaAST/guards.test.ts @@ -0,0 +1,100 @@ +import { describe, it } from "@effect/vitest" +import { assertFalse, assertTrue } from "@effect/vitest/utils" +import * as S from "effect/Schema" +import * as AST from "effect/SchemaAST" + +describe("guards", () => { + it("isDeclaration", () => { + assertTrue(AST.isDeclaration(S.OptionFromSelf(S.Number).ast)) + assertFalse(AST.isDeclaration(S.Number.ast)) + }) + + it("isTemplateLiteral", () => { + assertTrue(AST.isTemplateLiteral(S.TemplateLiteral(S.Literal("a"), S.String).ast)) + assertFalse(AST.isTemplateLiteral(S.Number.ast)) + }) + + it("isSuspend", () => { + type A = readonly [number, A | null] + const schema: S.Schema = S.suspend( // intended outer suspend + () => S.Tuple(S.Number, S.Union(schema, S.Literal(null))) + ) + assertTrue(AST.isSuspend(schema.ast)) + assertFalse(AST.isSuspend(S.Number.ast)) + }) + + it("isTransform", () => { + assertTrue(AST.isTransformation(S.Trim.ast)) + assertFalse(AST.isTransformation(S.Number.ast)) + }) + + it("isUndefinedKeyword", () => { + assertTrue(AST.isUndefinedKeyword(S.Undefined.ast)) + assertFalse(AST.isUndefinedKeyword(S.Number.ast)) + }) + + it("isVoidKeyword", () => { + assertTrue(AST.isVoidKeyword(S.Void.ast)) + assertFalse(AST.isVoidKeyword(S.Unknown.ast)) + }) + + it("isSymbolKeyword", () => { + assertTrue(AST.isSymbolKeyword(S.SymbolFromSelf.ast)) + assertFalse(AST.isSymbolKeyword(S.Unknown.ast)) + }) + + it("isObjectKeyword", () => { + assertTrue(AST.isObjectKeyword(S.Object.ast)) + assertFalse(AST.isObjectKeyword(S.Unknown.ast)) + }) + + it("isEnums", () => { + enum Fruits { + Apple, + Banana + } + assertTrue(AST.isEnums(S.Enums(Fruits).ast)) + assertFalse(AST.isEnums(S.Unknown.ast)) + }) + + it("isNeverKeyword", () => { + assertTrue(AST.isNeverKeyword(S.Never.ast)) + assertFalse(AST.isNeverKeyword(S.Unknown.ast)) + }) + + it("isUniqueSymbol", () => { + assertTrue(AST.isUniqueSymbol(S.UniqueSymbolFromSelf(Symbol.for("effect/Schema/test/a")).ast)) + assertFalse(AST.isUniqueSymbol(S.Unknown.ast)) + }) + + it("isUnknownKeyword", () => { + assertTrue(AST.isUnknownKeyword(S.Unknown.ast)) + assertFalse(AST.isUnknownKeyword(S.Any.ast)) + }) + + it("isAnyKeyword", () => { + assertTrue(AST.isAnyKeyword(S.Any.ast)) + assertFalse(AST.isAnyKeyword(S.Unknown.ast)) + }) + + it("isBooleanKeyword", () => { + assertTrue(AST.isBooleanKeyword(S.Boolean.ast)) + assertFalse(AST.isBooleanKeyword(S.Unknown.ast)) + }) + + it("isBigIntKeyword", () => { + assertTrue(AST.isBigIntKeyword(S.BigIntFromSelf.ast)) + assertFalse(AST.isBigIntKeyword(S.Unknown.ast)) + }) + + it("isParameter", () => { + assertTrue(AST.isParameter(AST.stringKeyword)) + assertTrue(AST.isParameter(AST.symbolKeyword)) + assertTrue(AST.isParameter(S.TemplateLiteral(S.String, S.Literal("-"), S.String).ast)) + assertTrue(AST.isParameter(S.String.pipe(S.minLength(2)).ast)) + assertTrue(AST.isParameter(S.TemplateLiteral(S.Literal("a", "b"), S.Literal("c")).ast)) + assertFalse(AST.isParameter(S.Number.pipe(S.int()).ast)) + assertFalse(AST.isParameter(S.NumberFromString.ast)) + assertFalse(AST.isParameter(S.NumberFromString.pipe(S.int()).ast)) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/SchemaAST/mutable.test.ts b/repos/effect/packages/effect/test/Schema/SchemaAST/mutable.test.ts new file mode 100644 index 0000000..38eb50b --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/SchemaAST/mutable.test.ts @@ -0,0 +1,39 @@ +import { describe, it } from "@effect/vitest" +import { strictEqual } from "@effect/vitest/utils" +import { identity } from "effect" +import * as S from "effect/Schema" +import * as AST from "effect/SchemaAST" + +const expectSameReference = (schema: S.Schema.Any) => { + const mutable = AST.mutable(AST.isSuspend(schema.ast) ? schema.ast.f() : schema.ast) + const mutable2 = AST.mutable(mutable) + strictEqual(mutable, mutable2) +} + +describe("mutable", () => { + it("tuple", () => { + expectSameReference(S.Tuple(S.String, S.Number)) + }) + + it("struct", () => { + expectSameReference(S.Struct({ a: S.String, b: S.Number })) + }) + + it("union", () => { + expectSameReference(S.Union(S.String, S.Number)) + }) + + it("suspend", () => { + expectSameReference(S.suspend(() => S.Struct({ a: S.String, b: S.Number }))) + }) + + it("refinement", () => { + expectSameReference(S.Array(S.String).pipe(S.maxItems(2))) + }) + + it("transformation", () => { + expectSameReference( + S.transform(S.Array(S.String), S.Array(S.String), { strict: true, decode: identity, encode: identity }) + ) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/SchemaAST/partial.test.ts b/repos/effect/packages/effect/test/Schema/SchemaAST/partial.test.ts new file mode 100644 index 0000000..c305d30 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/SchemaAST/partial.test.ts @@ -0,0 +1,134 @@ +import { describe, it } from "@effect/vitest" +import { deepStrictEqual } from "@effect/vitest/utils" +import * as S from "effect/Schema" +import * as AST from "effect/SchemaAST" + +describe("partial", () => { + describe("{ exact: false }", () => { + it("struct", () => { + // type A = { readonly a: string } + // type B = Partial + const schema = S.partial(S.Struct({ a: S.String })) + const expected = S.Struct({ a: S.optional(S.String) }) + deepStrictEqual(schema.ast, expected.ast) + }) + + describe("tuple", () => { + it("e", () => { + // type A = [string] + // type B = Partial + const tuple = new AST.TupleType( + [new AST.OptionalType(AST.stringKeyword, false)], + [], + true + ) + deepStrictEqual( + AST.partial(tuple), + new AST.TupleType([new AST.OptionalType(AST.orUndefined(AST.stringKeyword), true)], [], true) + ) + }) + + it("e + r", () => { + // type A = readonly [string, ...Array] + // type B = Partial + const tuple = new AST.TupleType( + [new AST.OptionalType(AST.stringKeyword, false)], + [new AST.Type(AST.numberKeyword)], + true + ) + deepStrictEqual( + AST.partial(tuple), + new AST.TupleType( + [new AST.OptionalType(AST.orUndefined(AST.stringKeyword), true)], + [new AST.Type(AST.orUndefined(AST.numberKeyword))], + true + ) + ) + }) + + it("e + r + e", () => { + // type A = readonly [string, ...Array, boolean] + // type B = Partial + const tuple = new AST.TupleType( + [new AST.OptionalType(AST.stringKeyword, false)], + [new AST.Type(AST.numberKeyword), new AST.Type(AST.booleanKeyword)], + true + ) + deepStrictEqual( + AST.partial(tuple), + new AST.TupleType( + [new AST.OptionalType(AST.orUndefined(AST.stringKeyword), true)], + [ + new AST.Type(AST.Union.make([AST.numberKeyword, AST.booleanKeyword, AST.undefinedKeyword])) + ], + true + ) + ) + }) + }) + }) + + describe("{ exact: true }", () => { + it("struct", () => { + // type A = { readonly a: string } + // type B = Partial + const schema = S.partialWith(S.Struct({ a: S.String }), { exact: true }) + const expected = S.Struct({ a: S.optionalWith(S.String, { exact: true }) }) + deepStrictEqual(schema.ast, expected.ast) + }) + + describe("tuple", () => { + it("e", () => { + // type A = [string] + // type B = Partial + const tuple = new AST.TupleType( + [new AST.OptionalType(AST.stringKeyword, false)], + [], + true + ) + deepStrictEqual( + AST.partial(tuple, { exact: true }), + new AST.TupleType([new AST.OptionalType(AST.stringKeyword, true)], [], true) + ) + }) + + it("e + r", () => { + // type A = readonly [string, ...Array] + // type B = Partial + const tuple = new AST.TupleType( + [new AST.OptionalType(AST.stringKeyword, false)], + [new AST.Type(AST.numberKeyword)], + true + ) + deepStrictEqual( + AST.partial(tuple, { exact: true }), + new AST.TupleType( + [new AST.OptionalType(AST.stringKeyword, true)], + [new AST.Type(AST.orUndefined(AST.numberKeyword))], + true + ) + ) + }) + + it("e + r + e", () => { + // type A = readonly [string, ...Array, boolean] + // type B = Partial + const tuple = new AST.TupleType( + [new AST.OptionalType(AST.stringKeyword, false)], + [new AST.Type(AST.numberKeyword), new AST.Type(AST.booleanKeyword)], + true + ) + deepStrictEqual( + AST.partial(tuple, { exact: true }), + new AST.TupleType( + [new AST.OptionalType(AST.stringKeyword, true)], + [ + new AST.Type(AST.Union.make([AST.numberKeyword, AST.booleanKeyword, AST.undefinedKeyword])) + ], + true + ) + ) + }) + }) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/SchemaAST/record.test.ts b/repos/effect/packages/effect/test/Schema/SchemaAST/record.test.ts new file mode 100644 index 0000000..95213d7 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/SchemaAST/record.test.ts @@ -0,0 +1,27 @@ +import { describe, it } from "@effect/vitest" +import { deepStrictEqual, throws } from "@effect/vitest/utils" +import * as AST from "effect/SchemaAST" + +describe("record", () => { + it("should throw on unsupported keys", () => { + throws( + () => AST.record(AST.undefinedKeyword, AST.numberKeyword), + new Error(`Unsupported key schema +schema (UndefinedKeyword): undefined`) + ) + }) + + it("should throw on unsupported literals", () => { + throws( + () => AST.record(new AST.Literal(true), AST.numberKeyword), + new Error(`Unsupported literal +details: literal value: true`) + ) + }) + + it("should support numeric literals as keys", () => { + deepStrictEqual(AST.record(new AST.Literal(1), AST.numberKeyword).propertySignatures, [ + new AST.PropertySignature(1, AST.numberKeyword, false, true) + ]) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/SchemaAST/suspend.test.ts b/repos/effect/packages/effect/test/Schema/SchemaAST/suspend.test.ts new file mode 100644 index 0000000..e86b2e3 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/SchemaAST/suspend.test.ts @@ -0,0 +1,34 @@ +import { describe, it } from "@effect/vitest" +import { assertTrue, strictEqual } from "@effect/vitest/utils" +import * as S from "effect/Schema" +import type * as AST from "effect/SchemaAST" +import * as Util from "../TestUtils.js" + +describe("AST.Suspend", () => { + it("should memoize the thunk", async () => { + let log = 0 + interface A { + readonly a: string + readonly as: ReadonlyArray + } + const schema = S.Struct({ + a: S.String, + as: S.Array(S.suspend((): S.Schema => { + log++ + return schema + })) + }) + await Util.assertions.decoding.succeed(schema, { a: "a1", as: [] }) + await Util.assertions.decoding.succeed(schema, { a: "a1", as: [{ a: "a2", as: [] }] }) + strictEqual(log, 1) + }) + + it("should memoize the AST", () => { + type A = readonly [number, A | null] + const schema: S.Schema = S.suspend( // intended outer suspend + () => S.Tuple(S.Number, S.Union(schema, S.Literal(null))) + ) + const ast = schema.ast as AST.Suspend + assertTrue(ast.f() === ast.f()) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/SchemaAST/typeAST.test.ts b/repos/effect/packages/effect/test/Schema/SchemaAST/typeAST.test.ts new file mode 100644 index 0000000..bad1164 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/SchemaAST/typeAST.test.ts @@ -0,0 +1,121 @@ +import { describe, it } from "@effect/vitest" +import { assertFalse, assertTrue, deepStrictEqual } from "@effect/vitest/utils" +import { Effect, Schema as S, SchemaAST as AST } from "effect" + +describe("typeAST", () => { + describe(`should return the same reference if the AST doesn't represent a transformation`, () => { + it("declaration (true)", () => { + const schema = S.OptionFromSelf(S.String) + assertTrue(AST.typeAST(schema.ast) === schema.ast) + }) + + it("declaration (false)", () => { + const schema = S.OptionFromSelf(S.NumberFromString) + assertFalse(AST.typeAST(schema.ast) === schema.ast) + }) + + it("tuple (true)", () => { + const schema = S.Tuple(S.String, S.Number) + assertTrue(AST.typeAST(schema.ast) === schema.ast) + }) + + it("tuple (false)", () => { + const schema = S.Tuple(S.String, S.NumberFromString) + assertFalse(AST.typeAST(schema.ast) === schema.ast) + }) + + it("array (true)", () => { + const schema = S.Array(S.Number) + assertTrue(AST.typeAST(schema.ast) === schema.ast) + }) + + it("array (false)", () => { + const schema = S.Array(S.NumberFromString) + assertFalse(AST.typeAST(schema.ast) === schema.ast) + }) + + it("union (true)", () => { + const schema = S.Union(S.String, S.Number) + assertTrue(AST.typeAST(schema.ast) === schema.ast) + }) + + it("union (false)", () => { + const schema = S.Union(S.String, S.NumberFromString) + assertFalse(AST.typeAST(schema.ast) === schema.ast) + }) + + it("struct (true)", () => { + const schema = S.Struct({ a: S.String, b: S.Number }) + assertTrue(AST.typeAST(schema.ast) === schema.ast) + }) + + it("struct (false)", () => { + const schema = S.Struct({ a: S.String, b: S.NumberFromString }) + assertFalse(AST.typeAST(schema.ast) === schema.ast) + }) + + it("record (true)", () => { + const schema = S.Record({ key: S.String, value: S.Number }) + assertTrue(AST.typeAST(schema.ast) === schema.ast) + }) + + it("record (false)", () => { + const schema = S.Record({ key: S.String, value: S.NumberFromString }) + assertFalse(AST.typeAST(schema.ast) === schema.ast) + }) + + it("refinement (true)", () => { + const schema = S.Number.pipe(S.filter((n) => n > 0)) + assertTrue(AST.typeAST(schema.ast) === schema.ast) + }) + + it("refinement (false)", () => { + const schema = S.NumberFromString.pipe(S.filter((n) => n > 0)) + assertFalse(AST.typeAST(schema.ast) === schema.ast) + }) + }) + + describe("Transformation", () => { + it("should preserve whitelisted annotations", () => { + const annotations: S.Annotations.GenericSchema = { + title: "title", + description: "description", + documentation: "documentation", + identifier: "id", + message: () => "message", + schemaId: "schemaId", + concurrency: 6, + batching: true, + parseIssueTitle: () => "parseIssueTitle", + parseOptions: { onExcessProperty: "error" }, + decodingFallback: () => Effect.succeed(7), + // whitelisted annotations + examples: [1, 2, 3], + default: 4, + jsonSchema: { type: "object" }, + arbitrary: () => (fc) => fc.constant(5), + pretty: () => () => "pretty", + equivalence: () => () => true + } + const schema = S.transform( + S.Number, + S.Number.annotations({ + title: "original-title", + description: "original-description" + }), + { decode: (n) => n, encode: (n) => n } + ).annotations(annotations) + deepStrictEqual(AST.typeAST(schema.ast).annotations, { + [AST.TitleAnnotationId]: "original-title", + [AST.DescriptionAnnotationId]: "original-description", + // whitelisted annotations + [AST.ExamplesAnnotationId]: annotations.examples, + [AST.DefaultAnnotationId]: annotations.default, + [AST.JSONSchemaAnnotationId]: annotations.jsonSchema, + [AST.ArbitraryAnnotationId]: annotations.arbitrary, + [AST.PrettyAnnotationId]: annotations.pretty, + [AST.EquivalenceAnnotationId]: annotations.equivalence + }) + }) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/SchemaAST/unify.test.ts b/repos/effect/packages/effect/test/Schema/SchemaAST/unify.test.ts new file mode 100644 index 0000000..532f8b7 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/SchemaAST/unify.test.ts @@ -0,0 +1,57 @@ +import { describe, it } from "@effect/vitest" +import { deepStrictEqual } from "@effect/vitest/utils" +import * as S from "effect/Schema" +import * as AST from "effect/SchemaAST" + +const expectUnify = (input: Array, expected: Array) => { + const actual = AST.unify(input.map((schema) => schema.ast)) + deepStrictEqual(actual, expected.map((e) => e.ast)) +} + +describe("AST.unify", () => { + it("should unify", () => { + expectUnify([], []) + + expectUnify([S.Any, S.String], [S.Any]) + expectUnify([S.Any, S.Unknown], [S.Any]) + expectUnify([S.Literal("a"), S.Any], [S.Any]) + + expectUnify([S.Unknown, S.String], [S.Unknown]) + expectUnify([S.Unknown, S.Literal("a")], [S.Unknown]) + + expectUnify([S.Object, S.Object], [S.Object]) + expectUnify([S.Object, S.Struct({ a: S.String })], [S.Object]) + expectUnify([S.Object, S.Tuple(S.String)], [S.Object]) + expectUnify([S.Object, S.String], [S.Object, S.String]) + + expectUnify([S.String, S.String], [S.String]) + expectUnify([S.String, S.Number], [S.String, S.Number]) + + expectUnify([S.Literal("a"), S.Literal("a")], [S.Literal("a")]) + expectUnify([S.Literal("a"), S.Literal("b")], [S.Literal("a"), S.Literal("b")]) + expectUnify([S.Literal("a"), S.String], [S.String]) + expectUnify([S.String, S.Literal("a")], [S.String]) + expectUnify([S.Literal("a"), S.Literal("b"), S.String], [S.String]) + expectUnify([S.Literal("a"), S.String, S.Literal("b")], [S.String]) + + expectUnify([S.Literal(1), S.Literal(1)], [S.Literal(1)]) + expectUnify([S.Literal(1), S.Literal(2)], [S.Literal(1), S.Literal(2)]) + expectUnify([S.Literal(1), S.Number], [S.Number]) + + expectUnify([S.Literal(true), S.Literal(true)], [S.Literal(true)]) + expectUnify([S.Literal(true), S.Literal(false)], [S.Literal(true), S.Literal(false)]) + expectUnify([S.Literal(true), S.Boolean], [S.Boolean]) + + expectUnify([S.Literal(1n), S.Literal(1n)], [S.Literal(1n)]) + expectUnify([S.Literal(1n), S.Literal(2n)], [S.Literal(1n), S.Literal(2n)]) + expectUnify([S.Literal(1n), S.BigIntFromSelf], [S.BigIntFromSelf]) + + expectUnify([S.UniqueSymbolFromSelf(Symbol.for("a")), S.UniqueSymbolFromSelf(Symbol.for("a"))], [ + S.UniqueSymbolFromSelf(Symbol.for("a")) + ]) + expectUnify([S.UniqueSymbolFromSelf(Symbol.for("a")), S.SymbolFromSelf], [S.SymbolFromSelf]) + + expectUnify([S.Struct({}), S.Struct({})], [S.Struct({})]) + expectUnify([S.Object, S.Struct({})], [S.Object, S.Struct({})]) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/SchemaTest.ts b/repos/effect/packages/effect/test/Schema/SchemaTest.ts new file mode 100644 index 0000000..ad63094 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/SchemaTest.ts @@ -0,0 +1,359 @@ +import type { SchemaAST } from "effect" +import { + Arbitrary, + Cause, + Context, + Effect, + Either, + FastCheck, + ParseResult, + Predicate, + Pretty, + Runtime, + Schema +} from "effect" + +// Defines parameters for FastCheck that exclude typed properties +export type UntypedParameters = Omit, "examples" | "reporter" | "asyncReporter"> + +// Configuration context for assertion behaviors +export class AssertConfig extends Context.Tag("AssertConfig")() {} + +// Provides assertion utilities for testing +export class Assert extends Context.Tag("Assert") void + readonly strictEqual: (actual: unknown, expected: unknown, message?: string) => void + readonly throws: (thunk: () => void, error?: Error | ((u: unknown) => undefined)) => void + readonly fail: (message: string) => void +}>() {} + +// Provides various assertions for Schema testing +export const assertions = Effect.gen(function*() { + const { deepStrictEqual, fail, strictEqual, throws } = yield* Assert + const config = yield* AssertConfig + + function assertInstanceOf any>( + value: unknown, + constructor: C, + message?: string, + ..._: Array + ): asserts value is InstanceType { + if (!(value instanceof constructor)) { + fail(message ?? `expected ${value} to be an instance of ${constructor}`) + } + } + + const out = { + ast: { + equals: (a: Schema.Schema, b: Schema.Schema) => { + deepStrictEqual(a.ast, b.ast) + } + }, + make: { + /** + * Ensures that the given constructor produces the expected value. + */ + succeed( + // Destructure to verify that "this" type is bound + { make }: { readonly make: (a: A) => B }, + input: A, + expected?: B + ) { + deepStrictEqual(make(input), expected ?? input) + }, + + /** + * Ensures that the given constructor throws the expected error. + */ + fail( + // Destructure to verify that "this" type is bound + { make }: { readonly make: (a: A) => B }, + input: A, + message: string + ) { + out.parseError(() => make(input), message) + } + }, + + arbitrary: { + /** + * Verifies that the schema generates valid arbitrary values that satisfy + * the schema. + */ + validateGeneratedValues(schema: Schema.Schema, options?: { + readonly params?: FastCheck.Parameters<[A]> | undefined + }) { + if (config.arbitrary?.validateGeneratedValues?.skip === true) { + return + } + const params = Predicate.isObject(config.arbitrary?.validateGeneratedValues) + ? { ...config.arbitrary?.validateGeneratedValues?.params, ...options?.params } + : options?.params + const is = Schema.is(schema) + const arb = Arbitrary.make(schema) + FastCheck.assert(FastCheck.property(arb, (a) => is(a)), params) + } + }, + + /** + * Verifies that the schema satisfies the roundtrip law: `decode(encode(a))` + * is equal to `a`. + */ + testRoundtripConsistency(schema: Schema.Schema, options?: { + readonly ignoreEncodingErrors?: ((issue: ParseResult.ParseIssue) => boolean) | undefined + readonly params?: FastCheck.Parameters<[A]> | undefined + }) { + if (config.testRoundtripConsistency?.skip === true) { + return + } + const params = Predicate.isObject(config.testRoundtripConsistency?.params) + ? { ...config.testRoundtripConsistency?.params, ...options?.params } + : options?.params + const arb = Arbitrary.make(schema) + const is = Schema.is(schema) + const encode = ParseResult.encode(schema) + const decode = ParseResult.decode(schema) + FastCheck.assert( + FastCheck.property(arb, (a) => { + const roundtrip = encode(a).pipe( + Effect.mapError((issue) => ["encoding", issue] as const), + Effect.flatMap((i) => decode(i).pipe(Effect.mapError((issue) => ["decoding", issue] as const))), + Effect.either, + Effect.runSync + ) + if (Either.isLeft(roundtrip)) { + if (roundtrip.left[0] === "encoding" && options?.ignoreEncodingErrors) { + return options.ignoreEncodingErrors(roundtrip.left[1]) + } + return false + } + return is(roundtrip.right) + }), + params + ) + }, + + decoding: { + /** + * Attempts to decode the given input using the provided schema. If the + * decoding is successful, the decoded value is compared to the expected + * value. Otherwise the test fails. + */ + async succeed( + schema: Schema.Schema, + input: unknown, + expected?: A, + options?: { + readonly parseOptions?: SchemaAST.ParseOptions | undefined + } | undefined + ) { + const decoded = ParseResult.decodeUnknown(schema)(input, options?.parseOptions) + return out.effect.succeed( + decoded, + arguments.length >= 3 ? // Account for `expected` being `undefined` + expected : + expected ?? input + ) + }, + + /** + * Attempts to decode the given input using the provided schema. If the + * decoding fails, the error message is compared to the expected message. + * Otherwise the test fails. + */ + async fail( + schema: Schema.Schema, + input: unknown, + message: string, + options?: { + readonly parseOptions?: SchemaAST.ParseOptions | undefined + } | undefined + ) { + const decoded = ParseResult.decodeUnknown(schema)(input, options?.parseOptions) + return out.effect.fail(decoded, message) + } + }, + + encoding: { + /** + * Attempts to encode the given input using the provided schema. If the + * decoding is successful, the decoded value is compared to the expected + * value. Otherwise the test fails. + */ + async succeed( + schema: Schema.Schema, + input: A, + expected?: I, + options?: { + readonly parseOptions?: SchemaAST.ParseOptions | undefined + } | undefined + ) { + const encoded = ParseResult.encodeUnknown(schema)(input, options?.parseOptions) + return out.effect.succeed( + encoded, + arguments.length >= 3 ? // Account for `expected` being `undefined` + expected : + expected ?? input + ) + }, + + /** + * Attempts to encode the given input using the provided schema. If the + * decoding fails, the error message is compared to the expected message. + * Otherwise the test fails. + */ + async fail( + schema: Schema.Schema, + input: A, + message: string, + options?: { + readonly parseOptions?: SchemaAST.ParseOptions | undefined + } | undefined + ) { + const encoded = ParseResult.encodeUnknown(schema)(input, options?.parseOptions) + return out.effect.fail(encoded, message) + } + }, + + promise: { + /** + * Ensures that the given promise rejects with a Fiber Failure containing the expected message. + * + * Useful to test `decodePromise` and `encodePromise`. + */ + async fail(promise: Promise, message: string) { + try { + const a = await promise + throw new Error(`Promise didn't reject, got: ${a}`) + } catch (e: unknown) { + if (Runtime.isFiberFailure(e) && Cause.isCause(e[Runtime.FiberFailureCauseId])) { + const cause = e[Runtime.FiberFailureCauseId] + if (Cause.isFailType(cause) && Predicate.hasProperty(cause.error, "message")) { + return deepStrictEqual(cause.error.message, message) + } + } + throw new Error(`Unknown promise rejection: ${e}`) + } + } + }, + + effect: { + /** + * Verifies that the effect succeeds with the expected value. + */ + async succeed( + effect: Effect.Effect, + a: A + ) { + deepStrictEqual(await Effect.runPromise(Effect.either(effect)), Either.right(a)) + }, + + /** + * Verifies that the effect fails with the expected message. + */ + async fail( + effect: Effect.Effect, + message: string + ) { + const effectWithMessage = Effect.gen(function*() { + const decoded = yield* Effect.either(effect) + if (Either.isLeft(decoded)) { + const message = yield* ParseResult.TreeFormatter.formatIssue(decoded.left) + return yield* Effect.fail(message) + } + return decoded.right + }) + const result = await Effect.runPromise(Effect.either(effectWithMessage)) + return out.either.left(result, message) + } + }, + + either: { + /** + * Verifies that the either is a `Right` with the expected value. + */ + right(either: Either.Either, right: R) { + if (Either.isRight(either)) { + deepStrictEqual(either.right, right) + } else { + // eslint-disable-next-line no-console + console.log(either.left) + fail(`expected a Right, got a Left: ${either.left}`) + } + }, + + /** + * Verifies that the either is a `Left` with the expected value. + */ + left(either: Either.Either, left: L) { + if (Either.isLeft(either)) { + deepStrictEqual(either.left, left) + } else { + // eslint-disable-next-line no-console + console.log(either.right) + fail(`expected a Left, got a Right: ${either.right}`) + } + }, + + /** + * Verifies that the either is a left with the expected value. + */ + async fail(either: Either.Either, message: string) { + const eitherWithMessage = Effect.gen(function*() { + const encoded = yield* Effect.either(either) + if (Either.isLeft(encoded)) { + const message = yield* ParseResult.TreeFormatter.formatIssue(encoded.left) + return yield* Effect.fail(message) + } + return encoded.right + }) + const result = await Effect.runPromise(Effect.either(eitherWithMessage)) + return out.either.left(result, message) + } + }, + + asserts: { + succeed(schema: Schema.Schema, input: unknown, options?: { + readonly parseOptions?: SchemaAST.ParseOptions | undefined + }) { + deepStrictEqual(Schema.asserts(schema, options?.parseOptions)(input), undefined) + }, + + fail( + schema: Schema.Schema, + input: unknown, + message: string, + options?: { + readonly parseOptions?: SchemaAST.ParseOptions | undefined + } + ) { + out.parseError(() => Schema.asserts(schema, options?.parseOptions)(input), message) + } + }, + + parseError(f: () => void, message: string) { + throws(f, (err) => { + assertInstanceOf(err, ParseResult.ParseError) + strictEqual(err.message, message) + }) + }, + + pretty(schema: Schema.Schema, a: A, expected: string) { + const pretty = Pretty.make(schema) + strictEqual(pretty(a), expected) + } + } + + return out +}) diff --git a/repos/effect/packages/effect/test/Schema/SchemaUserland.test.ts b/repos/effect/packages/effect/test/Schema/SchemaUserland.test.ts new file mode 100644 index 0000000..307d279 --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/SchemaUserland.test.ts @@ -0,0 +1,75 @@ +/** + * It contains a collection of user-defined APIs to keep track of what might break in the event of breaking changes. + */ +import { describe, it } from "@effect/vitest" +import { deepStrictEqual, strictEqual } from "@effect/vitest/utils" +import { Record, Schema, SchemaAST as AST } from "effect" +import * as Util from "./TestUtils.js" + +const structTypeSchema = ( + schema: Schema.Struct +): Schema.Struct<{ [K in keyof Fields]: Schema.Schema> }> => + Schema.Struct(Record.map(schema.fields, (field) => { + switch (field.ast._tag) { + case "PropertySignatureDeclaration": + return Schema.make(AST.typeAST(field.ast.type)) + case "PropertySignatureTransformation": + return Schema.make(AST.typeAST(field.ast.to.type)) + default: + return Schema.make(AST.typeAST(field.ast)) + } + })) as any + +describe("SchemaUserland", () => { + it("structTypeSchema", () => { + // Discord: https://discordapp.com/channels/795981131316985866/847382157861060618/1266533881788502096 + // goal: `Schema.typeSchema` for structs, retaining the type + + // v-- this must be a struct + const schema = structTypeSchema(Schema.Struct({ + a: Schema.NumberFromString, + b: Schema.propertySignature(Schema.NumberFromString), + c: Schema.optionalWith(Schema.NumberFromString, { as: "Option" }) + })) + deepStrictEqual(schema.fields.a.ast, Schema.Number.ast) + deepStrictEqual(schema.fields.b.ast, Schema.Number.ast) + const c = schema.fields.c.ast + strictEqual(c._tag, "Declaration") + deepStrictEqual((c as AST.Declaration).typeParameters, [Schema.Number.ast]) + }) + + it("detect that a struct does not contain a specific field", async () => { + // Discord: https://discordapp.com/channels/795981131316985866/847382157861060618/1268175268019830906 + class A extends Schema.Class("A")( + Schema.Struct({ + a: Schema.String, + b: Schema.propertySignature( + Schema.Array(Schema.Struct({ + d: Schema.String + })).annotations({ parseOptions: { onExcessProperty: "ignore" } }) + ).pipe(Schema.fromKey("c")) + }).annotations({ + parseOptions: { onExcessProperty: "error" } + }) + ) { + readonly _tag = "A" + } + await Util.assertions.decoding.succeed(A, { a: "a", c: [{ d: "d" }] }, new A({ a: "a", b: [{ d: "d" }] })) + await Util.assertions.decoding.succeed( + A, + { a: "a", c: [{ d: "d", ignored: null }] }, + new A({ a: "a", b: [{ d: "d" }] }) + ) + await Util.assertions.decoding.fail( + A, + { a: "a", c: [{ d: "d" }], not_allowed: null }, + `(A (Encoded side) <-> A) +└─ Encoded side transformation failure + └─ A (Encoded side) + └─ Encoded side transformation failure + └─ Struct (Encoded side) + └─ ["not_allowed"] + └─ is unexpected, expected: "a" | "c"` + ) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/Serializable.test.ts b/repos/effect/packages/effect/test/Schema/Serializable.test.ts new file mode 100644 index 0000000..46505ef --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/Serializable.test.ts @@ -0,0 +1,118 @@ +import { describe, test } from "@effect/vitest" +import { deepStrictEqual } from "@effect/vitest/utils" +import { Effect, Exit } from "effect" +import * as S from "effect/Schema" + +class Person extends S.Class("Person")({ + id: S.Number, + name: S.String +}) {} + +class GetPersonById extends S.Class("GetPersonById")({ + id: S.Number +}) { + get [S.symbolSerializable]() { + return GetPersonById + } + get [S.symbolWithResult]() { + return { + success: Person, + failure: S.String, + defect: S.Defect + } + } +} + +describe("Serializable", () => { + test("serialize", () => { + const req = new GetPersonById({ id: 123 }) + deepStrictEqual(Effect.runSync(S.serialize(req)), { + id: 123 + }) + }) + + test("deserialize", () => { + const req = new GetPersonById({ id: 123 }) + deepStrictEqual( + Effect.runSync(S.deserialize(req, { + id: 456 + })), + new GetPersonById({ id: 456 }) + ) + }) + + test("serializeFailure", () => { + const req = new GetPersonById({ id: 123 }) + deepStrictEqual( + Effect.runSync( + S.serializeFailure(req, "fail") + ), + "fail" + ) + }) + + test("serializeSuccess", () => { + const req = new GetPersonById({ id: 123 }) + deepStrictEqual( + Effect.runSync( + S.serializeSuccess(req, new Person({ id: 123, name: "foo" })) + ), + { id: 123, name: "foo" } + ) + }) + + test("serializeExit", () => { + const req = new GetPersonById({ id: 123 }) + deepStrictEqual( + Effect.runSync( + S.serializeExit(req, Exit.succeed(new Person({ id: 123, name: "foo" }))) + ), + { _tag: "Success", value: { id: 123, name: "foo" } } + ) + deepStrictEqual( + Effect.runSync( + S.serializeExit(req, Exit.fail("fail")) + ), + { _tag: "Failure", cause: { _tag: "Fail", error: "fail" } } + ) + }) + + test("deserializeFailure", () => { + const req = new GetPersonById({ id: 123 }) + deepStrictEqual( + Effect.runSync( + S.deserializeFailure(req, "fail") + ), + "fail" + ) + }) + + test("deserializeSuccess", () => { + const req = new GetPersonById({ id: 123 }) + deepStrictEqual( + Effect.runSync( + S.deserializeSuccess(req, { id: 123, name: "foo" }) + ), + new Person({ id: 123, name: "foo" }) + ) + }) + + test("deserializeExit", () => { + const req = new GetPersonById({ id: 123 }) + deepStrictEqual( + Effect.runSync( + S.deserializeExit(req, { _tag: "Success", value: { id: 123, name: "foo" } }) + ), + Exit.succeed(new Person({ id: 123, name: "foo" })) + ) + deepStrictEqual( + Effect.runSync( + S.deserializeExit(req, { + _tag: "Failure", + cause: { _tag: "Fail", error: "fail" } + }) + ), + Exit.fail("fail") + ) + }) +}) diff --git a/repos/effect/packages/effect/test/Schema/TestUtils.ts b/repos/effect/packages/effect/test/Schema/TestUtils.ts new file mode 100644 index 0000000..d7c2bfb --- /dev/null +++ b/repos/effect/packages/effect/test/Schema/TestUtils.ts @@ -0,0 +1,150 @@ +import { deepStrictEqual, fail, strictEqual, throws } from "@effect/vitest/utils" +import { Context, Effect, ParseResult, Schema as S, SchemaAST as AST } from "effect" +import * as SchemaTest from "./SchemaTest.js" + +export const assertions = Effect.runSync( + SchemaTest.assertions.pipe( + Effect.provideService(SchemaTest.Assert, { + deepStrictEqual, + strictEqual, + throws, + fail + }), + Effect.provideService(SchemaTest.AssertConfig, { + arbitrary: { + validateGeneratedValues: { + skip: false + } + }, + testRoundtripConsistency: { + skip: false + } + }) + ) +) + +export const onExcessPropertyError: AST.ParseOptions = { + onExcessProperty: "error" +} + +export const onExcessPropertyPreserve: AST.ParseOptions = { + onExcessProperty: "preserve" +} + +export const ErrorsAll: AST.ParseOptions = { + errors: "all" +} + +export const NumberFromChar = S.Char.pipe(S.compose(S.NumberFromString)).annotations({ + identifier: "NumberFromChar" +}) + +export const AsyncDeclaration = S.declare( + [], + { + decode: () => (u) => Effect.andThen(Effect.sleep("10 millis"), Effect.succeed(u)), + encode: () => (u) => Effect.andThen(Effect.sleep("10 millis"), Effect.succeed(u)) + }, + { + identifier: "AsyncDeclaration" + } +) + +export const AsyncStringWithoutIdentifier = effectify(S.String) +export const AsyncString = effectify(S.String).annotations({ identifier: "AsyncString" }) + +const Name = Context.GenericTag<"Name", string>("Name") + +export const DependencyString = S.transformOrFail( + S.String, + S.String, + { strict: true, decode: (s) => Effect.andThen(Name, s), encode: (s) => Effect.andThen(Name, s) } +).annotations({ identifier: "DependencyString" }) + +export const expectFields = (f1: S.Struct.Fields, f2: S.Struct.Fields) => { + const ks1 = Reflect.ownKeys(f1).sort().map((k) => [k, f1[k].ast.toString()]) + const ks2 = Reflect.ownKeys(f2).sort().map((k) => [k, f2[k].ast.toString()]) + deepStrictEqual(ks1, ks2) +} + +export const Defect = S.transform(S.String, S.Object, { + strict: true, + decode: (s) => ({ input: s }), + encode: (u) => JSON.stringify(u) +}) + +function effectifyDecode( + decode: ( + fromA: any, + options: AST.ParseOptions, + self: AST.Transformation, + fromI: any + ) => Effect.Effect +): ( + fromA: any, + options: AST.ParseOptions, + self: AST.Transformation, + fromI: any +) => Effect.Effect { + return (fromA, options, ast, fromI) => + ParseResult.flatMap(Effect.sleep("10 millis"), () => decode(fromA, options, ast, fromI)) +} + +function effectifyAST(ast: AST.AST): AST.AST { + switch (ast._tag) { + case "TupleType": + return new AST.TupleType( + ast.elements.map((e) => new AST.OptionalType(effectifyAST(e.type), e.isOptional, e.annotations)), + ast.rest.map((annotatedAST) => new AST.Type(effectifyAST(annotatedAST.type), annotatedAST.annotations)), + ast.isReadonly, + ast.annotations + ) + case "TypeLiteral": + return new AST.TypeLiteral( + ast.propertySignatures.map((p) => + new AST.PropertySignature(p.name, effectifyAST(p.type), p.isOptional, p.isReadonly, p.annotations) + ), + ast.indexSignatures.map((is) => { + return new AST.IndexSignature(is.parameter, effectifyAST(is.type), is.isReadonly) + }), + ast.annotations + ) + case "Union": + return AST.Union.make(ast.types.map((ast) => effectifyAST(ast)), ast.annotations) + case "Suspend": + return new AST.Suspend(() => effectifyAST(ast.f()), ast.annotations) + case "Refinement": + return new AST.Refinement( + effectifyAST(ast.from), + ast.filter, + ast.annotations + ) + case "Transformation": + return new AST.Transformation( + effectifyAST(ast.from), + effectifyAST(ast.to), + new AST.FinalTransformation( + effectifyDecode(ParseResult.getFinalTransformation(ast.transformation, true)), + effectifyDecode(ParseResult.getFinalTransformation(ast.transformation, false)) + ), + ast.annotations + ) + } + const schema = S.make(ast) + const decode = S.decode(schema) + const encode = S.encode(schema) + return new AST.Transformation( + AST.encodedAST(ast), + AST.typeAST(ast), + new AST.FinalTransformation( + (a, options) => + Effect.flatMap(Effect.sleep("10 millis"), () => ParseResult.mapError(decode(a, options), (e) => e.issue)), + (a, options) => + Effect.flatMap(Effect.sleep("10 millis"), () => ParseResult.mapError(encode(a, options), (e) => e.issue)) + ) + ) +} + +function effectify(schema: S.Schema): S.Schema { + return S.make(effectifyAST(schema.ast)) +} diff --git a/repos/effect/packages/effect/test/Scope.test.ts b/repos/effect/packages/effect/test/Scope.test.ts new file mode 100644 index 0000000..952fcd4 --- /dev/null +++ b/repos/effect/packages/effect/test/Scope.test.ts @@ -0,0 +1,147 @@ +import { describe, it } from "@effect/vitest" +import { assertTrue, deepStrictEqual, strictEqual } from "@effect/vitest/utils" +import { Deferred, Effect, identity, pipe, Ref, Scope } from "effect" + +type Action = Acquire | Use | Release + +const OP_ACQUIRE = 0 as const +type OP_ACQUIRE = typeof OP_ACQUIRE + +const OP_USE = 1 as const +type OP_USE = typeof OP_USE + +const OP_RELEASE = 2 as const +type OP_RELEASE = typeof OP_RELEASE + +interface Acquire { + readonly op: OP_ACQUIRE + readonly id: number +} + +interface Use { + readonly op: OP_USE + readonly id: number +} + +interface Release { + readonly op: OP_RELEASE + readonly id: number +} + +const acquire = (id: number): Action => ({ op: OP_ACQUIRE, id }) +const use = (id: number): Action => ({ op: OP_USE, id }) +const release = (id: number): Action => ({ op: OP_RELEASE, id }) +const isAcquire = (self: Action): self is Use => self.op === OP_ACQUIRE +const isUse = (self: Action): self is Use => self.op === OP_USE +const isRelease = (self: Action): self is Use => self.op === OP_RELEASE + +const resource = (id: number, ref: Ref.Ref>): Effect.Effect => { + return pipe( + Ref.update(ref, (actions) => [...actions, acquire(id)]), + Effect.as(id), + Effect.uninterruptible, + Effect.ensuring( + Effect.scopeWith((scope) => scope.addFinalizer(() => Ref.update(ref, (actions) => [...actions, release(id)]))) + ) + ) +} + +describe("Scope", () => { + it.effect("runs finalizers when the scope is closed", () => + Effect.gen(function*() { + const ref = yield* Ref.make>([]) + yield* Effect.scoped(pipe( + resource(1, ref), + Effect.flatMap((id) => Ref.update(ref, (actions) => [...actions, use(id)])) + )) + const result = yield* Ref.get(ref) + deepStrictEqual(result, [acquire(1), use(1), release(1)]) + })) + it.effect("runs finalizers in parallel", () => + Effect.gen(function*() { + const deferred = yield* Deferred.make() + const result = yield* pipe( + Effect.addFinalizer(() => Deferred.succeed(deferred, void 0)), + Effect.zipRight(Effect.addFinalizer(() => Deferred.await(deferred)), { + concurrent: true, + concurrentFinalizers: true + }), + Effect.scoped, + Effect.asVoid + ) + strictEqual(result, undefined) + })) + it.effect("runs finalizers in parallel when the scope is closed", () => + Effect.gen(function*() { + const ref = yield* Ref.make>([]) + yield* Effect.scoped( + pipe( + Effect.parallelFinalizers(resource(1, ref)), + Effect.zip(resource(2, ref), { concurrent: true, concurrentFinalizers: true }), + Effect.flatMap(([resource1, resource2]) => + pipe( + Ref.update(ref, (actions) => [...actions, use(resource1)]), + Effect.zip(Ref.update(ref, (actions) => [...actions, use(resource2)]), { concurrent: true }) + ) + ) + ) + ) + const result = yield* Ref.get(ref) + assertTrue(result.slice(0, 2).some((action) => isAcquire(action) && action.id === 1)) + assertTrue(result.slice(0, 2).some((action) => isAcquire(action) && action.id === 2)) + assertTrue(result.slice(2, 4).some((action) => isUse(action) && action.id === 1)) + assertTrue(result.slice(2, 4).some((action) => isUse(action) && action.id === 2)) + assertTrue(result.slice(4, 6).some((action) => isRelease(action) && action.id === 1)) + assertTrue(result.slice(4, 6).some((action) => isRelease(action) && action.id === 2)) + })) + it.effect("preserves order of nested sequential finalizers", () => + Effect.gen(function*() { + const ref = yield* Ref.make>([]) + const left = Effect.sequentialFinalizers(pipe(resource(1, ref), Effect.zipRight(resource(2, ref)))) + const right = Effect.sequentialFinalizers(pipe(resource(3, ref), Effect.zipRight(resource(4, ref)))) + yield* Effect.scoped(Effect.parallelFinalizers(pipe(left, Effect.zip(right, { concurrent: true })))) + const actions = yield* Ref.get(ref) + const action1Index = actions.findIndex((action) => action.op === OP_RELEASE && action.id === 1) + const action2Index = actions.findIndex((action) => action.op === OP_RELEASE && action.id === 2) + const action3Index = actions.findIndex((action) => action.op === OP_RELEASE && action.id === 3) + const action4Index = actions.findIndex((action) => action.op === OP_RELEASE && action.id === 4) + assertTrue(action2Index < action1Index) + assertTrue(action4Index < action3Index) + })) + it.scoped("withEarlyRelease", () => + Effect.gen(function*() { + const ref = yield* Ref.make>([]) + const left = resource(1, ref) + const right = Effect.withEarlyRelease(resource(2, ref)) + yield* pipe(left, Effect.zipRight(pipe(right, Effect.flatMap(([release, _]) => release)))) + const actions = yield* Ref.get(ref) + deepStrictEqual(actions[0], acquire(1)) + deepStrictEqual(actions[1], acquire(2)) + deepStrictEqual(actions[2], release(2)) + })) + it.effect("using", () => + Effect.gen(function*() { + const ref1 = yield* Ref.make>([]) + const ref2 = yield* Ref.make>([]) + yield* pipe( + resource(1, ref1), + Effect.using(() => + pipe(Ref.update(ref1, (actions) => [...actions, use(1)]), Effect.zipRight(resource(2, ref2))) + ), + Effect.zipRight(Ref.update(ref2, (actions) => [...actions, use(2)])), + Effect.scoped + ) + const actions1 = yield* Ref.get(ref1) + const actions2 = yield* Ref.get(ref2) + deepStrictEqual(actions1, [acquire(1), use(1), release(1)]) + deepStrictEqual(actions2, [acquire(2), use(2), release(2)]) + })) + it.effect( + ".pipe", + () => + Effect.gen(function*() { + const scope = yield* Scope.make() + strictEqual(scope.pipe(identity), scope) + }) + ) +}) diff --git a/repos/effect/packages/effect/test/ScopedCache.test.ts b/repos/effect/packages/effect/test/ScopedCache.test.ts new file mode 100644 index 0000000..052cdad --- /dev/null +++ b/repos/effect/packages/effect/test/ScopedCache.test.ts @@ -0,0 +1,863 @@ +import { describe, it } from "@effect/vitest" +import { + assertFalse, + assertLeft, + assertNone, + assertRight, + assertSome, + assertTrue, + deepStrictEqual, + strictEqual +} from "@effect/vitest/utils" +import { + Array, + Cause, + Chunk, + Context, + Duration, + Effect, + Exit, + FastCheck as fc, + Fiber, + Hash, + HashMap, + identity, + pipe, + Ref, + Schedule, + Scope, + ScopedCache, + TestClock, + TestServices +} from "effect" +import { dual } from "effect/Function" +import * as ObservableResource from "./utils/cache/ObservableResource.js" +import * as WatchableLookup from "./utils/cache/WatchableLookup.js" + +const hash = dual< + (y: number) => (x: number) => number, + (x: number, y: number) => number +>(2, (x, y) => Hash.number(x ^ y)) + +const hashEffect = dual< + (y: number) => (x: number) => Effect.Effect, + (x: number, y: number) => Effect.Effect +>(2, (x, y) => Effect.sync(() => hash(x, y))) + +describe("ScopedCache", () => { + it("cacheStats - should correctly keep track of cache size, hits and misses", () => + fc.assert( + fc.asyncProperty(fc.integer(), async (salt) => { + const program = Effect.gen(function*() { + const capacity = 10 + const scopedCache = ScopedCache.make({ + lookup: hashEffect(salt), + capacity, + timeToLive: Duration.infinity + }) + const { hits, misses, size } = yield* pipe( + scopedCache, + Effect.flatMap((cache) => + pipe( + Effect.forEach( + Array.map(Array.range(1, capacity), (n) => (n / 2) | 0), + (n) => Effect.scoped(Effect.zipRight(cache.get(n), Effect.void)), + { concurrency: "unbounded", discard: true } + ), + Effect.flatMap(() => cache.cacheStats) + ) + ) + ) + strictEqual(hits, 4) + strictEqual(misses, 6) + strictEqual(size, 6) + }) + return Effect.runPromise(Effect.scoped(program)) + }) + )) + + it.effect("invalidate - should properly remove and clean a resource from the cache", () => + Effect.gen(function*() { + const capacity = 100 + const observablesResources = yield* ( + Effect.forEach( + Array.range(0, capacity - 1), + () => ObservableResource.makeVoid() + ) + ) + const scopedCache = ScopedCache.make({ + capacity, + timeToLive: Duration.infinity, + lookup: (key: number) => observablesResources[key].scoped + }) + yield* (Effect.scoped(Effect.gen(function*() { + const cache = yield* scopedCache + yield* (Effect.forEach( + Array.range(0, capacity - 1), + (n) => Effect.scoped(Effect.zipRight(cache.get(n), Effect.void)), + { concurrency: "unbounded", discard: true } + )) + yield* (cache.invalidate(42)) + const cacheContainsKey42 = yield* (cache.contains(42)) + const { hits, misses, size } = yield* (cache.cacheStats) + yield* (observablesResources[42].assertAcquiredOnceAndCleaned()) + yield* (Effect.forEach( + pipe( + observablesResources, + Array.filter((_, index) => index !== 42) + ), + (observableResource) => observableResource.assertAcquiredOnceAndNotCleaned() + )) + assertFalse(cacheContainsKey42) + strictEqual(hits, 0) + strictEqual(misses, 100) + strictEqual(size, 99) + }))) + })) + + it.effect("invalidate - should not invalidate anything before effect is evaluated", () => + Effect.gen(function*() { + const observablesResource = yield* (ObservableResource.makeVoid()) + const scopedCache = ScopedCache.make({ + capacity: 4, + timeToLive: Duration.infinity, + lookup: () => observablesResource.scoped + }) + yield* (Effect.scoped(Effect.gen(function*() { + const cache = yield* scopedCache + yield* (Effect.scoped(Effect.zipRight(cache.get(void 0), Effect.void))) + const invalidateEffect = cache.invalidate(void 0) + const cacheContainsKey42BeforeInvalidate = yield* (cache.contains(void 0)) + yield* (observablesResource.assertAcquiredOnceAndNotCleaned()) + yield* (Effect.scoped(Effect.zipRight(cache.get(void 0), Effect.void))) + yield* invalidateEffect + const cacheContainsKey42AfterInvalidate = yield* (cache.contains(void 0)) + yield* (observablesResource.assertAcquiredOnceAndCleaned()) + assertTrue(cacheContainsKey42BeforeInvalidate) + assertFalse(cacheContainsKey42AfterInvalidate) + }))) + })) + + it.effect("invalidateAll - should properly remove and clean all resource from the cache", () => + Effect.gen(function*() { + const capacity = 100 + const observablesResources = yield* ( + Effect.forEach( + Array.range(0, capacity - 1), + () => ObservableResource.makeVoid() + ) + ) + const scopedCache = ScopedCache.make({ + capacity, + timeToLive: Duration.infinity, + lookup: (key: number) => observablesResources[key].scoped + }) + yield* (Effect.scoped(Effect.gen(function*() { + const cache = yield* scopedCache + yield* (Effect.forEach( + Array.range(0, capacity - 1), + (n) => Effect.scoped(Effect.zipRight(cache.get(n), Effect.void)), + { concurrency: "unbounded", discard: true } + )) + yield* (cache.invalidateAll) + const contains = yield* pipe( + Effect.forEach( + Array.range(0, capacity - 1), + (n) => Effect.scoped(cache.contains(n)), + { concurrency: "unbounded" } + ), + Effect.map((_) => _.every(identity)) + ) + const { hits, misses, size } = yield* (cache.cacheStats) + yield* (Effect.forEach( + observablesResources, + (observableResource) => observableResource.assertAcquiredOnceAndCleaned() + )) + assertFalse(contains) + strictEqual(hits, 0) + strictEqual(misses, 100) + strictEqual(size, 0) + }))) + })) + + it.effect("get - should not put anything in the cache before the scoped effect returned by get is used", () => + Effect.gen(function*() { + const observablesResource = yield* (ObservableResource.makeVoid()) + const scopedCache = ScopedCache.make({ + capacity: 1, + timeToLive: Duration.seconds(60), + lookup: () => observablesResource.scoped + }) + yield* (Effect.scoped(Effect.gen(function*() { + const cache = yield* scopedCache + yield* (observablesResource.assertNotAcquired()) + // Not actually retreiving from the cache + // @effect-diagnostics-next-line floatingEffect:off + cache.get(void 0) + yield* (observablesResource.assertNotAcquired()) + const contains = yield* (cache.contains(void 0)) + assertFalse(contains) + }))) + })) + + it("get - when used sequentially, should properly call correct lookup", () => + fc.assert(fc.asyncProperty(fc.integer(), (salt) => { + const program = Effect.gen(function*() { + const scopedCache = ScopedCache.make({ + capacity: 10, + timeToLive: Duration.infinity, + lookup: hashEffect(salt) + }) + yield* (Effect.scoped(Effect.gen(function*() { + const cache = yield* scopedCache + const actual = yield* ( + Effect.forEach( + Array.range(1, 10), + (n) => Effect.scoped(Effect.flatMap(cache.get(n), Effect.succeed)) + ) + ) + const expected = Array.map(Array.range(1, 10), hash(salt)) + deepStrictEqual(actual, expected) + }))) + }) + return Effect.runPromise(program) + }))) + + it("get - when used concurrently, should properly call correct lookup", () => + fc.assert(fc.asyncProperty(fc.integer(), (salt) => { + const program = Effect.gen(function*() { + const scopedCache = ScopedCache.make({ + capacity: 10, + timeToLive: Duration.infinity, + lookup: hashEffect(salt) + }) + yield* (Effect.scoped(Effect.gen(function*() { + const cache = yield* scopedCache + const actual = yield* ( + Effect.forEach( + Array.range(1, 10), + (n) => Effect.scoped(Effect.flatMap(cache.get(n), Effect.succeed)), + { concurrency: "unbounded" } + ) + ) + const expected = Array.map(Array.range(1, 10), hash(salt)) + deepStrictEqual(actual, expected) + }))) + }) + return Effect.runPromise(program) + }))) + + it("get - should clean and remove old resource to respect cache capacity", () => + fc.assert(fc.asyncProperty(fc.integer(), (salt) => { + const program = Effect.gen(function*() { + const scopedCache = ScopedCache.make({ + capacity: 5, + timeToLive: Duration.infinity, + lookup: hashEffect(salt) + }) + yield* (Effect.scoped(Effect.gen(function*() { + const cache = yield* scopedCache + const actual = yield* ( + Effect.forEach( + Array.range(1, 10), + (n) => Effect.scoped(Effect.flatMap(cache.get(n), Effect.succeed)) + ) + ) + const expected = Array.map(Array.range(1, 10), hash(salt)) + const cacheStats = yield* (cache.cacheStats) + deepStrictEqual(actual, expected) + strictEqual(cacheStats.size, 5) + }))) + }) + return Effect.runPromise(program) + }))) + + it.effect("get - sequential use of the scoped effect returned by a single call to get should create only one resource", () => + Effect.gen(function*() { + const subResource = yield* (ObservableResource.makeVoid()) + const scopedCache = ScopedCache.make({ + capacity: 1, + timeToLive: Duration.seconds(60), + lookup: (_: void) => subResource.scoped + }) + yield* (Effect.scoped(Effect.gen(function*() { + const cache = yield* scopedCache + yield* (subResource.assertNotAcquired()) + const resourceScopedProxy = cache.get(void 0) + yield* (subResource.assertNotAcquired()) + yield* (Effect.scoped(resourceScopedProxy)) + yield* (subResource.assertAcquiredOnceAndNotCleaned()) + yield* (Effect.scoped(resourceScopedProxy)) + yield* (subResource.assertAcquiredOnceAndNotCleaned()) + }))) + yield* (subResource.assertAcquiredOnceAndCleaned()) + })) + + it.effect("get - sequential use should create only one resource", () => + Effect.gen(function*() { + const subResource = yield* (ObservableResource.makeVoid()) + const scopedCache = ScopedCache.make({ + capacity: 1, + timeToLive: Duration.seconds(60), + lookup: (_: void) => subResource.scoped + }) + yield* (Effect.scoped(Effect.gen(function*() { + const cache = yield* scopedCache + yield* (subResource.assertNotAcquired()) + yield* (Effect.scoped(cache.get(void 0))) + yield* (subResource.assertAcquiredOnceAndNotCleaned()) + yield* (Effect.scoped(cache.get(void 0))) + yield* (subResource.assertAcquiredOnceAndNotCleaned()) + }))) + yield* (subResource.assertAcquiredOnceAndCleaned()) + })) + + it.effect("get - sequential use of a failing scoped effect should cache the error and immediately call the resource finalizer", () => + Effect.gen(function*() { + const watchableLookup = yield* ( + WatchableLookup.makeEffect(() => + Effect.fail(new Cause.RuntimeException("fail")) + ) + ) + const scopedCache = ScopedCache.make({ + capacity: 1, + timeToLive: Duration.seconds(60), + lookup: (key: void) => watchableLookup(key) + }) + yield* (Effect.scoped(Effect.gen(function*() { + const cache = yield* scopedCache + yield* (watchableLookup.assertCalledTimes(void 0, (n) => strictEqual(n, 0))) + const resourceScopedProxy = cache.get(void 0) + yield* (watchableLookup.assertCalledTimes(void 0, (n) => strictEqual(n, 0))) + yield* (Effect.either(Effect.scoped(resourceScopedProxy))) + yield* (watchableLookup.assertAllCleanedForKey(void 0)) + yield* (Effect.either(Effect.scoped(resourceScopedProxy))) + yield* (watchableLookup.assertCalledTimes(void 0, (n) => strictEqual(n, 1))) + }))) + })) + + it.effect("get - concurrent use of the scoped effect returned by a single call to get should create only one resource", () => + Effect.gen(function*() { + const subResource = yield* (ObservableResource.makeVoid()) + const scopedCache = ScopedCache.make({ + capacity: 1, + timeToLive: Duration.seconds(60), + lookup: (_: void) => subResource.scoped + }) + yield* (Effect.scoped(Effect.gen(function*() { + const cache = yield* scopedCache + const scoped = cache.get(void 0) + const scope1 = yield* (Scope.make()) + const scope2 = yield* (Scope.make()) + const acquire1 = Effect.provide(scoped, Context.make(Scope.Scope, scope1)) + const release1: Scope.Scope.Finalizer = (exit) => Scope.close(scope1, exit) + const acquire2 = Effect.provide(scoped, Context.make(Scope.Scope, scope2)) + const release2: Scope.Scope.Finalizer = (exit) => Scope.close(scope2, exit) + yield* (subResource.assertNotAcquired()) + yield* acquire2 + yield* (subResource.assertAcquiredOnceAndNotCleaned()) + yield* acquire1 + yield* (subResource.assertAcquiredOnceAndNotCleaned()) + yield* (release2(Exit.void)) + yield* (release1(Exit.void)) + yield* (subResource.assertAcquiredOnceAndNotCleaned()) + }))) + yield* (subResource.assertAcquiredOnceAndCleaned()) + })) + + it.effect("get - concurrent use on a failing scoped effect should cache the error and immediately call the resource finalizer", () => + Effect.gen(function*() { + const watchableLookup = yield* ( + WatchableLookup.makeEffect(() => + Effect.fail(new Cause.RuntimeException("fail")) + ) + ) + const scopedCache = ScopedCache.make({ + capacity: 1, + timeToLive: Duration.seconds(60), + lookup: (key: void) => watchableLookup(key) + }) + yield* (Effect.scoped(Effect.gen(function*() { + const cache = yield* scopedCache + yield* (watchableLookup.assertCalledTimes(void 0, (n) => strictEqual(n, 0))) + const resourceScopedProxy = cache.get(void 0) + yield* (watchableLookup.assertCalledTimes(void 0, (n) => strictEqual(n, 0))) + yield* (Effect.zip( + Effect.either(Effect.scoped(resourceScopedProxy)), + Effect.either(Effect.scoped(resourceScopedProxy)), + { concurrent: true } + )) + yield* (watchableLookup.assertAllCleanedForKey(void 0)) + yield* (watchableLookup.assertCalledTimes(void 0, (n) => strictEqual(n, 1))) + }))) + })) + + it.effect("get - when two scoped effects returned by two calls to get live longer than the cache, the resource should be cleaned only when it is not in use anymore", () => + Effect.gen(function*() { + const subResource = yield* (ObservableResource.makeVoid()) + const scopedCache = ScopedCache.make({ + capacity: 1, + timeToLive: Duration.seconds(60), + lookup: (_: void) => subResource.scoped + }) + const scope1 = yield* (Scope.make()) + const scope2 = yield* (Scope.make()) + const [release1, release2] = yield* (Effect.scoped(Effect.gen(function*() { + const cache = yield* scopedCache + yield* (Effect.provide( + cache.get(void 0), + Context.make(Scope.Scope, scope1) + )) + yield* (Effect.provide( + cache.get(void 0), + Context.make(Scope.Scope, scope2) + )) + const release1: Scope.Scope.Finalizer = (exit) => Scope.close(scope1, exit) + const release2: Scope.Scope.Finalizer = (exit) => Scope.close(scope2, exit) + return [release1, release2] as const + }))) + yield* (subResource.assertAcquiredOnceAndNotCleaned()) + yield* (release1(Exit.void)) + yield* (subResource.assertAcquiredOnceAndNotCleaned()) + yield* (release2(Exit.void)) + yield* (subResource.assertAcquiredOnceAndCleaned()) + })) + + it.effect("get - when two scoped effects obtained by a single scoped effect returned by a single call to get live longer than the cache, the resource should be cleaned only when it is not in use anymore", () => + Effect.gen(function*() { + const subResource = yield* (ObservableResource.makeVoid()) + const scopedCache = ScopedCache.make({ + capacity: 1, + timeToLive: Duration.seconds(60), + lookup: (_: void) => subResource.scoped + }) + const scope1 = yield* (Scope.make()) + const scope2 = yield* (Scope.make()) + const [release1, release2] = yield* (Effect.scoped(Effect.gen(function*() { + const cache = yield* scopedCache + const scoped = cache.get(void 0) + yield* (Effect.provide(scoped, Context.make(Scope.Scope, scope1))) + yield* (Effect.provide(scoped, Context.make(Scope.Scope, scope2))) + const release1: Scope.Scope.Finalizer = (exit) => Scope.close(scope1, exit) + const release2: Scope.Scope.Finalizer = (exit) => Scope.close(scope2, exit) + return [release1, release2] as const + }))) + yield* (subResource.assertAcquiredOnceAndNotCleaned()) + yield* (release1(Exit.void)) + yield* (subResource.assertAcquiredOnceAndNotCleaned()) + yield* (release2(Exit.void)) + yield* (subResource.assertAcquiredOnceAndCleaned()) + })) + + it("get - should clean old resources if the cache size is exceeded", () => { + const arb = fc.integer({ min: 1, max: 5 }).chain((cacheSize) => + fc.integer({ min: cacheSize, max: cacheSize + 3 }) + .map((numCreatedKey) => [cacheSize, numCreatedKey] as const) + ) + return fc.assert(fc.asyncProperty(arb, ([cacheSize, numCreatedKey]) => { + const program = Effect.gen(function*() { + const watchableLookup = yield* (WatchableLookup.make(() => void 0)) + const scopedCache = ScopedCache.make({ + capacity: cacheSize, + timeToLive: Duration.seconds(60), + lookup: (key: number) => watchableLookup(key) + }) + yield* (Effect.scoped(Effect.gen(function*() { + const cache = yield* scopedCache + yield* ( + Effect.forEach( + Array.range(0, numCreatedKey - 1), + (key) => Effect.scoped(Effect.asVoid(cache.get(key))), + { discard: true } + ) + ) + const createdResources = yield* (watchableLookup.createdResources()) + const cleanedAssertions = numCreatedKey - cacheSize - 1 + const oldestResourceCleaned = cleanedAssertions <= 0 + ? Array.empty() + : pipe( + Array.range(0, numCreatedKey - cacheSize - 1), + Array.flatMap((key) => Chunk.toReadonlyArray(HashMap.unsafeGet(createdResources, key))), + Array.map((resource) => resource.assertAcquiredOnceAndCleaned()) + ) + yield* (Effect.all(oldestResourceCleaned, { discard: true })) + const newestResourceNotCleanedYet = pipe( + Array.range(numCreatedKey - cacheSize, numCreatedKey - 1), + Array.flatMap((key) => Chunk.toReadonlyArray(HashMap.unsafeGet(createdResources, key))), + Array.map((resource) => resource.assertAcquiredOnceAndNotCleaned()) + ) + yield* (Effect.all(newestResourceNotCleanedYet, { discard: true })) + }))) + }) + return Effect.runPromise(program) + })) + }) + + it.effect("get - the scoped effect returned by get should recall lookup function if resource is too old and release the previous resource", () => + Effect.gen(function*() { + const watchableLookup = yield* (WatchableLookup.makeVoid()) + yield* (Effect.scoped(Effect.gen(function*() { + const cache = yield* (ScopedCache.make({ + capacity: 10, + timeToLive: Duration.seconds(10), + lookup: (key: void) => watchableLookup(key) + })) + const scoped = cache.get(void 0) + yield* (Effect.scoped(Effect.asVoid(scoped))) + yield* (TestClock.adjust(Duration.seconds(5))) + yield* (Effect.scoped(Effect.asVoid(scoped))) + yield* (watchableLookup.assertCalledTimes(void 0, (n) => strictEqual(n, 1))) + yield* (TestClock.adjust(Duration.seconds(4))) + yield* (Effect.scoped(Effect.asVoid(scoped))) + yield* (watchableLookup.assertCalledTimes(void 0, (n) => strictEqual(n, 1))) + yield* (TestClock.adjust(Duration.seconds(2))) + yield* (Effect.scoped(Effect.asVoid(scoped))) + yield* (watchableLookup.assertCalledTimes(void 0, (n) => strictEqual(n, 2))) + yield* (watchableLookup.assertFirstNCreatedResourcesCleaned(void 0, 1)) + }))) + })) + + it.effect("get - should recall lookup function if resource is too old and release old resource when using the scoped effect multiple times", () => + Effect.gen(function*() { + const watchableLookup = yield* (WatchableLookup.makeVoid()) + yield* (Effect.scoped(Effect.gen(function*() { + const cache = yield* (ScopedCache.make({ + capacity: 10, + timeToLive: Duration.seconds(10), + lookup: (key: void) => watchableLookup(key) + })) + const scoped = Effect.scoped(Effect.asVoid(cache.get(void 0))) + yield* scoped + yield* (TestClock.adjust(Duration.seconds(5))) + yield* scoped + yield* (watchableLookup.assertCalledTimes(void 0, (n) => strictEqual(n, 1))) + yield* (TestClock.adjust(Duration.seconds(4))) + yield* scoped + yield* (watchableLookup.assertCalledTimes(void 0, (n) => strictEqual(n, 1))) + yield* (TestClock.adjust(Duration.seconds(2))) + yield* scoped + yield* (watchableLookup.assertCalledTimes(void 0, (n) => strictEqual(n, 2))) + yield* (watchableLookup.assertFirstNCreatedResourcesCleaned(void 0, 1)) + }))) + })) + + it.effect("get - when resource is expired but still used it should wait until resource is not cleaned anymore to clean immediately", () => + Effect.gen(function*() { + const watchableLookup = yield* (WatchableLookup.makeVoid()) + yield* (Effect.scoped(Effect.gen(function*() { + const cache = yield* (ScopedCache.make({ + capacity: 10, + timeToLive: Duration.seconds(10), + lookup: (key: void) => watchableLookup(key) + })) + const scope = yield* (Scope.make()) + const acquire = Effect.provide( + cache.get(void 0), + Context.make(Scope.Scope, scope) + ) + const release: Scope.Scope.Finalizer = (exit) => Scope.close(scope, exit) + yield* acquire + yield* (TestClock.adjust(Duration.seconds(11))) + yield* (Effect.scoped(Effect.asVoid(cache.get(void 0)))) + yield* (watchableLookup.assertCalledTimes(void 0, (n) => strictEqual(n, 2))) + const firstCreatedResource = yield* (watchableLookup.firstCreatedResource(void 0)) + yield* (firstCreatedResource.assertAcquiredOnceAndNotCleaned()) + yield* (release(Exit.void)) + yield* (firstCreatedResource.assertAcquiredOnceAndCleaned()) + }))) + })) + + it.effect("getOption - should return None if resource is not in cache", () => + Effect.scoped(Effect.gen(function*() { + const scopedCache = yield* (ScopedCache.make({ + capacity: 1, + timeToLive: Duration.infinity, + lookup: (i: number) => Effect.succeed(i) + })) + const option = yield* (scopedCache.getOption(1)) + assertNone(option) + }))) + + it.effect("getOption - should return Some if pending", () => + Effect.scoped(Effect.gen(function*() { + const scopedCache = yield* (ScopedCache.make({ + capacity: 1, + timeToLive: Duration.infinity, + lookup: (i: number) => TestServices.provideLive(Effect.delay(Effect.succeed(i), Duration.millis(10))) + })) + yield* pipe(scopedCache.get(1), Effect.scoped, Effect.fork) + yield* (TestServices.provideLive(Effect.sleep(Duration.millis(5)))) + const option = yield* pipe(scopedCache.getOption(1), Effect.scoped) + assertSome(option, 1) + }))) + + it.effect("getOptionComplete - should return None if pending", () => + Effect.scoped(Effect.gen(function*() { + const scopedCache = yield* (ScopedCache.make({ + capacity: 1, + timeToLive: Duration.infinity, + lookup: (i: number) => Effect.delay(Effect.succeed(i), Duration.millis(10)) + })) + yield* pipe(scopedCache.get(1), Effect.scoped, Effect.fork) + yield* (TestClock.adjust(Duration.millis(9))) + const option = yield* pipe(scopedCache.getOptionComplete(1), Effect.scoped) + assertNone(option) + }))) + + it.effect("getOptionComplete - should return Some if complete", () => + Effect.scoped(Effect.gen(function*() { + const scopedCache = yield* (ScopedCache.make({ + capacity: 1, + timeToLive: Duration.infinity, + lookup: (i: number) => TestServices.provideLive(Effect.delay(Effect.succeed(i), Duration.millis(10))) + })) + yield* pipe(scopedCache.get(1), Effect.scoped) + const option = yield* pipe(scopedCache.getOptionComplete(1), Effect.scoped) + assertSome(option, 1) + }))) + + it.effect("refresh - should update the cache with a new value", () => + Effect.gen(function*() { + const inc = (n: number) => n * 10 + const retrieve = (multiplier: Ref.Ref) => (key: number) => + pipe( + Ref.updateAndGet(multiplier, inc), + Effect.map((multiplier) => key * multiplier) + ) + const seed = 1 + const key = 123 + const ref = yield* (Ref.make(seed)) + const scopedCache = ScopedCache.make({ + capacity: 1, + timeToLive: Duration.infinity, + lookup: retrieve(ref) + }) + const [val1, val2, val3] = yield* (Effect.scoped(Effect.gen(function*() { + const cache = yield* scopedCache + const val1 = yield* (cache.get(key)) + yield* (cache.refresh(key)) + const val2 = yield* (cache.get(key)) + const val3 = yield* (cache.get(key)) + return [val1, val2, val3] as const + }))) + strictEqual(val2, val3) + strictEqual(val2, inc(val1)) + })) + + it.effect("refresh - should clean old resource when making a new one", () => + Effect.gen(function*() { + const watchableLookup = yield* (WatchableLookup.makeVoid()) + const scopedCache = ScopedCache.make({ + capacity: 1, + timeToLive: Duration.infinity, + lookup: (key: void) => watchableLookup(key) + }) + yield* (Effect.scoped(Effect.gen(function*() { + const cache = yield* scopedCache + yield* (Effect.scoped(cache.get(void 0))) + yield* (cache.refresh(void 0)) + const createdResources = yield* pipe( + watchableLookup.createdResources(), + Effect.map(HashMap.unsafeGet(void 0)) + ) + yield* (Chunk.unsafeHead(createdResources).assertAcquiredOnceAndCleaned()) + yield* (Chunk.unsafeGet(createdResources, 1).assertAcquiredOnceAndNotCleaned()) + }))) + })) + + it.effect("refresh - should update the cache with a new value even if the last get or refresh failed", () => + Effect.gen(function*() { + const error = new Cause.RuntimeException("Must be a multiple of 3") + const inc = (n: number) => n + 1 + const retrieve = (number: Ref.Ref) => (key: number) => + pipe( + Ref.updateAndGet(number, inc), + Effect.flatMap((n) => + n % 3 === 0 + ? Effect.fail(error) + : Effect.succeed(key * n) + ) + ) + const seed = 2 + const key = 1 + const ref = yield* (Ref.make(seed)) + const scopedCache = ScopedCache.make({ + capacity: 1, + timeToLive: Duration.infinity, + lookup: retrieve(ref) + }) + const result = yield* (Effect.scoped(Effect.gen(function*() { + const cache = yield* scopedCache + const failure1 = yield* (Effect.either(cache.get(key))) + yield* (cache.refresh(key)) + const value1 = yield* (Effect.either(cache.get(key))) + yield* (cache.refresh(key)) + const failure2 = yield* (Effect.either(cache.refresh(key))) + yield* (cache.refresh(key)) + const value2 = yield* (Effect.either(cache.get(key))) + return { failure1, value1, failure2, value2 } + }))) + assertLeft(result.failure1, error) + assertLeft(result.failure2, error) + assertRight(result.value1, 4) + assertRight(result.value2, 7) + })) + + it.effect("refresh - should create and acquire subresource if the key doesn't exist in the cache", () => + Effect.gen(function*() { + const capacity = 100 + const scopedCache = ScopedCache.make({ + capacity, + timeToLive: Duration.infinity, + lookup: (_: number) => Effect.void + }) + yield* (Effect.scoped(Effect.gen(function*() { + const cache = yield* scopedCache + const count0 = yield* (cache.size) + yield* (Effect.forEach(Array.range(1, capacity), (key) => cache.refresh(key), { discard: true })) + const count1 = yield* (cache.size) + strictEqual(count0, 0) + strictEqual(count1, capacity) + }))) + })) + + it("refresh - should clean old resource if cache size is exceeded", () => { + const arb = fc.integer({ min: 1, max: 5 }).chain((cacheSize) => + fc.integer({ min: cacheSize, max: cacheSize + 3 }) + .map((numCreatedKey) => [cacheSize, numCreatedKey] as const) + ) + return fc.assert(fc.asyncProperty(arb, ([cacheSize, numCreatedKey]) => { + const program = Effect.gen(function*() { + const watchableLookup = yield* (WatchableLookup.make(() => void 0)) + const scopedCache = ScopedCache.make({ + capacity: cacheSize, + timeToLive: Duration.seconds(60), + lookup: (key: number) => watchableLookup(key) + }) + yield* (Effect.scoped(Effect.gen(function*() { + const cache = yield* scopedCache + yield* (Effect.forEach( + Array.range(0, numCreatedKey - 1), + (key) => cache.refresh(key), + { discard: true } + )) + const createdResources = yield* (watchableLookup.createdResources()) + const cleanedAssertions = numCreatedKey - cacheSize - 1 + const oldestResourceCleaned = cleanedAssertions <= 0 + ? Array.empty() + : pipe( + Array.range(0, numCreatedKey - cacheSize - 1), + Array.flatMap((key) => Chunk.toReadonlyArray(HashMap.unsafeGet(createdResources, key))), + Array.map((resource) => resource.assertAcquiredOnceAndCleaned()) + ) + yield* (Effect.all(oldestResourceCleaned, { discard: true })) + const newestResourceNotCleanedYet = pipe( + Array.range(numCreatedKey - cacheSize, numCreatedKey - 1), + Array.flatMap((key) => Chunk.toReadonlyArray(HashMap.unsafeGet(createdResources, key))), + Array.map((resource) => resource.assertAcquiredOnceAndNotCleaned()) + ) + yield* (Effect.all(newestResourceNotCleanedYet, { discard: true })) + }))) + }) + return Effect.runPromise(program) + })) + }) + + it.effect("refresh - should not clean the resource if it's not yet expired until the new resource is ready", () => + Effect.gen(function*() { + const watchableLookup = yield* (WatchableLookup.makeVoid()) + yield* (Effect.scoped(Effect.gen(function*() { + const cache = yield* (ScopedCache.make({ + capacity: 10, + timeToLive: Duration.seconds(10), + lookup: watchableLookup + })) + yield* (Effect.scoped(Effect.asVoid(cache.get(void 0)))) + yield* (TestClock.adjust(Duration.seconds(9))) + yield* (watchableLookup.lock()) + const refreshFiber = yield* (Effect.fork(cache.refresh(void 0))) + yield* pipe( + watchableLookup.getCalledTimes(void 0), + Effect.repeat(pipe( + Schedule.recurWhile((calledTimes) => calledTimes < 2), + Schedule.compose(Schedule.elapsed), + Schedule.whileOutput((elapsed) => Duration.lessThan(elapsed, Duration.millis(100))) + )) + ) + yield* (TestServices.provideLive(Effect.sleep(Duration.millis(100)))) + yield* (watchableLookup.assertCalledTimes(void 0, (n) => strictEqual(n, 2))) + const firstCreatedResource = yield* (watchableLookup.firstCreatedResource(void 0)) + yield* (firstCreatedResource.assertAcquiredOnceAndNotCleaned()) + yield* (watchableLookup.unlock()) + yield* (Fiber.join(refreshFiber)) + yield* (firstCreatedResource.assertAcquiredOnceAndCleaned()) + }))) + })) + + it.effect("refresh - should clean the resource if it's expired and not in used", () => + Effect.gen(function*() { + const watchableLookup = yield* (WatchableLookup.makeVoid()) + yield* (Effect.scoped(Effect.gen(function*() { + const cache = yield* (ScopedCache.make({ + capacity: 10, + timeToLive: Duration.seconds(10), + lookup: watchableLookup + })) + yield* (Effect.scoped(Effect.asVoid(cache.get(void 0)))) + yield* (TestClock.adjust(Duration.seconds(11))) + yield* (watchableLookup.lock()) + const refreshFiber = yield* (Effect.fork(cache.refresh(void 0))) + yield* pipe( + watchableLookup.getCalledTimes(void 0), + Effect.repeat(pipe( + Schedule.recurWhile((calledTimes) => calledTimes < 1), + Schedule.compose(Schedule.elapsed), + Schedule.whileOutput((elapsed) => Duration.lessThan(elapsed, Duration.millis(100))) + )) + ) + yield* (TestServices.provideLive(Effect.sleep(Duration.millis(100)))) + yield* (watchableLookup.assertCalledTimes(void 0, (n) => strictEqual(n, 2))) + yield* (watchableLookup.assertFirstNCreatedResourcesCleaned(void 0, 1)) + yield* (watchableLookup.unlock()) + yield* (Fiber.join(refreshFiber)) + }))) + })) + + it.effect("refresh - should wait to clean expired resource until it's not in use anymore", () => + Effect.gen(function*() { + const watchableLookup = yield* (WatchableLookup.makeVoid()) + yield* (Effect.scoped(Effect.gen(function*() { + const cache = yield* (ScopedCache.make({ + capacity: 10, + timeToLive: Duration.seconds(10), + lookup: watchableLookup + })) + const scope = yield* (Scope.make()) + const acquire = Effect.provide( + cache.get(void 0), + Context.make(Scope.Scope, scope) + ) + const release: Scope.Scope.Finalizer = (exit) => Scope.close(scope, exit) + yield* acquire + yield* (TestClock.adjust(Duration.seconds(11))) + yield* (cache.refresh(void 0)) + yield* (watchableLookup.assertCalledTimes(void 0, (n) => strictEqual(n, 2))) + const firstCreatedResource = yield* (watchableLookup.firstCreatedResource(void 0)) + yield* (firstCreatedResource.assertAcquiredOnceAndNotCleaned()) + yield* (release(Exit.void)) + yield* (firstCreatedResource.assertAcquiredOnceAndCleaned()) + }))) + })) + it.effect(".pipe", () => + Effect.gen(function*() { + const cache = yield* pipe( + ScopedCache.make({ + capacity: 10, + timeToLive: Duration.seconds(10), + lookup: () => Effect.void + }), + Effect.scoped + ) + strictEqual(cache.pipe(identity), cache) + })) +}) diff --git a/repos/effect/packages/effect/test/ScopedRef.test.ts b/repos/effect/packages/effect/test/ScopedRef.test.ts new file mode 100644 index 0000000..3e2f7ab --- /dev/null +++ b/repos/effect/packages/effect/test/ScopedRef.test.ts @@ -0,0 +1,86 @@ +import { describe, it } from "@effect/vitest" +import { strictEqual } from "@effect/vitest/utils" +import { Effect, identity, pipe, ScopedRef } from "effect" +import * as Counter from "./utils/counter.js" + +describe("ScopedRef", () => { + it.scoped("single set", () => + Effect.gen(function*() { + const counter = yield* (Counter.make()) + const ref = yield* (ScopedRef.make(() => 0)) + yield* (ScopedRef.set(ref, counter.acquire())) + const result = yield* (ScopedRef.get(ref)) + strictEqual(result, 1) + })) + it.scoped("dual set", () => + Effect.gen(function*() { + const counter = yield* (Counter.make()) + const ref = yield* (ScopedRef.make(() => 0)) + yield* pipe( + ScopedRef.set(ref, counter.acquire()), + Effect.zipRight(ScopedRef.set(ref, counter.acquire())) + ) + const result = yield* (ScopedRef.get(ref)) + strictEqual(result, 2) + })) + it.scoped("release on swap", () => + Effect.gen(function*() { + const counter = yield* (Counter.make()) + const ref = yield* (ScopedRef.make(() => 0)) + yield* pipe( + ScopedRef.set(ref, counter.acquire()), + Effect.zipRight(ScopedRef.set(ref, counter.acquire())) + ) + + const acquired = yield* (counter.acquired()) + const released = yield* (counter.released()) + strictEqual(acquired, 2) + strictEqual(released, 1) + })) + it.scoped("double release on double swap", () => + Effect.gen(function*() { + const counter = yield* (Counter.make()) + const ref = yield* (ScopedRef.make(() => 0)) + yield* ( + pipe( + ScopedRef.set(ref, counter.acquire()), + Effect.zipRight(ScopedRef.set(ref, counter.acquire())), + Effect.zipRight(ScopedRef.set(ref, counter.acquire())) + ) + ) + const acquired = yield* (counter.acquired()) + const released = yield* (counter.released()) + strictEqual(acquired, 3) + strictEqual(released, 2) + })) + it.effect("full release", () => + Effect.gen(function*() { + const counter = yield* (Counter.make()) + yield* pipe( + ScopedRef.make(() => 0), + Effect.flatMap((ref) => + pipe( + ScopedRef.set(ref, counter.acquire()), + Effect.zipRight(ScopedRef.set(ref, counter.acquire())), + Effect.zipRight(ScopedRef.set(ref, counter.acquire())) + ) + ), + Effect.scoped + ) + const acquired = yield* (counter.acquired()) + const released = yield* (counter.released()) + strictEqual(acquired, 3) + strictEqual(released, 3) + })) + it.effect("full release", () => + Effect.gen(function*() { + const ref = yield* Effect.scoped(ScopedRef.make(() => 0)) + strictEqual(ref.pipe(identity), ref) + })) + it.scoped("subtype of Effect", () => + Effect.gen(function*() { + const ref = yield* ScopedRef.make(() => 0) + const result = yield* ref + strictEqual(result, 0) + })) +}) diff --git a/repos/effect/packages/effect/test/Sink/collecting.test.ts b/repos/effect/packages/effect/test/Sink/collecting.test.ts new file mode 100644 index 0000000..7fe53ba --- /dev/null +++ b/repos/effect/packages/effect/test/Sink/collecting.test.ts @@ -0,0 +1,264 @@ +import { describe, it } from "@effect/vitest" +import { deepStrictEqual, strictEqual } from "@effect/vitest/utils" +import * as Chunk from "effect/Chunk" +import * as Effect from "effect/Effect" +import { constTrue, pipe } from "effect/Function" +import * as Option from "effect/Option" +import * as Sink from "effect/Sink" +import * as Stream from "effect/Stream" + +describe("Sink", () => { + it.effect("collectAllN - respects the given limit", () => + Effect.gen(function*() { + const stream = pipe( + Stream.fromChunk(Chunk.make(1, 2, 3, 4)), + Stream.transduce(Sink.collectAllN(3)) + ) + const result = yield* (Stream.runCollect(stream)) + deepStrictEqual( + Array.from(Chunk.map(result, (chunk) => Array.from(chunk))), + [[1, 2, 3], [4]] + ) + })) + + it.effect("collectAllN - produces empty trailing chunks", () => + Effect.gen(function*() { + const stream = pipe( + Stream.fromChunk(Chunk.make(1, 2, 3, 4)), + Stream.transduce(Sink.collectAllN(4)) + ) + const result = yield* (Stream.runCollect(stream)) + deepStrictEqual( + Array.from(Chunk.map(result, (chunk) => Array.from(chunk))), + [[1, 2, 3, 4], []] + ) + })) + + it.effect("collectAllN - produces empty trailing chunks", () => + Effect.gen(function*() { + const stream = pipe( + Stream.fromChunk(Chunk.empty()), + Stream.transduce(Sink.collectAllN(3)) + ) + const result = yield* (Stream.runCollect(stream)) + deepStrictEqual( + Array.from(Chunk.map(result, (chunk) => Array.from(chunk))), + [[]] + ) + })) + + it.effect("collectAllToSet", () => + Effect.gen(function*() { + const stream = Stream.make(1, 2, 3, 3, 4) + const result = yield* pipe(stream, Stream.run(Sink.collectAllToSet())) + deepStrictEqual(Array.from(result), [1, 2, 3, 4]) + })) + + it.effect("collectAllToSetN - respects the given limit", () => + Effect.gen(function*() { + const stream = pipe( + Stream.fromChunks(Chunk.make(1, 2, 1), Chunk.make(2, 3, 3, 4)), + Stream.transduce(Sink.collectAllToSetN(3)) + ) + const result = yield* (Stream.runCollect(stream)) + deepStrictEqual( + Array.from(Chunk.map(result, (set) => Array.from(set))), + [[1, 2, 3], [4]] + ) + })) + + it.effect("collectAllToSetN - handles empty input", () => + Effect.gen(function*() { + const stream = pipe( + Stream.fromChunk(Chunk.empty()), + Stream.transduce(Sink.collectAllToSetN(3)) + ) + const result = yield* (Stream.runCollect(stream)) + deepStrictEqual( + Array.from(Chunk.map(result, (set) => Array.from(set))), + [[]] + ) + })) + + it.effect("collectAllToMap", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.range(0, 9), + Stream.run(Sink.collectAllToMap( + (n) => n % 3, + (x, y) => x + y + )) + ) + deepStrictEqual( + Array.from(result), + [[0, 18], [1, 12], [2, 15]] + ) + })) + + it.effect("collectAllToMapN - respects the given limit", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.make(1, 1, 2, 2, 3, 2, 4, 5), + Stream.transduce(Sink.collectAllToMapN( + 2, + (n) => n % 3, + (x, y) => x + y + )), + Stream.runCollect + ) + deepStrictEqual( + Array.from(Chunk.map(result, (chunk) => Array.from(chunk))), + [[[1, 2], [2, 4]], [[0, 3], [2, 2]], [[1, 4], [2, 5]]] + ) + })) + + it.effect("collectAllToMapN - collects as long as map size doesn't exceed the limit", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.fromChunks(Chunk.make(0, 1, 2), Chunk.make(3, 4, 5), Chunk.make(6, 7, 8, 9)), + Stream.transduce(Sink.collectAllToMapN( + 3, + (n) => n % 3, + (x, y) => x + y + )), + Stream.runCollect + ) + deepStrictEqual( + Array.from(Chunk.map(result, (chunk) => Array.from(chunk))), + [[[0, 18], [1, 12], [2, 15]]] + ) + })) + + it.effect("collectAllToMapN - handles empty input", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.fromChunk(Chunk.empty()), + Stream.transduce(Sink.collectAllToMapN( + 3, + (n) => n % 3, + (x, y) => x + y + )), + Stream.runCollect + ) + deepStrictEqual( + Array.from(Chunk.map(result, (chunk) => Array.from(chunk))), + [[]] + ) + })) + + it.effect("collectAllUntil", () => + Effect.gen(function*() { + const sink = Sink.collectAllUntil((n) => n > 4) + const input = Chunk.make( + Chunk.make(3, 4, 5, 6, 7, 2), + Chunk.empty(), + Chunk.make(3, 4, 5, 6, 5, 4, 3, 2), + Chunk.empty() + ) + const result = yield* pipe(Stream.fromChunks(...input), Stream.transduce(sink), Stream.runCollect) + deepStrictEqual( + Array.from(result).map((chunk) => Array.from(chunk)), + [[3, 4, 5], [6], [7], [2, 3, 4, 5], [6], [5], [4, 3, 2]] + ) + })) + + it.effect("collectAllUntilEffect", () => + Effect.gen(function*() { + const sink = Sink.collectAllUntilEffect((n: number) => Effect.succeed(n > 4)) + const input = Chunk.make( + Chunk.make(3, 4, 5, 6, 7, 2), + Chunk.empty(), + Chunk.make(3, 4, 5, 6, 5, 4, 3, 2), + Chunk.empty() + ) + const result = yield* pipe(Stream.fromChunks(...input), Stream.transduce(sink), Stream.runCollect) + deepStrictEqual( + Array.from(result).map((chunk) => Array.from(chunk)), + [[3, 4, 5], [6], [7], [2, 3, 4, 5], [6], [5], [4, 3, 2]] + ) + })) + + it.effect("collectAllWhile", () => + Effect.gen(function*() { + const sink = pipe( + Sink.collectAllWhile((n) => n < 5), + Sink.zipLeft(Sink.collectAllWhile((n) => n >= 5)) + ) + const input = Chunk.make( + Chunk.make(3, 4, 5, 6, 7, 2), + Chunk.empty(), + Chunk.make(3, 4, 5, 6, 5, 4, 3, 2), + Chunk.empty() + ) + const result = yield* pipe(Stream.fromChunks(...input), Stream.transduce(sink), Stream.runCollect) + deepStrictEqual( + Array.from(result).map((chunk) => Array.from(chunk)), + [[3, 4], [2, 3, 4], [4, 3, 2]] + ) + })) + + it.effect("collectAllWhileEffect", () => + Effect.gen(function*() { + const sink = pipe( + Sink.collectAllWhileEffect((n: number) => Effect.succeed(n < 5)), + Sink.zipLeft(Sink.collectAllWhileEffect((n: number) => Effect.succeed(n >= 5))) + ) + const input = Chunk.make( + Chunk.make(3, 4, 5, 6, 7, 2), + Chunk.empty(), + Chunk.make(3, 4, 5, 6, 5, 4, 3, 2), + Chunk.empty() + ) + const result = yield* pipe(Stream.fromChunks(...input), Stream.transduce(sink), Stream.runCollect) + deepStrictEqual( + Array.from(result).map((chunk) => Array.from(chunk)), + [[3, 4], [2, 3, 4], [4, 3, 2]] + ) + })) + + it.effect("collectAllWhileWith - example 1", () => + Effect.gen(function*() { + const program = (chunkSize: number) => + pipe( + Stream.fromChunk(Chunk.range(1, 10)), + Stream.rechunk(chunkSize), + Stream.run(pipe( + Sink.sum, + Sink.collectAllWhileWith({ + initial: -1, + while: (n) => n === n, + body: (acc, curr) => acc + curr + }) + )) + ) + const result1 = yield* (program(1)) + const result2 = yield* (program(3)) + const result3 = yield* (program(20)) + strictEqual(result1, 54) + strictEqual(result2, 54) + strictEqual(result3, 54) + })) + + it.effect("collectAllWhileWith - example 2", () => + Effect.gen(function*() { + const sink = pipe( + Sink.head(), + Sink.collectAllWhileWith({ + initial: Chunk.empty(), + while: Option.match({ + onNone: constTrue, + onSome: (n) => n < 5 + }), + body: (acc, option) => Option.isSome(option) ? pipe(acc, Chunk.append(option.value)) : acc + }) + ) + const stream = Stream.fromChunk(Chunk.range(1, 100)) + const result = yield* pipe( + stream, + Stream.concat(stream), + Stream.rechunk(3), + Stream.run(sink) + ) + deepStrictEqual(Array.from(result), [1, 2, 3, 4]) + })) +}) diff --git a/repos/effect/packages/effect/test/Sink/constructors.test.ts b/repos/effect/packages/effect/test/Sink/constructors.test.ts new file mode 100644 index 0000000..eb0f2bd --- /dev/null +++ b/repos/effect/packages/effect/test/Sink/constructors.test.ts @@ -0,0 +1,191 @@ +import { describe, it } from "@effect/vitest" +import { assertTrue, deepStrictEqual } from "@effect/vitest/utils" +import type * as Chunk from "effect/Chunk" +import * as Deferred from "effect/Deferred" +import * as Effect from "effect/Effect" +import * as Effectable from "effect/Effectable" +import * as Exit from "effect/Exit" +import * as Fiber from "effect/Fiber" +import { pipe } from "effect/Function" +import type * as MutableQueue from "effect/MutableQueue" +import type * as MutableRef from "effect/MutableRef" +import type * as Option from "effect/Option" +import { pipeArguments } from "effect/Pipeable" +import * as PubSub from "effect/PubSub" +import * as Queue from "effect/Queue" +import * as Sink from "effect/Sink" +import * as Stream from "effect/Stream" +import * as internalQueue from "../../src/internal/queue.js" + +describe("Sink", () => { + it.effect("drain - fails if upstream fails", () => + Effect.gen(function*() { + const stream = pipe( + Stream.make(1), + Stream.mapEffect(() => Effect.fail("boom!")) + ) + const result = yield* pipe(stream, Stream.run(Sink.drain), Effect.exit) + deepStrictEqual(result, Exit.fail("boom!")) + })) + + it.effect("fromEffect", () => + Effect.gen(function*() { + const sink = Sink.fromEffect(Effect.succeed("ok")) + const result = yield* pipe(Stream.make(1, 2, 3), Stream.run(sink)) + deepStrictEqual(result, "ok") + })) + + it.effect("fromQueue - should enqueue all elements", () => + Effect.gen(function*() { + const queue = yield* (Queue.unbounded()) + yield* pipe(Stream.make(1, 2, 3), Stream.run(Sink.fromQueue(queue))) + const result = yield* (Queue.takeAll(queue)) + deepStrictEqual(Array.from(result), [1, 2, 3]) + })) + + it.effect("fromQueueWithShutdown - should enqueue all elements and shutdown the queue", () => + Effect.gen(function*() { + const queue = yield* pipe(Queue.unbounded(), Effect.map(createQueueSpy)) + yield* pipe(Stream.make(1, 2, 3), Stream.run(Sink.fromQueue(queue, { shutdown: true }))) + const enqueuedValues = yield* (Queue.takeAll(queue)) + const isShutdown = yield* (Queue.isShutdown(queue)) + deepStrictEqual(Array.from(enqueuedValues), [1, 2, 3]) + assertTrue(isShutdown) + })) + + it.effect("fromPubSub - should publish all elements", () => + Effect.gen(function*() { + const deferred1 = yield* (Deferred.make()) + const deferred2 = yield* (Deferred.make()) + const pubsub = yield* (PubSub.unbounded()) + const fiber = yield* pipe( + PubSub.subscribe(pubsub), + Effect.flatMap((subscription) => + pipe( + Deferred.succeed(deferred1, void 0), + Effect.zipRight(Deferred.await(deferred2)), + Effect.zipRight(Queue.takeAll(subscription)) + ) + ), + Effect.scoped, + Effect.fork + ) + yield* (Deferred.await(deferred1)) + yield* pipe(Stream.make(1, 2, 3), Stream.run(Sink.fromPubSub(pubsub))) + yield* (Deferred.succeed(deferred2, void 0)) + const result = yield* (Fiber.join(fiber)) + deepStrictEqual(Array.from(result), [1, 2, 3]) + })) + + it.effect("fromPubSub(_, { shutdown: true }) - should shutdown the pubsub", () => + Effect.gen(function*() { + const pubsub = yield* (PubSub.unbounded()) + yield* pipe(Stream.make(1, 2, 3), Stream.run(Sink.fromPubSub(pubsub, { shutdown: true }))) + const isShutdown = yield* (PubSub.isShutdown(pubsub)) + assertTrue(isShutdown) + })) +}) + +const createQueueSpy = (queue: Queue.Queue): Queue.Queue => new QueueSpy(queue) + +class QueueSpy extends Effectable.Class implements Queue.Queue { + readonly [Queue.DequeueTypeId] = internalQueue.dequeueVariance + readonly [Queue.EnqueueTypeId] = internalQueue.enqueueVariance + private isShutdownInternal = false + readonly queue: Queue.BackingQueue + readonly shutdownFlag: MutableRef.MutableRef + readonly shutdownHook: Deferred.Deferred + readonly strategy: Queue.Strategy + readonly takers: MutableQueue.MutableQueue> + + constructor(readonly backingQueue: Queue.Queue) { + super() + this.queue = backingQueue.queue + this.shutdownFlag = backingQueue.shutdownFlag + this.shutdownHook = backingQueue.shutdownHook + this.strategy = backingQueue.strategy + this.takers = backingQueue.takers + } + + commit() { + return this.take + } + + pipe() { + return pipeArguments(this, arguments) + } + + unsafeOffer(value: A): boolean { + return Queue.unsafeOffer(this.backingQueue, value) + } + + offer(a: A) { + return Queue.offer(this.backingQueue, a) + } + + offerAll(elements: Iterable) { + return Queue.offerAll(this.backingQueue, elements) + } + + capacity(): number { + return Queue.capacity(this.backingQueue) + } + + get size(): Effect.Effect { + return Queue.size(this.backingQueue) + } + + unsafeSize(): Option.Option { + return this.backingQueue.unsafeSize() + } + + get awaitShutdown(): Effect.Effect { + return Queue.awaitShutdown(this.backingQueue) + } + + isActive(): boolean { + return !this.isShutdownInternal + } + + get isShutdown(): Effect.Effect { + return Effect.sync(() => this.isShutdownInternal) + } + + get shutdown(): Effect.Effect { + return Effect.sync(() => { + this.isShutdownInternal = true + }) + } + + get isFull(): Effect.Effect { + return Queue.isFull(this.backingQueue) + } + + get isEmpty(): Effect.Effect { + return Queue.isEmpty(this.backingQueue) + } + + get take(): Effect.Effect { + return Queue.take(this.backingQueue) + } + + get takeAll(): Effect.Effect> { + return Queue.takeAll(this.backingQueue) + } + + takeUpTo(max: number): Effect.Effect> { + return Queue.takeUpTo(this.backingQueue, max) + } + + takeBetween(min: number, max: number): Effect.Effect> { + return Queue.takeBetween(this.backingQueue, min, max) + } + + takeN(n: number): Effect.Effect> { + return Queue.takeN(this.backingQueue, n) + } + + poll(): Effect.Effect> { + return Queue.poll(this.backingQueue) + } +} diff --git a/repos/effect/packages/effect/test/Sink/dropping.test.ts b/repos/effect/packages/effect/test/Sink/dropping.test.ts new file mode 100644 index 0000000..2e49e6d --- /dev/null +++ b/repos/effect/packages/effect/test/Sink/dropping.test.ts @@ -0,0 +1,77 @@ +import { describe, it } from "@effect/vitest" +import { deepStrictEqual } from "@effect/vitest/utils" +import * as Effect from "effect/Effect" +import * as Either from "effect/Either" +import { pipe } from "effect/Function" +import * as Sink from "effect/Sink" +import * as Stream from "effect/Stream" + +describe("Sink", () => { + it.effect("dropUntil", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.make(1, 2, 3, 4, 5, 1, 2, 3, 4, 5), + Stream.pipeThrough(Sink.dropUntil((n) => n >= 3)), + Stream.runCollect + ) + deepStrictEqual(Array.from(result), [4, 5, 1, 2, 3, 4, 5]) + })) + + it.effect("dropUntilEffect - happy path", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.make(1, 2, 3, 4, 5, 1, 2, 3, 4, 5), + Stream.pipeThrough(Sink.dropUntilEffect((n) => Effect.succeed(n >= 3))), + Stream.runCollect + ) + deepStrictEqual(Array.from(result), [4, 5, 1, 2, 3, 4, 5]) + })) + + it.effect("dropUntilEffect - error", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.make(1, 2, 3), + Stream.concat(Stream.fail("Aie")), + Stream.concat(Stream.make(5, 1, 2, 3, 4, 5)), + Stream.pipeThrough(Sink.dropUntilEffect((n) => Effect.succeed(n >= 2))), + Stream.either, + Stream.runCollect + ) + deepStrictEqual(Array.from(result), [Either.right(3), Either.left("Aie")]) + })) + + it.effect("dropWhile", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.make(1, 2, 3, 4, 5, 1, 2, 3, 4, 5), + Stream.pipeThrough(Sink.dropWhile((n) => n < 3)), + Stream.runCollect + ) + deepStrictEqual(Array.from(result), [3, 4, 5, 1, 2, 3, 4, 5]) + })) + + it.effect("dropWhileEffect - happy path", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.make(1, 2, 3, 4, 5, 1, 2, 3, 4, 5), + Stream.pipeThrough(Sink.dropWhileEffect((n) => Effect.succeed(n < 3))), + Stream.runCollect + ) + deepStrictEqual(Array.from(result), [3, 4, 5, 1, 2, 3, 4, 5]) + })) + + it.effect("dropWhileEffect - error", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.concat( + Stream.make(1, 2, 3), + Stream.fail("Aie") + ), + Stream.concat(Stream.make(5, 1, 2, 3, 4, 5)), + Stream.pipeThrough(Sink.dropWhileEffect((n) => Effect.succeed(n < 3))), + Stream.either, + Stream.runCollect + ) + deepStrictEqual(Array.from(result), [Either.right(3), Either.left("Aie")]) + })) +}) diff --git a/repos/effect/packages/effect/test/Sink/elements.test.ts b/repos/effect/packages/effect/test/Sink/elements.test.ts new file mode 100644 index 0000000..43241dc --- /dev/null +++ b/repos/effect/packages/effect/test/Sink/elements.test.ts @@ -0,0 +1,116 @@ +import { describe, it } from "@effect/vitest" +import { assertSome, assertTrue, deepStrictEqual, strictEqual } from "@effect/vitest/utils" +import * as Chunk from "effect/Chunk" +import * as Effect from "effect/Effect" +import { pipe } from "effect/Function" +import * as Sink from "effect/Sink" +import * as Stream from "effect/Stream" + +describe("Sink", () => { + it.effect("every", () => + Effect.gen(function*() { + const chunk = Chunk.make(1, 2, 3, 4, 5) + const predicate = (n: number) => n < 6 + const result = yield* pipe( + Stream.fromChunk(chunk), + Stream.run(Sink.every(predicate)) + ) + assertTrue(result) + })) + + it.effect("head", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.fromChunks(Chunk.range(1, 10), Chunk.range(1, 3), Chunk.range(2, 5)), + Stream.run(Sink.head()) + ) + assertSome(result, 1) + })) + + it.effect("last", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.fromChunks(Chunk.range(1, 10), Chunk.range(1, 3), Chunk.range(2, 5)), + Stream.run(Sink.last()) + ) + assertSome(result, 5) + })) + + it.effect("take - repeats until the source is exhausted", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.fromChunks( + Chunk.make(1, 2), + Chunk.make(3, 4, 5), + Chunk.empty(), + Chunk.make(6, 7), + Chunk.make(8, 9) + ), + Stream.run(Sink.collectAllFrom(Sink.take(3))) + ) + deepStrictEqual( + Array.from(result).map((chunk) => Array.from(chunk)), + [[1, 2, 3], [4, 5, 6], [7, 8, 9], []] + ) + })) + + it.effect("some", () => + Effect.gen(function*() { + const chunk = Chunk.make(1, 2, 3, 4, 5) + const predicate = (n: number) => n === 3 + const result = yield* pipe( + Stream.fromChunk(chunk), + Stream.run(Sink.some(predicate)) + ) + assertTrue(result) + })) + + it.effect("sum", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.fromChunks( + Chunk.make(1, 2), + Chunk.make(3, 4, 5), + Chunk.empty(), + Chunk.make(6, 7), + Chunk.make(8, 9) + ), + Stream.run(pipe( + Sink.collectAllFrom(Sink.sum), + Sink.map(Chunk.reduce(0, (x, y) => x + y)) + )) + ) + strictEqual(result, 45) + })) + + it.effect("take", () => + Effect.gen(function*() { + const n = 4 + const chunks = Chunk.make( + Chunk.make(1, 2), + Chunk.make(3, 4, 5), + Chunk.empty(), + Chunk.make(6, 7), + Chunk.make(8, 9) + ) + const [chunk, leftover] = yield* pipe( + Stream.fromChunks(...chunks), + Stream.peel(Sink.take(n)), + Effect.flatMap(([chunk, stream]) => + pipe( + Stream.runCollect(stream), + Effect.map((leftover) => [chunk, leftover] as const) + ) + ), + Effect.scoped + ) + deepStrictEqual( + Array.from(chunk), + Array.from(pipe(Chunk.flatten(chunks), Chunk.take(n))) + ) + deepStrictEqual( + Array.from(leftover), + Array.from(pipe(Chunk.flatten(chunks), Chunk.drop(n))) + ) + })) +}) diff --git a/repos/effect/packages/effect/test/Sink/environment.test.ts b/repos/effect/packages/effect/test/Sink/environment.test.ts new file mode 100644 index 0000000..039c882 --- /dev/null +++ b/repos/effect/packages/effect/test/Sink/environment.test.ts @@ -0,0 +1,20 @@ +import { describe, it } from "@effect/vitest" +import { strictEqual } from "@effect/vitest/utils" +import * as Context from "effect/Context" +import * as Effect from "effect/Effect" +import { pipe } from "effect/Function" +import * as Sink from "effect/Sink" +import * as Stream from "effect/Stream" + +describe("Sink", () => { + it.effect("contextWithSink", () => + Effect.gen(function*() { + const tag = Context.GenericTag("string") + const sink = pipe( + Sink.contextWithSink((env: Context.Context) => Sink.succeed(pipe(env, Context.get(tag)))), + Sink.provideContext(pipe(Context.empty(), Context.add(tag, "use this"))) + ) + const result = yield* pipe(Stream.make("ignore this"), Stream.run(sink)) + strictEqual(result, "use this") + })) +}) diff --git a/repos/effect/packages/effect/test/Sink/error-handling.test.ts b/repos/effect/packages/effect/test/Sink/error-handling.test.ts new file mode 100644 index 0000000..591490f --- /dev/null +++ b/repos/effect/packages/effect/test/Sink/error-handling.test.ts @@ -0,0 +1,29 @@ +import { describe, it } from "@effect/vitest" +import { deepStrictEqual } from "@effect/vitest/utils" +import * as Effect from "effect/Effect" +import * as Exit from "effect/Exit" +import { pipe } from "effect/Function" +import * as Sink from "effect/Sink" +import * as Stream from "effect/Stream" + +describe("Sink", () => { + it.effect("propagates errors", () => + Effect.gen(function*() { + const ErrorStream = "ErrorStream" as const + const ErrorMapped = "ErrorMapped" as const + const ErrorSink = "ErrorSink" as const + const result = yield* pipe( + Stream.fail(ErrorStream), + Stream.mapError(() => ErrorMapped), + Stream.run( + pipe( + Sink.drain, + Sink.mapInputEffect((input: number) => Effect.try(() => input)), + Sink.mapError(() => ErrorSink) + ) + ), + Effect.exit + ) + deepStrictEqual(result, Exit.fail(ErrorMapped)) + })) +}) diff --git a/repos/effect/packages/effect/test/Sink/filtering.test.ts b/repos/effect/packages/effect/test/Sink/filtering.test.ts new file mode 100644 index 0000000..33e5186 --- /dev/null +++ b/repos/effect/packages/effect/test/Sink/filtering.test.ts @@ -0,0 +1,42 @@ +import { describe, it } from "@effect/vitest" +import { deepStrictEqual, strictEqual } from "@effect/vitest/utils" +import * as Effect from "effect/Effect" +import { pipe } from "effect/Function" +import * as Sink from "effect/Sink" +import * as Stream from "effect/Stream" + +describe("Sink", () => { + it.effect("filterInput", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.range(1, 9), + Stream.run(pipe(Sink.collectAll(), Sink.filterInput((n) => n % 2 === 0))) + ) + deepStrictEqual(Array.from(result), [2, 4, 6, 8]) + })) + + it.effect("filterInputEffect - happy path", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.range(1, 9), + Stream.run(pipe( + Sink.collectAll(), + Sink.filterInputEffect((n) => Effect.succeed(n % 2 === 0)) + )) + ) + deepStrictEqual(Array.from(result), [2, 4, 6, 8]) + })) + + it.effect("filterInputEffect - error", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.range(1, 9), + Stream.run(pipe( + Sink.collectAll(), + Sink.filterInputEffect(() => Effect.fail("fail")) + )), + Effect.flip + ) + strictEqual(result, "fail") + })) +}) diff --git a/repos/effect/packages/effect/test/Sink/finalization.test.ts b/repos/effect/packages/effect/test/Sink/finalization.test.ts new file mode 100644 index 0000000..0f3b55d --- /dev/null +++ b/repos/effect/packages/effect/test/Sink/finalization.test.ts @@ -0,0 +1,32 @@ +import { describe, it } from "@effect/vitest" +import { assertTrue } from "@effect/vitest/utils" +import * as Effect from "effect/Effect" +import { pipe } from "effect/Function" +import * as Ref from "effect/Ref" +import * as Sink from "effect/Sink" +import * as Stream from "effect/Stream" + +describe("Sink", () => { + it.effect("ensuring - happy path", () => + Effect.gen(function*() { + const ref = yield* Ref.make(false) + yield* pipe( + Stream.make(1, 2, 3, 4, 5), + Stream.run(pipe(Sink.drain, Sink.ensuring(Ref.set(ref, true)))) + ) + const result = yield* Ref.get(ref) + assertTrue(result) + })) + + it.effect("ensuring - error", () => + Effect.gen(function*() { + const ref = yield* Ref.make(false) + yield* pipe( + Stream.fail("boom!"), + Stream.run(pipe(Sink.drain, Sink.ensuring(Ref.set(ref, true)))), + Effect.ignore + ) + const result = yield* Ref.get(ref) + assertTrue(result) + })) +}) diff --git a/repos/effect/packages/effect/test/Sink/folding.test.ts b/repos/effect/packages/effect/test/Sink/folding.test.ts new file mode 100644 index 0000000..ca9d810 --- /dev/null +++ b/repos/effect/packages/effect/test/Sink/folding.test.ts @@ -0,0 +1,247 @@ +import { describe, it } from "@effect/vitest" +import { deepStrictEqual, strictEqual } from "@effect/vitest/utils" +import * as Chunk from "effect/Chunk" +import * as Effect from "effect/Effect" +import * as Exit from "effect/Exit" +import { absurd, constTrue, pipe } from "effect/Function" +import * as Ref from "effect/Ref" +import * as Sink from "effect/Sink" +import * as Stream from "effect/Stream" + +describe("Sink", () => { + it.effect("fold - empty", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.empty, + Stream.transduce(Sink.fold(0, constTrue, (x, y) => x + y)), + Stream.runCollect + ) + deepStrictEqual(Array.from(result), [0]) + })) + + it.effect("fold - termination in the middle", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.range(1, 9), + Stream.run(Sink.fold(0, (n) => n <= 5, (x, y) => x + y)) + ) + strictEqual(result, 6) + })) + + it.effect("fold - immediate termination", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.range(1, 9), + Stream.run(Sink.fold(0, (n) => n <= -1, (x, y) => x + y)) + ) + strictEqual(result, 0) + })) + + it.effect("fold - no termination", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.range(1, 9), + Stream.run(Sink.fold(0, (n) => n <= 500, (x, y) => x + y)) + ) + strictEqual(result, 45) + })) + + it.effect("foldLeft equivalence with Chunk.reduce", () => + Effect.gen(function*() { + const stream = Stream.range(1, 9) + const result1 = yield* pipe(stream, Stream.run(Sink.foldLeft("", (s, n) => s + `${n}`))) + const result2 = yield* pipe(stream, Stream.runCollect, Effect.map(Chunk.reduce("", (s, n) => s + `${n}`))) + strictEqual(result1, result2) + })) + + it.effect("foldEffect - empty", () => + Effect.gen(function*() { + const sink = Sink.foldEffect(0, constTrue, (x, y: number) => Effect.succeed(x + y)) + const result = yield* pipe(Stream.empty, Stream.transduce(sink), Stream.runCollect) + deepStrictEqual(Array.from(result), [0]) + })) + + it.effect("foldEffect - short circuits", () => + Effect.gen(function*() { + const empty: Stream.Stream = Stream.empty + const single = Stream.make(1) + const double = Stream.make(1, 2) + const failed = Stream.fail("Ouch") + const run = (stream: Stream.Stream) => + pipe( + Ref.make(Chunk.empty()), + Effect.flatMap((ref) => + pipe( + stream, + Stream.transduce(Sink.foldEffect( + 0, + constTrue, + (_, y: number) => pipe(Ref.update(ref, Chunk.append(y)), Effect.as(30)) + )), + Stream.runCollect, + Effect.flatMap((exit) => + pipe( + Ref.get(ref), + Effect.map((result) => [Array.from(exit), Array.from(result)]) + ) + ) + ) + ), + Effect.exit + ) + const result1 = yield* run(empty) + const result2 = yield* run(single) + const result3 = yield* run(double) + const result4 = yield* run(failed) + deepStrictEqual(result1, Exit.succeed([[0], []])) + deepStrictEqual(result2, Exit.succeed([[30], [1]])) + deepStrictEqual(result3, Exit.succeed([[30], [1, 2]])) + deepStrictEqual(result4, Exit.fail("Ouch")) + })) + + it.effect("foldUntil", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.make(1, 1, 1, 1, 1, 1), + Stream.transduce(Sink.foldUntil(0, 3, (x, y) => x + y)), + Stream.runCollect + ) + deepStrictEqual(Array.from(result), [3, 3, 0]) + })) + + it.effect("foldUntilEffect", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.make(1, 1, 1, 1, 1, 1), + Stream.transduce(Sink.foldUntilEffect(0, 3, (x, y) => Effect.succeed(x + y))), + Stream.runCollect + ) + deepStrictEqual(Array.from(result), [3, 3, 0]) + })) + + it.effect("foldWeighted", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.make(1, 5, 2, 3), + Stream.transduce(Sink.foldWeighted({ + initial: Chunk.empty(), + maxCost: 12, + cost: (_, n) => n * 2, + body: (acc, curr) => pipe(acc, Chunk.append(curr)) + })), + Stream.runCollect + ) + deepStrictEqual( + Array.from(result).map((chunk) => Array.from(chunk)), + [[1, 5], [2, 3]] + ) + })) + + it.effect("foldWeightedDecompose - empty", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.empty, + Stream.transduce(Sink.foldWeightedDecompose({ + initial: 0, + maxCost: 1_000, + cost: (_, n) => n, + decompose: Chunk.of, + body: (acc, curr) => acc + curr + })), + Stream.runCollect + ) + deepStrictEqual(Array.from(result), [0]) + })) + + it.effect("foldWeightedDecompose - simple", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.make(1, 5, 1), + Stream.transduce(Sink.foldWeightedDecompose({ + initial: Chunk.empty(), + maxCost: 4, + cost: (_, n) => n, + decompose: (n) => n > 1 ? Chunk.make(n - 1, 1) : Chunk.of(n), + body: (acc, curr) => pipe(acc, Chunk.append(curr)) + })), + Stream.runCollect + ) + deepStrictEqual( + Array.from(result).map((chunk) => Array.from(chunk)), + [[1, 3], [1, 1, 1]] + ) + })) + + it.effect("foldWeightedEffect", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.make(1, 5, 2, 3), + Stream.transduce(Sink.foldWeightedEffect({ + initial: Chunk.empty(), + maxCost: 12, + cost: (_, n) => Effect.succeed(n * 2), + body: (acc, curr) => Effect.succeed(pipe(acc, Chunk.append(curr))) + })), + Stream.runCollect + ) + deepStrictEqual( + Array.from(result).map((chunk) => Array.from(chunk)), + [[1, 5], [2, 3]] + ) + })) + + it.effect("foldWeightedDecomposeEffect - empty", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.empty, + Stream.transduce(Sink.foldWeightedDecomposeEffect({ + initial: 0, + maxCost: 1_000, + cost: (_, n) => Effect.succeed(n), + decompose: (input) => Effect.succeed(Chunk.of(input)), + body: (acc, curr) => Effect.succeed(acc + curr) + })), + Stream.runCollect + ) + deepStrictEqual(Array.from(result), [0]) + })) + + it.effect("foldWeightedDecomposeEffect - simple", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.make(1, 5, 1), + Stream.transduce(Sink.foldWeightedDecomposeEffect({ + initial: Chunk.empty(), + maxCost: 4, + cost: (_, n) => Effect.succeed(n), + decompose: (n) => Effect.succeed(n > 1 ? Chunk.make(n - 1, 1) : Chunk.of(n)), + body: (acc, curr) => Effect.succeed(pipe(acc, Chunk.append(curr))) + })), + Stream.runCollect + ) + deepStrictEqual( + Array.from(result).map((chunk) => Array.from(chunk)), + [[1, 3], [1, 1, 1]] + ) + })) + + it.effect("foldSink - handles leftovers", () => + Effect.gen(function*() { + const sink = pipe( + Sink.fail("boom"), + Sink.foldSink({ + onFailure: (err) => + pipe( + Sink.collectAll(), + Sink.map((chunk) => [Array.from(chunk), err] as const) + ), + onSuccess: (_) => absurd, string], number, never, string>>(_) + }) + ) + const result = yield* pipe( + Stream.make(1, 2, 3), + Stream.run(sink) + ) + deepStrictEqual(result, [[1, 2, 3], "boom"]) + })) +}) diff --git a/repos/effect/packages/effect/test/Sink/foreign.test.ts b/repos/effect/packages/effect/test/Sink/foreign.test.ts new file mode 100644 index 0000000..423dbd3 --- /dev/null +++ b/repos/effect/packages/effect/test/Sink/foreign.test.ts @@ -0,0 +1,35 @@ +import { describe, it } from "@effect/vitest" +import { deepStrictEqual } from "@effect/vitest/utils" +import * as Context from "effect/Context" +import * as Effect from "effect/Effect" +import * as Either from "effect/Either" +import * as Exit from "effect/Exit" +import { pipe } from "effect/Function" +import * as Option from "effect/Option" +import * as Random from "effect/Random" +import type * as Sink from "effect/Sink" +import * as Stream from "effect/Stream" +import { unify } from "effect/Unify" + +const runSink = (sink: Sink.Sink) => Stream.run(Effect.void, sink) + +describe("Channel.Foreign", () => { + it.effect("Tag", () => + Effect.gen(function*() { + const tag = Context.GenericTag("number") + const result = yield* pipe(tag, runSink, Effect.provideService(tag, 10)) + deepStrictEqual(result, 10) + })) + + it.effect("Unify", () => + Effect.gen(function*() { + const unifiedEffect = unify((yield* (Random.nextInt)) > 1 ? Effect.succeed(0) : Effect.fail(1)) + const unifiedExit = unify((yield* (Random.nextInt)) > 1 ? Exit.succeed(0) : Exit.fail(1)) + const unifiedEither = unify((yield* (Random.nextInt)) > 1 ? Either.right(0) : Either.left(1)) + const unifiedOption = unify((yield* (Random.nextInt)) > 1 ? Option.some(0) : Option.none()) + deepStrictEqual(yield* (runSink(unifiedEffect)), 0) + deepStrictEqual(yield* (runSink(unifiedExit)), 0) + deepStrictEqual(yield* (runSink(unifiedEither)), 0) + deepStrictEqual(yield* (runSink(unifiedOption)), 0) + })) +}) diff --git a/repos/effect/packages/effect/test/Sink/mapping.test.ts b/repos/effect/packages/effect/test/Sink/mapping.test.ts new file mode 100644 index 0000000..a5b5ebf --- /dev/null +++ b/repos/effect/packages/effect/test/Sink/mapping.test.ts @@ -0,0 +1,191 @@ +import { describe, it } from "@effect/vitest" +import { assertLeft, deepStrictEqual, strictEqual } from "@effect/vitest/utils" +import * as Cause from "effect/Cause" +import * as Chunk from "effect/Chunk" +import * as Effect from "effect/Effect" +import { pipe } from "effect/Function" +import * as Sink from "effect/Sink" +import * as Stream from "effect/Stream" + +describe("Sink", () => { + it.effect("as", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.range(1, 9), + Stream.run(pipe(Sink.succeed(1), Sink.as("as"))) + ) + strictEqual(result, "as") + })) + + it.effect("mapInput - happy path", () => + Effect.gen(function*() { + const sink = pipe( + Sink.collectAll(), + Sink.mapInput((input: string) => Number.parseInt(input)) + ) + const result = yield* pipe(Stream.make("1", "2", "3"), Stream.run(sink)) + deepStrictEqual(Array.from(result), [1, 2, 3]) + })) + + it.effect("mapInput - error", () => + Effect.gen(function*() { + const sink = pipe( + Sink.fail("Ouch"), + Sink.mapInput((input: string) => Number.parseInt(input)) + ) + const result = yield* pipe(Stream.make("1", "2", "3"), Stream.run(sink), Effect.either) + assertLeft(result, "Ouch") + })) + + it.effect("mapInputChunks - happy path", () => + Effect.gen(function*() { + const sink = pipe( + Sink.collectAll(), + Sink.mapInputChunks(Chunk.map((_) => Number.parseInt(_))) + ) + const result = yield* pipe(Stream.make("1", "2", "3"), Stream.run(sink)) + deepStrictEqual(Array.from(result), [1, 2, 3]) + })) + + it.effect("mapInputChunks - error", () => + Effect.gen(function*() { + const sink = pipe( + Sink.fail("Ouch"), + Sink.mapInputChunks(Chunk.map(Number.parseInt)) + ) + const result = yield* pipe(Stream.make("1", "2", "3"), Stream.run(sink), Effect.either) + assertLeft(result, "Ouch") + })) + + it.effect("mapInputEffect - happy path", () => + Effect.gen(function*() { + const sink = pipe( + Sink.collectAll(), + Sink.mapInputEffect((s: string) => Effect.try(() => Number.parseInt(s))) + ) + const result = yield* pipe(Stream.make("1", "2", "3"), Stream.run(sink)) + deepStrictEqual(Array.from(result), [1, 2, 3]) + })) + + it.effect("mapInputEffect - error", () => + Effect.gen(function*() { + const sink = pipe( + Sink.fail("Ouch"), + Sink.mapInputEffect((s: string) => Effect.try(() => Number.parseInt(s))) + ) + const result = yield* pipe(Stream.make("1", "2", "3"), Stream.run(sink), Effect.either) + assertLeft(result, "Ouch") + })) + + it.effect("mapInputEffect - error in transformation", () => + Effect.gen(function*() { + const sink = pipe( + Sink.collectAll(), + Sink.mapInputEffect((s: string) => + Effect.try(() => { + const result = Number.parseInt(s) + if (Number.isNaN(result)) { + throw new Cause.RuntimeException(`Cannot parse "${s}" to an integer`) + } + return result + }) + ) + ) + const result = yield* pipe(Stream.make("1", "a"), Stream.run(sink), Effect.flip) + deepStrictEqual(result.error, new Cause.RuntimeException("Cannot parse \"a\" to an integer")) + })) + + it.effect("mapInputChunksEffect - happy path", () => + Effect.gen(function*() { + const sink = pipe( + Sink.collectAll(), + Sink.mapInputChunksEffect((chunk: Chunk.Chunk) => + pipe( + chunk, + Effect.forEach((s) => Effect.try(() => Number.parseInt(s))), + Effect.map(Chunk.unsafeFromArray) + ) + ) + ) + const result = yield* pipe(Stream.make("1", "2", "3"), Stream.run(sink)) + deepStrictEqual(Array.from(result), [1, 2, 3]) + })) + + it.effect("mapInputChunksEffect - error", () => + Effect.gen(function*() { + const sink = pipe( + Sink.fail("Ouch"), + Sink.mapInputChunksEffect((chunk: Chunk.Chunk) => + pipe( + chunk, + Effect.forEach((s) => Effect.try(() => Number.parseInt(s))), + Effect.map(Chunk.unsafeFromArray) + ) + ) + ) + const result = yield* pipe(Stream.make("1", "2", "3"), Stream.run(sink), Effect.either) + assertLeft(result, "Ouch") + })) + + it.effect("mapInputChunksEffect - error in transformation", () => + Effect.gen(function*() { + const sink = pipe( + Sink.collectAll(), + Sink.mapInputChunksEffect((chunk: Chunk.Chunk) => + pipe( + chunk, + Effect.forEach((s) => + Effect.try(() => { + const result = Number.parseInt(s) + if (Number.isNaN(result)) { + throw new Cause.RuntimeException(`Cannot parse "${s}" to an integer`) + } + return result + }) + ), + Effect.map(Chunk.unsafeFromArray) + ) + ) + ) + const result = yield* pipe(Stream.make("1", "a"), Stream.run(sink), Effect.flip) + deepStrictEqual(result.error, new Cause.RuntimeException("Cannot parse \"a\" to an integer")) + })) + + it.effect("map", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.range(1, 9), + Stream.run(pipe(Sink.succeed(1), Sink.map((n) => `${n}`))) + ) + strictEqual(result, "1") + })) + + it.effect("mapEffect - happy path", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.range(1, 9), + Stream.run(pipe(Sink.succeed(1), Sink.mapEffect((n) => Effect.succeed(n + 1)))) + ) + strictEqual(result, 2) + })) + + it.effect("mapEffect - error", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.range(1, 9), + Stream.run(pipe(Sink.succeed(1), Sink.mapEffect(() => Effect.fail("fail")))), + Effect.flip + ) + strictEqual(result, "fail") + })) + + it.effect("mapError", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.range(1, 9), + Stream.run(pipe(Sink.fail("fail"), Sink.mapError((s) => s + "!"))), + Effect.either + ) + assertLeft(result, "fail!") + })) +}) diff --git a/repos/effect/packages/effect/test/Sink/racing.test.ts b/repos/effect/packages/effect/test/Sink/racing.test.ts new file mode 100644 index 0000000..ce9eff2 --- /dev/null +++ b/repos/effect/packages/effect/test/Sink/racing.test.ts @@ -0,0 +1,78 @@ +import { describe, it } from "@effect/vitest" +import { assertTrue } from "@effect/vitest/utils" +import * as Chunk from "effect/Chunk" +import * as Effect from "effect/Effect" +import * as Either from "effect/Either" +import { constVoid, pipe } from "effect/Function" +import * as Option from "effect/Option" +import * as Random from "effect/Random" +import * as Sink from "effect/Sink" +import * as Stream from "effect/Stream" +import { unfoldEffect } from "../utils/unfoldEffect.js" + +const findSink = (a: A): Sink.Sink => + pipe( + Sink.fold, A>( + Option.none(), + Option.isNone, + (_, v) => (a === v ? Option.some(a) : Option.none()) + ), + Sink.mapEffect(Option.match({ + onNone: () => Effect.failSync(constVoid), + onSome: Effect.succeed + })) + ) + +const sinkRaceLaw = ( + stream: Stream.Stream, + sink1: Sink.Sink, + sink2: Sink.Sink +): Effect.Effect => + pipe( + Effect.all({ + result1: pipe(stream, Stream.run(sink1), Effect.either), + result2: pipe(stream, Stream.run(sink2), Effect.either), + result3: pipe(stream, Stream.run(pipe(sink1, Sink.raceBoth(sink2))), Effect.either) + }), + Effect.map(({ result1, result2, result3 }) => + pipe( + result3, + Either.match({ + onLeft: () => Either.isLeft(result1) || Either.isLeft(result2), + onRight: Either.match({ + onLeft: (a) => Either.isRight(result1) && result1.right === a, + onRight: (a) => Either.isRight(result2) && result2.right === a + }) + }) + ) + ) + ) + +describe("Sink", () => { + it.effect("raceBoth", () => + Effect.gen(function*() { + const ints = yield* unfoldEffect( + 0, + (n) => + Effect.map( + Random.nextIntBetween(0, 10), + (i) => n <= 20 ? Option.some([i, n + 1] as const) : Option.none() + ) + ) + const success1 = yield* (Random.nextBoolean) + const success2 = yield* (Random.nextBoolean) + const chunk = pipe( + Chunk.unsafeFromArray(ints), + Chunk.appendAll(success1 ? Chunk.of(20) : Chunk.empty()), + Chunk.appendAll(success2 ? Chunk.of(40) : Chunk.empty()) + ) + const result = yield* ( + sinkRaceLaw( + Stream.fromIterableEffect(Random.shuffle(chunk)), + findSink(20), + findSink(40) + ) + ) + assertTrue(result) + })) +}) diff --git a/repos/effect/packages/effect/test/Sink/refining.test.ts b/repos/effect/packages/effect/test/Sink/refining.test.ts new file mode 100644 index 0000000..d906bb3 --- /dev/null +++ b/repos/effect/packages/effect/test/Sink/refining.test.ts @@ -0,0 +1,57 @@ +import { describe, it } from "@effect/vitest" +import { deepStrictEqual } from "@effect/vitest/utils" +import * as Cause from "effect/Cause" +import * as Effect from "effect/Effect" +import * as Exit from "effect/Exit" +import { pipe } from "effect/Function" +import * as Option from "effect/Option" +import * as Sink from "effect/Sink" +import * as Stream from "effect/Stream" + +describe("Sink", () => { + it.effect("refineOrDie", () => + Effect.gen(function*() { + const exception = new Cause.RuntimeException() + const refinedTo = "refined" + const sink = pipe( + Sink.fail(exception), + Sink.refineOrDie((error) => + Cause.isRuntimeException(error) ? + Option.some(refinedTo) : + Option.none() + ) + ) + const result = yield* pipe(Stream.make(1, 2, 3), Stream.run(sink), Effect.exit) + deepStrictEqual(result, Exit.fail(refinedTo)) + })) + + it.effect("refineOrDieWith - refines", () => + Effect.gen(function*() { + const exception = new Cause.RuntimeException() + const refinedTo = "refined" + const sink = pipe( + Sink.fail(exception), + Sink.refineOrDieWith((error) => + Cause.isRuntimeException(error) ? + Option.some(refinedTo) : + Option.none(), (error) => error.message) + ) + const result = yield* pipe(Stream.make(1, 2, 3), Stream.run(sink), Effect.exit) + deepStrictEqual(result, Exit.fail(refinedTo)) + })) + + it.effect("refineOrDieWith - dies", () => + Effect.gen(function*() { + const exception = new Cause.RuntimeException() + const refinedTo = "refined" + const sink = pipe( + Sink.fail(exception), + Sink.refineOrDieWith((error) => + Cause.isIllegalArgumentException(error) ? + Option.some(refinedTo) : + Option.none(), (error) => error.message) + ) + const result = yield* pipe(Stream.make(1, 2, 3), Stream.run(sink), Effect.exit) + deepStrictEqual(result, Exit.die("")) + })) +}) diff --git a/repos/effect/packages/effect/test/Sink/scoping.test.ts b/repos/effect/packages/effect/test/Sink/scoping.test.ts new file mode 100644 index 0000000..0963669 --- /dev/null +++ b/repos/effect/packages/effect/test/Sink/scoping.test.ts @@ -0,0 +1,52 @@ +import { describe, it } from "@effect/vitest" +import { assertFalse, assertTrue, strictEqual } from "@effect/vitest/utils" +import * as Effect from "effect/Effect" +import { pipe } from "effect/Function" +import * as Ref from "effect/Ref" +import * as Sink from "effect/Sink" +import * as Stream from "effect/Stream" + +describe("Sink", () => { + it.effect("unwrapScoped - happy path", () => + Effect.gen(function*() { + const ref = yield* (Ref.make(false)) + const resource = Effect.acquireRelease( + Effect.succeed(100), + () => Ref.set(ref, true) + ) + const sink = pipe( + resource, + Effect.map((n) => + pipe( + Sink.count, + Sink.mapEffect((count) => + pipe( + Ref.get(ref), + Effect.map((closed) => [count + n, closed] as const) + ) + ) + ) + ), + Sink.unwrapScoped + ) + const [result, state] = yield* pipe(Stream.make(1, 2, 3), Stream.run(sink)) + const finalState = yield* (Ref.get(ref)) + strictEqual(result, 103) + assertFalse(state) + assertTrue(finalState) + })) + + it.effect("unwrapScoped - error", () => + Effect.gen(function*() { + const ref = yield* (Ref.make(false)) + const resource = Effect.acquireRelease( + Effect.succeed(100), + () => Ref.set(ref, true) + ) + const sink = pipe(resource, Effect.as(Sink.succeed("ok")), Sink.unwrapScoped) + const result = yield* pipe(Stream.fail("fail"), Stream.run(sink)) + const finalState = yield* (Ref.get(ref)) + strictEqual(result, "ok") + assertTrue(finalState) + })) +}) diff --git a/repos/effect/packages/effect/test/Sink/sequencing.test.ts b/repos/effect/packages/effect/test/Sink/sequencing.test.ts new file mode 100644 index 0000000..15b87c1 --- /dev/null +++ b/repos/effect/packages/effect/test/Sink/sequencing.test.ts @@ -0,0 +1,52 @@ +import { describe, it } from "@effect/vitest" +import { assertNone, assertSome, deepStrictEqual, strictEqual } from "@effect/vitest/utils" +import * as Chunk from "effect/Chunk" +import * as Effect from "effect/Effect" +import { pipe } from "effect/Function" +import * as Option from "effect/Option" +import * as Sink from "effect/Sink" +import * as Stream from "effect/Stream" + +describe("Sink", () => { + it.effect("flatMap - empty input", () => + Effect.gen(function*() { + const sink = pipe(Sink.head(), Sink.flatMap(Sink.succeed)) + const result = yield* pipe(Stream.empty, Stream.run(sink)) + assertNone(result) + })) + + it.effect("flatMap - non-empty input", () => + Effect.gen(function*() { + const sink = pipe(Sink.head(), Sink.flatMap(Sink.succeed)) + const result = yield* pipe(Stream.make(1, 2, 3), Stream.run(sink)) + assertSome(result, 1) + })) + + it.effect("flatMap - with leftovers", () => + Effect.gen(function*() { + const chunks = Chunk.make( + Chunk.make(1, 2), + Chunk.make(3, 4, 5), + Chunk.empty(), + Chunk.make(7, 8, 9, 10) + ) + const sink = pipe( + Sink.head(), + Sink.flatMap((head) => + pipe( + Sink.count, + Sink.map((count) => [head, count] as const) + ) + ) + ) + const [option, count] = yield* pipe(Stream.fromChunks(...chunks), Stream.run(sink)) + deepStrictEqual(option, Chunk.head(Chunk.flatten(chunks))) + strictEqual( + count + Option.match(option, { + onNone: () => 0, + onSome: () => 1 + }), + pipe(chunks, Chunk.map(Chunk.size), Chunk.reduce(0, (a, b) => a + b)) + ) + })) +}) diff --git a/repos/effect/packages/effect/test/Sink/traversing.test.ts b/repos/effect/packages/effect/test/Sink/traversing.test.ts new file mode 100644 index 0000000..dc120b2 --- /dev/null +++ b/repos/effect/packages/effect/test/Sink/traversing.test.ts @@ -0,0 +1,133 @@ +import { describe, it } from "@effect/vitest" +import { assertNone, assertTrue, deepStrictEqual, strictEqual } from "@effect/vitest/utils" +import * as Chunk from "effect/Chunk" +import * as Effect from "effect/Effect" +import * as Equal from "effect/Equal" +import { identity, pipe } from "effect/Function" +import * as Option from "effect/Option" +import * as Sink from "effect/Sink" +import * as Stream from "effect/Stream" + +describe("Sink", () => { + it.effect("findEffect - with head sink", () => + Effect.gen(function*() { + const sink = pipe( + Sink.head(), + Sink.findEffect(Option.match({ + onNone: () => Effect.succeed(false), + onSome: (n) => Effect.succeed(n >= 10) + })) + ) + const result = yield* pipe( + [1, 3, 7, 20], + Effect.forEach((n) => + pipe( + Stream.range(1, 99), + Stream.rechunk(n), + Stream.run(sink), + Effect.map((option) => Equal.equals(option, Option.some(Option.some(10)))) + ) + ) + ) + assertTrue(result.every(identity)) + })) + + it.effect("findEffect - take sink across multiple chunks", () => + Effect.gen(function*() { + const sink = pipe( + Sink.take(4), + Sink.findEffect((chunk) => Effect.succeed(pipe(chunk, Chunk.reduce(0, (x, y) => x + y)) > 10)) + ) + const result = yield* pipe( + Stream.fromIterable(Chunk.range(1, 8)), + Stream.rechunk(2), + Stream.run(sink), + Effect.map(Option.getOrElse(() => Chunk.empty())) + ) + deepStrictEqual(Array.from(result), [5, 6, 7, 8]) + })) + + it.effect("findEffect - empty stream terminates with none", () => + Effect.gen(function*() { + const sink = pipe( + Sink.sum, + Sink.findEffect((n) => Effect.succeed(n > 0)) + ) + const result = yield* pipe( + Stream.fromIterable([]), + Stream.run(sink) + ) + assertNone(result) + })) + + it.effect("findEffect - unsatisfied condition terminates with none", () => + Effect.gen(function*() { + const sink = pipe( + Sink.head(), + Sink.findEffect(Option.match({ + onNone: () => Effect.succeed(false), + onSome: (n) => Effect.succeed(n >= 3) + })) + ) + const result = yield* pipe( + Stream.fromIterable([1, 2]), + Stream.run(sink) + ) + assertNone(result) + })) + + it.effect("forEachWhile - handles leftovers", () => + Effect.gen(function*() { + const [result, value] = yield* pipe( + Stream.range(1, 4), + Stream.run(pipe( + Sink.forEachWhile((n: number) => Effect.succeed(n <= 3)), + Sink.collectLeftover + )) + ) + strictEqual(result, undefined) + deepStrictEqual(Array.from(value), [4]) + })) + + it.effect("splitWhere - should split a stream on a predicate and run each part into the sink", () => + Effect.gen(function*() { + const stream = Stream.make(1, 2, 3, 4, 5, 6, 7, 8) + const result = yield* pipe( + stream, + Stream.transduce(pipe(Sink.collectAll(), Sink.splitWhere((n) => n % 2 === 0))), + Stream.runCollect + ) + deepStrictEqual( + Array.from(result).map((chunk) => Array.from(chunk)), + [[1], [2, 3], [4, 5], [6, 7], [8]] + ) + })) + + it.effect("splitWhere - should split a stream on a predicate and run each part into the sink, in several chunks", () => + Effect.gen(function*() { + const stream = Stream.fromChunks(Chunk.make(1, 2, 3, 4), Chunk.make(5, 6, 7, 8)) + const result = yield* pipe( + stream, + Stream.transduce(pipe(Sink.collectAll(), Sink.splitWhere((n) => n % 2 === 0))), + Stream.runCollect + ) + deepStrictEqual( + Array.from(result).map((chunk) => Array.from(chunk)), + [[1], [2, 3], [4, 5], [6, 7], [8]] + ) + })) + + it.effect("splitWhere - should not yield an empty sink if split on the first element", () => + Effect.gen(function*() { + const stream = Stream.make(1, 2, 3, 4, 5, 6, 7, 8) + const result = yield* pipe( + stream, + Stream.transduce(pipe(Sink.collectAll(), Sink.splitWhere((n) => n % 2 !== 0))), + Stream.runCollect + ) + deepStrictEqual( + Array.from(result).map((chunk) => Array.from(chunk)), + [[1, 2], [3, 4], [5, 6], [7, 8]] + ) + })) +}) diff --git a/repos/effect/packages/effect/test/Sink/zipping.test.ts b/repos/effect/packages/effect/test/Sink/zipping.test.ts new file mode 100644 index 0000000..5635e3b --- /dev/null +++ b/repos/effect/packages/effect/test/Sink/zipping.test.ts @@ -0,0 +1,93 @@ +import { describe, it } from "@effect/vitest" +import { assertSome, assertTrue, strictEqual } from "@effect/vitest/utils" +import * as Chunk from "effect/Chunk" +import * as Effect from "effect/Effect" +import * as Either from "effect/Either" +import { constVoid, pipe } from "effect/Function" +import * as Option from "effect/Option" +import * as Random from "effect/Random" +import * as Sink from "effect/Sink" +import * as Stream from "effect/Stream" +import { unfoldEffect } from "../utils/unfoldEffect.js" + +const findSink = (a: A): Sink.Sink => + pipe( + Sink.fold, A>( + Option.none(), + Option.isNone, + (_, v) => (a === v ? Option.some(a) : Option.none()) + ), + Sink.mapEffect(Option.match({ + onNone: () => Effect.failSync(constVoid), + onSome: Effect.succeed + })) + ) + +const zipParLaw = ( + stream: Stream.Stream, + sink1: Sink.Sink, + sink2: Sink.Sink +): Effect.Effect => + pipe( + Effect.all({ + zb: pipe(stream, Stream.run(sink1), Effect.either), + zc: pipe(stream, Stream.run(sink2), Effect.either), + zbc: pipe(stream, Stream.run(pipe(sink1, Sink.zip(sink2, { concurrent: true }))), Effect.either) + }), + Effect.map(({ zb, zbc, zc }) => + Either.match(zbc, { + onLeft: (e) => (Either.isLeft(zb) && zb.left === e) || (Either.isLeft(zc) && zc.left === e), + onRight: ([b, c]) => Either.isRight(zb) && zb.right === b && Either.isRight(zc) && zc.right === c + }) + ) + ) + +describe("Sink", () => { + it.effect("zipParLeft", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.make(1, 2, 3), + Stream.run(pipe( + Sink.head(), + Sink.zipLeft(Sink.succeed("hello"), { concurrent: true }) + )) + ) + assertSome(result, 1) + })) + + it.effect("zipParRight", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.make(1, 2, 3), + Stream.run(pipe( + Sink.head(), + Sink.zipRight(Sink.succeed("hello"), { concurrent: true }) + )) + ) + strictEqual(result, "hello") + })) + + it.effect("zipWithPar - coherence", () => + Effect.gen(function*() { + const ints = yield* (unfoldEffect(0, (n) => + pipe( + Random.nextIntBetween(0, 10), + Effect.map((i) => n < 20 ? Option.some([i, n + 1] as const) : Option.none()) + ))) + const success1 = yield* (Random.nextBoolean) + const success2 = yield* (Random.nextBoolean) + const chunk = pipe( + Chunk.unsafeFromArray(ints), + Chunk.appendAll(success1 ? Chunk.of(20) : Chunk.empty()), + Chunk.appendAll(success2 ? Chunk.of(40) : Chunk.empty()) + ) + const result = yield* ( + zipParLaw( + Stream.fromIterableEffect(Random.shuffle(chunk)), + findSink(20), + findSink(40) + ) + ) + assertTrue(result) + })) +}) diff --git a/repos/effect/packages/effect/test/SortedMap.test.ts b/repos/effect/packages/effect/test/SortedMap.test.ts new file mode 100644 index 0000000..eeae234 --- /dev/null +++ b/repos/effect/packages/effect/test/SortedMap.test.ts @@ -0,0 +1,304 @@ +import { describe, it } from "@effect/vitest" +import { assertFalse, assertNone, assertSome, assertTrue, deepStrictEqual, strictEqual } from "@effect/vitest/utils" +import { Equal, Hash, Number as Num, pipe, SortedMap as SM } from "effect" + +class Key implements Equal.Equal { + constructor(readonly id: number) {} + + [Hash.symbol](): number { + return Hash.hash(this.id) + } + + [Equal.symbol](u: unknown): boolean { + return u instanceof Key && this.id === u.id + } +} + +class Value implements Equal.Equal { + constructor(readonly id: number) {} + + [Hash.symbol](): number { + return Hash.hash(this.id) + } + + [Equal.symbol](u: unknown): boolean { + return u instanceof Value && this.id === u.id + } +} + +function key(n: number): Key { + return new Key(n) +} + +function value(n: number): Value { + return new Value(n) +} + +function makeSortedMap(...numbers: Array): SM.SortedMap { + const entries = numbers.map(([k, v]) => [key(k), value(v)] as const) + return SM.fromIterable(entries, (self: Key, that: Key) => self.id > that.id ? 1 : self.id < that.id ? -1 : 0) +} + +function makeNumericSortedMap( + ...numbers: Array +): SM.SortedMap { + return SM.fromIterable(numbers, (self: number, that: number) => self > that ? 1 : self < that ? -1 : 0) +} + +describe("SortedMap", () => { + it("toString", () => { + const map = makeNumericSortedMap([0, 10], [1, 20], [2, 30]) + + strictEqual( + String(map), + `{ + "_id": "SortedMap", + "values": [ + [ + 0, + 10 + ], + [ + 1, + 20 + ], + [ + 2, + 30 + ] + ] +}` + ) + }) + + it("toJSON", () => { + const map = makeNumericSortedMap([0, 10], [1, 20], [2, 30]) + + deepStrictEqual(map.toJSON(), { _id: "SortedMap", values: [[0, 10], [1, 20], [2, 30]] }) + }) + + it("inspect", () => { + if (typeof window !== "undefined") { + return + } + // eslint-disable-next-line @typescript-eslint/no-require-imports + const { inspect } = require("node:util") + + const map = makeNumericSortedMap([0, 10], [1, 20], [2, 30]) + + deepStrictEqual(inspect(map), inspect({ _id: "SortedMap", values: [[0, 10], [1, 20], [2, 30]] })) + }) + + it("entries", () => { + const map = makeSortedMap([0, 10], [1, 20], [2, 30]) + + const result = Array.from(map) + + deepStrictEqual([ + [key(0), value(10)], + [key(1), value(20)], + [key(2), value(30)] + ], result) + }) + + it("get", () => { + const map = makeSortedMap([0, 10], [1, 20], [2, 30]) + + assertSome(pipe(map, SM.get(key(0))), value(10)) + assertNone(pipe(map, SM.get(key(4)))) + }) + + it("has", () => { + const map = makeSortedMap([0, 10], [1, 20], [2, 30]) + + assertTrue(pipe(map, SM.has(key(0)))) + assertFalse(pipe(map, SM.has(key(4)))) + }) + + it("headOption", () => { + const map1 = makeSortedMap([0, 10], [1, 20], [2, 30]) + const map2 = SM.empty(Num.Order) + + assertSome(SM.headOption(map1), [key(0), value(10)]) + assertNone(SM.headOption(map2)) + }) + + it("lastOption", () => { + const map1 = makeSortedMap([0, 10], [1, 20], [2, 30]) + const map2 = SM.empty(Num.Order) + + assertSome(SM.lastOption(map1), [key(2), value(30)]) + assertNone(SM.lastOption(map2)) + }) + + it("isEmpty", () => { + const map1 = makeSortedMap([0, 10], [1, 20], [2, 30]) + const map2 = SM.empty(Num.Order) + + assertFalse(SM.isEmpty(map1)) + assertTrue(SM.isEmpty(map2)) + }) + + it("isNonEmpty", () => { + const map1 = makeSortedMap([0, 10], [1, 20], [2, 30]) + const map2 = SM.empty(Num.Order) + + assertTrue(SM.isNonEmpty(map1)) + assertFalse(SM.isNonEmpty(map2)) + }) + + it("map", () => { + const map1 = makeSortedMap([0, 10], [1, 20], [2, 30]) + + const result1 = Array.from(pipe(map1, SM.map((value) => value.id))) + + deepStrictEqual( + result1, + [ + [key(0), 10], + [key(1), 20], + [key(2), 30] + ] + ) + + const map2 = makeSortedMap([0, 10], [1, 20], [2, 30]) + + const result2 = Array.from(pipe(map2, SM.map((key, value) => key.id + value.id))) + + deepStrictEqual( + result2, + [ + [key(0), 10], + [key(1), 21], + [key(2), 32] + ] + ) + }) + + it("partition", () => { + const map1 = makeSortedMap([1, 10], [2, 20], [3, 30], [4, 40], [5, 50]) + + const [excl, satisfying] = pipe( + map1, + SM.partition((member) => member.id <= 3) + ) + + deepStrictEqual( + Array.from(satisfying), + [ + [key(1), value(10)], + [key(2), value(20)], + [key(3), value(30)] + ] + ) + deepStrictEqual( + Array.from(excl), + [ + [key(4), value(40)], + [key(5), value(50)] + ] + ) + + const [excl2, satisfying2] = pipe( + map1, + SM.partition((member) => member.id <= 6) + ) + + deepStrictEqual( + Array.from(satisfying2), + [ + [key(1), value(10)], + [key(2), value(20)], + [key(3), value(30)], + [key(4), value(40)], + [key(5), value(50)] + ] + ) + + deepStrictEqual( + Array.from(excl2), + [] + ) + + const [excl3, satisfying3] = pipe( + map1, + SM.partition((member) => member.id === 0) + ) + + deepStrictEqual( + Array.from(excl3), + [ + [key(1), value(10)], + [key(2), value(20)], + [key(3), value(30)], + [key(4), value(40)], + [key(5), value(50)] + ] + ) + + deepStrictEqual( + Array.from(satisfying3), + [] + ) + }) + + it("reduce", () => { + const map1 = makeSortedMap([0, 10], [1, 20], [2, 30]) + const result1 = pipe(map1, SM.reduce("", (acc, value) => acc + value.id)) + strictEqual(result1, "102030") + + const map2 = makeSortedMap([0, 10], [1, 20], [2, 30]) + const result2 = pipe(map2, SM.reduce("", (acc, value, key) => acc + key.id + value.id)) + strictEqual(result2, "010120230") + }) + + it("remove", () => { + const map = makeSortedMap([0, 10], [1, 20], [2, 30]) + + assertTrue(pipe(map, SM.has(key(0)))) + + const result1 = pipe(map, SM.remove(key(0))) + + assertFalse(pipe(result1, SM.has(key(0)))) + }) + + it("set", () => { + const map = makeSortedMap([0, 10], [1, 20], [2, 30]) + + assertFalse(pipe(map, SM.has(key(4)))) + + const result1 = pipe(map, SM.set(key(4), value(40))) + + assertTrue(pipe(result1, SM.has(key(4)))) + }) + + it("size", () => { + const map = makeSortedMap([0, 10], [1, 20], [2, 30]) + + strictEqual(SM.size(map), 3) + }) + + it("keys", () => { + const map = makeSortedMap([0, 10], [1, 20], [2, 30]) + + const result = Array.from(SM.keys(map)) + + deepStrictEqual(result, [key(0), key(1), key(2)]) + }) + + it("values", () => { + const map = makeSortedMap([0, 10], [1, 20], [2, 30]) + + const result = Array.from(SM.values(map)) + + deepStrictEqual(result, [value(10), value(20), value(30)]) + }) + + it("entries", () => { + const map = makeSortedMap([0, 10], [1, 20], [2, 30]) + + const result = Array.from(SM.entries(map)) + + deepStrictEqual(result, [[key(0), value(10)], [key(1), value(20)], [key(2), value(30)]]) + }) +}) diff --git a/repos/effect/packages/effect/test/SortedSet.test.ts b/repos/effect/packages/effect/test/SortedSet.test.ts new file mode 100644 index 0000000..368bc54 --- /dev/null +++ b/repos/effect/packages/effect/test/SortedSet.test.ts @@ -0,0 +1,430 @@ +import { describe, it } from "@effect/vitest" +import { assertFalse, assertTrue, deepStrictEqual, strictEqual } from "@effect/vitest/utils" +import { Equal, Hash, Order, pipe, SortedSet, String as Str } from "effect" + +class Member implements Equal.Equal { + constructor(readonly id: string) {} + + [Hash.symbol](): number { + return Hash.hash(this.id) + } + + [Equal.symbol](u: unknown): boolean { + return u instanceof Member && this.id === u.id + } +} + +const OrdMember: Order.Order = pipe(Str.Order, Order.mapInput((member) => member.id)) + +function makeNumericSortedSet( + ...numbers: Array +): SortedSet.SortedSet { + return SortedSet.fromIterable(numbers, (self, that: number) => self > that ? 1 : self < that ? -1 : 0) +} + +describe("SortedSet", () => { + it("fromIterable", () => { + deepStrictEqual(Array.from(SortedSet.fromIterable(["c", "a", "b"], Str.Order)), ["a", "b", "c"]) + deepStrictEqual(Array.from(pipe(["c", "a", "b"], SortedSet.fromIterable(Str.Order))), ["a", "b", "c"]) + }) + + it("is", () => { + const set = makeNumericSortedSet(0, 1, 2) + const arr = Array.from(set) + assertTrue(SortedSet.isSortedSet(set)) + assertFalse(SortedSet.isSortedSet(arr)) + }) + + it("toString", () => { + const set = makeNumericSortedSet(0, 1, 2) + strictEqual( + String(set), + `{ + "_id": "SortedSet", + "values": [ + 0, + 1, + 2 + ] +}` + ) + }) + + it("toJSON", () => { + const set = makeNumericSortedSet(0, 1, 2) + deepStrictEqual(set.toJSON(), { _id: "SortedSet", values: [0, 1, 2] }) + }) + + it("inspect", () => { + if (typeof window !== "undefined") { + return + } + // eslint-disable-next-line @typescript-eslint/no-require-imports + const { inspect } = require("node:util") + const set = makeNumericSortedSet(0, 1, 2) + deepStrictEqual(inspect(set), inspect({ _id: "SortedSet", values: [0, 1, 2] })) + }) + + it("add", () => { + const set = pipe( + SortedSet.empty(OrdMember), + SortedSet.add(new Member("worker_000000")), + SortedSet.add(new Member("worker_000001")), + SortedSet.add(new Member("worker_000001")), + SortedSet.add(new Member("worker_000002")), + SortedSet.add(new Member("worker_000001")) + ) + + deepStrictEqual( + Array.from(set), + [ + new Member("worker_000000"), + new Member("worker_000001"), + new Member("worker_000002") + ] + ) + }) + + it("difference", () => { + const set1 = pipe( + SortedSet.empty(OrdMember), + SortedSet.add(new Member("worker_000000")), + SortedSet.add(new Member("worker_000001")), + SortedSet.add(new Member("worker_000002")) + ) + + const set2 = [ + new Member("worker_000001"), + new Member("worker_000002"), + new Member("worker_000003") + ] + + const set3 = [ + new Member("worker_000000"), + new Member("worker_000001"), + new Member("worker_000002") + ] + + deepStrictEqual( + Array.from(pipe( + set1, + SortedSet.difference(set2) + )), + [new Member("worker_000000")] + ) + deepStrictEqual( + Array.from(pipe(set1, SortedSet.difference(set3))), + [] + ) + }) + + it("every", () => { + const isWorker = (member: Member) => member.id.indexOf("worker") !== -1 + const isWorker1 = (member: Member) => member.id === "worker_000001" + const set = pipe( + SortedSet.empty(OrdMember), + SortedSet.add(new Member("worker_000000")), + SortedSet.add(new Member("worker_000001")), + SortedSet.add(new Member("worker_000002")) + ) + + const result1 = pipe(set, SortedSet.every(isWorker)) + const result2 = pipe(set, SortedSet.every(isWorker1)) + + assertTrue(result1) + assertFalse(result2) + }) + + it("some", () => { + const isWorker1 = (member: Member) => member.id === "worker_000001" + const isWorker4 = (member: Member) => member.id === "worker_000004" + const set = pipe( + SortedSet.empty(OrdMember), + SortedSet.add(new Member("worker_000000")), + SortedSet.add(new Member("worker_000001")), + SortedSet.add(new Member("worker_000002")) + ) + + const result1 = pipe(set, SortedSet.some(isWorker1)) + const result2 = pipe(set, SortedSet.some(isWorker4)) + + assertTrue(result1) + assertFalse(result2) + }) + + it("filter", () => { + const isWorker1 = (member: Member) => member.id === "worker_000001" + const set = pipe( + SortedSet.empty(OrdMember), + SortedSet.add(new Member("worker_000000")), + SortedSet.add(new Member("worker_000001")), + SortedSet.add(new Member("worker_000002")) + ) + + const result = pipe(set, SortedSet.filter(isWorker1)) + + deepStrictEqual(Array.from(result), [new Member("worker_000001")]) + }) + + it("flatMap", () => { + const set1 = pipe( + SortedSet.empty(OrdMember), + SortedSet.add(new Member("worker_000000")), + SortedSet.add(new Member("worker_000001")), + SortedSet.add(new Member("worker_000002")) + ) + + const set2 = [ + new Member("worker_000001"), + new Member("worker_000002"), + new Member("worker_000003") + ] + + const result = pipe(set1, SortedSet.flatMap(OrdMember, (a) => [...set2, a])) + + deepStrictEqual( + Array.from(result), + [ + new Member("worker_000000"), + new Member("worker_000001"), + new Member("worker_000002"), + new Member("worker_000003") + ] + ) + }) + + it("forEach", () => { + const set1 = pipe( + SortedSet.empty(OrdMember), + SortedSet.add(new Member("worker_000000")), + SortedSet.add(new Member("worker_000001")), + SortedSet.add(new Member("worker_000002")) + ) + + const result: Array = [] + + pipe( + set1, + SortedSet.forEach((member) => { + result.push(member.id) + }) + ) + + deepStrictEqual(result, ["worker_000000", "worker_000001", "worker_000002"]) + }) + + it("has", () => { + const set = pipe( + SortedSet.empty(OrdMember), + SortedSet.add(new Member("worker_000000")), + SortedSet.add(new Member("worker_000001")), + SortedSet.add(new Member("worker_000002")) + ) + + assertTrue(pipe(set, SortedSet.has(new Member("worker_000000")))) + assertFalse(pipe(set, SortedSet.has(new Member("worker_000004")))) + }) + + it("intersection", () => { + const set1 = pipe( + SortedSet.empty(OrdMember), + SortedSet.add(new Member("worker_000000")), + SortedSet.add(new Member("worker_000001")), + SortedSet.add(new Member("worker_000002")) + ) + + const set2 = [ + new Member("worker_000001"), + new Member("worker_000002"), + new Member("worker_000003") + ] + + const set3 = [ + new Member("worker_000005") + ] + + const result1 = pipe(set1, SortedSet.intersection(set2)) + const result2 = pipe(set1, SortedSet.intersection(set3)) + + deepStrictEqual( + Array.from(result1), + [ + new Member("worker_000001"), + new Member("worker_000002") + ] + ) + deepStrictEqual(Array.from(result2), []) + }) + + it("isSubset", () => { + const set1 = pipe( + SortedSet.empty(OrdMember), + SortedSet.add(new Member("worker_000000")), + SortedSet.add(new Member("worker_000001")), + SortedSet.add(new Member("worker_000002")) + ) + + const set2 = pipe( + SortedSet.empty(OrdMember), + SortedSet.add(new Member("worker_000001")), + SortedSet.add(new Member("worker_000002")) + ) + + const set3 = pipe( + SortedSet.empty(OrdMember), + SortedSet.add(new Member("worker_000005")) + ) + + assertTrue(pipe(set2, SortedSet.isSubset(set1))) + assertFalse(pipe(set3, SortedSet.isSubset(set1))) + }) + + it("map", () => { + const set = pipe( + SortedSet.empty(OrdMember), + SortedSet.add(new Member("worker_000000")), + SortedSet.add(new Member("worker_000001")), + SortedSet.add(new Member("worker_000002")) + ) + + const result = pipe( + set, + SortedSet.map(Str.Order, (member) => member.id.replace(/_\d+/g, "")) + ) + + deepStrictEqual(Array.from(result), ["worker"]) + }) + + it("partition", () => { + const set = pipe( + SortedSet.empty(OrdMember), + SortedSet.add(new Member("worker_000000")), + SortedSet.add(new Member("worker_000001")), + SortedSet.add(new Member("worker_000002")), + SortedSet.add(new Member("worker_000003")) + ) + + const result = pipe( + set, + SortedSet.partition((member) => member.id.endsWith("1") || member.id.endsWith("3")) + ) + + deepStrictEqual( + Array.from(result[0]), + [ + new Member("worker_000000"), + new Member("worker_000002") + ] + ) + deepStrictEqual( + Array.from(result[1]), + [ + new Member("worker_000001"), + new Member("worker_000003") + ] + ) + }) + + it("remove", () => { + const set = pipe( + SortedSet.empty(OrdMember), + SortedSet.add(new Member("worker_000000")), + SortedSet.add(new Member("worker_000001")), + SortedSet.add(new Member("worker_000002")) + ) + + const result = pipe(set, SortedSet.remove(new Member("worker_000000"))) + + deepStrictEqual( + Array.from(result), + [ + new Member("worker_000001"), + new Member("worker_000002") + ] + ) + }) + + it("size", () => { + const set = pipe( + SortedSet.empty(OrdMember), + SortedSet.add(new Member("worker_000000")), + SortedSet.add(new Member("worker_000001")), + SortedSet.add(new Member("worker_000002")) + ) + + strictEqual(SortedSet.size(set), 3) + }) + + it("toggle", () => { + const member = new Member("worker_000000") + let set = pipe( + SortedSet.empty(OrdMember), + SortedSet.add(new Member("worker_000000")), + SortedSet.add(new Member("worker_000001")), + SortedSet.add(new Member("worker_000002")) + ) + + assertTrue(pipe(set, SortedSet.has(member))) + + set = pipe(set, SortedSet.toggle(member)) + + assertFalse(pipe(set, SortedSet.has(member))) + + set = pipe(set, SortedSet.toggle(member)) + + assertTrue(pipe(set, SortedSet.has(member))) + }) + + it("union", () => { + const set1 = pipe( + SortedSet.empty(OrdMember), + SortedSet.add(new Member("worker_000000")), + SortedSet.add(new Member("worker_000001")), + SortedSet.add(new Member("worker_000002")) + ) + + const set2 = pipe( + SortedSet.empty(OrdMember), + SortedSet.add(new Member("worker_000001")), + SortedSet.add(new Member("worker_000002")), + SortedSet.add(new Member("worker_000003")) + ) + + const set3: Array = [] + + const result1 = pipe(set1, SortedSet.union(set2)) + const result2 = pipe(set1, SortedSet.union(set3)) + + deepStrictEqual( + Array.from(result1), + [ + new Member("worker_000000"), + new Member("worker_000001"), + new Member("worker_000002"), + new Member("worker_000003") + ] + ) + deepStrictEqual(result2, set1) + }) + + it("values", () => { + const set = SortedSet.make(Str.Order)("c", "a", "b") + const values = SortedSet.values(set) + deepStrictEqual(Array.from(values), ["a", "b", "c"]) + }) + + it("pipe()", () => { + strictEqual(SortedSet.make(Str.Order)("c", "a", "b").pipe(SortedSet.size), 3) + }) + + it("Equal.symbol", () => { + assertTrue(Equal.equals(SortedSet.empty(Str.Order), SortedSet.empty(Str.Order))) + const set1 = SortedSet.make(Str.Order)("c", "a", "b") + const set2 = SortedSet.make(Str.Order)("c", "a", "b") + const set3 = SortedSet.make(Str.Order)("d", "b", "a") + assertTrue(Equal.equals(set1, set2)) + assertTrue(Equal.equals(set2, set1)) + assertFalse(Equal.equals(set1, set3)) + assertFalse(Equal.equals(set3, set1)) + }) +}) diff --git a/repos/effect/packages/effect/test/Stream/aggregation.test.ts b/repos/effect/packages/effect/test/Stream/aggregation.test.ts new file mode 100644 index 0000000..b129966 --- /dev/null +++ b/repos/effect/packages/effect/test/Stream/aggregation.test.ts @@ -0,0 +1,356 @@ +import { describe, it } from "@effect/vitest" +import { assertTrue, deepStrictEqual } from "@effect/vitest/utils" +import * as Cause from "effect/Cause" +import * as Chunk from "effect/Chunk" +import * as Deferred from "effect/Deferred" +import * as Duration from "effect/Duration" +import * as Effect from "effect/Effect" +import * as Either from "effect/Either" +import * as Exit from "effect/Exit" +import * as Fiber from "effect/Fiber" +import { constTrue, constVoid, pipe } from "effect/Function" +import * as Option from "effect/Option" +import * as Queue from "effect/Queue" +import * as Ref from "effect/Ref" +import * as Schedule from "effect/Schedule" +import * as Sink from "effect/Sink" +import * as Stream from "effect/Stream" +import * as Take from "effect/Take" +import * as TestClock from "effect/TestClock" +import * as TestServices from "effect/TestServices" +import { chunkCoordination } from "../utils/coordination.js" + +describe("Stream", () => { + it.effect("aggregate - simple example", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.make(1, 1, 1, 1), + Stream.aggregate( + Sink.foldUntil(Chunk.empty(), 3, Chunk.prepend) + ), + Stream.runCollect + ) + deepStrictEqual(Array.from(Chunk.flatten(result)), [1, 1, 1, 1]) + assertTrue(Array.from(result).every((chunk) => chunk.length <= 3)) + })) + + it.effect("aggregate - error propagation #1", () => + Effect.gen(function*() { + const error = new Cause.RuntimeException("Boom") + const result = yield* pipe( + Stream.make(1, 1, 1, 1), + Stream.aggregate(Sink.die(error)), + Stream.runCollect, + Effect.exit + ) + deepStrictEqual(result, Exit.die(error)) + })) + + it.effect("aggregate - error propagation #2", () => + Effect.gen(function*() { + const error = new Cause.RuntimeException("Boom") + const result = yield* pipe( + Stream.make(1, 1), + Stream.aggregate( + Sink.foldLeftEffect(Chunk.empty(), () => Effect.die(error)) + ), + Stream.runCollect, + Effect.exit + ) + deepStrictEqual(result, Exit.die(error)) + })) + + it.effect("aggregate - interruption propagation #1", () => + Effect.gen(function*() { + const latch = yield* (Deferred.make()) + const ref = yield* (Ref.make(false)) + const sink = Sink.foldEffect(Chunk.empty(), constTrue, (acc, curr) => { + if (curr === 1) { + return Effect.succeed(Chunk.prepend(acc, curr)) + } + return pipe( + Deferred.succeed(latch, void 0), + Effect.zipRight(Effect.never), + Effect.onInterrupt(() => Ref.set(ref, true)) + ) + }) + const fiber = yield* pipe( + Stream.make(1, 1, 2), + Stream.aggregate(sink), + Stream.runCollect, + Effect.fork + ) + yield* (Deferred.await(latch)) + yield* (Fiber.interrupt(fiber)) + const result = yield* (Ref.get(ref)) + assertTrue(result) + })) + + it.effect("aggregate - interruption propagation #2", () => + Effect.gen(function*() { + const latch = yield* (Deferred.make()) + const ref = yield* (Ref.make(false)) + const sink = Sink.fromEffect(pipe( + Deferred.succeed(latch, void 0), + Effect.zipRight(Effect.never), + Effect.onInterrupt(() => Ref.set(ref, true)) + )) + const fiber = yield* pipe( + Stream.make(1, 1, 2), + Stream.aggregate(sink), + Stream.runCollect, + Effect.fork + ) + yield* (Deferred.await(latch)) + yield* (Fiber.interrupt(fiber)) + const result = yield* (Ref.get(ref)) + assertTrue(result) + })) + + it.effect("aggregate - leftover handling", () => + Effect.gen(function*() { + const input = [1, 2, 2, 3, 2, 3] + const result = yield* pipe( + Stream.fromIterable(input), + Stream.aggregate(Sink.foldWeighted({ + initial: Chunk.empty(), + maxCost: 4, + cost: (_, n) => n, + body: (acc, curr) => Chunk.append(acc, curr) + })), + Stream.runCollect + ) + deepStrictEqual(Array.from(Chunk.flatten(result)), input) + })) + + it.effect("aggregate - ZIO issue 6395", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.make(1, 2, 3), + Stream.aggregate(Sink.collectAllN(2)), + Stream.runCollect + ) + deepStrictEqual( + Array.from(result).map((chunk) => Array.from(chunk)), + [[1, 2], [3]] + ) + })) + + // Explicitly uses live Clock + it.effect("issue from zio-kafka", () => + Effect.gen(function*() { + const queue = yield* (Queue.unbounded>()) + const fiber = yield* pipe( + Stream.fromQueue(queue), + Stream.flattenTake, + Stream.aggregate( + Sink.foldLeft(Chunk.empty(), (acc, n) => Chunk.append(acc, n)) + ), + Stream.runCollect, + Effect.fork + ) + yield* (TestServices.provideLive(Effect.sleep(Duration.seconds(1)))) + yield* (Queue.offer(queue, Take.chunk(Chunk.make(1, 2, 3, 4, 5)))) + yield* (TestServices.provideLive(Effect.sleep(Duration.seconds(1)))) + yield* (Queue.offer(queue, Take.chunk(Chunk.make(6, 7, 8, 9, 10)))) + yield* (TestServices.provideLive(Effect.sleep(Duration.seconds(1)))) + yield* (Queue.offer(queue, Take.chunk(Chunk.make(11, 12, 13, 14, 15)))) + yield* (Queue.offer(queue, Take.end)) + const result = yield* pipe( + Fiber.join(fiber), + Effect.map(Chunk.filter(Chunk.isNonEmpty)) + ) + deepStrictEqual( + Array.from(result).map((chunk) => Array.from(chunk)), + [[1, 2, 3, 4, 5], [6, 7, 8, 9, 10], [11, 12, 13, 14, 15]] + ) + })) + + it.effect("aggregateWithin - child fiber handling", () => + Effect.gen(function*() { + const coordination = yield* (chunkCoordination([ + Chunk.of(1), + Chunk.of(2), + Chunk.of(3) + ])) + const fiber = yield* pipe( + Stream.fromQueue(coordination.queue), + Stream.map(Take.make), + Stream.tap(() => coordination.proceed), + Stream.flattenTake, + Stream.aggregateWithin( + Sink.last(), + Schedule.fixed(Duration.millis(200)) + ), + Stream.interruptWhen(Effect.never), + Stream.take(2), + Stream.runCollect, + Effect.fork + ) + yield* pipe( + coordination.offer, + Effect.zipRight(TestClock.adjust(Duration.millis(100))), + Effect.zipRight(coordination.awaitNext), + Effect.repeatN(3) + ) + const results = yield* pipe(Fiber.join(fiber), Effect.map(Chunk.compact)) + deepStrictEqual(Array.from(results), [2, 3]) + })) + + it.effect("aggregateWithinEither - simple example", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.make(1, 1, 1, 1, 2, 2), + Stream.aggregateWithinEither( + pipe( + Sink.fold( + [[] as Array, true] as readonly [Array, boolean], + (tuple) => tuple[1], + ([array], curr: number): readonly [Array, boolean] => { + if (curr === 1) { + return [[curr, ...array], true] + } + return [[curr, ...array], false] + } + ), + Sink.map((tuple) => tuple[0]) + ), + Schedule.spaced(Duration.minutes(30)) + ), + Stream.runCollect + ) + deepStrictEqual( + Array.from(result), + [Either.right([2, 1, 1, 1, 1]), Either.right([2])] + ) + })) + + it.effect("aggregateWithinEither - fails fast", () => + Effect.gen(function*() { + const queue = yield* (Queue.unbounded()) + yield* pipe( + Stream.range(1, 9), + Stream.tap((n) => + pipe( + Effect.fail("Boom"), + Effect.when(() => n === 6), + Effect.zipRight(Queue.offer(queue, n)) + ) + ), + Stream.aggregateWithinEither( + Sink.foldUntil(void 0, 5, constVoid), + Schedule.forever + ), + Stream.runDrain, + Effect.catchAll(() => Effect.succeed(void 0)) + ) + const result = yield* (Queue.takeAll(queue)) + yield* (Queue.shutdown(queue)) + deepStrictEqual(Array.from(result), [1, 2, 3, 4, 5]) + })) + + it.effect("aggregateWithinEither - error propagation #1", () => + Effect.gen(function*() { + const error = new Cause.RuntimeException("Boom") + const result = yield* pipe( + Stream.make(1, 1, 1, 1), + Stream.aggregateWithinEither( + Sink.die(error), + Schedule.spaced(Duration.minutes(30)) + ), + Stream.runCollect, + Effect.exit + ) + deepStrictEqual(result, Exit.die(error)) + })) + + it.effect("aggregateWithinEither - error propagation #2", () => + Effect.gen(function*() { + const error = new Cause.RuntimeException("Boom") + const result = yield* pipe( + Stream.make(1, 1), + Stream.aggregateWithinEither( + Sink.foldEffect(Chunk.empty(), constTrue, () => Effect.die(error)), + Schedule.spaced(Duration.minutes(30)) + ), + Stream.runCollect, + Effect.exit + ) + deepStrictEqual(result, Exit.die(error)) + })) + + it.effect("aggregateWithinEither - interruption propagation #1", () => + Effect.gen(function*() { + const latch = yield* (Deferred.make()) + const ref = yield* (Ref.make(false)) + const sink = Sink.foldEffect(Chunk.empty(), constTrue, (acc, curr) => { + if (curr === 1) { + return Effect.succeed(Chunk.prepend(acc, curr)) + } + return pipe( + Deferred.succeed(latch, void 0), + Effect.zipRight(Effect.never), + Effect.onInterrupt(() => Ref.set(ref, true)) + ) + }) + const fiber = yield* pipe( + Stream.make(1, 1, 2), + Stream.aggregateWithinEither(sink, Schedule.spaced(Duration.minutes(30))), + Stream.runCollect, + Effect.fork + ) + yield* (Deferred.await(latch)) + yield* (Fiber.interrupt(fiber)) + const result = yield* (Ref.get(ref)) + assertTrue(result) + })) + + it.effect("aggregateWithinEither - interruption propagation #2", () => + Effect.gen(function*() { + const latch = yield* (Deferred.make()) + const ref = yield* (Ref.make(false)) + const sink = Sink.fromEffect(pipe( + Deferred.succeed(latch, void 0), + Effect.zipRight(Effect.never), + Effect.onInterrupt(() => Ref.set(ref, true)) + )) + const fiber = yield* pipe( + Stream.make(1, 1, 2), + Stream.aggregateWithinEither(sink, Schedule.spaced(Duration.minutes(30))), + Stream.runCollect, + Effect.fork + ) + yield* (Deferred.await(latch)) + yield* (Fiber.interrupt(fiber)) + const result = yield* (Ref.get(ref)) + assertTrue(result) + })) + + it.effect("aggregateWithinEither - leftover handling", () => + Effect.gen(function*() { + const input = [1, 2, 2, 3, 2, 3] + const fiber = yield* pipe( + Stream.fromIterable(input), + Stream.aggregateWithinEither( + Sink.foldWeighted({ + initial: Chunk.empty(), + maxCost: 4, + cost: (_, n) => n, + body: (acc, curr) => Chunk.append(acc, curr) + }), + Schedule.spaced(Duration.millis(100)) + ), + Stream.filterMap((either) => + Either.isRight(either) ? + Option.some(either.right) : + Option.none() + ), + Stream.runCollect, + Effect.map(Chunk.flatten), + Effect.fork + ) + yield* (TestClock.adjust(Duration.minutes(31))) + const result = yield* (Fiber.join(fiber)) + deepStrictEqual(Array.from(result), input) + })) +}) diff --git a/repos/effect/packages/effect/test/Stream/async.test.ts b/repos/effect/packages/effect/test/Stream/async.test.ts new file mode 100644 index 0000000..be4b1fd --- /dev/null +++ b/repos/effect/packages/effect/test/Stream/async.test.ts @@ -0,0 +1,465 @@ +import { describe, it } from "@effect/vitest" +import { assertFalse, assertTrue, deepStrictEqual } from "@effect/vitest/utils" +import * as Cause from "effect/Cause" +import * as Chunk from "effect/Chunk" +import * as Deferred from "effect/Deferred" +import * as Effect from "effect/Effect" +import * as Exit from "effect/Exit" +import * as Fiber from "effect/Fiber" +import { pipe } from "effect/Function" +import * as Option from "effect/Option" +import * as Ref from "effect/Ref" +import * as Sink from "effect/Sink" +import * as Stream from "effect/Stream" + +describe("Stream", () => { + it.effect("async", () => + Effect.gen(function*() { + const array = [1, 2, 3, 4, 5] + const result = yield* pipe( + Stream.async((emit) => { + array.forEach((n) => { + emit(Effect.succeed(Chunk.of(n))) + }) + }), + Stream.take(array.length), + Stream.runCollect + ) + deepStrictEqual(Array.from(result), array) + })) + + it.effect("async - with cleanup", () => + Effect.gen(function*() { + const ref = yield* (Ref.make(false)) + const latch = yield* (Deferred.make()) + const fiber = yield* pipe( + Stream.async((emit) => { + emit.chunk(Chunk.of(void 0)) + return Ref.set(ref, true) + }), + Stream.tap(() => Deferred.succeed(latch, void 0)), + Stream.runDrain, + Effect.fork + ) + yield* (Deferred.await(latch)) + yield* (Fiber.interrupt(fiber)) + const result = yield* (Ref.get(ref)) + assertTrue(result) + })) + + it.effect("async - signals the end of the stream", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.async((emit) => { + emit.end() + return Effect.void + }), + Stream.runCollect + ) + assertTrue(Chunk.isEmpty(result)) + })) + + it.effect("async - handles errors", () => + Effect.gen(function*() { + const error = new Cause.RuntimeException("boom") + const result = yield* pipe( + Stream.async((emit) => { + emit.fromEffect(Effect.fail(error)) + return Effect.void + }), + Stream.runCollect, + Effect.exit + ) + deepStrictEqual(result, Exit.fail(error)) + })) + + it.effect("async - handles defects", () => + Effect.gen(function*() { + const error = new Cause.RuntimeException("boom") + const result = yield* pipe( + Stream.async(() => { + throw error + }), + Stream.runCollect, + Effect.exit + ) + deepStrictEqual(result, Exit.die(error)) + })) + + it.effect("async - backpressure", () => + Effect.gen(function*() { + const refCount = yield* (Ref.make(0)) + const refDone = yield* (Ref.make(false)) + const stream = Stream.async>((emit) => { + Promise.all( + // 1st consumed by sink, 2-6 – in queue, 7th – back pressured + [1, 2, 3, 4, 5, 6, 7].map((n) => + emit.fromEffectChunk( + pipe( + Ref.set(refCount, n), + Effect.zipRight(Effect.succeed(Chunk.of(1))) + ) + ) + ) + ).then(() => + emit.fromEffect( + pipe( + Ref.set(refDone, true), + Effect.zipRight(Effect.fail(Option.none())) + ) + ) + ) + return Effect.void + }, 5) + const sink = pipe(Sink.take(1), Sink.zipRight(Sink.never)) + const fiber = yield* pipe(stream, Stream.run(sink), Effect.fork) + yield* pipe(Ref.get(refCount), Effect.repeat({ while: (n) => n !== 7 })) + const result = yield* (Ref.get(refDone)) + yield* pipe(Fiber.interrupt(fiber), Effect.exit) + assertFalse(result) + })) + + it.effect("asyncEffect - simple example", () => + Effect.gen(function*() { + const array = [1, 2, 3, 4, 5] + const latch = yield* (Deferred.make()) + const fiber = yield* pipe( + Stream.asyncEffect((emit) => { + array.forEach((n) => { + emit(Effect.succeed(Chunk.of(n))) + }) + return pipe( + Deferred.succeed(latch, void 0), + Effect.zipRight(Effect.void) + ) + }), + Stream.take(array.length), + Stream.runCollect, + Effect.fork + ) + yield* (Deferred.await(latch)) + const result = yield* (Fiber.join(fiber)) + deepStrictEqual(Array.from(result), array) + })) + + it.effect("asyncEffect - handles errors", () => + Effect.gen(function*() { + const error = new Cause.RuntimeException("boom") + const result = yield* pipe( + Stream.asyncEffect((emit) => { + emit.fromEffect(Effect.fail(error)) + return Effect.void + }), + Stream.runCollect, + Effect.exit + ) + deepStrictEqual(result, Exit.fail(error)) + })) + + it.effect("asyncEffect - handles defects", () => + Effect.gen(function*() { + const error = new Cause.RuntimeException("boom") + const result = yield* pipe( + Stream.asyncEffect(() => { + throw error + }), + Stream.runCollect, + Effect.exit + ) + deepStrictEqual(result, Exit.die(error)) + })) + + it.effect("asyncEffect - signals the end of the stream", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.asyncEffect((emit) => { + emit(Effect.fail(Option.none())) + return Effect.void + }), + Stream.runCollect + ) + assertTrue(Chunk.isEmpty(result)) + })) + + it.effect("asyncEffect - backpressure", () => + Effect.gen(function*() { + const refCount = yield* (Ref.make(0)) + const refDone = yield* (Ref.make(false)) + const stream = Stream.asyncEffect>((emit) => { + Promise.all( + // 1st consumed by sink, 2-6 – in queue, 7th – back pressured + [1, 2, 3, 4, 5, 6, 7].map((n) => + emit.fromEffectChunk( + pipe( + Ref.set(refCount, n), + Effect.zipRight(Effect.succeed(Chunk.of(1))) + ) + ) + ) + ).then(() => + emit.fromEffect( + pipe( + Ref.set(refDone, true), + Effect.zipRight(Effect.fail(Option.none())) + ) + ) + ) + return Effect.void + }, 5) + const sink = pipe(Sink.take(1), Sink.zipRight(Sink.never)) + const fiber = yield* pipe(stream, Stream.run(sink), Effect.fork) + yield* pipe(Ref.get(refCount), Effect.repeat({ while: (n) => n !== 7 })) + const result = yield* (Ref.get(refDone)) + yield* (Fiber.interrupt(fiber)) + assertFalse(result) + })) + + // it.effect("asyncOption - signals the end of the stream", () => + // Effect.gen(function*() { + // const result = yield* ( + // Stream.asyncOption((emit) => { + // emit(Effect.fail(Option.none())) + // return Option.none() + // }), + // Stream.runCollect + // ) + // assertTrue(Chunk.isEmpty(result)) + // })) + + // it.effect("asyncOption - some", () => + // Effect.gen(function*() { + // const chunk = Chunk.range(1, 5) + // const result = yield* ( + // Stream.asyncOption(() => Option.some(Stream.fromChunk(chunk))), + // Stream.runCollect + // ) + // deepStrictEqual(Array.from(result), Array.from(chunk)) + // })) + + // it.effect("asyncOption - none", () => + // Effect.gen(function*() { + // const array = [1, 2, 3, 4, 5] + // const result = yield* ( + // Stream.asyncOption((emit) => { + // array.forEach((n) => { + // emit(Effect.succeed(Chunk.of(n))) + // }) + // return Option.none() + // }), + // Stream.take(array.length), + // Stream.runCollect + // ) + // deepStrictEqual(Array.from(result), array) + // })) + + // it.effect("asyncOption - handles errors", () => + // Effect.gen(function*() { + // const error = new Cause.RuntimeException("boom") + // const result = yield* ( + // Stream.asyncOption((emit) => { + // emit.fromEffect(Effect.fail(error)) + // return Option.none() + // }), + // Stream.runCollect, + // Effect.exit + // ) + // deepStrictEqual(result, Exit.fail(error)) + // })) + + // it.effect("asyncOption - handles defects", () => + // Effect.gen(function*() { + // const error = new Cause.RuntimeException("boom") + // const result = yield* ( + // Stream.asyncOption(() => { + // throw error + // }), + // Stream.runCollect, + // Effect.exit + // ) + // deepStrictEqual(result, Exit.die(error)) + // })) + + // it.effect("asyncOption - backpressure", () => + // Effect.gen(function*() { + // const refCount = yield* (Ref.make(0)) + // const refDone = yield* (Ref.make(false)) + // const stream = Stream.asyncOption>((emit) => { + // Promise.all( + // // 1st consumed by sink, 2-6 – in queue, 7th – back pressured + // [1, 2, 3, 4, 5, 6, 7].map((n) => + // emit.fromEffectChunk( + // pipe( + // Ref.set(refCount, n), + // Effect.zipRight(Effect.succeed(Chunk.of(1))) + // ) + // ) + // ) + // ).then(() => + // emit.fromEffect( + // pipe( + // Ref.set(refDone, true), + // Effect.zipRight(Effect.fail(Option.none())) + // ) + // ) + // ) + // return Option.none() + // }, 5) + // const sink = pipe(Sink.take(1), Sink.zipRight(Sink.never)) + // const fiber = yield* (stream, Stream.run(sink), Effect.fork) + // yield* (Ref.get(refCount), Effect.repeat({ while: (n) => n !== 7 })) + // const result = yield* (Ref.get(refDone)) + // yield* (Fiber.interrupt(fiber), Effect.exit) + // assertFalse(result) + // })) + + it.effect("asyncScoped", () => + Effect.gen(function*() { + const array = [1, 2, 3, 4, 5] + const latch = yield* (Deferred.make()) + const fiber = yield* pipe( + Stream.asyncScoped((cb) => { + array.forEach((n) => { + cb(Effect.succeed(Chunk.of(n))) + }) + return pipe( + Deferred.succeed(latch, void 0), + Effect.asVoid + ) + }), + Stream.take(array.length), + Stream.run(Sink.collectAll()), + Effect.fork + ) + yield* (Deferred.await(latch)) + const result = yield* (Fiber.join(fiber)) + deepStrictEqual(Array.from(result), array) + })) + + it.effect("asyncScoped - signals the end of the stream", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.asyncScoped((cb) => { + cb(Effect.fail(Option.none())) + return Effect.void + }), + Stream.runCollect + ) + assertTrue(Chunk.isEmpty(result)) + })) + + it.effect("asyncScoped - handles errors", () => + Effect.gen(function*() { + const error = new Cause.RuntimeException("boom") + const result = yield* pipe( + Stream.asyncScoped((cb) => { + cb(Effect.fail(Option.some(error))) + return Effect.void + }), + Stream.runCollect, + Effect.exit + ) + deepStrictEqual(result, Exit.fail(error)) + })) + + it.effect("asyncScoped - handles defects", () => + Effect.gen(function*() { + const error = new Cause.RuntimeException("boom") + const result = yield* pipe( + Stream.asyncScoped(() => { + throw error + }), + Stream.runCollect, + Effect.exit + ) + deepStrictEqual(result, Exit.die(error)) + })) + + it.effect("asyncScoped - backpressure", () => + Effect.gen(function*() { + const refCount = yield* (Ref.make(0)) + const refDone = yield* (Ref.make(false)) + const stream = Stream.asyncScoped>((cb) => { + Promise.all( + // 1st consumed by sink, 2-6 – in queue, 7th – back pressured + [1, 2, 3, 4, 5, 6, 7].map((n) => + cb( + pipe( + Ref.set(refCount, n), + Effect.zipRight(Effect.succeed(Chunk.of(1))) + ) + ) + ) + ).then(() => + cb( + pipe( + Ref.set(refDone, true), + Effect.zipRight(Effect.fail(Option.none())) + ) + ) + ) + return Effect.void + }, 5) + const sink = pipe(Sink.take(1), Sink.zipRight(Sink.never)) + const fiber = yield* pipe(stream, Stream.run(sink), Effect.fork) + yield* pipe(Ref.get(refCount), Effect.repeat({ while: (n) => n !== 7 })) + const result = yield* (Ref.get(refDone)) + yield* pipe(Fiber.interrupt(fiber), Effect.exit) + assertFalse(result) + })) + + it.effect("asyncPush", () => + Effect.gen(function*() { + const array = [1, 2, 3, 4, 5] + const latch = yield* Deferred.make() + const fiber = yield* Stream.asyncPush((emit) => { + array.forEach((n) => { + emit.single(n) + }) + return pipe( + Deferred.succeed(latch, void 0), + Effect.asVoid + ) + }).pipe( + Stream.take(array.length), + Stream.run(Sink.collectAll()), + Effect.fork + ) + yield* Deferred.await(latch) + const result = yield* Fiber.join(fiber) + deepStrictEqual(Array.from(result), array) + })) + + it.effect("asyncPush - signals the end of the stream", () => + Effect.gen(function*() { + const result = yield* Stream.asyncPush((emit) => { + emit.end() + return Effect.void + }).pipe(Stream.runCollect) + assertTrue(Chunk.isEmpty(result)) + })) + + it.effect("asyncPush - handles errors", () => + Effect.gen(function*() { + const error = new Cause.RuntimeException("boom") + const result = yield* Stream.asyncPush((emit) => { + emit.fail(error) + return Effect.void + }).pipe( + Stream.runCollect, + Effect.exit + ) + deepStrictEqual(result, Exit.fail(error)) + })) + + it.effect("asyncPush - handles defects", () => + Effect.gen(function*() { + const error = new Cause.RuntimeException("boom") + const result = yield* Stream.asyncPush(() => { + throw error + }).pipe( + Stream.runCollect, + Effect.exit + ) + deepStrictEqual(result, Exit.die(error)) + })) +}) diff --git a/repos/effect/packages/effect/test/Stream/broadcasting.test.ts b/repos/effect/packages/effect/test/Stream/broadcasting.test.ts new file mode 100644 index 0000000..98335cf --- /dev/null +++ b/repos/effect/packages/effect/test/Stream/broadcasting.test.ts @@ -0,0 +1,197 @@ +import { describe, it } from "@effect/vitest" +import { assertLeft, deepStrictEqual } from "@effect/vitest/utils" +import * as Chunk from "effect/Chunk" +import * as Deferred from "effect/Deferred" +import * as Effect from "effect/Effect" +import * as Fiber from "effect/Fiber" +import { pipe } from "effect/Function" +import * as Ref from "effect/Ref" +import * as Schedule from "effect/Schedule" +import * as Sink from "effect/Sink" +import * as Stream from "effect/Stream" +import * as TestClock from "effect/TestClock" + +describe("Stream", () => { + it.effect("broadcast - values", () => + Effect.gen(function*() { + const { result1, result2 } = yield* pipe( + Stream.range(0, 4), + Stream.broadcast(2, 12), + Effect.flatMap((streams) => + Effect.all({ + result1: Stream.runCollect(streams[0]), + result2: Stream.runCollect(streams[1]) + }) + ), + Effect.scoped + ) + const expected = [0, 1, 2, 3, 4] + deepStrictEqual(Array.from(result1), expected) + deepStrictEqual(Array.from(result2), expected) + })) + + it.effect("broadcast - errors", () => + Effect.gen(function*() { + const { result1, result2 } = yield* pipe( + Stream.make(0), + Stream.concat(Stream.fail("boom")), + Stream.broadcast(2, 12), + Effect.flatMap((streams) => + Effect.all({ + result1: pipe(streams[0], Stream.runCollect, Effect.either), + result2: pipe(streams[1], Stream.runCollect, Effect.either) + }) + ), + Effect.scoped + ) + assertLeft(result1, "boom") + assertLeft(result2, "boom") + })) + + it.effect("broadcast - backpressure", () => + Effect.gen(function*() { + const { result1, result2 } = yield* pipe( + Stream.range(0, 4), + Stream.flatMap(Stream.succeed), + Stream.broadcast(2, 2), + Effect.flatMap((streams) => + Effect.gen(function*() { + const ref = yield* (Ref.make(Chunk.empty())) + const latch = yield* (Deferred.make()) + const fiber = yield* pipe( + streams[0], + Stream.tap((n) => + pipe( + Ref.update(ref, Chunk.append(n)), + Effect.zipRight(pipe( + Deferred.succeed(latch, void 0), + Effect.when(() => n === 1) + )) + ) + ), + Stream.runDrain, + Effect.fork + ) + yield* (Deferred.await(latch)) + const result1 = yield* (Ref.get(ref)) + yield* (Stream.runDrain(streams[1])) + yield* (Fiber.await(fiber)) + const result2 = yield* (Ref.get(ref)) + return { result1, result2 } + }) + ), + Effect.scoped + ) + deepStrictEqual(Array.from(result1), [0, 1]) + deepStrictEqual(Array.from(result2), [0, 1, 2, 3, 4]) + })) + + it.effect("broadcast - unsubscribe", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.range(0, 4), + Stream.broadcast(2, 2), + Effect.flatMap((streams) => + pipe( + Stream.toPull(streams[0]), + Effect.ignore, + Effect.scoped, + Effect.zipRight(Stream.runCollect(streams[1])) + ) + ), + Effect.scoped + ) + deepStrictEqual(Array.from(result), [0, 1, 2, 3, 4]) + })) + + it.scoped("share sequenced", () => + Effect.gen(function*() { + const sharedStream = yield* Stream.fromSchedule(Schedule.spaced("1 seconds")).pipe( + Stream.share({ capacity: 16 }) + ) + + const firstFiber = yield* sharedStream.pipe( + Stream.take(1), + Stream.run(Sink.collectAll()), + Effect.map(Array.from), + Effect.fork + ) + + yield* TestClock.adjust("1 second") + + const first = yield* Fiber.join(firstFiber) + deepStrictEqual(first, [0]) + + const secondFiber = yield* sharedStream.pipe( + Stream.take(1), + Stream.run(Sink.collectAll()), + Effect.map(Array.from), + Effect.fork + ) + + yield* TestClock.adjust("1 second") + + const second = yield* Fiber.join(secondFiber) + deepStrictEqual(second, [0]) + })) + + it.scoped("share sequenced with idleTimeToLive", () => + Effect.gen(function*() { + const sharedStream = yield* Stream.fromSchedule(Schedule.spaced("1 seconds")).pipe( + Stream.share({ + capacity: 16, + idleTimeToLive: "1 second" + }) + ) + + const firstFiber = yield* sharedStream.pipe( + Stream.take(1), + Stream.run(Sink.collectAll()), + Effect.map(Array.from), + Effect.fork + ) + + yield* TestClock.adjust("1 second") + + const first = yield* Fiber.join(firstFiber) + deepStrictEqual(first, [0]) + + const secondFiber = yield* sharedStream.pipe( + Stream.take(1), + Stream.run(Sink.collectAll()), + Effect.map(Array.from), + Effect.fork + ) + + yield* TestClock.adjust("1 second") + + const second = yield* Fiber.join(secondFiber) + deepStrictEqual(second, [1]) + })) + + it.scoped("share parallel", () => + Effect.gen(function*() { + const sharedStream = yield* Stream.fromSchedule(Schedule.spaced("1 seconds")).pipe( + Stream.share({ capacity: 16 }) + ) + + const fiber1 = yield* sharedStream.pipe( + Stream.take(1), + Stream.run(Sink.collectAll()), + Effect.map((x) => Array.from(x)), + Effect.fork + ) + const fiber2 = yield* sharedStream.pipe( + Stream.take(2), + Stream.run(Sink.collectAll()), + Effect.map((x) => Array.from(x)), + Effect.fork + ) + + yield* TestClock.adjust("2 second") + const [result1, result2] = yield* Fiber.joinAll([fiber1, fiber2]) + + deepStrictEqual(result1, [0]) + deepStrictEqual(result2, [0, 1]) + })) +}) diff --git a/repos/effect/packages/effect/test/Stream/buffering.test.ts b/repos/effect/packages/effect/test/Stream/buffering.test.ts new file mode 100644 index 0000000..5b6def2 --- /dev/null +++ b/repos/effect/packages/effect/test/Stream/buffering.test.ts @@ -0,0 +1,525 @@ +import { describe, it } from "@effect/vitest" +import { deepStrictEqual } from "@effect/vitest/utils" +import * as Cause from "effect/Cause" +import * as Chunk from "effect/Chunk" +import * as Deferred from "effect/Deferred" +import * as Effect from "effect/Effect" +import * as Exit from "effect/Exit" +import { pipe } from "effect/Function" +import * as Ref from "effect/Ref" +import * as Stream from "effect/Stream" + +describe("Stream", () => { + it.effect("buffer - maintains elements and ordering", () => + Effect.gen(function*() { + const chunks = Chunk.make( + Chunk.range(0, 3), + Chunk.range(2, 5), + Chunk.range(3, 7) + ) + const result = yield* pipe( + Stream.fromChunks(...chunks), + Stream.buffer({ capacity: 2 }), + Stream.runCollect + ) + deepStrictEqual(Array.from(result), Array.from(Chunk.flatten(chunks))) + })) + + it.effect("buffer - buffers a stream with a failure", () => + Effect.gen(function*() { + const error = new Cause.RuntimeException("boom") + const result = yield* pipe( + Stream.range(0, 9), + Stream.concat(Stream.fail(error)), + Stream.buffer({ capacity: 2 }), + Stream.runCollect, + Effect.exit + ) + deepStrictEqual(result, Exit.fail(error)) + })) + + it.effect("buffer - fast producer progresses independently", () => + Effect.gen(function*() { + const ref = yield* (Ref.make(Chunk.empty())) + const latch = yield* (Deferred.make()) + const stream = pipe( + Stream.range(1, 4), + Stream.tap((n) => + pipe( + Ref.update(ref, Chunk.append(n)), + Effect.zipRight(pipe( + Deferred.succeed(latch, void 0), + Effect.when(() => n === 4) + )) + ) + ), + Stream.buffer({ capacity: 2 }) + ) + const result1 = yield* pipe(stream, Stream.take(2), Stream.runCollect) + yield* (Deferred.await(latch)) + const result2 = yield* (Ref.get(ref)) + deepStrictEqual(Array.from(result1), [1, 2]) + deepStrictEqual(Array.from(result2), [1, 2, 3, 4]) + })) + + it.effect("bufferChunks - maintains elements and ordering", () => + Effect.gen(function*() { + const chunks = Chunk.make( + Chunk.range(0, 3), + Chunk.range(2, 5), + Chunk.range(3, 7) + ) + const result = yield* pipe( + Stream.fromChunks(...chunks), + Stream.bufferChunks({ capacity: 2 }), + Stream.runCollect + ) + deepStrictEqual(Array.from(result), Array.from(Chunk.flatten(chunks))) + })) + + it.effect("bufferChunks - buffers a stream with a failure", () => + Effect.gen(function*() { + const error = new Cause.RuntimeException("boom") + const result = yield* pipe( + Stream.range(0, 9), + Stream.concat(Stream.fail(error)), + Stream.bufferChunks({ capacity: 2 }), + Stream.runCollect, + Effect.exit + ) + deepStrictEqual(result, Exit.fail(error)) + })) + + it.effect("bufferChunks - fast producer progresses independently", () => + Effect.gen(function*() { + const ref = yield* (Ref.make(Chunk.empty())) + const latch = yield* (Deferred.make()) + const stream = pipe( + Stream.range(1, 4), + Stream.tap((n) => + pipe( + Ref.update(ref, Chunk.append(n)), + Effect.zipRight(pipe( + Deferred.succeed(latch, void 0), + Effect.when(() => n === 4) + )) + ) + ), + Stream.bufferChunks({ capacity: 2 }) + ) + const result1 = yield* pipe(stream, Stream.take(2), Stream.runCollect) + yield* (Deferred.await(latch)) + const result2 = yield* (Ref.get(ref)) + deepStrictEqual(Array.from(result1), [1, 2]) + deepStrictEqual(Array.from(result2), [1, 2, 3, 4]) + })) + + it.effect("bufferChunksDropping - buffers a stream with a failure", () => + Effect.gen(function*() { + const error = new Cause.RuntimeException("boom") + const result = yield* pipe( + Stream.range(1, 1_000), + Stream.concat(Stream.fail(error)), + Stream.concat(Stream.range(1_001, 2_000)), + Stream.bufferChunks({ capacity: 2, strategy: "dropping" }), + Stream.runCollect, + Effect.exit + ) + deepStrictEqual(result, Exit.fail(error)) + })) + + it.effect("bufferChunksDropping - fast producer progress independently", () => + Effect.gen(function*() { + const ref = yield* (Ref.make(Chunk.empty())) + const latch1 = yield* (Deferred.make()) + const latch2 = yield* (Deferred.make()) + const latch3 = yield* (Deferred.make()) + const latch4 = yield* (Deferred.make()) + const stream1 = pipe( + Stream.make(0), + Stream.concat( + pipe( + Stream.fromEffect(Deferred.await(latch1)), + Stream.flatMap(() => + pipe( + Stream.range(1, 16), + Stream.rechunk(1), + Stream.ensuring(Deferred.succeed(latch2, void 0)) + ) + ) + ) + ) + ) + const stream2 = pipe( + Stream.fromEffect(Deferred.await(latch3)), + Stream.flatMap(() => + pipe( + Stream.range(17, 24), + Stream.rechunk(1), + Stream.ensuring(Deferred.succeed(latch4, void 0)) + ) + ) + ) + const stream3 = Stream.make(-1) + const stream = pipe( + stream1, + Stream.concat(stream2), + Stream.concat(stream3), + Stream.bufferChunks({ capacity: 8, strategy: "dropping" }) + ) + const { result1, result2, result3 } = yield* pipe( + Stream.toPull(stream), + Effect.flatMap((pull) => + Effect.gen(function*() { + const result1 = yield* pull + yield* (Deferred.succeed(latch1, void 0)) + yield* (Deferred.await(latch2)) + yield* pipe( + pull, + Effect.flatMap((chunk) => Ref.update(ref, Chunk.appendAll(chunk))), + Effect.repeatN(7) + ) + const result2 = yield* (Ref.get(ref)) + yield* (Deferred.succeed(latch3, void 0)) + yield* (Deferred.await(latch4)) + yield* pipe( + pull, + Effect.flatMap((chunk) => Ref.update(ref, Chunk.appendAll(chunk))), + Effect.repeatN(7) + ) + const result3 = yield* (Ref.get(ref)) + return { result1, result2, result3 } + }) + ), + Effect.scoped + ) + const expected1 = [0] + const expected2 = [1, 2, 3, 4, 5, 6, 7, 8] + const expected3 = [1, 2, 3, 4, 5, 6, 7, 8, 17, 18, 19, 20, 21, 22, 23, 24] + deepStrictEqual(Array.from(result1), expected1) + deepStrictEqual(Array.from(result2), expected2) + deepStrictEqual(Array.from(result3), expected3) + })) + + it.effect("bufferChunksSliding - buffers a stream with a failure", () => + Effect.gen(function*() { + const error = new Cause.RuntimeException("boom") + const result = yield* pipe( + Stream.range(1, 1_000), + Stream.concat(Stream.fail(error)), + Stream.concat(Stream.range(1_001, 2_000)), + Stream.bufferChunks({ capacity: 2, strategy: "sliding" }), + Stream.runCollect, + Effect.exit + ) + deepStrictEqual(result, Exit.fail(error)) + })) + + it.effect("bufferChunksSliding - fast producer progress independently", () => + Effect.gen(function*() { + const ref = yield* (Ref.make(Chunk.empty())) + const latch1 = yield* (Deferred.make()) + const latch2 = yield* (Deferred.make()) + const latch3 = yield* (Deferred.make()) + const latch4 = yield* (Deferred.make()) + const latch5 = yield* (Deferred.make()) + const stream1 = pipe( + Stream.make(0), + Stream.concat( + pipe( + Stream.fromEffect(Deferred.await(latch1)), + Stream.flatMap(() => + pipe( + Stream.range(1, 16), + Stream.rechunk(1), + Stream.ensuring(Deferred.succeed(latch2, void 0)) + ) + ) + ) + ) + ) + const stream2 = pipe( + Stream.fromEffect(Deferred.await(latch3)), + Stream.flatMap(() => + pipe( + Stream.range(17, 25), + Stream.rechunk(1), + Stream.ensuring(Deferred.succeed(latch4, void 0)) + ) + ) + ) + const stream3 = pipe( + Stream.fromEffect(Deferred.await(latch5)), + Stream.flatMap(() => Stream.make(-1)) + ) + const stream = pipe( + stream1, + Stream.concat(stream2), + Stream.concat(stream3), + Stream.bufferChunks({ capacity: 8, strategy: "sliding" }) + ) + const { result1, result2, result3 } = yield* pipe( + Stream.toPull(stream), + Effect.flatMap((pull) => + Effect.gen(function*() { + const result1 = yield* pull + yield* (Deferred.succeed(latch1, void 0)) + yield* (Deferred.await(latch2)) + yield* pipe( + pull, + Effect.flatMap((chunk) => Ref.update(ref, Chunk.appendAll(chunk))), + Effect.repeatN(7) + ) + const result2 = yield* (Ref.get(ref)) + yield* (Deferred.succeed(latch3, void 0)) + yield* (Deferred.await(latch4)) + yield* pipe( + pull, + Effect.flatMap((chunk) => Ref.update(ref, Chunk.appendAll(chunk))), + Effect.repeatN(7) + ) + const result3 = yield* (Ref.get(ref)) + return { result1, result2, result3 } + }) + ), + Effect.scoped + ) + const expected1 = [0] + const expected2 = [9, 10, 11, 12, 13, 14, 15, 16] + const expected3 = [9, 10, 11, 12, 13, 14, 15, 16, 18, 19, 20, 21, 22, 23, 24, 25] + deepStrictEqual(Array.from(result1), expected1) + deepStrictEqual(Array.from(result2), expected2) + deepStrictEqual(Array.from(result3), expected3) + })) + + it.effect("bufferDropping - buffers a stream with a failure", () => + Effect.gen(function*() { + const error = new Cause.RuntimeException("boom") + const result = yield* pipe( + Stream.range(1, 1_000), + Stream.concat(Stream.fail(error)), + Stream.concat(Stream.range(1_000, 2_000)), + Stream.buffer({ capacity: 2, strategy: "dropping" }), + Stream.runCollect, + Effect.exit + ) + deepStrictEqual(result, Exit.fail(error)) + })) + + it.effect("bufferDropping - fast producer progress independently", () => + Effect.gen(function*() { + const ref = yield* (Ref.make(Chunk.empty())) + const latch1 = yield* (Deferred.make()) + const latch2 = yield* (Deferred.make()) + const latch3 = yield* (Deferred.make()) + const latch4 = yield* (Deferred.make()) + const stream1 = pipe( + Stream.make(0), + Stream.concat( + pipe( + Stream.fromEffect(Deferred.await(latch1)), + Stream.flatMap(() => + pipe( + Stream.range(1, 17), + Stream.rechunk(1), + Stream.ensuring(Deferred.succeed(latch2, void 0)) + ) + ) + ) + ) + ) + const stream2 = pipe( + Stream.fromEffect(Deferred.await(latch3)), + Stream.flatMap(() => + pipe( + Stream.range(17, 24), + Stream.rechunk(1), + Stream.ensuring(Deferred.succeed(latch4, void 0)) + ) + ) + ) + const stream3 = Stream.make(-1) + const stream = pipe( + stream1, + Stream.concat(stream2), + Stream.concat(stream3), + Stream.buffer({ capacity: 8, strategy: "dropping" }) + ) + const { result1, result2, result3 } = yield* pipe( + Stream.toPull(stream), + Effect.flatMap((pull) => + Effect.gen(function*() { + const result1 = yield* pull + yield* (Deferred.succeed(latch1, void 0)) + yield* (Deferred.await(latch2)) + yield* pipe( + pull, + Effect.flatMap((chunk) => Ref.update(ref, Chunk.appendAll(chunk))), + Effect.repeatN(7) + ) + const result2 = yield* (Ref.get(ref)) + yield* (Deferred.succeed(latch3, void 0)) + yield* (Deferred.await(latch4)) + yield* pipe( + pull, + Effect.flatMap((chunk) => Ref.update(ref, Chunk.appendAll(chunk))), + Effect.repeatN(7) + ) + const result3 = yield* (Ref.get(ref)) + return { result1, result2, result3 } + }) + ), + Effect.scoped + ) + const expected1 = [0] + const expected2 = [1, 2, 3, 4, 5, 6, 7, 8] + const expected3 = [1, 2, 3, 4, 5, 6, 7, 8, 17, 18, 19, 20, 21, 22, 23, 24] + deepStrictEqual(Array.from(result1), expected1) + deepStrictEqual(Array.from(result2), expected2) + deepStrictEqual(Array.from(result3), expected3) + })) + + it.effect("bufferSliding - buffers a stream with a failure", () => + Effect.gen(function*() { + const error = new Cause.RuntimeException("boom") + const result = yield* pipe( + Stream.range(1, 1_000), + Stream.concat(Stream.fail(error)), + Stream.concat(Stream.range(1_001, 2_000)), + Stream.buffer({ capacity: 2, strategy: "sliding" }), + Stream.runCollect, + Effect.exit + ) + deepStrictEqual(result, Exit.fail(error)) + })) + + it.effect("bufferSliding - fast producer progress independently", () => + Effect.gen(function*() { + const ref = yield* (Ref.make(Chunk.empty())) + const latch1 = yield* (Deferred.make()) + const latch2 = yield* (Deferred.make()) + const latch3 = yield* (Deferred.make()) + const latch4 = yield* (Deferred.make()) + const stream1 = pipe( + Stream.make(0), + Stream.concat( + pipe( + Stream.fromEffect(Deferred.await(latch1)), + Stream.flatMap(() => + pipe( + Stream.range(1, 16), + Stream.rechunk(1), + Stream.ensuring(Deferred.succeed(latch2, void 0)) + ) + ) + ) + ) + ) + const stream2 = pipe( + Stream.fromEffect(Deferred.await(latch3)), + Stream.flatMap(() => + pipe( + Stream.range(17, 24), + Stream.rechunk(1), + Stream.ensuring(Deferred.succeed(latch4, void 0)) + ) + ) + ) + const stream3 = Stream.make(-1) + const stream = pipe( + stream1, + Stream.concat(stream2), + Stream.concat(stream3), + Stream.buffer({ capacity: 8, strategy: "sliding" }) + ) + const { result1, result2, result3 } = yield* pipe( + Stream.toPull(stream), + Effect.flatMap((pull) => + Effect.gen(function*() { + const result1 = yield* pull + yield* (Deferred.succeed(latch1, void 0)) + yield* (Deferred.await(latch2)) + yield* pipe( + pull, + Effect.flatMap((chunk) => Ref.update(ref, Chunk.appendAll(chunk))), + Effect.repeatN(7) + ) + const result2 = yield* (Ref.get(ref)) + yield* (Deferred.succeed(latch3, void 0)) + yield* (Deferred.await(latch4)) + yield* pipe( + pull, + Effect.flatMap((chunk) => Ref.update(ref, Chunk.appendAll(chunk))), + Effect.repeatN(7) + ) + const result3 = yield* (Ref.get(ref)) + return { result1, result2, result3 } + }) + ), + Effect.scoped + ) + const expected1 = [0] + const expected2 = [9, 10, 11, 12, 13, 14, 15, 16] + const expected3 = [9, 10, 11, 12, 13, 14, 15, 16, 18, 19, 20, 21, 22, 23, 24, -1] + deepStrictEqual(Array.from(result1), expected1) + deepStrictEqual(Array.from(result2), expected2) + deepStrictEqual(Array.from(result3), expected3) + })) + + it.effect("bufferSliding - propagates defects", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.fromEffect(Effect.dieMessage("boom")), + Stream.buffer({ capacity: 1, strategy: "sliding" }), + Stream.runDrain, + Effect.exit + ) + deepStrictEqual(result, Exit.die(new Cause.RuntimeException("boom"))) + })) + + it.effect("bufferUnbounded - buffers the stream", () => + Effect.gen(function*() { + const chunk = Chunk.range(0, 10) + const result = yield* pipe( + Stream.fromIterable(chunk), + Stream.buffer({ capacity: "unbounded" }), + Stream.runCollect + ) + deepStrictEqual(Array.from(result), Array.from(chunk)) + })) + + it.effect("bufferUnbounded - buffers a stream with a failure", () => + Effect.gen(function*() { + const error = new Cause.RuntimeException("boom") + const result = yield* pipe( + Stream.range(0, 9), + Stream.concat(Stream.fail(error)), + Stream.buffer({ capacity: "unbounded" }), + Stream.runCollect, + Effect.exit + ) + deepStrictEqual(result, Exit.fail(error)) + })) + + it.effect("bufferUnbounded - fast producer progress independently", () => + Effect.gen(function*() { + const ref = yield* (Ref.make(Chunk.empty())) + const latch = yield* (Deferred.make()) + const stream = pipe( + Stream.range(1, 999), + Stream.tap((n) => + pipe( + Ref.update(ref, Chunk.append(n)), + Effect.zipRight(pipe(Deferred.succeed(latch, void 0), Effect.when(() => n === 999))) + ) + ), + Stream.rechunk(999), + Stream.buffer({ capacity: "unbounded" }) + ) + const result1 = yield* pipe(stream, Stream.take(2), Stream.runCollect) + yield* (Deferred.await(latch)) + const result2 = yield* (Ref.get(ref)) + deepStrictEqual(Array.from(result1), [1, 2]) + deepStrictEqual(Array.from(result2), Array.from(Chunk.range(1, 999))) + })) +}) diff --git a/repos/effect/packages/effect/test/Stream/changing.test.ts b/repos/effect/packages/effect/test/Stream/changing.test.ts new file mode 100644 index 0000000..c1700c7 --- /dev/null +++ b/repos/effect/packages/effect/test/Stream/changing.test.ts @@ -0,0 +1,46 @@ +import { describe, it } from "@effect/vitest" +import { deepStrictEqual } from "@effect/vitest/utils" +import * as Chunk from "effect/Chunk" +import * as Effect from "effect/Effect" +import { pipe } from "effect/Function" +import * as Stream from "effect/Stream" + +describe("Stream", () => { + it.effect("changes", () => + Effect.gen(function*() { + const stream = Stream.range(0, 19) + const result = yield* pipe( + stream, + Stream.changes, + Stream.runCollect + ) + const expected = yield* pipe( + stream, + Stream.runCollect, + Effect.map(Chunk.reduce(Chunk.empty(), (acc, n) => + acc.length === 0 || Chunk.unsafeGet(acc, 0) !== n ? Chunk.append(acc, n) : acc)) + ) + deepStrictEqual(Array.from(result), Array.from(expected)) + })) + + it.effect("changesWithEffect", () => + Effect.gen(function*() { + const stream = Stream.range(0, 19) + const result = yield* pipe( + stream, + Stream.changesWithEffect((left, right) => Effect.succeed(left === right)), + Stream.runCollect + ) + const expected = yield* pipe( + stream, + Stream.runCollect, + Effect.map( + Chunk.reduce( + Chunk.empty(), + (acc, n) => acc.length === 0 || Chunk.unsafeGet(acc, 0) !== n ? Chunk.append(acc, n) : acc + ) + ) + ) + deepStrictEqual(Array.from(result), Array.from(expected)) + })) +}) diff --git a/repos/effect/packages/effect/test/Stream/collecting.test.ts b/repos/effect/packages/effect/test/Stream/collecting.test.ts new file mode 100644 index 0000000..a0f3caa --- /dev/null +++ b/repos/effect/packages/effect/test/Stream/collecting.test.ts @@ -0,0 +1,166 @@ +import { describe, it } from "@effect/vitest" +import { assertLeft, assertRight, deepStrictEqual } from "@effect/vitest/utils" +import * as Chunk from "effect/Chunk" +import * as Effect from "effect/Effect" +import * as Either from "effect/Either" +import { identity, pipe } from "effect/Function" +import * as Option from "effect/Option" +import * as Stream from "effect/Stream" + +describe("Stream", () => { + it.effect("collect", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.make(Either.left(1), Either.right(2), Either.left(3)), + Stream.filterMap((either) => + Either.isRight(either) ? + Option.some(either.right) : + Option.none() + ), + Stream.runCollect + ) + deepStrictEqual(Array.from(result), [2]) + })) + + it.effect("collectEffect - simple example", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.make(Either.left(1), Either.right(2), Either.left(3)), + Stream.filterMapEffect((either) => + Either.isRight(either) ? + Option.some(Effect.succeed(either.right * 2)) : + Option.none() + ), + Stream.runCollect + ) + deepStrictEqual(Array.from(result), [4]) + })) + + it.effect("collectEffect - multiple chunks", () => + Effect.gen(function*() { + const chunks = Chunk.make( + Chunk.make(Either.left(1), Either.right(2)), + Chunk.make(Either.right(3), Either.left(4)) + ) + const result = yield* pipe( + Stream.fromChunks(...chunks), + Stream.filterMapEffect((either) => + Either.isRight(either) ? + Option.some(Effect.succeed(either.right * 10)) : + Option.none() + ), + Stream.runCollect + ) + deepStrictEqual(Array.from(result), [20, 30]) + })) + + it.effect("collectEffect - handles failures", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.make(Either.left(1), Either.right(2), Either.left(3)), + Stream.filterMapEffect(() => Option.some(Effect.fail("Ouch"))), + Stream.runDrain, + Effect.either + ) + assertLeft(result, "Ouch") + })) + + it.effect("collectEffect - laziness on chunks", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.make(1, 2, 3), + Stream.filterMapEffect((n) => + n === 3 ? + Option.some(Effect.fail("boom")) : + Option.some(Effect.succeed(n)) + ), + Stream.either, + Stream.runCollect + ) + deepStrictEqual( + Array.from(result), + [Either.right(1), Either.right(2), Either.left("boom")] + ) + })) + + it.effect("collectWhile - simple example", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.make(Option.some(1), Option.some(2), Option.none(), Option.some(4)), + Stream.filterMapWhile(identity), + Stream.runCollect + ) + deepStrictEqual(Array.from(result), [1, 2]) + })) + + it.effect("collectWhile - short circuits", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.make(Option.some(1)), + Stream.concat(Stream.fail("Ouch")), + Stream.filterMapWhile((option) => Option.isNone(option) ? Option.some(1) : Option.none()), + Stream.runDrain, + Effect.either + ) + assertRight(result, void 0) + })) + + it.effect("collectWhileEffect - simple example", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.make(Option.some(1), Option.some(2), Option.none(), Option.some(4)), + Stream.filterMapWhileEffect((option) => + Option.isSome(option) ? + Option.some(Effect.succeed(option.value * 2)) : + Option.none() + ), + Stream.runCollect + ) + deepStrictEqual(Array.from(result), [2, 4]) + })) + + it.effect("collectWhileEffect - short circuits", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.make(Option.some(1)), + Stream.concat(Stream.fail("Ouch")), + Stream.filterMapWhileEffect((option) => + Option.isNone(option) ? + Option.some(Effect.succeed(1)) : + Option.none() + ), + Stream.runDrain, + Effect.either + ) + assertRight(result, void 0) + })) + + it.effect("collectWhileEffect - fails", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.make(Option.some(1), Option.some(2), Option.none(), Option.some(3)), + Stream.filterMapWhileEffect(() => Option.some(Effect.fail("Ouch"))), + Stream.runDrain, + Effect.either + ) + assertLeft(result, "Ouch") + })) + + it.effect("collectWhileEffect - laziness on chunks", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.make(1, 2, 3, 4), + Stream.filterMapWhileEffect((n) => + n === 3 ? + Option.some(Effect.fail("boom")) : + Option.some(Effect.succeed(n)) + ), + Stream.either, + Stream.runCollect + ) + deepStrictEqual( + Array.from(result), + [Either.right(1), Either.right(2), Either.left("boom")] + ) + })) +}) diff --git a/repos/effect/packages/effect/test/Stream/concatenation.test.ts b/repos/effect/packages/effect/test/Stream/concatenation.test.ts new file mode 100644 index 0000000..afa7735 --- /dev/null +++ b/repos/effect/packages/effect/test/Stream/concatenation.test.ts @@ -0,0 +1,43 @@ +import { describe, it } from "@effect/vitest" +import { deepStrictEqual } from "@effect/vitest/utils" +import * as Chunk from "effect/Chunk" +import * as Effect from "effect/Effect" +import { pipe } from "effect/Function" +import * as Ref from "effect/Ref" +import * as Stream from "effect/Stream" + +describe("Stream", () => { + it.effect("concat - simple example", () => + Effect.gen(function*() { + const stream1 = Stream.make(1, 2, 3) + const stream2 = Stream.make(4, 5, 6) + const { result1, result2 } = yield* (Effect.all({ + result1: pipe( + stream1, + Stream.runCollect, + Effect.zipWith( + pipe(stream2, Stream.runCollect), + (chunk1, chunk2) => pipe(chunk1, Chunk.appendAll(chunk2)) + ) + ), + result2: pipe( + stream1, + Stream.concat(stream2), + Stream.runCollect + ) + })) + deepStrictEqual(Array.from(result1), Array.from(result2)) + })) + + it.effect("concat - finalizer ordering", () => + Effect.gen(function*() { + const ref = yield* (Ref.make(Chunk.empty())) + yield* pipe( + Stream.finalizer(Ref.update(ref, Chunk.append("Second"))), + Stream.concat(Stream.finalizer(Ref.update(ref, Chunk.append("First")))), + Stream.runDrain + ) + const result = yield* (Ref.get(ref)) + deepStrictEqual(Array.from(result), ["Second", "First"]) + })) +}) diff --git a/repos/effect/packages/effect/test/Stream/conditionals.test.ts b/repos/effect/packages/effect/test/Stream/conditionals.test.ts new file mode 100644 index 0000000..aaed492 --- /dev/null +++ b/repos/effect/packages/effect/test/Stream/conditionals.test.ts @@ -0,0 +1,194 @@ +import { describe, it } from "@effect/vitest" +import { deepStrictEqual } from "@effect/vitest/utils" +import * as Cause from "effect/Cause" +import * as Effect from "effect/Effect" +import * as Exit from "effect/Exit" +import { constFalse, constTrue, constVoid, pipe } from "effect/Function" +import * as Option from "effect/Option" +import * as Stream from "effect/Stream" + +describe("Stream", () => { + it.effect("when - returns the stream if the condition is satisfied", () => + Effect.gen(function*() { + const stream = Stream.make(1, 2, 3, 4, 5) + const { result1, result2 } = yield* (Effect.all({ + result1: pipe(stream, Stream.when(constTrue), Stream.runCollect), + result2: Stream.runCollect(stream) + })) + deepStrictEqual(Array.from(result1), Array.from(result2)) + })) + + it.effect("when - returns an empty stream if the condition is not satisfied", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.make(1, 2, 3, 4, 5), + Stream.when(constFalse), + Stream.runCollect + ) + deepStrictEqual(Array.from(result), []) + })) + + it.effect("when - dies if the condition throws an exception", () => + Effect.gen(function*() { + const error = new Cause.RuntimeException("boom") + const result = yield* pipe( + Stream.make(1, 2, 3), + Stream.when(() => { + throw error + }), + Stream.runDrain, + Effect.exit + ) + deepStrictEqual(result, Exit.die(error)) + })) + + it.effect("whenCase - returns the resulting stream if the given partial function is defined for the given value", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.whenCase( + () => Option.some(1), + (option) => + Option.isSome(option) ? + Option.some(Stream.make(option.value)) : + Option.none() + ), + Stream.runCollect + ) + deepStrictEqual(Array.from(result), [1]) + })) + + it.effect("whenCase - returns an empty stream if the given partial function is not defined for the given value", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.whenCase( + () => Option.none(), + (option) => + Option.isSome(option) ? + Option.some(Stream.make(option.value)) : + Option.none() + ), + Stream.runCollect + ) + deepStrictEqual(Array.from(result), []) + })) + + it.effect("whenCase - dies if evaluating the given value throws an exception", () => + Effect.gen(function*() { + const error = new Cause.RuntimeException("boom") + const result = yield* pipe( + Stream.whenCase( + () => { + throw error + }, + () => Option.some(Stream.empty) + ), + Stream.runDrain, + Effect.exit + ) + deepStrictEqual(result, Exit.die(error)) + })) + + it.effect("whenCase - dies if the partial function throws an exception", () => + Effect.gen(function*() { + const error = new Cause.RuntimeException("boom") + const result = yield* pipe( + Stream.whenCase( + constVoid, + (): Option.Option> => { + throw error + } + ), + Stream.runDrain, + Effect.exit + ) + deepStrictEqual(result, Exit.die(error)) + })) + + it.effect("whenCaseEffect - returns the resulting stream if the given partial function is defined for the given effectful value", () => + Effect.gen(function*() { + const result = yield* pipe( + Effect.succeed(Option.some(1)), + Stream.whenCaseEffect( + (option) => + Option.isSome(option) ? + Option.some(Stream.make(option.value)) : + Option.none() + ), + Stream.runCollect + ) + deepStrictEqual(Array.from(result), [1]) + })) + + it.effect("whenCaseEffect - returns an empty stream if the given partial function is not defined for the given effectful value", () => + Effect.gen(function*() { + const result = yield* pipe( + Effect.succeed>(Option.none()), + Stream.whenCaseEffect( + (option) => + Option.isSome(option) ? + Option.some(Stream.make(option.value)) : + Option.none() + ), + Stream.runCollect + ) + deepStrictEqual(Array.from(result), []) + })) + + it.effect("whenCaseEffect - fails if the effectful value is a failure", () => + Effect.gen(function*() { + const error = new Cause.RuntimeException("boom") + const result = yield* pipe( + Effect.fail(error), + Stream.whenCaseEffect(() => Option.some(Stream.empty)), + Stream.runCollect, + Effect.exit + ) + deepStrictEqual(result, Exit.fail(error)) + })) + + it.effect("whenCaseEffect - dies if the given partial function throws an exception", () => + Effect.gen(function*() { + const error = new Cause.RuntimeException("boom") + const result = yield* pipe( + Effect.void, + Stream.whenCaseEffect((): Option.Option> => { + throw error + }), + Stream.runCollect, + Effect.exit + ) + deepStrictEqual(result, Exit.die(error)) + })) + + it.effect("whenEffect - returns the stream if the effectful condition is satisfied", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.make(1, 2, 3, 4, 5), + Stream.whenEffect(Effect.succeed(true)), + Stream.runCollect + ) + deepStrictEqual(Array.from(result), [1, 2, 3, 4, 5]) + })) + + it.effect("whenEffect - returns an empty stream if the effectful condition is not satisfied", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.make(1, 2, 3, 4, 5), + Stream.whenEffect(Effect.succeed(false)), + Stream.runCollect + ) + deepStrictEqual(Array.from(result), []) + })) + + it.effect("whenEffect - fails if the effectful condition fails", () => + Effect.gen(function*() { + const error = new Cause.RuntimeException("boom") + const result = yield* pipe( + Stream.make(1, 2, 3), + Stream.whenEffect(Effect.fail(error)), + Stream.runDrain, + Effect.exit + ) + deepStrictEqual(result, Exit.fail(error)) + })) +}) diff --git a/repos/effect/packages/effect/test/Stream/constructors.test.ts b/repos/effect/packages/effect/test/Stream/constructors.test.ts new file mode 100644 index 0000000..c7a56de --- /dev/null +++ b/repos/effect/packages/effect/test/Stream/constructors.test.ts @@ -0,0 +1,375 @@ +import { describe, it } from "@effect/vitest" +import { assertFalse, assertLeft, assertTrue, deepStrictEqual } from "@effect/vitest/utils" +import * as Chunk from "effect/Chunk" +import * as Duration from "effect/Duration" +import * as Effect from "effect/Effect" +import * as Either from "effect/Either" +import * as Exit from "effect/Exit" +import * as fc from "effect/FastCheck" +import * as Fiber from "effect/Fiber" +import { identity, pipe } from "effect/Function" +import * as Option from "effect/Option" +import * as Queue from "effect/Queue" +import * as Ref from "effect/Ref" +import * as Schedule from "effect/Schedule" +import * as Stream from "effect/Stream" +import * as TestClock from "effect/TestClock" +import { chunkCoordination } from "../utils/coordination.js" + +const chunkArb = ( + arb: fc.Arbitrary, + constraints?: fc.ArrayConstraints +): fc.Arbitrary> => fc.array(arb, constraints).map(Chunk.fromIterable) + +const grouped = (arr: Array, size: number): Array> => { + const builder: Array> = [] + for (let i = 0; i < arr.length; i = i + size) { + builder.push(arr.slice(i, i + size)) + } + return builder +} + +describe("Stream", () => { + it("concatAll", () => + fc.assert(fc.asyncProperty(fc.array(chunkArb(fc.integer())), async (chunks) => { + const stream = pipe( + Chunk.fromIterable(chunks), + Chunk.map(Stream.fromChunk), + Stream.concatAll + ) + const actual = await Effect.runPromise(Stream.runCollect(stream)) + const expected = Chunk.flatten(Chunk.fromIterable(chunks)) + deepStrictEqual(Array.from(actual), Array.from(expected)) + }))) + + it.effect("finalizer - happy path", () => + Effect.gen(function*() { + const ref = yield* (Ref.make(Chunk.empty())) + yield* pipe( + Stream.acquireRelease( + Ref.update(ref, Chunk.append("Acquire")), + () => Ref.update(ref, Chunk.append("Release")) + ), + Stream.flatMap(() => Stream.finalizer(Ref.update(ref, Chunk.append("Use")))), + Stream.ensuring(Ref.update(ref, Chunk.append("Ensuring"))), + Stream.runDrain + ) + const result = yield* (Ref.get(ref)) + deepStrictEqual(Array.from(result), ["Acquire", "Use", "Release", "Ensuring"]) + })) + + it.effect("finalizer - finalizer is not run if stream is not pulled", () => + Effect.gen(function*() { + const ref = yield* (Ref.make(false)) + // @effect-diagnostics-next-line floatingEffect:off + yield* pipe( + Stream.finalizer(Ref.set(ref, true)), + Stream.toPull, + Effect.scoped + ) + const result = yield* (Ref.get(ref)) + assertFalse(result) + })) + + it("fromChunk", () => + fc.assert(fc.asyncProperty(chunkArb(fc.integer()), async (chunk) => { + const stream = Stream.fromChunk(chunk) + const result = await Effect.runPromise(Stream.runCollect(stream)) + deepStrictEqual(Array.from(result), Array.from(chunk)) + }))) + + it("fromChunks", () => + fc.assert(fc.asyncProperty(fc.array(chunkArb(fc.integer())), async (chunks) => { + const stream = Stream.fromChunks(...chunks) + const result = await Effect.runPromise(Stream.runCollect(stream)) + deepStrictEqual( + Array.from(result), + Array.from(Chunk.flatten(Chunk.fromIterable(chunks))) + ) + }))) + + it.effect("fromChunks - discards empty chunks", () => + Effect.gen(function*() { + const chunks = [Chunk.of(1), Chunk.empty(), Chunk.of(1)] + const result = yield* pipe( + Stream.fromChunks(...chunks), + Stream.toPull, + Effect.flatMap((pull) => + pipe( + Chunk.range(1, 3), + Effect.forEach(() => pipe(Effect.either(pull), Effect.map(Either.map(Chunk.toReadonlyArray)))) + ) + ), + Effect.scoped + ) + deepStrictEqual(Array.from(result), [ + Either.right([1]), + Either.right([1]), + Either.left(Option.none()) + ]) + })) + + it.effect("fromEffect - failure", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.fromEffect(Effect.fail("error")), + Stream.runCollect, + Effect.either + ) + assertLeft(result, "error") + })) + + it.effect("fromEffectOption - emit one element with success", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.fromEffectOption(Effect.succeed(5)), + Stream.runCollect + ) + deepStrictEqual(Array.from(result), [5]) + })) + + it.effect("fromEffectOption - emit one element with failure", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.fromEffectOption(Effect.fail(Option.some(5))), + Stream.runCollect, + Effect.either + ) + assertLeft(result, 5) + })) + + it.effect("fromEffectOption - do not emit any element", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.fromEffectOption(Effect.fail(Option.none())), + Stream.runCollect + ) + deepStrictEqual(Array.from(result), []) + })) + + it.effect("fromSchedule", () => + Effect.gen(function*() { + const schedule = pipe( + Schedule.exponential(Duration.seconds(1)), + Schedule.zipLeft(Schedule.recurs(5)) + ) + const fiber = yield* pipe( + Stream.fromSchedule(schedule), + Stream.runCollect, + Effect.fork + ) + yield* (TestClock.adjust(Duration.seconds(62))) + const result = yield* (Fiber.join(fiber)) + const expected = [ + Duration.seconds(1), + Duration.seconds(2), + Duration.seconds(4), + Duration.seconds(8), + Duration.seconds(16) + ] + deepStrictEqual(Array.from(result), expected) + })) + + it.effect("fromQueue - emits queued elements", () => + Effect.gen(function*() { + const coordination = yield* (chunkCoordination([Chunk.make(1, 2)])) + const fiber = yield* pipe( + Stream.fromQueue(coordination.queue), + Stream.filterMapWhile(Exit.match({ + onFailure: Option.none, + onSuccess: Option.some + })), + Stream.flattenChunks, + Stream.tap(() => coordination.proceed), + Stream.runCollect, + Effect.fork + ) + yield* (coordination.offer) + const result = yield* (Fiber.join(fiber)) + deepStrictEqual(Array.from(result), [1, 2]) + })) + + it.effect("fromQueue - chunks up to the max chunk size", () => + Effect.gen(function*() { + const queue = yield* (Queue.unbounded()) + yield* (Queue.offerAll(queue, [1, 2, 3, 4, 5, 6, 7])) + const result = yield* pipe( + Stream.fromQueue(queue, { maxChunkSize: 2 }), + Stream.mapChunks((chunk) => Chunk.of(Array.from(chunk))), + Stream.take(3), + Stream.runCollect + ) + assertTrue(Array.from(result).every((array) => array.length <= 2)) + })) + + it.effect("fromAsyncIterable", () => + Effect.gen(function*() { + async function* asyncIterable() { + yield 1 + yield 2 + yield 3 + } + + const stream = Stream.fromAsyncIterable(asyncIterable(), identity) + const result = yield* (Stream.runCollect(stream)) + deepStrictEqual(Array.from(result), [1, 2, 3]) + })) + + it.effect("fromReadableStream", () => + Effect.gen(function*() { + class FromReadableStreamError { + readonly _tag = "FromReadableStreamError" + constructor(readonly error: unknown) {} + } + class NumberSource implements UnderlyingDefaultSource { + #counter = 0 + pull(controller: ReadableStreamDefaultController) { + controller.enqueue(this.#counter) + this.#counter = this.#counter + 1 + } + } + + const result = yield* pipe( + Stream.fromReadableStream({ + evaluate: () => new ReadableStream(new NumberSource()), + onError: (error) => new FromReadableStreamError(error) + }), + Stream.take(10), + Stream.runCollect + ) + + deepStrictEqual(Array.from(result), Array.from({ length: 10 }, (_, i) => i)) + })) + + it.effect("iterate", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.iterate(1, (n) => n + 1), + Stream.take(10), + Stream.runCollect + ) + deepStrictEqual(Array.from(result), Array.from(Chunk.range(1, 10))) + })) + + it.effect("range - includes both endpoints", () => + Effect.gen(function*() { + const result = yield* (Stream.runCollect(Stream.range(1, 2))) + deepStrictEqual(Array.from(result), [1, 2]) + })) + + it.effect("range - two large ranges can be concatenated", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.range(1, 1_000), + Stream.concat(Stream.range(1_001, 2_000)), + Stream.runCollect + ) + deepStrictEqual(Array.from(result), Array.from(Chunk.range(1, 2000))) + })) + + it.effect("range - two small ranges can be concatenated", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.range(1, 10), + Stream.concat(Stream.range(11, 20)), + Stream.runCollect + ) + deepStrictEqual(Array.from(result), Array.from(Chunk.range(1, 20))) + })) + + it.effect("range - emits no values when start > end", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.range(2, 1), + Stream.runCollect + ) + deepStrictEqual(Array.from(result), []) + })) + + it.effect("range - emits 1 value when start === end", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.range(1, 1), + Stream.runCollect + ) + deepStrictEqual(Array.from(result), [1]) + })) + + it.effect("range - emits values in chunks of chunkSize", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.range(1, 9, 2), + Stream.mapChunks((chunk) => Chunk.make(pipe(chunk, Chunk.reduce(0, (x, y) => x + y)))), + Stream.runCollect + ) + deepStrictEqual( + Array.from(result), + [1 + 2, 3 + 4, 5 + 6, 7 + 8, 9] + ) + })) + + it("rechunk", () => + fc.assert( + fc.asyncProperty(fc.array(chunkArb(fc.integer())), fc.integer({ min: 1, max: 100 }), async (chunks, n) => { + const stream = pipe( + Stream.fromChunks(...chunks), + Stream.rechunk(n), + Stream.mapChunks(Chunk.of) + ) + const actual = await Effect.runPromise(Stream.runCollect(stream)) + const expected = chunks.map((chunk) => Array.from(chunk)).flat() + deepStrictEqual( + Array.from(actual).map((chunk) => Array.from(chunk)), + grouped(expected, n) + ) + }) + )) + + it.effect("unfold", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.unfold(0, (n) => + n < 10 ? + Option.some([n, n + 1] as const) : + Option.none()), + Stream.runCollect + ) + deepStrictEqual(Array.from(result), Array.from(Chunk.range(0, 9))) + })) + + it.effect("unfoldChunk", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.unfoldChunk(0, (n) => + n < 10 ? + Option.some([Chunk.make(n, n + 1), n + 2] as const) : + Option.none()), + Stream.runCollect + ) + deepStrictEqual(Array.from(result), Array.from(Chunk.range(0, 9))) + })) + + it.effect("unfoldChunkEffect", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.unfoldChunkEffect(0, (n) => + n < 10 ? + Effect.succeed(Option.some([Chunk.make(n, n + 1), n + 2] as const)) : + Effect.succeed(Option.none())), + Stream.runCollect + ) + deepStrictEqual(Array.from(result), Array.from(Chunk.range(0, 9))) + })) + + it.effect("unfoldEffect", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.unfoldEffect(0, (n) => + n < 10 ? + Effect.succeed(Option.some([n, n + 1] as const)) : + Effect.succeed(Option.none())), + Stream.runCollect + ) + deepStrictEqual(Array.from(result), Array.from(Chunk.range(0, 9))) + })) +}) diff --git a/repos/effect/packages/effect/test/Stream/conversions.test.ts b/repos/effect/packages/effect/test/Stream/conversions.test.ts new file mode 100644 index 0000000..e1e23c9 --- /dev/null +++ b/repos/effect/packages/effect/test/Stream/conversions.test.ts @@ -0,0 +1,85 @@ +import { describe, it } from "@effect/vitest" +import { deepStrictEqual } from "@effect/vitest/utils" +import * as Cause from "effect/Cause" +import * as Chunk from "effect/Chunk" +import * as Effect from "effect/Effect" +import * as Exit from "effect/Exit" +import { pipe } from "effect/Function" +import * as Queue from "effect/Queue" +import * as Stream from "effect/Stream" +import * as Take from "effect/Take" + +describe("Stream", () => { + it.effect("toQueue", () => + Effect.gen(function*() { + const chunk = Chunk.make(1, 2, 3) + const stream = pipe( + Stream.fromChunk(chunk), + Stream.flatMap(Stream.succeed) + ) + const result = yield* pipe( + stream, + Stream.toQueue({ capacity: 1_000 }), + Effect.flatMap((queue) => + pipe( + Queue.size(queue), + Effect.repeat({ while: (size) => size !== chunk.length + 1 }), + Effect.zipRight(Queue.takeAll(queue)) + ) + ), + Effect.scoped + ) + const expected = pipe( + chunk, + Chunk.map(Take.of), + Chunk.append(Take.end) + ) + deepStrictEqual(Array.from(result), Array.from(expected)) + })) + + it.effect("toQueueUnbounded", () => + Effect.gen(function*() { + const chunk = Chunk.make(1, 2, 3) + const stream = pipe( + Stream.fromChunk(chunk), + Stream.flatMap(Stream.succeed) + ) + const result = yield* pipe( + Stream.toQueue(stream, { strategy: "unbounded" }), + Effect.flatMap((queue) => + pipe( + Queue.size(queue), + Effect.repeat({ while: (size) => size !== chunk.length + 1 }), + Effect.zipRight(Queue.takeAll(queue)) + ) + ), + Effect.scoped + ) + const expected = pipe( + chunk, + Chunk.map(Take.of), + Chunk.append(Take.end) + ) + deepStrictEqual(Array.from(result), Array.from(expected)) + })) + + it.effect("toQueueOfElements - propagates defects", () => + Effect.gen(function*() { + const queue = yield* pipe( + Stream.dieMessage("die"), + Stream.toQueueOfElements({ capacity: 1 }), + Effect.flatMap(Queue.take), + Effect.scoped + ) + deepStrictEqual(queue, Exit.die(new Cause.RuntimeException("die"))) + })) + + it("toAsyncIterable", async () => { + const stream = Stream.make(1, 2, 3) + const results: Array = [] + for await (const result of Stream.toAsyncIterable(stream)) { + results.push(result) + } + deepStrictEqual(results, [1, 2, 3]) + }) +}) diff --git a/repos/effect/packages/effect/test/Stream/distributing.test.ts b/repos/effect/packages/effect/test/Stream/distributing.test.ts new file mode 100644 index 0000000..5992f11 --- /dev/null +++ b/repos/effect/packages/effect/test/Stream/distributing.test.ts @@ -0,0 +1,51 @@ +import { describe, it } from "@effect/vitest" +import { strictEqual } from "@effect/vitest/utils" +import * as Deferred from "effect/Deferred" +import * as Effect from "effect/Effect" +import * as Exit from "effect/Exit" +import { constTrue, pipe } from "effect/Function" +import * as Option from "effect/Option" +import * as Stream from "effect/Stream" + +describe("Stream", () => { + it.effect("distributedWithDynamic - ensures no race between subscription and stream end", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.empty, + Stream.distributedWithDynamic({ + maximumLag: 1, + decide: () => Effect.succeed(constTrue) + }), + Effect.flatMap((add) => { + const subscribe = pipe( + add, + Effect.map(([_, queue]) => + pipe( + Stream.fromQueue(queue), + Stream.filterMapWhile(Exit.match({ + onFailure: Option.none, + onSuccess: Option.some + })) + ) + ), + Stream.unwrap + ) + return pipe( + Deferred.make(), + Effect.flatMap((onEnd) => + pipe( + subscribe, + Stream.ensuring(Deferred.succeed(onEnd, void 0)), + Stream.runDrain, + Effect.fork, + Effect.zipRight(Deferred.await(onEnd)), + Effect.zipRight(Stream.runDrain(subscribe)) + ) + ) + ) + }), + Effect.scoped + ) + strictEqual(result, undefined) + })) +}) diff --git a/repos/effect/packages/effect/test/Stream/do-notation.test.ts b/repos/effect/packages/effect/test/Stream/do-notation.test.ts new file mode 100644 index 0000000..0084312 --- /dev/null +++ b/repos/effect/packages/effect/test/Stream/do-notation.test.ts @@ -0,0 +1,79 @@ +import { describe, it } from "@effect/vitest" +import * as Util from "@effect/vitest/utils" +import * as Chunk from "effect/Chunk" +import * as Effect from "effect/Effect" +import * as Either from "effect/Either" +import { pipe } from "effect/Function" +import * as Stream from "effect/Stream" + +const expectRight = (s: Stream.Stream, expected: R) => { + Util.deepStrictEqual(Chunk.toArray(Effect.runSync(Stream.runCollect(Stream.either(s)))), [Either.right(expected)]) +} + +const expectLeft = (s: Stream.Stream, expected: L) => { + Util.deepStrictEqual(Chunk.toArray(Effect.runSync(Stream.runCollect(Stream.either(s)))), [Either.left(expected)]) +} + +describe("do notation", () => { + it("Do", () => { + expectRight(Stream.Do, {}) + }) + + it("bindTo", () => { + expectRight(pipe(Stream.succeed(1), Stream.bindTo("a")), { a: 1 }) + expectLeft(pipe(Stream.fail("left"), Stream.bindTo("a")), "left") + expectRight( + pipe( + Stream.succeed(1), + Stream.bindTo("__proto__"), + Stream.let("x", () => 2) + ), + { x: 2, ["__proto__"]: 1 } + ) + }) + + it("bind", () => { + expectRight(pipe(Stream.succeed(1), Stream.bindTo("a"), Stream.bind("b", ({ a }) => Stream.succeed(a + 1))), { + a: 1, + b: 2 + }) + expectLeft( + pipe(Stream.succeed(1), Stream.bindTo("a"), Stream.bind("b", () => Stream.fail("left"))), + "left" + ) + expectLeft( + pipe(Stream.fail("left"), Stream.bindTo("a"), Stream.bind("b", () => Stream.succeed(2))), + "left" + ) + expectRight( + pipe( + Stream.succeed(1), + Stream.bindTo("a"), + (x) => + pipe( + x, + Stream.bind("__proto__", ({ a }) => Stream.succeed(a + 1)) + ) as Stream.Stream<{ a: number; __proto__: number }, unknown, never>, + Stream.bind("x", () => Stream.succeed(2)) + ), + { a: 1, x: 2, ["__proto__"]: 2 } + ) + }) + + it("let", () => { + expectRight(pipe(Stream.succeed(1), Stream.bindTo("a"), Stream.let("b", ({ a }) => a + 1)), { a: 1, b: 2 }) + expectLeft( + pipe(Stream.fail("left"), Stream.bindTo("a"), Stream.let("b", () => 2)), + "left" + ) + expectRight( + pipe( + Stream.succeed(1), + Stream.bindTo("a"), + Stream.let("__proto__", ({ a }) => a + 1), + Stream.let("x", ({ a }) => a + 2) + ), + { a: 1, x: 3, ["__proto__"]: 2 } + ) + }) +}) diff --git a/repos/effect/packages/effect/test/Stream/draining.test.ts b/repos/effect/packages/effect/test/Stream/draining.test.ts new file mode 100644 index 0000000..68b57c9 --- /dev/null +++ b/repos/effect/packages/effect/test/Stream/draining.test.ts @@ -0,0 +1,95 @@ +import { describe, it } from "@effect/vitest" +import { assertLeft, assertTrue, deepStrictEqual, strictEqual } from "@effect/vitest/utils" +import * as Cause from "effect/Cause" +import * as Chunk from "effect/Chunk" +import * as Deferred from "effect/Deferred" +import * as Effect from "effect/Effect" +import * as Exit from "effect/Exit" +import { pipe } from "effect/Function" +import * as Ref from "effect/Ref" +import * as Stream from "effect/Stream" + +describe("Stream", () => { + it.effect("drain - simple example", () => + Effect.gen(function*() { + const ref = yield* (Ref.make(Chunk.empty())) + yield* pipe( + Stream.range(0, 9), + Stream.mapEffect((n) => Ref.update(ref, Chunk.append(n))), + Stream.drain, + Stream.runDrain + ) + const result = yield* (Ref.get(ref)) + deepStrictEqual(Array.from(result), Array.from(Chunk.range(0, 9))) + })) + + it.effect("drain - is not too eager", () => + Effect.gen(function*() { + const ref = yield* (Ref.make(0)) + const result1 = yield* pipe( + Stream.make(1), + Stream.tap((n) => Ref.set(ref, n)), + Stream.concat(Stream.fail("fail")), + Stream.runDrain, + Effect.either + ) + const result2 = yield* (Ref.get(ref)) + assertLeft(result1, "fail") + strictEqual(result2, 1) + })) + + it.effect("drainFork - runs the other stream in the background", () => + Effect.gen(function*() { + const latch = yield* (Deferred.make()) + const result = yield* pipe( + Stream.fromEffect(Deferred.await(latch)), + Stream.drainFork(Stream.fromEffect(Deferred.succeed(latch, void 0))), + Stream.runDrain + ) + strictEqual(result, undefined) + })) + + it.effect("drainFork - interrupts the background stream when the foreground exits", () => + Effect.gen(function*() { + const ref = yield* (Ref.make(false)) + const latch = yield* (Deferred.make()) + yield* pipe( + Stream.make(1, 2, 3), + Stream.concat(Stream.drain(Stream.fromEffect(Deferred.await(latch)))), + Stream.drainFork( + pipe( + Deferred.succeed(latch, void 0), + Effect.zipRight(Effect.never), + Effect.onInterrupt(() => Ref.set(ref, true)), + Stream.fromEffect + ) + ), + Stream.runDrain + ) + const result = yield* (Ref.get(ref)) + assertTrue(result) + })) + + it.effect("drainFork - fails the foreground stream if the background fails with a typed error", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.never, + Stream.drainFork(Stream.fail("boom")), + Stream.runDrain, + Effect.exit + ) + deepStrictEqual(result, Exit.fail("boom")) + })) + + it.effect("drainFork - fails the foreground stream if the background fails with a defect", () => + Effect.gen(function*() { + const error = new Cause.RuntimeException("boom") + const result = yield* pipe( + Stream.never, + Stream.drainFork(Stream.die(error)), + Stream.runDrain, + Effect.exit + ) + deepStrictEqual(result, Exit.die(error)) + })) +}) diff --git a/repos/effect/packages/effect/test/Stream/dropping.test.ts b/repos/effect/packages/effect/test/Stream/dropping.test.ts new file mode 100644 index 0000000..2cd1b06 --- /dev/null +++ b/repos/effect/packages/effect/test/Stream/dropping.test.ts @@ -0,0 +1,92 @@ +import { describe, it } from "@effect/vitest" +import { assertLeft, assertRight, deepStrictEqual } from "@effect/vitest/utils" +import * as Chunk from "effect/Chunk" +import * as Effect from "effect/Effect" +import { constTrue, pipe } from "effect/Function" +import * as Stream from "effect/Stream" + +describe("Stream", () => { + it.effect("drop - simple example", () => + Effect.gen(function*() { + const n = 2 + const stream = Stream.make(1, 2, 3, 4, 5) + const { result1, result2 } = yield* (Effect.all({ + result1: pipe(stream, Stream.drop(n), Stream.runCollect), + result2: pipe(stream, Stream.runCollect, Effect.map(Chunk.drop(n))) + })) + deepStrictEqual(Array.from(result1), Array.from(result2)) + })) + + it.effect("drop - does not swallow errors", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.fail("Ouch"), + Stream.concat(Stream.make(1)), + Stream.drop(1), + Stream.runDrain, + Effect.either + ) + assertLeft(result, "Ouch") + })) + + it.effect("dropRight - simple example", () => + Effect.gen(function*() { + const n = 2 + const stream = Stream.make(1, 2, 3, 4, 5) + const { result1, result2 } = yield* (Effect.all({ + result1: pipe(stream, Stream.dropRight(n), Stream.runCollect), + result2: pipe(stream, Stream.runCollect, Effect.map(Chunk.dropRight(n))) + })) + deepStrictEqual(Array.from(result1), Array.from(result2)) + })) + + it.effect("dropRight - does not swallow errors", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.make(1), + Stream.concat(Stream.fail("Ouch")), + Stream.dropRight(1), + Stream.runDrain, + Effect.either + ) + assertLeft(result, "Ouch") + })) + + it.effect("dropUntil", () => + Effect.gen(function*() { + const stream = Stream.make(1, 2, 3, 4, 5) + const f = (n: number) => n < 3 + const { result1, result2 } = yield* (Effect.all({ + result1: pipe(stream, Stream.dropUntil(f), Stream.runCollect), + result2: pipe( + Stream.runCollect(stream), + Effect.map((chunk) => pipe(chunk, Chunk.dropWhile((n) => !f(n)), Chunk.drop(1))) + ) + })) + deepStrictEqual(Array.from(result1), Array.from(result2)) + })) + + it.effect("dropWhile", () => + Effect.gen(function*() { + const stream = Stream.make(1, 2, 3, 4, 5) + const f = (n: number) => n < 3 + const { result1, result2 } = yield* (Effect.all({ + result1: pipe(stream, Stream.dropWhile(f), Stream.runCollect), + result2: pipe(stream, Stream.runCollect, Effect.map(Chunk.dropWhile(f))) + })) + deepStrictEqual(Array.from(result1), Array.from(result2)) + })) + + it.effect("dropWhile - short circuits", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.make(1), + Stream.concat(Stream.fail("Ouch")), + Stream.take(1), + Stream.dropWhile(constTrue), + Stream.runDrain, + Effect.either + ) + assertRight(result, void 0) + })) +}) diff --git a/repos/effect/packages/effect/test/Stream/encoding.test.ts b/repos/effect/packages/effect/test/Stream/encoding.test.ts new file mode 100644 index 0000000..46aa43c --- /dev/null +++ b/repos/effect/packages/effect/test/Stream/encoding.test.ts @@ -0,0 +1,66 @@ +import { describe, it } from "@effect/vitest" +import { deepStrictEqual, strictEqual } from "@effect/vitest/utils" +import * as Chunk from "effect/Chunk" +import * as Effect from "effect/Effect" +import { pipe } from "effect/Function" +import * as Stream from "effect/Stream" + +describe("Stream", () => { + it.effect("decodeText/encodeText round trip", () => + Effect.gen(function*() { + const items = ["a", "b", "c", "d", "e", "f", "g", "h", "i"] + const encoded = yield* pipe( + Stream.fromIterable(items), + Stream.encodeText, + Stream.runCollect + ) + strictEqual(encoded.length, 9) + const decoded = yield* pipe( + Stream.fromChunk(encoded), + Stream.decodeText(), + Stream.runCollect + ) + deepStrictEqual(Chunk.toReadonlyArray(decoded), items) + })) + + it.effect("decodeText handles multi-byte characters split across chunks", () => + Effect.gen(function*() { + // 🌍 is U+1F30D — four UTF-8 bytes: [0xF0, 0x9F, 0x8C, 0x8D] + const bytes = new TextEncoder().encode("🌍") + + // Split the bytes mid-character across two chunks + const stream = Stream.fromChunks( + Chunk.of(bytes.slice(0, 2)), // [0xF0, 0x9F] + Chunk.of(bytes.slice(2, 4)) // [0x8C, 0x8D] + ) + + const result = yield* pipe( + stream, + Stream.decodeText(), + Stream.mkString + ) + + strictEqual(result, "🌍") + })) + + it.effect("decodeText handles mixed ASCII and multi-byte characters across chunks", () => + Effect.gen(function*() { + // "Hello 🌍!" encoded as UTF-8 + const bytes = new TextEncoder().encode("Hello 🌍!") + + // Split in the middle of the emoji (after "Hello " + first 2 bytes of emoji) + const splitPoint = 6 + 2 // "Hello " is 6 bytes, then 2 of 4 emoji bytes + const stream = Stream.fromChunks( + Chunk.of(bytes.slice(0, splitPoint)), + Chunk.of(bytes.slice(splitPoint)) + ) + + const result = yield* pipe( + stream, + Stream.decodeText(), + Stream.mkString + ) + + strictEqual(result, "Hello 🌍!") + })) +}) diff --git a/repos/effect/packages/effect/test/Stream/environment.test.ts b/repos/effect/packages/effect/test/Stream/environment.test.ts new file mode 100644 index 0000000..2366ce2 --- /dev/null +++ b/repos/effect/packages/effect/test/Stream/environment.test.ts @@ -0,0 +1,221 @@ +import { describe, it } from "@effect/vitest" +import { deepStrictEqual, strictEqual } from "@effect/vitest/utils" +import * as Array from "effect/Array" +import * as Chunk from "effect/Chunk" +import * as Context from "effect/Context" +import * as Effect from "effect/Effect" +import * as Exit from "effect/Exit" +import { pipe } from "effect/Function" +import * as Layer from "effect/Layer" +import * as Stream from "effect/Stream" +import type * as Tracer from "effect/Tracer" + +interface StringService { + readonly string: string +} + +const StringService = Context.GenericTag("string") + +describe("Stream", () => { + it.effect("context", () => + Effect.gen(function*() { + const context = pipe( + Context.empty(), + Context.add(StringService, { string: "test" }) + ) + const result = yield* pipe( + Stream.context(), + Stream.map(Context.get(StringService)), + Stream.provideContext(context), + Stream.runCollect + ) + deepStrictEqual(Chunk.toReadonlyArray(result), [{ string: "test" }]) + })) + + it.effect("contextWith", () => + Effect.gen(function*() { + const result = yield* pipe( + StringService, + Stream.provideContext( + pipe( + Context.empty(), + Context.add(StringService, { string: "test" }) + ) + ), + Stream.runHead, + Effect.flatten + ) + deepStrictEqual(result, { string: "test" }) + })) + + it.effect("contextWithEffect - success", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.contextWithEffect((context: Context.Context) => + Effect.succeed(pipe(context, Context.get(StringService))) + ), + Stream.provideContext( + pipe( + Context.empty(), + Context.add(StringService, { string: "test" }) + ) + ), + Stream.runHead, + Effect.flatten + ) + deepStrictEqual(result, { string: "test" }) + })) + + it.effect("contextWithEffect - fails", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.contextWithEffect((_: Context.Context) => Effect.fail("boom")), + Stream.provideContext( + pipe( + Context.empty(), + Context.add(StringService, { string: "test" }) + ) + ), + Stream.runHead, + Effect.exit + ) + deepStrictEqual(result, Exit.fail("boom")) + })) + + it.effect("contextWithStream - success", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.contextWithStream((context: Context.Context) => + Stream.succeed(pipe(context, Context.get(StringService))) + ), + Stream.provideContext( + pipe( + Context.empty(), + Context.add(StringService, { string: "test" }) + ) + ), + Stream.runHead, + Effect.flatten + ) + deepStrictEqual(result, { string: "test" }) + })) + + it.effect("contextWithStream - fails", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.contextWithStream((_: Context.Context) => Stream.fail("boom")), + Stream.provideContext( + pipe( + Context.empty(), + Context.add(StringService, { string: "test" }) + ) + ), + Stream.runHead, + Effect.exit + ) + deepStrictEqual(result, Exit.fail("boom")) + })) + + it.effect("provide", () => + Effect.gen(function*() { + const stream = StringService + const layer = Layer.succeed(StringService, { string: "test" }) + const result = yield* pipe( + stream, + Stream.provideLayer(layer), + Stream.map((s) => s.string), + Stream.runCollect + ) + deepStrictEqual(Chunk.toReadonlyArray(result), ["test"]) + })) + + it.effect("provideServiceStream", () => + Effect.gen(function*() { + const stream = StringService + const service = Stream.succeed({ string: "test" }) + const result = yield* pipe( + stream, + Stream.provideServiceStream(StringService, service), + Stream.map((s) => s.string), + Stream.runCollect + ) + deepStrictEqual(Chunk.toReadonlyArray(result), ["test"]) + })) + + it.effect("serviceWith", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.map(StringService, (service) => service.string), + Stream.provideLayer(Layer.succeed(StringService, { string: "test" })), + Stream.runCollect + ) + deepStrictEqual(Chunk.toReadonlyArray(result), ["test"]) + })) + + it.effect("serviceWithEffect", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.mapEffect(StringService, (service) => Effect.succeed(service.string)), + Stream.provideLayer(Layer.succeed(StringService, { string: "test" })), + Stream.runCollect + ) + deepStrictEqual(Chunk.toReadonlyArray(result), ["test"]) + })) + + it.effect("serviceWithStream", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.flatMap(StringService, (service) => Stream.succeed(service.string)), + Stream.provideLayer(Layer.succeed(StringService, { string: "test" })), + Stream.runCollect + ) + deepStrictEqual(Chunk.toReadonlyArray(result), ["test"]) + })) + + it.effect("deep provide", () => + Effect.gen(function*() { + const messages: Array = [] + const effect = Effect.acquireRelease( + pipe(StringService, Effect.tap((s) => Effect.sync(() => messages.push(s.string)))), + () => pipe(StringService, Effect.tap((s) => Effect.sync(() => messages.push(s.string)))) + ) + const L0 = Layer.succeed(StringService, { string: "test0" }) + const L1 = Layer.succeed(StringService, { string: "test1" }) + const L2 = Layer.succeed(StringService, { string: "test2" }) + const stream = pipe( + Stream.scoped(effect), + Stream.provideSomeLayer(L1), + Stream.concat(pipe(Stream.scoped(effect), Stream.provideSomeLayer(L2))), + Stream.provideSomeLayer(L0) + ) + yield* (Stream.runDrain(stream)) + deepStrictEqual(messages, ["test1", "test1", "test2", "test2"]) + })) + + it.effect("withSpan", () => + Effect.gen(function*() { + const spans = yield* pipe( + Stream.make(1, 2, 3), + Stream.mapEffect((i) => + Effect.withSpan( + Effect.currentSpan, + `span.${i}` + ) + ), + Stream.withSpan("span"), + Stream.runCollect, + Effect.map(Chunk.toReadonlyArray) + ) + strictEqual(spans.length, 3) + deepStrictEqual( + pipe( + Array.map(spans, (s) => s.parent), + Array.getSomes, + Array.filter((s): s is Tracer.Span => s._tag === "Span"), + Array.map((s) => s.name) + ), + ["span", "span", "span"] + ) + deepStrictEqual(Array.map(spans, (s) => s.name), ["span.1", "span.2", "span.3"]) + })) +}) diff --git a/repos/effect/packages/effect/test/Stream/error-handling.test.ts b/repos/effect/packages/effect/test/Stream/error-handling.test.ts new file mode 100644 index 0000000..abf60b8 --- /dev/null +++ b/repos/effect/packages/effect/test/Stream/error-handling.test.ts @@ -0,0 +1,362 @@ +import { describe, it } from "@effect/vitest" +import { assertLeft, assertTrue, deepStrictEqual } from "@effect/vitest/utils" +import * as Cause from "effect/Cause" +import * as Chunk from "effect/Chunk" +import * as Effect from "effect/Effect" +import * as Either from "effect/Either" +import * as Exit from "effect/Exit" +import { identity, pipe } from "effect/Function" +import * as Option from "effect/Option" +import * as Ref from "effect/Ref" +import * as Stream from "effect/Stream" + +describe("Stream", () => { + it.effect("absolve - happy path", () => + Effect.gen(function*() { + const chunk = Chunk.range(1, 10) + const result = yield* pipe( + chunk, + Chunk.map(Either.right), + Stream.fromIterable, + Stream.mapEffect(identity), + Stream.runCollect + ) + deepStrictEqual(Array.from(result), Array.from(chunk)) + })) + + it.effect("absolve - failure", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.fromIterable(pipe(Chunk.range(1, 10), Chunk.map(Either.right))), + Stream.concat(Stream.succeed(Either.left("Ouch"))), + Stream.mapEffect(identity), + Stream.runCollect, + Effect.exit + ) + deepStrictEqual(result, Exit.fail("Ouch")) + })) + + it.effect("absolve - round trip #1", () => + Effect.gen(function*() { + const xss = Stream.fromIterable(pipe(Chunk.range(1, 10), Chunk.map(Either.right))) + const stream = pipe(xss, Stream.concat(Stream.succeed(Either.left("Ouch"))), Stream.concat(xss)) + const { result1, result2 } = yield* (Effect.all({ + result1: Stream.runCollect(stream), + result2: pipe(Stream.mapEffect(stream, identity), Stream.either, Stream.runCollect) + })) + deepStrictEqual( + Array.from(pipe(result1, Chunk.take(result2.length))), + Array.from(result2) + ) + })) + + it.effect("absolve - round trip #2", () => + Effect.gen(function*() { + const xss = Stream.fromIterable(pipe(Chunk.range(1, 10), Chunk.map(Either.right))) + const stream = pipe(xss, Stream.concat(Stream.fail("Ouch"))) + const { result1, result2 } = yield* (Effect.all({ + result1: Effect.exit(Stream.runCollect(stream)), + result2: pipe(stream, Stream.either, Stream.mapEffect(identity), Stream.runCollect, Effect.exit) + })) + deepStrictEqual(result1, Exit.fail("Ouch")) + deepStrictEqual(result2, Exit.fail("Ouch")) + })) + + it.effect("catchAllCause - recovery from errors", () => + Effect.gen(function*() { + const stream1 = pipe(Stream.make(1, 2), Stream.concat(Stream.fail("boom"))) + const stream2 = Stream.make(3, 4) + const result = yield* pipe( + stream1, + Stream.catchAllCause(() => stream2), + Stream.runCollect + ) + deepStrictEqual(Array.from(result), [1, 2, 3, 4]) + })) + + it.effect("catchAllCause - recovery from defects", () => + Effect.gen(function*() { + const stream1 = pipe(Stream.make(1, 2), Stream.concat(Stream.dieMessage("boom"))) + const stream2 = Stream.make(3, 4) + const result = yield* pipe( + stream1, + Stream.catchAllCause(() => stream2), + Stream.runCollect + ) + deepStrictEqual(Array.from(result), [1, 2, 3, 4]) + })) + + it.effect("catchAllCause - happy path", () => + Effect.gen(function*() { + const stream1 = Stream.make(1, 2) + const stream2 = Stream.make(3, 4) + const result = yield* pipe( + stream1, + Stream.catchAllCause(() => stream2), + Stream.runCollect + ) + deepStrictEqual(Array.from(result), [1, 2]) + })) + + it.effect("catchAllCause - executes finalizers", () => + Effect.gen(function*() { + const ref = yield* (Ref.make(Chunk.empty())) + const stream1 = pipe( + Stream.make(1, 2), + Stream.concat(Stream.fail("boom")), + Stream.ensuring(Ref.update(ref, Chunk.append("s1"))) + ) + const stream2 = pipe( + Stream.make(1, 2), + Stream.concat(Stream.fail("boom")), + Stream.ensuring(Ref.update(ref, Chunk.append("s2"))) + ) + yield* pipe( + stream1, + Stream.catchAllCause(() => stream2), + Stream.runCollect, + Effect.exit + ) + const result = yield* (Ref.get(ref)) + deepStrictEqual(Array.from(result), ["s1", "s2"]) + })) + + it.effect("catchAllCause - releases all resources by the time the failover stream has started", () => + Effect.gen(function*() { + const ref = yield* (Ref.make(Chunk.empty())) + const stream = pipe( + Stream.finalizer(Ref.update(ref, Chunk.append(1))), + Stream.crossRight(Stream.finalizer(Ref.update(ref, Chunk.append(2)))), + Stream.crossRight(Stream.finalizer(Ref.update(ref, Chunk.append(3)))), + Stream.crossRight(Stream.fail("boom")) + ) + const result = yield* pipe( + Stream.drain(stream), + Stream.catchAllCause(() => Stream.fromEffect(Ref.get(ref))), + Stream.runCollect + ) + deepStrictEqual(Array.from(Chunk.flatten(result)), [3, 2, 1]) + })) + + it.effect("catchAllCause - propagates the right Exit value to the failing stream (ZIO #3609)", () => + Effect.gen(function*() { + const ref = yield* (Ref.make>(Exit.void)) + yield* pipe( + Stream.acquireRelease( + Effect.void, + (_, exit) => Ref.set(ref, exit) + ), + Stream.flatMap(() => Stream.fail("boom")), + Stream.either, + Stream.runDrain, + Effect.exit + ) + const result = yield* (Ref.get(ref)) + deepStrictEqual(result, Exit.fail("boom")) + })) + + it.effect("catchSome - recovery from some errors", () => + Effect.gen(function*() { + const stream1 = pipe( + Stream.make(1, 2), + Stream.concat(Stream.fail("boom")) + ) + const stream2 = Stream.make(3, 4) + const result = yield* pipe( + stream1, + Stream.catchSome((error) => error === "boom" ? Option.some(stream2) : Option.none()), + Stream.runCollect + ) + deepStrictEqual(Array.from(result), [1, 2, 3, 4]) + })) + + it.effect("catchSome - fails stream when partial function does not match", () => + Effect.gen(function*() { + const stream1 = pipe( + Stream.make(1, 2), + Stream.concat(Stream.fail("boom")) + ) + const stream2 = Stream.make(3, 4) + const result = yield* pipe( + stream1, + Stream.catchSome((error) => error === "boomer" ? Option.some(stream2) : Option.none()), + Stream.runCollect, + Effect.either + ) + assertLeft(result, "boom") + })) + + it.effect("catchSomeCause - recovery from some errors", () => + Effect.gen(function*() { + const stream1 = pipe( + Stream.make(1, 2), + Stream.concat(Stream.failCause(Cause.fail("boom"))) + ) + const stream2 = Stream.make(3, 4) + const result = yield* pipe( + stream1, + Stream.catchSomeCause((cause) => + Cause.isFailType(cause) && cause.error === "boom" ? + Option.some(stream2) : + Option.none() + ), + Stream.runCollect + ) + deepStrictEqual(Array.from(result), [1, 2, 3, 4]) + })) + + it.effect("catchSomeCause - fails stream when partial function does not match", () => + Effect.gen(function*() { + const stream1 = pipe( + Stream.make(1, 2), + Stream.concat(Stream.fail("boom")) + ) + const stream2 = Stream.make(3, 4) + const result = yield* pipe( + stream1, + Stream.catchSomeCause((cause) => + Cause.isEmpty(cause) ? + Option.some(stream2) : + Option.none() + ), + Stream.runCollect, + Effect.either + ) + assertLeft(result, "boom") + })) + + it.effect("catchTag", () => + Effect.gen(function*() { + class ErrorA { + readonly _tag = "ErrorA" + } + class ErrorB { + readonly _tag = "ErrorB" + } + + const result1 = yield* pipe( + Stream.fail(new ErrorA()), + Stream.catchTag("ErrorA", () => Stream.make(1, 2)), + Stream.runCollect + ) + + const result2 = yield* pipe( + Stream.fail(new ErrorA()), + Stream.flatMap(() => Stream.fail(new ErrorB())), + Stream.catchTag("ErrorB", () => Stream.make(1, 2)), + Stream.runCollect, + Effect.either + ) + + deepStrictEqual(Chunk.toReadonlyArray(result1), [1, 2]) + assertTrue(Either.isLeft(result2) && result2.left._tag === "ErrorA") + })) + + it.effect("onError", () => + Effect.gen(function*() { + const ref = yield* (Ref.make(false)) + const exit = yield* pipe( + Stream.fail("boom"), + Stream.onError(() => Ref.set(ref, true)), + Stream.runDrain, + Effect.exit + ) + const called = yield* (Ref.get(ref)) + deepStrictEqual(exit, Exit.fail("boom")) + assertTrue(called) + })) + + it.effect("orElse", () => + Effect.gen(function*() { + const stream1 = pipe( + Stream.make(1, 2, 3), + Stream.concat(Stream.fail("boom")) + ) + const stream2 = Stream.make(4, 5, 6) + const result = yield* pipe( + stream1, + Stream.orElse(() => stream2), + Stream.runCollect + ) + deepStrictEqual(Array.from(result), [1, 2, 3, 4, 5, 6]) + })) + + it.effect("orElseEither", () => + Effect.gen(function*() { + const stream1 = pipe( + Stream.make(1), + Stream.concat(Stream.fail("boom")) + ) + const stream2 = Stream.make(2) + const result = yield* pipe( + stream1, + Stream.orElseEither(() => stream2), + Stream.runCollect + ) + deepStrictEqual(Array.from(result), [Either.left(1), Either.right(2)]) + })) + + it.effect("orElseFail", () => + Effect.gen(function*() { + const stream = pipe(Stream.succeed(1), Stream.concat(Stream.fail("boom"))) + const result = yield* pipe( + stream, + Stream.orElseFail(() => "boomer"), + Stream.runCollect, + Effect.either + ) + assertLeft(result, "boomer") + })) + + it.effect("orElseIfEmpty - produce default value if stream is empty", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.empty, + Stream.orElseIfEmpty(() => 0), + Stream.runCollect + ) + deepStrictEqual(Array.from(result), [0]) + })) + + it.effect("orElseIfEmpty - ignores default value when stream is not empty", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.make(1), + Stream.orElseIfEmpty(() => 0), + Stream.runCollect + ) + deepStrictEqual(Array.from(result), [1]) + })) + + it.effect("orElseIfEmptyStream - consume default stream if stream is empty", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.empty, + Stream.orElseIfEmptyStream(() => Stream.range(0, 4)), + Stream.runCollect + ) + deepStrictEqual(Array.from(result), [0, 1, 2, 3, 4]) + })) + + it.effect("orElseIfEmptyStream - should throw the correct error from the default stream", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.empty, + Stream.orElseIfEmptyStream(() => Stream.fail("Ouch")), + Stream.runCollect, + Effect.either + ) + assertLeft(result, "Ouch") + })) + + it.effect("orElseSucceed", () => + Effect.gen(function*() { + const stream = pipe(Stream.succeed(1), Stream.concat(Stream.fail("boom"))) + const result = yield* pipe( + stream, + Stream.orElseSucceed(() => 2), + Stream.runCollect + ) + deepStrictEqual(Array.from(result), [1, 2]) + })) +}) diff --git a/repos/effect/packages/effect/test/Stream/filtering.test.ts b/repos/effect/packages/effect/test/Stream/filtering.test.ts new file mode 100644 index 0000000..b8c646f --- /dev/null +++ b/repos/effect/packages/effect/test/Stream/filtering.test.ts @@ -0,0 +1,49 @@ +import { describe, it } from "@effect/vitest" +import { deepStrictEqual } from "@effect/vitest/utils" +import * as Chunk from "effect/Chunk" +import * as Effect from "effect/Effect" +import * as Either from "effect/Either" +import { pipe } from "effect/Function" +import * as Stream from "effect/Stream" + +describe("Stream", () => { + it.effect("filter", () => + Effect.gen(function*() { + const stream = Stream.make(1, 2, 3, 4, 5) + const f = (n: number) => n % 2 === 0 + const { result1, result2 } = yield* (Effect.all({ + result1: pipe(stream, Stream.filter(f), Stream.runCollect), + result2: pipe(stream, Stream.runCollect, Effect.map(Chunk.filter(f))) + })) + deepStrictEqual(Array.from(result1), Array.from(result2)) + })) + + it.effect("filterEffect - simple example", () => + Effect.gen(function*() { + const stream = Stream.make(1, 2, 3, 4, 5) + const f = (n: number) => Effect.succeed(n % 2 === 0) + const { result1, result2 } = yield* (Effect.all({ + result1: pipe(stream, Stream.filterEffect(f), Stream.runCollect), + result2: pipe(stream, Stream.runCollect, Effect.flatMap(Effect.filter(f))) + })) + deepStrictEqual(Array.from(result1), Array.from(result2)) + })) + + it.effect("filterEffect - laziness on chunks", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.make(1, 2, 3), + Stream.filterEffect((n) => + n === 3 ? + Effect.fail("boom") : + Effect.succeed(true) + ), + Stream.either, + Stream.runCollect + ) + deepStrictEqual( + Array.from(result), + [Either.right(1), Either.right(2), Either.left("boom")] + ) + })) +}) diff --git a/repos/effect/packages/effect/test/Stream/finding.test.ts b/repos/effect/packages/effect/test/Stream/finding.test.ts new file mode 100644 index 0000000..ad417ac --- /dev/null +++ b/repos/effect/packages/effect/test/Stream/finding.test.ts @@ -0,0 +1,69 @@ +import { describe, it } from "@effect/vitest" +import { deepStrictEqual } from "@effect/vitest/utils" +import * as Chunk from "effect/Chunk" +import * as Effect from "effect/Effect" +import * as Either from "effect/Either" +import { pipe } from "effect/Function" +import * as Option from "effect/Option" +import * as Stream from "effect/Stream" + +describe("Stream", () => { + it.effect("find", () => + Effect.gen(function*() { + const stream = Stream.make(1, 2, 3, 4, 5) + const f = (n: number) => n === 4 + const { result1, result2 } = yield* (Effect.all({ + result1: pipe(stream, Stream.find(f), Stream.runCollect), + result2: pipe( + stream, + Stream.runCollect, + Effect.map(Chunk.findFirst(f)), + Effect.map(Option.match({ + onNone: () => Chunk.empty(), + onSome: Chunk.of + })) + ) + })) + deepStrictEqual(Array.from(result1), Array.from(result2)) + })) + + it.effect("findEffect - simple example", () => + Effect.gen(function*() { + const stream = Stream.make(1, 2, 3, 4, 5) + const f = (n: number) => Effect.succeed(n === 4) + const { result1, result2 } = yield* (Effect.all({ + result1: pipe(stream, Stream.findEffect(f), Stream.runCollect), + result2: pipe( + Stream.runCollect(stream), + Effect.flatMap((chunk) => + pipe( + Effect.findFirst(chunk, f), + Effect.map(Option.match({ + onNone: () => Chunk.empty(), + onSome: Chunk.of + })) + ) + ) + ) + })) + deepStrictEqual(Array.from(result1), Array.from(result2)) + })) + + it.effect("findEffect - throws correct error", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.make(1, 2, 3), + Stream.findEffect((n) => + n === 3 ? + Effect.fail("boom") : + Effect.succeed(false) + ), + Stream.either, + Stream.runCollect + ) + deepStrictEqual( + Array.from(result), + [Either.left("boom")] + ) + })) +}) diff --git a/repos/effect/packages/effect/test/Stream/foreign.test.ts b/repos/effect/packages/effect/test/Stream/foreign.test.ts new file mode 100644 index 0000000..8841982 --- /dev/null +++ b/repos/effect/packages/effect/test/Stream/foreign.test.ts @@ -0,0 +1,113 @@ +import { describe, it } from "@effect/vitest" +import { assertLeft, assertRight, deepStrictEqual } from "@effect/vitest/utils" +import * as Cause from "effect/Cause" +import * as Chunk from "effect/Chunk" +import * as Context from "effect/Context" +import * as Effect from "effect/Effect" +import * as Either from "effect/Either" +import * as Exit from "effect/Exit" +import { pipe } from "effect/Function" +import * as Option from "effect/Option" +import * as Random from "effect/Random" +import * as Stream from "effect/Stream" +import { unify } from "effect/Unify" + +describe("Stream.Foreign", () => { + it.effect("Tag", () => + Effect.gen(function*() { + const tag = Context.GenericTag("number") + const result = yield* pipe( + tag, + Stream.runCollect, + Effect.map(Chunk.toReadonlyArray), + Effect.provideService(tag, 10) + ) + deepStrictEqual(result, [10]) + })) + + it.effect("Unify", () => + Effect.gen(function*() { + const unifiedEffect = unify((yield* (Random.nextInt)) > 1 ? Effect.succeed(0) : Effect.fail(1)) + const unifiedExit = unify((yield* (Random.nextInt)) > 1 ? Exit.succeed(0) : Exit.fail(1)) + const unifiedEither = unify((yield* (Random.nextInt)) > 1 ? Either.right(0) : Either.left(1)) + const unifiedOption = unify((yield* (Random.nextInt)) > 1 ? Option.some(0) : Option.none()) + deepStrictEqual(Chunk.toReadonlyArray(yield* (Stream.runCollect(unifiedEffect))), [0]) + deepStrictEqual(Chunk.toReadonlyArray(yield* (Stream.runCollect(unifiedExit))), [0]) + deepStrictEqual(Chunk.toReadonlyArray(yield* (Stream.runCollect(unifiedEither))), [0]) + deepStrictEqual(Chunk.toReadonlyArray(yield* (Stream.runCollect(unifiedOption))), [0]) + })) + + it.effect("Either.right", () => + Effect.gen(function*() { + const tag = Context.GenericTag("number") + + const result = yield* pipe( + Either.right(10), + Stream.runCollect, + Effect.map(Chunk.toReadonlyArray), + Effect.provideService(tag, 10) + ) + deepStrictEqual(result, [10]) + })) + + it.effect("Either.left", () => + Effect.gen(function*() { + const tag = Context.GenericTag("number") + const result = yield* pipe( + Either.left(10), + Stream.runCollect, + Effect.either, + Effect.provideService(tag, 10) + ) + assertLeft(result, 10) + })) + + it.effect("Option.some", () => + Effect.gen(function*() { + const tag = Context.GenericTag("number") + const result = yield* pipe( + Option.some(10), + Stream.runCollect, + Effect.map(Chunk.toReadonlyArray), + Effect.provideService(tag, 10) + ) + deepStrictEqual(result, [10]) + })) + + it.effect("Option.none", () => + Effect.gen(function*() { + const tag = Context.GenericTag("number") + const result = yield* pipe( + Option.none(), + Stream.runCollect, + Effect.either, + Effect.provideService(tag, 10) + ) + assertLeft(result, new Cause.NoSuchElementException()) + })) + + it.effect("Effect.fail", () => + Effect.gen(function*() { + const tag = Context.GenericTag("number") + const result = yield* pipe( + Effect.fail("ok"), + Stream.runCollect, + Effect.either, + Effect.provideService(tag, 10) + ) + assertLeft(result, "ok") + })) + + it.effect("Effect.succeed", () => + Effect.gen(function*() { + const tag = Context.GenericTag("number") + const result = yield* pipe( + Effect.succeed("ok"), + Stream.runCollect, + Effect.map(Chunk.toReadonlyArray), + Effect.either, + Effect.provideService(tag, 10) + ) + assertRight(result, ["ok"]) + })) +}) diff --git a/repos/effect/packages/effect/test/Stream/fromEventListener.test.ts b/repos/effect/packages/effect/test/Stream/fromEventListener.test.ts new file mode 100644 index 0000000..a283073 --- /dev/null +++ b/repos/effect/packages/effect/test/Stream/fromEventListener.test.ts @@ -0,0 +1,22 @@ +import { describe, it } from "@effect/vitest" +import { Effect, pipe, Stream } from "effect" + +class TestTarget extends EventTarget { + emit() { + this.dispatchEvent(new Event("test-event")) + } +} + +describe("Stream.fromEventListener", () => { + it.effect("emitted count", (ctx) => + Effect.gen(function*() { + const target = new TestTarget() + + const count = yield* pipe( + Stream.fromEventListener(target, "test-event"), + Stream.interruptWhen(Effect.sync(() => target.emit()).pipe(Effect.repeatN(2))), + Stream.runCount + ) + ctx.expect(count).toEqual(3) + })) +}) diff --git a/repos/effect/packages/effect/test/Stream/getters.test.ts b/repos/effect/packages/effect/test/Stream/getters.test.ts new file mode 100644 index 0000000..bea5dee --- /dev/null +++ b/repos/effect/packages/effect/test/Stream/getters.test.ts @@ -0,0 +1,43 @@ +import { describe, it } from "@effect/vitest" +import { assertLeft, deepStrictEqual } from "@effect/vitest/utils" +import * as Effect from "effect/Effect" +import { pipe } from "effect/Function" +import * as Option from "effect/Option" +import * as Stream from "effect/Stream" + +describe("Stream", () => { + it.effect("some", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.succeed(Option.some(1)), + Stream.concat(Stream.succeed(Option.none())), + Stream.some, + Stream.runCollect, + Effect.either + ) + assertLeft(result, Option.none()) + })) + + it.effect("some", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.succeed(Option.some(1)), + Stream.concat(Stream.succeed(Option.none())), + Stream.someOrElse(() => -1), + Stream.runCollect + ) + deepStrictEqual(Array.from(result), [1, -1]) + })) + + it.effect("someOrFail", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.succeed(Option.some(1)), + Stream.concat(Stream.succeed(Option.none())), + Stream.someOrFail(() => -1), + Stream.runCollect, + Effect.either + ) + assertLeft(result, -1) + })) +}) diff --git a/repos/effect/packages/effect/test/Stream/grouping.test.ts b/repos/effect/packages/effect/test/Stream/grouping.test.ts new file mode 100644 index 0000000..68934da --- /dev/null +++ b/repos/effect/packages/effect/test/Stream/grouping.test.ts @@ -0,0 +1,425 @@ +import { describe, it } from "@effect/vitest" +import { assertLeft, deepStrictEqual, strictEqual } from "@effect/vitest/utils" +import * as Chunk from "effect/Chunk" +import * as Duration from "effect/Duration" +import * as Effect from "effect/Effect" +import * as Exit from "effect/Exit" +import * as Fiber from "effect/Fiber" +import { identity, pipe } from "effect/Function" +import * as GroupBy from "effect/GroupBy" +import * as Option from "effect/Option" +import * as Ref from "effect/Ref" +import * as Sink from "effect/Sink" +import * as Stream from "effect/Stream" +import * as TestClock from "effect/TestClock" +import * as Handoff from "../../src/internal/stream/handoff.js" +import { chunkCoordination } from "../utils/coordination.js" + +describe("Stream", () => { + it.effect("groupBy - values", () => + Effect.gen(function*() { + const words = pipe( + Chunk.makeBy(() => Chunk.range(0, 99))(100), + Chunk.flatten, + Chunk.map((n) => String(n)) + ) + const result = yield* pipe( + Stream.fromIterable(words), + Stream.groupByKey(identity, { bufferSize: 8192 }), + GroupBy.evaluate((key, stream) => + pipe( + Stream.runCollect(stream), + Effect.map((leftover) => [key, leftover.length] as const), + Stream.fromEffect + ) + ), + Stream.runCollect + ) + deepStrictEqual( + Array.from(result), + Array.from({ length: 100 }, (_, i) => i).map((n) => [String(n), 100] as const) + ) + })) + + it.effect("groupBy - first", () => + Effect.gen(function*() { + const words = pipe( + Chunk.makeBy(() => Chunk.range(0, 99))(1_000), + Chunk.flatten, + Chunk.map((n) => String(n)) + ) + const result = yield* pipe( + Stream.fromIterable(words), + Stream.groupByKey(identity, { bufferSize: 1050 }), + GroupBy.first(2), + GroupBy.evaluate((key, stream) => + pipe( + Stream.runCollect(stream), + Effect.map((leftover) => [key, leftover.length] as const), + Stream.fromEffect + ) + ), + Stream.runCollect + ) + deepStrictEqual(Array.from(result), [["0", 1_000], ["1", 1_000]]) + })) + + it.effect("groupBy - filter", () => + Effect.gen(function*() { + const words = Array.from({ length: 100 }, () => Array.from({ length: 100 }, (_, i) => i)).flat() + const result = yield* pipe( + Stream.fromIterable(words), + Stream.groupByKey(identity, { bufferSize: 1050 }), + GroupBy.filter((n) => n <= 5), + GroupBy.evaluate((key, stream) => + pipe( + Stream.runCollect(stream), + Effect.map((leftover) => [key, leftover.length] as const), + Stream.fromEffect + ) + ), + Stream.runCollect + ) + deepStrictEqual(Array.from(result), [ + [0, 100], + [1, 100], + [2, 100], + [3, 100], + [4, 100], + [5, 100] + ]) + })) + + it.effect("groupBy - outer errors", () => + Effect.gen(function*() { + const words = ["abc", "test", "test", "foo"] + const result = yield* pipe( + Stream.fromIterable(words), + Stream.concat(Stream.fail("boom")), + Stream.groupByKey(identity), + GroupBy.evaluate((_, stream) => Stream.drain(stream)), + Stream.runCollect, + Effect.either + ) + assertLeft(result, "boom") + })) + + it.effect("grouped - sanity check", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.make(1, 2, 3, 4, 5), + Stream.grouped(2), + Stream.runCollect + ) + deepStrictEqual( + Array.from(result).map((chunk) => Array.from(chunk)), + [[1, 2], [3, 4], [5]] + ) + })) + + it.effect("grouped - group size is correct", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.range(0, 99), + Stream.grouped(10), + Stream.map(Chunk.size), + Stream.runCollect + ) + deepStrictEqual( + Array.from(result), + Array.from({ length: 10 }, () => 10) + ) + })) + + it.effect("grouped - does not emit empty chunks", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.fromIterable(Chunk.empty()), + Stream.grouped(5), + Stream.runCollect + ) + deepStrictEqual(Array.from(result), []) + })) + + it.effect("grouped - emits elements properly when a failure occurs", () => + Effect.gen(function*() { + const ref = yield* (Ref.make(Chunk.empty>())) + const streamChunks = Stream.fromChunks(Chunk.range(1, 4), Chunk.range(5, 7), Chunk.of(8)) + const stream = pipe( + streamChunks, + Stream.concat(Stream.fail("Ouch")), + Stream.grouped(3) + ) + const either = yield* pipe( + stream, + Stream.mapEffect((chunk) => Ref.update(ref, Chunk.append(Array.from(chunk)))), + Stream.runCollect, + Effect.either + ) + const result = yield* (Ref.get(ref)) + assertLeft(either, "Ouch") + deepStrictEqual(Array.from(result), [[1, 2, 3], [4, 5, 6], [7, 8]]) + })) + + it.effect("groupedWithin - group based on time passed", () => + Effect.gen(function*() { + const coordination = yield* (chunkCoordination([ + Chunk.make(1, 2), + Chunk.make(3, 4), + Chunk.of(5) + ])) + const stream = pipe( + Stream.fromQueue(coordination.queue), + Stream.filterMapWhile(Exit.match({ + onSuccess: Option.some, + onFailure: Option.none + })), + Stream.flattenChunks, + Stream.groupedWithin(10, Duration.seconds(2)), + Stream.tap(() => coordination.proceed) + ) + const fiber = yield* (Effect.fork(Stream.runCollect(stream))) + yield* pipe( + coordination.offer, + Effect.zipRight(TestClock.adjust(Duration.seconds(2))), + Effect.zipRight(coordination.awaitNext) + ) + yield* pipe( + coordination.offer, + Effect.zipRight(TestClock.adjust(Duration.seconds(2))), + Effect.zipRight(coordination.awaitNext) + ) + yield* (coordination.offer) + const result = yield* (Fiber.join(fiber)) + deepStrictEqual( + Array.from(result).map((chunk) => Array.from(chunk)), + [[1, 2], [3, 4], [5]] + ) + })) + + it.effect("groupedWithin - group based on time passed (ZIO Issue #5013)", () => + Effect.gen(function*() { + const coordination = yield* pipe( + Chunk.range(1, 29), + Chunk.map(Chunk.of), + chunkCoordination + ) + const latch = yield* (Handoff.make()) + const ref = yield* (Ref.make(0)) + const fiber = yield* pipe( + Stream.fromQueue(coordination.queue), + Stream.filterMapWhile(Exit.match({ + onSuccess: Option.some, + onFailure: Option.none + })), + Stream.flattenChunks, + Stream.tap(() => coordination.proceed), + Stream.groupedWithin(10, Duration.seconds(3)), + Stream.tap((chunk) => + pipe( + Ref.update(ref, (n) => n + chunk.length), + Effect.zipRight(pipe(latch, Handoff.offer(void 0))) + ) + ), + Stream.run(Sink.take(5)), + Effect.fork + ) + yield* pipe( + coordination.offer, + Effect.zipRight(TestClock.adjust(Duration.seconds(1))), + Effect.zipRight(coordination.awaitNext) + ) + yield* pipe( + coordination.offer, + Effect.zipRight(TestClock.adjust(Duration.seconds(1))), + Effect.zipRight(coordination.awaitNext) + ) + yield* pipe( + coordination.offer, + Effect.zipRight(TestClock.adjust(Duration.seconds(1))), + Effect.zipRight(coordination.awaitNext) + ) + const result1 = yield* pipe( + Handoff.take(latch), + Effect.zipRight(Ref.get(ref)) + ) + yield* pipe( + coordination.offer, + Effect.zipRight(TestClock.adjust(Duration.seconds(1))), + Effect.zipRight(coordination.awaitNext) + ) + yield* pipe( + coordination.offer, + Effect.zipRight(TestClock.adjust(Duration.seconds(1))), + Effect.zipRight(coordination.awaitNext) + ) + yield* pipe( + coordination.offer, + Effect.zipRight(TestClock.adjust(Duration.seconds(1))), + Effect.zipRight(coordination.awaitNext) + ) + const result2 = yield* pipe( + Handoff.take(latch), + Effect.zipRight(Ref.get(ref)) + ) + yield* pipe( + coordination.offer, + Effect.zipRight(TestClock.adjust(Duration.seconds(1))), + Effect.zipRight(coordination.awaitNext) + ) + yield* pipe( + coordination.offer, + Effect.zipRight(TestClock.adjust(Duration.seconds(1))), + Effect.zipRight(coordination.awaitNext) + ) + yield* pipe( + coordination.offer, + Effect.zipRight(TestClock.adjust(Duration.seconds(1))), + Effect.zipRight(coordination.awaitNext) + ) + const result3 = yield* pipe( + Handoff.take(latch), + Effect.zipRight(Ref.get(ref)) + ) + // This part is to make sure schedule clock is being restarted when the + // specified amount of elements has been reached. + yield* pipe( + TestClock.adjust(Duration.seconds(2)), + Effect.zipRight( + pipe( + coordination.offer, + Effect.zipRight(coordination.awaitNext), + Effect.repeatN(9) + ) + ) + ) + const result4 = yield* pipe( + Handoff.take(latch), + Effect.zipRight(Ref.get(ref)) + ) + yield* pipe( + coordination.offer, + Effect.zipRight(coordination.awaitNext), + Effect.zipRight(TestClock.adjust(Duration.seconds(2))), + Effect.zipRight( + pipe( + coordination.offer, + Effect.zipRight(coordination.awaitNext), + Effect.repeatN(8) + ) + ) + ) + const result5 = yield* pipe( + Handoff.take(latch), + Effect.zipRight(Ref.get(ref)) + ) + const result = yield* (Fiber.join(fiber)) + deepStrictEqual( + Array.from(result).map((chunk) => Array.from(chunk)), + [ + [1, 2, 3], + [4, 5, 6], + [7, 8, 9], + [10, 11, 12, 13, 14, 15, 16, 17, 18, 19], + [20, 21, 22, 23, 24, 25, 26, 27, 28, 29] + ] + ) + strictEqual(result1, 3) + strictEqual(result2, 6) + strictEqual(result3, 9) + strictEqual(result4, 19) + strictEqual(result5, 29) + })) + + it.effect("groupedWithin - group immediately when chunk size is reached", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.make(1, 2, 3, 4), + Stream.groupedWithin(2, Duration.seconds(10)), + Stream.runCollect + ) + deepStrictEqual( + Array.from(result).map((chunk) => Array.from(chunk)), + [[1, 2], [3, 4]] + ) + })) + + it.effect("groupAdjacentBy - one big chunk", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.fromIterable([ + { code: 1, message: "A" }, + { code: 1, message: "B" }, + { code: 1, message: "D" }, + { code: 2, message: "C" } + ]), + Stream.groupAdjacentBy((x) => x.code), + Stream.runCollect + ) + deepStrictEqual( + Array.from(result).map(([, chunk]) => Array.from(chunk)), + [ + [ + { code: 1, message: "A" }, + { code: 1, message: "B" }, + { code: 1, message: "D" } + ], + [ + { code: 2, message: "C" } + ] + ] + ) + })) + + it.effect("groupAdjacentBy - several single element chunks", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.fromChunks( + Chunk.make({ code: 1, message: "A" }), + Chunk.make({ code: 1, message: "B" }), + Chunk.make({ code: 1, message: "D" }), + Chunk.make({ code: 2, message: "C" }) + ), + Stream.groupAdjacentBy((x) => x.code), + Stream.runCollect + ) + deepStrictEqual( + Array.from(result).map(([, chunk]) => Array.from(chunk)), + [ + [ + { code: 1, message: "A" }, + { code: 1, message: "B" }, + { code: 1, message: "D" } + ], + [ + { code: 2, message: "C" } + ] + ] + ) + })) + + it.effect("groupAdjacentBy - group across chunks", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.fromChunks( + Chunk.make({ code: 1, message: "A" }, { code: 1, message: "B" }), + Chunk.make({ code: 1, message: "D" }, { code: 2, message: "C" }) + ), + Stream.groupAdjacentBy((x) => x.code), + Stream.runCollect + ) + deepStrictEqual( + Array.from(result).map(([, chunk]) => Array.from(chunk)), + [ + [ + { code: 1, message: "A" }, + { code: 1, message: "B" }, + { code: 1, message: "D" } + ], + [ + { code: 2, message: "C" } + ] + ] + ) + })) +}) diff --git a/repos/effect/packages/effect/test/Stream/halting.test.ts b/repos/effect/packages/effect/test/Stream/halting.test.ts new file mode 100644 index 0000000..3c73c39 --- /dev/null +++ b/repos/effect/packages/effect/test/Stream/halting.test.ts @@ -0,0 +1,139 @@ +import { describe, it } from "@effect/vitest" +import { assertFalse, assertLeft, deepStrictEqual } from "@effect/vitest/utils" +import * as Chunk from "effect/Chunk" +import * as Deferred from "effect/Deferred" +import * as Duration from "effect/Duration" +import * as Effect from "effect/Effect" +import * as Exit from "effect/Exit" +import * as Fiber from "effect/Fiber" +import { pipe } from "effect/Function" +import * as Option from "effect/Option" +import * as Queue from "effect/Queue" +import * as Ref from "effect/Ref" +import * as Stream from "effect/Stream" +import * as TestClock from "effect/TestClock" +import { chunkCoordination } from "../utils/coordination.js" + +describe("Stream", () => { + it.effect("haltWhen - halts after the current element", () => + Effect.gen(function*() { + const ref = yield* (Ref.make(false)) + const latch = yield* (Deferred.make()) + const halt = yield* (Deferred.make()) + yield* pipe( + Deferred.await(latch), + Effect.onInterrupt(() => Ref.set(ref, true)), + Stream.fromEffect, + Stream.haltWhen(Deferred.await(halt)), + Stream.runDrain, + Effect.fork + ) + yield* (Deferred.succeed(halt, void 0)) + yield* (Deferred.succeed(latch, void 0)) + const result = yield* (Ref.get(ref)) + assertFalse(result) + })) + + it.effect("haltWhen - propagates errors", () => + Effect.gen(function*() { + const halt = yield* (Deferred.make()) + yield* (Deferred.fail(halt, "fail")) + const result = yield* pipe( + Stream.make(0), + Stream.forever, + Stream.haltWhen(Deferred.await(halt)), + Stream.runDrain, + Effect.either + ) + assertLeft(result, "fail") + })) + + it.effect("haltWhenDeferred - halts after the current element", () => + Effect.gen(function*() { + const ref = yield* (Ref.make(false)) + const latch = yield* (Deferred.make()) + const halt = yield* (Deferred.make()) + yield* pipe( + Deferred.await(latch), + Effect.onInterrupt(() => Ref.set(ref, true)), + Stream.fromEffect, + Stream.haltWhenDeferred(halt), + Stream.runDrain, + Effect.fork + ) + yield* (Deferred.succeed(halt, void 0)) + yield* (Deferred.succeed(latch, void 0)) + const result = yield* (Ref.get(ref)) + assertFalse(result) + })) + + it.effect("haltWhenDeferred - propagates errors", () => + Effect.gen(function*() { + const halt = yield* (Deferred.make()) + yield* (Deferred.fail(halt, "fail")) + const result = yield* pipe( + Stream.make(1), + Stream.haltWhenDeferred(halt), + Stream.runDrain, + Effect.either + ) + assertLeft(result, "fail") + })) + + it.effect("haltAfter - halts after the given duration", () => + Effect.gen(function*() { + const coordination = yield* (chunkCoordination([ + Chunk.of(1), + Chunk.of(2), + Chunk.of(3), + Chunk.of(4) + ])) + const fiber = yield* pipe( + Stream.fromQueue(coordination.queue), + Stream.filterMapWhile(Exit.match({ + onFailure: Option.none, + onSuccess: Option.some + })), + Stream.haltAfter(Duration.seconds(5)), + Stream.tap(() => coordination.proceed), + Stream.runCollect, + Effect.fork + ) + yield* pipe( + coordination.offer, + Effect.zipRight(TestClock.adjust(Duration.seconds(3))), + Effect.zipRight(coordination.awaitNext) + ) + yield* pipe( + coordination.offer, + Effect.zipRight(TestClock.adjust(Duration.seconds(3))), + Effect.zipRight(coordination.awaitNext) + ) + yield* pipe( + coordination.offer, + Effect.zipRight(TestClock.adjust(Duration.seconds(3))), + Effect.zipRight(coordination.awaitNext) + ) + yield* (coordination.offer) + const result = yield* (Fiber.join(fiber)) + deepStrictEqual( + Array.from(result).map((chunk) => Array.from(chunk)), + [[1], [2], [3]] + ) + })) + + it.effect("haltAfter - will process first chunk", () => + Effect.gen(function*() { + const queue = yield* (Queue.unbounded()) + const fiber = yield* pipe( + Stream.fromQueue(queue), + Stream.haltAfter(Duration.seconds(5)), + Stream.runCollect, + Effect.fork + ) + yield* (TestClock.adjust(Duration.seconds(6))) + yield* (Queue.offer(queue, 1)) + const result = yield* (Fiber.join(fiber)) + deepStrictEqual(Array.from(result), [1]) + })) +}) diff --git a/repos/effect/packages/effect/test/Stream/interleaving.test.ts b/repos/effect/packages/effect/test/Stream/interleaving.test.ts new file mode 100644 index 0000000..483d384 --- /dev/null +++ b/repos/effect/packages/effect/test/Stream/interleaving.test.ts @@ -0,0 +1,75 @@ +import { describe, it } from "@effect/vitest" +import { deepStrictEqual } from "@effect/vitest/utils" +import * as Chunk from "effect/Chunk" +import * as Effect from "effect/Effect" +import { pipe } from "effect/Function" +import * as Option from "effect/Option" +import * as Stream from "effect/Stream" + +describe("Stream", () => { + it.effect("interleave", () => + Effect.gen(function*() { + const stream1 = Stream.make(2, 3) + const stream2 = Stream.make(5, 6, 7) + const result = yield* pipe( + stream1, + Stream.interleave(stream2), + Stream.runCollect + ) + deepStrictEqual(Array.from(result), [2, 5, 3, 6, 7]) + })) + + it.effect("interleaveWith", () => + Effect.gen(function*() { + const interleave = ( + bools: Chunk.Chunk, + numbers1: Chunk.Chunk, + numbers2: Chunk.Chunk + ): Chunk.Chunk => + pipe( + Chunk.head(bools), + Option.map((head) => { + if (head) { + if (Chunk.isNonEmpty(numbers1)) { + const head = pipe(numbers1, Chunk.unsafeGet(0)) + const tail = pipe(numbers1, Chunk.drop(1)) + return pipe( + interleave(pipe(bools, Chunk.drop(1)), tail, numbers2), + Chunk.prepend(head) + ) + } + if (Chunk.isNonEmpty(numbers2)) { + return interleave(pipe(bools, Chunk.drop(1)), Chunk.empty(), numbers2) + } + return Chunk.empty() + } + if (Chunk.isNonEmpty(numbers2)) { + const head = pipe(numbers2, Chunk.unsafeGet(0)) + const tail = pipe(numbers2, Chunk.drop(1)) + return pipe( + interleave(pipe(bools, Chunk.drop(1)), numbers1, tail), + Chunk.prepend(head) + ) + } + if (Chunk.isNonEmpty(numbers1)) { + return interleave(pipe(bools, Chunk.drop(1)), numbers1, Chunk.empty()) + } + return Chunk.empty() + }), + Option.getOrElse(() => Chunk.empty()) + ) + const boolStream = Stream.make(true, true, false, true, false) + const stream1 = Stream.make(1, 2, 3, 4, 5) + const stream2 = Stream.make(4, 5, 6, 7, 8) + const interleavedStream = yield* pipe( + stream1, + Stream.interleaveWith(stream2, boolStream), + Stream.runCollect + ) + const bools = yield* (Stream.runCollect(boolStream)) + const numbers1 = yield* (Stream.runCollect(stream1)) + const numbers2 = yield* (Stream.runCollect(stream2)) + const interleavedChunks = interleave(bools, numbers1, numbers2) + deepStrictEqual(Array.from(interleavedStream), Array.from(interleavedChunks)) + })) +}) diff --git a/repos/effect/packages/effect/test/Stream/interrupting.test.ts b/repos/effect/packages/effect/test/Stream/interrupting.test.ts new file mode 100644 index 0000000..7ad6a01 --- /dev/null +++ b/repos/effect/packages/effect/test/Stream/interrupting.test.ts @@ -0,0 +1,213 @@ +import { describe, it } from "@effect/vitest" +import { assertLeft, assertTrue, deepStrictEqual, strictEqual } from "@effect/vitest/utils" +import { Schedule } from "effect" +import * as Chunk from "effect/Chunk" +import * as Deferred from "effect/Deferred" +import * as Duration from "effect/Duration" +import * as Effect from "effect/Effect" +import * as Exit from "effect/Exit" +import * as Fiber from "effect/Fiber" +import { pipe } from "effect/Function" +import * as Option from "effect/Option" +import * as Queue from "effect/Queue" +import * as Ref from "effect/Ref" +import * as Stream from "effect/Stream" +import * as TestClock from "effect/TestClock" +import { chunkCoordination } from "../utils/coordination.js" + +describe("Stream", () => { + it.effect("interruptWhen - preserves the scope of inner fibers", () => + Effect.gen(function*() { + const deferred = yield* (Deferred.make()) + const queue1 = yield* (Queue.unbounded>()) + const queue2 = yield* (Queue.unbounded>()) + yield* (Queue.offer(queue1, Chunk.of(1))) + yield* (Queue.offer(queue2, Chunk.of(2))) + yield* pipe(Queue.offer(queue1, Chunk.of(3)), Effect.fork) + yield* pipe(Queue.offer(queue2, Chunk.of(4)), Effect.fork) + const stream1 = Stream.fromChunkQueue(queue1) + const stream2 = Stream.fromChunkQueue(queue2) + const stream = pipe( + stream1, + Stream.zipLatest(stream2), + Stream.interruptWhen(Deferred.await(deferred)), + Stream.take(3) + ) + const result = yield* (Stream.runDrain(stream)) + strictEqual(result, undefined) + })) + + it.effect("interruptWhen - interrupts the current element", () => + Effect.gen(function*() { + const ref = yield* (Ref.make(false)) + const latch = yield* (Deferred.make()) + const halt = yield* (Deferred.make()) + const started = yield* (Deferred.make()) + const fiber = yield* pipe( + Stream.fromEffect(pipe( + Deferred.succeed(started, void 0), + Effect.zipRight(Deferred.await(latch)), + Effect.onInterrupt(() => Ref.set(ref, true)) + )), + Stream.interruptWhen(Deferred.await(halt)), + Stream.runDrain, + Effect.fork + ) + yield* pipe( + Deferred.await(started), + Effect.zipRight(Deferred.succeed(halt, void 0)) + ) + yield* (Fiber.await(fiber)) + const result = yield* (Ref.get(ref)) + assertTrue(result) + })) + + it.effect("interruptWhen - propagates errors", () => + Effect.gen(function*() { + const halt = yield* (Deferred.make()) + yield* (Deferred.fail(halt, "fail")) + const result = yield* pipe( + Stream.never, + Stream.interruptWhen(Deferred.await(halt)), + Stream.runDrain, + Effect.either + ) + assertLeft(result, "fail") + })) + + it.effect("interruptWhenDeferred - interrupts the current element", () => + Effect.gen(function*() { + const ref = yield* (Ref.make(false)) + const latch = yield* (Deferred.make()) + const halt = yield* (Deferred.make()) + const started = yield* (Deferred.make()) + const fiber = yield* pipe( + Stream.fromEffect(pipe( + Deferred.succeed(started, void 0), + Effect.zipRight(Deferred.await(latch)), + Effect.onInterrupt(() => Ref.set(ref, true)) + )), + Stream.interruptWhenDeferred(halt), + Stream.runDrain, + Effect.fork + ) + yield* pipe( + Deferred.await(started), + Effect.zipRight(Deferred.succeed(halt, void 0)) + ) + yield* (Fiber.await(fiber)) + const result = yield* (Ref.get(ref)) + assertTrue(result) + })) + + it.effect("interruptWhenDeferred - propagates errors", () => + Effect.gen(function*() { + const halt = yield* (Deferred.make()) + yield* (Deferred.fail(halt, "fail")) + const result = yield* pipe( + Stream.never, + Stream.interruptWhenDeferred(halt), + Stream.runDrain, + Effect.either + ) + assertLeft(result, "fail") + })) + + it.effect("interruptAfter - halts after the given duration", () => + Effect.gen(function*() { + const coordination = yield* (chunkCoordination([ + Chunk.of(1), + Chunk.of(2), + Chunk.of(3), + Chunk.of(4) + ])) + const fiber = yield* pipe( + Stream.fromQueue(coordination.queue), + Stream.filterMapWhile(Exit.match({ + onFailure: Option.none, + onSuccess: Option.some + })), + Stream.interruptAfter(Duration.seconds(5)), + Stream.tap(() => coordination.proceed), + Stream.runCollect, + Effect.fork + ) + yield* pipe( + coordination.offer, + Effect.zipRight(TestClock.adjust(Duration.seconds(3))), + Effect.zipRight(coordination.awaitNext) + ) + yield* pipe( + coordination.offer, + Effect.zipRight(TestClock.adjust(Duration.seconds(3))), + Effect.zipRight(coordination.awaitNext) + ) + yield* (coordination.offer) + const result = yield* (Fiber.join(fiber)) + deepStrictEqual( + Array.from(result).map((chunk) => Array.from(chunk)), + [[1], [2]] + ) + })) + + it.effect("interruptAfter - will process first chunk", () => + Effect.gen(function*() { + const queue = yield* (Queue.unbounded()) + const fiber = yield* pipe( + Stream.fromQueue(queue), + Stream.interruptAfter(Duration.seconds(5)), + Stream.runCollect, + Effect.fork + ) + yield* (TestClock.adjust(Duration.seconds(6))) + yield* (Queue.offer(queue, 1)) + const result = yield* (Fiber.join(fiber)) + deepStrictEqual(Array.from(result), []) + })) + + it.effect("interruptWhen - interrupts the effect", () => + Effect.gen(function*() { + let interrupted = false + const effect = Effect.never.pipe( + Effect.onInterrupt(() => + Effect.sync(() => { + interrupted = true + }) + ) + ) + + const fiber = yield* Stream.fromSchedule(Schedule.spaced("1 second")).pipe( + Stream.interruptWhen(effect), + Stream.take(1), + Stream.runDrain, + Effect.fork + ) + yield* TestClock.adjust("1 seconds") + yield* fiber.await + + assertTrue(interrupted) + })) + + it.effect("forked children are not interrupted early by interruptWhen", () => + Effect.gen(function*() { + const queue = yield* Queue.unbounded() + const ref = yield* Ref.make(0) + yield* Stream.fromQueue(queue).pipe( + Stream.runForEach(() => Ref.update(ref, (n) => n + 1)), + Effect.fork, + Effect.as(Stream.concat(Stream.succeed(""), Stream.never)), + Stream.unwrapScoped, + Stream.interruptWhen(Effect.never), + Stream.runDrain, + Effect.fork + ) + yield* Queue.offer(queue, "message").pipe( + Effect.forever, + Effect.fork + ) + const result = yield* Ref.get(ref).pipe( + Effect.repeat({ until: (n) => n >= 10 }) + ) + strictEqual(result, 10) + })) +}) diff --git a/repos/effect/packages/effect/test/Stream/interspersing.test.ts b/repos/effect/packages/effect/test/Stream/interspersing.test.ts new file mode 100644 index 0000000..547d4bf --- /dev/null +++ b/repos/effect/packages/effect/test/Stream/interspersing.test.ts @@ -0,0 +1,76 @@ +import { describe, it } from "@effect/vitest" +import { deepStrictEqual } from "@effect/vitest/utils" +import * as Chunk from "effect/Chunk" +import * as Effect from "effect/Effect" +import { pipe } from "effect/Function" +import * as Stream from "effect/Stream" + +describe("Stream", () => { + it.effect("intersperse - several values", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.make(1, 2, 3, 4), + Stream.map(String), + Stream.intersperse("."), + Stream.runCollect + ) + deepStrictEqual(Array.from(result), ["1", ".", "2", ".", "3", ".", "4"]) + })) + + it.effect("intersperseAffixes - several values", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.make(1, 2, 3, 4), + Stream.map(String), + Stream.intersperseAffixes({ start: "[", middle: ".", end: "]" }), + Stream.runCollect + ) + deepStrictEqual(Array.from(result), ["[", "1", ".", "2", ".", "3", ".", "4", "]"]) + })) + + it.effect("intersperse - single value", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.make(1), + Stream.map(String), + Stream.intersperse("."), + Stream.runCollect + ) + deepStrictEqual(Array.from(result), ["1"]) + })) + + it.effect("intersperseAffixes - single value", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.make(1), + Stream.map(String), + Stream.intersperseAffixes({ start: "[", middle: ".", end: "]" }), + Stream.runCollect + ) + deepStrictEqual(Array.from(result), ["[", "1", "]"]) + })) + + it.effect("intersperse - several from repeat effect (ZIO #3729)", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.repeatEffect(Effect.succeed(42)), + Stream.map(String), + Stream.take(4), + Stream.intersperse("."), + Stream.runCollect + ) + deepStrictEqual(Array.from(result), ["42", ".", "42", ".", "42", ".", "42"]) + })) + + it.effect("intersperse - several from repeat effect chunk single element (ZIO #3729)", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.repeatEffectChunk(Effect.succeed(Chunk.of(42))), + Stream.map(String), + Stream.intersperse("."), + Stream.take(4), + Stream.runCollect + ) + deepStrictEqual(Array.from(result), ["42", ".", "42", "."]) + })) +}) diff --git a/repos/effect/packages/effect/test/Stream/lifecycle.test.ts b/repos/effect/packages/effect/test/Stream/lifecycle.test.ts new file mode 100644 index 0000000..3bd36c9 --- /dev/null +++ b/repos/effect/packages/effect/test/Stream/lifecycle.test.ts @@ -0,0 +1,31 @@ +import { describe, it } from "@effect/vitest" +import { deepStrictEqual, strictEqual } from "@effect/vitest/utils" +import * as Effect from "effect/Effect" +import { pipe } from "effect/Function" +import * as Stream from "effect/Stream" + +describe("Stream", () => { + it.effect("onStart", () => + Effect.gen(function*() { + let counter = 0 + const result = yield* pipe( + Stream.make(1, 1), + Stream.onStart(Effect.sync(() => counter++)), + Stream.runCollect + ) + strictEqual(counter, 1) + deepStrictEqual(Array.from(result), [1, 1]) + })) + + it.effect("onEnd", () => + Effect.gen(function*() { + let counter = 0 + const result = yield* pipe( + Stream.make(1, 2, 3), + Stream.onEnd(Effect.sync(() => counter++)), + Stream.runCollect + ) + strictEqual(counter, 1) + deepStrictEqual(Array.from(result), [1, 2, 3]) + })) +}) diff --git a/repos/effect/packages/effect/test/Stream/mapping.test.ts b/repos/effect/packages/effect/test/Stream/mapping.test.ts new file mode 100644 index 0000000..b98904f --- /dev/null +++ b/repos/effect/packages/effect/test/Stream/mapping.test.ts @@ -0,0 +1,352 @@ +import { describe, it } from "@effect/vitest" +import { assertFalse, assertLeft, assertTrue, deepStrictEqual, strictEqual } from "@effect/vitest/utils" +import * as Cause from "effect/Cause" +import * as Chunk from "effect/Chunk" +import * as Deferred from "effect/Deferred" +import * as Duration from "effect/Duration" +import * as Effect from "effect/Effect" +import * as Either from "effect/Either" +import * as Exit from "effect/Exit" +import * as Fiber from "effect/Fiber" +import { identity, pipe } from "effect/Function" +import * as Queue from "effect/Queue" +import * as Ref from "effect/Ref" +import * as Stream from "effect/Stream" +import * as TestClock from "effect/TestClock" + +describe("Stream", () => { + it.effect("map", () => + Effect.gen(function*() { + const stream = Stream.make(1, 2, 3, 4, 5) + const f = (n: number) => n * 2 + const { result1, result2 } = yield* (Effect.all({ + result1: pipe(stream, Stream.map(f), Stream.runCollect), + result2: pipe(Stream.runCollect(stream), Effect.map(Chunk.map(f))) + })) + deepStrictEqual(Array.from(result1), Array.from(result2)) + })) + + it.effect("mapAccum", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.make(1, 1, 1), + Stream.mapAccum(0, (acc, curr) => [acc + curr, acc + curr]), + Stream.runCollect + ) + deepStrictEqual(Array.from(result), [1, 2, 3]) + })) + + it.effect("mapAccumEffect - happy path", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.make(1, 1, 1), + Stream.mapAccumEffect(0, (acc, curr) => Effect.succeed([acc + curr, acc + curr])), + Stream.runCollect + ) + deepStrictEqual(Array.from(result), [1, 2, 3]) + })) + + it.effect("mapAccumEffect - error", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.make(1, 1, 1), + Stream.mapAccumEffect(0, () => Effect.fail("Ouch")), + Stream.runCollect, + Effect.either + ) + assertLeft(result, "Ouch") + })) + + it.effect("mapAccumEffect - laziness on chunks", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.make(1, 2, 3), + Stream.mapAccumEffect(void 0, (_, n) => + n === 3 ? + Effect.fail("boom") : + Effect.succeed([void 0, n] as const)), + Stream.either, + Stream.runCollect + ) + deepStrictEqual( + Array.from(result), + [Either.right(1), Either.right(2), Either.left("boom")] + ) + })) + + it.effect("mapConcat", () => + Effect.gen(function*() { + const stream = Stream.make(1, 2, 3, 4, 5) + const f = (n: number) => Chunk.of(n) + const { result1, result2 } = yield* (Effect.all({ + result1: pipe(stream, Stream.mapConcat(f), Stream.runCollect), + result2: pipe(Stream.runCollect(stream), Effect.map(Chunk.flatMap((n) => f(n)))) + })) + deepStrictEqual(Array.from(result1), Array.from(result2)) + })) + + it.effect("mapConcatChunk", () => + Effect.gen(function*() { + const stream = Stream.make(1, 2, 3, 4, 5) + const f = (n: number) => Chunk.of(n) + const { result1, result2 } = yield* (Effect.all({ + result1: pipe(stream, Stream.mapConcatChunk(f), Stream.runCollect), + result2: pipe(Stream.runCollect(stream), Effect.map(Chunk.flatMap((n) => f(n)))) + })) + deepStrictEqual(Array.from(result1), Array.from(result2)) + })) + + it.effect("mapConcatChunkEffect - happy path", () => + Effect.gen(function*() { + const stream = Stream.make(1, 2, 3, 4, 5) + const f = (n: number) => Chunk.of(n) + const { result1, result2 } = yield* (Effect.all({ + result1: pipe(stream, Stream.mapConcatChunkEffect((n) => Effect.succeed(f(n))), Stream.runCollect), + result2: pipe(Stream.runCollect(stream), Effect.map(Chunk.flatMap((n) => f(n)))) + })) + deepStrictEqual(Array.from(result1), Array.from(result2)) + })) + + it.effect("mapConcatChunkEffect - error", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.make(1, 2, 3), + Stream.mapConcatChunkEffect(() => Effect.fail("Ouch")), + Stream.runCollect, + Effect.either + ) + assertLeft(result, "Ouch") + })) + + it.effect("mapConcatEffect - happy path", () => + Effect.gen(function*() { + const stream = Stream.make(1, 2, 3, 4, 5) + const f = (n: number) => Chunk.of(n) + const { result1, result2 } = yield* (Effect.all({ + result1: pipe(stream, Stream.mapConcatEffect((n) => Effect.succeed(f(n))), Stream.runCollect), + result2: pipe(Stream.runCollect(stream), Effect.map(Chunk.flatMap((n) => f(n)))) + })) + deepStrictEqual(Array.from(result1), Array.from(result2)) + })) + + it.effect("mapConcatEffect - error", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.make(1, 2, 3), + Stream.mapConcatEffect(() => Effect.fail("Ouch")), + Stream.runCollect, + Effect.either + ) + assertLeft(result, "Ouch") + })) + + it.effect("mapError", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.fail("123"), + Stream.mapError((n) => Number.parseInt(n)), + Stream.runCollect, + Effect.either + ) + assertLeft(result, 123) + })) + + it.effect("mapErrorCause", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.failCause(Cause.fail("123")), + Stream.mapErrorCause(Cause.map((s) => Number.parseInt(s))), + Stream.runCollect, + Effect.either + ) + assertLeft(result, 123) + })) + + it.effect("mapEffect - Effect.forEach equivalence", () => + Effect.gen(function*() { + const chunk = Chunk.make(1, 2, 3, 4, 5) + const stream = Stream.fromIterable(chunk) + const f = (n: number) => Effect.succeed(n * 2) + const { result1, result2 } = yield* (Effect.all({ + result1: pipe(stream, Stream.mapEffect(f), Stream.runCollect), + result2: pipe(chunk, Effect.forEach(f)) + })) + deepStrictEqual(Array.from(result1), Array.from(result2)) + })) + + it.effect("mapEffect - laziness on chunks", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.make(1, 2, 3), + Stream.mapEffect((n) => + n === 3 ? + Effect.fail("boom") : + Effect.succeed(n) + ), + Stream.either, + Stream.runCollect + ) + deepStrictEqual( + Array.from(result), + [Either.right(1), Either.right(2), Either.left("boom")] + ) + })) + + it.effect("mapEffectPar - Effect.forEachParN equivalence", () => + Effect.gen(function*() { + const concurrency = 8 + const chunk = Chunk.make(1, 2, 3, 4, 5) + const stream = Stream.fromIterable(chunk) + const f = (n: number) => Effect.succeed(n * 2) + const { result1, result2 } = yield* (Effect.all({ + result1: pipe(stream, Stream.mapEffect(f, { concurrency }), Stream.runCollect), + result2: Effect.forEach(chunk, f, { concurrency }) + })) + deepStrictEqual(Array.from(result1), Array.from(result2)) + })) + + it.effect("mapEffectPar - ordering when parallelism is 1", () => + Effect.gen(function*() { + const queue = yield* (Queue.unbounded()) + yield* pipe( + Stream.range(0, 8), + Stream.mapEffect((n) => Queue.offer(queue, n), { concurrency: 1 }), + Stream.runDrain + ) + const result = yield* (Queue.takeAll(queue)) + deepStrictEqual(Array.from(result), [0, 1, 2, 3, 4, 5, 6, 7, 8]) + })) + + it.effect("mapEffectPar - interruption propagation", () => + Effect.gen(function*() { + const ref = yield* (Ref.make(false)) + const latch = yield* (Deferred.make()) + const fiber = yield* pipe( + Stream.make(void 0), + Stream.mapEffect(() => + pipe( + Deferred.succeed(latch, void 0), + Effect.zipRight(Effect.never), + Effect.onInterrupt(() => Ref.set(ref, true)) + ), { concurrency: 2 }), + Stream.runDrain, + Effect.fork + ) + yield* (Deferred.await(latch)) + yield* (Fiber.interrupt(fiber)) + const result = yield* (Ref.get(ref)) + assertTrue(result) + })) + + it.effect("mapEffectPar - guarantees ordering", () => + Effect.gen(function*() { + const n = 4096 + const chunk = Chunk.make(1, 2, 3, 4, 5) + const stream = Stream.fromChunk(chunk) + const { result1, result2 } = yield* (Effect.all({ + result1: pipe(stream, Stream.mapEffect(Effect.succeed), Stream.runCollect), + result2: pipe(stream, Stream.mapEffect(Effect.succeed, { concurrency: n }), Stream.runCollect) + })) + deepStrictEqual(Array.from(result1), Array.from(result2)) + })) + + it.effect("mapEffectPar - awaits child fibers properly", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.fromIterable(Chunk.range(0, 10)), + Stream.interruptWhen(Effect.never), + Stream.mapEffect(() => pipe(Effect.succeed(1), Effect.repeatN(200)), { concurrency: 8 }), + Stream.runDrain, + Effect.exit + ) + assertFalse(Exit.isInterrupted(result)) + })) + + it.effect("mapEffectPar - interrupts pending tasks when one of the tasks fails", () => + Effect.gen(function*() { + const ref = yield* (Ref.make(0)) + const latch1 = yield* (Deferred.make()) + const latch2 = yield* (Deferred.make()) + const result = yield* pipe( + Stream.make(1, 2, 3), + Stream.mapEffect( + (n) => + n === 1 ? + pipe( + Deferred.succeed(latch1, void 0), + Effect.zipRight(Effect.never), + Effect.onInterrupt(() => Ref.update(ref, (n) => n + 1)) + ) : + n === 2 ? + pipe( + Deferred.succeed(latch2, void 0), + Effect.zipRight(Effect.never), + Effect.onInterrupt(() => Ref.update(ref, (n) => n + 1)) + ) : + pipe( + Deferred.await(latch1), + Effect.zipRight(Deferred.await(latch1)), + Effect.zipRight(Effect.fail("boom")) + ), + { concurrency: 3 } + ), + Stream.runDrain, + Effect.exit + ) + const count = yield* (Ref.get(ref)) + strictEqual(count, 2) + deepStrictEqual(result, Exit.fail("boom")) + })) + + it.effect("mapEffectPar - propagates the correct error with subsequent calls to mapEffectPar (ZIO #4514)", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.fromIterable(Chunk.range(1, 50)), + Stream.mapEffect((n) => n < 10 ? Effect.succeed(n) : Effect.fail("boom"), { concurrency: 20 }), + Stream.mapEffect((n) => Effect.succeed(n), { concurrency: 20 }), + Stream.runCollect, + Effect.either + ) + assertLeft(result, "boom") + })) + + it.effect("mapEffectPar - propagates the error of the original stream", () => + Effect.gen(function*() { + const fiber = yield* pipe( + Stream.range(1, 10), + Stream.concat(Stream.fail(new Cause.RuntimeException("boom"))), + Stream.mapEffect(() => Effect.sleep(Duration.seconds(1)), { concurrency: 2 }), + Stream.runDrain, + Effect.fork + ) + yield* (TestClock.adjust(Duration.seconds(5))) + const exit = yield* (Fiber.await(fiber)) + deepStrictEqual(exit, Exit.fail(new Cause.RuntimeException("boom"))) + })) + + it.effect("mapEffectParUnordered - mapping with failure is failure", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.fromIterable(Chunk.range(0, 3)), + Stream.mapEffect(() => Effect.fail("fail"), { concurrency: 10, unordered: true }), + Stream.runDrain, + Effect.exit + ) + deepStrictEqual(result, Exit.fail("fail")) + })) + + it.effect("mapEffect with key", () => + Effect.gen(function*() { + const fiber = yield* pipe( + Stream.make(10, 20, 30, 40), + Stream.mapEffect((n) => Effect.delay(Effect.succeed(n), n), { key: identity }), + Stream.runCollect, + Effect.fork + ) + yield* TestClock.adjust(40) + const exit = fiber.unsafePoll() + assertTrue(Exit.isExit(exit)) + assertTrue(Exit.isSuccess(exit)) + deepStrictEqual(Chunk.toReadonlyArray(exit.value), [10, 20, 30, 40]) + })) +}) diff --git a/repos/effect/packages/effect/test/Stream/merging.test.ts b/repos/effect/packages/effect/test/Stream/merging.test.ts new file mode 100644 index 0000000..eebe372 --- /dev/null +++ b/repos/effect/packages/effect/test/Stream/merging.test.ts @@ -0,0 +1,171 @@ +import { describe, it } from "@effect/vitest" +import { assertLeft, assertTrue, deepStrictEqual } from "@effect/vitest/utils" +import * as Chunk from "effect/Chunk" +import * as Duration from "effect/Duration" +import * as Effect from "effect/Effect" +import * as Exit from "effect/Exit" +import * as Fiber from "effect/Fiber" +import { constVoid, pipe } from "effect/Function" +import * as HashSet from "effect/HashSet" +import * as Queue from "effect/Queue" +import * as Stream from "effect/Stream" +import * as TestClock from "effect/TestClock" +import * as TestServices from "effect/TestServices" + +describe("Stream", () => { + it.effect("merge - slower stream", () => + Effect.gen(function*() { + const stream1 = Stream.make(1, 2, 3, 4) + const stream2 = Stream.tap( + Stream.make(5, 6, 7, 8), + () => TestServices.provideLive(Effect.sleep(Duration.millis(10))) + ) + const result = yield* pipe( + Stream.merge(stream1, stream2), + Stream.runCollect + ) + deepStrictEqual([...result], [1, 2, 3, 4, 5, 6, 7, 8]) + })) + + it.effect("mergeAll - short circuiting", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.mergeAll([Stream.never, Stream.make(1)], { concurrency: 2 }), + Stream.take(1), + Stream.runCollect + ) + deepStrictEqual(Array.from(result), [1]) + })) + + it.effect("mergeWithTag", (ctx) => + Effect.gen(function*() { + const stream = Stream.mergeWithTag({ + a: Stream.make(0), + b: Stream.make("") + }, { concurrency: 1 }) + + const res = Chunk.toArray(yield* Stream.runCollect(stream)) + ctx.expect(res).toEqual([ + { _tag: "a", value: 0 }, + { _tag: "b", value: "" } + ]) + })) + + it.effect("mergeHaltLeft - terminates as soon as the first stream terminates", () => + Effect.gen(function*() { + const queue1 = yield* (Queue.unbounded()) + const queue2 = yield* (Queue.unbounded()) + const stream1 = Stream.fromQueue(queue1) + const stream2 = Stream.fromQueue(queue2) + const fiber = yield* pipe( + stream1, + Stream.merge(stream2, { haltStrategy: "left" }), + Stream.runCollect, + Effect.fork + ) + yield* pipe(Queue.offer(queue1, 1), Effect.zipRight(TestClock.adjust(Duration.seconds(1)))) + yield* pipe(Queue.offer(queue1, 2), Effect.zipRight(TestClock.adjust(Duration.seconds(1)))) + yield* pipe(Queue.shutdown(queue1), Effect.zipRight(TestClock.adjust(Duration.seconds(1)))) + yield* (Queue.offer(queue2, 3)) + const result = yield* (Fiber.join(fiber)) + deepStrictEqual(Array.from(result), [1, 2]) + })) + + it.effect("mergeHaltEither - interrupts pulling on finish", () => + Effect.gen(function*() { + const stream1 = Stream.make(1, 2, 3) + const stream2 = Stream.fromEffect(pipe(Effect.sleep(Duration.seconds(5)), Effect.as(4))) + const result = yield* pipe( + stream1, + Stream.merge(stream2, { haltStrategy: "left" }), + Stream.runCollect + ) + deepStrictEqual(Array.from(result), [1, 2, 3]) + })) + + it.effect("mergeHaltRight - terminates as soon as the second stream terminates", () => + Effect.gen(function*() { + const queue1 = yield* (Queue.unbounded()) + const queue2 = yield* (Queue.unbounded()) + const stream1 = Stream.fromQueue(queue1) + const stream2 = Stream.fromQueue(queue2) + const fiber = yield* pipe( + stream1, + Stream.merge(stream2, { haltStrategy: "right" }), + Stream.runCollect, + Effect.fork + ) + yield* pipe(Queue.offer(queue2, 1), Effect.zipRight(TestClock.adjust(Duration.seconds(1)))) + yield* pipe(Queue.offer(queue2, 2), Effect.zipRight(TestClock.adjust(Duration.seconds(1)))) + yield* pipe(Queue.shutdown(queue2), Effect.zipRight(TestClock.adjust(Duration.seconds(1)))) + yield* (Queue.offer(queue1, 3)) + const result = yield* (Fiber.join(fiber)) + deepStrictEqual(Array.from(result), [1, 2]) + })) + + it.effect("mergeHaltEither - terminates as soon as either stream terminates", () => + Effect.gen(function*() { + const queue1 = yield* (Queue.unbounded()) + const queue2 = yield* (Queue.unbounded()) + const stream1 = Stream.fromQueue(queue1) + const stream2 = Stream.fromQueue(queue2) + const fiber = yield* pipe( + stream1, + Stream.merge(stream2, { haltStrategy: "either" }), + Stream.runCollect, + Effect.fork + ) + yield* (Queue.shutdown(queue1)) + yield* (TestClock.adjust(Duration.seconds(1))) + yield* (Queue.offer(queue2, 1)) + const result = yield* (Fiber.join(fiber)) + assertTrue(Chunk.isEmpty(result)) + })) + + it.effect("merge - equivalence with set union", () => + Effect.gen(function*() { + const stream1 = Stream.make(1, 2, 3, 4) + const stream2 = Stream.make(5, 6, 7, 8) + const { result1, result2 } = yield* (Effect.all({ + result1: pipe( + stream1, + Stream.merge(stream2), + Stream.runCollect, + Effect.map(HashSet.fromIterable) + ), + result2: pipe( + Stream.runCollect(stream1), + Effect.zipWith( + Stream.runCollect(stream2), + (chunk1, chunk2) => pipe(chunk1, Chunk.appendAll(chunk2)) + ), + Effect.map(HashSet.fromIterable) + ) + })) + deepStrictEqual(Array.from(result1), Array.from(result2)) + })) + + it.effect("merge - fails as soon as one stream fails", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.make(1, 2, 3), + Stream.merge(Stream.fail(void 0)), + Stream.runCollect, + Effect.exit + ) + assertTrue(Exit.isFailure(result)) + })) + + it.effect("mergeWith - prioritizes failures", () => + Effect.gen(function*() { + const stream1 = Stream.never + const stream2 = Stream.fail("Ouch") + const result = yield* pipe( + stream1, + Stream.mergeWith(stream2, { onSelf: constVoid, onOther: constVoid }), + Stream.runCollect, + Effect.either + ) + assertLeft(result, "Ouch") + })) +}) diff --git a/repos/effect/packages/effect/test/Stream/pagination.test.ts b/repos/effect/packages/effect/test/Stream/pagination.test.ts new file mode 100644 index 0000000..cc0ed2b --- /dev/null +++ b/repos/effect/packages/effect/test/Stream/pagination.test.ts @@ -0,0 +1,86 @@ +import { describe, it } from "@effect/vitest" +import { deepStrictEqual } from "@effect/vitest/utils" +import * as Chunk from "effect/Chunk" +import * as Effect from "effect/Effect" +import { pipe } from "effect/Function" +import * as Option from "effect/Option" +import * as Stream from "effect/Stream" + +describe("Stream", () => { + it.effect("paginate", () => + Effect.gen(function*() { + const s: readonly [number, Array] = [0, [1, 2, 3]] + const result = yield* pipe( + Stream.paginate(s, ([n, nums]) => + nums.length === 0 ? + [n, Option.none()] as const : + [n, Option.some([nums[0], nums.slice(1)] as const)] as const), + Stream.runCollect + ) + deepStrictEqual(Array.from(result), [0, 1, 2, 3]) + })) + + it.effect("paginateEffect", () => + Effect.gen(function*() { + const s: readonly [number, Array] = [0, [1, 2, 3]] + const result = yield* pipe( + Stream.paginateEffect( + s, + ( + [n, nums] + ): Effect.Effect]>]> => + nums.length === 0 ? + Effect.succeed([n, Option.none()]) : + Effect.succeed([n, Option.some([nums[0], nums.slice(1)])]) + ), + Stream.runCollect + ) + deepStrictEqual(Array.from(result), [0, 1, 2, 3]) + })) + + it.effect("paginateChunk", () => + Effect.gen(function*() { + const s: readonly [Chunk.Chunk, Array] = [Chunk.of(0), [1, 2, 3, 4, 5]] + const pageSize = 2 + const result = yield* pipe( + Stream.paginateChunk(s, ([chunk, nums]) => + nums.length === 0 ? + [chunk, Option.none()] as const : + [ + chunk, + Option.some( + [ + Chunk.fromIterable(nums.slice(0, pageSize)), + nums.slice(pageSize) + ] as const + ) + ] as const), + Stream.runCollect + ) + deepStrictEqual(Array.from(result), [0, 1, 2, 3, 4, 5]) + })) + + it.effect("paginateChunkEffect", () => + Effect.gen(function*() { + const s: readonly [Chunk.Chunk, Array] = [Chunk.of(0), [1, 2, 3, 4, 5]] + const pageSize = 2 + const result = yield* pipe( + Stream.paginateChunkEffect(s, ([chunk, nums]) => + nums.length === 0 ? + Effect.succeed([chunk, Option.none, Array]>()] as const) : + Effect.succeed( + [ + chunk, + Option.some( + [ + Chunk.fromIterable(nums.slice(0, pageSize)), + nums.slice(pageSize) + ] as const + ) + ] as const + )), + Stream.runCollect + ) + deepStrictEqual(Array.from(result), [0, 1, 2, 3, 4, 5]) + })) +}) diff --git a/repos/effect/packages/effect/test/Stream/partitioning.test.ts b/repos/effect/packages/effect/test/Stream/partitioning.test.ts new file mode 100644 index 0000000..d24f1af --- /dev/null +++ b/repos/effect/packages/effect/test/Stream/partitioning.test.ts @@ -0,0 +1,103 @@ +import { describe, it } from "@effect/vitest" +import { assertLeft, deepStrictEqual, strictEqual } from "@effect/vitest/utils" +import * as Chunk from "effect/Chunk" +import * as Deferred from "effect/Deferred" +import * as Effect from "effect/Effect" +import * as Either from "effect/Either" +import * as Fiber from "effect/Fiber" +import { pipe } from "effect/Function" +import * as Ref from "effect/Ref" +import * as Stream from "effect/Stream" + +describe("Stream", () => { + it.effect("partitionEither - allows repeated runs without hanging", () => + Effect.gen(function*() { + const stream = pipe( + Stream.fromIterable(Chunk.empty()), + Stream.partitionEither((n) => Effect.succeed(n % 2 === 0 ? Either.left(n) : Either.right(n))), + Effect.map(([evens, odds]) => pipe(evens, Stream.mergeEither(odds))), + Effect.flatMap(Stream.runCollect), + Effect.scoped + ) + const result = yield* pipe( + Effect.all(Array.from({ length: 100 }, () => stream)), + Effect.as(0) + ) + strictEqual(result, 0) + })) + + it.effect("partition - values", () => + Effect.gen(function*() { + const { result1, result2 } = yield* pipe( + Stream.range(0, 5), + Stream.partition((n) => n % 2 === 0), + Effect.flatMap(([odds, evens]) => + Effect.all({ + result1: Stream.runCollect(evens), + result2: Stream.runCollect(odds) + }) + ), + Effect.scoped + ) + deepStrictEqual(Array.from(result1), [0, 2, 4]) + deepStrictEqual(Array.from(result2), [1, 3, 5]) + })) + + it.effect("partition - errors", () => + Effect.gen(function*() { + const { result1, result2 } = yield* pipe( + Stream.make(0), + Stream.concat(Stream.fail("boom")), + Stream.partition((n) => n % 2 === 0), + Effect.flatMap(([evens, odds]) => + Effect.all({ + result1: Effect.either(Stream.runCollect(evens)), + result2: Effect.either(Stream.runCollect(odds)) + }) + ), + Effect.scoped + ) + assertLeft(result1, "boom") + assertLeft(result2, "boom") + })) + + it.effect("partition - backpressure", () => + Effect.gen(function*() { + const { result1, result2, result3 } = yield* pipe( + Stream.range(0, 5), + Stream.partition((n) => (n % 2 === 0), { bufferSize: 1 }), + Effect.flatMap(([odds, evens]) => + Effect.gen(function*() { + const ref = yield* (Ref.make(Chunk.empty())) + const latch = yield* (Deferred.make()) + const fiber = yield* pipe( + evens, + Stream.tap((n) => + pipe( + Ref.update(ref, Chunk.prepend(n)), + Effect.zipRight( + pipe( + Deferred.succeed(latch, void 0), + Effect.when(() => n === 2) + ) + ) + ) + ), + Stream.runDrain, + Effect.fork + ) + yield* (Deferred.await(latch)) + const result1 = yield* (Ref.get(ref)) + const result2 = yield* (Stream.runCollect(odds)) + yield* (Fiber.await(fiber)) + const result3 = yield* (Ref.get(ref)) + return { result1, result2, result3 } + }) + ), + Effect.scoped + ) + deepStrictEqual(Array.from(result1), [2, 0]) + deepStrictEqual(Array.from(result2), [1, 3, 5]) + deepStrictEqual(Array.from(result3), [4, 2, 0]) + })) +}) diff --git a/repos/effect/packages/effect/test/Stream/peeling.test.ts b/repos/effect/packages/effect/test/Stream/peeling.test.ts new file mode 100644 index 0000000..1b6f497 --- /dev/null +++ b/repos/effect/packages/effect/test/Stream/peeling.test.ts @@ -0,0 +1,45 @@ +import { describe, it } from "@effect/vitest" +import { deepStrictEqual } from "@effect/vitest/utils" +import * as Chunk from "effect/Chunk" +import * as Effect from "effect/Effect" +import * as Exit from "effect/Exit" +import { constTrue, pipe } from "effect/Function" +import * as Sink from "effect/Sink" +import * as Stream from "effect/Stream" + +describe("Stream", () => { + it.effect("peel", () => + Effect.gen(function*() { + const sink = Sink.take(3) + const [peeled, rest] = yield* pipe( + Stream.fromChunks(Chunk.range(1, 3), Chunk.range(4, 6)), + Stream.peel(sink), + Effect.flatMap(([peeled, rest]) => + pipe( + Stream.runCollect(rest), + Effect.map((rest) => [peeled, rest]) + ) + ), + Effect.scoped + ) + deepStrictEqual(Array.from(peeled), [1, 2, 3]) + deepStrictEqual(Array.from(rest), [4, 5, 6]) + })) + + it.effect("peel - propagates errors", () => + Effect.gen(function*() { + const stream = Stream.repeatEffect(Effect.fail("fail")) + const sink = Sink.fold, number>( + Chunk.empty(), + constTrue, + Chunk.append + ) + const result = yield* pipe( + stream, + Stream.peel(sink), + Effect.exit, + Effect.scoped + ) + deepStrictEqual(result, Exit.fail("fail")) + })) +}) diff --git a/repos/effect/packages/effect/test/Stream/racing.test.ts b/repos/effect/packages/effect/test/Stream/racing.test.ts new file mode 100644 index 0000000..111fa17 --- /dev/null +++ b/repos/effect/packages/effect/test/Stream/racing.test.ts @@ -0,0 +1,68 @@ +import { describe, it } from "@effect/vitest" +import { deepStrictEqual } from "@effect/vitest/utils" +import * as Chunk from "effect/Chunk" +import * as Effect from "effect/Effect" +import * as Fiber from "effect/Fiber" +import { pipe } from "effect/Function" +import * as Schedule from "effect/Schedule" +import * as Stream from "effect/Stream" +import * as TestClock from "effect/TestClock" + +describe("Stream", () => { + it.effect("raceAll sync", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.raceAll( + Stream.make(0, 1, 2, 3), + Stream.make(4, 5, 6, 7), + Stream.make(7, 8, 9, 10) + ), + Stream.runCollect, + Effect.map(Chunk.toReadonlyArray) + ) + deepStrictEqual(result, [0, 1, 2, 3]) + })) + + it.effect("raceAll async", () => + Effect.gen(function*() { + const fiber = yield* pipe( + Stream.raceAll( + Stream.fromSchedule(Schedule.spaced("1 second")), + Stream.fromSchedule(Schedule.spaced("2 second")) + ), + Stream.take(5), + Stream.runCollect, + Effect.map(Chunk.toReadonlyArray), + Effect.fork + ) + yield* TestClock.adjust("5 second") + const result = yield* Fiber.join(fiber) + deepStrictEqual(result, [0, 1, 2, 3, 4]) + })) + + it.effect("raceAll combined async + sync", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.raceAll( + Stream.fromSchedule(Schedule.spaced("1 second")), + Stream.make(0, 1, 2, 3) + ), + Stream.runCollect, + Effect.map(Chunk.toReadonlyArray) + ) + deepStrictEqual(result, [0, 1, 2, 3]) + })) + + it.effect("raceAll combined sync + async", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.raceAll( + Stream.make(0, 1, 2, 3), + Stream.fromSchedule(Schedule.spaced("1 second")) + ), + Stream.runCollect, + Effect.map(Chunk.toReadonlyArray) + ) + deepStrictEqual(result, [0, 1, 2, 3]) + })) +}) diff --git a/repos/effect/packages/effect/test/Stream/repeating.test.ts b/repos/effect/packages/effect/test/Stream/repeating.test.ts new file mode 100644 index 0000000..1eef782 --- /dev/null +++ b/repos/effect/packages/effect/test/Stream/repeating.test.ts @@ -0,0 +1,354 @@ +import { describe, it } from "@effect/vitest" +import { deepStrictEqual, strictEqual } from "@effect/vitest/utils" +import * as Chunk from "effect/Chunk" +import * as Duration from "effect/Duration" +import * as Effect from "effect/Effect" +import * as Either from "effect/Either" +import * as Exit from "effect/Exit" +import * as fc from "effect/FastCheck" +import * as Fiber from "effect/Fiber" +import { constVoid, identity, pipe } from "effect/Function" +import * as Option from "effect/Option" +import * as Ref from "effect/Ref" +import * as Schedule from "effect/Schedule" +import * as Stream from "effect/Stream" +import * as TestClock from "effect/TestClock" +import * as TestEnvironment from "effect/TestContext" + +describe("Stream", () => { + it.effect("forever", () => + Effect.gen(function*() { + const ref = yield* (Ref.make(0)) + yield* pipe( + Stream.make(1), + Stream.forever, + Stream.runForEachWhile(() => Ref.modify(ref, (sum) => [sum >= 9 ? false : true, sum + 1] as const)) + ) + const result = yield* (Ref.get(ref)) + strictEqual(result, 10) + })) + + it.effect("repeat", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.make(1), + Stream.repeat(Schedule.recurs(4)), + Stream.runCollect + ) + deepStrictEqual(Array.from(result), [1, 1, 1, 1, 1]) + })) + + it.effect("tick", () => + Effect.gen(function*() { + const fiber = yield* pipe( + Stream.tick("10 millis"), + Stream.take(2), + Stream.runCollect, + Effect.fork + ) + yield* (TestClock.adjust(Duration.millis(50))) + const result = yield* (Fiber.join(fiber)) + deepStrictEqual(Array.from(result), [undefined, undefined]) + })) + + it.effect("repeat - short circuits", () => + Effect.gen(function*() { + const ref = yield* (Ref.make(Chunk.empty())) + const fiber = yield* pipe( + Stream.fromEffect(Ref.update(ref, Chunk.prepend(1))), + Stream.repeat(Schedule.spaced(Duration.millis(10))), + Stream.take(2), + Stream.runDrain, + Effect.fork + ) + yield* (TestClock.adjust(Duration.millis(50))) + yield* (Fiber.join(fiber)) + const result = yield* (Ref.get(ref)) + deepStrictEqual(Array.from(result), [1, 1]) + })) + + it.effect("repeat - Schedule.CurrentIterationMetadata", () => + Effect.gen(function*() { + const ref = yield* (Ref.make(Chunk.empty())) + const fiber = yield* pipe( + Stream.fromEffect( + Schedule.CurrentIterationMetadata.pipe( + Effect.flatMap((currentIterationMetadata) => Ref.update(ref, Chunk.append(currentIterationMetadata))) + ) + ), + Stream.repeat(Schedule.exponential(Duration.millis(10))), + Stream.runDrain, + Effect.fork + ) + + yield* (TestClock.adjust(Duration.millis(70))) + yield* (Fiber.interrupt(fiber)) + const result = yield* (Ref.get(ref)) + deepStrictEqual(Array.from(result), [ + { + elapsed: Duration.zero, + elapsedSincePrevious: Duration.zero, + input: undefined, + output: undefined, + now: 0, + recurrence: 0, + start: 0 + }, + { + elapsed: Duration.zero, + elapsedSincePrevious: Duration.zero, + input: undefined, + output: Duration.millis(10), + now: 0, + recurrence: 1, + start: 0 + }, + { + elapsed: Duration.millis(10), + elapsedSincePrevious: Duration.millis(10), + input: undefined, + output: Duration.millis(20), + now: 10, + recurrence: 2, + start: 0 + }, + { + elapsed: Duration.millis(30), + elapsedSincePrevious: Duration.millis(20), + input: undefined, + output: Duration.millis(40), + now: 30, + recurrence: 3, + start: 0 + } + ]) + })) + + it.effect("repeat - does not swallow errors on a repetition", () => + Effect.gen(function*() { + const ref = yield* (Ref.make(0)) + const result = yield* pipe( + Stream.fromEffect(pipe( + Ref.getAndUpdate(ref, (n) => n + 1), + Effect.flatMap((n) => n <= 2 ? Effect.succeed(n) : Effect.fail("boom")) + )), + Stream.repeat(Schedule.recurs(3)), + Stream.runDrain, + Effect.exit + ) + deepStrictEqual(result, Exit.fail("boom")) + })) + + it.effect("repeatEither", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.make(1), + Stream.repeatEither(Schedule.recurs(4)), + Stream.runCollect + ) + deepStrictEqual(Array.from(result), [ + Either.right(1), + Either.right(1), + Either.left(0), + Either.right(1), + Either.left(1), + Either.right(1), + Either.left(2), + Either.right(1), + Either.left(3) + ]) + })) + + it.effect("repeatEffectOption - emit elements", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.repeatEffectOption(Effect.succeed(1)), + Stream.take(2), + Stream.runCollect + ) + deepStrictEqual(Array.from(result), [1, 1]) + })) + + it.effect("repeatEffectOption - emit elements until pull fails with None", () => + Effect.gen(function*() { + const ref = yield* (Ref.make(0)) + const result = yield* pipe( + Stream.repeatEffectOption( + pipe( + Ref.updateAndGet(ref, (n) => n + 1), + Effect.flatMap((n) => + n >= 5 ? + Effect.fail(Option.none()) : + Effect.succeed(n) + ) + ) + ), + Stream.take(10), + Stream.runCollect + ) + deepStrictEqual(Array.from(result), [1, 2, 3, 4]) + })) + + it.effect("repeatEffectOption - stops evaluating the effect once it fails with None", () => + Effect.gen(function*() { + const ref = yield* (Ref.make(0)) + yield* pipe( + Stream.repeatEffectOption(pipe( + Ref.updateAndGet(ref, (n) => n + 1), + Effect.zipRight(Effect.fail(Option.none())) + )), + Stream.toPull, + Effect.flatMap((pull) => + pipe( + Effect.ignore(pull), + Effect.zipRight(Effect.ignore(pull)) + ) + ), + Effect.scoped + ) + const result = yield* (Ref.get(ref)) + strictEqual(result, 1) + })) + + it.effect("repeatEffectWithSchedule", () => + Effect.gen(function*() { + const ref = yield* (Ref.make(Chunk.empty())) + const fiber = yield* pipe( + Stream.repeatEffectWithSchedule( + Ref.update(ref, Chunk.append(1)), + Schedule.spaced(Duration.millis(10)) + ), + Stream.take(2), + Stream.runDrain, + Effect.fork + ) + yield* (TestClock.adjust(Duration.millis(50))) + yield* (Fiber.join(fiber)) + const result = yield* (Ref.get(ref)) + deepStrictEqual(Array.from(result), [1, 1]) + }), 10000) + + it("repeatEffectWithSchedule - allow schedule to rely on effect value", () => + fc.assert(fc.asyncProperty(fc.integer({ min: 1, max: 100 }), async (length) => { + const effect = Effect.gen(function*() { + const ref = yield* (Ref.make(0)) + const effect = pipe( + Ref.getAndUpdate(ref, (n) => n + 1), + Effect.filterOrFail( + (n) => n <= length + 1, + constVoid + ) + ) + const schedule = pipe( + Schedule.identity(), + Schedule.whileOutput((n) => n < length) + ) + const stream = Stream.repeatEffectWithSchedule(effect, schedule) + return yield* pipe( + Stream.runCollect(stream), + Effect.provide(TestEnvironment.TestContext) + ) + }) + const result = await Effect.runPromise(effect) + deepStrictEqual(Array.from(result), Array.from(Chunk.range(0, length))) + }))) + + it.effect("repeatEffectWithSchedule - should perform repetitions in addition to the first execution (one repetition)", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.repeatEffectWithSchedule(Effect.succeed(1), Schedule.once), + Stream.runCollect + ) + deepStrictEqual(Array.from(result), [1, 1]) + })) + + it.effect("repeatEffectWithSchedule - should perform repetitions in addition to the first execution (zero repetitions)", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.repeatEffectWithSchedule(Effect.succeed(1), Schedule.stop), + Stream.runCollect + ) + deepStrictEqual(Array.from(result), [1]) + })) + + it.effect("repeatEffectWithSchedule - emits before delaying according to the schedule", () => + Effect.gen(function*() { + const ref = yield* (Ref.make(0)) + const schedule = Schedule.spaced(Duration.seconds(1)) + const fiber = yield* pipe( + Stream.repeatEffectWithSchedule(Effect.void, schedule), + Stream.tap(() => Ref.update(ref, (n) => n + 1)), + Stream.runDrain, + Effect.fork + ) + yield* (TestClock.adjust(Duration.seconds(0))) + const result1 = yield* (Ref.get(ref)) + yield* (TestClock.adjust(Duration.seconds(1))) + const result2 = yield* (Ref.get(ref)) + yield* (Fiber.interrupt(fiber)) + strictEqual(result1, 1) + strictEqual(result2, 2) + })) + + it.effect("repeatEither - short circuits", () => + Effect.gen(function*() { + const ref = yield* (Ref.make(Chunk.empty())) + const fiber = yield* pipe( + Stream.fromEffect(Ref.update(ref, Chunk.prepend(1))), + Stream.repeatEither(Schedule.spaced(Duration.millis(10))), + Stream.take(3), // take one of the schedule outputs + Stream.runDrain, + Effect.fork + ) + yield* (TestClock.adjust(Duration.millis(50))) + yield* (Fiber.join(fiber)) + const result = yield* (Ref.get(ref)) + deepStrictEqual(Array.from(result), [1, 1]) + })) + + it.effect("repeatElements - simple", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.make("A", "B", "C"), + Stream.repeatElements(Schedule.once), + Stream.runCollect + ) + deepStrictEqual(Array.from(result), ["A", "A", "B", "B", "C", "C"]) + })) + + it.effect("repeatElements - short circuits in a schedule", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.make("A", "B", "C"), + Stream.repeatElements(Schedule.once), + Stream.take(4), + Stream.runCollect + ) + deepStrictEqual(Array.from(result), ["A", "A", "B", "B"]) + })) + + it.effect("repeatElements - short circuits after schedule", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.make("A", "B", "C"), + Stream.repeatElements(Schedule.once), + Stream.take(3), + Stream.runCollect + ) + deepStrictEqual(Array.from(result), ["A", "A", "B"]) + })) + + it.effect("repeatElementsWith", () => + Effect.gen(function*() { + const schedule = pipe( + Schedule.recurs(0), + Schedule.zipRight(Schedule.fromFunction(() => 123)) + ) + const result = yield* pipe( + Stream.make("A", "B", "C"), + Stream.repeatElementsWith(schedule, { onElement: identity, onSchedule: String }), + Stream.runCollect + ) + deepStrictEqual(Array.from(result), ["A", "123", "B", "123", "C", "123"]) + })) +}) diff --git a/repos/effect/packages/effect/test/Stream/retrying.test.ts b/repos/effect/packages/effect/test/Stream/retrying.test.ts new file mode 100644 index 0000000..abefc2f --- /dev/null +++ b/repos/effect/packages/effect/test/Stream/retrying.test.ts @@ -0,0 +1,176 @@ +import { describe, it } from "@effect/vitest" +import { deepStrictEqual } from "@effect/vitest/utils" +import * as Array from "effect/Array" +import * as Chunk from "effect/Chunk" +import * as Clock from "effect/Clock" +import * as Duration from "effect/Duration" +import * as Effect from "effect/Effect" +import * as Fiber from "effect/Fiber" +import { pipe } from "effect/Function" +import * as Option from "effect/Option" +import * as Ref from "effect/Ref" +import * as Schedule from "effect/Schedule" +import * as Stream from "effect/Stream" +import * as TestClock from "effect/TestClock" + +describe("Stream", () => { + it.effect("retry - retries a failing stream", () => + Effect.gen(function*() { + const ref = yield* Ref.make(0) + const stream = pipe( + Stream.fromEffect(Ref.getAndUpdate(ref, (n) => n + 1)), + Stream.concat(Stream.fail(Option.none())) + ) + const result = yield* pipe( + stream, + Stream.retry(Schedule.forever), + Stream.take(2), + Stream.runCollect + ) + deepStrictEqual(Array.fromIterable(result), [0, 1]) + })) + + it.effect("retry - cleans up resources before restarting the stream", () => + Effect.gen(function*() { + const ref = yield* Ref.make(0) + const stream = pipe( + Effect.addFinalizer(() => Ref.getAndUpdate(ref, (n) => n + 1)), + Effect.as( + pipe( + Stream.fromEffect(Ref.get(ref)), + Stream.concat(Stream.fail(Option.none())) + ) + ), + Stream.unwrapScoped + ) + const result = yield* pipe( + stream, + Stream.retry(Schedule.forever), + Stream.take(2), + Stream.runCollect + ) + deepStrictEqual(Array.fromIterable(result), [0, 1]) + })) + + it.effect("retry - retries a failing stream according to a schedule", () => + Effect.gen(function*() { + const ref = yield* Ref.make(Chunk.empty()) + const stream = pipe( + Stream.fromEffect( + pipe( + Clock.currentTimeMillis, + Effect.flatMap((n) => Ref.update(ref, Chunk.prepend(n))) + ) + ), + Stream.flatMap(() => Stream.fail(Option.none())) + ) + const fiber = yield* pipe( + stream, + Stream.retry(Schedule.exponential(Duration.seconds(1))), + Stream.take(3), + Stream.runDrain, + Effect.fork + ) + yield* TestClock.adjust(Duration.seconds(1)) + yield* TestClock.adjust(Duration.seconds(2)) + yield* Fiber.interrupt(fiber) + const result = yield* pipe(Ref.get(ref), Effect.map(Chunk.map((n) => new Date(n).getSeconds()))) + deepStrictEqual(Array.fromIterable(result), [3, 1, 0]) + })) + + it.effect("retry - reset the schedule after a successful pull", () => + Effect.gen(function*() { + const times = yield* Ref.make(Chunk.empty()) + const ref = yield* Ref.make(0) + const effect = pipe( + Clock.currentTimeMillis, + Effect.flatMap((time) => + pipe( + Ref.update(times, Chunk.prepend(time / 1000)), + Effect.zipRight(Ref.updateAndGet(ref, (n) => n + 1)) + ) + ) + ) + const stream = pipe( + Stream.fromEffect(effect), + Stream.flatMap((attempt) => + attempt === 3 || attempt === 5 ? + Stream.succeed(attempt) : + Stream.fail(Option.none()) + ), + Stream.forever + ) + const fiber = yield* pipe( + stream, + Stream.retry(Schedule.exponential(Duration.seconds(1))), + Stream.take(2), + Stream.runDrain, + Effect.fork + ) + yield* TestClock.adjust(Duration.seconds(1)) + yield* TestClock.adjust(Duration.seconds(2)) + yield* TestClock.adjust(Duration.seconds(1)) + yield* Fiber.join(fiber) + const result = yield* Ref.get(times) + deepStrictEqual(Array.fromIterable(result), [4, 3, 3, 1, 0]) + })) + + it.effect("retry - Schedule.CurrentIterationMetadata", () => + Effect.gen(function*() { + const iterationMetadata = yield* Ref.make(Chunk.empty()) + const fiber = yield* pipe( + Stream.fail(1), + Stream.catchAll((x) => + Effect.gen(function*() { + const currentIterationMetadata = yield* Schedule.CurrentIterationMetadata + yield* Ref.update(iterationMetadata, Chunk.append(currentIterationMetadata)) + return yield* Effect.fail(x) + }) + ), + Stream.retry(Schedule.exponential(Duration.seconds(1))), + Stream.runDrain, + Effect.fork + ) + yield* TestClock.adjust(Duration.seconds(7)) + yield* Fiber.interrupt(fiber) + const result = yield* Ref.get(iterationMetadata) + deepStrictEqual(Array.fromIterable(result), [ + { + elapsed: Duration.zero, + elapsedSincePrevious: Duration.zero, + input: undefined, + output: undefined, + now: 0, + recurrence: 0, + start: 0 + }, + { + elapsed: Duration.zero, + elapsedSincePrevious: Duration.zero, + input: 1, + output: Duration.millis(1000), + now: 0, + recurrence: 1, + start: 0 + }, + { + elapsed: Duration.seconds(1), + elapsedSincePrevious: Duration.seconds(1), + input: 1, + output: Duration.millis(2000), + now: 1000, + recurrence: 2, + start: 0 + }, + { + elapsed: Duration.seconds(3), + elapsedSincePrevious: Duration.seconds(2), + input: 1, + output: Duration.millis(4000), + now: 3000, + recurrence: 3, + start: 0 + } + ]) + })) +}) diff --git a/repos/effect/packages/effect/test/Stream/running.test.ts b/repos/effect/packages/effect/test/Stream/running.test.ts new file mode 100644 index 0000000..28ffaa0 --- /dev/null +++ b/repos/effect/packages/effect/test/Stream/running.test.ts @@ -0,0 +1,152 @@ +import { describe, it } from "@effect/vitest" +import { assertFalse, assertNone, assertSome, assertTrue, deepStrictEqual, strictEqual } from "@effect/vitest/utils" +import * as Chunk from "effect/Chunk" +import * as Effect from "effect/Effect" +import { pipe } from "effect/Function" +import * as Ref from "effect/Ref" +import * as Sink from "effect/Sink" +import * as Stream from "effect/Stream" + +describe("Stream", () => { + it.effect("runFoldWhile", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.make(1, 1, 1, 1, 1), + Stream.runFoldWhile(0, (n) => n < 3, (x, y) => x + y) + ) + strictEqual(result, 3) + })) + + it.effect("runForEach - with a small data set", () => + Effect.gen(function*() { + const ref = yield* (Ref.make(0)) + yield* pipe( + Stream.make(1, 1, 1, 1, 1), + Stream.runForEach((i) => Ref.update(ref, (n) => n + i)) + ) + const result = yield* (Ref.get(ref)) + strictEqual(result, 5) + })) + + it.effect("runForEach - with a bigger data set", () => + Effect.gen(function*() { + const ref = yield* (Ref.make(0)) + yield* pipe( + Stream.fromIterable(Array.from({ length: 1_000 }, () => 1)), + Stream.runForEach((i) => Ref.update(ref, (n) => n + i)) + ) + const result = yield* (Ref.get(ref)) + strictEqual(result, 1_000) + })) + + it.effect("runForEachWhile - with a small data set", () => + Effect.gen(function*() { + const expected = 3 + const ref = yield* (Ref.make(0)) + yield* pipe( + Stream.make(1, 1, 1, 1, 1, 1), + Stream.runForEachWhile((n) => + Ref.modify(ref, (sum) => + sum >= expected ? + [false, sum] as const : + [true, sum + n]) + ) + ) + const result = yield* (Ref.get(ref)) + strictEqual(result, expected) + })) + + it.effect("runForEachWhile - with a bigger data set", () => + Effect.gen(function*() { + const expected = 500 + const ref = yield* (Ref.make(0)) + yield* pipe( + Stream.fromIterable(Array.from({ length: 1_000 }, () => 1)), + Stream.runForEachWhile((n) => + Ref.modify(ref, (sum) => + sum >= expected ? + [false, sum] as const : + [true, sum + n] as const) + ) + ) + const result = yield* (Ref.get(ref)) + strictEqual(result, expected) + })) + + it.effect("runForEachWhile - short circuits", () => + Effect.gen(function*() { + const ref = yield* (Ref.make(true)) + yield* pipe( + Stream.make(true, true, false), + Stream.concat(Stream.drain(Stream.fromEffect(Ref.set(ref, false)))), + Stream.runForEachWhile(Effect.succeed) + ) + const result = yield* (Ref.get(ref)) + assertTrue(result) + })) + + it.effect("runHead - non-empty stream", () => + Effect.gen(function*() { + const result = yield* (Stream.runHead(Stream.make(1, 2, 3, 4))) + assertSome(result, 1) + })) + + it.effect("runHead - empty stream", () => + Effect.gen(function*() { + const result = yield* (Stream.runHead(Stream.empty)) + assertNone(result) + })) + + it.effect("runHead - pulls up to the first non-empty chunk", () => + Effect.gen(function*() { + const ref = yield* (Ref.make(Chunk.empty())) + const head = yield* pipe( + Stream.make( + Stream.drain(Stream.fromEffect(Ref.update(ref, Chunk.prepend(1)))), + Stream.drain(Stream.fromEffect(Ref.update(ref, Chunk.prepend(2)))), + Stream.make(1), + Stream.drain(Stream.fromEffect(Ref.update(ref, Chunk.prepend(3)))) + ), + Stream.flatten(), + Stream.runHead + ) + const result = yield* (Ref.get(ref)) + assertSome(head, 1) + deepStrictEqual(Array.from(result), [2, 1]) + })) + + it.effect("runLast - non-empty stream", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.make(1, 2, 3, 4), + Stream.runLast + ) + assertSome(result, 4) + })) + + it.effect("runLast - empty stream", () => + Effect.gen(function*() { + const result = yield* pipe(Stream.empty, Stream.runLast) + assertNone(result) + })) + + it.effect("runScoped - properly closes resources", () => + Effect.gen(function*() { + const ref = yield* (Ref.make(false)) + const resource = Effect.acquireRelease( + Effect.succeed(1), + () => Ref.set(ref, true) + ) + const stream = pipe(Stream.scoped(resource), Stream.flatMap((a) => Stream.make(a, a, a))) + const [result, state] = yield* pipe( + stream, + Stream.runScoped(Sink.collectAll()), + Effect.flatMap((chunk) => pipe(Ref.get(ref), Effect.map((closed) => [chunk, closed] as const))), + Effect.scoped + ) + const finalState = yield* (Ref.get(ref)) + deepStrictEqual(Array.from(result), [1, 1, 1]) + assertFalse(state) + assertTrue(finalState) + })) +}) diff --git a/repos/effect/packages/effect/test/Stream/scanning.test.ts b/repos/effect/packages/effect/test/Stream/scanning.test.ts new file mode 100644 index 0000000..2b4a9e6 --- /dev/null +++ b/repos/effect/packages/effect/test/Stream/scanning.test.ts @@ -0,0 +1,38 @@ +import { describe, it } from "@effect/vitest" +import { deepStrictEqual } from "@effect/vitest/utils" +import * as Array from "effect/Array" +import * as Chunk from "effect/Chunk" +import * as Effect from "effect/Effect" +import { pipe } from "effect/Function" +import * as Stream from "effect/Stream" + +describe("Stream", () => { + it.effect("scan", () => + Effect.gen(function*() { + const stream = Stream.make(1, 2, 3, 4, 5) + const { result1, result2 } = yield* Effect.all({ + result1: pipe(stream, Stream.scan(0, (acc, curr) => acc + curr), Stream.runCollect), + result2: pipe( + Stream.runCollect(stream), + Effect.map((chunk) => + pipe( + Chunk.toReadonlyArray(chunk), + Array.scan(0, (acc, curr) => acc + curr) + ) + ) + ) + }) + deepStrictEqual(Chunk.toReadonlyArray(result1), result2) + })) + + it.effect("scanReduce", () => + Effect.gen(function*() { + const stream = Stream.make(1, 2, 3, 4, 5) + const result = yield* pipe( + stream, + Stream.scanReduce((acc, curr) => acc + curr), + Stream.runCollect + ) + deepStrictEqual(Chunk.toReadonlyArray(result), [1, 3, 6, 10, 15]) + })) +}) diff --git a/repos/effect/packages/effect/test/Stream/scheduling.test.ts b/repos/effect/packages/effect/test/Stream/scheduling.test.ts new file mode 100644 index 0000000..205228d --- /dev/null +++ b/repos/effect/packages/effect/test/Stream/scheduling.test.ts @@ -0,0 +1,55 @@ +import { describe, it } from "@effect/vitest" +import { deepStrictEqual } from "@effect/vitest/utils" +import * as Clock from "effect/Clock" +import * as Duration from "effect/Duration" +import * as Effect from "effect/Effect" +import * as Fiber from "effect/Fiber" +import { identity, pipe } from "effect/Function" +import * as Schedule from "effect/Schedule" +import * as Stream from "effect/Stream" +import * as TestClock from "effect/TestClock" + +describe("Stream", () => { + it.effect("schedule", () => + Effect.gen(function*() { + const start = yield* Clock.currentTimeMillis + const fiber = yield* pipe( + Stream.range(1, 8), + Stream.schedule(Schedule.fixed(Duration.millis(100))), + Stream.mapEffect((n) => + pipe( + Clock.currentTimeMillis, + Effect.map((now) => [n, now - start] as const) + ) + ), + Stream.runCollect, + Effect.fork + ) + yield* TestClock.adjust(Duration.millis(800)) + const result = yield* Fiber.join(fiber) + deepStrictEqual(Array.from(result), [ + [1, 100], + [2, 200], + [3, 300], + [4, 400], + [5, 500], + [6, 600], + [7, 700], + [8, 800] + ]) + })) + + it.effect("scheduleWith", () => + Effect.gen(function*() { + const schedule = pipe( + Schedule.recurs(2), + Schedule.zipRight(Schedule.fromFunction(() => "Done")) + ) + const result = yield* pipe( + Stream.make("A", "B", "C", "A", "B", "C"), + Stream.scheduleWith(schedule, { onElement: (s) => s.toLowerCase(), onSchedule: identity }), + Stream.runCollect + ) + deepStrictEqual(Array.from(result), ["a", "b", "c", "Done", "a", "b", "c", "Done"]) + })) +}) diff --git a/repos/effect/packages/effect/test/Stream/scoping.test.ts b/repos/effect/packages/effect/test/Stream/scoping.test.ts new file mode 100644 index 0000000..a925d93 --- /dev/null +++ b/repos/effect/packages/effect/test/Stream/scoping.test.ts @@ -0,0 +1,197 @@ +import { describe, it } from "@effect/vitest" +import { assertFalse, assertLeft, assertSome, assertTrue, deepStrictEqual } from "@effect/vitest/utils" +import * as Array from "effect/Array" +import * as Cause from "effect/Cause" +import * as Chunk from "effect/Chunk" +import * as Deferred from "effect/Deferred" +import * as Effect from "effect/Effect" +import * as Exit from "effect/Exit" +import * as Fiber from "effect/Fiber" +import * as FiberId from "effect/FiberId" +import * as Ref from "effect/Ref" +import * as Scope from "effect/Scope" +import * as Stream from "effect/Stream" + +describe("Stream", () => { + it.effect("acquireRelease - simple example", () => + Effect.gen(function*() { + const ref = yield* Ref.make(false) + const stream = Stream.acquireRelease( + Effect.succeed(Chunk.range(0, 2)), + () => Ref.set(ref, true) + ).pipe(Stream.flatMap(Stream.fromIterable)) + const result = yield* Stream.runCollect(stream) + const released = yield* Ref.get(ref) + assertTrue(released) + deepStrictEqual(Chunk.toArray(result), [0, 1, 2]) + })) + + it.effect("acquireRelease - short circuits", () => + Effect.gen(function*() { + const ref = yield* Ref.make(false) + const stream = Stream.acquireRelease( + Effect.succeed(Chunk.range(0, 2)), + () => Ref.set(ref, true) + ).pipe( + Stream.flatMap(Stream.fromIterable), + Stream.take(2) + ) + const result = yield* Stream.runCollect(stream) + const released = yield* Ref.get(ref) + assertTrue(released) + deepStrictEqual(Chunk.toArray(result), [0, 1]) + })) + + it.effect("acquireRelease - no acquisition when short circuiting", () => + Effect.gen(function*() { + const ref = yield* Ref.make(false) + const stream = Stream.make(1).pipe( + Stream.concat( + Stream.acquireRelease( + Ref.set(ref, true), + () => Effect.void + ) + ), + Stream.take(0) + ) + yield* Stream.runDrain(stream) + const result = yield* Ref.get(ref) + assertFalse(result) + })) + + it.effect("acquireRelease - releases when there are defects", () => + Effect.gen(function*() { + const ref = yield* Ref.make(false) + yield* Stream.acquireRelease( + Effect.void, + () => Ref.set(ref, true) + ).pipe( + Stream.flatMap(() => Stream.fromEffect(Effect.dieMessage("boom"))), + Stream.runDrain, + Effect.exit + ) + const result = yield* Ref.get(ref) + assertTrue(result) + })) + + it.effect("acquireRelease - flatMap associativity does not effect lifetime", () => + Effect.gen(function*() { + const leftAssoc = yield* Stream.acquireRelease( + Ref.make(true), + (ref) => Ref.set(ref, false) + ).pipe( + Stream.flatMap(Stream.succeed), + Stream.flatMap((ref) => Stream.fromEffect(Ref.get(ref))), + Stream.runCollect, + Effect.map(Chunk.head) + ) + const rightAssoc = yield* Stream.acquireRelease( + Ref.make(true), + (ref) => Ref.set(ref, false) + ).pipe( + Stream.flatMap((ref) => + Stream.succeed(ref).pipe( + Stream.flatMap((ref) => Stream.fromEffect(Ref.get(ref))) + ) + ), + Stream.runCollect, + Effect.map(Chunk.head) + ) + assertSome(leftAssoc, true) + assertSome(rightAssoc, true) + })) + + it.effect("acquireRelease - propagates errors", () => + Effect.gen(function*() { + const result = yield* Stream.acquireRelease( + Effect.void, + () => Effect.dieMessage("die") + ).pipe( + Stream.runCollect, + Effect.exit + ) + deepStrictEqual(result, Exit.die(new Cause.RuntimeException("die"))) + })) + + it.effect("ensuring", () => + Effect.gen(function*() { + const ref = yield* Ref.make(Chunk.empty()) + yield* Stream.acquireRelease( + Ref.update(ref, Chunk.append("Acquire")), + () => Ref.update(ref, Chunk.append("Release")) + ).pipe( + Stream.crossRight(Stream.fromEffect(Ref.update(ref, Chunk.append("Use")))), + Stream.ensuring(Ref.update(ref, Chunk.append("Ensuring"))), + Stream.runDrain + ) + const result = yield* Ref.get(ref) + deepStrictEqual(Chunk.toArray(result), ["Acquire", "Use", "Release", "Ensuring"]) + })) + + it.effect("scoped - preserves the failure of an effect", () => + Effect.gen(function*() { + const result = yield* Stream.scoped(Effect.fail("fail")).pipe( + Stream.runCollect, + Effect.either + ) + assertLeft(result, "fail") + })) + + it.effect("scoped - preserves the interruptibility of an effect", () => + Effect.gen(function*() { + const isInterruptible1 = yield* Effect.checkInterruptible(Effect.succeed).pipe( + Stream.scoped, + Stream.runHead + ) + const isInterruptible2 = yield* Effect.uninterruptible( + Effect.checkInterruptible(Effect.succeed) + ).pipe(Stream.scoped, Stream.runHead) + assertSome(isInterruptible1, true) + assertSome(isInterruptible2, false) + })) + + it("unwrapScoped", async () => { + const awaiter = Deferred.unsafeMake(FiberId.none) + const program = Effect.gen(function*() { + const stream = (deferred: Deferred.Deferred, ref: Ref.Ref>) => + Effect.acquireRelease( + Ref.update(ref, (array) => [...array, "acquire outer"]), + () => Ref.update(ref, (array) => [...array, "release outer"]) + ).pipe( + Effect.zipRight(Deferred.succeed(deferred, void 0)), + Effect.zipRight(Deferred.await(awaiter)), + Effect.zipRight(Effect.succeed(Stream.make(1, 2, 3))), + Stream.unwrapScoped + ) + const ref = yield* Ref.make>([]) + const deferred = yield* Deferred.make() + const fiber = yield* stream(deferred, ref).pipe(Stream.runDrain, Effect.fork) + yield* Deferred.await(deferred) + yield* Fiber.interrupt(fiber) + return yield* Ref.get(ref) + }) + const result = await Effect.runPromise(program) + await Effect.runPromise(Deferred.succeed(awaiter, void 0)) + deepStrictEqual(result, ["acquire outer", "release outer"]) + }) + + it.effect("preserves the scope", () => + Effect.gen(function*() { + const ref = yield* Ref.make(Array.empty()) + const scope = yield* Scope.make() + yield* Stream.make(1, 2).pipe( + Stream.flatMap((i) => + Stream.fromEffect(Effect.acquireRelease( + Ref.update(ref, Array.append(`Acquire: ${i}`)), + () => Ref.update(ref, Array.append(`Release: ${i}`)) + )), { bufferSize: 1, concurrency: "unbounded" }), + Stream.runDrain, + Scope.extend(scope) + ) + const before = yield* Ref.getAndSet(ref, Array.empty()) + yield* Scope.close(scope, Exit.void) + const after = yield* Ref.get(ref) + deepStrictEqual(before, ["Acquire: 1", "Acquire: 2"]) + deepStrictEqual(after, ["Release: 2", "Release: 1"]) + })) +}) diff --git a/repos/effect/packages/effect/test/Stream/sequencing.test.ts b/repos/effect/packages/effect/test/Stream/sequencing.test.ts new file mode 100644 index 0000000..c898e0c --- /dev/null +++ b/repos/effect/packages/effect/test/Stream/sequencing.test.ts @@ -0,0 +1,794 @@ +import { describe, it } from "@effect/vitest" +import { assertLeft, assertTrue, deepStrictEqual, strictEqual } from "@effect/vitest/utils" +import * as Cause from "effect/Cause" +import * as Chunk from "effect/Chunk" +import * as Deferred from "effect/Deferred" +import * as Effect from "effect/Effect" +import * as Equal from "effect/Equal" +import * as Exit from "effect/Exit" +import * as Fiber from "effect/Fiber" +import { identity, pipe } from "effect/Function" +import * as Ref from "effect/Ref" +import * as Scope from "effect/Scope" +import * as Stream from "effect/Stream" +import * as Take from "effect/Take" + +const withPermitsScoped = (permits: number) => (semaphore: Effect.Semaphore) => + Effect.acquireRelease( + semaphore.take(permits), + (n) => semaphore.release(n) + ) + +describe("Stream", () => { + it.effect("branchAfter - switches streams", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.fromChunk(Chunk.range(0, 5)), + Stream.branchAfter(1, (values) => { + if (Equal.equals(values, Chunk.make(0))) { + return Stream.identity() + } + throw new Error("should have branched after 0") + }), + Stream.runCollect + ) + deepStrictEqual(Array.from(result), [1, 2, 3, 4, 5]) + })) + + it.effect("branchAfter - emits data if less than n elements are collected", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.fromChunk(Chunk.range(1, 5)), + Stream.branchAfter(6, (chunk) => Stream.prepend(Stream.identity(), chunk)), + Stream.runCollect + ) + deepStrictEqual(Array.from(result), [1, 2, 3, 4, 5]) + })) + + it.effect("branchAfter - applies the new stream once on remaining upstream", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.fromIterable(Chunk.range(1, 5)), + Stream.rechunk(2), + Stream.branchAfter(1, (chunk) => Stream.prepend(Stream.identity(), chunk)), + Stream.runCollect + ) + deepStrictEqual(Array.from(result), [1, 2, 3, 4, 5]) + })) + + it.effect("execute", () => + Effect.gen(function*() { + const ref = yield* (Ref.make(Chunk.empty())) + yield* (Stream.runDrain(Stream.execute(Ref.set(ref, Chunk.fromIterable([1]))))) + const result = yield* (Ref.get(ref)) + deepStrictEqual(Array.from(result), [1]) + })) + + it.effect("flatMap - deep flatMap stack safety", () => + Effect.gen(function*() { + const fib = (n: number): Stream.Stream => + n <= 1 ? + Stream.succeed(n) : + pipe( + fib(n - 1), + Stream.flatMap((a) => + pipe( + fib(n - 2), + Stream.flatMap((b) => Stream.succeed(a + b)) + ) + ) + ) + const result = yield* (Stream.runCollect(fib(10))) + deepStrictEqual(Array.from(result), [55]) + })) + + it.effect("flatMap - left identity", () => + Effect.gen(function*() { + const f = (n: number) => Stream.succeed(n * 2) + const { result1, result2 } = yield* (Effect.all({ + result1: pipe(Stream.make(1), Stream.flatMap(f), Stream.runCollect), + result2: Stream.runCollect(f(1)) + })) + deepStrictEqual(Array.from(result1), Array.from(result2)) + })) + + it.effect("flatMap - right identity", () => + Effect.gen(function*() { + const { result1, result2 } = yield* (Effect.all({ + result1: pipe(Stream.make(1), Stream.flatMap((n) => Stream.make(n)), Stream.runCollect), + result2: Stream.runCollect(Stream.make(1)) + })) + deepStrictEqual(Array.from(result1), Array.from(result2)) + })) + + it.effect("flatMap - associativity", () => + Effect.gen(function*() { + const stream = Stream.range(0, 4) + const f = (n: number) => Stream.succeed(n * 2) + const g = (n: number) => Stream.succeed(String(n)) + const { result1, result2 } = yield* (Effect.all({ + result1: pipe(stream, Stream.flatMap(f), Stream.flatMap(g), Stream.runCollect), + result2: pipe(stream, Stream.flatMap((n) => pipe(f(n), Stream.flatMap(g))), Stream.runCollect) + })) + deepStrictEqual(Array.from(result1), Array.from(result2)) + })) + + it.effect("flatMap - inner finalizers", () => + Effect.gen(function*() { + const ref = yield* (Ref.make(Chunk.empty())) + const push = (n: number) => Ref.update(ref, Chunk.append(n)) + const latch = yield* (Deferred.make()) + const fiber = yield* pipe( + Stream.make( + Stream.acquireRelease(push(1), () => push(1)), + Stream.fromEffect(push(2)), + pipe( + Stream.acquireRelease(push(3), () => push(3)), + Stream.crossRight(Stream.fromEffect(pipe( + Deferred.succeed(latch, void 0), + Effect.zipRight(Effect.never) + ))) + ) + ), + Stream.flatMap(identity), + Stream.runDrain, + Effect.fork + ) + yield* (Deferred.await(latch)) + yield* (Fiber.interrupt(fiber)) + const result = yield* (Ref.get(ref)) + deepStrictEqual(Array.from(result), [1, 1, 2, 3, 3]) + })) + + it.effect("flatMap - finalizer ordering #1", () => + Effect.gen(function*() { + const ref = yield* (Ref.make(Chunk.empty())) + const push = (message: string) => Ref.update(ref, Chunk.append(message)) + const chunks = Chunk.make(Chunk.of(void 0), Chunk.of(void 0)) + yield* pipe( + Stream.acquireRelease(push("open 1"), () => push("close 1")), + Stream.flatMap(() => + pipe( + Stream.fromChunks(...chunks), + Stream.tap(() => push("use 2")), + Stream.ensuring(push("close 2")), + Stream.flatMap(() => + pipe( + Stream.acquireRelease(push("open 3"), () => push("close 3")), + Stream.flatMap(() => + pipe( + Stream.fromChunks(...chunks), + Stream.tap(() => push("use 4")), + Stream.ensuring(push("close 4")) + ) + ) + ) + ) + ) + ), + Stream.runDrain + ) + const result = yield* (Ref.get(ref)) + deepStrictEqual(Array.from(result), [ + "open 1", + "use 2", + "open 3", + "use 4", + "use 4", + "close 4", + "close 3", + "use 2", + "open 3", + "use 4", + "use 4", + "close 4", + "close 3", + "close 2", + "close 1" + ]) + })) + + it.effect("flatMap - finalizer ordering #2", () => + Effect.gen(function*() { + const ref = yield* (Ref.make(Chunk.empty())) + const push = (message: string) => Ref.update(ref, Chunk.append(message)) + const chunks = Chunk.make(Chunk.of(1), Chunk.of(2)) + yield* pipe( + Stream.fromChunks(...chunks), + Stream.tap(() => push("use 1")), + Stream.flatMap(() => Stream.acquireRelease(push("open 2"), () => push("close 2"))), + Stream.runDrain + ) + const result = yield* (Ref.get(ref)) + deepStrictEqual(Array.from(result), [ + "use 1", + "open 2", + "close 2", + "use 1", + "open 2", + "close 2" + ]) + })) + + it.effect("flatMap - exit signal", () => + Effect.gen(function*() { + const ref = yield* (Ref.make(false)) + const inner = pipe( + Stream.acquireRelease(Effect.void, (_, exit) => + Exit.match(exit, { + onFailure: () => Ref.set(ref, true), + onSuccess: () => Effect.void + })), + Stream.flatMap(() => Stream.fail("Ouch")) + ) + yield* pipe( + Stream.succeed(void 0), + Stream.flatMap(() => inner), + Stream.runDrain, + Effect.either + ) + const result = yield* (Ref.get(ref)) + assertTrue(result) + })) + + it.effect("flatMap - finalizers are registered in the proper order", () => + Effect.gen(function*() { + const ref = yield* (Ref.make(Chunk.empty())) + const push = (n: number) => Ref.update(ref, Chunk.prepend(n)) + yield* pipe( + Stream.finalizer(push(1)), + Stream.flatMap(() => Stream.finalizer(push(2))), + Stream.toPull, + Effect.flatten, + Effect.scoped + ) + const result = yield* (Ref.get(ref)) + deepStrictEqual(Array.from(result), [1, 2]) + })) + + it.effect("flatMap - early release finalizer concatenation is preserved", () => + Effect.gen(function*() { + const ref = yield* (Ref.make(Chunk.empty())) + const push = (n: number) => Ref.update(ref, Chunk.prepend(n)) + const stream = pipe( + Stream.finalizer(push(1)), + Stream.flatMap(() => Stream.finalizer(push(2))) + ) + const result = yield* pipe( + Scope.make(), + Effect.flatMap((scope) => + pipe( + Scope.extend(scope)(Stream.toPull(stream)), + Effect.flatMap((pull) => + pipe( + pull, + Effect.zipRight(Scope.close(scope, Exit.void)), + Effect.zipRight(Ref.get(ref)) + ) + ) + ) + ) + ) + deepStrictEqual(Array.from(result), [1, 2]) + })) + + it.effect("flatMapPar - guarantee ordering", () => + Effect.gen(function*() { + const stream = Stream.fromIterable([1, 2, 3, 4, 5]) + const { result1, result2 } = yield* (Effect.all({ + result1: pipe(stream, Stream.flatMap((n) => Stream.make(n, n)), Stream.runCollect), + result2: pipe(stream, Stream.flatMap((n) => Stream.make(n, n), { concurrency: 2 }), Stream.runCollect) + })) + deepStrictEqual(Array.from(result1), Array.from(result2)) + })) + + it.effect("flatMapPar - consistency with flatMap", () => + Effect.gen(function*() { + const stream = Stream.fromIterable([1, 2, 3, 4, 5]) + const { result1, result2 } = yield* (Effect.all({ + result1: pipe( + stream, + Stream.flatMap((n) => Stream.make(n, n)), + Stream.runCollect + ), + result2: pipe( + stream, + Stream.flatMap((n) => Stream.make(n, n), { concurrency: 100 }), + Stream.runCollect + ) + })) + deepStrictEqual(Array.from(result1), Array.from(result2)) + })) + + it.effect("flatMapPar - interruption propagation", () => + Effect.gen(function*() { + const ref = yield* (Ref.make(false)) + const latch = yield* (Deferred.make()) + const fiber = yield* pipe( + Stream.make(void 0), + Stream.flatMap(() => + pipe( + Deferred.succeed(latch, void 0), + Effect.zipRight(Effect.never), + Effect.onInterrupt(() => Ref.set(ref, true)), + Stream.fromEffect + ), { concurrency: 2 }), + Stream.runDrain, + Effect.fork + ) + yield* (Deferred.await(latch)) + yield* (Fiber.interrupt(fiber)) + const result = yield* (Ref.get(ref)) + assertTrue(result) + })) + + it.effect("flatMap - inner errors interrupt all fibers", () => + Effect.gen(function*() { + const ref = yield* (Ref.make(false)) + const latch = yield* (Deferred.make()) + const result = yield* pipe( + Stream.make( + Stream.fromEffect( + pipe( + Deferred.succeed(latch, void 0), + Effect.zipRight(Effect.never), + Effect.onInterrupt(() => Ref.set(ref, true)) + ) + ), + Stream.fromEffect( + pipe( + Deferred.await(latch), + Effect.zipRight(Effect.fail("Ouch")) + ) + ) + ), + Stream.flatMap(identity, { concurrency: 2 }), + Stream.runDrain, + Effect.either + ) + const cancelled = yield* (Ref.get(ref)) + assertTrue(cancelled) + assertLeft(result, "Ouch") + })) + + it.effect("flatMapPar - outer errors interrupt all fiberrs", () => + Effect.gen(function*() { + const ref = yield* (Ref.make(false)) + const latch = yield* (Deferred.make()) + const result = yield* pipe( + Stream.make(void 0), + Stream.concat(Stream.fromEffect(pipe( + Deferred.await(latch), + Effect.zipRight(Effect.fail("Ouch")) + ))), + Stream.flatMap(() => + pipe( + Deferred.succeed(latch, void 0), + Effect.zipRight(Effect.never), + Effect.onInterrupt(() => Ref.set(ref, true)), + Stream.fromEffect + ), { concurrency: 2 }), + Stream.runDrain, + Effect.either + ) + const cancelled = yield* (Ref.get(ref)) + assertTrue(cancelled) + assertLeft(result, "Ouch") + })) + + it.effect("flatMapPar - inner defects interrupt all fibers", () => + Effect.gen(function*() { + const defect = new Cause.RuntimeException("Ouch") + const ref = yield* (Ref.make(false)) + const latch = yield* (Deferred.make()) + const result = yield* pipe( + Stream.make( + Stream.fromEffect(pipe( + Deferred.succeed(latch, void 0), + Effect.zipRight(Effect.never), + Effect.onInterrupt(() => Ref.set(ref, true)) + )), + Stream.fromEffect(pipe( + Deferred.await(latch), + Effect.zipRight(Effect.die(defect)) + )) + ), + Stream.flatMap(identity, { concurrency: 2 }), + Stream.runDrain, + Effect.exit + ) + const cancelled = yield* (Ref.get(ref)) + assertTrue(cancelled) + deepStrictEqual(result, Exit.die(defect)) + })) + + it.effect("flatMapPar - outer defects interrupt all fibers", () => + Effect.gen(function*() { + const defect = new Cause.RuntimeException("Ouch") + const ref = yield* (Ref.make(false)) + const latch = yield* (Deferred.make()) + const result = yield* pipe( + Stream.make(void 0), + Stream.concat(Stream.fromEffect(pipe( + Deferred.await(latch), + Effect.zipRight(Effect.die(defect)) + ))), + Stream.flatMap(() => + pipe( + Deferred.succeed(latch, void 0), + Effect.zipRight(Effect.never), + Effect.onInterrupt(() => Ref.set(ref, true)), + Stream.fromEffect + ), { concurrency: 2 }), + Stream.runDrain, + Effect.exit + ) + const cancelled = yield* (Ref.get(ref)) + assertTrue(cancelled) + deepStrictEqual(result, Exit.die(defect)) + })) + + it.effect("flatMapPar - finalizer ordering", () => + Effect.gen(function*() { + const ref = yield* (Ref.make(Chunk.empty())) + const push = (message: string) => Ref.update(ref, Chunk.append(message)) + const inner = Stream.acquireRelease(push("Inner Acquire"), () => push("Inner Release")) + yield* pipe( + Stream.acquireRelease( + pipe( + push("Outer Acquire"), + Effect.as(inner) + ), + () => push("Outer Release") + ), + Stream.flatMap(identity, { concurrency: 2 }), + Stream.runDrain + ) + const result = yield* (Ref.get(ref)) + deepStrictEqual(Array.from(result), [ + "Outer Acquire", + "Inner Acquire", + "Inner Release", + "Outer Release" + ]) + })) + + it.effect("flatMapParSwitch - guarantee ordering no parallelism", () => + Effect.gen(function*() { + const ref = yield* (Ref.make(false)) + const semaphore = yield* (Effect.makeSemaphore(1)) + yield* pipe( + Stream.make(1, 2, 3, 4), + Stream.flatMap((n) => { + if (n > 3) { + return pipe( + Stream.acquireRelease(Effect.void, () => Ref.set(ref, true)), + Stream.flatMap(() => Stream.empty) + ) + } + return pipe( + Stream.scoped(withPermitsScoped(1)(semaphore)), + Stream.flatMap(() => Stream.never) + ) + }, { concurrency: 1, switch: true }), + Stream.runDrain + ) + const result = yield* (semaphore.withPermits(1)(Ref.get(ref))) + assertTrue(result) + })) + + it.effect("flatMapParSwitch - guarantee ordering with parallelism", () => + Effect.gen(function*() { + const ref = yield* (Ref.make(0)) + const semaphore = yield* (Effect.makeSemaphore(4)) + yield* pipe( + Stream.range(1, 12), + Stream.flatMap((n) => { + if (n > 8) { + return pipe( + Stream.acquireRelease( + Effect.void, + () => Ref.update(ref, (n) => n + 1) + ), + Stream.flatMap(() => Stream.empty) + ) + } + return pipe( + Stream.scoped(withPermitsScoped(1)(semaphore)), + Stream.flatMap(() => Stream.never) + ) + }, { concurrency: 4, switch: true }), + Stream.runDrain + ) + const result = yield* pipe( + Ref.get(ref), + semaphore.withPermits(4) + ) + strictEqual(result, 4) + })) + + it.effect("flatMapParSwitch - short circuiting", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.make(Stream.never, Stream.make(1)), + Stream.flatMap(identity, { concurrency: 2, switch: true }), + Stream.take(1), + Stream.runCollect + ) + deepStrictEqual(Array.from(result), [1]) + })) + + it.effect("flatMapParSwitch - interruption propagation", () => + Effect.gen(function*() { + const ref = yield* (Ref.make(false)) + const latch = yield* (Deferred.make()) + const fiber = yield* pipe( + Stream.make(void 0), + Stream.flatMap(() => + pipe( + Deferred.succeed(latch, void 0), + Effect.zipRight(Effect.never), + Effect.onInterrupt(() => Ref.set(ref, true)), + Stream.fromEffect + ), { switch: true }), + Stream.runCollect, + Effect.fork + ) + yield* (Deferred.await(latch)) + yield* (Fiber.interrupt(fiber)) + const result = yield* (Ref.get(ref)) + assertTrue(result) + })) + + it.effect("flatMapParSwitch - inner errors interrupt all fibers", () => + Effect.gen(function*() { + const ref = yield* (Ref.make(false)) + const latch = yield* (Deferred.make()) + const result = yield* pipe( + Stream.make( + Stream.fromEffect( + pipe( + Deferred.succeed(latch, void 0), + Effect.zipRight(Effect.never), + Effect.onInterrupt(() => Ref.set(ref, true)) + ) + ), + Stream.fromEffect( + pipe( + Deferred.await(latch), + Effect.zipRight(Effect.fail("Ouch")) + ) + ) + ), + Stream.flatMap(identity, { concurrency: 2, switch: true }), + Stream.runDrain, + Effect.either + ) + const cancelled = yield* (Ref.get(ref)) + assertTrue(cancelled) + assertLeft(result, "Ouch") + })) + + it.effect("flatMapParSwitch - outer errors interrupt all fibers", () => + Effect.gen(function*() { + const ref = yield* (Ref.make(false)) + const latch = yield* (Deferred.make()) + const result = yield* pipe( + Stream.make(void 0), + Stream.concat(Stream.fromEffect(pipe( + Deferred.await(latch), + Effect.zipRight(Effect.fail("Ouch")) + ))), + Stream.flatMap(() => + pipe( + Deferred.succeed(latch, void 0), + Effect.zipRight(Effect.never), + Effect.onInterrupt(() => Ref.set(ref, true)), + Stream.fromEffect + ), { concurrency: 2, switch: true }), + Stream.runDrain, + Effect.either + ) + const cancelled = yield* (Ref.get(ref)) + assertTrue(cancelled) + assertLeft(result, "Ouch") + })) + + it.effect("flatMapParSwitch - inner defects interrupt all fibers", () => + Effect.gen(function*() { + const error = new Cause.RuntimeException("Ouch") + const ref = yield* (Ref.make(false)) + const latch = yield* (Deferred.make()) + const result = yield* pipe( + Stream.make( + Stream.fromEffect( + pipe( + Deferred.succeed(latch, void 0), + Effect.zipRight(Effect.never), + Effect.onInterrupt(() => Ref.set(ref, true)) + ) + ), + Stream.fromEffect( + pipe( + Deferred.await(latch), + Effect.zipRight(Effect.die(error)) + ) + ) + ), + Stream.flatMap(identity, { concurrency: 2, switch: true }), + Stream.runDrain, + Effect.exit + ) + const cancelled = yield* (Ref.get(ref)) + assertTrue(cancelled) + deepStrictEqual(result, Exit.die(error)) + })) + + it.effect("flatMapParSwitch - outer defects interrupt all fibers", () => + Effect.gen(function*() { + const error = new Cause.RuntimeException("Ouch") + const ref = yield* (Ref.make(false)) + const latch = yield* (Deferred.make()) + const result = yield* pipe( + Stream.make(void 0), + Stream.concat(Stream.fromEffect(pipe( + Deferred.await(latch), + Effect.zipRight(Effect.die(error)) + ))), + Stream.flatMap(() => + pipe( + Deferred.succeed(latch, void 0), + Effect.zipRight(Effect.never), + Effect.onInterrupt(() => Ref.set(ref, true)), + Stream.fromEffect + ), { concurrency: 2, switch: true }), + Stream.runDrain, + Effect.exit + ) + const cancelled = yield* (Ref.get(ref)) + assertTrue(cancelled) + deepStrictEqual(result, Exit.die(error)) + })) + + it.effect("flatMapParSwitch - finalizer ordering", () => + Effect.gen(function*() { + const ref = yield* (Ref.make(Chunk.empty())) + const push = (message: string) => Ref.update(ref, Chunk.append(message)) + const inner = Stream.acquireRelease(push("Inner Acquire"), () => push("Inner Release")) + yield* pipe( + Stream.acquireRelease( + pipe( + push("Outer Acquire"), + Effect.as(inner) + ), + () => push("Outer Release") + ), + Stream.flatMap(identity, { concurrency: 2, switch: true }), + Stream.runDrain + ) + const result = yield* (Ref.get(ref)) + deepStrictEqual(Array.from(result), [ + "Outer Acquire", + "Inner Acquire", + "Inner Release", + "Outer Release" + ]) + })) + + it.effect("flattenChunks", () => + Effect.gen(function*() { + const chunks = Chunk.make(Chunk.make(1, 2), Chunk.make(3, 4), Chunk.make(5, 6)) + const result = yield* pipe( + Stream.fromChunks(chunks), + Stream.flattenChunks, + Stream.chunks, + Stream.runCollect + ) + deepStrictEqual( + Array.from(result).map((chunk) => Array.from(chunk)), + Array.from(chunks).map((chunk) => Array.from(chunk)) + ) + })) + + it.effect("flattenExitOption - happy path", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.range(0, 9), + Stream.toQueue({ capacity: 1 }), + Effect.flatMap((queue) => + pipe( + Stream.fromQueue(queue), + Stream.map((take) => take.exit), + Stream.flattenExitOption, + Stream.runCollect + ) + ), + Effect.scoped + ) + deepStrictEqual( + Array.from(Chunk.flatten(result)), + Array.from(Chunk.range(0, 9)) + ) + })) + + it.effect("flattenExitOption - failure", () => + Effect.gen(function*() { + const error = new Cause.RuntimeException("boom") + const result = yield* pipe( + Stream.range(0, 9), + Stream.concat(Stream.fail(error)), + Stream.toQueue({ capacity: 1 }), + Effect.flatMap((queue) => + pipe( + Stream.fromQueue(queue), + Stream.map((take) => take.exit), + Stream.flattenExitOption, + Stream.runCollect + ) + ), + Effect.scoped, + Effect.exit + ) + deepStrictEqual(result, Exit.fail(error)) + })) + + it.effect("flattenIterables", () => + Effect.gen(function*() { + const iterables = [[1, 2, 3], [4, 5, 6], [7, 8, 9]] + const result = yield* pipe( + Stream.fromIterable(iterables), + Stream.flattenIterables, + Stream.runCollect + ) + deepStrictEqual(Array.from(result), iterables.flatMap(identity)) + })) + + it.effect("flattenTake - happy path", () => + Effect.gen(function*() { + const chunks = Chunk.make(Chunk.range(0, 3), Chunk.range(4, 8)) + const result = yield* pipe( + Stream.fromChunks(...chunks), + Stream.mapChunks((chunk) => Chunk.of(Take.chunk(chunk))), + Stream.flattenTake, + Stream.runCollect + ) + deepStrictEqual(Array.from(result), Array.from(Chunk.flatten(chunks))) + })) + + it.effect("flattenTake - stop collecting on Exit.Failure", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.make( + Take.chunk(Chunk.make(1, 2)), + Take.of(3), + Take.end + ), + Stream.flattenTake, + Stream.runCollect + ) + deepStrictEqual(Array.from(result), [1, 2, 3]) + })) + + it.effect("flattenTake - works with empty chunks", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.make( + Take.chunk(Chunk.empty()), + Take.chunk(Chunk.empty()) + ), + Stream.flattenTake, + Stream.runCollect + ) + assertTrue(Chunk.isEmpty(result)) + })) + + it.effect("flattenTake - works with empty streams", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.fromIterable>([]), + Stream.flattenTake, + Stream.runCollect + ) + assertTrue(Chunk.isEmpty(result)) + })) +}) diff --git a/repos/effect/packages/effect/test/Stream/sliding.test.ts b/repos/effect/packages/effect/test/Stream/sliding.test.ts new file mode 100644 index 0000000..758c94e --- /dev/null +++ b/repos/effect/packages/effect/test/Stream/sliding.test.ts @@ -0,0 +1,138 @@ +import { describe, it } from "@effect/vitest" +import { assertLeft, deepStrictEqual } from "@effect/vitest/utils" +import * as Chunk from "effect/Chunk" +import * as Effect from "effect/Effect" +import { pipe } from "effect/Function" +import * as Ref from "effect/Ref" +import * as Stream from "effect/Stream" + +describe("Stream", () => { + it.effect("sliding - returns a sliding window", () => + Effect.gen(function*() { + const stream0 = Stream.fromChunks( + Chunk.empty(), + Chunk.make(1), + Chunk.empty(), + Chunk.make(2, 3, 4, 5) + ) + const stream1 = pipe( + Stream.empty, + Stream.concat(Stream.make(1)), + Stream.concat(Stream.empty), + Stream.concat(Stream.make(2)), + Stream.concat(Stream.make(3, 4, 5)) + ) + const stream2 = pipe( + Stream.make(1), + Stream.concat(Stream.empty), + Stream.concat(Stream.make(2)), + Stream.concat(Stream.empty), + Stream.concat(Stream.make(3, 4, 5)) + ) + const stream3 = pipe( + Stream.fromChunk(Chunk.make(1)), + Stream.concat(Stream.fromChunk(Chunk.make(2))), + Stream.concat(Stream.make(3, 4, 5)) + ) + const result1 = yield* pipe( + Stream.make(1, 2, 3, 4, 5), + Stream.sliding(2), + Stream.runCollect + ) + const result2 = yield* pipe( + stream0, + Stream.sliding(2), + Stream.runCollect + ) + const result3 = yield* pipe( + stream1, + Stream.sliding(2), + Stream.runCollect + ) + const result4 = yield* pipe( + stream2, + Stream.sliding(2), + Stream.runCollect + ) + const result5 = yield* pipe( + stream3, + Stream.sliding(2), + Stream.runCollect + ) + const expected = [[1, 2], [2, 3], [3, 4], [4, 5]] + deepStrictEqual(Array.from(result1).map((chunk) => Array.from(chunk)), expected) + deepStrictEqual(Array.from(result2).map((chunk) => Array.from(chunk)), expected) + deepStrictEqual(Array.from(result3).map((chunk) => Array.from(chunk)), expected) + deepStrictEqual(Array.from(result4).map((chunk) => Array.from(chunk)), expected) + deepStrictEqual(Array.from(result5).map((chunk) => Array.from(chunk)), expected) + })) + + it.effect("sliding - returns all elements if chunkSize is greater than the size of the stream", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.range(1, 5), + Stream.sliding(6), + Stream.runCollect + ) + deepStrictEqual(Array.from(result).map((chunk) => Array.from(chunk)), [[1, 2, 3, 4, 5]]) + })) + + it.effect("sliding - is mostly equivalent to ZStream#grouped when stepSize and chunkSize are equal", () => + Effect.gen(function*() { + const stream = Stream.range(1, 5) + const { result1, result2 } = yield* (Effect.all({ + result1: pipe(stream, Stream.slidingSize(3, 3), Stream.runCollect), + result2: pipe(stream, Stream.grouped(3), Stream.runCollect) + })) + deepStrictEqual( + Array.from(result1).map((chunk) => Array.from(chunk)), + Array.from(result2).map((chunk) => Array.from(chunk)) + ) + })) + + it.effect("sliding - fails if upstream produces an error", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.make(1, 2, 3), + Stream.concat(Stream.fail("Ouch")), + Stream.concat(Stream.make(4, 5)), + Stream.sliding(2), + Stream.runCollect, + Effect.either + ) + assertLeft(result, "Ouch") + })) + + it.effect("sliding - should return an empty chunk when the stream is empty", () => + Effect.gen(function*() { + const result = yield* pipe(Stream.empty, Stream.sliding(2), Stream.runCollect) + deepStrictEqual(Array.from(result), []) + })) + + it.effect("sliding - emits elements properly when a failure occurs", () => + Effect.gen(function*() { + const ref = yield* (Ref.make(Chunk.empty>())) + const streamChunks = Stream.fromChunks( + Chunk.range(1, 4), + Chunk.range(5, 7), + Chunk.of(8) + ) + const stream = pipe( + streamChunks, + Stream.concat(Stream.fail("Ouch")), + Stream.slidingSize(3, 3) + ) + const either = yield* pipe( + stream, + Stream.mapEffect((chunk) => Ref.update(ref, Chunk.append(chunk))), + Stream.runCollect, + Effect.either + ) + const result = yield* (Ref.get(ref)) + assertLeft(either, "Ouch") + deepStrictEqual( + Array.from(result).map((chunk) => Array.from(chunk)), + [[1, 2, 3], [4, 5, 6], [7, 8]] + ) + })) +}) diff --git a/repos/effect/packages/effect/test/Stream/splitting.test.ts b/repos/effect/packages/effect/test/Stream/splitting.test.ts new file mode 100644 index 0000000..1eb1125 --- /dev/null +++ b/repos/effect/packages/effect/test/Stream/splitting.test.ts @@ -0,0 +1,292 @@ +import { describe, it } from "@effect/vitest" +import { deepStrictEqual, strictEqual } from "@effect/vitest/utils" +import * as Chunk from "effect/Chunk" +import * as Effect from "effect/Effect" +import * as fc from "effect/FastCheck" +import { pipe } from "effect/Function" +import * as Stream from "effect/Stream" + +const weirdStringForSplitLines: fc.Arbitrary> = fc.array( + fc.string().filter((s) => s !== "\n" && s !== "\r") +).map((strings) => { + if (strings.length > 0 && strings[strings.length - 1] === "") { + return [...strings, "a"] + } + return strings +}) + +const testSplitLines = ( + input: ReadonlyArray> +): Effect.Effect, ReadonlyArray]> => { + const str = input.flatMap((chunk) => Chunk.toReadonlyArray(chunk).join("")).join("") + const expected = str.split(/\r?\n/) + return pipe( + Stream.fromChunks(...input), + Stream.splitLines, + Stream.runCollect, + Effect.map((chunk) => [expected, Chunk.toReadonlyArray(chunk)] as const) + ) +} + +describe("Stream", () => { + it.effect("split - should split properly", () => + Effect.gen(function*() { + const chunks = Chunk.make( + Chunk.range(1, 2), + Chunk.range(3, 4), + Chunk.range(5, 6), + Chunk.make(7, 8, 9), + Chunk.of(10) + ) + const { result1, result2 } = yield* (Effect.all({ + result1: pipe( + Stream.range(0, 9), + Stream.split((n) => n % 4 === 0), + Stream.runCollect + ), + result2: pipe( + Stream.fromChunks(...chunks), + Stream.split((n) => n % 3 === 0), + Stream.runCollect + ) + })) + deepStrictEqual( + Array.from(result1).map((chunk) => Array.from(chunk)), + [[1, 2, 3], [5, 6, 7], [9]] + ) + deepStrictEqual( + Array.from(result2).map((chunk) => Array.from(chunk)), + [[1, 2], [4, 5], [7, 8], [10]] + ) + })) + + it.effect("split - is equivalent to identity when the predicate is not satisfied", () => + Effect.gen(function*() { + const stream = Stream.range(1, 10) + const { result1, result2 } = yield* (Effect.all({ + result1: pipe(stream, Stream.split((n) => n % 11 === 0), Stream.runCollect), + result2: pipe( + Stream.runCollect(stream), + Effect.map((chunk) => pipe(Chunk.of(chunk), Chunk.filter(Chunk.isNonEmpty))) + ) + })) + deepStrictEqual( + Array.from(result1).map((chunk) => Array.from(chunk)), + [Array.from(Chunk.range(1, 10))] + ) + deepStrictEqual( + Array.from(result1).map((chunk) => Array.from(chunk)), + Array.from(result2).map((chunk) => Array.from(chunk)) + ) + })) + + it.effect("split - should output empty chunk when stream is empty", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.empty, + Stream.split((n: number) => n % 11 === 0), + Stream.runCollect + ) + deepStrictEqual(Array.from(result), []) + })) + + it.effect("splitOnChunk - consecutive delimiter yields empty Chunk", () => + Effect.gen(function*() { + const input = Stream.make( + Chunk.make(1, 2), + Chunk.of(1), + Chunk.make(2, 1, 2, 3, 1, 2), + Chunk.make(1, 2) + ) + const splitSequence = Chunk.make(1, 2) + const result = yield* pipe( + Stream.flattenChunks(input), + Stream.splitOnChunk(splitSequence), + Stream.map(Chunk.size), + Stream.runCollect + ) + deepStrictEqual(Array.from(result), [0, 0, 0, 1, 0]) + })) + + it.effect("splitOnChunk - preserves data", () => + Effect.gen(function*() { + const splitSequence = Chunk.make(0, 1) + const stream = Stream.make(1, 1, 1, 1, 1, 1) + const result = yield* pipe( + stream, + Stream.splitOnChunk(splitSequence), + Stream.runCollect, + Effect.map(Chunk.flatten) + ) + deepStrictEqual(Array.from(result), [1, 1, 1, 1, 1, 1]) + })) + + it.effect("splitOnChunk - handles leftovers", () => + Effect.gen(function*() { + const splitSequence = Chunk.make(0, 1) + const result = yield* pipe( + Stream.fromChunks(Chunk.make(1, 0, 2, 0, 1, 2), Chunk.of(2)), + Stream.splitOnChunk(splitSequence), + Stream.runCollect + ) + deepStrictEqual( + Array.from(result).map((chunk) => Array.from(chunk)), + [[1, 0, 2], [2, 2]] + ) + })) + + it.effect("splitOnChunk - works", () => + Effect.gen(function*() { + const splitSequence = Chunk.make(0, 1) + const result = yield* pipe( + Stream.make(1, 2, 0, 1, 3, 4, 0, 1, 5, 6, 5, 6), + Stream.splitOnChunk(splitSequence), + Stream.runCollect + ) + deepStrictEqual( + Array.from(result).map((chunk) => Array.from(chunk)), + [[1, 2], [3, 4], [5, 6, 5, 6]] + ) + })) + + it.effect("splitOnChunk - works from Chunks", () => + Effect.gen(function*() { + const splitSequence = Chunk.make(0, 1) + const result = yield* pipe( + Stream.fromChunks( + Chunk.make(1, 2), + splitSequence, + Chunk.make(3, 4), + splitSequence, + Chunk.make(5, 6), + Chunk.make(5, 6) + ), + Stream.splitOnChunk(splitSequence), + Stream.runCollect + ) + deepStrictEqual( + Array.from(result).map((chunk) => Array.from(chunk)), + [[1, 2], [3, 4], [5, 6, 5, 6]] + ) + })) + + it.effect("splitOnChunk - single delimiter edge case", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.make(0), + Stream.splitOnChunk(Chunk.make(0)), + Stream.runCollect + ) + deepStrictEqual( + Array.from(result).map((chunk) => Array.from(chunk)), + [[]] + ) + })) + + it.effect("splitOnChunk - no delimiter in data", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.fromChunks(Chunk.make(1, 2), Chunk.make(1, 2), Chunk.make(1, 2)), + Stream.splitOnChunk(Chunk.make(1, 1)), + Stream.runCollect + ) + deepStrictEqual( + Array.from(result).map((chunk) => Array.from(chunk)), + [[1, 2, 1, 2, 1, 2]] + ) + })) + + it.effect("splitOnChunk - delimiter on the boundary", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.fromChunks(Chunk.make(1, 2), Chunk.make(1, 2)), + Stream.splitOnChunk(Chunk.make(2, 1)), + Stream.runCollect + ) + deepStrictEqual( + Array.from(result).map((chunk) => Array.from(chunk)), + [[1], [2]] + ) + })) + + it("splitLines - preserves data", () => + fc.assert(fc.asyncProperty(weirdStringForSplitLines, async (lines) => { + const data = lines.join("\n") + const program = pipe( + Stream.fromChunk(Chunk.of(data)), + Stream.splitLines, + Stream.runCollect, + Effect.map((chunk) => Chunk.toReadonlyArray(chunk).join("\n")) + ) + const result = await Effect.runPromise(program) + strictEqual(result, data) + }))) + + // it("splitLines - preserves data in chunks", () => + // fc.asyncProperty(fc.property(weirdStringForSplitLines), async (lines) => { + // const data = + // }) + // ) + // // test("preserves data in chunks") { + // // check(weirdStringGenForSplitLines) { xs => + // // val data = Chunk.fromIterable(xs.sliding(2, 2).toList.map(_.mkString("\n"))) + // // testSplitLines(Seq(data)) + // // } + // // }, + + it.effect("splitLines - handles leftovers", () => + Effect.gen(function*() { + const chunks = [Chunk.of("abc\nbc")] + const [expected, result] = yield* (testSplitLines(chunks)) + deepStrictEqual(expected, result) + })) + + it.effect("splitLines - handles leftovers 2", () => + Effect.gen(function*() { + const chunks = [ + Chunk.make("aa", "bb"), + Chunk.make("\nbbc\n", "ddb", "bd"), + Chunk.make("abc", "\n"), + Chunk.of("abc") + ] + const [expected, result] = yield* (testSplitLines(chunks)) + deepStrictEqual(expected, result) + })) + + it.effect("splitLines - aggregates chunks", () => + Effect.gen(function*() { + const chunks = [Chunk.make("abc", "\n", "bc", "\n", "bcd", "bcd")] + const [expected, result] = yield* (testSplitLines(chunks)) + deepStrictEqual(expected, result) + })) + + it.effect("splitLines - single newline edge case", () => + Effect.gen(function*() { + const chunks = [Chunk.of("\n")] + const [, result] = yield* (testSplitLines(chunks)) + // JavaScript arrays split `"\n"` into `["", ""]`, so we manually assert + // that the output should be the empty string here + deepStrictEqual([""], result) + })) + + it.effect("splitLines - no newlines", () => + Effect.gen(function*() { + const chunks = [Chunk.make("abc", "abc", "abc")] + const [expected, result] = yield* (testSplitLines(chunks)) + deepStrictEqual(expected, result) + })) + + it.effect("splitLines - \\r\\n on the boundary", () => + Effect.gen(function*() { + const chunks = [Chunk.make("abc\r", "\nabc")] + const [expected, result] = yield* (testSplitLines(chunks)) + deepStrictEqual(expected, result) + })) + + it.effect("splitLines - ZIO issue #6360", () => + Effect.gen(function*() { + const chunks = [Chunk.make("AAAAABBBB#\r\r\r\n", "test")] + const [_, result] = yield* (testSplitLines(chunks)) + deepStrictEqual(["AAAAABBBB#\r\r", "test"], result) + })) +}) diff --git a/repos/effect/packages/effect/test/Stream/streamable.test.ts b/repos/effect/packages/effect/test/Stream/streamable.test.ts new file mode 100644 index 0000000..6b00512 --- /dev/null +++ b/repos/effect/packages/effect/test/Stream/streamable.test.ts @@ -0,0 +1,24 @@ +import { describe, it } from "@effect/vitest" +import { deepStrictEqual } from "@effect/vitest/utils" +import * as Effect from "effect/Effect" +import * as Stream from "effect/Stream" +import * as Streamable from "effect/Streamable" + +describe("Streamable", () => { + it.effect( + "allows creating custom Stream types", + () => + Effect.gen(function*() { + class MyStream extends Streamable.Class { + toStream() { + return Stream.fromIterable([1, 2, 3]) + } + } + const stream = new MyStream() + + const values = Array.from(yield* Stream.runCollect(stream)) + + deepStrictEqual(values, [1, 2, 3]) + }) + ) +}) diff --git a/repos/effect/packages/effect/test/Stream/taking.test.ts b/repos/effect/packages/effect/test/Stream/taking.test.ts new file mode 100644 index 0000000..69ee1b8 --- /dev/null +++ b/repos/effect/packages/effect/test/Stream/taking.test.ts @@ -0,0 +1,163 @@ +import { describe, it } from "@effect/vitest" +import { assertFalse, deepStrictEqual } from "@effect/vitest/utils" +import * as Chunk from "effect/Chunk" +import * as Effect from "effect/Effect" +import * as Either from "effect/Either" +import { constFalse, pipe } from "effect/Function" +import * as Ref from "effect/Ref" +import * as Stream from "effect/Stream" + +describe("Stream", () => { + it.effect("take", () => + Effect.gen(function*() { + const take = 3 + const stream = Stream.range(1, 5) + const { result1, result2 } = yield* (Effect.all({ + result1: pipe(stream, Stream.take(take), Stream.runCollect), + result2: pipe(Stream.runCollect(stream), Effect.map(Chunk.take(take))) + })) + deepStrictEqual(Array.from(result1), Array.from(result2)) + })) + + it.effect("take - short circuits", () => + Effect.gen(function*() { + const ref = yield* (Ref.make(false)) + const stream = pipe( + Stream.make(1), + Stream.concat(Stream.drain(Stream.fromEffect(Ref.set(ref, true)))), + Stream.take(0) + ) + yield* (Stream.runDrain(stream)) + const result = yield* (Ref.get(ref)) + assertFalse(result) + })) + + it.effect("take - taking 0 short circuits", () => + Effect.gen(function*() { + const result = yield* pipe(Stream.never, Stream.take(0), Stream.runCollect) + deepStrictEqual(Array.from(result), []) + })) + + it.effect("take - taking 1 short circuits", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.make(1), + Stream.concat(Stream.never), + Stream.take(1), + Stream.runCollect + ) + deepStrictEqual(Array.from(result), [1]) + })) + + it.effect("takeRight", () => + Effect.gen(function*() { + const take = 3 + const stream = Stream.range(1, 5) + const { result1, result2 } = yield* (Effect.all({ + result1: pipe(stream, Stream.takeRight(take), Stream.runCollect), + result2: pipe(Stream.runCollect(stream), Effect.map(Chunk.takeRight(take))) + })) + deepStrictEqual(Array.from(result1), Array.from(result2)) + })) + + it.effect("takeUntil", () => + Effect.gen(function*() { + const stream = Stream.range(1, 5) + const f = (n: number) => n % 3 === 0 + const { result1, result2 } = yield* (Effect.all({ + result1: pipe(stream, Stream.takeUntil(f), Stream.runCollect), + result2: pipe( + Stream.runCollect(stream), + Effect.map((chunk) => + pipe( + chunk, + Chunk.takeWhile((a) => !f(a)), + Chunk.appendAll(pipe(chunk, Chunk.dropWhile((a) => !f(a)), Chunk.take(1))) + ) + ) + ) + })) + deepStrictEqual(Array.from(result1), Array.from(result2)) + })) + + it.effect("takeUntilEffect", () => + Effect.gen(function*() { + const stream = Stream.range(1, 5) + const f = (n: number) => Effect.succeed(n % 3 === 0) + const { result1, result2 } = yield* (Effect.all({ + result1: pipe(stream, Stream.takeUntilEffect(f), Stream.runCollect), + result2: pipe( + Stream.runCollect(stream), + Effect.flatMap((chunk) => + pipe( + chunk, + Effect.takeWhile((a) => Effect.negate(f(a))), + Effect.map(Chunk.unsafeFromArray), + Effect.zipWith( + pipe( + chunk, + Effect.dropWhile((a) => Effect.negate(f(a))), + Effect.map(Chunk.unsafeFromArray), + Effect.map(Chunk.take(1)) + ), + (chunk1, chunk2) => Chunk.appendAll(chunk1, chunk2) + ) + ) + ) + ) + })) + deepStrictEqual(Array.from(result1), Array.from(result2)) + })) + + it.effect("takeUntilEffect - laziness on chunks", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.make(1, 2, 3), + Stream.takeUntilEffect((n) => + n === 2 ? + Effect.fail("boom") : + Effect.succeed(false) + ), + Stream.either, + Stream.runCollect + ) + deepStrictEqual(Array.from(result), [Either.right(1), Either.left("boom")]) + })) + + it.effect("takeWhile", () => + Effect.gen(function*() { + const stream = Stream.range(1, 5) + const f = (n: number) => n <= 3 + const { result1, result2 } = yield* (Effect.all({ + result1: pipe(stream, Stream.takeWhile(f), Stream.runCollect), + result2: pipe(Stream.runCollect(stream), Effect.map(Chunk.takeWhile(f))) + })) + deepStrictEqual(Array.from(result1), Array.from(result2)) + })) + + it.effect("takeWhile - does not stop when hitting an empty chunk (ZIO #4272)", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.fromChunks(Chunk.of(1), Chunk.of(2), Chunk.of(3)), + Stream.mapChunks(Chunk.flatMap((n) => + n === 2 ? + Chunk.empty() : + Chunk.of(n) + )), + Stream.takeWhile((n) => n !== 4), + Stream.runCollect + ) + deepStrictEqual(Array.from(result), [1, 3]) + })) + + it.effect("takeWhile - short circuits", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.make(1), + Stream.concat(Stream.fail("Ouch")), + Stream.takeWhile(constFalse), + Stream.runCollect + ) + deepStrictEqual(Array.from(result), []) + })) +}) diff --git a/repos/effect/packages/effect/test/Stream/tapping.test.ts b/repos/effect/packages/effect/test/Stream/tapping.test.ts new file mode 100644 index 0000000..45a9746 --- /dev/null +++ b/repos/effect/packages/effect/test/Stream/tapping.test.ts @@ -0,0 +1,191 @@ +import { describe, it } from "@effect/vitest" +import { assertLeft, deepStrictEqual, strictEqual } from "@effect/vitest/utils" +import * as Chunk from "effect/Chunk" +import * as Effect from "effect/Effect" +import * as Either from "effect/Either" +import { pipe } from "effect/Function" +import * as Ref from "effect/Ref" +import * as Sink from "effect/Sink" +import * as Stream from "effect/Stream" + +describe("Stream", () => { + it.effect("tap", () => + Effect.gen(function*() { + const ref = yield* (Ref.make(0)) + const result = yield* pipe( + Stream.make(1, 1), + Stream.tap((i) => Ref.update(ref, (n) => i + n)), + Stream.runCollect + ) + const sum = yield* (Ref.get(ref)) + strictEqual(sum, 2) + deepStrictEqual(Array.from(result), [1, 1]) + })) + + it.effect("tap - laziness on chunks", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.make(1, 2, 3), + Stream.tap((n) => pipe(Effect.fail("error"), Effect.when(() => n === 3))), + Stream.either, + Stream.runCollect + ) + deepStrictEqual(Array.from(result), [ + Either.right(1), + Either.right(2), + Either.left("error") + ]) + })) + + it.effect("tapBoth - just tap values", () => + Effect.gen(function*() { + const ref = yield* (Ref.make(0)) + const values = Chunk.make(1, 1) + const result = yield* pipe( + Stream.fromChunk(values), + Stream.tapBoth({ + onSuccess: (v) => Ref.update(ref, (_) => _ + v), + onFailure: (e) => Effect.die(`Unexpected attempt to tap an error ${e}`) + }), + Stream.runCollect + ) + + deepStrictEqual(Array.from(result), Array.from(values)) + strictEqual(yield* (Ref.get(ref)), 2) + })) + + it.effect("tapBoth - just tap an error", () => + Effect.gen(function*() { + const ref = yield* (Ref.make("")) + const result = yield* pipe( + Stream.fail("Ouch"), + Stream.tapBoth({ + onSuccess: (v) => Effect.die(`Unexpected attempt to tap a value ${v}`), + onFailure: (e) => Ref.update(ref, (_) => _ + e) + }), + Stream.runCollect, + Effect.either + ) + + assertLeft(result, "Ouch") + strictEqual(yield* (Ref.get(ref)), "Ouch") + })) + + it.effect("tapBoth - tap values and then error", () => + Effect.gen(function*() { + const error = yield* (Ref.make("")) + const sum = yield* (Ref.make(0)) + const values = Chunk.make(1, 1) + const result = yield* pipe( + Stream.fromChunk(values), + Stream.concat(Stream.fail("Ouch")), + Stream.tapBoth({ + onSuccess: (v) => Ref.update(sum, (_) => _ + v), + onFailure: (e) => Ref.update(error, (_) => _ + e) + }), + Stream.runCollect, + Effect.either + ) + + assertLeft(result, "Ouch") + strictEqual(yield* (Ref.get(error)), "Ouch") + strictEqual(yield* (Ref.get(sum)), 2) + })) + + it.effect("tapBoth - tap chunks lazily", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.make(1, 2, 3), + Stream.tapBoth({ + onSuccess: (n) => + pipe( + Effect.fail("error"), + Effect.when(() => n === 3) + ), + onFailure: () => Effect.void + }), + Stream.either, + Stream.runCollect + ) + + deepStrictEqual(Array.from(result), [ + Either.right(1), + Either.right(2), + Either.left("error") + ]) + })) + + it.effect("tapError", () => + Effect.gen(function*() { + const ref = yield* (Ref.make("")) + const result = yield* pipe( + Stream.make(1, 1), + Stream.concat(Stream.fail("Ouch")), + Stream.tapError((e) => Ref.update(ref, (s) => s + e)), + Stream.runCollect, + Effect.either + ) + assertLeft(result, "Ouch") + })) + + it.effect("tapSink - sink that is done after stream", () => + Effect.gen(function*() { + const ref = yield* (Ref.make(0)) + const sink = Sink.forEach((i: number) => Ref.update(ref, (n) => i + n)) + const result = yield* pipe( + Stream.make(1, 1, 2, 3, 5, 8), + Stream.tapSink(sink), + Stream.runCollect + ) + const sum = yield* (Ref.get(ref)) + strictEqual(sum, 20) + deepStrictEqual(Array.from(result), [1, 1, 2, 3, 5, 8]) + })) + + it.effect("tapSink - sink that is done before stream", () => + Effect.gen(function*() { + const ref = yield* (Ref.make(0)) + const sink = pipe( + Sink.take(3), + Sink.map(Chunk.reduce(0, (x, y) => x + y)), + Sink.mapEffect((i) => Ref.update(ref, (n) => n + i)) + ) + const result = yield* pipe( + Stream.make(1, 1, 2, 3, 5, 8), + Stream.rechunk(1), + Stream.tapSink(sink), + Stream.runCollect + ) + const sum = yield* (Ref.get(ref)) + strictEqual(sum, 4) + deepStrictEqual(Array.from(result), [1, 1, 2, 3, 5, 8]) + })) + + it.effect("tapSink - sink that fails before stream", () => + Effect.gen(function*() { + const sink = Sink.fail("error") + const result = yield* pipe( + Stream.never, + Stream.tapSink(sink), + Stream.runCollect, + Effect.flip + ) + strictEqual(result, "error") + })) + + it.effect("tapSink - does not read ahead", () => + Effect.gen(function*() { + const ref = yield* (Ref.make(0)) + const sink = Sink.forEach((i: number) => Ref.update(ref, (n) => i + n)) + yield* pipe( + Stream.make(1, 2, 3, 4, 5), + Stream.rechunk(1), + Stream.forever, + Stream.tapSink(sink), + Stream.take(3), + Stream.runDrain + ) + const result = yield* (Ref.get(ref)) + strictEqual(result, 6) + })) +}) diff --git a/repos/effect/packages/effect/test/Stream/throttling.test.ts b/repos/effect/packages/effect/test/Stream/throttling.test.ts new file mode 100644 index 0000000..64fe513 --- /dev/null +++ b/repos/effect/packages/effect/test/Stream/throttling.test.ts @@ -0,0 +1,353 @@ +import { describe, it } from "@effect/vitest" +import { assertFailure, assertTrue, deepStrictEqual } from "@effect/vitest/utils" +import * as Cause from "effect/Cause" +import * as Chunk from "effect/Chunk" +import * as Clock from "effect/Clock" +import * as Duration from "effect/Duration" +import * as Effect from "effect/Effect" +import * as Exit from "effect/Exit" +import * as Fiber from "effect/Fiber" +import { pipe } from "effect/Function" +import * as Option from "effect/Option" +import * as Queue from "effect/Queue" +import * as Ref from "effect/Ref" +import * as Schedule from "effect/Schedule" +import * as Stream from "effect/Stream" +import * as TestClock from "effect/TestClock" +import { chunkCoordination } from "../utils/coordination.js" + +describe("Stream", () => { + it.effect("throttleEnforce - free elements", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.make(1, 2, 3, 4), + Stream.throttle({ cost: () => 0, units: 0, duration: Duration.infinity, strategy: "enforce" }), + Stream.runCollect + ) + deepStrictEqual(Array.from(result), [1, 2, 3, 4]) + })) + + it.effect("throttleEnforce - no bandwidth", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.make(1, 2, 3, 4), + Stream.throttle({ cost: () => 1, units: 0, duration: Duration.infinity, strategy: "enforce" }), + Stream.runCollect + ) + assertTrue(Chunk.isEmpty(result)) + })) + + it.effect("throttleEnforce - refill bucket tokens", () => + Effect.gen(function*() { + const fiber = yield* pipe( + Stream.fromSchedule(Schedule.spaced(Duration.millis(100))), + Stream.take(10), + Stream.throttle({ cost: () => 1, units: 1, duration: Duration.millis(200), strategy: "enforce" }), + Stream.runCollect, + Effect.fork + ) + yield* TestClock.adjust(Duration.seconds(1)) + const result = yield* Fiber.join(fiber) + deepStrictEqual(Array.from(result), [0, 2, 4, 6, 8]) + })) + + it.effect("throttleShape", () => + Effect.gen(function*() { + const queue = yield* (Queue.bounded(10)) + const fiber = yield* pipe( + Stream.fromQueue(queue), + Stream.throttle({ + strategy: "shape", + cost: Chunk.reduce(0, (x, y) => x + y), + units: 1, + duration: Duration.seconds(1) + }), + Stream.toPull, + Effect.flatMap((pull) => + Effect.gen(function*() { + yield* (Queue.offer(queue, 1)) + const result1 = yield* pull + yield* (Queue.offer(queue, 2)) + const result2 = yield* pull + yield* (Effect.sleep(Duration.seconds(4))) + yield* (Queue.offer(queue, 3)) + const result3 = yield* pull + return [Array.from(result1), Array.from(result2), Array.from(result3)] as const + }) + ), + Effect.scoped, + Effect.fork + ) + yield* (TestClock.adjust(Duration.seconds(8))) + const result = yield* (Fiber.join(fiber)) + deepStrictEqual(result, [[1], [2], [3]]) + })) + + it.effect("throttleShape - infinite bandwidth", () => + Effect.gen(function*() { + const queue = yield* (Queue.bounded(10)) + const result = yield* pipe( + Stream.fromQueue(queue), + Stream.throttle({ + strategy: "shape", + cost: () => 100_000, + units: 1, + duration: Duration.zero + }), + Stream.toPull, + Effect.flatMap((pull) => + Effect.gen(function*() { + yield* (Queue.offer(queue, 1)) + const result1 = yield* pull + yield* (Queue.offer(queue, 2)) + const result2 = yield* pull + const elapsed = yield* (Clock.currentTimeMillis) + return [Array.from(result1), Array.from(result2), elapsed] as const + }) + ), + Effect.scoped + ) + deepStrictEqual(result, [[1], [2], 0]) + })) + + it.effect("throttleShape - with burst", () => + Effect.gen(function*() { + const queue = yield* (Queue.bounded(10)) + const fiber = yield* pipe( + Stream.fromQueue(queue), + Stream.throttle({ + strategy: "shape", + cost: Chunk.reduce(0, (x, y) => x + y), + units: 1, + duration: Duration.seconds(1), + burst: 2 + }), + Stream.toPull, + Effect.flatMap((pull) => + Effect.gen(function*() { + yield* (Queue.offer(queue, 1)) + const result1 = yield* pull + yield* (TestClock.adjust(Duration.seconds(2))) + yield* (Queue.offer(queue, 2)) + const result2 = yield* pull + yield* (TestClock.adjust(Duration.seconds(4))) + yield* (Queue.offer(queue, 3)) + const result3 = yield* pull + return [Array.from(result1), Array.from(result2), Array.from(result3)] as const + }) + ), + Effect.scoped, + Effect.fork + ) + const result = yield* (Fiber.join(fiber)) + deepStrictEqual(result, [[1], [2], [3]]) + })) + + it.effect("throttleShape - free elements", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.make(1, 2, 3, 4), + Stream.throttle({ + strategy: "shape", + cost: () => 0, + units: 1, + duration: Duration.infinity + }), + Stream.runCollect + ) + deepStrictEqual(Array.from(result), [1, 2, 3, 4]) + })) + + it.effect("debounce - should drop earlier chunks within waitTime", () => + Effect.gen(function*() { + const coordination = yield* (chunkCoordination([ + Chunk.of(1), + Chunk.make(3, 4), + Chunk.of(5), + Chunk.make(6, 7) + ])) + const stream = pipe( + Stream.fromQueue(coordination.queue), + Stream.filterMapWhile(Exit.match({ + onSuccess: Option.some, + onFailure: Option.none + })), + Stream.debounce(Duration.seconds(1)), + Stream.tap(() => coordination.proceed) + ) + const fiber = yield* pipe(stream, Stream.runCollect, Effect.fork) + yield* (Effect.fork(coordination.offer)) + yield* pipe( + Effect.sleep(Duration.millis(500)), + Effect.zipRight(coordination.offer), + Effect.fork + ) + yield* pipe( + Effect.sleep(Duration.seconds(2)), + Effect.zipRight(coordination.offer), + Effect.fork + ) + yield* pipe( + Effect.sleep(Duration.millis(2500)), + Effect.zipRight(coordination.offer), + Effect.fork + ) + yield* (TestClock.adjust(Duration.millis(3500))) + const result = yield* (Fiber.join(fiber)) + deepStrictEqual( + Array.from(result).map((chunk) => Array.from(chunk)), + [[3, 4], [6, 7]] + ) + })) + + it.effect("debounce - should take latest chunk within waitTime", () => + Effect.gen(function*() { + const coordination = yield* (chunkCoordination([ + Chunk.make(1, 2), + Chunk.make(3, 4), + Chunk.make(5, 6) + ])) + const stream = pipe( + Stream.fromQueue(coordination.queue), + Stream.filterMapWhile(Exit.match({ onSuccess: Option.some, onFailure: Option.none })), + Stream.debounce(Duration.seconds(1)), + Stream.tap(() => coordination.proceed) + ) + const fiber = yield* pipe(stream, Stream.runCollect, Effect.fork) + yield* pipe( + coordination.offer, + Effect.zipRight(coordination.offer), + Effect.zipRight(coordination.offer) + ) + yield* (TestClock.adjust(Duration.seconds(1))) + const result = yield* (Fiber.join(fiber)) + deepStrictEqual( + Array.from(result).map((chunk) => Array.from(chunk)), + [[5, 6]] + ) + })) + + it.effect("debounce - should work properly with parallelization", () => + Effect.gen(function*() { + const coordination = yield* (chunkCoordination([ + Chunk.of(1), + Chunk.of(2), + Chunk.of(3) + ])) + const stream = pipe( + Stream.fromQueue(coordination.queue), + Stream.filterMapWhile(Exit.match({ onSuccess: Option.some, onFailure: Option.none })), + Stream.debounce(Duration.seconds(1)), + Stream.tap(() => coordination.proceed) + ) + const fiber = yield* pipe(stream, Stream.runCollect, Effect.fork) + yield* (Effect.all([ + coordination.offer, + coordination.offer, + coordination.offer + ], { concurrency: 3, discard: true })) + yield* (TestClock.adjust(Duration.seconds(1))) + const result = yield* (Fiber.join(fiber)) + deepStrictEqual( + Array.from(result).map((chunk) => Array.from(chunk)), + [[3]] + ) + })) + + it.effect("debounce - should handle empty chunks properly", () => + Effect.gen(function*() { + const fiber = yield* pipe( + Stream.make(1, 2, 3), + Stream.schedule(Schedule.fixed(Duration.millis(500))), + Stream.debounce(Duration.seconds(1)), + Stream.runCollect, + Effect.fork + ) + yield* (TestClock.adjust(Duration.seconds(3))) + const result = yield* (Fiber.join(fiber)) + deepStrictEqual(Array.from(result), [3]) + })) + + it.effect("debounce - should fail immediately", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.fromEffect(Effect.fail(Option.none())), + Stream.debounce(Duration.infinity), + Stream.runCollect, + Effect.exit + ) + assertFailure(result, Cause.fail(Option.none())) + })) + + it.effect("debounce - should work with empty streams", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.empty, + Stream.debounce(Duration.seconds(5)), + Stream.runCollect + ) + assertTrue(Chunk.isEmpty(result)) + })) + + it.effect("debounce - should pick last element from every chunk", () => + Effect.gen(function*() { + const fiber = yield* pipe( + Stream.make(1, 2, 3), + Stream.debounce(Duration.seconds(1)), + Stream.runCollect, + Effect.fork + ) + yield* (TestClock.adjust(Duration.seconds(1))) + const result = yield* (Fiber.join(fiber)) + deepStrictEqual(Array.from(result), [3]) + })) + + it.effect("debounce - should interrupt fibers properly", () => + Effect.gen(function*() { + const coordination = yield* (chunkCoordination([ + Chunk.of(1), + Chunk.of(2), + Chunk.of(3) + ])) + const fiber = yield* pipe( + Stream.fromQueue(coordination.queue), + Stream.tap(() => coordination.proceed), + // TODO: remove + Stream.flatMap((exit) => Stream.fromEffectOption(Effect.suspend(() => exit))), + Stream.flattenChunks, + Stream.debounce(Duration.millis(200)), + Stream.interruptWhen(Effect.never), + Stream.take(1), + Stream.runCollect, + Effect.fork + ) + yield* pipe( + coordination.offer, + Effect.zipRight(TestClock.adjust(Duration.millis(100))), + Effect.zipRight(coordination.awaitNext), + Effect.repeatN(3) + ) + yield* (TestClock.adjust(Duration.millis(100))) + const result = yield* (Fiber.join(fiber)) + deepStrictEqual(Array.from(result), [3]) + })) + + it.effect("debounce - should interrupt children fiber on stream interruption", () => + Effect.gen(function*() { + const ref = yield* (Ref.make(false)) + const fiber = yield* pipe( + Stream.fromEffect(Effect.void), + Stream.concat(Stream.fromEffect(pipe( + Effect.never, + Effect.onInterrupt(() => Ref.set(ref, true)) + ))), + Stream.debounce(Duration.millis(800)), + Stream.runDrain, + Effect.fork + ) + yield* (TestClock.adjust(Duration.minutes(1))) + yield* (Fiber.interrupt(fiber)) + const result = yield* (Ref.get(ref)) + assertTrue(result) + })) +}) diff --git a/repos/effect/packages/effect/test/Stream/timeouts.test.ts b/repos/effect/packages/effect/test/Stream/timeouts.test.ts new file mode 100644 index 0000000..1b6ff22 --- /dev/null +++ b/repos/effect/packages/effect/test/Stream/timeouts.test.ts @@ -0,0 +1,149 @@ +import { describe, it } from "@effect/vitest" +import { assertLeft, deepStrictEqual } from "@effect/vitest/utils" +import * as Cause from "effect/Cause" +import * as Chunk from "effect/Chunk" +import * as Duration from "effect/Duration" +import * as Effect from "effect/Effect" +import * as Exit from "effect/Exit" +import * as Fiber from "effect/Fiber" +import { pipe } from "effect/Function" +import * as Option from "effect/Option" +import * as Queue from "effect/Queue" +import * as Stream from "effect/Stream" +import * as TestClock from "effect/TestClock" +import { chunkCoordination } from "../utils/coordination.js" + +describe("Stream", () => { + it.effect("timeout - succeed", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.succeed(1), + Stream.timeout(Duration.infinity), + Stream.runCollect + ) + deepStrictEqual(Array.from(result), [1]) + })) + + it.effect("timeout - should end the stream", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.range(0, 4), + Stream.tap(() => Effect.sleep(Duration.infinity)), + Stream.timeout(Duration.zero), + Stream.runCollect + ) + deepStrictEqual(Array.from(result), []) + })) + + it.effect("timeoutFail - succeed", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.range(0, 4), + Stream.tap(() => Effect.sleep(Duration.infinity)), + Stream.timeoutFail(() => false, Duration.zero), + Stream.runDrain, + Effect.map(() => true), + Effect.either + ) + assertLeft(result, false) + })) + + it.effect("timeoutFail - failures", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.fail("original"), + Stream.timeoutFail(() => "timeout", Duration.minutes(15)), + Stream.runDrain, + Effect.flip + ) + deepStrictEqual(result, "original") + })) + + it.effect("timeoutFailCause", () => + Effect.gen(function*() { + const error = new Cause.RuntimeException("boom") + const result = yield* pipe( + Stream.range(0, 4), + Stream.tap(() => Effect.sleep(Duration.infinity)), + Stream.timeoutFailCause(() => Cause.die(error), Duration.zero), + Stream.runDrain, + Effect.sandbox, + Effect.either + ) + assertLeft(result, Cause.die(error)) + })) + + it.effect("timeoutTo - succeed", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.range(0, 4), + Stream.timeoutTo(Duration.infinity, Stream.succeed(-1)), + Stream.runCollect + ) + deepStrictEqual(Array.from(result), [0, 1, 2, 3, 4]) + })) + + it.effect("timeoutTo - should switch streams", () => + Effect.gen(function*() { + const coordination = yield* chunkCoordination([ + Chunk.of(1), + Chunk.of(2), + Chunk.of(3) + ]) + const fiber = yield* pipe( + Stream.fromQueue(coordination.queue), + Stream.filterMapWhile(Exit.match({ onSuccess: Option.some, onFailure: Option.none })), + Stream.flattenChunks, + Stream.timeoutTo(Duration.seconds(2), Stream.succeed(4)), + Stream.tap(() => coordination.proceed), + Stream.runCollect, + Effect.fork + ) + yield* pipe( + coordination.offer, + Effect.zipRight(TestClock.adjust(Duration.seconds(1))), + Effect.zipRight(coordination.awaitNext) + ) + yield* pipe( + coordination.offer, + Effect.zipRight(TestClock.adjust(Duration.seconds(3))), + Effect.zipRight(coordination.awaitNext) + ) + yield* coordination.offer + const result = yield* Fiber.join(fiber) + deepStrictEqual(Array.from(result), [1, 2, 4]) + })) + + it.effect("timeoutTo - should not apply timeout after switch", () => + Effect.gen(function*() { + const queue1 = yield* Queue.unbounded() + const queue2 = yield* Queue.unbounded() + const stream1 = Stream.fromQueue(queue1) + const stream2 = Stream.fromQueue(queue2) + const fiber = yield* pipe( + stream1, + Stream.timeoutTo(Duration.seconds(2), stream2), + Stream.runCollect, + Effect.fork + ) + yield* pipe( + Queue.offer(queue1, 1), + Effect.zipRight(TestClock.adjust(Duration.seconds(1))) + ) + yield* pipe( + Queue.offer(queue1, 2), + Effect.zipRight(TestClock.adjust(Duration.seconds(3))) + ) + yield* Queue.offer(queue1, 3) + yield* pipe( + Queue.offer(queue2, 4), + Effect.zipRight(TestClock.adjust(Duration.seconds(3))) + ) + yield* pipe( + Queue.offer(queue2, 5), + Effect.zipRight(Queue.shutdown(queue2)) + ) + const result = yield* Fiber.join(fiber) + deepStrictEqual(Array.from(result), [1, 2, 4, 5]) + })) +}) diff --git a/repos/effect/packages/effect/test/Stream/transducing.test.ts b/repos/effect/packages/effect/test/Stream/transducing.test.ts new file mode 100644 index 0000000..56b26e0 --- /dev/null +++ b/repos/effect/packages/effect/test/Stream/transducing.test.ts @@ -0,0 +1,56 @@ +import { describe, it } from "@effect/vitest" +import { assertLeft, deepStrictEqual } from "@effect/vitest/utils" +import * as Chunk from "effect/Chunk" +import * as Effect from "effect/Effect" +import { constTrue, pipe } from "effect/Function" +import * as Sink from "effect/Sink" +import * as Stream from "effect/Stream" + +describe("Stream", () => { + it.effect("transduce - simple example", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.make("1", "2", ",", "3", "4"), + Stream.transduce( + pipe( + Sink.collectAllWhile((char: string) => Number.isInteger(Number.parseInt(char))), + Sink.zipLeft(Sink.collectAllWhile((char: string) => !Number.isInteger(Number.parseInt(char)))) + ) + ), + Stream.map(Chunk.join("")), + Stream.runCollect + ) + deepStrictEqual(Array.from(result), ["12", "34"]) + })) + + it.effect("transduce - no remainder", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.make(1, 2, 3, 4), + Stream.transduce(Sink.fold(100, (n) => n % 2 === 0, (acc, n) => acc + n)), + Stream.runCollect + ) + deepStrictEqual(Array.from(result), [101, 105, 104]) + })) + + it.effect("transduce - with a sink that always signals more", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.make(1, 2, 3), + Stream.transduce(Sink.fold(0, constTrue, (acc, n) => acc + n)), + Stream.runCollect + ) + deepStrictEqual(Array.from(result), [6]) + })) + + it.effect("transduce - propagates scope error", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.make(1, 2, 3), + Stream.transduce(Sink.fail("Woops")), + Stream.runCollect, + Effect.either + ) + assertLeft(result, "Woops") + })) +}) diff --git a/repos/effect/packages/effect/test/Stream/zipping.test.ts b/repos/effect/packages/effect/test/Stream/zipping.test.ts new file mode 100644 index 0000000..dc2ee1a --- /dev/null +++ b/repos/effect/packages/effect/test/Stream/zipping.test.ts @@ -0,0 +1,490 @@ +import { describe, it } from "@effect/vitest" +import { assertLeft, assertTrue, deepStrictEqual, strictEqual } from "@effect/vitest/utils" +import * as Cause from "effect/Cause" +import * as Chunk from "effect/Chunk" +import * as Deferred from "effect/Deferred" +import * as Effect from "effect/Effect" +import * as Exit from "effect/Exit" +import * as fc from "effect/FastCheck" +import * as Fiber from "effect/Fiber" +import { identity, pipe } from "effect/Function" +import * as Number from "effect/Number" +import * as Option from "effect/Option" +import * as Order from "effect/Order" +import * as Queue from "effect/Queue" +import * as Stream from "effect/Stream" +import * as Take from "effect/Take" + +const chunkArb = ( + arb: fc.Arbitrary, + constraints?: fc.ArrayConstraints +): fc.Arbitrary> => fc.array(arb, constraints).map(Chunk.unsafeFromArray) + +const OrderByKey: Order.Order = pipe( + Number.Order, + Order.mapInput((tuple) => tuple[0]) +) + +export const splitChunks = (chunks: Chunk.Chunk>): fc.Arbitrary>> => { + const split = (chunks: Chunk.Chunk>): fc.Arbitrary>> => + fc.integer({ min: 0, max: Math.max(chunks.length - 1, 0) }).chain((i) => { + const chunk = Chunk.unsafeGet(chunks, i) + return fc.integer({ min: 0, max: Math.max(chunk.length - 1, 0) }).map((j) => { + const [left, right] = pipe(chunk, Chunk.splitAt(j)) + return pipe( + chunks, + Chunk.take(i), + Chunk.appendAll(Chunk.of(left)), + Chunk.appendAll(Chunk.of(right)), + Chunk.appendAll(pipe(chunks, Chunk.drop(i + 1))) + ) + }) + }) + return fc.oneof(fc.constant(chunks), split(chunks).chain((chunks) => splitChunks(chunks))) +} + +describe("Stream", () => { + it("zipAllSortedByKeyWith", () => { + const intArb = fc.integer({ min: 1, max: 100 }) + const chunkArb = fc.array(fc.tuple(intArb, intArb)).map((entries) => + pipe(Chunk.fromIterable(new Map(entries)), Chunk.sort(OrderByKey)) + ) + const chunksArb = chunkArb.chain((chunk) => splitChunks(Chunk.of(chunk))) + return fc.assert(fc.asyncProperty(chunksArb, chunksArb, async (as, bs) => { + const left = Stream.fromChunks(...as) + const right = Stream.fromChunks(...bs) + const actual = Stream.zipAllSortedByKeyWith(left, { + other: right, + onSelf: identity, + onOther: identity, + onBoth: (x, y) => x + y, + order: Number.Order + }) + const expected = pipe( + Chunk.flatten(as), + Chunk.reduce(new Map(Array.from(Chunk.flatten(bs))), (map, [k, v]) => + pipe( + Option.fromNullable(map.get(k)), + Option.match({ + onNone: () => map.set(k, v), + onSome: (v1) => map.set(k, v + v1) + }) + )), + Chunk.fromIterable, + Chunk.sort(OrderByKey) + ) + const result = await Effect.runPromise(Stream.runCollect(actual)) + deepStrictEqual(Array.from(result), Array.from(expected)) + })) + }) + + it.effect("zip - does not pull too much when one of the streams is done", () => + Effect.gen(function*() { + const left = pipe( + Stream.fromChunks(Chunk.make(1, 2), Chunk.make(3, 4), Chunk.of(5)), + Stream.concat(Stream.fail("boom")) + ) + const right = Stream.fromChunks(Chunk.make("a", "b"), Chunk.of("c")) + const result = yield* pipe(left, Stream.zip(right), Stream.runCollect) + deepStrictEqual(Array.from(result), [[1, "a"], [2, "b"], [3, "c"]]) + })) + + it("zip - equivalence with Chunk.zip", () => + fc.assert( + fc.asyncProperty(fc.array(chunkArb(fc.integer())), fc.array(chunkArb(fc.integer())), async (left, right) => { + const stream = pipe( + Stream.fromChunks(...left), + Stream.zip(Stream.fromChunks(...right)) + ) + const expected = pipe( + Chunk.flatten(Chunk.unsafeFromArray(left)), + Chunk.zip(Chunk.flatten(Chunk.unsafeFromArray(right))) + ) + const actual = await Effect.runPromise(Stream.runCollect(stream)) + deepStrictEqual(Array.from(actual), Array.from(expected)) + }) + )) + + it.effect("zip - terminate in uninterruptible region", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.make(1), + Stream.zip(Stream.make(2)), + Stream.runDrain, + Effect.uninterruptible + ) + strictEqual(result, undefined) + })) + + it.effect("zipWith - prioritizes failures", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.never, + Stream.zipWith(Stream.fail("Ouch"), () => Option.none()), + Stream.runCollect, + Effect.either + ) + assertLeft(result, "Ouch") + })) + + it.effect("zipWith - dies if one of the streams throws an exception", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.make(1), + Stream.flatMap(() => + Stream.sync(() => { + throw new Cause.RuntimeException("Ouch") + }) + ), + Stream.zip(Stream.make(1)), + Stream.runCollect, + Effect.exit + ) + deepStrictEqual( + result, + Exit.failCause(Cause.die(new Cause.RuntimeException("Ouch"))) + ) + })) + + // TODO: handle Chunk.zipAllWith + // it("zipAllWith", () => + // fc.assert(fc.asyncProperty( + // fc.array(chunkArb(fc.integer()).filter((chunk) => chunk.length > 0)), + // fc.array(chunkArb(fc.integer()).filter((chunk) => chunk.length > 0)), + // async (left, right) => { + // const stream = pipe( + // Stream.fromChunks(...left), + // Stream.map(Option.some), + // Stream.zipAll( + // pipe(Stream.fromChunks(...right), Stream.map(Option.some)), + // Option.none() as Option.Option, + // Option.none() as Option.Option + // ) + // ) + // const actual = await Effect.runPromise(Stream.runCollect(stream)) + // const expected = pipe( + // Chunk.flatten(Chunk.fromIterable(left)), + // Chunk.zipAllWith( + // Chunk.flatten(Chunk.fromIterable(right)), + // (a, b) => [Option.some(a), Option.some(b)] as const, + // (a) => [Option.some(a), Option.none()] as const, + // (b) => [Option.none(), Option.some(b)] as const + // ) + // ) + // deepStrictEqual(Array.from(actual), Array.from(expected)) + // } + // ))) + + it.effect("zipAll - prioritizes failures", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.never, + Stream.zipAll({ + other: Stream.fail("Ouch"), + defaultSelf: Option.none(), + defaultOther: Option.none() + }), + Stream.runCollect, + Effect.either + ) + assertLeft(result, "Ouch") + })) + + it.effect("zipWithIndex", () => + Effect.gen(function*() { + const stream = Stream.make(1, 2, 3, 4, 5) + const { result1, result2 } = yield* Effect.all({ + result1: Stream.runCollect(Stream.zipWithIndex(stream)), + result2: pipe(Stream.runCollect(stream), Effect.map(Chunk.map((a, i) => [a, i] as const))) + }) + deepStrictEqual(Array.from(result1), Array.from(result2)) + })) + + it.effect("zipLatest", () => + Effect.gen(function*() { + const left = yield* Queue.unbounded>() + const right = yield* Queue.unbounded>() + const output = yield* Queue.bounded>(1) + yield* pipe( + Stream.fromChunkQueue(left), + Stream.zipLatest(Stream.fromChunkQueue(right)), + Stream.runIntoQueue(output), + Effect.fork + ) + yield* Queue.offer(left, Chunk.make(0)) + yield* Queue.offerAll(right, [Chunk.make(0), Chunk.make(1)]) + const chunk1 = yield* pipe( + Queue.take(output), + Effect.flatMap(Take.done), + Effect.replicateEffect(2), + Effect.map(Chunk.unsafeFromArray), + Effect.map(Chunk.flatten) + ) + yield* Queue.offerAll(left, [Chunk.make(1), Chunk.make(2)]) + const chunk2 = yield* pipe( + Queue.take(output), + Effect.flatMap(Take.done), + Effect.replicateEffect(2), + Effect.map(Chunk.unsafeFromArray), + Effect.map(Chunk.flatten) + ) + deepStrictEqual(Array.from(chunk1), [[0, 0], [0, 1]]) + deepStrictEqual(Array.from(chunk2), [[1, 1], [2, 1]]) + })) + + it.effect("zipLatestWith - handles empty pulls properly", () => + Effect.gen(function*() { + const stream0 = Stream.fromChunks( + Chunk.empty(), + Chunk.empty(), + Chunk.make(2) + ) + const stream1 = Stream.fromChunks(Chunk.make(1), Chunk.make(1)) + const deferred = yield* Deferred.make() + const latch = yield* Deferred.make() + const fiber = yield* pipe( + stream0, + Stream.concat(Stream.fromEffect(Deferred.await(deferred))), + Stream.concat(Stream.make(2)), + Stream.zipLatestWith( + pipe( + Stream.make(1, 1), + Stream.ensuring(Deferred.succeed(latch, void 0)), + Stream.concat(stream1) + ), + (_, n) => n + ), + Stream.take(3), + Stream.runCollect, + Effect.fork + ) + yield* Deferred.await(latch) + yield* Deferred.succeed(deferred, 2) + const result = yield* Fiber.join(fiber) + deepStrictEqual(Array.from(result), [1, 1, 1]) + })) + + it.effect("zipLatestWith - handles empty pulls properly (JVM Only - LOL)", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.unfold(0, (n) => + Option.some( + [ + n < 3 ? Chunk.empty() : Chunk.of(2), + n + 1 + ] as const + )), + Stream.flattenChunks, + Stream.forever, + Stream.zipLatestWith(Stream.forever(Stream.make(1)), (_, n) => n), + Stream.take(3), + Stream.runCollect + ) + deepStrictEqual(Array.from(result), [1, 1, 1]) + })) + + it("zipLatestWith - preserves partial ordering of stream elements", () => { + const sortedChunkArb = chunkArb(fc.integer({ min: 1, max: 100 })) + .map(Chunk.sort(Number.Order)) + const sortedChunksArb = sortedChunkArb.chain((chunk) => splitChunks(Chunk.of(chunk))) + return fc.assert(fc.asyncProperty(sortedChunksArb, sortedChunksArb, async (left, right) => { + const stream = pipe( + Stream.fromChunks(...left), + Stream.zipLatestWith(Stream.fromChunks(...right), (l, r) => l + r) + ) + const result = await Effect.runPromise(Stream.runCollect(stream)) + const [isSorted] = Chunk.isEmpty(result) ? [true] : pipe( + result, + Chunk.drop(1), + Chunk.reduce( + [true as boolean, pipe(result, Chunk.unsafeGet(0))] as const, + ([isSorted, last], curr) => [isSorted && last <= curr, curr] as const + ) + ) + assertTrue(isSorted) + })) + }) + + it.effect("zipWithNext", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.make(1, 2, 3), + Stream.zipWithNext, + Stream.runCollect + ) + deepStrictEqual(Array.from(result), [ + [1, Option.some(2)], + [2, Option.some(3)], + [3, Option.none()] + ]) + })) + + it.effect("zipWithNext - should work with multiple chunks", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.fromChunks(Chunk.of(1), Chunk.of(2), Chunk.of(3)), + Stream.zipWithNext, + Stream.runCollect + ) + deepStrictEqual(Array.from(result), [ + [1, Option.some(2)], + [2, Option.some(3)], + [3, Option.none()] + ]) + })) + + it.effect("zipWithNext - should work with an empty stream", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.empty, + Stream.zipWithNext, + Stream.runCollect + ) + deepStrictEqual(Array.from(result), []) + })) + + it("zipWithNext - should output the same values as zipping with the tail plus the last element", () => + fc.assert(fc.asyncProperty(fc.array(chunkArb(fc.integer())), async (chunks) => { + const stream = Stream.fromChunks(...chunks) + const { result1, result2 } = await Effect.runPromise(Effect.all({ + result1: pipe( + stream, + Stream.zipWithNext, + Stream.runCollect + ), + result2: Stream.runCollect( + Stream.zipAll(stream, { + other: Stream.map(Stream.drop(stream, 1), Option.some), + defaultSelf: 0, + defaultOther: Option.none() + }) + ) + })) + deepStrictEqual(Array.from(result1), Array.from(result2)) + }))) + + it.effect("zipWithPrevious - should zip with previous element for a single chunk", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.make(1, 2, 3), + Stream.zipWithPrevious, + Stream.runCollect + ) + deepStrictEqual(Array.from(result), [ + [Option.none(), 1], + [Option.some(1), 2], + [Option.some(2), 3] + ]) + })) + + it.effect("zipWithPrevious - should work with multiple chunks", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.fromChunks(Chunk.of(1), Chunk.of(2), Chunk.of(3)), + Stream.zipWithPrevious, + Stream.runCollect + ) + deepStrictEqual(Array.from(result), [ + [Option.none(), 1], + [Option.some(1), 2], + [Option.some(2), 3] + ]) + })) + + it.effect("zipWithPrevious - should work with an empty stream", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.empty, + Stream.zipWithPrevious, + Stream.runCollect + ) + deepStrictEqual(Array.from(result), []) + })) + + it("zipWithPrevious - should output same values as first element plus zipping with init", () => + fc.assert(fc.asyncProperty(fc.array(chunkArb(fc.integer())), async (chunks) => { + const stream = Stream.fromChunks(...chunks) + const { result1, result2 } = await Effect.runPromise(Effect.all({ + result1: pipe( + stream, + Stream.zipWithPrevious, + Stream.runCollect + ), + result2: pipe( + Stream.make(Option.none()), + Stream.concat(pipe(stream, Stream.map(Option.some))), + Stream.zip(stream), + Stream.runCollect + ) + })) + deepStrictEqual(Array.from(result1), Array.from(result2)) + }))) + + it.effect("zipWithPreviousAndNext", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.make(1, 2, 3), + Stream.zipWithPreviousAndNext, + Stream.runCollect + ) + deepStrictEqual(Array.from(result), [ + [Option.none(), 1, Option.some(2)], + [Option.some(1), 2, Option.some(3)], + [Option.some(2), 3, Option.none()] + ]) + })) + + it("zipWithPreviousAndNext - should output same values as zipping with both previous and next element", () => + fc.assert(fc.asyncProperty(fc.array(chunkArb(fc.integer()), { minLength: 0, maxLength: 5 }), async (chunks) => { + const stream = Stream.fromChunks(...chunks) + const previous = pipe( + Stream.make(Option.none()), + Stream.concat(pipe(stream, Stream.map(Option.some))) + ) + const next = pipe( + stream, + Stream.drop(1), + Stream.map(Option.some), + Stream.concat(Stream.make(Option.none())) + ) + const { result1, result2 } = await pipe( + Effect.all({ + result1: pipe( + stream, + Stream.zipWithPreviousAndNext, + Stream.runCollect + ), + result2: pipe( + previous, + Stream.zip(stream), + Stream.zipFlatten(next), + Stream.runCollect + ) + }), + Effect.runPromise + ) + deepStrictEqual(Array.from(result1), Array.from(result2)) + }))) + + it.effect("zipLatestAll", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.zipLatestAll( + Stream.make(1, 2, 3).pipe(Stream.rechunk(1)), + Stream.make("a", "b", "c").pipe(Stream.rechunk(1)), + Stream.make(true, false, true).pipe(Stream.rechunk(1)) + ), + Stream.runCollect, + Effect.map(Chunk.toReadonlyArray) + ) + deepStrictEqual(result, [ + [1, "a", true], + [2, "a", true], + [3, "a", true], + [3, "b", true], + [3, "c", true], + [3, "c", false], + [3, "c", true] + ]) + })) +}) diff --git a/repos/effect/packages/effect/test/String.test.ts b/repos/effect/packages/effect/test/String.test.ts new file mode 100644 index 0000000..45905f5 --- /dev/null +++ b/repos/effect/packages/effect/test/String.test.ts @@ -0,0 +1,280 @@ +import { describe, it } from "@effect/vitest" +import { assertFalse, assertNone, assertSome, assertTrue, deepStrictEqual, strictEqual } from "@effect/vitest/utils" +import { Order, pipe, String as Str } from "effect" + +describe("String", () => { + it("isString", () => { + assertTrue(Str.isString("a")) + assertFalse(Str.isString(1)) + assertFalse(Str.isString(true)) + }) + + it("empty", () => { + strictEqual(Str.empty, "") + }) + + it("Equivalence", () => { + assertTrue(Str.Equivalence("a", "a")) + assertFalse(Str.Equivalence("a", "b")) + }) + + it("Order", () => { + const lessThan = Order.lessThan(Str.Order) + const lessThanOrEqualTo = Order.lessThanOrEqualTo(Str.Order) + assertTrue(pipe("a", lessThan("b"))) + assertFalse(pipe("a", lessThan("a"))) + assertTrue(pipe("a", lessThanOrEqualTo("a"))) + assertFalse(pipe("b", lessThan("a"))) + assertFalse(pipe("b", lessThanOrEqualTo("a"))) + }) + + it("concat", () => { + strictEqual(pipe("a", Str.concat("b")), "ab") + }) + + it("isEmpty", () => { + assertTrue(Str.isEmpty("")) + assertFalse(Str.isEmpty("a")) + }) + + it("isNonEmpty", () => { + assertFalse(Str.isNonEmpty("")) + assertTrue(Str.isNonEmpty("a")) + }) + + it("length", () => { + strictEqual(Str.length(""), 0) + strictEqual(Str.length("a"), 1) + strictEqual(Str.length("aaa"), 3) + }) + + it("toUpperCase", () => { + strictEqual(Str.toUpperCase("a"), "A") + }) + + it("toLowerCase", () => { + strictEqual(Str.toLowerCase("A"), "a") + }) + + it("capitalize", () => { + strictEqual(Str.capitalize(""), "") + strictEqual(Str.capitalize("abc"), "Abc") + }) + + it("uncapitalize", () => { + strictEqual(Str.uncapitalize(""), "") + strictEqual(Str.uncapitalize("Abc"), "abc") + }) + + it("replace", () => { + strictEqual(pipe("abc", Str.replace("b", "d")), "adc") + }) + + it("split", () => { + deepStrictEqual(pipe("abc", Str.split("")), ["a", "b", "c"]) + deepStrictEqual(pipe("", Str.split("")), [""]) + }) + + it("trim", () => { + strictEqual(pipe(" a ", Str.trim), "a") + }) + + it("trimStart", () => { + strictEqual(pipe(" a ", Str.trimStart), "a ") + }) + + it("trimEnd", () => { + strictEqual(pipe(" a ", Str.trimEnd), " a") + }) + + it("includes", () => { + assertTrue(pipe("abc", Str.includes("b"))) + assertFalse(pipe("abc", Str.includes("d"))) + assertTrue(pipe("abc", Str.includes("b", 1))) + assertFalse(pipe("abc", Str.includes("a", 1))) + }) + + it("startsWith", () => { + assertTrue(pipe("abc", Str.startsWith("a"))) + assertFalse(pipe("bc", Str.startsWith("a"))) + assertTrue(pipe("abc", Str.startsWith("b", 1))) + assertFalse(pipe("bc", Str.startsWith("a", 1))) + }) + + it("endsWith", () => { + assertTrue(pipe("abc", Str.endsWith("c"))) + assertFalse(pipe("ab", Str.endsWith("c"))) + assertTrue(pipe("abc", Str.endsWith("b", 2))) + assertFalse(pipe("abc", Str.endsWith("c", 2))) + }) + + it("slice", () => { + deepStrictEqual(pipe("abcd", Str.slice(1, 3)), "bc") + }) + + it("charCodeAt", () => { + assertSome(pipe("abc", Str.charCodeAt(1)), 98) + assertNone(pipe("abc", Str.charCodeAt(4))) + }) + + it("substring", () => { + strictEqual(pipe("abcd", Str.substring(1)), "bcd") + strictEqual(pipe("abcd", Str.substring(1, 3)), "bc") + }) + + it("at", () => { + assertSome(pipe("abc", Str.at(1)), "b") + assertNone(pipe("abc", Str.at(4))) + }) + + it("charAt", () => { + assertSome(pipe("abc", Str.charAt(1)), "b") + assertNone(pipe("abc", Str.charAt(4))) + }) + + it("codePointAt", () => { + assertSome(pipe("abc", Str.codePointAt(1)), 98) + assertNone(pipe("abc", Str.codePointAt(4))) + }) + + it("indexOf", () => { + assertSome(pipe("abbbc", Str.indexOf("b")), 1) + assertNone(pipe("abbbc", Str.indexOf("d"))) + }) + + it("lastIndexOf", () => { + assertSome(pipe("abbbc", Str.lastIndexOf("b")), 3) + assertNone(pipe("abbbc", Str.lastIndexOf("d"))) + }) + + it("localeCompare", () => { + strictEqual(pipe("a", Str.localeCompare("b")), -1) + strictEqual(pipe("b", Str.localeCompare("a")), 1) + strictEqual(pipe("a", Str.localeCompare("a")), 0) + }) + + it("match", () => { + assertSome(pipe("a", Str.match(/a/)), "a".match(/a/)) + assertNone(pipe("a", Str.match(/b/))) + }) + + it("matchAll", () => { + strictEqual(Array.from(pipe("apple, banana", Str.matchAll(/a[pn]/g))).length, 3) + strictEqual(Array.from(pipe("apple, banana", Str.matchAll(/c/g))).length, 0) + }) + + it("normalize", () => { + const str = "\u1E9B\u0323" + strictEqual(pipe(str, Str.normalize()), "\u1E9B\u0323") + strictEqual(pipe(str, Str.normalize("NFC")), "\u1E9B\u0323") + strictEqual(pipe(str, Str.normalize("NFD")), "\u017F\u0323\u0307") + strictEqual(pipe(str, Str.normalize("NFKC")), "\u1E69") + strictEqual(pipe(str, Str.normalize("NFKD")), "\u0073\u0323\u0307") + }) + + it("padEnd", () => { + strictEqual(pipe("a", Str.padEnd(5)), "a ") + strictEqual(pipe("a", Str.padEnd(5, "_")), "a____") + }) + + it("padStart", () => { + strictEqual(pipe("a", Str.padStart(5)), " a") + strictEqual(pipe("a", Str.padStart(5, "_")), "____a") + }) + + it("repeat", () => { + strictEqual(pipe("a", Str.repeat(3)), "aaa") + }) + + it("replaceAll", () => { + strictEqual(pipe("ababb", Str.replaceAll("b", "c")), "acacc") + strictEqual(pipe("ababb", Str.replaceAll(/ba/g, "cc")), "accbb") + }) + + it("search", () => { + assertSome(pipe("ababb", Str.search("b")), 1) + assertSome(pipe("ababb", Str.search(/abb/)), 2) + assertNone(pipe("ababb", Str.search(/c/))) + }) + + it("toLocaleLowerCase", () => { + const locales = ["tr", "TR", "tr-TR", "tr-u-co-search", "tr-x-turkish"] + strictEqual(pipe("\u0130", Str.toLocaleLowerCase(locales)), "i") + }) + + it("toLocaleUpperCase", () => { + const locales = ["lt", "LT", "lt-LT", "lt-u-co-phonebk", "lt-x-lietuva"] + strictEqual(pipe("i\u0307", Str.toLocaleUpperCase(locales)), "I") + }) + + describe("takeLeft", () => { + it("should take the specified number of characters from the left side of a string", () => { + strictEqual(Str.takeLeft("Hello, World!", 7), "Hello, ") + }) + + it("should return the string for `n` larger than the string length", () => { + const string = "Hello, World!" + strictEqual(Str.takeLeft(string, 100), string) + }) + + it("should return the empty string for a negative `n`", () => { + strictEqual(Str.takeLeft("Hello, World!", -1), "") + }) + + it("should round down if `n` is a float", () => { + strictEqual(Str.takeLeft("Hello, World!", 5.5), "Hello") + }) + }) + + describe("takeRight", () => { + it("should take the specified number of characters from the right side of a string", () => { + strictEqual(Str.takeRight("Hello, World!", 7), " World!") + }) + + it("should return the string for `n` larger than the string length", () => { + const string = "Hello, World!" + strictEqual(Str.takeRight(string, 100), string) + }) + + it("should return the empty string for a negative `n`", () => { + strictEqual(Str.takeRight("Hello, World!", -1), "") + }) + + it("should round down if `n` is a float", () => { + strictEqual(Str.takeRight("Hello, World!", 6.5), "World!") + }) + }) + + describe("stripMargin", () => { + it("should strip a leading prefix from each line", () => { + const string = `| + |Hello, + |World! + |` + const result = Str.stripMargin(string) + strictEqual(result, "\nHello,\nWorld!\n") + }) + + it("should strip a leading prefix from each line using a margin character", () => { + const string = "\n$\n $Hello,\r\n $World!\n $" + const result = Str.stripMarginWith(string, "$") + strictEqual(result, "\n\nHello,\r\nWorld!\n") + }) + }) + + describe("linesWithSeparators", () => { + it("should split a string into lines with separators", () => { + const string = "\n$\n $Hello,\r\n $World!\n $" + const result = Str.linesWithSeparators(string) + deepStrictEqual(Array.from(result), ["\n", "$\n", " $Hello,\r\n", " $World!\n", " $"]) + }) + }) + + describe("linesIterator", () => { + it("should split a string into lines", () => { + const string = "\n$\n $Hello,\r\n $World!\n $" + const result = Str.linesIterator(string) + deepStrictEqual(Array.from(result), ["", "$", " $Hello,", " $World!", " $"]) + }) + }) +}) diff --git a/repos/effect/packages/effect/test/Struct.test.ts b/repos/effect/packages/effect/test/Struct.test.ts new file mode 100644 index 0000000..5999549 --- /dev/null +++ b/repos/effect/packages/effect/test/Struct.test.ts @@ -0,0 +1,129 @@ +import { describe, it } from "@effect/vitest" +import { deepStrictEqual, strictEqual } from "@effect/vitest/utils" +import { Number, pipe, String, Struct } from "effect" + +describe("Struct", () => { + it("pick", () => { + deepStrictEqual(pipe({ a: "a", b: 1, c: true }, Struct.pick("a", "b")), { a: "a", b: 1 }) + deepStrictEqual(Struct.pick({ a: "a", b: 1, c: true }, "a", "b"), { a: "a", b: 1 }) + + const record1: Record = {} + deepStrictEqual(pipe(record1, Struct.pick("a", "b")), {}) + const record2: Record = { b: 1 } + deepStrictEqual(pipe(record2, Struct.pick("a", "b")), { b: 1 }) + + const optionalStringStruct1: { + a?: string + b: number + c: boolean + } = { b: 1, c: true } + deepStrictEqual(pipe(optionalStringStruct1, Struct.pick("a", "b")), { b: 1 }) + const optionalStringStruct2: { + a?: string + b: number + c: boolean + } = { a: "a", b: 1, c: true } + deepStrictEqual(pipe(optionalStringStruct2, Struct.pick("a", "b")), { a: "a", b: 1 }) + + const a = Symbol.for("a") + const optionalSymbolStruct1: { + [a]?: string + b: number + c: boolean + } = { b: 1, c: true } + deepStrictEqual(pipe(optionalSymbolStruct1, Struct.pick(a, "b")), { b: 1 }) + const optionalSymbolStruct2: { + [a]?: string + b: number + c: boolean + } = { [a]: "a", b: 1, c: true } + deepStrictEqual(pipe(optionalSymbolStruct2, Struct.pick(a, "b")), { [a]: "a", b: 1 }) + }) + + it("omit", () => { + deepStrictEqual(pipe({ a: "a", b: 1, c: true }, Struct.omit("c")), { a: "a", b: 1 }) + deepStrictEqual(Struct.omit({ a: "a", b: 1, c: true }, "c"), { a: "a", b: 1 }) + + const record1: Record = {} + deepStrictEqual(pipe(record1, Struct.omit("a", "c")), {}) + const record2: Record = { b: 1 } + deepStrictEqual(pipe(record2, Struct.omit("a", "c")), { b: 1 }) + + const optionalStringStruct1: { + a?: string + b: number + c: boolean + } = { b: 1, c: true } + deepStrictEqual(pipe(optionalStringStruct1, Struct.omit("c")), { b: 1 }) + const optionalStringStruct2: { + a?: string + b: number + c: boolean + } = { a: "a", b: 1, c: true } + deepStrictEqual(pipe(optionalStringStruct2, Struct.omit("c")), { a: "a", b: 1 }) + + const a = Symbol.for("a") + const optionalSymbolStruct1: { + [a]?: string + b: number + c: boolean + } = { b: 1, c: true } + deepStrictEqual(pipe(optionalSymbolStruct1, Struct.omit("c")), { b: 1 }) + const optionalSymbolStruct2: { + [a]?: string + b: number + c: boolean + } = { [a]: "a", b: 1, c: true } + deepStrictEqual(pipe(optionalSymbolStruct2, Struct.omit("c")), { [a]: "a", b: 1 }) + }) + + it("evolve", () => { + const res1 = pipe( + { a: "a", b: 1, c: true, d: "extra" }, + Struct.evolve({ + a: (s) => s.length, + b: (b) => b > 0, + c: (c) => !c + }) + ) + + deepStrictEqual(res1, { a: 1, b: true, c: false, d: "extra" }) + + const x: Record<"b", number> = Object.create({ a: 1 }) + x.b = 1 + const res2 = pipe(x, Struct.evolve({ b: (b) => b > 0 })) + + deepStrictEqual(res2, { b: true }) + + // dual + const res3 = Struct.evolve({ a: 1 }, { a: (x) => x > 0 }) + deepStrictEqual(res3, { a: true }) + }) + + it("struct", () => { + const PersonEquivalence = Struct.getEquivalence({ + name: String.Equivalence, + age: Number.Equivalence + }) + + deepStrictEqual( + PersonEquivalence({ name: "John", age: 25 }, { name: "John", age: 25 }), + true + ) + deepStrictEqual( + PersonEquivalence({ name: "John", age: 25 }, { name: "John", age: 40 }), + false + ) + }) + + it("get", () => { + strictEqual(pipe({ a: 1 }, Struct.get("a")), 1) + strictEqual(pipe({}, Struct.get("a")), undefined) + }) + + it("entries", () => { + const c = Symbol("c") + // should not include symbol keys + deepStrictEqual(Struct.entries({ a: "a", b: 1, [c]: 2 }), [["a", "a"], ["b", 1]]) + }) +}) diff --git a/repos/effect/packages/effect/test/SubscriptionRef.test.ts b/repos/effect/packages/effect/test/SubscriptionRef.test.ts new file mode 100644 index 0000000..099c371 --- /dev/null +++ b/repos/effect/packages/effect/test/SubscriptionRef.test.ts @@ -0,0 +1,96 @@ +import { describe, it } from "@effect/vitest" +import { assertTrue, deepStrictEqual } from "@effect/vitest/utils" +import { Chunk, Deferred, Effect, Equal, Exit, Fiber, Number, pipe, Random, Stream, SubscriptionRef } from "effect" + +describe("SubscriptionRef", () => { + it.effect("multiple subscribers can receive changes", () => + Effect.gen(function*() { + const subscriptionRef = yield* (SubscriptionRef.make(0)) + const deferred1 = yield* (Deferred.make()) + const deferred2 = yield* (Deferred.make()) + const subscriber1 = yield* pipe( + subscriptionRef.changes, + Stream.tap(() => Deferred.succeed(deferred1, void 0)), + Stream.take(3), + Stream.runCollect, + Effect.fork + ) + yield* (Deferred.await(deferred1)) + yield* (SubscriptionRef.update(subscriptionRef, (n) => n + 1)) + const subscriber2 = yield* pipe( + subscriptionRef.changes, + Stream.tap(() => Deferred.succeed(deferred2, void 0)), + Stream.take(2), + Stream.runCollect, + Effect.fork + ) + yield* (Deferred.await(deferred2)) + yield* (SubscriptionRef.update(subscriptionRef, (n) => n + 1)) + const result1 = yield* (Fiber.join(subscriber1)) + const result2 = yield* (Fiber.join(subscriber2)) + deepStrictEqual(Array.from(result1), [0, 1, 2]) + deepStrictEqual(Array.from(result2), [1, 2]) + })) + + it.effect("subscriptions are interruptible", () => + Effect.gen(function*() { + const subscriptionRef = yield* (SubscriptionRef.make(0)) + const deferred1 = yield* (Deferred.make()) + const deferred2 = yield* (Deferred.make()) + const subscriber1 = yield* pipe( + subscriptionRef.changes, + Stream.tap(() => Deferred.succeed(deferred1, void 0)), + Stream.take(5), + Stream.runCollect, + Effect.fork + ) + yield* (Deferred.await(deferred1)) + yield* (SubscriptionRef.update(subscriptionRef, (n) => n + 1)) + const subscriber2 = yield* pipe( + subscriptionRef.changes, + Stream.tap(() => Deferred.succeed(deferred2, void 0)), + Stream.take(2), + Stream.runCollect, + Effect.fork + ) + yield* (Deferred.await(deferred2)) + yield* (SubscriptionRef.update(subscriptionRef, (n) => n + 1)) + const result1 = yield* (Fiber.interrupt(subscriber1)) + const result2 = yield* (Fiber.join(subscriber2)) + assertTrue(Exit.isInterrupted(result1)) + deepStrictEqual(Array.from(result2), [1, 2]) + })) + + it.effect("concurrent subscribes and unsubscribes are handled correctly", () => + Effect.gen(function*() { + const subscriber = (subscriptionRef: SubscriptionRef.SubscriptionRef) => + pipe( + Random.nextIntBetween(0, 200), + Effect.flatMap((n) => + pipe( + subscriptionRef.changes, + Stream.take(n), + Stream.runCollect + ) + ) + ) + const subscriptionRef = yield* (SubscriptionRef.make(0)) + const fiber = yield* pipe( + SubscriptionRef.update(subscriptionRef, (n) => n + 1), + Effect.forever, + Effect.fork + ) + const result = yield* ( + Effect.map( + Effect.all( + Array.from({ length: 2 }, () => subscriber(subscriptionRef)), + { concurrency: 2 } + ), + Chunk.unsafeFromArray + ) + ) + yield* (Fiber.interrupt(fiber)) + const isSorted = Chunk.every(result, (chunk) => Equal.equals(chunk, Chunk.sort(chunk, Number.Order))) + assertTrue(isSorted) + })) +}) diff --git a/repos/effect/packages/effect/test/Symbol.test.ts b/repos/effect/packages/effect/test/Symbol.test.ts new file mode 100644 index 0000000..56f17dd --- /dev/null +++ b/repos/effect/packages/effect/test/Symbol.test.ts @@ -0,0 +1,19 @@ +import { describe, it } from "@effect/vitest" +import { assertFalse, assertTrue } from "@effect/vitest/utils" +import { Symbol as Sym } from "effect" + +describe("Symbol", () => { + it("isSymbol", () => { + assertTrue(Sym.isSymbol(Symbol.for("effect/test/a"))) + assertFalse(Sym.isSymbol(1n)) + assertFalse(Sym.isSymbol(1)) + assertFalse(Sym.isSymbol("a")) + assertFalse(Sym.isSymbol(true)) + }) + + it("Equivalence", () => { + const eq = Sym.Equivalence + assertTrue(eq(Symbol.for("effect/test/a"), Symbol.for("effect/test/a"))) + assertFalse(eq(Symbol.for("effect/test/a"), Symbol.for("effect/test/b"))) + }) +}) diff --git a/repos/effect/packages/effect/test/SynchronizedRef.test.ts b/repos/effect/packages/effect/test/SynchronizedRef.test.ts new file mode 100644 index 0000000..e1eafe9 --- /dev/null +++ b/repos/effect/packages/effect/test/SynchronizedRef.test.ts @@ -0,0 +1,105 @@ +import { describe, it } from "@effect/vitest" +import { deepStrictEqual, strictEqual } from "@effect/vitest/utils" +import { Deferred, Effect, Exit, Fiber, Option, pipe, SynchronizedRef } from "effect" + +const current = "value" +const update = "new value" +const failure = "failure" + +type State = Active | Changed | Closed + +interface Active { + readonly _tag: "Active" +} + +interface Changed { + readonly _tag: "Changed" +} + +interface Closed { + readonly _tag: "Closed" +} + +export const Active: State = { _tag: "Active" } +export const Changed: State = { _tag: "Changed" } +export const Closed: State = { _tag: "Closed" } + +const isActive = (self: State): boolean => self._tag === "Active" +const isChanged = (self: State): boolean => self._tag === "Changed" +const isClosed = (self: State): boolean => self._tag === "Closed" + +describe("SynchronizedRef", () => { + it.effect("get", () => + Effect.gen(function*() { + const result = yield* pipe(SynchronizedRef.make(current), Effect.flatMap(SynchronizedRef.get)) + strictEqual(result, current) + })) + it.effect("getAndUpdateEffect - happy path", () => + Effect.gen(function*() { + const ref = yield* SynchronizedRef.make(current) + const result1 = yield* SynchronizedRef.getAndUpdateEffect(ref, () => Effect.succeed(update)) + const result2 = yield* ref + strictEqual(result1, current) + strictEqual(result2, update) + })) + it.effect("getAndUpdateEffect - with failure", () => + Effect.gen(function*() { + const ref = yield* SynchronizedRef.make(current) + const result = yield* pipe(SynchronizedRef.getAndUpdateEffect(ref, (_) => Effect.fail(failure)), Effect.exit) + deepStrictEqual(result, Exit.fail(failure)) + })) + it.effect("getAndUpdateSomeEffect - happy path", () => + Effect.gen(function*() { + const ref = yield* SynchronizedRef.make(Active) + const result1 = yield* (SynchronizedRef.getAndUpdateSomeEffect(ref, (state) => + isClosed(state) ? + Option.some(Effect.succeed(Changed)) : + Option.none())) + const result2 = yield* SynchronizedRef.get(ref) + deepStrictEqual(result1, Active) + deepStrictEqual(result2, Active) + })) + it.effect("getAndUpdateSomeEffect - twice", () => + Effect.gen(function*() { + const ref = yield* SynchronizedRef.make(Active) + const result1 = yield* SynchronizedRef.getAndUpdateSomeEffect(ref, (state) => + isActive(state) ? + Option.some(Effect.succeed(Changed)) : + Option.none()) + const result2 = yield* SynchronizedRef.getAndUpdateSomeEffect(ref, (state) => + isClosed(state) + ? Option.some(Effect.succeed(Active)) + : isChanged(state) + ? Option.some(Effect.succeed(Closed)) + : Option.none()) + const result3 = yield* ref + deepStrictEqual(result1, Active) + deepStrictEqual(result2, Changed) + deepStrictEqual(result3, Closed) + })) + it.effect("getAndUpdateSomeEffect - with failure", () => + Effect.gen(function*() { + const ref = yield* SynchronizedRef.make(Active) + const result = yield* pipe( + SynchronizedRef.getAndUpdateSomeEffect(ref, (state) => + isActive(state) ? + Option.some(Effect.fail(failure)) : + Option.none()), + Effect.exit + ) + deepStrictEqual(result, Exit.fail(failure)) + })) + it.effect("getAndUpdateSomeEffect - interrupt parent fiber and update", () => + Effect.gen(function*() { + const deferred = yield* Deferred.make>() + const latch = yield* Deferred.make() + const makeAndWait = Deferred.complete(deferred, SynchronizedRef.make(Active)).pipe( + Effect.zipRight(Deferred.await(latch)) + ) + const fiber = yield* Effect.fork(makeAndWait) + const ref = yield* Deferred.await(deferred) + yield* Fiber.interrupt(fiber) + const result = yield* SynchronizedRef.updateAndGetEffect(ref, (_) => Effect.succeed(Closed)) + deepStrictEqual(result, Closed) + })) +}) diff --git a/repos/effect/packages/effect/test/TArray.test.ts b/repos/effect/packages/effect/test/TArray.test.ts new file mode 100644 index 0000000..253b11c --- /dev/null +++ b/repos/effect/packages/effect/test/TArray.test.ts @@ -0,0 +1,1240 @@ +import { describe, it } from "@effect/vitest" +import { + assertFalse, + assertLeft, + assertNone, + assertSome, + assertTrue, + deepStrictEqual, + strictEqual +} from "@effect/vitest/utils" +import { Cause, Chunk, Effect, Exit, Fiber, identity, Number, Option, pipe, STM, TArray, TRef } from "effect" +import { constFalse, constTrue } from "effect/Function" + +const largePrime = 223 + +const makeRepeats = (blocks: number, length: number): STM.STM> => + TArray.fromIterable(Array.from({ length: blocks * length }, (_, i) => (i % length) + 1)) + +const makeStair = (length: number): STM.STM> => + TArray.fromIterable(Array.from({ length }, (_, i) => i + 1)) + +const makeStairWithHoles = (length: number): STM.STM>> => + TArray.fromIterable(Array.from({ length }, (_, i) => i % 3 === 0 ? Option.none() : Option.some(i))) + +const makeTArray = (length: number, value: A): STM.STM> => + TArray.fromIterable(Array.from({ length }, () => value)) + +const valuesOf = (array: TArray.TArray): STM.STM> => + pipe(array, TArray.reduce, A>([], (acc, a) => [...acc, a])) + +describe("TArray", () => { + it.effect("collectFirst - finds and transforms correctly", () => + Effect.gen(function*() { + const n = 10 + const array = yield* (makeStairWithHoles(n)) + const result = yield* (pipe( + array, + TArray.collectFirst((option) => + Option.isSome(option) && option.value > 2 ? + Option.some(String(option.value)) : + Option.none() + ) + )) + assertSome(result, "4") + })) + + it.effect("collectFirst - succeeds for empty array", () => + Effect.gen(function*() { + const array = yield* (makeTArray>(0, Option.none())) + const result = yield* (pipe(array, TArray.collectFirst(identity))) + assertNone(result) + })) + + it.effect("collectFirst - fails to find absent", () => + Effect.gen(function*() { + const n = 10 + const array = yield* (makeStairWithHoles(n)) + const result = yield* (pipe( + array, + TArray.collectFirst((option) => + Option.isSome(option) && option.value > n ? + Option.some(String(option.value)) : + Option.none() + ) + )) + assertNone(result) + })) + + it.effect("collectFirst - is atomic", () => + Effect.gen(function*() { + const n = 1_000 + const array = yield* (makeStairWithHoles(n)) + const fiber = yield* (pipe( + array, + TArray.collectFirst((option) => + Option.isSome(option) && option.value % largePrime === 0 ? + Option.some(String(option.value)) : + Option.none() + ), + Effect.fork + )) + yield* (pipe( + Chunk.range(0, n - 1), + STM.forEach((n) => pipe(array, TArray.update(n, () => Option.some(1)))) + )) + const result = yield* (Fiber.join(fiber)) + assertTrue( + (Option.isSome(result) && result.value === String(largePrime)) || + Option.isNone(result) + ) + })) + + it.effect("collectFirstSTM - finds and transforms correctly", () => + Effect.gen(function*() { + const n = 10 + const array = yield* (makeStairWithHoles(n)) + const result = yield* (pipe( + array, + TArray.collectFirstSTM((option) => + Option.isSome(option) && option.value > 2 ? + Option.some(STM.succeed(String(option.value))) : + Option.none() + ) + )) + assertSome(result, "4") + })) + + it.effect("collectFirstSTM - succeeds for empty array", () => + Effect.gen(function*() { + const array = yield* (makeTArray>(0, Option.none())) + const result = yield* (pipe( + array, + TArray.collectFirstSTM((option) => + Option.isSome(option) ? + Option.some(STM.succeed(option.value)) : + Option.none() + ) + )) + assertNone(result) + })) + + it.effect("collectFirstSTM - fails to find absent", () => + Effect.gen(function*() { + const n = 10 + const array = yield* (makeStairWithHoles(n)) + const result = yield* (pipe( + array, + TArray.collectFirstSTM((option) => + Option.isSome(option) && option.value > n ? + Option.some(STM.succeed(String(option.value))) : + Option.none() + ) + )) + assertNone(result) + })) + + it.effect("collectFirstSTM - is atomic", () => + Effect.gen(function*() { + const n = 1_000 + const array = yield* (makeStairWithHoles(n)) + const fiber = yield* (pipe( + array, + TArray.collectFirstSTM((option) => + Option.isSome(option) && option.value % largePrime === 0 ? + Option.some(STM.succeed(String(option.value))) : + Option.none() + ), + Effect.fork + )) + yield* (pipe( + Chunk.range(0, n - 1), + STM.forEach((n) => pipe(array, TArray.update(n, () => Option.some(1)))) + )) + const result = yield* (Fiber.join(fiber)) + assertTrue( + (Option.isSome(result) && result.value === String(largePrime)) || + Option.isNone(result) + ) + })) + + it.effect("collectFirstSTM - fails on errors before result found", () => + Effect.gen(function*() { + const n = 10 + const array = yield* (makeStairWithHoles(n)) + const result = yield* (pipe( + array, + TArray.collectFirstSTM((option) => + Option.isSome(option) && option.value > 2 ? + Option.some(STM.succeed(String(option.value))) : + Option.some(STM.fail("boom")) + ), + STM.flip + )) + strictEqual(result, "boom") + })) + + it.effect("collectFirstSTM - succeeds on errors after result found", () => + Effect.gen(function*() { + const n = 10 + const array = yield* (makeStairWithHoles(n)) + const result = yield* (pipe( + array, + TArray.collectFirstSTM((option) => + Option.isSome(option) ? + option.value > 2 ? + Option.some(STM.succeed(String(option.value))) : + option.value === 7 ? + Option.some(STM.fail("boom")) : + Option.none() : + Option.none() + ) + )) + assertSome(result, "4") + })) + + it.effect("contains - true when in the array", () => + Effect.gen(function*() { + const n = 10 + const array = yield* (makeStair(n)) + const result = yield* (pipe(array, TArray.contains(3))) + assertTrue(result) + })) + + it.effect("contains - false when not in the array", () => + Effect.gen(function*() { + const n = 10 + const array = yield* (makeStair(n)) + const result = yield* (pipe(array, TArray.contains(n + 1))) + assertFalse(result) + })) + + it.effect("contains - false for empty array", () => + Effect.gen(function*() { + const array = yield* (TArray.empty()) + const result = yield* (pipe(array, TArray.contains(0))) + assertFalse(result) + })) + + it.effect("count - computes correct sum", () => + Effect.gen(function*() { + const n = 10 + const array = yield* (makeStair(n)) + const result = yield* (pipe(array, TArray.count((n) => n % 2 === 0))) + strictEqual(result, 5) + })) + + it.effect("count - zero when the predicate does not match", () => + Effect.gen(function*() { + const n = 10 + const array = yield* (makeStair(n)) + const result = yield* (pipe(array, TArray.count((i) => i > n))) + strictEqual(result, 0) + })) + + it.effect("count - zero for empty array", () => + Effect.gen(function*() { + const array = yield* (TArray.empty()) + const result = yield* (pipe(array, TArray.count(constTrue))) + strictEqual(result, 0) + })) + + it.effect("countSTM - computes correct sum", () => + Effect.gen(function*() { + const n = 10 + const array = yield* (makeStair(n)) + const result = yield* (pipe(array, TArray.countSTM((n) => STM.succeed(n % 2 === 0)))) + strictEqual(result, 5) + })) + + it.effect("countSTM - zero when the predicate does not match", () => + Effect.gen(function*() { + const n = 10 + const array = yield* (makeStair(n)) + const result = yield* (pipe(array, TArray.countSTM((i) => STM.succeed(i > n)))) + strictEqual(result, 0) + })) + + it.effect("countSTM - zero for empty array", () => + Effect.gen(function*() { + const array = yield* (TArray.empty()) + const result = yield* (pipe(array, TArray.countSTM(() => STM.succeed(true)))) + strictEqual(result, 0) + })) + + it.effect("every - detects satisfaction", () => + Effect.gen(function*() { + const n = 10 + const array = yield* (makeStair(n)) + const result = yield* (pipe(array, TArray.every((i) => i < n + 1))) + assertTrue(result) + })) + + it.effect("every - detects lack of satisfaction", () => + Effect.gen(function*() { + const n = 10 + const array = yield* (makeStair(n)) + const result = yield* (pipe(array, TArray.every((i) => i < n - 1))) + assertFalse(result) + })) + + it.effect("every - detects lack of satisfaction", () => + Effect.gen(function*() { + const array = yield* (TArray.empty()) + const result = yield* (pipe(array, TArray.every(constFalse))) + assertTrue(result) + })) + + it.effect("everySTM - detects satisfaction", () => + Effect.gen(function*() { + const n = 10 + const array = yield* (makeStair(n)) + const result = yield* (pipe(array, TArray.everySTM((i) => STM.succeed(i < n + 1)))) + assertTrue(result) + })) + + it.effect("everySTM - detects lack of satisfaction", () => + Effect.gen(function*() { + const n = 10 + const array = yield* (makeStair(n)) + const result = yield* (pipe(array, TArray.everySTM((i) => STM.succeed(i < n - 1)))) + assertFalse(result) + })) + + it.effect("everySTM - detects lack of satisfaction", () => + Effect.gen(function*() { + const array = yield* (TArray.empty()) + const result = yield* (pipe(array, TArray.everySTM(() => STM.succeed(false)))) + assertTrue(result) + })) + + it.effect("everySTM - fails for errors before counterexample", () => + Effect.gen(function*() { + const n = 10 + const array = yield* (makeStair(n)) + const result = yield* (pipe( + array, + TArray.everySTM((n) => n === 4 ? STM.fail("boom") : STM.succeed(n !== 5)), + STM.flip + )) + strictEqual(result, "boom") + })) + + it.effect("everySTM - fails for errors after counterexample", () => + Effect.gen(function*() { + const n = 10 + const array = yield* (makeStair(n)) + const result = yield* (pipe( + array, + TArray.everySTM((n) => n === 6 ? STM.fail("boom") : STM.succeed(n === 5)), + STM.flip + )) + strictEqual(result, "boom") + })) + + it.effect("get - happy path", () => + Effect.gen(function*() { + const result = yield* (pipe( + makeTArray(1, 42), + STM.flatMap(TArray.get(0)) + )) + strictEqual(result, 42) + })) + + it.effect("findFirst - is correct", () => + Effect.gen(function*() { + const n = 10 + const array = yield* (makeStair(n)) + const result = yield* (pipe(array, TArray.findFirst((n) => n % 5 === 0))) + assertSome(result, 5) + })) + + it.effect("findFirst - succeeds for empty array", () => + Effect.gen(function*() { + const array = yield* (TArray.empty()) + const result = yield* (pipe(array, TArray.findFirst(constTrue))) + assertNone(result) + })) + + it.effect("findFirst - fails to find absent", () => + Effect.gen(function*() { + const n = 10 + const array = yield* (makeStair(n)) + const result = yield* (pipe(array, TArray.findFirst((i) => i > n))) + assertNone(result) + })) + + it.effect("findFirst - is atomic", () => + Effect.gen(function*() { + const n = 1_000 + const array = yield* (makeStair(n)) + const fiber = yield* (pipe( + array, + TArray.findFirst((n) => n % largePrime === 0), + Effect.fork + )) + yield* (pipe( + Chunk.range(1, n - 1), + STM.forEach((n) => pipe(array, TArray.update(n, () => 1))) + )) + const result = yield* (Fiber.join(fiber)) + assertTrue( + (Option.isSome(result) && result.value === largePrime) || + Option.isNone(result) + ) + })) + + it.effect("findFirstIndex - correct index if in array", () => + Effect.gen(function*() { + const array = yield* (makeRepeats(3, 3)) + const result = yield* (pipe(array, TArray.findFirstIndex(2))) + assertSome(result, 1) + })) + + it.effect("findFirstIndex - none for empty array", () => + Effect.gen(function*() { + const array = yield* (TArray.empty()) + const result = yield* (pipe(array, TArray.findFirstIndex(1))) + assertNone(result) + })) + + it.effect("findFirstIndex - none if absent", () => + Effect.gen(function*() { + const array = yield* (makeRepeats(3, 3)) + const result = yield* (pipe(array, TArray.findFirstIndex(4))) + assertNone(result) + })) + + it.effect("findFirstIndexFrom - correct index if in array, with offset", () => + Effect.gen(function*() { + const array = yield* (makeRepeats(3, 3)) + const result = yield* (pipe(array, TArray.findFirstIndexFrom(2, 2))) + assertSome(result, 4) + })) + + it.effect("findFirstIndexFrom - none if absent after offset", () => + Effect.gen(function*() { + const array = yield* (makeRepeats(3, 3)) + const result = yield* (pipe(array, TArray.findFirstIndexFrom(1, 7))) + assertNone(result) + })) + + it.effect("findFirstIndexFrom - none for a negative offset", () => + Effect.gen(function*() { + const array = yield* (makeRepeats(3, 3)) + const result = yield* (pipe(array, TArray.findFirstIndexFrom(1, -1))) + assertNone(result) + })) + + it.effect("findFirstIndexFrom - none for an offset that is too large", () => + Effect.gen(function*() { + const array = yield* (makeRepeats(3, 3)) + const result = yield* (pipe(array, TArray.findFirstIndexFrom(1, 9))) + assertNone(result) + })) + + it.effect("findFirstIndexWhere - determines the correct index", () => + Effect.gen(function*() { + const n = 10 + const array = yield* (makeStair(n)) + const result = yield* (pipe(array, TArray.findFirstIndexWhere((n) => n % 5 === 0))) + assertSome(result, 4) + })) + + it.effect("findFirstIndexWhere - none for empty array", () => + Effect.gen(function*() { + const array = yield* (TArray.empty()) + const result = yield* (pipe(array, TArray.findFirstIndexWhere(constTrue))) + assertNone(result) + })) + + it.effect("findFirstIndexWhere - none for empty array", () => + Effect.gen(function*() { + const n = 10 + const array = yield* (makeStair(n)) + const result = yield* (pipe(array, TArray.findFirstIndexWhere((i) => i > n))) + assertNone(result) + })) + + it.effect("findFirstIndexWhere - is atomic", () => + Effect.gen(function*() { + const n = 1_000 + const array = yield* (makeStair(n)) + const fiber = yield* (pipe( + array, + TArray.findFirstIndexWhere((n) => n % largePrime === 0), + Effect.fork + )) + yield* (pipe( + Chunk.range(0, n - 1), + STM.forEach((n) => pipe(array, TArray.update(n, () => 1))) + )) + const result = yield* (Fiber.join(fiber)) + assertTrue( + (Option.isSome(result) && result.value === largePrime - 1) || + Option.isNone(result) + ) + })) + + it.effect("findFirstIndexWhereFrom - determines the correct index, with offset", () => + Effect.gen(function*() { + const n = 10 + const array = yield* (makeStair(n)) + const result = yield* (pipe(array, TArray.findFirstIndexWhereFrom((n) => n % 2 === 0, 5))) + assertSome(result, 5) + })) + + it.effect("findFirstIndexWhereFrom - none if absent after offset", () => + Effect.gen(function*() { + const n = 10 + const array = yield* (makeStair(n)) + const result = yield* (pipe(array, TArray.findFirstIndexWhereFrom((n) => n % 7 === 0, 7))) + assertNone(result) + })) + + it.effect("findFirstIndexWhereFrom - none for a negative offset", () => + Effect.gen(function*() { + const n = 10 + const array = yield* (makeStair(n)) + const result = yield* (pipe(array, TArray.findFirstIndexWhereFrom(constTrue, -1))) + assertNone(result) + })) + + it.effect("findFirstIndexWhereFrom - none for an offset that is too large", () => + Effect.gen(function*() { + const n = 10 + const array = yield* (makeStair(n)) + const result = yield* (pipe(array, TArray.findFirstIndexWhereFrom(constTrue, n + 1))) + assertNone(result) + })) + + it.effect("findFirstIndexWhereSTM - determines the correct index", () => + Effect.gen(function*() { + const n = 10 + const array = yield* (makeStair(n)) + const result = yield* (pipe(array, TArray.findFirstIndexWhereSTM((n) => STM.succeed(n % 5 === 0)))) + assertSome(result, 4) + })) + + it.effect("findFirstIndexWhereSTM - none for empty array", () => + Effect.gen(function*() { + const array = yield* (TArray.empty()) + const result = yield* (pipe(array, TArray.findFirstIndexWhereSTM(() => STM.succeed(true)))) + assertNone(result) + })) + + it.effect("findFirstIndexWhereSTM - none for empty array", () => + Effect.gen(function*() { + const n = 10 + const array = yield* (makeStair(n)) + const result = yield* (pipe(array, TArray.findFirstIndexWhereSTM((i) => STM.succeed(i > n)))) + assertNone(result) + })) + + it.effect("findFirstIndexWhereSTM - is atomic", () => + Effect.gen(function*() { + const n = 1_000 + const array = yield* (makeStair(n)) + const fiber = yield* (pipe( + array, + TArray.findFirstIndexWhereSTM((n) => STM.succeed(n % largePrime === 0)), + Effect.fork + )) + yield* (pipe( + Chunk.range(0, n - 1), + STM.forEach((n) => pipe(array, TArray.update(n, () => 1))) + )) + const result = yield* (Fiber.join(fiber)) + assertTrue( + (Option.isSome(result) && result.value === largePrime - 1) || + Option.isNone(result) + ) + })) + + it.effect("findFirstIndexWhereSTM - fails on errors before result found", () => + Effect.gen(function*() { + const n = 10 + const array = yield* (makeStair(n)) + const result = yield* (pipe( + array, + TArray.findFirstIndexWhereSTM((n) => n === 4 ? STM.fail("boom") : STM.succeed(n % 5 === 0)), + STM.flip + )) + strictEqual(result, "boom") + })) + + it.effect("findFirstIndexWhereSTM - succeeds on errors after result found", () => + Effect.gen(function*() { + const n = 10 + const array = yield* (makeStair(n)) + const result = yield* (pipe( + array, + TArray.findFirstIndexWhereSTM((n) => n === 6 ? STM.fail("boom") : STM.succeed(n % 5 === 0)) + )) + assertSome(result, 4) + })) + + it.effect("findFirstIndexWhereFromSTM - determines the correct index, with offset", () => + Effect.gen(function*() { + const n = 10 + const array = yield* (makeStair(n)) + const result = yield* (pipe(array, TArray.findFirstIndexWhereFromSTM((n) => STM.succeed(n % 2 === 0), 5))) + assertSome(result, 5) + })) + + it.effect("findFirstIndexWhereFromSTM - none if absent after offset", () => + Effect.gen(function*() { + const n = 10 + const array = yield* (makeStair(n)) + const result = yield* (pipe(array, TArray.findFirstIndexWhereFromSTM((n) => STM.succeed(n % 7 === 0), 7))) + assertNone(result) + })) + + it.effect("findFirstIndexWhereFromSTM - none for a negative offset", () => + Effect.gen(function*() { + const n = 10 + const array = yield* (makeStair(n)) + const result = yield* (pipe(array, TArray.findFirstIndexWhereFromSTM(() => STM.succeed(true), -1))) + assertNone(result) + })) + + it.effect("findFirstIndexWherFromeSTM - none for an offset that is too large", () => + Effect.gen(function*() { + const n = 10 + const array = yield* (makeStair(n)) + const result = yield* (pipe(array, TArray.findFirstIndexWhereFromSTM(() => STM.succeed(true), n + 1))) + assertNone(result) + })) + + it.effect("findFirstIndexWhereFromSTM - succeeds when error excluded by offset", () => + Effect.gen(function*() { + const n = 10 + const array = yield* (makeStair(n)) + const result = yield* (pipe( + array, + TArray.findFirstIndexWhereFromSTM((n) => + n === 1 + ? STM.fail("boom") + : STM.succeed(n % 5 === 0), 2) + )) + assertSome(result, 4) + })) + + it.effect("findFirstSTM - is correct", () => + Effect.gen(function*() { + const n = 10 + const array = yield* (makeStair(n)) + const result = yield* (pipe(array, TArray.findFirstSTM((n) => STM.succeed(n % 5 === 0)))) + assertSome(result, 5) + })) + + it.effect("findFirstSTM - succeeds for empty array", () => + Effect.gen(function*() { + const array = yield* (TArray.empty()) + const result = yield* (pipe(array, TArray.findFirstSTM(() => STM.succeed(true)))) + assertNone(result) + })) + + it.effect("findFirstSTM - fails to find absent", () => + Effect.gen(function*() { + const n = 10 + const array = yield* (makeStair(n)) + const result = yield* (pipe(array, TArray.findFirstSTM((i) => STM.succeed(i > n)))) + assertNone(result) + })) + + it.effect("findFirstSTM - is atomic", () => + Effect.gen(function*() { + const n = 1_000 + const array = yield* (makeStair(n)) + const fiber = yield* (pipe( + array, + TArray.findFirstSTM((n) => STM.succeed(n % largePrime === 0)), + Effect.fork + )) + yield* (pipe( + Chunk.range(1, n - 1), + STM.forEach((n) => pipe(array, TArray.update(n, () => 1))) + )) + const result = yield* (Fiber.join(fiber)) + assertTrue( + (Option.isSome(result) && result.value === largePrime) || + Option.isNone(result) + ) + })) + + it.effect("findFirstSTM - fails on errors before result found", () => + Effect.gen(function*() { + const n = 10 + const array = yield* (makeStair(n)) + const result = yield* (pipe( + array, + TArray.findFirstSTM((n) => n === 4 ? STM.fail("boom") : STM.succeed(n % 5 === 0)), + STM.flip + )) + strictEqual(result, "boom") + })) + + it.effect("findFirstSTM - succeeds on errors after result found", () => + Effect.gen(function*() { + const n = 10 + const array = yield* (makeStair(n)) + const result = yield* (pipe( + array, + TArray.findFirstSTM((n) => n === 6 ? STM.fail("boom") : STM.succeed(n % 5 === 0)) + )) + assertSome(result, 5) + })) + + it.effect("findLast - is correct", () => + Effect.gen(function*() { + const n = 10 + const array = yield* (makeStair(n)) + const result = yield* (pipe(array, TArray.findLast((n) => n % 5 === 0))) + assertSome(result, 10) + })) + + it.effect("findLast - succeeds for empty array", () => + Effect.gen(function*() { + const array = yield* (TArray.empty()) + const result = yield* (pipe(array, TArray.findLast(constTrue))) + assertNone(result) + })) + + it.effect("findLast - fails to find absent", () => + Effect.gen(function*() { + const n = 10 + const array = yield* (makeStair(n)) + const result = yield* (pipe(array, TArray.findLast((i) => i > n))) + assertNone(result) + })) + + it.effect("findLast - is atomic", () => + Effect.gen(function*() { + const n = 1_000 + const array = yield* (makeStair(n)) + const fiber = yield* (pipe( + array, + TArray.findLast((n) => n % largePrime === 0), + Effect.fork + )) + yield* (pipe( + Chunk.range(1, n - 1), + STM.forEach((n) => pipe(array, TArray.update(n, () => 1))) + )) + const result = yield* (Fiber.join(fiber)) + assertTrue( + (Option.isSome(result) && result.value === largePrime) || + Option.isNone(result) + ) + })) + + it.effect("findLastIndex - correct index if in array", () => + Effect.gen(function*() { + const array = yield* (makeRepeats(3, 3)) + const result = yield* (pipe(array, TArray.findLastIndex(2))) + assertSome(result, 7) + })) + + it.effect("findLastIndex - none for empty array", () => + Effect.gen(function*() { + const array = yield* (TArray.empty()) + const result = yield* (pipe(array, TArray.findLastIndex(1))) + assertNone(result) + })) + + it.effect("findLastIndex - none if absent", () => + Effect.gen(function*() { + const array = yield* (makeRepeats(3, 3)) + const result = yield* (pipe(array, TArray.findLastIndex(4))) + assertNone(result) + })) + + it.effect("findLastIndexFrom - correct index if in array, with limit", () => + Effect.gen(function*() { + const array = yield* (makeRepeats(3, 3)) + const result = yield* (pipe(array, TArray.findLastIndexFrom(2, 6))) + assertSome(result, 4) + })) + + it.effect("findLastIndexFrom - none if absent before limit", () => + Effect.gen(function*() { + const array = yield* (makeRepeats(3, 3)) + const result = yield* (pipe(array, TArray.findLastIndexFrom(3, 1))) + assertNone(result) + })) + + it.effect("findLastIndexFrom - none for a negative limit", () => + Effect.gen(function*() { + const array = yield* (makeRepeats(3, 3)) + const result = yield* (pipe(array, TArray.findLastIndexFrom(2, -1))) + assertNone(result) + })) + + it.effect("findLastIndexFrom - none for a limit that is too large", () => + Effect.gen(function*() { + const array = yield* (makeRepeats(3, 3)) + const result = yield* (pipe(array, TArray.findLastIndexFrom(2, 9))) + assertNone(result) + })) + + it.effect("findLastSTM - is correct", () => + Effect.gen(function*() { + const n = 10 + const array = yield* (makeStair(n)) + const result = yield* (pipe(array, TArray.findLastSTM((n) => STM.succeed(n % 5 === 0)))) + assertSome(result, 10) + })) + + it.effect("findLastSTM - succeeds for empty array", () => + Effect.gen(function*() { + const array = yield* (TArray.empty()) + const result = yield* (pipe(array, TArray.findLastSTM(() => STM.succeed(true)))) + assertNone(result) + })) + + it.effect("findLastSTM - fails to find absent", () => + Effect.gen(function*() { + const n = 10 + const array = yield* (makeStair(n)) + const result = yield* (pipe(array, TArray.findLastSTM((i) => STM.succeed(i > n)))) + assertNone(result) + })) + + it.effect("findLastSTM - is atomic", () => + Effect.gen(function*() { + const n = 1_000 + const array = yield* (makeStair(n)) + const fiber = yield* (pipe( + array, + TArray.findLastSTM((n) => STM.succeed(n % largePrime === 0)), + Effect.fork + )) + yield* (pipe( + Chunk.range(1, n - 1), + STM.forEach((n) => pipe(array, TArray.update(n, () => 1))) + )) + const result = yield* (Fiber.join(fiber)) + assertTrue( + (Option.isSome(result) && result.value === largePrime) || + Option.isNone(result) + ) + })) + + it.effect("findLastSTM - succeeds on errors before result found", () => + Effect.gen(function*() { + const n = 10 + const array = yield* (makeStair(n)) + const result = yield* (pipe( + array, + TArray.findLastSTM((n) => n === 4 ? STM.fail("boom") : STM.succeed(n % 7 === 0)) + )) + assertSome(result, 7) + })) + + it.effect("findLastSTM - fails on errors after result found", () => + Effect.gen(function*() { + const n = 10 + const array = yield* (makeStair(n)) + const result = yield* (pipe( + array, + TArray.findLastSTM((n) => n === 8 ? STM.fail("boom") : STM.succeed(n % 7 === 0)), + STM.flip + )) + strictEqual(result, "boom") + })) + + it.effect("forEach - side-effect is transactional", () => + Effect.gen(function*() { + const n = 10 + const ref = yield* (TRef.make(0)) + const array = yield* (makeTArray(n, 1)) + const fiber = yield* (pipe( + array, + TArray.forEach((n) => pipe(ref, TRef.update((i) => i + n))), + Effect.fork + )) + const result = yield* (TRef.get(ref)) + yield* (Fiber.join(fiber)) + assertTrue(result === 0 || result === n) + })) + + it.effect("get - should fail when the index is out of bounds", () => + Effect.gen(function*() { + const result = yield* (pipe( + makeTArray(1, 42), + STM.flatMap(TArray.get(-1)), + Effect.exit + )) + deepStrictEqual(result, Exit.die(new Cause.RuntimeException("Index out of bounds"))) + })) + + it.effect("headOption - retrieves the first item in the array", () => + Effect.gen(function*() { + const n = 10 + const array = yield* (makeStair(n)) + const result = yield* (TArray.headOption(array)) + assertSome(result, 1) + })) + + it.effect("headOption - is none for an empty array", () => + Effect.gen(function*() { + const array = yield* (TArray.empty()) + const result = yield* (TArray.headOption(array)) + assertNone(result) + })) + + it.effect("lastOption - retrieves the last entry", () => + Effect.gen(function*() { + const n = 10 + const array = yield* (makeStair(n)) + const result = yield* (TArray.lastOption(array)) + assertSome(result, n) + })) + + it.effect("lastOption - is none for an empty array", () => + Effect.gen(function*() { + const array = yield* (TArray.empty()) + const result = yield* (TArray.lastOption(array)) + assertNone(result) + })) + + it.effect("maxOption - computes correct maximum", () => + Effect.gen(function*() { + const n = 10 + const array = yield* (makeStair(n)) + const result = yield* (pipe(array, TArray.maxOption(Number.Order))) + assertSome(result, n) + })) + + it.effect("maxOption - returns none for an empty array", () => + Effect.gen(function*() { + const array = yield* (TArray.empty()) + const result = yield* (pipe(array, TArray.maxOption(Number.Order))) + assertNone(result) + })) + + it.effect("minOption - computes correct minimum", () => + Effect.gen(function*() { + const n = 10 + const array = yield* (makeStair(n)) + const result = yield* (pipe(array, TArray.minOption(Number.Order))) + assertSome(result, 1) + })) + + it.effect("minOption - returns none for an empty array", () => + Effect.gen(function*() { + const array = yield* (TArray.empty()) + const result = yield* (pipe(array, TArray.maxOption(Number.Order))) + assertNone(result) + })) + + it.effect("reduce - is atomic", () => + Effect.gen(function*() { + const n = 1_000 + const array = yield* (makeTArray(n, 0)) + const fiber = yield* (pipe(array, TArray.reduce(0, (x, y) => x + y), Effect.fork)) + yield* (pipe( + Chunk.range(0, n - 1), + STM.forEach((n) => pipe(array, TArray.update(n, (n) => n + 1))) + )) + const result = yield* (Fiber.join(fiber)) + assertTrue(result === 0 || result === n) + })) + + it.effect("reduceOption - reduces correctly", () => + Effect.gen(function*() { + const n = 10 + const array = yield* (makeStair(n)) + const result = yield* (pipe(array, TArray.reduceOption((x, y) => x + y))) + assertSome(result, (n * (n + 1)) / 2) + })) + + it.effect("reduceOption - single entry", () => + Effect.gen(function*() { + const array = yield* (makeTArray(1, 1)) + const result = yield* (pipe(array, TArray.reduceOption((x, y) => x + y))) + assertSome(result, 1) + })) + + it.effect("reduceOption - none for empty array", () => + Effect.gen(function*() { + const array = yield* (TArray.empty()) + const result = yield* (pipe(array, TArray.reduceOption((x, y) => x + y))) + assertNone(result) + })) + + it.effect("reduceOption - is atomic", () => + Effect.gen(function*() { + const n = 1_000 + const array = yield* (makeStair(n)) + const fiber = yield* (pipe(array, TArray.reduceOption((x, y) => x + y), Effect.fork)) + yield* (pipe( + Chunk.range(0, n - 1), + STM.forEach((n) => pipe(array, TArray.update(n, () => 1))) + )) + const result = yield* (Fiber.join(fiber)) + assertTrue( + Option.isSome(result) && + (result.value === (n * (n + 1)) / 2 || result.value === n) + ) + })) + + it.effect("reduceOptionSTM - reduces correctly", () => + Effect.gen(function*() { + const n = 10 + const array = yield* (makeStair(n)) + const result = yield* (pipe(array, TArray.reduceOptionSTM((x, y) => STM.succeed(x + y)))) + assertSome(result, (n * (n + 1)) / 2) + })) + + it.effect("reduceOptionSTM - single entry", () => + Effect.gen(function*() { + const array = yield* (makeTArray(1, 1)) + const result = yield* (pipe(array, TArray.reduceOptionSTM((x, y) => STM.succeed(x + y)))) + assertSome(result, 1) + })) + + it.effect("reduceOptionSTM - none for empty array", () => + Effect.gen(function*() { + const array = yield* (TArray.empty()) + const result = yield* (pipe(array, TArray.reduceOptionSTM((x, y) => STM.succeed(x + y)))) + assertNone(result) + })) + + it.effect("reduceOptionSTM - is atomic", () => + Effect.gen(function*() { + const n = 1_000 + const array = yield* (makeStair(n)) + const fiber = yield* (pipe(array, TArray.reduceOptionSTM((x, y) => STM.succeed(x + y)), Effect.fork)) + yield* (pipe( + Chunk.range(0, n - 1), + STM.forEach((n) => pipe(array, TArray.update(n, () => 1))) + )) + const result = yield* (Fiber.join(fiber)) + assertTrue( + Option.isSome(result) && + (result.value === (n * (n + 1)) / 2 || result.value === n) + ) + })) + + it.effect("reduceOptionSTM - fails on errors", () => + Effect.gen(function*() { + const n = 10 + const array = yield* (makeStair(n)) + const result = yield* (pipe( + array, + TArray.reduceOptionSTM((x, y) => y === 4 ? STM.fail("boom") : STM.succeed(x + y)), + STM.flip + )) + strictEqual(result, "boom") + })) + + it.effect("reduceSTM - is atomic", () => + Effect.gen(function*() { + const n = 1_000 + const array = yield* (makeTArray(n, 0)) + const fiber = yield* (pipe(array, TArray.reduceSTM(0, (x, y) => STM.succeed(x + y)), Effect.fork)) + yield* (pipe( + Chunk.range(0, n - 1), + STM.forEach((n) => pipe(array, TArray.update(n, (n) => n + 1))) + )) + const result = yield* (Fiber.join(fiber)) + assertTrue(result === 0 || result === n) + })) + + it.effect("reduceSTM - returns failures", () => + Effect.gen(function*() { + const n = 1_000 + const failInTheMiddle = (acc: number, n: number): STM.STM => + acc === Math.floor(n / 2) ? STM.fail("boom") : STM.succeed(acc + n) + const array = yield* (makeTArray(n, 1)) + const result = yield* (pipe(array, TArray.reduceSTM(0, failInTheMiddle), STM.either)) + assertLeft(result, "boom") + })) + + it.effect("size - returns the correct size", () => + Effect.gen(function*() { + const n = 10 + const array = yield* (makeStair(n)) + const result = TArray.size(array) + strictEqual(result, n) + })) + + it.effect("some - detects satisfaction", () => + Effect.gen(function*() { + const n = 10 + const array = yield* (makeStair(n)) + const result = yield* (pipe(array, TArray.some((n) => n % 2 === 0))) + assertTrue(result) + })) + + it.effect("some - detects lack of satisfaction", () => + Effect.gen(function*() { + const n = 10 + const array = yield* (makeStair(n)) + const result = yield* (pipe(array, TArray.some((n) => n % 11 === 0))) + assertFalse(result) + })) + + it.effect("some - false for empty array", () => + Effect.gen(function*() { + const array = yield* (TArray.empty()) + const result = yield* (pipe(array, TArray.some(constTrue))) + assertFalse(result) + })) + + it.effect("someSTM - detects satisfaction", () => + Effect.gen(function*() { + const n = 10 + const array = yield* (makeStair(n)) + const result = yield* (pipe(array, TArray.someSTM((n) => STM.succeed(n % 2 === 0)))) + assertTrue(result) + })) + + it.effect("someSTM - detects lack of satisfaction", () => + Effect.gen(function*() { + const n = 10 + const array = yield* (makeStair(n)) + const result = yield* (pipe(array, TArray.someSTM((n) => STM.succeed(n % 11 === 0)))) + assertFalse(result) + })) + + it.effect("someSTM - false for empty array", () => + Effect.gen(function*() { + const array = yield* (TArray.empty()) + const result = yield* (pipe(array, TArray.someSTM(() => STM.succeed(true)))) + assertFalse(result) + })) + + it.effect("someSTM - fails for errors before witness", () => + Effect.gen(function*() { + const n = 10 + const array = yield* (makeStair(n)) + const result = yield* (pipe( + array, + TArray.someSTM((n) => n === 4 ? STM.fail("boom") : STM.succeed(n === 5)), + STM.flip + )) + strictEqual(result, "boom") + })) + + it.effect("someSTM - fails for errors after witness", () => + Effect.gen(function*() { + const n = 10 + const array = yield* (makeStair(n)) + const result = yield* (pipe( + array, + TArray.someSTM((n) => n === 6 ? STM.fail("boom") : STM.succeed(n === 5)), + STM.flip + )) + strictEqual(result, "boom") + })) + + it.effect("transform - updates values atomically", () => + Effect.gen(function*() { + const n = 1_000 + const array = yield* (makeTArray(n, "a")) + const fiber = yield* (pipe(array, TArray.transform((s) => s + "+b"), Effect.fork)) + yield* (pipe( + Chunk.range(0, n - 1), + STM.forEach((n) => pipe(array, TArray.update(n, (s) => s + "+c"))) + )) + yield* (Fiber.join(fiber)) + const first = yield* (pipe(array, TArray.get(0))) + const last = yield* (pipe(array, TArray.get(n - 1))) + assertTrue( + (first === "a+b+c" && last === "a+b+c") || + (first === "a+c+b" && last === "a+c+b") + ) + })) + + it.effect("transformSTM - updates values atomically", () => + Effect.gen(function*() { + const n = 1_000 + const array = yield* (makeTArray(n, "a")) + const fiber = yield* (pipe(array, TArray.transformSTM((s) => STM.succeed(s + "+b")), Effect.fork)) + yield* (pipe( + Chunk.range(0, n - 1), + STM.forEach((n) => pipe(array, TArray.update(n, (s) => s + "+c"))) + )) + yield* (Fiber.join(fiber)) + const first = yield* (pipe(array, TArray.get(0))) + const last = yield* (pipe(array, TArray.get(n - 1))) + assertTrue( + (first === "a+b+c" && last === "a+b+c") || + (first === "a+c+b" && last === "a+c+b") + ) + })) + + it.effect("transformSTM - updates all or nothing", () => + Effect.gen(function*() { + const n = 1_000 + const array = yield* (makeTArray(n, 0)) + yield* (pipe(array, TArray.update(Math.floor(n / 2), () => 1))) + const result = yield* (pipe( + array, + TArray.transformSTM((n) => n === 0 ? STM.succeed(42) : STM.fail("boom")), + STM.either + )) + const first = yield* (pipe(array, TArray.get(0))) + strictEqual(first, 0) + assertLeft(result, "boom") + })) + + it.effect("update - happy path", () => + Effect.gen(function*() { + const array = yield* (makeTArray(1, 42)) + const result = yield* (pipe( + array, + TArray.update(0, (n) => -n), + STM.zipRight(valuesOf(array)) + )) + deepStrictEqual(result, [-42]) + })) + + it.effect("update - dies with index out of bounds", () => + Effect.gen(function*() { + const array = yield* (makeTArray(1, 42)) + const result = yield* (pipe(array, TArray.update(-1, identity), Effect.exit)) + deepStrictEqual(result, Exit.die(new Cause.RuntimeException("Index out of bounds"))) + })) + + it.effect("updateSTM - happy path", () => + Effect.gen(function*() { + const array = yield* (makeTArray(1, 42)) + const result = yield* (pipe( + array, + TArray.updateSTM(0, (n) => STM.succeed(-n)), + STM.zipRight(valuesOf(array)) + )) + deepStrictEqual(result, [-42]) + })) + + it.effect("updateSTM - dies with index out of bounds", () => + Effect.gen(function*() { + const array = yield* (makeTArray(1, 42)) + const result = yield* (pipe(array, TArray.updateSTM(-1, (n) => STM.succeed(n)), Effect.exit)) + deepStrictEqual(result, Exit.die(new Cause.RuntimeException("Index out of bounds"))) + })) + + it.effect("updateSTM - handles failures", () => + Effect.gen(function*() { + const n = 10 + const array = yield* (makeTArray(n, 0)) + const result = yield* (pipe( + array, + TArray.updateSTM(0, (_: number) => STM.fail("boom")), + STM.commit, + Effect.either + )) + assertLeft(result, "boom") + })) + + it.effect("toChunk", () => + Effect.gen(function*() { + const array = yield* (TArray.make(1, 2, 3, 4, 5)) + const result = yield* (TArray.toArray(array)) + deepStrictEqual(Array.from(result), [1, 2, 3, 4, 5]) + })) +}) diff --git a/repos/effect/packages/effect/test/TMap.test.ts b/repos/effect/packages/effect/test/TMap.test.ts new file mode 100644 index 0000000..f6ba1f7 --- /dev/null +++ b/repos/effect/packages/effect/test/TMap.test.ts @@ -0,0 +1,487 @@ +import { describe, it } from "@effect/vitest" +import { assertFalse, assertNone, assertSome, assertTrue, deepStrictEqual, strictEqual } from "@effect/vitest/utils" +import { Array, Chunk, Effect, Equal, Exit, FastCheck as fc, Hash, Option, pipe, STM, TMap } from "effect" +import { equivalentElements } from "./utils/equals.js" + +class HashContainer implements Equal.Equal { + constructor(readonly i: number) {} + [Hash.symbol](): number { + return this.i + } + [Equal.symbol](that: unknown): boolean { + return that instanceof HashContainer && this.i === that.i + } +} + +const mapEntriesArb: fc.Arbitrary> = fc.uniqueArray(fc.char()) + .chain((keys) => + fc.uniqueArray(fc.integer()) + .map((values) => pipe(keys, Array.zip(values))) + ) + +describe("TMap", () => { + it.effect("empty", () => + Effect.gen(function*() { + const transaction = pipe( + TMap.empty(), + STM.flatMap(TMap.isEmpty) + ) + const result = yield* (STM.commit(transaction)) + assertTrue(result) + })) + + it.effect("fromIterable", () => + Effect.gen(function*() { + const transaction = pipe( + TMap.fromIterable([["a", 1], ["b", 2], ["c", 2], ["b", 3]]), + STM.flatMap(TMap.toArray) + ) + const result = yield* (STM.commit(transaction)) + deepStrictEqual(result, [["a", 1], ["b", 3], ["c", 2]]) + })) + + it.effect("get - existing element", () => + Effect.gen(function*() { + const transaction = pipe( + TMap.make(["a", 1], ["b", 2]), + STM.flatMap(TMap.get("a")) + ) + const result = yield* (STM.commit(transaction)) + assertSome(result, 1) + })) + + it.effect("get - non-existing element", () => + Effect.gen(function*() { + const transaction = pipe( + TMap.empty(), + STM.flatMap(TMap.get("a")) + ) + const result = yield* (STM.commit(transaction)) + assertNone(result) + })) + + it.effect("getOrElse - existing element", () => + Effect.gen(function*() { + const transaction = pipe( + TMap.make(["a", 1], ["b", 2]), + STM.flatMap(TMap.getOrElse("a", () => 10)) + ) + const result = yield* (STM.commit(transaction)) + deepStrictEqual(result, 1) + })) + + it.effect("getOrElse - non-existing element", () => + Effect.gen(function*() { + const transaction = pipe( + TMap.empty(), + STM.flatMap(TMap.getOrElse("a", () => 10)) + ) + const result = yield* (STM.commit(transaction)) + deepStrictEqual(result, 10) + })) + + it.effect("has - existing element", () => + Effect.gen(function*() { + const transaction = pipe( + TMap.make(["a", 1], ["b", 2]), + STM.flatMap(TMap.has("a")) + ) + const result = yield* (STM.commit(transaction)) + assertTrue(result) + })) + + it.effect("has - non-existing element", () => + Effect.gen(function*() { + const transaction = pipe( + TMap.empty(), + STM.flatMap(TMap.has("a")) + ) + const result = yield* (STM.commit(transaction)) + assertFalse(result) + })) + + it("keys - collect all keys", () => + fc.assert(fc.asyncProperty(mapEntriesArb, async (entries) => { + const transaction = pipe( + TMap.fromIterable(entries), + STM.flatMap(TMap.keys) + ) + const result = await Effect.runPromise(STM.commit(transaction)) + const keys = entries.map((entry) => entry[0]) + strictEqual(pipe(result, Array.differenceWith(equivalentElements())(keys)).length, 0) + strictEqual(pipe(keys, Array.differenceWith(equivalentElements())(result)).length, 0) + }))) + + it.effect("merge", () => + Effect.gen(function*() { + const transaction = pipe( + TMap.make(["a", 1]), + STM.flatMap((map) => + STM.all({ + result1: pipe(map, TMap.merge("a", 2, (x, y) => x + y)), + result2: pipe(map, TMap.merge("b", 2, (x, y) => x + y)) + }) + ) + ) + const { result1, result2 } = yield* (STM.commit(transaction)) + strictEqual(result1, 3) + strictEqual(result2, 2) + })) + + it.effect("reduce - non-empty map", () => + Effect.gen(function*() { + const transaction = pipe( + TMap.make(["a", 1], ["b", 2], ["c", 3]), + STM.flatMap(TMap.reduce(0, (acc, value) => acc + value)) + ) + const result = yield* (STM.commit(transaction)) + strictEqual(result, 6) + })) + + it.effect("reduce - empty map", () => + Effect.gen(function*() { + const transaction = pipe( + TMap.empty(), + STM.flatMap(TMap.reduce(0, (acc, value) => acc + value)) + ) + const result = yield* (STM.commit(transaction)) + strictEqual(result, 0) + })) + + it.effect("reduceSTM - non-empty map", () => + Effect.gen(function*() { + const transaction = pipe( + TMap.make(["a", 1], ["b", 2], ["c", 3]), + STM.flatMap(TMap.reduceSTM(0, (acc, value) => STM.succeed(acc + value))) + ) + const result = yield* (STM.commit(transaction)) + strictEqual(result, 6) + })) + + it.effect("reduceSTM - empty map", () => + Effect.gen(function*() { + const transaction = pipe( + TMap.empty(), + STM.flatMap(TMap.reduceSTM(0, (acc, value) => STM.succeed(acc + value))) + ) + const result = yield* (STM.commit(transaction)) + strictEqual(result, 0) + })) + + it.effect("remove - remove an existing element", () => + Effect.gen(function*() { + const transaction = pipe( + TMap.make(["a", 1], ["b", 2]), + STM.tap(TMap.remove("a")), + STM.flatMap(TMap.get("a")) + ) + const result = yield* (STM.commit(transaction)) + assertNone(result) + })) + + it.effect("remove - remove an non-existing element", () => + Effect.gen(function*() { + const transaction = pipe( + TMap.empty(), + STM.tap(TMap.remove("a")), + STM.flatMap(TMap.get("a")) + ) + const result = yield* (STM.commit(transaction)) + assertNone(result) + })) + + it.effect("removeIf", () => + Effect.gen(function*() { + const transaction = STM.gen(function*() { + const map = yield* (TMap.make(["a", 1], ["aa", 2], ["aaa", 3])) + const removed = yield* (pipe(map, TMap.removeIf((_, value) => value > 1))) + const a = yield* (pipe(map, TMap.has("a"))) + const aa = yield* (pipe(map, TMap.has("aa"))) + const aaa = yield* (pipe(map, TMap.has("aaa"))) + return [removed, a, aa, aaa] as const + }) + const result = yield* (STM.commit(transaction)) + deepStrictEqual(result, [[["aaa", 3], ["aa", 2]], true, false, false]) + })) + + it.effect("removeIf > { discard: true }", () => + Effect.gen(function*() { + const transaction = STM.gen(function*() { + const map = yield* (TMap.make(["a", 1], ["aa", 2], ["aaa", 3])) + yield* (pipe(map, TMap.removeIf((key) => key === "aa", { discard: true }))) + const a = yield* (pipe(map, TMap.has("a"))) + const aa = yield* (pipe(map, TMap.has("aa"))) + const aaa = yield* (pipe(map, TMap.has("aaa"))) + return [a, aa, aaa] as const + }) + const result = yield* (STM.commit(transaction)) + deepStrictEqual(result, [true, false, true]) + })) + + it.effect("retainIf", () => + Effect.gen(function*() { + const transaction = STM.gen(function*() { + const map = yield* (TMap.make(["a", 1], ["aa", 2], ["aaa", 3])) + const removed = yield* (pipe(map, TMap.retainIf((key) => key === "aa"))) + const a = yield* (pipe(map, TMap.has("a"))) + const aa = yield* (pipe(map, TMap.has("aa"))) + const aaa = yield* (pipe(map, TMap.has("aaa"))) + return [removed, a, aa, aaa] as const + }) + const result = yield* (STM.commit(transaction)) + deepStrictEqual(result, [[["aaa", 3], ["a", 1]], false, true, false]) + })) + + it.effect("retainIf > { discard: true }", () => + Effect.gen(function*() { + const transaction = STM.gen(function*() { + const map = yield* (TMap.make(["a", 1], ["aa", 2], ["aaa", 3])) + yield* (pipe(map, TMap.retainIf((key) => key === "aa", { discard: true }))) + const a = yield* (pipe(map, TMap.has("a"))) + const aa = yield* (pipe(map, TMap.has("aa"))) + const aaa = yield* (pipe(map, TMap.has("aaa"))) + return [a, aa, aaa] as const + }) + const result = yield* (STM.commit(transaction)) + deepStrictEqual(result, [false, true, false]) + })) + + it.effect("set - adds new element", () => + Effect.gen(function*() { + const transaction = pipe( + TMap.empty(), + STM.tap(TMap.set("a", 1)), + STM.flatMap(TMap.get("a")) + ) + const result = yield* (STM.commit(transaction)) + assertSome(result, 1) + })) + + it.effect("set - overwrites an existing element", () => + Effect.gen(function*() { + const transaction = pipe( + TMap.make(["a", 1], ["b", 2]), + STM.tap(TMap.set("a", 10)), + STM.flatMap(TMap.get("a")) + ) + const result = yield* (STM.commit(transaction)) + assertSome(result, 10) + })) + + it.effect("set - add many keys with negative hash codes", () => + Effect.gen(function*() { + const entries = Array.makeBy(1_000, (i) => i + 1) + .map((i) => [new HashContainer(i), i] as const) + const transaction = pipe( + TMap.empty(), + STM.tap((map) => STM.all(entries.map((entry) => pipe(map, TMap.set(entry[0], entry[1]))))), + STM.flatMap(TMap.toArray) + ) + const result = yield* (STM.commit(transaction)) + strictEqual(pipe(result, Array.differenceWith(equivalentElements())(entries)).length, 0) + strictEqual(pipe(entries, Array.differenceWith(equivalentElements())(result)).length, 0) + })) + + it.effect("setIfAbsent", () => + Effect.gen(function*() { + const transaction = pipe( + TMap.make(["a", 1]), + STM.tap(TMap.setIfAbsent("b", 2)), + STM.tap(TMap.setIfAbsent("a", 10)), + STM.flatMap(TMap.toArray) + ) + const result = yield* (STM.commit(transaction)) + const expected = [["a", 1], ["b", 2]] + strictEqual(pipe(result, Array.differenceWith(equivalentElements())(expected)).length, 0) + strictEqual(pipe(expected, Array.differenceWith(equivalentElements())(result)).length, 0) + })) + + it.effect("size", () => + Effect.gen(function*() { + const transaction = pipe( + TMap.fromIterable([["a", 1], ["b", 2]]), + STM.flatMap(TMap.size) + ) + const result = yield* (STM.commit(transaction)) + strictEqual(result, 2) + })) + + it("toChunk - collect all elements", () => + fc.assert(fc.asyncProperty(mapEntriesArb, async (entries) => { + const transaction = pipe( + TMap.fromIterable(entries), + STM.flatMap(TMap.toChunk) + ) + const result = await Effect.runPromise(STM.commit(transaction)) + assertTrue(Chunk.isChunk(result)) + strictEqual(pipe(Chunk.toReadonlyArray(result), Array.differenceWith(equivalentElements())(entries)).length, 0) + strictEqual(pipe(entries, Array.differenceWith(equivalentElements())(Chunk.toReadonlyArray(result))).length, 0) + }))) + + it("toReadonlyArray - collect all elements", () => + fc.assert(fc.asyncProperty(mapEntriesArb, async (entries) => { + const transaction = pipe( + TMap.fromIterable(entries), + STM.flatMap(TMap.toArray) + ) + const result = await Effect.runPromise(STM.commit(transaction)) + strictEqual(pipe(result, Array.differenceWith(equivalentElements())(entries)).length, 0) + strictEqual(pipe(entries, Array.differenceWith(equivalentElements())(result)).length, 0) + }))) + + it("toMap - collect all elements", () => + fc.assert(fc.asyncProperty(mapEntriesArb, async (entries) => { + const transaction = pipe( + TMap.fromIterable(entries), + STM.flatMap(TMap.toMap) + ) + const result = await Effect.runPromise(STM.commit(transaction)) + strictEqual(pipe(Array.fromIterable(result), Array.differenceWith(equivalentElements())(entries)).length, 0) + strictEqual(pipe(entries, Array.differenceWith(equivalentElements())(Array.fromIterable(result))).length, 0) + }))) + + it.effect("transform", () => + Effect.gen(function*() { + const transaction = pipe( + TMap.make(["a", 1], ["aa", 2], ["aaa", 3]), + STM.tap((map) => TMap.transform(map, (key, value) => [key.replaceAll("a", "b"), value * 2])), + STM.flatMap(TMap.toArray) + ) + const result = yield* (STM.commit(transaction)) + const expected = [["b", 2], ["bb", 4], ["bbb", 6]] + strictEqual(pipe(result, Array.differenceWith(equivalentElements())(expected)).length, 0) + strictEqual(pipe(expected, Array.differenceWith(equivalentElements())(result)).length, 0) + })) + + it.effect("transform - handles keys with negative hash codes", () => + Effect.gen(function*() { + const transaction = pipe( + TMap.make([new HashContainer(-1), 1], [new HashContainer(-2), 2], [new HashContainer(-3), 3]), + STM.tap((map) => TMap.transform(map, (key, value) => [new HashContainer(key.i * -2), value * 2])), + STM.flatMap(TMap.toArray) + ) + const result = yield* (STM.commit(transaction)) + const expected = [[new HashContainer(2), 2], [new HashContainer(4), 4], [new HashContainer(6), 6]] + strictEqual(pipe(result, Array.differenceWith(equivalentElements())(expected)).length, 0) + strictEqual(pipe(expected, Array.differenceWith(equivalentElements())(result)).length, 0) + })) + + it.effect("transform - and shrink", () => + Effect.gen(function*() { + const transaction = pipe( + TMap.make(["a", 1], ["aa", 2], ["aaa", 3]), + STM.tap((map) => TMap.transform(map, (_, value) => ["key", value * 2])), + STM.flatMap(TMap.toArray) + ) + const result = yield* (STM.commit(transaction)) + deepStrictEqual(result, [["key", 6]]) + })) + + it.effect("transformSTM", () => + Effect.gen(function*() { + const transaction = pipe( + TMap.make(["a", 1], ["aa", 2], ["aaa", 3]), + STM.tap((map) => TMap.transformSTM(map, (key, value) => STM.succeed([key.replaceAll("a", "b"), value * 2]))), + STM.flatMap(TMap.toArray) + ) + const result = yield* (STM.commit(transaction)) + const expected = [["b", 2], ["bb", 4], ["bbb", 6]] + strictEqual(pipe(result, Array.differenceWith(equivalentElements())(expected)).length, 0) + strictEqual(pipe(expected, Array.differenceWith(equivalentElements())(result)).length, 0) + })) + + it.effect("transformSTM - and shrink", () => + Effect.gen(function*() { + const transaction = pipe( + TMap.make(["a", 1], ["aa", 2], ["aaa", 3]), + STM.tap((map) => TMap.transformSTM(map, (_, value) => STM.succeed(["key", value * 2]))), + STM.flatMap(TMap.toArray) + ) + const result = yield* (STM.commit(transaction)) + deepStrictEqual(result, [["key", 6]]) + })) + + it.effect("transformValues", () => + Effect.gen(function*() { + const transaction = pipe( + TMap.make(["a", 1], ["aa", 2], ["aaa", 3]), + STM.tap((map) => TMap.transformValues(map, (value) => value * 2)), + STM.flatMap(TMap.toArray) + ) + const result = yield* (STM.commit(transaction)) + const expected = [["a", 2], ["aa", 4], ["aaa", 6]] + strictEqual(pipe(result, Array.differenceWith(equivalentElements())(expected)).length, 0) + strictEqual(pipe(expected, Array.differenceWith(equivalentElements())(result)).length, 0) + })) + + it.effect("transformValues - parallel", () => + Effect.gen(function*() { + const map = yield* (TMap.make(["a", 0])) + const effect = pipe( + map, + TMap.transformValues((n) => n + 1), + STM.commit, + Effect.repeatN(999) + ) + yield* (Effect.replicateEffect(effect, 2)) + const result = yield* (pipe(map, TMap.get("a"))) + assertSome(result, 2_000) + })) + + it.effect("transformValuesSTM", () => + Effect.gen(function*() { + const transaction = pipe( + TMap.make(["a", 1], ["aa", 2], ["aaa", 3]), + STM.tap((map) => TMap.transformValuesSTM(map, (value) => STM.succeed(value * 2))), + STM.flatMap(TMap.toArray) + ) + const result = yield* (STM.commit(transaction)) + const expected = [["a", 2], ["aa", 4], ["aaa", 6]] + strictEqual(pipe(result, Array.differenceWith(equivalentElements())(expected)).length, 0) + strictEqual(pipe(expected, Array.differenceWith(equivalentElements())(result)).length, 0) + })) + + it.effect("updateWith", () => + Effect.gen(function*() { + const transaction = STM.gen(function*() { + const map = yield* (TMap.make(["a", 1], ["b", 2])) + yield* (pipe(map, TMap.updateWith("a", Option.map((n) => n + 1)))) + yield* (pipe(map, TMap.updateWith("b", () => Option.none()))) + yield* (pipe(map, TMap.updateWith("c", () => Option.some(3)))) + yield* (pipe(map, TMap.updateWith("d", () => Option.none()))) + return yield* (TMap.toArray(map)) + }) + const result = yield* (STM.commit(transaction)) + deepStrictEqual(result, [["a", 2], ["c", 3]]) + })) + + it("values - collect all values", () => + fc.assert(fc.asyncProperty(mapEntriesArb, async (entries) => { + const transaction = pipe( + TMap.fromIterable(entries), + STM.flatMap(TMap.values) + ) + const result = await Effect.runPromise(STM.commit(transaction)) + const values = entries.map((entry) => entry[1]) + strictEqual(pipe(result, Array.differenceWith(equivalentElements())(values)).length, 0) + strictEqual(pipe(values, Array.differenceWith(equivalentElements())(result)).length, 0) + }))) + + it.effect("avoid issues due to race conditions (ZIO Issue #4648)", () => + Effect.gen(function*() { + const keys = Array.range(0, 10) + const map = yield* (TMap.fromIterable(Array.map(keys, (n, i) => [n, i]))) + const result = yield* (pipe( + Effect.forEach(keys, (key) => + pipe( + TMap.remove(map, key), + STM.commit, + Effect.fork, + Effect.zipRight(TMap.toChunk(map)), + Effect.asVoid + ), { discard: true }), + Effect.exit + )) + deepStrictEqual(result, Exit.void) + })) +}) diff --git a/repos/effect/packages/effect/test/TPriorityQueue.test.ts b/repos/effect/packages/effect/test/TPriorityQueue.test.ts new file mode 100644 index 0000000..590ca92 --- /dev/null +++ b/repos/effect/packages/effect/test/TPriorityQueue.test.ts @@ -0,0 +1,194 @@ +import { describe, it } from "@effect/vitest" +import { assertTrue, deepStrictEqual, strictEqual } from "@effect/vitest/utils" +import { + Array as Arr, + Effect, + FastCheck as fc, + Number as number, + Option, + Order, + pipe, + STM, + TPriorityQueue +} from "effect" +import { equivalentElements } from "./utils/equals.js" + +interface Event { + readonly time: number + readonly description: string +} + +const orderByTime: Order.Order = pipe( + number.Order, + Order.mapInput((event) => event.time) +) + +const eventArb: fc.Arbitrary = fc.tuple( + fc.integer({ min: -10, max: 10 }), + fc.asciiString({ minLength: 1 }) +).map(([time, description]) => ({ time, description })) + +const eventsArb: fc.Arbitrary> = fc.array(eventArb) + +const predicateArb: fc.Arbitrary<(event: Event) => boolean> = fc.func(fc.boolean()).map((f) => (e: Event) => f(e)) + +describe("TPriorityQueue", () => { + it("isEmpty", () => + fc.assert(fc.asyncProperty(eventsArb, async (events) => { + const transaction = pipe( + TPriorityQueue.empty(orderByTime), + STM.tap(TPriorityQueue.offerAll(events)), + STM.flatMap(TPriorityQueue.isEmpty) + ) + const result = await Effect.runPromise(STM.commit(transaction)) + strictEqual(result, events.length === 0) + }))) + + it("isNonEmpty", () => + fc.assert(fc.asyncProperty(eventsArb, async (events) => { + const transaction = pipe( + TPriorityQueue.empty(orderByTime), + STM.tap(TPriorityQueue.offerAll(events)), + STM.flatMap(TPriorityQueue.isNonEmpty) + ) + const result = await Effect.runPromise(STM.commit(transaction)) + strictEqual(result, events.length > 0) + }))) + + it("offerAll and takeAll", () => + fc.assert(fc.asyncProperty(eventsArb, async (events) => { + const transaction = pipe( + TPriorityQueue.empty(orderByTime), + STM.tap(TPriorityQueue.offerAll(events)), + STM.flatMap(TPriorityQueue.takeAll), + STM.map((chunk) => Array.from(chunk)) + ) + const result = await Effect.runPromise(STM.commit(transaction)) + strictEqual(pipe(result, Arr.differenceWith(equivalentElements())(events)).length, 0) + strictEqual(pipe(events, Arr.differenceWith(equivalentElements())(result)).length, 0) + deepStrictEqual(result, pipe(result, Arr.sort(orderByTime))) + }))) + + it("removeIf", () => + fc.assert(fc.asyncProperty(eventsArb, predicateArb, async (events, f) => { + const transaction = pipe( + TPriorityQueue.fromIterable(orderByTime)(events), + STM.tap(TPriorityQueue.removeIf(f)), + STM.flatMap(TPriorityQueue.toArray) + ) + const result = await Effect.runPromise(STM.commit(transaction)) + const filtered = Arr.filter(events, (a) => !f(a)) + strictEqual(Arr.differenceWith(equivalentElements())(result, filtered).length, 0) + strictEqual(Arr.differenceWith(equivalentElements())(filtered, result).length, 0) + deepStrictEqual(result, Arr.sort(orderByTime)(result)) + }))) + + it("retainIf", () => + fc.assert(fc.asyncProperty(eventsArb, predicateArb, async (events, f) => { + const transaction = pipe( + TPriorityQueue.fromIterable(orderByTime)(events), + STM.tap(TPriorityQueue.retainIf(f)), + STM.flatMap(TPriorityQueue.toArray) + ) + const result = await Effect.runPromise(STM.commit(transaction)) + const filtered = Arr.filter(events, f) + strictEqual(Arr.differenceWith(equivalentElements())(result, filtered).length, 0) + strictEqual(Arr.differenceWith(equivalentElements())(filtered, result).length, 0) + deepStrictEqual(result, Arr.sort(orderByTime)(result)) + }))) + + it("take", () => + fc.assert(fc.asyncProperty(eventsArb, async (events) => { + const transaction = pipe( + TPriorityQueue.fromIterable(orderByTime)(events), + STM.flatMap((queue) => + STM.all(pipe( + TPriorityQueue.take(queue), + STM.replicate(events.length) + )) + ), + STM.map((chunk) => Array.from(chunk)) + ) + const result = await Effect.runPromise(STM.commit(transaction)) + strictEqual(pipe(result, Arr.differenceWith(equivalentElements())(events)).length, 0) + strictEqual(pipe(events, Arr.differenceWith(equivalentElements())(result)).length, 0) + deepStrictEqual(result, pipe(result, Arr.sort(orderByTime))) + }))) + + it("takeOption", () => + fc.assert( + fc.asyncProperty(eventsArb.filter((events) => events.length > 0), async (events) => { + const transaction = pipe( + TPriorityQueue.fromIterable(orderByTime)(events), + STM.flatMap((queue) => + pipe( + TPriorityQueue.takeOption(queue), + STM.tap(() => TPriorityQueue.takeAll(queue)), + STM.flatMap((left) => + pipe( + TPriorityQueue.takeOption(queue), + STM.map((right) => [left, right] as const) + ) + ) + ) + ) + ) + const result = await Effect.runPromise(STM.commit(transaction)) + assertTrue(Option.isSome(result[0])) + assertTrue(Option.isNone(result[1])) + }) + )) + + it("takeUpTo", () => + fc.assert( + fc.asyncProperty( + eventsArb.chain((events) => fc.tuple(fc.constant(events), fc.integer({ min: 0, max: events.length }))), + async ([events, n]) => { + const transaction = pipe( + TPriorityQueue.fromIterable(orderByTime)(events), + STM.flatMap((queue) => + pipe( + queue, + TPriorityQueue.takeUpTo(n), + STM.flatMap((left) => + pipe( + TPriorityQueue.takeAll(queue), + STM.map((right) => [...left, ...right]) + ) + ) + ) + ) + ) + const result = await Effect.runPromise(STM.commit(transaction)) + strictEqual(pipe(result, Arr.differenceWith(equivalentElements())(events)).length, 0) + strictEqual(pipe(events, Arr.differenceWith(equivalentElements())(result)).length, 0) + deepStrictEqual(result, pipe(result, Arr.sort(orderByTime))) + } + ) + )) + + it("toChunk", () => + fc.assert(fc.asyncProperty(eventsArb, async (events) => { + const transaction = pipe( + TPriorityQueue.fromIterable(orderByTime)(events), + STM.flatMap(TPriorityQueue.toChunk), + STM.map((chunk) => Array.from(chunk)) + ) + const result = await Effect.runPromise(STM.commit(transaction)) + strictEqual(pipe(result, Arr.differenceWith(equivalentElements())(events)).length, 0) + strictEqual(pipe(events, Arr.differenceWith(equivalentElements())(result)).length, 0) + deepStrictEqual(result, pipe(result, Arr.sort(orderByTime))) + }))) + + it("toReadonlyArray", () => + fc.assert(fc.asyncProperty(eventsArb, async (events) => { + const transaction = pipe( + TPriorityQueue.fromIterable(orderByTime)(events), + STM.flatMap(TPriorityQueue.toArray) + ) + const result = await Effect.runPromise(STM.commit(transaction)) + strictEqual(pipe(result, Arr.differenceWith(equivalentElements())(events)).length, 0) + strictEqual(pipe(events, Arr.differenceWith(equivalentElements())(result)).length, 0) + deepStrictEqual(result, pipe(result, Arr.sort(orderByTime))) + }))) +}) diff --git a/repos/effect/packages/effect/test/TPubSub.test.ts b/repos/effect/packages/effect/test/TPubSub.test.ts new file mode 100644 index 0000000..95c35cd --- /dev/null +++ b/repos/effect/packages/effect/test/TPubSub.test.ts @@ -0,0 +1,902 @@ +import { describe, it } from "@effect/vitest" +import { deepStrictEqual } from "@effect/vitest/utils" +import { + Array as Arr, + Deferred, + Effect, + FastCheck as fc, + Fiber, + Number as number, + pipe, + STM, + TPubSub, + TQueue +} from "effect" + +const sort: (array: ReadonlyArray) => ReadonlyArray = Arr.sort(number.Order) + +describe("TPubSub", () => { + it("sequential publishers and subscribers - with one publisher and one subscriber", () => + fc.assert(fc.asyncProperty(fc.integer({ min: 1 }), fc.array(fc.integer()), async (n, as) => { + const program = Effect.gen(function*() { + const deferred1 = yield* (Deferred.make()) + const deferred2 = yield* (Deferred.make()) + const pubsub = yield* (TPubSub.bounded(n)) + const subscriber = yield* (pipe( + TPubSub.subscribeScoped(pubsub), + Effect.flatMap((subscription) => + pipe( + deferred1, + Deferred.succeed(void 0), + Effect.zipRight(Deferred.await(deferred2)), + Effect.zipRight(pipe( + as.slice(0, n), + Effect.forEach(() => TQueue.take(subscription)) + )) + ) + ), + Effect.scoped, + Effect.fork + )) + yield* (Deferred.await(deferred1)) + yield* (pipe( + as.slice(0, n), + Effect.forEach((n) => pipe(pubsub, TPubSub.publish(n))) + )) + yield* (pipe(deferred2, Deferred.succeed(void 0))) + return yield* (Fiber.join(subscriber)) + }) + const result = await Effect.runPromise(program) + deepStrictEqual(Array.from(result), as.slice(0, n)) + }))) + + it("sequential publishers and subscribers - with one publisher and two subscribers", () => + fc.assert(fc.asyncProperty(fc.integer({ min: 1 }), fc.array(fc.integer()), async (n, as) => { + const program = Effect.gen(function*() { + const deferred1 = yield* (Deferred.make()) + const deferred2 = yield* (Deferred.make()) + const deferred3 = yield* (Deferred.make()) + const pubsub = yield* (TPubSub.bounded(n)) + const subscriber1 = yield* (pipe( + TPubSub.subscribeScoped(pubsub), + Effect.flatMap((subscription) => + pipe( + deferred1, + Deferred.succeed(void 0), + Effect.zipRight(Deferred.await(deferred3)), + Effect.zipRight(pipe( + as.slice(0, n), + Effect.forEach(() => TQueue.take(subscription)) + )) + ) + ), + Effect.scoped, + Effect.fork + )) + const subscriber2 = yield* (pipe( + TPubSub.subscribeScoped(pubsub), + Effect.flatMap((subscription) => + pipe( + deferred2, + Deferred.succeed(void 0), + Effect.zipRight(Deferred.await(deferred3)), + Effect.zipRight(pipe( + as.slice(0, n), + Effect.forEach(() => TQueue.take(subscription)) + )) + ) + ), + Effect.scoped, + Effect.fork + )) + yield* (Deferred.await(deferred1)) + yield* (Deferred.await(deferred2)) + yield* (pipe( + as.slice(0, n), + Effect.forEach((n) => pipe(pubsub, TPubSub.publish(n))) + )) + yield* (pipe(deferred3, Deferred.succeed(void 0))) + const result1 = yield* (Fiber.join(subscriber1)) + const result2 = yield* (Fiber.join(subscriber2)) + return { result1, result2 } + }) + const { result1, result2 } = await Effect.runPromise(program) + deepStrictEqual(Array.from(result1), as.slice(0, n)) + deepStrictEqual(Array.from(result2), as.slice(0, n)) + }))) + + it("concurrent publishers and subscribers - one to one", () => + fc.assert(fc.asyncProperty(fc.integer({ min: 1 }), fc.array(fc.integer()), async (n, as) => { + const program = Effect.gen(function*() { + const deferred = yield* (Deferred.make()) + const pubsub = yield* (TPubSub.bounded(n)) + const subscriber = yield* (pipe( + TPubSub.subscribeScoped(pubsub), + Effect.flatMap((subscription) => + pipe( + deferred, + Deferred.succeed(void 0), + Effect.zipRight(pipe(as.slice(0, n), Effect.forEach(() => TQueue.take(subscription)))) + ) + ), + Effect.fork + )) + yield* (Deferred.await(deferred)) + yield* (pipe( + as.slice(0, n), + Effect.forEach((n) => pipe(pubsub, TPubSub.publish(n))), + Effect.fork + )) + return yield* (Fiber.join(subscriber)) + }) + const result = await Effect.runPromise(Effect.scoped(program)) + deepStrictEqual(Array.from(result), as.slice(0, n)) + }))) + + it("concurrent publishers and subscribers - one to many", () => + fc.assert(fc.asyncProperty(fc.integer({ min: 1 }), fc.array(fc.integer()), async (n, as) => { + const program = Effect.gen(function*() { + const deferred1 = yield* (Deferred.make()) + const deferred2 = yield* (Deferred.make()) + const pubsub = yield* (TPubSub.bounded(n)) + const subscriber1 = yield* (pipe( + TPubSub.subscribeScoped(pubsub), + Effect.flatMap((subscription) => + pipe( + deferred1, + Deferred.succeed(void 0), + Effect.zipRight(pipe(as.slice(0, n), Effect.forEach(() => TQueue.take(subscription)))) + ) + ), + Effect.fork + )) + const subscriber2 = yield* (pipe( + TPubSub.subscribeScoped(pubsub), + Effect.flatMap((subscription) => + pipe( + deferred2, + Deferred.succeed(void 0), + Effect.zipRight(pipe(as.slice(0, n), Effect.forEach(() => TQueue.take(subscription)))) + ) + ), + Effect.fork + )) + yield* (Deferred.await(deferred1)) + yield* (Deferred.await(deferred2)) + yield* (pipe( + as.slice(0, n), + Effect.forEach((n) => pipe(pubsub, TPubSub.publish(n))), + Effect.fork + )) + const result1 = yield* (Fiber.join(subscriber1)) + const result2 = yield* (Fiber.join(subscriber2)) + return { result1, result2 } + }) + const { result1, result2 } = await Effect.runPromise(Effect.scoped(program)) + deepStrictEqual(Array.from(result1), as.slice(0, n)) + deepStrictEqual(Array.from(result2), as.slice(0, n)) + }))) + + it("concurrent publishers and subscribers - many to many", () => + fc.assert(fc.asyncProperty(fc.integer({ min: 1 }), fc.array(fc.integer({ min: 1 })), async (n, as) => { + const program = Effect.gen(function*() { + const deferred1 = yield* (Deferred.make()) + const deferred2 = yield* (Deferred.make()) + const pubsub = yield* (TPubSub.bounded(n * 2)) + const subscriber1 = yield* (pipe( + TPubSub.subscribeScoped(pubsub), + Effect.flatMap((subscription) => + pipe( + deferred1, + Deferred.succeed(void 0), + Effect.zipRight(pipe( + [...as, ...as].slice(0, n * 2), + Effect.forEach(() => TQueue.take(subscription)) + )) + ) + ), + Effect.scoped, + Effect.fork + )) + const subscriber2 = yield* (pipe( + TPubSub.subscribeScoped(pubsub), + Effect.flatMap((subscription) => + pipe( + deferred2, + Deferred.succeed(void 0), + Effect.zipRight(pipe( + [...as, ...as].slice(0, n * 2), + Effect.forEach(() => TQueue.take(subscription)) + )) + ) + ), + Effect.scoped, + Effect.fork + )) + yield* (Deferred.await(deferred1)) + yield* (Deferred.await(deferred2)) + yield* (pipe( + as.slice(0, n), + Effect.forEach((n) => pipe(pubsub, TPubSub.publish(n))), + Effect.fork + )) + yield* (pipe( + as.slice(0, n).map((n) => n !== 0 ? -n : n), + Effect.forEach((n) => pipe(pubsub, TPubSub.publish(n))), + Effect.fork + )) + const result1 = yield* (Fiber.join(subscriber1)) + const result2 = yield* (Fiber.join(subscriber2)) + return { result1, result2 } + }) + const { result1, result2 } = await Effect.runPromise(program) + deepStrictEqual( + Array.from(result1).filter((n) => n > 0), + as.slice(0, n) + ) + deepStrictEqual( + Array.from(result1).filter((n) => n < 0), + as.slice(0, n).map((n) => -n) + ) + deepStrictEqual( + Array.from(result2).filter((n) => n > 0), + as.slice(0, n) + ) + deepStrictEqual( + Array.from(result2).filter((n) => n < 0), + as.slice(0, n).map((n) => -n) + ) + }))) + + it("back pressure - one to one", () => + fc.assert(fc.asyncProperty(fc.integer({ min: 1 }), fc.array(fc.integer({ min: 1 })), async (n, as) => { + const program = Effect.gen(function*() { + const deferred = yield* (Deferred.make()) + const pubsub = yield* (TPubSub.bounded(n)) + const subscriber = yield* (pipe( + TPubSub.subscribeScoped(pubsub), + Effect.flatMap((subscription) => + pipe( + deferred, + Deferred.succeed(void 0), + Effect.zipRight(pipe(as, Effect.forEach(() => TQueue.take(subscription)))) + ) + ), + Effect.scoped, + Effect.fork + )) + yield* (Deferred.await(deferred)) + yield* (pipe(as, Effect.forEach((n) => pipe(pubsub, TPubSub.publish(n))), Effect.fork)) + return yield* (Fiber.join(subscriber)) + }) + const result = await Effect.runPromise(program) + deepStrictEqual(Array.from(result), as) + }))) + + it("back pressure - one to many", () => + fc.assert(fc.asyncProperty(fc.integer({ min: 1 }), fc.array(fc.integer({ min: 1 })), async (n, as) => { + const program = Effect.gen(function*() { + const deferred1 = yield* (Deferred.make()) + const deferred2 = yield* (Deferred.make()) + const pubsub = yield* (TPubSub.bounded(n)) + const subscriber1 = yield* (pipe( + TPubSub.subscribeScoped(pubsub), + Effect.flatMap((subscription) => + pipe( + deferred1, + Deferred.succeed(void 0), + Effect.zipRight(pipe(as, Effect.forEach(() => TQueue.take(subscription)))) + ) + ), + Effect.scoped, + Effect.fork + )) + const subscriber2 = yield* (pipe( + TPubSub.subscribeScoped(pubsub), + Effect.flatMap((subscription) => + pipe( + deferred2, + Deferred.succeed(void 0), + Effect.zipRight(pipe(as, Effect.forEach(() => TQueue.take(subscription)))) + ) + ), + Effect.scoped, + Effect.fork + )) + yield* (Deferred.await(deferred1)) + yield* (Deferred.await(deferred2)) + yield* (pipe(as, Effect.forEach((n) => pipe(pubsub, TPubSub.publish(n))), Effect.fork)) + const result1 = yield* (Fiber.join(subscriber1)) + const result2 = yield* (Fiber.join(subscriber2)) + return { result1, result2 } + }) + const { result1, result2 } = await Effect.runPromise(program) + deepStrictEqual(Array.from(result1), as) + deepStrictEqual(Array.from(result2), as) + }))) + + it("back pressure - many to many", () => + fc.assert(fc.asyncProperty(fc.integer({ min: 1 }), fc.array(fc.integer({ min: 1 })), async (n, as) => { + const program = Effect.gen(function*() { + const deferred1 = yield* (Deferred.make()) + const deferred2 = yield* (Deferred.make()) + const pubsub = yield* (TPubSub.bounded(n * 2)) + const subscriber1 = yield* (pipe( + TPubSub.subscribeScoped(pubsub), + Effect.flatMap((subscription) => + pipe( + deferred1, + Deferred.succeed(void 0), + Effect.zipRight(pipe( + [...as, ...as], + Effect.forEach(() => TQueue.take(subscription)) + )) + ) + ), + Effect.scoped, + Effect.fork + )) + const subscriber2 = yield* (pipe( + TPubSub.subscribeScoped(pubsub), + Effect.flatMap((subscription) => + pipe( + deferred2, + Deferred.succeed(void 0), + Effect.zipRight(pipe( + [...as, ...as], + Effect.forEach(() => TQueue.take(subscription)) + )) + ) + ), + Effect.scoped, + Effect.fork + )) + yield* (Deferred.await(deferred1)) + yield* (Deferred.await(deferred2)) + yield* (pipe( + as, + Effect.forEach((n) => pipe(pubsub, TPubSub.publish(n))), + Effect.fork + )) + yield* (pipe( + as.map((n) => -n), + Effect.forEach((n) => pipe(pubsub, TPubSub.publish(n))), + Effect.fork + )) + const result1 = yield* (Fiber.join(subscriber1)) + const result2 = yield* (Fiber.join(subscriber2)) + return { result1, result2 } + }) + const { result1, result2 } = await Effect.runPromise(program) + deepStrictEqual(Array.from(result1).filter((n) => n > 0), as) + deepStrictEqual(Array.from(result1).filter((n) => n < 0), as.map((n) => -n)) + deepStrictEqual(Array.from(result2).filter((n) => n > 0), as) + deepStrictEqual(Array.from(result2).filter((n) => n < 0), as.map((n) => -n)) + }))) + + it("dropping - one to one", () => + fc.assert(fc.asyncProperty(fc.integer({ min: 1 }), fc.array(fc.integer({ min: 1 })), async (n, as) => { + const program = Effect.gen(function*() { + const deferred = yield* (Deferred.make()) + const pubsub = yield* (TPubSub.dropping(n)) + const subscriber = yield* (pipe( + TPubSub.subscribeScoped(pubsub), + Effect.flatMap((subscription) => + pipe( + deferred, + Deferred.succeed(void 0), + Effect.zipRight(pipe( + as.slice(0, n), + Effect.forEach(() => TQueue.take(subscription)) + )) + ) + ), + Effect.scoped, + Effect.fork + )) + yield* (Deferred.await(deferred)) + yield* (pipe(as, Effect.forEach((n) => pipe(pubsub, TPubSub.publish(n))), Effect.fork)) + return yield* (Fiber.join(subscriber)) + }) + const result = await Effect.runPromise(program) + deepStrictEqual(Array.from(result), as.slice(0, n)) + }))) + + it("dropping - one to many", () => + fc.assert(fc.asyncProperty(fc.integer({ min: 1 }), fc.array(fc.integer({ min: 1 })), async (n, as) => { + const program = Effect.gen(function*() { + const deferred1 = yield* (Deferred.make()) + const deferred2 = yield* (Deferred.make()) + const pubsub = yield* (TPubSub.dropping(n)) + const subscriber1 = yield* (pipe( + TPubSub.subscribeScoped(pubsub), + Effect.flatMap((subscription) => + pipe( + deferred1, + Deferred.succeed(void 0), + Effect.zipRight(pipe( + as.slice(0, n), + Effect.forEach(() => TQueue.take(subscription)) + )) + ) + ), + Effect.scoped, + Effect.fork + )) + const subscriber2 = yield* (pipe( + TPubSub.subscribeScoped(pubsub), + Effect.flatMap((subscription) => + pipe( + deferred2, + Deferred.succeed(void 0), + Effect.zipRight(pipe( + as.slice(0, n), + Effect.forEach(() => TQueue.take(subscription)) + )) + ) + ), + Effect.scoped, + Effect.fork + )) + yield* (Deferred.await(deferred1)) + yield* (Deferred.await(deferred2)) + yield* (pipe(as, Effect.forEach((n) => pipe(pubsub, TPubSub.publish(n))), Effect.fork)) + const result1 = yield* (Fiber.join(subscriber1)) + const result2 = yield* (Fiber.join(subscriber2)) + return { result1, result2 } + }) + const { result1, result2 } = await Effect.runPromise(program) + deepStrictEqual(Array.from(result1), as.slice(0, n)) + deepStrictEqual(Array.from(result2), as.slice(0, n)) + }))) + + it("dropping - many to many", () => + fc.assert(fc.asyncProperty(fc.integer({ min: 1 }), fc.array(fc.integer({ min: 1 })), async (n, as) => { + const program = Effect.gen(function*() { + const deferred1 = yield* (Deferred.make()) + const deferred2 = yield* (Deferred.make()) + const pubsub = yield* (TPubSub.dropping(n * 2)) + const subscriber1 = yield* (pipe( + TPubSub.subscribeScoped(pubsub), + Effect.flatMap((subscription) => + pipe( + deferred1, + Deferred.succeed(void 0), + Effect.zipRight(pipe( + [...as, ...as].slice(0, n * 2), + Effect.forEach(() => TQueue.take(subscription)) + )) + ) + ), + Effect.scoped, + Effect.fork + )) + const subscriber2 = yield* (pipe( + TPubSub.subscribeScoped(pubsub), + Effect.flatMap((subscription) => + pipe( + deferred2, + Deferred.succeed(void 0), + Effect.zipRight(pipe( + [...as, ...as].slice(0, n * 2), + Effect.forEach(() => TQueue.take(subscription)) + )) + ) + ), + Effect.scoped, + Effect.fork + )) + yield* (Deferred.await(deferred1)) + yield* (Deferred.await(deferred2)) + yield* (pipe( + as, + Effect.forEach((n) => pipe(pubsub, TPubSub.publish(n))), + Effect.fork + )) + yield* (pipe( + as.map((n) => -n), + Effect.forEach((n) => pipe(pubsub, TPubSub.publish(n))), + Effect.fork + )) + const result1 = yield* (Fiber.join(subscriber1)) + const result2 = yield* (Fiber.join(subscriber2)) + return { result1, result2 } + }) + const { result1, result2 } = await Effect.runPromise(program) + deepStrictEqual( + Array.from(result1).filter((n) => n > 0), + as.slice(0, Array.from(result1).filter((n) => n > 0).length) + ) + deepStrictEqual( + Array.from(result1).filter((n) => n < 0), + as.slice(0, n).map((n) => -n).slice(0, Array.from(result1).filter((n) => n < 0).length) + ) + deepStrictEqual( + Array.from(result2).filter((n) => n > 0), + as.slice(0, Array.from(result2).filter((n) => n > 0).length) + ) + deepStrictEqual( + Array.from(result2).filter((n) => n < 0), + as.slice(0, n).map((n) => -n).slice(0, Array.from(result2).filter((n) => n < 0).length) + ) + }))) + + it("sliding - one to one", () => + fc.assert(fc.asyncProperty(fc.integer({ min: 1 }), fc.array(fc.integer({ min: 1 })), async (n, as) => { + const program = Effect.gen(function*() { + const deferred = yield* (Deferred.make()) + const pubsub = yield* (TPubSub.sliding(n)) + const subscriber = yield* (pipe( + TPubSub.subscribeScoped(pubsub), + Effect.flatMap((subscription) => + pipe( + deferred, + Deferred.succeed(void 0), + Effect.zipRight(pipe( + as.slice(0, n), + Effect.forEach(() => TQueue.take(subscription)) + )) + ) + ), + Effect.scoped, + Effect.fork + )) + yield* (Deferred.await(deferred)) + const publisher = yield* (pipe(sort(as), Effect.forEach((n) => pipe(pubsub, TPubSub.publish(n))), Effect.fork)) + yield* (Fiber.join(publisher)) + return yield* (Fiber.join(subscriber)) + }) + const result = await Effect.runPromise(program) + deepStrictEqual(Array.from(result), sort(Array.from(result))) + }))) + + it("sliding - one to many", () => + fc.assert(fc.asyncProperty(fc.integer({ min: 1 }), fc.array(fc.integer({ min: 1 })), async (n, as) => { + const program = Effect.gen(function*() { + const deferred1 = yield* (Deferred.make()) + const deferred2 = yield* (Deferred.make()) + const pubsub = yield* (TPubSub.sliding(n)) + const subscriber1 = yield* (pipe( + TPubSub.subscribeScoped(pubsub), + Effect.flatMap((subscription) => + pipe( + deferred1, + Deferred.succeed(void 0), + Effect.zipRight(pipe( + as.slice(0, n), + Effect.forEach(() => TQueue.take(subscription)) + )) + ) + ), + Effect.scoped, + Effect.fork + )) + const subscriber2 = yield* (pipe( + TPubSub.subscribeScoped(pubsub), + Effect.flatMap((subscription) => + pipe( + deferred2, + Deferred.succeed(void 0), + Effect.zipRight(pipe( + as.slice(0, n), + Effect.forEach(() => TQueue.take(subscription)) + )) + ) + ), + Effect.scoped, + Effect.fork + )) + yield* (Deferred.await(deferred1)) + yield* (Deferred.await(deferred2)) + yield* (pipe(sort(as), Effect.forEach((n) => pipe(pubsub, TPubSub.publish(n))), Effect.fork)) + const result1 = yield* (Fiber.join(subscriber1)) + const result2 = yield* (Fiber.join(subscriber2)) + return { result1, result2 } + }) + const { result1, result2 } = await Effect.runPromise(program) + deepStrictEqual(Array.from(result1), sort(Array.from(result1))) + deepStrictEqual(Array.from(result2), sort(Array.from(result2))) + }))) + + it("sliding - many to many", () => + fc.assert(fc.asyncProperty(fc.integer({ min: 1 }), fc.array(fc.integer({ min: 1 })), async (n, as) => { + const program = Effect.gen(function*() { + const deferred1 = yield* (Deferred.make()) + const deferred2 = yield* (Deferred.make()) + const pubsub = yield* (TPubSub.sliding(n * 2)) + const subscriber1 = yield* (pipe( + TPubSub.subscribeScoped(pubsub), + Effect.flatMap((subscription) => + pipe( + deferred1, + Deferred.succeed(void 0), + Effect.zipRight(pipe( + [...as, ...as].slice(0, n * 2), + Effect.forEach(() => TQueue.take(subscription)) + )) + ) + ), + Effect.scoped, + Effect.fork + )) + const subscriber2 = yield* (pipe( + TPubSub.subscribeScoped(pubsub), + Effect.flatMap((subscription) => + pipe( + deferred2, + Deferred.succeed(void 0), + Effect.zipRight(pipe( + [...as, ...as].slice(0, n * 2), + Effect.forEach(() => TQueue.take(subscription)) + )) + ) + ), + Effect.scoped, + Effect.fork + )) + yield* (Deferred.await(deferred1)) + yield* (Deferred.await(deferred2)) + yield* (pipe( + sort(as), + Effect.forEach((n) => pipe(pubsub, TPubSub.publish(n))), + Effect.fork + )) + yield* (pipe( + sort(as.map((n) => -n)), + Effect.forEach((n) => pipe(pubsub, TPubSub.publish(n))), + Effect.fork + )) + const result1 = yield* (Fiber.join(subscriber1)) + const result2 = yield* (Fiber.join(subscriber2)) + return { result1, result2 } + }) + const { result1, result2 } = await Effect.runPromise(program) + deepStrictEqual( + Array.from(result1).filter((n) => n > 0), + sort(Array.from(result1).filter((n) => n > 0)) + ) + deepStrictEqual( + Array.from(result1).filter((n) => n < 0), + sort(Array.from(result1).filter((n) => n < 0)) + ) + deepStrictEqual( + Array.from(result2).filter((n) => n > 0), + sort(Array.from(result2).filter((n) => n > 0)) + ) + deepStrictEqual( + Array.from(result2).filter((n) => n < 0), + sort(Array.from(result2).filter((n) => n < 0)) + ) + }))) + + it("unbounded - one to one", () => + fc.assert(fc.asyncProperty(fc.array(fc.integer({ min: 1 })), async (as) => { + const program = Effect.gen(function*() { + const deferred = yield* (Deferred.make()) + const pubsub = yield* (TPubSub.unbounded()) + const subscriber = yield* (pipe( + STM.commit(TPubSub.subscribe(pubsub)), + Effect.flatMap((subscription) => + pipe( + deferred, + Deferred.succeed(void 0), + Effect.zipRight(pipe(as, Effect.forEach(() => TQueue.take(subscription)))) + ) + ), + Effect.scoped, + Effect.fork + )) + yield* (Deferred.await(deferred)) + yield* (pipe(as, Effect.forEach((n) => pipe(pubsub, TPubSub.publish(n))), Effect.fork)) + return yield* (Fiber.join(subscriber)) + }) + const result = await Effect.runPromise(program) + deepStrictEqual(Array.from(result), as) + }))) + + it("unbounded - one to many", () => + fc.assert(fc.asyncProperty(fc.array(fc.integer({ min: 1 })), async (as) => { + const program = Effect.gen(function*() { + const deferred1 = yield* (Deferred.make()) + const deferred2 = yield* (Deferred.make()) + const pubsub = yield* (TPubSub.unbounded()) + const subscriber1 = yield* (pipe( + TPubSub.subscribeScoped(pubsub), + Effect.flatMap((subscription) => + pipe( + deferred1, + Deferred.succeed(void 0), + Effect.zipRight(pipe(as, Effect.forEach(() => TQueue.take(subscription)))) + ) + ), + Effect.scoped, + Effect.fork + )) + const subscriber2 = yield* (pipe( + TPubSub.subscribeScoped(pubsub), + Effect.flatMap((subscription) => + pipe( + deferred2, + Deferred.succeed(void 0), + Effect.zipRight(pipe(as, Effect.forEach(() => TQueue.take(subscription)))) + ) + ), + Effect.scoped, + Effect.fork + )) + yield* (Deferred.await(deferred1)) + yield* (Deferred.await(deferred2)) + yield* (pipe(as, Effect.forEach((n) => pipe(pubsub, TPubSub.publish(n))), Effect.fork)) + const result1 = yield* (Fiber.join(subscriber1)) + const result2 = yield* (Fiber.join(subscriber2)) + return { result1, result2 } + }) + const { result1, result2 } = await Effect.runPromise(program) + deepStrictEqual(Array.from(result1), as) + deepStrictEqual(Array.from(result2), as) + }))) + + it("unbounded - many to many", () => + fc.assert(fc.asyncProperty(fc.array(fc.integer({ min: 1 })), async (as) => { + const program = Effect.gen(function*() { + const deferred1 = yield* (Deferred.make()) + const deferred2 = yield* (Deferred.make()) + const pubsub = yield* (TPubSub.unbounded()) + const subscriber1 = yield* (pipe( + TPubSub.subscribeScoped(pubsub), + Effect.flatMap((subscription) => + pipe( + deferred1, + Deferred.succeed(void 0), + Effect.zipRight(pipe( + [...as, ...as], + Effect.forEach(() => TQueue.take(subscription)) + )) + ) + ), + Effect.scoped, + Effect.fork + )) + const subscriber2 = yield* (pipe( + TPubSub.subscribeScoped(pubsub), + Effect.flatMap((subscription) => + pipe( + deferred2, + Deferred.succeed(void 0), + Effect.zipRight(pipe( + [...as, ...as], + Effect.forEach(() => TQueue.take(subscription)) + )) + ) + ), + Effect.scoped, + Effect.fork + )) + yield* (Deferred.await(deferred1)) + yield* (Deferred.await(deferred2)) + yield* (pipe( + as, + Effect.forEach((n) => pipe(pubsub, TPubSub.publish(n))), + Effect.fork + )) + yield* (pipe( + as.map((n) => -n), + Effect.forEach((n) => pipe(pubsub, TPubSub.publish(n))), + Effect.fork + )) + const result1 = yield* (Fiber.join(subscriber1)) + const result2 = yield* (Fiber.join(subscriber2)) + return { result1, result2 } + }) + const { result1, result2 } = await Effect.runPromise(program) + deepStrictEqual(Array.from(result1).filter((n) => n > 0), as) + deepStrictEqual(Array.from(result1).filter((n) => n < 0), as.map((n) => -n)) + deepStrictEqual(Array.from(result2).filter((n) => n > 0), as) + deepStrictEqual(Array.from(result2).filter((n) => n < 0), as.map((n) => -n)) + }))) + + it.effect("unbounded - undefined/null values", () => + Effect.gen(function*() { + const as = [null, undefined, null, undefined] + const deferred1 = yield* (Deferred.make()) + const deferred2 = yield* (Deferred.make()) + const pubsub = yield* (TPubSub.unbounded()) + const subscriber1 = yield* (pipe( + TPubSub.subscribeScoped(pubsub), + Effect.flatMap((subscription) => + pipe( + deferred1, + Deferred.succeed(void 0), + Effect.zipRight(pipe(as, Effect.forEach(() => TQueue.take(subscription)))) + ) + ), + Effect.scoped, + Effect.fork + )) + const subscriber2 = yield* (pipe( + TPubSub.subscribeScoped(pubsub), + Effect.flatMap((subscription) => + pipe( + deferred2, + Deferred.succeed(void 0), + Effect.zipRight(pipe(as, Effect.forEach(() => TQueue.take(subscription)))) + ) + ), + Effect.scoped, + Effect.fork + )) + yield* (Deferred.await(deferred1)) + yield* (Deferred.await(deferred2)) + yield* (pipe(as, Effect.forEach((n) => pipe(pubsub, TPubSub.publish(n))), Effect.fork)) + const result1 = yield* (Fiber.join(subscriber1)) + const result2 = yield* (Fiber.join(subscriber2)) + deepStrictEqual(result1, as) + deepStrictEqual(result2, as) + })) + + it.effect("bounded - undefined/null values", () => + Effect.gen(function*() { + const as = [null, undefined] + const deferred1 = yield* (Deferred.make()) + const deferred2 = yield* (Deferred.make()) + const pubsub = yield* (TPubSub.bounded(2)) + const subscriber1 = yield* (pipe( + TPubSub.subscribeScoped(pubsub), + Effect.flatMap((subscription) => + pipe( + deferred1, + Deferred.succeed(void 0), + Effect.zipRight(pipe(as, Effect.forEach(() => TQueue.take(subscription)))) + ) + ), + Effect.scoped, + Effect.fork + )) + const subscriber2 = yield* (pipe( + TPubSub.subscribeScoped(pubsub), + Effect.flatMap((subscription) => + pipe( + deferred2, + Deferred.succeed(void 0), + Effect.zipRight(pipe(as, Effect.forEach(() => TQueue.take(subscription)))) + ) + ), + Effect.scoped, + Effect.fork + )) + yield* (Deferred.await(deferred1)) + yield* (Deferred.await(deferred2)) + yield* (pipe(as, Effect.forEach((n) => pipe(pubsub, TPubSub.publish(n))), Effect.fork)) + const result1 = yield* (Fiber.join(subscriber1)) + const result2 = yield* (Fiber.join(subscriber2)) + deepStrictEqual(result1, as) + deepStrictEqual(result2, as) + })) + + it.effect("dropping - undefined/null values", () => + Effect.gen(function*() { + const as = [null, undefined, null, undefined] + const n = 2 + const deferred = yield* (Deferred.make()) + const pubsub = yield* (TPubSub.dropping(n)) + const subscriber = yield* (pipe( + TPubSub.subscribeScoped(pubsub), + Effect.flatMap((subscription) => + pipe( + deferred, + Deferred.succeed(void 0), + Effect.zipRight(pipe( + as.slice(0, n), + Effect.forEach(() => TQueue.take(subscription)) + )) + ) + ), + Effect.scoped, + Effect.fork + )) + yield* (Deferred.await(deferred)) + yield* (pipe(as, Effect.forEach((n) => pipe(pubsub, TPubSub.publish(n))), Effect.fork)) + const result = yield* (Fiber.join(subscriber)) + deepStrictEqual(result, as.slice(0, n)) + })) +}) diff --git a/repos/effect/packages/effect/test/TQueue.test.ts b/repos/effect/packages/effect/test/TQueue.test.ts new file mode 100644 index 0000000..e62d73c --- /dev/null +++ b/repos/effect/packages/effect/test/TQueue.test.ts @@ -0,0 +1,167 @@ +import { describe, it } from "@effect/vitest" +import { assertFalse, assertNone, assertSome, assertTrue, deepStrictEqual, strictEqual } from "@effect/vitest/utils" +import { Effect, pipe, STM, TQueue } from "effect" + +describe("TQueue", () => { + it.effect("bounded", () => + Effect.gen(function*() { + const capacity = 5 + const result = yield* (pipe(TQueue.bounded(capacity), STM.map(TQueue.capacity))) + strictEqual(result, capacity) + })) + + it.effect("unbounded", () => + Effect.gen(function*() { + const result = yield* (pipe( + TQueue.unbounded(), + STM.map(TQueue.capacity), + STM.commit + )) + strictEqual(result, Number.MAX_SAFE_INTEGER) + })) + + it.effect("offer & take", () => + Effect.gen(function*() { + const queue = yield* (TQueue.bounded(5)) + yield* (pipe(queue, TQueue.offer(1))) + yield* (pipe(queue, TQueue.offer(2))) + yield* (pipe(queue, TQueue.offer(3))) + const result1 = yield* (TQueue.take(queue)) + const result2 = yield* (TQueue.take(queue)) + const result3 = yield* (TQueue.take(queue)) + strictEqual(result1, 1) + strictEqual(result2, 2) + strictEqual(result3, 3) + })) + + it.effect("offer & take undefined", () => + Effect.gen(function*() { + const queue = yield* (TQueue.bounded(5)) + yield* (pipe(queue, TQueue.offer(undefined))) + yield* (pipe(queue, TQueue.offer(undefined))) + const result1 = yield* (TQueue.take(queue)) + const result2 = yield* (TQueue.take(queue)) + strictEqual(result1, undefined) + strictEqual(result2, undefined) + })) + + it.effect("offerAll & takeAll", () => + Effect.gen(function*() { + const array = [1, 2, 3, 4, 5] + const queue = yield* (TQueue.bounded(5)) + yield* (pipe(queue, TQueue.offerAll(array))) + const result = yield* (TQueue.takeAll(queue)) + deepStrictEqual(Array.from(result), array) + })) + + it.effect("takeUpTo", () => + Effect.gen(function*() { + const queue = yield* (TQueue.bounded(5)) + yield* (pipe(queue, TQueue.offerAll([1, 2, 3, 4, 5]))) + const result = yield* (pipe(queue, TQueue.takeUpTo(3))) + const size = yield* (TQueue.size(queue)) + deepStrictEqual(Array.from(result), [1, 2, 3]) + strictEqual(size, 2) + })) + + it.effect("takeUpTo - larger than queue", () => + Effect.gen(function*() { + const array = [1, 2, 3, 4, 5] + const queue = yield* (TQueue.bounded(5)) + yield* (pipe(queue, TQueue.offerAll(array))) + const result = yield* (pipe(queue, TQueue.takeUpTo(7))) + const size = yield* (TQueue.size(queue)) + deepStrictEqual(Array.from(result), array) + strictEqual(size, 0) + })) + + it.effect("poll", () => + Effect.gen(function*() { + const queue = yield* (TQueue.bounded(5)) + yield* (pipe(queue, TQueue.offerAll([1, 2, 3]))) + const result = yield* (TQueue.poll(queue)) + assertSome(result, 1) + })) + + it.effect("poll undefined", () => + Effect.gen(function*() { + const queue = yield* (TQueue.bounded(5)) + yield* (pipe(queue, TQueue.offerAll([undefined, undefined, undefined]))) + const result = yield* (TQueue.poll(queue)) + assertSome(result, undefined) + })) + + it.effect("poll - empty queue", () => + Effect.gen(function*() { + const queue = yield* (TQueue.bounded(5)) + const result = yield* (TQueue.poll(queue)) + assertNone(result) + })) + + it.effect("seek", () => + Effect.gen(function*() { + const queue = yield* (TQueue.bounded(5)) + yield* (pipe(queue, TQueue.offerAll([1, 2, 3, 4, 5]))) + const result = yield* (pipe(queue, TQueue.seek((n) => n === 3))) + const size = yield* (TQueue.size(queue)) + strictEqual(result, 3) + strictEqual(size, 2) + })) + + it.effect("size", () => + Effect.gen(function*() { + const queue = yield* (TQueue.unbounded()) + yield* (pipe(queue, TQueue.offerAll([1, 2, 3, 4, 5]))) + const result = yield* (TQueue.size(queue)) + strictEqual(result, 5) + })) + + it.effect("peek", () => + Effect.gen(function*() { + const queue = yield* (TQueue.unbounded()) + yield* (pipe(queue, TQueue.offerAll([1, 2, 3, 4, 5]))) + const result = yield* (TQueue.peek(queue)) + const size = yield* (TQueue.size(queue)) + strictEqual(result, 1) + strictEqual(size, 5) + })) + + it.effect("peekOption", () => + Effect.gen(function*() { + const queue = yield* (TQueue.unbounded()) + yield* (pipe(queue, TQueue.offerAll([1, 2, 3, 4, 5]))) + const result = yield* (TQueue.peekOption(queue)) + const size = yield* (TQueue.size(queue)) + assertSome(result, 1) + strictEqual(size, 5) + })) + + it.effect("peekOption - empty queu", () => + Effect.gen(function*() { + const queue = yield* (TQueue.unbounded()) + const result = yield* (TQueue.peekOption(queue)) + assertNone(result) + })) + + it.effect("isEmpty", () => + Effect.gen(function*() { + const queue1 = yield* (TQueue.unbounded()) + const queue2 = yield* (TQueue.unbounded()) + yield* (pipe(queue1, TQueue.offerAll([1, 2, 3, 4, 5]))) + const result1 = yield* (TQueue.isEmpty(queue1)) + const result2 = yield* (TQueue.isEmpty(queue2)) + assertFalse(result1) + assertTrue(result2) + })) + + it.effect("isFull", () => + Effect.gen(function*() { + const queue1 = yield* (TQueue.bounded(5)) + const queue2 = yield* (TQueue.bounded(5)) + yield* (pipe(queue1, TQueue.offerAll([1, 2, 3, 4, 5]))) + const result1 = yield* (TQueue.isFull(queue1)) + const result2 = yield* (TQueue.isFull(queue2)) + assertTrue(result1) + assertFalse(result2) + })) +}) diff --git a/repos/effect/packages/effect/test/TRandom.test.ts b/repos/effect/packages/effect/test/TRandom.test.ts new file mode 100644 index 0000000..0f6914e --- /dev/null +++ b/repos/effect/packages/effect/test/TRandom.test.ts @@ -0,0 +1,42 @@ +import { describe, it } from "@effect/vitest" +import { assertTrue } from "@effect/vitest/utils" +import * as Effect from "effect/Effect" +import * as fc from "effect/FastCheck" +import { pipe } from "effect/Function" +import * as STM from "effect/STM" +import * as TRandom from "effect/TRandom" + +const floatsArb: fc.Arbitrary = fc.tuple( + fc.float({ noDefaultInfinity: true, noNaN: true }), + fc.float({ noDefaultInfinity: true, noNaN: true }) +) + .filter(([a, b]) => a !== b) + .map(([a, b]) => b > a ? [a, b] : [b, a]) + +const intsArb: fc.Arbitrary = fc.tuple(fc.integer(), fc.integer()) + .filter(([a, b]) => a !== b) + .map(([a, b]) => b > a ? [a, b] : [b, a]) + +describe("TRandom", () => { + it("nextIntBetween - generates integers in the specified range", () => + fc.assert(fc.asyncProperty(intsArb, async ([min, max]) => { + const result = await pipe( + STM.commit(TRandom.nextRange(min, max)), + Effect.provide(TRandom.live), + Effect.runPromise + ) + assertTrue(result >= min) + assertTrue(result < max) + }))) + + it("nextRange - generates numbers in the specified range", () => + fc.assert(fc.asyncProperty(floatsArb, async ([min, max]) => { + const result = await pipe( + STM.commit(TRandom.nextRange(min, max)), + Effect.provide(TRandom.live), + Effect.runPromise + ) + assertTrue(result >= min) + assertTrue(result < max) + }))) +}) diff --git a/repos/effect/packages/effect/test/TReentrantLock.test.ts b/repos/effect/packages/effect/test/TReentrantLock.test.ts new file mode 100644 index 0000000..a4be915 --- /dev/null +++ b/repos/effect/packages/effect/test/TReentrantLock.test.ts @@ -0,0 +1,231 @@ +import { describe, it } from "@effect/vitest" +import { assertNone, strictEqual } from "@effect/vitest/utils" +import * as Deferred from "effect/Deferred" +import * as Effect from "effect/Effect" +import type * as Exit from "effect/Exit" +import * as Fiber from "effect/Fiber" +import { pipe } from "effect/Function" +import * as Option from "effect/Option" +import * as Ref from "effect/Ref" +import * as Schedule from "effect/Schedule" +import * as STM from "effect/STM" +import * as TReentrantLock from "effect/TReentrantLock" + +const pollSchedule = (): Schedule.Schedule>, Option.Option>> => + pipe( + Schedule.recurs(100), + Schedule.zipRight( + pipe( + Schedule.identity>>(), + Schedule.whileOutput(Option.isNone) + ) + ) + ) + +describe("TReentrantLock", () => { + it.effect("one read lock", () => + Effect.gen(function*() { + const lock = yield* (TReentrantLock.make) + const result = yield* (pipe( + TReentrantLock.readLock(lock), + Effect.flatMap(Effect.succeed), + Effect.scoped + )) + strictEqual(result, 1) + })) + + it.effect("two read locks from the same fiber", () => + Effect.gen(function*() { + const lock = yield* (TReentrantLock.make) + const result = yield* (pipe( + TReentrantLock.readLock(lock), + Effect.flatMap(() => + pipe( + TReentrantLock.readLock(lock), + Effect.flatMap(Effect.succeed), + Effect.scoped + ) + ), + Effect.scoped + )) + strictEqual(result, 2) + })) + + it.effect("two read locks from different fibers", () => + Effect.gen(function*() { + const lock = yield* (TReentrantLock.make) + const rLatch = yield* (Deferred.make()) + const mLatch = yield* (Deferred.make()) + const wLatch = yield* (Deferred.make()) + yield* (pipe( + TReentrantLock.readLock(lock), + Effect.flatMap((count) => + pipe( + mLatch, + Deferred.succeed(void 0), + Effect.zipRight(Deferred.await(rLatch)), + Effect.as(count) + ) + ), + Effect.scoped, + Effect.fork + )) + yield* (Deferred.await(mLatch)) + const fiber = yield* (pipe( + TReentrantLock.readLock(lock), + Effect.flatMap((count) => + pipe( + wLatch, + Deferred.succeed(void 0), + Effect.as(count) + ) + ), + Effect.scoped, + Effect.fork + )) + yield* (Deferred.await(wLatch)) + const result = yield* (Fiber.join(fiber)) + strictEqual(result, 1) + })) + + it.effect("one write lock, then one read lock, different fibers", () => + Effect.gen(function*() { + const lock = yield* (TReentrantLock.make) + const rLatch = yield* (Deferred.make()) + const mLatch = yield* (Deferred.make()) + const wLatch = yield* (Deferred.make()) + yield* (pipe( + TReentrantLock.writeLock(lock), + Effect.flatMap((count) => + pipe( + rLatch, + Deferred.succeed(void 0), + Effect.zipRight(Deferred.await(wLatch)), + Effect.as(count) + ) + ), + Effect.scoped, + Effect.fork + )) + yield* (Deferred.await(rLatch)) + const fiber = yield* (pipe( + mLatch, + Deferred.succeed(void 0), + Effect.zipRight(Effect.scoped(TReentrantLock.readLock(lock))), + Effect.fork + )) + yield* (Deferred.await(mLatch)) + const locks = yield* (pipe( + TReentrantLock.readLocks(lock), + STM.zipWith(TReentrantLock.writeLocks(lock), (x, y) => x + y), + STM.commit + )) + const option = yield* (pipe( + Fiber.poll(fiber), + Effect.repeat(pollSchedule()) + )) + yield* (pipe(wLatch, Deferred.succeed(void 0))) + const readerCount = yield* (Fiber.join(fiber)) + strictEqual(locks, 1) + assertNone(option) + strictEqual(readerCount, 1) + })) + + it.effect("write lock followed by read lock from the same fiber", () => + Effect.gen(function*() { + const lock = yield* (TReentrantLock.make) + const ref = yield* (Ref.make(0)) + const readerCount = yield* (pipe( + TReentrantLock.writeLock(lock), + Effect.flatMap(() => + pipe( + TReentrantLock.readLock(lock), + Effect.flatMap((count) => + pipe( + STM.commit(TReentrantLock.writeLocks(lock)), + Effect.flatMap((n) => pipe(ref, Ref.set(n))), + Effect.as(count) + ) + ), + Effect.scoped + ) + ), + Effect.scoped + )) + const writerCount = yield* (Ref.get(ref)) + strictEqual(readerCount, 1) + strictEqual(writerCount, 1) + })) + + it.effect("upgrade read lock to write lock from the same fiber", () => + Effect.gen(function*() { + const lock = yield* (TReentrantLock.make) + const ref = yield* (Ref.make(0)) + const readerCount = yield* (pipe( + TReentrantLock.readLock(lock), + Effect.flatMap(() => + pipe( + TReentrantLock.writeLock(lock), + Effect.flatMap((count) => + pipe( + TReentrantLock.writeLocks(lock), + Effect.flatMap((n) => pipe(ref, Ref.set(n))), + Effect.as(count) + ) + ), + Effect.scoped + ) + ), + Effect.scoped + )) + const writerCount = yield* (Ref.get(ref)) + strictEqual(readerCount, 1) + strictEqual(writerCount, 1) + })) + + it.effect("read to writer upgrade with other readers", () => + Effect.gen(function*() { + const lock = yield* (TReentrantLock.make) + const rLatch = yield* (Deferred.make()) + const mLatch = yield* (Deferred.make()) + const wLatch = yield* (Deferred.make()) + yield* (pipe( + TReentrantLock.readLock(lock), + Effect.flatMap((count) => + pipe( + mLatch, + Deferred.succeed(void 0), + Effect.zipRight(Deferred.await(rLatch)), + Effect.as(count) + ) + ), + Effect.scoped, + Effect.fork + )) + yield* (Deferred.await(mLatch)) + const fiber = yield* (pipe( + TReentrantLock.readLock(lock), + Effect.flatMap(() => + pipe( + wLatch, + Deferred.succeed(void 0), + Effect.zipRight( + pipe( + TReentrantLock.writeLock(lock), + Effect.flatMap(Effect.succeed), + Effect.scoped + ) + ) + ) + ), + Effect.scoped, + Effect.fork + )) + yield* (Deferred.await(wLatch)) + const option = yield* (pipe(Fiber.poll(fiber), Effect.repeat(pollSchedule()))) + yield* (pipe(rLatch, Deferred.succeed(void 0))) + const count = yield* (Fiber.join(fiber)) + assertNone(option) + strictEqual(count, 1) + })) +}) diff --git a/repos/effect/packages/effect/test/TSet.test.ts b/repos/effect/packages/effect/test/TSet.test.ts new file mode 100644 index 0000000..b5e2606 --- /dev/null +++ b/repos/effect/packages/effect/test/TSet.test.ts @@ -0,0 +1,299 @@ +import { describe, it } from "@effect/vitest" +import { assertFalse, assertTrue, deepStrictEqual, strictEqual } from "@effect/vitest/utils" +import * as Chunk from "effect/Chunk" +import * as Effect from "effect/Effect" +import { pipe } from "effect/Function" +import * as STM from "effect/STM" +import * as TSet from "effect/TSet" + +describe("TSet", () => { + it.effect("add - new element", () => + Effect.gen(function*() { + const transaction = pipe( + TSet.empty(), + STM.tap(TSet.add(1)), + STM.flatMap(TSet.toReadonlySet) + ) + const result = yield* (STM.commit(transaction)) + deepStrictEqual(result, new Set([1])) + })) + + it.effect("add - duplicate element", () => + Effect.gen(function*() { + const transaction = pipe( + TSet.make(1), + STM.tap(TSet.add(1)), + STM.flatMap(TSet.toReadonlySet) + ) + const result = yield* (STM.commit(transaction)) + deepStrictEqual(result, new Set([1])) + })) + + it.effect("difference", () => + Effect.gen(function*() { + const transaction = STM.gen(function*() { + const set1 = yield* (TSet.make(1, 2, 3)) + const set2 = yield* (TSet.make(1, 4, 5)) + yield* (pipe(set1, TSet.difference(set2))) + return yield* (TSet.toArray(set1)) + }) + const result = yield* (STM.commit(transaction)) + deepStrictEqual(result, [2, 3]) + })) + + it.effect("empty", () => + Effect.gen(function*() { + const transaction = pipe( + TSet.empty(), + STM.flatMap(TSet.isEmpty) + ) + const result = yield* (STM.commit(transaction)) + assertTrue(result) + })) + + it.effect("fromIterable", () => + Effect.gen(function*() { + const transaction = pipe( + TSet.fromIterable([1, 2, 2, 3]), + STM.flatMap(TSet.toReadonlySet) + ) + const result = yield* (STM.commit(transaction)) + deepStrictEqual(result, new Set([1, 2, 3])) + })) + + it.effect("has - existing element", () => + Effect.gen(function*() { + const transaction = pipe( + TSet.make(1, 2, 3, 4), + STM.flatMap(TSet.has(1)) + ) + const result = yield* (STM.commit(transaction)) + assertTrue(result) + })) + + it.effect("has - non-existing element", () => + Effect.gen(function*() { + const transaction = pipe( + TSet.empty(), + STM.flatMap(TSet.has(1)) + ) + const result = yield* (STM.commit(transaction)) + assertFalse(result) + })) + + it.effect("intersection", () => + Effect.gen(function*() { + const transaction = STM.gen(function*() { + const set1 = yield* (TSet.make(1, 2, 3)) + const set2 = yield* (TSet.make(1, 4, 5)) + yield* (pipe(set1, TSet.intersection(set2))) + return yield* (TSet.toArray(set1)) + }) + const result = yield* (STM.commit(transaction)) + deepStrictEqual(result, [1]) + })) + + it.effect("make", () => + Effect.gen(function*() { + const transaction = pipe( + TSet.make(1, 2, 2, 3), + STM.flatMap(TSet.toReadonlySet) + ) + const result = yield* (STM.commit(transaction)) + deepStrictEqual(result, new Set([1, 2, 3])) + })) + + it.effect("reduce - non-empty set", () => + Effect.gen(function*() { + const transaction = pipe( + TSet.make(1, 2, 3), + STM.flatMap(TSet.reduce(0, (x, y) => x + y)) + ) + const result = yield* (STM.commit(transaction)) + strictEqual(result, 6) + })) + + it.effect("reduce - empty set", () => + Effect.gen(function*() { + const transaction = pipe( + TSet.empty(), + STM.flatMap(TSet.reduce(0, (x, y) => x + y)) + ) + const result = yield* (STM.commit(transaction)) + strictEqual(result, 0) + })) + + it.effect("reduceSTM - non-empty set", () => + Effect.gen(function*() { + const transaction = pipe( + TSet.make(1, 2, 3), + STM.flatMap(TSet.reduceSTM(0, (x, y) => STM.succeed(x + y))) + ) + const result = yield* (STM.commit(transaction)) + strictEqual(result, 6) + })) + + it.effect("reduceSTM - empty set", () => + Effect.gen(function*() { + const transaction = pipe( + TSet.empty(), + STM.flatMap(TSet.reduceSTM(0, (x, y) => STM.succeed(x + y))) + ) + const result = yield* (STM.commit(transaction)) + strictEqual(result, 0) + })) + + it.effect("remove - existing element", () => + Effect.gen(function*() { + const transaction = pipe( + TSet.make(1, 2), + STM.tap(TSet.remove(1)), + STM.flatMap(TSet.toReadonlySet) + ) + const result = yield* (STM.commit(transaction)) + deepStrictEqual(result, new Set([2])) + })) + + it.effect("remove - non-existing element", () => + Effect.gen(function*() { + const transaction = pipe( + TSet.make(1, 2), + STM.tap(TSet.remove(3)), + STM.flatMap(TSet.toReadonlySet) + ) + const result = yield* (STM.commit(transaction)) + deepStrictEqual(result, new Set([1, 2])) + })) + + it.effect("retainIf", () => + Effect.gen(function*() { + const transaction = STM.gen(function*() { + const set = yield* (TSet.make("a", "aa", "aaa")) + const removed = yield* (pipe(set, TSet.retainIf((s) => s === "aa"))) + const a = yield* (pipe(set, TSet.has("a"))) + const aa = yield* (pipe(set, TSet.has("aa"))) + const aaa = yield* (pipe(set, TSet.has("aaa"))) + return [Array.from(removed), a, aa, aaa] + }) + const result = yield* (STM.commit(transaction)) + deepStrictEqual(result, [["aaa", "a"], false, true, false]) + })) + + it.effect("retainIf > { discard: true }", () => + Effect.gen(function*() { + const transaction = STM.gen(function*() { + const set = yield* (TSet.make("a", "aa", "aaa")) + yield* (pipe(set, TSet.retainIf((s) => s === "aa", { discard: true }))) + const a = yield* (pipe(set, TSet.has("a"))) + const aa = yield* (pipe(set, TSet.has("aa"))) + const aaa = yield* (pipe(set, TSet.has("aaa"))) + return [a, aa, aaa] + }) + const result = yield* (STM.commit(transaction)) + deepStrictEqual(result, [false, true, false]) + })) + + it.effect("removeIf", () => + Effect.gen(function*() { + const transaction = STM.gen(function*() { + const set = yield* (TSet.make("a", "aa", "aaa")) + const removed = yield* (pipe(set, TSet.removeIf((s) => s === "aa"))) + const a = yield* (pipe(set, TSet.has("a"))) + const aa = yield* (pipe(set, TSet.has("aa"))) + const aaa = yield* (pipe(set, TSet.has("aaa"))) + return [Array.from(removed), a, aa, aaa] + }) + const result = yield* (STM.commit(transaction)) + deepStrictEqual(result, [["aa"], true, false, true]) + })) + + it.effect("removeIf > { discard: true }", () => + Effect.gen(function*() { + const transaction = STM.gen(function*() { + const set = yield* (TSet.make("a", "aa", "aaa")) + yield* (pipe(set, TSet.removeIf((s) => s === "aa", { discard: true }))) + const a = yield* (pipe(set, TSet.has("a"))) + const aa = yield* (pipe(set, TSet.has("aa"))) + const aaa = yield* (pipe(set, TSet.has("aaa"))) + return [a, aa, aaa] + }) + const result = yield* (STM.commit(transaction)) + deepStrictEqual(result, [true, false, true]) + })) + + it.effect("transform", () => + Effect.gen(function*() { + const transaction = pipe( + TSet.make(1, 2, 3), + STM.tap((set) => TSet.transform(set, (n) => n * 2)), + STM.flatMap(TSet.toArray) + ) + const result = yield* (STM.commit(transaction)) + deepStrictEqual(result, [2, 4, 6]) + })) + + it.effect("transform - and shrink", () => + Effect.gen(function*() { + const transaction = pipe( + TSet.make(1, 2, 3), + STM.tap(TSet.transform(() => 1)), + STM.flatMap(TSet.toArray) + ) + const result = yield* (STM.commit(transaction)) + deepStrictEqual(result, [1]) + })) + + it.effect("transformSTM", () => + Effect.gen(function*() { + const transaction = pipe( + TSet.make(1, 2, 3), + STM.tap((set) => TSet.transformSTM(set, (n) => STM.succeed(n * 2))), + STM.flatMap(TSet.toArray) + ) + const result = yield* (STM.commit(transaction)) + deepStrictEqual(result, [2, 4, 6]) + })) + + it.effect("transformSTM - and shrink", () => + Effect.gen(function*() { + const transaction = pipe( + TSet.make(1, 2, 3), + STM.tap(TSet.transformSTM(() => STM.succeed(1))), + STM.flatMap(TSet.toArray) + ) + const result = yield* (STM.commit(transaction)) + deepStrictEqual(result, [1]) + })) + + it.effect("size", () => + Effect.gen(function*() { + const transaction = pipe( + TSet.make(1, 2, 3, 4), + STM.flatMap(TSet.size) + ) + const result = yield* (STM.commit(transaction)) + strictEqual(result, 4) + })) + + it.effect("union", () => + Effect.gen(function*() { + const transaction = STM.gen(function*() { + const set1 = yield* (TSet.make(1, 2, 3)) + const set2 = yield* (TSet.make(1, 4, 5)) + yield* (pipe(set1, TSet.union(set2))) + return yield* (TSet.toArray(set1)) + }) + const result = yield* (STM.commit(transaction)) + deepStrictEqual(result, [1, 2, 3, 4, 5]) + })) + + it.effect("toChunk", () => + Effect.gen(function*() { + const transaction = STM.gen(function*() { + const set = yield* (TSet.make(1, 2, 3)) + return yield* (TSet.toChunk(set)) + }) + const result = yield* (STM.commit(transaction)) + deepStrictEqual(result, Chunk.make(1, 2, 3)) + })) +}) diff --git a/repos/effect/packages/effect/test/TSubscriptionRef.test.ts b/repos/effect/packages/effect/test/TSubscriptionRef.test.ts new file mode 100644 index 0000000..77410f2 --- /dev/null +++ b/repos/effect/packages/effect/test/TSubscriptionRef.test.ts @@ -0,0 +1,159 @@ +import { describe, it } from "@effect/vitest" +import { assertTrue, deepStrictEqual } from "@effect/vitest/utils" +import { + Chunk, + Deferred, + Effect, + Equal, + Exit, + Fiber, + Number, + pipe, + Random, + STM, + Stream, + TSubscriptionRef +} from "effect" + +describe.concurrent("TSubscriptionRef", () => { + it.effect("only emits comitted values", () => + Effect.gen(function*() { + const subscriptionRef = yield* (TSubscriptionRef.make(0)) + + const transaction = pipe( + TSubscriptionRef.update(subscriptionRef, (n) => n + 1), + STM.tap(() => TSubscriptionRef.update(subscriptionRef, (n) => n + 1)) + ) + + const subscriber = yield* (pipe( + TSubscriptionRef.changesStream(subscriptionRef), + Stream.take(1), + Stream.runCollect, + Effect.fork + )) + // stream doesn't work properly without a yield, it will drop values + yield* (Effect.yieldNow()) + yield* (STM.commit(transaction)) + yield* (Effect.yieldNow()) + const result = yield* (Fiber.join(subscriber)) + + deepStrictEqual(Array.from(result), [2]) + })) + + it.effect("emits every comitted value", () => + Effect.gen(function*() { + const subscriptionRef = yield* (TSubscriptionRef.make(0)) + + const transaction = pipe( + TSubscriptionRef.update(subscriptionRef, (n) => n + 1), + STM.commit, + // stream doesn't work properly without a yield, it will drop the first value without this + Effect.tap(() => Effect.yieldNow()), + Effect.flatMap(() => TSubscriptionRef.update(subscriptionRef, (n) => n + 1)) + ) + + const subscriber = yield* (pipe( + TSubscriptionRef.changesStream(subscriptionRef), + Stream.take(2), + Stream.runCollect, + Effect.fork + )) + // stream doesn't work properly without a yield, it will drop the first value without this + yield* (Effect.yieldNow()) + yield* transaction + const result = yield* (Fiber.join(subscriber)) + + deepStrictEqual(Array.from(result), [1, 2]) + })) + + it.effect("multiple subscribers can receive committed values", () => + Effect.gen(function*() { + const subscriptionRef = yield* (TSubscriptionRef.make(0)) + const deferred1 = yield* (Deferred.make()) + const deferred2 = yield* (Deferred.make()) + const subscriber1 = yield* (pipe( + TSubscriptionRef.changesStream(subscriptionRef), + Stream.tap(() => Deferred.succeed(deferred1, void 0)), + Stream.take(3), + Stream.runCollect, + Effect.fork + )) + yield* (Deferred.await(deferred1)) + yield* (TSubscriptionRef.update(subscriptionRef, (n) => n + 1)) + const subscriber2 = yield* (pipe( + TSubscriptionRef.changesStream(subscriptionRef), + Stream.tap(() => Deferred.succeed(deferred2, void 0)), + Stream.take(2), + Stream.runCollect, + Effect.fork + )) + yield* (Deferred.await(deferred2)) + yield* (TSubscriptionRef.update(subscriptionRef, (n) => n + 1)) + const result1 = yield* (Fiber.join(subscriber1)) + const result2 = yield* (Fiber.join(subscriber2)) + deepStrictEqual(Array.from(result1), [0, 1, 2]) + deepStrictEqual(Array.from(result2), [1, 2]) + })) + + it.effect("subscriptions are interruptible", () => + Effect.gen(function*() { + const ref = yield* (TSubscriptionRef.make(0)) + const deferred1 = yield* (Deferred.make()) + const deferred2 = yield* (Deferred.make()) + const subscriber1 = yield* pipe( + TSubscriptionRef.changesStream(ref), + Stream.tap(() => Deferred.succeed(deferred1, void 0)), + Stream.take(5), + Stream.runCollect, + Effect.fork + ) + yield* (Deferred.await(deferred1)) + yield* (TSubscriptionRef.update(ref, (n) => n + 1)) + const subscriber2 = yield* pipe( + TSubscriptionRef.changesStream(ref), + Stream.tap(() => Deferred.succeed(deferred2, void 0)), + Stream.take(2), + Stream.runCollect, + Effect.fork + ) + yield* (Deferred.await(deferred2)) + yield* (TSubscriptionRef.update(ref, (n) => n + 1)) + const result1 = yield* (Fiber.interrupt(subscriber1)) + const result2 = yield* (Fiber.join(subscriber2)) + assertTrue(Exit.isInterrupted(result1)) + deepStrictEqual(Array.from(result2), [1, 2]) + })) + + it.effect("concurrent subscribes and unsubscribes are handled correctly", () => + Effect.gen(function*() { + const subscriber = (subscriptionRef: TSubscriptionRef.TSubscriptionRef) => + pipe( + Random.nextIntBetween(0, 200), + Effect.flatMap((n) => + pipe( + TSubscriptionRef.changesStream(subscriptionRef), + Stream.take(n), + Stream.runCollect + ) + ) + ) + const ref = yield* (TSubscriptionRef.make(0)) + const fiber = yield* pipe( + TSubscriptionRef.update(ref, (n) => n + 1), + Effect.forever, + Effect.fork + ) + const result = yield* ( + Effect.map( + Effect.all( + Array.from({ length: 2 }, () => subscriber(ref)), + { concurrency: 2 } + ), + Chunk.unsafeFromArray + ) + ) + yield* (Fiber.interrupt(fiber)) + const isSorted = Chunk.every(result, (chunk) => Equal.equals(chunk, Chunk.sort(chunk, Number.Order))) + assertTrue(isSorted) + })) +}) diff --git a/repos/effect/packages/effect/test/TestClock.test.ts b/repos/effect/packages/effect/test/TestClock.test.ts new file mode 100644 index 0000000..14356be --- /dev/null +++ b/repos/effect/packages/effect/test/TestClock.test.ts @@ -0,0 +1,34 @@ +import { describe, it } from "@effect/vitest" +import { deepStrictEqual, strictEqual } from "@effect/vitest/utils" +import { DateTime, Effect, TestClock } from "effect" + +describe("TestClock", () => { + describe("setTime", () => { + const arbitraryDateTime = DateTime.unsafeMake("2023-12-31T11:00:00.000Z") + it.effect("should set the current Date using an Instant", () => + Effect.gen(function*() { + yield* TestClock.setTime(arbitraryDateTime.epochMillis) + const now = yield* DateTime.now + deepStrictEqual(now, arbitraryDateTime) + })) + it.effect("should set the current time using a DateTime", () => + Effect.gen(function*() { + yield* TestClock.setTime(arbitraryDateTime) + const now = yield* DateTime.now + deepStrictEqual(now, arbitraryDateTime) + })) + it.effect("should set the current time using a Date", () => + Effect.gen(function*() { + yield* TestClock.setTime(DateTime.toDate(arbitraryDateTime)) + const now = yield* DateTime.now + deepStrictEqual(now, arbitraryDateTime) + })) + + it.effect("should floor nanoseconds for fractional millisecond instants", () => + Effect.gen(function*() { + yield* TestClock.setTime(199023438.0000004) + const testClock = yield* TestClock.testClock() + strictEqual(testClock.unsafeCurrentTimeNanos(), 199023438000000n) + })) + }) +}) diff --git a/repos/effect/packages/effect/test/Tracer.test.ts b/repos/effect/packages/effect/test/Tracer.test.ts new file mode 100644 index 0000000..0873de1 --- /dev/null +++ b/repos/effect/packages/effect/test/Tracer.test.ts @@ -0,0 +1,350 @@ +import { describe, it } from "@effect/vitest" +import { assertInclude, assertNone, assertTrue, deepStrictEqual, strictEqual } from "@effect/vitest/utils" +import { Cause, Context, Duration, Effect, Fiber, FiberId, Layer, Option, pipe, TestClock, Tracer } from "effect" +import type { Span } from "effect/Tracer" +import type { NativeSpan } from "../src/internal/tracer.js" + +describe("Tracer", () => { + it.effect("includes trace when errored", () => + Effect.gen(function*() { + let maybeSpan: undefined | Span + const getSpan = Effect.functionWithSpan({ + body: (_id: string) => + Effect.currentSpan.pipe(Effect.flatMap((span) => { + maybeSpan = span + return Effect.fail("error") + })), + options: (id) => ({ + name: `span-${id}`, + attributes: { id } + }) + }) + yield* Effect.flip(getSpan("fail")) + assertTrue(maybeSpan !== undefined) + assertInclude(maybeSpan!.attributes.get("code.stacktrace") as string, "Tracer.test.ts:22:26") + })) + + it.effect("captures stack", () => + Effect.gen(function*() { + const cause = yield* Effect.die(new Error("boom")).pipe( + Effect.withSpan("C", { context: Tracer.DisablePropagation.context(true) }), + Effect.sandbox, + Effect.flip + ) + assertInclude(Cause.pretty(cause), "Tracer.test.ts:29:39") + })) + + describe("withSpan", () => { + it.effect("no parent", () => + Effect.gen(function*() { + const span = yield* Effect.withSpan("A")(Effect.currentSpan) + deepStrictEqual(span.name, "A") + assertNone(span.parent) + strictEqual(span.attributes.get("code.stacktrace"), undefined) + })) + + it.effect("parent", () => + Effect.gen(function*() { + const span = yield* ( + Effect.withSpan("B")( + Effect.withSpan("A")(Effect.currentSpan) + ) + ) + + deepStrictEqual(span.name, "A") + deepStrictEqual(Option.map(span.parent, (span) => (span as Span).name), Option.some("B")) + })) + + it.effect("parent when root is set", () => + Effect.gen(function*() { + const span = yield* ( + Effect.withSpan("B")(Effect.withSpan("A", { root: true })(Effect.currentSpan)) + ) + + deepStrictEqual(span.name, "A") + assertNone(span.parent) + })) + + it.effect("external parent", () => + Effect.gen(function*() { + const span = yield* ( + Effect.withSpan("A", { + parent: { + _tag: "ExternalSpan", + spanId: "000", + traceId: "111", + sampled: true, + context: Context.empty() + } + })(Effect.currentSpan) + ) + deepStrictEqual(span.name, "A") + deepStrictEqual(Option.map(span.parent, (span) => span.spanId), Option.some("000")) + })) + + it.effect("correct time", () => + Effect.gen(function*() { + const spanFiber = yield* ( + Effect.fork(Effect.withSpan("A")(Effect.delay(Duration.seconds(1))(Effect.currentSpan))) + ) + + yield* (TestClock.adjust(Duration.seconds(2))) + + const span = yield* (Fiber.join(spanFiber)) + + deepStrictEqual(span.name, "A") + deepStrictEqual(span.status.startTime, 0n) + deepStrictEqual((span.status as any)["endTime"], 1000000000n) + deepStrictEqual(span.status._tag, "Ended") + })) + }) + + it.effect("annotateSpans", () => + Effect.gen(function*() { + const span = yield* ( + Effect.annotateSpans( + Effect.withSpan("A")(Effect.currentSpan), + "key", + "value" + ) + ) + + deepStrictEqual(span.name, "A") + assertNone(span.parent) + deepStrictEqual(span.attributes.get("key"), "value") + })) + + it.effect("annotateSpans record", () => + Effect.gen(function*() { + const span = yield* ( + Effect.annotateSpans( + Effect.withSpan("A")(Effect.currentSpan), + { key: "value", key2: "value2" } + ) + ) + + deepStrictEqual(span.attributes.get("key"), "value") + deepStrictEqual(span.attributes.get("key2"), "value2") + })) + + it.effect("logger", () => + Effect.gen(function*() { + yield* (TestClock.adjust(Duration.millis(0.01))) + + const [span, fiberId] = yield* pipe( + Effect.log("event"), + Effect.zipRight(Effect.all([Effect.currentSpan, Effect.fiberId])), + Effect.withSpan("A") + ) + + deepStrictEqual(span.name, "A") + assertNone(span.parent) + deepStrictEqual((span as NativeSpan).events, [["event", 10000n, { + "effect.fiberId": FiberId.threadName(fiberId), + "effect.logLevel": "INFO" + }]]) + })) + + it.effect("withTracerTiming false", () => + Effect.gen(function*() { + yield* (TestClock.adjust(Duration.millis(1))) + + const span = yield* pipe( + Effect.withSpan("A")(Effect.currentSpan), + Effect.withTracerTiming(false) + ) + + deepStrictEqual(span.status.startTime, 0n) + })) + + it.effect("useSpanScoped", () => + Effect.gen(function*() { + const span = yield* Effect.scoped(Effect.makeSpanScoped("A")) + deepStrictEqual(span.status._tag, "Ended") + strictEqual(span.attributes.get("code.stacktrace"), undefined) + })) + + it.effect("annotateCurrentSpan", () => + Effect.gen(function*() { + yield* (Effect.annotateCurrentSpan("key", "value")) + const span = yield* (Effect.currentSpan) + deepStrictEqual(span.attributes.get("key"), "value") + }).pipe( + Effect.withSpan("A") + )) + + it.effect("withParentSpan", () => + Effect.gen(function*() { + const span = yield* (Effect.currentSpan) + deepStrictEqual( + span.parent.pipe( + Option.map((_) => _.spanId) + ), + Option.some("456") + ) + }).pipe( + Effect.withSpan("A"), + Effect.withParentSpan({ + _tag: "ExternalSpan", + traceId: "123", + spanId: "456", + sampled: true, + context: Context.empty() + }) + )) + + it.effect("Layer.parentSpan", () => + Effect.gen(function*() { + const span = yield* Effect.makeSpan("child") + const parent = yield* Option.filter(span.parent, (span): span is Span => span._tag === "Span") + deepStrictEqual(parent.name, "parent") + strictEqual(span.attributes.get("code.stacktrace"), undefined) + strictEqual(parent.attributes.get("code.stacktrace"), undefined) + }).pipe( + Effect.provide(Layer.unwrapScoped( + Effect.map( + Effect.makeSpanScoped("parent"), + (span) => Layer.parentSpan(span) + ) + )) + )) + + it.effect("Layer.span", () => + Effect.gen(function*() { + const span = yield* Effect.makeSpan("child") + const parent = span.parent.pipe( + Option.filter((span): span is Span => span._tag === "Span"), + Option.getOrThrow + ) + strictEqual(parent.name, "parent") + strictEqual(parent.attributes.get("code.stacktrace"), undefined) + }).pipe( + Effect.provide(Layer.span("parent")) + )) + + it.effect("Layer.span onEnd", () => + Effect.gen(function*() { + let onEndCalled = false + const span = yield* pipe( + Effect.currentSpan, + Effect.provide(Layer.span("span", { + onEnd: (span, _exit) => + Effect.sync(() => { + strictEqual(span.name, "span") + onEndCalled = true + }) + })) + ) + strictEqual(span.name, "span") + strictEqual(onEndCalled, true) + })) + + it.effect("linkSpans", () => + Effect.gen(function*() { + const childA = yield* (Effect.makeSpan("childA")) + const childB = yield* (Effect.makeSpan("childB")) + const currentSpan = yield* pipe( + Effect.currentSpan, + Effect.withSpan("A", { links: [{ _tag: "SpanLink", span: childB, attributes: {} }] }), + Effect.linkSpans(childA) + ) + deepStrictEqual( + currentSpan.links.map((_) => _.span), + [childA, childB] + ) + })) + + it.effect("Layer.withSpan", () => + Effect.gen(function*() { + let onEndCalled = false + const layer = Layer.effectDiscard(Effect.gen(function*() { + const span = yield* Effect.currentSpan + strictEqual(span.name, "span") + strictEqual(span.attributes.get("code.stacktrace"), undefined) + })).pipe( + Layer.withSpan("span", { + onEnd: (span, _exit) => + Effect.sync(() => { + strictEqual(span.name, "span") + onEndCalled = true + }) + }) + ) + + const span = yield* pipe(Effect.currentSpan, Effect.provide(layer), Effect.option) + + assertNone(span) + strictEqual(onEndCalled, true) + })) +}) + +it.effect("withTracerEnabled", () => + Effect.gen(function*() { + const span = yield* pipe( + Effect.currentSpan, + Effect.withSpan("A"), + Effect.withTracerEnabled(false) + ) + const spanB = yield* pipe( + Effect.currentSpan, + Effect.withSpan("B"), + Effect.withTracerEnabled(true) + ) + + deepStrictEqual(span.name, "A") + deepStrictEqual(span.spanId, "noop") + deepStrictEqual(spanB.name, "B") + })) + +describe("Tracer.DisablePropagation", () => { + it.effect("creates noop span", () => + Effect.gen(function*() { + const span = yield* Effect.currentSpan.pipe( + Effect.withSpan("A", { context: Tracer.DisablePropagation.context(true) }) + ) + const spanB = yield* Effect.currentSpan.pipe( + Effect.withSpan("B") + ) + + deepStrictEqual(span.name, "A") + deepStrictEqual(span.spanId, "noop") + deepStrictEqual(spanB.name, "B") + })) + + it.effect("isnt used as parent span", () => + Effect.gen(function*() { + const span = yield* Effect.currentSpan.pipe( + Effect.withSpan("child"), + Effect.withSpan("disabled", { context: Tracer.DisablePropagation.context(true) }), + Effect.withSpan("parent") + ) + strictEqual(span.name, "child") + assertTrue(span.parent._tag === "Some" && span.parent.value._tag === "Span") + strictEqual(span.parent.value.name, "parent") + })) +}) + +describe("functionWithSpan", () => { + const getSpan = Effect.functionWithSpan({ + body: (_id: string) => Effect.currentSpan, + options: (id) => ({ + name: `span-${id}`, + attributes: { id } + }) + }) + + it.effect("no parent", () => + Effect.gen(function*() { + const span = yield* getSpan("A") + deepStrictEqual(span.name, "span-A") + assertNone(span.parent) + strictEqual(span.attributes.get("code.stacktrace"), undefined) + })) + + it.effect("parent", () => + Effect.gen(function*() { + const span = yield* Effect.withSpan("B")(getSpan("A")) + deepStrictEqual(span.name, "span-A") + deepStrictEqual(Option.map(span.parent, (span) => (span as Span).name), Option.some("B")) + })) +}) diff --git a/repos/effect/packages/effect/test/Trie.test.ts b/repos/effect/packages/effect/test/Trie.test.ts new file mode 100644 index 0000000..e2ec0ad --- /dev/null +++ b/repos/effect/packages/effect/test/Trie.test.ts @@ -0,0 +1,542 @@ +import { describe, it } from "@effect/vitest" +import { assertNone, assertSome, deepStrictEqual, strictEqual, throws } from "@effect/vitest/utils" +import * as Equal from "effect/Equal" +import { pipe } from "effect/Function" +import * as Option from "effect/Option" +import * as Trie from "effect/Trie" + +describe("Trie", () => { + it("toString", () => { + const trie = pipe( + Trie.empty(), + Trie.insert("a", 0), + Trie.insert("b", 1) + ) + + strictEqual( + String(trie), + `{ + "_id": "Trie", + "values": [ + [ + "a", + 0 + ], + [ + "b", + 1 + ] + ] +}` + ) + }) + + it("toJSON", () => { + const trie = pipe( + Trie.empty(), + Trie.insert("a", 0), + Trie.insert("b", 1) + ) + + deepStrictEqual(trie.toJSON(), { _id: "Trie", values: [["a", 0], ["b", 1]] }) + }) + + it("inspect", () => { + if (typeof window !== "undefined") { + return + } + // eslint-disable-next-line @typescript-eslint/no-require-imports + const { inspect } = require("node:util") + + const trie = pipe( + Trie.empty(), + Trie.insert("a", 0), + Trie.insert("b", 1) + ) + + deepStrictEqual(inspect(trie), inspect({ _id: "Trie", values: [["a", 0], ["b", 1]] })) + }) + + it("iterable empty", () => { + const trie = Trie.empty() + + strictEqual(Trie.size(trie), 0) + deepStrictEqual(Array.from(trie), []) + }) + + it("insert", () => { + const trie1 = Trie.empty().pipe( + Trie.insert("call", 0) + ) + + const trie2 = trie1.pipe(Trie.insert("me", 1)) + const trie3 = trie2.pipe(Trie.insert("mind", 2)) + const trie4 = trie3.pipe(Trie.insert("mid", 3)) + + deepStrictEqual(Array.from(trie1), [["call", 0]]) + deepStrictEqual(Array.from(trie2), [["call", 0], ["me", 1]]) + deepStrictEqual(Array.from(trie3), [["call", 0], ["me", 1], ["mind", 2]]) + deepStrictEqual(Array.from(trie4), [["call", 0], ["me", 1], ["mid", 3], ["mind", 2]]) + }) + + it("fromIterable empty", () => { + const iterable: Array<[string, number]> = [] + const trie = Trie.fromIterable(iterable) + deepStrictEqual(Array.from(trie), iterable) + }) + + it("make", () => { + const trie = Trie.make(["ca", 0], ["me", 1]) + deepStrictEqual(Array.from(trie), [["ca", 0], ["me", 1]]) + strictEqual(Equal.equals(Trie.fromIterable([["ca", 0], ["me", 1]]), trie), true) + }) + + it("fromIterable [1]", () => { + const iterable: Array<[string, number]> = [["ca", 0], ["me", 1]] + const trie = Trie.fromIterable(iterable) + deepStrictEqual(Array.from(trie), iterable) + strictEqual(Equal.equals(Trie.make(["ca", 0], ["me", 1]), trie), true) + }) + + it("fromIterable [2]", () => { + const iterable: Array = [["call", 0], ["me", 1], ["mind", 2], ["mid", 3]] + const trie = Trie.fromIterable(iterable) + deepStrictEqual(Array.from(trie), [["call", 0], ["me", 1], ["mid", 3], ["mind", 2]]) + }) + + it("fromIterable [3]", () => { + const iterable: Array<[string, number]> = [["a", 0], ["b", 1]] + const trie = Trie.fromIterable(iterable) + deepStrictEqual(Array.from(trie), iterable) + }) + + it("fromIterable [4]", () => { + const iterable: Array<[string, number]> = [["a", 0]] + const trie = Trie.fromIterable(iterable) + deepStrictEqual(Array.from(trie), iterable) + }) + + it("fromIterable [5]", () => { + const iterable: Array<[string, number]> = [["shells", 0], ["she", 1]] + const trie = Trie.fromIterable(iterable) + deepStrictEqual(Array.from(trie), [["she", 1], ["shells", 0]]) + }) + + it("size", () => { + const trie = Trie.empty().pipe( + Trie.insert("a", 0), + Trie.insert("b", 1) + ) + + strictEqual(Trie.size(trie), 2) + }) + + it("isEmpty", () => { + const trie = Trie.empty() + const trie1 = trie.pipe(Trie.insert("ma", 0)) + strictEqual(Trie.isEmpty(trie), true) + strictEqual(Trie.isEmpty(trie1), false) + }) + + it("get [1]", () => { + const trie = Trie.empty().pipe( + Trie.insert("call", 0), + Trie.insert("me", 1), + Trie.insert("mind", 2), + Trie.insert("mid", 3) + ) + assertSome(Trie.get(trie, "call"), 0) + assertSome(Trie.get(trie, "me"), 1) + assertSome(Trie.get(trie, "mind"), 2) + assertSome(Trie.get(trie, "mid"), 3) + assertNone(Trie.get(trie, "cale")) + assertNone(Trie.get(trie, "ma")) + assertNone(Trie.get(trie, "midn")) + assertNone(Trie.get(trie, "mea")) + }) + + it("get [2]", () => { + const trie = Trie.empty().pipe( + Trie.insert("shells", 0), + Trie.insert("sells", 1), + Trie.insert("she", 2) + ) + + assertNone(Trie.get(trie, "sell")) + assertSome(Trie.get(trie, "sells"), 1) + assertNone(Trie.get(trie, "shell")) + assertSome(Trie.get(trie, "she"), 2) + }) + + it("has", () => { + const trie = Trie.empty().pipe( + Trie.insert("call", 0), + Trie.insert("me", 1), + Trie.insert("mind", 2), + Trie.insert("mid", 3) + ) + strictEqual(Trie.has(trie, "call"), true) + strictEqual(Trie.has(trie, "me"), true) + strictEqual(Trie.has(trie, "mind"), true) + strictEqual(Trie.has(trie, "mid"), true) + strictEqual(Trie.has(trie, "cale"), false) + strictEqual(Trie.has(trie, "ma"), false) + strictEqual(Trie.has(trie, "midn"), false) + strictEqual(Trie.has(trie, "mea"), false) + }) + + it("unsafeGet", () => { + const trie = Trie.empty().pipe( + Trie.insert("call", 0), + Trie.insert("me", 1) + ) + throws(() => Trie.unsafeGet(trie, "mae")) + }) + + it("remove", () => { + const trie = Trie.empty().pipe( + Trie.insert("call", 0), + Trie.insert("me", 1), + Trie.insert("mind", 2), + Trie.insert("mid", 3) + ) + + const trie1 = trie.pipe(Trie.remove("call")) + const trie2 = trie1.pipe(Trie.remove("mea")) + + deepStrictEqual(Trie.get(trie, "call"), Option.some(0)) + deepStrictEqual(Trie.get(trie1, "call"), Option.none()) + deepStrictEqual(Trie.get(trie2, "call"), Option.none()) + + deepStrictEqual(Array.from(trie), [["call", 0], ["me", 1], ["mid", 3], ["mind", 2]]) + deepStrictEqual(Array.from(trie1), [["me", 1], ["mid", 3], ["mind", 2]]) + deepStrictEqual(Array.from(trie2), [["me", 1], ["mid", 3], ["mind", 2]]) + }) + + it("keys", () => { + const trie = Trie.empty().pipe( + Trie.insert("cab", 0), + Trie.insert("abc", 1), + Trie.insert("bca", 2) + ) + + const result = Array.from(Trie.keys(trie)) + deepStrictEqual(result, ["abc", "bca", "cab"]) + }) + + it("keys alphabetical order", () => { + const trie = Trie.make( + ["abc", 0], + ["bac", 0], + ["b", 0], + ["ca", 0], + ["cac", 0], + ["c", 0], + ["abb", 0], + ["ba", 0], + ["a", 0], + ["bca", 0], + ["cab", 0], + ["dca", 0], + ["ab", 0], + ["adc", 0] + ) + + const result = Array.from(Trie.keys(trie)) + deepStrictEqual(result, [ + "a", + "ab", + "abb", + "abc", + "adc", + "b", + "ba", + "bac", + "bca", + "c", + "ca", + "cab", + "cac", + "dca" + ]) + }) + + it("values", () => { + const trie = Trie.empty().pipe( + Trie.insert("call", 0), + Trie.insert("me", 1), + Trie.insert("and", 2) + ) + + const result = Array.from(Trie.values(trie)) + deepStrictEqual(result, [2, 0, 1]) + }) + + it("entries", () => { + const trie = Trie.empty().pipe( + Trie.insert("call", 0), + Trie.insert("me", 1) + ) + + const result = Array.from(Trie.entries(trie)) + deepStrictEqual(result, [["call", 0], ["me", 1]]) + }) + + it("toEntries", () => { + const trie = Trie.empty().pipe( + Trie.insert("call", 0), + Trie.insert("me", 1) + ) + + const result = Trie.toEntries(trie) + deepStrictEqual(result, [["call", 0], ["me", 1]]) + }) + + it("keysWithPrefix", () => { + const trie = Trie.empty().pipe( + Trie.insert("she", 0), + Trie.insert("shells", 1), + Trie.insert("sea", 2), + Trie.insert("sells", 3), + Trie.insert("by", 4), + Trie.insert("the", 5), + Trie.insert("sea", 6), + Trie.insert("shore", 7) + ) + + const result = Array.from(Trie.keysWithPrefix(trie, "she")) + deepStrictEqual(result, ["she", "shells"]) + }) + + it("valuesWithPrefix", () => { + const trie = pipe( + Trie.empty(), + Trie.insert("shells", 0), + Trie.insert("sells", 1), + Trie.insert("sea", 2), + Trie.insert("she", 3) + ) + + const result = Array.from(Trie.valuesWithPrefix(trie, "she")) + deepStrictEqual(result, [3, 0]) + }) + + it("entriesWithPrefix", () => { + const trie = pipe( + Trie.empty(), + Trie.insert("shells", 0), + Trie.insert("sells", 1), + Trie.insert("sea", 2), + Trie.insert("she", 3) + ) + + const result = Array.from(Trie.entriesWithPrefix(trie, "she")) + deepStrictEqual(result, [["she", 3], ["shells", 0]]) + }) + + it("toEntriesWithPrefix", () => { + const trie = Trie.empty().pipe( + Trie.insert("shells", 0), + Trie.insert("sells", 1), + Trie.insert("sea", 2), + Trie.insert("she", 3) + ) + + const result = Trie.toEntriesWithPrefix(trie, "she") + deepStrictEqual(result, [["she", 3], ["shells", 0]]) + }) + + it("longestPrefixOf", () => { + const trie = Trie.empty().pipe( + Trie.insert("shells", 0), + Trie.insert("sells", 1), + Trie.insert("she", 2) + ) + + deepStrictEqual(Trie.longestPrefixOf(trie, "sell"), Option.none()) + deepStrictEqual(Trie.longestPrefixOf(trie, "sells"), Option.some(["sells", 1])) + deepStrictEqual(Trie.longestPrefixOf(trie, "shell"), Option.some(["she", 2])) + deepStrictEqual(Trie.longestPrefixOf(trie, "shellsort"), Option.some(["shells", 0])) + }) + + it("map", () => { + const trie = Trie.empty().pipe( + Trie.insert("shells", 0), + Trie.insert("sells", 1), + Trie.insert("she", 2) + ) + + const trieMapV = Trie.empty().pipe( + Trie.insert("shells", 1), + Trie.insert("sells", 2), + Trie.insert("she", 3) + ) + + const trieMapK = Trie.empty().pipe( + Trie.insert("shells", 6), + Trie.insert("sells", 5), + Trie.insert("she", 3) + ) + + strictEqual(Equal.equals(Trie.map(trie, (v) => v + 1), trieMapV), true) + strictEqual(Equal.equals(Trie.map(trie, (_, k) => k.length), trieMapK), true) + }) + + it("filter", () => { + const trie = Trie.empty().pipe( + Trie.insert("shells", 0), + Trie.insert("sells", 1), + Trie.insert("she", 2) + ) + + const trieMapV = Trie.empty().pipe( + Trie.insert("she", 2) + ) + + const trieMapK = Trie.empty().pipe( + Trie.insert("shells", 0), + Trie.insert("sells", 1) + ) + + strictEqual(Equal.equals(Trie.filter(trie, (v) => v > 1), trieMapV), true) + strictEqual(Equal.equals(Trie.filter(trie, (_, k) => k.length > 3), trieMapK), true) + }) + + it("filterMap", () => { + const trie = Trie.empty().pipe( + Trie.insert("shells", 0), + Trie.insert("sells", 1), + Trie.insert("she", 2) + ) + + const trieMapV = Trie.empty().pipe( + Trie.insert("she", 2) + ) + + const trieMapK = Trie.empty().pipe( + Trie.insert("shells", 0), + Trie.insert("sells", 1) + ) + + strictEqual(Equal.equals(Trie.filterMap(trie, (v) => v > 1 ? Option.some(v) : Option.none()), trieMapV), true) + strictEqual( + Equal.equals(Trie.filterMap(trie, (v, k) => k.length > 3 ? Option.some(v) : Option.none()), trieMapK), + true + ) + }) + + it("compact", () => { + const trie = Trie.empty>().pipe( + Trie.insert("shells", Option.some(0)), + Trie.insert("sells", Option.none()), + Trie.insert("she", Option.some(2)) + ) + + const trieMapV = Trie.empty().pipe( + Trie.insert("shells", 0), + Trie.insert("she", 2) + ) + + strictEqual(Equal.equals(Trie.compact(trie), trieMapV), true) + }) + + it("modify", () => { + const trie = Trie.empty().pipe( + Trie.insert("shells", 0), + Trie.insert("sells", 1), + Trie.insert("she", 2) + ) + + deepStrictEqual(trie.pipe(Trie.modify("she", (v) => v + 10), Trie.get("she")), Option.some(12)) + strictEqual(Equal.equals(trie.pipe(Trie.modify("me", (v) => v)), trie), true) + }) + + it("removeMany", () => { + const trie = Trie.empty().pipe( + Trie.insert("shells", 0), + Trie.insert("sells", 1), + Trie.insert("she", 2) + ) + + strictEqual( + Equal.equals(trie.pipe(Trie.removeMany(["she", "sells"])), Trie.empty().pipe(Trie.insert("shells", 0))), + true + ) + }) + + it("insertMany", () => { + const trie = Trie.empty().pipe( + Trie.insert("shells", 0), + Trie.insert("sells", 1), + Trie.insert("she", 2) + ) + + const trieInsert = Trie.empty().pipe( + Trie.insert("shells", 0), + Trie.insertMany( + [["sells", 1], ["she", 2]] + ) + ) + + strictEqual( + Equal.equals(trie, trieInsert), + true + ) + }) + + it("reduce", () => { + const trie = Trie.empty().pipe( + Trie.insert("shells", 0), + Trie.insert("sells", 1), + Trie.insert("she", 2) + ) + + strictEqual( + trie.pipe( + Trie.reduce(0, (acc, n) => acc + n) + ), + 3 + ) + strictEqual( + trie.pipe( + Trie.reduce(10, (acc, n) => acc + n) + ), + 13 + ) + strictEqual( + trie.pipe( + Trie.reduce("", (acc, _, key) => acc + key) + ), + "sellssheshells" + ) + }) + + it("forEach", () => { + let value = 0 + + Trie.empty().pipe( + Trie.insert("shells", 0), + Trie.insert("sells", 1), + Trie.insert("she", 2), + Trie.forEach((n, key) => { + value += n + key.length + }) + ) + + strictEqual(value, 17) + }) + + it("Equal.symbol", () => { + strictEqual( + Equal.equals(Trie.empty(), Trie.empty()), + true + ) + strictEqual( + Equal.equals( + Trie.make(["call", 0], ["me", 1]), + Trie.make(["call", 0], ["me", 1]) + ), + true + ) + }) +}) diff --git a/repos/effect/packages/effect/test/Tuple.test.ts b/repos/effect/packages/effect/test/Tuple.test.ts new file mode 100644 index 0000000..c5d164d --- /dev/null +++ b/repos/effect/packages/effect/test/Tuple.test.ts @@ -0,0 +1,43 @@ +import { describe, it } from "@effect/vitest" +import { deepStrictEqual, strictEqual } from "@effect/vitest/utils" +import { pipe, Tuple } from "effect" + +describe("Tuple", () => { + it("make", () => { + deepStrictEqual(Tuple.make("a", 1, true), ["a", 1, true]) + }) + + it("appendElement", () => { + deepStrictEqual(pipe(Tuple.make("a", 1), Tuple.appendElement(true)), ["a", 1, true]) + }) + + it("getFirst", () => { + strictEqual(Tuple.getFirst(Tuple.make("a", 1)), "a") + }) + + it("getSecond", () => { + strictEqual(Tuple.getSecond(Tuple.make("a", 1)), 1) + }) + + it("mapBoth", () => { + deepStrictEqual( + Tuple.mapBoth(Tuple.make("a", 1), { + onFirst: (s) => s + "!", + onSecond: (n) => n * 2 + }), + ["a!", 2] + ) + }) + + it("map", () => { + deepStrictEqual(Tuple.map(["a", 1, false], (x) => x.toString().toUpperCase()), ["A", "1", "FALSE"]) + }) + + it("swap", () => { + deepStrictEqual(Tuple.swap(Tuple.make("a", 1)), [1, "a"]) + }) + + it("at", () => { + deepStrictEqual(Tuple.at([1, "hello", true], 1), "hello") + }) +}) diff --git a/repos/effect/packages/effect/test/assertions.test.ts b/repos/effect/packages/effect/test/assertions.test.ts new file mode 100644 index 0000000..f1aaa3f --- /dev/null +++ b/repos/effect/packages/effect/test/assertions.test.ts @@ -0,0 +1,112 @@ +import { Cause, Option } from "effect" +import * as assert from "node:assert" +import { assert as vassert, describe, expect, it } from "vitest" +import * as Util from "./util.js" + +// where `fails: false` there is a problem with the assertion library + +describe("node:assert", () => { + describe("Option", () => { + describe("none vs some", () => { + it("deepStrictEqual", { fails: true }, () => { + assert.deepStrictEqual(Option.none(), Option.some(2)) + }) + }) + }) + + describe("Cause", () => { + describe("sequential vs parallel", () => { + it("deepStrictEqual", { fails: true }, () => { + assert.deepStrictEqual(Cause.sequential(Cause.empty, Cause.empty), Cause.parallel(Cause.empty, Cause.empty)) + }) + }) + }) +}) + +describe("vitest assert", () => { + describe("Option", () => { + describe("none vs some", () => { + it("deepStrictEqual", { fails: false }, () => { + vassert.deepStrictEqual(Option.none(), Option.some(2)) + }) + }) + }) + + describe("Cause", () => { + describe("sequential vs parallel", () => { + it("deepStrictEqual", { fails: true }, () => { + vassert.deepStrictEqual(Cause.sequential(Cause.empty, Cause.empty), Cause.parallel(Cause.empty, Cause.empty)) + }) + }) + }) +}) + +describe("expect", () => { + describe("Option", () => { + describe("none vs some", () => { + it("toEqual", { fails: true }, () => { + expect(Option.none()).toEqual(Option.some(1)) + }) + + it("toStrictEqual", { fails: true }, () => { + expect(Option.none()).toStrictEqual(Option.some(1)) + }) + }) + }) + + describe("Cause", () => { + describe("sequential vs parallel", () => { + it("toEqual", { fails: false }, () => { + expect(Cause.sequential(Cause.empty, Cause.empty)).toEqual(Cause.parallel(Cause.empty, Cause.empty)) + }) + + it("toStrictEqual", { fails: false }, () => { + expect(Cause.sequential(Cause.empty, Cause.empty)).toStrictEqual(Cause.parallel(Cause.empty, Cause.empty)) + }) + }) + }) +}) + +describe("utils", () => { + it("assertInstanceOf", () => { + Util.assertInstanceOf(new Error(), Error) + Util.throws(() => Util.assertInstanceOf(1, Error), (err) => { + Util.assertTrue(err instanceof Error) + Util.strictEqual(err.message, "expected 1 to be an instance of Error") + }) + }) + + it("assertInclude", () => { + Util.assertInclude("abc", "b") + Util.throws(() => Util.assertInclude(undefined, "a"), (err) => { + Util.assertTrue(err instanceof Error) + Util.strictEqual( + err.message, + `Expected + +undefined + +to include + +a` + ) + }) + }) + + it("assertMatch", () => { + Util.assertMatch("abc", /b/) + Util.throws(() => Util.assertMatch("a", /b/), (err) => { + Util.assertTrue(err instanceof Error) + Util.strictEqual( + err.message, + `Expected + +a + +to match + +/b/` + ) + }) + }) +}) diff --git a/repos/effect/packages/effect/test/util.ts b/repos/effect/packages/effect/test/util.ts new file mode 100644 index 0000000..e69817e --- /dev/null +++ b/repos/effect/packages/effect/test/util.ts @@ -0,0 +1,170 @@ +import type { Cause } from "effect" +import { Either, Equal, Exit, Option, Predicate } from "effect" +import * as assert from "node:assert" +import { assert as vassert } from "vitest" + +// ---------------------------- +// Primitives +// ---------------------------- + +/** + * Throws an `AssertionError` with the provided error message. + */ +export function fail(message: string) { + assert.fail(message) +} + +export function deepStrictEqual(actual: A, expected: A, message?: string, ..._: Array) { + assert.deepStrictEqual(actual, expected, message) +} + +export function notDeepStrictEqual(actual: A, expected: A, message?: string, ..._: Array) { + assert.notDeepStrictEqual(actual, expected, message) +} + +export function strictEqual(actual: A, expected: A, message?: string, ..._: Array) { + assert.strictEqual(actual, expected, message) +} + +/** + * Asserts that `actual` is equal to `expected` using the `Equal.equals` trait. + */ +export function assertEquals(actual: A, expected: A, message?: string, ..._: Array) { + if (!Equal.equals(actual, expected)) { + deepStrictEqual(actual, expected, message) // show diff + fail(message ?? "Expected values to be Equal.equals") + } +} + +export function doesNotThrow(thunk: () => void, message?: string, ..._: Array) { + assert.doesNotThrow(thunk, message) +} + +// ---------------------------- +// Derived +// ---------------------------- + +/** + * Asserts that `value` is an instance of `constructor`. + */ +export function assertInstanceOf any>( + value: unknown, + constructor: C, + message?: string, + ..._: Array +): asserts value is InstanceType { + // @ts-ignore + vassert.instanceOf(value, constructor, message) +} + +export function assertTrue(self: unknown, message?: string, ..._: Array): asserts self { + strictEqual(self, true, message) +} + +export function assertFalse(self: boolean, message?: string, ..._: Array) { + strictEqual(self, false, message) +} + +export function assertInclude(actual: string | undefined, expected: string, ..._: Array) { + if (Predicate.isString(expected)) { + if (!actual?.includes(expected)) { + fail(`Expected\n\n${actual}\n\nto include\n\n${expected}`) + } + } +} + +export function assertMatch(actual: string, regexp: RegExp, ..._: Array) { + if (!regexp.test(actual)) { + fail(`Expected\n\n${actual}\n\nto match\n\n${regexp}`) + } +} + +export function throws(thunk: () => void, error?: Error | ((u: unknown) => undefined), ..._: Array) { + try { + thunk() + fail("Expected to throw an error") + } catch (e) { + if (error !== undefined) { + if (Predicate.isFunction(error)) { + error(e) + } else { + deepStrictEqual(e, error) + } + } + } +} + +export async function throwsAsync( + thunk: () => Promise, + error?: Error | ((u: unknown) => undefined), + ..._: Array +) { + try { + await thunk() + fail("Expected to throw an error") + } catch (e) { + if (error !== undefined) { + if (Predicate.isFunction(error)) { + error(e) + } else { + deepStrictEqual(e, error) + } + } + } +} + +// ---------------------------- +// Option +// ---------------------------- + +export function assertNone(option: Option.Option, ..._: Array): asserts option is Option.None { + deepStrictEqual(option, Option.none()) +} + +export function assertSome( + option: Option.Option, + expected: A, + ..._: Array +): asserts option is Option.Some { + deepStrictEqual(option, Option.some(expected)) +} + +// ---------------------------- +// Either +// ---------------------------- + +export function assertLeft( + either: Either.Either, + expected: L, + ..._: Array +): asserts either is Either.Left { + deepStrictEqual(either, Either.left(expected)) +} + +export function assertRight( + either: Either.Either, + expected: R, + ..._: Array +): asserts either is Either.Right { + deepStrictEqual(either, Either.right(expected)) +} + +// ---------------------------- +// Exit +// ---------------------------- + +export function assertFailure( + exit: Exit.Exit, + expected: Cause.Cause, + ..._: Array +): asserts exit is Exit.Failure { + deepStrictEqual(exit, Exit.failCause(expected)) +} + +export function assertSuccess( + exit: Exit.Exit, + expected: A, + ..._: Array +): asserts exit is Exit.Success { + deepStrictEqual(exit, Exit.succeed(expected)) +} diff --git a/repos/effect/packages/effect/test/utils/cache/ObservableResource.ts b/repos/effect/packages/effect/test/utils/cache/ObservableResource.ts new file mode 100644 index 0000000..c314da2 --- /dev/null +++ b/repos/effect/packages/effect/test/utils/cache/ObservableResource.ts @@ -0,0 +1,71 @@ +import { strictEqual } from "@effect/vitest/utils" +import * as Effect from "effect/Effect" +import * as ExecutionStrategy from "effect/ExecutionStrategy" +import { pipe } from "effect/Function" +import * as Ref from "effect/Ref" +import * as Scope from "effect/Scope" + +export interface ObservableResource { + readonly scoped: Effect.Effect + assertNotAcquired(): Effect.Effect + assertAcquiredOnceAndCleaned(): Effect.Effect + assertAcquiredOnceAndNotCleaned(): Effect.Effect +} + +class ObservableResourceImpl implements ObservableResource { + constructor( + readonly scoped: Effect.Effect, + readonly getState: Effect.Effect + ) {} + + assertNotAcquired(): Effect.Effect { + return Effect.map(this.getState, ([numAcquisition, numCleaned]) => { + strictEqual(numAcquisition, 0, "Resource acquired when it should not have") + strictEqual(numCleaned, 0, "Resource cleaned when it should not have") + }) + } + + assertAcquiredOnceAndCleaned(): Effect.Effect { + return Effect.map(this.getState, ([numAcquisition, numCleaned]) => { + strictEqual(numAcquisition, 1, "Resource not acquired once") + strictEqual(numCleaned, 1, "Resource not cleaned when it should have") + }) + } + + assertAcquiredOnceAndNotCleaned(): Effect.Effect { + return Effect.map(this.getState, ([numAcquisition, numCleaned]) => { + strictEqual(numAcquisition, 1, "Resource not acquired once") + strictEqual(numCleaned, 0, "Resource cleaned when it should not have") + }) + } +} + +export const makeVoid = (): Effect.Effect> => make(void 0) + +export const make = (value: V): Effect.Effect> => makeEffect(Effect.succeed(value)) + +export const makeEffect = ( + effect: Effect.Effect +): Effect.Effect> => + pipe( + Effect.zip(Ref.make(0), Ref.make(0)), + Effect.map(([resourceAcquisitionCount, resourceAcquisitionReleasing]) => { + const getState = Effect.zip( + Ref.get(resourceAcquisitionCount), + Ref.get(resourceAcquisitionReleasing) + ) + const scoped = Effect.uninterruptibleMask((restore) => + Effect.gen(function*() { + const parent = yield* Effect.scope + const child = yield* Scope.fork(parent, ExecutionStrategy.sequential) + yield* Ref.update(resourceAcquisitionCount, (n) => n + 1) + yield* Scope.addFinalizer(child, Ref.update(resourceAcquisitionReleasing, (n) => n + 1)) + return yield* Effect.acquireReleaseInterruptible( + restore(effect), + (exit) => Scope.close(child, exit) + ) + }) + ) + return new ObservableResourceImpl(scoped, getState) + }) + ) diff --git a/repos/effect/packages/effect/test/utils/cache/WatchableLookup.ts b/repos/effect/packages/effect/test/utils/cache/WatchableLookup.ts new file mode 100644 index 0000000..eec9a05 --- /dev/null +++ b/repos/effect/packages/effect/test/utils/cache/WatchableLookup.ts @@ -0,0 +1,141 @@ +import { assertTrue } from "@effect/vitest/utils" +import * as Chunk from "effect/Chunk" +import * as Duration from "effect/Duration" +import * as Effect from "effect/Effect" +import { identity, pipe } from "effect/Function" +import * as HashMap from "effect/HashMap" +import * as Option from "effect/Option" +import * as Ref from "effect/Ref" +import * as Schedule from "effect/Schedule" +import type * as Scope from "effect/Scope" +import * as TestServices from "effect/TestServices" +import * as ObservableResource from "./ObservableResource.js" + +export interface WatchableLookup { + (key: Key): Effect.Effect + lock: () => Effect.Effect + unlock: () => Effect.Effect + createdResources: () => Effect.Effect< + HashMap.HashMap>> + > + firstCreatedResource: (key: Key) => Effect.Effect> + getCalledTimes: (key: Key) => Effect.Effect + resourcesCleaned: ( + resources: Iterable> + ) => Effect.Effect + assertCalledTimes: (key: Key, sizeAssertion: (value: number) => void) => Effect.Effect + assertFirstNCreatedResourcesCleaned: (key: Key, n: number) => Effect.Effect + assertAllCleaned: () => Effect.Effect + assertAllCleanedForKey: (key: Key) => Effect.Effect + assertAtLeastOneResourceNotCleanedForKey: (key: Key) => Effect.Effect +} + +export const make = ( + concreteLookup: (key: Key) => Value +): Effect.Effect> => makeEffect((key) => Effect.succeed(concreteLookup(key))) + +export const makeVoid = (): Effect.Effect> => make((_: void) => void 0) + +export const makeEffect = ( + concreteLookup: (key: Key) => Effect.Effect +): Effect.Effect> => + Effect.map( + Effect.zip( + Ref.make(false), + Ref.make(HashMap.empty>>()) + ), + ([blocked, resources]): WatchableLookup => { + function lookup(key: Key): Effect.Effect { + return Effect.flatten(Effect.gen(function*() { + const observableResource = yield* ObservableResource.makeEffect(concreteLookup(key)) + yield* Ref.update(resources, (resourceMap) => { + const newResource = pipe( + HashMap.get(resourceMap, key), + Option.getOrElse(() => Chunk.empty>()), + Chunk.append(observableResource) + ) + return HashMap.set(resourceMap, key, newResource) + }) + const schedule = Schedule.intersect( + Schedule.recurWhile(identity), + Schedule.exponential(Duration.millis(10), 2.0) + ) + yield* pipe( + Ref.get(blocked), + Effect.repeat(schedule), + TestServices.provideLive + ) + return observableResource.scoped + })) + } + const lock = () => Ref.set(blocked, true) + const unlock = () => Ref.set(blocked, false) + const createdResources = () => Ref.get(resources) + const firstCreatedResource = (key: Key) => + Effect.map( + Ref.get(resources), + (map) => Chunk.unsafeHead(HashMap.unsafeGet(map, key)) + ) + const getCalledTimes = (key: Key) => + Effect.map( + createdResources(), + (map) => + Option.match(HashMap.get(map, key), { + onNone: () => 0, + onSome: Chunk.size + }) + ) + const resourcesCleaned = (resources: Iterable>) => + Effect.forEach(resources, (resource) => Effect.suspend(() => resource.assertAcquiredOnceAndCleaned())) + const assertCalledTimes = (key: Key, sizeAssertion: (value: number) => void) => + Effect.flatMap(getCalledTimes(key), (n) => Effect.sync(() => sizeAssertion(n))) + const assertFirstNCreatedResourcesCleaned = (key: Key, n: number) => + Effect.flatMap(createdResources(), (resources) => + resourcesCleaned(pipe( + HashMap.get(resources, key), + Option.match({ + onNone: () => Chunk.empty>(), + onSome: Chunk.take(n) + }) + ))) + const assertAllCleaned = () => + Effect.flatMap(createdResources(), (resources) => + resourcesCleaned( + Chunk.flatten(Chunk.unsafeFromArray(HashMap.toValues(resources))) + )) + const assertAllCleanedForKey = (key: Key) => + Effect.flatMap(createdResources(), (resources) => + resourcesCleaned(pipe(HashMap.get(resources, key), Option.getOrElse(() => Chunk.empty())))) + const assertAtLeastOneResourceNotCleanedForKey = (key: Key) => + Effect.flatMap(createdResources(), (resources) => { + const resourcesForKey = pipe( + HashMap.get(resources, key), + Option.getOrElse(() => + Chunk.empty() + ) + ) + return pipe( + Effect.reduce(resourcesForKey, false, (acc, resource) => + pipe( + Effect.suspend(() => resource.assertAcquiredOnceAndNotCleaned()), + Effect.isSuccess, + Effect.map((isSuccess) => acc || isSuccess) + )), + Effect.map((atLeastOneNotCleaned) => Effect.sync(() => assertTrue(atLeastOneNotCleaned))) + ) + }) + return Object.assign(lookup, { + lock, + unlock, + createdResources, + firstCreatedResource, + getCalledTimes, + resourcesCleaned, + assertCalledTimes, + assertFirstNCreatedResourcesCleaned, + assertAllCleaned, + assertAllCleanedForKey, + assertAtLeastOneResourceNotCleanedForKey + }) + } + ) diff --git a/repos/effect/packages/effect/test/utils/cause.ts b/repos/effect/packages/effect/test/utils/cause.ts new file mode 100644 index 0000000..c823d13 --- /dev/null +++ b/repos/effect/packages/effect/test/utils/cause.ts @@ -0,0 +1,82 @@ +import * as Cause from "effect/Cause" +import * as fc from "effect/FastCheck" +import * as FiberId from "effect/FiberId" + +export const causesArb = ( + n: number, + error: fc.Arbitrary, + defect: fc.Arbitrary +): fc.Arbitrary> => { + const fiberId: fc.Arbitrary = fc.tuple( + fc.integer(), + fc.integer() + ).map(([a, b]) => FiberId.make(a, b)) + + const empty = fc.constant(Cause.empty) + const failure = error.map(Cause.fail) + const die = defect.map(Cause.die) + const interrupt = fiberId.map(Cause.interrupt) + + const sequential = (n: number): fc.Arbitrary> => { + return fc.integer({ min: 1, max: n - 1 }).chain((i) => + causesN(i).chain((left) => causesN(n - i).map((right) => Cause.sequential(left, right))) + ) + } + + const parallel = (n: number): fc.Arbitrary> => { + return fc.integer({ min: 1, max: n - 1 }).chain((i) => + causesN(i).chain((left) => causesN(n - i).map((right) => Cause.parallel(left, right))) + ) + } + + const causesN = (n: number): fc.Arbitrary> => { + if (n === 1) { + return fc.oneof(empty, failure, die, interrupt) + } + return fc.oneof(sequential(n), parallel(n)) + } + + return causesN(n) +} + +export const causes: fc.Arbitrary> = causesArb( + 1, + fc.string(), + fc.string().map((message) => new Error(message)) +) + +export const errors: fc.Arbitrary = fc.string() + +export const errorCauseFunctions: fc.Arbitrary<(s: string) => Cause.Cause> = fc.func(causes) + +export const equalCauses: fc.Arbitrary< + readonly [Cause.Cause, Cause.Cause] +> = fc.tuple(causes, causes, causes) + .chain(([a, b, c]) => { + const causeCases: ReadonlyArray, Cause.Cause]> = [ + [a, a], + [ + Cause.sequential(Cause.sequential(a, b), c), + Cause.sequential(a, Cause.sequential(b, c)) + ], + [ + Cause.sequential(a, Cause.parallel(b, c)), + Cause.parallel(Cause.sequential(a, b), Cause.sequential(a, c)) + ], + [ + Cause.parallel(Cause.parallel(a, b), c), + Cause.parallel(a, Cause.parallel(b, c)) + ], + [ + Cause.parallel(Cause.sequential(a, c), Cause.sequential(b, c)), + Cause.sequential(Cause.parallel(a, b), c) + ], + [ + Cause.parallel(a, b), + Cause.parallel(b, a) + ], + [a, Cause.sequential(a, Cause.empty)], + [a, Cause.parallel(a, Cause.empty)] + ] + return fc.integer({ min: 0, max: causeCases.length - 1 }).map((i) => causeCases[i]) + }) diff --git a/repos/effect/packages/effect/test/utils/coordination.ts b/repos/effect/packages/effect/test/utils/coordination.ts new file mode 100644 index 0000000..3fd9fe4 --- /dev/null +++ b/repos/effect/packages/effect/test/utils/coordination.ts @@ -0,0 +1,60 @@ +import * as Chunk from "effect/Chunk" +import * as Effect from "effect/Effect" +import * as Exit from "effect/Exit" +import { pipe } from "effect/Function" +import * as Option from "effect/Option" +import * as Queue from "effect/Queue" +import * as Ref from "effect/Ref" + +export interface ChunkCoordination { + readonly queue: Queue.Queue, Option.Option>> + readonly offer: Effect.Effect + readonly proceed: Effect.Effect + readonly awaitNext: Effect.Effect +} + +export const chunkCoordination = ( + _chunks: Iterable> +): Effect.Effect> => + Effect.gen(function*() { + const chunks = Chunk.fromIterable(_chunks) + const queue = yield* Queue.unbounded, Option.Option>>() + const ps = yield* Queue.unbounded() + const ref = yield* Ref.make, Option.Option>>>>( + pipe( + chunks, + Chunk.dropRight(1), + Chunk.map((chunk) => Chunk.of(Exit.succeed(chunk))), + Chunk.appendAll( + pipe( + Chunk.last(chunks), + Option.map((chunk) => + Chunk.unsafeFromArray, Option.Option>>([ + Exit.succeed(chunk), + Exit.fail(Option.none()) + ]) + ), + Option.match({ + onNone: () => Chunk.empty, Option.Option>>>(), + onSome: Chunk.of + }) + ) + ) + ) + ) + return { + queue, + offer: pipe( + Ref.modify(ref, (chunk) => { + if (Chunk.isEmpty(chunk)) { + return [Chunk.empty(), Chunk.empty()] + } + return [Chunk.unsafeHead(chunk), Chunk.drop(1)(chunk)] + }), + Effect.flatMap((chunks) => Queue.offerAll(queue, chunks)), + Effect.asVoid + ), + proceed: pipe(Queue.offer(ps, void 0), Effect.asVoid), + awaitNext: Queue.take(ps) + } + }) diff --git a/repos/effect/packages/effect/test/utils/counter.ts b/repos/effect/packages/effect/test/utils/counter.ts new file mode 100644 index 0000000..f10d351 --- /dev/null +++ b/repos/effect/packages/effect/test/utils/counter.ts @@ -0,0 +1,54 @@ +import * as Effect from "effect/Effect" +import { pipe } from "effect/Function" +import * as Ref from "effect/Ref" +import type * as Scope from "effect/Scope" + +interface Counter { + acquire(): Effect.Effect + incrementAcquire(): Effect.Effect + incrementRelease(): Effect.Effect + acquired(): Effect.Effect + released(): Effect.Effect +} + +class CounterImpl implements Counter { + constructor(readonly ref: Ref.Ref) {} + + acquire(): Effect.Effect { + return pipe( + this.incrementAcquire(), + Effect.zipRight(Effect.addFinalizer(() => this.incrementRelease())), + Effect.zipRight(this.acquired()), + Effect.uninterruptible + ) + } + + incrementAcquire(): Effect.Effect { + return Ref.modify(this.ref, ([acquire, release]) => [acquire + 1, [acquire + 1, release] as const] as const) + } + + incrementRelease(): Effect.Effect { + return Ref.modify(this.ref, ([acquire, release]) => [release + 1, [acquire, release + 1] as const] as const) + } + + acquired(): Effect.Effect { + return pipe( + Ref.get(this.ref), + Effect.map((tuple) => tuple[0]) + ) + } + + released(): Effect.Effect { + return pipe( + Ref.get(this.ref), + Effect.map((tuple) => tuple[1]) + ) + } +} + +export const make = (): Effect.Effect => { + return pipe( + Ref.make([0, 0]), + Effect.map((ref) => new CounterImpl(ref as any)) + ) +} diff --git a/repos/effect/packages/effect/test/utils/equals.ts b/repos/effect/packages/effect/test/utils/equals.ts new file mode 100644 index 0000000..ba21dfe --- /dev/null +++ b/repos/effect/packages/effect/test/utils/equals.ts @@ -0,0 +1,11 @@ +import * as Equal from "effect/Equal" +import type * as Equivalence from "effect/Equivalence" + +export const equivalentElements = (): Equivalence.Equivalence => (x, y) => { + if (Array.isArray(x) && Array.isArray(y)) { + if (x.length === y.length) { + return x.every((v, i) => Equal.equals(v, y[i])) + } + } + return Equal.equals(x, y) +} diff --git a/repos/effect/packages/effect/test/utils/latch.ts b/repos/effect/packages/effect/test/utils/latch.ts new file mode 100644 index 0000000..2c05f51 --- /dev/null +++ b/repos/effect/packages/effect/test/utils/latch.ts @@ -0,0 +1,32 @@ +import * as Deferred from "effect/Deferred" +import * as Effect from "effect/Effect" +import { pipe } from "effect/Function" +import * as Ref from "effect/Ref" + +export const withLatch = ( + f: (release: Effect.Effect) => Effect.Effect +): Effect.Effect => { + return pipe( + Deferred.make(), + Effect.flatMap((latch) => + pipe(f(pipe(Deferred.succeed(latch, void 0), Effect.asVoid)), Effect.zipLeft(Deferred.await(latch))) + ) + ) +} + +export const withLatchAwait = ( + f: (release: Effect.Effect, wait: Effect.Effect) => Effect.Effect +): Effect.Effect => { + return Effect.gen(function*() { + const ref = yield* Ref.make(true) + const latch = yield* Deferred.make() + const result = yield* f( + pipe(Deferred.succeed(latch, void 0), Effect.asVoid), + Effect.uninterruptibleMask((restore) => + pipe(Ref.set(ref, false), Effect.zipRight(restore(Deferred.await(latch)))) + ) + ) + yield* Deferred.await(latch).pipe(Effect.whenEffect(Ref.get(ref))) + return result + }) +} diff --git a/repos/effect/packages/effect/test/utils/unfoldEffect.ts b/repos/effect/packages/effect/test/utils/unfoldEffect.ts new file mode 100644 index 0000000..b0735f9 --- /dev/null +++ b/repos/effect/packages/effect/test/utils/unfoldEffect.ts @@ -0,0 +1,25 @@ +import * as Effect from "effect/Effect" +import * as List from "effect/List" +import * as Option from "effect/Option" + +export const unfoldEffect = ( + s: S, + f: (s: S) => Effect.Effect, E, R> +): Effect.Effect, E, R> => + Effect.map( + unfoldEffectLoop(s, f, List.empty()), + (list) => Array.from(List.reverse(list)) + ) + +const unfoldEffectLoop = ( + s: S, + f: (s: S) => Effect.Effect, E, R>, + acc: List.List +): Effect.Effect, E, R> => + Effect.flatMap(f(s), (option) => { + if (Option.isSome(option)) { + return unfoldEffectLoop(option.value[1], f, List.prepend(acc, option.value[0])) + } else { + return Effect.succeed(acc) + } + }) diff --git a/repos/effect/packages/effect/tsconfig.build.json b/repos/effect/packages/effect/tsconfig.build.json new file mode 100644 index 0000000..152f93d --- /dev/null +++ b/repos/effect/packages/effect/tsconfig.build.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.src.json", + "compilerOptions": { + "types": ["node"], + "tsBuildInfoFile": ".tsbuildinfo/build.tsbuildinfo", + "outDir": "build/esm", + "declarationDir": "build/dts", + "stripInternal": true + } +} diff --git a/repos/effect/packages/effect/tsconfig.json b/repos/effect/packages/effect/tsconfig.json new file mode 100644 index 0000000..2c291d2 --- /dev/null +++ b/repos/effect/packages/effect/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.base.json", + "include": [], + "references": [ + { "path": "tsconfig.src.json" }, + { "path": "tsconfig.test.json" } + ] +} diff --git a/repos/effect/packages/effect/tsconfig.src.json b/repos/effect/packages/effect/tsconfig.src.json new file mode 100644 index 0000000..375b1f0 --- /dev/null +++ b/repos/effect/packages/effect/tsconfig.src.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.base.json", + "include": ["src"], + "compilerOptions": { + "types": ["node"], + "outDir": "build/src", + "tsBuildInfoFile": ".tsbuildinfo/src.tsbuildinfo", + "rootDir": "src" + } +} diff --git a/repos/effect/packages/effect/tsconfig.test.json b/repos/effect/packages/effect/tsconfig.test.json new file mode 100644 index 0000000..854c19a --- /dev/null +++ b/repos/effect/packages/effect/tsconfig.test.json @@ -0,0 +1,14 @@ +{ + "extends": "../../tsconfig.base.json", + "include": ["test"], + "references": [ + { "path": "tsconfig.src.json" }, + { "path": "../vitest/tsconfig.src.json" } + ], + "compilerOptions": { + "types": ["node"], + "tsBuildInfoFile": ".tsbuildinfo/test.tsbuildinfo", + "rootDir": "test", + "outDir": "build/test" // Some packages import test files from `effect`, hence we need to emit `d.ts` files here. + } +} diff --git a/repos/effect/packages/effect/vitest.config.ts b/repos/effect/packages/effect/vitest.config.ts new file mode 100644 index 0000000..15def63 --- /dev/null +++ b/repos/effect/packages/effect/vitest.config.ts @@ -0,0 +1,13 @@ +import { mergeConfig, type ViteUserConfig } from "vitest/config" +import shared from "../../vitest.shared.js" + +const config: ViteUserConfig = { + test: { + coverage: { + reporter: ["html"], + include: ["src/Schema.ts"] + } + } +} + +export default mergeConfig(shared, config) diff --git a/repos/effect/packages/experimental/CHANGELOG.md b/repos/effect/packages/experimental/CHANGELOG.md new file mode 100644 index 0000000..d48c7f6 --- /dev/null +++ b/repos/effect/packages/experimental/CHANGELOG.md @@ -0,0 +1,4087 @@ +# @effect/experimental + +## 0.60.0 + +### Patch Changes + +- Updated dependencies [[`f7bb09b`](https://github.com/Effect-TS/effect/commit/f7bb09b022f195d1f2b3c23d49e74b011ec5d109), [`bd7552a`](https://github.com/Effect-TS/effect/commit/bd7552a19cc0ed575507ac6cc0879a57e24ebd31), [`ad1a7eb`](https://github.com/Effect-TS/effect/commit/ad1a7eb7f6bebaf91c80be2443ac0439226d0098), [`0d32048`](https://github.com/Effect-TS/effect/commit/0d32048f9836e2b23a6ba3ec5f43f0a000bb92fb), [`0d32048`](https://github.com/Effect-TS/effect/commit/0d32048f9836e2b23a6ba3ec5f43f0a000bb92fb)]: + - effect@3.21.0 + - @effect/platform@0.96.0 + +## 0.59.0 + +### Patch Changes + +- Updated dependencies [[`fc82e81`](https://github.com/Effect-TS/effect/commit/fc82e81448bd9136a37580139ce46a2c61b11b54), [`82996bc`](https://github.com/Effect-TS/effect/commit/82996bce8debffcb44feb98bb862cf2662bd56b7), [`4d97a61`](https://github.com/Effect-TS/effect/commit/4d97a61a15b9dd6a0eece65b8f0c035e16d42ada), [`f6b0960`](https://github.com/Effect-TS/effect/commit/f6b0960bf3184109920dfed16ee7dfd7d67bc0f2), [`8798a84`](https://github.com/Effect-TS/effect/commit/8798a843218e6c0c0d3a8eee83360880e370b4da)]: + - effect@3.20.0 + - @effect/platform@0.95.0 + +## 0.58.0 + +### Patch Changes + +- Updated dependencies [[`77eeb86`](https://github.com/Effect-TS/effect/commit/77eeb86ddf208e51ec25932af83d52d3b4700371), [`ff7053f`](https://github.com/Effect-TS/effect/commit/ff7053f6d8508567b6145239f97aacc5773b0c53), [`287c32c`](https://github.com/Effect-TS/effect/commit/287c32c9f10da8e96f2b9ef8424316189d9ad4b3)]: + - effect@3.19.13 + - @effect/platform@0.94.0 + +## 0.57.11 + +### Patch Changes + +- [#5864](https://github.com/Effect-TS/effect/pull/5864) [`ebe2e52`](https://github.com/Effect-TS/effect/commit/ebe2e5278420d4b2bf30113a3b0a5ed68382d870) Thanks @mattiamanzati! - Ensure Devtools Tracer does not cut off span options + +- Updated dependencies [[`3f9bbfe`](https://github.com/Effect-TS/effect/commit/3f9bbfe9ef78303ecc6817b68ec9671f4d42d249)]: + - effect@3.19.9 + +## 0.57.10 + +### Patch Changes + +- [#5847](https://github.com/Effect-TS/effect/pull/5847) [`96c9537`](https://github.com/Effect-TS/effect/commit/96c9537f73a87a651c348488bdce7efbfd8360d1) Thanks @tim-smart! - ensure PersistedQueue memory driver removes items + +## 0.57.9 + +### Patch Changes + +- [#5837](https://github.com/Effect-TS/effect/pull/5837) [`811852a`](https://github.com/Effect-TS/effect/commit/811852a61868136bb7b3367450f02e5a8fb8a3f9) Thanks @tim-smart! - support idempotent offers to PersistedQueue + +## 0.57.8 + +### Patch Changes + +- [#5829](https://github.com/Effect-TS/effect/pull/5829) [`7b2cd37`](https://github.com/Effect-TS/effect/commit/7b2cd374cbc8b7a2e800926082febf54173c9e49) Thanks @tim-smart! - reset redis staging area on a schedule for PersistedQueue + +## 0.57.7 + +### Patch Changes + +- [#5820](https://github.com/Effect-TS/effect/pull/5820) [`c379c45`](https://github.com/Effect-TS/effect/commit/c379c45777cea3ec8c985d55f597902813bd3ae2) Thanks @tim-smart! - ensure redis PersistedQueue removes stale pending items on reset + +- Updated dependencies [[`f03b8e5`](https://github.com/Effect-TS/effect/commit/f03b8e55f12019cc855a1306e9cbfc7611a9e281)]: + - effect@3.19.8 + +## 0.57.6 + +### Patch Changes + +- [#5816](https://github.com/Effect-TS/effect/pull/5816) [`a9206ce`](https://github.com/Effect-TS/effect/commit/a9206ceee64daf3b12a778b60ee2bfdede748c57) Thanks @tim-smart! - add dynamic batch size to PersistedQueue based on pending takers + +## 0.57.5 + +### Patch Changes + +- [#5808](https://github.com/Effect-TS/effect/pull/5808) [`7b23d9a`](https://github.com/Effect-TS/effect/commit/7b23d9a812d5c100d9d9af16bd50251ea2f91b4b) Thanks @tim-smart! - add PersistedQueue module + +## 0.57.4 + +### Patch Changes + +- [#5773](https://github.com/Effect-TS/effect/pull/5773) [`9283499`](https://github.com/Effect-TS/effect/commit/9283499a1c1e6caa9cc9090c8419e8d49b575cd8) Thanks @tim-smart! - add RateLimiter helpers `makeWithRateLimiter` and `makeSleep` + +## 0.57.3 + +### Patch Changes + +- [#5763](https://github.com/Effect-TS/effect/pull/5763) [`1df657e`](https://github.com/Effect-TS/effect/commit/1df657e4688f602262d854947c15634849f2ba98) Thanks @tim-smart! - fix RateLimiter fixed window alogrithim + +## 0.57.2 + +### Patch Changes + +- [#5761](https://github.com/Effect-TS/effect/pull/5761) [`1deeb6a`](https://github.com/Effect-TS/effect/commit/1deeb6a58cde29e99f57c56d9adcdcda7ac610d0) Thanks @tim-smart! - fix partial window calulation for fixed window rate limiter + +- Updated dependencies [[`e144f02`](https://github.com/Effect-TS/effect/commit/e144f02c93258f0bb37bd10ee9849f2836914e2f)]: + - @effect/platform@0.93.3 + +## 0.57.1 + +### Patch Changes + +- [#5731](https://github.com/Effect-TS/effect/pull/5731) [`796a3b5`](https://github.com/Effect-TS/effect/commit/796a3b5aa3f6e0bd85583cc59f39bc059403345a) Thanks @tim-smart! - add persisted RateLimiter to @effect/experimental + +## 0.57.0 + +### Patch Changes + +- Updated dependencies [[`3c15d5f`](https://github.com/Effect-TS/effect/commit/3c15d5f99fb8d8470a00c5a33d9ba3cac89dfe4c), [`3863fa8`](https://github.com/Effect-TS/effect/commit/3863fa89f61e63e5529fd961e37333bddf7db64a), [`2a03c76`](https://github.com/Effect-TS/effect/commit/2a03c76c2781ca7e9e228e838eab2eb0d0795b1d), [`24a1685`](https://github.com/Effect-TS/effect/commit/24a1685c70a9ed157468650f95a5c3da3f2c2433)]: + - effect@3.19.0 + - @effect/platform@0.93.0 + +## 0.56.0 + +### Patch Changes + +- Updated dependencies [[`1c6ab74`](https://github.com/Effect-TS/effect/commit/1c6ab74b314b2b6df8bb1b1a0cb9527ceda0e3fa), [`70fe803`](https://github.com/Effect-TS/effect/commit/70fe803469db3355ffbf8359b52c351f1c2dc137), [`c296e32`](https://github.com/Effect-TS/effect/commit/c296e32554143b84ae8987046984e1cf1852417c), [`a098ddf`](https://github.com/Effect-TS/effect/commit/a098ddfc551f5aa0a7c36f9b4928372a64d4d9f2)]: + - effect@3.18.0 + - @effect/platform@0.92.0 + +## 0.55.0 + +### Patch Changes + +- Updated dependencies [[`d4d86a8`](https://github.com/Effect-TS/effect/commit/d4d86a81f02b94e09fce8004ce2c5369c505ca5a)]: + - @effect/platform@0.91.0 + +## 0.54.6 + +### Patch Changes + +- [#5385](https://github.com/Effect-TS/effect/pull/5385) [`9951993`](https://github.com/Effect-TS/effect/commit/995199396ee7031033bea1a9c890a6db066e870a) Thanks @tim-smart! - add setMany to ResultPersistence + +## 0.54.5 + +### Patch Changes + +- [#5365](https://github.com/Effect-TS/effect/pull/5365) [`b254747`](https://github.com/Effect-TS/effect/commit/b254747d478d451f3a04dac2f9050bd89176fd19) Thanks @tim-smart! - only trigger Reactivity invalidation on successful mutation + +## 0.54.4 + +### Patch Changes + +- [#5342](https://github.com/Effect-TS/effect/pull/5342) [`a9554ea`](https://github.com/Effect-TS/effect/commit/a9554ea53a4524b773426571338d14f1c1046d18) Thanks @tim-smart! - add Reactivity accessor apis + +- [#5344](https://github.com/Effect-TS/effect/pull/5344) [`1765ca8`](https://github.com/Effect-TS/effect/commit/1765ca8dddde47a7602083c46898b07a80c3cd28) Thanks @tim-smart! - add Reactivity.unsafeRegister + +## 0.54.3 + +### Patch Changes + +- [#5307](https://github.com/Effect-TS/effect/pull/5307) [`1f028e5`](https://github.com/Effect-TS/effect/commit/1f028e5d286fdfdc12357a3da3955c9446ce7a0c) Thanks @tim-smart! - locally provide EventLog service where possible + +- Updated dependencies [[`7d7c55d`](https://github.com/Effect-TS/effect/commit/7d7c55dadeea2f9de16e60abff124085733e1953)]: + - effect@3.17.4 + +## 0.54.2 + +### Patch Changes + +- [#5299](https://github.com/Effect-TS/effect/pull/5299) [`0138aa5`](https://github.com/Effect-TS/effect/commit/0138aa5fdf1af8bc206809201cebd75a865fa020) Thanks @tim-smart! - use base64 for EventLog.Identity strings + +## 0.54.1 + +### Patch Changes + +- [#5286](https://github.com/Effect-TS/effect/pull/5286) [`ffc17c6`](https://github.com/Effect-TS/effect/commit/ffc17c6d129adb898946d543b772af951b97e08d) Thanks @tim-smart! - add EventLog.layerIdentityKvs + +## 0.54.0 + +### Patch Changes + +- Updated dependencies [[`7813640`](https://github.com/Effect-TS/effect/commit/7813640279d9e3a3e7fc0a29bfb5c6d5fb3c270f)]: + - @effect/platform@0.90.0 + +## 0.53.0 + +### Patch Changes + +- Updated dependencies [[`40c3c87`](https://github.com/Effect-TS/effect/commit/40c3c875f724264312b43002859c82bed9ad0df9), [`ed2c74a`](https://github.com/Effect-TS/effect/commit/ed2c74ae8fa4ea0dd06ea84a3e58cd32e6916104), [`073a1b8`](https://github.com/Effect-TS/effect/commit/073a1b8be5dbfa87454393ee7346f5bc36a4fd63), [`f382e99`](https://github.com/Effect-TS/effect/commit/f382e99e409838a879246250fc3994b9bf5b3c2c), [`e8c7ba5`](https://github.com/Effect-TS/effect/commit/e8c7ba5fd3eb0c3ae3039fc24c09d69391987989), [`7e10415`](https://github.com/Effect-TS/effect/commit/7e1041599ade25103428703f5d2dfd7378a09636), [`e9bdece`](https://github.com/Effect-TS/effect/commit/e9bdececdc24f60a246be5055eca71a0d49ea7f2), [`8d95eb0`](https://github.com/Effect-TS/effect/commit/8d95eb0356b1d1736204836c275d201a547d208d)]: + - effect@3.17.0 + - @effect/platform@0.89.0 + +## 0.52.2 + +### Patch Changes + +- [#5219](https://github.com/Effect-TS/effect/pull/5219) [`d720f10`](https://github.com/Effect-TS/effect/commit/d720f1072770fa08ff81990e4f938f75a2399337) Thanks @tim-smart! - optimize experimental dataLoader + +## 0.52.1 + +### Patch Changes + +- [#5211](https://github.com/Effect-TS/effect/pull/5211) [`d25f22b`](https://github.com/Effect-TS/effect/commit/d25f22be7598abe977caf6cdac3b0dd78b438c48) Thanks @mattiamanzati! - Removed some unnecessary single-arg pipe calls + +- Updated dependencies [[`f5dfabf`](https://github.com/Effect-TS/effect/commit/f5dfabf51ba481a4468c1509c537314978ef6cec), [`17a5ea8`](https://github.com/Effect-TS/effect/commit/17a5ea8fa29785fe6e4c9480f2a2e9c8c59f3f38), [`d25f22b`](https://github.com/Effect-TS/effect/commit/d25f22be7598abe977caf6cdac3b0dd78b438c48)]: + - effect@3.16.14 + - @effect/platform@0.88.1 + +## 0.52.0 + +### Patch Changes + +- Updated dependencies [[`27206d7`](https://github.com/Effect-TS/effect/commit/27206d7f0558d7fe28de57bf54f1d0cc83acc92e), [`dbabf5e`](https://github.com/Effect-TS/effect/commit/dbabf5e76fa63b050d2b6c466713c7dc59f07d3c)]: + - @effect/platform@0.88.0 + +## 0.51.14 + +### Patch Changes + +- Updated dependencies [[`c1c05a8`](https://github.com/Effect-TS/effect/commit/c1c05a8242fb5df7445b4a12387a60eac7726eb7), [`81fe4a2`](https://github.com/Effect-TS/effect/commit/81fe4a2c81d5e30e180a60e68c52016a27b350db)]: + - effect@3.16.13 + - @effect/platform@0.87.13 + +## 0.51.13 + +### Patch Changes + +- Updated dependencies [[`32ba77a`](https://github.com/Effect-TS/effect/commit/32ba77ae304d2161362a73e8b61965332626cf2d), [`d5e25b2`](https://github.com/Effect-TS/effect/commit/d5e25b237f05670ee42b386cb40b2cb448fc11d7)]: + - @effect/platform@0.87.12 + +## 0.51.12 + +### Patch Changes + +- Updated dependencies [[`001392b`](https://github.com/Effect-TS/effect/commit/001392ba8bfcad101bb034348a7415012fb12f72), [`7bfb099`](https://github.com/Effect-TS/effect/commit/7bfb099cb5528511b8d63045c4fbb4dc9cb18528)]: + - @effect/platform@0.87.11 + +## 0.51.11 + +### Patch Changes + +- Updated dependencies [[`678318d`](https://github.com/Effect-TS/effect/commit/678318d2e88233156b006acda56c2d138ee3ffa0), [`678318d`](https://github.com/Effect-TS/effect/commit/678318d2e88233156b006acda56c2d138ee3ffa0)]: + - @effect/platform@0.87.10 + +## 0.51.10 + +### Patch Changes + +- Updated dependencies [[`54514a2`](https://github.com/Effect-TS/effect/commit/54514a2f53166de27ad7e756dbf12194691fd4af)]: + - @effect/platform@0.87.9 + +## 0.51.9 + +### Patch Changes + +- Updated dependencies [[`4ce4f82`](https://github.com/Effect-TS/effect/commit/4ce4f824f6fdef492be1d35c05a490ffce518c89)]: + - @effect/platform@0.87.8 + +## 0.51.8 + +### Patch Changes + +- Updated dependencies [[`a9b617f`](https://github.com/Effect-TS/effect/commit/a9b617f125171ed76cd79ab46d7a924daf3b0e70), [`7e26e86`](https://github.com/Effect-TS/effect/commit/7e26e86524abcc93713d6ad7eee486638c98f7c2)]: + - @effect/platform@0.87.7 + +## 0.51.7 + +### Patch Changes + +- Updated dependencies [[`905da99`](https://github.com/Effect-TS/effect/commit/905da996aad665057b4ca6dba1a4af44fb8835bd)]: + - effect@3.16.12 + - @effect/platform@0.87.6 + +## 0.51.6 + +### Patch Changes + +- [#5144](https://github.com/Effect-TS/effect/pull/5144) [`96c1292`](https://github.com/Effect-TS/effect/commit/96c129262835410b311a51d0bf7f58b8f6fc9a12) Thanks @tim-smart! - simplify VariantSchema Class types + +## 0.51.5 + +### Patch Changes + +- Updated dependencies [[`2fd8676`](https://github.com/Effect-TS/effect/commit/2fd8676c803cd40000dfc3231f5daecaa0e0ebd2)]: + - @effect/platform@0.87.5 + +## 0.51.4 + +### Patch Changes + +- Updated dependencies [[`e82a4fd`](https://github.com/Effect-TS/effect/commit/e82a4fd60f6528d08cef1a4aba0abe0d3ba741ad)]: + - @effect/platform@0.87.4 + +## 0.51.3 + +### Patch Changes + +- Updated dependencies [[`1b6e396`](https://github.com/Effect-TS/effect/commit/1b6e396d699f3cbbc56b68f99055cf746529bb9e)]: + - @effect/platform@0.87.3 + +## 0.51.2 + +### Patch Changes + +- Updated dependencies [[`4fea68c`](https://github.com/Effect-TS/effect/commit/4fea68ca7a25a3c39a1ab68b3885534513ab0c81), [`b927954`](https://github.com/Effect-TS/effect/commit/b9279543cf5688dd8a577af80456959c615217d0), [`99590a6`](https://github.com/Effect-TS/effect/commit/99590a6ca9128eb1ede265b6670b655311995614), [`6c3e24c`](https://github.com/Effect-TS/effect/commit/6c3e24c2308f7d4a29b8f4270ab81bca22ac6bb4)]: + - @effect/platform@0.87.2 + - effect@3.16.11 + +## 0.51.1 + +### Patch Changes + +- Updated dependencies [[`faad30e`](https://github.com/Effect-TS/effect/commit/faad30ec8742916be59f9db642d0fc98225b636c)]: + - effect@3.16.10 + - @effect/platform@0.87.1 + +## 0.51.0 + +### Patch Changes + +- Updated dependencies [[`b5bac9a`](https://github.com/Effect-TS/effect/commit/b5bac9ac2913fcd11b02322624f03b544eef53ba)]: + - @effect/platform@0.87.0 + +## 0.50.0 + +### Patch Changes + +- Updated dependencies [[`5137c70`](https://github.com/Effect-TS/effect/commit/5137c703461d8d3b363c112140a6e7f798241d07), [`c23d25c`](https://github.com/Effect-TS/effect/commit/c23d25c3e7c541f1f63b28484d8c461d86c67e99), [`5137c70`](https://github.com/Effect-TS/effect/commit/5137c703461d8d3b363c112140a6e7f798241d07), [`5137c70`](https://github.com/Effect-TS/effect/commit/5137c703461d8d3b363c112140a6e7f798241d07)]: + - effect@3.16.9 + - @effect/platform@0.86.0 + +## 0.49.2 + +### Patch Changes + +- Updated dependencies [[`914a191`](https://github.com/Effect-TS/effect/commit/914a191e7cb6341a3d0e965bccd27c336cf22e44)]: + - @effect/platform@0.85.2 + +## 0.49.1 + +### Patch Changes + +- Updated dependencies [[`8cb98d5`](https://github.com/Effect-TS/effect/commit/8cb98d53e68330228287ce2a2e0d8a4c86bcab3b), [`db2dd3c`](https://github.com/Effect-TS/effect/commit/db2dd3c3a8a77d791eae19e66153527e1cde4e6e)]: + - effect@3.16.8 + - @effect/platform@0.85.1 + +## 0.49.0 + +### Patch Changes + +- Updated dependencies [[`93687dd`](https://github.com/Effect-TS/effect/commit/93687ddbb25ce3b324cd2b83d2ccff225e97307e), [`93687dd`](https://github.com/Effect-TS/effect/commit/93687ddbb25ce3b324cd2b83d2ccff225e97307e), [`93687dd`](https://github.com/Effect-TS/effect/commit/93687ddbb25ce3b324cd2b83d2ccff225e97307e)]: + - @effect/platform@0.85.0 + +## 0.48.12 + +### Patch Changes + +- Updated dependencies [[`1bb0d8a`](https://github.com/Effect-TS/effect/commit/1bb0d8ab96782e99434356266b38251554ea0294)]: + - effect@3.16.7 + - @effect/platform@0.84.11 + +## 0.48.11 + +### Patch Changes + +- Updated dependencies [[`a5f7595`](https://github.com/Effect-TS/effect/commit/a5f75956ef9a15a83c416517ef493f0ee2f5ee8a), [`a02470c`](https://github.com/Effect-TS/effect/commit/a02470c75579e91525a25adb3f21b3650d042fdd), [`bf369b2`](https://github.com/Effect-TS/effect/commit/bf369b2902a0e0b195d957c18b9efd180942cf8b), [`f891d45`](https://github.com/Effect-TS/effect/commit/f891d45adffdafd3f94a2eca23faa354e3a409a8)]: + - effect@3.16.6 + - @effect/platform@0.84.10 + +## 0.48.10 + +### Patch Changes + +- Updated dependencies [[`bf418ef`](https://github.com/Effect-TS/effect/commit/bf418ef14a0f2ec965535793d5cea8fa8ba177ac)]: + - effect@3.16.5 + - @effect/platform@0.84.9 + +## 0.48.9 + +### Patch Changes + +- Updated dependencies [[`8b9db77`](https://github.com/Effect-TS/effect/commit/8b9db7742846af0f58fd8e8b7acb7f4f5ff487ec)]: + - @effect/platform@0.84.8 + +## 0.48.8 + +### Patch Changes + +- Updated dependencies [[`74ab9a0`](https://github.com/Effect-TS/effect/commit/74ab9a0a9e16d6e019369d256e1e24175c8bc3f3), [`770008e`](https://github.com/Effect-TS/effect/commit/770008eca3aad2899a2ed951236e575793294b28)]: + - effect@3.16.4 + - @effect/platform@0.84.7 + +## 0.48.7 + +### Patch Changes + +- [#4978](https://github.com/Effect-TS/effect/pull/4978) [`a2d57c9`](https://github.com/Effect-TS/effect/commit/a2d57c9ac596445009ca12859b78e00e5d89b936) Thanks @mattiamanzati! - Fix Machine initialize R argument + +## 0.48.6 + +### Patch Changes + +- Updated dependencies [[`ceea77a`](https://github.com/Effect-TS/effect/commit/ceea77a13055f145520f763e3fce5b8ff15d728f)]: + - @effect/platform@0.84.6 + +## 0.48.5 + +### Patch Changes + +- Updated dependencies [[`ec52c6a`](https://github.com/Effect-TS/effect/commit/ec52c6a2211e76972462b15b9d5a9d6d56761b7a)]: + - @effect/platform@0.84.5 + +## 0.48.4 + +### Patch Changes + +- Updated dependencies [[`87722fc`](https://github.com/Effect-TS/effect/commit/87722fce693a9b49284bbddbf82d30714c688261), [`36217ee`](https://github.com/Effect-TS/effect/commit/36217eeb1337edd9ac3f9a635b80a6385d22ae8f)]: + - effect@3.16.3 + - @effect/platform@0.84.4 + +## 0.48.3 + +### Patch Changes + +- Updated dependencies [[`ab7684f`](https://github.com/Effect-TS/effect/commit/ab7684f1c2a0671bf091f255d220e3a4cc7f528e)]: + - @effect/platform@0.84.3 + +## 0.48.2 + +### Patch Changes + +- Updated dependencies [[`0ddf148`](https://github.com/Effect-TS/effect/commit/0ddf148a247aa87af043d276b8453a714a400897)]: + - effect@3.16.2 + - @effect/platform@0.84.2 + +## 0.48.1 + +### Patch Changes + +- Updated dependencies [[`71174d0`](https://github.com/Effect-TS/effect/commit/71174d09691314a9b6b66189e456fd21e3eb6543), [`d615e6e`](https://github.com/Effect-TS/effect/commit/d615e6e5b944f6fd5e627e31752c7ca7e4e1c17d)]: + - @effect/platform@0.84.1 + - effect@3.16.1 + +## 0.48.0 + +### Patch Changes + +- Updated dependencies [[`ee0bd5d`](https://github.com/Effect-TS/effect/commit/ee0bd5d24864752c54cb359f67a67dd903971ec4), [`5189800`](https://github.com/Effect-TS/effect/commit/51898004e11766b8cf6d95e960b636f6d5db79ec), [`58bfeaa`](https://github.com/Effect-TS/effect/commit/58bfeaa64ded8c88f772b184311c0c0dbac10960), [`194d748`](https://github.com/Effect-TS/effect/commit/194d7486943f56f3267ef415395ac220a4b3e634), [`918c9ea`](https://github.com/Effect-TS/effect/commit/918c9ea1a57facb154f0fb26792021f337054dee), [`9198e6f`](https://github.com/Effect-TS/effect/commit/9198e6fcc1a3ff4fefb3363004de558d8de01f40), [`2a370bf`](https://github.com/Effect-TS/effect/commit/2a370bf625fdeede5659721468eb0d527e403279), [`58ccb91`](https://github.com/Effect-TS/effect/commit/58ccb91328c8df5d49808b673738bc09df355201), [`fd47834`](https://github.com/Effect-TS/effect/commit/fd478348203fa89462b0a1d067ce4de034353df4)]: + - effect@3.16.0 + - @effect/platform@0.84.0 + +## 0.47.0 + +### Patch Changes + +- Updated dependencies [[`5522520`](https://github.com/Effect-TS/effect/commit/55225206ab9af0ad60b1c0654690a8a096d625cd), [`cc5bb2b`](https://github.com/Effect-TS/effect/commit/cc5bb2b918a9450a975f702dabcea891bda382cb)]: + - @effect/platform@0.83.0 + - effect@3.15.5 + +## 0.46.8 + +### Patch Changes + +- Updated dependencies [[`0617b9d`](https://github.com/Effect-TS/effect/commit/0617b9dc365f1963b36949ad7f9023ab6eb94524)]: + - @effect/platform@0.82.8 + +## 0.46.7 + +### Patch Changes + +- Updated dependencies [[`f570554`](https://github.com/Effect-TS/effect/commit/f57055459524587b041340577dad85476bb35f81), [`78047e8`](https://github.com/Effect-TS/effect/commit/78047e8dfc8005b66f87afe50bb95981fea51561), [`c20b95a`](https://github.com/Effect-TS/effect/commit/c20b95a99ffe452b4774c844d397a905f713b6d6), [`94ada43`](https://github.com/Effect-TS/effect/commit/94ada430928d5685bdbef513e87562c20774a3a2)]: + - effect@3.15.4 + - @effect/platform@0.82.7 + +## 0.46.6 + +### Patch Changes + +- Updated dependencies [[`618903b`](https://github.com/Effect-TS/effect/commit/618903ba9ae96e2bfe6ee31f61c4359b915f2a36)]: + - @effect/platform@0.82.6 + +## 0.46.5 + +### Patch Changes + +- Updated dependencies [[`7764a07`](https://github.com/Effect-TS/effect/commit/7764a07d960c60df81f14e1dc949518f4bbe494a), [`4577f54`](https://github.com/Effect-TS/effect/commit/4577f548d67273e576cdde423bdd34a4b910766a), [`30a0d9c`](https://github.com/Effect-TS/effect/commit/30a0d9cb51c84290d51b1361d72ff5cee33c13c7)]: + - @effect/platform@0.82.5 + - effect@3.15.3 + +## 0.46.4 + +### Patch Changes + +- Updated dependencies [[`d45e8a8`](https://github.com/Effect-TS/effect/commit/d45e8a8ac8227192f504e39e6d04fdcf4fb1d225), [`d13b68e`](https://github.com/Effect-TS/effect/commit/d13b68e3a9456d0bfee9bca8273a7b44a9c69087)]: + - @effect/platform@0.82.4 + +## 0.46.3 + +### Patch Changes + +- Updated dependencies [[`b8722b8`](https://github.com/Effect-TS/effect/commit/b8722b817e2306fe8c8245f3f9e32d85b824b961), [`a328f4b`](https://github.com/Effect-TS/effect/commit/a328f4b4fe717dd53e5b04a30f387433c32f7328)]: + - effect@3.15.2 + - @effect/platform@0.82.3 + +## 0.46.2 + +### Patch Changes + +- Updated dependencies [[`739a3d4`](https://github.com/Effect-TS/effect/commit/739a3d4a4565915fe2e690003f4f9085cb4422fc)]: + - @effect/platform@0.82.2 + +## 0.46.1 + +### Patch Changes + +- Updated dependencies [[`787ce70`](https://github.com/Effect-TS/effect/commit/787ce7042e35b657963473c6efe47752868cd811), [`1269641`](https://github.com/Effect-TS/effect/commit/1269641a99ae43069f7648ff79ffe8729b54b348), [`1269641`](https://github.com/Effect-TS/effect/commit/1269641a99ae43069f7648ff79ffe8729b54b348)]: + - effect@3.15.1 + - @effect/platform@0.82.1 + +## 0.46.0 + +### Patch Changes + +- Updated dependencies [[`c654595`](https://github.com/Effect-TS/effect/commit/c65459587b51da140b78098e81fdbfece65d53e2), [`d9f5dea`](https://github.com/Effect-TS/effect/commit/d9f5deae0f02f5de2b9fcb1cca8b142ba4bc2bba), [`49aa723`](https://github.com/Effect-TS/effect/commit/49aa7236a15e13f818c86edbca08c4af67c8dfaf), [`74c14d0`](https://github.com/Effect-TS/effect/commit/74c14d01d0cb48cf517a1b6e29a373a96ed0ff5b), [`e4f49b6`](https://github.com/Effect-TS/effect/commit/e4f49b66857e01b74ab6a9a0bc7132f44cd04cbb), [`6f02224`](https://github.com/Effect-TS/effect/commit/6f02224b3fc46a682ad2defb1a260841956c6780), [`1dcfd41`](https://github.com/Effect-TS/effect/commit/1dcfd41ff96abd706901293a00c1893cb29dd8fd), [`b21ab16`](https://github.com/Effect-TS/effect/commit/b21ab16b6f773e7ec4369db4e752c35e719f7870), [`fcf1822`](https://github.com/Effect-TS/effect/commit/fcf1822f98fcda60351d64e9d2c2c13563d7e6db), [`0061dd1`](https://github.com/Effect-TS/effect/commit/0061dd140740165e91569a684cce27a77b23229e), [`8421e6e`](https://github.com/Effect-TS/effect/commit/8421e6e49332bca8f96f482dfd48680e238b3a89), [`a9b3fb7`](https://github.com/Effect-TS/effect/commit/a9b3fb78abcfdb525318a956fd02fcadeb56143e), [`fa10f56`](https://github.com/Effect-TS/effect/commit/fa10f56b96bd9af070ba99ebc3279aa93954261e)]: + - effect@3.15.0 + - @effect/platform@0.82.0 + +## 0.45.1 + +### Patch Changes + +- Updated dependencies [[`24a9ebb`](https://github.com/Effect-TS/effect/commit/24a9ebbb5af598f0bfd6ecc45307e528043fe011)]: + - effect@3.14.22 + - @effect/platform@0.81.1 + +## 0.45.0 + +### Patch Changes + +- Updated dependencies [[`672920f`](https://github.com/Effect-TS/effect/commit/672920f85da8abd5f9d4ad85e29248a2aca57ed8)]: + - @effect/platform@0.81.0 + +## 0.44.21 + +### Patch Changes + +- Updated dependencies [[`2f3b7d4`](https://github.com/Effect-TS/effect/commit/2f3b7d4e1fa1ef8790b0ca4da22eb88872ee31df)]: + - effect@3.14.21 + - @effect/platform@0.80.21 + +## 0.44.20 + +### Patch Changes + +- Updated dependencies [[`17e2f30`](https://github.com/Effect-TS/effect/commit/17e2f3091408cf0fca9414d4af3bdf7b2765b378)]: + - effect@3.14.20 + - @effect/platform@0.80.20 + +## 0.44.19 + +### Patch Changes + +- Updated dependencies [[`056a910`](https://github.com/Effect-TS/effect/commit/056a910d0a0b8b00b0dc9df4a070466b2b5c2f6c), [`e25e7bb`](https://github.com/Effect-TS/effect/commit/e25e7bbc1797733916f48f501425d9f2ef310d9f), [`3273d57`](https://github.com/Effect-TS/effect/commit/3273d572c2b3175a842677f19efeea4cd65ab016)]: + - effect@3.14.19 + - @effect/platform@0.80.19 + +## 0.44.18 + +### Patch Changes + +- Updated dependencies [[`b1164d4`](https://github.com/Effect-TS/effect/commit/b1164d49a1dfdf299e9971367b6fc6be4df0ddff)]: + - effect@3.14.18 + - @effect/platform@0.80.18 + +## 0.44.17 + +### Patch Changes + +- Updated dependencies [[`0b54681`](https://github.com/Effect-TS/effect/commit/0b54681cd89245e211d8f49272be0f1bf2f81813), [`41a59d5`](https://github.com/Effect-TS/effect/commit/41a59d5916a296b12b0d5ead9e859e05f40b4cce)]: + - effect@3.14.17 + - @effect/platform@0.80.17 + +## 0.44.16 + +### Patch Changes + +- Updated dependencies [[`ee14444`](https://github.com/Effect-TS/effect/commit/ee144441021ec77039e43396eaf90714687bb495), [`f1c8583`](https://github.com/Effect-TS/effect/commit/f1c8583f8c3ea9415f813795ca2940a897c9ba9a)]: + - effect@3.14.16 + - @effect/platform@0.80.16 + +## 0.44.15 + +### Patch Changes + +- Updated dependencies [[`239cc99`](https://github.com/Effect-TS/effect/commit/239cc995ce645946210a3c3d2cb52bd3547c0687), [`8b6c947`](https://github.com/Effect-TS/effect/commit/8b6c947eaa8e45a67ecb3c37d45cd27f3e41d165), [`c50a63b`](https://github.com/Effect-TS/effect/commit/c50a63bbecb9f560b9cae349c447eed877d1b9b6)]: + - effect@3.14.15 + - @effect/platform@0.80.15 + +## 0.44.14 + +### Patch Changes + +- Updated dependencies [[`6ed8d15`](https://github.com/Effect-TS/effect/commit/6ed8d1589beb181d30abc79afebdaabc1d101538)]: + - effect@3.14.14 + - @effect/platform@0.80.14 + +## 0.44.13 + +### Patch Changes + +- Updated dependencies [[`ee77788`](https://github.com/Effect-TS/effect/commit/ee77788747e7ebbde6bfa88256cde49dbbad3608), [`5fce6ba`](https://github.com/Effect-TS/effect/commit/5fce6ba19c3cc63cc0104e737e581ad989dedbf0), [`570e45f`](https://github.com/Effect-TS/effect/commit/570e45f8cb936e42ec48f67f21bb2b7252f36c0c)]: + - effect@3.14.13 + - @effect/platform@0.80.13 + +## 0.44.12 + +### Patch Changes + +- Updated dependencies [[`c2ad9ee`](https://github.com/Effect-TS/effect/commit/c2ad9ee9f3c4c743390edf35ed9e85a20be33811), [`9c68654`](https://github.com/Effect-TS/effect/commit/9c686542b6eb3ea188cb70673ef2e41223633e89)]: + - effect@3.14.12 + - @effect/platform@0.80.12 + +## 0.44.11 + +### Patch Changes + +- Updated dependencies [[`e536127`](https://github.com/Effect-TS/effect/commit/e536127c1e6f2fb3a542c73ae919435a629a346b)]: + - effect@3.14.11 + - @effect/platform@0.80.11 + +## 0.44.10 + +### Patch Changes + +- Updated dependencies [[`bc7efa3`](https://github.com/Effect-TS/effect/commit/bc7efa3b031bb25e1ed3c8f2d3fb5e8da166cadc)]: + - effect@3.14.10 + - @effect/platform@0.80.10 + +## 0.44.9 + +### Patch Changes + +- Updated dependencies [[`d78249f`](https://github.com/Effect-TS/effect/commit/d78249f0b67f63cf4baf806ff090cba33293daf0)]: + - effect@3.14.9 + - @effect/platform@0.80.9 + +## 0.44.8 + +### Patch Changes + +- Updated dependencies [[`b3a2d32`](https://github.com/Effect-TS/effect/commit/b3a2d32772e6f7f20eacf2e18128e99324c4d378)]: + - effect@3.14.8 + - @effect/platform@0.80.8 + +## 0.44.7 + +### Patch Changes + +- Updated dependencies [[`b542a4b`](https://github.com/Effect-TS/effect/commit/b542a4bf195be0c9af1523e1ba96c953decc4d25)]: + - effect@3.14.7 + - @effect/platform@0.80.7 + +## 0.44.6 + +### Patch Changes + +- Updated dependencies [[`47618c1`](https://github.com/Effect-TS/effect/commit/47618c1ad84ebcc5a51133a3fff5aa5012d49d45), [`6077882`](https://github.com/Effect-TS/effect/commit/60778824a4794336c33807801f813f8751d1c7e4)]: + - effect@3.14.6 + - @effect/platform@0.80.6 + +## 0.44.5 + +### Patch Changes + +- Updated dependencies [[`40dbfef`](https://github.com/Effect-TS/effect/commit/40dbfeff239b6e567706752114f31b2fce7de4e3), [`85fba81`](https://github.com/Effect-TS/effect/commit/85fba815ac07eb13d4227a69ac76a18e4b94df18), [`5a5ebdd`](https://github.com/Effect-TS/effect/commit/5a5ebdddfaddd259538b4599a6676281faca778e)]: + - effect@3.14.5 + - @effect/platform@0.80.5 + +## 0.44.4 + +### Patch Changes + +- Updated dependencies [[`e4ba2c6`](https://github.com/Effect-TS/effect/commit/e4ba2c66a878e81b5e295d6d49aaf724b80a28ef)]: + - effect@3.14.4 + - @effect/platform@0.80.4 + +## 0.44.3 + +### Patch Changes + +- Updated dependencies [[`37aa8e1`](https://github.com/Effect-TS/effect/commit/37aa8e137725a902e70cd1e468ea98b873aa5056), [`34f03d6`](https://github.com/Effect-TS/effect/commit/34f03d66875f21f266f102223a03cd14c2ed6ea6)]: + - effect@3.14.3 + - @effect/platform@0.80.3 + +## 0.44.2 + +### Patch Changes + +- Updated dependencies [[`f87991b`](https://github.com/Effect-TS/effect/commit/f87991b6d8a2edfaf90b01cebda4b466992ae865), [`f87991b`](https://github.com/Effect-TS/effect/commit/f87991b6d8a2edfaf90b01cebda4b466992ae865), [`0a3e3e1`](https://github.com/Effect-TS/effect/commit/0a3e3e18eea5e0d1882f1a6c906198e6ef226a41)]: + - effect@3.14.2 + - @effect/platform@0.80.2 + +## 0.44.1 + +### Patch Changes + +- Updated dependencies [[`4a274fe`](https://github.com/Effect-TS/effect/commit/4a274fe9f623182b6b902827e0e83bd89ca3b05c)]: + - effect@3.14.1 + - @effect/platform@0.80.1 + +## 0.44.0 + +### Minor Changes + +- [#4469](https://github.com/Effect-TS/effect/pull/4469) [`aba2d1d`](https://github.com/Effect-TS/effect/commit/aba2d1d831ea149481bd4dd755528c0afa8239ce) Thanks @tim-smart! - move the MsgPack, Ndjson & ChannelSchema modules to @effect/platform + +- [#4469](https://github.com/Effect-TS/effect/pull/4469) [`3131f8f`](https://github.com/Effect-TS/effect/commit/3131f8fd12ba9eb31b90fa2f42bf88b12309133c) Thanks @tim-smart! - refactor of @effect/cluster packages + +- [#4469](https://github.com/Effect-TS/effect/pull/4469) [`aba2d1d`](https://github.com/Effect-TS/effect/commit/aba2d1d831ea149481bd4dd755528c0afa8239ce) Thanks @tim-smart! - Move SocketServer modules to @effect/platform + +### Patch Changes + +- [#4469](https://github.com/Effect-TS/effect/pull/4469) [`aba2d1d`](https://github.com/Effect-TS/effect/commit/aba2d1d831ea149481bd4dd755528c0afa8239ce) Thanks @tim-smart! - use unhandled error log level for SocketServer errors + +- Updated dependencies [[`1f47e4e`](https://github.com/Effect-TS/effect/commit/1f47e4e12546ab691b29bfb7b5128bb17b93baa5), [`26dd75f`](https://github.com/Effect-TS/effect/commit/26dd75f276a0d8a63eab313bd5a167d5072c9780), [`aba2d1d`](https://github.com/Effect-TS/effect/commit/aba2d1d831ea149481bd4dd755528c0afa8239ce), [`3131f8f`](https://github.com/Effect-TS/effect/commit/3131f8fd12ba9eb31b90fa2f42bf88b12309133c), [`aba2d1d`](https://github.com/Effect-TS/effect/commit/aba2d1d831ea149481bd4dd755528c0afa8239ce), [`04dff2d`](https://github.com/Effect-TS/effect/commit/04dff2d01ac68c260f29a6d4743381825c353c86), [`c7fac0c`](https://github.com/Effect-TS/effect/commit/c7fac0cd7eadcd5cc0c3a987051c5b57ad271638), [`aba2d1d`](https://github.com/Effect-TS/effect/commit/aba2d1d831ea149481bd4dd755528c0afa8239ce), [`ffaa3f3`](https://github.com/Effect-TS/effect/commit/ffaa3f3969df26610fcc02ad537340641d44e803), [`ab957c1`](https://github.com/Effect-TS/effect/commit/ab957c1fee714868f56c7ab4e802b9d449e9b666), [`35db9ce`](https://github.com/Effect-TS/effect/commit/35db9ce228f1416c8abacc6dc9c36fbd0f33ef0f), [`aba2d1d`](https://github.com/Effect-TS/effect/commit/aba2d1d831ea149481bd4dd755528c0afa8239ce), [`cf77ea9`](https://github.com/Effect-TS/effect/commit/cf77ea9ab4fc89e66a43f682a9926ccdee6c57ed), [`26dd75f`](https://github.com/Effect-TS/effect/commit/26dd75f276a0d8a63eab313bd5a167d5072c9780), [`baaab60`](https://github.com/Effect-TS/effect/commit/baaab60b737f35dfab8e4a21bce28a195d19e899)]: + - effect@3.14.0 + - @effect/platform@0.80.0 + +## 0.43.4 + +### Patch Changes + +- Updated dependencies [[`5662363`](https://github.com/Effect-TS/effect/commit/566236361e270e575ef1cbf308ad1967c82a362c), [`5f1fd15`](https://github.com/Effect-TS/effect/commit/5f1fd15308ab154791580059b89877d19a2055c2), [`8bb1460`](https://github.com/Effect-TS/effect/commit/8bb1460c824f66f0f25ebd899c5e74e388089c37)]: + - @effect/platform@0.79.4 + - @effect/platform-node@0.75.4 + +## 0.43.3 + +### Patch Changes + +- Updated dependencies [[`0c4803f`](https://github.com/Effect-TS/effect/commit/0c4803fcc69262d11a97ce49d0e9b4288df0651f), [`6f65ac4`](https://github.com/Effect-TS/effect/commit/6f65ac4eac1489cd6ea390e18b0908670722adad)]: + - effect@3.13.12 + - @effect/platform@0.79.3 + - @effect/platform-node@0.75.3 + +## 0.43.2 + +### Patch Changes + +- Updated dependencies [[`fad8cca`](https://github.com/Effect-TS/effect/commit/fad8cca9bbfcc2eaeb44b97c15dbe0a1eda75315), [`4296293`](https://github.com/Effect-TS/effect/commit/4296293049414d0cf2d915a26c552b09f946b9a0), [`9c241ab`](https://github.com/Effect-TS/effect/commit/9c241abe47ccf7a5257b98a4a64a63054a12741d), [`082b0c1`](https://github.com/Effect-TS/effect/commit/082b0c1b9f4252bcdd69608f2e4a9226f953ac3f), [`be12983`](https://github.com/Effect-TS/effect/commit/be12983bc7e7537b41cd8910fc4eb7d1da56ab07), [`de88127`](https://github.com/Effect-TS/effect/commit/de88127a5a5906ccece98af74787b5ae0e65e431)]: + - effect@3.13.11 + - @effect/platform@0.79.2 + - @effect/platform-node@0.75.2 + +## 0.43.1 + +### Patch Changes + +- Updated dependencies [[`527c964`](https://github.com/Effect-TS/effect/commit/527c9645229f5be9714a7e60a38a9e753c4bbfb1)]: + - effect@3.13.10 + - @effect/platform@0.79.1 + - @effect/platform-node@0.75.1 + +## 0.43.0 + +### Patch Changes + +- Updated dependencies [[`bbdc279`](https://github.com/Effect-TS/effect/commit/bbdc2795a461cb2d1fe19b2669526a6ef590c3d4), [`88fe129`](https://github.com/Effect-TS/effect/commit/88fe12923740765c0335a6e6203fdcc6a463edca), [`d630249`](https://github.com/Effect-TS/effect/commit/d630249426113088abe8b382db4f14d80f2160c2), [`2976e52`](https://github.com/Effect-TS/effect/commit/2976e52538d9dc9ffdcbc84d4ac748cff9305971)]: + - @effect/platform-node@0.75.0 + - @effect/platform@0.79.0 + - effect@3.13.9 + +## 0.42.1 + +### Patch Changes + +- Updated dependencies [[`c65d336`](https://github.com/Effect-TS/effect/commit/c65d3362d07ec815ff3b46278314e8a31706ddc2), [`22d2ebb`](https://github.com/Effect-TS/effect/commit/22d2ebb4b11f5a44351a4736e65da391a3b647d0)]: + - effect@3.13.8 + - @effect/platform@0.78.1 + - @effect/platform-node@0.74.1 + +## 0.42.0 + +### Patch Changes + +- Updated dependencies [[`c5bcf53`](https://github.com/Effect-TS/effect/commit/c5bcf53b7cb49dacffdd2a6cd8eb48cc452b417e)]: + - @effect/platform@0.78.0 + - @effect/platform-node@0.74.0 + +## 0.41.7 + +### Patch Changes + +- Updated dependencies [[`840cc73`](https://github.com/Effect-TS/effect/commit/840cc7329908db7ca693ef47b07d4f845c29cadd), [`99fcbf7`](https://github.com/Effect-TS/effect/commit/99fcbf712d40a90ac5c8843237d26914146d7312), [`9bf8a74`](https://github.com/Effect-TS/effect/commit/9bf8a74b967f18d931743dd5196af326c9118e9c), [`87ba23c`](https://github.com/Effect-TS/effect/commit/87ba23c41c193503ed0c612b0d32d0b253794c64), [`f910880`](https://github.com/Effect-TS/effect/commit/f91088069057f3b4529753f5bc5532b028d726df), [`0d01480`](https://github.com/Effect-TS/effect/commit/0d014803e4f688f74386a80abd65485e1a319244)]: + - @effect/platform@0.77.7 + - effect@3.13.7 + - @effect/platform-node@0.73.7 + +## 0.41.6 + +### Patch Changes + +- Updated dependencies [[`3154ce4`](https://github.com/Effect-TS/effect/commit/3154ce4692fa18b804982158d3c4c8a8a5fae386)]: + - effect@3.13.6 + - @effect/platform@0.77.6 + - @effect/platform-node@0.73.6 + +## 0.41.5 + +### Patch Changes + +- Updated dependencies [[`367bb35`](https://github.com/Effect-TS/effect/commit/367bb35f4c2a254e1fb211d96db2474a7aed9020), [`6cf11c3`](https://github.com/Effect-TS/effect/commit/6cf11c3a75773ceec2877c85ddc760f381f0866d), [`a0acec8`](https://github.com/Effect-TS/effect/commit/a0acec851f72e19466363d24b9cc218acd00006a)]: + - effect@3.13.5 + - @effect/platform@0.77.5 + - @effect/platform-node@0.73.5 + +## 0.41.4 + +### Patch Changes + +- Updated dependencies [[`e0746f9`](https://github.com/Effect-TS/effect/commit/e0746f9aa398b69c6542e375910683bf17f49f46), [`17d9e89`](https://github.com/Effect-TS/effect/commit/17d9e89f9851663bdbb6c1e685601d97806114a4)]: + - @effect/platform@0.77.4 + - effect@3.13.4 + - @effect/platform-node@0.73.4 + +## 0.41.3 + +### Patch Changes + +- Updated dependencies [[`cc5588d`](https://github.com/Effect-TS/effect/commit/cc5588df07f9103513547cb429ce041b9436a8bd), [`623c8cd`](https://github.com/Effect-TS/effect/commit/623c8cd053ed6ee3d353aaa8778d484670fca2bb), [`00b4eb1`](https://github.com/Effect-TS/effect/commit/00b4eb1ece12a16e222e6220965bb4024d6752ac), [`f2aee98`](https://github.com/Effect-TS/effect/commit/f2aee989b0a600900ce83e7f460d02908620c80f), [`fb798eb`](https://github.com/Effect-TS/effect/commit/fb798eb9061f1191badc017d1aa649360254da20), [`2251b15`](https://github.com/Effect-TS/effect/commit/2251b1528810bb695b37ce388b653cec0c5bf80c), [`2e15c1e`](https://github.com/Effect-TS/effect/commit/2e15c1e33648add0b29fe274fbcb7294b7515085), [`a4979db`](https://github.com/Effect-TS/effect/commit/a4979db021aef16e731be64df196b72088fc4376), [`b74255a`](https://github.com/Effect-TS/effect/commit/b74255a304ad49d60bedb1a260fd697f370af27a), [`d7f6a5c`](https://github.com/Effect-TS/effect/commit/d7f6a5c7d26c1963dcd864ca62360d20d08c7b49), [`9dd8979`](https://github.com/Effect-TS/effect/commit/9dd8979e940915b1cc1b1f264f3d019c77a65a02), [`477b488`](https://github.com/Effect-TS/effect/commit/477b488284f47c5469d7fba3e4065fb7e3b6556e), [`10932cb`](https://github.com/Effect-TS/effect/commit/10932cbf58fc721ada631cebec42f773ce96d3cc), [`9f6c784`](https://github.com/Effect-TS/effect/commit/9f6c78468b3b5e9ebfc38ffdfb70702901ee977b), [`2c639ec`](https://github.com/Effect-TS/effect/commit/2c639ecee332de4266e36022c989c35ae4e02105), [`886aaa8`](https://github.com/Effect-TS/effect/commit/886aaa81e06dfd3cd9391e8ea987d8cd5ada1124)]: + - effect@3.13.3 + - @effect/platform@0.77.3 + - @effect/platform-node@0.73.3 + +## 0.41.2 + +### Patch Changes + +- Updated dependencies [[`31be72a`](https://github.com/Effect-TS/effect/commit/31be72ada118cb84a942e67b1663263f8db74a9f), [`3e7ce97`](https://github.com/Effect-TS/effect/commit/3e7ce97f8a41756a039cf635d0b3d9a75d781097), [`31be72a`](https://github.com/Effect-TS/effect/commit/31be72ada118cb84a942e67b1663263f8db74a9f)]: + - effect@3.13.2 + - @effect/platform@0.77.2 + - @effect/platform-node@0.73.2 + +## 0.41.1 + +### Patch Changes + +- Updated dependencies [[`b56a211`](https://github.com/Effect-TS/effect/commit/b56a2110569fd0ec0b57ac137743e926d49f51cc)]: + - effect@3.13.1 + - @effect/platform@0.77.1 + - @effect/platform-node@0.73.1 + +## 0.41.0 + +### Patch Changes + +- Updated dependencies [[`8baef83`](https://github.com/Effect-TS/effect/commit/8baef83e7ff0b7bc0738b680e1ef013065386cff), [`655bfe2`](https://github.com/Effect-TS/effect/commit/655bfe29e44cc3f0fb9b4e53038f50b891c188df), [`d90cbc2`](https://github.com/Effect-TS/effect/commit/d90cbc274e2742d18671fe65aa4764c057eb6cba), [`75632bd`](https://github.com/Effect-TS/effect/commit/75632bd44b8025101d652ccbaeef898c7086c91c), [`c874a2e`](https://github.com/Effect-TS/effect/commit/c874a2e4b17e9d71904ca8375bb77b020975cb1d), [`bf865e5`](https://github.com/Effect-TS/effect/commit/bf865e5833f77fd8f6c06944ca9d507b54488301), [`f98b2b7`](https://github.com/Effect-TS/effect/commit/f98b2b7592cf20f9d85313e7f1e964cb65878138), [`de8ce92`](https://github.com/Effect-TS/effect/commit/de8ce924923eaa4e1b761a97eb45ec967389f3d5), [`cf8b2dd`](https://github.com/Effect-TS/effect/commit/cf8b2dd112f8e092ed99d78fd728db0f91c29050), [`db426a5`](https://github.com/Effect-TS/effect/commit/db426a5fb41ab84d18e3c8753a7329b4de544245), [`6862444`](https://github.com/Effect-TS/effect/commit/6862444094906ad4f2cb077ff3b9cc0b73880c8c), [`5fc8a90`](https://github.com/Effect-TS/effect/commit/5fc8a90ba46a5fd9f3b643f0b5aeadc69d717339), [`546a492`](https://github.com/Effect-TS/effect/commit/546a492e60eb2b8b048a489a474b934ea0877005), [`65c4796`](https://github.com/Effect-TS/effect/commit/65c47966ce39055f02cf5c808daabb3ea6442b0b), [`9760fdc`](https://github.com/Effect-TS/effect/commit/9760fdc37bdaef9da8b150e46b86ddfbe2ad9221), [`5b471e7`](https://github.com/Effect-TS/effect/commit/5b471e7d4317e8ee5d72bbbd3e0c9775160949ab), [`4f810cc`](https://github.com/Effect-TS/effect/commit/4f810cc2770e9f1f266851d2cb6257112c12af49)]: + - effect@3.13.0 + - @effect/platform@0.77.0 + - @effect/platform-node@0.73.0 + +## 0.40.1 + +### Patch Changes + +- Updated dependencies [[`4018eae`](https://github.com/Effect-TS/effect/commit/4018eaed2733241676ddb8c52416f463a8c32e35), [`543d36d`](https://github.com/Effect-TS/effect/commit/543d36d1a11452560b01ab966a82529ad5fee8c9), [`c407726`](https://github.com/Effect-TS/effect/commit/c407726f79df4a567a9631cddd8effaa16b3535d), [`f70a65a`](https://github.com/Effect-TS/effect/commit/f70a65ac80c6635d80b12beaf4d32a9cc59fa143), [`ba409f6`](https://github.com/Effect-TS/effect/commit/ba409f69c41aeaa29e475c0630735726eaf4dbac), [`3d2e356`](https://github.com/Effect-TS/effect/commit/3d2e3565e8a43d1bdb5daee8db3b90f56d71d859)]: + - effect@3.12.12 + - @effect/platform@0.76.1 + - @effect/platform-node@0.72.1 + +## 0.40.0 + +### Patch Changes + +- Updated dependencies [[`b6a032f`](https://github.com/Effect-TS/effect/commit/b6a032f07bffa020a848c813881879395134fa20), [`42ddd5f`](https://github.com/Effect-TS/effect/commit/42ddd5f144ce9f9d94a036679ebbd626446d37f5), [`2fe447c`](https://github.com/Effect-TS/effect/commit/2fe447c6354d334f9c591b8a8481818f5f0e797e), [`2473ad5`](https://github.com/Effect-TS/effect/commit/2473ad5cf23582e3a41338091fa526ffe611288d)]: + - effect@3.12.11 + - @effect/platform-node@0.72.0 + - @effect/platform@0.76.0 + +## 0.39.4 + +### Patch Changes + +- Updated dependencies [[`e30f132`](https://github.com/Effect-TS/effect/commit/e30f132c336c9d0760bad39f82a55c7ce5159eb7), [`33fa667`](https://github.com/Effect-TS/effect/commit/33fa667c2623be1026e1ccee91bd44f73b09020a), [`87f5f28`](https://github.com/Effect-TS/effect/commit/87f5f2842e4196cb88d13f10f443ff0567e82832), [`7d57ecd`](https://github.com/Effect-TS/effect/commit/7d57ecdaf5da2345ebbf9c22df50317578bde0f5), [`4dbd170`](https://github.com/Effect-TS/effect/commit/4dbd170538e8fb7a36aa7c469c6f93b6c7000091)]: + - effect@3.12.10 + - @effect/platform@0.75.4 + - @effect/platform-node@0.71.4 + +## 0.39.3 + +### Patch Changes + +- Updated dependencies [[`1b4a4e9`](https://github.com/Effect-TS/effect/commit/1b4a4e904ef5227ec7d9114d4e417eca19eed940)]: + - effect@3.12.9 + - @effect/platform@0.75.3 + - @effect/platform-node@0.71.3 + +## 0.39.2 + +### Patch Changes + +- Updated dependencies [[`59b3cfb`](https://github.com/Effect-TS/effect/commit/59b3cfbbd5713dd9475998e95fad5534c0b21466), [`766113c`](https://github.com/Effect-TS/effect/commit/766113c0ea3512cdb887650ead8ba314236e22ee), [`bb05fb8`](https://github.com/Effect-TS/effect/commit/bb05fb83457355b1ca567228a9e041edfb6fd85d), [`712277f`](https://github.com/Effect-TS/effect/commit/712277f949052a24b46e4aa234063a6abf395c90), [`f269122`](https://github.com/Effect-TS/effect/commit/f269122508693b111142994dd48698ddc75f3d69), [`8f6006a`](https://github.com/Effect-TS/effect/commit/8f6006a610fb6d6c7b8d14209a7323338a8964ff), [`62934fc`](https://github.com/Effect-TS/effect/commit/62934fc61ae870b0e86ef0711c2852743adee9db), [`c45b559`](https://github.com/Effect-TS/effect/commit/c45b5592b5fd1189a5c932cfe05bd7d5f6d68508), [`430c846`](https://github.com/Effect-TS/effect/commit/430c846cbac05b187e3d24ac8dfee0cf22506f7c), [`7b03057`](https://github.com/Effect-TS/effect/commit/7b03057507d2dab5e6793beb9c578dedaaeb15fe), [`a9c94c8`](https://github.com/Effect-TS/effect/commit/a9c94c807755610831211a686d2fad849ab38eb4), [`107e6f0`](https://github.com/Effect-TS/effect/commit/107e6f0557a1e2d3b0dce25d62fa1e2601521752), [`c9175ae`](https://github.com/Effect-TS/effect/commit/c9175aef41cb1e3b689d0ac0a4f53d8107376b58), [`65c11b9`](https://github.com/Effect-TS/effect/commit/65c11b9266ec9447c31c26fe3ed35c73bd3b81fd), [`e386d2f`](https://github.com/Effect-TS/effect/commit/e386d2f1b3ab3ac2c14ee76de11f5963d32a3df4), [`9172efb`](https://github.com/Effect-TS/effect/commit/9172efba98bc6a82353e6ec2af61ac08f038ba64)]: + - @effect/platform@0.75.2 + - effect@3.12.8 + - @effect/platform-node@0.71.2 + +## 0.39.1 + +### Patch Changes + +- Updated dependencies [[`8dff1d1`](https://github.com/Effect-TS/effect/commit/8dff1d1bff76cdba643cad7f0bf864300f08bc61)]: + - effect@3.12.7 + - @effect/platform@0.75.1 + - @effect/platform-node@0.71.1 + +## 0.39.0 + +### Minor Changes + +- [#4306](https://github.com/Effect-TS/effect/pull/4306) [`5e43ce5`](https://github.com/Effect-TS/effect/commit/5e43ce50bae116865906112e7f88d390739d778b) Thanks @tim-smart! - eliminate Scope by default in some layer apis + +### Patch Changes + +- Updated dependencies [[`5e43ce5`](https://github.com/Effect-TS/effect/commit/5e43ce50bae116865906112e7f88d390739d778b), [`289c13b`](https://github.com/Effect-TS/effect/commit/289c13b38e8e35b214d46d385d05dead176c87cd), [`76eb7d0`](https://github.com/Effect-TS/effect/commit/76eb7d0fbce3c009c8f77e84c178cb15bbed9709), [`8b4e75d`](https://github.com/Effect-TS/effect/commit/8b4e75d35daea807c447ca760948a717aa66bb52), [`fc5e0f0`](https://github.com/Effect-TS/effect/commit/fc5e0f0d357a0051cfa01c1ede83ffdd3cb41ab1), [`004fd2b`](https://github.com/Effect-TS/effect/commit/004fd2bbd1459e64fb1b57f02eeb791ca5ea1ea5), [`b2a31be`](https://github.com/Effect-TS/effect/commit/b2a31be85c35d891351ce4f9a2cc93ece0c257f6), [`5514d05`](https://github.com/Effect-TS/effect/commit/5514d05b5cd586ff5868b8bd41c959e95e6c33cd), [`bf5f0ae`](https://github.com/Effect-TS/effect/commit/bf5f0ae9daa0170471678e22585e8ec14ce667bb), [`3b19bcf`](https://github.com/Effect-TS/effect/commit/3b19bcfd3aaadb6c9253428622df524537c8e626), [`b064b3b`](https://github.com/Effect-TS/effect/commit/b064b3b293615fd268cc5a5647d0981eb67750b8), [`eb264ed`](https://github.com/Effect-TS/effect/commit/eb264ed8a6e8c92a9dc7006f766c6ca2e5d29e03), [`289c13b`](https://github.com/Effect-TS/effect/commit/289c13b38e8e35b214d46d385d05dead176c87cd), [`f474678`](https://github.com/Effect-TS/effect/commit/f474678bf10b8f1c80e3dc096ddc7ecf20b2b23e), [`ee187d0`](https://github.com/Effect-TS/effect/commit/ee187d098007a402844c94d04f0cd8f07695377a)]: + - @effect/platform@0.75.0 + - effect@3.12.6 + - @effect/platform-node@0.71.0 + +## 0.38.0 + +### Minor Changes + +- [#3978](https://github.com/Effect-TS/effect/pull/3978) [`bd0d489`](https://github.com/Effect-TS/effect/commit/bd0d4892b098bc4c589444af9f50259c2c02ec0f) Thanks @tim-smart! - improve defaults for VariantSchema.Overrideable + +### Patch Changes + +- [#3978](https://github.com/Effect-TS/effect/pull/3978) [`bd0d489`](https://github.com/Effect-TS/effect/commit/bd0d4892b098bc4c589444af9f50259c2c02ec0f) Thanks @tim-smart! - add experimental EventLog modules + +- Updated dependencies [[`a8b0ddb`](https://github.com/Effect-TS/effect/commit/a8b0ddb84710054799fc8f57485b95d00093ada1), [`8653072`](https://github.com/Effect-TS/effect/commit/86530720d7a03e118d2c5a8bf5a997cee7e7f3d6), [`507d546`](https://github.com/Effect-TS/effect/commit/507d546bd49db31000425fb5da88c434e4291bea), [`a8b0ddb`](https://github.com/Effect-TS/effect/commit/a8b0ddb84710054799fc8f57485b95d00093ada1), [`8db239b`](https://github.com/Effect-TS/effect/commit/8db239b9c869a3707f6566b9d9dbdf53c4df03fc), [`84a0911`](https://github.com/Effect-TS/effect/commit/84a091181634c3a022c94234cec7764a3aeef1be), [`84a0911`](https://github.com/Effect-TS/effect/commit/84a091181634c3a022c94234cec7764a3aeef1be), [`3179a9f`](https://github.com/Effect-TS/effect/commit/3179a9f65d23369a6a9a1f80f7750566dd28df22), [`6cb9b76`](https://github.com/Effect-TS/effect/commit/6cb9b766396d0b2ed995cf26957359713efd202e), [`1fcbe55`](https://github.com/Effect-TS/effect/commit/1fcbe55345042d8468f6a98c84081bd00b6bcf5a), [`d9a63d9`](https://github.com/Effect-TS/effect/commit/d9a63d9d385653865954cac895065360d54cc56b), [`6cb9b76`](https://github.com/Effect-TS/effect/commit/6cb9b766396d0b2ed995cf26957359713efd202e)]: + - effect@3.12.5 + - @effect/platform@0.74.0 + - @effect/platform-node@0.70.0 + +## 0.37.1 + +### Patch Changes + +- Updated dependencies [[`5b50ea4`](https://github.com/Effect-TS/effect/commit/5b50ea4a10cf9acd51f9624b2474d9d5ded74019), [`c170a68`](https://github.com/Effect-TS/effect/commit/c170a68b6266100774461fcd6c0e0fabb60112f2), [`a66c2eb`](https://github.com/Effect-TS/effect/commit/a66c2eb473245092cd41f04c2eb2b7b02cf53718), [`c9e5e1b`](https://github.com/Effect-TS/effect/commit/c9e5e1be17c0c84d3d4e2abc3c60215cdb56bbbe), [`7b3d58d`](https://github.com/Effect-TS/effect/commit/7b3d58d7aec2152ec282460871d3e9de45ed254d)]: + - effect@3.12.4 + - @effect/platform@0.73.1 + - @effect/platform-node@0.69.1 + +## 0.37.0 + +### Patch Changes + +- Updated dependencies [[`d7dac48`](https://github.com/Effect-TS/effect/commit/d7dac48a477cdfeec509dbe9f33fce6a1b02b63d), [`c110032`](https://github.com/Effect-TS/effect/commit/c110032322450a8824ba38ae24335a538cd2ce9a), [`1d7fd2b`](https://github.com/Effect-TS/effect/commit/1d7fd2b7ee8eeecc912d27adf76ed897db236dc5), [`1d7fd2b`](https://github.com/Effect-TS/effect/commit/1d7fd2b7ee8eeecc912d27adf76ed897db236dc5), [`23ac740`](https://github.com/Effect-TS/effect/commit/23ac740c7dd4610b7d265c2071b88b0968419e9a), [`8cd7319`](https://github.com/Effect-TS/effect/commit/8cd7319b6568bfc7a30ca16c104d189e37eac3a0)]: + - effect@3.12.3 + - @effect/platform@0.73.0 + - @effect/platform-node@0.69.0 + +## 0.36.2 + +### Patch Changes + +- Updated dependencies [[`734af82`](https://github.com/Effect-TS/effect/commit/734af82138e78b9c57a8355b1c6b80e80d38b222), [`b63c780`](https://github.com/Effect-TS/effect/commit/b63c78010893101520448ddda7019c487cf7eedd), [`212e784`](https://github.com/Effect-TS/effect/commit/212e78475f527147ec27c090bd13f789f55add7a), [`f852cb0`](https://github.com/Effect-TS/effect/commit/f852cb02040ea2f165e9b449615b8b1366add5d5), [`7276ae2`](https://github.com/Effect-TS/effect/commit/7276ae21062896adbb7508ac5b2dece95316322f), [`212e784`](https://github.com/Effect-TS/effect/commit/212e78475f527147ec27c090bd13f789f55add7a), [`212e784`](https://github.com/Effect-TS/effect/commit/212e78475f527147ec27c090bd13f789f55add7a), [`212e784`](https://github.com/Effect-TS/effect/commit/212e78475f527147ec27c090bd13f789f55add7a), [`c640d77`](https://github.com/Effect-TS/effect/commit/c640d77b33ad417876f4e8ffe8574ee6cbe5607f), [`0def088`](https://github.com/Effect-TS/effect/commit/0def0887cfdb6755729a64dfd52b3b9f46b0576c)]: + - effect@3.12.2 + - @effect/platform@0.72.2 + - @effect/platform-node@0.68.2 + +## 0.36.1 + +### Patch Changes + +- Updated dependencies [[`302b57d`](https://github.com/Effect-TS/effect/commit/302b57d2cbf9b9ccc17450945aeebfb33cfe8d43), [`0988083`](https://github.com/Effect-TS/effect/commit/0988083d4594938590df5a287e5b27d38526dd07), [`8b46be6`](https://github.com/Effect-TS/effect/commit/8b46be6a3b8160362ab5ea9171c5e6932505125c), [`bfe8027`](https://github.com/Effect-TS/effect/commit/bfe802734b450a4b4ee069d1125dd37995db2bff), [`16dd657`](https://github.com/Effect-TS/effect/commit/16dd657033d8afac2ffea567b3c8bb27c9b249b6), [`39db211`](https://github.com/Effect-TS/effect/commit/39db211414e90c8db8fdad7dc8ce5b4661bcfaef)]: + - effect@3.12.1 + - @effect/platform@0.72.1 + - @effect/platform-node@0.68.1 + +## 0.36.0 + +### Patch Changes + +- Updated dependencies [[`abb22a4`](https://github.com/Effect-TS/effect/commit/abb22a429b9c52c31e84856294f175d2064a9b4d), [`f369a89`](https://github.com/Effect-TS/effect/commit/f369a89e98bc682969803b9304adaf4557bb36c2), [`642376c`](https://github.com/Effect-TS/effect/commit/642376c63fd7d78754db991631a4d50a5dc79aa3), [`3d2b7a7`](https://github.com/Effect-TS/effect/commit/3d2b7a7e942a7157afae5b1cdbc6f3fef116428e), [`ef64c6f`](https://github.com/Effect-TS/effect/commit/ef64c6fec0d47da573c04230dde9ea729366d871), [`73f9c6f`](https://github.com/Effect-TS/effect/commit/73f9c6f2ff091512cf904cc54ab59965b86e87c8), [`17cb451`](https://github.com/Effect-TS/effect/commit/17cb4514590e8a86263f7aed009f24da8a237342), [`d801820`](https://github.com/Effect-TS/effect/commit/d80182060c2ee945d7e0e4728812abf9465a0d6a), [`e1eeb2d`](https://github.com/Effect-TS/effect/commit/e1eeb2d7064b3870041dab142f3057970699bbf1), [`c11f3a6`](https://github.com/Effect-TS/effect/commit/c11f3a60a05c3b5fc8e7ce90136728154dc505b0), [`618f7e0`](https://github.com/Effect-TS/effect/commit/618f7e092a1011e5090dca1e69b5e9285689654b), [`c0ba834`](https://github.com/Effect-TS/effect/commit/c0ba834d1995cf5a8b250e4780fd43f3e3881151), [`e1eeb2d`](https://github.com/Effect-TS/effect/commit/e1eeb2d7064b3870041dab142f3057970699bbf1)]: + - effect@3.12.0 + - @effect/platform@0.72.0 + - @effect/platform-node@0.68.0 + +## 0.35.3 + +### Patch Changes + +- Updated dependencies [[`39457d4`](https://github.com/Effect-TS/effect/commit/39457d4897d9bc7df8af5c05d352866bbeae82eb), [`a475cc2`](https://github.com/Effect-TS/effect/commit/a475cc25fd7c9f26b27a8e98f8fbe43cc9e6ee3e), [`199214e`](https://github.com/Effect-TS/effect/commit/199214e21c616d8a0ccd7ed5f92e944e6c580193), [`b3c160d`](https://github.com/Effect-TS/effect/commit/b3c160d7a1fdfc2d3fb2440530f1ab80efc65133)]: + - effect@3.11.10 + - @effect/platform@0.71.7 + - @effect/platform-node@0.67.3 + +## 0.35.2 + +### Patch Changes + +- Updated dependencies [[`1c08a0b`](https://github.com/Effect-TS/effect/commit/1c08a0b8505badcffb4d9cade5a746ea90c9557e), [`1ce703b`](https://github.com/Effect-TS/effect/commit/1ce703b041bbd7560c5c437c9b9be48f027937fd), [`1ce703b`](https://github.com/Effect-TS/effect/commit/1ce703b041bbd7560c5c437c9b9be48f027937fd)]: + - effect@3.11.9 + - @effect/platform@0.71.6 + - @effect/platform-node@0.67.2 + +## 0.35.1 + +### Patch Changes + +- Updated dependencies [[`05d71f8`](https://github.com/Effect-TS/effect/commit/05d71f85622305705d8316817694a09762e60865), [`e66b920`](https://github.com/Effect-TS/effect/commit/e66b9205f25ab425d30640886eb3fb2c4715bc26)]: + - @effect/platform@0.71.5 + - @effect/platform-node@0.67.1 + +## 0.35.0 + +### Patch Changes + +- Updated dependencies [[`909181a`](https://github.com/Effect-TS/effect/commit/909181a9ce9052a80432ccf52187e0723004bf7f), [`909181a`](https://github.com/Effect-TS/effect/commit/909181a9ce9052a80432ccf52187e0723004bf7f), [`011e2b6`](https://github.com/Effect-TS/effect/commit/011e2b604e2e61e7b788243b0aab105fd301ec7f), [`1a6b52d`](https://github.com/Effect-TS/effect/commit/1a6b52dcf020d36e38a7bc90b648152cf5a8ccba)]: + - @effect/platform@0.71.4 + - @effect/platform-node@0.67.0 + - effect@3.11.8 + +## 0.34.3 + +### Patch Changes + +- Updated dependencies [[`6984508`](https://github.com/Effect-TS/effect/commit/6984508c87f1bd91213b44c19b25ab5e2dcc1ce0), [`883639c`](https://github.com/Effect-TS/effect/commit/883639cc8ce47757f1cd39439391a8028c0812fe)]: + - @effect/platform@0.71.3 + - @effect/platform-node@0.66.3 + +## 0.34.2 + +### Patch Changes + +- Updated dependencies [[`2408616`](https://github.com/Effect-TS/effect/commit/24086163b60b09cc6d0885bd565ef080dcbe866b), [`cec0b4d`](https://github.com/Effect-TS/effect/commit/cec0b4d152ef660be2ccdb0927255f2471436e6e), [`cec0b4d`](https://github.com/Effect-TS/effect/commit/cec0b4d152ef660be2ccdb0927255f2471436e6e), [`8d978c5`](https://github.com/Effect-TS/effect/commit/8d978c53f6fcc98d9d645ecba3e4b55d4297dd36), [`cec0b4d`](https://github.com/Effect-TS/effect/commit/cec0b4d152ef660be2ccdb0927255f2471436e6e), [`cec0b4d`](https://github.com/Effect-TS/effect/commit/cec0b4d152ef660be2ccdb0927255f2471436e6e)]: + - effect@3.11.7 + - @effect/platform@0.71.2 + - @effect/platform-node@0.66.2 + +## 0.34.1 + +### Patch Changes + +- Updated dependencies [[`1d3df5b`](https://github.com/Effect-TS/effect/commit/1d3df5bc4324e88a392c348db35fd9d029c7b25e)]: + - @effect/platform@0.71.1 + - @effect/platform-node@0.66.1 + +## 0.34.0 + +### Patch Changes + +- Updated dependencies [[`662d1ce`](https://github.com/Effect-TS/effect/commit/662d1ce6fb7da384a95888d5b2bb5605bdf3208d), [`c99a0f3`](https://github.com/Effect-TS/effect/commit/c99a0f376d049d3793ed33e146d9873b8a5e5b78), [`11fc401`](https://github.com/Effect-TS/effect/commit/11fc401f436f99bf4be95f56d50b0e4bdfe5edea), [`c99a0f3`](https://github.com/Effect-TS/effect/commit/c99a0f376d049d3793ed33e146d9873b8a5e5b78), [`31c62d8`](https://github.com/Effect-TS/effect/commit/31c62d83cbdcf9850a8b5331faa239601c60f78a)]: + - effect@3.11.6 + - @effect/platform@0.71.0 + - @effect/platform-node@0.66.0 + +## 0.33.7 + +### Patch Changes + +- [#4114](https://github.com/Effect-TS/effect/pull/4114) [`ef70ffc`](https://github.com/Effect-TS/effect/commit/ef70ffc417ec035ede40c62b7316e447cc7c1932) Thanks @tim-smart! - ensure Reactivity events aren't missed + +- Updated dependencies [[`9f5a6f7`](https://github.com/Effect-TS/effect/commit/9f5a6f701bf7ba31adccd1f1bcfa8ab5614c9be8), [`22905cf`](https://github.com/Effect-TS/effect/commit/22905cf5addfb1ff3d2a6135c52036be958ae911), [`9f5a6f7`](https://github.com/Effect-TS/effect/commit/9f5a6f701bf7ba31adccd1f1bcfa8ab5614c9be8), [`1e59e4f`](https://github.com/Effect-TS/effect/commit/1e59e4fd778da18296812a2a32f36ca8ae50f60d), [`9f5a6f7`](https://github.com/Effect-TS/effect/commit/9f5a6f701bf7ba31adccd1f1bcfa8ab5614c9be8), [`8d914e5`](https://github.com/Effect-TS/effect/commit/8d914e504e7a22d0ea628e8af265ee450ff9530f), [`03bb00f`](https://github.com/Effect-TS/effect/commit/03bb00faa74f9e168a54a8cc0828a664fbb1ab05), [`9f5a6f7`](https://github.com/Effect-TS/effect/commit/9f5a6f701bf7ba31adccd1f1bcfa8ab5614c9be8), [`14e1149`](https://github.com/Effect-TS/effect/commit/14e1149f1af5a022f06eb8c2e4ba9fec17fe7426), [`9f5a6f7`](https://github.com/Effect-TS/effect/commit/9f5a6f701bf7ba31adccd1f1bcfa8ab5614c9be8), [`9f5a6f7`](https://github.com/Effect-TS/effect/commit/9f5a6f701bf7ba31adccd1f1bcfa8ab5614c9be8)]: + - effect@3.11.5 + - @effect/platform@0.70.7 + - @effect/platform-node@0.65.7 + +## 0.33.6 + +### Patch Changes + +- Updated dependencies [[`9a5b8e3`](https://github.com/Effect-TS/effect/commit/9a5b8e36d184bd4967a88752cb6e755e1be263af)]: + - @effect/platform@0.70.6 + - @effect/platform-node@0.65.6 + +## 0.33.5 + +### Patch Changes + +- [#4087](https://github.com/Effect-TS/effect/pull/4087) [`518b258`](https://github.com/Effect-TS/effect/commit/518b258a8a67ecd332a9252c35cc060f8368dee2) Thanks @tim-smart! - remove Socket write indirection + +- Updated dependencies [[`415f4c9`](https://github.com/Effect-TS/effect/commit/415f4c98321868531727a83cbaad70164f5e4c40), [`518b258`](https://github.com/Effect-TS/effect/commit/518b258a8a67ecd332a9252c35cc060f8368dee2), [`6e323a3`](https://github.com/Effect-TS/effect/commit/6e323a36faaee46b328c8e3cf60a76b3aff9907f), [`518b258`](https://github.com/Effect-TS/effect/commit/518b258a8a67ecd332a9252c35cc060f8368dee2), [`6e323a3`](https://github.com/Effect-TS/effect/commit/6e323a36faaee46b328c8e3cf60a76b3aff9907f)]: + - @effect/platform@0.70.5 + - effect@3.11.4 + - @effect/platform-node@0.65.5 + +## 0.33.4 + +### Patch Changes + +- Updated dependencies [[`90906f7`](https://github.com/Effect-TS/effect/commit/90906f7f154b12c7182e8f39e3c55ef3937db857), [`3862cd3`](https://github.com/Effect-TS/effect/commit/3862cd3c7f6a542ed65fb81255b3bd696ce2f567), [`3862cd3`](https://github.com/Effect-TS/effect/commit/3862cd3c7f6a542ed65fb81255b3bd696ce2f567), [`343b6aa`](https://github.com/Effect-TS/effect/commit/343b6aa6ac4a74276bfc7c63ccbf4a1d72bc1bed), [`afba339`](https://github.com/Effect-TS/effect/commit/afba339adc11dad56b5a3b7ca94487e58f34d613)]: + - effect@3.11.3 + - @effect/platform@0.70.4 + - @effect/platform-node@0.65.4 + +## 0.33.3 + +### Patch Changes + +- Updated dependencies [[`7044730`](https://github.com/Effect-TS/effect/commit/70447306be1aeeb7d87c230b2a96ec87b993ede9)]: + - @effect/platform@0.70.3 + - @effect/platform-node@0.65.3 + +## 0.33.2 + +### Patch Changes + +- Updated dependencies [[`01cee56`](https://github.com/Effect-TS/effect/commit/01cee560b58d94b24cc20e98083251b73e658b41), [`c2249ea`](https://github.com/Effect-TS/effect/commit/c2249ea13fd98ab7d9aa628787931356d8ec2860), [`1358aa5`](https://github.com/Effect-TS/effect/commit/1358aa5326eaa85ef13ee8d1fed0b4a4288ed3eb), [`1de3fe7`](https://github.com/Effect-TS/effect/commit/1de3fe7d1cbafd6391eaa38c2300b99e332cc2aa)]: + - effect@3.11.2 + - @effect/platform-node@0.65.2 + - @effect/platform@0.70.2 + +## 0.33.1 + +### Patch Changes + +- Updated dependencies [[`dd8a2d8`](https://github.com/Effect-TS/effect/commit/dd8a2d8e80d33b16719fc69361eaedf0b59d4620), [`a71bfef`](https://github.com/Effect-TS/effect/commit/a71bfef46f5061bb2502a61a333638a987b62273)]: + - effect@3.11.1 + - @effect/platform@0.70.1 + - @effect/platform-node@0.65.1 + +## 0.33.0 + +### Patch Changes + +- Updated dependencies [[`147434b`](https://github.com/Effect-TS/effect/commit/147434b03d5e1fd692dd9f126e5ab0910f3b76d3), [`6e69493`](https://github.com/Effect-TS/effect/commit/6e694930048bbaf98110f35f41566aeb9752d471), [`147434b`](https://github.com/Effect-TS/effect/commit/147434b03d5e1fd692dd9f126e5ab0910f3b76d3), [`5eff3f6`](https://github.com/Effect-TS/effect/commit/5eff3f6fa3aae7e86948a62cbfd63b8d6c3bdf92), [`d9fe79b`](https://github.com/Effect-TS/effect/commit/d9fe79bb5a3fe105d8e7a3bc2922a8ad936a5d10), [`251d189`](https://github.com/Effect-TS/effect/commit/251d189420bbba71990574e91098c499065f9a9b), [`5a259f3`](https://github.com/Effect-TS/effect/commit/5a259f3711b4369f55d885b568bdb21136155261), [`b4ce4ea`](https://github.com/Effect-TS/effect/commit/b4ce4ea7fd514a7e572f2dcd879c98f334981b0e), [`15fcc5a`](https://github.com/Effect-TS/effect/commit/15fcc5a0ea4bbf40ab48fa6a04fdda74f76f4c07), [`9bc9a47`](https://github.com/Effect-TS/effect/commit/9bc9a476800dc645903c888a68bb1d3baa3383c6), [`aadb8a4`](https://github.com/Effect-TS/effect/commit/aadb8a48d2cba197c06ec9996505510e48e4e5cb), [`1e2747c`](https://github.com/Effect-TS/effect/commit/1e2747c63a4820d1459cbbc88c71212983bd68bd), [`9264162`](https://github.com/Effect-TS/effect/commit/9264162a82783a651776fb7b87604564a63e7070), [`e0b9b09`](https://github.com/Effect-TS/effect/commit/e0b9b09e70c386b2da17d1f0a15b0511861c89e8), [`c36f3b9`](https://github.com/Effect-TS/effect/commit/c36f3b95df5ce9d71b66f22f26ce12eda8d3e848), [`672bde5`](https://github.com/Effect-TS/effect/commit/672bde5bec51c7d6f9862828e6a654cb2cb6f93d), [`aadb8a4`](https://github.com/Effect-TS/effect/commit/aadb8a48d2cba197c06ec9996505510e48e4e5cb)]: + - effect@3.11.0 + - @effect/platform-node@0.65.0 + - @effect/platform@0.70.0 + +## 0.32.17 + +### Patch Changes + +- Updated dependencies [[`3069614`](https://github.com/Effect-TS/effect/commit/30696149271129fc618f6f2ccd1d8f2f6c0f9cd7), [`09a5e52`](https://github.com/Effect-TS/effect/commit/09a5e522fd9b221f05d85b1d1c8a740d4973c302)]: + - effect@3.10.20 + - @effect/platform@0.69.32 + - @effect/platform-node@0.64.34 + +## 0.32.16 + +### Patch Changes + +- Updated dependencies [[`e6d4a37`](https://github.com/Effect-TS/effect/commit/e6d4a37c1d7e657b5ea44063a1cf586808228fe5)]: + - @effect/platform-node@0.64.33 + - @effect/platform@0.69.31 + +## 0.32.15 + +### Patch Changes + +- [#4029](https://github.com/Effect-TS/effect/pull/4029) [`5e17400`](https://github.com/Effect-TS/effect/commit/5e1740017cb97026e7f583c5fe71847c606df388) Thanks @gcanti! - Avoid "Cannot access 'ParentSpan' before initialization" error during module initialization + +## 0.32.14 + +### Patch Changes + +- Updated dependencies [[`270f199`](https://github.com/Effect-TS/effect/commit/270f199b31810fd643e4c22818698adcbdb5d396)]: + - @effect/platform-node@0.64.32 + - @effect/platform@0.69.30 + +## 0.32.13 + +### Patch Changes + +- [#4021](https://github.com/Effect-TS/effect/pull/4021) [`e9dfea3`](https://github.com/Effect-TS/effect/commit/e9dfea3f394444ebd8929e5cfe05ce740cf84d6e) Thanks @tim-smart! - add .reactive method to SqlClient interface + +- Updated dependencies [[`24cc35e`](https://github.com/Effect-TS/effect/commit/24cc35e26d6ed4a076470bc687ffd99cc50991b3)]: + - @effect/platform-node@0.64.31 + - @effect/platform@0.69.29 + +## 0.32.12 + +### Patch Changes + +- [#4007](https://github.com/Effect-TS/effect/pull/4007) [`944025b`](https://github.com/Effect-TS/effect/commit/944025bc5ce139f4a85846aa689bf30ec06a8ec1) Thanks @gcanti! - Wrap JSDoc @example tags with a TypeScript fence, closes #4002 + +- Updated dependencies [[`edd72be`](https://github.com/Effect-TS/effect/commit/edd72be57b904d60c9cbffc2537901821a9da537), [`a3e2771`](https://github.com/Effect-TS/effect/commit/a3e277170a1f7cf61fd629acb60304c7e81d9498), [`944025b`](https://github.com/Effect-TS/effect/commit/944025bc5ce139f4a85846aa689bf30ec06a8ec1), [`54addee`](https://github.com/Effect-TS/effect/commit/54addee438a644bf010646c52042c7b89c5fc0a7), [`a9e00e4`](https://github.com/Effect-TS/effect/commit/a9e00e43f0b5dd22c1f9d5b78be6383daea09c20)]: + - @effect/platform@0.69.28 + - @effect/platform-node@0.64.30 + - effect@3.10.19 + +## 0.32.11 + +### Patch Changes + +- Updated dependencies [[`af409cf`](https://github.com/Effect-TS/effect/commit/af409cf1d2ff973be11cc079ea373eaeedca25de), [`beaccae`](https://github.com/Effect-TS/effect/commit/beaccae2d15931e9fe475fb50a0b3638243fe3f7)]: + - effect@3.10.18 + - @effect/platform@0.69.27 + - @effect/platform-node@0.64.29 + +## 0.32.10 + +### Patch Changes + +- Updated dependencies [[`c963886`](https://github.com/Effect-TS/effect/commit/c963886d5817986fcbd6bfa4ddf50aca8b6c8184), [`42c4ce6`](https://github.com/Effect-TS/effect/commit/42c4ce6f8d8c7d847e97757650a8ad9419a829d7)]: + - @effect/platform@0.69.26 + - effect@3.10.17 + - @effect/platform-node@0.64.28 + +## 0.32.9 + +### Patch Changes + +- Updated dependencies [[`320557a`](https://github.com/Effect-TS/effect/commit/320557ab18d13c5e22fc7dc0d2a157eae461012f), [`4dca30c`](https://github.com/Effect-TS/effect/commit/4dca30cfcdafe4542e236489f71d6f171a5b4e38), [`1d99867`](https://github.com/Effect-TS/effect/commit/1d998671be3cd11043f232822e91dd8c98fccfa9), [`6dae414`](https://github.com/Effect-TS/effect/commit/6dae4147991a97ec14a99289bd25fadae7541e8d), [`6b0d737`](https://github.com/Effect-TS/effect/commit/6b0d737078bf63b97891e6bc47affc04b28f9cf7), [`d8356aa`](https://github.com/Effect-TS/effect/commit/d8356aad428a0c2290db52380220f81d9ec94232), [`7b93dd6`](https://github.com/Effect-TS/effect/commit/7b93dd622e2ab79c7072d79d0d9611e446202201)]: + - @effect/platform@0.69.25 + - effect@3.10.16 + - @effect/platform-node@0.64.27 + +## 0.32.8 + +### Patch Changes + +- Updated dependencies [[`3cc6514`](https://github.com/Effect-TS/effect/commit/3cc6514d2dd64e010cb760cc29bfce98c349bb10)]: + - @effect/platform@0.69.24 + - @effect/platform-node@0.64.26 + +## 0.32.7 + +### Patch Changes + +- Updated dependencies [[`3aff4d3`](https://github.com/Effect-TS/effect/commit/3aff4d38837c213bb2987973dc4b98febb9f92d2)]: + - @effect/platform@0.69.23 + - @effect/platform-node@0.64.25 + +## 0.32.6 + +### Patch Changes + +- [#3937](https://github.com/Effect-TS/effect/pull/3937) [`4e9e256`](https://github.com/Effect-TS/effect/commit/4e9e256e9f0b4a9e9b202d3bb703b5a4622e75cb) Thanks @tim-smart! - add Reactivity module to experimental + +- Updated dependencies [[`8398b32`](https://github.com/Effect-TS/effect/commit/8398b3208242a88239d4449910b7baf923cfe3b6), [`72e55b7`](https://github.com/Effect-TS/effect/commit/72e55b7c610784fcebdbadc592c876e23e76a986)]: + - effect@3.10.15 + - @effect/platform@0.69.22 + - @effect/platform-node@0.64.24 + +## 0.32.5 + +### Patch Changes + +- Updated dependencies [[`f983946`](https://github.com/Effect-TS/effect/commit/f9839467b4cad6e788297764ef9f9f0b9fd203f9), [`2d8a750`](https://github.com/Effect-TS/effect/commit/2d8a75081eb83a0a81f817fdf6f428369c5064ab)]: + - effect@3.10.14 + - @effect/platform@0.69.21 + - @effect/platform-node@0.64.23 + +## 0.32.4 + +### Patch Changes + +- Updated dependencies [[`995bbdf`](https://github.com/Effect-TS/effect/commit/995bbdffea2e332f203cd5b474cd6a1c77dfa6ae)]: + - effect@3.10.13 + - @effect/platform@0.69.20 + - @effect/platform-node@0.64.22 + +## 0.32.3 + +### Patch Changes + +- Updated dependencies [[`eb8c52d`](https://github.com/Effect-TS/effect/commit/eb8c52d8b4c5e067ebf0a81eb742f5822e6439b5)]: + - @effect/platform-node@0.64.21 + - @effect/platform@0.69.19 + +## 0.32.2 + +### Patch Changes + +- Updated dependencies [[`a0584ec`](https://github.com/Effect-TS/effect/commit/a0584ece92ed784bfb139e9c5a699f02d1e71c2d), [`dd14efe`](https://github.com/Effect-TS/effect/commit/dd14efe0ace255f571273aae876adea96267d7e6), [`dd14efe`](https://github.com/Effect-TS/effect/commit/dd14efe0ace255f571273aae876adea96267d7e6)]: + - @effect/platform@0.69.18 + - @effect/platform-node@0.64.20 + - effect@3.10.12 + +## 0.32.1 + +### Patch Changes + +- Updated dependencies [[`5eef499`](https://github.com/Effect-TS/effect/commit/5eef4998b6ccb7a5404d9e4fef85e57fa35fbb8a), [`8240b1c`](https://github.com/Effect-TS/effect/commit/8240b1c10d45312fc863cb679b1a1e8441af0c1a), [`5eef499`](https://github.com/Effect-TS/effect/commit/5eef4998b6ccb7a5404d9e4fef85e57fa35fbb8a), [`a2bd4df`](https://github.com/Effect-TS/effect/commit/a2bd4dfa3d9a28a7d02ee177baf173c92a4dee7b)]: + - effect@3.10.11 + - @effect/platform@0.69.17 + - @effect/platform-node@0.64.19 + +## 0.32.0 + +### Minor Changes + +- [#3896](https://github.com/Effect-TS/effect/pull/3896) [`12b3275`](https://github.com/Effect-TS/effect/commit/12b32753afbf252d156ff613f9e88662b160a7e5) Thanks @tim-smart! - simplify DevTool/Server module + +### Patch Changes + +- Updated dependencies []: + - @effect/platform-node@0.64.18 + +## 0.31.0 + +### Minor Changes + +- [#3893](https://github.com/Effect-TS/effect/pull/3893) [`cd720ae`](https://github.com/Effect-TS/effect/commit/cd720aedf7f2571edec0843d6a633e84e4832b28) Thanks @tim-smart! - simplify SocketServer api + +- [#3893](https://github.com/Effect-TS/effect/pull/3893) [`cd720ae`](https://github.com/Effect-TS/effect/commit/cd720aedf7f2571edec0843d6a633e84e4832b28) Thanks @tim-smart! - use Mailbox for DevTools implementation + +### Patch Changes + +- Updated dependencies [[`cd720ae`](https://github.com/Effect-TS/effect/commit/cd720aedf7f2571edec0843d6a633e84e4832b28), [`cd720ae`](https://github.com/Effect-TS/effect/commit/cd720aedf7f2571edec0843d6a633e84e4832b28), [`cd720ae`](https://github.com/Effect-TS/effect/commit/cd720aedf7f2571edec0843d6a633e84e4832b28), [`b631f40`](https://github.com/Effect-TS/effect/commit/b631f40abbe649b2a089764585b5c39f6a695ac6), [`7d89650`](https://github.com/Effect-TS/effect/commit/7d8965036cd2ea435c8441ffec3345488baebf85)]: + - effect@3.10.10 + - @effect/platform@0.69.16 + - @effect/platform-node@0.64.17 + +## 0.30.16 + +### Patch Changes + +- Updated dependencies [[`8a30e1d`](https://github.com/Effect-TS/effect/commit/8a30e1dfa3a7103bf5414fc6a7fca3088d8c8c00)]: + - @effect/platform-node@0.64.16 + - @effect/platform@0.69.15 + +## 0.30.15 + +### Patch Changes + +- [#3882](https://github.com/Effect-TS/effect/pull/3882) [`257ab1b`](https://github.com/Effect-TS/effect/commit/257ab1b539fa6e930b7ae2583a188376372200d7) Thanks @tim-smart! - simplify Socket internal code + +- Updated dependencies [[`a123e80`](https://github.com/Effect-TS/effect/commit/a123e80f111a625428a5b5622b7f55ee1073566b), [`bd5fcd3`](https://github.com/Effect-TS/effect/commit/bd5fcd3e6b603b1e505af90d6a00627c8eca6d41), [`0289d3b`](https://github.com/Effect-TS/effect/commit/0289d3b6391031d00329365bab9791b355031fe3), [`7386b71`](https://github.com/Effect-TS/effect/commit/7386b710e5be570e17f468928a6ed19d549a3e12), [`07c493a`](https://github.com/Effect-TS/effect/commit/07c493a598e096c7810cd06def8cfa43493c46b1), [`257ab1b`](https://github.com/Effect-TS/effect/commit/257ab1b539fa6e930b7ae2583a188376372200d7), [`4211a23`](https://github.com/Effect-TS/effect/commit/4211a2355bb3af3f0e756e2aae9d293379f25662)]: + - effect@3.10.9 + - @effect/platform-node@0.64.15 + - @effect/platform@0.69.14 + +## 0.30.14 + +### Patch Changes + +- [#3875](https://github.com/Effect-TS/effect/pull/3875) [`6f86821`](https://github.com/Effect-TS/effect/commit/6f8682184ac7a7e8eec61c93b028af034c2c7254) Thanks @tim-smart! - fix DevTools error logging + +- Updated dependencies [[`68b5c9e`](https://github.com/Effect-TS/effect/commit/68b5c9e44f34192cef26e1cadda5e661a027df41), [`9c9928d`](https://github.com/Effect-TS/effect/commit/9c9928dfeacd9ac33dc37eb0ca3d7d8c39175ada), [`6306e66`](https://github.com/Effect-TS/effect/commit/6306e6656092b350d4ede5746da6f245ec9f7e07), [`361c7f3`](https://github.com/Effect-TS/effect/commit/361c7f39a2c10ede9324847c3d3ba192a6f9b20a)]: + - effect@3.10.8 + - @effect/platform@0.69.13 + - @effect/platform-node@0.64.14 + +## 0.30.13 + +### Patch Changes + +- Updated dependencies [[`33f5b9f`](https://github.com/Effect-TS/effect/commit/33f5b9ffaebea4f1bd0e391b44c41fb6230e743a), [`50f0281`](https://github.com/Effect-TS/effect/commit/50f0281b0d2116726b8927a6217622d5f394f3e4)]: + - effect@3.10.7 + - @effect/platform@0.69.12 + - @effect/platform-node@0.64.13 + +## 0.30.12 + +### Patch Changes + +- Updated dependencies [[`ce1c21f`](https://github.com/Effect-TS/effect/commit/ce1c21ffc11902ac9ab453a51904207859d38552), [`81ddd45`](https://github.com/Effect-TS/effect/commit/81ddd45fc074b98206fafab416d9a5a28b31e07a)]: + - effect@3.10.6 + - @effect/platform@0.69.11 + - @effect/platform-node@0.64.12 + +## 0.30.11 + +### Patch Changes + +- Updated dependencies [[`3a6d757`](https://github.com/Effect-TS/effect/commit/3a6d757badeebe00d8ef4d67530d073c8264dcfa), [`59d813a`](https://github.com/Effect-TS/effect/commit/59d813aa4973d1115cfc70cc3667508335f49693)]: + - effect@3.10.5 + - @effect/platform@0.69.10 + - @effect/platform-node@0.64.11 + +## 0.30.10 + +### Patch Changes + +- Updated dependencies [[`2367708`](https://github.com/Effect-TS/effect/commit/2367708be449f9526a2047e321302d7bfb16f18e)]: + - @effect/platform@0.69.9 + - effect@3.10.4 + - @effect/platform-node@0.64.10 + +## 0.30.9 + +### Patch Changes + +- Updated dependencies [[`522f7c5`](https://github.com/Effect-TS/effect/commit/522f7c518a5acfb55ef96d6796869f002cc3eaf8)]: + - @effect/platform@0.69.8 + - @effect/platform-node@0.64.9 + +## 0.30.8 + +### Patch Changes + +- Updated dependencies [[`690d6c5`](https://github.com/Effect-TS/effect/commit/690d6c54d2145adb0af545c447db7d4755bf3c6b), [`b9423d8`](https://github.com/Effect-TS/effect/commit/b9423d8bf8181a2389fdbce1e3c14ac6fe8d54f5), [`279fe3a`](https://github.com/Effect-TS/effect/commit/279fe3a7168fe84e520c2cc88ba189a15f03a2bc)]: + - @effect/platform@0.69.7 + - effect@3.10.3 + - @effect/platform-node@0.64.8 + +## 0.30.7 + +### Patch Changes + +- Updated dependencies [[`714e119`](https://github.com/Effect-TS/effect/commit/714e11945e45e5a2554ee058e6c43f82a8e309cf), [`c1afd55`](https://github.com/Effect-TS/effect/commit/c1afd55c54e61f9c432823d21b3d016f79160a37), [`42cd72a`](https://github.com/Effect-TS/effect/commit/42cd72a44ca9593e4d81fbb50e8111625fd0fb81)]: + - effect@3.10.2 + - @effect/platform@0.69.6 + - @effect/platform-node@0.64.7 + +## 0.30.6 + +### Patch Changes + +- [#3818](https://github.com/Effect-TS/effect/pull/3818) [`9604d6b`](https://github.com/Effect-TS/effect/commit/9604d6b616435103dafea8b53637a9d1450b4750) Thanks @tim-smart! - wait for connection in DevTools client with 1s timeout + +- Updated dependencies [[`9604d6b`](https://github.com/Effect-TS/effect/commit/9604d6b616435103dafea8b53637a9d1450b4750)]: + - effect@3.10.1 + - @effect/platform@0.69.5 + - @effect/platform-node@0.64.6 + +## 0.30.5 + +### Patch Changes + +- Updated dependencies [[`c86b1d7`](https://github.com/Effect-TS/effect/commit/c86b1d7cd47b66df190ef9775a475467c1abdbd6)]: + - @effect/platform@0.69.4 + - @effect/platform-node@0.64.5 + +## 0.30.4 + +### Patch Changes + +- Updated dependencies [[`d5fba63`](https://github.com/Effect-TS/effect/commit/d5fba6391e1005e374aa0238f13edfbd65848313), [`1eb2c30`](https://github.com/Effect-TS/effect/commit/1eb2c30ba064398db5790e376dedcfad55b7b005), [`02d413e`](https://github.com/Effect-TS/effect/commit/02d413e7b6bc1c64885969c37cc3e4e690c94d7d)]: + - @effect/platform@0.69.3 + - @effect/platform-node@0.64.4 + +## 0.30.3 + +### Patch Changes + +- Updated dependencies [[`e7afc47`](https://github.com/Effect-TS/effect/commit/e7afc47ce83e381c3f4aed2b2974e3b3d86a2340)]: + - @effect/platform@0.69.2 + - @effect/platform-node@0.64.3 + +## 0.30.2 + +### Patch Changes + +- Updated dependencies [[`a4aa34a`](https://github.com/Effect-TS/effect/commit/a4aa34a0c32b79f7c95f3eb36ee69a8e8e23684c)]: + - @effect/platform-node@0.64.2 + +## 0.30.1 + +### Patch Changes + +- Updated dependencies [[`7564f56`](https://github.com/Effect-TS/effect/commit/7564f56bb2844cf39d2b0d2d9e93cf9b2205e9a8), [`7564f56`](https://github.com/Effect-TS/effect/commit/7564f56bb2844cf39d2b0d2d9e93cf9b2205e9a8)]: + - @effect/platform@0.69.1 + - @effect/platform-node@0.64.1 + +## 0.30.0 + +### Patch Changes + +- [#3764](https://github.com/Effect-TS/effect/pull/3764) [`6d9de6b`](https://github.com/Effect-TS/effect/commit/6d9de6b871c5c08e6509a4e830c3d74758faa198) Thanks @tim-smart! - add identifiers to VariantSchema variants + +- Updated dependencies [[`4a01828`](https://github.com/Effect-TS/effect/commit/4a01828b66d6213e9bbe18979c893b13f7bb29bf), [`6d9de6b`](https://github.com/Effect-TS/effect/commit/6d9de6b871c5c08e6509a4e830c3d74758faa198), [`4a01828`](https://github.com/Effect-TS/effect/commit/4a01828b66d6213e9bbe18979c893b13f7bb29bf), [`c79c4c1`](https://github.com/Effect-TS/effect/commit/c79c4c178390fe61ff6dda88c9e058862349343a), [`38d30f0`](https://github.com/Effect-TS/effect/commit/38d30f08b8da62f9c3e308b9250738cb8d17bdb5), [`5821ce3`](https://github.com/Effect-TS/effect/commit/5821ce3455b47d25e0a40cae6ce22af9db5fa556)]: + - effect@3.10.0 + - @effect/platform@0.69.0 + - @effect/platform-node@0.64.0 + +## 0.29.6 + +### Patch Changes + +- Updated dependencies [[`382556f`](https://github.com/Effect-TS/effect/commit/382556f8930780c0634de681077706113a8c8239), [`97cb014`](https://github.com/Effect-TS/effect/commit/97cb0145114b2cd2f378e98f6c4ff5bf2c1865f5)]: + - @effect/schema@0.75.5 + - @effect/platform@0.68.6 + - @effect/platform-node@0.63.6 + +## 0.29.5 + +### Patch Changes + +- Updated dependencies [[`2036402`](https://github.com/Effect-TS/effect/commit/20364020b8b75a684791aa93d90626758023e9e9)]: + - @effect/platform@0.68.5 + - @effect/platform-node@0.63.5 + +## 0.29.4 + +### Patch Changes + +- Updated dependencies [[`1b1ef29`](https://github.com/Effect-TS/effect/commit/1b1ef29ae302322f69dc938f9337aa97b4c63266)]: + - @effect/platform@0.68.4 + - @effect/platform-node@0.63.4 + +## 0.29.3 + +### Patch Changes + +- Updated dependencies [[`61a99b2`](https://github.com/Effect-TS/effect/commit/61a99b2bf9d757870ef0c2ec9d4c877cdd364a3d), [`8c33087`](https://github.com/Effect-TS/effect/commit/8c330879425e80bed2f65e407cd59e991f0d7bec), [`3bcdfb3`](https://github.com/Effect-TS/effect/commit/3bcdfb3b6453959f449b075130e2db941653f722)]: + - effect@3.9.2 + - @effect/platform-node@0.63.3 + - @effect/platform@0.68.3 + - @effect/schema@0.75.4 + +## 0.29.2 + +### Patch Changes + +- [#3631](https://github.com/Effect-TS/effect/pull/3631) [`bd160a4`](https://github.com/Effect-TS/effect/commit/bd160a4f714b0f1cb5867e458fd70f9131b060d6) Thanks @tim-smart! - add Sse module to experimental, for parsing server-side-events + +- Updated dependencies [[`360ec14`](https://github.com/Effect-TS/effect/commit/360ec14dd4102c526aef7433a8881ad4d9beab75)]: + - @effect/schema@0.75.3 + - @effect/platform@0.68.2 + - @effect/platform-node@0.63.2 + +## 0.29.1 + +### Patch Changes + +- Updated dependencies [[`b75ac5d`](https://github.com/Effect-TS/effect/commit/b75ac5d0909115507bedc90f18f2d34deb217769)]: + - @effect/platform@0.68.1 + - @effect/platform-node@0.63.1 + +## 0.29.0 + +### Patch Changes + +- Updated dependencies [[`f02b354`](https://github.com/Effect-TS/effect/commit/f02b354ab5b0451143b82bb73dc866be29adec85), [`90ceeab`](https://github.com/Effect-TS/effect/commit/90ceeab3a04051b740af18c8af8bd73ee8ec6363), [`90ceeab`](https://github.com/Effect-TS/effect/commit/90ceeab3a04051b740af18c8af8bd73ee8ec6363), [`90ceeab`](https://github.com/Effect-TS/effect/commit/90ceeab3a04051b740af18c8af8bd73ee8ec6363)]: + - @effect/schema@0.75.2 + - @effect/platform-node@0.63.0 + - @effect/platform@0.68.0 + +## 0.28.1 + +### Patch Changes + +- Updated dependencies [[`3b2ad1d`](https://github.com/Effect-TS/effect/commit/3b2ad1d58a2e33dc1a72b7037396bd25ca1702a9)]: + - @effect/platform@0.67.1 + - effect@3.9.1 + - @effect/schema@0.75.1 + - @effect/platform-node@0.62.1 + +## 0.28.0 + +### Patch Changes + +- Updated dependencies [[`ff3d1aa`](https://github.com/Effect-TS/effect/commit/ff3d1aab290b4d1173b2dfc7e4c76abb4babdc16), [`0ba66f2`](https://github.com/Effect-TS/effect/commit/0ba66f2451641fd6990e02ec1ed01c014db9dab0), [`bf77f51`](https://github.com/Effect-TS/effect/commit/bf77f51b323c383224ebf08adf77a7a6e8c9b3cd), [`016f9ad`](https://github.com/Effect-TS/effect/commit/016f9ad931a4b3d09a34e5caf13d87c5b8e9c984), [`0779681`](https://github.com/Effect-TS/effect/commit/07796813f07de035719728733096ba64ce333469), [`534129f`](https://github.com/Effect-TS/effect/commit/534129f8113ce1a8ec50828083e16da9c86326c6), [`d75140c`](https://github.com/Effect-TS/effect/commit/d75140c7a664ceda43142d999f4ff8dcd36d6dda), [`be0451c`](https://github.com/Effect-TS/effect/commit/be0451c149b6618af79cb839cdf04af2db1efb03), [`9237ac6`](https://github.com/Effect-TS/effect/commit/9237ac69bc07de5b3b60076a0ad2921c21de7457), [`be0451c`](https://github.com/Effect-TS/effect/commit/be0451c149b6618af79cb839cdf04af2db1efb03), [`5b36494`](https://github.com/Effect-TS/effect/commit/5b364942e9a9003fdb8217324f8a2d8369c969da), [`c716adb`](https://github.com/Effect-TS/effect/commit/c716adb250ebbea1d1048d818ef7fed4f621d186), [`4986391`](https://github.com/Effect-TS/effect/commit/49863919cd8628c962a712fb1df30d2983820933), [`d75140c`](https://github.com/Effect-TS/effect/commit/d75140c7a664ceda43142d999f4ff8dcd36d6dda), [`d1387ae`](https://github.com/Effect-TS/effect/commit/d1387aebd1ff01bbebde26be46d488956e4daef6)]: + - effect@3.9.0 + - @effect/platform@0.67.0 + - @effect/schema@0.75.0 + - @effect/platform-node@0.62.0 + +## 0.27.4 + +### Patch Changes + +- Updated dependencies [[`88e85db`](https://github.com/Effect-TS/effect/commit/88e85db34bd402526e27a323e950d053fa34d232), [`f40da15`](https://github.com/Effect-TS/effect/commit/f40da15fbeb7c491840b8f409d47de79720891c3), [`8e94585`](https://github.com/Effect-TS/effect/commit/8e94585abe62753bf3af28bfae77926a7c570ac3), [`83887ca`](https://github.com/Effect-TS/effect/commit/83887ca1b1793916913d8550a4db4450cd14a044), [`5266b6c`](https://github.com/Effect-TS/effect/commit/5266b6cd86d76c3886da041c8829bca04b1a3110), [`cdead5c`](https://github.com/Effect-TS/effect/commit/cdead5c9cfd54dc6c4f215d9732f654c4a12e991), [`f40da15`](https://github.com/Effect-TS/effect/commit/f40da15fbeb7c491840b8f409d47de79720891c3), [`766a8af`](https://github.com/Effect-TS/effect/commit/766a8af307b414aca3648d91c4eab7493a5ec862)]: + - effect@3.8.5 + - @effect/platform@0.66.3 + - @effect/platform-node@0.61.4 + - @effect/schema@0.74.2 + +## 0.27.3 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform-node@0.61.3 + +## 0.27.2 + +### Patch Changes + +- [#3676](https://github.com/Effect-TS/effect/pull/3676) [`0a68746`](https://github.com/Effect-TS/effect/commit/0a68746c89651c364db2ee8c72dcfe552e1782ea) Thanks @tomglaize! - Add VariantSchema fieldFromKey utility to rename the encoded side of a field by variant. + + Example usage: + + ```ts + import { Schema } from "@effect/schema" + import { VariantSchema } from "@effect/experimental" + + const { Class, fieldFromKey } = VariantSchema.make({ + variants: ["domain", "json"], + defaultVariant: "domain" + }) + + class User extends Class("User")({ + id: Schema.Int, + firstName: Schema.String.pipe(fieldFromKey({ json: "first_name" })) + }) {} + + console.log( + Schema.encodeSync(User.json)({ + id: 1, + firstName: "Bob" + }) + ) + /* + { id: 1, first_name: 'Bob' } + */ + ``` + +- Updated dependencies [[`734eae6`](https://github.com/Effect-TS/effect/commit/734eae654f215e4adca457d04d2a1728b1a55c83), [`fd83d0e`](https://github.com/Effect-TS/effect/commit/fd83d0e548feff9ea2d53d370a0b626c4a1d940e), [`4509656`](https://github.com/Effect-TS/effect/commit/45096569d50262275ee984f44c456f5c83b62683), [`ad7e1de`](https://github.com/Effect-TS/effect/commit/ad7e1de948745c0751bfdac96671028ff4b7a727), [`090e41c`](https://github.com/Effect-TS/effect/commit/090e41c636d720b1c7d89684a739855765ed4382), [`090e41c`](https://github.com/Effect-TS/effect/commit/090e41c636d720b1c7d89684a739855765ed4382)]: + - @effect/schema@0.74.1 + - @effect/platform@0.66.2 + - effect@3.8.4 + - @effect/platform-node@0.61.2 + +## 0.27.1 + +### Patch Changes + +- Updated dependencies [[`3812788`](https://github.com/Effect-TS/effect/commit/3812788d79caaab8f559a62fd443018a04ac5647)]: + - @effect/platform@0.66.1 + - @effect/platform-node@0.61.1 + +## 0.27.0 + +### Patch Changes + +- Updated dependencies [[`de48aa5`](https://github.com/Effect-TS/effect/commit/de48aa54e98d97722a8a4c2c8f9e1fe1d4560ea2)]: + - @effect/schema@0.74.0 + - @effect/platform-node@0.61.0 + - @effect/platform@0.66.0 + +## 0.26.6 + +### Patch Changes + +- Updated dependencies [[`321b201`](https://github.com/Effect-TS/effect/commit/321b201adcb6bbbeb806b3467dd0b4cf063ccda8), [`bb5ec6b`](https://github.com/Effect-TS/effect/commit/bb5ec6b4b6a6f537394596c5a596faf52cb2aef4)]: + - @effect/platform@0.65.5 + - effect@3.8.3 + - @effect/platform-node@0.60.5 + - @effect/schema@0.73.4 + +## 0.26.5 + +### Patch Changes + +- Updated dependencies [[`e6440a7`](https://github.com/Effect-TS/effect/commit/e6440a74fb3f12f6422ed794c07cb44af91cbacc)]: + - @effect/schema@0.73.3 + - @effect/platform@0.65.4 + - @effect/platform-node@0.60.4 + +## 0.26.4 + +### Patch Changes + +- [#3632](https://github.com/Effect-TS/effect/pull/3632) [`b86b47d`](https://github.com/Effect-TS/effect/commit/b86b47d57ca30e1c3587a134a4c79aaf7bfa8980) Thanks @tim-smart! - fix assignability of ChannelSchema apis + +## 0.26.3 + +### Patch Changes + +- Updated dependencies [[`f0d8ef1`](https://github.com/Effect-TS/effect/commit/f0d8ef1ce97ec2a87b09b3e24150cfeab85d6e2f)]: + - effect@3.8.2 + - @effect/platform@0.65.3 + - @effect/platform-node@0.60.3 + - @effect/schema@0.73.2 + +## 0.26.2 + +### Patch Changes + +- Updated dependencies [[`10bf621`](https://github.com/Effect-TS/effect/commit/10bf6213f36d8ddb00f058a4609b85220f3d8334), [`f56ab78`](https://github.com/Effect-TS/effect/commit/f56ab785cbee0c1c43bd2c182c35602f486f61f0), [`ae36fa6`](https://github.com/Effect-TS/effect/commit/ae36fa68f754eeab9a54b6dc0f8b44db513aa2b6)]: + - effect@3.8.1 + - @effect/schema@0.73.1 + - @effect/platform@0.65.2 + - @effect/platform-node@0.60.2 + +## 0.26.1 + +### Patch Changes + +- Updated dependencies [[`e44c5f2`](https://github.com/Effect-TS/effect/commit/e44c5f228215738fe4e75023c7461bf9521249cb)]: + - @effect/platform@0.65.1 + - @effect/platform-node@0.60.1 + +## 0.26.0 + +### Patch Changes + +- [#3541](https://github.com/Effect-TS/effect/pull/3541) [`f1b5b3c`](https://github.com/Effect-TS/effect/commit/f1b5b3c36230f177cf01f1b5a5e9a06b8039e9ed) Thanks @tim-smart! - add string variants to Ndjson apis + +- [#3541](https://github.com/Effect-TS/effect/pull/3541) [`f1b5b3c`](https://github.com/Effect-TS/effect/commit/f1b5b3c36230f177cf01f1b5a5e9a06b8039e9ed) Thanks @tim-smart! - make sure DevTools sends final messsages + +- Updated dependencies [[`fcfa6ee`](https://github.com/Effect-TS/effect/commit/fcfa6ee30ffd07d998bf22799357bf58580a116f), [`bb9931b`](https://github.com/Effect-TS/effect/commit/bb9931b62e249a3b801f2cb9d097aec0c8511af7), [`5798f76`](https://github.com/Effect-TS/effect/commit/5798f7619529de33e5ba06f551806f68fedc19db), [`5f0bfa1`](https://github.com/Effect-TS/effect/commit/5f0bfa17205398d4e4818bfbcf9e1b505b3b1fc5), [`7fdf9d9`](https://github.com/Effect-TS/effect/commit/7fdf9d9aa1e2c1c125cbf87991e6efbf4abb7b07), [`812a4e8`](https://github.com/Effect-TS/effect/commit/812a4e86e2d1aa23b477ef5829aa0e5c07784936), [`6a128f6`](https://github.com/Effect-TS/effect/commit/6a128f63f9b41fec2db70790b3bbb96cb9afa1ab), [`273565e`](https://github.com/Effect-TS/effect/commit/273565e7901639e8d0541930ab715aea9c80fbaa), [`adf7d7a`](https://github.com/Effect-TS/effect/commit/adf7d7a7dfce3a7021e9f3b0d847dc85be89d754), [`569a801`](https://github.com/Effect-TS/effect/commit/569a8017ef0a0bc203e4312867cbdd37b0effbd7), [`aa1fa53`](https://github.com/Effect-TS/effect/commit/aa1fa5301e886b9657c8eb0d38cb87cef92a8305), [`02f6b06`](https://github.com/Effect-TS/effect/commit/02f6b0660e12bee1069532a9cc18d3ab855257be), [`12b893e`](https://github.com/Effect-TS/effect/commit/12b893e63cc6dfada4aca7773b4783940e2edf25), [`bbad27e`](https://github.com/Effect-TS/effect/commit/bbad27ec0a90860593f759405caa877e7f4a655f), [`7041393`](https://github.com/Effect-TS/effect/commit/7041393cff132e96566d3f36da0483a6ff6195e4), [`e0d21a5`](https://github.com/Effect-TS/effect/commit/e0d21a54c8323728fbb75a32f4820a9996257809), [`adf7d7a`](https://github.com/Effect-TS/effect/commit/adf7d7a7dfce3a7021e9f3b0d847dc85be89d754), [`007289a`](https://github.com/Effect-TS/effect/commit/007289a52d5877f8e90e2dacf38171ff9bf603fd), [`42a8f99`](https://github.com/Effect-TS/effect/commit/42a8f99740eefdaf2c4544d2c345313f97547a36), [`eebfd29`](https://github.com/Effect-TS/effect/commit/eebfd29633fd5d38b505c5c0842036f61f05e913), [`040703d`](https://github.com/Effect-TS/effect/commit/040703d0e100cd5511e52d812c15492414262b5e)]: + - effect@3.8.0 + - @effect/schema@0.73.0 + - @effect/platform@0.65.0 + - @effect/platform-node@0.60.0 + +## 0.25.2 + +### Patch Changes + +- Updated dependencies [[`35a0f81`](https://github.com/Effect-TS/effect/commit/35a0f813141652d696461cd5d19fd146adaf85be), [`8261c5a`](https://github.com/Effect-TS/effect/commit/8261c5ae6fe86872292ec1fc1a58ab9cea2f5f51)]: + - effect@3.7.3 + - @effect/platform@0.64.1 + - @effect/platform-node@0.59.1 + - @effect/schema@0.72.4 + +## 0.25.1 + +### Patch Changes + +- [#3574](https://github.com/Effect-TS/effect/pull/3574) [`ce86193`](https://github.com/Effect-TS/effect/commit/ce86193546fd4cf5682df6cef0ab1d486136cc15) Thanks @tim-smart! - fix VariantSchema variant validation + +## 0.25.0 + +### Patch Changes + +- Updated dependencies [[`f6acb71`](https://github.com/Effect-TS/effect/commit/f6acb71b17a0e6b0d449e7f661c9e2c3d335fcac), [`90ac8f6`](https://github.com/Effect-TS/effect/commit/90ac8f6f6053a2e4498f8b0cc56fe12777d02e1a), [`90ac8f6`](https://github.com/Effect-TS/effect/commit/90ac8f6f6053a2e4498f8b0cc56fe12777d02e1a), [`3791e24`](https://github.com/Effect-TS/effect/commit/3791e241636b1dfe924a56f380ebc9a7ff0827a9), [`3791e24`](https://github.com/Effect-TS/effect/commit/3791e241636b1dfe924a56f380ebc9a7ff0827a9)]: + - @effect/schema@0.72.3 + - @effect/platform-node@0.59.0 + - @effect/platform@0.64.0 + +## 0.24.4 + +### Patch Changes + +- [#3557](https://github.com/Effect-TS/effect/pull/3557) [`c969f74`](https://github.com/Effect-TS/effect/commit/c969f74f6111e852fc55c126d2ee384cba718878) Thanks @tim-smart! - add ChannelSchema module to /experimental, for integrating /schema with Channels + +## 0.24.3 + +### Patch Changes + +- Updated dependencies [[`4a701c4`](https://github.com/Effect-TS/effect/commit/4a701c406da032563fedae459536c00ae5cfe3c7)]: + - @effect/platform@0.63.3 + - @effect/platform-node@0.58.3 + +## 0.24.2 + +### Patch Changes + +- Updated dependencies [[`8a601d7`](https://github.com/Effect-TS/effect/commit/8a601d7a1f8ffe52ac9e6d67e9282a1495fe59c9), [`353ba19`](https://github.com/Effect-TS/effect/commit/353ba19f9b2b9e959f0a00d058c6d40a4bc02db7)]: + - effect@3.7.2 + - @effect/platform@0.63.2 + - @effect/platform-node@0.58.2 + - @effect/schema@0.72.2 + +## 0.24.1 + +### Patch Changes + +- Updated dependencies [[`79859e7`](https://github.com/Effect-TS/effect/commit/79859e71040d8edf1868b8530b90c650f4321eff), [`f6a469c`](https://github.com/Effect-TS/effect/commit/f6a469c190b9f00eee5ea0cd4d5912a0ef8b46f5), [`dcb9ec0`](https://github.com/Effect-TS/effect/commit/dcb9ec0db443894dd204d87450f779c44b9ad7f1), [`79aa6b1`](https://github.com/Effect-TS/effect/commit/79aa6b136e1f29b36f34e88cb2ff162bff2bb4ed)]: + - effect@3.7.1 + - @effect/platform@0.63.1 + - @effect/platform-node@0.58.1 + - @effect/schema@0.72.1 + +## 0.24.0 + +### Patch Changes + +- Updated dependencies [[`db89601`](https://github.com/Effect-TS/effect/commit/db89601ee9c1050c4e762b7bd7ec65a6a2799dfe), [`2f456cc`](https://github.com/Effect-TS/effect/commit/2f456cce5012b9fcb6b4e039190d527813b75b92), [`8745e41`](https://github.com/Effect-TS/effect/commit/8745e41ed96e3765dc6048efc2a9afbe05c8a1e9), [`e557838`](https://github.com/Effect-TS/effect/commit/e55783886b046d3c5f33447f455f9ccf2fa75922), [`d6e7e40`](https://github.com/Effect-TS/effect/commit/d6e7e40b1e2ad0c59aa02f07344d28601b14ebdc), [`8356321`](https://github.com/Effect-TS/effect/commit/8356321598da04bd77c1001f45a4e447bec5591d), [`192f2eb`](https://github.com/Effect-TS/effect/commit/192f2ebb2c4ddbf4bfd8baedd32140b2376868f4), [`718cb70`](https://github.com/Effect-TS/effect/commit/718cb70038629a6d58d02e407760e341f7c94474), [`e9d0310`](https://github.com/Effect-TS/effect/commit/e9d03107acbf204d9304f3e8aea0816b7d3c7dfb), [`6bf28f7`](https://github.com/Effect-TS/effect/commit/6bf28f7e3b1e5e0608ff567205fea0581d11666f)]: + - effect@3.7.0 + - @effect/platform@0.63.0 + - @effect/platform-node@0.58.0 + - @effect/schema@0.72.0 + +## 0.23.7 + +### Patch Changes + +- Updated dependencies [[`e809286`](https://github.com/Effect-TS/effect/commit/e8092865900608c4df7a6b7991b1c13cc1e4ca2d)]: + - effect@3.6.8 + - @effect/platform@0.62.5 + - @effect/platform-node@0.57.5 + - @effect/schema@0.71.4 + +## 0.23.6 + +### Patch Changes + +- Updated dependencies [[`e7a65e3`](https://github.com/Effect-TS/effect/commit/e7a65e3c6a08636bbfce3d3af3098bf28474364d), [`50ec889`](https://github.com/Effect-TS/effect/commit/50ec8897a49b7d1fe84f63107f89d543c52f3dfc)]: + - @effect/platform@0.62.4 + - effect@3.6.7 + - @effect/platform-node@0.57.4 + - @effect/schema@0.71.3 + +## 0.23.5 + +### Patch Changes + +- Updated dependencies [[`f960bf4`](https://github.com/Effect-TS/effect/commit/f960bf45239e9badac6e0ad3a602f4174cd7bbdf), [`46a575f`](https://github.com/Effect-TS/effect/commit/46a575f48a05457b782fb21f7827d338c9b59320)]: + - effect@3.6.6 + - @effect/platform@0.62.3 + - @effect/platform-node@0.57.3 + - @effect/schema@0.71.2 + +## 0.23.4 + +### Patch Changes + +- [#3487](https://github.com/Effect-TS/effect/pull/3487) [`00670d0`](https://github.com/Effect-TS/effect/commit/00670d0f8dfda42150c640e3e949e1f8ad3bdaed) Thanks @tim-smart! - ensure VariantSchema preserves opaque types + +- [#3488](https://github.com/Effect-TS/effect/pull/3488) [`8dd3959`](https://github.com/Effect-TS/effect/commit/8dd3959e967ca2b38ba601d94a80f1c50e9445e0) Thanks @tim-smart! - move VariantSchema.extract to factory, and copy type level behaviour + +- [#3482](https://github.com/Effect-TS/effect/pull/3482) [`dba570a`](https://github.com/Effect-TS/effect/commit/dba570a8e9554958626e5a8ec9ca556345b1bfd2) Thanks @tim-smart! - make VariantSchema constructor apis internal + +- [#3493](https://github.com/Effect-TS/effect/pull/3493) [`f2c8dbb`](https://github.com/Effect-TS/effect/commit/f2c8dbb77e196c9a36cb3bf2ae3b82ce68e9874d) Thanks @tim-smart! - add VariantSchema.Union constructor + +- [#3478](https://github.com/Effect-TS/effect/pull/3478) [`da52556`](https://github.com/Effect-TS/effect/commit/da52556cfe5a3b35d21563dfdbdde2146d74d3e1) Thanks @tim-smart! - Add VariantSchema.fieldEvolve api + +- Updated dependencies [[`413994c`](https://github.com/Effect-TS/effect/commit/413994c9792f16d9d57cca3ae6eb254bf93bd261), [`14a47a8`](https://github.com/Effect-TS/effect/commit/14a47a8c1f3cff2186b8fe7a919a1d773888fb5b), [`0c09841`](https://github.com/Effect-TS/effect/commit/0c0984173be3d58f050b300a1a8aa89d76ba49ae)]: + - @effect/platform@0.62.2 + - effect@3.6.5 + - @effect/platform-node@0.57.2 + - @effect/schema@0.71.1 + +## 0.23.3 + +### Patch Changes + +- [#3471](https://github.com/Effect-TS/effect/pull/3471) [`c3446d3`](https://github.com/Effect-TS/effect/commit/c3446d3e57b0cbfe9341d6f2aebf5f5d6fefefe3) Thanks @tim-smart! - add VariantSchema.Overrideable, for creating fields with optional defaults + +- Updated dependencies [[`9efe0e5`](https://github.com/Effect-TS/effect/commit/9efe0e5b57ac557399be620822c21cc6e9add285)]: + - @effect/platform@0.62.1 + - @effect/platform-node@0.57.1 + +## 0.23.2 + +### Patch Changes + +- [#3467](https://github.com/Effect-TS/effect/pull/3467) [`cfcfbdf`](https://github.com/Effect-TS/effect/commit/cfcfbdfe586b011a5edc28083fd5391edeee0023) Thanks @tim-smart! - add VariantSchema.fields for accessing the fields + +## 0.23.1 + +### Patch Changes + +- [#3455](https://github.com/Effect-TS/effect/pull/3455) [`e9da539`](https://github.com/Effect-TS/effect/commit/e9da5396bba99b2ddc20c97c7955154e6da4cab5) Thanks @tim-smart! - add VariantSchema module to experimental + + The `VariantSchema` module can be used to schemas with multiple variants. + + ```ts + import { VariantSchema } from "@effect/experimental" + import { Schema } from "@effect/schema" + import { DateTime } from "effect" + + export const { Class, Field, Struct } = VariantSchema.factory({ + variants: ["database", "api"], + defaultVariant: "database" + }) + + class User extends Class("User")({ + id: Schema.Number, + createdAt: Field({ + database: Schema.DateTimeUtc.pipe( + Schema.optionalWith({ default: DateTime.unsafeNow }) + ), + api: Schema.DateTimeUtc + }), + updateAt: Field({ + database: Schema.DateTimeUtc.pipe( + Schema.optionalWith({ default: DateTime.unsafeNow }) + ), + api: Schema.DateTimeUtc + }) + }) {} + + // the class will use the `defaultVariant` fields + const user = new User({ id: 1 }) + user.createdAt + user.updateAt + + // access the `Schema.Struct` variants as static props + User.database + User.api + ``` + +## 0.23.0 + +### Patch Changes + +- [#3450](https://github.com/Effect-TS/effect/pull/3450) [`0e42a8f`](https://github.com/Effect-TS/effect/commit/0e42a8f045ecb1fd3d080edf3d49fef16a9b0ca1) Thanks @tim-smart! - update dependencies + +- Updated dependencies [[`c1987e2`](https://github.com/Effect-TS/effect/commit/c1987e25c8f5c48bdc9ad223d7a6f2c32f93f5a1), [`8295281`](https://github.com/Effect-TS/effect/commit/8295281ae9bd7441e680402540bf3c8682ec417b), [`c940df6`](https://github.com/Effect-TS/effect/commit/c940df63800bf3c4396d91cf28ec34938642fd2c), [`00b6c6d`](https://github.com/Effect-TS/effect/commit/00b6c6d4001f5de728b7d990a1b14560b4961a63), [`1ceed14`](https://github.com/Effect-TS/effect/commit/1ceed149dc64f4874e64b5cf2f954eba0a5a1f12), [`f8d95a6`](https://github.com/Effect-TS/effect/commit/f8d95a61ad0762147933c5c32bb6d7237e18eef4), [`0e42a8f`](https://github.com/Effect-TS/effect/commit/0e42a8f045ecb1fd3d080edf3d49fef16a9b0ca1)]: + - @effect/schema@0.71.0 + - effect@3.6.4 + - @effect/platform@0.62.0 + - @effect/platform-node@0.57.0 + +## 0.22.6 + +### Patch Changes + +- Updated dependencies [[`04adcac`](https://github.com/Effect-TS/effect/commit/04adcace913e6fc483df266874a68005e9e04ccf)]: + - effect@3.6.3 + - @effect/platform@0.61.8 + - @effect/platform-node@0.56.9 + - @effect/schema@0.70.4 + +## 0.22.5 + +### Patch Changes + +- Updated dependencies [[`17245a4`](https://github.com/Effect-TS/effect/commit/17245a4e783c19dee51529600b3b40f164fa59bc), [`630d40e`](https://github.com/Effect-TS/effect/commit/630d40eaa7eb4d2f8b6705b16d4f426bc28a7d09), [`630d40e`](https://github.com/Effect-TS/effect/commit/630d40eaa7eb4d2f8b6705b16d4f426bc28a7d09)]: + - @effect/platform@0.61.7 + - @effect/platform-node@0.56.8 + +## 0.22.4 + +### Patch Changes + +- Updated dependencies [[`99ad841`](https://github.com/Effect-TS/effect/commit/99ad8415293a82d08bd7043c563b29e2b468ca74), [`d829b57`](https://github.com/Effect-TS/effect/commit/d829b576357f2e3b203ab7e107a1492de903a106), [`fd4b2f6`](https://github.com/Effect-TS/effect/commit/fd4b2f6516b325740dde615f1cf0229edf13ca0c)]: + - @effect/schema@0.70.3 + - @effect/platform@0.61.6 + - effect@3.6.2 + - @effect/platform-node@0.56.7 + +## 0.22.3 + +### Patch Changes + +- Updated dependencies [[`76b0496`](https://github.com/Effect-TS/effect/commit/76b0496ff9d7670e3f4c07ae924d30ed7f613cee)]: + - @effect/platform-node@0.56.6 + +## 0.22.2 + +### Patch Changes + +- Updated dependencies [[`056b710`](https://github.com/Effect-TS/effect/commit/056b7108978e70612176c23991916f678d947f38), [`056b710`](https://github.com/Effect-TS/effect/commit/056b7108978e70612176c23991916f678d947f38)]: + - @effect/platform-node@0.56.5 + - @effect/platform@0.61.5 + +## 0.22.1 + +### Patch Changes + +- [#3420](https://github.com/Effect-TS/effect/pull/3420) [`fb18738`](https://github.com/Effect-TS/effect/commit/fb18738e7f0586eec2b3e658c94c3fd932e70427) Thanks @tim-smart! - properly interrupt DevTools client during finalization + +## 0.22.0 + +### Minor Changes + +- [#3417](https://github.com/Effect-TS/effect/pull/3417) [`5b0a98d`](https://github.com/Effect-TS/effect/commit/5b0a98de7998a8a22b54e5e6bd8d0e1ab988a0e7) Thanks @tim-smart! - remove TimeToLive trait in favour of `timeToLive` options + +## 0.21.4 + +### Patch Changes + +- Updated dependencies [[`e7cb109`](https://github.com/Effect-TS/effect/commit/e7cb109d0754207024a64d55b6bd2a674dd8ed7d)]: + - @effect/platform@0.61.4 + - @effect/platform-node@0.56.4 + +## 0.21.3 + +### Patch Changes + +- Updated dependencies [[`510a34d`](https://github.com/Effect-TS/effect/commit/510a34d4cc5d2f51347a53847f6c7db84d2b17c6), [`fb9f786`](https://github.com/Effect-TS/effect/commit/fb9f7867f0c895e63f9ef23e8d0941248c42179d), [`45dbb9f`](https://github.com/Effect-TS/effect/commit/45dbb9ffeaf93d9e4df99d0cd4920e41ba9a3978)]: + - effect@3.6.1 + - @effect/platform@0.61.3 + - @effect/platform-node@0.56.3 + - @effect/schema@0.70.2 + +## 0.21.2 + +### Patch Changes + +- [#3394](https://github.com/Effect-TS/effect/pull/3394) [`5c3a4d0`](https://github.com/Effect-TS/effect/commit/5c3a4d005c79efab0be928b22e37f436bc93b2ad) Thanks @tim-smart! - ensure DevTools client is shutdown + +- Updated dependencies [[`3dce357`](https://github.com/Effect-TS/effect/commit/3dce357efe4a4451d7d29859d08ac11713999b1a), [`657fc48`](https://github.com/Effect-TS/effect/commit/657fc48bb32daf2dc09c9335b3cbc3152bcbdd3b)]: + - @effect/schema@0.70.1 + - @effect/platform@0.61.2 + - @effect/platform-node@0.56.2 + +## 0.21.1 + +### Patch Changes + +- Updated dependencies [[`11223bf`](https://github.com/Effect-TS/effect/commit/11223bf9cbf5b822e0bf9a9fb2b35b2ad88af692)]: + - @effect/platform@0.61.1 + - @effect/platform-node@0.56.1 + +## 0.21.0 + +### Patch Changes + +- Updated dependencies [[`1e0fe80`](https://github.com/Effect-TS/effect/commit/1e0fe802b36c257971296617473ce0abe730e8dc), [`8135294`](https://github.com/Effect-TS/effect/commit/8135294b591ea94fde7e6f94a504608f0e630520), [`cd255a4`](https://github.com/Effect-TS/effect/commit/cd255a48872d8fb924cf713ef73f0883a9cc6987), [`3845646`](https://github.com/Effect-TS/effect/commit/3845646828e98f3c7cda1217f6cfe5f642ac0603), [`2d09078`](https://github.com/Effect-TS/effect/commit/2d09078c5948b37fc2f79ef858fe4ca3e4814085), [`4bce5a0`](https://github.com/Effect-TS/effect/commit/4bce5a0274203550ccf117d830721891b0a3d182), [`4ddbff0`](https://github.com/Effect-TS/effect/commit/4ddbff0bb4e3ffddfeb509c59835b83245fb975e), [`e74cc38`](https://github.com/Effect-TS/effect/commit/e74cc38cb420a320c4d7ef98180f19d452a8b316), [`bb069b4`](https://github.com/Effect-TS/effect/commit/bb069b49ef291c532a02c1e8e74271f6d1bb32ec), [`cd255a4`](https://github.com/Effect-TS/effect/commit/cd255a48872d8fb924cf713ef73f0883a9cc6987), [`7d02174`](https://github.com/Effect-TS/effect/commit/7d02174af3bcbf054e5cdddb821c91d0f47e8285)]: + - effect@3.6.0 + - @effect/schema@0.70.0 + - @effect/platform@0.61.0 + - @effect/platform-node@0.56.0 + +## 0.20.4 + +### Patch Changes + +- Updated dependencies [[`7c0da50`](https://github.com/Effect-TS/effect/commit/7c0da5050d30cb804f4eacb15995d0fb7f3a28d2), [`2fc0ff4`](https://github.com/Effect-TS/effect/commit/2fc0ff4c59c25977018f6ac70ced99b04a8c7b2b), [`6359644`](https://github.com/Effect-TS/effect/commit/635964446323cf55d4060559337e710e4a24496e), [`f262665`](https://github.com/Effect-TS/effect/commit/f262665c2773492c01e5dd0e8d6db235aafaaad8), [`7f41e42`](https://github.com/Effect-TS/effect/commit/7f41e428830bf3043b8be0d28dcd235d5747c942), [`9bbe7a6`](https://github.com/Effect-TS/effect/commit/9bbe7a681430ebf5c10167bb7140ba3742e46bb7), [`f566fd1`](https://github.com/Effect-TS/effect/commit/f566fd1d7eea531a0d981dd24037f14a603a1273)]: + - @effect/schema@0.69.3 + - effect@3.5.9 + - @effect/platform@0.60.3 + - @effect/platform-node@0.55.3 + +## 0.20.3 + +### Patch Changes + +- Updated dependencies [[`eb4d014`](https://github.com/Effect-TS/effect/commit/eb4d014c559e1b4c95b3fb9295fe77593c17ed7a), [`1ba640c`](https://github.com/Effect-TS/effect/commit/1ba640c702f187a866023bf043c26e25cce941ef), [`c8c71bd`](https://github.com/Effect-TS/effect/commit/c8c71bd20eb87d23133dac6156b83bb08941597c), [`a26ce58`](https://github.com/Effect-TS/effect/commit/a26ce581ca7d407e1e81439b58c8045b3fa65231), [`fc20f73`](https://github.com/Effect-TS/effect/commit/fc20f73c69e577981cb64714de2adc97e1004dae)]: + - @effect/platform@0.60.2 + - effect@3.5.8 + - @effect/platform-node@0.55.2 + - @effect/schema@0.69.2 + +## 0.20.2 + +### Patch Changes + +- Updated dependencies [[`f241154`](https://github.com/Effect-TS/effect/commit/f241154added5d91e95866c39481f09cdb13bd4d)]: + - @effect/schema@0.69.1 + - @effect/platform@0.60.1 + - @effect/platform-node@0.55.1 + +## 0.20.1 + +### Patch Changes + +- [#3325](https://github.com/Effect-TS/effect/pull/3325) [`b2b02cb`](https://github.com/Effect-TS/effect/commit/b2b02cbd0268478e856b0976aa4aa20f6918171f) Thanks @IMax153! - ensure `DevTools` client flushes requests on interrupt + +## 0.20.0 + +### Patch Changes + +- Updated dependencies [[`20807a4`](https://github.com/Effect-TS/effect/commit/20807a45edeb4334e903dca5d708cd62a71702d8)]: + - @effect/schema@0.69.0 + - @effect/platform@0.60.0 + - @effect/platform-node@0.55.0 + +## 0.19.4 + +### Patch Changes + +- [#3310](https://github.com/Effect-TS/effect/pull/3310) [`99bddcf`](https://github.com/Effect-TS/effect/commit/99bddcfb3d6eab4d489d055404e26ad81afe52fc) Thanks @fubhy! - Added additional pure annotations to improve tree-shakeability + +- Updated dependencies [[`3afcc93`](https://github.com/Effect-TS/effect/commit/3afcc93413a3d910beb69e4ce9ae120e4adaffd5), [`99bddcf`](https://github.com/Effect-TS/effect/commit/99bddcfb3d6eab4d489d055404e26ad81afe52fc), [`6921c4f`](https://github.com/Effect-TS/effect/commit/6921c4fb8c45badff09b493043b85ca71302b560)]: + - effect@3.5.7 + - @effect/platform@0.59.3 + - @effect/platform-node@0.54.4 + - @effect/schema@0.68.27 + +## 0.19.3 + +### Patch Changes + +- Updated dependencies [[`f0285d3`](https://github.com/Effect-TS/effect/commit/f0285d3af6a18829123bc1818331c67206becbc4), [`8ec4955`](https://github.com/Effect-TS/effect/commit/8ec49555ed3b3c98093fa4d135a4c57a3f16ebd1), [`3ac2d76`](https://github.com/Effect-TS/effect/commit/3ac2d76048da09e876cf6c3aee3397febd843fe9), [`cc327a1`](https://github.com/Effect-TS/effect/commit/cc327a1bccd22a4ee27ec7e58b53205e93b23e2c), [`4bfe4fb`](https://github.com/Effect-TS/effect/commit/4bfe4fb5c82f597c9beea9baa92e772593598b60), [`2b14d18`](https://github.com/Effect-TS/effect/commit/2b14d181462cad8359da4fa6bc6dfda0f742c398)]: + - @effect/schema@0.68.26 + - effect@3.5.6 + - @effect/platform@0.59.2 + - @effect/platform-node@0.54.3 + +## 0.19.2 + +### Patch Changes + +- Updated dependencies [[`a9d7800`](https://github.com/Effect-TS/effect/commit/a9d7800f6a253192b653d77778b0674f39b1ca39), [`fcecff7`](https://github.com/Effect-TS/effect/commit/fcecff7f7e12b295a252f124861b801c73072151), [`adbf753`](https://github.com/Effect-TS/effect/commit/adbf75340a9db15dc5cadc66e911a8978a195a65), [`adbf753`](https://github.com/Effect-TS/effect/commit/adbf75340a9db15dc5cadc66e911a8978a195a65)]: + - effect@3.5.5 + - @effect/platform@0.59.1 + - @effect/platform-node@0.54.2 + - @effect/schema@0.68.25 + +## 0.19.1 + +### Patch Changes + +- Updated dependencies [[`07db4ac`](https://github.com/Effect-TS/effect/commit/07db4ac8da9d07ce31bd62470a73e362a4291a0c)]: + - @effect/platform-node@0.54.1 + +## 0.19.0 + +### Minor Changes + +- [#3260](https://github.com/Effect-TS/effect/pull/3260) [`53c0db0`](https://github.com/Effect-TS/effect/commit/53c0db06872d5b5edea2a706e83249908385325c) Thanks @tim-smart! - replace /platform RefailError with use of the "cause" property + +### Patch Changes + +- [#3253](https://github.com/Effect-TS/effect/pull/3253) [`ed0dde4`](https://github.com/Effect-TS/effect/commit/ed0dde4888e6f1a97ad5bba06b755d26a6a1c52e) Thanks @tim-smart! - update dependencies + +- [#3259](https://github.com/Effect-TS/effect/pull/3259) [`5be9cc0`](https://github.com/Effect-TS/effect/commit/5be9cc044025a9541b9b7acefa2d3fc05fa1301b) Thanks @IMax153! - allow unpacking newline delimited JSON to ignore empty lines + +- Updated dependencies [[`53c0db0`](https://github.com/Effect-TS/effect/commit/53c0db06872d5b5edea2a706e83249908385325c), [`ed0dde4`](https://github.com/Effect-TS/effect/commit/ed0dde4888e6f1a97ad5bba06b755d26a6a1c52e), [`ada68b3`](https://github.com/Effect-TS/effect/commit/ada68b3e61c67907c2a281c024c84d818186ca4c), [`ca775ce`](https://github.com/Effect-TS/effect/commit/ca775cec53baebc1a43d9b8852a3ac6726178498), [`5be9cc0`](https://github.com/Effect-TS/effect/commit/5be9cc044025a9541b9b7acefa2d3fc05fa1301b), [`203658f`](https://github.com/Effect-TS/effect/commit/203658f8001c132b25764ab70344b171683b554c), [`eb1c4d4`](https://github.com/Effect-TS/effect/commit/eb1c4d44e54b9d8d201a366d1ff94face2a6dcd3)]: + - @effect/platform-node@0.54.0 + - @effect/platform@0.59.0 + - effect@3.5.4 + - @effect/schema@0.68.24 + +## 0.18.1 + +### Patch Changes + +- Updated dependencies [[`edb0da3`](https://github.com/Effect-TS/effect/commit/edb0da383746d760f35d8582f5fb0cc0eeca9217), [`edb0da3`](https://github.com/Effect-TS/effect/commit/edb0da383746d760f35d8582f5fb0cc0eeca9217), [`a1db40a`](https://github.com/Effect-TS/effect/commit/a1db40a650ab842e778654f0d88e80f2ef4fd6f3), [`c8d3fb0`](https://github.com/Effect-TS/effect/commit/c8d3fb0fe23585f6efb724af51fbab3ba1ad6e83), [`dabd028`](https://github.com/Effect-TS/effect/commit/dabd028decf9b7983ca16ebe0f48c05c11a84b68), [`786b2ab`](https://github.com/Effect-TS/effect/commit/786b2ab29d525c877bb84035dac9e2d6499339d1), [`fc57354`](https://github.com/Effect-TS/effect/commit/fc573547d41667016fce05eaee75960fcc6dce4d)]: + - effect@3.5.3 + - @effect/schema@0.68.23 + - @effect/platform@0.58.27 + - @effect/platform-node@0.53.26 + +## 0.18.0 + +### Minor Changes + +- [#3233](https://github.com/Effect-TS/effect/pull/3233) [`cb9f8a1`](https://github.com/Effect-TS/effect/commit/cb9f8a1ef656170a79bccdb73e4588ab4243b287) Thanks @tim-smart! - revert PrimaryKey fallback in /experimental Persistence + +### Patch Changes + +- Updated dependencies [[`639208e`](https://github.com/Effect-TS/effect/commit/639208eeb8a44622994f832bc2d45d06ab636bc8), [`6684b4c`](https://github.com/Effect-TS/effect/commit/6684b4c27d77a7fcc7af2e261a450edf971b62b5), [`6684b4c`](https://github.com/Effect-TS/effect/commit/6684b4c27d77a7fcc7af2e261a450edf971b62b5), [`6684b4c`](https://github.com/Effect-TS/effect/commit/6684b4c27d77a7fcc7af2e261a450edf971b62b5)]: + - effect@3.5.2 + - @effect/platform@0.58.26 + - @effect/platform-node@0.53.25 + - @effect/schema@0.68.22 + +## 0.17.27 + +### Patch Changes + +- Updated dependencies [[`0623fca`](https://github.com/Effect-TS/effect/commit/0623fca41679b0e3c5a10dd0f8985f91670bd721)]: + - @effect/platform@0.58.25 + - @effect/platform-node@0.53.24 + +## 0.17.26 + +### Patch Changes + +- Updated dependencies [[`55fdd76`](https://github.com/Effect-TS/effect/commit/55fdd761ee95afd73b6a892c13fee92b36c02837)]: + - effect@3.5.1 + - @effect/platform@0.58.24 + - @effect/platform-node@0.53.23 + - @effect/schema@0.68.21 + +## 0.17.25 + +### Patch Changes + +- Updated dependencies [[`a1f5b83`](https://github.com/Effect-TS/effect/commit/a1f5b831a1bc7535988b370d68d0b3eb1123e0ce), [`a1f5b83`](https://github.com/Effect-TS/effect/commit/a1f5b831a1bc7535988b370d68d0b3eb1123e0ce), [`a1f5b83`](https://github.com/Effect-TS/effect/commit/a1f5b831a1bc7535988b370d68d0b3eb1123e0ce), [`60bc3d0`](https://github.com/Effect-TS/effect/commit/60bc3d0867b13e48b24dc22604b4dd2e7b2c1ca4), [`5ab348f`](https://github.com/Effect-TS/effect/commit/5ab348f265db3d283aa091ddca6d2d49137c16f2), [`60bc3d0`](https://github.com/Effect-TS/effect/commit/60bc3d0867b13e48b24dc22604b4dd2e7b2c1ca4), [`3e04bf8`](https://github.com/Effect-TS/effect/commit/3e04bf8a7127e956cadb7684a8f4c661df57663b), [`e7fc45f`](https://github.com/Effect-TS/effect/commit/e7fc45f0c7002aafdaec7878149ac064cd104ea3), [`a1f5b83`](https://github.com/Effect-TS/effect/commit/a1f5b831a1bc7535988b370d68d0b3eb1123e0ce), [`4626de5`](https://github.com/Effect-TS/effect/commit/4626de59c25b384216faa0be87bf0b8cd36357d0), [`f01e7db`](https://github.com/Effect-TS/effect/commit/f01e7db317827255d7901f523f2e28b43298e8df), [`60bc3d0`](https://github.com/Effect-TS/effect/commit/60bc3d0867b13e48b24dc22604b4dd2e7b2c1ca4), [`79d2d91`](https://github.com/Effect-TS/effect/commit/79d2d91464d95dde0e9444d43e7a7f309f05d6e6), [`ac71f37`](https://github.com/Effect-TS/effect/commit/ac71f378f2413e5aa91c95f649ffe898d6a26114), [`8432360`](https://github.com/Effect-TS/effect/commit/8432360ce68614a419bb328083a4109d0fc8aa93), [`e4bf1bf`](https://github.com/Effect-TS/effect/commit/e4bf1bf2b4a970eacd77c9b77b5ea8c68bc84498), [`13cb861`](https://github.com/Effect-TS/effect/commit/13cb861a5eded15c55c6cdcf6a8acde8320367a6), [`79d2d91`](https://github.com/Effect-TS/effect/commit/79d2d91464d95dde0e9444d43e7a7f309f05d6e6), [`e7fc45f`](https://github.com/Effect-TS/effect/commit/e7fc45f0c7002aafdaec7878149ac064cd104ea3), [`9f66825`](https://github.com/Effect-TS/effect/commit/9f66825f1fce0fe8d10420c285f7dc4c71e8af8d)]: + - effect@3.5.0 + - @effect/platform@0.58.23 + - @effect/platform-node@0.53.22 + - @effect/schema@0.68.20 + +## 0.17.24 + +### Patch Changes + +- Updated dependencies [[`7af137c`](https://github.com/Effect-TS/effect/commit/7af137c9433f6e74959b3887561ec1e6f12e10ee), [`ee4b3dc`](https://github.com/Effect-TS/effect/commit/ee4b3dc5f68d19dc3ae1c2d12901c5b8ffbebabb), [`366f2ee`](https://github.com/Effect-TS/effect/commit/366f2ee3fb6f712a44e8f84fc188612e5ecc016d), [`366f2ee`](https://github.com/Effect-TS/effect/commit/366f2ee3fb6f712a44e8f84fc188612e5ecc016d), [`097d25c`](https://github.com/Effect-TS/effect/commit/097d25cb5d13c049e01789651be56b09620186ef)]: + - effect@3.4.9 + - @effect/platform-node@0.53.21 + - @effect/platform@0.58.22 + - @effect/schema@0.68.19 + +## 0.17.23 + +### Patch Changes + +- Updated dependencies [[`5d5cc6c`](https://github.com/Effect-TS/effect/commit/5d5cc6cfd7d63b07081290fb189b364999201fc5), [`a435e0f`](https://github.com/Effect-TS/effect/commit/a435e0fc5378b33a49bcec92ee235df6f16a2419), [`b5554db`](https://github.com/Effect-TS/effect/commit/b5554db36c4dd6f64fa5e6a62a29b2759c54217a), [`359ff8a`](https://github.com/Effect-TS/effect/commit/359ff8aa2e4e6389bf56d759baa804e2a7674a16), [`a9c4fb3`](https://github.com/Effect-TS/effect/commit/a9c4fb3bf3c6e92cd1c142b0605fddf7eb3c697c), [`f7534b9`](https://github.com/Effect-TS/effect/commit/f7534b94cba06b143a3d4f29275d92874a939559)]: + - @effect/schema@0.68.18 + - effect@3.4.8 + - @effect/platform@0.58.21 + - @effect/platform-node@0.53.20 + +## 0.17.22 + +### Patch Changes + +- Updated dependencies [[`15967cf`](https://github.com/Effect-TS/effect/commit/15967cf18931fb6ede3083eb687a8dfff371cc56), [`2328e17`](https://github.com/Effect-TS/effect/commit/2328e17577112db17c29b7756942a0ff64a70ee0), [`a5737d6`](https://github.com/Effect-TS/effect/commit/a5737d6db2b921605c332eabbc5402ee3d17357b)]: + - @effect/schema@0.68.17 + - effect@3.4.7 + - @effect/platform@0.58.20 + - @effect/platform-node@0.53.19 + +## 0.17.21 + +### Patch Changes + +- [#3158](https://github.com/Effect-TS/effect/pull/3158) [`c3e3ed6`](https://github.com/Effect-TS/effect/commit/c3e3ed64911385fdcfb734c5756bcb2f865df147) Thanks @tim-smart! - drop requirement of PrimaryKey in /experimental Persistence + +## 0.17.20 + +### Patch Changes + +- [#3150](https://github.com/Effect-TS/effect/pull/3150) [`271ec23`](https://github.com/Effect-TS/effect/commit/271ec23adb09169a054040d2b47cf54564c8283a) Thanks @tim-smart! - remove console.log from Redis persistence + +## 0.17.19 + +### Patch Changes + +- [#3149](https://github.com/Effect-TS/effect/pull/3149) [`cb22726`](https://github.com/Effect-TS/effect/commit/cb2272656881aa5878a1c3fc0b12d8fbc66eb63c) Thanks @tim-smart! - add PersistedCache module to /experimental + +- Updated dependencies [[`d006cec`](https://github.com/Effect-TS/effect/commit/d006cec022e8524dbfd6dc6df751fe4c86b10042), [`cb22726`](https://github.com/Effect-TS/effect/commit/cb2272656881aa5878a1c3fc0b12d8fbc66eb63c), [`e911cfd`](https://github.com/Effect-TS/effect/commit/e911cfdc79418462d7e9000976fded15ea6b738d)]: + - @effect/schema@0.68.16 + - @effect/platform@0.58.19 + - @effect/platform-node@0.53.18 + +## 0.17.18 + +### Patch Changes + +- [#3138](https://github.com/Effect-TS/effect/pull/3138) [`422a2ee`](https://github.com/Effect-TS/effect/commit/422a2ee597320cd8ce4b53fe449a1b73806ebf1a) Thanks @tim-smart! - log DevTool's errors at debug level + +- Updated dependencies [[`7f8900a`](https://github.com/Effect-TS/effect/commit/7f8900a1de9addeb0d371103a2c5c2aa3e4ff95e)]: + - @effect/platform@0.58.18 + - @effect/platform-node@0.53.17 + +## 0.17.17 + +### Patch Changes + +- Updated dependencies [[`5c0ceb0`](https://github.com/Effect-TS/effect/commit/5c0ceb00826cce9e50bf9d41d83e191d5352c030), [`5c0ceb0`](https://github.com/Effect-TS/effect/commit/5c0ceb00826cce9e50bf9d41d83e191d5352c030), [`34faeb6`](https://github.com/Effect-TS/effect/commit/34faeb6305ba52af4d6f8bdd2e633bb6a5a7a35b), [`33735b1`](https://github.com/Effect-TS/effect/commit/33735b16b41bd26929d8f4754c190925db6323b7), [`5c0ceb0`](https://github.com/Effect-TS/effect/commit/5c0ceb00826cce9e50bf9d41d83e191d5352c030), [`139d4b3`](https://github.com/Effect-TS/effect/commit/139d4b39fb3bff2eeaa7c0c809c581da42425a83)]: + - effect@3.4.6 + - @effect/schema@0.68.15 + - @effect/platform@0.58.17 + - @effect/platform-node@0.53.16 + +## 0.17.16 + +### Patch Changes + +- Updated dependencies [[`61e5964`](https://github.com/Effect-TS/effect/commit/61e59640fd993216cca8ace0ac8abd9104e213ce)]: + - @effect/schema@0.68.14 + - @effect/platform@0.58.16 + - @effect/platform-node@0.53.15 + +## 0.17.15 + +### Patch Changes + +- Updated dependencies [[`cb76bcb`](https://github.com/Effect-TS/effect/commit/cb76bcb2f8858a90db4f785efee262cea1b9844e), [`baa90df`](https://github.com/Effect-TS/effect/commit/baa90df9663f5f37d7b6814dad25142d53dbc720)]: + - @effect/schema@0.68.13 + - @effect/platform@0.58.15 + - @effect/platform-node@0.53.14 + +## 0.17.14 + +### Patch Changes + +- [#3110](https://github.com/Effect-TS/effect/pull/3110) [`296a9e2`](https://github.com/Effect-TS/effect/commit/296a9e24b536678c0623016b1f51653f2f2c4fe8) Thanks @tim-smart! - fix deadlock in @effect/experimental dataLoader + +## 0.17.13 + +### Patch Changes + +- Updated dependencies [[`52a87c7`](https://github.com/Effect-TS/effect/commit/52a87c7a0b9536398deaf8ec507e53a82c607219), [`6d2280e`](https://github.com/Effect-TS/effect/commit/6d2280e9497c95cb0e965ca462c825345074eedf)]: + - @effect/platform@0.58.14 + - @effect/platform-node@0.53.13 + +## 0.17.12 + +### Patch Changes + +- Updated dependencies [[`a047af9`](https://github.com/Effect-TS/effect/commit/a047af99447dfffc729e9c8ef0ca143537927e91), [`dbd53ea`](https://github.com/Effect-TS/effect/commit/dbd53ea363c71a24449cb068251054c3a1acf864), [`d990544`](https://github.com/Effect-TS/effect/commit/d9905444b9e800850cb65899114ca0e502e68fe8)]: + - effect@3.4.5 + - @effect/platform@0.58.13 + - @effect/schema@0.68.12 + - @effect/platform-node@0.53.12 + +## 0.17.11 + +### Patch Changes + +- Updated dependencies [[`74e0ad2`](https://github.com/Effect-TS/effect/commit/74e0ad23b4c36f41b7fd10856b20f8b701bc4044), [`74e0ad2`](https://github.com/Effect-TS/effect/commit/74e0ad23b4c36f41b7fd10856b20f8b701bc4044), [`72638e3`](https://github.com/Effect-TS/effect/commit/72638e3d99f0e93a24febf6c225256ce92d4a20b), [`d7dde2b`](https://github.com/Effect-TS/effect/commit/d7dde2b4af08b37af859d4c327c1f5c6f00cf9d9), [`9b2fc3b`](https://github.com/Effect-TS/effect/commit/9b2fc3b9dfd304a2bd0508ef2313cfc54357be0c), [`d71c192`](https://github.com/Effect-TS/effect/commit/d71c192b89fd1162423acddc5fd3d6270fbf2ef6)]: + - @effect/platform@0.58.12 + - effect@3.4.4 + - @effect/schema@0.68.11 + - @effect/platform-node@0.53.11 + +## 0.17.10 + +### Patch Changes + +- Updated dependencies [[`a5b95b5`](https://github.com/Effect-TS/effect/commit/a5b95b548284e4798654ae7ce6883fa49108f0ea), [`5e29579`](https://github.com/Effect-TS/effect/commit/5e29579187cb8420ea4930b3999fec984f8999f4)]: + - @effect/platform@0.58.11 + - @effect/platform-node@0.53.10 + +## 0.17.9 + +### Patch Changes + +- [#3079](https://github.com/Effect-TS/effect/pull/3079) [`bbdd365`](https://github.com/Effect-TS/effect/commit/bbdd36567706c94cdec45bacea825941c347b6cd) Thanks @tim-smart! - update dependencies + +- [#3079](https://github.com/Effect-TS/effect/pull/3079) [`bbdd365`](https://github.com/Effect-TS/effect/commit/bbdd36567706c94cdec45bacea825941c347b6cd) Thanks @tim-smart! - update to typescript 5.5 + +- Updated dependencies [[`a48ee84`](https://github.com/Effect-TS/effect/commit/a48ee845ac21bbde9baf938af9e97a98322211c9), [`c342739`](https://github.com/Effect-TS/effect/commit/c3427396226e1ad7b95b40595a23f9bdff3e3365), [`8898e5e`](https://github.com/Effect-TS/effect/commit/8898e5e238622f6337583d91ee23609c1f5ccdf7), [`a48ee84`](https://github.com/Effect-TS/effect/commit/a48ee845ac21bbde9baf938af9e97a98322211c9), [`a48ee84`](https://github.com/Effect-TS/effect/commit/a48ee845ac21bbde9baf938af9e97a98322211c9), [`ff78636`](https://github.com/Effect-TS/effect/commit/ff786367c522975f40f0f179a0ecdfcfab7ecbdb), [`c86bd4e`](https://github.com/Effect-TS/effect/commit/c86bd4e134c23146c216f9ff97e03781d55991b6), [`bbdd365`](https://github.com/Effect-TS/effect/commit/bbdd36567706c94cdec45bacea825941c347b6cd), [`ab3180f`](https://github.com/Effect-TS/effect/commit/ab3180f827041d0ea3b2d72254a1a8683e99e056), [`bbdd365`](https://github.com/Effect-TS/effect/commit/bbdd36567706c94cdec45bacea825941c347b6cd)]: + - @effect/platform@0.58.10 + - effect@3.4.3 + - @effect/platform-node@0.53.9 + - @effect/schema@0.68.10 + +## 0.17.8 + +### Patch Changes + +- Updated dependencies [[`0b47fdf`](https://github.com/Effect-TS/effect/commit/0b47fdfe449f42de89e0e88b61ae5140f629e5c4)]: + - @effect/schema@0.68.9 + - @effect/platform@0.58.9 + - @effect/platform-node@0.53.8 + +## 0.17.7 + +### Patch Changes + +- Updated dependencies [[`192261b`](https://github.com/Effect-TS/effect/commit/192261b2aec94e9913ceed83683fdcfbc9fca66f), [`3da1497`](https://github.com/Effect-TS/effect/commit/3da1497b5c9cc886d300258bc928fd68a4fefe6f)]: + - @effect/schema@0.68.8 + - effect@3.4.2 + - @effect/platform@0.58.8 + - @effect/platform-node@0.53.7 + +## 0.17.6 + +### Patch Changes + +- Updated dependencies [[`027004a`](https://github.com/Effect-TS/effect/commit/027004a897f654791e75faa28eefb50dd0244b6e)]: + - @effect/platform@0.58.7 + - @effect/platform-node@0.53.6 + +## 0.17.5 + +### Patch Changes + +- Updated dependencies [[`66a1910`](https://github.com/Effect-TS/effect/commit/66a19109ff90c4252123b8809b8c8a74681dba6a), [`2e8e252`](https://github.com/Effect-TS/effect/commit/2e8e2520cac712f0eb644553bd476429ebd674e4)]: + - effect@3.4.1 + - @effect/platform@0.58.6 + - @effect/platform-node@0.53.5 + - @effect/schema@0.68.7 + +## 0.17.4 + +### Patch Changes + +- Updated dependencies [[`37a07a2`](https://github.com/Effect-TS/effect/commit/37a07a2d8d1ce09ab965c0ada84a3fae9a6aba05)]: + - @effect/platform@0.58.5 + - @effect/platform-node@0.53.4 + +## 0.17.3 + +### Patch Changes + +- Updated dependencies [[`b77fb0a`](https://github.com/Effect-TS/effect/commit/b77fb0a811ec1ad0e794917077c9a90824515db8)]: + - @effect/platform@0.58.4 + - @effect/platform-node@0.53.3 + +## 0.17.2 + +### Patch Changes + +- Updated dependencies [[`530fa9e`](https://github.com/Effect-TS/effect/commit/530fa9e36b8532589b948fc4faa37593f36b7f42)]: + - @effect/schema@0.68.6 + - @effect/platform@0.58.3 + - @effect/platform-node@0.53.2 + +## 0.17.1 + +### Patch Changes + +- Updated dependencies [[`1d62815`](https://github.com/Effect-TS/effect/commit/1d62815a50f34115606940ffa397442d75a20c81)]: + - @effect/schema@0.68.5 + - @effect/platform@0.58.2 + - @effect/platform-node@0.53.1 + +## 0.17.0 + +### Minor Changes + +- [#3036](https://github.com/Effect-TS/effect/pull/3036) [`5a248aa`](https://github.com/Effect-TS/effect/commit/5a248aa5ab2db3f7131ebc79bb9871a76de57973) Thanks @tim-smart! - remove dependency on /platform-node from DevTools + +### Patch Changes + +- Updated dependencies [[`5a248aa`](https://github.com/Effect-TS/effect/commit/5a248aa5ab2db3f7131ebc79bb9871a76de57973), [`5a248aa`](https://github.com/Effect-TS/effect/commit/5a248aa5ab2db3f7131ebc79bb9871a76de57973)]: + - @effect/platform@0.58.1 + - @effect/platform-node@0.53.0 + +## 0.16.34 + +### Patch Changes + +- Updated dependencies [[`c0ce180`](https://github.com/Effect-TS/effect/commit/c0ce180861ad0938053c0e6145e813fa6404df3b), [`63dd0c3`](https://github.com/Effect-TS/effect/commit/63dd0c3af45876c1caad7d03356c74daf551c628), [`61707b6`](https://github.com/Effect-TS/effect/commit/61707b6ffc7397c2ba0dce22512b44955724f60f), [`9c1b5b3`](https://github.com/Effect-TS/effect/commit/9c1b5b39e6c19604ce834f072a114ad392c50a06), [`a35faf8`](https://github.com/Effect-TS/effect/commit/a35faf8d116f94899bfc03feab33b004c8ddfdf7), [`ff73c0c`](https://github.com/Effect-TS/effect/commit/ff73c0cacd66132bfad2e5211b3eae347729c667), [`984d516`](https://github.com/Effect-TS/effect/commit/984d516ccd9412dc41188f6a46b748dd20dd5848), [`8c3b8a2`](https://github.com/Effect-TS/effect/commit/8c3b8a2ce208eab753b6206a51605a424f104e98), [`017e2f9`](https://github.com/Effect-TS/effect/commit/017e2f9b371ce24ea4945e5d7390c934ad3c39cf), [`91bf8a2`](https://github.com/Effect-TS/effect/commit/91bf8a2e9d1959393b3cf7366cc1d584d3e666b7), [`c6a4a26`](https://github.com/Effect-TS/effect/commit/c6a4a266606575fd2c7165940c4072ad4c57d01f)]: + - effect@3.4.0 + - @effect/platform-node@0.52.0 + - @effect/platform@0.58.0 + - @effect/schema@0.68.4 + +## 0.16.33 + +### Patch Changes + +- Updated dependencies [[`3ba7ea1`](https://github.com/Effect-TS/effect/commit/3ba7ea1c3c2923e85bf2f17e41176f8f8796d203)]: + - @effect/platform@0.57.8 + - @effect/platform-node@0.51.17 + +## 0.16.32 + +### Patch Changes + +- Updated dependencies [[`d473800`](https://github.com/Effect-TS/effect/commit/d47380012c3241d7287b66968d33a2414275ce7b)]: + - @effect/schema@0.68.3 + - @effect/platform@0.57.7 + - @effect/platform-node@0.51.16 + +## 0.16.31 + +### Patch Changes + +- Updated dependencies [[`eb341b3`](https://github.com/Effect-TS/effect/commit/eb341b3eb34ad64499371bc08b7f59e429979d8a)]: + - @effect/schema@0.68.2 + - @effect/platform@0.57.6 + - @effect/platform-node@0.51.15 + +## 0.16.30 + +### Patch Changes + +- Updated dependencies [[`b8ea6aa`](https://github.com/Effect-TS/effect/commit/b8ea6aa479006358042b4256ee0a1c5cfbe57acb)]: + - @effect/platform@0.57.5 + - @effect/platform-node@0.51.14 + +## 0.16.29 + +### Patch Changes + +- Updated dependencies [[`b51e266`](https://github.com/Effect-TS/effect/commit/b51e26662b879b55d2c5164b7c97742739aa9446), [`6c89408`](https://github.com/Effect-TS/effect/commit/6c89408cd7b9204ec4c5828a46cd5312d8afb5e7)]: + - @effect/schema@0.68.1 + - effect@3.3.5 + - @effect/platform@0.57.4 + - @effect/platform-node@0.51.13 + +## 0.16.28 + +### Patch Changes + +- Updated dependencies [[`f6c7977`](https://github.com/Effect-TS/effect/commit/f6c79772e632c440b7e5221bb75f0ef9d3c3b005), [`a67b8fe`](https://github.com/Effect-TS/effect/commit/a67b8fe2ace08419424811b5f0d9a5378eaea352)]: + - @effect/schema@0.68.0 + - effect@3.3.4 + - @effect/platform@0.57.3 + - @effect/platform-node@0.51.12 + +## 0.16.27 + +### Patch Changes + +- Updated dependencies [[`3b15e1b`](https://github.com/Effect-TS/effect/commit/3b15e1b505c0b0e62a03b4a3605d42a9932cc99c), [`06ede85`](https://github.com/Effect-TS/effect/commit/06ede85d6e84710e6622463be95ff3927fb30dad), [`3a750b2`](https://github.com/Effect-TS/effect/commit/3a750b25b1ed92094a7f7ebc332a6bcfb212871b), [`7204ca5`](https://github.com/Effect-TS/effect/commit/7204ca5761c2b1d27999a624db23aa10b6e0504d)]: + - @effect/schema@0.67.24 + - effect@3.3.3 + - @effect/platform@0.57.2 + - @effect/platform-node@0.51.11 + +## 0.16.26 + +### Patch Changes + +- [#2980](https://github.com/Effect-TS/effect/pull/2980) [`7f987a3`](https://github.com/Effect-TS/effect/commit/7f987a30f1e9332d44ca23e1b2b9aec102214e58) Thanks @tim-smart! - improve dataLoader concurrency when max batch size is hit + +- Updated dependencies [[`2ee4f2b`](https://github.com/Effect-TS/effect/commit/2ee4f2be7fd63074a9cbac6dcdfb533b6683533a), [`07e12ec`](https://github.com/Effect-TS/effect/commit/07e12ecdb0e20b9763bd9e9058e567a7c8862efc), [`3572646`](https://github.com/Effect-TS/effect/commit/3572646d5e0804f85bc7f64633fb95722533f9dd), [`1aed347`](https://github.com/Effect-TS/effect/commit/1aed347a125ed3847ec90863424810d6759cbc85), [`df4bf4b`](https://github.com/Effect-TS/effect/commit/df4bf4b62e7b316c6647da0271fc5544a84e7ba2), [`f085f92`](https://github.com/Effect-TS/effect/commit/f085f92dfa204afb41823ffc27d437225137643d), [`9b3b4ac`](https://github.com/Effect-TS/effect/commit/9b3b4ac639d98aae33883926bece1e31fa280d22)]: + - @effect/schema@0.67.23 + - @effect/platform@0.57.1 + - effect@3.3.2 + - @effect/platform-node@0.51.10 + +## 0.16.25 + +### Patch Changes + +- Updated dependencies [[`eb98c5b`](https://github.com/Effect-TS/effect/commit/eb98c5b79ab50aa0cde239bd4e660dd19dbab612), [`184fed8`](https://github.com/Effect-TS/effect/commit/184fed83ac36cba05a75a5a8013f740f9f696e3b), [`4d3fbe8`](https://github.com/Effect-TS/effect/commit/4d3fbe82e8cec13ccd0cd0b2096deac6818fb59a), [`6068e07`](https://github.com/Effect-TS/effect/commit/6068e073d4cc8b3c8583583fd5eb3efe43f7d5ba), [`3a77e20`](https://github.com/Effect-TS/effect/commit/3a77e209783933bac3aaddba1b05ff6a9ac72b36), [`d79ca17`](https://github.com/Effect-TS/effect/commit/d79ca17d9fa432571c69714776cab5cf8fef9c34)]: + - effect@3.3.1 + - @effect/platform@0.57.0 + - @effect/schema@0.67.22 + - @effect/platform-node@0.51.9 + +## 0.16.24 + +### Patch Changes + +- Updated dependencies [[`2b9ddfc`](https://github.com/Effect-TS/effect/commit/2b9ddfcbac505d98551e764a43923854907ca5c1), [`1f4ac00`](https://github.com/Effect-TS/effect/commit/1f4ac00a91c336c9c9c9b8c3ed9ceb9920ebc9bd), [`9305b76`](https://github.com/Effect-TS/effect/commit/9305b764cceeae4f16564435ae7172f79c2bf822), [`0f40d98`](https://github.com/Effect-TS/effect/commit/0f40d989da10f68df3ecd72b36849401ad679bfb), [`188f0a5`](https://github.com/Effect-TS/effect/commit/188f0a5c57ed0d7c9e5852e0c1c998f1b95810a1), [`b761ef0`](https://github.com/Effect-TS/effect/commit/b761ef00eaf6c67b7ffe34798b98aae5347ab376), [`b53f69b`](https://github.com/Effect-TS/effect/commit/b53f69bff1452a487b21198cd83961f844e02d36), [`0f40d98`](https://github.com/Effect-TS/effect/commit/0f40d989da10f68df3ecd72b36849401ad679bfb), [`5bd549e`](https://github.com/Effect-TS/effect/commit/5bd549e4bd7144727db438ecca6b8dc9b3ef7e22), [`67f160a`](https://github.com/Effect-TS/effect/commit/67f160a213de0219a565d4bf653b3cbf24f58e8f), [`67f160a`](https://github.com/Effect-TS/effect/commit/67f160a213de0219a565d4bf653b3cbf24f58e8f)]: + - @effect/platform@0.56.0 + - effect@3.3.0 + - @effect/schema@0.67.21 + - @effect/platform-node@0.51.8 + +## 0.16.23 + +### Patch Changes + +- Updated dependencies [[`4c6bc7f`](https://github.com/Effect-TS/effect/commit/4c6bc7f190c142dc9db70b365a2bf30715a98e62), [`a67d602`](https://github.com/Effect-TS/effect/commit/a67d60276f96cd20b76145b4cee13efca6c6158a)]: + - @effect/schema@0.67.20 + - @effect/platform@0.55.7 + - @effect/platform-node@0.51.7 + +## 0.16.22 + +### Patch Changes + +- Updated dependencies [[`8c5d280`](https://github.com/Effect-TS/effect/commit/8c5d280c0402284a4e58372867a15a431cb99461), [`6ba6d26`](https://github.com/Effect-TS/effect/commit/6ba6d269f5891e6b11aa35c5281dde4bf3273004), [`cd7496b`](https://github.com/Effect-TS/effect/commit/cd7496ba214eabac2e3c297f513fcbd5b11f0e91), [`3f28bf2`](https://github.com/Effect-TS/effect/commit/3f28bf274333611906175446b772243f34f1b6d5), [`5817820`](https://github.com/Effect-TS/effect/commit/58178204a770d1a78c06945ef438f9fffbb50afa), [`349a036`](https://github.com/Effect-TS/effect/commit/349a036ffb08351481c060655660a6ccf26473de), [`799aa20`](https://github.com/Effect-TS/effect/commit/799aa20b4f618736ba33a5297fda90a75d4c26c6)]: + - effect@3.2.9 + - @effect/schema@0.67.19 + - @effect/platform@0.55.6 + - @effect/platform-node@0.51.6 + +## 0.16.21 + +### Patch Changes + +- Updated dependencies [[`a0dd1c1`](https://github.com/Effect-TS/effect/commit/a0dd1c1ede2a1e856ecb0e67826ec992016fef97)]: + - @effect/schema@0.67.18 + - @effect/platform@0.55.5 + - @effect/platform-node@0.51.5 + +## 0.16.20 + +### Patch Changes + +- Updated dependencies [[`d9d22e7`](https://github.com/Effect-TS/effect/commit/d9d22e7c4d5e31d5b46644c729b027796e467c16), [`3c080f7`](https://github.com/Effect-TS/effect/commit/3c080f74b2e2290edb6143c3aa01026e57f87a2a), [`3c080f7`](https://github.com/Effect-TS/effect/commit/3c080f74b2e2290edb6143c3aa01026e57f87a2a), [`7d6d875`](https://github.com/Effect-TS/effect/commit/7d6d8750077d9c8379f37240745240d7f3b7a4f8), [`70cda70`](https://github.com/Effect-TS/effect/commit/70cda704e8e31c80737b95121c8199e726ea132f), [`fb91f17`](https://github.com/Effect-TS/effect/commit/fb91f17098b48497feca9ec976feb87e4a82451b)]: + - @effect/schema@0.67.17 + - effect@3.2.8 + - @effect/platform@0.55.4 + - @effect/platform-node@0.51.4 + +## 0.16.19 + +### Patch Changes + +- Updated dependencies [[`5745886`](https://github.com/Effect-TS/effect/commit/57458869859943410221ccc87f8cecfba7c79d92), [`6801fca`](https://github.com/Effect-TS/effect/commit/6801fca44366be3ee1b6b99f54bd4f38a1b5e4f4)]: + - @effect/schema@0.67.16 + - effect@3.2.7 + - @effect/platform@0.55.3 + - @effect/platform-node@0.51.3 + +## 0.16.18 + +### Patch Changes + +- Updated dependencies [[`2c2280b`](https://github.com/Effect-TS/effect/commit/2c2280b98a11fc002663c55792a4fa5781cd5fb6), [`e2740fc`](https://github.com/Effect-TS/effect/commit/e2740fc4e212ba85a90541e8c8d85b0bcd5c2e7c), [`cc8ac50`](https://github.com/Effect-TS/effect/commit/cc8ac5080daba8622ca2ff5dab5c37ddfab732ba), [`60fe3d5`](https://github.com/Effect-TS/effect/commit/60fe3d5fb2be168dd35c6d0cb8ac8f55deb30fc0)]: + - @effect/platform@0.55.2 + - @effect/schema@0.67.15 + - effect@3.2.6 + - @effect/platform-node@0.51.2 + +## 0.16.17 + +### Patch Changes + +- Updated dependencies [[`c5846e9`](https://github.com/Effect-TS/effect/commit/c5846e99137e9eb02efd31865e26f49f0d2c7c03)]: + - @effect/schema@0.67.14 + - @effect/platform-node@0.51.1 + - @effect/platform@0.55.1 + +## 0.16.16 + +### Patch Changes + +- Updated dependencies [[`608b01f`](https://github.com/Effect-TS/effect/commit/608b01fc342dbae2a642b308a67b84ead530ecea), [`031c712`](https://github.com/Effect-TS/effect/commit/031c7122a24ac42e48d6a434646b4f5d279d7442), [`a44e532`](https://github.com/Effect-TS/effect/commit/a44e532cf3a6a498b12a5aacf8124aa267e24ba0), [`5133ca9`](https://github.com/Effect-TS/effect/commit/5133ca9dc4b8da0e28951316da9ab55dfbe0fbb9)]: + - effect@3.2.5 + - @effect/platform@0.55.0 + - @effect/platform-node@0.51.0 + - @effect/schema@0.67.13 + +## 0.16.15 + +### Patch Changes + +- Updated dependencies [[`1af94df`](https://github.com/Effect-TS/effect/commit/1af94df6b74aeb4f6ebcbe80e074b4cb252e62e3), [`1af94df`](https://github.com/Effect-TS/effect/commit/1af94df6b74aeb4f6ebcbe80e074b4cb252e62e3), [`f8038ca`](https://github.com/Effect-TS/effect/commit/f8038cadd5f50d397469e5fdbc70dd8f69671f50), [`e376641`](https://github.com/Effect-TS/effect/commit/e3766411b60ebb45d31e9c9d94efa099121d4d58), [`c07e0ce`](https://github.com/Effect-TS/effect/commit/c07e0cea8ce165887e2c9dfa5d669eba9b2fb798), [`e313a01`](https://github.com/Effect-TS/effect/commit/e313a01b7e80f6cb7704055a190e5623c9d22c6d), [`1af94df`](https://github.com/Effect-TS/effect/commit/1af94df6b74aeb4f6ebcbe80e074b4cb252e62e3)]: + - effect@3.2.4 + - @effect/platform@0.54.0 + - @effect/platform-node@0.50.0 + - @effect/schema@0.67.12 + +## 0.16.14 + +### Patch Changes + +- [#2803](https://github.com/Effect-TS/effect/pull/2803) [`5af633e`](https://github.com/Effect-TS/effect/commit/5af633eb5ff6560a64d87263d1692bb9c75f7b3c) Thanks @tim-smart! - update dependencies + +- Updated dependencies [[`5af633e`](https://github.com/Effect-TS/effect/commit/5af633eb5ff6560a64d87263d1692bb9c75f7b3c), [`45578e8`](https://github.com/Effect-TS/effect/commit/45578e8faa80ae33d23e08f6f19467f818b7788f)]: + - @effect/platform-node@0.49.14 + - @effect/schema@0.67.11 + - effect@3.2.3 + - @effect/platform@0.53.14 + +## 0.16.13 + +### Patch Changes + +- Updated dependencies [[`7cc8020`](https://github.com/Effect-TS/effect/commit/7cc802018395804ae2fbce20f610bb7ff6081c00), [`5d9266e`](https://github.com/Effect-TS/effect/commit/5d9266e8c740746ac9e186c3df6090a1b57fbe2a), [`9f8122e`](https://github.com/Effect-TS/effect/commit/9f8122e78884ab47c5e5f364d86eee1d1543cc61), [`6a6f670`](https://github.com/Effect-TS/effect/commit/6a6f6706b8613c8c7c10971b8d81a0f9e440a6f2), [`c1eaef9`](https://github.com/Effect-TS/effect/commit/c1eaef910420dae416923d172ee58d219e921d0f), [`78ffc27`](https://github.com/Effect-TS/effect/commit/78ffc27ee3fa708433c25fa118c53d38d90d08bc)]: + - @effect/platform-node@0.49.13 + - effect@3.2.2 + - @effect/platform@0.53.13 + - @effect/schema@0.67.10 + +## 0.16.12 + +### Patch Changes + +- Updated dependencies [[`5432fff`](https://github.com/Effect-TS/effect/commit/5432fff7c9a69d43910426c1053ebfc3b73ebed6)]: + - @effect/schema@0.67.9 + - @effect/platform@0.53.12 + - @effect/platform-node@0.49.12 + +## 0.16.11 + +### Patch Changes + +- Updated dependencies [[`c1e991d`](https://github.com/Effect-TS/effect/commit/c1e991dd5ba87901cd0e05697a8b4a267e7e954a)]: + - effect@3.2.1 + - @effect/platform@0.53.11 + - @effect/platform-node@0.49.11 + - @effect/schema@0.67.8 + +## 0.16.10 + +### Patch Changes + +- [#2778](https://github.com/Effect-TS/effect/pull/2778) [`7135748`](https://github.com/Effect-TS/effect/commit/713574813a0f64085db0b5240ba39e7a0a7c137e) Thanks [@tim-smart](https://github.com/tim-smart)! - capture stack trace for tracing spans + +- Updated dependencies [[`146cadd`](https://github.com/Effect-TS/effect/commit/146cadd9d004634a3ff85c480bf92cf975c853e2), [`7135748`](https://github.com/Effect-TS/effect/commit/713574813a0f64085db0b5240ba39e7a0a7c137e), [`7135748`](https://github.com/Effect-TS/effect/commit/713574813a0f64085db0b5240ba39e7a0a7c137e), [`963b4e7`](https://github.com/Effect-TS/effect/commit/963b4e7ac87e2468feb6a344f7ab4ee4ad711198), [`64c9414`](https://github.com/Effect-TS/effect/commit/64c9414e960e82058ca09bbb3976d6fbef303a8e), [`7135748`](https://github.com/Effect-TS/effect/commit/713574813a0f64085db0b5240ba39e7a0a7c137e), [`2cbb76b`](https://github.com/Effect-TS/effect/commit/2cbb76bb52500a3f4bf27d1c91482518cbea56d7), [`870c5fa`](https://github.com/Effect-TS/effect/commit/870c5fa52cd61e745e8e828d38c3f09f00737553), [`146cadd`](https://github.com/Effect-TS/effect/commit/146cadd9d004634a3ff85c480bf92cf975c853e2), [`7135748`](https://github.com/Effect-TS/effect/commit/713574813a0f64085db0b5240ba39e7a0a7c137e), [`64c9414`](https://github.com/Effect-TS/effect/commit/64c9414e960e82058ca09bbb3976d6fbef303a8e)]: + - effect@3.2.0 + - @effect/platform@0.53.10 + - @effect/platform-node@0.49.10 + - @effect/schema@0.67.7 + +## 0.16.9 + +### Patch Changes + +- Updated dependencies [[`17da864`](https://github.com/Effect-TS/effect/commit/17da864e4a6f80becdb82db7dece2ba583bfdda3), [`17fc22e`](https://github.com/Effect-TS/effect/commit/17fc22e132593c5caa563705a4748ba0f04a853c), [`810f222`](https://github.com/Effect-TS/effect/commit/810f222268792b13067c7a7bf317b93a9bb8917b), [`596aaea`](https://github.com/Effect-TS/effect/commit/596aaea022648b2e06fb1ec22f1652043d6fe64e), [`ff0efa0`](https://github.com/Effect-TS/effect/commit/ff0efa0a1415a41d4a4312a16cf7a63def86db3f)]: + - @effect/schema@0.67.6 + - @effect/platform@0.53.9 + - effect@3.1.6 + - @effect/platform-node@0.49.9 + +## 0.16.8 + +### Patch Changes + +- Updated dependencies [[`9c514de`](https://github.com/Effect-TS/effect/commit/9c514de28152696edff008324d2d7e67d55afd56)]: + - @effect/schema@0.67.5 + - @effect/platform@0.53.8 + - @effect/platform-node@0.49.8 + +## 0.16.7 + +### Patch Changes + +- Updated dependencies [[`ee08593`](https://github.com/Effect-TS/effect/commit/ee0859398ecc2589cab0d017bef6a17e00c34dfd), [`da6d7d8`](https://github.com/Effect-TS/effect/commit/da6d7d845246e9d04631d64fa7694944b6010d09)]: + - @effect/schema@0.67.4 + - @effect/platform@0.53.7 + - @effect/platform-node@0.49.7 + +## 0.16.6 + +### Patch Changes + +- [#2750](https://github.com/Effect-TS/effect/pull/2750) [`6ac4847`](https://github.com/Effect-TS/effect/commit/6ac48479447c01a4f35d655552af93e47e562610) Thanks [@tim-smart](https://github.com/tim-smart)! - fix memory leak in Socket's + +- Updated dependencies [[`6ac4847`](https://github.com/Effect-TS/effect/commit/6ac48479447c01a4f35d655552af93e47e562610), [`6ac4847`](https://github.com/Effect-TS/effect/commit/6ac48479447c01a4f35d655552af93e47e562610)]: + - @effect/platform@0.53.6 + - effect@3.1.5 + - @effect/platform-node@0.49.6 + - @effect/schema@0.67.3 + +## 0.16.5 + +### Patch Changes + +- Updated dependencies [[`89a3afb`](https://github.com/Effect-TS/effect/commit/89a3afbe191c83b84b17bfaa95519aff0749afbe), [`992c8e2`](https://github.com/Effect-TS/effect/commit/992c8e21535db9f0c66e81d32fee8af56a96274f)]: + - @effect/schema@0.67.2 + - @effect/platform@0.53.5 + - @effect/platform-node@0.49.5 + +## 0.16.4 + +### Patch Changes + +- Updated dependencies [[`e41e911`](https://github.com/Effect-TS/effect/commit/e41e91122fa6dd12fc81e50dcad0db891be67146)]: + - effect@3.1.4 + - @effect/platform@0.53.4 + - @effect/platform-node@0.49.4 + - @effect/schema@0.67.1 + +## 0.16.3 + +### Patch Changes + +- Updated dependencies [[`d7e4997`](https://github.com/Effect-TS/effect/commit/d7e49971fe97b7ee5fb7991f3f5ac4d627a26338)]: + - @effect/schema@0.67.0 + - @effect/platform@0.53.3 + - @effect/platform-node@0.49.3 + +## 0.16.2 + +### Patch Changes + +- Updated dependencies [[`1f6dc96`](https://github.com/Effect-TS/effect/commit/1f6dc96f51c7bb9c8d11415358308604ba7c7c8e), [`1f6dc96`](https://github.com/Effect-TS/effect/commit/1f6dc96f51c7bb9c8d11415358308604ba7c7c8e)]: + - @effect/platform-node@0.49.2 + - effect@3.1.3 + - @effect/platform@0.53.2 + - @effect/schema@0.66.16 + +## 0.16.1 + +### Patch Changes + +- Updated dependencies [[`121d6d9`](https://github.com/Effect-TS/effect/commit/121d6d93755138c7510ba3ab4f0019ec0cb91890)]: + - @effect/schema@0.66.15 + - @effect/platform@0.53.1 + - @effect/platform-node@0.49.1 + +## 0.16.0 + +### Minor Changes + +- [#2703](https://github.com/Effect-TS/effect/pull/2703) [`d57fbbb`](https://github.com/Effect-TS/effect/commit/d57fbbbd6c466936213a671fc3cd2390064f864e) Thanks [@tim-smart](https://github.com/tim-smart)! - replace isows with WebSocketConstructor service in @effect/platform/Socket + + You now have to provide a WebSocketConstructor implementation to the `Socket.makeWebSocket` api. + + ```ts + import * as Socket from "@effect/platform/Socket" + import * as NodeSocket from "@effect/platform-node/NodeSocket" + import { Effect } from "effect" + + Socket.makeWebSocket("ws://localhost:8080").pipe( + Effect.provide(NodeSocket.layerWebSocketConstructor) // use "ws" npm package + ) + ``` + +### Patch Changes + +- Updated dependencies [[`d57fbbb`](https://github.com/Effect-TS/effect/commit/d57fbbbd6c466936213a671fc3cd2390064f864e)]: + - @effect/platform-node@0.49.0 + - @effect/platform@0.53.0 + +## 0.15.18 + +### Patch Changes + +- Updated dependencies [[`5866c62`](https://github.com/Effect-TS/effect/commit/5866c621d7eb4cc84e4ba972bfdfd219734cd45d)]: + - @effect/platform@0.52.3 + - @effect/platform-node@0.48.4 + +## 0.15.17 + +### Patch Changes + +- [#2679](https://github.com/Effect-TS/effect/pull/2679) [`2e1cdf6`](https://github.com/Effect-TS/effect/commit/2e1cdf67d141281288fffe9a5c10d1379a800513) Thanks [@tim-smart](https://github.com/tim-smart)! - ensure all type ids are annotated with `unique symbol` + +- Updated dependencies [[`e4b82d2`](https://github.com/Effect-TS/effect/commit/e4b82d239bf0974173b5d687b45d1b1899be615f), [`2e1cdf6`](https://github.com/Effect-TS/effect/commit/2e1cdf67d141281288fffe9a5c10d1379a800513)]: + - @effect/platform-node@0.48.3 + - @effect/platform@0.52.2 + - effect@3.1.2 + - @effect/schema@0.66.14 + +## 0.15.16 + +### Patch Changes + +- Updated dependencies [[`e5e56d1`](https://github.com/Effect-TS/effect/commit/e5e56d138dbed3204636f605229c6685f89659fc)]: + - effect@3.1.1 + - @effect/platform@0.52.1 + - @effect/platform-node@0.48.2 + - @effect/schema@0.66.13 + +## 0.15.15 + +### Patch Changes + +- Updated dependencies [[`9deab0a`](https://github.com/Effect-TS/effect/commit/9deab0aec9e99501f9441843e34df9afa10c5be9), [`7719b8a`](https://github.com/Effect-TS/effect/commit/7719b8a7350c14e952ffe685bfd5308773b3e271)]: + - @effect/platform@0.52.0 + - @effect/platform-node@0.48.1 + +## 0.15.14 + +### Patch Changes + +- [#2543](https://github.com/Effect-TS/effect/pull/2543) [`a023f28`](https://github.com/Effect-TS/effect/commit/a023f28336f3865687d9a30c1883e36909906d85) Thanks [@github-actions](https://github.com/apps/github-actions)! - set span `kind` where applicable + +- Updated dependencies [[`c3c12c6`](https://github.com/Effect-TS/effect/commit/c3c12c6625633fe80e79f9db75a3b8cf8ca8b11d), [`ba64ea6`](https://github.com/Effect-TS/effect/commit/ba64ea6757810c5e74cad3863a7d19d4d38af66b), [`b5de2d2`](https://github.com/Effect-TS/effect/commit/b5de2d2ce5b1afe8be90827bf898a95cec40eb2b), [`a1c7ab8`](https://github.com/Effect-TS/effect/commit/a1c7ab8ffedacd18c1fc784f4ff5844f79498b83), [`a023f28`](https://github.com/Effect-TS/effect/commit/a023f28336f3865687d9a30c1883e36909906d85), [`0ec93cb`](https://github.com/Effect-TS/effect/commit/0ec93cb4f166e7401c171c2f8e8276ce958d9a57), [`1c9454d`](https://github.com/Effect-TS/effect/commit/1c9454d532eae79b9f759aea77f59332cc6d18ed), [`a023f28`](https://github.com/Effect-TS/effect/commit/a023f28336f3865687d9a30c1883e36909906d85), [`92d56db`](https://github.com/Effect-TS/effect/commit/92d56dbb3f33e36636c2a2f1030c56492e39cf4d)]: + - effect@3.1.0 + - @effect/platform-node@0.48.0 + - @effect/platform@0.51.0 + - @effect/schema@0.66.12 + +## 0.15.13 + +### Patch Changes + +- [#2656](https://github.com/Effect-TS/effect/pull/2656) [`557707b`](https://github.com/Effect-TS/effect/commit/557707bc9e5f230c8964d2757012075c34339b5c) Thanks [@tim-smart](https://github.com/tim-smart)! - update dependencies + +- Updated dependencies [[`16039a0`](https://github.com/Effect-TS/effect/commit/16039a08f04f11545e2fdf40952788a8f9cef04f), [`557707b`](https://github.com/Effect-TS/effect/commit/557707bc9e5f230c8964d2757012075c34339b5c), [`d1d33e1`](https://github.com/Effect-TS/effect/commit/d1d33e10b25109f44b5ab1c6e4d778a59c0d3eeb), [`f4ed306`](https://github.com/Effect-TS/effect/commit/f4ed3068a70b50302d078a30d18ca3cfd2bc679c), [`661004f`](https://github.com/Effect-TS/effect/commit/661004f4bf5f8b25f5a0678c21a3a822188ce461), [`e79cb83`](https://github.com/Effect-TS/effect/commit/e79cb83d3b19098bc40a3012e2a059b8426306c2)]: + - @effect/platform@0.50.8 + - @effect/platform-node@0.47.8 + - effect@3.0.8 + - @effect/schema@0.66.11 + +## 0.15.12 + +### Patch Changes + +- Updated dependencies [[`18de56b`](https://github.com/Effect-TS/effect/commit/18de56b4a6b6d1f99230dfabf9147d59ea4dd759)]: + - effect@3.0.7 + - @effect/platform@0.50.7 + - @effect/platform-node@0.47.7 + - @effect/schema@0.66.10 + +## 0.15.11 + +### Patch Changes + +- [#2626](https://github.com/Effect-TS/effect/pull/2626) [`027418e`](https://github.com/Effect-TS/effect/commit/027418edaa6aa6c0ae4861b95832827b45adace4) Thanks [@fubhy](https://github.com/fubhy)! - Reintroduce custom `NoInfer` type + +- [#2609](https://github.com/Effect-TS/effect/pull/2609) [`ac1898e`](https://github.com/Effect-TS/effect/commit/ac1898eb7bc96880f911c276048e2ea3d6fe9c50) Thanks [@patroza](https://github.com/patroza)! - change: BatchedRequestResolver works with NonEmptyArray + +- Updated dependencies [[`ffe4f4e`](https://github.com/Effect-TS/effect/commit/ffe4f4e95db35fff6869e360b072e3837befa0a1), [`027418e`](https://github.com/Effect-TS/effect/commit/027418edaa6aa6c0ae4861b95832827b45adace4), [`ac1898e`](https://github.com/Effect-TS/effect/commit/ac1898eb7bc96880f911c276048e2ea3d6fe9c50), [`ffe4f4e`](https://github.com/Effect-TS/effect/commit/ffe4f4e95db35fff6869e360b072e3837befa0a1), [`8206529`](https://github.com/Effect-TS/effect/commit/8206529d6a7bbf3e3c6f670afb0381e83176736e)]: + - effect@3.0.6 + - @effect/schema@0.66.9 + - @effect/platform@0.50.6 + - @effect/platform-node@0.47.6 + +## 0.15.10 + +### Patch Changes + +- Updated dependencies [[`6222404`](https://github.com/Effect-TS/effect/commit/62224044678751829ed2f128e05133a91c6b0569), [`868ed2a`](https://github.com/Effect-TS/effect/commit/868ed2a8fe94ee7f4206a6070f29dcf2a5ba1dc3)]: + - effect@3.0.5 + - @effect/platform@0.50.5 + - @effect/platform-node@0.47.5 + - @effect/schema@0.66.8 + +## 0.15.9 + +### Patch Changes + +- Updated dependencies [[`dd41c6c`](https://github.com/Effect-TS/effect/commit/dd41c6c725b1c1c980683275d8fa69779902187e), [`9a24667`](https://github.com/Effect-TS/effect/commit/9a246672008a2b668d43fbfd2fe5508c54b2b920)]: + - @effect/schema@0.66.7 + - effect@3.0.4 + - @effect/platform@0.50.4 + - @effect/platform-node@0.47.4 + +## 0.15.8 + +### Patch Changes + +- Updated dependencies [[`9dfc156`](https://github.com/Effect-TS/effect/commit/9dfc156dc13fb4da9c777aae3acece4b5ecf0064), [`80271bd`](https://github.com/Effect-TS/effect/commit/80271bdc648e9efa659ce66b2c255754a6a1a8b0), [`b3b51a2`](https://github.com/Effect-TS/effect/commit/b3b51a2ea0c6ab92a363db46ebaa7e1176d089f5), [`e4ba97d`](https://github.com/Effect-TS/effect/commit/e4ba97d060c16bdf4e3b5bd5db6777f121a6768c)]: + - @effect/schema@0.66.6 + - @effect/platform@0.50.3 + - @effect/platform-node@0.47.3 + +## 0.15.7 + +### Patch Changes + +- Updated dependencies [[`b3fe829`](https://github.com/Effect-TS/effect/commit/b3fe829e8b12726afe94086b5375968f41a26411), [`a58b7de`](https://github.com/Effect-TS/effect/commit/a58b7deb8bb1d3b0dd636decf5d16f115f37eb72), [`d90e8c3`](https://github.com/Effect-TS/effect/commit/d90e8c3090cbc78e2bc7b51c974df66ffefacdfa)]: + - @effect/schema@0.66.5 + - @effect/platform@0.50.2 + - @effect/platform-node@0.47.2 + +## 0.15.6 + +### Patch Changes + +- Updated dependencies [[`773b8e0`](https://github.com/Effect-TS/effect/commit/773b8e01521e8fa7c38ff15d92d21d6fd6dad56f)]: + - @effect/schema@0.66.4 + - @effect/platform@0.50.1 + - @effect/platform-node@0.47.1 + +## 0.15.5 + +### Patch Changes + +- Updated dependencies [[`6f38dff`](https://github.com/Effect-TS/effect/commit/6f38dff41ffa34532cc2f25b90446550c5730bb6), [`a7b4b84`](https://github.com/Effect-TS/effect/commit/a7b4b84bd5a25f51aba922f9259c3a58c98c6a4e), [`a3b0e6c`](https://github.com/Effect-TS/effect/commit/a3b0e6c490772e6d44b5d98dcf2729c4d5310ecc), [`6f38dff`](https://github.com/Effect-TS/effect/commit/6f38dff41ffa34532cc2f25b90446550c5730bb6)]: + - @effect/platform-node@0.47.0 + - @effect/platform@0.50.0 + - effect@3.0.3 + - @effect/schema@0.66.3 + +## 0.15.4 + +### Patch Changes + +- [#2562](https://github.com/Effect-TS/effect/pull/2562) [`2cecdbd`](https://github.com/Effect-TS/effect/commit/2cecdbd1cf30befce4e84796ccd953ea55ecfb86) Thanks [@fubhy](https://github.com/fubhy)! - Added provenance publishing + +- Updated dependencies [[`2cecdbd`](https://github.com/Effect-TS/effect/commit/2cecdbd1cf30befce4e84796ccd953ea55ecfb86)]: + - @effect/platform-node@0.46.4 + - @effect/platform@0.49.4 + - effect@3.0.2 + - @effect/schema@0.66.2 + +## 0.15.3 + +### Patch Changes + +- Updated dependencies [[`8d39d65`](https://github.com/Effect-TS/effect/commit/8d39d6554af548228ad767112ce2e0b1f68fa8e1)]: + - @effect/platform@0.49.3 + - @effect/platform-node@0.46.3 + +## 0.15.2 + +### Patch Changes + +- Updated dependencies [[`5ef0a1a`](https://github.com/Effect-TS/effect/commit/5ef0a1ae9b773fa2481550cb0d43ff7a0e03cd44)]: + - @effect/platform@0.49.2 + - @effect/platform-node@0.46.2 + +## 0.15.1 + +### Patch Changes + +- [#2555](https://github.com/Effect-TS/effect/pull/2555) [`8edacca`](https://github.com/Effect-TS/effect/commit/8edacca37f8e37c01a63fec332b06d9361efaa7b) Thanks [@tim-smart](https://github.com/tim-smart)! - prevent use of `Array` as import name to solve bundler issues + +- Updated dependencies [[`3da0cfa`](https://github.com/Effect-TS/effect/commit/3da0cfa12c407fd930dc480be1ecc9217a8058f8), [`570e8d8`](https://github.com/Effect-TS/effect/commit/570e8d87e7c0e9ad4cd2686462fdb9b4812f7716), [`b2b5d66`](https://github.com/Effect-TS/effect/commit/b2b5d6626b18eb5289f364ffab5240e84b04d085), [`87c5687`](https://github.com/Effect-TS/effect/commit/87c5687de0782dab177b7861217fa3b040046282), [`8edacca`](https://github.com/Effect-TS/effect/commit/8edacca37f8e37c01a63fec332b06d9361efaa7b)]: + - effect@3.0.1 + - @effect/schema@0.66.1 + - @effect/platform@0.49.1 + - @effect/platform-node@0.46.1 + +## 0.15.0 + +### Minor Changes + +- [#2207](https://github.com/Effect-TS/effect/pull/2207) [`5a2314b`](https://github.com/Effect-TS/effect/commit/5a2314b70ec79c2c02b51cef45a5ddec8327daa1) Thanks [@github-actions](https://github.com/apps/github-actions)! - replace use of `unit` terminology with `void` + + For all the data types. + + ```ts + Effect.unit // => Effect.void + Stream.unit // => Stream.void + + // etc + ``` + +- [#2207](https://github.com/Effect-TS/effect/pull/2207) [`2fb7d9c`](https://github.com/Effect-TS/effect/commit/2fb7d9ca15037ff62a578bb9fe5732da5f4f317d) Thanks [@github-actions](https://github.com/apps/github-actions)! - Release Effect 3.0 🎉 + +### Patch Changes + +- [#2529](https://github.com/Effect-TS/effect/pull/2529) [`78b767c`](https://github.com/Effect-TS/effect/commit/78b767c2b1625186e17131761a0edbac25d21850) Thanks [@fubhy](https://github.com/fubhy)! - Renamed `ReadonlyArray` and `ReadonlyRecord` modules for better discoverability. + +- Updated dependencies [[`1b5f0c7`](https://github.com/Effect-TS/effect/commit/1b5f0c77e7fd477a0026071e82129a948227f4b3), [`d50a652`](https://github.com/Effect-TS/effect/commit/d50a652479f4d1d64f48da05c79fa847e6e51548), [`9aeae46`](https://github.com/Effect-TS/effect/commit/9aeae461fdf9265389cf3dfe4e428b037215ba5f), [`9a3bd47`](https://github.com/Effect-TS/effect/commit/9a3bd47ebd0750c7e498162734f6d21895de0cb2), [`6c6087a`](https://github.com/Effect-TS/effect/commit/6c6087a4a897b64252346426660782d31c13f769), [`6460414`](https://github.com/Effect-TS/effect/commit/6460414351a45fb8e0a457c63f3653422efee766), [`cf69f46`](https://github.com/Effect-TS/effect/commit/cf69f46690058d71eeada03cfb40dc744573e9e4), [`cf69f46`](https://github.com/Effect-TS/effect/commit/cf69f46690058d71eeada03cfb40dc744573e9e4), [`e542371`](https://github.com/Effect-TS/effect/commit/e542371981f8b4b484979feaad8a25b1f45e2df0), [`be9d025`](https://github.com/Effect-TS/effect/commit/be9d025e42355260ace02dd135851a8935a4deba), [`78b767c`](https://github.com/Effect-TS/effect/commit/78b767c2b1625186e17131761a0edbac25d21850), [`aa4a3b5`](https://github.com/Effect-TS/effect/commit/aa4a3b550da1c1020265ac389ed3f309388994a2), [`1499974`](https://github.com/Effect-TS/effect/commit/14999741d2e19c1747f6a7e19d68977f6429cdb8), [`1b5f0c7`](https://github.com/Effect-TS/effect/commit/1b5f0c77e7fd477a0026071e82129a948227f4b3), [`5c2b561`](https://github.com/Effect-TS/effect/commit/5c2b5614f583b88784ed68126ae939832fb3c092), [`6c6087a`](https://github.com/Effect-TS/effect/commit/6c6087a4a897b64252346426660782d31c13f769), [`a18f594`](https://github.com/Effect-TS/effect/commit/a18f5948f1439a147232448b2c443472fda0eceb), [`1499974`](https://github.com/Effect-TS/effect/commit/14999741d2e19c1747f6a7e19d68977f6429cdb8), [`2f96d93`](https://github.com/Effect-TS/effect/commit/2f96d938b90f8c19377583279e3c7afd9b509c50), [`5a2314b`](https://github.com/Effect-TS/effect/commit/5a2314b70ec79c2c02b51cef45a5ddec8327daa1), [`25d74f8`](https://github.com/Effect-TS/effect/commit/25d74f8c4d2dd4a9e5ec57ce2f20d36dedd25343), [`6c6087a`](https://github.com/Effect-TS/effect/commit/6c6087a4a897b64252346426660782d31c13f769), [`271b79f`](https://github.com/Effect-TS/effect/commit/271b79fc0b66a6c11e07a8779ff8800493a7eac2), [`1b5f0c7`](https://github.com/Effect-TS/effect/commit/1b5f0c77e7fd477a0026071e82129a948227f4b3), [`2fb7d9c`](https://github.com/Effect-TS/effect/commit/2fb7d9ca15037ff62a578bb9fe5732da5f4f317d), [`53d1c2a`](https://github.com/Effect-TS/effect/commit/53d1c2a77559081fbb89667e343346375c6d6650), [`e7e1bbe`](https://github.com/Effect-TS/effect/commit/e7e1bbe68486fdf31c8f84b0880522d39adcaad3), [`10c169e`](https://github.com/Effect-TS/effect/commit/10c169eadc874e91b4defca3f467b4e6a50fd8f3), [`6424181`](https://github.com/Effect-TS/effect/commit/64241815fe6a939e91e6947253e7dceea1306aa8)]: + - effect@3.0.0 + - @effect/schema@0.66.0 + - @effect/platform-node@0.46.0 + - @effect/platform@0.49.0 + +## 0.14.14 + +### Patch Changes + +- Updated dependencies [[`b79cc59`](https://github.com/Effect-TS/effect/commit/b79cc59dbe64b9a0a7742dc9100a9d36c8e46b72)]: + - @effect/platform-node@0.45.31 + - @effect/platform@0.48.29 + +## 0.14.13 + +### Patch Changes + +- [#2472](https://github.com/Effect-TS/effect/pull/2472) [`8709856`](https://github.com/Effect-TS/effect/commit/870985694ae985c3cb9360ad8a25c60e6f785f55) Thanks [@tim-smart](https://github.com/tim-smart)! - add Subscribable trait / module + + Subscribable represents a resource that has a current value and can be subscribed to for updates. + + The following data types are subscribable: + - A `SubscriptionRef` + - An `Actor` from the experimental `Machine` module + +- [#2472](https://github.com/Effect-TS/effect/pull/2472) [`8709856`](https://github.com/Effect-TS/effect/commit/870985694ae985c3cb9360ad8a25c60e6f785f55) Thanks [@tim-smart](https://github.com/tim-smart)! - add Readable module / trait + + `Readable` is a common interface for objects that can be read from using a `get` + Effect. + + For example, `Ref`'s implement `Readable`: + + ```ts + import { Effect, Readable, Ref } from "effect" + import assert from "assert" + + Effect.gen(function* (_) { + const ref = yield* _(Ref.make(123)) + assert(Readable.isReadable(ref)) + + const result = yield* _(ref.get) + assert(result === 123) + }) + ``` + +- Updated dependencies [[`0aee906`](https://github.com/Effect-TS/effect/commit/0aee906f034539344db6fbac08919de3e28eccde), [`41c8102`](https://github.com/Effect-TS/effect/commit/41c810228b1a50e4b41f19e735d7c62fe8d36871), [`4c37001`](https://github.com/Effect-TS/effect/commit/4c370013417e18c4f564818de1341a8fccb43b4c), [`776ef2b`](https://github.com/Effect-TS/effect/commit/776ef2bb66db9aa9f68b7beab14f6986f9c1288b), [`217147e`](https://github.com/Effect-TS/effect/commit/217147ea67c5c42c96f024775c41e5b070f81e4c), [`8a69b4e`](https://github.com/Effect-TS/effect/commit/8a69b4ef6a3a06d2e21fe2e11a626038beefb4e1), [`90776ec`](https://github.com/Effect-TS/effect/commit/90776ec8e8671d835b65fc33ead1de6c864b81b9), [`b3acf47`](https://github.com/Effect-TS/effect/commit/b3acf47f9c9dfae1c99377aa906097aaa2d47d44), [`8709856`](https://github.com/Effect-TS/effect/commit/870985694ae985c3cb9360ad8a25c60e6f785f55), [`232c353`](https://github.com/Effect-TS/effect/commit/232c353c2e6f743f38e57639ee30e324ffa9c2a9), [`0d3231a`](https://github.com/Effect-TS/effect/commit/0d3231a195202635ecc0bf6bbf6a08fc017d0d69), [`0ca835c`](https://github.com/Effect-TS/effect/commit/0ca835cbac8e69072a93ace83b534219faba24e8), [`8709856`](https://github.com/Effect-TS/effect/commit/870985694ae985c3cb9360ad8a25c60e6f785f55), [`da22adc`](https://github.com/Effect-TS/effect/commit/da22adc6507563876f1c416fd22a5f9206cc1395), [`d590094`](https://github.com/Effect-TS/effect/commit/d5900943489ec1e0891836aeafb5ce99fb9c75c7), [`c22b019`](https://github.com/Effect-TS/effect/commit/c22b019e5eaf9d3a937a3d99cadbb8f8e9116a70), [`e983740`](https://github.com/Effect-TS/effect/commit/e9837401145605aff5bc2ec7e73004f397c5d2d1), [`e3e0924`](https://github.com/Effect-TS/effect/commit/e3e09247d46a35430fc60e4aa4032cc50814f212)]: + - @effect/schema@0.65.0 + - effect@2.4.19 + - @effect/platform-node@0.45.30 + - @effect/platform@0.48.28 + +## 0.14.12 + +### Patch Changes + +- Updated dependencies [[`c6dd3c6`](https://github.com/Effect-TS/effect/commit/c6dd3c6909cafe05adc8450c5a499260e17e60d3), [`672f137`](https://github.com/Effect-TS/effect/commit/672f13747ddf6dac3ba304fd4511b1df44ab566d), [`42b3651`](https://github.com/Effect-TS/effect/commit/42b36519f356bae9258a1ea1d416e2902b973e85), [`672f137`](https://github.com/Effect-TS/effect/commit/672f13747ddf6dac3ba304fd4511b1df44ab566d)]: + - @effect/platform@0.48.27 + - @effect/schema@0.64.20 + - @effect/platform-node@0.45.29 + +## 0.14.11 + +### Patch Changes + +- Updated dependencies [[`365a486`](https://github.com/Effect-TS/effect/commit/365a4865de5e47ce09f4cfd51fc0f67438f82a57)]: + - @effect/platform@0.48.26 + - @effect/platform-node@0.45.28 + +## 0.14.10 + +### Patch Changes + +- Updated dependencies [[`dadc690`](https://github.com/Effect-TS/effect/commit/dadc6906121c512bc32be22b52adbd1ada834594), [`58f66fe`](https://github.com/Effect-TS/effect/commit/58f66fecd4e646c6c8f10995df9faab17022eb8f), [`3cad21d`](https://github.com/Effect-TS/effect/commit/3cad21daa5d2332d33692498c87b7ffff979e304), [`d209171`](https://github.com/Effect-TS/effect/commit/d2091714a786820ebae4bef04a9d67d25dd08e88)]: + - effect@2.4.18 + - @effect/schema@0.64.19 + - @effect/platform@0.48.25 + - @effect/platform-node@0.45.27 + +## 0.14.9 + +### Patch Changes + +- Updated dependencies [[`9c6a500`](https://github.com/Effect-TS/effect/commit/9c6a5001b467b6255c68a922f4b6e8d692b63d01), [`8fdfda6`](https://github.com/Effect-TS/effect/commit/8fdfda6618be848c01b399d13bc05a9a3adfb613), [`607b2e7`](https://github.com/Effect-TS/effect/commit/607b2e7a7fd9318c57acf4e50ec61747eea74ad7), [`8fdfda6`](https://github.com/Effect-TS/effect/commit/8fdfda6618be848c01b399d13bc05a9a3adfb613), [`35ad0ba`](https://github.com/Effect-TS/effect/commit/35ad0ba9f3ba27c60453620e514b980f819f92af), [`8206caf`](https://github.com/Effect-TS/effect/commit/8206caf7c2d22c68be4313318b61cfdacf6222b6), [`7ddd654`](https://github.com/Effect-TS/effect/commit/7ddd65415b65ccb654ad04f4dbefe39402f15117), [`7ddd654`](https://github.com/Effect-TS/effect/commit/7ddd65415b65ccb654ad04f4dbefe39402f15117), [`8fdfda6`](https://github.com/Effect-TS/effect/commit/8fdfda6618be848c01b399d13bc05a9a3adfb613), [`f456ba2`](https://github.com/Effect-TS/effect/commit/f456ba273bae21a6dcf8c966c50c97b5f0897d9f)]: + - @effect/platform@0.48.24 + - effect@2.4.17 + - @effect/platform-node@0.45.26 + - @effect/schema@0.64.18 + +## 0.14.8 + +### Patch Changes + +- Updated dependencies [[`5170ce7`](https://github.com/Effect-TS/effect/commit/5170ce708c606283e8a30d273950f1a21c7eddc2), [`63a1df2`](https://github.com/Effect-TS/effect/commit/63a1df2e4de3766f48f15676fbd0360ab9c27816), [`5170ce7`](https://github.com/Effect-TS/effect/commit/5170ce708c606283e8a30d273950f1a21c7eddc2), [`62a7f23`](https://github.com/Effect-TS/effect/commit/62a7f23937c0dfaca67a7b2f055b85cfde25ed11), [`7cc2b41`](https://github.com/Effect-TS/effect/commit/7cc2b41d6c551fdca2590b06681c5ad9832aba46), [`74a5dae`](https://github.com/Effect-TS/effect/commit/74a5daed0e65b32a36e026bfcf66d02269cb967a), [`8b46fde`](https://github.com/Effect-TS/effect/commit/8b46fdebf2c075a74cd2cd29dfb69531d20fc154)]: + - @effect/platform@0.48.23 + - effect@2.4.16 + - @effect/schema@0.64.17 + - @effect/platform-node@0.45.25 + +## 0.14.7 + +### Patch Changes + +- Updated dependencies [[`a31917a`](https://github.com/Effect-TS/effect/commit/a31917aa4b05b1189b7a8e0bedb60bb3d49262ad), [`4cd2bed`](https://github.com/Effect-TS/effect/commit/4cd2bedf978f864bddd289d1c524c8e868bf587b), [`6cc6267`](https://github.com/Effect-TS/effect/commit/6cc6267026d9bfb1a9882cddf534787327e86ec1)]: + - @effect/schema@0.64.16 + - @effect/platform@0.48.22 + - @effect/platform-node@0.45.24 + +## 0.14.6 + +### Patch Changes + +- [#2421](https://github.com/Effect-TS/effect/pull/2421) [`c34eb3e`](https://github.com/Effect-TS/effect/commit/c34eb3ecae1bc858bd17207b5c82935acc7a95d9) Thanks [@tim-smart](https://github.com/tim-smart)! - don't persist items with a TimeToLive of zero + +- Updated dependencies [[`d7688c0`](https://github.com/Effect-TS/effect/commit/d7688c0c72717fe7876c871567f6946dabfc0546), [`b3a4fac`](https://github.com/Effect-TS/effect/commit/b3a4face2acaca422f0b0530436e8f13129f3b3a), [`5ded019`](https://github.com/Effect-TS/effect/commit/5ded019970169e3c1f2a375d0876b95fb1ff67f5)]: + - effect@2.4.15 + - @effect/schema@0.64.15 + - @effect/platform@0.48.21 + - @effect/platform-node@0.45.23 + +## 0.14.5 + +### Patch Changes + +- Updated dependencies [[`4789083`](https://github.com/Effect-TS/effect/commit/4789083283bdaec456982d614ebc4a496ea0e7f7), [`4789083`](https://github.com/Effect-TS/effect/commit/4789083283bdaec456982d614ebc4a496ea0e7f7)]: + - @effect/platform-node@0.45.22 + - @effect/platform@0.48.20 + +## 0.14.4 + +### Patch Changes + +- Updated dependencies [[`fb7285e`](https://github.com/Effect-TS/effect/commit/fb7285e8d6a70527df7137a6a3efdd03ae61cb8b)]: + - @effect/platform-node@0.45.21 + - @effect/platform@0.48.19 + +## 0.14.3 + +### Patch Changes + +- Updated dependencies [[`26435ec`](https://github.com/Effect-TS/effect/commit/26435ecfa06569dc18d1801ccf38213a43b7c334), [`a76e5e1`](https://github.com/Effect-TS/effect/commit/a76e5e131a35c88a72771fb745df08f60fbc0e18), [`6180c0c`](https://github.com/Effect-TS/effect/commit/6180c0cc51dee785cfce72220a52c9fc3b9bf9aa)]: + - @effect/platform-node@0.45.20 + - @effect/platform@0.48.18 + - @effect/schema@0.64.14 + - effect@2.4.14 + +## 0.14.2 + +### Patch Changes + +- Updated dependencies [[`47a8f1b`](https://github.com/Effect-TS/effect/commit/47a8f1b644d8294692d92cacd3c8c7543edbfabe), [`8c9abe2`](https://github.com/Effect-TS/effect/commit/8c9abe2b35c46d8891d4b2c14ff9eb46302a14f3), [`8c9abe2`](https://github.com/Effect-TS/effect/commit/8c9abe2b35c46d8891d4b2c14ff9eb46302a14f3), [`3336287`](https://github.com/Effect-TS/effect/commit/3336287ff55a25e56d759b83847bfaa21c40f499), [`54b7c00`](https://github.com/Effect-TS/effect/commit/54b7c0077fa784ad2646b812d6a44641f672edcd), [`3336287`](https://github.com/Effect-TS/effect/commit/3336287ff55a25e56d759b83847bfaa21c40f499), [`47a8f1b`](https://github.com/Effect-TS/effect/commit/47a8f1b644d8294692d92cacd3c8c7543edbfabe)]: + - @effect/platform@0.48.17 + - @effect/platform-node@0.45.19 + - effect@2.4.13 + - @effect/schema@0.64.13 + +## 0.14.1 + +### Patch Changes + +- [#2385](https://github.com/Effect-TS/effect/pull/2385) [`3307729`](https://github.com/Effect-TS/effect/commit/3307729de162a033fa9caa8e14c111013dcf0d87) Thanks [@tim-smart](https://github.com/tim-smart)! - update typescript to 5.4 + +- Updated dependencies [[`9392de6`](https://github.com/Effect-TS/effect/commit/9392de6baa6861662abc2bd3171897145f5ea073), [`75a8d16`](https://github.com/Effect-TS/effect/commit/75a8d16247cc14860cdd7fd948ef542c50c2d55e), [`3307729`](https://github.com/Effect-TS/effect/commit/3307729de162a033fa9caa8e14c111013dcf0d87), [`9392de6`](https://github.com/Effect-TS/effect/commit/9392de6baa6861662abc2bd3171897145f5ea073), [`3307729`](https://github.com/Effect-TS/effect/commit/3307729de162a033fa9caa8e14c111013dcf0d87), [`d17a427`](https://github.com/Effect-TS/effect/commit/d17a427c4427972fb55c45a058780716dc408631)]: + - @effect/schema@0.64.12 + - @effect/platform-node@0.45.18 + - @effect/platform@0.48.16 + - effect@2.4.12 + +## 0.14.0 + +### Minor Changes + +- [#2383](https://github.com/Effect-TS/effect/pull/2383) [`317b5b8`](https://github.com/Effect-TS/effect/commit/317b5b8e8c8c2207469b3ebfcf72bf3a9f7cbc60) Thanks [@tim-smart](https://github.com/tim-smart)! - add TimeToLive support to Persistence module + +### Patch Changes + +- [#2384](https://github.com/Effect-TS/effect/pull/2384) [`2f488c4`](https://github.com/Effect-TS/effect/commit/2f488c436de52576562803c57ebc132ef40ccdd8) Thanks [@tim-smart](https://github.com/tim-smart)! - update dependencies + +- [#2383](https://github.com/Effect-TS/effect/pull/2383) [`317b5b8`](https://github.com/Effect-TS/effect/commit/317b5b8e8c8c2207469b3ebfcf72bf3a9f7cbc60) Thanks [@tim-smart](https://github.com/tim-smart)! - add TimeToLive module to @effect/experimental + + A trait for attaching expiry information to objects. + + ```ts + import * as TimeToLive from "@effect/experimental" + import { Duration, Exit } from "effect" + + class User { + [TimeToLive.symbol](exit: Exit.Exit) { + return Exit.isSuccess(exit) ? Duration.seconds(60) : Duration.zero + } + } + ``` + +- [#2383](https://github.com/Effect-TS/effect/pull/2383) [`317b5b8`](https://github.com/Effect-TS/effect/commit/317b5b8e8c8c2207469b3ebfcf72bf3a9f7cbc60) Thanks [@tim-smart](https://github.com/tim-smart)! - add Redis Persistence module + +- Updated dependencies [[`2f488c4`](https://github.com/Effect-TS/effect/commit/2f488c436de52576562803c57ebc132ef40ccdd8), [`37ca592`](https://github.com/Effect-TS/effect/commit/37ca592a4101ad90adbf8c8b3f727faf3110cae5), [`317b5b8`](https://github.com/Effect-TS/effect/commit/317b5b8e8c8c2207469b3ebfcf72bf3a9f7cbc60)]: + - @effect/platform-node@0.45.17 + - effect@2.4.11 + - @effect/schema@0.64.11 + - @effect/platform@0.48.15 + +## 0.13.10 + +### Patch Changes + +- Updated dependencies [[`9bab1f9`](https://github.com/Effect-TS/effect/commit/9bab1f9fa5b999740755e4e82485cb77c638643a), [`9bbde5b`](https://github.com/Effect-TS/effect/commit/9bbde5be9a0168d1c2a0308bfc27167ed62f3968)]: + - effect@2.4.10 + - @effect/platform@0.48.14 + - @effect/platform-node@0.45.16 + - @effect/schema@0.64.10 + +## 0.13.9 + +### Patch Changes + +- Updated dependencies [[`dc7e497`](https://github.com/Effect-TS/effect/commit/dc7e49720df416870a7483f48adc40aeb23fe32d), [`ffaf7c3`](https://github.com/Effect-TS/effect/commit/ffaf7c36514f88496cdd2fdfdf0bc7ba5a2e5cd4)]: + - @effect/schema@0.64.9 + - @effect/platform@0.48.13 + - @effect/platform-node@0.45.15 + +## 0.13.8 + +### Patch Changes + +- Updated dependencies [[`e0af20e`](https://github.com/Effect-TS/effect/commit/e0af20ec5f6d0b19d66c5ebf610969d55bfc6c22)]: + - @effect/schema@0.64.8 + - @effect/platform@0.48.12 + - @effect/platform-node@0.45.14 + +## 0.13.7 + +### Patch Changes + +- Updated dependencies [[`0f6c7b4`](https://github.com/Effect-TS/effect/commit/0f6c7b426eb3432f60e3a17f8cd92ceac91597bf)]: + - @effect/platform@0.48.11 + - @effect/platform-node@0.45.13 + +## 0.13.6 + +### Patch Changes + +- [#2355](https://github.com/Effect-TS/effect/pull/2355) [`151b785`](https://github.com/Effect-TS/effect/commit/151b7850fa60b066c2bc62602a0ea510ae76c1fa) Thanks [@jrmdayn](https://github.com/jrmdayn)! - Add clear api to remove all items of a result persistence + +- Updated dependencies [[`71fd528`](https://github.com/Effect-TS/effect/commit/71fd5287500f9ce155a7d9f0df6ee3e0ac3aeb99)]: + - @effect/platform-node@0.45.12 + - @effect/platform@0.48.10 + - effect@2.4.9 + - @effect/schema@0.64.7 + +## 0.13.5 + +### Patch Changes + +- [#2352](https://github.com/Effect-TS/effect/pull/2352) [`63f8372`](https://github.com/Effect-TS/effect/commit/63f83722b137e2ab7fdf8ef947c65ed107221353) Thanks [@tim-smart](https://github.com/tim-smart)! - remove use of `__proto__` + +- Updated dependencies [[`595140a`](https://github.com/Effect-TS/effect/commit/595140a13bda09bf22c669196440868e8a274599), [`5f5fcd9`](https://github.com/Effect-TS/effect/commit/5f5fcd969ae30ed6fe61d566a571498d9e895e16), [`bb0b69e`](https://github.com/Effect-TS/effect/commit/bb0b69e519698c7c76aa68217de423c78ad16566), [`7a45ad0`](https://github.com/Effect-TS/effect/commit/7a45ad0a5f715d64a69b28a8ee3573e5f86909c3), [`5c3b1cc`](https://github.com/Effect-TS/effect/commit/5c3b1ccba182d0f636a973729f9c6bfb12539dc8), [`6f7dfc9`](https://github.com/Effect-TS/effect/commit/6f7dfc9637bd641beb93b14e027dcfcb5d2c8feb), [`88b8583`](https://github.com/Effect-TS/effect/commit/88b85838e03d4f33036f9d16c9c00a487fa99bd8), [`cb20824`](https://github.com/Effect-TS/effect/commit/cb20824416cbf251188395d0aad3622e3a5d7ff2), [`6b20bad`](https://github.com/Effect-TS/effect/commit/6b20badebb3a7ca4d38857753e8ecaa09d02ccfb), [`4e64e9b`](https://github.com/Effect-TS/effect/commit/4e64e9b9876de6bfcbabe39e18a91a08e5f3fbb0), [`3851a02`](https://github.com/Effect-TS/effect/commit/3851a022c481006aec1db36651e4b4fd727aa742), [`5f5fcd9`](https://github.com/Effect-TS/effect/commit/5f5fcd969ae30ed6fe61d566a571498d9e895e16), [`814e5b8`](https://github.com/Effect-TS/effect/commit/814e5b828f68210b9e8f336fd6ac688646835dd9), [`a45a525`](https://github.com/Effect-TS/effect/commit/a45a525e7ccf07704dff1666f1e390282b5bac91)]: + - @effect/schema@0.64.6 + - effect@2.4.8 + - @effect/platform@0.48.9 + - @effect/platform-node@0.45.11 + +## 0.13.4 + +### Patch Changes + +- Updated dependencies [[`69d27bb`](https://github.com/Effect-TS/effect/commit/69d27bb633884b6b50f9c3d9e95c29f09b4860b5), [`d0f56c6`](https://github.com/Effect-TS/effect/commit/d0f56c68e604b1cf8dd4e761a3f3cf3631b3cec1)]: + - @effect/platform@0.48.8 + - @effect/schema@0.64.5 + - @effect/platform-node@0.45.10 + +## 0.13.3 + +### Patch Changes + +- Updated dependencies [[`f908948`](https://github.com/Effect-TS/effect/commit/f908948fd05771a670c0b746e2dd9caa9408ef83)]: + - @effect/platform@0.48.7 + - @effect/platform-node@0.45.9 + +## 0.13.2 + +### Patch Changes + +- Updated dependencies [[`eb93283`](https://github.com/Effect-TS/effect/commit/eb93283985913d7b04ca750e36ac8513e7b6cef6)]: + - effect@2.4.7 + - @effect/platform@0.48.6 + - @effect/platform-node@0.45.8 + - @effect/schema@0.64.4 + +## 0.13.1 + +### Patch Changes + +- Updated dependencies [[`e006e4a`](https://github.com/Effect-TS/effect/commit/e006e4a538c97bae6ca1efa74802159e8a688fcb)]: + - @effect/platform@0.48.5 + - @effect/platform-node@0.45.7 + +## 0.13.0 + +### Minor Changes + +- [#2323](https://github.com/Effect-TS/effect/pull/2323) [`d6d3f4e`](https://github.com/Effect-TS/effect/commit/d6d3f4ea4c81031876c4e58a016225cc2f5a81ea) Thanks [@tim-smart](https://github.com/tim-smart)! - remove tag argument for Machine serializable procedures + +## 0.12.3 + +### Patch Changes + +- Updated dependencies [[`cfef6ec`](https://github.com/Effect-TS/effect/commit/cfef6ecd1fe801cec1a3cbfb7f064fc394b0ad73)]: + - @effect/schema@0.64.3 + - @effect/platform@0.48.4 + - @effect/platform-node@0.45.6 + +## 0.12.2 + +### Patch Changes + +- [#2316](https://github.com/Effect-TS/effect/pull/2316) [`7b64c66`](https://github.com/Effect-TS/effect/commit/7b64c662ed7ba6e88146a9cba5b3f46c7213f921) Thanks [@tim-smart](https://github.com/tim-smart)! - support Infinity in DevTools metrics schema + +## 0.12.1 + +### Patch Changes + +- Updated dependencies [[`c362e06`](https://github.com/Effect-TS/effect/commit/c362e066550252d5a9fcbc31a4b34d0e17c50699), [`83ddd6f`](https://github.com/Effect-TS/effect/commit/83ddd6f41029724b2cbd144cf309463967ed1164)]: + - @effect/platform@0.48.3 + - @effect/platform-node@0.45.5 + +## 0.12.0 + +### Minor Changes + +- [#2311](https://github.com/Effect-TS/effect/pull/2311) [`9971186`](https://github.com/Effect-TS/effect/commit/99711862722188fbb5ed3ee75126ad5edf13f72f) Thanks [@tim-smart](https://github.com/tim-smart)! - propogate channel Done type in MsgPack module apis + +- [#2311](https://github.com/Effect-TS/effect/pull/2311) [`9971186`](https://github.com/Effect-TS/effect/commit/99711862722188fbb5ed3ee75126ad5edf13f72f) Thanks [@tim-smart](https://github.com/tim-smart)! - use Ndson for DevTools protocol (instead of msgpack) + +### Patch Changes + +- [#2311](https://github.com/Effect-TS/effect/pull/2311) [`9971186`](https://github.com/Effect-TS/effect/commit/99711862722188fbb5ed3ee75126ad5edf13f72f) Thanks [@tim-smart](https://github.com/tim-smart)! - add Ndjson module to experimental + + Allows you to encode + decode "new line delimited json" + +- Updated dependencies [[`89748c9`](https://github.com/Effect-TS/effect/commit/89748c90b36cb5eb880a9ab9323b252338dee848), [`4f35a7e`](https://github.com/Effect-TS/effect/commit/4f35a7e7c4eba598924aff24d1158b9056bb24be), [`9971186`](https://github.com/Effect-TS/effect/commit/99711862722188fbb5ed3ee75126ad5edf13f72f)]: + - @effect/schema@0.64.2 + - @effect/platform@0.48.2 + - effect@2.4.6 + - @effect/platform-node@0.45.4 + +## 0.11.1 + +### Patch Changes + +- Updated dependencies [[`d10f876`](https://github.com/Effect-TS/effect/commit/d10f876cd98da275bc5dc5750a91a7fc95e97541), [`743ae6d`](https://github.com/Effect-TS/effect/commit/743ae6d12b249f0b35b31b65b2f7ec91d83ee387), [`a75bc48`](https://github.com/Effect-TS/effect/commit/a75bc48e0e3278d0f70665fedecc5ae7ec447e24), [`bce21c5`](https://github.com/Effect-TS/effect/commit/bce21c5ded2177114666ba229bd5029fa000dee3), [`c7d3036`](https://github.com/Effect-TS/effect/commit/c7d303630b7f0825cb2e584557c5767a67214d9f)]: + - @effect/schema@0.64.1 + - effect@2.4.5 + - @effect/platform@0.48.1 + - @effect/platform-node@0.45.3 + +## 0.11.0 + +### Minor Changes + +- [#2279](https://github.com/Effect-TS/effect/pull/2279) [`bdff193`](https://github.com/Effect-TS/effect/commit/bdff193365dd9ec2863573b08eb960aa8dee5c93) Thanks [@gcanti](https://github.com/gcanti)! - - `src/DevTools/Domain.ts` + - use `OptionEncoded` in `SpanFrom` + - `src/Machine.ts` + - use `ExitEncoded` in `SerializableActor` and `boot` + +### Patch Changes + +- Updated dependencies [[`5d47ee0`](https://github.com/Effect-TS/effect/commit/5d47ee0855e492532085b6092879b1b952d84949), [`5d47ee0`](https://github.com/Effect-TS/effect/commit/5d47ee0855e492532085b6092879b1b952d84949), [`1cb7f9c`](https://github.com/Effect-TS/effect/commit/1cb7f9cff7c2272a32fc7a324d87b02e2cd8a2f5), [`5d47ee0`](https://github.com/Effect-TS/effect/commit/5d47ee0855e492532085b6092879b1b952d84949), [`817a04c`](https://github.com/Effect-TS/effect/commit/817a04cb2df0f4140984dc97eb3e1bb14a6c4a38), [`d90a99d`](https://github.com/Effect-TS/effect/commit/d90a99d03d074adc7cd2533f15419138264da5a2), [`dd05faa`](https://github.com/Effect-TS/effect/commit/dd05faa621555ef3585ecd914ac13ecd89b710f4), [`509be1a`](https://github.com/Effect-TS/effect/commit/509be1a0817118489750cf028523134677e44a8a), [`1cb7f9c`](https://github.com/Effect-TS/effect/commit/1cb7f9cff7c2272a32fc7a324d87b02e2cd8a2f5), [`dd05faa`](https://github.com/Effect-TS/effect/commit/dd05faa621555ef3585ecd914ac13ecd89b710f4), [`e7ca973`](https://github.com/Effect-TS/effect/commit/e7ca973c5430ae60716701e58bedd4632ff971fd), [`a1f44cb`](https://github.com/Effect-TS/effect/commit/a1f44cb5112713ff9a3ac3d91a63a2c99d6b7fc1), [`d910dd2`](https://github.com/Effect-TS/effect/commit/d910dd2ca1e8e5aa2f09d9bf3694ede745758f99), [`e7ca973`](https://github.com/Effect-TS/effect/commit/e7ca973c5430ae60716701e58bedd4632ff971fd), [`802674b`](https://github.com/Effect-TS/effect/commit/802674b379b7559ad3ff09b33388891445a9e48b), [`bdff193`](https://github.com/Effect-TS/effect/commit/bdff193365dd9ec2863573b08eb960aa8dee5c93)]: + - @effect/schema@0.64.0 + - effect@2.4.4 + - @effect/platform@0.48.0 + - @effect/platform-node@0.45.2 + +## 0.10.1 + +### Patch Changes + +- [#2274](https://github.com/Effect-TS/effect/pull/2274) [`8b552a2`](https://github.com/Effect-TS/effect/commit/8b552a28a3843c5057af342b8bc1534ed804a23d) Thanks [@tim-smart](https://github.com/tim-smart)! - improve Persistence error messages + +- Updated dependencies [[`0680545`](https://github.com/Effect-TS/effect/commit/068054540f19bb23a79c7c021ed8b2fe34f3e19f), [`20e63fb`](https://github.com/Effect-TS/effect/commit/20e63fb9207210f3fe2d136ec40d0a2dbff3225e), [`20e63fb`](https://github.com/Effect-TS/effect/commit/20e63fb9207210f3fe2d136ec40d0a2dbff3225e)]: + - @effect/platform-node@0.45.1 + - @effect/platform@0.47.1 + - effect@2.4.3 + - @effect/schema@0.63.4 + +## 0.10.0 + +### Minor Changes + +- [#2261](https://github.com/Effect-TS/effect/pull/2261) [`fa9663c`](https://github.com/Effect-TS/effect/commit/fa9663cb854ca03dba672d7857ecff84f1140c9e) Thanks [@tim-smart](https://github.com/tim-smart)! - move Socket module to platform + +### Patch Changes + +- [#2246](https://github.com/Effect-TS/effect/pull/2246) [`5234a9a`](https://github.com/Effect-TS/effect/commit/5234a9aa26f4f1732f423dc89c32ac679d065543) Thanks [@tim-smart](https://github.com/tim-smart)! - add support for SpanEvent's to DevTools protocol + +- [#2254](https://github.com/Effect-TS/effect/pull/2254) [`98b921a`](https://github.com/Effect-TS/effect/commit/98b921a1cf1c099db02e21b19f11e8ec02b908b5) Thanks [@tim-smart](https://github.com/tim-smart)! - send a final metrics snapshot on DevTools shutdown + +- [#2256](https://github.com/Effect-TS/effect/pull/2256) [`f82875f`](https://github.com/Effect-TS/effect/commit/f82875f4e2baca2ddacf5e4fc7efcc9c1ee14f16) Thanks [@tim-smart](https://github.com/tim-smart)! - add Machine module to experimental + + The Machine module can be used to create effectful state machines. Here is an + example of a machine that sends emails: + + ```ts + import { Machine } from "@effect/experimental" + import { runMain } from "@effect/platform-node/NodeRuntime" + import { Data, Effect, List, Request, Schedule } from "effect" + + class SendError extends Data.TaggedError("SendError")<{ + readonly email: string + readonly reason: string + }> {} + + class SendEmail extends Request.TaggedClass("SendEmail")< + void, + SendError, + { + readonly email: string + readonly message: string + } + > {} + + class ProcessEmail extends Request.TaggedClass("ProcessEmail")< + void, + never, + {} + > {} + + class Shutdown extends Request.TaggedClass("Shutdown") {} + + const mailer = Machine.makeWith>()((_, previous) => + Effect.gen(function* (_) { + const ctx = yield* _(Machine.MachineContext) + const state = previous ?? List.empty() + + if (List.isCons(state)) { + yield* _( + ctx.unsafeSend(new ProcessEmail()), + Effect.replicateEffect(List.size(state)) + ) + } + + return Machine.procedures.make(state).pipe( + Machine.procedures.addPrivate()( + "ProcessEmail", + ({ state }) => + Effect.gen(function* (_) { + if (List.isNil(state)) { + return [void 0, state] + } + const req = state.head + yield* _( + Effect.log(`Sending email to ${req.email}`), + Effect.delay(500) + ) + return [void 0, state.tail] + }) + ), + Machine.procedures.add()("SendEmail", (ctx) => + ctx + .send(new ProcessEmail()) + .pipe(Effect.as([void 0, List.append(ctx.state, ctx.request)])) + ), + Machine.procedures.add()("Shutdown", () => + Effect.log("Shutting down").pipe(Effect.zipRight(Effect.interrupt)) + ) + ) + }) + ).pipe(Machine.retry(Schedule.forever)) + + Effect.gen(function* (_) { + const actor = yield* _(Machine.boot(mailer)) + yield* _( + actor.send( + new SendEmail({ email: "test@example.com", message: "Hello, World!" }) + ) + ) + yield* _( + actor.send( + new SendEmail({ email: "test@example.com", message: "Hello, World!" }) + ) + ) + yield* _( + actor.send( + new SendEmail({ email: "test@example.com", message: "Hello, World!" }) + ) + ) + yield* _(actor.send(new Shutdown())) + }).pipe(Effect.scoped, runMain) + ``` + +- Updated dependencies [[`e03811e`](https://github.com/Effect-TS/effect/commit/e03811e80c93e986e6348b3b67ac2ed6d5fefff0), [`ac41d84`](https://github.com/Effect-TS/effect/commit/ac41d84776484cdce8165b7ca2c9c9b6377eee2d), [`0f3d99c`](https://github.com/Effect-TS/effect/commit/0f3d99c27521ec6b221b644a0fffc79199c3acca), [`6137533`](https://github.com/Effect-TS/effect/commit/613753300c7705518ab1fea2f370b032851c2750), [`f373529`](https://github.com/Effect-TS/effect/commit/f373529999f4b8bc92b634f6ea14f19271388eed), [`1bf9f31`](https://github.com/Effect-TS/effect/commit/1bf9f31f07667de677673f7c29a4e7a26ebad3c8), [`e3ff789`](https://github.com/Effect-TS/effect/commit/e3ff789226f89e71eb28ca38ce79f90af6a03f1a), [`6137533`](https://github.com/Effect-TS/effect/commit/613753300c7705518ab1fea2f370b032851c2750), [`507ba40`](https://github.com/Effect-TS/effect/commit/507ba4060ff043c1a8d541dae723fa6940633b00), [`4064ea0`](https://github.com/Effect-TS/effect/commit/4064ea04e0b3fa23108ee471cd89ab2482b2f6e5), [`e466afe`](https://github.com/Effect-TS/effect/commit/e466afe32f2de598ceafd8982bd0cfbd388e5671), [`465be79`](https://github.com/Effect-TS/effect/commit/465be7926afe98169837d8a4ed5ebc059a732d21), [`f373529`](https://github.com/Effect-TS/effect/commit/f373529999f4b8bc92b634f6ea14f19271388eed), [`de74eb8`](https://github.com/Effect-TS/effect/commit/de74eb80a79eebde5ff645033765e7a617e92f27), [`d8e6940`](https://github.com/Effect-TS/effect/commit/d8e694040f67da6fefc0f5c98fc8e15c0b48822e), [`fa9663c`](https://github.com/Effect-TS/effect/commit/fa9663cb854ca03dba672d7857ecff84f1140c9e), [`fa9663c`](https://github.com/Effect-TS/effect/commit/fa9663cb854ca03dba672d7857ecff84f1140c9e)]: + - effect@2.4.2 + - @effect/platform@0.47.0 + - @effect/schema@0.63.3 + - @effect/platform-node@0.45.0 + +## 0.9.20 + +### Patch Changes + +- Updated dependencies [[`a4a0006`](https://github.com/Effect-TS/effect/commit/a4a0006c7f19fc261df5cda16963d73457e4d6ac), [`7535080`](https://github.com/Effect-TS/effect/commit/7535080f2e2f9859711031161600c01807cc43ea), [`39f583e`](https://github.com/Effect-TS/effect/commit/39f583eaeb29eecd6eaec3b113b24d9d413153df), [`f428198`](https://github.com/Effect-TS/effect/commit/f428198725d4b9e304ecd5ff8bad8f92d871dbe3), [`0a37676`](https://github.com/Effect-TS/effect/commit/0a37676aa0eb2a21e17af2e6df9f81f52bbc8831), [`bd1d7ac`](https://github.com/Effect-TS/effect/commit/bd1d7ac75eea57a94d5e2d8e1edccb3136e84899), [`c035972`](https://github.com/Effect-TS/effect/commit/c035972dfabdd3cb3372b5ab468aa2fd0d808f4d), [`6f503b7`](https://github.com/Effect-TS/effect/commit/6f503b774d893bf2af34f66202e270d8c45d5f31)]: + - effect@2.4.1 + - @effect/platform@0.46.3 + - @effect/schema@0.63.2 + - @effect/platform-node@0.44.11 + +## 0.9.19 + +### Patch Changes + +- Updated dependencies [[`5d30853`](https://github.com/Effect-TS/effect/commit/5d308534cac6f187227185393c0bac9eb27f90ab), [`6e350ed`](https://github.com/Effect-TS/effect/commit/6e350ed611feb0341e00aafd3c3905cd5ba53f07)]: + - @effect/schema@0.63.1 + - @effect/platform@0.46.2 + - @effect/platform-node@0.44.10 + +## 0.9.18 + +### Patch Changes + +- Updated dependencies [[`aa6556f`](https://github.com/Effect-TS/effect/commit/aa6556f007117caea84d6965aa30846a11879e9d)]: + - @effect/platform@0.46.1 + - @effect/platform-node@0.44.9 + +## 0.9.17 + +### Patch Changes + +- Updated dependencies [[`5de7be5`](https://github.com/Effect-TS/effect/commit/5de7be5beca2e963b503e6029dcc3217848187d2), [`489fcf3`](https://github.com/Effect-TS/effect/commit/489fcf363ff2b2a953166b740cb9a62d7fc2a101), [`7d9c3bf`](https://github.com/Effect-TS/effect/commit/7d9c3bff6c18d451e0e4781042945ec5c7be1b9f), [`d8d278b`](https://github.com/Effect-TS/effect/commit/d8d278b2efb2966947029885e01f7b68348a021f), [`14c5711`](https://github.com/Effect-TS/effect/commit/14c57110078f0862b8da5c7a2c5d980f54447484), [`5de7be5`](https://github.com/Effect-TS/effect/commit/5de7be5beca2e963b503e6029dcc3217848187d2), [`54ddbb7`](https://github.com/Effect-TS/effect/commit/54ddbb720aeeb657537b01ae221cdcd5e919c1a6), [`b9cb3a9`](https://github.com/Effect-TS/effect/commit/b9cb3a9c9bfdd75536bd70b4e8b557c12d4923ff), [`585fcce`](https://github.com/Effect-TS/effect/commit/585fcce162d0f07a48d7cd984a9b722966fbebbe), [`93b412d`](https://github.com/Effect-TS/effect/commit/93b412d4a9ed762dc9fa5807e51fad0fc78a614a), [`55b26a6`](https://github.com/Effect-TS/effect/commit/55b26a6342b4826f1116e7a1eb660118c274458e), [`136ef40`](https://github.com/Effect-TS/effect/commit/136ef40fe4a394abfa5c6a7ec103eea57251423e), [`a025b12`](https://github.com/Effect-TS/effect/commit/a025b121235ba01cfce8d62a775491880c575561), [`2097739`](https://github.com/Effect-TS/effect/commit/20977393d2383bff709304e81ec7d51cafd57108), [`f24ac9f`](https://github.com/Effect-TS/effect/commit/f24ac9f0c2c520add58f09fbdcec5defda03bd52)]: + - effect@2.4.0 + - @effect/platform@0.46.0 + - @effect/schema@0.63.0 + - @effect/platform-node@0.44.8 + +## 0.9.16 + +### Patch Changes + +- [#2187](https://github.com/Effect-TS/effect/pull/2187) [`e6d36c0`](https://github.com/Effect-TS/effect/commit/e6d36c0813d836f17eabb6a9c7849baffca12dbf) Thanks [@tim-smart](https://github.com/tim-smart)! - update development dependencies + +- Updated dependencies [[`5ad2eec`](https://github.com/Effect-TS/effect/commit/5ad2eece0280b6db6a749d25cac1dcf6d33659a9), [`e6d36c0`](https://github.com/Effect-TS/effect/commit/e6d36c0813d836f17eabb6a9c7849baffca12dbf)]: + - effect@2.3.8 + - @effect/platform-node@0.44.7 + - @effect/schema@0.62.9 + - @effect/platform@0.45.6 + +## 0.9.15 + +### Patch Changes + +- Updated dependencies [[`6daf084`](https://github.com/Effect-TS/effect/commit/6daf0845de008772011db8d7c75b7c37a6b4d334)]: + - @effect/platform@0.45.5 + - @effect/platform-node@0.44.6 + +## 0.9.14 + +### Patch Changes + +- [#2163](https://github.com/Effect-TS/effect/pull/2163) [`ca2b411`](https://github.com/Effect-TS/effect/commit/ca2b411e3c11ddb19d2321c7266049b7f4115617) Thanks [@IMax153](https://github.com/IMax153)! - Fix the order in which the experimental data-loader's internal worker enables interruptibility + +- Updated dependencies [[`bc8404d`](https://github.com/Effect-TS/effect/commit/bc8404d54fd42072d200c0399cb39672837afa9f), [`abcb7d9`](https://github.com/Effect-TS/effect/commit/abcb7d983a4a85b43b7175e952f5b331b9019aea), [`2c5cbcd`](https://github.com/Effect-TS/effect/commit/2c5cbcd1161b4f40dab184999291e817314107de), [`6565916`](https://github.com/Effect-TS/effect/commit/6565916ef254bf910e47d25fd0ef55e7cb420241), [`abcb7d9`](https://github.com/Effect-TS/effect/commit/abcb7d983a4a85b43b7175e952f5b331b9019aea)]: + - effect@2.3.7 + - @effect/platform@0.45.4 + - @effect/platform-node@0.44.5 + - @effect/schema@0.62.8 + +## 0.9.13 + +### Patch Changes + +- Updated dependencies [[`09532a8`](https://github.com/Effect-TS/effect/commit/09532a86b7d0cc23557c89158f0342753dfce4b0)]: + - @effect/platform@0.45.3 + - @effect/platform-node@0.44.4 + +## 0.9.12 + +### Patch Changes + +- Updated dependencies [[`f612749`](https://github.com/Effect-TS/effect/commit/f612749ddfff40cadef3387100135f2cb9a4a9f3)]: + - @effect/platform-node@0.44.3 + +## 0.9.11 + +### Patch Changes + +- Updated dependencies [[`b1163b2`](https://github.com/Effect-TS/effect/commit/b1163b2bd67b65bafbbb39fc4c67576e5cbaf444), [`b46b869`](https://github.com/Effect-TS/effect/commit/b46b869e59a6da5aa235a9fcc25e1e0d24e9e8f8), [`dbff62c`](https://github.com/Effect-TS/effect/commit/dbff62c3026054350a671f6210058ec5844c285e), [`de1b226`](https://github.com/Effect-TS/effect/commit/de1b226282b5ab6c2809dd93f3bdb066f24a1333), [`a663390`](https://github.com/Effect-TS/effect/commit/a66339090ae7b960f8a8b90a0dcdc505de5aaf3e), [`ff88f80`](https://github.com/Effect-TS/effect/commit/ff88f808c4ed9947a148045849e7410b00acad0a), [`11be07b`](https://github.com/Effect-TS/effect/commit/11be07bf65d82cfdf994cdb9d8ca937f995cb4f0), [`c568645`](https://github.com/Effect-TS/effect/commit/c5686451c87d26382135a1c63b00ef171bb24f62), [`88835e5`](https://github.com/Effect-TS/effect/commit/88835e575a0bfbeff9a3696a332f32192c940e12), [`e572b07`](https://github.com/Effect-TS/effect/commit/e572b076e9b4369d9cc8e55414006eef376c93d9), [`e787a57`](https://github.com/Effect-TS/effect/commit/e787a5772e30d8b840cb98b49d36996e7d659a6c), [`b415577`](https://github.com/Effect-TS/effect/commit/b415577f6c576073733929c858e5aac27b6d5880), [`ff8046f`](https://github.com/Effect-TS/effect/commit/ff8046f57dfd073eba60ce6d3144ab060fbf93ce), [`44c3b43`](https://github.com/Effect-TS/effect/commit/44c3b43653e64d7e425d39815d8ff405acec9b99)]: + - effect@2.3.6 + - @effect/schema@0.62.7 + - @effect/platform@0.45.2 + - @effect/platform-node@0.44.2 + +## 0.9.10 + +### Patch Changes + +- Updated dependencies [[`65895ab`](https://github.com/Effect-TS/effect/commit/65895ab982e0917ac92f0827e387e7cf61be1e69)]: + - @effect/platform@0.45.1 + - @effect/platform-node@0.44.1 + +## 0.9.9 + +### Patch Changes + +- Updated dependencies [[`2b62548`](https://github.com/Effect-TS/effect/commit/2b6254845882f399636d24223c483e5489e3cff4)]: + - @effect/platform-node@0.44.0 + - @effect/platform@0.45.0 + +## 0.9.8 + +### Patch Changes + +- Updated dependencies [[`aef2b8b`](https://github.com/Effect-TS/effect/commit/aef2b8bb636ada07224dc9cf491bebe622c1aeda), [`b881365`](https://github.com/Effect-TS/effect/commit/b8813650355322ea2fc1fbaa4f846bd87a7a05f3), [`7eecb1c`](https://github.com/Effect-TS/effect/commit/7eecb1c6cebe36550df3cca85a46867adbcaa2ca)]: + - @effect/schema@0.62.6 + - effect@2.3.5 + - @effect/platform@0.44.7 + - @effect/platform-node@0.43.7 + +## 0.9.7 + +### Patch Changes + +- [#2115](https://github.com/Effect-TS/effect/pull/2115) [`ec78c95`](https://github.com/Effect-TS/effect/commit/ec78c9566bcbe55222a3ba676ed196d5528f1b7b) Thanks [@tim-smart](https://github.com/tim-smart)! - fix Persistence KeyValueStore adapter + +## 0.9.6 + +### Patch Changes + +- Updated dependencies [[`17bda66`](https://github.com/Effect-TS/effect/commit/17bda66431c999a546920c10adb205e6c8bea7d1)]: + - effect@2.3.4 + - @effect/platform@0.44.6 + - @effect/platform-node@0.43.6 + - @effect/schema@0.62.5 + +## 0.9.5 + +### Patch Changes + +- Updated dependencies [[`1c6d18b`](https://github.com/Effect-TS/effect/commit/1c6d18b422b0bd800f2ed036dba9cb78db296c03), [`13d3266`](https://github.com/Effect-TS/effect/commit/13d3266f331f7aa49b55dd244d4e749a82255274), [`a344b42`](https://github.com/Effect-TS/effect/commit/a344b420862f71532a28c72f00b7ba54776d744d)]: + - @effect/schema@0.62.4 + - @effect/platform@0.44.5 + - @effect/platform-node@0.43.5 + +## 0.9.4 + +### Patch Changes + +- [#2106](https://github.com/Effect-TS/effect/pull/2106) [`b7d9a55`](https://github.com/Effect-TS/effect/commit/b7d9a55ebb3db5c1d64a2c75c5b1f12ebe1faf39) Thanks [@tim-smart](https://github.com/tim-smart)! - don't require tags for Persistence keys + +- Updated dependencies [[`efd41d8`](https://github.com/Effect-TS/effect/commit/efd41d8131c3d90867608969ef7c4eef490eb5e6), [`0f83515`](https://github.com/Effect-TS/effect/commit/0f83515a9c01d13c7c15a3f026e02d22c3c6bb7f), [`0f83515`](https://github.com/Effect-TS/effect/commit/0f83515a9c01d13c7c15a3f026e02d22c3c6bb7f)]: + - effect@2.3.3 + - @effect/platform@0.44.4 + - @effect/platform-node@0.43.4 + - @effect/schema@0.62.3 + +## 0.9.3 + +### Patch Changes + +- Updated dependencies [[`6654f5f`](https://github.com/Effect-TS/effect/commit/6654f5f0f6b9d97165ede5e04ca16776e2599328), [`2eb11b4`](https://github.com/Effect-TS/effect/commit/2eb11b47752cedf233ef4c4395d9c4efc9b9e180), [`56c09bd`](https://github.com/Effect-TS/effect/commit/56c09bd369279a6a7785209d172739935818cba6), [`71aa5b1`](https://github.com/Effect-TS/effect/commit/71aa5b1c180dcb8b53aefe232d12a97bd06b5447), [`1700af8`](https://github.com/Effect-TS/effect/commit/1700af8af1131602887da721914c8562b6342393)]: + - effect@2.3.2 + - @effect/platform@0.44.3 + - @effect/platform-node@0.43.3 + - @effect/schema@0.62.2 + +## 0.9.2 + +### Patch Changes + +- Updated dependencies [[`29739dd`](https://github.com/Effect-TS/effect/commit/29739dde8e6232824d49c4c7f8856de245249c5c)]: + - @effect/platform@0.44.2 + - @effect/platform-node@0.43.2 + +## 0.9.1 + +### Patch Changes + +- Updated dependencies [[`b5a8215`](https://github.com/Effect-TS/effect/commit/b5a8215ee2a97a8865d69ee55ce1b9835948c922)]: + - effect@2.3.1 + - @effect/platform@0.44.1 + - @effect/platform-node@0.43.1 + - @effect/schema@0.62.1 + +## 0.9.0 + +### Minor Changes + +- [#2006](https://github.com/Effect-TS/effect/pull/2006) [`9a2d1c1`](https://github.com/Effect-TS/effect/commit/9a2d1c1468ea0789b34767ad683da074f061ea9c) Thanks [@github-actions](https://github.com/apps/github-actions)! - With this change we now require a string key to be provided for all tags and renames the dear old `Tag` to `GenericTag`, so when previously you could do: + + ```ts + import { Effect, Context } from "effect" + interface Service { + readonly _: unique symbol + } + const Service = Context.Tag< + Service, + { + number: Effect.Effect + } + >() + ``` + + you are now mandated to do: + + ```ts + import { Effect, Context } from "effect" + interface Service { + readonly _: unique symbol + } + const Service = Context.GenericTag< + Service, + { + number: Effect.Effect + } + >("Service") + ``` + + This makes by default all tags globals and ensures better debuggaility when unexpected errors arise. + + Furthermore we introduce a new way of constructing tags that should be considered the new default: + + ```ts + import { Effect, Context } from "effect" + class Service extends Context.Tag("Service")< + Service, + { + number: Effect.Effect + } + >() {} + + const program = Effect.flatMap(Service, ({ number }) => number).pipe( + Effect.flatMap((_) => Effect.log(`number: ${_}`)) + ) + ``` + + this will use "Service" as the key and will create automatically an opaque identifier (the class) to be used at the type level, it does something similar to the above in a single shot. + +- [#2006](https://github.com/Effect-TS/effect/pull/2006) [`1a77f72`](https://github.com/Effect-TS/effect/commit/1a77f72cdaf43d6cdc91b6060f82832edcdbbcb3) Thanks [@github-actions](https://github.com/apps/github-actions)! - change `Effect` type parameters order from `Effect` to `Effect` + +- [#2006](https://github.com/Effect-TS/effect/pull/2006) [`a34dbdc`](https://github.com/Effect-TS/effect/commit/a34dbdc1552c73c1b612676f262a0c735ce444a7) Thanks [@github-actions](https://github.com/apps/github-actions)! - - Schema: change type parameters order from `Schema` to `Schema` + - Serializable: change type parameters order from `Serializable` to `Serializable` + - Class: change type parameters order from `Class` to `Class` + - PropertySignature: change type parameters order from `PropertySignature` to `PropertySignature` + +- [#2006](https://github.com/Effect-TS/effect/pull/2006) [`b1e2086`](https://github.com/Effect-TS/effect/commit/b1e2086ea8bf410e4ad75d71c0760825924e8f9f) Thanks [@github-actions](https://github.com/apps/github-actions)! - remove re-exports from platform packages + +- [#2006](https://github.com/Effect-TS/effect/pull/2006) [`9a2d1c1`](https://github.com/Effect-TS/effect/commit/9a2d1c1468ea0789b34767ad683da074f061ea9c) Thanks [@github-actions](https://github.com/apps/github-actions)! - This change enables `Effect.serviceConstants` and `Effect.serviceMembers` to access any constant in the service, not only the effects, namely it is now possible to do: + + ```ts + import { Effect, Context } from "effect" + + class NumberRepo extends Context.TagClass("NumberRepo")< + NumberRepo, + { + readonly numbers: Array + } + >() { + static numbers = Effect.serviceConstants(NumberRepo).numbers + } + ``` + +### Patch Changes + +- [#2006](https://github.com/Effect-TS/effect/pull/2006) [`2131a8c`](https://github.com/Effect-TS/effect/commit/2131a8cfd2b7570ace56591fd7da4b3a856ab531) Thanks [@github-actions](https://github.com/apps/github-actions)! - encode per chunk in MsgPack.pack/unpack + +- Updated dependencies [[`96bcee2`](https://github.com/Effect-TS/effect/commit/96bcee21021aecd8ffd86440a2c9be353c4668e3), [`b1e2086`](https://github.com/Effect-TS/effect/commit/b1e2086ea8bf410e4ad75d71c0760825924e8f9f), [`4cd6e14`](https://github.com/Effect-TS/effect/commit/4cd6e144945b6c398f5f5abe3471ff7fb3372bfd), [`96bcee2`](https://github.com/Effect-TS/effect/commit/96bcee21021aecd8ffd86440a2c9be353c4668e3), [`c77f635`](https://github.com/Effect-TS/effect/commit/c77f635f8a26ca6d83cb569d911f8eee79033fd9), [`e343a74`](https://github.com/Effect-TS/effect/commit/e343a74843dd9edf879417fa94cb51de7ed5b402), [`acf1894`](https://github.com/Effect-TS/effect/commit/acf1894f45945dbe5c39451e36aabb4b5092f257), [`9dc04c8`](https://github.com/Effect-TS/effect/commit/9dc04c88a2ea9c68122cb2632a76f0f4be40329a), [`9a2d1c1`](https://github.com/Effect-TS/effect/commit/9a2d1c1468ea0789b34767ad683da074f061ea9c), [`1a77f72`](https://github.com/Effect-TS/effect/commit/1a77f72cdaf43d6cdc91b6060f82832edcdbbcb3), [`af47aa3`](https://github.com/Effect-TS/effect/commit/af47aa37196ad542c9c23a4896d8ef98147e1205), [`d3f9f4d`](https://github.com/Effect-TS/effect/commit/d3f9f4d4032b1131c62f4ddb21a4583e4e8d7c18), [`c986f0e`](https://github.com/Effect-TS/effect/commit/c986f0e0ce4d22ba08177ed351152718479ab63c), [`96bcee2`](https://github.com/Effect-TS/effect/commit/96bcee21021aecd8ffd86440a2c9be353c4668e3), [`d3f9f4d`](https://github.com/Effect-TS/effect/commit/d3f9f4d4032b1131c62f4ddb21a4583e4e8d7c18), [`d3f9f4d`](https://github.com/Effect-TS/effect/commit/d3f9f4d4032b1131c62f4ddb21a4583e4e8d7c18), [`d3f9f4d`](https://github.com/Effect-TS/effect/commit/d3f9f4d4032b1131c62f4ddb21a4583e4e8d7c18), [`70dde23`](https://github.com/Effect-TS/effect/commit/70dde238f81125e353fd7bde5fc24ecd8969bf97), [`a34dbdc`](https://github.com/Effect-TS/effect/commit/a34dbdc1552c73c1b612676f262a0c735ce444a7), [`d3f9f4d`](https://github.com/Effect-TS/effect/commit/d3f9f4d4032b1131c62f4ddb21a4583e4e8d7c18), [`81b7425`](https://github.com/Effect-TS/effect/commit/81b7425320cbbe2a6cf547a3e3ab3549cdba14cf), [`b1e2086`](https://github.com/Effect-TS/effect/commit/b1e2086ea8bf410e4ad75d71c0760825924e8f9f), [`02c3461`](https://github.com/Effect-TS/effect/commit/02c34615d02f91269ea04036d0306fccf4e39e18), [`0e56e99`](https://github.com/Effect-TS/effect/commit/0e56e998ab9815c4d096c239a553cb86a0f99af9), [`8b0ded9`](https://github.com/Effect-TS/effect/commit/8b0ded9f10ba0d96fcb9af24eff2dbd9341f85e3), [`8dd83e8`](https://github.com/Effect-TS/effect/commit/8dd83e854bfcaa6dab876994c5f813dcfb486c28), [`5127afe`](https://github.com/Effect-TS/effect/commit/5127afec1c519e0a3d7460844a9101a96272f29e), [`d75f6fe`](https://github.com/Effect-TS/effect/commit/d75f6fe6499deb0a5ee9ec94af3b5fd4eb03a2d0), [`7356e5c`](https://github.com/Effect-TS/effect/commit/7356e5cc16e9d70f18c02dee1dcb4ad539fd130a), [`3077cde`](https://github.com/Effect-TS/effect/commit/3077cde08a60246821a940964a84dd7f7c8b9f54), [`be19ce0`](https://github.com/Effect-TS/effect/commit/be19ce0b8bdf1fac80bb8d7e0b06a86986b47409), [`4a5d01a`](https://github.com/Effect-TS/effect/commit/4a5d01a409e9b6dd53893e65f8e5c9247f568021), [`78f47ab`](https://github.com/Effect-TS/effect/commit/78f47abfe3cb0a8bbde818b1c5fc603270538b47), [`6361ee2`](https://github.com/Effect-TS/effect/commit/6361ee2e83bdfead24045c3d058a7298efc18113), [`52e5d20`](https://github.com/Effect-TS/effect/commit/52e5d2077582bf51f25861c7139fc920c2c24166), [`c6137ec`](https://github.com/Effect-TS/effect/commit/c6137ec62c6b5542d5062ae1a3c936cb915dee22), [`86f665d`](https://github.com/Effect-TS/effect/commit/86f665d7bd25ba0a3f046a2384798378310dcf0c), [`f5ae081`](https://github.com/Effect-TS/effect/commit/f5ae08195e68e76faeac258c565d79da4e01e7d6), [`4a5d01a`](https://github.com/Effect-TS/effect/commit/4a5d01a409e9b6dd53893e65f8e5c9247f568021), [`60686f5`](https://github.com/Effect-TS/effect/commit/60686f5c38bef1b93a3a0dda9b6596d46aceab03), [`9a2d1c1`](https://github.com/Effect-TS/effect/commit/9a2d1c1468ea0789b34767ad683da074f061ea9c), [`5127afe`](https://github.com/Effect-TS/effect/commit/5127afec1c519e0a3d7460844a9101a96272f29e), [`56b8691`](https://github.com/Effect-TS/effect/commit/56b86916bf3da18002f3655d859dbc487eb5a6de), [`8ee2931`](https://github.com/Effect-TS/effect/commit/8ee293159b4f7cb7af8558287a0a047f3a69743d), [`6727474`](https://github.com/Effect-TS/effect/commit/672747497490a30d36dd49c06db19aabf09dc7f0), [`5127afe`](https://github.com/Effect-TS/effect/commit/5127afec1c519e0a3d7460844a9101a96272f29e)]: + - effect@2.3.0 + - @effect/platform@0.44.0 + - @effect/schema@0.62.0 + - @effect/platform-node@0.43.0 + +## 0.8.10 + +### Patch Changes + +- Updated dependencies [[`3ddfdbf`](https://github.com/Effect-TS/effect/commit/3ddfdbf914edea536aef207cec6695f33496258c), [`3ddfdbf`](https://github.com/Effect-TS/effect/commit/3ddfdbf914edea536aef207cec6695f33496258c)]: + - effect@2.2.5 + - @effect/platform@0.43.11 + - @effect/platform-node@0.42.11 + - @effect/schema@0.61.7 + +## 0.8.9 + +### Patch Changes + +- Updated dependencies [[`d0b911c`](https://github.com/Effect-TS/effect/commit/d0b911c75f284c7aa87f25aa96926e6bde7690d0), [`330e1a4`](https://github.com/Effect-TS/effect/commit/330e1a4e2c1fc0af6c80c80c81dd38c3e50fab78), [`6928a2b`](https://github.com/Effect-TS/effect/commit/6928a2b0bae86a4bdfbece0aa32924207c2d5a70), [`296bc1c`](https://github.com/Effect-TS/effect/commit/296bc1c9d24986d299d2669115d584cb27b73c60)]: + - effect@2.2.4 + - @effect/platform@0.43.10 + - @effect/schema@0.61.6 + - @effect/platform-node@0.42.10 + +## 0.8.8 + +### Patch Changes + +- Updated dependencies [[`1b841a9`](https://github.com/Effect-TS/effect/commit/1b841a91fed86825cd2867cf1e68e41d8ff26b4e)]: + - @effect/platform@0.43.9 + - @effect/platform-node@0.42.9 + +## 0.8.7 + +### Patch Changes + +- Updated dependencies [[`32bf796`](https://github.com/Effect-TS/effect/commit/32bf796c3e5db1b2b68e8b1b20db664295991643)]: + - @effect/platform@0.43.8 + - @effect/platform-node@0.42.8 + +## 0.8.6 + +### Patch Changes + +- Updated dependencies [[`cde08f3`](https://github.com/Effect-TS/effect/commit/cde08f354ed2ff2921d1d98bd539c7d65a2ddd73)]: + - @effect/platform@0.43.7 + - @effect/platform-node@0.42.7 + +## 0.8.5 + +### Patch Changes + +- Updated dependencies [[`c96bb17`](https://github.com/Effect-TS/effect/commit/c96bb17043e2cec1eaeb319614a4c2904d876beb)]: + - @effect/platform@0.43.6 + - @effect/platform-node@0.42.6 + +## 0.8.4 + +### Patch Changes + +- Updated dependencies [[`f1ff44b`](https://github.com/Effect-TS/effect/commit/f1ff44b58cdb1886b38681e8fedc309eb9ac6853), [`13785cf`](https://github.com/Effect-TS/effect/commit/13785cf4a5082d8d9cf8d7c991141dee0d2b4d31)]: + - @effect/schema@0.61.5 + - @effect/platform@0.43.5 + - @effect/platform-node@0.42.5 + +## 0.8.3 + +### Patch Changes + +- [#1999](https://github.com/Effect-TS/effect/pull/1999) [`78f5921`](https://github.com/Effect-TS/effect/commit/78f59211502ded6fcbe15a49d6fde941cccc9d52) Thanks [@tim-smart](https://github.com/tim-smart)! - ensure forked fibers are interruptible + +- Updated dependencies [[`22794e0`](https://github.com/Effect-TS/effect/commit/22794e0ba00e40281f30a22fa84412003c24877d), [`f73e6c0`](https://github.com/Effect-TS/effect/commit/f73e6c033fb0729a9cfa5eb4bc39f79d3126e247), [`6bf02c7`](https://github.com/Effect-TS/effect/commit/6bf02c70fe10a04d1b34d6666f95416e42a6225a), [`78f5921`](https://github.com/Effect-TS/effect/commit/78f59211502ded6fcbe15a49d6fde941cccc9d52)]: + - effect@2.2.3 + - @effect/schema@0.61.4 + - @effect/platform-node@0.42.4 + - @effect/platform@0.43.4 + +## 0.8.2 + +### Patch Changes + +- Updated dependencies [[`9863e2f`](https://github.com/Effect-TS/effect/commit/9863e2fb3561dc019965aeccd6584a418fc8b401)]: + - @effect/schema@0.61.3 + - @effect/platform@0.43.3 + - @effect/platform-node@0.42.3 + +## 0.8.1 + +### Patch Changes + +- Updated dependencies [[`64f710a`](https://github.com/Effect-TS/effect/commit/64f710aa49dec6ffcd33ee23438d0774f5489733)]: + - @effect/schema@0.61.2 + - @effect/platform@0.43.2 + - @effect/platform-node@0.42.2 + +## 0.8.0 + +### Minor Changes + +- [#1985](https://github.com/Effect-TS/effect/pull/1985) [`634af60`](https://github.com/Effect-TS/effect/commit/634af60a2f9d407f42357edc29ca4c14a005fdf9) Thanks [@tim-smart](https://github.com/tim-smart)! - add lmdb implementation of persistence + +## 0.7.2 + +### Patch Changes + +- Updated dependencies [[`c7550f9`](https://github.com/Effect-TS/effect/commit/c7550f96e1006eee832ce5025bf0c197a65935ea), [`8d1f6e4`](https://github.com/Effect-TS/effect/commit/8d1f6e4bb13e221804fb1762ef19e02bcefc8f61), [`d404561`](https://github.com/Effect-TS/effect/commit/d404561e47ec2fa5f68709a308ee5d2ee959141d), [`7b84a3c`](https://github.com/Effect-TS/effect/commit/7b84a3c7e4b9c8dc02294b0e3cc3ae3becea977b), [`1a84dee`](https://github.com/Effect-TS/effect/commit/1a84dee0e9ddbfaf2610e4d7c00c7020c427171a), [`ac30bf4`](https://github.com/Effect-TS/effect/commit/ac30bf4cd53de0663784f65ae6bee8279333df97)]: + - @effect/schema@0.61.1 + - effect@2.2.2 + - @effect/platform@0.43.1 + - @effect/platform-node@0.42.1 + +## 0.7.1 + +### Patch Changes + +- [#1968](https://github.com/Effect-TS/effect/pull/1968) [`fdf7b0e`](https://github.com/Effect-TS/effect/commit/fdf7b0e6647419fb70e18be64b60e652de42e97d) Thanks [@IMax153](https://github.com/IMax153)! - ensure data-loader worker fiber can be interrupted if forked in an uninterruptible region + +## 0.7.0 + +### Minor Changes + +- [#1922](https://github.com/Effect-TS/effect/pull/1922) [`62b40e8`](https://github.com/Effect-TS/effect/commit/62b40e8479371d6663c0255aaca56a1ae0d59764) Thanks [@gcanti](https://github.com/gcanti)! - add context tracking to Schema, closes #1873 + +### Patch Changes + +- Updated dependencies [[`84da31f`](https://github.com/Effect-TS/effect/commit/84da31f0643e8651b9d311b30526b1e4edfbdfb8), [`62b40e8`](https://github.com/Effect-TS/effect/commit/62b40e8479371d6663c0255aaca56a1ae0d59764), [`645bea2`](https://github.com/Effect-TS/effect/commit/645bea2551129f94a5b0e38347e28067dee531bb), [`62b40e8`](https://github.com/Effect-TS/effect/commit/62b40e8479371d6663c0255aaca56a1ae0d59764)]: + - effect@2.2.1 + - @effect/schema@0.61.0 + - @effect/platform-node@0.42.0 + - @effect/platform@0.43.0 + +## 0.6.11 + +### Patch Changes + +- Updated dependencies [[`202befc`](https://github.com/Effect-TS/effect/commit/202befc2ecbeb117c4fa85ef9b12a3d3a48273d2), [`fe05ad7`](https://github.com/Effect-TS/effect/commit/fe05ad7bcb3b88d47800ab69ebf53641023676f1), [`ee4ff8a`](https://github.com/Effect-TS/effect/commit/ee4ff8a943141fcf2877af92c5877ee87a989fb9), [`ee4ff8a`](https://github.com/Effect-TS/effect/commit/ee4ff8a943141fcf2877af92c5877ee87a989fb9), [`ee4ff8a`](https://github.com/Effect-TS/effect/commit/ee4ff8a943141fcf2877af92c5877ee87a989fb9), [`ee4ff8a`](https://github.com/Effect-TS/effect/commit/ee4ff8a943141fcf2877af92c5877ee87a989fb9), [`10df798`](https://github.com/Effect-TS/effect/commit/10df798639e556f9d88265ef7fc3cf8a3bbe3874)]: + - effect@2.2.0 + - @effect/platform@0.42.7 + - @effect/platform-node@0.41.8 + - @effect/schema@0.60.7 + +## 0.6.10 + +### Patch Changes + +- Updated dependencies [[`21b9edd`](https://github.com/Effect-TS/effect/commit/21b9edde464f7c5624ef54ad1b5e264204a37625)]: + - effect@2.1.2 + - @effect/platform@0.42.6 + - @effect/platform-node@0.41.7 + - @effect/schema@0.60.6 + +## 0.6.9 + +### Patch Changes + +- Updated dependencies [[`3bf67cf`](https://github.com/Effect-TS/effect/commit/3bf67cf64ff27ffaa811b07751875cb161ac3385)]: + - @effect/schema@0.60.5 + - @effect/platform@0.42.5 + - @effect/platform-node@0.41.6 + +## 0.6.8 + +### Patch Changes + +- Updated dependencies [[`0d1af1e`](https://github.com/Effect-TS/effect/commit/0d1af1e38c11b94e152beaccd0ff7569a1b3f5b7), [`0d1af1e`](https://github.com/Effect-TS/effect/commit/0d1af1e38c11b94e152beaccd0ff7569a1b3f5b7), [`a222524`](https://github.com/Effect-TS/effect/commit/a2225247e9de2e013d287320790fde88c081dbbd)]: + - @effect/schema@0.60.4 + - effect@2.1.1 + - @effect/platform@0.42.4 + - @effect/platform-node@0.41.5 + +## 0.6.7 + +### Patch Changes + +- Updated dependencies [[`d543221`](https://github.com/Effect-TS/effect/commit/d5432213e91ab620aa66e0fd92a6593134d18940), [`2530d47`](https://github.com/Effect-TS/effect/commit/2530d470b0ad5df7e636921eedfb1cbe42821f94), [`f493929`](https://github.com/Effect-TS/effect/commit/f493929ab88d2ea137ca5fbff70bdc6c9d804d80), [`5911fa9`](https://github.com/Effect-TS/effect/commit/5911fa9c9440dd3bc1ee38542bcd15f8c75a4637)]: + - @effect/schema@0.60.3 + - @effect/platform@0.42.3 + - @effect/platform-node@0.41.4 + +## 0.6.6 + +### Patch Changes + +- [#1926](https://github.com/Effect-TS/effect/pull/1926) [`169bc30`](https://github.com/Effect-TS/effect/commit/169bc3011ef8001fb75d844a46d8b7954131451b) Thanks [@tim-smart](https://github.com/tim-smart)! - use sliding queue for DevTools client + +## 0.6.5 + +### Patch Changes + +- Updated dependencies [[`05c44b3`](https://github.com/Effect-TS/effect/commit/05c44b30662554dde50b70bad79f13ae895fda02), [`05c44b3`](https://github.com/Effect-TS/effect/commit/05c44b30662554dde50b70bad79f13ae895fda02), [`05c44b3`](https://github.com/Effect-TS/effect/commit/05c44b30662554dde50b70bad79f13ae895fda02), [`05c44b3`](https://github.com/Effect-TS/effect/commit/05c44b30662554dde50b70bad79f13ae895fda02), [`05c44b3`](https://github.com/Effect-TS/effect/commit/05c44b30662554dde50b70bad79f13ae895fda02), [`05c44b3`](https://github.com/Effect-TS/effect/commit/05c44b30662554dde50b70bad79f13ae895fda02)]: + - effect@2.1.0 + - @effect/platform@0.42.2 + - @effect/platform-node@0.41.3 + - @effect/schema@0.60.2 + +## 0.6.4 + +### Patch Changes + +- Updated dependencies [[`f7f19f6`](https://github.com/Effect-TS/effect/commit/f7f19f66a5fa349baa2412c1f9f15111c437df09)]: + - effect@2.0.5 + - @effect/platform@0.42.1 + - @effect/platform-node@0.41.2 + - @effect/schema@0.60.1 + +## 0.6.3 + +### Patch Changes + +- Updated dependencies [[`ec2bdfa`](https://github.com/Effect-TS/effect/commit/ec2bdfae2da717f28147b9d6820d3494cb240945), [`687e02e`](https://github.com/Effect-TS/effect/commit/687e02e7d84dc06957844160761fda90929470ab), [`536c1df`](https://github.com/Effect-TS/effect/commit/536c1dfb7833961dfb2fbd6bcd2dbdfa2f208d51), [`8eec87e`](https://github.com/Effect-TS/effect/commit/8eec87e311ce55281a98517e6df0ef103b43e8a8), [`540b294`](https://github.com/Effect-TS/effect/commit/540b2941dd0a81e9688311583ce7e2e140d6e7a5), [`8eec87e`](https://github.com/Effect-TS/effect/commit/8eec87e311ce55281a98517e6df0ef103b43e8a8), [`536c1df`](https://github.com/Effect-TS/effect/commit/536c1dfb7833961dfb2fbd6bcd2dbdfa2f208d51), [`a3f96d6`](https://github.com/Effect-TS/effect/commit/a3f96d615b8b3e238dbfa01ef713c87e6f4532be), [`71ed54c`](https://github.com/Effect-TS/effect/commit/71ed54c3fbb1ead5da2776bc6207050cb073ada4), [`0c397e7`](https://github.com/Effect-TS/effect/commit/0c397e762008a0de40c7526c9d99ff2cfe4f7a6a), [`8eec87e`](https://github.com/Effect-TS/effect/commit/8eec87e311ce55281a98517e6df0ef103b43e8a8), [`b557a10`](https://github.com/Effect-TS/effect/commit/b557a10b773e321bea77fc4951f0ef171dd193c9), [`536c1df`](https://github.com/Effect-TS/effect/commit/536c1dfb7833961dfb2fbd6bcd2dbdfa2f208d51), [`8eec87e`](https://github.com/Effect-TS/effect/commit/8eec87e311ce55281a98517e6df0ef103b43e8a8), [`8eec87e`](https://github.com/Effect-TS/effect/commit/8eec87e311ce55281a98517e6df0ef103b43e8a8), [`74b9094`](https://github.com/Effect-TS/effect/commit/74b90940e571c73a6b76cafa88ffb8a1c949cb4c), [`337e80f`](https://github.com/Effect-TS/effect/commit/337e80f69bc36966f889c439b819db2f84cae496), [`25adce7`](https://github.com/Effect-TS/effect/commit/25adce7ae76ce834096dca1ed70a60ad1a349217), [`536c1df`](https://github.com/Effect-TS/effect/commit/536c1dfb7833961dfb2fbd6bcd2dbdfa2f208d51), [`48a3d40`](https://github.com/Effect-TS/effect/commit/48a3d40aed0f923f567b8911dade732ff472d981)]: + - @effect/schema@0.60.0 + - effect@2.0.4 + - @effect/platform-node@0.41.1 + - @effect/platform@0.42.0 + +## 0.6.2 + +### Patch Changes + +- Updated dependencies [[`5b46e99`](https://github.com/Effect-TS/effect/commit/5b46e996d30e2497eb23095e2c21eee04438edf5), [`87f7ef2`](https://github.com/Effect-TS/effect/commit/87f7ef28a3c27e2e4f2fcfa465f85bb2a45a3d6b), [`1d3a06b`](https://github.com/Effect-TS/effect/commit/1d3a06bb58ad1ac123ae8f9d42b4345f9c9c53c0), [`210d27e`](https://github.com/Effect-TS/effect/commit/210d27e999e066ea9b907301150c65f9ff080b39), [`1d3a06b`](https://github.com/Effect-TS/effect/commit/1d3a06bb58ad1ac123ae8f9d42b4345f9c9c53c0)]: + - @effect/schema@0.59.1 + - effect@2.0.3 + - @effect/platform-node@0.41.0 + - @effect/platform@0.41.0 + +## 0.6.1 + +### Patch Changes + +- Updated dependencies [[`c4b84f7`](https://github.com/Effect-TS/effect/commit/c4b84f724ae809f3450d71c3ea5d629205fc479f), [`c4b84f7`](https://github.com/Effect-TS/effect/commit/c4b84f724ae809f3450d71c3ea5d629205fc479f), [`c4b84f7`](https://github.com/Effect-TS/effect/commit/c4b84f724ae809f3450d71c3ea5d629205fc479f), [`c4b84f7`](https://github.com/Effect-TS/effect/commit/c4b84f724ae809f3450d71c3ea5d629205fc479f)]: + - @effect/schema@0.59.0 + - @effect/platform@0.40.4 + - @effect/platform-node@0.40.4 + +## 0.6.0 + +### Minor Changes + +- [#1842](https://github.com/Effect-TS/effect/pull/1842) [`7b2f874`](https://github.com/Effect-TS/effect/commit/7b2f8743d96753c3e24ac4cc6715a4a7f4a2ca0c) Thanks [@fubhy](https://github.com/fubhy)! - Schema: refactor `ParseResult` module: + - add `Union` issue, and replace `UnionMember` with `Union` + - add `Tuple` issue, and replace `Index` with `Tuple` + - add `TypeLiteral` issue + - add `Transform` issue + - add `Refinement` issue + - add `ast` field to `Member` + - rename `UnionMember` to `Member` + - `Type`: rename `expected` to `ast` + - `ParseError` replace `errors` field with `error` field and refactor `parseError` constructor accordingly + - `Index` replace `errors` field with `error` field + - `Key` replace `errors` field with `error` field + - `Member` replace `errors` field with `error` field + - `ParseError` replace `errors` field with `error` field + - make `ParseError` a `Data.TaggedError` + - `Forbidden`: add `actual` field + +### Patch Changes + +- Updated dependencies [[`7b2f874`](https://github.com/Effect-TS/effect/commit/7b2f8743d96753c3e24ac4cc6715a4a7f4a2ca0c), [`a904a73`](https://github.com/Effect-TS/effect/commit/a904a739459bfd0fa7844b00b902d2fa984fb014), [`7b2f874`](https://github.com/Effect-TS/effect/commit/7b2f8743d96753c3e24ac4cc6715a4a7f4a2ca0c), [`7b2f874`](https://github.com/Effect-TS/effect/commit/7b2f8743d96753c3e24ac4cc6715a4a7f4a2ca0c), [`92c0322`](https://github.com/Effect-TS/effect/commit/92c0322a58bf7e5b8dbb602186030839e89df5af), [`7b2f874`](https://github.com/Effect-TS/effect/commit/7b2f8743d96753c3e24ac4cc6715a4a7f4a2ca0c), [`7b2f874`](https://github.com/Effect-TS/effect/commit/7b2f8743d96753c3e24ac4cc6715a4a7f4a2ca0c)]: + - @effect/schema@0.58.0 + - @effect/platform-node@0.40.3 + - @effect/platform@0.40.3 + +## 0.5.6 + +### Patch Changes + +- Updated dependencies [[`4c90c54`](https://github.com/Effect-TS/effect/commit/4c90c54d87c91f75f3ad114926cdf3b0c25df091), [`d5a1949`](https://github.com/Effect-TS/effect/commit/d5a19499aac7c1d147674a35ac69992177c7536c), [`d3d3bda`](https://github.com/Effect-TS/effect/commit/d3d3bda74c794153def9027e0c40896e72cd5d14)]: + - @effect/platform@0.40.2 + - effect@2.0.2 + - @effect/platform-node@0.40.2 + - @effect/schema@0.57.2 + +## 0.5.5 + +### Patch Changes + +- Updated dependencies [[`16bd87d`](https://github.com/Effect-TS/effect/commit/16bd87d32611b966dc42ea4fc979764f97a49071)]: + - effect@2.0.1 + - @effect/platform@0.40.1 + - @effect/platform-node@0.40.1 + - @effect/schema@0.57.1 + +## 0.5.4 + +### Patch Changes + +- [#1849](https://github.com/Effect-TS/effect/pull/1849) [`389a8b1`](https://github.com/Effect-TS/effect/commit/389a8b1c7fbbb1e024dfaf56f13dc2c99dc2af9b) Thanks [@fubhy](https://github.com/fubhy)! - Add `/experimental` package + +- Updated dependencies [[`d987daa`](https://github.com/Effect-TS/effect/commit/d987daafaddd43b6ade74916a08236c19ea0a9fa), [`7b5eaa3`](https://github.com/Effect-TS/effect/commit/7b5eaa3838c79bf4bdccf91b94d61bbc38a2ec95), [`0724211`](https://github.com/Effect-TS/effect/commit/072421149c36010748ff6b6ee19c15c6cffefe09), [`9f2bc5a`](https://github.com/Effect-TS/effect/commit/9f2bc5a19e0b678a0a85e84daac290922b0fd57d), [`04fb8b4`](https://github.com/Effect-TS/effect/commit/04fb8b428b19bba85a2c79910c5e363340d074e7), [`d0471ca`](https://github.com/Effect-TS/effect/commit/d0471ca7b544746674b9e1750202da72b0a21233), [`bcf0900`](https://github.com/Effect-TS/effect/commit/bcf0900b58f449262556f80bff21e771a37272aa), [`99d22cb`](https://github.com/Effect-TS/effect/commit/99d22cbee13cc2111a4a634cbe73b9b7d7fd88c7), [`6299b84`](https://github.com/Effect-TS/effect/commit/6299b84c11e5d1fe79fa538df8935018c7613747), [`c0aeb5e`](https://github.com/Effect-TS/effect/commit/c0aeb5e302869bcd7d7627f8cc5b630d07c12d10), [`693b8f3`](https://github.com/Effect-TS/effect/commit/693b8f3a3dfd43ae61f0d9292cdf356be7329f2f)]: + - @effect/platform-node@0.40.0 + - @effect/platform@0.40.0 + - @effect/schema@0.57.0 + - effect@2.0.0 + +## 0.5.3 + +### Patch Changes + +- [#42](https://github.com/Effect-TS/experimental/pull/42) [`fcf22ce`](https://github.com/Effect-TS/experimental/commit/fcf22ce465439ee276fea654cb4e4b97393df6c6) Thanks [@tim-smart](https://github.com/tim-smart)! - switch to handler api for socket and socket server + +## 0.5.2 + +### Patch Changes + +- [#40](https://github.com/Effect-TS/experimental/pull/40) [`04b8b17`](https://github.com/Effect-TS/experimental/commit/04b8b17715ae35ef774109be0f2b86f9d6390792) Thanks [@tim-smart](https://github.com/tim-smart)! - add source to SocketServer sockets + +## 0.5.1 + +### Patch Changes + +- [#37](https://github.com/Effect-TS/experimental/pull/37) [`9659fbe`](https://github.com/Effect-TS/experimental/commit/9659fbe13bec5270b0cc5e8035a89cfb17d4b6af) Thanks [@tim-smart](https://github.com/tim-smart)! - fix use of timeout + +## 0.5.0 + +### Minor Changes + +- [#35](https://github.com/Effect-TS/experimental/pull/35) [`c9366db`](https://github.com/Effect-TS/experimental/commit/c9366db09b15b802a6272b6e81c5cfd2c7507595) Thanks [@tim-smart](https://github.com/tim-smart)! - update effect + +## 0.4.0 + +### Minor Changes + +- [#33](https://github.com/Effect-TS/experimental/pull/33) [`d716090`](https://github.com/Effect-TS/experimental/commit/d716090f539b56d5093a82a10dd5fd6b2b369e86) Thanks [@tim-smart](https://github.com/tim-smart)! - update effect + +## 0.3.2 + +### Patch Changes + +- [#31](https://github.com/Effect-TS/experimental/pull/31) [`216e3d3`](https://github.com/Effect-TS/experimental/commit/216e3d31867124d2d38676b526830f4a4c7d7c5d) Thanks [@tim-smart](https://github.com/tim-smart)! - update /platform + +## 0.3.1 + +### Patch Changes + +- [#29](https://github.com/Effect-TS/experimental/pull/29) [`7f346ac`](https://github.com/Effect-TS/experimental/commit/7f346ac95ad2b31e07f1494e8c78e66b2e1bdb40) Thanks [@tim-smart](https://github.com/tim-smart)! - add metrics to dev tools + +## 0.3.0 + +### Minor Changes + +- [#27](https://github.com/Effect-TS/experimental/pull/27) [`6618248`](https://github.com/Effect-TS/experimental/commit/6618248e0f2f4aec2330aa9092bf05b55c763164) Thanks [@tim-smart](https://github.com/tim-smart)! - update effect + +## 0.2.8 + +### Patch Changes + +- [#25](https://github.com/Effect-TS/experimental/pull/25) [`e8d5e6d`](https://github.com/Effect-TS/experimental/commit/e8d5e6d5fdc529d55bd3e3c6d9a4bc59dedd5060) Thanks [@tim-smart](https://github.com/tim-smart)! - fix server interrupt + +## 0.2.7 + +### Patch Changes + +- [#23](https://github.com/Effect-TS/experimental/pull/23) [`fc89b83`](https://github.com/Effect-TS/experimental/commit/fc89b8351386bef64638b3880d7383f6a7e6dee3) Thanks [@tim-smart](https://github.com/tim-smart)! - seperate DevTools modules + +## 0.2.6 + +### Patch Changes + +- [#20](https://github.com/Effect-TS/experimental/pull/20) [`963f717`](https://github.com/Effect-TS/experimental/commit/963f717d4c9f231660f9145185b957edfd30abbd) Thanks [@tim-smart](https://github.com/tim-smart)! - add websocket server + +## 0.2.5 + +### Patch Changes + +- [#18](https://github.com/Effect-TS/experimental/pull/18) [`26e8dca`](https://github.com/Effect-TS/experimental/commit/26e8dcaf850ae362bc554422241aed5d4ad88e10) Thanks [@tim-smart](https://github.com/tim-smart)! - remove duplicate onmessage in ws + +- [#18](https://github.com/Effect-TS/experimental/pull/18) [`26e8dca`](https://github.com/Effect-TS/experimental/commit/26e8dcaf850ae362bc554422241aed5d4ad88e10) Thanks [@tim-smart](https://github.com/tim-smart)! - add DevTools module + +- [#18](https://github.com/Effect-TS/experimental/pull/18) [`26e8dca`](https://github.com/Effect-TS/experimental/commit/26e8dcaf850ae362bc554422241aed5d4ad88e10) Thanks [@tim-smart](https://github.com/tim-smart)! - add SocketServer.run + +## 0.2.4 + +### Patch Changes + +- [#16](https://github.com/Effect-TS/experimental/pull/16) [`56f4f76`](https://github.com/Effect-TS/experimental/commit/56f4f76cb6391d8d30a7bc654b9e6bfbcfa7961e) Thanks [@tim-smart](https://github.com/tim-smart)! - add SocketServer module + +- [#16](https://github.com/Effect-TS/experimental/pull/16) [`56f4f76`](https://github.com/Effect-TS/experimental/commit/56f4f76cb6391d8d30a7bc654b9e6bfbcfa7961e) Thanks [@tim-smart](https://github.com/tim-smart)! - add run to Socket to make it retryable + +## 0.2.3 + +### Patch Changes + +- [#13](https://github.com/Effect-TS/experimental/pull/13) [`8ffa6e1`](https://github.com/Effect-TS/experimental/commit/8ffa6e1afa0ec9f672cbbba3bb680f3126bab7ec) Thanks [@tim-smart](https://github.com/tim-smart)! - add WebSocket + +## 0.2.2 + +### Patch Changes + +- [#11](https://github.com/Effect-TS/experimental/pull/11) [`3a83e5a`](https://github.com/Effect-TS/experimental/commit/3a83e5af3f38f676abccd532e69289aaf0cebceb) Thanks [@tim-smart](https://github.com/tim-smart)! - refactor Socket api + +## 0.2.1 + +### Patch Changes + +- [#9](https://github.com/Effect-TS/experimental/pull/9) [`b7f0163`](https://github.com/Effect-TS/experimental/commit/b7f0163d4b98f14705e8b810521ed800d4d2874a) Thanks [@tim-smart](https://github.com/tim-smart)! - add support for platform key value store + +## 0.2.0 + +### Minor Changes + +- [#8](https://github.com/Effect-TS/experimental/pull/8) [`a39bfc5`](https://github.com/Effect-TS/experimental/commit/a39bfc5ba19fe9e574b7063e5c98bae8944bae4b) Thanks [@tim-smart](https://github.com/tim-smart)! - use Schema/Serializable for peristence + +## 0.1.1 + +### Patch Changes + +- [#3](https://github.com/Effect-TS/experimental/pull/3) [`7ab9e0b`](https://github.com/Effect-TS/experimental/commit/7ab9e0b3f015f271210c75c90c76f294918d786c) Thanks [@tim-smart](https://github.com/tim-smart)! - add Socket module + +- [#3](https://github.com/Effect-TS/experimental/pull/3) [`7ab9e0b`](https://github.com/Effect-TS/experimental/commit/7ab9e0b3f015f271210c75c90c76f294918d786c) Thanks [@tim-smart](https://github.com/tim-smart)! - add MsgPack module + +## 0.1.0 + +### Minor Changes + +- [#1](https://github.com/Effect-TS/experimental/pull/1) [`ff79b4b`](https://github.com/Effect-TS/experimental/commit/ff79b4bda01acfc5753c44e812ac36852130d4d8) Thanks [@tim-smart](https://github.com/tim-smart)! - add RequestResolver & Persistance diff --git a/repos/effect/packages/experimental/LICENSE b/repos/effect/packages/experimental/LICENSE new file mode 100644 index 0000000..7f6fe48 --- /dev/null +++ b/repos/effect/packages/experimental/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023-present The Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/repos/effect/packages/experimental/README.md b/repos/effect/packages/experimental/README.md new file mode 100644 index 0000000..afeb291 --- /dev/null +++ b/repos/effect/packages/experimental/README.md @@ -0,0 +1,5 @@ +# `@effect/experimental` + +## Documentation + +- **API Reference**: [View the full documentation](https://effect-ts.github.io/effect/docs/experimental). diff --git a/repos/effect/packages/experimental/docgen.json b/repos/effect/packages/experimental/docgen.json new file mode 100644 index 0000000..e684c3e --- /dev/null +++ b/repos/effect/packages/experimental/docgen.json @@ -0,0 +1,5 @@ +{ + "$schema": "../../node_modules/@effect/docgen/schema.json", + "srcLink": "https://github.com/Effect-TS/effect/tree/main/packages/experimental/src/", + "exclude": ["src/internal/**/*.ts"] +} diff --git a/repos/effect/packages/experimental/examples/dev-tools.ts b/repos/effect/packages/experimental/examples/dev-tools.ts new file mode 100644 index 0000000..f4ec73d --- /dev/null +++ b/repos/effect/packages/experimental/examples/dev-tools.ts @@ -0,0 +1,18 @@ +import * as DevTools from "@effect/experimental/DevTools" +import { NodeRuntime, NodeSocket } from "@effect/platform-node" +import { Effect, Layer } from "effect" + +const program = Effect.log("Hello!").pipe( + Effect.delay(2000), + Effect.withSpan("Hi", { attributes: { foo: "bar" } }), + Effect.forever +) + +program.pipe( + Effect.provide( + DevTools.layerWebSocket().pipe( + Layer.provide(NodeSocket.layerWebSocketConstructor) + ) + ), + NodeRuntime.runMain +) diff --git a/repos/effect/packages/experimental/examples/machine.ts b/repos/effect/packages/experimental/examples/machine.ts new file mode 100644 index 0000000..3375ab0 --- /dev/null +++ b/repos/effect/packages/experimental/examples/machine.ts @@ -0,0 +1,76 @@ +import { Machine } from "@effect/experimental" +import { runMain } from "@effect/platform-node/NodeRuntime" +import { Data, Effect, List, pipe, Request, Schedule } from "effect" + +class SendError extends Data.TaggedError("SendError")<{ + readonly email: string + readonly reason: string +}> {} + +class SendEmail extends Request.TaggedClass("SendEmail")< + void, + SendError, + { + readonly email: string + readonly message: string + } +> {} + +class ProcessEmail extends Request.TaggedClass("ProcessEmail")< + void, + never, + {} +> {} + +class Shutdown extends Request.TaggedClass("Shutdown")< + void, + never, + {} +> {} + +const mailer = Machine.makeWith>()((_, previous) => + Effect.gen(function*() { + const ctx = yield* Machine.MachineContext + const state = previous ?? List.empty() + + if (List.isCons(state)) { + yield* ctx.unsafeSend(new ProcessEmail()).pipe(Effect.replicateEffect(List.size(state))) + } + + return Machine.procedures.make(state).pipe( + Machine.procedures.addPrivate()("ProcessEmail", ({ state }) => + Effect.gen(function*() { + if (List.isNil(state)) { + return [void 0, state] + } + const req = state.head + yield* Effect.log(`Sending email to ${req.email}`).pipe(Effect.delay(500)) + return [void 0, state.tail] + })), + Machine.procedures.add()("SendEmail", (ctx) => + ctx.send(new ProcessEmail()).pipe( + Effect.as([void 0, List.append(ctx.state, ctx.request)]) + )), + Machine.procedures.add()("Shutdown", () => + Effect.log("Shutting down").pipe( + Effect.zipRight(Effect.interrupt) + )) + ) + }) +).pipe( + Machine.retry(Schedule.forever) +) + +const program = Effect.gen(function*() { + const actor = yield* Machine.boot(mailer) + yield* actor.send(new SendEmail({ email: "test@example.com", message: "Hello, World!" })) + yield* actor.send(new SendEmail({ email: "test@example.com", message: "Hello, World!" })) + yield* actor.send(new SendEmail({ email: "test@example.com", message: "Hello, World!" })) + yield* actor.send(new Shutdown()) +}) + +pipe( + program, + Effect.scoped, + runMain +) diff --git a/repos/effect/packages/experimental/examples/rate-limiter.ts b/repos/effect/packages/experimental/examples/rate-limiter.ts new file mode 100644 index 0000000..6b29c25 --- /dev/null +++ b/repos/effect/packages/experimental/examples/rate-limiter.ts @@ -0,0 +1,69 @@ +import { RateLimiter } from "@effect/experimental" +import * as RedisRateLimiter from "@effect/experimental/RateLimiter/Redis" +import { NodeRuntime } from "@effect/platform-node" +import { Effect, Layer } from "effect" + +// create a RateLimiter layer using Redis as the backing store. +// +// You can also use RateLimiter.layerStoreMemory for an in-memory store. +const RateLimiterLayer = RateLimiter.layer.pipe( + Layer.provide(RedisRateLimiter.layerStore({ + host: "localhost", + port: 6379 + })) +) + +Effect.gen(function*() { + const limiter = yield* RateLimiter.RateLimiter + + // the `consume` effect will attempt to consume a token from the rate limiter. + // + // When `onExceeded` is set to "delay", the effect will return the time to + // wait for the token to become available. + const consume = limiter.consume({ + algorithm: "token-bucket", + onExceeded: "delay", + window: "10 seconds", + limit: 5, + key: "user-123" + }) + + // `consume` returns the metadata about the rate limiting operation. + // + // ``` + // { + // delay: { _id: 'Duration', _tag: 'Millis', millis: 0 }, + // limit: 5, + // remaining: 4, + // resetAfter: { _id: 'Duration', _tag: 'Millis', millis: 2000 } + // } + // ``` + console.log(yield* consume) + + // If `onExceeded` is set to "fail", the effect will fail with a + // RateLimiter.RateLimitExceeded error when the limit is exceeded. + yield* limiter.consume({ + algorithm: "token-bucket", + onExceeded: "fail", + window: "10 seconds", + limit: 5, + key: "user-123" + }) + + // You can also use `RateLimiter.makeWithRateLimiter` to access a function + // that applies rate limiting to an effect. + const withRateLimiter = yield* RateLimiter.makeWithRateLimiter + + yield* Effect.log("Attempting rate limited operation").pipe( + withRateLimiter({ + algorithm: "token-bucket", + onExceeded: "delay", + window: "10 seconds", + limit: 5, + key: "user-123" + }) + ) +}).pipe( + Effect.provide(RateLimiterLayer), + NodeRuntime.runMain +) diff --git a/repos/effect/packages/experimental/examples/redis/docker-compose.yaml b/repos/effect/packages/experimental/examples/redis/docker-compose.yaml new file mode 100644 index 0000000..14e7625 --- /dev/null +++ b/repos/effect/packages/experimental/examples/redis/docker-compose.yaml @@ -0,0 +1,5 @@ +services: + redis: + image: redis:alpine + ports: + - "6379:6379" diff --git a/repos/effect/packages/experimental/examples/redis/resolver.ts b/repos/effect/packages/experimental/examples/redis/resolver.ts new file mode 100644 index 0000000..ae8190c --- /dev/null +++ b/repos/effect/packages/experimental/examples/redis/resolver.ts @@ -0,0 +1,46 @@ +import * as Redis from "@effect/experimental/Persistence/Redis" +import { persisted } from "@effect/experimental/RequestResolver" +import { runMain } from "@effect/platform-node/NodeRuntime" +import { Array, Effect, Exit, pipe, PrimaryKey, RequestResolver, Schema } from "effect" + +class User extends Schema.Class("User")({ + id: Schema.Number, + name: Schema.String +}) {} + +class GetUserById extends Schema.TaggedRequest()("GetUserById", { + failure: Schema.String, + success: User, + payload: { + id: Schema.Number + } +}) { + [PrimaryKey.symbol]() { + return `GetUserById:${this.id}` + } +} + +const program = Effect.gen(function*() { + const resolver = yield* RequestResolver.fromEffectTagged()({ + GetUserById: (reqs) => { + console.log("uncached requests", reqs.length) + return Effect.forEach(reqs, (req) => Effect.succeed(new User({ id: req.id, name: "John" }))) + } + }).pipe(persisted({ + storeId: "users", + timeToLive: (_req, exit) => Exit.isSuccess(exit) ? 30000 : 0 + })) + + const users = yield* Effect.forEach(Array.range(1, 5), (id) => Effect.request(new GetUserById({ id }), resolver), { + batching: true + }) + + console.log(users) +}) + +pipe( + program, + Effect.scoped, + Effect.provide(Redis.layerResult({})), + runMain +) diff --git a/repos/effect/packages/experimental/examples/serializable-machine.ts b/repos/effect/packages/experimental/examples/serializable-machine.ts new file mode 100644 index 0000000..4e5c42e --- /dev/null +++ b/repos/effect/packages/experimental/examples/serializable-machine.ts @@ -0,0 +1,81 @@ +import { Machine } from "@effect/experimental" +import { runMain } from "@effect/platform-node/NodeRuntime" +import { Effect, List, pipe, Schedule, Schema } from "effect" + +class SendError extends Schema.TaggedError()( + "SendError", + { + email: Schema.String, + reason: Schema.String + } +) {} + +class SendEmail extends Schema.TaggedRequest()( + "SendEmail", + { + failure: SendError, + success: Schema.Void, + payload: { + email: Schema.String, + message: Schema.String + } + } +) {} + +class ProcessEmail extends Schema.TaggedRequest()( + "ProcessEmail", + { failure: Schema.Never, success: Schema.Void, payload: {} } +) {} + +class Shutdown extends Schema.TaggedRequest()( + "Shutdown", + { failure: Schema.Never, success: Schema.Void, payload: {} } +) {} + +const mailer = Machine.makeSerializable({ + state: Schema.List(SendEmail) +}, (_, previous) => + Effect.gen(function*() { + const ctx = yield* Machine.MachineContext + const state = previous ?? List.empty() + + if (List.isCons(state)) { + yield* ctx.unsafeSend(new ProcessEmail()).pipe(Effect.replicateEffect(List.size(state))) + } + + return Machine.serializable.make(state).pipe( + Machine.serializable.addPrivate(ProcessEmail, ({ state }) => + Effect.gen(function*() { + if (List.isNil(state)) { + return [void 0, state] + } + const req = state.head + yield* Effect.log(`Sending email to ${req.email}`).pipe(Effect.delay(500)) + return [void 0, state.tail] + })), + Machine.serializable.add(SendEmail, (ctx) => + ctx.send(new ProcessEmail()).pipe( + Effect.as([void 0, List.append(ctx.state, ctx.request)]) + )), + Machine.serializable.add(Shutdown, () => + Effect.log("Shutting down").pipe( + Effect.zipRight(Effect.interrupt) + )) + ) + })).pipe( + Machine.retry(Schedule.forever) + ) + +const program = Effect.gen(function*() { + const actor = yield* Machine.boot(mailer) + yield* actor.send(new SendEmail({ email: "test@example.com", message: "Hello, World!" })) + yield* actor.send(new SendEmail({ email: "test@example.com", message: "Hello, World!" })) + yield* actor.send(new SendEmail({ email: "test@example.com", message: "Hello, World!" })) + yield* actor.send(new Shutdown()) +}) + +pipe( + program, + Effect.scoped, + runMain +) diff --git a/repos/effect/packages/experimental/package.json b/repos/effect/packages/experimental/package.json new file mode 100644 index 0000000..dc5be64 --- /dev/null +++ b/repos/effect/packages/experimental/package.json @@ -0,0 +1,73 @@ +{ + "name": "@effect/experimental", + "version": "0.60.0", + "type": "module", + "license": "MIT", + "description": "Experimental modules for the Effect ecosystem", + "homepage": "https://effect.website", + "repository": { + "type": "git", + "url": "https://github.com/Effect-TS/effect.git", + "directory": "packages/experimental" + }, + "bugs": { + "url": "https://github.com/Effect-TS/effect/issues" + }, + "tags": [ + "typescript", + "algebraic-data-types", + "functional-programming" + ], + "keywords": [ + "typescript", + "algebraic-data-types", + "functional-programming" + ], + "publishConfig": { + "access": "public", + "provenance": true, + "directory": "dist", + "linkDirectory": false + }, + "exports": { + "./package.json": "./package.json", + ".": "./src/index.ts", + "./*": "./src/*.ts", + "./internal/*": null + }, + "scripts": { + "codegen": "build-utils prepare-v3", + "build": "pnpm build-esm && pnpm build-annotate && pnpm build-cjs && build-utils pack-v3", + "build-esm": "tsc -b tsconfig.build.json", + "build-cjs": "babel build/esm --plugins @babel/transform-export-namespace-from --plugins @babel/transform-modules-commonjs --out-dir build/cjs --source-maps", + "build-annotate": "babel build/esm --plugins annotate-pure-calls --out-dir build/esm --source-maps", + "check": "tsc -b tsconfig.json", + "test": "vitest", + "coverage": "vitest --coverage" + }, + "dependencies": { + "uuid": "^11.0.3" + }, + "peerDependencies": { + "@effect/platform": "workspace:^", + "effect": "workspace:^", + "ioredis": "^5", + "lmdb": "^3" + }, + "peerDependenciesMeta": { + "ioredis": { + "optional": true + }, + "lmdb": { + "optional": true + } + }, + "devDependencies": { + "@cloudflare/workers-types": "^4.20250515.0", + "@effect/platform-node": "workspace:^", + "@testcontainers/redis": "^11.8.1", + "@types/ws": "^8.18.1", + "ioredis": "^5.6.1", + "lmdb": "^3.3.0" + } +} diff --git a/repos/effect/packages/experimental/src/DevTools.ts b/repos/effect/packages/experimental/src/DevTools.ts new file mode 100644 index 0000000..04a8b7f --- /dev/null +++ b/repos/effect/packages/experimental/src/DevTools.ts @@ -0,0 +1,30 @@ +/** + * @since 1.0.0 + */ +import * as Socket from "@effect/platform/Socket" +import * as Layer from "effect/Layer" +import * as Client from "./DevTools/Client.js" + +/** + * @since 1.0.0 + * @category layers + */ +export const layerSocket: Layer.Layer = Client.layerTracer + +/** + * @since 1.0.0 + * @category layers + */ +export const layerWebSocket = (url = "ws://localhost:34437"): Layer.Layer => + Client.layerTracer.pipe( + Layer.provide(Socket.layerWebSocket(url)) + ) + +/** + * @since 1.0.0 + * @category layers + */ +export const layer = (url = "ws://localhost:34437"): Layer.Layer => + layerWebSocket(url).pipe( + Layer.provide(Socket.layerWebSocketConstructorGlobal) + ) diff --git a/repos/effect/packages/experimental/src/DevTools/Client.ts b/repos/effect/packages/experimental/src/DevTools/Client.ts new file mode 100644 index 0000000..aa77cfe --- /dev/null +++ b/repos/effect/packages/experimental/src/DevTools/Client.ts @@ -0,0 +1,218 @@ +/** + * @since 1.0.0 + */ +import * as Ndjson from "@effect/platform/Ndjson" +import * as Socket from "@effect/platform/Socket" +import * as Cause from "effect/Cause" +import * as Context from "effect/Context" +import * as Deferred from "effect/Deferred" +import * as Effect from "effect/Effect" +import * as Exit from "effect/Exit" +import { pipe } from "effect/Function" +import * as Layer from "effect/Layer" +import * as Mailbox from "effect/Mailbox" +import * as Metric from "effect/Metric" +import * as MetricState from "effect/MetricState" +import * as Schedule from "effect/Schedule" +import type * as Scope from "effect/Scope" +import * as Stream from "effect/Stream" +import * as Tracer from "effect/Tracer" +import * as Domain from "./Domain.js" + +/** + * @since 1.0.0 + * @category models + */ +export interface ClientImpl { + readonly unsafeAddSpan: (_: Domain.Span | Domain.SpanEvent) => void +} + +/** + * @since 1.0.0 + * @category models + */ +export interface Client { + readonly _: unique symbol +} + +/** + * @since 1.0.0 + * @category tags + */ +export const Client = Context.GenericTag("@effect/experimental/DevTools/Client") + +/** + * @since 1.0.0 + * @category constructors + */ +export const make: Effect.Effect = Effect.gen(function*() { + const socket = yield* Socket.Socket + const requests = yield* Mailbox.make() + + function metricsSnapshot(): Domain.MetricsSnapshot { + const snapshot = Metric.unsafeSnapshot() + const metrics: Array = [] + + for (let i = 0, len = snapshot.length; i < len; i++) { + const metricPair = snapshot[i] + if (MetricState.isCounterState(metricPair.metricState)) { + metrics.push({ + _tag: "Counter", + name: metricPair.metricKey.name, + description: metricPair.metricKey.description, + tags: metricPair.metricKey.tags, + state: metricPair.metricState + }) + } else if (MetricState.isGaugeState(metricPair.metricState)) { + metrics.push({ + _tag: "Gauge", + name: metricPair.metricKey.name, + description: metricPair.metricKey.description, + tags: metricPair.metricKey.tags, + state: metricPair.metricState + }) + } else if (MetricState.isHistogramState(metricPair.metricState)) { + metrics.push({ + _tag: "Histogram", + name: metricPair.metricKey.name, + description: metricPair.metricKey.description, + tags: metricPair.metricKey.tags, + state: metricPair.metricState + }) + } else if (MetricState.isSummaryState(metricPair.metricState)) { + metrics.push({ + _tag: "Summary", + name: metricPair.metricKey.name, + description: metricPair.metricKey.description, + tags: metricPair.metricKey.tags, + state: metricPair.metricState + }) + } else if (MetricState.isFrequencyState(metricPair.metricState)) { + metrics.push({ + _tag: "Frequency", + name: metricPair.metricKey.name, + description: metricPair.metricKey.description, + tags: metricPair.metricKey.tags, + state: { + occurrences: Object.fromEntries(metricPair.metricState.occurrences.entries()) + } + }) + } + } + + return { + _tag: "MetricsSnapshot", + metrics + } + } + + const connected = yield* Deferred.make() + + yield* Mailbox.toStream(requests).pipe( + Stream.pipeThroughChannel( + Ndjson.duplexSchemaString(Socket.toChannelString(socket), { + inputSchema: Domain.Request, + outputSchema: Domain.Response + }) + ), + Stream.runForEach((req) => { + Deferred.unsafeDone(connected, Exit.void) + switch (req._tag) { + case "MetricsRequest": { + return requests.offer(metricsSnapshot()) + } + case "Pong": { + return Effect.void + } + } + }), + Effect.tapErrorCause(Effect.logDebug), + Effect.retry(Schedule.spaced("1 seconds")), + Effect.forkScoped, + Effect.uninterruptible + ) + + yield* Effect.addFinalizer(() => + requests.offer(metricsSnapshot()).pipe( + Effect.zipRight(Effect.fiberIdWith((id) => requests.failCause(Cause.interrupt(id)))) + ) + ) + yield* requests.offer({ _tag: "Ping" }).pipe( + Effect.delay("3 seconds"), + Effect.forever, + Effect.forkScoped, + Effect.interruptible + ) + + yield* Deferred.await(connected).pipe( + Effect.timeoutOption("1 second") + ) + + return Client.of({ + unsafeAddSpan: (request) => requests.unsafeOffer(request) + }) +}).pipe( + Effect.annotateLogs({ + package: "@effect/experimental", + module: "DevTools", + service: "Client" + }) +) + +/** + * @since 1.0.0 + * @category layers + */ +export const layer: Layer.Layer = Layer.scoped(Client, make) + +/** + * @since 1.0.0 + * @category constructors + */ +export const makeTracer: Effect.Effect = Effect.gen(function*() { + const client = yield* Client + const currentTracer = yield* Effect.tracer + + return Tracer.make({ + span(name, parent, context, links, startTime, kind, options) { + const span = currentTracer.span(name, parent, context, links, startTime, kind, options) + client.unsafeAddSpan(span) + const oldEvent = span.event + span.event = function(this: any, name, startTime, attributes) { + client.unsafeAddSpan({ + _tag: "SpanEvent", + traceId: span.traceId, + spanId: span.spanId, + name, + startTime, + attributes: attributes || {} + }) + return oldEvent.call(this, name, startTime, attributes) + } + const oldEnd = span.end + span.end = function(this: any) { + client.unsafeAddSpan(span) + return oldEnd.apply(this, arguments as any) + } + return span + }, + context: currentTracer.context + }) +}).pipe( + Effect.annotateLogs({ + package: "@effect/experimental", + module: "DevTools", + service: "Tracer" + }) +) + +/** + * @since 1.0.0 + * @category layers + */ +export const layerTracer: Layer.Layer = pipe( + makeTracer, + Effect.map(Layer.setTracer), + Layer.unwrapEffect, + Layer.provide(layer) +) diff --git a/repos/effect/packages/experimental/src/DevTools/Domain.ts b/repos/effect/packages/experimental/src/DevTools/Domain.ts new file mode 100644 index 0000000..a9cdfad --- /dev/null +++ b/repos/effect/packages/experimental/src/DevTools/Domain.ts @@ -0,0 +1,380 @@ +/** + * @since 1.0.0 + */ +import type { Option } from "effect/Option" +import * as Schema from "effect/Schema" + +/** + * @since 1.0.0 + * @category schemas + */ +export const SpanStatusStarted = Schema.Struct({ + _tag: Schema.Literal("Started"), + startTime: Schema.BigInt +}) + +/** + * @since 1.0.0 + * @category schemas + */ +export const SpanStatusEnded = Schema.Struct({ + _tag: Schema.Literal("Ended"), + startTime: Schema.BigInt, + endTime: Schema.BigInt +}) + +/** + * @since 1.0.0 + * @category schemas + */ +export const SpanStatus = Schema.Union(SpanStatusStarted, SpanStatusEnded) + +/** + * @since 1.0.0 + * @category schemas + */ +export const ExternalSpan = Schema.Struct({ + _tag: Schema.Literal("ExternalSpan"), + spanId: Schema.String, + traceId: Schema.String, + sampled: Schema.Boolean +}) + +/** + * @since 1.0.0 + * @category schemas + */ +export interface ExternalSpanFrom extends Schema.Schema.Encoded {} + +/** + * @since 1.0.0 + * @category schemas + */ +export interface ExternalSpan extends Schema.Schema.Type {} + +/** + * @since 1.0.0 + * @category schemas + */ +export const Span: Schema.Schema = Schema.Struct({ + _tag: Schema.Literal("Span"), + spanId: Schema.String, + traceId: Schema.String, + name: Schema.String, + sampled: Schema.Boolean, + attributes: Schema.ReadonlyMap({ key: Schema.String, value: Schema.Unknown }), + status: SpanStatus, + parent: Schema.Option( + Schema.suspend(() => ParentSpan) + // add a title annotation to avoid "Cannot access 'ParentSpan' before initialization" error during module initialization + .annotations({ title: "ParentSpan" }) + ) +}) + +/** + * @since 1.0.0 + * @category schemas + */ +export const SpanEvent: Schema.Schema< + SpanEvent, + { + readonly _tag: "SpanEvent" + readonly spanId: string + readonly traceId: string + readonly name: string + readonly attributes: { readonly [x: string]: unknown } + readonly startTime: string + } +> = Schema.Struct({ + _tag: Schema.Literal("SpanEvent"), + traceId: Schema.String, + spanId: Schema.String, + name: Schema.String, + startTime: Schema.BigInt, + attributes: Schema.Record({ key: Schema.String, value: Schema.Unknown }) +}) + +/** + * @since 1.0.0 + * @category schemas + */ +export const ParentSpan = Schema.Union(Span, ExternalSpan) + +/** + * @since 1.0.0 + * @category schemas + */ +export type ParentSpanFrom = SpanFrom | ExternalSpanFrom + +/** + * @since 1.0.0 + * @category schemas + */ +export type ParentSpan = Span | ExternalSpan + +/** + * @since 1.0.0 + * @category schemas + */ +export interface SpanFrom { + readonly _tag: "Span" + readonly spanId: string + readonly traceId: string + readonly name: string + readonly sampled: boolean + readonly attributes: ReadonlyArray + readonly parent: Schema.OptionEncoded + readonly status: { + readonly _tag: "Started" + readonly startTime: string + } | { + readonly _tag: "Ended" + readonly startTime: string + readonly endTime: string + } +} + +/** + * @since 1.0.0 + * @category schemas + */ +export interface Span { + readonly _tag: "Span" + readonly spanId: string + readonly traceId: string + readonly name: string + readonly sampled: boolean + readonly attributes: ReadonlyMap + readonly parent: Option + readonly status: { + readonly _tag: "Started" + readonly startTime: bigint + } | { + readonly _tag: "Ended" + readonly startTime: bigint + readonly endTime: bigint + } +} + +/** + * @since 1.0.0 + * @category schemas + */ +export interface SpanEvent { + readonly _tag: "SpanEvent" + readonly spanId: string + readonly traceId: string + readonly name: string + readonly attributes: { readonly [x: string]: unknown } + readonly startTime: bigint +} + +/** + * @since 1.0.0 + * @category schemas + */ +export const Ping = Schema.Struct({ + _tag: Schema.Literal("Ping") +}) + +/** + * @since 1.0.0 + * @category schemas + */ +export const Pong = Schema.Struct({ + _tag: Schema.Literal("Pong") +}) + +/** + * @since 1.0.0 + * @category schemas + */ +export const MetricsRequest = Schema.Struct({ + _tag: Schema.Literal("MetricsRequest") +}) + +/** + * @since 1.0.0 + * @category schemas + */ +export const MetricLabel = Schema.Struct({ + key: Schema.String, + value: Schema.String +}) + +/** + * @since 1.0.0 + * @category schemas + */ +export const metric = (tag: Tag, state: Schema.Schema) => + Schema.Struct({ + _tag: Schema.Literal(tag), + name: Schema.String, + description: Schema.optionalWith(Schema.String, { as: "Option" }), + tags: Schema.Array(MetricLabel), + state + }) + +/** + * @since 1.0.0 + * @category schemas + */ +export const Counter = metric( + "Counter", + Schema.Struct({ + count: Schema.Union(Schema.Number, Schema.BigInt) + }) +) + +/** + * @since 1.0.0 + * @category schemas + */ +export const Frequency = metric( + "Frequency", + Schema.Struct({ + occurrences: Schema.Record({ key: Schema.String, value: Schema.Number }) + }) +) + +/** + * @since 1.0.0 + * @category schemas + */ +export const Gauge = metric( + "Gauge", + Schema.Struct({ + value: Schema.Union(Schema.Number, Schema.BigInt) + }) +) + +const numberOrInfinity = Schema.transform( + Schema.Union(Schema.Number, Schema.Null), + Schema.Number, + { + strict: true, + decode: (i) => i === null ? Number.POSITIVE_INFINITY : i, + encode: (i) => Number.isFinite(i) ? i : null + } +) + +/** + * @since 1.0.0 + * @category schemas + */ +export const Histogram = metric( + "Histogram", + Schema.Struct({ + buckets: Schema.Array(Schema.Tuple( + numberOrInfinity, + Schema.Number + )), + count: Schema.Number, + min: Schema.Number, + max: Schema.Number, + sum: Schema.Number + }) +) + +/** + * @since 1.0.0 + * @category schemas + */ +export const Summary = metric( + "Summary", + Schema.Struct({ + error: Schema.Number, + quantiles: Schema.Array(Schema.Tuple(Schema.Number, Schema.Option(Schema.Number))), + count: Schema.Number, + min: Schema.Number, + max: Schema.Number, + sum: Schema.Number + }) +) + +/** + * @since 1.0.0 + * @category schemas + */ +export const Metric = Schema.Union(Counter, Frequency, Gauge, Histogram, Summary) + +/** + * @since 1.0.0 + * @category schemas + */ +export type Metric = Schema.Schema.Type + +/** + * @since 1.0.0 + * @category schemas + */ +export type MetricFrom = Schema.Schema.Encoded + +/** + * @since 1.0.0 + * @category schemas + */ +export const MetricsSnapshot = Schema.Struct({ + _tag: Schema.Literal("MetricsSnapshot"), + metrics: Schema.Array(Metric) +}) + +/** + * @since 1.0.0 + * @category schemas + */ +export type MetricsSnapshot = Schema.Schema.Type + +/** + * @since 1.0.0 + * @category schemas + */ +export type MetricsSnapshotFrom = Schema.Schema.Encoded + +/** + * @since 1.0.0 + * @category schemas + */ +export const Request = Schema.Union(Ping, Span, SpanEvent, MetricsSnapshot) + +/** + * @since 1.0.0 + * @category schemas + */ +export type Request = Schema.Schema.Type + +/** + * @since 1.0.0 + * @category schemas + */ +export declare namespace Request { + /** + * @since 1.0.0 + * @category schemas + */ + export type WithoutPing = Exclude +} + +/** + * @since 1.0.0 + * @category schemas + */ +export const Response = Schema.Union(Pong, MetricsRequest) + +/** + * @since 1.0.0 + * @category schemas + */ +export type Response = Schema.Schema.Type + +/** + * @since 1.0.0 + * @category schemas + */ +export declare namespace Response { + /** + * @since 1.0.0 + * @category schemas + */ + export type WithoutPong = Exclude +} diff --git a/repos/effect/packages/experimental/src/DevTools/Server.ts b/repos/effect/packages/experimental/src/DevTools/Server.ts new file mode 100644 index 0000000..92ef64b --- /dev/null +++ b/repos/effect/packages/experimental/src/DevTools/Server.ts @@ -0,0 +1,56 @@ +/** + * @since 1.0.0 + */ +import * as Ndjson from "@effect/platform/Ndjson" +import * as Socket from "@effect/platform/Socket" +import * as SocketServer from "@effect/platform/SocketServer" +import * as Effect from "effect/Effect" +import * as Mailbox from "effect/Mailbox" +import * as Stream from "effect/Stream" +import * as Domain from "./Domain.js" + +/** + * @since 1.0.0 + * @category models + */ +export interface Client { + readonly queue: Mailbox.ReadonlyMailbox + readonly request: (_: Domain.Response.WithoutPong) => Effect.Effect +} + +/** + * @since 1.0.0 + * @category constructors + */ +export const run = Effect.fnUntraced(function*(handle: (client: Client) => Effect.Effect<_, E, R>) { + const server = yield* SocketServer.SocketServer + return yield* server.run((socket) => + Effect.gen(function*() { + const responses = yield* Mailbox.make() + const requests = yield* Mailbox.make() + + const client: Client = { + queue: requests, + request: (res) => responses.offer(res) + } + + yield* Mailbox.toStream(responses).pipe( + Stream.pipeThroughChannel( + Ndjson.duplexSchemaString(Socket.toChannelString(socket), { + inputSchema: Domain.Response, + outputSchema: Domain.Request + }) + ), + Stream.runForEach((req) => + req._tag === "Ping" + ? responses.offer({ _tag: "Pong" }) + : requests.offer(req) + ), + Effect.ensuring(Effect.zipRight(responses.shutdown, requests.shutdown)), + Effect.fork + ) + + yield* handle(client) + }) + ) +}) diff --git a/repos/effect/packages/experimental/src/Event.ts b/repos/effect/packages/experimental/src/Event.ts new file mode 100644 index 0000000..4e325b5 --- /dev/null +++ b/repos/effect/packages/experimental/src/Event.ts @@ -0,0 +1,261 @@ +/** + * @since 1.0.0 + */ +import * as MsgPack from "@effect/platform/MsgPack" +import { pipeArguments } from "effect/Pipeable" +import * as Predicate from "effect/Predicate" +import * as Schema from "effect/Schema" + +/** + * @since 1.0.0 + * @category type ids + */ +export const TypeId: unique symbol = Symbol.for("@effect/experimental/Event") + +/** + * @since 1.0.0 + * @category type ids + */ +export type TypeId = typeof TypeId + +/** + * @since 1.0.0 + * @category guards + */ +export const isEvent = (u: unknown): u is Event => Predicate.hasProperty(u, TypeId) + +/** + * Represents an event in an EventLog. + * + * @since 1.0.0 + * @category models + */ +export interface Event< + out Tag extends string, + in out Payload extends Schema.Schema.Any = typeof Schema.Void, + in out Success extends Schema.Schema.Any = typeof Schema.Void, + in out Error extends Schema.Schema.All = typeof Schema.Never +> { + readonly [TypeId]: TypeId + readonly tag: Tag + readonly primaryKey: (payload: Schema.Schema.Type) => string + readonly payload: Payload + readonly payloadMsgPack: MsgPack.schema + readonly success: Success + readonly error: Error +} + +/** + * @since 1.0.0 + * @category models + */ +export interface EventHandler { + readonly _: unique symbol + readonly tag: Tag +} + +/** + * @since 1.0.0 + * @category models + */ +export declare namespace Event { + /** + * @since 1.0.0 + * @category models + */ + export interface Any { + readonly [TypeId]: TypeId + readonly tag: string + } + + /** + * @since 1.0.0 + * @category models + */ + export interface AnyWithProps extends Event {} + + /** + * @since 1.0.0 + * @category models + */ + export type ToService = A extends Event< + infer _Tag, + infer _Payload, + infer _Success, + infer _Error + > ? EventHandler<_Tag> : + never + + /** + * @since 1.0.0 + * @category models + */ + export type Tag = A extends Event< + infer _Tag, + infer _Payload, + infer _Success, + infer _Error + > ? _Tag : + never + + /** + * @since 1.0.0 + * @category models + */ + export type ErrorSchema = A extends Event< + infer _Tag, + infer _Payload, + infer _Success, + infer _Error + > ? _Error + : never + + /** + * @since 1.0.0 + * @category models + */ + export type Error = Schema.Schema.Type> + + /** + * @since 1.0.0 + * @category models + */ + export type AddError = A extends Event< + infer _Tag, + infer _Payload, + infer _Success, + infer _Error + > ? Event<_Tag, _Payload, _Success, _Error | Error> + : never + + /** + * @since 1.0.0 + * @category models + */ + export type PayloadSchema = A extends Event< + infer _Tag, + infer _Payload, + infer _Success, + infer _Error + > ? _Payload + : never + + /** + * @since 1.0.0 + * @category models + */ + export type Payload = Schema.Schema.Type> + + /** + * @since 1.0.0 + * @category models + */ + export type TaggedPayload = A extends Event< + infer _Tag, + infer _Payload, + infer _Success, + infer _Error + > ? { + readonly _tag: _Tag + readonly payload: Schema.Schema.Type<_Payload> + } + : never + + /** + * @since 1.0.0 + * @category models + */ + export type SuccessSchema = A extends Event< + infer _Tag, + infer _Payload, + infer _Success, + infer _Error + > ? _Success + : never + + /** + * @since 1.0.0 + * @category models + */ + export type Success = Schema.Schema.Type> + + /** + * @since 1.0.0 + * @category models + */ + export type Context = A extends Event< + infer _Name, + infer _Payload, + infer _Success, + infer _Error + > ? Schema.Schema.Context<_Payload> | Schema.Schema.Context<_Success> | Schema.Schema.Context<_Error> + : never + + /** + * @since 1.0.0 + * @category models + */ + export type WithTag = Extract + + /** + * @since 1.0.0 + * @category models + */ + export type ExcludeTag = Exclude + + /** + * @since 1.0.0 + * @category models + */ + export type PayloadWithTag = Payload> + + /** + * @since 1.0.0 + * @category models + */ + export type SuccessWithTag = Success> + + /** + * @since 1.0.0 + * @category models + */ + export type ErrorWithTag = Error> + + /** + * @since 1.0.0 + * @category models + */ + export type ContextWithTag = Context> +} + +const Proto = { + [TypeId]: TypeId, + pipe() { + return pipeArguments(this, arguments) + } +} + +/** + * @since 1.0.0 + * @category constructors + */ +export const make = < + Tag extends string, + Payload extends Schema.Schema.Any = typeof Schema.Void, + Success extends Schema.Schema.Any = typeof Schema.Void, + Error extends Schema.Schema.All = typeof Schema.Never +>(options: { + readonly tag: Tag + readonly primaryKey: (payload: Schema.Schema.Type) => string + readonly payload?: Payload + readonly success?: Success + readonly error?: Error +}): Event => + Object.assign(Object.create(Proto), { + tag: options.tag, + primaryKey: options.primaryKey, + payload: options.payload ?? Schema.Void, + payloadMsgPack: MsgPack.schema(options.payload ?? Schema.Void), + success: options.success ?? Schema.Void, + error: options.error ?? Schema.Never + }) diff --git a/repos/effect/packages/experimental/src/EventGroup.ts b/repos/effect/packages/experimental/src/EventGroup.ts new file mode 100644 index 0000000..74d7195 --- /dev/null +++ b/repos/effect/packages/experimental/src/EventGroup.ts @@ -0,0 +1,158 @@ +/** + * @since 1.0.0 + */ +import * as HttpApiSchema from "@effect/platform/HttpApiSchema" +import { type Pipeable, pipeArguments } from "effect/Pipeable" +import * as Predicate from "effect/Predicate" +import * as Record from "effect/Record" +import type * as Schema from "effect/Schema" +import type { Event } from "./Event.js" +import * as EventApi from "./Event.js" + +/** + * @since 1.0.0 + * @category type ids + */ +export const TypeId: unique symbol = Symbol.for("@effect/experimental/EventGroup") + +/** + * @since 1.0.0 + * @category type ids + */ +export type TypeId = typeof TypeId + +/** + * @since 1.0.0 + * @category guards + */ +export const isEventGroup = (u: unknown): u is EventGroup.Any => Predicate.hasProperty(u, TypeId) + +/** + * An `EventGroup` is a collection of `Event`s. You can use an `EventGroup` to + * represent a portion of your domain. + * + * The events can be implemented later using the `EventLogBuilder.group` api. + * + * @since 1.0.0 + * @category models + */ +export interface EventGroup< + out Events extends Event.Any = never +> extends Pipeable { + new(_: never): {} + + readonly [TypeId]: TypeId + readonly events: Record.ReadonlyRecord + + /** + * Add an `Event` to the `EventGroup`. + */ + add< + Tag extends string, + Payload extends Schema.Schema.Any = typeof Schema.Void, + Success extends Schema.Schema.Any = typeof Schema.Void, + Error extends Schema.Schema.All = typeof Schema.Never + >(options: { + readonly tag: Tag + readonly primaryKey: (payload: Schema.Schema.Type) => string + readonly payload?: Payload + readonly success?: Success + readonly error?: Error + }): EventGroup> + + /** + * Add an error schema to all the events in the `EventGroup`. + */ + addError(error: Error): EventGroup> +} + +/** + * @since 1.0.0 + * @category models + */ +export declare namespace EventGroup { + /** + * @since 1.0.0 + * @category models + */ + export interface Any { + readonly [TypeId]: TypeId + } + + /** + * @since 1.0.0 + * @category models + */ + export type AnyWithProps = EventGroup + + /** + * @since 1.0.0 + * @category models + */ + export type ToService = A extends EventGroup ? Event.ToService<_Events> + : never + + /** + * @since 1.0.0 + * @category models + */ + export type Events = Group extends EventGroup ? _Events + : never + + /** + * @since 1.0.0 + * @category models + */ + export type Context = Event.Context> +} + +const Proto = { + [TypeId]: TypeId, + add(this: EventGroup.AnyWithProps, options: { + readonly tag: string + readonly primaryKey: (payload: Schema.Schema.Any) => string + readonly payload?: Schema.Schema.Any + readonly success?: Schema.Schema.Any + readonly error?: Schema.Schema.All + }) { + return makeProto({ + events: { + ...this.events, + [options.tag]: EventApi.make(options) + } + }) + }, + addError(this: EventGroup.AnyWithProps, error: Schema.Schema.Any) { + return makeProto({ + events: Record.map(this.events, (event) => + EventApi.make({ + ...event, + error: HttpApiSchema.UnionUnify(event.error, error) + })) + }) + }, + pipe() { + return pipeArguments(this, arguments) + } +} + +const makeProto = < + Events extends Event.Any +>(options: { + readonly events: Record.ReadonlyRecord +}): EventGroup => { + function EventGroup() {} + Object.setPrototypeOf(EventGroup, Proto) + return Object.assign(EventGroup, options) as any +} + +/** + * An `EventGroup` is a collection of `Event`s. You can use an `EventGroup` to + * represent a portion of your domain. + * + * The events can be implemented later using the `EventLog.group` api. + * + * @since 1.0.0 + * @category constructors + */ +export const empty: EventGroup = makeProto({ events: Record.empty() }) diff --git a/repos/effect/packages/experimental/src/EventJournal.ts b/repos/effect/packages/experimental/src/EventJournal.ts new file mode 100644 index 0000000..4126ff2 --- /dev/null +++ b/repos/effect/packages/experimental/src/EventJournal.ts @@ -0,0 +1,606 @@ +/** + * @since 1.0.0 + */ +import * as MsgPack from "@effect/platform/MsgPack" +import * as Context from "effect/Context" +import * as DateTime from "effect/DateTime" +import * as Effect from "effect/Effect" +import * as Layer from "effect/Layer" +import * as PubSub from "effect/PubSub" +import type * as Queue from "effect/Queue" +import * as Schema from "effect/Schema" +import type { Scope } from "effect/Scope" +import * as Uuid from "uuid" + +/** + * @since 1.0.0 + * @category tags + */ +export class EventJournal extends Context.Tag("@effect/experimental/EventJournal")< + EventJournal, + { + /** + * Read all the entries in the journal. + */ + readonly entries: Effect.Effect, EventJournalError> + + /** + * Write an event to the journal, performing an effect before committing the + * event. + */ + readonly write: (options: { + readonly event: string + readonly primaryKey: string + readonly payload: Uint8Array + readonly effect: (entry: Entry) => Effect.Effect + }) => Effect.Effect + + /** + * Write events from a remote source to the journal. + */ + readonly writeFromRemote: ( + options: { + readonly remoteId: RemoteId + readonly entries: ReadonlyArray + readonly compact?: + | ((uncommitted: ReadonlyArray) => Effect.Effect< + ReadonlyArray<[compacted: ReadonlyArray, remoteEntries: ReadonlyArray]>, + EventJournalError + >) + | undefined + readonly effect: (options: { + readonly entry: Entry + readonly conflicts: ReadonlyArray + }) => Effect.Effect + } + ) => Effect.Effect + + /** + * Return the uncommitted entries for a remote source. + */ + readonly withRemoteUncommited: ( + remoteId: RemoteId, + f: (entries: ReadonlyArray) => Effect.Effect + ) => Effect.Effect + + /** + * Retrieve the last known sequence number for a remote source. + */ + readonly nextRemoteSequence: (remoteId: RemoteId) => Effect.Effect + + /** + * The entries added to the local journal. + */ + readonly changes: Effect.Effect, never, Scope> + + /** + * Remove all data + */ + readonly destroy: Effect.Effect + } +>() {} + +/** + * @since 1.0.0 + * @category errors + */ +export const ErrorTypeId: unique symbol = Symbol.for("@effect/experimental/EventJournal/ErrorId") + +/** + * @since 1.0.0 + * @category errors + */ +export type ErrorTypeId = typeof ErrorTypeId + +/** + * @since 1.0.0 + * @category errors + */ +export class EventJournalError + extends Schema.TaggedClass("@effect/experimental/EventJournal/Error")("EventJournalError", { + method: Schema.String, + cause: Schema.Defect + }) +{ + /** + * @since 1.0.0 + */ + readonly [ErrorTypeId]: ErrorTypeId = ErrorTypeId +} + +/** + * @since 1.0.0 + * @category remote + */ +export const RemoteIdTypeId: unique symbol = Symbol.for("@effect/experimental/EventJournal/RemoteId") + +/** + * @since 1.0.0 + * @category remote + */ +export const RemoteId = Schema.Uint8ArrayFromSelf.pipe(Schema.brand(RemoteIdTypeId)) + +/** + * @since 1.0.0 + * @category remote + */ +export type RemoteId = typeof RemoteId.Type + +/** + * @since 1.0.0 + * @category remote + */ +export const makeRemoteId = (): RemoteId => Uuid.v4({}, new Uint8Array(16)) as RemoteId + +/** + * @since 1.0.0 + * @category entry + */ +export const EntryIdTypeId: unique symbol = Symbol.for("@effect/experimental/EventJournal/EntryId") + +/** + * @since 1.0.0 + * @category entry + */ +export const EntryId = Schema.Uint8ArrayFromSelf.pipe(Schema.brand(EntryIdTypeId)) + +/** + * @since 1.0.0 + * @category entry + */ +export type EntryId = typeof EntryId.Type + +/** + * @since 1.0.0 + * @category entry + */ +export const makeEntryId = (options: { msecs?: number } = {}): EntryId => { + return Uuid.v7(options, new Uint8Array(16)) as EntryId +} + +/** + * @since 1.0.0 + * @category entry + */ +export const entryIdMillis = (entryId: EntryId): number => { + const bytes = new Uint8Array(8) + bytes.set(entryId.subarray(0, 6), 2) + return Number(new DataView(bytes.buffer).getBigUint64(0)) +} + +/** + * @since 1.0.0 + * @category entry + */ +export class Entry extends Schema.Class("@effect/experimental/EventJournal/Entry")({ + id: EntryId, + event: Schema.String, + primaryKey: Schema.String, + payload: Schema.Uint8ArrayFromSelf +}) { + /** + * @since 1.0.0 + */ + static arrayMsgPack = Schema.Array(MsgPack.schema(Entry)) + + /** + * @since 1.0.0 + */ + static encodeArray = Schema.encode(Entry.arrayMsgPack) + + /** + * @since 1.0.0 + */ + static decodeArray = Schema.decode(Entry.arrayMsgPack) + + /** + * @since 1.0.0 + */ + get idString(): string { + return Uuid.stringify(this.id) + } + + /** + * @since 1.0.0 + */ + get createdAtMillis(): number { + return entryIdMillis(this.id) + } + + /** + * @since 1.0.0 + */ + get createdAt(): DateTime.Utc { + return DateTime.unsafeMake(this.createdAtMillis) + } +} + +/** + * @since 1.0.0 + * @category entry + */ +export class RemoteEntry extends Schema.Class("@effect/experimental/EventJournal/RemoteEntry")({ + remoteSequence: Schema.Number, + entry: Entry +}) {} + +/** + * @since 1.0.0 + * @category memory + */ +export const makeMemory: Effect.Effect = Effect.gen(function*() { + const journal: Array = [] + const byId = new Map() + const remotes = new Map }>() + const pubsub = yield* PubSub.unbounded() + + const ensureRemote = (remoteId: RemoteId) => { + const remoteIdString = Uuid.stringify(remoteId) + let remote = remotes.get(remoteIdString) + if (remote) return remote + remote = { sequence: 0, missing: journal.slice() } + remotes.set(remoteIdString, remote) + return remote + } + + return EventJournal.of({ + entries: Effect.sync(() => journal.slice()), + write({ effect, event, payload, primaryKey }) { + return Effect.acquireUseRelease( + Effect.sync(() => + new Entry({ + id: makeEntryId(), + event, + primaryKey, + payload + }, { disableValidation: true }) + ), + effect, + (entry, exit) => + Effect.suspend(() => { + if (exit._tag === "Failure" || byId.has(entry.idString)) return Effect.void + journal.push(entry) + byId.set(entry.idString, entry) + remotes.forEach((remote) => { + remote.missing.push(entry) + }) + return pubsub.publish(entry) + }) + ) + }, + writeFromRemote: (options) => + Effect.gen(function*() { + const remote = ensureRemote(options.remoteId) + const uncommittedRemotes: Array = [] + const uncommitted: Array = [] + for (const remoteEntry of options.entries) { + if (byId.has(remoteEntry.entry.idString)) { + if (remoteEntry.remoteSequence > remote.sequence) { + remote.sequence = remoteEntry.remoteSequence + } + continue + } + uncommittedRemotes.push(remoteEntry) + uncommitted.push(remoteEntry.entry) + } + + const brackets = options.compact + ? yield* options.compact(uncommittedRemotes) + : [[uncommitted, uncommittedRemotes]] as const + + for (const [compacted, remoteEntries] of brackets) { + for (const originEntry of compacted) { + const entryMillis = entryIdMillis(originEntry.id) + const conflicts: Array = [] + for (let i = journal.length - 1; i >= -1; i--) { + const entry = journal[i] + if (entry !== undefined && entry.createdAtMillis > entryMillis) { + continue + } + for (let j = i + 2; j < journal.length; j++) { + const entry = journal[j]! + if (entry.event === originEntry.event && entry.primaryKey === originEntry.primaryKey) { + conflicts.push(entry) + } + } + yield* options.effect({ entry: originEntry, conflicts }) + break + } + } + for (let j = 0; j < remoteEntries.length; j++) { + const remoteEntry = remoteEntries[j] + journal.push(remoteEntry.entry) + if (remoteEntry.remoteSequence > remote.sequence) { + remote.sequence = remoteEntry.remoteSequence + } + } + journal.sort((a, b) => a.createdAtMillis - b.createdAtMillis) + } + }), + withRemoteUncommited: (remoteId, f) => + Effect.acquireUseRelease( + Effect.sync(() => ensureRemote(remoteId).missing.slice()), + f, + (entries, exit) => + Effect.sync(() => { + if (exit._tag === "Failure") return + const last = entries[entries.length - 1] + if (!last) return + const remote = ensureRemote(remoteId) + for (let i = remote.missing.length - 1; i >= 0; i--) { + if (remote.missing[i].id === last.id) { + remote.missing = remote.missing.slice(i + 1) + break + } + } + }) + ), + nextRemoteSequence: (remoteId) => Effect.sync(() => ensureRemote(remoteId).sequence), + changes: PubSub.subscribe(pubsub), + destroy: Effect.sync(() => { + journal.length = 0 + byId.clear() + remotes.clear() + }) + }) +}) + +/** + * @since 1.0.0 + * @category memory + */ +export const layerMemory: Layer.Layer = Layer.effect(EventJournal, makeMemory) + +/** + * @since 1.0.0 + * @category indexed db + */ +export const makeIndexedDb = (options?: { + readonly database?: string +}): Effect.Effect => + Effect.gen(function*() { + const database = options?.database ?? "effect_event_journal" + const openRequest = indexedDB.open(database, 1) + openRequest.onupgradeneeded = () => { + const db = openRequest.result + + const entries = db.createObjectStore("entries", { keyPath: "id" }) + entries.createIndex("id", "id", { unique: true }) + entries.createIndex("event", "event") + + const remotes = db.createObjectStore("remotes", { keyPath: ["remoteId", "entryId"] }) + remotes.createIndex("id", ["remoteId", "entryId"], { unique: true }) + remotes.createIndex("sequence", ["remoteId", "sequence"], { unique: true }) + + const remoteEntryId = db.createObjectStore("remoteEntryId", { keyPath: ["remoteId"] }) + remoteEntryId.createIndex("id", "remoteId", { unique: true }) + } + const db = yield* Effect.acquireRelease( + idbReq("open", () => openRequest), + (db) => Effect.sync(() => db.close()) + ) + + const pubsub = yield* PubSub.unbounded() + + return EventJournal.of({ + entries: idbReq( + "entries", + () => + db.transaction("entries", "readonly") + .objectStore("entries") + .index("id") + .getAll() + ).pipe( + Effect.flatMap((_) => + decodeEntryIdbArray(_).pipe( + Effect.mapError((cause) => new EventJournalError({ method: "entries", cause })) + ) + ) + ), + write: ({ effect, event, payload, primaryKey }) => + Effect.uninterruptibleMask((restore) => { + const entry = new Entry({ + id: makeEntryId(), + event, + primaryKey, + payload + }, { disableValidation: true }) + return restore(effect(entry)).pipe( + Effect.zipLeft(idbReq( + "write", + () => + db + .transaction("entries", "readwrite") + .objectStore("entries") + .put(encodeEntryIdb(entry)) + )), + Effect.zipLeft(pubsub.publish(entry)) + ) + }), + writeFromRemote: (options) => + Effect.gen(function*() { + const uncommitted: Array = [] + const uncommittedRemotes: Array = [] + + yield* Effect.async((resume) => { + const tx = db.transaction(["entries", "remotes"], "readwrite") + const entries = tx.objectStore("entries") + const remotes = tx.objectStore("remotes") + const iterator = options.entries[Symbol.iterator]() + const handleNext = (state: IteratorResult) => { + if (state.done) return + const remoteEntry = state.value + const entry = remoteEntry.entry + entries.get(entry.id).onsuccess = (event) => { + if ((event.target as any).result) { + remotes.put({ + remoteId: options.remoteId, + entryId: remoteEntry.entry.id, + sequence: remoteEntry.remoteSequence + }) + handleNext(iterator.next()) + return + } + uncommitted.push(entry) + uncommittedRemotes.push(remoteEntry) + handleNext(iterator.next()) + } + } + handleNext(iterator.next()) + tx.oncomplete = () => resume(Effect.void) + tx.onerror = () => + resume(Effect.fail(new EventJournalError({ method: "writeFromRemote", cause: tx.error }))) + return Effect.sync(() => tx.abort()) + }) + + const brackets = options.compact + ? yield* options.compact(uncommittedRemotes) + : [[uncommitted, uncommittedRemotes]] as const + + for (const [compacted, remoteEntries] of brackets) { + for (const originEntry of compacted) { + const conflicts: Array = [] + yield* Effect.async((resume) => { + const tx = db.transaction("entries", "readonly") + const entries = tx.objectStore("entries") + const cursorRequest = entries.index("id").openCursor( + IDBKeyRange.lowerBound(originEntry.id, true), + "next" + ) + cursorRequest.onsuccess = () => { + const cursor = cursorRequest.result! + if (!cursor) return + const decodedEntry = decodeEntryIdb(cursor.value) + if ( + decodedEntry.event === originEntry.event && + decodedEntry.primaryKey === originEntry.primaryKey + ) { + conflicts.push(decodedEntry) + } + cursor.continue() + } + tx.oncomplete = () => resume(Effect.void) + tx.onerror = () => + resume(Effect.fail(new EventJournalError({ method: "writeFromRemote", cause: tx.error }))) + return Effect.sync(() => tx.abort()) + }) + + yield* options.effect({ entry: originEntry, conflicts }) + } + + yield* Effect.async((resume) => { + const tx = db.transaction(["entries", "remotes"], "readwrite") + const entries = tx.objectStore("entries") + const remotes = tx.objectStore("remotes") + for (const remoteEntry of remoteEntries) { + entries.add(encodeEntryIdb(remoteEntry.entry)) + remotes.put({ + remoteId: options.remoteId, + entryId: remoteEntry.entry.id, + sequence: remoteEntry.remoteSequence + }) + } + tx.oncomplete = () => resume(Effect.void) + tx.onerror = () => + resume(Effect.fail(new EventJournalError({ method: "writeFromRemote", cause: tx.error }))) + return Effect.sync(() => tx.abort()) + }) + } + }), + withRemoteUncommited: (remoteId, f) => + Effect.async, EventJournalError>((resume) => { + const entries: Array = [] + const tx = db.transaction(["entries", "remotes", "remoteEntryId"], "readwrite") + + const entriesStore = tx.objectStore("entries") + const remotesStore = tx.objectStore("remotes") + const remoteEntryIdStore = tx.objectStore("remoteEntryId") + + remoteEntryIdStore.get(remoteId).onsuccess = (event) => { + const startEntryId = (event.target as any).result?.entryId + const entryCursor = entriesStore.index("id").openCursor( + startEntryId ? IDBKeyRange.lowerBound(startEntryId, true) : null, + "next" + ) + entryCursor.onsuccess = () => { + const cursor = entryCursor.result + if (!cursor) return + const entry = decodeEntryIdb(cursor.value) + remotesStore.get([remoteId, entry.id]).onsuccess = (event) => { + if (!(event.target as any).result) entries.push(entry) + cursor.continue() + } + } + } + + tx.oncomplete = () => resume(Effect.succeed(entries)) + tx.onerror = () => + resume(Effect.fail(new EventJournalError({ method: "withRemoteUncommited", cause: tx.error }))) + return Effect.sync(() => tx.abort()) + }).pipe( + Effect.flatMap((entries) => { + if (entries.length === 0) return f(entries) + const entryId = entries[entries.length - 1].id + return Effect.uninterruptibleMask((restore) => + restore(f(entries)).pipe( + Effect.zipLeft(idbReq("withRemoteUncommited", () => + db.transaction("remoteEntryId", "readwrite").objectStore("remoteEntryId").put({ + remoteId, + entryId + }))) + ) + ) + }) + ), + nextRemoteSequence: (remoteId) => + Effect.async((resume) => { + const tx = db.transaction("remotes", "readonly") + let sequence = 0 + const cursorRequest = tx.objectStore("remotes").index("sequence").openCursor( + IDBKeyRange.bound([remoteId, 0], [remoteId, Infinity]), + "prev" + ) + cursorRequest.onsuccess = () => { + const cursor = cursorRequest.result + if (!cursor) return + sequence = cursor.value.sequence + 1 + } + tx.oncomplete = () => resume(Effect.succeed(sequence)) + tx.onerror = () => + resume(Effect.fail(new EventJournalError({ method: "nextRemoteSequence", cause: tx.error }))) + return Effect.sync(() => tx.abort()) + }), + changes: PubSub.subscribe(pubsub), + destroy: Effect.sync(() => { + indexedDB.deleteDatabase(database) + }) + }) + }) + +const decodeEntryIdb = Schema.decodeSync(Entry) +const encodeEntryIdb = Schema.encodeSync(Entry) +const EntryIdbArray = Schema.Array(Entry) +const decodeEntryIdbArray = Schema.decodeUnknown(EntryIdbArray) + +/** + * @since 1.0.0 + * @category indexed db + */ +export const layerIndexedDb = (options?: { + readonly database?: string +}): Layer.Layer => + Layer.scoped( + EventJournal, + makeIndexedDb(options) + ) + +const idbReq = (method: string, evaluate: () => IDBRequest) => + Effect.async((resume) => { + const request = evaluate() + if (request.readyState === "done") { + resume(Effect.succeed(request.result)) + return + } + request.onsuccess = () => resume(Effect.succeed(request.result)) + request.onerror = () => resume(Effect.fail(new EventJournalError({ method, cause: request.error }))) + }) diff --git a/repos/effect/packages/experimental/src/EventLog.ts b/repos/effect/packages/experimental/src/EventLog.ts new file mode 100644 index 0000000..be454aa --- /dev/null +++ b/repos/effect/packages/experimental/src/EventLog.ts @@ -0,0 +1,764 @@ +/** + * @since 1.0.0 + */ +import type * as Error from "@effect/platform/Error" +import * as KeyValueStore from "@effect/platform/KeyValueStore" +import * as Chunk from "effect/Chunk" +import * as Context from "effect/Context" +import * as Effect from "effect/Effect" +import * as FiberMap from "effect/FiberMap" +import * as FiberRef from "effect/FiberRef" +import { identity } from "effect/Function" +import * as Layer from "effect/Layer" +import * as Option from "effect/Option" +import type * as ParseResult from "effect/ParseResult" +import { type Pipeable, pipeArguments } from "effect/Pipeable" +import * as Predicate from "effect/Predicate" +import * as Queue from "effect/Queue" +import type * as Record from "effect/Record" +import * as Redacted from "effect/Redacted" +import * as Schema from "effect/Schema" +import type { Scope } from "effect/Scope" +import type { Covariant } from "effect/Types" +import type { Event } from "./Event.js" +import type { EventGroup } from "./EventGroup.js" +import type { EventJournalError, RemoteEntry, RemoteId } from "./EventJournal.js" +import { Entry, EventJournal, makeEntryId } from "./EventJournal.js" +import { type EventLogRemote } from "./EventLogRemote.js" +import * as Reactivity from "./Reactivity.js" + +/** + * @since 1.0.0 + * @category schema + */ +export const SchemaTypeId: unique symbol = Symbol.for("@effect/experimental/EventLog/EventLogSchema") + +/** + * @since 1.0.0 + * @category schema + */ +export type SchemaTypeId = typeof SchemaTypeId + +/** + * @since 1.0.0 + * @category schema + */ +export const isEventLogSchema = (u: unknown): u is EventLogSchema => Predicate.hasProperty(u, SchemaTypeId) + +/** + * @since 1.0.0 + * @category schema + */ +export interface EventLogSchema { + new(_: never): {} + readonly [SchemaTypeId]: SchemaTypeId + readonly groups: ReadonlyArray +} + +/** + * @since 1.0.0 + * @category schema + */ +export const schema = >( + ...groups: Groups +): EventLogSchema => { + function EventLog() {} + EventLog[SchemaTypeId] = SchemaTypeId + EventLog.groups = groups + return EventLog as any +} + +/** + * @since 1.0.0 + * @category handlers + */ +export const HandlersTypeId: unique symbol = Symbol.for("@effect/experimental/EventLog/Handlers") + +/** + * @since 1.0.0 + * @category handlers + */ +export type HandlersTypeId = typeof HandlersTypeId + +/** + * Represents a handled `EventGroup`. + * + * @since 1.0.0 + * @category handlers + */ +export interface Handlers< + R, + Events extends Event.Any = never +> extends Pipeable { + readonly [HandlersTypeId]: { + _Endpoints: Covariant + } + readonly group: EventGroup.AnyWithProps + readonly handlers: Record.ReadonlyRecord> + readonly context: Context.Context + + /** + * Add the implementation for an `Event` to a `Handlers` group. + */ + handle( + name: Tag, + handler: ( + options: { + readonly payload: Event.PayloadWithTag + readonly entry: Entry + readonly conflicts: Array<{ + readonly entry: Entry + readonly payload: Event.PayloadWithTag + }> + } + ) => Effect.Effect, Event.ErrorWithTag, R1> + ): Handlers< + R | R1, + Event.ExcludeTag + > +} + +/** + * @since 1.0.0 + * @category handlers + */ +export declare namespace Handlers { + /** + * @since 1.0.0 + * @category handlers + */ + export interface Any { + readonly [HandlersTypeId]: any + } + + /** + * @since 1.0.0 + * @category handlers + */ + export type Item = { + readonly event: Event.AnyWithProps + readonly context: Context.Context + readonly handler: (options: { + readonly payload: any + readonly entry: Entry + readonly conflicts: Array<{ + readonly entry: Entry + readonly payload: any + }> + }) => Effect.Effect + } + + /** + * @since 1.0.0 + * @category handlers + */ + export type ValidateReturn = A extends ( + | Handlers< + infer _R, + infer _Events + > + | Effect.Effect< + Handlers< + infer _R, + infer _Events + >, + infer _EX, + infer _RX + > + ) ? [_Events] extends [never] ? A + : `Event not handled: ${Event.Tag<_Events>}` : + `Must return the implemented handlers` + + /** + * @since 1.0.0 + * @category handlers + */ + export type Error = A extends Effect.Effect< + Handlers< + infer _R, + infer _Events + >, + infer _EX, + infer _RX + > ? _EX : + never + + /** + * @since 1.0.0 + * @category handlers + */ + export type Context = A extends Handlers< + infer _R, + infer _Events + > ? _R | Event.Context<_Events> : + A extends Effect.Effect< + Handlers< + infer _R, + infer _Events + >, + infer _EX, + infer _RX + > ? _R | _RX | Event.Context<_Events> : + never +} + +const handlersProto = { + [HandlersTypeId]: { + _Endpoints: identity + }, + handle( + this: Handlers, + tag: Tag, + handler: (payload: any) => Effect.Effect + ): Handlers { + return makeHandlers({ + group: this.group, + context: this.context, + handlers: { + ...this.handlers, + [tag]: { + event: this.group.events[tag], + context: this.context, + handler + } + } + }) + }, + pipe() { + return pipeArguments(this, arguments) + } +} + +const makeHandlers = (options: { + readonly group: EventGroup.AnyWithProps + readonly handlers: Record.ReadonlyRecord> + readonly context: Context.Context +}): Handlers => Object.assign(Object.create(handlersProto), options) + +/** + * @since 1.0.0 + * @category handlers + */ +export const group = ( + group: EventGroup, + f: (handlers: Handlers) => Handlers.ValidateReturn +): Layer.Layer, Handlers.Error, Exclude, Scope>> => + Effect.gen(function*() { + const context = yield* Effect.context>() + const result = f(makeHandlers({ + group: group as any, + handlers: {}, + context + })) + const handlers = Effect.isEffect(result) ? yield* (result as any as Effect.Effect) : result + const registry = yield* Registry + yield* registry.add(handlers) + }).pipe( + Layer.scopedDiscard, + Layer.provide(Registry.layer) + ) as any + +/** + * @since 1.0.0 + * @category compaction + */ +export const groupCompaction = ( + group: EventGroup, + effect: (options: { + readonly primaryKey: string + readonly entries: Array + readonly events: Array> + readonly write: >( + tag: Tag, + payload: Event.PayloadWithTag + ) => Effect.Effect + }) => Effect.Effect +): Layer.Layer> => + Effect.gen(function*() { + const log = yield* EventLog + const context = yield* Effect.context>() + + yield* log.registerCompaction({ + events: Object.keys(group.events), + effect: Effect.fnUntraced(function*({ entries, write }) { + const writePayload = (timestamp: number, tag: string, payload: any) => + Effect.gen(function*() { + const event = group.events[tag] as any as Event.AnyWithProps + const entry = new Entry({ + id: makeEntryId({ msecs: timestamp }), + event: tag, + payload: yield* (Schema.encode(event.payloadMsgPack)(payload).pipe( + Effect.locally(FiberRef.currentContext, context), + Effect.orDie + ) as Effect.Effect), + primaryKey: event.primaryKey(payload) + }, { disableValidation: true }) + yield* write(entry) + }) + + const byPrimaryKey = new Map< + string, + { + readonly entries: Array + readonly taggedPayloads: Array<{ + readonly _tag: string + readonly payload: any + }> + } + >() + for (const entry of entries) { + const payload = + yield* (Schema.decodeUnknown((group.events[entry.event] as any).payloadMsgPack)(entry.payload).pipe( + Effect.locally(FiberRef.currentContext, context) + ) as Effect.Effect) + + if (byPrimaryKey.has(entry.primaryKey)) { + const record = byPrimaryKey.get(entry.primaryKey)! + record.entries.push(entry) + record.taggedPayloads.push({ + _tag: entry.event, + payload + }) + } else { + byPrimaryKey.set(entry.primaryKey, { + entries: [entry], + taggedPayloads: [{ _tag: entry.event, payload }] + }) + } + } + + for (const [primaryKey, { entries, taggedPayloads }] of byPrimaryKey) { + yield* (effect({ + primaryKey, + entries, + events: taggedPayloads as any, + write(tag, payload) { + return writePayload(entries[0].createdAtMillis, tag, payload) + } + }).pipe( + Effect.locally(FiberRef.currentContext, context) + ) as Effect.Effect) + } + }) + }) + }).pipe( + Layer.scopedDiscard, + Layer.provide(layerEventLog) + ) + +/** + * @since 1.0.0 + * @category reactivity + */ +export const groupReactivity = ( + group: EventGroup, + keys: + | { readonly [Tag in Event.Tag]?: ReadonlyArray } + | ReadonlyArray +): Layer.Layer => + Effect.gen(function*() { + const log = yield* EventLog + if (!Array.isArray(keys)) { + yield* log.registerReactivity(keys as any) + return + } + const obj: Record> = {} + for (const tag in group.events) { + obj[tag] = keys + } + yield* log.registerReactivity(obj) + }).pipe( + Layer.scopedDiscard, + Layer.provide(layerEventLog) + ) + +/** + * @since 1.0.0 + * @category layers + */ +export class Registry extends Context.Tag("@effect/experimental/EventLog/Registry")< + Registry, + { + readonly add: (handlers: Handlers.Any) => Effect.Effect + readonly handlers: Effect.Effect>> + } +>() { + /** + * @since 1.0.0 + */ + static layer = Layer.sync(Registry, () => { + const items: Record> = {} + + return { + add: (handlers: Handlers) => + Effect.sync(() => { + for (const tag in handlers.handlers) { + items[tag] = handlers.handlers[tag] + } + }), + handlers: Effect.sync(() => items) + } as any + }) +} + +/** + * @since 1.0.0 + * @category tags + */ +export class Identity extends Context.Tag("@effect/experimental/EventLog/Identity") +}>() { + /** + * @since 1.0.0 + */ + static makeRandom() { + return Identity.of({ + publicKey: crypto.randomUUID(), + privateKey: Redacted.make(crypto.getRandomValues(new Uint8Array(32))) + }) + } + /** + * @since 1.0.0 + */ + static readonly Schema = Schema.Struct({ + publicKey: Schema.String, + privateKey: Schema.Redacted(Schema.Uint8ArrayFromBase64) + }) + /** + * @since 1.0.0 + */ + static readonly SchemaFromString = Schema.StringFromBase64Url.pipe( + Schema.compose(Schema.parseJson(this.Schema)) + ) + + /** + * @since 1.0.0 + */ + static decodeString = (s: string): Identity["Type"] => Schema.decodeSync(Identity.SchemaFromString)(s) + /** + * @since 1.0.0 + */ + static encodeString = (identity: Identity["Type"]): string => Schema.encodeSync(Identity.SchemaFromString)(identity) +} + +/** + * Generates a random `Identity` and stores it in a `KeyValueStore`. + * + * @since 1.0.0 + * @category layers + */ +export const layerIdentityKvs = (options: { + readonly key: string +}): Layer.Layer => + Layer.effect( + Identity, + Effect.gen(function*() { + const store = (yield* KeyValueStore.KeyValueStore).forSchema(Identity.Schema) + const current = yield* store.get(options.key) + if (Option.isSome(current)) { + return current.value + } + const identity = Identity.makeRandom() + yield* store.set(options.key, identity) + return identity + }) + ) + +/** + * @since 1.0.0 + * @category tags + */ +export class EventLog extends Context.Tag("@effect/experimental/EventLog/EventLog")>>(options: { + readonly schema: EventLogSchema + readonly event: Tag + readonly payload: Event.PayloadWithTag, Tag> + }) => Effect.Effect< + Event.SuccessWithTag, Tag>, + Event.ErrorWithTag, Tag> | EventJournalError + > + readonly registerRemote: (remote: EventLogRemote) => Effect.Effect + readonly registerCompaction: (options: { + readonly events: ReadonlyArray + readonly effect: (options: { + readonly entries: ReadonlyArray + readonly write: (entry: Entry) => Effect.Effect + }) => Effect.Effect + }) => Effect.Effect + readonly registerReactivity: (keys: Record>) => Effect.Effect + readonly entries: Effect.Effect, EventJournalError> + readonly destroy: Effect.Effect +}>() {} + +const make = Effect.gen(function*() { + const identity = yield* Identity + const registry = yield* Registry + const journal = yield* EventJournal + const handlers = yield* registry.handlers + const remotes = yield* FiberMap.make() + const compactors = new Map + readonly effect: (options: { + readonly entries: ReadonlyArray + readonly write: (entry: Entry) => Effect.Effect + }) => Effect.Effect + }>() + const journalSemaphore = yield* Effect.makeSemaphore(1) + + const reactivity = yield* Reactivity.Reactivity + const reactivityKeys: Record> = {} + + const runRemote = Effect.fnUntraced( + function*(remote: EventLogRemote) { + const startSequence = yield* journal.nextRemoteSequence(remote.id) + const changes = yield* remote.changes(identity, startSequence) + + yield* changes.takeAll.pipe( + Effect.flatMap(([entries]) => + journal.writeFromRemote({ + remoteId: remote.id, + entries: Chunk.toReadonlyArray(entries), + compact: compactors.size > 0 ? + Effect.fnUntraced(function*(remoteEntries) { + let unprocessed = remoteEntries as Array + const brackets: Array<[Array, Array]> = [] + let uncompacted: Array = [] + let uncompactedRemote: Array = [] + while (true) { + let i = 0 + for (; i < unprocessed.length; i++) { + const remoteEntry = unprocessed[i] + if (!compactors.has(remoteEntry.entry.event)) { + uncompacted.push(remoteEntry.entry) + uncompactedRemote.push(remoteEntry) + continue + } + if (uncompacted.length > 0) { + brackets.push([uncompacted, uncompactedRemote]) + uncompacted = [] + uncompactedRemote = [] + } + const compactor = compactors.get(remoteEntry.entry.event)! + const entry = remoteEntry.entry + const entries = [entry] + const remoteEntries = [remoteEntry] + const compacted: Array = [] + const currentEntries = unprocessed + unprocessed = [] + for (let j = i + 1; j < currentEntries.length; j++) { + const nextRemoteEntry = currentEntries[j] + if (!compactor.events.has(nextRemoteEntry.entry.event)) { + unprocessed.push(nextRemoteEntry) + continue + } + entries.push(nextRemoteEntry.entry) + remoteEntries.push(nextRemoteEntry) + } + yield* compactor.effect({ + entries, + write(entry) { + return Effect.sync(() => { + compacted.push(entry) + }) + } + }) + brackets.push([compacted, remoteEntries]) + break + } + if (i === unprocessed.length) { + brackets.push([unprocessed.map((_) => _.entry), unprocessed]) + break + } + } + return brackets + }) : + undefined, + effect: Effect.fnUntraced( + function*({ conflicts, entry }) { + const handler = handlers[entry.event] + if (!handler) { + return yield* Effect.logDebug(`Event handler not found for: "${entry.event}"`) + } + const decodePayload = Schema.decode( + handlers[entry.event].event.payloadMsgPack as unknown as Schema.Schema + ) + const decodedConflicts: Array<{ entry: Entry; payload: any }> = new Array(conflicts.length) + for (let i = 0; i < conflicts.length; i++) { + decodedConflicts[i] = { + entry: conflicts[i], + payload: yield* decodePayload(conflicts[i].payload) + } + } + yield* handler.handler({ + payload: yield* decodePayload(entry.payload), + entry, + conflicts: decodedConflicts + }) + if (reactivityKeys[entry.event]) { + for (const key of reactivityKeys[entry.event]) { + reactivity.unsafeInvalidate({ + [key]: [entry.primaryKey] + }) + } + } + }, + Effect.catchAllCause(Effect.log), + (effect, { entry }) => + Effect.annotateLogs(effect, { + service: "EventLog", + effect: "writeFromRemote", + entryId: entry.idString + }) + ) + }).pipe(journalSemaphore.withPermits(1)) + ), + Effect.catchAllCause(Effect.log), + Effect.forever, + Effect.annotateLogs({ + service: "EventLog", + effect: "runRemote consume" + }), + Effect.fork + ) + + const write = journal.withRemoteUncommited(remote.id, (entries) => remote.write(identity, entries)) + yield* Effect.addFinalizer(() => Effect.ignore(write)) + yield* write + return yield* Queue.takeBetween(yield* journal.changes, 1, Number.MAX_SAFE_INTEGER).pipe( + Effect.zipRight(Effect.sleep(500)), + Effect.zipRight(write), + Effect.catchAllCause(Effect.log), + Effect.forever + ) + }, + Effect.scoped, + Effect.provideService(Identity, identity), + Effect.interruptible + ) + + const writeHandler = Effect.fnUntraced(function*(handler: Handlers.Item, options: { + readonly schema: EventLogSchema + readonly event: string + readonly payload: any + }) { + const payload = yield* Effect.orDie( + Schema.encode(handlers[options.event].event.payloadMsgPack as unknown as Schema.Schema)( + options.payload + ) + ) + return yield* journalSemaphore.withPermits(1)(journal.write({ + event: options.event, + primaryKey: handler.event.primaryKey(options.payload), + payload, + effect: (entry) => + Effect.tap( + handler.handler({ + payload: options.payload, + entry, + conflicts: [] + }), + () => { + if (reactivityKeys[entry.event]) { + for (const key of reactivityKeys[entry.event]) { + reactivity.unsafeInvalidate({ + [key]: [entry.primaryKey] + }) + } + } + } + ) + })) + }, (effect, handler) => Effect.mapInputContext(effect, (context) => Context.merge(handler.context, context))) + + return EventLog.of({ + write: (options: { + readonly schema: EventLogSchema + readonly event: string + readonly payload: any + }) => { + const handler = handlers[options.event] + if (handler === undefined) { + return Effect.die(`Event handler not found for: "${options.event}"`) + } + return writeHandler(handler, options) as any + }, + entries: journal.entries, + registerRemote: (remote) => + Effect.acquireRelease( + FiberMap.run(remotes, remote.id, runRemote(remote)), + () => FiberMap.remove(remotes, remote.id) + ), + registerCompaction: (options) => + Effect.acquireRelease( + Effect.sync(() => { + const events = new Set(options.events) + const compactor = { + events, + effect: options.effect + } + for (const event of options.events) { + compactors.set(event, compactor) + } + }), + () => + Effect.sync(() => { + for (const event of options.events) { + compactors.delete(event) + } + }) + ), + registerReactivity: (keys) => + Effect.sync(() => { + Object.assign(reactivityKeys, keys) + }), + destroy: journal.destroy + }) +}) + +/** + * @since 1.0.0 + * @category layers + */ +export const layerEventLog: Layer.Layer = Layer.scoped(EventLog, make).pipe( + Layer.provide([Registry.layer, Reactivity.layer]) +) + +/** + * @since 1.0.0 + * @category layers + */ +export const layer = (_schema: EventLogSchema): Layer.Layer< + EventLog, + never, + EventGroup.ToService | EventJournal | Identity +> => layerEventLog as any + +/** + * @since 1.0.0 + * @category client + */ +export const makeClient = ( + schema: EventLogSchema +): Effect.Effect< + (>>( + event: Tag, + payload: Event.PayloadWithTag, Tag> + ) => Effect.Effect< + Event.SuccessWithTag, Tag>, + Event.ErrorWithTag, Tag> | EventJournalError + >), + never, + EventLog +> => + Effect.gen(function*() { + const log = yield* EventLog + + return >>( + event: Tag, + payload: Event.PayloadWithTag, Tag> + ): Effect.Effect< + Event.SuccessWithTag, Tag>, + Event.ErrorWithTag, Tag> | EventJournalError + > => log.write({ schema, event, payload }) + }) diff --git a/repos/effect/packages/experimental/src/EventLogEncryption.ts b/repos/effect/packages/experimental/src/EventLogEncryption.ts new file mode 100644 index 0000000..d098c22 --- /dev/null +++ b/repos/effect/packages/experimental/src/EventLogEncryption.ts @@ -0,0 +1,143 @@ +/** + * @since 1.0.0 + */ +import * as Context from "effect/Context" +import * as Effect from "effect/Effect" +import * as Layer from "effect/Layer" +import * as Redacted from "effect/Redacted" +import * as Schema from "effect/Schema" +import { Entry, EntryId, RemoteEntry } from "./EventJournal.js" +import type { Identity } from "./EventLog.js" + +/** + * @since 1.0.0 + * @category models + */ +export const EncryptedEntry = Schema.Struct({ + entryId: EntryId, + encryptedEntry: Schema.Uint8ArrayFromSelf +}) + +/** + * @since 1.0.0 + * @category models + */ +export interface EncryptedRemoteEntry extends Schema.Schema.Type {} + +/** + * @since 1.0.0 + * @category models + */ +export const EncryptedRemoteEntry = Schema.Struct({ + sequence: Schema.Number, + iv: Schema.Uint8ArrayFromSelf, + entryId: EntryId, + encryptedEntry: Schema.Uint8ArrayFromSelf +}) + +/** + * @since 1.0.0 + * @category encrytion + */ +export class EventLogEncryption extends Context.Tag("@effect/experimental/EventLogEncryption")< + EventLogEncryption, + { + readonly encrypt: ( + identity: typeof Identity.Service, + entries: ReadonlyArray + ) => Effect.Effect<{ + readonly iv: Uint8Array + readonly encryptedEntries: ReadonlyArray + }> + readonly decrypt: ( + identity: typeof Identity.Service, + entries: ReadonlyArray + ) => Effect.Effect> + readonly sha256String: (data: Uint8Array) => Effect.Effect + readonly sha256: (data: Uint8Array) => Effect.Effect + } +>() {} + +/** + * @since 1.0.0 + * @category encrytion + */ +export const makeEncryptionSubtle = (crypto: Crypto): Effect.Effect => + Effect.sync(() => { + const keyCache = new WeakMap() + const getKey = (identity: typeof Identity.Service) => + Effect.suspend(() => { + if (keyCache.has(identity)) { + return Effect.succeed(keyCache.get(identity)!) + } + return Effect.promise(() => + crypto.subtle.importKey( + "raw", + Redacted.value(identity.privateKey), + "AES-GCM", + true, + ["encrypt", "decrypt"] + ) + ).pipe( + Effect.tap((key) => { + keyCache.set(identity, key) + }) + ) + }) + + return EventLogEncryption.of({ + encrypt: (identity, entries) => + Effect.gen(function*() { + const data = yield* Effect.orDie(Entry.encodeArray(entries)) + const key = yield* getKey(identity) + const iv = crypto.getRandomValues(new Uint8Array(12)) + const encryptedEntries = yield* Effect.promise(() => + Promise.all( + data.map((entry) => crypto.subtle.encrypt({ name: "AES-GCM", iv, tagLength: 128 }, key, entry)) + ) + ) + return { + iv, + encryptedEntries: encryptedEntries.map((entry) => new Uint8Array(entry)) + } + }), + decrypt: (identity, entries) => + Effect.gen(function*() { + const key = yield* getKey(identity) + const decryptedData = (yield* Effect.promise(() => + Promise.all(entries.map((data) => + crypto.subtle.decrypt( + { name: "AES-GCM", iv: data.iv, tagLength: 128 }, + key, + data.encryptedEntry + ) + )) + )).map((buffer) => new Uint8Array(buffer)) + const decoded = yield* Effect.orDie(Entry.decodeArray(decryptedData)) + return decoded.map((entry, i) => new RemoteEntry({ remoteSequence: entries[i].sequence, entry })) + }), + sha256: (data) => + Effect.promise(() => crypto.subtle.digest("SHA-256", data)).pipe( + Effect.map((hash) => new Uint8Array(hash)) + ), + sha256String: (data) => + Effect.map( + Effect.promise(() => crypto.subtle.digest("SHA-256", data)), + (hash) => { + const hashArray = Array.from(new Uint8Array(hash)) + const hashHex = hashArray + .map((bytes) => bytes.toString(16).padStart(2, "0")) + .join("") + return hashHex + } + ) + }) + }) + +/** + * @since 1.0.0 + * @category encrytion + */ +export const layerSubtle: Layer.Layer = Layer.suspend(() => + Layer.effect(EventLogEncryption, makeEncryptionSubtle(globalThis.crypto)) +) diff --git a/repos/effect/packages/experimental/src/EventLogRemote.ts b/repos/effect/packages/experimental/src/EventLogRemote.ts new file mode 100644 index 0000000..1522c79 --- /dev/null +++ b/repos/effect/packages/experimental/src/EventLogRemote.ts @@ -0,0 +1,470 @@ +/** + * @since 1.0.0 + */ +import * as MsgPack from "@effect/platform/MsgPack" +import * as Socket from "@effect/platform/Socket" +import * as Deferred from "effect/Deferred" +import * as Effect from "effect/Effect" +import * as Exit from "effect/Exit" +import * as Layer from "effect/Layer" +import * as Mailbox from "effect/Mailbox" +import * as RcMap from "effect/RcMap" +import * as Schedule from "effect/Schedule" +import * as Schema from "effect/Schema" +import * as Scope from "effect/Scope" +import type { Entry } from "./EventJournal.js" +import { RemoteEntry, RemoteId } from "./EventJournal.js" +import type { Identity } from "./EventLog.js" +import { EventLog } from "./EventLog.js" +import { EncryptedEntry, EncryptedRemoteEntry, EventLogEncryption, layerSubtle } from "./EventLogEncryption.js" + +/** + * @since 1.0.0 + * @category models + */ +export interface EventLogRemote { + readonly id: RemoteId + readonly changes: ( + identity: typeof Identity.Service, + startSequence: number + ) => Effect.Effect, never, Scope.Scope> + readonly write: (identity: typeof Identity.Service, entries: ReadonlyArray) => Effect.Effect +} + +/** + * @since 1.0.0 + * @category protocol + */ +export class Hello extends Schema.TaggedClass("@effect/experimental/EventLogRemote/Hello")("Hello", { + remoteId: RemoteId +}) {} + +/** + * @since 1.0.0 + * @category protocol + */ +export class ChunkedMessage + extends Schema.TaggedClass("@effect/experimental/EventLogRemote/ChunkedMessage")("ChunkedMessage", { + id: Schema.Number, + part: Schema.Tuple(Schema.Number, Schema.Number), + data: Schema.Uint8ArrayFromSelf + }) +{ + /** + * @since 1.0.0 + */ + static split(id: number, data: Uint8Array): ReadonlyArray { + const parts = Math.ceil(data.byteLength / constChunkSize) + const result: Array = new Array(parts) + for (let i = 0; i < parts; i++) { + const start = i * constChunkSize + const end = Math.min((i + 1) * constChunkSize, data.byteLength) + result[i] = new ChunkedMessage({ id, part: [i, parts], data: data.subarray(start, end) }) + } + return result + } + + /** + * @since 1.0.0 + */ + static join( + map: Map + count: number + bytes: number + }>, + part: ChunkedMessage + ): Uint8Array | undefined { + const [index, total] = part.part + let entry = map.get(part.id) + if (!entry) { + entry = { + parts: new Array(total), + count: 0, + bytes: 0 + } + map.set(part.id, entry) + } + entry.parts[index] = part.data + entry.count++ + entry.bytes += part.data.byteLength + if (entry.count !== total) { + return + } + const data = new Uint8Array(entry.bytes) + let offset = 0 + for (const part of entry.parts) { + data.set(part, offset) + offset += part.byteLength + } + map.delete(part.id) + return data + } +} + +/** + * @since 1.0.0 + * @category protocol + */ +export class WriteEntries + extends Schema.TaggedClass("@effect/experimental/EventLogRemote/WriteEntries")("WriteEntries", { + publicKey: Schema.String, + id: Schema.Number, + iv: Schema.Uint8ArrayFromSelf, + encryptedEntries: Schema.Array(EncryptedEntry) + }) +{} + +/** + * @since 1.0.0 + * @category protocol + */ +export class Ack extends Schema.TaggedClass("@effect/experimental/EventLogRemote/Ack")("Ack", { + id: Schema.Number, + sequenceNumbers: Schema.Array(Schema.Number) +}) {} + +/** + * @since 1.0.0 + * @category protocol + */ +export class RequestChanges + extends Schema.TaggedClass("@effect/experimental/EventLogRemote/RequestChanges")("RequestChanges", { + publicKey: Schema.String, + startSequence: Schema.Number + }) +{} + +/** + * @since 1.0.0 + * @category protocol + */ +export class Changes extends Schema.TaggedClass("@effect/experimental/EventLogRemote/Changes")("Changes", { + publicKey: Schema.String, + entries: Schema.Array(EncryptedRemoteEntry) +}) {} + +/** + * @since 1.0.0 + * @category protocol + */ +export class StopChanges + extends Schema.TaggedClass("@effect/experimental/EventLogRemote/StopChanges")("StopChanges", { + publicKey: Schema.String + }) +{} + +/** + * @since 1.0.0 + * @category protocol + */ +export class Ping extends Schema.TaggedClass("@effect/experimental/EventLogRemote/Ping")("Ping", { + id: Schema.Number +}) {} + +/** + * @since 1.0.0 + * @category protocol + */ +export class Pong extends Schema.TaggedClass("@effect/experimental/EventLogRemote/Pong")("Pong", { + id: Schema.Number +}) {} + +/** + * @since 1.0.0 + * @category protocol + */ +export const ProtocolRequest = Schema.Union( + WriteEntries, + RequestChanges, + StopChanges, + ChunkedMessage, + Ping +) + +/** + * @since 1.0.0 + * @category protocol + */ +export const ProtocolRequestMsgPack = MsgPack.schema(ProtocolRequest) + +/** + * @since 1.0.0 + * @category protocol + */ +export const decodeRequest = Schema.decodeSync(ProtocolRequestMsgPack) + +/** + * @since 1.0.0 + * @category protocol + */ +export const encodeRequest = Schema.encodeSync(ProtocolRequestMsgPack) + +/** + * @since 1.0.0 + * @category protocol + */ +export const ProtocolResponse = Schema.Union( + Hello, + Ack, + Changes, + ChunkedMessage, + Pong +) + +/** + * @since 1.0.0 + * @category protocol + */ +export const ProtocolResponseMsgPack = MsgPack.schema(ProtocolResponse) + +/** + * @since 1.0.0 + * @category protocol + */ +export const decodeResponse = Schema.decodeSync(ProtocolResponseMsgPack) + +/** + * @since 1.0.0 + * @category protocol + */ +export const encodeResponse = Schema.encodeSync(ProtocolResponseMsgPack) + +/** + * @since 1.0.0 + * @category change + */ +export class RemoteAdditions + extends Schema.TaggedClass("@effect/experimental/EventLogRemote/RemoveAdditions")( + "RemoveAdditions", + { entries: Schema.Array(RemoteEntry) } + ) +{} + +const constChunkSize = 512_000 + +/** + * @since 1.0.0 + * @category construtors + */ +export const fromSocket = (options?: { + readonly disablePing?: boolean +}): Effect.Effect< + void, + never, + Scope.Scope | EventLog | EventLogEncryption | Socket.Socket +> => + Effect.gen(function*() { + const log = yield* EventLog + const socket = yield* Socket.Socket + const encryption = yield* EventLogEncryption + const scope = yield* Effect.scope + const writeRaw = yield* socket.writer + + function* writeGen(request: typeof ProtocolRequest.Type) { + const data = encodeRequest(request) + if (request._tag !== "WriteEntries" || data.byteLength <= constChunkSize) { + return yield* writeRaw(data) + } + const id = request.id + for (const part of ChunkedMessage.split(id, data)) { + yield* writeRaw(encodeRequest(part)) + } + } + + const write = (request: typeof ProtocolRequest.Type) => Effect.gen(() => writeGen(request)) + + yield* Effect.gen(function*() { + let pendingCounter = 0 + const pending = new Map + readonly deferred: Deferred.Deferred + readonly publicKey: string + }>() + const chunks = new Map + count: number + bytes: number + }>() + + const subscriptions = yield* RcMap.make({ + lookup: (publicKey: string) => + Effect.acquireRelease( + Mailbox.make(), + (mailbox) => + Effect.zipRight( + mailbox.shutdown, + Effect.ignoreLogged(write(new StopChanges({ publicKey }))) + ) + ) + }) + const identities = new WeakMap() + const badPing = yield* Deferred.make() + + let latestPing = 0 + let latestPong = 0 + + if (options?.disablePing !== true) { + yield* Effect.suspend(() => { + if (latestPing !== latestPong) { + return Deferred.fail(badPing, new Error("Ping timeout")) + } + return write(new Ping({ id: ++latestPing })) + }).pipe( + Effect.delay(10000), + Effect.forever, + Effect.fork, + Effect.interruptible + ) + } + + function handleMessage(res: typeof ProtocolResponse.Type) { + switch (res._tag) { + case "Hello": { + return log.registerRemote({ + id: res.remoteId, + write: (identity, entries) => + Effect.gen(function*() { + const encrypted = yield* encryption.encrypt(identity, entries) + const deferred = yield* Deferred.make() + const id = pendingCounter++ + pending.set(id, { + entries, + deferred, + publicKey: identity.publicKey + }) + yield* Effect.orDie(write( + new WriteEntries({ + publicKey: identity.publicKey, + id, + iv: encrypted.iv, + encryptedEntries: encrypted.encryptedEntries.map((encryptedEntry, i) => ({ + entryId: entries[i].id, + encryptedEntry + })) + }) + )) + yield* Deferred.await(deferred) + }), + changes: (identity, startSequence) => + Effect.gen(function*() { + const mailbox = yield* RcMap.get(subscriptions, identity.publicKey) + identities.set(mailbox, identity) + yield* Effect.orDie(write( + new RequestChanges({ + publicKey: identity.publicKey, + startSequence + }) + )) + return mailbox + }) + }).pipe(Scope.extend(scope)) + } + case "Ack": { + return Effect.gen(function*() { + const entry = pending.get(res.id) + if (!entry) return + pending.delete(res.id) + const { deferred, entries, publicKey } = entry + const remoteEntries = res.sequenceNumbers.map((sequenceNumber, i) => { + const entry = entries[i] + return new RemoteEntry({ + remoteSequence: sequenceNumber, + entry + }) + }) + const mailbox = yield* RcMap.get(subscriptions, publicKey) + yield* mailbox.offerAll(remoteEntries) + yield* Deferred.done(deferred, Exit.void) + }) + } + case "Pong": { + latestPong = res.id + if (res.id === latestPing) { + return + } + return Effect.fail(new Error("Pong id mismatch")) + } + case "Changes": { + return Effect.gen(function*() { + const mailbox = yield* RcMap.get(subscriptions, res.publicKey) + const identity = identities.get(mailbox)! + const entries = yield* encryption.decrypt(identity, res.entries) + yield* mailbox.offerAll(entries) + }).pipe(Effect.scoped) + } + case "ChunkedMessage": { + const data = ChunkedMessage.join(chunks, res) + if (!data) return + return handleMessage(decodeResponse(data)) + } + } + } + + return yield* socket.run((data) => handleMessage(decodeResponse(data))).pipe( + Effect.raceFirst(Deferred.await(badPing)) + ) + }).pipe( + Effect.scoped, + Effect.tapErrorCause(Effect.logDebug), + Effect.retry({ + schedule: Schedule.exponential(100).pipe( + Schedule.union(Schedule.spaced(5000)) + ) + }), + Effect.annotateLogs({ + service: "EventLogRemote", + method: "fromSocket" + }), + Effect.forkScoped, + Effect.interruptible + ) + }) + +/** + * @since 1.0.0 + * @category construtors + */ +export const fromWebSocket = ( + url: string, + options?: { + readonly disablePing?: boolean + } +): Effect.Effect => + Effect.gen(function*() { + const socket = yield* Socket.makeWebSocket(url) + return yield* fromSocket(options).pipe( + Effect.provideService(Socket.Socket, socket) + ) + }) + +/** + * @since 1.0.0 + * @category layers + */ +export const layerWebSocket = ( + url: string, + options?: { + readonly disablePing?: boolean + } +): Layer.Layer< + never, + never, + | Socket.WebSocketConstructor + | EventLog + | EventLogEncryption +> => Layer.scopedDiscard(fromWebSocket(url, options)) + +/** + * @since 1.0.0 + * @category layers + */ +export const layerWebSocketBrowser = ( + url: string, + options?: { + readonly disablePing?: boolean + } +): Layer.Layer => + layerWebSocket(url, options).pipe( + Layer.provide([layerSubtle, Socket.layerWebSocketConstructorGlobal]) + ) diff --git a/repos/effect/packages/experimental/src/EventLogServer.ts b/repos/effect/packages/experimental/src/EventLogServer.ts new file mode 100644 index 0000000..6f4cab6 --- /dev/null +++ b/repos/effect/packages/experimental/src/EventLogServer.ts @@ -0,0 +1,288 @@ +/** + * @since 1.0.0 + */ +import type * as HttpServerError from "@effect/platform/HttpServerError" +import * as HttpServerRequest from "@effect/platform/HttpServerRequest" +import * as HttpServerResponse from "@effect/platform/HttpServerResponse" +import * as MsgPack from "@effect/platform/MsgPack" +import type * as Socket from "@effect/platform/Socket" +import * as Chunk from "effect/Chunk" +import * as Context from "effect/Context" +import * as Effect from "effect/Effect" +import * as FiberMap from "effect/FiberMap" +import * as Layer from "effect/Layer" +import * as Mailbox from "effect/Mailbox" +import * as PubSub from "effect/PubSub" +import * as RcMap from "effect/RcMap" +import * as Schema from "effect/Schema" +import type * as Scope from "effect/Scope" +import * as Uuid from "uuid" +import type { RemoteId } from "./EventJournal.js" +import { EntryId, makeRemoteId } from "./EventJournal.js" +import { EncryptedRemoteEntry } from "./EventLogEncryption.js" +import type { ProtocolRequest, ProtocolResponse } from "./EventLogRemote.js" +import { Ack, Changes, ChunkedMessage, decodeRequest, encodeResponse, Hello, Pong } from "./EventLogRemote.js" + +const constChunkSize = 512_000 + +/** + * @since 1.0.0 + * @category constructors + */ +export const makeHandler: Effect.Effect< + (socket: Socket.Socket) => Effect.Effect, + never, + Storage +> = Effect.gen(function*() { + const storage = yield* Storage + const remoteId = yield* storage.getId + let chunkId = 0 + + function* handler(socket: Socket.Socket) { + const subscriptions = yield* FiberMap.make() + const writeRaw = yield* socket.writer + const chunks = new Map + count: number + bytes: number + }>() + let latestSequence = -1 + + function* writeGen(response: typeof ProtocolResponse.Type) { + const data = encodeResponse(response) + if (response._tag !== "Changes" || data.byteLength <= constChunkSize) { + return yield* writeRaw(data) + } + const id = chunkId++ + for (const part of ChunkedMessage.split(id, data)) { + yield* writeRaw(encodeResponse(part)) + } + } + const write = (response: typeof ProtocolResponse.Type) => Effect.gen(() => writeGen(response)) + + yield* Effect.fork(write(new Hello({ remoteId }))) + + function handleRequest(request: typeof ProtocolRequest.Type) { + switch (request._tag) { + case "Ping": { + return write(new Pong({ id: request.id })) + } + case "WriteEntries": { + if (request.encryptedEntries.length === 0) { + return write( + new Ack({ + id: request.id, + sequenceNumbers: [] + }) + ) + } + return Effect.gen(function*() { + const entries = request.encryptedEntries.map(({ encryptedEntry, entryId }) => + new PersistedEntry({ + entryId, + iv: request.iv, + encryptedEntry + }) + ) + const encrypted = yield* storage.write(request.publicKey, entries) + latestSequence = encrypted[encrypted.length - 1].sequence + return yield* write( + new Ack({ + id: request.id, + sequenceNumbers: encrypted.map((e) => e.sequence) + }) + ) + }) + } + case "RequestChanges": { + return Effect.gen(function*() { + const changes = yield* storage.changes(request.publicKey, request.startSequence) + return yield* changes.takeAll.pipe( + Effect.flatMap(function([entries]) { + const latestEntries: Array = [] + for (const entry of entries) { + if (entry.sequence <= latestSequence) continue + latestEntries.push(entry) + latestSequence = entry.sequence + } + if (latestEntries.length === 0) return Effect.void + return write( + new Changes({ + publicKey: request.publicKey, + entries: Chunk.toReadonlyArray(entries) + }) + ) + }), + Effect.forever + ) + }).pipe( + Effect.scoped, + FiberMap.run(subscriptions, request.publicKey) + ) + } + case "StopChanges": { + return FiberMap.remove(subscriptions, request.publicKey) + } + case "ChunkedMessage": { + const data = ChunkedMessage.join(chunks, request) + if (!data) return + return handleRequest(decodeRequest(data)) + } + } + } + + yield* socket.run((data) => handleRequest(decodeRequest(data))).pipe(Effect.catchAllCause(Effect.logDebug)) + } + + return (socket) => + Effect.gen(() => handler(socket)).pipe(Effect.annotateLogs({ + module: "EventLogServer" + })) +}) + +/** + * @since 1.0.0 + * @category websockets + */ +export const makeHandlerHttp: Effect.Effect< + Effect.Effect< + HttpServerResponse.HttpServerResponse, + HttpServerError.RequestError | Socket.SocketError, + HttpServerRequest.HttpServerRequest | Scope.Scope + >, + never, + Storage +> = Effect.gen(function*() { + const handler = yield* makeHandler + + return Effect.gen(function*() { + const request = yield* HttpServerRequest.HttpServerRequest + const socket = yield* request.upgrade + yield* handler(socket) + return HttpServerResponse.empty() + }).pipe(Effect.annotateLogs({ + module: "EventLogServer" + })) +}) + +/** + * @since 1.0.0 + * @category storage + */ +export class PersistedEntry extends Schema.Class("@effect/experimental/EventLogServer/PersistedEntry")({ + entryId: EntryId, + iv: Schema.Uint8ArrayFromSelf, + encryptedEntry: Schema.Uint8ArrayFromSelf +}) { + /** + * @since 1.0.0 + */ + static fromMsgPack = MsgPack.schema(PersistedEntry) + + /** + * @since 1.0.0 + */ + static encode = Schema.encodeSync(this.fromMsgPack) + + /** + * @since 1.0.0 + */ + get entryIdString(): string { + return Uuid.stringify(this.entryId) + } +} + +/** + * @since 1.0.0 + * @category storage + */ +export class Storage extends Context.Tag("@effect/experimental/EventLogServer/Storage")< + Storage, + { + readonly getId: Effect.Effect + readonly write: ( + publicKey: string, + entries: ReadonlyArray + ) => Effect.Effect> + readonly entries: ( + publicKey: string, + startSequence: number + ) => Effect.Effect> + readonly changes: ( + publicKey: string, + startSequence: number + ) => Effect.Effect, never, Scope.Scope> + } +>() {} + +/** + * @since 1.0.0 + * @category storage + */ +export const makeStorageMemory: Effect.Effect = Effect.gen(function*() { + const knownIds = new Map() + const journals = new Map>() + const remoteId = makeRemoteId() + const ensureJournal = (publicKey: string) => { + let journal = journals.get(publicKey) + if (journal) return journal + journal = [] + journals.set(publicKey, journal) + return journal + } + const pubsubs = yield* RcMap.make({ + lookup: (_publicKey: string) => + Effect.acquireRelease( + PubSub.unbounded(), + PubSub.shutdown + ), + idleTimeToLive: 60000 + }) + + return Storage.of({ + getId: Effect.succeed(remoteId), + write: (publicKey, entries) => + Effect.gen(function*() { + const active = yield* RcMap.keys(pubsubs) + const pubsub = active.includes(publicKey) ? yield* RcMap.get(pubsubs, publicKey) : undefined + const journal = ensureJournal(publicKey) + const encryptedEntries: Array = [] + for (const entry of entries) { + const idString = entry.entryIdString + if (knownIds.has(idString)) continue + const encrypted = EncryptedRemoteEntry.make({ + sequence: journal.length, + entryId: entry.entryId, + iv: entry.iv, + encryptedEntry: entry.encryptedEntry + }) + encryptedEntries.push(encrypted) + knownIds.set(idString, encrypted.sequence) + journal.push(encrypted) + pubsub?.unsafeOffer(encrypted) + } + return encryptedEntries + }).pipe(Effect.scoped), + entries: (publicKey, startSequence) => Effect.sync(() => ensureJournal(publicKey).slice(startSequence)), + changes: (publicKey, startSequence) => + Effect.gen(function*() { + const mailbox = yield* Mailbox.make() + const pubsub = yield* RcMap.get(pubsubs, publicKey) + const queue = yield* pubsub.subscribe + yield* mailbox.offerAll(ensureJournal(publicKey).slice(startSequence)) + yield* queue.takeBetween(1, Number.MAX_SAFE_INTEGER).pipe( + Effect.tap((chunk) => mailbox.offerAll(chunk)), + Effect.forever, + Effect.forkScoped, + Effect.interruptible + ) + return mailbox + }) + }) +}) + +/** + * @since 1.0.0 + * @category storage + */ +export const layerStorageMemory: Layer.Layer = Layer.scoped(Storage, makeStorageMemory) diff --git a/repos/effect/packages/experimental/src/EventLogServer/Cloudflare.ts b/repos/effect/packages/experimental/src/EventLogServer/Cloudflare.ts new file mode 100644 index 0000000..e105e26 --- /dev/null +++ b/repos/effect/packages/experimental/src/EventLogServer/Cloudflare.ts @@ -0,0 +1,188 @@ +/** + * @since 1.0.0 + */ +/// +import { DurableObject } from "cloudflare:workers" +import * as Cause from "effect/Cause" +import * as Effect from "effect/Effect" +import type * as Layer from "effect/Layer" +import * as ManagedRuntime from "effect/ManagedRuntime" +import { RemoteId } from "../EventJournal.js" +import type { EncryptedRemoteEntry } from "../EventLogEncryption.js" +import * as EventLogRemote from "../EventLogRemote.js" +import * as EventLogServer from "../EventLogServer.js" + +/** + * @since 1.0.0 + * @category DurableObject + */ +export abstract class EventLogDurableObject extends DurableObject { + /** + * @since 1.0.0 + */ + readonly runtime: ManagedRuntime.ManagedRuntime + + constructor(options: { + readonly ctx: DurableObjectState + readonly env: unknown + readonly storageLayer: Layer.Layer + }) { + super(options.ctx, options.env) + this.ctx.setHibernatableWebSocketEventTimeout(5000) + this.runtime = ManagedRuntime.make(options.storageLayer) + } + + /** + * @since 1.0.0 + */ + webSocketMessage(ws: WebSocket, message: string | ArrayBuffer) { + return this.handleRequest( + ws, + EventLogRemote.decodeRequest( + message instanceof ArrayBuffer + ? new Uint8Array(message) + : new TextEncoder().encode(message) + ) + ) + } + + private chunks = new Map< + number, + { + readonly parts: Array + count: number + bytes: number + } + >() + /** + * @since 1.0.0 + */ + private async handleRequest( + ws: WebSocket, + request: typeof EventLogRemote.ProtocolRequest.Type + ): Promise { + switch (request._tag) { + case "WriteEntries": { + return Effect.gen(this, function*() { + const storage = yield* EventLogServer.Storage + const entries = request.encryptedEntries.map( + ({ encryptedEntry, entryId }) => + new EventLogServer.PersistedEntry({ + entryId, + iv: request.iv, + encryptedEntry + }) + ) + const encryptedEntries = yield* storage.write( + request.publicKey, + entries + ) + ws.send( + EventLogRemote.encodeResponse( + new EventLogRemote.Ack({ + id: request.id, + sequenceNumbers: encryptedEntries.map((_) => _.sequence) + }) + ) + ) + const changes = this.encodeChanges( + request.publicKey, + encryptedEntries + ) + for (const peer of this.ctx.getWebSockets()) { + if (peer === ws) continue + for (const change of changes) { + peer.send(change) + } + } + }).pipe(this.runtime.runPromise) + } + case "ChunkedMessage": { + const data = EventLogRemote.ChunkedMessage.join(this.chunks, request) + if (!data) return + return this.handleRequest(ws, EventLogRemote.decodeRequest(data)) + } + case "RequestChanges": { + return Effect.gen(this, function*() { + const storage = yield* EventLogServer.Storage + const entries = yield* storage.entries( + request.publicKey, + request.startSequence + ) + if (entries.length === 0) return + const changes = this.encodeChanges(request.publicKey, entries) + for (const change of changes) { + ws.send(change) + } + }).pipe(this.runtime.runPromise) + } + } + } + + /** + * @since 1.0.0 + */ + private encodeChanges( + publicKey: string, + entries: ReadonlyArray + ): ReadonlyArray { + let changes = [ + EventLogRemote.encodeResponse( + new EventLogRemote.Changes({ + publicKey, + entries + }) + ) + ] + if (changes[0].byteLength > 512_000) { + changes = EventLogRemote.ChunkedMessage.split( + Math.floor(Math.random() * 1_000_000_000), + changes[0] + ).map((_) => EventLogRemote.encodeResponse(_)) + } + return changes + } + + /** + * @since 1.0.0 + */ + webSocketError(_ws: WebSocket, error: Error): void { + this.runtime.runFork(Effect.logWarning(Cause.fail(error))) + } + + /** + * @since 1.0.0 + */ + webSocketClose(_ws: WebSocket, code: number, reason: string): void { + this.runtime.runFork(Effect.logWarning("WebSocket closed", { code, reason })) + } + + /** + * @since 1.0.0 + */ + async fetch(): Promise { + const webSocketPair = new WebSocketPair() + const [client, server] = Object.values(webSocketPair) + + this.ctx.acceptWebSocket(server) + + EventLogServer.Storage.pipe( + Effect.flatMap((_) => _.getId), + Effect.tap((remoteId) => { + server.send( + EventLogRemote.encodeResponse( + new EventLogRemote.Hello({ + remoteId: RemoteId.make(remoteId) + }) + ) + ) + }), + this.runtime.runFork + ) + + return new Response(null, { + status: 101, + webSocket: client + }) + } +} diff --git a/repos/effect/packages/experimental/src/Machine.ts b/repos/effect/packages/experimental/src/Machine.ts new file mode 100644 index 0000000..0b7c305 --- /dev/null +++ b/repos/effect/packages/experimental/src/Machine.ts @@ -0,0 +1,908 @@ +/** + * @since 1.0.0 + */ +import * as Arr from "effect/Array" +import * as Cause from "effect/Cause" +import * as Context from "effect/Context" +import * as Deferred from "effect/Deferred" +import * as Effect from "effect/Effect" +import * as Fiber from "effect/Fiber" +import * as FiberMap from "effect/FiberMap" +import * as FiberRef from "effect/FiberRef" +import * as FiberRefs from "effect/FiberRefs" +import * as FiberSet from "effect/FiberSet" +import { dual, identity, pipe } from "effect/Function" +import { globalValue } from "effect/GlobalValue" +import * as Option from "effect/Option" +import type * as ParseResult from "effect/ParseResult" +import type { Pipeable } from "effect/Pipeable" +import { pipeArguments } from "effect/Pipeable" +import * as PubSub from "effect/PubSub" +import * as Queue from "effect/Queue" +import * as Readable from "effect/Readable" +import type { Request } from "effect/Request" +import type * as Schedule from "effect/Schedule" +import * as Schema from "effect/Schema" +import type * as Scope from "effect/Scope" +import * as Stream from "effect/Stream" +import * as Subscribable from "effect/Subscribable" +import * as Tracer from "effect/Tracer" +import * as Procedure from "./Machine/Procedure.js" +import type { ProcedureList } from "./Machine/ProcedureList.js" +import type { SerializableProcedureList } from "./Machine/SerializableProcedureList.js" + +/** + * @since 1.0.0 + * @category procedures + */ +export * as procedures from "./Machine/ProcedureList.js" + +/** + * @since 1.0.0 + * @category procedures + */ +export * as serializable from "./Machine/SerializableProcedureList.js" + +export { + /** + * @since 1.0.0 + * @category symbols + */ + NoReply +} from "./Machine/Procedure.js" + +/** + * @since 1.0.0 + * @category type ids + */ +export const TypeId: unique symbol = Symbol.for("@effect/experimental/Machine") + +/** + * @since 1.0.0 + * @category type ids + */ +export type TypeId = typeof TypeId + +/** + * @since 1.0.0 + * @category models + */ +export interface Machine< + State, + Public extends Procedure.TaggedRequest.Any, + Private extends Procedure.TaggedRequest.Any, + Input, + InitErr, + R +> extends Pipeable { + readonly [TypeId]: TypeId + readonly initialize: Machine.Initialize + readonly retryPolicy: Schedule.Schedule | undefined +} + +/** + * @since 1.0.0 + * @category type ids + */ +export const SerializableTypeId: unique symbol = Symbol.for("@effect/experimental/Machine/Serializable") + +/** + * @since 1.0.0 + * @category type ids + */ +export type SerializableTypeId = typeof SerializableTypeId + +/** + * @since 1.0.0 + * @category models + */ +export interface SerializableMachine< + State, + Public extends Schema.TaggedRequest.All, + Private extends Schema.TaggedRequest.All, + Input, + InitErr, + R, + SR +> extends + Machine< + State, + Public, + Private, + Input, + InitErr, + R + > +{ + readonly [SerializableTypeId]: SerializableTypeId + readonly schemaInput: Schema.Schema + readonly schemaState: Schema.Schema +} + +/** + * @since 1.0.0 + * @category type ids + */ +export const ActorTypeId: unique symbol = Symbol.for("@effect/experimental/Machine/Actor") + +/** + * @since 1.0.0 + * @category type ids + */ +export type ActorTypeId = typeof ActorTypeId + +/** + * @since 1.0.0 + * @category errors + */ +export class MachineDefect extends Schema.TaggedError()("MachineDefect", { + cause: Schema.Defect +}) { + /** + * @since 1.0.0 + */ + static wrap(effect: Effect.Effect): Effect.Effect { + return Effect.catchAllCause( + Effect.orDie(effect), + (cause) => Effect.fail(new MachineDefect({ cause: Cause.squash(cause) })) + ) + } +} + +/** + * @since 1.0.0 + * @category tags + */ +export class MachineContext extends Context.Tag("@effect/experimental/Machine/Context")< + MachineContext, + Procedure.Procedure.BaseContext +>() {} + +/** + * @since 1.0.0 + * @category models + */ +export declare namespace Machine { + /** + * @since 1.0.0 + * @category models + */ + export type Any = + | Machine + | Machine + | Machine + | Machine + | Machine + + /** + * @since 1.0.0 + * @category models + */ + export type Initialize< + Input, + State, + Public extends Procedure.TaggedRequest.Any, + Private extends Procedure.TaggedRequest.Any, + R, + E, + InitR + > = ( + input: Input, + previousState?: State | undefined + ) => Effect.Effect, E, InitR> + + /** + * @since 1.0.0 + * @category models + */ + export type InitializeSerializable< + Input, + State, + Public extends Schema.TaggedRequest.All, + Private extends Schema.TaggedRequest.All, + R, + E, + InitR + > = ( + input: Input, + previousState?: State | undefined + ) => Effect.Effect, E, InitR> + + /** + * @since 1.0.0 + */ + export type Public = M extends Machine ? Public + : never + + /** + * @since 1.0.0 + */ + export type Private = M extends Machine ? + Private + : never + + /** + * @since 1.0.0 + */ + export type State = M extends Machine ? State + : never + + /** + * @since 1.0.0 + */ + export type InitError = M extends Machine ? + InitErr + : never + + /** + * @since 1.0.0 + */ + export type Context = M extends Machine ? R + : never + + /** + * @since 1.0.0 + */ + export type Input = M extends Machine ? Input + : never + + /** + * @since 1.0.0 + */ + export type AddContext = M extends SerializableMachine< + infer State, + infer Public, + infer Private, + infer Input, + infer InitErr, + infer R2, + infer SR + > ? SerializableMachine< + State, + Public, + Private, + Input, + InitErr | E, + R | R2, + SR + > : + M extends Machine ? Machine< + State, + Public, + Private, + Input, + InitErr | E, + R | R2 + > : + never +} + +/** + * @since 1.0.0 + * @category models + */ +export interface Actor extends Subscribable.Subscribable> { + readonly [ActorTypeId]: ActorTypeId + readonly machine: M + readonly input: Machine.Input + readonly send: >(request: Req) => Effect.Effect< + Request.Success, + Request.Error + > + readonly join: Effect.Effect | MachineDefect> +} + +const ActorProto = { + [ActorTypeId]: ActorTypeId, + [Readable.TypeId]: Readable.TypeId, + [Subscribable.TypeId]: Subscribable.TypeId, + pipe() { + return pipeArguments(this, arguments) + } +} + +/** + * @since 1.0.0 + * @category models + */ +export interface SerializableActor extends Actor { + readonly sendUnknown: (request: unknown) => Effect.Effect< + Schema.ExitEncoded, + ParseResult.ParseError + > +} + +/** + * @since 1.0.0 + * @category constructors + */ +export const make: { + ( + initialize: Effect.Effect, InitErr, R> + ): Machine> + ( + initialize: Machine.Initialize + ): Machine> +} = ( + initialize: + | Machine.Initialize + | Effect.Effect, InitErr, R> +): Machine> => ({ + [TypeId]: TypeId, + initialize: Effect.isEffect(initialize) ? (() => initialize) : initialize as any, + retryPolicy: undefined, + pipe() { + return pipeArguments(this, arguments) + } +}) + +/** + * @since 1.0.0 + * @category constructors + */ +export const makeWith = (): { + ( + initialize: Effect.Effect, InitErr, R> + ): Machine> + ( + initialize: Machine.Initialize + ): Machine> +} => make + +/** + * @since 1.0.0 + * @category constructors + */ +export const makeSerializable: { + < + State, + IS, + RS, + Public extends Schema.TaggedRequest.All, + Private extends Schema.TaggedRequest.All, + InitErr, + R + >( + options: { + readonly state: Schema.Schema + readonly input?: undefined + }, + initialize: + | Effect.Effect, InitErr, R> + | Machine.InitializeSerializable + ): SerializableMachine, RS> + < + State, + IS, + RS, + Input, + II, + RI, + Public extends Schema.TaggedRequest.All, + Private extends Schema.TaggedRequest.All, + InitErr, + R + >( + options: { + readonly state: Schema.Schema + readonly input: Schema.Schema + }, + initialize: Machine.InitializeSerializable + ): SerializableMachine, RS | RI> +} = < + State, + IS, + RS, + Input, + II, + RI, + Public extends Schema.TaggedRequest.All, + Private extends Schema.TaggedRequest.All, + InitErr, + R +>( + options: { + readonly state: Schema.Schema + readonly input?: Schema.Schema | undefined + }, + initialize: + | Machine.InitializeSerializable + | Effect.Effect, InitErr, R> +): SerializableMachine, RS | RI> => (({ + [TypeId]: TypeId, + [SerializableTypeId]: SerializableTypeId, + initialize: Effect.isEffect(initialize) ? (() => initialize) : initialize as any, + identifier: "SerializableMachine", + retryPolicy: undefined, + schemaInput: options.input as any, + schemaState: options.state as any, + + pipe() { + return pipeArguments(this, arguments) + } +}) as any) + +/** + * @since 1.0.0 + * @category combinators + */ +export const retry: { + | MachineDefect, R>( + policy: Schedule.Schedule + ): (self: M) => Machine.AddContext + | MachineDefect, R>( + self: M, + policy: Schedule.Schedule + ): Machine.AddContext +} = dual(2, | MachineDefect, R>( + self: M, + retryPolicy: Schedule.Schedule +): Machine.AddContext => (({ + ...self, + retryPolicy +}) as any)) + +/** + * @since 1.0.0 + * @category tracing + */ +export const currentTracingEnabled: FiberRef.FiberRef = globalValue( + "@effect/experimental/Machine/currentTracingEnabled", + () => FiberRef.unsafeMake(true) +) + +/** + * @since 1.0.0 + * @category tracing + */ +export const withTracingEnabled: { + (enabled: boolean): (effect: Effect.Effect) => Effect.Effect + (effect: Effect.Effect, enabled: boolean): Effect.Effect +} = dual( + 2, + (self: Effect.Effect, enabled: boolean) => Effect.locally(self, currentTracingEnabled, enabled) +) + +/** + * @since 1.0.0 + * @category runtime + */ +export const boot = < + M extends Machine.Any +>( + self: M, + ...[input, options]: [Machine.Input] extends [void] ? [ + input?: Machine.Input, + options?: { readonly previousState?: Machine.State } + ] : + [ + input: Machine.Input, + options?: { readonly previousState?: Machine.State } + ] +): Effect.Effect< + M extends { readonly [SerializableTypeId]: SerializableTypeId } ? SerializableActor : Actor, + never, + Machine.Context | Scope.Scope +> => + Effect.gen(function*() { + const context = yield* Effect.context>() + const requests = yield* Queue.unbounded< + readonly [ + Procedure.TaggedRequest.Any, + Deferred.Deferred, + Tracer.AnySpan | undefined, + addSpans: boolean + ] + >() + const pubsub = yield* Effect.acquireRelease( + PubSub.unbounded>(), + PubSub.shutdown + ) + const latch = yield* Deferred.make() + + let currentState: Machine.State = undefined as any + let runState: { + readonly identifier: string + readonly publicTags: Set + readonly decodeRequest: (u: unknown) => Effect.Effect, ParseResult.ParseError> + } = { + identifier: "Unknown", + publicTags: new Set(), + decodeRequest: undefined as any + } + + const requestContext = >(request: R) => + Effect.sync(() => { + const fiber = Option.getOrThrow(Fiber.getCurrentFiber()) + const fiberRefs = fiber.getFiberRefs() + const context = FiberRefs.getOrDefault(fiberRefs, FiberRef.currentContext) + + const deferred = Deferred.unsafeMake, Request.Error>(fiber.id()) + const span: Tracer.AnySpan | undefined = context.unsafeMap.get(Tracer.ParentSpan.key) + const addSpans = FiberRefs.getOrDefault(fiberRefs, currentTracingEnabled) + + return [request, deferred, span, addSpans] as const + }) + + const send = >(request: R) => + Effect.flatMap( + requestContext(request), + (item) => { + if (!item[3]) { + return Queue.offer(requests, item).pipe( + Effect.zipRight(Deferred.await(item[1])), + Effect.onInterrupt(() => Deferred.interrupt(item[1])) + ) + } + const [, deferred, span] = item + return Effect.useSpan(`Machine.send ${request._tag}`, { + parent: span, + attributes: { + "effect.machine": runState.identifier, + ...request + }, + kind: "client", + captureStackTrace: false + }, (span) => + Queue.offer(requests, [request, deferred, span, true]).pipe( + Effect.zipRight(Deferred.await(deferred)), + Effect.onInterrupt(() => Deferred.interrupt(deferred)) + )) + } + ) + + const sendIgnore = >(request: R) => + Effect.flatMap( + requestContext(request), + (item) => { + if (!item[3]) { + return Queue.offer(requests, item) + } + const [, deferred, span] = item + return Effect.useSpan(`Machine.sendIgnore ${request._tag}`, { + parent: span, + attributes: { + "effect.machine": runState.identifier, + ...request + }, + kind: "client", + captureStackTrace: false + }, (span) => Queue.offer(requests, [request, deferred, span, true])) + } + ) + + const sendExternal = >(request: R) => + Effect.suspend(() => + runState.publicTags.has(request._tag) + ? send(request) + : Effect.die(`Request ${request._tag} marked as internal`) + ) + + const sendUnknown = (u: unknown) => + Effect.suspend(() => + runState.decodeRequest(u).pipe( + Effect.flatMap((req) => + Effect.flatMap( + Effect.exit(send(req)), + (exit) => Schema.serializeExit(req, exit) + ) + ), + Effect.provide(context) + ) + ) as Effect.Effect, ParseResult.ParseError> + + const publishState = (newState: Machine.State) => { + if (currentState !== newState) { + currentState = newState + return PubSub.publish(pubsub, newState) + } + return Effect.void + } + + const run = Effect.gen(function*() { + const fiberSet = yield* FiberSet.make() + const fiberMap = yield* FiberMap.make() + + const fork = (effect: Effect.Effect) => + Effect.asVoid(FiberSet.run(fiberSet, MachineDefect.wrap(effect))) + const forkWith: { + (state: Machine.State): ( + effect: Effect.Effect + ) => Effect.Effect], never, R> + ( + effect: Effect.Effect, + state: Machine.State + ): Effect.Effect], never, R> + } = dual(2, ( + effect: Effect.Effect, + state: Machine.State + ): Effect.Effect], never, R> => + Effect.map(fork(effect), (_) => [_, state] as const)) + + const forkReplace: { + (id: string): (effect: Effect.Effect) => Effect.Effect + (effect: Effect.Effect, id: string): Effect.Effect + } = dual(2, (effect: Effect.Effect, id: string): Effect.Effect => + Effect.asVoid( + FiberMap.run(fiberMap, id, MachineDefect.wrap(effect)) + )) + const forkReplaceWith: { + ( + id: string, + state: Machine.State + ): (effect: Effect.Effect) => Effect.Effect], never, R> + ( + effect: Effect.Effect, + id: string, + state: Machine.State + ): Effect.Effect], never, R> + } = dual(3, ( + effect: Effect.Effect, + id: string, + state: Machine.State + ): Effect.Effect], never, R> => + Effect.map(forkReplace(effect, id), (_) => [_, state] as const)) + + const forkOne: { + (id: string): (effect: Effect.Effect) => Effect.Effect + (effect: Effect.Effect, id: string): Effect.Effect + } = dual(2, (effect: Effect.Effect, id: string): Effect.Effect => + Effect.asVoid(FiberMap.run(fiberMap, id, MachineDefect.wrap(effect), { onlyIfMissing: true }))) + + const forkOneWith: { + ( + id: string, + state: Machine.State + ): (effect: Effect.Effect) => Effect.Effect], never, R> + ( + effect: Effect.Effect, + id: string, + state: Machine.State + ): Effect.Effect], never, R> + } = dual(3, ( + effect: Effect.Effect, + id: string, + state: Machine.State + ): Effect.Effect], never, R> => + Effect.map( + forkOne(effect, id), + (_) => + [_, state] as const + )) + + const contextProto: Procedure.Procedure.ContextProto, Machine.State> = { + sendAwait: send, + send: sendIgnore, + unsafeSend: sendIgnore as any, + unsafeSendAwait: send as any, + fork, + forkWith, + forkOne, + forkOneWith, + forkReplace, + forkReplaceWith + } + + const procedures = yield* pipe( + self.initialize(input, currentState ?? options?.previousState) as Effect.Effect< + SerializableProcedureList, Machine.Public, Machine.Private, never>, + Machine.InitError + >, + Effect.provideService(MachineContext, contextProto) + ) + const procedureMap: Record< + string, + Procedure.Procedure, Machine.Context> + > = Object.fromEntries( + procedures.private.map((p) => [p.tag, p]).concat( + procedures.public.map((p) => [p.tag, p]) + ) + ) + + runState = { + identifier: procedures.identifier, + publicTags: new Set(procedures.public.map((p) => + p.tag + )), + decodeRequest: Schema.decodeUnknown( + Schema.Union( + ...Arr.filter( + procedures.public, + Procedure.isSerializable + ).map((p) => p.schema) + ) + ) + } + yield* publishState(procedures.initialState) + yield* Deferred.succeed(latch, void 0) + + const process = pipe( + Queue.take(requests), + Effect.flatMap(([request, deferred, span, addSpan]) => + Effect.flatMap(Deferred.isDone(deferred), (done) => { + if (done) { + return Effect.void + } + + const procedure = procedureMap[request._tag] + if (procedure === undefined) { + return Deferred.die(deferred, `Unknown request ${request._tag}`) + } + const context = Object.create(contextProto) + context.state = currentState + context.request = request + context.deferred = deferred + + let handler = Effect.matchCauseEffect( + procedure.handler(context), + { + onFailure: (e) => { + if (Cause.isFailure(e)) { + return Deferred.failCause(deferred, e) + } + // defects kill the actor + return Effect.zipRight( + Deferred.failCause(deferred, e), + Effect.failCause(e) + ) + }, + onSuccess: ([response, newState]) => { + if (response === Procedure.NoReply) { + return publishState(newState) + } + return Effect.zipRight( + publishState(newState), + Deferred.succeed(deferred, response) + ) + } + } + ) + if (addSpan) { + handler = Effect.withSpan(handler, `Machine.process ${request._tag}`, { + kind: "server", + parent: span, + attributes: { + "effect.machine": runState.identifier + }, + captureStackTrace: false + }) + } else if (span !== undefined) { + handler = Effect.provideService(handler, Tracer.ParentSpan, span) + } + + return handler + }) + ), + Effect.forever, + Effect.provideService(MachineContext, contextProto) + ) + + yield* pipe( + Effect.all([ + process, + FiberSet.join(fiberSet), + FiberMap.join(fiberMap) + ], { concurrency: "unbounded", discard: true }), + Effect.onExit((exit) => { + if (exit._tag === "Success") return Effect.die("absurd") + return Effect.flatMap( + Queue.takeAll(requests), + Effect.forEach(([, deferred]) => Deferred.failCause(deferred, exit.cause)) + ) + }), + Effect.tapErrorCause((cause) => + FiberRef.getWith( + FiberRef.unhandledErrorLogLevel, + Option.match({ + onNone: () => Effect.void, + onSome: (level) => + Effect.log(`Unhandled Machine (${runState.identifier}) failure`, cause).pipe( + Effect.locally(FiberRef.currentLogLevel, level) + ) + }) + ) + ), + Effect.catchAllDefect((cause) => Effect.fail(new MachineDefect({ cause }))) + ) + }).pipe(Effect.scoped) as Effect.Effect< + never, + MachineDefect | Machine.InitError + > + + const fiber = yield* pipe( + run, + self.retryPolicy ? + Effect.retry(self.retryPolicy) : + identity, + Effect.forkScoped, + Effect.interruptible + ) + + yield* Deferred.await(latch) + + return identity>(Object.assign(Object.create(ActorProto), { + machine: self, + input: input!, + get: Effect.sync(() => currentState), + changes: Stream.concat( + Stream.sync(() => currentState), + Stream.fromPubSub(pubsub) + ), + send: sendExternal, + sendUnknown, + join: Fiber.join(fiber) + })) as any + }) + +/** + * @since 1.0.0 + * @category runtime + */ +export const snapshot = < + State, + Public extends Schema.TaggedRequest.All, + Private extends Schema.TaggedRequest.All, + Input, + InitErr, + R, + SR +>( + self: Actor< + SerializableMachine< + State, + Public, + Private, + Input, + InitErr, + R, + SR + > + > +): Effect.Effect<[input: unknown, state: unknown], ParseResult.ParseError, SR> => + Effect.zip( + Schema.encode(self.machine.schemaInput)(self.input), + Effect.flatMap(self.get, Schema.encode(self.machine.schemaState)) + ) + +/** + * @since 1.0.0 + * @category runtime + */ +export const restore = < + State, + Public extends Schema.TaggedRequest.All, + Private extends Schema.TaggedRequest.All, + Input, + InitErr, + R, + SR +>( + self: SerializableMachine< + State, + Public, + Private, + Input, + InitErr, + R, + SR + >, + snapshot: readonly [input: unknown, state: unknown] +): Effect.Effect< + Actor< + SerializableMachine< + State, + Public, + Private, + Input, + InitErr, + R, + SR + > + >, + ParseResult.ParseError, + R | SR +> => + Effect.flatMap( + Schema.decodeUnknown(Schema.Tuple(self.schemaInput, self.schemaState))(snapshot), + ([input, previousState]) => (boot as any)(self, input, { previousState }) + ) diff --git a/repos/effect/packages/experimental/src/Machine/Procedure.ts b/repos/effect/packages/experimental/src/Machine/Procedure.ts new file mode 100644 index 0000000..3dec66d --- /dev/null +++ b/repos/effect/packages/experimental/src/Machine/Procedure.ts @@ -0,0 +1,254 @@ +/** + * @since 1.0.0 + */ +import type * as Deferred from "effect/Deferred" +import type * as Effect from "effect/Effect" +import { type Pipeable, pipeArguments } from "effect/Pipeable" +import * as Predicate from "effect/Predicate" +import type { Request } from "effect/Request" +import type * as Schema from "effect/Schema" + +/** + * @since 1.0.0 + * @category type ids + */ +export const TypeId: unique symbol = Symbol.for("@effect/experimental/Machine/Procedure") + +/** + * @since 1.0.0 + * @category type ids + */ +export type TypeId = typeof TypeId + +/** + * @since 1.0.0 + * @category models + */ +export interface TaggedRequest extends Request { + readonly _tag: Tag +} + +/** + * @since 1.0.0 + * @category models + */ +export declare namespace TaggedRequest { + /** + * @since 1.0.0 + * @category models + */ + export type Any = + | TaggedRequest + | TaggedRequest +} + +/** + * @since 1.0.0 + * @category models + */ +export interface Procedure extends Pipeable { + readonly [TypeId]: TypeId + readonly tag: Request["_tag"] + readonly handler: Handler +} + +/** + * @since 1.0.0 + * @category type ids + */ +export const SerializableTypeId: unique symbol = Symbol.for("@effect/experimental/Machine/SerializableProcedure") + +/** + * @since 1.0.0 + * @category type ids + */ +export type SerializableTypeId = typeof SerializableTypeId + +/** + * @since 1.0.0 + * @category refinements + */ +export const isSerializable = (u: unknown): u is SerializableProcedure => + Predicate.hasProperty(u, SerializableTypeId) + +/** + * @since 1.0.0 + * @category models + */ +export interface SerializableProcedure + extends Procedure +{ + readonly [SerializableTypeId]: SerializableTypeId + readonly schema: Schema.Schema +} + +/** + * @since 1.0.0 + * @category symbols + */ +export const NoReply = Symbol.for("@effect/experimental/Machine/Procedure/NoReply") + +/** + * @since 1.0.0 + * @category symbols + */ +export type NoReply = typeof NoReply + +/** + * @since 1.0.0 + * @category models + */ +export type Handler< + Request extends TaggedRequest.Any, + State, + Requests extends TaggedRequest.Any, + R +> = ( + context: Procedure.Context +) => Effect.Effect< + readonly [response: Request.Success | NoReply, state: State], + Request.Error, + R +> + +/** + * @since 1.0.0 + * @category models + */ +export declare namespace Procedure { + /** + * @since 1.0.0 + * @category models + */ + export interface BaseContext { + readonly fork: (effect: Effect.Effect) => Effect.Effect + readonly forkOne: { + (id: string): (effect: Effect.Effect) => Effect.Effect + (effect: Effect.Effect, id: string): Effect.Effect + } + readonly forkReplace: { + (id: string): (effect: Effect.Effect) => Effect.Effect + (effect: Effect.Effect, id: string): Effect.Effect + } + readonly unsafeSend: (request: Req) => Effect.Effect + readonly unsafeSendAwait: (request: Req) => Effect.Effect< + Request.Success, + Request.Error + > + } + + /** + * @since 1.0.0 + * @category models + */ + export interface ContextProto extends BaseContext { + readonly send: (request: Req) => Effect.Effect + readonly sendAwait: (request: Req) => Effect.Effect< + Request.Success, + Request.Error + > + readonly forkWith: { + (state: State): ( + effect: Effect.Effect + ) => Effect.Effect + ( + effect: Effect.Effect, + state: State + ): Effect.Effect + } + readonly forkOneWith: { + ( + id: string, + state: State + ): (effect: Effect.Effect) => Effect.Effect + ( + effect: Effect.Effect, + id: string, + state: State + ): Effect.Effect + } + readonly forkReplaceWith: { + ( + id: string, + state: State + ): (effect: Effect.Effect) => Effect.Effect + ( + effect: Effect.Effect, + id: string, + state: State + ): Effect.Effect + } + } + + /** + * @since 1.0.0 + * @category models + */ + export interface Context + extends ContextProto + { + readonly request: Request + readonly state: State + readonly deferred: Deferred.Deferred< + Request.Success, + Request.Error + > + } + + /** + * @since 1.0.0 + * @category models + */ + export type InferRequest

= P extends Procedure ? Req : never + + /** + * @since 1.0.0 + * @category models + */ + export type InferContext

= P extends Procedure ? R : never +} + +/** + * @since 1.0.0 + * @category constructors + */ +export const make = () => +() => +( + tag: Req["_tag"], + handler: Handler +): Procedure => ({ + [TypeId]: TypeId, + handler, + tag, + pipe() { + return pipeArguments(this, arguments) + } +}) + +/** + * @since 1.0.0 + * @category constructors + */ +export const makeSerializable = < + Requests extends TaggedRequest.Any, + State +>() => +< + Req extends Schema.TaggedRequest.All, + IS, + R, + RS +>( + schema: Schema.Schema & { readonly _tag: Req["_tag"] }, + handler: Handler +): SerializableProcedure> => ({ + [TypeId]: TypeId, + [SerializableTypeId]: SerializableTypeId, + schema: schema as any, + handler, + tag: schema._tag, + pipe() { + return pipeArguments(this, arguments) + } +}) diff --git a/repos/effect/packages/experimental/src/Machine/ProcedureList.ts b/repos/effect/packages/experimental/src/Machine/ProcedureList.ts new file mode 100644 index 0000000..0194a06 --- /dev/null +++ b/repos/effect/packages/experimental/src/Machine/ProcedureList.ts @@ -0,0 +1,271 @@ +/** + * @since 1.0.0 + */ +import * as Effect from "effect/Effect" +import * as Effectable from "effect/Effectable" +import { dual } from "effect/Function" +import type * as Types from "effect/Types" +import * as Procedure from "./Procedure.js" + +/** + * @since 1.0.0 + * @category type ids + */ +export const TypeId: unique symbol = Symbol.for("@effect/experimental/Machine/ProcedureList") + +/** + * @since 1.0.0 + * @category type ids + */ +export type TypeId = typeof TypeId + +/** + * @since 1.0.0 + * @category models + */ +export interface ProcedureList< + State, + Public extends Procedure.TaggedRequest.Any, + Private extends Procedure.TaggedRequest.Any, + R +> extends Effect.Effect> { + readonly [TypeId]: TypeId + readonly initialState: State + readonly public: ReadonlyArray> + readonly private: ReadonlyArray> + readonly identifier: string +} + +const Proto = { + ...Effectable.CommitPrototype, + [TypeId]: TypeId, + commit() { + return Effect.succeed(this) + } +} + +const makeProto = ( + options: { + readonly initialState: State + readonly public: ReadonlyArray> + readonly private: ReadonlyArray> + readonly identifier: string + } +): ProcedureList => Object.assign(Object.create(Proto), options) + +/** + * @since 1.0.0 + * @category constructors + */ +export const make = (initialState: State, options?: { + readonly identifier?: string +}): ProcedureList => + makeProto({ + initialState, + public: [], + private: [], + identifier: options?.identifier ?? "Unknown" + }) + +/** + * @since 1.0.0 + * @category combinators + */ +export const addProcedure: { + < + Req extends Procedure.TaggedRequest.Any, + State, + R2 + >( + procedure: Procedure.Procedure + ): < + Public extends Procedure.TaggedRequest.Any, + Private extends Procedure.TaggedRequest.Any, + R + >( + self: ProcedureList + ) => ProcedureList + < + State, + Public extends Procedure.TaggedRequest.Any, + Private extends Procedure.TaggedRequest.Any, + R, + Req extends Procedure.TaggedRequest.Any, + R2 + >( + self: ProcedureList, + procedure: Procedure.Procedure + ): ProcedureList +} = dual( + 2, + < + State, + Public extends Procedure.TaggedRequest.Any, + Private extends Procedure.TaggedRequest.Any, + R, + Req extends Procedure.TaggedRequest.Any, + R2 + >( + self: ProcedureList, + procedure: Procedure.Procedure + ): ProcedureList => + makeProto({ + ...self, + public: [...self.public, procedure] as any + }) +) + +/** + * @since 1.0.0 + * @category combinators + */ +export const addProcedurePrivate: { + < + Req extends Procedure.TaggedRequest.Any, + State, + R2 + >( + procedure: Procedure.Procedure + ): < + Public extends Procedure.TaggedRequest.Any, + Private extends Procedure.TaggedRequest.Any, + R + >( + self: ProcedureList + ) => ProcedureList + < + State, + Public extends Procedure.TaggedRequest.Any, + Private extends Procedure.TaggedRequest.Any, + R, + Req extends Procedure.TaggedRequest.Any, + R2 + >( + self: ProcedureList, + procedure: Procedure.Procedure + ): ProcedureList +} = dual( + 2, + < + State, + Public extends Procedure.TaggedRequest.Any, + Private extends Procedure.TaggedRequest.Any, + R, + Req extends Procedure.TaggedRequest.Any, + R2 + >( + self: ProcedureList, + procedure: Procedure.Procedure + ): ProcedureList => + makeProto({ + ...self, + private: [...self.private, procedure] as any + }) +) + +/** + * @since 1.0.0 + * @category combinators + */ +export const add = (): { + < + State, + Public extends Procedure.TaggedRequest.Any, + Private extends Procedure.TaggedRequest.Any, + R2 + >( + tag: Req["_tag"], + handler: Procedure.Handler, Types.NoInfer | Types.NoInfer, R2> + ): ( + self: ProcedureList + ) => ProcedureList + < + State, + Public extends Procedure.TaggedRequest.Any, + Private extends Procedure.TaggedRequest.Any, + R, + R2 + >( + self: ProcedureList, + tag: Req["_tag"], + handler: Procedure.Handler, Types.NoInfer | Types.NoInfer, R2> + ): ProcedureList +} => + dual( + 3, + < + State, + Public extends Procedure.TaggedRequest.Any, + Private extends Procedure.TaggedRequest.Any, + R, + R2 + >( + self: ProcedureList, + tag: Req["_tag"], + handler: Procedure.Handler, Types.NoInfer | Types.NoInfer, R2> + ): ProcedureList => + addProcedure(self, Procedure.make()()(tag, handler)) + ) + +/** + * @since 1.0.0 + * @category combinators + */ +export const addPrivate = (): { + < + State, + Public extends Procedure.TaggedRequest.Any, + Private extends Procedure.TaggedRequest.Any, + R2 + >( + tag: Req["_tag"], + handler: Procedure.Handler, Types.NoInfer | Types.NoInfer, R2> + ): ( + self: ProcedureList + ) => ProcedureList + < + State, + Public extends Procedure.TaggedRequest.Any, + Private extends Procedure.TaggedRequest.Any, + R, + R2 + >( + self: ProcedureList, + tag: Req["_tag"], + handler: Procedure.Handler, Types.NoInfer | Types.NoInfer, R2> + ): ProcedureList +} => + dual( + 3, + < + State, + Public extends Procedure.TaggedRequest.Any, + Private extends Procedure.TaggedRequest.Any, + R, + R2 + >( + self: ProcedureList, + tag: Req["_tag"], + handler: Procedure.Handler, Types.NoInfer | Types.NoInfer, R2> + ): ProcedureList => + addProcedurePrivate(self, Procedure.make()()(tag, handler)) + ) + +/** + * @since 1.0.0 + * @category combinators + */ +export const withInitialState: { + ( + initialState: Types.NoInfer + ): ( + self: ProcedureList + ) => ProcedureList + ( + self: ProcedureList, + initialState: Types.NoInfer + ): ProcedureList +} = dual(2, ( + self: ProcedureList, + initialState: Types.NoInfer +): ProcedureList => makeProto({ ...self, initialState })) diff --git a/repos/effect/packages/experimental/src/Machine/SerializableProcedureList.ts b/repos/effect/packages/experimental/src/Machine/SerializableProcedureList.ts new file mode 100644 index 0000000..644d450 --- /dev/null +++ b/repos/effect/packages/experimental/src/Machine/SerializableProcedureList.ts @@ -0,0 +1,175 @@ +/** + * @since 1.0.0 + */ +import type * as Effect from "effect/Effect" +import { dual } from "effect/Function" +import type * as Schema from "effect/Schema" +import type * as Types from "effect/Types" +import * as Procedure from "./Procedure.js" +import * as ProcedureList from "./ProcedureList.js" + +/** + * @since 1.0.0 + * @category models + */ +export interface SerializableProcedureList< + State, + Public extends Schema.TaggedRequest.All, + Private extends Schema.TaggedRequest.All, + R +> extends Effect.Effect> { + readonly [ProcedureList.TypeId]: ProcedureList.TypeId + readonly initialState: State + readonly public: ReadonlyArray> + readonly private: ReadonlyArray> + readonly identifier: string +} + +/** + * @since 1.0.0 + * @category constructors + */ +export const make: ( + initialState: State, + options?: { + readonly identifier?: string + } +) => SerializableProcedureList = ProcedureList.make as any + +/** + * @since 1.0.0 + * @category combinators + */ +export const add: { + < + Req extends Schema.TaggedRequest.All, + I, + ReqR, + State, + Public extends Schema.TaggedRequest.All, + Private extends Schema.TaggedRequest.All, + R2 + >( + schema: Schema.Schema & { readonly _tag: Req["_tag"] }, + handler: Procedure.Handler, Types.NoInfer | Types.NoInfer, R2> + ): ( + self: SerializableProcedureList + ) => SerializableProcedureList< + State, + Req | Public, + Private, + R | R2 | Schema.SerializableWithResult.Context + > + < + State, + Public extends Schema.TaggedRequest.All, + Private extends Schema.TaggedRequest.All, + R, + Req extends Schema.TaggedRequest.All, + I, + ReqR, + R2 + >( + self: SerializableProcedureList, + schema: Schema.Schema & { readonly _tag: Req["_tag"] }, + handler: Procedure.Handler, Types.NoInfer | Types.NoInfer, R2> + ): SerializableProcedureList> +} = dual( + 3, + < + State, + Public extends Schema.TaggedRequest.All, + Private extends Schema.TaggedRequest.All, + R, + Req extends Schema.TaggedRequest.All, + I, + ReqR, + R2 + >( + self: SerializableProcedureList, + schema: Schema.Schema & { readonly _tag: Req["_tag"] }, + handler: Procedure.Handler, Types.NoInfer | Types.NoInfer, R2> + ): SerializableProcedureList< + State, + Req | Public, + Private, + R | R2 | Schema.SerializableWithResult.Context + > => ProcedureList.addProcedure(self, Procedure.makeSerializable()(schema, handler)) as any +) + +/** + * @since 1.0.0 + * @category combinators + */ +export const addPrivate: { + < + Req extends Schema.TaggedRequest.All, + I, + ReqR, + State, + Public extends Schema.TaggedRequest.All, + Private extends Schema.TaggedRequest.All, + R2 + >( + schema: Schema.Schema & { readonly _tag: Req["_tag"] }, + handler: Procedure.Handler, Types.NoInfer | Types.NoInfer, R2> + ): ( + self: SerializableProcedureList + ) => SerializableProcedureList< + State, + Public, + Private | Req, + R | R2 | Schema.SerializableWithResult.Context + > + < + State, + Public extends Schema.TaggedRequest.All, + Private extends Schema.TaggedRequest.All, + R, + Req extends Schema.TaggedRequest.All, + I, + ReqR, + R2 + >( + self: SerializableProcedureList, + schema: Schema.Schema & { readonly _tag: Req["_tag"] }, + handler: Procedure.Handler, Types.NoInfer | Types.NoInfer, R2> + ): SerializableProcedureList> +} = dual( + 3, + < + State, + Public extends Schema.TaggedRequest.All, + Private extends Schema.TaggedRequest.All, + R, + Req extends Schema.TaggedRequest.All, + I, + ReqR, + R2 + >( + self: SerializableProcedureList, + schema: Schema.Schema & { readonly _tag: Req["_tag"] }, + handler: Procedure.Handler, Types.NoInfer | Types.NoInfer, R2> + ): SerializableProcedureList< + State, + Public, + Private | Req, + R | R2 | Schema.SerializableWithResult.Context + > => ProcedureList.addProcedurePrivate(self, Procedure.makeSerializable()(schema, handler)) as any +) + +/** + * @since 1.0.0 + * @category combinators + */ +export const withInitialState: { + ( + initialState: Types.NoInfer + ): ( + self: SerializableProcedureList + ) => SerializableProcedureList + ( + self: SerializableProcedureList, + initialState: Types.NoInfer + ): SerializableProcedureList +} = ProcedureList.withInitialState as any diff --git a/repos/effect/packages/experimental/src/PersistedCache.ts b/repos/effect/packages/experimental/src/PersistedCache.ts new file mode 100644 index 0000000..e4fe777 --- /dev/null +++ b/repos/effect/packages/experimental/src/PersistedCache.ts @@ -0,0 +1,102 @@ +/** + * @since 1.0.0 + */ +import * as Cache from "effect/Cache" +import * as Data from "effect/Data" +import type * as Duration from "effect/Duration" +import * as Effect from "effect/Effect" +import * as Equal from "effect/Equal" +import { identity, pipe } from "effect/Function" +import * as Hash from "effect/Hash" +import * as Option from "effect/Option" +import type * as Schema from "effect/Schema" +import type * as Scope from "effect/Scope" +import * as Tracer from "effect/Tracer" +import * as Persistence from "./Persistence.js" + +class CacheRequest extends Data.Class<{ + readonly key: K + readonly span: Option.Option +}> { + [Equal.symbol](that: CacheRequest): boolean { + return Equal.equals(this.key, that.key) + } + [Hash.symbol]() { + return Hash.hash(this.key) + } +} + +/** + * @since 1.0.0 + * @category models + */ +export interface PersistedCache { + readonly get: ( + key: K + ) => Effect.Effect< + Schema.WithResult.Success, + Schema.WithResult.Failure | Persistence.PersistenceError + > + readonly invalidate: (key: K) => Effect.Effect +} + +/** + * @since 1.0.0 + * @category constructors + */ +export const make = (options: { + readonly storeId: string + readonly lookup: (key: K) => Effect.Effect, Schema.WithResult.Failure, R> + readonly timeToLive: (...args: Persistence.ResultPersistence.TimeToLiveArgs) => Duration.DurationInput + readonly inMemoryCapacity?: number | undefined + readonly inMemoryTTL?: Duration.DurationInput | undefined +}): Effect.Effect< + PersistedCache, + never, + Schema.SerializableWithResult.Context | R | Persistence.ResultPersistence | Scope.Scope +> => + Persistence.ResultPersistence.pipe( + Effect.flatMap((_) => + _.make({ + storeId: options.storeId, + timeToLive: options.timeToLive as any + }) + ), + Effect.bindTo("store"), + Effect.bind("inMemoryCache", ({ store }) => + Cache.make({ + lookup: (request: CacheRequest) => { + const effect: Effect.Effect< + Schema.WithResult.Success, + Schema.WithResult.Failure | Persistence.PersistenceError, + Schema.SerializableWithResult.Context | R + > = pipe( + store.get(request.key as any), + Effect.flatMap(Option.match({ + onNone: () => + options.lookup(request.key).pipe( + Effect.exit, + Effect.tap((exit) => store.set(request.key as any, exit)), + Effect.flatten + ), + onSome: identity + })) + ) as any + return request.span._tag === "Some" ? Effect.withParentSpan(effect, request.span.value) : effect + }, + capacity: options.inMemoryCapacity ?? 64, + timeToLive: options.inMemoryTTL ?? 10_000 + })), + Effect.map(({ inMemoryCache, store }) => + identity>({ + get: (key) => + Effect.serviceOption(Tracer.ParentSpan).pipe( + Effect.flatMap((span) => inMemoryCache.get(new CacheRequest({ key, span }))) + ), + invalidate: (key) => + store.remove(key as any).pipe( + Effect.zipRight(inMemoryCache.invalidate(new CacheRequest({ key, span: Option.none() }))) + ) + }) + ) + ) diff --git a/repos/effect/packages/experimental/src/PersistedQueue.ts b/repos/effect/packages/experimental/src/PersistedQueue.ts new file mode 100644 index 0000000..1f119ba --- /dev/null +++ b/repos/effect/packages/experimental/src/PersistedQueue.ts @@ -0,0 +1,278 @@ +/** + * @since 1.0.0 + */ +import * as Context from "effect/Context" +import * as Effect from "effect/Effect" +import * as Exit from "effect/Exit" +import * as Iterable from "effect/Iterable" +import * as Layer from "effect/Layer" +import type * as ParseResult from "effect/ParseResult" +import * as Schema from "effect/Schema" +import * as Scope from "effect/Scope" + +/** + * @since 1.0.0 + * @category Type IDs + */ +export const TypeId: TypeId = "~@effect/experimental/PersistedQueue" + +/** + * @since 1.0.0 + * @category Type IDs + */ +export type TypeId = "~@effect/experimental/PersistedQueue" + +/** + * @since 1.0.0 + * @category Models + */ +export interface PersistedQueue { + readonly [TypeId]: TypeId + + /** + * Adds an element to the queue. Returns the id of the enqueued element. + * + * If an element with the same id already exists in the queue, it will not be + * added again. + */ + readonly offer: (value: A, options?: { + readonly id: string | undefined + }) => Effect.Effect + + /** + * Takes an element from the queue. + * If the queue is empty, it will wait until an element is available. + * + * If the returned effect succeeds, the element is marked as processed, + * otherwise it will be retried according to the provided options. + * + * By default, max attempts is set to 10. + */ + readonly take: ( + f: (value: A, metadata: { + readonly id: string + readonly attempts: number + }) => Effect.Effect, + options?: { + readonly maxAttempts?: number | undefined + } + ) => Effect.Effect +} + +/** + * @since 1.0.0 + * @category Factory + */ +export class PersistedQueueFactory extends Context.Tag("@effect/experimental/PersistedQueue/PersistedQueueFactory")< + PersistedQueueFactory, + { + readonly make: (options: { + readonly name: string + readonly schema: Schema.Schema + }) => Effect.Effect> + } +>() {} + +/** + * @since 1.0.0 + * @category Accessors + */ +export const make = (options: { + readonly name: string + readonly schema: Schema.Schema +}): Effect.Effect, never, PersistedQueueFactory> => + Effect.flatMap( + PersistedQueueFactory, + (factory) => factory.make(options) + ) + +/** + * @since 1.0.0 + * @category Factory + */ +export const makeFactory = Effect.gen(function*() { + const store = yield* PersistedQueueStore + + return PersistedQueueFactory.of({ + make(options: { + readonly name: string + readonly schema: Schema.Schema + }) { + const encodeUnknown = Schema.encodeUnknown(options.schema) + const decodeUnknown = Schema.decodeUnknown(options.schema) + + return Effect.succeed>({ + [TypeId]: TypeId, + offer: (value, opts) => + Effect.flatMap( + encodeUnknown(value), + (element) => { + const id = opts?.id ?? crypto.randomUUID() + return Effect.as( + store.offer({ + name: options.name, + id, + element, + isCustomId: opts?.id !== undefined + }), + id + ) + } + ), + take: (f, opts) => + Effect.uninterruptibleMask(Effect.fnUntraced(function*(restore) { + const scope = yield* Scope.make() + const item = yield* store.take({ + name: options.name, + maxAttempts: opts?.maxAttempts ?? 10 + }).pipe( + Scope.extend(scope), + restore + ) + const exit = yield* decodeUnknown(item.element).pipe( + Effect.flatMap((value) => f(value, { id: item.id, attempts: item.attempts })), + restore, + Effect.exit + ) + yield* Scope.close(scope, exit) + return yield* exit + })) + }) + } + }) +}) + +/** + * @since 1.0.0 + * @category Factory + */ +export const layer: Layer.Layer< + PersistedQueueFactory, + never, + PersistedQueueStore +> = Layer.effect(PersistedQueueFactory, makeFactory) + +/** + * @since 1.0.0 + * @category Errors + */ +export const ErrorTypeId: ErrorTypeId = "~@effect/experimental/PersistedQueue/PersistedQueueError" + +/** + * @since 1.0.0 + * @category Errors + */ +export type ErrorTypeId = "~@effect/experimental/PersistedQueue/PersistedQueueError" + +/** + * @since 1.0.0 + * @category Errors + */ +export class PersistedQueueError extends Schema.TaggedError( + "@effect/experimental/PersistedQueue/PersistedQueueError" +)("PersistedQueueError", { + message: Schema.String, + cause: Schema.optional(Schema.Defect) +}) { + /** + * @since 1.0.0 + */ + readonly [ErrorTypeId]: ErrorTypeId = ErrorTypeId +} + +/** + * @since 1.0.0 + * @category Store + */ +export class PersistedQueueStore extends Context.Tag("@effect/experimental/PersistedQueue/PersistedQueueStore")< + PersistedQueueStore, + { + readonly offer: ( + options: { + readonly name: string + readonly id: string + readonly element: unknown + readonly isCustomId: boolean + } + ) => Effect.Effect + + readonly take: (options: { + readonly name: string + readonly maxAttempts: number + }) => Effect.Effect< + { + readonly id: string + readonly attempts: number + readonly element: unknown + }, + PersistedQueueError, + Scope.Scope + > + } +>() {} + +/** + * @since 1.0.0 + * @category Store + */ +export const layerStoreMemory: Layer.Layer< + PersistedQueueStore +> = Layer.sync(PersistedQueueStore, () => { + type Entry = { + readonly id: string + attempts: number + readonly element: unknown + } + const ids = new Set() + const queues = new Map + }>() + const getOrCreateQueue = (name: string) => { + let queue = queues.get(name) + if (!queue) { + queue = { + latch: Effect.unsafeMakeLatch(false), + items: new Set() + } + queues.set(name, queue) + } + return queue + } + + return PersistedQueueStore.of({ + offer: (options) => + Effect.sync(() => { + if (ids.has(options.id)) return + ids.add(options.id) + const queue = getOrCreateQueue(options.name) + queue.items.add({ id: options.id, attempts: 0, element: options.element }) + queue.latch.unsafeOpen() + }), + take: Effect.fnUntraced(function*(options) { + const queue = getOrCreateQueue(options.name) + while (true) { + yield* queue.latch.await + const item = Iterable.unsafeHead(queue.items) + queue.items.delete(item) + if (queue.items.size === 0) { + queue.latch.unsafeClose() + } + yield* Effect.addFinalizer((exit) => { + if (exit._tag === "Success") { + return Effect.void + } else if (!Exit.isInterrupted(exit)) { + item.attempts += 1 + } + if (item.attempts >= options.maxAttempts) { + return Effect.void + } + queue.items.add(item) + queue.latch.unsafeOpen() + return Effect.void + }) + return item + } + }) + }) +}) diff --git a/repos/effect/packages/experimental/src/PersistedQueue/Redis.ts b/repos/effect/packages/experimental/src/PersistedQueue/Redis.ts new file mode 100644 index 0000000..678dc0f --- /dev/null +++ b/repos/effect/packages/experimental/src/PersistedQueue/Redis.ts @@ -0,0 +1,410 @@ +/** + * @since 1.0.0 + */ +import * as Cause from "effect/Cause" +import * as Config from "effect/Config" +import * as Duration from "effect/Duration" +import * as Effect from "effect/Effect" +import * as Exit from "effect/Exit" +import * as Layer from "effect/Layer" +import * as Mailbox from "effect/Mailbox" +import * as MutableRef from "effect/MutableRef" +import * as Option from "effect/Option" +import * as RcMap from "effect/RcMap" +import * as Schedule from "effect/Schedule" +import type { RedisOptions } from "ioredis" +import { Redis } from "ioredis" +import * as PersistedQueue from "../PersistedQueue.js" + +interface RedisWithQueue extends Redis { + offer( + keyQueue: string, + keyIds: string, + id: string, + payload: string + ): Promise + resetQueue( + keyQueue: string, + keyPending: string, + prefix: string + ): Promise + requeue( + keyQueue: string, + keyPending: string, + keyLock: string, + id: string, + payload: string + ): Promise + take( + keyQueue: string, + keyPending: string, + prefix: string, + workerId: string, + batchSize: number, + pttl: number + ): Promise | null> + complete( + keyPending: string, + keyLock: string, + id: string + ): Promise + failed( + keyPending: string, + keyLock: string, + keyFailed: string, + id: string, + payload: string + ): Promise +} + +/** + * @since 1.0.0 + * @category Constructors + */ +export const make = Effect.fnUntraced(function*( + options: RedisOptions & { + readonly prefix?: string | undefined + readonly pollInterval?: Duration.DurationInput | undefined + readonly lockRefreshInterval?: Duration.DurationInput | undefined + readonly lockExpiration?: Duration.DurationInput | undefined + } +) { + const pollInterval = options.pollInterval ? Duration.decode(options.pollInterval) : Duration.seconds(1) + + const redis = yield* Effect.acquireRelease( + Effect.sync(() => new Redis(options) as RedisWithQueue), + (redis) => Effect.promise(() => redis.quit()) + ) + + redis.defineCommand("offer", { + lua: ` +local key_queue = KEYS[1] +local key_ids = KEYS[2] +local id = ARGV[1] +local payload = ARGV[2] + +local result = redis.call("SADD", key_ids, id) +if result == 1 then + redis.call("RPUSH", key_queue, payload) +end +`, + numberOfKeys: 2, + readOnly: false + }) + + redis.defineCommand("resetQueue", { + lua: ` +local key_queue = KEYS[1] +local key_pending = KEYS[2] +local prefix = ARGV[1] + +local entries = redis.call("HGETALL", key_pending) +for id, payload in pairs(entries) do + local lock_key = prefix .. id .. ":lock" + local exists = redis.call("EXISTS", lock_key) + if exists == 0 then + redis.call("RPUSH", key_queue, payload) + redis.call("HDEL", key_pending, id) + end +end +`, + numberOfKeys: 2, + readOnly: false + }) + + redis.defineCommand("requeue", { + lua: ` +local key_queue = KEYS[1] +local key_pending = KEYS[2] +local key_lock = KEYS[3] +local id = ARGV[1] +local payload = ARGV[2] + +redis.call("DEL", key_lock) +redis.call("HDEL", key_pending, id) +redis.call("RPUSH", key_queue, payload) +`, + numberOfKeys: 3, + readOnly: false + }) + + redis.defineCommand("complete", { + lua: ` +local key_pending = KEYS[1] +local key_lock = KEYS[2] +local id = ARGV[1] + +redis.call("DEL", key_lock) +redis.call("HDEL", key_pending, id) +`, + numberOfKeys: 2, + readOnly: false + }) + + redis.defineCommand("failed", { + lua: ` +local key_pending = KEYS[1] +local key_lock = KEYS[2] +local key_failed = KEYS[3] +local id = ARGV[1] +local payload = ARGV[2] + +redis.call("DEL", key_lock) +redis.call("HDEL", key_pending, id) +redis.call("RPUSH", key_failed, payload) +`, + numberOfKeys: 2, + readOnly: false + }) + + redis.defineCommand("take", { + lua: ` +local key_queue = KEYS[1] +local key_pending = KEYS[2] +local prefix = ARGV[1] +local worker_id = ARGV[2] +local batch_size = tonumber(ARGV[3]) +local pttl = ARGV[4] + +local payloads = redis.call("LPOP", key_queue, batch_size) +if not payloads then + return nil +end + +for i, payload in ipairs(payloads) do + local id = cjson.decode(payload).id + local key_lock = prefix .. id .. ":lock" + redis.call("SET", key_lock, worker_id, "PX", pttl) + redis.call("HSET", key_pending, id, payload) +end + +return payloads +`, + numberOfKeys: 2, + readOnly: false + }) + + const lockRefreshMillis = options.lockRefreshInterval ? Duration.toMillis(options.lockRefreshInterval) : 30_000 + const lockExpirationMillis = options.lockExpiration ? Duration.toMillis(options.lockExpiration) : 90_000 + const prefix = options.prefix ?? "effectq:" + const keyQueue = (name: string) => `${prefix}${name}` + const keyLock = (id: string) => `${prefix}${id}:lock` + const keyPending = (name: string) => `${prefix}${name}:pending` + const keyFailed = (name: string) => `${prefix}${name}:failed` + const workerId = crypto.randomUUID() + + type Element = { + readonly id: string + readonly element: unknown + attempts: number + lastFailure?: string + } + + const mailboxes = yield* RcMap.make({ + lookup: Effect.fnUntraced(function*(name: string) { + const queueKey = keyQueue(name) + const pendingKey = keyPending(name) + const mailbox = yield* Mailbox.make() + const takers = MutableRef.make(0) + const pollLatch = Effect.unsafeMakeLatch() + const takenLatch = Effect.unsafeMakeLatch() + + yield* Effect.addFinalizer(() => + Effect.flatMap( + mailbox.clear, + (elements) => + elements.length === 0 + ? Effect.void + : Effect.promise(() => + Promise.all(Array.from(elements, (element) => + redis.requeue( + queueKey, + pendingKey, + keyLock(element.id), + element.id, + JSON.stringify(element) + ))) + ) + ) + ) + + yield* Effect.sync(() => { + redis.resetQueue(queueKey, pendingKey, prefix) + }).pipe( + Effect.andThen(Effect.sleep(lockRefreshMillis)), + Effect.forever, + Effect.forkScoped, + Effect.interruptible + ) + + const poll = (size: number) => + Effect.promise(() => + redis.take( + queueKey, + pendingKey, + prefix, + workerId, + size, + lockExpirationMillis + ) + ) + + yield* Effect.gen(function*() { + while (true) { + yield* pollLatch.await + yield* Effect.yieldNow() + const results = takers.current === 0 ? null : yield* poll(takers.current) + if (results === null) { + yield* Effect.sleep(pollInterval) + continue + } + takenLatch.unsafeClose() + yield* mailbox.offerAll(results.map((json) => JSON.parse(json))) + yield* takenLatch.await + yield* Effect.yieldNow() + } + }).pipe( + Effect.sandbox, + Effect.retry(Schedule.spaced(500)), + Effect.forkScoped, + Effect.interruptible + ) + + return { mailbox, takers, pollLatch, takenLatch } as const + }), + idleTimeToLive: Duration.seconds(30) + }) + + const activeLockKeys = new Set() + + yield* Effect.gen(function*() { + while (true) { + yield* Effect.sleep(lockRefreshMillis) + activeLockKeys.forEach((key) => { + redis.pexpire(key, lockExpirationMillis) + }) + } + }).pipe( + Effect.forkScoped, + Effect.interruptible, + Effect.annotateLogs({ + package: "@effect/experimental", + module: "PersistedQueue/Redis", + fiber: "refreshLocks" + }) + ) + + return PersistedQueue.PersistedQueueStore.of({ + offer: ({ element, id, isCustomId, name }) => + Effect.tryPromise({ + try: (): Promise => + isCustomId + ? redis.offer( + `${prefix}${name}`, + `${prefix}${name}:ids`, + id, + JSON.stringify({ id, element, attempts: 0 }) + ) + : redis.lpush(`${prefix}${name}`, JSON.stringify({ id, element, attempts: 0 })), + catch: (cause) => + new PersistedQueue.PersistedQueueError({ + message: "Failed to offer element to persisted queue", + cause + }) + }), + take: (options) => + Effect.uninterruptibleMask((restore) => + RcMap.get(mailboxes, options.name).pipe( + Effect.flatMap(({ mailbox, pollLatch, takenLatch, takers }) => { + takers.current++ + if (takers.current === 1) { + pollLatch.unsafeOpen() + } + return Effect.tap(restore(mailbox.take as Effect.Effect), () => { + takers.current-- + if (takers.current === 0) { + pollLatch.unsafeClose() + takenLatch.unsafeOpen() + } else if (Option.getOrUndefined(mailbox.unsafeSize()) === 0) { + takenLatch.unsafeOpen() + } + }) + }), + Effect.scoped, + Effect.tap((element) => { + const lock = keyLock(element.id) + activeLockKeys.add(lock) + return Effect.addFinalizer(Exit.match({ + onFailure: (cause) => { + activeLockKeys.delete(lock) + const nextAttempts = element.attempts + 1 + if (nextAttempts >= options.maxAttempts) { + return Effect.promise(() => + redis.failed( + keyPending(options.name), + lock, + keyFailed(options.name), + element.id, + JSON.stringify({ + ...element, + lastFailure: Cause.pretty(cause, { renderErrorCause: true }), + attempts: nextAttempts + }) + ) + ) + } + return Effect.promise(() => + redis.requeue( + keyQueue(options.name), + keyPending(options.name), + lock, + element.id, + JSON.stringify( + Cause.isInterruptedOnly(cause) + ? element + : { + ...element, + lastFailure: Cause.pretty(cause, { renderErrorCause: true }), + attempts: nextAttempts + } + ) + ) + ) + }, + onSuccess: () => { + activeLockKeys.delete(lock) + return Effect.promise(() => + redis.complete( + keyPending(options.name), + lock, + element.id + ) + ) + } + })) + }) + ) + ) + }) +}) + +/** + * @since 1.0.0 + * @category Layers + */ +export const layerStore = ( + options: RedisOptions & { + readonly prefix?: string | undefined + readonly pollInterval?: Duration.DurationInput | undefined + readonly lockRefreshInterval?: Duration.DurationInput | undefined + readonly lockExpiration?: Duration.DurationInput | undefined + } +) => Layer.scoped(PersistedQueue.PersistedQueueStore, make(options)) + +/** + * @since 1.0.0 + * @category Layers + */ +export const layerStoreConfig = ( + options: Config.Config.Wrap +) => Layer.scoped(PersistedQueue.PersistedQueueStore, Effect.flatMap(Config.unwrap(options), make)) diff --git a/repos/effect/packages/experimental/src/Persistence.ts b/repos/effect/packages/experimental/src/Persistence.ts new file mode 100644 index 0000000..783956d --- /dev/null +++ b/repos/effect/packages/experimental/src/Persistence.ts @@ -0,0 +1,465 @@ +/** + * @since 1.0.0 + */ +import { TypeIdError } from "@effect/platform/Error" +import * as KeyValueStore from "@effect/platform/KeyValueStore" +import * as Arr from "effect/Array" +import type * as Clock from "effect/Clock" +import * as Context from "effect/Context" +import * as Duration from "effect/Duration" +import * as Effect from "effect/Effect" +import type * as Exit from "effect/Exit" +import { identity } from "effect/Function" +import * as Layer from "effect/Layer" +import * as Option from "effect/Option" +import * as ParseResult from "effect/ParseResult" +import * as PrimaryKey from "effect/PrimaryKey" +import * as Schema from "effect/Schema" +import type * as Scope from "effect/Scope" + +/** + * @since 1.0.0 + * @category type ids + */ +export const ErrorTypeId: unique symbol = Symbol.for("@effect/experimental/PersistenceError") + +/** + * @since 1.0.0 + * @category type ids + */ +export type ErrorTypeId = typeof ErrorTypeId + +/** + * @since 1.0.0 + * @category errors + */ +export type PersistenceError = PersistenceParseError | PersistenceBackingError + +/** + * @since 1.0.0 + * @category errors + */ +export class PersistenceParseError extends TypeIdError(ErrorTypeId, "PersistenceError")<{ + readonly reason: "ParseError" + readonly method: string + readonly error: ParseResult.ParseError["issue"] +}> { + /** + * @since 1.0.0 + */ + static make(method: string, error: ParseResult.ParseError["issue"]) { + return new PersistenceParseError({ reason: "ParseError", method, error }) + } + + get message() { + return ParseResult.TreeFormatter.formatIssueSync(this.error) + } +} + +/** + * @since 1.0.0 + * @category errors + */ +export class PersistenceBackingError extends TypeIdError(ErrorTypeId, "PersistenceError")<{ + readonly reason: "BackingError" + readonly method: string + readonly cause: unknown +}> { + /** + * @since 1.0.0 + */ + static make(method: string, cause: unknown) { + return new PersistenceBackingError({ reason: "BackingError", method, cause }) + } + + get message() { + return this.reason + } +} + +/** + * @since 1.0.0 + * @category type ids + */ +export const BackingPersistenceTypeId: unique symbol = Symbol.for("@effect/experimental/BackingPersistence") + +/** + * @since 1.0.0 + * @category type ids + */ +export type BackingPersistenceTypeId = typeof BackingPersistenceTypeId + +/** + * @since 1.0.0 + * @category models + */ +export interface BackingPersistence { + readonly [BackingPersistenceTypeId]: BackingPersistenceTypeId + readonly make: (storeId: string) => Effect.Effect +} + +/** + * @since 1.0.0 + * @category models + */ +export interface BackingPersistenceStore { + readonly get: (key: string) => Effect.Effect, PersistenceError> + readonly getMany: (key: Array) => Effect.Effect>, PersistenceError> + readonly set: ( + key: string, + value: unknown, + ttl: Option.Option + ) => Effect.Effect + readonly setMany: ( + entries: ReadonlyArray]> + ) => Effect.Effect + readonly remove: (key: string) => Effect.Effect + readonly clear: Effect.Effect +} + +/** + * @since 1.0.0 + * @category tags + */ +export const BackingPersistence: Context.Tag = Context.GenericTag< + BackingPersistence +>( + "@effect/experimental/BackingPersistence" +) + +/** + * @since 1.0.0 + * @category type ids + */ +export const ResultPersistenceTypeId: unique symbol = Symbol.for("@effect/experimental/ResultPersistence") + +/** + * @since 1.0.0 + * @category type ids + */ +export type ResultPersistenceTypeId = typeof ResultPersistenceTypeId + +/** + * @since 1.0.0 + * @category models + */ +export interface ResultPersistence { + readonly [ResultPersistenceTypeId]: ResultPersistenceTypeId + readonly make: (options: { + readonly storeId: string + readonly timeToLive?: (key: ResultPersistence.KeyAny, exit: Exit.Exit) => Duration.DurationInput + }) => Effect.Effect +} + +/** + * @since 1.0.0 + * @category models + */ +export interface ResultPersistenceStore { + readonly get: ( + key: ResultPersistence.Key + ) => Effect.Effect>, PersistenceError, R> + readonly getMany: ( + key: ReadonlyArray> + ) => Effect.Effect>>, PersistenceError, R> + readonly set: ( + key: ResultPersistence.Key, + value: Exit.Exit + ) => Effect.Effect + readonly setMany: ( + entries: Iterable, Exit.Exit]> + ) => Effect.Effect + readonly remove: ( + key: ResultPersistence.Key + ) => Effect.Effect + readonly clear: Effect.Effect +} + +/** + * @since 1.0.0 + * @category models + */ +export interface Persistable extends + Schema.WithResult< + A["Type"], + A["Encoded"], + E["Type"], + E["Encoded"], + A["Context"] | E["Context"] + >, + PrimaryKey.PrimaryKey +{} + +/** + * @since 1.0.0 + * @category models + */ +export declare namespace ResultPersistence { + /** + * @since 1.0.0 + * @category models + */ + export interface Key extends Schema.WithResult, PrimaryKey.PrimaryKey {} + /** + * @since 1.0.0 + * @category models + */ + export type KeyAny = Persistable + + /** + * @since 1.0.0 + * @category models + */ + export type TimeToLiveArgs = A extends infer K + ? K extends Persistable ? [request: K, exit: Exit.Exit<_A["Type"], _E["Type"]>] + : never + : never +} + +/** + * @since 1.0.0 + * @category tags + */ +export const ResultPersistence: Context.Tag = Context.GenericTag< + ResultPersistence +>( + "@effect/experimental/ResultPersistence" +) + +/** + * @since 1.0.0 + * @category layers + */ +export const layerResult = Layer.effect( + ResultPersistence, + Effect.gen(function*() { + const backing = yield* BackingPersistence + return ResultPersistence.of({ + [ResultPersistenceTypeId]: ResultPersistenceTypeId, + make: (options) => + Effect.gen(function*() { + const storage = yield* backing.make(options.storeId) + const timeToLive = options.timeToLive ?? (() => Duration.infinity) + const parse = ( + method: string, + key: ResultPersistence.Key, + value: unknown + ) => + Effect.mapError( + Schema.deserializeExit(key, value), + (_) => PersistenceParseError.make(method, _.issue) + ) + const encode = ( + method: string, + key: ResultPersistence.Key, + value: Exit.Exit + ) => + Effect.mapError( + Schema.serializeExit(key, value), + (_) => PersistenceParseError.make(method, _.issue) + ) + const makeKey = ( + key: ResultPersistence.Key + ) => key[PrimaryKey.symbol]() + + return identity({ + get: (key) => + Effect.flatMap( + storage.get(makeKey(key)), + Option.match({ + onNone: () => Effect.succeedNone, + onSome: (_) => Effect.asSome(parse("get", key, _)) + }) + ), + getMany: (keys) => + Effect.flatMap( + storage.getMany(keys.map(makeKey)), + Effect.forEach((result, i) => { + const key = keys[i] + return Option.match(result, { + onNone: () => Effect.succeedNone, + onSome: (_) => + parse("getMany", key, _).pipe( + Effect.tapError((_) => storage.remove(makeKey(keys[i]))), + Effect.option + ) + }) + }) + ), + set: (key, value) => { + const ttl = Duration.decode(timeToLive(key, value)) + if (Duration.isZero(ttl)) { + return Effect.void + } + return encode("set", key, value).pipe( + Effect.flatMap((encoded) => + storage.set(makeKey(key), encoded, Duration.isFinite(ttl) ? Option.some(ttl) : Option.none()) + ) + ) + }, + setMany: Effect.fnUntraced(function*(entries) { + const encodedEntries = Arr.empty]>() + for (const [key, value] of entries) { + const ttl = Duration.decode(timeToLive(key, value)) + if (Duration.isZero(ttl)) continue + const encoded = yield* encode("setMany", key, value) + encodedEntries.push([makeKey(key), encoded, Duration.isFinite(ttl) ? Option.some(ttl) : Option.none()]) + } + if (encodedEntries.length === 0) return + return yield* storage.setMany(encodedEntries).pipe( + Effect.catchAll((error) => Effect.fail(PersistenceBackingError.make("setMany", error))) + ) + }), + remove: (key) => storage.remove(makeKey(key)), + clear: storage.clear + }) + }) + }) + }) +) + +/** + * @since 1.0.0 + * @category layers + */ +export const layerMemory: Layer.Layer = Layer.sync( + BackingPersistence, + () => { + const stores = new Map>() + const getStore = (storeId: string) => { + let store = stores.get(storeId) + if (store === undefined) { + store = new Map() + stores.set(storeId, store) + } + return store + } + return BackingPersistence.of({ + [BackingPersistenceTypeId]: BackingPersistenceTypeId, + make: (storeId) => + Effect.map(Effect.clock, (clock) => { + const map = getStore(storeId) + const unsafeGet = (key: string): Option.Option => { + const value = map.get(key) + if (value === undefined) { + return Option.none() + } else if (value[1] !== null && value[1] <= clock.unsafeCurrentTimeMillis()) { + map.delete(key) + return Option.none() + } + return Option.some(value[0]) + } + return identity({ + get: (key) => Effect.sync(() => unsafeGet(key)), + getMany: (keys) => Effect.sync(() => keys.map(unsafeGet)), + set: (key, value, ttl) => Effect.sync(() => map.set(key, [value, unsafeTtlToExpires(clock, ttl)])), + setMany: (entries) => + Effect.sync(() => { + for (const [key, value, ttl] of entries) { + map.set(key, [value, unsafeTtlToExpires(clock, ttl)]) + } + }), + remove: (key) => Effect.sync(() => map.delete(key)), + clear: Effect.sync(() => map.clear()) + }) + }) + }) + } +) + +/** + * @since 1.0.0 + * @category layers + */ +export const layerKeyValueStore: Layer.Layer = Layer.effect( + BackingPersistence, + Effect.gen(function*() { + const backing = yield* KeyValueStore.KeyValueStore + return BackingPersistence.of({ + [BackingPersistenceTypeId]: BackingPersistenceTypeId, + make: (storeId) => + Effect.map(Effect.clock, (clock) => { + const store = KeyValueStore.prefix(backing, storeId) + const get = (method: string, key: string) => + Effect.flatMap( + Effect.mapError( + store.get(key), + (error) => PersistenceBackingError.make(method, error) + ), + Option.match({ + onNone: () => Effect.succeedNone, + onSome: (s) => + Effect.flatMap( + Effect.try({ + try: () => JSON.parse(s), + catch: (error) => PersistenceBackingError.make(method, error) + }), + (_) => { + if (!Array.isArray(_)) return Effect.succeedNone + const [value, expires] = _ as [unknown, number | null] + if (expires !== null && expires <= clock.unsafeCurrentTimeMillis()) { + return Effect.as(Effect.ignore(store.remove(key)), Option.none()) + } + return Effect.succeed(Option.some(value)) + } + ) + }) + ) + return identity({ + get: (key) => get("get", key), + getMany: (keys) => Effect.forEach(keys, (key) => get("getMany", key), { concurrency: "unbounded" }), + set: (key, value, ttl) => + Effect.flatMap( + Effect.try({ + try: () => JSON.stringify([value, unsafeTtlToExpires(clock, ttl)]), + catch: (error) => PersistenceBackingError.make("set", error) + }), + (u) => + Effect.mapError( + store.set(key, u), + (error) => PersistenceBackingError.make("set", error) + ) + ), + setMany: (entries) => + Effect.forEach(entries, ([key, value, ttl]) => { + const expires = unsafeTtlToExpires(clock, ttl) + if (expires === null) return Effect.void + const encoded = JSON.stringify([value, expires]) + return store.set(key, encoded) + }, { concurrency: "unbounded", discard: true }).pipe( + Effect.mapError((error) => PersistenceBackingError.make("setMany", error)) + ), + remove: (key) => + Effect.mapError( + store.remove(key), + (error) => PersistenceBackingError.make("remove", error) + ), + clear: Effect.mapError(store.clear, (error) => PersistenceBackingError.make("clear", error)) + }) + }) + }) + }) +) + +/** + * @since 1.0.0 + * @category layers + */ +export const layerResultMemory: Layer.Layer = layerResult.pipe( + Layer.provide(layerMemory) +) + +/** + * @since 1.0.0 + * @category layers + */ +export const layerResultKeyValueStore: Layer.Layer = layerResult + .pipe( + Layer.provide(layerKeyValueStore) + ) + +/** + * @since 1.0.0 + */ +export const unsafeTtlToExpires = (clock: Clock.Clock, ttl: Option.Option): number | null => + ttl._tag === "None" ? null : clock.unsafeCurrentTimeMillis() + Duration.toMillis(ttl.value) diff --git a/repos/effect/packages/experimental/src/Persistence/Lmdb.ts b/repos/effect/packages/experimental/src/Persistence/Lmdb.ts new file mode 100644 index 0000000..8125bc0 --- /dev/null +++ b/repos/effect/packages/experimental/src/Persistence/Lmdb.ts @@ -0,0 +1,104 @@ +/** + * @since 1.0.0 + */ +import * as Arr from "effect/Array" +import * as Effect from "effect/Effect" +import { identity } from "effect/Function" +import * as Layer from "effect/Layer" +import * as Option from "effect/Option" +import * as Lmdb from "lmdb" +import * as Persistence from "../Persistence.js" + +/** + * @since 1.0.0 + * @category constructors + */ +export const make = (options: Lmdb.RootDatabaseOptionsWithPath) => + Effect.gen(function*() { + const lmdb = yield* Effect.acquireRelease( + Effect.sync(() => Lmdb.open(options)), + (lmdb) => Effect.promise(() => lmdb.close()) + ) + + return Persistence.BackingPersistence.of({ + [Persistence.BackingPersistenceTypeId]: Persistence.BackingPersistenceTypeId, + make: (storeId) => + Effect.gen(function*() { + const clock = yield* Effect.clock + const store = yield* Effect.acquireRelease( + Effect.sync(() => lmdb.openDB({ name: storeId })), + (store) => Effect.promise(() => store.close()) + ) + const valueToOption = (key: string, _: any) => { + if (!Arr.isArray(_)) return Option.none() + const [value, expires] = _ as [unknown, number | null] + if (expires !== null && expires <= clock.unsafeCurrentTimeMillis()) { + store.remove(key) + return Option.none() + } + return Option.some(value) + } + return identity({ + get: (key) => + Effect.try({ + try: () => valueToOption(key, store.get(key)), + catch: (error) => Persistence.PersistenceBackingError.make("get", error) + }), + getMany: (keys) => + Effect.map( + Effect.tryPromise({ + try: () => store.getMany(keys), + catch: (error) => Persistence.PersistenceBackingError.make("getMany", error) + }), + Arr.map((value, i) => valueToOption(keys[i], value)) + ), + set: (key, value, ttl) => + Effect.tryPromise({ + try: () => store.put(key, [value, Persistence.unsafeTtlToExpires(clock, ttl)]), + catch: (error) => Persistence.PersistenceBackingError.make("set", error) + }), + setMany: (entries) => + Effect.tryPromise({ + try: () => + Promise.all(entries.map(([key, value, ttl]) => + store.put(key, [value, Persistence.unsafeTtlToExpires(clock, ttl)]) + )), + catch: (error) => + Persistence.PersistenceBackingError.make("setMany", error) + }), + remove: (key) => + Effect.tryPromise({ + try: () => store.remove(key), + catch: (error) => Persistence.PersistenceBackingError.make("remove", error) + }), + clear: Effect.tryPromise({ + try: () => store.clearAsync(), + catch: (error) => Persistence.PersistenceBackingError.make("clear", error) + }) + }) + }) + }) + }) + +/** + * @since 1.0.0 + * @category layers + */ +export const layer = ( + options: Lmdb.RootDatabaseOptionsWithPath +): Layer.Layer => + Layer.scoped( + Persistence.BackingPersistence, + make(options) + ) + +/** + * @since 1.0.0 + * @category layers + */ +export const layerResult = ( + options: Lmdb.RootDatabaseOptionsWithPath +): Layer.Layer => + Persistence.layerResult.pipe( + Layer.provide(layer(options)) + ) diff --git a/repos/effect/packages/experimental/src/Persistence/Redis.ts b/repos/effect/packages/experimental/src/Persistence/Redis.ts new file mode 100644 index 0000000..5beab53 --- /dev/null +++ b/repos/effect/packages/experimental/src/Persistence/Redis.ts @@ -0,0 +1,149 @@ +/** + * @since 1.0.0 + */ +import * as Arr from "effect/Array" +import * as Config from "effect/Config" +import type { ConfigError } from "effect/ConfigError" +import * as Duration from "effect/Duration" +import * as Effect from "effect/Effect" +import { identity } from "effect/Function" +import * as Layer from "effect/Layer" +import * as Option from "effect/Option" +import type { RedisOptions } from "ioredis" +import { Redis } from "ioredis" +import * as Persistence from "../Persistence.js" + +/** + * @since 1.0.0 + * @category constructors + */ +export const make = Effect.fnUntraced(function*(options: RedisOptions) { + const redis = yield* Effect.acquireRelease( + Effect.sync(() => new Redis(options)), + (redis) => Effect.promise(() => redis.quit()) + ) + return Persistence.BackingPersistence.of({ + [Persistence.BackingPersistenceTypeId]: Persistence.BackingPersistenceTypeId, + make: (prefix) => + Effect.sync(() => { + const prefixed = (key: string) => `${prefix}:${key}` + const parse = (method: string) => (str: string | null) => { + if (str === null) { + return Effect.succeedNone + } + return Effect.try({ + try: () => Option.some(JSON.parse(str)), + catch: (error) => Persistence.PersistenceBackingError.make(method, error) + }) + } + return identity({ + get: (key) => + Effect.flatMap( + Effect.tryPromise({ + try: () => redis.get(prefixed(key)), + catch: (error) => Persistence.PersistenceBackingError.make("get", error) + }), + parse("get") + ), + getMany: (keys) => + Effect.flatMap( + Effect.tryPromise({ + try: () => redis.mget(keys.map(prefixed)), + catch: (error) => Persistence.PersistenceBackingError.make("getMany", error) + }), + Effect.forEach(parse("getMany")) + ), + set: (key, value, ttl) => + Effect.tryMapPromise( + Effect.try({ + try: () => JSON.stringify(value), + catch: (error) => Persistence.PersistenceBackingError.make("set", error) + }), + { + try: (value) => + ttl._tag === "None" + ? redis.set(prefixed(key), value) + : redis.set(prefixed(key), value, "PX", Duration.toMillis(ttl.value)), + catch: (error) => Persistence.PersistenceBackingError.make("set", error) + } + ), + setMany: (entries) => + Effect.suspend(() => { + const sets = new Map() + const expires = Arr.empty<[string, number]>() + for (const [key, value, ttl] of entries) { + const pkey = prefixed(key) + sets.set(pkey, JSON.stringify(value)) + if (Option.isSome(ttl)) { + expires.push([pkey, Duration.toMillis(ttl.value)]) + } + } + const multi = redis.multi() + multi.mset(sets) + for (const [key, ms] of expires) { + multi.pexpire(key, ms) + } + return Effect.tryPromise({ + try: () => multi.exec(), + catch: (error) => Persistence.PersistenceBackingError.make("setMany", error) + }) + }), + remove: (key) => + Effect.tryPromise({ + try: () => redis.del(prefixed(key)), + catch: (error) => Persistence.PersistenceBackingError.make("remove", error) + }), + clear: Effect.tryPromise({ + try: () => redis.keys(`${prefix}:*`).then((keys) => redis.del(keys)), + catch: (error) => Persistence.PersistenceBackingError.make("clear", error) + }) + }) + }) + }) +}) + +/** + * @since 1.0.0 + * @category layers + */ +export const layer = ( + options: RedisOptions +): Layer.Layer => + Layer.scoped( + Persistence.BackingPersistence, + make(options) + ) + +/** + * @since 1.0.0 + * @category layers + */ +export const layerConfig = ( + options: Config.Config.Wrap +): Layer.Layer => + Layer.scoped( + Persistence.BackingPersistence, + Effect.flatMap(Config.unwrap(options), make) + ) + +/** + * @since 1.0.0 + * @category layers + */ +export const layerResult = ( + options: RedisOptions +): Layer.Layer => + Persistence.layerResult.pipe( + Layer.provide(layer(options)) + ) + +/** + * @since 1.0.0 + * @category layers + */ +export const layerResultConfig = ( + options: Config.Config.Wrap +): Layer.Layer => + Persistence.layerResult.pipe( + Layer.provide(layerConfig(options)) + ) diff --git a/repos/effect/packages/experimental/src/RateLimiter.ts b/repos/effect/packages/experimental/src/RateLimiter.ts new file mode 100644 index 0000000..e255947 --- /dev/null +++ b/repos/effect/packages/experimental/src/RateLimiter.ts @@ -0,0 +1,486 @@ +/** + * @since 1.0.0 + */ +import * as Context from "effect/Context" +import * as Duration from "effect/Duration" +import * as Effect from "effect/Effect" +import { identity } from "effect/Function" +import * as Layer from "effect/Layer" +import * as Schema from "effect/Schema" + +/** + * @since 1.0.0 + * @category Type IDs + */ +export const TypeId: TypeId = "~@effect/experimental/RateLimiter" + +/** + * @since 1.0.0 + * @category Type IDs + */ +export type TypeId = "~@effect/experimental/RateLimiter" + +/** + * @since 1.0.0 + * @category Models + */ +export interface RateLimiter { + readonly [TypeId]: TypeId + + readonly consume: (options: { + readonly algorithm?: "fixed-window" | "token-bucket" | undefined + readonly onExceeded?: "delay" | "fail" | undefined + readonly window: Duration.DurationInput + readonly limit: number + readonly key: string + readonly tokens?: number | undefined + }) => Effect.Effect +} + +/** + * @since 1.0.0 + * @category Tags + */ +export const RateLimiter: Context.Tag = Context.GenericTag(TypeId) + +/** + * @since 1.0.0 + * @category Constructors + */ +export const make: Effect.Effect< + RateLimiter, + never, + RateLimiterStore +> = Effect.gen(function*() { + const store = yield* RateLimiterStore + + return identity({ + [TypeId]: TypeId, + consume(options) { + const tokens = options.tokens ?? 1 + const onExceeded = options.onExceeded ?? "fail" + const algorithm = options.algorithm ?? "fixed-window" + const window = Duration.decode(options.window) + const windowMillis = Duration.toMillis(window) + const refillRate = Duration.unsafeDivide(window, options.limit) + const refillRateMillis = Duration.toMillis(refillRate) + + if (tokens > options.limit) { + return onExceeded === "fail" + ? Effect.fail( + new RateLimitExceeded({ + key: options.key, + retryAfter: window, + limit: options.limit, + remaining: 0 + }) + ) + : Effect.succeed({ + delay: window, + limit: options.limit, + remaining: 0, + resetAfter: window + }) + } + + if (algorithm === "fixed-window") { + return Effect.flatMap( + store.fixedWindow({ + key: options.key, + tokens, + refillRate, + limit: onExceeded === "fail" ? options.limit : undefined + }), + ([count, ttl]) => { + if (onExceeded === "fail") { + const remaining = options.limit - count + if (remaining < 0) { + return Effect.fail( + new RateLimitExceeded({ + key: options.key, + retryAfter: Duration.millis(ttl), + limit: options.limit, + remaining: 0 + }) + ) + } + return Effect.succeed({ + delay: Duration.zero, + limit: options.limit, + remaining, + resetAfter: Duration.millis(ttl) + }) + } + const ttlTotal = count * refillRateMillis + const elapsed = ttlTotal - ttl + const windowNumber = Math.floor((count - 1) / options.limit) + const remaining = (windowNumber * windowMillis) - elapsed + const delay = remaining <= 0 ? Duration.zero : Duration.millis(remaining) + return Effect.succeed({ + delay, + limit: options.limit, + remaining: options.limit - count, + resetAfter: Duration.times(window, Math.ceil(ttl / windowMillis)) + }) + } + ) + } + + return Effect.flatMap( + store.tokenBucket({ + key: options.key, + tokens, + limit: options.limit, + refillRate, + allowOverflow: onExceeded === "delay" + }), + (remaining) => { + if (onExceeded === "fail") { + if (remaining < 0) { + return Effect.fail( + new RateLimitExceeded({ + key: options.key, + retryAfter: Duration.times(refillRate, -remaining), + limit: options.limit, + remaining: 0 + }) + ) + } + return Effect.succeed({ + delay: Duration.zero, + limit: options.limit, + remaining, + resetAfter: Duration.times(refillRate, options.limit - remaining) + }) + } + if (remaining >= 0) { + return Effect.succeed({ + delay: Duration.zero, + limit: options.limit, + remaining, + resetAfter: Duration.times(refillRate, options.limit - remaining) + }) + } + return Effect.succeed({ + delay: Duration.times(refillRate, -remaining), + limit: options.limit, + remaining, + resetAfter: Duration.times(refillRate, options.limit - remaining) + }) + } + ) + } + }) +}) + +/** + * @since 1.0.0 + * @category Layers + */ +export const layer: Layer.Layer< + RateLimiter, + never, + RateLimiterStore +> = Layer.effect(RateLimiter, make) + +/** + * Access a function that applies rate limiting to an effect. + * + * ```ts + * import { RateLimiter } from "@effect/experimental" + * import { Effect } from "effect" + * + * Effect.gen(function*() { + * // Access the `withLimiter` function from the RateLimiter module + * const withLimiter = yield* RateLimiter.makeWithRateLimiter + * + * // Apply a rate limiter to an effect + * yield* Effect.log("Making a request with rate limiting").pipe( + * withLimiter({ + * key: "some-key", + * limit: 10, + * onExceeded: "delay", + * window: "5 seconds", + * algorithm: "fixed-window" + * }) + * ) + * }) + * ``` + * + * @since 1.0.0 + * @category Accessors + */ +export const makeWithRateLimiter: Effect.Effect< + ((options: { + readonly algorithm?: "fixed-window" | "token-bucket" | undefined + readonly onExceeded?: "delay" | "fail" | undefined + readonly window: Duration.DurationInput + readonly limit: number + readonly key: string + readonly tokens?: number | undefined + }) => (effect: Effect.Effect) => Effect.Effect), + never, + RateLimiter +> = Effect.map( + RateLimiter, + (limiter) => (options) => (effect) => + Effect.flatMap(limiter.consume(options), ({ delay }) => { + if (Duration.isZero(delay)) return effect + return Effect.delay(effect, delay) + }) +) + +/** + * Access a function that sleeps when the rate limit is exceeded. + * + * ```ts + * import { RateLimiter } from "@effect/experimental" + * import { Effect } from "effect" + * + * export default Effect.gen(function*() { + * // Access the `sleep` function from the RateLimiter module + * const sleep = yield* RateLimiter.makeSleep + * + * // Use the `sleep` function with specific rate limiting parameters. + * // This will only sleep if the rate limit has been exceeded. + * yield* sleep({ + * key: "some-key", + * limit: 10, + * window: "5 seconds", + * algorithm: "fixed-window" + * }) + * }) + * ``` + * + * @since 1.0.0 + * @category Accessors + */ +export const makeSleep: Effect.Effect< + ((options: { + readonly algorithm?: "fixed-window" | "token-bucket" | undefined + readonly window: Duration.DurationInput + readonly limit: number + readonly key: string + readonly tokens?: number | undefined + }) => Effect.Effect), + never, + RateLimiter +> = Effect.map( + RateLimiter, + (limiter) => (options) => + Effect.flatMap( + limiter.consume({ + ...options, + onExceeded: "delay" + }) as Effect.Effect, + (result) => { + if (Duration.isZero(result.delay)) return Effect.succeed(result) + return Effect.as(Effect.sleep(result.delay), result) + } + ) +) + +/** + * @since 1.0.0 + * @category Errors + */ +export const ErrorTypeId: ErrorTypeId = "~@effect/experimental/RateLimiter/RateLimiterError" + +/** + * @since 1.0.0 + * @category Errors + */ +export type ErrorTypeId = "~@effect/experimental/RateLimiter/RateLimiterError" + +/** + * @since 1.0.0 + * @category Errors + */ +export class RateLimitExceeded extends Schema.TaggedError( + "@effect/experimental/RateLimiter/RateLimitExceeded" +)("RateLimiterError", { + retryAfter: Schema.DurationFromMillis, + key: Schema.String, + limit: Schema.Number, + remaining: Schema.Number +}) { + /** + * @since 1.0.0 + */ + readonly [ErrorTypeId]: ErrorTypeId = ErrorTypeId + + /** + * @since 1.0.0 + */ + readonly reason = "Exceeded" + + /** + * @since 1.0.0 + */ + get message(): string { + return `Rate limit exceeded` + } +} + +/** + * @since 1.0.0 + * @category Errors + */ +export class RateLimitStoreError extends Schema.TaggedError( + "@effect/experimental/RateLimiter/RateLimitStoreError" +)("RateLimiterError", { + message: Schema.String, + cause: Schema.optional(Schema.Defect) +}) { + /** + * @since 1.0.0 + */ + readonly [ErrorTypeId]: ErrorTypeId = ErrorTypeId + + /** + * @since 1.0.0 + */ + readonly reason = "StoreError" +} + +/** + * @since 1.0.0 + * @category Errors + */ +export const RateLimiterError = Schema.Union(RateLimitExceeded, RateLimitStoreError) + +/** + * @since 1.0.0 + * @category Errors + */ +export type RateLimiterError = RateLimitExceeded | RateLimitStoreError + +/** + * @since 1.0.0 + * @category Models + */ +export interface ConsumeResult { + /** + * The amount of delay to wait before making the next request, when the rate + * limiter is using the "delay" `onExceeded` strategy. + * + * It will be Duration.zero if the request is allowed immediately. + */ + readonly delay: Duration.Duration + + /** + * The maximum number of requests allowed in the current window. + */ + readonly limit: number + + /** + * The number of remaining requests in the current window. + */ + readonly remaining: number + + /** + * The time until the rate limit fully resets. + */ + readonly resetAfter: Duration.Duration +} + +/** + * @since 1.0.0 + * @category RateLimiterStore + */ +export class RateLimiterStore extends Context.Tag("@effect/experimental/RateLimiter/RateLimiterStore")< + RateLimiterStore, + { + /** + * Returns the token count *after* taking the specified `tokens` and time to + * live for the `key`. + * + * If `limit` is provided, the number of taken tokens will be capped at the + * limit. + * + * In the case the limit is exceeded, the returned count will be greater + * than the limit, but the TTL will not be updated. + */ + readonly fixedWindow: (options: { + readonly key: string + readonly tokens: number + readonly refillRate: Duration.Duration + readonly limit: number | undefined + }) => Effect.Effect + + /** + * Returns the current remaining tokens for the `key` after consuming the + * specified amount of tokens. + * + * If `allowOverflow` is true, the number of tokens can drop below zero. + * + * In the case of no overflow, the returned token count will only be + * negative if the requested tokens exceed the available tokens, but the + * real token count will not be persisted below zero. + */ + readonly tokenBucket: (options: { + readonly key: string + readonly tokens: number + readonly limit: number + readonly refillRate: Duration.Duration + readonly allowOverflow: boolean + }) => Effect.Effect + } +>() {} + +/** + * @since 1.0.0 + * @category RateLimiterStore + */ +export const layerStoreMemory: Layer.Layer< + RateLimiterStore +> = Layer.sync(RateLimiterStore, () => { + const fixedCounters = new Map() + const tokenBuckets = new Map() + + return RateLimiterStore.of({ + fixedWindow: (options) => + Effect.clockWith((clock) => + Effect.sync(() => { + const refillRateMillis = Duration.toMillis(options.refillRate) + const now = clock.unsafeCurrentTimeMillis() + let counter = fixedCounters.get(options.key) + if (!counter || counter.expiresAt <= now) { + counter = { count: 0, expiresAt: now } + fixedCounters.set(options.key, counter) + } + if (options.limit && counter.count + options.tokens > options.limit) { + return [counter.count + options.tokens, counter.expiresAt - now] as const + } + counter.count += options.tokens + counter.expiresAt += refillRateMillis * options.tokens + return [counter.count, counter.expiresAt - now] as const + }) + ), + tokenBucket: (options) => + Effect.clockWith((clock) => + Effect.sync(() => { + const refillRateMillis = Duration.toMillis(options.refillRate) + const now = clock.unsafeCurrentTimeMillis() + let bucket = tokenBuckets.get(options.key) + if (!bucket) { + bucket = { tokens: options.limit, lastRefill: now } + tokenBuckets.set(options.key, bucket) + } else { + const elapsed = now - bucket.lastRefill + const tokensToAdd = Math.floor(elapsed / refillRateMillis) + if (tokensToAdd > 0) { + bucket.tokens = Math.min(options.limit, bucket.tokens + tokensToAdd) + bucket.lastRefill += tokensToAdd * refillRateMillis + } + } + + const newTokenCount = bucket.tokens - options.tokens + if (options.allowOverflow || newTokenCount >= 0) { + bucket.tokens = newTokenCount + } + return newTokenCount + }) + ) + }) +}) diff --git a/repos/effect/packages/experimental/src/RateLimiter/Redis.ts b/repos/effect/packages/experimental/src/RateLimiter/Redis.ts new file mode 100644 index 0000000..47cfe8b --- /dev/null +++ b/repos/effect/packages/experimental/src/RateLimiter/Redis.ts @@ -0,0 +1,158 @@ +/** + * @since 1.0.0 + */ +import * as Config from "effect/Config" +import * as Duration from "effect/Duration" +import * as Effect from "effect/Effect" +import * as Layer from "effect/Layer" +import type { RedisOptions } from "ioredis" +import { Redis } from "ioredis" +import * as RateLimiter from "../RateLimiter.js" + +interface RedisWithRateLimiting extends Redis { + fixedWindow(key: string, tokens: number, refillMillis: number, limit?: number): Promise<[number, number]> + tokenBucket( + key: string, + tokens: number, + refillMillis: number, + limit: number, + now: number, + overflow: 0 | 1 + ): Promise +} + +/** + * @since 1.0.0 + * @category Constructors + */ +export const make = Effect.fnUntraced(function*( + options: RedisOptions & { + readonly prefix?: string | undefined + } +) { + const prefix = options.prefix ?? "ratelimiter:" + const redis = yield* Effect.acquireRelease( + Effect.sync(() => new Redis(options) as RedisWithRateLimiting), + (redis) => Effect.promise(() => redis.quit()) + ) + + redis.defineCommand("fixedWindow", { + lua: ` +local key = KEYS[1] +local tokens = tonumber(ARGV[1]) +local refillms = tonumber(ARGV[2]) +local limit = tonumber(ARGV[3]) +local current = tonumber(redis.call("GET", key)) + +if not current then + local nextpttl = refillms * tokens + redis.call("SET", key, tokens, "PX", nextpttl) + return { tokens, nextpttl } +end + +local currentpttl = tonumber(redis.call("PTTL", key) or "0") +local next = current + tokens +if limit and next > limit then + return { next, currentpttl } +end + +local nextpttl = currentpttl + (refillms * tokens) +redis.call("SET", key, next, "PX", nextpttl) +return { next, nextpttl } +`, + numberOfKeys: 1, + readOnly: false + }) + + redis.defineCommand("tokenBucket", { + lua: ` +local key = KEYS[1] +local last_refill_key = key .. ":refill" +local tokens = tonumber(ARGV[1]) +local refill_ms = tonumber(ARGV[2]) +local limit = tonumber(ARGV[3]) +local now = tonumber(ARGV[4]) +local overflow = ARGV[5] == "1" +local current = tonumber(redis.call("GET", key)) +local last_refill = tonumber(redis.call("GET", last_refill_key)) + +if not current then + current = limit + last_refill = now + redis.call("SET", key, current) + redis.call("SET", last_refill_key, last_refill) +end + +local elapsed = now - last_refill +local refill_amount = math.floor(elapsed / refill_ms) +if refill_amount > 0 then + current = math.min(current + refill_amount, limit) + last_refill = last_refill + (refill_amount * refill_ms) + redis.call("SET", last_refill_key, last_refill) +end + +local next = current - tokens +if next < 0 and not overflow then + redis.call("SET", key, current) + return next +end + +redis.call("SET", key, next) +return next +`, + numberOfKeys: 1, + readOnly: false + }) + + return RateLimiter.RateLimiterStore.of({ + fixedWindow(options) { + const key = `${prefix}${options.key}` + const refillMillis = Duration.toMillis(options.refillRate) + return Effect.tryPromise({ + try: () => redis.fixedWindow(key, options.tokens, refillMillis, options.limit), + catch: (cause) => + new RateLimiter.RateLimitStoreError({ + message: `Failed to execute fixedWindow rate limiting command`, + cause + }) + }) + }, + tokenBucket(options) { + const key = `${prefix}${options.key}` + const refillMillis = Duration.toMillis(options.refillRate) + return Effect.clockWith((clock) => + Effect.tryPromise({ + try: () => + redis.tokenBucket( + key, + options.tokens, + refillMillis, + options.limit, + clock.unsafeCurrentTimeMillis(), + options.allowOverflow ? 1 : 0 + ), + catch: (cause) => + new RateLimiter.RateLimitStoreError({ + message: `Failed to execute tokenBucket rate limiting command`, + cause + }) + }) + ) + } + }) +}) + +/** + * @since 1.0.0 + * @category Layers + */ +export const layerStore = (options: RedisOptions & { readonly prefix?: string | undefined }) => + Layer.scoped(RateLimiter.RateLimiterStore, make(options)) + +/** + * @since 1.0.0 + * @category Layers + */ +export const layerStoreConfig = ( + options: Config.Config.Wrap +) => Layer.scoped(RateLimiter.RateLimiterStore, Effect.flatMap(Config.unwrap(options), make)) diff --git a/repos/effect/packages/experimental/src/Reactivity.ts b/repos/effect/packages/experimental/src/Reactivity.ts new file mode 100644 index 0000000..11a5fd5 --- /dev/null +++ b/repos/effect/packages/experimental/src/Reactivity.ts @@ -0,0 +1,270 @@ +/** + * @since 1.0.0 + */ +import * as Context from "effect/Context" +import * as Effect from "effect/Effect" +import * as Exit from "effect/Exit" +import * as FiberHandle from "effect/FiberHandle" +import { dual } from "effect/Function" +import * as Hash from "effect/Hash" +import * as Layer from "effect/Layer" +import * as Mailbox from "effect/Mailbox" +import type { ReadonlyRecord } from "effect/Record" +import * as Scope from "effect/Scope" +import * as Stream from "effect/Stream" + +/** + * @since 1.0.0 + * @category tags + */ +export class Reactivity extends Context.Tag("@effect/experimental/Reactivity")< + Reactivity, + Reactivity.Service +>() {} + +/** + * @since 1.0.0 + * @category constructors + */ +export const make = Effect.sync(() => { + const handlers = new Map void>>() + + const unsafeInvalidate = (keys: ReadonlyArray | ReadonlyRecord>): void => { + if (Array.isArray(keys)) { + for (let i = 0; i < keys.length; i++) { + const set = handlers.get(stringOrHash(keys[i])) + if (set === undefined) continue + for (const run of set) run() + } + } else { + const record = keys as ReadonlyRecord> + for (const key in record) { + const hashes = idHashes(key, record[key]) + for (let i = 0; i < hashes.length; i++) { + const set = handlers.get(hashes[i]) + if (set === undefined) continue + for (const run of set) run() + } + + const set = handlers.get(key) + if (set !== undefined) { + for (const run of set) run() + } + } + } + } + + const invalidate = ( + keys: ReadonlyArray | ReadonlyRecord> + ): Effect.Effect => Effect.sync(() => unsafeInvalidate(keys)) + + const mutation = ( + keys: ReadonlyArray | ReadonlyRecord>, + effect: Effect.Effect + ): Effect.Effect => Effect.zipLeft(effect, invalidate(keys)) + + const unsafeRegister = ( + keys: ReadonlyArray | ReadonlyRecord>, + handler: () => void + ): () => void => { + const resolvedKeys = Array.isArray(keys) ? keys.map(stringOrHash) : recordHashes(keys as any) + for (let i = 0; i < resolvedKeys.length; i++) { + let set = handlers.get(resolvedKeys[i]) + if (set === undefined) { + set = new Set() + handlers.set(resolvedKeys[i], set) + } + set.add(handler) + } + return () => { + for (let i = 0; i < resolvedKeys.length; i++) { + const set = handlers.get(resolvedKeys[i])! + set.delete(handler) + if (set.size === 0) { + handlers.delete(resolvedKeys[i]) + } + } + } + } + + const query = ( + keys: ReadonlyArray | ReadonlyRecord>, + effect: Effect.Effect + ): Effect.Effect, never, R | Scope.Scope> => + Effect.gen(function*() { + const scope = yield* Effect.scope + const results = yield* Mailbox.make() + const runFork = yield* FiberHandle.makeRuntime() + + let running = false + let pending = false + const handleExit = (exit: Exit.Exit) => { + if (exit._tag === "Failure") { + results.unsafeDone(Exit.failCause(exit.cause)) + } else { + results.unsafeOffer(exit.value) + } + if (pending) { + pending = false + runFork(effect).addObserver(handleExit) + } else { + running = false + } + } + + function run() { + if (running) { + pending = true + return + } + running = true + runFork(effect).addObserver(handleExit) + } + + const cancel = unsafeRegister(keys, run) + yield* Scope.addFinalizer(scope, Effect.sync(cancel)) + run() + + return results as Mailbox.ReadonlyMailbox + }) + + const stream = ( + tables: ReadonlyArray | ReadonlyRecord>, + effect: Effect.Effect + ): Stream.Stream> => + query(tables, effect).pipe( + Effect.map(Mailbox.toStream), + Stream.unwrapScoped + ) + + return Reactivity.of({ mutation, query, stream, unsafeInvalidate, invalidate, unsafeRegister }) +}) + +/** + * @since 1.0.0 + * @category accessors + */ +export const mutation: { + ( + keys: ReadonlyArray | ReadonlyRecord> + ): (effect: Effect.Effect) => Effect.Effect + ( + effect: Effect.Effect, + keys: ReadonlyArray | ReadonlyRecord> + ): Effect.Effect +} = dual(2, ( + effect: Effect.Effect, + keys: ReadonlyArray | ReadonlyRecord> +): Effect.Effect => Effect.flatMap(Reactivity, (r) => r.mutation(keys, effect))) + +/** + * @since 1.0.0 + * @category accessors + */ +export const query: { + ( + keys: ReadonlyArray | ReadonlyRecord> + ): ( + effect: Effect.Effect + ) => Effect.Effect, never, R | Scope.Scope | Reactivity> + ( + effect: Effect.Effect, + keys: ReadonlyArray | ReadonlyRecord> + ): Effect.Effect, never, R | Scope.Scope | Reactivity> +} = dual(2, ( + effect: Effect.Effect, + keys: ReadonlyArray | ReadonlyRecord> +): Effect.Effect, never, R | Scope.Scope | Reactivity> => + Effect.flatMap(Reactivity, (r) => r.query(keys, effect))) + +/** + * @since 1.0.0 + * @category accessors + */ +export const stream: { + ( + keys: ReadonlyArray | ReadonlyRecord> + ): (effect: Effect.Effect) => Stream.Stream | Reactivity> + ( + effect: Effect.Effect, + keys: ReadonlyArray | ReadonlyRecord> + ): Stream.Stream | Reactivity> +} = dual(2, ( + effect: Effect.Effect, + keys: ReadonlyArray | ReadonlyRecord> +): Stream.Stream | Reactivity> => + Reactivity.pipe( + Effect.flatMap((r) => r.query(keys, effect)), + Effect.map(Mailbox.toStream), + Stream.unwrapScoped + )) + +/** + * @since 1.0.0 + * @category accessors + */ +export const invalidate = ( + keys: ReadonlyArray | ReadonlyRecord> +): Effect.Effect => Effect.flatMap(Reactivity, (r) => r.invalidate(keys)) + +/** + * @since 1.0.0 + * @category layers + */ +export const layer: Layer.Layer = Layer.scoped(Reactivity, make) + +/** + * @since 1.0.0 + * @category model + */ +export declare namespace Reactivity { + /** + * @since 1.0.0 + * @category model + */ + export interface Service { + readonly unsafeInvalidate: (keys: ReadonlyArray | ReadonlyRecord>) => void + readonly unsafeRegister: ( + keys: ReadonlyArray | ReadonlyRecord>, + handler: () => void + ) => () => void + readonly invalidate: ( + keys: ReadonlyArray | ReadonlyRecord> + ) => Effect.Effect + readonly mutation: ( + keys: ReadonlyArray | ReadonlyRecord>, + effect: Effect.Effect + ) => Effect.Effect + readonly query: ( + keys: ReadonlyArray | ReadonlyRecord>, + effect: Effect.Effect + ) => Effect.Effect, never, R | Scope.Scope> + readonly stream: ( + keys: ReadonlyArray | ReadonlyRecord>, + effect: Effect.Effect + ) => Stream.Stream> + } +} + +function stringOrHash(u: unknown): string | number { + return typeof u === "string" ? u : Hash.hash(u) +} + +const idHashes = (keyHash: number | string, ids: ReadonlyArray): ReadonlyArray => { + const hashes: Array = new Array(ids.length) + for (let i = 0; i < ids.length; i++) { + hashes[i] = `${keyHash}:${stringOrHash(ids[i])}` + } + return hashes +} + +const recordHashes = (record: ReadonlyRecord>): ReadonlyArray => { + const hashes: Array = [] + for (const key in record) { + hashes.push(key) + for (const idHash of idHashes(key, record[key])) { + hashes.push(idHash) + } + } + return hashes +} diff --git a/repos/effect/packages/experimental/src/RequestResolver.ts b/repos/effect/packages/experimental/src/RequestResolver.ts new file mode 100644 index 0000000..cca706b --- /dev/null +++ b/repos/effect/packages/experimental/src/RequestResolver.ts @@ -0,0 +1,217 @@ +/** + * @since 1.0.0 + */ +import * as Arr from "effect/Array" +import type * as Duration from "effect/Duration" +import * as Effect from "effect/Effect" +import * as Either from "effect/Either" +import * as Fiber from "effect/Fiber" +import { dual, pipe } from "effect/Function" +import * as Option from "effect/Option" +import * as Request from "effect/Request" +import * as RequestResolver from "effect/RequestResolver" +import * as Runtime from "effect/Runtime" +import type * as Schema from "effect/Schema" +import * as Scope from "effect/Scope" +import * as Persistence from "./Persistence.js" + +interface DataLoaderItem> { + readonly request: A + readonly resume: (effect: Effect.Effect, Request.Request.Error>) => void +} + +/** + * @since 1.0.0 + * @category combinators + */ +export const dataLoader = dual< + (options: { + readonly window: Duration.DurationInput + readonly maxBatchSize?: number + }) => >( + self: RequestResolver.RequestResolver + ) => Effect.Effect, never, Scope.Scope>, + >( + self: RequestResolver.RequestResolver, + options: { + readonly window: Duration.DurationInput + readonly maxBatchSize?: number + } + ) => Effect.Effect, never, Scope.Scope> +>( + 2, + Effect.fnUntraced(function*< + A extends Request.Request + >(self: RequestResolver.RequestResolver, options: { + readonly window: Duration.DurationInput + readonly maxBatchSize?: number + }) { + const maxSize = options.maxBatchSize ?? Infinity + const scope = yield* Effect.scope + const runtime = yield* Effect.runtime().pipe( + Effect.interruptible + ) + const runFork = Runtime.runFork(runtime) + + let batch = new Set>() + const process = (items: Iterable>) => + Effect.withRequestCaching( + Effect.forEach( + items, + ({ request, resume }) => + Effect.request(request, self).pipe( + Effect.exit, + Effect.map(resume) + ), + { batching: true, discard: true } + ), + false + ) + const delayedProcess = Effect.sleep(options.window).pipe( + Effect.flatMap(() => { + const currentBatch = batch + batch = new Set() + fiber = undefined + return process(currentBatch) + }) + ) + + let fiber: Fiber.RuntimeFiber | undefined + yield* Scope.addFinalizer(scope, Effect.suspend(() => fiber ? Fiber.interrupt(fiber) : Effect.void)) + + return RequestResolver.fromEffect((request: A) => + Effect.async, Request.Request.Error>((resume) => { + const item: DataLoaderItem = { request, resume } + batch.add(item) + if (batch.size >= maxSize) { + const currentBatch = batch + batch = new Set() + if (fiber) { + const parent = Option.getOrThrow(Fiber.getCurrentFiber()) + fiber.unsafeInterruptAsFork(parent.id()) + fiber = undefined + } + runFork(process(currentBatch)) + } else if (!fiber) { + fiber = runFork(delayedProcess) + } + + return Effect.sync(() => { + batch.delete(item) + }) + }) + ) + }) +) + +/** + * @since 1.0.0 + * @category model + */ +export interface PersistedRequest extends Request.Request, Schema.WithResult {} + +/** + * @since 1.0.0 + * @category model + */ +export declare namespace PersistedRequest { + /** + * @since 1.0.0 + * @category model + */ + export type Any = PersistedRequest | PersistedRequest +} + +/** + * @since 1.0.0 + * @category combinators + */ +export const persisted: { + (options: { + readonly storeId: string + readonly timeToLive: (...args: Persistence.ResultPersistence.TimeToLiveArgs) => Duration.DurationInput + }): ( + self: RequestResolver.RequestResolver + ) => Effect.Effect< + RequestResolver.RequestResolver>, + never, + Persistence.ResultPersistence | Scope.Scope + > + ( + self: RequestResolver.RequestResolver, + options: { + readonly storeId: string + readonly timeToLive: (...args: Persistence.ResultPersistence.TimeToLiveArgs) => Duration.DurationInput + } + ): Effect.Effect< + RequestResolver.RequestResolver>, + never, + Persistence.ResultPersistence | Scope.Scope + > +} = dual(2, ( + self: RequestResolver.RequestResolver, + options: { + readonly storeId: string + readonly timeToLive: (...args: Persistence.ResultPersistence.TimeToLiveArgs) => Duration.DurationInput + } +): Effect.Effect< + RequestResolver.RequestResolver>, + never, + Persistence.ResultPersistence | Scope.Scope +> => + Effect.gen(function*() { + const storage = yield* (yield* Persistence.ResultPersistence).make({ + storeId: options.storeId, + timeToLive: options.timeToLive as any + }) + + const partition = (requests: ReadonlyArray) => + storage.getMany(requests as any).pipe( + Effect.map( + Arr.partitionMap((_, i) => + Option.match(_, { + onNone: () => Either.left(requests[i]), + onSome: (_) => Either.right([requests[i], _] as const) + }) + ) + ), + Effect.orElseSucceed(() => [requests, []] as const) + ) + + const set = ( + request: Req, + result: Request.Request.Result + ): Effect.Effect => Effect.ignoreLogged(storage.set(request as any, result)) + + return RequestResolver.makeBatched((requests: Arr.NonEmptyArray) => + Effect.flatMap(partition(requests), ([remaining, results]) => { + const completeCached = Effect.forEach( + results, + ([request, result]) => Request.complete(request, result as any) as Effect.Effect, + { discard: true } + ) + const completeUncached = pipe( + Effect.forEach( + remaining, + (request) => Effect.exit(Effect.request(request, self)), + { batching: true } + ), + Effect.flatMap((results) => + Effect.forEach( + results, + (result, i) => { + const request = remaining[i] + return Effect.zipRight( + set(request, result as any), + Request.complete(request, result as any) + ) + }, + { discard: true } + ) + ), + Effect.withRequestCaching(false) + ) + return Effect.zipRight(completeCached, completeUncached) + }) + ) + })) diff --git a/repos/effect/packages/experimental/src/Sse.ts b/repos/effect/packages/experimental/src/Sse.ts new file mode 100644 index 0000000..7788fe5 --- /dev/null +++ b/repos/effect/packages/experimental/src/Sse.ts @@ -0,0 +1,352 @@ +/** + * @since 1.0.0 + */ +import * as Channel from "effect/Channel" +import * as Chunk from "effect/Chunk" +import * as Data from "effect/Data" +import * as Duration from "effect/Duration" +import * as Effect from "effect/Effect" +import * as Mailbox from "effect/Mailbox" +import { hasProperty } from "effect/Predicate" +import type * as AsyncInput from "effect/SingleProducerAsyncInput" + +/** + * @since 1.0.0 + * @category constructors + */ +export const makeChannel = (options?: { + readonly bufferSize?: number +}): Channel.Channel< + Chunk.Chunk, + Chunk.Chunk, + IE, + IE, + void, + Done +> => { + const events = Mailbox.make(options?.bufferSize ?? 16).pipe( + Effect.map((mailbox) => { + let events: Array = [] + let retry: Retry | undefined + const parser = makeParser((event) => { + switch (event._tag) { + case "Retry": + return (retry = event) + case "Event": + return events.push(event) + } + }) + const input: AsyncInput.AsyncInputProducer< + IE, + Chunk.Chunk, + Done + > = { + awaitRead() { + return Effect.void + }, + emit(chunks) { + Chunk.forEach(chunks, parser.feed) + const toEmit = events + events = [] + return retry + ? Effect.zipRight(mailbox.offerAll(toEmit), mailbox.fail(retry)) + : mailbox.offerAll(toEmit) + }, + error(cause) { + return mailbox.failCause(cause) + }, + done(_) { + return mailbox.end + } + } + return Channel.embedInput(Mailbox.toChannel(mailbox), input) + }), + Channel.unwrap + ) + + const withRetry: Channel.Channel< + Chunk.Chunk, + Chunk.Chunk, + IE, + IE, + void, + Done + > = Channel.catchAll(events, (error) => + Retry.is(error) ? + Effect.sleep(error.duration).pipe( + Effect.as(withRetry), + Channel.unwrap + ) : + Channel.fail(error)) + + return withRetry +} + +/** + * Create a SSE parser. + * + * Adapted from https://github.com/rexxars/eventsource-parser under MIT license. + * + * @since 1.0.0 + * @category constructors + */ +export function makeParser(onParse: (event: AnyEvent) => void): Parser { + // Processing state + let isFirstChunk: boolean + let buffer: string + let startingPosition: number + let startingFieldLength: number + + // Event state + let eventId: string | undefined + let lastEventId: string | undefined + let eventName: string | undefined + let data: string + + reset() + return { feed, reset } + + function reset(): void { + isFirstChunk = true + buffer = "" + startingPosition = 0 + startingFieldLength = -1 + + eventId = undefined + eventName = undefined + data = "" + } + + function feed(chunk: string): void { + buffer = buffer ? buffer + chunk : chunk + + // Strip any UTF8 byte order mark (BOM) at the start of the stream. + // Note that we do not strip any non - UTF8 BOM, as eventsource streams are + // always decoded as UTF8 as per the specification. + if (isFirstChunk && hasBom(buffer)) { + buffer = buffer.slice(BOM.length) + } + + isFirstChunk = false + + // Set up chunk-specific processing state + const length = buffer.length + let position = 0 + let discardTrailingNewline = false + + // Read the current buffer byte by byte + while (position < length) { + // EventSource allows for carriage return + line feed, which means we + // need to ignore a linefeed character if the previous character was a + // carriage return + // @todo refactor to reduce nesting, consider checking previous byte? + // @todo but consider multiple chunks etc + if (discardTrailingNewline) { + if (buffer[position] === "\n") { + ++position + } + discardTrailingNewline = false + } + + let lineLength = -1 + let fieldLength = startingFieldLength + let character: string + + for (let index = startingPosition; lineLength < 0 && index < length; ++index) { + character = buffer[index] + if (character === ":" && fieldLength < 0) { + fieldLength = index - position + } else if (character === "\r") { + discardTrailingNewline = true + lineLength = index - position + } else if (character === "\n") { + lineLength = index - position + } + } + + if (lineLength < 0) { + startingPosition = length - position + startingFieldLength = fieldLength + break + } else { + startingPosition = 0 + startingFieldLength = -1 + } + + parseEventStreamLine(buffer, position, fieldLength, lineLength) + + position += lineLength + 1 + } + + if (position === length) { + // If we consumed the entire buffer to read the event, reset the buffer + buffer = "" + } else if (position > 0) { + // If there are bytes left to process, set the buffer to the unprocessed + // portion of the buffer only + buffer = buffer.slice(position) + } + } + + function parseEventStreamLine( + lineBuffer: string, + index: number, + fieldLength: number, + lineLength: number + ) { + if (lineLength === 0) { + // We reached the last line of this event + if (data.length > 0) { + onParse({ + _tag: "Event", + id: eventId, + event: eventName ?? "message", + data: data.slice(0, -1) // remove trailing newline + }) + data = "" + eventId = undefined + } + eventName = undefined + return + } + + const noValue = fieldLength < 0 + const field = lineBuffer.slice(index, index + (noValue ? lineLength : fieldLength)) + let step = 0 + + if (noValue) { + step = lineLength + } else if (lineBuffer[index + fieldLength + 1] === " ") { + step = fieldLength + 2 + } else { + step = fieldLength + 1 + } + + const position = index + step + const valueLength = lineLength - step + const value = lineBuffer.slice(position, position + valueLength).toString() + + if (field === "data") { + data += value ? `${value}\n` : "\n" + } else if (field === "event") { + eventName = value + } else if (field === "id" && !value.includes("\u0000")) { + eventId = value + lastEventId = value + } else if (field === "retry") { + const retry = parseInt(value, 10) + if (!Number.isNaN(retry)) { + onParse(new Retry({ duration: Duration.millis(retry), lastEventId })) + } + } + } +} + +const BOM = [239, 187, 191] + +function hasBom(buffer: string) { + return BOM.every((charCode: number, index: number) => buffer.charCodeAt(index) === charCode) +} + +/** + * @since 1.0.0 + * @category models + */ +export interface Parser { + feed(chunk: string): void + reset(): void +} + +/** + * @since 1.0.0 + * @category models + */ +export interface Encoder { + write(event: AnyEvent): string +} + +/** + * @since 1.0.0 + * @category models + */ +export interface Event { + readonly _tag: "Event" + readonly event: string + readonly id: string | undefined + readonly data: string +} + +/** + * @since 1.0.0 + * @category models + */ +export interface EventEncoded { + readonly event: string + readonly id: string | undefined + readonly data: string +} + +/** + * @since 1.0.0 + * @category type ids + */ +export const RetryTypeId: unique symbol = Symbol.for("@effect/experimental/Sse/Retry") + +/** + * @since 1.0.0 + * @category type ids + */ +export type RetryTypeId = typeof RetryTypeId + +/** + * @since 1.0.0 + * @category models + */ +export class Retry extends Data.TaggedClass("Retry")<{ + readonly duration: Duration.Duration + readonly lastEventId: string | undefined +}> { + /** + * @since 1.0.0 + */ + readonly [RetryTypeId]: RetryTypeId = RetryTypeId + /** + * @since 1.0.0 + */ + static is(u: unknown): u is Retry { + return hasProperty(u, RetryTypeId) + } +} + +/** + * @since 1.0.0 + * @category models + */ +export type AnyEvent = Event | Retry + +/** + * @since 1.0.0 + * @category constructors + */ +export const encoder: Encoder = { + write(event: AnyEvent): string { + switch (event._tag) { + case "Event": { + let data = "" + if (event.id !== undefined) { + data += `id: ${event.id}\n` + } + if (event.event !== "message") { + data += `event: ${event.event}\n` + } + if (event.data !== "") { + data += `data: ${event.data.replace(/\n/g, "\ndata: ")}\n` + } + return data + "\n" + } + case "Retry": { + return `retry: ${Duration.toMillis(event.duration)}\n\n` + } + } + } +} diff --git a/repos/effect/packages/experimental/src/VariantSchema.ts b/repos/effect/packages/experimental/src/VariantSchema.ts new file mode 100644 index 0000000..6c22683 --- /dev/null +++ b/repos/effect/packages/experimental/src/VariantSchema.ts @@ -0,0 +1,677 @@ +/** + * @since 1.0.0 + */ +import type { Brand } from "effect/Brand" +import type * as Effect from "effect/Effect" +import { constUndefined, dual } from "effect/Function" +import * as Option from "effect/Option" +import * as ParseResult from "effect/ParseResult" +import { type Pipeable, pipeArguments } from "effect/Pipeable" +import * as Predicate from "effect/Predicate" +import * as Schema from "effect/Schema" +import type * as AST from "effect/SchemaAST" +import * as Struct_ from "effect/Struct" + +/** + * @since 1.0.0 + * @category type ids + */ +export const TypeId: unique symbol = Symbol.for("@effect/experimental/VariantSchema") + +/** + * @since 1.0.0 + * @category type ids + */ +export type TypeId = typeof TypeId + +const cacheSymbol = Symbol.for("@effect/experimental/VariantSchema/cache") + +/** + * @since 1.0.0 + * @category models + */ +export interface Struct extends Pipeable { + readonly [TypeId]: A + /** @internal */ + [cacheSymbol]?: Record +} + +/** + * @since 1.0.0 + * @category guards + */ +export const isStruct = (u: unknown): u is Struct => Predicate.hasProperty(u, TypeId) + +/** + * @since 1.0.0 + * @category models + */ +export declare namespace Struct { + /** + * @since 1.0.0 + * @category models + */ + export type Any = { readonly [TypeId]: any } + + /** + * @since 1.0.0 + * @category models + */ + export type Fields = { + readonly [key: string]: + | Schema.Schema.All + | Schema.PropertySignature.All + | Field + | Struct + | undefined + } + + /** + * @since 1.0.0 + * @category models + */ + export type Validate = { + readonly [K in keyof A]: A[K] extends { readonly [TypeId]: infer _ } ? Validate : + A[K] extends Field ? [keyof Config] extends [Variant] ? {} : "field must have valid variants" + : {} + } +} + +/** + * @since 1.0.0 + * @category type ids + */ +export const FieldTypeId: unique symbol = Symbol.for( + "@effect/experimental/VariantSchema/Field" +) + +/** + * @since 1.0.0 + * @category type ids + */ +export type FieldTypeId = typeof FieldTypeId + +/** + * @since 1.0.0 + * @category models + */ +export interface Field extends Pipeable { + readonly [FieldTypeId]: FieldTypeId + readonly schemas: A +} + +/** + * @since 1.0.0 + * @category guards + */ +export const isField = (u: unknown): u is Field => Predicate.hasProperty(u, FieldTypeId) + +/** + * @since 1.0.0 + * @category models + */ +export declare namespace Field { + /** + * @since 1.0.0 + * @category models + */ + export type Any = { readonly [FieldTypeId]: FieldTypeId } + + /** + * @since 1.0.0 + * @category models + */ + type ValueAny = Schema.Schema.All | Schema.PropertySignature.All + + /** + * @since 1.0.0 + * @category models + */ + export type Config = { + readonly [key: string]: Schema.Schema.All | Schema.PropertySignature.All | undefined + } + + /** + * @since 1.0.0 + * @category models + */ + export type ConfigWithKeys = { + readonly [P in K]?: Schema.Schema.All | Schema.PropertySignature.All + } + + /** + * @since 1.0.0 + * @category models + */ + export type Fields = { + readonly [key: string]: + | Schema.Schema.All + | Schema.PropertySignature.All + | Field + | Struct + | undefined + } +} + +/** + * @since 1.0.0 + * @category extractors + */ +export type ExtractFields = { + readonly [ + K in keyof Fields as [Fields[K]] extends [Field] ? V extends keyof Config ? K + : never + : K + ]: [Fields[K]] extends [Struct] ? Extract + : [Fields[K]] extends [Field] + ? [Config[V]] extends [Schema.Schema.All | Schema.PropertySignature.All] ? Config[V] + : never + : [Fields[K]] extends [Schema.Schema.All | Schema.PropertySignature.All] ? Fields[K] + : never +} + +/** + * @since 1.0.0 + * @category extractors + */ +export type Extract, IsDefault = false> = [A] extends [ + Struct +] ? + IsDefault extends true + ? [A] extends [Schema.Schema.Any] ? A : Schema.Struct>> + : Schema.Struct>> + : never + +const extract: { + ( + variant: V, + options?: { + readonly isDefault?: IsDefault | undefined + } + ): >(self: A) => Extract + , const IsDefault extends boolean = false>(self: A, variant: V, options?: { + readonly isDefault?: IsDefault | undefined + }): Extract +} = dual( + (args) => isStruct(args[0]), + >( + self: A, + variant: V, + options?: { + readonly isDefault?: boolean | undefined + } + ): Extract => { + const cache = self[cacheSymbol] ?? (self[cacheSymbol] = {}) + const cacheKey = options?.isDefault === true ? "__default" : variant + if (cache[cacheKey] !== undefined) { + return cache[cacheKey] as any + } + const fields: Record = {} + for (const key of Object.keys(self[TypeId])) { + const value = self[TypeId][key] + if (TypeId in value) { + if (options?.isDefault === true && Schema.isSchema(value)) { + fields[key] = value + } else { + fields[key] = extract(value, variant) + } + } else if (FieldTypeId in value) { + if (variant in value.schemas) { + fields[key] = value.schemas[variant] + } + } else { + fields[key] = value + } + } + return cache[cacheKey] = Schema.Struct(fields) as any + } +) + +/** + * @category accessors + * @since 1.0.0 + */ +export const fields = >(self: A): A[TypeId] => self[TypeId] + +type RequiredKeys = { + [K in keyof T]-?: {} extends Pick ? never : K +}[keyof T] + +/** + * @since 1.0.0 + * @category models + */ +export interface Class< + Self, + Fields extends Struct.Fields, + SchemaFields extends Schema.Struct.Fields, + A, + I, + R, + C +> extends Schema.Schema, R>, Struct> { + new( + props: RequiredKeys extends never ? void | Schema.Simplify + : Schema.Simplify, + options?: { + readonly disableValidation?: boolean + } + ): A + + readonly ast: AST.Transformation + + make, X>( + this: { new(...args: Args): X }, + ...args: Args + ): X + + annotations( + annotations: Schema.Annotations.Schema + ): Schema.SchemaClass + + readonly identifier: string + readonly fields: Schema.Simplify +} + +type ClassFromFields< + Self, + Fields extends Struct.Fields, + SchemaFields extends Schema.Struct.Fields +> = Class< + Self, + Fields, + SchemaFields, + Schema.Struct.Type, + Schema.Struct.Encoded, + Schema.Struct.Context, + Schema.Struct.Constructor +> + +type MissingSelfGeneric = + `Missing \`Self\` generic - use \`class Self extends Class()(${Params}{ ... })\`` + +/** + * @since 1.0.0 + * @category models + */ +export interface Union>> extends + Schema.Union< + { + readonly [K in keyof Members]: [Members[K]] extends [Schema.Schema.All] ? Members[K] : never + } + > +{} + +/** + * @since 1.0.0 + * @category models + */ +export declare namespace Union { + /** + * @since 1.0.0 + * @category models + */ + export type Variants>, Variants extends string> = { + readonly [Variant in Variants]: Schema.Union< + { + [K in keyof Members]: Extract + } + > + } +} + +/** + * @since 1.0.0 + * @category models + */ +export interface fromKey extends + Schema.PropertySignature< + ":", + Schema.Schema.Type, + Key, + ":", + Schema.Schema.Encoded, + false, + Schema.Schema.Context + > +{} + +/** + * @since 1.0.0 + * @category models + */ +export declare namespace fromKey { + /** + * @since 1.0.0 + */ + export type Rename = S extends Schema.PropertySignature< + infer _TypeToken, + infer _Type, + infer _Key, + infer _EncodedToken, + infer _Encoded, + infer _HasDefault, + infer _R + > ? Schema.PropertySignature<_TypeToken, _Type, Key, _EncodedToken, _Encoded, _HasDefault, _R> + : S extends Schema.Schema.All ? fromKey + : never +} + +/** + * @since 1.0.0 + * @category constructors + */ +export const make = < + const Variants extends ReadonlyArray, + const Default extends Variants[number] +>(options: { + readonly variants: Variants + readonly defaultVariant: Default +}): { + readonly Struct: ( + fields: A & Struct.Validate + ) => Struct + readonly Field: >( + config: A & { readonly [K in Exclude]: never } + ) => Field + readonly FieldOnly: >( + ...keys: Keys + ) => ( + schema: S + ) => Field<{ readonly [K in Keys[number]]: S }> + readonly FieldExcept: >( + ...keys: Keys + ) => ( + schema: S + ) => Field<{ readonly [K in Exclude]: S }> + readonly fieldEvolve: { + < + Self extends Field | Field.ValueAny, + const Mapping + extends (Self extends Field ? { readonly [K in keyof S]?: (variant: S[K]) => Field.ValueAny } + : { readonly [K in Variants[number]]?: (variant: Self) => Field.ValueAny }) + >(f: Mapping): (self: Self) => Field< + Self extends Field ? { + readonly [K in keyof S]: K extends keyof Mapping + ? Mapping[K] extends (arg: any) => any ? ReturnType : S[K] + : S[K] + } : + { + readonly [K in Variants[number]]: K extends keyof Mapping + ? Mapping[K] extends (arg: any) => any ? ReturnType : Self + : Self + } + > + < + Self extends Field | Field.ValueAny, + const Mapping extends (Self extends Field ? { + readonly [K in keyof S]?: (variant: S[K]) => Field.ValueAny + } + : { readonly [K in Variants[number]]?: (variant: Self) => Field.ValueAny }) + >(self: Self, f: Mapping): Field< + Self extends Field ? { + readonly [K in keyof S]: K extends keyof Mapping + ? Mapping[K] extends (arg: any) => any ? ReturnType : S[K] + : S[K] + } : + { + readonly [K in Variants[number]]: K extends keyof Mapping + ? Mapping[K] extends (arg: any) => any ? ReturnType : Self + : Self + } + > + } + readonly fieldFromKey: { + < + Self extends Field | Field.ValueAny, + const Mapping extends (Self extends Field ? { readonly [K in keyof S]?: string } + : { readonly [K in Variants[number]]?: string }) + >( + mapping: Mapping + ): (self: Self) => Field< + Self extends Field ? { + readonly [K in keyof S]: K extends keyof Mapping ? + Mapping[K] extends string ? fromKey.Rename + : S[K] : + S[K] + } : + { + readonly [K in Variants[number]]: K extends keyof Mapping ? + Mapping[K] extends string ? fromKey.Rename + : Self : + Self + } + > + < + Self extends Field | Field.ValueAny, + const Mapping extends (Self extends Field ? { readonly [K in keyof S]?: string } + : { readonly [K in Variants[number]]?: string }) + >( + self: Self, + mapping: Mapping + ): Field< + Self extends Field ? { + readonly [K in keyof S]: K extends keyof Mapping ? + Mapping[K] extends string ? fromKey.Rename + : S[K] : + S[K] + } : + { + readonly [K in Variants[number]]: K extends keyof Mapping ? + Mapping[K] extends string ? fromKey.Rename + : Self : + Self + } + > + } + readonly Class: ( + identifier: string + ) => ( + fields: Fields & Struct.Validate, + annotations?: Schema.Annotations.Schema + ) => [Self] extends [never] ? MissingSelfGeneric + : + & ClassFromFields< + Self, + Fields, + ExtractFields + > + & { + readonly [V in Variants[number]]: Extract> + } + readonly Union: >>( + ...members: Members + ) => Union & Union.Variants + readonly extract: { + ( + variant: V + ): >(self: A) => Extract + >( + self: A, + variant: V + ): Extract + } +} => { + function Class(identifier: string) { + return function( + fields: Struct.Fields, + annotations?: Schema.Annotations.Schema + ) { + const variantStruct = Struct(fields) + const schema = extract(variantStruct, options.defaultVariant, { + isDefault: true + }) + class Base extends Schema.Class(identifier)(schema.fields, annotations) { + static [TypeId] = fields + } + for (const variant of options.variants) { + Object.defineProperty(Base, variant, { + value: extract(variantStruct, variant).annotations({ + identifier: `${identifier}.${variant}`, + title: `${identifier}.${variant}` + }) + }) + } + return Base + } + } + function FieldOnly(...keys: Keys) { + return function(schema: S) { + const obj: Record = {} + for (const key of keys) { + obj[key] = schema + } + return Field(obj) + } + } + function FieldExcept(...keys: Keys) { + return function(schema: S) { + const obj: Record = {} + for (const variant of options.variants) { + if (!keys.includes(variant)) { + obj[variant] = schema + } + } + return Field(obj) + } + } + function UnionVariants(...members: ReadonlyArray>) { + return Union(members, options.variants) + } + const fieldEvolve = dual( + 2, + ( + self: Field | Schema.Schema.All | Schema.PropertySignature.All, + f: Record Field.ValueAny> + ): Field => { + const field = isField(self) ? self : Field(Object.fromEntries( + options.variants.map((variant) => [variant, self]) + )) + return Field(Struct_.evolve(field.schemas, f)) + } + ) + const fieldFromKey = dual( + 2, + ( + self: + | Field<{ + readonly [key: string]: Schema.Schema.All | Schema.PropertySignature.Any | undefined + }> + | Schema.Schema.All + | Schema.PropertySignature.Any, + mapping: Record + ): Field => { + const obj: Record = {} + if (isField(self)) { + for (const [key, schema] of Object.entries(self.schemas)) { + obj[key] = mapping[key] !== undefined ? renameFieldValue(schema as any, mapping[key]) : schema + } + } else { + for (const key of options.variants) { + obj[key] = mapping[key] !== undefined ? renameFieldValue(self as any, mapping[key]) : self + } + } + return Field(obj) + } + ) + const extractVariants = dual( + 2, + (self: Struct, variant: string): any => + extract(self, variant, { + isDefault: variant === options.defaultVariant + }) + ) + return { + Struct, + Field, + FieldOnly, + FieldExcept, + Class, + Union: UnionVariants, + fieldEvolve, + fieldFromKey, + extract: extractVariants + } as any +} + +/** + * @since 1.0.0 + * @category overrideable + */ +export const Override = (value: A): A & Brand<"Override"> => value as any + +/** + * @since 1.0.0 + * @category overrideable + */ +export interface Overrideable + extends Schema.PropertySignature<":", (To & Brand<"Override">) | undefined, never, ":", From, true, R> +{} + +/** + * @since 1.0.0 + * @category overrideable + */ +export const Overrideable = ( + from: Schema.Schema, + to: Schema.Schema, + options: { + readonly generate: (_: Option.Option) => Effect.Effect + readonly decode?: Schema.Schema + readonly constructorDefault?: () => To + } +): Overrideable => + Schema.transformOrFail( + from, + Schema.Union(Schema.Undefined, to as Schema.brand, "Override">), + { + decode: (_) => options.decode ? ParseResult.decode(options.decode)(_) : ParseResult.succeed(undefined), + encode: (dt) => options.generate(dt === undefined ? Option.none() : Option.some(dt)) + } + ).pipe(Schema.propertySignature, Schema.withConstructorDefault(options.constructorDefault ?? constUndefined as any)) + +const StructProto = { + pipe() { + return pipeArguments(this, arguments) + } +} + +const Struct = (fields: A): Struct => { + const self = Object.create(StructProto) + self[TypeId] = fields + return self +} + +const FieldProto = { + [FieldTypeId]: FieldTypeId, + pipe() { + return pipeArguments(this, arguments) + } +} + +const Field = (schemas: A): Field => { + const self = Object.create(FieldProto) + self.schemas = schemas + return self +} + +const Union = >, Variants extends ReadonlyArray>( + members: Members, + variants: Variants +) => { + class VariantUnion extends (Schema.Union(...members.filter((member) => Schema.isSchema(member))) as any) {} + for (const variant of variants) { + Object.defineProperty(VariantUnion, variant, { + value: Schema.Union(...members.map((member) => extract(member, variant))) + }) + } + return VariantUnion +} + +const renameFieldValue = ( + self: F, + key: string +) => + Schema.isPropertySignature(self) + ? Schema.fromKey(self, key) + : Schema.fromKey(Schema.propertySignature(self), key) diff --git a/repos/effect/packages/experimental/src/index.ts b/repos/effect/packages/experimental/src/index.ts new file mode 100644 index 0000000..3e26b13 --- /dev/null +++ b/repos/effect/packages/experimental/src/index.ts @@ -0,0 +1,84 @@ +/** + * @since 1.0.0 + */ +export * as DevTools from "./DevTools.js" + +/** + * @since 1.0.0 + */ +export * as Event from "./Event.js" + +/** + * @since 1.0.0 + */ +export * as EventGroup from "./EventGroup.js" + +/** + * @since 1.0.0 + */ +export * as EventJournal from "./EventJournal.js" + +/** + * @since 1.0.0 + */ +export * as EventLog from "./EventLog.js" + +/** + * @since 1.0.0 + */ +export * as EventLogEncryption from "./EventLogEncryption.js" + +/** + * @since 1.0.0 + */ +export * as EventLogRemote from "./EventLogRemote.js" + +/** + * @since 1.0.0 + */ +export * as EventLogServer from "./EventLogServer.js" + +/** + * @since 1.0.0 + */ +export * as Machine from "./Machine.js" + +/** + * @since 1.0.0 + */ +export * as PersistedCache from "./PersistedCache.js" + +/** + * @since 1.0.0 + */ +export * as PersistedQueue from "./PersistedQueue.js" + +/** + * @since 1.0.0 + */ +export * as Persistence from "./Persistence.js" + +/** + * @since 1.0.0 + */ +export * as RateLimiter from "./RateLimiter.js" + +/** + * @since 1.0.0 + */ +export * as Reactivity from "./Reactivity.js" + +/** + * @since 1.0.0 + */ +export * as RequestResolver from "./RequestResolver.js" + +/** + * @since 1.0.0 + */ +export * as Sse from "./Sse.js" + +/** + * @since 1.0.0 + */ +export * as VariantSchema from "./VariantSchema.js" diff --git a/repos/effect/packages/experimental/test/Machine.test.ts b/repos/effect/packages/experimental/test/Machine.test.ts new file mode 100644 index 0000000..1342f1d --- /dev/null +++ b/repos/effect/packages/experimental/test/Machine.test.ts @@ -0,0 +1,267 @@ +import * as DevTools from "@effect/experimental/DevTools" +import * as Machine from "@effect/experimental/Machine" +import { assert, describe, test } from "@effect/vitest" +import { Cause, Chunk, Context, Deferred, Effect, Exit, Layer, Schema, Stream } from "effect" + +class Increment + extends Schema.TaggedRequest()("Increment", { failure: Schema.Never, success: Schema.Number, payload: {} }) +{} +class Decrement + extends Schema.TaggedRequest()("Decrement", { failure: Schema.Never, success: Schema.Number, payload: {} }) +{} +class IncrementBy extends Schema.TaggedRequest()("IncrementBy", { + failure: Schema.Never, + success: Schema.Number, + payload: { + number: Schema.Number + } +}) {} +class DelayedIncrementBy extends Schema.TaggedRequest()("DelayedIncrementBy", { + failure: Schema.Never, + success: Schema.Void, + payload: { + delay: Schema.Positive, + number: Schema.Number + } +}) {} +class Multiply + extends Schema.TaggedRequest()("Multiply", { failure: Schema.Never, success: Schema.Number, payload: {} }) +{} + +class FailBackground extends Schema.TaggedRequest()("FailBackground", { + failure: Schema.Never, + success: Schema.Void, + payload: {} +}) {} + +const counter = Machine.makeWith()( + (input, previous) => + Machine.procedures.make(previous ?? input, { + identifier: `Counter(${input})` + }).pipe( + Machine.procedures.add()("Increment", ({ state }) => + Effect.sync(() => { + const count = state + 1 + return [count, count] + })), + Machine.procedures.add()("Decrement", ({ state }) => + Effect.sync(() => { + const count = state - 1 + return [count, count] + })), + Machine.procedures.add()("IncrementBy", ({ request, state }) => + Effect.sync(() => { + const count = state + request.number + return [count, count] + })), + Machine.procedures.add()( + "FailBackground", + ({ forkWith, state }) => forkWith(Effect.fail("error"), state) + ) + ) +) + +const counterSerializable = Machine.makeSerializable( + { state: Schema.NumberFromString, input: Schema.Number }, + (input, previous) => + Machine.serializable.make(previous ?? input, { + identifier: `Counter(${input})` + }).pipe( + Machine.serializable.add(Increment, ({ state }) => + Effect.sync(() => { + const count = state + 1 + return [count, count] + })), + Machine.serializable.add(Decrement, ({ state }) => + Effect.sync(() => { + const count = state - 1 + return [count, count] + })), + Machine.serializable.add(IncrementBy, ({ request, state }) => + Effect.sync(() => { + const count = state + request.number + return [count, count] + })), + Machine.serializable.add( + FailBackground, + ({ forkWith, state }) => forkWith(Effect.fail("error"), state) + ) + ) +) + +const delayedCounter = Machine.makeWith()( + (input, previous) => + Machine.procedures.make(previous ?? input, { + identifier: `Counter(${input})` + }).pipe( + Machine.procedures.addPrivate()("IncrementBy", ({ request, state }) => + Effect.sync(() => { + const count = state + request.number + return [count, count] + })), + Machine.procedures.add()( + "DelayedIncrementBy", + ({ forkWith, request, sendAwait, state }) => + sendAwait(new IncrementBy({ number: request.number })).pipe( + Effect.delay(request.delay), + forkWith(state) + ) + ) + ) +) + +class Multiplier extends Context.Tag("Multiplier")() { + static Live = Layer.succeed(this, 2) +} + +const withContext = Machine.make( + (input: number, previous?: number) => + Effect.gen(function*() { + const multiplier = yield* Multiplier + return Machine.procedures.make(previous ?? input).pipe( + Machine.procedures.add()("Multiply", ({ state }) => + Effect.sync(() => { + const count = state * multiplier + return [count, count] + })) + ) + }) +) + +const timerLoop = Machine.make( + Effect.gen(function*() { + const { unsafeSend } = yield* Machine.MachineContext + + // queue initial message + yield* unsafeSend(new Increment()) + + return Machine.procedures.make(0).pipe( + Machine.procedures.addPrivate()( + "Increment", + (ctx) => + ctx.send(new Increment()).pipe( + Effect.delay(20), + ctx.forkOne("timer"), + Effect.as([ctx.state + 1, ctx.state + 1]) + ) + ) + ) + }) +) + +const deferReply = Machine.make( + Machine.procedures.make(0).pipe( + Machine.procedures.add()( + "Increment", + (ctx) => { + const count = ctx.state + 1 + return Deferred.succeed(ctx.deferred, count).pipe( + Effect.delay(10), + ctx.fork, + Effect.as([Machine.NoReply, count]) + ) + } + ) + ) +) + +describe("Machine", () => { + test("counter", () => + Effect.gen(function*() { + yield* Effect.sleep(500) // wait for DevTools + + const booted = yield* Machine.boot(counter, 0) + yield* Effect.sleep(10) + assert.strictEqual(yield* booted.get, 0) + assert.strictEqual(yield* booted.send(new Increment()), 1) + assert.strictEqual(yield* booted.send(new Increment()), 2) + assert.strictEqual(yield* booted.send(new IncrementBy({ number: 2 })), 4) + assert.strictEqual(yield* booted.send(new Decrement()), 3) + assert.strictEqual(yield* booted.send(new FailBackground()), undefined) + const cause = yield* booted.join.pipe(Effect.sandbox, Effect.flip) + const failure = Cause.failures(cause).pipe(Chunk.unsafeHead) + assert.deepStrictEqual(failure.cause, "error") + }).pipe(Effect.scoped, Machine.withTracingEnabled(true), Effect.provide(DevTools.layer()), Effect.runPromise)) + + test("init context", () => + Effect.gen(function*() { + const booted = yield* Machine.boot(withContext, 20) + assert.strictEqual(yield* booted.get, 20) + assert.strictEqual(yield* booted.send(new Multiply()), 40) + }).pipe( + Effect.scoped, + Effect.provide(Multiplier.Live), + Effect.runPromise + )) + + test("forkWithState", () => + Effect.gen(function*() { + const booted = yield* Machine.boot(delayedCounter, 2) + assert.strictEqual(yield* booted.get, 2) + assert.deepStrictEqual( + // @ts-expect-error + yield* booted.send(new IncrementBy({ number: 2 })).pipe(Effect.exit), + Exit.die("Request IncrementBy marked as internal") + ) + assert.strictEqual(yield* booted.send(new DelayedIncrementBy({ number: 2, delay: 10 })), undefined) + assert.strictEqual(yield* booted.get, 2) + yield* Effect.sleep(10) + assert.strictEqual(yield* booted.get, 4) + }).pipe(Effect.scoped, Effect.runPromise)) + + test("changes", () => + Effect.gen(function*() { + const booted = yield* Machine.boot(counter, 0) + const results: Array = [] + yield* booted.changes.pipe( + Stream.runForEach((i) => + Effect.sync(() => { + results.push(i) + }) + ), + Effect.fork + ) + yield* Effect.sleep(0) + assert.strictEqual(yield* booted.send(new Increment()), 1) + assert.strictEqual(yield* booted.send(new Increment()), 2) + assert.strictEqual(yield* booted.send(new Increment()), 3) + + assert.deepStrictEqual(results, [0, 1, 2, 3]) + }).pipe(Effect.scoped, Effect.runPromise)) + + test("unsafeSend initializer", () => + Effect.gen(function*() { + const actor = yield* Machine.boot(timerLoop) + const results = yield* actor.changes.pipe( + Stream.take(5), + Stream.runCollect + ) + assert.deepStrictEqual(Chunk.toReadonlyArray(results), [0, 1, 2, 3, 4]) + }).pipe(Effect.scoped, Effect.runPromise)) + + test("NoReply", () => + Effect.gen(function*() { + const actor = yield* Machine.boot(deferReply) + assert.strictEqual(yield* actor.send(new Increment()), 1) + }).pipe(Effect.scoped, Effect.runPromise)) +}) + +describe("SerializableMachine", () => { + test("counter", () => + Effect.gen(function*() { + const actor = yield* Machine.boot(counterSerializable, 10) + + assert.strictEqual(yield* actor.get, 10) + assert.strictEqual(yield* actor.send(new Increment()), 11) + assert.strictEqual(yield* actor.send(new Increment()), 12) + assert.deepStrictEqual(yield* actor.sendUnknown({ _tag: "Decrement" }), { + _tag: "Success", + value: 11 + }) + const snapshot = yield* Machine.snapshot(actor) + assert.deepStrictEqual(snapshot, [10, "11"]) + + const restored = yield* Machine.restore(counterSerializable, snapshot) + assert.strictEqual(yield* restored.get, 11) + }).pipe(Effect.scoped, Effect.runPromise)) +}) diff --git a/repos/effect/packages/experimental/test/PersistedCache.test.ts b/repos/effect/packages/experimental/test/PersistedCache.test.ts new file mode 100644 index 0000000..e1db8cc --- /dev/null +++ b/repos/effect/packages/experimental/test/PersistedCache.test.ts @@ -0,0 +1,65 @@ +import * as PersistedCache from "@effect/experimental/PersistedCache" +import * as Persistence from "@effect/experimental/Persistence" +import { KeyValueStore } from "@effect/platform" +import { assert, describe, it } from "@effect/vitest" +import { Effect, Exit, Layer, Option, PrimaryKey, Schema } from "effect" + +class User extends Schema.Class("User")({ + id: Schema.Number, + name: Schema.String +}) {} + +class TTLRequest extends Schema.TaggedRequest()("TTLRequest", { + failure: Schema.String, + success: User, + payload: { + id: Schema.Number + } +}) implements Persistence.Persistable { + [PrimaryKey.symbol]() { + return `TTLRequest:${this.id}` + } +} + +describe("PersistedCache", () => { + const testsuite = (storeId: "memory" | "kvs", layer: Layer.Layer) => + it.scoped(storeId, () => + Effect.gen(function*() { + const persistence = yield* Persistence.ResultPersistence + const store = yield* persistence.make({ storeId: "users" }) + let invocations = 0 + let cache = yield* PersistedCache.make({ + storeId: "users", + lookup: (req: TTLRequest) => + Effect.sync(() => { + invocations++ + return new User({ id: req.id, name: "John" }) + }), + timeToLive: (_req, _exit) => 5000 + }) + const user = yield* cache.get(new TTLRequest({ id: 1 })) + assert.deepStrictEqual(user, new User({ id: 1, name: "John" })) + assert.deepStrictEqual( + yield* store.get(new TTLRequest({ id: 1 })), + Option.some(Exit.succeed(new User({ id: 1, name: "John" }))) + ) + assert.strictEqual(invocations, 1) + assert.deepStrictEqual(yield* cache.get(new TTLRequest({ id: 1 })), new User({ id: 1, name: "John" })) + assert.strictEqual(invocations, 1) + + cache = yield* PersistedCache.make({ + storeId: "users", + lookup: (req: TTLRequest) => + Effect.sync(() => { + invocations++ + return new User({ id: req.id, name: "John" }) + }), + timeToLive: (_req, _exit) => 5000 + }) + assert.deepStrictEqual(yield* cache.get(new TTLRequest({ id: 1 })), new User({ id: 1, name: "John" })) + assert.strictEqual(invocations, 1) + }).pipe(Effect.provide(layer))) + + testsuite("memory", Persistence.layerResultMemory) + testsuite("kvs", Persistence.layerResultKeyValueStore.pipe(Layer.provide(KeyValueStore.layerMemory))) +}) diff --git a/repos/effect/packages/experimental/test/PersistedQueue.test.ts b/repos/effect/packages/experimental/test/PersistedQueue.test.ts new file mode 100644 index 0000000..e57dfdc --- /dev/null +++ b/repos/effect/packages/experimental/test/PersistedQueue.test.ts @@ -0,0 +1,120 @@ +import { PersistedQueue } from "@effect/experimental" +import * as RedisPersistedQueue from "@effect/experimental/PersistedQueue/Redis" +import { assert, it } from "@effect/vitest" +import { Effect, Fiber, Layer, Schema, TestClock, TestServices } from "effect" +import { RedisContainer } from "./utils/redis.js" + +const layerMemory = PersistedQueue.layer.pipe( + Layer.provide(PersistedQueue.layerStoreMemory) +) + +const layerRedis = PersistedQueue.layer.pipe( + Layer.provide(Layer.unwrapEffect(Effect.gen(function*() { + const container = yield* RedisContainer + return RedisPersistedQueue.layerStore({ + host: container.getHost(), + port: container.getMappedPort(6379) + }) + }))), + Layer.provide(RedisContainer.layer) +) +;([ + ["Memory", layerMemory], + ["Redis", layerRedis] +] as const).forEach(([name, layer]) => { + it.layer(layer, { timeout: "30 seconds" })(`PersistedQueue (${name})`, (it) => { + it.effect("offer + take", () => + Effect.gen(function*() { + const queue = yield* PersistedQueue.make({ + name: "test-queue-a", + schema: Item + }) + + yield* queue.offer({ n: 42n }) + yield* queue.take(Effect.fnUntraced(function*(value) { + assert.strictEqual(value.n, 42n) + })) + })) + + it.effect("interrupt", () => + Effect.gen(function*() { + const queue = yield* PersistedQueue.make({ + name: "test-queue-b", + schema: Item + }) + + yield* queue.offer({ n: 42n }) + + const latch = Effect.unsafeMakeLatch() + const fiber = yield* queue.take(Effect.fnUntraced(function*(_value) { + yield* latch.open + return yield* Effect.never + })).pipe(Effect.fork) + + const fiber2 = yield* queue.take((val) => Effect.succeed(val)).pipe(Effect.fork) + + yield* latch.await + + // allow some real time to pass to ensure the second take is really + // waiting + yield* TestClock.adjust(1000) + yield* Effect.sleep(1000).pipe( + TestServices.provideLive + ) + assert.isNull(fiber2.unsafePoll()) + + yield* Fiber.interrupt(fiber) + + yield* TestClock.adjust(1000) + + assert.strictEqual((yield* Fiber.join(fiber2)).n, 42n) + })) + + it.effect("failure", () => + Effect.gen(function*() { + const queue = yield* PersistedQueue.make({ + name: "test-queue-c", + schema: Item + }) + + yield* queue.offer({ n: 42n }) + + const error = yield* queue.take(() => Effect.fail("boom")).pipe(Effect.flip) + assert.strictEqual(error, "boom") + + const value = yield* queue.take((val, { attempts }) => { + assert.strictEqual(attempts, 1) + return Effect.succeed(val) + }) + assert.strictEqual(value.n, 42n) + })) + + it.effect("idempotent offer", () => + Effect.gen(function*() { + const queue = yield* PersistedQueue.make({ + name: "idempotent-offer", + schema: Item + }) + + yield* queue.offer({ n: 42n }, { id: "custom-id" }) + yield* queue.offer({ n: 42n }, { id: "custom-id" }) + yield* queue.take(Effect.fnUntraced(function*(value) { + assert.strictEqual(value.n, 42n) + })) + const fiber = yield* queue.take(Effect.fnUntraced(function*(value) { + assert.strictEqual(value.n, 42n) + })).pipe(Effect.fork) + + yield* TestClock.adjust(1000) + yield* Effect.sleep(1000).pipe( + TestServices.provideLive + ) + + assert.isNull(fiber.unsafePoll()) + })) + }) +}) + +const Item = Schema.Struct({ + n: Schema.BigInt +}) diff --git a/repos/effect/packages/experimental/test/RateLimiter.test.ts b/repos/effect/packages/experimental/test/RateLimiter.test.ts new file mode 100644 index 0000000..931075e --- /dev/null +++ b/repos/effect/packages/experimental/test/RateLimiter.test.ts @@ -0,0 +1,140 @@ +import { RateLimiter } from "@effect/experimental" +import { assert, describe, expect, it } from "@effect/vitest" +import { Effect } from "effect" +import * as Duration from "effect/Duration" +import * as TestClock from "effect/TestClock" + +describe("RateLimiter", () => { + describe("fixed-window", () => { + it.effect("onExceeded delay", () => + Effect.gen(function*() { + const limiter = yield* RateLimiter.make + const consume = limiter.consume({ + algorithm: "fixed-window", + onExceeded: "delay", + window: "1 minute", + limit: 5, + tokens: 1, + key: "a" + }) + yield* Effect.repeatN(consume, 3) // 1 + 3 + let result = yield* consume // 5 + expect(result.delay).toEqual(Duration.zero) + result = yield* consume // 6 + expect(result.delay).toEqual(Duration.minutes(1)) + + yield* Effect.repeatN(consume, 2) // 7,8,9 + result = yield* consume // 10 + expect(result.delay).toEqual(Duration.minutes(1)) + result = yield* consume // 11 + expect(result.delay).toEqual(Duration.minutes(2)) + + yield* TestClock.adjust(Duration.seconds(30)) + + result = yield* consume // 12 + expect(result.delay).toEqual(Duration.seconds(90)) + + yield* TestClock.adjust(Duration.seconds(45)) + + result = yield* consume // 13 + expect(result.delay).toEqual(Duration.seconds(45)) + }).pipe( + Effect.provide(RateLimiter.layerStoreMemory) + )) + + it.effect("onExceeded fail", () => + Effect.gen(function*() { + const limiter = yield* RateLimiter.make + const consume = limiter.consume({ + algorithm: "fixed-window", + onExceeded: "fail", + window: "1 minute", + limit: 5, + tokens: 1, + key: "a" + }) + yield* Effect.repeatN(consume, 3) + let result = yield* consume + expect(result.delay).toEqual(Duration.zero) + let error = yield* Effect.flip(consume) + assert(error.reason === "Exceeded") + expect(error.retryAfter).toEqual(Duration.minutes(1)) + expect(error.remaining).toEqual(0) + + yield* TestClock.adjust(Duration.seconds(30)) + + error = yield* Effect.flip(consume) + assert(error.reason === "Exceeded") + expect(error.retryAfter).toEqual(Duration.seconds(30)) + expect(error.remaining).toEqual(0) + + yield* TestClock.adjust(Duration.seconds(30)) + + result = yield* consume + expect(result.delay).toEqual(Duration.zero) + expect(result.remaining).toEqual(4) + }).pipe( + Effect.provide(RateLimiter.layerStoreMemory) + )) + }) + + describe("token-bucket", () => { + it.effect("onExceeded delay", () => + Effect.gen(function*() { + const limiter = yield* RateLimiter.make + const consume = limiter.consume({ + algorithm: "token-bucket", + onExceeded: "delay", + window: "1 minute", + limit: 5, + tokens: 1, + key: "a" + }) + const refillRate = Duration.unsafeDivide(Duration.minutes(1), 5) + yield* Effect.repeatN(consume, 3) // 1 + 3 + let result = yield* consume // 5 + expect(result.delay).toEqual(Duration.zero) + result = yield* consume // 6 + expect(result.delay).toEqual(refillRate) + result = yield* consume // 7 + expect(result.delay).toEqual(Duration.times(refillRate, 2)) + + yield* TestClock.adjust(Duration.minutes(1)) // 2 + + result = yield* consume // 3 + expect(result.delay).toEqual(Duration.zero) + expect(result.remaining).toEqual(2) + }).pipe( + Effect.provide(RateLimiter.layerStoreMemory) + )) + + it.effect("onExceeded fail", () => + Effect.gen(function*() { + const limiter = yield* RateLimiter.make + const consume = limiter.consume({ + algorithm: "token-bucket", + onExceeded: "fail", + window: "1 minute", + limit: 5, + tokens: 1, + key: "a" + }) + const refillRate = Duration.unsafeDivide(Duration.minutes(1), 5) + yield* Effect.repeatN(consume, 3) + let result = yield* consume + expect(result.delay).toEqual(Duration.zero) + const error = yield* Effect.flip(consume) + assert(error.reason === "Exceeded") + expect(error.retryAfter).toEqual(Duration.seconds(12)) + expect(error.remaining).toEqual(0) + + yield* TestClock.adjust(Duration.times(refillRate, 3)) + + result = yield* consume + expect(result.delay).toEqual(Duration.zero) + expect(result.remaining).toEqual(2) + }).pipe( + Effect.provide(RateLimiter.layerStoreMemory) + )) + }) +}) diff --git a/repos/effect/packages/experimental/test/RequestResolver.test.ts b/repos/effect/packages/experimental/test/RequestResolver.test.ts new file mode 100644 index 0000000..491f7e6 --- /dev/null +++ b/repos/effect/packages/experimental/test/RequestResolver.test.ts @@ -0,0 +1,146 @@ +import * as Persistence from "@effect/experimental/Persistence" +import * as RequestResolverX from "@effect/experimental/RequestResolver" +import { KeyValueStore } from "@effect/platform" +import { assert, describe, it } from "@effect/vitest" +import { Array, Effect, Exit, Layer, PrimaryKey, Request, RequestResolver, Schema, TestClock } from "effect" +import type { NonEmptyArray } from "effect/Array" + +class User extends Schema.Class("User")({ + id: Schema.Number, + name: Schema.String +}) {} + +class MyRequest extends Schema.TaggedRequest()("MyRequest", { + failure: Schema.String, + success: User, + payload: { + id: Schema.Number + } +}) { + [PrimaryKey.symbol]() { + return `MyRequest:${this.id}` + } +} + +class TTLRequest extends Schema.TaggedRequest()("TTLRequest", { + failure: Schema.String, + success: User, + payload: { + id: Schema.Number + } +}) { + [PrimaryKey.symbol]() { + return `TTLRequest:${this.id}` + } + // [TimeToLive.symbol](exit: Exit.Exit) { + // return Exit.isSuccess(exit) ? 5000 : 1 + // } +} + +describe("RequestResolver", () => { + describe("persisted", () => { + const testsuite = ( + storeId: "memory" | "kvs" | "lmdb", + layer: Layer.Layer + ) => + it.effect(storeId, () => + Effect.gen(function*() { + let count = 0 + const baseResolver = RequestResolver.makeBatched((reqs: NonEmptyArray) => { + count += reqs.length + return Effect.forEach(reqs, (req) => { + if (req.id === -1) return Request.fail(req, "not found") + return Request.succeed(req, new User({ id: req.id, name: "John" })) + }, { discard: true }) + }) + const persisted = yield* RequestResolverX.persisted(baseResolver, { + storeId, + timeToLive: (_req, exit) => Exit.isSuccess(exit) ? 5000 : 1 + }) + let users = yield* Effect.forEach( + Array.range(1, 5), + (id) => Effect.request(new MyRequest({ id }), persisted), + { + batching: true + } + ) + assert.strictEqual(count, 5) + assert.strictEqual(users.length, 5) + users = yield* Effect.forEach(Array.range(1, 5), (id) => Effect.request(new MyRequest({ id }), persisted), { + batching: true + }) + assert.strictEqual(count, 5) + assert.strictEqual(users.length, 5) + + // ttl + let results = yield* Effect.forEach( + Array.range(-1, 3), + (id) => Effect.exit(Effect.request(new TTLRequest({ id }), persisted)), + { + batching: true + } + ) + + assert.strictEqual(count, 10) + assert.strictEqual(results.length, 5) + assert(Exit.isFailure(results[0])) + assert(Exit.isSuccess(results[1])) + + results = yield* Effect.forEach( + Array.range(-1, 3), + (id) => Effect.exit(Effect.request(new TTLRequest({ id }), persisted)), + { + batching: true + } + ) + assert.strictEqual(count, 10) + assert.strictEqual(results.length, 5) + + yield* TestClock.adjust(1) + + results = yield* Effect.forEach( + Array.range(-1, 3), + (id) => Effect.exit(Effect.request(new TTLRequest({ id }), persisted)), + { + batching: true + } + ) + assert.strictEqual(count, 11) + assert.strictEqual(results.length, 5) + + yield* TestClock.adjust(5000) + + results = yield* Effect.forEach( + Array.range(-1, 3), + (id) => Effect.exit(Effect.request(new TTLRequest({ id }), persisted)), + { + batching: true + } + ) + assert.strictEqual(count, 16) + assert.strictEqual(results.length, 5) + + // clear + const persistence = yield* Persistence.ResultPersistence + const store = yield* persistence.make({ storeId }) + yield* store.clear + + users = yield* Effect.forEach(Array.range(1, 5), (id) => Effect.request(new MyRequest({ id }), persisted), { + batching: true + }) + assert.strictEqual(count, 21) + assert.strictEqual(users.length, 5) + }).pipe(Effect.scoped, Effect.provide(layer))) + + testsuite("memory", Persistence.layerResultMemory) + testsuite("kvs", Persistence.layerResultKeyValueStore.pipe(Layer.provide(KeyValueStore.layerMemory))) + // testsuite( + // "lmdb", + // Effect.gen(function*(_) { + // const fs = yield* _(FileSystem.FileSystem) + // const dir = yield* _(fs.makeTempDirectoryScoped()) + // return PersistenceLmdb.layerResult({ path: dir }) + // }).pipe(Layer.unwrapScoped, Layer.provide(NodeContext.layer)) + // ) + }) +}) diff --git a/repos/effect/packages/experimental/test/utils/extend.ts b/repos/effect/packages/experimental/test/utils/extend.ts new file mode 100644 index 0000000..24ed103 --- /dev/null +++ b/repos/effect/packages/experimental/test/utils/extend.ts @@ -0,0 +1,57 @@ +import * as V from "@effect/vitest" +import * as Effect from "effect/Effect" +import { pipe } from "effect/Function" + +export type API = V.API + +export const it: API = V.it + +export const effect = (() => { + const f = ( + name: string, + self: () => Effect.Effect, + timeout = 5_000 + ) => { + return it( + name, + () => + pipe( + Effect.suspend(self), + Effect.runPromise + ), + timeout + ) + } + return Object.assign(f, { + skip: ( + name: string, + self: () => Effect.Effect, + timeout = 5_000 + ) => { + return it.skip( + name, + () => + pipe( + Effect.suspend(self), + Effect.runPromise + ), + timeout + ) + }, + only: ( + name: string, + self: () => Effect.Effect, + timeout = 5_000 + ) => { + return it.only( + name, + () => + pipe( + Effect.suspend(self), + Effect.runPromise + ), + timeout + ) + } + }) +})() diff --git a/repos/effect/packages/experimental/test/utils/redis.ts b/repos/effect/packages/experimental/test/utils/redis.ts new file mode 100644 index 0000000..933d70a --- /dev/null +++ b/repos/effect/packages/experimental/test/utils/redis.ts @@ -0,0 +1,23 @@ +import type { StartedRedisContainer } from "@testcontainers/redis" +import * as Redis from "@testcontainers/redis" +import { Context, Data, Effect, Layer } from "effect" + +export class ContainerError extends Data.TaggedError("ContainerError")<{ + cause: unknown +}> {} + +export class RedisContainer extends Context.Tag("test/RedisContainer")< + RedisContainer, + StartedRedisContainer +>() { + static layer = Layer.scoped( + this, + Effect.acquireRelease( + Effect.tryPromise({ + try: () => new Redis.RedisContainer("redis").start(), + catch: (cause) => new ContainerError({ cause }) + }), + (container) => Effect.promise(() => container.stop()) + ) + ) +} diff --git a/repos/effect/packages/experimental/tsconfig.build.json b/repos/effect/packages/experimental/tsconfig.build.json new file mode 100644 index 0000000..472aa65 --- /dev/null +++ b/repos/effect/packages/experimental/tsconfig.build.json @@ -0,0 +1,13 @@ +{ + "extends": "./tsconfig.src.json", + "references": [ + { "path": "../effect/tsconfig.build.json" }, + { "path": "../platform/tsconfig.build.json" } + ], + "compilerOptions": { + "tsBuildInfoFile": ".tsbuildinfo/build.tsbuildinfo", + "outDir": "build/esm", + "declarationDir": "build/dts", + "stripInternal": true + } +} diff --git a/repos/effect/packages/experimental/tsconfig.examples.json b/repos/effect/packages/experimental/tsconfig.examples.json new file mode 100644 index 0000000..2c9ae59 --- /dev/null +++ b/repos/effect/packages/experimental/tsconfig.examples.json @@ -0,0 +1,13 @@ +{ + "extends": "../../tsconfig.base.json", + "include": ["examples"], + "references": [ + { "path": "tsconfig.build.json" }, + { "path": "../platform-node/tsconfig.build.json" } + ], + "compilerOptions": { + "tsBuildInfoFile": ".tsbuildinfo/examples.tsbuildinfo", + "rootDir": "examples", + "noEmit": true + } +} diff --git a/repos/effect/packages/experimental/tsconfig.json b/repos/effect/packages/experimental/tsconfig.json new file mode 100644 index 0000000..3edbf6b --- /dev/null +++ b/repos/effect/packages/experimental/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.base.json", + "include": [], + "references": [ + { "path": "tsconfig.src.json" }, + { "path": "tsconfig.test.json" }, + { "path": "tsconfig.examples.json" } + ] +} diff --git a/repos/effect/packages/experimental/tsconfig.src.json b/repos/effect/packages/experimental/tsconfig.src.json new file mode 100644 index 0000000..9c2b466 --- /dev/null +++ b/repos/effect/packages/experimental/tsconfig.src.json @@ -0,0 +1,13 @@ +{ + "extends": "../../tsconfig.base.json", + "include": ["src"], + "references": [ + { "path": "../effect/tsconfig.src.json" }, + { "path": "../platform/tsconfig.src.json" } + ], + "compilerOptions": { + "tsBuildInfoFile": ".tsbuildinfo/src.tsbuildinfo", + "rootDir": "src", + "outDir": "build/src" + } +} diff --git a/repos/effect/packages/experimental/tsconfig.test.json b/repos/effect/packages/experimental/tsconfig.test.json new file mode 100644 index 0000000..185e706 --- /dev/null +++ b/repos/effect/packages/experimental/tsconfig.test.json @@ -0,0 +1,13 @@ +{ + "extends": "../../tsconfig.base.json", + "include": ["test"], + "references": [ + { "path": "tsconfig.src.json" }, + { "path": "../vitest/tsconfig.src.json" } + ], + "compilerOptions": { + "tsBuildInfoFile": ".tsbuildinfo/test.tsbuildinfo", + "rootDir": "test", + "noEmit": true + } +} diff --git a/repos/effect/packages/experimental/vitest.config.ts b/repos/effect/packages/experimental/vitest.config.ts new file mode 100644 index 0000000..578d066 --- /dev/null +++ b/repos/effect/packages/experimental/vitest.config.ts @@ -0,0 +1,6 @@ +import { mergeConfig, type ViteUserConfig } from "vitest/config" +import shared from "../../vitest.shared.js" + +const config: ViteUserConfig = {} + +export default mergeConfig(shared, config) diff --git a/repos/effect/packages/opentelemetry/CHANGELOG.md b/repos/effect/packages/opentelemetry/CHANGELOG.md new file mode 100644 index 0000000..7465666 --- /dev/null +++ b/repos/effect/packages/opentelemetry/CHANGELOG.md @@ -0,0 +1,2564 @@ +# @effect/opentelemetry + +## 0.63.0 + +### Patch Changes + +- [#5780](https://github.com/Effect-TS/effect/pull/5780) [`0d32048`](https://github.com/Effect-TS/effect/commit/0d32048f9836e2b23a6ba3ec5f43f0a000bb92fb) Thanks @mikearnaldi! - Add logs to first propagated span, in the following case before this fix the log would not be added to the `p` span because `Effect.fn` adds a fake span for the purpose of adding a stack frame. + + ```ts + import { Effect } from "effect" + + const f = Effect.fn(function* () { + yield* Effect.logWarning("FooBar") + return yield* Effect.fail("Oops") + }) + + const p = f().pipe(Effect.withSpan("p")) + ``` + +- Updated dependencies [[`f7bb09b`](https://github.com/Effect-TS/effect/commit/f7bb09b022f195d1f2b3c23d49e74b011ec5d109), [`bd7552a`](https://github.com/Effect-TS/effect/commit/bd7552a19cc0ed575507ac6cc0879a57e24ebd31), [`ad1a7eb`](https://github.com/Effect-TS/effect/commit/ad1a7eb7f6bebaf91c80be2443ac0439226d0098), [`0d32048`](https://github.com/Effect-TS/effect/commit/0d32048f9836e2b23a6ba3ec5f43f0a000bb92fb), [`0d32048`](https://github.com/Effect-TS/effect/commit/0d32048f9836e2b23a6ba3ec5f43f0a000bb92fb)]: + - effect@3.21.0 + - @effect/platform@0.96.0 + +## 0.62.0 + +### Patch Changes + +- Updated dependencies [[`fc82e81`](https://github.com/Effect-TS/effect/commit/fc82e81448bd9136a37580139ce46a2c61b11b54), [`82996bc`](https://github.com/Effect-TS/effect/commit/82996bce8debffcb44feb98bb862cf2662bd56b7), [`4d97a61`](https://github.com/Effect-TS/effect/commit/4d97a61a15b9dd6a0eece65b8f0c035e16d42ada), [`f6b0960`](https://github.com/Effect-TS/effect/commit/f6b0960bf3184109920dfed16ee7dfd7d67bc0f2), [`8798a84`](https://github.com/Effect-TS/effect/commit/8798a843218e6c0c0d3a8eee83360880e370b4da)]: + - effect@3.20.0 + - @effect/platform@0.95.0 + +## 0.61.0 + +### Minor Changes + +- [#5927](https://github.com/Effect-TS/effect/pull/5927) [`f4972ed`](https://github.com/Effect-TS/effect/commit/f4972eda6c3179070d0167a30985b760afa0a9f9) Thanks @davidgoli! - Add protobuf protocol support for OTLP exporters + + This introduces an `OtlpSerialization` service for choosing between JSON and Protobuf encoding. + + **Breaking changes:** + - `Otlp.layer` now requires an `OtlpSerialization` layer to be provided for + the desired encoding format. + + **JSON encoding:** + + ```typescript + import { Layer } from "effect" + import { Otlp, OtlpSerialization } from "@effect/opentelemetry" + + // Option 1: Explicit JSON layer + const layer = Otlp.layerJson({ + baseUrl: "http://localhost:4318", + resource: { serviceName: "my-service" } + }) + + // Option 2: Use `layer` and provide OtlpSerialization JSON layer + const layer = Otlp.layer({ + baseUrl: "http://localhost:4318", + resource: { serviceName: "my-service" } + }).pipe(Layer.provide(OtlpSerialization.layerJson)) + ``` + + **Protobuf encoding:** + + ```typescript + import { Otlp } from "@effect/opentelemetry" + + // Simply use layerProtobuf for protobuf encoding + const layer = Otlp.layerProtobuf({ + baseUrl: "http://localhost:4318", + resource: { serviceName: "my-service" } + }) + ``` + +- [#5952](https://github.com/Effect-TS/effect/pull/5952) [`4725a7e`](https://github.com/Effect-TS/effect/commit/4725a7eceac8b8b66ee55bbd975e1adab67df271) Thanks @clayroach! - Make @opentelemetry/sdk-trace-node and @opentelemetry/sdk-trace-web required peer dependencies instead of optional. This fixes module resolution errors when importing from the main entry point. + +### Patch Changes + +- [#5929](https://github.com/Effect-TS/effect/pull/5929) [`abdab5c`](https://github.com/Effect-TS/effect/commit/abdab5cc4ede8272799f86caa6557a8a9674ab37) Thanks @schickling! - Fix `Span.addEvent` to correctly handle the 2-argument overload with attributes. + + Previously, calling `span.addEvent("name", { foo: "bar" })` would throw `TypeError: {} is not iterable` because the implementation incorrectly treated the attributes object as a `TimeInput`. The fix adds proper runtime type discrimination to distinguish between `TimeInput` (number, Date, or HrTime tuple) and `Attributes` (plain object). + +- Updated dependencies [[`7e925ea`](https://github.com/Effect-TS/effect/commit/7e925eae4a9db556bcbf7e8b6a762ccf8588aa3b), [`118e7a4`](https://github.com/Effect-TS/effect/commit/118e7a4af5b86f6d707a40d3b03157b6bf5827e7), [`d7e75d6`](https://github.com/Effect-TS/effect/commit/d7e75d6d15294bbcd7ac49a0e9005848379ea86f), [`4860d1e`](https://github.com/Effect-TS/effect/commit/4860d1e09b436061ea4aeca07605a669793560fc)]: + - effect@3.19.15 + - @effect/platform@0.94.2 + +## 0.60.0 + +### Patch Changes + +- Updated dependencies [[`77eeb86`](https://github.com/Effect-TS/effect/commit/77eeb86ddf208e51ec25932af83d52d3b4700371), [`ff7053f`](https://github.com/Effect-TS/effect/commit/ff7053f6d8508567b6145239f97aacc5773b0c53), [`287c32c`](https://github.com/Effect-TS/effect/commit/287c32c9f10da8e96f2b9ef8424316189d9ad4b3)]: + - effect@3.19.13 + - @effect/platform@0.94.0 + +## 0.59.3 + +### Patch Changes + +- [#5890](https://github.com/Effect-TS/effect/pull/5890) [`03355c1`](https://github.com/Effect-TS/effect/commit/03355c1470705168e79ca62f57a30f236cc8033d) Thanks @schickling! - Fix `Tracer.currentOtelSpan` to work with OTLP module + + `currentOtelSpan` now works with both the official OpenTelemetry SDK and the lightweight OTLP module. When using OTLP, it returns a wrapper that conforms to the OpenTelemetry Span interface. + + Closes #5889 + +- Updated dependencies [[`a6dfca9`](https://github.com/Effect-TS/effect/commit/a6dfca93b676eeffe4db64945b01e2004b395cb8), [`a0a84d8`](https://github.com/Effect-TS/effect/commit/a0a84d8df05d18023ffcb1f60af91d14c2b8db57)]: + - effect@3.19.12 + - @effect/platform@0.93.8 + +## 0.59.2 + +### Patch Changes + +- [#5863](https://github.com/Effect-TS/effect/pull/5863) [`5be3b6a`](https://github.com/Effect-TS/effect/commit/5be3b6add400dcf281475dbefbfede5b69c63940) Thanks @mikearnaldi! - Widen @opentelemetry/sdk-logs peer dependency range + +- Updated dependencies [[`3f9bbfe`](https://github.com/Effect-TS/effect/commit/3f9bbfe9ef78303ecc6817b68ec9671f4d42d249)]: + - effect@3.19.9 + +## 0.59.1 + +### Patch Changes + +- [#5735](https://github.com/Effect-TS/effect/pull/5735) [`973c90a`](https://github.com/Effect-TS/effect/commit/973c90af48a43a48070a56128f8ce0d0b98afbab) Thanks @tim-smart! - convert bigints to string for opentelemetry attributes + +## 0.59.0 + +### Patch Changes + +- Updated dependencies [[`3c15d5f`](https://github.com/Effect-TS/effect/commit/3c15d5f99fb8d8470a00c5a33d9ba3cac89dfe4c), [`3863fa8`](https://github.com/Effect-TS/effect/commit/3863fa89f61e63e5529fd961e37333bddf7db64a), [`2a03c76`](https://github.com/Effect-TS/effect/commit/2a03c76c2781ca7e9e228e838eab2eb0d0795b1d), [`24a1685`](https://github.com/Effect-TS/effect/commit/24a1685c70a9ed157468650f95a5c3da3f2c2433)]: + - effect@3.19.0 + - @effect/platform@0.93.0 + +## 0.58.0 + +### Minor Changes + +- [#5302](https://github.com/Effect-TS/effect/pull/5302) [`70fe803`](https://github.com/Effect-TS/effect/commit/70fe803469db3355ffbf8359b52c351f1c2dc137) Thanks @mikearnaldi! - Automatically set otel parent when present as external span + +### Patch Changes + +- Updated dependencies [[`1c6ab74`](https://github.com/Effect-TS/effect/commit/1c6ab74b314b2b6df8bb1b1a0cb9527ceda0e3fa), [`70fe803`](https://github.com/Effect-TS/effect/commit/70fe803469db3355ffbf8359b52c351f1c2dc137), [`c296e32`](https://github.com/Effect-TS/effect/commit/c296e32554143b84ae8987046984e1cf1852417c), [`a098ddf`](https://github.com/Effect-TS/effect/commit/a098ddfc551f5aa0a7c36f9b4928372a64d4d9f2)]: + - effect@3.18.0 + - @effect/platform@0.92.0 + +## 0.57.0 + +### Patch Changes + +- Updated dependencies [[`d4d86a8`](https://github.com/Effect-TS/effect/commit/d4d86a81f02b94e09fce8004ce2c5369c505ca5a)]: + - @effect/platform@0.91.0 + +## 0.56.6 + +### Patch Changes + +- [#5485](https://github.com/Effect-TS/effect/pull/5485) [`f8b8d3d`](https://github.com/Effect-TS/effect/commit/f8b8d3db1a3a53069a134eaad9f4fb42117193c7) Thanks @tim-smart! - fix traceFlags propagation when set to 0 + +- Updated dependencies [[`333be04`](https://github.com/Effect-TS/effect/commit/333be046b50e8300f5cb70b871448e0628b7b37c)]: + - @effect/platform@0.90.8 + +## 0.56.5 + +### Patch Changes + +- [#5444](https://github.com/Effect-TS/effect/pull/5444) [`8f8c401`](https://github.com/Effect-TS/effect/commit/8f8c4011f4b90c540c08efd2e850fe7b265e098c) Thanks @mikearnaldi! - Propagate spanId and traceId to otel logs + +## 0.56.4 + +### Patch Changes + +- [#5413](https://github.com/Effect-TS/effect/pull/5413) [`2965aa7`](https://github.com/Effect-TS/effect/commit/2965aa7cff54d7155f291f97b0154486176e81be) Thanks @IMax153! - Remove @opentelemetry/semantic-conventions from Effect-native OTLP modules + +- Updated dependencies [[`84bc300`](https://github.com/Effect-TS/effect/commit/84bc3003b42ad51210e9e1248efd04c5d0e3dd1e), [`fef9771`](https://github.com/Effect-TS/effect/commit/fef9771eab24af6415be946df0c9f64eba01cef7)]: + - effect@3.17.8 + - @effect/platform@0.90.5 + +## 0.56.3 + +### Patch Changes + +- [#5397](https://github.com/Effect-TS/effect/pull/5397) [`0e46e24`](https://github.com/Effect-TS/effect/commit/0e46e24c24e9edb8bf2e29835a94013e9c34d034) Thanks @IMax153! - Avoid issues with ESM builds by removing dependency on `@opentelemetry/semantic-conventions` + +- Updated dependencies [[`8c7bb52`](https://github.com/Effect-TS/effect/commit/8c7bb52dc78850be72566decba6222870e3733d0), [`0e46e24`](https://github.com/Effect-TS/effect/commit/0e46e24c24e9edb8bf2e29835a94013e9c34d034)]: + - @effect/platform@0.90.4 + +## 0.56.2 + +### Patch Changes + +- [#5380](https://github.com/Effect-TS/effect/pull/5380) [`1689874`](https://github.com/Effect-TS/effect/commit/168987408de9270b15802e7d6cc0ec42a57452d5) Thanks @tim-smart! - temporarily disable otlp exporter if endpoint can't be reached + +## 0.56.1 + +### Patch Changes + +- [#5334](https://github.com/Effect-TS/effect/pull/5334) [`6df2620`](https://github.com/Effect-TS/effect/commit/6df262086342cb215877863e85b0562e2780a2d5) Thanks @johtso! - improve baseUrl handling in Otlp constructor + +## 0.56.0 + +### Patch Changes + +- [#5257](https://github.com/Effect-TS/effect/pull/5257) [`d070f2e`](https://github.com/Effect-TS/effect/commit/d070f2e7d51da490ed64ce06652f4ae119fae331) Thanks @jrmdayn! - use URL constructor for joining url's in Otlp.layer + +- Updated dependencies [[`7813640`](https://github.com/Effect-TS/effect/commit/7813640279d9e3a3e7fc0a29bfb5c6d5fb3c270f)]: + - @effect/platform@0.90.0 + +## 0.55.0 + +### Patch Changes + +- Updated dependencies [[`40c3c87`](https://github.com/Effect-TS/effect/commit/40c3c875f724264312b43002859c82bed9ad0df9), [`ed2c74a`](https://github.com/Effect-TS/effect/commit/ed2c74ae8fa4ea0dd06ea84a3e58cd32e6916104), [`073a1b8`](https://github.com/Effect-TS/effect/commit/073a1b8be5dbfa87454393ee7346f5bc36a4fd63), [`f382e99`](https://github.com/Effect-TS/effect/commit/f382e99e409838a879246250fc3994b9bf5b3c2c), [`e8c7ba5`](https://github.com/Effect-TS/effect/commit/e8c7ba5fd3eb0c3ae3039fc24c09d69391987989), [`7e10415`](https://github.com/Effect-TS/effect/commit/7e1041599ade25103428703f5d2dfd7378a09636), [`e9bdece`](https://github.com/Effect-TS/effect/commit/e9bdececdc24f60a246be5055eca71a0d49ea7f2), [`8d95eb0`](https://github.com/Effect-TS/effect/commit/8d95eb0356b1d1736204836c275d201a547d208d)]: + - effect@3.17.0 + - @effect/platform@0.89.0 + +## 0.54.1 + +### Patch Changes + +- [#5192](https://github.com/Effect-TS/effect/pull/5192) [`17a5ea8`](https://github.com/Effect-TS/effect/commit/17a5ea8fa29785fe6e4c9480f2a2e9c8c59f3f38) Thanks @nikelborm! - Updated deprecated OTel Resource attributes names and values. + + Many of the attributes have undergone the process of deprecation not once, but twice. Most of the constants holding attribute names have been renamed. These are minor changes. + + Additionally, there were numerous changes to the attribute keys themselves. These changes can be considered major. + + In the `@opentelemetry/semantic-conventions` package, new attributes having ongoing discussion about them are going through a process called incubation, until a consensus about their necessity and form is reached. Otel team [recommends](https://github.com/open-telemetry/opentelemetry-js/blob/main/semantic-conventions/README.md#unstable-semconv) devs to copy them directly into their code. Luckily, it's not necessary because all of the new attribute names and values came out of this process (some of them were changed again) and are now considered stable. + + ## Reasoning for minor version bump + + | Package | Major attribute changes | Major value changes | + | -------------------------- | ----------------------------------------------------------------------------- | --------------------------------- | + | Clickhouse client | `db.system` -> `db.system.name`
`db.name` -> `db.namespace` | | + | MsSQL client | `db.system` -> `db.system.name`
`db.name` -> `db.namespace` | `mssql` -> `microsoft.sql_server` | + | MySQL client | `db.system` -> `db.system.name`
`db.name` -> `db.namespace` | | + | Pg client | `db.system` -> `db.system.name`
`db.name` -> `db.namespace` | | + | Bun SQLite client | `db.system` -> `db.system.name` | | + | Node SQLite client | `db.system` -> `db.system.name` | | + | React.Native SQLite client | `db.system` -> `db.system.name` | | + | Wasm SQLite client | `db.system` -> `db.system.name` | | + | SQLite Do client | `db.system` -> `db.system.name` | | + | LibSQL client | `db.system` -> `db.system.name` | | + | D1 client | `db.system` -> `db.system.name` | | + | Kysely client | `db.statement` -> `db.query.text` | | + | @effect/sql | `db.statement` -> `db.query.text`
`db.operation` -> `db.operation.name` | | + +- Updated dependencies [[`f5dfabf`](https://github.com/Effect-TS/effect/commit/f5dfabf51ba481a4468c1509c537314978ef6cec), [`17a5ea8`](https://github.com/Effect-TS/effect/commit/17a5ea8fa29785fe6e4c9480f2a2e9c8c59f3f38), [`d25f22b`](https://github.com/Effect-TS/effect/commit/d25f22be7598abe977caf6cdac3b0dd78b438c48)]: + - effect@3.16.14 + - @effect/platform@0.88.1 + +## 0.54.0 + +### Patch Changes + +- Updated dependencies [[`27206d7`](https://github.com/Effect-TS/effect/commit/27206d7f0558d7fe28de57bf54f1d0cc83acc92e), [`dbabf5e`](https://github.com/Effect-TS/effect/commit/dbabf5e76fa63b050d2b6c466713c7dc59f07d3c)]: + - @effect/platform@0.88.0 + +## 0.53.14 + +### Patch Changes + +- Updated dependencies [[`c1c05a8`](https://github.com/Effect-TS/effect/commit/c1c05a8242fb5df7445b4a12387a60eac7726eb7), [`81fe4a2`](https://github.com/Effect-TS/effect/commit/81fe4a2c81d5e30e180a60e68c52016a27b350db)]: + - effect@3.16.13 + - @effect/platform@0.87.13 + +## 0.53.13 + +### Patch Changes + +- Updated dependencies [[`32ba77a`](https://github.com/Effect-TS/effect/commit/32ba77ae304d2161362a73e8b61965332626cf2d), [`d5e25b2`](https://github.com/Effect-TS/effect/commit/d5e25b237f05670ee42b386cb40b2cb448fc11d7)]: + - @effect/platform@0.87.12 + +## 0.53.12 + +### Patch Changes + +- Updated dependencies [[`001392b`](https://github.com/Effect-TS/effect/commit/001392ba8bfcad101bb034348a7415012fb12f72), [`7bfb099`](https://github.com/Effect-TS/effect/commit/7bfb099cb5528511b8d63045c4fbb4dc9cb18528)]: + - @effect/platform@0.87.11 + +## 0.53.11 + +### Patch Changes + +- Updated dependencies [[`678318d`](https://github.com/Effect-TS/effect/commit/678318d2e88233156b006acda56c2d138ee3ffa0), [`678318d`](https://github.com/Effect-TS/effect/commit/678318d2e88233156b006acda56c2d138ee3ffa0)]: + - @effect/platform@0.87.10 + +## 0.53.10 + +### Patch Changes + +- Updated dependencies [[`54514a2`](https://github.com/Effect-TS/effect/commit/54514a2f53166de27ad7e756dbf12194691fd4af)]: + - @effect/platform@0.87.9 + +## 0.53.9 + +### Patch Changes + +- Updated dependencies [[`4ce4f82`](https://github.com/Effect-TS/effect/commit/4ce4f824f6fdef492be1d35c05a490ffce518c89)]: + - @effect/platform@0.87.8 + +## 0.53.8 + +### Patch Changes + +- [#5161](https://github.com/Effect-TS/effect/pull/5161) [`03f4e8c`](https://github.com/Effect-TS/effect/commit/03f4e8cdb25f43e5ecddf9ba3627105d7e957185) Thanks @tim-smart! - load otel attributes from Config in Otlp modules + +## 0.53.7 + +### Patch Changes + +- Updated dependencies [[`a9b617f`](https://github.com/Effect-TS/effect/commit/a9b617f125171ed76cd79ab46d7a924daf3b0e70), [`7e26e86`](https://github.com/Effect-TS/effect/commit/7e26e86524abcc93713d6ad7eee486638c98f7c2)]: + - @effect/platform@0.87.7 + +## 0.53.6 + +### Patch Changes + +- Updated dependencies [[`905da99`](https://github.com/Effect-TS/effect/commit/905da996aad665057b4ca6dba1a4af44fb8835bd)]: + - effect@3.16.12 + - @effect/platform@0.87.6 + +## 0.53.5 + +### Patch Changes + +- Updated dependencies [[`2fd8676`](https://github.com/Effect-TS/effect/commit/2fd8676c803cd40000dfc3231f5daecaa0e0ebd2)]: + - @effect/platform@0.87.5 + +## 0.53.4 + +### Patch Changes + +- Updated dependencies [[`e82a4fd`](https://github.com/Effect-TS/effect/commit/e82a4fd60f6528d08cef1a4aba0abe0d3ba741ad)]: + - @effect/platform@0.87.4 + +## 0.53.3 + +### Patch Changes + +- Updated dependencies [[`1b6e396`](https://github.com/Effect-TS/effect/commit/1b6e396d699f3cbbc56b68f99055cf746529bb9e)]: + - @effect/platform@0.87.3 + +## 0.53.2 + +### Patch Changes + +- Updated dependencies [[`4fea68c`](https://github.com/Effect-TS/effect/commit/4fea68ca7a25a3c39a1ab68b3885534513ab0c81), [`b927954`](https://github.com/Effect-TS/effect/commit/b9279543cf5688dd8a577af80456959c615217d0), [`99590a6`](https://github.com/Effect-TS/effect/commit/99590a6ca9128eb1ede265b6670b655311995614), [`6c3e24c`](https://github.com/Effect-TS/effect/commit/6c3e24c2308f7d4a29b8f4270ab81bca22ac6bb4)]: + - @effect/platform@0.87.2 + - effect@3.16.11 + +## 0.53.1 + +### Patch Changes + +- [#5107](https://github.com/Effect-TS/effect/pull/5107) [`c1a9c9d`](https://github.com/Effect-TS/effect/commit/c1a9c9d0c32ea25e890bf0e3aea23fa8d05b4d4f) Thanks @tim-smart! - add option to exclude log spans from OtlpLogger + +- Updated dependencies [[`faad30e`](https://github.com/Effect-TS/effect/commit/faad30ec8742916be59f9db642d0fc98225b636c)]: + - effect@3.16.10 + - @effect/platform@0.87.1 + +## 0.53.0 + +### Patch Changes + +- Updated dependencies [[`b5bac9a`](https://github.com/Effect-TS/effect/commit/b5bac9ac2913fcd11b02322624f03b544eef53ba)]: + - @effect/platform@0.87.0 + +## 0.52.0 + +### Patch Changes + +- Updated dependencies [[`5137c70`](https://github.com/Effect-TS/effect/commit/5137c703461d8d3b363c112140a6e7f798241d07), [`c23d25c`](https://github.com/Effect-TS/effect/commit/c23d25c3e7c541f1f63b28484d8c461d86c67e99), [`5137c70`](https://github.com/Effect-TS/effect/commit/5137c703461d8d3b363c112140a6e7f798241d07), [`5137c70`](https://github.com/Effect-TS/effect/commit/5137c703461d8d3b363c112140a6e7f798241d07)]: + - effect@3.16.9 + - @effect/platform@0.86.0 + +## 0.51.2 + +### Patch Changes + +- Updated dependencies [[`914a191`](https://github.com/Effect-TS/effect/commit/914a191e7cb6341a3d0e965bccd27c336cf22e44)]: + - @effect/platform@0.85.2 + +## 0.51.1 + +### Patch Changes + +- Updated dependencies [[`8cb98d5`](https://github.com/Effect-TS/effect/commit/8cb98d53e68330228287ce2a2e0d8a4c86bcab3b), [`db2dd3c`](https://github.com/Effect-TS/effect/commit/db2dd3c3a8a77d791eae19e66153527e1cde4e6e)]: + - effect@3.16.8 + - @effect/platform@0.85.1 + +## 0.51.0 + +### Patch Changes + +- Updated dependencies [[`93687dd`](https://github.com/Effect-TS/effect/commit/93687ddbb25ce3b324cd2b83d2ccff225e97307e), [`93687dd`](https://github.com/Effect-TS/effect/commit/93687ddbb25ce3b324cd2b83d2ccff225e97307e), [`93687dd`](https://github.com/Effect-TS/effect/commit/93687ddbb25ce3b324cd2b83d2ccff225e97307e)]: + - @effect/platform@0.85.0 + +## 0.50.11 + +### Patch Changes + +- Updated dependencies [[`1bb0d8a`](https://github.com/Effect-TS/effect/commit/1bb0d8ab96782e99434356266b38251554ea0294)]: + - effect@3.16.7 + - @effect/platform@0.84.11 + +## 0.50.10 + +### Patch Changes + +- Updated dependencies [[`a5f7595`](https://github.com/Effect-TS/effect/commit/a5f75956ef9a15a83c416517ef493f0ee2f5ee8a), [`a02470c`](https://github.com/Effect-TS/effect/commit/a02470c75579e91525a25adb3f21b3650d042fdd), [`bf369b2`](https://github.com/Effect-TS/effect/commit/bf369b2902a0e0b195d957c18b9efd180942cf8b), [`f891d45`](https://github.com/Effect-TS/effect/commit/f891d45adffdafd3f94a2eca23faa354e3a409a8)]: + - effect@3.16.6 + - @effect/platform@0.84.10 + +## 0.50.9 + +### Patch Changes + +- Updated dependencies [[`bf418ef`](https://github.com/Effect-TS/effect/commit/bf418ef14a0f2ec965535793d5cea8fa8ba177ac)]: + - effect@3.16.5 + - @effect/platform@0.84.9 + +## 0.50.8 + +### Patch Changes + +- Updated dependencies [[`8b9db77`](https://github.com/Effect-TS/effect/commit/8b9db7742846af0f58fd8e8b7acb7f4f5ff487ec)]: + - @effect/platform@0.84.8 + +## 0.50.7 + +### Patch Changes + +- Updated dependencies [[`74ab9a0`](https://github.com/Effect-TS/effect/commit/74ab9a0a9e16d6e019369d256e1e24175c8bc3f3), [`770008e`](https://github.com/Effect-TS/effect/commit/770008eca3aad2899a2ed951236e575793294b28)]: + - effect@3.16.4 + - @effect/platform@0.84.7 + +## 0.50.6 + +### Patch Changes + +- Updated dependencies [[`ceea77a`](https://github.com/Effect-TS/effect/commit/ceea77a13055f145520f763e3fce5b8ff15d728f)]: + - @effect/platform@0.84.6 + +## 0.50.5 + +### Patch Changes + +- Updated dependencies [[`ec52c6a`](https://github.com/Effect-TS/effect/commit/ec52c6a2211e76972462b15b9d5a9d6d56761b7a)]: + - @effect/platform@0.84.5 + +## 0.50.4 + +### Patch Changes + +- Updated dependencies [[`87722fc`](https://github.com/Effect-TS/effect/commit/87722fce693a9b49284bbddbf82d30714c688261), [`36217ee`](https://github.com/Effect-TS/effect/commit/36217eeb1337edd9ac3f9a635b80a6385d22ae8f)]: + - effect@3.16.3 + - @effect/platform@0.84.4 + +## 0.50.3 + +### Patch Changes + +- Updated dependencies [[`ab7684f`](https://github.com/Effect-TS/effect/commit/ab7684f1c2a0671bf091f255d220e3a4cc7f528e)]: + - @effect/platform@0.84.3 + +## 0.50.2 + +### Patch Changes + +- Updated dependencies [[`0ddf148`](https://github.com/Effect-TS/effect/commit/0ddf148a247aa87af043d276b8453a714a400897)]: + - effect@3.16.2 + - @effect/platform@0.84.2 + +## 0.50.1 + +### Patch Changes + +- Updated dependencies [[`71174d0`](https://github.com/Effect-TS/effect/commit/71174d09691314a9b6b66189e456fd21e3eb6543), [`d615e6e`](https://github.com/Effect-TS/effect/commit/d615e6e5b944f6fd5e627e31752c7ca7e4e1c17d)]: + - @effect/platform@0.84.1 + - effect@3.16.1 + +## 0.50.0 + +### Patch Changes + +- Updated dependencies [[`ee0bd5d`](https://github.com/Effect-TS/effect/commit/ee0bd5d24864752c54cb359f67a67dd903971ec4), [`5189800`](https://github.com/Effect-TS/effect/commit/51898004e11766b8cf6d95e960b636f6d5db79ec), [`58bfeaa`](https://github.com/Effect-TS/effect/commit/58bfeaa64ded8c88f772b184311c0c0dbac10960), [`194d748`](https://github.com/Effect-TS/effect/commit/194d7486943f56f3267ef415395ac220a4b3e634), [`918c9ea`](https://github.com/Effect-TS/effect/commit/918c9ea1a57facb154f0fb26792021f337054dee), [`9198e6f`](https://github.com/Effect-TS/effect/commit/9198e6fcc1a3ff4fefb3363004de558d8de01f40), [`2a370bf`](https://github.com/Effect-TS/effect/commit/2a370bf625fdeede5659721468eb0d527e403279), [`58ccb91`](https://github.com/Effect-TS/effect/commit/58ccb91328c8df5d49808b673738bc09df355201), [`fd47834`](https://github.com/Effect-TS/effect/commit/fd478348203fa89462b0a1d067ce4de034353df4)]: + - effect@3.16.0 + - @effect/platform@0.84.0 + +## 0.49.0 + +### Patch Changes + +- Updated dependencies [[`5522520`](https://github.com/Effect-TS/effect/commit/55225206ab9af0ad60b1c0654690a8a096d625cd), [`cc5bb2b`](https://github.com/Effect-TS/effect/commit/cc5bb2b918a9450a975f702dabcea891bda382cb)]: + - @effect/platform@0.83.0 + - effect@3.15.5 + +## 0.48.8 + +### Patch Changes + +- Updated dependencies [[`0617b9d`](https://github.com/Effect-TS/effect/commit/0617b9dc365f1963b36949ad7f9023ab6eb94524)]: + - @effect/platform@0.82.8 + +## 0.48.7 + +### Patch Changes + +- Updated dependencies [[`f570554`](https://github.com/Effect-TS/effect/commit/f57055459524587b041340577dad85476bb35f81), [`78047e8`](https://github.com/Effect-TS/effect/commit/78047e8dfc8005b66f87afe50bb95981fea51561), [`c20b95a`](https://github.com/Effect-TS/effect/commit/c20b95a99ffe452b4774c844d397a905f713b6d6), [`94ada43`](https://github.com/Effect-TS/effect/commit/94ada430928d5685bdbef513e87562c20774a3a2)]: + - effect@3.15.4 + - @effect/platform@0.82.7 + +## 0.48.6 + +### Patch Changes + +- Updated dependencies [[`618903b`](https://github.com/Effect-TS/effect/commit/618903ba9ae96e2bfe6ee31f61c4359b915f2a36)]: + - @effect/platform@0.82.6 + +## 0.48.5 + +### Patch Changes + +- Updated dependencies [[`7764a07`](https://github.com/Effect-TS/effect/commit/7764a07d960c60df81f14e1dc949518f4bbe494a), [`4577f54`](https://github.com/Effect-TS/effect/commit/4577f548d67273e576cdde423bdd34a4b910766a), [`30a0d9c`](https://github.com/Effect-TS/effect/commit/30a0d9cb51c84290d51b1361d72ff5cee33c13c7)]: + - @effect/platform@0.82.5 + - effect@3.15.3 + +## 0.48.4 + +### Patch Changes + +- Updated dependencies [[`d45e8a8`](https://github.com/Effect-TS/effect/commit/d45e8a8ac8227192f504e39e6d04fdcf4fb1d225), [`d13b68e`](https://github.com/Effect-TS/effect/commit/d13b68e3a9456d0bfee9bca8273a7b44a9c69087)]: + - @effect/platform@0.82.4 + +## 0.48.3 + +### Patch Changes + +- Updated dependencies [[`b8722b8`](https://github.com/Effect-TS/effect/commit/b8722b817e2306fe8c8245f3f9e32d85b824b961), [`a328f4b`](https://github.com/Effect-TS/effect/commit/a328f4b4fe717dd53e5b04a30f387433c32f7328)]: + - effect@3.15.2 + - @effect/platform@0.82.3 + +## 0.48.2 + +### Patch Changes + +- Updated dependencies [[`739a3d4`](https://github.com/Effect-TS/effect/commit/739a3d4a4565915fe2e690003f4f9085cb4422fc)]: + - @effect/platform@0.82.2 + +## 0.48.1 + +### Patch Changes + +- Updated dependencies [[`787ce70`](https://github.com/Effect-TS/effect/commit/787ce7042e35b657963473c6efe47752868cd811), [`1269641`](https://github.com/Effect-TS/effect/commit/1269641a99ae43069f7648ff79ffe8729b54b348), [`1269641`](https://github.com/Effect-TS/effect/commit/1269641a99ae43069f7648ff79ffe8729b54b348)]: + - effect@3.15.1 + - @effect/platform@0.82.1 + +## 0.48.0 + +### Patch Changes + +- Updated dependencies [[`c654595`](https://github.com/Effect-TS/effect/commit/c65459587b51da140b78098e81fdbfece65d53e2), [`d9f5dea`](https://github.com/Effect-TS/effect/commit/d9f5deae0f02f5de2b9fcb1cca8b142ba4bc2bba), [`49aa723`](https://github.com/Effect-TS/effect/commit/49aa7236a15e13f818c86edbca08c4af67c8dfaf), [`74c14d0`](https://github.com/Effect-TS/effect/commit/74c14d01d0cb48cf517a1b6e29a373a96ed0ff5b), [`e4f49b6`](https://github.com/Effect-TS/effect/commit/e4f49b66857e01b74ab6a9a0bc7132f44cd04cbb), [`6f02224`](https://github.com/Effect-TS/effect/commit/6f02224b3fc46a682ad2defb1a260841956c6780), [`1dcfd41`](https://github.com/Effect-TS/effect/commit/1dcfd41ff96abd706901293a00c1893cb29dd8fd), [`b21ab16`](https://github.com/Effect-TS/effect/commit/b21ab16b6f773e7ec4369db4e752c35e719f7870), [`fcf1822`](https://github.com/Effect-TS/effect/commit/fcf1822f98fcda60351d64e9d2c2c13563d7e6db), [`0061dd1`](https://github.com/Effect-TS/effect/commit/0061dd140740165e91569a684cce27a77b23229e), [`8421e6e`](https://github.com/Effect-TS/effect/commit/8421e6e49332bca8f96f482dfd48680e238b3a89), [`a9b3fb7`](https://github.com/Effect-TS/effect/commit/a9b3fb78abcfdb525318a956fd02fcadeb56143e), [`fa10f56`](https://github.com/Effect-TS/effect/commit/fa10f56b96bd9af070ba99ebc3279aa93954261e)]: + - effect@3.15.0 + - @effect/platform@0.82.0 + +## 0.47.1 + +### Patch Changes + +- Updated dependencies [[`24a9ebb`](https://github.com/Effect-TS/effect/commit/24a9ebbb5af598f0bfd6ecc45307e528043fe011)]: + - effect@3.14.22 + - @effect/platform@0.81.1 + +## 0.47.0 + +### Patch Changes + +- Updated dependencies [[`672920f`](https://github.com/Effect-TS/effect/commit/672920f85da8abd5f9d4ad85e29248a2aca57ed8)]: + - @effect/platform@0.81.0 + +## 0.46.18 + +### Patch Changes + +- Updated dependencies [[`2f3b7d4`](https://github.com/Effect-TS/effect/commit/2f3b7d4e1fa1ef8790b0ca4da22eb88872ee31df)]: + - effect@3.14.21 + - @effect/platform@0.80.21 + +## 0.46.17 + +### Patch Changes + +- Updated dependencies [[`17e2f30`](https://github.com/Effect-TS/effect/commit/17e2f3091408cf0fca9414d4af3bdf7b2765b378)]: + - effect@3.14.20 + - @effect/platform@0.80.20 + +## 0.46.16 + +### Patch Changes + +- Updated dependencies [[`056a910`](https://github.com/Effect-TS/effect/commit/056a910d0a0b8b00b0dc9df4a070466b2b5c2f6c), [`e25e7bb`](https://github.com/Effect-TS/effect/commit/e25e7bbc1797733916f48f501425d9f2ef310d9f), [`3273d57`](https://github.com/Effect-TS/effect/commit/3273d572c2b3175a842677f19efeea4cd65ab016)]: + - effect@3.14.19 + - @effect/platform@0.80.19 + +## 0.46.15 + +### Patch Changes + +- Updated dependencies [[`b1164d4`](https://github.com/Effect-TS/effect/commit/b1164d49a1dfdf299e9971367b6fc6be4df0ddff)]: + - effect@3.14.18 + - @effect/platform@0.80.18 + +## 0.46.14 + +### Patch Changes + +- [#4808](https://github.com/Effect-TS/effect/pull/4808) [`b5b7da3`](https://github.com/Effect-TS/effect/commit/b5b7da31998dac258677fb51225800f313a87c86) Thanks @tim-smart! - add a shutdownTimeout option to the @effect/opentelemetry exporters + +- Updated dependencies [[`0b54681`](https://github.com/Effect-TS/effect/commit/0b54681cd89245e211d8f49272be0f1bf2f81813), [`41a59d5`](https://github.com/Effect-TS/effect/commit/41a59d5916a296b12b0d5ead9e859e05f40b4cce)]: + - effect@3.14.17 + - @effect/platform@0.80.17 + +## 0.46.13 + +### Patch Changes + +- Updated dependencies [[`ee14444`](https://github.com/Effect-TS/effect/commit/ee144441021ec77039e43396eaf90714687bb495), [`f1c8583`](https://github.com/Effect-TS/effect/commit/f1c8583f8c3ea9415f813795ca2940a897c9ba9a)]: + - effect@3.14.16 + - @effect/platform@0.80.16 + +## 0.46.12 + +### Patch Changes + +- Updated dependencies [[`239cc99`](https://github.com/Effect-TS/effect/commit/239cc995ce645946210a3c3d2cb52bd3547c0687), [`8b6c947`](https://github.com/Effect-TS/effect/commit/8b6c947eaa8e45a67ecb3c37d45cd27f3e41d165), [`c50a63b`](https://github.com/Effect-TS/effect/commit/c50a63bbecb9f560b9cae349c447eed877d1b9b6)]: + - effect@3.14.15 + - @effect/platform@0.80.15 + +## 0.46.11 + +### Patch Changes + +- Updated dependencies [[`6ed8d15`](https://github.com/Effect-TS/effect/commit/6ed8d1589beb181d30abc79afebdaabc1d101538)]: + - effect@3.14.14 + - @effect/platform@0.80.14 + +## 0.46.10 + +### Patch Changes + +- Updated dependencies [[`ee77788`](https://github.com/Effect-TS/effect/commit/ee77788747e7ebbde6bfa88256cde49dbbad3608), [`5fce6ba`](https://github.com/Effect-TS/effect/commit/5fce6ba19c3cc63cc0104e737e581ad989dedbf0), [`570e45f`](https://github.com/Effect-TS/effect/commit/570e45f8cb936e42ec48f67f21bb2b7252f36c0c)]: + - effect@3.14.13 + - @effect/platform@0.80.13 + +## 0.46.9 + +### Patch Changes + +- [#4772](https://github.com/Effect-TS/effect/pull/4772) [`082e615`](https://github.com/Effect-TS/effect/commit/082e6157493f2ba4c943620cdaf858cbfefd7df6) Thanks @tim-smart! - include error cause in otel exception.stacktrace + +## 0.46.8 + +### Patch Changes + +- Updated dependencies [[`c2ad9ee`](https://github.com/Effect-TS/effect/commit/c2ad9ee9f3c4c743390edf35ed9e85a20be33811), [`9c68654`](https://github.com/Effect-TS/effect/commit/9c686542b6eb3ea188cb70673ef2e41223633e89)]: + - effect@3.14.12 + - @effect/platform@0.80.12 + +## 0.46.7 + +### Patch Changes + +- [#4760](https://github.com/Effect-TS/effect/pull/4760) [`43b22df`](https://github.com/Effect-TS/effect/commit/43b22df02f9504ef17b3462c2305f4e89e190763) Thanks @tim-smart! - disable tracer for Otlp http client + +- Updated dependencies [[`e536127`](https://github.com/Effect-TS/effect/commit/e536127c1e6f2fb3a542c73ae919435a629a346b)]: + - effect@3.14.11 + - @effect/platform@0.80.11 + +## 0.46.6 + +### Patch Changes + +- Updated dependencies [[`bc7efa3`](https://github.com/Effect-TS/effect/commit/bc7efa3b031bb25e1ed3c8f2d3fb5e8da166cadc)]: + - effect@3.14.10 + - @effect/platform@0.80.10 + +## 0.46.5 + +### Patch Changes + +- [#4740](https://github.com/Effect-TS/effect/pull/4740) [`b7a64c9`](https://github.com/Effect-TS/effect/commit/b7a64c90bbd7fcdedb56a2bcb1e080a3323d7699) Thanks @tim-smart! - add Otlp module to @effect/opentelemetry + + This module allows you to setup an exporter for Traces, Metrics & Logs with one + Layer. + + It also has no dependency on the @opentelemetry libraries, so you don't need to + add any additional deps to your package.json. + + ```ts + import * as Otlp from "@effect/opentelemetry/Otlp" + import * as FetchHttpClient from "@effect/platform/FetchHttpClient" + import { Effect, Layer, Schedule } from "effect" + + // Includes an Effect Tracer, Logger & Metric exporter + const Observability = Otlp.layer({ + baseUrl: "http://localhost:4318", + resource: { + serviceName: "my-service" + } + }).pipe(Layer.provide(FetchHttpClient.layer)) + ``` + +- [#4740](https://github.com/Effect-TS/effect/pull/4740) [`b7a64c9`](https://github.com/Effect-TS/effect/commit/b7a64c90bbd7fcdedb56a2bcb1e080a3323d7699) Thanks @tim-smart! - add Effect native OtlpMetrics & OtlpLogger + +- Updated dependencies [[`d78249f`](https://github.com/Effect-TS/effect/commit/d78249f0b67f63cf4baf806ff090cba33293daf0)]: + - effect@3.14.9 + - @effect/platform@0.80.9 + +## 0.46.4 + +### Patch Changes + +- [#4732](https://github.com/Effect-TS/effect/pull/4732) [`049f0ec`](https://github.com/Effect-TS/effect/commit/049f0ec78e9e9333daaeab67e6182632684e5310) Thanks @tim-smart! - include cause in otel exceptions + +## 0.46.3 + +### Patch Changes + +- [#4730](https://github.com/Effect-TS/effect/pull/4730) [`d36ccc0`](https://github.com/Effect-TS/effect/commit/d36ccc0c3ce79d2f4214fc5341272812aefaca0e) Thanks @tim-smart! - adjust otel export when max batch size is reached + +## 0.46.2 + +### Patch Changes + +- [#4727](https://github.com/Effect-TS/effect/pull/4727) [`46a64bd`](https://github.com/Effect-TS/effect/commit/46a64bd30b69176d6d2ca99823a4de02b3dcf40c) Thanks @tim-smart! - add effect native otlp exporter + +## 0.46.1 + +### Patch Changes + +- Updated dependencies [[`b3a2d32`](https://github.com/Effect-TS/effect/commit/b3a2d32772e6f7f20eacf2e18128e99324c4d378)]: + - effect@3.14.8 + +## 0.46.0 + +### Minor Changes + +- [#4696](https://github.com/Effect-TS/effect/pull/4696) [`f367477`](https://github.com/Effect-TS/effect/commit/f3674777fcd4c1797ac1b46be48c3ad2d6321cfc) Thanks @titouancreach! - update otel peer dependencies to ^2.0.0 and fix breaking changes + +### Patch Changes + +- Updated dependencies [[`b542a4b`](https://github.com/Effect-TS/effect/commit/b542a4bf195be0c9af1523e1ba96c953decc4d25)]: + - effect@3.14.7 + +## 0.45.6 + +### Patch Changes + +- Updated dependencies [[`47618c1`](https://github.com/Effect-TS/effect/commit/47618c1ad84ebcc5a51133a3fff5aa5012d49d45), [`6077882`](https://github.com/Effect-TS/effect/commit/60778824a4794336c33807801f813f8751d1c7e4)]: + - effect@3.14.6 + +## 0.45.5 + +### Patch Changes + +- Updated dependencies [[`40dbfef`](https://github.com/Effect-TS/effect/commit/40dbfeff239b6e567706752114f31b2fce7de4e3), [`5a5ebdd`](https://github.com/Effect-TS/effect/commit/5a5ebdddfaddd259538b4599a6676281faca778e)]: + - effect@3.14.5 + +## 0.45.4 + +### Patch Changes + +- Updated dependencies [[`e4ba2c6`](https://github.com/Effect-TS/effect/commit/e4ba2c66a878e81b5e295d6d49aaf724b80a28ef)]: + - effect@3.14.4 + +## 0.45.3 + +### Patch Changes + +- Updated dependencies [[`37aa8e1`](https://github.com/Effect-TS/effect/commit/37aa8e137725a902e70cd1e468ea98b873aa5056), [`34f03d6`](https://github.com/Effect-TS/effect/commit/34f03d66875f21f266f102223a03cd14c2ed6ea6)]: + - effect@3.14.3 + +## 0.45.2 + +### Patch Changes + +- Updated dependencies [[`f87991b`](https://github.com/Effect-TS/effect/commit/f87991b6d8a2edfaf90b01cebda4b466992ae865), [`f87991b`](https://github.com/Effect-TS/effect/commit/f87991b6d8a2edfaf90b01cebda4b466992ae865), [`0a3e3e1`](https://github.com/Effect-TS/effect/commit/0a3e3e18eea5e0d1882f1a6c906198e6ef226a41)]: + - effect@3.14.2 + +## 0.45.1 + +### Patch Changes + +- Updated dependencies [[`4a274fe`](https://github.com/Effect-TS/effect/commit/4a274fe9f623182b6b902827e0e83bd89ca3b05c)]: + - effect@3.14.1 + +## 0.45.0 + +### Minor Changes + +- [#4469](https://github.com/Effect-TS/effect/pull/4469) [`04dff2d`](https://github.com/Effect-TS/effect/commit/04dff2d01ac68c260f29a6d4743381825c353c86) Thanks @tim-smart! - add Tracer Span.addLinks, for dynamically linking spans + +### Patch Changes + +- Updated dependencies [[`1f47e4e`](https://github.com/Effect-TS/effect/commit/1f47e4e12546ab691b29bfb7b5128bb17b93baa5), [`26dd75f`](https://github.com/Effect-TS/effect/commit/26dd75f276a0d8a63eab313bd5a167d5072c9780), [`04dff2d`](https://github.com/Effect-TS/effect/commit/04dff2d01ac68c260f29a6d4743381825c353c86), [`c7fac0c`](https://github.com/Effect-TS/effect/commit/c7fac0cd7eadcd5cc0c3a987051c5b57ad271638), [`aba2d1d`](https://github.com/Effect-TS/effect/commit/aba2d1d831ea149481bd4dd755528c0afa8239ce), [`ffaa3f3`](https://github.com/Effect-TS/effect/commit/ffaa3f3969df26610fcc02ad537340641d44e803), [`ab957c1`](https://github.com/Effect-TS/effect/commit/ab957c1fee714868f56c7ab4e802b9d449e9b666), [`35db9ce`](https://github.com/Effect-TS/effect/commit/35db9ce228f1416c8abacc6dc9c36fbd0f33ef0f), [`cf77ea9`](https://github.com/Effect-TS/effect/commit/cf77ea9ab4fc89e66a43f682a9926ccdee6c57ed), [`26dd75f`](https://github.com/Effect-TS/effect/commit/26dd75f276a0d8a63eab313bd5a167d5072c9780), [`baaab60`](https://github.com/Effect-TS/effect/commit/baaab60b737f35dfab8e4a21bce28a195d19e899)]: + - effect@3.14.0 + +## 0.44.12 + +### Patch Changes + +- Updated dependencies [[`0c4803f`](https://github.com/Effect-TS/effect/commit/0c4803fcc69262d11a97ce49d0e9b4288df0651f), [`6f65ac4`](https://github.com/Effect-TS/effect/commit/6f65ac4eac1489cd6ea390e18b0908670722adad)]: + - effect@3.13.12 + +## 0.44.11 + +### Patch Changes + +- Updated dependencies [[`fad8cca`](https://github.com/Effect-TS/effect/commit/fad8cca9bbfcc2eaeb44b97c15dbe0a1eda75315), [`4296293`](https://github.com/Effect-TS/effect/commit/4296293049414d0cf2d915a26c552b09f946b9a0), [`9c241ab`](https://github.com/Effect-TS/effect/commit/9c241abe47ccf7a5257b98a4a64a63054a12741d), [`082b0c1`](https://github.com/Effect-TS/effect/commit/082b0c1b9f4252bcdd69608f2e4a9226f953ac3f), [`be12983`](https://github.com/Effect-TS/effect/commit/be12983bc7e7537b41cd8910fc4eb7d1da56ab07), [`de88127`](https://github.com/Effect-TS/effect/commit/de88127a5a5906ccece98af74787b5ae0e65e431)]: + - effect@3.13.11 + +## 0.44.10 + +### Patch Changes + +- Updated dependencies [[`527c964`](https://github.com/Effect-TS/effect/commit/527c9645229f5be9714a7e60a38a9e753c4bbfb1)]: + - effect@3.13.10 + +## 0.44.9 + +### Patch Changes + +- Updated dependencies [[`2976e52`](https://github.com/Effect-TS/effect/commit/2976e52538d9dc9ffdcbc84d4ac748cff9305971)]: + - effect@3.13.9 + +## 0.44.8 + +### Patch Changes + +- Updated dependencies [[`c65d336`](https://github.com/Effect-TS/effect/commit/c65d3362d07ec815ff3b46278314e8a31706ddc2), [`22d2ebb`](https://github.com/Effect-TS/effect/commit/22d2ebb4b11f5a44351a4736e65da391a3b647d0)]: + - effect@3.13.8 + +## 0.44.7 + +### Patch Changes + +- Updated dependencies [[`840cc73`](https://github.com/Effect-TS/effect/commit/840cc7329908db7ca693ef47b07d4f845c29cadd), [`9bf8a74`](https://github.com/Effect-TS/effect/commit/9bf8a74b967f18d931743dd5196af326c9118e9c), [`87ba23c`](https://github.com/Effect-TS/effect/commit/87ba23c41c193503ed0c612b0d32d0b253794c64)]: + - effect@3.13.7 + +## 0.44.6 + +### Patch Changes + +- Updated dependencies [[`3154ce4`](https://github.com/Effect-TS/effect/commit/3154ce4692fa18b804982158d3c4c8a8a5fae386)]: + - effect@3.13.6 + +## 0.44.5 + +### Patch Changes + +- Updated dependencies [[`367bb35`](https://github.com/Effect-TS/effect/commit/367bb35f4c2a254e1fb211d96db2474a7aed9020), [`6cf11c3`](https://github.com/Effect-TS/effect/commit/6cf11c3a75773ceec2877c85ddc760f381f0866d), [`a0acec8`](https://github.com/Effect-TS/effect/commit/a0acec851f72e19466363d24b9cc218acd00006a)]: + - effect@3.13.5 + +## 0.44.4 + +### Patch Changes + +- Updated dependencies [[`17d9e89`](https://github.com/Effect-TS/effect/commit/17d9e89f9851663bdbb6c1e685601d97806114a4)]: + - effect@3.13.4 + +## 0.44.3 + +### Patch Changes + +- Updated dependencies [[`cc5588d`](https://github.com/Effect-TS/effect/commit/cc5588df07f9103513547cb429ce041b9436a8bd), [`623c8cd`](https://github.com/Effect-TS/effect/commit/623c8cd053ed6ee3d353aaa8778d484670fca2bb), [`00b4eb1`](https://github.com/Effect-TS/effect/commit/00b4eb1ece12a16e222e6220965bb4024d6752ac), [`f2aee98`](https://github.com/Effect-TS/effect/commit/f2aee989b0a600900ce83e7f460d02908620c80f), [`fb798eb`](https://github.com/Effect-TS/effect/commit/fb798eb9061f1191badc017d1aa649360254da20), [`2251b15`](https://github.com/Effect-TS/effect/commit/2251b1528810bb695b37ce388b653cec0c5bf80c), [`2e15c1e`](https://github.com/Effect-TS/effect/commit/2e15c1e33648add0b29fe274fbcb7294b7515085), [`a4979db`](https://github.com/Effect-TS/effect/commit/a4979db021aef16e731be64df196b72088fc4376), [`b74255a`](https://github.com/Effect-TS/effect/commit/b74255a304ad49d60bedb1a260fd697f370af27a), [`d7f6a5c`](https://github.com/Effect-TS/effect/commit/d7f6a5c7d26c1963dcd864ca62360d20d08c7b49), [`9dd8979`](https://github.com/Effect-TS/effect/commit/9dd8979e940915b1cc1b1f264f3d019c77a65a02), [`477b488`](https://github.com/Effect-TS/effect/commit/477b488284f47c5469d7fba3e4065fb7e3b6556e), [`10932cb`](https://github.com/Effect-TS/effect/commit/10932cbf58fc721ada631cebec42f773ce96d3cc), [`9f6c784`](https://github.com/Effect-TS/effect/commit/9f6c78468b3b5e9ebfc38ffdfb70702901ee977b), [`2c639ec`](https://github.com/Effect-TS/effect/commit/2c639ecee332de4266e36022c989c35ae4e02105), [`886aaa8`](https://github.com/Effect-TS/effect/commit/886aaa81e06dfd3cd9391e8ea987d8cd5ada1124)]: + - effect@3.13.3 + +## 0.44.2 + +### Patch Changes + +- Updated dependencies [[`31be72a`](https://github.com/Effect-TS/effect/commit/31be72ada118cb84a942e67b1663263f8db74a9f)]: + - effect@3.13.2 + +## 0.44.1 + +### Patch Changes + +- Updated dependencies [[`b56a211`](https://github.com/Effect-TS/effect/commit/b56a2110569fd0ec0b57ac137743e926d49f51cc)]: + - effect@3.13.1 + +## 0.44.0 + +### Patch Changes + +- Updated dependencies [[`8baef83`](https://github.com/Effect-TS/effect/commit/8baef83e7ff0b7bc0738b680e1ef013065386cff), [`655bfe2`](https://github.com/Effect-TS/effect/commit/655bfe29e44cc3f0fb9b4e53038f50b891c188df), [`d90cbc2`](https://github.com/Effect-TS/effect/commit/d90cbc274e2742d18671fe65aa4764c057eb6cba), [`75632bd`](https://github.com/Effect-TS/effect/commit/75632bd44b8025101d652ccbaeef898c7086c91c), [`c874a2e`](https://github.com/Effect-TS/effect/commit/c874a2e4b17e9d71904ca8375bb77b020975cb1d), [`bf865e5`](https://github.com/Effect-TS/effect/commit/bf865e5833f77fd8f6c06944ca9d507b54488301), [`f98b2b7`](https://github.com/Effect-TS/effect/commit/f98b2b7592cf20f9d85313e7f1e964cb65878138), [`de8ce92`](https://github.com/Effect-TS/effect/commit/de8ce924923eaa4e1b761a97eb45ec967389f3d5), [`cf8b2dd`](https://github.com/Effect-TS/effect/commit/cf8b2dd112f8e092ed99d78fd728db0f91c29050), [`db426a5`](https://github.com/Effect-TS/effect/commit/db426a5fb41ab84d18e3c8753a7329b4de544245), [`6862444`](https://github.com/Effect-TS/effect/commit/6862444094906ad4f2cb077ff3b9cc0b73880c8c), [`5fc8a90`](https://github.com/Effect-TS/effect/commit/5fc8a90ba46a5fd9f3b643f0b5aeadc69d717339), [`546a492`](https://github.com/Effect-TS/effect/commit/546a492e60eb2b8b048a489a474b934ea0877005), [`65c4796`](https://github.com/Effect-TS/effect/commit/65c47966ce39055f02cf5c808daabb3ea6442b0b), [`9760fdc`](https://github.com/Effect-TS/effect/commit/9760fdc37bdaef9da8b150e46b86ddfbe2ad9221), [`5b471e7`](https://github.com/Effect-TS/effect/commit/5b471e7d4317e8ee5d72bbbd3e0c9775160949ab), [`4f810cc`](https://github.com/Effect-TS/effect/commit/4f810cc2770e9f1f266851d2cb6257112c12af49)]: + - effect@3.13.0 + +## 0.43.2 + +### Patch Changes + +- Updated dependencies [[`4018eae`](https://github.com/Effect-TS/effect/commit/4018eaed2733241676ddb8c52416f463a8c32e35), [`543d36d`](https://github.com/Effect-TS/effect/commit/543d36d1a11452560b01ab966a82529ad5fee8c9), [`f70a65a`](https://github.com/Effect-TS/effect/commit/f70a65ac80c6635d80b12beaf4d32a9cc59fa143), [`ba409f6`](https://github.com/Effect-TS/effect/commit/ba409f69c41aeaa29e475c0630735726eaf4dbac), [`3d2e356`](https://github.com/Effect-TS/effect/commit/3d2e3565e8a43d1bdb5daee8db3b90f56d71d859)]: + - effect@3.12.12 + +## 0.43.1 + +### Patch Changes + +- Updated dependencies [[`b6a032f`](https://github.com/Effect-TS/effect/commit/b6a032f07bffa020a848c813881879395134fa20), [`42ddd5f`](https://github.com/Effect-TS/effect/commit/42ddd5f144ce9f9d94a036679ebbd626446d37f5), [`2fe447c`](https://github.com/Effect-TS/effect/commit/2fe447c6354d334f9c591b8a8481818f5f0e797e)]: + - effect@3.12.11 + +## 0.43.0 + +### Minor Changes + +- [#4371](https://github.com/Effect-TS/effect/pull/4371) [`57d1623`](https://github.com/Effect-TS/effect/commit/57d1623d78883a7e3efd00a5ccf4c2ae8273e222) Thanks @jpowersdev! - Add `LoggerProvider` support from `@opentelemetry/sdk-logs` to `@effect/opentelemetry`. + +### Patch Changes + +- [#4371](https://github.com/Effect-TS/effect/pull/4371) [`57d1623`](https://github.com/Effect-TS/effect/commit/57d1623d78883a7e3efd00a5ccf4c2ae8273e222) Thanks @jpowersdev! - Use Resource.layerFromEnv by default in NodeSdk.layer + +- Updated dependencies [[`e30f132`](https://github.com/Effect-TS/effect/commit/e30f132c336c9d0760bad39f82a55c7ce5159eb7), [`33fa667`](https://github.com/Effect-TS/effect/commit/33fa667c2623be1026e1ccee91bd44f73b09020a), [`87f5f28`](https://github.com/Effect-TS/effect/commit/87f5f2842e4196cb88d13f10f443ff0567e82832), [`4dbd170`](https://github.com/Effect-TS/effect/commit/4dbd170538e8fb7a36aa7c469c6f93b6c7000091)]: + - effect@3.12.10 + +## 0.42.9 + +### Patch Changes + +- Updated dependencies [[`1b4a4e9`](https://github.com/Effect-TS/effect/commit/1b4a4e904ef5227ec7d9114d4e417eca19eed940)]: + - effect@3.12.9 + +## 0.42.8 + +### Patch Changes + +- [#4380](https://github.com/Effect-TS/effect/pull/4380) [`c45b559`](https://github.com/Effect-TS/effect/commit/c45b5592b5fd1189a5c932cfe05bd7d5f6d68508) Thanks @fubhy! - Fixed module imports + +- Updated dependencies [[`766113c`](https://github.com/Effect-TS/effect/commit/766113c0ea3512cdb887650ead8ba314236e22ee), [`712277f`](https://github.com/Effect-TS/effect/commit/712277f949052a24b46e4aa234063a6abf395c90), [`f269122`](https://github.com/Effect-TS/effect/commit/f269122508693b111142994dd48698ddc75f3d69), [`430c846`](https://github.com/Effect-TS/effect/commit/430c846cbac05b187e3d24ac8dfee0cf22506f7c), [`7b03057`](https://github.com/Effect-TS/effect/commit/7b03057507d2dab5e6793beb9c578dedaaeb15fe), [`a9c94c8`](https://github.com/Effect-TS/effect/commit/a9c94c807755610831211a686d2fad849ab38eb4), [`107e6f0`](https://github.com/Effect-TS/effect/commit/107e6f0557a1e2d3b0dce25d62fa1e2601521752), [`65c11b9`](https://github.com/Effect-TS/effect/commit/65c11b9266ec9447c31c26fe3ed35c73bd3b81fd), [`e386d2f`](https://github.com/Effect-TS/effect/commit/e386d2f1b3ab3ac2c14ee76de11f5963d32a3df4), [`9172efb`](https://github.com/Effect-TS/effect/commit/9172efba98bc6a82353e6ec2af61ac08f038ba64)]: + - effect@3.12.8 + +## 0.42.7 + +### Patch Changes + +- Updated dependencies [[`8dff1d1`](https://github.com/Effect-TS/effect/commit/8dff1d1bff76cdba643cad7f0bf864300f08bc61)]: + - effect@3.12.7 + +## 0.42.6 + +### Patch Changes + +- Updated dependencies [[`289c13b`](https://github.com/Effect-TS/effect/commit/289c13b38e8e35b214d46d385d05dead176c87cd), [`8b4e75d`](https://github.com/Effect-TS/effect/commit/8b4e75d35daea807c447ca760948a717aa66bb52), [`fc5e0f0`](https://github.com/Effect-TS/effect/commit/fc5e0f0d357a0051cfa01c1ede83ffdd3cb41ab1), [`004fd2b`](https://github.com/Effect-TS/effect/commit/004fd2bbd1459e64fb1b57f02eeb791ca5ea1ea5), [`b2a31be`](https://github.com/Effect-TS/effect/commit/b2a31be85c35d891351ce4f9a2cc93ece0c257f6), [`5514d05`](https://github.com/Effect-TS/effect/commit/5514d05b5cd586ff5868b8bd41c959e95e6c33cd), [`bf5f0ae`](https://github.com/Effect-TS/effect/commit/bf5f0ae9daa0170471678e22585e8ec14ce667bb), [`3b19bcf`](https://github.com/Effect-TS/effect/commit/3b19bcfd3aaadb6c9253428622df524537c8e626), [`b064b3b`](https://github.com/Effect-TS/effect/commit/b064b3b293615fd268cc5a5647d0981eb67750b8), [`289c13b`](https://github.com/Effect-TS/effect/commit/289c13b38e8e35b214d46d385d05dead176c87cd), [`f474678`](https://github.com/Effect-TS/effect/commit/f474678bf10b8f1c80e3dc096ddc7ecf20b2b23e), [`ee187d0`](https://github.com/Effect-TS/effect/commit/ee187d098007a402844c94d04f0cd8f07695377a)]: + - effect@3.12.6 + +## 0.42.5 + +### Patch Changes + +- Updated dependencies [[`a8b0ddb`](https://github.com/Effect-TS/effect/commit/a8b0ddb84710054799fc8f57485b95d00093ada1), [`507d546`](https://github.com/Effect-TS/effect/commit/507d546bd49db31000425fb5da88c434e4291bea), [`a8b0ddb`](https://github.com/Effect-TS/effect/commit/a8b0ddb84710054799fc8f57485b95d00093ada1), [`8db239b`](https://github.com/Effect-TS/effect/commit/8db239b9c869a3707f6566b9d9dbdf53c4df03fc), [`84a0911`](https://github.com/Effect-TS/effect/commit/84a091181634c3a022c94234cec7764a3aeef1be), [`84a0911`](https://github.com/Effect-TS/effect/commit/84a091181634c3a022c94234cec7764a3aeef1be), [`3179a9f`](https://github.com/Effect-TS/effect/commit/3179a9f65d23369a6a9a1f80f7750566dd28df22), [`6cb9b76`](https://github.com/Effect-TS/effect/commit/6cb9b766396d0b2ed995cf26957359713efd202e), [`1fcbe55`](https://github.com/Effect-TS/effect/commit/1fcbe55345042d8468f6a98c84081bd00b6bcf5a), [`d9a63d9`](https://github.com/Effect-TS/effect/commit/d9a63d9d385653865954cac895065360d54cc56b)]: + - effect@3.12.5 + +## 0.42.4 + +### Patch Changes + +- Updated dependencies [[`5b50ea4`](https://github.com/Effect-TS/effect/commit/5b50ea4a10cf9acd51f9624b2474d9d5ded74019), [`c170a68`](https://github.com/Effect-TS/effect/commit/c170a68b6266100774461fcd6c0e0fabb60112f2), [`a66c2eb`](https://github.com/Effect-TS/effect/commit/a66c2eb473245092cd41f04c2eb2b7b02cf53718)]: + - effect@3.12.4 + +## 0.42.3 + +### Patch Changes + +- Updated dependencies [[`d7dac48`](https://github.com/Effect-TS/effect/commit/d7dac48a477cdfeec509dbe9f33fce6a1b02b63d), [`1d7fd2b`](https://github.com/Effect-TS/effect/commit/1d7fd2b7ee8eeecc912d27adf76ed897db236dc5), [`1d7fd2b`](https://github.com/Effect-TS/effect/commit/1d7fd2b7ee8eeecc912d27adf76ed897db236dc5)]: + - effect@3.12.3 + +## 0.42.2 + +### Patch Changes + +- Updated dependencies [[`734af82`](https://github.com/Effect-TS/effect/commit/734af82138e78b9c57a8355b1c6b80e80d38b222), [`b63c780`](https://github.com/Effect-TS/effect/commit/b63c78010893101520448ddda7019c487cf7eedd), [`c640d77`](https://github.com/Effect-TS/effect/commit/c640d77b33ad417876f4e8ffe8574ee6cbe5607f), [`0def088`](https://github.com/Effect-TS/effect/commit/0def0887cfdb6755729a64dfd52b3b9f46b0576c)]: + - effect@3.12.2 + +## 0.42.1 + +### Patch Changes + +- Updated dependencies [[`302b57d`](https://github.com/Effect-TS/effect/commit/302b57d2cbf9b9ccc17450945aeebfb33cfe8d43), [`0988083`](https://github.com/Effect-TS/effect/commit/0988083d4594938590df5a287e5b27d38526dd07), [`8b46be6`](https://github.com/Effect-TS/effect/commit/8b46be6a3b8160362ab5ea9171c5e6932505125c), [`bfe8027`](https://github.com/Effect-TS/effect/commit/bfe802734b450a4b4ee069d1125dd37995db2bff), [`16dd657`](https://github.com/Effect-TS/effect/commit/16dd657033d8afac2ffea567b3c8bb27c9b249b6), [`39db211`](https://github.com/Effect-TS/effect/commit/39db211414e90c8db8fdad7dc8ce5b4661bcfaef)]: + - effect@3.12.1 + +## 0.42.0 + +### Patch Changes + +- Updated dependencies [[`abb22a4`](https://github.com/Effect-TS/effect/commit/abb22a429b9c52c31e84856294f175d2064a9b4d), [`f369a89`](https://github.com/Effect-TS/effect/commit/f369a89e98bc682969803b9304adaf4557bb36c2), [`642376c`](https://github.com/Effect-TS/effect/commit/642376c63fd7d78754db991631a4d50a5dc79aa3), [`3d2b7a7`](https://github.com/Effect-TS/effect/commit/3d2b7a7e942a7157afae5b1cdbc6f3fef116428e), [`73f9c6f`](https://github.com/Effect-TS/effect/commit/73f9c6f2ff091512cf904cc54ab59965b86e87c8), [`17cb451`](https://github.com/Effect-TS/effect/commit/17cb4514590e8a86263f7aed009f24da8a237342), [`d801820`](https://github.com/Effect-TS/effect/commit/d80182060c2ee945d7e0e4728812abf9465a0d6a), [`e1eeb2d`](https://github.com/Effect-TS/effect/commit/e1eeb2d7064b3870041dab142f3057970699bbf1), [`c11f3a6`](https://github.com/Effect-TS/effect/commit/c11f3a60a05c3b5fc8e7ce90136728154dc505b0), [`618f7e0`](https://github.com/Effect-TS/effect/commit/618f7e092a1011e5090dca1e69b5e9285689654b), [`c0ba834`](https://github.com/Effect-TS/effect/commit/c0ba834d1995cf5a8b250e4780fd43f3e3881151), [`e1eeb2d`](https://github.com/Effect-TS/effect/commit/e1eeb2d7064b3870041dab142f3057970699bbf1)]: + - effect@3.12.0 + +## 0.41.8 + +### Patch Changes + +- Updated dependencies [[`39457d4`](https://github.com/Effect-TS/effect/commit/39457d4897d9bc7df8af5c05d352866bbeae82eb), [`a475cc2`](https://github.com/Effect-TS/effect/commit/a475cc25fd7c9f26b27a8e98f8fbe43cc9e6ee3e), [`199214e`](https://github.com/Effect-TS/effect/commit/199214e21c616d8a0ccd7ed5f92e944e6c580193), [`b3c160d`](https://github.com/Effect-TS/effect/commit/b3c160d7a1fdfc2d3fb2440530f1ab80efc65133)]: + - effect@3.11.10 + +## 0.41.7 + +### Patch Changes + +- Updated dependencies [[`1c08a0b`](https://github.com/Effect-TS/effect/commit/1c08a0b8505badcffb4d9cade5a746ea90c9557e), [`1ce703b`](https://github.com/Effect-TS/effect/commit/1ce703b041bbd7560c5c437c9b9be48f027937fd), [`1ce703b`](https://github.com/Effect-TS/effect/commit/1ce703b041bbd7560c5c437c9b9be48f027937fd)]: + - effect@3.11.9 + +## 0.41.6 + +### Patch Changes + +- Updated dependencies [[`1a6b52d`](https://github.com/Effect-TS/effect/commit/1a6b52dcf020d36e38a7bc90b648152cf5a8ccba)]: + - effect@3.11.8 + +## 0.41.5 + +### Patch Changes + +- Updated dependencies [[`2408616`](https://github.com/Effect-TS/effect/commit/24086163b60b09cc6d0885bd565ef080dcbe866b), [`cec0b4d`](https://github.com/Effect-TS/effect/commit/cec0b4d152ef660be2ccdb0927255f2471436e6e), [`cec0b4d`](https://github.com/Effect-TS/effect/commit/cec0b4d152ef660be2ccdb0927255f2471436e6e), [`8d978c5`](https://github.com/Effect-TS/effect/commit/8d978c53f6fcc98d9d645ecba3e4b55d4297dd36), [`cec0b4d`](https://github.com/Effect-TS/effect/commit/cec0b4d152ef660be2ccdb0927255f2471436e6e), [`cec0b4d`](https://github.com/Effect-TS/effect/commit/cec0b4d152ef660be2ccdb0927255f2471436e6e)]: + - effect@3.11.7 + +## 0.41.4 + +### Patch Changes + +- Updated dependencies [[`662d1ce`](https://github.com/Effect-TS/effect/commit/662d1ce6fb7da384a95888d5b2bb5605bdf3208d), [`31c62d8`](https://github.com/Effect-TS/effect/commit/31c62d83cbdcf9850a8b5331faa239601c60f78a)]: + - effect@3.11.6 + +## 0.41.3 + +### Patch Changes + +- Updated dependencies [[`9f5a6f7`](https://github.com/Effect-TS/effect/commit/9f5a6f701bf7ba31adccd1f1bcfa8ab5614c9be8), [`22905cf`](https://github.com/Effect-TS/effect/commit/22905cf5addfb1ff3d2a6135c52036be958ae911), [`9f5a6f7`](https://github.com/Effect-TS/effect/commit/9f5a6f701bf7ba31adccd1f1bcfa8ab5614c9be8), [`1e59e4f`](https://github.com/Effect-TS/effect/commit/1e59e4fd778da18296812a2a32f36ca8ae50f60d), [`8d914e5`](https://github.com/Effect-TS/effect/commit/8d914e504e7a22d0ea628e8af265ee450ff9530f), [`03bb00f`](https://github.com/Effect-TS/effect/commit/03bb00faa74f9e168a54a8cc0828a664fbb1ab05), [`9f5a6f7`](https://github.com/Effect-TS/effect/commit/9f5a6f701bf7ba31adccd1f1bcfa8ab5614c9be8), [`14e1149`](https://github.com/Effect-TS/effect/commit/14e1149f1af5a022f06eb8c2e4ba9fec17fe7426), [`9f5a6f7`](https://github.com/Effect-TS/effect/commit/9f5a6f701bf7ba31adccd1f1bcfa8ab5614c9be8), [`9f5a6f7`](https://github.com/Effect-TS/effect/commit/9f5a6f701bf7ba31adccd1f1bcfa8ab5614c9be8)]: + - effect@3.11.5 + +## 0.41.2 + +### Patch Changes + +- Updated dependencies [[`518b258`](https://github.com/Effect-TS/effect/commit/518b258a8a67ecd332a9252c35cc060f8368dee2), [`6e323a3`](https://github.com/Effect-TS/effect/commit/6e323a36faaee46b328c8e3cf60a76b3aff9907f), [`6e323a3`](https://github.com/Effect-TS/effect/commit/6e323a36faaee46b328c8e3cf60a76b3aff9907f)]: + - effect@3.11.4 + +## 0.41.1 + +### Patch Changes + +- Updated dependencies [[`90906f7`](https://github.com/Effect-TS/effect/commit/90906f7f154b12c7182e8f39e3c55ef3937db857), [`3862cd3`](https://github.com/Effect-TS/effect/commit/3862cd3c7f6a542ed65fb81255b3bd696ce2f567), [`3862cd3`](https://github.com/Effect-TS/effect/commit/3862cd3c7f6a542ed65fb81255b3bd696ce2f567), [`343b6aa`](https://github.com/Effect-TS/effect/commit/343b6aa6ac4a74276bfc7c63ccbf4a1d72bc1bed), [`afba339`](https://github.com/Effect-TS/effect/commit/afba339adc11dad56b5a3b7ca94487e58f34d613)]: + - effect@3.11.3 + +## 0.41.0 + +### Minor Changes + +- [#4070](https://github.com/Effect-TS/effect/pull/4070) [`ac662f4`](https://github.com/Effect-TS/effect/commit/ac662f44646bb3370528e76b0545cf485bcf659c) Thanks @tim-smart! - ensure opentelemetry tags have unique identifiers + +### Patch Changes + +- [#4070](https://github.com/Effect-TS/effect/pull/4070) [`ac662f4`](https://github.com/Effect-TS/effect/commit/ac662f44646bb3370528e76b0545cf485bcf659c) Thanks @tim-smart! - expose Otel Tracer in Tracer layers + +## 0.40.2 + +### Patch Changes + +- Updated dependencies [[`01cee56`](https://github.com/Effect-TS/effect/commit/01cee560b58d94b24cc20e98083251b73e658b41)]: + - effect@3.11.2 + +## 0.40.1 + +### Patch Changes + +- Updated dependencies [[`dd8a2d8`](https://github.com/Effect-TS/effect/commit/dd8a2d8e80d33b16719fc69361eaedf0b59d4620), [`a71bfef`](https://github.com/Effect-TS/effect/commit/a71bfef46f5061bb2502a61a333638a987b62273)]: + - effect@3.11.1 + +## 0.40.0 + +### Patch Changes + +- Updated dependencies [[`147434b`](https://github.com/Effect-TS/effect/commit/147434b03d5e1fd692dd9f126e5ab0910f3b76d3), [`6e69493`](https://github.com/Effect-TS/effect/commit/6e694930048bbaf98110f35f41566aeb9752d471), [`147434b`](https://github.com/Effect-TS/effect/commit/147434b03d5e1fd692dd9f126e5ab0910f3b76d3), [`5eff3f6`](https://github.com/Effect-TS/effect/commit/5eff3f6fa3aae7e86948a62cbfd63b8d6c3bdf92), [`d9fe79b`](https://github.com/Effect-TS/effect/commit/d9fe79bb5a3fe105d8e7a3bc2922a8ad936a5d10), [`251d189`](https://github.com/Effect-TS/effect/commit/251d189420bbba71990574e91098c499065f9a9b), [`5a259f3`](https://github.com/Effect-TS/effect/commit/5a259f3711b4369f55d885b568bdb21136155261), [`b4ce4ea`](https://github.com/Effect-TS/effect/commit/b4ce4ea7fd514a7e572f2dcd879c98f334981b0e), [`15fcc5a`](https://github.com/Effect-TS/effect/commit/15fcc5a0ea4bbf40ab48fa6a04fdda74f76f4c07), [`9bc9a47`](https://github.com/Effect-TS/effect/commit/9bc9a476800dc645903c888a68bb1d3baa3383c6), [`aadb8a4`](https://github.com/Effect-TS/effect/commit/aadb8a48d2cba197c06ec9996505510e48e4e5cb), [`1e2747c`](https://github.com/Effect-TS/effect/commit/1e2747c63a4820d1459cbbc88c71212983bd68bd), [`9264162`](https://github.com/Effect-TS/effect/commit/9264162a82783a651776fb7b87604564a63e7070), [`e0b9b09`](https://github.com/Effect-TS/effect/commit/e0b9b09e70c386b2da17d1f0a15b0511861c89e8), [`c36f3b9`](https://github.com/Effect-TS/effect/commit/c36f3b95df5ce9d71b66f22f26ce12eda8d3e848), [`aadb8a4`](https://github.com/Effect-TS/effect/commit/aadb8a48d2cba197c06ec9996505510e48e4e5cb)]: + - effect@3.11.0 + +## 0.39.20 + +### Patch Changes + +- Updated dependencies [[`3069614`](https://github.com/Effect-TS/effect/commit/30696149271129fc618f6f2ccd1d8f2f6c0f9cd7), [`09a5e52`](https://github.com/Effect-TS/effect/commit/09a5e522fd9b221f05d85b1d1c8a740d4973c302)]: + - effect@3.10.20 + +## 0.39.19 + +### Patch Changes + +- Updated dependencies [[`944025b`](https://github.com/Effect-TS/effect/commit/944025bc5ce139f4a85846aa689bf30ec06a8ec1), [`54addee`](https://github.com/Effect-TS/effect/commit/54addee438a644bf010646c52042c7b89c5fc0a7)]: + - effect@3.10.19 + +## 0.39.18 + +### Patch Changes + +- Updated dependencies [[`af409cf`](https://github.com/Effect-TS/effect/commit/af409cf1d2ff973be11cc079ea373eaeedca25de)]: + - effect@3.10.18 + +## 0.39.17 + +### Patch Changes + +- Updated dependencies [[`42c4ce6`](https://github.com/Effect-TS/effect/commit/42c4ce6f8d8c7d847e97757650a8ad9419a829d7)]: + - effect@3.10.17 + +## 0.39.16 + +### Patch Changes + +- Updated dependencies [[`4dca30c`](https://github.com/Effect-TS/effect/commit/4dca30cfcdafe4542e236489f71d6f171a5b4e38), [`1d99867`](https://github.com/Effect-TS/effect/commit/1d998671be3cd11043f232822e91dd8c98fccfa9), [`6dae414`](https://github.com/Effect-TS/effect/commit/6dae4147991a97ec14a99289bd25fadae7541e8d), [`6b0d737`](https://github.com/Effect-TS/effect/commit/6b0d737078bf63b97891e6bc47affc04b28f9cf7), [`d8356aa`](https://github.com/Effect-TS/effect/commit/d8356aad428a0c2290db52380220f81d9ec94232)]: + - effect@3.10.16 + +## 0.39.15 + +### Patch Changes + +- Updated dependencies [[`8398b32`](https://github.com/Effect-TS/effect/commit/8398b3208242a88239d4449910b7baf923cfe3b6), [`72e55b7`](https://github.com/Effect-TS/effect/commit/72e55b7c610784fcebdbadc592c876e23e76a986)]: + - effect@3.10.15 + +## 0.39.14 + +### Patch Changes + +- Updated dependencies [[`f983946`](https://github.com/Effect-TS/effect/commit/f9839467b4cad6e788297764ef9f9f0b9fd203f9), [`2d8a750`](https://github.com/Effect-TS/effect/commit/2d8a75081eb83a0a81f817fdf6f428369c5064ab)]: + - effect@3.10.14 + +## 0.39.13 + +### Patch Changes + +- Updated dependencies [[`995bbdf`](https://github.com/Effect-TS/effect/commit/995bbdffea2e332f203cd5b474cd6a1c77dfa6ae)]: + - effect@3.10.13 + +## 0.39.12 + +### Patch Changes + +- Updated dependencies [[`dd14efe`](https://github.com/Effect-TS/effect/commit/dd14efe0ace255f571273aae876adea96267d7e6)]: + - effect@3.10.12 + +## 0.39.11 + +### Patch Changes + +- Updated dependencies [[`5eef499`](https://github.com/Effect-TS/effect/commit/5eef4998b6ccb7a5404d9e4fef85e57fa35fbb8a)]: + - effect@3.10.11 + +## 0.39.10 + +### Patch Changes + +- Updated dependencies [[`cd720ae`](https://github.com/Effect-TS/effect/commit/cd720aedf7f2571edec0843d6a633e84e4832b28), [`cd720ae`](https://github.com/Effect-TS/effect/commit/cd720aedf7f2571edec0843d6a633e84e4832b28), [`b631f40`](https://github.com/Effect-TS/effect/commit/b631f40abbe649b2a089764585b5c39f6a695ac6)]: + - effect@3.10.10 + +## 0.39.9 + +### Patch Changes + +- Updated dependencies [[`a123e80`](https://github.com/Effect-TS/effect/commit/a123e80f111a625428a5b5622b7f55ee1073566b), [`bd5fcd3`](https://github.com/Effect-TS/effect/commit/bd5fcd3e6b603b1e505af90d6a00627c8eca6d41), [`0289d3b`](https://github.com/Effect-TS/effect/commit/0289d3b6391031d00329365bab9791b355031fe3), [`7386b71`](https://github.com/Effect-TS/effect/commit/7386b710e5be570e17f468928a6ed19d549a3e12), [`4211a23`](https://github.com/Effect-TS/effect/commit/4211a2355bb3af3f0e756e2aae9d293379f25662)]: + - effect@3.10.9 + +## 0.39.8 + +### Patch Changes + +- [#3874](https://github.com/Effect-TS/effect/pull/3874) [`c348b4a`](https://github.com/Effect-TS/effect/commit/c348b4abf07b3dec4ba9bb64ec38615e87e1b195) Thanks @jbmusso! - Fix WebSdk.layer not properly infering when passing an evaluate argument of type Effect + +- Updated dependencies [[`68b5c9e`](https://github.com/Effect-TS/effect/commit/68b5c9e44f34192cef26e1cadda5e661a027df41), [`9c9928d`](https://github.com/Effect-TS/effect/commit/9c9928dfeacd9ac33dc37eb0ca3d7d8c39175ada), [`6306e66`](https://github.com/Effect-TS/effect/commit/6306e6656092b350d4ede5746da6f245ec9f7e07), [`361c7f3`](https://github.com/Effect-TS/effect/commit/361c7f39a2c10ede9324847c3d3ba192a6f9b20a)]: + - effect@3.10.8 + +## 0.39.7 + +### Patch Changes + +- Updated dependencies [[`33f5b9f`](https://github.com/Effect-TS/effect/commit/33f5b9ffaebea4f1bd0e391b44c41fb6230e743a), [`50f0281`](https://github.com/Effect-TS/effect/commit/50f0281b0d2116726b8927a6217622d5f394f3e4)]: + - effect@3.10.7 + +## 0.39.6 + +### Patch Changes + +- Updated dependencies [[`ce1c21f`](https://github.com/Effect-TS/effect/commit/ce1c21ffc11902ac9ab453a51904207859d38552)]: + - effect@3.10.6 + +## 0.39.5 + +### Patch Changes + +- Updated dependencies [[`3a6d757`](https://github.com/Effect-TS/effect/commit/3a6d757badeebe00d8ef4d67530d073c8264dcfa), [`59d813a`](https://github.com/Effect-TS/effect/commit/59d813aa4973d1115cfc70cc3667508335f49693)]: + - effect@3.10.5 + +## 0.39.4 + +### Patch Changes + +- Updated dependencies [[`2367708`](https://github.com/Effect-TS/effect/commit/2367708be449f9526a2047e321302d7bfb16f18e)]: + - effect@3.10.4 + +## 0.39.3 + +### Patch Changes + +- Updated dependencies [[`b9423d8`](https://github.com/Effect-TS/effect/commit/b9423d8bf8181a2389fdbce1e3c14ac6fe8d54f5)]: + - effect@3.10.3 + +## 0.39.2 + +### Patch Changes + +- Updated dependencies [[`714e119`](https://github.com/Effect-TS/effect/commit/714e11945e45e5a2554ee058e6c43f82a8e309cf), [`c1afd55`](https://github.com/Effect-TS/effect/commit/c1afd55c54e61f9c432823d21b3d016f79160a37)]: + - effect@3.10.2 + +## 0.39.1 + +### Patch Changes + +- Updated dependencies [[`9604d6b`](https://github.com/Effect-TS/effect/commit/9604d6b616435103dafea8b53637a9d1450b4750)]: + - effect@3.10.1 + +## 0.39.0 + +### Patch Changes + +- Updated dependencies [[`4a01828`](https://github.com/Effect-TS/effect/commit/4a01828b66d6213e9bbe18979c893b13f7bb29bf), [`4a01828`](https://github.com/Effect-TS/effect/commit/4a01828b66d6213e9bbe18979c893b13f7bb29bf), [`c79c4c1`](https://github.com/Effect-TS/effect/commit/c79c4c178390fe61ff6dda88c9e058862349343a), [`38d30f0`](https://github.com/Effect-TS/effect/commit/38d30f08b8da62f9c3e308b9250738cb8d17bdb5), [`5821ce3`](https://github.com/Effect-TS/effect/commit/5821ce3455b47d25e0a40cae6ce22af9db5fa556)]: + - effect@3.10.0 + +## 0.38.2 + +### Patch Changes + +- Updated dependencies [[`61a99b2`](https://github.com/Effect-TS/effect/commit/61a99b2bf9d757870ef0c2ec9d4c877cdd364a3d)]: + - effect@3.9.2 + +## 0.38.1 + +### Patch Changes + +- Updated dependencies [[`3b2ad1d`](https://github.com/Effect-TS/effect/commit/3b2ad1d58a2e33dc1a72b7037396bd25ca1702a9)]: + - effect@3.9.1 + +## 0.38.0 + +### Minor Changes + +- [#3737](https://github.com/Effect-TS/effect/pull/3737) [`6fc1e02`](https://github.com/Effect-TS/effect/commit/6fc1e020623dc10ece26572350db6ef824f8734d) Thanks @tim-smart! - remove Tracer.withActiveSpan + +### Patch Changes + +- [#3737](https://github.com/Effect-TS/effect/pull/3737) [`6fc1e02`](https://github.com/Effect-TS/effect/commit/6fc1e020623dc10ece26572350db6ef824f8734d) Thanks @tim-smart! - add Tracer.withSpanContext + + This api is useful for attaching a parent span to an Effect from an opentelemetry + span outside of Effect. + + ```typescript + import { Effect } from "effect" + import { Tracer } from "@effect/opentelemetry" + import * as OtelApi from "@opentelemetry/api" + + await OtelApi.trace.getTracer("test").startActiveSpan( + "otel-span", + { + root: true + }, + async (span) => { + try { + await Effect.runPromise( + Effect.log("inside otel parent span").pipe( + Tracer.withSpanContext(span.spanContext()) + ) + ) + } finally { + span.end() + } + } + ) + ``` + +- Updated dependencies [[`ff3d1aa`](https://github.com/Effect-TS/effect/commit/ff3d1aab290b4d1173b2dfc7e4c76abb4babdc16), [`0ba66f2`](https://github.com/Effect-TS/effect/commit/0ba66f2451641fd6990e02ec1ed01c014db9dab0), [`bf77f51`](https://github.com/Effect-TS/effect/commit/bf77f51b323c383224ebf08adf77a7a6e8c9b3cd), [`016f9ad`](https://github.com/Effect-TS/effect/commit/016f9ad931a4b3d09a34e5caf13d87c5b8e9c984), [`0779681`](https://github.com/Effect-TS/effect/commit/07796813f07de035719728733096ba64ce333469), [`534129f`](https://github.com/Effect-TS/effect/commit/534129f8113ce1a8ec50828083e16da9c86326c6), [`d75140c`](https://github.com/Effect-TS/effect/commit/d75140c7a664ceda43142d999f4ff8dcd36d6dda), [`be0451c`](https://github.com/Effect-TS/effect/commit/be0451c149b6618af79cb839cdf04af2db1efb03), [`9237ac6`](https://github.com/Effect-TS/effect/commit/9237ac69bc07de5b3b60076a0ad2921c21de7457), [`be0451c`](https://github.com/Effect-TS/effect/commit/be0451c149b6618af79cb839cdf04af2db1efb03), [`5b36494`](https://github.com/Effect-TS/effect/commit/5b364942e9a9003fdb8217324f8a2d8369c969da), [`c716adb`](https://github.com/Effect-TS/effect/commit/c716adb250ebbea1d1048d818ef7fed4f621d186), [`4986391`](https://github.com/Effect-TS/effect/commit/49863919cd8628c962a712fb1df30d2983820933), [`d75140c`](https://github.com/Effect-TS/effect/commit/d75140c7a664ceda43142d999f4ff8dcd36d6dda), [`d1387ae`](https://github.com/Effect-TS/effect/commit/d1387aebd1ff01bbebde26be46d488956e4daef6)]: + - effect@3.9.0 + +## 0.37.6 + +### Patch Changes + +- Updated dependencies [[`88e85db`](https://github.com/Effect-TS/effect/commit/88e85db34bd402526e27a323e950d053fa34d232), [`83887ca`](https://github.com/Effect-TS/effect/commit/83887ca1b1793916913d8550a4db4450cd14a044), [`5266b6c`](https://github.com/Effect-TS/effect/commit/5266b6cd86d76c3886da041c8829bca04b1a3110), [`cdead5c`](https://github.com/Effect-TS/effect/commit/cdead5c9cfd54dc6c4f215d9732f654c4a12e991), [`766a8af`](https://github.com/Effect-TS/effect/commit/766a8af307b414aca3648d91c4eab7493a5ec862)]: + - effect@3.8.5 + +## 0.37.5 + +### Patch Changes + +- [#3705](https://github.com/Effect-TS/effect/pull/3705) [`534abce`](https://github.com/Effect-TS/effect/commit/534abce3c9455ee2612f51d2d1449b4e0510fbe4) Thanks @Schniz! - add withActiveSpan function to attach Effect to current Span + + This function allows you to connect the Effect spans into a parent span + that was created outside of Effect, using the OpenTelemetry context propagation: + + ```ts + Effect.gen(function* () { + yield* Effect.sleep("100 millis").pipe(Effect.withSpan("sleep")) + yield* Console.log("done") + }).pipe( + Effect.withSpan("program"), + // This connects child spans to the current OpenTelemetry context + Tracer.withActiveSpan + ) + ``` + +## 0.37.4 + +### Patch Changes + +- Updated dependencies [[`4509656`](https://github.com/Effect-TS/effect/commit/45096569d50262275ee984f44c456f5c83b62683)]: + - effect@3.8.4 + +## 0.37.3 + +### Patch Changes + +- [#3644](https://github.com/Effect-TS/effect/pull/3644) [`bb5ec6b`](https://github.com/Effect-TS/effect/commit/bb5ec6b4b6a6f537394596c5a596faf52cb2aef4) Thanks @tim-smart! - fix encoding of logs to tracer span events + +- Updated dependencies [[`bb5ec6b`](https://github.com/Effect-TS/effect/commit/bb5ec6b4b6a6f537394596c5a596faf52cb2aef4)]: + - effect@3.8.3 + +## 0.37.2 + +### Patch Changes + +- Updated dependencies [[`f0d8ef1`](https://github.com/Effect-TS/effect/commit/f0d8ef1ce97ec2a87b09b3e24150cfeab85d6e2f)]: + - effect@3.8.2 + +## 0.37.1 + +### Patch Changes + +- Updated dependencies [[`10bf621`](https://github.com/Effect-TS/effect/commit/10bf6213f36d8ddb00f058a4609b85220f3d8334), [`ae36fa6`](https://github.com/Effect-TS/effect/commit/ae36fa68f754eeab9a54b6dc0f8b44db513aa2b6)]: + - effect@3.8.1 + +## 0.37.0 + +### Minor Changes + +- [#3541](https://github.com/Effect-TS/effect/pull/3541) [`007289a`](https://github.com/Effect-TS/effect/commit/007289a52d5877f8e90e2dacf38171ff9bf603fd) Thanks @mikearnaldi! - Cache some fiber references in the runtime to optimize reading in hot-paths + +### Patch Changes + +- Updated dependencies [[`fcfa6ee`](https://github.com/Effect-TS/effect/commit/fcfa6ee30ffd07d998bf22799357bf58580a116f), [`bb9931b`](https://github.com/Effect-TS/effect/commit/bb9931b62e249a3b801f2cb9d097aec0c8511af7), [`5798f76`](https://github.com/Effect-TS/effect/commit/5798f7619529de33e5ba06f551806f68fedc19db), [`5f0bfa1`](https://github.com/Effect-TS/effect/commit/5f0bfa17205398d4e4818bfbcf9e1b505b3b1fc5), [`812a4e8`](https://github.com/Effect-TS/effect/commit/812a4e86e2d1aa23b477ef5829aa0e5c07784936), [`273565e`](https://github.com/Effect-TS/effect/commit/273565e7901639e8d0541930ab715aea9c80fbaa), [`569a801`](https://github.com/Effect-TS/effect/commit/569a8017ef0a0bc203e4312867cbdd37b0effbd7), [`aa1fa53`](https://github.com/Effect-TS/effect/commit/aa1fa5301e886b9657c8eb0d38cb87cef92a8305), [`02f6b06`](https://github.com/Effect-TS/effect/commit/02f6b0660e12bee1069532a9cc18d3ab855257be), [`12b893e`](https://github.com/Effect-TS/effect/commit/12b893e63cc6dfada4aca7773b4783940e2edf25), [`bbad27e`](https://github.com/Effect-TS/effect/commit/bbad27ec0a90860593f759405caa877e7f4a655f), [`adf7d7a`](https://github.com/Effect-TS/effect/commit/adf7d7a7dfce3a7021e9f3b0d847dc85be89d754), [`007289a`](https://github.com/Effect-TS/effect/commit/007289a52d5877f8e90e2dacf38171ff9bf603fd), [`42a8f99`](https://github.com/Effect-TS/effect/commit/42a8f99740eefdaf2c4544d2c345313f97547a36), [`eebfd29`](https://github.com/Effect-TS/effect/commit/eebfd29633fd5d38b505c5c0842036f61f05e913), [`040703d`](https://github.com/Effect-TS/effect/commit/040703d0e100cd5511e52d812c15492414262b5e)]: + - effect@3.8.0 + +## 0.36.3 + +### Patch Changes + +- Updated dependencies [[`35a0f81`](https://github.com/Effect-TS/effect/commit/35a0f813141652d696461cd5d19fd146adaf85be)]: + - effect@3.7.3 + +## 0.36.2 + +### Patch Changes + +- Updated dependencies [[`8a601d7`](https://github.com/Effect-TS/effect/commit/8a601d7a1f8ffe52ac9e6d67e9282a1495fe59c9), [`353ba19`](https://github.com/Effect-TS/effect/commit/353ba19f9b2b9e959f0a00d058c6d40a4bc02db7)]: + - effect@3.7.2 + +## 0.36.1 + +### Patch Changes + +- Updated dependencies [[`79859e7`](https://github.com/Effect-TS/effect/commit/79859e71040d8edf1868b8530b90c650f4321eff), [`f6a469c`](https://github.com/Effect-TS/effect/commit/f6a469c190b9f00eee5ea0cd4d5912a0ef8b46f5), [`dcb9ec0`](https://github.com/Effect-TS/effect/commit/dcb9ec0db443894dd204d87450f779c44b9ad7f1), [`79aa6b1`](https://github.com/Effect-TS/effect/commit/79aa6b136e1f29b36f34e88cb2ff162bff2bb4ed)]: + - effect@3.7.1 + +## 0.36.0 + +### Patch Changes + +- Updated dependencies [[`db89601`](https://github.com/Effect-TS/effect/commit/db89601ee9c1050c4e762b7bd7ec65a6a2799dfe), [`2f456cc`](https://github.com/Effect-TS/effect/commit/2f456cce5012b9fcb6b4e039190d527813b75b92), [`8745e41`](https://github.com/Effect-TS/effect/commit/8745e41ed96e3765dc6048efc2a9afbe05c8a1e9), [`e557838`](https://github.com/Effect-TS/effect/commit/e55783886b046d3c5f33447f455f9ccf2fa75922), [`d6e7e40`](https://github.com/Effect-TS/effect/commit/d6e7e40b1e2ad0c59aa02f07344d28601b14ebdc), [`8356321`](https://github.com/Effect-TS/effect/commit/8356321598da04bd77c1001f45a4e447bec5591d), [`192f2eb`](https://github.com/Effect-TS/effect/commit/192f2ebb2c4ddbf4bfd8baedd32140b2376868f4), [`718cb70`](https://github.com/Effect-TS/effect/commit/718cb70038629a6d58d02e407760e341f7c94474), [`e9d0310`](https://github.com/Effect-TS/effect/commit/e9d03107acbf204d9304f3e8aea0816b7d3c7dfb), [`6bf28f7`](https://github.com/Effect-TS/effect/commit/6bf28f7e3b1e5e0608ff567205fea0581d11666f)]: + - effect@3.7.0 + +## 0.35.8 + +### Patch Changes + +- Updated dependencies [[`e809286`](https://github.com/Effect-TS/effect/commit/e8092865900608c4df7a6b7991b1c13cc1e4ca2d)]: + - effect@3.6.8 + +## 0.35.7 + +### Patch Changes + +- Updated dependencies [[`50ec889`](https://github.com/Effect-TS/effect/commit/50ec8897a49b7d1fe84f63107f89d543c52f3dfc)]: + - effect@3.6.7 + +## 0.35.6 + +### Patch Changes + +- Updated dependencies [[`f960bf4`](https://github.com/Effect-TS/effect/commit/f960bf45239e9badac6e0ad3a602f4174cd7bbdf), [`46a575f`](https://github.com/Effect-TS/effect/commit/46a575f48a05457b782fb21f7827d338c9b59320)]: + - effect@3.6.6 + +## 0.35.5 + +### Patch Changes + +- Updated dependencies [[`14a47a8`](https://github.com/Effect-TS/effect/commit/14a47a8c1f3cff2186b8fe7a919a1d773888fb5b), [`0c09841`](https://github.com/Effect-TS/effect/commit/0c0984173be3d58f050b300a1a8aa89d76ba49ae)]: + - effect@3.6.5 + +## 0.35.4 + +### Patch Changes + +- Updated dependencies [[`8295281`](https://github.com/Effect-TS/effect/commit/8295281ae9bd7441e680402540bf3c8682ec417b), [`c940df6`](https://github.com/Effect-TS/effect/commit/c940df63800bf3c4396d91cf28ec34938642fd2c), [`00b6c6d`](https://github.com/Effect-TS/effect/commit/00b6c6d4001f5de728b7d990a1b14560b4961a63), [`f8d95a6`](https://github.com/Effect-TS/effect/commit/f8d95a61ad0762147933c5c32bb6d7237e18eef4)]: + - effect@3.6.4 + +## 0.35.3 + +### Patch Changes + +- Updated dependencies [[`04adcac`](https://github.com/Effect-TS/effect/commit/04adcace913e6fc483df266874a68005e9e04ccf)]: + - effect@3.6.3 + +## 0.35.2 + +### Patch Changes + +- Updated dependencies [[`fd4b2f6`](https://github.com/Effect-TS/effect/commit/fd4b2f6516b325740dde615f1cf0229edf13ca0c)]: + - effect@3.6.2 + +## 0.35.1 + +### Patch Changes + +- Updated dependencies [[`510a34d`](https://github.com/Effect-TS/effect/commit/510a34d4cc5d2f51347a53847f6c7db84d2b17c6), [`45dbb9f`](https://github.com/Effect-TS/effect/commit/45dbb9ffeaf93d9e4df99d0cd4920e41ba9a3978)]: + - effect@3.6.1 + +## 0.35.0 + +### Patch Changes + +- Updated dependencies [[`1e0fe80`](https://github.com/Effect-TS/effect/commit/1e0fe802b36c257971296617473ce0abe730e8dc), [`8135294`](https://github.com/Effect-TS/effect/commit/8135294b591ea94fde7e6f94a504608f0e630520), [`cd255a4`](https://github.com/Effect-TS/effect/commit/cd255a48872d8fb924cf713ef73f0883a9cc6987), [`3845646`](https://github.com/Effect-TS/effect/commit/3845646828e98f3c7cda1217f6cfe5f642ac0603), [`2d09078`](https://github.com/Effect-TS/effect/commit/2d09078c5948b37fc2f79ef858fe4ca3e4814085), [`4bce5a0`](https://github.com/Effect-TS/effect/commit/4bce5a0274203550ccf117d830721891b0a3d182), [`4ddbff0`](https://github.com/Effect-TS/effect/commit/4ddbff0bb4e3ffddfeb509c59835b83245fb975e), [`e74cc38`](https://github.com/Effect-TS/effect/commit/e74cc38cb420a320c4d7ef98180f19d452a8b316), [`bb069b4`](https://github.com/Effect-TS/effect/commit/bb069b49ef291c532a02c1e8e74271f6d1bb32ec), [`cd255a4`](https://github.com/Effect-TS/effect/commit/cd255a48872d8fb924cf713ef73f0883a9cc6987), [`7d02174`](https://github.com/Effect-TS/effect/commit/7d02174af3bcbf054e5cdddb821c91d0f47e8285)]: + - effect@3.6.0 + +## 0.34.42 + +### Patch Changes + +- Updated dependencies [[`6359644`](https://github.com/Effect-TS/effect/commit/635964446323cf55d4060559337e710e4a24496e), [`7f41e42`](https://github.com/Effect-TS/effect/commit/7f41e428830bf3043b8be0d28dcd235d5747c942), [`f566fd1`](https://github.com/Effect-TS/effect/commit/f566fd1d7eea531a0d981dd24037f14a603a1273)]: + - effect@3.5.9 + +## 0.34.41 + +### Patch Changes + +- Updated dependencies [[`1ba640c`](https://github.com/Effect-TS/effect/commit/1ba640c702f187a866023bf043c26e25cce941ef), [`c8c71bd`](https://github.com/Effect-TS/effect/commit/c8c71bd20eb87d23133dac6156b83bb08941597c), [`a26ce58`](https://github.com/Effect-TS/effect/commit/a26ce581ca7d407e1e81439b58c8045b3fa65231)]: + - effect@3.5.8 + +## 0.34.40 + +### Patch Changes + +- [#3310](https://github.com/Effect-TS/effect/pull/3310) [`99bddcf`](https://github.com/Effect-TS/effect/commit/99bddcfb3d6eab4d489d055404e26ad81afe52fc) Thanks @fubhy! - Added additional pure annotations to improve tree-shakeability + +- Updated dependencies [[`3afcc93`](https://github.com/Effect-TS/effect/commit/3afcc93413a3d910beb69e4ce9ae120e4adaffd5), [`99bddcf`](https://github.com/Effect-TS/effect/commit/99bddcfb3d6eab4d489d055404e26ad81afe52fc)]: + - effect@3.5.7 + +## 0.34.39 + +### Patch Changes + +- Updated dependencies [[`cc327a1`](https://github.com/Effect-TS/effect/commit/cc327a1bccd22a4ee27ec7e58b53205e93b23e2c), [`4bfe4fb`](https://github.com/Effect-TS/effect/commit/4bfe4fb5c82f597c9beea9baa92e772593598b60), [`2b14d18`](https://github.com/Effect-TS/effect/commit/2b14d181462cad8359da4fa6bc6dfda0f742c398)]: + - effect@3.5.6 + +## 0.34.38 + +### Patch Changes + +- Updated dependencies [[`a9d7800`](https://github.com/Effect-TS/effect/commit/a9d7800f6a253192b653d77778b0674f39b1ca39)]: + - effect@3.5.5 + +## 0.34.37 + +### Patch Changes + +- [#3254](https://github.com/Effect-TS/effect/pull/3254) [`1b45236`](https://github.com/Effect-TS/effect/commit/1b4523699f91bc1e04ce30de1c007f0c0cf6e214) Thanks @tim-smart! - force flush otel provider before calling shutdown + +- Updated dependencies [[`ed0dde4`](https://github.com/Effect-TS/effect/commit/ed0dde4888e6f1a97ad5bba06b755d26a6a1c52e), [`ca775ce`](https://github.com/Effect-TS/effect/commit/ca775cec53baebc1a43d9b8852a3ac6726178498), [`5be9cc0`](https://github.com/Effect-TS/effect/commit/5be9cc044025a9541b9b7acefa2d3fc05fa1301b), [`203658f`](https://github.com/Effect-TS/effect/commit/203658f8001c132b25764ab70344b171683b554c), [`eb1c4d4`](https://github.com/Effect-TS/effect/commit/eb1c4d44e54b9d8d201a366d1ff94face2a6dcd3)]: + - effect@3.5.4 + +## 0.34.36 + +### Patch Changes + +- Updated dependencies [[`edb0da3`](https://github.com/Effect-TS/effect/commit/edb0da383746d760f35d8582f5fb0cc0eeca9217), [`c8d3fb0`](https://github.com/Effect-TS/effect/commit/c8d3fb0fe23585f6efb724af51fbab3ba1ad6e83), [`dabd028`](https://github.com/Effect-TS/effect/commit/dabd028decf9b7983ca16ebe0f48c05c11a84b68), [`786b2ab`](https://github.com/Effect-TS/effect/commit/786b2ab29d525c877bb84035dac9e2d6499339d1), [`fc57354`](https://github.com/Effect-TS/effect/commit/fc573547d41667016fce05eaee75960fcc6dce4d)]: + - effect@3.5.3 + +## 0.34.35 + +### Patch Changes + +- Updated dependencies [[`639208e`](https://github.com/Effect-TS/effect/commit/639208eeb8a44622994f832bc2d45d06ab636bc8), [`6684b4c`](https://github.com/Effect-TS/effect/commit/6684b4c27d77a7fcc7af2e261a450edf971b62b5), [`6684b4c`](https://github.com/Effect-TS/effect/commit/6684b4c27d77a7fcc7af2e261a450edf971b62b5), [`6684b4c`](https://github.com/Effect-TS/effect/commit/6684b4c27d77a7fcc7af2e261a450edf971b62b5)]: + - effect@3.5.2 + +## 0.34.34 + +### Patch Changes + +- Updated dependencies [[`55fdd76`](https://github.com/Effect-TS/effect/commit/55fdd761ee95afd73b6a892c13fee92b36c02837)]: + - effect@3.5.1 + +## 0.34.33 + +### Patch Changes + +- Updated dependencies [[`a1f5b83`](https://github.com/Effect-TS/effect/commit/a1f5b831a1bc7535988b370d68d0b3eb1123e0ce), [`a1f5b83`](https://github.com/Effect-TS/effect/commit/a1f5b831a1bc7535988b370d68d0b3eb1123e0ce), [`a1f5b83`](https://github.com/Effect-TS/effect/commit/a1f5b831a1bc7535988b370d68d0b3eb1123e0ce), [`60bc3d0`](https://github.com/Effect-TS/effect/commit/60bc3d0867b13e48b24dc22604b4dd2e7b2c1ca4), [`5ab348f`](https://github.com/Effect-TS/effect/commit/5ab348f265db3d283aa091ddca6d2d49137c16f2), [`60bc3d0`](https://github.com/Effect-TS/effect/commit/60bc3d0867b13e48b24dc22604b4dd2e7b2c1ca4), [`3e04bf8`](https://github.com/Effect-TS/effect/commit/3e04bf8a7127e956cadb7684a8f4c661df57663b), [`e7fc45f`](https://github.com/Effect-TS/effect/commit/e7fc45f0c7002aafdaec7878149ac064cd104ea3), [`a1f5b83`](https://github.com/Effect-TS/effect/commit/a1f5b831a1bc7535988b370d68d0b3eb1123e0ce), [`4626de5`](https://github.com/Effect-TS/effect/commit/4626de59c25b384216faa0be87bf0b8cd36357d0), [`f01e7db`](https://github.com/Effect-TS/effect/commit/f01e7db317827255d7901f523f2e28b43298e8df), [`60bc3d0`](https://github.com/Effect-TS/effect/commit/60bc3d0867b13e48b24dc22604b4dd2e7b2c1ca4), [`79d2d91`](https://github.com/Effect-TS/effect/commit/79d2d91464d95dde0e9444d43e7a7f309f05d6e6), [`ac71f37`](https://github.com/Effect-TS/effect/commit/ac71f378f2413e5aa91c95f649ffe898d6a26114), [`8432360`](https://github.com/Effect-TS/effect/commit/8432360ce68614a419bb328083a4109d0fc8aa93), [`e4bf1bf`](https://github.com/Effect-TS/effect/commit/e4bf1bf2b4a970eacd77c9b77b5ea8c68bc84498), [`13cb861`](https://github.com/Effect-TS/effect/commit/13cb861a5eded15c55c6cdcf6a8acde8320367a6), [`79d2d91`](https://github.com/Effect-TS/effect/commit/79d2d91464d95dde0e9444d43e7a7f309f05d6e6), [`e7fc45f`](https://github.com/Effect-TS/effect/commit/e7fc45f0c7002aafdaec7878149ac064cd104ea3), [`9f66825`](https://github.com/Effect-TS/effect/commit/9f66825f1fce0fe8d10420c285f7dc4c71e8af8d)]: + - effect@3.5.0 + +## 0.34.32 + +### Patch Changes + +- Updated dependencies [[`7af137c`](https://github.com/Effect-TS/effect/commit/7af137c9433f6e74959b3887561ec1e6f12e10ee), [`ee4b3dc`](https://github.com/Effect-TS/effect/commit/ee4b3dc5f68d19dc3ae1c2d12901c5b8ffbebabb), [`097d25c`](https://github.com/Effect-TS/effect/commit/097d25cb5d13c049e01789651be56b09620186ef)]: + - effect@3.4.9 + +## 0.34.31 + +### Patch Changes + +- Updated dependencies [[`a435e0f`](https://github.com/Effect-TS/effect/commit/a435e0fc5378b33a49bcec92ee235df6f16a2419), [`b5554db`](https://github.com/Effect-TS/effect/commit/b5554db36c4dd6f64fa5e6a62a29b2759c54217a), [`a9c4fb3`](https://github.com/Effect-TS/effect/commit/a9c4fb3bf3c6e92cd1c142b0605fddf7eb3c697c)]: + - effect@3.4.8 + +## 0.34.30 + +### Patch Changes + +- Updated dependencies [[`a5737d6`](https://github.com/Effect-TS/effect/commit/a5737d6db2b921605c332eabbc5402ee3d17357b)]: + - effect@3.4.7 + +## 0.34.29 + +### Patch Changes + +- Updated dependencies [[`5c0ceb0`](https://github.com/Effect-TS/effect/commit/5c0ceb00826cce9e50bf9d41d83e191d5352c030), [`5c0ceb0`](https://github.com/Effect-TS/effect/commit/5c0ceb00826cce9e50bf9d41d83e191d5352c030), [`33735b1`](https://github.com/Effect-TS/effect/commit/33735b16b41bd26929d8f4754c190925db6323b7), [`5c0ceb0`](https://github.com/Effect-TS/effect/commit/5c0ceb00826cce9e50bf9d41d83e191d5352c030), [`139d4b3`](https://github.com/Effect-TS/effect/commit/139d4b39fb3bff2eeaa7c0c809c581da42425a83)]: + - effect@3.4.6 + +## 0.34.28 + +### Patch Changes + +- Updated dependencies [[`a047af9`](https://github.com/Effect-TS/effect/commit/a047af99447dfffc729e9c8ef0ca143537927e91)]: + - effect@3.4.5 + +## 0.34.27 + +### Patch Changes + +- Updated dependencies [[`72638e3`](https://github.com/Effect-TS/effect/commit/72638e3d99f0e93a24febf6c225256ce92d4a20b), [`d7dde2b`](https://github.com/Effect-TS/effect/commit/d7dde2b4af08b37af859d4c327c1f5c6f00cf9d9), [`9b2fc3b`](https://github.com/Effect-TS/effect/commit/9b2fc3b9dfd304a2bd0508ef2313cfc54357be0c)]: + - effect@3.4.4 + +## 0.34.26 + +### Patch Changes + +- [#3079](https://github.com/Effect-TS/effect/pull/3079) [`bbdd365`](https://github.com/Effect-TS/effect/commit/bbdd36567706c94cdec45bacea825941c347b6cd) Thanks @tim-smart! - update dependencies + +- [#3079](https://github.com/Effect-TS/effect/pull/3079) [`bbdd365`](https://github.com/Effect-TS/effect/commit/bbdd36567706c94cdec45bacea825941c347b6cd) Thanks @tim-smart! - update to typescript 5.5 + +- Updated dependencies [[`c342739`](https://github.com/Effect-TS/effect/commit/c3427396226e1ad7b95b40595a23f9bdff3e3365), [`8898e5e`](https://github.com/Effect-TS/effect/commit/8898e5e238622f6337583d91ee23609c1f5ccdf7), [`ff78636`](https://github.com/Effect-TS/effect/commit/ff786367c522975f40f0f179a0ecdfcfab7ecbdb), [`c86bd4e`](https://github.com/Effect-TS/effect/commit/c86bd4e134c23146c216f9ff97e03781d55991b6), [`bbdd365`](https://github.com/Effect-TS/effect/commit/bbdd36567706c94cdec45bacea825941c347b6cd)]: + - effect@3.4.3 + +## 0.34.25 + +### Patch Changes + +- Updated dependencies [[`3da1497`](https://github.com/Effect-TS/effect/commit/3da1497b5c9cc886d300258bc928fd68a4fefe6f)]: + - effect@3.4.2 + +## 0.34.24 + +### Patch Changes + +- Updated dependencies [[`66a1910`](https://github.com/Effect-TS/effect/commit/66a19109ff90c4252123b8809b8c8a74681dba6a)]: + - effect@3.4.1 + +## 0.34.23 + +### Patch Changes + +- Updated dependencies [[`c0ce180`](https://github.com/Effect-TS/effect/commit/c0ce180861ad0938053c0e6145e813fa6404df3b), [`61707b6`](https://github.com/Effect-TS/effect/commit/61707b6ffc7397c2ba0dce22512b44955724f60f), [`9c1b5b3`](https://github.com/Effect-TS/effect/commit/9c1b5b39e6c19604ce834f072a114ad392c50a06), [`a35faf8`](https://github.com/Effect-TS/effect/commit/a35faf8d116f94899bfc03feab33b004c8ddfdf7), [`ff73c0c`](https://github.com/Effect-TS/effect/commit/ff73c0cacd66132bfad2e5211b3eae347729c667), [`984d516`](https://github.com/Effect-TS/effect/commit/984d516ccd9412dc41188f6a46b748dd20dd5848), [`8c3b8a2`](https://github.com/Effect-TS/effect/commit/8c3b8a2ce208eab753b6206a51605a424f104e98), [`017e2f9`](https://github.com/Effect-TS/effect/commit/017e2f9b371ce24ea4945e5d7390c934ad3c39cf), [`91bf8a2`](https://github.com/Effect-TS/effect/commit/91bf8a2e9d1959393b3cf7366cc1d584d3e666b7), [`c6a4a26`](https://github.com/Effect-TS/effect/commit/c6a4a266606575fd2c7165940c4072ad4c57d01f)]: + - effect@3.4.0 + +## 0.34.22 + +### Patch Changes + +- Updated dependencies [[`6c89408`](https://github.com/Effect-TS/effect/commit/6c89408cd7b9204ec4c5828a46cd5312d8afb5e7)]: + - effect@3.3.5 + +## 0.34.21 + +### Patch Changes + +- Updated dependencies [[`a67b8fe`](https://github.com/Effect-TS/effect/commit/a67b8fe2ace08419424811b5f0d9a5378eaea352)]: + - effect@3.3.4 + +## 0.34.20 + +### Patch Changes + +- Updated dependencies [[`06ede85`](https://github.com/Effect-TS/effect/commit/06ede85d6e84710e6622463be95ff3927fb30dad), [`7204ca5`](https://github.com/Effect-TS/effect/commit/7204ca5761c2b1d27999a624db23aa10b6e0504d)]: + - effect@3.3.3 + +## 0.34.19 + +### Patch Changes + +- Updated dependencies [[`3572646`](https://github.com/Effect-TS/effect/commit/3572646d5e0804f85bc7f64633fb95722533f9dd), [`1aed347`](https://github.com/Effect-TS/effect/commit/1aed347a125ed3847ec90863424810d6759cbc85), [`df4bf4b`](https://github.com/Effect-TS/effect/commit/df4bf4b62e7b316c6647da0271fc5544a84e7ba2), [`f085f92`](https://github.com/Effect-TS/effect/commit/f085f92dfa204afb41823ffc27d437225137643d)]: + - effect@3.3.2 + +## 0.34.18 + +### Patch Changes + +- Updated dependencies [[`eb98c5b`](https://github.com/Effect-TS/effect/commit/eb98c5b79ab50aa0cde239bd4e660dd19dbab612), [`184fed8`](https://github.com/Effect-TS/effect/commit/184fed83ac36cba05a75a5a8013f740f9f696e3b), [`6068e07`](https://github.com/Effect-TS/effect/commit/6068e073d4cc8b3c8583583fd5eb3efe43f7d5ba), [`3a77e20`](https://github.com/Effect-TS/effect/commit/3a77e209783933bac3aaddba1b05ff6a9ac72b36)]: + - effect@3.3.1 + +## 0.34.17 + +### Patch Changes + +- Updated dependencies [[`1f4ac00`](https://github.com/Effect-TS/effect/commit/1f4ac00a91c336c9c9c9b8c3ed9ceb9920ebc9bd), [`9305b76`](https://github.com/Effect-TS/effect/commit/9305b764cceeae4f16564435ae7172f79c2bf822), [`0f40d98`](https://github.com/Effect-TS/effect/commit/0f40d989da10f68df3ecd72b36849401ad679bfb), [`b761ef0`](https://github.com/Effect-TS/effect/commit/b761ef00eaf6c67b7ffe34798b98aae5347ab376), [`b53f69b`](https://github.com/Effect-TS/effect/commit/b53f69bff1452a487b21198cd83961f844e02d36), [`0f40d98`](https://github.com/Effect-TS/effect/commit/0f40d989da10f68df3ecd72b36849401ad679bfb), [`5bd549e`](https://github.com/Effect-TS/effect/commit/5bd549e4bd7144727db438ecca6b8dc9b3ef7e22), [`67f160a`](https://github.com/Effect-TS/effect/commit/67f160a213de0219a565d4bf653b3cbf24f58e8f)]: + - effect@3.3.0 + +## 0.34.16 + +### Patch Changes + +- Updated dependencies [[`8c5d280`](https://github.com/Effect-TS/effect/commit/8c5d280c0402284a4e58372867a15a431cb99461), [`6ba6d26`](https://github.com/Effect-TS/effect/commit/6ba6d269f5891e6b11aa35c5281dde4bf3273004), [`3f28bf2`](https://github.com/Effect-TS/effect/commit/3f28bf274333611906175446b772243f34f1b6d5), [`5817820`](https://github.com/Effect-TS/effect/commit/58178204a770d1a78c06945ef438f9fffbb50afa)]: + - effect@3.2.9 + +## 0.34.15 + +### Patch Changes + +- Updated dependencies [[`fb91f17`](https://github.com/Effect-TS/effect/commit/fb91f17098b48497feca9ec976feb87e4a82451b)]: + - effect@3.2.8 + +## 0.34.14 + +### Patch Changes + +- Updated dependencies [[`6801fca`](https://github.com/Effect-TS/effect/commit/6801fca44366be3ee1b6b99f54bd4f38a1b5e4f4)]: + - effect@3.2.7 + +## 0.34.13 + +### Patch Changes + +- Updated dependencies [[`cc8ac50`](https://github.com/Effect-TS/effect/commit/cc8ac5080daba8622ca2ff5dab5c37ddfab732ba)]: + - effect@3.2.6 + +## 0.34.12 + +### Patch Changes + +- Updated dependencies [[`608b01f`](https://github.com/Effect-TS/effect/commit/608b01fc342dbae2a642b308a67b84ead530ecea), [`031c712`](https://github.com/Effect-TS/effect/commit/031c7122a24ac42e48d6a434646b4f5d279d7442), [`a44e532`](https://github.com/Effect-TS/effect/commit/a44e532cf3a6a498b12a5aacf8124aa267e24ba0)]: + - effect@3.2.5 + +## 0.34.11 + +### Patch Changes + +- Updated dependencies [[`1af94df`](https://github.com/Effect-TS/effect/commit/1af94df6b74aeb4f6ebcbe80e074b4cb252e62e3), [`e313a01`](https://github.com/Effect-TS/effect/commit/e313a01b7e80f6cb7704055a190e5623c9d22c6d)]: + - effect@3.2.4 + +## 0.34.10 + +### Patch Changes + +- Updated dependencies [[`45578e8`](https://github.com/Effect-TS/effect/commit/45578e8faa80ae33d23e08f6f19467f818b7788f)]: + - effect@3.2.3 + +## 0.34.9 + +### Patch Changes + +- Updated dependencies [[`5d9266e`](https://github.com/Effect-TS/effect/commit/5d9266e8c740746ac9e186c3df6090a1b57fbe2a), [`9f8122e`](https://github.com/Effect-TS/effect/commit/9f8122e78884ab47c5e5f364d86eee1d1543cc61), [`6a6f670`](https://github.com/Effect-TS/effect/commit/6a6f6706b8613c8c7c10971b8d81a0f9e440a6f2)]: + - effect@3.2.2 + +## 0.34.8 + +### Patch Changes + +- Updated dependencies [[`c1e991d`](https://github.com/Effect-TS/effect/commit/c1e991dd5ba87901cd0e05697a8b4a267e7e954a)]: + - effect@3.2.1 + +## 0.34.7 + +### Patch Changes + +- [#2778](https://github.com/Effect-TS/effect/pull/2778) [`7135748`](https://github.com/Effect-TS/effect/commit/713574813a0f64085db0b5240ba39e7a0a7c137e) Thanks [@tim-smart](https://github.com/tim-smart)! - properly record exceptions in otel spans + +- Updated dependencies [[`146cadd`](https://github.com/Effect-TS/effect/commit/146cadd9d004634a3ff85c480bf92cf975c853e2), [`7135748`](https://github.com/Effect-TS/effect/commit/713574813a0f64085db0b5240ba39e7a0a7c137e), [`7135748`](https://github.com/Effect-TS/effect/commit/713574813a0f64085db0b5240ba39e7a0a7c137e), [`963b4e7`](https://github.com/Effect-TS/effect/commit/963b4e7ac87e2468feb6a344f7ab4ee4ad711198), [`64c9414`](https://github.com/Effect-TS/effect/commit/64c9414e960e82058ca09bbb3976d6fbef303a8e), [`7135748`](https://github.com/Effect-TS/effect/commit/713574813a0f64085db0b5240ba39e7a0a7c137e), [`2cbb76b`](https://github.com/Effect-TS/effect/commit/2cbb76bb52500a3f4bf27d1c91482518cbea56d7), [`870c5fa`](https://github.com/Effect-TS/effect/commit/870c5fa52cd61e745e8e828d38c3f09f00737553), [`7135748`](https://github.com/Effect-TS/effect/commit/713574813a0f64085db0b5240ba39e7a0a7c137e), [`64c9414`](https://github.com/Effect-TS/effect/commit/64c9414e960e82058ca09bbb3976d6fbef303a8e)]: + - effect@3.2.0 + +## 0.34.6 + +### Patch Changes + +- Updated dependencies [[`17fc22e`](https://github.com/Effect-TS/effect/commit/17fc22e132593c5caa563705a4748ba0f04a853c), [`810f222`](https://github.com/Effect-TS/effect/commit/810f222268792b13067c7a7bf317b93a9bb8917b), [`596aaea`](https://github.com/Effect-TS/effect/commit/596aaea022648b2e06fb1ec22f1652043d6fe64e)]: + - effect@3.1.6 + +## 0.34.5 + +### Patch Changes + +- [#2736](https://github.com/Effect-TS/effect/pull/2736) [`40c2b1d`](https://github.com/Effect-TS/effect/commit/40c2b1d9234bcfc9ab4039282b621ca092f800cd) Thanks [@patroza](https://github.com/patroza)! - update otel dependencies + +- Updated dependencies [[`6ac4847`](https://github.com/Effect-TS/effect/commit/6ac48479447c01a4f35d655552af93e47e562610)]: + - effect@3.1.5 + +## 0.34.4 + +### Patch Changes + +- Updated dependencies [[`e41e911`](https://github.com/Effect-TS/effect/commit/e41e91122fa6dd12fc81e50dcad0db891be67146)]: + - effect@3.1.4 + +## 0.34.3 + +### Patch Changes + +- Updated dependencies [[`1f6dc96`](https://github.com/Effect-TS/effect/commit/1f6dc96f51c7bb9c8d11415358308604ba7c7c8e)]: + - effect@3.1.3 + +## 0.34.2 + +### Patch Changes + +- Updated dependencies [[`2e1cdf6`](https://github.com/Effect-TS/effect/commit/2e1cdf67d141281288fffe9a5c10d1379a800513)]: + - effect@3.1.2 + +## 0.34.1 + +### Patch Changes + +- Updated dependencies [[`e5e56d1`](https://github.com/Effect-TS/effect/commit/e5e56d138dbed3204636f605229c6685f89659fc)]: + - effect@3.1.1 + +## 0.34.0 + +### Minor Changes + +- [#2543](https://github.com/Effect-TS/effect/pull/2543) [`a023f28`](https://github.com/Effect-TS/effect/commit/a023f28336f3865687d9a30c1883e36909906d85) Thanks [@github-actions](https://github.com/apps/github-actions)! - add `kind` property to `Tracer.Span` + + This can be used to specify what kind of service created the span. + +### Patch Changes + +- Updated dependencies [[`c3c12c6`](https://github.com/Effect-TS/effect/commit/c3c12c6625633fe80e79f9db75a3b8cf8ca8b11d), [`ba64ea6`](https://github.com/Effect-TS/effect/commit/ba64ea6757810c5e74cad3863a7d19d4d38af66b), [`b5de2d2`](https://github.com/Effect-TS/effect/commit/b5de2d2ce5b1afe8be90827bf898a95cec40eb2b), [`a1c7ab8`](https://github.com/Effect-TS/effect/commit/a1c7ab8ffedacd18c1fc784f4ff5844f79498b83), [`a023f28`](https://github.com/Effect-TS/effect/commit/a023f28336f3865687d9a30c1883e36909906d85), [`1c9454d`](https://github.com/Effect-TS/effect/commit/1c9454d532eae79b9f759aea77f59332cc6d18ed), [`92d56db`](https://github.com/Effect-TS/effect/commit/92d56dbb3f33e36636c2a2f1030c56492e39cf4d)]: + - effect@3.1.0 + +## 0.33.5 + +### Patch Changes + +- [#2656](https://github.com/Effect-TS/effect/pull/2656) [`557707b`](https://github.com/Effect-TS/effect/commit/557707bc9e5f230c8964d2757012075c34339b5c) Thanks [@tim-smart](https://github.com/tim-smart)! - update dependencies + +- Updated dependencies [[`557707b`](https://github.com/Effect-TS/effect/commit/557707bc9e5f230c8964d2757012075c34339b5c), [`f4ed306`](https://github.com/Effect-TS/effect/commit/f4ed3068a70b50302d078a30d18ca3cfd2bc679c), [`661004f`](https://github.com/Effect-TS/effect/commit/661004f4bf5f8b25f5a0678c21a3a822188ce461), [`e79cb83`](https://github.com/Effect-TS/effect/commit/e79cb83d3b19098bc40a3012e2a059b8426306c2)]: + - effect@3.0.8 + +## 0.33.4 + +### Patch Changes + +- Updated dependencies [[`18de56b`](https://github.com/Effect-TS/effect/commit/18de56b4a6b6d1f99230dfabf9147d59ea4dd759)]: + - effect@3.0.7 + +## 0.33.3 + +### Patch Changes + +- [#2623](https://github.com/Effect-TS/effect/pull/2623) [`8492f5b`](https://github.com/Effect-TS/effect/commit/8492f5b944459fc966807202eccc9b7802799e6f) Thanks [@tim-smart](https://github.com/tim-smart)! - allow for multiple otel span processors & metric readers + +- Updated dependencies [[`ffe4f4e`](https://github.com/Effect-TS/effect/commit/ffe4f4e95db35fff6869e360b072e3837befa0a1), [`027418e`](https://github.com/Effect-TS/effect/commit/027418edaa6aa6c0ae4861b95832827b45adace4), [`ac1898e`](https://github.com/Effect-TS/effect/commit/ac1898eb7bc96880f911c276048e2ea3d6fe9c50), [`ffe4f4e`](https://github.com/Effect-TS/effect/commit/ffe4f4e95db35fff6869e360b072e3837befa0a1)]: + - effect@3.0.6 + +## 0.33.2 + +### Patch Changes + +- Updated dependencies [[`6222404`](https://github.com/Effect-TS/effect/commit/62224044678751829ed2f128e05133a91c6b0569), [`868ed2a`](https://github.com/Effect-TS/effect/commit/868ed2a8fe94ee7f4206a6070f29dcf2a5ba1dc3)]: + - effect@3.0.5 + +## 0.33.1 + +### Patch Changes + +- Updated dependencies [[`9a24667`](https://github.com/Effect-TS/effect/commit/9a246672008a2b668d43fbfd2fe5508c54b2b920)]: + - effect@3.0.4 + +## 0.33.0 + +### Minor Changes + +- [#2572](https://github.com/Effect-TS/effect/pull/2572) [`c8c798a`](https://github.com/Effect-TS/effect/commit/c8c798a46a193fa678194b31fe2af99048fe71e0) Thanks [@nickrttn](https://github.com/nickrttn)! - Update @opentelemetry/\* peer dependencies to ensure allowed versions include used imports + +### Patch Changes + +- [#2575](https://github.com/Effect-TS/effect/pull/2575) [`d2ebe0e`](https://github.com/Effect-TS/effect/commit/d2ebe0e48aae248ac014fb976fa2d7bf3038394d) Thanks [@tim-smart](https://github.com/tim-smart)! - add otel Resource.layerFromEnv, for constructing a resource from env variables + +## 0.32.3 + +### Patch Changes + +- Updated dependencies [[`a7b4b84`](https://github.com/Effect-TS/effect/commit/a7b4b84bd5a25f51aba922f9259c3a58c98c6a4e)]: + - effect@3.0.3 + +## 0.32.2 + +### Patch Changes + +- [#2562](https://github.com/Effect-TS/effect/pull/2562) [`2cecdbd`](https://github.com/Effect-TS/effect/commit/2cecdbd1cf30befce4e84796ccd953ea55ecfb86) Thanks [@fubhy](https://github.com/fubhy)! - Added provenance publishing + +- Updated dependencies [[`2cecdbd`](https://github.com/Effect-TS/effect/commit/2cecdbd1cf30befce4e84796ccd953ea55ecfb86)]: + - effect@3.0.2 + +## 0.32.1 + +### Patch Changes + +- [#2555](https://github.com/Effect-TS/effect/pull/2555) [`8edacca`](https://github.com/Effect-TS/effect/commit/8edacca37f8e37c01a63fec332b06d9361efaa7b) Thanks [@tim-smart](https://github.com/tim-smart)! - prevent use of `Array` as import name to solve bundler issues + +- Updated dependencies [[`3da0cfa`](https://github.com/Effect-TS/effect/commit/3da0cfa12c407fd930dc480be1ecc9217a8058f8), [`570e8d8`](https://github.com/Effect-TS/effect/commit/570e8d87e7c0e9ad4cd2686462fdb9b4812f7716), [`8edacca`](https://github.com/Effect-TS/effect/commit/8edacca37f8e37c01a63fec332b06d9361efaa7b)]: + - effect@3.0.1 + +## 0.32.0 + +### Minor Changes + +- [#2207](https://github.com/Effect-TS/effect/pull/2207) [`5a2314b`](https://github.com/Effect-TS/effect/commit/5a2314b70ec79c2c02b51cef45a5ddec8327daa1) Thanks [@github-actions](https://github.com/apps/github-actions)! - replace use of `unit` terminology with `void` + + For all the data types. + + ```ts + Effect.unit // => Effect.void + Stream.unit // => Stream.void + + // etc + ``` + +- [#2207](https://github.com/Effect-TS/effect/pull/2207) [`2fb7d9c`](https://github.com/Effect-TS/effect/commit/2fb7d9ca15037ff62a578bb9fe5732da5f4f317d) Thanks [@github-actions](https://github.com/apps/github-actions)! - Release Effect 3.0 🎉 + +### Patch Changes + +- [#2529](https://github.com/Effect-TS/effect/pull/2529) [`78b767c`](https://github.com/Effect-TS/effect/commit/78b767c2b1625186e17131761a0edbac25d21850) Thanks [@fubhy](https://github.com/fubhy)! - Renamed `ReadonlyArray` and `ReadonlyRecord` modules for better discoverability. + +- Updated dependencies [[`1b5f0c7`](https://github.com/Effect-TS/effect/commit/1b5f0c77e7fd477a0026071e82129a948227f4b3), [`d50a652`](https://github.com/Effect-TS/effect/commit/d50a652479f4d1d64f48da05c79fa847e6e51548), [`9a3bd47`](https://github.com/Effect-TS/effect/commit/9a3bd47ebd0750c7e498162734f6d21895de0cb2), [`be9d025`](https://github.com/Effect-TS/effect/commit/be9d025e42355260ace02dd135851a8935a4deba), [`78b767c`](https://github.com/Effect-TS/effect/commit/78b767c2b1625186e17131761a0edbac25d21850), [`1499974`](https://github.com/Effect-TS/effect/commit/14999741d2e19c1747f6a7e19d68977f6429cdb8), [`1b5f0c7`](https://github.com/Effect-TS/effect/commit/1b5f0c77e7fd477a0026071e82129a948227f4b3), [`5c2b561`](https://github.com/Effect-TS/effect/commit/5c2b5614f583b88784ed68126ae939832fb3c092), [`a18f594`](https://github.com/Effect-TS/effect/commit/a18f5948f1439a147232448b2c443472fda0eceb), [`1499974`](https://github.com/Effect-TS/effect/commit/14999741d2e19c1747f6a7e19d68977f6429cdb8), [`2f96d93`](https://github.com/Effect-TS/effect/commit/2f96d938b90f8c19377583279e3c7afd9b509c50), [`5a2314b`](https://github.com/Effect-TS/effect/commit/5a2314b70ec79c2c02b51cef45a5ddec8327daa1), [`271b79f`](https://github.com/Effect-TS/effect/commit/271b79fc0b66a6c11e07a8779ff8800493a7eac2), [`1b5f0c7`](https://github.com/Effect-TS/effect/commit/1b5f0c77e7fd477a0026071e82129a948227f4b3), [`2fb7d9c`](https://github.com/Effect-TS/effect/commit/2fb7d9ca15037ff62a578bb9fe5732da5f4f317d), [`53d1c2a`](https://github.com/Effect-TS/effect/commit/53d1c2a77559081fbb89667e343346375c6d6650), [`e7e1bbe`](https://github.com/Effect-TS/effect/commit/e7e1bbe68486fdf31c8f84b0880522d39adcaad3), [`10c169e`](https://github.com/Effect-TS/effect/commit/10c169eadc874e91b4defca3f467b4e6a50fd8f3), [`6424181`](https://github.com/Effect-TS/effect/commit/64241815fe6a939e91e6947253e7dceea1306aa8)]: + - effect@3.0.0 + +## 0.31.29 + +### Patch Changes + +- Updated dependencies [[`41c8102`](https://github.com/Effect-TS/effect/commit/41c810228b1a50e4b41f19e735d7c62fe8d36871), [`776ef2b`](https://github.com/Effect-TS/effect/commit/776ef2bb66db9aa9f68b7beab14f6986f9c1288b), [`217147e`](https://github.com/Effect-TS/effect/commit/217147ea67c5c42c96f024775c41e5b070f81e4c), [`90776ec`](https://github.com/Effect-TS/effect/commit/90776ec8e8671d835b65fc33ead1de6c864b81b9), [`8709856`](https://github.com/Effect-TS/effect/commit/870985694ae985c3cb9360ad8a25c60e6f785f55), [`232c353`](https://github.com/Effect-TS/effect/commit/232c353c2e6f743f38e57639ee30e324ffa9c2a9), [`0ca835c`](https://github.com/Effect-TS/effect/commit/0ca835cbac8e69072a93ace83b534219faba24e8), [`8709856`](https://github.com/Effect-TS/effect/commit/870985694ae985c3cb9360ad8a25c60e6f785f55), [`e983740`](https://github.com/Effect-TS/effect/commit/e9837401145605aff5bc2ec7e73004f397c5d2d1), [`e3e0924`](https://github.com/Effect-TS/effect/commit/e3e09247d46a35430fc60e4aa4032cc50814f212)]: + - effect@2.4.19 + +## 0.31.28 + +### Patch Changes + +- Updated dependencies [[`dadc690`](https://github.com/Effect-TS/effect/commit/dadc6906121c512bc32be22b52adbd1ada834594)]: + - effect@2.4.18 + +## 0.31.27 + +### Patch Changes + +- Updated dependencies [[`8fdfda6`](https://github.com/Effect-TS/effect/commit/8fdfda6618be848c01b399d13bc05a9a3adfb613), [`607b2e7`](https://github.com/Effect-TS/effect/commit/607b2e7a7fd9318c57acf4e50ec61747eea74ad7), [`8fdfda6`](https://github.com/Effect-TS/effect/commit/8fdfda6618be848c01b399d13bc05a9a3adfb613), [`8206caf`](https://github.com/Effect-TS/effect/commit/8206caf7c2d22c68be4313318b61cfdacf6222b6), [`7ddd654`](https://github.com/Effect-TS/effect/commit/7ddd65415b65ccb654ad04f4dbefe39402f15117), [`7ddd654`](https://github.com/Effect-TS/effect/commit/7ddd65415b65ccb654ad04f4dbefe39402f15117), [`8fdfda6`](https://github.com/Effect-TS/effect/commit/8fdfda6618be848c01b399d13bc05a9a3adfb613), [`f456ba2`](https://github.com/Effect-TS/effect/commit/f456ba273bae21a6dcf8c966c50c97b5f0897d9f)]: + - effect@2.4.17 + +## 0.31.26 + +### Patch Changes + +- [#2433](https://github.com/Effect-TS/effect/pull/2433) [`ee10a50`](https://github.com/Effect-TS/effect/commit/ee10a50da859abf7154636d6d3e59511f0e0f590) Thanks [@vecerek](https://github.com/vecerek)! - Exports an empty NodeSDK layer suitable for incremental adoption and unit testing purposes + +- Updated dependencies [[`5170ce7`](https://github.com/Effect-TS/effect/commit/5170ce708c606283e8a30d273950f1a21c7eddc2)]: + - effect@2.4.16 + +## 0.31.25 + +### Patch Changes + +- Updated dependencies [[`d7688c0`](https://github.com/Effect-TS/effect/commit/d7688c0c72717fe7876c871567f6946dabfc0546), [`b3a4fac`](https://github.com/Effect-TS/effect/commit/b3a4face2acaca422f0b0530436e8f13129f3b3a)]: + - effect@2.4.15 + +## 0.31.24 + +### Patch Changes + +- Updated dependencies [[`6180c0c`](https://github.com/Effect-TS/effect/commit/6180c0cc51dee785cfce72220a52c9fc3b9bf9aa)]: + - effect@2.4.14 + +## 0.31.23 + +### Patch Changes + +- Updated dependencies [[`3336287`](https://github.com/Effect-TS/effect/commit/3336287ff55a25e56d759b83847bfaa21c40f499), [`54b7c00`](https://github.com/Effect-TS/effect/commit/54b7c0077fa784ad2646b812d6a44641f672edcd), [`3336287`](https://github.com/Effect-TS/effect/commit/3336287ff55a25e56d759b83847bfaa21c40f499)]: + - effect@2.4.13 + +## 0.31.22 + +### Patch Changes + +- [#2385](https://github.com/Effect-TS/effect/pull/2385) [`3307729`](https://github.com/Effect-TS/effect/commit/3307729de162a033fa9caa8e14c111013dcf0d87) Thanks [@tim-smart](https://github.com/tim-smart)! - update typescript to 5.4 + +- Updated dependencies [[`3307729`](https://github.com/Effect-TS/effect/commit/3307729de162a033fa9caa8e14c111013dcf0d87)]: + - effect@2.4.12 + +## 0.31.21 + +### Patch Changes + +- [#2384](https://github.com/Effect-TS/effect/pull/2384) [`2f488c4`](https://github.com/Effect-TS/effect/commit/2f488c436de52576562803c57ebc132ef40ccdd8) Thanks [@tim-smart](https://github.com/tim-smart)! - update dependencies + +- Updated dependencies [[`2f488c4`](https://github.com/Effect-TS/effect/commit/2f488c436de52576562803c57ebc132ef40ccdd8), [`37ca592`](https://github.com/Effect-TS/effect/commit/37ca592a4101ad90adbf8c8b3f727faf3110cae5), [`317b5b8`](https://github.com/Effect-TS/effect/commit/317b5b8e8c8c2207469b3ebfcf72bf3a9f7cbc60)]: + - effect@2.4.11 + +## 0.31.20 + +### Patch Changes + +- [#2375](https://github.com/Effect-TS/effect/pull/2375) [`9bab1f9`](https://github.com/Effect-TS/effect/commit/9bab1f9fa5b999740755e4e82485cb77c638643a) Thanks [@tim-smart](https://github.com/tim-smart)! - Make @effect/opentelemetry metrics conform to the spec + - Metric labels add new data points instead of completely new metric data + - Start times are determined from the first occurrence of a metric + +- Updated dependencies [[`9bab1f9`](https://github.com/Effect-TS/effect/commit/9bab1f9fa5b999740755e4e82485cb77c638643a), [`9bbde5b`](https://github.com/Effect-TS/effect/commit/9bbde5be9a0168d1c2a0308bfc27167ed62f3968)]: + - effect@2.4.10 + +## 0.31.19 + +### Patch Changes + +- Updated dependencies [[`71fd528`](https://github.com/Effect-TS/effect/commit/71fd5287500f9ce155a7d9f0df6ee3e0ac3aeb99)]: + - effect@2.4.9 + +## 0.31.18 + +### Patch Changes + +- Updated dependencies [[`bb0b69e`](https://github.com/Effect-TS/effect/commit/bb0b69e519698c7c76aa68217de423c78ad16566), [`6b20bad`](https://github.com/Effect-TS/effect/commit/6b20badebb3a7ca4d38857753e8ecaa09d02ccfb), [`4e64e9b`](https://github.com/Effect-TS/effect/commit/4e64e9b9876de6bfcbabe39e18a91a08e5f3fbb0), [`3851a02`](https://github.com/Effect-TS/effect/commit/3851a022c481006aec1db36651e4b4fd727aa742), [`5f5fcd9`](https://github.com/Effect-TS/effect/commit/5f5fcd969ae30ed6fe61d566a571498d9e895e16), [`814e5b8`](https://github.com/Effect-TS/effect/commit/814e5b828f68210b9e8f336fd6ac688646835dd9)]: + - effect@2.4.8 + +## 0.31.17 + +### Patch Changes + +- Updated dependencies [[`eb93283`](https://github.com/Effect-TS/effect/commit/eb93283985913d7b04ca750e36ac8513e7b6cef6)]: + - effect@2.4.7 + +## 0.31.16 + +### Patch Changes + +- Updated dependencies [[`4f35a7e`](https://github.com/Effect-TS/effect/commit/4f35a7e7c4eba598924aff24d1158b9056bb24be), [`9971186`](https://github.com/Effect-TS/effect/commit/99711862722188fbb5ed3ee75126ad5edf13f72f)]: + - effect@2.4.6 + +## 0.31.15 + +### Patch Changes + +- Updated dependencies [[`bce21c5`](https://github.com/Effect-TS/effect/commit/bce21c5ded2177114666ba229bd5029fa000dee3), [`c7d3036`](https://github.com/Effect-TS/effect/commit/c7d303630b7f0825cb2e584557c5767a67214d9f)]: + - effect@2.4.5 + +## 0.31.14 + +### Patch Changes + +- Updated dependencies [[`5d47ee0`](https://github.com/Effect-TS/effect/commit/5d47ee0855e492532085b6092879b1b952d84949), [`817a04c`](https://github.com/Effect-TS/effect/commit/817a04cb2df0f4140984dc97eb3e1bb14a6c4a38), [`d90a99d`](https://github.com/Effect-TS/effect/commit/d90a99d03d074adc7cd2533f15419138264da5a2), [`dd05faa`](https://github.com/Effect-TS/effect/commit/dd05faa621555ef3585ecd914ac13ecd89b710f4), [`dd05faa`](https://github.com/Effect-TS/effect/commit/dd05faa621555ef3585ecd914ac13ecd89b710f4), [`802674b`](https://github.com/Effect-TS/effect/commit/802674b379b7559ad3ff09b33388891445a9e48b)]: + - effect@2.4.4 + +## 0.31.13 + +### Patch Changes + +- Updated dependencies [[`20e63fb`](https://github.com/Effect-TS/effect/commit/20e63fb9207210f3fe2d136ec40d0a2dbff3225e), [`20e63fb`](https://github.com/Effect-TS/effect/commit/20e63fb9207210f3fe2d136ec40d0a2dbff3225e)]: + - effect@2.4.3 + +## 0.31.12 + +### Patch Changes + +- Updated dependencies [[`e03811e`](https://github.com/Effect-TS/effect/commit/e03811e80c93e986e6348b3b67ac2ed6d5fefff0), [`ac41d84`](https://github.com/Effect-TS/effect/commit/ac41d84776484cdce8165b7ca2c9c9b6377eee2d), [`6137533`](https://github.com/Effect-TS/effect/commit/613753300c7705518ab1fea2f370b032851c2750), [`f373529`](https://github.com/Effect-TS/effect/commit/f373529999f4b8bc92b634f6ea14f19271388eed), [`1bf9f31`](https://github.com/Effect-TS/effect/commit/1bf9f31f07667de677673f7c29a4e7a26ebad3c8), [`e3ff789`](https://github.com/Effect-TS/effect/commit/e3ff789226f89e71eb28ca38ce79f90af6a03f1a), [`6137533`](https://github.com/Effect-TS/effect/commit/613753300c7705518ab1fea2f370b032851c2750), [`507ba40`](https://github.com/Effect-TS/effect/commit/507ba4060ff043c1a8d541dae723fa6940633b00), [`e466afe`](https://github.com/Effect-TS/effect/commit/e466afe32f2de598ceafd8982bd0cfbd388e5671), [`f373529`](https://github.com/Effect-TS/effect/commit/f373529999f4b8bc92b634f6ea14f19271388eed), [`de74eb8`](https://github.com/Effect-TS/effect/commit/de74eb80a79eebde5ff645033765e7a617e92f27)]: + - effect@2.4.2 + +## 0.31.11 + +### Patch Changes + +- Updated dependencies [[`a4a0006`](https://github.com/Effect-TS/effect/commit/a4a0006c7f19fc261df5cda16963d73457e4d6ac), [`0a37676`](https://github.com/Effect-TS/effect/commit/0a37676aa0eb2a21e17af2e6df9f81f52bbc8831), [`6f503b7`](https://github.com/Effect-TS/effect/commit/6f503b774d893bf2af34f66202e270d8c45d5f31)]: + - effect@2.4.1 + +## 0.31.10 + +### Patch Changes + +- Updated dependencies [[`5de7be5`](https://github.com/Effect-TS/effect/commit/5de7be5beca2e963b503e6029dcc3217848187d2), [`489fcf3`](https://github.com/Effect-TS/effect/commit/489fcf363ff2b2a953166b740cb9a62d7fc2a101), [`7d9c3bf`](https://github.com/Effect-TS/effect/commit/7d9c3bff6c18d451e0e4781042945ec5c7be1b9f), [`d8d278b`](https://github.com/Effect-TS/effect/commit/d8d278b2efb2966947029885e01f7b68348a021f), [`14c5711`](https://github.com/Effect-TS/effect/commit/14c57110078f0862b8da5c7a2c5d980f54447484), [`5de7be5`](https://github.com/Effect-TS/effect/commit/5de7be5beca2e963b503e6029dcc3217848187d2), [`b9cb3a9`](https://github.com/Effect-TS/effect/commit/b9cb3a9c9bfdd75536bd70b4e8b557c12d4923ff), [`585fcce`](https://github.com/Effect-TS/effect/commit/585fcce162d0f07a48d7cd984a9b722966fbebbe), [`93b412d`](https://github.com/Effect-TS/effect/commit/93b412d4a9ed762dc9fa5807e51fad0fc78a614a), [`55b26a6`](https://github.com/Effect-TS/effect/commit/55b26a6342b4826f1116e7a1eb660118c274458e), [`a025b12`](https://github.com/Effect-TS/effect/commit/a025b121235ba01cfce8d62a775491880c575561), [`2097739`](https://github.com/Effect-TS/effect/commit/20977393d2383bff709304e81ec7d51cafd57108)]: + - effect@2.4.0 + +## 0.31.9 + +### Patch Changes + +- [#2187](https://github.com/Effect-TS/effect/pull/2187) [`e6d36c0`](https://github.com/Effect-TS/effect/commit/e6d36c0813d836f17eabb6a9c7849baffca12dbf) Thanks [@tim-smart](https://github.com/tim-smart)! - update development dependencies + +- Updated dependencies [[`5ad2eec`](https://github.com/Effect-TS/effect/commit/5ad2eece0280b6db6a749d25cac1dcf6d33659a9), [`e6d36c0`](https://github.com/Effect-TS/effect/commit/e6d36c0813d836f17eabb6a9c7849baffca12dbf)]: + - effect@2.3.8 + +## 0.31.8 + +### Patch Changes + +- Updated dependencies [[`bc8404d`](https://github.com/Effect-TS/effect/commit/bc8404d54fd42072d200c0399cb39672837afa9f), [`2c5cbcd`](https://github.com/Effect-TS/effect/commit/2c5cbcd1161b4f40dab184999291e817314107de), [`6565916`](https://github.com/Effect-TS/effect/commit/6565916ef254bf910e47d25fd0ef55e7cb420241)]: + - effect@2.3.7 + +## 0.31.7 + +### Patch Changes + +- Updated dependencies [[`b1163b2`](https://github.com/Effect-TS/effect/commit/b1163b2bd67b65bafbbb39fc4c67576e5cbaf444), [`b46b869`](https://github.com/Effect-TS/effect/commit/b46b869e59a6da5aa235a9fcc25e1e0d24e9e8f8), [`de1b226`](https://github.com/Effect-TS/effect/commit/de1b226282b5ab6c2809dd93f3bdb066f24a1333), [`a663390`](https://github.com/Effect-TS/effect/commit/a66339090ae7b960f8a8b90a0dcdc505de5aaf3e), [`ff88f80`](https://github.com/Effect-TS/effect/commit/ff88f808c4ed9947a148045849e7410b00acad0a), [`11be07b`](https://github.com/Effect-TS/effect/commit/11be07bf65d82cfdf994cdb9d8ca937f995cb4f0), [`c568645`](https://github.com/Effect-TS/effect/commit/c5686451c87d26382135a1c63b00ef171bb24f62), [`88835e5`](https://github.com/Effect-TS/effect/commit/88835e575a0bfbeff9a3696a332f32192c940e12), [`b415577`](https://github.com/Effect-TS/effect/commit/b415577f6c576073733929c858e5aac27b6d5880), [`ff8046f`](https://github.com/Effect-TS/effect/commit/ff8046f57dfd073eba60ce6d3144ab060fbf93ce)]: + - effect@2.3.6 + +## 0.31.6 + +### Patch Changes + +- [#2131](https://github.com/Effect-TS/effect/pull/2131) [`9ee4de0`](https://github.com/Effect-TS/effect/commit/9ee4de01bb7edeefaa20cdd470e85f3c539d858f) Thanks [@tim-smart](https://github.com/tim-smart)! - ignore errors when attempting to shutdown otel + +## 0.31.5 + +### Patch Changes + +- Updated dependencies [[`b881365`](https://github.com/Effect-TS/effect/commit/b8813650355322ea2fc1fbaa4f846bd87a7a05f3)]: + - effect@2.3.5 + +## 0.31.4 + +### Patch Changes + +- Updated dependencies [[`17bda66`](https://github.com/Effect-TS/effect/commit/17bda66431c999a546920c10adb205e6c8bea7d1)]: + - effect@2.3.4 + +## 0.31.3 + +### Patch Changes + +- Updated dependencies [[`efd41d8`](https://github.com/Effect-TS/effect/commit/efd41d8131c3d90867608969ef7c4eef490eb5e6), [`0f83515`](https://github.com/Effect-TS/effect/commit/0f83515a9c01d13c7c15a3f026e02d22c3c6bb7f), [`0f83515`](https://github.com/Effect-TS/effect/commit/0f83515a9c01d13c7c15a3f026e02d22c3c6bb7f)]: + - effect@2.3.3 + +## 0.31.2 + +### Patch Changes + +- Updated dependencies [[`6654f5f`](https://github.com/Effect-TS/effect/commit/6654f5f0f6b9d97165ede5e04ca16776e2599328), [`2eb11b4`](https://github.com/Effect-TS/effect/commit/2eb11b47752cedf233ef4c4395d9c4efc9b9e180), [`56c09bd`](https://github.com/Effect-TS/effect/commit/56c09bd369279a6a7785209d172739935818cba6), [`71aa5b1`](https://github.com/Effect-TS/effect/commit/71aa5b1c180dcb8b53aefe232d12a97bd06b5447), [`1700af8`](https://github.com/Effect-TS/effect/commit/1700af8af1131602887da721914c8562b6342393)]: + - effect@2.3.2 + +## 0.31.1 + +### Patch Changes + +- Updated dependencies [[`b5a8215`](https://github.com/Effect-TS/effect/commit/b5a8215ee2a97a8865d69ee55ce1b9835948c922)]: + - effect@2.3.1 + +## 0.31.0 + +### Minor Changes + +- [#2006](https://github.com/Effect-TS/effect/pull/2006) [`9a2d1c1`](https://github.com/Effect-TS/effect/commit/9a2d1c1468ea0789b34767ad683da074f061ea9c) Thanks [@github-actions](https://github.com/apps/github-actions)! - With this change we now require a string key to be provided for all tags and renames the dear old `Tag` to `GenericTag`, so when previously you could do: + + ```ts + import { Effect, Context } from "effect" + interface Service { + readonly _: unique symbol + } + const Service = Context.Tag< + Service, + { + number: Effect.Effect + } + >() + ``` + + you are now mandated to do: + + ```ts + import { Effect, Context } from "effect" + interface Service { + readonly _: unique symbol + } + const Service = Context.GenericTag< + Service, + { + number: Effect.Effect + } + >("Service") + ``` + + This makes by default all tags globals and ensures better debuggaility when unexpected errors arise. + + Furthermore we introduce a new way of constructing tags that should be considered the new default: + + ```ts + import { Effect, Context } from "effect" + class Service extends Context.Tag("Service")< + Service, + { + number: Effect.Effect + } + >() {} + + const program = Effect.flatMap(Service, ({ number }) => number).pipe( + Effect.flatMap((_) => Effect.log(`number: ${_}`)) + ) + ``` + + this will use "Service" as the key and will create automatically an opaque identifier (the class) to be used at the type level, it does something similar to the above in a single shot. + +- [#2006](https://github.com/Effect-TS/effect/pull/2006) [`1a77f72`](https://github.com/Effect-TS/effect/commit/1a77f72cdaf43d6cdc91b6060f82832edcdbbcb3) Thanks [@github-actions](https://github.com/apps/github-actions)! - change `Effect` type parameters order from `Effect` to `Effect` + +- [#2006](https://github.com/Effect-TS/effect/pull/2006) [`9a2d1c1`](https://github.com/Effect-TS/effect/commit/9a2d1c1468ea0789b34767ad683da074f061ea9c) Thanks [@github-actions](https://github.com/apps/github-actions)! - This change enables `Effect.serviceConstants` and `Effect.serviceMembers` to access any constant in the service, not only the effects, namely it is now possible to do: + + ```ts + import { Effect, Context } from "effect" + + class NumberRepo extends Context.TagClass("NumberRepo")< + NumberRepo, + { + readonly numbers: Array + } + >() { + static numbers = Effect.serviceConstants(NumberRepo).numbers + } + ``` + +### Patch Changes + +- Updated dependencies [[`96bcee2`](https://github.com/Effect-TS/effect/commit/96bcee21021aecd8ffd86440a2c9be353c4668e3), [`96bcee2`](https://github.com/Effect-TS/effect/commit/96bcee21021aecd8ffd86440a2c9be353c4668e3), [`c77f635`](https://github.com/Effect-TS/effect/commit/c77f635f8a26ca6d83cb569d911f8eee79033fd9), [`e343a74`](https://github.com/Effect-TS/effect/commit/e343a74843dd9edf879417fa94cb51de7ed5b402), [`acf1894`](https://github.com/Effect-TS/effect/commit/acf1894f45945dbe5c39451e36aabb4b5092f257), [`9a2d1c1`](https://github.com/Effect-TS/effect/commit/9a2d1c1468ea0789b34767ad683da074f061ea9c), [`1a77f72`](https://github.com/Effect-TS/effect/commit/1a77f72cdaf43d6cdc91b6060f82832edcdbbcb3), [`c986f0e`](https://github.com/Effect-TS/effect/commit/c986f0e0ce4d22ba08177ed351152718479ab63c), [`96bcee2`](https://github.com/Effect-TS/effect/commit/96bcee21021aecd8ffd86440a2c9be353c4668e3), [`70dde23`](https://github.com/Effect-TS/effect/commit/70dde238f81125e353fd7bde5fc24ecd8969bf97), [`81b7425`](https://github.com/Effect-TS/effect/commit/81b7425320cbbe2a6cf547a3e3ab3549cdba14cf), [`02c3461`](https://github.com/Effect-TS/effect/commit/02c34615d02f91269ea04036d0306fccf4e39e18), [`0e56e99`](https://github.com/Effect-TS/effect/commit/0e56e998ab9815c4d096c239a553cb86a0f99af9), [`8b0ded9`](https://github.com/Effect-TS/effect/commit/8b0ded9f10ba0d96fcb9af24eff2dbd9341f85e3), [`8dd83e8`](https://github.com/Effect-TS/effect/commit/8dd83e854bfcaa6dab876994c5f813dcfb486c28), [`5127afe`](https://github.com/Effect-TS/effect/commit/5127afec1c519e0a3d7460844a9101a96272f29e), [`d75f6fe`](https://github.com/Effect-TS/effect/commit/d75f6fe6499deb0a5ee9ec94af3b5fd4eb03a2d0), [`7356e5c`](https://github.com/Effect-TS/effect/commit/7356e5cc16e9d70f18c02dee1dcb4ad539fd130a), [`3077cde`](https://github.com/Effect-TS/effect/commit/3077cde08a60246821a940964a84dd7f7c8b9f54), [`be19ce0`](https://github.com/Effect-TS/effect/commit/be19ce0b8bdf1fac80bb8d7e0b06a86986b47409), [`4a5d01a`](https://github.com/Effect-TS/effect/commit/4a5d01a409e9b6dd53893e65f8e5c9247f568021), [`78f47ab`](https://github.com/Effect-TS/effect/commit/78f47abfe3cb0a8bbde818b1c5fc603270538b47), [`52e5d20`](https://github.com/Effect-TS/effect/commit/52e5d2077582bf51f25861c7139fc920c2c24166), [`c6137ec`](https://github.com/Effect-TS/effect/commit/c6137ec62c6b5542d5062ae1a3c936cb915dee22), [`f5ae081`](https://github.com/Effect-TS/effect/commit/f5ae08195e68e76faeac258c565d79da4e01e7d6), [`4a5d01a`](https://github.com/Effect-TS/effect/commit/4a5d01a409e9b6dd53893e65f8e5c9247f568021), [`60686f5`](https://github.com/Effect-TS/effect/commit/60686f5c38bef1b93a3a0dda9b6596d46aceab03), [`9a2d1c1`](https://github.com/Effect-TS/effect/commit/9a2d1c1468ea0789b34767ad683da074f061ea9c), [`5127afe`](https://github.com/Effect-TS/effect/commit/5127afec1c519e0a3d7460844a9101a96272f29e), [`8ee2931`](https://github.com/Effect-TS/effect/commit/8ee293159b4f7cb7af8558287a0a047f3a69743d), [`6727474`](https://github.com/Effect-TS/effect/commit/672747497490a30d36dd49c06db19aabf09dc7f0), [`5127afe`](https://github.com/Effect-TS/effect/commit/5127afec1c519e0a3d7460844a9101a96272f29e)]: + - effect@2.3.0 + +## 0.30.14 + +### Patch Changes + +- Updated dependencies [[`3ddfdbf`](https://github.com/Effect-TS/effect/commit/3ddfdbf914edea536aef207cec6695f33496258c), [`3ddfdbf`](https://github.com/Effect-TS/effect/commit/3ddfdbf914edea536aef207cec6695f33496258c)]: + - effect@2.2.5 + +## 0.30.13 + +### Patch Changes + +- Updated dependencies [[`d0b911c`](https://github.com/Effect-TS/effect/commit/d0b911c75f284c7aa87f25aa96926e6bde7690d0), [`330e1a4`](https://github.com/Effect-TS/effect/commit/330e1a4e2c1fc0af6c80c80c81dd38c3e50fab78), [`6928a2b`](https://github.com/Effect-TS/effect/commit/6928a2b0bae86a4bdfbece0aa32924207c2d5a70), [`296bc1c`](https://github.com/Effect-TS/effect/commit/296bc1c9d24986d299d2669115d584cb27b73c60)]: + - effect@2.2.4 + +## 0.30.12 + +### Patch Changes + +- Updated dependencies [[`22794e0`](https://github.com/Effect-TS/effect/commit/22794e0ba00e40281f30a22fa84412003c24877d), [`f73e6c0`](https://github.com/Effect-TS/effect/commit/f73e6c033fb0729a9cfa5eb4bc39f79d3126e247)]: + - effect@2.2.3 + +## 0.30.11 + +### Patch Changes + +- Updated dependencies [[`d404561`](https://github.com/Effect-TS/effect/commit/d404561e47ec2fa5f68709a308ee5d2ee959141d), [`7b84a3c`](https://github.com/Effect-TS/effect/commit/7b84a3c7e4b9c8dc02294b0e3cc3ae3becea977b)]: + - effect@2.2.2 + +## 0.30.10 + +### Patch Changes + +- Updated dependencies [[`84da31f`](https://github.com/Effect-TS/effect/commit/84da31f0643e8651b9d311b30526b1e4edfbdfb8), [`645bea2`](https://github.com/Effect-TS/effect/commit/645bea2551129f94a5b0e38347e28067dee531bb)]: + - effect@2.2.1 + +## 0.30.9 + +### Patch Changes + +- Updated dependencies [[`202befc`](https://github.com/Effect-TS/effect/commit/202befc2ecbeb117c4fa85ef9b12a3d3a48273d2), [`ee4ff8a`](https://github.com/Effect-TS/effect/commit/ee4ff8a943141fcf2877af92c5877ee87a989fb9), [`ee4ff8a`](https://github.com/Effect-TS/effect/commit/ee4ff8a943141fcf2877af92c5877ee87a989fb9), [`ee4ff8a`](https://github.com/Effect-TS/effect/commit/ee4ff8a943141fcf2877af92c5877ee87a989fb9), [`ee4ff8a`](https://github.com/Effect-TS/effect/commit/ee4ff8a943141fcf2877af92c5877ee87a989fb9), [`10df798`](https://github.com/Effect-TS/effect/commit/10df798639e556f9d88265ef7fc3cf8a3bbe3874)]: + - effect@2.2.0 + +## 0.30.8 + +### Patch Changes + +- Updated dependencies [[`21b9edd`](https://github.com/Effect-TS/effect/commit/21b9edde464f7c5624ef54ad1b5e264204a37625)]: + - effect@2.1.2 + +## 0.30.7 + +### Patch Changes + +- Updated dependencies [[`a222524`](https://github.com/Effect-TS/effect/commit/a2225247e9de2e013d287320790fde88c081dbbd)]: + - effect@2.1.1 + +## 0.30.6 + +### Patch Changes + +- Updated dependencies [[`05c44b3`](https://github.com/Effect-TS/effect/commit/05c44b30662554dde50b70bad79f13ae895fda02), [`05c44b3`](https://github.com/Effect-TS/effect/commit/05c44b30662554dde50b70bad79f13ae895fda02), [`05c44b3`](https://github.com/Effect-TS/effect/commit/05c44b30662554dde50b70bad79f13ae895fda02), [`05c44b3`](https://github.com/Effect-TS/effect/commit/05c44b30662554dde50b70bad79f13ae895fda02), [`05c44b3`](https://github.com/Effect-TS/effect/commit/05c44b30662554dde50b70bad79f13ae895fda02), [`05c44b3`](https://github.com/Effect-TS/effect/commit/05c44b30662554dde50b70bad79f13ae895fda02)]: + - effect@2.1.0 + +## 0.30.5 + +### Patch Changes + +- Updated dependencies [[`f7f19f6`](https://github.com/Effect-TS/effect/commit/f7f19f66a5fa349baa2412c1f9f15111c437df09)]: + - effect@2.0.5 + +## 0.30.4 + +### Patch Changes + +- Updated dependencies [[`536c1df`](https://github.com/Effect-TS/effect/commit/536c1dfb7833961dfb2fbd6bcd2dbdfa2f208d51), [`8eec87e`](https://github.com/Effect-TS/effect/commit/8eec87e311ce55281a98517e6df0ef103b43e8a8), [`540b294`](https://github.com/Effect-TS/effect/commit/540b2941dd0a81e9688311583ce7e2e140d6e7a5), [`536c1df`](https://github.com/Effect-TS/effect/commit/536c1dfb7833961dfb2fbd6bcd2dbdfa2f208d51), [`a3f96d6`](https://github.com/Effect-TS/effect/commit/a3f96d615b8b3e238dbfa01ef713c87e6f4532be), [`8eec87e`](https://github.com/Effect-TS/effect/commit/8eec87e311ce55281a98517e6df0ef103b43e8a8), [`536c1df`](https://github.com/Effect-TS/effect/commit/536c1dfb7833961dfb2fbd6bcd2dbdfa2f208d51), [`8eec87e`](https://github.com/Effect-TS/effect/commit/8eec87e311ce55281a98517e6df0ef103b43e8a8), [`8eec87e`](https://github.com/Effect-TS/effect/commit/8eec87e311ce55281a98517e6df0ef103b43e8a8), [`25adce7`](https://github.com/Effect-TS/effect/commit/25adce7ae76ce834096dca1ed70a60ad1a349217), [`536c1df`](https://github.com/Effect-TS/effect/commit/536c1dfb7833961dfb2fbd6bcd2dbdfa2f208d51)]: + - effect@2.0.4 + +## 0.30.3 + +### Patch Changes + +- Updated dependencies [[`87f7ef2`](https://github.com/Effect-TS/effect/commit/87f7ef28a3c27e2e4f2fcfa465f85bb2a45a3d6b), [`1d3a06b`](https://github.com/Effect-TS/effect/commit/1d3a06bb58ad1ac123ae8f9d42b4345f9c9c53c0)]: + - effect@2.0.3 + +## 0.30.2 + +### Patch Changes + +- Updated dependencies [[`d5a1949`](https://github.com/Effect-TS/effect/commit/d5a19499aac7c1d147674a35ac69992177c7536c)]: + - effect@2.0.2 + +## 0.30.1 + +### Patch Changes + +- Updated dependencies [[`16bd87d`](https://github.com/Effect-TS/effect/commit/16bd87d32611b966dc42ea4fc979764f97a49071)]: + - effect@2.0.1 + +## 0.30.0 + +### Minor Changes + +- [`d0471ca`](https://github.com/Effect-TS/effect/commit/d0471ca7b544746674b9e1750202da72b0a21233) Thanks [@mikearnaldi](https://github.com/mikearnaldi)! - Switch to monorepo structure + +- [#1846](https://github.com/Effect-TS/effect/pull/1846) [`693b8f3`](https://github.com/Effect-TS/effect/commit/693b8f3a3dfd43ae61f0d9292cdf356be7329f2f) Thanks [@fubhy](https://github.com/fubhy)! - Enabled `exactOptionalPropertyTypes` throughout + +### Patch Changes + +- [`d987daa`](https://github.com/Effect-TS/effect/commit/d987daafaddd43b6ade74916a08236c19ea0a9fa) Thanks [@mikearnaldi](https://github.com/mikearnaldi)! - Switch effect dependency to caret + +- [#1847](https://github.com/Effect-TS/effect/pull/1847) [`bcf0900`](https://github.com/Effect-TS/effect/commit/bcf0900b58f449262556f80bff21e771a37272aa) Thanks [@fubhy](https://github.com/fubhy)! - Avoid inline creation & spreading of objects and arrays + +- Updated dependencies [[`d987daa`](https://github.com/Effect-TS/effect/commit/d987daafaddd43b6ade74916a08236c19ea0a9fa), [`7b5eaa3`](https://github.com/Effect-TS/effect/commit/7b5eaa3838c79bf4bdccf91b94d61bbc38a2ec95), [`0724211`](https://github.com/Effect-TS/effect/commit/072421149c36010748ff6b6ee19c15c6cffefe09), [`9f2bc5a`](https://github.com/Effect-TS/effect/commit/9f2bc5a19e0b678a0a85e84daac290922b0fd57d), [`04fb8b4`](https://github.com/Effect-TS/effect/commit/04fb8b428b19bba85a2c79910c5e363340d074e7), [`d0471ca`](https://github.com/Effect-TS/effect/commit/d0471ca7b544746674b9e1750202da72b0a21233), [`bcf0900`](https://github.com/Effect-TS/effect/commit/bcf0900b58f449262556f80bff21e771a37272aa), [`6299b84`](https://github.com/Effect-TS/effect/commit/6299b84c11e5d1fe79fa538df8935018c7613747)]: + - effect@2.0.0 + +## 0.29.0 + +### Minor Changes + +- [#106](https://github.com/Effect-TS/opentelemetry/pull/106) [`d0fb6b6`](https://github.com/Effect-TS/opentelemetry/commit/d0fb6b6aa18c9c0021cda1a421492c8ba1cb5400) Thanks [@tim-smart](https://github.com/tim-smart)! - update effect + +## 0.28.0 + +### Minor Changes + +- [#104](https://github.com/Effect-TS/opentelemetry/pull/104) [`29484a9`](https://github.com/Effect-TS/opentelemetry/commit/29484a979e1a72e5099cf935f3f2c75624e58f5c) Thanks [@tim-smart](https://github.com/tim-smart)! - update effect + +## 0.27.0 + +### Minor Changes + +- [#102](https://github.com/Effect-TS/opentelemetry/pull/102) [`056e416`](https://github.com/Effect-TS/opentelemetry/commit/056e416edf7fda5bce8ff89a05672c894c47a332) Thanks [@tim-smart](https://github.com/tim-smart)! - update effect + +## 0.26.0 + +### Minor Changes + +- [#100](https://github.com/Effect-TS/opentelemetry/pull/100) [`091e0e5`](https://github.com/Effect-TS/opentelemetry/commit/091e0e57342b09bcf58c91b8f4628c5d2e74b039) Thanks [@tim-smart](https://github.com/tim-smart)! - update effect + +## 0.25.1 + +### Patch Changes + +- [#98](https://github.com/Effect-TS/opentelemetry/pull/98) [`93b6fab`](https://github.com/Effect-TS/opentelemetry/commit/93b6fabe6167ceb15ea35e86d8539a8117d0e203) Thanks [@tim-smart](https://github.com/tim-smart)! - update effect + +## 0.25.0 + +### Minor Changes + +- [#96](https://github.com/Effect-TS/opentelemetry/pull/96) [`63b82a3`](https://github.com/Effect-TS/opentelemetry/commit/63b82a3768ec47a5b38ff2b0dc61c24c226c0e5f) Thanks [@tim-smart](https://github.com/tim-smart)! - update effect + +### Patch Changes + +- [#96](https://github.com/Effect-TS/opentelemetry/pull/96) [`63b82a3`](https://github.com/Effect-TS/opentelemetry/commit/63b82a3768ec47a5b38ff2b0dc61c24c226c0e5f) Thanks [@tim-smart](https://github.com/tim-smart)! - accept Effect's for sdk constructors + +## 0.24.0 + +### Minor Changes + +- [#94](https://github.com/Effect-TS/opentelemetry/pull/94) [`e1afffc`](https://github.com/Effect-TS/opentelemetry/commit/e1afffccee1638c42db0d61fc3dd47ee728cbf21) Thanks [@tim-smart](https://github.com/tim-smart)! - update effect + +## 0.23.0 + +### Minor Changes + +- [#92](https://github.com/Effect-TS/opentelemetry/pull/92) [`0a39315`](https://github.com/Effect-TS/opentelemetry/commit/0a39315de29d79488ab808db12f75ddf50098a79) Thanks [@tim-smart](https://github.com/tim-smart)! - update effect + +## 0.22.1 + +### Patch Changes + +- [#89](https://github.com/Effect-TS/opentelemetry/pull/89) [`97db04d`](https://github.com/Effect-TS/opentelemetry/commit/97db04dae7d7f5e97b79fff8ecd4e32c0486ec67) Thanks [@tim-smart](https://github.com/tim-smart)! - update effect + +## 0.22.0 + +### Minor Changes + +- [#86](https://github.com/Effect-TS/opentelemetry/pull/86) [`ac41f21`](https://github.com/Effect-TS/opentelemetry/commit/ac41f21b691299d1472f0ecfacca9ba274b71afb) Thanks [@fubhy](https://github.com/fubhy)! - Switch to peer dependencies + +- [#88](https://github.com/Effect-TS/opentelemetry/pull/88) [`10be3aa`](https://github.com/Effect-TS/opentelemetry/commit/10be3aa6ded8a028eec1a46830ab68bceaf41ca3) Thanks [@tim-smart](https://github.com/tim-smart)! - update effect + +## 0.21.1 + +### Patch Changes + +- [#84](https://github.com/Effect-TS/opentelemetry/pull/84) [`6553dac`](https://github.com/Effect-TS/opentelemetry/commit/6553dac0c28523e1e09bd5628159688a1aa7b00b) Thanks [@tim-smart](https://github.com/tim-smart)! - update effect + +## 0.21.0 + +### Minor Changes + +- [#82](https://github.com/Effect-TS/opentelemetry/pull/82) [`8e1b9f6`](https://github.com/Effect-TS/opentelemetry/commit/8e1b9f6509e108cc145313bc7dea8f4d1629cc53) Thanks [@tim-smart](https://github.com/tim-smart)! - add Resource config to sdk layers + +- [#83](https://github.com/Effect-TS/opentelemetry/pull/83) [`fab878b`](https://github.com/Effect-TS/opentelemetry/commit/fab878bbd99e9c7163f87ba5ef40986b3cb33eaf) Thanks [@tim-smart](https://github.com/tim-smart)! - update effect + +- [#79](https://github.com/Effect-TS/opentelemetry/pull/79) [`fca35c8`](https://github.com/Effect-TS/opentelemetry/commit/fca35c8cec7760b6e172fdb9890972f36dc2ebc5) Thanks [@tim-smart](https://github.com/tim-smart)! - use scoped TracerProvider + +- [#79](https://github.com/Effect-TS/opentelemetry/pull/79) [`fca35c8`](https://github.com/Effect-TS/opentelemetry/commit/fca35c8cec7760b6e172fdb9890972f36dc2ebc5) Thanks [@tim-smart](https://github.com/tim-smart)! - update Sdk apis + +### Patch Changes + +- [#79](https://github.com/Effect-TS/opentelemetry/pull/79) [`fca35c8`](https://github.com/Effect-TS/opentelemetry/commit/fca35c8cec7760b6e172fdb9890972f36dc2ebc5) Thanks [@tim-smart](https://github.com/tim-smart)! - add WebSdk module + +## 0.20.0 + +### Minor Changes + +- [#77](https://github.com/Effect-TS/opentelemetry/pull/77) [`03d1e6a`](https://github.com/Effect-TS/opentelemetry/commit/03d1e6ad53e125528f1093da7c2ac6b4007c4d6d) Thanks [@tim-smart](https://github.com/tim-smart)! - update effect + +## 0.19.0 + +### Minor Changes + +- [#75](https://github.com/Effect-TS/opentelemetry/pull/75) [`706479f`](https://github.com/Effect-TS/opentelemetry/commit/706479fcb2e31f0ed057038abbf17a47a966fba3) Thanks [@tim-smart](https://github.com/tim-smart)! - update effect + +## 0.18.0 + +### Minor Changes + +- [#72](https://github.com/Effect-TS/opentelemetry/pull/72) [`9e563ef`](https://github.com/Effect-TS/opentelemetry/commit/9e563ef249b0b556c03b7c3e1d9872ef9c54c8b3) Thanks [@tim-smart](https://github.com/tim-smart)! - update effect + +- [#74](https://github.com/Effect-TS/opentelemetry/pull/74) [`fe93fd2`](https://github.com/Effect-TS/opentelemetry/commit/fe93fd2b3e641563425fe38b56efb8c750018f02) Thanks [@tim-smart](https://github.com/tim-smart)! - use lazy arg for node sdk layer config + +## 0.17.0 + +### Minor Changes + +- [#70](https://github.com/Effect-TS/opentelemetry/pull/70) [`f1b1d03`](https://github.com/Effect-TS/opentelemetry/commit/f1b1d039b686a2a5733cc19b0fca3a3f7abaf2d8) Thanks [@gcanti](https://github.com/gcanti)! - update effect + +## 0.16.0 + +### Minor Changes + +- [#68](https://github.com/Effect-TS/opentelemetry/pull/68) [`6340117`](https://github.com/Effect-TS/opentelemetry/commit/6340117165cd08a36c0178cb6b48e5db86e745ea) Thanks [@tim-smart](https://github.com/tim-smart)! - update effect + +## 0.15.1 + +### Patch Changes + +- [#66](https://github.com/Effect-TS/opentelemetry/pull/66) [`09cf96b`](https://github.com/Effect-TS/opentelemetry/commit/09cf96b7fd29f38bcbb1fa16f736fc2f42f1a75f) Thanks [@tim-smart](https://github.com/tim-smart)! - fix root spans + +## 0.15.0 + +### Minor Changes + +- [#64](https://github.com/Effect-TS/opentelemetry/pull/64) [`4b0608e`](https://github.com/Effect-TS/opentelemetry/commit/4b0608e50b9ab6406bf69ec12ae1d138bebe5497) Thanks [@tim-smart](https://github.com/tim-smart)! - update effect + +## 0.14.0 + +### Minor Changes + +- [#61](https://github.com/Effect-TS/opentelemetry/pull/61) [`7d1898b`](https://github.com/Effect-TS/opentelemetry/commit/7d1898b7efbab4aa6a7e43999591629cb67f80bd) Thanks [@tim-smart](https://github.com/tim-smart)! - switch to preconstruct for builds + +## 0.13.1 + +### Patch Changes + +- [#57](https://github.com/Effect-TS/opentelemetry/pull/57) [`d857912`](https://github.com/Effect-TS/opentelemetry/commit/d857912359f0a842ac56bb6cfdee97b1badde479) Thanks [@tim-smart](https://github.com/tim-smart)! - update effect + +## 0.13.0 + +### Minor Changes + +- [#55](https://github.com/Effect-TS/opentelemetry/pull/55) [`1ae529d`](https://github.com/Effect-TS/opentelemetry/commit/1ae529d310e79bd703a6060589dfe919bbe2b2c4) Thanks [@tim-smart](https://github.com/tim-smart)! - update to effect package + +## 0.12.0 + +### Minor Changes + +- [#54](https://github.com/Effect-TS/opentelemetry/pull/54) [`2af42ea`](https://github.com/Effect-TS/opentelemetry/commit/2af42ea90abde92c6c3832e585389c6697f487b3) Thanks [@tim-smart](https://github.com/tim-smart)! - update dependencies + +### Patch Changes + +- [#52](https://github.com/Effect-TS/opentelemetry/pull/52) [`c9dcaf5`](https://github.com/Effect-TS/opentelemetry/commit/c9dcaf562bf8bd4567ec1355ee0744c3ddbb4e3f) Thanks [@tim-smart](https://github.com/tim-smart)! - makeExternalSpan is now compatible with the span context interface + +- [#52](https://github.com/Effect-TS/opentelemetry/pull/52) [`c9dcaf5`](https://github.com/Effect-TS/opentelemetry/commit/c9dcaf562bf8bd4567ec1355ee0744c3ddbb4e3f) Thanks [@tim-smart](https://github.com/tim-smart)! - add currentOtelSpan accessor + +## 0.11.1 + +### Patch Changes + +- [#50](https://github.com/Effect-TS/opentelemetry/pull/50) [`70f551a`](https://github.com/Effect-TS/opentelemetry/commit/70f551afa76109dc4ab8ad76c55995cd67f2ef86) Thanks [@tim-smart](https://github.com/tim-smart)! - expose otel tracer with tag + layer + +## 0.11.0 + +### Minor Changes + +- [#48](https://github.com/Effect-TS/opentelemetry/pull/48) [`2487f92`](https://github.com/Effect-TS/opentelemetry/commit/2487f92a6dccfc87666fced4a37df60aebb38f9d) Thanks [@tim-smart](https://github.com/tim-smart)! - update /io + +## 0.10.0 + +### Minor Changes + +- [#46](https://github.com/Effect-TS/opentelemetry/pull/46) [`fcd35a7`](https://github.com/Effect-TS/opentelemetry/commit/fcd35a76db860d9f8999f294d0b1add3f9d65a99) Thanks [@tim-smart](https://github.com/tim-smart)! - remove Metric labels + +- [#46](https://github.com/Effect-TS/opentelemetry/pull/46) [`fcd35a7`](https://github.com/Effect-TS/opentelemetry/commit/fcd35a76db860d9f8999f294d0b1add3f9d65a99) Thanks [@tim-smart](https://github.com/tim-smart)! - update dependencies + +## 0.9.1 + +### Patch Changes + +- [#9](https://github.com/Effect-TS/opentelemetry/pull/9) [`3a3e25f`](https://github.com/Effect-TS/opentelemetry/commit/3a3e25fd153bc70a2be500f1c9c69002e238ed7a) Thanks [@tim-smart](https://github.com/tim-smart)! - add Metrics module + +## 0.9.0 + +### Minor Changes + +- [#44](https://github.com/Effect-TS/opentelemetry/pull/44) [`d0a845d`](https://github.com/Effect-TS/opentelemetry/commit/d0a845da6e409718951834ad6d5681e5dfb4acbc) Thanks [@tim-smart](https://github.com/tim-smart)! - update dependencies + +### Patch Changes + +- [#41](https://github.com/Effect-TS/opentelemetry/pull/41) [`271984d`](https://github.com/Effect-TS/opentelemetry/commit/271984d7a56096810d51a31bad652b501b854445) Thanks [@tim-smart](https://github.com/tim-smart)! - use peer deps for /data, /io and @opentelemetry/api + +- [#43](https://github.com/Effect-TS/opentelemetry/pull/43) [`80faed0`](https://github.com/Effect-TS/opentelemetry/commit/80faed049abf6c57f35d4545f9d6499014783aeb) Thanks [@tim-smart](https://github.com/tim-smart)! - update build tools + +## 0.8.2 + +### Patch Changes + +- [#39](https://github.com/Effect-TS/opentelemetry/pull/39) [`4345aa6`](https://github.com/Effect-TS/opentelemetry/commit/4345aa606b82d0690859359fb47074d365b4a729) Thanks [@tim-smart](https://github.com/tim-smart)! - set status.interrupted attribute on interruption + +## 0.8.1 + +### Patch Changes + +- [#37](https://github.com/Effect-TS/opentelemetry/pull/37) [`843a017`](https://github.com/Effect-TS/opentelemetry/commit/843a017515fc0546f60a742a9b8ab860edbeac22) Thanks [@tim-smart](https://github.com/tim-smart)! - add additional info to interrupted spans + +## 0.8.0 + +### Minor Changes + +- [#35](https://github.com/Effect-TS/opentelemetry/pull/35) [`c7e4387`](https://github.com/Effect-TS/opentelemetry/commit/c7e438784bb3396e529ccd4bb751aa48e579ba2c) Thanks [@tim-smart](https://github.com/tim-smart)! - update /io - support for links + +## 0.7.1 + +### Patch Changes + +- [#32](https://github.com/Effect-TS/opentelemetry/pull/32) [`d66c490`](https://github.com/Effect-TS/opentelemetry/commit/d66c4906a78606549bcbbc852cd36a6626473a10) Thanks [@tim-smart](https://github.com/tim-smart)! - remove name from makeExternalSpan + +## 0.7.0 + +### Minor Changes + +- [#30](https://github.com/Effect-TS/opentelemetry/pull/30) [`3b2e776`](https://github.com/Effect-TS/opentelemetry/commit/3b2e77600709e2dbc0a7bca158c1604d7b027427) Thanks [@tim-smart](https://github.com/tim-smart)! - update /data and /io + +## 0.6.0 + +### Minor Changes + +- [#28](https://github.com/Effect-TS/opentelemetry/pull/28) [`933eafa`](https://github.com/Effect-TS/opentelemetry/commit/933eafa22317e6f946ef0bab66ab5426092b9bf1) Thanks [@tim-smart](https://github.com/tim-smart)! - update /io and /data + +## 0.5.0 + +### Minor Changes + +- [#26](https://github.com/Effect-TS/opentelemetry/pull/26) [`365a7d2`](https://github.com/Effect-TS/opentelemetry/commit/365a7d2c85180f3491b433aa617ee213ccc1e855) Thanks [@tim-smart](https://github.com/tim-smart)! - update /io + +## 0.4.0 + +### Minor Changes + +- [#24](https://github.com/Effect-TS/opentelemetry/pull/24) [`bc689b0`](https://github.com/Effect-TS/opentelemetry/commit/bc689b06680700d6025556e2cefec9213002dc17) Thanks [@tim-smart](https://github.com/tim-smart)! - update /io and /data + +## 0.3.1 + +### Patch Changes + +- [#22](https://github.com/Effect-TS/opentelemetry/pull/22) [`d78883d`](https://github.com/Effect-TS/opentelemetry/commit/d78883d12471150a626698e9432a100abf730e97) Thanks [@tim-smart](https://github.com/tim-smart)! - move node-sdk to dependencies + +## 0.3.0 + +### Minor Changes + +- [#20](https://github.com/Effect-TS/opentelemetry/pull/20) [`347904e`](https://github.com/Effect-TS/opentelemetry/commit/347904e5330b0c1ce1d809dbb4aeea1fd22f4231) Thanks [@tim-smart](https://github.com/tim-smart)! - update /io and /data + +### Patch Changes + +- [#20](https://github.com/Effect-TS/opentelemetry/pull/20) [`347904e`](https://github.com/Effect-TS/opentelemetry/commit/347904e5330b0c1ce1d809dbb4aeea1fd22f4231) Thanks [@tim-smart](https://github.com/tim-smart)! - add supervisor for correctly setting otel context for fiber executions + +## 0.2.0 + +### Minor Changes + +- [#18](https://github.com/Effect-TS/opentelemetry/pull/18) [`867195a`](https://github.com/Effect-TS/opentelemetry/commit/867195a3622d2678e684bc84959bbe7e3ada9c3c) Thanks [@tim-smart](https://github.com/tim-smart)! - update /io + - adds support for nanosecond precision in timing + - add `makeExternalSpan` utility for creating parent spans + +## 0.1.3 + +### Patch Changes + +- [#16](https://github.com/Effect-TS/opentelemetry/pull/16) [`fa067d9`](https://github.com/Effect-TS/opentelemetry/commit/fa067d9cfa173a18e00dc9d9d19048af26c09ef3) Thanks [@tim-smart](https://github.com/tim-smart)! - add helper for constructing NodeSdk config + +## 0.1.2 + +### Patch Changes + +- [#14](https://github.com/Effect-TS/opentelemetry/pull/14) [`31653b4`](https://github.com/Effect-TS/opentelemetry/commit/31653b4ba6daa91bb74fd0b8b2a42b60e4b2c25e) Thanks [@tim-smart](https://github.com/tim-smart)! - make NodeSdk config an Effect + +## 0.1.1 + +### Patch Changes + +- [#12](https://github.com/Effect-TS/opentelemetry/pull/12) [`0bc9ca2`](https://github.com/Effect-TS/opentelemetry/commit/0bc9ca2cccf252608a1a91a13554e901d345e5e1) Thanks [@tim-smart](https://github.com/tim-smart)! - add /Resource module + +## 0.1.0 + +### Minor Changes + +- [#8](https://github.com/Effect-TS/opentelemetry/pull/8) [`acc90fd`](https://github.com/Effect-TS/opentelemetry/commit/acc90fd41175a99bbe148c97518a4e312c72b92a) Thanks [@tim-smart](https://github.com/tim-smart)! - implement /io/Tracer + +### Patch Changes + +- [#10](https://github.com/Effect-TS/opentelemetry/pull/10) [`f9d9045`](https://github.com/Effect-TS/opentelemetry/commit/f9d90459ba47a30edbf56edc33d024bd73f335f1) Thanks [@tim-smart](https://github.com/tim-smart)! - add NodeSdk module diff --git a/repos/effect/packages/opentelemetry/LICENSE b/repos/effect/packages/opentelemetry/LICENSE new file mode 100644 index 0000000..f8f4392 --- /dev/null +++ b/repos/effect/packages/opentelemetry/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020-present The Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/repos/effect/packages/opentelemetry/README.md b/repos/effect/packages/opentelemetry/README.md new file mode 100644 index 0000000..8c7f714 --- /dev/null +++ b/repos/effect/packages/opentelemetry/README.md @@ -0,0 +1,5 @@ +# `@effect/opentelemetry` + +## Documentation + +- **API Reference**: [View the full documentation](https://effect-ts.github.io/effect/docs/opentelemetry). diff --git a/repos/effect/packages/opentelemetry/docgen.json b/repos/effect/packages/opentelemetry/docgen.json new file mode 100644 index 0000000..e1ab57f --- /dev/null +++ b/repos/effect/packages/opentelemetry/docgen.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../node_modules/@effect/docgen/schema.json", + "srcLink": "https://github.com/Effect-TS/effect/tree/main/packages/opentelemetry/src/", + "exclude": [ + "src/internal/**/*.ts" + ] +} diff --git a/repos/effect/packages/opentelemetry/examples/index.ts b/repos/effect/packages/opentelemetry/examples/index.ts new file mode 100644 index 0000000..8786045 --- /dev/null +++ b/repos/effect/packages/opentelemetry/examples/index.ts @@ -0,0 +1,25 @@ +import * as NodeSdk from "@effect/opentelemetry/NodeSdk" +import { ConsoleSpanExporter, SimpleSpanProcessor } from "@opentelemetry/sdk-trace-base" +import * as Effect from "effect/Effect" +import { pipe } from "effect/Function" + +const NodeSdkLive = NodeSdk.layer(() => ({ + resource: { + serviceName: "example" + }, + spanProcessor: new SimpleSpanProcessor(new ConsoleSpanExporter()) +})) + +const program = pipe( + Effect.log("Hello"), + Effect.withSpan("c"), + Effect.withSpan("b"), + Effect.withSpan("a") +) + +pipe( + program, + Effect.provide(NodeSdkLive), + Effect.catchAllCause(Effect.logError), + Effect.runFork +) diff --git a/repos/effect/packages/opentelemetry/examples/metrics.ts b/repos/effect/packages/opentelemetry/examples/metrics.ts new file mode 100644 index 0000000..c4a1b9f --- /dev/null +++ b/repos/effect/packages/opentelemetry/examples/metrics.ts @@ -0,0 +1,83 @@ +import * as NodeSdk from "@effect/opentelemetry/NodeSdk" +import { PrometheusExporter } from "@opentelemetry/exporter-prometheus" +import { millis, seconds } from "effect/Duration" +import * as Effect from "effect/Effect" +import { pipe } from "effect/Function" +import * as Metric from "effect/Metric" + +const counter = Metric.counter("count", { + description: "An example counter" +}) + +const incrementCounter = pipe( + Metric.increment(counter), + Effect.delay(seconds(1)), + Effect.forever +) + +const timer = Metric.timer("timer") + +const timerLoop = pipe( + Effect.randomWith((_) => _.nextRange(1, 1000)), + Effect.flatMap((_) => Effect.sleep(millis(_))), + Metric.trackDuration(timer), + Effect.forever +) + +const freq = Metric.frequency("freq") +const labels = [ + "cake", + "pie", + "cookie", + "brownie", + "muffin" +] + +const freqLoop = Effect.randomWith((_) => _.nextIntBetween(0, labels.length)).pipe( + Effect.flatMap((_) => Metric.update(freq, labels[_])), + Effect.zipRight(Effect.sleep("1 seconds")), + Effect.forever +) + +const summary = Metric.summary({ + name: "summary", + maxAge: "1 days", + maxSize: 1000, + error: 0.01, + quantiles: [0.1, 0.5, 0.9] +}) + +const summaryLoop = Effect.randomWith((_) => _.nextRange(100, 1000)).pipe( + Metric.trackSuccess(summary), + Effect.zipRight(Effect.sleep("10 millis")), + Effect.forever +) + +const spawner = Effect.randomWith((_) => _.nextIntBetween(500, 1500)).pipe( + Effect.flatMap((_) => Effect.fork(Effect.sleep(_))), + Effect.flatMap((_) => _.await), + Effect.forever +) + +const program = Effect.gen(function*() { + yield* Effect.fork(incrementCounter) + yield* Effect.fork(timerLoop) + yield* Effect.fork(freqLoop) + yield* Effect.fork(summaryLoop) + yield* Effect.fork(spawner) +}) + +const MetricsLive = NodeSdk.layer(() => ({ + resource: { + serviceName: "example" + }, + metricReader: new PrometheusExporter({ port: 9464 }) +})) + +pipe( + program, + Effect.awaitAllChildren, + Effect.provide(MetricsLive), + Effect.catchAllCause(Effect.logError), + Effect.runFork +) diff --git a/repos/effect/packages/opentelemetry/examples/native-exporter.ts b/repos/effect/packages/opentelemetry/examples/native-exporter.ts new file mode 100644 index 0000000..ed11600 --- /dev/null +++ b/repos/effect/packages/opentelemetry/examples/native-exporter.ts @@ -0,0 +1,32 @@ +import * as Otlp from "@effect/opentelemetry/Otlp" +import * as FetchHttpClient from "@effect/platform/FetchHttpClient" +import { Effect, Layer, Schedule } from "effect" +import * as Logger from "effect/Logger" +import * as LogLevel from "effect/LogLevel" + +const Observability = Otlp.layerJson({ + baseUrl: "http://localhost:4318", + resource: { + serviceName: "my-service" + } +}).pipe(Layer.provide(FetchHttpClient.layer)) + +const program = Effect.log("Hello").pipe( + Effect.withSpan("c"), + Effect.withSpan("b"), + Effect.withSpan("a"), + Effect.schedule(Schedule.spaced(1000)), + Effect.annotateSpans("working", true) +) + +const failingProgram = Effect.fail(new Error("Failing program")).pipe( + Effect.withSpan("d") +) + +program.pipe( + Effect.andThen(failingProgram), + Effect.provide(Observability), + Effect.catchAllCause(Effect.logError), + Logger.withMinimumLogLevel(LogLevel.All), + Effect.runFork +) diff --git a/repos/effect/packages/opentelemetry/examples/otlp-exporter.ts b/repos/effect/packages/opentelemetry/examples/otlp-exporter.ts new file mode 100644 index 0000000..c8b32d8 --- /dev/null +++ b/repos/effect/packages/opentelemetry/examples/otlp-exporter.ts @@ -0,0 +1,33 @@ +import * as NodeSdk from "@effect/opentelemetry/NodeSdk" +import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http" +import { BatchSpanProcessor } from "@opentelemetry/sdk-trace-base" +import { seconds } from "effect/Duration" +import * as Effect from "effect/Effect" +import { pipe } from "effect/Function" + +const NodeSdkLive = NodeSdk.layer(() => ({ + resource: { + serviceName: "example" + }, + spanProcessor: new BatchSpanProcessor( + new OTLPTraceExporter({ + url: "http://localhost:4318/v1/traces" + }) + ) +})) + +const program = pipe( + Effect.log("Hello"), + Effect.withSpan("c"), + Effect.withSpan("b"), + Effect.withSpan("a"), + Effect.repeatN(50), + Effect.annotateSpans("working", true) +) + +pipe( + Effect.delay(program, seconds(1)), + Effect.provide(NodeSdkLive), + Effect.catchAllCause(Effect.logError), + Effect.runFork +) diff --git a/repos/effect/packages/opentelemetry/package.json b/repos/effect/packages/opentelemetry/package.json new file mode 100644 index 0000000..4278377 --- /dev/null +++ b/repos/effect/packages/opentelemetry/package.json @@ -0,0 +1,84 @@ +{ + "name": "@effect/opentelemetry", + "version": "0.63.0", + "type": "module", + "license": "MIT", + "description": "OpenTelemetry integration for Effect", + "homepage": "https://effect.website", + "repository": { + "type": "git", + "url": "https://github.com/Effect-TS/effect.git", + "directory": "packages/opentelemetry" + }, + "bugs": { + "url": "https://github.com/Effect-TS/effect/issues" + }, + "tags": [ + "opentelemetry", + "observability", + "tracing", + "metrics", + "typescript", + "algebraic-data-types", + "functional-programming" + ], + "keywords": [ + "opentelemetry", + "observability", + "tracing", + "metrics", + "typescript", + "algebraic-data-types", + "functional-programming" + ], + "publishConfig": { + "access": "public", + "provenance": true, + "directory": "dist", + "linkDirectory": false + }, + "exports": { + "./package.json": "./package.json", + ".": "./src/index.ts", + "./*": "./src/*.ts", + "./internal/*": null + }, + "scripts": { + "codegen": "build-utils prepare-v3", + "build": "pnpm build-esm && pnpm build-annotate && pnpm build-cjs && build-utils pack-v3", + "build-esm": "tsc -b tsconfig.build.json", + "build-cjs": "babel build/esm --plugins @babel/transform-export-namespace-from --plugins @babel/transform-modules-commonjs --out-dir build/cjs --source-maps", + "build-annotate": "babel build/esm --plugins annotate-pure-calls --out-dir build/esm --source-maps", + "check": "tsc -b tsconfig.json", + "test": "vitest", + "coverage": "vitest --coverage" + }, + "peerDependencies": { + "@effect/platform": "workspace:^", + "@opentelemetry/api": "^1.9", + "@opentelemetry/resources": "^2.0.0", + "@opentelemetry/sdk-logs": ">=0.203.0 <0.300.0", + "@opentelemetry/sdk-metrics": "^2.0.0", + "@opentelemetry/sdk-trace-base": "^2.0.0", + "@opentelemetry/sdk-trace-node": "^2.0.0", + "@opentelemetry/sdk-trace-web": "^2.0.0", + "@opentelemetry/semantic-conventions": "^1.33.0", + "effect": "workspace:^" + }, + "devDependencies": { + "@effect/platform": "workspace:^", + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/context-async-hooks": "^2.2.0", + "@opentelemetry/exporter-metrics-otlp-http": "0.208.0", + "@opentelemetry/exporter-prometheus": "^0.208.0", + "@opentelemetry/exporter-trace-otlp-http": "^0.208.0", + "@opentelemetry/otlp-exporter-base": "^0.208.0", + "@opentelemetry/resources": "^2.2.0", + "@opentelemetry/sdk-logs": "^0.208.0", + "@opentelemetry/sdk-metrics": "^2.2.0", + "@opentelemetry/sdk-trace-base": "^2.2.0", + "@opentelemetry/sdk-trace-node": "^2.2.0", + "@opentelemetry/sdk-trace-web": "^2.2.0", + "@opentelemetry/semantic-conventions": "^1.38.0" + } +} diff --git a/repos/effect/packages/opentelemetry/src/Logger.ts b/repos/effect/packages/opentelemetry/src/Logger.ts new file mode 100644 index 0000000..4fc6004 --- /dev/null +++ b/repos/effect/packages/opentelemetry/src/Logger.ts @@ -0,0 +1,127 @@ +/** + * @since 1.0.0 + */ +import * as Otel from "@opentelemetry/sdk-logs" +import type { NonEmptyReadonlyArray } from "effect/Array" +import * as Arr from "effect/Array" +import * as Context from "effect/Context" +import type { DurationInput } from "effect/Duration" +import * as Effect from "effect/Effect" +import * as FiberId from "effect/FiberId" +import * as FiberRef from "effect/FiberRef" +import * as FiberRefs from "effect/FiberRefs" +import * as Layer from "effect/Layer" +import * as Logger from "effect/Logger" +import * as Tracer from "effect/Tracer" +import { unknownToAttributeValue } from "./internal/utils.js" +import { Resource } from "./Resource.js" + +/** + * @since 1.0.0 + * @category tags + */ +export class OtelLoggerProvider extends Context.Tag("@effect/opentelemetry/Logger/OtelLoggerProvider")< + OtelLoggerProvider, + Otel.LoggerProvider +>() {} + +/** + * @since 1.0.0 + * @category constructors + */ +export const make: Effect.Effect< + Logger.Logger, + never, + OtelLoggerProvider +> = Effect.gen(function*() { + const loggerProvider = yield* OtelLoggerProvider + const clock = yield* Effect.clock + const otelLogger = loggerProvider.getLogger("@effect/opentelemetry") + + return Logger.make((options) => { + const now = options.date.getTime() + + const attributes: Record = { + fiberId: FiberId.threadName(options.fiberId) + } + + const maybeSpan = Context.getOption( + FiberRefs.getOrDefault(options.context, FiberRef.currentContext), + Tracer.ParentSpan + ) + + if (maybeSpan._tag === "Some") { + attributes.spanId = maybeSpan.value.spanId + attributes.traceId = maybeSpan.value.traceId + } + + for (const [key, value] of options.annotations) { + attributes[key] = unknownToAttributeValue(value) + } + for (const span of options.spans) { + attributes[`logSpan.${span.label}`] = `${now - span.startTime}ms` + } + + const message = Arr.ensure(options.message).map(unknownToAttributeValue) + otelLogger.emit({ + body: message.length === 1 ? message[0] : message, + severityText: options.logLevel.label, + severityNumber: options.logLevel.ordinal, + timestamp: options.date, + observedTimestamp: clock.unsafeCurrentTimeMillis(), + attributes + }) + }) +}) + +/** + * @since 1.0.0 + * @category layers + */ +export const layerLoggerAdd: Layer.Layer< + never, + never, + OtelLoggerProvider +> = Logger.addEffect(make) + +/** + * @since 1.0.0 + * @category layers + */ +export const layerLoggerReplace: Layer.Layer< + never, + never, + OtelLoggerProvider +> = Logger.replaceEffect(Logger.defaultLogger, make) + +/** + * @since 1.0.0 + * @category layers + */ +export const layerLoggerProvider = ( + processor: Otel.LogRecordProcessor | NonEmptyReadonlyArray, + config?: Omit & { + readonly shutdownTimeout?: DurationInput | undefined + } +): Layer.Layer => + Layer.scoped( + OtelLoggerProvider, + Effect.flatMap(Resource, (resource) => + Effect.acquireRelease( + Effect.sync(() => + new Otel.LoggerProvider({ + ...(config ?? undefined), + processors: Arr.ensure(processor), + resource + }) + ), + (provider) => + Effect.promise( + () => provider.forceFlush().then(() => provider.shutdown()) + ).pipe( + Effect.ignoreLogged, + Effect.interruptible, + Effect.timeoutOption(config?.shutdownTimeout ?? 3000) + ) + )) + ) diff --git a/repos/effect/packages/opentelemetry/src/Metrics.ts b/repos/effect/packages/opentelemetry/src/Metrics.ts new file mode 100644 index 0000000..7ef8082 --- /dev/null +++ b/repos/effect/packages/opentelemetry/src/Metrics.ts @@ -0,0 +1,40 @@ +/** + * @since 1.0.0 + */ +import type { MetricProducer, MetricReader } from "@opentelemetry/sdk-metrics" +import type { NonEmptyReadonlyArray } from "effect/Array" +import type { DurationInput } from "effect/Duration" +import type * as Effect from "effect/Effect" +import type { LazyArg } from "effect/Function" +import type { Layer } from "effect/Layer" +import type * as Scope from "effect/Scope" +import * as internal from "./internal/metrics.js" +import type { Resource } from "./Resource.js" + +/** + * @since 1.0.0 + * @category producer + */ +export const makeProducer: Effect.Effect = internal.makeProducer + +/** + * @since 1.0.0 + * @category producer + */ +export const registerProducer: ( + self: MetricProducer, + metricReader: LazyArg> +) => Effect.Effect, never, Scope.Scope> = internal.registerProducer + +/** + * @since 1.0.0 + * @category layers + */ +export const layer: ( + evaluate: LazyArg>, + options?: { + readonly shutdownTimeout?: + | DurationInput + | undefined + } +) => Layer = internal.layer diff --git a/repos/effect/packages/opentelemetry/src/NodeSdk.ts b/repos/effect/packages/opentelemetry/src/NodeSdk.ts new file mode 100644 index 0000000..c2b923c --- /dev/null +++ b/repos/effect/packages/opentelemetry/src/NodeSdk.ts @@ -0,0 +1,125 @@ +/** + * @since 1.0.0 + */ +import type * as OtelApi from "@opentelemetry/api" +import type { LoggerProviderConfig, LogRecordProcessor } from "@opentelemetry/sdk-logs" +import type { MetricReader } from "@opentelemetry/sdk-metrics" +import type { SpanProcessor, TracerConfig } from "@opentelemetry/sdk-trace-base" +import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node" +import type { NonEmptyReadonlyArray } from "effect/Array" +import type { DurationInput } from "effect/Duration" +import * as Effect from "effect/Effect" +import { constant, type LazyArg } from "effect/Function" +import * as Layer from "effect/Layer" +import { isNonEmpty } from "./internal/utils.js" +import * as Logger from "./Logger.js" +import * as Metrics from "./Metrics.js" +import * as Resource from "./Resource.js" +import * as Tracer from "./Tracer.js" + +/** + * @since 1.0.0 + * @category model + */ +export interface Configuration { + readonly spanProcessor?: SpanProcessor | ReadonlyArray | undefined + readonly tracerConfig?: Omit | undefined + readonly metricReader?: MetricReader | ReadonlyArray | undefined + readonly logRecordProcessor?: LogRecordProcessor | ReadonlyArray | undefined + readonly loggerProviderConfig?: Omit | undefined + readonly resource?: { + readonly serviceName: string + readonly serviceVersion?: string + readonly attributes?: OtelApi.Attributes + } | undefined + readonly shutdownTimeout?: DurationInput | undefined +} + +/** + * @since 1.0.0 + * @category layers + */ +export const layerTracerProvider = ( + processor: SpanProcessor | NonEmptyReadonlyArray, + config?: Omit & { + readonly shutdownTimeout?: DurationInput | undefined + } +): Layer.Layer => + Layer.scoped( + Tracer.OtelTracerProvider, + Effect.flatMap( + Resource.Resource, + (resource) => + Effect.acquireRelease( + Effect.sync(() => { + const provider = new NodeTracerProvider({ + ...(config ?? undefined), + resource, + spanProcessors: Array.isArray(processor) ? (processor as any) : [processor] + }) + return provider + }), + (provider) => + Effect.promise(() => provider.forceFlush().then(() => provider.shutdown())).pipe( + Effect.ignoreLogged, + Effect.interruptible, + Effect.timeoutOption(config?.shutdownTimeout ?? 3000) + ) + ) + ) + ) + +/** + * @since 1.0.0 + * @category layer + */ +export const layer: { + (evaluate: LazyArg): Layer.Layer + (evaluate: Effect.Effect): Layer.Layer +} = ( + evaluate: LazyArg | Effect.Effect +): Layer.Layer => + Layer.unwrapEffect( + Effect.map( + Effect.isEffect(evaluate) + ? evaluate as Effect.Effect + : Effect.sync(evaluate), + (config) => { + const ResourceLive = Resource.layerFromEnv(config.resource && Resource.configToAttributes(config.resource)) + + const TracerLive = isNonEmpty(config.spanProcessor) + ? Layer.provide( + Tracer.layer, + layerTracerProvider(config.spanProcessor, { + ...config.tracerConfig, + shutdownTimeout: config.shutdownTimeout + }) + ) + : Layer.empty + + const MetricsLive = isNonEmpty(config.metricReader) + ? Metrics.layer(constant(config.metricReader), config) + : Layer.empty + + const LoggerLive = isNonEmpty(config.logRecordProcessor) + ? Layer.provide( + Logger.layerLoggerAdd, + Logger.layerLoggerProvider(config.logRecordProcessor, { + ...config.loggerProviderConfig, + shutdownTimeout: config.shutdownTimeout + }) + ) + : Layer.empty + + return Layer.mergeAll(TracerLive, MetricsLive, LoggerLive).pipe( + Layer.provideMerge(ResourceLive) + ) + } + ) + ) + +/** + * @since 2.0.0 + * @category layer + */ +export const layerEmpty: Layer.Layer = Resource.layerEmpty diff --git a/repos/effect/packages/opentelemetry/src/Otlp.ts b/repos/effect/packages/opentelemetry/src/Otlp.ts new file mode 100644 index 0000000..72d6c35 --- /dev/null +++ b/repos/effect/packages/opentelemetry/src/Otlp.ts @@ -0,0 +1,118 @@ +/** + * @since 1.0.0 + */ +import type * as Headers from "@effect/platform/Headers" +import type * as HttpClient from "@effect/platform/HttpClient" +import * as HttpClientRequest from "@effect/platform/HttpClientRequest" +import type * as Duration from "effect/Duration" +import { flow } from "effect/Function" +import * as Layer from "effect/Layer" +import type * as Logger from "effect/Logger" +import type * as Tracer from "effect/Tracer" +import * as OtlpLogger from "./OtlpLogger.js" +import * as OtlpMetrics from "./OtlpMetrics.js" +import * as OtlpSerialization from "./OtlpSerialization.js" +import * as OtlpTracer from "./OtlpTracer.js" + +/** + * Creates an OTLP layer. + * + * @since 1.0.0 + * @category Layers + */ +export const layer = (options: { + readonly baseUrl: string + readonly resource?: { + readonly serviceName?: string | undefined + readonly serviceVersion?: string | undefined + readonly attributes?: Record + } + readonly headers?: Headers.Input | undefined + readonly maxBatchSize?: number | undefined + readonly replaceLogger?: Logger.Logger | undefined + readonly tracerContext?: ((f: () => X, span: Tracer.AnySpan) => X) | undefined + readonly loggerExportInterval?: Duration.DurationInput | undefined + readonly loggerExcludeLogSpans?: boolean | undefined + readonly metricsExportInterval?: Duration.DurationInput | undefined + readonly tracerExportInterval?: Duration.DurationInput | undefined + readonly shutdownTimeout?: Duration.DurationInput | undefined +}): Layer.Layer => { + const baseReq = HttpClientRequest.get(options.baseUrl) + const url = (path: string) => HttpClientRequest.appendUrl(baseReq, path).url + return Layer.mergeAll( + OtlpLogger.layer({ + replaceLogger: options.replaceLogger, + url: url("/v1/logs"), + resource: options.resource, + headers: options.headers, + exportInterval: options.loggerExportInterval, + maxBatchSize: options.maxBatchSize, + shutdownTimeout: options.shutdownTimeout, + excludeLogSpans: options.loggerExcludeLogSpans + }), + OtlpMetrics.layer({ + url: url("/v1/metrics"), + resource: options.resource, + headers: options.headers, + exportInterval: options.metricsExportInterval, + shutdownTimeout: options.shutdownTimeout + }), + OtlpTracer.layer({ + url: url("/v1/traces"), + resource: options.resource, + headers: options.headers, + exportInterval: options.tracerExportInterval, + maxBatchSize: options.maxBatchSize, + context: options.tracerContext, + shutdownTimeout: options.shutdownTimeout + }) + ) +} + +/** + * Creates an OTLP layer with JSON serialization. + * + * @since 1.0.0 + * @category Layers + */ +export const layerJson: (options: { + readonly baseUrl: string + readonly resource?: { + readonly serviceName?: string | undefined + readonly serviceVersion?: string | undefined + readonly attributes?: Record + } + readonly headers?: Headers.Input | undefined + readonly maxBatchSize?: number | undefined + readonly replaceLogger?: Logger.Logger | undefined + readonly tracerContext?: ((f: () => X, span: Tracer.AnySpan) => X) | undefined + readonly loggerExportInterval?: Duration.DurationInput | undefined + readonly loggerExcludeLogSpans?: boolean | undefined + readonly metricsExportInterval?: Duration.DurationInput | undefined + readonly tracerExportInterval?: Duration.DurationInput | undefined + readonly shutdownTimeout?: Duration.DurationInput | undefined +}) => Layer.Layer = flow(layer, Layer.provide(OtlpSerialization.layerJson)) + +/** + * Creates an OTLP layer with Protobuf serialization. + * + * @since 1.0.0 + * @category Layers + */ +export const layerProtobuf: (options: { + readonly baseUrl: string + readonly resource?: { + readonly serviceName?: string | undefined + readonly serviceVersion?: string | undefined + readonly attributes?: Record + } + readonly headers?: Headers.Input | undefined + readonly maxBatchSize?: number | undefined + readonly replaceLogger?: Logger.Logger | undefined + readonly tracerContext?: ((f: () => X, span: Tracer.AnySpan) => X) | undefined + readonly loggerExportInterval?: Duration.DurationInput | undefined + readonly loggerExcludeLogSpans?: boolean | undefined + readonly metricsExportInterval?: Duration.DurationInput | undefined + readonly tracerExportInterval?: Duration.DurationInput | undefined + readonly shutdownTimeout?: Duration.DurationInput | undefined +}) => Layer.Layer = flow(layer, Layer.provide(OtlpSerialization.layerProtobuf)) diff --git a/repos/effect/packages/opentelemetry/src/OtlpLogger.ts b/repos/effect/packages/opentelemetry/src/OtlpLogger.ts new file mode 100644 index 0000000..651cef6 --- /dev/null +++ b/repos/effect/packages/opentelemetry/src/OtlpLogger.ts @@ -0,0 +1,263 @@ +/** + * @since 1.0.0 + */ +import type * as Headers from "@effect/platform/Headers" +import type * as HttpClient from "@effect/platform/HttpClient" +import * as Arr from "effect/Array" +import * as Cause from "effect/Cause" +import * as Context from "effect/Context" +import * as Duration from "effect/Duration" +import * as Effect from "effect/Effect" +import * as FiberId from "effect/FiberId" +import * as FiberRef from "effect/FiberRef" +import * as FiberRefs from "effect/FiberRefs" +import type * as Layer from "effect/Layer" +import * as Logger from "effect/Logger" +import type * as LogLevel from "effect/LogLevel" +import * as Option from "effect/Option" +import type * as Scope from "effect/Scope" +import * as Tracer from "effect/Tracer" +import * as Exporter from "./internal/otlpExporter.js" +import type { AnyValue, Fixed64, KeyValue, Resource } from "./OtlpResource.js" +import * as OtlpResource from "./OtlpResource.js" +import { OtlpSerialization } from "./OtlpSerialization.js" + +/** + * @since 1.0.0 + * @category Constructors + */ +export const make: ( + options: { + readonly url: string + readonly resource?: { + readonly serviceName?: string | undefined + readonly serviceVersion?: string | undefined + readonly attributes?: Record + } | undefined + readonly headers?: Headers.Input | undefined + readonly exportInterval?: Duration.DurationInput | undefined + readonly maxBatchSize?: number | undefined + readonly shutdownTimeout?: Duration.DurationInput | undefined + readonly excludeLogSpans?: boolean | undefined + } +) => Effect.Effect< + Logger.Logger, + never, + HttpClient.HttpClient | OtlpSerialization | Scope.Scope +> = Effect.fnUntraced(function*(options) { + const otelResource = yield* OtlpResource.fromConfig(options.resource) + const scope: IInstrumentationScope = { + name: OtlpResource.unsafeServiceName(otelResource) + } + const serialization = yield* OtlpSerialization + + const exporter = yield* Exporter.make({ + label: "OtlpLogger", + url: options.url, + headers: options.headers, + maxBatchSize: options.maxBatchSize ?? 1000, + exportInterval: options.exportInterval ?? Duration.seconds(1), + body(data) { + const body: IExportLogsServiceRequest = { + resourceLogs: [{ + resource: otelResource, + scopeLogs: [{ + scope, + logRecords: data + }] + }] + } + return serialization.logs(body) + }, + shutdownTimeout: options.shutdownTimeout ?? Duration.seconds(3) + }) + + const opts = { + excludeLogSpans: options.excludeLogSpans ?? false + } + return Logger.make((options) => { + exporter.push(makeLogRecord(options, opts)) + }) +}) + +/** + * @since 1.0.0 + * @category Layers + */ +export const layer = (options: { + readonly url: string + readonly resource?: { + readonly serviceName?: string | undefined + readonly serviceVersion?: string | undefined + readonly attributes?: Record + } | undefined + readonly replaceLogger?: Logger.Logger | undefined + readonly headers?: Headers.Input | undefined + readonly exportInterval?: Duration.DurationInput | undefined + readonly maxBatchSize?: number | undefined + readonly shutdownTimeout?: Duration.DurationInput | undefined + readonly excludeLogSpans?: boolean | undefined +}): Layer.Layer => + options.replaceLogger ? Logger.replaceScoped(options.replaceLogger, make(options)) : Logger.addScoped(make(options)) + +// internal + +const makeLogRecord = (options: Logger.Logger.Options, opts: { + readonly excludeLogSpans: boolean +}): ILogRecord => { + const now = options.date.getTime() + const nanosString = `${now}000000` + + const attributes = OtlpResource.entriesToAttributes(options.annotations) + attributes.push({ + key: "fiberId", + value: { stringValue: FiberId.threadName(options.fiberId) } + }) + if (!opts.excludeLogSpans) { + for (const span of options.spans) { + attributes.push({ + key: `logSpan.${span.label}`, + value: { stringValue: `${now - span.startTime}ms` } + }) + } + } + if (!Cause.isEmpty(options.cause)) { + attributes.push({ + key: "log.error", + value: { stringValue: Cause.pretty(options.cause, { renderErrorCause: true }) } + }) + } + + const message = Arr.ensure(options.message) + const maybeSpan = Context.getOption( + FiberRefs.getOrDefault(options.context, FiberRef.currentContext), + Tracer.ParentSpan + ) + + const logRecord: ILogRecord = { + severityNumber: logLevelToSeverityNumber(options.logLevel), + severityText: options.logLevel.label, + timeUnixNano: nanosString, + observedTimeUnixNano: nanosString, + attributes, + body: OtlpResource.unknownToAttributeValue(message.length === 1 ? message[0] : message), + droppedAttributesCount: 0 + } + + if (Option.isSome(maybeSpan)) { + logRecord.traceId = maybeSpan.value.traceId + logRecord.spanId = maybeSpan.value.spanId + } + + return logRecord +} + +/** Properties of an ExportLogsServiceRequest. */ +interface IExportLogsServiceRequest { + /** ExportLogsServiceRequest resourceLogs */ + resourceLogs?: Array +} + +/** Properties of an InstrumentationScope. */ +interface IInstrumentationScope { + /** InstrumentationScope name */ + name: string + /** InstrumentationScope version */ + version?: string + /** InstrumentationScope attributes */ + attributes?: Array + /** InstrumentationScope droppedAttributesCount */ + droppedAttributesCount?: number +} +/** Properties of a ResourceLogs. */ +interface IResourceLogs { + /** ResourceLogs resource */ + resource?: Resource + /** ResourceLogs scopeLogs */ + scopeLogs: Array + /** ResourceLogs schemaUrl */ + schemaUrl?: string +} +/** Properties of an ScopeLogs. */ +interface IScopeLogs { + /** IScopeLogs scope */ + scope?: IInstrumentationScope + /** IScopeLogs logRecords */ + logRecords?: Array + /** IScopeLogs schemaUrl */ + schemaUrl?: string | null +} +/** Properties of a LogRecord. */ +interface ILogRecord { + /** LogRecord timeUnixNano */ + timeUnixNano: Fixed64 + /** LogRecord observedTimeUnixNano */ + observedTimeUnixNano: Fixed64 + /** LogRecord severityNumber */ + severityNumber?: ESeverityNumber + /** LogRecord severityText */ + severityText?: string + /** LogRecord body */ + body?: AnyValue + /** LogRecord attributes */ + attributes: Array + /** LogRecord droppedAttributesCount */ + droppedAttributesCount: number + /** LogRecord flags */ + flags?: number + /** LogRecord traceId */ + traceId?: string | Uint8Array + /** LogRecord spanId */ + spanId?: string | Uint8Array +} + +const logLevelToSeverityNumber = (logLevel: LogLevel.LogLevel): ESeverityNumber => { + switch (logLevel._tag) { + case "Trace": + return ESeverityNumber.SEVERITY_NUMBER_TRACE + case "Debug": + return ESeverityNumber.SEVERITY_NUMBER_DEBUG + case "Info": + return ESeverityNumber.SEVERITY_NUMBER_INFO + case "Warning": + return ESeverityNumber.SEVERITY_NUMBER_WARN + case "Error": + return ESeverityNumber.SEVERITY_NUMBER_ERROR + case "Fatal": + return ESeverityNumber.SEVERITY_NUMBER_FATAL + default: + return ESeverityNumber.SEVERITY_NUMBER_UNSPECIFIED + } +} + +/** + * Numerical value of the severity, normalized to values described in Log Data Model. + */ +const enum ESeverityNumber { + /** Unspecified. Do NOT use as default */ + SEVERITY_NUMBER_UNSPECIFIED = 0, + SEVERITY_NUMBER_TRACE = 1, + SEVERITY_NUMBER_TRACE2 = 2, + SEVERITY_NUMBER_TRACE3 = 3, + SEVERITY_NUMBER_TRACE4 = 4, + SEVERITY_NUMBER_DEBUG = 5, + SEVERITY_NUMBER_DEBUG2 = 6, + SEVERITY_NUMBER_DEBUG3 = 7, + SEVERITY_NUMBER_DEBUG4 = 8, + SEVERITY_NUMBER_INFO = 9, + SEVERITY_NUMBER_INFO2 = 10, + SEVERITY_NUMBER_INFO3 = 11, + SEVERITY_NUMBER_INFO4 = 12, + SEVERITY_NUMBER_WARN = 13, + SEVERITY_NUMBER_WARN2 = 14, + SEVERITY_NUMBER_WARN3 = 15, + SEVERITY_NUMBER_WARN4 = 16, + SEVERITY_NUMBER_ERROR = 17, + SEVERITY_NUMBER_ERROR2 = 18, + SEVERITY_NUMBER_ERROR3 = 19, + SEVERITY_NUMBER_ERROR4 = 20, + SEVERITY_NUMBER_FATAL = 21, + SEVERITY_NUMBER_FATAL2 = 22, + SEVERITY_NUMBER_FATAL3 = 23, + SEVERITY_NUMBER_FATAL4 = 24 +} diff --git a/repos/effect/packages/opentelemetry/src/OtlpMetrics.ts b/repos/effect/packages/opentelemetry/src/OtlpMetrics.ts new file mode 100644 index 0000000..54a221a --- /dev/null +++ b/repos/effect/packages/opentelemetry/src/OtlpMetrics.ts @@ -0,0 +1,575 @@ +/** + * @since 1.0.0 + */ +import type * as Headers from "@effect/platform/Headers" +import type * as HttpClient from "@effect/platform/HttpClient" +import * as Arr from "effect/Array" +import * as Duration from "effect/Duration" +import * as Effect from "effect/Effect" +import * as Layer from "effect/Layer" +import * as Metric from "effect/Metric" +import type * as MetricKey from "effect/MetricKey" +import * as MetricState from "effect/MetricState" +import * as Option from "effect/Option" +import type * as Scope from "effect/Scope" +import * as Exporter from "./internal/otlpExporter.js" +import type { Fixed64, KeyValue } from "./OtlpResource.js" +import * as OtlpResource from "./OtlpResource.js" +import { OtlpSerialization } from "./OtlpSerialization.js" + +/** + * @since 1.0.0 + * @category Constructors + */ +export const make: (options: { + readonly url: string + readonly resource?: { + readonly serviceName?: string | undefined + readonly serviceVersion?: string | undefined + readonly attributes?: Record + } | undefined + readonly headers?: Headers.Input | undefined + readonly exportInterval?: Duration.DurationInput | undefined + readonly shutdownTimeout?: Duration.DurationInput | undefined +}) => Effect.Effect< + void, + never, + HttpClient.HttpClient | OtlpSerialization | Scope.Scope +> = Effect.fnUntraced(function*(options) { + const clock = yield* Effect.clock + const startTime = String(clock.unsafeCurrentTimeNanos()) + + const resource = yield* OtlpResource.fromConfig(options.resource) + const metricsScope: IInstrumentationScope = { + name: OtlpResource.unsafeServiceName(resource) + } + const serialization = yield* OtlpSerialization + + const snapshot = () => { + const snapshot = Metric.unsafeSnapshot() + const nowNanos = clock.unsafeCurrentTimeNanos() + const nowTime = String(nowNanos) + const metricData: Array = [] + const metricDataByName = new Map() + const addMetricData = (data: IMetric) => { + metricData.push(data) + metricDataByName.set(data.name, data) + } + + for (let i = 0, len = snapshot.length; i < len; i++) { + const { metricKey, metricState } = snapshot[i] + let unit = "1" + const attributes = Arr.reduce(metricKey.tags, [], (acc: Array, label) => { + if (label.key === "unit" || label.key === "time_unit") { + unit = label.value + } + acc.push({ key: label.key, value: { stringValue: label.value } }) + return acc + }) + + if (MetricState.isCounterState(metricState)) { + const dataPoint: INumberDataPoint = { + attributes, + startTimeUnixNano: startTime, + timeUnixNano: nowTime + } + if (typeof metricState.count === "bigint") { + dataPoint.asInt = Number(metricState.count) + } else { + dataPoint.asDouble = metricState.count + } + if (metricDataByName.has(metricKey.name)) { + metricDataByName.get(metricKey.name)!.sum!.dataPoints.push(dataPoint) + } else { + const key = metricKey as MetricKey.MetricKey.Counter + addMetricData({ + name: metricKey.name, + description: getOrEmpty(key.description), + unit, + sum: { + aggregationTemporality: EAggregationTemporality.AGGREGATION_TEMPORALITY_CUMULATIVE, + isMonotonic: key.keyType.incremental, + dataPoints: [dataPoint] + } + }) + } + } else if (MetricState.isGaugeState(metricState)) { + const dataPoint: INumberDataPoint = { + attributes, + startTimeUnixNano: startTime, + timeUnixNano: nowTime + } + if (typeof metricState.value === "bigint") { + dataPoint.asInt = Number(metricState.value) + } else { + dataPoint.asDouble = metricState.value + } + if (metricDataByName.has(metricKey.name)) { + metricDataByName.get(metricKey.name)!.gauge!.dataPoints.push(dataPoint) + } else { + addMetricData({ + name: metricKey.name, + description: getOrEmpty(metricKey.description), + unit, + gauge: { + dataPoints: [dataPoint] + } + }) + } + } else if (MetricState.isHistogramState(metricState)) { + const size = metricState.buckets.length + const buckets = { + boundaries: Arr.allocate(size - 1) as Array, + counts: Arr.allocate(size) as Array + } + let i = 0 + let prev = 0 + for (const [boundary, value] of metricState.buckets) { + if (i < size - 1) { + buckets.boundaries[i] = boundary + } + buckets.counts[i] = value - prev + prev = value + i++ + } + const dataPoint: IHistogramDataPoint = { + attributes, + startTimeUnixNano: startTime, + timeUnixNano: nowTime, + count: metricState.count, + min: metricState.min, + max: metricState.max, + sum: metricState.sum, + bucketCounts: buckets.counts, + explicitBounds: buckets.boundaries + } + + if (metricDataByName.has(metricKey.name)) { + metricDataByName.get(metricKey.name)!.histogram!.dataPoints.push(dataPoint) + } else { + addMetricData({ + name: metricKey.name, + description: getOrEmpty(metricKey.description), + unit, + histogram: { + aggregationTemporality: EAggregationTemporality.AGGREGATION_TEMPORALITY_CUMULATIVE, + dataPoints: [dataPoint] + } + }) + } + } else if (MetricState.isFrequencyState(metricState)) { + const dataPoints: Array = [] + for (const [freqKey, value] of metricState.occurrences) { + dataPoints.push({ + attributes: [...attributes, { key: "key", value: { stringValue: freqKey } }], + startTimeUnixNano: startTime, + timeUnixNano: nowTime, + asInt: value + }) + } + if (metricDataByName.has(metricKey.name)) { + // eslint-disable-next-line no-restricted-syntax + metricDataByName.get(metricKey.name)!.sum!.dataPoints.push(...dataPoints) + } else { + addMetricData({ + name: metricKey.name, + description: getOrEmpty(metricKey.description), + unit, + sum: { + aggregationTemporality: EAggregationTemporality.AGGREGATION_TEMPORALITY_CUMULATIVE, + isMonotonic: true, + dataPoints + } + }) + } + } else if (MetricState.isSummaryState(metricState)) { + const dataPoints: Array = [{ + attributes: [...attributes, { key: "quantile", value: { stringValue: "min" } }], + startTimeUnixNano: startTime, + timeUnixNano: nowTime, + asDouble: metricState.min + }] + for (const [quantile, value] of metricState.quantiles) { + dataPoints.push({ + attributes: [...attributes, { key: "quantile", value: { stringValue: quantile.toString() } }], + startTimeUnixNano: startTime, + timeUnixNano: nowTime, + asDouble: value._tag === "Some" ? value.value : 0 + }) + } + dataPoints.push({ + attributes: [...attributes, { key: "quantile", value: { stringValue: "max" } }], + startTimeUnixNano: startTime, + timeUnixNano: nowTime, + asDouble: metricState.max + }) + const countDataPoint: INumberDataPoint = { + attributes, + startTimeUnixNano: startTime, + timeUnixNano: nowTime, + asInt: metricState.count + } + const sumDataPoint: INumberDataPoint = { + attributes, + startTimeUnixNano: startTime, + timeUnixNano: nowTime, + asDouble: metricState.sum + } + + if (metricDataByName.has(`${metricKey.name}_quantiles`)) { + // eslint-disable-next-line no-restricted-syntax + metricDataByName.get(`${metricKey.name}_quantiles`)!.sum!.dataPoints.push(...dataPoints) + metricDataByName.get(`${metricKey.name}_count`)!.sum!.dataPoints.push(countDataPoint) + metricDataByName.get(`${metricKey.name}_sum`)!.sum!.dataPoints.push(sumDataPoint) + } else { + addMetricData({ + name: `${metricKey.name}_quantiles`, + description: getOrEmpty(metricKey.description), + unit, + sum: { + aggregationTemporality: EAggregationTemporality.AGGREGATION_TEMPORALITY_CUMULATIVE, + isMonotonic: false, + dataPoints + } + }) + addMetricData({ + name: `${metricKey.name}_count`, + description: getOrEmpty(metricKey.description), + unit: "1", + sum: { + aggregationTemporality: EAggregationTemporality.AGGREGATION_TEMPORALITY_CUMULATIVE, + isMonotonic: true, + dataPoints: [countDataPoint] + } + }) + addMetricData({ + name: `${metricKey.name}_sum`, + description: getOrEmpty(metricKey.description), + unit: "1", + sum: { + aggregationTemporality: EAggregationTemporality.AGGREGATION_TEMPORALITY_CUMULATIVE, + isMonotonic: true, + dataPoints: [sumDataPoint] + } + }) + } + } + } + + const body: IExportMetricsServiceRequest = { + resourceMetrics: [{ + resource, + scopeMetrics: [{ + scope: metricsScope, + metrics: metricData + }] + }] + } + + return serialization.metrics(body) + } + + yield* Exporter.make({ + label: "OtlpMetrics", + url: options.url, + headers: options.headers, + maxBatchSize: "disabled", + exportInterval: options.exportInterval ?? Duration.seconds(10), + body: snapshot, + shutdownTimeout: options.shutdownTimeout ?? Duration.seconds(3) + }) +}) + +/** + * @since 1.0.0 + * @category Layers + */ +export const layer = (options: { + readonly url: string + readonly resource?: { + readonly serviceName?: string | undefined + readonly serviceVersion?: string | undefined + readonly attributes?: Record + } | undefined + readonly headers?: Headers.Input | undefined + readonly exportInterval?: Duration.DurationInput | undefined + readonly shutdownTimeout?: Duration.DurationInput | undefined +}): Layer.Layer => Layer.scopedDiscard(make(options)) + +// internal + +const getOrEmpty = Option.getOrElse(() => "") + +/** Properties of an InstrumentationScope. */ +interface IInstrumentationScope { + /** InstrumentationScope name */ + name: string + /** InstrumentationScope version */ + version?: string + /** InstrumentationScope attributes */ + attributes?: Array + /** InstrumentationScope droppedAttributesCount */ + droppedAttributesCount?: number +} + +/** Properties of an ExportMetricsServiceRequest. */ +interface IExportMetricsServiceRequest { + /** ExportMetricsServiceRequest resourceMetrics */ + resourceMetrics: Array +} +/** Properties of a ResourceMetrics. */ +interface IResourceMetrics { + /** ResourceMetrics resource */ + resource?: OtlpResource.Resource + /** ResourceMetrics scopeMetrics */ + scopeMetrics: Array + /** ResourceMetrics schemaUrl */ + schemaUrl?: string +} +/** Properties of an IScopeMetrics. */ +interface IScopeMetrics { + /** ScopeMetrics scope */ + scope?: IInstrumentationScope + /** ScopeMetrics metrics */ + metrics: Array + /** ScopeMetrics schemaUrl */ + schemaUrl?: string +} +/** Properties of a Metric. */ +interface IMetric { + /** Metric name */ + name: string + /** Metric description */ + description?: string + /** Metric unit */ + unit?: string + /** Metric gauge */ + gauge?: IGauge + /** Metric sum */ + sum?: ISum + /** Metric histogram */ + histogram?: IHistogram + /** Metric exponentialHistogram */ + exponentialHistogram?: IExponentialHistogram + /** Metric summary */ + summary?: ISummary +} +/** Properties of a Gauge. */ +interface IGauge { + /** Gauge dataPoints */ + dataPoints: Array +} +/** Properties of a Sum. */ +interface ISum { + /** Sum dataPoints */ + dataPoints: Array + /** Sum aggregationTemporality */ + aggregationTemporality: EAggregationTemporality + /** Sum isMonotonic */ + isMonotonic: boolean +} +/** Properties of a Histogram. */ +interface IHistogram { + /** Histogram dataPoints */ + dataPoints: Array + /** Histogram aggregationTemporality */ + aggregationTemporality?: EAggregationTemporality +} +/** Properties of an ExponentialHistogram. */ +interface IExponentialHistogram { + /** ExponentialHistogram dataPoints */ + dataPoints: Array + /** ExponentialHistogram aggregationTemporality */ + aggregationTemporality?: EAggregationTemporality +} +/** Properties of a Summary. */ +interface ISummary { + /** Summary dataPoints */ + dataPoints: Array +} +/** Properties of a NumberDataPoint. */ +interface INumberDataPoint { + /** NumberDataPoint attributes */ + attributes: Array + /** NumberDataPoint startTimeUnixNano */ + startTimeUnixNano?: Fixed64 + /** NumberDataPoint timeUnixNano */ + timeUnixNano?: Fixed64 + /** NumberDataPoint asDouble */ + asDouble?: number | null + /** NumberDataPoint asInt */ + asInt?: number + /** NumberDataPoint exemplars */ + exemplars?: Array + /** NumberDataPoint flags */ + flags?: number +} +/** Properties of a HistogramDataPoint. */ +interface IHistogramDataPoint { + /** HistogramDataPoint attributes */ + attributes?: Array + /** HistogramDataPoint startTimeUnixNano */ + startTimeUnixNano?: Fixed64 + /** HistogramDataPoint timeUnixNano */ + timeUnixNano?: Fixed64 + /** HistogramDataPoint count */ + count?: number + /** HistogramDataPoint sum */ + sum?: number + /** HistogramDataPoint bucketCounts */ + bucketCounts?: Array + /** HistogramDataPoint explicitBounds */ + explicitBounds?: Array + /** HistogramDataPoint exemplars */ + exemplars?: Array + /** HistogramDataPoint flags */ + flags?: number + /** HistogramDataPoint min */ + min?: number + /** HistogramDataPoint max */ + max?: number +} +/** Properties of an ExponentialHistogramDataPoint. */ +interface IExponentialHistogramDataPoint { + /** ExponentialHistogramDataPoint attributes */ + attributes?: Array + /** ExponentialHistogramDataPoint startTimeUnixNano */ + startTimeUnixNano?: Fixed64 + /** ExponentialHistogramDataPoint timeUnixNano */ + timeUnixNano?: Fixed64 + /** ExponentialHistogramDataPoint count */ + count?: number + /** ExponentialHistogramDataPoint sum */ + sum?: number + /** ExponentialHistogramDataPoint scale */ + scale?: number + /** ExponentialHistogramDataPoint zeroCount */ + zeroCount?: number + /** ExponentialHistogramDataPoint positive */ + positive?: IBuckets + /** ExponentialHistogramDataPoint negative */ + negative?: IBuckets + /** ExponentialHistogramDataPoint flags */ + flags?: number + /** ExponentialHistogramDataPoint exemplars */ + exemplars?: Array + /** ExponentialHistogramDataPoint min */ + min?: number + /** ExponentialHistogramDataPoint max */ + max?: number +} +/** Properties of a SummaryDataPoint. */ +interface ISummaryDataPoint { + /** SummaryDataPoint attributes */ + attributes?: Array + /** SummaryDataPoint startTimeUnixNano */ + startTimeUnixNano?: number + /** SummaryDataPoint timeUnixNano */ + timeUnixNano?: string + /** SummaryDataPoint count */ + count?: number + /** SummaryDataPoint sum */ + sum?: number + /** SummaryDataPoint quantileValues */ + quantileValues?: Array + /** SummaryDataPoint flags */ + flags?: number +} +/** Properties of a ValueAtQuantile. */ +interface IValueAtQuantile { + /** ValueAtQuantile quantile */ + quantile?: number + /** ValueAtQuantile value */ + value?: number +} +/** Properties of a Buckets. */ +interface IBuckets { + /** Buckets offset */ + offset?: number + /** Buckets bucketCounts */ + bucketCounts?: Array +} +/** Properties of an Exemplar. */ +interface IExemplar { + /** Exemplar filteredAttributes */ + filteredAttributes?: Array + /** Exemplar timeUnixNano */ + timeUnixNano?: string + /** Exemplar asDouble */ + asDouble?: number + /** Exemplar asInt */ + asInt?: number + /** Exemplar spanId */ + spanId?: string | Uint8Array + /** Exemplar traceId */ + traceId?: string | Uint8Array +} +/** + * AggregationTemporality defines how a metric aggregator reports aggregated + * values. It describes how those values relate to the time interval over + * which they are aggregated. + */ +const enum EAggregationTemporality { + AGGREGATION_TEMPORALITY_UNSPECIFIED = 0, + /** DELTA is an AggregationTemporality for a metric aggregator which reports + changes since last report time. Successive metrics contain aggregation of + values from continuous and non-overlapping intervals. + + The values for a DELTA metric are based only on the time interval + associated with one measurement cycle. There is no dependency on + previous measurements like is the case for CUMULATIVE metrics. + + For example, consider a system measuring the number of requests that + it receives and reports the sum of these requests every second as a + DELTA metric: + + 1. The system starts receiving at time=t_0. + 2. A request is received, the system measures 1 request. + 3. A request is received, the system measures 1 request. + 4. A request is received, the system measures 1 request. + 5. The 1 second collection cycle ends. A metric is exported for the + number of requests received over the interval of time t_0 to + t_0+1 with a value of 3. + 6. A request is received, the system measures 1 request. + 7. A request is received, the system measures 1 request. + 8. The 1 second collection cycle ends. A metric is exported for the + number of requests received over the interval of time t_0+1 to + t_0+2 with a value of 2. */ + AGGREGATION_TEMPORALITY_DELTA = 1, + /** CUMULATIVE is an AggregationTemporality for a metric aggregator which + reports changes since a fixed start time. This means that current values + of a CUMULATIVE metric depend on all previous measurements since the + start time. Because of this, the sender is required to retain this state + in some form. If this state is lost or invalidated, the CUMULATIVE metric + values MUST be reset and a new fixed start time following the last + reported measurement time sent MUST be used. + + For example, consider a system measuring the number of requests that + it receives and reports the sum of these requests every second as a + CUMULATIVE metric: + + 1. The system starts receiving at time=t_0. + 2. A request is received, the system measures 1 request. + 3. A request is received, the system measures 1 request. + 4. A request is received, the system measures 1 request. + 5. The 1 second collection cycle ends. A metric is exported for the + number of requests received over the interval of time t_0 to + t_0+1 with a value of 3. + 6. A request is received, the system measures 1 request. + 7. A request is received, the system measures 1 request. + 8. The 1 second collection cycle ends. A metric is exported for the + number of requests received over the interval of time t_0 to + t_0+2 with a value of 5. + 9. The system experiences a fault and loses state. + 10. The system recovers and resumes receiving at time=t_1. + 11. A request is received, the system measures 1 request. + 12. The 1 second collection cycle ends. A metric is exported for the + number of requests received over the interval of time t_1 to + t_0+1 with a value of 1. + + Note: Even though, when reporting changes since last report time, using + CUMULATIVE is valid, it is not recommended. This may cause problems for + systems that do not use start_time to determine when the aggregation + value was reset (e.g. Prometheus). */ + AGGREGATION_TEMPORALITY_CUMULATIVE = 2 +} diff --git a/repos/effect/packages/opentelemetry/src/OtlpResource.ts b/repos/effect/packages/opentelemetry/src/OtlpResource.ts new file mode 100644 index 0000000..ac72cbe --- /dev/null +++ b/repos/effect/packages/opentelemetry/src/OtlpResource.ts @@ -0,0 +1,232 @@ +/** + * @since 1.0.0 + */ +import * as Arr from "effect/Array" +import * as Config from "effect/Config" +import * as Effect from "effect/Effect" +import * as Inspectable from "effect/Inspectable" + +const ATTR_SERVICE_NAME = "service.name" +const ATTR_SERVICE_VERSION = "service.version" + +/** + * @since 1.0.0 + * @category Models + */ +export interface Resource { + /** Resource attributes */ + attributes: Array + /** Resource droppedAttributesCount */ + droppedAttributesCount: number +} + +/** + * @since 1.0.0 + * @category Constructors + */ +export const make = (options: { + readonly serviceName: string + readonly serviceVersion?: string | undefined + readonly attributes?: Record | undefined +}): Resource => { + const resourceAttributes = options.attributes + ? entriesToAttributes(Object.entries(options.attributes)) + : [] + resourceAttributes.push({ + key: ATTR_SERVICE_NAME, + value: { + stringValue: options.serviceName + } + }) + if (options.serviceVersion) { + resourceAttributes.push({ + key: ATTR_SERVICE_VERSION, + value: { + stringValue: options.serviceVersion + } + }) + } + + return { + attributes: resourceAttributes, + droppedAttributesCount: 0 + } +} + +/** + * @since 1.0.0 + * @category Constructors + */ +export const fromConfig: ( + options?: { + readonly serviceName?: string | undefined + readonly serviceVersion?: string | undefined + readonly attributes?: Record | undefined + } | undefined +) => Effect.Effect = Effect.fnUntraced(function*(options?: { + readonly serviceName?: string | undefined + readonly serviceVersion?: string | undefined + readonly attributes?: Record | undefined +}) { + const attributes = yield* Config.string("OTEL_RESOURCE_ATTRIBUTES").pipe( + Config.map((s) => { + const attrs = s.split(",") + return Arr.reduce(attrs, {} as Record, (acc, attr) => { + const parts = attr.split("=") + if (parts.length !== 2) { + return acc + } + acc[parts[0].trim()] = parts[1].trim() + return acc + }) + }), + Config.withDefault({}), + Effect.map((envAttrs) => ({ + ...envAttrs, + ...options?.attributes + })) + ) + const serviceName = options?.serviceName ?? attributes[ATTR_SERVICE_NAME] as string ?? + (yield* Config.string("OTEL_SERVICE_NAME")) + const serviceVersion = options?.serviceVersion ?? attributes[ATTR_SERVICE_VERSION] as string ?? + (yield* Config.string("OTEL_SERVICE_VERSION").pipe(Config.withDefault(undefined))) + return make({ + serviceName, + serviceVersion, + attributes + }) +}, Effect.orDie) + +/** + * @since 1.0.0 + * @category Attributes + */ +export const unsafeServiceName = (resource: Resource): string => { + const serviceNameAttribute = resource.attributes.find( + (attr) => attr.key === ATTR_SERVICE_NAME + ) + if (!serviceNameAttribute || !serviceNameAttribute.value.stringValue) { + throw new Error("Resource does not contain a service name") + } + return serviceNameAttribute.value.stringValue +} + +/** + * @since 1.0.0 + * @category Attributes + */ +export const entriesToAttributes = (entries: Iterable<[string, unknown]>): Array => { + const attributes: Array = [] + for (const [key, value] of entries) { + attributes.push({ + key, + value: unknownToAttributeValue(value) + }) + } + return attributes +} + +/** + * @since 1.0.0 + * @category Attributes + */ +export const unknownToAttributeValue = (value: unknown): AnyValue => { + if (Array.isArray(value)) { + return { + arrayValue: { + values: value.map(unknownToAttributeValue) + } + } + } + switch (typeof value) { + case "string": + return { + stringValue: value + } + case "bigint": + return { + intValue: Number(value) + } + case "number": + return Number.isInteger(value) + ? { + intValue: value + } + : { + doubleValue: value + } + case "boolean": + return { + boolValue: value + } + default: + return { + stringValue: Inspectable.toStringUnknown(value) + } + } +} + +/** + * @since 1.0.0 + * @category Models + */ +export interface KeyValue { + /** KeyValue key */ + key: string + /** KeyValue value */ + value: AnyValue +} + +/** + * @since 1.0.0 + * @category Models + */ +export interface AnyValue { + /** AnyValue stringValue */ + stringValue?: string | null + /** AnyValue boolValue */ + boolValue?: boolean | null + /** AnyValue intValue */ + intValue?: number | null + /** AnyValue doubleValue */ + doubleValue?: number | null + /** AnyValue arrayValue */ + arrayValue?: ArrayValue + /** AnyValue kvlistValue */ + kvlistValue?: KeyValueList + /** AnyValue bytesValue */ + bytesValue?: Uint8Array +} + +/** + * @since 1.0.0 + * @category Models + */ +export interface ArrayValue { + /** ArrayValue values */ + values: Array +} + +/** + * @since 1.0.0 + * @category Models + */ +export interface KeyValueList { + /** KeyValueList values */ + values: Array +} + +/** + * @since 1.0.0 + * @category Models + */ +export interface LongBits { + low: number + high: number +} + +/** + * @since 1.0.0 + * @category Models + */ +export type Fixed64 = LongBits | string | number diff --git a/repos/effect/packages/opentelemetry/src/OtlpSerialization.ts b/repos/effect/packages/opentelemetry/src/OtlpSerialization.ts new file mode 100644 index 0000000..2174fab --- /dev/null +++ b/repos/effect/packages/opentelemetry/src/OtlpSerialization.ts @@ -0,0 +1,64 @@ +/** + * OtlpSerialization service for tree-shakable protobuf support. + * + * This module provides the `OtlpSerialization` service that abstracts the + * encoding of OTLP telemetry data to HttpBody. + * + * @since 1.0.0 + */ +import * as HttpBody from "@effect/platform/HttpBody" +import * as Context from "effect/Context" +import * as Layer from "effect/Layer" +import * as OtlpProtobuf from "./internal/otlpProtobuf.js" + +/** + * @since 1.0.0 + * @category Tags + */ +export class OtlpSerialization extends Context.Tag("@effect/opentelemetry/OtlpSerialization")< + OtlpSerialization, + { + /** + * Encodes trace data for transmission. + */ + readonly traces: (data: unknown) => HttpBody.HttpBody + /** + * Encodes metrics data for transmission. + */ + readonly metrics: (data: unknown) => HttpBody.HttpBody + /** + * Encodes logs data for transmission. + */ + readonly logs: (data: unknown) => HttpBody.HttpBody + } +>() {} + +/** + * JSON serializer layer for OTLP telemetry data. + * + * It encodes telemetry data as JSON with `application/json` content type. + * + * @since 1.0.0 + * @category Layers + */ +export const layerJson: Layer.Layer = Layer.succeed(OtlpSerialization, { + traces: (data) => HttpBody.unsafeJson(data), + metrics: (data) => HttpBody.unsafeJson(data), + logs: (data) => HttpBody.unsafeJson(data) +}) + +/** + * Protobuf serializer layer for OTLP telemetry data. + * + * This serializer encodes telemetry data using Protocol Buffers binary + * format with `application/x-protobuf` content type. It provides more + * efficient wire format compared to JSON. + * + * @since 1.0.0 + * @category Layers + */ +export const layerProtobuf: Layer.Layer = Layer.succeed(OtlpSerialization, { + traces: (data) => HttpBody.uint8Array(OtlpProtobuf.encodeTracesData(data as any), "application/x-protobuf"), + metrics: (data) => HttpBody.uint8Array(OtlpProtobuf.encodeMetricsData(data as any), "application/x-protobuf"), + logs: (data) => HttpBody.uint8Array(OtlpProtobuf.encodeLogsData(data as any), "application/x-protobuf") +}) diff --git a/repos/effect/packages/opentelemetry/src/OtlpTracer.ts b/repos/effect/packages/opentelemetry/src/OtlpTracer.ts new file mode 100644 index 0000000..1a6130d --- /dev/null +++ b/repos/effect/packages/opentelemetry/src/OtlpTracer.ts @@ -0,0 +1,352 @@ +/** + * @since 1.0.0 + */ +import type * as Headers from "@effect/platform/Headers" +import type * as HttpClient from "@effect/platform/HttpClient" +import * as Cause from "effect/Cause" +import type * as Context from "effect/Context" +import * as Duration from "effect/Duration" +import * as Effect from "effect/Effect" +import type * as Exit from "effect/Exit" +import * as Layer from "effect/Layer" +import * as Option from "effect/Option" +import type * as Scope from "effect/Scope" +import * as Tracer from "effect/Tracer" +import type { ExtractTag } from "effect/Types" +import * as Exporter from "./internal/otlpExporter.js" +import type { KeyValue, Resource } from "./OtlpResource.js" +import { entriesToAttributes } from "./OtlpResource.js" +import * as OtlpResource from "./OtlpResource.js" +import { OtlpSerialization } from "./OtlpSerialization.js" + +const ATTR_EXCEPTION_TYPE = "exception.type" +const ATTR_EXCEPTION_MESSAGE = "exception.message" +const ATTR_EXCEPTION_STACKTRACE = "exception.stacktrace" + +/** + * @since 1.0.0 + * @category Constructors + */ +export const make: ( + options: { + readonly url: string + readonly resource?: { + readonly serviceName?: string | undefined + readonly serviceVersion?: string | undefined + readonly attributes?: Record + } | undefined + readonly headers?: Headers.Input | undefined + readonly exportInterval?: Duration.DurationInput | undefined + readonly maxBatchSize?: number | undefined + readonly context?: ((f: () => X, span: Tracer.AnySpan) => X) | undefined + readonly shutdownTimeout?: Duration.DurationInput | undefined + } +) => Effect.Effect< + Tracer.Tracer, + never, + HttpClient.HttpClient | OtlpSerialization | Scope.Scope +> = Effect.fnUntraced(function*(options) { + const otelResource = yield* OtlpResource.fromConfig(options.resource) + const scope: Scope = { + name: OtlpResource.unsafeServiceName(otelResource) + } + const serialization = yield* OtlpSerialization + + const exporter = yield* Exporter.make({ + label: "OtlpTracer", + url: options.url, + headers: options.headers, + exportInterval: options.exportInterval ?? Duration.seconds(5), + maxBatchSize: options.maxBatchSize ?? 1000, + body(spans) { + const data: TraceData = { + resourceSpans: [{ + resource: otelResource, + scopeSpans: [{ + scope, + spans + }] + }] + } + return serialization.traces(data) + }, + shutdownTimeout: options.shutdownTimeout ?? Duration.seconds(3) + }) + + const exportFn = (span: SpanImpl) => { + exporter.push(makeOtlpSpan(span)) + } + + return Tracer.make({ + span(name, parent, context, links, startTime, kind) { + return makeSpan({ + name, + parent, + context, + status: { + _tag: "Started", + startTime + }, + attributes: new Map(), + links, + sampled: true, + kind, + export: exportFn + }) + }, + context: options.context ? + function(f, fiber) { + if (fiber.currentSpan === undefined) { + return f() + } + return options.context!(f, fiber.currentSpan) + } : + defaultContext + }) +}) + +/** + * @since 1.0.0 + * @category Layers + */ +export const layer = (options: { + readonly url: string + readonly resource?: { + readonly serviceName?: string | undefined + readonly serviceVersion?: string | undefined + readonly attributes?: Record + } | undefined + readonly headers?: Headers.Input | undefined + readonly exportInterval?: Duration.DurationInput | undefined + readonly maxBatchSize?: number | undefined + readonly context?: ((f: () => X, span: Tracer.AnySpan) => X) | undefined + readonly shutdownTimeout?: Duration.DurationInput | undefined +}): Layer.Layer => + Layer.unwrapScoped(Effect.map(make(options), Layer.setTracer)) + +// internal + +function defaultContext(f: () => X, _: any): X { + return f() +} + +interface SpanImpl extends Tracer.Span { + readonly export: (span: SpanImpl) => void + readonly attributes: Map + readonly links: Array + readonly events: Array<[name: string, startTime: bigint, attributes: Record | undefined]> + status: Tracer.SpanStatus +} + +const SpanProto = { + _tag: "Span", + end(this: SpanImpl, endTime: bigint, exit: Exit.Exit) { + this.status = { + _tag: "Ended", + startTime: this.status.startTime, + endTime, + exit + } + this.export(this) + }, + attribute(this: SpanImpl, key: string, value: unknown) { + this.attributes.set(key, value) + }, + event(this: SpanImpl, name: string, startTime: bigint, attributes?: Record) { + this.events.push([name, startTime, attributes]) + }, + addLinks(this: SpanImpl, links: ReadonlyArray) { + // eslint-disable-next-line no-restricted-syntax + this.links.push(...links) + } +} + +const makeSpan = (options: { + readonly name: string + readonly parent: Option.Option + readonly context: Context.Context + readonly status: Tracer.SpanStatus + readonly attributes: ReadonlyMap + readonly links: ReadonlyArray + readonly sampled: boolean + readonly kind: Tracer.SpanKind + readonly export: (span: SpanImpl) => void +}): SpanImpl => { + const self = Object.assign(Object.create(SpanProto), options) + if (Option.isSome(self.parent)) { + self.traceId = self.parent.value.traceId + } else { + self.traceId = generateId(32) + } + self.spanId = generateId(16) + self.events = [] + return self +} + +const generateId = (len: number): string => { + const chars = "0123456789abcdef" + let result = "" + for (let i = 0; i < len; i++) { + result += chars[Math.floor(Math.random() * chars.length)] + } + return result +} + +const makeOtlpSpan = (self: SpanImpl): OtlpSpan => { + const status = self.status as ExtractTag + const attributes = entriesToAttributes(self.attributes.entries()) + const events = self.events.map(([name, startTime, attributes]) => ({ + name, + timeUnixNano: String(startTime), + attributes: attributes + ? entriesToAttributes(Object.entries(attributes)) + : [], + droppedAttributesCount: 0 + })) + let otelStatus: Status + + if (status.exit._tag === "Success") { + otelStatus = constOtelStatusSuccess + } else if (Cause.isInterruptedOnly(status.exit.cause)) { + otelStatus = { + code: StatusCode.Ok, + message: Cause.pretty(status.exit.cause) + } + } else { + const errors = Cause.prettyErrors(status.exit.cause) + const firstError = errors[0] + otelStatus = { + code: StatusCode.Error + } + attributes.push({ + key: "span.label", + value: { stringValue: "⚠︎ Interrupted" } + }, { + key: "status.interrupted", + value: { boolValue: true } + }) + if (firstError) { + otelStatus.message = firstError.message + events.push({ + name: "exception", + timeUnixNano: String(status.endTime), + droppedAttributesCount: 0, + attributes: [ + { + "key": ATTR_EXCEPTION_TYPE, + "value": { + "stringValue": firstError.name + } + }, + { + "key": ATTR_EXCEPTION_MESSAGE, + "value": { + "stringValue": firstError.message + } + }, + { + "key": ATTR_EXCEPTION_STACKTRACE, + "value": { + "stringValue": Cause.pretty(status.exit.cause, { renderErrorCause: true }) + } + } + ] + }) + } + } + + return { + traceId: self.traceId, + spanId: self.spanId, + parentSpanId: Option.isSome(self.parent) ? self.parent.value.spanId : undefined, + name: self.name, + kind: SpanKind[self.kind], + startTimeUnixNano: String(status.startTime), + endTimeUnixNano: String(status.endTime), + attributes, + droppedAttributesCount: 0, + events, + droppedEventsCount: 0, + status: otelStatus, + links: self.links.map((link) => ({ + traceId: link.span.traceId, + spanId: link.span.spanId, + attributes: entriesToAttributes(Object.entries(link.attributes)), + droppedAttributesCount: 0 + })), + droppedLinksCount: 0 + } +} + +interface TraceData { + readonly resourceSpans: Array +} + +interface ResourceSpan { + readonly resource: Resource + readonly scopeSpans: Array +} + +interface ScopeSpan { + readonly scope: Scope + readonly spans: Array +} + +interface Scope { + readonly name: string +} + +interface OtlpSpan { + readonly traceId: string + readonly spanId: string + readonly parentSpanId: string | undefined + readonly name: string + readonly kind: number + readonly startTimeUnixNano: string + readonly endTimeUnixNano: string + readonly attributes: Array + readonly droppedAttributesCount: number + readonly events: Array + readonly droppedEventsCount: number + readonly status: Status + readonly links: Array + readonly droppedLinksCount: number +} + +interface Event { + readonly attributes: Array + readonly name: string + readonly timeUnixNano: string + readonly droppedAttributesCount: number +} + +interface Link { + readonly attributes: Array + readonly spanId: string + readonly traceId: string + readonly droppedAttributesCount: number +} + +interface Status { + readonly code: StatusCode + message?: string +} + +const enum StatusCode { + Unset = 0, + Ok = 1, + Error = 2 +} + +enum SpanKind { + unspecified = 0, + internal = 1, + server = 2, + client = 3, + producer = 4, + consumer = 5 +} + +const constOtelStatusSuccess: Status = { + code: StatusCode.Ok +} diff --git a/repos/effect/packages/opentelemetry/src/Resource.ts b/repos/effect/packages/opentelemetry/src/Resource.ts new file mode 100644 index 0000000..75611a3 --- /dev/null +++ b/repos/effect/packages/opentelemetry/src/Resource.ts @@ -0,0 +1,111 @@ +/** + * @since 1.0.0 + */ +import type * as OtelApi from "@opentelemetry/api" +import * as Resources from "@opentelemetry/resources" +import * as OtelSemConv from "@opentelemetry/semantic-conventions" +import * as Arr from "effect/Array" +import * as Config from "effect/Config" +import { GenericTag } from "effect/Context" +import * as Effect from "effect/Effect" +import { pipe } from "effect/Function" +import * as Layer from "effect/Layer" + +/** + * @since 1.0.0 + * @category identifier + */ +export interface Resource { + readonly _: unique symbol +} + +/** + * @since 1.0.0 + * @category tag + */ +export const Resource = GenericTag("@effect/opentelemetry/Resource") + +/** + * @since 1.0.0 + * @category layer + */ +export const layer = (config: { + readonly serviceName: string + readonly serviceVersion?: string + readonly attributes?: OtelApi.Attributes +}) => + Layer.succeed( + Resource, + Resources.resourceFromAttributes(configToAttributes(config)) + ) + +/** + * @since 1.0.0 + * @category config + */ +export const configToAttributes = (options: { + readonly serviceName: string + readonly serviceVersion?: string + readonly attributes?: OtelApi.Attributes +}): Record => { + const attributes: Record = { + ...(options.attributes ?? undefined), + [OtelSemConv.ATTR_SERVICE_NAME]: options.serviceName, + [OtelSemConv.ATTR_TELEMETRY_SDK_NAME]: "@effect/opentelemetry", + [OtelSemConv.ATTR_TELEMETRY_SDK_LANGUAGE]: typeof (globalThis as any).document === "undefined" + ? OtelSemConv.TELEMETRY_SDK_LANGUAGE_VALUE_NODEJS + : OtelSemConv.TELEMETRY_SDK_LANGUAGE_VALUE_WEBJS + } + if (options.serviceVersion) { + attributes[OtelSemConv.ATTR_SERVICE_VERSION] = options.serviceVersion + } + return attributes +} + +/** + * @since 1.0.0 + * @category layer + */ +export const layerFromEnv = ( + additionalAttributes?: + | OtelApi.Attributes + | undefined +): Layer.Layer => + Layer.effect( + Resource, + Effect.gen(function*() { + const serviceName = yield* pipe(Config.string("OTEL_SERVICE_NAME"), Config.option, Effect.orDie) + const attributes = yield* pipe( + Config.string("OTEL_RESOURCE_ATTRIBUTES"), + Config.withDefault(""), + Config.map((s) => { + const attrs = s.split(",") + return Arr.reduce(attrs, {} as OtelApi.Attributes, (acc, attr) => { + const parts = attr.split("=") + if (parts.length !== 2) { + return acc + } + acc[parts[0].trim()] = parts[1].trim() + return acc + }) + }), + Effect.orDie + ) + if (serviceName._tag === "Some") { + attributes[OtelSemConv.ATTR_SERVICE_NAME] = serviceName.value + } + if (additionalAttributes) { + Object.assign(attributes, additionalAttributes) + } + return Resources.resourceFromAttributes(attributes) + }) + ) + +/** + * @since 2.0.0 + * @category layer + */ +export const layerEmpty = Layer.succeed( + Resource, + Resources.emptyResource() +) diff --git a/repos/effect/packages/opentelemetry/src/Tracer.ts b/repos/effect/packages/opentelemetry/src/Tracer.ts new file mode 100644 index 0000000..2814062 --- /dev/null +++ b/repos/effect/packages/opentelemetry/src/Tracer.ts @@ -0,0 +1,151 @@ +/** + * @since 1.0.0 + */ +import type * as Otel from "@opentelemetry/api" +import type { NoSuchElementException } from "effect/Cause" +import type { Tag } from "effect/Context" +import type { Effect } from "effect/Effect" +import type { Layer } from "effect/Layer" +import type { ExternalSpan, ParentSpan, Tracer as EffectTracer } from "effect/Tracer" +import * as internal from "./internal/tracer.js" +import type { Resource } from "./Resource.js" + +/** + * @since 1.0.0 + * @category constructors + */ +export const make: Effect = internal.make + +/** + * @since 1.0.0 + * @category constructors + */ +export const makeExternalSpan: ( + options: { + readonly traceId: string + readonly spanId: string + readonly traceFlags?: number | undefined + readonly traceState?: string | Otel.TraceState | undefined + } +) => ExternalSpan = internal.makeExternalSpan + +/** + * Get the current OpenTelemetry span. + * + * Works with both the official OpenTelemetry API (via `Tracer.layer`, `NodeSdk.layer`, etc.) + * and the lightweight OTLP module (`OtlpTracer.layer`). + * + * When using OTLP, the returned span is a wrapper that conforms to the + * OpenTelemetry `Span` interface. + * + * @since 1.0.0 + * @category accessors + */ +export const currentOtelSpan: Effect = internal.currentOtelSpan + +/** + * @since 1.0.0 + * @category layers + */ +export const layerWithoutOtelTracer: Layer = internal.layerWithoutOtelTracer + +/** + * @since 1.0.0 + * @category layers + */ +export const layer: Layer = internal.layer + +/** + * @since 1.0.0 + * @category layers + */ +export const layerGlobal: Layer = internal.layerGlobal + +/** + * @since 1.0.0 + * @category layers + */ +export const layerTracer: Layer = internal.layerTracer + +/** + * @since 1.0.0 + * @category layers + */ +export const layerGlobalTracer: Layer = internal.layerGlobalTracer + +/** + * @since 1.0.0 + * @category tags + */ +export interface OtelTracerProvider { + readonly _: unique symbol +} + +/** + * @since 1.0.0 + * @category tags + */ +export const OtelTracerProvider: Tag = internal.TracerProvider + +/** + * @since 1.0.0 + * @category tags + */ +export interface OtelTracer { + readonly _: unique symbol +} + +/** + * @since 1.0.0 + * @category tags + */ +export const OtelTracer: Tag = internal.Tracer + +/** + * @since 1.0.0 + * @category tags + */ +export interface OtelTraceFlags { + readonly _: unique symbol +} + +/** + * @since 1.0.0 + * @category tags + */ +export const OtelTraceFlags: Tag = internal.traceFlagsTag + +/** + * @since 1.0.0 + * @category tags + */ +export interface OtelTraceState { + readonly _: unique symbol +} + +/** + * @since 1.0.0 + * @category tags + */ +export const OtelTraceState: Tag = internal.traceStateTag + +/** + * Set the effect's parent span from the given opentelemetry `SpanContext`. + * + * This is handy when you set up OpenTelemetry outside of Effect and want to + * attach to a parent span. + * + * @since 1.0.0 + * @category propagation + */ +export const withSpanContext: { + ( + spanContext: Otel.SpanContext + ): ( + effect: Effect + ) => Effect> + ( + effect: Effect, + spanContext: Otel.SpanContext + ): Effect> +} = internal.withSpanContext diff --git a/repos/effect/packages/opentelemetry/src/WebSdk.ts b/repos/effect/packages/opentelemetry/src/WebSdk.ts new file mode 100644 index 0000000..9f09315 --- /dev/null +++ b/repos/effect/packages/opentelemetry/src/WebSdk.ts @@ -0,0 +1,101 @@ +/** + * @since 1.0.0 + */ +import type * as OtelApi from "@opentelemetry/api" +import type { LoggerProviderConfig, LogRecordProcessor } from "@opentelemetry/sdk-logs" +import type { MetricReader } from "@opentelemetry/sdk-metrics" +import type { SpanProcessor, TracerConfig } from "@opentelemetry/sdk-trace-base" +import { WebTracerProvider } from "@opentelemetry/sdk-trace-web" +import type { NonEmptyReadonlyArray } from "effect/Array" +import * as Effect from "effect/Effect" +import { constant, type LazyArg } from "effect/Function" +import * as Layer from "effect/Layer" +import { isNonEmpty } from "./internal/utils.js" +import * as Logger from "./Logger.js" +import * as Metrics from "./Metrics.js" +import * as Resource from "./Resource.js" +import * as Tracer from "./Tracer.js" + +/** + * @since 1.0.0 + * @category model + */ +export interface Configuration { + readonly spanProcessor?: SpanProcessor | ReadonlyArray | undefined + readonly tracerConfig?: Omit + readonly metricReader?: MetricReader | ReadonlyArray | undefined + readonly logRecordProcessor?: LogRecordProcessor | ReadonlyArray | undefined + readonly loggerProviderConfig?: Omit | undefined + readonly resource: { + readonly serviceName: string + readonly serviceVersion?: string + readonly attributes?: OtelApi.Attributes + } +} + +/** + * @since 1.0.0 + * @category layers + */ +export const layerTracerProvider = ( + processor: SpanProcessor | NonEmptyReadonlyArray, + config?: Omit +): Layer.Layer => + Layer.scoped( + Tracer.OtelTracerProvider, + Effect.flatMap( + Resource.Resource, + (resource) => + Effect.acquireRelease( + Effect.sync(() => { + const provider = new WebTracerProvider({ + ...(config ?? undefined), + resource, + spanProcessors: Array.isArray(processor) ? (processor as any) : [processor] + }) + return provider + }), + (provider) => Effect.ignoreLogged(Effect.promise(() => provider.forceFlush().then(() => provider.shutdown()))) + ) + ) + ) + +/** + * @since 1.0.0 + * @category layer + */ +export const layer: { + (evaluate: LazyArg): Layer.Layer + (evaluate: Effect.Effect): Layer.Layer +} = ( + evaluate: LazyArg | Effect.Effect +): Layer.Layer => + Layer.unwrapEffect( + Effect.map( + Effect.isEffect(evaluate) + ? evaluate as Effect.Effect + : Effect.sync(evaluate), + (config) => { + const ResourceLive = Resource.layer(config.resource) + + const TracerLive = isNonEmpty(config.spanProcessor) + ? Layer.provide(Tracer.layer, layerTracerProvider(config.spanProcessor, config.tracerConfig)) + : Layer.empty + + const LoggerLive = isNonEmpty(config.logRecordProcessor) + ? Layer.provide( + Logger.layerLoggerAdd, + Logger.layerLoggerProvider(config.logRecordProcessor, config.loggerProviderConfig) + ) + : Layer.empty + + const MetricsLive = isNonEmpty(config.metricReader) + ? Metrics.layer(constant(config.metricReader)) + : Layer.empty + + return Layer.mergeAll(TracerLive, MetricsLive, LoggerLive).pipe( + Layer.provideMerge(ResourceLive) + ) + } + ) + ) diff --git a/repos/effect/packages/opentelemetry/src/index.ts b/repos/effect/packages/opentelemetry/src/index.ts new file mode 100644 index 0000000..2e5985a --- /dev/null +++ b/repos/effect/packages/opentelemetry/src/index.ts @@ -0,0 +1,64 @@ +/** + * @since 1.0.0 + */ +export * as Logger from "./Logger.js" + +/** + * @since 1.0.0 + */ +export * as Metrics from "./Metrics.js" + +/** + * @since 1.0.0 + */ +export * as NodeSdk from "./NodeSdk.js" + +/** + * @since 1.0.0 + */ +export * as Otlp from "./Otlp.js" + +/** + * @since 1.0.0 + */ +export * as OtlpLogger from "./OtlpLogger.js" + +/** + * @since 1.0.0 + */ +export * as OtlpMetrics from "./OtlpMetrics.js" + +/** + * @since 1.0.0 + */ +export * as OtlpResource from "./OtlpResource.js" + +/** + * OtlpSerialization service for tree-shakable protobuf support. + * + * This module provides the `OtlpSerialization` service that abstracts the + * encoding of OTLP telemetry data to HttpBody. + * + * @since 1.0.0 + */ +export * as OtlpSerialization from "./OtlpSerialization.js" + +/** + * @since 1.0.0 + */ +export * as OtlpTracer from "./OtlpTracer.js" + +/** + * @since 1.0.0 + */ +export * as Resource from "./Resource.js" + +/** + * @since 1.0.0 + */ +export * as Tracer from "./Tracer.js" + +/** + * @since 1.0.0 + */ +export * as WebSdk from "./WebSdk.js" diff --git a/repos/effect/packages/opentelemetry/src/internal/metrics.ts b/repos/effect/packages/opentelemetry/src/internal/metrics.ts new file mode 100644 index 0000000..125dabb --- /dev/null +++ b/repos/effect/packages/opentelemetry/src/internal/metrics.ts @@ -0,0 +1,332 @@ +import type { HrTime } from "@opentelemetry/api" +import { ValueType } from "@opentelemetry/api" +import type * as Resources from "@opentelemetry/resources" +import type { + CollectionResult, + DataPoint, + Histogram, + MetricCollectOptions, + MetricData, + MetricProducer, + MetricReader +} from "@opentelemetry/sdk-metrics" +import { AggregationTemporality, DataPointType, InstrumentType } from "@opentelemetry/sdk-metrics" +import type { InstrumentDescriptor } from "@opentelemetry/sdk-metrics/build/src/InstrumentDescriptor.js" +import * as Arr from "effect/Array" +import type { DurationInput } from "effect/Duration" +import * as Effect from "effect/Effect" +import type { LazyArg } from "effect/Function" +import * as Layer from "effect/Layer" +import * as Metric from "effect/Metric" +import type * as MetricKey from "effect/MetricKey" +import * as MetricKeyType from "effect/MetricKeyType" +import * as MetricState from "effect/MetricState" +import * as Option from "effect/Option" +import * as Resource from "../Resource.js" + +const sdkName = "@effect/opentelemetry/Metrics" + +type MetricDataWithInstrumentDescriptor = MetricData & { + readonly descriptor: InstrumentDescriptor +} + +/** @internal */ +export class MetricProducerImpl implements MetricProducer { + constructor(readonly resource: Resources.Resource) {} + + startTimes = new Map() + + startTimeFor(name: string, hrTime: HrTime) { + if (this.startTimes.has(name)) { + return this.startTimes.get(name)! + } + this.startTimes.set(name, hrTime) + return hrTime + } + + collect(_options?: MetricCollectOptions): Promise { + const snapshot = Metric.unsafeSnapshot() + const hrTimeNow = currentHrTime() + const metricData: Array = [] + const metricDataByName = new Map() + const addMetricData = (data: MetricDataWithInstrumentDescriptor) => { + metricData.push(data) + metricDataByName.set(data.descriptor.name, data) + } + + for (let i = 0, len = snapshot.length; i < len; i++) { + const { metricKey, metricState } = snapshot[i] + const attributes = Arr.reduce(metricKey.tags, {}, (acc: Record, label) => { + acc[label.key] = label.value + return acc + }) + const descriptor = descriptorFromKey(metricKey, attributes) + const startTime = this.startTimeFor(descriptor.name, hrTimeNow) + + if (MetricState.isCounterState(metricState)) { + const dataPoint: DataPoint = { + startTime, + endTime: hrTimeNow, + attributes, + value: Number(metricState.count) + } + if (metricDataByName.has(descriptor.name)) { + metricDataByName.get(descriptor.name)!.dataPoints.push(dataPoint as any) + } else { + addMetricData({ + dataPointType: DataPointType.SUM, + descriptor, + isMonotonic: descriptor.type === InstrumentType.COUNTER, + aggregationTemporality: AggregationTemporality.CUMULATIVE, + dataPoints: [dataPoint] + }) + } + } else if (MetricState.isGaugeState(metricState)) { + const dataPoint: DataPoint = { + startTime, + endTime: hrTimeNow, + attributes, + value: Number(metricState.value) + } + if (metricDataByName.has(descriptor.name)) { + metricDataByName.get(descriptor.name)!.dataPoints.push(dataPoint as any) + } else { + addMetricData({ + dataPointType: DataPointType.GAUGE, + descriptor, + aggregationTemporality: AggregationTemporality.CUMULATIVE, + dataPoints: [dataPoint] + }) + } + } else if (MetricState.isHistogramState(metricState)) { + const size = metricState.buckets.length + const buckets = { + boundaries: Arr.allocate(size - 1) as Array, + counts: Arr.allocate(size) as Array + } + let i = 0 + let prev = 0 + for (const [boundary, value] of metricState.buckets) { + if (i < size - 1) { + buckets.boundaries[i] = boundary + } + buckets.counts[i] = value - prev + prev = value + i++ + } + const dataPoint: DataPoint = { + startTime, + endTime: hrTimeNow, + attributes, + value: { + buckets, + count: metricState.count, + min: metricState.min, + max: metricState.max, + sum: metricState.sum + } + } + + if (metricDataByName.has(descriptor.name)) { + metricDataByName.get(descriptor.name)!.dataPoints.push(dataPoint as any) + } else { + addMetricData({ + dataPointType: DataPointType.HISTOGRAM, + descriptor, + aggregationTemporality: AggregationTemporality.CUMULATIVE, + dataPoints: [dataPoint] + }) + } + } else if (MetricState.isFrequencyState(metricState)) { + const dataPoints: Array> = [] + for (const [freqKey, value] of metricState.occurrences) { + dataPoints.push({ + startTime, + endTime: hrTimeNow, + attributes: { + ...attributes, + key: freqKey + }, + value + }) + } + if (metricDataByName.has(descriptor.name)) { + // eslint-disable-next-line no-restricted-syntax + metricDataByName.get(descriptor.name)!.dataPoints.push(...dataPoints as any) + } else { + addMetricData({ + dataPointType: DataPointType.SUM, + descriptor: descriptorFromKey(metricKey, attributes), + aggregationTemporality: AggregationTemporality.CUMULATIVE, + isMonotonic: true, + dataPoints + }) + } + } else if (MetricState.isSummaryState(metricState)) { + const dataPoints: Array> = [{ + startTime, + endTime: hrTimeNow, + attributes: { ...attributes, quantile: "min" }, + value: metricState.min + }] + for (const [quantile, value] of metricState.quantiles) { + dataPoints.push({ + startTime, + endTime: hrTimeNow, + attributes: { ...attributes, quantile: quantile.toString() }, + value: value._tag === "Some" ? value.value : 0 + }) + } + dataPoints.push({ + startTime, + endTime: hrTimeNow, + attributes: { ...attributes, quantile: "max" }, + value: metricState.max + }) + const countDataPoint: DataPoint = { + startTime, + endTime: hrTimeNow, + attributes, + value: metricState.count + } + const sumDataPoint: DataPoint = { + startTime, + endTime: hrTimeNow, + attributes, + value: metricState.sum + } + + if (metricDataByName.has(`${descriptor.name}_quantiles`)) { + // eslint-disable-next-line no-restricted-syntax + metricDataByName.get(`${descriptor.name}_quantiles`)!.dataPoints.push(...dataPoints as any) + metricDataByName.get(`${descriptor.name}_count`)!.dataPoints.push(countDataPoint as any) + metricDataByName.get(`${descriptor.name}_sum`)!.dataPoints.push(sumDataPoint as any) + } else { + addMetricData({ + dataPointType: DataPointType.SUM, + descriptor: descriptorFromKey(metricKey, attributes, "quantiles"), + aggregationTemporality: AggregationTemporality.CUMULATIVE, + isMonotonic: false, + dataPoints + }) + addMetricData({ + dataPointType: DataPointType.SUM, + descriptor: { + ...descriptorMeta(metricKey, "count"), + unit: "1", + type: InstrumentType.COUNTER, + valueType: ValueType.INT + }, + aggregationTemporality: AggregationTemporality.CUMULATIVE, + isMonotonic: true, + dataPoints: [countDataPoint] + }) + addMetricData({ + dataPointType: DataPointType.SUM, + descriptor: { + ...descriptorMeta(metricKey, "sum"), + unit: "1", + type: InstrumentType.COUNTER, + valueType: ValueType.DOUBLE + }, + aggregationTemporality: AggregationTemporality.CUMULATIVE, + isMonotonic: true, + dataPoints: [sumDataPoint] + }) + } + } + } + + return Promise.resolve({ + resourceMetrics: { + resource: this.resource, + scopeMetrics: [{ + scope: { name: sdkName }, + metrics: metricData + }] + }, + errors: [] + }) + } +} + +const descriptorMeta = ( + metricKey: MetricKey.MetricKey.Untyped, + suffix?: string +) => ({ + name: suffix ? `${metricKey.name}_${suffix}` : metricKey.name, + description: Option.getOrElse(metricKey.description, () => ""), + advice: {} +}) + +const descriptorFromKey = ( + metricKey: MetricKey.MetricKey.Untyped, + tags: Record, + suffix?: string +): InstrumentDescriptor => ({ + ...descriptorMeta(metricKey, suffix), + unit: tags.unit ?? tags.time_unit ?? "1", + type: instrumentTypeFromKey(metricKey), + valueType: "bigint" in metricKey.keyType && metricKey.keyType.bigint === true ? ValueType.INT : ValueType.DOUBLE +}) + +const instrumentTypeFromKey = (key: MetricKey.MetricKey.Untyped): InstrumentType => { + if (MetricKeyType.isHistogramKey(key.keyType)) { + return InstrumentType.HISTOGRAM + } else if (MetricKeyType.isGaugeKey(key.keyType)) { + return InstrumentType.OBSERVABLE_GAUGE + } else if (MetricKeyType.isFrequencyKey(key.keyType)) { + return InstrumentType.COUNTER + } else if (MetricKeyType.isCounterKey(key.keyType) && key.keyType.incremental) { + return InstrumentType.COUNTER + } + + return InstrumentType.UP_DOWN_COUNTER +} + +const currentHrTime = (): HrTime => { + const now = Date.now() + return [Math.floor(now / 1000), (now % 1000) * 1000000] +} + +/** @internal */ +export const makeProducer = Effect.map( + Resource.Resource, + (resource): MetricProducer => new MetricProducerImpl(resource) +) + +/** @internal */ +export const registerProducer = ( + self: MetricProducer, + metricReader: LazyArg>, + options?: { + readonly shutdownTimeout?: DurationInput | undefined + } +) => + Effect.acquireRelease( + Effect.sync(() => { + const reader = metricReader() + const readers: Array = Array.isArray(reader) ? reader : [reader] as any + readers.forEach((reader) => reader.setMetricProducer(self)) + return readers + }), + (readers) => + Effect.promise(() => + Promise.all( + readers.map((reader) => reader.shutdown()) + ) + ).pipe( + Effect.ignoreLogged, + Effect.interruptible, + Effect.timeoutOption(options?.shutdownTimeout ?? 3000) + ) + ) + +/** @internal */ +export const layer = (evaluate: LazyArg>, options?: { + readonly shutdownTimeout?: DurationInput | undefined +}) => + Layer.scopedDiscard(Effect.flatMap( + makeProducer, + (producer) => registerProducer(producer, evaluate, options) + )) diff --git a/repos/effect/packages/opentelemetry/src/internal/otlpExporter.ts b/repos/effect/packages/opentelemetry/src/internal/otlpExporter.ts new file mode 100644 index 0000000..1e54798 --- /dev/null +++ b/repos/effect/packages/opentelemetry/src/internal/otlpExporter.ts @@ -0,0 +1,126 @@ +import * as Headers from "@effect/platform/Headers" +import type * as HttpBody from "@effect/platform/HttpBody" +import * as HttpClient from "@effect/platform/HttpClient" +import * as HttpClientError from "@effect/platform/HttpClientError" +import * as HttpClientRequest from "@effect/platform/HttpClientRequest" +import * as Duration from "effect/Duration" +import * as Effect from "effect/Effect" +import * as FiberSet from "effect/FiberSet" +import * as Num from "effect/Number" +import * as Option from "effect/Option" +import * as Schedule from "effect/Schedule" +import * as Scope from "effect/Scope" + +const policy = Schedule.forever.pipe( + Schedule.passthrough, + Schedule.addDelay((error) => { + if ( + HttpClientError.isHttpClientError(error) + && error._tag === "ResponseError" + && error.response.status === 429 + ) { + const retryAfter = Option.fromNullable(error.response.headers["retry-after"]).pipe( + Option.flatMap(Num.parse), + Option.getOrElse(() => 5) + ) + return Duration.seconds(retryAfter) + } + return Duration.seconds(1) + }) +) + +/** @internal */ +export const make: ( + options: { + readonly url: string + readonly headers: Headers.Input | undefined + readonly label: string + readonly exportInterval: Duration.DurationInput + readonly maxBatchSize: number | "disabled" + readonly body: (data: Array) => HttpBody.HttpBody + readonly shutdownTimeout: Duration.DurationInput + } +) => Effect.Effect< + { readonly push: (data: unknown) => void }, + never, + HttpClient.HttpClient | Scope.Scope +> = Effect.fnUntraced(function*(options) { + const clock = yield* Effect.clock + const scope = yield* Effect.scope + const exportInterval = Duration.decode(options.exportInterval) + let disabledUntil: number | undefined = undefined + + const client = HttpClient.filterStatusOk(yield* HttpClient.HttpClient).pipe( + HttpClient.retryTransient({ schedule: policy, times: 3 }) + ) + + let headers = Headers.unsafeFromRecord({ + "user-agent": `effect-opentelemetry-${options.label}/0.0.0` + }) + if (options.headers) { + headers = Headers.merge(Headers.fromInput(options.headers), headers) + } + + const request = HttpClientRequest.post(options.url, { headers }) + let buffer: Array = [] + const runExport = Effect.suspend(() => { + if (disabledUntil !== undefined && clock.unsafeCurrentTimeMillis() < disabledUntil) { + return Effect.void + } else if (disabledUntil !== undefined) { + disabledUntil = undefined + } + const items = buffer + if (options.maxBatchSize !== "disabled") { + if (buffer.length === 0) { + return Effect.void + } + buffer = [] + } + const body = options.body(items) + const requestWithBody = HttpClientRequest.setBody(request, body) + + return client.execute(requestWithBody).pipe( + Effect.asVoid, + Effect.withTracerEnabled(false) + ) + }).pipe( + Effect.catchAllCause((cause) => { + if (disabledUntil !== undefined) return Effect.void + disabledUntil = clock.unsafeCurrentTimeMillis() + Duration.toMillis("1 minute") + return Effect.logDebug(`Disabling ${options.label} for 60 seconds`, cause) + }) + ) + + yield* Scope.addFinalizer( + scope, + runExport.pipe( + Effect.ignore, + Effect.interruptible, + Effect.timeoutOption(options.shutdownTimeout) + ) + ) + + yield* Effect.sleep(exportInterval).pipe( + Effect.zipRight(runExport), + Effect.forever, + Effect.annotateLogs({ + package: "@effect/opentelemetry", + module: options.label + }), + Effect.forkIn(scope), + Effect.interruptible + ) + + const runFork = yield* FiberSet.makeRuntime().pipe( + Effect.interruptible + ) + return { + push(data) { + if (disabledUntil !== undefined) return + buffer.push(data) + if (options.maxBatchSize !== "disabled" && buffer.length >= options.maxBatchSize) { + runFork(runExport) + } + } + } +}) diff --git a/repos/effect/packages/opentelemetry/src/internal/otlpProtobuf.ts b/repos/effect/packages/opentelemetry/src/internal/otlpProtobuf.ts new file mode 100644 index 0000000..26d7f46 --- /dev/null +++ b/repos/effect/packages/opentelemetry/src/internal/otlpProtobuf.ts @@ -0,0 +1,729 @@ +/** + * OTLP Protobuf encoding for traces, metrics, and logs. + * + * Implements the protobuf wire format according to: + * https://github.com/open-telemetry/opentelemetry-proto + * + * @internal + */ + +import type { AnyValue, KeyValue, Resource } from "../OtlpResource.js" +import * as Proto from "./protobuf.js" + +// Common types (opentelemetry.proto.common.v1) + +/** + * Encodes an AnyValue message. + * + * message AnyValue { + * oneof value { + * string string_value = 1; + * bool bool_value = 2; + * int64 int_value = 3; + * double double_value = 4; + * ArrayValue array_value = 5; + * KeyValueList kvlist_value = 6; + * bytes bytes_value = 7; + * } + * } + */ +export const encodeAnyValue = (value: AnyValue): Uint8Array => { + if (value.stringValue !== undefined && value.stringValue !== null) { + return Proto.stringField(1, value.stringValue) + } + if (value.boolValue !== undefined && value.boolValue !== null) { + return Proto.boolField(2, value.boolValue) + } + if (value.intValue !== undefined && value.intValue !== null) { + return Proto.varintField(3, BigInt(value.intValue)) + } + if (value.doubleValue !== undefined && value.doubleValue !== null) { + return Proto.doubleField(4, value.doubleValue) + } + if (value.arrayValue !== undefined) { + return Proto.messageField(5, encodeArrayValue(value.arrayValue)) + } + if (value.kvlistValue !== undefined) { + return Proto.messageField(6, encodeKeyValueList(value.kvlistValue)) + } + if (value.bytesValue !== undefined) { + return Proto.lengthDelimitedField(7, value.bytesValue) + } + return new Uint8Array(0) +} + +/** + * Encodes an ArrayValue message. + * + * message ArrayValue { + * repeated AnyValue values = 1; + * } + */ +export const encodeArrayValue = (value: { values: ReadonlyArray }): Uint8Array => + Proto.repeatedField(1, value.values, encodeAnyValue) + +/** + * Encodes a KeyValueList message. + * + * message KeyValueList { + * repeated KeyValue values = 1; + * } + */ +export const encodeKeyValueList = (value: { values: ReadonlyArray }): Uint8Array => + Proto.repeatedField(1, value.values, encodeKeyValue) + +/** + * Encodes a KeyValue message. + * + * message KeyValue { + * string key = 1; + * AnyValue value = 2; + * } + */ +export const encodeKeyValue = (kv: KeyValue): Uint8Array => + Proto.concat( + Proto.stringField(1, kv.key), + Proto.messageField(2, encodeAnyValue(kv.value)) + ) + +/** + * Encodes an InstrumentationScope message. + * + * message InstrumentationScope { + * string name = 1; + * string version = 2; + * repeated KeyValue attributes = 3; + * uint32 dropped_attributes_count = 4; + * } + */ +export const encodeInstrumentationScope = (scope: { + readonly name: string + readonly version?: string + readonly attributes?: ReadonlyArray + readonly droppedAttributesCount?: number +}): Uint8Array => + Proto.concat( + Proto.stringField(1, scope.name), + Proto.optionalStringField(2, scope.version), + scope.attributes ? Proto.repeatedField(3, scope.attributes, encodeKeyValue) : new Uint8Array(0), + scope.droppedAttributesCount ? Proto.varintField(4, scope.droppedAttributesCount) : new Uint8Array(0) + ) + +// Resource types (opentelemetry.proto.resource.v1) + +/** + * Encodes a Resource message. + * + * message Resource { + * repeated KeyValue attributes = 1; + * uint32 dropped_attributes_count = 2; + * } + */ +export const encodeResource = (resource: Resource): Uint8Array => + Proto.concat( + Proto.repeatedField(1, resource.attributes, encodeKeyValue), + resource.droppedAttributesCount > 0 + ? Proto.varintField(2, resource.droppedAttributesCount) + : new Uint8Array(0) + ) + +// Trace types (opentelemetry.proto.trace.v1) + +/** + * Status code enum + */ +export const StatusCode = { + Unset: 0, + Ok: 1, + Error: 2 +} as const + +/** + * SpanKind enum + */ +export const SpanKind = { + Unspecified: 0, + Internal: 1, + Server: 2, + Client: 3, + Producer: 4, + Consumer: 5 +} as const + +/** + * Encodes a Status message. + * + * message Status { + * string message = 2; + * StatusCode code = 3; + * } + */ +export const encodeStatus = (status: { + readonly code: number + readonly message?: string +}): Uint8Array => + Proto.concat( + Proto.optionalStringField(2, status.message), + Proto.varintField(3, status.code) + ) + +/** + * Encodes an Event message. + * + * message Event { + * fixed64 time_unix_nano = 1; + * string name = 2; + * repeated KeyValue attributes = 3; + * uint32 dropped_attributes_count = 4; + * } + */ +export const encodeEvent = (event: { + readonly timeUnixNano: string + readonly name: string + readonly attributes: ReadonlyArray + readonly droppedAttributesCount: number +}): Uint8Array => + Proto.concat( + Proto.fixed64Field(1, BigInt(event.timeUnixNano)), + Proto.stringField(2, event.name), + Proto.repeatedField(3, event.attributes, encodeKeyValue), + event.droppedAttributesCount > 0 + ? Proto.varintField(4, event.droppedAttributesCount) + : new Uint8Array(0) + ) + +/** + * Encodes a Link message. + * + * message Link { + * bytes trace_id = 1; + * bytes span_id = 2; + * string trace_state = 3; + * repeated KeyValue attributes = 4; + * uint32 dropped_attributes_count = 5; + * fixed32 flags = 6; + * } + */ +export const encodeLink = (link: { + readonly traceId: string + readonly spanId: string + readonly traceState?: string + readonly attributes: ReadonlyArray + readonly droppedAttributesCount: number + readonly flags?: number +}): Uint8Array => + Proto.concat( + Proto.bytesFieldFromHex(1, link.traceId), + Proto.bytesFieldFromHex(2, link.spanId), + Proto.optionalStringField(3, link.traceState), + Proto.repeatedField(4, link.attributes, encodeKeyValue), + link.droppedAttributesCount > 0 + ? Proto.varintField(5, link.droppedAttributesCount) + : new Uint8Array(0), + link.flags !== undefined ? Proto.fixed32Field(6, link.flags) : new Uint8Array(0) + ) + +/** + * Encodes a Span message. + * + * message Span { + * bytes trace_id = 1; + * bytes span_id = 2; + * string trace_state = 3; + * bytes parent_span_id = 4; + * string name = 5; + * SpanKind kind = 6; + * fixed64 start_time_unix_nano = 7; + * fixed64 end_time_unix_nano = 8; + * repeated KeyValue attributes = 9; + * uint32 dropped_attributes_count = 10; + * repeated Event events = 11; + * uint32 dropped_events_count = 12; + * repeated Link links = 13; + * uint32 dropped_links_count = 14; + * Status status = 15; + * fixed32 flags = 16; + * } + */ +export const encodeSpan = (span: { + readonly traceId: string + readonly spanId: string + readonly traceState?: string + readonly parentSpanId?: string + readonly name: string + readonly kind: number + readonly startTimeUnixNano: string + readonly endTimeUnixNano: string + readonly attributes: ReadonlyArray + readonly droppedAttributesCount: number + readonly events: ReadonlyArray<{ + readonly timeUnixNano: string + readonly name: string + readonly attributes: ReadonlyArray + readonly droppedAttributesCount: number + }> + readonly droppedEventsCount: number + readonly links: ReadonlyArray<{ + readonly traceId: string + readonly spanId: string + readonly traceState?: string + readonly attributes: ReadonlyArray + readonly droppedAttributesCount: number + readonly flags?: number + }> + readonly droppedLinksCount: number + readonly status: { + readonly code: number + readonly message?: string + } + readonly flags?: number +}): Uint8Array => + Proto.concat( + Proto.bytesFieldFromHex(1, span.traceId), + Proto.bytesFieldFromHex(2, span.spanId), + Proto.optionalStringField(3, span.traceState), + span.parentSpanId !== undefined + ? Proto.bytesFieldFromHex(4, span.parentSpanId) + : new Uint8Array(0), + Proto.stringField(5, span.name), + Proto.varintField(6, span.kind), + Proto.fixed64Field(7, BigInt(span.startTimeUnixNano)), + Proto.fixed64Field(8, BigInt(span.endTimeUnixNano)), + Proto.repeatedField(9, span.attributes, encodeKeyValue), + span.droppedAttributesCount > 0 + ? Proto.varintField(10, span.droppedAttributesCount) + : new Uint8Array(0), + Proto.repeatedField(11, span.events, encodeEvent), + span.droppedEventsCount > 0 + ? Proto.varintField(12, span.droppedEventsCount) + : new Uint8Array(0), + Proto.repeatedField(13, span.links, encodeLink), + span.droppedLinksCount > 0 + ? Proto.varintField(14, span.droppedLinksCount) + : new Uint8Array(0), + Proto.messageField(15, encodeStatus(span.status)), + span.flags !== undefined ? Proto.fixed32Field(16, span.flags) : new Uint8Array(0) + ) + +/** + * Encodes a ScopeSpans message. + * + * message ScopeSpans { + * InstrumentationScope scope = 1; + * repeated Span spans = 2; + * string schema_url = 3; + * } + */ +export const encodeScopeSpans = (scopeSpans: { + readonly scope: { readonly name: string; readonly version?: string } + readonly spans: ReadonlyArray[0]> + readonly schemaUrl?: string +}): Uint8Array => + Proto.concat( + Proto.messageField(1, encodeInstrumentationScope(scopeSpans.scope)), + Proto.repeatedField(2, scopeSpans.spans, encodeSpan), + Proto.optionalStringField(3, scopeSpans.schemaUrl) + ) + +/** + * Encodes a ResourceSpans message. + * + * message ResourceSpans { + * Resource resource = 1; + * repeated ScopeSpans scope_spans = 2; + * string schema_url = 3; + * } + */ +export const encodeResourceSpans = (resourceSpans: { + readonly resource: Resource + readonly scopeSpans: ReadonlyArray[0]> + readonly schemaUrl?: string +}): Uint8Array => + Proto.concat( + Proto.messageField(1, encodeResource(resourceSpans.resource)), + Proto.repeatedField(2, resourceSpans.scopeSpans, encodeScopeSpans), + Proto.optionalStringField(3, resourceSpans.schemaUrl) + ) + +/** + * Encodes a TracesData message (top-level export request). + * + * message TracesData { + * repeated ResourceSpans resource_spans = 1; + * } + */ +export const encodeTracesData = (tracesData: { + readonly resourceSpans: ReadonlyArray[0]> +}): Uint8Array => Proto.repeatedField(1, tracesData.resourceSpans, encodeResourceSpans) + +// Metrics types (opentelemetry.proto.metrics.v1) + +/** + * AggregationTemporality enum + */ +export const AggregationTemporality = { + Unspecified: 0, + Delta: 1, + Cumulative: 2 +} as const + +/** + * Encodes a NumberDataPoint message. + * + * message NumberDataPoint { + * repeated KeyValue attributes = 7; + * fixed64 start_time_unix_nano = 2; + * fixed64 time_unix_nano = 3; + * oneof value { + * double as_double = 4; + * sfixed64 as_int = 6; + * } + * repeated Exemplar exemplars = 5; + * uint32 flags = 8; + * } + */ +export const encodeNumberDataPoint = (point: { + readonly attributes: ReadonlyArray + readonly startTimeUnixNano: string + readonly timeUnixNano: string + readonly asDouble?: number | undefined + readonly asInt?: string | number | bigint | undefined + readonly flags?: number | undefined +}): Uint8Array => + Proto.concat( + Proto.fixed64Field(2, BigInt(point.startTimeUnixNano)), + Proto.fixed64Field(3, BigInt(point.timeUnixNano)), + point.asDouble !== undefined + ? Proto.doubleField(4, point.asDouble) + : new Uint8Array(0), + point.asInt !== undefined + ? Proto.fixed64Field(6, BigInt(point.asInt)) + : new Uint8Array(0), + Proto.repeatedField(7, point.attributes, encodeKeyValue), + point.flags !== undefined ? Proto.varintField(8, point.flags) : new Uint8Array(0) + ) + +/** + * Encodes a HistogramDataPoint message. + * + * message HistogramDataPoint { + * repeated KeyValue attributes = 9; + * fixed64 start_time_unix_nano = 2; + * fixed64 time_unix_nano = 3; + * fixed64 count = 4; + * optional double sum = 5; + * repeated fixed64 bucket_counts = 6; + * repeated double explicit_bounds = 7; + * optional double min = 11; + * optional double max = 12; + * uint32 flags = 10; + * } + */ +export const encodeHistogramDataPoint = (point: { + readonly attributes: ReadonlyArray + readonly startTimeUnixNano: string + readonly timeUnixNano: string + readonly count: string | number | bigint + readonly sum?: number | undefined + readonly bucketCounts: ReadonlyArray + readonly explicitBounds: ReadonlyArray + readonly min?: number | undefined + readonly max?: number | undefined + readonly flags?: number | undefined +}): Uint8Array => { + // Pack bucket counts as repeated fixed64 + const bucketCountsEncoded = Proto.concat( + ...point.bucketCounts.map((count) => Proto.fixed64Field(6, BigInt(count))) + ) + // Pack explicit bounds as repeated double + const explicitBoundsEncoded = Proto.concat( + ...point.explicitBounds.map((bound) => Proto.doubleField(7, bound)) + ) + return Proto.concat( + Proto.fixed64Field(2, BigInt(point.startTimeUnixNano)), + Proto.fixed64Field(3, BigInt(point.timeUnixNano)), + Proto.fixed64Field(4, BigInt(point.count)), + point.sum !== undefined ? Proto.doubleField(5, point.sum) : new Uint8Array(0), + bucketCountsEncoded, + explicitBoundsEncoded, + Proto.repeatedField(9, point.attributes, encodeKeyValue), + point.flags !== undefined ? Proto.varintField(10, point.flags) : new Uint8Array(0), + point.min !== undefined ? Proto.doubleField(11, point.min) : new Uint8Array(0), + point.max !== undefined ? Proto.doubleField(12, point.max) : new Uint8Array(0) + ) +} + +/** + * Encodes a Gauge message. + * + * message Gauge { + * repeated NumberDataPoint data_points = 1; + * } + */ +export const encodeGauge = (gauge: { + readonly dataPoints: ReadonlyArray[0]> +}): Uint8Array => Proto.repeatedField(1, gauge.dataPoints, encodeNumberDataPoint) + +/** + * Encodes a Sum message. + * + * message Sum { + * repeated NumberDataPoint data_points = 1; + * AggregationTemporality aggregation_temporality = 2; + * bool is_monotonic = 3; + * } + */ +export const encodeSum = (sum: { + readonly dataPoints: ReadonlyArray[0]> + readonly aggregationTemporality: number + readonly isMonotonic: boolean +}): Uint8Array => + Proto.concat( + Proto.repeatedField(1, sum.dataPoints, encodeNumberDataPoint), + Proto.varintField(2, sum.aggregationTemporality), + Proto.boolField(3, sum.isMonotonic) + ) + +/** + * Encodes a Histogram message. + * + * message Histogram { + * repeated HistogramDataPoint data_points = 1; + * AggregationTemporality aggregation_temporality = 2; + * } + */ +export const encodeHistogram = (histogram: { + readonly dataPoints: ReadonlyArray[0]> + readonly aggregationTemporality: number +}): Uint8Array => + Proto.concat( + Proto.repeatedField(1, histogram.dataPoints, encodeHistogramDataPoint), + Proto.varintField(2, histogram.aggregationTemporality) + ) + +/** + * Encodes a Metric message. + * + * message Metric { + * string name = 1; + * string description = 2; + * string unit = 3; + * oneof data { + * Gauge gauge = 5; + * Sum sum = 7; + * Histogram histogram = 9; + * ExponentialHistogram exponential_histogram = 10; + * Summary summary = 11; + * } + * } + */ +export const encodeMetric = (metric: { + readonly name: string + readonly description?: string | undefined + readonly unit?: string | undefined + readonly gauge?: Parameters[0] | undefined + readonly sum?: Parameters[0] | undefined + readonly histogram?: Parameters[0] | undefined +}): Uint8Array => + Proto.concat( + Proto.stringField(1, metric.name), + Proto.optionalStringField(2, metric.description), + Proto.optionalStringField(3, metric.unit), + metric.gauge !== undefined + ? Proto.messageField(5, encodeGauge(metric.gauge)) + : new Uint8Array(0), + metric.sum !== undefined + ? Proto.messageField(7, encodeSum(metric.sum)) + : new Uint8Array(0), + metric.histogram !== undefined + ? Proto.messageField(9, encodeHistogram(metric.histogram)) + : new Uint8Array(0) + ) + +/** + * Encodes a ScopeMetrics message. + * + * message ScopeMetrics { + * InstrumentationScope scope = 1; + * repeated Metric metrics = 2; + * string schema_url = 3; + * } + */ +export const encodeScopeMetrics = (scopeMetrics: { + readonly scope: { readonly name: string; readonly version?: string } + readonly metrics: ReadonlyArray[0]> + readonly schemaUrl?: string +}): Uint8Array => + Proto.concat( + Proto.messageField(1, encodeInstrumentationScope(scopeMetrics.scope)), + Proto.repeatedField(2, scopeMetrics.metrics, encodeMetric), + Proto.optionalStringField(3, scopeMetrics.schemaUrl) + ) + +/** + * Encodes a ResourceMetrics message. + * + * message ResourceMetrics { + * Resource resource = 1; + * repeated ScopeMetrics scope_metrics = 2; + * string schema_url = 3; + * } + */ +export const encodeResourceMetrics = (resourceMetrics: { + readonly resource: Resource + readonly scopeMetrics: ReadonlyArray[0]> + readonly schemaUrl?: string +}): Uint8Array => + Proto.concat( + Proto.messageField(1, encodeResource(resourceMetrics.resource)), + Proto.repeatedField(2, resourceMetrics.scopeMetrics, encodeScopeMetrics), + Proto.optionalStringField(3, resourceMetrics.schemaUrl) + ) + +/** + * Encodes a MetricsData message (top-level export request). + * + * message MetricsData { + * repeated ResourceMetrics resource_metrics = 1; + * } + */ +export const encodeMetricsData = (metricsData: { + readonly resourceMetrics: ReadonlyArray[0]> +}): Uint8Array => Proto.repeatedField(1, metricsData.resourceMetrics, encodeResourceMetrics) + +// Logs types (opentelemetry.proto.logs.v1) + +/** + * SeverityNumber enum + */ +export const SeverityNumber = { + Unspecified: 0, + Trace: 1, + Trace2: 2, + Trace3: 3, + Trace4: 4, + Debug: 5, + Debug2: 6, + Debug3: 7, + Debug4: 8, + Info: 9, + Info2: 10, + Info3: 11, + Info4: 12, + Warn: 13, + Warn2: 14, + Warn3: 15, + Warn4: 16, + Error: 17, + Error2: 18, + Error3: 19, + Error4: 20, + Fatal: 21, + Fatal2: 22, + Fatal3: 23, + Fatal4: 24 +} as const + +/** + * Encodes a LogRecord message. + * + * message LogRecord { + * fixed64 time_unix_nano = 1; + * fixed64 observed_time_unix_nano = 11; + * SeverityNumber severity_number = 2; + * string severity_text = 3; + * AnyValue body = 5; + * repeated KeyValue attributes = 6; + * uint32 dropped_attributes_count = 7; + * fixed32 flags = 8; + * bytes trace_id = 9; + * bytes span_id = 10; + * } + */ +export const encodeLogRecord = (record: { + readonly timeUnixNano: string + readonly observedTimeUnixNano?: string | undefined + readonly severityNumber?: number | undefined + readonly severityText?: string | undefined + readonly body?: AnyValue | undefined + readonly attributes: ReadonlyArray + readonly droppedAttributesCount?: number | undefined + readonly flags?: number | undefined + readonly traceId?: string | undefined + readonly spanId?: string | undefined +}): Uint8Array => + Proto.concat( + Proto.fixed64Field(1, BigInt(record.timeUnixNano)), + record.severityNumber !== undefined + ? Proto.varintField(2, record.severityNumber) + : new Uint8Array(0), + Proto.optionalStringField(3, record.severityText), + record.body !== undefined + ? Proto.messageField(5, encodeAnyValue(record.body)) + : new Uint8Array(0), + Proto.repeatedField(6, record.attributes, encodeKeyValue), + record.droppedAttributesCount !== undefined && record.droppedAttributesCount > 0 + ? Proto.varintField(7, record.droppedAttributesCount) + : new Uint8Array(0), + record.flags !== undefined ? Proto.fixed32Field(8, record.flags) : new Uint8Array(0), + record.traceId !== undefined && record.traceId !== "" + ? Proto.bytesFieldFromHex(9, record.traceId) + : new Uint8Array(0), + record.spanId !== undefined && record.spanId !== "" + ? Proto.bytesFieldFromHex(10, record.spanId) + : new Uint8Array(0), + record.observedTimeUnixNano !== undefined + ? Proto.fixed64Field(11, BigInt(record.observedTimeUnixNano)) + : new Uint8Array(0) + ) + +/** + * Encodes a ScopeLogs message. + * + * message ScopeLogs { + * InstrumentationScope scope = 1; + * repeated LogRecord log_records = 2; + * string schema_url = 3; + * } + */ +export const encodeScopeLogs = (scopeLogs: { + readonly scope: { readonly name: string; readonly version?: string } + readonly logRecords: ReadonlyArray[0]> + readonly schemaUrl?: string +}): Uint8Array => + Proto.concat( + Proto.messageField(1, encodeInstrumentationScope(scopeLogs.scope)), + Proto.repeatedField(2, scopeLogs.logRecords, encodeLogRecord), + Proto.optionalStringField(3, scopeLogs.schemaUrl) + ) + +/** + * Encodes a ResourceLogs message. + * + * message ResourceLogs { + * Resource resource = 1; + * repeated ScopeLogs scope_logs = 2; + * string schema_url = 3; + * } + */ +export const encodeResourceLogs = (resourceLogs: { + readonly resource: Resource + readonly scopeLogs: ReadonlyArray[0]> + readonly schemaUrl?: string +}): Uint8Array => + Proto.concat( + Proto.messageField(1, encodeResource(resourceLogs.resource)), + Proto.repeatedField(2, resourceLogs.scopeLogs, encodeScopeLogs), + Proto.optionalStringField(3, resourceLogs.schemaUrl) + ) + +/** + * Encodes a LogsData message (top-level export request). + * + * message LogsData { + * repeated ResourceLogs resource_logs = 1; + * } + */ +export const encodeLogsData = (logsData: { + readonly resourceLogs: ReadonlyArray[0]> +}): Uint8Array => Proto.repeatedField(1, logsData.resourceLogs, encodeResourceLogs) diff --git a/repos/effect/packages/opentelemetry/src/internal/protobuf.ts b/repos/effect/packages/opentelemetry/src/internal/protobuf.ts new file mode 100644 index 0000000..aea5c98 --- /dev/null +++ b/repos/effect/packages/opentelemetry/src/internal/protobuf.ts @@ -0,0 +1,219 @@ +/** + * Low-level protobuf wire format encoding utilities. + * + * Protobuf wire types: + * - 0: Varint (int32, int64, uint32, uint64, sint32, sint64, bool, enum) + * - 1: 64-bit (fixed64, sfixed64, double) + * - 2: Length-delimited (string, bytes, embedded messages, packed repeated fields) + * - 5: 32-bit (fixed32, sfixed32, float) + * + * @internal + */ + +const enum WireType { + Varint = 0, + Fixed64 = 1, + LengthDelimited = 2, + Fixed32 = 5 +} + +/** + * Encodes a field tag (field number + wire type) + */ +const encodeTag = (fieldNumber: number, wireType: WireType): number => (fieldNumber << 3) | wireType + +/** + * Encodes a varint (variable-length integer) + */ +export const encodeVarint = (value: number | bigint): Uint8Array => { + const bytes: Array = [] + let n = typeof value === "bigint" ? value : BigInt(value) + while (n > 0x7fn) { + bytes.push(Number(n & 0x7fn) | 0x80) + n >>= 7n + } + bytes.push(Number(n)) + return new Uint8Array(bytes) +} + +/** + * Encodes a signed varint using ZigZag encoding + */ +export const encodeSint = (value: number | bigint): Uint8Array => { + const n = typeof value === "bigint" ? value : BigInt(value) + const zigzag = (n << 1n) ^ (n >> 63n) + return encodeVarint(zigzag) +} + +/** + * Encodes a 64-bit fixed value (little-endian) + */ +export const encodeFixed64 = (value: bigint): Uint8Array => { + const bytes = new Uint8Array(8) + const view = new DataView(bytes.buffer) + view.setBigUint64(0, value, true) + return bytes +} + +/** + * Encodes a 32-bit fixed value (little-endian) + */ +export const encodeFixed32 = (value: number): Uint8Array => { + const bytes = new Uint8Array(4) + const view = new DataView(bytes.buffer) + view.setUint32(0, value, true) + return bytes +} + +/** + * Encodes a double (64-bit float, little-endian) + */ +export const encodeDouble = (value: number): Uint8Array => { + const bytes = new Uint8Array(8) + const view = new DataView(bytes.buffer) + view.setFloat64(0, value, true) + return bytes +} + +/** + * Encodes a string to UTF-8 bytes + */ +export const encodeString = (value: string): Uint8Array => new TextEncoder().encode(value) + +/** + * Encodes bytes as a hex string to Uint8Array + */ +export const encodeHexBytes = (hex: string): Uint8Array => { + const bytes = new Uint8Array(hex.length / 2) + for (let i = 0; i < hex.length; i += 2) { + bytes[i / 2] = parseInt(hex.slice(i, i + 2), 16) + } + return bytes +} + +/** + * Concatenates multiple Uint8Arrays + */ +export const concat = (...arrays: Array): Uint8Array => { + const totalLength = arrays.reduce((sum, arr) => sum + arr.length, 0) + const result = new Uint8Array(totalLength) + let offset = 0 + for (const arr of arrays) { + result.set(arr, offset) + offset += arr.length + } + return result +} + +// Field encoders + +/** + * Encodes a varint field + */ +export const varintField = (fieldNumber: number, value: number | bigint): Uint8Array => + concat( + encodeVarint(encodeTag(fieldNumber, WireType.Varint)), + encodeVarint(value) + ) + +/** + * Encodes a sint field (ZigZag encoded) + */ +export const sintField = (fieldNumber: number, value: number | bigint): Uint8Array => + concat( + encodeVarint(encodeTag(fieldNumber, WireType.Varint)), + encodeSint(value) + ) + +/** + * Encodes a bool field + */ +export const boolField = (fieldNumber: number, value: boolean): Uint8Array => varintField(fieldNumber, value ? 1 : 0) + +/** + * Encodes a fixed64 field + */ +export const fixed64Field = (fieldNumber: number, value: bigint): Uint8Array => + concat( + encodeVarint(encodeTag(fieldNumber, WireType.Fixed64)), + encodeFixed64(value) + ) + +/** + * Encodes a fixed32 field + */ +export const fixed32Field = (fieldNumber: number, value: number): Uint8Array => + concat( + encodeVarint(encodeTag(fieldNumber, WireType.Fixed32)), + encodeFixed32(value) + ) + +/** + * Encodes a double field + */ +export const doubleField = (fieldNumber: number, value: number): Uint8Array => + concat( + encodeVarint(encodeTag(fieldNumber, WireType.Fixed64)), + encodeDouble(value) + ) + +/** + * Encodes a length-delimited field (bytes, string, embedded message) + */ +export const lengthDelimitedField = (fieldNumber: number, value: Uint8Array): Uint8Array => + concat( + encodeVarint(encodeTag(fieldNumber, WireType.LengthDelimited)), + encodeVarint(value.length), + value + ) + +/** + * Encodes a string field + */ +export const stringField = (fieldNumber: number, value: string): Uint8Array => + lengthDelimitedField(fieldNumber, encodeString(value)) + +/** + * Encodes a bytes field from hex string + */ +export const bytesFieldFromHex = (fieldNumber: number, hex: string): Uint8Array => + lengthDelimitedField(fieldNumber, encodeHexBytes(hex)) + +/** + * Encodes an embedded message field + */ +export const messageField = (fieldNumber: number, message: Uint8Array): Uint8Array => + lengthDelimitedField(fieldNumber, message) + +/** + * Encodes repeated fields + */ +export const repeatedField = ( + fieldNumber: number, + values: ReadonlyArray, + encode: (value: T) => Uint8Array +): Uint8Array => concat(...values.map((v) => messageField(fieldNumber, encode(v)))) + +/** + * Encodes repeated varint fields (not packed) + */ +export const repeatedVarintField = ( + fieldNumber: number, + values: ReadonlyArray +): Uint8Array => concat(...values.map((v) => varintField(fieldNumber, v))) + +/** + * Helper to conditionally encode an optional field + */ +export const optionalField = ( + value: T | undefined, + encode: (v: T) => Uint8Array +): Uint8Array => value !== undefined ? encode(value) : new Uint8Array(0) + +/** + * Helper to conditionally encode a string field if non-empty + */ +export const optionalStringField = ( + fieldNumber: number, + value: string | undefined +): Uint8Array => value !== undefined && value !== "" ? stringField(fieldNumber, value) : new Uint8Array(0) diff --git a/repos/effect/packages/opentelemetry/src/internal/tracer.ts b/repos/effect/packages/opentelemetry/src/internal/tracer.ts new file mode 100644 index 0000000..7a6bfae --- /dev/null +++ b/repos/effect/packages/opentelemetry/src/internal/tracer.ts @@ -0,0 +1,448 @@ +import * as OtelApi from "@opentelemetry/api" +import * as OtelSemConv from "@opentelemetry/semantic-conventions" +import * as Cause from "effect/Cause" +import type * as Clock from "effect/Clock" +import * as Context from "effect/Context" +import * as Effect from "effect/Effect" +import * as Exit from "effect/Exit" +import { constTrue, dual } from "effect/Function" +import * as Layer from "effect/Layer" +import * as Option from "effect/Option" +import * as EffectTracer from "effect/Tracer" +import { Resource } from "../Resource.js" +import type { OtelTraceFlags, OtelTracer, OtelTracerProvider, OtelTraceState } from "../Tracer.js" +import { nanosToHrTime, recordToAttributes, unknownToAttributeValue } from "./utils.js" + +const OtelSpanTypeId = Symbol.for("@effect/opentelemetry/Tracer/OtelSpan") + +const kindMap = { + "internal": OtelApi.SpanKind.INTERNAL, + "client": OtelApi.SpanKind.CLIENT, + "server": OtelApi.SpanKind.SERVER, + "producer": OtelApi.SpanKind.PRODUCER, + "consumer": OtelApi.SpanKind.CONSUMER +} + +const getOtelParent = (tracer: OtelApi.TraceAPI, otelContext: OtelApi.Context, context: Context.Context) => { + const active = tracer.getSpan(otelContext) + const otelParent = active ? active.spanContext() : undefined + return otelParent + ? Option.some( + EffectTracer.externalSpan({ + spanId: otelParent.spanId, + traceId: otelParent.traceId, + sampled: (otelParent.traceFlags & 1) === 1, + context + }) + ) + : Option.none() +} + +/** @internal */ +export class OtelSpan implements EffectTracer.Span { + readonly [OtelSpanTypeId]: typeof OtelSpanTypeId + readonly _tag = "Span" + + readonly span: OtelApi.Span + readonly spanId: string + readonly traceId: string + readonly attributes = new Map() + readonly sampled: boolean + readonly parent: Option.Option + status: EffectTracer.SpanStatus + + constructor( + contextApi: OtelApi.ContextAPI, + traceApi: OtelApi.TraceAPI, + tracer: OtelApi.Tracer, + readonly name: string, + effectParent: Option.Option, + readonly context: Context.Context, + readonly links: Array, + startTime: bigint, + readonly kind: EffectTracer.SpanKind, + options?: EffectTracer.SpanOptions + ) { + this[OtelSpanTypeId] = OtelSpanTypeId + const active = contextApi.active() + this.parent = effectParent._tag === "Some" + ? effectParent + : (options?.root !== true) + ? getOtelParent(traceApi, active, context) + : Option.none() + this.span = tracer.startSpan( + name, + { + startTime: nanosToHrTime(startTime), + links: links.length > 0 + ? links.map((link) => ({ + context: makeSpanContext(link.span), + attributes: recordToAttributes(link.attributes) + })) + : undefined as any, + kind: kindMap[this.kind] + }, + this.parent._tag === "Some" + ? populateContext(active, this.parent.value, context) + : OtelApi.trace.deleteSpan(active) + ) + const spanContext = this.span.spanContext() + this.spanId = spanContext.spanId + this.traceId = spanContext.traceId + this.status = { + _tag: "Started", + startTime + } + this.sampled = (spanContext.traceFlags & OtelApi.TraceFlags.SAMPLED) === OtelApi.TraceFlags.SAMPLED + } + + attribute(key: string, value: unknown) { + this.span.setAttribute(key, unknownToAttributeValue(value)) + this.attributes.set(key, value) + } + + addLinks(links: ReadonlyArray): void { + // eslint-disable-next-line no-restricted-syntax + this.links.push(...links) + this.span.addLinks(links.map((link) => ({ + context: makeSpanContext(link.span), + attributes: recordToAttributes(link.attributes) + }))) + } + + end(endTime: bigint, exit: Exit.Exit) { + const hrTime = nanosToHrTime(endTime) + this.status = { + _tag: "Ended", + endTime, + exit, + startTime: this.status.startTime + } + + if (exit._tag === "Success") { + this.span.setStatus({ code: OtelApi.SpanStatusCode.OK }) + } else { + if (Cause.isInterruptedOnly(exit.cause)) { + this.span.setStatus({ + code: OtelApi.SpanStatusCode.OK, + message: Cause.pretty(exit.cause) + }) + this.span.setAttribute("span.label", "⚠︎ Interrupted") + this.span.setAttribute("status.interrupted", true) + } else { + const firstError = Cause.prettyErrors(exit.cause)[0] + if (firstError) { + firstError.stack = Cause.pretty(exit.cause, { renderErrorCause: true }) + this.span.recordException(firstError, hrTime) + this.span.setStatus({ + code: OtelApi.SpanStatusCode.ERROR, + message: firstError.message + }) + } else { + // empty cause means no error + this.span.setStatus({ code: OtelApi.SpanStatusCode.OK }) + } + } + } + this.span.end(hrTime) + } + + event(name: string, startTime: bigint, attributes?: Record) { + this.span.addEvent( + name, + attributes ? recordToAttributes(attributes) : undefined, + nanosToHrTime(startTime) + ) + } +} + +/** @internal */ +export const TracerProvider = Context.GenericTag( + "@effect/opentelemetry/Tracer/OtelTracerProvider" +) + +/** @internal */ +export const Tracer = Context.GenericTag("@effect/opentelemetry/Tracer/OtelTracer") + +/** @internal */ +export const make = Effect.map(Tracer, (tracer) => + EffectTracer.make({ + span(name, parent, context, links, startTime, kind, options) { + return new OtelSpan( + OtelApi.context, + OtelApi.trace, + tracer, + name, + parent, + context, + links.slice(), + startTime, + kind, + options + ) + }, + context(execution, fiber) { + const currentSpan = fiber.currentSpan + + if (currentSpan === undefined) { + return execution() + } + + return OtelApi.context.with( + populateContext(OtelApi.context.active(), currentSpan), + execution + ) + } + })) + +/** @internal */ +export const traceFlagsTag = Context.GenericTag( + "@effect/opentelemetry/Tracer/OtelTraceFlags" +) + +/** @internal */ +export const traceStateTag = Context.GenericTag( + "@effect/opentelemetry/Tracer/OtelTraceState" +) + +/** @internal */ +export const makeExternalSpan = (options: { + readonly traceId: string + readonly spanId: string + readonly traceFlags?: number | undefined + readonly traceState?: string | OtelApi.TraceState | undefined +}): EffectTracer.ExternalSpan => { + let context = Context.empty() + + if (options.traceFlags !== undefined) { + context = Context.add(context, traceFlagsTag, options.traceFlags) + } + + if (typeof options.traceState === "string") { + context = Option.match(createTraceState(options.traceState), { + onNone: () => context, + onSome: (traceState) => Context.add(context, traceStateTag, traceState) + }) + } else if (options.traceState) { + context = Context.add(context, traceStateTag, options.traceState) + } + + return { + _tag: "ExternalSpan", + traceId: options.traceId, + spanId: options.spanId, + sampled: options.traceFlags !== undefined + ? (options.traceFlags & OtelApi.TraceFlags.SAMPLED) === OtelApi.TraceFlags.SAMPLED + : true, + context + } +} + +const makeOtelSpan = (span: EffectTracer.Span, clock: Clock.Clock): OtelApi.Span => { + const spanContext: OtelApi.SpanContext = { + traceId: span.traceId, + spanId: span.spanId, + traceFlags: span.sampled ? OtelApi.TraceFlags.SAMPLED : OtelApi.TraceFlags.NONE, + isRemote: false + } + + let exit = Exit.void + + const self: OtelApi.Span = { + spanContext: () => spanContext, + setAttribute(key, value) { + span.attribute(key, value) + return self + }, + setAttributes(attributes) { + for (const [key, value] of Object.entries(attributes)) { + span.attribute(key, value) + } + return self + }, + addEvent(name) { + let attributes: OtelApi.Attributes | undefined = undefined + let startTime: OtelApi.TimeInput | undefined = undefined + if (arguments.length === 3) { + attributes = arguments[1] + startTime = arguments[2] + } else if (arguments.length === 2) { + const arg1 = arguments[1] + if (isTimeInput(arg1)) { + startTime = arg1 + } else { + attributes = arg1 + } + } + span.event(name, convertOtelTimeInput(startTime, clock), attributes) + return self + }, + addLink(link) { + span.addLinks([{ + _tag: "SpanLink", + span: makeExternalSpan(link.context), + attributes: link.attributes ?? {} + }]) + return self + }, + addLinks(links) { + span.addLinks(links.map((link) => ({ + _tag: "SpanLink", + span: makeExternalSpan(link.context), + attributes: link.attributes ?? {} + }))) + return self + }, + setStatus(status) { + exit = OtelApi.SpanStatusCode.ERROR + ? Exit.die(status.message ?? "Unknown error") + : Exit.void + return self + }, + updateName: () => self, + end(endTime) { + const time = convertOtelTimeInput(endTime, clock) + span.end(time, exit) + return self + }, + isRecording: constTrue, + recordException(exception, timeInput) { + const time = convertOtelTimeInput(timeInput, clock) + const cause = Cause.fail(exception) + const error = Cause.prettyErrors(cause)[0] + span.event(error.message, time, { + "exception.type": error.name, + "exception.message": error.message, + "exception.stacktrace": error.stack ?? "" + }) + } + } + return self +} + +const bigint1e6 = BigInt(1_000_000) +const bigint1e9 = BigInt(1_000_000_000) + +/** Distinguishes TimeInput (number | Date | [number, number]) from Attributes (plain object) */ +const isTimeInput = (u: unknown): u is OtelApi.TimeInput => + typeof u === "number" || + u instanceof Date || + (Array.isArray(u) && u.length === 2 && typeof u[0] === "number" && typeof u[1] === "number") + +const convertOtelTimeInput = (input: OtelApi.TimeInput | undefined, clock: Clock.Clock): bigint => { + if (input === undefined) { + return clock.unsafeCurrentTimeNanos() + } else if (typeof input === "number") { + return BigInt(Math.round(input * 1_000_000)) + } else if (input instanceof Date) { + return BigInt(input.getTime()) * bigint1e6 + } + const [seconds, nanos] = input + return BigInt(seconds) * bigint1e9 + BigInt(nanos) +} + +/** @internal */ +export const currentOtelSpan: Effect.Effect = Effect.clockWith((clock) => + Effect.map(Effect.currentSpan, (span): OtelApi.Span => { + if (OtelSpanTypeId in span) { + return (span as OtelSpan).span + } + return makeOtelSpan(span, clock) + }) +) + +/** @internal */ +export const layerGlobalProvider = Layer.sync( + TracerProvider, + () => OtelApi.trace.getTracerProvider() +) + +/** @internal */ +export const layerTracer = Layer.effect( + Tracer, + Effect.flatMap( + Effect.zip(Resource, TracerProvider), + ([resource, provider]) => + Effect.sync(() => + provider.getTracer( + resource.attributes[OtelSemConv.ATTR_SERVICE_NAME] as string, + resource.attributes[OtelSemConv.ATTR_SERVICE_VERSION] as string + ) + ) + ) +) + +/** @internal */ +export const layerGlobalTracer = layerTracer.pipe( + Layer.provide(layerGlobalProvider) +) + +/** @internal */ +export const layerGlobal = Layer.unwrapEffect(Effect.map(make, Layer.setTracer)).pipe( + Layer.provideMerge(layerGlobalTracer) +) + +/** @internal */ +export const layerWithoutOtelTracer = Layer.unwrapEffect(Effect.map(make, Layer.setTracer)) + +/** @internal */ +export const layer = layerWithoutOtelTracer.pipe( + Layer.provideMerge(layerTracer) +) + +// ------------------------------------------------------------------------------------- +// utils +// ------------------------------------------------------------------------------------- + +const createTraceState = Option.liftThrowable(OtelApi.createTraceState) + +const populateContext = ( + otelContext: OtelApi.Context, + span: EffectTracer.AnySpan, + context?: Context.Context +): OtelApi.Context => + span instanceof OtelSpan ? + OtelApi.trace.setSpan(otelContext, span.span) : + OtelApi.trace.setSpanContext(otelContext, makeSpanContext(span, context)) + +const makeSpanContext = (span: EffectTracer.AnySpan, context?: Context.Context): OtelApi.SpanContext => ({ + spanId: span.spanId, + traceId: span.traceId, + isRemote: span._tag === "ExternalSpan", + traceFlags: Option.getOrElse( + context ? + extractTraceTag(span, context, traceFlagsTag) : + Context.getOption(span.context, traceFlagsTag), + () => OtelApi.TraceFlags.SAMPLED + ), + traceState: Option.getOrUndefined( + context ? + extractTraceTag(span, context, traceStateTag) : + Context.getOption(span.context, traceStateTag) + ) as OtelApi.TraceState +}) + +const extractTraceTag = ( + parent: EffectTracer.AnySpan, + context: Context.Context, + tag: Context.Tag +) => + Option.orElse( + Context.getOption(context, tag), + () => Context.getOption(parent.context, tag) + ) + +/** @internal */ +export const withSpanContext = dual< + ( + spanContext: OtelApi.SpanContext + ) => (effect: Effect.Effect) => Effect.Effect>, + ( + effect: Effect.Effect, + spanContext: OtelApi.SpanContext + ) => Effect.Effect> +>(2, ( + effect: Effect.Effect, + spanContext: OtelApi.SpanContext +): Effect.Effect> => + Effect.withParentSpan(effect, makeExternalSpan(spanContext))) diff --git a/repos/effect/packages/opentelemetry/src/internal/utils.ts b/repos/effect/packages/opentelemetry/src/internal/utils.ts new file mode 100644 index 0000000..59b42e7 --- /dev/null +++ b/repos/effect/packages/opentelemetry/src/internal/utils.ts @@ -0,0 +1,31 @@ +import type * as OtelApi from "@opentelemetry/api" +import type { NonEmptyReadonlyArray } from "effect/Array" +import * as Inspectable from "effect/Inspectable" + +const bigint1e9 = 1_000_000_000n + +/** @internal */ +export const nanosToHrTime = (timestamp: bigint): OtelApi.HrTime => { + return [Number(timestamp / bigint1e9), Number(timestamp % bigint1e9)] +} + +/** @internal */ +export const recordToAttributes = (value: Record): OtelApi.Attributes => + Object.entries(value).reduce((acc, [key, value]) => { + acc[key] = unknownToAttributeValue(value) + return acc + }, {} as OtelApi.Attributes) + +/** @internal */ +export const unknownToAttributeValue = (value: unknown): OtelApi.AttributeValue => { + if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") { + return value + } else if (typeof value === "bigint") { + return value.toString() + } + return Inspectable.toStringUnknown(value) +} + +/** @internal */ +export const isNonEmpty =
(a: A | ReadonlyArray | undefined): a is A | NonEmptyReadonlyArray => + a !== undefined && !(Array.isArray(a) && a.length === 0) diff --git a/repos/effect/packages/opentelemetry/test/Logger.test.ts b/repos/effect/packages/opentelemetry/test/Logger.test.ts new file mode 100644 index 0000000..5d87a39 --- /dev/null +++ b/repos/effect/packages/opentelemetry/test/Logger.test.ts @@ -0,0 +1,50 @@ +import * as NodeSdk from "@effect/opentelemetry/NodeSdk" +import * as it from "@effect/vitest" +import { InMemoryLogRecordExporter, SimpleLogRecordProcessor } from "@opentelemetry/sdk-logs" +import * as Effect from "effect/Effect" +import { describe, expect } from "vitest" + +describe("Logger", () => { + describe("provided", () => { + const exporter = new InMemoryLogRecordExporter() + + const TracingLive = NodeSdk.layer(Effect.sync(() => ({ + resource: { + serviceName: "test" + }, + logRecordProcessor: [new SimpleLogRecordProcessor(exporter)] + }))) + + it.effect("emits log records", () => + Effect.provide( + Effect.gen(function*() { + yield* Effect.log("test").pipe( + Effect.repeatN(9) + ) + + expect(exporter.getFinishedLogRecords()).toHaveLength(10) + }), + TracingLive + )) + }) + + describe("not provided", () => { + const exporter = new InMemoryLogRecordExporter() + + const TracingLive = NodeSdk.layer(Effect.sync(() => ({ + resource: { + serviceName: "test" + } + }))) + + it.effect("withSpan", () => + Effect.provide( + Effect.gen(function*() { + yield* Effect.log("test") + + expect(exporter.getFinishedLogRecords()).toHaveLength(0) + }), + TracingLive + )) + }) +}) diff --git a/repos/effect/packages/opentelemetry/test/Metrics.test.ts b/repos/effect/packages/opentelemetry/test/Metrics.test.ts new file mode 100644 index 0000000..ec3bda8 --- /dev/null +++ b/repos/effect/packages/opentelemetry/test/Metrics.test.ts @@ -0,0 +1,295 @@ +import { assert, describe, it } from "@effect/vitest" +import { ValueType } from "@opentelemetry/api" +import { resourceFromAttributes } from "@opentelemetry/resources" +import * as Effect from "effect/Effect" +import * as Metric from "effect/Metric" +import * as internal from "../src/internal/metrics.js" + +const findMetric = (metrics: any, name: string) => + metrics.resourceMetrics.scopeMetrics[0].metrics.find((_: any) => _.descriptor.name === name) + +describe("Metrics", () => { + it.effect("gauge", () => + Effect.gen(function*() { + const resource = resourceFromAttributes({ + name: "test", + version: "1.0.0" + }) + const producer = new internal.MetricProducerImpl(resource) + const gauge = Metric.gauge("rps") + + yield* Metric.set(gauge, 10).pipe(Effect.tagMetrics("key", "value"), Effect.tagMetrics("unit", "requests")) + yield* Metric.set(gauge, 10).pipe(Effect.tagMetrics("key", "value")) + yield* Metric.set(gauge, 20).pipe(Effect.tagMetrics("key", "value")) + + const results = yield* Effect.promise(() => producer.collect()) + const object = JSON.parse(JSON.stringify(results)) + assert.deepEqual(object.resourceMetrics.resource._rawAttributes, [ + ["name", "test"], + ["version", "1.0.0"] + ]) + assert.equal(object.resourceMetrics.scopeMetrics.length, 1) + const metric = findMetric(object, "rps") + assert.deepEqual(metric, { + "dataPointType": 2, + "descriptor": { + "advice": {}, + "name": "rps", + "description": "", + "unit": "requests", + "type": "OBSERVABLE_GAUGE", + "valueType": ValueType.DOUBLE + }, + "aggregationTemporality": 1, + "dataPoints": [ + { + "startTime": metric.dataPoints[0].startTime, + "endTime": metric.dataPoints[0].endTime, + "attributes": { + "unit": "requests", + "key": "value" + }, + "value": 10 + }, + { + "startTime": metric.dataPoints[0].startTime, + "endTime": metric.dataPoints[0].endTime, + "attributes": { + "key": "value" + }, + "value": 20 + } + ] + }) + })) + + it.effect("gauge bigint", () => + Effect.gen(function*() { + const producer = new internal.MetricProducerImpl( + resourceFromAttributes({ + name: "test", + version: "1.0.0" + }) + ) + const gauge = Metric.gauge("rps-bigint", { bigint: true }) + + yield* Metric.set(gauge, 10n).pipe(Effect.tagMetrics("key", "value"), Effect.tagMetrics("unit", "requests")) + yield* Metric.set(gauge, 10n).pipe(Effect.tagMetrics("key", "value")) + yield* Metric.set(gauge, 20n).pipe(Effect.tagMetrics("key", "value")) + + const results = yield* Effect.promise(() => producer.collect()) + const object = JSON.parse(JSON.stringify(results)) + assert.deepEqual(object.resourceMetrics.resource._rawAttributes, [ + ["name", "test"], + ["version", "1.0.0"] + ]) + assert.equal(object.resourceMetrics.scopeMetrics.length, 1) + const metric = findMetric(object, "rps-bigint") + assert.deepEqual(metric, { + "dataPointType": 2, + "descriptor": { + "advice": {}, + "name": "rps-bigint", + "description": "", + "unit": "requests", + "type": "OBSERVABLE_GAUGE", + "valueType": ValueType.INT + }, + "aggregationTemporality": 1, + "dataPoints": [ + { + "startTime": metric.dataPoints[0].startTime, + "endTime": metric.dataPoints[0].endTime, + "attributes": { + "unit": "requests", + "key": "value" + }, + "value": 10 + }, + { + "startTime": metric.dataPoints[0].startTime, + "endTime": metric.dataPoints[0].endTime, + "attributes": { + "key": "value" + }, + "value": 20 + } + ] + }) + })) + + it.effect("counter", () => + Effect.gen(function*() { + const producer = new internal.MetricProducerImpl( + resourceFromAttributes({ + name: "test", + version: "1.0.0" + }) + ) + const counter = Metric.counter("counter", { description: "Example" }) + + yield* Metric.increment(counter).pipe(Effect.tagMetrics("key", "value"), Effect.tagMetrics("unit", "requests")) + yield* Metric.increment(counter).pipe(Effect.tagMetrics("key", "value")) + yield* Metric.increment(counter).pipe(Effect.tagMetrics("key", "value")) + + const results = yield* Effect.promise(() => producer.collect()) + const object = JSON.parse(JSON.stringify(results)) + assert.deepEqual(object.resourceMetrics.resource._rawAttributes, [ + ["name", "test"], + ["version", "1.0.0"] + ]) + assert.equal(object.resourceMetrics.scopeMetrics.length, 1) + const metric = findMetric(object, "counter") + assert.deepEqual(metric, { + "dataPointType": 3, + "descriptor": { + "advice": {}, + "name": "counter", + "description": "Example", + "unit": "requests", + "type": "UP_DOWN_COUNTER", + "valueType": ValueType.DOUBLE + }, + "isMonotonic": false, + "aggregationTemporality": 1, + "dataPoints": [ + { + "startTime": metric.dataPoints[0].startTime, + "endTime": metric.dataPoints[0].endTime, + "attributes": { + "unit": "requests", + "key": "value" + }, + "value": 1 + }, + { + "startTime": metric.dataPoints[0].startTime, + "endTime": metric.dataPoints[0].endTime, + "attributes": { + "key": "value" + }, + "value": 2 + } + ] + }) + })) + + it.effect("counter-inc", () => + Effect.gen(function*() { + const producer = new internal.MetricProducerImpl( + resourceFromAttributes({ + name: "test", + version: "1.0.0" + }) + ) + const counter = Metric.counter("counter-inc", { + description: "Example", + incremental: true + }) + + yield* Metric.increment(counter).pipe(Effect.tagMetrics("key", "value"), Effect.tagMetrics("unit", "requests")) + yield* Metric.increment(counter).pipe(Effect.tagMetrics("key", "value")) + yield* Metric.increment(counter).pipe(Effect.tagMetrics("key", "value")) + + const results = yield* Effect.promise(() => producer.collect()) + const object = JSON.parse(JSON.stringify(results)) + assert.deepEqual(object.resourceMetrics.resource._rawAttributes, [ + ["name", "test"], + ["version", "1.0.0"] + ]) + assert.equal(object.resourceMetrics.scopeMetrics.length, 1) + const metric = findMetric(object, "counter-inc") + assert.deepEqual(metric, { + "dataPointType": 3, + "descriptor": { + "advice": {}, + "name": "counter-inc", + "description": "Example", + "unit": "requests", + "type": "COUNTER", + "valueType": ValueType.DOUBLE + }, + "isMonotonic": true, + "aggregationTemporality": 1, + "dataPoints": [ + { + "startTime": metric.dataPoints[0].startTime, + "endTime": metric.dataPoints[0].endTime, + "attributes": { + "unit": "requests", + "key": "value" + }, + "value": 1 + }, + { + "startTime": metric.dataPoints[0].startTime, + "endTime": metric.dataPoints[0].endTime, + "attributes": { + "key": "value" + }, + "value": 2 + } + ] + }) + })) + + it.effect("counter-bigint", () => + Effect.gen(function*() { + const producer = new internal.MetricProducerImpl( + resourceFromAttributes({ + name: "test", + version: "1.0.0" + }) + ) + const counter = Metric.counter("counter-bigint", { + description: "Example", + incremental: true, + bigint: true + }) + + yield* Metric.increment(counter).pipe(Effect.tagMetrics("key", "value"), Effect.tagMetrics("unit", "requests")) + yield* Metric.increment(counter).pipe(Effect.tagMetrics("key", "value")) + yield* Metric.increment(counter).pipe(Effect.tagMetrics("key", "value")) + + const results = yield* Effect.promise(() => producer.collect()) + const object = JSON.parse(JSON.stringify(results)) + assert.deepEqual(object.resourceMetrics.resource._rawAttributes, [ + ["name", "test"], + ["version", "1.0.0"] + ]) + assert.equal(object.resourceMetrics.scopeMetrics.length, 1) + const metric = findMetric(object, "counter-bigint") + assert.deepEqual(metric, { + "dataPointType": 3, + "descriptor": { + "advice": {}, + "name": "counter-bigint", + "description": "Example", + "unit": "requests", + "type": "COUNTER", + "valueType": ValueType.INT + }, + "isMonotonic": true, + "aggregationTemporality": 1, + "dataPoints": [ + { + "startTime": metric.dataPoints[0].startTime, + "endTime": metric.dataPoints[0].endTime, + "attributes": { + "unit": "requests", + "key": "value" + }, + "value": 1 + }, + { + "startTime": metric.dataPoints[0].startTime, + "endTime": metric.dataPoints[0].endTime, + "attributes": { + "key": "value" + }, + "value": 2 + } + ] + }) + })) +}) diff --git a/repos/effect/packages/opentelemetry/test/OtlpSerialization.test.ts b/repos/effect/packages/opentelemetry/test/OtlpSerialization.test.ts new file mode 100644 index 0000000..9296491 --- /dev/null +++ b/repos/effect/packages/opentelemetry/test/OtlpSerialization.test.ts @@ -0,0 +1,22 @@ +import { assert, describe, expect, it } from "@effect/vitest" +import * as Effect from "effect/Effect" +import * as OtlpSerialization from "../src/OtlpSerialization.js" + +describe("OtlpSerialization override behavior", () => { + it.effect("json roundtrip", () => + Effect.gen(function*() { + const serialization = yield* OtlpSerialization.OtlpSerialization + const body = serialization.traces({ test: "data" }) + assert(body._tag === "Uint8Array") + expect(body.contentType).toBe("application/json") + const result = JSON.parse(new TextDecoder().decode(body.body)) + expect(result).toEqual({ test: "data" }) + }).pipe(Effect.provide(OtlpSerialization.layerJson))) + + it.effect("protobuf layer provides protobuf HttpBody", () => + Effect.gen(function*() { + const serialization = yield* OtlpSerialization.OtlpSerialization + const body = serialization.traces({ resourceSpans: [] }) + expect(body.contentType).toBe("application/x-protobuf") + }).pipe(Effect.provide(OtlpSerialization.layerProtobuf))) +}) diff --git a/repos/effect/packages/opentelemetry/test/Protobuf.test.ts b/repos/effect/packages/opentelemetry/test/Protobuf.test.ts new file mode 100644 index 0000000..154bc7e --- /dev/null +++ b/repos/effect/packages/opentelemetry/test/Protobuf.test.ts @@ -0,0 +1,383 @@ +import { describe, expect, it } from "@effect/vitest" +import * as Effect from "effect/Effect" +import * as OtlpProtobuf from "../src/internal/otlpProtobuf.js" +import * as Proto from "../src/internal/protobuf.js" +import * as OtlpSerialization from "../src/OtlpSerialization.js" + +describe("Protobuf encoding", () => { + describe("primitives", () => { + it("encodeVarint small values", () => { + // 0 encodes to [0] + expect(Proto.encodeVarint(0)).toEqual(new Uint8Array([0])) + // 1 encodes to [1] + expect(Proto.encodeVarint(1)).toEqual(new Uint8Array([1])) + // 127 encodes to [127] (single byte max) + expect(Proto.encodeVarint(127)).toEqual(new Uint8Array([127])) + // 128 encodes to [128, 1] (requires continuation bit) + expect(Proto.encodeVarint(128)).toEqual(new Uint8Array([128, 1])) + // 300 encodes to [172, 2] + expect(Proto.encodeVarint(300)).toEqual(new Uint8Array([172, 2])) + }) + + it("encodeVarint large values", () => { + // 16384 = 0x4000 encodes to [128, 128, 1] + expect(Proto.encodeVarint(16384)).toEqual(new Uint8Array([128, 128, 1])) + }) + + it("encodeFixed64", () => { + const result = Proto.encodeFixed64(BigInt("1234567890123456789")) + expect(result.length).toBe(8) + // Little-endian encoding + const view = new DataView(result.buffer) + expect(view.getBigUint64(0, true)).toBe(BigInt("1234567890123456789")) + }) + + it("encodeFixed32", () => { + const result = Proto.encodeFixed32(0x12345678) + expect(result.length).toBe(4) + const view = new DataView(result.buffer) + expect(view.getUint32(0, true)).toBe(0x12345678) + }) + + it("encodeDouble", () => { + const result = Proto.encodeDouble(3.14159) + expect(result.length).toBe(8) + const view = new DataView(result.buffer) + expect(view.getFloat64(0, true)).toBeCloseTo(3.14159) + }) + + it("encodeString", () => { + const result = Proto.encodeString("hello") + expect(result).toEqual(new Uint8Array([104, 101, 108, 108, 111])) + }) + + it("encodeHexBytes", () => { + const result = Proto.encodeHexBytes("deadbeef") + expect(result).toEqual(new Uint8Array([0xde, 0xad, 0xbe, 0xef])) + }) + + it("concat", () => { + const a = new Uint8Array([1, 2, 3]) + const b = new Uint8Array([4, 5]) + const c = new Uint8Array([6]) + const result = Proto.concat(a, b, c) + expect(result).toEqual(new Uint8Array([1, 2, 3, 4, 5, 6])) + }) + }) + + describe("field encoders", () => { + it("varintField", () => { + // field 1, value 150 + // tag = (1 << 3) | 0 = 8 + // value = 150 = [150, 1] (as varint) + const result = Proto.varintField(1, 150) + expect(result).toEqual(new Uint8Array([8, 150, 1])) + }) + + it("boolField", () => { + // field 2, true + // tag = (2 << 3) | 0 = 16 + const trueResult = Proto.boolField(2, true) + expect(trueResult).toEqual(new Uint8Array([16, 1])) + + const falseResult = Proto.boolField(2, false) + expect(falseResult).toEqual(new Uint8Array([16, 0])) + }) + + it("stringField", () => { + // field 1, value "hi" + // tag = (1 << 3) | 2 = 10 (length-delimited) + // length = 2 + // data = [104, 105] + const result = Proto.stringField(1, "hi") + expect(result).toEqual(new Uint8Array([10, 2, 104, 105])) + }) + + it("fixed64Field", () => { + // field 1, wire type 1 (64-bit) + // tag = (1 << 3) | 1 = 9 + const result = Proto.fixed64Field(1, BigInt(1)) + expect(result[0]).toBe(9) + expect(result.length).toBe(9) // 1 tag + 8 data + }) + + it("messageField", () => { + // field 2, embedded message [1, 2, 3] + // tag = (2 << 3) | 2 = 18 (length-delimited) + // length = 3 + const result = Proto.messageField(2, new Uint8Array([1, 2, 3])) + expect(result).toEqual(new Uint8Array([18, 3, 1, 2, 3])) + }) + }) + + describe("OTLP types", () => { + it("encodeAnyValue - string", () => { + const result = OtlpProtobuf.encodeAnyValue({ stringValue: "test" }) + // field 1 (string_value), length-delimited + // tag = (1 << 3) | 2 = 10 + expect(result[0]).toBe(10) + expect(result[1]).toBe(4) // length + }) + + it("encodeAnyValue - bool", () => { + const result = OtlpProtobuf.encodeAnyValue({ boolValue: true }) + // field 2 (bool_value), varint + // tag = (2 << 3) | 0 = 16 + expect(result).toEqual(new Uint8Array([16, 1])) + }) + + it("encodeAnyValue - int", () => { + const result = OtlpProtobuf.encodeAnyValue({ intValue: 42 }) + // field 3 (int_value), varint + // tag = (3 << 3) | 0 = 24 + expect(result[0]).toBe(24) + }) + + it("encodeAnyValue - double", () => { + const result = OtlpProtobuf.encodeAnyValue({ doubleValue: 3.14 }) + // field 4 (double_value), 64-bit + // tag = (4 << 3) | 1 = 33 + expect(result[0]).toBe(33) + expect(result.length).toBe(9) // 1 tag + 8 data + }) + + it("encodeKeyValue", () => { + const result = OtlpProtobuf.encodeKeyValue({ + key: "test", + value: { stringValue: "value" } + }) + // Should contain field 1 (key) and field 2 (value) + expect(result.length).toBeGreaterThan(0) + // First byte should be tag for field 1 string + expect(result[0]).toBe(10) // (1 << 3) | 2 = 10 + }) + + it("encodeResource", () => { + const result = OtlpProtobuf.encodeResource({ + attributes: [ + { key: "service.name", value: { stringValue: "test-service" } } + ], + droppedAttributesCount: 0 + }) + // Should encode attributes as repeated field 1 + expect(result.length).toBeGreaterThan(0) + }) + + it("encodeStatus", () => { + const okStatus = OtlpProtobuf.encodeStatus({ code: OtlpProtobuf.StatusCode.Ok }) + expect(okStatus.length).toBeGreaterThan(0) + + const errorStatus = OtlpProtobuf.encodeStatus({ + code: OtlpProtobuf.StatusCode.Error, + message: "test error" + }) + expect(errorStatus.length).toBeGreaterThan(okStatus.length) + }) + + it("encodeSpan", () => { + const result = OtlpProtobuf.encodeSpan({ + traceId: "0123456789abcdef0123456789abcdef", + spanId: "0123456789abcdef", + name: "test-span", + kind: OtlpProtobuf.SpanKind.Internal, + startTimeUnixNano: "1000000000", + endTimeUnixNano: "2000000000", + attributes: [ + { key: "test.attr", value: { stringValue: "value" } } + ], + droppedAttributesCount: 0, + events: [], + droppedEventsCount: 0, + links: [], + droppedLinksCount: 0, + status: { code: OtlpProtobuf.StatusCode.Ok } + }) + expect(result.length).toBeGreaterThan(0) + // Should be a valid protobuf message + // Verify it starts with field 1 (trace_id) bytes + expect(result[0]).toBe(10) // (1 << 3) | 2 = 10 (length-delimited) + }) + + it("encodeTracesData", () => { + const result = OtlpProtobuf.encodeTracesData({ + resourceSpans: [{ + resource: { + attributes: [ + { key: "service.name", value: { stringValue: "test" } } + ], + droppedAttributesCount: 0 + }, + scopeSpans: [{ + scope: { name: "test-scope" }, + spans: [{ + traceId: "0123456789abcdef0123456789abcdef", + spanId: "0123456789abcdef", + name: "test-span", + kind: OtlpProtobuf.SpanKind.Server, + startTimeUnixNano: "1000000000000000000", + endTimeUnixNano: "2000000000000000000", + attributes: [], + droppedAttributesCount: 0, + events: [], + droppedEventsCount: 0, + links: [], + droppedLinksCount: 0, + status: { code: OtlpProtobuf.StatusCode.Ok } + }] + }] + }] + }) + expect(result.length).toBeGreaterThan(0) + }) + + it("encodeMetricsData", () => { + const result = OtlpProtobuf.encodeMetricsData({ + resourceMetrics: [{ + resource: { + attributes: [ + { key: "service.name", value: { stringValue: "test" } } + ], + droppedAttributesCount: 0 + }, + scopeMetrics: [{ + scope: { name: "test-scope" }, + metrics: [{ + name: "test.counter", + description: "A test counter", + unit: "1", + sum: { + dataPoints: [{ + attributes: [], + startTimeUnixNano: "1000000000000000000", + timeUnixNano: "2000000000000000000", + asInt: "42" + }], + aggregationTemporality: OtlpProtobuf.AggregationTemporality.Cumulative, + isMonotonic: true + } + }] + }] + }] + }) + expect(result.length).toBeGreaterThan(0) + }) + + it("encodeLogsData", () => { + const result = OtlpProtobuf.encodeLogsData({ + resourceLogs: [{ + resource: { + attributes: [ + { key: "service.name", value: { stringValue: "test" } } + ], + droppedAttributesCount: 0 + }, + scopeLogs: [{ + scope: { name: "test-scope" }, + logRecords: [{ + timeUnixNano: "1000000000000000000", + severityNumber: OtlpProtobuf.SeverityNumber.Info, + severityText: "INFO", + body: { stringValue: "Test log message" }, + attributes: [ + { key: "log.key", value: { stringValue: "log.value" } } + ], + droppedAttributesCount: 0 + }] + }] + }] + }) + expect(result.length).toBeGreaterThan(0) + }) + }) + + describe("edge cases", () => { + it("handles empty arrays", () => { + const result = OtlpProtobuf.encodeTracesData({ + resourceSpans: [] + }) + // Empty repeated field should produce empty output + expect(result.length).toBe(0) + }) + + it("handles optional fields", () => { + const result = OtlpProtobuf.encodeSpan({ + traceId: "0123456789abcdef0123456789abcdef", + spanId: "0123456789abcdef", + name: "test", + kind: OtlpProtobuf.SpanKind.Internal, + startTimeUnixNano: "0", + endTimeUnixNano: "0", + attributes: [], + droppedAttributesCount: 0, + events: [], + droppedEventsCount: 0, + links: [], + droppedLinksCount: 0, + status: { code: OtlpProtobuf.StatusCode.Unset } + }) + expect(result.length).toBeGreaterThan(0) + }) + + it("handles special characters in strings", () => { + const result = OtlpProtobuf.encodeAnyValue({ + stringValue: "hello\nworld\t\r\n" + }) + expect(result.length).toBeGreaterThan(0) + }) + + it("handles unicode strings", () => { + const result = OtlpProtobuf.encodeAnyValue({ + stringValue: "hello" + }) + expect(result.length).toBeGreaterThan(0) + }) + + it("handles large numbers", () => { + const result = Proto.encodeVarint(BigInt("9223372036854775807")) + expect(result.length).toBe(9) // Max varint size for 64-bit + }) + }) + + describe("OtlpSerialization", () => { + const sampleTracesData = { + resourceSpans: [{ + resource: { + attributes: [{ key: "service.name", value: { stringValue: "test" } }], + droppedAttributesCount: 0 + }, + scopeSpans: [{ + scope: { name: "test-scope" }, + spans: [{ + traceId: "0123456789abcdef0123456789abcdef", + spanId: "0123456789abcdef", + name: "test-span", + kind: 1, + startTimeUnixNano: "1000000000000000000", + endTimeUnixNano: "2000000000000000000", + attributes: [], + droppedAttributesCount: 0, + events: [], + droppedEventsCount: 0, + links: [], + droppedLinksCount: 0, + status: { code: 1 } + }] + }] + }] + } + + it.effect("json serializer returns HttpBody with json content type", () => + Effect.gen(function*() { + const serialization = yield* OtlpSerialization.OtlpSerialization + const body = serialization.traces(sampleTracesData) + expect(body.contentType).toBe("application/json") + }).pipe(Effect.provide(OtlpSerialization.layerJson))) + + it.effect("protobuf serializer returns HttpBody with protobuf content type", () => + Effect.gen(function*() { + const serialization = yield* OtlpSerialization.OtlpSerialization + const body = serialization.traces(sampleTracesData) + expect(body.contentType).toBe("application/x-protobuf") + }).pipe(Effect.provide(OtlpSerialization.layerProtobuf))) + }) +}) diff --git a/repos/effect/packages/opentelemetry/test/Tracer.test.ts b/repos/effect/packages/opentelemetry/test/Tracer.test.ts new file mode 100644 index 0000000..c5bb26e --- /dev/null +++ b/repos/effect/packages/opentelemetry/test/Tracer.test.ts @@ -0,0 +1,295 @@ +import * as NodeSdk from "@effect/opentelemetry/NodeSdk" +import * as OtlpSerialization from "@effect/opentelemetry/OtlpSerialization" +import * as OtlpTracer from "@effect/opentelemetry/OtlpTracer" +import * as Tracer from "@effect/opentelemetry/Tracer" +import { HttpClient } from "@effect/platform" +import { assert, describe, expect, it } from "@effect/vitest" +import * as OtelApi from "@opentelemetry/api" +import { AsyncHooksContextManager } from "@opentelemetry/context-async-hooks" +import { InMemorySpanExporter, SimpleSpanProcessor } from "@opentelemetry/sdk-trace-base" +import * as Console from "effect/Console" +import * as Effect from "effect/Effect" +import * as FiberRef from "effect/FiberRef" +import * as Layer from "effect/Layer" +import * as Runtime from "effect/Runtime" +import { OtelSpan } from "../src/internal/tracer.js" + +class Exporter extends Effect.Service()("Exporter", { + effect: Effect.sync(() => ({ exporter: new InMemorySpanExporter() })) +}) {} + +const TracingLive = Layer.unwrapEffect(Effect.gen(function*() { + const { exporter } = yield* Exporter + + return NodeSdk.layer(Effect.sync(() => ({ + resource: { + serviceName: "test" + }, + spanProcessor: [new SimpleSpanProcessor(exporter)] + }))) +})).pipe(Layer.provideMerge(Exporter.Default)) + +// needed to test context propagation +const contextManager = new AsyncHooksContextManager() +OtelApi.context.setGlobalContextManager(contextManager) + +describe("Tracer", () => { + describe("provided", () => { + it.effect("withSpan", () => + Effect.provide( + Effect.withSpan("ok")( + Effect.gen(function*() { + const span = yield* Effect.currentSpan + expect(span).instanceOf(OtelSpan) + }) + ), + TracingLive + )) + + it.effect("withSpan links", () => + Effect.gen(function*() { + const linkedSpan = yield* Effect.makeSpanScoped("B") + const span = yield* Effect.currentSpan.pipe( + Effect.withSpan("A"), + Effect.linkSpans(linkedSpan) + ) + assert(span instanceof OtelSpan) + expect(span.links.length).toBe(1) + }).pipe( + Effect.scoped, + Effect.provide(TracingLive) + )) + + it.effect("supervisor sets context", () => + Effect.provide( + Effect.withSpan("ok")( + Effect.sync(() => { + expect(OtelApi.trace.getSpan(OtelApi.context.active())).toBeDefined() + }) + ), + TracingLive + )) + + it.effect("supervisor sets context generator", () => + Effect.gen(function*() { + yield* Effect.yieldNow() + expect(OtelApi.trace.getSpan(OtelApi.context.active())).toBeDefined() + }).pipe( + Effect.withSpan("ok"), + Effect.provide(TracingLive) + )) + + it.effect("currentOtelSpan", () => + Effect.provide( + Effect.withSpan("ok")( + Effect.gen(function*() { + const span = yield* Effect.currentSpan + const otelSpan = yield* Tracer.currentOtelSpan + expect((span as OtelSpan).span).toBe(otelSpan) + }) + ), + TracingLive + )) + + it.scoped("withSpanContext", () => + Effect.gen(function*() { + const effect = Effect.gen(function*() { + const span = yield* Effect.currentParentSpan + assert(span._tag === "Span") + const parent = yield* span.parent + return parent + }).pipe(Effect.withSpan("child")) + + const runtime = yield* Effect.runtime() + + yield* Effect.promise(async () => { + await OtelApi.trace.getTracer("test").startActiveSpan("otel-span", { + root: true, + attributes: { "root": "yes" } + }, async (span) => { + try { + const parent = await Runtime.runPromise( + runtime, + Tracer.withSpanContext( + effect, + span.spanContext() + ) + ) + const { spanId, traceId } = span.spanContext() + expect(parent).toMatchObject({ + spanId, + traceId + }) + } finally { + span.end() + } + }) + }) + }).pipe(Effect.provide(TracingLive))) + }) + + describe("not provided", () => { + it.effect("withSpan", () => + Effect.withSpan("ok")( + Effect.gen(function*() { + const span = yield* Effect.currentSpan + expect(span).not.instanceOf(OtelSpan) + }) + )) + }) + + describe("OTLP tracer", () => { + const MockHttpClient = Layer.succeed( + HttpClient.HttpClient, + HttpClient.make(() => Effect.die("mock http client")) + ) + const OtlpTracingLive = OtlpTracer.layer({ + url: "http://localhost:4318/v1/traces", + resource: { + serviceName: "test-otlp" + } + }).pipe(Layer.provide(MockHttpClient), Layer.provide(OtlpSerialization.layerJson)) + + it.effect("currentOtelSpan works with OTLP tracer", () => + Effect.provide( + Effect.withSpan("ok")( + Effect.gen(function*() { + const span = yield* Effect.currentSpan + const otelSpan = yield* Tracer.currentOtelSpan + const spanContext = otelSpan.spanContext() + expect(spanContext.traceId).toBe(span.traceId) + expect(spanContext.spanId).toBe(span.spanId) + expect(spanContext.traceFlags).toBe(OtelApi.TraceFlags.SAMPLED) + expect(spanContext.isRemote).toBe(false) + expect(otelSpan.isRecording()).toBe(true) + + // it should proxy attribute changes + otelSpan.setAttribute("key", "value") + expect(span.attributes.get("key")).toEqual("value") + }) + ), + OtlpTracingLive + )) + + it.effect("addEvent with attributes (2-arg overload) does not throw", () => + Effect.provide( + Effect.withSpan("root")( + Effect.gen(function*() { + const otelSpan = yield* Tracer.currentOtelSpan + expect(() => otelSpan.addEvent("test-event", { foo: "bar", count: 42 })).not.toThrow() + }) + ), + OtlpTracingLive + )) + + it.effect("addEvent with HrTime tuple (2-arg overload) does not throw", () => + Effect.provide( + Effect.withSpan("root")( + Effect.gen(function*() { + const otelSpan = yield* Tracer.currentOtelSpan + expect(() => otelSpan.addEvent("test-event", [1, 2])).not.toThrow() + }) + ), + OtlpTracingLive + )) + + it.effect("addEvent with number timestamp (2-arg overload) does not throw", () => + Effect.provide( + Effect.withSpan("root")( + Effect.gen(function*() { + const otelSpan = yield* Tracer.currentOtelSpan + expect(() => otelSpan.addEvent("test-event", Date.now())).not.toThrow() + }) + ), + OtlpTracingLive + )) + + it.effect("addEvent with Date timestamp (2-arg overload) does not throw", () => + Effect.provide( + Effect.withSpan("root")( + Effect.gen(function*() { + const otelSpan = yield* Tracer.currentOtelSpan + expect(() => otelSpan.addEvent("test-event", new Date())).not.toThrow() + }) + ), + OtlpTracingLive + )) + + it.effect("addEvent with name only (1-arg) does not throw", () => + Effect.provide( + Effect.withSpan("root")( + Effect.gen(function*() { + const otelSpan = yield* Tracer.currentOtelSpan + expect(() => otelSpan.addEvent("test-event")).not.toThrow() + }) + ), + OtlpTracingLive + )) + + it.effect("addEvent with attributes and timestamp (3-arg overload) does not throw", () => + Effect.provide( + Effect.withSpan("root")( + Effect.gen(function*() { + const otelSpan = yield* Tracer.currentOtelSpan + expect(() => otelSpan.addEvent("test-event", { foo: "bar" }, Date.now())).not.toThrow() + }) + ), + OtlpTracingLive + )) + }) + + describe("Log Attributes", () => { + it.effect("propagates attributes with Effect.fnUntraced", () => + Effect.gen(function*() { + const f = Effect.fnUntraced(function*() { + yield* Effect.logWarning("FooBar") + return yield* Effect.fail("Oops") + }) + + const p = f().pipe(Effect.withSpan("p")) + + yield* Effect.ignore(p) + + const { exporter } = yield* Exporter + + assert.isNotEmpty(exporter.getFinishedSpans()[0].events.filter((_) => _.name === "FooBar")) + assert.isNotEmpty(exporter.getFinishedSpans()[0].events.filter((_) => _.name === "exception")) + }).pipe(Effect.provide(TracingLive))) + + it.effect("propagates attributes with Effect.fn(name)", () => + Effect.gen(function*() { + const f = Effect.fn("f")(function*() { + yield* Effect.logWarning("FooBar") + return yield* Effect.fail("Oops") + }) + + const p = f().pipe(Effect.withSpan("p")) + + yield* Effect.ignore(p) + + const { exporter } = yield* Exporter + + assert.isNotEmpty(exporter.getFinishedSpans()[0].events.filter((_) => _.name === "FooBar")) + assert.isNotEmpty(exporter.getFinishedSpans()[0].events.filter((_) => _.name === "exception")) + }).pipe(Effect.provide(TracingLive))) + + it.effect("propagates attributes with Effect.fn", () => + Effect.gen(function*() { + const f = Effect.fn(function*() { + yield* Effect.logWarning("FooBar") + return yield* Effect.fail("Oops") + }) + + const p = f().pipe(Effect.withSpan("p")) + + yield* Effect.ignore(p) + + const { exporter } = yield* Exporter + + yield* Console.log(Array.from(yield* FiberRef.get(FiberRef.currentLoggers))) + + assert.isNotEmpty(exporter.getFinishedSpans()[0].events.filter((_) => _.name === "FooBar")) + assert.isNotEmpty(exporter.getFinishedSpans()[0].events.filter((_) => _.name === "exception")) + }).pipe(Effect.provide(TracingLive))) + }) +}) diff --git a/repos/effect/packages/opentelemetry/tsconfig.build.json b/repos/effect/packages/opentelemetry/tsconfig.build.json new file mode 100644 index 0000000..472aa65 --- /dev/null +++ b/repos/effect/packages/opentelemetry/tsconfig.build.json @@ -0,0 +1,13 @@ +{ + "extends": "./tsconfig.src.json", + "references": [ + { "path": "../effect/tsconfig.build.json" }, + { "path": "../platform/tsconfig.build.json" } + ], + "compilerOptions": { + "tsBuildInfoFile": ".tsbuildinfo/build.tsbuildinfo", + "outDir": "build/esm", + "declarationDir": "build/dts", + "stripInternal": true + } +} diff --git a/repos/effect/packages/opentelemetry/tsconfig.examples.json b/repos/effect/packages/opentelemetry/tsconfig.examples.json new file mode 100644 index 0000000..023edd4 --- /dev/null +++ b/repos/effect/packages/opentelemetry/tsconfig.examples.json @@ -0,0 +1,14 @@ +{ + "extends": "../../tsconfig.base.json", + "include": ["examples"], + "references": [ + { "path": "tsconfig.build.json" }, + { "path": "../effect/tsconfig.build.json" }, + { "path": "../platform-node/tsconfig.build.json" } + ], + "compilerOptions": { + "tsBuildInfoFile": ".tsbuildinfo/examples.tsbuildinfo", + "rootDir": "examples", + "noEmit": true + } +} diff --git a/repos/effect/packages/opentelemetry/tsconfig.json b/repos/effect/packages/opentelemetry/tsconfig.json new file mode 100644 index 0000000..3edbf6b --- /dev/null +++ b/repos/effect/packages/opentelemetry/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.base.json", + "include": [], + "references": [ + { "path": "tsconfig.src.json" }, + { "path": "tsconfig.test.json" }, + { "path": "tsconfig.examples.json" } + ] +} diff --git a/repos/effect/packages/opentelemetry/tsconfig.src.json b/repos/effect/packages/opentelemetry/tsconfig.src.json new file mode 100644 index 0000000..9c2b466 --- /dev/null +++ b/repos/effect/packages/opentelemetry/tsconfig.src.json @@ -0,0 +1,13 @@ +{ + "extends": "../../tsconfig.base.json", + "include": ["src"], + "references": [ + { "path": "../effect/tsconfig.src.json" }, + { "path": "../platform/tsconfig.src.json" } + ], + "compilerOptions": { + "tsBuildInfoFile": ".tsbuildinfo/src.tsbuildinfo", + "rootDir": "src", + "outDir": "build/src" + } +} diff --git a/repos/effect/packages/opentelemetry/tsconfig.test.json b/repos/effect/packages/opentelemetry/tsconfig.test.json new file mode 100644 index 0000000..185e706 --- /dev/null +++ b/repos/effect/packages/opentelemetry/tsconfig.test.json @@ -0,0 +1,13 @@ +{ + "extends": "../../tsconfig.base.json", + "include": ["test"], + "references": [ + { "path": "tsconfig.src.json" }, + { "path": "../vitest/tsconfig.src.json" } + ], + "compilerOptions": { + "tsBuildInfoFile": ".tsbuildinfo/test.tsbuildinfo", + "rootDir": "test", + "noEmit": true + } +} diff --git a/repos/effect/packages/opentelemetry/vitest.config.ts b/repos/effect/packages/opentelemetry/vitest.config.ts new file mode 100644 index 0000000..578d066 --- /dev/null +++ b/repos/effect/packages/opentelemetry/vitest.config.ts @@ -0,0 +1,6 @@ +import { mergeConfig, type ViteUserConfig } from "vitest/config" +import shared from "../../vitest.shared.js" + +const config: ViteUserConfig = {} + +export default mergeConfig(shared, config) diff --git a/repos/effect/packages/platform-browser/CHANGELOG.md b/repos/effect/packages/platform-browser/CHANGELOG.md new file mode 100644 index 0000000..7bf255f --- /dev/null +++ b/repos/effect/packages/platform-browser/CHANGELOG.md @@ -0,0 +1,3958 @@ +# @effect/platform-browser + +## 0.76.0 + +### Patch Changes + +- Updated dependencies [[`f7bb09b`](https://github.com/Effect-TS/effect/commit/f7bb09b022f195d1f2b3c23d49e74b011ec5d109), [`bd7552a`](https://github.com/Effect-TS/effect/commit/bd7552a19cc0ed575507ac6cc0879a57e24ebd31), [`ad1a7eb`](https://github.com/Effect-TS/effect/commit/ad1a7eb7f6bebaf91c80be2443ac0439226d0098), [`0d32048`](https://github.com/Effect-TS/effect/commit/0d32048f9836e2b23a6ba3ec5f43f0a000bb92fb), [`0d32048`](https://github.com/Effect-TS/effect/commit/0d32048f9836e2b23a6ba3ec5f43f0a000bb92fb)]: + - effect@3.21.0 + - @effect/platform@0.96.0 + +## 0.75.0 + +### Patch Changes + +- Updated dependencies [[`fc82e81`](https://github.com/Effect-TS/effect/commit/fc82e81448bd9136a37580139ce46a2c61b11b54), [`82996bc`](https://github.com/Effect-TS/effect/commit/82996bce8debffcb44feb98bb862cf2662bd56b7), [`4d97a61`](https://github.com/Effect-TS/effect/commit/4d97a61a15b9dd6a0eece65b8f0c035e16d42ada), [`f6b0960`](https://github.com/Effect-TS/effect/commit/f6b0960bf3184109920dfed16ee7dfd7d67bc0f2), [`8798a84`](https://github.com/Effect-TS/effect/commit/8798a843218e6c0c0d3a8eee83360880e370b4da)]: + - effect@3.20.0 + - @effect/platform@0.95.0 + +## 0.74.0 + +### Patch Changes + +- Updated dependencies [[`77eeb86`](https://github.com/Effect-TS/effect/commit/77eeb86ddf208e51ec25932af83d52d3b4700371), [`ff7053f`](https://github.com/Effect-TS/effect/commit/ff7053f6d8508567b6145239f97aacc5773b0c53), [`287c32c`](https://github.com/Effect-TS/effect/commit/287c32c9f10da8e96f2b9ef8424316189d9ad4b3)]: + - effect@3.19.13 + - @effect/platform@0.94.0 + +## 0.73.0 + +### Patch Changes + +- Updated dependencies [[`3c15d5f`](https://github.com/Effect-TS/effect/commit/3c15d5f99fb8d8470a00c5a33d9ba3cac89dfe4c), [`3863fa8`](https://github.com/Effect-TS/effect/commit/3863fa89f61e63e5529fd961e37333bddf7db64a), [`2a03c76`](https://github.com/Effect-TS/effect/commit/2a03c76c2781ca7e9e228e838eab2eb0d0795b1d), [`24a1685`](https://github.com/Effect-TS/effect/commit/24a1685c70a9ed157468650f95a5c3da3f2c2433)]: + - effect@3.19.0 + - @effect/platform@0.93.0 + +## 0.72.0 + +### Patch Changes + +- Updated dependencies [[`1c6ab74`](https://github.com/Effect-TS/effect/commit/1c6ab74b314b2b6df8bb1b1a0cb9527ceda0e3fa), [`70fe803`](https://github.com/Effect-TS/effect/commit/70fe803469db3355ffbf8359b52c351f1c2dc137), [`c296e32`](https://github.com/Effect-TS/effect/commit/c296e32554143b84ae8987046984e1cf1852417c), [`a098ddf`](https://github.com/Effect-TS/effect/commit/a098ddfc551f5aa0a7c36f9b4928372a64d4d9f2)]: + - effect@3.18.0 + - @effect/platform@0.92.0 + +## 0.71.0 + +### Patch Changes + +- Updated dependencies [[`d4d86a8`](https://github.com/Effect-TS/effect/commit/d4d86a81f02b94e09fce8004ce2c5369c505ca5a)]: + - @effect/platform@0.91.0 + +## 0.70.0 + +### Patch Changes + +- Updated dependencies [[`7813640`](https://github.com/Effect-TS/effect/commit/7813640279d9e3a3e7fc0a29bfb5c6d5fb3c270f)]: + - @effect/platform@0.90.0 + +## 0.69.0 + +### Patch Changes + +- Updated dependencies [[`40c3c87`](https://github.com/Effect-TS/effect/commit/40c3c875f724264312b43002859c82bed9ad0df9), [`ed2c74a`](https://github.com/Effect-TS/effect/commit/ed2c74ae8fa4ea0dd06ea84a3e58cd32e6916104), [`073a1b8`](https://github.com/Effect-TS/effect/commit/073a1b8be5dbfa87454393ee7346f5bc36a4fd63), [`f382e99`](https://github.com/Effect-TS/effect/commit/f382e99e409838a879246250fc3994b9bf5b3c2c), [`e8c7ba5`](https://github.com/Effect-TS/effect/commit/e8c7ba5fd3eb0c3ae3039fc24c09d69391987989), [`7e10415`](https://github.com/Effect-TS/effect/commit/7e1041599ade25103428703f5d2dfd7378a09636), [`e9bdece`](https://github.com/Effect-TS/effect/commit/e9bdececdc24f60a246be5055eca71a0d49ea7f2), [`8d95eb0`](https://github.com/Effect-TS/effect/commit/8d95eb0356b1d1736204836c275d201a547d208d)]: + - effect@3.17.0 + - @effect/platform@0.89.0 + +## 0.68.1 + +### Patch Changes + +- Updated dependencies [[`f5dfabf`](https://github.com/Effect-TS/effect/commit/f5dfabf51ba481a4468c1509c537314978ef6cec), [`17a5ea8`](https://github.com/Effect-TS/effect/commit/17a5ea8fa29785fe6e4c9480f2a2e9c8c59f3f38), [`d25f22b`](https://github.com/Effect-TS/effect/commit/d25f22be7598abe977caf6cdac3b0dd78b438c48)]: + - effect@3.16.14 + - @effect/platform@0.88.1 + +## 0.68.0 + +### Patch Changes + +- Updated dependencies [[`27206d7`](https://github.com/Effect-TS/effect/commit/27206d7f0558d7fe28de57bf54f1d0cc83acc92e), [`dbabf5e`](https://github.com/Effect-TS/effect/commit/dbabf5e76fa63b050d2b6c466713c7dc59f07d3c)]: + - @effect/platform@0.88.0 + +## 0.67.13 + +### Patch Changes + +- Updated dependencies [[`c1c05a8`](https://github.com/Effect-TS/effect/commit/c1c05a8242fb5df7445b4a12387a60eac7726eb7), [`81fe4a2`](https://github.com/Effect-TS/effect/commit/81fe4a2c81d5e30e180a60e68c52016a27b350db)]: + - effect@3.16.13 + - @effect/platform@0.87.13 + +## 0.67.12 + +### Patch Changes + +- Updated dependencies [[`32ba77a`](https://github.com/Effect-TS/effect/commit/32ba77ae304d2161362a73e8b61965332626cf2d), [`d5e25b2`](https://github.com/Effect-TS/effect/commit/d5e25b237f05670ee42b386cb40b2cb448fc11d7)]: + - @effect/platform@0.87.12 + +## 0.67.11 + +### Patch Changes + +- Updated dependencies [[`001392b`](https://github.com/Effect-TS/effect/commit/001392ba8bfcad101bb034348a7415012fb12f72), [`7bfb099`](https://github.com/Effect-TS/effect/commit/7bfb099cb5528511b8d63045c4fbb4dc9cb18528)]: + - @effect/platform@0.87.11 + +## 0.67.10 + +### Patch Changes + +- Updated dependencies [[`678318d`](https://github.com/Effect-TS/effect/commit/678318d2e88233156b006acda56c2d138ee3ffa0), [`678318d`](https://github.com/Effect-TS/effect/commit/678318d2e88233156b006acda56c2d138ee3ffa0)]: + - @effect/platform@0.87.10 + +## 0.67.9 + +### Patch Changes + +- Updated dependencies [[`54514a2`](https://github.com/Effect-TS/effect/commit/54514a2f53166de27ad7e756dbf12194691fd4af)]: + - @effect/platform@0.87.9 + +## 0.67.8 + +### Patch Changes + +- Updated dependencies [[`4ce4f82`](https://github.com/Effect-TS/effect/commit/4ce4f824f6fdef492be1d35c05a490ffce518c89)]: + - @effect/platform@0.87.8 + +## 0.67.7 + +### Patch Changes + +- Updated dependencies [[`a9b617f`](https://github.com/Effect-TS/effect/commit/a9b617f125171ed76cd79ab46d7a924daf3b0e70), [`7e26e86`](https://github.com/Effect-TS/effect/commit/7e26e86524abcc93713d6ad7eee486638c98f7c2)]: + - @effect/platform@0.87.7 + +## 0.67.6 + +### Patch Changes + +- Updated dependencies [[`905da99`](https://github.com/Effect-TS/effect/commit/905da996aad665057b4ca6dba1a4af44fb8835bd)]: + - effect@3.16.12 + - @effect/platform@0.87.6 + +## 0.67.5 + +### Patch Changes + +- Updated dependencies [[`2fd8676`](https://github.com/Effect-TS/effect/commit/2fd8676c803cd40000dfc3231f5daecaa0e0ebd2)]: + - @effect/platform@0.87.5 + +## 0.67.4 + +### Patch Changes + +- Updated dependencies [[`e82a4fd`](https://github.com/Effect-TS/effect/commit/e82a4fd60f6528d08cef1a4aba0abe0d3ba741ad)]: + - @effect/platform@0.87.4 + +## 0.67.3 + +### Patch Changes + +- Updated dependencies [[`1b6e396`](https://github.com/Effect-TS/effect/commit/1b6e396d699f3cbbc56b68f99055cf746529bb9e)]: + - @effect/platform@0.87.3 + +## 0.67.2 + +### Patch Changes + +- Updated dependencies [[`4fea68c`](https://github.com/Effect-TS/effect/commit/4fea68ca7a25a3c39a1ab68b3885534513ab0c81), [`b927954`](https://github.com/Effect-TS/effect/commit/b9279543cf5688dd8a577af80456959c615217d0), [`99590a6`](https://github.com/Effect-TS/effect/commit/99590a6ca9128eb1ede265b6670b655311995614), [`6c3e24c`](https://github.com/Effect-TS/effect/commit/6c3e24c2308f7d4a29b8f4270ab81bca22ac6bb4)]: + - @effect/platform@0.87.2 + - effect@3.16.11 + +## 0.67.1 + +### Patch Changes + +- Updated dependencies [[`faad30e`](https://github.com/Effect-TS/effect/commit/faad30ec8742916be59f9db642d0fc98225b636c)]: + - effect@3.16.10 + - @effect/platform@0.87.1 + +## 0.67.0 + +### Patch Changes + +- Updated dependencies [[`b5bac9a`](https://github.com/Effect-TS/effect/commit/b5bac9ac2913fcd11b02322624f03b544eef53ba)]: + - @effect/platform@0.87.0 + +## 0.66.0 + +### Patch Changes + +- Updated dependencies [[`5137c70`](https://github.com/Effect-TS/effect/commit/5137c703461d8d3b363c112140a6e7f798241d07), [`c23d25c`](https://github.com/Effect-TS/effect/commit/c23d25c3e7c541f1f63b28484d8c461d86c67e99), [`5137c70`](https://github.com/Effect-TS/effect/commit/5137c703461d8d3b363c112140a6e7f798241d07), [`5137c70`](https://github.com/Effect-TS/effect/commit/5137c703461d8d3b363c112140a6e7f798241d07)]: + - effect@3.16.9 + - @effect/platform@0.86.0 + +## 0.65.2 + +### Patch Changes + +- Updated dependencies [[`914a191`](https://github.com/Effect-TS/effect/commit/914a191e7cb6341a3d0e965bccd27c336cf22e44)]: + - @effect/platform@0.85.2 + +## 0.65.1 + +### Patch Changes + +- Updated dependencies [[`8cb98d5`](https://github.com/Effect-TS/effect/commit/8cb98d53e68330228287ce2a2e0d8a4c86bcab3b), [`db2dd3c`](https://github.com/Effect-TS/effect/commit/db2dd3c3a8a77d791eae19e66153527e1cde4e6e)]: + - effect@3.16.8 + - @effect/platform@0.85.1 + +## 0.65.0 + +### Patch Changes + +- Updated dependencies [[`93687dd`](https://github.com/Effect-TS/effect/commit/93687ddbb25ce3b324cd2b83d2ccff225e97307e), [`93687dd`](https://github.com/Effect-TS/effect/commit/93687ddbb25ce3b324cd2b83d2ccff225e97307e), [`93687dd`](https://github.com/Effect-TS/effect/commit/93687ddbb25ce3b324cd2b83d2ccff225e97307e)]: + - @effect/platform@0.85.0 + +## 0.64.11 + +### Patch Changes + +- Updated dependencies [[`1bb0d8a`](https://github.com/Effect-TS/effect/commit/1bb0d8ab96782e99434356266b38251554ea0294)]: + - effect@3.16.7 + - @effect/platform@0.84.11 + +## 0.64.10 + +### Patch Changes + +- Updated dependencies [[`a5f7595`](https://github.com/Effect-TS/effect/commit/a5f75956ef9a15a83c416517ef493f0ee2f5ee8a), [`a02470c`](https://github.com/Effect-TS/effect/commit/a02470c75579e91525a25adb3f21b3650d042fdd), [`bf369b2`](https://github.com/Effect-TS/effect/commit/bf369b2902a0e0b195d957c18b9efd180942cf8b), [`f891d45`](https://github.com/Effect-TS/effect/commit/f891d45adffdafd3f94a2eca23faa354e3a409a8)]: + - effect@3.16.6 + - @effect/platform@0.84.10 + +## 0.64.9 + +### Patch Changes + +- Updated dependencies [[`bf418ef`](https://github.com/Effect-TS/effect/commit/bf418ef14a0f2ec965535793d5cea8fa8ba177ac)]: + - effect@3.16.5 + - @effect/platform@0.84.9 + +## 0.64.8 + +### Patch Changes + +- Updated dependencies [[`8b9db77`](https://github.com/Effect-TS/effect/commit/8b9db7742846af0f58fd8e8b7acb7f4f5ff487ec)]: + - @effect/platform@0.84.8 + +## 0.64.7 + +### Patch Changes + +- Updated dependencies [[`74ab9a0`](https://github.com/Effect-TS/effect/commit/74ab9a0a9e16d6e019369d256e1e24175c8bc3f3), [`770008e`](https://github.com/Effect-TS/effect/commit/770008eca3aad2899a2ed951236e575793294b28)]: + - effect@3.16.4 + - @effect/platform@0.84.7 + +## 0.64.6 + +### Patch Changes + +- Updated dependencies [[`ceea77a`](https://github.com/Effect-TS/effect/commit/ceea77a13055f145520f763e3fce5b8ff15d728f)]: + - @effect/platform@0.84.6 + +## 0.64.5 + +### Patch Changes + +- Updated dependencies [[`ec52c6a`](https://github.com/Effect-TS/effect/commit/ec52c6a2211e76972462b15b9d5a9d6d56761b7a)]: + - @effect/platform@0.84.5 + +## 0.64.4 + +### Patch Changes + +- Updated dependencies [[`87722fc`](https://github.com/Effect-TS/effect/commit/87722fce693a9b49284bbddbf82d30714c688261), [`36217ee`](https://github.com/Effect-TS/effect/commit/36217eeb1337edd9ac3f9a635b80a6385d22ae8f)]: + - effect@3.16.3 + - @effect/platform@0.84.4 + +## 0.64.3 + +### Patch Changes + +- Updated dependencies [[`ab7684f`](https://github.com/Effect-TS/effect/commit/ab7684f1c2a0671bf091f255d220e3a4cc7f528e)]: + - @effect/platform@0.84.3 + +## 0.64.2 + +### Patch Changes + +- Updated dependencies [[`0ddf148`](https://github.com/Effect-TS/effect/commit/0ddf148a247aa87af043d276b8453a714a400897)]: + - effect@3.16.2 + - @effect/platform@0.84.2 + +## 0.64.1 + +### Patch Changes + +- Updated dependencies [[`71174d0`](https://github.com/Effect-TS/effect/commit/71174d09691314a9b6b66189e456fd21e3eb6543), [`d615e6e`](https://github.com/Effect-TS/effect/commit/d615e6e5b944f6fd5e627e31752c7ca7e4e1c17d)]: + - @effect/platform@0.84.1 + - effect@3.16.1 + +## 0.64.0 + +### Patch Changes + +- Updated dependencies [[`ee0bd5d`](https://github.com/Effect-TS/effect/commit/ee0bd5d24864752c54cb359f67a67dd903971ec4), [`5189800`](https://github.com/Effect-TS/effect/commit/51898004e11766b8cf6d95e960b636f6d5db79ec), [`58bfeaa`](https://github.com/Effect-TS/effect/commit/58bfeaa64ded8c88f772b184311c0c0dbac10960), [`194d748`](https://github.com/Effect-TS/effect/commit/194d7486943f56f3267ef415395ac220a4b3e634), [`918c9ea`](https://github.com/Effect-TS/effect/commit/918c9ea1a57facb154f0fb26792021f337054dee), [`9198e6f`](https://github.com/Effect-TS/effect/commit/9198e6fcc1a3ff4fefb3363004de558d8de01f40), [`2a370bf`](https://github.com/Effect-TS/effect/commit/2a370bf625fdeede5659721468eb0d527e403279), [`58ccb91`](https://github.com/Effect-TS/effect/commit/58ccb91328c8df5d49808b673738bc09df355201), [`fd47834`](https://github.com/Effect-TS/effect/commit/fd478348203fa89462b0a1d067ce4de034353df4)]: + - effect@3.16.0 + - @effect/platform@0.84.0 + +## 0.63.0 + +### Patch Changes + +- Updated dependencies [[`5522520`](https://github.com/Effect-TS/effect/commit/55225206ab9af0ad60b1c0654690a8a096d625cd), [`cc5bb2b`](https://github.com/Effect-TS/effect/commit/cc5bb2b918a9450a975f702dabcea891bda382cb)]: + - @effect/platform@0.83.0 + - effect@3.15.5 + +## 0.62.8 + +### Patch Changes + +- Updated dependencies [[`0617b9d`](https://github.com/Effect-TS/effect/commit/0617b9dc365f1963b36949ad7f9023ab6eb94524)]: + - @effect/platform@0.82.8 + +## 0.62.7 + +### Patch Changes + +- Updated dependencies [[`f570554`](https://github.com/Effect-TS/effect/commit/f57055459524587b041340577dad85476bb35f81), [`78047e8`](https://github.com/Effect-TS/effect/commit/78047e8dfc8005b66f87afe50bb95981fea51561), [`c20b95a`](https://github.com/Effect-TS/effect/commit/c20b95a99ffe452b4774c844d397a905f713b6d6), [`94ada43`](https://github.com/Effect-TS/effect/commit/94ada430928d5685bdbef513e87562c20774a3a2)]: + - effect@3.15.4 + - @effect/platform@0.82.7 + +## 0.62.6 + +### Patch Changes + +- Updated dependencies [[`618903b`](https://github.com/Effect-TS/effect/commit/618903ba9ae96e2bfe6ee31f61c4359b915f2a36)]: + - @effect/platform@0.82.6 + +## 0.62.5 + +### Patch Changes + +- Updated dependencies [[`7764a07`](https://github.com/Effect-TS/effect/commit/7764a07d960c60df81f14e1dc949518f4bbe494a), [`4577f54`](https://github.com/Effect-TS/effect/commit/4577f548d67273e576cdde423bdd34a4b910766a), [`30a0d9c`](https://github.com/Effect-TS/effect/commit/30a0d9cb51c84290d51b1361d72ff5cee33c13c7)]: + - @effect/platform@0.82.5 + - effect@3.15.3 + +## 0.62.4 + +### Patch Changes + +- Updated dependencies [[`d45e8a8`](https://github.com/Effect-TS/effect/commit/d45e8a8ac8227192f504e39e6d04fdcf4fb1d225), [`d13b68e`](https://github.com/Effect-TS/effect/commit/d13b68e3a9456d0bfee9bca8273a7b44a9c69087)]: + - @effect/platform@0.82.4 + +## 0.62.3 + +### Patch Changes + +- Updated dependencies [[`b8722b8`](https://github.com/Effect-TS/effect/commit/b8722b817e2306fe8c8245f3f9e32d85b824b961), [`a328f4b`](https://github.com/Effect-TS/effect/commit/a328f4b4fe717dd53e5b04a30f387433c32f7328)]: + - effect@3.15.2 + - @effect/platform@0.82.3 + +## 0.62.2 + +### Patch Changes + +- Updated dependencies [[`739a3d4`](https://github.com/Effect-TS/effect/commit/739a3d4a4565915fe2e690003f4f9085cb4422fc)]: + - @effect/platform@0.82.2 + +## 0.62.1 + +### Patch Changes + +- Updated dependencies [[`787ce70`](https://github.com/Effect-TS/effect/commit/787ce7042e35b657963473c6efe47752868cd811), [`1269641`](https://github.com/Effect-TS/effect/commit/1269641a99ae43069f7648ff79ffe8729b54b348), [`1269641`](https://github.com/Effect-TS/effect/commit/1269641a99ae43069f7648ff79ffe8729b54b348)]: + - effect@3.15.1 + - @effect/platform@0.82.1 + +## 0.62.0 + +### Patch Changes + +- Updated dependencies [[`c654595`](https://github.com/Effect-TS/effect/commit/c65459587b51da140b78098e81fdbfece65d53e2), [`d9f5dea`](https://github.com/Effect-TS/effect/commit/d9f5deae0f02f5de2b9fcb1cca8b142ba4bc2bba), [`49aa723`](https://github.com/Effect-TS/effect/commit/49aa7236a15e13f818c86edbca08c4af67c8dfaf), [`74c14d0`](https://github.com/Effect-TS/effect/commit/74c14d01d0cb48cf517a1b6e29a373a96ed0ff5b), [`e4f49b6`](https://github.com/Effect-TS/effect/commit/e4f49b66857e01b74ab6a9a0bc7132f44cd04cbb), [`6f02224`](https://github.com/Effect-TS/effect/commit/6f02224b3fc46a682ad2defb1a260841956c6780), [`1dcfd41`](https://github.com/Effect-TS/effect/commit/1dcfd41ff96abd706901293a00c1893cb29dd8fd), [`b21ab16`](https://github.com/Effect-TS/effect/commit/b21ab16b6f773e7ec4369db4e752c35e719f7870), [`fcf1822`](https://github.com/Effect-TS/effect/commit/fcf1822f98fcda60351d64e9d2c2c13563d7e6db), [`0061dd1`](https://github.com/Effect-TS/effect/commit/0061dd140740165e91569a684cce27a77b23229e), [`8421e6e`](https://github.com/Effect-TS/effect/commit/8421e6e49332bca8f96f482dfd48680e238b3a89), [`a9b3fb7`](https://github.com/Effect-TS/effect/commit/a9b3fb78abcfdb525318a956fd02fcadeb56143e), [`fa10f56`](https://github.com/Effect-TS/effect/commit/fa10f56b96bd9af070ba99ebc3279aa93954261e)]: + - effect@3.15.0 + - @effect/platform@0.82.0 + +## 0.61.1 + +### Patch Changes + +- Updated dependencies [[`24a9ebb`](https://github.com/Effect-TS/effect/commit/24a9ebbb5af598f0bfd6ecc45307e528043fe011)]: + - effect@3.14.22 + - @effect/platform@0.81.1 + +## 0.61.0 + +### Patch Changes + +- Updated dependencies [[`672920f`](https://github.com/Effect-TS/effect/commit/672920f85da8abd5f9d4ad85e29248a2aca57ed8)]: + - @effect/platform@0.81.0 + +## 0.60.12 + +### Patch Changes + +- Updated dependencies [[`2f3b7d4`](https://github.com/Effect-TS/effect/commit/2f3b7d4e1fa1ef8790b0ca4da22eb88872ee31df)]: + - effect@3.14.21 + - @effect/platform@0.80.21 + +## 0.60.11 + +### Patch Changes + +- Updated dependencies [[`17e2f30`](https://github.com/Effect-TS/effect/commit/17e2f3091408cf0fca9414d4af3bdf7b2765b378)]: + - effect@3.14.20 + - @effect/platform@0.80.20 + +## 0.60.10 + +### Patch Changes + +- Updated dependencies [[`056a910`](https://github.com/Effect-TS/effect/commit/056a910d0a0b8b00b0dc9df4a070466b2b5c2f6c), [`e25e7bb`](https://github.com/Effect-TS/effect/commit/e25e7bbc1797733916f48f501425d9f2ef310d9f), [`3273d57`](https://github.com/Effect-TS/effect/commit/3273d572c2b3175a842677f19efeea4cd65ab016)]: + - effect@3.14.19 + - @effect/platform@0.80.19 + +## 0.60.9 + +### Patch Changes + +- Updated dependencies [[`b1164d4`](https://github.com/Effect-TS/effect/commit/b1164d49a1dfdf299e9971367b6fc6be4df0ddff)]: + - effect@3.14.18 + - @effect/platform@0.80.18 + +## 0.60.8 + +### Patch Changes + +- Updated dependencies [[`0b54681`](https://github.com/Effect-TS/effect/commit/0b54681cd89245e211d8f49272be0f1bf2f81813), [`41a59d5`](https://github.com/Effect-TS/effect/commit/41a59d5916a296b12b0d5ead9e859e05f40b4cce)]: + - effect@3.14.17 + - @effect/platform@0.80.17 + +## 0.60.7 + +### Patch Changes + +- Updated dependencies [[`ee14444`](https://github.com/Effect-TS/effect/commit/ee144441021ec77039e43396eaf90714687bb495), [`f1c8583`](https://github.com/Effect-TS/effect/commit/f1c8583f8c3ea9415f813795ca2940a897c9ba9a)]: + - effect@3.14.16 + - @effect/platform@0.80.16 + +## 0.60.6 + +### Patch Changes + +- Updated dependencies [[`239cc99`](https://github.com/Effect-TS/effect/commit/239cc995ce645946210a3c3d2cb52bd3547c0687), [`8b6c947`](https://github.com/Effect-TS/effect/commit/8b6c947eaa8e45a67ecb3c37d45cd27f3e41d165), [`c50a63b`](https://github.com/Effect-TS/effect/commit/c50a63bbecb9f560b9cae349c447eed877d1b9b6)]: + - effect@3.14.15 + - @effect/platform@0.80.15 + +## 0.60.5 + +### Patch Changes + +- Updated dependencies [[`6ed8d15`](https://github.com/Effect-TS/effect/commit/6ed8d1589beb181d30abc79afebdaabc1d101538)]: + - effect@3.14.14 + - @effect/platform@0.80.14 + +## 0.60.4 + +### Patch Changes + +- Updated dependencies [[`ee77788`](https://github.com/Effect-TS/effect/commit/ee77788747e7ebbde6bfa88256cde49dbbad3608), [`5fce6ba`](https://github.com/Effect-TS/effect/commit/5fce6ba19c3cc63cc0104e737e581ad989dedbf0), [`570e45f`](https://github.com/Effect-TS/effect/commit/570e45f8cb936e42ec48f67f21bb2b7252f36c0c)]: + - effect@3.14.13 + - @effect/platform@0.80.13 + +## 0.60.3 + +### Patch Changes + +- Updated dependencies [[`c2ad9ee`](https://github.com/Effect-TS/effect/commit/c2ad9ee9f3c4c743390edf35ed9e85a20be33811), [`9c68654`](https://github.com/Effect-TS/effect/commit/9c686542b6eb3ea188cb70673ef2e41223633e89)]: + - effect@3.14.12 + - @effect/platform@0.80.12 + +## 0.60.2 + +### Patch Changes + +- Updated dependencies [[`e536127`](https://github.com/Effect-TS/effect/commit/e536127c1e6f2fb3a542c73ae919435a629a346b)]: + - effect@3.14.11 + - @effect/platform@0.80.11 + +## 0.60.1 + +### Patch Changes + +- Updated dependencies [[`bc7efa3`](https://github.com/Effect-TS/effect/commit/bc7efa3b031bb25e1ed3c8f2d3fb5e8da166cadc)]: + - effect@3.14.10 + - @effect/platform@0.80.10 + +## 0.60.0 + +### Minor Changes + +- [#4747](https://github.com/Effect-TS/effect/pull/4747) [`e857e9a`](https://github.com/Effect-TS/effect/commit/e857e9ad5d59972387b14c39ea0b253e6f659b3e) Thanks @titouancreach! - Clone PermissionStatus properties explicitly + +### Patch Changes + +- Updated dependencies [[`d78249f`](https://github.com/Effect-TS/effect/commit/d78249f0b67f63cf4baf806ff090cba33293daf0)]: + - effect@3.14.9 + - @effect/platform@0.80.9 + +## 0.59.8 + +### Patch Changes + +- Updated dependencies [[`b3a2d32`](https://github.com/Effect-TS/effect/commit/b3a2d32772e6f7f20eacf2e18128e99324c4d378)]: + - effect@3.14.8 + - @effect/platform@0.80.8 + +## 0.59.7 + +### Patch Changes + +- Updated dependencies [[`b542a4b`](https://github.com/Effect-TS/effect/commit/b542a4bf195be0c9af1523e1ba96c953decc4d25)]: + - effect@3.14.7 + - @effect/platform@0.80.7 + +## 0.59.6 + +### Patch Changes + +- Updated dependencies [[`47618c1`](https://github.com/Effect-TS/effect/commit/47618c1ad84ebcc5a51133a3fff5aa5012d49d45), [`6077882`](https://github.com/Effect-TS/effect/commit/60778824a4794336c33807801f813f8751d1c7e4)]: + - effect@3.14.6 + - @effect/platform@0.80.6 + +## 0.59.5 + +### Patch Changes + +- Updated dependencies [[`40dbfef`](https://github.com/Effect-TS/effect/commit/40dbfeff239b6e567706752114f31b2fce7de4e3), [`85fba81`](https://github.com/Effect-TS/effect/commit/85fba815ac07eb13d4227a69ac76a18e4b94df18), [`5a5ebdd`](https://github.com/Effect-TS/effect/commit/5a5ebdddfaddd259538b4599a6676281faca778e)]: + - effect@3.14.5 + - @effect/platform@0.80.5 + +## 0.59.4 + +### Patch Changes + +- Updated dependencies [[`e4ba2c6`](https://github.com/Effect-TS/effect/commit/e4ba2c66a878e81b5e295d6d49aaf724b80a28ef)]: + - effect@3.14.4 + - @effect/platform@0.80.4 + +## 0.59.3 + +### Patch Changes + +- Updated dependencies [[`37aa8e1`](https://github.com/Effect-TS/effect/commit/37aa8e137725a902e70cd1e468ea98b873aa5056), [`34f03d6`](https://github.com/Effect-TS/effect/commit/34f03d66875f21f266f102223a03cd14c2ed6ea6)]: + - effect@3.14.3 + - @effect/platform@0.80.3 + +## 0.59.2 + +### Patch Changes + +- Updated dependencies [[`f87991b`](https://github.com/Effect-TS/effect/commit/f87991b6d8a2edfaf90b01cebda4b466992ae865), [`f87991b`](https://github.com/Effect-TS/effect/commit/f87991b6d8a2edfaf90b01cebda4b466992ae865), [`0a3e3e1`](https://github.com/Effect-TS/effect/commit/0a3e3e18eea5e0d1882f1a6c906198e6ef226a41)]: + - effect@3.14.2 + - @effect/platform@0.80.2 + +## 0.59.1 + +### Patch Changes + +- Updated dependencies [[`4a274fe`](https://github.com/Effect-TS/effect/commit/4a274fe9f623182b6b902827e0e83bd89ca3b05c)]: + - effect@3.14.1 + - @effect/platform@0.80.1 + +## 0.59.0 + +### Minor Changes + +- [#4469](https://github.com/Effect-TS/effect/pull/4469) [`3131f8f`](https://github.com/Effect-TS/effect/commit/3131f8fd12ba9eb31b90fa2f42bf88b12309133c) Thanks @tim-smart! - refactor of @effect/cluster packages + +### Patch Changes + +- Updated dependencies [[`1f47e4e`](https://github.com/Effect-TS/effect/commit/1f47e4e12546ab691b29bfb7b5128bb17b93baa5), [`26dd75f`](https://github.com/Effect-TS/effect/commit/26dd75f276a0d8a63eab313bd5a167d5072c9780), [`aba2d1d`](https://github.com/Effect-TS/effect/commit/aba2d1d831ea149481bd4dd755528c0afa8239ce), [`3131f8f`](https://github.com/Effect-TS/effect/commit/3131f8fd12ba9eb31b90fa2f42bf88b12309133c), [`aba2d1d`](https://github.com/Effect-TS/effect/commit/aba2d1d831ea149481bd4dd755528c0afa8239ce), [`04dff2d`](https://github.com/Effect-TS/effect/commit/04dff2d01ac68c260f29a6d4743381825c353c86), [`c7fac0c`](https://github.com/Effect-TS/effect/commit/c7fac0cd7eadcd5cc0c3a987051c5b57ad271638), [`aba2d1d`](https://github.com/Effect-TS/effect/commit/aba2d1d831ea149481bd4dd755528c0afa8239ce), [`ffaa3f3`](https://github.com/Effect-TS/effect/commit/ffaa3f3969df26610fcc02ad537340641d44e803), [`ab957c1`](https://github.com/Effect-TS/effect/commit/ab957c1fee714868f56c7ab4e802b9d449e9b666), [`35db9ce`](https://github.com/Effect-TS/effect/commit/35db9ce228f1416c8abacc6dc9c36fbd0f33ef0f), [`aba2d1d`](https://github.com/Effect-TS/effect/commit/aba2d1d831ea149481bd4dd755528c0afa8239ce), [`cf77ea9`](https://github.com/Effect-TS/effect/commit/cf77ea9ab4fc89e66a43f682a9926ccdee6c57ed), [`26dd75f`](https://github.com/Effect-TS/effect/commit/26dd75f276a0d8a63eab313bd5a167d5072c9780), [`baaab60`](https://github.com/Effect-TS/effect/commit/baaab60b737f35dfab8e4a21bce28a195d19e899)]: + - effect@3.14.0 + - @effect/platform@0.80.0 + +## 0.58.4 + +### Patch Changes + +- Updated dependencies [[`5662363`](https://github.com/Effect-TS/effect/commit/566236361e270e575ef1cbf308ad1967c82a362c), [`5f1fd15`](https://github.com/Effect-TS/effect/commit/5f1fd15308ab154791580059b89877d19a2055c2), [`8bb1460`](https://github.com/Effect-TS/effect/commit/8bb1460c824f66f0f25ebd899c5e74e388089c37)]: + - @effect/platform@0.79.4 + +## 0.58.3 + +### Patch Changes + +- Updated dependencies [[`0c4803f`](https://github.com/Effect-TS/effect/commit/0c4803fcc69262d11a97ce49d0e9b4288df0651f), [`6f65ac4`](https://github.com/Effect-TS/effect/commit/6f65ac4eac1489cd6ea390e18b0908670722adad)]: + - effect@3.13.12 + - @effect/platform@0.79.3 + +## 0.58.2 + +### Patch Changes + +- Updated dependencies [[`fad8cca`](https://github.com/Effect-TS/effect/commit/fad8cca9bbfcc2eaeb44b97c15dbe0a1eda75315), [`4296293`](https://github.com/Effect-TS/effect/commit/4296293049414d0cf2d915a26c552b09f946b9a0), [`9c241ab`](https://github.com/Effect-TS/effect/commit/9c241abe47ccf7a5257b98a4a64a63054a12741d), [`082b0c1`](https://github.com/Effect-TS/effect/commit/082b0c1b9f4252bcdd69608f2e4a9226f953ac3f), [`be12983`](https://github.com/Effect-TS/effect/commit/be12983bc7e7537b41cd8910fc4eb7d1da56ab07), [`de88127`](https://github.com/Effect-TS/effect/commit/de88127a5a5906ccece98af74787b5ae0e65e431)]: + - effect@3.13.11 + - @effect/platform@0.79.2 + +## 0.58.1 + +### Patch Changes + +- Updated dependencies [[`527c964`](https://github.com/Effect-TS/effect/commit/527c9645229f5be9714a7e60a38a9e753c4bbfb1)]: + - effect@3.13.10 + - @effect/platform@0.79.1 + +## 0.58.0 + +### Minor Changes + +- [#4573](https://github.com/Effect-TS/effect/pull/4573) [`88fe129`](https://github.com/Effect-TS/effect/commit/88fe12923740765c0335a6e6203fdcc6a463edca) Thanks @tim-smart! - remove Scope from HttpClient requirements + + Before: + + ```ts + import { HttpClient } from "@effect/platform" + import { Effect } from "effect" + + Effect.gen(function* () { + const client = yield* HttpClient.HttpClient + const response = yield* client.get("https://api.github.com/users/octocat") + return yield* response.json + }).pipe(Effect.scoped) + ``` + + After: + + ```ts + import { HttpClient } from "@effect/platform" + import { Effect } from "effect" + + Effect.gen(function* () { + const client = yield* HttpClient.HttpClient + const response = yield* client.get("https://api.github.com/users/octocat") + return yield* response.json + }) // no need to add Effect.scoped + ``` + +### Patch Changes + +- [#4580](https://github.com/Effect-TS/effect/pull/4580) [`bbdc279`](https://github.com/Effect-TS/effect/commit/bbdc2795a461cb2d1fe19b2669526a6ef590c3d4) Thanks @tim-smart! - prevent worker handler interrupts from shutting down runner + +- Updated dependencies [[`88fe129`](https://github.com/Effect-TS/effect/commit/88fe12923740765c0335a6e6203fdcc6a463edca), [`d630249`](https://github.com/Effect-TS/effect/commit/d630249426113088abe8b382db4f14d80f2160c2), [`2976e52`](https://github.com/Effect-TS/effect/commit/2976e52538d9dc9ffdcbc84d4ac748cff9305971)]: + - @effect/platform@0.79.0 + - effect@3.13.9 + +## 0.57.1 + +### Patch Changes + +- Updated dependencies [[`c65d336`](https://github.com/Effect-TS/effect/commit/c65d3362d07ec815ff3b46278314e8a31706ddc2), [`22d2ebb`](https://github.com/Effect-TS/effect/commit/22d2ebb4b11f5a44351a4736e65da391a3b647d0)]: + - effect@3.13.8 + - @effect/platform@0.78.1 + +## 0.57.0 + +### Patch Changes + +- Updated dependencies [[`c5bcf53`](https://github.com/Effect-TS/effect/commit/c5bcf53b7cb49dacffdd2a6cd8eb48cc452b417e)]: + - @effect/platform@0.78.0 + +## 0.56.7 + +### Patch Changes + +- Updated dependencies [[`840cc73`](https://github.com/Effect-TS/effect/commit/840cc7329908db7ca693ef47b07d4f845c29cadd), [`9bf8a74`](https://github.com/Effect-TS/effect/commit/9bf8a74b967f18d931743dd5196af326c9118e9c), [`87ba23c`](https://github.com/Effect-TS/effect/commit/87ba23c41c193503ed0c612b0d32d0b253794c64), [`f910880`](https://github.com/Effect-TS/effect/commit/f91088069057f3b4529753f5bc5532b028d726df), [`0d01480`](https://github.com/Effect-TS/effect/commit/0d014803e4f688f74386a80abd65485e1a319244)]: + - @effect/platform@0.77.7 + - effect@3.13.7 + +## 0.56.6 + +### Patch Changes + +- Updated dependencies [[`3154ce4`](https://github.com/Effect-TS/effect/commit/3154ce4692fa18b804982158d3c4c8a8a5fae386)]: + - effect@3.13.6 + - @effect/platform@0.77.6 + +## 0.56.5 + +### Patch Changes + +- Updated dependencies [[`367bb35`](https://github.com/Effect-TS/effect/commit/367bb35f4c2a254e1fb211d96db2474a7aed9020), [`6cf11c3`](https://github.com/Effect-TS/effect/commit/6cf11c3a75773ceec2877c85ddc760f381f0866d), [`a0acec8`](https://github.com/Effect-TS/effect/commit/a0acec851f72e19466363d24b9cc218acd00006a)]: + - effect@3.13.5 + - @effect/platform@0.77.5 + +## 0.56.4 + +### Patch Changes + +- Updated dependencies [[`e0746f9`](https://github.com/Effect-TS/effect/commit/e0746f9aa398b69c6542e375910683bf17f49f46), [`17d9e89`](https://github.com/Effect-TS/effect/commit/17d9e89f9851663bdbb6c1e685601d97806114a4)]: + - @effect/platform@0.77.4 + - effect@3.13.4 + +## 0.56.3 + +### Patch Changes + +- Updated dependencies [[`cc5588d`](https://github.com/Effect-TS/effect/commit/cc5588df07f9103513547cb429ce041b9436a8bd), [`623c8cd`](https://github.com/Effect-TS/effect/commit/623c8cd053ed6ee3d353aaa8778d484670fca2bb), [`00b4eb1`](https://github.com/Effect-TS/effect/commit/00b4eb1ece12a16e222e6220965bb4024d6752ac), [`f2aee98`](https://github.com/Effect-TS/effect/commit/f2aee989b0a600900ce83e7f460d02908620c80f), [`fb798eb`](https://github.com/Effect-TS/effect/commit/fb798eb9061f1191badc017d1aa649360254da20), [`2251b15`](https://github.com/Effect-TS/effect/commit/2251b1528810bb695b37ce388b653cec0c5bf80c), [`2e15c1e`](https://github.com/Effect-TS/effect/commit/2e15c1e33648add0b29fe274fbcb7294b7515085), [`a4979db`](https://github.com/Effect-TS/effect/commit/a4979db021aef16e731be64df196b72088fc4376), [`b74255a`](https://github.com/Effect-TS/effect/commit/b74255a304ad49d60bedb1a260fd697f370af27a), [`d7f6a5c`](https://github.com/Effect-TS/effect/commit/d7f6a5c7d26c1963dcd864ca62360d20d08c7b49), [`9dd8979`](https://github.com/Effect-TS/effect/commit/9dd8979e940915b1cc1b1f264f3d019c77a65a02), [`477b488`](https://github.com/Effect-TS/effect/commit/477b488284f47c5469d7fba3e4065fb7e3b6556e), [`10932cb`](https://github.com/Effect-TS/effect/commit/10932cbf58fc721ada631cebec42f773ce96d3cc), [`9f6c784`](https://github.com/Effect-TS/effect/commit/9f6c78468b3b5e9ebfc38ffdfb70702901ee977b), [`2c639ec`](https://github.com/Effect-TS/effect/commit/2c639ecee332de4266e36022c989c35ae4e02105), [`886aaa8`](https://github.com/Effect-TS/effect/commit/886aaa81e06dfd3cd9391e8ea987d8cd5ada1124)]: + - effect@3.13.3 + - @effect/platform@0.77.3 + +## 0.56.2 + +### Patch Changes + +- Updated dependencies [[`31be72a`](https://github.com/Effect-TS/effect/commit/31be72ada118cb84a942e67b1663263f8db74a9f), [`3e7ce97`](https://github.com/Effect-TS/effect/commit/3e7ce97f8a41756a039cf635d0b3d9a75d781097), [`31be72a`](https://github.com/Effect-TS/effect/commit/31be72ada118cb84a942e67b1663263f8db74a9f)]: + - effect@3.13.2 + - @effect/platform@0.77.2 + +## 0.56.1 + +### Patch Changes + +- Updated dependencies [[`b56a211`](https://github.com/Effect-TS/effect/commit/b56a2110569fd0ec0b57ac137743e926d49f51cc)]: + - effect@3.13.1 + - @effect/platform@0.77.1 + +## 0.56.0 + +### Patch Changes + +- Updated dependencies [[`8baef83`](https://github.com/Effect-TS/effect/commit/8baef83e7ff0b7bc0738b680e1ef013065386cff), [`655bfe2`](https://github.com/Effect-TS/effect/commit/655bfe29e44cc3f0fb9b4e53038f50b891c188df), [`d90cbc2`](https://github.com/Effect-TS/effect/commit/d90cbc274e2742d18671fe65aa4764c057eb6cba), [`75632bd`](https://github.com/Effect-TS/effect/commit/75632bd44b8025101d652ccbaeef898c7086c91c), [`c874a2e`](https://github.com/Effect-TS/effect/commit/c874a2e4b17e9d71904ca8375bb77b020975cb1d), [`bf865e5`](https://github.com/Effect-TS/effect/commit/bf865e5833f77fd8f6c06944ca9d507b54488301), [`f98b2b7`](https://github.com/Effect-TS/effect/commit/f98b2b7592cf20f9d85313e7f1e964cb65878138), [`de8ce92`](https://github.com/Effect-TS/effect/commit/de8ce924923eaa4e1b761a97eb45ec967389f3d5), [`cf8b2dd`](https://github.com/Effect-TS/effect/commit/cf8b2dd112f8e092ed99d78fd728db0f91c29050), [`db426a5`](https://github.com/Effect-TS/effect/commit/db426a5fb41ab84d18e3c8753a7329b4de544245), [`6862444`](https://github.com/Effect-TS/effect/commit/6862444094906ad4f2cb077ff3b9cc0b73880c8c), [`5fc8a90`](https://github.com/Effect-TS/effect/commit/5fc8a90ba46a5fd9f3b643f0b5aeadc69d717339), [`546a492`](https://github.com/Effect-TS/effect/commit/546a492e60eb2b8b048a489a474b934ea0877005), [`65c4796`](https://github.com/Effect-TS/effect/commit/65c47966ce39055f02cf5c808daabb3ea6442b0b), [`9760fdc`](https://github.com/Effect-TS/effect/commit/9760fdc37bdaef9da8b150e46b86ddfbe2ad9221), [`5b471e7`](https://github.com/Effect-TS/effect/commit/5b471e7d4317e8ee5d72bbbd3e0c9775160949ab), [`4f810cc`](https://github.com/Effect-TS/effect/commit/4f810cc2770e9f1f266851d2cb6257112c12af49)]: + - effect@3.13.0 + - @effect/platform@0.77.0 + +## 0.55.1 + +### Patch Changes + +- Updated dependencies [[`4018eae`](https://github.com/Effect-TS/effect/commit/4018eaed2733241676ddb8c52416f463a8c32e35), [`543d36d`](https://github.com/Effect-TS/effect/commit/543d36d1a11452560b01ab966a82529ad5fee8c9), [`c407726`](https://github.com/Effect-TS/effect/commit/c407726f79df4a567a9631cddd8effaa16b3535d), [`f70a65a`](https://github.com/Effect-TS/effect/commit/f70a65ac80c6635d80b12beaf4d32a9cc59fa143), [`ba409f6`](https://github.com/Effect-TS/effect/commit/ba409f69c41aeaa29e475c0630735726eaf4dbac), [`3d2e356`](https://github.com/Effect-TS/effect/commit/3d2e3565e8a43d1bdb5daee8db3b90f56d71d859)]: + - effect@3.12.12 + - @effect/platform@0.76.1 + +## 0.55.0 + +### Minor Changes + +- [#4429](https://github.com/Effect-TS/effect/pull/4429) [`2473ad5`](https://github.com/Effect-TS/effect/commit/2473ad5cf23582e3a41338091fa526ffe611288d) Thanks @tim-smart! - run platform workers in a Scope, send errors or termination to a CloseLatch + +### Patch Changes + +- [#4433](https://github.com/Effect-TS/effect/pull/4433) [`3ffe06d`](https://github.com/Effect-TS/effect/commit/3ffe06df875ab1e64ee99b8470b5d9c7d39feaaa) Thanks @tim-smart! - ensure last port in browser worker closes naturally with the outer scope + +- Updated dependencies [[`b6a032f`](https://github.com/Effect-TS/effect/commit/b6a032f07bffa020a848c813881879395134fa20), [`42ddd5f`](https://github.com/Effect-TS/effect/commit/42ddd5f144ce9f9d94a036679ebbd626446d37f5), [`2fe447c`](https://github.com/Effect-TS/effect/commit/2fe447c6354d334f9c591b8a8481818f5f0e797e), [`2473ad5`](https://github.com/Effect-TS/effect/commit/2473ad5cf23582e3a41338091fa526ffe611288d)]: + - effect@3.12.11 + - @effect/platform@0.76.0 + +## 0.54.4 + +### Patch Changes + +- Updated dependencies [[`e30f132`](https://github.com/Effect-TS/effect/commit/e30f132c336c9d0760bad39f82a55c7ce5159eb7), [`33fa667`](https://github.com/Effect-TS/effect/commit/33fa667c2623be1026e1ccee91bd44f73b09020a), [`87f5f28`](https://github.com/Effect-TS/effect/commit/87f5f2842e4196cb88d13f10f443ff0567e82832), [`7d57ecd`](https://github.com/Effect-TS/effect/commit/7d57ecdaf5da2345ebbf9c22df50317578bde0f5), [`4dbd170`](https://github.com/Effect-TS/effect/commit/4dbd170538e8fb7a36aa7c469c6f93b6c7000091)]: + - effect@3.12.10 + - @effect/platform@0.75.4 + +## 0.54.3 + +### Patch Changes + +- Updated dependencies [[`1b4a4e9`](https://github.com/Effect-TS/effect/commit/1b4a4e904ef5227ec7d9114d4e417eca19eed940)]: + - effect@3.12.9 + - @effect/platform@0.75.3 + +## 0.54.2 + +### Patch Changes + +- Updated dependencies [[`59b3cfb`](https://github.com/Effect-TS/effect/commit/59b3cfbbd5713dd9475998e95fad5534c0b21466), [`766113c`](https://github.com/Effect-TS/effect/commit/766113c0ea3512cdb887650ead8ba314236e22ee), [`bb05fb8`](https://github.com/Effect-TS/effect/commit/bb05fb83457355b1ca567228a9e041edfb6fd85d), [`712277f`](https://github.com/Effect-TS/effect/commit/712277f949052a24b46e4aa234063a6abf395c90), [`f269122`](https://github.com/Effect-TS/effect/commit/f269122508693b111142994dd48698ddc75f3d69), [`8f6006a`](https://github.com/Effect-TS/effect/commit/8f6006a610fb6d6c7b8d14209a7323338a8964ff), [`c45b559`](https://github.com/Effect-TS/effect/commit/c45b5592b5fd1189a5c932cfe05bd7d5f6d68508), [`430c846`](https://github.com/Effect-TS/effect/commit/430c846cbac05b187e3d24ac8dfee0cf22506f7c), [`7b03057`](https://github.com/Effect-TS/effect/commit/7b03057507d2dab5e6793beb9c578dedaaeb15fe), [`a9c94c8`](https://github.com/Effect-TS/effect/commit/a9c94c807755610831211a686d2fad849ab38eb4), [`107e6f0`](https://github.com/Effect-TS/effect/commit/107e6f0557a1e2d3b0dce25d62fa1e2601521752), [`c9175ae`](https://github.com/Effect-TS/effect/commit/c9175aef41cb1e3b689d0ac0a4f53d8107376b58), [`65c11b9`](https://github.com/Effect-TS/effect/commit/65c11b9266ec9447c31c26fe3ed35c73bd3b81fd), [`e386d2f`](https://github.com/Effect-TS/effect/commit/e386d2f1b3ab3ac2c14ee76de11f5963d32a3df4), [`9172efb`](https://github.com/Effect-TS/effect/commit/9172efba98bc6a82353e6ec2af61ac08f038ba64)]: + - @effect/platform@0.75.2 + - effect@3.12.8 + +## 0.54.1 + +### Patch Changes + +- Updated dependencies [[`8dff1d1`](https://github.com/Effect-TS/effect/commit/8dff1d1bff76cdba643cad7f0bf864300f08bc61)]: + - effect@3.12.7 + - @effect/platform@0.75.1 + +## 0.54.0 + +### Patch Changes + +- Updated dependencies [[`5e43ce5`](https://github.com/Effect-TS/effect/commit/5e43ce50bae116865906112e7f88d390739d778b), [`289c13b`](https://github.com/Effect-TS/effect/commit/289c13b38e8e35b214d46d385d05dead176c87cd), [`76eb7d0`](https://github.com/Effect-TS/effect/commit/76eb7d0fbce3c009c8f77e84c178cb15bbed9709), [`8b4e75d`](https://github.com/Effect-TS/effect/commit/8b4e75d35daea807c447ca760948a717aa66bb52), [`fc5e0f0`](https://github.com/Effect-TS/effect/commit/fc5e0f0d357a0051cfa01c1ede83ffdd3cb41ab1), [`004fd2b`](https://github.com/Effect-TS/effect/commit/004fd2bbd1459e64fb1b57f02eeb791ca5ea1ea5), [`b2a31be`](https://github.com/Effect-TS/effect/commit/b2a31be85c35d891351ce4f9a2cc93ece0c257f6), [`5514d05`](https://github.com/Effect-TS/effect/commit/5514d05b5cd586ff5868b8bd41c959e95e6c33cd), [`bf5f0ae`](https://github.com/Effect-TS/effect/commit/bf5f0ae9daa0170471678e22585e8ec14ce667bb), [`3b19bcf`](https://github.com/Effect-TS/effect/commit/3b19bcfd3aaadb6c9253428622df524537c8e626), [`b064b3b`](https://github.com/Effect-TS/effect/commit/b064b3b293615fd268cc5a5647d0981eb67750b8), [`eb264ed`](https://github.com/Effect-TS/effect/commit/eb264ed8a6e8c92a9dc7006f766c6ca2e5d29e03), [`289c13b`](https://github.com/Effect-TS/effect/commit/289c13b38e8e35b214d46d385d05dead176c87cd), [`f474678`](https://github.com/Effect-TS/effect/commit/f474678bf10b8f1c80e3dc096ddc7ecf20b2b23e), [`ee187d0`](https://github.com/Effect-TS/effect/commit/ee187d098007a402844c94d04f0cd8f07695377a)]: + - @effect/platform@0.75.0 + - effect@3.12.6 + +## 0.53.0 + +### Patch Changes + +- Updated dependencies [[`a8b0ddb`](https://github.com/Effect-TS/effect/commit/a8b0ddb84710054799fc8f57485b95d00093ada1), [`8653072`](https://github.com/Effect-TS/effect/commit/86530720d7a03e118d2c5a8bf5a997cee7e7f3d6), [`507d546`](https://github.com/Effect-TS/effect/commit/507d546bd49db31000425fb5da88c434e4291bea), [`a8b0ddb`](https://github.com/Effect-TS/effect/commit/a8b0ddb84710054799fc8f57485b95d00093ada1), [`8db239b`](https://github.com/Effect-TS/effect/commit/8db239b9c869a3707f6566b9d9dbdf53c4df03fc), [`84a0911`](https://github.com/Effect-TS/effect/commit/84a091181634c3a022c94234cec7764a3aeef1be), [`84a0911`](https://github.com/Effect-TS/effect/commit/84a091181634c3a022c94234cec7764a3aeef1be), [`3179a9f`](https://github.com/Effect-TS/effect/commit/3179a9f65d23369a6a9a1f80f7750566dd28df22), [`6cb9b76`](https://github.com/Effect-TS/effect/commit/6cb9b766396d0b2ed995cf26957359713efd202e), [`1fcbe55`](https://github.com/Effect-TS/effect/commit/1fcbe55345042d8468f6a98c84081bd00b6bcf5a), [`d9a63d9`](https://github.com/Effect-TS/effect/commit/d9a63d9d385653865954cac895065360d54cc56b), [`6cb9b76`](https://github.com/Effect-TS/effect/commit/6cb9b766396d0b2ed995cf26957359713efd202e)]: + - effect@3.12.5 + - @effect/platform@0.74.0 + +## 0.52.1 + +### Patch Changes + +- Updated dependencies [[`5b50ea4`](https://github.com/Effect-TS/effect/commit/5b50ea4a10cf9acd51f9624b2474d9d5ded74019), [`c170a68`](https://github.com/Effect-TS/effect/commit/c170a68b6266100774461fcd6c0e0fabb60112f2), [`a66c2eb`](https://github.com/Effect-TS/effect/commit/a66c2eb473245092cd41f04c2eb2b7b02cf53718), [`c9e5e1b`](https://github.com/Effect-TS/effect/commit/c9e5e1be17c0c84d3d4e2abc3c60215cdb56bbbe), [`7b3d58d`](https://github.com/Effect-TS/effect/commit/7b3d58d7aec2152ec282460871d3e9de45ed254d)]: + - effect@3.12.4 + - @effect/platform@0.73.1 + +## 0.52.0 + +### Patch Changes + +- Updated dependencies [[`d7dac48`](https://github.com/Effect-TS/effect/commit/d7dac48a477cdfeec509dbe9f33fce6a1b02b63d), [`c110032`](https://github.com/Effect-TS/effect/commit/c110032322450a8824ba38ae24335a538cd2ce9a), [`1d7fd2b`](https://github.com/Effect-TS/effect/commit/1d7fd2b7ee8eeecc912d27adf76ed897db236dc5), [`1d7fd2b`](https://github.com/Effect-TS/effect/commit/1d7fd2b7ee8eeecc912d27adf76ed897db236dc5), [`23ac740`](https://github.com/Effect-TS/effect/commit/23ac740c7dd4610b7d265c2071b88b0968419e9a), [`8cd7319`](https://github.com/Effect-TS/effect/commit/8cd7319b6568bfc7a30ca16c104d189e37eac3a0)]: + - effect@3.12.3 + - @effect/platform@0.73.0 + +## 0.51.2 + +### Patch Changes + +- Updated dependencies [[`734af82`](https://github.com/Effect-TS/effect/commit/734af82138e78b9c57a8355b1c6b80e80d38b222), [`b63c780`](https://github.com/Effect-TS/effect/commit/b63c78010893101520448ddda7019c487cf7eedd), [`212e784`](https://github.com/Effect-TS/effect/commit/212e78475f527147ec27c090bd13f789f55add7a), [`f852cb0`](https://github.com/Effect-TS/effect/commit/f852cb02040ea2f165e9b449615b8b1366add5d5), [`7276ae2`](https://github.com/Effect-TS/effect/commit/7276ae21062896adbb7508ac5b2dece95316322f), [`212e784`](https://github.com/Effect-TS/effect/commit/212e78475f527147ec27c090bd13f789f55add7a), [`212e784`](https://github.com/Effect-TS/effect/commit/212e78475f527147ec27c090bd13f789f55add7a), [`212e784`](https://github.com/Effect-TS/effect/commit/212e78475f527147ec27c090bd13f789f55add7a), [`c640d77`](https://github.com/Effect-TS/effect/commit/c640d77b33ad417876f4e8ffe8574ee6cbe5607f), [`0def088`](https://github.com/Effect-TS/effect/commit/0def0887cfdb6755729a64dfd52b3b9f46b0576c)]: + - effect@3.12.2 + - @effect/platform@0.72.2 + +## 0.51.1 + +### Patch Changes + +- Updated dependencies [[`302b57d`](https://github.com/Effect-TS/effect/commit/302b57d2cbf9b9ccc17450945aeebfb33cfe8d43), [`0988083`](https://github.com/Effect-TS/effect/commit/0988083d4594938590df5a287e5b27d38526dd07), [`8b46be6`](https://github.com/Effect-TS/effect/commit/8b46be6a3b8160362ab5ea9171c5e6932505125c), [`bfe8027`](https://github.com/Effect-TS/effect/commit/bfe802734b450a4b4ee069d1125dd37995db2bff), [`16dd657`](https://github.com/Effect-TS/effect/commit/16dd657033d8afac2ffea567b3c8bb27c9b249b6), [`39db211`](https://github.com/Effect-TS/effect/commit/39db211414e90c8db8fdad7dc8ce5b4661bcfaef)]: + - effect@3.12.1 + - @effect/platform@0.72.1 + +## 0.51.0 + +### Patch Changes + +- Updated dependencies [[`abb22a4`](https://github.com/Effect-TS/effect/commit/abb22a429b9c52c31e84856294f175d2064a9b4d), [`f369a89`](https://github.com/Effect-TS/effect/commit/f369a89e98bc682969803b9304adaf4557bb36c2), [`642376c`](https://github.com/Effect-TS/effect/commit/642376c63fd7d78754db991631a4d50a5dc79aa3), [`3d2b7a7`](https://github.com/Effect-TS/effect/commit/3d2b7a7e942a7157afae5b1cdbc6f3fef116428e), [`ef64c6f`](https://github.com/Effect-TS/effect/commit/ef64c6fec0d47da573c04230dde9ea729366d871), [`73f9c6f`](https://github.com/Effect-TS/effect/commit/73f9c6f2ff091512cf904cc54ab59965b86e87c8), [`17cb451`](https://github.com/Effect-TS/effect/commit/17cb4514590e8a86263f7aed009f24da8a237342), [`d801820`](https://github.com/Effect-TS/effect/commit/d80182060c2ee945d7e0e4728812abf9465a0d6a), [`e1eeb2d`](https://github.com/Effect-TS/effect/commit/e1eeb2d7064b3870041dab142f3057970699bbf1), [`c11f3a6`](https://github.com/Effect-TS/effect/commit/c11f3a60a05c3b5fc8e7ce90136728154dc505b0), [`618f7e0`](https://github.com/Effect-TS/effect/commit/618f7e092a1011e5090dca1e69b5e9285689654b), [`c0ba834`](https://github.com/Effect-TS/effect/commit/c0ba834d1995cf5a8b250e4780fd43f3e3881151), [`e1eeb2d`](https://github.com/Effect-TS/effect/commit/e1eeb2d7064b3870041dab142f3057970699bbf1)]: + - effect@3.12.0 + - @effect/platform@0.72.0 + +## 0.50.7 + +### Patch Changes + +- Updated dependencies [[`39457d4`](https://github.com/Effect-TS/effect/commit/39457d4897d9bc7df8af5c05d352866bbeae82eb), [`a475cc2`](https://github.com/Effect-TS/effect/commit/a475cc25fd7c9f26b27a8e98f8fbe43cc9e6ee3e), [`199214e`](https://github.com/Effect-TS/effect/commit/199214e21c616d8a0ccd7ed5f92e944e6c580193), [`b3c160d`](https://github.com/Effect-TS/effect/commit/b3c160d7a1fdfc2d3fb2440530f1ab80efc65133)]: + - effect@3.11.10 + - @effect/platform@0.71.7 + +## 0.50.6 + +### Patch Changes + +- Updated dependencies [[`1c08a0b`](https://github.com/Effect-TS/effect/commit/1c08a0b8505badcffb4d9cade5a746ea90c9557e), [`1ce703b`](https://github.com/Effect-TS/effect/commit/1ce703b041bbd7560c5c437c9b9be48f027937fd), [`1ce703b`](https://github.com/Effect-TS/effect/commit/1ce703b041bbd7560c5c437c9b9be48f027937fd)]: + - effect@3.11.9 + - @effect/platform@0.71.6 + +## 0.50.5 + +### Patch Changes + +- Updated dependencies [[`05d71f8`](https://github.com/Effect-TS/effect/commit/05d71f85622305705d8316817694a09762e60865), [`e66b920`](https://github.com/Effect-TS/effect/commit/e66b9205f25ab425d30640886eb3fb2c4715bc26)]: + - @effect/platform@0.71.5 + +## 0.50.4 + +### Patch Changes + +- Updated dependencies [[`909181a`](https://github.com/Effect-TS/effect/commit/909181a9ce9052a80432ccf52187e0723004bf7f), [`909181a`](https://github.com/Effect-TS/effect/commit/909181a9ce9052a80432ccf52187e0723004bf7f), [`1a6b52d`](https://github.com/Effect-TS/effect/commit/1a6b52dcf020d36e38a7bc90b648152cf5a8ccba)]: + - @effect/platform@0.71.4 + - effect@3.11.8 + +## 0.50.3 + +### Patch Changes + +- Updated dependencies [[`6984508`](https://github.com/Effect-TS/effect/commit/6984508c87f1bd91213b44c19b25ab5e2dcc1ce0), [`883639c`](https://github.com/Effect-TS/effect/commit/883639cc8ce47757f1cd39439391a8028c0812fe)]: + - @effect/platform@0.71.3 + +## 0.50.2 + +### Patch Changes + +- Updated dependencies [[`2408616`](https://github.com/Effect-TS/effect/commit/24086163b60b09cc6d0885bd565ef080dcbe866b), [`cec0b4d`](https://github.com/Effect-TS/effect/commit/cec0b4d152ef660be2ccdb0927255f2471436e6e), [`cec0b4d`](https://github.com/Effect-TS/effect/commit/cec0b4d152ef660be2ccdb0927255f2471436e6e), [`8d978c5`](https://github.com/Effect-TS/effect/commit/8d978c53f6fcc98d9d645ecba3e4b55d4297dd36), [`cec0b4d`](https://github.com/Effect-TS/effect/commit/cec0b4d152ef660be2ccdb0927255f2471436e6e), [`cec0b4d`](https://github.com/Effect-TS/effect/commit/cec0b4d152ef660be2ccdb0927255f2471436e6e)]: + - effect@3.11.7 + - @effect/platform@0.71.2 + +## 0.50.1 + +### Patch Changes + +- Updated dependencies [[`1d3df5b`](https://github.com/Effect-TS/effect/commit/1d3df5bc4324e88a392c348db35fd9d029c7b25e)]: + - @effect/platform@0.71.1 + +## 0.50.0 + +### Patch Changes + +- Updated dependencies [[`662d1ce`](https://github.com/Effect-TS/effect/commit/662d1ce6fb7da384a95888d5b2bb5605bdf3208d), [`c99a0f3`](https://github.com/Effect-TS/effect/commit/c99a0f376d049d3793ed33e146d9873b8a5e5b78), [`11fc401`](https://github.com/Effect-TS/effect/commit/11fc401f436f99bf4be95f56d50b0e4bdfe5edea), [`c99a0f3`](https://github.com/Effect-TS/effect/commit/c99a0f376d049d3793ed33e146d9873b8a5e5b78), [`31c62d8`](https://github.com/Effect-TS/effect/commit/31c62d83cbdcf9850a8b5331faa239601c60f78a)]: + - effect@3.11.6 + - @effect/platform@0.71.0 + +## 0.49.7 + +### Patch Changes + +- [#4105](https://github.com/Effect-TS/effect/pull/4105) [`619b6d9`](https://github.com/Effect-TS/effect/commit/619b6d925d872b4110322d4bf46b11c0fbc1cccf) Thanks @SandroMaglione! - Refactor of `Clipboard` module with `TypeId` and custom error + +- Updated dependencies [[`9f5a6f7`](https://github.com/Effect-TS/effect/commit/9f5a6f701bf7ba31adccd1f1bcfa8ab5614c9be8), [`22905cf`](https://github.com/Effect-TS/effect/commit/22905cf5addfb1ff3d2a6135c52036be958ae911), [`9f5a6f7`](https://github.com/Effect-TS/effect/commit/9f5a6f701bf7ba31adccd1f1bcfa8ab5614c9be8), [`1e59e4f`](https://github.com/Effect-TS/effect/commit/1e59e4fd778da18296812a2a32f36ca8ae50f60d), [`9f5a6f7`](https://github.com/Effect-TS/effect/commit/9f5a6f701bf7ba31adccd1f1bcfa8ab5614c9be8), [`8d914e5`](https://github.com/Effect-TS/effect/commit/8d914e504e7a22d0ea628e8af265ee450ff9530f), [`03bb00f`](https://github.com/Effect-TS/effect/commit/03bb00faa74f9e168a54a8cc0828a664fbb1ab05), [`9f5a6f7`](https://github.com/Effect-TS/effect/commit/9f5a6f701bf7ba31adccd1f1bcfa8ab5614c9be8), [`14e1149`](https://github.com/Effect-TS/effect/commit/14e1149f1af5a022f06eb8c2e4ba9fec17fe7426), [`9f5a6f7`](https://github.com/Effect-TS/effect/commit/9f5a6f701bf7ba31adccd1f1bcfa8ab5614c9be8), [`9f5a6f7`](https://github.com/Effect-TS/effect/commit/9f5a6f701bf7ba31adccd1f1bcfa8ab5614c9be8)]: + - effect@3.11.5 + - @effect/platform@0.70.7 + +## 0.49.6 + +### Patch Changes + +- Updated dependencies [[`9a5b8e3`](https://github.com/Effect-TS/effect/commit/9a5b8e36d184bd4967a88752cb6e755e1be263af)]: + - @effect/platform@0.70.6 + +## 0.49.5 + +### Patch Changes + +- Updated dependencies [[`415f4c9`](https://github.com/Effect-TS/effect/commit/415f4c98321868531727a83cbaad70164f5e4c40), [`518b258`](https://github.com/Effect-TS/effect/commit/518b258a8a67ecd332a9252c35cc060f8368dee2), [`6e323a3`](https://github.com/Effect-TS/effect/commit/6e323a36faaee46b328c8e3cf60a76b3aff9907f), [`518b258`](https://github.com/Effect-TS/effect/commit/518b258a8a67ecd332a9252c35cc060f8368dee2), [`6e323a3`](https://github.com/Effect-TS/effect/commit/6e323a36faaee46b328c8e3cf60a76b3aff9907f)]: + - @effect/platform@0.70.5 + - effect@3.11.4 + +## 0.49.4 + +### Patch Changes + +- Updated dependencies [[`90906f7`](https://github.com/Effect-TS/effect/commit/90906f7f154b12c7182e8f39e3c55ef3937db857), [`3862cd3`](https://github.com/Effect-TS/effect/commit/3862cd3c7f6a542ed65fb81255b3bd696ce2f567), [`3862cd3`](https://github.com/Effect-TS/effect/commit/3862cd3c7f6a542ed65fb81255b3bd696ce2f567), [`343b6aa`](https://github.com/Effect-TS/effect/commit/343b6aa6ac4a74276bfc7c63ccbf4a1d72bc1bed), [`afba339`](https://github.com/Effect-TS/effect/commit/afba339adc11dad56b5a3b7ca94487e58f34d613)]: + - effect@3.11.3 + - @effect/platform@0.70.4 + +## 0.49.3 + +### Patch Changes + +- Updated dependencies [[`7044730`](https://github.com/Effect-TS/effect/commit/70447306be1aeeb7d87c230b2a96ec87b993ede9)]: + - @effect/platform@0.70.3 + +## 0.49.2 + +### Patch Changes + +- Updated dependencies [[`01cee56`](https://github.com/Effect-TS/effect/commit/01cee560b58d94b24cc20e98083251b73e658b41), [`c2249ea`](https://github.com/Effect-TS/effect/commit/c2249ea13fd98ab7d9aa628787931356d8ec2860), [`1358aa5`](https://github.com/Effect-TS/effect/commit/1358aa5326eaa85ef13ee8d1fed0b4a4288ed3eb), [`1de3fe7`](https://github.com/Effect-TS/effect/commit/1de3fe7d1cbafd6391eaa38c2300b99e332cc2aa)]: + - effect@3.11.2 + - @effect/platform@0.70.2 + +## 0.49.1 + +### Patch Changes + +- Updated dependencies [[`dd8a2d8`](https://github.com/Effect-TS/effect/commit/dd8a2d8e80d33b16719fc69361eaedf0b59d4620), [`a71bfef`](https://github.com/Effect-TS/effect/commit/a71bfef46f5061bb2502a61a333638a987b62273)]: + - effect@3.11.1 + - @effect/platform@0.70.1 + +## 0.49.0 + +### Patch Changes + +- Updated dependencies [[`147434b`](https://github.com/Effect-TS/effect/commit/147434b03d5e1fd692dd9f126e5ab0910f3b76d3), [`6e69493`](https://github.com/Effect-TS/effect/commit/6e694930048bbaf98110f35f41566aeb9752d471), [`147434b`](https://github.com/Effect-TS/effect/commit/147434b03d5e1fd692dd9f126e5ab0910f3b76d3), [`5eff3f6`](https://github.com/Effect-TS/effect/commit/5eff3f6fa3aae7e86948a62cbfd63b8d6c3bdf92), [`d9fe79b`](https://github.com/Effect-TS/effect/commit/d9fe79bb5a3fe105d8e7a3bc2922a8ad936a5d10), [`251d189`](https://github.com/Effect-TS/effect/commit/251d189420bbba71990574e91098c499065f9a9b), [`5a259f3`](https://github.com/Effect-TS/effect/commit/5a259f3711b4369f55d885b568bdb21136155261), [`b4ce4ea`](https://github.com/Effect-TS/effect/commit/b4ce4ea7fd514a7e572f2dcd879c98f334981b0e), [`15fcc5a`](https://github.com/Effect-TS/effect/commit/15fcc5a0ea4bbf40ab48fa6a04fdda74f76f4c07), [`9bc9a47`](https://github.com/Effect-TS/effect/commit/9bc9a476800dc645903c888a68bb1d3baa3383c6), [`aadb8a4`](https://github.com/Effect-TS/effect/commit/aadb8a48d2cba197c06ec9996505510e48e4e5cb), [`1e2747c`](https://github.com/Effect-TS/effect/commit/1e2747c63a4820d1459cbbc88c71212983bd68bd), [`9264162`](https://github.com/Effect-TS/effect/commit/9264162a82783a651776fb7b87604564a63e7070), [`e0b9b09`](https://github.com/Effect-TS/effect/commit/e0b9b09e70c386b2da17d1f0a15b0511861c89e8), [`c36f3b9`](https://github.com/Effect-TS/effect/commit/c36f3b95df5ce9d71b66f22f26ce12eda8d3e848), [`672bde5`](https://github.com/Effect-TS/effect/commit/672bde5bec51c7d6f9862828e6a654cb2cb6f93d), [`aadb8a4`](https://github.com/Effect-TS/effect/commit/aadb8a48d2cba197c06ec9996505510e48e4e5cb)]: + - effect@3.11.0 + - @effect/platform@0.70.0 + +## 0.48.32 + +### Patch Changes + +- Updated dependencies [[`3069614`](https://github.com/Effect-TS/effect/commit/30696149271129fc618f6f2ccd1d8f2f6c0f9cd7), [`09a5e52`](https://github.com/Effect-TS/effect/commit/09a5e522fd9b221f05d85b1d1c8a740d4973c302)]: + - effect@3.10.20 + - @effect/platform@0.69.32 + +## 0.48.31 + +### Patch Changes + +- Updated dependencies [[`e6d4a37`](https://github.com/Effect-TS/effect/commit/e6d4a37c1d7e657b5ea44063a1cf586808228fe5)]: + - @effect/platform@0.69.31 + +## 0.48.30 + +### Patch Changes + +- Updated dependencies [[`270f199`](https://github.com/Effect-TS/effect/commit/270f199b31810fd643e4c22818698adcbdb5d396)]: + - @effect/platform@0.69.30 + +## 0.48.29 + +### Patch Changes + +- Updated dependencies [[`24cc35e`](https://github.com/Effect-TS/effect/commit/24cc35e26d6ed4a076470bc687ffd99cc50991b3)]: + - @effect/platform@0.69.29 + +## 0.48.28 + +### Patch Changes + +- Updated dependencies [[`edd72be`](https://github.com/Effect-TS/effect/commit/edd72be57b904d60c9cbffc2537901821a9da537), [`a3e2771`](https://github.com/Effect-TS/effect/commit/a3e277170a1f7cf61fd629acb60304c7e81d9498), [`944025b`](https://github.com/Effect-TS/effect/commit/944025bc5ce139f4a85846aa689bf30ec06a8ec1), [`54addee`](https://github.com/Effect-TS/effect/commit/54addee438a644bf010646c52042c7b89c5fc0a7), [`a9e00e4`](https://github.com/Effect-TS/effect/commit/a9e00e43f0b5dd22c1f9d5b78be6383daea09c20)]: + - @effect/platform@0.69.28 + - effect@3.10.19 + +## 0.48.27 + +### Patch Changes + +- Updated dependencies [[`af409cf`](https://github.com/Effect-TS/effect/commit/af409cf1d2ff973be11cc079ea373eaeedca25de), [`beaccae`](https://github.com/Effect-TS/effect/commit/beaccae2d15931e9fe475fb50a0b3638243fe3f7)]: + - effect@3.10.18 + - @effect/platform@0.69.27 + +## 0.48.26 + +### Patch Changes + +- Updated dependencies [[`c963886`](https://github.com/Effect-TS/effect/commit/c963886d5817986fcbd6bfa4ddf50aca8b6c8184), [`42c4ce6`](https://github.com/Effect-TS/effect/commit/42c4ce6f8d8c7d847e97757650a8ad9419a829d7)]: + - @effect/platform@0.69.26 + - effect@3.10.17 + +## 0.48.25 + +### Patch Changes + +- Updated dependencies [[`320557a`](https://github.com/Effect-TS/effect/commit/320557ab18d13c5e22fc7dc0d2a157eae461012f), [`4dca30c`](https://github.com/Effect-TS/effect/commit/4dca30cfcdafe4542e236489f71d6f171a5b4e38), [`1d99867`](https://github.com/Effect-TS/effect/commit/1d998671be3cd11043f232822e91dd8c98fccfa9), [`6dae414`](https://github.com/Effect-TS/effect/commit/6dae4147991a97ec14a99289bd25fadae7541e8d), [`6b0d737`](https://github.com/Effect-TS/effect/commit/6b0d737078bf63b97891e6bc47affc04b28f9cf7), [`d8356aa`](https://github.com/Effect-TS/effect/commit/d8356aad428a0c2290db52380220f81d9ec94232), [`7b93dd6`](https://github.com/Effect-TS/effect/commit/7b93dd622e2ab79c7072d79d0d9611e446202201)]: + - @effect/platform@0.69.25 + - effect@3.10.16 + +## 0.48.24 + +### Patch Changes + +- Updated dependencies [[`3cc6514`](https://github.com/Effect-TS/effect/commit/3cc6514d2dd64e010cb760cc29bfce98c349bb10)]: + - @effect/platform@0.69.24 + +## 0.48.23 + +### Patch Changes + +- [#3902](https://github.com/Effect-TS/effect/pull/3902) [`18bdbac`](https://github.com/Effect-TS/effect/commit/18bdbacfcd0817dc90f11ec5bc72dfe3c2c456c7) Thanks @SandroMaglione! - Added `Permissions` module to `@effect/platform-browser` + +- Updated dependencies [[`3aff4d3`](https://github.com/Effect-TS/effect/commit/3aff4d38837c213bb2987973dc4b98febb9f92d2)]: + - @effect/platform@0.69.23 + +## 0.48.22 + +### Patch Changes + +- Updated dependencies [[`8398b32`](https://github.com/Effect-TS/effect/commit/8398b3208242a88239d4449910b7baf923cfe3b6), [`72e55b7`](https://github.com/Effect-TS/effect/commit/72e55b7c610784fcebdbadc592c876e23e76a986)]: + - effect@3.10.15 + - @effect/platform@0.69.22 + +## 0.48.21 + +### Patch Changes + +- Updated dependencies [[`f983946`](https://github.com/Effect-TS/effect/commit/f9839467b4cad6e788297764ef9f9f0b9fd203f9), [`2d8a750`](https://github.com/Effect-TS/effect/commit/2d8a75081eb83a0a81f817fdf6f428369c5064ab)]: + - effect@3.10.14 + - @effect/platform@0.69.21 + +## 0.48.20 + +### Patch Changes + +- Updated dependencies [[`995bbdf`](https://github.com/Effect-TS/effect/commit/995bbdffea2e332f203cd5b474cd6a1c77dfa6ae)]: + - effect@3.10.13 + - @effect/platform@0.69.20 + +## 0.48.19 + +### Patch Changes + +- Updated dependencies [[`eb8c52d`](https://github.com/Effect-TS/effect/commit/eb8c52d8b4c5e067ebf0a81eb742f5822e6439b5)]: + - @effect/platform@0.69.19 + +## 0.48.18 + +### Patch Changes + +- [#3904](https://github.com/Effect-TS/effect/pull/3904) [`dd14efe`](https://github.com/Effect-TS/effect/commit/dd14efe0ace255f571273aae876adea96267d7e6) Thanks @tim-smart! - improve platform/Worker shutdown and logging + +- Updated dependencies [[`a0584ec`](https://github.com/Effect-TS/effect/commit/a0584ece92ed784bfb139e9c5a699f02d1e71c2d), [`dd14efe`](https://github.com/Effect-TS/effect/commit/dd14efe0ace255f571273aae876adea96267d7e6), [`dd14efe`](https://github.com/Effect-TS/effect/commit/dd14efe0ace255f571273aae876adea96267d7e6)]: + - @effect/platform@0.69.18 + - effect@3.10.12 + +## 0.48.17 + +### Patch Changes + +- Updated dependencies [[`5eef499`](https://github.com/Effect-TS/effect/commit/5eef4998b6ccb7a5404d9e4fef85e57fa35fbb8a), [`8240b1c`](https://github.com/Effect-TS/effect/commit/8240b1c10d45312fc863cb679b1a1e8441af0c1a), [`5eef499`](https://github.com/Effect-TS/effect/commit/5eef4998b6ccb7a5404d9e4fef85e57fa35fbb8a)]: + - effect@3.10.11 + - @effect/platform@0.69.17 + +## 0.48.16 + +### Patch Changes + +- Updated dependencies [[`cd720ae`](https://github.com/Effect-TS/effect/commit/cd720aedf7f2571edec0843d6a633e84e4832b28), [`cd720ae`](https://github.com/Effect-TS/effect/commit/cd720aedf7f2571edec0843d6a633e84e4832b28), [`cd720ae`](https://github.com/Effect-TS/effect/commit/cd720aedf7f2571edec0843d6a633e84e4832b28), [`b631f40`](https://github.com/Effect-TS/effect/commit/b631f40abbe649b2a089764585b5c39f6a695ac6), [`7d89650`](https://github.com/Effect-TS/effect/commit/7d8965036cd2ea435c8441ffec3345488baebf85)]: + - effect@3.10.10 + - @effect/platform@0.69.16 + +## 0.48.15 + +### Patch Changes + +- Updated dependencies [[`8a30e1d`](https://github.com/Effect-TS/effect/commit/8a30e1dfa3a7103bf5414fc6a7fca3088d8c8c00)]: + - @effect/platform@0.69.15 + +## 0.48.14 + +### Patch Changes + +- Updated dependencies [[`a123e80`](https://github.com/Effect-TS/effect/commit/a123e80f111a625428a5b5622b7f55ee1073566b), [`bd5fcd3`](https://github.com/Effect-TS/effect/commit/bd5fcd3e6b603b1e505af90d6a00627c8eca6d41), [`0289d3b`](https://github.com/Effect-TS/effect/commit/0289d3b6391031d00329365bab9791b355031fe3), [`7386b71`](https://github.com/Effect-TS/effect/commit/7386b710e5be570e17f468928a6ed19d549a3e12), [`07c493a`](https://github.com/Effect-TS/effect/commit/07c493a598e096c7810cd06def8cfa43493c46b1), [`257ab1b`](https://github.com/Effect-TS/effect/commit/257ab1b539fa6e930b7ae2583a188376372200d7), [`4211a23`](https://github.com/Effect-TS/effect/commit/4211a2355bb3af3f0e756e2aae9d293379f25662)]: + - effect@3.10.9 + - @effect/platform@0.69.14 + +## 0.48.13 + +### Patch Changes + +- Updated dependencies [[`68b5c9e`](https://github.com/Effect-TS/effect/commit/68b5c9e44f34192cef26e1cadda5e661a027df41), [`9c9928d`](https://github.com/Effect-TS/effect/commit/9c9928dfeacd9ac33dc37eb0ca3d7d8c39175ada), [`6306e66`](https://github.com/Effect-TS/effect/commit/6306e6656092b350d4ede5746da6f245ec9f7e07), [`361c7f3`](https://github.com/Effect-TS/effect/commit/361c7f39a2c10ede9324847c3d3ba192a6f9b20a)]: + - effect@3.10.8 + - @effect/platform@0.69.13 + +## 0.48.12 + +### Patch Changes + +- Updated dependencies [[`33f5b9f`](https://github.com/Effect-TS/effect/commit/33f5b9ffaebea4f1bd0e391b44c41fb6230e743a), [`50f0281`](https://github.com/Effect-TS/effect/commit/50f0281b0d2116726b8927a6217622d5f394f3e4)]: + - effect@3.10.7 + - @effect/platform@0.69.12 + +## 0.48.11 + +### Patch Changes + +- Updated dependencies [[`ce1c21f`](https://github.com/Effect-TS/effect/commit/ce1c21ffc11902ac9ab453a51904207859d38552), [`81ddd45`](https://github.com/Effect-TS/effect/commit/81ddd45fc074b98206fafab416d9a5a28b31e07a)]: + - effect@3.10.6 + - @effect/platform@0.69.11 + +## 0.48.10 + +### Patch Changes + +- Updated dependencies [[`3a6d757`](https://github.com/Effect-TS/effect/commit/3a6d757badeebe00d8ef4d67530d073c8264dcfa), [`59d813a`](https://github.com/Effect-TS/effect/commit/59d813aa4973d1115cfc70cc3667508335f49693)]: + - effect@3.10.5 + - @effect/platform@0.69.10 + +## 0.48.9 + +### Patch Changes + +- Updated dependencies [[`2367708`](https://github.com/Effect-TS/effect/commit/2367708be449f9526a2047e321302d7bfb16f18e)]: + - @effect/platform@0.69.9 + - effect@3.10.4 + +## 0.48.8 + +### Patch Changes + +- Updated dependencies [[`522f7c5`](https://github.com/Effect-TS/effect/commit/522f7c518a5acfb55ef96d6796869f002cc3eaf8)]: + - @effect/platform@0.69.8 + +## 0.48.7 + +### Patch Changes + +- Updated dependencies [[`690d6c5`](https://github.com/Effect-TS/effect/commit/690d6c54d2145adb0af545c447db7d4755bf3c6b), [`b9423d8`](https://github.com/Effect-TS/effect/commit/b9423d8bf8181a2389fdbce1e3c14ac6fe8d54f5), [`279fe3a`](https://github.com/Effect-TS/effect/commit/279fe3a7168fe84e520c2cc88ba189a15f03a2bc)]: + - @effect/platform@0.69.7 + - effect@3.10.3 + +## 0.48.6 + +### Patch Changes + +- Updated dependencies [[`714e119`](https://github.com/Effect-TS/effect/commit/714e11945e45e5a2554ee058e6c43f82a8e309cf), [`c1afd55`](https://github.com/Effect-TS/effect/commit/c1afd55c54e61f9c432823d21b3d016f79160a37), [`42cd72a`](https://github.com/Effect-TS/effect/commit/42cd72a44ca9593e4d81fbb50e8111625fd0fb81)]: + - effect@3.10.2 + - @effect/platform@0.69.6 + +## 0.48.5 + +### Patch Changes + +- Updated dependencies [[`9604d6b`](https://github.com/Effect-TS/effect/commit/9604d6b616435103dafea8b53637a9d1450b4750)]: + - effect@3.10.1 + - @effect/platform@0.69.5 + +## 0.48.4 + +### Patch Changes + +- Updated dependencies [[`c86b1d7`](https://github.com/Effect-TS/effect/commit/c86b1d7cd47b66df190ef9775a475467c1abdbd6)]: + - @effect/platform@0.69.4 + +## 0.48.3 + +### Patch Changes + +- Updated dependencies [[`d5fba63`](https://github.com/Effect-TS/effect/commit/d5fba6391e1005e374aa0238f13edfbd65848313), [`1eb2c30`](https://github.com/Effect-TS/effect/commit/1eb2c30ba064398db5790e376dedcfad55b7b005), [`02d413e`](https://github.com/Effect-TS/effect/commit/02d413e7b6bc1c64885969c37cc3e4e690c94d7d)]: + - @effect/platform@0.69.3 + +## 0.48.2 + +### Patch Changes + +- Updated dependencies [[`e7afc47`](https://github.com/Effect-TS/effect/commit/e7afc47ce83e381c3f4aed2b2974e3b3d86a2340)]: + - @effect/platform@0.69.2 + +## 0.48.1 + +### Patch Changes + +- Updated dependencies [[`7564f56`](https://github.com/Effect-TS/effect/commit/7564f56bb2844cf39d2b0d2d9e93cf9b2205e9a8), [`7564f56`](https://github.com/Effect-TS/effect/commit/7564f56bb2844cf39d2b0d2d9e93cf9b2205e9a8)]: + - @effect/platform@0.69.1 + +## 0.48.0 + +### Patch Changes + +- Updated dependencies [[`4a01828`](https://github.com/Effect-TS/effect/commit/4a01828b66d6213e9bbe18979c893b13f7bb29bf), [`6d9de6b`](https://github.com/Effect-TS/effect/commit/6d9de6b871c5c08e6509a4e830c3d74758faa198), [`4a01828`](https://github.com/Effect-TS/effect/commit/4a01828b66d6213e9bbe18979c893b13f7bb29bf), [`c79c4c1`](https://github.com/Effect-TS/effect/commit/c79c4c178390fe61ff6dda88c9e058862349343a), [`38d30f0`](https://github.com/Effect-TS/effect/commit/38d30f08b8da62f9c3e308b9250738cb8d17bdb5), [`5821ce3`](https://github.com/Effect-TS/effect/commit/5821ce3455b47d25e0a40cae6ce22af9db5fa556)]: + - effect@3.10.0 + - @effect/platform@0.69.0 + +## 0.47.6 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform@0.68.6 + +## 0.47.5 + +### Patch Changes + +- Updated dependencies [[`2036402`](https://github.com/Effect-TS/effect/commit/20364020b8b75a684791aa93d90626758023e9e9)]: + - @effect/platform@0.68.5 + +## 0.47.4 + +### Patch Changes + +- Updated dependencies [[`1b1ef29`](https://github.com/Effect-TS/effect/commit/1b1ef29ae302322f69dc938f9337aa97b4c63266)]: + - @effect/platform@0.68.4 + +## 0.47.3 + +### Patch Changes + +- Updated dependencies [[`61a99b2`](https://github.com/Effect-TS/effect/commit/61a99b2bf9d757870ef0c2ec9d4c877cdd364a3d), [`8c33087`](https://github.com/Effect-TS/effect/commit/8c330879425e80bed2f65e407cd59e991f0d7bec)]: + - effect@3.9.2 + - @effect/platform@0.68.3 + +## 0.47.2 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform@0.68.2 + +## 0.47.1 + +### Patch Changes + +- Updated dependencies [[`b75ac5d`](https://github.com/Effect-TS/effect/commit/b75ac5d0909115507bedc90f18f2d34deb217769)]: + - @effect/platform@0.68.1 + +## 0.47.0 + +### Minor Changes + +- [#3756](https://github.com/Effect-TS/effect/pull/3756) [`90ceeab`](https://github.com/Effect-TS/effect/commit/90ceeab3a04051b740af18c8af8bd73ee8ec6363) Thanks @tim-smart! - remove HttpClient.Service type + +- [#3756](https://github.com/Effect-TS/effect/pull/3756) [`90ceeab`](https://github.com/Effect-TS/effect/commit/90ceeab3a04051b740af18c8af8bd73ee8ec6363) Thanks @tim-smart! - constrain HttpClient success type to HttpClientResponse + +- [#3756](https://github.com/Effect-TS/effect/pull/3756) [`90ceeab`](https://github.com/Effect-TS/effect/commit/90ceeab3a04051b740af18c8af8bd73ee8ec6363) Thanks @tim-smart! - add HttpClient accessor apis + + These apis allow you to easily send requests without first accessing the `HttpClient` service. + + Below is an example of using the `get` accessor api to send a GET request: + + ```ts + import { FetchHttpClient, HttpClient } from "@effect/platform" + import { Effect } from "effect" + + const program = HttpClient.get( + "https://jsonplaceholder.typicode.com/posts/1" + ).pipe( + Effect.andThen((response) => response.json), + Effect.scoped, + Effect.provide(FetchHttpClient.layer) + ) + + Effect.runPromise(program) + /* + Output: + { + userId: 1, + id: 1, + title: 'sunt aut facere repellat provident occaecati excepturi optio reprehenderit', + body: 'quia et suscipit\n' + + 'suscipit recusandae consequuntur expedita et cum\n' + + 'reprehenderit molestiae ut ut quas totam\n' + + 'nostrum rerum est autem sunt rem eveniet architecto' + } + */ + ``` + +### Patch Changes + +- Updated dependencies [[`90ceeab`](https://github.com/Effect-TS/effect/commit/90ceeab3a04051b740af18c8af8bd73ee8ec6363), [`90ceeab`](https://github.com/Effect-TS/effect/commit/90ceeab3a04051b740af18c8af8bd73ee8ec6363), [`90ceeab`](https://github.com/Effect-TS/effect/commit/90ceeab3a04051b740af18c8af8bd73ee8ec6363)]: + - @effect/platform@0.68.0 + +## 0.46.1 + +### Patch Changes + +- Updated dependencies [[`3b2ad1d`](https://github.com/Effect-TS/effect/commit/3b2ad1d58a2e33dc1a72b7037396bd25ca1702a9)]: + - @effect/platform@0.67.1 + - effect@3.9.1 + +## 0.46.0 + +### Patch Changes + +- Updated dependencies [[`ff3d1aa`](https://github.com/Effect-TS/effect/commit/ff3d1aab290b4d1173b2dfc7e4c76abb4babdc16), [`0ba66f2`](https://github.com/Effect-TS/effect/commit/0ba66f2451641fd6990e02ec1ed01c014db9dab0), [`bf77f51`](https://github.com/Effect-TS/effect/commit/bf77f51b323c383224ebf08adf77a7a6e8c9b3cd), [`016f9ad`](https://github.com/Effect-TS/effect/commit/016f9ad931a4b3d09a34e5caf13d87c5b8e9c984), [`0779681`](https://github.com/Effect-TS/effect/commit/07796813f07de035719728733096ba64ce333469), [`534129f`](https://github.com/Effect-TS/effect/commit/534129f8113ce1a8ec50828083e16da9c86326c6), [`d75140c`](https://github.com/Effect-TS/effect/commit/d75140c7a664ceda43142d999f4ff8dcd36d6dda), [`be0451c`](https://github.com/Effect-TS/effect/commit/be0451c149b6618af79cb839cdf04af2db1efb03), [`9237ac6`](https://github.com/Effect-TS/effect/commit/9237ac69bc07de5b3b60076a0ad2921c21de7457), [`be0451c`](https://github.com/Effect-TS/effect/commit/be0451c149b6618af79cb839cdf04af2db1efb03), [`5b36494`](https://github.com/Effect-TS/effect/commit/5b364942e9a9003fdb8217324f8a2d8369c969da), [`c716adb`](https://github.com/Effect-TS/effect/commit/c716adb250ebbea1d1048d818ef7fed4f621d186), [`4986391`](https://github.com/Effect-TS/effect/commit/49863919cd8628c962a712fb1df30d2983820933), [`d75140c`](https://github.com/Effect-TS/effect/commit/d75140c7a664ceda43142d999f4ff8dcd36d6dda), [`d1387ae`](https://github.com/Effect-TS/effect/commit/d1387aebd1ff01bbebde26be46d488956e4daef6)]: + - effect@3.9.0 + - @effect/platform@0.67.0 + +## 0.45.3 + +### Patch Changes + +- Updated dependencies [[`88e85db`](https://github.com/Effect-TS/effect/commit/88e85db34bd402526e27a323e950d053fa34d232), [`f40da15`](https://github.com/Effect-TS/effect/commit/f40da15fbeb7c491840b8f409d47de79720891c3), [`8e94585`](https://github.com/Effect-TS/effect/commit/8e94585abe62753bf3af28bfae77926a7c570ac3), [`83887ca`](https://github.com/Effect-TS/effect/commit/83887ca1b1793916913d8550a4db4450cd14a044), [`5266b6c`](https://github.com/Effect-TS/effect/commit/5266b6cd86d76c3886da041c8829bca04b1a3110), [`cdead5c`](https://github.com/Effect-TS/effect/commit/cdead5c9cfd54dc6c4f215d9732f654c4a12e991), [`f40da15`](https://github.com/Effect-TS/effect/commit/f40da15fbeb7c491840b8f409d47de79720891c3), [`766a8af`](https://github.com/Effect-TS/effect/commit/766a8af307b414aca3648d91c4eab7493a5ec862)]: + - effect@3.8.5 + - @effect/platform@0.66.3 + +## 0.45.2 + +### Patch Changes + +- Updated dependencies [[`fd83d0e`](https://github.com/Effect-TS/effect/commit/fd83d0e548feff9ea2d53d370a0b626c4a1d940e), [`4509656`](https://github.com/Effect-TS/effect/commit/45096569d50262275ee984f44c456f5c83b62683), [`090e41c`](https://github.com/Effect-TS/effect/commit/090e41c636d720b1c7d89684a739855765ed4382)]: + - @effect/platform@0.66.2 + - effect@3.8.4 + +## 0.45.1 + +### Patch Changes + +- Updated dependencies [[`3812788`](https://github.com/Effect-TS/effect/commit/3812788d79caaab8f559a62fd443018a04ac5647)]: + - @effect/platform@0.66.1 + +## 0.45.0 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform@0.66.0 + +## 0.44.5 + +### Patch Changes + +- Updated dependencies [[`321b201`](https://github.com/Effect-TS/effect/commit/321b201adcb6bbbeb806b3467dd0b4cf063ccda8), [`bb5ec6b`](https://github.com/Effect-TS/effect/commit/bb5ec6b4b6a6f537394596c5a596faf52cb2aef4)]: + - @effect/platform@0.65.5 + - effect@3.8.3 + +## 0.44.4 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform@0.65.4 + +## 0.44.3 + +### Patch Changes + +- Updated dependencies [[`f0d8ef1`](https://github.com/Effect-TS/effect/commit/f0d8ef1ce97ec2a87b09b3e24150cfeab85d6e2f)]: + - effect@3.8.2 + - @effect/platform@0.65.3 + +## 0.44.2 + +### Patch Changes + +- Updated dependencies [[`10bf621`](https://github.com/Effect-TS/effect/commit/10bf6213f36d8ddb00f058a4609b85220f3d8334), [`ae36fa6`](https://github.com/Effect-TS/effect/commit/ae36fa68f754eeab9a54b6dc0f8b44db513aa2b6)]: + - effect@3.8.1 + - @effect/platform@0.65.2 + +## 0.44.1 + +### Patch Changes + +- Updated dependencies [[`e44c5f2`](https://github.com/Effect-TS/effect/commit/e44c5f228215738fe4e75023c7461bf9521249cb)]: + - @effect/platform@0.65.1 + +## 0.44.0 + +### Minor Changes + +- [#3541](https://github.com/Effect-TS/effect/pull/3541) [`7041393`](https://github.com/Effect-TS/effect/commit/7041393cff132e96566d3f36da0483a6ff6195e4) Thanks @tim-smart! - refactor /platform HttpClient + + #### HttpClient.fetch removed + + The `HttpClient.fetch` client implementation has been removed. Instead, you can + access a `HttpClient` using the corresponding `Context.Tag`. + + ```ts + import { FetchHttpClient, HttpClient } from "@effect/platform" + import { Effect } from "effect" + + Effect.gen(function* () { + const client = yield* HttpClient.HttpClient + + // make a get request + yield* client.get("https://jsonplaceholder.typicode.com/todos/1") + }).pipe( + Effect.scoped, + // the fetch client has been moved to the `FetchHttpClient` module + Effect.provide(FetchHttpClient.layer) + ) + ``` + + #### `HttpClient` interface now uses methods + + Instead of being a function that returns the response, the `HttpClient` + interface now uses methods to make requests. + + Some shorthand methods have been added to the `HttpClient` interface to make + less complex requests easier. + + ```ts + import { + FetchHttpClient, + HttpClient, + HttpClientRequest + } from "@effect/platform" + import { Effect } from "effect" + + Effect.gen(function* () { + const client = yield* HttpClient.HttpClient + + // make a get request + yield* client.get("https://jsonplaceholder.typicode.com/todos/1") + // make a post request + yield* client.post("https://jsonplaceholder.typicode.com/todos") + + // execute a request instance + yield* client.execute( + HttpClientRequest.get("https://jsonplaceholder.typicode.com/todos/1") + ) + }) + ``` + + #### Scoped `HttpClientResponse` helpers removed + + The `HttpClientResponse` helpers that also supplied the `Scope` have been removed. + + Instead, you can use the `HttpClientResponse` methods directly, and explicitly + add a `Effect.scoped` to the pipeline. + + ```ts + import { FetchHttpClient, HttpClient } from "@effect/platform" + import { Effect } from "effect" + + Effect.gen(function* () { + const client = yield* HttpClient.HttpClient + + yield* client.get("https://jsonplaceholder.typicode.com/todos/1").pipe( + Effect.flatMap((response) => response.json), + Effect.scoped // supply the `Scope` + ) + }) + ``` + + #### Some apis have been renamed + + Including the `HttpClientRequest` body apis, which is to make them more + discoverable. + +### Patch Changes + +- Updated dependencies [[`fcfa6ee`](https://github.com/Effect-TS/effect/commit/fcfa6ee30ffd07d998bf22799357bf58580a116f), [`bb9931b`](https://github.com/Effect-TS/effect/commit/bb9931b62e249a3b801f2cb9d097aec0c8511af7), [`5798f76`](https://github.com/Effect-TS/effect/commit/5798f7619529de33e5ba06f551806f68fedc19db), [`5f0bfa1`](https://github.com/Effect-TS/effect/commit/5f0bfa17205398d4e4818bfbcf9e1b505b3b1fc5), [`812a4e8`](https://github.com/Effect-TS/effect/commit/812a4e86e2d1aa23b477ef5829aa0e5c07784936), [`6a128f6`](https://github.com/Effect-TS/effect/commit/6a128f63f9b41fec2db70790b3bbb96cb9afa1ab), [`273565e`](https://github.com/Effect-TS/effect/commit/273565e7901639e8d0541930ab715aea9c80fbaa), [`adf7d7a`](https://github.com/Effect-TS/effect/commit/adf7d7a7dfce3a7021e9f3b0d847dc85be89d754), [`569a801`](https://github.com/Effect-TS/effect/commit/569a8017ef0a0bc203e4312867cbdd37b0effbd7), [`aa1fa53`](https://github.com/Effect-TS/effect/commit/aa1fa5301e886b9657c8eb0d38cb87cef92a8305), [`02f6b06`](https://github.com/Effect-TS/effect/commit/02f6b0660e12bee1069532a9cc18d3ab855257be), [`12b893e`](https://github.com/Effect-TS/effect/commit/12b893e63cc6dfada4aca7773b4783940e2edf25), [`bbad27e`](https://github.com/Effect-TS/effect/commit/bbad27ec0a90860593f759405caa877e7f4a655f), [`7041393`](https://github.com/Effect-TS/effect/commit/7041393cff132e96566d3f36da0483a6ff6195e4), [`e0d21a5`](https://github.com/Effect-TS/effect/commit/e0d21a54c8323728fbb75a32f4820a9996257809), [`adf7d7a`](https://github.com/Effect-TS/effect/commit/adf7d7a7dfce3a7021e9f3b0d847dc85be89d754), [`007289a`](https://github.com/Effect-TS/effect/commit/007289a52d5877f8e90e2dacf38171ff9bf603fd), [`42a8f99`](https://github.com/Effect-TS/effect/commit/42a8f99740eefdaf2c4544d2c345313f97547a36), [`eebfd29`](https://github.com/Effect-TS/effect/commit/eebfd29633fd5d38b505c5c0842036f61f05e913), [`040703d`](https://github.com/Effect-TS/effect/commit/040703d0e100cd5511e52d812c15492414262b5e)]: + - effect@3.8.0 + - @effect/platform@0.65.0 + +## 0.43.1 + +### Patch Changes + +- Updated dependencies [[`35a0f81`](https://github.com/Effect-TS/effect/commit/35a0f813141652d696461cd5d19fd146adaf85be), [`8261c5a`](https://github.com/Effect-TS/effect/commit/8261c5ae6fe86872292ec1fc1a58ab9cea2f5f51)]: + - effect@3.7.3 + - @effect/platform@0.64.1 + +## 0.43.0 + +### Patch Changes + +- Updated dependencies [[`90ac8f6`](https://github.com/Effect-TS/effect/commit/90ac8f6f6053a2e4498f8b0cc56fe12777d02e1a), [`90ac8f6`](https://github.com/Effect-TS/effect/commit/90ac8f6f6053a2e4498f8b0cc56fe12777d02e1a), [`3791e24`](https://github.com/Effect-TS/effect/commit/3791e241636b1dfe924a56f380ebc9a7ff0827a9), [`3791e24`](https://github.com/Effect-TS/effect/commit/3791e241636b1dfe924a56f380ebc9a7ff0827a9)]: + - @effect/platform@0.64.0 + +## 0.42.3 + +### Patch Changes + +- Updated dependencies [[`4a701c4`](https://github.com/Effect-TS/effect/commit/4a701c406da032563fedae459536c00ae5cfe3c7)]: + - @effect/platform@0.63.3 + +## 0.42.2 + +### Patch Changes + +- Updated dependencies [[`8a601d7`](https://github.com/Effect-TS/effect/commit/8a601d7a1f8ffe52ac9e6d67e9282a1495fe59c9), [`353ba19`](https://github.com/Effect-TS/effect/commit/353ba19f9b2b9e959f0a00d058c6d40a4bc02db7)]: + - effect@3.7.2 + - @effect/platform@0.63.2 + +## 0.42.1 + +### Patch Changes + +- Updated dependencies [[`79859e7`](https://github.com/Effect-TS/effect/commit/79859e71040d8edf1868b8530b90c650f4321eff), [`f6a469c`](https://github.com/Effect-TS/effect/commit/f6a469c190b9f00eee5ea0cd4d5912a0ef8b46f5), [`dcb9ec0`](https://github.com/Effect-TS/effect/commit/dcb9ec0db443894dd204d87450f779c44b9ad7f1), [`79aa6b1`](https://github.com/Effect-TS/effect/commit/79aa6b136e1f29b36f34e88cb2ff162bff2bb4ed)]: + - effect@3.7.1 + - @effect/platform@0.63.1 + +## 0.42.0 + +### Patch Changes + +- Updated dependencies [[`db89601`](https://github.com/Effect-TS/effect/commit/db89601ee9c1050c4e762b7bd7ec65a6a2799dfe), [`2f456cc`](https://github.com/Effect-TS/effect/commit/2f456cce5012b9fcb6b4e039190d527813b75b92), [`8745e41`](https://github.com/Effect-TS/effect/commit/8745e41ed96e3765dc6048efc2a9afbe05c8a1e9), [`e557838`](https://github.com/Effect-TS/effect/commit/e55783886b046d3c5f33447f455f9ccf2fa75922), [`d6e7e40`](https://github.com/Effect-TS/effect/commit/d6e7e40b1e2ad0c59aa02f07344d28601b14ebdc), [`8356321`](https://github.com/Effect-TS/effect/commit/8356321598da04bd77c1001f45a4e447bec5591d), [`192f2eb`](https://github.com/Effect-TS/effect/commit/192f2ebb2c4ddbf4bfd8baedd32140b2376868f4), [`718cb70`](https://github.com/Effect-TS/effect/commit/718cb70038629a6d58d02e407760e341f7c94474), [`e9d0310`](https://github.com/Effect-TS/effect/commit/e9d03107acbf204d9304f3e8aea0816b7d3c7dfb), [`6bf28f7`](https://github.com/Effect-TS/effect/commit/6bf28f7e3b1e5e0608ff567205fea0581d11666f)]: + - effect@3.7.0 + - @effect/platform@0.63.0 + +## 0.41.5 + +### Patch Changes + +- Updated dependencies [[`e809286`](https://github.com/Effect-TS/effect/commit/e8092865900608c4df7a6b7991b1c13cc1e4ca2d)]: + - effect@3.6.8 + - @effect/platform@0.62.5 + +## 0.41.4 + +### Patch Changes + +- [#3506](https://github.com/Effect-TS/effect/pull/3506) [`e7a65e3`](https://github.com/Effect-TS/effect/commit/e7a65e3c6a08636bbfce3d3af3098bf28474364d) Thanks @tim-smart! - use Logger.pretty for runMain, and support dual usage + +- Updated dependencies [[`e7a65e3`](https://github.com/Effect-TS/effect/commit/e7a65e3c6a08636bbfce3d3af3098bf28474364d), [`50ec889`](https://github.com/Effect-TS/effect/commit/50ec8897a49b7d1fe84f63107f89d543c52f3dfc)]: + - @effect/platform@0.62.4 + - effect@3.6.7 + +## 0.41.3 + +### Patch Changes + +- Updated dependencies [[`f960bf4`](https://github.com/Effect-TS/effect/commit/f960bf45239e9badac6e0ad3a602f4174cd7bbdf), [`46a575f`](https://github.com/Effect-TS/effect/commit/46a575f48a05457b782fb21f7827d338c9b59320)]: + - effect@3.6.6 + - @effect/platform@0.62.3 + +## 0.41.2 + +### Patch Changes + +- Updated dependencies [[`413994c`](https://github.com/Effect-TS/effect/commit/413994c9792f16d9d57cca3ae6eb254bf93bd261), [`14a47a8`](https://github.com/Effect-TS/effect/commit/14a47a8c1f3cff2186b8fe7a919a1d773888fb5b), [`0c09841`](https://github.com/Effect-TS/effect/commit/0c0984173be3d58f050b300a1a8aa89d76ba49ae)]: + - @effect/platform@0.62.2 + - effect@3.6.5 + +## 0.41.1 + +### Patch Changes + +- Updated dependencies [[`9efe0e5`](https://github.com/Effect-TS/effect/commit/9efe0e5b57ac557399be620822c21cc6e9add285)]: + - @effect/platform@0.62.1 + +## 0.41.0 + +### Patch Changes + +- [#3450](https://github.com/Effect-TS/effect/pull/3450) [`0e42a8f`](https://github.com/Effect-TS/effect/commit/0e42a8f045ecb1fd3d080edf3d49fef16a9b0ca1) Thanks @tim-smart! - update dependencies + +- Updated dependencies [[`8295281`](https://github.com/Effect-TS/effect/commit/8295281ae9bd7441e680402540bf3c8682ec417b), [`c940df6`](https://github.com/Effect-TS/effect/commit/c940df63800bf3c4396d91cf28ec34938642fd2c), [`00b6c6d`](https://github.com/Effect-TS/effect/commit/00b6c6d4001f5de728b7d990a1b14560b4961a63), [`f8d95a6`](https://github.com/Effect-TS/effect/commit/f8d95a61ad0762147933c5c32bb6d7237e18eef4)]: + - effect@3.6.4 + - @effect/platform@0.62.0 + +## 0.40.8 + +### Patch Changes + +- Updated dependencies [[`04adcac`](https://github.com/Effect-TS/effect/commit/04adcace913e6fc483df266874a68005e9e04ccf)]: + - effect@3.6.3 + - @effect/platform@0.61.8 + +## 0.40.7 + +### Patch Changes + +- Updated dependencies [[`17245a4`](https://github.com/Effect-TS/effect/commit/17245a4e783c19dee51529600b3b40f164fa59bc), [`630d40e`](https://github.com/Effect-TS/effect/commit/630d40eaa7eb4d2f8b6705b16d4f426bc28a7d09), [`630d40e`](https://github.com/Effect-TS/effect/commit/630d40eaa7eb4d2f8b6705b16d4f426bc28a7d09)]: + - @effect/platform@0.61.7 + +## 0.40.6 + +### Patch Changes + +- Updated dependencies [[`d829b57`](https://github.com/Effect-TS/effect/commit/d829b576357f2e3b203ab7e107a1492de903a106), [`fd4b2f6`](https://github.com/Effect-TS/effect/commit/fd4b2f6516b325740dde615f1cf0229edf13ca0c)]: + - @effect/platform@0.61.6 + - effect@3.6.2 + +## 0.40.5 + +### Patch Changes + +- Updated dependencies [[`056b710`](https://github.com/Effect-TS/effect/commit/056b7108978e70612176c23991916f678d947f38)]: + - @effect/platform@0.61.5 + +## 0.40.4 + +### Patch Changes + +- Updated dependencies [[`e7cb109`](https://github.com/Effect-TS/effect/commit/e7cb109d0754207024a64d55b6bd2a674dd8ed7d)]: + - @effect/platform@0.61.4 + +## 0.40.3 + +### Patch Changes + +- Updated dependencies [[`510a34d`](https://github.com/Effect-TS/effect/commit/510a34d4cc5d2f51347a53847f6c7db84d2b17c6), [`fb9f786`](https://github.com/Effect-TS/effect/commit/fb9f7867f0c895e63f9ef23e8d0941248c42179d), [`45dbb9f`](https://github.com/Effect-TS/effect/commit/45dbb9ffeaf93d9e4df99d0cd4920e41ba9a3978)]: + - effect@3.6.1 + - @effect/platform@0.61.3 + +## 0.40.2 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform@0.61.2 + +## 0.40.1 + +### Patch Changes + +- Updated dependencies [[`11223bf`](https://github.com/Effect-TS/effect/commit/11223bf9cbf5b822e0bf9a9fb2b35b2ad88af692)]: + - @effect/platform@0.61.1 + +## 0.40.0 + +### Patch Changes + +- Updated dependencies [[`1e0fe80`](https://github.com/Effect-TS/effect/commit/1e0fe802b36c257971296617473ce0abe730e8dc), [`8135294`](https://github.com/Effect-TS/effect/commit/8135294b591ea94fde7e6f94a504608f0e630520), [`cd255a4`](https://github.com/Effect-TS/effect/commit/cd255a48872d8fb924cf713ef73f0883a9cc6987), [`3845646`](https://github.com/Effect-TS/effect/commit/3845646828e98f3c7cda1217f6cfe5f642ac0603), [`2d09078`](https://github.com/Effect-TS/effect/commit/2d09078c5948b37fc2f79ef858fe4ca3e4814085), [`4bce5a0`](https://github.com/Effect-TS/effect/commit/4bce5a0274203550ccf117d830721891b0a3d182), [`4ddbff0`](https://github.com/Effect-TS/effect/commit/4ddbff0bb4e3ffddfeb509c59835b83245fb975e), [`e74cc38`](https://github.com/Effect-TS/effect/commit/e74cc38cb420a320c4d7ef98180f19d452a8b316), [`bb069b4`](https://github.com/Effect-TS/effect/commit/bb069b49ef291c532a02c1e8e74271f6d1bb32ec), [`cd255a4`](https://github.com/Effect-TS/effect/commit/cd255a48872d8fb924cf713ef73f0883a9cc6987), [`7d02174`](https://github.com/Effect-TS/effect/commit/7d02174af3bcbf054e5cdddb821c91d0f47e8285)]: + - effect@3.6.0 + - @effect/platform@0.61.0 + +## 0.39.3 + +### Patch Changes + +- Updated dependencies [[`6359644`](https://github.com/Effect-TS/effect/commit/635964446323cf55d4060559337e710e4a24496e), [`7f41e42`](https://github.com/Effect-TS/effect/commit/7f41e428830bf3043b8be0d28dcd235d5747c942), [`f566fd1`](https://github.com/Effect-TS/effect/commit/f566fd1d7eea531a0d981dd24037f14a603a1273)]: + - effect@3.5.9 + - @effect/platform@0.60.3 + +## 0.39.2 + +### Patch Changes + +- [#3352](https://github.com/Effect-TS/effect/pull/3352) [`1dbd0a1`](https://github.com/Effect-TS/effect/commit/1dbd0a17120743f2b28dd374936b650c7d18bf13) Thanks @tim-smart! - make access to `self` lazy in platform-browser WorkerRunner + +- Updated dependencies [[`eb4d014`](https://github.com/Effect-TS/effect/commit/eb4d014c559e1b4c95b3fb9295fe77593c17ed7a), [`1ba640c`](https://github.com/Effect-TS/effect/commit/1ba640c702f187a866023bf043c26e25cce941ef), [`c8c71bd`](https://github.com/Effect-TS/effect/commit/c8c71bd20eb87d23133dac6156b83bb08941597c), [`a26ce58`](https://github.com/Effect-TS/effect/commit/a26ce581ca7d407e1e81439b58c8045b3fa65231), [`fc20f73`](https://github.com/Effect-TS/effect/commit/fc20f73c69e577981cb64714de2adc97e1004dae)]: + - @effect/platform@0.60.2 + - effect@3.5.8 + +## 0.39.1 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform@0.60.1 + +## 0.39.0 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform@0.60.0 + +## 0.38.4 + +### Patch Changes + +- [#3310](https://github.com/Effect-TS/effect/pull/3310) [`99bddcf`](https://github.com/Effect-TS/effect/commit/99bddcfb3d6eab4d489d055404e26ad81afe52fc) Thanks @fubhy! - Added additional pure annotations to improve tree-shakeability + +- Updated dependencies [[`3afcc93`](https://github.com/Effect-TS/effect/commit/3afcc93413a3d910beb69e4ce9ae120e4adaffd5), [`99bddcf`](https://github.com/Effect-TS/effect/commit/99bddcfb3d6eab4d489d055404e26ad81afe52fc)]: + - effect@3.5.7 + - @effect/platform@0.59.3 + +## 0.38.3 + +### Patch Changes + +- Updated dependencies [[`cc327a1`](https://github.com/Effect-TS/effect/commit/cc327a1bccd22a4ee27ec7e58b53205e93b23e2c), [`4bfe4fb`](https://github.com/Effect-TS/effect/commit/4bfe4fb5c82f597c9beea9baa92e772593598b60), [`2b14d18`](https://github.com/Effect-TS/effect/commit/2b14d181462cad8359da4fa6bc6dfda0f742c398)]: + - effect@3.5.6 + - @effect/platform@0.59.2 + +## 0.38.2 + +### Patch Changes + +- Updated dependencies [[`a9d7800`](https://github.com/Effect-TS/effect/commit/a9d7800f6a253192b653d77778b0674f39b1ca39), [`fcecff7`](https://github.com/Effect-TS/effect/commit/fcecff7f7e12b295a252f124861b801c73072151), [`adbf753`](https://github.com/Effect-TS/effect/commit/adbf75340a9db15dc5cadc66e911a8978a195a65), [`adbf753`](https://github.com/Effect-TS/effect/commit/adbf75340a9db15dc5cadc66e911a8978a195a65)]: + - effect@3.5.5 + - @effect/platform@0.59.1 + +## 0.38.1 + +### Patch Changes + +- [#3263](https://github.com/Effect-TS/effect/pull/3263) [`1d51d1b`](https://github.com/Effect-TS/effect/commit/1d51d1b627cd277a1defea81b4b58fd81bf76f32) Thanks @tim-smart! - allow creating browser WorkerRunner from a MessagePort + +## 0.38.0 + +### Minor Changes + +- [#3260](https://github.com/Effect-TS/effect/pull/3260) [`53c0db0`](https://github.com/Effect-TS/effect/commit/53c0db06872d5b5edea2a706e83249908385325c) Thanks @tim-smart! - replace /platform RefailError with use of the "cause" property + +- [#3255](https://github.com/Effect-TS/effect/pull/3255) [`ada68b3`](https://github.com/Effect-TS/effect/commit/ada68b3e61c67907c2a281c024c84d818186ca4c) Thanks @tim-smart! - refactor & simplify /platform backing workers + + Improves worker performance by 2x + +### Patch Changes + +- Updated dependencies [[`53c0db0`](https://github.com/Effect-TS/effect/commit/53c0db06872d5b5edea2a706e83249908385325c), [`ed0dde4`](https://github.com/Effect-TS/effect/commit/ed0dde4888e6f1a97ad5bba06b755d26a6a1c52e), [`ada68b3`](https://github.com/Effect-TS/effect/commit/ada68b3e61c67907c2a281c024c84d818186ca4c), [`ca775ce`](https://github.com/Effect-TS/effect/commit/ca775cec53baebc1a43d9b8852a3ac6726178498), [`5be9cc0`](https://github.com/Effect-TS/effect/commit/5be9cc044025a9541b9b7acefa2d3fc05fa1301b), [`203658f`](https://github.com/Effect-TS/effect/commit/203658f8001c132b25764ab70344b171683b554c), [`eb1c4d4`](https://github.com/Effect-TS/effect/commit/eb1c4d44e54b9d8d201a366d1ff94face2a6dcd3)]: + - @effect/platform@0.59.0 + - effect@3.5.4 + +## 0.37.27 + +### Patch Changes + +- [#3241](https://github.com/Effect-TS/effect/pull/3241) [`a1db40a`](https://github.com/Effect-TS/effect/commit/a1db40a650ab842e778654f0d88e80f2ef4fd6f3) Thanks @tim-smart! - ensure interrupts are handled in WorkerRunner + +- Updated dependencies [[`edb0da3`](https://github.com/Effect-TS/effect/commit/edb0da383746d760f35d8582f5fb0cc0eeca9217), [`a1db40a`](https://github.com/Effect-TS/effect/commit/a1db40a650ab842e778654f0d88e80f2ef4fd6f3), [`c8d3fb0`](https://github.com/Effect-TS/effect/commit/c8d3fb0fe23585f6efb724af51fbab3ba1ad6e83), [`dabd028`](https://github.com/Effect-TS/effect/commit/dabd028decf9b7983ca16ebe0f48c05c11a84b68), [`786b2ab`](https://github.com/Effect-TS/effect/commit/786b2ab29d525c877bb84035dac9e2d6499339d1), [`fc57354`](https://github.com/Effect-TS/effect/commit/fc573547d41667016fce05eaee75960fcc6dce4d)]: + - effect@3.5.3 + - @effect/platform@0.58.27 + +## 0.37.26 + +### Patch Changes + +- Updated dependencies [[`639208e`](https://github.com/Effect-TS/effect/commit/639208eeb8a44622994f832bc2d45d06ab636bc8), [`6684b4c`](https://github.com/Effect-TS/effect/commit/6684b4c27d77a7fcc7af2e261a450edf971b62b5), [`6684b4c`](https://github.com/Effect-TS/effect/commit/6684b4c27d77a7fcc7af2e261a450edf971b62b5), [`6684b4c`](https://github.com/Effect-TS/effect/commit/6684b4c27d77a7fcc7af2e261a450edf971b62b5)]: + - effect@3.5.2 + - @effect/platform@0.58.26 + +## 0.37.25 + +### Patch Changes + +- Updated dependencies [[`0623fca`](https://github.com/Effect-TS/effect/commit/0623fca41679b0e3c5a10dd0f8985f91670bd721)]: + - @effect/platform@0.58.25 + +## 0.37.24 + +### Patch Changes + +- Updated dependencies [[`55fdd76`](https://github.com/Effect-TS/effect/commit/55fdd761ee95afd73b6a892c13fee92b36c02837)]: + - effect@3.5.1 + - @effect/platform@0.58.24 + +## 0.37.23 + +### Patch Changes + +- Updated dependencies [[`a1f5b83`](https://github.com/Effect-TS/effect/commit/a1f5b831a1bc7535988b370d68d0b3eb1123e0ce), [`a1f5b83`](https://github.com/Effect-TS/effect/commit/a1f5b831a1bc7535988b370d68d0b3eb1123e0ce), [`a1f5b83`](https://github.com/Effect-TS/effect/commit/a1f5b831a1bc7535988b370d68d0b3eb1123e0ce), [`60bc3d0`](https://github.com/Effect-TS/effect/commit/60bc3d0867b13e48b24dc22604b4dd2e7b2c1ca4), [`5ab348f`](https://github.com/Effect-TS/effect/commit/5ab348f265db3d283aa091ddca6d2d49137c16f2), [`60bc3d0`](https://github.com/Effect-TS/effect/commit/60bc3d0867b13e48b24dc22604b4dd2e7b2c1ca4), [`3e04bf8`](https://github.com/Effect-TS/effect/commit/3e04bf8a7127e956cadb7684a8f4c661df57663b), [`e7fc45f`](https://github.com/Effect-TS/effect/commit/e7fc45f0c7002aafdaec7878149ac064cd104ea3), [`a1f5b83`](https://github.com/Effect-TS/effect/commit/a1f5b831a1bc7535988b370d68d0b3eb1123e0ce), [`4626de5`](https://github.com/Effect-TS/effect/commit/4626de59c25b384216faa0be87bf0b8cd36357d0), [`f01e7db`](https://github.com/Effect-TS/effect/commit/f01e7db317827255d7901f523f2e28b43298e8df), [`60bc3d0`](https://github.com/Effect-TS/effect/commit/60bc3d0867b13e48b24dc22604b4dd2e7b2c1ca4), [`79d2d91`](https://github.com/Effect-TS/effect/commit/79d2d91464d95dde0e9444d43e7a7f309f05d6e6), [`ac71f37`](https://github.com/Effect-TS/effect/commit/ac71f378f2413e5aa91c95f649ffe898d6a26114), [`8432360`](https://github.com/Effect-TS/effect/commit/8432360ce68614a419bb328083a4109d0fc8aa93), [`e4bf1bf`](https://github.com/Effect-TS/effect/commit/e4bf1bf2b4a970eacd77c9b77b5ea8c68bc84498), [`13cb861`](https://github.com/Effect-TS/effect/commit/13cb861a5eded15c55c6cdcf6a8acde8320367a6), [`79d2d91`](https://github.com/Effect-TS/effect/commit/79d2d91464d95dde0e9444d43e7a7f309f05d6e6), [`e7fc45f`](https://github.com/Effect-TS/effect/commit/e7fc45f0c7002aafdaec7878149ac064cd104ea3), [`9f66825`](https://github.com/Effect-TS/effect/commit/9f66825f1fce0fe8d10420c285f7dc4c71e8af8d)]: + - effect@3.5.0 + - @effect/platform@0.58.23 + +## 0.37.22 + +### Patch Changes + +- Updated dependencies [[`7af137c`](https://github.com/Effect-TS/effect/commit/7af137c9433f6e74959b3887561ec1e6f12e10ee), [`ee4b3dc`](https://github.com/Effect-TS/effect/commit/ee4b3dc5f68d19dc3ae1c2d12901c5b8ffbebabb), [`366f2ee`](https://github.com/Effect-TS/effect/commit/366f2ee3fb6f712a44e8f84fc188612e5ecc016d), [`366f2ee`](https://github.com/Effect-TS/effect/commit/366f2ee3fb6f712a44e8f84fc188612e5ecc016d), [`097d25c`](https://github.com/Effect-TS/effect/commit/097d25cb5d13c049e01789651be56b09620186ef)]: + - effect@3.4.9 + - @effect/platform@0.58.22 + +## 0.37.21 + +### Patch Changes + +- Updated dependencies [[`a435e0f`](https://github.com/Effect-TS/effect/commit/a435e0fc5378b33a49bcec92ee235df6f16a2419), [`b5554db`](https://github.com/Effect-TS/effect/commit/b5554db36c4dd6f64fa5e6a62a29b2759c54217a), [`a9c4fb3`](https://github.com/Effect-TS/effect/commit/a9c4fb3bf3c6e92cd1c142b0605fddf7eb3c697c)]: + - effect@3.4.8 + - @effect/platform@0.58.21 + +## 0.37.20 + +### Patch Changes + +- Updated dependencies [[`a5737d6`](https://github.com/Effect-TS/effect/commit/a5737d6db2b921605c332eabbc5402ee3d17357b)]: + - effect@3.4.7 + - @effect/platform@0.58.20 + +## 0.37.19 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform@0.58.19 + +## 0.37.18 + +### Patch Changes + +- Updated dependencies [[`7f8900a`](https://github.com/Effect-TS/effect/commit/7f8900a1de9addeb0d371103a2c5c2aa3e4ff95e)]: + - @effect/platform@0.58.18 + +## 0.37.17 + +### Patch Changes + +- Updated dependencies [[`5c0ceb0`](https://github.com/Effect-TS/effect/commit/5c0ceb00826cce9e50bf9d41d83e191d5352c030), [`5c0ceb0`](https://github.com/Effect-TS/effect/commit/5c0ceb00826cce9e50bf9d41d83e191d5352c030), [`33735b1`](https://github.com/Effect-TS/effect/commit/33735b16b41bd26929d8f4754c190925db6323b7), [`5c0ceb0`](https://github.com/Effect-TS/effect/commit/5c0ceb00826cce9e50bf9d41d83e191d5352c030), [`139d4b3`](https://github.com/Effect-TS/effect/commit/139d4b39fb3bff2eeaa7c0c809c581da42425a83)]: + - effect@3.4.6 + - @effect/platform@0.58.17 + +## 0.37.16 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform@0.58.16 + +## 0.37.15 + +### Patch Changes + +- Updated dependencies [[`baa90df`](https://github.com/Effect-TS/effect/commit/baa90df9663f5f37d7b6814dad25142d53dbc720)]: + - @effect/platform@0.58.15 + +## 0.37.14 + +### Patch Changes + +- Updated dependencies [[`52a87c7`](https://github.com/Effect-TS/effect/commit/52a87c7a0b9536398deaf8ec507e53a82c607219), [`6d2280e`](https://github.com/Effect-TS/effect/commit/6d2280e9497c95cb0e965ca462c825345074eedf)]: + - @effect/platform@0.58.14 + +## 0.37.13 + +### Patch Changes + +- Updated dependencies [[`a047af9`](https://github.com/Effect-TS/effect/commit/a047af99447dfffc729e9c8ef0ca143537927e91), [`dbd53ea`](https://github.com/Effect-TS/effect/commit/dbd53ea363c71a24449cb068251054c3a1acf864)]: + - effect@3.4.5 + - @effect/platform@0.58.13 + +## 0.37.12 + +### Patch Changes + +- Updated dependencies [[`74e0ad2`](https://github.com/Effect-TS/effect/commit/74e0ad23b4c36f41b7fd10856b20f8b701bc4044), [`74e0ad2`](https://github.com/Effect-TS/effect/commit/74e0ad23b4c36f41b7fd10856b20f8b701bc4044), [`72638e3`](https://github.com/Effect-TS/effect/commit/72638e3d99f0e93a24febf6c225256ce92d4a20b), [`d7dde2b`](https://github.com/Effect-TS/effect/commit/d7dde2b4af08b37af859d4c327c1f5c6f00cf9d9), [`9b2fc3b`](https://github.com/Effect-TS/effect/commit/9b2fc3b9dfd304a2bd0508ef2313cfc54357be0c)]: + - @effect/platform@0.58.12 + - effect@3.4.4 + +## 0.37.11 + +### Patch Changes + +- Updated dependencies [[`a5b95b5`](https://github.com/Effect-TS/effect/commit/a5b95b548284e4798654ae7ce6883fa49108f0ea), [`5e29579`](https://github.com/Effect-TS/effect/commit/5e29579187cb8420ea4930b3999fec984f8999f4)]: + - @effect/platform@0.58.11 + +## 0.37.10 + +### Patch Changes + +- [#3079](https://github.com/Effect-TS/effect/pull/3079) [`bbdd365`](https://github.com/Effect-TS/effect/commit/bbdd36567706c94cdec45bacea825941c347b6cd) Thanks @tim-smart! - update dependencies + +- [#3079](https://github.com/Effect-TS/effect/pull/3079) [`bbdd365`](https://github.com/Effect-TS/effect/commit/bbdd36567706c94cdec45bacea825941c347b6cd) Thanks @tim-smart! - update to typescript 5.5 + +- Updated dependencies [[`a48ee84`](https://github.com/Effect-TS/effect/commit/a48ee845ac21bbde9baf938af9e97a98322211c9), [`c342739`](https://github.com/Effect-TS/effect/commit/c3427396226e1ad7b95b40595a23f9bdff3e3365), [`8898e5e`](https://github.com/Effect-TS/effect/commit/8898e5e238622f6337583d91ee23609c1f5ccdf7), [`a48ee84`](https://github.com/Effect-TS/effect/commit/a48ee845ac21bbde9baf938af9e97a98322211c9), [`a48ee84`](https://github.com/Effect-TS/effect/commit/a48ee845ac21bbde9baf938af9e97a98322211c9), [`ff78636`](https://github.com/Effect-TS/effect/commit/ff786367c522975f40f0f179a0ecdfcfab7ecbdb), [`c86bd4e`](https://github.com/Effect-TS/effect/commit/c86bd4e134c23146c216f9ff97e03781d55991b6), [`ab3180f`](https://github.com/Effect-TS/effect/commit/ab3180f827041d0ea3b2d72254a1a8683e99e056), [`bbdd365`](https://github.com/Effect-TS/effect/commit/bbdd36567706c94cdec45bacea825941c347b6cd)]: + - @effect/platform@0.58.10 + - effect@3.4.3 + +## 0.37.9 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform@0.58.9 + +## 0.37.8 + +### Patch Changes + +- Updated dependencies [[`3da1497`](https://github.com/Effect-TS/effect/commit/3da1497b5c9cc886d300258bc928fd68a4fefe6f)]: + - effect@3.4.2 + - @effect/platform@0.58.8 + +## 0.37.7 + +### Patch Changes + +- Updated dependencies [[`027004a`](https://github.com/Effect-TS/effect/commit/027004a897f654791e75faa28eefb50dd0244b6e)]: + - @effect/platform@0.58.7 + +## 0.37.6 + +### Patch Changes + +- Updated dependencies [[`66a1910`](https://github.com/Effect-TS/effect/commit/66a19109ff90c4252123b8809b8c8a74681dba6a), [`2e8e252`](https://github.com/Effect-TS/effect/commit/2e8e2520cac712f0eb644553bd476429ebd674e4)]: + - effect@3.4.1 + - @effect/platform@0.58.6 + +## 0.37.5 + +### Patch Changes + +- Updated dependencies [[`37a07a2`](https://github.com/Effect-TS/effect/commit/37a07a2d8d1ce09ab965c0ada84a3fae9a6aba05)]: + - @effect/platform@0.58.5 + +## 0.37.4 + +### Patch Changes + +- Updated dependencies [[`b77fb0a`](https://github.com/Effect-TS/effect/commit/b77fb0a811ec1ad0e794917077c9a90824515db8)]: + - @effect/platform@0.58.4 + +## 0.37.3 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform@0.58.3 + +## 0.37.2 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform@0.58.2 + +## 0.37.1 + +### Patch Changes + +- Updated dependencies [[`5a248aa`](https://github.com/Effect-TS/effect/commit/5a248aa5ab2db3f7131ebc79bb9871a76de57973)]: + - @effect/platform@0.58.1 + +## 0.37.0 + +### Minor Changes + +- [#2938](https://github.com/Effect-TS/effect/pull/2938) [`63dd0c3`](https://github.com/Effect-TS/effect/commit/63dd0c3af45876c1caad7d03356c74daf551c628) Thanks @tim-smart! - restructure platform http to use flattened modules + + Instead of using the previous re-exports, you now use the modules directly. + + Before: + + ```ts + import { HttpClient } from "@effect/platform" + + HttpClient.request.get("/").pipe(HttpClient.client.fetchOk) + ``` + + After: + + ```ts + import { HttpClient, HttpClientRequest } from "@effect/platform" + + HttpClientRequest.get("/").pipe(HttpClient.fetchOk) + ``` + +### Patch Changes + +- Updated dependencies [[`c0ce180`](https://github.com/Effect-TS/effect/commit/c0ce180861ad0938053c0e6145e813fa6404df3b), [`63dd0c3`](https://github.com/Effect-TS/effect/commit/63dd0c3af45876c1caad7d03356c74daf551c628), [`61707b6`](https://github.com/Effect-TS/effect/commit/61707b6ffc7397c2ba0dce22512b44955724f60f), [`9c1b5b3`](https://github.com/Effect-TS/effect/commit/9c1b5b39e6c19604ce834f072a114ad392c50a06), [`a35faf8`](https://github.com/Effect-TS/effect/commit/a35faf8d116f94899bfc03feab33b004c8ddfdf7), [`ff73c0c`](https://github.com/Effect-TS/effect/commit/ff73c0cacd66132bfad2e5211b3eae347729c667), [`984d516`](https://github.com/Effect-TS/effect/commit/984d516ccd9412dc41188f6a46b748dd20dd5848), [`8c3b8a2`](https://github.com/Effect-TS/effect/commit/8c3b8a2ce208eab753b6206a51605a424f104e98), [`017e2f9`](https://github.com/Effect-TS/effect/commit/017e2f9b371ce24ea4945e5d7390c934ad3c39cf), [`91bf8a2`](https://github.com/Effect-TS/effect/commit/91bf8a2e9d1959393b3cf7366cc1d584d3e666b7), [`c6a4a26`](https://github.com/Effect-TS/effect/commit/c6a4a266606575fd2c7165940c4072ad4c57d01f)]: + - effect@3.4.0 + - @effect/platform@0.58.0 + +## 0.36.8 + +### Patch Changes + +- [#3030](https://github.com/Effect-TS/effect/pull/3030) [`3ba7ea1`](https://github.com/Effect-TS/effect/commit/3ba7ea1c3c2923e85bf2f17e41176f8f8796d203) Thanks @tim-smart! - update find-my-way-ts & multipasta + +- Updated dependencies [[`3ba7ea1`](https://github.com/Effect-TS/effect/commit/3ba7ea1c3c2923e85bf2f17e41176f8f8796d203)]: + - @effect/platform@0.57.8 + +## 0.36.7 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform@0.57.7 + +## 0.36.6 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform@0.57.6 + +## 0.36.5 + +### Patch Changes + +- Updated dependencies [[`b8ea6aa`](https://github.com/Effect-TS/effect/commit/b8ea6aa479006358042b4256ee0a1c5cfbe57acb)]: + - @effect/platform@0.57.5 + +## 0.36.4 + +### Patch Changes + +- Updated dependencies [[`6c89408`](https://github.com/Effect-TS/effect/commit/6c89408cd7b9204ec4c5828a46cd5312d8afb5e7)]: + - effect@3.3.5 + - @effect/platform@0.57.4 + +## 0.36.3 + +### Patch Changes + +- Updated dependencies [[`a67b8fe`](https://github.com/Effect-TS/effect/commit/a67b8fe2ace08419424811b5f0d9a5378eaea352)]: + - effect@3.3.4 + - @effect/platform@0.57.3 + +## 0.36.2 + +### Patch Changes + +- Updated dependencies [[`06ede85`](https://github.com/Effect-TS/effect/commit/06ede85d6e84710e6622463be95ff3927fb30dad), [`7204ca5`](https://github.com/Effect-TS/effect/commit/7204ca5761c2b1d27999a624db23aa10b6e0504d)]: + - effect@3.3.3 + - @effect/platform@0.57.2 + +## 0.36.1 + +### Patch Changes + +- Updated dependencies [[`07e12ec`](https://github.com/Effect-TS/effect/commit/07e12ecdb0e20b9763bd9e9058e567a7c8862efc), [`3572646`](https://github.com/Effect-TS/effect/commit/3572646d5e0804f85bc7f64633fb95722533f9dd), [`1aed347`](https://github.com/Effect-TS/effect/commit/1aed347a125ed3847ec90863424810d6759cbc85), [`df4bf4b`](https://github.com/Effect-TS/effect/commit/df4bf4b62e7b316c6647da0271fc5544a84e7ba2), [`f085f92`](https://github.com/Effect-TS/effect/commit/f085f92dfa204afb41823ffc27d437225137643d)]: + - @effect/platform@0.57.1 + - effect@3.3.2 + +## 0.36.0 + +### Minor Changes + +- [#2966](https://github.com/Effect-TS/effect/pull/2966) [`4d3fbe8`](https://github.com/Effect-TS/effect/commit/4d3fbe82e8cec13ccd0cd0b2096deac6818fb59a) Thanks @tim-smart! - fix KeyValueStore for react native by making constructors lazy + +### Patch Changes + +- Updated dependencies [[`eb98c5b`](https://github.com/Effect-TS/effect/commit/eb98c5b79ab50aa0cde239bd4e660dd19dbab612), [`184fed8`](https://github.com/Effect-TS/effect/commit/184fed83ac36cba05a75a5a8013f740f9f696e3b), [`4d3fbe8`](https://github.com/Effect-TS/effect/commit/4d3fbe82e8cec13ccd0cd0b2096deac6818fb59a), [`6068e07`](https://github.com/Effect-TS/effect/commit/6068e073d4cc8b3c8583583fd5eb3efe43f7d5ba), [`3a77e20`](https://github.com/Effect-TS/effect/commit/3a77e209783933bac3aaddba1b05ff6a9ac72b36)]: + - effect@3.3.1 + - @effect/platform@0.57.0 + +## 0.35.8 + +### Patch Changes + +- Updated dependencies [[`2b9ddfc`](https://github.com/Effect-TS/effect/commit/2b9ddfcbac505d98551e764a43923854907ca5c1), [`1f4ac00`](https://github.com/Effect-TS/effect/commit/1f4ac00a91c336c9c9c9b8c3ed9ceb9920ebc9bd), [`9305b76`](https://github.com/Effect-TS/effect/commit/9305b764cceeae4f16564435ae7172f79c2bf822), [`0f40d98`](https://github.com/Effect-TS/effect/commit/0f40d989da10f68df3ecd72b36849401ad679bfb), [`188f0a5`](https://github.com/Effect-TS/effect/commit/188f0a5c57ed0d7c9e5852e0c1c998f1b95810a1), [`b761ef0`](https://github.com/Effect-TS/effect/commit/b761ef00eaf6c67b7ffe34798b98aae5347ab376), [`b53f69b`](https://github.com/Effect-TS/effect/commit/b53f69bff1452a487b21198cd83961f844e02d36), [`0f40d98`](https://github.com/Effect-TS/effect/commit/0f40d989da10f68df3ecd72b36849401ad679bfb), [`5bd549e`](https://github.com/Effect-TS/effect/commit/5bd549e4bd7144727db438ecca6b8dc9b3ef7e22), [`67f160a`](https://github.com/Effect-TS/effect/commit/67f160a213de0219a565d4bf653b3cbf24f58e8f)]: + - @effect/platform@0.56.0 + - effect@3.3.0 + +## 0.35.7 + +### Patch Changes + +- Updated dependencies [[`a67d602`](https://github.com/Effect-TS/effect/commit/a67d60276f96cd20b76145b4cee13efca6c6158a)]: + - @effect/platform@0.55.7 + +## 0.35.6 + +### Patch Changes + +- Updated dependencies [[`8c5d280`](https://github.com/Effect-TS/effect/commit/8c5d280c0402284a4e58372867a15a431cb99461), [`6ba6d26`](https://github.com/Effect-TS/effect/commit/6ba6d269f5891e6b11aa35c5281dde4bf3273004), [`3f28bf2`](https://github.com/Effect-TS/effect/commit/3f28bf274333611906175446b772243f34f1b6d5), [`5817820`](https://github.com/Effect-TS/effect/commit/58178204a770d1a78c06945ef438f9fffbb50afa), [`799aa20`](https://github.com/Effect-TS/effect/commit/799aa20b4f618736ba33a5297fda90a75d4c26c6)]: + - effect@3.2.9 + - @effect/platform@0.55.6 + +## 0.35.5 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform@0.55.5 + +## 0.35.4 + +### Patch Changes + +- Updated dependencies [[`fb91f17`](https://github.com/Effect-TS/effect/commit/fb91f17098b48497feca9ec976feb87e4a82451b)]: + - effect@3.2.8 + - @effect/platform@0.55.4 + +## 0.35.3 + +### Patch Changes + +- Updated dependencies [[`6801fca`](https://github.com/Effect-TS/effect/commit/6801fca44366be3ee1b6b99f54bd4f38a1b5e4f4)]: + - effect@3.2.7 + - @effect/platform@0.55.3 + +## 0.35.2 + +### Patch Changes + +- Updated dependencies [[`2c2280b`](https://github.com/Effect-TS/effect/commit/2c2280b98a11fc002663c55792a4fa5781cd5fb6), [`cc8ac50`](https://github.com/Effect-TS/effect/commit/cc8ac5080daba8622ca2ff5dab5c37ddfab732ba)]: + - @effect/platform@0.55.2 + - effect@3.2.6 + +## 0.35.1 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform@0.55.1 + +## 0.35.0 + +### Minor Changes + +- [#2835](https://github.com/Effect-TS/effect/pull/2835) [`5133ca9`](https://github.com/Effect-TS/effect/commit/5133ca9dc4b8da0e28951316da9ab55dfbe0fbb9) Thanks @tim-smart! - remove pool resizing in platform workers to enable concurrent access + +### Patch Changes + +- Updated dependencies [[`608b01f`](https://github.com/Effect-TS/effect/commit/608b01fc342dbae2a642b308a67b84ead530ecea), [`031c712`](https://github.com/Effect-TS/effect/commit/031c7122a24ac42e48d6a434646b4f5d279d7442), [`a44e532`](https://github.com/Effect-TS/effect/commit/a44e532cf3a6a498b12a5aacf8124aa267e24ba0), [`5133ca9`](https://github.com/Effect-TS/effect/commit/5133ca9dc4b8da0e28951316da9ab55dfbe0fbb9)]: + - effect@3.2.5 + - @effect/platform@0.55.0 + +## 0.34.0 + +### Minor Changes + +- [#2801](https://github.com/Effect-TS/effect/pull/2801) [`1af94df`](https://github.com/Effect-TS/effect/commit/1af94df6b74aeb4f6ebcbe80e074b4cb252e62e3) Thanks @tim-smart! - remove `permits` from workers, to prevent issues with pool resizing + +### Patch Changes + +- Updated dependencies [[`1af94df`](https://github.com/Effect-TS/effect/commit/1af94df6b74aeb4f6ebcbe80e074b4cb252e62e3), [`1af94df`](https://github.com/Effect-TS/effect/commit/1af94df6b74aeb4f6ebcbe80e074b4cb252e62e3), [`c07e0ce`](https://github.com/Effect-TS/effect/commit/c07e0cea8ce165887e2c9dfa5d669eba9b2fb798), [`e313a01`](https://github.com/Effect-TS/effect/commit/e313a01b7e80f6cb7704055a190e5623c9d22c6d), [`1af94df`](https://github.com/Effect-TS/effect/commit/1af94df6b74aeb4f6ebcbe80e074b4cb252e62e3)]: + - effect@3.2.4 + - @effect/platform@0.54.0 + +## 0.33.29 + +### Patch Changes + +- Updated dependencies [[`45578e8`](https://github.com/Effect-TS/effect/commit/45578e8faa80ae33d23e08f6f19467f818b7788f)]: + - effect@3.2.3 + - @effect/platform@0.53.14 + +## 0.33.28 + +### Patch Changes + +- Updated dependencies [[`5d9266e`](https://github.com/Effect-TS/effect/commit/5d9266e8c740746ac9e186c3df6090a1b57fbe2a), [`9f8122e`](https://github.com/Effect-TS/effect/commit/9f8122e78884ab47c5e5f364d86eee1d1543cc61), [`6a6f670`](https://github.com/Effect-TS/effect/commit/6a6f6706b8613c8c7c10971b8d81a0f9e440a6f2), [`c1eaef9`](https://github.com/Effect-TS/effect/commit/c1eaef910420dae416923d172ee58d219e921d0f)]: + - effect@3.2.2 + - @effect/platform@0.53.13 + +## 0.33.27 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform@0.53.12 + +## 0.33.26 + +### Patch Changes + +- Updated dependencies [[`c1e991d`](https://github.com/Effect-TS/effect/commit/c1e991dd5ba87901cd0e05697a8b4a267e7e954a)]: + - effect@3.2.1 + - @effect/platform@0.53.11 + +## 0.33.25 + +### Patch Changes + +- Updated dependencies [[`146cadd`](https://github.com/Effect-TS/effect/commit/146cadd9d004634a3ff85c480bf92cf975c853e2), [`7135748`](https://github.com/Effect-TS/effect/commit/713574813a0f64085db0b5240ba39e7a0a7c137e), [`7135748`](https://github.com/Effect-TS/effect/commit/713574813a0f64085db0b5240ba39e7a0a7c137e), [`963b4e7`](https://github.com/Effect-TS/effect/commit/963b4e7ac87e2468feb6a344f7ab4ee4ad711198), [`64c9414`](https://github.com/Effect-TS/effect/commit/64c9414e960e82058ca09bbb3976d6fbef303a8e), [`7135748`](https://github.com/Effect-TS/effect/commit/713574813a0f64085db0b5240ba39e7a0a7c137e), [`2cbb76b`](https://github.com/Effect-TS/effect/commit/2cbb76bb52500a3f4bf27d1c91482518cbea56d7), [`870c5fa`](https://github.com/Effect-TS/effect/commit/870c5fa52cd61e745e8e828d38c3f09f00737553), [`146cadd`](https://github.com/Effect-TS/effect/commit/146cadd9d004634a3ff85c480bf92cf975c853e2), [`7135748`](https://github.com/Effect-TS/effect/commit/713574813a0f64085db0b5240ba39e7a0a7c137e), [`64c9414`](https://github.com/Effect-TS/effect/commit/64c9414e960e82058ca09bbb3976d6fbef303a8e)]: + - effect@3.2.0 + - @effect/platform@0.53.10 + +## 0.33.24 + +### Patch Changes + +- [#2761](https://github.com/Effect-TS/effect/pull/2761) [`17fc22e`](https://github.com/Effect-TS/effect/commit/17fc22e132593c5caa563705a4748ba0f04a853c) Thanks [@KhraksMamtsov](https://github.com/KhraksMamtsov)! - Add `{ once: true }` to all `"abort"` event listeners for `AbortController` to automatically remove handlers after execution + +- Updated dependencies [[`17fc22e`](https://github.com/Effect-TS/effect/commit/17fc22e132593c5caa563705a4748ba0f04a853c), [`810f222`](https://github.com/Effect-TS/effect/commit/810f222268792b13067c7a7bf317b93a9bb8917b), [`596aaea`](https://github.com/Effect-TS/effect/commit/596aaea022648b2e06fb1ec22f1652043d6fe64e)]: + - @effect/platform@0.53.9 + - effect@3.1.6 + +## 0.33.23 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform@0.53.8 + +## 0.33.22 + +### Patch Changes + +- [#2691](https://github.com/Effect-TS/effect/pull/2691) [`dc06f27`](https://github.com/Effect-TS/effect/commit/dc06f273fdcc9beeecef26f6e844fcce31b092a2) Thanks [@KhraksMamtsov](https://github.com/KhraksMamtsov)! - add Geolocation module to @effect/platform-browser + +- Updated dependencies []: + - @effect/platform@0.53.7 + +## 0.33.21 + +### Patch Changes + +- Updated dependencies [[`6ac4847`](https://github.com/Effect-TS/effect/commit/6ac48479447c01a4f35d655552af93e47e562610), [`6ac4847`](https://github.com/Effect-TS/effect/commit/6ac48479447c01a4f35d655552af93e47e562610)]: + - @effect/platform@0.53.6 + - effect@3.1.5 + +## 0.33.20 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform@0.53.5 + +## 0.33.19 + +### Patch Changes + +- Updated dependencies [[`e41e911`](https://github.com/Effect-TS/effect/commit/e41e91122fa6dd12fc81e50dcad0db891be67146)]: + - effect@3.1.4 + - @effect/platform@0.53.4 + +## 0.33.18 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform@0.53.3 + +## 0.33.17 + +### Patch Changes + +- [#2722](https://github.com/Effect-TS/effect/pull/2722) [`d4fb55d`](https://github.com/Effect-TS/effect/commit/d4fb55dc04d370c19a1176fa13ff7266c222e15e) Thanks [@tim-smart](https://github.com/tim-smart)! - run .close() when browser worker shuts down + +## 0.33.16 + +### Patch Changes + +- Updated dependencies [[`1f6dc96`](https://github.com/Effect-TS/effect/commit/1f6dc96f51c7bb9c8d11415358308604ba7c7c8e)]: + - effect@3.1.3 + - @effect/platform@0.53.2 + +## 0.33.15 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform@0.53.1 + +## 0.33.14 + +### Patch Changes + +- Updated dependencies [[`d57fbbb`](https://github.com/Effect-TS/effect/commit/d57fbbbd6c466936213a671fc3cd2390064f864e)]: + - @effect/platform@0.53.0 + +## 0.33.13 + +### Patch Changes + +- Updated dependencies [[`5866c62`](https://github.com/Effect-TS/effect/commit/5866c621d7eb4cc84e4ba972bfdfd219734cd45d)]: + - @effect/platform@0.52.3 + +## 0.33.12 + +### Patch Changes + +- Updated dependencies [[`2e1cdf6`](https://github.com/Effect-TS/effect/commit/2e1cdf67d141281288fffe9a5c10d1379a800513)]: + - @effect/platform@0.52.2 + - effect@3.1.2 + +## 0.33.11 + +### Patch Changes + +- Updated dependencies [[`e5e56d1`](https://github.com/Effect-TS/effect/commit/e5e56d138dbed3204636f605229c6685f89659fc)]: + - effect@3.1.1 + - @effect/platform@0.52.1 + +## 0.33.10 + +### Patch Changes + +- Updated dependencies [[`9deab0a`](https://github.com/Effect-TS/effect/commit/9deab0aec9e99501f9441843e34df9afa10c5be9), [`7719b8a`](https://github.com/Effect-TS/effect/commit/7719b8a7350c14e952ffe685bfd5308773b3e271)]: + - @effect/platform@0.52.0 + +## 0.33.9 + +### Patch Changes + +- [#2543](https://github.com/Effect-TS/effect/pull/2543) [`a1c7ab8`](https://github.com/Effect-TS/effect/commit/a1c7ab8ffedacd18c1fc784f4ff5844f79498b83) Thanks [@github-actions](https://github.com/apps/github-actions)! - added Stream.fromEventListener, and BrowserStream.{fromEventListenerWindow, fromEventListenerDocument} for constructing a stream from addEventListener + +- Updated dependencies [[`c3c12c6`](https://github.com/Effect-TS/effect/commit/c3c12c6625633fe80e79f9db75a3b8cf8ca8b11d), [`ba64ea6`](https://github.com/Effect-TS/effect/commit/ba64ea6757810c5e74cad3863a7d19d4d38af66b), [`b5de2d2`](https://github.com/Effect-TS/effect/commit/b5de2d2ce5b1afe8be90827bf898a95cec40eb2b), [`a1c7ab8`](https://github.com/Effect-TS/effect/commit/a1c7ab8ffedacd18c1fc784f4ff5844f79498b83), [`a023f28`](https://github.com/Effect-TS/effect/commit/a023f28336f3865687d9a30c1883e36909906d85), [`0ec93cb`](https://github.com/Effect-TS/effect/commit/0ec93cb4f166e7401c171c2f8e8276ce958d9a57), [`1c9454d`](https://github.com/Effect-TS/effect/commit/1c9454d532eae79b9f759aea77f59332cc6d18ed), [`a023f28`](https://github.com/Effect-TS/effect/commit/a023f28336f3865687d9a30c1883e36909906d85), [`92d56db`](https://github.com/Effect-TS/effect/commit/92d56dbb3f33e36636c2a2f1030c56492e39cf4d)]: + - effect@3.1.0 + - @effect/platform@0.51.0 + +## 0.33.8 + +### Patch Changes + +- [#2651](https://github.com/Effect-TS/effect/pull/2651) [`46b1327`](https://github.com/Effect-TS/effect/commit/46b1327ff0eadb215fd98b40375738cc15f1bc58) Thanks [@tim-smart](https://github.com/tim-smart)! - fix issue where "self" can be undefined in /platform workers + +- Updated dependencies [[`16039a0`](https://github.com/Effect-TS/effect/commit/16039a08f04f11545e2fdf40952788a8f9cef04f), [`557707b`](https://github.com/Effect-TS/effect/commit/557707bc9e5f230c8964d2757012075c34339b5c), [`d1d33e1`](https://github.com/Effect-TS/effect/commit/d1d33e10b25109f44b5ab1c6e4d778a59c0d3eeb), [`f4ed306`](https://github.com/Effect-TS/effect/commit/f4ed3068a70b50302d078a30d18ca3cfd2bc679c), [`661004f`](https://github.com/Effect-TS/effect/commit/661004f4bf5f8b25f5a0678c21a3a822188ce461), [`e79cb83`](https://github.com/Effect-TS/effect/commit/e79cb83d3b19098bc40a3012e2a059b8426306c2)]: + - @effect/platform@0.50.8 + - effect@3.0.8 + +## 0.33.7 + +### Patch Changes + +- Updated dependencies [[`18de56b`](https://github.com/Effect-TS/effect/commit/18de56b4a6b6d1f99230dfabf9147d59ea4dd759)]: + - effect@3.0.7 + - @effect/platform@0.50.7 + +## 0.33.6 + +### Patch Changes + +- Updated dependencies [[`ffe4f4e`](https://github.com/Effect-TS/effect/commit/ffe4f4e95db35fff6869e360b072e3837befa0a1), [`027418e`](https://github.com/Effect-TS/effect/commit/027418edaa6aa6c0ae4861b95832827b45adace4), [`ac1898e`](https://github.com/Effect-TS/effect/commit/ac1898eb7bc96880f911c276048e2ea3d6fe9c50), [`ffe4f4e`](https://github.com/Effect-TS/effect/commit/ffe4f4e95db35fff6869e360b072e3837befa0a1)]: + - effect@3.0.6 + - @effect/platform@0.50.6 + +## 0.33.5 + +### Patch Changes + +- Updated dependencies [[`6222404`](https://github.com/Effect-TS/effect/commit/62224044678751829ed2f128e05133a91c6b0569), [`868ed2a`](https://github.com/Effect-TS/effect/commit/868ed2a8fe94ee7f4206a6070f29dcf2a5ba1dc3)]: + - effect@3.0.5 + - @effect/platform@0.50.5 + +## 0.33.4 + +### Patch Changes + +- Updated dependencies [[`9a24667`](https://github.com/Effect-TS/effect/commit/9a246672008a2b668d43fbfd2fe5508c54b2b920)]: + - effect@3.0.4 + - @effect/platform@0.50.4 + +## 0.33.3 + +### Patch Changes + +- Updated dependencies [[`b3b51a2`](https://github.com/Effect-TS/effect/commit/b3b51a2ea0c6ab92a363db46ebaa7e1176d089f5)]: + - @effect/platform@0.50.3 + +## 0.33.2 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform@0.50.2 + +## 0.33.1 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform@0.50.1 + +## 0.33.0 + +### Minor Changes + +- [#2567](https://github.com/Effect-TS/effect/pull/2567) [`6f38dff`](https://github.com/Effect-TS/effect/commit/6f38dff41ffa34532cc2f25b90446550c5730bb6) Thanks [@tim-smart](https://github.com/tim-smart)! - add URL & AbortSignal to Http.client.makeDefault + +### Patch Changes + +- [#2567](https://github.com/Effect-TS/effect/pull/2567) [`6f38dff`](https://github.com/Effect-TS/effect/commit/6f38dff41ffa34532cc2f25b90446550c5730bb6) Thanks [@tim-smart](https://github.com/tim-smart)! - add more span attributes to http traces + +- Updated dependencies [[`6f38dff`](https://github.com/Effect-TS/effect/commit/6f38dff41ffa34532cc2f25b90446550c5730bb6), [`a7b4b84`](https://github.com/Effect-TS/effect/commit/a7b4b84bd5a25f51aba922f9259c3a58c98c6a4e), [`a3b0e6c`](https://github.com/Effect-TS/effect/commit/a3b0e6c490772e6d44b5d98dcf2729c4d5310ecc), [`6f38dff`](https://github.com/Effect-TS/effect/commit/6f38dff41ffa34532cc2f25b90446550c5730bb6)]: + - @effect/platform@0.50.0 + - effect@3.0.3 + +## 0.32.4 + +### Patch Changes + +- [#2562](https://github.com/Effect-TS/effect/pull/2562) [`2cecdbd`](https://github.com/Effect-TS/effect/commit/2cecdbd1cf30befce4e84796ccd953ea55ecfb86) Thanks [@fubhy](https://github.com/fubhy)! - Added provenance publishing + +- Updated dependencies [[`2cecdbd`](https://github.com/Effect-TS/effect/commit/2cecdbd1cf30befce4e84796ccd953ea55ecfb86)]: + - @effect/platform@0.49.4 + - effect@3.0.2 + +## 0.32.3 + +### Patch Changes + +- Updated dependencies [[`8d39d65`](https://github.com/Effect-TS/effect/commit/8d39d6554af548228ad767112ce2e0b1f68fa8e1)]: + - @effect/platform@0.49.3 + +## 0.32.2 + +### Patch Changes + +- Updated dependencies [[`5ef0a1a`](https://github.com/Effect-TS/effect/commit/5ef0a1ae9b773fa2481550cb0d43ff7a0e03cd44)]: + - @effect/platform@0.49.2 + +## 0.32.1 + +### Patch Changes + +- Updated dependencies [[`3da0cfa`](https://github.com/Effect-TS/effect/commit/3da0cfa12c407fd930dc480be1ecc9217a8058f8), [`570e8d8`](https://github.com/Effect-TS/effect/commit/570e8d87e7c0e9ad4cd2686462fdb9b4812f7716), [`87c5687`](https://github.com/Effect-TS/effect/commit/87c5687de0782dab177b7861217fa3b040046282), [`8edacca`](https://github.com/Effect-TS/effect/commit/8edacca37f8e37c01a63fec332b06d9361efaa7b)]: + - effect@3.0.1 + - @effect/platform@0.49.1 + +## 0.32.0 + +### Minor Changes + +- [#2207](https://github.com/Effect-TS/effect/pull/2207) [`aa4a3b5`](https://github.com/Effect-TS/effect/commit/aa4a3b550da1c1020265ac389ed3f309388994a2) Thanks [@github-actions](https://github.com/apps/github-actions)! - Swap type parameters in /platform data types + + A codemod has been released to make migration easier: + + ``` + npx @effect/codemod platform-0.49 src/**/* + ``` + +- [#2207](https://github.com/Effect-TS/effect/pull/2207) [`5a2314b`](https://github.com/Effect-TS/effect/commit/5a2314b70ec79c2c02b51cef45a5ddec8327daa1) Thanks [@github-actions](https://github.com/apps/github-actions)! - replace use of `unit` terminology with `void` + + For all the data types. + + ```ts + Effect.unit // => Effect.void + Stream.unit // => Stream.void + + // etc + ``` + +- [#2207](https://github.com/Effect-TS/effect/pull/2207) [`2fb7d9c`](https://github.com/Effect-TS/effect/commit/2fb7d9ca15037ff62a578bb9fe5732da5f4f317d) Thanks [@github-actions](https://github.com/apps/github-actions)! - Release Effect 3.0 🎉 + +### Patch Changes + +- [#2207](https://github.com/Effect-TS/effect/pull/2207) [`6460414`](https://github.com/Effect-TS/effect/commit/6460414351a45fb8e0a457c63f3653422efee766) Thanks [@github-actions](https://github.com/apps/github-actions)! - properly handle multiple ports in SharedWorker + +- Updated dependencies [[`1b5f0c7`](https://github.com/Effect-TS/effect/commit/1b5f0c77e7fd477a0026071e82129a948227f4b3), [`d50a652`](https://github.com/Effect-TS/effect/commit/d50a652479f4d1d64f48da05c79fa847e6e51548), [`9a3bd47`](https://github.com/Effect-TS/effect/commit/9a3bd47ebd0750c7e498162734f6d21895de0cb2), [`6460414`](https://github.com/Effect-TS/effect/commit/6460414351a45fb8e0a457c63f3653422efee766), [`cf69f46`](https://github.com/Effect-TS/effect/commit/cf69f46690058d71eeada03cfb40dc744573e9e4), [`cf69f46`](https://github.com/Effect-TS/effect/commit/cf69f46690058d71eeada03cfb40dc744573e9e4), [`be9d025`](https://github.com/Effect-TS/effect/commit/be9d025e42355260ace02dd135851a8935a4deba), [`78b767c`](https://github.com/Effect-TS/effect/commit/78b767c2b1625186e17131761a0edbac25d21850), [`aa4a3b5`](https://github.com/Effect-TS/effect/commit/aa4a3b550da1c1020265ac389ed3f309388994a2), [`1499974`](https://github.com/Effect-TS/effect/commit/14999741d2e19c1747f6a7e19d68977f6429cdb8), [`1b5f0c7`](https://github.com/Effect-TS/effect/commit/1b5f0c77e7fd477a0026071e82129a948227f4b3), [`5c2b561`](https://github.com/Effect-TS/effect/commit/5c2b5614f583b88784ed68126ae939832fb3c092), [`6c6087a`](https://github.com/Effect-TS/effect/commit/6c6087a4a897b64252346426660782d31c13f769), [`a18f594`](https://github.com/Effect-TS/effect/commit/a18f5948f1439a147232448b2c443472fda0eceb), [`1499974`](https://github.com/Effect-TS/effect/commit/14999741d2e19c1747f6a7e19d68977f6429cdb8), [`2f96d93`](https://github.com/Effect-TS/effect/commit/2f96d938b90f8c19377583279e3c7afd9b509c50), [`5a2314b`](https://github.com/Effect-TS/effect/commit/5a2314b70ec79c2c02b51cef45a5ddec8327daa1), [`25d74f8`](https://github.com/Effect-TS/effect/commit/25d74f8c4d2dd4a9e5ec57ce2f20d36dedd25343), [`6c6087a`](https://github.com/Effect-TS/effect/commit/6c6087a4a897b64252346426660782d31c13f769), [`271b79f`](https://github.com/Effect-TS/effect/commit/271b79fc0b66a6c11e07a8779ff8800493a7eac2), [`1b5f0c7`](https://github.com/Effect-TS/effect/commit/1b5f0c77e7fd477a0026071e82129a948227f4b3), [`2fb7d9c`](https://github.com/Effect-TS/effect/commit/2fb7d9ca15037ff62a578bb9fe5732da5f4f317d), [`53d1c2a`](https://github.com/Effect-TS/effect/commit/53d1c2a77559081fbb89667e343346375c6d6650), [`e7e1bbe`](https://github.com/Effect-TS/effect/commit/e7e1bbe68486fdf31c8f84b0880522d39adcaad3), [`10c169e`](https://github.com/Effect-TS/effect/commit/10c169eadc874e91b4defca3f467b4e6a50fd8f3), [`6424181`](https://github.com/Effect-TS/effect/commit/64241815fe6a939e91e6947253e7dceea1306aa8)]: + - effect@3.0.0 + - @effect/platform@0.49.0 + +## 0.31.29 + +### Patch Changes + +- Updated dependencies [[`b79cc59`](https://github.com/Effect-TS/effect/commit/b79cc59dbe64b9a0a7742dc9100a9d36c8e46b72)]: + - @effect/platform@0.48.29 + +## 0.31.28 + +### Patch Changes + +- Updated dependencies [[`41c8102`](https://github.com/Effect-TS/effect/commit/41c810228b1a50e4b41f19e735d7c62fe8d36871), [`776ef2b`](https://github.com/Effect-TS/effect/commit/776ef2bb66db9aa9f68b7beab14f6986f9c1288b), [`217147e`](https://github.com/Effect-TS/effect/commit/217147ea67c5c42c96f024775c41e5b070f81e4c), [`90776ec`](https://github.com/Effect-TS/effect/commit/90776ec8e8671d835b65fc33ead1de6c864b81b9), [`8709856`](https://github.com/Effect-TS/effect/commit/870985694ae985c3cb9360ad8a25c60e6f785f55), [`232c353`](https://github.com/Effect-TS/effect/commit/232c353c2e6f743f38e57639ee30e324ffa9c2a9), [`0ca835c`](https://github.com/Effect-TS/effect/commit/0ca835cbac8e69072a93ace83b534219faba24e8), [`8709856`](https://github.com/Effect-TS/effect/commit/870985694ae985c3cb9360ad8a25c60e6f785f55), [`d590094`](https://github.com/Effect-TS/effect/commit/d5900943489ec1e0891836aeafb5ce99fb9c75c7), [`e983740`](https://github.com/Effect-TS/effect/commit/e9837401145605aff5bc2ec7e73004f397c5d2d1), [`e3e0924`](https://github.com/Effect-TS/effect/commit/e3e09247d46a35430fc60e4aa4032cc50814f212)]: + - effect@2.4.19 + - @effect/platform@0.48.28 + +## 0.31.27 + +### Patch Changes + +- Updated dependencies [[`c6dd3c6`](https://github.com/Effect-TS/effect/commit/c6dd3c6909cafe05adc8450c5a499260e17e60d3), [`672f137`](https://github.com/Effect-TS/effect/commit/672f13747ddf6dac3ba304fd4511b1df44ab566d), [`672f137`](https://github.com/Effect-TS/effect/commit/672f13747ddf6dac3ba304fd4511b1df44ab566d)]: + - @effect/platform@0.48.27 + +## 0.31.26 + +### Patch Changes + +- Updated dependencies [[`365a486`](https://github.com/Effect-TS/effect/commit/365a4865de5e47ce09f4cfd51fc0f67438f82a57)]: + - @effect/platform@0.48.26 + +## 0.31.25 + +### Patch Changes + +- [#2466](https://github.com/Effect-TS/effect/pull/2466) [`8941328`](https://github.com/Effect-TS/effect/commit/89413286d34d9a9d6d5c2a97b9bbab2d82167994) Thanks [@jessekelly881](https://github.com/jessekelly881)! - made Logger.structuredLogger the default for Browser.runMain + +- Updated dependencies [[`dadc690`](https://github.com/Effect-TS/effect/commit/dadc6906121c512bc32be22b52adbd1ada834594), [`d209171`](https://github.com/Effect-TS/effect/commit/d2091714a786820ebae4bef04a9d67d25dd08e88)]: + - effect@2.4.18 + - @effect/platform@0.48.25 + +## 0.31.24 + +### Patch Changes + +- Updated dependencies [[`9c6a500`](https://github.com/Effect-TS/effect/commit/9c6a5001b467b6255c68a922f4b6e8d692b63d01), [`8fdfda6`](https://github.com/Effect-TS/effect/commit/8fdfda6618be848c01b399d13bc05a9a3adfb613), [`607b2e7`](https://github.com/Effect-TS/effect/commit/607b2e7a7fd9318c57acf4e50ec61747eea74ad7), [`8fdfda6`](https://github.com/Effect-TS/effect/commit/8fdfda6618be848c01b399d13bc05a9a3adfb613), [`35ad0ba`](https://github.com/Effect-TS/effect/commit/35ad0ba9f3ba27c60453620e514b980f819f92af), [`8206caf`](https://github.com/Effect-TS/effect/commit/8206caf7c2d22c68be4313318b61cfdacf6222b6), [`7ddd654`](https://github.com/Effect-TS/effect/commit/7ddd65415b65ccb654ad04f4dbefe39402f15117), [`7ddd654`](https://github.com/Effect-TS/effect/commit/7ddd65415b65ccb654ad04f4dbefe39402f15117), [`8fdfda6`](https://github.com/Effect-TS/effect/commit/8fdfda6618be848c01b399d13bc05a9a3adfb613), [`f456ba2`](https://github.com/Effect-TS/effect/commit/f456ba273bae21a6dcf8c966c50c97b5f0897d9f)]: + - @effect/platform@0.48.24 + - effect@2.4.17 + +## 0.31.23 + +### Patch Changes + +- [#2454](https://github.com/Effect-TS/effect/pull/2454) [`63a1df2`](https://github.com/Effect-TS/effect/commit/63a1df2e4de3766f48f15676fbd0360ab9c27816) Thanks [@tim-smart](https://github.com/tim-smart)! - add support for binary data with XHR client + +- Updated dependencies [[`5170ce7`](https://github.com/Effect-TS/effect/commit/5170ce708c606283e8a30d273950f1a21c7eddc2), [`63a1df2`](https://github.com/Effect-TS/effect/commit/63a1df2e4de3766f48f15676fbd0360ab9c27816), [`5170ce7`](https://github.com/Effect-TS/effect/commit/5170ce708c606283e8a30d273950f1a21c7eddc2), [`74a5dae`](https://github.com/Effect-TS/effect/commit/74a5daed0e65b32a36e026bfcf66d02269cb967a)]: + - @effect/platform@0.48.23 + - effect@2.4.16 + +## 0.31.22 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform@0.48.22 + +## 0.31.21 + +### Patch Changes + +- Updated dependencies [[`d7688c0`](https://github.com/Effect-TS/effect/commit/d7688c0c72717fe7876c871567f6946dabfc0546), [`b3a4fac`](https://github.com/Effect-TS/effect/commit/b3a4face2acaca422f0b0530436e8f13129f3b3a)]: + - effect@2.4.15 + - @effect/platform@0.48.21 + +## 0.31.20 + +### Patch Changes + +- Updated dependencies [[`4789083`](https://github.com/Effect-TS/effect/commit/4789083283bdaec456982d614ebc4a496ea0e7f7), [`4789083`](https://github.com/Effect-TS/effect/commit/4789083283bdaec456982d614ebc4a496ea0e7f7)]: + - @effect/platform@0.48.20 + +## 0.31.19 + +### Patch Changes + +- Updated dependencies [[`fb7285e`](https://github.com/Effect-TS/effect/commit/fb7285e8d6a70527df7137a6a3efdd03ae61cb8b)]: + - @effect/platform@0.48.19 + +## 0.31.18 + +### Patch Changes + +- Updated dependencies [[`26435ec`](https://github.com/Effect-TS/effect/commit/26435ecfa06569dc18d1801ccf38213a43b7c334), [`6180c0c`](https://github.com/Effect-TS/effect/commit/6180c0cc51dee785cfce72220a52c9fc3b9bf9aa)]: + - @effect/platform@0.48.18 + - effect@2.4.14 + +## 0.31.17 + +### Patch Changes + +- Updated dependencies [[`47a8f1b`](https://github.com/Effect-TS/effect/commit/47a8f1b644d8294692d92cacd3c8c7543edbfabe), [`8c9abe2`](https://github.com/Effect-TS/effect/commit/8c9abe2b35c46d8891d4b2c14ff9eb46302a14f3), [`8c9abe2`](https://github.com/Effect-TS/effect/commit/8c9abe2b35c46d8891d4b2c14ff9eb46302a14f3), [`3336287`](https://github.com/Effect-TS/effect/commit/3336287ff55a25e56d759b83847bfaa21c40f499), [`54b7c00`](https://github.com/Effect-TS/effect/commit/54b7c0077fa784ad2646b812d6a44641f672edcd), [`3336287`](https://github.com/Effect-TS/effect/commit/3336287ff55a25e56d759b83847bfaa21c40f499), [`47a8f1b`](https://github.com/Effect-TS/effect/commit/47a8f1b644d8294692d92cacd3c8c7543edbfabe)]: + - @effect/platform@0.48.17 + - effect@2.4.13 + +## 0.31.16 + +### Patch Changes + +- [#2387](https://github.com/Effect-TS/effect/pull/2387) [`75a8d16`](https://github.com/Effect-TS/effect/commit/75a8d16247cc14860cdd7fd948ef542c50c2d55e) Thanks [@tim-smart](https://github.com/tim-smart)! - add Cookies module to /platform http + + To add cookies to a http response: + + ```ts + import * as Http from "@effect/platform/HttpServer" + + Http.response.empty().pipe( + Http.response.setCookies([ + ["name", "value"], + ["foo", "bar", { httpOnly: true }] + ]) + ) + ``` + + You can also use cookies with the http client: + + ```ts + import * as Http from "@effect/platform/HttpClient" + import { Effect, Ref } from "effect" + + Effect.gen(function* (_) { + const ref = yield* _(Ref.make(Http.cookies.empty)) + const defaultClient = yield* _(Http.client.Client) + const clientWithCookies = defaultClient.pipe( + Http.client.withCookiesRef(ref), + Http.client.filterStatusOk + ) + + // cookies will be stored in the ref and sent in any subsequent requests + yield* _( + Http.request.get("https://www.google.com/"), + clientWithCookies, + Effect.scoped + ) + }) + ``` + +- [#2385](https://github.com/Effect-TS/effect/pull/2385) [`3307729`](https://github.com/Effect-TS/effect/commit/3307729de162a033fa9caa8e14c111013dcf0d87) Thanks [@tim-smart](https://github.com/tim-smart)! - update typescript to 5.4 + +- Updated dependencies [[`75a8d16`](https://github.com/Effect-TS/effect/commit/75a8d16247cc14860cdd7fd948ef542c50c2d55e), [`3307729`](https://github.com/Effect-TS/effect/commit/3307729de162a033fa9caa8e14c111013dcf0d87)]: + - @effect/platform@0.48.16 + - effect@2.4.12 + +## 0.31.15 + +### Patch Changes + +- [#2384](https://github.com/Effect-TS/effect/pull/2384) [`2f488c4`](https://github.com/Effect-TS/effect/commit/2f488c436de52576562803c57ebc132ef40ccdd8) Thanks [@tim-smart](https://github.com/tim-smart)! - update dependencies + +- Updated dependencies [[`2f488c4`](https://github.com/Effect-TS/effect/commit/2f488c436de52576562803c57ebc132ef40ccdd8), [`37ca592`](https://github.com/Effect-TS/effect/commit/37ca592a4101ad90adbf8c8b3f727faf3110cae5), [`317b5b8`](https://github.com/Effect-TS/effect/commit/317b5b8e8c8c2207469b3ebfcf72bf3a9f7cbc60)]: + - effect@2.4.11 + - @effect/platform@0.48.15 + +## 0.31.14 + +### Patch Changes + +- Updated dependencies [[`9bab1f9`](https://github.com/Effect-TS/effect/commit/9bab1f9fa5b999740755e4e82485cb77c638643a), [`9bbde5b`](https://github.com/Effect-TS/effect/commit/9bbde5be9a0168d1c2a0308bfc27167ed62f3968)]: + - effect@2.4.10 + - @effect/platform@0.48.14 + +## 0.31.13 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform@0.48.13 + +## 0.31.12 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform@0.48.12 + +## 0.31.11 + +### Patch Changes + +- Updated dependencies [[`0f6c7b4`](https://github.com/Effect-TS/effect/commit/0f6c7b426eb3432f60e3a17f8cd92ceac91597bf)]: + - @effect/platform@0.48.11 + +## 0.31.10 + +### Patch Changes + +- [#2357](https://github.com/Effect-TS/effect/pull/2357) [`71fd528`](https://github.com/Effect-TS/effect/commit/71fd5287500f9ce155a7d9f0df6ee3e0ac3aeb99) Thanks [@tim-smart](https://github.com/tim-smart)! - make more data types in /platform implement Inspectable + +- Updated dependencies [[`71fd528`](https://github.com/Effect-TS/effect/commit/71fd5287500f9ce155a7d9f0df6ee3e0ac3aeb99)]: + - @effect/platform@0.48.10 + - effect@2.4.9 + +## 0.31.9 + +### Patch Changes + +- Updated dependencies [[`bb0b69e`](https://github.com/Effect-TS/effect/commit/bb0b69e519698c7c76aa68217de423c78ad16566), [`6b20bad`](https://github.com/Effect-TS/effect/commit/6b20badebb3a7ca4d38857753e8ecaa09d02ccfb), [`4e64e9b`](https://github.com/Effect-TS/effect/commit/4e64e9b9876de6bfcbabe39e18a91a08e5f3fbb0), [`3851a02`](https://github.com/Effect-TS/effect/commit/3851a022c481006aec1db36651e4b4fd727aa742), [`5f5fcd9`](https://github.com/Effect-TS/effect/commit/5f5fcd969ae30ed6fe61d566a571498d9e895e16), [`814e5b8`](https://github.com/Effect-TS/effect/commit/814e5b828f68210b9e8f336fd6ac688646835dd9)]: + - effect@2.4.8 + - @effect/platform@0.48.9 + +## 0.31.8 + +### Patch Changes + +- Updated dependencies [[`69d27bb`](https://github.com/Effect-TS/effect/commit/69d27bb633884b6b50f9c3d9e95c29f09b4860b5)]: + - @effect/platform@0.48.8 + +## 0.31.7 + +### Patch Changes + +- Updated dependencies [[`f908948`](https://github.com/Effect-TS/effect/commit/f908948fd05771a670c0b746e2dd9caa9408ef83)]: + - @effect/platform@0.48.7 + +## 0.31.6 + +### Patch Changes + +- Updated dependencies [[`eb93283`](https://github.com/Effect-TS/effect/commit/eb93283985913d7b04ca750e36ac8513e7b6cef6)]: + - effect@2.4.7 + - @effect/platform@0.48.6 + +## 0.31.5 + +### Patch Changes + +- Updated dependencies [[`e006e4a`](https://github.com/Effect-TS/effect/commit/e006e4a538c97bae6ca1efa74802159e8a688fcb)]: + - @effect/platform@0.48.5 + +## 0.31.4 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform@0.48.4 + +## 0.31.3 + +### Patch Changes + +- Updated dependencies [[`c362e06`](https://github.com/Effect-TS/effect/commit/c362e066550252d5a9fcbc31a4b34d0e17c50699), [`83ddd6f`](https://github.com/Effect-TS/effect/commit/83ddd6f41029724b2cbd144cf309463967ed1164)]: + - @effect/platform@0.48.3 + +## 0.31.2 + +### Patch Changes + +- Updated dependencies [[`4f35a7e`](https://github.com/Effect-TS/effect/commit/4f35a7e7c4eba598924aff24d1158b9056bb24be), [`9971186`](https://github.com/Effect-TS/effect/commit/99711862722188fbb5ed3ee75126ad5edf13f72f)]: + - @effect/platform@0.48.2 + - effect@2.4.6 + +## 0.31.1 + +### Patch Changes + +- Updated dependencies [[`bce21c5`](https://github.com/Effect-TS/effect/commit/bce21c5ded2177114666ba229bd5029fa000dee3), [`c7d3036`](https://github.com/Effect-TS/effect/commit/c7d303630b7f0825cb2e584557c5767a67214d9f)]: + - effect@2.4.5 + - @effect/platform@0.48.1 + +## 0.31.0 + +### Minor Changes + +- [#2287](https://github.com/Effect-TS/effect/pull/2287) [`a1f44cb`](https://github.com/Effect-TS/effect/commit/a1f44cb5112713ff9a3ac3d91a63a2c99d6b7fc1) Thanks [@tim-smart](https://github.com/tim-smart)! - add option to /platform runMain to disable error reporting + +### Patch Changes + +- Updated dependencies [[`5d47ee0`](https://github.com/Effect-TS/effect/commit/5d47ee0855e492532085b6092879b1b952d84949), [`1cb7f9c`](https://github.com/Effect-TS/effect/commit/1cb7f9cff7c2272a32fc7a324d87b02e2cd8a2f5), [`817a04c`](https://github.com/Effect-TS/effect/commit/817a04cb2df0f4140984dc97eb3e1bb14a6c4a38), [`d90a99d`](https://github.com/Effect-TS/effect/commit/d90a99d03d074adc7cd2533f15419138264da5a2), [`dd05faa`](https://github.com/Effect-TS/effect/commit/dd05faa621555ef3585ecd914ac13ecd89b710f4), [`509be1a`](https://github.com/Effect-TS/effect/commit/509be1a0817118489750cf028523134677e44a8a), [`1cb7f9c`](https://github.com/Effect-TS/effect/commit/1cb7f9cff7c2272a32fc7a324d87b02e2cd8a2f5), [`dd05faa`](https://github.com/Effect-TS/effect/commit/dd05faa621555ef3585ecd914ac13ecd89b710f4), [`e7ca973`](https://github.com/Effect-TS/effect/commit/e7ca973c5430ae60716701e58bedd4632ff971fd), [`a1f44cb`](https://github.com/Effect-TS/effect/commit/a1f44cb5112713ff9a3ac3d91a63a2c99d6b7fc1), [`d910dd2`](https://github.com/Effect-TS/effect/commit/d910dd2ca1e8e5aa2f09d9bf3694ede745758f99), [`e7ca973`](https://github.com/Effect-TS/effect/commit/e7ca973c5430ae60716701e58bedd4632ff971fd), [`802674b`](https://github.com/Effect-TS/effect/commit/802674b379b7559ad3ff09b33388891445a9e48b), [`bdff193`](https://github.com/Effect-TS/effect/commit/bdff193365dd9ec2863573b08eb960aa8dee5c93)]: + - effect@2.4.4 + - @effect/platform@0.48.0 + +## 0.30.12 + +### Patch Changes + +- Updated dependencies [[`0680545`](https://github.com/Effect-TS/effect/commit/068054540f19bb23a79c7c021ed8b2fe34f3e19f), [`20e63fb`](https://github.com/Effect-TS/effect/commit/20e63fb9207210f3fe2d136ec40d0a2dbff3225e), [`20e63fb`](https://github.com/Effect-TS/effect/commit/20e63fb9207210f3fe2d136ec40d0a2dbff3225e)]: + - @effect/platform@0.47.1 + - effect@2.4.3 + +## 0.30.11 + +### Patch Changes + +- Updated dependencies [[`e03811e`](https://github.com/Effect-TS/effect/commit/e03811e80c93e986e6348b3b67ac2ed6d5fefff0), [`ac41d84`](https://github.com/Effect-TS/effect/commit/ac41d84776484cdce8165b7ca2c9c9b6377eee2d), [`0f3d99c`](https://github.com/Effect-TS/effect/commit/0f3d99c27521ec6b221b644a0fffc79199c3acca), [`6137533`](https://github.com/Effect-TS/effect/commit/613753300c7705518ab1fea2f370b032851c2750), [`f373529`](https://github.com/Effect-TS/effect/commit/f373529999f4b8bc92b634f6ea14f19271388eed), [`1bf9f31`](https://github.com/Effect-TS/effect/commit/1bf9f31f07667de677673f7c29a4e7a26ebad3c8), [`e3ff789`](https://github.com/Effect-TS/effect/commit/e3ff789226f89e71eb28ca38ce79f90af6a03f1a), [`6137533`](https://github.com/Effect-TS/effect/commit/613753300c7705518ab1fea2f370b032851c2750), [`507ba40`](https://github.com/Effect-TS/effect/commit/507ba4060ff043c1a8d541dae723fa6940633b00), [`4064ea0`](https://github.com/Effect-TS/effect/commit/4064ea04e0b3fa23108ee471cd89ab2482b2f6e5), [`e466afe`](https://github.com/Effect-TS/effect/commit/e466afe32f2de598ceafd8982bd0cfbd388e5671), [`f373529`](https://github.com/Effect-TS/effect/commit/f373529999f4b8bc92b634f6ea14f19271388eed), [`de74eb8`](https://github.com/Effect-TS/effect/commit/de74eb80a79eebde5ff645033765e7a617e92f27), [`fa9663c`](https://github.com/Effect-TS/effect/commit/fa9663cb854ca03dba672d7857ecff84f1140c9e), [`fa9663c`](https://github.com/Effect-TS/effect/commit/fa9663cb854ca03dba672d7857ecff84f1140c9e)]: + - effect@2.4.2 + - @effect/platform@0.47.0 + +## 0.30.10 + +### Patch Changes + +- Updated dependencies [[`a4a0006`](https://github.com/Effect-TS/effect/commit/a4a0006c7f19fc261df5cda16963d73457e4d6ac), [`7535080`](https://github.com/Effect-TS/effect/commit/7535080f2e2f9859711031161600c01807cc43ea), [`0a37676`](https://github.com/Effect-TS/effect/commit/0a37676aa0eb2a21e17af2e6df9f81f52bbc8831), [`bd1d7ac`](https://github.com/Effect-TS/effect/commit/bd1d7ac75eea57a94d5e2d8e1edccb3136e84899), [`6f503b7`](https://github.com/Effect-TS/effect/commit/6f503b774d893bf2af34f66202e270d8c45d5f31)]: + - effect@2.4.1 + - @effect/platform@0.46.3 + +## 0.30.9 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform@0.46.2 + +## 0.30.8 + +### Patch Changes + +- Updated dependencies [[`aa6556f`](https://github.com/Effect-TS/effect/commit/aa6556f007117caea84d6965aa30846a11879e9d)]: + - @effect/platform@0.46.1 + +## 0.30.7 + +### Patch Changes + +- Updated dependencies [[`5de7be5`](https://github.com/Effect-TS/effect/commit/5de7be5beca2e963b503e6029dcc3217848187d2), [`489fcf3`](https://github.com/Effect-TS/effect/commit/489fcf363ff2b2a953166b740cb9a62d7fc2a101), [`7d9c3bf`](https://github.com/Effect-TS/effect/commit/7d9c3bff6c18d451e0e4781042945ec5c7be1b9f), [`d8d278b`](https://github.com/Effect-TS/effect/commit/d8d278b2efb2966947029885e01f7b68348a021f), [`14c5711`](https://github.com/Effect-TS/effect/commit/14c57110078f0862b8da5c7a2c5d980f54447484), [`5de7be5`](https://github.com/Effect-TS/effect/commit/5de7be5beca2e963b503e6029dcc3217848187d2), [`b9cb3a9`](https://github.com/Effect-TS/effect/commit/b9cb3a9c9bfdd75536bd70b4e8b557c12d4923ff), [`585fcce`](https://github.com/Effect-TS/effect/commit/585fcce162d0f07a48d7cd984a9b722966fbebbe), [`93b412d`](https://github.com/Effect-TS/effect/commit/93b412d4a9ed762dc9fa5807e51fad0fc78a614a), [`55b26a6`](https://github.com/Effect-TS/effect/commit/55b26a6342b4826f1116e7a1eb660118c274458e), [`a025b12`](https://github.com/Effect-TS/effect/commit/a025b121235ba01cfce8d62a775491880c575561), [`2097739`](https://github.com/Effect-TS/effect/commit/20977393d2383bff709304e81ec7d51cafd57108)]: + - effect@2.4.0 + - @effect/platform@0.46.0 + +## 0.30.6 + +### Patch Changes + +- [#2188](https://github.com/Effect-TS/effect/pull/2188) [`9893d90`](https://github.com/Effect-TS/effect/commit/9893d90f533ac947560a458c639842bf33b5f411) Thanks [@tim-smart](https://github.com/tim-smart)! - support MessagePort in browser workers + +- [#2187](https://github.com/Effect-TS/effect/pull/2187) [`e6d36c0`](https://github.com/Effect-TS/effect/commit/e6d36c0813d836f17eabb6a9c7849baffca12dbf) Thanks [@tim-smart](https://github.com/tim-smart)! - update development dependencies + +- Updated dependencies [[`5ad2eec`](https://github.com/Effect-TS/effect/commit/5ad2eece0280b6db6a749d25cac1dcf6d33659a9), [`e6d36c0`](https://github.com/Effect-TS/effect/commit/e6d36c0813d836f17eabb6a9c7849baffca12dbf)]: + - effect@2.3.8 + - @effect/platform@0.45.6 + +## 0.30.5 + +### Patch Changes + +- Updated dependencies [[`6daf084`](https://github.com/Effect-TS/effect/commit/6daf0845de008772011db8d7c75b7c37a6b4d334)]: + - @effect/platform@0.45.5 + +## 0.30.4 + +### Patch Changes + +- Updated dependencies [[`bc8404d`](https://github.com/Effect-TS/effect/commit/bc8404d54fd42072d200c0399cb39672837afa9f), [`abcb7d9`](https://github.com/Effect-TS/effect/commit/abcb7d983a4a85b43b7175e952f5b331b9019aea), [`2c5cbcd`](https://github.com/Effect-TS/effect/commit/2c5cbcd1161b4f40dab184999291e817314107de), [`6565916`](https://github.com/Effect-TS/effect/commit/6565916ef254bf910e47d25fd0ef55e7cb420241), [`abcb7d9`](https://github.com/Effect-TS/effect/commit/abcb7d983a4a85b43b7175e952f5b331b9019aea)]: + - effect@2.3.7 + - @effect/platform@0.45.4 + +## 0.30.3 + +### Patch Changes + +- Updated dependencies [[`09532a8`](https://github.com/Effect-TS/effect/commit/09532a86b7d0cc23557c89158f0342753dfce4b0)]: + - @effect/platform@0.45.3 + +## 0.30.2 + +### Patch Changes + +- Updated dependencies [[`b1163b2`](https://github.com/Effect-TS/effect/commit/b1163b2bd67b65bafbbb39fc4c67576e5cbaf444), [`b46b869`](https://github.com/Effect-TS/effect/commit/b46b869e59a6da5aa235a9fcc25e1e0d24e9e8f8), [`de1b226`](https://github.com/Effect-TS/effect/commit/de1b226282b5ab6c2809dd93f3bdb066f24a1333), [`a663390`](https://github.com/Effect-TS/effect/commit/a66339090ae7b960f8a8b90a0dcdc505de5aaf3e), [`ff88f80`](https://github.com/Effect-TS/effect/commit/ff88f808c4ed9947a148045849e7410b00acad0a), [`11be07b`](https://github.com/Effect-TS/effect/commit/11be07bf65d82cfdf994cdb9d8ca937f995cb4f0), [`c568645`](https://github.com/Effect-TS/effect/commit/c5686451c87d26382135a1c63b00ef171bb24f62), [`88835e5`](https://github.com/Effect-TS/effect/commit/88835e575a0bfbeff9a3696a332f32192c940e12), [`b415577`](https://github.com/Effect-TS/effect/commit/b415577f6c576073733929c858e5aac27b6d5880), [`ff8046f`](https://github.com/Effect-TS/effect/commit/ff8046f57dfd073eba60ce6d3144ab060fbf93ce), [`44c3b43`](https://github.com/Effect-TS/effect/commit/44c3b43653e64d7e425d39815d8ff405acec9b99)]: + - effect@2.3.6 + - @effect/platform@0.45.2 + +## 0.30.1 + +### Patch Changes + +- [#2133](https://github.com/Effect-TS/effect/pull/2133) [`65895ab`](https://github.com/Effect-TS/effect/commit/65895ab982e0917ac92f0827e387e7cf61be1e69) Thanks [@tim-smart](https://github.com/tim-smart)! - use Schema.TaggedError for worker errors + +- Updated dependencies [[`65895ab`](https://github.com/Effect-TS/effect/commit/65895ab982e0917ac92f0827e387e7cf61be1e69)]: + - @effect/platform@0.45.1 + +## 0.30.0 + +### Minor Changes + +- [#2119](https://github.com/Effect-TS/effect/pull/2119) [`2b62548`](https://github.com/Effect-TS/effect/commit/2b6254845882f399636d24223c483e5489e3cff4) Thanks [@tim-smart](https://github.com/tim-smart)! - add Scope to Http client + + This change adds a scope to the default http client, ensuring connections are + cleaned up if you abort the request at any point. + + Some response helpers have been added to reduce the noise. + + ```ts + import * as Http from "@effect/platform/HttpClient" + import { Effect } from "effect" + + // instead of + Http.request.get("/").pipe( + Http.client.fetchOk(), + Effect.flatMap((_) => _.json), + Effect.scoped + ) + + // you can do + Http.request.get("/").pipe(Http.client.fetchOk(), Http.response.json) + + // other helpers include + Http.response.text + Http.response.stream + Http.response.arrayBuffer + Http.response.urlParamsBody + Http.response.formData + Http.response.schema * Effect + ``` + +### Patch Changes + +- Updated dependencies [[`2b62548`](https://github.com/Effect-TS/effect/commit/2b6254845882f399636d24223c483e5489e3cff4)]: + - @effect/platform@0.45.0 + +## 0.29.8 + +### Patch Changes + +- Updated dependencies [[`b881365`](https://github.com/Effect-TS/effect/commit/b8813650355322ea2fc1fbaa4f846bd87a7a05f3)]: + - effect@2.3.5 + - @effect/platform@0.44.7 + +## 0.29.7 + +### Patch Changes + +- [#2117](https://github.com/Effect-TS/effect/pull/2117) [`bb09ea3`](https://github.com/Effect-TS/effect/commit/bb09ea3d1ecf408a56562624ed1b2534be115c32) Thanks [@tim-smart](https://github.com/tim-smart)! - add XMLHttpRequest client to @effect/platform-browser + +## 0.29.6 + +### Patch Changes + +- Updated dependencies [[`17bda66`](https://github.com/Effect-TS/effect/commit/17bda66431c999a546920c10adb205e6c8bea7d1)]: + - effect@2.3.4 + - @effect/platform@0.44.6 + +## 0.29.5 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform@0.44.5 + +## 0.29.4 + +### Patch Changes + +- Updated dependencies [[`efd41d8`](https://github.com/Effect-TS/effect/commit/efd41d8131c3d90867608969ef7c4eef490eb5e6), [`0f83515`](https://github.com/Effect-TS/effect/commit/0f83515a9c01d13c7c15a3f026e02d22c3c6bb7f), [`0f83515`](https://github.com/Effect-TS/effect/commit/0f83515a9c01d13c7c15a3f026e02d22c3c6bb7f)]: + - effect@2.3.3 + - @effect/platform@0.44.4 + +## 0.29.3 + +### Patch Changes + +- Updated dependencies [[`6654f5f`](https://github.com/Effect-TS/effect/commit/6654f5f0f6b9d97165ede5e04ca16776e2599328), [`2eb11b4`](https://github.com/Effect-TS/effect/commit/2eb11b47752cedf233ef4c4395d9c4efc9b9e180), [`56c09bd`](https://github.com/Effect-TS/effect/commit/56c09bd369279a6a7785209d172739935818cba6), [`71aa5b1`](https://github.com/Effect-TS/effect/commit/71aa5b1c180dcb8b53aefe232d12a97bd06b5447), [`1700af8`](https://github.com/Effect-TS/effect/commit/1700af8af1131602887da721914c8562b6342393)]: + - effect@2.3.2 + - @effect/platform@0.44.3 + +## 0.29.2 + +### Patch Changes + +- Updated dependencies [[`29739dd`](https://github.com/Effect-TS/effect/commit/29739dde8e6232824d49c4c7f8856de245249c5c)]: + - @effect/platform@0.44.2 + +## 0.29.1 + +### Patch Changes + +- Updated dependencies [[`b5a8215`](https://github.com/Effect-TS/effect/commit/b5a8215ee2a97a8865d69ee55ce1b9835948c922)]: + - effect@2.3.1 + - @effect/platform@0.44.1 + +## 0.29.0 + +### Minor Changes + +- [#2006](https://github.com/Effect-TS/effect/pull/2006) [`9a2d1c1`](https://github.com/Effect-TS/effect/commit/9a2d1c1468ea0789b34767ad683da074f061ea9c) Thanks [@github-actions](https://github.com/apps/github-actions)! - With this change we now require a string key to be provided for all tags and renames the dear old `Tag` to `GenericTag`, so when previously you could do: + + ```ts + import { Effect, Context } from "effect" + interface Service { + readonly _: unique symbol + } + const Service = Context.Tag< + Service, + { + number: Effect.Effect + } + >() + ``` + + you are now mandated to do: + + ```ts + import { Effect, Context } from "effect" + interface Service { + readonly _: unique symbol + } + const Service = Context.GenericTag< + Service, + { + number: Effect.Effect + } + >("Service") + ``` + + This makes by default all tags globals and ensures better debuggaility when unexpected errors arise. + + Furthermore we introduce a new way of constructing tags that should be considered the new default: + + ```ts + import { Effect, Context } from "effect" + class Service extends Context.Tag("Service")< + Service, + { + number: Effect.Effect + } + >() {} + + const program = Effect.flatMap(Service, ({ number }) => number).pipe( + Effect.flatMap((_) => Effect.log(`number: ${_}`)) + ) + ``` + + this will use "Service" as the key and will create automatically an opaque identifier (the class) to be used at the type level, it does something similar to the above in a single shot. + +- [#2006](https://github.com/Effect-TS/effect/pull/2006) [`1a77f72`](https://github.com/Effect-TS/effect/commit/1a77f72cdaf43d6cdc91b6060f82832edcdbbcb3) Thanks [@github-actions](https://github.com/apps/github-actions)! - change `Effect` type parameters order from `Effect` to `Effect` + +- [#2006](https://github.com/Effect-TS/effect/pull/2006) [`af47aa3`](https://github.com/Effect-TS/effect/commit/af47aa37196ad542c9c23a4896d8ef98147e1205) Thanks [@github-actions](https://github.com/apps/github-actions)! - move where platform worker spawn function is provided + + With this change, the point in which you provide the spawn function moves closer + to the edge, where you provide platform specific implementation. + + This seperates even more platform concerns from your business logic. Example: + + ```ts + import { Worker } from "@effect/platform" + import { BrowserWorker } from "@effect/platform-browser" + import { Effect } from "effect" + + Worker.makePool({ ... }).pipe( + Effect.provide(BrowserWorker.layer(() => new globalThis.Worker(...))) + ) + ``` + +- [#2006](https://github.com/Effect-TS/effect/pull/2006) [`a34dbdc`](https://github.com/Effect-TS/effect/commit/a34dbdc1552c73c1b612676f262a0c735ce444a7) Thanks [@github-actions](https://github.com/apps/github-actions)! - - Schema: change type parameters order from `Schema` to `Schema` + - Serializable: change type parameters order from `Serializable` to `Serializable` + - Class: change type parameters order from `Class` to `Class` + - PropertySignature: change type parameters order from `PropertySignature` to `PropertySignature` + +- [#2006](https://github.com/Effect-TS/effect/pull/2006) [`9a2d1c1`](https://github.com/Effect-TS/effect/commit/9a2d1c1468ea0789b34767ad683da074f061ea9c) Thanks [@github-actions](https://github.com/apps/github-actions)! - This change enables `Effect.serviceConstants` and `Effect.serviceMembers` to access any constant in the service, not only the effects, namely it is now possible to do: + + ```ts + import { Effect, Context } from "effect" + + class NumberRepo extends Context.TagClass("NumberRepo")< + NumberRepo, + { + readonly numbers: Array + } + >() { + static numbers = Effect.serviceConstants(NumberRepo).numbers + } + ``` + +### Patch Changes + +- Updated dependencies [[`96bcee2`](https://github.com/Effect-TS/effect/commit/96bcee21021aecd8ffd86440a2c9be353c4668e3), [`b1e2086`](https://github.com/Effect-TS/effect/commit/b1e2086ea8bf410e4ad75d71c0760825924e8f9f), [`96bcee2`](https://github.com/Effect-TS/effect/commit/96bcee21021aecd8ffd86440a2c9be353c4668e3), [`c77f635`](https://github.com/Effect-TS/effect/commit/c77f635f8a26ca6d83cb569d911f8eee79033fd9), [`e343a74`](https://github.com/Effect-TS/effect/commit/e343a74843dd9edf879417fa94cb51de7ed5b402), [`acf1894`](https://github.com/Effect-TS/effect/commit/acf1894f45945dbe5c39451e36aabb4b5092f257), [`9a2d1c1`](https://github.com/Effect-TS/effect/commit/9a2d1c1468ea0789b34767ad683da074f061ea9c), [`1a77f72`](https://github.com/Effect-TS/effect/commit/1a77f72cdaf43d6cdc91b6060f82832edcdbbcb3), [`af47aa3`](https://github.com/Effect-TS/effect/commit/af47aa37196ad542c9c23a4896d8ef98147e1205), [`c986f0e`](https://github.com/Effect-TS/effect/commit/c986f0e0ce4d22ba08177ed351152718479ab63c), [`96bcee2`](https://github.com/Effect-TS/effect/commit/96bcee21021aecd8ffd86440a2c9be353c4668e3), [`70dde23`](https://github.com/Effect-TS/effect/commit/70dde238f81125e353fd7bde5fc24ecd8969bf97), [`a34dbdc`](https://github.com/Effect-TS/effect/commit/a34dbdc1552c73c1b612676f262a0c735ce444a7), [`81b7425`](https://github.com/Effect-TS/effect/commit/81b7425320cbbe2a6cf547a3e3ab3549cdba14cf), [`b1e2086`](https://github.com/Effect-TS/effect/commit/b1e2086ea8bf410e4ad75d71c0760825924e8f9f), [`02c3461`](https://github.com/Effect-TS/effect/commit/02c34615d02f91269ea04036d0306fccf4e39e18), [`0e56e99`](https://github.com/Effect-TS/effect/commit/0e56e998ab9815c4d096c239a553cb86a0f99af9), [`8b0ded9`](https://github.com/Effect-TS/effect/commit/8b0ded9f10ba0d96fcb9af24eff2dbd9341f85e3), [`8dd83e8`](https://github.com/Effect-TS/effect/commit/8dd83e854bfcaa6dab876994c5f813dcfb486c28), [`5127afe`](https://github.com/Effect-TS/effect/commit/5127afec1c519e0a3d7460844a9101a96272f29e), [`d75f6fe`](https://github.com/Effect-TS/effect/commit/d75f6fe6499deb0a5ee9ec94af3b5fd4eb03a2d0), [`7356e5c`](https://github.com/Effect-TS/effect/commit/7356e5cc16e9d70f18c02dee1dcb4ad539fd130a), [`3077cde`](https://github.com/Effect-TS/effect/commit/3077cde08a60246821a940964a84dd7f7c8b9f54), [`be19ce0`](https://github.com/Effect-TS/effect/commit/be19ce0b8bdf1fac80bb8d7e0b06a86986b47409), [`4a5d01a`](https://github.com/Effect-TS/effect/commit/4a5d01a409e9b6dd53893e65f8e5c9247f568021), [`78f47ab`](https://github.com/Effect-TS/effect/commit/78f47abfe3cb0a8bbde818b1c5fc603270538b47), [`6361ee2`](https://github.com/Effect-TS/effect/commit/6361ee2e83bdfead24045c3d058a7298efc18113), [`52e5d20`](https://github.com/Effect-TS/effect/commit/52e5d2077582bf51f25861c7139fc920c2c24166), [`c6137ec`](https://github.com/Effect-TS/effect/commit/c6137ec62c6b5542d5062ae1a3c936cb915dee22), [`86f665d`](https://github.com/Effect-TS/effect/commit/86f665d7bd25ba0a3f046a2384798378310dcf0c), [`f5ae081`](https://github.com/Effect-TS/effect/commit/f5ae08195e68e76faeac258c565d79da4e01e7d6), [`4a5d01a`](https://github.com/Effect-TS/effect/commit/4a5d01a409e9b6dd53893e65f8e5c9247f568021), [`60686f5`](https://github.com/Effect-TS/effect/commit/60686f5c38bef1b93a3a0dda9b6596d46aceab03), [`9a2d1c1`](https://github.com/Effect-TS/effect/commit/9a2d1c1468ea0789b34767ad683da074f061ea9c), [`5127afe`](https://github.com/Effect-TS/effect/commit/5127afec1c519e0a3d7460844a9101a96272f29e), [`8ee2931`](https://github.com/Effect-TS/effect/commit/8ee293159b4f7cb7af8558287a0a047f3a69743d), [`6727474`](https://github.com/Effect-TS/effect/commit/672747497490a30d36dd49c06db19aabf09dc7f0), [`5127afe`](https://github.com/Effect-TS/effect/commit/5127afec1c519e0a3d7460844a9101a96272f29e)]: + - effect@2.3.0 + - @effect/platform@0.44.0 + +## 0.28.11 + +### Patch Changes + +- Updated dependencies [[`3ddfdbf`](https://github.com/Effect-TS/effect/commit/3ddfdbf914edea536aef207cec6695f33496258c), [`3ddfdbf`](https://github.com/Effect-TS/effect/commit/3ddfdbf914edea536aef207cec6695f33496258c)]: + - effect@2.2.5 + - @effect/platform@0.43.11 + +## 0.28.10 + +### Patch Changes + +- Updated dependencies [[`d0b911c`](https://github.com/Effect-TS/effect/commit/d0b911c75f284c7aa87f25aa96926e6bde7690d0), [`330e1a4`](https://github.com/Effect-TS/effect/commit/330e1a4e2c1fc0af6c80c80c81dd38c3e50fab78), [`6928a2b`](https://github.com/Effect-TS/effect/commit/6928a2b0bae86a4bdfbece0aa32924207c2d5a70), [`296bc1c`](https://github.com/Effect-TS/effect/commit/296bc1c9d24986d299d2669115d584cb27b73c60)]: + - effect@2.2.4 + - @effect/platform@0.43.10 + +## 0.28.9 + +### Patch Changes + +- Updated dependencies [[`1b841a9`](https://github.com/Effect-TS/effect/commit/1b841a91fed86825cd2867cf1e68e41d8ff26b4e)]: + - @effect/platform@0.43.9 + +## 0.28.8 + +### Patch Changes + +- Updated dependencies [[`32bf796`](https://github.com/Effect-TS/effect/commit/32bf796c3e5db1b2b68e8b1b20db664295991643)]: + - @effect/platform@0.43.8 + +## 0.28.7 + +### Patch Changes + +- Updated dependencies [[`cde08f3`](https://github.com/Effect-TS/effect/commit/cde08f354ed2ff2921d1d98bd539c7d65a2ddd73)]: + - @effect/platform@0.43.7 + +## 0.28.6 + +### Patch Changes + +- Updated dependencies [[`c96bb17`](https://github.com/Effect-TS/effect/commit/c96bb17043e2cec1eaeb319614a4c2904d876beb)]: + - @effect/platform@0.43.6 + +## 0.28.5 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform@0.43.5 + +## 0.28.4 + +### Patch Changes + +- [#1999](https://github.com/Effect-TS/effect/pull/1999) [`78f5921`](https://github.com/Effect-TS/effect/commit/78f59211502ded6fcbe15a49d6fde941cccc9d52) Thanks [@tim-smart](https://github.com/tim-smart)! - ensure forked fibers are interruptible + +- Updated dependencies [[`22794e0`](https://github.com/Effect-TS/effect/commit/22794e0ba00e40281f30a22fa84412003c24877d), [`f73e6c0`](https://github.com/Effect-TS/effect/commit/f73e6c033fb0729a9cfa5eb4bc39f79d3126e247), [`78f5921`](https://github.com/Effect-TS/effect/commit/78f59211502ded6fcbe15a49d6fde941cccc9d52)]: + - effect@2.2.3 + - @effect/platform@0.43.4 + +## 0.28.3 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform@0.43.3 + +## 0.28.2 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform@0.43.2 + +## 0.28.1 + +### Patch Changes + +- Updated dependencies [[`d404561`](https://github.com/Effect-TS/effect/commit/d404561e47ec2fa5f68709a308ee5d2ee959141d), [`7b84a3c`](https://github.com/Effect-TS/effect/commit/7b84a3c7e4b9c8dc02294b0e3cc3ae3becea977b)]: + - effect@2.2.2 + - @effect/platform@0.43.1 + +## 0.28.0 + +### Minor Changes + +- [#1922](https://github.com/Effect-TS/effect/pull/1922) [`62b40e8`](https://github.com/Effect-TS/effect/commit/62b40e8479371d6663c0255aaca56a1ae0d59764) Thanks [@gcanti](https://github.com/gcanti)! - add context tracking to Schema, closes #1873 + +### Patch Changes + +- Updated dependencies [[`84da31f`](https://github.com/Effect-TS/effect/commit/84da31f0643e8651b9d311b30526b1e4edfbdfb8), [`645bea2`](https://github.com/Effect-TS/effect/commit/645bea2551129f94a5b0e38347e28067dee531bb), [`62b40e8`](https://github.com/Effect-TS/effect/commit/62b40e8479371d6663c0255aaca56a1ae0d59764)]: + - effect@2.2.1 + - @effect/platform@0.43.0 + +## 0.27.8 + +### Patch Changes + +- Updated dependencies [[`202befc`](https://github.com/Effect-TS/effect/commit/202befc2ecbeb117c4fa85ef9b12a3d3a48273d2), [`fe05ad7`](https://github.com/Effect-TS/effect/commit/fe05ad7bcb3b88d47800ab69ebf53641023676f1), [`ee4ff8a`](https://github.com/Effect-TS/effect/commit/ee4ff8a943141fcf2877af92c5877ee87a989fb9), [`ee4ff8a`](https://github.com/Effect-TS/effect/commit/ee4ff8a943141fcf2877af92c5877ee87a989fb9), [`ee4ff8a`](https://github.com/Effect-TS/effect/commit/ee4ff8a943141fcf2877af92c5877ee87a989fb9), [`ee4ff8a`](https://github.com/Effect-TS/effect/commit/ee4ff8a943141fcf2877af92c5877ee87a989fb9), [`10df798`](https://github.com/Effect-TS/effect/commit/10df798639e556f9d88265ef7fc3cf8a3bbe3874)]: + - effect@2.2.0 + - @effect/platform@0.42.7 + +## 0.27.7 + +### Patch Changes + +- Updated dependencies [[`21b9edd`](https://github.com/Effect-TS/effect/commit/21b9edde464f7c5624ef54ad1b5e264204a37625)]: + - effect@2.1.2 + - @effect/platform@0.42.6 + +## 0.27.6 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform@0.42.5 + +## 0.27.5 + +### Patch Changes + +- Updated dependencies [[`a222524`](https://github.com/Effect-TS/effect/commit/a2225247e9de2e013d287320790fde88c081dbbd)]: + - effect@2.1.1 + - @effect/platform@0.42.4 + +## 0.27.4 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform@0.42.3 + +## 0.27.3 + +### Patch Changes + +- Updated dependencies [[`05c44b3`](https://github.com/Effect-TS/effect/commit/05c44b30662554dde50b70bad79f13ae895fda02), [`05c44b3`](https://github.com/Effect-TS/effect/commit/05c44b30662554dde50b70bad79f13ae895fda02), [`05c44b3`](https://github.com/Effect-TS/effect/commit/05c44b30662554dde50b70bad79f13ae895fda02), [`05c44b3`](https://github.com/Effect-TS/effect/commit/05c44b30662554dde50b70bad79f13ae895fda02), [`05c44b3`](https://github.com/Effect-TS/effect/commit/05c44b30662554dde50b70bad79f13ae895fda02), [`05c44b3`](https://github.com/Effect-TS/effect/commit/05c44b30662554dde50b70bad79f13ae895fda02)]: + - effect@2.1.0 + - @effect/platform@0.42.2 + +## 0.27.2 + +### Patch Changes + +- Updated dependencies [[`f7f19f6`](https://github.com/Effect-TS/effect/commit/f7f19f66a5fa349baa2412c1f9f15111c437df09)]: + - effect@2.0.5 + - @effect/platform@0.42.1 + +## 0.27.1 + +### Patch Changes + +- Updated dependencies [[`536c1df`](https://github.com/Effect-TS/effect/commit/536c1dfb7833961dfb2fbd6bcd2dbdfa2f208d51), [`8eec87e`](https://github.com/Effect-TS/effect/commit/8eec87e311ce55281a98517e6df0ef103b43e8a8), [`540b294`](https://github.com/Effect-TS/effect/commit/540b2941dd0a81e9688311583ce7e2e140d6e7a5), [`536c1df`](https://github.com/Effect-TS/effect/commit/536c1dfb7833961dfb2fbd6bcd2dbdfa2f208d51), [`a3f96d6`](https://github.com/Effect-TS/effect/commit/a3f96d615b8b3e238dbfa01ef713c87e6f4532be), [`8eec87e`](https://github.com/Effect-TS/effect/commit/8eec87e311ce55281a98517e6df0ef103b43e8a8), [`536c1df`](https://github.com/Effect-TS/effect/commit/536c1dfb7833961dfb2fbd6bcd2dbdfa2f208d51), [`8eec87e`](https://github.com/Effect-TS/effect/commit/8eec87e311ce55281a98517e6df0ef103b43e8a8), [`8eec87e`](https://github.com/Effect-TS/effect/commit/8eec87e311ce55281a98517e6df0ef103b43e8a8), [`25adce7`](https://github.com/Effect-TS/effect/commit/25adce7ae76ce834096dca1ed70a60ad1a349217), [`536c1df`](https://github.com/Effect-TS/effect/commit/536c1dfb7833961dfb2fbd6bcd2dbdfa2f208d51), [`48a3d40`](https://github.com/Effect-TS/effect/commit/48a3d40aed0f923f567b8911dade732ff472d981)]: + - effect@2.0.4 + - @effect/platform@0.42.0 + +## 0.27.0 + +### Minor Changes + +- [#1885](https://github.com/Effect-TS/effect/pull/1885) [`1d3a06b`](https://github.com/Effect-TS/effect/commit/1d3a06bb58ad1ac123ae8f9d42b4345f9c9c53c0) Thanks [@mikearnaldi](https://github.com/mikearnaldi)! - lift worker shutdown to /platform implementation + +### Patch Changes + +- [#1885](https://github.com/Effect-TS/effect/pull/1885) [`1d3a06b`](https://github.com/Effect-TS/effect/commit/1d3a06bb58ad1ac123ae8f9d42b4345f9c9c53c0) Thanks [@mikearnaldi](https://github.com/mikearnaldi)! - Avoid killing all fibers on interrupt + +- Updated dependencies [[`87f7ef2`](https://github.com/Effect-TS/effect/commit/87f7ef28a3c27e2e4f2fcfa465f85bb2a45a3d6b), [`1d3a06b`](https://github.com/Effect-TS/effect/commit/1d3a06bb58ad1ac123ae8f9d42b4345f9c9c53c0), [`1d3a06b`](https://github.com/Effect-TS/effect/commit/1d3a06bb58ad1ac123ae8f9d42b4345f9c9c53c0)]: + - effect@2.0.3 + - @effect/platform@0.41.0 + +## 0.26.4 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform@0.40.4 + +## 0.26.3 + +### Patch Changes + +- Updated dependencies [[`92c0322`](https://github.com/Effect-TS/effect/commit/92c0322a58bf7e5b8dbb602186030839e89df5af)]: + - @effect/platform@0.40.3 + +## 0.26.2 + +### Patch Changes + +- Updated dependencies [[`4c90c54`](https://github.com/Effect-TS/effect/commit/4c90c54d87c91f75f3ad114926cdf3b0c25df091), [`d5a1949`](https://github.com/Effect-TS/effect/commit/d5a19499aac7c1d147674a35ac69992177c7536c), [`d3d3bda`](https://github.com/Effect-TS/effect/commit/d3d3bda74c794153def9027e0c40896e72cd5d14)]: + - @effect/platform@0.40.2 + - effect@2.0.2 + +## 0.26.1 + +### Patch Changes + +- Updated dependencies [[`16bd87d`](https://github.com/Effect-TS/effect/commit/16bd87d32611b966dc42ea4fc979764f97a49071)]: + - effect@2.0.1 + - @effect/platform@0.40.1 + +## 0.26.0 + +### Minor Changes + +- [`d0471ca`](https://github.com/Effect-TS/effect/commit/d0471ca7b544746674b9e1750202da72b0a21233) Thanks [@mikearnaldi](https://github.com/mikearnaldi)! - Switch to monorepo structure + +### Patch Changes + +- [`d987daa`](https://github.com/Effect-TS/effect/commit/d987daafaddd43b6ade74916a08236c19ea0a9fa) Thanks [@mikearnaldi](https://github.com/mikearnaldi)! - Switch effect dependency to caret + +- Updated dependencies [[`d987daa`](https://github.com/Effect-TS/effect/commit/d987daafaddd43b6ade74916a08236c19ea0a9fa), [`7b5eaa3`](https://github.com/Effect-TS/effect/commit/7b5eaa3838c79bf4bdccf91b94d61bbc38a2ec95), [`0724211`](https://github.com/Effect-TS/effect/commit/072421149c36010748ff6b6ee19c15c6cffefe09), [`9f2bc5a`](https://github.com/Effect-TS/effect/commit/9f2bc5a19e0b678a0a85e84daac290922b0fd57d), [`04fb8b4`](https://github.com/Effect-TS/effect/commit/04fb8b428b19bba85a2c79910c5e363340d074e7), [`d0471ca`](https://github.com/Effect-TS/effect/commit/d0471ca7b544746674b9e1750202da72b0a21233), [`bcf0900`](https://github.com/Effect-TS/effect/commit/bcf0900b58f449262556f80bff21e771a37272aa), [`6299b84`](https://github.com/Effect-TS/effect/commit/6299b84c11e5d1fe79fa538df8935018c7613747), [`c0aeb5e`](https://github.com/Effect-TS/effect/commit/c0aeb5e302869bcd7d7627f8cc5b630d07c12d10), [`693b8f3`](https://github.com/Effect-TS/effect/commit/693b8f3a3dfd43ae61f0d9292cdf356be7329f2f)]: + - @effect/platform@0.40.0 + - effect@2.0.0 + +## 0.25.0 + +### Minor Changes + +- [#372](https://github.com/Effect-TS/platform/pull/372) [`15784c9`](https://github.com/Effect-TS/platform/commit/15784c920dcae40f328bb45ac850987135207365) Thanks [@tim-smart](https://github.com/tim-smart)! - update dependencies + +- [#373](https://github.com/Effect-TS/platform/pull/373) [`b042ba5`](https://github.com/Effect-TS/platform/commit/b042ba5ae78a1eed592e543c233fe3040d6a60da) Thanks [@tim-smart](https://github.com/tim-smart)! - update effect + +### Patch Changes + +- Updated dependencies [[`5d5f62b`](https://github.com/Effect-TS/platform/commit/5d5f62b03ffdbca0a986d968e1dbb45886dfa827), [`15784c9`](https://github.com/Effect-TS/platform/commit/15784c920dcae40f328bb45ac850987135207365), [`b042ba5`](https://github.com/Effect-TS/platform/commit/b042ba5ae78a1eed592e543c233fe3040d6a60da), [`49fb154`](https://github.com/Effect-TS/platform/commit/49fb15439f18701321db8ded839243b9dd8de71a)]: + - @effect/platform@0.39.0 + +## 0.24.0 + +### Minor Changes + +- [#367](https://github.com/Effect-TS/platform/pull/367) [`7d1584b`](https://github.com/Effect-TS/platform/commit/7d1584b23d464651c206201ff304c6eb4bebfc3a) Thanks [@tim-smart](https://github.com/tim-smart)! - update effect + +### Patch Changes + +- Updated dependencies [[`7d1584b`](https://github.com/Effect-TS/platform/commit/7d1584b23d464651c206201ff304c6eb4bebfc3a)]: + - @effect/platform@0.38.0 + +## 0.23.8 + +### Patch Changes + +- Updated dependencies [[`e2c545a`](https://github.com/Effect-TS/platform/commit/e2c545a328c2bccbba661540a8835b10bce4b438), [`1d6bf73`](https://github.com/Effect-TS/platform/commit/1d6bf730dad0a6bbb282f436ec7d5870de76ca3a), [`3351136`](https://github.com/Effect-TS/platform/commit/335113601c238104eb2e331d26b5e463bde80dff)]: + - @effect/platform@0.37.8 + +## 0.23.7 + +### Patch Changes + +- Updated dependencies [[`df3af6b`](https://github.com/Effect-TS/platform/commit/df3af6be61572bab15004bbca2c5739d8206f3c3)]: + - @effect/platform@0.37.7 + +## 0.23.6 + +### Patch Changes + +- Updated dependencies [[`6dbc587`](https://github.com/Effect-TS/platform/commit/6dbc587868d2703ad9a4c9995cb9dacdfc29c364), [`6dbc587`](https://github.com/Effect-TS/platform/commit/6dbc587868d2703ad9a4c9995cb9dacdfc29c364)]: + - @effect/platform@0.37.6 + +## 0.23.5 + +### Patch Changes + +- [#354](https://github.com/Effect-TS/platform/pull/354) [`190bc84`](https://github.com/Effect-TS/platform/commit/190bc84b137a729a38b6812e220085b3d12cb124) Thanks [@tim-smart](https://github.com/tim-smart)! - add Layer support to SerializedWorker + +- Updated dependencies [[`190bc84`](https://github.com/Effect-TS/platform/commit/190bc84b137a729a38b6812e220085b3d12cb124)]: + - @effect/platform@0.37.5 + +## 0.23.4 + +### Patch Changes + +- [#352](https://github.com/Effect-TS/platform/pull/352) [`1c02a35`](https://github.com/Effect-TS/platform/commit/1c02a35df2f34601b547e17ddeab98236e10f77d) Thanks [@tim-smart](https://github.com/tim-smart)! - interrupt all fibers on worker interrupt + +- Updated dependencies [[`1c02a35`](https://github.com/Effect-TS/platform/commit/1c02a35df2f34601b547e17ddeab98236e10f77d), [`1c02a35`](https://github.com/Effect-TS/platform/commit/1c02a35df2f34601b547e17ddeab98236e10f77d)]: + - @effect/platform@0.37.4 + +## 0.23.3 + +### Patch Changes + +- [#350](https://github.com/Effect-TS/platform/pull/350) [`b30e5e3`](https://github.com/Effect-TS/platform/commit/b30e5e3874f22037f92253037fff6952f537ee40) Thanks [@tim-smart](https://github.com/tim-smart)! - add decode option to worker runner + +- Updated dependencies [[`b30e5e3`](https://github.com/Effect-TS/platform/commit/b30e5e3874f22037f92253037fff6952f537ee40)]: + - @effect/platform@0.37.3 + +## 0.23.2 + +### Patch Changes + +- [#348](https://github.com/Effect-TS/platform/pull/348) [`28edc60`](https://github.com/Effect-TS/platform/commit/28edc60d2fcd30160529c677a9ffd786775e534b) Thanks [@tim-smart](https://github.com/tim-smart)! - add layer worker runner apis + +- Updated dependencies [[`28edc60`](https://github.com/Effect-TS/platform/commit/28edc60d2fcd30160529c677a9ffd786775e534b)]: + - @effect/platform@0.37.2 + +## 0.23.1 + +### Patch Changes + +- [#344](https://github.com/Effect-TS/platform/pull/344) [`5b7cdbd`](https://github.com/Effect-TS/platform/commit/5b7cdbdf8ded48903a9f39df800fd7a22f73f0f7) Thanks [@tim-smart](https://github.com/tim-smart)! - support error and output transfers in worker runners + +- [#344](https://github.com/Effect-TS/platform/pull/344) [`5b7cdbd`](https://github.com/Effect-TS/platform/commit/5b7cdbdf8ded48903a9f39df800fd7a22f73f0f7) Thanks [@tim-smart](https://github.com/tim-smart)! - support initialMessage in workers + +- [#344](https://github.com/Effect-TS/platform/pull/344) [`5b7cdbd`](https://github.com/Effect-TS/platform/commit/5b7cdbdf8ded48903a9f39df800fd7a22f73f0f7) Thanks [@tim-smart](https://github.com/tim-smart)! - add Schema transforms to Transferable + +- [#344](https://github.com/Effect-TS/platform/pull/344) [`5b7cdbd`](https://github.com/Effect-TS/platform/commit/5b7cdbdf8ded48903a9f39df800fd7a22f73f0f7) Thanks [@tim-smart](https://github.com/tim-smart)! - make worker encoding return Effects + +- Updated dependencies [[`5b7cdbd`](https://github.com/Effect-TS/platform/commit/5b7cdbdf8ded48903a9f39df800fd7a22f73f0f7), [`5b7cdbd`](https://github.com/Effect-TS/platform/commit/5b7cdbdf8ded48903a9f39df800fd7a22f73f0f7), [`5b7cdbd`](https://github.com/Effect-TS/platform/commit/5b7cdbdf8ded48903a9f39df800fd7a22f73f0f7), [`5b7cdbd`](https://github.com/Effect-TS/platform/commit/5b7cdbdf8ded48903a9f39df800fd7a22f73f0f7)]: + - @effect/platform@0.37.1 + +## 0.23.0 + +### Minor Changes + +- [#341](https://github.com/Effect-TS/platform/pull/341) [`649f57f`](https://github.com/Effect-TS/platform/commit/649f57fdf557eed5f8405a4a4553dfc47fd8d4b1) Thanks [@tim-smart](https://github.com/tim-smart)! - use peer deps for /platform-\* + +### Patch Changes + +- Updated dependencies [[`649f57f`](https://github.com/Effect-TS/platform/commit/649f57fdf557eed5f8405a4a4553dfc47fd8d4b1), [`649f57f`](https://github.com/Effect-TS/platform/commit/649f57fdf557eed5f8405a4a4553dfc47fd8d4b1)]: + - @effect/platform@0.37.0 + +## 0.22.2 + +### Patch Changes + +- Updated dependencies [[`7eaa8e5`](https://github.com/Effect-TS/platform/commit/7eaa8e52b18d408688e7b4909bcf016b0c04e80a), [`7eaa8e5`](https://github.com/Effect-TS/platform/commit/7eaa8e52b18d408688e7b4909bcf016b0c04e80a), [`7eaa8e5`](https://github.com/Effect-TS/platform/commit/7eaa8e52b18d408688e7b4909bcf016b0c04e80a)]: + - @effect/platform@0.36.0 + +## 0.22.1 + +### Patch Changes + +- [#335](https://github.com/Effect-TS/platform/pull/335) [`4f0166e`](https://github.com/Effect-TS/platform/commit/4f0166ee2241bd9b71739c98d428b5809313e46e) Thanks [@tim-smart](https://github.com/tim-smart)! - add SerializedWorker + +- Updated dependencies [[`4f0166e`](https://github.com/Effect-TS/platform/commit/4f0166ee2241bd9b71739c98d428b5809313e46e), [`4f0166e`](https://github.com/Effect-TS/platform/commit/4f0166ee2241bd9b71739c98d428b5809313e46e)]: + - @effect/platform@0.35.0 + +## 0.22.0 + +### Minor Changes + +- [#331](https://github.com/Effect-TS/platform/pull/331) [`db1ca18`](https://github.com/Effect-TS/platform/commit/db1ca18725f9dd4be1c36ddc80faa3aa53c10eb7) Thanks [@tim-smart](https://github.com/tim-smart)! - update effect + +### Patch Changes + +- Updated dependencies [[`db1ca18`](https://github.com/Effect-TS/platform/commit/db1ca18725f9dd4be1c36ddc80faa3aa53c10eb7)]: + - @effect/platform@0.34.0 + +## 0.21.1 + +### Patch Changes + +- Updated dependencies [[`162aa91`](https://github.com/Effect-TS/platform/commit/162aa915934112983c543a6be2a9d7091b86fac9)]: + - @effect/platform@0.33.1 + +## 0.21.0 + +### Minor Changes + +- [#321](https://github.com/Effect-TS/platform/pull/321) [`16a5bca`](https://github.com/Effect-TS/platform/commit/16a5bca2bd4aed570ce95233a0e47350010d031f) Thanks [@tim-smart](https://github.com/tim-smart)! - update effect + +### Patch Changes + +- Updated dependencies [[`425365e`](https://github.com/Effect-TS/platform/commit/425365ebc40c52a6e2a4bff865c3a982ce74f4ed), [`425365e`](https://github.com/Effect-TS/platform/commit/425365ebc40c52a6e2a4bff865c3a982ce74f4ed), [`16a5bca`](https://github.com/Effect-TS/platform/commit/16a5bca2bd4aed570ce95233a0e47350010d031f)]: + - @effect/platform@0.33.0 + +## 0.20.3 + +### Patch Changes + +- [#316](https://github.com/Effect-TS/platform/pull/316) [`19431f0`](https://github.com/Effect-TS/platform/commit/19431f0b5ccb8beacd502de876962f55cabf6ed4) Thanks [@tim-smart](https://github.com/tim-smart)! - add logging to runMain + +## 0.20.2 + +### Patch Changes + +- Updated dependencies [[`cc1f588`](https://github.com/Effect-TS/platform/commit/cc1f5886bf4188e0128b64b9e2a67f789680cab0)]: + - @effect/platform@0.32.2 + +## 0.20.1 + +### Patch Changes + +- [#310](https://github.com/Effect-TS/platform/pull/310) [`14239fb`](https://github.com/Effect-TS/platform/commit/14239fb11ae45db1a02d9ba883d0412a9c9e6343) Thanks [@tim-smart](https://github.com/tim-smart)! - update effect + +- Updated dependencies [[`14239fb`](https://github.com/Effect-TS/platform/commit/14239fb11ae45db1a02d9ba883d0412a9c9e6343)]: + - @effect/platform@0.32.1 + +## 0.20.0 + +### Minor Changes + +- [#307](https://github.com/Effect-TS/platform/pull/307) [`746f969`](https://github.com/Effect-TS/platform/commit/746f9692e2f7133dcb413e0eea08ac7b6b97a9bd) Thanks [@tim-smart](https://github.com/tim-smart)! - update effect + +### Patch Changes + +- Updated dependencies [[`746f969`](https://github.com/Effect-TS/platform/commit/746f9692e2f7133dcb413e0eea08ac7b6b97a9bd), [`92e56a1`](https://github.com/Effect-TS/platform/commit/92e56a1f844f28f26621a1887cc4da045039066d), [`92e56a1`](https://github.com/Effect-TS/platform/commit/92e56a1f844f28f26621a1887cc4da045039066d)]: + - @effect/platform@0.32.0 + +## 0.19.2 + +### Patch Changes + +- Updated dependencies [[`7a46ec6`](https://github.com/Effect-TS/platform/commit/7a46ec679e2d4718919c407d0c6c5f0fdc35e62d)]: + - @effect/platform@0.31.2 + +## 0.19.1 + +### Patch Changes + +- Updated dependencies [[`b712491`](https://github.com/Effect-TS/platform/commit/b71249168eb4623de8dbd28cd0102be688f5caa3)]: + - @effect/platform@0.31.1 + +## 0.19.0 + +### Minor Changes + +- [#291](https://github.com/Effect-TS/platform/pull/291) [`5a677f1`](https://github.com/Effect-TS/platform/commit/5a677f1062d7373e21839dfa51db26beef15dca4) Thanks [@tim-smart](https://github.com/tim-smart)! - update effect + +### Patch Changes + +- Updated dependencies [[`5a677f1`](https://github.com/Effect-TS/platform/commit/5a677f1062d7373e21839dfa51db26beef15dca4), [`624855f`](https://github.com/Effect-TS/platform/commit/624855f635162b2c1232429253477d0805e02657)]: + - @effect/platform@0.31.0 + +## 0.18.6 + +### Patch Changes + +- Updated dependencies [[`d5d0932`](https://github.com/Effect-TS/platform/commit/d5d093219cde4f51afb9251d9ba4270fc70be0c1)]: + - @effect/platform@0.30.6 + +## 0.18.5 + +### Patch Changes + +- Updated dependencies [[`36e449c`](https://github.com/Effect-TS/platform/commit/36e449c95fab80dc54505cef2071dcbecce35b4f)]: + - @effect/platform@0.30.5 + +## 0.18.4 + +### Patch Changes + +- [#275](https://github.com/Effect-TS/platform/pull/275) [`e28989e`](https://github.com/Effect-TS/platform/commit/e28989ebd1813cec7ce68f7dd8718f2254e05cad) Thanks [@tim-smart](https://github.com/tim-smart)! - add stack to WorkerError + +- Updated dependencies [[`e28989e`](https://github.com/Effect-TS/platform/commit/e28989ebd1813cec7ce68f7dd8718f2254e05cad)]: + - @effect/platform@0.30.4 + +## 0.18.3 + +### Patch Changes + +- [#273](https://github.com/Effect-TS/platform/pull/273) [`589cd44`](https://github.com/Effect-TS/platform/commit/589cd4440d48f42d8bf19b72d1c2996f68ba56bf) Thanks [@tim-smart](https://github.com/tim-smart)! - use removeEventListener over signal + +- [#272](https://github.com/Effect-TS/platform/pull/272) [`1a055ac`](https://github.com/Effect-TS/platform/commit/1a055ac959faf12e9c57768b20babea12b1f7d2d) Thanks [@tim-smart](https://github.com/tim-smart)! - add WorkerError to send api + +- Updated dependencies [[`1a055ac`](https://github.com/Effect-TS/platform/commit/1a055ac959faf12e9c57768b20babea12b1f7d2d)]: + - @effect/platform@0.30.3 + +## 0.18.2 + +### Patch Changes + +- Updated dependencies [[`3257fd5`](https://github.com/Effect-TS/platform/commit/3257fd52016af5a38c135de5f0aa33aac7de2538)]: + - @effect/platform@0.30.2 + +## 0.18.1 + +### Patch Changes + +- Updated dependencies [[`58f5ccc`](https://github.com/Effect-TS/platform/commit/58f5ccc07d74abe6820debc0179665e5ef76b5c4)]: + - @effect/platform@0.30.1 + +## 0.18.0 + +### Minor Changes + +- [#267](https://github.com/Effect-TS/platform/pull/267) [`3d38b40`](https://github.com/Effect-TS/platform/commit/3d38b40a939e32c6c0e8b62dd53a844a6f389182) Thanks [@tim-smart](https://github.com/tim-smart)! - update effect + +### Patch Changes + +- [#265](https://github.com/Effect-TS/platform/pull/265) [`6341f23`](https://github.com/Effect-TS/platform/commit/6341f23a6354c348dc8559a058f63d926164eeab) Thanks [@tim-smart](https://github.com/tim-smart)! - ensure clipboard is accessed lazily + +- Updated dependencies [[`3d38b40`](https://github.com/Effect-TS/platform/commit/3d38b40a939e32c6c0e8b62dd53a844a6f389182)]: + - @effect/platform@0.30.0 + +## 0.17.1 + +### Patch Changes + +- Updated dependencies [[`2bbe692`](https://github.com/Effect-TS/platform/commit/2bbe6928aa5e6929e58877ba236547310bca7e2b)]: + - @effect/platform@0.29.1 + +## 0.17.0 + +### Minor Changes + +- [#250](https://github.com/Effect-TS/platform/pull/250) [`6e18090`](https://github.com/Effect-TS/platform/commit/6e18090db4686cd5564ab9dc3d8771d7b3ad97fa) Thanks [@tim-smart](https://github.com/tim-smart)! - updated FormData model and apis + +### Patch Changes + +- Updated dependencies [[`6e18090`](https://github.com/Effect-TS/platform/commit/6e18090db4686cd5564ab9dc3d8771d7b3ad97fa)]: + - @effect/platform@0.29.0 + +## 0.16.4 + +### Patch Changes + +- Updated dependencies [[`8f5e6a2`](https://github.com/Effect-TS/platform/commit/8f5e6a2f2ced4408b0b311b0456828855e1cb958)]: + - @effect/platform@0.28.4 + +## 0.16.3 + +### Patch Changes + +- Updated dependencies [[`9f79c1f`](https://github.com/Effect-TS/platform/commit/9f79c1f5278e60b3bcbd59f08e20189bcb25a84e)]: + - @effect/platform@0.28.3 + +## 0.16.2 + +### Patch Changes + +- Updated dependencies [[`fea76da`](https://github.com/Effect-TS/platform/commit/fea76da05190a65912911bd5b6f9cc0bef3b2edc)]: + - @effect/platform@0.28.2 + +## 0.16.1 + +### Patch Changes + +- Updated dependencies [[`43d2e29`](https://github.com/Effect-TS/platform/commit/43d2e2984fe88b39e907f45f089206ed88ad52d1)]: + - @effect/platform@0.28.1 + +## 0.16.0 + +### Minor Changes + +- [#251](https://github.com/Effect-TS/platform/pull/251) [`05fef78`](https://github.com/Effect-TS/platform/commit/05fef784ac975059fb6335576feadc7f34644314) Thanks [@fubhy](https://github.com/fubhy)! - Re-added exports for http module + +### Patch Changes + +- Updated dependencies [[`05fef78`](https://github.com/Effect-TS/platform/commit/05fef784ac975059fb6335576feadc7f34644314)]: + - @effect/platform@0.28.0 + +## 0.15.4 + +### Patch Changes + +- Updated dependencies [[`8a4b1c1`](https://github.com/Effect-TS/platform/commit/8a4b1c14808d9815eb93a5b10d8a5b26c4dd027b)]: + - @effect/platform@0.27.4 + +## 0.15.3 + +### Patch Changes + +- [#243](https://github.com/Effect-TS/platform/pull/243) [`1ac0a42`](https://github.com/Effect-TS/platform/commit/1ac0a4208184ef1d23d5ad41a7f0e32bc4d80d85) Thanks [@tim-smart](https://github.com/tim-smart)! - fix worker interruption + +- Updated dependencies [[`1ac0a42`](https://github.com/Effect-TS/platform/commit/1ac0a4208184ef1d23d5ad41a7f0e32bc4d80d85)]: + - @effect/platform@0.27.3 + +## 0.15.2 + +### Patch Changes + +- Updated dependencies [[`e2aa7cd`](https://github.com/Effect-TS/platform/commit/e2aa7cd606a735809fbf79327cfebc009e89d84d)]: + - @effect/platform@0.27.2 + +## 0.15.1 + +### Patch Changes + +- [#239](https://github.com/Effect-TS/platform/pull/239) [`4d94b9d`](https://github.com/Effect-TS/platform/commit/4d94b9d30adba2bf4f6f6e1d4cd735e6362667c5) Thanks [@tim-smart](https://github.com/tim-smart)! - update effect + +- Updated dependencies [[`4d94b9d`](https://github.com/Effect-TS/platform/commit/4d94b9d30adba2bf4f6f6e1d4cd735e6362667c5)]: + - @effect/platform@0.27.1 + +## 0.15.0 + +### Minor Changes + +- [#237](https://github.com/Effect-TS/platform/pull/237) [`1f79ed6`](https://github.com/Effect-TS/platform/commit/1f79ed6b4d2ee9ae2b59c4536854566c579e77c4) Thanks [@tim-smart](https://github.com/tim-smart)! - update effect + +### Patch Changes + +- Updated dependencies [[`1f79ed6`](https://github.com/Effect-TS/platform/commit/1f79ed6b4d2ee9ae2b59c4536854566c579e77c4)]: + - @effect/platform@0.27.0 + +## 0.14.8 + +### Patch Changes + +- [#235](https://github.com/Effect-TS/platform/pull/235) [`6e14c02`](https://github.com/Effect-TS/platform/commit/6e14c02db668f380bb92f19037685fe40592a8fe) Thanks [@tim-smart](https://github.com/tim-smart)! - fix for hanging worker shutdown + +- Updated dependencies [[`6e14c02`](https://github.com/Effect-TS/platform/commit/6e14c02db668f380bb92f19037685fe40592a8fe)]: + - @effect/platform@0.26.7 + +## 0.14.7 + +### Patch Changes + +- [#233](https://github.com/Effect-TS/platform/pull/233) [`71947e0`](https://github.com/Effect-TS/platform/commit/71947e0e0aa9dccf9aad6f63dd98a6b6c89f23b4) Thanks [@tim-smart](https://github.com/tim-smart)! - fix worker scope hanging on close + +- Updated dependencies [[`71947e0`](https://github.com/Effect-TS/platform/commit/71947e0e0aa9dccf9aad6f63dd98a6b6c89f23b4)]: + - @effect/platform@0.26.6 + +## 0.14.6 + +### Patch Changes + +- [#231](https://github.com/Effect-TS/platform/pull/231) [`a3cbba4`](https://github.com/Effect-TS/platform/commit/a3cbba4a0fa0f1ef99a6d7e54f5ab46c6813ef00) Thanks [@tim-smart](https://github.com/tim-smart)! - add onCreate and broadcast to pool options + +- Updated dependencies [[`a3cbba4`](https://github.com/Effect-TS/platform/commit/a3cbba4a0fa0f1ef99a6d7e54f5ab46c6813ef00)]: + - @effect/platform@0.26.5 + +## 0.14.5 + +### Patch Changes + +- [#229](https://github.com/Effect-TS/platform/pull/229) [`4661a8c`](https://github.com/Effect-TS/platform/commit/4661a8c63a13cc6630d5f3cbac90f4ff1d096e09) Thanks [@tim-smart](https://github.com/tim-smart)! - type worker runner success as never + +- [#229](https://github.com/Effect-TS/platform/pull/229) [`4661a8c`](https://github.com/Effect-TS/platform/commit/4661a8c63a13cc6630d5f3cbac90f4ff1d096e09) Thanks [@tim-smart](https://github.com/tim-smart)! - disable worker pool scaling + +- Updated dependencies [[`4661a8c`](https://github.com/Effect-TS/platform/commit/4661a8c63a13cc6630d5f3cbac90f4ff1d096e09), [`4661a8c`](https://github.com/Effect-TS/platform/commit/4661a8c63a13cc6630d5f3cbac90f4ff1d096e09)]: + - @effect/platform@0.26.4 + +## 0.14.4 + +### Patch Changes + +- Updated dependencies [[`abb6baa`](https://github.com/Effect-TS/platform/commit/abb6baa61346580f97d2ab91b84a7342b5becc60)]: + - @effect/platform@0.26.3 + +## 0.14.3 + +### Patch Changes + +- [#221](https://github.com/Effect-TS/platform/pull/221) [`3e57e82`](https://github.com/Effect-TS/platform/commit/3e57e8224bf7b4474b21ef1dc25db13107d9b635) Thanks [@tim-smart](https://github.com/tim-smart)! - export WorkerRunner layers + +## 0.14.2 + +### Patch Changes + +- Updated dependencies [[`f37f58c`](https://github.com/Effect-TS/platform/commit/f37f58ca21c1d5dfedc40c01cde0ffbc954d7e32)]: + - @effect/platform@0.26.2 + +## 0.14.1 + +### Patch Changes + +- Updated dependencies [[`7471ac1`](https://github.com/Effect-TS/platform/commit/7471ac139f3c6867cd0d228ec54e88abd1384f5c)]: + - @effect/platform@0.26.1 + +## 0.14.0 + +### Minor Changes + +- [#215](https://github.com/Effect-TS/platform/pull/215) [`59da2a6`](https://github.com/Effect-TS/platform/commit/59da2a6877e219b2ca0433aeeecab4ad7487816b) Thanks [@tim-smart](https://github.com/tim-smart)! - seperate request processing in http client + +### Patch Changes + +- Updated dependencies [[`59da2a6`](https://github.com/Effect-TS/platform/commit/59da2a6877e219b2ca0433aeeecab4ad7487816b)]: + - @effect/platform@0.26.0 + +## 0.13.1 + +### Patch Changes + +- [#213](https://github.com/Effect-TS/platform/pull/213) [`38a49eb`](https://github.com/Effect-TS/platform/commit/38a49eb6ea99ef773007a98ec262898207c8f3c7) Thanks [@tim-smart](https://github.com/tim-smart)! - update effect + +- Updated dependencies [[`38a49eb`](https://github.com/Effect-TS/platform/commit/38a49eb6ea99ef773007a98ec262898207c8f3c7)]: + - @effect/platform@0.25.1 + +## 0.13.0 + +### Minor Changes + +- [#211](https://github.com/Effect-TS/platform/pull/211) [`9ec45cb`](https://github.com/Effect-TS/platform/commit/9ec45cba6b7d5016079ccad9357934f12afe8750) Thanks [@tim-smart](https://github.com/tim-smart)! - update effect + +### Patch Changes + +- Updated dependencies [[`9ec45cb`](https://github.com/Effect-TS/platform/commit/9ec45cba6b7d5016079ccad9357934f12afe8750)]: + - @effect/platform@0.25.0 + +## 0.12.0 + +### Minor Changes + +- [#209](https://github.com/Effect-TS/platform/pull/209) [`9c51aa1`](https://github.com/Effect-TS/platform/commit/9c51aa18beb7fd34023863ca069d3dde372765d8) Thanks [@tim-smart](https://github.com/tim-smart)! - update effect + +### Patch Changes + +- Updated dependencies [[`9c51aa1`](https://github.com/Effect-TS/platform/commit/9c51aa18beb7fd34023863ca069d3dde372765d8)]: + - @effect/platform@0.24.0 + +## 0.11.1 + +### Patch Changes + +- Updated dependencies [[`b47639b`](https://github.com/Effect-TS/platform/commit/b47639b1df021beb075469921e9ef7a08c174555), [`41f8a65`](https://github.com/Effect-TS/platform/commit/41f8a650238bfbac5b8e18d58a431c3605b71aa5)]: + - @effect/platform@0.23.1 + +## 0.11.0 + +### Minor Changes + +- [#204](https://github.com/Effect-TS/platform/pull/204) [`ee0c08f`](https://github.com/Effect-TS/platform/commit/ee0c08fd9828eae32696da1bde33d50a3ad9edf3) Thanks [@tim-smart](https://github.com/tim-smart)! - update effect + +### Patch Changes + +- Updated dependencies [[`ee0c08f`](https://github.com/Effect-TS/platform/commit/ee0c08fd9828eae32696da1bde33d50a3ad9edf3)]: + - @effect/platform@0.23.0 + +## 0.10.2 + +### Patch Changes + +- [#194](https://github.com/Effect-TS/platform/pull/194) [`79b71d8`](https://github.com/Effect-TS/platform/commit/79b71d8cb3aa6520b2dcb7930850b423174e04b2) Thanks [@tim-smart](https://github.com/tim-smart)! - add Worker & WorkerRunner modules + +- Updated dependencies [[`79b71d8`](https://github.com/Effect-TS/platform/commit/79b71d8cb3aa6520b2dcb7930850b423174e04b2)]: + - @effect/platform@0.22.1 + +## 0.10.1 + +### Patch Changes + +- Updated dependencies [[`1e94b15`](https://github.com/Effect-TS/platform/commit/1e94b1588e51df20f9c4fc4871b246048751506c)]: + - @effect/platform@0.22.0 + +## 0.10.0 + +### Minor Changes + +- [#193](https://github.com/Effect-TS/platform/pull/193) [`9ec4b1d`](https://github.com/Effect-TS/platform/commit/9ec4b1d284caa1c4f19a58c46ed7c25fb10d39a5) Thanks [@tim-smart](https://github.com/tim-smart)! - update effect + +### Patch Changes + +- [#191](https://github.com/Effect-TS/platform/pull/191) [`2711aea`](https://github.com/Effect-TS/platform/commit/2711aea855936c82c282e61fbc6d2f1a1ab6778a) Thanks [@tim-smart](https://github.com/tim-smart)! - update effect + +- Updated dependencies [[`2711aea`](https://github.com/Effect-TS/platform/commit/2711aea855936c82c282e61fbc6d2f1a1ab6778a), [`9ec4b1d`](https://github.com/Effect-TS/platform/commit/9ec4b1d284caa1c4f19a58c46ed7c25fb10d39a5)]: + - @effect/platform@0.21.0 + +## 0.9.0 + +### Minor Changes + +- [#189](https://github.com/Effect-TS/platform/pull/189) [`b07f8cd`](https://github.com/Effect-TS/platform/commit/b07f8cd50ef44d577aa981a532025aedb364df13) Thanks [@tim-smart](https://github.com/tim-smart)! - update effect + +### Patch Changes + +- Updated dependencies [[`b07f8cd`](https://github.com/Effect-TS/platform/commit/b07f8cd50ef44d577aa981a532025aedb364df13)]: + - @effect/platform@0.20.0 + +## 0.8.0 + +### Minor Changes + +- [#184](https://github.com/Effect-TS/platform/pull/184) [`903b599`](https://github.com/Effect-TS/platform/commit/903b5995bb407c399846e6b75e47e53098b2c80d) Thanks [@tim-smart](https://github.com/tim-smart)! - update effect + +### Patch Changes + +- Updated dependencies [[`903b599`](https://github.com/Effect-TS/platform/commit/903b5995bb407c399846e6b75e47e53098b2c80d), [`a3bcda4`](https://github.com/Effect-TS/platform/commit/a3bcda4c2c6655ab86769cca60bece5eb64f866e)]: + - @effect/platform@0.19.0 + +## 0.7.8 + +### Patch Changes + +- [#181](https://github.com/Effect-TS/platform/pull/181) [`d0d5458`](https://github.com/Effect-TS/platform/commit/d0d545869baeb91d594804ab759713f424eb7a11) Thanks [@tim-smart](https://github.com/tim-smart)! - fix error type exports + +## 0.7.7 + +### Patch Changes + +- [#179](https://github.com/Effect-TS/platform/pull/179) [`843488f`](https://github.com/Effect-TS/platform/commit/843488f79b253518f131693faf2955f5c795a1bc) Thanks [@tim-smart](https://github.com/tim-smart)! - update effect + +- Updated dependencies [[`843488f`](https://github.com/Effect-TS/platform/commit/843488f79b253518f131693faf2955f5c795a1bc)]: + - @effect/platform@0.18.7 + +## 0.7.6 + +### Patch Changes + +- Updated dependencies [[`7e4e2a5`](https://github.com/Effect-TS/platform/commit/7e4e2a5d815c677e4eb6adb2c6e9369414a79384), [`d1c2b38`](https://github.com/Effect-TS/platform/commit/d1c2b38cbb1189249c0bfd47582e00ff771428e3)]: + - @effect/platform@0.18.6 + +## 0.7.5 + +### Patch Changes + +- [#171](https://github.com/Effect-TS/platform/pull/171) [`fbbcaa9`](https://github.com/Effect-TS/platform/commit/fbbcaa9b1d4f48f204072a802fb11bcb29813664) Thanks [@tim-smart](https://github.com/tim-smart)! - remove preserveModules patch for preconstruct + +- Updated dependencies [[`fbbcaa9`](https://github.com/Effect-TS/platform/commit/fbbcaa9b1d4f48f204072a802fb11bcb29813664)]: + - @effect/platform@0.18.5 + +## 0.7.4 + +### Patch Changes + +- [#169](https://github.com/Effect-TS/platform/pull/169) [`bd8778d`](https://github.com/Effect-TS/platform/commit/bd8778d1a534f28cab4b326bb25c086fafed8101) Thanks [@tim-smart](https://github.com/tim-smart)! - fix nested modules + +- Updated dependencies [[`bd8778d`](https://github.com/Effect-TS/platform/commit/bd8778d1a534f28cab4b326bb25c086fafed8101)]: + - @effect/platform@0.18.4 + +## 0.7.3 + +### Patch Changes + +- [#167](https://github.com/Effect-TS/platform/pull/167) [`7027589`](https://github.com/Effect-TS/platform/commit/7027589d6dde621065eb8834a2b1ba4d3adc943b) Thanks [@tim-smart](https://github.com/tim-smart)! - build with preconstruct + +- Updated dependencies [[`7027589`](https://github.com/Effect-TS/platform/commit/7027589d6dde621065eb8834a2b1ba4d3adc943b)]: + - @effect/platform@0.18.3 + +## 0.7.2 + +### Patch Changes + +- [#165](https://github.com/Effect-TS/platform/pull/165) [`7e3a741`](https://github.com/Effect-TS/platform/commit/7e3a74197325566df47f9b4459e518eea0762d13) Thanks [@fubhy](https://github.com/fubhy)! - Fix peer deps version range + +- Updated dependencies [[`7e3a741`](https://github.com/Effect-TS/platform/commit/7e3a74197325566df47f9b4459e518eea0762d13)]: + - @effect/platform@0.18.2 + +## 0.7.1 + +### Patch Changes + +- [#163](https://github.com/Effect-TS/platform/pull/163) [`c957232`](https://github.com/Effect-TS/platform/commit/c9572328ee37f44e93e933da622b21df414bf5c6) Thanks [@tim-smart](https://github.com/tim-smart)! - update effect + +- Updated dependencies [[`c957232`](https://github.com/Effect-TS/platform/commit/c9572328ee37f44e93e933da622b21df414bf5c6)]: + - @effect/platform@0.18.1 + +## 0.7.0 + +### Minor Changes + +- [#160](https://github.com/Effect-TS/platform/pull/160) [`c2dc0ab`](https://github.com/Effect-TS/platform/commit/c2dc0abb20b073fd19e38b4e61a08b1edee0f37f) Thanks [@fubhy](https://github.com/fubhy)! - update to effect package + +### Patch Changes + +- Updated dependencies [[`c2dc0ab`](https://github.com/Effect-TS/platform/commit/c2dc0abb20b073fd19e38b4e61a08b1edee0f37f)]: + - @effect/platform@0.18.0 + +## 0.6.1 + +### Patch Changes + +- Updated dependencies [[`9b10bf3`](https://github.com/Effect-TS/platform/commit/9b10bf394106ba0bafd8440dc0b3fba30a5cc1ea)]: + - @effect/platform@0.17.1 + +## 0.6.0 + +### Minor Changes + +- [#156](https://github.com/Effect-TS/platform/pull/156) [`e6c4101`](https://github.com/Effect-TS/platform/commit/e6c41011e5420d90c543dd25d87036d4150f3e85) Thanks [@tim-smart](https://github.com/tim-smart)! - update dependencies + +### Patch Changes + +- Updated dependencies [[`e6c4101`](https://github.com/Effect-TS/platform/commit/e6c41011e5420d90c543dd25d87036d4150f3e85)]: + - @effect/platform@0.17.0 + +## 0.5.0 + +### Minor Changes + +- [#155](https://github.com/Effect-TS/platform/pull/155) [`937b9e5`](https://github.com/Effect-TS/platform/commit/937b9e5c00f80bea128f21c7f5bfa662ba1d45bd) Thanks [@tim-smart](https://github.com/tim-smart)! - use direct deps in sibling packages + +## 0.4.1 + +### Patch Changes + +- Updated dependencies [[`492f0e7`](https://github.com/Effect-TS/platform/commit/492f0e700e939ded6ff17eeca4d50a9e1ce59219)]: + - @effect/platform@0.16.1 + +## 0.4.0 + +### Minor Changes + +- [#145](https://github.com/Effect-TS/platform/pull/145) [`d0522be`](https://github.com/Effect-TS/platform/commit/d0522be6f824571d83be8c6aa16a3d7caa1b3447) Thanks [@tim-smart](https://github.com/tim-smart)! - update dependencies + +### Patch Changes + +- Updated dependencies [[`d0522be`](https://github.com/Effect-TS/platform/commit/d0522be6f824571d83be8c6aa16a3d7caa1b3447), [`6583ad4`](https://github.com/Effect-TS/platform/commit/6583ad4ef5b718620c873208bb11196d35733034)]: + - @effect/platform@0.16.0 + +## 0.3.2 + +### Patch Changes + +- [#131](https://github.com/Effect-TS/platform/pull/131) [`06e27ce`](https://github.com/Effect-TS/platform/commit/06e27ce29553ea8d0a234b941fa1de1a51996fbf) Thanks [@jessekelly881](https://github.com/jessekelly881)! - add Clipboard module to /platform-browser + +- Updated dependencies [[`06e27ce`](https://github.com/Effect-TS/platform/commit/06e27ce29553ea8d0a234b941fa1de1a51996fbf)]: + - @effect/platform@0.15.2 + +## 0.3.1 + +### Patch Changes + +- Updated dependencies [[`2b2f658`](https://github.com/Effect-TS/platform/commit/2b2f6583a7e589a4c7ab8c22bec390ef755f54c3)]: + - @effect/platform@0.15.1 + +## 0.3.0 + +### Minor Changes + +- [#135](https://github.com/Effect-TS/platform/pull/135) [`99f2a49`](https://github.com/Effect-TS/platform/commit/99f2a49c614a5b80646f6600a170609fe7e38025) Thanks [@tim-smart](https://github.com/tim-smart)! - update dependencies + +### Patch Changes + +- Updated dependencies [[`99f2a49`](https://github.com/Effect-TS/platform/commit/99f2a49c614a5b80646f6600a170609fe7e38025)]: + - @effect/platform@0.15.0 + +## 0.2.1 + +### Patch Changes + +- Updated dependencies [[`1d2c403`](https://github.com/Effect-TS/platform/commit/1d2c4033af11f18ba09f53dcfdf8b3fc399bd22f), [`1d2c403`](https://github.com/Effect-TS/platform/commit/1d2c4033af11f18ba09f53dcfdf8b3fc399bd22f)]: + - @effect/platform@0.14.1 + +## 0.2.0 + +### Minor Changes + +- [#130](https://github.com/Effect-TS/platform/pull/130) [`2713c4f`](https://github.com/Effect-TS/platform/commit/2713c4f766f5493303221772368710a09033658d) Thanks [@tim-smart](https://github.com/tim-smart)! - update dependencies + +### Patch Changes + +- Updated dependencies [[`2713c4f`](https://github.com/Effect-TS/platform/commit/2713c4f766f5493303221772368710a09033658d)]: + - @effect/platform@0.14.0 + +## 0.1.2 + +### Patch Changes + +- Updated dependencies [[`eb54e53`](https://github.com/Effect-TS/platform/commit/eb54e53d95e7b863d8ffdff9de12b0abd462b217)]: + - @effect/platform@0.13.16 + +## 0.1.1 + +### Patch Changes + +- Updated dependencies [[`07089a8`](https://github.com/Effect-TS/platform/commit/07089a877fd72b2c1b30016f92af162bbb6ff2c8)]: + - @effect/platform@0.13.15 + +## 0.1.0 + +### Minor Changes + +- [#111](https://github.com/Effect-TS/platform/pull/111) [`6e96703`](https://github.com/Effect-TS/platform/commit/6e96703186f38bd481bffa906e0f99dee89b8e7e) Thanks [@jessekelly881](https://github.com/jessekelly881)! - add /platform-browser + +### Patch Changes + +- [#120](https://github.com/Effect-TS/platform/pull/120) [`9cda8c9`](https://github.com/Effect-TS/platform/commit/9cda8c9ce78d5a9c841a828df20401a0dc07b747) Thanks [@tim-smart](https://github.com/tim-smart)! - add KeyValueStore.SchemaStore + +- [#111](https://github.com/Effect-TS/platform/pull/111) [`6e96703`](https://github.com/Effect-TS/platform/commit/6e96703186f38bd481bffa906e0f99dee89b8e7e) Thanks [@jessekelly881](https://github.com/jessekelly881)! - add KeyValueStore module + +- [#120](https://github.com/Effect-TS/platform/pull/120) [`9cda8c9`](https://github.com/Effect-TS/platform/commit/9cda8c9ce78d5a9c841a828df20401a0dc07b747) Thanks [@tim-smart](https://github.com/tim-smart)! - add KeyValueStore.prefix + +- Updated dependencies [[`9cda8c9`](https://github.com/Effect-TS/platform/commit/9cda8c9ce78d5a9c841a828df20401a0dc07b747), [`6e96703`](https://github.com/Effect-TS/platform/commit/6e96703186f38bd481bffa906e0f99dee89b8e7e), [`9cda8c9`](https://github.com/Effect-TS/platform/commit/9cda8c9ce78d5a9c841a828df20401a0dc07b747)]: + - @effect/platform@0.13.14 diff --git a/repos/effect/packages/platform-browser/LICENSE b/repos/effect/packages/platform-browser/LICENSE new file mode 100644 index 0000000..be1f5c1 --- /dev/null +++ b/repos/effect/packages/platform-browser/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Effectful Technologies Inc + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/repos/effect/packages/platform-browser/README.md b/repos/effect/packages/platform-browser/README.md new file mode 100644 index 0000000..2acda4f --- /dev/null +++ b/repos/effect/packages/platform-browser/README.md @@ -0,0 +1,7 @@ +# `@effect/platform-browser` + +Provides browser-specific implementations for the abstractions defined in [`@effect/platform`](https://github.com/Effect-TS/effect/tree/main/packages/platform). This allows you to write platform-independent code that can seamlessly run in the browser, leveraging browser capabilities while keeping your application portable across multiple environments. + +## Documentation + +- **API Reference**: [View the full documentation](https://effect-ts.github.io/effect/docs/platform-browser). diff --git a/repos/effect/packages/platform-browser/docgen.json b/repos/effect/packages/platform-browser/docgen.json new file mode 100644 index 0000000..a353370 --- /dev/null +++ b/repos/effect/packages/platform-browser/docgen.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../node_modules/@effect/docgen/schema.json", + "srcLink": "https://github.com/Effect-TS/effect/tree/main/packages/platform-browser/src/", + "exclude": [ + "src/internal/**/*.ts" + ] +} diff --git a/repos/effect/packages/platform-browser/examples/keyValueStore.ts b/repos/effect/packages/platform-browser/examples/keyValueStore.ts new file mode 100644 index 0000000..c4036ef --- /dev/null +++ b/repos/effect/packages/platform-browser/examples/keyValueStore.ts @@ -0,0 +1,9 @@ +import * as KeyValueStore from "@effect/platform/KeyValueStore" +import * as Effect from "effect/Effect" + +const program = KeyValueStore.KeyValueStore.pipe( + Effect.flatMap((kv) => kv.set("foo", "bar")), + Effect.provide(KeyValueStore.layerMemory) +) + +Effect.runPromise(program) diff --git a/repos/effect/packages/platform-browser/package.json b/repos/effect/packages/platform-browser/package.json new file mode 100644 index 0000000..2abdfe8 --- /dev/null +++ b/repos/effect/packages/platform-browser/package.json @@ -0,0 +1,64 @@ +{ + "name": "@effect/platform-browser", + "type": "module", + "version": "0.76.0", + "license": "MIT", + "description": "Platform specific implementations for the browser", + "homepage": "https://effect.website", + "repository": { + "type": "git", + "url": "https://github.com/Effect-TS/effect.git", + "directory": "packages/platform-browser" + }, + "bugs": { + "url": "https://github.com/Effect-TS/effect/issues" + }, + "tags": [ + "browser", + "typescript", + "algebraic-data-types", + "functional-programming" + ], + "keywords": [ + "browser", + "typescript", + "algebraic-data-types", + "functional-programming" + ], + "publishConfig": { + "access": "public", + "provenance": true, + "directory": "dist", + "linkDirectory": false + }, + "exports": { + "./package.json": "./package.json", + ".": "./src/index.ts", + "./*": "./src/*.ts", + "./internal/*": null + }, + "scripts": { + "codegen": "build-utils prepare-v3", + "build": "pnpm build-esm && pnpm build-annotate && pnpm build-cjs && build-utils pack-v3", + "build-esm": "tsc -b tsconfig.build.json", + "build-cjs": "babel build/esm --plugins @babel/transform-export-namespace-from --plugins @babel/transform-modules-commonjs --out-dir build/cjs --source-maps", + "build-annotate": "babel build/esm --plugins annotate-pure-calls --out-dir build/esm --source-maps", + "check": "tsc -b tsconfig.json", + "test": "vitest", + "coverage": "vitest --coverage" + }, + "peerDependencies": { + "@effect/platform": "workspace:^", + "effect": "workspace:^" + }, + "devDependencies": { + "@effect/platform": "workspace:^", + "@effect/rpc": "workspace:^", + "effect": "workspace:^", + "happy-dom": "^17.4.7", + "mock-xmlhttprequest": "^8.4.1" + }, + "dependencies": { + "multipasta": "^0.2.7" + } +} diff --git a/repos/effect/packages/platform-browser/src/BrowserHttpClient.ts b/repos/effect/packages/platform-browser/src/BrowserHttpClient.ts new file mode 100644 index 0000000..24dde80 --- /dev/null +++ b/repos/effect/packages/platform-browser/src/BrowserHttpClient.ts @@ -0,0 +1,37 @@ +/** + * @since 1.0.0 + */ +import type * as HttpClient from "@effect/platform/HttpClient" +import * as Context from "effect/Context" +import type { Effect } from "effect/Effect" +import type * as FiberRef from "effect/FiberRef" +import type { LazyArg } from "effect/Function" +import type * as Layer from "effect/Layer" +import * as internal from "./internal/httpClient.js" + +/** + * @since 1.0.0 + * @category layers + */ +export const layerXMLHttpRequest: Layer.Layer = internal.layerXMLHttpRequest + +/** + * @since 1.0.0 + * @category tags + */ +export class XMLHttpRequest extends Context.Tag(internal.xhrTagKey)< + XMLHttpRequest, + LazyArg +>() {} + +/** + * @since 1.0.0 + * @category fiber refs + */ +export const currentXHRResponseType: FiberRef.FiberRef<"text" | "arraybuffer"> = internal.currentXHRResponseType + +/** + * @since 1.0.0 + * @category fiber refs + */ +export const withXHRArrayBuffer: (effect: Effect) => Effect = internal.withXHRArrayBuffer diff --git a/repos/effect/packages/platform-browser/src/BrowserKeyValueStore.ts b/repos/effect/packages/platform-browser/src/BrowserKeyValueStore.ts new file mode 100644 index 0000000..0c582f4 --- /dev/null +++ b/repos/effect/packages/platform-browser/src/BrowserKeyValueStore.ts @@ -0,0 +1,22 @@ +/** + * @since 1.0.0 + */ +import type * as KeyValueStore from "@effect/platform/KeyValueStore" +import type * as Layer from "effect/Layer" +import * as internal from "./internal/keyValueStore.js" + +/** + * Creates a KeyValueStore layer that uses the browser's localStorage api. Values are stored between sessions. + * + * @since 1.0.0 + * @category models + */ +export const layerLocalStorage: Layer.Layer = internal.layerLocalStorage + +/** + * Creates a KeyValueStore layer that uses the browser's sessionStorage api. Values are stored only for the current session. + * + * @since 1.0.0 + * @category models + */ +export const layerSessionStorage: Layer.Layer = internal.layerSessionStorage diff --git a/repos/effect/packages/platform-browser/src/BrowserRuntime.ts b/repos/effect/packages/platform-browser/src/BrowserRuntime.ts new file mode 100644 index 0000000..2729356 --- /dev/null +++ b/repos/effect/packages/platform-browser/src/BrowserRuntime.ts @@ -0,0 +1,11 @@ +/** + * @since 1.0.0 + */ +import type { RunMain } from "@effect/platform/Runtime" +import * as internal from "./internal/runtime.js" + +/** + * @since 1.0.0 + * @category runtime + */ +export const runMain: RunMain = internal.runMain diff --git a/repos/effect/packages/platform-browser/src/BrowserSocket.ts b/repos/effect/packages/platform-browser/src/BrowserSocket.ts new file mode 100644 index 0000000..c63e919 --- /dev/null +++ b/repos/effect/packages/platform-browser/src/BrowserSocket.ts @@ -0,0 +1,25 @@ +/** + * @since 1.0.0 + */ +import * as Socket from "@effect/platform/Socket" +import * as Layer from "effect/Layer" + +/** + * @since 1.0.0 + * @category layers + */ +export const layerWebSocket = (url: string, options?: { + readonly closeCodeIsError?: (code: number) => boolean +}): Layer.Layer => + Layer.scoped(Socket.Socket, Socket.makeWebSocket(url, options)).pipe( + Layer.provide(layerWebSocketConstructor) + ) + +/** + * A WebSocket constructor that uses globalThis.WebSocket. + * + * @since 1.0.0 + * @category layers + */ +export const layerWebSocketConstructor: Layer.Layer = + Socket.layerWebSocketConstructorGlobal diff --git a/repos/effect/packages/platform-browser/src/BrowserStream.ts b/repos/effect/packages/platform-browser/src/BrowserStream.ts new file mode 100644 index 0000000..50c22d4 --- /dev/null +++ b/repos/effect/packages/platform-browser/src/BrowserStream.ts @@ -0,0 +1,34 @@ +/** + * @since 1.0.0 + */ + +import type * as Stream from "effect/Stream" +import * as internal from "./internal/stream.js" + +/** + * Creates a `Stream` from window.addEventListener. + * @since 1.0.0 + */ +export const fromEventListenerWindow: ( + type: K, + options?: boolean | { + readonly capture?: boolean + readonly passive?: boolean + readonly once?: boolean + readonly bufferSize?: number | "unbounded" | undefined + } | undefined +) => Stream.Stream = internal.fromEventListenerWindow + +/** + * Creates a `Stream` from document.addEventListener. + * @since 1.0.0 + */ +export const fromEventListenerDocument: ( + type: K, + options?: boolean | { + readonly capture?: boolean + readonly passive?: boolean + readonly once?: boolean + readonly bufferSize?: number | "unbounded" | undefined + } | undefined +) => Stream.Stream = internal.fromEventListenerDocument diff --git a/repos/effect/packages/platform-browser/src/BrowserWorker.ts b/repos/effect/packages/platform-browser/src/BrowserWorker.ts new file mode 100644 index 0000000..c1221b6 --- /dev/null +++ b/repos/effect/packages/platform-browser/src/BrowserWorker.ts @@ -0,0 +1,33 @@ +/** + * @since 1.0.0 + */ +import type * as Worker from "@effect/platform/Worker" +import type * as Layer from "effect/Layer" +import * as internal from "./internal/worker.js" +/** + * @since 1.0.0 + * @category layers + */ +export const layerManager: Layer.Layer = internal.layerManager + +/** + * @since 1.0.0 + * @category layers + */ +export const layerWorker: Layer.Layer = internal.layerWorker + +/** + * @since 1.0.0 + * @category layers + */ +export const layer: ( + spawn: (id: number) => Worker | SharedWorker | MessagePort +) => Layer.Layer = internal.layer + +/** + * @since 1.0.0 + * @category layers + */ +export const layerPlatform: ( + spawn: (id: number) => globalThis.Worker | globalThis.SharedWorker | MessagePort +) => Layer.Layer = internal.layerPlatform diff --git a/repos/effect/packages/platform-browser/src/BrowserWorkerRunner.ts b/repos/effect/packages/platform-browser/src/BrowserWorkerRunner.ts new file mode 100644 index 0000000..b11467d --- /dev/null +++ b/repos/effect/packages/platform-browser/src/BrowserWorkerRunner.ts @@ -0,0 +1,33 @@ +/** + * @since 1.0.0 + */ +import type * as Runner from "@effect/platform/WorkerRunner" +import type * as Layer from "effect/Layer" +import * as internal from "./internal/workerRunner.js" + +export { + /** + * @since 1.0.0 + * @category re-exports + */ + launch +} from "@effect/platform/WorkerRunner" + +/** + * @since 1.0.0 + * @category constructors + */ +export const make: (self: MessagePort | Window) => Runner.PlatformRunner = internal.make + +/** + * @since 1.0.0 + * @category layers + */ +export const layer: Layer.Layer = internal.layer + +/** + * @since 1.0.0 + * @category layers + */ +export const layerMessagePort: (port: MessagePort | Window) => Layer.Layer = + internal.layerMessagePort diff --git a/repos/effect/packages/platform-browser/src/Clipboard.ts b/repos/effect/packages/platform-browser/src/Clipboard.ts new file mode 100644 index 0000000..cf9c29c --- /dev/null +++ b/repos/effect/packages/platform-browser/src/Clipboard.ts @@ -0,0 +1,123 @@ +/** + * @since 1.0.0 + */ +import { TypeIdError } from "@effect/platform/Error" +import * as Context from "effect/Context" +import * as Effect from "effect/Effect" +import * as Layer from "effect/Layer" + +/** + * @since 1.0.0 + * @category type ids + */ +export const TypeId: unique symbol = Symbol.for("@effect/platform-browser/Clipboard") + +/** + * @since 1.0.0 + * @category type ids + */ +export type TypeId = typeof TypeId + +/** + * @since 1.0.0 + * @category interface + */ +export interface Clipboard { + readonly [TypeId]: TypeId + + readonly read: Effect.Effect + readonly readString: Effect.Effect + readonly write: (items: ClipboardItems) => Effect.Effect + readonly writeString: (text: string) => Effect.Effect + readonly writeBlob: (blob: Blob) => Effect.Effect + readonly clear: Effect.Effect +} + +/** + * @since 1.0.0 + * @category type ids + */ +export const ErrorTypeId: unique symbol = Symbol.for("@effect/platform-browser/Clipboard/ClipboardError") + +/** + * @since 1.0.0 + * @category type ids + */ +export type ErrorTypeId = typeof ErrorTypeId + +/** + * @since 1.0.0 + * @category errors + */ +export class ClipboardError extends TypeIdError(ErrorTypeId, "ClipboardError")<{ + readonly message: string + readonly cause: unknown +}> {} + +/** + * @since 1.0.0 + * @category tag + */ +export const Clipboard: Context.Tag = Context.GenericTag( + "@effect/platform-browser/Clipboard" +) + +/** + * @since 1.0.0 + * @category constructor + */ +export const make = ( + impl: Omit +): Clipboard => + Clipboard.of({ + ...impl, + [TypeId]: TypeId, + clear: impl.writeString(""), + writeBlob: (blob: Blob) => impl.write([new ClipboardItem({ [blob.type]: blob })]) + }) + +/** + * A layer that directly interfaces with the navigator.clipboard api + * + * @since 1.0.0 + * @category layers + */ +export const layer: Layer.Layer = Layer.succeed( + Clipboard, + make({ + read: Effect.tryPromise({ + try: () => navigator.clipboard.read(), + catch: (cause) => + new ClipboardError({ + cause, + "message": "Unable to read from clipboard" + }) + }), + write: (s: Array) => + Effect.tryPromise({ + try: () => navigator.clipboard.write(s), + catch: (cause) => + new ClipboardError({ + cause, + "message": "Unable to write to clipboard" + }) + }), + readString: Effect.tryPromise({ + try: () => navigator.clipboard.readText(), + catch: (cause) => + new ClipboardError({ + cause, + "message": "Unable to read a string from clipboard" + }) + }), + writeString: (text: string) => + Effect.tryPromise({ + try: () => navigator.clipboard.writeText(text), + catch: (cause) => + new ClipboardError({ + cause, + "message": "Unable to write a string to clipboard" + }) + }) + }) +) diff --git a/repos/effect/packages/platform-browser/src/Geolocation.ts b/repos/effect/packages/platform-browser/src/Geolocation.ts new file mode 100644 index 0000000..d779f3a --- /dev/null +++ b/repos/effect/packages/platform-browser/src/Geolocation.ts @@ -0,0 +1,138 @@ +/** + * @since 1.0.0 + */ +import { TypeIdError } from "@effect/platform/Error" +import * as Context from "effect/Context" +import * as Effect from "effect/Effect" +import * as Either from "effect/Either" +import { identity } from "effect/Function" +import * as Layer from "effect/Layer" +import * as Queue from "effect/Queue" +import * as Stream from "effect/Stream" + +/** + * @since 1.0.0 + * @category type ids + */ +export const TypeId: unique symbol = Symbol.for("@effect/platform-browser/Geolocation") + +/** + * @since 1.0.0 + * @category type ids + */ +export type TypeId = typeof TypeId + +/** + * @since 1.0.0 + * @category models + */ +export interface Geolocation { + readonly [TypeId]: TypeId + readonly getCurrentPosition: ( + options?: PositionOptions | undefined + ) => Effect.Effect + readonly watchPosition: ( + options?: + | PositionOptions & { + readonly bufferSize?: number | undefined + } + | undefined + ) => Stream.Stream +} + +/** + * @since 1.0.0 + * @category tags + */ +export const Geolocation: Context.Tag = Context.GenericTag( + "@effect/platform-browser/Geolocation" +) + +/** + * @since 1.0.0 + * @category type ids + */ +export const ErrorTypeId: unique symbol = Symbol.for("@effect/platform-browser/Geolocation/GeolocationError") + +/** + * @since 1.0.0 + * @category type ids + */ +export type ErrorTypeId = typeof ErrorTypeId + +/** + * @since 1.0.0 + * @category errors + */ +export class GeolocationError extends TypeIdError(ErrorTypeId, "GeolocationError")<{ + readonly reason: "PositionUnavailable" | "PermissionDenied" | "Timeout" + readonly cause: unknown +}> { + get message() { + return this.reason + } +} + +const makeQueue = ( + options: + | PositionOptions & { + readonly bufferSize?: number | undefined + } + | undefined +) => + Queue.sliding>(options?.bufferSize ?? 16).pipe( + Effect.tap((queue) => + Effect.acquireRelease( + Effect.sync(() => + navigator.geolocation.watchPosition( + (position) => queue.unsafeOffer(Either.right(position)), + (cause) => { + if (cause.code === cause.PERMISSION_DENIED) { + queue.unsafeOffer(Either.left(new GeolocationError({ reason: "PermissionDenied", cause }))) + } else if (cause.code === cause.TIMEOUT) { + queue.unsafeOffer(Either.left(new GeolocationError({ reason: "Timeout", cause }))) + } + }, + options + ) + ), + (handleId) => Effect.sync(() => navigator.geolocation.clearWatch(handleId)) + ) + ) + ) + +/** + * @since 1.0.0 + * @category layers + */ +export const layer: Layer.Layer = Layer.succeed( + Geolocation, + Geolocation.of({ + [TypeId]: TypeId, + getCurrentPosition: (options) => + makeQueue(options).pipe( + Effect.flatMap(Queue.take), + Effect.flatten, + Effect.scoped + ), + watchPosition: (options) => + makeQueue(options).pipe( + Effect.map(Stream.fromQueue), + Stream.unwrapScoped, + Stream.mapEffect(identity) + ) + }) +) + +/** + * @since 1.0.0 + * @category accessors + */ +export const watchPosition = ( + options?: + | PositionOptions & { + readonly bufferSize?: number | undefined + } + | undefined +): Stream.Stream => + Stream.unwrap(Effect.map(Geolocation, (geolocation) => geolocation.watchPosition(options))) diff --git a/repos/effect/packages/platform-browser/src/Permissions.ts b/repos/effect/packages/platform-browser/src/Permissions.ts new file mode 100644 index 0000000..b4f6857 --- /dev/null +++ b/repos/effect/packages/platform-browser/src/Permissions.ts @@ -0,0 +1,98 @@ +/** + * @since 1.0.0 + */ +import { TypeIdError } from "@effect/platform/Error" +import * as Context from "effect/Context" +import * as Effect from "effect/Effect" +import * as Layer from "effect/Layer" + +/** + * @since 1.0.0 + * @category type ids + */ +export const TypeId: unique symbol = Symbol.for("@effect/platform-browser/Permissions") + +/** + * @since 1.0.0 + * @category type ids + */ +export type TypeId = typeof TypeId + +/** + * Wrapper on the Permission API (`navigator.permissions`) + * with methods for querying status of permissions. + * + * @since 1.0.0 + * @category interface + */ +export interface Permissions { + readonly [TypeId]: TypeId + + /** + * Returns the state of a user permission on the global scope. + */ + readonly query: ( + name: Name + ) => Effect.Effect< + // `name` is identical to the name passed to Permissions.query + // https://developer.mozilla.org/en-US/docs/Web/API/PermissionStatus + Omit & { name: Name }, + PermissionsError + > +} + +/** + * @since 1.0.0 + * @category type ids + */ +export const ErrorTypeId: unique symbol = Symbol.for("@effect/platform-browser/Permissions/PermissionsError") + +/** + * @since 1.0.0 + * @category type ids + */ +export type ErrorTypeId = typeof ErrorTypeId + +/** + * @since 1.0.0 + * @category errors + */ +export class PermissionsError extends TypeIdError(ErrorTypeId, "PermissionsError")<{ + /** https://developer.mozilla.org/en-US/docs/Web/API/Permissions/query#exceptions */ + readonly reason: "InvalidStateError" | "TypeError" + readonly cause: unknown +}> { + get message() { + return this.reason + } +} + +/** + * @since 1.0.0 + * @category tags + */ +export const Permissions: Context.Tag = Context.GenericTag( + "@effect/platform-browser/Permissions" +) + +/** + * A layer that directly interfaces with the `navigator.permissions` api + * + * @since 1.0.0 + * @category layers + */ +export const layer: Layer.Layer = Layer.succeed( + Permissions, + Permissions.of({ + [TypeId]: TypeId, + query: (name) => + Effect.tryPromise({ + try: () => navigator.permissions.query({ name }) as Promise, + catch: (cause) => + new PermissionsError({ + reason: cause instanceof DOMException ? "InvalidStateError" : "TypeError", + cause + }) + }) + }) +) diff --git a/repos/effect/packages/platform-browser/src/index.ts b/repos/effect/packages/platform-browser/src/index.ts new file mode 100644 index 0000000..0b5af32 --- /dev/null +++ b/repos/effect/packages/platform-browser/src/index.ts @@ -0,0 +1,49 @@ +/** + * @since 1.0.0 + */ +export * as BrowserHttpClient from "./BrowserHttpClient.js" + +/** + * @since 1.0.0 + */ +export * as BrowserKeyValueStore from "./BrowserKeyValueStore.js" + +/** + * @since 1.0.0 + */ +export * as BrowserRuntime from "./BrowserRuntime.js" + +/** + * @since 1.0.0 + */ +export * as BrowserSocket from "./BrowserSocket.js" + +/** + * @since 1.0.0 + */ +export * as BrowserStream from "./BrowserStream.js" + +/** + * @since 1.0.0 + */ +export * as BrowserWorker from "./BrowserWorker.js" + +/** + * @since 1.0.0 + */ +export * as BrowserWorkerRunner from "./BrowserWorkerRunner.js" + +/** + * @since 1.0.0 + */ +export * as Clipboard from "./Clipboard.js" + +/** + * @since 1.0.0 + */ +export * as Geolocation from "./Geolocation.js" + +/** + * @since 1.0.0 + */ +export * as Permissions from "./Permissions.js" diff --git a/repos/effect/packages/platform-browser/src/internal/httpClient.ts b/repos/effect/packages/platform-browser/src/internal/httpClient.ts new file mode 100644 index 0000000..0302d27 --- /dev/null +++ b/repos/effect/packages/platform-browser/src/internal/httpClient.ts @@ -0,0 +1,324 @@ +import * as Cookies from "@effect/platform/Cookies" +import * as Headers from "@effect/platform/Headers" +import * as Client from "@effect/platform/HttpClient" +import * as Error from "@effect/platform/HttpClientError" +import type * as ClientRequest from "@effect/platform/HttpClientRequest" +import * as ClientResponse from "@effect/platform/HttpClientResponse" +import * as IncomingMessage from "@effect/platform/HttpIncomingMessage" +import * as UrlParams from "@effect/platform/UrlParams" +import * as Context from "effect/Context" +import * as Effect from "effect/Effect" +import * as FiberRef from "effect/FiberRef" +import { type LazyArg } from "effect/Function" +import { globalValue } from "effect/GlobalValue" +import * as Inspectable from "effect/Inspectable" +import * as Option from "effect/Option" +import * as Stream from "effect/Stream" +import * as HeaderParser from "multipasta/HeadersParser" + +/** @internal */ +export const xhrTagKey = "@effect/platform-browser/BrowserHttpClient/XMLHttpRequest" + +const xhrTag = Context.GenericTag>(xhrTagKey) + +/** @internal */ +export const currentXHRResponseType = globalValue( + "@effect/platform-browser/BrowserHttpClient/currentXHRResponseType", + () => FiberRef.unsafeMake<"text" | "arraybuffer">("text") +) + +/** @internal */ +export const withXHRArrayBuffer = (effect: Effect.Effect): Effect.Effect => + Effect.locally( + effect, + currentXHRResponseType, + "arraybuffer" + ) + +const makeXhr = () => new XMLHttpRequest() + +const makeXMLHttpRequest = Client.make((request, url, signal, fiber) => + Effect.suspend(() => { + const xhr = Context.getOrElse( + fiber.getFiberRef(FiberRef.currentContext), + xhrTag, + () => makeXhr + )() + signal.addEventListener("abort", () => { + xhr.abort() + xhr.onreadystatechange = null + }, { once: true }) + xhr.open(request.method, url.toString(), true) + xhr.responseType = fiber.getFiberRef(currentXHRResponseType) + Object.entries(request.headers).forEach(([k, v]) => { + xhr.setRequestHeader(k, v) + }) + return Effect.zipRight( + sendBody(xhr, request), + Effect.async((resume) => { + let sent = false + const onChange = () => { + if (!sent && xhr.readyState >= 2) { + sent = true + resume(Effect.succeed(new ClientResponseImpl(request, xhr))) + } + } + xhr.onreadystatechange = onChange + xhr.onerror = (_event) => { + resume(Effect.fail( + new Error.RequestError({ + request, + reason: "Transport", + cause: xhr.statusText + }) + )) + } + onChange() + return Effect.void + }) + ) + }) +) + +const sendBody = ( + xhr: XMLHttpRequest, + request: ClientRequest.HttpClientRequest +): Effect.Effect => { + const body = request.body + switch (body._tag) { + case "Empty": + return Effect.sync(() => xhr.send()) + case "Raw": + return Effect.sync(() => xhr.send(body.body as any)) + case "Uint8Array": + return Effect.sync(() => xhr.send(body.body)) + case "FormData": + return Effect.sync(() => xhr.send(body.formData)) + case "Stream": + return Effect.matchEffect( + Stream.runFold(body.stream, new Uint8Array(0), (acc, chunk) => { + const next = new Uint8Array(acc.length + chunk.length) + next.set(acc, 0) + next.set(chunk, acc.length) + return next + }), + { + onFailure: (cause) => + Effect.fail( + new Error.RequestError({ + request, + reason: "Encode", + cause + }) + ), + onSuccess: (body) => Effect.sync(() => xhr.send(body)) + } + ) + } +} + +const encoder = new TextEncoder() + +/** @internal */ +export abstract class IncomingMessageImpl extends Inspectable.Class + implements IncomingMessage.HttpIncomingMessage +{ + readonly [IncomingMessage.TypeId]: IncomingMessage.TypeId + + constructor( + readonly source: XMLHttpRequest, + readonly onError: (error: unknown) => E + ) { + super() + this[IncomingMessage.TypeId] = IncomingMessage.TypeId + this._rawHeaderString = source.getAllResponseHeaders() + } + + private _rawHeaderString: string + private _rawHeaders: Record> | undefined + private _headers: Headers.Headers | undefined + get headers() { + if (this._headers) { + return this._headers + } + if (this._rawHeaderString === "") { + return this._headers = Headers.empty + } + const parser = HeaderParser.make() + const result = parser(encoder.encode(this._rawHeaderString + "\r\n"), 0) + this._rawHeaders = result._tag === "Headers" ? result.headers : undefined + const parsed = result._tag === "Headers" ? Headers.fromInput(result.headers) : Headers.empty + return this._headers = parsed + } + + cachedCookies: Cookies.Cookies | undefined + get cookies() { + if (this.cachedCookies) { + return this.cachedCookies + } + if (this._rawHeaders === undefined) { + return Cookies.empty + } else if (this._rawHeaders["set-cookie"] === undefined) { + return this.cachedCookies = Cookies.empty + } + return this.cachedCookies = Cookies.fromSetCookie(this._rawHeaders["set-cookie"]) + } + + get remoteAddress() { + return Option.none() + } + + _textEffect: Effect.Effect | undefined + get text(): Effect.Effect { + if (this._textEffect) { + return this._textEffect + } + return this._textEffect = Effect.async((resume) => { + if (this.source.readyState === 4) { + resume(Effect.succeed(this.source.responseText)) + return + } + + const onReadyStateChange = () => { + if (this.source.readyState === 4) { + resume(Effect.succeed(this.source.responseText)) + } + } + const onError = () => { + resume(Effect.fail(this.onError(this.source.statusText))) + } + this.source.addEventListener("readystatechange", onReadyStateChange) + this.source.addEventListener("error", onError) + return Effect.sync(() => { + this.source.removeEventListener("readystatechange", onReadyStateChange) + this.source.removeEventListener("error", onError) + }) + }).pipe( + Effect.cached, + Effect.runSync + ) + } + + get json(): Effect.Effect { + return Effect.tryMap(this.text, { + try: (_) => _ === "" ? null : JSON.parse(_) as unknown, + catch: this.onError + }) + } + + get urlParamsBody(): Effect.Effect { + return Effect.flatMap(this.text, (_) => + Effect.try({ + try: () => UrlParams.fromInput(new URLSearchParams(_)), + catch: this.onError + })) + } + + get stream(): Stream.Stream { + return Stream.async((emit) => { + let offset = 0 + const onReadyStateChange = () => { + if (this.source.readyState === 3) { + emit.single(encoder.encode(this.source.responseText.slice(offset))) + offset = this.source.responseText.length + } else if (this.source.readyState === 4) { + if (offset < this.source.responseText.length) { + emit.single(encoder.encode(this.source.responseText.slice(offset))) + } + emit.end() + } + } + const onError = () => { + emit.fail(this.onError(this.source.statusText)) + } + this.source.addEventListener("readystatechange", onReadyStateChange) + this.source.addEventListener("error", onError) + onReadyStateChange() + return Effect.sync(() => { + this.source.removeEventListener("readystatechange", onReadyStateChange) + this.source.removeEventListener("error", onError) + }) + }) + } + + _arrayBufferEffect: Effect.Effect | undefined + get arrayBuffer(): Effect.Effect { + if (this._arrayBufferEffect) { + return this._arrayBufferEffect + } + return this._arrayBufferEffect = Effect.async((resume) => { + if (this.source.readyState === 4) { + resume(Effect.succeed(this.source.response)) + return + } + + const onReadyStateChange = () => { + if (this.source.readyState === 4) { + resume(Effect.succeed(this.source.response)) + } + } + const onError = () => { + resume(Effect.fail(this.onError(this.source.statusText))) + } + this.source.addEventListener("readystatechange", onReadyStateChange) + this.source.addEventListener("error", onError) + return Effect.sync(() => { + this.source.removeEventListener("readystatechange", onReadyStateChange) + this.source.removeEventListener("error", onError) + }) + }).pipe( + Effect.map((response) => { + if (typeof response === "string") { + const arr = encoder.encode(response) + return arr.byteLength !== arr.buffer.byteLength + ? arr.buffer.slice(arr.byteOffset, arr.byteOffset + arr.byteLength) + : arr.buffer + } + return response + }), + Effect.cached, + Effect.runSync + ) + } +} + +class ClientResponseImpl extends IncomingMessageImpl implements ClientResponse.HttpClientResponse { + readonly [ClientResponse.TypeId]: ClientResponse.TypeId + + constructor( + readonly request: ClientRequest.HttpClientRequest, + source: XMLHttpRequest + ) { + super(source, (cause) => + new Error.ResponseError({ + request, + response: this, + reason: "Decode", + cause + })) + this[ClientResponse.TypeId] = ClientResponse.TypeId + } + + get status() { + return this.source.status + } + + get formData(): Effect.Effect { + return Effect.die("Not implemented") + } + + toString(): string { + return `ClientResponse(${this.status})` + } + + toJSON(): unknown { + return IncomingMessage.inspect(this, { + _id: "@effect/platform/HttpClientResponse", + request: this.request.toJSON(), + status: this.status + }) + } +} + +/** @internal */ +export const layerXMLHttpRequest = Client.layerMergedContext(Effect.succeed(makeXMLHttpRequest)) diff --git a/repos/effect/packages/platform-browser/src/internal/keyValueStore.ts b/repos/effect/packages/platform-browser/src/internal/keyValueStore.ts new file mode 100644 index 0000000..3b9ca18 --- /dev/null +++ b/repos/effect/packages/platform-browser/src/internal/keyValueStore.ts @@ -0,0 +1,7 @@ +import * as KeyValueStore from "@effect/platform/KeyValueStore" + +/** @internal */ +export const layerSessionStorage = KeyValueStore.layerStorage(() => sessionStorage) + +/** @internal */ +export const layerLocalStorage = KeyValueStore.layerStorage(() => localStorage) diff --git a/repos/effect/packages/platform-browser/src/internal/runtime.ts b/repos/effect/packages/platform-browser/src/internal/runtime.ts new file mode 100644 index 0000000..f0844cc --- /dev/null +++ b/repos/effect/packages/platform-browser/src/internal/runtime.ts @@ -0,0 +1,8 @@ +import { makeRunMain } from "@effect/platform/Runtime" + +/** @internal */ +export const runMain = makeRunMain(({ fiber }) => { + addEventListener("beforeunload", () => { + fiber.unsafeInterruptAsFork(fiber.id()) + }) +}) diff --git a/repos/effect/packages/platform-browser/src/internal/stream.ts b/repos/effect/packages/platform-browser/src/internal/stream.ts new file mode 100644 index 0000000..b38e590 --- /dev/null +++ b/repos/effect/packages/platform-browser/src/internal/stream.ts @@ -0,0 +1,27 @@ +/** + * @since 1.0.0 + */ + +import * as Stream from "effect/Stream" + +/** @internal */ +export const fromEventListenerWindow = ( + type: K, + options?: boolean | { + readonly capture?: boolean + readonly passive?: boolean + readonly once?: boolean + readonly bufferSize?: number | "unbounded" | undefined + } | undefined +) => Stream.fromEventListener(window, type, options) + +/** @internal */ +export const fromEventListenerDocument = ( + type: K, + options?: boolean | { + readonly capture?: boolean + readonly passive?: boolean + readonly once?: boolean + readonly bufferSize?: number | "unbounded" | undefined + } | undefined +) => Stream.fromEventListener(document, type, options) diff --git a/repos/effect/packages/platform-browser/src/internal/worker.ts b/repos/effect/packages/platform-browser/src/internal/worker.ts new file mode 100644 index 0000000..c7c3306 --- /dev/null +++ b/repos/effect/packages/platform-browser/src/internal/worker.ts @@ -0,0 +1,58 @@ +import * as Worker from "@effect/platform/Worker" +import { WorkerError } from "@effect/platform/WorkerError" +import * as Deferred from "effect/Deferred" +import * as Effect from "effect/Effect" +import * as Layer from "effect/Layer" +import * as Scope from "effect/Scope" + +const platformWorkerImpl = Worker.makePlatform()({ + setup({ scope, worker }) { + const port = "port" in worker ? worker.port : worker + return Effect.as( + Scope.addFinalizer( + scope, + Effect.sync(() => { + port.postMessage([1]) + }) + ), + port + ) + }, + listen({ deferred, emit, port, scope }) { + function onMessage(event: MessageEvent) { + emit(event.data) + } + function onError(event: ErrorEvent) { + Deferred.unsafeDone( + deferred, + new WorkerError({ reason: "unknown", cause: event.error ?? event.message }) + ) + } + port.addEventListener("message", onMessage as any) + port.addEventListener("error", onError as any) + if ("start" in port) { + port.start() + } + return Scope.addFinalizer( + scope, + Effect.sync(() => { + port.removeEventListener("message", onMessage as any) + port.removeEventListener("error", onError as any) + }) + ) + } +}) + +/** @internal */ +export const layerWorker = Layer.succeed(Worker.PlatformWorker, platformWorkerImpl) + +/** @internal */ +export const layerManager = Layer.provide(Worker.layerManager, layerWorker) + +/** @internal */ +export const layer = (spawn: (id: number) => globalThis.Worker | globalThis.SharedWorker | MessagePort) => + Layer.merge(layerManager, Worker.layerSpawner(spawn)) + +/** @internal */ +export const layerPlatform = (spawn: (id: number) => globalThis.Worker | globalThis.SharedWorker | MessagePort) => + Layer.merge(layerWorker, Worker.layerSpawner(spawn)) diff --git a/repos/effect/packages/platform-browser/src/internal/workerRunner.ts b/repos/effect/packages/platform-browser/src/internal/workerRunner.ts new file mode 100644 index 0000000..d160393 --- /dev/null +++ b/repos/effect/packages/platform-browser/src/internal/workerRunner.ts @@ -0,0 +1,155 @@ +import { WorkerError } from "@effect/platform/WorkerError" +import * as Runner from "@effect/platform/WorkerRunner" +import * as Cause from "effect/Cause" +import * as Context from "effect/Context" +import * as Deferred from "effect/Deferred" +import * as Effect from "effect/Effect" +import * as ExecStrategy from "effect/ExecutionStrategy" +import * as Exit from "effect/Exit" +import * as FiberSet from "effect/FiberSet" +import { identity } from "effect/Function" +import { globalValue } from "effect/GlobalValue" +import * as Layer from "effect/Layer" +import * as Mailbox from "effect/Mailbox" +import * as Runtime from "effect/Runtime" +import * as Scope from "effect/Scope" + +const cachedPorts = globalValue("@effect/platform-browser/Worker/cachedPorts", () => new Set()) +function globalHandleConnect(event: MessageEvent) { + cachedPorts.add((event as MessageEvent).ports[0]) +} +if (typeof self !== "undefined" && "onconnect" in self) { + self.onconnect = globalHandleConnect +} + +/** @internal */ +export const make = (self: MessagePort | Window) => + Runner.PlatformRunner.of({ + [Runner.PlatformRunnerTypeId]: Runner.PlatformRunnerTypeId, + start: Effect.fnUntraced(function*(closeLatch: Deferred.Deferred) { + const disconnects = yield* Mailbox.make() + let currentPortId = 0 + + const ports = new Map() + const send = (portId: number, message: O, transfer?: ReadonlyArray) => + Effect.sync(() => { + ;(ports.get(portId)?.[0] ?? self).postMessage([1, message], { + transfer: transfer as any + }) + }) + + const run = Effect.fnUntraced(function*( + handler: (portId: number, message: I) => Effect.Effect | void + ) { + const scope = yield* Effect.scope + const runtime = (yield* Effect.interruptible(Effect.runtime())).pipe( + Runtime.updateContext(Context.omit(Scope.Scope)) + ) as Runtime.Runtime + const fiberSet = yield* FiberSet.make() + const runFork = Runtime.runFork(runtime) + function onExit(exit: Exit.Exit) { + if (exit._tag === "Failure" && !Cause.isInterruptedOnly(exit.cause)) { + Deferred.unsafeDone(closeLatch, Exit.die(Cause.squash(exit.cause))) + } + } + + function onMessage(portId: number) { + return function(event: MessageEvent) { + const message = event.data as Runner.BackingRunner.Message + if (message[0] === 0) { + const result = handler(portId, message[1]) + if (Effect.isEffect(result)) { + const fiber = runFork(result) + fiber.addObserver(onExit) + FiberSet.unsafeAdd(fiberSet, fiber) + } + } else { + const port = ports.get(portId) + if (!port) { + return + } else if (ports.size === 1) { + // let the last port close with the outer scope + return Deferred.unsafeDone(closeLatch, Exit.void) + } + ports.delete(portId) + Effect.runFork(Scope.close(port[1], Exit.void)) + } + } + } + function onMessageError(error: MessageEvent) { + Deferred.unsafeDone( + closeLatch, + new WorkerError({ reason: "decode", cause: error.data }) + ) + } + function onError(error: any) { + Deferred.unsafeDone( + closeLatch, + new WorkerError({ reason: "unknown", cause: error.data }) + ) + } + function handlePort(port: MessagePort) { + const fiber = Scope.fork(scope, ExecStrategy.sequential).pipe( + Effect.flatMap((scope) => { + const portId = currentPortId++ + ports.set(portId, [port, scope]) + const onMsg = onMessage(portId) + port.addEventListener("message", onMsg) + port.addEventListener("messageerror", onMessageError) + if ("start" in port) { + port.start() + } + port.postMessage([0]) + return Scope.addFinalizer( + scope, + Effect.sync(() => { + port.removeEventListener("message", onMsg) + port.removeEventListener("messageerror", onError) + port.close() + }) + ) + }), + runFork + ) + fiber.addObserver(onExit) + FiberSet.unsafeAdd(fiberSet, fiber) + } + self.addEventListener("error", onError) + let prevOnConnect: unknown | undefined + if ("onconnect" in self) { + prevOnConnect = self.onconnect + self.onconnect = function(event: MessageEvent) { + const port = (event as MessageEvent).ports[0] + handlePort(port) + } + for (const port of cachedPorts) { + handlePort(port) + } + cachedPorts.clear() + yield* Scope.addFinalizer( + scope, + Effect.sync(() => self.close()) + ) + } else { + handlePort(self as any) + } + yield* Scope.addFinalizer( + scope, + Effect.sync(() => { + self.removeEventListener("error", onError) + if ("onconnect" in self) { + self.onconnect = prevOnConnect + } + }) + ) + }) + + return identity>({ run, send, disconnects }) + }) as any + }) + +/** @internal */ +export const layerMessagePort = (port: MessagePort | Window) => Layer.succeed(Runner.PlatformRunner, make(port)) + +/** @internal */ +export const layer = Layer.sync(Runner.PlatformRunner, () => make(self)) diff --git a/repos/effect/packages/platform-browser/test/BrowserHttpClient.test.ts b/repos/effect/packages/platform-browser/test/BrowserHttpClient.test.ts new file mode 100644 index 0000000..ded77ce --- /dev/null +++ b/repos/effect/packages/platform-browser/test/BrowserHttpClient.test.ts @@ -0,0 +1,96 @@ +import { Cookies, HttpClient } from "@effect/platform" +import { BrowserHttpClient } from "@effect/platform-browser" +import { assert, describe, it } from "@effect/vitest" +import { Chunk, Effect, Layer, Stream } from "effect" +import * as MXHR from "mock-xmlhttprequest" + +const layer = (...args: Parameters) => + Layer.unwrapEffect(Effect.sync(() => { + const server = MXHR.newServer(...args) + return BrowserHttpClient.layerXMLHttpRequest.pipe( + Layer.provide(Layer.succeed(BrowserHttpClient.XMLHttpRequest, server.xhrFactory)) + ) + })) + +describe("BrowserHttpClient", () => { + it.effect("json", () => + Effect.gen(function*() { + const body = yield* HttpClient.get("http://localhost:8080/my/url").pipe( + Effect.flatMap((_) => _.json) + ) + assert.deepStrictEqual(body, { message: "Success!" }) + }).pipe(Effect.provide(layer({ + get: ["http://localhost:8080/my/url", { + headers: { "Content-Type": "application/json" }, + body: "{ \"message\": \"Success!\" }" + }] + })))) + + it.effect("stream", () => + Effect.gen(function*() { + const body = yield* HttpClient.get("http://localhost:8080/my/url").pipe( + Effect.map((_) => + _.stream.pipe( + Stream.decodeText(), + Stream.mkString + ) + ), + Stream.unwrapScoped, + Stream.runCollect + ) + assert.deepStrictEqual(Chunk.unsafeHead(body), "{ \"message\": \"Success!\" }") + }).pipe(Effect.provide(layer({ + get: ["http://localhost:8080/my/url", { + headers: { "Content-Type": "application/json" }, + body: "{ \"message\": \"Success!\" }" + }] + })))) + + it.effect("cookies", () => + Effect.gen(function*() { + const cookies = yield* HttpClient.get("http://localhost:8080/my/url").pipe( + Effect.map((res) => res.cookies) + ) + assert.deepStrictEqual(Cookies.toRecord(cookies), { + foo: "bar" + }) + }).pipe( + Effect.provide(layer({ + get: ["http://localhost:8080/my/url", { + headers: { "Content-Type": "application/json", "Set-Cookie": "foo=bar; HttpOnly; Secure" }, + body: "{ \"message\": \"Success!\" }" + }] + })) + )) + + it.effect("arrayBuffer", () => + Effect.gen(function*() { + const body = yield* HttpClient.get("http://localhost:8080/my/url").pipe( + Effect.flatMap((_) => _.arrayBuffer), + BrowserHttpClient.withXHRArrayBuffer + ) + assert.strictEqual(new TextDecoder().decode(body), "{ \"message\": \"Success!\" }") + }).pipe( + Effect.provide(layer({ + get: ["http://localhost:8080/my/url", { + headers: { "Content-Type": "application/json" }, + body: "{ \"message\": \"Success!\" }" + }] + })) + )) + + it.effect("arrayBuffer without withXHRArrayBuffer", () => + Effect.gen(function*() { + const body = yield* HttpClient.get("http://localhost:8080/my/url").pipe( + Effect.flatMap((_) => _.arrayBuffer) + ) + assert.strictEqual(new TextDecoder().decode(body), "{ \"message\": \"Success!\" }") + }).pipe( + Effect.provide(layer({ + get: ["http://localhost:8080/my/url", { + headers: { "Content-Type": "application/json" }, + body: "{ \"message\": \"Success!\" }" + }] + })) + )) +}) diff --git a/repos/effect/packages/platform-browser/test/KeyValueStore.test.ts b/repos/effect/packages/platform-browser/test/KeyValueStore.test.ts new file mode 100644 index 0000000..e91d1c4 --- /dev/null +++ b/repos/effect/packages/platform-browser/test/KeyValueStore.test.ts @@ -0,0 +1,7 @@ +import * as Kv from "@effect/platform-browser/BrowserKeyValueStore" +import { describe } from "@effect/vitest" +// @ts-ignore +import { testLayer } from "../../platform/test/KeyValueStore.test.js" + +describe("KeyValueStore / layerLocalStorage", () => testLayer(Kv.layerLocalStorage)) +describe("KeyValueStore / layerSessionStorage", () => testLayer(Kv.layerSessionStorage)) diff --git a/repos/effect/packages/platform-browser/test/Permissions.test.ts b/repos/effect/packages/platform-browser/test/Permissions.test.ts new file mode 100644 index 0000000..a964f95 --- /dev/null +++ b/repos/effect/packages/platform-browser/test/Permissions.test.ts @@ -0,0 +1,12 @@ +import { Permissions } from "@effect/platform-browser" +import { assert, describe, it } from "@effect/vitest" +import { Effect } from "effect" + +describe("Permissions", () => { + it.effect("should be able to query permissions", () => + Effect.gen(function*() { + const service = yield* Permissions.Permissions + const permissions = yield* service.query("geolocation") + assert.strictEqual(permissions.state, "granted") + }).pipe(Effect.provide(Permissions.layer))) +}) diff --git a/repos/effect/packages/platform-browser/test/RpcWorker.test.ts b/repos/effect/packages/platform-browser/test/RpcWorker.test.ts new file mode 100644 index 0000000..e9bfea1 --- /dev/null +++ b/repos/effect/packages/platform-browser/test/RpcWorker.test.ts @@ -0,0 +1,20 @@ +import "@vitest/web-worker" + +import * as BrowserWorker from "@effect/platform-browser/BrowserWorker" +import * as RpcClient from "@effect/rpc/RpcClient" +import * as RpcServer from "@effect/rpc/RpcServer" +import { describe } from "@effect/vitest" +import { Layer } from "effect" +import { UsersClient } from "./fixtures/rpc-schemas.js" +import { e2eSuite } from "./rpc-e2e.js" + +describe("RpcWorker", () => { + const WorkerClient = UsersClient.layer.pipe( + Layer.provide(RpcClient.layerProtocolWorker({ size: 1 })), + Layer.provide(BrowserWorker.layerPlatform(() => new Worker(new URL("./fixtures/rpc-worker.ts", import.meta.url)))), + Layer.merge(Layer.succeed(RpcServer.Protocol, { + supportsAck: true + } as any)) + ) + e2eSuite("e2e worker", WorkerClient, false) +}) diff --git a/repos/effect/packages/platform-browser/test/Worker.test.ts b/repos/effect/packages/platform-browser/test/Worker.test.ts new file mode 100644 index 0000000..cc49453 --- /dev/null +++ b/repos/effect/packages/platform-browser/test/Worker.test.ts @@ -0,0 +1,141 @@ +import * as BrowserWorker from "@effect/platform-browser/BrowserWorker" +import * as EffectWorker from "@effect/platform/Worker" +import "@vitest/web-worker" +import { assert, describe, it } from "@effect/vitest" +import { Chunk, Effect, Exit, Option, Stream } from "effect" +import type { WorkerMessage } from "./fixtures/schema.js" +import { + GetPersonById, + GetSpan, + GetUserById, + InitialMessage, + Person, + RunnerInterrupt, + User +} from "./fixtures/schema.js" + +describe.sequential("Worker", () => { + it("executes streams", () => + Effect.gen(function*() { + const pool = yield* EffectWorker.makePool({ + size: 1 + }) + const items = yield* pool.execute(99).pipe(Stream.runCollect) + assert.strictEqual(items.length, 100) + }).pipe( + Effect.scoped, + Effect.provide( + BrowserWorker.layer(() => new globalThis.Worker(new URL("./fixtures/worker.ts", import.meta.url))) + ), + Effect.runPromise + )) + + it("Serialized", () => + Effect.gen(function*() { + const pool = yield* EffectWorker.makePoolSerialized({ size: 1 }) + const people = yield* pool.execute(new GetPersonById({ id: 123 })).pipe(Stream.runCollect) + assert.deepStrictEqual(Chunk.toReadonlyArray(people), [ + new Person({ id: 123, name: "test", data: new Uint8Array([1, 2, 3]) }), + new Person({ id: 123, name: "ing", data: new Uint8Array([4, 5, 6]) }) + ]) + }).pipe( + Effect.scoped, + Effect.provide( + BrowserWorker.layer(() => new globalThis.Worker(new URL("./fixtures/serializedWorker.ts", import.meta.url))) + ), + Effect.runPromise + )) + + it("Serialized with initialMessage", () => + Effect.gen(function*() { + const pool = yield* EffectWorker.makePoolSerialized({ + size: 1, + initialMessage: () => new InitialMessage({ name: "custom", data: new Uint8Array([1, 2, 3]) }) + }) + const user = yield* pool.executeEffect(new GetUserById({ id: 123 })) + assert.deepStrictEqual(user, new User({ id: 123, name: "custom" })) + const people = yield* pool.execute(new GetPersonById({ id: 123 })).pipe(Stream.runCollect) + assert.deepStrictEqual(Chunk.toReadonlyArray(people), [ + new Person({ id: 123, name: "test", data: new Uint8Array([1, 2, 3]) }), + new Person({ id: 123, name: "ing", data: new Uint8Array([4, 5, 6]) }) + ]) + }).pipe( + Effect.scoped, + Effect.provide( + BrowserWorker.layer(() => new globalThis.Worker(new URL("./fixtures/serializedWorker.ts", import.meta.url))) + ), + Effect.runPromise + )) + + it("tracing", () => + Effect.gen(function*() { + const parentSpan = yield* Effect.currentSpan + const pool = yield* EffectWorker.makePoolSerialized({ + size: 1 + }) + const span = yield* pool.executeEffect(new GetSpan()).pipe(Effect.tapErrorCause(Effect.log)) + assert.deepStrictEqual( + span.parent, + Option.some({ + traceId: parentSpan.traceId, + spanId: parentSpan.spanId + }) + ) + }).pipe( + Effect.withSpan("test"), + Effect.scoped, + Effect.provide( + BrowserWorker.layer(() => new globalThis.Worker(new URL("./fixtures/serializedWorker.ts", import.meta.url))) + ), + Effect.runPromise + )) + + it("SharedWorker", () => + Effect.gen(function*() { + const pool = yield* EffectWorker.makePool({ + size: 1 + }) + const items = yield* pool.execute(99).pipe(Stream.runCollect) + assert.strictEqual(items.length, 100) + }).pipe( + Effect.scoped, + Effect.provide( + BrowserWorker.layer(() => new globalThis.SharedWorker(new URL("./fixtures/worker.ts", import.meta.url))) + ), + Effect.runPromise + )) + + // TODO: vitest/web-worker doesn't support postMessage throwing errors + // it("send error", () => + // Effect.gen(function*(_) { + // const pool = yield* _(EffectWorker.makePool({ + // spawn: () => new globalThis.Worker(new URL("./fixtures/worker.ts", import.meta.url)), + // transfers(_message) { + // return [new Uint8Array([1, 2, 3])] + // }, + // size: 1 + // })) + // const items = yield* _(pool.execute(99), Stream.runCollect, Effect.flip) + // console.log(items) + // }).pipe(Effect.scoped, Effect.provide(EffectWorker.layerManager), Effect.runPromise)) + + it.scoped("interrupt runner", () => + Effect.gen(function*() { + const pool = yield* EffectWorker.makePoolSerialized({ + size: 1, + initialMessage: () => new InitialMessage({ name: "custom", data: new Uint8Array([1, 2, 3]) }) + }) + + const exit = yield* pool.execute(new RunnerInterrupt()).pipe( + Stream.runDrain, + Effect.exit + ) + assert.isTrue(Exit.isInterrupted(exit)) + }).pipe( + Effect.provide( + BrowserWorker.layer(() => + new globalThis.SharedWorker(new URL("./fixtures/serializedWorker.ts", import.meta.url)) + ) + ) + )) +}) diff --git a/repos/effect/packages/platform-browser/test/fixtures/rpc-schemas.ts b/repos/effect/packages/platform-browser/test/fixtures/rpc-schemas.ts new file mode 120000 index 0000000..ff01864 --- /dev/null +++ b/repos/effect/packages/platform-browser/test/fixtures/rpc-schemas.ts @@ -0,0 +1 @@ +../../../platform-node/test/fixtures/rpc-schemas.ts \ No newline at end of file diff --git a/repos/effect/packages/platform-browser/test/fixtures/rpc-worker.ts b/repos/effect/packages/platform-browser/test/fixtures/rpc-worker.ts new file mode 100644 index 0000000..ff89fb5 --- /dev/null +++ b/repos/effect/packages/platform-browser/test/fixtures/rpc-worker.ts @@ -0,0 +1,12 @@ +import { BrowserWorkerRunner } from "@effect/platform-browser" +import * as RpcServer from "@effect/rpc/RpcServer" +import * as Effect from "effect/Effect" +import * as Layer from "effect/Layer" +import { RpcLive } from "./rpc-schemas.js" + +const MainLive = RpcLive.pipe( + Layer.provide(RpcServer.layerProtocolWorkerRunner), + Layer.provide(BrowserWorkerRunner.layer) +) + +Effect.runFork(BrowserWorkerRunner.launch(MainLive)) diff --git a/repos/effect/packages/platform-browser/test/fixtures/schema.ts b/repos/effect/packages/platform-browser/test/fixtures/schema.ts new file mode 100644 index 0000000..b1875d4 --- /dev/null +++ b/repos/effect/packages/platform-browser/test/fixtures/schema.ts @@ -0,0 +1,64 @@ +import * as Transferable from "@effect/platform/Transferable" +import * as Schema from "effect/Schema" + +export class User extends Schema.Class("User")({ + id: Schema.Number, + name: Schema.String +}) {} + +export class GetUserById extends Schema.TaggedRequest()("GetUserById", { + failure: Schema.Never, + success: User, + payload: { + id: Schema.Number + } +}) {} + +export class Person extends Schema.Class("Person")({ + id: Schema.Number, + name: Schema.String, + data: Transferable.Uint8Array +}) {} + +export class GetPersonById extends Schema.TaggedRequest()("GetPersonById", { + failure: Schema.Never, + success: Person, + payload: { + id: Schema.Number + } +}) {} + +export class RunnerInterrupt extends Schema.TaggedRequest()("RunnerInterrupt", { + failure: Schema.Never, + success: Schema.Void, + payload: {} +}) {} + +export class InitialMessage extends Schema.TaggedRequest()("InitialMessage", { + failure: Schema.Never, + success: Schema.Void, + payload: { + name: Schema.String, + data: Transferable.Uint8Array + } +}) {} + +export class GetSpan extends Schema.TaggedRequest()( + "GetSpan", + { + failure: Schema.Never, + success: Schema.Struct({ + name: Schema.String, + traceId: Schema.String, + spanId: Schema.String, + parent: Schema.Option(Schema.Struct({ + traceId: Schema.String, + spanId: Schema.String + })) + }), + payload: {} + } +) {} + +export const WorkerMessage = Schema.Union(GetUserById, GetPersonById, InitialMessage, GetSpan, RunnerInterrupt) +export type WorkerMessage = Schema.Schema.Type diff --git a/repos/effect/packages/platform-browser/test/fixtures/serializedWorker.ts b/repos/effect/packages/platform-browser/test/fixtures/serializedWorker.ts new file mode 100644 index 0000000..d7649a6 --- /dev/null +++ b/repos/effect/packages/platform-browser/test/fixtures/serializedWorker.ts @@ -0,0 +1,38 @@ +import * as BrowserRunner from "@effect/platform-browser/BrowserWorkerRunner" +import * as Runner from "@effect/platform/WorkerRunner" +import { Context, Effect, Layer, Option, Stream } from "effect" +import { Person, User, WorkerMessage } from "./schema.js" + +interface Name { + readonly _: unique symbol +} +const Name = Context.GenericTag("Name") + +const WorkerLive = Runner.layerSerialized(WorkerMessage, { + GetPersonById: (req) => + Stream.make( + new Person({ id: req.id, name: "test", data: new Uint8Array([1, 2, 3]) }), + new Person({ id: req.id, name: "ing", data: new Uint8Array([4, 5, 6]) }) + ), + GetUserById: (req) => Effect.map(Name, (name) => new User({ id: req.id, name })), + InitialMessage: (req) => Layer.succeed(Name, req.name), + GetSpan: (_) => + Effect.gen(function*() { + const span = yield* Effect.currentSpan.pipe(Effect.orDie) + return { + traceId: span.traceId, + spanId: span.spanId, + name: span.name, + parent: Option.map(span.parent, (span) => ({ + traceId: span.traceId, + spanId: span.spanId + })) + } + }).pipe(Effect.withSpan("GetSpan")), + RunnerInterrupt: () => Effect.interrupt +}) + .pipe( + Layer.provide(BrowserRunner.layer) + ) + +Effect.runFork(Runner.launch(WorkerLive)) diff --git a/repos/effect/packages/platform-browser/test/fixtures/worker.ts b/repos/effect/packages/platform-browser/test/fixtures/worker.ts new file mode 100644 index 0000000..171d8c1 --- /dev/null +++ b/repos/effect/packages/platform-browser/test/fixtures/worker.ts @@ -0,0 +1,9 @@ +import * as BrowserRunner from "@effect/platform-browser/BrowserWorkerRunner" +import * as Runner from "@effect/platform/WorkerRunner" +import { Effect, Layer, Stream } from "effect" + +const WorkerLive = Runner.layer((n: number) => Stream.range(0, n)).pipe( + Layer.provide(BrowserRunner.layer) +) + +Effect.runFork(Runner.launch(WorkerLive)) diff --git a/repos/effect/packages/platform-browser/test/rpc-e2e.ts b/repos/effect/packages/platform-browser/test/rpc-e2e.ts new file mode 120000 index 0000000..f502a69 --- /dev/null +++ b/repos/effect/packages/platform-browser/test/rpc-e2e.ts @@ -0,0 +1 @@ +../../platform-node/test/rpc-e2e.ts \ No newline at end of file diff --git a/repos/effect/packages/platform-browser/tsconfig.build.json b/repos/effect/packages/platform-browser/tsconfig.build.json new file mode 100644 index 0000000..472aa65 --- /dev/null +++ b/repos/effect/packages/platform-browser/tsconfig.build.json @@ -0,0 +1,13 @@ +{ + "extends": "./tsconfig.src.json", + "references": [ + { "path": "../effect/tsconfig.build.json" }, + { "path": "../platform/tsconfig.build.json" } + ], + "compilerOptions": { + "tsBuildInfoFile": ".tsbuildinfo/build.tsbuildinfo", + "outDir": "build/esm", + "declarationDir": "build/dts", + "stripInternal": true + } +} diff --git a/repos/effect/packages/platform-browser/tsconfig.examples.json b/repos/effect/packages/platform-browser/tsconfig.examples.json new file mode 100644 index 0000000..faf5ae0 --- /dev/null +++ b/repos/effect/packages/platform-browser/tsconfig.examples.json @@ -0,0 +1,12 @@ +{ + "extends": "../../tsconfig.base.json", + "include": ["examples"], + "references": [ + { "path": "tsconfig.build.json" } + ], + "compilerOptions": { + "tsBuildInfoFile": ".tsbuildinfo/examples.tsbuildinfo", + "rootDir": "examples", + "noEmit": true + } +} diff --git a/repos/effect/packages/platform-browser/tsconfig.json b/repos/effect/packages/platform-browser/tsconfig.json new file mode 100644 index 0000000..3edbf6b --- /dev/null +++ b/repos/effect/packages/platform-browser/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.base.json", + "include": [], + "references": [ + { "path": "tsconfig.src.json" }, + { "path": "tsconfig.test.json" }, + { "path": "tsconfig.examples.json" } + ] +} diff --git a/repos/effect/packages/platform-browser/tsconfig.src.json b/repos/effect/packages/platform-browser/tsconfig.src.json new file mode 100644 index 0000000..9c2b466 --- /dev/null +++ b/repos/effect/packages/platform-browser/tsconfig.src.json @@ -0,0 +1,13 @@ +{ + "extends": "../../tsconfig.base.json", + "include": ["src"], + "references": [ + { "path": "../effect/tsconfig.src.json" }, + { "path": "../platform/tsconfig.src.json" } + ], + "compilerOptions": { + "tsBuildInfoFile": ".tsbuildinfo/src.tsbuildinfo", + "rootDir": "src", + "outDir": "build/src" + } +} diff --git a/repos/effect/packages/platform-browser/tsconfig.test.json b/repos/effect/packages/platform-browser/tsconfig.test.json new file mode 100644 index 0000000..6571534 --- /dev/null +++ b/repos/effect/packages/platform-browser/tsconfig.test.json @@ -0,0 +1,15 @@ +{ + "extends": "../../tsconfig.base.json", + "include": ["test"], + "references": [ + { "path": "tsconfig.src.json" }, + { "path": "../effect/tsconfig.test.json" }, // We import test files from `effect`. + { "path": "../rpc/tsconfig.src.json" }, + { "path": "../vitest/tsconfig.src.json" } + ], + "compilerOptions": { + "tsBuildInfoFile": ".tsbuildinfo/test.tsbuildinfo", + "rootDir": "test", + "noEmit": true + } +} diff --git a/repos/effect/packages/platform-browser/vitest.config.ts b/repos/effect/packages/platform-browser/vitest.config.ts new file mode 100644 index 0000000..3a190f9 --- /dev/null +++ b/repos/effect/packages/platform-browser/vitest.config.ts @@ -0,0 +1,10 @@ +import { mergeConfig, type ViteUserConfig } from "vitest/config" +import shared from "../../vitest.shared.js" + +const config: ViteUserConfig = { + test: { + environment: "happy-dom" + } +} + +export default mergeConfig(shared, config) diff --git a/repos/effect/packages/platform-bun/CHANGELOG.md b/repos/effect/packages/platform-bun/CHANGELOG.md new file mode 100644 index 0000000..21362c5 --- /dev/null +++ b/repos/effect/packages/platform-bun/CHANGELOG.md @@ -0,0 +1,5237 @@ +# @effect/platform-bun + +## 0.89.0 + +### Patch Changes + +- Updated dependencies [[`f7bb09b`](https://github.com/Effect-TS/effect/commit/f7bb09b022f195d1f2b3c23d49e74b011ec5d109), [`bd7552a`](https://github.com/Effect-TS/effect/commit/bd7552a19cc0ed575507ac6cc0879a57e24ebd31), [`ad1a7eb`](https://github.com/Effect-TS/effect/commit/ad1a7eb7f6bebaf91c80be2443ac0439226d0098), [`0d32048`](https://github.com/Effect-TS/effect/commit/0d32048f9836e2b23a6ba3ec5f43f0a000bb92fb), [`0d32048`](https://github.com/Effect-TS/effect/commit/0d32048f9836e2b23a6ba3ec5f43f0a000bb92fb)]: + - effect@3.21.0 + - @effect/cluster@0.58.0 + - @effect/platform@0.96.0 + - @effect/platform-node-shared@0.59.0 + - @effect/rpc@0.75.0 + - @effect/sql@0.51.0 + +## 0.88.0 + +### Patch Changes + +- Updated dependencies [[`fc82e81`](https://github.com/Effect-TS/effect/commit/fc82e81448bd9136a37580139ce46a2c61b11b54), [`82996bc`](https://github.com/Effect-TS/effect/commit/82996bce8debffcb44feb98bb862cf2662bd56b7), [`4d97a61`](https://github.com/Effect-TS/effect/commit/4d97a61a15b9dd6a0eece65b8f0c035e16d42ada), [`f6b0960`](https://github.com/Effect-TS/effect/commit/f6b0960bf3184109920dfed16ee7dfd7d67bc0f2), [`8798a84`](https://github.com/Effect-TS/effect/commit/8798a843218e6c0c0d3a8eee83360880e370b4da)]: + - effect@3.20.0 + - @effect/cluster@0.57.0 + - @effect/platform@0.95.0 + - @effect/platform-node-shared@0.58.0 + - @effect/rpc@0.74.0 + - @effect/sql@0.50.0 + +## 0.87.1 + +### Patch Changes + +- [#5977](https://github.com/Effect-TS/effect/pull/5977) [`118e7a4`](https://github.com/Effect-TS/effect/commit/118e7a4af5b86f6d707a40d3b03157b6bf5827e7) Thanks @scotttrinh! - Added `rows` and `isTTY` properties to `Terminal` + +- Updated dependencies [[`7e925ea`](https://github.com/Effect-TS/effect/commit/7e925eae4a9db556bcbf7e8b6a762ccf8588aa3b), [`118e7a4`](https://github.com/Effect-TS/effect/commit/118e7a4af5b86f6d707a40d3b03157b6bf5827e7), [`d7e75d6`](https://github.com/Effect-TS/effect/commit/d7e75d6d15294bbcd7ac49a0e9005848379ea86f), [`4860d1e`](https://github.com/Effect-TS/effect/commit/4860d1e09b436061ea4aeca07605a669793560fc)]: + - effect@3.19.15 + - @effect/platform-node-shared@0.57.1 + - @effect/platform@0.94.2 + +## 0.87.0 + +### Patch Changes + +- Updated dependencies [[`77eeb86`](https://github.com/Effect-TS/effect/commit/77eeb86ddf208e51ec25932af83d52d3b4700371), [`ff7053f`](https://github.com/Effect-TS/effect/commit/ff7053f6d8508567b6145239f97aacc5773b0c53), [`287c32c`](https://github.com/Effect-TS/effect/commit/287c32c9f10da8e96f2b9ef8424316189d9ad4b3)]: + - effect@3.19.13 + - @effect/platform@0.94.0 + - @effect/cluster@0.56.0 + - @effect/platform-node-shared@0.57.0 + - @effect/rpc@0.73.0 + - @effect/sql@0.49.0 + +## 0.86.0 + +### Patch Changes + +- Updated dependencies [[`811852a`](https://github.com/Effect-TS/effect/commit/811852a61868136bb7b3367450f02e5a8fb8a3f9)]: + - @effect/sql@0.48.6 + - @effect/cluster@0.55.0 + - @effect/platform-node-shared@0.56.0 + +## 0.85.0 + +### Patch Changes + +- Updated dependencies []: + - @effect/cluster@0.54.0 + - @effect/platform-node-shared@0.55.0 + +## 0.84.0 + +### Patch Changes + +- Updated dependencies [[`794c790`](https://github.com/Effect-TS/effect/commit/794c790d736f62784bff800fda5a656026d93749), [`079975c`](https://github.com/Effect-TS/effect/commit/079975c69d80c62461da5c51fe89e02c44dfa2ea), [`62f7636`](https://github.com/Effect-TS/effect/commit/62f76361ee01ed816687774c5302e7f8c5ff6a42)]: + - @effect/rpc@0.72.2 + - effect@3.19.5 + - @effect/cluster@0.53.0 + - @effect/platform-node-shared@0.54.0 + +## 0.83.0 + +### Patch Changes + +- Updated dependencies [[`571025c`](https://github.com/Effect-TS/effect/commit/571025ceaff6ef432a61bf65735a5a0f45118313), [`d43577b`](https://github.com/Effect-TS/effect/commit/d43577be59ae510812287b1cbffe6da15c040452)]: + - @effect/cluster@0.52.0 + - @effect/sql@0.48.0 + - @effect/rpc@0.72.1 + - @effect/platform-node-shared@0.53.0 + +## 0.82.0 + +### Minor Changes + +- [#5606](https://github.com/Effect-TS/effect/pull/5606) [`24a1685`](https://github.com/Effect-TS/effect/commit/24a1685c70a9ed157468650f95a5c3da3f2c2433) Thanks @tim-smart! - backport @effect/cluster from effect v4 + + @effect/cluster no longer requires a Shard Manager, and instead relies on the + `RunnerStorage` service to track runner state. + + To migrate, remove any Shard Manager deployments and use the updated layers in + `@effect/platform-node` or `@effect/platform-bun`. + + # Breaking Changes + - `ShardManager` module has been removed + - `EntityNotManagedByRunner` error has been removed + - Shard locks now use database advisory locks, which requires stable sessions + for database connections. This means load balancers or proxies that rotate + connections may cause issues. + - `@effect/platform-node/NodeClusterSocketRunner` is now + `@effect/cluster/NodeClusterSocket` + - `@effect/platform-node/NodeClusterHttpRunner` is now + `@effect/cluster/NodeClusterHttp` + - `@effect/platform-bun/BunClusterSocketRunner` is now + `@effect/cluster/BunClusterSocket` + - `@effect/platform-bun/BunClusterHttpRunner` is now + `@effect/cluster/BunClusterHttp` + + # New Features + - `RunnerHealth.layerK8s` has been added, which uses the Kubernetes API to track + runner health and liveness. To use it, you will need a service account with + permissions to read pod information. + +### Patch Changes + +- Updated dependencies [[`3c15d5f`](https://github.com/Effect-TS/effect/commit/3c15d5f99fb8d8470a00c5a33d9ba3cac89dfe4c), [`3863fa8`](https://github.com/Effect-TS/effect/commit/3863fa89f61e63e5529fd961e37333bddf7db64a), [`2a03c76`](https://github.com/Effect-TS/effect/commit/2a03c76c2781ca7e9e228e838eab2eb0d0795b1d), [`24a1685`](https://github.com/Effect-TS/effect/commit/24a1685c70a9ed157468650f95a5c3da3f2c2433), [`24a1685`](https://github.com/Effect-TS/effect/commit/24a1685c70a9ed157468650f95a5c3da3f2c2433)]: + - effect@3.19.0 + - @effect/platform-node-shared@0.52.0 + - @effect/cluster@0.51.0 + - @effect/rpc@0.72.0 + - @effect/platform@0.93.0 + - @effect/sql@0.47.0 + +## 0.81.1 + +### Patch Changes + +- [#5602](https://github.com/Effect-TS/effect/pull/5602) [`64b764b`](https://github.com/Effect-TS/effect/commit/64b764b3207eb13cacb13da31343aaf425e966bf) Thanks @tim-smart! - guard against race conditions in NodeSocketServer + +- Updated dependencies [[`64b764b`](https://github.com/Effect-TS/effect/commit/64b764b3207eb13cacb13da31343aaf425e966bf)]: + - @effect/cluster@0.50.3 + - @effect/platform-node-shared@0.51.3 + +## 0.81.0 + +### Patch Changes + +- Updated dependencies [[`1c6ab74`](https://github.com/Effect-TS/effect/commit/1c6ab74b314b2b6df8bb1b1a0cb9527ceda0e3fa), [`70fe803`](https://github.com/Effect-TS/effect/commit/70fe803469db3355ffbf8359b52c351f1c2dc137), [`c296e32`](https://github.com/Effect-TS/effect/commit/c296e32554143b84ae8987046984e1cf1852417c), [`a098ddf`](https://github.com/Effect-TS/effect/commit/a098ddfc551f5aa0a7c36f9b4928372a64d4d9f2)]: + - effect@3.18.0 + - @effect/platform@0.92.0 + - @effect/cluster@0.50.0 + - @effect/platform-node-shared@0.51.0 + - @effect/rpc@0.71.0 + - @effect/sql@0.46.0 + +## 0.80.0 + +### Patch Changes + +- Updated dependencies [[`d4d86a8`](https://github.com/Effect-TS/effect/commit/d4d86a81f02b94e09fce8004ce2c5369c505ca5a)]: + - @effect/platform@0.91.0 + - @effect/rpc@0.70.0 + - @effect/cluster@0.49.0 + - @effect/platform-node-shared@0.50.0 + - @effect/sql@0.45.0 + +## 0.79.1 + +### Patch Changes + +- [#5517](https://github.com/Effect-TS/effect/pull/5517) [`de07e58`](https://github.com/Effect-TS/effect/commit/de07e5805496b80226ba6a5efc2b4c05e1aba4b8) Thanks @tim-smart! - add onOpen option to Socket.run + +- Updated dependencies [[`de07e58`](https://github.com/Effect-TS/effect/commit/de07e5805496b80226ba6a5efc2b4c05e1aba4b8), [`de07e58`](https://github.com/Effect-TS/effect/commit/de07e5805496b80226ba6a5efc2b4c05e1aba4b8)]: + - @effect/platform-node-shared@0.49.2 + - @effect/cluster@0.48.10 + - @effect/platform@0.90.10 + - @effect/rpc@0.69.3 + +## 0.79.0 + +### Patch Changes + +- Updated dependencies [[`3e163b2`](https://github.com/Effect-TS/effect/commit/3e163b24cc2b647e25566ba29ef25c3f57609042)]: + - @effect/rpc@0.69.0 + - @effect/cluster@0.48.0 + - @effect/platform-node-shared@0.49.0 + +## 0.78.0 + +### Patch Changes + +- Updated dependencies [[`a949539`](https://github.com/Effect-TS/effect/commit/a94953971c2e908890dfda00f8560d317306c328), [`a949539`](https://github.com/Effect-TS/effect/commit/a94953971c2e908890dfda00f8560d317306c328)]: + - @effect/cluster@0.47.0 + - effect@3.17.7 + - @effect/platform-node-shared@0.48.0 + +## 0.77.1 + +### Patch Changes + +- [#5347](https://github.com/Effect-TS/effect/pull/5347) [`20f0d69`](https://github.com/Effect-TS/effect/commit/20f0d6978e0e98464f23b6582c37c6ce12319f29) Thanks @tim-smart! - update Cluster layer conditional storage types + +- Updated dependencies [[`d0b5fd1`](https://github.com/Effect-TS/effect/commit/d0b5fd1f7a292a47b9eeb058e5df57ace9a5ab14), [`20f0d69`](https://github.com/Effect-TS/effect/commit/20f0d6978e0e98464f23b6582c37c6ce12319f29)]: + - @effect/cluster@0.46.4 + - @effect/sql@0.44.1 + - @effect/platform-node-shared@0.47.2 + +## 0.77.0 + +### Patch Changes + +- Updated dependencies [[`5a0f4f1`](https://github.com/Effect-TS/effect/commit/5a0f4f176687a39d9fa46bb894bb7ac3175b0e87), [`e9cbd26`](https://github.com/Effect-TS/effect/commit/e9cbd2673401723aa811b0535202e4f57baf6d2c)]: + - effect@3.17.1 + - @effect/rpc@0.68.0 + - @effect/cluster@0.46.0 + - @effect/platform-node-shared@0.47.0 + +## 0.76.0 + +### Patch Changes + +- Updated dependencies [[`7813640`](https://github.com/Effect-TS/effect/commit/7813640279d9e3a3e7fc0a29bfb5c6d5fb3c270f)]: + - @effect/platform@0.90.0 + - @effect/cluster@0.45.0 + - @effect/platform-node-shared@0.46.0 + - @effect/rpc@0.67.0 + - @effect/sql@0.44.0 + +## 0.75.0 + +### Patch Changes + +- Updated dependencies [[`40c3c87`](https://github.com/Effect-TS/effect/commit/40c3c875f724264312b43002859c82bed9ad0df9), [`ed2c74a`](https://github.com/Effect-TS/effect/commit/ed2c74ae8fa4ea0dd06ea84a3e58cd32e6916104), [`073a1b8`](https://github.com/Effect-TS/effect/commit/073a1b8be5dbfa87454393ee7346f5bc36a4fd63), [`f382e99`](https://github.com/Effect-TS/effect/commit/f382e99e409838a879246250fc3994b9bf5b3c2c), [`e8c7ba5`](https://github.com/Effect-TS/effect/commit/e8c7ba5fd3eb0c3ae3039fc24c09d69391987989), [`7e10415`](https://github.com/Effect-TS/effect/commit/7e1041599ade25103428703f5d2dfd7378a09636), [`e9bdece`](https://github.com/Effect-TS/effect/commit/e9bdececdc24f60a246be5055eca71a0d49ea7f2), [`8d95eb0`](https://github.com/Effect-TS/effect/commit/8d95eb0356b1d1736204836c275d201a547d208d)]: + - effect@3.17.0 + - @effect/cluster@0.44.0 + - @effect/platform@0.89.0 + - @effect/platform-node-shared@0.45.0 + - @effect/rpc@0.66.0 + - @effect/sql@0.43.0 + +## 0.74.0 + +### Patch Changes + +- Updated dependencies [[`f5dfabf`](https://github.com/Effect-TS/effect/commit/f5dfabf51ba481a4468c1509c537314978ef6cec), [`17a5ea8`](https://github.com/Effect-TS/effect/commit/17a5ea8fa29785fe6e4c9480f2a2e9c8c59f3f38), [`d25f22b`](https://github.com/Effect-TS/effect/commit/d25f22be7598abe977caf6cdac3b0dd78b438c48)]: + - effect@3.16.14 + - @effect/sql@0.42.0 + - @effect/platform@0.88.1 + - @effect/cluster@0.43.0 + - @effect/platform-node-shared@0.44.0 + - @effect/rpc@0.65.1 + +## 0.73.0 + +### Patch Changes + +- Updated dependencies [[`27206d7`](https://github.com/Effect-TS/effect/commit/27206d7f0558d7fe28de57bf54f1d0cc83acc92e), [`dbabf5e`](https://github.com/Effect-TS/effect/commit/dbabf5e76fa63b050d2b6c466713c7dc59f07d3c)]: + - @effect/platform@0.88.0 + - @effect/cluster@0.42.0 + - @effect/platform-node-shared@0.43.0 + - @effect/rpc@0.65.0 + - @effect/sql@0.41.0 + +## 0.72.18 + +### Patch Changes + +- Updated dependencies [[`c1c05a8`](https://github.com/Effect-TS/effect/commit/c1c05a8242fb5df7445b4a12387a60eac7726eb7), [`5b7cd92`](https://github.com/Effect-TS/effect/commit/5b7cd923e786c38a0802faf0fe75498ab3cccf28), [`81fe4a2`](https://github.com/Effect-TS/effect/commit/81fe4a2c81d5e30e180a60e68c52016a27b350db)]: + - effect@3.16.13 + - @effect/rpc@0.64.14 + - @effect/cluster@0.41.18 + - @effect/platform@0.87.13 + - @effect/platform-node-shared@0.42.18 + - @effect/sql@0.40.14 + +## 0.72.17 + +### Patch Changes + +- Updated dependencies [[`56b33c3`](https://github.com/Effect-TS/effect/commit/56b33c357cfc5f8976486f48e93032058c02d876)]: + - @effect/cluster@0.41.17 + - @effect/platform-node-shared@0.42.17 + +## 0.72.16 + +### Patch Changes + +- Updated dependencies [[`32ba77a`](https://github.com/Effect-TS/effect/commit/32ba77ae304d2161362a73e8b61965332626cf2d), [`d5e25b2`](https://github.com/Effect-TS/effect/commit/d5e25b237f05670ee42b386cb40b2cb448fc11d7)]: + - @effect/platform@0.87.12 + - @effect/platform-node-shared@0.42.16 + - @effect/cluster@0.41.16 + - @effect/rpc@0.64.13 + - @effect/sql@0.40.13 + +## 0.72.15 + +### Patch Changes + +- Updated dependencies [[`79a1947`](https://github.com/Effect-TS/effect/commit/79a1947359cbd89a47ea315cdd86a3d250f28f43), [`001392b`](https://github.com/Effect-TS/effect/commit/001392ba8bfcad101bb034348a7415012fb12f72), [`7bfb099`](https://github.com/Effect-TS/effect/commit/7bfb099cb5528511b8d63045c4fbb4dc9cb18528)]: + - @effect/rpc@0.64.12 + - @effect/platform@0.87.11 + - @effect/cluster@0.41.15 + - @effect/platform-node-shared@0.42.15 + - @effect/sql@0.40.12 + +## 0.72.14 + +### Patch Changes + +- [#5175](https://github.com/Effect-TS/effect/pull/5175) [`678318d`](https://github.com/Effect-TS/effect/commit/678318d2e88233156b006acda56c2d138ee3ffa0) Thanks @tim-smart! - propagate headers to HttpServerResponse.raw(Response) + +- Updated dependencies [[`678318d`](https://github.com/Effect-TS/effect/commit/678318d2e88233156b006acda56c2d138ee3ffa0), [`678318d`](https://github.com/Effect-TS/effect/commit/678318d2e88233156b006acda56c2d138ee3ffa0)]: + - @effect/platform@0.87.10 + - @effect/cluster@0.41.14 + - @effect/platform-node-shared@0.42.14 + - @effect/rpc@0.64.11 + - @effect/sql@0.40.11 + +## 0.72.13 + +### Patch Changes + +- Updated dependencies [[`54514a2`](https://github.com/Effect-TS/effect/commit/54514a2f53166de27ad7e756dbf12194691fd4af)]: + - @effect/platform@0.87.9 + - @effect/cluster@0.41.13 + - @effect/platform-node-shared@0.42.13 + - @effect/rpc@0.64.10 + - @effect/sql@0.40.10 + +## 0.72.12 + +### Patch Changes + +- Updated dependencies [[`4ce4f82`](https://github.com/Effect-TS/effect/commit/4ce4f824f6fdef492be1d35c05a490ffce518c89)]: + - @effect/platform@0.87.8 + - @effect/cluster@0.41.12 + - @effect/platform-node-shared@0.42.12 + - @effect/rpc@0.64.9 + - @effect/sql@0.40.9 + +## 0.72.11 + +### Patch Changes + +- Updated dependencies [[`a9b617f`](https://github.com/Effect-TS/effect/commit/a9b617f125171ed76cd79ab46d7a924daf3b0e70), [`7e26e86`](https://github.com/Effect-TS/effect/commit/7e26e86524abcc93713d6ad7eee486638c98f7c2)]: + - @effect/platform@0.87.7 + - @effect/cluster@0.41.11 + - @effect/platform-node-shared@0.42.11 + - @effect/rpc@0.64.8 + - @effect/sql@0.40.8 + +## 0.72.10 + +### Patch Changes + +- Updated dependencies [[`905da99`](https://github.com/Effect-TS/effect/commit/905da996aad665057b4ca6dba1a4af44fb8835bd)]: + - effect@3.16.12 + - @effect/cluster@0.41.10 + - @effect/platform@0.87.6 + - @effect/platform-node-shared@0.42.10 + - @effect/rpc@0.64.7 + - @effect/sql@0.40.7 + +## 0.72.9 + +### Patch Changes + +- Updated dependencies []: + - @effect/sql@0.40.6 + - @effect/cluster@0.41.9 + - @effect/platform-node-shared@0.42.9 + +## 0.72.8 + +### Patch Changes + +- Updated dependencies [[`2fd8676`](https://github.com/Effect-TS/effect/commit/2fd8676c803cd40000dfc3231f5daecaa0e0ebd2)]: + - @effect/platform@0.87.5 + - @effect/cluster@0.41.8 + - @effect/platform-node-shared@0.42.8 + - @effect/rpc@0.64.6 + - @effect/sql@0.40.5 + +## 0.72.7 + +### Patch Changes + +- Updated dependencies [[`b01d2e0`](https://github.com/Effect-TS/effect/commit/b01d2e0d591418e10e9e362698205d848e97a9b7)]: + - @effect/cluster@0.41.7 + - @effect/platform-node-shared@0.42.7 + +## 0.72.6 + +### Patch Changes + +- Updated dependencies [[`7fdc16b`](https://github.com/Effect-TS/effect/commit/7fdc16bd88b872f5918384e4acda3731aab018da), [`e82a4fd`](https://github.com/Effect-TS/effect/commit/e82a4fd60f6528d08cef1a4aba0abe0d3ba741ad)]: + - @effect/cluster@0.41.6 + - @effect/platform@0.87.4 + - @effect/platform-node-shared@0.42.6 + - @effect/rpc@0.64.5 + - @effect/sql@0.40.4 + +## 0.72.5 + +### Patch Changes + +- [#5128](https://github.com/Effect-TS/effect/pull/5128) [`1b6e396`](https://github.com/Effect-TS/effect/commit/1b6e396d699f3cbbc56b68f99055cf746529bb9e) Thanks @tim-smart! - attach http request scope to stream lifetime for stream responses + +- Updated dependencies [[`1b6e396`](https://github.com/Effect-TS/effect/commit/1b6e396d699f3cbbc56b68f99055cf746529bb9e), [`46c3216`](https://github.com/Effect-TS/effect/commit/46c321657d93393506278327418e36f8e7a77f86)]: + - @effect/platform@0.87.3 + - @effect/sql@0.40.3 + - @effect/cluster@0.41.5 + - @effect/platform-node-shared@0.42.5 + - @effect/rpc@0.64.4 + +## 0.72.4 + +### Patch Changes + +- Updated dependencies [[`4fea68c`](https://github.com/Effect-TS/effect/commit/4fea68ca7a25a3c39a1ab68b3885534513ab0c81), [`b927954`](https://github.com/Effect-TS/effect/commit/b9279543cf5688dd8a577af80456959c615217d0), [`99590a6`](https://github.com/Effect-TS/effect/commit/99590a6ca9128eb1ede265b6670b655311995614), [`6c3e24c`](https://github.com/Effect-TS/effect/commit/6c3e24c2308f7d4a29b8f4270ab81bca22ac6bb4)]: + - @effect/platform@0.87.2 + - effect@3.16.11 + - @effect/cluster@0.41.4 + - @effect/platform-node-shared@0.42.4 + - @effect/rpc@0.64.3 + - @effect/sql@0.40.2 + +## 0.72.3 + +### Patch Changes + +- Updated dependencies [[`faad30e`](https://github.com/Effect-TS/effect/commit/faad30ec8742916be59f9db642d0fc98225b636c)]: + - effect@3.16.10 + - @effect/cluster@0.41.3 + - @effect/platform@0.87.1 + - @effect/platform-node-shared@0.42.3 + - @effect/rpc@0.64.2 + - @effect/sql@0.40.1 + +## 0.72.2 + +### Patch Changes + +- Updated dependencies [[`112a93a`](https://github.com/Effect-TS/effect/commit/112a93a9bab73e95e79f7b3502d1a7b1acd668fc)]: + - @effect/rpc@0.64.1 + - @effect/cluster@0.41.2 + - @effect/platform-node-shared@0.42.2 + +## 0.72.1 + +### Patch Changes + +- Updated dependencies [[`d5fd2c1`](https://github.com/Effect-TS/effect/commit/d5fd2c1526f06228853ed8317d9688c4af5f285a), [`9d189d7`](https://github.com/Effect-TS/effect/commit/9d189d744aa3307e055094c66f580453d95ff99d)]: + - @effect/cluster@0.41.1 + - @effect/platform-node-shared@0.42.1 + +## 0.72.0 + +### Patch Changes + +- Updated dependencies [[`b5bac9a`](https://github.com/Effect-TS/effect/commit/b5bac9ac2913fcd11b02322624f03b544eef53ba), [`867919c`](https://github.com/Effect-TS/effect/commit/867919c8be9a2f770699c0db852a3f566017ffd6)]: + - @effect/rpc@0.64.0 + - @effect/platform@0.87.0 + - @effect/cluster@0.41.0 + - @effect/platform-node-shared@0.42.0 + - @effect/sql@0.40.0 + +## 0.71.0 + +### Patch Changes + +- Updated dependencies [[`5137c70`](https://github.com/Effect-TS/effect/commit/5137c703461d8d3b363c112140a6e7f798241d07), [`c23d25c`](https://github.com/Effect-TS/effect/commit/c23d25c3e7c541f1f63b28484d8c461d86c67e99), [`5137c70`](https://github.com/Effect-TS/effect/commit/5137c703461d8d3b363c112140a6e7f798241d07), [`5137c70`](https://github.com/Effect-TS/effect/commit/5137c703461d8d3b363c112140a6e7f798241d07)]: + - effect@3.16.9 + - @effect/platform@0.86.0 + - @effect/cluster@0.40.0 + - @effect/platform-node-shared@0.41.0 + - @effect/rpc@0.63.0 + - @effect/sql@0.39.0 + +## 0.70.5 + +### Patch Changes + +- Updated dependencies [[`ff90206`](https://github.com/Effect-TS/effect/commit/ff90206fc56f5c1eb1675603652462a83a27421d)]: + - @effect/cluster@0.39.5 + - @effect/platform-node-shared@0.40.5 + +## 0.70.4 + +### Patch Changes + +- Updated dependencies [[`a8d99b2`](https://github.com/Effect-TS/effect/commit/a8d99b2ec2f55d9aa6e7d00a5138e80380716877)]: + - @effect/rpc@0.62.4 + - @effect/cluster@0.39.4 + - @effect/platform-node-shared@0.40.4 + +## 0.70.3 + +### Patch Changes + +- Updated dependencies [[`914a191`](https://github.com/Effect-TS/effect/commit/914a191e7cb6341a3d0e965bccd27c336cf22e44)]: + - @effect/platform@0.85.2 + - @effect/cluster@0.39.3 + - @effect/platform-node-shared@0.40.3 + - @effect/rpc@0.62.3 + - @effect/sql@0.38.2 + +## 0.70.2 + +### Patch Changes + +- Updated dependencies [[`ddfd1e4`](https://github.com/Effect-TS/effect/commit/ddfd1e43db60e3b779d18a221344423c5f3c7416)]: + - @effect/rpc@0.62.2 + - @effect/cluster@0.39.2 + - @effect/platform-node-shared@0.40.2 + +## 0.70.1 + +### Patch Changes + +- Updated dependencies [[`8cb98d5`](https://github.com/Effect-TS/effect/commit/8cb98d53e68330228287ce2a2e0d8a4c86bcab3b), [`db2dd3c`](https://github.com/Effect-TS/effect/commit/db2dd3c3a8a77d791eae19e66153527e1cde4e6e)]: + - effect@3.16.8 + - @effect/cluster@0.39.1 + - @effect/platform@0.85.1 + - @effect/platform-node-shared@0.40.1 + - @effect/rpc@0.62.1 + - @effect/sql@0.38.1 + +## 0.70.0 + +### Patch Changes + +- Updated dependencies [[`93687dd`](https://github.com/Effect-TS/effect/commit/93687ddbb25ce3b324cd2b83d2ccff225e97307e), [`93687dd`](https://github.com/Effect-TS/effect/commit/93687ddbb25ce3b324cd2b83d2ccff225e97307e), [`93687dd`](https://github.com/Effect-TS/effect/commit/93687ddbb25ce3b324cd2b83d2ccff225e97307e)]: + - @effect/platform@0.85.0 + - @effect/cluster@0.39.0 + - @effect/platform-node-shared@0.40.0 + - @effect/rpc@0.62.0 + - @effect/sql@0.38.0 + +## 0.69.16 + +### Patch Changes + +- Updated dependencies [[`1bb0d8a`](https://github.com/Effect-TS/effect/commit/1bb0d8ab96782e99434356266b38251554ea0294), [`cbac1ac`](https://github.com/Effect-TS/effect/commit/cbac1ac61a4e15ad15828563b39eef412bcee66e)]: + - effect@3.16.7 + - @effect/cluster@0.38.16 + - @effect/rpc@0.61.15 + - @effect/platform@0.84.11 + - @effect/platform-node-shared@0.39.16 + - @effect/sql@0.37.12 + +## 0.69.15 + +### Patch Changes + +- Updated dependencies [[`a5f7595`](https://github.com/Effect-TS/effect/commit/a5f75956ef9a15a83c416517ef493f0ee2f5ee8a), [`a02470c`](https://github.com/Effect-TS/effect/commit/a02470c75579e91525a25adb3f21b3650d042fdd), [`bf369b2`](https://github.com/Effect-TS/effect/commit/bf369b2902a0e0b195d957c18b9efd180942cf8b), [`f891d45`](https://github.com/Effect-TS/effect/commit/f891d45adffdafd3f94a2eca23faa354e3a409a8)]: + - effect@3.16.6 + - @effect/platform@0.84.10 + - @effect/cluster@0.38.15 + - @effect/platform-node-shared@0.39.15 + - @effect/rpc@0.61.14 + - @effect/sql@0.37.11 + +## 0.69.14 + +### Patch Changes + +- Updated dependencies [[`ee3a197`](https://github.com/Effect-TS/effect/commit/ee3a1973f54d7611ae99979edfed3020e94e1126), [`ee3a197`](https://github.com/Effect-TS/effect/commit/ee3a1973f54d7611ae99979edfed3020e94e1126)]: + - @effect/rpc@0.61.13 + - @effect/cluster@0.38.14 + - @effect/platform-node-shared@0.39.14 + +## 0.69.13 + +### Patch Changes + +- Updated dependencies [[`e0d3d42`](https://github.com/Effect-TS/effect/commit/e0d3d424d8f4e6a8ada017160406991f02b3c068)]: + - @effect/rpc@0.61.12 + - @effect/cluster@0.38.13 + - @effect/platform-node-shared@0.39.13 + +## 0.69.12 + +### Patch Changes + +- Updated dependencies [[`dca92fd`](https://github.com/Effect-TS/effect/commit/dca92fd8cf41f07561f55d863def5a9f62275f53)]: + - @effect/cluster@0.38.12 + - @effect/rpc@0.61.11 + - @effect/platform-node-shared@0.39.12 + +## 0.69.11 + +### Patch Changes + +- Updated dependencies [[`cc283b9`](https://github.com/Effect-TS/effect/commit/cc283b968235da3caf6c3e3a09b525fe09618fee)]: + - @effect/cluster@0.38.11 + - @effect/platform-node-shared@0.39.11 + +## 0.69.10 + +### Patch Changes + +- Updated dependencies [[`6e2e886`](https://github.com/Effect-TS/effect/commit/6e2e886f060c4ac057926b68d2e441c279480c30), [`bf418ef`](https://github.com/Effect-TS/effect/commit/bf418ef14a0f2ec965535793d5cea8fa8ba177ac)]: + - @effect/cluster@0.38.10 + - effect@3.16.5 + - @effect/platform-node-shared@0.39.10 + - @effect/platform@0.84.9 + - @effect/rpc@0.61.10 + - @effect/sql@0.37.10 + +## 0.69.9 + +### Patch Changes + +- Updated dependencies [[`7bf6cb9`](https://github.com/Effect-TS/effect/commit/7bf6cb943810e403f472a901ed29ccbbf76a46b2), [`7bf6cb9`](https://github.com/Effect-TS/effect/commit/7bf6cb943810e403f472a901ed29ccbbf76a46b2)]: + - @effect/rpc@0.61.9 + - @effect/cluster@0.38.9 + - @effect/platform-node-shared@0.39.9 + +## 0.69.8 + +### Patch Changes + +- Updated dependencies []: + - @effect/cluster@0.38.8 + - @effect/platform-node-shared@0.39.8 + +## 0.69.7 + +### Patch Changes + +- Updated dependencies [[`22166f8`](https://github.com/Effect-TS/effect/commit/22166f80c677cad6b4719e0e0253a9d06f964626)]: + - @effect/cluster@0.38.7 + - @effect/platform-node-shared@0.39.7 + +## 0.69.6 + +### Patch Changes + +- [#4998](https://github.com/Effect-TS/effect/pull/4998) [`f8ff7dc`](https://github.com/Effect-TS/effect/commit/f8ff7dccfe6ebd3409ab95c57f61764643d19a2b) Thanks @tim-smart! - expose MessageStorage in cluster clientOnly layers + +- Updated dependencies [[`f8ff7dc`](https://github.com/Effect-TS/effect/commit/f8ff7dccfe6ebd3409ab95c57f61764643d19a2b), [`8b9db77`](https://github.com/Effect-TS/effect/commit/8b9db7742846af0f58fd8e8b7acb7f4f5ff487ec)]: + - @effect/platform-node-shared@0.39.6 + - @effect/platform@0.84.8 + - @effect/cluster@0.38.6 + - @effect/rpc@0.61.8 + - @effect/sql@0.37.9 + +## 0.69.5 + +### Patch Changes + +- Updated dependencies [[`74ab9a0`](https://github.com/Effect-TS/effect/commit/74ab9a0a9e16d6e019369d256e1e24175c8bc3f3), [`770008e`](https://github.com/Effect-TS/effect/commit/770008eca3aad2899a2ed951236e575793294b28)]: + - effect@3.16.4 + - @effect/cluster@0.38.5 + - @effect/platform@0.84.7 + - @effect/platform-node-shared@0.39.5 + - @effect/rpc@0.61.7 + - @effect/sql@0.37.8 + +## 0.69.4 + +### Patch Changes + +- Updated dependencies [[`7e59d0e`](https://github.com/Effect-TS/effect/commit/7e59d0e2e004d86b8d0778e99c6fcd173fcb682a)]: + - @effect/cluster@0.38.4 + - @effect/platform-node-shared@0.39.4 + +## 0.69.3 + +### Patch Changes + +- Updated dependencies [[`59575c5`](https://github.com/Effect-TS/effect/commit/59575c5bf17a32c8b76c42e3794222b20e766581)]: + - @effect/cluster@0.38.3 + - @effect/platform-node-shared@0.39.3 + - @effect/sql@0.37.7 + +## 0.69.2 + +### Patch Changes + +- [#4975](https://github.com/Effect-TS/effect/pull/4975) [`ceea77a`](https://github.com/Effect-TS/effect/commit/ceea77a13055f145520f763e3fce5b8ff15d728f) Thanks @tim-smart! - allow wrapping a web Response with HttpServerResponse.raw on some platforms + +- Updated dependencies [[`d244b63`](https://github.com/Effect-TS/effect/commit/d244b6345ea1d2ac88812562b0c170683913d502), [`ceea77a`](https://github.com/Effect-TS/effect/commit/ceea77a13055f145520f763e3fce5b8ff15d728f)]: + - @effect/cluster@0.38.2 + - @effect/platform@0.84.6 + - @effect/platform-node-shared@0.39.2 + - @effect/rpc@0.61.6 + - @effect/sql@0.37.6 + +## 0.69.1 + +### Patch Changes + +- Updated dependencies [[`612c739`](https://github.com/Effect-TS/effect/commit/612c73979abc44825feae573c8902b6484923aaa)]: + - @effect/cluster@0.38.1 + - @effect/platform-node-shared@0.39.1 + +## 0.69.0 + +### Patch Changes + +- Updated dependencies [[`3086405`](https://github.com/Effect-TS/effect/commit/308640563041004d790f08d2ba75cc3a85fdf752), [`ec52c6a`](https://github.com/Effect-TS/effect/commit/ec52c6a2211e76972462b15b9d5a9d6d56761b7a), [`71e1e6c`](https://github.com/Effect-TS/effect/commit/71e1e6c535c11a3ec498540a3af3c1a313a5319b), [`d0067ca`](https://github.com/Effect-TS/effect/commit/d0067caef053b2855d93dcef59ea585d0fad9d8c), [`8c79abe`](https://github.com/Effect-TS/effect/commit/8c79abeb47d070d8880b652d31626497d3005a4e)]: + - @effect/cluster@0.38.0 + - @effect/platform@0.84.5 + - @effect/rpc@0.61.5 + - @effect/platform-node-shared@0.39.0 + - @effect/sql@0.37.5 + +## 0.68.2 + +### Patch Changes + +- Updated dependencies [[`6dfbae9`](https://github.com/Effect-TS/effect/commit/6dfbae946ea12ecee7234f5785335f3e7f8335b4)]: + - @effect/cluster@0.37.2 + - @effect/platform-node-shared@0.38.2 + +## 0.68.1 + +### Patch Changes + +- Updated dependencies []: + - @effect/cluster@0.37.1 + - @effect/platform-node-shared@0.38.1 + +## 0.68.0 + +### Patch Changes + +- Updated dependencies [[`87722fc`](https://github.com/Effect-TS/effect/commit/87722fce693a9b49284bbddbf82d30714c688261), [`36217ee`](https://github.com/Effect-TS/effect/commit/36217eeb1337edd9ac3f9a635b80a6385d22ae8f), [`a116aea`](https://github.com/Effect-TS/effect/commit/a116aeade97c83d8c96f17cdc5cf3b5a0bd9be74)]: + - effect@3.16.3 + - @effect/rpc@0.61.4 + - @effect/cluster@0.37.0 + - @effect/platform@0.84.4 + - @effect/platform-node-shared@0.38.0 + - @effect/sql@0.37.4 + +## 0.67.3 + +### Patch Changes + +- Updated dependencies [[`ab7684f`](https://github.com/Effect-TS/effect/commit/ab7684f1c2a0671bf091f255d220e3a4cc7f528e)]: + - @effect/platform@0.84.3 + - @effect/cluster@0.36.3 + - @effect/platform-node-shared@0.37.3 + - @effect/rpc@0.61.3 + - @effect/sql@0.37.3 + +## 0.67.2 + +### Patch Changes + +- Updated dependencies [[`0ddf148`](https://github.com/Effect-TS/effect/commit/0ddf148a247aa87af043d276b8453a714a400897), [`a77afb1`](https://github.com/Effect-TS/effect/commit/a77afb1f7191a57a68b09fcdee5e9f27a0682b0a)]: + - effect@3.16.2 + - @effect/rpc@0.61.2 + - @effect/cluster@0.36.2 + - @effect/platform@0.84.2 + - @effect/platform-node-shared@0.37.2 + - @effect/sql@0.37.2 + +## 0.67.1 + +### Patch Changes + +- Updated dependencies [[`71174d0`](https://github.com/Effect-TS/effect/commit/71174d09691314a9b6b66189e456fd21e3eb6543), [`d615e6e`](https://github.com/Effect-TS/effect/commit/d615e6e5b944f6fd5e627e31752c7ca7e4e1c17d)]: + - @effect/platform@0.84.1 + - effect@3.16.1 + - @effect/cluster@0.36.1 + - @effect/platform-node-shared@0.37.1 + - @effect/rpc@0.61.1 + - @effect/sql@0.37.1 + +## 0.67.0 + +### Patch Changes + +- Updated dependencies [[`ee0bd5d`](https://github.com/Effect-TS/effect/commit/ee0bd5d24864752c54cb359f67a67dd903971ec4), [`5189800`](https://github.com/Effect-TS/effect/commit/51898004e11766b8cf6d95e960b636f6d5db79ec), [`58bfeaa`](https://github.com/Effect-TS/effect/commit/58bfeaa64ded8c88f772b184311c0c0dbac10960), [`194d748`](https://github.com/Effect-TS/effect/commit/194d7486943f56f3267ef415395ac220a4b3e634), [`918c9ea`](https://github.com/Effect-TS/effect/commit/918c9ea1a57facb154f0fb26792021f337054dee), [`9198e6f`](https://github.com/Effect-TS/effect/commit/9198e6fcc1a3ff4fefb3363004de558d8de01f40), [`2a370bf`](https://github.com/Effect-TS/effect/commit/2a370bf625fdeede5659721468eb0d527e403279), [`58ccb91`](https://github.com/Effect-TS/effect/commit/58ccb91328c8df5d49808b673738bc09df355201), [`fd47834`](https://github.com/Effect-TS/effect/commit/fd478348203fa89462b0a1d067ce4de034353df4)]: + - effect@3.16.0 + - @effect/cluster@0.36.0 + - @effect/platform@0.84.0 + - @effect/platform-node-shared@0.37.0 + - @effect/rpc@0.61.0 + - @effect/sql@0.37.0 + +## 0.66.0 + +### Patch Changes + +- Updated dependencies [[`5522520`](https://github.com/Effect-TS/effect/commit/55225206ab9af0ad60b1c0654690a8a096d625cd), [`cc5bb2b`](https://github.com/Effect-TS/effect/commit/cc5bb2b918a9450a975f702dabcea891bda382cb)]: + - @effect/platform@0.83.0 + - effect@3.15.5 + - @effect/cluster@0.35.0 + - @effect/platform-node-shared@0.36.0 + - @effect/rpc@0.60.0 + - @effect/sql@0.36.0 + +## 0.65.5 + +### Patch Changes + +- Updated dependencies [[`0617b9d`](https://github.com/Effect-TS/effect/commit/0617b9dc365f1963b36949ad7f9023ab6eb94524), [`58c5fd3`](https://github.com/Effect-TS/effect/commit/58c5fd3dd30eceb6c8afea90406768b0e348f48f)]: + - @effect/platform@0.82.8 + - @effect/cluster@0.34.5 + - @effect/platform-node-shared@0.35.5 + - @effect/rpc@0.59.9 + - @effect/sql@0.35.8 + +## 0.65.4 + +### Patch Changes + +- Updated dependencies [[`f570554`](https://github.com/Effect-TS/effect/commit/f57055459524587b041340577dad85476bb35f81), [`78047e8`](https://github.com/Effect-TS/effect/commit/78047e8dfc8005b66f87afe50bb95981fea51561), [`c20b95a`](https://github.com/Effect-TS/effect/commit/c20b95a99ffe452b4774c844d397a905f713b6d6), [`94ada43`](https://github.com/Effect-TS/effect/commit/94ada430928d5685bdbef513e87562c20774a3a2)]: + - effect@3.15.4 + - @effect/platform-node-shared@0.35.4 + - @effect/platform@0.82.7 + - @effect/cluster@0.34.4 + - @effect/rpc@0.59.8 + - @effect/sql@0.35.7 + +## 0.65.3 + +### Patch Changes + +- Updated dependencies [[`618903b`](https://github.com/Effect-TS/effect/commit/618903ba9ae96e2bfe6ee31f61c4359b915f2a36)]: + - @effect/platform@0.82.6 + - @effect/cluster@0.34.3 + - @effect/platform-node-shared@0.35.3 + - @effect/rpc@0.59.7 + - @effect/sql@0.35.6 + +## 0.65.2 + +### Patch Changes + +- Updated dependencies [[`7764a07`](https://github.com/Effect-TS/effect/commit/7764a07d960c60df81f14e1dc949518f4bbe494a), [`4577f54`](https://github.com/Effect-TS/effect/commit/4577f548d67273e576cdde423bdd34a4b910766a), [`30a0d9c`](https://github.com/Effect-TS/effect/commit/30a0d9cb51c84290d51b1361d72ff5cee33c13c7)]: + - @effect/platform@0.82.5 + - effect@3.15.3 + - @effect/cluster@0.34.2 + - @effect/platform-node-shared@0.35.2 + - @effect/rpc@0.59.6 + - @effect/sql@0.35.5 + +## 0.65.1 + +### Patch Changes + +- Updated dependencies [[`1627a02`](https://github.com/Effect-TS/effect/commit/1627a0299a07c3538ca15293f1ac3ffa7eeb45f3), [`d45e8a8`](https://github.com/Effect-TS/effect/commit/d45e8a8ac8227192f504e39e6d04fdcf4fb1d225), [`89657ac`](https://github.com/Effect-TS/effect/commit/89657ac2fbda9ba38ac2962ce96949e536a464f9), [`d13b68e`](https://github.com/Effect-TS/effect/commit/d13b68e3a9456d0bfee9bca8273a7b44a9c69087)]: + - @effect/cluster@0.34.1 + - @effect/platform@0.82.4 + - @effect/sql@0.35.4 + - @effect/platform-node-shared@0.35.1 + - @effect/rpc@0.59.5 + +## 0.65.0 + +### Patch Changes + +- Updated dependencies [[`b8722b8`](https://github.com/Effect-TS/effect/commit/b8722b817e2306fe8c8245f3f9e32d85b824b961), [`a328f4b`](https://github.com/Effect-TS/effect/commit/a328f4b4fe717dd53e5b04a30f387433c32f7328), [`eaf8405`](https://github.com/Effect-TS/effect/commit/eaf8405ab9bb52423050eb0d23dd7d3c21c18141)]: + - effect@3.15.2 + - @effect/platform@0.82.3 + - @effect/cluster@0.34.0 + - @effect/platform-node-shared@0.35.0 + - @effect/rpc@0.59.4 + - @effect/sql@0.35.3 + +## 0.64.3 + +### Patch Changes + +- Updated dependencies [[`739a3d4`](https://github.com/Effect-TS/effect/commit/739a3d4a4565915fe2e690003f4f9085cb4422fc)]: + - @effect/platform@0.82.2 + - @effect/cluster@0.33.3 + - @effect/platform-node-shared@0.34.3 + - @effect/rpc@0.59.3 + - @effect/sql@0.35.2 + +## 0.64.2 + +### Patch Changes + +- Updated dependencies [[`787ce70`](https://github.com/Effect-TS/effect/commit/787ce7042e35b657963473c6efe47752868cd811), [`1269641`](https://github.com/Effect-TS/effect/commit/1269641a99ae43069f7648ff79ffe8729b54b348), [`1269641`](https://github.com/Effect-TS/effect/commit/1269641a99ae43069f7648ff79ffe8729b54b348)]: + - effect@3.15.1 + - @effect/cluster@0.33.2 + - @effect/platform@0.82.1 + - @effect/platform-node-shared@0.34.2 + - @effect/rpc@0.59.2 + - @effect/sql@0.35.1 + +## 0.64.1 + +### Patch Changes + +- Updated dependencies [[`6495440`](https://github.com/Effect-TS/effect/commit/64954405eb57313722023b87c0d92761980e2713)]: + - @effect/rpc@0.59.1 + - @effect/cluster@0.33.1 + - @effect/platform-node-shared@0.34.1 + +## 0.64.0 + +### Patch Changes + +- Updated dependencies [[`c654595`](https://github.com/Effect-TS/effect/commit/c65459587b51da140b78098e81fdbfece65d53e2), [`d9f5dea`](https://github.com/Effect-TS/effect/commit/d9f5deae0f02f5de2b9fcb1cca8b142ba4bc2bba), [`49aa723`](https://github.com/Effect-TS/effect/commit/49aa7236a15e13f818c86edbca08c4af67c8dfaf), [`74c14d0`](https://github.com/Effect-TS/effect/commit/74c14d01d0cb48cf517a1b6e29a373a96ed0ff5b), [`e4f49b6`](https://github.com/Effect-TS/effect/commit/e4f49b66857e01b74ab6a9a0bc7132f44cd04cbb), [`6f02224`](https://github.com/Effect-TS/effect/commit/6f02224b3fc46a682ad2defb1a260841956c6780), [`1dcfd41`](https://github.com/Effect-TS/effect/commit/1dcfd41ff96abd706901293a00c1893cb29dd8fd), [`b21ab16`](https://github.com/Effect-TS/effect/commit/b21ab16b6f773e7ec4369db4e752c35e719f7870), [`fcf1822`](https://github.com/Effect-TS/effect/commit/fcf1822f98fcda60351d64e9d2c2c13563d7e6db), [`0061dd1`](https://github.com/Effect-TS/effect/commit/0061dd140740165e91569a684cce27a77b23229e), [`8421e6e`](https://github.com/Effect-TS/effect/commit/8421e6e49332bca8f96f482dfd48680e238b3a89), [`a9b3fb7`](https://github.com/Effect-TS/effect/commit/a9b3fb78abcfdb525318a956fd02fcadeb56143e), [`fa10f56`](https://github.com/Effect-TS/effect/commit/fa10f56b96bd9af070ba99ebc3279aa93954261e)]: + - effect@3.15.0 + - @effect/platform@0.82.0 + - @effect/cluster@0.33.0 + - @effect/platform-node-shared@0.34.0 + - @effect/rpc@0.59.0 + - @effect/sql@0.35.0 + +## 0.63.0 + +### Patch Changes + +- Updated dependencies [[`cd6cd0e`](https://github.com/Effect-TS/effect/commit/cd6cd0eacd6b09d6dd48b30b32edeb4a3c3075f9)]: + - @effect/rpc@0.58.0 + - @effect/cluster@0.32.0 + - @effect/platform-node-shared@0.33.0 + +## 0.62.1 + +### Patch Changes + +- Updated dependencies [[`24a9ebb`](https://github.com/Effect-TS/effect/commit/24a9ebbb5af598f0bfd6ecc45307e528043fe011)]: + - effect@3.14.22 + - @effect/cluster@0.31.1 + - @effect/platform@0.81.1 + - @effect/platform-node-shared@0.32.1 + - @effect/rpc@0.57.1 + - @effect/sql@0.34.1 + +## 0.62.0 + +### Patch Changes + +- Updated dependencies [[`672920f`](https://github.com/Effect-TS/effect/commit/672920f85da8abd5f9d4ad85e29248a2aca57ed8)]: + - @effect/platform@0.81.0 + - @effect/cluster@0.31.0 + - @effect/platform-node-shared@0.32.0 + - @effect/rpc@0.57.0 + - @effect/sql@0.34.0 + +## 0.61.11 + +### Patch Changes + +- Updated dependencies [[`2f3b7d4`](https://github.com/Effect-TS/effect/commit/2f3b7d4e1fa1ef8790b0ca4da22eb88872ee31df)]: + - effect@3.14.21 + - @effect/cluster@0.30.11 + - @effect/platform@0.80.21 + - @effect/platform-node-shared@0.31.11 + - @effect/rpc@0.56.9 + - @effect/sql@0.33.21 + +## 0.61.10 + +### Patch Changes + +- Updated dependencies [[`17e2f30`](https://github.com/Effect-TS/effect/commit/17e2f3091408cf0fca9414d4af3bdf7b2765b378)]: + - effect@3.14.20 + - @effect/cluster@0.30.10 + - @effect/platform@0.80.20 + - @effect/platform-node-shared@0.31.10 + - @effect/rpc@0.56.8 + - @effect/sql@0.33.20 + +## 0.61.9 + +### Patch Changes + +- Updated dependencies [[`2d55bc5`](https://github.com/Effect-TS/effect/commit/2d55bc52c596afd8381f8ad1badc69efa0be8a78), [`114dad9`](https://github.com/Effect-TS/effect/commit/114dad9a93613986eb5d306cbcfda3fb37ec1a1b)]: + - @effect/cluster@0.30.9 + - @effect/platform-node-shared@0.31.9 + +## 0.61.8 + +### Patch Changes + +- Updated dependencies [[`1b30f61`](https://github.com/Effect-TS/effect/commit/1b30f616e75580933284657cb2cefab5a7903323)]: + - @effect/cluster@0.30.8 + - @effect/platform-node-shared@0.31.8 + +## 0.61.7 + +### Patch Changes + +- Updated dependencies [[`146af39`](https://github.com/Effect-TS/effect/commit/146af39d8d3b4e82aceb13de9749e6c4120c580b), [`056a910`](https://github.com/Effect-TS/effect/commit/056a910d0a0b8b00b0dc9df4a070466b2b5c2f6c), [`e25e7bb`](https://github.com/Effect-TS/effect/commit/e25e7bbc1797733916f48f501425d9f2ef310d9f), [`3273d57`](https://github.com/Effect-TS/effect/commit/3273d572c2b3175a842677f19efeea4cd65ab016)]: + - @effect/cluster@0.30.7 + - effect@3.14.19 + - @effect/platform@0.80.19 + - @effect/platform-node-shared@0.31.7 + - @effect/rpc@0.56.7 + - @effect/sql@0.33.19 + +## 0.61.6 + +### Patch Changes + +- Updated dependencies [[`b1164d4`](https://github.com/Effect-TS/effect/commit/b1164d49a1dfdf299e9971367b6fc6be4df0ddff)]: + - effect@3.14.18 + - @effect/cluster@0.30.6 + - @effect/platform@0.80.18 + - @effect/platform-node-shared@0.31.6 + - @effect/rpc@0.56.6 + - @effect/sql@0.33.18 + +## 0.61.5 + +### Patch Changes + +- Updated dependencies [[`0b54681`](https://github.com/Effect-TS/effect/commit/0b54681cd89245e211d8f49272be0f1bf2f81813), [`41a59d5`](https://github.com/Effect-TS/effect/commit/41a59d5916a296b12b0d5ead9e859e05f40b4cce)]: + - effect@3.14.17 + - @effect/cluster@0.30.5 + - @effect/platform@0.80.17 + - @effect/platform-node-shared@0.31.5 + - @effect/rpc@0.56.5 + - @effect/sql@0.33.17 + +## 0.61.4 + +### Patch Changes + +- Updated dependencies [[`ee14444`](https://github.com/Effect-TS/effect/commit/ee144441021ec77039e43396eaf90714687bb495), [`f1c8583`](https://github.com/Effect-TS/effect/commit/f1c8583f8c3ea9415f813795ca2940a897c9ba9a)]: + - effect@3.14.16 + - @effect/platform@0.80.16 + - @effect/cluster@0.30.4 + - @effect/platform-node-shared@0.31.4 + - @effect/rpc@0.56.4 + - @effect/sql@0.33.16 + +## 0.61.3 + +### Patch Changes + +- Updated dependencies [[`239cc99`](https://github.com/Effect-TS/effect/commit/239cc995ce645946210a3c3d2cb52bd3547c0687), [`8b6c947`](https://github.com/Effect-TS/effect/commit/8b6c947eaa8e45a67ecb3c37d45cd27f3e41d165), [`c50a63b`](https://github.com/Effect-TS/effect/commit/c50a63bbecb9f560b9cae349c447eed877d1b9b6)]: + - effect@3.14.15 + - @effect/cluster@0.30.3 + - @effect/platform@0.80.15 + - @effect/platform-node-shared@0.31.3 + - @effect/rpc@0.56.3 + - @effect/sql@0.33.15 + +## 0.61.2 + +### Patch Changes + +- Updated dependencies [[`664293f`](https://github.com/Effect-TS/effect/commit/664293f975a282920a7208e966adaf4634c42ef4), [`6ed8d15`](https://github.com/Effect-TS/effect/commit/6ed8d1589beb181d30abc79afebdaabc1d101538)]: + - @effect/cluster@0.30.2 + - effect@3.14.14 + - @effect/platform-node-shared@0.31.2 + - @effect/platform@0.80.14 + - @effect/rpc@0.56.2 + - @effect/sql@0.33.14 + +## 0.61.1 + +### Patch Changes + +- Updated dependencies [[`ee77788`](https://github.com/Effect-TS/effect/commit/ee77788747e7ebbde6bfa88256cde49dbbad3608), [`5fce6ba`](https://github.com/Effect-TS/effect/commit/5fce6ba19c3cc63cc0104e737e581ad989dedbf0), [`570e45f`](https://github.com/Effect-TS/effect/commit/570e45f8cb936e42ec48f67f21bb2b7252f36c0c)]: + - effect@3.14.13 + - @effect/cluster@0.30.1 + - @effect/platform@0.80.13 + - @effect/platform-node-shared@0.31.1 + - @effect/rpc@0.56.1 + - @effect/sql@0.33.13 + +## 0.61.0 + +### Patch Changes + +- Updated dependencies [[`d6e1156`](https://github.com/Effect-TS/effect/commit/d6e115617fc1a26a846b55f407965a330145dbee), [`2c66c16`](https://github.com/Effect-TS/effect/commit/2c66c16375dc2fe128f7b4e78c5f5c27c25c0d19)]: + - @effect/rpc@0.56.0 + - @effect/cluster@0.30.0 + - @effect/platform-node-shared@0.31.0 + +## 0.60.22 + +### Patch Changes + +- Updated dependencies [[`c2ad9ee`](https://github.com/Effect-TS/effect/commit/c2ad9ee9f3c4c743390edf35ed9e85a20be33811), [`9c68654`](https://github.com/Effect-TS/effect/commit/9c686542b6eb3ea188cb70673ef2e41223633e89)]: + - effect@3.14.12 + - @effect/cluster@0.29.22 + - @effect/platform@0.80.12 + - @effect/platform-node-shared@0.30.22 + - @effect/rpc@0.55.17 + - @effect/sql@0.33.12 + +## 0.60.21 + +### Patch Changes + +- Updated dependencies [[`e536127`](https://github.com/Effect-TS/effect/commit/e536127c1e6f2fb3a542c73ae919435a629a346b), [`b5ad11e`](https://github.com/Effect-TS/effect/commit/b5ad11e511424c6d5c32e34e7ee9d04f0110617d)]: + - effect@3.14.11 + - @effect/rpc@0.55.16 + - @effect/cluster@0.29.21 + - @effect/platform@0.80.11 + - @effect/platform-node-shared@0.30.21 + - @effect/sql@0.33.11 + +## 0.60.20 + +### Patch Changes + +- Updated dependencies [[`d3df84e`](https://github.com/Effect-TS/effect/commit/d3df84e8af8e00a297e2329faeae625de0a95a71)]: + - @effect/rpc@0.55.15 + - @effect/cluster@0.29.20 + - @effect/platform-node-shared@0.30.20 + +## 0.60.19 + +### Patch Changes + +- Updated dependencies [[`bc7efa3`](https://github.com/Effect-TS/effect/commit/bc7efa3b031bb25e1ed3c8f2d3fb5e8da166cadc)]: + - effect@3.14.10 + - @effect/cluster@0.29.19 + - @effect/platform@0.80.10 + - @effect/platform-node-shared@0.30.19 + - @effect/rpc@0.55.14 + - @effect/sql@0.33.10 + +## 0.60.18 + +### Patch Changes + +- Updated dependencies [[`d78249f`](https://github.com/Effect-TS/effect/commit/d78249f0b67f63cf4baf806ff090cba33293daf0)]: + - effect@3.14.9 + - @effect/cluster@0.29.18 + - @effect/platform@0.80.9 + - @effect/platform-node-shared@0.30.18 + - @effect/rpc@0.55.13 + - @effect/sql@0.33.9 + +## 0.60.17 + +### Patch Changes + +- Updated dependencies [[`58eaca9`](https://github.com/Effect-TS/effect/commit/58eaca9ef14032fc310f4a0e3c09513bac1cb50a)]: + - @effect/rpc@0.55.12 + - @effect/cluster@0.29.17 + - @effect/platform-node-shared@0.30.17 + +## 0.60.16 + +### Patch Changes + +- Updated dependencies [[`a79b732`](https://github.com/Effect-TS/effect/commit/a79b732bddea8bfca091c4fed0dd87aa0b1ab1f0), [`a79b732`](https://github.com/Effect-TS/effect/commit/a79b732bddea8bfca091c4fed0dd87aa0b1ab1f0)]: + - @effect/cluster@0.29.16 + - @effect/platform-node-shared@0.30.16 + +## 0.60.15 + +### Patch Changes + +- Updated dependencies [[`6966708`](https://github.com/Effect-TS/effect/commit/6966708a3061a3eb4bcfcb4d5877657fb41a019a)]: + - @effect/cluster@0.29.15 + - @effect/platform-node-shared@0.30.15 + +## 0.60.14 + +### Patch Changes + +- Updated dependencies [[`da21953`](https://github.com/Effect-TS/effect/commit/da21953a3831bf5974ab6add8fcc7fad1c0ba472)]: + - @effect/cluster@0.29.14 + - @effect/platform-node-shared@0.30.14 + +## 0.60.13 + +### Patch Changes + +- Updated dependencies [[`b3a2d32`](https://github.com/Effect-TS/effect/commit/b3a2d32772e6f7f20eacf2e18128e99324c4d378), [`896fbbf`](https://github.com/Effect-TS/effect/commit/896fbbf6ed6c11e099747e8aafb67b28edc4e466)]: + - effect@3.14.8 + - @effect/cluster@0.29.13 + - @effect/platform@0.80.8 + - @effect/platform-node-shared@0.30.13 + - @effect/rpc@0.55.11 + - @effect/sql@0.33.8 + +## 0.60.12 + +### Patch Changes + +- Updated dependencies [[`b542a4b`](https://github.com/Effect-TS/effect/commit/b542a4bf195be0c9af1523e1ba96c953decc4d25)]: + - effect@3.14.7 + - @effect/cluster@0.29.12 + - @effect/platform@0.80.7 + - @effect/platform-node-shared@0.30.12 + - @effect/rpc@0.55.10 + - @effect/sql@0.33.7 + +## 0.60.11 + +### Patch Changes + +- Updated dependencies [[`a1d4673`](https://github.com/Effect-TS/effect/commit/a1d4673a423dfed050c0a762664d9d64002cfa90)]: + - @effect/rpc@0.55.9 + - @effect/cluster@0.29.11 + - @effect/platform-node-shared@0.30.11 + +## 0.60.10 + +### Patch Changes + +- Updated dependencies [[`47618c1`](https://github.com/Effect-TS/effect/commit/47618c1ad84ebcc5a51133a3fff5aa5012d49d45), [`6077882`](https://github.com/Effect-TS/effect/commit/60778824a4794336c33807801f813f8751d1c7e4)]: + - effect@3.14.6 + - @effect/cluster@0.29.10 + - @effect/platform@0.80.6 + - @effect/platform-node-shared@0.30.10 + - @effect/rpc@0.55.8 + - @effect/sql@0.33.6 + +## 0.60.9 + +### Patch Changes + +- Updated dependencies [[`4414042`](https://github.com/Effect-TS/effect/commit/44140423a2fb185f92f7db4d5b383f9b62a97bf9)]: + - @effect/rpc@0.55.7 + - @effect/cluster@0.29.9 + - @effect/platform-node-shared@0.30.9 + +## 0.60.8 + +### Patch Changes + +- Updated dependencies [[`40dbfef`](https://github.com/Effect-TS/effect/commit/40dbfeff239b6e567706752114f31b2fce7de4e3), [`85fba81`](https://github.com/Effect-TS/effect/commit/85fba815ac07eb13d4227a69ac76a18e4b94df18), [`5a5ebdd`](https://github.com/Effect-TS/effect/commit/5a5ebdddfaddd259538b4599a6676281faca778e)]: + - effect@3.14.5 + - @effect/platform@0.80.5 + - @effect/cluster@0.29.8 + - @effect/platform-node-shared@0.30.8 + - @effect/rpc@0.55.6 + - @effect/sql@0.33.5 + +## 0.60.7 + +### Patch Changes + +- Updated dependencies [[`e4ba2c6`](https://github.com/Effect-TS/effect/commit/e4ba2c66a878e81b5e295d6d49aaf724b80a28ef), [`e3e5873`](https://github.com/Effect-TS/effect/commit/e3e5873f30080bb0e5eed8a876170acaa6ed47ff), [`26c060c`](https://github.com/Effect-TS/effect/commit/26c060c65914a623220a20356991784f974bfe18)]: + - effect@3.14.4 + - @effect/rpc@0.55.5 + - @effect/cluster@0.29.7 + - @effect/platform@0.80.4 + - @effect/platform-node-shared@0.30.7 + - @effect/sql@0.33.4 + +## 0.60.6 + +### Patch Changes + +- Updated dependencies [[`0ec5e03`](https://github.com/Effect-TS/effect/commit/0ec5e0353a1db5d27c3500deba0df61001258e76), [`05c4d77`](https://github.com/Effect-TS/effect/commit/05c4d772acc42b7425add7b22f914c5ee3ff84bd), [`37aa8e1`](https://github.com/Effect-TS/effect/commit/37aa8e137725a902e70cd1e468ea98b873aa5056), [`34f03d6`](https://github.com/Effect-TS/effect/commit/34f03d66875f21f266f102223a03cd14c2ed6ea6)]: + - @effect/rpc@0.55.4 + - effect@3.14.3 + - @effect/cluster@0.29.6 + - @effect/platform-node-shared@0.30.6 + - @effect/platform@0.80.3 + - @effect/sql@0.33.3 + +## 0.60.5 + +### Patch Changes + +- Updated dependencies [[`f87991b`](https://github.com/Effect-TS/effect/commit/f87991b6d8a2edfaf90b01cebda4b466992ae865), [`f87991b`](https://github.com/Effect-TS/effect/commit/f87991b6d8a2edfaf90b01cebda4b466992ae865), [`0a3e3e1`](https://github.com/Effect-TS/effect/commit/0a3e3e18eea5e0d1882f1a6c906198e6ef226a41)]: + - effect@3.14.2 + - @effect/cluster@0.29.5 + - @effect/platform@0.80.2 + - @effect/platform-node-shared@0.30.5 + - @effect/rpc@0.55.3 + - @effect/sql@0.33.2 + +## 0.60.4 + +### Patch Changes + +- Updated dependencies [[`d2f11e5`](https://github.com/Effect-TS/effect/commit/d2f11e557de4639762124951252170fbf4d7c906)]: + - @effect/rpc@0.55.2 + - @effect/cluster@0.29.4 + - @effect/platform-node-shared@0.30.4 + +## 0.60.3 + +### Patch Changes + +- Updated dependencies [[`18a7936`](https://github.com/Effect-TS/effect/commit/18a7936832158daa69e3c09a6caae55e3d6c0b86)]: + - @effect/cluster@0.29.3 + - @effect/platform-node-shared@0.30.3 + +## 0.60.2 + +### Patch Changes + +- Updated dependencies [[`3a99a2d`](https://github.com/Effect-TS/effect/commit/3a99a2dbaa38348c1f6e210a531fcfb99b5e73c5)]: + - @effect/cluster@0.29.2 + - @effect/platform-node-shared@0.30.2 + +## 0.60.1 + +### Patch Changes + +- Updated dependencies [[`4a274fe`](https://github.com/Effect-TS/effect/commit/4a274fe9f623182b6b902827e0e83bd89ca3b05c), [`814733f`](https://github.com/Effect-TS/effect/commit/814733fe62bb3dc91c6cd632d16a8d2076b3755b)]: + - effect@3.14.1 + - @effect/cluster@0.29.1 + - @effect/platform@0.80.1 + - @effect/platform-node-shared@0.30.1 + - @effect/rpc@0.55.1 + - @effect/sql@0.33.1 + +## 0.60.0 + +### Minor Changes + +- [#4469](https://github.com/Effect-TS/effect/pull/4469) [`3131f8f`](https://github.com/Effect-TS/effect/commit/3131f8fd12ba9eb31b90fa2f42bf88b12309133c) Thanks @tim-smart! - refactor of @effect/cluster packages + +### Patch Changes + +- [#4469](https://github.com/Effect-TS/effect/pull/4469) [`aba2d1d`](https://github.com/Effect-TS/effect/commit/aba2d1d831ea149481bd4dd755528c0afa8239ce) Thanks @tim-smart! - Move SocketServer modules to @effect/platform + +- Updated dependencies [[`1f47e4e`](https://github.com/Effect-TS/effect/commit/1f47e4e12546ab691b29bfb7b5128bb17b93baa5), [`26dd75f`](https://github.com/Effect-TS/effect/commit/26dd75f276a0d8a63eab313bd5a167d5072c9780), [`aba2d1d`](https://github.com/Effect-TS/effect/commit/aba2d1d831ea149481bd4dd755528c0afa8239ce), [`3131f8f`](https://github.com/Effect-TS/effect/commit/3131f8fd12ba9eb31b90fa2f42bf88b12309133c), [`aba2d1d`](https://github.com/Effect-TS/effect/commit/aba2d1d831ea149481bd4dd755528c0afa8239ce), [`aba2d1d`](https://github.com/Effect-TS/effect/commit/aba2d1d831ea149481bd4dd755528c0afa8239ce), [`04dff2d`](https://github.com/Effect-TS/effect/commit/04dff2d01ac68c260f29a6d4743381825c353c86), [`c7fac0c`](https://github.com/Effect-TS/effect/commit/c7fac0cd7eadcd5cc0c3a987051c5b57ad271638), [`aba2d1d`](https://github.com/Effect-TS/effect/commit/aba2d1d831ea149481bd4dd755528c0afa8239ce), [`ffaa3f3`](https://github.com/Effect-TS/effect/commit/ffaa3f3969df26610fcc02ad537340641d44e803), [`ab957c1`](https://github.com/Effect-TS/effect/commit/ab957c1fee714868f56c7ab4e802b9d449e9b666), [`35db9ce`](https://github.com/Effect-TS/effect/commit/35db9ce228f1416c8abacc6dc9c36fbd0f33ef0f), [`aba2d1d`](https://github.com/Effect-TS/effect/commit/aba2d1d831ea149481bd4dd755528c0afa8239ce), [`cf77ea9`](https://github.com/Effect-TS/effect/commit/cf77ea9ab4fc89e66a43f682a9926ccdee6c57ed), [`26dd75f`](https://github.com/Effect-TS/effect/commit/26dd75f276a0d8a63eab313bd5a167d5072c9780), [`baaab60`](https://github.com/Effect-TS/effect/commit/baaab60b737f35dfab8e4a21bce28a195d19e899)]: + - effect@3.14.0 + - @effect/platform@0.80.0 + - @effect/platform-node-shared@0.30.0 + - @effect/cluster@0.29.0 + - @effect/rpc@0.55.0 + - @effect/sql@0.33.0 + +## 0.59.4 + +### Patch Changes + +- Updated dependencies [[`5662363`](https://github.com/Effect-TS/effect/commit/566236361e270e575ef1cbf308ad1967c82a362c), [`5f1fd15`](https://github.com/Effect-TS/effect/commit/5f1fd15308ab154791580059b89877d19a2055c2), [`8bb1460`](https://github.com/Effect-TS/effect/commit/8bb1460c824f66f0f25ebd899c5e74e388089c37)]: + - @effect/platform@0.79.4 + - @effect/platform-node-shared@0.29.4 + +## 0.59.3 + +### Patch Changes + +- Updated dependencies [[`0c4803f`](https://github.com/Effect-TS/effect/commit/0c4803fcc69262d11a97ce49d0e9b4288df0651f), [`6f65ac4`](https://github.com/Effect-TS/effect/commit/6f65ac4eac1489cd6ea390e18b0908670722adad)]: + - effect@3.13.12 + - @effect/platform@0.79.3 + - @effect/platform-node-shared@0.29.3 + +## 0.59.2 + +### Patch Changes + +- Updated dependencies [[`fad8cca`](https://github.com/Effect-TS/effect/commit/fad8cca9bbfcc2eaeb44b97c15dbe0a1eda75315), [`4296293`](https://github.com/Effect-TS/effect/commit/4296293049414d0cf2d915a26c552b09f946b9a0), [`9c241ab`](https://github.com/Effect-TS/effect/commit/9c241abe47ccf7a5257b98a4a64a63054a12741d), [`082b0c1`](https://github.com/Effect-TS/effect/commit/082b0c1b9f4252bcdd69608f2e4a9226f953ac3f), [`be12983`](https://github.com/Effect-TS/effect/commit/be12983bc7e7537b41cd8910fc4eb7d1da56ab07), [`de88127`](https://github.com/Effect-TS/effect/commit/de88127a5a5906ccece98af74787b5ae0e65e431)]: + - effect@3.13.11 + - @effect/platform@0.79.2 + - @effect/platform-node-shared@0.29.2 + +## 0.59.1 + +### Patch Changes + +- Updated dependencies [[`527c964`](https://github.com/Effect-TS/effect/commit/527c9645229f5be9714a7e60a38a9e753c4bbfb1)]: + - effect@3.13.10 + - @effect/platform@0.79.1 + - @effect/platform-node-shared@0.29.1 + +## 0.59.0 + +### Patch Changes + +- [#4580](https://github.com/Effect-TS/effect/pull/4580) [`bbdc279`](https://github.com/Effect-TS/effect/commit/bbdc2795a461cb2d1fe19b2669526a6ef590c3d4) Thanks @tim-smart! - prevent worker handler interrupts from shutting down runner + +- [#4583](https://github.com/Effect-TS/effect/pull/4583) [`d630249`](https://github.com/Effect-TS/effect/commit/d630249426113088abe8b382db4f14d80f2160c2) Thanks @tim-smart! - support Layer.launch when using WorkerRunner + +- Updated dependencies [[`88fe129`](https://github.com/Effect-TS/effect/commit/88fe12923740765c0335a6e6203fdcc6a463edca), [`d630249`](https://github.com/Effect-TS/effect/commit/d630249426113088abe8b382db4f14d80f2160c2), [`2976e52`](https://github.com/Effect-TS/effect/commit/2976e52538d9dc9ffdcbc84d4ac748cff9305971)]: + - @effect/platform@0.79.0 + - effect@3.13.9 + - @effect/platform-node-shared@0.29.0 + +## 0.58.1 + +### Patch Changes + +- Updated dependencies [[`c65d336`](https://github.com/Effect-TS/effect/commit/c65d3362d07ec815ff3b46278314e8a31706ddc2), [`22d2ebb`](https://github.com/Effect-TS/effect/commit/22d2ebb4b11f5a44351a4736e65da391a3b647d0)]: + - effect@3.13.8 + - @effect/platform@0.78.1 + - @effect/platform-node-shared@0.28.1 + +## 0.58.0 + +### Patch Changes + +- Updated dependencies [[`c5bcf53`](https://github.com/Effect-TS/effect/commit/c5bcf53b7cb49dacffdd2a6cd8eb48cc452b417e)]: + - @effect/platform-node-shared@0.28.0 + - @effect/platform@0.78.0 + +## 0.57.7 + +### Patch Changes + +- [#4539](https://github.com/Effect-TS/effect/pull/4539) [`99fcbf7`](https://github.com/Effect-TS/effect/commit/99fcbf712d40a90ac5c8843237d26914146d7312) Thanks @schickling! - Fixed interruption of timeout in worker shutdown + +- Updated dependencies [[`05306d5`](https://github.com/Effect-TS/effect/commit/05306d5cc55b94a23c175de798fc6a5e93a3ab74), [`840cc73`](https://github.com/Effect-TS/effect/commit/840cc7329908db7ca693ef47b07d4f845c29cadd), [`9bf8a74`](https://github.com/Effect-TS/effect/commit/9bf8a74b967f18d931743dd5196af326c9118e9c), [`87ba23c`](https://github.com/Effect-TS/effect/commit/87ba23c41c193503ed0c612b0d32d0b253794c64), [`f910880`](https://github.com/Effect-TS/effect/commit/f91088069057f3b4529753f5bc5532b028d726df), [`0d01480`](https://github.com/Effect-TS/effect/commit/0d014803e4f688f74386a80abd65485e1a319244)]: + - @effect/platform-node-shared@0.27.7 + - @effect/platform@0.77.7 + - effect@3.13.7 + +## 0.57.6 + +### Patch Changes + +- Updated dependencies [[`3154ce4`](https://github.com/Effect-TS/effect/commit/3154ce4692fa18b804982158d3c4c8a8a5fae386)]: + - effect@3.13.6 + - @effect/platform@0.77.6 + - @effect/platform-node-shared@0.27.6 + +## 0.57.5 + +### Patch Changes + +- Updated dependencies [[`367bb35`](https://github.com/Effect-TS/effect/commit/367bb35f4c2a254e1fb211d96db2474a7aed9020), [`6cf11c3`](https://github.com/Effect-TS/effect/commit/6cf11c3a75773ceec2877c85ddc760f381f0866d), [`a0acec8`](https://github.com/Effect-TS/effect/commit/a0acec851f72e19466363d24b9cc218acd00006a)]: + - effect@3.13.5 + - @effect/platform@0.77.5 + - @effect/platform-node-shared@0.27.5 + +## 0.57.4 + +### Patch Changes + +- Updated dependencies [[`e0746f9`](https://github.com/Effect-TS/effect/commit/e0746f9aa398b69c6542e375910683bf17f49f46), [`17d9e89`](https://github.com/Effect-TS/effect/commit/17d9e89f9851663bdbb6c1e685601d97806114a4)]: + - @effect/platform@0.77.4 + - effect@3.13.4 + - @effect/platform-node-shared@0.27.4 + +## 0.57.3 + +### Patch Changes + +- Updated dependencies [[`cc5588d`](https://github.com/Effect-TS/effect/commit/cc5588df07f9103513547cb429ce041b9436a8bd), [`623c8cd`](https://github.com/Effect-TS/effect/commit/623c8cd053ed6ee3d353aaa8778d484670fca2bb), [`00b4eb1`](https://github.com/Effect-TS/effect/commit/00b4eb1ece12a16e222e6220965bb4024d6752ac), [`f2aee98`](https://github.com/Effect-TS/effect/commit/f2aee989b0a600900ce83e7f460d02908620c80f), [`fb798eb`](https://github.com/Effect-TS/effect/commit/fb798eb9061f1191badc017d1aa649360254da20), [`2251b15`](https://github.com/Effect-TS/effect/commit/2251b1528810bb695b37ce388b653cec0c5bf80c), [`2e15c1e`](https://github.com/Effect-TS/effect/commit/2e15c1e33648add0b29fe274fbcb7294b7515085), [`a4979db`](https://github.com/Effect-TS/effect/commit/a4979db021aef16e731be64df196b72088fc4376), [`b74255a`](https://github.com/Effect-TS/effect/commit/b74255a304ad49d60bedb1a260fd697f370af27a), [`d7f6a5c`](https://github.com/Effect-TS/effect/commit/d7f6a5c7d26c1963dcd864ca62360d20d08c7b49), [`9dd8979`](https://github.com/Effect-TS/effect/commit/9dd8979e940915b1cc1b1f264f3d019c77a65a02), [`477b488`](https://github.com/Effect-TS/effect/commit/477b488284f47c5469d7fba3e4065fb7e3b6556e), [`10932cb`](https://github.com/Effect-TS/effect/commit/10932cbf58fc721ada631cebec42f773ce96d3cc), [`9f6c784`](https://github.com/Effect-TS/effect/commit/9f6c78468b3b5e9ebfc38ffdfb70702901ee977b), [`2c639ec`](https://github.com/Effect-TS/effect/commit/2c639ecee332de4266e36022c989c35ae4e02105), [`886aaa8`](https://github.com/Effect-TS/effect/commit/886aaa81e06dfd3cd9391e8ea987d8cd5ada1124)]: + - effect@3.13.3 + - @effect/platform@0.77.3 + - @effect/platform-node-shared@0.27.3 + +## 0.57.2 + +### Patch Changes + +- Updated dependencies [[`31be72a`](https://github.com/Effect-TS/effect/commit/31be72ada118cb84a942e67b1663263f8db74a9f), [`3e7ce97`](https://github.com/Effect-TS/effect/commit/3e7ce97f8a41756a039cf635d0b3d9a75d781097), [`31be72a`](https://github.com/Effect-TS/effect/commit/31be72ada118cb84a942e67b1663263f8db74a9f)]: + - effect@3.13.2 + - @effect/platform@0.77.2 + - @effect/platform-node-shared@0.27.2 + +## 0.57.1 + +### Patch Changes + +- Updated dependencies [[`b56a211`](https://github.com/Effect-TS/effect/commit/b56a2110569fd0ec0b57ac137743e926d49f51cc)]: + - effect@3.13.1 + - @effect/platform@0.77.1 + - @effect/platform-node-shared@0.27.1 + +## 0.57.0 + +### Patch Changes + +- Updated dependencies [[`8baef83`](https://github.com/Effect-TS/effect/commit/8baef83e7ff0b7bc0738b680e1ef013065386cff), [`655bfe2`](https://github.com/Effect-TS/effect/commit/655bfe29e44cc3f0fb9b4e53038f50b891c188df), [`d90cbc2`](https://github.com/Effect-TS/effect/commit/d90cbc274e2742d18671fe65aa4764c057eb6cba), [`75632bd`](https://github.com/Effect-TS/effect/commit/75632bd44b8025101d652ccbaeef898c7086c91c), [`c874a2e`](https://github.com/Effect-TS/effect/commit/c874a2e4b17e9d71904ca8375bb77b020975cb1d), [`bf865e5`](https://github.com/Effect-TS/effect/commit/bf865e5833f77fd8f6c06944ca9d507b54488301), [`f98b2b7`](https://github.com/Effect-TS/effect/commit/f98b2b7592cf20f9d85313e7f1e964cb65878138), [`de8ce92`](https://github.com/Effect-TS/effect/commit/de8ce924923eaa4e1b761a97eb45ec967389f3d5), [`cf8b2dd`](https://github.com/Effect-TS/effect/commit/cf8b2dd112f8e092ed99d78fd728db0f91c29050), [`db426a5`](https://github.com/Effect-TS/effect/commit/db426a5fb41ab84d18e3c8753a7329b4de544245), [`6862444`](https://github.com/Effect-TS/effect/commit/6862444094906ad4f2cb077ff3b9cc0b73880c8c), [`5fc8a90`](https://github.com/Effect-TS/effect/commit/5fc8a90ba46a5fd9f3b643f0b5aeadc69d717339), [`546a492`](https://github.com/Effect-TS/effect/commit/546a492e60eb2b8b048a489a474b934ea0877005), [`65c4796`](https://github.com/Effect-TS/effect/commit/65c47966ce39055f02cf5c808daabb3ea6442b0b), [`9760fdc`](https://github.com/Effect-TS/effect/commit/9760fdc37bdaef9da8b150e46b86ddfbe2ad9221), [`5b471e7`](https://github.com/Effect-TS/effect/commit/5b471e7d4317e8ee5d72bbbd3e0c9775160949ab), [`4f810cc`](https://github.com/Effect-TS/effect/commit/4f810cc2770e9f1f266851d2cb6257112c12af49)]: + - effect@3.13.0 + - @effect/platform@0.77.0 + - @effect/platform-node-shared@0.27.0 + +## 0.56.1 + +### Patch Changes + +- Updated dependencies [[`4018eae`](https://github.com/Effect-TS/effect/commit/4018eaed2733241676ddb8c52416f463a8c32e35), [`543d36d`](https://github.com/Effect-TS/effect/commit/543d36d1a11452560b01ab966a82529ad5fee8c9), [`c407726`](https://github.com/Effect-TS/effect/commit/c407726f79df4a567a9631cddd8effaa16b3535d), [`f70a65a`](https://github.com/Effect-TS/effect/commit/f70a65ac80c6635d80b12beaf4d32a9cc59fa143), [`ba409f6`](https://github.com/Effect-TS/effect/commit/ba409f69c41aeaa29e475c0630735726eaf4dbac), [`3d2e356`](https://github.com/Effect-TS/effect/commit/3d2e3565e8a43d1bdb5daee8db3b90f56d71d859)]: + - effect@3.12.12 + - @effect/platform@0.76.1 + - @effect/platform-node-shared@0.26.1 + +## 0.56.0 + +### Minor Changes + +- [#4429](https://github.com/Effect-TS/effect/pull/4429) [`2473ad5`](https://github.com/Effect-TS/effect/commit/2473ad5cf23582e3a41338091fa526ffe611288d) Thanks @tim-smart! - run platform workers in a Scope, send errors or termination to a CloseLatch + +### Patch Changes + +- Updated dependencies [[`b6a032f`](https://github.com/Effect-TS/effect/commit/b6a032f07bffa020a848c813881879395134fa20), [`42ddd5f`](https://github.com/Effect-TS/effect/commit/42ddd5f144ce9f9d94a036679ebbd626446d37f5), [`2fe447c`](https://github.com/Effect-TS/effect/commit/2fe447c6354d334f9c591b8a8481818f5f0e797e), [`2473ad5`](https://github.com/Effect-TS/effect/commit/2473ad5cf23582e3a41338091fa526ffe611288d)]: + - effect@3.12.11 + - @effect/platform@0.76.0 + - @effect/platform-node-shared@0.26.0 + +## 0.55.4 + +### Patch Changes + +- Updated dependencies [[`e30f132`](https://github.com/Effect-TS/effect/commit/e30f132c336c9d0760bad39f82a55c7ce5159eb7), [`33fa667`](https://github.com/Effect-TS/effect/commit/33fa667c2623be1026e1ccee91bd44f73b09020a), [`87f5f28`](https://github.com/Effect-TS/effect/commit/87f5f2842e4196cb88d13f10f443ff0567e82832), [`7d57ecd`](https://github.com/Effect-TS/effect/commit/7d57ecdaf5da2345ebbf9c22df50317578bde0f5), [`4dbd170`](https://github.com/Effect-TS/effect/commit/4dbd170538e8fb7a36aa7c469c6f93b6c7000091)]: + - effect@3.12.10 + - @effect/platform@0.75.4 + - @effect/platform-node-shared@0.25.4 + +## 0.55.3 + +### Patch Changes + +- Updated dependencies [[`1b4a4e9`](https://github.com/Effect-TS/effect/commit/1b4a4e904ef5227ec7d9114d4e417eca19eed940)]: + - effect@3.12.9 + - @effect/platform@0.75.3 + - @effect/platform-node-shared@0.25.3 + +## 0.55.2 + +### Patch Changes + +- [#4353](https://github.com/Effect-TS/effect/pull/4353) [`8f6006a`](https://github.com/Effect-TS/effect/commit/8f6006a610fb6d6c7b8d14209a7323338a8964ff) Thanks @tim-smart! - fix HttpServerRequest.arrayBuffer for bun & web handlers + +- Updated dependencies [[`59b3cfb`](https://github.com/Effect-TS/effect/commit/59b3cfbbd5713dd9475998e95fad5534c0b21466), [`766113c`](https://github.com/Effect-TS/effect/commit/766113c0ea3512cdb887650ead8ba314236e22ee), [`bb05fb8`](https://github.com/Effect-TS/effect/commit/bb05fb83457355b1ca567228a9e041edfb6fd85d), [`712277f`](https://github.com/Effect-TS/effect/commit/712277f949052a24b46e4aa234063a6abf395c90), [`f269122`](https://github.com/Effect-TS/effect/commit/f269122508693b111142994dd48698ddc75f3d69), [`8f6006a`](https://github.com/Effect-TS/effect/commit/8f6006a610fb6d6c7b8d14209a7323338a8964ff), [`c45b559`](https://github.com/Effect-TS/effect/commit/c45b5592b5fd1189a5c932cfe05bd7d5f6d68508), [`430c846`](https://github.com/Effect-TS/effect/commit/430c846cbac05b187e3d24ac8dfee0cf22506f7c), [`7b03057`](https://github.com/Effect-TS/effect/commit/7b03057507d2dab5e6793beb9c578dedaaeb15fe), [`a9c94c8`](https://github.com/Effect-TS/effect/commit/a9c94c807755610831211a686d2fad849ab38eb4), [`107e6f0`](https://github.com/Effect-TS/effect/commit/107e6f0557a1e2d3b0dce25d62fa1e2601521752), [`c9175ae`](https://github.com/Effect-TS/effect/commit/c9175aef41cb1e3b689d0ac0a4f53d8107376b58), [`65c11b9`](https://github.com/Effect-TS/effect/commit/65c11b9266ec9447c31c26fe3ed35c73bd3b81fd), [`e386d2f`](https://github.com/Effect-TS/effect/commit/e386d2f1b3ab3ac2c14ee76de11f5963d32a3df4), [`9172efb`](https://github.com/Effect-TS/effect/commit/9172efba98bc6a82353e6ec2af61ac08f038ba64)]: + - @effect/platform@0.75.2 + - effect@3.12.8 + - @effect/platform-node-shared@0.25.2 + +## 0.55.1 + +### Patch Changes + +- Updated dependencies [[`8dff1d1`](https://github.com/Effect-TS/effect/commit/8dff1d1bff76cdba643cad7f0bf864300f08bc61)]: + - effect@3.12.7 + - @effect/platform@0.75.1 + - @effect/platform-node-shared@0.25.1 + +## 0.55.0 + +### Patch Changes + +- Updated dependencies [[`5e43ce5`](https://github.com/Effect-TS/effect/commit/5e43ce50bae116865906112e7f88d390739d778b), [`289c13b`](https://github.com/Effect-TS/effect/commit/289c13b38e8e35b214d46d385d05dead176c87cd), [`76eb7d0`](https://github.com/Effect-TS/effect/commit/76eb7d0fbce3c009c8f77e84c178cb15bbed9709), [`8b4e75d`](https://github.com/Effect-TS/effect/commit/8b4e75d35daea807c447ca760948a717aa66bb52), [`fc5e0f0`](https://github.com/Effect-TS/effect/commit/fc5e0f0d357a0051cfa01c1ede83ffdd3cb41ab1), [`004fd2b`](https://github.com/Effect-TS/effect/commit/004fd2bbd1459e64fb1b57f02eeb791ca5ea1ea5), [`b2a31be`](https://github.com/Effect-TS/effect/commit/b2a31be85c35d891351ce4f9a2cc93ece0c257f6), [`5514d05`](https://github.com/Effect-TS/effect/commit/5514d05b5cd586ff5868b8bd41c959e95e6c33cd), [`bf5f0ae`](https://github.com/Effect-TS/effect/commit/bf5f0ae9daa0170471678e22585e8ec14ce667bb), [`3b19bcf`](https://github.com/Effect-TS/effect/commit/3b19bcfd3aaadb6c9253428622df524537c8e626), [`b064b3b`](https://github.com/Effect-TS/effect/commit/b064b3b293615fd268cc5a5647d0981eb67750b8), [`eb264ed`](https://github.com/Effect-TS/effect/commit/eb264ed8a6e8c92a9dc7006f766c6ca2e5d29e03), [`289c13b`](https://github.com/Effect-TS/effect/commit/289c13b38e8e35b214d46d385d05dead176c87cd), [`f474678`](https://github.com/Effect-TS/effect/commit/f474678bf10b8f1c80e3dc096ddc7ecf20b2b23e), [`ee187d0`](https://github.com/Effect-TS/effect/commit/ee187d098007a402844c94d04f0cd8f07695377a)]: + - @effect/platform@0.75.0 + - effect@3.12.6 + - @effect/platform-node-shared@0.25.0 + +## 0.54.0 + +### Patch Changes + +- Updated dependencies [[`a8b0ddb`](https://github.com/Effect-TS/effect/commit/a8b0ddb84710054799fc8f57485b95d00093ada1), [`8653072`](https://github.com/Effect-TS/effect/commit/86530720d7a03e118d2c5a8bf5a997cee7e7f3d6), [`507d546`](https://github.com/Effect-TS/effect/commit/507d546bd49db31000425fb5da88c434e4291bea), [`a8b0ddb`](https://github.com/Effect-TS/effect/commit/a8b0ddb84710054799fc8f57485b95d00093ada1), [`8db239b`](https://github.com/Effect-TS/effect/commit/8db239b9c869a3707f6566b9d9dbdf53c4df03fc), [`84a0911`](https://github.com/Effect-TS/effect/commit/84a091181634c3a022c94234cec7764a3aeef1be), [`84a0911`](https://github.com/Effect-TS/effect/commit/84a091181634c3a022c94234cec7764a3aeef1be), [`3179a9f`](https://github.com/Effect-TS/effect/commit/3179a9f65d23369a6a9a1f80f7750566dd28df22), [`6cb9b76`](https://github.com/Effect-TS/effect/commit/6cb9b766396d0b2ed995cf26957359713efd202e), [`1fcbe55`](https://github.com/Effect-TS/effect/commit/1fcbe55345042d8468f6a98c84081bd00b6bcf5a), [`d9a63d9`](https://github.com/Effect-TS/effect/commit/d9a63d9d385653865954cac895065360d54cc56b), [`6cb9b76`](https://github.com/Effect-TS/effect/commit/6cb9b766396d0b2ed995cf26957359713efd202e)]: + - effect@3.12.5 + - @effect/platform@0.74.0 + - @effect/platform-node-shared@0.24.0 + +## 0.53.1 + +### Patch Changes + +- Updated dependencies [[`5b50ea4`](https://github.com/Effect-TS/effect/commit/5b50ea4a10cf9acd51f9624b2474d9d5ded74019), [`c170a68`](https://github.com/Effect-TS/effect/commit/c170a68b6266100774461fcd6c0e0fabb60112f2), [`a66c2eb`](https://github.com/Effect-TS/effect/commit/a66c2eb473245092cd41f04c2eb2b7b02cf53718), [`c9e5e1b`](https://github.com/Effect-TS/effect/commit/c9e5e1be17c0c84d3d4e2abc3c60215cdb56bbbe), [`7b3d58d`](https://github.com/Effect-TS/effect/commit/7b3d58d7aec2152ec282460871d3e9de45ed254d)]: + - effect@3.12.4 + - @effect/platform@0.73.1 + - @effect/platform-node-shared@0.23.1 + +## 0.53.0 + +### Patch Changes + +- [#4242](https://github.com/Effect-TS/effect/pull/4242) [`c1a0339`](https://github.com/Effect-TS/effect/commit/c1a0339034a291fd4463371afbcfc8adcf8994ae) Thanks @fubhy! - Add missing exports + +- Updated dependencies [[`d7dac48`](https://github.com/Effect-TS/effect/commit/d7dac48a477cdfeec509dbe9f33fce6a1b02b63d), [`c110032`](https://github.com/Effect-TS/effect/commit/c110032322450a8824ba38ae24335a538cd2ce9a), [`1d7fd2b`](https://github.com/Effect-TS/effect/commit/1d7fd2b7ee8eeecc912d27adf76ed897db236dc5), [`1d7fd2b`](https://github.com/Effect-TS/effect/commit/1d7fd2b7ee8eeecc912d27adf76ed897db236dc5), [`23ac740`](https://github.com/Effect-TS/effect/commit/23ac740c7dd4610b7d265c2071b88b0968419e9a), [`8cd7319`](https://github.com/Effect-TS/effect/commit/8cd7319b6568bfc7a30ca16c104d189e37eac3a0)]: + - effect@3.12.3 + - @effect/platform@0.73.0 + - @effect/platform-node-shared@0.23.0 + +## 0.52.2 + +### Patch Changes + +- Updated dependencies [[`734af82`](https://github.com/Effect-TS/effect/commit/734af82138e78b9c57a8355b1c6b80e80d38b222), [`b63c780`](https://github.com/Effect-TS/effect/commit/b63c78010893101520448ddda7019c487cf7eedd), [`212e784`](https://github.com/Effect-TS/effect/commit/212e78475f527147ec27c090bd13f789f55add7a), [`f852cb0`](https://github.com/Effect-TS/effect/commit/f852cb02040ea2f165e9b449615b8b1366add5d5), [`7276ae2`](https://github.com/Effect-TS/effect/commit/7276ae21062896adbb7508ac5b2dece95316322f), [`212e784`](https://github.com/Effect-TS/effect/commit/212e78475f527147ec27c090bd13f789f55add7a), [`212e784`](https://github.com/Effect-TS/effect/commit/212e78475f527147ec27c090bd13f789f55add7a), [`212e784`](https://github.com/Effect-TS/effect/commit/212e78475f527147ec27c090bd13f789f55add7a), [`c640d77`](https://github.com/Effect-TS/effect/commit/c640d77b33ad417876f4e8ffe8574ee6cbe5607f), [`0def088`](https://github.com/Effect-TS/effect/commit/0def0887cfdb6755729a64dfd52b3b9f46b0576c)]: + - effect@3.12.2 + - @effect/platform@0.72.2 + - @effect/platform-node-shared@0.22.2 + +## 0.52.1 + +### Patch Changes + +- Updated dependencies [[`302b57d`](https://github.com/Effect-TS/effect/commit/302b57d2cbf9b9ccc17450945aeebfb33cfe8d43), [`0988083`](https://github.com/Effect-TS/effect/commit/0988083d4594938590df5a287e5b27d38526dd07), [`8b46be6`](https://github.com/Effect-TS/effect/commit/8b46be6a3b8160362ab5ea9171c5e6932505125c), [`bfe8027`](https://github.com/Effect-TS/effect/commit/bfe802734b450a4b4ee069d1125dd37995db2bff), [`16dd657`](https://github.com/Effect-TS/effect/commit/16dd657033d8afac2ffea567b3c8bb27c9b249b6), [`39db211`](https://github.com/Effect-TS/effect/commit/39db211414e90c8db8fdad7dc8ce5b4661bcfaef)]: + - effect@3.12.1 + - @effect/platform@0.72.1 + - @effect/platform-node-shared@0.22.1 + +## 0.52.0 + +### Patch Changes + +- Updated dependencies [[`abb22a4`](https://github.com/Effect-TS/effect/commit/abb22a429b9c52c31e84856294f175d2064a9b4d), [`f369a89`](https://github.com/Effect-TS/effect/commit/f369a89e98bc682969803b9304adaf4557bb36c2), [`642376c`](https://github.com/Effect-TS/effect/commit/642376c63fd7d78754db991631a4d50a5dc79aa3), [`3d2b7a7`](https://github.com/Effect-TS/effect/commit/3d2b7a7e942a7157afae5b1cdbc6f3fef116428e), [`ef64c6f`](https://github.com/Effect-TS/effect/commit/ef64c6fec0d47da573c04230dde9ea729366d871), [`73f9c6f`](https://github.com/Effect-TS/effect/commit/73f9c6f2ff091512cf904cc54ab59965b86e87c8), [`17cb451`](https://github.com/Effect-TS/effect/commit/17cb4514590e8a86263f7aed009f24da8a237342), [`d801820`](https://github.com/Effect-TS/effect/commit/d80182060c2ee945d7e0e4728812abf9465a0d6a), [`e1eeb2d`](https://github.com/Effect-TS/effect/commit/e1eeb2d7064b3870041dab142f3057970699bbf1), [`c11f3a6`](https://github.com/Effect-TS/effect/commit/c11f3a60a05c3b5fc8e7ce90136728154dc505b0), [`618f7e0`](https://github.com/Effect-TS/effect/commit/618f7e092a1011e5090dca1e69b5e9285689654b), [`c0ba834`](https://github.com/Effect-TS/effect/commit/c0ba834d1995cf5a8b250e4780fd43f3e3881151), [`e1eeb2d`](https://github.com/Effect-TS/effect/commit/e1eeb2d7064b3870041dab142f3057970699bbf1)]: + - effect@3.12.0 + - @effect/platform@0.72.0 + - @effect/platform-node-shared@0.22.0 + +## 0.51.7 + +### Patch Changes + +- Updated dependencies [[`39457d4`](https://github.com/Effect-TS/effect/commit/39457d4897d9bc7df8af5c05d352866bbeae82eb), [`a475cc2`](https://github.com/Effect-TS/effect/commit/a475cc25fd7c9f26b27a8e98f8fbe43cc9e6ee3e), [`199214e`](https://github.com/Effect-TS/effect/commit/199214e21c616d8a0ccd7ed5f92e944e6c580193), [`b3c160d`](https://github.com/Effect-TS/effect/commit/b3c160d7a1fdfc2d3fb2440530f1ab80efc65133)]: + - effect@3.11.10 + - @effect/platform@0.71.7 + - @effect/platform-node-shared@0.21.7 + +## 0.51.6 + +### Patch Changes + +- Updated dependencies [[`1c08a0b`](https://github.com/Effect-TS/effect/commit/1c08a0b8505badcffb4d9cade5a746ea90c9557e), [`1ce703b`](https://github.com/Effect-TS/effect/commit/1ce703b041bbd7560c5c437c9b9be48f027937fd), [`1ce703b`](https://github.com/Effect-TS/effect/commit/1ce703b041bbd7560c5c437c9b9be48f027937fd)]: + - effect@3.11.9 + - @effect/platform@0.71.6 + - @effect/platform-node-shared@0.21.6 + +## 0.51.5 + +### Patch Changes + +- Updated dependencies [[`05d71f8`](https://github.com/Effect-TS/effect/commit/05d71f85622305705d8316817694a09762e60865), [`e66b920`](https://github.com/Effect-TS/effect/commit/e66b9205f25ab425d30640886eb3fb2c4715bc26)]: + - @effect/platform@0.71.5 + - @effect/platform-node-shared@0.21.5 + +## 0.51.4 + +### Patch Changes + +- Updated dependencies [[`909181a`](https://github.com/Effect-TS/effect/commit/909181a9ce9052a80432ccf52187e0723004bf7f), [`909181a`](https://github.com/Effect-TS/effect/commit/909181a9ce9052a80432ccf52187e0723004bf7f), [`1a6b52d`](https://github.com/Effect-TS/effect/commit/1a6b52dcf020d36e38a7bc90b648152cf5a8ccba)]: + - @effect/platform@0.71.4 + - effect@3.11.8 + - @effect/platform-node-shared@0.21.4 + +## 0.51.3 + +### Patch Changes + +- Updated dependencies [[`6984508`](https://github.com/Effect-TS/effect/commit/6984508c87f1bd91213b44c19b25ab5e2dcc1ce0), [`883639c`](https://github.com/Effect-TS/effect/commit/883639cc8ce47757f1cd39439391a8028c0812fe)]: + - @effect/platform@0.71.3 + - @effect/platform-node-shared@0.21.3 + +## 0.51.2 + +### Patch Changes + +- Updated dependencies [[`2408616`](https://github.com/Effect-TS/effect/commit/24086163b60b09cc6d0885bd565ef080dcbe866b), [`cec0b4d`](https://github.com/Effect-TS/effect/commit/cec0b4d152ef660be2ccdb0927255f2471436e6e), [`cec0b4d`](https://github.com/Effect-TS/effect/commit/cec0b4d152ef660be2ccdb0927255f2471436e6e), [`8d978c5`](https://github.com/Effect-TS/effect/commit/8d978c53f6fcc98d9d645ecba3e4b55d4297dd36), [`cec0b4d`](https://github.com/Effect-TS/effect/commit/cec0b4d152ef660be2ccdb0927255f2471436e6e), [`cec0b4d`](https://github.com/Effect-TS/effect/commit/cec0b4d152ef660be2ccdb0927255f2471436e6e)]: + - effect@3.11.7 + - @effect/platform@0.71.2 + - @effect/platform-node-shared@0.21.2 + +## 0.51.1 + +### Patch Changes + +- Updated dependencies [[`1d3df5b`](https://github.com/Effect-TS/effect/commit/1d3df5bc4324e88a392c348db35fd9d029c7b25e)]: + - @effect/platform@0.71.1 + - @effect/platform-node-shared@0.21.1 + +## 0.51.0 + +### Patch Changes + +- Updated dependencies [[`662d1ce`](https://github.com/Effect-TS/effect/commit/662d1ce6fb7da384a95888d5b2bb5605bdf3208d), [`c99a0f3`](https://github.com/Effect-TS/effect/commit/c99a0f376d049d3793ed33e146d9873b8a5e5b78), [`11fc401`](https://github.com/Effect-TS/effect/commit/11fc401f436f99bf4be95f56d50b0e4bdfe5edea), [`c99a0f3`](https://github.com/Effect-TS/effect/commit/c99a0f376d049d3793ed33e146d9873b8a5e5b78), [`31c62d8`](https://github.com/Effect-TS/effect/commit/31c62d83cbdcf9850a8b5331faa239601c60f78a)]: + - effect@3.11.6 + - @effect/platform@0.71.0 + - @effect/platform-node-shared@0.21.0 + +## 0.50.7 + +### Patch Changes + +- Updated dependencies [[`9f5a6f7`](https://github.com/Effect-TS/effect/commit/9f5a6f701bf7ba31adccd1f1bcfa8ab5614c9be8), [`22905cf`](https://github.com/Effect-TS/effect/commit/22905cf5addfb1ff3d2a6135c52036be958ae911), [`9f5a6f7`](https://github.com/Effect-TS/effect/commit/9f5a6f701bf7ba31adccd1f1bcfa8ab5614c9be8), [`1e59e4f`](https://github.com/Effect-TS/effect/commit/1e59e4fd778da18296812a2a32f36ca8ae50f60d), [`9f5a6f7`](https://github.com/Effect-TS/effect/commit/9f5a6f701bf7ba31adccd1f1bcfa8ab5614c9be8), [`8d914e5`](https://github.com/Effect-TS/effect/commit/8d914e504e7a22d0ea628e8af265ee450ff9530f), [`03bb00f`](https://github.com/Effect-TS/effect/commit/03bb00faa74f9e168a54a8cc0828a664fbb1ab05), [`9f5a6f7`](https://github.com/Effect-TS/effect/commit/9f5a6f701bf7ba31adccd1f1bcfa8ab5614c9be8), [`14e1149`](https://github.com/Effect-TS/effect/commit/14e1149f1af5a022f06eb8c2e4ba9fec17fe7426), [`9f5a6f7`](https://github.com/Effect-TS/effect/commit/9f5a6f701bf7ba31adccd1f1bcfa8ab5614c9be8), [`9f5a6f7`](https://github.com/Effect-TS/effect/commit/9f5a6f701bf7ba31adccd1f1bcfa8ab5614c9be8)]: + - effect@3.11.5 + - @effect/platform@0.70.7 + - @effect/platform-node-shared@0.20.7 + +## 0.50.6 + +### Patch Changes + +- Updated dependencies [[`9a5b8e3`](https://github.com/Effect-TS/effect/commit/9a5b8e36d184bd4967a88752cb6e755e1be263af)]: + - @effect/platform@0.70.6 + - @effect/platform-node-shared@0.20.6 + +## 0.50.5 + +### Patch Changes + +- [#4095](https://github.com/Effect-TS/effect/pull/4095) [`76b5996`](https://github.com/Effect-TS/effect/commit/76b59960a25149c37344977002a36f3116c8610c) Thanks @Zamion101! - Implement remoteAddress using BunServer.requestIp(source) + +- Updated dependencies [[`415f4c9`](https://github.com/Effect-TS/effect/commit/415f4c98321868531727a83cbaad70164f5e4c40), [`518b258`](https://github.com/Effect-TS/effect/commit/518b258a8a67ecd332a9252c35cc060f8368dee2), [`6e323a3`](https://github.com/Effect-TS/effect/commit/6e323a36faaee46b328c8e3cf60a76b3aff9907f), [`518b258`](https://github.com/Effect-TS/effect/commit/518b258a8a67ecd332a9252c35cc060f8368dee2), [`6e323a3`](https://github.com/Effect-TS/effect/commit/6e323a36faaee46b328c8e3cf60a76b3aff9907f)]: + - @effect/platform@0.70.5 + - effect@3.11.4 + - @effect/platform-node-shared@0.20.5 + +## 0.50.4 + +### Patch Changes + +- Updated dependencies [[`90906f7`](https://github.com/Effect-TS/effect/commit/90906f7f154b12c7182e8f39e3c55ef3937db857), [`3862cd3`](https://github.com/Effect-TS/effect/commit/3862cd3c7f6a542ed65fb81255b3bd696ce2f567), [`3862cd3`](https://github.com/Effect-TS/effect/commit/3862cd3c7f6a542ed65fb81255b3bd696ce2f567), [`343b6aa`](https://github.com/Effect-TS/effect/commit/343b6aa6ac4a74276bfc7c63ccbf4a1d72bc1bed), [`afba339`](https://github.com/Effect-TS/effect/commit/afba339adc11dad56b5a3b7ca94487e58f34d613)]: + - effect@3.11.3 + - @effect/platform@0.70.4 + - @effect/platform-node-shared@0.20.4 + +## 0.50.3 + +### Patch Changes + +- Updated dependencies [[`7044730`](https://github.com/Effect-TS/effect/commit/70447306be1aeeb7d87c230b2a96ec87b993ede9)]: + - @effect/platform@0.70.3 + - @effect/platform-node-shared@0.20.3 + +## 0.50.2 + +### Patch Changes + +- Updated dependencies [[`01cee56`](https://github.com/Effect-TS/effect/commit/01cee560b58d94b24cc20e98083251b73e658b41), [`c2249ea`](https://github.com/Effect-TS/effect/commit/c2249ea13fd98ab7d9aa628787931356d8ec2860), [`1358aa5`](https://github.com/Effect-TS/effect/commit/1358aa5326eaa85ef13ee8d1fed0b4a4288ed3eb), [`1de3fe7`](https://github.com/Effect-TS/effect/commit/1de3fe7d1cbafd6391eaa38c2300b99e332cc2aa)]: + - effect@3.11.2 + - @effect/platform@0.70.2 + - @effect/platform-node-shared@0.20.2 + +## 0.50.1 + +### Patch Changes + +- Updated dependencies [[`dd8a2d8`](https://github.com/Effect-TS/effect/commit/dd8a2d8e80d33b16719fc69361eaedf0b59d4620), [`a71bfef`](https://github.com/Effect-TS/effect/commit/a71bfef46f5061bb2502a61a333638a987b62273)]: + - effect@3.11.1 + - @effect/platform@0.70.1 + - @effect/platform-node-shared@0.20.1 + +## 0.50.0 + +### Patch Changes + +- [#3835](https://github.com/Effect-TS/effect/pull/3835) [`5eff3f6`](https://github.com/Effect-TS/effect/commit/5eff3f6fa3aae7e86948a62cbfd63b8d6c3bdf92) Thanks @tim-smart! - fix multipart support for bun http server + +- Updated dependencies [[`147434b`](https://github.com/Effect-TS/effect/commit/147434b03d5e1fd692dd9f126e5ab0910f3b76d3), [`6e69493`](https://github.com/Effect-TS/effect/commit/6e694930048bbaf98110f35f41566aeb9752d471), [`147434b`](https://github.com/Effect-TS/effect/commit/147434b03d5e1fd692dd9f126e5ab0910f3b76d3), [`5eff3f6`](https://github.com/Effect-TS/effect/commit/5eff3f6fa3aae7e86948a62cbfd63b8d6c3bdf92), [`d9fe79b`](https://github.com/Effect-TS/effect/commit/d9fe79bb5a3fe105d8e7a3bc2922a8ad936a5d10), [`251d189`](https://github.com/Effect-TS/effect/commit/251d189420bbba71990574e91098c499065f9a9b), [`5a259f3`](https://github.com/Effect-TS/effect/commit/5a259f3711b4369f55d885b568bdb21136155261), [`b4ce4ea`](https://github.com/Effect-TS/effect/commit/b4ce4ea7fd514a7e572f2dcd879c98f334981b0e), [`15fcc5a`](https://github.com/Effect-TS/effect/commit/15fcc5a0ea4bbf40ab48fa6a04fdda74f76f4c07), [`9bc9a47`](https://github.com/Effect-TS/effect/commit/9bc9a476800dc645903c888a68bb1d3baa3383c6), [`aadb8a4`](https://github.com/Effect-TS/effect/commit/aadb8a48d2cba197c06ec9996505510e48e4e5cb), [`1e2747c`](https://github.com/Effect-TS/effect/commit/1e2747c63a4820d1459cbbc88c71212983bd68bd), [`9264162`](https://github.com/Effect-TS/effect/commit/9264162a82783a651776fb7b87604564a63e7070), [`e0b9b09`](https://github.com/Effect-TS/effect/commit/e0b9b09e70c386b2da17d1f0a15b0511861c89e8), [`c36f3b9`](https://github.com/Effect-TS/effect/commit/c36f3b95df5ce9d71b66f22f26ce12eda8d3e848), [`672bde5`](https://github.com/Effect-TS/effect/commit/672bde5bec51c7d6f9862828e6a654cb2cb6f93d), [`aadb8a4`](https://github.com/Effect-TS/effect/commit/aadb8a48d2cba197c06ec9996505510e48e4e5cb)]: + - effect@3.11.0 + - @effect/platform@0.70.0 + - @effect/platform-node-shared@0.20.0 + +## 0.49.34 + +### Patch Changes + +- Updated dependencies [[`3069614`](https://github.com/Effect-TS/effect/commit/30696149271129fc618f6f2ccd1d8f2f6c0f9cd7), [`09a5e52`](https://github.com/Effect-TS/effect/commit/09a5e522fd9b221f05d85b1d1c8a740d4973c302)]: + - effect@3.10.20 + - @effect/platform@0.69.32 + - @effect/platform-node-shared@0.19.33 + +## 0.49.33 + +### Patch Changes + +- Updated dependencies [[`e6d4a37`](https://github.com/Effect-TS/effect/commit/e6d4a37c1d7e657b5ea44063a1cf586808228fe5)]: + - @effect/platform@0.69.31 + - @effect/platform-node-shared@0.19.32 + +## 0.49.32 + +### Patch Changes + +- Updated dependencies [[`270f199`](https://github.com/Effect-TS/effect/commit/270f199b31810fd643e4c22818698adcbdb5d396)]: + - @effect/platform@0.69.30 + - @effect/platform-node-shared@0.19.31 + +## 0.49.31 + +### Patch Changes + +- Updated dependencies [[`24cc35e`](https://github.com/Effect-TS/effect/commit/24cc35e26d6ed4a076470bc687ffd99cc50991b3)]: + - @effect/platform@0.69.29 + - @effect/platform-node-shared@0.19.30 + +## 0.49.30 + +### Patch Changes + +- Updated dependencies [[`edd72be`](https://github.com/Effect-TS/effect/commit/edd72be57b904d60c9cbffc2537901821a9da537), [`a3e2771`](https://github.com/Effect-TS/effect/commit/a3e277170a1f7cf61fd629acb60304c7e81d9498), [`944025b`](https://github.com/Effect-TS/effect/commit/944025bc5ce139f4a85846aa689bf30ec06a8ec1), [`54addee`](https://github.com/Effect-TS/effect/commit/54addee438a644bf010646c52042c7b89c5fc0a7), [`a9e00e4`](https://github.com/Effect-TS/effect/commit/a9e00e43f0b5dd22c1f9d5b78be6383daea09c20)]: + - @effect/platform@0.69.28 + - effect@3.10.19 + - @effect/platform-node-shared@0.19.29 + +## 0.49.29 + +### Patch Changes + +- Updated dependencies [[`af409cf`](https://github.com/Effect-TS/effect/commit/af409cf1d2ff973be11cc079ea373eaeedca25de), [`beaccae`](https://github.com/Effect-TS/effect/commit/beaccae2d15931e9fe475fb50a0b3638243fe3f7)]: + - effect@3.10.18 + - @effect/platform@0.69.27 + - @effect/platform-node-shared@0.19.28 + +## 0.49.28 + +### Patch Changes + +- Updated dependencies [[`c963886`](https://github.com/Effect-TS/effect/commit/c963886d5817986fcbd6bfa4ddf50aca8b6c8184), [`42c4ce6`](https://github.com/Effect-TS/effect/commit/42c4ce6f8d8c7d847e97757650a8ad9419a829d7)]: + - @effect/platform@0.69.26 + - effect@3.10.17 + - @effect/platform-node-shared@0.19.27 + +## 0.49.27 + +### Patch Changes + +- Updated dependencies [[`320557a`](https://github.com/Effect-TS/effect/commit/320557ab18d13c5e22fc7dc0d2a157eae461012f), [`4dca30c`](https://github.com/Effect-TS/effect/commit/4dca30cfcdafe4542e236489f71d6f171a5b4e38), [`1d99867`](https://github.com/Effect-TS/effect/commit/1d998671be3cd11043f232822e91dd8c98fccfa9), [`6dae414`](https://github.com/Effect-TS/effect/commit/6dae4147991a97ec14a99289bd25fadae7541e8d), [`6b0d737`](https://github.com/Effect-TS/effect/commit/6b0d737078bf63b97891e6bc47affc04b28f9cf7), [`d8356aa`](https://github.com/Effect-TS/effect/commit/d8356aad428a0c2290db52380220f81d9ec94232), [`7b93dd6`](https://github.com/Effect-TS/effect/commit/7b93dd622e2ab79c7072d79d0d9611e446202201)]: + - @effect/platform@0.69.25 + - effect@3.10.16 + - @effect/platform-node-shared@0.19.26 + +## 0.49.26 + +### Patch Changes + +- Updated dependencies [[`3cc6514`](https://github.com/Effect-TS/effect/commit/3cc6514d2dd64e010cb760cc29bfce98c349bb10)]: + - @effect/platform@0.69.24 + - @effect/platform-node-shared@0.19.25 + +## 0.49.25 + +### Patch Changes + +- Updated dependencies [[`3aff4d3`](https://github.com/Effect-TS/effect/commit/3aff4d38837c213bb2987973dc4b98febb9f92d2)]: + - @effect/platform@0.69.23 + - @effect/platform-node-shared@0.19.24 + +## 0.49.24 + +### Patch Changes + +- Updated dependencies [[`8398b32`](https://github.com/Effect-TS/effect/commit/8398b3208242a88239d4449910b7baf923cfe3b6), [`72e55b7`](https://github.com/Effect-TS/effect/commit/72e55b7c610784fcebdbadc592c876e23e76a986)]: + - effect@3.10.15 + - @effect/platform@0.69.22 + - @effect/platform-node-shared@0.19.23 + +## 0.49.23 + +### Patch Changes + +- Updated dependencies [[`f983946`](https://github.com/Effect-TS/effect/commit/f9839467b4cad6e788297764ef9f9f0b9fd203f9), [`2d8a750`](https://github.com/Effect-TS/effect/commit/2d8a75081eb83a0a81f817fdf6f428369c5064ab)]: + - effect@3.10.14 + - @effect/platform@0.69.21 + - @effect/platform-node-shared@0.19.22 + +## 0.49.22 + +### Patch Changes + +- Updated dependencies [[`995bbdf`](https://github.com/Effect-TS/effect/commit/995bbdffea2e332f203cd5b474cd6a1c77dfa6ae)]: + - effect@3.10.13 + - @effect/platform@0.69.20 + - @effect/platform-node-shared@0.19.21 + +## 0.49.21 + +### Patch Changes + +- Updated dependencies [[`eb8c52d`](https://github.com/Effect-TS/effect/commit/eb8c52d8b4c5e067ebf0a81eb742f5822e6439b5)]: + - @effect/platform@0.69.19 + - @effect/platform-node-shared@0.19.20 + +## 0.49.20 + +### Patch Changes + +- [#3904](https://github.com/Effect-TS/effect/pull/3904) [`dd14efe`](https://github.com/Effect-TS/effect/commit/dd14efe0ace255f571273aae876adea96267d7e6) Thanks @tim-smart! - improve platform/Worker shutdown and logging + +- Updated dependencies [[`a0584ec`](https://github.com/Effect-TS/effect/commit/a0584ece92ed784bfb139e9c5a699f02d1e71c2d), [`dd14efe`](https://github.com/Effect-TS/effect/commit/dd14efe0ace255f571273aae876adea96267d7e6), [`dd14efe`](https://github.com/Effect-TS/effect/commit/dd14efe0ace255f571273aae876adea96267d7e6)]: + - @effect/platform-node-shared@0.19.19 + - @effect/platform@0.69.18 + - effect@3.10.12 + +## 0.49.19 + +### Patch Changes + +- Updated dependencies [[`5eef499`](https://github.com/Effect-TS/effect/commit/5eef4998b6ccb7a5404d9e4fef85e57fa35fbb8a), [`8240b1c`](https://github.com/Effect-TS/effect/commit/8240b1c10d45312fc863cb679b1a1e8441af0c1a), [`5eef499`](https://github.com/Effect-TS/effect/commit/5eef4998b6ccb7a5404d9e4fef85e57fa35fbb8a)]: + - effect@3.10.11 + - @effect/platform@0.69.17 + - @effect/platform-node-shared@0.19.18 + +## 0.49.18 + +### Patch Changes + +- Updated dependencies [[`3ff8e5b`](https://github.com/Effect-TS/effect/commit/3ff8e5b4138c89b56111c075b290e4084d7d169c)]: + - @effect/platform-node-shared@0.19.17 + +## 0.49.17 + +### Patch Changes + +- Updated dependencies [[`cd720ae`](https://github.com/Effect-TS/effect/commit/cd720aedf7f2571edec0843d6a633e84e4832b28), [`cd720ae`](https://github.com/Effect-TS/effect/commit/cd720aedf7f2571edec0843d6a633e84e4832b28), [`cd720ae`](https://github.com/Effect-TS/effect/commit/cd720aedf7f2571edec0843d6a633e84e4832b28), [`b631f40`](https://github.com/Effect-TS/effect/commit/b631f40abbe649b2a089764585b5c39f6a695ac6), [`7d89650`](https://github.com/Effect-TS/effect/commit/7d8965036cd2ea435c8441ffec3345488baebf85)]: + - effect@3.10.10 + - @effect/platform-node-shared@0.19.16 + - @effect/platform@0.69.16 + +## 0.49.16 + +### Patch Changes + +- [#3885](https://github.com/Effect-TS/effect/pull/3885) [`8a30e1d`](https://github.com/Effect-TS/effect/commit/8a30e1dfa3a7103bf5414fc6a7fca3088d8c8c00) Thanks @tim-smart! - simplify HttpApiBuilder handler logic + +- Updated dependencies [[`8a30e1d`](https://github.com/Effect-TS/effect/commit/8a30e1dfa3a7103bf5414fc6a7fca3088d8c8c00)]: + - @effect/platform@0.69.15 + - @effect/platform-node-shared@0.19.15 + +## 0.49.15 + +### Patch Changes + +- Updated dependencies [[`a123e80`](https://github.com/Effect-TS/effect/commit/a123e80f111a625428a5b5622b7f55ee1073566b), [`bd5fcd3`](https://github.com/Effect-TS/effect/commit/bd5fcd3e6b603b1e505af90d6a00627c8eca6d41), [`0289d3b`](https://github.com/Effect-TS/effect/commit/0289d3b6391031d00329365bab9791b355031fe3), [`7386b71`](https://github.com/Effect-TS/effect/commit/7386b710e5be570e17f468928a6ed19d549a3e12), [`07c493a`](https://github.com/Effect-TS/effect/commit/07c493a598e096c7810cd06def8cfa43493c46b1), [`257ab1b`](https://github.com/Effect-TS/effect/commit/257ab1b539fa6e930b7ae2583a188376372200d7), [`4211a23`](https://github.com/Effect-TS/effect/commit/4211a2355bb3af3f0e756e2aae9d293379f25662)]: + - effect@3.10.9 + - @effect/platform@0.69.14 + - @effect/platform-node-shared@0.19.14 + +## 0.49.14 + +### Patch Changes + +- Updated dependencies [[`68b5c9e`](https://github.com/Effect-TS/effect/commit/68b5c9e44f34192cef26e1cadda5e661a027df41), [`9c9928d`](https://github.com/Effect-TS/effect/commit/9c9928dfeacd9ac33dc37eb0ca3d7d8c39175ada), [`6306e66`](https://github.com/Effect-TS/effect/commit/6306e6656092b350d4ede5746da6f245ec9f7e07), [`361c7f3`](https://github.com/Effect-TS/effect/commit/361c7f39a2c10ede9324847c3d3ba192a6f9b20a)]: + - effect@3.10.8 + - @effect/platform@0.69.13 + - @effect/platform-node-shared@0.19.13 + +## 0.49.13 + +### Patch Changes + +- Updated dependencies [[`33f5b9f`](https://github.com/Effect-TS/effect/commit/33f5b9ffaebea4f1bd0e391b44c41fb6230e743a), [`50f0281`](https://github.com/Effect-TS/effect/commit/50f0281b0d2116726b8927a6217622d5f394f3e4)]: + - effect@3.10.7 + - @effect/platform@0.69.12 + - @effect/platform-node-shared@0.19.12 + +## 0.49.12 + +### Patch Changes + +- Updated dependencies [[`ce1c21f`](https://github.com/Effect-TS/effect/commit/ce1c21ffc11902ac9ab453a51904207859d38552), [`81ddd45`](https://github.com/Effect-TS/effect/commit/81ddd45fc074b98206fafab416d9a5a28b31e07a)]: + - effect@3.10.6 + - @effect/platform@0.69.11 + - @effect/platform-node-shared@0.19.11 + +## 0.49.11 + +### Patch Changes + +- Updated dependencies [[`3a6d757`](https://github.com/Effect-TS/effect/commit/3a6d757badeebe00d8ef4d67530d073c8264dcfa), [`59d813a`](https://github.com/Effect-TS/effect/commit/59d813aa4973d1115cfc70cc3667508335f49693)]: + - effect@3.10.5 + - @effect/platform@0.69.10 + - @effect/platform-node-shared@0.19.10 + +## 0.49.10 + +### Patch Changes + +- Updated dependencies [[`2367708`](https://github.com/Effect-TS/effect/commit/2367708be449f9526a2047e321302d7bfb16f18e)]: + - @effect/platform@0.69.9 + - effect@3.10.4 + - @effect/platform-node-shared@0.19.9 + +## 0.49.9 + +### Patch Changes + +- Updated dependencies [[`522f7c5`](https://github.com/Effect-TS/effect/commit/522f7c518a5acfb55ef96d6796869f002cc3eaf8)]: + - @effect/platform@0.69.8 + - @effect/platform-node-shared@0.19.8 + +## 0.49.8 + +### Patch Changes + +- Updated dependencies [[`690d6c5`](https://github.com/Effect-TS/effect/commit/690d6c54d2145adb0af545c447db7d4755bf3c6b), [`b9423d8`](https://github.com/Effect-TS/effect/commit/b9423d8bf8181a2389fdbce1e3c14ac6fe8d54f5), [`279fe3a`](https://github.com/Effect-TS/effect/commit/279fe3a7168fe84e520c2cc88ba189a15f03a2bc)]: + - @effect/platform@0.69.7 + - effect@3.10.3 + - @effect/platform-node-shared@0.19.7 + +## 0.49.7 + +### Patch Changes + +- Updated dependencies [[`714e119`](https://github.com/Effect-TS/effect/commit/714e11945e45e5a2554ee058e6c43f82a8e309cf), [`c1afd55`](https://github.com/Effect-TS/effect/commit/c1afd55c54e61f9c432823d21b3d016f79160a37), [`42cd72a`](https://github.com/Effect-TS/effect/commit/42cd72a44ca9593e4d81fbb50e8111625fd0fb81)]: + - effect@3.10.2 + - @effect/platform@0.69.6 + - @effect/platform-node-shared@0.19.6 + +## 0.49.6 + +### Patch Changes + +- Updated dependencies [[`9604d6b`](https://github.com/Effect-TS/effect/commit/9604d6b616435103dafea8b53637a9d1450b4750)]: + - effect@3.10.1 + - @effect/platform@0.69.5 + - @effect/platform-node-shared@0.19.5 + +## 0.49.5 + +### Patch Changes + +- Updated dependencies [[`c86b1d7`](https://github.com/Effect-TS/effect/commit/c86b1d7cd47b66df190ef9775a475467c1abdbd6)]: + - @effect/platform@0.69.4 + - @effect/platform-node-shared@0.19.4 + +## 0.49.4 + +### Patch Changes + +- Updated dependencies [[`d5fba63`](https://github.com/Effect-TS/effect/commit/d5fba6391e1005e374aa0238f13edfbd65848313), [`1eb2c30`](https://github.com/Effect-TS/effect/commit/1eb2c30ba064398db5790e376dedcfad55b7b005), [`02d413e`](https://github.com/Effect-TS/effect/commit/02d413e7b6bc1c64885969c37cc3e4e690c94d7d)]: + - @effect/platform@0.69.3 + - @effect/platform-node-shared@0.19.3 + +## 0.49.3 + +### Patch Changes + +- Updated dependencies [[`e7afc47`](https://github.com/Effect-TS/effect/commit/e7afc47ce83e381c3f4aed2b2974e3b3d86a2340)]: + - @effect/platform@0.69.2 + - @effect/platform-node-shared@0.19.2 + +## 0.49.2 + +### Patch Changes + +- [#3806](https://github.com/Effect-TS/effect/pull/3806) [`a4aa34a`](https://github.com/Effect-TS/effect/commit/a4aa34a0c32b79f7c95f3eb36ee69a8e8e23684c) Thanks @tim-smart! - fix HttpServer.layerContext access before initialization + +## 0.49.1 + +### Patch Changes + +- [#3802](https://github.com/Effect-TS/effect/pull/3802) [`7564f56`](https://github.com/Effect-TS/effect/commit/7564f56bb2844cf39d2b0d2d9e93cf9b2205e9a8) Thanks @tim-smart! - add HttpServer.layerContext to platform-node/bun + +- Updated dependencies [[`7564f56`](https://github.com/Effect-TS/effect/commit/7564f56bb2844cf39d2b0d2d9e93cf9b2205e9a8), [`7564f56`](https://github.com/Effect-TS/effect/commit/7564f56bb2844cf39d2b0d2d9e93cf9b2205e9a8)]: + - @effect/platform@0.69.1 + - @effect/platform-node-shared@0.19.1 + +## 0.49.0 + +### Patch Changes + +- Updated dependencies [[`4a01828`](https://github.com/Effect-TS/effect/commit/4a01828b66d6213e9bbe18979c893b13f7bb29bf), [`6d9de6b`](https://github.com/Effect-TS/effect/commit/6d9de6b871c5c08e6509a4e830c3d74758faa198), [`4a01828`](https://github.com/Effect-TS/effect/commit/4a01828b66d6213e9bbe18979c893b13f7bb29bf), [`c79c4c1`](https://github.com/Effect-TS/effect/commit/c79c4c178390fe61ff6dda88c9e058862349343a), [`38d30f0`](https://github.com/Effect-TS/effect/commit/38d30f08b8da62f9c3e308b9250738cb8d17bdb5), [`5821ce3`](https://github.com/Effect-TS/effect/commit/5821ce3455b47d25e0a40cae6ce22af9db5fa556)]: + - effect@3.10.0 + - @effect/platform@0.69.0 + - @effect/platform-node-shared@0.19.0 + +## 0.48.6 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform@0.68.6 + - @effect/platform-node-shared@0.18.6 + +## 0.48.5 + +### Patch Changes + +- Updated dependencies [[`2036402`](https://github.com/Effect-TS/effect/commit/20364020b8b75a684791aa93d90626758023e9e9)]: + - @effect/platform@0.68.5 + - @effect/platform-node-shared@0.18.5 + +## 0.48.4 + +### Patch Changes + +- Updated dependencies [[`1b1ef29`](https://github.com/Effect-TS/effect/commit/1b1ef29ae302322f69dc938f9337aa97b4c63266)]: + - @effect/platform@0.68.4 + - @effect/platform-node-shared@0.18.4 + +## 0.48.3 + +### Patch Changes + +- [#3769](https://github.com/Effect-TS/effect/pull/3769) [`8c33087`](https://github.com/Effect-TS/effect/commit/8c330879425e80bed2f65e407cd59e991f0d7bec) Thanks @tim-smart! - add support for WebSocket protocols option + +- Updated dependencies [[`61a99b2`](https://github.com/Effect-TS/effect/commit/61a99b2bf9d757870ef0c2ec9d4c877cdd364a3d), [`8c33087`](https://github.com/Effect-TS/effect/commit/8c330879425e80bed2f65e407cd59e991f0d7bec)]: + - effect@3.9.2 + - @effect/platform@0.68.3 + - @effect/platform-node-shared@0.18.3 + +## 0.48.2 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform@0.68.2 + - @effect/platform-node-shared@0.18.2 + +## 0.48.1 + +### Patch Changes + +- Updated dependencies [[`b75ac5d`](https://github.com/Effect-TS/effect/commit/b75ac5d0909115507bedc90f18f2d34deb217769)]: + - @effect/platform@0.68.1 + - @effect/platform-node-shared@0.18.1 + +## 0.48.0 + +### Patch Changes + +- Updated dependencies [[`90ceeab`](https://github.com/Effect-TS/effect/commit/90ceeab3a04051b740af18c8af8bd73ee8ec6363), [`90ceeab`](https://github.com/Effect-TS/effect/commit/90ceeab3a04051b740af18c8af8bd73ee8ec6363), [`90ceeab`](https://github.com/Effect-TS/effect/commit/90ceeab3a04051b740af18c8af8bd73ee8ec6363)]: + - @effect/platform@0.68.0 + - @effect/platform-node-shared@0.18.0 + +## 0.47.1 + +### Patch Changes + +- Updated dependencies [[`3b2ad1d`](https://github.com/Effect-TS/effect/commit/3b2ad1d58a2e33dc1a72b7037396bd25ca1702a9)]: + - @effect/platform@0.67.1 + - effect@3.9.1 + - @effect/platform-node-shared@0.17.1 + +## 0.47.0 + +### Patch Changes + +- Updated dependencies [[`ff3d1aa`](https://github.com/Effect-TS/effect/commit/ff3d1aab290b4d1173b2dfc7e4c76abb4babdc16), [`0ba66f2`](https://github.com/Effect-TS/effect/commit/0ba66f2451641fd6990e02ec1ed01c014db9dab0), [`bf77f51`](https://github.com/Effect-TS/effect/commit/bf77f51b323c383224ebf08adf77a7a6e8c9b3cd), [`016f9ad`](https://github.com/Effect-TS/effect/commit/016f9ad931a4b3d09a34e5caf13d87c5b8e9c984), [`0779681`](https://github.com/Effect-TS/effect/commit/07796813f07de035719728733096ba64ce333469), [`534129f`](https://github.com/Effect-TS/effect/commit/534129f8113ce1a8ec50828083e16da9c86326c6), [`d75140c`](https://github.com/Effect-TS/effect/commit/d75140c7a664ceda43142d999f4ff8dcd36d6dda), [`be0451c`](https://github.com/Effect-TS/effect/commit/be0451c149b6618af79cb839cdf04af2db1efb03), [`9237ac6`](https://github.com/Effect-TS/effect/commit/9237ac69bc07de5b3b60076a0ad2921c21de7457), [`be0451c`](https://github.com/Effect-TS/effect/commit/be0451c149b6618af79cb839cdf04af2db1efb03), [`5b36494`](https://github.com/Effect-TS/effect/commit/5b364942e9a9003fdb8217324f8a2d8369c969da), [`c716adb`](https://github.com/Effect-TS/effect/commit/c716adb250ebbea1d1048d818ef7fed4f621d186), [`4986391`](https://github.com/Effect-TS/effect/commit/49863919cd8628c962a712fb1df30d2983820933), [`d75140c`](https://github.com/Effect-TS/effect/commit/d75140c7a664ceda43142d999f4ff8dcd36d6dda), [`d1387ae`](https://github.com/Effect-TS/effect/commit/d1387aebd1ff01bbebde26be46d488956e4daef6)]: + - effect@3.9.0 + - @effect/platform@0.67.0 + - @effect/platform-node-shared@0.17.0 + +## 0.46.4 + +### Patch Changes + +- Updated dependencies [[`88e85db`](https://github.com/Effect-TS/effect/commit/88e85db34bd402526e27a323e950d053fa34d232), [`f40da15`](https://github.com/Effect-TS/effect/commit/f40da15fbeb7c491840b8f409d47de79720891c3), [`8e94585`](https://github.com/Effect-TS/effect/commit/8e94585abe62753bf3af28bfae77926a7c570ac3), [`83887ca`](https://github.com/Effect-TS/effect/commit/83887ca1b1793916913d8550a4db4450cd14a044), [`5266b6c`](https://github.com/Effect-TS/effect/commit/5266b6cd86d76c3886da041c8829bca04b1a3110), [`cdead5c`](https://github.com/Effect-TS/effect/commit/cdead5c9cfd54dc6c4f215d9732f654c4a12e991), [`f40da15`](https://github.com/Effect-TS/effect/commit/f40da15fbeb7c491840b8f409d47de79720891c3), [`766a8af`](https://github.com/Effect-TS/effect/commit/766a8af307b414aca3648d91c4eab7493a5ec862)]: + - effect@3.8.5 + - @effect/platform@0.66.3 + - @effect/platform-node-shared@0.16.4 + +## 0.46.3 + +### Patch Changes + +- Updated dependencies [[`660cd0f`](https://github.com/Effect-TS/effect/commit/660cd0f93610e5e5588f25b852ae7cf4f1dd05bc)]: + - @effect/platform-node-shared@0.16.3 + +## 0.46.2 + +### Patch Changes + +- Updated dependencies [[`fd83d0e`](https://github.com/Effect-TS/effect/commit/fd83d0e548feff9ea2d53d370a0b626c4a1d940e), [`4509656`](https://github.com/Effect-TS/effect/commit/45096569d50262275ee984f44c456f5c83b62683), [`090e41c`](https://github.com/Effect-TS/effect/commit/090e41c636d720b1c7d89684a739855765ed4382)]: + - @effect/platform@0.66.2 + - effect@3.8.4 + - @effect/platform-node-shared@0.16.2 + +## 0.46.1 + +### Patch Changes + +- Updated dependencies [[`3812788`](https://github.com/Effect-TS/effect/commit/3812788d79caaab8f559a62fd443018a04ac5647), [`0e0af6d`](https://github.com/Effect-TS/effect/commit/0e0af6d6593d041bd2cdbced9fdaf8265ce166f2)]: + - @effect/platform@0.66.1 + - @effect/platform-node-shared@0.16.1 + +## 0.46.0 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform-node-shared@0.16.0 + - @effect/platform@0.66.0 + +## 0.45.5 + +### Patch Changes + +- Updated dependencies [[`321b201`](https://github.com/Effect-TS/effect/commit/321b201adcb6bbbeb806b3467dd0b4cf063ccda8), [`bb5ec6b`](https://github.com/Effect-TS/effect/commit/bb5ec6b4b6a6f537394596c5a596faf52cb2aef4)]: + - @effect/platform@0.65.5 + - effect@3.8.3 + - @effect/platform-node-shared@0.15.5 + +## 0.45.4 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform@0.65.4 + - @effect/platform-node-shared@0.15.4 + +## 0.45.3 + +### Patch Changes + +- Updated dependencies [[`f0d8ef1`](https://github.com/Effect-TS/effect/commit/f0d8ef1ce97ec2a87b09b3e24150cfeab85d6e2f)]: + - effect@3.8.2 + - @effect/platform@0.65.3 + - @effect/platform-node-shared@0.15.3 + +## 0.45.2 + +### Patch Changes + +- Updated dependencies [[`10bf621`](https://github.com/Effect-TS/effect/commit/10bf6213f36d8ddb00f058a4609b85220f3d8334), [`cd75658`](https://github.com/Effect-TS/effect/commit/cd756584c352064cb1654be7118a925d57475d49), [`ae36fa6`](https://github.com/Effect-TS/effect/commit/ae36fa68f754eeab9a54b6dc0f8b44db513aa2b6)]: + - effect@3.8.1 + - @effect/platform-node-shared@0.15.2 + - @effect/platform@0.65.2 + +## 0.45.1 + +### Patch Changes + +- Updated dependencies [[`e44c5f2`](https://github.com/Effect-TS/effect/commit/e44c5f228215738fe4e75023c7461bf9521249cb)]: + - @effect/platform@0.65.1 + - @effect/platform-node-shared@0.15.1 + +## 0.45.0 + +### Minor Changes + +- [#3541](https://github.com/Effect-TS/effect/pull/3541) [`7041393`](https://github.com/Effect-TS/effect/commit/7041393cff132e96566d3f36da0483a6ff6195e4) Thanks @tim-smart! - refactor /platform HttpClient + + #### HttpClient.fetch removed + + The `HttpClient.fetch` client implementation has been removed. Instead, you can + access a `HttpClient` using the corresponding `Context.Tag`. + + ```ts + import { FetchHttpClient, HttpClient } from "@effect/platform" + import { Effect } from "effect" + + Effect.gen(function* () { + const client = yield* HttpClient.HttpClient + + // make a get request + yield* client.get("https://jsonplaceholder.typicode.com/todos/1") + }).pipe( + Effect.scoped, + // the fetch client has been moved to the `FetchHttpClient` module + Effect.provide(FetchHttpClient.layer) + ) + ``` + + #### `HttpClient` interface now uses methods + + Instead of being a function that returns the response, the `HttpClient` + interface now uses methods to make requests. + + Some shorthand methods have been added to the `HttpClient` interface to make + less complex requests easier. + + ```ts + import { + FetchHttpClient, + HttpClient, + HttpClientRequest + } from "@effect/platform" + import { Effect } from "effect" + + Effect.gen(function* () { + const client = yield* HttpClient.HttpClient + + // make a get request + yield* client.get("https://jsonplaceholder.typicode.com/todos/1") + // make a post request + yield* client.post("https://jsonplaceholder.typicode.com/todos") + + // execute a request instance + yield* client.execute( + HttpClientRequest.get("https://jsonplaceholder.typicode.com/todos/1") + ) + }) + ``` + + #### Scoped `HttpClientResponse` helpers removed + + The `HttpClientResponse` helpers that also supplied the `Scope` have been removed. + + Instead, you can use the `HttpClientResponse` methods directly, and explicitly + add a `Effect.scoped` to the pipeline. + + ```ts + import { FetchHttpClient, HttpClient } from "@effect/platform" + import { Effect } from "effect" + + Effect.gen(function* () { + const client = yield* HttpClient.HttpClient + + yield* client.get("https://jsonplaceholder.typicode.com/todos/1").pipe( + Effect.flatMap((response) => response.json), + Effect.scoped // supply the `Scope` + ) + }) + ``` + + #### Some apis have been renamed + + Including the `HttpClientRequest` body apis, which is to make them more + discoverable. + +### Patch Changes + +- Updated dependencies [[`fcfa6ee`](https://github.com/Effect-TS/effect/commit/fcfa6ee30ffd07d998bf22799357bf58580a116f), [`bb9931b`](https://github.com/Effect-TS/effect/commit/bb9931b62e249a3b801f2cb9d097aec0c8511af7), [`5798f76`](https://github.com/Effect-TS/effect/commit/5798f7619529de33e5ba06f551806f68fedc19db), [`5f0bfa1`](https://github.com/Effect-TS/effect/commit/5f0bfa17205398d4e4818bfbcf9e1b505b3b1fc5), [`812a4e8`](https://github.com/Effect-TS/effect/commit/812a4e86e2d1aa23b477ef5829aa0e5c07784936), [`6a128f6`](https://github.com/Effect-TS/effect/commit/6a128f63f9b41fec2db70790b3bbb96cb9afa1ab), [`273565e`](https://github.com/Effect-TS/effect/commit/273565e7901639e8d0541930ab715aea9c80fbaa), [`adf7d7a`](https://github.com/Effect-TS/effect/commit/adf7d7a7dfce3a7021e9f3b0d847dc85be89d754), [`569a801`](https://github.com/Effect-TS/effect/commit/569a8017ef0a0bc203e4312867cbdd37b0effbd7), [`aa1fa53`](https://github.com/Effect-TS/effect/commit/aa1fa5301e886b9657c8eb0d38cb87cef92a8305), [`02f6b06`](https://github.com/Effect-TS/effect/commit/02f6b0660e12bee1069532a9cc18d3ab855257be), [`12b893e`](https://github.com/Effect-TS/effect/commit/12b893e63cc6dfada4aca7773b4783940e2edf25), [`bbad27e`](https://github.com/Effect-TS/effect/commit/bbad27ec0a90860593f759405caa877e7f4a655f), [`7041393`](https://github.com/Effect-TS/effect/commit/7041393cff132e96566d3f36da0483a6ff6195e4), [`e0d21a5`](https://github.com/Effect-TS/effect/commit/e0d21a54c8323728fbb75a32f4820a9996257809), [`adf7d7a`](https://github.com/Effect-TS/effect/commit/adf7d7a7dfce3a7021e9f3b0d847dc85be89d754), [`007289a`](https://github.com/Effect-TS/effect/commit/007289a52d5877f8e90e2dacf38171ff9bf603fd), [`42a8f99`](https://github.com/Effect-TS/effect/commit/42a8f99740eefdaf2c4544d2c345313f97547a36), [`eebfd29`](https://github.com/Effect-TS/effect/commit/eebfd29633fd5d38b505c5c0842036f61f05e913), [`040703d`](https://github.com/Effect-TS/effect/commit/040703d0e100cd5511e52d812c15492414262b5e)]: + - effect@3.8.0 + - @effect/platform@0.65.0 + - @effect/platform-node-shared@0.15.0 + +## 0.44.1 + +### Patch Changes + +- Updated dependencies [[`35a0f81`](https://github.com/Effect-TS/effect/commit/35a0f813141652d696461cd5d19fd146adaf85be), [`8261c5a`](https://github.com/Effect-TS/effect/commit/8261c5ae6fe86872292ec1fc1a58ab9cea2f5f51)]: + - effect@3.7.3 + - @effect/platform@0.64.1 + - @effect/platform-node-shared@0.14.1 + +## 0.44.0 + +### Minor Changes + +- [#3565](https://github.com/Effect-TS/effect/pull/3565) [`90ac8f6`](https://github.com/Effect-TS/effect/commit/90ac8f6f6053a2e4498f8b0cc56fe12777d02e1a) Thanks @tim-smart! - move Etag implementation to /platform + +### Patch Changes + +- Updated dependencies [[`90ac8f6`](https://github.com/Effect-TS/effect/commit/90ac8f6f6053a2e4498f8b0cc56fe12777d02e1a), [`90ac8f6`](https://github.com/Effect-TS/effect/commit/90ac8f6f6053a2e4498f8b0cc56fe12777d02e1a), [`3791e24`](https://github.com/Effect-TS/effect/commit/3791e241636b1dfe924a56f380ebc9a7ff0827a9), [`3791e24`](https://github.com/Effect-TS/effect/commit/3791e241636b1dfe924a56f380ebc9a7ff0827a9)]: + - @effect/platform-node-shared@0.14.0 + - @effect/platform@0.64.0 + +## 0.43.3 + +### Patch Changes + +- Updated dependencies [[`4a701c4`](https://github.com/Effect-TS/effect/commit/4a701c406da032563fedae459536c00ae5cfe3c7), [`64c2292`](https://github.com/Effect-TS/effect/commit/64c22927aa01e0159b0fa98ff55e742069af399c)]: + - @effect/platform@0.63.3 + - @effect/platform-node-shared@0.13.3 + +## 0.43.2 + +### Patch Changes + +- Updated dependencies [[`8a601d7`](https://github.com/Effect-TS/effect/commit/8a601d7a1f8ffe52ac9e6d67e9282a1495fe59c9), [`353ba19`](https://github.com/Effect-TS/effect/commit/353ba19f9b2b9e959f0a00d058c6d40a4bc02db7)]: + - effect@3.7.2 + - @effect/platform@0.63.2 + - @effect/platform-node-shared@0.13.2 + +## 0.43.1 + +### Patch Changes + +- Updated dependencies [[`79859e7`](https://github.com/Effect-TS/effect/commit/79859e71040d8edf1868b8530b90c650f4321eff), [`f6a469c`](https://github.com/Effect-TS/effect/commit/f6a469c190b9f00eee5ea0cd4d5912a0ef8b46f5), [`dcb9ec0`](https://github.com/Effect-TS/effect/commit/dcb9ec0db443894dd204d87450f779c44b9ad7f1), [`79aa6b1`](https://github.com/Effect-TS/effect/commit/79aa6b136e1f29b36f34e88cb2ff162bff2bb4ed)]: + - effect@3.7.1 + - @effect/platform@0.63.1 + - @effect/platform-node-shared@0.13.1 + +## 0.43.0 + +### Patch Changes + +- Updated dependencies [[`db89601`](https://github.com/Effect-TS/effect/commit/db89601ee9c1050c4e762b7bd7ec65a6a2799dfe), [`2f456cc`](https://github.com/Effect-TS/effect/commit/2f456cce5012b9fcb6b4e039190d527813b75b92), [`8745e41`](https://github.com/Effect-TS/effect/commit/8745e41ed96e3765dc6048efc2a9afbe05c8a1e9), [`e557838`](https://github.com/Effect-TS/effect/commit/e55783886b046d3c5f33447f455f9ccf2fa75922), [`d6e7e40`](https://github.com/Effect-TS/effect/commit/d6e7e40b1e2ad0c59aa02f07344d28601b14ebdc), [`8356321`](https://github.com/Effect-TS/effect/commit/8356321598da04bd77c1001f45a4e447bec5591d), [`192f2eb`](https://github.com/Effect-TS/effect/commit/192f2ebb2c4ddbf4bfd8baedd32140b2376868f4), [`718cb70`](https://github.com/Effect-TS/effect/commit/718cb70038629a6d58d02e407760e341f7c94474), [`e9d0310`](https://github.com/Effect-TS/effect/commit/e9d03107acbf204d9304f3e8aea0816b7d3c7dfb), [`6bf28f7`](https://github.com/Effect-TS/effect/commit/6bf28f7e3b1e5e0608ff567205fea0581d11666f)]: + - effect@3.7.0 + - @effect/platform@0.63.0 + - @effect/platform-node-shared@0.13.0 + +## 0.42.5 + +### Patch Changes + +- Updated dependencies [[`e809286`](https://github.com/Effect-TS/effect/commit/e8092865900608c4df7a6b7991b1c13cc1e4ca2d)]: + - effect@3.6.8 + - @effect/platform@0.62.5 + - @effect/platform-node-shared@0.12.5 + +## 0.42.4 + +### Patch Changes + +- Updated dependencies [[`e7a65e3`](https://github.com/Effect-TS/effect/commit/e7a65e3c6a08636bbfce3d3af3098bf28474364d), [`50ec889`](https://github.com/Effect-TS/effect/commit/50ec8897a49b7d1fe84f63107f89d543c52f3dfc)]: + - @effect/platform-node-shared@0.12.4 + - @effect/platform@0.62.4 + - effect@3.6.7 + +## 0.42.3 + +### Patch Changes + +- Updated dependencies [[`f960bf4`](https://github.com/Effect-TS/effect/commit/f960bf45239e9badac6e0ad3a602f4174cd7bbdf), [`46a575f`](https://github.com/Effect-TS/effect/commit/46a575f48a05457b782fb21f7827d338c9b59320)]: + - effect@3.6.6 + - @effect/platform@0.62.3 + - @effect/platform-node-shared@0.12.3 + +## 0.42.2 + +### Patch Changes + +- Updated dependencies [[`413994c`](https://github.com/Effect-TS/effect/commit/413994c9792f16d9d57cca3ae6eb254bf93bd261), [`14a47a8`](https://github.com/Effect-TS/effect/commit/14a47a8c1f3cff2186b8fe7a919a1d773888fb5b), [`0c09841`](https://github.com/Effect-TS/effect/commit/0c0984173be3d58f050b300a1a8aa89d76ba49ae)]: + - @effect/platform@0.62.2 + - effect@3.6.5 + - @effect/platform-node-shared@0.12.2 + +## 0.42.1 + +### Patch Changes + +- Updated dependencies [[`9efe0e5`](https://github.com/Effect-TS/effect/commit/9efe0e5b57ac557399be620822c21cc6e9add285)]: + - @effect/platform@0.62.1 + - @effect/platform-node-shared@0.12.1 + +## 0.42.0 + +### Patch Changes + +- [#3450](https://github.com/Effect-TS/effect/pull/3450) [`0e42a8f`](https://github.com/Effect-TS/effect/commit/0e42a8f045ecb1fd3d080edf3d49fef16a9b0ca1) Thanks @tim-smart! - update dependencies + +- Updated dependencies [[`8295281`](https://github.com/Effect-TS/effect/commit/8295281ae9bd7441e680402540bf3c8682ec417b), [`c940df6`](https://github.com/Effect-TS/effect/commit/c940df63800bf3c4396d91cf28ec34938642fd2c), [`00b6c6d`](https://github.com/Effect-TS/effect/commit/00b6c6d4001f5de728b7d990a1b14560b4961a63), [`f8d95a6`](https://github.com/Effect-TS/effect/commit/f8d95a61ad0762147933c5c32bb6d7237e18eef4)]: + - effect@3.6.4 + - @effect/platform@0.62.0 + - @effect/platform-node-shared@0.12.0 + +## 0.41.8 + +### Patch Changes + +- Updated dependencies [[`04adcac`](https://github.com/Effect-TS/effect/commit/04adcace913e6fc483df266874a68005e9e04ccf)]: + - effect@3.6.3 + - @effect/platform@0.61.8 + - @effect/platform-node-shared@0.11.8 + +## 0.41.7 + +### Patch Changes + +- Updated dependencies [[`17245a4`](https://github.com/Effect-TS/effect/commit/17245a4e783c19dee51529600b3b40f164fa59bc), [`630d40e`](https://github.com/Effect-TS/effect/commit/630d40eaa7eb4d2f8b6705b16d4f426bc28a7d09), [`630d40e`](https://github.com/Effect-TS/effect/commit/630d40eaa7eb4d2f8b6705b16d4f426bc28a7d09)]: + - @effect/platform@0.61.7 + - @effect/platform-node-shared@0.11.7 + +## 0.41.6 + +### Patch Changes + +- Updated dependencies [[`d829b57`](https://github.com/Effect-TS/effect/commit/d829b576357f2e3b203ab7e107a1492de903a106), [`fd4b2f6`](https://github.com/Effect-TS/effect/commit/fd4b2f6516b325740dde615f1cf0229edf13ca0c)]: + - @effect/platform@0.61.6 + - effect@3.6.2 + - @effect/platform-node-shared@0.11.6 + +## 0.41.5 + +### Patch Changes + +- [#3409](https://github.com/Effect-TS/effect/pull/3409) [`056b710`](https://github.com/Effect-TS/effect/commit/056b7108978e70612176c23991916f678d947f38) Thanks @sukovanej! - Add `BunHttpServer.layerTest`. + + ```ts + import { HttpClientRequest, HttpRouter, HttpServer } from "@effect/platform" + import { BunHttpServer } from "@effect/platform-bun" + import { expect, it } from "bun:test" + import { Effect } from "effect" + + it("test", () => + Effect.gen(function* (_) { + yield* HttpServer.serveEffect(HttpRouter.empty) + const response = yield* HttpClientRequest.get("/non-existing") + expect(response.status).toEqual(404) + }).pipe( + Effect.provide(BunHttpServer.layerTest), + Effect.scoped, + Effect.runPromise + )) + ``` + +- Updated dependencies [[`056b710`](https://github.com/Effect-TS/effect/commit/056b7108978e70612176c23991916f678d947f38)]: + - @effect/platform@0.61.5 + - @effect/platform-node-shared@0.11.5 + +## 0.41.4 + +### Patch Changes + +- Updated dependencies [[`e7cb109`](https://github.com/Effect-TS/effect/commit/e7cb109d0754207024a64d55b6bd2a674dd8ed7d)]: + - @effect/platform@0.61.4 + - @effect/platform-node-shared@0.11.4 + +## 0.41.3 + +### Patch Changes + +- Updated dependencies [[`510a34d`](https://github.com/Effect-TS/effect/commit/510a34d4cc5d2f51347a53847f6c7db84d2b17c6), [`fb9f786`](https://github.com/Effect-TS/effect/commit/fb9f7867f0c895e63f9ef23e8d0941248c42179d), [`45dbb9f`](https://github.com/Effect-TS/effect/commit/45dbb9ffeaf93d9e4df99d0cd4920e41ba9a3978)]: + - effect@3.6.1 + - @effect/platform@0.61.3 + - @effect/platform-node-shared@0.11.3 + +## 0.41.2 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform@0.61.2 + - @effect/platform-node-shared@0.11.2 + +## 0.41.1 + +### Patch Changes + +- Updated dependencies [[`11223bf`](https://github.com/Effect-TS/effect/commit/11223bf9cbf5b822e0bf9a9fb2b35b2ad88af692)]: + - @effect/platform@0.61.1 + - @effect/platform-node-shared@0.11.1 + +## 0.41.0 + +### Patch Changes + +- Updated dependencies [[`1e0fe80`](https://github.com/Effect-TS/effect/commit/1e0fe802b36c257971296617473ce0abe730e8dc), [`8135294`](https://github.com/Effect-TS/effect/commit/8135294b591ea94fde7e6f94a504608f0e630520), [`cd255a4`](https://github.com/Effect-TS/effect/commit/cd255a48872d8fb924cf713ef73f0883a9cc6987), [`3845646`](https://github.com/Effect-TS/effect/commit/3845646828e98f3c7cda1217f6cfe5f642ac0603), [`2d09078`](https://github.com/Effect-TS/effect/commit/2d09078c5948b37fc2f79ef858fe4ca3e4814085), [`4bce5a0`](https://github.com/Effect-TS/effect/commit/4bce5a0274203550ccf117d830721891b0a3d182), [`4ddbff0`](https://github.com/Effect-TS/effect/commit/4ddbff0bb4e3ffddfeb509c59835b83245fb975e), [`e74cc38`](https://github.com/Effect-TS/effect/commit/e74cc38cb420a320c4d7ef98180f19d452a8b316), [`bb069b4`](https://github.com/Effect-TS/effect/commit/bb069b49ef291c532a02c1e8e74271f6d1bb32ec), [`cd255a4`](https://github.com/Effect-TS/effect/commit/cd255a48872d8fb924cf713ef73f0883a9cc6987), [`7d02174`](https://github.com/Effect-TS/effect/commit/7d02174af3bcbf054e5cdddb821c91d0f47e8285)]: + - effect@3.6.0 + - @effect/platform@0.61.0 + - @effect/platform-node-shared@0.11.0 + +## 0.40.3 + +### Patch Changes + +- Updated dependencies [[`6359644`](https://github.com/Effect-TS/effect/commit/635964446323cf55d4060559337e710e4a24496e), [`7f41e42`](https://github.com/Effect-TS/effect/commit/7f41e428830bf3043b8be0d28dcd235d5747c942), [`f566fd1`](https://github.com/Effect-TS/effect/commit/f566fd1d7eea531a0d981dd24037f14a603a1273)]: + - effect@3.5.9 + - @effect/platform@0.60.3 + - @effect/platform-node-shared@0.10.3 + +## 0.40.2 + +### Patch Changes + +- Updated dependencies [[`eb4d014`](https://github.com/Effect-TS/effect/commit/eb4d014c559e1b4c95b3fb9295fe77593c17ed7a), [`1ba640c`](https://github.com/Effect-TS/effect/commit/1ba640c702f187a866023bf043c26e25cce941ef), [`c8c71bd`](https://github.com/Effect-TS/effect/commit/c8c71bd20eb87d23133dac6156b83bb08941597c), [`a26ce58`](https://github.com/Effect-TS/effect/commit/a26ce581ca7d407e1e81439b58c8045b3fa65231), [`fc20f73`](https://github.com/Effect-TS/effect/commit/fc20f73c69e577981cb64714de2adc97e1004dae)]: + - @effect/platform@0.60.2 + - effect@3.5.8 + - @effect/platform-node-shared@0.10.2 + +## 0.40.1 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform@0.60.1 + - @effect/platform-node-shared@0.10.1 + +## 0.40.0 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform@0.60.0 + - @effect/platform-node-shared@0.10.0 + +## 0.39.3 + +### Patch Changes + +- [#3310](https://github.com/Effect-TS/effect/pull/3310) [`99bddcf`](https://github.com/Effect-TS/effect/commit/99bddcfb3d6eab4d489d055404e26ad81afe52fc) Thanks @fubhy! - Added additional pure annotations to improve tree-shakeability + +- Updated dependencies [[`3afcc93`](https://github.com/Effect-TS/effect/commit/3afcc93413a3d910beb69e4ce9ae120e4adaffd5), [`99bddcf`](https://github.com/Effect-TS/effect/commit/99bddcfb3d6eab4d489d055404e26ad81afe52fc)]: + - effect@3.5.7 + - @effect/platform@0.59.3 + - @effect/platform-node-shared@0.9.3 + +## 0.39.2 + +### Patch Changes + +- Updated dependencies [[`cc327a1`](https://github.com/Effect-TS/effect/commit/cc327a1bccd22a4ee27ec7e58b53205e93b23e2c), [`4bfe4fb`](https://github.com/Effect-TS/effect/commit/4bfe4fb5c82f597c9beea9baa92e772593598b60), [`2b14d18`](https://github.com/Effect-TS/effect/commit/2b14d181462cad8359da4fa6bc6dfda0f742c398)]: + - effect@3.5.6 + - @effect/platform@0.59.2 + - @effect/platform-node-shared@0.9.2 + +## 0.39.1 + +### Patch Changes + +- Updated dependencies [[`a9d7800`](https://github.com/Effect-TS/effect/commit/a9d7800f6a253192b653d77778b0674f39b1ca39), [`fcecff7`](https://github.com/Effect-TS/effect/commit/fcecff7f7e12b295a252f124861b801c73072151), [`adbf753`](https://github.com/Effect-TS/effect/commit/adbf75340a9db15dc5cadc66e911a8978a195a65), [`adbf753`](https://github.com/Effect-TS/effect/commit/adbf75340a9db15dc5cadc66e911a8978a195a65)]: + - effect@3.5.5 + - @effect/platform@0.59.1 + - @effect/platform-node-shared@0.9.1 + +## 0.39.0 + +### Minor Changes + +- [#3260](https://github.com/Effect-TS/effect/pull/3260) [`53c0db0`](https://github.com/Effect-TS/effect/commit/53c0db06872d5b5edea2a706e83249908385325c) Thanks @tim-smart! - replace /platform RefailError with use of the "cause" property + +- [#3255](https://github.com/Effect-TS/effect/pull/3255) [`ada68b3`](https://github.com/Effect-TS/effect/commit/ada68b3e61c67907c2a281c024c84d818186ca4c) Thanks @tim-smart! - refactor & simplify /platform backing workers + + Improves worker performance by 2x + +### Patch Changes + +- [#3253](https://github.com/Effect-TS/effect/pull/3253) [`ed0dde4`](https://github.com/Effect-TS/effect/commit/ed0dde4888e6f1a97ad5bba06b755d26a6a1c52e) Thanks @tim-smart! - update dependencies + +- Updated dependencies [[`53c0db0`](https://github.com/Effect-TS/effect/commit/53c0db06872d5b5edea2a706e83249908385325c), [`ed0dde4`](https://github.com/Effect-TS/effect/commit/ed0dde4888e6f1a97ad5bba06b755d26a6a1c52e), [`ada68b3`](https://github.com/Effect-TS/effect/commit/ada68b3e61c67907c2a281c024c84d818186ca4c), [`ca775ce`](https://github.com/Effect-TS/effect/commit/ca775cec53baebc1a43d9b8852a3ac6726178498), [`5be9cc0`](https://github.com/Effect-TS/effect/commit/5be9cc044025a9541b9b7acefa2d3fc05fa1301b), [`203658f`](https://github.com/Effect-TS/effect/commit/203658f8001c132b25764ab70344b171683b554c), [`eb1c4d4`](https://github.com/Effect-TS/effect/commit/eb1c4d44e54b9d8d201a366d1ff94face2a6dcd3)]: + - @effect/platform-node-shared@0.9.0 + - @effect/platform@0.59.0 + - effect@3.5.4 + +## 0.38.26 + +### Patch Changes + +- Updated dependencies [[`edb0da3`](https://github.com/Effect-TS/effect/commit/edb0da383746d760f35d8582f5fb0cc0eeca9217), [`a1db40a`](https://github.com/Effect-TS/effect/commit/a1db40a650ab842e778654f0d88e80f2ef4fd6f3), [`c8d3fb0`](https://github.com/Effect-TS/effect/commit/c8d3fb0fe23585f6efb724af51fbab3ba1ad6e83), [`dabd028`](https://github.com/Effect-TS/effect/commit/dabd028decf9b7983ca16ebe0f48c05c11a84b68), [`786b2ab`](https://github.com/Effect-TS/effect/commit/786b2ab29d525c877bb84035dac9e2d6499339d1), [`fc57354`](https://github.com/Effect-TS/effect/commit/fc573547d41667016fce05eaee75960fcc6dce4d)]: + - effect@3.5.3 + - @effect/platform@0.58.27 + - @effect/platform-node-shared@0.8.26 + +## 0.38.25 + +### Patch Changes + +- Updated dependencies [[`639208e`](https://github.com/Effect-TS/effect/commit/639208eeb8a44622994f832bc2d45d06ab636bc8), [`6684b4c`](https://github.com/Effect-TS/effect/commit/6684b4c27d77a7fcc7af2e261a450edf971b62b5), [`6684b4c`](https://github.com/Effect-TS/effect/commit/6684b4c27d77a7fcc7af2e261a450edf971b62b5), [`6684b4c`](https://github.com/Effect-TS/effect/commit/6684b4c27d77a7fcc7af2e261a450edf971b62b5)]: + - effect@3.5.2 + - @effect/platform@0.58.26 + - @effect/platform-node-shared@0.8.25 + +## 0.38.24 + +### Patch Changes + +- Updated dependencies [[`0623fca`](https://github.com/Effect-TS/effect/commit/0623fca41679b0e3c5a10dd0f8985f91670bd721)]: + - @effect/platform@0.58.25 + - @effect/platform-node-shared@0.8.24 + +## 0.38.23 + +### Patch Changes + +- Updated dependencies [[`55fdd76`](https://github.com/Effect-TS/effect/commit/55fdd761ee95afd73b6a892c13fee92b36c02837)]: + - effect@3.5.1 + - @effect/platform@0.58.24 + - @effect/platform-node-shared@0.8.23 + +## 0.38.22 + +### Patch Changes + +- Updated dependencies [[`a1f5b83`](https://github.com/Effect-TS/effect/commit/a1f5b831a1bc7535988b370d68d0b3eb1123e0ce), [`a1f5b83`](https://github.com/Effect-TS/effect/commit/a1f5b831a1bc7535988b370d68d0b3eb1123e0ce), [`a1f5b83`](https://github.com/Effect-TS/effect/commit/a1f5b831a1bc7535988b370d68d0b3eb1123e0ce), [`60bc3d0`](https://github.com/Effect-TS/effect/commit/60bc3d0867b13e48b24dc22604b4dd2e7b2c1ca4), [`5ab348f`](https://github.com/Effect-TS/effect/commit/5ab348f265db3d283aa091ddca6d2d49137c16f2), [`60bc3d0`](https://github.com/Effect-TS/effect/commit/60bc3d0867b13e48b24dc22604b4dd2e7b2c1ca4), [`3e04bf8`](https://github.com/Effect-TS/effect/commit/3e04bf8a7127e956cadb7684a8f4c661df57663b), [`e7fc45f`](https://github.com/Effect-TS/effect/commit/e7fc45f0c7002aafdaec7878149ac064cd104ea3), [`a1f5b83`](https://github.com/Effect-TS/effect/commit/a1f5b831a1bc7535988b370d68d0b3eb1123e0ce), [`4626de5`](https://github.com/Effect-TS/effect/commit/4626de59c25b384216faa0be87bf0b8cd36357d0), [`f01e7db`](https://github.com/Effect-TS/effect/commit/f01e7db317827255d7901f523f2e28b43298e8df), [`60bc3d0`](https://github.com/Effect-TS/effect/commit/60bc3d0867b13e48b24dc22604b4dd2e7b2c1ca4), [`79d2d91`](https://github.com/Effect-TS/effect/commit/79d2d91464d95dde0e9444d43e7a7f309f05d6e6), [`ac71f37`](https://github.com/Effect-TS/effect/commit/ac71f378f2413e5aa91c95f649ffe898d6a26114), [`8432360`](https://github.com/Effect-TS/effect/commit/8432360ce68614a419bb328083a4109d0fc8aa93), [`e4bf1bf`](https://github.com/Effect-TS/effect/commit/e4bf1bf2b4a970eacd77c9b77b5ea8c68bc84498), [`13cb861`](https://github.com/Effect-TS/effect/commit/13cb861a5eded15c55c6cdcf6a8acde8320367a6), [`79d2d91`](https://github.com/Effect-TS/effect/commit/79d2d91464d95dde0e9444d43e7a7f309f05d6e6), [`e7fc45f`](https://github.com/Effect-TS/effect/commit/e7fc45f0c7002aafdaec7878149ac064cd104ea3), [`9f66825`](https://github.com/Effect-TS/effect/commit/9f66825f1fce0fe8d10420c285f7dc4c71e8af8d)]: + - effect@3.5.0 + - @effect/platform@0.58.23 + - @effect/platform-node-shared@0.8.22 + +## 0.38.21 + +### Patch Changes + +- [#3209](https://github.com/Effect-TS/effect/pull/3209) [`366f2ee`](https://github.com/Effect-TS/effect/commit/366f2ee3fb6f712a44e8f84fc188612e5ecc016d) Thanks @tim-smart! - simplify /platform http response handling + +- Updated dependencies [[`7af137c`](https://github.com/Effect-TS/effect/commit/7af137c9433f6e74959b3887561ec1e6f12e10ee), [`ee4b3dc`](https://github.com/Effect-TS/effect/commit/ee4b3dc5f68d19dc3ae1c2d12901c5b8ffbebabb), [`366f2ee`](https://github.com/Effect-TS/effect/commit/366f2ee3fb6f712a44e8f84fc188612e5ecc016d), [`366f2ee`](https://github.com/Effect-TS/effect/commit/366f2ee3fb6f712a44e8f84fc188612e5ecc016d), [`097d25c`](https://github.com/Effect-TS/effect/commit/097d25cb5d13c049e01789651be56b09620186ef)]: + - effect@3.4.9 + - @effect/platform@0.58.22 + - @effect/platform-node-shared@0.8.21 + +## 0.38.20 + +### Patch Changes + +- Updated dependencies [[`a435e0f`](https://github.com/Effect-TS/effect/commit/a435e0fc5378b33a49bcec92ee235df6f16a2419), [`b5554db`](https://github.com/Effect-TS/effect/commit/b5554db36c4dd6f64fa5e6a62a29b2759c54217a), [`a9c4fb3`](https://github.com/Effect-TS/effect/commit/a9c4fb3bf3c6e92cd1c142b0605fddf7eb3c697c)]: + - effect@3.4.8 + - @effect/platform@0.58.21 + - @effect/platform-node-shared@0.8.20 + +## 0.38.19 + +### Patch Changes + +- Updated dependencies [[`a5737d6`](https://github.com/Effect-TS/effect/commit/a5737d6db2b921605c332eabbc5402ee3d17357b)]: + - effect@3.4.7 + - @effect/platform@0.58.20 + - @effect/platform-node-shared@0.8.19 + +## 0.38.18 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform@0.58.19 + - @effect/platform-node-shared@0.8.18 + +## 0.38.17 + +### Patch Changes + +- Updated dependencies [[`7f8900a`](https://github.com/Effect-TS/effect/commit/7f8900a1de9addeb0d371103a2c5c2aa3e4ff95e)]: + - @effect/platform@0.58.18 + - @effect/platform-node-shared@0.8.17 + +## 0.38.16 + +### Patch Changes + +- Updated dependencies [[`5c0ceb0`](https://github.com/Effect-TS/effect/commit/5c0ceb00826cce9e50bf9d41d83e191d5352c030), [`5c0ceb0`](https://github.com/Effect-TS/effect/commit/5c0ceb00826cce9e50bf9d41d83e191d5352c030), [`33735b1`](https://github.com/Effect-TS/effect/commit/33735b16b41bd26929d8f4754c190925db6323b7), [`5c0ceb0`](https://github.com/Effect-TS/effect/commit/5c0ceb00826cce9e50bf9d41d83e191d5352c030), [`139d4b3`](https://github.com/Effect-TS/effect/commit/139d4b39fb3bff2eeaa7c0c809c581da42425a83)]: + - effect@3.4.6 + - @effect/platform@0.58.17 + - @effect/platform-node-shared@0.8.16 + +## 0.38.15 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform@0.58.16 + - @effect/platform-node-shared@0.8.15 + +## 0.38.14 + +### Patch Changes + +- Updated dependencies [[`baa90df`](https://github.com/Effect-TS/effect/commit/baa90df9663f5f37d7b6814dad25142d53dbc720)]: + - @effect/platform@0.58.15 + - @effect/platform-node-shared@0.8.14 + +## 0.38.13 + +### Patch Changes + +- Updated dependencies [[`52a87c7`](https://github.com/Effect-TS/effect/commit/52a87c7a0b9536398deaf8ec507e53a82c607219), [`6d2280e`](https://github.com/Effect-TS/effect/commit/6d2280e9497c95cb0e965ca462c825345074eedf)]: + - @effect/platform@0.58.14 + - @effect/platform-node-shared@0.8.13 + +## 0.38.12 + +### Patch Changes + +- Updated dependencies [[`a047af9`](https://github.com/Effect-TS/effect/commit/a047af99447dfffc729e9c8ef0ca143537927e91), [`dbd53ea`](https://github.com/Effect-TS/effect/commit/dbd53ea363c71a24449cb068251054c3a1acf864)]: + - effect@3.4.5 + - @effect/platform@0.58.13 + - @effect/platform-node-shared@0.8.12 + +## 0.38.11 + +### Patch Changes + +- Updated dependencies [[`74e0ad2`](https://github.com/Effect-TS/effect/commit/74e0ad23b4c36f41b7fd10856b20f8b701bc4044), [`74e0ad2`](https://github.com/Effect-TS/effect/commit/74e0ad23b4c36f41b7fd10856b20f8b701bc4044), [`72638e3`](https://github.com/Effect-TS/effect/commit/72638e3d99f0e93a24febf6c225256ce92d4a20b), [`d7dde2b`](https://github.com/Effect-TS/effect/commit/d7dde2b4af08b37af859d4c327c1f5c6f00cf9d9), [`9b2fc3b`](https://github.com/Effect-TS/effect/commit/9b2fc3b9dfd304a2bd0508ef2313cfc54357be0c)]: + - @effect/platform@0.58.12 + - effect@3.4.4 + - @effect/platform-node-shared@0.8.11 + +## 0.38.10 + +### Patch Changes + +- Updated dependencies [[`a5b95b5`](https://github.com/Effect-TS/effect/commit/a5b95b548284e4798654ae7ce6883fa49108f0ea), [`5e29579`](https://github.com/Effect-TS/effect/commit/5e29579187cb8420ea4930b3999fec984f8999f4)]: + - @effect/platform@0.58.11 + - @effect/platform-node-shared@0.8.10 + +## 0.38.9 + +### Patch Changes + +- [#3079](https://github.com/Effect-TS/effect/pull/3079) [`bbdd365`](https://github.com/Effect-TS/effect/commit/bbdd36567706c94cdec45bacea825941c347b6cd) Thanks @tim-smart! - update dependencies + +- [#3079](https://github.com/Effect-TS/effect/pull/3079) [`bbdd365`](https://github.com/Effect-TS/effect/commit/bbdd36567706c94cdec45bacea825941c347b6cd) Thanks @tim-smart! - update to typescript 5.5 + +- Updated dependencies [[`a48ee84`](https://github.com/Effect-TS/effect/commit/a48ee845ac21bbde9baf938af9e97a98322211c9), [`c342739`](https://github.com/Effect-TS/effect/commit/c3427396226e1ad7b95b40595a23f9bdff3e3365), [`8898e5e`](https://github.com/Effect-TS/effect/commit/8898e5e238622f6337583d91ee23609c1f5ccdf7), [`a48ee84`](https://github.com/Effect-TS/effect/commit/a48ee845ac21bbde9baf938af9e97a98322211c9), [`a48ee84`](https://github.com/Effect-TS/effect/commit/a48ee845ac21bbde9baf938af9e97a98322211c9), [`ff78636`](https://github.com/Effect-TS/effect/commit/ff786367c522975f40f0f179a0ecdfcfab7ecbdb), [`c86bd4e`](https://github.com/Effect-TS/effect/commit/c86bd4e134c23146c216f9ff97e03781d55991b6), [`bbdd365`](https://github.com/Effect-TS/effect/commit/bbdd36567706c94cdec45bacea825941c347b6cd), [`ab3180f`](https://github.com/Effect-TS/effect/commit/ab3180f827041d0ea3b2d72254a1a8683e99e056), [`bbdd365`](https://github.com/Effect-TS/effect/commit/bbdd36567706c94cdec45bacea825941c347b6cd)]: + - @effect/platform@0.58.10 + - effect@3.4.3 + - @effect/platform-node-shared@0.8.9 + +## 0.38.8 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform@0.58.9 + - @effect/platform-node-shared@0.8.8 + +## 0.38.7 + +### Patch Changes + +- Updated dependencies [[`3da1497`](https://github.com/Effect-TS/effect/commit/3da1497b5c9cc886d300258bc928fd68a4fefe6f)]: + - effect@3.4.2 + - @effect/platform@0.58.8 + - @effect/platform-node-shared@0.8.7 + +## 0.38.6 + +### Patch Changes + +- Updated dependencies [[`027004a`](https://github.com/Effect-TS/effect/commit/027004a897f654791e75faa28eefb50dd0244b6e)]: + - @effect/platform@0.58.7 + - @effect/platform-node-shared@0.8.6 + +## 0.38.5 + +### Patch Changes + +- Updated dependencies [[`66a1910`](https://github.com/Effect-TS/effect/commit/66a19109ff90c4252123b8809b8c8a74681dba6a), [`2e8e252`](https://github.com/Effect-TS/effect/commit/2e8e2520cac712f0eb644553bd476429ebd674e4)]: + - effect@3.4.1 + - @effect/platform@0.58.6 + - @effect/platform-node-shared@0.8.5 + +## 0.38.4 + +### Patch Changes + +- Updated dependencies [[`37a07a2`](https://github.com/Effect-TS/effect/commit/37a07a2d8d1ce09ab965c0ada84a3fae9a6aba05)]: + - @effect/platform@0.58.5 + - @effect/platform-node-shared@0.8.4 + +## 0.38.3 + +### Patch Changes + +- Updated dependencies [[`b77fb0a`](https://github.com/Effect-TS/effect/commit/b77fb0a811ec1ad0e794917077c9a90824515db8)]: + - @effect/platform@0.58.4 + - @effect/platform-node-shared@0.8.3 + +## 0.38.2 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform@0.58.3 + - @effect/platform-node-shared@0.8.2 + +## 0.38.1 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform@0.58.2 + - @effect/platform-node-shared@0.8.1 + +## 0.38.0 + +### Minor Changes + +- [#3036](https://github.com/Effect-TS/effect/pull/3036) [`5a248aa`](https://github.com/Effect-TS/effect/commit/5a248aa5ab2db3f7131ebc79bb9871a76de57973) Thanks @tim-smart! - rename NodeSocket.fromNetSocket to .fromDuplex + +### Patch Changes + +- Updated dependencies [[`5a248aa`](https://github.com/Effect-TS/effect/commit/5a248aa5ab2db3f7131ebc79bb9871a76de57973), [`5a248aa`](https://github.com/Effect-TS/effect/commit/5a248aa5ab2db3f7131ebc79bb9871a76de57973)]: + - @effect/platform@0.58.1 + - @effect/platform-node-shared@0.8.0 + +## 0.37.0 + +### Minor Changes + +- [#2938](https://github.com/Effect-TS/effect/pull/2938) [`63dd0c3`](https://github.com/Effect-TS/effect/commit/63dd0c3af45876c1caad7d03356c74daf551c628) Thanks @tim-smart! - restructure platform http to use flattened modules + + Instead of using the previous re-exports, you now use the modules directly. + + Before: + + ```ts + import { HttpClient } from "@effect/platform" + + HttpClient.request.get("/").pipe(HttpClient.client.fetchOk) + ``` + + After: + + ```ts + import { HttpClient, HttpClientRequest } from "@effect/platform" + + HttpClientRequest.get("/").pipe(HttpClient.fetchOk) + ``` + +### Patch Changes + +- Updated dependencies [[`c0ce180`](https://github.com/Effect-TS/effect/commit/c0ce180861ad0938053c0e6145e813fa6404df3b), [`63dd0c3`](https://github.com/Effect-TS/effect/commit/63dd0c3af45876c1caad7d03356c74daf551c628), [`61707b6`](https://github.com/Effect-TS/effect/commit/61707b6ffc7397c2ba0dce22512b44955724f60f), [`9c1b5b3`](https://github.com/Effect-TS/effect/commit/9c1b5b39e6c19604ce834f072a114ad392c50a06), [`a35faf8`](https://github.com/Effect-TS/effect/commit/a35faf8d116f94899bfc03feab33b004c8ddfdf7), [`ff73c0c`](https://github.com/Effect-TS/effect/commit/ff73c0cacd66132bfad2e5211b3eae347729c667), [`984d516`](https://github.com/Effect-TS/effect/commit/984d516ccd9412dc41188f6a46b748dd20dd5848), [`8c3b8a2`](https://github.com/Effect-TS/effect/commit/8c3b8a2ce208eab753b6206a51605a424f104e98), [`017e2f9`](https://github.com/Effect-TS/effect/commit/017e2f9b371ce24ea4945e5d7390c934ad3c39cf), [`91bf8a2`](https://github.com/Effect-TS/effect/commit/91bf8a2e9d1959393b3cf7366cc1d584d3e666b7), [`c6a4a26`](https://github.com/Effect-TS/effect/commit/c6a4a266606575fd2c7165940c4072ad4c57d01f)]: + - effect@3.4.0 + - @effect/platform-node-shared@0.7.0 + - @effect/platform@0.58.0 + +## 0.36.17 + +### Patch Changes + +- Updated dependencies [[`3ba7ea1`](https://github.com/Effect-TS/effect/commit/3ba7ea1c3c2923e85bf2f17e41176f8f8796d203)]: + - @effect/platform-node-shared@0.6.17 + - @effect/platform@0.57.8 + +## 0.36.16 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform@0.57.7 + - @effect/platform-node-shared@0.6.16 + +## 0.36.15 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform@0.57.6 + - @effect/platform-node-shared@0.6.15 + +## 0.36.14 + +### Patch Changes + +- Updated dependencies [[`b8ea6aa`](https://github.com/Effect-TS/effect/commit/b8ea6aa479006358042b4256ee0a1c5cfbe57acb)]: + - @effect/platform@0.57.5 + - @effect/platform-node-shared@0.6.14 + +## 0.36.13 + +### Patch Changes + +- Updated dependencies [[`6c89408`](https://github.com/Effect-TS/effect/commit/6c89408cd7b9204ec4c5828a46cd5312d8afb5e7)]: + - effect@3.3.5 + - @effect/platform@0.57.4 + - @effect/platform-node-shared@0.6.13 + +## 0.36.12 + +### Patch Changes + +- Updated dependencies [[`a67b8fe`](https://github.com/Effect-TS/effect/commit/a67b8fe2ace08419424811b5f0d9a5378eaea352)]: + - effect@3.3.4 + - @effect/platform@0.57.3 + - @effect/platform-node-shared@0.6.12 + +## 0.36.11 + +### Patch Changes + +- Updated dependencies [[`06ede85`](https://github.com/Effect-TS/effect/commit/06ede85d6e84710e6622463be95ff3927fb30dad), [`7204ca5`](https://github.com/Effect-TS/effect/commit/7204ca5761c2b1d27999a624db23aa10b6e0504d)]: + - effect@3.3.3 + - @effect/platform@0.57.2 + - @effect/platform-node-shared@0.6.11 + +## 0.36.10 + +### Patch Changes + +- Updated dependencies [[`07e12ec`](https://github.com/Effect-TS/effect/commit/07e12ecdb0e20b9763bd9e9058e567a7c8862efc), [`3572646`](https://github.com/Effect-TS/effect/commit/3572646d5e0804f85bc7f64633fb95722533f9dd), [`1aed347`](https://github.com/Effect-TS/effect/commit/1aed347a125ed3847ec90863424810d6759cbc85), [`df4bf4b`](https://github.com/Effect-TS/effect/commit/df4bf4b62e7b316c6647da0271fc5544a84e7ba2), [`f085f92`](https://github.com/Effect-TS/effect/commit/f085f92dfa204afb41823ffc27d437225137643d)]: + - @effect/platform-node-shared@0.6.10 + - @effect/platform@0.57.1 + - effect@3.3.2 + +## 0.36.9 + +### Patch Changes + +- Updated dependencies [[`eb98c5b`](https://github.com/Effect-TS/effect/commit/eb98c5b79ab50aa0cde239bd4e660dd19dbab612), [`184fed8`](https://github.com/Effect-TS/effect/commit/184fed83ac36cba05a75a5a8013f740f9f696e3b), [`4d3fbe8`](https://github.com/Effect-TS/effect/commit/4d3fbe82e8cec13ccd0cd0b2096deac6818fb59a), [`6068e07`](https://github.com/Effect-TS/effect/commit/6068e073d4cc8b3c8583583fd5eb3efe43f7d5ba), [`3a77e20`](https://github.com/Effect-TS/effect/commit/3a77e209783933bac3aaddba1b05ff6a9ac72b36)]: + - effect@3.3.1 + - @effect/platform@0.57.0 + - @effect/platform-node-shared@0.6.9 + +## 0.36.8 + +### Patch Changes + +- Updated dependencies [[`2b9ddfc`](https://github.com/Effect-TS/effect/commit/2b9ddfcbac505d98551e764a43923854907ca5c1), [`1f4ac00`](https://github.com/Effect-TS/effect/commit/1f4ac00a91c336c9c9c9b8c3ed9ceb9920ebc9bd), [`9305b76`](https://github.com/Effect-TS/effect/commit/9305b764cceeae4f16564435ae7172f79c2bf822), [`0f40d98`](https://github.com/Effect-TS/effect/commit/0f40d989da10f68df3ecd72b36849401ad679bfb), [`188f0a5`](https://github.com/Effect-TS/effect/commit/188f0a5c57ed0d7c9e5852e0c1c998f1b95810a1), [`b761ef0`](https://github.com/Effect-TS/effect/commit/b761ef00eaf6c67b7ffe34798b98aae5347ab376), [`b53f69b`](https://github.com/Effect-TS/effect/commit/b53f69bff1452a487b21198cd83961f844e02d36), [`0f40d98`](https://github.com/Effect-TS/effect/commit/0f40d989da10f68df3ecd72b36849401ad679bfb), [`5bd549e`](https://github.com/Effect-TS/effect/commit/5bd549e4bd7144727db438ecca6b8dc9b3ef7e22), [`67f160a`](https://github.com/Effect-TS/effect/commit/67f160a213de0219a565d4bf653b3cbf24f58e8f)]: + - @effect/platform@0.56.0 + - effect@3.3.0 + - @effect/platform-node-shared@0.6.8 + +## 0.36.7 + +### Patch Changes + +- Updated dependencies [[`a67d602`](https://github.com/Effect-TS/effect/commit/a67d60276f96cd20b76145b4cee13efca6c6158a)]: + - @effect/platform@0.55.7 + - @effect/platform-node-shared@0.6.7 + +## 0.36.6 + +### Patch Changes + +- Updated dependencies [[`8c5d280`](https://github.com/Effect-TS/effect/commit/8c5d280c0402284a4e58372867a15a431cb99461), [`6ba6d26`](https://github.com/Effect-TS/effect/commit/6ba6d269f5891e6b11aa35c5281dde4bf3273004), [`3f28bf2`](https://github.com/Effect-TS/effect/commit/3f28bf274333611906175446b772243f34f1b6d5), [`5817820`](https://github.com/Effect-TS/effect/commit/58178204a770d1a78c06945ef438f9fffbb50afa), [`799aa20`](https://github.com/Effect-TS/effect/commit/799aa20b4f618736ba33a5297fda90a75d4c26c6)]: + - effect@3.2.9 + - @effect/platform@0.55.6 + - @effect/platform-node-shared@0.6.6 + +## 0.36.5 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform@0.55.5 + - @effect/platform-node-shared@0.6.5 + +## 0.36.4 + +### Patch Changes + +- Updated dependencies [[`fb91f17`](https://github.com/Effect-TS/effect/commit/fb91f17098b48497feca9ec976feb87e4a82451b)]: + - effect@3.2.8 + - @effect/platform@0.55.4 + - @effect/platform-node-shared@0.6.4 + +## 0.36.3 + +### Patch Changes + +- Updated dependencies [[`6801fca`](https://github.com/Effect-TS/effect/commit/6801fca44366be3ee1b6b99f54bd4f38a1b5e4f4)]: + - effect@3.2.7 + - @effect/platform@0.55.3 + - @effect/platform-node-shared@0.6.3 + +## 0.36.2 + +### Patch Changes + +- Updated dependencies [[`2c2280b`](https://github.com/Effect-TS/effect/commit/2c2280b98a11fc002663c55792a4fa5781cd5fb6), [`cc8ac50`](https://github.com/Effect-TS/effect/commit/cc8ac5080daba8622ca2ff5dab5c37ddfab732ba)]: + - @effect/platform@0.55.2 + - effect@3.2.6 + - @effect/platform-node-shared@0.6.2 + +## 0.36.1 + +### Patch Changes + +- Updated dependencies [[`c5c94ed`](https://github.com/Effect-TS/effect/commit/c5c94edf1ddb0abb5c0e2adbb4ec2578a98d8e07)]: + - @effect/platform-node-shared@0.6.1 + - @effect/platform@0.55.1 + +## 0.36.0 + +### Minor Changes + +- [#2835](https://github.com/Effect-TS/effect/pull/2835) [`5133ca9`](https://github.com/Effect-TS/effect/commit/5133ca9dc4b8da0e28951316da9ab55dfbe0fbb9) Thanks @tim-smart! - remove pool resizing in platform workers to enable concurrent access + +### Patch Changes + +- Updated dependencies [[`608b01f`](https://github.com/Effect-TS/effect/commit/608b01fc342dbae2a642b308a67b84ead530ecea), [`031c712`](https://github.com/Effect-TS/effect/commit/031c7122a24ac42e48d6a434646b4f5d279d7442), [`a44e532`](https://github.com/Effect-TS/effect/commit/a44e532cf3a6a498b12a5aacf8124aa267e24ba0), [`5133ca9`](https://github.com/Effect-TS/effect/commit/5133ca9dc4b8da0e28951316da9ab55dfbe0fbb9)]: + - effect@3.2.5 + - @effect/platform@0.55.0 + - @effect/platform-node-shared@0.6.0 + +## 0.35.0 + +### Minor Changes + +- [#2801](https://github.com/Effect-TS/effect/pull/2801) [`1af94df`](https://github.com/Effect-TS/effect/commit/1af94df6b74aeb4f6ebcbe80e074b4cb252e62e3) Thanks @tim-smart! - remove `permits` from workers, to prevent issues with pool resizing + +### Patch Changes + +- Updated dependencies [[`1af94df`](https://github.com/Effect-TS/effect/commit/1af94df6b74aeb4f6ebcbe80e074b4cb252e62e3), [`1af94df`](https://github.com/Effect-TS/effect/commit/1af94df6b74aeb4f6ebcbe80e074b4cb252e62e3), [`c07e0ce`](https://github.com/Effect-TS/effect/commit/c07e0cea8ce165887e2c9dfa5d669eba9b2fb798), [`e313a01`](https://github.com/Effect-TS/effect/commit/e313a01b7e80f6cb7704055a190e5623c9d22c6d), [`1af94df`](https://github.com/Effect-TS/effect/commit/1af94df6b74aeb4f6ebcbe80e074b4cb252e62e3)]: + - effect@3.2.4 + - @effect/platform@0.54.0 + - @effect/platform-node-shared@0.5.0 + +## 0.34.20 + +### Patch Changes + +- [#2803](https://github.com/Effect-TS/effect/pull/2803) [`5af633e`](https://github.com/Effect-TS/effect/commit/5af633eb5ff6560a64d87263d1692bb9c75f7b3c) Thanks @tim-smart! - update dependencies + +- Updated dependencies [[`45578e8`](https://github.com/Effect-TS/effect/commit/45578e8faa80ae33d23e08f6f19467f818b7788f)]: + - effect@3.2.3 + - @effect/platform@0.53.14 + - @effect/platform-node-shared@0.4.33 + +## 0.34.19 + +### Patch Changes + +- Updated dependencies [[`5d9266e`](https://github.com/Effect-TS/effect/commit/5d9266e8c740746ac9e186c3df6090a1b57fbe2a), [`9f8122e`](https://github.com/Effect-TS/effect/commit/9f8122e78884ab47c5e5f364d86eee1d1543cc61), [`6a6f670`](https://github.com/Effect-TS/effect/commit/6a6f6706b8613c8c7c10971b8d81a0f9e440a6f2), [`c1eaef9`](https://github.com/Effect-TS/effect/commit/c1eaef910420dae416923d172ee58d219e921d0f)]: + - effect@3.2.2 + - @effect/platform@0.53.13 + - @effect/platform-node-shared@0.4.32 + +## 0.34.18 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform@0.53.12 + - @effect/platform-node-shared@0.4.31 + +## 0.34.17 + +### Patch Changes + +- Updated dependencies [[`c1e991d`](https://github.com/Effect-TS/effect/commit/c1e991dd5ba87901cd0e05697a8b4a267e7e954a)]: + - effect@3.2.1 + - @effect/platform@0.53.11 + - @effect/platform-node-shared@0.4.30 + +## 0.34.16 + +### Patch Changes + +- Updated dependencies [[`146cadd`](https://github.com/Effect-TS/effect/commit/146cadd9d004634a3ff85c480bf92cf975c853e2), [`7135748`](https://github.com/Effect-TS/effect/commit/713574813a0f64085db0b5240ba39e7a0a7c137e), [`7135748`](https://github.com/Effect-TS/effect/commit/713574813a0f64085db0b5240ba39e7a0a7c137e), [`963b4e7`](https://github.com/Effect-TS/effect/commit/963b4e7ac87e2468feb6a344f7ab4ee4ad711198), [`64c9414`](https://github.com/Effect-TS/effect/commit/64c9414e960e82058ca09bbb3976d6fbef303a8e), [`7135748`](https://github.com/Effect-TS/effect/commit/713574813a0f64085db0b5240ba39e7a0a7c137e), [`2cbb76b`](https://github.com/Effect-TS/effect/commit/2cbb76bb52500a3f4bf27d1c91482518cbea56d7), [`870c5fa`](https://github.com/Effect-TS/effect/commit/870c5fa52cd61e745e8e828d38c3f09f00737553), [`146cadd`](https://github.com/Effect-TS/effect/commit/146cadd9d004634a3ff85c480bf92cf975c853e2), [`7135748`](https://github.com/Effect-TS/effect/commit/713574813a0f64085db0b5240ba39e7a0a7c137e), [`64c9414`](https://github.com/Effect-TS/effect/commit/64c9414e960e82058ca09bbb3976d6fbef303a8e)]: + - effect@3.2.0 + - @effect/platform@0.53.10 + - @effect/platform-node-shared@0.4.29 + +## 0.34.15 + +### Patch Changes + +- [#2761](https://github.com/Effect-TS/effect/pull/2761) [`17fc22e`](https://github.com/Effect-TS/effect/commit/17fc22e132593c5caa563705a4748ba0f04a853c) Thanks [@KhraksMamtsov](https://github.com/KhraksMamtsov)! - Add `{ once: true }` to all `"abort"` event listeners for `AbortController` to automatically remove handlers after execution + +- Updated dependencies [[`17fc22e`](https://github.com/Effect-TS/effect/commit/17fc22e132593c5caa563705a4748ba0f04a853c), [`810f222`](https://github.com/Effect-TS/effect/commit/810f222268792b13067c7a7bf317b93a9bb8917b), [`596aaea`](https://github.com/Effect-TS/effect/commit/596aaea022648b2e06fb1ec22f1652043d6fe64e)]: + - @effect/platform@0.53.9 + - effect@3.1.6 + - @effect/platform-node-shared@0.4.28 + +## 0.34.14 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform@0.53.8 + - @effect/platform-node-shared@0.4.27 + +## 0.34.13 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform@0.53.7 + - @effect/platform-node-shared@0.4.26 + +## 0.34.12 + +### Patch Changes + +- Updated dependencies [[`6ac4847`](https://github.com/Effect-TS/effect/commit/6ac48479447c01a4f35d655552af93e47e562610), [`6ac4847`](https://github.com/Effect-TS/effect/commit/6ac48479447c01a4f35d655552af93e47e562610)]: + - @effect/platform-node-shared@0.4.25 + - @effect/platform@0.53.6 + - effect@3.1.5 + +## 0.34.11 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform@0.53.5 + - @effect/platform-node-shared@0.4.24 + +## 0.34.10 + +### Patch Changes + +- Updated dependencies [[`e41e911`](https://github.com/Effect-TS/effect/commit/e41e91122fa6dd12fc81e50dcad0db891be67146)]: + - effect@3.1.4 + - @effect/platform@0.53.4 + - @effect/platform-node-shared@0.4.23 + +## 0.34.9 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform@0.53.3 + - @effect/platform-node-shared@0.4.22 + +## 0.34.8 + +### Patch Changes + +- [#2722](https://github.com/Effect-TS/effect/pull/2722) [`d4fb55d`](https://github.com/Effect-TS/effect/commit/d4fb55dc04d370c19a1176fa13ff7266c222e15e) Thanks [@tim-smart](https://github.com/tim-smart)! - run .close() when browser worker shuts down + +## 0.34.7 + +### Patch Changes + +- Updated dependencies [[`1f6dc96`](https://github.com/Effect-TS/effect/commit/1f6dc96f51c7bb9c8d11415358308604ba7c7c8e)]: + - effect@3.1.3 + - @effect/platform@0.53.2 + - @effect/platform-node-shared@0.4.21 + +## 0.34.6 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform@0.53.1 + - @effect/platform-node-shared@0.4.20 + +## 0.34.5 + +### Patch Changes + +- Updated dependencies [[`d57fbbb`](https://github.com/Effect-TS/effect/commit/d57fbbbd6c466936213a671fc3cd2390064f864e)]: + - @effect/platform@0.53.0 + - @effect/platform-node-shared@0.4.19 + +## 0.34.4 + +### Patch Changes + +- Updated dependencies [[`5866c62`](https://github.com/Effect-TS/effect/commit/5866c621d7eb4cc84e4ba972bfdfd219734cd45d)]: + - @effect/platform@0.52.3 + - @effect/platform-node-shared@0.4.18 + +## 0.34.3 + +### Patch Changes + +- Updated dependencies [[`2e1cdf6`](https://github.com/Effect-TS/effect/commit/2e1cdf67d141281288fffe9a5c10d1379a800513)]: + - @effect/platform@0.52.2 + - effect@3.1.2 + - @effect/platform-node-shared@0.4.17 + +## 0.34.2 + +### Patch Changes + +- Updated dependencies [[`e5e56d1`](https://github.com/Effect-TS/effect/commit/e5e56d138dbed3204636f605229c6685f89659fc)]: + - effect@3.1.1 + - @effect/platform@0.52.1 + - @effect/platform-node-shared@0.4.16 + +## 0.34.1 + +### Patch Changes + +- Updated dependencies [[`9deab0a`](https://github.com/Effect-TS/effect/commit/9deab0aec9e99501f9441843e34df9afa10c5be9), [`7719b8a`](https://github.com/Effect-TS/effect/commit/7719b8a7350c14e952ffe685bfd5308773b3e271)]: + - @effect/platform@0.52.0 + - @effect/platform-node-shared@0.4.15 + +## 0.34.0 + +### Minor Changes + +- [#2543](https://github.com/Effect-TS/effect/pull/2543) [`0ec93cb`](https://github.com/Effect-TS/effect/commit/0ec93cb4f166e7401c171c2f8e8276ce958d9a57) Thanks [@github-actions](https://github.com/apps/github-actions)! - \* capitalised Http.multipart.FileSchema and Http.multipart.FilesSchema + - exported Http.multipart.FileSchema + - added Http.multipart.SingleFileSchema + +### Patch Changes + +- Updated dependencies [[`c3c12c6`](https://github.com/Effect-TS/effect/commit/c3c12c6625633fe80e79f9db75a3b8cf8ca8b11d), [`ba64ea6`](https://github.com/Effect-TS/effect/commit/ba64ea6757810c5e74cad3863a7d19d4d38af66b), [`b5de2d2`](https://github.com/Effect-TS/effect/commit/b5de2d2ce5b1afe8be90827bf898a95cec40eb2b), [`a1c7ab8`](https://github.com/Effect-TS/effect/commit/a1c7ab8ffedacd18c1fc784f4ff5844f79498b83), [`a023f28`](https://github.com/Effect-TS/effect/commit/a023f28336f3865687d9a30c1883e36909906d85), [`0ec93cb`](https://github.com/Effect-TS/effect/commit/0ec93cb4f166e7401c171c2f8e8276ce958d9a57), [`1c9454d`](https://github.com/Effect-TS/effect/commit/1c9454d532eae79b9f759aea77f59332cc6d18ed), [`a023f28`](https://github.com/Effect-TS/effect/commit/a023f28336f3865687d9a30c1883e36909906d85), [`92d56db`](https://github.com/Effect-TS/effect/commit/92d56dbb3f33e36636c2a2f1030c56492e39cf4d)]: + - effect@3.1.0 + - @effect/platform@0.51.0 + - @effect/platform-node-shared@0.4.14 + +## 0.33.13 + +### Patch Changes + +- [#2656](https://github.com/Effect-TS/effect/pull/2656) [`557707b`](https://github.com/Effect-TS/effect/commit/557707bc9e5f230c8964d2757012075c34339b5c) Thanks [@tim-smart](https://github.com/tim-smart)! - update dependencies + +- Updated dependencies [[`16039a0`](https://github.com/Effect-TS/effect/commit/16039a08f04f11545e2fdf40952788a8f9cef04f), [`557707b`](https://github.com/Effect-TS/effect/commit/557707bc9e5f230c8964d2757012075c34339b5c), [`d1d33e1`](https://github.com/Effect-TS/effect/commit/d1d33e10b25109f44b5ab1c6e4d778a59c0d3eeb), [`f4ed306`](https://github.com/Effect-TS/effect/commit/f4ed3068a70b50302d078a30d18ca3cfd2bc679c), [`661004f`](https://github.com/Effect-TS/effect/commit/661004f4bf5f8b25f5a0678c21a3a822188ce461), [`e79cb83`](https://github.com/Effect-TS/effect/commit/e79cb83d3b19098bc40a3012e2a059b8426306c2)]: + - @effect/platform@0.50.8 + - @effect/platform-node-shared@0.4.13 + - effect@3.0.8 + +## 0.33.12 + +### Patch Changes + +- Updated dependencies [[`18de56b`](https://github.com/Effect-TS/effect/commit/18de56b4a6b6d1f99230dfabf9147d59ea4dd759)]: + - effect@3.0.7 + - @effect/platform@0.50.7 + - @effect/platform-node-shared@0.4.12 + +## 0.33.11 + +### Patch Changes + +- Updated dependencies [[`ffe4f4e`](https://github.com/Effect-TS/effect/commit/ffe4f4e95db35fff6869e360b072e3837befa0a1), [`027418e`](https://github.com/Effect-TS/effect/commit/027418edaa6aa6c0ae4861b95832827b45adace4), [`ac1898e`](https://github.com/Effect-TS/effect/commit/ac1898eb7bc96880f911c276048e2ea3d6fe9c50), [`ffe4f4e`](https://github.com/Effect-TS/effect/commit/ffe4f4e95db35fff6869e360b072e3837befa0a1)]: + - effect@3.0.6 + - @effect/platform@0.50.6 + - @effect/platform-node-shared@0.4.11 + +## 0.33.10 + +### Patch Changes + +- Updated dependencies [[`6222404`](https://github.com/Effect-TS/effect/commit/62224044678751829ed2f128e05133a91c6b0569), [`868ed2a`](https://github.com/Effect-TS/effect/commit/868ed2a8fe94ee7f4206a6070f29dcf2a5ba1dc3)]: + - effect@3.0.5 + - @effect/platform@0.50.5 + - @effect/platform-node-shared@0.4.10 + +## 0.33.9 + +### Patch Changes + +- Updated dependencies [[`9a24667`](https://github.com/Effect-TS/effect/commit/9a246672008a2b668d43fbfd2fe5508c54b2b920)]: + - effect@3.0.4 + - @effect/platform@0.50.4 + - @effect/platform-node-shared@0.4.9 + +## 0.33.8 + +### Patch Changes + +- Updated dependencies [[`b3b51a2`](https://github.com/Effect-TS/effect/commit/b3b51a2ea0c6ab92a363db46ebaa7e1176d089f5)]: + - @effect/platform@0.50.3 + - @effect/platform-node-shared@0.4.8 + +## 0.33.7 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform@0.50.2 + - @effect/platform-node-shared@0.4.7 + +## 0.33.6 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform@0.50.1 + - @effect/platform-node-shared@0.4.6 + +## 0.33.5 + +### Patch Changes + +- Updated dependencies [[`6f38dff`](https://github.com/Effect-TS/effect/commit/6f38dff41ffa34532cc2f25b90446550c5730bb6), [`a7b4b84`](https://github.com/Effect-TS/effect/commit/a7b4b84bd5a25f51aba922f9259c3a58c98c6a4e), [`a3b0e6c`](https://github.com/Effect-TS/effect/commit/a3b0e6c490772e6d44b5d98dcf2729c4d5310ecc), [`6f38dff`](https://github.com/Effect-TS/effect/commit/6f38dff41ffa34532cc2f25b90446550c5730bb6)]: + - @effect/platform@0.50.0 + - effect@3.0.3 + - @effect/platform-node-shared@0.4.5 + +## 0.33.4 + +### Patch Changes + +- [#2562](https://github.com/Effect-TS/effect/pull/2562) [`2cecdbd`](https://github.com/Effect-TS/effect/commit/2cecdbd1cf30befce4e84796ccd953ea55ecfb86) Thanks [@fubhy](https://github.com/fubhy)! - Added provenance publishing + +- Updated dependencies [[`2cecdbd`](https://github.com/Effect-TS/effect/commit/2cecdbd1cf30befce4e84796ccd953ea55ecfb86)]: + - @effect/platform-node-shared@0.4.4 + - @effect/platform@0.49.4 + - effect@3.0.2 + +## 0.33.3 + +### Patch Changes + +- Updated dependencies [[`8d39d65`](https://github.com/Effect-TS/effect/commit/8d39d6554af548228ad767112ce2e0b1f68fa8e1)]: + - @effect/platform@0.49.3 + - @effect/platform-node-shared@0.4.3 + +## 0.33.2 + +### Patch Changes + +- Updated dependencies [[`5ef0a1a`](https://github.com/Effect-TS/effect/commit/5ef0a1ae9b773fa2481550cb0d43ff7a0e03cd44)]: + - @effect/platform-node-shared@0.4.2 + - @effect/platform@0.49.2 + +## 0.33.1 + +### Patch Changes + +- Updated dependencies [[`3da0cfa`](https://github.com/Effect-TS/effect/commit/3da0cfa12c407fd930dc480be1ecc9217a8058f8), [`570e8d8`](https://github.com/Effect-TS/effect/commit/570e8d87e7c0e9ad4cd2686462fdb9b4812f7716), [`87c5687`](https://github.com/Effect-TS/effect/commit/87c5687de0782dab177b7861217fa3b040046282), [`8edacca`](https://github.com/Effect-TS/effect/commit/8edacca37f8e37c01a63fec332b06d9361efaa7b)]: + - effect@3.0.1 + - @effect/platform-node-shared@0.4.1 + - @effect/platform@0.49.1 + +## 0.33.0 + +### Minor Changes + +- [#2207](https://github.com/Effect-TS/effect/pull/2207) [`aa4a3b5`](https://github.com/Effect-TS/effect/commit/aa4a3b550da1c1020265ac389ed3f309388994a2) Thanks [@github-actions](https://github.com/apps/github-actions)! - Swap type parameters in /platform data types + + A codemod has been released to make migration easier: + + ``` + npx @effect/codemod platform-0.49 src/**/* + ``` + +- [#2207](https://github.com/Effect-TS/effect/pull/2207) [`5a2314b`](https://github.com/Effect-TS/effect/commit/5a2314b70ec79c2c02b51cef45a5ddec8327daa1) Thanks [@github-actions](https://github.com/apps/github-actions)! - replace use of `unit` terminology with `void` + + For all the data types. + + ```ts + Effect.unit // => Effect.void + Stream.unit // => Stream.void + + // etc + ``` + +- [#2207](https://github.com/Effect-TS/effect/pull/2207) [`2fb7d9c`](https://github.com/Effect-TS/effect/commit/2fb7d9ca15037ff62a578bb9fe5732da5f4f317d) Thanks [@github-actions](https://github.com/apps/github-actions)! - Release Effect 3.0 🎉 + +### Patch Changes + +- [#2207](https://github.com/Effect-TS/effect/pull/2207) [`6460414`](https://github.com/Effect-TS/effect/commit/6460414351a45fb8e0a457c63f3653422efee766) Thanks [@github-actions](https://github.com/apps/github-actions)! - properly handle multiple ports in SharedWorker + +- [#2529](https://github.com/Effect-TS/effect/pull/2529) [`78b767c`](https://github.com/Effect-TS/effect/commit/78b767c2b1625186e17131761a0edbac25d21850) Thanks [@fubhy](https://github.com/fubhy)! - Renamed `ReadonlyArray` and `ReadonlyRecord` modules for better discoverability. + +- Updated dependencies [[`1b5f0c7`](https://github.com/Effect-TS/effect/commit/1b5f0c77e7fd477a0026071e82129a948227f4b3), [`d50a652`](https://github.com/Effect-TS/effect/commit/d50a652479f4d1d64f48da05c79fa847e6e51548), [`9a3bd47`](https://github.com/Effect-TS/effect/commit/9a3bd47ebd0750c7e498162734f6d21895de0cb2), [`6460414`](https://github.com/Effect-TS/effect/commit/6460414351a45fb8e0a457c63f3653422efee766), [`cf69f46`](https://github.com/Effect-TS/effect/commit/cf69f46690058d71eeada03cfb40dc744573e9e4), [`cf69f46`](https://github.com/Effect-TS/effect/commit/cf69f46690058d71eeada03cfb40dc744573e9e4), [`be9d025`](https://github.com/Effect-TS/effect/commit/be9d025e42355260ace02dd135851a8935a4deba), [`78b767c`](https://github.com/Effect-TS/effect/commit/78b767c2b1625186e17131761a0edbac25d21850), [`aa4a3b5`](https://github.com/Effect-TS/effect/commit/aa4a3b550da1c1020265ac389ed3f309388994a2), [`1499974`](https://github.com/Effect-TS/effect/commit/14999741d2e19c1747f6a7e19d68977f6429cdb8), [`1b5f0c7`](https://github.com/Effect-TS/effect/commit/1b5f0c77e7fd477a0026071e82129a948227f4b3), [`5c2b561`](https://github.com/Effect-TS/effect/commit/5c2b5614f583b88784ed68126ae939832fb3c092), [`6c6087a`](https://github.com/Effect-TS/effect/commit/6c6087a4a897b64252346426660782d31c13f769), [`a18f594`](https://github.com/Effect-TS/effect/commit/a18f5948f1439a147232448b2c443472fda0eceb), [`1499974`](https://github.com/Effect-TS/effect/commit/14999741d2e19c1747f6a7e19d68977f6429cdb8), [`2f96d93`](https://github.com/Effect-TS/effect/commit/2f96d938b90f8c19377583279e3c7afd9b509c50), [`5a2314b`](https://github.com/Effect-TS/effect/commit/5a2314b70ec79c2c02b51cef45a5ddec8327daa1), [`25d74f8`](https://github.com/Effect-TS/effect/commit/25d74f8c4d2dd4a9e5ec57ce2f20d36dedd25343), [`6c6087a`](https://github.com/Effect-TS/effect/commit/6c6087a4a897b64252346426660782d31c13f769), [`271b79f`](https://github.com/Effect-TS/effect/commit/271b79fc0b66a6c11e07a8779ff8800493a7eac2), [`1b5f0c7`](https://github.com/Effect-TS/effect/commit/1b5f0c77e7fd477a0026071e82129a948227f4b3), [`2fb7d9c`](https://github.com/Effect-TS/effect/commit/2fb7d9ca15037ff62a578bb9fe5732da5f4f317d), [`53d1c2a`](https://github.com/Effect-TS/effect/commit/53d1c2a77559081fbb89667e343346375c6d6650), [`e7e1bbe`](https://github.com/Effect-TS/effect/commit/e7e1bbe68486fdf31c8f84b0880522d39adcaad3), [`10c169e`](https://github.com/Effect-TS/effect/commit/10c169eadc874e91b4defca3f467b4e6a50fd8f3), [`6424181`](https://github.com/Effect-TS/effect/commit/64241815fe6a939e91e6947253e7dceea1306aa8)]: + - effect@3.0.0 + - @effect/platform@0.49.0 + - @effect/platform-node-shared@0.4.0 + +## 0.32.42 + +### Patch Changes + +- [#2517](https://github.com/Effect-TS/effect/pull/2517) [`b79cc59`](https://github.com/Effect-TS/effect/commit/b79cc59dbe64b9a0a7742dc9100a9d36c8e46b72) Thanks [@tim-smart](https://github.com/tim-smart)! - add uninterruptible option to http routes, for marking a route as uninterruptible + +- Updated dependencies [[`b79cc59`](https://github.com/Effect-TS/effect/commit/b79cc59dbe64b9a0a7742dc9100a9d36c8e46b72)]: + - @effect/platform@0.48.29 + - @effect/platform-node-shared@0.3.29 + +## 0.32.41 + +### Patch Changes + +- [#2504](https://github.com/Effect-TS/effect/pull/2504) [`da22adc`](https://github.com/Effect-TS/effect/commit/da22adc6507563876f1c416fd22a5f9206cc1395) Thanks [@tim-smart](https://github.com/tim-smart)! - use a FiberSet to run http server fibers + +- [#2515](https://github.com/Effect-TS/effect/pull/2515) [`d590094`](https://github.com/Effect-TS/effect/commit/d5900943489ec1e0891836aeafb5ce99fb9c75c7) Thanks [@tim-smart](https://github.com/tim-smart)! - add Http.router.uninterruptible, for marking a route as uninterruptible + +- Updated dependencies [[`41c8102`](https://github.com/Effect-TS/effect/commit/41c810228b1a50e4b41f19e735d7c62fe8d36871), [`776ef2b`](https://github.com/Effect-TS/effect/commit/776ef2bb66db9aa9f68b7beab14f6986f9c1288b), [`217147e`](https://github.com/Effect-TS/effect/commit/217147ea67c5c42c96f024775c41e5b070f81e4c), [`90776ec`](https://github.com/Effect-TS/effect/commit/90776ec8e8671d835b65fc33ead1de6c864b81b9), [`8709856`](https://github.com/Effect-TS/effect/commit/870985694ae985c3cb9360ad8a25c60e6f785f55), [`232c353`](https://github.com/Effect-TS/effect/commit/232c353c2e6f743f38e57639ee30e324ffa9c2a9), [`0ca835c`](https://github.com/Effect-TS/effect/commit/0ca835cbac8e69072a93ace83b534219faba24e8), [`8709856`](https://github.com/Effect-TS/effect/commit/870985694ae985c3cb9360ad8a25c60e6f785f55), [`d590094`](https://github.com/Effect-TS/effect/commit/d5900943489ec1e0891836aeafb5ce99fb9c75c7), [`e983740`](https://github.com/Effect-TS/effect/commit/e9837401145605aff5bc2ec7e73004f397c5d2d1), [`e3e0924`](https://github.com/Effect-TS/effect/commit/e3e09247d46a35430fc60e4aa4032cc50814f212)]: + - effect@2.4.19 + - @effect/platform@0.48.28 + - @effect/platform-node-shared@0.3.28 + +## 0.32.40 + +### Patch Changes + +- Updated dependencies [[`c6dd3c6`](https://github.com/Effect-TS/effect/commit/c6dd3c6909cafe05adc8450c5a499260e17e60d3), [`672f137`](https://github.com/Effect-TS/effect/commit/672f13747ddf6dac3ba304fd4511b1df44ab566d), [`672f137`](https://github.com/Effect-TS/effect/commit/672f13747ddf6dac3ba304fd4511b1df44ab566d)]: + - @effect/platform@0.48.27 + - @effect/platform-node-shared@0.3.27 + +## 0.32.39 + +### Patch Changes + +- Updated dependencies [[`365a486`](https://github.com/Effect-TS/effect/commit/365a4865de5e47ce09f4cfd51fc0f67438f82a57)]: + - @effect/platform@0.48.26 + - @effect/platform-node-shared@0.3.26 + +## 0.32.38 + +### Patch Changes + +- Updated dependencies [[`dadc690`](https://github.com/Effect-TS/effect/commit/dadc6906121c512bc32be22b52adbd1ada834594), [`d209171`](https://github.com/Effect-TS/effect/commit/d2091714a786820ebae4bef04a9d67d25dd08e88)]: + - effect@2.4.18 + - @effect/platform@0.48.25 + - @effect/platform-node-shared@0.3.25 + +## 0.32.37 + +### Patch Changes + +- Updated dependencies [[`f993857`](https://github.com/Effect-TS/effect/commit/f993857d5bb21ff7317ec69e481499632f0365f3), [`9c6a500`](https://github.com/Effect-TS/effect/commit/9c6a5001b467b6255c68a922f4b6e8d692b63d01), [`8fdfda6`](https://github.com/Effect-TS/effect/commit/8fdfda6618be848c01b399d13bc05a9a3adfb613), [`607b2e7`](https://github.com/Effect-TS/effect/commit/607b2e7a7fd9318c57acf4e50ec61747eea74ad7), [`8fdfda6`](https://github.com/Effect-TS/effect/commit/8fdfda6618be848c01b399d13bc05a9a3adfb613), [`35ad0ba`](https://github.com/Effect-TS/effect/commit/35ad0ba9f3ba27c60453620e514b980f819f92af), [`8206caf`](https://github.com/Effect-TS/effect/commit/8206caf7c2d22c68be4313318b61cfdacf6222b6), [`7ddd654`](https://github.com/Effect-TS/effect/commit/7ddd65415b65ccb654ad04f4dbefe39402f15117), [`7ddd654`](https://github.com/Effect-TS/effect/commit/7ddd65415b65ccb654ad04f4dbefe39402f15117), [`8fdfda6`](https://github.com/Effect-TS/effect/commit/8fdfda6618be848c01b399d13bc05a9a3adfb613), [`f456ba2`](https://github.com/Effect-TS/effect/commit/f456ba273bae21a6dcf8c966c50c97b5f0897d9f)]: + - @effect/platform-node-shared@0.3.24 + - @effect/platform@0.48.24 + - effect@2.4.17 + +## 0.32.36 + +### Patch Changes + +- Updated dependencies [[`5170ce7`](https://github.com/Effect-TS/effect/commit/5170ce708c606283e8a30d273950f1a21c7eddc2), [`63a1df2`](https://github.com/Effect-TS/effect/commit/63a1df2e4de3766f48f15676fbd0360ab9c27816), [`5170ce7`](https://github.com/Effect-TS/effect/commit/5170ce708c606283e8a30d273950f1a21c7eddc2), [`74a5dae`](https://github.com/Effect-TS/effect/commit/74a5daed0e65b32a36e026bfcf66d02269cb967a)]: + - @effect/platform@0.48.23 + - effect@2.4.16 + - @effect/platform-node-shared@0.3.23 + +## 0.32.35 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform@0.48.22 + - @effect/platform-node-shared@0.3.22 + +## 0.32.34 + +### Patch Changes + +- Updated dependencies [[`d7688c0`](https://github.com/Effect-TS/effect/commit/d7688c0c72717fe7876c871567f6946dabfc0546), [`b3a4fac`](https://github.com/Effect-TS/effect/commit/b3a4face2acaca422f0b0530436e8f13129f3b3a)]: + - effect@2.4.15 + - @effect/platform@0.48.21 + - @effect/platform-node-shared@0.3.21 + +## 0.32.33 + +### Patch Changes + +- Updated dependencies [[`4789083`](https://github.com/Effect-TS/effect/commit/4789083283bdaec456982d614ebc4a496ea0e7f7), [`4789083`](https://github.com/Effect-TS/effect/commit/4789083283bdaec456982d614ebc4a496ea0e7f7)]: + - @effect/platform@0.48.20 + - @effect/platform-node-shared@0.3.20 + +## 0.32.32 + +### Patch Changes + +- Updated dependencies [[`fb7285e`](https://github.com/Effect-TS/effect/commit/fb7285e8d6a70527df7137a6a3efdd03ae61cb8b)]: + - @effect/platform@0.48.19 + - @effect/platform-node-shared@0.3.19 + +## 0.32.31 + +### Patch Changes + +- Updated dependencies [[`26435ec`](https://github.com/Effect-TS/effect/commit/26435ecfa06569dc18d1801ccf38213a43b7c334), [`26435ec`](https://github.com/Effect-TS/effect/commit/26435ecfa06569dc18d1801ccf38213a43b7c334), [`6180c0c`](https://github.com/Effect-TS/effect/commit/6180c0cc51dee785cfce72220a52c9fc3b9bf9aa)]: + - @effect/platform-node-shared@0.3.18 + - @effect/platform@0.48.18 + - effect@2.4.14 + +## 0.32.30 + +### Patch Changes + +- [#2403](https://github.com/Effect-TS/effect/pull/2403) [`8c9abe2`](https://github.com/Effect-TS/effect/commit/8c9abe2b35c46d8891d4b2c14ff9eb46302a14f3) Thanks [@tim-smart](https://github.com/tim-smart)! - add set-cookie headers in Http.response.toWeb + +- Updated dependencies [[`47a8f1b`](https://github.com/Effect-TS/effect/commit/47a8f1b644d8294692d92cacd3c8c7543edbfabe), [`8c9abe2`](https://github.com/Effect-TS/effect/commit/8c9abe2b35c46d8891d4b2c14ff9eb46302a14f3), [`8c9abe2`](https://github.com/Effect-TS/effect/commit/8c9abe2b35c46d8891d4b2c14ff9eb46302a14f3), [`3336287`](https://github.com/Effect-TS/effect/commit/3336287ff55a25e56d759b83847bfaa21c40f499), [`54b7c00`](https://github.com/Effect-TS/effect/commit/54b7c0077fa784ad2646b812d6a44641f672edcd), [`3336287`](https://github.com/Effect-TS/effect/commit/3336287ff55a25e56d759b83847bfaa21c40f499), [`47a8f1b`](https://github.com/Effect-TS/effect/commit/47a8f1b644d8294692d92cacd3c8c7543edbfabe)]: + - @effect/platform@0.48.17 + - effect@2.4.13 + - @effect/platform-node-shared@0.3.17 + +## 0.32.29 + +### Patch Changes + +- [#2387](https://github.com/Effect-TS/effect/pull/2387) [`75a8d16`](https://github.com/Effect-TS/effect/commit/75a8d16247cc14860cdd7fd948ef542c50c2d55e) Thanks [@tim-smart](https://github.com/tim-smart)! - add Cookies module to /platform http + + To add cookies to a http response: + + ```ts + import * as Http from "@effect/platform/HttpServer" + + Http.response.empty().pipe( + Http.response.setCookies([ + ["name", "value"], + ["foo", "bar", { httpOnly: true }] + ]) + ) + ``` + + You can also use cookies with the http client: + + ```ts + import * as Http from "@effect/platform/HttpClient" + import { Effect, Ref } from "effect" + + Effect.gen(function* (_) { + const ref = yield* _(Ref.make(Http.cookies.empty)) + const defaultClient = yield* _(Http.client.Client) + const clientWithCookies = defaultClient.pipe( + Http.client.withCookiesRef(ref), + Http.client.filterStatusOk + ) + + // cookies will be stored in the ref and sent in any subsequent requests + yield* _( + Http.request.get("https://www.google.com/"), + clientWithCookies, + Effect.scoped + ) + }) + ``` + +- [#2385](https://github.com/Effect-TS/effect/pull/2385) [`3307729`](https://github.com/Effect-TS/effect/commit/3307729de162a033fa9caa8e14c111013dcf0d87) Thanks [@tim-smart](https://github.com/tim-smart)! - update typescript to 5.4 + +- Updated dependencies [[`75a8d16`](https://github.com/Effect-TS/effect/commit/75a8d16247cc14860cdd7fd948ef542c50c2d55e), [`3307729`](https://github.com/Effect-TS/effect/commit/3307729de162a033fa9caa8e14c111013dcf0d87)]: + - @effect/platform-node-shared@0.3.16 + - @effect/platform@0.48.16 + - effect@2.4.12 + +## 0.32.28 + +### Patch Changes + +- [#2384](https://github.com/Effect-TS/effect/pull/2384) [`2f488c4`](https://github.com/Effect-TS/effect/commit/2f488c436de52576562803c57ebc132ef40ccdd8) Thanks [@tim-smart](https://github.com/tim-smart)! - update dependencies + +- Updated dependencies [[`2f488c4`](https://github.com/Effect-TS/effect/commit/2f488c436de52576562803c57ebc132ef40ccdd8), [`37ca592`](https://github.com/Effect-TS/effect/commit/37ca592a4101ad90adbf8c8b3f727faf3110cae5), [`317b5b8`](https://github.com/Effect-TS/effect/commit/317b5b8e8c8c2207469b3ebfcf72bf3a9f7cbc60)]: + - @effect/platform-node-shared@0.3.15 + - effect@2.4.11 + - @effect/platform@0.48.15 + +## 0.32.27 + +### Patch Changes + +- Updated dependencies [[`9bab1f9`](https://github.com/Effect-TS/effect/commit/9bab1f9fa5b999740755e4e82485cb77c638643a), [`9bbde5b`](https://github.com/Effect-TS/effect/commit/9bbde5be9a0168d1c2a0308bfc27167ed62f3968)]: + - effect@2.4.10 + - @effect/platform@0.48.14 + - @effect/platform-node-shared@0.3.14 + +## 0.32.26 + +### Patch Changes + +- Updated dependencies [[`1879f62`](https://github.com/Effect-TS/effect/commit/1879f629d0c4815dbb5955779247cd3f3da5cd85)]: + - @effect/platform-node-shared@0.3.13 + - @effect/platform@0.48.13 + +## 0.32.25 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform@0.48.12 + - @effect/platform-node-shared@0.3.12 + +## 0.32.24 + +### Patch Changes + +- Updated dependencies [[`0f6c7b4`](https://github.com/Effect-TS/effect/commit/0f6c7b426eb3432f60e3a17f8cd92ceac91597bf)]: + - @effect/platform-node-shared@0.3.11 + - @effect/platform@0.48.11 + +## 0.32.23 + +### Patch Changes + +- [#2357](https://github.com/Effect-TS/effect/pull/2357) [`71fd528`](https://github.com/Effect-TS/effect/commit/71fd5287500f9ce155a7d9f0df6ee3e0ac3aeb99) Thanks [@tim-smart](https://github.com/tim-smart)! - make more data types in /platform implement Inspectable + +- Updated dependencies [[`71fd528`](https://github.com/Effect-TS/effect/commit/71fd5287500f9ce155a7d9f0df6ee3e0ac3aeb99)]: + - @effect/platform-node-shared@0.3.10 + - @effect/platform@0.48.10 + - effect@2.4.9 + +## 0.32.22 + +### Patch Changes + +- Updated dependencies [[`bb0b69e`](https://github.com/Effect-TS/effect/commit/bb0b69e519698c7c76aa68217de423c78ad16566), [`6b20bad`](https://github.com/Effect-TS/effect/commit/6b20badebb3a7ca4d38857753e8ecaa09d02ccfb), [`4e64e9b`](https://github.com/Effect-TS/effect/commit/4e64e9b9876de6bfcbabe39e18a91a08e5f3fbb0), [`3851a02`](https://github.com/Effect-TS/effect/commit/3851a022c481006aec1db36651e4b4fd727aa742), [`5f5fcd9`](https://github.com/Effect-TS/effect/commit/5f5fcd969ae30ed6fe61d566a571498d9e895e16), [`814e5b8`](https://github.com/Effect-TS/effect/commit/814e5b828f68210b9e8f336fd6ac688646835dd9)]: + - effect@2.4.8 + - @effect/platform@0.48.9 + - @effect/platform-node-shared@0.3.9 + +## 0.32.21 + +### Patch Changes + +- Updated dependencies [[`69d27bb`](https://github.com/Effect-TS/effect/commit/69d27bb633884b6b50f9c3d9e95c29f09b4860b5)]: + - @effect/platform-node-shared@0.3.8 + - @effect/platform@0.48.8 + +## 0.32.20 + +### Patch Changes + +- Updated dependencies [[`f908948`](https://github.com/Effect-TS/effect/commit/f908948fd05771a670c0b746e2dd9caa9408ef83)]: + - @effect/platform@0.48.7 + - @effect/platform-node-shared@0.3.7 + +## 0.32.19 + +### Patch Changes + +- Updated dependencies [[`eb93283`](https://github.com/Effect-TS/effect/commit/eb93283985913d7b04ca750e36ac8513e7b6cef6)]: + - effect@2.4.7 + - @effect/platform@0.48.6 + - @effect/platform-node-shared@0.3.6 + +## 0.32.18 + +### Patch Changes + +- Updated dependencies [[`e006e4a`](https://github.com/Effect-TS/effect/commit/e006e4a538c97bae6ca1efa74802159e8a688fcb)]: + - @effect/platform-node-shared@0.3.5 + - @effect/platform@0.48.5 + +## 0.32.17 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform@0.48.4 + - @effect/platform-node-shared@0.3.4 + +## 0.32.16 + +### Patch Changes + +- Updated dependencies [[`c362e06`](https://github.com/Effect-TS/effect/commit/c362e066550252d5a9fcbc31a4b34d0e17c50699), [`83ddd6f`](https://github.com/Effect-TS/effect/commit/83ddd6f41029724b2cbd144cf309463967ed1164)]: + - @effect/platform-node-shared@0.3.3 + - @effect/platform@0.48.3 + +## 0.32.15 + +### Patch Changes + +- Updated dependencies [[`4f35a7e`](https://github.com/Effect-TS/effect/commit/4f35a7e7c4eba598924aff24d1158b9056bb24be), [`9971186`](https://github.com/Effect-TS/effect/commit/99711862722188fbb5ed3ee75126ad5edf13f72f)]: + - @effect/platform@0.48.2 + - effect@2.4.6 + - @effect/platform-node-shared@0.3.2 + +## 0.32.14 + +### Patch Changes + +- Updated dependencies [[`bce21c5`](https://github.com/Effect-TS/effect/commit/bce21c5ded2177114666ba229bd5029fa000dee3), [`c7d3036`](https://github.com/Effect-TS/effect/commit/c7d303630b7f0825cb2e584557c5767a67214d9f)]: + - effect@2.4.5 + - @effect/platform@0.48.1 + - @effect/platform-node-shared@0.3.1 + +## 0.32.13 + +### Patch Changes + +- [#2283](https://github.com/Effect-TS/effect/pull/2283) [`509be1a`](https://github.com/Effect-TS/effect/commit/509be1a0817118489750cf028523134677e44a8a) Thanks [@tim-smart](https://github.com/tim-smart)! - add SocketCloseError with additional metadata + +- Updated dependencies [[`5d47ee0`](https://github.com/Effect-TS/effect/commit/5d47ee0855e492532085b6092879b1b952d84949), [`1cb7f9c`](https://github.com/Effect-TS/effect/commit/1cb7f9cff7c2272a32fc7a324d87b02e2cd8a2f5), [`817a04c`](https://github.com/Effect-TS/effect/commit/817a04cb2df0f4140984dc97eb3e1bb14a6c4a38), [`d90a99d`](https://github.com/Effect-TS/effect/commit/d90a99d03d074adc7cd2533f15419138264da5a2), [`dd05faa`](https://github.com/Effect-TS/effect/commit/dd05faa621555ef3585ecd914ac13ecd89b710f4), [`509be1a`](https://github.com/Effect-TS/effect/commit/509be1a0817118489750cf028523134677e44a8a), [`1cb7f9c`](https://github.com/Effect-TS/effect/commit/1cb7f9cff7c2272a32fc7a324d87b02e2cd8a2f5), [`dd05faa`](https://github.com/Effect-TS/effect/commit/dd05faa621555ef3585ecd914ac13ecd89b710f4), [`e7ca973`](https://github.com/Effect-TS/effect/commit/e7ca973c5430ae60716701e58bedd4632ff971fd), [`a1f44cb`](https://github.com/Effect-TS/effect/commit/a1f44cb5112713ff9a3ac3d91a63a2c99d6b7fc1), [`d910dd2`](https://github.com/Effect-TS/effect/commit/d910dd2ca1e8e5aa2f09d9bf3694ede745758f99), [`e7ca973`](https://github.com/Effect-TS/effect/commit/e7ca973c5430ae60716701e58bedd4632ff971fd), [`802674b`](https://github.com/Effect-TS/effect/commit/802674b379b7559ad3ff09b33388891445a9e48b), [`bdff193`](https://github.com/Effect-TS/effect/commit/bdff193365dd9ec2863573b08eb960aa8dee5c93)]: + - effect@2.4.4 + - @effect/platform@0.48.0 + - @effect/platform-node-shared@0.3.0 + +## 0.32.12 + +### Patch Changes + +- [#2276](https://github.com/Effect-TS/effect/pull/2276) [`0680545`](https://github.com/Effect-TS/effect/commit/068054540f19bb23a79c7c021ed8b2fe34f3e19f) Thanks [@tim-smart](https://github.com/tim-smart)! - improve /platform error messages + +- Updated dependencies [[`0680545`](https://github.com/Effect-TS/effect/commit/068054540f19bb23a79c7c021ed8b2fe34f3e19f), [`20e63fb`](https://github.com/Effect-TS/effect/commit/20e63fb9207210f3fe2d136ec40d0a2dbff3225e), [`20e63fb`](https://github.com/Effect-TS/effect/commit/20e63fb9207210f3fe2d136ec40d0a2dbff3225e)]: + - @effect/platform@0.47.1 + - effect@2.4.3 + - @effect/platform-node-shared@0.2.5 + +## 0.32.11 + +### Patch Changes + +- [#2267](https://github.com/Effect-TS/effect/pull/2267) [`0f3d99c`](https://github.com/Effect-TS/effect/commit/0f3d99c27521ec6b221b644a0fffc79199c3acca) Thanks [@tim-smart](https://github.com/tim-smart)! - propogate Socket handler errors to .run Effect + +- [#2261](https://github.com/Effect-TS/effect/pull/2261) [`fa9663c`](https://github.com/Effect-TS/effect/commit/fa9663cb854ca03dba672d7857ecff84f1140c9e) Thanks [@tim-smart](https://github.com/tim-smart)! - add websocket support to platform http server + + You can use the `Http.request.upgrade*` apis to access the `Socket` for the request. + + Here is an example server that handles websockets on the `/ws` path: + + ```ts + import { NodeHttpServer, NodeRuntime } from "@effect/platform-node" + import * as Http from "@effect/platform/HttpServer" + import { Console, Effect, Layer, Schedule, Stream } from "effect" + import { createServer } from "node:http" + + const ServerLive = NodeHttpServer.server.layer(() => createServer(), { + port: 3000 + }) + + const HttpLive = Http.router.empty.pipe( + Http.router.get( + "/ws", + Effect.gen(function* (_) { + yield* _( + Stream.fromSchedule(Schedule.spaced(1000)), + Stream.map(JSON.stringify), + Stream.encodeText, + Stream.pipeThroughChannel(Http.request.upgradeChannel()), + Stream.decodeText(), + Stream.runForEach(Console.log) + ) + return Http.response.empty() + }) + ), + Http.server.serve(Http.middleware.logger), + Http.server.withLogAddress, + Layer.provide(ServerLive) + ) + + NodeRuntime.runMain(Layer.launch(HttpLive)) + ``` + +- Updated dependencies [[`e03811e`](https://github.com/Effect-TS/effect/commit/e03811e80c93e986e6348b3b67ac2ed6d5fefff0), [`ac41d84`](https://github.com/Effect-TS/effect/commit/ac41d84776484cdce8165b7ca2c9c9b6377eee2d), [`0f3d99c`](https://github.com/Effect-TS/effect/commit/0f3d99c27521ec6b221b644a0fffc79199c3acca), [`6137533`](https://github.com/Effect-TS/effect/commit/613753300c7705518ab1fea2f370b032851c2750), [`f373529`](https://github.com/Effect-TS/effect/commit/f373529999f4b8bc92b634f6ea14f19271388eed), [`1bf9f31`](https://github.com/Effect-TS/effect/commit/1bf9f31f07667de677673f7c29a4e7a26ebad3c8), [`e3ff789`](https://github.com/Effect-TS/effect/commit/e3ff789226f89e71eb28ca38ce79f90af6a03f1a), [`6137533`](https://github.com/Effect-TS/effect/commit/613753300c7705518ab1fea2f370b032851c2750), [`507ba40`](https://github.com/Effect-TS/effect/commit/507ba4060ff043c1a8d541dae723fa6940633b00), [`4064ea0`](https://github.com/Effect-TS/effect/commit/4064ea04e0b3fa23108ee471cd89ab2482b2f6e5), [`e466afe`](https://github.com/Effect-TS/effect/commit/e466afe32f2de598ceafd8982bd0cfbd388e5671), [`f373529`](https://github.com/Effect-TS/effect/commit/f373529999f4b8bc92b634f6ea14f19271388eed), [`de74eb8`](https://github.com/Effect-TS/effect/commit/de74eb80a79eebde5ff645033765e7a617e92f27), [`fa9663c`](https://github.com/Effect-TS/effect/commit/fa9663cb854ca03dba672d7857ecff84f1140c9e), [`fa9663c`](https://github.com/Effect-TS/effect/commit/fa9663cb854ca03dba672d7857ecff84f1140c9e)]: + - effect@2.4.2 + - @effect/platform-node-shared@0.2.4 + - @effect/platform@0.47.0 + +## 0.32.10 + +### Patch Changes + +- Updated dependencies [[`a4a0006`](https://github.com/Effect-TS/effect/commit/a4a0006c7f19fc261df5cda16963d73457e4d6ac), [`7535080`](https://github.com/Effect-TS/effect/commit/7535080f2e2f9859711031161600c01807cc43ea), [`0a37676`](https://github.com/Effect-TS/effect/commit/0a37676aa0eb2a21e17af2e6df9f81f52bbc8831), [`bd1d7ac`](https://github.com/Effect-TS/effect/commit/bd1d7ac75eea57a94d5e2d8e1edccb3136e84899), [`6f503b7`](https://github.com/Effect-TS/effect/commit/6f503b774d893bf2af34f66202e270d8c45d5f31)]: + - effect@2.4.1 + - @effect/platform@0.46.3 + - @effect/platform-node-shared@0.2.3 + +## 0.32.9 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform@0.46.2 + - @effect/platform-node-shared@0.2.2 + +## 0.32.8 + +### Patch Changes + +- Updated dependencies [[`aa6556f`](https://github.com/Effect-TS/effect/commit/aa6556f007117caea84d6965aa30846a11879e9d)]: + - @effect/platform@0.46.1 + - @effect/platform-node-shared@0.2.1 + +## 0.32.7 + +### Patch Changes + +- Updated dependencies [[`5de7be5`](https://github.com/Effect-TS/effect/commit/5de7be5beca2e963b503e6029dcc3217848187d2), [`489fcf3`](https://github.com/Effect-TS/effect/commit/489fcf363ff2b2a953166b740cb9a62d7fc2a101), [`7d9c3bf`](https://github.com/Effect-TS/effect/commit/7d9c3bff6c18d451e0e4781042945ec5c7be1b9f), [`d8d278b`](https://github.com/Effect-TS/effect/commit/d8d278b2efb2966947029885e01f7b68348a021f), [`14c5711`](https://github.com/Effect-TS/effect/commit/14c57110078f0862b8da5c7a2c5d980f54447484), [`5de7be5`](https://github.com/Effect-TS/effect/commit/5de7be5beca2e963b503e6029dcc3217848187d2), [`b9cb3a9`](https://github.com/Effect-TS/effect/commit/b9cb3a9c9bfdd75536bd70b4e8b557c12d4923ff), [`585fcce`](https://github.com/Effect-TS/effect/commit/585fcce162d0f07a48d7cd984a9b722966fbebbe), [`93b412d`](https://github.com/Effect-TS/effect/commit/93b412d4a9ed762dc9fa5807e51fad0fc78a614a), [`55b26a6`](https://github.com/Effect-TS/effect/commit/55b26a6342b4826f1116e7a1eb660118c274458e), [`a025b12`](https://github.com/Effect-TS/effect/commit/a025b121235ba01cfce8d62a775491880c575561), [`2097739`](https://github.com/Effect-TS/effect/commit/20977393d2383bff709304e81ec7d51cafd57108)]: + - effect@2.4.0 + - @effect/platform@0.46.0 + - @effect/platform-node-shared@0.2.0 + +## 0.32.6 + +### Patch Changes + +- [#2187](https://github.com/Effect-TS/effect/pull/2187) [`e6d36c0`](https://github.com/Effect-TS/effect/commit/e6d36c0813d836f17eabb6a9c7849baffca12dbf) Thanks [@tim-smart](https://github.com/tim-smart)! - update development dependencies + +- Updated dependencies [[`5ad2eec`](https://github.com/Effect-TS/effect/commit/5ad2eece0280b6db6a749d25cac1dcf6d33659a9), [`e6d36c0`](https://github.com/Effect-TS/effect/commit/e6d36c0813d836f17eabb6a9c7849baffca12dbf)]: + - effect@2.3.8 + - @effect/platform-node-shared@0.1.14 + - @effect/platform@0.45.6 + +## 0.32.5 + +### Patch Changes + +- Updated dependencies [[`6daf084`](https://github.com/Effect-TS/effect/commit/6daf0845de008772011db8d7c75b7c37a6b4d334)]: + - @effect/platform@0.45.5 + - @effect/platform-node-shared@0.1.13 + +## 0.32.4 + +### Patch Changes + +- Updated dependencies [[`bc8404d`](https://github.com/Effect-TS/effect/commit/bc8404d54fd42072d200c0399cb39672837afa9f), [`abcb7d9`](https://github.com/Effect-TS/effect/commit/abcb7d983a4a85b43b7175e952f5b331b9019aea), [`2c5cbcd`](https://github.com/Effect-TS/effect/commit/2c5cbcd1161b4f40dab184999291e817314107de), [`6565916`](https://github.com/Effect-TS/effect/commit/6565916ef254bf910e47d25fd0ef55e7cb420241), [`abcb7d9`](https://github.com/Effect-TS/effect/commit/abcb7d983a4a85b43b7175e952f5b331b9019aea)]: + - effect@2.3.7 + - @effect/platform@0.45.4 + - @effect/platform-node-shared@0.1.12 + +## 0.32.3 + +### Patch Changes + +- Updated dependencies [[`09532a8`](https://github.com/Effect-TS/effect/commit/09532a86b7d0cc23557c89158f0342753dfce4b0)]: + - @effect/platform@0.45.3 + - @effect/platform-node-shared@0.1.11 + +## 0.32.2 + +### Patch Changes + +- Updated dependencies [[`b1163b2`](https://github.com/Effect-TS/effect/commit/b1163b2bd67b65bafbbb39fc4c67576e5cbaf444), [`b46b869`](https://github.com/Effect-TS/effect/commit/b46b869e59a6da5aa235a9fcc25e1e0d24e9e8f8), [`de1b226`](https://github.com/Effect-TS/effect/commit/de1b226282b5ab6c2809dd93f3bdb066f24a1333), [`a663390`](https://github.com/Effect-TS/effect/commit/a66339090ae7b960f8a8b90a0dcdc505de5aaf3e), [`ff88f80`](https://github.com/Effect-TS/effect/commit/ff88f808c4ed9947a148045849e7410b00acad0a), [`11be07b`](https://github.com/Effect-TS/effect/commit/11be07bf65d82cfdf994cdb9d8ca937f995cb4f0), [`c568645`](https://github.com/Effect-TS/effect/commit/c5686451c87d26382135a1c63b00ef171bb24f62), [`88835e5`](https://github.com/Effect-TS/effect/commit/88835e575a0bfbeff9a3696a332f32192c940e12), [`b415577`](https://github.com/Effect-TS/effect/commit/b415577f6c576073733929c858e5aac27b6d5880), [`ff8046f`](https://github.com/Effect-TS/effect/commit/ff8046f57dfd073eba60ce6d3144ab060fbf93ce), [`44c3b43`](https://github.com/Effect-TS/effect/commit/44c3b43653e64d7e425d39815d8ff405acec9b99)]: + - effect@2.3.6 + - @effect/platform@0.45.2 + - @effect/platform-node-shared@0.1.10 + +## 0.32.1 + +### Patch Changes + +- Updated dependencies [[`65895ab`](https://github.com/Effect-TS/effect/commit/65895ab982e0917ac92f0827e387e7cf61be1e69)]: + - @effect/platform@0.45.1 + - @effect/platform-node-shared@0.1.9 + +## 0.32.0 + +### Minor Changes + +- [#2119](https://github.com/Effect-TS/effect/pull/2119) [`2b62548`](https://github.com/Effect-TS/effect/commit/2b6254845882f399636d24223c483e5489e3cff4) Thanks [@tim-smart](https://github.com/tim-smart)! - add Scope to Http client + + This change adds a scope to the default http client, ensuring connections are + cleaned up if you abort the request at any point. + + Some response helpers have been added to reduce the noise. + + ```ts + import * as Http from "@effect/platform/HttpClient" + import { Effect } from "effect" + + // instead of + Http.request.get("/").pipe( + Http.client.fetchOk(), + Effect.flatMap((_) => _.json), + Effect.scoped + ) + + // you can do + Http.request.get("/").pipe(Http.client.fetchOk(), Http.response.json) + + // other helpers include + Http.response.text + Http.response.stream + Http.response.arrayBuffer + Http.response.urlParamsBody + Http.response.formData + Http.response.schema * Effect + ``` + +### Patch Changes + +- Updated dependencies [[`2b62548`](https://github.com/Effect-TS/effect/commit/2b6254845882f399636d24223c483e5489e3cff4)]: + - @effect/platform@0.45.0 + - @effect/platform-node-shared@0.1.8 + +## 0.31.7 + +### Patch Changes + +- Updated dependencies [[`b881365`](https://github.com/Effect-TS/effect/commit/b8813650355322ea2fc1fbaa4f846bd87a7a05f3)]: + - effect@2.3.5 + - @effect/platform@0.44.7 + - @effect/platform-node-shared@0.1.7 + +## 0.31.6 + +### Patch Changes + +- Updated dependencies [[`17bda66`](https://github.com/Effect-TS/effect/commit/17bda66431c999a546920c10adb205e6c8bea7d1)]: + - effect@2.3.4 + - @effect/platform@0.44.6 + - @effect/platform-node-shared@0.1.6 + +## 0.31.5 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform@0.44.5 + - @effect/platform-node-shared@0.1.5 + +## 0.31.4 + +### Patch Changes + +- Updated dependencies [[`efd41d8`](https://github.com/Effect-TS/effect/commit/efd41d8131c3d90867608969ef7c4eef490eb5e6), [`0f83515`](https://github.com/Effect-TS/effect/commit/0f83515a9c01d13c7c15a3f026e02d22c3c6bb7f), [`0f83515`](https://github.com/Effect-TS/effect/commit/0f83515a9c01d13c7c15a3f026e02d22c3c6bb7f)]: + - effect@2.3.3 + - @effect/platform@0.44.4 + - @effect/platform-node-shared@0.1.4 + +## 0.31.3 + +### Patch Changes + +- Updated dependencies [[`6654f5f`](https://github.com/Effect-TS/effect/commit/6654f5f0f6b9d97165ede5e04ca16776e2599328), [`2eb11b4`](https://github.com/Effect-TS/effect/commit/2eb11b47752cedf233ef4c4395d9c4efc9b9e180), [`56c09bd`](https://github.com/Effect-TS/effect/commit/56c09bd369279a6a7785209d172739935818cba6), [`71aa5b1`](https://github.com/Effect-TS/effect/commit/71aa5b1c180dcb8b53aefe232d12a97bd06b5447), [`1700af8`](https://github.com/Effect-TS/effect/commit/1700af8af1131602887da721914c8562b6342393)]: + - effect@2.3.2 + - @effect/platform@0.44.3 + - @effect/platform-node-shared@0.1.3 + +## 0.31.2 + +### Patch Changes + +- Updated dependencies [[`29739dd`](https://github.com/Effect-TS/effect/commit/29739dde8e6232824d49c4c7f8856de245249c5c)]: + - @effect/platform@0.44.2 + - @effect/platform-node-shared@0.1.2 + +## 0.31.1 + +### Patch Changes + +- Updated dependencies [[`b5a8215`](https://github.com/Effect-TS/effect/commit/b5a8215ee2a97a8865d69ee55ce1b9835948c922)]: + - effect@2.3.1 + - @effect/platform@0.44.1 + - @effect/platform-node-shared@0.1.1 + +## 0.31.0 + +### Minor Changes + +- [#2006](https://github.com/Effect-TS/effect/pull/2006) [`9a2d1c1`](https://github.com/Effect-TS/effect/commit/9a2d1c1468ea0789b34767ad683da074f061ea9c) Thanks [@github-actions](https://github.com/apps/github-actions)! - With this change we now require a string key to be provided for all tags and renames the dear old `Tag` to `GenericTag`, so when previously you could do: + + ```ts + import { Effect, Context } from "effect" + interface Service { + readonly _: unique symbol + } + const Service = Context.Tag< + Service, + { + number: Effect.Effect + } + >() + ``` + + you are now mandated to do: + + ```ts + import { Effect, Context } from "effect" + interface Service { + readonly _: unique symbol + } + const Service = Context.GenericTag< + Service, + { + number: Effect.Effect + } + >("Service") + ``` + + This makes by default all tags globals and ensures better debuggaility when unexpected errors arise. + + Furthermore we introduce a new way of constructing tags that should be considered the new default: + + ```ts + import { Effect, Context } from "effect" + class Service extends Context.Tag("Service")< + Service, + { + number: Effect.Effect + } + >() {} + + const program = Effect.flatMap(Service, ({ number }) => number).pipe( + Effect.flatMap((_) => Effect.log(`number: ${_}`)) + ) + ``` + + this will use "Service" as the key and will create automatically an opaque identifier (the class) to be used at the type level, it does something similar to the above in a single shot. + +- [#2006](https://github.com/Effect-TS/effect/pull/2006) [`1a77f72`](https://github.com/Effect-TS/effect/commit/1a77f72cdaf43d6cdc91b6060f82832edcdbbcb3) Thanks [@github-actions](https://github.com/apps/github-actions)! - change `Effect` type parameters order from `Effect` to `Effect` + +- [#2006](https://github.com/Effect-TS/effect/pull/2006) [`af47aa3`](https://github.com/Effect-TS/effect/commit/af47aa37196ad542c9c23a4896d8ef98147e1205) Thanks [@github-actions](https://github.com/apps/github-actions)! - move where platform worker spawn function is provided + + With this change, the point in which you provide the spawn function moves closer + to the edge, where you provide platform specific implementation. + + This seperates even more platform concerns from your business logic. Example: + + ```ts + import { Worker } from "@effect/platform" + import { BrowserWorker } from "@effect/platform-browser" + import { Effect } from "effect" + + Worker.makePool({ ... }).pipe( + Effect.provide(BrowserWorker.layer(() => new globalThis.Worker(...))) + ) + ``` + +- [#2006](https://github.com/Effect-TS/effect/pull/2006) [`a34dbdc`](https://github.com/Effect-TS/effect/commit/a34dbdc1552c73c1b612676f262a0c735ce444a7) Thanks [@github-actions](https://github.com/apps/github-actions)! - - Schema: change type parameters order from `Schema` to `Schema` + - Serializable: change type parameters order from `Serializable` to `Serializable` + - Class: change type parameters order from `Class` to `Class` + - PropertySignature: change type parameters order from `PropertySignature` to `PropertySignature` + +- [#2006](https://github.com/Effect-TS/effect/pull/2006) [`b1e2086`](https://github.com/Effect-TS/effect/commit/b1e2086ea8bf410e4ad75d71c0760825924e8f9f) Thanks [@github-actions](https://github.com/apps/github-actions)! - remove re-exports from platform packages + +- [#2006](https://github.com/Effect-TS/effect/pull/2006) [`9a2d1c1`](https://github.com/Effect-TS/effect/commit/9a2d1c1468ea0789b34767ad683da074f061ea9c) Thanks [@github-actions](https://github.com/apps/github-actions)! - This change enables `Effect.serviceConstants` and `Effect.serviceMembers` to access any constant in the service, not only the effects, namely it is now possible to do: + + ```ts + import { Effect, Context } from "effect" + + class NumberRepo extends Context.TagClass("NumberRepo")< + NumberRepo, + { + readonly numbers: Array + } + >() { + static numbers = Effect.serviceConstants(NumberRepo).numbers + } + ``` + +### Patch Changes + +- Updated dependencies [[`96bcee2`](https://github.com/Effect-TS/effect/commit/96bcee21021aecd8ffd86440a2c9be353c4668e3), [`b1e2086`](https://github.com/Effect-TS/effect/commit/b1e2086ea8bf410e4ad75d71c0760825924e8f9f), [`96bcee2`](https://github.com/Effect-TS/effect/commit/96bcee21021aecd8ffd86440a2c9be353c4668e3), [`c77f635`](https://github.com/Effect-TS/effect/commit/c77f635f8a26ca6d83cb569d911f8eee79033fd9), [`e343a74`](https://github.com/Effect-TS/effect/commit/e343a74843dd9edf879417fa94cb51de7ed5b402), [`acf1894`](https://github.com/Effect-TS/effect/commit/acf1894f45945dbe5c39451e36aabb4b5092f257), [`9a2d1c1`](https://github.com/Effect-TS/effect/commit/9a2d1c1468ea0789b34767ad683da074f061ea9c), [`1a77f72`](https://github.com/Effect-TS/effect/commit/1a77f72cdaf43d6cdc91b6060f82832edcdbbcb3), [`af47aa3`](https://github.com/Effect-TS/effect/commit/af47aa37196ad542c9c23a4896d8ef98147e1205), [`c986f0e`](https://github.com/Effect-TS/effect/commit/c986f0e0ce4d22ba08177ed351152718479ab63c), [`96bcee2`](https://github.com/Effect-TS/effect/commit/96bcee21021aecd8ffd86440a2c9be353c4668e3), [`70dde23`](https://github.com/Effect-TS/effect/commit/70dde238f81125e353fd7bde5fc24ecd8969bf97), [`a34dbdc`](https://github.com/Effect-TS/effect/commit/a34dbdc1552c73c1b612676f262a0c735ce444a7), [`81b7425`](https://github.com/Effect-TS/effect/commit/81b7425320cbbe2a6cf547a3e3ab3549cdba14cf), [`b1e2086`](https://github.com/Effect-TS/effect/commit/b1e2086ea8bf410e4ad75d71c0760825924e8f9f), [`02c3461`](https://github.com/Effect-TS/effect/commit/02c34615d02f91269ea04036d0306fccf4e39e18), [`0e56e99`](https://github.com/Effect-TS/effect/commit/0e56e998ab9815c4d096c239a553cb86a0f99af9), [`8b0ded9`](https://github.com/Effect-TS/effect/commit/8b0ded9f10ba0d96fcb9af24eff2dbd9341f85e3), [`8dd83e8`](https://github.com/Effect-TS/effect/commit/8dd83e854bfcaa6dab876994c5f813dcfb486c28), [`5127afe`](https://github.com/Effect-TS/effect/commit/5127afec1c519e0a3d7460844a9101a96272f29e), [`d75f6fe`](https://github.com/Effect-TS/effect/commit/d75f6fe6499deb0a5ee9ec94af3b5fd4eb03a2d0), [`7356e5c`](https://github.com/Effect-TS/effect/commit/7356e5cc16e9d70f18c02dee1dcb4ad539fd130a), [`3077cde`](https://github.com/Effect-TS/effect/commit/3077cde08a60246821a940964a84dd7f7c8b9f54), [`be19ce0`](https://github.com/Effect-TS/effect/commit/be19ce0b8bdf1fac80bb8d7e0b06a86986b47409), [`4a5d01a`](https://github.com/Effect-TS/effect/commit/4a5d01a409e9b6dd53893e65f8e5c9247f568021), [`78f47ab`](https://github.com/Effect-TS/effect/commit/78f47abfe3cb0a8bbde818b1c5fc603270538b47), [`6361ee2`](https://github.com/Effect-TS/effect/commit/6361ee2e83bdfead24045c3d058a7298efc18113), [`52e5d20`](https://github.com/Effect-TS/effect/commit/52e5d2077582bf51f25861c7139fc920c2c24166), [`c6137ec`](https://github.com/Effect-TS/effect/commit/c6137ec62c6b5542d5062ae1a3c936cb915dee22), [`86f665d`](https://github.com/Effect-TS/effect/commit/86f665d7bd25ba0a3f046a2384798378310dcf0c), [`f5ae081`](https://github.com/Effect-TS/effect/commit/f5ae08195e68e76faeac258c565d79da4e01e7d6), [`4a5d01a`](https://github.com/Effect-TS/effect/commit/4a5d01a409e9b6dd53893e65f8e5c9247f568021), [`60686f5`](https://github.com/Effect-TS/effect/commit/60686f5c38bef1b93a3a0dda9b6596d46aceab03), [`9a2d1c1`](https://github.com/Effect-TS/effect/commit/9a2d1c1468ea0789b34767ad683da074f061ea9c), [`5127afe`](https://github.com/Effect-TS/effect/commit/5127afec1c519e0a3d7460844a9101a96272f29e), [`8ee2931`](https://github.com/Effect-TS/effect/commit/8ee293159b4f7cb7af8558287a0a047f3a69743d), [`6727474`](https://github.com/Effect-TS/effect/commit/672747497490a30d36dd49c06db19aabf09dc7f0), [`5127afe`](https://github.com/Effect-TS/effect/commit/5127afec1c519e0a3d7460844a9101a96272f29e)]: + - effect@2.3.0 + - @effect/platform@0.44.0 + - @effect/platform-node-shared@0.1.0 + +## 0.30.11 + +### Patch Changes + +- Updated dependencies [[`3ddfdbf`](https://github.com/Effect-TS/effect/commit/3ddfdbf914edea536aef207cec6695f33496258c), [`3ddfdbf`](https://github.com/Effect-TS/effect/commit/3ddfdbf914edea536aef207cec6695f33496258c)]: + - effect@2.2.5 + - @effect/platform@0.43.11 + - @effect/platform-node@0.42.11 + +## 0.30.10 + +### Patch Changes + +- Updated dependencies [[`d0b911c`](https://github.com/Effect-TS/effect/commit/d0b911c75f284c7aa87f25aa96926e6bde7690d0), [`330e1a4`](https://github.com/Effect-TS/effect/commit/330e1a4e2c1fc0af6c80c80c81dd38c3e50fab78), [`6928a2b`](https://github.com/Effect-TS/effect/commit/6928a2b0bae86a4bdfbece0aa32924207c2d5a70), [`296bc1c`](https://github.com/Effect-TS/effect/commit/296bc1c9d24986d299d2669115d584cb27b73c60)]: + - effect@2.2.4 + - @effect/platform@0.43.10 + - @effect/platform-node@0.42.10 + +## 0.30.9 + +### Patch Changes + +- Updated dependencies [[`1b841a9`](https://github.com/Effect-TS/effect/commit/1b841a91fed86825cd2867cf1e68e41d8ff26b4e)]: + - @effect/platform@0.43.9 + - @effect/platform-node@0.42.9 + +## 0.30.8 + +### Patch Changes + +- Updated dependencies [[`32bf796`](https://github.com/Effect-TS/effect/commit/32bf796c3e5db1b2b68e8b1b20db664295991643)]: + - @effect/platform@0.43.8 + - @effect/platform-node@0.42.8 + +## 0.30.7 + +### Patch Changes + +- Updated dependencies [[`cde08f3`](https://github.com/Effect-TS/effect/commit/cde08f354ed2ff2921d1d98bd539c7d65a2ddd73)]: + - @effect/platform@0.43.7 + - @effect/platform-node@0.42.7 + +## 0.30.6 + +### Patch Changes + +- Updated dependencies [[`c96bb17`](https://github.com/Effect-TS/effect/commit/c96bb17043e2cec1eaeb319614a4c2904d876beb)]: + - @effect/platform@0.43.6 + - @effect/platform-node@0.42.6 + +## 0.30.5 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform@0.43.5 + - @effect/platform-node@0.42.5 + +## 0.30.4 + +### Patch Changes + +- [#1999](https://github.com/Effect-TS/effect/pull/1999) [`78f5921`](https://github.com/Effect-TS/effect/commit/78f59211502ded6fcbe15a49d6fde941cccc9d52) Thanks [@tim-smart](https://github.com/tim-smart)! - ensure forked fibers are interruptible + +- Updated dependencies [[`22794e0`](https://github.com/Effect-TS/effect/commit/22794e0ba00e40281f30a22fa84412003c24877d), [`f73e6c0`](https://github.com/Effect-TS/effect/commit/f73e6c033fb0729a9cfa5eb4bc39f79d3126e247), [`78f5921`](https://github.com/Effect-TS/effect/commit/78f59211502ded6fcbe15a49d6fde941cccc9d52)]: + - effect@2.2.3 + - @effect/platform-node@0.42.4 + - @effect/platform@0.43.4 + +## 0.30.3 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform@0.43.3 + - @effect/platform-node@0.42.3 + +## 0.30.2 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform@0.43.2 + - @effect/platform-node@0.42.2 + +## 0.30.1 + +### Patch Changes + +- Updated dependencies [[`d404561`](https://github.com/Effect-TS/effect/commit/d404561e47ec2fa5f68709a308ee5d2ee959141d), [`7b84a3c`](https://github.com/Effect-TS/effect/commit/7b84a3c7e4b9c8dc02294b0e3cc3ae3becea977b)]: + - effect@2.2.2 + - @effect/platform@0.43.1 + - @effect/platform-node@0.42.1 + +## 0.30.0 + +### Minor Changes + +- [#1922](https://github.com/Effect-TS/effect/pull/1922) [`62b40e8`](https://github.com/Effect-TS/effect/commit/62b40e8479371d6663c0255aaca56a1ae0d59764) Thanks [@gcanti](https://github.com/gcanti)! - add context tracking to Schema, closes #1873 + +### Patch Changes + +- Updated dependencies [[`84da31f`](https://github.com/Effect-TS/effect/commit/84da31f0643e8651b9d311b30526b1e4edfbdfb8), [`645bea2`](https://github.com/Effect-TS/effect/commit/645bea2551129f94a5b0e38347e28067dee531bb), [`62b40e8`](https://github.com/Effect-TS/effect/commit/62b40e8479371d6663c0255aaca56a1ae0d59764)]: + - effect@2.2.1 + - @effect/platform-node@0.42.0 + - @effect/platform@0.43.0 + +## 0.29.8 + +### Patch Changes + +- Updated dependencies [[`202befc`](https://github.com/Effect-TS/effect/commit/202befc2ecbeb117c4fa85ef9b12a3d3a48273d2), [`fe05ad7`](https://github.com/Effect-TS/effect/commit/fe05ad7bcb3b88d47800ab69ebf53641023676f1), [`ee4ff8a`](https://github.com/Effect-TS/effect/commit/ee4ff8a943141fcf2877af92c5877ee87a989fb9), [`ee4ff8a`](https://github.com/Effect-TS/effect/commit/ee4ff8a943141fcf2877af92c5877ee87a989fb9), [`ee4ff8a`](https://github.com/Effect-TS/effect/commit/ee4ff8a943141fcf2877af92c5877ee87a989fb9), [`ee4ff8a`](https://github.com/Effect-TS/effect/commit/ee4ff8a943141fcf2877af92c5877ee87a989fb9), [`10df798`](https://github.com/Effect-TS/effect/commit/10df798639e556f9d88265ef7fc3cf8a3bbe3874)]: + - effect@2.2.0 + - @effect/platform@0.42.7 + - @effect/platform-node@0.41.8 + +## 0.29.7 + +### Patch Changes + +- Updated dependencies [[`21b9edd`](https://github.com/Effect-TS/effect/commit/21b9edde464f7c5624ef54ad1b5e264204a37625)]: + - effect@2.1.2 + - @effect/platform@0.42.6 + - @effect/platform-node@0.41.7 + +## 0.29.6 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform@0.42.5 + - @effect/platform-node@0.41.6 + +## 0.29.5 + +### Patch Changes + +- Updated dependencies [[`a222524`](https://github.com/Effect-TS/effect/commit/a2225247e9de2e013d287320790fde88c081dbbd)]: + - effect@2.1.1 + - @effect/platform@0.42.4 + - @effect/platform-node@0.41.5 + +## 0.29.4 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform@0.42.3 + - @effect/platform-node@0.41.4 + +## 0.29.3 + +### Patch Changes + +- Updated dependencies [[`05c44b3`](https://github.com/Effect-TS/effect/commit/05c44b30662554dde50b70bad79f13ae895fda02), [`05c44b3`](https://github.com/Effect-TS/effect/commit/05c44b30662554dde50b70bad79f13ae895fda02), [`05c44b3`](https://github.com/Effect-TS/effect/commit/05c44b30662554dde50b70bad79f13ae895fda02), [`05c44b3`](https://github.com/Effect-TS/effect/commit/05c44b30662554dde50b70bad79f13ae895fda02), [`05c44b3`](https://github.com/Effect-TS/effect/commit/05c44b30662554dde50b70bad79f13ae895fda02), [`05c44b3`](https://github.com/Effect-TS/effect/commit/05c44b30662554dde50b70bad79f13ae895fda02)]: + - effect@2.1.0 + - @effect/platform@0.42.2 + - @effect/platform-node@0.41.3 + +## 0.29.2 + +### Patch Changes + +- Updated dependencies [[`f7f19f6`](https://github.com/Effect-TS/effect/commit/f7f19f66a5fa349baa2412c1f9f15111c437df09)]: + - effect@2.0.5 + - @effect/platform@0.42.1 + - @effect/platform-node@0.41.2 + +## 0.29.1 + +### Patch Changes + +- Updated dependencies [[`536c1df`](https://github.com/Effect-TS/effect/commit/536c1dfb7833961dfb2fbd6bcd2dbdfa2f208d51), [`8eec87e`](https://github.com/Effect-TS/effect/commit/8eec87e311ce55281a98517e6df0ef103b43e8a8), [`540b294`](https://github.com/Effect-TS/effect/commit/540b2941dd0a81e9688311583ce7e2e140d6e7a5), [`536c1df`](https://github.com/Effect-TS/effect/commit/536c1dfb7833961dfb2fbd6bcd2dbdfa2f208d51), [`a3f96d6`](https://github.com/Effect-TS/effect/commit/a3f96d615b8b3e238dbfa01ef713c87e6f4532be), [`71ed54c`](https://github.com/Effect-TS/effect/commit/71ed54c3fbb1ead5da2776bc6207050cb073ada4), [`8eec87e`](https://github.com/Effect-TS/effect/commit/8eec87e311ce55281a98517e6df0ef103b43e8a8), [`536c1df`](https://github.com/Effect-TS/effect/commit/536c1dfb7833961dfb2fbd6bcd2dbdfa2f208d51), [`8eec87e`](https://github.com/Effect-TS/effect/commit/8eec87e311ce55281a98517e6df0ef103b43e8a8), [`8eec87e`](https://github.com/Effect-TS/effect/commit/8eec87e311ce55281a98517e6df0ef103b43e8a8), [`25adce7`](https://github.com/Effect-TS/effect/commit/25adce7ae76ce834096dca1ed70a60ad1a349217), [`536c1df`](https://github.com/Effect-TS/effect/commit/536c1dfb7833961dfb2fbd6bcd2dbdfa2f208d51), [`48a3d40`](https://github.com/Effect-TS/effect/commit/48a3d40aed0f923f567b8911dade732ff472d981)]: + - effect@2.0.4 + - @effect/platform-node@0.41.1 + - @effect/platform@0.42.0 + +## 0.29.0 + +### Minor Changes + +- [#1885](https://github.com/Effect-TS/effect/pull/1885) [`1d3a06b`](https://github.com/Effect-TS/effect/commit/1d3a06bb58ad1ac123ae8f9d42b4345f9c9c53c0) Thanks [@mikearnaldi](https://github.com/mikearnaldi)! - lift worker shutdown to /platform implementation + +### Patch Changes + +- [#1885](https://github.com/Effect-TS/effect/pull/1885) [`1d3a06b`](https://github.com/Effect-TS/effect/commit/1d3a06bb58ad1ac123ae8f9d42b4345f9c9c53c0) Thanks [@mikearnaldi](https://github.com/mikearnaldi)! - Avoid killing all fibers on interrupt + +- Updated dependencies [[`87f7ef2`](https://github.com/Effect-TS/effect/commit/87f7ef28a3c27e2e4f2fcfa465f85bb2a45a3d6b), [`1d3a06b`](https://github.com/Effect-TS/effect/commit/1d3a06bb58ad1ac123ae8f9d42b4345f9c9c53c0), [`1d3a06b`](https://github.com/Effect-TS/effect/commit/1d3a06bb58ad1ac123ae8f9d42b4345f9c9c53c0)]: + - effect@2.0.3 + - @effect/platform-node@0.41.0 + - @effect/platform@0.41.0 + +## 0.28.4 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform@0.40.4 + - @effect/platform-node@0.40.4 + +## 0.28.3 + +### Patch Changes + +- [#1879](https://github.com/Effect-TS/effect/pull/1879) [`92c0322`](https://github.com/Effect-TS/effect/commit/92c0322a58bf7e5b8dbb602186030839e89df5af) Thanks [@tim-smart](https://github.com/tim-smart)! - add http Multiplex module + +- Updated dependencies [[`92c0322`](https://github.com/Effect-TS/effect/commit/92c0322a58bf7e5b8dbb602186030839e89df5af)]: + - @effect/platform-node@0.40.3 + - @effect/platform@0.40.3 + +## 0.28.2 + +### Patch Changes + +- Updated dependencies [[`4c90c54`](https://github.com/Effect-TS/effect/commit/4c90c54d87c91f75f3ad114926cdf3b0c25df091), [`d5a1949`](https://github.com/Effect-TS/effect/commit/d5a19499aac7c1d147674a35ac69992177c7536c), [`d3d3bda`](https://github.com/Effect-TS/effect/commit/d3d3bda74c794153def9027e0c40896e72cd5d14)]: + - @effect/platform@0.40.2 + - effect@2.0.2 + - @effect/platform-node@0.40.2 + +## 0.28.1 + +### Patch Changes + +- Updated dependencies [[`16bd87d`](https://github.com/Effect-TS/effect/commit/16bd87d32611b966dc42ea4fc979764f97a49071)]: + - effect@2.0.1 + - @effect/platform@0.40.1 + - @effect/platform-node@0.40.1 + +## 0.28.0 + +### Minor Changes + +- [`d0471ca`](https://github.com/Effect-TS/effect/commit/d0471ca7b544746674b9e1750202da72b0a21233) Thanks [@mikearnaldi](https://github.com/mikearnaldi)! - Switch to monorepo structure + +- [#1846](https://github.com/Effect-TS/effect/pull/1846) [`693b8f3`](https://github.com/Effect-TS/effect/commit/693b8f3a3dfd43ae61f0d9292cdf356be7329f2f) Thanks [@fubhy](https://github.com/fubhy)! - Enabled `exactOptionalPropertyTypes` throughout + +### Patch Changes + +- [`d987daa`](https://github.com/Effect-TS/effect/commit/d987daafaddd43b6ade74916a08236c19ea0a9fa) Thanks [@mikearnaldi](https://github.com/mikearnaldi)! - Switch effect dependency to caret + +- Updated dependencies [[`d987daa`](https://github.com/Effect-TS/effect/commit/d987daafaddd43b6ade74916a08236c19ea0a9fa), [`7b5eaa3`](https://github.com/Effect-TS/effect/commit/7b5eaa3838c79bf4bdccf91b94d61bbc38a2ec95), [`0724211`](https://github.com/Effect-TS/effect/commit/072421149c36010748ff6b6ee19c15c6cffefe09), [`9f2bc5a`](https://github.com/Effect-TS/effect/commit/9f2bc5a19e0b678a0a85e84daac290922b0fd57d), [`04fb8b4`](https://github.com/Effect-TS/effect/commit/04fb8b428b19bba85a2c79910c5e363340d074e7), [`d0471ca`](https://github.com/Effect-TS/effect/commit/d0471ca7b544746674b9e1750202da72b0a21233), [`bcf0900`](https://github.com/Effect-TS/effect/commit/bcf0900b58f449262556f80bff21e771a37272aa), [`99d22cb`](https://github.com/Effect-TS/effect/commit/99d22cbee13cc2111a4a634cbe73b9b7d7fd88c7), [`6299b84`](https://github.com/Effect-TS/effect/commit/6299b84c11e5d1fe79fa538df8935018c7613747), [`c0aeb5e`](https://github.com/Effect-TS/effect/commit/c0aeb5e302869bcd7d7627f8cc5b630d07c12d10), [`693b8f3`](https://github.com/Effect-TS/effect/commit/693b8f3a3dfd43ae61f0d9292cdf356be7329f2f)]: + - @effect/platform-node@0.40.0 + - @effect/platform@0.40.0 + - effect@2.0.0 + +## 0.27.0 + +### Minor Changes + +- [#369](https://github.com/Effect-TS/platform/pull/369) [`5d5f62b`](https://github.com/Effect-TS/platform/commit/5d5f62b03ffdbca0a986d968e1dbb45886dfa827) Thanks [@tim-smart](https://github.com/tim-smart)! - rename server FormData module to Multipart + +- [#372](https://github.com/Effect-TS/platform/pull/372) [`15784c9`](https://github.com/Effect-TS/platform/commit/15784c920dcae40f328bb45ac850987135207365) Thanks [@tim-smart](https://github.com/tim-smart)! - update dependencies + +- [#373](https://github.com/Effect-TS/platform/pull/373) [`b042ba5`](https://github.com/Effect-TS/platform/commit/b042ba5ae78a1eed592e543c233fe3040d6a60da) Thanks [@tim-smart](https://github.com/tim-smart)! - update effect + +### Patch Changes + +- Updated dependencies [[`5d5f62b`](https://github.com/Effect-TS/platform/commit/5d5f62b03ffdbca0a986d968e1dbb45886dfa827), [`15784c9`](https://github.com/Effect-TS/platform/commit/15784c920dcae40f328bb45ac850987135207365), [`b042ba5`](https://github.com/Effect-TS/platform/commit/b042ba5ae78a1eed592e543c233fe3040d6a60da), [`49fb154`](https://github.com/Effect-TS/platform/commit/49fb15439f18701321db8ded839243b9dd8de71a)]: + - @effect/platform-node@0.39.0 + - @effect/platform@0.39.0 + +## 0.26.0 + +### Minor Changes + +- [#367](https://github.com/Effect-TS/platform/pull/367) [`7d1584b`](https://github.com/Effect-TS/platform/commit/7d1584b23d464651c206201ff304c6eb4bebfc3a) Thanks [@tim-smart](https://github.com/tim-smart)! - update effect + +### Patch Changes + +- Updated dependencies [[`7d1584b`](https://github.com/Effect-TS/platform/commit/7d1584b23d464651c206201ff304c6eb4bebfc3a)]: + - @effect/platform-node@0.38.0 + - @effect/platform@0.38.0 + +## 0.25.10 + +### Patch Changes + +- [#366](https://github.com/Effect-TS/platform/pull/366) [`1d6bf73`](https://github.com/Effect-TS/platform/commit/1d6bf730dad0a6bbb282f436ec7d5870de76ca3a) Thanks [@tim-smart](https://github.com/tim-smart)! - add Scope to every http request + +- [#365](https://github.com/Effect-TS/platform/pull/365) [`3351136`](https://github.com/Effect-TS/platform/commit/335113601c238104eb2e331d26b5e463bde80dff) Thanks [@tim-smart](https://github.com/tim-smart)! - respond with 503 on server induced interrupt + +- Updated dependencies [[`e2c545a`](https://github.com/Effect-TS/platform/commit/e2c545a328c2bccbba661540a8835b10bce4b438), [`1d6bf73`](https://github.com/Effect-TS/platform/commit/1d6bf730dad0a6bbb282f436ec7d5870de76ca3a), [`3351136`](https://github.com/Effect-TS/platform/commit/335113601c238104eb2e331d26b5e463bde80dff)]: + - @effect/platform@0.37.8 + - @effect/platform-node@0.37.10 + +## 0.25.9 + +### Patch Changes + +- Updated dependencies [[`df3af6b`](https://github.com/Effect-TS/platform/commit/df3af6be61572bab15004bbca2c5739d8206f3c3)]: + - @effect/platform@0.37.7 + - @effect/platform-node@0.37.9 + +## 0.25.8 + +### Patch Changes + +- Updated dependencies [[`6dbc587`](https://github.com/Effect-TS/platform/commit/6dbc587868d2703ad9a4c9995cb9dacdfc29c364), [`6dbc587`](https://github.com/Effect-TS/platform/commit/6dbc587868d2703ad9a4c9995cb9dacdfc29c364)]: + - @effect/platform-node@0.37.8 + - @effect/platform@0.37.6 + +## 0.25.7 + +### Patch Changes + +- [#357](https://github.com/Effect-TS/platform/pull/357) [`6db1c07`](https://github.com/Effect-TS/platform/commit/6db1c0768d8afd8a45c0af31cbdfc40c9319e48b) Thanks [@tim-smart](https://github.com/tim-smart)! - respond witu 499 on interrupt + +- Updated dependencies [[`6db1c07`](https://github.com/Effect-TS/platform/commit/6db1c0768d8afd8a45c0af31cbdfc40c9319e48b)]: + - @effect/platform-node@0.37.7 + +## 0.25.6 + +### Patch Changes + +- [#354](https://github.com/Effect-TS/platform/pull/354) [`190bc84`](https://github.com/Effect-TS/platform/commit/190bc84b137a729a38b6812e220085b3d12cb124) Thanks [@tim-smart](https://github.com/tim-smart)! - add Layer support to SerializedWorker + +- Updated dependencies [[`190bc84`](https://github.com/Effect-TS/platform/commit/190bc84b137a729a38b6812e220085b3d12cb124)]: + - @effect/platform-node@0.37.6 + - @effect/platform@0.37.5 + +## 0.25.5 + +### Patch Changes + +- [#352](https://github.com/Effect-TS/platform/pull/352) [`1c02a35`](https://github.com/Effect-TS/platform/commit/1c02a35df2f34601b547e17ddeab98236e10f77d) Thanks [@tim-smart](https://github.com/tim-smart)! - interrupt all fibers on worker interrupt + +- Updated dependencies [[`1c02a35`](https://github.com/Effect-TS/platform/commit/1c02a35df2f34601b547e17ddeab98236e10f77d), [`1c02a35`](https://github.com/Effect-TS/platform/commit/1c02a35df2f34601b547e17ddeab98236e10f77d)]: + - @effect/platform-node@0.37.5 + - @effect/platform@0.37.4 + +## 0.25.4 + +### Patch Changes + +- [#350](https://github.com/Effect-TS/platform/pull/350) [`b30e5e3`](https://github.com/Effect-TS/platform/commit/b30e5e3874f22037f92253037fff6952f537ee40) Thanks [@tim-smart](https://github.com/tim-smart)! - add decode option to worker runner + +- Updated dependencies [[`b30e5e3`](https://github.com/Effect-TS/platform/commit/b30e5e3874f22037f92253037fff6952f537ee40)]: + - @effect/platform-node@0.37.4 + - @effect/platform@0.37.3 + +## 0.25.3 + +### Patch Changes + +- [#348](https://github.com/Effect-TS/platform/pull/348) [`28edc60`](https://github.com/Effect-TS/platform/commit/28edc60d2fcd30160529c677a9ffd786775e534b) Thanks [@tim-smart](https://github.com/tim-smart)! - add layer worker runner apis + +- Updated dependencies [[`28edc60`](https://github.com/Effect-TS/platform/commit/28edc60d2fcd30160529c677a9ffd786775e534b)]: + - @effect/platform-node@0.37.3 + - @effect/platform@0.37.2 + +## 0.25.2 + +### Patch Changes + +- Updated dependencies [[`c0fdc3d`](https://github.com/Effect-TS/platform/commit/c0fdc3df8d8fc057fc388f5cb1a17d707d54f3eb)]: + - @effect/platform-node@0.37.2 + +## 0.25.1 + +### Patch Changes + +- [#344](https://github.com/Effect-TS/platform/pull/344) [`5b7cdbd`](https://github.com/Effect-TS/platform/commit/5b7cdbdf8ded48903a9f39df800fd7a22f73f0f7) Thanks [@tim-smart](https://github.com/tim-smart)! - support error and output transfers in worker runners + +- [#344](https://github.com/Effect-TS/platform/pull/344) [`5b7cdbd`](https://github.com/Effect-TS/platform/commit/5b7cdbdf8ded48903a9f39df800fd7a22f73f0f7) Thanks [@tim-smart](https://github.com/tim-smart)! - support initialMessage in workers + +- [#344](https://github.com/Effect-TS/platform/pull/344) [`5b7cdbd`](https://github.com/Effect-TS/platform/commit/5b7cdbdf8ded48903a9f39df800fd7a22f73f0f7) Thanks [@tim-smart](https://github.com/tim-smart)! - add Schema transforms to Transferable + +- [#344](https://github.com/Effect-TS/platform/pull/344) [`5b7cdbd`](https://github.com/Effect-TS/platform/commit/5b7cdbdf8ded48903a9f39df800fd7a22f73f0f7) Thanks [@tim-smart](https://github.com/tim-smart)! - make worker encoding return Effects + +- Updated dependencies [[`5b7cdbd`](https://github.com/Effect-TS/platform/commit/5b7cdbdf8ded48903a9f39df800fd7a22f73f0f7), [`5b7cdbd`](https://github.com/Effect-TS/platform/commit/5b7cdbdf8ded48903a9f39df800fd7a22f73f0f7), [`5b7cdbd`](https://github.com/Effect-TS/platform/commit/5b7cdbdf8ded48903a9f39df800fd7a22f73f0f7), [`5b7cdbd`](https://github.com/Effect-TS/platform/commit/5b7cdbdf8ded48903a9f39df800fd7a22f73f0f7)]: + - @effect/platform-node@0.37.1 + - @effect/platform@0.37.1 + +## 0.25.0 + +### Minor Changes + +- [#341](https://github.com/Effect-TS/platform/pull/341) [`649f57f`](https://github.com/Effect-TS/platform/commit/649f57fdf557eed5f8405a4a4553dfc47fd8d4b1) Thanks [@tim-smart](https://github.com/tim-smart)! - use peer deps for /platform-\* + +### Patch Changes + +- Updated dependencies [[`649f57f`](https://github.com/Effect-TS/platform/commit/649f57fdf557eed5f8405a4a4553dfc47fd8d4b1), [`649f57f`](https://github.com/Effect-TS/platform/commit/649f57fdf557eed5f8405a4a4553dfc47fd8d4b1)]: + - @effect/platform-node@0.37.0 + - @effect/platform@0.37.0 + +## 0.24.0 + +### Minor Changes + +- [#338](https://github.com/Effect-TS/platform/pull/338) [`7eaa8e5`](https://github.com/Effect-TS/platform/commit/7eaa8e52b18d408688e7b4909bcf016b0c04e80a) Thanks [@tim-smart](https://github.com/tim-smart)! - change http serve api to return immediately + +- [#338](https://github.com/Effect-TS/platform/pull/338) [`7eaa8e5`](https://github.com/Effect-TS/platform/commit/7eaa8e52b18d408688e7b4909bcf016b0c04e80a) Thanks [@tim-smart](https://github.com/tim-smart)! - Http.server.serve now returns a Layer + +### Patch Changes + +- [#338](https://github.com/Effect-TS/platform/pull/338) [`7eaa8e5`](https://github.com/Effect-TS/platform/commit/7eaa8e52b18d408688e7b4909bcf016b0c04e80a) Thanks [@tim-smart](https://github.com/tim-smart)! - add Http.server.serveEffect + +- Updated dependencies [[`7eaa8e5`](https://github.com/Effect-TS/platform/commit/7eaa8e52b18d408688e7b4909bcf016b0c04e80a), [`7eaa8e5`](https://github.com/Effect-TS/platform/commit/7eaa8e52b18d408688e7b4909bcf016b0c04e80a), [`7eaa8e5`](https://github.com/Effect-TS/platform/commit/7eaa8e52b18d408688e7b4909bcf016b0c04e80a)]: + - @effect/platform-node@0.36.0 + - @effect/platform@0.36.0 + +## 0.23.1 + +### Patch Changes + +- [#335](https://github.com/Effect-TS/platform/pull/335) [`4f0166e`](https://github.com/Effect-TS/platform/commit/4f0166ee2241bd9b71739c98d428b5809313e46e) Thanks [@tim-smart](https://github.com/tim-smart)! - add SerializedWorker + +- Updated dependencies [[`4f0166e`](https://github.com/Effect-TS/platform/commit/4f0166ee2241bd9b71739c98d428b5809313e46e), [`4f0166e`](https://github.com/Effect-TS/platform/commit/4f0166ee2241bd9b71739c98d428b5809313e46e)]: + - @effect/platform-node@0.35.1 + - @effect/platform@0.35.0 + +## 0.23.0 + +### Minor Changes + +- [#331](https://github.com/Effect-TS/platform/pull/331) [`db1ca18`](https://github.com/Effect-TS/platform/commit/db1ca18725f9dd4be1c36ddc80faa3aa53c10eb7) Thanks [@tim-smart](https://github.com/tim-smart)! - update effect + +### Patch Changes + +- Updated dependencies [[`db1ca18`](https://github.com/Effect-TS/platform/commit/db1ca18725f9dd4be1c36ddc80faa3aa53c10eb7)]: + - @effect/platform-node@0.35.0 + - @effect/platform@0.34.0 + +## 0.22.3 + +### Patch Changes + +- Updated dependencies [[`5c75749`](https://github.com/Effect-TS/platform/commit/5c75749d451f8e79e1cb8057729691e4b3c1c6aa)]: + - @effect/platform-node@0.34.3 + +## 0.22.2 + +### Patch Changes + +- Updated dependencies [[`162aa91`](https://github.com/Effect-TS/platform/commit/162aa915934112983c543a6be2a9d7091b86fac9)]: + - @effect/platform@0.33.1 + - @effect/platform-node@0.34.2 + +## 0.22.1 + +### Patch Changes + +- [#324](https://github.com/Effect-TS/platform/pull/324) [`6b90c81`](https://github.com/Effect-TS/platform/commit/6b90c81391e613a25db564aebb9a64971ce077a5) Thanks [@tim-smart](https://github.com/tim-smart)! - improve serve api + +- Updated dependencies [[`6b90c81`](https://github.com/Effect-TS/platform/commit/6b90c81391e613a25db564aebb9a64971ce077a5)]: + - @effect/platform-node@0.34.1 + +## 0.22.0 + +### Minor Changes + +- [#321](https://github.com/Effect-TS/platform/pull/321) [`16a5bca`](https://github.com/Effect-TS/platform/commit/16a5bca2bd4aed570ce95233a0e47350010d031f) Thanks [@tim-smart](https://github.com/tim-smart)! - update effect + +### Patch Changes + +- Updated dependencies [[`425365e`](https://github.com/Effect-TS/platform/commit/425365ebc40c52a6e2a4bff865c3a982ce74f4ed), [`425365e`](https://github.com/Effect-TS/platform/commit/425365ebc40c52a6e2a4bff865c3a982ce74f4ed), [`16a5bca`](https://github.com/Effect-TS/platform/commit/16a5bca2bd4aed570ce95233a0e47350010d031f)]: + - @effect/platform-node@0.34.0 + - @effect/platform@0.33.0 + +## 0.21.5 + +### Patch Changes + +- Updated dependencies [[`19431f0`](https://github.com/Effect-TS/platform/commit/19431f0b5ccb8beacd502de876962f55cabf6ed4)]: + - @effect/platform-node@0.33.5 + +## 0.21.4 + +### Patch Changes + +- Updated dependencies [[`e63cf81`](https://github.com/Effect-TS/platform/commit/e63cf819dc26588e29a0177afb1665aa5fd96dfd)]: + - @effect/platform-node@0.33.4 + +## 0.21.3 + +### Patch Changes + +- Updated dependencies [[`cc1f588`](https://github.com/Effect-TS/platform/commit/cc1f5886bf4188e0128b64b9e2a67f789680cab0)]: + - @effect/platform-node@0.33.3 + - @effect/platform@0.32.2 + +## 0.21.2 + +### Patch Changes + +- [#310](https://github.com/Effect-TS/platform/pull/310) [`14239fb`](https://github.com/Effect-TS/platform/commit/14239fb11ae45db1a02d9ba883d0412a9c9e6343) Thanks [@tim-smart](https://github.com/tim-smart)! - update effect + +- Updated dependencies [[`14239fb`](https://github.com/Effect-TS/platform/commit/14239fb11ae45db1a02d9ba883d0412a9c9e6343)]: + - @effect/platform-node@0.33.2 + - @effect/platform@0.32.1 + +## 0.21.1 + +### Patch Changes + +- Updated dependencies [[`4da9a1b`](https://github.com/Effect-TS/platform/commit/4da9a1b73f7644561eab5d7d0d3dcc3b1b8b9b64)]: + - @effect/platform-node@0.33.1 + +## 0.21.0 + +### Minor Changes + +- [#307](https://github.com/Effect-TS/platform/pull/307) [`746f969`](https://github.com/Effect-TS/platform/commit/746f9692e2f7133dcb413e0eea08ac7b6b97a9bd) Thanks [@tim-smart](https://github.com/tim-smart)! - update effect + +### Patch Changes + +- Updated dependencies [[`746f969`](https://github.com/Effect-TS/platform/commit/746f9692e2f7133dcb413e0eea08ac7b6b97a9bd), [`92e56a1`](https://github.com/Effect-TS/platform/commit/92e56a1f844f28f26621a1887cc4da045039066d), [`92e56a1`](https://github.com/Effect-TS/platform/commit/92e56a1f844f28f26621a1887cc4da045039066d)]: + - @effect/platform@0.32.0 + - @effect/platform-node@0.33.0 + +## 0.20.3 + +### Patch Changes + +- Updated dependencies [[`7a46ec6`](https://github.com/Effect-TS/platform/commit/7a46ec679e2d4718919c407d0c6c5f0fdc35e62d)]: + - @effect/platform@0.31.2 + - @effect/platform-node@0.32.3 + +## 0.20.2 + +### Patch Changes + +- Updated dependencies [[`2f1ca0c`](https://github.com/Effect-TS/platform/commit/2f1ca0cd6d39062fef5717f322cec6767f243def)]: + - @effect/platform-node@0.32.2 + +## 0.20.1 + +### Patch Changes + +- [#293](https://github.com/Effect-TS/platform/pull/293) [`5a7d254`](https://github.com/Effect-TS/platform/commit/5a7d25406b0841cf6ec49218bd3324a4ddc3df5b) Thanks [@tim-smart](https://github.com/tim-smart)! - ensure http methods are uppercase + +- Updated dependencies [[`b712491`](https://github.com/Effect-TS/platform/commit/b71249168eb4623de8dbd28cd0102be688f5caa3), [`5a7d254`](https://github.com/Effect-TS/platform/commit/5a7d25406b0841cf6ec49218bd3324a4ddc3df5b)]: + - @effect/platform@0.31.1 + - @effect/platform-node@0.32.1 + +## 0.20.0 + +### Minor Changes + +- [#291](https://github.com/Effect-TS/platform/pull/291) [`5a677f1`](https://github.com/Effect-TS/platform/commit/5a677f1062d7373e21839dfa51db26beef15dca4) Thanks [@tim-smart](https://github.com/tim-smart)! - update effect + +### Patch Changes + +- [#289](https://github.com/Effect-TS/platform/pull/289) [`624855f`](https://github.com/Effect-TS/platform/commit/624855f635162b2c1232429253477d0805e02657) Thanks [@tim-smart](https://github.com/tim-smart)! - update deps + +- Updated dependencies [[`5a677f1`](https://github.com/Effect-TS/platform/commit/5a677f1062d7373e21839dfa51db26beef15dca4), [`624855f`](https://github.com/Effect-TS/platform/commit/624855f635162b2c1232429253477d0805e02657)]: + - @effect/platform-node@0.32.0 + - @effect/platform@0.31.0 + +## 0.19.9 + +### Patch Changes + +- Updated dependencies [[`d5d0932`](https://github.com/Effect-TS/platform/commit/d5d093219cde4f51afb9251d9ba4270fc70be0c1)]: + - @effect/platform@0.30.6 + - @effect/platform-node@0.31.9 + +## 0.19.8 + +### Patch Changes + +- [#285](https://github.com/Effect-TS/platform/pull/285) [`a13377b`](https://github.com/Effect-TS/platform/commit/a13377b21b1369947f76d1719dd0b4acc5c64086) Thanks [@IMax153](https://github.com/IMax153)! - avoid mutating global state with Terminal service + +- Updated dependencies [[`a13377b`](https://github.com/Effect-TS/platform/commit/a13377b21b1369947f76d1719dd0b4acc5c64086)]: + - @effect/platform-node@0.31.8 + +## 0.19.7 + +### Patch Changes + +- [#283](https://github.com/Effect-TS/platform/pull/283) [`efd464b`](https://github.com/Effect-TS/platform/commit/efd464bd0b16bb6bf3bb7507f9da835b380fb1a2) Thanks [@tim-smart](https://github.com/tim-smart)! - add WorkerManager to Node/BunContext + +- [#283](https://github.com/Effect-TS/platform/pull/283) [`efd464b`](https://github.com/Effect-TS/platform/commit/efd464bd0b16bb6bf3bb7507f9da835b380fb1a2) Thanks [@tim-smart](https://github.com/tim-smart)! - remove Terminal from Node/BunContext + +- Updated dependencies [[`efd464b`](https://github.com/Effect-TS/platform/commit/efd464bd0b16bb6bf3bb7507f9da835b380fb1a2), [`efd464b`](https://github.com/Effect-TS/platform/commit/efd464bd0b16bb6bf3bb7507f9da835b380fb1a2)]: + - @effect/platform-node@0.31.7 + +## 0.19.6 + +### Patch Changes + +- [#280](https://github.com/Effect-TS/platform/pull/280) [`d8e2234`](https://github.com/Effect-TS/platform/commit/d8e2234bc2fa0794e2a4b6a693ae1e7c1836bfb8) Thanks [@mikearnaldi](https://github.com/mikearnaldi)! - Recursive interrupt all fibers on kill + +- Updated dependencies [[`534cb34`](https://github.com/Effect-TS/platform/commit/534cb3486b55e08f9c9cb3f0d955b04da128986c), [`534cb34`](https://github.com/Effect-TS/platform/commit/534cb3486b55e08f9c9cb3f0d955b04da128986c), [`d8e2234`](https://github.com/Effect-TS/platform/commit/d8e2234bc2fa0794e2a4b6a693ae1e7c1836bfb8)]: + - @effect/platform-node@0.31.6 + +## 0.19.5 + +### Patch Changes + +- Updated dependencies [[`36e449c`](https://github.com/Effect-TS/platform/commit/36e449c95fab80dc54505cef2071dcbecce35b4f)]: + - @effect/platform@0.30.5 + - @effect/platform-node@0.31.5 + +## 0.19.4 + +### Patch Changes + +- [#275](https://github.com/Effect-TS/platform/pull/275) [`e28989e`](https://github.com/Effect-TS/platform/commit/e28989ebd1813cec7ce68f7dd8718f2254e05cad) Thanks [@tim-smart](https://github.com/tim-smart)! - add stack to WorkerError + +- Updated dependencies [[`e28989e`](https://github.com/Effect-TS/platform/commit/e28989ebd1813cec7ce68f7dd8718f2254e05cad)]: + - @effect/platform-node@0.31.4 + - @effect/platform@0.30.4 + +## 0.19.3 + +### Patch Changes + +- [#273](https://github.com/Effect-TS/platform/pull/273) [`589cd44`](https://github.com/Effect-TS/platform/commit/589cd4440d48f42d8bf19b72d1c2996f68ba56bf) Thanks [@tim-smart](https://github.com/tim-smart)! - use removeEventListener over signal + +- [#272](https://github.com/Effect-TS/platform/pull/272) [`1a055ac`](https://github.com/Effect-TS/platform/commit/1a055ac959faf12e9c57768b20babea12b1f7d2d) Thanks [@tim-smart](https://github.com/tim-smart)! - add WorkerError to send api + +- Updated dependencies [[`1a055ac`](https://github.com/Effect-TS/platform/commit/1a055ac959faf12e9c57768b20babea12b1f7d2d)]: + - @effect/platform-node@0.31.3 + - @effect/platform@0.30.3 + +## 0.19.2 + +### Patch Changes + +- Updated dependencies [[`3257fd5`](https://github.com/Effect-TS/platform/commit/3257fd52016af5a38c135de5f0aa33aac7de2538)]: + - @effect/platform-node@0.31.2 + - @effect/platform@0.30.2 + +## 0.19.1 + +### Patch Changes + +- Updated dependencies [[`58f5ccc`](https://github.com/Effect-TS/platform/commit/58f5ccc07d74abe6820debc0179665e5ef76b5c4)]: + - @effect/platform-node@0.31.1 + - @effect/platform@0.30.1 + +## 0.19.0 + +### Minor Changes + +- [#267](https://github.com/Effect-TS/platform/pull/267) [`3d38b40`](https://github.com/Effect-TS/platform/commit/3d38b40a939e32c6c0e8b62dd53a844a6f389182) Thanks [@tim-smart](https://github.com/tim-smart)! - update effect + +### Patch Changes + +- Updated dependencies [[`3d38b40`](https://github.com/Effect-TS/platform/commit/3d38b40a939e32c6c0e8b62dd53a844a6f389182)]: + - @effect/platform-node@0.31.0 + - @effect/platform@0.30.0 + +## 0.18.1 + +### Patch Changes + +- Updated dependencies [[`2bbe692`](https://github.com/Effect-TS/platform/commit/2bbe6928aa5e6929e58877ba236547310bca7e2b)]: + - @effect/platform-node@0.30.1 + - @effect/platform@0.29.1 + +## 0.18.0 + +### Minor Changes + +- [#250](https://github.com/Effect-TS/platform/pull/250) [`6e18090`](https://github.com/Effect-TS/platform/commit/6e18090db4686cd5564ab9dc3d8771d7b3ad97fa) Thanks [@tim-smart](https://github.com/tim-smart)! - updated FormData model and apis + +### Patch Changes + +- Updated dependencies [[`6e18090`](https://github.com/Effect-TS/platform/commit/6e18090db4686cd5564ab9dc3d8771d7b3ad97fa)]: + - @effect/platform-node@0.30.0 + - @effect/platform@0.29.0 + +## 0.17.4 + +### Patch Changes + +- Updated dependencies [[`8f5e6a2`](https://github.com/Effect-TS/platform/commit/8f5e6a2f2ced4408b0b311b0456828855e1cb958)]: + - @effect/platform-node@0.29.4 + - @effect/platform@0.28.4 + +## 0.17.3 + +### Patch Changes + +- Updated dependencies [[`9f79c1f`](https://github.com/Effect-TS/platform/commit/9f79c1f5278e60b3bcbd59f08e20189bcb25a84e)]: + - @effect/platform@0.28.3 + - @effect/platform-node@0.29.3 + +## 0.17.2 + +### Patch Changes + +- [#256](https://github.com/Effect-TS/platform/pull/256) [`62cbddb`](https://github.com/Effect-TS/platform/commit/62cbddb530371291123dea220bfebcc0521b54df) Thanks [@jessekelly881](https://github.com/jessekelly881)! - fix: added missing File type export + +- [#255](https://github.com/Effect-TS/platform/pull/255) [`fea76da`](https://github.com/Effect-TS/platform/commit/fea76da05190a65912911bd5b6f9cc0bef3b2edc) Thanks [@IMax153](https://github.com/IMax153)! - add basic Terminal interface for prompting user input + +- Updated dependencies [[`62cbddb`](https://github.com/Effect-TS/platform/commit/62cbddb530371291123dea220bfebcc0521b54df), [`fea76da`](https://github.com/Effect-TS/platform/commit/fea76da05190a65912911bd5b6f9cc0bef3b2edc)]: + - @effect/platform-node@0.29.2 + - @effect/platform@0.28.2 + +## 0.17.1 + +### Patch Changes + +- [#253](https://github.com/Effect-TS/platform/pull/253) [`43d2e29`](https://github.com/Effect-TS/platform/commit/43d2e2984fe88b39e907f45f089206ed88ad52d1) Thanks [@fubhy](https://github.com/fubhy)! - Update dependencies + +- Updated dependencies [[`43d2e29`](https://github.com/Effect-TS/platform/commit/43d2e2984fe88b39e907f45f089206ed88ad52d1)]: + - @effect/platform-node@0.29.1 + - @effect/platform@0.28.1 + +## 0.17.0 + +### Minor Changes + +- [#251](https://github.com/Effect-TS/platform/pull/251) [`05fef78`](https://github.com/Effect-TS/platform/commit/05fef784ac975059fb6335576feadc7f34644314) Thanks [@fubhy](https://github.com/fubhy)! - Re-added exports for http module + +### Patch Changes + +- Updated dependencies [[`05fef78`](https://github.com/Effect-TS/platform/commit/05fef784ac975059fb6335576feadc7f34644314)]: + - @effect/platform-node@0.29.0 + - @effect/platform@0.28.0 + +## 0.16.4 + +### Patch Changes + +- Updated dependencies [[`8a4b1c1`](https://github.com/Effect-TS/platform/commit/8a4b1c14808d9815eb93a5b10d8a5b26c4dd027b)]: + - @effect/platform-node@0.28.4 + - @effect/platform@0.27.4 + +## 0.16.3 + +### Patch Changes + +- [#243](https://github.com/Effect-TS/platform/pull/243) [`1ac0a42`](https://github.com/Effect-TS/platform/commit/1ac0a4208184ef1d23d5ad41a7f0e32bc4d80d85) Thanks [@tim-smart](https://github.com/tim-smart)! - fix worker interruption + +- Updated dependencies [[`1ac0a42`](https://github.com/Effect-TS/platform/commit/1ac0a4208184ef1d23d5ad41a7f0e32bc4d80d85)]: + - @effect/platform-node@0.28.3 + - @effect/platform@0.27.3 + +## 0.16.2 + +### Patch Changes + +- [#241](https://github.com/Effect-TS/platform/pull/241) [`e2aa7cd`](https://github.com/Effect-TS/platform/commit/e2aa7cd606a735809fbf79327cfebc009e89d84d) Thanks [@tim-smart](https://github.com/tim-smart)! - decrease bun worker close timeout + +- Updated dependencies [[`e2aa7cd`](https://github.com/Effect-TS/platform/commit/e2aa7cd606a735809fbf79327cfebc009e89d84d)]: + - @effect/platform@0.27.2 + - @effect/platform-node@0.28.2 + +## 0.16.1 + +### Patch Changes + +- [#239](https://github.com/Effect-TS/platform/pull/239) [`4d94b9d`](https://github.com/Effect-TS/platform/commit/4d94b9d30adba2bf4f6f6e1d4cd735e6362667c5) Thanks [@tim-smart](https://github.com/tim-smart)! - update effect + +- Updated dependencies [[`4d94b9d`](https://github.com/Effect-TS/platform/commit/4d94b9d30adba2bf4f6f6e1d4cd735e6362667c5)]: + - @effect/platform-node@0.28.1 + - @effect/platform@0.27.1 + +## 0.16.0 + +### Minor Changes + +- [#237](https://github.com/Effect-TS/platform/pull/237) [`1f79ed6`](https://github.com/Effect-TS/platform/commit/1f79ed6b4d2ee9ae2b59c4536854566c579e77c4) Thanks [@tim-smart](https://github.com/tim-smart)! - update effect + +### Patch Changes + +- Updated dependencies [[`1f79ed6`](https://github.com/Effect-TS/platform/commit/1f79ed6b4d2ee9ae2b59c4536854566c579e77c4)]: + - @effect/platform-node@0.28.0 + - @effect/platform@0.27.0 + +## 0.15.9 + +### Patch Changes + +- [#235](https://github.com/Effect-TS/platform/pull/235) [`6e14c02`](https://github.com/Effect-TS/platform/commit/6e14c02db668f380bb92f19037685fe40592a8fe) Thanks [@tim-smart](https://github.com/tim-smart)! - fix for hanging worker shutdown + +- Updated dependencies [[`6e14c02`](https://github.com/Effect-TS/platform/commit/6e14c02db668f380bb92f19037685fe40592a8fe)]: + - @effect/platform-node@0.27.9 + - @effect/platform@0.26.7 + +## 0.15.8 + +### Patch Changes + +- [#233](https://github.com/Effect-TS/platform/pull/233) [`71947e0`](https://github.com/Effect-TS/platform/commit/71947e0e0aa9dccf9aad6f63dd98a6b6c89f23b4) Thanks [@tim-smart](https://github.com/tim-smart)! - fix worker scope hanging on close + +- Updated dependencies [[`71947e0`](https://github.com/Effect-TS/platform/commit/71947e0e0aa9dccf9aad6f63dd98a6b6c89f23b4)]: + - @effect/platform-node@0.27.8 + - @effect/platform@0.26.6 + +## 0.15.7 + +### Patch Changes + +- [#231](https://github.com/Effect-TS/platform/pull/231) [`a3cbba4`](https://github.com/Effect-TS/platform/commit/a3cbba4a0fa0f1ef99a6d7e54f5ab46c6813ef00) Thanks [@tim-smart](https://github.com/tim-smart)! - add onCreate and broadcast to pool options + +- Updated dependencies [[`a3cbba4`](https://github.com/Effect-TS/platform/commit/a3cbba4a0fa0f1ef99a6d7e54f5ab46c6813ef00)]: + - @effect/platform-node@0.27.7 + - @effect/platform@0.26.5 + +## 0.15.6 + +### Patch Changes + +- [#229](https://github.com/Effect-TS/platform/pull/229) [`4661a8c`](https://github.com/Effect-TS/platform/commit/4661a8c63a13cc6630d5f3cbac90f4ff1d096e09) Thanks [@tim-smart](https://github.com/tim-smart)! - type worker runner success as never + +- [#229](https://github.com/Effect-TS/platform/pull/229) [`4661a8c`](https://github.com/Effect-TS/platform/commit/4661a8c63a13cc6630d5f3cbac90f4ff1d096e09) Thanks [@tim-smart](https://github.com/tim-smart)! - disable worker pool scaling + +- Updated dependencies [[`4661a8c`](https://github.com/Effect-TS/platform/commit/4661a8c63a13cc6630d5f3cbac90f4ff1d096e09), [`4661a8c`](https://github.com/Effect-TS/platform/commit/4661a8c63a13cc6630d5f3cbac90f4ff1d096e09)]: + - @effect/platform-node@0.27.6 + - @effect/platform@0.26.4 + +## 0.15.5 + +### Patch Changes + +- Updated dependencies [[`abb6baa`](https://github.com/Effect-TS/platform/commit/abb6baa61346580f97d2ab91b84a7342b5becc60)]: + - @effect/platform@0.26.3 + - @effect/platform-node@0.27.5 + +## 0.15.4 + +### Patch Changes + +- [#223](https://github.com/Effect-TS/platform/pull/223) [`3ab8299`](https://github.com/Effect-TS/platform/commit/3ab82991a21e15b4b7f5e53bc2d6e5a807f23698) Thanks [@tim-smart](https://github.com/tim-smart)! - add apis to access underlying http request source + +- Updated dependencies [[`3ab8299`](https://github.com/Effect-TS/platform/commit/3ab82991a21e15b4b7f5e53bc2d6e5a807f23698), [`3ab8299`](https://github.com/Effect-TS/platform/commit/3ab82991a21e15b4b7f5e53bc2d6e5a807f23698)]: + - @effect/platform-node@0.27.4 + +## 0.15.3 + +### Patch Changes + +- [#221](https://github.com/Effect-TS/platform/pull/221) [`3e57e82`](https://github.com/Effect-TS/platform/commit/3e57e8224bf7b4474b21ef1dc25db13107d9b635) Thanks [@tim-smart](https://github.com/tim-smart)! - export WorkerRunner layers + +- Updated dependencies [[`3e57e82`](https://github.com/Effect-TS/platform/commit/3e57e8224bf7b4474b21ef1dc25db13107d9b635)]: + - @effect/platform-node@0.27.3 + +## 0.15.2 + +### Patch Changes + +- Updated dependencies [[`f37f58c`](https://github.com/Effect-TS/platform/commit/f37f58ca21c1d5dfedc40c01cde0ffbc954d7e32)]: + - @effect/platform@0.26.2 + - @effect/platform-node@0.27.2 + +## 0.15.1 + +### Patch Changes + +- Updated dependencies [[`7471ac1`](https://github.com/Effect-TS/platform/commit/7471ac139f3c6867cd0d228ec54e88abd1384f5c)]: + - @effect/platform@0.26.1 + - @effect/platform-node@0.27.1 + +## 0.15.0 + +### Minor Changes + +- [#215](https://github.com/Effect-TS/platform/pull/215) [`59da2a6`](https://github.com/Effect-TS/platform/commit/59da2a6877e219b2ca0433aeeecab4ad7487816b) Thanks [@tim-smart](https://github.com/tim-smart)! - seperate request processing in http client + +### Patch Changes + +- Updated dependencies [[`59da2a6`](https://github.com/Effect-TS/platform/commit/59da2a6877e219b2ca0433aeeecab4ad7487816b)]: + - @effect/platform@0.26.0 + - @effect/platform-node@0.27.0 + +## 0.14.1 + +### Patch Changes + +- [#213](https://github.com/Effect-TS/platform/pull/213) [`38a49eb`](https://github.com/Effect-TS/platform/commit/38a49eb6ea99ef773007a98ec262898207c8f3c7) Thanks [@tim-smart](https://github.com/tim-smart)! - update effect + +- Updated dependencies [[`38a49eb`](https://github.com/Effect-TS/platform/commit/38a49eb6ea99ef773007a98ec262898207c8f3c7)]: + - @effect/platform-node@0.26.1 + - @effect/platform@0.25.1 + +## 0.14.0 + +### Minor Changes + +- [#211](https://github.com/Effect-TS/platform/pull/211) [`9ec45cb`](https://github.com/Effect-TS/platform/commit/9ec45cba6b7d5016079ccad9357934f12afe8750) Thanks [@tim-smart](https://github.com/tim-smart)! - update effect + +### Patch Changes + +- Updated dependencies [[`9ec45cb`](https://github.com/Effect-TS/platform/commit/9ec45cba6b7d5016079ccad9357934f12afe8750)]: + - @effect/platform-node@0.26.0 + - @effect/platform@0.25.0 + +## 0.13.0 + +### Minor Changes + +- [#209](https://github.com/Effect-TS/platform/pull/209) [`9c51aa1`](https://github.com/Effect-TS/platform/commit/9c51aa18beb7fd34023863ca069d3dde372765d8) Thanks [@tim-smart](https://github.com/tim-smart)! - update effect + +### Patch Changes + +- Updated dependencies [[`9c51aa1`](https://github.com/Effect-TS/platform/commit/9c51aa18beb7fd34023863ca069d3dde372765d8)]: + - @effect/platform-node@0.25.0 + - @effect/platform@0.24.0 + +## 0.12.1 + +### Patch Changes + +- Updated dependencies [[`b47639b`](https://github.com/Effect-TS/platform/commit/b47639b1df021beb075469921e9ef7a08c174555), [`41f8a65`](https://github.com/Effect-TS/platform/commit/41f8a650238bfbac5b8e18d58a431c3605b71aa5)]: + - @effect/platform-node@0.24.1 + - @effect/platform@0.23.1 + +## 0.12.0 + +### Minor Changes + +- [#204](https://github.com/Effect-TS/platform/pull/204) [`ee0c08f`](https://github.com/Effect-TS/platform/commit/ee0c08fd9828eae32696da1bde33d50a3ad9edf3) Thanks [@tim-smart](https://github.com/tim-smart)! - update effect + +### Patch Changes + +- Updated dependencies [[`ee0c08f`](https://github.com/Effect-TS/platform/commit/ee0c08fd9828eae32696da1bde33d50a3ad9edf3)]: + - @effect/platform-node@0.24.0 + - @effect/platform@0.23.0 + +## 0.11.2 + +### Patch Changes + +- [#194](https://github.com/Effect-TS/platform/pull/194) [`79b71d8`](https://github.com/Effect-TS/platform/commit/79b71d8cb3aa6520b2dcb7930850b423174e04b2) Thanks [@tim-smart](https://github.com/tim-smart)! - add Worker & WorkerRunner modules + +- Updated dependencies [[`79b71d8`](https://github.com/Effect-TS/platform/commit/79b71d8cb3aa6520b2dcb7930850b423174e04b2)]: + - @effect/platform-node@0.23.2 + - @effect/platform@0.22.1 + +## 0.11.1 + +### Patch Changes + +- Updated dependencies [[`58a002a`](https://github.com/Effect-TS/platform/commit/58a002acaafdb31e601c0de1878f4a8dee723e13)]: + - @effect/platform-node@0.23.1 + +## 0.11.0 + +### Minor Changes + +- [#199](https://github.com/Effect-TS/platform/pull/199) [`1e94b15`](https://github.com/Effect-TS/platform/commit/1e94b1588e51df20f9c4fc4871b246048751506c) Thanks [@tim-smart](https://github.com/tim-smart)! - enable tracing by default + +### Patch Changes + +- Updated dependencies [[`fcc5871`](https://github.com/Effect-TS/platform/commit/fcc5871d326296334ff9a421860d69e697eea559), [`fcc5871`](https://github.com/Effect-TS/platform/commit/fcc5871d326296334ff9a421860d69e697eea559), [`fcc5871`](https://github.com/Effect-TS/platform/commit/fcc5871d326296334ff9a421860d69e697eea559), [`1e94b15`](https://github.com/Effect-TS/platform/commit/1e94b1588e51df20f9c4fc4871b246048751506c)]: + - @effect/platform-node@0.23.0 + - @effect/platform@0.22.0 + +## 0.10.1 + +### Patch Changes + +- Updated dependencies [[`25ce726`](https://github.com/Effect-TS/platform/commit/25ce72656a9addbb1f4d539ea69423b73fe43f46)]: + - @effect/platform-node@0.22.1 + +## 0.10.0 + +### Minor Changes + +- [#193](https://github.com/Effect-TS/platform/pull/193) [`9ec4b1d`](https://github.com/Effect-TS/platform/commit/9ec4b1d284caa1c4f19a58c46ed7c25fb10d39a5) Thanks [@tim-smart](https://github.com/tim-smart)! - update effect + +### Patch Changes + +- [#191](https://github.com/Effect-TS/platform/pull/191) [`2711aea`](https://github.com/Effect-TS/platform/commit/2711aea855936c82c282e61fbc6d2f1a1ab6778a) Thanks [@tim-smart](https://github.com/tim-smart)! - update effect + +- Updated dependencies [[`2711aea`](https://github.com/Effect-TS/platform/commit/2711aea855936c82c282e61fbc6d2f1a1ab6778a), [`9ec4b1d`](https://github.com/Effect-TS/platform/commit/9ec4b1d284caa1c4f19a58c46ed7c25fb10d39a5)]: + - @effect/platform-node@0.22.0 + - @effect/platform@0.21.0 + +## 0.9.0 + +### Minor Changes + +- [#189](https://github.com/Effect-TS/platform/pull/189) [`b07f8cd`](https://github.com/Effect-TS/platform/commit/b07f8cd50ef44d577aa981a532025aedb364df13) Thanks [@tim-smart](https://github.com/tim-smart)! - update effect + +### Patch Changes + +- Updated dependencies [[`b07f8cd`](https://github.com/Effect-TS/platform/commit/b07f8cd50ef44d577aa981a532025aedb364df13)]: + - @effect/platform-node@0.21.0 + - @effect/platform@0.20.0 + +## 0.8.1 + +### Patch Changes + +- [#187](https://github.com/Effect-TS/platform/pull/187) [`26e05da`](https://github.com/Effect-TS/platform/commit/26e05dad112fa43403b23ebc815a98f0c95e0558) Thanks [@tim-smart](https://github.com/tim-smart)! - fix order of pre response access + +- Updated dependencies [[`26e05da`](https://github.com/Effect-TS/platform/commit/26e05dad112fa43403b23ebc815a98f0c95e0558)]: + - @effect/platform-node@0.20.1 + +## 0.8.0 + +### Minor Changes + +- [#184](https://github.com/Effect-TS/platform/pull/184) [`903b599`](https://github.com/Effect-TS/platform/commit/903b5995bb407c399846e6b75e47e53098b2c80d) Thanks [@tim-smart](https://github.com/tim-smart)! - update effect + +### Patch Changes + +- [#186](https://github.com/Effect-TS/platform/pull/186) [`a3bcda4`](https://github.com/Effect-TS/platform/commit/a3bcda4c2c6655ab86769cca60bece5eb64f866e) Thanks [@tim-smart](https://github.com/tim-smart)! - add pre response handlers to http + +- Updated dependencies [[`903b599`](https://github.com/Effect-TS/platform/commit/903b5995bb407c399846e6b75e47e53098b2c80d), [`a3bcda4`](https://github.com/Effect-TS/platform/commit/a3bcda4c2c6655ab86769cca60bece5eb64f866e)]: + - @effect/platform-node@0.20.0 + - @effect/platform@0.19.0 + +## 0.7.9 + +### Patch Changes + +- [#181](https://github.com/Effect-TS/platform/pull/181) [`d0d5458`](https://github.com/Effect-TS/platform/commit/d0d545869baeb91d594804ab759713f424eb7a11) Thanks [@tim-smart](https://github.com/tim-smart)! - fix error type exports + +- Updated dependencies [[`d0d5458`](https://github.com/Effect-TS/platform/commit/d0d545869baeb91d594804ab759713f424eb7a11)]: + - @effect/platform-node@0.19.9 + +## 0.7.8 + +### Patch Changes + +- [#179](https://github.com/Effect-TS/platform/pull/179) [`843488f`](https://github.com/Effect-TS/platform/commit/843488f79b253518f131693faf2955f5c795a1bc) Thanks [@tim-smart](https://github.com/tim-smart)! - update effect + +- Updated dependencies [[`843488f`](https://github.com/Effect-TS/platform/commit/843488f79b253518f131693faf2955f5c795a1bc)]: + - @effect/platform-node@0.19.8 + - @effect/platform@0.18.7 + +## 0.7.7 + +### Patch Changes + +- Updated dependencies [[`6116f8b`](https://github.com/Effect-TS/platform/commit/6116f8b39533c897445713c7dce531c0c60a1cbb)]: + - @effect/platform-node@0.19.7 + +## 0.7.6 + +### Patch Changes + +- Updated dependencies [[`7e4e2a5`](https://github.com/Effect-TS/platform/commit/7e4e2a5d815c677e4eb6adb2c6e9369414a79384), [`d1c2b38`](https://github.com/Effect-TS/platform/commit/d1c2b38cbb1189249c0bfd47582e00ff771428e3)]: + - @effect/platform@0.18.6 + - @effect/platform-node@0.19.6 + +## 0.7.5 + +### Patch Changes + +- [#171](https://github.com/Effect-TS/platform/pull/171) [`fbbcaa9`](https://github.com/Effect-TS/platform/commit/fbbcaa9b1d4f48f204072a802fb11bcb29813664) Thanks [@tim-smart](https://github.com/tim-smart)! - remove preserveModules patch for preconstruct + +- Updated dependencies [[`fbbcaa9`](https://github.com/Effect-TS/platform/commit/fbbcaa9b1d4f48f204072a802fb11bcb29813664)]: + - @effect/platform-node@0.19.5 + - @effect/platform@0.18.5 + +## 0.7.4 + +### Patch Changes + +- [#169](https://github.com/Effect-TS/platform/pull/169) [`bd8778d`](https://github.com/Effect-TS/platform/commit/bd8778d1a534f28cab4b326bb25c086fafed8101) Thanks [@tim-smart](https://github.com/tim-smart)! - fix nested modules + +- Updated dependencies [[`bd8778d`](https://github.com/Effect-TS/platform/commit/bd8778d1a534f28cab4b326bb25c086fafed8101)]: + - @effect/platform@0.18.4 + - @effect/platform-node@0.19.4 + +## 0.7.3 + +### Patch Changes + +- [#167](https://github.com/Effect-TS/platform/pull/167) [`7027589`](https://github.com/Effect-TS/platform/commit/7027589d6dde621065eb8834a2b1ba4d3adc943b) Thanks [@tim-smart](https://github.com/tim-smart)! - build with preconstruct + +- Updated dependencies [[`7027589`](https://github.com/Effect-TS/platform/commit/7027589d6dde621065eb8834a2b1ba4d3adc943b)]: + - @effect/platform-node@0.19.3 + - @effect/platform@0.18.3 + +## 0.7.2 + +### Patch Changes + +- [#165](https://github.com/Effect-TS/platform/pull/165) [`7e3a741`](https://github.com/Effect-TS/platform/commit/7e3a74197325566df47f9b4459e518eea0762d13) Thanks [@fubhy](https://github.com/fubhy)! - Fix peer deps version range + +- Updated dependencies [[`7e3a741`](https://github.com/Effect-TS/platform/commit/7e3a74197325566df47f9b4459e518eea0762d13)]: + - @effect/platform-node@0.19.2 + - @effect/platform@0.18.2 + +## 0.7.1 + +### Patch Changes + +- [#163](https://github.com/Effect-TS/platform/pull/163) [`c957232`](https://github.com/Effect-TS/platform/commit/c9572328ee37f44e93e933da622b21df414bf5c6) Thanks [@tim-smart](https://github.com/tim-smart)! - update effect + +- Updated dependencies [[`c957232`](https://github.com/Effect-TS/platform/commit/c9572328ee37f44e93e933da622b21df414bf5c6)]: + - @effect/platform-node@0.19.1 + - @effect/platform@0.18.1 + +## 0.7.0 + +### Minor Changes + +- [#160](https://github.com/Effect-TS/platform/pull/160) [`c2dc0ab`](https://github.com/Effect-TS/platform/commit/c2dc0abb20b073fd19e38b4e61a08b1edee0f37f) Thanks [@fubhy](https://github.com/fubhy)! - update to effect package + +### Patch Changes + +- Updated dependencies [[`c2dc0ab`](https://github.com/Effect-TS/platform/commit/c2dc0abb20b073fd19e38b4e61a08b1edee0f37f)]: + - @effect/platform-node@0.19.0 + - @effect/platform@0.18.0 + +## 0.6.1 + +### Patch Changes + +- Updated dependencies [[`9b10bf3`](https://github.com/Effect-TS/platform/commit/9b10bf394106ba0bafd8440dc0b3fba30a5cc1ea)]: + - @effect/platform@0.17.1 + - @effect/platform-node@0.18.1 + +## 0.6.0 + +### Minor Changes + +- [#156](https://github.com/Effect-TS/platform/pull/156) [`e6c4101`](https://github.com/Effect-TS/platform/commit/e6c41011e5420d90c543dd25d87036d4150f3e85) Thanks [@tim-smart](https://github.com/tim-smart)! - update dependencies + +### Patch Changes + +- Updated dependencies [[`e6c4101`](https://github.com/Effect-TS/platform/commit/e6c41011e5420d90c543dd25d87036d4150f3e85)]: + - @effect/platform-node@0.18.0 + - @effect/platform@0.17.0 + +## 0.5.0 + +### Minor Changes + +- [#153](https://github.com/Effect-TS/platform/pull/153) [`be1b6f0`](https://github.com/Effect-TS/platform/commit/be1b6f036246713a55462ec76e8d999eae654cd7) Thanks [@IMax153](https://github.com/IMax153)! - make platform-node an explicit dependency of platform-bun + +- [#155](https://github.com/Effect-TS/platform/pull/155) [`937b9e5`](https://github.com/Effect-TS/platform/commit/937b9e5c00f80bea128f21c7f5bfa662ba1d45bd) Thanks [@tim-smart](https://github.com/tim-smart)! - use direct deps in sibling packages + +### Patch Changes + +- Updated dependencies [[`be1b6f0`](https://github.com/Effect-TS/platform/commit/be1b6f036246713a55462ec76e8d999eae654cd7), [`937b9e5`](https://github.com/Effect-TS/platform/commit/937b9e5c00f80bea128f21c7f5bfa662ba1d45bd)]: + - @effect/platform-node@0.17.0 + +## 0.4.2 + +### Patch Changes + +- Updated dependencies [[`ea877f8`](https://github.com/Effect-TS/platform/commit/ea877f8948a43a394658abf8b781a56a097624e9)]: + - @effect/platform-node@0.16.2 + +## 0.4.1 + +### Patch Changes + +- [#148](https://github.com/Effect-TS/platform/pull/148) [`492f0e7`](https://github.com/Effect-TS/platform/commit/492f0e700e939ded6ff17eeca4d50a9e1ce59219) Thanks [@tim-smart](https://github.com/tim-smart)! - add IncomingMessage.remoteAddress + +- Updated dependencies [[`492f0e7`](https://github.com/Effect-TS/platform/commit/492f0e700e939ded6ff17eeca4d50a9e1ce59219)]: + - @effect/platform-node@0.16.1 + +## 0.4.0 + +### Minor Changes + +- [#145](https://github.com/Effect-TS/platform/pull/145) [`d0522be`](https://github.com/Effect-TS/platform/commit/d0522be6f824571d83be8c6aa16a3d7caa1b3447) Thanks [@tim-smart](https://github.com/tim-smart)! - update dependencies + +### Patch Changes + +- Updated dependencies [[`d0522be`](https://github.com/Effect-TS/platform/commit/d0522be6f824571d83be8c6aa16a3d7caa1b3447), [`6583ad4`](https://github.com/Effect-TS/platform/commit/6583ad4ef5b718620c873208bb11196d35733034)]: + - @effect/platform-node@0.16.0 + +## 0.3.3 + +### Patch Changes + +- Updated dependencies [[`8571c36`](https://github.com/Effect-TS/platform/commit/8571c36f1f8a6ab36b23ee26922cf58def15196e)]: + - @effect/platform-node@0.15.2 + +## 0.3.2 + +### Patch Changes + +- [#140](https://github.com/Effect-TS/platform/pull/140) [`fadedf1`](https://github.com/Effect-TS/platform/commit/fadedf15990b438d4baea3bb9a5b63a911715f0b) Thanks [@tim-smart](https://github.com/tim-smart)! - remove host from bun server urls + +## 0.3.1 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform-node@0.15.1 + +## 0.3.0 + +### Minor Changes + +- [#135](https://github.com/Effect-TS/platform/pull/135) [`99f2a49`](https://github.com/Effect-TS/platform/commit/99f2a49c614a5b80646f6600a170609fe7e38025) Thanks [@tim-smart](https://github.com/tim-smart)! - update dependencies + +### Patch Changes + +- Updated dependencies [[`99f2a49`](https://github.com/Effect-TS/platform/commit/99f2a49c614a5b80646f6600a170609fe7e38025)]: + - @effect/platform-node@0.15.0 + +## 0.2.1 + +### Patch Changes + +- [#133](https://github.com/Effect-TS/platform/pull/133) [`1d2c403`](https://github.com/Effect-TS/platform/commit/1d2c4033af11f18ba09f53dcfdf8b3fc399bd22f) Thanks [@tim-smart](https://github.com/tim-smart)! - add http platform abstraction + +- [#133](https://github.com/Effect-TS/platform/pull/133) [`1d2c403`](https://github.com/Effect-TS/platform/commit/1d2c4033af11f18ba09f53dcfdf8b3fc399bd22f) Thanks [@tim-smart](https://github.com/tim-smart)! - handle HEAD requests + +- Updated dependencies [[`1d2c403`](https://github.com/Effect-TS/platform/commit/1d2c4033af11f18ba09f53dcfdf8b3fc399bd22f), [`1d2c403`](https://github.com/Effect-TS/platform/commit/1d2c4033af11f18ba09f53dcfdf8b3fc399bd22f)]: + - @effect/platform-node@0.14.1 + +## 0.2.0 + +### Minor Changes + +- [#130](https://github.com/Effect-TS/platform/pull/130) [`2713c4f`](https://github.com/Effect-TS/platform/commit/2713c4f766f5493303221772368710a09033658d) Thanks [@tim-smart](https://github.com/tim-smart)! - update dependencies + +### Patch Changes + +- Updated dependencies [[`2713c4f`](https://github.com/Effect-TS/platform/commit/2713c4f766f5493303221772368710a09033658d)]: + - @effect/platform-node@1.0.0 + +## 0.1.1 + +### Patch Changes + +- Updated dependencies [[`12fbbe9`](https://github.com/Effect-TS/platform/commit/12fbbe9366e3a07895326614ec911ff2601138b1)]: + - @effect/platform-node@0.13.18 + +## 0.1.0 + +### Minor Changes + +- [#125](https://github.com/Effect-TS/platform/pull/125) [`eb54e53`](https://github.com/Effect-TS/platform/commit/eb54e53d95e7b863d8ffdff9de12b0abd462b217) Thanks [@tim-smart](https://github.com/tim-smart)! - add /platform-bun + +### Patch Changes + +- [#125](https://github.com/Effect-TS/platform/pull/125) [`eb54e53`](https://github.com/Effect-TS/platform/commit/eb54e53d95e7b863d8ffdff9de12b0abd462b217) Thanks [@tim-smart](https://github.com/tim-smart)! - restruture platform-node for platform-bun reuse + +- Updated dependencies [[`eb54e53`](https://github.com/Effect-TS/platform/commit/eb54e53d95e7b863d8ffdff9de12b0abd462b217)]: + - @effect/platform-node@0.13.17 diff --git a/repos/effect/packages/platform-bun/LICENSE b/repos/effect/packages/platform-bun/LICENSE new file mode 100644 index 0000000..be1f5c1 --- /dev/null +++ b/repos/effect/packages/platform-bun/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Effectful Technologies Inc + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/repos/effect/packages/platform-bun/README.md b/repos/effect/packages/platform-bun/README.md new file mode 100644 index 0000000..b2cc371 --- /dev/null +++ b/repos/effect/packages/platform-bun/README.md @@ -0,0 +1,7 @@ +# `@effect/platform-bun` + +Provides Bun-specific implementations for the abstractions defined in [`@effect/platform`](https://github.com/Effect-TS/effect/tree/main/packages/platform), allowing you to write platform-independent code that runs smoothly in Bun environments. + +## Documentation + +- **API Reference**: [View the full documentation](https://effect-ts.github.io/effect/docs/platform-bun). diff --git a/repos/effect/packages/platform-bun/docgen.json b/repos/effect/packages/platform-bun/docgen.json new file mode 100644 index 0000000..6d02004 --- /dev/null +++ b/repos/effect/packages/platform-bun/docgen.json @@ -0,0 +1,5 @@ +{ + "$schema": "../../node_modules/@effect/docgen/schema.json", + "srcLink": "https://github.com/Effect-TS/effect/tree/main/packages/platform-bun/src/", + "exclude": ["src/internal/**/*.ts"] +} diff --git a/repos/effect/packages/platform-bun/examples/http-client.ts b/repos/effect/packages/platform-bun/examples/http-client.ts new file mode 100644 index 0000000..7c27e3e --- /dev/null +++ b/repos/effect/packages/platform-bun/examples/http-client.ts @@ -0,0 +1,57 @@ +import { FetchHttpClient, HttpClient, HttpClientRequest, HttpClientResponse } from "@effect/platform" +import type { HttpBody, HttpClientError } from "@effect/platform" +import { BunRuntime } from "@effect/platform-bun" +import { Context, Effect, Layer } from "effect" +import type * as ParseResult from "effect/ParseResult" +import * as Schema from "effect/Schema" + +class Todo extends Schema.Class("Todo")({ + userId: Schema.Number, + id: Schema.Number, + title: Schema.String, + completed: Schema.Boolean +}) {} + +const TodoWithoutId = Schema.Struct(Todo.fields).pipe(Schema.omit("id")) +type TodoWithoutId = Schema.Schema.Type + +interface TodoService { + readonly create: ( + _: TodoWithoutId + ) => Effect.Effect +} +const TodoService = Context.GenericTag("@effect/platform-bun/examples/TodoService") + +const makeTodoService = Effect.gen(function*() { + const defaultClient = yield* HttpClient.HttpClient + const clientWithBaseUrl = defaultClient.pipe( + HttpClient.filterStatusOk, + HttpClient.mapRequest(HttpClientRequest.prependUrl("https://jsonplaceholder.typicode.com")) + ) + + const addTodoWithoutIdBody = HttpClientRequest.schemaBodyJson(TodoWithoutId) + const create = (todo: TodoWithoutId) => + addTodoWithoutIdBody( + HttpClientRequest.post("/todos"), + todo + ).pipe( + Effect.flatMap(clientWithBaseUrl.execute), + Effect.flatMap(HttpClientResponse.schemaBodyJson(Todo)), + Effect.scoped + ) + + return TodoService.of({ create }) +}) + +const TodoServiceLive = Layer.effect(TodoService, makeTodoService).pipe( + Layer.provide(FetchHttpClient.layer) +) + +Effect.flatMap( + TodoService, + (todos) => todos.create({ userId: 1, title: "test", completed: false }) +).pipe( + Effect.tap(Effect.log), + Effect.provide(TodoServiceLive), + BunRuntime.runMain +) diff --git a/repos/effect/packages/platform-bun/examples/http-router.ts b/repos/effect/packages/platform-bun/examples/http-router.ts new file mode 100644 index 0000000..d479dfc --- /dev/null +++ b/repos/effect/packages/platform-bun/examples/http-router.ts @@ -0,0 +1,72 @@ +import { + HttpMiddleware, + HttpRouter, + HttpServer, + HttpServerRequest, + HttpServerRespondable, + HttpServerResponse, + Multipart +} from "@effect/platform" +import { BunHttpServer, BunRuntime } from "@effect/platform-bun" +import { Effect, Layer, Schedule, Schema, Stream } from "effect" + +const ServerLive = BunHttpServer.layer({ port: 3000 }) + +class MyError extends Schema.TaggedError()("MyError", { + message: Schema.String +}) { + [HttpServerRespondable.symbol]() { + return HttpServerResponse.schemaJson(MyError)(this, { status: 403 }) + } +} + +const HttpLive = HttpRouter.empty.pipe( + HttpRouter.get( + "/", + Effect.map( + HttpServerRequest.HttpServerRequest, + (req) => HttpServerResponse.text(req.url) + ) + ), + HttpRouter.get("/package", HttpServerResponse.file("./package.json")), + HttpRouter.get("/fail", new MyError({ message: "failed" })), + HttpRouter.get("/sleep", Effect.as(Effect.sleep("10 seconds"), HttpServerResponse.empty())), + HttpRouter.post( + "/upload", + Effect.gen(function*() { + const data = yield* HttpServerRequest.schemaBodyForm(Schema.Struct({ + files: Multipart.FilesSchema + })) + console.log("got files", data.files) + return HttpServerResponse.empty() + }) + ), + HttpRouter.get( + "/stream", + Effect.gen(function*() { + yield* Effect.addFinalizer(() => Effect.log("handler completed")) + const stream = Stream.fromSchedule(Schedule.spaced(1000)).pipe( + Stream.map((n) => `Hello World ${n}`), + Stream.tap((_) => Effect.log(_)), + Stream.encodeText + ) + return HttpServerResponse.stream(stream) + }) + ), + HttpRouter.get( + "/ws", + Stream.fromSchedule(Schedule.spaced(1000)).pipe( + Stream.map(JSON.stringify), + Stream.pipeThroughChannel(HttpServerRequest.upgradeChannel()), + Stream.decodeText(), + Stream.runForEach((_) => Effect.log(_)), + Effect.annotateLogs("ws", "recv"), + Effect.as(HttpServerResponse.empty()) + ) + ), + HttpServer.serve(HttpMiddleware.logger), + HttpServer.withLogAddress, + Layer.provide(ServerLive) +) + +BunRuntime.runMain(Layer.launch(HttpLive)) diff --git a/repos/effect/packages/platform-bun/examples/http-server.ts b/repos/effect/packages/platform-bun/examples/http-server.ts new file mode 100644 index 0000000..45a8bd0 --- /dev/null +++ b/repos/effect/packages/platform-bun/examples/http-server.ts @@ -0,0 +1,9 @@ +import { HttpServer, HttpServerResponse } from "@effect/platform" +import { BunHttpServer, BunRuntime } from "@effect/platform-bun" +import { Layer } from "effect" + +const HttpLive = HttpServer.serve(HttpServerResponse.text("Hello World")).pipe( + Layer.provide(BunHttpServer.layer({ port: 3000 })) +) + +BunRuntime.runMain(Layer.launch(HttpLive)) diff --git a/repos/effect/packages/platform-bun/examples/http-tag-router.ts b/repos/effect/packages/platform-bun/examples/http-tag-router.ts new file mode 100644 index 0000000..8cb4d78 --- /dev/null +++ b/repos/effect/packages/platform-bun/examples/http-tag-router.ts @@ -0,0 +1,44 @@ +import { HttpMiddleware, HttpRouter, HttpServer, HttpServerResponse } from "@effect/platform" +import { BunHttpServer, BunRuntime } from "@effect/platform-bun" +import { Effect, Layer } from "effect" + +// You can define router instances using `HttpRouter.Tag` +class UserRouter extends HttpRouter.Tag("UserRouter")() {} + +// Create `Layer`'s for your routes with `UserRouter.use` +const GetUsers = UserRouter.use((router) => + Effect.gen(function*() { + yield* router.get("/", HttpServerResponse.text("got users")) + }) +) + +const CreateUser = UserRouter.use((router) => + Effect.gen(function*() { + yield* router.post("/", HttpServerResponse.text("created user")) + }) +) + +// Merge all the routes together with `Layer.mergeAll` +const AllUserRoutes = Layer.mergeAll(GetUsers, CreateUser).pipe( + Layer.provideMerge(UserRouter.Live) +) + +// `HttpRouter.Default` can also be used. Here we combine our `UserRouter` with +// the default router. +const AllRoutes = HttpRouter.Default.use((router) => + Effect.gen(function*() { + yield* router.mount("/users", yield* UserRouter.router) + }) +).pipe(Layer.provide(AllUserRoutes)) + +const ServerLive = BunHttpServer.layer({ port: 3000 }) + +// use the `.unwrap` api to turn the underlying `HttpRouter` into another layer. +// Here we use `HttpServer.serve` to create a server from the `HttpRouter`. +const HttpLive = HttpRouter.Default.unwrap(HttpServer.serve(HttpMiddleware.logger)).pipe( + HttpServer.withLogAddress, + Layer.provide(AllRoutes), + Layer.provide(ServerLive) +) + +BunRuntime.runMain(Layer.launch(HttpLive)) diff --git a/repos/effect/packages/platform-bun/examples/worker.ts b/repos/effect/packages/platform-bun/examples/worker.ts new file mode 100644 index 0000000..71ae2a9 --- /dev/null +++ b/repos/effect/packages/platform-bun/examples/worker.ts @@ -0,0 +1,30 @@ +import { Worker } from "@effect/platform" +import { BunRuntime, BunWorker } from "@effect/platform-bun" +import { Console, Context, Effect, Layer, Stream } from "effect" +import * as OS from "node:os" + +class MyWorkerPool extends Context.Tag("@app/MyWorkerPool")< + MyWorkerPool, + Worker.WorkerPool +>() {} + +const PoolLive = Worker.makePoolLayer(MyWorkerPool, { + size: OS.availableParallelism() +}).pipe( + Layer.provide(BunWorker.layer(() => new globalThis.Worker(`${__dirname}/worker/range.ts`))) +) + +Effect.gen(function*() { + const pool = yield* MyWorkerPool + yield* Effect.all([ + pool.execute(5).pipe( + Stream.runForEach((_) => Console.log("worker 1", _)) + ), + pool.execute(10).pipe( + Stream.runForEach((_) => Console.log("worker 2", _)) + ), + pool.execute(15).pipe( + Stream.runForEach((_) => Console.log("worker 3", _)) + ) + ], { concurrency: "inherit" }) +}).pipe(Effect.provide(PoolLive), BunRuntime.runMain) diff --git a/repos/effect/packages/platform-bun/examples/worker/range.ts b/repos/effect/packages/platform-bun/examples/worker/range.ts new file mode 100644 index 0000000..e4800cc --- /dev/null +++ b/repos/effect/packages/platform-bun/examples/worker/range.ts @@ -0,0 +1,11 @@ +import { BunRuntime, BunWorkerRunner } from "@effect/platform-bun" +import * as Runner from "@effect/platform/WorkerRunner" +import { Effect, Layer, Stream } from "effect" + +const WorkerLive = Effect.gen(function*() { + yield* Runner.make((n: number) => Stream.range(0, n)) + yield* Effect.log("worker started") + yield* Effect.addFinalizer(() => Effect.log("worker closed")) +}).pipe(Layer.scopedDiscard, Layer.provide(BunWorkerRunner.layer)) + +BunRuntime.runMain(Runner.launch(WorkerLive)) diff --git a/repos/effect/packages/platform-bun/package.json b/repos/effect/packages/platform-bun/package.json new file mode 100644 index 0000000..ea6d0ef --- /dev/null +++ b/repos/effect/packages/platform-bun/package.json @@ -0,0 +1,69 @@ +{ + "name": "@effect/platform-bun", + "type": "module", + "version": "0.89.0", + "license": "MIT", + "description": "Platform specific implementations for the Bun runtime", + "homepage": "https://effect.website", + "repository": { + "type": "git", + "url": "https://github.com/Effect-TS/effect.git", + "directory": "packages/platform-bun" + }, + "bugs": { + "url": "https://github.com/Effect-TS/effect/issues" + }, + "tags": [ + "bun", + "typescript", + "algebraic-data-types", + "functional-programming" + ], + "keywords": [ + "bun", + "typescript", + "algebraic-data-types", + "functional-programming" + ], + "publishConfig": { + "access": "public", + "provenance": true, + "directory": "dist", + "linkDirectory": false + }, + "exports": { + "./package.json": "./package.json", + ".": "./src/index.ts", + "./*": "./src/*.ts", + "./internal/*": null + }, + "scripts": { + "codegen": "build-utils prepare-v3", + "build": "pnpm build-esm && pnpm build-annotate && pnpm build-cjs && build-utils pack-v3", + "build-esm": "tsc -b tsconfig.build.json", + "build-cjs": "babel build/esm --plugins @babel/transform-export-namespace-from --plugins @babel/transform-modules-commonjs --out-dir build/cjs --source-maps", + "build-annotate": "babel build/esm --plugins annotate-pure-calls --out-dir build/esm --source-maps", + "check": "tsc -b tsconfig.json", + "test": "vitest", + "coverage": "vitest --coverage" + }, + "peerDependencies": { + "@effect/cluster": "workspace:^", + "@effect/platform": "workspace:^", + "@effect/rpc": "workspace:^", + "@effect/sql": "workspace:^", + "effect": "workspace:^" + }, + "dependencies": { + "@effect/platform-node-shared": "workspace:^", + "multipasta": "^0.2.7" + }, + "devDependencies": { + "@effect/cluster": "workspace:^", + "@effect/platform": "workspace:^", + "@effect/rpc": "workspace:^", + "@effect/sql": "workspace:^", + "@types/bun": "^1.2.2", + "effect": "workspace:^" + } +} diff --git a/repos/effect/packages/platform-bun/src/BunClusterHttp.ts b/repos/effect/packages/platform-bun/src/BunClusterHttp.ts new file mode 100644 index 0000000..476a4fa --- /dev/null +++ b/repos/effect/packages/platform-bun/src/BunClusterHttp.ts @@ -0,0 +1,123 @@ +/** + * @since 1.0.0 + */ +import * as HttpRunner from "@effect/cluster/HttpRunner" +import * as MessageStorage from "@effect/cluster/MessageStorage" +import * as RunnerHealth from "@effect/cluster/RunnerHealth" +import * as Runners from "@effect/cluster/Runners" +import * as RunnerStorage from "@effect/cluster/RunnerStorage" +import type { Sharding } from "@effect/cluster/Sharding" +import * as ShardingConfig from "@effect/cluster/ShardingConfig" +import * as SqlMessageStorage from "@effect/cluster/SqlMessageStorage" +import * as SqlRunnerStorage from "@effect/cluster/SqlRunnerStorage" +import type * as Etag from "@effect/platform/Etag" +import * as FetchHttpClient from "@effect/platform/FetchHttpClient" +import type { HttpPlatform } from "@effect/platform/HttpPlatform" +import type { HttpServer } from "@effect/platform/HttpServer" +import type { ServeError } from "@effect/platform/HttpServerError" +import * as RpcSerialization from "@effect/rpc/RpcSerialization" +import type { SqlClient } from "@effect/sql/SqlClient" +import type { ConfigError } from "effect/ConfigError" +import * as Effect from "effect/Effect" +import * as Layer from "effect/Layer" +import * as Option from "effect/Option" +import type { BunContext } from "./BunContext.js" +import * as BunHttpServer from "./BunHttpServer.js" +import * as BunSocket from "./BunSocket.js" + +/** + * @since 1.0.0 + * @category Layers + */ +export const layerHttpServer: Layer.Layer< + | HttpPlatform + | Etag.Generator + | BunContext + | HttpServer, + ServeError, + ShardingConfig.ShardingConfig +> = Effect.gen(function*() { + const config = yield* ShardingConfig.ShardingConfig + const listenAddress = Option.orElse(config.runnerListenAddress, () => config.runnerAddress) + if (listenAddress._tag === "None") { + return yield* Effect.die("BunClusterHttp.layerHttpServer: ShardingConfig.runnerAddress is None") + } + return BunHttpServer.layer(listenAddress.value) +}).pipe(Layer.unwrapEffect) + +/** + * @since 1.0.0 + * @category Layers + */ +export const layer = < + const ClientOnly extends boolean = false, + const Storage extends "local" | "sql" | "byo" = never +>(options: { + readonly transport: "http" | "websocket" + readonly serialization?: "msgpack" | "ndjson" | undefined + readonly clientOnly?: ClientOnly | undefined + readonly storage?: Storage | undefined + readonly shardingConfig?: Partial | undefined +}): ClientOnly extends true ? Layer.Layer< + Sharding | Runners.Runners | ("byo" extends Storage ? never : MessageStorage.MessageStorage), + ConfigError, + "local" extends Storage ? never + : "byo" extends Storage ? (MessageStorage.MessageStorage | RunnerStorage.RunnerStorage) + : SqlClient + > : + Layer.Layer< + Sharding | Runners.Runners | MessageStorage.MessageStorage, + ServeError | ConfigError, + "local" extends Storage ? never + : "byo" extends Storage ? (MessageStorage.MessageStorage | RunnerStorage.RunnerStorage) + : SqlClient + > => +{ + const layer: Layer.Layer = options.clientOnly + // client only + ? options.transport === "http" + ? Layer.provide(HttpRunner.layerHttpClientOnly, FetchHttpClient.layer) + : Layer.provide(HttpRunner.layerWebsocketClientOnly, BunSocket.layerWebSocketConstructor) + // with server + : options.transport === "http" + ? Layer.provide(HttpRunner.layerHttp, [layerHttpServer, FetchHttpClient.layer]) + : Layer.provide(HttpRunner.layerWebsocket, [layerHttpServer, BunSocket.layerWebSocketConstructor]) + + const runnerHealth: Layer.Layer = options?.clientOnly + ? Layer.empty as any + // TODO: when bun supports adding custom CA certificates + // : options?.runnerHealth === "k8s" + // ? RunnerHealth.layerK8s().pipe( + // Layer.provide([NodeFileSystem.layer, layerHttpClientK8s]) + // ) + : RunnerHealth.layerPing.pipe( + Layer.provide(Runners.layerRpc), + Layer.provide( + options.transport === "http" + ? HttpRunner.layerClientProtocolHttpDefault.pipe(Layer.provide(FetchHttpClient.layer)) + : HttpRunner.layerClientProtocolWebsocketDefault.pipe(Layer.provide(BunSocket.layerWebSocketConstructor)) + ) + ) + + return layer.pipe( + Layer.provide(runnerHealth), + Layer.provideMerge( + options?.storage === "local" + ? MessageStorage.layerNoop + : options?.storage === "byo" + ? Layer.empty + : Layer.orDie(SqlMessageStorage.layer) + ), + Layer.provide( + options?.storage === "local" + ? RunnerStorage.layerMemory + : options?.storage === "byo" + ? Layer.empty + : Layer.orDie(SqlRunnerStorage.layer) + ), + Layer.provide(ShardingConfig.layerFromEnv(options?.shardingConfig)), + Layer.provide( + options?.serialization === "ndjson" ? RpcSerialization.layerNdjson : RpcSerialization.layerMsgPack + ) + ) as any +} diff --git a/repos/effect/packages/platform-bun/src/BunClusterSocket.ts b/repos/effect/packages/platform-bun/src/BunClusterSocket.ts new file mode 100644 index 0000000..82599f6 --- /dev/null +++ b/repos/effect/packages/platform-bun/src/BunClusterSocket.ts @@ -0,0 +1,101 @@ +/** + * @since 1.0.0 + */ +import * as MessageStorage from "@effect/cluster/MessageStorage" +import * as RunnerHealth from "@effect/cluster/RunnerHealth" +import * as Runners from "@effect/cluster/Runners" +import * as RunnerStorage from "@effect/cluster/RunnerStorage" +import type { Sharding } from "@effect/cluster/Sharding" +import * as ShardingConfig from "@effect/cluster/ShardingConfig" +import * as SocketRunner from "@effect/cluster/SocketRunner" +import * as SqlMessageStorage from "@effect/cluster/SqlMessageStorage" +import * as SqlRunnerStorage from "@effect/cluster/SqlRunnerStorage" +import { layerClientProtocol, layerSocketServer } from "@effect/platform-node-shared/NodeClusterSocket" +import type * as SocketServer from "@effect/platform/SocketServer" +import * as RpcSerialization from "@effect/rpc/RpcSerialization" +import type { SqlClient } from "@effect/sql/SqlClient" +import type { ConfigError } from "effect/ConfigError" +import * as Layer from "effect/Layer" + +export { + /** + * @since 1.0.0 + * @category Re-exports + */ + layerClientProtocol, + /** + * @since 1.0.0 + * @category Re-exports + */ + layerSocketServer +} + +/** + * @since 1.0.0 + * @category Layers + */ +export const layer = < + const ClientOnly extends boolean = false, + const Storage extends "local" | "sql" | "byo" = never +>( + options?: { + readonly serialization?: "msgpack" | "ndjson" | undefined + readonly clientOnly?: ClientOnly | undefined + readonly storage?: Storage | undefined + readonly shardingConfig?: Partial | undefined + } +): ClientOnly extends true ? Layer.Layer< + Sharding | Runners.Runners | ("byo" extends Storage ? never : MessageStorage.MessageStorage), + ConfigError, + "local" extends Storage ? never + : "byo" extends Storage ? (MessageStorage.MessageStorage | RunnerStorage.RunnerStorage) + : SqlClient + > : + Layer.Layer< + Sharding | Runners.Runners | ("byo" extends Storage ? never : MessageStorage.MessageStorage), + SocketServer.SocketServerError | ConfigError, + "local" extends Storage ? never + : "byo" extends Storage ? (MessageStorage.MessageStorage | RunnerStorage.RunnerStorage) + : SqlClient + > => +{ + const layer: Layer.Layer = options?.clientOnly + // client only + ? Layer.provide(SocketRunner.layerClientOnly, layerClientProtocol) + // with server + : Layer.provide(SocketRunner.layer, [layerSocketServer, layerClientProtocol]) + + const runnerHealth: Layer.Layer = options?.clientOnly + ? Layer.empty as any + // TODO: when bun supports adding custom CA certificates + // : options?.runnerHealth === "k8s" + // ? RunnerHealth.layerK8s().pipe( + // Layer.provide([NodeFileSystem.layer, layerHttpClientK8s]) + // ) + : RunnerHealth.layerPing.pipe( + Layer.provide(Runners.layerRpc), + Layer.provide(layerClientProtocol) + ) + + return layer.pipe( + Layer.provide(runnerHealth), + Layer.provideMerge( + options?.storage === "local" + ? MessageStorage.layerNoop + : options?.storage === "byo" + ? Layer.empty + : Layer.orDie(SqlMessageStorage.layer) + ), + Layer.provide( + options?.storage === "local" + ? RunnerStorage.layerMemory + : options?.storage === "byo" + ? Layer.empty + : Layer.orDie(SqlRunnerStorage.layer) + ), + Layer.provide(ShardingConfig.layerFromEnv(options?.shardingConfig)), + Layer.provide( + options?.serialization === "ndjson" ? RpcSerialization.layerNdjson : RpcSerialization.layerMsgPack + ) + ) as any +} diff --git a/repos/effect/packages/platform-bun/src/BunCommandExecutor.ts b/repos/effect/packages/platform-bun/src/BunCommandExecutor.ts new file mode 100644 index 0000000..7e47b1c --- /dev/null +++ b/repos/effect/packages/platform-bun/src/BunCommandExecutor.ts @@ -0,0 +1,13 @@ +/** + * @since 1.0.0 + */ +import * as NodeCommandExecutor from "@effect/platform-node-shared/NodeCommandExecutor" +import type { CommandExecutor } from "@effect/platform/CommandExecutor" +import type { FileSystem } from "@effect/platform/FileSystem" +import type { Layer } from "effect/Layer" + +/** + * @since 1.0.0 + * @category layer + */ +export const layer: Layer = NodeCommandExecutor.layer diff --git a/repos/effect/packages/platform-bun/src/BunContext.ts b/repos/effect/packages/platform-bun/src/BunContext.ts new file mode 100644 index 0000000..cba341e --- /dev/null +++ b/repos/effect/packages/platform-bun/src/BunContext.ts @@ -0,0 +1,40 @@ +/** + * @since 1.0.0 + */ +import type * as CommandExecutor from "@effect/platform/CommandExecutor" +import type * as FileSystem from "@effect/platform/FileSystem" +import type * as Path from "@effect/platform/Path" +import type * as Terminal from "@effect/platform/Terminal" +import type * as Worker from "@effect/platform/Worker" +import { pipe } from "effect/Function" +import * as Layer from "effect/Layer" +import * as BunCommandExecutor from "./BunCommandExecutor.js" +import * as BunFileSystem from "./BunFileSystem.js" +import * as BunPath from "./BunPath.js" +import * as BunTerminal from "./BunTerminal.js" +import * as BunWorker from "./BunWorker.js" + +/** + * @since 1.0.0 + * @category models + */ +export type BunContext = + | CommandExecutor.CommandExecutor + | FileSystem.FileSystem + | Path.Path + | Terminal.Terminal + | Worker.WorkerManager + +/** + * @since 1.0.0 + * @category layer + */ +export const layer: Layer.Layer = pipe( + Layer.mergeAll( + BunPath.layer, + BunCommandExecutor.layer, + BunTerminal.layer, + BunWorker.layerManager + ), + Layer.provideMerge(BunFileSystem.layer) +) diff --git a/repos/effect/packages/platform-bun/src/BunFileSystem.ts b/repos/effect/packages/platform-bun/src/BunFileSystem.ts new file mode 100644 index 0000000..982eefa --- /dev/null +++ b/repos/effect/packages/platform-bun/src/BunFileSystem.ts @@ -0,0 +1,12 @@ +/** + * @since 1.0.0 + */ +import * as NodeFileSystem from "@effect/platform-node-shared/NodeFileSystem" +import type { FileSystem } from "@effect/platform/FileSystem" +import type { Layer } from "effect/Layer" + +/** + * @since 1.0.0 + * @category layer + */ +export const layer: Layer = NodeFileSystem.layer diff --git a/repos/effect/packages/platform-bun/src/BunFileSystem/ParcelWatcher.ts b/repos/effect/packages/platform-bun/src/BunFileSystem/ParcelWatcher.ts new file mode 100644 index 0000000..ce126e6 --- /dev/null +++ b/repos/effect/packages/platform-bun/src/BunFileSystem/ParcelWatcher.ts @@ -0,0 +1,12 @@ +/** + * @since 1.0.0 + */ +import * as ParcelWatcher from "@effect/platform-node-shared/NodeFileSystem/ParcelWatcher" +import type { WatchBackend } from "@effect/platform/FileSystem" +import type { Layer } from "effect/Layer" + +/** + * @since 1.0.0 + * @category layer + */ +export const layer: Layer = ParcelWatcher.layer diff --git a/repos/effect/packages/platform-bun/src/BunHttpPlatform.ts b/repos/effect/packages/platform-bun/src/BunHttpPlatform.ts new file mode 100644 index 0000000..b85156f --- /dev/null +++ b/repos/effect/packages/platform-bun/src/BunHttpPlatform.ts @@ -0,0 +1,21 @@ +/** + * @since 1.0.0 + */ +import type * as Etag from "@effect/platform/Etag" +import type * as FileSystem from "@effect/platform/FileSystem" +import type * as Platform from "@effect/platform/HttpPlatform" +import type * as Effect from "effect/Effect" +import type * as Layer from "effect/Layer" +import * as internal from "./internal/httpPlatform.js" + +/** + * @since 1.0.0 + * @category constructors + */ +export const make: Effect.Effect = internal.make + +/** + * @since 1.0.0 + * @category layers + */ +export const layer: Layer.Layer = internal.layer diff --git a/repos/effect/packages/platform-bun/src/BunHttpServer.ts b/repos/effect/packages/platform-bun/src/BunHttpServer.ts new file mode 100644 index 0000000..f3884e6 --- /dev/null +++ b/repos/effect/packages/platform-bun/src/BunHttpServer.ts @@ -0,0 +1,96 @@ +/** + * @since 1.0.0 + */ +import type * as Etag from "@effect/platform/Etag" +import type * as HttpClient from "@effect/platform/HttpClient" +import type * as Platform from "@effect/platform/HttpPlatform" +import type * as Server from "@effect/platform/HttpServer" +import type * as HttpServerError from "@effect/platform/HttpServerError" +import type * as Bun from "bun" +import type * as Config from "effect/Config" +import type * as ConfigError from "effect/ConfigError" +import type * as Effect from "effect/Effect" +import type * as Layer from "effect/Layer" +import type * as Scope from "effect/Scope" +import type * as BunContext from "./BunContext.js" +import * as internal from "./internal/httpServer.js" + +/** + * @since 1.0.0 + * @category Options + */ +export type ServeOptions> }> = + & ( + | Omit + | Bun.TLSServeOptions + | Bun.UnixServeOptions + | Bun.UnixTLSServeOptions + ) + & { readonly routes?: R } + +/** + * @since 1.0.0 + * @category constructors + */ +export const make: > } = {}>( + options: ServeOptions +) => Effect.Effect = internal.make + +/** + * @since 1.0.0 + * @category layers + */ +export const layerServer: > } = {}>( + options: ServeOptions +) => Layer.Layer = internal.layerServer + +/** + * @since 1.0.0 + * @category layers + */ +export const layer: > } = {}>( + options: ServeOptions +) => Layer.Layer = internal.layer + +/** + * Layer starting a server on a random port and producing an `HttpClient` + * with prepended url of the running http server. + * + * @since 1.0.0 + * @category layers + */ +export const layerTest: Layer.Layer< + | HttpClient.HttpClient + | Server.HttpServer + | Platform.HttpPlatform + | Etag.Generator + | BunContext.BunContext, + HttpServerError.ServeError +> = internal.layerTest + +/** + * @since 1.0.0 + * @category layers + */ +export const layerConfig: > } = {}>( + options: Config.Config.Wrap> +) => Layer.Layer< + Server.HttpServer | Platform.HttpPlatform | Etag.Generator | BunContext.BunContext, + ConfigError.ConfigError +> = internal.layerConfig + +/** + * A Layer providing the `HttpPlatform`, `FileSystem`, `Etag.Generator`, and `Path` + * services. + * + * The `FileSystem` service is a no-op implementation, so this layer is only + * useful for platforms that have no file system. + * + * @since 1.0.0 + * @category layers + */ +export const layerContext: Layer.Layer< + | Platform.HttpPlatform + | Etag.Generator + | BunContext.BunContext +> = internal.layerContext diff --git a/repos/effect/packages/platform-bun/src/BunHttpServerRequest.ts b/repos/effect/packages/platform-bun/src/BunHttpServerRequest.ts new file mode 100644 index 0000000..72560ce --- /dev/null +++ b/repos/effect/packages/platform-bun/src/BunHttpServerRequest.ts @@ -0,0 +1,11 @@ +/** + * @since 1.0.0 + */ +import type * as ServerRequest from "@effect/platform/HttpServerRequest" +import * as internal from "./internal/httpServer.js" + +/** + * @category conversions + * @since 1.0.0 + */ +export const toRequest: (self: ServerRequest.HttpServerRequest) => Request = internal.requestSource diff --git a/repos/effect/packages/platform-bun/src/BunKeyValueStore.ts b/repos/effect/packages/platform-bun/src/BunKeyValueStore.ts new file mode 100644 index 0000000..bf8f3a3 --- /dev/null +++ b/repos/effect/packages/platform-bun/src/BunKeyValueStore.ts @@ -0,0 +1,15 @@ +/** + * @since 1.0.0 + */ +import * as KVSN from "@effect/platform-node-shared/NodeKeyValueStore" +import type * as PlatformError from "@effect/platform/Error" +import type * as KeyValueStore from "@effect/platform/KeyValueStore" +import type * as Layer from "effect/Layer" + +/** + * @since 1.0.0 + * @category layers + */ +export const layerFileSystem: ( + directory: string +) => Layer.Layer = KVSN.layerFileSystem diff --git a/repos/effect/packages/platform-bun/src/BunMultipart.ts b/repos/effect/packages/platform-bun/src/BunMultipart.ts new file mode 100644 index 0000000..e3ad05b --- /dev/null +++ b/repos/effect/packages/platform-bun/src/BunMultipart.ts @@ -0,0 +1,25 @@ +/** + * @since 1.0.0 + */ +import type * as FileSystem from "@effect/platform/FileSystem" +import type * as Multipart from "@effect/platform/Multipart" +import type * as Path from "@effect/platform/Path" +import type * as Effect from "effect/Effect" +import type * as Scope from "effect/Scope" +import type * as Stream from "effect/Stream" +import * as internal from "./internal/multipart.js" + +/** + * @since 1.0.0 + * @category constructors + */ +export const stream: (source: Request) => Stream.Stream = internal.stream + +/** + * @since 1.0.0 + * @category constructors + */ +export const persisted: ( + source: Request +) => Effect.Effect = + internal.persisted diff --git a/repos/effect/packages/platform-bun/src/BunPath.ts b/repos/effect/packages/platform-bun/src/BunPath.ts new file mode 100644 index 0000000..1097c6d --- /dev/null +++ b/repos/effect/packages/platform-bun/src/BunPath.ts @@ -0,0 +1,25 @@ +/** + * @since 1.0.0 + */ + +import * as NodePath from "@effect/platform-node-shared/NodePath" +import type { Path } from "@effect/platform/Path" +import type { Layer } from "effect/Layer" + +/** + * @since 1.0.0 + * @category layer + */ +export const layer: Layer = NodePath.layer + +/** + * @since 1.0.0 + * @category layer + */ +export const layerPosix: Layer = NodePath.layerPosix + +/** + * @since 1.0.0 + * @category layer + */ +export const layerWin32: Layer = NodePath.layerWin32 diff --git a/repos/effect/packages/platform-bun/src/BunRuntime.ts b/repos/effect/packages/platform-bun/src/BunRuntime.ts new file mode 100644 index 0000000..d6c012f --- /dev/null +++ b/repos/effect/packages/platform-bun/src/BunRuntime.ts @@ -0,0 +1,11 @@ +/** + * @since 1.0.0 + */ +import * as NodeRuntime from "@effect/platform-node-shared/NodeRuntime" +import type { RunMain } from "@effect/platform/Runtime" + +/** + * @since 1.0.0 + * @category runtime + */ +export const runMain: RunMain = NodeRuntime.runMain diff --git a/repos/effect/packages/platform-bun/src/BunSink.ts b/repos/effect/packages/platform-bun/src/BunSink.ts new file mode 100644 index 0000000..78c09ed --- /dev/null +++ b/repos/effect/packages/platform-bun/src/BunSink.ts @@ -0,0 +1,8 @@ +/** + * @since 1.0.0 + */ + +/** + * @since 1.0.0 + */ +export * from "@effect/platform-node-shared/NodeSink" diff --git a/repos/effect/packages/platform-bun/src/BunSocket.ts b/repos/effect/packages/platform-bun/src/BunSocket.ts new file mode 100644 index 0000000..e4f6f19 --- /dev/null +++ b/repos/effect/packages/platform-bun/src/BunSocket.ts @@ -0,0 +1,30 @@ +/** + * @since 1.0.0 + */ +import * as Socket from "@effect/platform/Socket" +import * as Layer from "effect/Layer" + +/** + * @since 1.0.0 + */ +export * from "@effect/platform-node-shared/NodeSocket" + +/** + * @since 1.0.0 + * @category layers + */ +export const layerWebSocket = (url: string, options?: { + readonly closeCodeIsError?: (code: number) => boolean +}): Layer.Layer => + Layer.scoped(Socket.Socket, Socket.makeWebSocket(url, options)).pipe( + Layer.provide(layerWebSocketConstructor) + ) + +/** + * @since 1.0.0 + * @category layers + */ +export const layerWebSocketConstructor: Layer.Layer = Layer.succeed( + Socket.WebSocketConstructor, + (url, protocols) => new globalThis.WebSocket(url, protocols) +) diff --git a/repos/effect/packages/platform-bun/src/BunSocketServer.ts b/repos/effect/packages/platform-bun/src/BunSocketServer.ts new file mode 100644 index 0000000..3c7ec9e --- /dev/null +++ b/repos/effect/packages/platform-bun/src/BunSocketServer.ts @@ -0,0 +1,8 @@ +/** + * @since 1.0.0 + */ + +/** + * @since 1.0.0 + */ +export * from "@effect/platform-node-shared/NodeSocketServer" diff --git a/repos/effect/packages/platform-bun/src/BunStream.ts b/repos/effect/packages/platform-bun/src/BunStream.ts new file mode 100644 index 0000000..4b55a58 --- /dev/null +++ b/repos/effect/packages/platform-bun/src/BunStream.ts @@ -0,0 +1,8 @@ +/** + * @since 1.0.0 + */ + +/** + * @since 1.0.0 + */ +export * from "@effect/platform-node-shared/NodeStream" diff --git a/repos/effect/packages/platform-bun/src/BunTerminal.ts b/repos/effect/packages/platform-bun/src/BunTerminal.ts new file mode 100644 index 0000000..e128394 --- /dev/null +++ b/repos/effect/packages/platform-bun/src/BunTerminal.ts @@ -0,0 +1,20 @@ +/** + * @since 1.0.0 + */ +import * as NodeTerminal from "@effect/platform-node-shared/NodeTerminal" +import type { Terminal, UserInput } from "@effect/platform/Terminal" +import type { Effect } from "effect/Effect" +import type { Layer } from "effect/Layer" +import type { Scope } from "effect/Scope" + +/** + * @since 1.0.0 + * @category constructors + */ +export const make: (shouldQuit?: (input: UserInput) => boolean) => Effect = NodeTerminal.make + +/** + * @since 1.0.0 + * @category layer + */ +export const layer: Layer = NodeTerminal.layer diff --git a/repos/effect/packages/platform-bun/src/BunWorker.ts b/repos/effect/packages/platform-bun/src/BunWorker.ts new file mode 100644 index 0000000..36357a2 --- /dev/null +++ b/repos/effect/packages/platform-bun/src/BunWorker.ts @@ -0,0 +1,34 @@ +/** + * @since 1.0.0 + */ +import type * as Worker from "@effect/platform/Worker" +import type * as Layer from "effect/Layer" +import * as internal from "./internal/worker.js" + +/** + * @since 1.0.0 + * @category layers + */ +export const layerManager: Layer.Layer = internal.layerManager + +/** + * @since 1.0.0 + * @category layers + */ +export const layerWorker: Layer.Layer = internal.layerWorker + +/** + * @since 1.0.0 + * @category layers + */ +export const layer: ( + spawn: (id: number) => Worker +) => Layer.Layer = internal.layer + +/** + * @since 1.0.0 + * @category layers + */ +export const layerPlatform: ( + spawn: (id: number) => globalThis.Worker +) => Layer.Layer = internal.layerPlatform diff --git a/repos/effect/packages/platform-bun/src/BunWorkerRunner.ts b/repos/effect/packages/platform-bun/src/BunWorkerRunner.ts new file mode 100644 index 0000000..4788e62 --- /dev/null +++ b/repos/effect/packages/platform-bun/src/BunWorkerRunner.ts @@ -0,0 +1,20 @@ +/** + * @since 1.0.0 + */ +import type * as Runner from "@effect/platform/WorkerRunner" +import type * as Layer from "effect/Layer" +import * as internal from "./internal/workerRunner.js" + +export { + /** + * @since 1.0.0 + * @category re-exports + */ + launch +} from "@effect/platform/WorkerRunner" + +/** + * @since 1.0.0 + * @category layers + */ +export const layer: Layer.Layer = internal.layer diff --git a/repos/effect/packages/platform-bun/src/index.ts b/repos/effect/packages/platform-bun/src/index.ts new file mode 100644 index 0000000..c14781e --- /dev/null +++ b/repos/effect/packages/platform-bun/src/index.ts @@ -0,0 +1,94 @@ +/** + * @since 1.0.0 + */ +export * as BunClusterHttp from "./BunClusterHttp.js" + +/** + * @since 1.0.0 + */ +export * as BunClusterSocket from "./BunClusterSocket.js" + +/** + * @since 1.0.0 + */ +export * as BunCommandExecutor from "./BunCommandExecutor.js" + +/** + * @since 1.0.0 + */ +export * as BunContext from "./BunContext.js" + +/** + * @since 1.0.0 + */ +export * as BunFileSystem from "./BunFileSystem.js" + +/** + * @since 1.0.0 + */ +export * as BunHttpPlatform from "./BunHttpPlatform.js" + +/** + * @since 1.0.0 + */ +export * as BunHttpServer from "./BunHttpServer.js" + +/** + * @since 1.0.0 + */ +export * as BunHttpServerRequest from "./BunHttpServerRequest.js" + +/** + * @since 1.0.0 + */ +export * as BunKeyValueStore from "./BunKeyValueStore.js" + +/** + * @since 1.0.0 + */ +export * as BunMultipart from "./BunMultipart.js" + +/** + * @since 1.0.0 + */ +export * as BunPath from "./BunPath.js" + +/** + * @since 1.0.0 + */ +export * as BunRuntime from "./BunRuntime.js" + +/** + * @since 1.0.0 + */ +export * as BunSink from "./BunSink.js" + +/** + * @since 1.0.0 + */ +export * as BunSocket from "./BunSocket.js" + +/** + * @since 1.0.0 + */ +export * as BunSocketServer from "./BunSocketServer.js" + +/** + * @since 1.0.0 + */ +export * as BunStream from "./BunStream.js" + +/** + * @since 1.0.0 + */ +export * as BunTerminal from "./BunTerminal.js" + +/** + * @since 1.0.0 + */ +export * as BunWorker from "./BunWorker.js" + +/** + * @since 1.0.0 + */ +export * as BunWorkerRunner from "./BunWorkerRunner.js" diff --git a/repos/effect/packages/platform-bun/src/internal/httpPlatform.ts b/repos/effect/packages/platform-bun/src/internal/httpPlatform.ts new file mode 100644 index 0000000..1c1eede --- /dev/null +++ b/repos/effect/packages/platform-bun/src/internal/httpPlatform.ts @@ -0,0 +1,25 @@ +import * as FileSystem from "@effect/platform-node-shared/NodeFileSystem" +import * as Etag from "@effect/platform/Etag" +import * as Platform from "@effect/platform/HttpPlatform" +import * as ServerResponse from "@effect/platform/HttpServerResponse" +import * as Layer from "effect/Layer" + +/** @internal */ +export const make = Platform.make({ + fileResponse(path, status, statusText, headers, start, end, _contentLength) { + let file = Bun.file(path) + if (start > 0 || end !== undefined) { + file = file.slice(start, end) + } + return ServerResponse.raw(file, { headers, status, statusText }) + }, + fileWebResponse(file, status, statusText, headers, _options) { + return ServerResponse.raw(file, { headers, status, statusText }) + } +}) + +/** @internal */ +export const layer = Layer.effect(Platform.HttpPlatform, make).pipe( + Layer.provide(FileSystem.layer), + Layer.provide(Etag.layer) +) diff --git a/repos/effect/packages/platform-bun/src/internal/httpServer.ts b/repos/effect/packages/platform-bun/src/internal/httpServer.ts new file mode 100644 index 0000000..bee77f1 --- /dev/null +++ b/repos/effect/packages/platform-bun/src/internal/httpServer.ts @@ -0,0 +1,478 @@ +import * as Cookies from "@effect/platform/Cookies" +import * as Etag from "@effect/platform/Etag" +import * as FetchHttpClient from "@effect/platform/FetchHttpClient" +import type * as FileSystem from "@effect/platform/FileSystem" +import * as Headers from "@effect/platform/Headers" +import * as App from "@effect/platform/HttpApp" +import * as IncomingMessage from "@effect/platform/HttpIncomingMessage" +import type { HttpMethod } from "@effect/platform/HttpMethod" +import * as Server from "@effect/platform/HttpServer" +import * as Error from "@effect/platform/HttpServerError" +import * as ServerRequest from "@effect/platform/HttpServerRequest" +import type * as ServerResponse from "@effect/platform/HttpServerResponse" +import type * as Multipart from "@effect/platform/Multipart" +import type * as Path from "@effect/platform/Path" +import * as Socket from "@effect/platform/Socket" +import * as UrlParams from "@effect/platform/UrlParams" +import type { Server as BunServer, ServerWebSocket, WebSocketServeOptions } from "bun" +import * as Config from "effect/Config" +import * as Deferred from "effect/Deferred" +import * as Effect from "effect/Effect" +import * as Exit from "effect/Exit" +import * as FiberSet from "effect/FiberSet" +import * as Inspectable from "effect/Inspectable" +import * as Layer from "effect/Layer" +import * as Option from "effect/Option" +import type { ReadonlyRecord } from "effect/Record" +import type * as Runtime from "effect/Runtime" +import type * as Scope from "effect/Scope" +import * as Stream from "effect/Stream" +import * as BunContext from "../BunContext.js" +import * as Platform from "../BunHttpPlatform.js" +import type * as BunHttpServer from "../BunHttpServer.js" +import * as MultipartBun from "./multipart.js" + +/** @internal */ +export const make = > } = {}>( + options: BunHttpServer.ServeOptions +): Effect.Effect => + Effect.gen(function*() { + const handlerStack: Array<(request: Request, server: BunServer) => Response | Promise> = [ + function(_request, _server) { + return new Response("not found", { status: 404 }) + } + ] + const server = Bun.serve({ + ...options as WebSocketServeOptions, + fetch: handlerStack[0], + websocket: { + open(ws) { + Deferred.unsafeDone(ws.data.deferred, Exit.succeed(ws)) + }, + message(ws, message) { + ws.data.run(message) + }, + close(ws, code, closeReason) { + Deferred.unsafeDone( + ws.data.closeDeferred, + Socket.defaultCloseCodeIsError(code) + ? Exit.fail(new Socket.SocketCloseError({ reason: "Close", code, closeReason })) + : Exit.void + ) + } + } + }) + + yield* Effect.addFinalizer(() => + Effect.sync(() => { + server.stop() + }) + ) + + return Server.make({ + address: { _tag: "TcpAddress", port: server.port!, hostname: server.hostname! }, + serve(httpApp, middleware) { + return Effect.gen(function*() { + const runFork = yield* FiberSet.makeRuntime() + const runtime = yield* Effect.runtime() + const app = App.toHandled(httpApp, (request, response) => + Effect.sync(() => { + ;(request as ServerRequestImpl).resolve(makeResponse(request, response, runtime)) + }), middleware) + + function handler(request: Request, server: BunServer) { + return new Promise((resolve, _reject) => { + const fiber = runFork(Effect.provideService( + app, + ServerRequest.HttpServerRequest, + new ServerRequestImpl(request, resolve, removeHost(request.url), server) + )) + request.signal.addEventListener("abort", () => { + runFork(fiber.interruptAsFork(Error.clientAbortFiberId)) + }, { once: true }) + }) + } + + yield* Effect.acquireRelease( + Effect.sync(() => { + handlerStack.push(handler) + server.reload({ fetch: handler }) + }), + () => + Effect.sync(() => { + handlerStack.pop() + server.reload({ fetch: handlerStack[handlerStack.length - 1] }) + }) + ) + }) + } + }) + }) + +const makeResponse = ( + request: ServerRequest.HttpServerRequest, + response: ServerResponse.HttpServerResponse, + runtime: Runtime.Runtime +): Response => { + const fields: { + headers: globalThis.Headers + status?: number + statusText?: string + } = { + headers: new globalThis.Headers(response.headers), + status: response.status + } + + if (!Cookies.isEmpty(response.cookies)) { + for (const header of Cookies.toSetCookieHeaders(response.cookies)) { + fields.headers.append("set-cookie", header) + } + } + + if (response.statusText !== undefined) { + fields.statusText = response.statusText + } + + if (request.method === "HEAD") { + return new Response(undefined, fields) + } + response = App.unsafeEjectStreamScope(response) + const body = response.body + switch (body._tag) { + case "Empty": { + return new Response(undefined, fields) + } + case "Uint8Array": + case "Raw": { + if (body.body instanceof Response) { + for (const [key, value] of fields.headers.entries()) { + body.body.headers.set(key, value) + } + return body.body + } + return new Response(body.body as any, fields) + } + case "FormData": { + return new Response(body.formData as any, fields) + } + case "Stream": { + return new Response( + Stream.toReadableStreamRuntime(body.stream, runtime), + fields + ) + } + } +} + +/** @internal */ +export const layerServer = > } = {}>( + options: BunHttpServer.ServeOptions +) => Layer.scoped(Server.HttpServer, make(options)) + +/** @internal */ +export const layerContext = Layer.mergeAll( + Platform.layer, + Etag.layerWeak, + BunContext.layer +) + +/** @internal */ +export const layer = > } = {}>( + options: BunHttpServer.ServeOptions +) => + Layer.mergeAll( + Layer.scoped(Server.HttpServer, make(options)), + layerContext + ) + +/** @internal */ +export const layerTest = Server.layerTestClient.pipe( + Layer.provide(FetchHttpClient.layer.pipe( + Layer.provide(Layer.succeed(FetchHttpClient.RequestInit, { keepalive: false })) + )), + Layer.provideMerge(layer({ port: 0 })) +) + +/** @internal */ +export const layerConfig = > } = {}>( + options: Config.Config.Wrap> +) => + Layer.mergeAll( + Layer.scoped(Server.HttpServer, Effect.flatMap(Config.unwrap(options), make)), + layerContext + ) + +interface WebSocketContext { + readonly deferred: Deferred.Deferred> + readonly closeDeferred: Deferred.Deferred + readonly buffer: Array + run: (_: Uint8Array | string) => void +} + +function wsDefaultRun(this: WebSocketContext, _: Uint8Array | string) { + this.buffer.push(_) +} + +class ServerRequestImpl extends Inspectable.Class implements ServerRequest.HttpServerRequest { + readonly [ServerRequest.TypeId]: ServerRequest.TypeId + readonly [IncomingMessage.TypeId]: IncomingMessage.TypeId + constructor( + readonly source: Request, + public resolve: (response: Response) => void, + readonly url: string, + private bunServer: BunServer, + public headersOverride?: Headers.Headers, + private remoteAddressOverride?: string + ) { + super() + this[ServerRequest.TypeId] = ServerRequest.TypeId + this[IncomingMessage.TypeId] = IncomingMessage.TypeId + } + toJSON(): unknown { + return IncomingMessage.inspect(this, { + _id: "@effect/platform/HttpServerRequest", + method: this.method, + url: this.originalUrl + }) + } + modify( + options: { + readonly url?: string | undefined + readonly headers?: Headers.Headers | undefined + readonly remoteAddress?: string | undefined + } + ) { + return new ServerRequestImpl( + this.source, + this.resolve, + options.url ?? this.url, + this.bunServer, + options.headers ?? this.headersOverride, + options.remoteAddress ?? this.remoteAddressOverride + ) + } + get method(): HttpMethod { + return this.source.method.toUpperCase() as HttpMethod + } + get originalUrl() { + return this.source.url + } + get remoteAddress(): Option.Option { + return this.remoteAddressOverride + ? Option.some(this.remoteAddressOverride) + : Option.fromNullable(this.bunServer.requestIP(this.source)?.address) + } + get headers(): Headers.Headers { + this.headersOverride ??= Headers.fromInput(this.source.headers) + return this.headersOverride + } + + private cachedCookies: ReadonlyRecord | undefined + get cookies() { + if (this.cachedCookies) { + return this.cachedCookies + } + return this.cachedCookies = Cookies.parseHeader(this.headers.cookie ?? "") + } + + get stream(): Stream.Stream { + return this.source.body + ? Stream.fromReadableStream(() => this.source.body as any, (cause) => + new Error.RequestError({ + request: this, + reason: "Decode", + cause + })) + : Stream.fail( + new Error.RequestError({ + request: this, + reason: "Decode", + description: "can not create stream from empty body" + }) + ) + } + + private textEffect: Effect.Effect | undefined + get text(): Effect.Effect { + if (this.textEffect) { + return this.textEffect + } + this.textEffect = Effect.runSync(Effect.cached( + Effect.tryPromise({ + try: () => this.source.text(), + catch: (cause) => + new Error.RequestError({ + request: this, + reason: "Decode", + cause + }) + }) + )) + return this.textEffect + } + + get json(): Effect.Effect { + return Effect.tryMap(this.text, { + try: (_) => JSON.parse(_) as unknown, + catch: (cause) => + new Error.RequestError({ + request: this, + reason: "Decode", + cause + }) + }) + } + + get urlParamsBody(): Effect.Effect { + return Effect.flatMap(this.text, (_) => + Effect.try({ + try: () => UrlParams.fromInput(new URLSearchParams(_)), + catch: (cause) => + new Error.RequestError({ + request: this, + reason: "Decode", + cause + }) + })) + } + + private multipartEffect: + | Effect.Effect< + Multipart.Persisted, + Multipart.MultipartError, + Scope.Scope | FileSystem.FileSystem | Path.Path + > + | undefined + get multipart(): Effect.Effect< + Multipart.Persisted, + Multipart.MultipartError, + Scope.Scope | FileSystem.FileSystem | Path.Path + > { + if (this.multipartEffect) { + return this.multipartEffect + } + this.multipartEffect = Effect.runSync(Effect.cached( + MultipartBun.persisted(this.source) + )) + return this.multipartEffect + } + + get multipartStream(): Stream.Stream { + return MultipartBun.stream(this.source) + } + + private arrayBufferEffect: Effect.Effect | undefined + get arrayBuffer(): Effect.Effect { + if (this.arrayBufferEffect) { + return this.arrayBufferEffect + } + this.arrayBufferEffect = Effect.runSync(Effect.cached( + Effect.tryPromise({ + try: () => this.source.arrayBuffer(), + catch: (cause) => + new Error.RequestError({ + request: this, + reason: "Decode", + cause + }) + }) + )) + return this.arrayBufferEffect + } + + get upgrade(): Effect.Effect { + return Effect.flatMap( + Effect.all([ + Deferred.make>(), + Deferred.make(), + Effect.makeSemaphore(1) + ]), + ([deferred, closeDeferred, semaphore]) => + Effect.async((resume) => { + const success = this.bunServer.upgrade(this.source, { + data: { + deferred, + closeDeferred, + buffer: [], + run: wsDefaultRun + } + }) + if (!success) { + resume(Effect.fail( + new Error.RequestError({ + request: this, + reason: "Decode", + description: "Not an upgradeable ServerRequest" + }) + )) + return + } + resume(Effect.map(Deferred.await(deferred), (ws) => { + const write = (chunk: Uint8Array | string | Socket.CloseEvent) => + Effect.sync(() => { + if (typeof chunk === "string") { + ws.sendText(chunk) + } else if (Socket.isCloseEvent(chunk)) { + ws.close(chunk.code, chunk.reason) + } else { + ws.sendBinary(chunk) + } + + return true + }) + const writer = Effect.succeed(write) + const runRaw = Effect.fnUntraced( + function*( + handler: (_: Uint8Array | string) => Effect.Effect<_, E, R> | void, + opts?: { readonly onOpen?: Effect.Effect | undefined } + ) { + const set = yield* FiberSet.make() + const run = yield* FiberSet.runtime(set)() + function runRaw(data: Uint8Array | string) { + const result = handler(data) + if (Effect.isEffect(result)) { + run(result) + } + } + ws.data.run = runRaw + ws.data.buffer.forEach(runRaw) + ws.data.buffer.length = 0 + if (opts?.onOpen) yield* opts.onOpen + return yield* FiberSet.join(set) + }, + Effect.scoped, + Effect.onExit((exit) => { + ws.close(exit._tag === "Success" ? 1000 : 1011) + return Effect.void + }), + Effect.raceFirst(Deferred.await(closeDeferred)), + semaphore.withPermits(1) + ) + + const encoder = new TextEncoder() + const run = (handler: (_: Uint8Array) => Effect.Effect<_, E, R> | void, opts?: { + readonly onOpen?: Effect.Effect | undefined + }) => runRaw((data) => typeof data === "string" ? handler(encoder.encode(data)) : handler(data), opts) + + return Socket.Socket.of({ + [Socket.TypeId]: Socket.TypeId, + run, + runRaw, + writer + }) + })) + }) + ) + } +} + +const removeHost = (url: string) => { + if (url[0] === "/") { + return url + } + const index = url.indexOf("/", url.indexOf("//") + 2) + return index === -1 ? "/" : url.slice(index) +} + +/** @internal */ +export const requestSource = (self: ServerRequest.HttpServerRequest) => (self as ServerRequestImpl).source diff --git a/repos/effect/packages/platform-bun/src/internal/multipart.ts b/repos/effect/packages/platform-bun/src/internal/multipart.ts new file mode 100644 index 0000000..e14babd --- /dev/null +++ b/repos/effect/packages/platform-bun/src/internal/multipart.ts @@ -0,0 +1,149 @@ +import * as Multipart from "@effect/platform/Multipart" +import * as Channel from "effect/Channel" +import * as Effect from "effect/Effect" +import { pipe } from "effect/Function" +import * as Inspectable from "effect/Inspectable" +import * as Stream from "effect/Stream" +import type { MultipartError, PartInfo } from "multipasta" +import { decodeField } from "multipasta" +import * as MP from "multipasta/web" + +/** @internal */ +export const stream = (source: Request): Stream.Stream => + pipe( + Multipart.makeConfig({}), + Effect.map((config) => { + const parser = MP.make({ + ...config, + headers: source.headers + }) + return Stream.fromReadableStream( + () => source.body!.pipeThrough(parser), + (cause) => convertError(cause as MultipartError) + ) + }), + Stream.unwrap, + Stream.map(convertPart) + ) + +/** @internal */ +export const persisted = (source: Request) => + Multipart.toPersisted(stream(source), (path, file) => + Effect.tryPromise({ + try: async () => { + const fileImpl = file as FileImpl + const writer = Bun.file(path).writer() + const reader = fileImpl.file.readable.getReader() + try { + while (true) { + const { done, value } = await reader.readMany() + if (done) break + for (const chunk of value) { + writer.write(chunk) + } + await writer.flush() + } + } finally { + reader.cancel() + await writer.end() + } + }, + catch: (cause) => new Multipart.MultipartError({ reason: "InternalError", cause }) + })) + +const convertPart = (part: MP.Part): Multipart.Part => + part._tag === "Field" ? new FieldImpl(part.info, part.value) : new FileImpl(part) + +abstract class PartBase extends Inspectable.Class { + readonly [Multipart.TypeId]: Multipart.TypeId + constructor() { + super() + this[Multipart.TypeId] = Multipart.TypeId + } +} + +class FieldImpl extends PartBase implements Multipart.Field { + readonly _tag = "Field" + readonly key: string + readonly contentType: string + readonly value: string + + constructor( + info: PartInfo, + value: Uint8Array + ) { + super() + this.key = info.name + this.contentType = info.contentType + this.value = decodeField(info, value) + } + + toJSON(): unknown { + return { + _id: "@effect/platform/Multipart/Part", + _tag: "Field", + key: this.key, + value: this.value, + contentType: this.contentType + } + } +} + +class FileImpl extends PartBase implements Multipart.File { + readonly _tag = "File" + readonly key: string + readonly name: string + readonly contentType: string + readonly content: Stream.Stream + readonly contentEffect: Effect.Effect + + constructor(readonly file: MP.File) { + super() + this.key = file.info.name + this.name = file.info.filename ?? file.info.name + this.contentType = file.info.contentType + this.content = Stream.fromReadableStream( + () => file.readable, + (cause) => new Multipart.MultipartError({ reason: "InternalError", cause }) + ) + this.contentEffect = Stream.toChannel(this.content).pipe( + Channel.pipeTo(Multipart.collectUint8Array), + Channel.run, + Effect.mapError((cause) => new Multipart.MultipartError({ reason: "InternalError", cause })) + ) + } + + toJSON(): unknown { + return { + _id: "@effect/platform/Multipart/Part", + _tag: "File", + key: this.key, + name: this.name, + contentType: this.contentType + } + } +} + +function convertError(cause: MultipartError): Multipart.MultipartError { + switch (cause._tag) { + case "ReachedLimit": { + switch (cause.limit) { + case "MaxParts": { + return new Multipart.MultipartError({ reason: "TooManyParts", cause }) + } + case "MaxFieldSize": { + return new Multipart.MultipartError({ reason: "FieldTooLarge", cause }) + } + case "MaxPartSize": { + return new Multipart.MultipartError({ reason: "FileTooLarge", cause }) + } + case "MaxTotalSize": { + return new Multipart.MultipartError({ reason: "BodyTooLarge", cause }) + } + } + } + default: { + return new Multipart.MultipartError({ reason: "Parse", cause }) + } + } +} diff --git a/repos/effect/packages/platform-bun/src/internal/worker.ts b/repos/effect/packages/platform-bun/src/internal/worker.ts new file mode 100644 index 0000000..ebb0677 --- /dev/null +++ b/repos/effect/packages/platform-bun/src/internal/worker.ts @@ -0,0 +1,68 @@ +import * as Worker from "@effect/platform/Worker" +import { WorkerError } from "@effect/platform/WorkerError" +import * as Deferred from "effect/Deferred" +import * as Effect from "effect/Effect" +import * as Exit from "effect/Exit" +import * as Layer from "effect/Layer" +import * as Scope from "effect/Scope" + +const platformWorkerImpl = Worker.makePlatform()({ + setup({ scope, worker }) { + return Effect.flatMap(Deferred.make(), (closeDeferred) => { + worker.addEventListener("close", () => { + Deferred.unsafeDone(closeDeferred, Exit.void) + }) + return Effect.as( + Scope.addFinalizer( + scope, + Effect.suspend(() => { + worker.postMessage([1]) + return Deferred.await(closeDeferred) + }).pipe( + Effect.interruptible, + Effect.timeout(5000), + Effect.catchAllCause(() => Effect.sync(() => worker.terminate())) + ) + ), + worker + ) + }) + }, + listen({ deferred, emit, port, scope }) { + function onMessage(event: MessageEvent) { + emit(event.data) + } + function onError(event: ErrorEvent) { + Deferred.unsafeDone( + deferred, + new WorkerError({ reason: "unknown", cause: event.error ?? event.message }) + ) + } + port.addEventListener("message", onMessage) + port.addEventListener("error", onError) + return Scope.addFinalizer( + scope, + Effect.sync(() => { + port.removeEventListener("message", onMessage) + port.removeEventListener("error", onError) + }) + ) + } +}) + +/** @internal */ +export const layerWorker = Layer.succeed(Worker.PlatformWorker, platformWorkerImpl) + +/** @internal */ +export const layerManager = Layer.provide(Worker.layerManager, layerWorker) + +/** @internal */ +export const layer = (spawn: (id: number) => globalThis.Worker) => + Layer.merge( + layerManager, + Worker.layerSpawner(spawn) + ) + +/** @internal */ +export const layerPlatform = (spawn: (id: number) => globalThis.Worker) => + Layer.merge(layerWorker, Worker.layerSpawner(spawn)) diff --git a/repos/effect/packages/platform-bun/src/internal/workerRunner.ts b/repos/effect/packages/platform-bun/src/internal/workerRunner.ts new file mode 100644 index 0000000..5ddacfc --- /dev/null +++ b/repos/effect/packages/platform-bun/src/internal/workerRunner.ts @@ -0,0 +1,87 @@ +import { WorkerError } from "@effect/platform/WorkerError" +import * as Runner from "@effect/platform/WorkerRunner" +import * as Cause from "effect/Cause" +import * as Context from "effect/Context" +import * as Deferred from "effect/Deferred" +import * as Effect from "effect/Effect" +import * as Exit from "effect/Exit" +import * as FiberSet from "effect/FiberSet" +import * as Layer from "effect/Layer" +import * as Runtime from "effect/Runtime" +import * as Scope from "effect/Scope" + +declare const self: MessagePort + +const platformRunnerImpl = Runner.PlatformRunner.of({ + [Runner.PlatformRunnerTypeId]: Runner.PlatformRunnerTypeId, + start: Effect.fnUntraced(function*(closeLatch: Deferred.Deferred) { + if (!("postMessage" in self)) { + return yield* new WorkerError({ reason: "spawn", cause: new Error("not in a Worker context") }) + } + const port = self + const run = Effect.fnUntraced(function*( + handler: (portId: number, message: any) => Effect.Effect | void + ) { + const scope = yield* Effect.scope + const runtime = (yield* Effect.runtime().pipe( + Effect.interruptible + )).pipe( + Runtime.updateContext(Context.omit(Scope.Scope)) + ) as Runtime.Runtime + const fiberSet = yield* FiberSet.make() + const runFork = Runtime.runFork(runtime) + const onExit = (exit: Exit.Exit) => { + if (exit._tag === "Failure" && !Cause.isInterruptedOnly(exit.cause)) { + Deferred.unsafeDone(closeLatch, Exit.die(Cause.squash(exit.cause))) + } + } + + function onMessage(event: MessageEvent) { + const message = (event as MessageEvent).data as Runner.BackingRunner.Message + if (message[0] === 0) { + const result = handler(0, message[1]) + if (Effect.isEffect(result)) { + const fiber = runFork(result) + fiber.addObserver(onExit) + FiberSet.unsafeAdd(fiberSet, fiber) + } + } else { + port.close() + Deferred.unsafeDone(closeLatch, Exit.void) + } + } + function onMessageError(error: MessageEvent) { + Deferred.unsafeDone( + closeLatch, + new WorkerError({ reason: "decode", cause: error.data }) + ) + } + function onError(error: MessageEvent) { + Deferred.unsafeDone( + closeLatch, + new WorkerError({ reason: "unknown", cause: error.data }) + ) + } + yield* Scope.addFinalizer( + scope, + Effect.sync(() => { + port.removeEventListener("message", onMessage) + port.removeEventListener("messageerror", onError) + }) + ) + port.addEventListener("message", onMessage) + port.addEventListener("messageerror", onMessageError) + port.postMessage([0]) + }) + const send = (_portId: number, message: any, transfer?: ReadonlyArray) => + Effect.sync(() => + port.postMessage([1, message], { + transfer: transfer as any + }) + ) + return { run, send } + }) +}) + +/** @internal */ +export const layer = Layer.succeed(Runner.PlatformRunner, platformRunnerImpl) diff --git a/repos/effect/packages/platform-bun/tsconfig.build.json b/repos/effect/packages/platform-bun/tsconfig.build.json new file mode 100644 index 0000000..0c73ced --- /dev/null +++ b/repos/effect/packages/platform-bun/tsconfig.build.json @@ -0,0 +1,17 @@ +{ + "extends": "./tsconfig.src.json", + "references": [ + { "path": "../cluster/tsconfig.build.json" }, + { "path": "../effect/tsconfig.build.json" }, + { "path": "../platform/tsconfig.build.json" }, + { "path": "../platform-node-shared/tsconfig.build.json" }, + { "path": "../rpc/tsconfig.build.json" }, + { "path": "../sql/tsconfig.build.json" } + ], + "compilerOptions": { + "tsBuildInfoFile": ".tsbuildinfo/build.tsbuildinfo", + "outDir": "build/esm", + "declarationDir": "build/dts", + "stripInternal": true + } +} diff --git a/repos/effect/packages/platform-bun/tsconfig.examples.json b/repos/effect/packages/platform-bun/tsconfig.examples.json new file mode 100644 index 0000000..5bea0ba --- /dev/null +++ b/repos/effect/packages/platform-bun/tsconfig.examples.json @@ -0,0 +1,13 @@ +{ + "extends": "../../tsconfig.base.json", + "include": ["examples"], + "references": [ + { "path": "tsconfig.build.json" } + ], + "compilerOptions": { + "tsBuildInfoFile": ".tsbuildinfo/examples.tsbuildinfo", + "rootDir": "examples", + "noEmit": true, + "types": ["bun"] + } +} diff --git a/repos/effect/packages/platform-bun/tsconfig.json b/repos/effect/packages/platform-bun/tsconfig.json new file mode 100644 index 0000000..3edbf6b --- /dev/null +++ b/repos/effect/packages/platform-bun/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.base.json", + "include": [], + "references": [ + { "path": "tsconfig.src.json" }, + { "path": "tsconfig.test.json" }, + { "path": "tsconfig.examples.json" } + ] +} diff --git a/repos/effect/packages/platform-bun/tsconfig.src.json b/repos/effect/packages/platform-bun/tsconfig.src.json new file mode 100644 index 0000000..c5b8163 --- /dev/null +++ b/repos/effect/packages/platform-bun/tsconfig.src.json @@ -0,0 +1,18 @@ +{ + "extends": "../../tsconfig.base.json", + "include": ["src"], + "references": [ + { "path": "../cluster/tsconfig.src.json" }, + { "path": "../effect/tsconfig.src.json" }, + { "path": "../platform/tsconfig.src.json" }, + { "path": "../platform-node-shared/tsconfig.src.json" }, + { "path": "../rpc/tsconfig.src.json" }, + { "path": "../sql/tsconfig.src.json" } + ], + "compilerOptions": { + "tsBuildInfoFile": ".tsbuildinfo/src.tsbuildinfo", + "rootDir": "src", + "outDir": "build/src", + "types": ["bun"] + } +} diff --git a/repos/effect/packages/platform-bun/tsconfig.test.json b/repos/effect/packages/platform-bun/tsconfig.test.json new file mode 100644 index 0000000..91a7b20 --- /dev/null +++ b/repos/effect/packages/platform-bun/tsconfig.test.json @@ -0,0 +1,14 @@ +{ + "extends": "../../tsconfig.base.json", + "include": ["test"], + "references": [ + { "path": "tsconfig.src.json" }, + { "path": "../vitest/tsconfig.src.json" } + ], + "compilerOptions": { + "tsBuildInfoFile": ".tsbuildinfo/test.tsbuildinfo", + "rootDir": "test", + "noEmit": true, + "types": ["bun"] + } +} diff --git a/repos/effect/packages/platform-bun/vitest.config.ts b/repos/effect/packages/platform-bun/vitest.config.ts new file mode 100644 index 0000000..578d066 --- /dev/null +++ b/repos/effect/packages/platform-bun/vitest.config.ts @@ -0,0 +1,6 @@ +import { mergeConfig, type ViteUserConfig } from "vitest/config" +import shared from "../../vitest.shared.js" + +const config: ViteUserConfig = {} + +export default mergeConfig(shared, config) diff --git a/repos/effect/packages/platform-node-shared/CHANGELOG.md b/repos/effect/packages/platform-node-shared/CHANGELOG.md new file mode 100644 index 0000000..fb1ef20 --- /dev/null +++ b/repos/effect/packages/platform-node-shared/CHANGELOG.md @@ -0,0 +1,3580 @@ +# @effect/platform-node-shared + +## 0.59.0 + +### Patch Changes + +- Updated dependencies [[`f7bb09b`](https://github.com/Effect-TS/effect/commit/f7bb09b022f195d1f2b3c23d49e74b011ec5d109), [`bd7552a`](https://github.com/Effect-TS/effect/commit/bd7552a19cc0ed575507ac6cc0879a57e24ebd31), [`ad1a7eb`](https://github.com/Effect-TS/effect/commit/ad1a7eb7f6bebaf91c80be2443ac0439226d0098), [`0d32048`](https://github.com/Effect-TS/effect/commit/0d32048f9836e2b23a6ba3ec5f43f0a000bb92fb), [`0d32048`](https://github.com/Effect-TS/effect/commit/0d32048f9836e2b23a6ba3ec5f43f0a000bb92fb)]: + - effect@3.21.0 + - @effect/cluster@0.58.0 + - @effect/platform@0.96.0 + - @effect/rpc@0.75.0 + - @effect/sql@0.51.0 + +## 0.58.0 + +### Patch Changes + +- Updated dependencies [[`fc82e81`](https://github.com/Effect-TS/effect/commit/fc82e81448bd9136a37580139ce46a2c61b11b54), [`82996bc`](https://github.com/Effect-TS/effect/commit/82996bce8debffcb44feb98bb862cf2662bd56b7), [`4d97a61`](https://github.com/Effect-TS/effect/commit/4d97a61a15b9dd6a0eece65b8f0c035e16d42ada), [`f6b0960`](https://github.com/Effect-TS/effect/commit/f6b0960bf3184109920dfed16ee7dfd7d67bc0f2), [`8798a84`](https://github.com/Effect-TS/effect/commit/8798a843218e6c0c0d3a8eee83360880e370b4da)]: + - effect@3.20.0 + - @effect/cluster@0.57.0 + - @effect/platform@0.95.0 + - @effect/rpc@0.74.0 + - @effect/sql@0.50.0 + +## 0.57.1 + +### Patch Changes + +- [#5977](https://github.com/Effect-TS/effect/pull/5977) [`118e7a4`](https://github.com/Effect-TS/effect/commit/118e7a4af5b86f6d707a40d3b03157b6bf5827e7) Thanks @scotttrinh! - Added `rows` and `isTTY` properties to `Terminal` + +- Updated dependencies [[`7e925ea`](https://github.com/Effect-TS/effect/commit/7e925eae4a9db556bcbf7e8b6a762ccf8588aa3b), [`118e7a4`](https://github.com/Effect-TS/effect/commit/118e7a4af5b86f6d707a40d3b03157b6bf5827e7), [`d7e75d6`](https://github.com/Effect-TS/effect/commit/d7e75d6d15294bbcd7ac49a0e9005848379ea86f), [`4860d1e`](https://github.com/Effect-TS/effect/commit/4860d1e09b436061ea4aeca07605a669793560fc)]: + - effect@3.19.15 + - @effect/platform@0.94.2 + +## 0.57.0 + +### Patch Changes + +- Updated dependencies [[`77eeb86`](https://github.com/Effect-TS/effect/commit/77eeb86ddf208e51ec25932af83d52d3b4700371), [`ff7053f`](https://github.com/Effect-TS/effect/commit/ff7053f6d8508567b6145239f97aacc5773b0c53), [`287c32c`](https://github.com/Effect-TS/effect/commit/287c32c9f10da8e96f2b9ef8424316189d9ad4b3)]: + - effect@3.19.13 + - @effect/platform@0.94.0 + - @effect/cluster@0.56.0 + - @effect/rpc@0.73.0 + - @effect/sql@0.49.0 + +## 0.56.0 + +### Patch Changes + +- Updated dependencies [[`811852a`](https://github.com/Effect-TS/effect/commit/811852a61868136bb7b3367450f02e5a8fb8a3f9)]: + - @effect/sql@0.48.6 + - @effect/cluster@0.55.0 + +## 0.55.0 + +### Patch Changes + +- Updated dependencies []: + - @effect/cluster@0.54.0 + +## 0.54.0 + +### Patch Changes + +- Updated dependencies [[`794c790`](https://github.com/Effect-TS/effect/commit/794c790d736f62784bff800fda5a656026d93749), [`079975c`](https://github.com/Effect-TS/effect/commit/079975c69d80c62461da5c51fe89e02c44dfa2ea), [`62f7636`](https://github.com/Effect-TS/effect/commit/62f76361ee01ed816687774c5302e7f8c5ff6a42)]: + - @effect/rpc@0.72.2 + - effect@3.19.5 + - @effect/cluster@0.53.0 + +## 0.53.0 + +### Patch Changes + +- Updated dependencies [[`571025c`](https://github.com/Effect-TS/effect/commit/571025ceaff6ef432a61bf65735a5a0f45118313), [`d43577b`](https://github.com/Effect-TS/effect/commit/d43577be59ae510812287b1cbffe6da15c040452)]: + - @effect/cluster@0.52.0 + - @effect/sql@0.48.0 + - @effect/rpc@0.72.1 + +## 0.52.0 + +### Minor Changes + +- [#5606](https://github.com/Effect-TS/effect/pull/5606) [`24a1685`](https://github.com/Effect-TS/effect/commit/24a1685c70a9ed157468650f95a5c3da3f2c2433) Thanks @tim-smart! - backport @effect/cluster from effect v4 + + @effect/cluster no longer requires a Shard Manager, and instead relies on the + `RunnerStorage` service to track runner state. + + To migrate, remove any Shard Manager deployments and use the updated layers in + `@effect/platform-node` or `@effect/platform-bun`. + + # Breaking Changes + - `ShardManager` module has been removed + - `EntityNotManagedByRunner` error has been removed + - Shard locks now use database advisory locks, which requires stable sessions + for database connections. This means load balancers or proxies that rotate + connections may cause issues. + - `@effect/platform-node/NodeClusterSocketRunner` is now + `@effect/cluster/NodeClusterSocket` + - `@effect/platform-node/NodeClusterHttpRunner` is now + `@effect/cluster/NodeClusterHttp` + - `@effect/platform-bun/BunClusterSocketRunner` is now + `@effect/cluster/BunClusterSocket` + - `@effect/platform-bun/BunClusterHttpRunner` is now + `@effect/cluster/BunClusterHttp` + + # New Features + - `RunnerHealth.layerK8s` has been added, which uses the Kubernetes API to track + runner health and liveness. To use it, you will need a service account with + permissions to read pod information. + +### Patch Changes + +- Updated dependencies [[`3c15d5f`](https://github.com/Effect-TS/effect/commit/3c15d5f99fb8d8470a00c5a33d9ba3cac89dfe4c), [`3863fa8`](https://github.com/Effect-TS/effect/commit/3863fa89f61e63e5529fd961e37333bddf7db64a), [`2a03c76`](https://github.com/Effect-TS/effect/commit/2a03c76c2781ca7e9e228e838eab2eb0d0795b1d), [`24a1685`](https://github.com/Effect-TS/effect/commit/24a1685c70a9ed157468650f95a5c3da3f2c2433), [`24a1685`](https://github.com/Effect-TS/effect/commit/24a1685c70a9ed157468650f95a5c3da3f2c2433)]: + - effect@3.19.0 + - @effect/cluster@0.51.0 + - @effect/rpc@0.72.0 + - @effect/platform@0.93.0 + - @effect/sql@0.47.0 + +## 0.51.6 + +### Patch Changes + +- [#5642](https://github.com/Effect-TS/effect/pull/5642) [`b8e3c6d`](https://github.com/Effect-TS/effect/commit/b8e3c6d510aec858ac34bfe5eb2b8fc5506fd669) Thanks @tim-smart! - fix ReferenceError in NodeSocket.fromNet + +- Updated dependencies [[`b8e3c6d`](https://github.com/Effect-TS/effect/commit/b8e3c6d510aec858ac34bfe5eb2b8fc5506fd669)]: + - @effect/cluster@0.50.6 + - @effect/rpc@0.71.1 + +## 0.51.5 + +### Patch Changes + +- [#5638](https://github.com/Effect-TS/effect/pull/5638) [`c11019d`](https://github.com/Effect-TS/effect/commit/c11019ddff54558e7537acbb4aca3e0d5a494839) Thanks @tim-smart! - don't use removeAllListeners in NodeSocket.fromNet + +## 0.51.4 + +### Patch Changes + +- [#5595](https://github.com/Effect-TS/effect/pull/5595) [`12c3ce1`](https://github.com/Effect-TS/effect/commit/12c3ce129f5092778be224fc129f4b84d319f2f9) Thanks @code-alexander! - Fixing stat error when `blksize` is undefined. + +## 0.51.3 + +### Patch Changes + +- [#5602](https://github.com/Effect-TS/effect/pull/5602) [`64b764b`](https://github.com/Effect-TS/effect/commit/64b764b3207eb13cacb13da31343aaf425e966bf) Thanks @tim-smart! - guard against race conditions in NodeSocketServer + +- Updated dependencies [[`64b764b`](https://github.com/Effect-TS/effect/commit/64b764b3207eb13cacb13da31343aaf425e966bf)]: + - @effect/cluster@0.50.3 + +## 0.51.2 + +### Patch Changes + +- [#5590](https://github.com/Effect-TS/effect/pull/5590) [`f4c4702`](https://github.com/Effect-TS/effect/commit/f4c4702ab01900c42c0af4662dfb7a5973619646) Thanks @tim-smart! - add openTimeout options to NodeSocket.makeNet + +- Updated dependencies [[`f4c4702`](https://github.com/Effect-TS/effect/commit/f4c4702ab01900c42c0af4662dfb7a5973619646), [`f6987c0`](https://github.com/Effect-TS/effect/commit/f6987c04ebf1386dc37729dfea1631ce364a5a96)]: + - @effect/cluster@0.50.2 + - @effect/platform@0.92.1 + +## 0.51.1 + +### Patch Changes + +- [#5585](https://github.com/Effect-TS/effect/pull/5585) [`cf17f2f`](https://github.com/Effect-TS/effect/commit/cf17f2f0319a57a886558b01549fea675cd78b69) Thanks @tim-smart! - keep socket error listener attached in NodeSocket + +- Updated dependencies [[`07802f7`](https://github.com/Effect-TS/effect/commit/07802f78fd410d800f0231129ee0866977399152), [`cf17f2f`](https://github.com/Effect-TS/effect/commit/cf17f2f0319a57a886558b01549fea675cd78b69)]: + - effect@3.18.1 + - @effect/cluster@0.50.1 + +## 0.51.0 + +### Patch Changes + +- Updated dependencies [[`1c6ab74`](https://github.com/Effect-TS/effect/commit/1c6ab74b314b2b6df8bb1b1a0cb9527ceda0e3fa), [`70fe803`](https://github.com/Effect-TS/effect/commit/70fe803469db3355ffbf8359b52c351f1c2dc137), [`c296e32`](https://github.com/Effect-TS/effect/commit/c296e32554143b84ae8987046984e1cf1852417c), [`a098ddf`](https://github.com/Effect-TS/effect/commit/a098ddfc551f5aa0a7c36f9b4928372a64d4d9f2)]: + - effect@3.18.0 + - @effect/platform@0.92.0 + - @effect/cluster@0.50.0 + - @effect/rpc@0.71.0 + - @effect/sql@0.46.0 + +## 0.50.1 + +### Patch Changes + +- [#5557](https://github.com/Effect-TS/effect/pull/5557) [`978b6ff`](https://github.com/Effect-TS/effect/commit/978b6ffc0b124d67d62a797211eff795f22cd1e6) Thanks @tim-smart! - allow NodeSocket.makeNet open to be interrupted + +- Updated dependencies [[`978b6ff`](https://github.com/Effect-TS/effect/commit/978b6ffc0b124d67d62a797211eff795f22cd1e6)]: + - @effect/cluster@0.49.1 + +## 0.50.0 + +### Patch Changes + +- Updated dependencies [[`d4d86a8`](https://github.com/Effect-TS/effect/commit/d4d86a81f02b94e09fce8004ce2c5369c505ca5a)]: + - @effect/platform@0.91.0 + - @effect/rpc@0.70.0 + - @effect/cluster@0.49.0 + - @effect/sql@0.45.0 + +## 0.49.2 + +### Patch Changes + +- [#5517](https://github.com/Effect-TS/effect/pull/5517) [`de07e58`](https://github.com/Effect-TS/effect/commit/de07e5805496b80226ba6a5efc2b4c05e1aba4b8) Thanks @tim-smart! - backport cluster improvements from effect 4 + +- Updated dependencies [[`de07e58`](https://github.com/Effect-TS/effect/commit/de07e5805496b80226ba6a5efc2b4c05e1aba4b8), [`de07e58`](https://github.com/Effect-TS/effect/commit/de07e5805496b80226ba6a5efc2b4c05e1aba4b8)]: + - @effect/cluster@0.48.10 + - @effect/platform@0.90.10 + - @effect/rpc@0.69.3 + +## 0.49.1 + +### Patch Changes + +- [#5481](https://github.com/Effect-TS/effect/pull/5481) [`333be04`](https://github.com/Effect-TS/effect/commit/333be046b50e8300f5cb70b871448e0628b7b37c) Thanks @jpowersdev! - Allow user to set extension of file created using `FileSystem.makeTempFile` + +- Updated dependencies [[`333be04`](https://github.com/Effect-TS/effect/commit/333be046b50e8300f5cb70b871448e0628b7b37c), [`0a9ec23`](https://github.com/Effect-TS/effect/commit/0a9ec23dca104ac6fd7ea5841e98f5fa7796be40)]: + - @effect/platform@0.90.8 + - @effect/cluster@0.48.4 + +## 0.49.0 + +### Patch Changes + +- Updated dependencies [[`3e163b2`](https://github.com/Effect-TS/effect/commit/3e163b24cc2b647e25566ba29ef25c3f57609042)]: + - @effect/rpc@0.69.0 + - @effect/cluster@0.48.0 + +## 0.48.1 + +### Patch Changes + +- [#5383](https://github.com/Effect-TS/effect/pull/5383) [`5c67e29`](https://github.com/Effect-TS/effect/commit/5c67e29945f3acf7c1146fdf3684966c8740d3b6) Thanks @IMax153! - Ensure that user input is always offered to the terminal input mailbox + +## 0.48.0 + +### Patch Changes + +- Updated dependencies [[`a949539`](https://github.com/Effect-TS/effect/commit/a94953971c2e908890dfda00f8560d317306c328), [`a949539`](https://github.com/Effect-TS/effect/commit/a94953971c2e908890dfda00f8560d317306c328)]: + - @effect/cluster@0.47.0 + - effect@3.17.7 + +## 0.47.2 + +### Patch Changes + +- [#5347](https://github.com/Effect-TS/effect/pull/5347) [`20f0d69`](https://github.com/Effect-TS/effect/commit/20f0d6978e0e98464f23b6582c37c6ce12319f29) Thanks @tim-smart! - update Cluster layer conditional storage types + +- Updated dependencies [[`d0b5fd1`](https://github.com/Effect-TS/effect/commit/d0b5fd1f7a292a47b9eeb058e5df57ace9a5ab14)]: + - @effect/cluster@0.46.4 + - @effect/sql@0.44.1 + +## 0.47.1 + +### Patch Changes + +- [#5327](https://github.com/Effect-TS/effect/pull/5327) [`695bc1a`](https://github.com/Effect-TS/effect/commit/695bc1ab8612cb5b326ecb57c80bd6e36dfd63ee) Thanks @fubhy! - Improved child process cleanup + +- [#5339](https://github.com/Effect-TS/effect/pull/5339) [`f1ad6c5`](https://github.com/Effect-TS/effect/commit/f1ad6c5ffc349d45ce64b285f2d0cf4ea77f9897) Thanks @fubhy! - Added `stderr` and `stdout` as `NodeStream` and `stderr` and `stdin` as `NodeSink` + +## 0.47.0 + +### Patch Changes + +- Updated dependencies [[`5a0f4f1`](https://github.com/Effect-TS/effect/commit/5a0f4f176687a39d9fa46bb894bb7ac3175b0e87), [`e9cbd26`](https://github.com/Effect-TS/effect/commit/e9cbd2673401723aa811b0535202e4f57baf6d2c)]: + - effect@3.17.1 + - @effect/rpc@0.68.0 + - @effect/cluster@0.46.0 + +## 0.46.0 + +### Patch Changes + +- Updated dependencies [[`7813640`](https://github.com/Effect-TS/effect/commit/7813640279d9e3a3e7fc0a29bfb5c6d5fb3c270f)]: + - @effect/platform@0.90.0 + - @effect/cluster@0.45.0 + - @effect/rpc@0.67.0 + - @effect/sql@0.44.0 + +## 0.45.0 + +### Patch Changes + +- Updated dependencies [[`40c3c87`](https://github.com/Effect-TS/effect/commit/40c3c875f724264312b43002859c82bed9ad0df9), [`ed2c74a`](https://github.com/Effect-TS/effect/commit/ed2c74ae8fa4ea0dd06ea84a3e58cd32e6916104), [`073a1b8`](https://github.com/Effect-TS/effect/commit/073a1b8be5dbfa87454393ee7346f5bc36a4fd63), [`f382e99`](https://github.com/Effect-TS/effect/commit/f382e99e409838a879246250fc3994b9bf5b3c2c), [`e8c7ba5`](https://github.com/Effect-TS/effect/commit/e8c7ba5fd3eb0c3ae3039fc24c09d69391987989), [`7e10415`](https://github.com/Effect-TS/effect/commit/7e1041599ade25103428703f5d2dfd7378a09636), [`e9bdece`](https://github.com/Effect-TS/effect/commit/e9bdececdc24f60a246be5055eca71a0d49ea7f2), [`8d95eb0`](https://github.com/Effect-TS/effect/commit/8d95eb0356b1d1736204836c275d201a547d208d)]: + - effect@3.17.0 + - @effect/cluster@0.44.0 + - @effect/platform@0.89.0 + - @effect/rpc@0.66.0 + - @effect/sql@0.43.0 + +## 0.44.0 + +### Patch Changes + +- Updated dependencies [[`f5dfabf`](https://github.com/Effect-TS/effect/commit/f5dfabf51ba481a4468c1509c537314978ef6cec), [`17a5ea8`](https://github.com/Effect-TS/effect/commit/17a5ea8fa29785fe6e4c9480f2a2e9c8c59f3f38), [`d25f22b`](https://github.com/Effect-TS/effect/commit/d25f22be7598abe977caf6cdac3b0dd78b438c48)]: + - effect@3.16.14 + - @effect/sql@0.42.0 + - @effect/platform@0.88.1 + - @effect/cluster@0.43.0 + - @effect/rpc@0.65.1 + +## 0.43.0 + +### Patch Changes + +- Updated dependencies [[`27206d7`](https://github.com/Effect-TS/effect/commit/27206d7f0558d7fe28de57bf54f1d0cc83acc92e), [`dbabf5e`](https://github.com/Effect-TS/effect/commit/dbabf5e76fa63b050d2b6c466713c7dc59f07d3c)]: + - @effect/platform@0.88.0 + - @effect/cluster@0.42.0 + - @effect/rpc@0.65.0 + - @effect/sql@0.41.0 + +## 0.42.18 + +### Patch Changes + +- Updated dependencies [[`c1c05a8`](https://github.com/Effect-TS/effect/commit/c1c05a8242fb5df7445b4a12387a60eac7726eb7), [`5b7cd92`](https://github.com/Effect-TS/effect/commit/5b7cd923e786c38a0802faf0fe75498ab3cccf28), [`81fe4a2`](https://github.com/Effect-TS/effect/commit/81fe4a2c81d5e30e180a60e68c52016a27b350db)]: + - effect@3.16.13 + - @effect/rpc@0.64.14 + - @effect/cluster@0.41.18 + - @effect/platform@0.87.13 + - @effect/sql@0.40.14 + +## 0.42.17 + +### Patch Changes + +- Updated dependencies [[`56b33c3`](https://github.com/Effect-TS/effect/commit/56b33c357cfc5f8976486f48e93032058c02d876)]: + - @effect/cluster@0.41.17 + +## 0.42.16 + +### Patch Changes + +- [#5174](https://github.com/Effect-TS/effect/pull/5174) [`d5e25b2`](https://github.com/Effect-TS/effect/commit/d5e25b237f05670ee42b386cb40b2cb448fc11d7) Thanks @schickling! - feat(platform): add recursive option to FileSystem.watch + + Added a `recursive` option to `FileSystem.watch` that allows watching for changes in subdirectories. When set to `true`, the watcher will monitor changes in all nested directories. + + Note: The recursive option is only supported on macOS and Windows. On other platforms, it will be ignored. + + Example: + + ```ts + import { FileSystem } from "@effect/platform" + import { Effect, Stream } from "effect" + + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem + + // Watch directory and all subdirectories + yield* fs + .watch("src", { recursive: true }) + .pipe(Stream.runForEach(console.log)) + }) + ``` + +- Updated dependencies [[`32ba77a`](https://github.com/Effect-TS/effect/commit/32ba77ae304d2161362a73e8b61965332626cf2d), [`d5e25b2`](https://github.com/Effect-TS/effect/commit/d5e25b237f05670ee42b386cb40b2cb448fc11d7)]: + - @effect/platform@0.87.12 + - @effect/cluster@0.41.16 + - @effect/rpc@0.64.13 + - @effect/sql@0.40.13 + +## 0.42.15 + +### Patch Changes + +- Updated dependencies [[`79a1947`](https://github.com/Effect-TS/effect/commit/79a1947359cbd89a47ea315cdd86a3d250f28f43), [`001392b`](https://github.com/Effect-TS/effect/commit/001392ba8bfcad101bb034348a7415012fb12f72), [`7bfb099`](https://github.com/Effect-TS/effect/commit/7bfb099cb5528511b8d63045c4fbb4dc9cb18528)]: + - @effect/rpc@0.64.12 + - @effect/platform@0.87.11 + - @effect/cluster@0.41.15 + - @effect/sql@0.40.12 + +## 0.42.14 + +### Patch Changes + +- Updated dependencies [[`678318d`](https://github.com/Effect-TS/effect/commit/678318d2e88233156b006acda56c2d138ee3ffa0), [`678318d`](https://github.com/Effect-TS/effect/commit/678318d2e88233156b006acda56c2d138ee3ffa0)]: + - @effect/platform@0.87.10 + - @effect/cluster@0.41.14 + - @effect/rpc@0.64.11 + - @effect/sql@0.40.11 + +## 0.42.13 + +### Patch Changes + +- Updated dependencies [[`54514a2`](https://github.com/Effect-TS/effect/commit/54514a2f53166de27ad7e756dbf12194691fd4af)]: + - @effect/platform@0.87.9 + - @effect/cluster@0.41.13 + - @effect/rpc@0.64.10 + - @effect/sql@0.40.10 + +## 0.42.12 + +### Patch Changes + +- Updated dependencies [[`4ce4f82`](https://github.com/Effect-TS/effect/commit/4ce4f824f6fdef492be1d35c05a490ffce518c89)]: + - @effect/platform@0.87.8 + - @effect/cluster@0.41.12 + - @effect/rpc@0.64.9 + - @effect/sql@0.40.9 + +## 0.42.11 + +### Patch Changes + +- Updated dependencies [[`a9b617f`](https://github.com/Effect-TS/effect/commit/a9b617f125171ed76cd79ab46d7a924daf3b0e70), [`7e26e86`](https://github.com/Effect-TS/effect/commit/7e26e86524abcc93713d6ad7eee486638c98f7c2)]: + - @effect/platform@0.87.7 + - @effect/cluster@0.41.11 + - @effect/rpc@0.64.8 + - @effect/sql@0.40.8 + +## 0.42.10 + +### Patch Changes + +- Updated dependencies [[`905da99`](https://github.com/Effect-TS/effect/commit/905da996aad665057b4ca6dba1a4af44fb8835bd)]: + - effect@3.16.12 + - @effect/cluster@0.41.10 + - @effect/platform@0.87.6 + - @effect/rpc@0.64.7 + - @effect/sql@0.40.7 + +## 0.42.9 + +### Patch Changes + +- Updated dependencies []: + - @effect/sql@0.40.6 + - @effect/cluster@0.41.9 + +## 0.42.8 + +### Patch Changes + +- Updated dependencies [[`2fd8676`](https://github.com/Effect-TS/effect/commit/2fd8676c803cd40000dfc3231f5daecaa0e0ebd2)]: + - @effect/platform@0.87.5 + - @effect/cluster@0.41.8 + - @effect/rpc@0.64.6 + - @effect/sql@0.40.5 + +## 0.42.7 + +### Patch Changes + +- Updated dependencies [[`b01d2e0`](https://github.com/Effect-TS/effect/commit/b01d2e0d591418e10e9e362698205d848e97a9b7)]: + - @effect/cluster@0.41.7 + +## 0.42.6 + +### Patch Changes + +- Updated dependencies [[`7fdc16b`](https://github.com/Effect-TS/effect/commit/7fdc16bd88b872f5918384e4acda3731aab018da), [`e82a4fd`](https://github.com/Effect-TS/effect/commit/e82a4fd60f6528d08cef1a4aba0abe0d3ba741ad)]: + - @effect/cluster@0.41.6 + - @effect/platform@0.87.4 + - @effect/rpc@0.64.5 + - @effect/sql@0.40.4 + +## 0.42.5 + +### Patch Changes + +- Updated dependencies [[`1b6e396`](https://github.com/Effect-TS/effect/commit/1b6e396d699f3cbbc56b68f99055cf746529bb9e), [`46c3216`](https://github.com/Effect-TS/effect/commit/46c321657d93393506278327418e36f8e7a77f86)]: + - @effect/platform@0.87.3 + - @effect/sql@0.40.3 + - @effect/cluster@0.41.5 + - @effect/rpc@0.64.4 + +## 0.42.4 + +### Patch Changes + +- Updated dependencies [[`4fea68c`](https://github.com/Effect-TS/effect/commit/4fea68ca7a25a3c39a1ab68b3885534513ab0c81), [`b927954`](https://github.com/Effect-TS/effect/commit/b9279543cf5688dd8a577af80456959c615217d0), [`99590a6`](https://github.com/Effect-TS/effect/commit/99590a6ca9128eb1ede265b6670b655311995614), [`6c3e24c`](https://github.com/Effect-TS/effect/commit/6c3e24c2308f7d4a29b8f4270ab81bca22ac6bb4)]: + - @effect/platform@0.87.2 + - effect@3.16.11 + - @effect/cluster@0.41.4 + - @effect/rpc@0.64.3 + - @effect/sql@0.40.2 + +## 0.42.3 + +### Patch Changes + +- Updated dependencies [[`faad30e`](https://github.com/Effect-TS/effect/commit/faad30ec8742916be59f9db642d0fc98225b636c)]: + - effect@3.16.10 + - @effect/cluster@0.41.3 + - @effect/platform@0.87.1 + - @effect/rpc@0.64.2 + - @effect/sql@0.40.1 + +## 0.42.2 + +### Patch Changes + +- Updated dependencies [[`112a93a`](https://github.com/Effect-TS/effect/commit/112a93a9bab73e95e79f7b3502d1a7b1acd668fc)]: + - @effect/rpc@0.64.1 + - @effect/cluster@0.41.2 + +## 0.42.1 + +### Patch Changes + +- Updated dependencies [[`d5fd2c1`](https://github.com/Effect-TS/effect/commit/d5fd2c1526f06228853ed8317d9688c4af5f285a), [`9d189d7`](https://github.com/Effect-TS/effect/commit/9d189d744aa3307e055094c66f580453d95ff99d)]: + - @effect/cluster@0.41.1 + +## 0.42.0 + +### Patch Changes + +- [#5084](https://github.com/Effect-TS/effect/pull/5084) [`f90813f`](https://github.com/Effect-TS/effect/commit/f90813f7573329fbb8af11fc460d811d8788955a) Thanks @tim-smart! - correctly propagate fs.watch errors when the stat fails + +- Updated dependencies [[`b5bac9a`](https://github.com/Effect-TS/effect/commit/b5bac9ac2913fcd11b02322624f03b544eef53ba), [`867919c`](https://github.com/Effect-TS/effect/commit/867919c8be9a2f770699c0db852a3f566017ffd6)]: + - @effect/rpc@0.64.0 + - @effect/platform@0.87.0 + - @effect/cluster@0.41.0 + - @effect/sql@0.40.0 + +## 0.41.0 + +### Patch Changes + +- Updated dependencies [[`5137c70`](https://github.com/Effect-TS/effect/commit/5137c703461d8d3b363c112140a6e7f798241d07), [`c23d25c`](https://github.com/Effect-TS/effect/commit/c23d25c3e7c541f1f63b28484d8c461d86c67e99), [`5137c70`](https://github.com/Effect-TS/effect/commit/5137c703461d8d3b363c112140a6e7f798241d07), [`5137c70`](https://github.com/Effect-TS/effect/commit/5137c703461d8d3b363c112140a6e7f798241d07)]: + - effect@3.16.9 + - @effect/platform@0.86.0 + - @effect/cluster@0.40.0 + - @effect/rpc@0.63.0 + - @effect/sql@0.39.0 + +## 0.40.5 + +### Patch Changes + +- Updated dependencies [[`ff90206`](https://github.com/Effect-TS/effect/commit/ff90206fc56f5c1eb1675603652462a83a27421d)]: + - @effect/cluster@0.39.5 + +## 0.40.4 + +### Patch Changes + +- Updated dependencies [[`a8d99b2`](https://github.com/Effect-TS/effect/commit/a8d99b2ec2f55d9aa6e7d00a5138e80380716877)]: + - @effect/rpc@0.62.4 + - @effect/cluster@0.39.4 + +## 0.40.3 + +### Patch Changes + +- Updated dependencies [[`914a191`](https://github.com/Effect-TS/effect/commit/914a191e7cb6341a3d0e965bccd27c336cf22e44)]: + - @effect/platform@0.85.2 + - @effect/cluster@0.39.3 + - @effect/rpc@0.62.3 + - @effect/sql@0.38.2 + +## 0.40.2 + +### Patch Changes + +- Updated dependencies [[`ddfd1e4`](https://github.com/Effect-TS/effect/commit/ddfd1e43db60e3b779d18a221344423c5f3c7416)]: + - @effect/rpc@0.62.2 + - @effect/cluster@0.39.2 + +## 0.40.1 + +### Patch Changes + +- Updated dependencies [[`8cb98d5`](https://github.com/Effect-TS/effect/commit/8cb98d53e68330228287ce2a2e0d8a4c86bcab3b), [`db2dd3c`](https://github.com/Effect-TS/effect/commit/db2dd3c3a8a77d791eae19e66153527e1cde4e6e)]: + - effect@3.16.8 + - @effect/cluster@0.39.1 + - @effect/platform@0.85.1 + - @effect/rpc@0.62.1 + - @effect/sql@0.38.1 + +## 0.40.0 + +### Patch Changes + +- Updated dependencies [[`93687dd`](https://github.com/Effect-TS/effect/commit/93687ddbb25ce3b324cd2b83d2ccff225e97307e), [`93687dd`](https://github.com/Effect-TS/effect/commit/93687ddbb25ce3b324cd2b83d2ccff225e97307e), [`93687dd`](https://github.com/Effect-TS/effect/commit/93687ddbb25ce3b324cd2b83d2ccff225e97307e)]: + - @effect/platform@0.85.0 + - @effect/cluster@0.39.0 + - @effect/rpc@0.62.0 + - @effect/sql@0.38.0 + +## 0.39.16 + +### Patch Changes + +- Updated dependencies [[`1bb0d8a`](https://github.com/Effect-TS/effect/commit/1bb0d8ab96782e99434356266b38251554ea0294), [`cbac1ac`](https://github.com/Effect-TS/effect/commit/cbac1ac61a4e15ad15828563b39eef412bcee66e)]: + - effect@3.16.7 + - @effect/cluster@0.38.16 + - @effect/rpc@0.61.15 + - @effect/platform@0.84.11 + - @effect/sql@0.37.12 + +## 0.39.15 + +### Patch Changes + +- Updated dependencies [[`a5f7595`](https://github.com/Effect-TS/effect/commit/a5f75956ef9a15a83c416517ef493f0ee2f5ee8a), [`a02470c`](https://github.com/Effect-TS/effect/commit/a02470c75579e91525a25adb3f21b3650d042fdd), [`bf369b2`](https://github.com/Effect-TS/effect/commit/bf369b2902a0e0b195d957c18b9efd180942cf8b), [`f891d45`](https://github.com/Effect-TS/effect/commit/f891d45adffdafd3f94a2eca23faa354e3a409a8)]: + - effect@3.16.6 + - @effect/platform@0.84.10 + - @effect/cluster@0.38.15 + - @effect/rpc@0.61.14 + - @effect/sql@0.37.11 + +## 0.39.14 + +### Patch Changes + +- Updated dependencies [[`ee3a197`](https://github.com/Effect-TS/effect/commit/ee3a1973f54d7611ae99979edfed3020e94e1126), [`ee3a197`](https://github.com/Effect-TS/effect/commit/ee3a1973f54d7611ae99979edfed3020e94e1126)]: + - @effect/rpc@0.61.13 + - @effect/cluster@0.38.14 + +## 0.39.13 + +### Patch Changes + +- Updated dependencies [[`e0d3d42`](https://github.com/Effect-TS/effect/commit/e0d3d424d8f4e6a8ada017160406991f02b3c068)]: + - @effect/rpc@0.61.12 + - @effect/cluster@0.38.13 + +## 0.39.12 + +### Patch Changes + +- Updated dependencies [[`dca92fd`](https://github.com/Effect-TS/effect/commit/dca92fd8cf41f07561f55d863def5a9f62275f53)]: + - @effect/cluster@0.38.12 + - @effect/rpc@0.61.11 + +## 0.39.11 + +### Patch Changes + +- Updated dependencies [[`cc283b9`](https://github.com/Effect-TS/effect/commit/cc283b968235da3caf6c3e3a09b525fe09618fee)]: + - @effect/cluster@0.38.11 + +## 0.39.10 + +### Patch Changes + +- Updated dependencies [[`6e2e886`](https://github.com/Effect-TS/effect/commit/6e2e886f060c4ac057926b68d2e441c279480c30), [`bf418ef`](https://github.com/Effect-TS/effect/commit/bf418ef14a0f2ec965535793d5cea8fa8ba177ac)]: + - @effect/cluster@0.38.10 + - effect@3.16.5 + - @effect/platform@0.84.9 + - @effect/rpc@0.61.10 + - @effect/sql@0.37.10 + +## 0.39.9 + +### Patch Changes + +- Updated dependencies [[`7bf6cb9`](https://github.com/Effect-TS/effect/commit/7bf6cb943810e403f472a901ed29ccbbf76a46b2), [`7bf6cb9`](https://github.com/Effect-TS/effect/commit/7bf6cb943810e403f472a901ed29ccbbf76a46b2)]: + - @effect/rpc@0.61.9 + - @effect/cluster@0.38.9 + +## 0.39.8 + +### Patch Changes + +- Updated dependencies []: + - @effect/cluster@0.38.8 + +## 0.39.7 + +### Patch Changes + +- Updated dependencies [[`22166f8`](https://github.com/Effect-TS/effect/commit/22166f80c677cad6b4719e0e0253a9d06f964626)]: + - @effect/cluster@0.38.7 + +## 0.39.6 + +### Patch Changes + +- [#4998](https://github.com/Effect-TS/effect/pull/4998) [`f8ff7dc`](https://github.com/Effect-TS/effect/commit/f8ff7dccfe6ebd3409ab95c57f61764643d19a2b) Thanks @tim-smart! - expose MessageStorage in cluster clientOnly layers + +- Updated dependencies [[`8b9db77`](https://github.com/Effect-TS/effect/commit/8b9db7742846af0f58fd8e8b7acb7f4f5ff487ec)]: + - @effect/platform@0.84.8 + - @effect/cluster@0.38.6 + - @effect/rpc@0.61.8 + - @effect/sql@0.37.9 + +## 0.39.5 + +### Patch Changes + +- Updated dependencies [[`74ab9a0`](https://github.com/Effect-TS/effect/commit/74ab9a0a9e16d6e019369d256e1e24175c8bc3f3), [`770008e`](https://github.com/Effect-TS/effect/commit/770008eca3aad2899a2ed951236e575793294b28)]: + - effect@3.16.4 + - @effect/cluster@0.38.5 + - @effect/platform@0.84.7 + - @effect/rpc@0.61.7 + - @effect/sql@0.37.8 + +## 0.39.4 + +### Patch Changes + +- Updated dependencies [[`7e59d0e`](https://github.com/Effect-TS/effect/commit/7e59d0e2e004d86b8d0778e99c6fcd173fcb682a)]: + - @effect/cluster@0.38.4 + +## 0.39.3 + +### Patch Changes + +- Updated dependencies [[`59575c5`](https://github.com/Effect-TS/effect/commit/59575c5bf17a32c8b76c42e3794222b20e766581)]: + - @effect/cluster@0.38.3 + - @effect/sql@0.37.7 + +## 0.39.2 + +### Patch Changes + +- Updated dependencies [[`d244b63`](https://github.com/Effect-TS/effect/commit/d244b6345ea1d2ac88812562b0c170683913d502), [`ceea77a`](https://github.com/Effect-TS/effect/commit/ceea77a13055f145520f763e3fce5b8ff15d728f)]: + - @effect/cluster@0.38.2 + - @effect/platform@0.84.6 + - @effect/rpc@0.61.6 + - @effect/sql@0.37.6 + +## 0.39.1 + +### Patch Changes + +- Updated dependencies [[`612c739`](https://github.com/Effect-TS/effect/commit/612c73979abc44825feae573c8902b6484923aaa)]: + - @effect/cluster@0.38.1 + +## 0.39.0 + +### Patch Changes + +- Updated dependencies [[`3086405`](https://github.com/Effect-TS/effect/commit/308640563041004d790f08d2ba75cc3a85fdf752), [`ec52c6a`](https://github.com/Effect-TS/effect/commit/ec52c6a2211e76972462b15b9d5a9d6d56761b7a), [`71e1e6c`](https://github.com/Effect-TS/effect/commit/71e1e6c535c11a3ec498540a3af3c1a313a5319b), [`d0067ca`](https://github.com/Effect-TS/effect/commit/d0067caef053b2855d93dcef59ea585d0fad9d8c), [`8c79abe`](https://github.com/Effect-TS/effect/commit/8c79abeb47d070d8880b652d31626497d3005a4e)]: + - @effect/cluster@0.38.0 + - @effect/platform@0.84.5 + - @effect/rpc@0.61.5 + - @effect/sql@0.37.5 + +## 0.38.2 + +### Patch Changes + +- Updated dependencies [[`6dfbae9`](https://github.com/Effect-TS/effect/commit/6dfbae946ea12ecee7234f5785335f3e7f8335b4)]: + - @effect/cluster@0.37.2 + +## 0.38.1 + +### Patch Changes + +- Updated dependencies []: + - @effect/cluster@0.37.1 + +## 0.38.0 + +### Patch Changes + +- Updated dependencies [[`87722fc`](https://github.com/Effect-TS/effect/commit/87722fce693a9b49284bbddbf82d30714c688261), [`36217ee`](https://github.com/Effect-TS/effect/commit/36217eeb1337edd9ac3f9a635b80a6385d22ae8f), [`a116aea`](https://github.com/Effect-TS/effect/commit/a116aeade97c83d8c96f17cdc5cf3b5a0bd9be74)]: + - effect@3.16.3 + - @effect/rpc@0.61.4 + - @effect/cluster@0.37.0 + - @effect/platform@0.84.4 + - @effect/sql@0.37.4 + +## 0.37.3 + +### Patch Changes + +- Updated dependencies [[`ab7684f`](https://github.com/Effect-TS/effect/commit/ab7684f1c2a0671bf091f255d220e3a4cc7f528e)]: + - @effect/platform@0.84.3 + - @effect/cluster@0.36.3 + - @effect/rpc@0.61.3 + - @effect/sql@0.37.3 + +## 0.37.2 + +### Patch Changes + +- Updated dependencies [[`0ddf148`](https://github.com/Effect-TS/effect/commit/0ddf148a247aa87af043d276b8453a714a400897), [`a77afb1`](https://github.com/Effect-TS/effect/commit/a77afb1f7191a57a68b09fcdee5e9f27a0682b0a)]: + - effect@3.16.2 + - @effect/rpc@0.61.2 + - @effect/cluster@0.36.2 + - @effect/platform@0.84.2 + - @effect/sql@0.37.2 + +## 0.37.1 + +### Patch Changes + +- Updated dependencies [[`71174d0`](https://github.com/Effect-TS/effect/commit/71174d09691314a9b6b66189e456fd21e3eb6543), [`d615e6e`](https://github.com/Effect-TS/effect/commit/d615e6e5b944f6fd5e627e31752c7ca7e4e1c17d)]: + - @effect/platform@0.84.1 + - effect@3.16.1 + - @effect/cluster@0.36.1 + - @effect/rpc@0.61.1 + - @effect/sql@0.37.1 + +## 0.37.0 + +### Patch Changes + +- Updated dependencies [[`ee0bd5d`](https://github.com/Effect-TS/effect/commit/ee0bd5d24864752c54cb359f67a67dd903971ec4), [`5189800`](https://github.com/Effect-TS/effect/commit/51898004e11766b8cf6d95e960b636f6d5db79ec), [`58bfeaa`](https://github.com/Effect-TS/effect/commit/58bfeaa64ded8c88f772b184311c0c0dbac10960), [`194d748`](https://github.com/Effect-TS/effect/commit/194d7486943f56f3267ef415395ac220a4b3e634), [`918c9ea`](https://github.com/Effect-TS/effect/commit/918c9ea1a57facb154f0fb26792021f337054dee), [`9198e6f`](https://github.com/Effect-TS/effect/commit/9198e6fcc1a3ff4fefb3363004de558d8de01f40), [`2a370bf`](https://github.com/Effect-TS/effect/commit/2a370bf625fdeede5659721468eb0d527e403279), [`58ccb91`](https://github.com/Effect-TS/effect/commit/58ccb91328c8df5d49808b673738bc09df355201), [`fd47834`](https://github.com/Effect-TS/effect/commit/fd478348203fa89462b0a1d067ce4de034353df4)]: + - effect@3.16.0 + - @effect/cluster@0.36.0 + - @effect/platform@0.84.0 + - @effect/rpc@0.61.0 + - @effect/sql@0.37.0 + +## 0.36.0 + +### Patch Changes + +- Updated dependencies [[`5522520`](https://github.com/Effect-TS/effect/commit/55225206ab9af0ad60b1c0654690a8a096d625cd), [`cc5bb2b`](https://github.com/Effect-TS/effect/commit/cc5bb2b918a9450a975f702dabcea891bda382cb)]: + - @effect/platform@0.83.0 + - effect@3.15.5 + - @effect/cluster@0.35.0 + - @effect/rpc@0.60.0 + - @effect/sql@0.36.0 + +## 0.35.5 + +### Patch Changes + +- Updated dependencies [[`0617b9d`](https://github.com/Effect-TS/effect/commit/0617b9dc365f1963b36949ad7f9023ab6eb94524), [`58c5fd3`](https://github.com/Effect-TS/effect/commit/58c5fd3dd30eceb6c8afea90406768b0e348f48f)]: + - @effect/platform@0.82.8 + - @effect/cluster@0.34.5 + - @effect/rpc@0.59.9 + - @effect/sql@0.35.8 + +## 0.35.4 + +### Patch Changes + +- [#4921](https://github.com/Effect-TS/effect/pull/4921) [`c20b95a`](https://github.com/Effect-TS/effect/commit/c20b95a99ffe452b4774c844d397a905f713b6d6) Thanks @tim-smart! - update /platform dependencies + +- Updated dependencies [[`f570554`](https://github.com/Effect-TS/effect/commit/f57055459524587b041340577dad85476bb35f81), [`78047e8`](https://github.com/Effect-TS/effect/commit/78047e8dfc8005b66f87afe50bb95981fea51561), [`c20b95a`](https://github.com/Effect-TS/effect/commit/c20b95a99ffe452b4774c844d397a905f713b6d6), [`94ada43`](https://github.com/Effect-TS/effect/commit/94ada430928d5685bdbef513e87562c20774a3a2)]: + - effect@3.15.4 + - @effect/platform@0.82.7 + - @effect/cluster@0.34.4 + - @effect/rpc@0.59.8 + - @effect/sql@0.35.7 + +## 0.35.3 + +### Patch Changes + +- Updated dependencies [[`618903b`](https://github.com/Effect-TS/effect/commit/618903ba9ae96e2bfe6ee31f61c4359b915f2a36)]: + - @effect/platform@0.82.6 + - @effect/cluster@0.34.3 + - @effect/rpc@0.59.7 + - @effect/sql@0.35.6 + +## 0.35.2 + +### Patch Changes + +- Updated dependencies [[`7764a07`](https://github.com/Effect-TS/effect/commit/7764a07d960c60df81f14e1dc949518f4bbe494a), [`4577f54`](https://github.com/Effect-TS/effect/commit/4577f548d67273e576cdde423bdd34a4b910766a), [`30a0d9c`](https://github.com/Effect-TS/effect/commit/30a0d9cb51c84290d51b1361d72ff5cee33c13c7)]: + - @effect/platform@0.82.5 + - effect@3.15.3 + - @effect/cluster@0.34.2 + - @effect/rpc@0.59.6 + - @effect/sql@0.35.5 + +## 0.35.1 + +### Patch Changes + +- Updated dependencies [[`1627a02`](https://github.com/Effect-TS/effect/commit/1627a0299a07c3538ca15293f1ac3ffa7eeb45f3), [`d45e8a8`](https://github.com/Effect-TS/effect/commit/d45e8a8ac8227192f504e39e6d04fdcf4fb1d225), [`89657ac`](https://github.com/Effect-TS/effect/commit/89657ac2fbda9ba38ac2962ce96949e536a464f9), [`d13b68e`](https://github.com/Effect-TS/effect/commit/d13b68e3a9456d0bfee9bca8273a7b44a9c69087)]: + - @effect/cluster@0.34.1 + - @effect/platform@0.82.4 + - @effect/sql@0.35.4 + - @effect/rpc@0.59.5 + +## 0.35.0 + +### Patch Changes + +- Updated dependencies [[`b8722b8`](https://github.com/Effect-TS/effect/commit/b8722b817e2306fe8c8245f3f9e32d85b824b961), [`a328f4b`](https://github.com/Effect-TS/effect/commit/a328f4b4fe717dd53e5b04a30f387433c32f7328), [`eaf8405`](https://github.com/Effect-TS/effect/commit/eaf8405ab9bb52423050eb0d23dd7d3c21c18141)]: + - effect@3.15.2 + - @effect/platform@0.82.3 + - @effect/cluster@0.34.0 + - @effect/rpc@0.59.4 + - @effect/sql@0.35.3 + +## 0.34.3 + +### Patch Changes + +- Updated dependencies [[`739a3d4`](https://github.com/Effect-TS/effect/commit/739a3d4a4565915fe2e690003f4f9085cb4422fc)]: + - @effect/platform@0.82.2 + - @effect/cluster@0.33.3 + - @effect/rpc@0.59.3 + - @effect/sql@0.35.2 + +## 0.34.2 + +### Patch Changes + +- Updated dependencies [[`787ce70`](https://github.com/Effect-TS/effect/commit/787ce7042e35b657963473c6efe47752868cd811), [`1269641`](https://github.com/Effect-TS/effect/commit/1269641a99ae43069f7648ff79ffe8729b54b348), [`1269641`](https://github.com/Effect-TS/effect/commit/1269641a99ae43069f7648ff79ffe8729b54b348)]: + - effect@3.15.1 + - @effect/cluster@0.33.2 + - @effect/platform@0.82.1 + - @effect/rpc@0.59.2 + - @effect/sql@0.35.1 + +## 0.34.1 + +### Patch Changes + +- Updated dependencies [[`6495440`](https://github.com/Effect-TS/effect/commit/64954405eb57313722023b87c0d92761980e2713)]: + - @effect/rpc@0.59.1 + - @effect/cluster@0.33.1 + +## 0.34.0 + +### Patch Changes + +- Updated dependencies [[`c654595`](https://github.com/Effect-TS/effect/commit/c65459587b51da140b78098e81fdbfece65d53e2), [`d9f5dea`](https://github.com/Effect-TS/effect/commit/d9f5deae0f02f5de2b9fcb1cca8b142ba4bc2bba), [`49aa723`](https://github.com/Effect-TS/effect/commit/49aa7236a15e13f818c86edbca08c4af67c8dfaf), [`74c14d0`](https://github.com/Effect-TS/effect/commit/74c14d01d0cb48cf517a1b6e29a373a96ed0ff5b), [`e4f49b6`](https://github.com/Effect-TS/effect/commit/e4f49b66857e01b74ab6a9a0bc7132f44cd04cbb), [`6f02224`](https://github.com/Effect-TS/effect/commit/6f02224b3fc46a682ad2defb1a260841956c6780), [`1dcfd41`](https://github.com/Effect-TS/effect/commit/1dcfd41ff96abd706901293a00c1893cb29dd8fd), [`b21ab16`](https://github.com/Effect-TS/effect/commit/b21ab16b6f773e7ec4369db4e752c35e719f7870), [`fcf1822`](https://github.com/Effect-TS/effect/commit/fcf1822f98fcda60351d64e9d2c2c13563d7e6db), [`0061dd1`](https://github.com/Effect-TS/effect/commit/0061dd140740165e91569a684cce27a77b23229e), [`8421e6e`](https://github.com/Effect-TS/effect/commit/8421e6e49332bca8f96f482dfd48680e238b3a89), [`a9b3fb7`](https://github.com/Effect-TS/effect/commit/a9b3fb78abcfdb525318a956fd02fcadeb56143e), [`fa10f56`](https://github.com/Effect-TS/effect/commit/fa10f56b96bd9af070ba99ebc3279aa93954261e)]: + - effect@3.15.0 + - @effect/platform@0.82.0 + - @effect/cluster@0.33.0 + - @effect/rpc@0.59.0 + - @effect/sql@0.35.0 + +## 0.33.0 + +### Patch Changes + +- Updated dependencies [[`cd6cd0e`](https://github.com/Effect-TS/effect/commit/cd6cd0eacd6b09d6dd48b30b32edeb4a3c3075f9)]: + - @effect/rpc@0.58.0 + - @effect/cluster@0.32.0 + +## 0.32.1 + +### Patch Changes + +- Updated dependencies [[`24a9ebb`](https://github.com/Effect-TS/effect/commit/24a9ebbb5af598f0bfd6ecc45307e528043fe011)]: + - effect@3.14.22 + - @effect/cluster@0.31.1 + - @effect/platform@0.81.1 + - @effect/rpc@0.57.1 + - @effect/sql@0.34.1 + +## 0.32.0 + +### Patch Changes + +- Updated dependencies [[`672920f`](https://github.com/Effect-TS/effect/commit/672920f85da8abd5f9d4ad85e29248a2aca57ed8)]: + - @effect/platform@0.81.0 + - @effect/cluster@0.31.0 + - @effect/rpc@0.57.0 + - @effect/sql@0.34.0 + +## 0.31.11 + +### Patch Changes + +- Updated dependencies [[`2f3b7d4`](https://github.com/Effect-TS/effect/commit/2f3b7d4e1fa1ef8790b0ca4da22eb88872ee31df)]: + - effect@3.14.21 + - @effect/cluster@0.30.11 + - @effect/platform@0.80.21 + - @effect/rpc@0.56.9 + - @effect/sql@0.33.21 + +## 0.31.10 + +### Patch Changes + +- Updated dependencies [[`17e2f30`](https://github.com/Effect-TS/effect/commit/17e2f3091408cf0fca9414d4af3bdf7b2765b378)]: + - effect@3.14.20 + - @effect/cluster@0.30.10 + - @effect/platform@0.80.20 + - @effect/rpc@0.56.8 + - @effect/sql@0.33.20 + +## 0.31.9 + +### Patch Changes + +- [#4827](https://github.com/Effect-TS/effect/pull/4827) [`114dad9`](https://github.com/Effect-TS/effect/commit/114dad9a93613986eb5d306cbcfda3fb37ec1a1b) Thanks @mlegenhausen! - Fix `StreamAdapter` error forwarding + +- Updated dependencies [[`2d55bc5`](https://github.com/Effect-TS/effect/commit/2d55bc52c596afd8381f8ad1badc69efa0be8a78)]: + - @effect/cluster@0.30.9 + +## 0.31.8 + +### Patch Changes + +- Updated dependencies [[`1b30f61`](https://github.com/Effect-TS/effect/commit/1b30f616e75580933284657cb2cefab5a7903323)]: + - @effect/cluster@0.30.8 + +## 0.31.7 + +### Patch Changes + +- Updated dependencies [[`146af39`](https://github.com/Effect-TS/effect/commit/146af39d8d3b4e82aceb13de9749e6c4120c580b), [`056a910`](https://github.com/Effect-TS/effect/commit/056a910d0a0b8b00b0dc9df4a070466b2b5c2f6c), [`e25e7bb`](https://github.com/Effect-TS/effect/commit/e25e7bbc1797733916f48f501425d9f2ef310d9f), [`3273d57`](https://github.com/Effect-TS/effect/commit/3273d572c2b3175a842677f19efeea4cd65ab016)]: + - @effect/cluster@0.30.7 + - effect@3.14.19 + - @effect/platform@0.80.19 + - @effect/rpc@0.56.7 + - @effect/sql@0.33.19 + +## 0.31.6 + +### Patch Changes + +- Updated dependencies [[`b1164d4`](https://github.com/Effect-TS/effect/commit/b1164d49a1dfdf299e9971367b6fc6be4df0ddff)]: + - effect@3.14.18 + - @effect/cluster@0.30.6 + - @effect/platform@0.80.18 + - @effect/rpc@0.56.6 + - @effect/sql@0.33.18 + +## 0.31.5 + +### Patch Changes + +- Updated dependencies [[`0b54681`](https://github.com/Effect-TS/effect/commit/0b54681cd89245e211d8f49272be0f1bf2f81813), [`41a59d5`](https://github.com/Effect-TS/effect/commit/41a59d5916a296b12b0d5ead9e859e05f40b4cce)]: + - effect@3.14.17 + - @effect/cluster@0.30.5 + - @effect/platform@0.80.17 + - @effect/rpc@0.56.5 + - @effect/sql@0.33.17 + +## 0.31.4 + +### Patch Changes + +- Updated dependencies [[`ee14444`](https://github.com/Effect-TS/effect/commit/ee144441021ec77039e43396eaf90714687bb495), [`f1c8583`](https://github.com/Effect-TS/effect/commit/f1c8583f8c3ea9415f813795ca2940a897c9ba9a)]: + - effect@3.14.16 + - @effect/platform@0.80.16 + - @effect/cluster@0.30.4 + - @effect/rpc@0.56.4 + - @effect/sql@0.33.16 + +## 0.31.3 + +### Patch Changes + +- Updated dependencies [[`239cc99`](https://github.com/Effect-TS/effect/commit/239cc995ce645946210a3c3d2cb52bd3547c0687), [`8b6c947`](https://github.com/Effect-TS/effect/commit/8b6c947eaa8e45a67ecb3c37d45cd27f3e41d165), [`c50a63b`](https://github.com/Effect-TS/effect/commit/c50a63bbecb9f560b9cae349c447eed877d1b9b6)]: + - effect@3.14.15 + - @effect/cluster@0.30.3 + - @effect/platform@0.80.15 + - @effect/rpc@0.56.3 + - @effect/sql@0.33.15 + +## 0.31.2 + +### Patch Changes + +- Updated dependencies [[`664293f`](https://github.com/Effect-TS/effect/commit/664293f975a282920a7208e966adaf4634c42ef4), [`6ed8d15`](https://github.com/Effect-TS/effect/commit/6ed8d1589beb181d30abc79afebdaabc1d101538)]: + - @effect/cluster@0.30.2 + - effect@3.14.14 + - @effect/platform@0.80.14 + - @effect/rpc@0.56.2 + - @effect/sql@0.33.14 + +## 0.31.1 + +### Patch Changes + +- Updated dependencies [[`ee77788`](https://github.com/Effect-TS/effect/commit/ee77788747e7ebbde6bfa88256cde49dbbad3608), [`5fce6ba`](https://github.com/Effect-TS/effect/commit/5fce6ba19c3cc63cc0104e737e581ad989dedbf0), [`570e45f`](https://github.com/Effect-TS/effect/commit/570e45f8cb936e42ec48f67f21bb2b7252f36c0c)]: + - effect@3.14.13 + - @effect/cluster@0.30.1 + - @effect/platform@0.80.13 + - @effect/rpc@0.56.1 + - @effect/sql@0.33.13 + +## 0.31.0 + +### Patch Changes + +- Updated dependencies [[`d6e1156`](https://github.com/Effect-TS/effect/commit/d6e115617fc1a26a846b55f407965a330145dbee), [`2c66c16`](https://github.com/Effect-TS/effect/commit/2c66c16375dc2fe128f7b4e78c5f5c27c25c0d19)]: + - @effect/rpc@0.56.0 + - @effect/cluster@0.30.0 + +## 0.30.22 + +### Patch Changes + +- Updated dependencies [[`c2ad9ee`](https://github.com/Effect-TS/effect/commit/c2ad9ee9f3c4c743390edf35ed9e85a20be33811), [`9c68654`](https://github.com/Effect-TS/effect/commit/9c686542b6eb3ea188cb70673ef2e41223633e89)]: + - effect@3.14.12 + - @effect/cluster@0.29.22 + - @effect/platform@0.80.12 + - @effect/rpc@0.55.17 + - @effect/sql@0.33.12 + +## 0.30.21 + +### Patch Changes + +- Updated dependencies [[`e536127`](https://github.com/Effect-TS/effect/commit/e536127c1e6f2fb3a542c73ae919435a629a346b), [`b5ad11e`](https://github.com/Effect-TS/effect/commit/b5ad11e511424c6d5c32e34e7ee9d04f0110617d)]: + - effect@3.14.11 + - @effect/rpc@0.55.16 + - @effect/cluster@0.29.21 + - @effect/platform@0.80.11 + - @effect/sql@0.33.11 + +## 0.30.20 + +### Patch Changes + +- Updated dependencies [[`d3df84e`](https://github.com/Effect-TS/effect/commit/d3df84e8af8e00a297e2329faeae625de0a95a71)]: + - @effect/rpc@0.55.15 + - @effect/cluster@0.29.20 + +## 0.30.19 + +### Patch Changes + +- Updated dependencies [[`bc7efa3`](https://github.com/Effect-TS/effect/commit/bc7efa3b031bb25e1ed3c8f2d3fb5e8da166cadc)]: + - effect@3.14.10 + - @effect/cluster@0.29.19 + - @effect/platform@0.80.10 + - @effect/rpc@0.55.14 + - @effect/sql@0.33.10 + +## 0.30.18 + +### Patch Changes + +- Updated dependencies [[`d78249f`](https://github.com/Effect-TS/effect/commit/d78249f0b67f63cf4baf806ff090cba33293daf0)]: + - effect@3.14.9 + - @effect/cluster@0.29.18 + - @effect/platform@0.80.9 + - @effect/rpc@0.55.13 + - @effect/sql@0.33.9 + +## 0.30.17 + +### Patch Changes + +- Updated dependencies [[`58eaca9`](https://github.com/Effect-TS/effect/commit/58eaca9ef14032fc310f4a0e3c09513bac1cb50a)]: + - @effect/rpc@0.55.12 + - @effect/cluster@0.29.17 + +## 0.30.16 + +### Patch Changes + +- Updated dependencies [[`a79b732`](https://github.com/Effect-TS/effect/commit/a79b732bddea8bfca091c4fed0dd87aa0b1ab1f0), [`a79b732`](https://github.com/Effect-TS/effect/commit/a79b732bddea8bfca091c4fed0dd87aa0b1ab1f0)]: + - @effect/cluster@0.29.16 + +## 0.30.15 + +### Patch Changes + +- Updated dependencies [[`6966708`](https://github.com/Effect-TS/effect/commit/6966708a3061a3eb4bcfcb4d5877657fb41a019a)]: + - @effect/cluster@0.29.15 + +## 0.30.14 + +### Patch Changes + +- Updated dependencies [[`da21953`](https://github.com/Effect-TS/effect/commit/da21953a3831bf5974ab6add8fcc7fad1c0ba472)]: + - @effect/cluster@0.29.14 + +## 0.30.13 + +### Patch Changes + +- Updated dependencies [[`b3a2d32`](https://github.com/Effect-TS/effect/commit/b3a2d32772e6f7f20eacf2e18128e99324c4d378), [`896fbbf`](https://github.com/Effect-TS/effect/commit/896fbbf6ed6c11e099747e8aafb67b28edc4e466)]: + - effect@3.14.8 + - @effect/cluster@0.29.13 + - @effect/platform@0.80.8 + - @effect/rpc@0.55.11 + - @effect/sql@0.33.8 + +## 0.30.12 + +### Patch Changes + +- Updated dependencies [[`b542a4b`](https://github.com/Effect-TS/effect/commit/b542a4bf195be0c9af1523e1ba96c953decc4d25)]: + - effect@3.14.7 + - @effect/cluster@0.29.12 + - @effect/platform@0.80.7 + - @effect/rpc@0.55.10 + - @effect/sql@0.33.7 + +## 0.30.11 + +### Patch Changes + +- Updated dependencies [[`a1d4673`](https://github.com/Effect-TS/effect/commit/a1d4673a423dfed050c0a762664d9d64002cfa90)]: + - @effect/rpc@0.55.9 + - @effect/cluster@0.29.11 + +## 0.30.10 + +### Patch Changes + +- Updated dependencies [[`47618c1`](https://github.com/Effect-TS/effect/commit/47618c1ad84ebcc5a51133a3fff5aa5012d49d45), [`6077882`](https://github.com/Effect-TS/effect/commit/60778824a4794336c33807801f813f8751d1c7e4)]: + - effect@3.14.6 + - @effect/cluster@0.29.10 + - @effect/platform@0.80.6 + - @effect/rpc@0.55.8 + - @effect/sql@0.33.6 + +## 0.30.9 + +### Patch Changes + +- Updated dependencies [[`4414042`](https://github.com/Effect-TS/effect/commit/44140423a2fb185f92f7db4d5b383f9b62a97bf9)]: + - @effect/rpc@0.55.7 + - @effect/cluster@0.29.9 + +## 0.30.8 + +### Patch Changes + +- Updated dependencies [[`40dbfef`](https://github.com/Effect-TS/effect/commit/40dbfeff239b6e567706752114f31b2fce7de4e3), [`85fba81`](https://github.com/Effect-TS/effect/commit/85fba815ac07eb13d4227a69ac76a18e4b94df18), [`5a5ebdd`](https://github.com/Effect-TS/effect/commit/5a5ebdddfaddd259538b4599a6676281faca778e)]: + - effect@3.14.5 + - @effect/platform@0.80.5 + - @effect/cluster@0.29.8 + - @effect/rpc@0.55.6 + - @effect/sql@0.33.5 + +## 0.30.7 + +### Patch Changes + +- Updated dependencies [[`e4ba2c6`](https://github.com/Effect-TS/effect/commit/e4ba2c66a878e81b5e295d6d49aaf724b80a28ef), [`e3e5873`](https://github.com/Effect-TS/effect/commit/e3e5873f30080bb0e5eed8a876170acaa6ed47ff), [`26c060c`](https://github.com/Effect-TS/effect/commit/26c060c65914a623220a20356991784f974bfe18)]: + - effect@3.14.4 + - @effect/rpc@0.55.5 + - @effect/cluster@0.29.7 + - @effect/platform@0.80.4 + - @effect/sql@0.33.4 + +## 0.30.6 + +### Patch Changes + +- Updated dependencies [[`0ec5e03`](https://github.com/Effect-TS/effect/commit/0ec5e0353a1db5d27c3500deba0df61001258e76), [`05c4d77`](https://github.com/Effect-TS/effect/commit/05c4d772acc42b7425add7b22f914c5ee3ff84bd), [`37aa8e1`](https://github.com/Effect-TS/effect/commit/37aa8e137725a902e70cd1e468ea98b873aa5056), [`34f03d6`](https://github.com/Effect-TS/effect/commit/34f03d66875f21f266f102223a03cd14c2ed6ea6)]: + - @effect/rpc@0.55.4 + - effect@3.14.3 + - @effect/cluster@0.29.6 + - @effect/platform@0.80.3 + - @effect/sql@0.33.3 + +## 0.30.5 + +### Patch Changes + +- Updated dependencies [[`f87991b`](https://github.com/Effect-TS/effect/commit/f87991b6d8a2edfaf90b01cebda4b466992ae865), [`f87991b`](https://github.com/Effect-TS/effect/commit/f87991b6d8a2edfaf90b01cebda4b466992ae865), [`0a3e3e1`](https://github.com/Effect-TS/effect/commit/0a3e3e18eea5e0d1882f1a6c906198e6ef226a41)]: + - effect@3.14.2 + - @effect/cluster@0.29.5 + - @effect/platform@0.80.2 + - @effect/rpc@0.55.3 + - @effect/sql@0.33.2 + +## 0.30.4 + +### Patch Changes + +- Updated dependencies [[`d2f11e5`](https://github.com/Effect-TS/effect/commit/d2f11e557de4639762124951252170fbf4d7c906)]: + - @effect/rpc@0.55.2 + - @effect/cluster@0.29.4 + +## 0.30.3 + +### Patch Changes + +- Updated dependencies [[`18a7936`](https://github.com/Effect-TS/effect/commit/18a7936832158daa69e3c09a6caae55e3d6c0b86)]: + - @effect/cluster@0.29.3 + +## 0.30.2 + +### Patch Changes + +- Updated dependencies [[`3a99a2d`](https://github.com/Effect-TS/effect/commit/3a99a2dbaa38348c1f6e210a531fcfb99b5e73c5)]: + - @effect/cluster@0.29.2 + +## 0.30.1 + +### Patch Changes + +- Updated dependencies [[`4a274fe`](https://github.com/Effect-TS/effect/commit/4a274fe9f623182b6b902827e0e83bd89ca3b05c), [`814733f`](https://github.com/Effect-TS/effect/commit/814733fe62bb3dc91c6cd632d16a8d2076b3755b)]: + - effect@3.14.1 + - @effect/cluster@0.29.1 + - @effect/platform@0.80.1 + - @effect/rpc@0.55.1 + - @effect/sql@0.33.1 + +## 0.30.0 + +### Minor Changes + +- [#4469](https://github.com/Effect-TS/effect/pull/4469) [`3131f8f`](https://github.com/Effect-TS/effect/commit/3131f8fd12ba9eb31b90fa2f42bf88b12309133c) Thanks @tim-smart! - refactor of @effect/cluster packages + +### Patch Changes + +- [#4469](https://github.com/Effect-TS/effect/pull/4469) [`aba2d1d`](https://github.com/Effect-TS/effect/commit/aba2d1d831ea149481bd4dd755528c0afa8239ce) Thanks @tim-smart! - Move SocketServer modules to @effect/platform + +- Updated dependencies [[`1f47e4e`](https://github.com/Effect-TS/effect/commit/1f47e4e12546ab691b29bfb7b5128bb17b93baa5), [`26dd75f`](https://github.com/Effect-TS/effect/commit/26dd75f276a0d8a63eab313bd5a167d5072c9780), [`aba2d1d`](https://github.com/Effect-TS/effect/commit/aba2d1d831ea149481bd4dd755528c0afa8239ce), [`3131f8f`](https://github.com/Effect-TS/effect/commit/3131f8fd12ba9eb31b90fa2f42bf88b12309133c), [`aba2d1d`](https://github.com/Effect-TS/effect/commit/aba2d1d831ea149481bd4dd755528c0afa8239ce), [`aba2d1d`](https://github.com/Effect-TS/effect/commit/aba2d1d831ea149481bd4dd755528c0afa8239ce), [`04dff2d`](https://github.com/Effect-TS/effect/commit/04dff2d01ac68c260f29a6d4743381825c353c86), [`c7fac0c`](https://github.com/Effect-TS/effect/commit/c7fac0cd7eadcd5cc0c3a987051c5b57ad271638), [`aba2d1d`](https://github.com/Effect-TS/effect/commit/aba2d1d831ea149481bd4dd755528c0afa8239ce), [`ffaa3f3`](https://github.com/Effect-TS/effect/commit/ffaa3f3969df26610fcc02ad537340641d44e803), [`ab957c1`](https://github.com/Effect-TS/effect/commit/ab957c1fee714868f56c7ab4e802b9d449e9b666), [`35db9ce`](https://github.com/Effect-TS/effect/commit/35db9ce228f1416c8abacc6dc9c36fbd0f33ef0f), [`aba2d1d`](https://github.com/Effect-TS/effect/commit/aba2d1d831ea149481bd4dd755528c0afa8239ce), [`cf77ea9`](https://github.com/Effect-TS/effect/commit/cf77ea9ab4fc89e66a43f682a9926ccdee6c57ed), [`26dd75f`](https://github.com/Effect-TS/effect/commit/26dd75f276a0d8a63eab313bd5a167d5072c9780), [`baaab60`](https://github.com/Effect-TS/effect/commit/baaab60b737f35dfab8e4a21bce28a195d19e899)]: + - effect@3.14.0 + - @effect/platform@0.80.0 + - @effect/cluster@0.29.0 + - @effect/rpc@0.55.0 + - @effect/sql@0.33.0 + +## 0.29.4 + +### Patch Changes + +- Updated dependencies [[`5662363`](https://github.com/Effect-TS/effect/commit/566236361e270e575ef1cbf308ad1967c82a362c), [`5f1fd15`](https://github.com/Effect-TS/effect/commit/5f1fd15308ab154791580059b89877d19a2055c2), [`8bb1460`](https://github.com/Effect-TS/effect/commit/8bb1460c824f66f0f25ebd899c5e74e388089c37)]: + - @effect/platform@0.79.4 + +## 0.29.3 + +### Patch Changes + +- Updated dependencies [[`0c4803f`](https://github.com/Effect-TS/effect/commit/0c4803fcc69262d11a97ce49d0e9b4288df0651f), [`6f65ac4`](https://github.com/Effect-TS/effect/commit/6f65ac4eac1489cd6ea390e18b0908670722adad)]: + - effect@3.13.12 + - @effect/platform@0.79.3 + +## 0.29.2 + +### Patch Changes + +- Updated dependencies [[`fad8cca`](https://github.com/Effect-TS/effect/commit/fad8cca9bbfcc2eaeb44b97c15dbe0a1eda75315), [`4296293`](https://github.com/Effect-TS/effect/commit/4296293049414d0cf2d915a26c552b09f946b9a0), [`9c241ab`](https://github.com/Effect-TS/effect/commit/9c241abe47ccf7a5257b98a4a64a63054a12741d), [`082b0c1`](https://github.com/Effect-TS/effect/commit/082b0c1b9f4252bcdd69608f2e4a9226f953ac3f), [`be12983`](https://github.com/Effect-TS/effect/commit/be12983bc7e7537b41cd8910fc4eb7d1da56ab07), [`de88127`](https://github.com/Effect-TS/effect/commit/de88127a5a5906ccece98af74787b5ae0e65e431)]: + - effect@3.13.11 + - @effect/platform@0.79.2 + +## 0.29.1 + +### Patch Changes + +- Updated dependencies [[`527c964`](https://github.com/Effect-TS/effect/commit/527c9645229f5be9714a7e60a38a9e753c4bbfb1)]: + - effect@3.13.10 + - @effect/platform@0.79.1 + +## 0.29.0 + +### Patch Changes + +- Updated dependencies [[`88fe129`](https://github.com/Effect-TS/effect/commit/88fe12923740765c0335a6e6203fdcc6a463edca), [`d630249`](https://github.com/Effect-TS/effect/commit/d630249426113088abe8b382db4f14d80f2160c2), [`2976e52`](https://github.com/Effect-TS/effect/commit/2976e52538d9dc9ffdcbc84d4ac748cff9305971)]: + - @effect/platform@0.79.0 + - effect@3.13.9 + +## 0.28.1 + +### Patch Changes + +- Updated dependencies [[`c65d336`](https://github.com/Effect-TS/effect/commit/c65d3362d07ec815ff3b46278314e8a31706ddc2), [`22d2ebb`](https://github.com/Effect-TS/effect/commit/22d2ebb4b11f5a44351a4736e65da391a3b647d0)]: + - effect@3.13.8 + - @effect/platform@0.78.1 + +## 0.28.0 + +### Patch Changes + +- [#4560](https://github.com/Effect-TS/effect/pull/4560) [`6cd4a10`](https://github.com/Effect-TS/effect/commit/6cd4a102f5d8e88c313dfa75c039617e8831b70e) Thanks @tim-smart! - ensure empty Chunk's don't cause NodeStream.toReadable to hang + +- Updated dependencies [[`c5bcf53`](https://github.com/Effect-TS/effect/commit/c5bcf53b7cb49dacffdd2a6cd8eb48cc452b417e)]: + - @effect/platform@0.78.0 + +## 0.27.7 + +### Patch Changes + +- [#4558](https://github.com/Effect-TS/effect/pull/4558) [`05306d5`](https://github.com/Effect-TS/effect/commit/05306d5cc55b94a23c175de798fc6a5e93a3ab74) Thanks @tim-smart! - refactor NodeStream.toReadable + +- Updated dependencies [[`840cc73`](https://github.com/Effect-TS/effect/commit/840cc7329908db7ca693ef47b07d4f845c29cadd), [`9bf8a74`](https://github.com/Effect-TS/effect/commit/9bf8a74b967f18d931743dd5196af326c9118e9c), [`87ba23c`](https://github.com/Effect-TS/effect/commit/87ba23c41c193503ed0c612b0d32d0b253794c64), [`f910880`](https://github.com/Effect-TS/effect/commit/f91088069057f3b4529753f5bc5532b028d726df), [`0d01480`](https://github.com/Effect-TS/effect/commit/0d014803e4f688f74386a80abd65485e1a319244)]: + - @effect/platform@0.77.7 + - effect@3.13.7 + +## 0.27.6 + +### Patch Changes + +- Updated dependencies [[`3154ce4`](https://github.com/Effect-TS/effect/commit/3154ce4692fa18b804982158d3c4c8a8a5fae386)]: + - effect@3.13.6 + - @effect/platform@0.77.6 + +## 0.27.5 + +### Patch Changes + +- Updated dependencies [[`367bb35`](https://github.com/Effect-TS/effect/commit/367bb35f4c2a254e1fb211d96db2474a7aed9020), [`6cf11c3`](https://github.com/Effect-TS/effect/commit/6cf11c3a75773ceec2877c85ddc760f381f0866d), [`a0acec8`](https://github.com/Effect-TS/effect/commit/a0acec851f72e19466363d24b9cc218acd00006a)]: + - effect@3.13.5 + - @effect/platform@0.77.5 + +## 0.27.4 + +### Patch Changes + +- Updated dependencies [[`e0746f9`](https://github.com/Effect-TS/effect/commit/e0746f9aa398b69c6542e375910683bf17f49f46), [`17d9e89`](https://github.com/Effect-TS/effect/commit/17d9e89f9851663bdbb6c1e685601d97806114a4)]: + - @effect/platform@0.77.4 + - effect@3.13.4 + +## 0.27.3 + +### Patch Changes + +- Updated dependencies [[`cc5588d`](https://github.com/Effect-TS/effect/commit/cc5588df07f9103513547cb429ce041b9436a8bd), [`623c8cd`](https://github.com/Effect-TS/effect/commit/623c8cd053ed6ee3d353aaa8778d484670fca2bb), [`00b4eb1`](https://github.com/Effect-TS/effect/commit/00b4eb1ece12a16e222e6220965bb4024d6752ac), [`f2aee98`](https://github.com/Effect-TS/effect/commit/f2aee989b0a600900ce83e7f460d02908620c80f), [`fb798eb`](https://github.com/Effect-TS/effect/commit/fb798eb9061f1191badc017d1aa649360254da20), [`2251b15`](https://github.com/Effect-TS/effect/commit/2251b1528810bb695b37ce388b653cec0c5bf80c), [`2e15c1e`](https://github.com/Effect-TS/effect/commit/2e15c1e33648add0b29fe274fbcb7294b7515085), [`a4979db`](https://github.com/Effect-TS/effect/commit/a4979db021aef16e731be64df196b72088fc4376), [`b74255a`](https://github.com/Effect-TS/effect/commit/b74255a304ad49d60bedb1a260fd697f370af27a), [`d7f6a5c`](https://github.com/Effect-TS/effect/commit/d7f6a5c7d26c1963dcd864ca62360d20d08c7b49), [`9dd8979`](https://github.com/Effect-TS/effect/commit/9dd8979e940915b1cc1b1f264f3d019c77a65a02), [`477b488`](https://github.com/Effect-TS/effect/commit/477b488284f47c5469d7fba3e4065fb7e3b6556e), [`10932cb`](https://github.com/Effect-TS/effect/commit/10932cbf58fc721ada631cebec42f773ce96d3cc), [`9f6c784`](https://github.com/Effect-TS/effect/commit/9f6c78468b3b5e9ebfc38ffdfb70702901ee977b), [`2c639ec`](https://github.com/Effect-TS/effect/commit/2c639ecee332de4266e36022c989c35ae4e02105), [`886aaa8`](https://github.com/Effect-TS/effect/commit/886aaa81e06dfd3cd9391e8ea987d8cd5ada1124)]: + - effect@3.13.3 + - @effect/platform@0.77.3 + +## 0.27.2 + +### Patch Changes + +- Updated dependencies [[`31be72a`](https://github.com/Effect-TS/effect/commit/31be72ada118cb84a942e67b1663263f8db74a9f), [`3e7ce97`](https://github.com/Effect-TS/effect/commit/3e7ce97f8a41756a039cf635d0b3d9a75d781097), [`31be72a`](https://github.com/Effect-TS/effect/commit/31be72ada118cb84a942e67b1663263f8db74a9f)]: + - effect@3.13.2 + - @effect/platform@0.77.2 + +## 0.27.1 + +### Patch Changes + +- Updated dependencies [[`b56a211`](https://github.com/Effect-TS/effect/commit/b56a2110569fd0ec0b57ac137743e926d49f51cc)]: + - effect@3.13.1 + - @effect/platform@0.77.1 + +## 0.27.0 + +### Patch Changes + +- Updated dependencies [[`8baef83`](https://github.com/Effect-TS/effect/commit/8baef83e7ff0b7bc0738b680e1ef013065386cff), [`655bfe2`](https://github.com/Effect-TS/effect/commit/655bfe29e44cc3f0fb9b4e53038f50b891c188df), [`d90cbc2`](https://github.com/Effect-TS/effect/commit/d90cbc274e2742d18671fe65aa4764c057eb6cba), [`75632bd`](https://github.com/Effect-TS/effect/commit/75632bd44b8025101d652ccbaeef898c7086c91c), [`c874a2e`](https://github.com/Effect-TS/effect/commit/c874a2e4b17e9d71904ca8375bb77b020975cb1d), [`bf865e5`](https://github.com/Effect-TS/effect/commit/bf865e5833f77fd8f6c06944ca9d507b54488301), [`f98b2b7`](https://github.com/Effect-TS/effect/commit/f98b2b7592cf20f9d85313e7f1e964cb65878138), [`de8ce92`](https://github.com/Effect-TS/effect/commit/de8ce924923eaa4e1b761a97eb45ec967389f3d5), [`cf8b2dd`](https://github.com/Effect-TS/effect/commit/cf8b2dd112f8e092ed99d78fd728db0f91c29050), [`db426a5`](https://github.com/Effect-TS/effect/commit/db426a5fb41ab84d18e3c8753a7329b4de544245), [`6862444`](https://github.com/Effect-TS/effect/commit/6862444094906ad4f2cb077ff3b9cc0b73880c8c), [`5fc8a90`](https://github.com/Effect-TS/effect/commit/5fc8a90ba46a5fd9f3b643f0b5aeadc69d717339), [`546a492`](https://github.com/Effect-TS/effect/commit/546a492e60eb2b8b048a489a474b934ea0877005), [`65c4796`](https://github.com/Effect-TS/effect/commit/65c47966ce39055f02cf5c808daabb3ea6442b0b), [`9760fdc`](https://github.com/Effect-TS/effect/commit/9760fdc37bdaef9da8b150e46b86ddfbe2ad9221), [`5b471e7`](https://github.com/Effect-TS/effect/commit/5b471e7d4317e8ee5d72bbbd3e0c9775160949ab), [`4f810cc`](https://github.com/Effect-TS/effect/commit/4f810cc2770e9f1f266851d2cb6257112c12af49)]: + - effect@3.13.0 + - @effect/platform@0.77.0 + +## 0.26.1 + +### Patch Changes + +- Updated dependencies [[`4018eae`](https://github.com/Effect-TS/effect/commit/4018eaed2733241676ddb8c52416f463a8c32e35), [`543d36d`](https://github.com/Effect-TS/effect/commit/543d36d1a11452560b01ab966a82529ad5fee8c9), [`c407726`](https://github.com/Effect-TS/effect/commit/c407726f79df4a567a9631cddd8effaa16b3535d), [`f70a65a`](https://github.com/Effect-TS/effect/commit/f70a65ac80c6635d80b12beaf4d32a9cc59fa143), [`ba409f6`](https://github.com/Effect-TS/effect/commit/ba409f69c41aeaa29e475c0630735726eaf4dbac), [`3d2e356`](https://github.com/Effect-TS/effect/commit/3d2e3565e8a43d1bdb5daee8db3b90f56d71d859)]: + - effect@3.12.12 + - @effect/platform@0.76.1 + +## 0.26.0 + +### Patch Changes + +- Updated dependencies [[`b6a032f`](https://github.com/Effect-TS/effect/commit/b6a032f07bffa020a848c813881879395134fa20), [`42ddd5f`](https://github.com/Effect-TS/effect/commit/42ddd5f144ce9f9d94a036679ebbd626446d37f5), [`2fe447c`](https://github.com/Effect-TS/effect/commit/2fe447c6354d334f9c591b8a8481818f5f0e797e), [`2473ad5`](https://github.com/Effect-TS/effect/commit/2473ad5cf23582e3a41338091fa526ffe611288d)]: + - effect@3.12.11 + - @effect/platform@0.76.0 + +## 0.25.4 + +### Patch Changes + +- Updated dependencies [[`e30f132`](https://github.com/Effect-TS/effect/commit/e30f132c336c9d0760bad39f82a55c7ce5159eb7), [`33fa667`](https://github.com/Effect-TS/effect/commit/33fa667c2623be1026e1ccee91bd44f73b09020a), [`87f5f28`](https://github.com/Effect-TS/effect/commit/87f5f2842e4196cb88d13f10f443ff0567e82832), [`7d57ecd`](https://github.com/Effect-TS/effect/commit/7d57ecdaf5da2345ebbf9c22df50317578bde0f5), [`4dbd170`](https://github.com/Effect-TS/effect/commit/4dbd170538e8fb7a36aa7c469c6f93b6c7000091)]: + - effect@3.12.10 + - @effect/platform@0.75.4 + +## 0.25.3 + +### Patch Changes + +- Updated dependencies [[`1b4a4e9`](https://github.com/Effect-TS/effect/commit/1b4a4e904ef5227ec7d9114d4e417eca19eed940)]: + - effect@3.12.9 + - @effect/platform@0.75.3 + +## 0.25.2 + +### Patch Changes + +- [#4345](https://github.com/Effect-TS/effect/pull/4345) [`c9175ae`](https://github.com/Effect-TS/effect/commit/c9175aef41cb1e3b689d0ac0a4f53d8107376b58) Thanks @ethanniser! - Addition of `sync` property to `FileSystem.File` representing the `fsync` syscall. + +- Updated dependencies [[`59b3cfb`](https://github.com/Effect-TS/effect/commit/59b3cfbbd5713dd9475998e95fad5534c0b21466), [`766113c`](https://github.com/Effect-TS/effect/commit/766113c0ea3512cdb887650ead8ba314236e22ee), [`bb05fb8`](https://github.com/Effect-TS/effect/commit/bb05fb83457355b1ca567228a9e041edfb6fd85d), [`712277f`](https://github.com/Effect-TS/effect/commit/712277f949052a24b46e4aa234063a6abf395c90), [`f269122`](https://github.com/Effect-TS/effect/commit/f269122508693b111142994dd48698ddc75f3d69), [`8f6006a`](https://github.com/Effect-TS/effect/commit/8f6006a610fb6d6c7b8d14209a7323338a8964ff), [`c45b559`](https://github.com/Effect-TS/effect/commit/c45b5592b5fd1189a5c932cfe05bd7d5f6d68508), [`430c846`](https://github.com/Effect-TS/effect/commit/430c846cbac05b187e3d24ac8dfee0cf22506f7c), [`7b03057`](https://github.com/Effect-TS/effect/commit/7b03057507d2dab5e6793beb9c578dedaaeb15fe), [`a9c94c8`](https://github.com/Effect-TS/effect/commit/a9c94c807755610831211a686d2fad849ab38eb4), [`107e6f0`](https://github.com/Effect-TS/effect/commit/107e6f0557a1e2d3b0dce25d62fa1e2601521752), [`c9175ae`](https://github.com/Effect-TS/effect/commit/c9175aef41cb1e3b689d0ac0a4f53d8107376b58), [`65c11b9`](https://github.com/Effect-TS/effect/commit/65c11b9266ec9447c31c26fe3ed35c73bd3b81fd), [`e386d2f`](https://github.com/Effect-TS/effect/commit/e386d2f1b3ab3ac2c14ee76de11f5963d32a3df4), [`9172efb`](https://github.com/Effect-TS/effect/commit/9172efba98bc6a82353e6ec2af61ac08f038ba64)]: + - @effect/platform@0.75.2 + - effect@3.12.8 + +## 0.25.1 + +### Patch Changes + +- Updated dependencies [[`8dff1d1`](https://github.com/Effect-TS/effect/commit/8dff1d1bff76cdba643cad7f0bf864300f08bc61)]: + - effect@3.12.7 + - @effect/platform@0.75.1 + +## 0.25.0 + +### Patch Changes + +- Updated dependencies [[`5e43ce5`](https://github.com/Effect-TS/effect/commit/5e43ce50bae116865906112e7f88d390739d778b), [`289c13b`](https://github.com/Effect-TS/effect/commit/289c13b38e8e35b214d46d385d05dead176c87cd), [`76eb7d0`](https://github.com/Effect-TS/effect/commit/76eb7d0fbce3c009c8f77e84c178cb15bbed9709), [`8b4e75d`](https://github.com/Effect-TS/effect/commit/8b4e75d35daea807c447ca760948a717aa66bb52), [`fc5e0f0`](https://github.com/Effect-TS/effect/commit/fc5e0f0d357a0051cfa01c1ede83ffdd3cb41ab1), [`004fd2b`](https://github.com/Effect-TS/effect/commit/004fd2bbd1459e64fb1b57f02eeb791ca5ea1ea5), [`b2a31be`](https://github.com/Effect-TS/effect/commit/b2a31be85c35d891351ce4f9a2cc93ece0c257f6), [`5514d05`](https://github.com/Effect-TS/effect/commit/5514d05b5cd586ff5868b8bd41c959e95e6c33cd), [`bf5f0ae`](https://github.com/Effect-TS/effect/commit/bf5f0ae9daa0170471678e22585e8ec14ce667bb), [`3b19bcf`](https://github.com/Effect-TS/effect/commit/3b19bcfd3aaadb6c9253428622df524537c8e626), [`b064b3b`](https://github.com/Effect-TS/effect/commit/b064b3b293615fd268cc5a5647d0981eb67750b8), [`eb264ed`](https://github.com/Effect-TS/effect/commit/eb264ed8a6e8c92a9dc7006f766c6ca2e5d29e03), [`289c13b`](https://github.com/Effect-TS/effect/commit/289c13b38e8e35b214d46d385d05dead176c87cd), [`f474678`](https://github.com/Effect-TS/effect/commit/f474678bf10b8f1c80e3dc096ddc7ecf20b2b23e), [`ee187d0`](https://github.com/Effect-TS/effect/commit/ee187d098007a402844c94d04f0cd8f07695377a)]: + - @effect/platform@0.75.0 + - effect@3.12.6 + +## 0.24.0 + +### Patch Changes + +- Updated dependencies [[`a8b0ddb`](https://github.com/Effect-TS/effect/commit/a8b0ddb84710054799fc8f57485b95d00093ada1), [`8653072`](https://github.com/Effect-TS/effect/commit/86530720d7a03e118d2c5a8bf5a997cee7e7f3d6), [`507d546`](https://github.com/Effect-TS/effect/commit/507d546bd49db31000425fb5da88c434e4291bea), [`a8b0ddb`](https://github.com/Effect-TS/effect/commit/a8b0ddb84710054799fc8f57485b95d00093ada1), [`8db239b`](https://github.com/Effect-TS/effect/commit/8db239b9c869a3707f6566b9d9dbdf53c4df03fc), [`84a0911`](https://github.com/Effect-TS/effect/commit/84a091181634c3a022c94234cec7764a3aeef1be), [`84a0911`](https://github.com/Effect-TS/effect/commit/84a091181634c3a022c94234cec7764a3aeef1be), [`3179a9f`](https://github.com/Effect-TS/effect/commit/3179a9f65d23369a6a9a1f80f7750566dd28df22), [`6cb9b76`](https://github.com/Effect-TS/effect/commit/6cb9b766396d0b2ed995cf26957359713efd202e), [`1fcbe55`](https://github.com/Effect-TS/effect/commit/1fcbe55345042d8468f6a98c84081bd00b6bcf5a), [`d9a63d9`](https://github.com/Effect-TS/effect/commit/d9a63d9d385653865954cac895065360d54cc56b), [`6cb9b76`](https://github.com/Effect-TS/effect/commit/6cb9b766396d0b2ed995cf26957359713efd202e)]: + - effect@3.12.5 + - @effect/platform@0.74.0 + +## 0.23.1 + +### Patch Changes + +- Updated dependencies [[`5b50ea4`](https://github.com/Effect-TS/effect/commit/5b50ea4a10cf9acd51f9624b2474d9d5ded74019), [`c170a68`](https://github.com/Effect-TS/effect/commit/c170a68b6266100774461fcd6c0e0fabb60112f2), [`a66c2eb`](https://github.com/Effect-TS/effect/commit/a66c2eb473245092cd41f04c2eb2b7b02cf53718), [`c9e5e1b`](https://github.com/Effect-TS/effect/commit/c9e5e1be17c0c84d3d4e2abc3c60215cdb56bbbe), [`7b3d58d`](https://github.com/Effect-TS/effect/commit/7b3d58d7aec2152ec282460871d3e9de45ed254d)]: + - effect@3.12.4 + - @effect/platform@0.73.1 + +## 0.23.0 + +### Patch Changes + +- [#4242](https://github.com/Effect-TS/effect/pull/4242) [`c1a0339`](https://github.com/Effect-TS/effect/commit/c1a0339034a291fd4463371afbcfc8adcf8994ae) Thanks @fubhy! - Add missing exports + +- Updated dependencies [[`d7dac48`](https://github.com/Effect-TS/effect/commit/d7dac48a477cdfeec509dbe9f33fce6a1b02b63d), [`c110032`](https://github.com/Effect-TS/effect/commit/c110032322450a8824ba38ae24335a538cd2ce9a), [`1d7fd2b`](https://github.com/Effect-TS/effect/commit/1d7fd2b7ee8eeecc912d27adf76ed897db236dc5), [`1d7fd2b`](https://github.com/Effect-TS/effect/commit/1d7fd2b7ee8eeecc912d27adf76ed897db236dc5), [`23ac740`](https://github.com/Effect-TS/effect/commit/23ac740c7dd4610b7d265c2071b88b0968419e9a), [`8cd7319`](https://github.com/Effect-TS/effect/commit/8cd7319b6568bfc7a30ca16c104d189e37eac3a0)]: + - effect@3.12.3 + - @effect/platform@0.73.0 + +## 0.22.2 + +### Patch Changes + +- Updated dependencies [[`734af82`](https://github.com/Effect-TS/effect/commit/734af82138e78b9c57a8355b1c6b80e80d38b222), [`b63c780`](https://github.com/Effect-TS/effect/commit/b63c78010893101520448ddda7019c487cf7eedd), [`212e784`](https://github.com/Effect-TS/effect/commit/212e78475f527147ec27c090bd13f789f55add7a), [`f852cb0`](https://github.com/Effect-TS/effect/commit/f852cb02040ea2f165e9b449615b8b1366add5d5), [`7276ae2`](https://github.com/Effect-TS/effect/commit/7276ae21062896adbb7508ac5b2dece95316322f), [`212e784`](https://github.com/Effect-TS/effect/commit/212e78475f527147ec27c090bd13f789f55add7a), [`212e784`](https://github.com/Effect-TS/effect/commit/212e78475f527147ec27c090bd13f789f55add7a), [`212e784`](https://github.com/Effect-TS/effect/commit/212e78475f527147ec27c090bd13f789f55add7a), [`c640d77`](https://github.com/Effect-TS/effect/commit/c640d77b33ad417876f4e8ffe8574ee6cbe5607f), [`0def088`](https://github.com/Effect-TS/effect/commit/0def0887cfdb6755729a64dfd52b3b9f46b0576c)]: + - effect@3.12.2 + - @effect/platform@0.72.2 + +## 0.22.1 + +### Patch Changes + +- Updated dependencies [[`302b57d`](https://github.com/Effect-TS/effect/commit/302b57d2cbf9b9ccc17450945aeebfb33cfe8d43), [`0988083`](https://github.com/Effect-TS/effect/commit/0988083d4594938590df5a287e5b27d38526dd07), [`8b46be6`](https://github.com/Effect-TS/effect/commit/8b46be6a3b8160362ab5ea9171c5e6932505125c), [`bfe8027`](https://github.com/Effect-TS/effect/commit/bfe802734b450a4b4ee069d1125dd37995db2bff), [`16dd657`](https://github.com/Effect-TS/effect/commit/16dd657033d8afac2ffea567b3c8bb27c9b249b6), [`39db211`](https://github.com/Effect-TS/effect/commit/39db211414e90c8db8fdad7dc8ce5b4661bcfaef)]: + - effect@3.12.1 + - @effect/platform@0.72.1 + +## 0.22.0 + +### Patch Changes + +- Updated dependencies [[`abb22a4`](https://github.com/Effect-TS/effect/commit/abb22a429b9c52c31e84856294f175d2064a9b4d), [`f369a89`](https://github.com/Effect-TS/effect/commit/f369a89e98bc682969803b9304adaf4557bb36c2), [`642376c`](https://github.com/Effect-TS/effect/commit/642376c63fd7d78754db991631a4d50a5dc79aa3), [`3d2b7a7`](https://github.com/Effect-TS/effect/commit/3d2b7a7e942a7157afae5b1cdbc6f3fef116428e), [`ef64c6f`](https://github.com/Effect-TS/effect/commit/ef64c6fec0d47da573c04230dde9ea729366d871), [`73f9c6f`](https://github.com/Effect-TS/effect/commit/73f9c6f2ff091512cf904cc54ab59965b86e87c8), [`17cb451`](https://github.com/Effect-TS/effect/commit/17cb4514590e8a86263f7aed009f24da8a237342), [`d801820`](https://github.com/Effect-TS/effect/commit/d80182060c2ee945d7e0e4728812abf9465a0d6a), [`e1eeb2d`](https://github.com/Effect-TS/effect/commit/e1eeb2d7064b3870041dab142f3057970699bbf1), [`c11f3a6`](https://github.com/Effect-TS/effect/commit/c11f3a60a05c3b5fc8e7ce90136728154dc505b0), [`618f7e0`](https://github.com/Effect-TS/effect/commit/618f7e092a1011e5090dca1e69b5e9285689654b), [`c0ba834`](https://github.com/Effect-TS/effect/commit/c0ba834d1995cf5a8b250e4780fd43f3e3881151), [`e1eeb2d`](https://github.com/Effect-TS/effect/commit/e1eeb2d7064b3870041dab142f3057970699bbf1)]: + - effect@3.12.0 + - @effect/platform@0.72.0 + +## 0.21.7 + +### Patch Changes + +- Updated dependencies [[`39457d4`](https://github.com/Effect-TS/effect/commit/39457d4897d9bc7df8af5c05d352866bbeae82eb), [`a475cc2`](https://github.com/Effect-TS/effect/commit/a475cc25fd7c9f26b27a8e98f8fbe43cc9e6ee3e), [`199214e`](https://github.com/Effect-TS/effect/commit/199214e21c616d8a0ccd7ed5f92e944e6c580193), [`b3c160d`](https://github.com/Effect-TS/effect/commit/b3c160d7a1fdfc2d3fb2440530f1ab80efc65133)]: + - effect@3.11.10 + - @effect/platform@0.71.7 + +## 0.21.6 + +### Patch Changes + +- Updated dependencies [[`1c08a0b`](https://github.com/Effect-TS/effect/commit/1c08a0b8505badcffb4d9cade5a746ea90c9557e), [`1ce703b`](https://github.com/Effect-TS/effect/commit/1ce703b041bbd7560c5c437c9b9be48f027937fd), [`1ce703b`](https://github.com/Effect-TS/effect/commit/1ce703b041bbd7560c5c437c9b9be48f027937fd)]: + - effect@3.11.9 + - @effect/platform@0.71.6 + +## 0.21.5 + +### Patch Changes + +- Updated dependencies [[`05d71f8`](https://github.com/Effect-TS/effect/commit/05d71f85622305705d8316817694a09762e60865), [`e66b920`](https://github.com/Effect-TS/effect/commit/e66b9205f25ab425d30640886eb3fb2c4715bc26)]: + - @effect/platform@0.71.5 + +## 0.21.4 + +### Patch Changes + +- Updated dependencies [[`909181a`](https://github.com/Effect-TS/effect/commit/909181a9ce9052a80432ccf52187e0723004bf7f), [`909181a`](https://github.com/Effect-TS/effect/commit/909181a9ce9052a80432ccf52187e0723004bf7f), [`1a6b52d`](https://github.com/Effect-TS/effect/commit/1a6b52dcf020d36e38a7bc90b648152cf5a8ccba)]: + - @effect/platform@0.71.4 + - effect@3.11.8 + +## 0.21.3 + +### Patch Changes + +- Updated dependencies [[`6984508`](https://github.com/Effect-TS/effect/commit/6984508c87f1bd91213b44c19b25ab5e2dcc1ce0), [`883639c`](https://github.com/Effect-TS/effect/commit/883639cc8ce47757f1cd39439391a8028c0812fe)]: + - @effect/platform@0.71.3 + +## 0.21.2 + +### Patch Changes + +- Updated dependencies [[`2408616`](https://github.com/Effect-TS/effect/commit/24086163b60b09cc6d0885bd565ef080dcbe866b), [`cec0b4d`](https://github.com/Effect-TS/effect/commit/cec0b4d152ef660be2ccdb0927255f2471436e6e), [`cec0b4d`](https://github.com/Effect-TS/effect/commit/cec0b4d152ef660be2ccdb0927255f2471436e6e), [`8d978c5`](https://github.com/Effect-TS/effect/commit/8d978c53f6fcc98d9d645ecba3e4b55d4297dd36), [`cec0b4d`](https://github.com/Effect-TS/effect/commit/cec0b4d152ef660be2ccdb0927255f2471436e6e), [`cec0b4d`](https://github.com/Effect-TS/effect/commit/cec0b4d152ef660be2ccdb0927255f2471436e6e)]: + - effect@3.11.7 + - @effect/platform@0.71.2 + +## 0.21.1 + +### Patch Changes + +- Updated dependencies [[`1d3df5b`](https://github.com/Effect-TS/effect/commit/1d3df5bc4324e88a392c348db35fd9d029c7b25e)]: + - @effect/platform@0.71.1 + +## 0.21.0 + +### Patch Changes + +- Updated dependencies [[`662d1ce`](https://github.com/Effect-TS/effect/commit/662d1ce6fb7da384a95888d5b2bb5605bdf3208d), [`c99a0f3`](https://github.com/Effect-TS/effect/commit/c99a0f376d049d3793ed33e146d9873b8a5e5b78), [`11fc401`](https://github.com/Effect-TS/effect/commit/11fc401f436f99bf4be95f56d50b0e4bdfe5edea), [`c99a0f3`](https://github.com/Effect-TS/effect/commit/c99a0f376d049d3793ed33e146d9873b8a5e5b78), [`31c62d8`](https://github.com/Effect-TS/effect/commit/31c62d83cbdcf9850a8b5331faa239601c60f78a)]: + - effect@3.11.6 + - @effect/platform@0.71.0 + +## 0.20.7 + +### Patch Changes + +- Updated dependencies [[`9f5a6f7`](https://github.com/Effect-TS/effect/commit/9f5a6f701bf7ba31adccd1f1bcfa8ab5614c9be8), [`22905cf`](https://github.com/Effect-TS/effect/commit/22905cf5addfb1ff3d2a6135c52036be958ae911), [`9f5a6f7`](https://github.com/Effect-TS/effect/commit/9f5a6f701bf7ba31adccd1f1bcfa8ab5614c9be8), [`1e59e4f`](https://github.com/Effect-TS/effect/commit/1e59e4fd778da18296812a2a32f36ca8ae50f60d), [`9f5a6f7`](https://github.com/Effect-TS/effect/commit/9f5a6f701bf7ba31adccd1f1bcfa8ab5614c9be8), [`8d914e5`](https://github.com/Effect-TS/effect/commit/8d914e504e7a22d0ea628e8af265ee450ff9530f), [`03bb00f`](https://github.com/Effect-TS/effect/commit/03bb00faa74f9e168a54a8cc0828a664fbb1ab05), [`9f5a6f7`](https://github.com/Effect-TS/effect/commit/9f5a6f701bf7ba31adccd1f1bcfa8ab5614c9be8), [`14e1149`](https://github.com/Effect-TS/effect/commit/14e1149f1af5a022f06eb8c2e4ba9fec17fe7426), [`9f5a6f7`](https://github.com/Effect-TS/effect/commit/9f5a6f701bf7ba31adccd1f1bcfa8ab5614c9be8), [`9f5a6f7`](https://github.com/Effect-TS/effect/commit/9f5a6f701bf7ba31adccd1f1bcfa8ab5614c9be8)]: + - effect@3.11.5 + - @effect/platform@0.70.7 + +## 0.20.6 + +### Patch Changes + +- Updated dependencies [[`9a5b8e3`](https://github.com/Effect-TS/effect/commit/9a5b8e36d184bd4967a88752cb6e755e1be263af)]: + - @effect/platform@0.70.6 + +## 0.20.5 + +### Patch Changes + +- [#4087](https://github.com/Effect-TS/effect/pull/4087) [`518b258`](https://github.com/Effect-TS/effect/commit/518b258a8a67ecd332a9252c35cc060f8368dee2) Thanks @tim-smart! - remove Socket write indirection + +- Updated dependencies [[`415f4c9`](https://github.com/Effect-TS/effect/commit/415f4c98321868531727a83cbaad70164f5e4c40), [`518b258`](https://github.com/Effect-TS/effect/commit/518b258a8a67ecd332a9252c35cc060f8368dee2), [`6e323a3`](https://github.com/Effect-TS/effect/commit/6e323a36faaee46b328c8e3cf60a76b3aff9907f), [`518b258`](https://github.com/Effect-TS/effect/commit/518b258a8a67ecd332a9252c35cc060f8368dee2), [`6e323a3`](https://github.com/Effect-TS/effect/commit/6e323a36faaee46b328c8e3cf60a76b3aff9907f)]: + - @effect/platform@0.70.5 + - effect@3.11.4 + +## 0.20.4 + +### Patch Changes + +- Updated dependencies [[`90906f7`](https://github.com/Effect-TS/effect/commit/90906f7f154b12c7182e8f39e3c55ef3937db857), [`3862cd3`](https://github.com/Effect-TS/effect/commit/3862cd3c7f6a542ed65fb81255b3bd696ce2f567), [`3862cd3`](https://github.com/Effect-TS/effect/commit/3862cd3c7f6a542ed65fb81255b3bd696ce2f567), [`343b6aa`](https://github.com/Effect-TS/effect/commit/343b6aa6ac4a74276bfc7c63ccbf4a1d72bc1bed), [`afba339`](https://github.com/Effect-TS/effect/commit/afba339adc11dad56b5a3b7ca94487e58f34d613)]: + - effect@3.11.3 + - @effect/platform@0.70.4 + +## 0.20.3 + +### Patch Changes + +- Updated dependencies [[`7044730`](https://github.com/Effect-TS/effect/commit/70447306be1aeeb7d87c230b2a96ec87b993ede9)]: + - @effect/platform@0.70.3 + +## 0.20.2 + +### Patch Changes + +- Updated dependencies [[`01cee56`](https://github.com/Effect-TS/effect/commit/01cee560b58d94b24cc20e98083251b73e658b41), [`c2249ea`](https://github.com/Effect-TS/effect/commit/c2249ea13fd98ab7d9aa628787931356d8ec2860), [`1358aa5`](https://github.com/Effect-TS/effect/commit/1358aa5326eaa85ef13ee8d1fed0b4a4288ed3eb), [`1de3fe7`](https://github.com/Effect-TS/effect/commit/1de3fe7d1cbafd6391eaa38c2300b99e332cc2aa)]: + - effect@3.11.2 + - @effect/platform@0.70.2 + +## 0.20.1 + +### Patch Changes + +- Updated dependencies [[`dd8a2d8`](https://github.com/Effect-TS/effect/commit/dd8a2d8e80d33b16719fc69361eaedf0b59d4620), [`a71bfef`](https://github.com/Effect-TS/effect/commit/a71bfef46f5061bb2502a61a333638a987b62273)]: + - effect@3.11.1 + - @effect/platform@0.70.1 + +## 0.20.0 + +### Patch Changes + +- Updated dependencies [[`147434b`](https://github.com/Effect-TS/effect/commit/147434b03d5e1fd692dd9f126e5ab0910f3b76d3), [`6e69493`](https://github.com/Effect-TS/effect/commit/6e694930048bbaf98110f35f41566aeb9752d471), [`147434b`](https://github.com/Effect-TS/effect/commit/147434b03d5e1fd692dd9f126e5ab0910f3b76d3), [`5eff3f6`](https://github.com/Effect-TS/effect/commit/5eff3f6fa3aae7e86948a62cbfd63b8d6c3bdf92), [`d9fe79b`](https://github.com/Effect-TS/effect/commit/d9fe79bb5a3fe105d8e7a3bc2922a8ad936a5d10), [`251d189`](https://github.com/Effect-TS/effect/commit/251d189420bbba71990574e91098c499065f9a9b), [`5a259f3`](https://github.com/Effect-TS/effect/commit/5a259f3711b4369f55d885b568bdb21136155261), [`b4ce4ea`](https://github.com/Effect-TS/effect/commit/b4ce4ea7fd514a7e572f2dcd879c98f334981b0e), [`15fcc5a`](https://github.com/Effect-TS/effect/commit/15fcc5a0ea4bbf40ab48fa6a04fdda74f76f4c07), [`9bc9a47`](https://github.com/Effect-TS/effect/commit/9bc9a476800dc645903c888a68bb1d3baa3383c6), [`aadb8a4`](https://github.com/Effect-TS/effect/commit/aadb8a48d2cba197c06ec9996505510e48e4e5cb), [`1e2747c`](https://github.com/Effect-TS/effect/commit/1e2747c63a4820d1459cbbc88c71212983bd68bd), [`9264162`](https://github.com/Effect-TS/effect/commit/9264162a82783a651776fb7b87604564a63e7070), [`e0b9b09`](https://github.com/Effect-TS/effect/commit/e0b9b09e70c386b2da17d1f0a15b0511861c89e8), [`c36f3b9`](https://github.com/Effect-TS/effect/commit/c36f3b95df5ce9d71b66f22f26ce12eda8d3e848), [`672bde5`](https://github.com/Effect-TS/effect/commit/672bde5bec51c7d6f9862828e6a654cb2cb6f93d), [`aadb8a4`](https://github.com/Effect-TS/effect/commit/aadb8a48d2cba197c06ec9996505510e48e4e5cb)]: + - effect@3.11.0 + - @effect/platform@0.70.0 + +## 0.19.33 + +### Patch Changes + +- Updated dependencies [[`3069614`](https://github.com/Effect-TS/effect/commit/30696149271129fc618f6f2ccd1d8f2f6c0f9cd7), [`09a5e52`](https://github.com/Effect-TS/effect/commit/09a5e522fd9b221f05d85b1d1c8a740d4973c302)]: + - effect@3.10.20 + - @effect/platform@0.69.32 + +## 0.19.32 + +### Patch Changes + +- Updated dependencies [[`e6d4a37`](https://github.com/Effect-TS/effect/commit/e6d4a37c1d7e657b5ea44063a1cf586808228fe5)]: + - @effect/platform@0.69.31 + +## 0.19.31 + +### Patch Changes + +- Updated dependencies [[`270f199`](https://github.com/Effect-TS/effect/commit/270f199b31810fd643e4c22818698adcbdb5d396)]: + - @effect/platform@0.69.30 + +## 0.19.30 + +### Patch Changes + +- Updated dependencies [[`24cc35e`](https://github.com/Effect-TS/effect/commit/24cc35e26d6ed4a076470bc687ffd99cc50991b3)]: + - @effect/platform@0.69.29 + +## 0.19.29 + +### Patch Changes + +- Updated dependencies [[`edd72be`](https://github.com/Effect-TS/effect/commit/edd72be57b904d60c9cbffc2537901821a9da537), [`a3e2771`](https://github.com/Effect-TS/effect/commit/a3e277170a1f7cf61fd629acb60304c7e81d9498), [`944025b`](https://github.com/Effect-TS/effect/commit/944025bc5ce139f4a85846aa689bf30ec06a8ec1), [`54addee`](https://github.com/Effect-TS/effect/commit/54addee438a644bf010646c52042c7b89c5fc0a7), [`a9e00e4`](https://github.com/Effect-TS/effect/commit/a9e00e43f0b5dd22c1f9d5b78be6383daea09c20)]: + - @effect/platform@0.69.28 + - effect@3.10.19 + +## 0.19.28 + +### Patch Changes + +- Updated dependencies [[`af409cf`](https://github.com/Effect-TS/effect/commit/af409cf1d2ff973be11cc079ea373eaeedca25de), [`beaccae`](https://github.com/Effect-TS/effect/commit/beaccae2d15931e9fe475fb50a0b3638243fe3f7)]: + - effect@3.10.18 + - @effect/platform@0.69.27 + +## 0.19.27 + +### Patch Changes + +- Updated dependencies [[`c963886`](https://github.com/Effect-TS/effect/commit/c963886d5817986fcbd6bfa4ddf50aca8b6c8184), [`42c4ce6`](https://github.com/Effect-TS/effect/commit/42c4ce6f8d8c7d847e97757650a8ad9419a829d7)]: + - @effect/platform@0.69.26 + - effect@3.10.17 + +## 0.19.26 + +### Patch Changes + +- Updated dependencies [[`320557a`](https://github.com/Effect-TS/effect/commit/320557ab18d13c5e22fc7dc0d2a157eae461012f), [`4dca30c`](https://github.com/Effect-TS/effect/commit/4dca30cfcdafe4542e236489f71d6f171a5b4e38), [`1d99867`](https://github.com/Effect-TS/effect/commit/1d998671be3cd11043f232822e91dd8c98fccfa9), [`6dae414`](https://github.com/Effect-TS/effect/commit/6dae4147991a97ec14a99289bd25fadae7541e8d), [`6b0d737`](https://github.com/Effect-TS/effect/commit/6b0d737078bf63b97891e6bc47affc04b28f9cf7), [`d8356aa`](https://github.com/Effect-TS/effect/commit/d8356aad428a0c2290db52380220f81d9ec94232), [`7b93dd6`](https://github.com/Effect-TS/effect/commit/7b93dd622e2ab79c7072d79d0d9611e446202201)]: + - @effect/platform@0.69.25 + - effect@3.10.16 + +## 0.19.25 + +### Patch Changes + +- Updated dependencies [[`3cc6514`](https://github.com/Effect-TS/effect/commit/3cc6514d2dd64e010cb760cc29bfce98c349bb10)]: + - @effect/platform@0.69.24 + +## 0.19.24 + +### Patch Changes + +- Updated dependencies [[`3aff4d3`](https://github.com/Effect-TS/effect/commit/3aff4d38837c213bb2987973dc4b98febb9f92d2)]: + - @effect/platform@0.69.23 + +## 0.19.23 + +### Patch Changes + +- Updated dependencies [[`8398b32`](https://github.com/Effect-TS/effect/commit/8398b3208242a88239d4449910b7baf923cfe3b6), [`72e55b7`](https://github.com/Effect-TS/effect/commit/72e55b7c610784fcebdbadc592c876e23e76a986)]: + - effect@3.10.15 + - @effect/platform@0.69.22 + +## 0.19.22 + +### Patch Changes + +- Updated dependencies [[`f983946`](https://github.com/Effect-TS/effect/commit/f9839467b4cad6e788297764ef9f9f0b9fd203f9), [`2d8a750`](https://github.com/Effect-TS/effect/commit/2d8a75081eb83a0a81f817fdf6f428369c5064ab)]: + - effect@3.10.14 + - @effect/platform@0.69.21 + +## 0.19.21 + +### Patch Changes + +- Updated dependencies [[`995bbdf`](https://github.com/Effect-TS/effect/commit/995bbdffea2e332f203cd5b474cd6a1c77dfa6ae)]: + - effect@3.10.13 + - @effect/platform@0.69.20 + +## 0.19.20 + +### Patch Changes + +- Updated dependencies [[`eb8c52d`](https://github.com/Effect-TS/effect/commit/eb8c52d8b4c5e067ebf0a81eb742f5822e6439b5)]: + - @effect/platform@0.69.19 + +## 0.19.19 + +### Patch Changes + +- [#3906](https://github.com/Effect-TS/effect/pull/3906) [`a0584ec`](https://github.com/Effect-TS/effect/commit/a0584ece92ed784bfb139e9c5a699f02d1e71c2d) Thanks @tim-smart! - ensure Socket send queue is not ended + +- Updated dependencies [[`a0584ec`](https://github.com/Effect-TS/effect/commit/a0584ece92ed784bfb139e9c5a699f02d1e71c2d), [`dd14efe`](https://github.com/Effect-TS/effect/commit/dd14efe0ace255f571273aae876adea96267d7e6), [`dd14efe`](https://github.com/Effect-TS/effect/commit/dd14efe0ace255f571273aae876adea96267d7e6)]: + - @effect/platform@0.69.18 + - effect@3.10.12 + +## 0.19.18 + +### Patch Changes + +- Updated dependencies [[`5eef499`](https://github.com/Effect-TS/effect/commit/5eef4998b6ccb7a5404d9e4fef85e57fa35fbb8a), [`8240b1c`](https://github.com/Effect-TS/effect/commit/8240b1c10d45312fc863cb679b1a1e8441af0c1a), [`5eef499`](https://github.com/Effect-TS/effect/commit/5eef4998b6ccb7a5404d9e4fef85e57fa35fbb8a)]: + - effect@3.10.11 + - @effect/platform@0.69.17 + +## 0.19.17 + +### Patch Changes + +- [#3894](https://github.com/Effect-TS/effect/pull/3894) [`3ff8e5b`](https://github.com/Effect-TS/effect/commit/3ff8e5b4138c89b56111c075b290e4084d7d169c) Thanks @tim-smart! - remove debug logging from NodeSocket + +## 0.19.16 + +### Patch Changes + +- [#3893](https://github.com/Effect-TS/effect/pull/3893) [`cd720ae`](https://github.com/Effect-TS/effect/commit/cd720aedf7f2571edec0843d6a633e84e4832b28) Thanks @tim-smart! - refactor Socket internal code + +- Updated dependencies [[`cd720ae`](https://github.com/Effect-TS/effect/commit/cd720aedf7f2571edec0843d6a633e84e4832b28), [`cd720ae`](https://github.com/Effect-TS/effect/commit/cd720aedf7f2571edec0843d6a633e84e4832b28), [`cd720ae`](https://github.com/Effect-TS/effect/commit/cd720aedf7f2571edec0843d6a633e84e4832b28), [`b631f40`](https://github.com/Effect-TS/effect/commit/b631f40abbe649b2a089764585b5c39f6a695ac6), [`7d89650`](https://github.com/Effect-TS/effect/commit/7d8965036cd2ea435c8441ffec3345488baebf85)]: + - effect@3.10.10 + - @effect/platform@0.69.16 + +## 0.19.15 + +### Patch Changes + +- Updated dependencies [[`8a30e1d`](https://github.com/Effect-TS/effect/commit/8a30e1dfa3a7103bf5414fc6a7fca3088d8c8c00)]: + - @effect/platform@0.69.15 + +## 0.19.14 + +### Patch Changes + +- [#3882](https://github.com/Effect-TS/effect/pull/3882) [`257ab1b`](https://github.com/Effect-TS/effect/commit/257ab1b539fa6e930b7ae2583a188376372200d7) Thanks @tim-smart! - simplify Socket internal code + +- Updated dependencies [[`a123e80`](https://github.com/Effect-TS/effect/commit/a123e80f111a625428a5b5622b7f55ee1073566b), [`bd5fcd3`](https://github.com/Effect-TS/effect/commit/bd5fcd3e6b603b1e505af90d6a00627c8eca6d41), [`0289d3b`](https://github.com/Effect-TS/effect/commit/0289d3b6391031d00329365bab9791b355031fe3), [`7386b71`](https://github.com/Effect-TS/effect/commit/7386b710e5be570e17f468928a6ed19d549a3e12), [`07c493a`](https://github.com/Effect-TS/effect/commit/07c493a598e096c7810cd06def8cfa43493c46b1), [`257ab1b`](https://github.com/Effect-TS/effect/commit/257ab1b539fa6e930b7ae2583a188376372200d7), [`4211a23`](https://github.com/Effect-TS/effect/commit/4211a2355bb3af3f0e756e2aae9d293379f25662)]: + - effect@3.10.9 + - @effect/platform@0.69.14 + +## 0.19.13 + +### Patch Changes + +- Updated dependencies [[`68b5c9e`](https://github.com/Effect-TS/effect/commit/68b5c9e44f34192cef26e1cadda5e661a027df41), [`9c9928d`](https://github.com/Effect-TS/effect/commit/9c9928dfeacd9ac33dc37eb0ca3d7d8c39175ada), [`6306e66`](https://github.com/Effect-TS/effect/commit/6306e6656092b350d4ede5746da6f245ec9f7e07), [`361c7f3`](https://github.com/Effect-TS/effect/commit/361c7f39a2c10ede9324847c3d3ba192a6f9b20a)]: + - effect@3.10.8 + - @effect/platform@0.69.13 + +## 0.19.12 + +### Patch Changes + +- Updated dependencies [[`33f5b9f`](https://github.com/Effect-TS/effect/commit/33f5b9ffaebea4f1bd0e391b44c41fb6230e743a), [`50f0281`](https://github.com/Effect-TS/effect/commit/50f0281b0d2116726b8927a6217622d5f394f3e4)]: + - effect@3.10.7 + - @effect/platform@0.69.12 + +## 0.19.11 + +### Patch Changes + +- Updated dependencies [[`ce1c21f`](https://github.com/Effect-TS/effect/commit/ce1c21ffc11902ac9ab453a51904207859d38552), [`81ddd45`](https://github.com/Effect-TS/effect/commit/81ddd45fc074b98206fafab416d9a5a28b31e07a)]: + - effect@3.10.6 + - @effect/platform@0.69.11 + +## 0.19.10 + +### Patch Changes + +- Updated dependencies [[`3a6d757`](https://github.com/Effect-TS/effect/commit/3a6d757badeebe00d8ef4d67530d073c8264dcfa), [`59d813a`](https://github.com/Effect-TS/effect/commit/59d813aa4973d1115cfc70cc3667508335f49693)]: + - effect@3.10.5 + - @effect/platform@0.69.10 + +## 0.19.9 + +### Patch Changes + +- Updated dependencies [[`2367708`](https://github.com/Effect-TS/effect/commit/2367708be449f9526a2047e321302d7bfb16f18e)]: + - @effect/platform@0.69.9 + - effect@3.10.4 + +## 0.19.8 + +### Patch Changes + +- Updated dependencies [[`522f7c5`](https://github.com/Effect-TS/effect/commit/522f7c518a5acfb55ef96d6796869f002cc3eaf8)]: + - @effect/platform@0.69.8 + +## 0.19.7 + +### Patch Changes + +- Updated dependencies [[`690d6c5`](https://github.com/Effect-TS/effect/commit/690d6c54d2145adb0af545c447db7d4755bf3c6b), [`b9423d8`](https://github.com/Effect-TS/effect/commit/b9423d8bf8181a2389fdbce1e3c14ac6fe8d54f5), [`279fe3a`](https://github.com/Effect-TS/effect/commit/279fe3a7168fe84e520c2cc88ba189a15f03a2bc)]: + - @effect/platform@0.69.7 + - effect@3.10.3 + +## 0.19.6 + +### Patch Changes + +- Updated dependencies [[`714e119`](https://github.com/Effect-TS/effect/commit/714e11945e45e5a2554ee058e6c43f82a8e309cf), [`c1afd55`](https://github.com/Effect-TS/effect/commit/c1afd55c54e61f9c432823d21b3d016f79160a37), [`42cd72a`](https://github.com/Effect-TS/effect/commit/42cd72a44ca9593e4d81fbb50e8111625fd0fb81)]: + - effect@3.10.2 + - @effect/platform@0.69.6 + +## 0.19.5 + +### Patch Changes + +- Updated dependencies [[`9604d6b`](https://github.com/Effect-TS/effect/commit/9604d6b616435103dafea8b53637a9d1450b4750)]: + - effect@3.10.1 + - @effect/platform@0.69.5 + +## 0.19.4 + +### Patch Changes + +- Updated dependencies [[`c86b1d7`](https://github.com/Effect-TS/effect/commit/c86b1d7cd47b66df190ef9775a475467c1abdbd6)]: + - @effect/platform@0.69.4 + +## 0.19.3 + +### Patch Changes + +- Updated dependencies [[`d5fba63`](https://github.com/Effect-TS/effect/commit/d5fba6391e1005e374aa0238f13edfbd65848313), [`1eb2c30`](https://github.com/Effect-TS/effect/commit/1eb2c30ba064398db5790e376dedcfad55b7b005), [`02d413e`](https://github.com/Effect-TS/effect/commit/02d413e7b6bc1c64885969c37cc3e4e690c94d7d)]: + - @effect/platform@0.69.3 + +## 0.19.2 + +### Patch Changes + +- Updated dependencies [[`e7afc47`](https://github.com/Effect-TS/effect/commit/e7afc47ce83e381c3f4aed2b2974e3b3d86a2340)]: + - @effect/platform@0.69.2 + +## 0.19.1 + +### Patch Changes + +- Updated dependencies [[`7564f56`](https://github.com/Effect-TS/effect/commit/7564f56bb2844cf39d2b0d2d9e93cf9b2205e9a8), [`7564f56`](https://github.com/Effect-TS/effect/commit/7564f56bb2844cf39d2b0d2d9e93cf9b2205e9a8)]: + - @effect/platform@0.69.1 + +## 0.19.0 + +### Patch Changes + +- Updated dependencies [[`4a01828`](https://github.com/Effect-TS/effect/commit/4a01828b66d6213e9bbe18979c893b13f7bb29bf), [`6d9de6b`](https://github.com/Effect-TS/effect/commit/6d9de6b871c5c08e6509a4e830c3d74758faa198), [`4a01828`](https://github.com/Effect-TS/effect/commit/4a01828b66d6213e9bbe18979c893b13f7bb29bf), [`c79c4c1`](https://github.com/Effect-TS/effect/commit/c79c4c178390fe61ff6dda88c9e058862349343a), [`38d30f0`](https://github.com/Effect-TS/effect/commit/38d30f08b8da62f9c3e308b9250738cb8d17bdb5), [`5821ce3`](https://github.com/Effect-TS/effect/commit/5821ce3455b47d25e0a40cae6ce22af9db5fa556)]: + - effect@3.10.0 + - @effect/platform@0.69.0 + +## 0.18.6 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform@0.68.6 + +## 0.18.5 + +### Patch Changes + +- Updated dependencies [[`2036402`](https://github.com/Effect-TS/effect/commit/20364020b8b75a684791aa93d90626758023e9e9)]: + - @effect/platform@0.68.5 + +## 0.18.4 + +### Patch Changes + +- Updated dependencies [[`1b1ef29`](https://github.com/Effect-TS/effect/commit/1b1ef29ae302322f69dc938f9337aa97b4c63266)]: + - @effect/platform@0.68.4 + +## 0.18.3 + +### Patch Changes + +- Updated dependencies [[`61a99b2`](https://github.com/Effect-TS/effect/commit/61a99b2bf9d757870ef0c2ec9d4c877cdd364a3d), [`8c33087`](https://github.com/Effect-TS/effect/commit/8c330879425e80bed2f65e407cd59e991f0d7bec)]: + - effect@3.9.2 + - @effect/platform@0.68.3 + +## 0.18.2 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform@0.68.2 + +## 0.18.1 + +### Patch Changes + +- Updated dependencies [[`b75ac5d`](https://github.com/Effect-TS/effect/commit/b75ac5d0909115507bedc90f18f2d34deb217769)]: + - @effect/platform@0.68.1 + +## 0.18.0 + +### Patch Changes + +- Updated dependencies [[`90ceeab`](https://github.com/Effect-TS/effect/commit/90ceeab3a04051b740af18c8af8bd73ee8ec6363), [`90ceeab`](https://github.com/Effect-TS/effect/commit/90ceeab3a04051b740af18c8af8bd73ee8ec6363), [`90ceeab`](https://github.com/Effect-TS/effect/commit/90ceeab3a04051b740af18c8af8bd73ee8ec6363)]: + - @effect/platform@0.68.0 + +## 0.17.1 + +### Patch Changes + +- Updated dependencies [[`3b2ad1d`](https://github.com/Effect-TS/effect/commit/3b2ad1d58a2e33dc1a72b7037396bd25ca1702a9)]: + - @effect/platform@0.67.1 + - effect@3.9.1 + +## 0.17.0 + +### Patch Changes + +- Updated dependencies [[`ff3d1aa`](https://github.com/Effect-TS/effect/commit/ff3d1aab290b4d1173b2dfc7e4c76abb4babdc16), [`0ba66f2`](https://github.com/Effect-TS/effect/commit/0ba66f2451641fd6990e02ec1ed01c014db9dab0), [`bf77f51`](https://github.com/Effect-TS/effect/commit/bf77f51b323c383224ebf08adf77a7a6e8c9b3cd), [`016f9ad`](https://github.com/Effect-TS/effect/commit/016f9ad931a4b3d09a34e5caf13d87c5b8e9c984), [`0779681`](https://github.com/Effect-TS/effect/commit/07796813f07de035719728733096ba64ce333469), [`534129f`](https://github.com/Effect-TS/effect/commit/534129f8113ce1a8ec50828083e16da9c86326c6), [`d75140c`](https://github.com/Effect-TS/effect/commit/d75140c7a664ceda43142d999f4ff8dcd36d6dda), [`be0451c`](https://github.com/Effect-TS/effect/commit/be0451c149b6618af79cb839cdf04af2db1efb03), [`9237ac6`](https://github.com/Effect-TS/effect/commit/9237ac69bc07de5b3b60076a0ad2921c21de7457), [`be0451c`](https://github.com/Effect-TS/effect/commit/be0451c149b6618af79cb839cdf04af2db1efb03), [`5b36494`](https://github.com/Effect-TS/effect/commit/5b364942e9a9003fdb8217324f8a2d8369c969da), [`c716adb`](https://github.com/Effect-TS/effect/commit/c716adb250ebbea1d1048d818ef7fed4f621d186), [`4986391`](https://github.com/Effect-TS/effect/commit/49863919cd8628c962a712fb1df30d2983820933), [`d75140c`](https://github.com/Effect-TS/effect/commit/d75140c7a664ceda43142d999f4ff8dcd36d6dda), [`d1387ae`](https://github.com/Effect-TS/effect/commit/d1387aebd1ff01bbebde26be46d488956e4daef6)]: + - effect@3.9.0 + - @effect/platform@0.67.0 + +## 0.16.4 + +### Patch Changes + +- Updated dependencies [[`88e85db`](https://github.com/Effect-TS/effect/commit/88e85db34bd402526e27a323e950d053fa34d232), [`f40da15`](https://github.com/Effect-TS/effect/commit/f40da15fbeb7c491840b8f409d47de79720891c3), [`8e94585`](https://github.com/Effect-TS/effect/commit/8e94585abe62753bf3af28bfae77926a7c570ac3), [`83887ca`](https://github.com/Effect-TS/effect/commit/83887ca1b1793916913d8550a4db4450cd14a044), [`5266b6c`](https://github.com/Effect-TS/effect/commit/5266b6cd86d76c3886da041c8829bca04b1a3110), [`cdead5c`](https://github.com/Effect-TS/effect/commit/cdead5c9cfd54dc6c4f215d9732f654c4a12e991), [`f40da15`](https://github.com/Effect-TS/effect/commit/f40da15fbeb7c491840b8f409d47de79720891c3), [`766a8af`](https://github.com/Effect-TS/effect/commit/766a8af307b414aca3648d91c4eab7493a5ec862)]: + - effect@3.8.5 + - @effect/platform@0.66.3 + +## 0.16.3 + +### Patch Changes + +- [#3679](https://github.com/Effect-TS/effect/pull/3679) [`660cd0f`](https://github.com/Effect-TS/effect/commit/660cd0f93610e5e5588f25b852ae7cf4f1dd05bc) Thanks @tim-smart! - add support for watch mode in .runMain + +## 0.16.2 + +### Patch Changes + +- Updated dependencies [[`fd83d0e`](https://github.com/Effect-TS/effect/commit/fd83d0e548feff9ea2d53d370a0b626c4a1d940e), [`4509656`](https://github.com/Effect-TS/effect/commit/45096569d50262275ee984f44c456f5c83b62683), [`090e41c`](https://github.com/Effect-TS/effect/commit/090e41c636d720b1c7d89684a739855765ed4382)]: + - @effect/platform@0.66.2 + - effect@3.8.4 + +## 0.16.1 + +### Patch Changes + +- [#3666](https://github.com/Effect-TS/effect/pull/3666) [`0e0af6d`](https://github.com/Effect-TS/effect/commit/0e0af6d6593d041bd2cdbced9fdaf8265ce166f2) Thanks @tim-smart! - revert #3656 + +- Updated dependencies [[`3812788`](https://github.com/Effect-TS/effect/commit/3812788d79caaab8f559a62fd443018a04ac5647)]: + - @effect/platform@0.66.1 + +## 0.16.0 + +### Patch Changes + +- [#3656](https://github.com/Effect-TS/effect/pull/3656) [`6d4d861`](https://github.com/Effect-TS/effect/commit/6d4d861db6860846da4ac0a25eaf41ced90eb97a) Thanks @tim-smart! - only call process.exit in runMain if signal has been intercepted + +- Updated dependencies []: + - @effect/platform@0.66.0 + +## 0.15.5 + +### Patch Changes + +- Updated dependencies [[`321b201`](https://github.com/Effect-TS/effect/commit/321b201adcb6bbbeb806b3467dd0b4cf063ccda8), [`bb5ec6b`](https://github.com/Effect-TS/effect/commit/bb5ec6b4b6a6f537394596c5a596faf52cb2aef4)]: + - @effect/platform@0.65.5 + - effect@3.8.3 + +## 0.15.4 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform@0.65.4 + +## 0.15.3 + +### Patch Changes + +- Updated dependencies [[`f0d8ef1`](https://github.com/Effect-TS/effect/commit/f0d8ef1ce97ec2a87b09b3e24150cfeab85d6e2f)]: + - effect@3.8.2 + - @effect/platform@0.65.3 + +## 0.15.2 + +### Patch Changes + +- [#3616](https://github.com/Effect-TS/effect/pull/3616) [`cd75658`](https://github.com/Effect-TS/effect/commit/cd756584c352064cb1654be7118a925d57475d49) Thanks @tim-smart! - use Mailbox for NodeStream module + +- Updated dependencies [[`10bf621`](https://github.com/Effect-TS/effect/commit/10bf6213f36d8ddb00f058a4609b85220f3d8334), [`ae36fa6`](https://github.com/Effect-TS/effect/commit/ae36fa68f754eeab9a54b6dc0f8b44db513aa2b6)]: + - effect@3.8.1 + - @effect/platform@0.65.2 + +## 0.15.1 + +### Patch Changes + +- Updated dependencies [[`e44c5f2`](https://github.com/Effect-TS/effect/commit/e44c5f228215738fe4e75023c7461bf9521249cb)]: + - @effect/platform@0.65.1 + +## 0.15.0 + +### Patch Changes + +- Updated dependencies [[`fcfa6ee`](https://github.com/Effect-TS/effect/commit/fcfa6ee30ffd07d998bf22799357bf58580a116f), [`bb9931b`](https://github.com/Effect-TS/effect/commit/bb9931b62e249a3b801f2cb9d097aec0c8511af7), [`5798f76`](https://github.com/Effect-TS/effect/commit/5798f7619529de33e5ba06f551806f68fedc19db), [`5f0bfa1`](https://github.com/Effect-TS/effect/commit/5f0bfa17205398d4e4818bfbcf9e1b505b3b1fc5), [`812a4e8`](https://github.com/Effect-TS/effect/commit/812a4e86e2d1aa23b477ef5829aa0e5c07784936), [`6a128f6`](https://github.com/Effect-TS/effect/commit/6a128f63f9b41fec2db70790b3bbb96cb9afa1ab), [`273565e`](https://github.com/Effect-TS/effect/commit/273565e7901639e8d0541930ab715aea9c80fbaa), [`adf7d7a`](https://github.com/Effect-TS/effect/commit/adf7d7a7dfce3a7021e9f3b0d847dc85be89d754), [`569a801`](https://github.com/Effect-TS/effect/commit/569a8017ef0a0bc203e4312867cbdd37b0effbd7), [`aa1fa53`](https://github.com/Effect-TS/effect/commit/aa1fa5301e886b9657c8eb0d38cb87cef92a8305), [`02f6b06`](https://github.com/Effect-TS/effect/commit/02f6b0660e12bee1069532a9cc18d3ab855257be), [`12b893e`](https://github.com/Effect-TS/effect/commit/12b893e63cc6dfada4aca7773b4783940e2edf25), [`bbad27e`](https://github.com/Effect-TS/effect/commit/bbad27ec0a90860593f759405caa877e7f4a655f), [`7041393`](https://github.com/Effect-TS/effect/commit/7041393cff132e96566d3f36da0483a6ff6195e4), [`e0d21a5`](https://github.com/Effect-TS/effect/commit/e0d21a54c8323728fbb75a32f4820a9996257809), [`adf7d7a`](https://github.com/Effect-TS/effect/commit/adf7d7a7dfce3a7021e9f3b0d847dc85be89d754), [`007289a`](https://github.com/Effect-TS/effect/commit/007289a52d5877f8e90e2dacf38171ff9bf603fd), [`42a8f99`](https://github.com/Effect-TS/effect/commit/42a8f99740eefdaf2c4544d2c345313f97547a36), [`eebfd29`](https://github.com/Effect-TS/effect/commit/eebfd29633fd5d38b505c5c0842036f61f05e913), [`040703d`](https://github.com/Effect-TS/effect/commit/040703d0e100cd5511e52d812c15492414262b5e)]: + - effect@3.8.0 + - @effect/platform@0.65.0 + +## 0.14.1 + +### Patch Changes + +- Updated dependencies [[`35a0f81`](https://github.com/Effect-TS/effect/commit/35a0f813141652d696461cd5d19fd146adaf85be), [`8261c5a`](https://github.com/Effect-TS/effect/commit/8261c5ae6fe86872292ec1fc1a58ab9cea2f5f51)]: + - effect@3.7.3 + - @effect/platform@0.64.1 + +## 0.14.0 + +### Minor Changes + +- [#3565](https://github.com/Effect-TS/effect/pull/3565) [`90ac8f6`](https://github.com/Effect-TS/effect/commit/90ac8f6f6053a2e4498f8b0cc56fe12777d02e1a) Thanks @tim-smart! - move Etag implementation to /platform + +### Patch Changes + +- Updated dependencies [[`90ac8f6`](https://github.com/Effect-TS/effect/commit/90ac8f6f6053a2e4498f8b0cc56fe12777d02e1a), [`90ac8f6`](https://github.com/Effect-TS/effect/commit/90ac8f6f6053a2e4498f8b0cc56fe12777d02e1a), [`3791e24`](https://github.com/Effect-TS/effect/commit/3791e241636b1dfe924a56f380ebc9a7ff0827a9), [`3791e24`](https://github.com/Effect-TS/effect/commit/3791e241636b1dfe924a56f380ebc9a7ff0827a9)]: + - @effect/platform@0.64.0 + +## 0.13.3 + +### Patch Changes + +- [#3555](https://github.com/Effect-TS/effect/pull/3555) [`64c2292`](https://github.com/Effect-TS/effect/commit/64c22927aa01e0159b0fa98ff55e742069af399c) Thanks @tim-smart! - leave node stream listeners attached to prevent unhandled errors + +- Updated dependencies [[`4a701c4`](https://github.com/Effect-TS/effect/commit/4a701c406da032563fedae459536c00ae5cfe3c7)]: + - @effect/platform@0.63.3 + +## 0.13.2 + +### Patch Changes + +- Updated dependencies [[`8a601d7`](https://github.com/Effect-TS/effect/commit/8a601d7a1f8ffe52ac9e6d67e9282a1495fe59c9), [`353ba19`](https://github.com/Effect-TS/effect/commit/353ba19f9b2b9e959f0a00d058c6d40a4bc02db7)]: + - effect@3.7.2 + - @effect/platform@0.63.2 + +## 0.13.1 + +### Patch Changes + +- Updated dependencies [[`79859e7`](https://github.com/Effect-TS/effect/commit/79859e71040d8edf1868b8530b90c650f4321eff), [`f6a469c`](https://github.com/Effect-TS/effect/commit/f6a469c190b9f00eee5ea0cd4d5912a0ef8b46f5), [`dcb9ec0`](https://github.com/Effect-TS/effect/commit/dcb9ec0db443894dd204d87450f779c44b9ad7f1), [`79aa6b1`](https://github.com/Effect-TS/effect/commit/79aa6b136e1f29b36f34e88cb2ff162bff2bb4ed)]: + - effect@3.7.1 + - @effect/platform@0.63.1 + +## 0.13.0 + +### Patch Changes + +- Updated dependencies [[`db89601`](https://github.com/Effect-TS/effect/commit/db89601ee9c1050c4e762b7bd7ec65a6a2799dfe), [`2f456cc`](https://github.com/Effect-TS/effect/commit/2f456cce5012b9fcb6b4e039190d527813b75b92), [`8745e41`](https://github.com/Effect-TS/effect/commit/8745e41ed96e3765dc6048efc2a9afbe05c8a1e9), [`e557838`](https://github.com/Effect-TS/effect/commit/e55783886b046d3c5f33447f455f9ccf2fa75922), [`d6e7e40`](https://github.com/Effect-TS/effect/commit/d6e7e40b1e2ad0c59aa02f07344d28601b14ebdc), [`8356321`](https://github.com/Effect-TS/effect/commit/8356321598da04bd77c1001f45a4e447bec5591d), [`192f2eb`](https://github.com/Effect-TS/effect/commit/192f2ebb2c4ddbf4bfd8baedd32140b2376868f4), [`718cb70`](https://github.com/Effect-TS/effect/commit/718cb70038629a6d58d02e407760e341f7c94474), [`e9d0310`](https://github.com/Effect-TS/effect/commit/e9d03107acbf204d9304f3e8aea0816b7d3c7dfb), [`6bf28f7`](https://github.com/Effect-TS/effect/commit/6bf28f7e3b1e5e0608ff567205fea0581d11666f)]: + - effect@3.7.0 + - @effect/platform@0.63.0 + +## 0.12.5 + +### Patch Changes + +- Updated dependencies [[`e809286`](https://github.com/Effect-TS/effect/commit/e8092865900608c4df7a6b7991b1c13cc1e4ca2d)]: + - effect@3.6.8 + - @effect/platform@0.62.5 + +## 0.12.4 + +### Patch Changes + +- [#3506](https://github.com/Effect-TS/effect/pull/3506) [`e7a65e3`](https://github.com/Effect-TS/effect/commit/e7a65e3c6a08636bbfce3d3af3098bf28474364d) Thanks @tim-smart! - use Logger.pretty for runMain, and support dual usage + +- Updated dependencies [[`e7a65e3`](https://github.com/Effect-TS/effect/commit/e7a65e3c6a08636bbfce3d3af3098bf28474364d), [`50ec889`](https://github.com/Effect-TS/effect/commit/50ec8897a49b7d1fe84f63107f89d543c52f3dfc)]: + - @effect/platform@0.62.4 + - effect@3.6.7 + +## 0.12.3 + +### Patch Changes + +- Updated dependencies [[`f960bf4`](https://github.com/Effect-TS/effect/commit/f960bf45239e9badac6e0ad3a602f4174cd7bbdf), [`46a575f`](https://github.com/Effect-TS/effect/commit/46a575f48a05457b782fb21f7827d338c9b59320)]: + - effect@3.6.6 + - @effect/platform@0.62.3 + +## 0.12.2 + +### Patch Changes + +- Updated dependencies [[`413994c`](https://github.com/Effect-TS/effect/commit/413994c9792f16d9d57cca3ae6eb254bf93bd261), [`14a47a8`](https://github.com/Effect-TS/effect/commit/14a47a8c1f3cff2186b8fe7a919a1d773888fb5b), [`0c09841`](https://github.com/Effect-TS/effect/commit/0c0984173be3d58f050b300a1a8aa89d76ba49ae)]: + - @effect/platform@0.62.2 + - effect@3.6.5 + +## 0.12.1 + +### Patch Changes + +- Updated dependencies [[`9efe0e5`](https://github.com/Effect-TS/effect/commit/9efe0e5b57ac557399be620822c21cc6e9add285)]: + - @effect/platform@0.62.1 + +## 0.12.0 + +### Patch Changes + +- [#3450](https://github.com/Effect-TS/effect/pull/3450) [`0e42a8f`](https://github.com/Effect-TS/effect/commit/0e42a8f045ecb1fd3d080edf3d49fef16a9b0ca1) Thanks @tim-smart! - update dependencies + +- Updated dependencies [[`8295281`](https://github.com/Effect-TS/effect/commit/8295281ae9bd7441e680402540bf3c8682ec417b), [`c940df6`](https://github.com/Effect-TS/effect/commit/c940df63800bf3c4396d91cf28ec34938642fd2c), [`00b6c6d`](https://github.com/Effect-TS/effect/commit/00b6c6d4001f5de728b7d990a1b14560b4961a63), [`f8d95a6`](https://github.com/Effect-TS/effect/commit/f8d95a61ad0762147933c5c32bb6d7237e18eef4)]: + - effect@3.6.4 + - @effect/platform@0.62.0 + +## 0.11.8 + +### Patch Changes + +- Updated dependencies [[`04adcac`](https://github.com/Effect-TS/effect/commit/04adcace913e6fc483df266874a68005e9e04ccf)]: + - effect@3.6.3 + - @effect/platform@0.61.8 + +## 0.11.7 + +### Patch Changes + +- Updated dependencies [[`17245a4`](https://github.com/Effect-TS/effect/commit/17245a4e783c19dee51529600b3b40f164fa59bc), [`630d40e`](https://github.com/Effect-TS/effect/commit/630d40eaa7eb4d2f8b6705b16d4f426bc28a7d09), [`630d40e`](https://github.com/Effect-TS/effect/commit/630d40eaa7eb4d2f8b6705b16d4f426bc28a7d09)]: + - @effect/platform@0.61.7 + +## 0.11.6 + +### Patch Changes + +- Updated dependencies [[`d829b57`](https://github.com/Effect-TS/effect/commit/d829b576357f2e3b203ab7e107a1492de903a106), [`fd4b2f6`](https://github.com/Effect-TS/effect/commit/fd4b2f6516b325740dde615f1cf0229edf13ca0c)]: + - @effect/platform@0.61.6 + - effect@3.6.2 + +## 0.11.5 + +### Patch Changes + +- Updated dependencies [[`056b710`](https://github.com/Effect-TS/effect/commit/056b7108978e70612176c23991916f678d947f38)]: + - @effect/platform@0.61.5 + +## 0.11.4 + +### Patch Changes + +- Updated dependencies [[`e7cb109`](https://github.com/Effect-TS/effect/commit/e7cb109d0754207024a64d55b6bd2a674dd8ed7d)]: + - @effect/platform@0.61.4 + +## 0.11.3 + +### Patch Changes + +- Updated dependencies [[`510a34d`](https://github.com/Effect-TS/effect/commit/510a34d4cc5d2f51347a53847f6c7db84d2b17c6), [`fb9f786`](https://github.com/Effect-TS/effect/commit/fb9f7867f0c895e63f9ef23e8d0941248c42179d), [`45dbb9f`](https://github.com/Effect-TS/effect/commit/45dbb9ffeaf93d9e4df99d0cd4920e41ba9a3978)]: + - effect@3.6.1 + - @effect/platform@0.61.3 + +## 0.11.2 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform@0.61.2 + +## 0.11.1 + +### Patch Changes + +- Updated dependencies [[`11223bf`](https://github.com/Effect-TS/effect/commit/11223bf9cbf5b822e0bf9a9fb2b35b2ad88af692)]: + - @effect/platform@0.61.1 + +## 0.11.0 + +### Patch Changes + +- Updated dependencies [[`1e0fe80`](https://github.com/Effect-TS/effect/commit/1e0fe802b36c257971296617473ce0abe730e8dc), [`8135294`](https://github.com/Effect-TS/effect/commit/8135294b591ea94fde7e6f94a504608f0e630520), [`cd255a4`](https://github.com/Effect-TS/effect/commit/cd255a48872d8fb924cf713ef73f0883a9cc6987), [`3845646`](https://github.com/Effect-TS/effect/commit/3845646828e98f3c7cda1217f6cfe5f642ac0603), [`2d09078`](https://github.com/Effect-TS/effect/commit/2d09078c5948b37fc2f79ef858fe4ca3e4814085), [`4bce5a0`](https://github.com/Effect-TS/effect/commit/4bce5a0274203550ccf117d830721891b0a3d182), [`4ddbff0`](https://github.com/Effect-TS/effect/commit/4ddbff0bb4e3ffddfeb509c59835b83245fb975e), [`e74cc38`](https://github.com/Effect-TS/effect/commit/e74cc38cb420a320c4d7ef98180f19d452a8b316), [`bb069b4`](https://github.com/Effect-TS/effect/commit/bb069b49ef291c532a02c1e8e74271f6d1bb32ec), [`cd255a4`](https://github.com/Effect-TS/effect/commit/cd255a48872d8fb924cf713ef73f0883a9cc6987), [`7d02174`](https://github.com/Effect-TS/effect/commit/7d02174af3bcbf054e5cdddb821c91d0f47e8285)]: + - effect@3.6.0 + - @effect/platform@0.61.0 + +## 0.10.3 + +### Patch Changes + +- Updated dependencies [[`6359644`](https://github.com/Effect-TS/effect/commit/635964446323cf55d4060559337e710e4a24496e), [`7f41e42`](https://github.com/Effect-TS/effect/commit/7f41e428830bf3043b8be0d28dcd235d5747c942), [`f566fd1`](https://github.com/Effect-TS/effect/commit/f566fd1d7eea531a0d981dd24037f14a603a1273)]: + - effect@3.5.9 + - @effect/platform@0.60.3 + +## 0.10.2 + +### Patch Changes + +- Updated dependencies [[`eb4d014`](https://github.com/Effect-TS/effect/commit/eb4d014c559e1b4c95b3fb9295fe77593c17ed7a), [`1ba640c`](https://github.com/Effect-TS/effect/commit/1ba640c702f187a866023bf043c26e25cce941ef), [`c8c71bd`](https://github.com/Effect-TS/effect/commit/c8c71bd20eb87d23133dac6156b83bb08941597c), [`a26ce58`](https://github.com/Effect-TS/effect/commit/a26ce581ca7d407e1e81439b58c8045b3fa65231), [`fc20f73`](https://github.com/Effect-TS/effect/commit/fc20f73c69e577981cb64714de2adc97e1004dae)]: + - @effect/platform@0.60.2 + - effect@3.5.8 + +## 0.10.1 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform@0.60.1 + +## 0.10.0 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform@0.60.0 + +## 0.9.3 + +### Patch Changes + +- [#3310](https://github.com/Effect-TS/effect/pull/3310) [`99bddcf`](https://github.com/Effect-TS/effect/commit/99bddcfb3d6eab4d489d055404e26ad81afe52fc) Thanks @fubhy! - Added additional pure annotations to improve tree-shakeability + +- Updated dependencies [[`3afcc93`](https://github.com/Effect-TS/effect/commit/3afcc93413a3d910beb69e4ce9ae120e4adaffd5), [`99bddcf`](https://github.com/Effect-TS/effect/commit/99bddcfb3d6eab4d489d055404e26ad81afe52fc)]: + - effect@3.5.7 + - @effect/platform@0.59.3 + +## 0.9.2 + +### Patch Changes + +- Updated dependencies [[`cc327a1`](https://github.com/Effect-TS/effect/commit/cc327a1bccd22a4ee27ec7e58b53205e93b23e2c), [`4bfe4fb`](https://github.com/Effect-TS/effect/commit/4bfe4fb5c82f597c9beea9baa92e772593598b60), [`2b14d18`](https://github.com/Effect-TS/effect/commit/2b14d181462cad8359da4fa6bc6dfda0f742c398)]: + - effect@3.5.6 + - @effect/platform@0.59.2 + +## 0.9.1 + +### Patch Changes + +- Updated dependencies [[`a9d7800`](https://github.com/Effect-TS/effect/commit/a9d7800f6a253192b653d77778b0674f39b1ca39), [`fcecff7`](https://github.com/Effect-TS/effect/commit/fcecff7f7e12b295a252f124861b801c73072151), [`adbf753`](https://github.com/Effect-TS/effect/commit/adbf75340a9db15dc5cadc66e911a8978a195a65), [`adbf753`](https://github.com/Effect-TS/effect/commit/adbf75340a9db15dc5cadc66e911a8978a195a65)]: + - effect@3.5.5 + - @effect/platform@0.59.1 + +## 0.9.0 + +### Minor Changes + +- [#3260](https://github.com/Effect-TS/effect/pull/3260) [`53c0db0`](https://github.com/Effect-TS/effect/commit/53c0db06872d5b5edea2a706e83249908385325c) Thanks @tim-smart! - replace /platform RefailError with use of the "cause" property + +### Patch Changes + +- [#3253](https://github.com/Effect-TS/effect/pull/3253) [`ed0dde4`](https://github.com/Effect-TS/effect/commit/ed0dde4888e6f1a97ad5bba06b755d26a6a1c52e) Thanks @tim-smart! - update dependencies + +- Updated dependencies [[`53c0db0`](https://github.com/Effect-TS/effect/commit/53c0db06872d5b5edea2a706e83249908385325c), [`ed0dde4`](https://github.com/Effect-TS/effect/commit/ed0dde4888e6f1a97ad5bba06b755d26a6a1c52e), [`ada68b3`](https://github.com/Effect-TS/effect/commit/ada68b3e61c67907c2a281c024c84d818186ca4c), [`ca775ce`](https://github.com/Effect-TS/effect/commit/ca775cec53baebc1a43d9b8852a3ac6726178498), [`5be9cc0`](https://github.com/Effect-TS/effect/commit/5be9cc044025a9541b9b7acefa2d3fc05fa1301b), [`203658f`](https://github.com/Effect-TS/effect/commit/203658f8001c132b25764ab70344b171683b554c), [`eb1c4d4`](https://github.com/Effect-TS/effect/commit/eb1c4d44e54b9d8d201a366d1ff94face2a6dcd3)]: + - @effect/platform@0.59.0 + - effect@3.5.4 + +## 0.8.26 + +### Patch Changes + +- Updated dependencies [[`edb0da3`](https://github.com/Effect-TS/effect/commit/edb0da383746d760f35d8582f5fb0cc0eeca9217), [`a1db40a`](https://github.com/Effect-TS/effect/commit/a1db40a650ab842e778654f0d88e80f2ef4fd6f3), [`c8d3fb0`](https://github.com/Effect-TS/effect/commit/c8d3fb0fe23585f6efb724af51fbab3ba1ad6e83), [`dabd028`](https://github.com/Effect-TS/effect/commit/dabd028decf9b7983ca16ebe0f48c05c11a84b68), [`786b2ab`](https://github.com/Effect-TS/effect/commit/786b2ab29d525c877bb84035dac9e2d6499339d1), [`fc57354`](https://github.com/Effect-TS/effect/commit/fc573547d41667016fce05eaee75960fcc6dce4d)]: + - effect@3.5.3 + - @effect/platform@0.58.27 + +## 0.8.25 + +### Patch Changes + +- Updated dependencies [[`639208e`](https://github.com/Effect-TS/effect/commit/639208eeb8a44622994f832bc2d45d06ab636bc8), [`6684b4c`](https://github.com/Effect-TS/effect/commit/6684b4c27d77a7fcc7af2e261a450edf971b62b5), [`6684b4c`](https://github.com/Effect-TS/effect/commit/6684b4c27d77a7fcc7af2e261a450edf971b62b5), [`6684b4c`](https://github.com/Effect-TS/effect/commit/6684b4c27d77a7fcc7af2e261a450edf971b62b5)]: + - effect@3.5.2 + - @effect/platform@0.58.26 + +## 0.8.24 + +### Patch Changes + +- Updated dependencies [[`0623fca`](https://github.com/Effect-TS/effect/commit/0623fca41679b0e3c5a10dd0f8985f91670bd721)]: + - @effect/platform@0.58.25 + +## 0.8.23 + +### Patch Changes + +- Updated dependencies [[`55fdd76`](https://github.com/Effect-TS/effect/commit/55fdd761ee95afd73b6a892c13fee92b36c02837)]: + - effect@3.5.1 + - @effect/platform@0.58.24 + +## 0.8.22 + +### Patch Changes + +- Updated dependencies [[`a1f5b83`](https://github.com/Effect-TS/effect/commit/a1f5b831a1bc7535988b370d68d0b3eb1123e0ce), [`a1f5b83`](https://github.com/Effect-TS/effect/commit/a1f5b831a1bc7535988b370d68d0b3eb1123e0ce), [`a1f5b83`](https://github.com/Effect-TS/effect/commit/a1f5b831a1bc7535988b370d68d0b3eb1123e0ce), [`60bc3d0`](https://github.com/Effect-TS/effect/commit/60bc3d0867b13e48b24dc22604b4dd2e7b2c1ca4), [`5ab348f`](https://github.com/Effect-TS/effect/commit/5ab348f265db3d283aa091ddca6d2d49137c16f2), [`60bc3d0`](https://github.com/Effect-TS/effect/commit/60bc3d0867b13e48b24dc22604b4dd2e7b2c1ca4), [`3e04bf8`](https://github.com/Effect-TS/effect/commit/3e04bf8a7127e956cadb7684a8f4c661df57663b), [`e7fc45f`](https://github.com/Effect-TS/effect/commit/e7fc45f0c7002aafdaec7878149ac064cd104ea3), [`a1f5b83`](https://github.com/Effect-TS/effect/commit/a1f5b831a1bc7535988b370d68d0b3eb1123e0ce), [`4626de5`](https://github.com/Effect-TS/effect/commit/4626de59c25b384216faa0be87bf0b8cd36357d0), [`f01e7db`](https://github.com/Effect-TS/effect/commit/f01e7db317827255d7901f523f2e28b43298e8df), [`60bc3d0`](https://github.com/Effect-TS/effect/commit/60bc3d0867b13e48b24dc22604b4dd2e7b2c1ca4), [`79d2d91`](https://github.com/Effect-TS/effect/commit/79d2d91464d95dde0e9444d43e7a7f309f05d6e6), [`ac71f37`](https://github.com/Effect-TS/effect/commit/ac71f378f2413e5aa91c95f649ffe898d6a26114), [`8432360`](https://github.com/Effect-TS/effect/commit/8432360ce68614a419bb328083a4109d0fc8aa93), [`e4bf1bf`](https://github.com/Effect-TS/effect/commit/e4bf1bf2b4a970eacd77c9b77b5ea8c68bc84498), [`13cb861`](https://github.com/Effect-TS/effect/commit/13cb861a5eded15c55c6cdcf6a8acde8320367a6), [`79d2d91`](https://github.com/Effect-TS/effect/commit/79d2d91464d95dde0e9444d43e7a7f309f05d6e6), [`e7fc45f`](https://github.com/Effect-TS/effect/commit/e7fc45f0c7002aafdaec7878149ac064cd104ea3), [`9f66825`](https://github.com/Effect-TS/effect/commit/9f66825f1fce0fe8d10420c285f7dc4c71e8af8d)]: + - effect@3.5.0 + - @effect/platform@0.58.23 + +## 0.8.21 + +### Patch Changes + +- Updated dependencies [[`7af137c`](https://github.com/Effect-TS/effect/commit/7af137c9433f6e74959b3887561ec1e6f12e10ee), [`ee4b3dc`](https://github.com/Effect-TS/effect/commit/ee4b3dc5f68d19dc3ae1c2d12901c5b8ffbebabb), [`366f2ee`](https://github.com/Effect-TS/effect/commit/366f2ee3fb6f712a44e8f84fc188612e5ecc016d), [`366f2ee`](https://github.com/Effect-TS/effect/commit/366f2ee3fb6f712a44e8f84fc188612e5ecc016d), [`097d25c`](https://github.com/Effect-TS/effect/commit/097d25cb5d13c049e01789651be56b09620186ef)]: + - effect@3.4.9 + - @effect/platform@0.58.22 + +## 0.8.20 + +### Patch Changes + +- Updated dependencies [[`a435e0f`](https://github.com/Effect-TS/effect/commit/a435e0fc5378b33a49bcec92ee235df6f16a2419), [`b5554db`](https://github.com/Effect-TS/effect/commit/b5554db36c4dd6f64fa5e6a62a29b2759c54217a), [`a9c4fb3`](https://github.com/Effect-TS/effect/commit/a9c4fb3bf3c6e92cd1c142b0605fddf7eb3c697c)]: + - effect@3.4.8 + - @effect/platform@0.58.21 + +## 0.8.19 + +### Patch Changes + +- Updated dependencies [[`a5737d6`](https://github.com/Effect-TS/effect/commit/a5737d6db2b921605c332eabbc5402ee3d17357b)]: + - effect@3.4.7 + - @effect/platform@0.58.20 + +## 0.8.18 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform@0.58.19 + +## 0.8.17 + +### Patch Changes + +- Updated dependencies [[`7f8900a`](https://github.com/Effect-TS/effect/commit/7f8900a1de9addeb0d371103a2c5c2aa3e4ff95e)]: + - @effect/platform@0.58.18 + +## 0.8.16 + +### Patch Changes + +- Updated dependencies [[`5c0ceb0`](https://github.com/Effect-TS/effect/commit/5c0ceb00826cce9e50bf9d41d83e191d5352c030), [`5c0ceb0`](https://github.com/Effect-TS/effect/commit/5c0ceb00826cce9e50bf9d41d83e191d5352c030), [`33735b1`](https://github.com/Effect-TS/effect/commit/33735b16b41bd26929d8f4754c190925db6323b7), [`5c0ceb0`](https://github.com/Effect-TS/effect/commit/5c0ceb00826cce9e50bf9d41d83e191d5352c030), [`139d4b3`](https://github.com/Effect-TS/effect/commit/139d4b39fb3bff2eeaa7c0c809c581da42425a83)]: + - effect@3.4.6 + - @effect/platform@0.58.17 + +## 0.8.15 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform@0.58.16 + +## 0.8.14 + +### Patch Changes + +- Updated dependencies [[`baa90df`](https://github.com/Effect-TS/effect/commit/baa90df9663f5f37d7b6814dad25142d53dbc720)]: + - @effect/platform@0.58.15 + +## 0.8.13 + +### Patch Changes + +- Updated dependencies [[`52a87c7`](https://github.com/Effect-TS/effect/commit/52a87c7a0b9536398deaf8ec507e53a82c607219), [`6d2280e`](https://github.com/Effect-TS/effect/commit/6d2280e9497c95cb0e965ca462c825345074eedf)]: + - @effect/platform@0.58.14 + +## 0.8.12 + +### Patch Changes + +- Updated dependencies [[`a047af9`](https://github.com/Effect-TS/effect/commit/a047af99447dfffc729e9c8ef0ca143537927e91), [`dbd53ea`](https://github.com/Effect-TS/effect/commit/dbd53ea363c71a24449cb068251054c3a1acf864)]: + - effect@3.4.5 + - @effect/platform@0.58.13 + +## 0.8.11 + +### Patch Changes + +- Updated dependencies [[`74e0ad2`](https://github.com/Effect-TS/effect/commit/74e0ad23b4c36f41b7fd10856b20f8b701bc4044), [`74e0ad2`](https://github.com/Effect-TS/effect/commit/74e0ad23b4c36f41b7fd10856b20f8b701bc4044), [`72638e3`](https://github.com/Effect-TS/effect/commit/72638e3d99f0e93a24febf6c225256ce92d4a20b), [`d7dde2b`](https://github.com/Effect-TS/effect/commit/d7dde2b4af08b37af859d4c327c1f5c6f00cf9d9), [`9b2fc3b`](https://github.com/Effect-TS/effect/commit/9b2fc3b9dfd304a2bd0508ef2313cfc54357be0c)]: + - @effect/platform@0.58.12 + - effect@3.4.4 + +## 0.8.10 + +### Patch Changes + +- Updated dependencies [[`a5b95b5`](https://github.com/Effect-TS/effect/commit/a5b95b548284e4798654ae7ce6883fa49108f0ea), [`5e29579`](https://github.com/Effect-TS/effect/commit/5e29579187cb8420ea4930b3999fec984f8999f4)]: + - @effect/platform@0.58.11 + +## 0.8.9 + +### Patch Changes + +- [#3079](https://github.com/Effect-TS/effect/pull/3079) [`bbdd365`](https://github.com/Effect-TS/effect/commit/bbdd36567706c94cdec45bacea825941c347b6cd) Thanks @tim-smart! - update dependencies + +- [#3079](https://github.com/Effect-TS/effect/pull/3079) [`bbdd365`](https://github.com/Effect-TS/effect/commit/bbdd36567706c94cdec45bacea825941c347b6cd) Thanks @tim-smart! - update to typescript 5.5 + +- Updated dependencies [[`a48ee84`](https://github.com/Effect-TS/effect/commit/a48ee845ac21bbde9baf938af9e97a98322211c9), [`c342739`](https://github.com/Effect-TS/effect/commit/c3427396226e1ad7b95b40595a23f9bdff3e3365), [`8898e5e`](https://github.com/Effect-TS/effect/commit/8898e5e238622f6337583d91ee23609c1f5ccdf7), [`a48ee84`](https://github.com/Effect-TS/effect/commit/a48ee845ac21bbde9baf938af9e97a98322211c9), [`a48ee84`](https://github.com/Effect-TS/effect/commit/a48ee845ac21bbde9baf938af9e97a98322211c9), [`ff78636`](https://github.com/Effect-TS/effect/commit/ff786367c522975f40f0f179a0ecdfcfab7ecbdb), [`c86bd4e`](https://github.com/Effect-TS/effect/commit/c86bd4e134c23146c216f9ff97e03781d55991b6), [`ab3180f`](https://github.com/Effect-TS/effect/commit/ab3180f827041d0ea3b2d72254a1a8683e99e056), [`bbdd365`](https://github.com/Effect-TS/effect/commit/bbdd36567706c94cdec45bacea825941c347b6cd)]: + - @effect/platform@0.58.10 + - effect@3.4.3 + +## 0.8.8 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform@0.58.9 + +## 0.8.7 + +### Patch Changes + +- Updated dependencies [[`3da1497`](https://github.com/Effect-TS/effect/commit/3da1497b5c9cc886d300258bc928fd68a4fefe6f)]: + - effect@3.4.2 + - @effect/platform@0.58.8 + +## 0.8.6 + +### Patch Changes + +- Updated dependencies [[`027004a`](https://github.com/Effect-TS/effect/commit/027004a897f654791e75faa28eefb50dd0244b6e)]: + - @effect/platform@0.58.7 + +## 0.8.5 + +### Patch Changes + +- Updated dependencies [[`66a1910`](https://github.com/Effect-TS/effect/commit/66a19109ff90c4252123b8809b8c8a74681dba6a), [`2e8e252`](https://github.com/Effect-TS/effect/commit/2e8e2520cac712f0eb644553bd476429ebd674e4)]: + - effect@3.4.1 + - @effect/platform@0.58.6 + +## 0.8.4 + +### Patch Changes + +- Updated dependencies [[`37a07a2`](https://github.com/Effect-TS/effect/commit/37a07a2d8d1ce09ab965c0ada84a3fae9a6aba05)]: + - @effect/platform@0.58.5 + +## 0.8.3 + +### Patch Changes + +- Updated dependencies [[`b77fb0a`](https://github.com/Effect-TS/effect/commit/b77fb0a811ec1ad0e794917077c9a90824515db8)]: + - @effect/platform@0.58.4 + +## 0.8.2 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform@0.58.3 + +## 0.8.1 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform@0.58.2 + +## 0.8.0 + +### Minor Changes + +- [#3036](https://github.com/Effect-TS/effect/pull/3036) [`5a248aa`](https://github.com/Effect-TS/effect/commit/5a248aa5ab2db3f7131ebc79bb9871a76de57973) Thanks @tim-smart! - rename NodeSocket.fromNetSocket to .fromDuplex + +### Patch Changes + +- Updated dependencies [[`5a248aa`](https://github.com/Effect-TS/effect/commit/5a248aa5ab2db3f7131ebc79bb9871a76de57973)]: + - @effect/platform@0.58.1 + +## 0.7.0 + +### Minor Changes + +- [#2938](https://github.com/Effect-TS/effect/pull/2938) [`63dd0c3`](https://github.com/Effect-TS/effect/commit/63dd0c3af45876c1caad7d03356c74daf551c628) Thanks @tim-smart! - restructure platform http to use flattened modules + + Instead of using the previous re-exports, you now use the modules directly. + + Before: + + ```ts + import { HttpClient } from "@effect/platform" + + HttpClient.request.get("/").pipe(HttpClient.client.fetchOk) + ``` + + After: + + ```ts + import { HttpClient, HttpClientRequest } from "@effect/platform" + + HttpClientRequest.get("/").pipe(HttpClient.fetchOk) + ``` + +### Patch Changes + +- Updated dependencies [[`c0ce180`](https://github.com/Effect-TS/effect/commit/c0ce180861ad0938053c0e6145e813fa6404df3b), [`63dd0c3`](https://github.com/Effect-TS/effect/commit/63dd0c3af45876c1caad7d03356c74daf551c628), [`61707b6`](https://github.com/Effect-TS/effect/commit/61707b6ffc7397c2ba0dce22512b44955724f60f), [`9c1b5b3`](https://github.com/Effect-TS/effect/commit/9c1b5b39e6c19604ce834f072a114ad392c50a06), [`a35faf8`](https://github.com/Effect-TS/effect/commit/a35faf8d116f94899bfc03feab33b004c8ddfdf7), [`ff73c0c`](https://github.com/Effect-TS/effect/commit/ff73c0cacd66132bfad2e5211b3eae347729c667), [`984d516`](https://github.com/Effect-TS/effect/commit/984d516ccd9412dc41188f6a46b748dd20dd5848), [`8c3b8a2`](https://github.com/Effect-TS/effect/commit/8c3b8a2ce208eab753b6206a51605a424f104e98), [`017e2f9`](https://github.com/Effect-TS/effect/commit/017e2f9b371ce24ea4945e5d7390c934ad3c39cf), [`91bf8a2`](https://github.com/Effect-TS/effect/commit/91bf8a2e9d1959393b3cf7366cc1d584d3e666b7), [`c6a4a26`](https://github.com/Effect-TS/effect/commit/c6a4a266606575fd2c7165940c4072ad4c57d01f)]: + - effect@3.4.0 + - @effect/platform@0.58.0 + +## 0.6.17 + +### Patch Changes + +- [#3030](https://github.com/Effect-TS/effect/pull/3030) [`3ba7ea1`](https://github.com/Effect-TS/effect/commit/3ba7ea1c3c2923e85bf2f17e41176f8f8796d203) Thanks @tim-smart! - update find-my-way-ts & multipasta + +- Updated dependencies [[`3ba7ea1`](https://github.com/Effect-TS/effect/commit/3ba7ea1c3c2923e85bf2f17e41176f8f8796d203)]: + - @effect/platform@0.57.8 + +## 0.6.16 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform@0.57.7 + +## 0.6.15 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform@0.57.6 + +## 0.6.14 + +### Patch Changes + +- Updated dependencies [[`b8ea6aa`](https://github.com/Effect-TS/effect/commit/b8ea6aa479006358042b4256ee0a1c5cfbe57acb)]: + - @effect/platform@0.57.5 + +## 0.6.13 + +### Patch Changes + +- Updated dependencies [[`6c89408`](https://github.com/Effect-TS/effect/commit/6c89408cd7b9204ec4c5828a46cd5312d8afb5e7)]: + - effect@3.3.5 + - @effect/platform@0.57.4 + +## 0.6.12 + +### Patch Changes + +- Updated dependencies [[`a67b8fe`](https://github.com/Effect-TS/effect/commit/a67b8fe2ace08419424811b5f0d9a5378eaea352)]: + - effect@3.3.4 + - @effect/platform@0.57.3 + +## 0.6.11 + +### Patch Changes + +- Updated dependencies [[`06ede85`](https://github.com/Effect-TS/effect/commit/06ede85d6e84710e6622463be95ff3927fb30dad), [`7204ca5`](https://github.com/Effect-TS/effect/commit/7204ca5761c2b1d27999a624db23aa10b6e0504d)]: + - effect@3.3.3 + - @effect/platform@0.57.2 + +## 0.6.10 + +### Patch Changes + +- [#2988](https://github.com/Effect-TS/effect/pull/2988) [`07e12ec`](https://github.com/Effect-TS/effect/commit/07e12ecdb0e20b9763bd9e9058e567a7c8862efc) Thanks @tim-smart! - refactor Socket to use do notation + +- Updated dependencies [[`07e12ec`](https://github.com/Effect-TS/effect/commit/07e12ecdb0e20b9763bd9e9058e567a7c8862efc), [`3572646`](https://github.com/Effect-TS/effect/commit/3572646d5e0804f85bc7f64633fb95722533f9dd), [`1aed347`](https://github.com/Effect-TS/effect/commit/1aed347a125ed3847ec90863424810d6759cbc85), [`df4bf4b`](https://github.com/Effect-TS/effect/commit/df4bf4b62e7b316c6647da0271fc5544a84e7ba2), [`f085f92`](https://github.com/Effect-TS/effect/commit/f085f92dfa204afb41823ffc27d437225137643d)]: + - @effect/platform@0.57.1 + - effect@3.3.2 + +## 0.6.9 + +### Patch Changes + +- Updated dependencies [[`eb98c5b`](https://github.com/Effect-TS/effect/commit/eb98c5b79ab50aa0cde239bd4e660dd19dbab612), [`184fed8`](https://github.com/Effect-TS/effect/commit/184fed83ac36cba05a75a5a8013f740f9f696e3b), [`4d3fbe8`](https://github.com/Effect-TS/effect/commit/4d3fbe82e8cec13ccd0cd0b2096deac6818fb59a), [`6068e07`](https://github.com/Effect-TS/effect/commit/6068e073d4cc8b3c8583583fd5eb3efe43f7d5ba), [`3a77e20`](https://github.com/Effect-TS/effect/commit/3a77e209783933bac3aaddba1b05ff6a9ac72b36)]: + - effect@3.3.1 + - @effect/platform@0.57.0 + +## 0.6.8 + +### Patch Changes + +- Updated dependencies [[`2b9ddfc`](https://github.com/Effect-TS/effect/commit/2b9ddfcbac505d98551e764a43923854907ca5c1), [`1f4ac00`](https://github.com/Effect-TS/effect/commit/1f4ac00a91c336c9c9c9b8c3ed9ceb9920ebc9bd), [`9305b76`](https://github.com/Effect-TS/effect/commit/9305b764cceeae4f16564435ae7172f79c2bf822), [`0f40d98`](https://github.com/Effect-TS/effect/commit/0f40d989da10f68df3ecd72b36849401ad679bfb), [`188f0a5`](https://github.com/Effect-TS/effect/commit/188f0a5c57ed0d7c9e5852e0c1c998f1b95810a1), [`b761ef0`](https://github.com/Effect-TS/effect/commit/b761ef00eaf6c67b7ffe34798b98aae5347ab376), [`b53f69b`](https://github.com/Effect-TS/effect/commit/b53f69bff1452a487b21198cd83961f844e02d36), [`0f40d98`](https://github.com/Effect-TS/effect/commit/0f40d989da10f68df3ecd72b36849401ad679bfb), [`5bd549e`](https://github.com/Effect-TS/effect/commit/5bd549e4bd7144727db438ecca6b8dc9b3ef7e22), [`67f160a`](https://github.com/Effect-TS/effect/commit/67f160a213de0219a565d4bf653b3cbf24f58e8f)]: + - @effect/platform@0.56.0 + - effect@3.3.0 + +## 0.6.7 + +### Patch Changes + +- Updated dependencies [[`a67d602`](https://github.com/Effect-TS/effect/commit/a67d60276f96cd20b76145b4cee13efca6c6158a)]: + - @effect/platform@0.55.7 + +## 0.6.6 + +### Patch Changes + +- Updated dependencies [[`8c5d280`](https://github.com/Effect-TS/effect/commit/8c5d280c0402284a4e58372867a15a431cb99461), [`6ba6d26`](https://github.com/Effect-TS/effect/commit/6ba6d269f5891e6b11aa35c5281dde4bf3273004), [`3f28bf2`](https://github.com/Effect-TS/effect/commit/3f28bf274333611906175446b772243f34f1b6d5), [`5817820`](https://github.com/Effect-TS/effect/commit/58178204a770d1a78c06945ef438f9fffbb50afa), [`799aa20`](https://github.com/Effect-TS/effect/commit/799aa20b4f618736ba33a5297fda90a75d4c26c6)]: + - effect@3.2.9 + - @effect/platform@0.55.6 + +## 0.6.5 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform@0.55.5 + +## 0.6.4 + +### Patch Changes + +- Updated dependencies [[`fb91f17`](https://github.com/Effect-TS/effect/commit/fb91f17098b48497feca9ec976feb87e4a82451b)]: + - effect@3.2.8 + - @effect/platform@0.55.4 + +## 0.6.3 + +### Patch Changes + +- Updated dependencies [[`6801fca`](https://github.com/Effect-TS/effect/commit/6801fca44366be3ee1b6b99f54bd4f38a1b5e4f4)]: + - effect@3.2.7 + - @effect/platform@0.55.3 + +## 0.6.2 + +### Patch Changes + +- Updated dependencies [[`2c2280b`](https://github.com/Effect-TS/effect/commit/2c2280b98a11fc002663c55792a4fa5781cd5fb6), [`cc8ac50`](https://github.com/Effect-TS/effect/commit/cc8ac5080daba8622ca2ff5dab5c37ddfab732ba)]: + - @effect/platform@0.55.2 + - effect@3.2.6 + +## 0.6.1 + +### Patch Changes + +- [#2859](https://github.com/Effect-TS/effect/pull/2859) [`c5c94ed`](https://github.com/Effect-TS/effect/commit/c5c94edf1ddb0abb5c0e2adbb4ec2578a98d8e07) Thanks @tim-smart! - remove temp directory for FileSystem makeTempFileScoped + +- Updated dependencies []: + - @effect/platform@0.55.1 + +## 0.6.0 + +### Minor Changes + +- [#2835](https://github.com/Effect-TS/effect/pull/2835) [`5133ca9`](https://github.com/Effect-TS/effect/commit/5133ca9dc4b8da0e28951316da9ab55dfbe0fbb9) Thanks @tim-smart! - remove pool resizing in platform workers to enable concurrent access + +### Patch Changes + +- Updated dependencies [[`608b01f`](https://github.com/Effect-TS/effect/commit/608b01fc342dbae2a642b308a67b84ead530ecea), [`031c712`](https://github.com/Effect-TS/effect/commit/031c7122a24ac42e48d6a434646b4f5d279d7442), [`a44e532`](https://github.com/Effect-TS/effect/commit/a44e532cf3a6a498b12a5aacf8124aa267e24ba0), [`5133ca9`](https://github.com/Effect-TS/effect/commit/5133ca9dc4b8da0e28951316da9ab55dfbe0fbb9)]: + - effect@3.2.5 + - @effect/platform@0.55.0 + +## 0.5.0 + +### Minor Changes + +- [#2801](https://github.com/Effect-TS/effect/pull/2801) [`1af94df`](https://github.com/Effect-TS/effect/commit/1af94df6b74aeb4f6ebcbe80e074b4cb252e62e3) Thanks @tim-smart! - remove `permits` from workers, to prevent issues with pool resizing + +### Patch Changes + +- Updated dependencies [[`1af94df`](https://github.com/Effect-TS/effect/commit/1af94df6b74aeb4f6ebcbe80e074b4cb252e62e3), [`1af94df`](https://github.com/Effect-TS/effect/commit/1af94df6b74aeb4f6ebcbe80e074b4cb252e62e3), [`c07e0ce`](https://github.com/Effect-TS/effect/commit/c07e0cea8ce165887e2c9dfa5d669eba9b2fb798), [`e313a01`](https://github.com/Effect-TS/effect/commit/e313a01b7e80f6cb7704055a190e5623c9d22c6d), [`1af94df`](https://github.com/Effect-TS/effect/commit/1af94df6b74aeb4f6ebcbe80e074b4cb252e62e3)]: + - effect@3.2.4 + - @effect/platform@0.54.0 + +## 0.4.33 + +### Patch Changes + +- Updated dependencies [[`45578e8`](https://github.com/Effect-TS/effect/commit/45578e8faa80ae33d23e08f6f19467f818b7788f)]: + - effect@3.2.3 + - @effect/platform@0.53.14 + +## 0.4.32 + +### Patch Changes + +- Updated dependencies [[`5d9266e`](https://github.com/Effect-TS/effect/commit/5d9266e8c740746ac9e186c3df6090a1b57fbe2a), [`9f8122e`](https://github.com/Effect-TS/effect/commit/9f8122e78884ab47c5e5f364d86eee1d1543cc61), [`6a6f670`](https://github.com/Effect-TS/effect/commit/6a6f6706b8613c8c7c10971b8d81a0f9e440a6f2), [`c1eaef9`](https://github.com/Effect-TS/effect/commit/c1eaef910420dae416923d172ee58d219e921d0f)]: + - effect@3.2.2 + - @effect/platform@0.53.13 + +## 0.4.31 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform@0.53.12 + +## 0.4.30 + +### Patch Changes + +- Updated dependencies [[`c1e991d`](https://github.com/Effect-TS/effect/commit/c1e991dd5ba87901cd0e05697a8b4a267e7e954a)]: + - effect@3.2.1 + - @effect/platform@0.53.11 + +## 0.4.29 + +### Patch Changes + +- Updated dependencies [[`146cadd`](https://github.com/Effect-TS/effect/commit/146cadd9d004634a3ff85c480bf92cf975c853e2), [`7135748`](https://github.com/Effect-TS/effect/commit/713574813a0f64085db0b5240ba39e7a0a7c137e), [`7135748`](https://github.com/Effect-TS/effect/commit/713574813a0f64085db0b5240ba39e7a0a7c137e), [`963b4e7`](https://github.com/Effect-TS/effect/commit/963b4e7ac87e2468feb6a344f7ab4ee4ad711198), [`64c9414`](https://github.com/Effect-TS/effect/commit/64c9414e960e82058ca09bbb3976d6fbef303a8e), [`7135748`](https://github.com/Effect-TS/effect/commit/713574813a0f64085db0b5240ba39e7a0a7c137e), [`2cbb76b`](https://github.com/Effect-TS/effect/commit/2cbb76bb52500a3f4bf27d1c91482518cbea56d7), [`870c5fa`](https://github.com/Effect-TS/effect/commit/870c5fa52cd61e745e8e828d38c3f09f00737553), [`146cadd`](https://github.com/Effect-TS/effect/commit/146cadd9d004634a3ff85c480bf92cf975c853e2), [`7135748`](https://github.com/Effect-TS/effect/commit/713574813a0f64085db0b5240ba39e7a0a7c137e), [`64c9414`](https://github.com/Effect-TS/effect/commit/64c9414e960e82058ca09bbb3976d6fbef303a8e)]: + - effect@3.2.0 + - @effect/platform@0.53.10 + +## 0.4.28 + +### Patch Changes + +- Updated dependencies [[`17fc22e`](https://github.com/Effect-TS/effect/commit/17fc22e132593c5caa563705a4748ba0f04a853c), [`810f222`](https://github.com/Effect-TS/effect/commit/810f222268792b13067c7a7bf317b93a9bb8917b), [`596aaea`](https://github.com/Effect-TS/effect/commit/596aaea022648b2e06fb1ec22f1652043d6fe64e)]: + - @effect/platform@0.53.9 + - effect@3.1.6 + +## 0.4.27 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform@0.53.8 + +## 0.4.26 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform@0.53.7 + +## 0.4.25 + +### Patch Changes + +- [#2750](https://github.com/Effect-TS/effect/pull/2750) [`6ac4847`](https://github.com/Effect-TS/effect/commit/6ac48479447c01a4f35d655552af93e47e562610) Thanks [@tim-smart](https://github.com/tim-smart)! - fix memory leak in Socket's + +- Updated dependencies [[`6ac4847`](https://github.com/Effect-TS/effect/commit/6ac48479447c01a4f35d655552af93e47e562610), [`6ac4847`](https://github.com/Effect-TS/effect/commit/6ac48479447c01a4f35d655552af93e47e562610)]: + - @effect/platform@0.53.6 + - effect@3.1.5 + +## 0.4.24 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform@0.53.5 + +## 0.4.23 + +### Patch Changes + +- Updated dependencies [[`e41e911`](https://github.com/Effect-TS/effect/commit/e41e91122fa6dd12fc81e50dcad0db891be67146)]: + - effect@3.1.4 + - @effect/platform@0.53.4 + +## 0.4.22 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform@0.53.3 + +## 0.4.21 + +### Patch Changes + +- Updated dependencies [[`1f6dc96`](https://github.com/Effect-TS/effect/commit/1f6dc96f51c7bb9c8d11415358308604ba7c7c8e)]: + - effect@3.1.3 + - @effect/platform@0.53.2 + +## 0.4.20 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform@0.53.1 + +## 0.4.19 + +### Patch Changes + +- Updated dependencies [[`d57fbbb`](https://github.com/Effect-TS/effect/commit/d57fbbbd6c466936213a671fc3cd2390064f864e)]: + - @effect/platform@0.53.0 + +## 0.4.18 + +### Patch Changes + +- Updated dependencies [[`5866c62`](https://github.com/Effect-TS/effect/commit/5866c621d7eb4cc84e4ba972bfdfd219734cd45d)]: + - @effect/platform@0.52.3 + +## 0.4.17 + +### Patch Changes + +- Updated dependencies [[`2e1cdf6`](https://github.com/Effect-TS/effect/commit/2e1cdf67d141281288fffe9a5c10d1379a800513)]: + - @effect/platform@0.52.2 + - effect@3.1.2 + +## 0.4.16 + +### Patch Changes + +- Updated dependencies [[`e5e56d1`](https://github.com/Effect-TS/effect/commit/e5e56d138dbed3204636f605229c6685f89659fc)]: + - effect@3.1.1 + - @effect/platform@0.52.1 + +## 0.4.15 + +### Patch Changes + +- Updated dependencies [[`9deab0a`](https://github.com/Effect-TS/effect/commit/9deab0aec9e99501f9441843e34df9afa10c5be9), [`7719b8a`](https://github.com/Effect-TS/effect/commit/7719b8a7350c14e952ffe685bfd5308773b3e271)]: + - @effect/platform@0.52.0 + +## 0.4.14 + +### Patch Changes + +- Updated dependencies [[`c3c12c6`](https://github.com/Effect-TS/effect/commit/c3c12c6625633fe80e79f9db75a3b8cf8ca8b11d), [`ba64ea6`](https://github.com/Effect-TS/effect/commit/ba64ea6757810c5e74cad3863a7d19d4d38af66b), [`b5de2d2`](https://github.com/Effect-TS/effect/commit/b5de2d2ce5b1afe8be90827bf898a95cec40eb2b), [`a1c7ab8`](https://github.com/Effect-TS/effect/commit/a1c7ab8ffedacd18c1fc784f4ff5844f79498b83), [`a023f28`](https://github.com/Effect-TS/effect/commit/a023f28336f3865687d9a30c1883e36909906d85), [`0ec93cb`](https://github.com/Effect-TS/effect/commit/0ec93cb4f166e7401c171c2f8e8276ce958d9a57), [`1c9454d`](https://github.com/Effect-TS/effect/commit/1c9454d532eae79b9f759aea77f59332cc6d18ed), [`a023f28`](https://github.com/Effect-TS/effect/commit/a023f28336f3865687d9a30c1883e36909906d85), [`92d56db`](https://github.com/Effect-TS/effect/commit/92d56dbb3f33e36636c2a2f1030c56492e39cf4d)]: + - effect@3.1.0 + - @effect/platform@0.51.0 + +## 0.4.13 + +### Patch Changes + +- [#2656](https://github.com/Effect-TS/effect/pull/2656) [`557707b`](https://github.com/Effect-TS/effect/commit/557707bc9e5f230c8964d2757012075c34339b5c) Thanks [@tim-smart](https://github.com/tim-smart)! - update dependencies + +- Updated dependencies [[`16039a0`](https://github.com/Effect-TS/effect/commit/16039a08f04f11545e2fdf40952788a8f9cef04f), [`557707b`](https://github.com/Effect-TS/effect/commit/557707bc9e5f230c8964d2757012075c34339b5c), [`d1d33e1`](https://github.com/Effect-TS/effect/commit/d1d33e10b25109f44b5ab1c6e4d778a59c0d3eeb), [`f4ed306`](https://github.com/Effect-TS/effect/commit/f4ed3068a70b50302d078a30d18ca3cfd2bc679c), [`661004f`](https://github.com/Effect-TS/effect/commit/661004f4bf5f8b25f5a0678c21a3a822188ce461), [`e79cb83`](https://github.com/Effect-TS/effect/commit/e79cb83d3b19098bc40a3012e2a059b8426306c2)]: + - @effect/platform@0.50.8 + - effect@3.0.8 + +## 0.4.12 + +### Patch Changes + +- Updated dependencies [[`18de56b`](https://github.com/Effect-TS/effect/commit/18de56b4a6b6d1f99230dfabf9147d59ea4dd759)]: + - effect@3.0.7 + - @effect/platform@0.50.7 + +## 0.4.11 + +### Patch Changes + +- Updated dependencies [[`ffe4f4e`](https://github.com/Effect-TS/effect/commit/ffe4f4e95db35fff6869e360b072e3837befa0a1), [`027418e`](https://github.com/Effect-TS/effect/commit/027418edaa6aa6c0ae4861b95832827b45adace4), [`ac1898e`](https://github.com/Effect-TS/effect/commit/ac1898eb7bc96880f911c276048e2ea3d6fe9c50), [`ffe4f4e`](https://github.com/Effect-TS/effect/commit/ffe4f4e95db35fff6869e360b072e3837befa0a1)]: + - effect@3.0.6 + - @effect/platform@0.50.6 + +## 0.4.10 + +### Patch Changes + +- Updated dependencies [[`6222404`](https://github.com/Effect-TS/effect/commit/62224044678751829ed2f128e05133a91c6b0569), [`868ed2a`](https://github.com/Effect-TS/effect/commit/868ed2a8fe94ee7f4206a6070f29dcf2a5ba1dc3)]: + - effect@3.0.5 + - @effect/platform@0.50.5 + +## 0.4.9 + +### Patch Changes + +- Updated dependencies [[`9a24667`](https://github.com/Effect-TS/effect/commit/9a246672008a2b668d43fbfd2fe5508c54b2b920)]: + - effect@3.0.4 + - @effect/platform@0.50.4 + +## 0.4.8 + +### Patch Changes + +- Updated dependencies [[`b3b51a2`](https://github.com/Effect-TS/effect/commit/b3b51a2ea0c6ab92a363db46ebaa7e1176d089f5)]: + - @effect/platform@0.50.3 + +## 0.4.7 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform@0.50.2 + +## 0.4.6 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform@0.50.1 + +## 0.4.5 + +### Patch Changes + +- Updated dependencies [[`6f38dff`](https://github.com/Effect-TS/effect/commit/6f38dff41ffa34532cc2f25b90446550c5730bb6), [`a7b4b84`](https://github.com/Effect-TS/effect/commit/a7b4b84bd5a25f51aba922f9259c3a58c98c6a4e), [`a3b0e6c`](https://github.com/Effect-TS/effect/commit/a3b0e6c490772e6d44b5d98dcf2729c4d5310ecc), [`6f38dff`](https://github.com/Effect-TS/effect/commit/6f38dff41ffa34532cc2f25b90446550c5730bb6)]: + - @effect/platform@0.50.0 + - effect@3.0.3 + +## 0.4.4 + +### Patch Changes + +- [#2562](https://github.com/Effect-TS/effect/pull/2562) [`2cecdbd`](https://github.com/Effect-TS/effect/commit/2cecdbd1cf30befce4e84796ccd953ea55ecfb86) Thanks [@fubhy](https://github.com/fubhy)! - Added provenance publishing + +- Updated dependencies [[`2cecdbd`](https://github.com/Effect-TS/effect/commit/2cecdbd1cf30befce4e84796ccd953ea55ecfb86)]: + - @effect/platform@0.49.4 + - effect@3.0.2 + +## 0.4.3 + +### Patch Changes + +- Updated dependencies [[`8d39d65`](https://github.com/Effect-TS/effect/commit/8d39d6554af548228ad767112ce2e0b1f68fa8e1)]: + - @effect/platform@0.49.3 + +## 0.4.2 + +### Patch Changes + +- [#2556](https://github.com/Effect-TS/effect/pull/2556) [`5ef0a1a`](https://github.com/Effect-TS/effect/commit/5ef0a1ae9b773fa2481550cb0d43ff7a0e03cd44) Thanks [@tim-smart](https://github.com/tim-smart)! - fix Command stdin being closed too early + +- Updated dependencies [[`5ef0a1a`](https://github.com/Effect-TS/effect/commit/5ef0a1ae9b773fa2481550cb0d43ff7a0e03cd44)]: + - @effect/platform@0.49.2 + +## 0.4.1 + +### Patch Changes + +- [#2542](https://github.com/Effect-TS/effect/pull/2542) [`87c5687`](https://github.com/Effect-TS/effect/commit/87c5687de0782dab177b7861217fa3b040046282) Thanks [@tim-smart](https://github.com/tim-smart)! - allow fs.watch backend to be customized + + If you want to use the @parcel/watcher backend, you now need to provide it to + your effects. + + ```ts + import { Layer } from "effect" + import { FileSystem } from "@effect/platform" + import { NodeFileSystem } from "@effect/platform-node" + import * as ParcelWatcher from "@effect/platform-node/NodeFileSystem/ParcelWatcher" + + // create a Layer that uses the ParcelWatcher backend + NodeFileSystem.layer.pipe(Layer.provide(ParcelWatcher.layer)) + ``` + +- Updated dependencies [[`3da0cfa`](https://github.com/Effect-TS/effect/commit/3da0cfa12c407fd930dc480be1ecc9217a8058f8), [`570e8d8`](https://github.com/Effect-TS/effect/commit/570e8d87e7c0e9ad4cd2686462fdb9b4812f7716), [`87c5687`](https://github.com/Effect-TS/effect/commit/87c5687de0782dab177b7861217fa3b040046282), [`8edacca`](https://github.com/Effect-TS/effect/commit/8edacca37f8e37c01a63fec332b06d9361efaa7b)]: + - effect@3.0.1 + - @effect/platform@0.49.1 + +## 0.4.0 + +### Minor Changes + +- [#2207](https://github.com/Effect-TS/effect/pull/2207) [`aa4a3b5`](https://github.com/Effect-TS/effect/commit/aa4a3b550da1c1020265ac389ed3f309388994a2) Thanks [@github-actions](https://github.com/apps/github-actions)! - Swap type parameters in /platform data types + + A codemod has been released to make migration easier: + + ``` + npx @effect/codemod platform-0.49 src/**/* + ``` + +- [#2207](https://github.com/Effect-TS/effect/pull/2207) [`5a2314b`](https://github.com/Effect-TS/effect/commit/5a2314b70ec79c2c02b51cef45a5ddec8327daa1) Thanks [@github-actions](https://github.com/apps/github-actions)! - replace use of `unit` terminology with `void` + + For all the data types. + + ```ts + Effect.unit // => Effect.void + Stream.unit // => Stream.void + + // etc + ``` + +- [#2207](https://github.com/Effect-TS/effect/pull/2207) [`2fb7d9c`](https://github.com/Effect-TS/effect/commit/2fb7d9ca15037ff62a578bb9fe5732da5f4f317d) Thanks [@github-actions](https://github.com/apps/github-actions)! - Release Effect 3.0 🎉 + +### Patch Changes + +- [#2529](https://github.com/Effect-TS/effect/pull/2529) [`78b767c`](https://github.com/Effect-TS/effect/commit/78b767c2b1625186e17131761a0edbac25d21850) Thanks [@fubhy](https://github.com/fubhy)! - Renamed `ReadonlyArray` and `ReadonlyRecord` modules for better discoverability. + +- Updated dependencies [[`1b5f0c7`](https://github.com/Effect-TS/effect/commit/1b5f0c77e7fd477a0026071e82129a948227f4b3), [`d50a652`](https://github.com/Effect-TS/effect/commit/d50a652479f4d1d64f48da05c79fa847e6e51548), [`9a3bd47`](https://github.com/Effect-TS/effect/commit/9a3bd47ebd0750c7e498162734f6d21895de0cb2), [`6460414`](https://github.com/Effect-TS/effect/commit/6460414351a45fb8e0a457c63f3653422efee766), [`cf69f46`](https://github.com/Effect-TS/effect/commit/cf69f46690058d71eeada03cfb40dc744573e9e4), [`cf69f46`](https://github.com/Effect-TS/effect/commit/cf69f46690058d71eeada03cfb40dc744573e9e4), [`be9d025`](https://github.com/Effect-TS/effect/commit/be9d025e42355260ace02dd135851a8935a4deba), [`78b767c`](https://github.com/Effect-TS/effect/commit/78b767c2b1625186e17131761a0edbac25d21850), [`aa4a3b5`](https://github.com/Effect-TS/effect/commit/aa4a3b550da1c1020265ac389ed3f309388994a2), [`1499974`](https://github.com/Effect-TS/effect/commit/14999741d2e19c1747f6a7e19d68977f6429cdb8), [`1b5f0c7`](https://github.com/Effect-TS/effect/commit/1b5f0c77e7fd477a0026071e82129a948227f4b3), [`5c2b561`](https://github.com/Effect-TS/effect/commit/5c2b5614f583b88784ed68126ae939832fb3c092), [`6c6087a`](https://github.com/Effect-TS/effect/commit/6c6087a4a897b64252346426660782d31c13f769), [`a18f594`](https://github.com/Effect-TS/effect/commit/a18f5948f1439a147232448b2c443472fda0eceb), [`1499974`](https://github.com/Effect-TS/effect/commit/14999741d2e19c1747f6a7e19d68977f6429cdb8), [`2f96d93`](https://github.com/Effect-TS/effect/commit/2f96d938b90f8c19377583279e3c7afd9b509c50), [`5a2314b`](https://github.com/Effect-TS/effect/commit/5a2314b70ec79c2c02b51cef45a5ddec8327daa1), [`25d74f8`](https://github.com/Effect-TS/effect/commit/25d74f8c4d2dd4a9e5ec57ce2f20d36dedd25343), [`6c6087a`](https://github.com/Effect-TS/effect/commit/6c6087a4a897b64252346426660782d31c13f769), [`271b79f`](https://github.com/Effect-TS/effect/commit/271b79fc0b66a6c11e07a8779ff8800493a7eac2), [`1b5f0c7`](https://github.com/Effect-TS/effect/commit/1b5f0c77e7fd477a0026071e82129a948227f4b3), [`2fb7d9c`](https://github.com/Effect-TS/effect/commit/2fb7d9ca15037ff62a578bb9fe5732da5f4f317d), [`53d1c2a`](https://github.com/Effect-TS/effect/commit/53d1c2a77559081fbb89667e343346375c6d6650), [`e7e1bbe`](https://github.com/Effect-TS/effect/commit/e7e1bbe68486fdf31c8f84b0880522d39adcaad3), [`10c169e`](https://github.com/Effect-TS/effect/commit/10c169eadc874e91b4defca3f467b4e6a50fd8f3), [`6424181`](https://github.com/Effect-TS/effect/commit/64241815fe6a939e91e6947253e7dceea1306aa8)]: + - effect@3.0.0 + - @effect/platform@0.49.0 + +## 0.3.29 + +### Patch Changes + +- Updated dependencies [[`b79cc59`](https://github.com/Effect-TS/effect/commit/b79cc59dbe64b9a0a7742dc9100a9d36c8e46b72)]: + - @effect/platform@0.48.29 + +## 0.3.28 + +### Patch Changes + +- Updated dependencies [[`41c8102`](https://github.com/Effect-TS/effect/commit/41c810228b1a50e4b41f19e735d7c62fe8d36871), [`776ef2b`](https://github.com/Effect-TS/effect/commit/776ef2bb66db9aa9f68b7beab14f6986f9c1288b), [`217147e`](https://github.com/Effect-TS/effect/commit/217147ea67c5c42c96f024775c41e5b070f81e4c), [`90776ec`](https://github.com/Effect-TS/effect/commit/90776ec8e8671d835b65fc33ead1de6c864b81b9), [`8709856`](https://github.com/Effect-TS/effect/commit/870985694ae985c3cb9360ad8a25c60e6f785f55), [`232c353`](https://github.com/Effect-TS/effect/commit/232c353c2e6f743f38e57639ee30e324ffa9c2a9), [`0ca835c`](https://github.com/Effect-TS/effect/commit/0ca835cbac8e69072a93ace83b534219faba24e8), [`8709856`](https://github.com/Effect-TS/effect/commit/870985694ae985c3cb9360ad8a25c60e6f785f55), [`d590094`](https://github.com/Effect-TS/effect/commit/d5900943489ec1e0891836aeafb5ce99fb9c75c7), [`e983740`](https://github.com/Effect-TS/effect/commit/e9837401145605aff5bc2ec7e73004f397c5d2d1), [`e3e0924`](https://github.com/Effect-TS/effect/commit/e3e09247d46a35430fc60e4aa4032cc50814f212)]: + - effect@2.4.19 + - @effect/platform@0.48.28 + +## 0.3.27 + +### Patch Changes + +- [#2486](https://github.com/Effect-TS/effect/pull/2486) [`672f137`](https://github.com/Effect-TS/effect/commit/672f13747ddf6dac3ba304fd4511b1df44ab566d) Thanks [@tim-smart](https://github.com/tim-smart)! - accept string as a valid Socket input + +- [#2486](https://github.com/Effect-TS/effect/pull/2486) [`672f137`](https://github.com/Effect-TS/effect/commit/672f13747ddf6dac3ba304fd4511b1df44ab566d) Thanks [@tim-smart](https://github.com/tim-smart)! - add Socket.runRaw to handle strings directly + +- Updated dependencies [[`c6dd3c6`](https://github.com/Effect-TS/effect/commit/c6dd3c6909cafe05adc8450c5a499260e17e60d3), [`672f137`](https://github.com/Effect-TS/effect/commit/672f13747ddf6dac3ba304fd4511b1df44ab566d), [`672f137`](https://github.com/Effect-TS/effect/commit/672f13747ddf6dac3ba304fd4511b1df44ab566d)]: + - @effect/platform@0.48.27 + +## 0.3.26 + +### Patch Changes + +- Updated dependencies [[`365a486`](https://github.com/Effect-TS/effect/commit/365a4865de5e47ce09f4cfd51fc0f67438f82a57)]: + - @effect/platform@0.48.26 + +## 0.3.25 + +### Patch Changes + +- Updated dependencies [[`dadc690`](https://github.com/Effect-TS/effect/commit/dadc6906121c512bc32be22b52adbd1ada834594), [`d209171`](https://github.com/Effect-TS/effect/commit/d2091714a786820ebae4bef04a9d67d25dd08e88)]: + - effect@2.4.18 + - @effect/platform@0.48.25 + +## 0.3.24 + +### Patch Changes + +- [#2458](https://github.com/Effect-TS/effect/pull/2458) [`f993857`](https://github.com/Effect-TS/effect/commit/f993857d5bb21ff7317ec69e481499632f0365f3) Thanks [@tim-smart](https://github.com/tim-smart)! - use node fs/promises for readdir, to fix recursive option + +- [#2427](https://github.com/Effect-TS/effect/pull/2427) [`9c6a500`](https://github.com/Effect-TS/effect/commit/9c6a5001b467b6255c68a922f4b6e8d692b63d01) Thanks [@devmatteini](https://github.com/devmatteini)! - add force option to FileSystem.remove + +- Updated dependencies [[`9c6a500`](https://github.com/Effect-TS/effect/commit/9c6a5001b467b6255c68a922f4b6e8d692b63d01), [`8fdfda6`](https://github.com/Effect-TS/effect/commit/8fdfda6618be848c01b399d13bc05a9a3adfb613), [`607b2e7`](https://github.com/Effect-TS/effect/commit/607b2e7a7fd9318c57acf4e50ec61747eea74ad7), [`8fdfda6`](https://github.com/Effect-TS/effect/commit/8fdfda6618be848c01b399d13bc05a9a3adfb613), [`35ad0ba`](https://github.com/Effect-TS/effect/commit/35ad0ba9f3ba27c60453620e514b980f819f92af), [`8206caf`](https://github.com/Effect-TS/effect/commit/8206caf7c2d22c68be4313318b61cfdacf6222b6), [`7ddd654`](https://github.com/Effect-TS/effect/commit/7ddd65415b65ccb654ad04f4dbefe39402f15117), [`7ddd654`](https://github.com/Effect-TS/effect/commit/7ddd65415b65ccb654ad04f4dbefe39402f15117), [`8fdfda6`](https://github.com/Effect-TS/effect/commit/8fdfda6618be848c01b399d13bc05a9a3adfb613), [`f456ba2`](https://github.com/Effect-TS/effect/commit/f456ba273bae21a6dcf8c966c50c97b5f0897d9f)]: + - @effect/platform@0.48.24 + - effect@2.4.17 + +## 0.3.23 + +### Patch Changes + +- Updated dependencies [[`5170ce7`](https://github.com/Effect-TS/effect/commit/5170ce708c606283e8a30d273950f1a21c7eddc2), [`63a1df2`](https://github.com/Effect-TS/effect/commit/63a1df2e4de3766f48f15676fbd0360ab9c27816), [`5170ce7`](https://github.com/Effect-TS/effect/commit/5170ce708c606283e8a30d273950f1a21c7eddc2), [`74a5dae`](https://github.com/Effect-TS/effect/commit/74a5daed0e65b32a36e026bfcf66d02269cb967a)]: + - @effect/platform@0.48.23 + - effect@2.4.16 + +## 0.3.22 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform@0.48.22 + +## 0.3.21 + +### Patch Changes + +- Updated dependencies [[`d7688c0`](https://github.com/Effect-TS/effect/commit/d7688c0c72717fe7876c871567f6946dabfc0546), [`b3a4fac`](https://github.com/Effect-TS/effect/commit/b3a4face2acaca422f0b0530436e8f13129f3b3a)]: + - effect@2.4.15 + - @effect/platform@0.48.21 + +## 0.3.20 + +### Patch Changes + +- Updated dependencies [[`4789083`](https://github.com/Effect-TS/effect/commit/4789083283bdaec456982d614ebc4a496ea0e7f7), [`4789083`](https://github.com/Effect-TS/effect/commit/4789083283bdaec456982d614ebc4a496ea0e7f7)]: + - @effect/platform@0.48.20 + +## 0.3.19 + +### Patch Changes + +- Updated dependencies [[`fb7285e`](https://github.com/Effect-TS/effect/commit/fb7285e8d6a70527df7137a6a3efdd03ae61cb8b)]: + - @effect/platform@0.48.19 + +## 0.3.18 + +### Patch Changes + +- [#2410](https://github.com/Effect-TS/effect/pull/2410) [`26435ec`](https://github.com/Effect-TS/effect/commit/26435ecfa06569dc18d1801ccf38213a43b7c334) Thanks [@tim-smart](https://github.com/tim-smart)! - add undici http client to @effect/platform-node + +- [#2410](https://github.com/Effect-TS/effect/pull/2410) [`26435ec`](https://github.com/Effect-TS/effect/commit/26435ecfa06569dc18d1801ccf38213a43b7c334) Thanks [@tim-smart](https://github.com/tim-smart)! - add NodeStream.toReadable + + With this api you can convert an Effect Stream into a node.js Readable stream. + + ```ts + import { Stream } from "effect" + import * as NodeStream from "@effect/platform-node/NodeStream" + + // Effect + NodeStream.toReadable(Stream.make("a", "b", "c")) + ``` + +- Updated dependencies [[`26435ec`](https://github.com/Effect-TS/effect/commit/26435ecfa06569dc18d1801ccf38213a43b7c334), [`6180c0c`](https://github.com/Effect-TS/effect/commit/6180c0cc51dee785cfce72220a52c9fc3b9bf9aa)]: + - @effect/platform@0.48.18 + - effect@2.4.14 + +## 0.3.17 + +### Patch Changes + +- Updated dependencies [[`47a8f1b`](https://github.com/Effect-TS/effect/commit/47a8f1b644d8294692d92cacd3c8c7543edbfabe), [`8c9abe2`](https://github.com/Effect-TS/effect/commit/8c9abe2b35c46d8891d4b2c14ff9eb46302a14f3), [`8c9abe2`](https://github.com/Effect-TS/effect/commit/8c9abe2b35c46d8891d4b2c14ff9eb46302a14f3), [`3336287`](https://github.com/Effect-TS/effect/commit/3336287ff55a25e56d759b83847bfaa21c40f499), [`54b7c00`](https://github.com/Effect-TS/effect/commit/54b7c0077fa784ad2646b812d6a44641f672edcd), [`3336287`](https://github.com/Effect-TS/effect/commit/3336287ff55a25e56d759b83847bfaa21c40f499), [`47a8f1b`](https://github.com/Effect-TS/effect/commit/47a8f1b644d8294692d92cacd3c8c7543edbfabe)]: + - @effect/platform@0.48.17 + - effect@2.4.13 + +## 0.3.16 + +### Patch Changes + +- [#2387](https://github.com/Effect-TS/effect/pull/2387) [`75a8d16`](https://github.com/Effect-TS/effect/commit/75a8d16247cc14860cdd7fd948ef542c50c2d55e) Thanks [@tim-smart](https://github.com/tim-smart)! - add Cookies module to /platform http + + To add cookies to a http response: + + ```ts + import * as Http from "@effect/platform/HttpServer" + + Http.response.empty().pipe( + Http.response.setCookies([ + ["name", "value"], + ["foo", "bar", { httpOnly: true }] + ]) + ) + ``` + + You can also use cookies with the http client: + + ```ts + import * as Http from "@effect/platform/HttpClient" + import { Effect, Ref } from "effect" + + Effect.gen(function* (_) { + const ref = yield* _(Ref.make(Http.cookies.empty)) + const defaultClient = yield* _(Http.client.Client) + const clientWithCookies = defaultClient.pipe( + Http.client.withCookiesRef(ref), + Http.client.filterStatusOk + ) + + // cookies will be stored in the ref and sent in any subsequent requests + yield* _( + Http.request.get("https://www.google.com/"), + clientWithCookies, + Effect.scoped + ) + }) + ``` + +- [#2385](https://github.com/Effect-TS/effect/pull/2385) [`3307729`](https://github.com/Effect-TS/effect/commit/3307729de162a033fa9caa8e14c111013dcf0d87) Thanks [@tim-smart](https://github.com/tim-smart)! - update typescript to 5.4 + +- Updated dependencies [[`75a8d16`](https://github.com/Effect-TS/effect/commit/75a8d16247cc14860cdd7fd948ef542c50c2d55e), [`3307729`](https://github.com/Effect-TS/effect/commit/3307729de162a033fa9caa8e14c111013dcf0d87)]: + - @effect/platform@0.48.16 + - effect@2.4.12 + +## 0.3.15 + +### Patch Changes + +- [#2384](https://github.com/Effect-TS/effect/pull/2384) [`2f488c4`](https://github.com/Effect-TS/effect/commit/2f488c436de52576562803c57ebc132ef40ccdd8) Thanks [@tim-smart](https://github.com/tim-smart)! - update dependencies + +- Updated dependencies [[`2f488c4`](https://github.com/Effect-TS/effect/commit/2f488c436de52576562803c57ebc132ef40ccdd8), [`37ca592`](https://github.com/Effect-TS/effect/commit/37ca592a4101ad90adbf8c8b3f727faf3110cae5), [`317b5b8`](https://github.com/Effect-TS/effect/commit/317b5b8e8c8c2207469b3ebfcf72bf3a9f7cbc60)]: + - effect@2.4.11 + - @effect/platform@0.48.15 + +## 0.3.14 + +### Patch Changes + +- Updated dependencies [[`9bab1f9`](https://github.com/Effect-TS/effect/commit/9bab1f9fa5b999740755e4e82485cb77c638643a), [`9bbde5b`](https://github.com/Effect-TS/effect/commit/9bbde5be9a0168d1c2a0308bfc27167ed62f3968)]: + - effect@2.4.10 + - @effect/platform@0.48.14 + +## 0.3.13 + +### Patch Changes + +- [#2368](https://github.com/Effect-TS/effect/pull/2368) [`1879f62`](https://github.com/Effect-TS/effect/commit/1879f629d0c4815dbb5955779247cd3f3da5cd85) Thanks [@tim-smart](https://github.com/tim-smart)! - fallback to node fs.watch if @parcel/watcher fails to import + +- Updated dependencies []: + - @effect/platform@0.48.13 + +## 0.3.12 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform@0.48.12 + +## 0.3.11 + +### Patch Changes + +- [#2360](https://github.com/Effect-TS/effect/pull/2360) [`0f6c7b4`](https://github.com/Effect-TS/effect/commit/0f6c7b426eb3432f60e3a17f8cd92ceac91597bf) Thanks [@tim-smart](https://github.com/tim-smart)! - add support for watching single files + +- Updated dependencies [[`0f6c7b4`](https://github.com/Effect-TS/effect/commit/0f6c7b426eb3432f60e3a17f8cd92ceac91597bf)]: + - @effect/platform@0.48.11 + +## 0.3.10 + +### Patch Changes + +- [#2357](https://github.com/Effect-TS/effect/pull/2357) [`71fd528`](https://github.com/Effect-TS/effect/commit/71fd5287500f9ce155a7d9f0df6ee3e0ac3aeb99) Thanks [@tim-smart](https://github.com/tim-smart)! - make more data types in /platform implement Inspectable + +- Updated dependencies [[`71fd528`](https://github.com/Effect-TS/effect/commit/71fd5287500f9ce155a7d9f0df6ee3e0ac3aeb99)]: + - @effect/platform@0.48.10 + - effect@2.4.9 + +## 0.3.9 + +### Patch Changes + +- Updated dependencies [[`bb0b69e`](https://github.com/Effect-TS/effect/commit/bb0b69e519698c7c76aa68217de423c78ad16566), [`6b20bad`](https://github.com/Effect-TS/effect/commit/6b20badebb3a7ca4d38857753e8ecaa09d02ccfb), [`4e64e9b`](https://github.com/Effect-TS/effect/commit/4e64e9b9876de6bfcbabe39e18a91a08e5f3fbb0), [`3851a02`](https://github.com/Effect-TS/effect/commit/3851a022c481006aec1db36651e4b4fd727aa742), [`5f5fcd9`](https://github.com/Effect-TS/effect/commit/5f5fcd969ae30ed6fe61d566a571498d9e895e16), [`814e5b8`](https://github.com/Effect-TS/effect/commit/814e5b828f68210b9e8f336fd6ac688646835dd9)]: + - effect@2.4.8 + - @effect/platform@0.48.9 + +## 0.3.8 + +### Patch Changes + +- [#2334](https://github.com/Effect-TS/effect/pull/2334) [`69d27bb`](https://github.com/Effect-TS/effect/commit/69d27bb633884b6b50f9c3d9e95c29f09b4860b5) Thanks [@tim-smart](https://github.com/tim-smart)! - add .watch method to /platform FileSystem + + It can be used to listen for file system events. Example: + + ```ts + import { FileSystem } from "@effect/platform" + import { NodeFileSystem, NodeRuntime } from "@effect/platform-node" + import { Console, Effect, Stream } from "effect" + + Effect.gen(function* (_) { + const fs = yield* _(FileSystem.FileSystem) + yield* _(fs.watch("./"), Stream.runForEach(Console.log)) + }).pipe(Effect.provide(NodeFileSystem.layer), NodeRuntime.runMain) + ``` + +- Updated dependencies [[`69d27bb`](https://github.com/Effect-TS/effect/commit/69d27bb633884b6b50f9c3d9e95c29f09b4860b5)]: + - @effect/platform@0.48.8 + +## 0.3.7 + +### Patch Changes + +- Updated dependencies [[`f908948`](https://github.com/Effect-TS/effect/commit/f908948fd05771a670c0b746e2dd9caa9408ef83)]: + - @effect/platform@0.48.7 + +## 0.3.6 + +### Patch Changes + +- Updated dependencies [[`eb93283`](https://github.com/Effect-TS/effect/commit/eb93283985913d7b04ca750e36ac8513e7b6cef6)]: + - effect@2.4.7 + - @effect/platform@0.48.6 + +## 0.3.5 + +### Patch Changes + +- [#2325](https://github.com/Effect-TS/effect/pull/2325) [`e006e4a`](https://github.com/Effect-TS/effect/commit/e006e4a538c97bae6ca1efa74802159e8a688fcb) Thanks [@tim-smart](https://github.com/tim-smart)! - ensure Socket fibers are interruptible + +- Updated dependencies [[`e006e4a`](https://github.com/Effect-TS/effect/commit/e006e4a538c97bae6ca1efa74802159e8a688fcb)]: + - @effect/platform@0.48.5 + +## 0.3.4 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform@0.48.4 + +## 0.3.3 + +### Patch Changes + +- [#2314](https://github.com/Effect-TS/effect/pull/2314) [`c362e06`](https://github.com/Effect-TS/effect/commit/c362e066550252d5a9fcbc31a4b34d0e17c50699) Thanks [@tim-smart](https://github.com/tim-smart)! - prevent unhandled fiber errors in Sockets + +- Updated dependencies [[`c362e06`](https://github.com/Effect-TS/effect/commit/c362e066550252d5a9fcbc31a4b34d0e17c50699), [`83ddd6f`](https://github.com/Effect-TS/effect/commit/83ddd6f41029724b2cbd144cf309463967ed1164)]: + - @effect/platform@0.48.3 + +## 0.3.2 + +### Patch Changes + +- Updated dependencies [[`4f35a7e`](https://github.com/Effect-TS/effect/commit/4f35a7e7c4eba598924aff24d1158b9056bb24be), [`9971186`](https://github.com/Effect-TS/effect/commit/99711862722188fbb5ed3ee75126ad5edf13f72f)]: + - @effect/platform@0.48.2 + - effect@2.4.6 + +## 0.3.1 + +### Patch Changes + +- Updated dependencies [[`bce21c5`](https://github.com/Effect-TS/effect/commit/bce21c5ded2177114666ba229bd5029fa000dee3), [`c7d3036`](https://github.com/Effect-TS/effect/commit/c7d303630b7f0825cb2e584557c5767a67214d9f)]: + - effect@2.4.5 + - @effect/platform@0.48.1 + +## 0.3.0 + +### Minor Changes + +- [#2287](https://github.com/Effect-TS/effect/pull/2287) [`a1f44cb`](https://github.com/Effect-TS/effect/commit/a1f44cb5112713ff9a3ac3d91a63a2c99d6b7fc1) Thanks [@tim-smart](https://github.com/tim-smart)! - add option to /platform runMain to disable error reporting + +### Patch Changes + +- [#2283](https://github.com/Effect-TS/effect/pull/2283) [`509be1a`](https://github.com/Effect-TS/effect/commit/509be1a0817118489750cf028523134677e44a8a) Thanks [@tim-smart](https://github.com/tim-smart)! - add SocketCloseError with additional metadata + +- [#2281](https://github.com/Effect-TS/effect/pull/2281) [`e7ca973`](https://github.com/Effect-TS/effect/commit/e7ca973c5430ae60716701e58bedd4632ff971fd) Thanks [@tim-smart](https://github.com/tim-smart)! - support closing a Socket by writing a CloseEvent + +- Updated dependencies [[`5d47ee0`](https://github.com/Effect-TS/effect/commit/5d47ee0855e492532085b6092879b1b952d84949), [`1cb7f9c`](https://github.com/Effect-TS/effect/commit/1cb7f9cff7c2272a32fc7a324d87b02e2cd8a2f5), [`817a04c`](https://github.com/Effect-TS/effect/commit/817a04cb2df0f4140984dc97eb3e1bb14a6c4a38), [`d90a99d`](https://github.com/Effect-TS/effect/commit/d90a99d03d074adc7cd2533f15419138264da5a2), [`dd05faa`](https://github.com/Effect-TS/effect/commit/dd05faa621555ef3585ecd914ac13ecd89b710f4), [`509be1a`](https://github.com/Effect-TS/effect/commit/509be1a0817118489750cf028523134677e44a8a), [`1cb7f9c`](https://github.com/Effect-TS/effect/commit/1cb7f9cff7c2272a32fc7a324d87b02e2cd8a2f5), [`dd05faa`](https://github.com/Effect-TS/effect/commit/dd05faa621555ef3585ecd914ac13ecd89b710f4), [`e7ca973`](https://github.com/Effect-TS/effect/commit/e7ca973c5430ae60716701e58bedd4632ff971fd), [`a1f44cb`](https://github.com/Effect-TS/effect/commit/a1f44cb5112713ff9a3ac3d91a63a2c99d6b7fc1), [`d910dd2`](https://github.com/Effect-TS/effect/commit/d910dd2ca1e8e5aa2f09d9bf3694ede745758f99), [`e7ca973`](https://github.com/Effect-TS/effect/commit/e7ca973c5430ae60716701e58bedd4632ff971fd), [`802674b`](https://github.com/Effect-TS/effect/commit/802674b379b7559ad3ff09b33388891445a9e48b), [`bdff193`](https://github.com/Effect-TS/effect/commit/bdff193365dd9ec2863573b08eb960aa8dee5c93)]: + - effect@2.4.4 + - @effect/platform@0.48.0 + +## 0.2.5 + +### Patch Changes + +- Updated dependencies [[`0680545`](https://github.com/Effect-TS/effect/commit/068054540f19bb23a79c7c021ed8b2fe34f3e19f), [`20e63fb`](https://github.com/Effect-TS/effect/commit/20e63fb9207210f3fe2d136ec40d0a2dbff3225e), [`20e63fb`](https://github.com/Effect-TS/effect/commit/20e63fb9207210f3fe2d136ec40d0a2dbff3225e)]: + - @effect/platform@0.47.1 + - effect@2.4.3 + +## 0.2.4 + +### Patch Changes + +- [#2267](https://github.com/Effect-TS/effect/pull/2267) [`0f3d99c`](https://github.com/Effect-TS/effect/commit/0f3d99c27521ec6b221b644a0fffc79199c3acca) Thanks [@tim-smart](https://github.com/tim-smart)! - propogate Socket handler errors to .run Effect + +- Updated dependencies [[`e03811e`](https://github.com/Effect-TS/effect/commit/e03811e80c93e986e6348b3b67ac2ed6d5fefff0), [`ac41d84`](https://github.com/Effect-TS/effect/commit/ac41d84776484cdce8165b7ca2c9c9b6377eee2d), [`0f3d99c`](https://github.com/Effect-TS/effect/commit/0f3d99c27521ec6b221b644a0fffc79199c3acca), [`6137533`](https://github.com/Effect-TS/effect/commit/613753300c7705518ab1fea2f370b032851c2750), [`f373529`](https://github.com/Effect-TS/effect/commit/f373529999f4b8bc92b634f6ea14f19271388eed), [`1bf9f31`](https://github.com/Effect-TS/effect/commit/1bf9f31f07667de677673f7c29a4e7a26ebad3c8), [`e3ff789`](https://github.com/Effect-TS/effect/commit/e3ff789226f89e71eb28ca38ce79f90af6a03f1a), [`6137533`](https://github.com/Effect-TS/effect/commit/613753300c7705518ab1fea2f370b032851c2750), [`507ba40`](https://github.com/Effect-TS/effect/commit/507ba4060ff043c1a8d541dae723fa6940633b00), [`4064ea0`](https://github.com/Effect-TS/effect/commit/4064ea04e0b3fa23108ee471cd89ab2482b2f6e5), [`e466afe`](https://github.com/Effect-TS/effect/commit/e466afe32f2de598ceafd8982bd0cfbd388e5671), [`f373529`](https://github.com/Effect-TS/effect/commit/f373529999f4b8bc92b634f6ea14f19271388eed), [`de74eb8`](https://github.com/Effect-TS/effect/commit/de74eb80a79eebde5ff645033765e7a617e92f27), [`fa9663c`](https://github.com/Effect-TS/effect/commit/fa9663cb854ca03dba672d7857ecff84f1140c9e), [`fa9663c`](https://github.com/Effect-TS/effect/commit/fa9663cb854ca03dba672d7857ecff84f1140c9e)]: + - effect@2.4.2 + - @effect/platform@0.47.0 + +## 0.2.3 + +### Patch Changes + +- Updated dependencies [[`a4a0006`](https://github.com/Effect-TS/effect/commit/a4a0006c7f19fc261df5cda16963d73457e4d6ac), [`7535080`](https://github.com/Effect-TS/effect/commit/7535080f2e2f9859711031161600c01807cc43ea), [`0a37676`](https://github.com/Effect-TS/effect/commit/0a37676aa0eb2a21e17af2e6df9f81f52bbc8831), [`bd1d7ac`](https://github.com/Effect-TS/effect/commit/bd1d7ac75eea57a94d5e2d8e1edccb3136e84899), [`6f503b7`](https://github.com/Effect-TS/effect/commit/6f503b774d893bf2af34f66202e270d8c45d5f31)]: + - effect@2.4.1 + - @effect/platform@0.46.3 + +## 0.2.2 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform@0.46.2 + +## 0.2.1 + +### Patch Changes + +- Updated dependencies [[`aa6556f`](https://github.com/Effect-TS/effect/commit/aa6556f007117caea84d6965aa30846a11879e9d)]: + - @effect/platform@0.46.1 + +## 0.2.0 + +### Minor Changes + +- [#2101](https://github.com/Effect-TS/effect/pull/2101) [`a025b12`](https://github.com/Effect-TS/effect/commit/a025b121235ba01cfce8d62a775491880c575561) Thanks [@github-actions](https://github.com/apps/github-actions)! - Swap type params of Either from `Either` to `Either`. + + Along the same line of the other changes this allows to shorten the most common types such as: + + ```ts + import { Either } from "effect" + + const right: Either.Either = Either.right("ok") + ``` + +### Patch Changes + +- Updated dependencies [[`5de7be5`](https://github.com/Effect-TS/effect/commit/5de7be5beca2e963b503e6029dcc3217848187d2), [`489fcf3`](https://github.com/Effect-TS/effect/commit/489fcf363ff2b2a953166b740cb9a62d7fc2a101), [`7d9c3bf`](https://github.com/Effect-TS/effect/commit/7d9c3bff6c18d451e0e4781042945ec5c7be1b9f), [`d8d278b`](https://github.com/Effect-TS/effect/commit/d8d278b2efb2966947029885e01f7b68348a021f), [`14c5711`](https://github.com/Effect-TS/effect/commit/14c57110078f0862b8da5c7a2c5d980f54447484), [`5de7be5`](https://github.com/Effect-TS/effect/commit/5de7be5beca2e963b503e6029dcc3217848187d2), [`b9cb3a9`](https://github.com/Effect-TS/effect/commit/b9cb3a9c9bfdd75536bd70b4e8b557c12d4923ff), [`585fcce`](https://github.com/Effect-TS/effect/commit/585fcce162d0f07a48d7cd984a9b722966fbebbe), [`93b412d`](https://github.com/Effect-TS/effect/commit/93b412d4a9ed762dc9fa5807e51fad0fc78a614a), [`55b26a6`](https://github.com/Effect-TS/effect/commit/55b26a6342b4826f1116e7a1eb660118c274458e), [`a025b12`](https://github.com/Effect-TS/effect/commit/a025b121235ba01cfce8d62a775491880c575561), [`2097739`](https://github.com/Effect-TS/effect/commit/20977393d2383bff709304e81ec7d51cafd57108)]: + - effect@2.4.0 + - @effect/platform@0.46.0 + +## 0.1.14 + +### Patch Changes + +- [#2187](https://github.com/Effect-TS/effect/pull/2187) [`e6d36c0`](https://github.com/Effect-TS/effect/commit/e6d36c0813d836f17eabb6a9c7849baffca12dbf) Thanks [@tim-smart](https://github.com/tim-smart)! - update development dependencies + +- Updated dependencies [[`5ad2eec`](https://github.com/Effect-TS/effect/commit/5ad2eece0280b6db6a749d25cac1dcf6d33659a9), [`e6d36c0`](https://github.com/Effect-TS/effect/commit/e6d36c0813d836f17eabb6a9c7849baffca12dbf)]: + - effect@2.3.8 + - @effect/platform@0.45.6 + +## 0.1.13 + +### Patch Changes + +- Updated dependencies [[`6daf084`](https://github.com/Effect-TS/effect/commit/6daf0845de008772011db8d7c75b7c37a6b4d334)]: + - @effect/platform@0.45.5 + +## 0.1.12 + +### Patch Changes + +- Updated dependencies [[`bc8404d`](https://github.com/Effect-TS/effect/commit/bc8404d54fd42072d200c0399cb39672837afa9f), [`abcb7d9`](https://github.com/Effect-TS/effect/commit/abcb7d983a4a85b43b7175e952f5b331b9019aea), [`2c5cbcd`](https://github.com/Effect-TS/effect/commit/2c5cbcd1161b4f40dab184999291e817314107de), [`6565916`](https://github.com/Effect-TS/effect/commit/6565916ef254bf910e47d25fd0ef55e7cb420241), [`abcb7d9`](https://github.com/Effect-TS/effect/commit/abcb7d983a4a85b43b7175e952f5b331b9019aea)]: + - effect@2.3.7 + - @effect/platform@0.45.4 + +## 0.1.11 + +### Patch Changes + +- Updated dependencies [[`09532a8`](https://github.com/Effect-TS/effect/commit/09532a86b7d0cc23557c89158f0342753dfce4b0)]: + - @effect/platform@0.45.3 + +## 0.1.10 + +### Patch Changes + +- Updated dependencies [[`b1163b2`](https://github.com/Effect-TS/effect/commit/b1163b2bd67b65bafbbb39fc4c67576e5cbaf444), [`b46b869`](https://github.com/Effect-TS/effect/commit/b46b869e59a6da5aa235a9fcc25e1e0d24e9e8f8), [`de1b226`](https://github.com/Effect-TS/effect/commit/de1b226282b5ab6c2809dd93f3bdb066f24a1333), [`a663390`](https://github.com/Effect-TS/effect/commit/a66339090ae7b960f8a8b90a0dcdc505de5aaf3e), [`ff88f80`](https://github.com/Effect-TS/effect/commit/ff88f808c4ed9947a148045849e7410b00acad0a), [`11be07b`](https://github.com/Effect-TS/effect/commit/11be07bf65d82cfdf994cdb9d8ca937f995cb4f0), [`c568645`](https://github.com/Effect-TS/effect/commit/c5686451c87d26382135a1c63b00ef171bb24f62), [`88835e5`](https://github.com/Effect-TS/effect/commit/88835e575a0bfbeff9a3696a332f32192c940e12), [`b415577`](https://github.com/Effect-TS/effect/commit/b415577f6c576073733929c858e5aac27b6d5880), [`ff8046f`](https://github.com/Effect-TS/effect/commit/ff8046f57dfd073eba60ce6d3144ab060fbf93ce), [`44c3b43`](https://github.com/Effect-TS/effect/commit/44c3b43653e64d7e425d39815d8ff405acec9b99)]: + - effect@2.3.6 + - @effect/platform@0.45.2 + +## 0.1.9 + +### Patch Changes + +- Updated dependencies [[`65895ab`](https://github.com/Effect-TS/effect/commit/65895ab982e0917ac92f0827e387e7cf61be1e69)]: + - @effect/platform@0.45.1 + +## 0.1.8 + +### Patch Changes + +- Updated dependencies [[`2b62548`](https://github.com/Effect-TS/effect/commit/2b6254845882f399636d24223c483e5489e3cff4)]: + - @effect/platform@0.45.0 + +## 0.1.7 + +### Patch Changes + +- Updated dependencies [[`b881365`](https://github.com/Effect-TS/effect/commit/b8813650355322ea2fc1fbaa4f846bd87a7a05f3)]: + - effect@2.3.5 + - @effect/platform@0.44.7 + +## 0.1.6 + +### Patch Changes + +- Updated dependencies [[`17bda66`](https://github.com/Effect-TS/effect/commit/17bda66431c999a546920c10adb205e6c8bea7d1)]: + - effect@2.3.4 + - @effect/platform@0.44.6 + +## 0.1.5 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform@0.44.5 + +## 0.1.4 + +### Patch Changes + +- Updated dependencies [[`efd41d8`](https://github.com/Effect-TS/effect/commit/efd41d8131c3d90867608969ef7c4eef490eb5e6), [`0f83515`](https://github.com/Effect-TS/effect/commit/0f83515a9c01d13c7c15a3f026e02d22c3c6bb7f), [`0f83515`](https://github.com/Effect-TS/effect/commit/0f83515a9c01d13c7c15a3f026e02d22c3c6bb7f)]: + - effect@2.3.3 + - @effect/platform@0.44.4 + +## 0.1.3 + +### Patch Changes + +- Updated dependencies [[`6654f5f`](https://github.com/Effect-TS/effect/commit/6654f5f0f6b9d97165ede5e04ca16776e2599328), [`2eb11b4`](https://github.com/Effect-TS/effect/commit/2eb11b47752cedf233ef4c4395d9c4efc9b9e180), [`56c09bd`](https://github.com/Effect-TS/effect/commit/56c09bd369279a6a7785209d172739935818cba6), [`71aa5b1`](https://github.com/Effect-TS/effect/commit/71aa5b1c180dcb8b53aefe232d12a97bd06b5447), [`1700af8`](https://github.com/Effect-TS/effect/commit/1700af8af1131602887da721914c8562b6342393)]: + - effect@2.3.2 + - @effect/platform@0.44.3 + +## 0.1.2 + +### Patch Changes + +- Updated dependencies [[`29739dd`](https://github.com/Effect-TS/effect/commit/29739dde8e6232824d49c4c7f8856de245249c5c)]: + - @effect/platform@0.44.2 + +## 0.1.1 + +### Patch Changes + +- Updated dependencies [[`b5a8215`](https://github.com/Effect-TS/effect/commit/b5a8215ee2a97a8865d69ee55ce1b9835948c922)]: + - effect@2.3.1 + - @effect/platform@0.44.1 + +## 0.1.0 + +### Minor Changes + +- [#2006](https://github.com/Effect-TS/effect/pull/2006) [`9a2d1c1`](https://github.com/Effect-TS/effect/commit/9a2d1c1468ea0789b34767ad683da074f061ea9c) Thanks [@github-actions](https://github.com/apps/github-actions)! - With this change we now require a string key to be provided for all tags and renames the dear old `Tag` to `GenericTag`, so when previously you could do: + + ```ts + import { Effect, Context } from "effect" + interface Service { + readonly _: unique symbol + } + const Service = Context.Tag< + Service, + { + number: Effect.Effect + } + >() + ``` + + you are now mandated to do: + + ```ts + import { Effect, Context } from "effect" + interface Service { + readonly _: unique symbol + } + const Service = Context.GenericTag< + Service, + { + number: Effect.Effect + } + >("Service") + ``` + + This makes by default all tags globals and ensures better debuggaility when unexpected errors arise. + + Furthermore we introduce a new way of constructing tags that should be considered the new default: + + ```ts + import { Effect, Context } from "effect" + class Service extends Context.Tag("Service")< + Service, + { + number: Effect.Effect + } + >() {} + + const program = Effect.flatMap(Service, ({ number }) => number).pipe( + Effect.flatMap((_) => Effect.log(`number: ${_}`)) + ) + ``` + + this will use "Service" as the key and will create automatically an opaque identifier (the class) to be used at the type level, it does something similar to the above in a single shot. + +- [#2006](https://github.com/Effect-TS/effect/pull/2006) [`1a77f72`](https://github.com/Effect-TS/effect/commit/1a77f72cdaf43d6cdc91b6060f82832edcdbbcb3) Thanks [@github-actions](https://github.com/apps/github-actions)! - change `Effect` type parameters order from `Effect` to `Effect` + +- [#2006](https://github.com/Effect-TS/effect/pull/2006) [`b1e2086`](https://github.com/Effect-TS/effect/commit/b1e2086ea8bf410e4ad75d71c0760825924e8f9f) Thanks [@github-actions](https://github.com/apps/github-actions)! - remove re-exports from platform packages + +- [#2006](https://github.com/Effect-TS/effect/pull/2006) [`9a2d1c1`](https://github.com/Effect-TS/effect/commit/9a2d1c1468ea0789b34767ad683da074f061ea9c) Thanks [@github-actions](https://github.com/apps/github-actions)! - This change enables `Effect.serviceConstants` and `Effect.serviceMembers` to access any constant in the service, not only the effects, namely it is now possible to do: + + ```ts + import { Effect, Context } from "effect" + + class NumberRepo extends Context.TagClass("NumberRepo")< + NumberRepo, + { + readonly numbers: Array + } + >() { + static numbers = Effect.serviceConstants(NumberRepo).numbers + } + ``` + +### Patch Changes + +- Updated dependencies [[`96bcee2`](https://github.com/Effect-TS/effect/commit/96bcee21021aecd8ffd86440a2c9be353c4668e3), [`b1e2086`](https://github.com/Effect-TS/effect/commit/b1e2086ea8bf410e4ad75d71c0760825924e8f9f), [`96bcee2`](https://github.com/Effect-TS/effect/commit/96bcee21021aecd8ffd86440a2c9be353c4668e3), [`c77f635`](https://github.com/Effect-TS/effect/commit/c77f635f8a26ca6d83cb569d911f8eee79033fd9), [`e343a74`](https://github.com/Effect-TS/effect/commit/e343a74843dd9edf879417fa94cb51de7ed5b402), [`acf1894`](https://github.com/Effect-TS/effect/commit/acf1894f45945dbe5c39451e36aabb4b5092f257), [`9a2d1c1`](https://github.com/Effect-TS/effect/commit/9a2d1c1468ea0789b34767ad683da074f061ea9c), [`1a77f72`](https://github.com/Effect-TS/effect/commit/1a77f72cdaf43d6cdc91b6060f82832edcdbbcb3), [`af47aa3`](https://github.com/Effect-TS/effect/commit/af47aa37196ad542c9c23a4896d8ef98147e1205), [`c986f0e`](https://github.com/Effect-TS/effect/commit/c986f0e0ce4d22ba08177ed351152718479ab63c), [`96bcee2`](https://github.com/Effect-TS/effect/commit/96bcee21021aecd8ffd86440a2c9be353c4668e3), [`70dde23`](https://github.com/Effect-TS/effect/commit/70dde238f81125e353fd7bde5fc24ecd8969bf97), [`a34dbdc`](https://github.com/Effect-TS/effect/commit/a34dbdc1552c73c1b612676f262a0c735ce444a7), [`81b7425`](https://github.com/Effect-TS/effect/commit/81b7425320cbbe2a6cf547a3e3ab3549cdba14cf), [`b1e2086`](https://github.com/Effect-TS/effect/commit/b1e2086ea8bf410e4ad75d71c0760825924e8f9f), [`02c3461`](https://github.com/Effect-TS/effect/commit/02c34615d02f91269ea04036d0306fccf4e39e18), [`0e56e99`](https://github.com/Effect-TS/effect/commit/0e56e998ab9815c4d096c239a553cb86a0f99af9), [`8b0ded9`](https://github.com/Effect-TS/effect/commit/8b0ded9f10ba0d96fcb9af24eff2dbd9341f85e3), [`8dd83e8`](https://github.com/Effect-TS/effect/commit/8dd83e854bfcaa6dab876994c5f813dcfb486c28), [`5127afe`](https://github.com/Effect-TS/effect/commit/5127afec1c519e0a3d7460844a9101a96272f29e), [`d75f6fe`](https://github.com/Effect-TS/effect/commit/d75f6fe6499deb0a5ee9ec94af3b5fd4eb03a2d0), [`7356e5c`](https://github.com/Effect-TS/effect/commit/7356e5cc16e9d70f18c02dee1dcb4ad539fd130a), [`3077cde`](https://github.com/Effect-TS/effect/commit/3077cde08a60246821a940964a84dd7f7c8b9f54), [`be19ce0`](https://github.com/Effect-TS/effect/commit/be19ce0b8bdf1fac80bb8d7e0b06a86986b47409), [`4a5d01a`](https://github.com/Effect-TS/effect/commit/4a5d01a409e9b6dd53893e65f8e5c9247f568021), [`78f47ab`](https://github.com/Effect-TS/effect/commit/78f47abfe3cb0a8bbde818b1c5fc603270538b47), [`6361ee2`](https://github.com/Effect-TS/effect/commit/6361ee2e83bdfead24045c3d058a7298efc18113), [`52e5d20`](https://github.com/Effect-TS/effect/commit/52e5d2077582bf51f25861c7139fc920c2c24166), [`c6137ec`](https://github.com/Effect-TS/effect/commit/c6137ec62c6b5542d5062ae1a3c936cb915dee22), [`86f665d`](https://github.com/Effect-TS/effect/commit/86f665d7bd25ba0a3f046a2384798378310dcf0c), [`f5ae081`](https://github.com/Effect-TS/effect/commit/f5ae08195e68e76faeac258c565d79da4e01e7d6), [`4a5d01a`](https://github.com/Effect-TS/effect/commit/4a5d01a409e9b6dd53893e65f8e5c9247f568021), [`60686f5`](https://github.com/Effect-TS/effect/commit/60686f5c38bef1b93a3a0dda9b6596d46aceab03), [`9a2d1c1`](https://github.com/Effect-TS/effect/commit/9a2d1c1468ea0789b34767ad683da074f061ea9c), [`5127afe`](https://github.com/Effect-TS/effect/commit/5127afec1c519e0a3d7460844a9101a96272f29e), [`8ee2931`](https://github.com/Effect-TS/effect/commit/8ee293159b4f7cb7af8558287a0a047f3a69743d), [`6727474`](https://github.com/Effect-TS/effect/commit/672747497490a30d36dd49c06db19aabf09dc7f0), [`5127afe`](https://github.com/Effect-TS/effect/commit/5127afec1c519e0a3d7460844a9101a96272f29e)]: + - effect@2.3.0 + - @effect/platform@0.44.0 diff --git a/repos/effect/packages/platform-node-shared/LICENSE b/repos/effect/packages/platform-node-shared/LICENSE new file mode 100644 index 0000000..be1f5c1 --- /dev/null +++ b/repos/effect/packages/platform-node-shared/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Effectful Technologies Inc + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/repos/effect/packages/platform-node-shared/README.md b/repos/effect/packages/platform-node-shared/README.md new file mode 100644 index 0000000..e80b7b1 --- /dev/null +++ b/repos/effect/packages/platform-node-shared/README.md @@ -0,0 +1,7 @@ +# `@effect/platform-node-shared` + +Provides shared utilities and abstractions used by the [`@effect/platform-node`](https://github.com/Effect-TS/effect/tree/main/packages/platform-node) package. + +## Documentation + +- **API Reference**: [View the full documentation](https://effect-ts.github.io/effect/docs/platform-node-shared). diff --git a/repos/effect/packages/platform-node-shared/docgen.json b/repos/effect/packages/platform-node-shared/docgen.json new file mode 100644 index 0000000..da9c0ef --- /dev/null +++ b/repos/effect/packages/platform-node-shared/docgen.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../node_modules/@effect/docgen/schema.json", + "srcLink": "https://github.com/Effect-TS/effect/tree/main/packages/platform-node-shared/src/", + "exclude": [ + "src/internal/**/*.ts" + ] +} diff --git a/repos/effect/packages/platform-node-shared/package.json b/repos/effect/packages/platform-node-shared/package.json new file mode 100644 index 0000000..5425b54 --- /dev/null +++ b/repos/effect/packages/platform-node-shared/package.json @@ -0,0 +1,72 @@ +{ + "name": "@effect/platform-node-shared", + "type": "module", + "version": "0.59.0", + "license": "MIT", + "description": "Unified interfaces for common platform-specific services", + "homepage": "https://effect.website", + "repository": { + "type": "git", + "url": "https://github.com/Effect-TS/effect.git", + "directory": "packages/platform-node-shared" + }, + "bugs": { + "url": "https://github.com/Effect-TS/effect/issues" + }, + "tags": [ + "typescript", + "algebraic-data-types", + "functional-programming" + ], + "keywords": [ + "typescript", + "algebraic-data-types", + "functional-programming" + ], + "publishConfig": { + "access": "public", + "provenance": true, + "directory": "dist", + "linkDirectory": false + }, + "engines": { + "node": ">=18.0.0" + }, + "exports": { + "./package.json": "./package.json", + ".": "./src/index.ts", + "./*": "./src/*.ts", + "./internal/*": null + }, + "scripts": { + "build": "pnpm build-esm && pnpm build-annotate && pnpm build-cjs && build-utils pack-v3", + "build-esm": "tsc -b tsconfig.build.json", + "build-cjs": "babel build/esm --plugins @babel/transform-export-namespace-from --plugins @babel/transform-modules-commonjs --out-dir build/cjs --source-maps", + "build-annotate": "babel build/esm --plugins annotate-pure-calls --out-dir build/esm --source-maps", + "check": "tsc -b tsconfig.json", + "test": "vitest", + "coverage": "vitest --coverage" + }, + "dependencies": { + "@parcel/watcher": "^2.5.1", + "multipasta": "^0.2.7", + "ws": "^8.18.2" + }, + "peerDependencies": { + "@effect/cluster": "workspace:^", + "@effect/platform": "workspace:^", + "@effect/rpc": "workspace:^", + "@effect/sql": "workspace:^", + "effect": "workspace:^" + }, + "devDependencies": { + "@effect/cluster": "workspace:^", + "@effect/platform": "workspace:^", + "@effect/rpc": "workspace:^", + "@effect/sql": "workspace:^", + "@types/tar": "^6.1.12", + "@types/ws": "^8.18.1", + "effect": "workspace:^", + "tar": "^6" + } +} diff --git a/repos/effect/packages/platform-node-shared/src/NodeClusterSocket.ts b/repos/effect/packages/platform-node-shared/src/NodeClusterSocket.ts new file mode 100644 index 0000000..93dad9d --- /dev/null +++ b/repos/effect/packages/platform-node-shared/src/NodeClusterSocket.ts @@ -0,0 +1,57 @@ +/** + * @since 1.0.0 + */ +import * as Runners from "@effect/cluster/Runners" +import * as ShardingConfig from "@effect/cluster/ShardingConfig" +import { Socket } from "@effect/platform/Socket" +import type * as SocketServer from "@effect/platform/SocketServer" +import * as RpcClient from "@effect/rpc/RpcClient" +import * as RpcSerialization from "@effect/rpc/RpcSerialization" +import * as Effect from "effect/Effect" +import * as Layer from "effect/Layer" +import * as Option from "effect/Option" +import * as NodeSocket from "./NodeSocket.js" +import * as NodeSocketServer from "./NodeSocketServer.js" + +/** + * @since 1.0.0 + * @category Layers + */ +export const layerClientProtocol: Layer.Layer< + Runners.RpcClientProtocol, + never, + RpcSerialization.RpcSerialization +> = Layer.effect(Runners.RpcClientProtocol)( + Effect.gen(function*() { + const serialization = yield* RpcSerialization.RpcSerialization + return Effect.fnUntraced(function*(address) { + const socket = yield* NodeSocket.makeNet({ + openTimeout: 1000, + timeout: 5500, + host: address.host, + port: address.port + }) + return yield* RpcClient.makeProtocolSocket().pipe( + Effect.provideService(Socket, socket), + Effect.provideService(RpcSerialization.RpcSerialization, serialization) + ) + }, Effect.orDie) + }) +) + +/** + * @since 1.0.0 + * @category Layers + */ +export const layerSocketServer: Layer.Layer< + SocketServer.SocketServer, + SocketServer.SocketServerError, + ShardingConfig.ShardingConfig +> = Effect.gen(function*() { + const config = yield* ShardingConfig.ShardingConfig + const listenAddress = Option.orElse(config.runnerListenAddress, () => config.runnerAddress) + if (listenAddress._tag === "None") { + return yield* Effect.die("layerSocketServer: ShardingConfig.runnerListenAddress is None") + } + return NodeSocketServer.layer(listenAddress.value) +}).pipe(Layer.unwrapEffect) diff --git a/repos/effect/packages/platform-node-shared/src/NodeCommandExecutor.ts b/repos/effect/packages/platform-node-shared/src/NodeCommandExecutor.ts new file mode 100644 index 0000000..3007ee3 --- /dev/null +++ b/repos/effect/packages/platform-node-shared/src/NodeCommandExecutor.ts @@ -0,0 +1,13 @@ +/** + * @since 1.0.0 + */ +import type { CommandExecutor } from "@effect/platform/CommandExecutor" +import type { FileSystem } from "@effect/platform/FileSystem" +import type { Layer } from "effect/Layer" +import * as internal from "./internal/commandExecutor.js" + +/** + * @since 1.0.0 + * @category layer + */ +export const layer: Layer = internal.layer diff --git a/repos/effect/packages/platform-node-shared/src/NodeFileSystem.ts b/repos/effect/packages/platform-node-shared/src/NodeFileSystem.ts new file mode 100644 index 0000000..3bbb9d4 --- /dev/null +++ b/repos/effect/packages/platform-node-shared/src/NodeFileSystem.ts @@ -0,0 +1,13 @@ +/** + * @since 1.0.0 + */ + +import type { FileSystem } from "@effect/platform/FileSystem" +import type { Layer } from "effect/Layer" +import * as internal from "./internal/fileSystem.js" + +/** + * @since 1.0.0 + * @category layer + */ +export const layer: Layer = internal.layer diff --git a/repos/effect/packages/platform-node-shared/src/NodeFileSystem/ParcelWatcher.ts b/repos/effect/packages/platform-node-shared/src/NodeFileSystem/ParcelWatcher.ts new file mode 100644 index 0000000..da39ea3 --- /dev/null +++ b/repos/effect/packages/platform-node-shared/src/NodeFileSystem/ParcelWatcher.ts @@ -0,0 +1,15 @@ +/** + * @since 1.0.0 + */ + +import type { WatchBackend } from "@effect/platform/FileSystem" +import type { Layer } from "effect/Layer" +import * as internal from "../internal/fileSystem/parcelWatcher.js" + +/** + * You can provide this Layer to use `@parcel/watcher` as the backend for watching files. + * + * @since 1.0.0 + * @category layer + */ +export const layer: Layer = internal.layer diff --git a/repos/effect/packages/platform-node-shared/src/NodeKeyValueStore.ts b/repos/effect/packages/platform-node-shared/src/NodeKeyValueStore.ts new file mode 100644 index 0000000..a8c5d81 --- /dev/null +++ b/repos/effect/packages/platform-node-shared/src/NodeKeyValueStore.ts @@ -0,0 +1,20 @@ +/** + * @since 1.0.0 + */ +import type * as PlatformError from "@effect/platform/Error" +import * as KeyValueStore from "@effect/platform/KeyValueStore" +import * as Layer from "effect/Layer" +import * as FileSystem from "./NodeFileSystem.js" +import * as Path from "./NodePath.js" + +/** + * @since 1.0.0 + * @category layers + */ +export const layerFileSystem: ( + directory: string +) => Layer.Layer = (directory: string) => + Layer.provide( + KeyValueStore.layerFileSystem(directory), + Layer.merge(FileSystem.layer, Path.layer) + ) diff --git a/repos/effect/packages/platform-node-shared/src/NodeMultipart.ts b/repos/effect/packages/platform-node-shared/src/NodeMultipart.ts new file mode 100644 index 0000000..c7df468 --- /dev/null +++ b/repos/effect/packages/platform-node-shared/src/NodeMultipart.ts @@ -0,0 +1,40 @@ +/** + * @since 1.0.0 + */ +import type * as FileSystem from "@effect/platform/FileSystem" +import type * as Multipart from "@effect/platform/Multipart" +import type * as Path from "@effect/platform/Path" +import type * as Effect from "effect/Effect" +import type * as Scope from "effect/Scope" +import type * as Stream from "effect/Stream" +import type { IncomingHttpHeaders } from "node:http" +import type { Readable } from "node:stream" +import * as internal from "./internal/multipart.js" + +/** + * @since 1.0.0 + * @category constructors + */ +export const stream: ( + source: Readable, + headers: IncomingHttpHeaders +) => Stream.Stream = internal.stream + +/** + * @since 1.0.0 + * @category constructors + */ +export const persisted: ( + source: Readable, + headers: IncomingHttpHeaders +) => Effect.Effect< + Multipart.Persisted, + Multipart.MultipartError, + FileSystem.FileSystem | Path.Path | Scope.Scope +> = internal.persisted + +/** + * @since 1.0.0 + * @category conversions + */ +export const fileToReadable: (file: Multipart.File) => Readable = internal.fileToReadable diff --git a/repos/effect/packages/platform-node-shared/src/NodePath.ts b/repos/effect/packages/platform-node-shared/src/NodePath.ts new file mode 100644 index 0000000..464ac6c --- /dev/null +++ b/repos/effect/packages/platform-node-shared/src/NodePath.ts @@ -0,0 +1,25 @@ +/** + * @since 1.0.0 + */ + +import type { Path } from "@effect/platform/Path" +import type { Layer } from "effect/Layer" +import * as internal from "./internal/path.js" + +/** + * @since 1.0.0 + * @category layer + */ +export const layer: Layer = internal.layer + +/** + * @since 1.0.0 + * @category layer + */ +export const layerPosix: Layer = internal.layerPosix + +/** + * @since 1.0.0 + * @category layer + */ +export const layerWin32: Layer = internal.layerWin32 diff --git a/repos/effect/packages/platform-node-shared/src/NodeRuntime.ts b/repos/effect/packages/platform-node-shared/src/NodeRuntime.ts new file mode 100644 index 0000000..2729356 --- /dev/null +++ b/repos/effect/packages/platform-node-shared/src/NodeRuntime.ts @@ -0,0 +1,11 @@ +/** + * @since 1.0.0 + */ +import type { RunMain } from "@effect/platform/Runtime" +import * as internal from "./internal/runtime.js" + +/** + * @since 1.0.0 + * @category runtime + */ +export const runMain: RunMain = internal.runMain diff --git a/repos/effect/packages/platform-node-shared/src/NodeSink.ts b/repos/effect/packages/platform-node-shared/src/NodeSink.ts new file mode 100644 index 0000000..4b4cdb3 --- /dev/null +++ b/repos/effect/packages/platform-node-shared/src/NodeSink.ts @@ -0,0 +1,77 @@ +/** + * @since 1.0.0 + */ +import type { PlatformError } from "@effect/platform/Error" +import { SystemError } from "@effect/platform/Error" +import type { Channel } from "effect/Channel" +import type { Chunk } from "effect/Chunk" +import type { LazyArg } from "effect/Function" +import type * as Sink from "effect/Sink" +import type { Writable } from "stream" +import * as internal from "./internal/sink.js" +import type { FromWritableOptions } from "./NodeStream.js" + +/** + * @category constructor + * @since 1.0.0 + */ +export const fromWritable: ( + evaluate: LazyArg, + onError: (error: unknown) => E, + options?: FromWritableOptions +) => Sink.Sink = internal.fromWritable + +/** + * @category constructor + * @since 1.0.0 + */ +export const fromWritableChannel: ( + writable: LazyArg, + onError: (error: unknown) => OE, + options?: FromWritableOptions +) => Channel, Chunk, IE | OE, IE, void, unknown> = internal.fromWritableChannel + +/** + * @category stdio + * @since 1.0.0 + */ +export const stdout: Sink.Sink = fromWritable( + () => process.stdout, + (cause) => + new SystemError({ + module: "Stream", + method: "stdout", + reason: "Unknown", + cause + }) +) + +/** + * @category stdio + * @since 1.0.0 + */ +export const stderr: Sink.Sink = fromWritable( + () => process.stderr, + (cause) => + new SystemError({ + module: "Stream", + method: "stderr", + reason: "Unknown", + cause + }) +) + +/** + * @category stdio + * @since 1.0.0 + */ +export const stdin: Sink.Sink = fromWritable( + () => process.stdin, + (cause) => + new SystemError({ + module: "Stream", + method: "stdin", + reason: "Unknown", + cause + }) +) diff --git a/repos/effect/packages/platform-node-shared/src/NodeSocket.ts b/repos/effect/packages/platform-node-shared/src/NodeSocket.ts new file mode 100644 index 0000000..15e0a2c --- /dev/null +++ b/repos/effect/packages/platform-node-shared/src/NodeSocket.ts @@ -0,0 +1,222 @@ +/** + * @since 1.0.0 + */ +import * as Socket from "@effect/platform/Socket" +import * as Channel from "effect/Channel" +import type * as Chunk from "effect/Chunk" +import * as Context from "effect/Context" +import * as Deferred from "effect/Deferred" +import type * as Duration from "effect/Duration" +import * as Effect from "effect/Effect" +import * as FiberSet from "effect/FiberSet" +import { identity } from "effect/Function" +import * as Layer from "effect/Layer" +import * as Scope from "effect/Scope" +import * as Net from "node:net" +import type { Duplex } from "node:stream" + +/** + * @since 1.0.0 + * @category tags + */ +export interface NetSocket { + readonly _: unique symbol +} + +/** + * @since 1.0.0 + * @category tags + */ +export const NetSocket: Context.Tag = Context.GenericTag( + "@effect/platform-node/NodeSocket/NetSocket" +) + +/** + * @since 1.0.0 + * @category constructors + */ +export const makeNet = ( + options: Net.NetConnectOpts & { + readonly openTimeout?: Duration.DurationInput | undefined + } +): Effect.Effect => + fromDuplex( + Effect.scopeWith((scope) => { + let conn: Net.Socket | undefined + return Effect.flatMap( + Scope.addFinalizer( + scope, + Effect.sync(() => { + if (!conn) return + if (conn.closed === false) { + if ("destroySoon" in conn) { + conn.destroySoon() + } else { + ;(conn as Net.Socket).destroy() + } + } + }) + ), + () => + Effect.async((resume) => { + conn = Net.createConnection(options) + conn.once("connect", () => { + resume(Effect.succeed(conn!)) + }) + conn.on("error", (cause) => { + resume(Effect.fail(new Socket.SocketGenericError({ reason: "Open", cause }))) + }) + }) + ) + }), + options + ) + +/** + * @since 1.0.0 + * @category constructors + */ +export const fromDuplex = ( + open: Effect.Effect, + options?: { + readonly openTimeout?: Duration.DurationInput | undefined + } +): Effect.Effect> => + Effect.withFiberRuntime>((fiber) => { + let currentSocket: Duplex | undefined + const latch = Effect.unsafeMakeLatch(false) + const openContext = fiber.currentContext as Context.Context + const run = (handler: (_: Uint8Array) => Effect.Effect<_, E, R> | void, opts?: { + readonly onOpen?: Effect.Effect | undefined + }) => + Effect.scopedWith(Effect.fnUntraced(function*(scope) { + const fiberSet = yield* FiberSet.make().pipe( + Scope.extend(scope) + ) + + let conn: Duplex | undefined = undefined + yield* Scope.addFinalizer( + scope, + Effect.sync(() => { + if (!conn) return + conn.off("data", onData) + conn.off("end", onEnd) + conn.off("error", onError) + conn.off("close", onClose) + }) + ) + + conn = yield* Scope.extend(open, scope).pipe( + options?.openTimeout ? + Effect.timeoutFail({ + duration: options.openTimeout, + onTimeout: () => + new Socket.SocketGenericError({ reason: "Open", cause: new Error("Connection timed out") }) + }) : + identity + ) + conn.on("end", onEnd) + conn.on("error", onError) + conn.on("close", onClose) + + const run = yield* Effect.provideService(FiberSet.runtime(fiberSet)(), NetSocket, conn as Net.Socket) + conn.on("data", onData) + + currentSocket = conn + yield* latch.open + if (opts?.onOpen) yield* opts.onOpen + + return yield* FiberSet.join(fiberSet) + + function onData(chunk: Uint8Array) { + const result = handler(chunk) + if (Effect.isEffect(result)) { + run(result) + } + } + function onEnd() { + Deferred.unsafeDone(fiberSet.deferred, Effect.void) + } + function onError(cause: Error) { + Deferred.unsafeDone( + fiberSet.deferred, + Effect.fail(new Socket.SocketGenericError({ reason: "Read", cause })) + ) + } + function onClose(hadError: boolean) { + Deferred.unsafeDone( + fiberSet.deferred, + Effect.fail( + new Socket.SocketCloseError({ + reason: "Close", + code: hadError ? 1006 : 1000 + }) + ) + ) + } + })).pipe( + Effect.mapInputContext((input: Context.Context) => Context.merge(openContext, input)), + Effect.ensuring(Effect.sync(() => { + latch.unsafeClose() + currentSocket = undefined + })), + Effect.interruptible + ) + + const write = (chunk: Uint8Array | string | Socket.CloseEvent) => + latch.whenOpen(Effect.async((resume) => { + const conn = currentSocket! + if (Socket.isCloseEvent(chunk)) { + conn.destroy(chunk.code > 1000 ? new Error(`closed with code ${chunk.code}`) : undefined) + return resume(Effect.void) + } + currentSocket!.write(chunk, (cause) => { + resume( + cause + ? Effect.fail(new Socket.SocketGenericError({ reason: "Write", cause })) + : Effect.void + ) + }) + })) + + const writer = Effect.acquireRelease( + Effect.succeed(write), + () => + Effect.sync(() => { + if (!currentSocket || currentSocket.writableEnded) return + currentSocket.end() + }) + ) + + return Effect.succeed(Socket.Socket.of({ + [Socket.TypeId]: Socket.TypeId, + run, + runRaw: run, + writer + })) + }) + +/** + * @since 1.0.0 + * @category constructors + */ +export const makeNetChannel = ( + options: Net.NetConnectOpts +): Channel.Channel< + Chunk.Chunk, + Chunk.Chunk, + Socket.SocketError | IE, + IE, + void, + unknown +> => + Channel.unwrapScoped( + Effect.map(makeNet(options), Socket.toChannelWith()) + ) + +/** + * @since 1.0.0 + * @category layers + */ +export const layerNet = (options: Net.NetConnectOpts): Layer.Layer => + Layer.effect(Socket.Socket, makeNet(options)) diff --git a/repos/effect/packages/platform-node-shared/src/NodeSocketServer.ts b/repos/effect/packages/platform-node-shared/src/NodeSocketServer.ts new file mode 100644 index 0000000..b8a650c --- /dev/null +++ b/repos/effect/packages/platform-node-shared/src/NodeSocketServer.ts @@ -0,0 +1,255 @@ +/** + * @since 1.0.0 + */ +import * as Socket from "@effect/platform/Socket" +import * as SocketServer from "@effect/platform/SocketServer" +import type { Cause } from "effect/Cause" +import * as Context from "effect/Context" +import * as Deferred from "effect/Deferred" +import * as Effect from "effect/Effect" +import * as Exit from "effect/Exit" +import * as FiberRef from "effect/FiberRef" +import * as FiberSet from "effect/FiberSet" +import { pipe } from "effect/Function" +import * as Layer from "effect/Layer" +import * as Scope from "effect/Scope" +import type * as Http from "node:http" +import * as Net from "node:net" +import * as WS from "ws" +import * as NodeSocket from "./NodeSocket.js" + +/** + * @since 1.0.0 + * @category tags + */ +export class IncomingMessage extends Context.Tag("@effect/platform-node-shared/NodeSocketServer/IncomingMessage")< + IncomingMessage, + Http.IncomingMessage +>() {} + +/** + * @since 1.0.0 + * @category constructors + */ +export const make = Effect.fnUntraced(function*( + options: Net.ServerOpts & Net.ListenOptions +) { + const errorDeferred = yield* Deferred.make() + const pending: Array = [] + const defaultOnConnection = (socket: Net.Socket) => { + pending.push(socket) + } + let onConnection = defaultOnConnection + + yield* Effect.addFinalizer(() => + Effect.async((resume) => { + server.close(() => resume(Effect.void)) + }) + ) + const server = Net.createServer(options, (conn) => onConnection(conn)) + server.on("error", (cause) => Deferred.unsafeDone(errorDeferred, Exit.fail(cause))) + + yield* Effect.async((resume) => { + server.listen(options, () => { + resume(Effect.void) + }) + }).pipe( + Effect.raceFirst(Effect.mapError(Deferred.await(errorDeferred), (cause) => + new SocketServer.SocketServerError({ + reason: "Open", + cause + }))) + ) + + const run = Effect.fnUntraced(function*(handler: (socket: Socket.Socket) => Effect.Effect<_, E, R>) { + const scope = yield* Scope.make() + const fiberSet = yield* FiberSet.make().pipe( + Scope.extend(scope) + ) + const run = yield* FiberSet.runtime(fiberSet)() + function onConnection_(conn: Net.Socket) { + let error: Error | undefined + conn.on("error", (err) => { + error = err + }) + pipe( + NodeSocket.fromDuplex( + Effect.acquireRelease( + Effect.suspend((): Effect.Effect => { + if (error) { + return Effect.fail(new Socket.SocketGenericError({ reason: "Open", cause: error })) + } else if (conn.closed) { + return Effect.fail( + new Socket.SocketCloseError({ + reason: "Close", + code: 1000 + }) + ) + } + return Effect.succeed(conn) + }), + (conn) => + Effect.sync(() => { + if (conn.closed === false) { + conn.destroySoon() + } + }) + ) + ), + Effect.flatMap(handler), + Effect.catchAllCause(reportUnhandledError), + Effect.provideService(NodeSocket.NetSocket, conn), + run + ) + } + return yield* Effect.async((_resume) => { + const prev = onConnection + onConnection = onConnection_ + pending.forEach(onConnection) + pending.length = 0 + return Effect.suspend(() => { + onConnection = prev + return Scope.close(scope, Exit.void) + }) + }).pipe( + Effect.raceFirst(Effect.mapError(Deferred.await(errorDeferred), (cause) => + new SocketServer.SocketServerError({ + reason: "Unknown", + cause + }))) + ) + }) + + const address = server.address()! + return SocketServer.SocketServer.of({ + address: typeof address === "string" ? + { + _tag: "UnixAddress", + path: address + } : + { + _tag: "TcpAddress", + hostname: address.address, + port: address.port + }, + run + }) +}) + +/** + * @since 1.0.0 + * @category layers + */ +export const layer = ( + options: Net.ServerOpts & Net.ListenOptions +): Layer.Layer => + Layer.scoped( + SocketServer.SocketServer, + make(options) + ) + +/** + * @since 1.0.0 + * @category constructors + */ +export const makeWebSocket: ( + options: WS.ServerOptions +) => Effect.Effect< + SocketServer.SocketServer["Type"], + SocketServer.SocketServerError, + Scope.Scope +> = Effect.fnUntraced(function*( + options: WS.ServerOptions +) { + const server = yield* Effect.acquireRelease( + Effect.sync(() => new WS.WebSocketServer(options)), + (server) => + Effect.async((resume) => { + server.close(() => resume(Effect.void)) + }) + ) + + yield* Effect.async((resume) => { + server.once("error", (error) => { + resume(Effect.fail( + new SocketServer.SocketServerError({ + reason: "Open", + cause: error + }) + )) + }) + server.once("listening", () => { + resume(Effect.void) + }) + }) + + const run = Effect.fnUntraced(function*(handler: (socket: Socket.Socket) => Effect.Effect<_, E, R>) { + const scope = yield* Scope.make() + const fiberSet = yield* FiberSet.make().pipe( + Scope.extend(scope) + ) + const run = yield* FiberSet.runtime(fiberSet)() + function onConnection(conn: Net.Socket, req: Http.IncomingMessage) { + pipe( + Socket.fromWebSocket( + Effect.acquireRelease( + Effect.succeed(conn as unknown as globalThis.WebSocket), + (conn) => + Effect.sync(() => { + conn.close() + }) + ) + ), + Effect.flatMap(handler), + Effect.catchAllCause(reportUnhandledError), + Effect.provideService(Socket.WebSocket, conn as any), + Effect.provideService(IncomingMessage, req), + run + ) + } + return yield* Effect.async((_resume) => { + server.on("connection", onConnection) + return Effect.sync(() => { + server.off("connection", onConnection) + }) + }).pipe( + Effect.ensuring(Scope.close(scope, Exit.void)) + ) + }) + + const address = server.address()! + return SocketServer.SocketServer.of({ + address: typeof address === "string" ? + { + _tag: "UnixAddress", + path: address + } : + { + _tag: "TcpAddress", + hostname: address.address, + port: address.port + }, + run + }) +}) + +/** + * @since 1.0.0 + * @category layers + */ +export const layerWebSocket = ( + options: WS.ServerOptions +): Layer.Layer => + Layer.scoped( + SocketServer.SocketServer, + makeWebSocket(options) + ) + +const reportUnhandledError = (cause: Cause) => + Effect.withFiberRuntime((fiber) => { + const unhandledLogLevel = fiber.getFiberRef(FiberRef.unhandledErrorLogLevel) + if (unhandledLogLevel._tag === "Some") { + return Effect.logWithLevel(unhandledLogLevel.value, cause, "Unhandled error in SocketServer") + } + return Effect.void + }) diff --git a/repos/effect/packages/platform-node-shared/src/NodeStream.ts b/repos/effect/packages/platform-node-shared/src/NodeStream.ts new file mode 100644 index 0000000..6cd072c --- /dev/null +++ b/repos/effect/packages/platform-node-shared/src/NodeStream.ts @@ -0,0 +1,154 @@ +/** + * @since 1.0.0 + */ +import type { PlatformError } from "@effect/platform/Error" +import type { SizeInput } from "@effect/platform/FileSystem" +import type { Channel } from "effect/Channel" +import type { Chunk } from "effect/Chunk" +import type { Effect } from "effect/Effect" +import type { LazyArg } from "effect/Function" +import * as Stream from "effect/Stream" +import type { Duplex, Readable } from "node:stream" +import * as internal from "./internal/stream.js" + +/** + * @category models + * @since 1.0.0 + */ +export interface FromReadableOptions { + /** Defaults to undefined, which lets Node.js decide the chunk size */ + readonly chunkSize?: SizeInput + /** Default to true, which means the stream will be closed when done */ + readonly closeOnDone?: boolean | undefined +} + +/** + * @category model + * @since 1.0.0 + */ +export interface FromWritableOptions { + readonly endOnDone?: boolean + readonly encoding?: BufferEncoding +} + +/** + * @category constructors + * @since 1.0.0 + */ +export const fromReadable: >( + evaluate: LazyArg, + onError: (error: unknown) => E, + options?: FromReadableOptions +) => Stream.Stream = internal.fromReadable + +/** + * @category constructors + * @since 1.0.0 + */ +export const fromReadableChannel: >( + evaluate: LazyArg, + onError: (error: unknown) => E, + options?: FromReadableOptions | undefined +) => Channel, unknown, E> = internal.fromReadableChannel + +/** + * @category constructors + * @since 1.0.0 + */ +export const fromDuplex: ( + evaluate: LazyArg, + onError: (error: unknown) => E, + options?: FromReadableOptions & FromWritableOptions +) => Channel, Chunk, IE | E, IE, void, unknown> = internal.fromDuplex + +/** + * @category combinators + * @since 1.0.0 + */ +export const pipeThroughDuplex: { + ( + duplex: LazyArg, + onError: (error: unknown) => E2, + options?: (FromReadableOptions & FromWritableOptions) | undefined + ): (self: Stream.Stream) => Stream.Stream + ( + self: Stream.Stream, + duplex: LazyArg, + onError: (error: unknown) => E2, + options?: (FromReadableOptions & FromWritableOptions) | undefined + ): Stream.Stream +} = internal.pipeThroughDuplex + +/** + * @category combinators + * @since 1.0.0 + */ +export const pipeThroughSimple: { + ( + duplex: LazyArg + ): (self: Stream.Stream) => Stream.Stream + ( + self: Stream.Stream, + duplex: LazyArg + ): Stream.Stream +} = internal.pipeThroughSimple + +/** + * @since 1.0.0 + * @category conversions + */ +export const toReadable: (stream: Stream.Stream) => Effect = + internal.toReadable + +/** + * @since 1.0.0 + * @category conversions + */ +export const toReadableNever: (stream: Stream.Stream) => Readable = + internal.toReadableNever + +/** + * @since 1.0.0 + * @category conversions + */ +export const toString: ( + readable: LazyArg, + options: { + readonly onFailure: (error: unknown) => E + readonly encoding?: BufferEncoding | undefined + readonly maxBytes?: SizeInput | undefined + } +) => Effect = internal.toString + +/** + * @since 1.0.0 + * @category conversions + */ +export const toUint8Array: ( + readable: LazyArg, + options: { readonly onFailure: (error: unknown) => E; readonly maxBytes?: SizeInput | undefined } +) => Effect = internal.toUint8Array + +/** + * @since 1.0.0 + * @category stdio + */ +export const stdin: Stream.Stream = internal.fromReadable(() => process.stdin, (err) => err, { + closeOnDone: false +}).pipe(Stream.orDie) + +/** + * @since 1.0.0 + * @category stdio + */ +export const stdout: Stream.Stream = internal.fromReadable(() => process.stdout, (err) => err, { + closeOnDone: false +}).pipe(Stream.orDie) + +/** + * @since 1.0.0 + * @category stdio + */ +export const stderr: Stream.Stream = internal.fromReadable(() => process.stderr, (err) => err, { + closeOnDone: false +}).pipe(Stream.orDie) diff --git a/repos/effect/packages/platform-node-shared/src/NodeTerminal.ts b/repos/effect/packages/platform-node-shared/src/NodeTerminal.ts new file mode 100644 index 0000000..8a052f0 --- /dev/null +++ b/repos/effect/packages/platform-node-shared/src/NodeTerminal.ts @@ -0,0 +1,20 @@ +/** + * @since 1.0.0 + */ +import type { Terminal, UserInput } from "@effect/platform/Terminal" +import type { Effect } from "effect/Effect" +import type { Layer } from "effect/Layer" +import type { Scope } from "effect/Scope" +import * as InternalTerminal from "./internal/terminal.js" +/** + * @since 1.0.0 + * @category constructors + */ +export const make: (shouldQuit?: (input: UserInput) => boolean) => Effect = + InternalTerminal.make + +/** + * @since 1.0.0 + * @category layer + */ +export const layer: Layer = InternalTerminal.layer diff --git a/repos/effect/packages/platform-node-shared/src/internal/commandExecutor.ts b/repos/effect/packages/platform-node-shared/src/internal/commandExecutor.ts new file mode 100644 index 0000000..a0cb175 --- /dev/null +++ b/repos/effect/packages/platform-node-shared/src/internal/commandExecutor.ts @@ -0,0 +1,251 @@ +import * as Command from "@effect/platform/Command" +import * as CommandExecutor from "@effect/platform/CommandExecutor" +import type * as Error from "@effect/platform/Error" +import * as FileSystem from "@effect/platform/FileSystem" +import * as Deferred from "effect/Deferred" +import * as Effect from "effect/Effect" +import { constUndefined, identity, pipe } from "effect/Function" +import * as Inspectable from "effect/Inspectable" +import * as Layer from "effect/Layer" +import * as Option from "effect/Option" +import type * as Scope from "effect/Scope" +import * as Sink from "effect/Sink" +import * as Stream from "effect/Stream" +import * as ChildProcess from "node:child_process" +import { handleErrnoException } from "./error.js" +import { fromWritable } from "./sink.js" +import { fromReadable } from "./stream.js" + +const inputToStdioOption = (stdin: Command.Command.Input): "pipe" | "inherit" => + typeof stdin === "string" ? stdin : "pipe" + +const outputToStdioOption = (output: Command.Command.Output): "pipe" | "inherit" => + typeof output === "string" ? output : "pipe" + +const toError = (err: unknown): Error => err instanceof globalThis.Error ? err : new globalThis.Error(String(err)) + +const toPlatformError = ( + method: string, + error: NodeJS.ErrnoException, + command: Command.Command +): Error.PlatformError => { + const flattened = Command.flatten(command).reduce((acc, curr) => { + const command = `${curr.command} ${curr.args.join(" ")}` + return acc.length === 0 ? command : `${acc} | ${command}` + }, "") + return handleErrnoException("Command", method)(error, [flattened]) +} + +type ExitCode = readonly [code: number | null, signal: NodeJS.Signals | null] +type ExitCodeDeferred = Deferred.Deferred + +const ProcessProto = { + [CommandExecutor.ProcessTypeId]: CommandExecutor.ProcessTypeId, + ...Inspectable.BaseProto, + toJSON(this: CommandExecutor.Process) { + return { + _id: "@effect/platform/CommandExecutor/Process", + pid: this.pid + } + } +} + +const runCommand = + (fileSystem: FileSystem.FileSystem) => + (command: Command.Command): Effect.Effect => { + switch (command._tag) { + case "StandardCommand": { + const spawn = Effect.flatMap( + Deferred.make(), + (exitCode) => + Effect.async( + (resume) => { + const handle = ChildProcess.spawn(command.command, command.args, { + stdio: [ + inputToStdioOption(command.stdin), + outputToStdioOption(command.stdout), + outputToStdioOption(command.stderr) + ], + cwd: Option.getOrElse(command.cwd, constUndefined), + shell: command.shell, + env: { ...process.env, ...Object.fromEntries(command.env) }, + detached: process.platform !== "win32" + }) + handle.on("error", (err) => { + resume(Effect.fail(toPlatformError("spawn", err, command))) + }) + handle.on("exit", (...args) => { + Deferred.unsafeDone(exitCode, Effect.succeed(args)) + }) + handle.on("spawn", () => { + resume(Effect.succeed([handle, exitCode])) + }) + return Effect.sync(() => { + handle.kill("SIGTERM") + }) + } + ) + ) + + const killProcessGroup = process.platform === "win32" ? + (handle: ChildProcess.ChildProcess, _: NodeJS.Signals) => + Effect.async((resume) => { + ChildProcess.exec(`taskkill /pid ${handle.pid} /T /F`, (error) => { + if (error) { + resume(Effect.fail(toPlatformError("kill", toError(error), command))) + } else { + resume(Effect.void) + } + }) + }) + : (handle: ChildProcess.ChildProcess, signal: NodeJS.Signals) => + Effect.try({ + try: () => process.kill(-handle.pid!, signal), + catch: (error) => toPlatformError("kill", toError(error), command) + }) + + const killProcess = (handle: ChildProcess.ChildProcess, signal: NodeJS.Signals) => + Effect.suspend(() => + handle.kill(signal) ? Effect.void : Effect.fail( + toPlatformError("kill", new globalThis.Error("Failed to kill process"), command) + ) + ) + + return pipe( + // Validate that the directory is accessible + Option.match(command.cwd, { + onNone: () => Effect.void, + onSome: (dir) => fileSystem.access(dir) + }), + Effect.zipRight( + Effect.acquireRelease( + spawn, + ([handle, exitCode]) => + Effect.flatMap(Deferred.isDone(exitCode), (done) => { + if (!done) { + // Process is still running, kill it + return killProcessGroup(handle, "SIGTERM").pipe( + Effect.orElse(() => killProcess(handle, "SIGTERM")), + Effect.zipRight(Deferred.await(exitCode)), + Effect.ignore + ) + } + + // Process has already exited, check if we need to clean up children + return Effect.flatMap(Deferred.await(exitCode), ([code]) => { + if (code !== 0 && code !== null) { + // Non-zero exit code, attempt to clean up process group + return killProcessGroup(handle, "SIGTERM").pipe(Effect.ignore) + } + + return Effect.void + }) + }) + ) + ), + Effect.map(([handle, exitCodeDeferred]): CommandExecutor.Process => { + let stdin: Sink.Sink = Sink.drain + + if (handle.stdin !== null) { + stdin = fromWritable( + () => handle.stdin!, + (err) => toPlatformError("toWritable", toError(err), command) + ) + } + + const exitCode: CommandExecutor.Process["exitCode"] = Effect.flatMap( + Deferred.await(exitCodeDeferred), + ([code, signal]) => { + if (code !== null) { + return Effect.succeed(CommandExecutor.ExitCode(code)) + } + // If code is `null`, then `signal` must be defined. See the NodeJS + // documentation for the `"exit"` event on a `child_process`. + // https://nodejs.org/api/child_process.html#child_process_event_exit + return Effect.fail( + toPlatformError( + "exitCode", + new globalThis.Error(`Process interrupted due to receipt of signal: ${signal}`), + command + ) + ) + } + ) + + const isRunning = Effect.negate(Deferred.isDone(exitCodeDeferred)) + + const kill: CommandExecutor.Process["kill"] = (signal = "SIGTERM") => + killProcessGroup(handle, signal).pipe( + Effect.orElse(() => killProcess(handle, signal)), + Effect.zipRight(Effect.asVoid(Deferred.await(exitCodeDeferred))) + ) + + const pid = CommandExecutor.ProcessId(handle.pid!) + const stderr = fromReadable( + () => handle.stderr!, + (err) => toPlatformError("fromReadable(stderr)", toError(err), command) + ) + let stdout: Stream.Stream = fromReadable< + Error.PlatformError, + Uint8Array + >( + () => handle.stdout!, + (err) => toPlatformError("fromReadable(stdout)", toError(err), command) + ) + // TODO: add Sink.isSink + if (typeof command.stdout !== "string") { + stdout = Stream.transduce(stdout, command.stdout) + } + return Object.assign(Object.create(ProcessProto), { + pid, + exitCode, + isRunning, + kill, + stdin, + stderr, + stdout + }) + }), + typeof command.stdin === "string" + ? identity + : Effect.tap((process) => + Effect.forkDaemon(Stream.run(command.stdin as Stream.Stream, process.stdin)) + ) + ) + } + case "PipedCommand": { + const flattened = Command.flatten(command) + if (flattened.length === 1) { + return pipe(flattened[0], runCommand(fileSystem)) + } + const head = flattened[0] + const tail = flattened.slice(1) + const initial = tail.slice(0, tail.length - 1) + const last = tail[tail.length - 1] + const stream = initial.reduce( + (stdin, command) => + pipe( + Command.stdin(command, stdin), + runCommand(fileSystem), + Effect.map((process) => process.stdout), + Stream.unwrapScoped + ), + pipe( + runCommand(fileSystem)(head), + Effect.map((process) => process.stdout), + Stream.unwrapScoped + ) + ) + return pipe(Command.stdin(last, stream), runCommand(fileSystem)) + } + } + } + +/** @internal */ +export const layer: Layer.Layer = Layer.effect( + CommandExecutor.CommandExecutor, + pipe( + FileSystem.FileSystem, + Effect.map((fileSystem) => CommandExecutor.makeExecutor(runCommand(fileSystem))) + ) +) diff --git a/repos/effect/packages/platform-node-shared/src/internal/error.ts b/repos/effect/packages/platform-node-shared/src/internal/error.ts new file mode 100644 index 0000000..5b6d814 --- /dev/null +++ b/repos/effect/packages/platform-node-shared/src/internal/error.ts @@ -0,0 +1,52 @@ +import type { PlatformError, SystemErrorReason } from "@effect/platform/Error" +import { SystemError } from "@effect/platform/Error" +import type { PathLike } from "node:fs" + +/** @internal */ +export const handleErrnoException = (module: SystemError["module"], method: string) => +( + err: NodeJS.ErrnoException, + [path]: [path: PathLike | number, ...args: Array] +): PlatformError => { + let reason: SystemErrorReason = "Unknown" + + switch (err.code) { + case "ENOENT": + reason = "NotFound" + break + + case "EACCES": + reason = "PermissionDenied" + break + + case "EEXIST": + reason = "AlreadyExists" + break + + case "EISDIR": + reason = "BadResource" + break + + case "ENOTDIR": + reason = "BadResource" + break + + case "EBUSY": + reason = "Busy" + break + + case "ELOOP": + reason = "BadResource" + break + } + + return new SystemError({ + reason, + module, + method, + pathOrDescriptor: path as string | number, + syscall: err.syscall, + description: err.message, + cause: err + }) +} diff --git a/repos/effect/packages/platform-node-shared/src/internal/fileSystem.ts b/repos/effect/packages/platform-node-shared/src/internal/fileSystem.ts new file mode 100644 index 0000000..9b4797a --- /dev/null +++ b/repos/effect/packages/platform-node-shared/src/internal/fileSystem.ts @@ -0,0 +1,648 @@ +import { effectify } from "@effect/platform/Effectify" +import * as Error from "@effect/platform/Error" +import * as FileSystem from "@effect/platform/FileSystem" +import type * as Context from "effect/Context" +import * as Effect from "effect/Effect" +import { pipe } from "effect/Function" +import * as Layer from "effect/Layer" +import * as Option from "effect/Option" +import * as Stream from "effect/Stream" +import * as Crypto from "node:crypto" +import * as NFS from "node:fs" +import * as OS from "node:os" +import * as Path from "node:path" +import { handleErrnoException } from "./error.js" + +const handleBadArgument = (method: string) => (cause: unknown) => + new Error.BadArgument({ + module: "FileSystem", + method, + cause + }) + +// == access + +const access = (() => { + const nodeAccess = effectify( + NFS.access, + handleErrnoException("FileSystem", "access"), + handleBadArgument("access") + ) + return (path: string, options?: FileSystem.AccessFileOptions) => { + let mode = NFS.constants.F_OK + if (options?.readable) { + mode |= NFS.constants.R_OK + } + if (options?.writable) { + mode |= NFS.constants.W_OK + } + return nodeAccess(path, mode) + } +})() + +// == copy + +const copy = (() => { + const nodeCp = effectify( + NFS.cp, + handleErrnoException("FileSystem", "copy"), + handleBadArgument("copy") + ) + return (fromPath: string, toPath: string, options?: FileSystem.CopyOptions) => + nodeCp(fromPath, toPath, { + force: options?.overwrite ?? false, + preserveTimestamps: options?.preserveTimestamps ?? false, + recursive: true + }) +})() + +// == copyFile + +const copyFile = (() => { + const nodeCopyFile = effectify( + NFS.copyFile, + handleErrnoException("FileSystem", "copyFile"), + handleBadArgument("copyFile") + ) + return (fromPath: string, toPath: string) => nodeCopyFile(fromPath, toPath) +})() + +// == chmod + +const chmod = (() => { + const nodeChmod = effectify( + NFS.chmod, + handleErrnoException("FileSystem", "chmod"), + handleBadArgument("chmod") + ) + return (path: string, mode: number) => nodeChmod(path, mode) +})() + +// == chown + +const chown = (() => { + const nodeChown = effectify( + NFS.chown, + handleErrnoException("FileSystem", "chown"), + handleBadArgument("chown") + ) + return (path: string, uid: number, gid: number) => nodeChown(path, uid, gid) +})() + +// == link + +const link = (() => { + const nodeLink = effectify( + NFS.link, + handleErrnoException("FileSystem", "link"), + handleBadArgument("link") + ) + return (existingPath: string, newPath: string) => nodeLink(existingPath, newPath) +})() + +// == makeDirectory + +const makeDirectory = (() => { + const nodeMkdir = effectify( + NFS.mkdir, + handleErrnoException("FileSystem", "makeDirectory"), + handleBadArgument("makeDirectory") + ) + return (path: string, options?: FileSystem.MakeDirectoryOptions) => + nodeMkdir(path, { + recursive: options?.recursive ?? false, + mode: options?.mode + }) +})() + +// == makeTempDirectory + +const makeTempDirectoryFactory = (method: string) => { + const nodeMkdtemp = effectify( + NFS.mkdtemp, + handleErrnoException("FileSystem", method), + handleBadArgument(method) + ) + return (options?: FileSystem.MakeTempDirectoryOptions) => + Effect.suspend(() => { + const prefix = options?.prefix ?? "" + const directory = typeof options?.directory === "string" + ? Path.join(options.directory, ".") + : OS.tmpdir() + + return nodeMkdtemp(prefix ? Path.join(directory, prefix) : directory + "/") + }) +} +const makeTempDirectory = makeTempDirectoryFactory("makeTempDirectory") + +// == remove + +const removeFactory = (method: string) => { + const nodeRm = effectify( + NFS.rm, + handleErrnoException("FileSystem", method), + handleBadArgument(method) + ) + return (path: string, options?: FileSystem.RemoveOptions) => + nodeRm( + path, + { recursive: options?.recursive ?? false, force: options?.force ?? false } + ) +} +const remove = removeFactory("remove") + +// == makeTempDirectoryScoped + +const makeTempDirectoryScoped = (() => { + const makeDirectory = makeTempDirectoryFactory("makeTempDirectoryScoped") + const removeDirectory = removeFactory("makeTempDirectoryScoped") + return ( + options?: FileSystem.MakeTempDirectoryOptions + ) => + Effect.acquireRelease( + makeDirectory(options), + (directory) => Effect.orDie(removeDirectory(directory, { recursive: true })) + ) +})() + +// == open + +const openFactory = (method: string) => { + const nodeOpen = effectify( + NFS.open, + handleErrnoException("FileSystem", method), + handleBadArgument(method) + ) + const nodeClose = effectify( + NFS.close, + handleErrnoException("FileSystem", method), + handleBadArgument(method) + ) + + return (path: string, options?: FileSystem.OpenFileOptions) => + pipe( + Effect.acquireRelease( + nodeOpen(path, options?.flag ?? "r", options?.mode), + (fd) => Effect.orDie(nodeClose(fd)) + ), + Effect.map((fd) => makeFile(FileSystem.FileDescriptor(fd), options?.flag?.startsWith("a") ?? false)) + ) +} +const open = openFactory("open") + +const makeFile = (() => { + const nodeReadFactory = (method: string) => + effectify( + NFS.read, + handleErrnoException("FileSystem", method), + handleBadArgument(method) + ) + const nodeRead = nodeReadFactory("read") + const nodeReadAlloc = nodeReadFactory("readAlloc") + const nodeStat = effectify( + NFS.fstat, + handleErrnoException("FileSystem", "stat"), + handleBadArgument("stat") + ) + const nodeTruncate = effectify( + NFS.ftruncate, + handleErrnoException("FileSystem", "truncate"), + handleBadArgument("truncate") + ) + + const nodeSync = effectify( + NFS.fsync, + handleErrnoException("FileSystem", "sync"), + handleBadArgument("sync") + ) + + const nodeWriteFactory = (method: string) => + effectify( + NFS.write, + handleErrnoException("FileSystem", method), + handleBadArgument(method) + ) + const nodeWrite = nodeWriteFactory("write") + const nodeWriteAll = nodeWriteFactory("writeAll") + + class FileImpl implements FileSystem.File { + readonly [FileSystem.FileTypeId]: FileSystem.FileTypeId + + private readonly semaphore = Effect.unsafeMakeSemaphore(1) + private position: bigint = 0n + + constructor( + readonly fd: FileSystem.File.Descriptor, + private readonly append: boolean + ) { + this[FileSystem.FileTypeId] = FileSystem.FileTypeId + } + + get stat() { + return Effect.map(nodeStat(this.fd), makeFileInfo) + } + + get sync() { + return nodeSync(this.fd) + } + + seek(offset: FileSystem.SizeInput, from: FileSystem.SeekMode) { + const offsetSize = FileSystem.Size(offset) + return this.semaphore.withPermits(1)( + Effect.sync(() => { + if (from === "start") { + this.position = offsetSize + } else if (from === "current") { + this.position = this.position + offsetSize + } + + return this.position + }) + ) + } + + read(buffer: Uint8Array) { + return this.semaphore.withPermits(1)( + Effect.map( + Effect.suspend(() => + nodeRead(this.fd, { + buffer, + position: this.position + }) + ), + (bytesRead) => { + const sizeRead = FileSystem.Size(bytesRead) + this.position = this.position + sizeRead + return sizeRead + } + ) + ) + } + + readAlloc(size: FileSystem.SizeInput) { + const sizeNumber = Number(size) + return this.semaphore.withPermits(1)(Effect.flatMap( + Effect.sync(() => Buffer.allocUnsafeSlow(sizeNumber)), + (buffer) => + Effect.map( + nodeReadAlloc(this.fd, { + buffer, + position: this.position + }), + (bytesRead): Option.Option => { + if (bytesRead === 0) { + return Option.none() + } + + this.position = this.position + BigInt(bytesRead) + if (bytesRead === sizeNumber) { + return Option.some(buffer) + } + + const dst = Buffer.allocUnsafeSlow(bytesRead) + buffer.copy(dst, 0, 0, bytesRead) + return Option.some(dst) + } + ) + )) + } + + truncate(length?: FileSystem.SizeInput) { + return this.semaphore.withPermits(1)( + Effect.map(nodeTruncate(this.fd, length ? Number(length) : undefined), () => { + if (!this.append) { + const len = BigInt(length ?? 0) + if (this.position > len) { + this.position = len + } + } + }) + ) + } + + write(buffer: Uint8Array) { + return this.semaphore.withPermits(1)( + Effect.map( + Effect.suspend(() => + nodeWrite(this.fd, buffer, undefined, undefined, this.append ? undefined : Number(this.position)) + ), + (bytesWritten) => { + const sizeWritten = FileSystem.Size(bytesWritten) + if (!this.append) { + this.position = this.position + sizeWritten + } + + return sizeWritten + } + ) + ) + } + + private writeAllChunk(buffer: Uint8Array): Effect.Effect { + return Effect.flatMap( + Effect.suspend(() => + nodeWriteAll(this.fd, buffer, undefined, undefined, this.append ? undefined : Number(this.position)) + ), + (bytesWritten) => { + if (bytesWritten === 0) { + return Effect.fail( + new Error.SystemError({ + module: "FileSystem", + method: "writeAll", + reason: "WriteZero", + pathOrDescriptor: this.fd, + description: "write returned 0 bytes written" + }) + ) + } + + if (!this.append) { + this.position = this.position + BigInt(bytesWritten) + } + + return bytesWritten < buffer.length ? this.writeAllChunk(buffer.subarray(bytesWritten)) : Effect.void + } + ) + } + + writeAll(buffer: Uint8Array) { + return this.semaphore.withPermits(1)(this.writeAllChunk(buffer)) + } + } + + return (fd: FileSystem.File.Descriptor, append: boolean): FileSystem.File => new FileImpl(fd, append) +})() + +// == makeTempFile + +const makeTempFileFactory = (method: string) => { + const makeDirectory = makeTempDirectoryFactory(method) + const open = openFactory(method) + const randomHexString = (bytes: number) => Effect.sync(() => Crypto.randomBytes(bytes).toString("hex")) + return (options?: FileSystem.MakeTempFileOptions) => + pipe( + Effect.zip(makeDirectory(options), randomHexString(6)), + Effect.map(([directory, random]) => Path.join(directory, random + (options?.suffix ?? ""))), + Effect.tap((path) => Effect.scoped(open(path, { flag: "w+" }))) + ) +} +const makeTempFile = makeTempFileFactory("makeTempFile") + +// == makeTempFileScoped + +const makeTempFileScoped = (() => { + const makeFile = makeTempFileFactory("makeTempFileScoped") + const removeDirectory = removeFactory("makeTempFileScoped") + return (options?: FileSystem.MakeTempFileOptions) => + Effect.acquireRelease( + makeFile(options), + (file) => Effect.orDie(removeDirectory(Path.dirname(file), { recursive: true })) + ) +})() + +// == readDirectory + +const readDirectory = (path: string, options?: FileSystem.ReadDirectoryOptions) => + Effect.tryPromise({ + try: () => NFS.promises.readdir(path, options), + catch: (err) => handleErrnoException("FileSystem", "readDirectory")(err as any, [path]) + }) + +// == readFile + +const readFile = (path: string) => + Effect.async((resume, signal) => { + try { + NFS.readFile(path, { signal }, (err, data) => { + if (err) { + resume(Effect.fail(handleErrnoException("FileSystem", "readFile")(err, [path]))) + } else { + resume(Effect.succeed(data)) + } + }) + } catch (err) { + resume(Effect.fail(handleBadArgument("readFile")(err))) + } + }) + +// == readLink + +const readLink = (() => { + const nodeReadLink = effectify( + NFS.readlink, + handleErrnoException("FileSystem", "readLink"), + handleBadArgument("readLink") + ) + return (path: string) => nodeReadLink(path) +})() + +// == realPath + +const realPath = (() => { + const nodeRealPath = effectify( + NFS.realpath, + handleErrnoException("FileSystem", "realPath"), + handleBadArgument("realPath") + ) + return (path: string) => nodeRealPath(path) +})() + +// == rename + +const rename = (() => { + const nodeRename = effectify( + NFS.rename, + handleErrnoException("FileSystem", "rename"), + handleBadArgument("rename") + ) + return (oldPath: string, newPath: string) => nodeRename(oldPath, newPath) +})() + +// == stat + +const makeFileInfo = (stat: NFS.Stats): FileSystem.File.Info => ({ + type: stat.isFile() ? + "File" : + stat.isDirectory() ? + "Directory" : + stat.isSymbolicLink() ? + "SymbolicLink" : + stat.isBlockDevice() ? + "BlockDevice" : + stat.isCharacterDevice() ? + "CharacterDevice" : + stat.isFIFO() ? + "FIFO" : + stat.isSocket() ? + "Socket" : + "Unknown", + mtime: Option.fromNullable(stat.mtime), + atime: Option.fromNullable(stat.atime), + birthtime: Option.fromNullable(stat.birthtime), + dev: stat.dev, + rdev: Option.fromNullable(stat.rdev), + ino: Option.fromNullable(stat.ino), + mode: stat.mode, + nlink: Option.fromNullable(stat.nlink), + uid: Option.fromNullable(stat.uid), + gid: Option.fromNullable(stat.gid), + size: FileSystem.Size(stat.size), + blksize: Option.map(Option.fromNullable(stat.blksize), FileSystem.Size), + blocks: Option.fromNullable(stat.blocks) +}) +const stat = (() => { + const nodeStat = effectify( + NFS.stat, + handleErrnoException("FileSystem", "stat"), + handleBadArgument("stat") + ) + return (path: string) => Effect.map(nodeStat(path), makeFileInfo) +})() + +// == symlink + +const symlink = (() => { + const nodeSymlink = effectify( + NFS.symlink, + handleErrnoException("FileSystem", "symlink"), + handleBadArgument("symlink") + ) + return (target: string, path: string) => nodeSymlink(target, path) +})() + +// == truncate + +const truncate = (() => { + const nodeTruncate = effectify( + NFS.truncate, + handleErrnoException("FileSystem", "truncate"), + handleBadArgument("truncate") + ) + return (path: string, length?: FileSystem.SizeInput) => + nodeTruncate(path, length !== undefined ? Number(length) : undefined) +})() + +// == utimes + +const utimes = (() => { + const nodeUtimes = effectify( + NFS.utimes, + handleErrnoException("FileSystem", "utime"), + handleBadArgument("utime") + ) + return (path: string, atime: number | Date, mtime: number | Date) => nodeUtimes(path, atime, mtime) +})() + +// == watch + +const watchNode = (path: string, options?: FileSystem.WatchOptions) => + Stream.asyncScoped((emit) => + Effect.acquireRelease( + Effect.sync(() => { + const watcher = NFS.watch(path, { recursive: options?.recursive }, (event, path) => { + if (!path) return + switch (event) { + case "rename": { + emit.fromEffect(Effect.matchEffect(stat(path), { + onSuccess: (_) => Effect.succeed(FileSystem.WatchEventCreate({ path })), + onFailure: (err) => + err._tag === "SystemError" && err.reason === "NotFound" + ? Effect.succeed(FileSystem.WatchEventRemove({ path })) + : Effect.fail(err) + })) + return + } + case "change": { + emit.single(FileSystem.WatchEventUpdate({ path })) + return + } + } + }) + watcher.on("error", (error) => { + emit.fail( + new Error.SystemError({ + module: "FileSystem", + reason: "Unknown", + method: "watch", + pathOrDescriptor: path, + cause: error + }) + ) + }) + watcher.on("close", () => { + emit.end() + }) + return watcher + }), + (watcher) => Effect.sync(() => watcher.close()) + ) + ) + +const watch = ( + backend: Option.Option>, + path: string, + options?: FileSystem.WatchOptions +) => + stat(path).pipe( + Effect.map((stat) => + backend.pipe( + Option.flatMap((_) => _.register(path, stat, options)), + Option.getOrElse(() => watchNode(path, options)) + ) + ), + Stream.unwrap + ) + +// == writeFile + +const writeFile = (path: string, data: Uint8Array, options?: FileSystem.WriteFileOptions) => + Effect.async((resume, signal) => { + try { + NFS.writeFile(path, data, { + signal, + flag: options?.flag, + mode: options?.mode + }, (err) => { + if (err) { + resume(Effect.fail(handleErrnoException("FileSystem", "writeFile")(err, [path]))) + } else { + resume(Effect.void) + } + }) + } catch (err) { + resume(Effect.fail(handleBadArgument("writeFile")(err))) + } + }) + +const makeFileSystem = Effect.map(Effect.serviceOption(FileSystem.WatchBackend), (backend) => + FileSystem.make({ + access, + chmod, + chown, + copy, + copyFile, + link, + makeDirectory, + makeTempDirectory, + makeTempDirectoryScoped, + makeTempFile, + makeTempFileScoped, + open, + readDirectory, + readFile, + readLink, + realPath, + remove, + rename, + stat, + symlink, + truncate, + utimes, + watch(path, options) { + return watch(backend, path, options) + }, + writeFile + })) + +/** @internal */ +export const layer = Layer.effect(FileSystem.FileSystem, makeFileSystem) diff --git a/repos/effect/packages/platform-node-shared/src/internal/fileSystem/parcelWatcher.ts b/repos/effect/packages/platform-node-shared/src/internal/fileSystem/parcelWatcher.ts new file mode 100644 index 0000000..f5be795 --- /dev/null +++ b/repos/effect/packages/platform-node-shared/src/internal/fileSystem/parcelWatcher.ts @@ -0,0 +1,64 @@ +import * as Error from "@effect/platform/Error" +import * as FileSystem from "@effect/platform/FileSystem" +import * as ParcelWatcher from "@parcel/watcher" +import * as Chunk from "effect/Chunk" +import * as Effect from "effect/Effect" +import * as Layer from "effect/Layer" +import * as Option from "effect/Option" +import * as Stream from "effect/Stream" + +const watchParcel = (path: string) => + Stream.asyncScoped((emit) => + Effect.acquireRelease( + Effect.tryPromise({ + try: () => + ParcelWatcher.subscribe(path, (cause, events) => { + if (cause) { + emit.fail( + new Error.SystemError({ + reason: "Unknown", + module: "FileSystem", + method: "watch", + pathOrDescriptor: path, + cause + }) + ) + } else { + emit.chunk(Chunk.unsafeFromArray(events.map((event) => { + switch (event.type) { + case "create": { + return FileSystem.WatchEventCreate({ path: event.path }) + } + case "update": { + return FileSystem.WatchEventUpdate({ path: event.path }) + } + case "delete": { + return FileSystem.WatchEventRemove({ path: event.path }) + } + } + }))) + } + }), + catch: (cause) => + new Error.SystemError({ + reason: "Unknown", + module: "FileSystem", + method: "watch", + pathOrDescriptor: path, + cause + }) + }), + (sub) => Effect.promise(() => sub.unsubscribe()) + ) + ) + +const backend = FileSystem.WatchBackend.of({ + register(path, stat, _options) { + if (stat.type !== "Directory") { + return Option.none() + } + return Option.some(watchParcel(path)) + } +}) + +export const layer = Layer.succeed(FileSystem.WatchBackend, backend) diff --git a/repos/effect/packages/platform-node-shared/src/internal/multipart.ts b/repos/effect/packages/platform-node-shared/src/internal/multipart.ts new file mode 100644 index 0000000..0ec3239 --- /dev/null +++ b/repos/effect/packages/platform-node-shared/src/internal/multipart.ts @@ -0,0 +1,141 @@ +import * as Multipart from "@effect/platform/Multipart" +import * as Effect from "effect/Effect" +import { pipe } from "effect/Function" +import * as Inspectable from "effect/Inspectable" +import * as Stream from "effect/Stream" +import type { MultipartError, PartInfo } from "multipasta" +import { decodeField } from "multipasta" +import * as MP from "multipasta/node" +import * as NFS from "node:fs" +import type { IncomingHttpHeaders } from "node:http" +import type { Readable } from "node:stream" +import * as NodeStreamP from "node:stream/promises" +import * as NodeStream from "./stream.js" + +/** @internal */ +export const stream = ( + source: Readable, + headers: IncomingHttpHeaders +): Stream.Stream => + pipe( + Multipart.makeConfig(headers as any), + Effect.map( + (config) => + NodeStream.fromReadable(() => { + const parser = MP.make(config) + source.pipe(parser) + return parser + }, (error) => convertError(error as any)) + ), + Stream.unwrap, + Stream.map(convertPart) + ) + +/** @internal */ +export const persisted = ( + source: Readable, + headers: IncomingHttpHeaders +) => + Multipart.toPersisted(stream(source, headers), (path, file) => + Effect.tryPromise({ + try: (signal) => NodeStreamP.pipeline((file as FileImpl).file, NFS.createWriteStream(path), { signal }), + catch: (cause) => new Multipart.MultipartError({ reason: "InternalError", cause }) + })) + +const convertPart = (part: MP.Part): Multipart.Part => + part._tag === "Field" ? new FieldImpl(part.info, part.value) : new FileImpl(part) + +abstract class PartBase extends Inspectable.Class { + readonly [Multipart.TypeId]: Multipart.TypeId + constructor() { + super() + this[Multipart.TypeId] = Multipart.TypeId + } +} + +class FieldImpl extends PartBase implements Multipart.Field { + readonly _tag = "Field" + readonly key: string + readonly contentType: string + readonly value: string + + constructor( + info: PartInfo, + value: Uint8Array + ) { + super() + this.key = info.name + this.contentType = info.contentType + this.value = decodeField(info, value) + } + + toJSON(): unknown { + return { + _id: "@effect/platform/Multipart/Part", + _tag: "Field", + key: this.key, + value: this.value, + contentType: this.contentType + } + } +} + +class FileImpl extends PartBase implements Multipart.File { + readonly _tag = "File" + readonly key: string + readonly name: string + readonly contentType: string + readonly content: Stream.Stream + readonly contentEffect: Effect.Effect + + constructor(readonly file: MP.FileStream) { + super() + this.key = file.info.name + this.name = file.filename ?? file.info.name + this.contentType = file.info.contentType + this.content = NodeStream.fromReadable( + () => file, + (cause) => new Multipart.MultipartError({ reason: "InternalError", cause }) + ) + this.contentEffect = NodeStream.toUint8Array(() => file, { + onFailure: (cause) => new Multipart.MultipartError({ reason: "InternalError", cause }) + }) + } + + toJSON(): unknown { + return { + _id: "@effect/platform/Multipart/Part", + _tag: "File", + key: this.key, + name: this.name, + contentType: this.contentType + } + } +} + +/** @internal */ +export const fileToReadable = (file: Multipart.File): Readable => (file as FileImpl).file + +function convertError(cause: MultipartError): Multipart.MultipartError { + switch (cause._tag) { + case "ReachedLimit": { + switch (cause.limit) { + case "MaxParts": { + return new Multipart.MultipartError({ reason: "TooManyParts", cause }) + } + case "MaxFieldSize": { + return new Multipart.MultipartError({ reason: "FieldTooLarge", cause }) + } + case "MaxPartSize": { + return new Multipart.MultipartError({ reason: "FileTooLarge", cause }) + } + case "MaxTotalSize": { + return new Multipart.MultipartError({ reason: "BodyTooLarge", cause }) + } + } + } + default: { + return new Multipart.MultipartError({ reason: "Parse", cause }) + } + } +} diff --git a/repos/effect/packages/platform-node-shared/src/internal/path.ts b/repos/effect/packages/platform-node-shared/src/internal/path.ts new file mode 100644 index 0000000..ef17483 --- /dev/null +++ b/repos/effect/packages/platform-node-shared/src/internal/path.ts @@ -0,0 +1,63 @@ +import { BadArgument } from "@effect/platform/Error" +import { Path, TypeId } from "@effect/platform/Path" +import * as Effect from "effect/Effect" +import * as Layer from "effect/Layer" +import * as NodePath from "node:path" +import * as NodeUrl from "node:url" + +const fromFileUrl = (url: URL): Effect.Effect => + Effect.try({ + try: () => NodeUrl.fileURLToPath(url), + catch: (error) => + new BadArgument({ + module: "Path", + method: "fromFileUrl", + description: `Invalid file URL: ${url}`, + cause: error + }) + }) + +const toFileUrl = (path: string): Effect.Effect => + Effect.try({ + try: () => NodeUrl.pathToFileURL(path), + catch: (error) => + new BadArgument({ + module: "Path", + method: "toFileUrl", + description: `Invalid path: ${path}`, + cause: error + }) + }) + +/** @internal */ +export const layerPosix = Layer.succeed( + Path, + Path.of({ + [TypeId]: TypeId, + ...NodePath.posix, + fromFileUrl, + toFileUrl + }) +) + +/** @internal */ +export const layerWin32 = Layer.succeed( + Path, + Path.of({ + [TypeId]: TypeId, + ...NodePath.win32, + fromFileUrl, + toFileUrl + }) +) + +/** @internal */ +export const layer = Layer.succeed( + Path, + Path.of({ + [TypeId]: TypeId, + ...NodePath, + fromFileUrl, + toFileUrl + }) +) diff --git a/repos/effect/packages/platform-node-shared/src/internal/runtime.ts b/repos/effect/packages/platform-node-shared/src/internal/runtime.ts new file mode 100644 index 0000000..1c6691f --- /dev/null +++ b/repos/effect/packages/platform-node-shared/src/internal/runtime.ts @@ -0,0 +1,34 @@ +import { makeRunMain } from "@effect/platform/Runtime" +import { constVoid } from "effect/Function" + +/** @internal */ +export const runMain = makeRunMain(({ + fiber, + teardown +}) => { + const keepAlive = setInterval(constVoid, 2 ** 31 - 1) + let receivedSignal = false + + fiber.addObserver((exit) => { + if (!receivedSignal) { + process.removeListener("SIGINT", onSigint) + process.removeListener("SIGTERM", onSigint) + } + clearInterval(keepAlive) + teardown(exit, (code) => { + if (receivedSignal || code !== 0) { + process.exit(code) + } + }) + }) + + function onSigint() { + receivedSignal = true + process.removeListener("SIGINT", onSigint) + process.removeListener("SIGTERM", onSigint) + fiber.unsafeInterruptAsFork(fiber.id()) + } + + process.on("SIGINT", onSigint) + process.on("SIGTERM", onSigint) +}) diff --git a/repos/effect/packages/platform-node-shared/src/internal/sink.ts b/repos/effect/packages/platform-node-shared/src/internal/sink.ts new file mode 100644 index 0000000..3877a7e --- /dev/null +++ b/repos/effect/packages/platform-node-shared/src/internal/sink.ts @@ -0,0 +1,57 @@ +import * as Channel from "effect/Channel" +import type * as Chunk from "effect/Chunk" +import * as Deferred from "effect/Deferred" +import * as Effect from "effect/Effect" +import type { LazyArg } from "effect/Function" +import * as Sink from "effect/Sink" +import type { Writable } from "node:stream" +import type { FromWritableOptions } from "../NodeStream.js" +import { writeInput } from "./stream.js" + +/** @internal */ +export const fromWritable = ( + evaluate: LazyArg, + onError: (error: unknown) => E, + options?: FromWritableOptions +): Sink.Sink => Sink.fromChannel(fromWritableChannel(evaluate, onError, options)) + +/** @internal */ +export const fromWritableChannel = ( + writable: LazyArg, + onError: (error: unknown) => OE, + options?: FromWritableOptions +): Channel.Channel, Chunk.Chunk, IE | OE, IE, void, unknown> => + Channel.flatMap( + Effect.zip( + Effect.sync(() => writable()), + Deferred.make() + ), + ([writable, deferred]) => + Channel.embedInput( + writableOutput(writable, deferred, onError), + writeInput( + writable, + (cause) => Deferred.failCause(deferred, cause), + options, + Deferred.complete(deferred, Effect.void) + ) + ) + ) + +const writableOutput = ( + writable: Writable | NodeJS.WritableStream, + deferred: Deferred.Deferred, + onError: (error: unknown) => E +) => + Effect.suspend(() => { + function handleError(err: unknown) { + Deferred.unsafeDone(deferred, Effect.fail(onError(err))) + } + writable.on("error", handleError) + return Effect.ensuring( + Deferred.await(deferred), + Effect.sync(() => { + writable.removeListener("error", handleError) + }) + ) + }) diff --git a/repos/effect/packages/platform-node-shared/src/internal/stream.ts b/repos/effect/packages/platform-node-shared/src/internal/stream.ts new file mode 100644 index 0000000..5b1fd03 --- /dev/null +++ b/repos/effect/packages/platform-node-shared/src/internal/stream.ts @@ -0,0 +1,375 @@ +import { type PlatformError, SystemError } from "@effect/platform/Error" +import type { SizeInput } from "@effect/platform/FileSystem" +import * as Cause from "effect/Cause" +import * as Channel from "effect/Channel" +import * as Chunk from "effect/Chunk" +import * as Effect from "effect/Effect" +import * as Exit from "effect/Exit" +import * as Fiber from "effect/Fiber" +import type { LazyArg } from "effect/Function" +import { dual } from "effect/Function" +import * as MutableRef from "effect/MutableRef" +import * as Runtime from "effect/Runtime" +import type * as AsyncInput from "effect/SingleProducerAsyncInput" +import * as Stream from "effect/Stream" +import type { Duplex, Writable } from "node:stream" +import { Readable } from "node:stream" +import type { FromReadableOptions, FromWritableOptions } from "../NodeStream.js" + +/** @internal */ +export const fromReadable = ( + evaluate: LazyArg, + onError: (error: unknown) => E, + options?: FromReadableOptions +): Stream.Stream => + Stream.fromChannel( + fromReadableChannel(evaluate, onError, options) + ) + +/** @internal */ +export const toString = ( + readable: LazyArg, + options: { + readonly onFailure: (error: unknown) => E + readonly encoding?: BufferEncoding | undefined + readonly maxBytes?: SizeInput | undefined + } +): Effect.Effect => { + const maxBytesNumber = options.maxBytes ? Number(options.maxBytes) : undefined + return Effect.acquireUseRelease( + Effect.sync(() => { + const stream = readable() + stream.setEncoding(options.encoding ?? "utf8") + return stream + }), + (stream) => + Effect.async((resume) => { + let string = "" + let bytes = 0 + stream.once("error", (err) => { + resume(Effect.fail(options.onFailure(err))) + }) + stream.once("end", () => { + resume(Effect.succeed(string)) + }) + stream.on("data", (chunk) => { + string += chunk + bytes += Buffer.byteLength(chunk) + if (maxBytesNumber && bytes > maxBytesNumber) { + resume(Effect.fail(options.onFailure(new Error("maxBytes exceeded")))) + } + }) + }), + (stream) => + Effect.sync(() => { + if ("closed" in stream && !stream.closed) { + stream.destroy() + } + }) + ) +} + +/** @internal */ +export const toUint8Array = ( + readable: LazyArg, + options: { + readonly onFailure: (error: unknown) => E + readonly maxBytes?: SizeInput | undefined + } +): Effect.Effect => { + const maxBytesNumber = options.maxBytes ? Number(options.maxBytes) : undefined + return Effect.acquireUseRelease( + Effect.sync(readable), + (stream) => + Effect.async((resume) => { + let buffer = Buffer.alloc(0) + let bytes = 0 + stream.once("error", (err) => { + resume(Effect.fail(options.onFailure(err))) + }) + stream.once("end", () => { + resume(Effect.succeed(buffer)) + }) + stream.on("data", (chunk) => { + buffer = Buffer.concat([buffer, chunk]) + bytes += chunk.length + if (maxBytesNumber && bytes > maxBytesNumber) { + resume(Effect.fail(options.onFailure(new Error("maxBytes exceeded")))) + } + }) + }), + (stream) => + Effect.sync(() => { + if ("closed" in stream && !stream.closed) { + stream.destroy() + } + }) + ) +} + +/** @internal */ +export const fromDuplex = , O = Uint8Array>( + evaluate: LazyArg, + onError: (error: unknown) => E, + options?: FromReadableOptions & FromWritableOptions +): Channel.Channel< + Chunk.Chunk, + Chunk.Chunk, + IE | E, + IE +> => + Channel.suspend(() => { + const duplex = evaluate() + if (!duplex.readable) { + return Channel.void + } + const exit = MutableRef.make | undefined>(undefined) + return Channel.embedInput( + unsafeReadableRead(duplex, onError, exit, options), + writeInput( + duplex, + (cause) => Effect.sync(() => MutableRef.set(exit, Exit.failCause(cause))), + options + ) + ) + }) + +/** @internal */ +export const pipeThroughDuplex = dual< + ( + duplex: LazyArg, + onError: (error: unknown) => E2, + options?: FromReadableOptions & FromWritableOptions + ) => (self: Stream.Stream) => Stream.Stream, + ( + self: Stream.Stream, + duplex: LazyArg, + onError: (error: unknown) => E2, + options?: FromReadableOptions & FromWritableOptions + ) => Stream.Stream +>( + (args) => Stream.StreamTypeId in args[0], + (self, duplex, onError, options) => + Stream.pipeThroughChannelOrFail( + self, + fromDuplex(duplex, onError, options) + ) +) + +/** @internal */ +export const pipeThroughSimple = dual< + ( + duplex: LazyArg + ) => (self: Stream.Stream) => Stream.Stream, + ( + self: Stream.Stream, + duplex: LazyArg + ) => Stream.Stream +>( + 2, + (self, duplex) => + Stream.pipeThroughChannelOrFail( + self, + fromDuplex(duplex, (cause) => + new SystemError({ + module: "Stream", + method: "pipeThroughSimple", + reason: "Unknown", + cause + })) + ) +) + +/** @internal */ +export const fromReadableChannel = ( + evaluate: LazyArg, + onError: (error: unknown) => E, + options?: FromReadableOptions | undefined +): Channel.Channel< + Chunk.Chunk, + unknown, + E +> => + Channel.suspend(() => + unsafeReadableRead( + evaluate(), + onError, + MutableRef.make(undefined), + options + ) + ) + +/** @internal */ +export const writeInput = ( + writable: Writable | NodeJS.WritableStream, + onFailure: (cause: Cause.Cause) => Effect.Effect, + { encoding, endOnDone = true }: FromWritableOptions = {}, + onDone = Effect.void +): AsyncInput.AsyncInputProducer, unknown> => { + const write = writeEffect(writable, encoding) + const close = endOnDone + ? Effect.async((resume) => { + if ("closed" in writable && writable.closed) { + resume(Effect.void) + } else { + writable.once("finish", () => resume(Effect.void)) + writable.end() + } + }) + : Effect.void + return { + awaitRead: () => Effect.void, + emit: write, + error: (cause) => Effect.zipRight(close, onFailure(cause)), + done: (_) => Effect.zipRight(close, onDone) + } +} + +/** @internal */ +export const writeEffect = ( + writable: Writable | NodeJS.WritableStream, + encoding?: BufferEncoding +) => +(chunk: Chunk.Chunk) => + chunk.length === 0 ? + Effect.void : + Effect.async((resume) => { + const iterator = chunk[Symbol.iterator]() + let next = iterator.next() + function loop() { + const item = next + next = iterator.next() + const success = writable.write(item.value, encoding as any) + if (next.done) { + resume(Effect.void) + } else if (success) { + loop() + } else { + writable.once("drain", loop) + } + } + loop() + }) + +const unsafeReadableRead = ( + readable: Readable | NodeJS.ReadableStream, + onError: (error: unknown) => E, + exit: MutableRef.MutableRef | undefined>, + options: FromReadableOptions | undefined +) => { + if (!readable.readable) { + return Channel.void + } + + const latch = Effect.unsafeMakeLatch(false) + function onReadable() { + latch.unsafeOpen() + } + function onErr(err: unknown) { + exit.current = Exit.fail(onError(err)) + latch.unsafeOpen() + } + function onEnd() { + exit.current = Exit.void + latch.unsafeOpen() + } + readable.on("readable", onReadable) + readable.on("error", onErr) + readable.on("end", onEnd) + + const chunkSize = options?.chunkSize ? Number(options.chunkSize) : undefined + const read = Channel.suspend(function loop(): Channel.Channel, unknown, E> { + let item = readable.read(chunkSize) as A | null + if (item === null) { + if (exit.current) { + return Channel.fromEffect(exit.current) + } + latch.unsafeClose() + return Channel.flatMap(latch.await, loop) + } + const arr = [item as A] + while (true) { + item = readable.read(chunkSize) + if (item === null) { + return Channel.flatMap(Channel.write(Chunk.unsafeFromArray(arr)), loop) + } + arr.push(item as A) + } + }) + + return Channel.ensuring( + read, + Effect.sync(() => { + readable.off("readable", onReadable) + readable.off("error", onErr) + readable.off("end", onEnd) + if (options?.closeOnDone !== false && "closed" in readable && !readable.closed) { + readable.destroy() + } + }) + ) +} + +class StreamAdapter extends Readable { + readonly readLatch: Effect.Latch + fiber: Fiber.RuntimeFiber | undefined = undefined + + constructor( + runtime: Runtime.Runtime, + stream: Stream.Stream + ) { + super({}) + this.readLatch = Effect.unsafeMakeLatch(false) + this.fiber = Runtime.runFork(runtime)( + this.readLatch.whenOpen( + Stream.runForEachChunk(stream, (chunk) => + this.readLatch.whenOpen(Effect.sync(() => { + if (chunk.length === 0) return + this.readLatch.unsafeClose() + for (const item of chunk) { + if (typeof item === "string") { + this.push(item, "utf8") + } else { + this.push(item) + } + } + }))) + ) + ) + this.fiber.addObserver((exit) => { + this.fiber = undefined + if (Exit.isSuccess(exit)) { + this.push(null) + } else { + this.destroy(Cause.squash(exit.cause) as any) + } + }) + } + + _read(_size: number): void { + this.readLatch.unsafeOpen() + } + + _destroy(error: Error | null, callback: (error?: Error | null | undefined) => void): void { + if (!this.fiber) { + return callback(error) + } + Effect.runFork(Fiber.interrupt(this.fiber)).addObserver((exit) => { + callback(exit._tag === "Failure" ? Cause.squash(exit.cause) as any : error) + }) + } +} + +/** @internal */ +export const toReadable = ( + stream: Stream.Stream +): Effect.Effect => + Effect.map( + Effect.runtime(), + (runtime) => new StreamAdapter(runtime, stream) + ) + +/** @internal */ +export const toReadableNever = ( + stream: Stream.Stream +): Readable => new StreamAdapter(Runtime.defaultRuntime, stream) diff --git a/repos/effect/packages/platform-node-shared/src/internal/terminal.ts b/repos/effect/packages/platform-node-shared/src/internal/terminal.ts new file mode 100644 index 0000000..e1db804 --- /dev/null +++ b/repos/effect/packages/platform-node-shared/src/internal/terminal.ts @@ -0,0 +1,104 @@ +import * as Error from "@effect/platform/Error" +import * as Terminal from "@effect/platform/Terminal" +import * as Effect from "effect/Effect" +import * as Exit from "effect/Exit" +import * as Layer from "effect/Layer" +import * as Mailbox from "effect/Mailbox" +import * as Option from "effect/Option" +import * as RcRef from "effect/RcRef" +import * as readline from "node:readline" + +const defaultShouldQuit = (input: Terminal.UserInput) => + input.key.ctrl && (input.key.name === "c" || input.key.name === "d") + +/** @internal */ +export const make = Effect.fnUntraced(function*( + shouldQuit: (input: Terminal.UserInput) => boolean = defaultShouldQuit +) { + const stdin = process.stdin + const stdout = process.stdout + + // Acquire readline interface with TTY setup/cleanup inside the scope + const rlRef = yield* RcRef.make({ + acquire: Effect.acquireRelease( + Effect.sync(() => { + const rl = readline.createInterface({ input: stdin, escapeCodeTimeout: 50 }) + readline.emitKeypressEvents(stdin, rl) + + if (stdin.isTTY) { + stdin.setRawMode(true) + } + return rl + }), + (rl) => + Effect.sync(() => { + if (stdin.isTTY) { + stdin.setRawMode(false) + } + rl.close() + }) + ) + }) + + const columns = Effect.sync(() => stdout.columns ?? 0) + const rows = Effect.sync(() => stdout.rows ?? 0) + const isTTY = Effect.sync(() => Boolean(stdout.isTTY)) + + const readInput = Effect.gen(function*() { + yield* RcRef.get(rlRef) + const mailbox = yield* Mailbox.make() + const handleKeypress = (s: string | undefined, k: readline.Key) => { + const userInput = { + input: Option.fromNullable(s), + key: { name: k.name ?? "", ctrl: !!k.ctrl, meta: !!k.meta, shift: !!k.shift } + } + mailbox.unsafeOffer(userInput) + if (shouldQuit(userInput)) { + mailbox.unsafeDone(Exit.void) + } + } + yield* Effect.addFinalizer(() => Effect.sync(() => stdin.off("keypress", handleKeypress))) + stdin.on("keypress", handleKeypress) + return mailbox as Mailbox.ReadonlyMailbox + }) + + const readLine = RcRef.get(rlRef).pipe( + Effect.flatMap((readlineInterface) => + Effect.async((resume) => { + const onLine = (line: string) => resume(Effect.succeed(line)) + readlineInterface.once("line", onLine) + return Effect.sync(() => readlineInterface.off("line", onLine)) + }) + ), + Effect.scoped + ) + + const display = (prompt: string) => + Effect.uninterruptible( + Effect.async((resume) => { + stdout.write(prompt, (err) => + err + ? resume(Effect.fail( + new Error.BadArgument({ + module: "Terminal", + method: "display", + description: "Failed to write prompt to stdout", + cause: err + }) + )) + : resume(Effect.void)) + }) + ) + + return Terminal.Terminal.of({ + columns, + rows, + isTTY, + readInput, + readLine, + display + }) +}) + +/** @internal */ +export const layer: Layer.Layer = Layer.scoped(Terminal.Terminal, make(defaultShouldQuit)) diff --git a/repos/effect/packages/platform-node-shared/test/CommandExecutor.test.ts b/repos/effect/packages/platform-node-shared/test/CommandExecutor.test.ts new file mode 100644 index 0000000..06ac774 --- /dev/null +++ b/repos/effect/packages/platform-node-shared/test/CommandExecutor.test.ts @@ -0,0 +1,474 @@ +import * as NodeCommandExecutor from "@effect/platform-node-shared/NodeCommandExecutor" +import * as NodeFileSystem from "@effect/platform-node-shared/NodeFileSystem" +import * as NodePath from "@effect/platform-node-shared/NodePath" +import * as Command from "@effect/platform/Command" +import type * as CommandExecutor from "@effect/platform/CommandExecutor" +import { SystemError } from "@effect/platform/Error" +import * as FileSystem from "@effect/platform/FileSystem" +import * as Path from "@effect/platform/Path" +import { describe, expect, it } from "@effect/vitest" +import * as Array from "effect/Array" +import * as Chunk from "effect/Chunk" +import * as Effect from "effect/Effect" +import * as Exit from "effect/Exit" +import * as Fiber from "effect/Fiber" +import { pipe } from "effect/Function" +import * as Layer from "effect/Layer" +import * as Option from "effect/Option" +import * as Order from "effect/Order" +import * as Stream from "effect/Stream" + +const TEST_BASH_SCRIPTS_PATH = [__dirname, "fixtures", "bash"] + +const TestLive = NodeCommandExecutor.layer.pipe( + Layer.provideMerge(NodeFileSystem.layer), + Layer.merge(NodePath.layer) +) + +const runPromise = ( + self: Effect.Effect +) => Effect.runPromise(Effect.provide(self, TestLive)) + +describe("Command", () => { + it("should convert stdout to a string", () => + runPromise(Effect.gen(function*() { + const command = Command.make("echo", "-n", "test") + const result = yield* Command.string(command) + expect(result).toEqual("test") + }))) + + it("should convert stdout to a list of lines", () => + runPromise(Effect.gen(function*() { + const command = Command.make("echo", "-n", "1\n2\n3") + const result = yield* Command.lines(command) + expect(result).toEqual(["1", "2", "3"]) + }))) + + it("should stream lines of output", () => + runPromise(Effect.gen(function*() { + const command = Command.make("echo", "-n", "1\n2\n3") + const result = yield* Stream.runCollect(Command.streamLines(command)) + expect(Chunk.toReadonlyArray(result)).toEqual(["1", "2", "3"]) + }))) + + it("should work with a Stream directly", () => + runPromise(Effect.gen(function*() { + const decoder = new TextDecoder("utf-8") + const command = Command.make("echo", "-n", "1\n2\n3") + const result = yield* pipe( + Command.stream(command), + Stream.mapChunks(Chunk.map((bytes) => decoder.decode(bytes))), + Stream.splitLines, + Stream.runCollect + ) + expect(Chunk.toReadonlyArray(result)).toEqual(["1", "2", "3"]) + }))) + + it("should fail when trying to run a command that does not exist", () => + runPromise(Effect.gen(function*() { + const command = Command.make("some-invalid-command", "test") + const result = yield* Effect.exit(Command.string(command)) + expect(result).toEqual(Exit.fail( + new SystemError({ + reason: "NotFound", + module: "Command", + method: "spawn", + pathOrDescriptor: "some-invalid-command test", + syscall: "spawn some-invalid-command", + description: "spawn some-invalid-command ENOENT" + }) + )) + }))) + + it("should pass environment variables", () => + runPromise(Effect.gen(function*() { + const command = pipe( + Command.make("bash", "-c", "echo -n \"var = $VAR\""), + Command.env({ VAR: "myValue" }) + ) + const result = yield* Command.string(command) + expect(result).toBe("var = myValue") + }))) + + it("should accept streaming stdin", () => + runPromise(Effect.gen(function*() { + const stdin = Stream.make(Buffer.from("a b c", "utf-8")) + const command = pipe(Command.make("cat"), Command.stdin(stdin)) + const result = yield* Command.string(command) + expect(result).toEqual("a b c") + }))) + + it("should accept string stdin", () => + runPromise(Effect.gen(function*() { + const stdin = "piped in" + const command = pipe(Command.make("cat"), Command.feed(stdin)) + const result = yield* Command.string(command) + expect(result).toEqual("piped in") + }))) + + it("should set the working directory", () => + runPromise(Effect.gen(function*() { + const path = yield* Path.Path + const command = pipe( + Command.make("ls"), + Command.workingDirectory(path.join(__dirname, "..", "src")) + ) + const result = yield* Command.lines(command) + expect(result).toContain("NodeCommandExecutor.ts") + }))) + + it("should be able to fall back to a different program", () => + runPromise(Effect.gen(function*() { + const command = Command.make("custom-echo", "-n", "test") + const result = yield* pipe( + Command.string(command), + Effect.catchTag("SystemError", (error) => { + if (error.reason === "NotFound") { + return Command.string(Command.make("echo", "-n", "test")) + } + return Effect.fail(error) + }) + ) + expect(result).toBe("test") + }))) + + it("should interrupt a process manually", () => + runPromise(Effect.gen(function*() { + const command = Command.make("sleep", "20") + const result = yield* pipe( + Effect.fork(Command.exitCode(command)), + Effect.flatMap((fiber) => Effect.fork(Fiber.interrupt(fiber))), + Effect.flatMap(Fiber.join) + ) + expect(Exit.isInterrupted(result)).toBe(true) + }))) + + // TODO: figure out how to get access to TestClock + // it("should interrupt a process due to a timeout", () => + // T.gen(function* (_) { + // const testClock = yield* _(TestClock) + + // const command = pipe( + // Command.make("sleep", "20"), + // Command.exitCode, + // T.timeout(5000) + // ) + + // const output = yield* _( + // pipe( + // T.do, + // T.bind("fiber", () => T.fork(command)), + // T.bind("adjustFiber", () => T.fork(testClock.adjust(5000))), + // T.tap(() => T.sleep(5000)), + // T.chain(({ adjustFiber, fiber }) => + // pipe( + // F.join(adjustFiber), + // T.chain(() => F.join(fiber)) + // ) + // ) + // ) + // ) + + // expect(O.isNone(output)).toBeTruthy() + // })) + + // TODO: this test is flaky + // it("should capture stderr and stdout separately", () => + // runPromise(Effect.gen(function*(_) { + // const command = pipe( + // Command.make("./duplex.sh"), + // Command.workingDirectory(TEST_BASH_SCRIPTS_DIRECTORY) + // ) + // const process = yield* _(Command.start(command)) + // const result = yield* _(pipe( + // process.stdout, + // Stream.zip(process.stderr), + // Stream.runCollect, + // Effect.map((bytes) => { + // const decoder = new TextDecoder("utf-8") + // return Array.from(bytes).flatMap(([left, right]) => + // [ + // decoder.decode(left), + // decoder.decode(right) + // ] as const + // ) + // }) + // )) + // expect(result).toEqual([ + // "stdout1\nstdout2\n", + // "stderr1\nstderr2\n" + // ]) + // }))) + + it("should return non-zero exit code in success channel", () => + runPromise(Effect.gen(function*() { + const path = yield* Path.Path + const command = pipe( + Command.make("./non-zero-exit.sh"), + Command.workingDirectory(path.join(...TEST_BASH_SCRIPTS_PATH)) + ) + const result = yield* Command.exitCode(command) + expect(result).toBe(1) + }))) + + it("should throw permission denied as a typed error", () => + runPromise(Effect.gen(function*() { + const path = yield* Path.Path + const command = pipe( + Command.make("./no-permissions.sh"), + Command.workingDirectory(path.join(...TEST_BASH_SCRIPTS_PATH)) + ) + const result = yield* Effect.exit(Command.string(command)) + expect(result).toEqual(Exit.fail( + new SystemError({ + reason: "PermissionDenied", + module: "Command", + method: "spawn", + pathOrDescriptor: "./no-permissions.sh ", + syscall: "spawn ./no-permissions.sh", + description: "spawn ./no-permissions.sh EACCES" + }) + )) + }))) + + it("should throw non-existent working directory as a typed error", () => + runPromise(Effect.gen(function*() { + const command = pipe( + Command.make("ls"), + Command.workingDirectory("/some/bad/path") + ) + const result = yield* Effect.exit(Command.lines(command)) + expect(result).toEqual(Exit.fail( + new SystemError({ + reason: "NotFound", + module: "FileSystem", + method: "access", + pathOrDescriptor: "/some/bad/path", + syscall: "access", + description: "ENOENT: no such file or directory, access '/some/bad/path'" + }) + )) + }))) + + it("should be able to kill a running process", () => + runPromise( + Effect.gen(function*() { + const path = yield* Path.Path + const command = pipe( + Command.make("./repeat.sh"), + Command.workingDirectory(path.join(...TEST_BASH_SCRIPTS_PATH)) + ) + const process = yield* Command.start(command) + const isRunningBeforeKill = yield* process.isRunning + yield* process.kill() + const isRunningAfterKill = yield* process.isRunning + expect(isRunningBeforeKill).toBe(true) + expect(isRunningAfterKill).toBe(false) + }).pipe(Effect.scoped) + )) + + it("should support piping commands together", () => + runPromise(Effect.gen(function*() { + const command = pipe( + Command.make("echo", "2\n1\n3"), + Command.pipeTo(Command.make("cat")), + Command.pipeTo(Command.make("sort")) + ) + const result = yield* Command.lines(command) + expect(result).toEqual(["1", "2", "3"]) + }))) + + it("should ensure that piping commands is associative", () => + runPromise(Effect.gen(function*() { + const command = pipe( + Command.make("echo", "2\n1\n3"), + Command.pipeTo(Command.make("cat")), + Command.pipeTo(Command.make("sort")), + Command.pipeTo(Command.make("head", "-2")) + ) + const lines1 = yield* Command.lines(command) + const lines2 = yield* Command.lines(command) + expect(lines1).toEqual(["1", "2"]) + expect(lines2).toEqual(["1", "2"]) + }))) + + it("should allow stdin on a piped command", () => + runPromise(Effect.gen(function*() { + const encoder = new TextEncoder() + const command = pipe( + Command.make("cat"), + Command.pipeTo(Command.make("sort")), + Command.pipeTo(Command.make("head", "-2")), + Command.stdin(Stream.make(encoder.encode("2\n1\n3"))) + ) + const result = yield* Command.lines(command) + expect(result).toEqual(["1", "2"]) + }))) + + it("should delegate env to all commands", () => { + const env = { key: "value" } + const command = pipe( + Command.make("cat"), + Command.pipeTo(Command.make("sort")), + Command.pipeTo(Command.make("head", "-2")), + Command.env(env) + ) + const envs = Command.flatten(command).map((command) => Object.fromEntries(command.env)) + expect(envs).toEqual([env, env, env]) + }) + + it("should delegate workingDirectory to all commands", () => { + const workingDirectory = "working-directory" + const command = pipe( + Command.make("cat"), + Command.pipeTo(Command.make("sort")), + Command.pipeTo(Command.make("head", "-2")), + Command.workingDirectory(workingDirectory) + ) + const directories = Command.flatten(command).map((command) => command.cwd) + expect(directories).toEqual([ + Option.some(workingDirectory), + Option.some(workingDirectory), + Option.some(workingDirectory) + ]) + }) + + it("should delegate stderr to the right-most command", () => { + const command = pipe( + Command.make("cat"), + Command.pipeTo(Command.make("sort")), + Command.pipeTo(Command.make("head", "-2")), + Command.stderr("inherit") + ) + const stderr = Command.flatten(command).map((command) => command.stderr) + expect(stderr).toEqual(["pipe", "pipe", "inherit"]) + }) + + it("should delegate stdout to the right-most command", () => { + const command = pipe( + Command.make("cat"), + Command.pipeTo(Command.make("sort")), + Command.pipeTo(Command.make("head", "-2")), + Command.stdout("inherit") + ) + const stdout = Command.flatten(command).map((command) => command.stdout) + expect(stdout).toEqual(["pipe", "pipe", "inherit"]) + }) + + it("exitCode after exit", () => + runPromise( + Effect.gen(function*() { + const command = Command.make("echo", "-n", "test") + const process = yield* Command.start(command) + yield* process.exitCode + const code = yield* process.exitCode + expect(code).toEqual(0) + }).pipe(Effect.scoped) + )) + + it("should kill all child processes in process group", () => + runPromise( + Effect.gen(function*() { + const path = yield* Path.Path + const command = pipe( + Command.make("./spawn-children.sh"), + Command.workingDirectory(path.join(...TEST_BASH_SCRIPTS_PATH)) + ) + + // Start the process that spawns children and grandchildren + const proc = yield* Command.start(command) + + // Give it time to spawn all processes + yield* Effect.sleep(500) + + // Verify the main process is running + const isRunningBeforeKill = yield* proc.isRunning + expect(isRunningBeforeKill).toBe(true) + + // Count processes before killing - should be at least 7 (1 parent + 3 children + 3 grandchildren) + const beforeKill = yield* pipe( + Command.string(Command.make("bash", "-c", "ps aux | grep spawn-children.sh | grep -v grep | wc -l")), + Effect.map((s) => parseInt(s.trim())), + Effect.orElse(() => Effect.succeed(0)) + ) + expect(beforeKill).toBeGreaterThanOrEqual(7) + + // Kill the main process + yield* proc.kill() + + // Verify the main process is no longer running + const isRunningAfterKill = yield* proc.isRunning + expect(isRunningAfterKill).toBe(false) + + // Give a moment for cleanup to complete + yield* Effect.sleep(500) + + // Check that no processes from the script are still running + const afterKill = yield* pipe( + Command.string(Command.make("bash", "-c", "ps aux | grep spawn-children.sh | grep -v grep | wc -l")), + Effect.map((s) => parseInt(s.trim())), + Effect.orElse(() => Effect.succeed(0)) + ) + expect(afterKill).toBe(0) + }).pipe(Effect.scoped) + )) + + it("should cleanup child processes when parent exits with non-zero code", () => + runPromise( + Effect.gen(function*() { + const path = yield* Path.Path + const command = pipe( + Command.make("./parent-exits-early.sh"), + Command.workingDirectory(path.join(...TEST_BASH_SCRIPTS_PATH)) + ) + + // Count processes before running the command + const beforeRun = yield* pipe( + Command.string(Command.make("bash", "-c", "ps aux | grep parent-exits-early.sh | grep -v grep | wc -l")), + Effect.map((s) => parseInt(s.trim())), + Effect.orElse(() => Effect.succeed(0)) + ) + expect(beforeRun).toBe(0) + + // Run the command that will spawn children and then exit with error + const exitCode = yield* Command.exitCode(command) + + // Verify it exited with code 1 + expect(exitCode).toBe(1) + + // Give a moment for cleanup to complete + yield* Effect.sleep(500) + + // Check that no child processes are still running + const afterExit = yield* pipe( + Command.string(Command.make("bash", "-c", "ps aux | grep 'sleep 30' | grep -v grep | wc -l")), + Effect.map((s) => parseInt(s.trim())), + Effect.orElse(() => Effect.succeed(0)) + ) + + // Child processes should be cleaned up after non-zero exit + expect(afterExit).toBe(0) + }).pipe(Effect.scoped) + )) + + it("should allow running commands in a shell", () => + runPromise( + Effect.gen(function*() { + const files = ["foo.txt", "bar.txt", "baz.txt"] + const path = yield* Path.Path + const fileSystem = yield* FileSystem.FileSystem + const tempDir = yield* fileSystem.makeTempDirectoryScoped() + yield* Effect.forEach( + files, + (file) => fileSystem.writeFile(path.join(tempDir, file), new Uint8Array()), + { discard: true } + ) + const command = Command.make("compgen", "-f").pipe( + Command.workingDirectory(tempDir), + Command.runInShell("/bin/bash") + ) + const lines = yield* Command.lines(command) + expect(Array.sort(files, Order.string)).toEqual(Array.sort(lines, Order.string)) + }).pipe(Effect.scoped) + )) +}) diff --git a/repos/effect/packages/platform-node-shared/test/FileSystem.test.ts b/repos/effect/packages/platform-node-shared/test/FileSystem.test.ts new file mode 100644 index 0000000..2a39d52 --- /dev/null +++ b/repos/effect/packages/platform-node-shared/test/FileSystem.test.ts @@ -0,0 +1,409 @@ +import * as NodeFileSystem from "@effect/platform-node-shared/NodeFileSystem" +import * as Fs from "@effect/platform/FileSystem" +import { assert, describe, expect, it } from "@effect/vitest" +import { pipe } from "effect" +import * as Chunk from "effect/Chunk" +import * as Effect from "effect/Effect" +import * as Fiber from "effect/Fiber" +import * as Stream from "effect/Stream" + +const runPromise = (self: Effect.Effect) => + Effect.runPromise( + Effect.provide(self, NodeFileSystem.layer) + ) + +describe("FileSystem", () => { + it("readFile", () => + runPromise(Effect.gen(function*() { + const fs = yield* Fs.FileSystem + const data = yield* fs.readFile(`${__dirname}/fixtures/text.txt`) + const text = new TextDecoder().decode(data) + expect(text.trim()).toEqual("lorem ipsum dolar sit amet") + }))) + + it("makeTempDirectory", () => + runPromise(Effect.gen(function*() { + const fs = yield* (Fs.FileSystem) + let dir = "" + yield* pipe( + Effect.gen(function*() { + dir = yield* fs.makeTempDirectory() + const stat = yield* fs.stat(dir) + expect(stat.type).toEqual("Directory") + }), + Effect.scoped + ) + const stat = yield* fs.stat(dir) + expect(stat.type).toEqual("Directory") + }))) + + it("makeTempDirectoryScoped", () => + runPromise(Effect.gen(function*() { + const fs = yield* Fs.FileSystem + let dir = "" + yield* pipe( + Effect.gen(function*() { + dir = yield* fs.makeTempDirectoryScoped() + const stat = yield* fs.stat(dir) + expect(stat.type).toEqual("Directory") + }), + Effect.scoped + ) + const error = yield* Effect.flip(fs.stat(dir)) + assert(error._tag === "SystemError" && error.reason === "NotFound") + }))) + + it("makeTempFile", () => + runPromise(Effect.gen(function*() { + const fs = yield* (Fs.FileSystem) + let file = "" + yield* pipe( + Effect.gen(function*() { + file = yield* fs.makeTempFile({ + "suffix": ".txt" + }) + expect(file.endsWith(".txt")).toBe(true) + const stat = yield* fs.stat(file) + expect(stat.type).toEqual("File") + }), + Effect.scoped + ) + const stat = yield* fs.stat(file) + expect(stat.type).toEqual("File") + }))) + + it("makeTempFileScoped", () => + runPromise(Effect.gen(function*() { + const fs = yield* (Fs.FileSystem) + let file = "" + yield* pipe( + Effect.gen(function*() { + file = yield* fs.makeTempFileScoped({ + "suffix": ".txt" + }) + expect(file.endsWith(".txt")).toBe(true) + const stat = yield* fs.stat(file) + expect(stat.type).toEqual("File") + }), + Effect.scoped + ) + const error = yield* Effect.flip(fs.stat(file)) + assert(error._tag === "SystemError" && error.reason === "NotFound") + }))) + + it("truncate", () => + runPromise(Effect.gen(function*() { + const fs = yield* Fs.FileSystem + const file = yield* fs.makeTempFile() + + const text = "hello world" + yield* fs.writeFile(file, new TextEncoder().encode(text)) + + const before = yield* pipe(fs.readFile(file), Effect.map((_) => new TextDecoder().decode(_))) + expect(before).toEqual(text) + + yield* fs.truncate(file) + + const after = yield* pipe(fs.readFile(file), Effect.map((_) => new TextDecoder().decode(_))) + expect(after).toEqual("") + }))) + + it("should track the cursor position when reading", () => + runPromise(Effect.gen(function*() { + const fs = yield* Fs.FileSystem + + yield* pipe( + Effect.gen(function*() { + let text: string + const file = yield* fs.open(`${__dirname}/fixtures/text.txt`) + + text = yield* pipe(Effect.flatten(file.readAlloc(Fs.Size(5))), Effect.map((_) => new TextDecoder().decode(_))) + expect(text).toBe("lorem") + + yield* file.seek(Fs.Size(7), "current") + text = yield* pipe(Effect.flatten(file.readAlloc(Fs.Size(5))), Effect.map((_) => new TextDecoder().decode(_))) + expect(text).toBe("dolar") + + yield* file.seek(Fs.Size(1), "current") + text = yield* pipe(Effect.flatten(file.readAlloc(Fs.Size(8))), Effect.map((_) => new TextDecoder().decode(_))) + expect(text).toBe("sit amet") + + yield* file.seek(Fs.Size(0), "start") + text = yield* pipe( + Effect.flatten(file.readAlloc(Fs.Size(11))), + Effect.map((_) => new TextDecoder().decode(_)) + ) + expect(text).toBe("lorem ipsum") + + text = yield* pipe( + fs.stream(`${__dirname}/fixtures/text.txt`, { offset: Fs.Size(6), bytesToRead: Fs.Size(5) }), + Stream.map((_) => new TextDecoder().decode(_)), + Stream.runCollect, + Effect.map(Chunk.join("")) + ) + expect(text).toBe("ipsum") + }), + Effect.scoped + ) + }))) + + it("should track the cursor position when writing", () => + runPromise(Effect.gen(function*() { + const fs = yield* Fs.FileSystem + + yield* pipe( + Effect.gen(function*() { + let text: string + const path = yield* fs.makeTempFileScoped() + const file = yield* fs.open(path, { flag: "w+" }) + + yield* file.write(new TextEncoder().encode("lorem ipsum")) + yield* file.write(new TextEncoder().encode(" ")) + yield* file.write(new TextEncoder().encode("dolor sit amet")) + text = yield* fs.readFileString(path) + expect(text).toBe("lorem ipsum dolor sit amet") + + yield* file.seek(Fs.Size(-4), "current") + yield* file.write(new TextEncoder().encode("hello world")) + text = yield* fs.readFileString(path) + expect(text).toBe("lorem ipsum dolor sit hello world") + + yield* file.seek(Fs.Size(6), "start") + yield* file.write(new TextEncoder().encode("blabl")) + text = yield* fs.readFileString(path) + expect(text).toBe("lorem blabl dolor sit hello world") + }), + Effect.scoped + ) + }))) + + it("should maintain a read cursor in append mode", () => + runPromise(Effect.gen(function*() { + const fs = yield* Fs.FileSystem + + yield* pipe( + Effect.gen(function*() { + let text: string + const path = yield* fs.makeTempFileScoped() + const file = yield* fs.open(path, { flag: "a+" }) + + yield* file.write(new TextEncoder().encode("foo")) + yield* file.seek(Fs.Size(0), "start") + + yield* file.write(new TextEncoder().encode("bar")) + text = yield* fs.readFileString(path) + expect(text).toBe("foobar") + + text = yield* pipe(Effect.flatten(file.readAlloc(Fs.Size(3))), Effect.map((_) => new TextDecoder().decode(_))) + expect(text).toBe("foo") + + yield* file.write(new TextEncoder().encode("baz")) + text = yield* fs.readFileString(path) + expect(text).toBe("foobarbaz") + + text = yield* pipe(Effect.flatten(file.readAlloc(Fs.Size(6))), Effect.map((_) => new TextDecoder().decode(_))) + expect(text).toBe("barbaz") + }), + Effect.scoped + ) + }))) + + it("should keep the current cursor if truncating doesn't affect it", () => + runPromise(Effect.gen(function*() { + const fs = yield* Fs.FileSystem + + yield* pipe( + Effect.gen(function*() { + const path = yield* fs.makeTempFileScoped() + const file = yield* fs.open(path, { flag: "w+" }) + + yield* file.write(new TextEncoder().encode("lorem ipsum dolor sit amet")) + yield* file.seek(Fs.Size(6), "start") + yield* file.truncate(Fs.Size(11)) + + const cursor = yield* file.seek(Fs.Size(0), "current") + expect(cursor).toBe(Fs.Size(6)) + }), + Effect.scoped + ) + }))) + + it("should update the current cursor if truncating affects it", () => + runPromise(Effect.gen(function*() { + const fs = yield* Fs.FileSystem + + yield* pipe( + Effect.gen(function*() { + const path = yield* fs.makeTempFileScoped() + const file = yield* fs.open(path, { flag: "w+" }) + + yield* file.write(new TextEncoder().encode("lorem ipsum dolor sit amet")) + yield* file.truncate(Fs.Size(11)) + + const cursor = yield* file.seek(Fs.Size(0), "current") + expect(cursor).toBe(Fs.Size(11)) + }), + Effect.scoped + ) + }))) + + describe("watch", () => { + // Simple test to verify watch API accepts recursive option + it("should accept recursive option", () => + runPromise(Effect.gen(function*() { + const fs = yield* Fs.FileSystem + + yield* Effect.scoped(Effect.gen(function*() { + const dir = yield* fs.makeTempDirectoryScoped() + + // Test that watch accepts no options + const fiber1 = yield* fs.watch(dir).pipe( + Stream.runDrain, + Effect.fork + ) + yield* Fiber.interrupt(fiber1) + + // Test that watch accepts recursive: true + const fiber2 = yield* fs.watch(dir, { recursive: true }).pipe( + Stream.runDrain, + Effect.fork + ) + yield* Fiber.interrupt(fiber2) + + // Test that watch accepts recursive: false + const fiber3 = yield* fs.watch(dir, { recursive: false }).pipe( + Stream.runDrain, + Effect.fork + ) + yield* Fiber.interrupt(fiber3) + })) + }))) + + it("should watch file changes in a directory", () => + runPromise(Effect.gen(function*() { + const fs = yield* Fs.FileSystem + + yield* Effect.scoped(Effect.gen(function*() { + const dir = yield* fs.makeTempDirectoryScoped() + let eventCount = 0 + + // Start watching the directory + const fiber = yield* fs.watch(dir).pipe( + Stream.tap((_event) => + Effect.sync(() => { + eventCount++ + }) + ), + Stream.runDrain, + Effect.fork + ) + + // Wait for the watcher to initialize + yield* Effect.sleep("500 millis") + + // Create a file - this should trigger an event + const testFile = `${dir}/test.txt` + yield* fs.writeFileString(testFile, "hello") + + // Wait for event to be processed + yield* Effect.sleep("1000 millis") + + // We should have received at least one event + expect(eventCount).toBeGreaterThan(0) + + yield* Fiber.interrupt(fiber) + })) + }))) + + it("should watch file changes recursively in subdirectories", () => + runPromise(Effect.gen(function*() { + const fs = yield* Fs.FileSystem + + yield* Effect.scoped(Effect.gen(function*() { + const dir = yield* fs.makeTempDirectoryScoped() + let eventCount = 0 + + // Create a subdirectory + const subdir = `${dir}/subdir` + yield* fs.makeDirectory(subdir) + + // Start watching with recursive option + const fiber = yield* fs.watch(dir, { recursive: true }).pipe( + Stream.tap((_event) => + Effect.sync(() => { + eventCount++ + }) + ), + Stream.runDrain, + Effect.fork + ) + + // Wait for the watcher to initialize + yield* Effect.sleep("500 millis") + + // Create a file in subdirectory - this should trigger an event + const testFile = `${subdir}/test.txt` + yield* fs.writeFileString(testFile, "hello from subdir") + + // Wait for event to be processed + yield* Effect.sleep("1000 millis") + + // We should have received at least one event + expect(eventCount).toBeGreaterThan(0) + + yield* Fiber.interrupt(fiber) + })) + }))) + + it("should not watch subdirectories when recursive is false", () => + runPromise(Effect.gen(function*() { + const fs = yield* Fs.FileSystem + + yield* Effect.scoped(Effect.gen(function*() { + const dir = yield* fs.makeTempDirectoryScoped() + let rootEventCount = 0 + let subEventCount = 0 + + // Create a subdirectory + const subdir = `${dir}/subdir` + yield* fs.makeDirectory(subdir) + + // Start watching WITHOUT recursive option (default is false) + const fiber = yield* fs.watch(dir, { recursive: false }).pipe( + Stream.tap((event) => + Effect.sync(() => { + if (event.path.includes("root.txt")) { + rootEventCount++ + } + if (event.path.includes("sub.txt")) { + subEventCount++ + } + }) + ), + Stream.runDrain, + Effect.fork + ) + + // Wait for the watcher to initialize + yield* Effect.sleep("500 millis") + + // Create a file in root directory - this SHOULD trigger an event + const rootFile = `${dir}/root.txt` + yield* fs.writeFileString(rootFile, "root file") + + // Create a file in subdirectory - this should NOT trigger an event + const subFile = `${subdir}/sub.txt` + yield* fs.writeFileString(subFile, "sub file") + + // Wait for events to be processed + yield* Effect.sleep("1000 millis") + + // We should have received events for root file but not subdirectory file + expect(rootEventCount).toBeGreaterThan(0) + expect(subEventCount).toBe(0) + + yield* Fiber.interrupt(fiber) + })) + }))) + }) +}) diff --git a/repos/effect/packages/platform-node-shared/test/KeyValueStore.test.ts b/repos/effect/packages/platform-node-shared/test/KeyValueStore.test.ts new file mode 100644 index 0000000..9a735c3 --- /dev/null +++ b/repos/effect/packages/platform-node-shared/test/KeyValueStore.test.ts @@ -0,0 +1,8 @@ +import * as KvN from "@effect/platform-node-shared/NodeKeyValueStore" +import { describe } from "@effect/vitest" +// @ts-ignore +import { testLayer } from "../../platform/test/KeyValueStore.test.js" + +const KeyValueLive = KvN.layerFileSystem(`${__dirname}/fixtures/kv`) + +describe.sequential("KeyValueStore / layerFileSystem", () => testLayer(KeyValueLive)) diff --git a/repos/effect/packages/platform-node-shared/test/PlatformConfigProvider.test.ts b/repos/effect/packages/platform-node-shared/test/PlatformConfigProvider.test.ts new file mode 100644 index 0000000..f478907 --- /dev/null +++ b/repos/effect/packages/platform-node-shared/test/PlatformConfigProvider.test.ts @@ -0,0 +1,50 @@ +import * as NodeFileSystem from "@effect/platform-node-shared/NodeFileSystem" +import * as NodePath from "@effect/platform-node-shared/NodePath" +import * as PlatformConfigProvider from "@effect/platform/PlatformConfigProvider" +import { assert, describe, it } from "@effect/vitest" +import { Config, ConfigProvider, Effect, Layer, pipe, Redacted } from "effect" + +const SetLive = PlatformConfigProvider.layerFileTree({ + rootDirectory: `${__dirname}/fixtures/config` +}).pipe( + Layer.provide(NodePath.layer), + Layer.provide(NodeFileSystem.layer) +) + +const AddLive = PlatformConfigProvider.layerFileTreeAdd({ + rootDirectory: `${__dirname}/fixtures/config` +}).pipe( + Layer.provide(NodePath.layer), + Layer.provide(NodeFileSystem.layer) +) + +describe("PlatformConfigProvider", () => { + it.effect("fromFileTree", () => + Effect.gen(function*() { + assert.strictEqual(Redacted.value(yield* Config.redacted("secret")), "keepitsafe") + assert.strictEqual(yield* Config.string("SHOUTING"), "value") + assert.strictEqual(yield* Config.integer("integer"), 123) + assert.strictEqual(yield* Config.string("nested/config"), "hello") + assert.strictEqual(yield* pipe(Config.string("config"), Config.nested("nested")), "hello") + const error = yield* pipe(Config.string("fallback"), Effect.flip) + assert.strictEqual(error._op, "MissingData") + }).pipe( + Effect.provide(SetLive), + Effect.withConfigProvider(ConfigProvider.fromJson({ + secret: "fail" + })) + )) + + it.effect("layerFileTreeAdd", () => + Effect.gen(function*() { + assert.strictEqual(Redacted.value(yield* Config.redacted("secret")), "shh") + assert.strictEqual(yield* Config.integer("integer"), 123) + assert.strictEqual(yield* Config.string("fallback"), "value") + }).pipe( + Effect.provide(AddLive), + Effect.withConfigProvider(ConfigProvider.fromJson({ + secret: "shh", + fallback: "value" + })) + )) +}) diff --git a/repos/effect/packages/platform-node-shared/test/Sink.test.ts b/repos/effect/packages/platform-node-shared/test/Sink.test.ts new file mode 100644 index 0000000..5dee1cd --- /dev/null +++ b/repos/effect/packages/platform-node-shared/test/Sink.test.ts @@ -0,0 +1,151 @@ +import * as NodeSink from "@effect/platform-node-shared/NodeSink" +import * as NodeStream from "@effect/platform-node-shared/NodeStream" +import { assert, describe, it } from "@effect/vitest" +import { pipe } from "effect" +import * as Chunk from "effect/Chunk" +import * as Data from "effect/Data" +import * as Effect from "effect/Effect" +import * as Stream from "effect/Stream" +import { createReadStream } from "fs" +import { join } from "path" +import { Writable } from "stream" +import Tar from "tar" + +const TEST_TARBALL = join(__dirname, "fixtures", "helloworld.tar.gz") + +describe("Sink", () => { + it("should write to a stream", () => + Effect.gen(function*() { + const items: Array = [] + let destroyed = false + yield* pipe( + Stream.make("a", "b", "c"), + Stream.run(NodeSink.fromWritable( + () => + new Writable({ + construct(callback) { + callback() + }, + write(chunk, _encoding, callback) { + items.push(chunk.toString()) + callback() + }, + destroy(_error, callback) { + destroyed = true + callback(null) + } + }), + () => "error" + )) + ) + assert.deepEqual(items, ["a", "b", "c"]) + assert.strictEqual(destroyed, true) + }).pipe(Effect.runPromise)) + + it("write error", () => + Effect.gen(function*() { + const items: Array = [] + let destroyed = false + const sink = NodeSink.fromWritable( + () => + new Writable({ + construct(callback) { + callback() + }, + write(chunk, _encoding, callback) { + items.push(chunk.toString()) + callback() + }, + destroy(_error, callback) { + destroyed = true + callback(null) + } + }), + () => "error" + ) + const result = yield* pipe(Stream.fail("a"), Stream.run(sink), Effect.flip) + assert.deepEqual(items, []) + assert.strictEqual(result, "a") + assert.strictEqual(destroyed, true) + }).pipe(Effect.runPromise)) + + it("endOnClose false", () => + Effect.gen(function*() { + const items: Array = [] + let destroyed = false + const sink = NodeSink.fromWritable( + () => + new Writable({ + construct(callback) { + callback() + }, + write(chunk, _encoding, callback) { + items.push(chunk.toString()) + callback() + }, + destroy(_error, callback) { + destroyed = true + callback(null) + } + }), + () => "error", + { endOnDone: false } + ) + yield* pipe(Stream.make("a", "b", "c"), Stream.run(sink)) + yield* Effect.sleep(0) + assert.deepEqual(items, ["a", "b", "c"]) + assert.strictEqual(destroyed, false) + }).pipe(Effect.runPromise)) + + it("should handle non-compliant node streams", () => + Effect.gen(function*() { + const stream = NodeStream.fromReadable<"error", Uint8Array>(() => createReadStream(TEST_TARBALL), () => "error") + const items = yield* pipe( + entries(stream), + Stream.flatMap((entry) => + NodeStream.fromReadable( + () => (entry as any), + (error) => new TarError({ error }) + ).pipe(Stream.map((content) => ({ path: entry.path, content: Buffer.from(content).toString("utf-8") }))) + ), + Stream.runCollect + ) + assert.deepEqual(Chunk.toReadonlyArray(items), [ + { path: "./tar/world.txt", content: "world\n" }, + { path: "./tar/hello.txt", content: "hello\n" } + ]) + }).pipe(Effect.runPromise)) +}) + +class TarError extends Data.TaggedError("TarError")<{ + readonly error: unknown +}> { +} + +const entries = ( + input: Stream.Stream +): Stream.Stream => + Stream.suspend(() => { + const parser = new Tar.Parse() + + const entries = Stream.async((emit) => { + parser.on("entry", (entry) => { + emit.single(entry) + }) + parser.on("close", () => { + emit.end() + }) + }) + + const pump = input.pipe( + Stream.run( + NodeSink.fromWritable( + () => parser, + (error) => new TarError({ error }) + ) + ), + Stream.zipRight(Stream.empty) + ) + + return Stream.merge(entries, pump) + }) diff --git a/repos/effect/packages/platform-node-shared/test/Stream.test.ts b/repos/effect/packages/platform-node-shared/test/Stream.test.ts new file mode 100644 index 0000000..58c049a --- /dev/null +++ b/repos/effect/packages/platform-node-shared/test/Stream.test.ts @@ -0,0 +1,183 @@ +import * as NodeStream from "@effect/platform-node-shared/NodeStream" +import { assert, describe, it } from "@effect/vitest" +import { Array, Channel, Chunk, identity, pipe, Stream } from "effect" +import * as Effect from "effect/Effect" +import { Duplex, Readable, Transform } from "stream" +import { createGzip, createUnzip } from "zlib" + +describe("Stream", () => { + it("should read a stream", () => + Effect.gen(function*() { + const stream = NodeStream.fromReadable<"error", string>(() => Readable.from(["a", "b", "c"]), () => "error") + const items = yield* Stream.runCollect(stream) + assert.deepEqual( + Chunk.toReadonlyArray(items), + ["a", "b", "c"] + ) + }).pipe(Effect.runPromise)) + + it("fromDuplex", () => + Effect.gen(function*() { + const channel = NodeStream.fromDuplex( + () => + new Transform({ + transform(chunk, _encoding, callback) { + callback(null, chunk.toString().toUpperCase()) + } + }), + () => "error" + ) + + const items = yield* pipe( + Stream.make("a", "b", "c"), + Stream.pipeThroughChannelOrFail(channel), + Stream.decodeText(), + Stream.mkString, + Stream.runCollect + ) + + assert.deepEqual( + Chunk.toReadonlyArray(items), + ["ABC"] + ) + }).pipe(Effect.runPromise)) + + it("fromDuplex failure", () => + Effect.gen(function*() { + const channel = NodeStream.fromDuplex( + () => + new Transform({ + transform(_chunk, _encoding, callback) { + callback(new Error()) + } + }), + () => "error" + ) + + const result = yield* pipe( + Stream.make("a", "b", "c"), + Stream.pipeThroughChannelOrFail(channel), + Stream.runDrain, + Effect.flip + ) + + assert.strictEqual(result, "error") + }).pipe(Effect.runPromise)) + + it("pipeThroughDuplex", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.make("a", "b", "c"), + NodeStream.pipeThroughDuplex( + () => + new Transform({ + transform(chunk, _encoding, callback) { + callback(null, chunk.toString().toUpperCase()) + } + }), + () => "error" as const + ), + Stream.decodeText(), + Stream.mkString, + Stream.runCollect + ) + + assert.deepEqual( + Chunk.toReadonlyArray(result), + ["ABC"] + ) + }).pipe(Effect.runPromise)) + + it("pipeThroughDuplex write error", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.make("a", "b", "c"), + NodeStream.pipeThroughDuplex( + () => + new Duplex({ + read() { + }, + write(_chunk, _encoding, callback) { + callback(new Error()) + } + }), + () => "error" as const + ), + Stream.runDrain, + Effect.flip + ) + + assert.strictEqual(result, "error") + }).pipe(Effect.runPromise)) + + it("pipeThroughSimple", () => + Effect.gen(function*() { + const result = yield* pipe( + Stream.make("a", Buffer.from("b"), "c"), + NodeStream.pipeThroughSimple( + () => + new Transform({ + transform(chunk, _encoding, callback) { + callback(null, chunk.toString().toUpperCase()) + } + }) + ), + Stream.decodeText(), + Stream.mkString, + Stream.runCollect + ) + + assert.deepEqual( + Chunk.toReadonlyArray(result), + ["ABC"] + ) + }).pipe(Effect.runPromise)) + + it("fromDuplex should work with node:zlib", () => + Effect.gen(function*() { + const text = "abcdefg1234567890" + const encoder = new TextEncoder() + const input = encoder.encode(text) + const stream = NodeStream.fromReadable<"error", Uint8Array>(() => Readable.from([input]), () => "error") + const deflate = NodeStream.fromDuplex<"error", "error", Uint8Array>(() => createGzip(), () => "error") + const inflate = NodeStream.fromDuplex(() => createUnzip(), () => "error") + const channel = Channel.pipeToOrFail(deflate, inflate) + const items = yield* pipe( + stream, + Stream.pipeThroughChannelOrFail(channel), + Stream.decodeText(), + Stream.mkString, + Stream.runCollect + ) + assert.deepEqual(Chunk.toReadonlyArray(items), [text]) + }).pipe(Effect.runPromise)) + + it("toReadable roundtrip", () => + Effect.gen(function*() { + const stream = Stream.range(0, 10000).pipe( + Stream.map((n) => String(n)) + ) + const readable = yield* NodeStream.toReadable(stream) + const outStream = NodeStream.fromReadable<"error", Uint8Array>(() => readable, () => "error") + const items = yield* pipe( + outStream, + Stream.decodeText(), + Stream.runCollect + ) + assert.strictEqual(Chunk.join(items, ""), Array.range(0, 10000).join("")) + }).pipe(Effect.runPromise)) + + it.effect("toReadable with error", () => + Effect.gen(function*() { + const stream = Stream.fail("error") + const readable = yield* NodeStream.toReadable(stream) + const error = yield* NodeStream.fromReadable( + () => readable, + identity + ).pipe( + Stream.runDrain, + Effect.flip + ) + assert.deepEqual(error, "error") + })) +}) diff --git a/repos/effect/packages/platform-node-shared/test/Terminal.test.ts b/repos/effect/packages/platform-node-shared/test/Terminal.test.ts new file mode 100644 index 0000000..79bb776 --- /dev/null +++ b/repos/effect/packages/platform-node-shared/test/Terminal.test.ts @@ -0,0 +1,32 @@ +import * as NodeTerminal from "@effect/platform-node-shared/NodeTerminal" +import * as Terminal from "@effect/platform/Terminal" +import { describe, expect, it } from "@effect/vitest" +import * as Effect from "effect/Effect" + +const runPromise = (self: Effect.Effect) => + Effect.runPromise( + Effect.provide(self, NodeTerminal.layer) + ) + +describe("Terminal", () => { + it("columns", () => + runPromise(Effect.gen(function*() { + const terminal = yield* Terminal.Terminal + const columns = yield* terminal.columns + expect(typeof columns).toEqual("number") + }))) + + it("rows", () => + runPromise(Effect.gen(function*() { + const terminal = yield* Terminal.Terminal + const rows = yield* terminal.rows + expect(typeof rows).toEqual("number") + }))) + + it("isTTY", () => + runPromise(Effect.gen(function*() { + const terminal = yield* Terminal.Terminal + const isTTY = yield* terminal.isTTY + expect(typeof isTTY).toEqual("boolean") + }))) +}) diff --git a/repos/effect/packages/platform-node-shared/test/fixtures/bash/duplex.sh b/repos/effect/packages/platform-node-shared/test/fixtures/bash/duplex.sh new file mode 100755 index 0000000..249a4de --- /dev/null +++ b/repos/effect/packages/platform-node-shared/test/fixtures/bash/duplex.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash + +function echoerr() { + echo "$@" 1>&2 +} + +echo "stdout1" +echoerr "stderr1" + +echo "stdout2" +echoerr "stderr2" diff --git a/repos/effect/packages/platform-node-shared/test/fixtures/bash/no-permissions.sh b/repos/effect/packages/platform-node-shared/test/fixtures/bash/no-permissions.sh new file mode 100644 index 0000000..2d7f858 --- /dev/null +++ b/repos/effect/packages/platform-node-shared/test/fixtures/bash/no-permissions.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash + +echo "this should not run because it doesn't have execute permissions" diff --git a/repos/effect/packages/platform-node-shared/test/fixtures/bash/non-zero-exit.sh b/repos/effect/packages/platform-node-shared/test/fixtures/bash/non-zero-exit.sh new file mode 100755 index 0000000..f019ff9 --- /dev/null +++ b/repos/effect/packages/platform-node-shared/test/fixtures/bash/non-zero-exit.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash + +exit 1 diff --git a/repos/effect/packages/platform-node-shared/test/fixtures/bash/parent-exits-early.sh b/repos/effect/packages/platform-node-shared/test/fixtures/bash/parent-exits-early.sh new file mode 100755 index 0000000..7f2eabb --- /dev/null +++ b/repos/effect/packages/platform-node-shared/test/fixtures/bash/parent-exits-early.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash + +# This script spawns child processes and then exits early +echo "Parent process started with PID $$" + +# Spawn multiple child processes that will outlive the parent +for i in {1..3}; do + ( + # Child process + child_pid=$BASHPID + echo "Child $i started with PID $child_pid" + + # Spawn a grandchild that runs for a long time + ( + grandchild_pid=$BASHPID + echo "Grandchild of child $i started with PID $grandchild_pid" + # Keep running for 30 seconds + sleep 30 + ) & + + # Keep the child running + sleep 30 + ) & +done + +# Give children time to start +sleep 0.5 + +# Exit early (simulating a crash or early termination) +echo "Parent exiting early with status 1..." +exit 1 diff --git a/repos/effect/packages/platform-node-shared/test/fixtures/bash/repeat.sh b/repos/effect/packages/platform-node-shared/test/fixtures/bash/repeat.sh new file mode 100755 index 0000000..3333fd9 --- /dev/null +++ b/repos/effect/packages/platform-node-shared/test/fixtures/bash/repeat.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash + +for i in {1..60}; do + echo "iteration: $i" + sleep 1 +done diff --git a/repos/effect/packages/platform-node-shared/test/fixtures/bash/spawn-children.sh b/repos/effect/packages/platform-node-shared/test/fixtures/bash/spawn-children.sh new file mode 100755 index 0000000..b192a58 --- /dev/null +++ b/repos/effect/packages/platform-node-shared/test/fixtures/bash/spawn-children.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash + +# This script spawns child processes to test process group cleanup +echo "Parent process started with PID $$" + +# Spawn multiple child processes +for i in {1..3}; do + ( + # Child process + child_pid=$BASHPID + echo "Child $i started with PID $child_pid" + + # Spawn a grandchild that runs for a long time + ( + grandchild_pid=$BASHPID + echo "Grandchild of child $i started with PID $grandchild_pid" + # Keep running for 60 seconds + for j in {1..60}; do + sleep 1 + done + ) & + + # Keep the child running + for j in {1..60}; do + sleep 1 + done + ) & +done + +# Keep the parent running +echo "Parent process waiting..." +wait diff --git a/repos/effect/packages/platform-node-shared/test/fixtures/config/SHOUTING b/repos/effect/packages/platform-node-shared/test/fixtures/config/SHOUTING new file mode 100644 index 0000000..6d4e150 --- /dev/null +++ b/repos/effect/packages/platform-node-shared/test/fixtures/config/SHOUTING @@ -0,0 +1 @@ +value diff --git a/repos/effect/packages/platform-node-shared/test/fixtures/config/integer b/repos/effect/packages/platform-node-shared/test/fixtures/config/integer new file mode 100644 index 0000000..190a180 --- /dev/null +++ b/repos/effect/packages/platform-node-shared/test/fixtures/config/integer @@ -0,0 +1 @@ +123 diff --git a/repos/effect/packages/platform-node-shared/test/fixtures/config/nested/config b/repos/effect/packages/platform-node-shared/test/fixtures/config/nested/config new file mode 100644 index 0000000..ce01362 --- /dev/null +++ b/repos/effect/packages/platform-node-shared/test/fixtures/config/nested/config @@ -0,0 +1 @@ +hello diff --git a/repos/effect/packages/platform-node-shared/test/fixtures/config/secret b/repos/effect/packages/platform-node-shared/test/fixtures/config/secret new file mode 100644 index 0000000..4a11bbc --- /dev/null +++ b/repos/effect/packages/platform-node-shared/test/fixtures/config/secret @@ -0,0 +1 @@ +keepitsafe diff --git a/repos/effect/packages/platform-node-shared/test/fixtures/helloworld.tar.gz b/repos/effect/packages/platform-node-shared/test/fixtures/helloworld.tar.gz new file mode 100644 index 0000000..7d47b90 Binary files /dev/null and b/repos/effect/packages/platform-node-shared/test/fixtures/helloworld.tar.gz differ diff --git a/repos/effect/packages/platform-node-shared/test/fixtures/text.txt b/repos/effect/packages/platform-node-shared/test/fixtures/text.txt new file mode 100644 index 0000000..72b190e --- /dev/null +++ b/repos/effect/packages/platform-node-shared/test/fixtures/text.txt @@ -0,0 +1 @@ +lorem ipsum dolar sit amet diff --git a/repos/effect/packages/platform-node-shared/tsconfig.build.json b/repos/effect/packages/platform-node-shared/tsconfig.build.json new file mode 100644 index 0000000..2f2dbea --- /dev/null +++ b/repos/effect/packages/platform-node-shared/tsconfig.build.json @@ -0,0 +1,17 @@ +{ + "extends": "./tsconfig.src.json", + "references": [ + { "path": "../cluster/tsconfig.build.json" }, + { "path": "../effect/tsconfig.build.json" }, + { "path": "../platform/tsconfig.build.json" }, + { "path": "../rpc/tsconfig.build.json" }, + { "path": "../sql/tsconfig.build.json" } + ], + "compilerOptions": { + "tsBuildInfoFile": ".tsbuildinfo/build.tsbuildinfo", + "outDir": "build/esm", + "declarationDir": "build/dts", + "stripInternal": true, + "types": ["node"] + } +} diff --git a/repos/effect/packages/platform-node-shared/tsconfig.json b/repos/effect/packages/platform-node-shared/tsconfig.json new file mode 100644 index 0000000..2c291d2 --- /dev/null +++ b/repos/effect/packages/platform-node-shared/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.base.json", + "include": [], + "references": [ + { "path": "tsconfig.src.json" }, + { "path": "tsconfig.test.json" } + ] +} diff --git a/repos/effect/packages/platform-node-shared/tsconfig.src.json b/repos/effect/packages/platform-node-shared/tsconfig.src.json new file mode 100644 index 0000000..719f012 --- /dev/null +++ b/repos/effect/packages/platform-node-shared/tsconfig.src.json @@ -0,0 +1,16 @@ +{ + "extends": "../../tsconfig.base.json", + "include": ["src"], + "references": [ + { "path": "../cluster/tsconfig.src.json" }, + { "path": "../effect/tsconfig.src.json" }, + { "path": "../platform/tsconfig.src.json" }, + { "path": "../rpc/tsconfig.src.json" }, + { "path": "../sql/tsconfig.src.json" } + ], + "compilerOptions": { + "tsBuildInfoFile": ".tsbuildinfo/src.tsbuildinfo", + "rootDir": "src", + "outDir": "build/src" + } +} diff --git a/repos/effect/packages/platform-node-shared/tsconfig.test.json b/repos/effect/packages/platform-node-shared/tsconfig.test.json new file mode 100644 index 0000000..c8636b1 --- /dev/null +++ b/repos/effect/packages/platform-node-shared/tsconfig.test.json @@ -0,0 +1,14 @@ +{ + "extends": "../../tsconfig.base.json", + "include": ["test"], + "references": [ + { "path": "tsconfig.src.json" }, + { "path": "../vitest/tsconfig.src.json" }, + { "path": "../effect/tsconfig.test.json" } // We import test files from `effect`. + ], + "compilerOptions": { + "tsBuildInfoFile": ".tsbuildinfo/test.tsbuildinfo", + "rootDir": "test", + "noEmit": true + } +} diff --git a/repos/effect/packages/platform-node-shared/vitest.config.ts b/repos/effect/packages/platform-node-shared/vitest.config.ts new file mode 100644 index 0000000..578d066 --- /dev/null +++ b/repos/effect/packages/platform-node-shared/vitest.config.ts @@ -0,0 +1,6 @@ +import { mergeConfig, type ViteUserConfig } from "vitest/config" +import shared from "../../vitest.shared.js" + +const config: ViteUserConfig = {} + +export default mergeConfig(shared, config) diff --git a/repos/effect/packages/platform-node/CHANGELOG.md b/repos/effect/packages/platform-node/CHANGELOG.md new file mode 100644 index 0000000..f89937e --- /dev/null +++ b/repos/effect/packages/platform-node/CHANGELOG.md @@ -0,0 +1,6011 @@ +# @effect/platform-node + +## 0.106.0 + +### Patch Changes + +- Updated dependencies [[`f7bb09b`](https://github.com/Effect-TS/effect/commit/f7bb09b022f195d1f2b3c23d49e74b011ec5d109), [`bd7552a`](https://github.com/Effect-TS/effect/commit/bd7552a19cc0ed575507ac6cc0879a57e24ebd31), [`ad1a7eb`](https://github.com/Effect-TS/effect/commit/ad1a7eb7f6bebaf91c80be2443ac0439226d0098), [`0d32048`](https://github.com/Effect-TS/effect/commit/0d32048f9836e2b23a6ba3ec5f43f0a000bb92fb), [`0d32048`](https://github.com/Effect-TS/effect/commit/0d32048f9836e2b23a6ba3ec5f43f0a000bb92fb)]: + - effect@3.21.0 + - @effect/cluster@0.58.0 + - @effect/platform@0.96.0 + - @effect/platform-node-shared@0.59.0 + - @effect/rpc@0.75.0 + - @effect/sql@0.51.0 + +## 0.105.0 + +### Patch Changes + +- Updated dependencies [[`fc82e81`](https://github.com/Effect-TS/effect/commit/fc82e81448bd9136a37580139ce46a2c61b11b54), [`82996bc`](https://github.com/Effect-TS/effect/commit/82996bce8debffcb44feb98bb862cf2662bd56b7), [`4d97a61`](https://github.com/Effect-TS/effect/commit/4d97a61a15b9dd6a0eece65b8f0c035e16d42ada), [`f6b0960`](https://github.com/Effect-TS/effect/commit/f6b0960bf3184109920dfed16ee7dfd7d67bc0f2), [`8798a84`](https://github.com/Effect-TS/effect/commit/8798a843218e6c0c0d3a8eee83360880e370b4da)]: + - effect@3.20.0 + - @effect/cluster@0.57.0 + - @effect/platform@0.95.0 + - @effect/platform-node-shared@0.58.0 + - @effect/rpc@0.74.0 + - @effect/sql@0.50.0 + +## 0.104.1 + +### Patch Changes + +- [#5977](https://github.com/Effect-TS/effect/pull/5977) [`118e7a4`](https://github.com/Effect-TS/effect/commit/118e7a4af5b86f6d707a40d3b03157b6bf5827e7) Thanks @scotttrinh! - Added `rows` and `isTTY` properties to `Terminal` + +- Updated dependencies [[`7e925ea`](https://github.com/Effect-TS/effect/commit/7e925eae4a9db556bcbf7e8b6a762ccf8588aa3b), [`118e7a4`](https://github.com/Effect-TS/effect/commit/118e7a4af5b86f6d707a40d3b03157b6bf5827e7), [`d7e75d6`](https://github.com/Effect-TS/effect/commit/d7e75d6d15294bbcd7ac49a0e9005848379ea86f), [`4860d1e`](https://github.com/Effect-TS/effect/commit/4860d1e09b436061ea4aeca07605a669793560fc)]: + - effect@3.19.15 + - @effect/platform-node-shared@0.57.1 + - @effect/platform@0.94.2 + +## 0.104.0 + +### Patch Changes + +- Updated dependencies [[`77eeb86`](https://github.com/Effect-TS/effect/commit/77eeb86ddf208e51ec25932af83d52d3b4700371), [`ff7053f`](https://github.com/Effect-TS/effect/commit/ff7053f6d8508567b6145239f97aacc5773b0c53), [`287c32c`](https://github.com/Effect-TS/effect/commit/287c32c9f10da8e96f2b9ef8424316189d9ad4b3)]: + - effect@3.19.13 + - @effect/platform@0.94.0 + - @effect/cluster@0.56.0 + - @effect/platform-node-shared@0.57.0 + - @effect/rpc@0.73.0 + - @effect/sql@0.49.0 + +## 0.103.0 + +### Patch Changes + +- Updated dependencies [[`811852a`](https://github.com/Effect-TS/effect/commit/811852a61868136bb7b3367450f02e5a8fb8a3f9)]: + - @effect/sql@0.48.6 + - @effect/cluster@0.55.0 + - @effect/platform-node-shared@0.56.0 + +## 0.102.0 + +### Patch Changes + +- Updated dependencies []: + - @effect/cluster@0.54.0 + - @effect/platform-node-shared@0.55.0 + +## 0.101.2 + +### Patch Changes + +- [#5797](https://github.com/Effect-TS/effect/pull/5797) [`8ebd29e`](https://github.com/Effect-TS/effect/commit/8ebd29ec10976222c200901d9b72779af743e6d5) Thanks @tim-smart! - use original status code if headers have already been sent + +- Updated dependencies [[`a2d965d`](https://github.com/Effect-TS/effect/commit/a2d965d2a22dcc018f81dbbcd55bfe33088d9411), [`8ebd29e`](https://github.com/Effect-TS/effect/commit/8ebd29ec10976222c200901d9b72779af743e6d5)]: + - @effect/cluster@0.53.6 + - @effect/platform@0.93.4 + +## 0.101.1 + +### Patch Changes + +- [#5783](https://github.com/Effect-TS/effect/pull/5783) [`8b879fb`](https://github.com/Effect-TS/effect/commit/8b879fb3b886a7262c9c8d9b2050cc128c5eb6f8) Thanks @tim-smart! - add EntityResource.makeK8sPod + +- Updated dependencies [[`8b879fb`](https://github.com/Effect-TS/effect/commit/8b879fb3b886a7262c9c8d9b2050cc128c5eb6f8)]: + - @effect/cluster@0.53.4 + +## 0.101.0 + +### Patch Changes + +- Updated dependencies [[`794c790`](https://github.com/Effect-TS/effect/commit/794c790d736f62784bff800fda5a656026d93749), [`079975c`](https://github.com/Effect-TS/effect/commit/079975c69d80c62461da5c51fe89e02c44dfa2ea), [`62f7636`](https://github.com/Effect-TS/effect/commit/62f76361ee01ed816687774c5302e7f8c5ff6a42)]: + - @effect/rpc@0.72.2 + - effect@3.19.5 + - @effect/cluster@0.53.0 + - @effect/platform-node-shared@0.54.0 + +## 0.100.0 + +### Patch Changes + +- Updated dependencies [[`571025c`](https://github.com/Effect-TS/effect/commit/571025ceaff6ef432a61bf65735a5a0f45118313), [`d43577b`](https://github.com/Effect-TS/effect/commit/d43577be59ae510812287b1cbffe6da15c040452)]: + - @effect/cluster@0.52.0 + - @effect/sql@0.48.0 + - @effect/rpc@0.72.1 + - @effect/platform-node-shared@0.53.0 + +## 0.99.0 + +### Minor Changes + +- [#5606](https://github.com/Effect-TS/effect/pull/5606) [`24a1685`](https://github.com/Effect-TS/effect/commit/24a1685c70a9ed157468650f95a5c3da3f2c2433) Thanks @tim-smart! - backport @effect/cluster from effect v4 + + @effect/cluster no longer requires a Shard Manager, and instead relies on the + `RunnerStorage` service to track runner state. + + To migrate, remove any Shard Manager deployments and use the updated layers in + `@effect/platform-node` or `@effect/platform-bun`. + + # Breaking Changes + - `ShardManager` module has been removed + - `EntityNotManagedByRunner` error has been removed + - Shard locks now use database advisory locks, which requires stable sessions + for database connections. This means load balancers or proxies that rotate + connections may cause issues. + - `@effect/platform-node/NodeClusterSocketRunner` is now + `@effect/cluster/NodeClusterSocket` + - `@effect/platform-node/NodeClusterHttpRunner` is now + `@effect/cluster/NodeClusterHttp` + - `@effect/platform-bun/BunClusterSocketRunner` is now + `@effect/cluster/BunClusterSocket` + - `@effect/platform-bun/BunClusterHttpRunner` is now + `@effect/cluster/BunClusterHttp` + + # New Features + - `RunnerHealth.layerK8s` has been added, which uses the Kubernetes API to track + runner health and liveness. To use it, you will need a service account with + permissions to read pod information. + +### Patch Changes + +- Updated dependencies [[`3c15d5f`](https://github.com/Effect-TS/effect/commit/3c15d5f99fb8d8470a00c5a33d9ba3cac89dfe4c), [`3863fa8`](https://github.com/Effect-TS/effect/commit/3863fa89f61e63e5529fd961e37333bddf7db64a), [`2a03c76`](https://github.com/Effect-TS/effect/commit/2a03c76c2781ca7e9e228e838eab2eb0d0795b1d), [`24a1685`](https://github.com/Effect-TS/effect/commit/24a1685c70a9ed157468650f95a5c3da3f2c2433), [`24a1685`](https://github.com/Effect-TS/effect/commit/24a1685c70a9ed157468650f95a5c3da3f2c2433)]: + - effect@3.19.0 + - @effect/platform-node-shared@0.52.0 + - @effect/cluster@0.51.0 + - @effect/rpc@0.72.0 + - @effect/platform@0.93.0 + - @effect/sql@0.47.0 + +## 0.98.4 + +### Patch Changes + +- [#5642](https://github.com/Effect-TS/effect/pull/5642) [`b8e3c6d`](https://github.com/Effect-TS/effect/commit/b8e3c6d510aec858ac34bfe5eb2b8fc5506fd669) Thanks @tim-smart! - fix ReferenceError in NodeSocket.fromNet + +- Updated dependencies [[`b8e3c6d`](https://github.com/Effect-TS/effect/commit/b8e3c6d510aec858ac34bfe5eb2b8fc5506fd669)]: + - @effect/platform-node-shared@0.51.6 + - @effect/cluster@0.50.6 + - @effect/rpc@0.71.1 + +## 0.98.3 + +### Patch Changes + +- [#5602](https://github.com/Effect-TS/effect/pull/5602) [`64b764b`](https://github.com/Effect-TS/effect/commit/64b764b3207eb13cacb13da31343aaf425e966bf) Thanks @tim-smart! - guard against race conditions in NodeSocketServer + +- Updated dependencies [[`64b764b`](https://github.com/Effect-TS/effect/commit/64b764b3207eb13cacb13da31343aaf425e966bf)]: + - @effect/cluster@0.50.3 + - @effect/platform-node-shared@0.51.3 + +## 0.98.2 + +### Patch Changes + +- [#5590](https://github.com/Effect-TS/effect/pull/5590) [`f4c4702`](https://github.com/Effect-TS/effect/commit/f4c4702ab01900c42c0af4662dfb7a5973619646) Thanks @tim-smart! - add openTimeout options to NodeSocket.makeNet + +- Updated dependencies [[`f4c4702`](https://github.com/Effect-TS/effect/commit/f4c4702ab01900c42c0af4662dfb7a5973619646), [`f6987c0`](https://github.com/Effect-TS/effect/commit/f6987c04ebf1386dc37729dfea1631ce364a5a96)]: + - @effect/platform-node-shared@0.51.2 + - @effect/cluster@0.50.2 + - @effect/platform@0.92.1 + +## 0.98.1 + +### Patch Changes + +- [#5585](https://github.com/Effect-TS/effect/pull/5585) [`cf17f2f`](https://github.com/Effect-TS/effect/commit/cf17f2f0319a57a886558b01549fea675cd78b69) Thanks @tim-smart! - keep socket error listener attached in NodeSocket + +- Updated dependencies [[`07802f7`](https://github.com/Effect-TS/effect/commit/07802f78fd410d800f0231129ee0866977399152), [`cf17f2f`](https://github.com/Effect-TS/effect/commit/cf17f2f0319a57a886558b01549fea675cd78b69)]: + - effect@3.18.1 + - @effect/platform-node-shared@0.51.1 + - @effect/cluster@0.50.1 + +## 0.98.0 + +### Patch Changes + +- Updated dependencies [[`1c6ab74`](https://github.com/Effect-TS/effect/commit/1c6ab74b314b2b6df8bb1b1a0cb9527ceda0e3fa), [`70fe803`](https://github.com/Effect-TS/effect/commit/70fe803469db3355ffbf8359b52c351f1c2dc137), [`c296e32`](https://github.com/Effect-TS/effect/commit/c296e32554143b84ae8987046984e1cf1852417c), [`a098ddf`](https://github.com/Effect-TS/effect/commit/a098ddfc551f5aa0a7c36f9b4928372a64d4d9f2)]: + - effect@3.18.0 + - @effect/platform@0.92.0 + - @effect/cluster@0.50.0 + - @effect/platform-node-shared@0.51.0 + - @effect/rpc@0.71.0 + - @effect/sql@0.46.0 + +## 0.97.1 + +### Patch Changes + +- [#5557](https://github.com/Effect-TS/effect/pull/5557) [`978b6ff`](https://github.com/Effect-TS/effect/commit/978b6ffc0b124d67d62a797211eff795f22cd1e6) Thanks @tim-smart! - allow NodeSocket.makeNet open to be interrupted + +- Updated dependencies [[`978b6ff`](https://github.com/Effect-TS/effect/commit/978b6ffc0b124d67d62a797211eff795f22cd1e6)]: + - @effect/platform-node-shared@0.50.1 + - @effect/cluster@0.49.1 + +## 0.97.0 + +### Patch Changes + +- Updated dependencies [[`d4d86a8`](https://github.com/Effect-TS/effect/commit/d4d86a81f02b94e09fce8004ce2c5369c505ca5a)]: + - @effect/platform@0.91.0 + - @effect/rpc@0.70.0 + - @effect/cluster@0.49.0 + - @effect/platform-node-shared@0.50.0 + - @effect/sql@0.45.0 + +## 0.96.1 + +### Patch Changes + +- [#5368](https://github.com/Effect-TS/effect/pull/5368) [`3b26094`](https://github.com/Effect-TS/effect/commit/3b2609409ac1e8c6939d699584f00b1b99c47e2e) Thanks @gcanti! - ## Annotation Behavior + + When you call `.annotations` on a schema, any identifier annotations that were previously set will now be removed. Identifiers are now always tied to the schema's `ast` reference (this was the intended behavior). + + **Example** + + ```ts + import { JSONSchema, Schema } from "effect" + + const schema = Schema.URL + + console.log(JSON.stringify(JSONSchema.make(schema), null, 2)) + /* + { + "$schema": "http://json-schema.org/draft-07/schema#", + "$defs": { + "URL": { + "type": "string", + "description": "a string to be decoded into a URL" + } + }, + "$ref": "#/$defs/URL" + } + */ + + const annotated = Schema.URL.annotations({ description: "description" }) + + console.log(JSON.stringify(JSONSchema.make(annotated), null, 2)) + /* + { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "string", + "description": "description" + } + */ + ``` + + ## OpenAPI 3.1 Compatibility + + OpenAPI 3.1 does not allow `nullable: true`. + Instead, the schema will now correctly use `{ "type": "null" }` inside a union. + + **Example** + + ```ts + import { JSONSchema, Schema } from "effect" + + const schema = Schema.NullOr(Schema.String) + + console.log( + JSON.stringify( + JSONSchema.fromAST(schema.ast, { + definitions: {}, + target: "openApi3.1" + }), + null, + 2 + ) + ) + /* + { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + */ + ``` + + ## Schema Description Deduplication + + Previously, when a schema was reused, only the first description was kept. + Now, every property keeps its own description, even if the schema is reused. + + **Example** + + ```ts + import { JSONSchema, Schema } from "effect" + + const schemaWithAnIdentifier = Schema.String.annotations({ + identifier: "my-id" + }) + + const schema = Schema.Struct({ + a: schemaWithAnIdentifier.annotations({ + description: "a-description" + }), + b: schemaWithAnIdentifier.annotations({ + description: "b-description" + }) + }) + + console.log(JSON.stringify(JSONSchema.make(schema), null, 2)) + /* + { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "required": [ + "a", + "b" + ], + "properties": { + "a": { + "type": "string", + "description": "a-description" + }, + "b": { + "type": "string", + "description": "b-description" + } + }, + "additionalProperties": false + } + */ + ``` + + ## Fragment Detection in Non-Refinement Schemas + + This patch fixes the issue where fragments (e.g. `jsonSchema.format`) were not detected on non-refinement schemas. + + **Example** + + ```ts + import { JSONSchema, Schema } from "effect" + + const schema = Schema.UUID.pipe( + Schema.compose(Schema.String), + Schema.annotations({ + identifier: "UUID", + title: "title", + description: "description", + jsonSchema: { + format: "uuid" // fragment + } + }) + ) + + console.log(JSON.stringify(JSONSchema.make(schema), null, 2)) + /* + { + "$schema": "http://json-schema.org/draft-07/schema#", + "$defs": { + "UUID": { + "type": "string", + "description": "description", + "format": "uuid", + "pattern": "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$", + "title": "title" + } + }, + "$ref": "#/$defs/UUID" + } + */ + ``` + + ## Nested Unions + + Nested unions are no longer flattened. Instead, they remain as nested `anyOf` arrays. + This is fine because JSON Schema allows nested `anyOf`. + + **Example** + + ```ts + import { JSONSchema, Schema } from "effect" + + const schema = Schema.Union( + Schema.NullOr(Schema.String), + Schema.Literal("a", null) + ) + + console.log(JSON.stringify(JSONSchema.make(schema), null, 2)) + /* + { + "$schema": "http://json-schema.org/draft-07/schema#", + "anyOf": [ + { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + { + "anyOf": [ + { + "type": "string", + "enum": [ + "a" + ] + }, + { + "type": "null" + } + ] + } + ] + } + */ + ``` + + ## Refinements without `jsonSchema` annotation + + Refinements that don't provide a `jsonSchema` annotation no longer cause errors. + They are simply ignored, so you can still generate a JSON Schema even when refinements can't easily be expressed. + +- Updated dependencies [[`3b26094`](https://github.com/Effect-TS/effect/commit/3b2609409ac1e8c6939d699584f00b1b99c47e2e), [`a33e491`](https://github.com/Effect-TS/effect/commit/a33e49153d944abd183fed93267fa7e52abae68b)]: + - effect@3.17.10 + +## 0.96.0 + +### Patch Changes + +- Updated dependencies [[`3e163b2`](https://github.com/Effect-TS/effect/commit/3e163b24cc2b647e25566ba29ef25c3f57609042)]: + - @effect/rpc@0.69.0 + - @effect/cluster@0.48.0 + - @effect/platform-node-shared@0.49.0 + +## 0.95.0 + +### Patch Changes + +- Updated dependencies [[`a949539`](https://github.com/Effect-TS/effect/commit/a94953971c2e908890dfda00f8560d317306c328), [`a949539`](https://github.com/Effect-TS/effect/commit/a94953971c2e908890dfda00f8560d317306c328)]: + - @effect/cluster@0.47.0 + - effect@3.17.7 + - @effect/platform-node-shared@0.48.0 + +## 0.94.2 + +### Patch Changes + +- [#5347](https://github.com/Effect-TS/effect/pull/5347) [`20f0d69`](https://github.com/Effect-TS/effect/commit/20f0d6978e0e98464f23b6582c37c6ce12319f29) Thanks @tim-smart! - update Cluster layer conditional storage types + +- Updated dependencies [[`d0b5fd1`](https://github.com/Effect-TS/effect/commit/d0b5fd1f7a292a47b9eeb058e5df57ace9a5ab14), [`20f0d69`](https://github.com/Effect-TS/effect/commit/20f0d6978e0e98464f23b6582c37c6ce12319f29)]: + - @effect/cluster@0.46.4 + - @effect/sql@0.44.1 + - @effect/platform-node-shared@0.47.2 + +## 0.94.1 + +### Patch Changes + +- [#5325](https://github.com/Effect-TS/effect/pull/5325) [`aed804e`](https://github.com/Effect-TS/effect/commit/aed804e396b96c00b032c3486d5e78189d4284e0) Thanks @tim-smart! - use Dispatcher.destroy() in undici finalizers + +- Updated dependencies [[`f187941`](https://github.com/Effect-TS/effect/commit/f187941946c675713b3539fc4d5480123037563a)]: + - effect@3.17.6 + +## 0.94.0 + +### Patch Changes + +- Updated dependencies [[`5a0f4f1`](https://github.com/Effect-TS/effect/commit/5a0f4f176687a39d9fa46bb894bb7ac3175b0e87), [`e9cbd26`](https://github.com/Effect-TS/effect/commit/e9cbd2673401723aa811b0535202e4f57baf6d2c)]: + - effect@3.17.1 + - @effect/rpc@0.68.0 + - @effect/cluster@0.46.0 + - @effect/platform-node-shared@0.47.0 + +## 0.93.0 + +### Patch Changes + +- Updated dependencies [[`7813640`](https://github.com/Effect-TS/effect/commit/7813640279d9e3a3e7fc0a29bfb5c6d5fb3c270f)]: + - @effect/platform@0.90.0 + - @effect/cluster@0.45.0 + - @effect/platform-node-shared@0.46.0 + - @effect/rpc@0.67.0 + - @effect/sql@0.44.0 + +## 0.92.0 + +### Patch Changes + +- Updated dependencies [[`40c3c87`](https://github.com/Effect-TS/effect/commit/40c3c875f724264312b43002859c82bed9ad0df9), [`ed2c74a`](https://github.com/Effect-TS/effect/commit/ed2c74ae8fa4ea0dd06ea84a3e58cd32e6916104), [`073a1b8`](https://github.com/Effect-TS/effect/commit/073a1b8be5dbfa87454393ee7346f5bc36a4fd63), [`f382e99`](https://github.com/Effect-TS/effect/commit/f382e99e409838a879246250fc3994b9bf5b3c2c), [`e8c7ba5`](https://github.com/Effect-TS/effect/commit/e8c7ba5fd3eb0c3ae3039fc24c09d69391987989), [`7e10415`](https://github.com/Effect-TS/effect/commit/7e1041599ade25103428703f5d2dfd7378a09636), [`e9bdece`](https://github.com/Effect-TS/effect/commit/e9bdececdc24f60a246be5055eca71a0d49ea7f2), [`8d95eb0`](https://github.com/Effect-TS/effect/commit/8d95eb0356b1d1736204836c275d201a547d208d)]: + - effect@3.17.0 + - @effect/cluster@0.44.0 + - @effect/platform@0.89.0 + - @effect/platform-node-shared@0.45.0 + - @effect/rpc@0.66.0 + - @effect/sql@0.43.0 + +## 0.91.0 + +### Patch Changes + +- Updated dependencies [[`f5dfabf`](https://github.com/Effect-TS/effect/commit/f5dfabf51ba481a4468c1509c537314978ef6cec), [`17a5ea8`](https://github.com/Effect-TS/effect/commit/17a5ea8fa29785fe6e4c9480f2a2e9c8c59f3f38), [`d25f22b`](https://github.com/Effect-TS/effect/commit/d25f22be7598abe977caf6cdac3b0dd78b438c48)]: + - effect@3.16.14 + - @effect/sql@0.42.0 + - @effect/platform@0.88.1 + - @effect/cluster@0.43.0 + - @effect/platform-node-shared@0.44.0 + - @effect/rpc@0.65.1 + +## 0.90.0 + +### Patch Changes + +- Updated dependencies [[`27206d7`](https://github.com/Effect-TS/effect/commit/27206d7f0558d7fe28de57bf54f1d0cc83acc92e), [`dbabf5e`](https://github.com/Effect-TS/effect/commit/dbabf5e76fa63b050d2b6c466713c7dc59f07d3c)]: + - @effect/platform@0.88.0 + - @effect/cluster@0.42.0 + - @effect/platform-node-shared@0.43.0 + - @effect/rpc@0.65.0 + - @effect/sql@0.41.0 + +## 0.89.6 + +### Patch Changes + +- Updated dependencies [[`c1c05a8`](https://github.com/Effect-TS/effect/commit/c1c05a8242fb5df7445b4a12387a60eac7726eb7), [`5b7cd92`](https://github.com/Effect-TS/effect/commit/5b7cd923e786c38a0802faf0fe75498ab3cccf28), [`81fe4a2`](https://github.com/Effect-TS/effect/commit/81fe4a2c81d5e30e180a60e68c52016a27b350db)]: + - effect@3.16.13 + - @effect/rpc@0.64.14 + - @effect/cluster@0.41.18 + - @effect/platform@0.87.13 + - @effect/platform-node-shared@0.42.18 + - @effect/sql@0.40.14 + +## 0.89.5 + +### Patch Changes + +- Updated dependencies [[`56b33c3`](https://github.com/Effect-TS/effect/commit/56b33c357cfc5f8976486f48e93032058c02d876)]: + - @effect/cluster@0.41.17 + - @effect/platform-node-shared@0.42.17 + +## 0.89.4 + +### Patch Changes + +- [#5174](https://github.com/Effect-TS/effect/pull/5174) [`d5e25b2`](https://github.com/Effect-TS/effect/commit/d5e25b237f05670ee42b386cb40b2cb448fc11d7) Thanks @schickling! - feat(platform): add recursive option to FileSystem.watch + + Added a `recursive` option to `FileSystem.watch` that allows watching for changes in subdirectories. When set to `true`, the watcher will monitor changes in all nested directories. + + Note: The `recursive` option behavior depends on the backend implementation. When using the default Node.js `fs.watch()` backend, the `recursive` option is supported on all platforms (Node.js v20+). When using `@parcel/watcher` (via `NodeFileSystem/ParcelWatcher` layer), watching is always recursive on all platforms and this option is ignored. + + Example: + + ```ts + import { FileSystem } from "@effect/platform" + import { Effect, Stream } from "effect" + + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem + + // Watch directory and all subdirectories + yield* fs + .watch("src", { recursive: true }) + .pipe(Stream.runForEach(console.log)) + }) + ``` + +- [#5194](https://github.com/Effect-TS/effect/pull/5194) [`5df2392`](https://github.com/Effect-TS/effect/commit/5df239272125eb6a2d50040adec9d0ed8a96950a) Thanks @tim-smart! - fix handling of failed streams in NodeHttpServer + +- Updated dependencies [[`32ba77a`](https://github.com/Effect-TS/effect/commit/32ba77ae304d2161362a73e8b61965332626cf2d), [`d5e25b2`](https://github.com/Effect-TS/effect/commit/d5e25b237f05670ee42b386cb40b2cb448fc11d7)]: + - @effect/platform@0.87.12 + - @effect/platform-node-shared@0.42.16 + - @effect/cluster@0.41.16 + - @effect/rpc@0.64.13 + - @effect/sql@0.40.13 + +## 0.89.3 + +### Patch Changes + +- Updated dependencies [[`79a1947`](https://github.com/Effect-TS/effect/commit/79a1947359cbd89a47ea315cdd86a3d250f28f43), [`001392b`](https://github.com/Effect-TS/effect/commit/001392ba8bfcad101bb034348a7415012fb12f72), [`7bfb099`](https://github.com/Effect-TS/effect/commit/7bfb099cb5528511b8d63045c4fbb4dc9cb18528)]: + - @effect/rpc@0.64.12 + - @effect/platform@0.87.11 + - @effect/cluster@0.41.15 + - @effect/platform-node-shared@0.42.15 + - @effect/sql@0.40.12 + +## 0.89.2 + +### Patch Changes + +- Updated dependencies [[`678318d`](https://github.com/Effect-TS/effect/commit/678318d2e88233156b006acda56c2d138ee3ffa0), [`678318d`](https://github.com/Effect-TS/effect/commit/678318d2e88233156b006acda56c2d138ee3ffa0)]: + - @effect/platform@0.87.10 + - @effect/cluster@0.41.14 + - @effect/platform-node-shared@0.42.14 + - @effect/rpc@0.64.11 + - @effect/sql@0.40.11 + +## 0.89.1 + +### Patch Changes + +- Updated dependencies [[`54514a2`](https://github.com/Effect-TS/effect/commit/54514a2f53166de27ad7e756dbf12194691fd4af)]: + - @effect/platform@0.87.9 + - @effect/cluster@0.41.13 + - @effect/platform-node-shared@0.42.13 + - @effect/rpc@0.64.10 + - @effect/sql@0.40.10 + +## 0.89.0 + +### Minor Changes + +- [#5168](https://github.com/Effect-TS/effect/pull/5168) [`2baddac`](https://github.com/Effect-TS/effect/commit/2baddac34760aefec27c530084ac5e47bd5b265f) Thanks @fubhy! - Consistent naming of `ClusterShardManager` options between `Bun` and `Node`. + + If you are currently using `NodeClusterShardManagerHttp`, please rename `options.protocol` to `options.transport`: + + ```ts + import * as NodeClusterShardManagerHttp from "@effect/platform-node/NodeClusterShardManagerHttp" + + // Before: + const manager = new NodeClusterShardManagerHttp.layer({ + protocol: "http" + // ... + }) + + // Before: + const manager = new NodeClusterShardManagerHttp.layer({ + transport: "http" + // ... + }) + ``` + +### Patch Changes + +- Updated dependencies [[`4ce4f82`](https://github.com/Effect-TS/effect/commit/4ce4f824f6fdef492be1d35c05a490ffce518c89)]: + - @effect/platform@0.87.8 + - @effect/cluster@0.41.12 + - @effect/platform-node-shared@0.42.12 + - @effect/rpc@0.64.9 + - @effect/sql@0.40.9 + +## 0.88.11 + +### Patch Changes + +- Updated dependencies [[`a9b617f`](https://github.com/Effect-TS/effect/commit/a9b617f125171ed76cd79ab46d7a924daf3b0e70), [`7e26e86`](https://github.com/Effect-TS/effect/commit/7e26e86524abcc93713d6ad7eee486638c98f7c2)]: + - @effect/platform@0.87.7 + - @effect/cluster@0.41.11 + - @effect/platform-node-shared@0.42.11 + - @effect/rpc@0.64.8 + - @effect/sql@0.40.8 + +## 0.88.10 + +### Patch Changes + +- Updated dependencies [[`905da99`](https://github.com/Effect-TS/effect/commit/905da996aad665057b4ca6dba1a4af44fb8835bd)]: + - effect@3.16.12 + - @effect/cluster@0.41.10 + - @effect/platform@0.87.6 + - @effect/platform-node-shared@0.42.10 + - @effect/rpc@0.64.7 + - @effect/sql@0.40.7 + +## 0.88.9 + +### Patch Changes + +- Updated dependencies []: + - @effect/sql@0.40.6 + - @effect/cluster@0.41.9 + - @effect/platform-node-shared@0.42.9 + +## 0.88.8 + +### Patch Changes + +- Updated dependencies [[`2fd8676`](https://github.com/Effect-TS/effect/commit/2fd8676c803cd40000dfc3231f5daecaa0e0ebd2)]: + - @effect/platform@0.87.5 + - @effect/cluster@0.41.8 + - @effect/platform-node-shared@0.42.8 + - @effect/rpc@0.64.6 + - @effect/sql@0.40.5 + +## 0.88.7 + +### Patch Changes + +- Updated dependencies [[`b01d2e0`](https://github.com/Effect-TS/effect/commit/b01d2e0d591418e10e9e362698205d848e97a9b7)]: + - @effect/cluster@0.41.7 + - @effect/platform-node-shared@0.42.7 + +## 0.88.6 + +### Patch Changes + +- Updated dependencies [[`7fdc16b`](https://github.com/Effect-TS/effect/commit/7fdc16bd88b872f5918384e4acda3731aab018da), [`e82a4fd`](https://github.com/Effect-TS/effect/commit/e82a4fd60f6528d08cef1a4aba0abe0d3ba741ad)]: + - @effect/cluster@0.41.6 + - @effect/platform@0.87.4 + - @effect/platform-node-shared@0.42.6 + - @effect/rpc@0.64.5 + - @effect/sql@0.40.4 + +## 0.88.5 + +### Patch Changes + +- Updated dependencies [[`1b6e396`](https://github.com/Effect-TS/effect/commit/1b6e396d699f3cbbc56b68f99055cf746529bb9e), [`46c3216`](https://github.com/Effect-TS/effect/commit/46c321657d93393506278327418e36f8e7a77f86)]: + - @effect/platform@0.87.3 + - @effect/sql@0.40.3 + - @effect/cluster@0.41.5 + - @effect/platform-node-shared@0.42.5 + - @effect/rpc@0.64.4 + +## 0.88.4 + +### Patch Changes + +- Updated dependencies [[`4fea68c`](https://github.com/Effect-TS/effect/commit/4fea68ca7a25a3c39a1ab68b3885534513ab0c81), [`b927954`](https://github.com/Effect-TS/effect/commit/b9279543cf5688dd8a577af80456959c615217d0), [`99590a6`](https://github.com/Effect-TS/effect/commit/99590a6ca9128eb1ede265b6670b655311995614), [`6c3e24c`](https://github.com/Effect-TS/effect/commit/6c3e24c2308f7d4a29b8f4270ab81bca22ac6bb4)]: + - @effect/platform@0.87.2 + - effect@3.16.11 + - @effect/cluster@0.41.4 + - @effect/platform-node-shared@0.42.4 + - @effect/rpc@0.64.3 + - @effect/sql@0.40.2 + +## 0.88.3 + +### Patch Changes + +- Updated dependencies [[`faad30e`](https://github.com/Effect-TS/effect/commit/faad30ec8742916be59f9db642d0fc98225b636c)]: + - effect@3.16.10 + - @effect/cluster@0.41.3 + - @effect/platform@0.87.1 + - @effect/platform-node-shared@0.42.3 + - @effect/rpc@0.64.2 + - @effect/sql@0.40.1 + +## 0.88.2 + +### Patch Changes + +- Updated dependencies [[`112a93a`](https://github.com/Effect-TS/effect/commit/112a93a9bab73e95e79f7b3502d1a7b1acd668fc)]: + - @effect/rpc@0.64.1 + - @effect/cluster@0.41.2 + - @effect/platform-node-shared@0.42.2 + +## 0.88.1 + +### Patch Changes + +- Updated dependencies [[`d5fd2c1`](https://github.com/Effect-TS/effect/commit/d5fd2c1526f06228853ed8317d9688c4af5f285a), [`9d189d7`](https://github.com/Effect-TS/effect/commit/9d189d744aa3307e055094c66f580453d95ff99d)]: + - @effect/cluster@0.41.1 + - @effect/platform-node-shared@0.42.1 + +## 0.88.0 + +### Patch Changes + +- Updated dependencies [[`b5bac9a`](https://github.com/Effect-TS/effect/commit/b5bac9ac2913fcd11b02322624f03b544eef53ba), [`867919c`](https://github.com/Effect-TS/effect/commit/867919c8be9a2f770699c0db852a3f566017ffd6)]: + - @effect/rpc@0.64.0 + - @effect/platform@0.87.0 + - @effect/cluster@0.41.0 + - @effect/platform-node-shared@0.42.0 + - @effect/sql@0.40.0 + +## 0.87.0 + +### Patch Changes + +- Updated dependencies [[`5137c70`](https://github.com/Effect-TS/effect/commit/5137c703461d8d3b363c112140a6e7f798241d07), [`c23d25c`](https://github.com/Effect-TS/effect/commit/c23d25c3e7c541f1f63b28484d8c461d86c67e99), [`5137c70`](https://github.com/Effect-TS/effect/commit/5137c703461d8d3b363c112140a6e7f798241d07), [`5137c70`](https://github.com/Effect-TS/effect/commit/5137c703461d8d3b363c112140a6e7f798241d07)]: + - effect@3.16.9 + - @effect/platform@0.86.0 + - @effect/cluster@0.40.0 + - @effect/platform-node-shared@0.41.0 + - @effect/rpc@0.63.0 + - @effect/sql@0.39.0 + +## 0.86.5 + +### Patch Changes + +- Updated dependencies [[`ff90206`](https://github.com/Effect-TS/effect/commit/ff90206fc56f5c1eb1675603652462a83a27421d)]: + - @effect/cluster@0.39.5 + - @effect/platform-node-shared@0.40.5 + +## 0.86.4 + +### Patch Changes + +- Updated dependencies [[`a8d99b2`](https://github.com/Effect-TS/effect/commit/a8d99b2ec2f55d9aa6e7d00a5138e80380716877)]: + - @effect/rpc@0.62.4 + - @effect/cluster@0.39.4 + - @effect/platform-node-shared@0.40.4 + +## 0.86.3 + +### Patch Changes + +- Updated dependencies [[`914a191`](https://github.com/Effect-TS/effect/commit/914a191e7cb6341a3d0e965bccd27c336cf22e44)]: + - @effect/platform@0.85.2 + - @effect/cluster@0.39.3 + - @effect/platform-node-shared@0.40.3 + - @effect/rpc@0.62.3 + - @effect/sql@0.38.2 + +## 0.86.2 + +### Patch Changes + +- Updated dependencies [[`ddfd1e4`](https://github.com/Effect-TS/effect/commit/ddfd1e43db60e3b779d18a221344423c5f3c7416)]: + - @effect/rpc@0.62.2 + - @effect/cluster@0.39.2 + - @effect/platform-node-shared@0.40.2 + +## 0.86.1 + +### Patch Changes + +- Updated dependencies [[`8cb98d5`](https://github.com/Effect-TS/effect/commit/8cb98d53e68330228287ce2a2e0d8a4c86bcab3b), [`db2dd3c`](https://github.com/Effect-TS/effect/commit/db2dd3c3a8a77d791eae19e66153527e1cde4e6e)]: + - effect@3.16.8 + - @effect/cluster@0.39.1 + - @effect/platform@0.85.1 + - @effect/platform-node-shared@0.40.1 + - @effect/rpc@0.62.1 + - @effect/sql@0.38.1 + +## 0.86.0 + +### Patch Changes + +- Updated dependencies [[`93687dd`](https://github.com/Effect-TS/effect/commit/93687ddbb25ce3b324cd2b83d2ccff225e97307e), [`93687dd`](https://github.com/Effect-TS/effect/commit/93687ddbb25ce3b324cd2b83d2ccff225e97307e), [`93687dd`](https://github.com/Effect-TS/effect/commit/93687ddbb25ce3b324cd2b83d2ccff225e97307e)]: + - @effect/platform@0.85.0 + - @effect/cluster@0.39.0 + - @effect/platform-node-shared@0.40.0 + - @effect/rpc@0.62.0 + - @effect/sql@0.38.0 + +## 0.85.16 + +### Patch Changes + +- Updated dependencies [[`1bb0d8a`](https://github.com/Effect-TS/effect/commit/1bb0d8ab96782e99434356266b38251554ea0294), [`cbac1ac`](https://github.com/Effect-TS/effect/commit/cbac1ac61a4e15ad15828563b39eef412bcee66e)]: + - effect@3.16.7 + - @effect/cluster@0.38.16 + - @effect/rpc@0.61.15 + - @effect/platform@0.84.11 + - @effect/platform-node-shared@0.39.16 + - @effect/sql@0.37.12 + +## 0.85.15 + +### Patch Changes + +- Updated dependencies [[`a5f7595`](https://github.com/Effect-TS/effect/commit/a5f75956ef9a15a83c416517ef493f0ee2f5ee8a), [`a02470c`](https://github.com/Effect-TS/effect/commit/a02470c75579e91525a25adb3f21b3650d042fdd), [`bf369b2`](https://github.com/Effect-TS/effect/commit/bf369b2902a0e0b195d957c18b9efd180942cf8b), [`f891d45`](https://github.com/Effect-TS/effect/commit/f891d45adffdafd3f94a2eca23faa354e3a409a8)]: + - effect@3.16.6 + - @effect/platform@0.84.10 + - @effect/cluster@0.38.15 + - @effect/platform-node-shared@0.39.15 + - @effect/rpc@0.61.14 + - @effect/sql@0.37.11 + +## 0.85.14 + +### Patch Changes + +- Updated dependencies [[`ee3a197`](https://github.com/Effect-TS/effect/commit/ee3a1973f54d7611ae99979edfed3020e94e1126), [`ee3a197`](https://github.com/Effect-TS/effect/commit/ee3a1973f54d7611ae99979edfed3020e94e1126)]: + - @effect/rpc@0.61.13 + - @effect/cluster@0.38.14 + - @effect/platform-node-shared@0.39.14 + +## 0.85.13 + +### Patch Changes + +- Updated dependencies [[`e0d3d42`](https://github.com/Effect-TS/effect/commit/e0d3d424d8f4e6a8ada017160406991f02b3c068)]: + - @effect/rpc@0.61.12 + - @effect/cluster@0.38.13 + - @effect/platform-node-shared@0.39.13 + +## 0.85.12 + +### Patch Changes + +- Updated dependencies [[`dca92fd`](https://github.com/Effect-TS/effect/commit/dca92fd8cf41f07561f55d863def5a9f62275f53)]: + - @effect/cluster@0.38.12 + - @effect/rpc@0.61.11 + - @effect/platform-node-shared@0.39.12 + +## 0.85.11 + +### Patch Changes + +- Updated dependencies [[`cc283b9`](https://github.com/Effect-TS/effect/commit/cc283b968235da3caf6c3e3a09b525fe09618fee)]: + - @effect/cluster@0.38.11 + - @effect/platform-node-shared@0.39.11 + +## 0.85.10 + +### Patch Changes + +- Updated dependencies [[`6e2e886`](https://github.com/Effect-TS/effect/commit/6e2e886f060c4ac057926b68d2e441c279480c30), [`bf418ef`](https://github.com/Effect-TS/effect/commit/bf418ef14a0f2ec965535793d5cea8fa8ba177ac)]: + - @effect/cluster@0.38.10 + - effect@3.16.5 + - @effect/platform-node-shared@0.39.10 + - @effect/platform@0.84.9 + - @effect/rpc@0.61.10 + - @effect/sql@0.37.10 + +## 0.85.9 + +### Patch Changes + +- Updated dependencies [[`7bf6cb9`](https://github.com/Effect-TS/effect/commit/7bf6cb943810e403f472a901ed29ccbbf76a46b2), [`7bf6cb9`](https://github.com/Effect-TS/effect/commit/7bf6cb943810e403f472a901ed29ccbbf76a46b2)]: + - @effect/rpc@0.61.9 + - @effect/cluster@0.38.9 + - @effect/platform-node-shared@0.39.9 + +## 0.85.8 + +### Patch Changes + +- Updated dependencies []: + - @effect/cluster@0.38.8 + - @effect/platform-node-shared@0.39.8 + +## 0.85.7 + +### Patch Changes + +- Updated dependencies [[`22166f8`](https://github.com/Effect-TS/effect/commit/22166f80c677cad6b4719e0e0253a9d06f964626)]: + - @effect/cluster@0.38.7 + - @effect/platform-node-shared@0.39.7 + +## 0.85.6 + +### Patch Changes + +- [#4998](https://github.com/Effect-TS/effect/pull/4998) [`f8ff7dc`](https://github.com/Effect-TS/effect/commit/f8ff7dccfe6ebd3409ab95c57f61764643d19a2b) Thanks @tim-smart! - expose MessageStorage in cluster clientOnly layers + +- Updated dependencies [[`f8ff7dc`](https://github.com/Effect-TS/effect/commit/f8ff7dccfe6ebd3409ab95c57f61764643d19a2b), [`8b9db77`](https://github.com/Effect-TS/effect/commit/8b9db7742846af0f58fd8e8b7acb7f4f5ff487ec)]: + - @effect/platform-node-shared@0.39.6 + - @effect/platform@0.84.8 + - @effect/cluster@0.38.6 + - @effect/rpc@0.61.8 + - @effect/sql@0.37.9 + +## 0.85.5 + +### Patch Changes + +- Updated dependencies [[`74ab9a0`](https://github.com/Effect-TS/effect/commit/74ab9a0a9e16d6e019369d256e1e24175c8bc3f3), [`770008e`](https://github.com/Effect-TS/effect/commit/770008eca3aad2899a2ed951236e575793294b28)]: + - effect@3.16.4 + - @effect/cluster@0.38.5 + - @effect/platform@0.84.7 + - @effect/platform-node-shared@0.39.5 + - @effect/rpc@0.61.7 + - @effect/sql@0.37.8 + +## 0.85.4 + +### Patch Changes + +- Updated dependencies [[`7e59d0e`](https://github.com/Effect-TS/effect/commit/7e59d0e2e004d86b8d0778e99c6fcd173fcb682a)]: + - @effect/cluster@0.38.4 + - @effect/platform-node-shared@0.39.4 + +## 0.85.3 + +### Patch Changes + +- Updated dependencies [[`59575c5`](https://github.com/Effect-TS/effect/commit/59575c5bf17a32c8b76c42e3794222b20e766581)]: + - @effect/cluster@0.38.3 + - @effect/platform-node-shared@0.39.3 + - @effect/sql@0.37.7 + +## 0.85.2 + +### Patch Changes + +- Updated dependencies [[`d244b63`](https://github.com/Effect-TS/effect/commit/d244b6345ea1d2ac88812562b0c170683913d502), [`ceea77a`](https://github.com/Effect-TS/effect/commit/ceea77a13055f145520f763e3fce5b8ff15d728f)]: + - @effect/cluster@0.38.2 + - @effect/platform@0.84.6 + - @effect/platform-node-shared@0.39.2 + - @effect/rpc@0.61.6 + - @effect/sql@0.37.6 + +## 0.85.1 + +### Patch Changes + +- Updated dependencies [[`612c739`](https://github.com/Effect-TS/effect/commit/612c73979abc44825feae573c8902b6484923aaa)]: + - @effect/cluster@0.38.1 + - @effect/platform-node-shared@0.39.1 + +## 0.85.0 + +### Patch Changes + +- Updated dependencies [[`3086405`](https://github.com/Effect-TS/effect/commit/308640563041004d790f08d2ba75cc3a85fdf752), [`ec52c6a`](https://github.com/Effect-TS/effect/commit/ec52c6a2211e76972462b15b9d5a9d6d56761b7a), [`71e1e6c`](https://github.com/Effect-TS/effect/commit/71e1e6c535c11a3ec498540a3af3c1a313a5319b), [`d0067ca`](https://github.com/Effect-TS/effect/commit/d0067caef053b2855d93dcef59ea585d0fad9d8c), [`8c79abe`](https://github.com/Effect-TS/effect/commit/8c79abeb47d070d8880b652d31626497d3005a4e)]: + - @effect/cluster@0.38.0 + - @effect/platform@0.84.5 + - @effect/rpc@0.61.5 + - @effect/platform-node-shared@0.39.0 + - @effect/sql@0.37.5 + +## 0.84.2 + +### Patch Changes + +- Updated dependencies [[`6dfbae9`](https://github.com/Effect-TS/effect/commit/6dfbae946ea12ecee7234f5785335f3e7f8335b4)]: + - @effect/cluster@0.37.2 + - @effect/platform-node-shared@0.38.2 + +## 0.84.1 + +### Patch Changes + +- Updated dependencies []: + - @effect/cluster@0.37.1 + - @effect/platform-node-shared@0.38.1 + +## 0.84.0 + +### Patch Changes + +- Updated dependencies [[`87722fc`](https://github.com/Effect-TS/effect/commit/87722fce693a9b49284bbddbf82d30714c688261), [`36217ee`](https://github.com/Effect-TS/effect/commit/36217eeb1337edd9ac3f9a635b80a6385d22ae8f), [`a116aea`](https://github.com/Effect-TS/effect/commit/a116aeade97c83d8c96f17cdc5cf3b5a0bd9be74)]: + - effect@3.16.3 + - @effect/rpc@0.61.4 + - @effect/cluster@0.37.0 + - @effect/platform@0.84.4 + - @effect/platform-node-shared@0.38.0 + - @effect/sql@0.37.4 + +## 0.83.3 + +### Patch Changes + +- Updated dependencies [[`ab7684f`](https://github.com/Effect-TS/effect/commit/ab7684f1c2a0671bf091f255d220e3a4cc7f528e)]: + - @effect/platform@0.84.3 + - @effect/cluster@0.36.3 + - @effect/platform-node-shared@0.37.3 + - @effect/rpc@0.61.3 + - @effect/sql@0.37.3 + +## 0.83.2 + +### Patch Changes + +- Updated dependencies [[`0ddf148`](https://github.com/Effect-TS/effect/commit/0ddf148a247aa87af043d276b8453a714a400897), [`a77afb1`](https://github.com/Effect-TS/effect/commit/a77afb1f7191a57a68b09fcdee5e9f27a0682b0a)]: + - effect@3.16.2 + - @effect/rpc@0.61.2 + - @effect/cluster@0.36.2 + - @effect/platform@0.84.2 + - @effect/platform-node-shared@0.37.2 + - @effect/sql@0.37.2 + +## 0.83.1 + +### Patch Changes + +- Updated dependencies [[`71174d0`](https://github.com/Effect-TS/effect/commit/71174d09691314a9b6b66189e456fd21e3eb6543), [`d615e6e`](https://github.com/Effect-TS/effect/commit/d615e6e5b944f6fd5e627e31752c7ca7e4e1c17d)]: + - @effect/platform@0.84.1 + - effect@3.16.1 + - @effect/cluster@0.36.1 + - @effect/platform-node-shared@0.37.1 + - @effect/rpc@0.61.1 + - @effect/sql@0.37.1 + +## 0.83.0 + +### Patch Changes + +- Updated dependencies [[`ee0bd5d`](https://github.com/Effect-TS/effect/commit/ee0bd5d24864752c54cb359f67a67dd903971ec4), [`5189800`](https://github.com/Effect-TS/effect/commit/51898004e11766b8cf6d95e960b636f6d5db79ec), [`58bfeaa`](https://github.com/Effect-TS/effect/commit/58bfeaa64ded8c88f772b184311c0c0dbac10960), [`194d748`](https://github.com/Effect-TS/effect/commit/194d7486943f56f3267ef415395ac220a4b3e634), [`918c9ea`](https://github.com/Effect-TS/effect/commit/918c9ea1a57facb154f0fb26792021f337054dee), [`9198e6f`](https://github.com/Effect-TS/effect/commit/9198e6fcc1a3ff4fefb3363004de558d8de01f40), [`2a370bf`](https://github.com/Effect-TS/effect/commit/2a370bf625fdeede5659721468eb0d527e403279), [`58ccb91`](https://github.com/Effect-TS/effect/commit/58ccb91328c8df5d49808b673738bc09df355201), [`fd47834`](https://github.com/Effect-TS/effect/commit/fd478348203fa89462b0a1d067ce4de034353df4)]: + - effect@3.16.0 + - @effect/cluster@0.36.0 + - @effect/platform@0.84.0 + - @effect/platform-node-shared@0.37.0 + - @effect/rpc@0.61.0 + - @effect/sql@0.37.0 + +## 0.82.0 + +### Patch Changes + +- Updated dependencies [[`5522520`](https://github.com/Effect-TS/effect/commit/55225206ab9af0ad60b1c0654690a8a096d625cd), [`cc5bb2b`](https://github.com/Effect-TS/effect/commit/cc5bb2b918a9450a975f702dabcea891bda382cb)]: + - @effect/platform@0.83.0 + - effect@3.15.5 + - @effect/cluster@0.35.0 + - @effect/platform-node-shared@0.36.0 + - @effect/rpc@0.60.0 + - @effect/sql@0.36.0 + +## 0.81.5 + +### Patch Changes + +- Updated dependencies [[`0617b9d`](https://github.com/Effect-TS/effect/commit/0617b9dc365f1963b36949ad7f9023ab6eb94524), [`58c5fd3`](https://github.com/Effect-TS/effect/commit/58c5fd3dd30eceb6c8afea90406768b0e348f48f)]: + - @effect/platform@0.82.8 + - @effect/cluster@0.34.5 + - @effect/platform-node-shared@0.35.5 + - @effect/rpc@0.59.9 + - @effect/sql@0.35.8 + +## 0.81.4 + +### Patch Changes + +- [#4921](https://github.com/Effect-TS/effect/pull/4921) [`c20b95a`](https://github.com/Effect-TS/effect/commit/c20b95a99ffe452b4774c844d397a905f713b6d6) Thanks @tim-smart! - update /platform dependencies + +- Updated dependencies [[`f570554`](https://github.com/Effect-TS/effect/commit/f57055459524587b041340577dad85476bb35f81), [`78047e8`](https://github.com/Effect-TS/effect/commit/78047e8dfc8005b66f87afe50bb95981fea51561), [`c20b95a`](https://github.com/Effect-TS/effect/commit/c20b95a99ffe452b4774c844d397a905f713b6d6), [`94ada43`](https://github.com/Effect-TS/effect/commit/94ada430928d5685bdbef513e87562c20774a3a2)]: + - effect@3.15.4 + - @effect/platform-node-shared@0.35.4 + - @effect/platform@0.82.7 + - @effect/cluster@0.34.4 + - @effect/rpc@0.59.8 + - @effect/sql@0.35.7 + +## 0.81.3 + +### Patch Changes + +- Updated dependencies [[`618903b`](https://github.com/Effect-TS/effect/commit/618903ba9ae96e2bfe6ee31f61c4359b915f2a36)]: + - @effect/platform@0.82.6 + - @effect/cluster@0.34.3 + - @effect/platform-node-shared@0.35.3 + - @effect/rpc@0.59.7 + - @effect/sql@0.35.6 + +## 0.81.2 + +### Patch Changes + +- Updated dependencies [[`7764a07`](https://github.com/Effect-TS/effect/commit/7764a07d960c60df81f14e1dc949518f4bbe494a), [`4577f54`](https://github.com/Effect-TS/effect/commit/4577f548d67273e576cdde423bdd34a4b910766a), [`30a0d9c`](https://github.com/Effect-TS/effect/commit/30a0d9cb51c84290d51b1361d72ff5cee33c13c7)]: + - @effect/platform@0.82.5 + - effect@3.15.3 + - @effect/cluster@0.34.2 + - @effect/platform-node-shared@0.35.2 + - @effect/rpc@0.59.6 + - @effect/sql@0.35.5 + +## 0.81.1 + +### Patch Changes + +- Updated dependencies [[`1627a02`](https://github.com/Effect-TS/effect/commit/1627a0299a07c3538ca15293f1ac3ffa7eeb45f3), [`d45e8a8`](https://github.com/Effect-TS/effect/commit/d45e8a8ac8227192f504e39e6d04fdcf4fb1d225), [`89657ac`](https://github.com/Effect-TS/effect/commit/89657ac2fbda9ba38ac2962ce96949e536a464f9), [`d13b68e`](https://github.com/Effect-TS/effect/commit/d13b68e3a9456d0bfee9bca8273a7b44a9c69087)]: + - @effect/cluster@0.34.1 + - @effect/platform@0.82.4 + - @effect/sql@0.35.4 + - @effect/platform-node-shared@0.35.1 + - @effect/rpc@0.59.5 + +## 0.81.0 + +### Patch Changes + +- Updated dependencies [[`b8722b8`](https://github.com/Effect-TS/effect/commit/b8722b817e2306fe8c8245f3f9e32d85b824b961), [`a328f4b`](https://github.com/Effect-TS/effect/commit/a328f4b4fe717dd53e5b04a30f387433c32f7328), [`eaf8405`](https://github.com/Effect-TS/effect/commit/eaf8405ab9bb52423050eb0d23dd7d3c21c18141)]: + - effect@3.15.2 + - @effect/platform@0.82.3 + - @effect/cluster@0.34.0 + - @effect/platform-node-shared@0.35.0 + - @effect/rpc@0.59.4 + - @effect/sql@0.35.3 + +## 0.80.3 + +### Patch Changes + +- Updated dependencies [[`739a3d4`](https://github.com/Effect-TS/effect/commit/739a3d4a4565915fe2e690003f4f9085cb4422fc)]: + - @effect/platform@0.82.2 + - @effect/cluster@0.33.3 + - @effect/platform-node-shared@0.34.3 + - @effect/rpc@0.59.3 + - @effect/sql@0.35.2 + +## 0.80.2 + +### Patch Changes + +- Updated dependencies [[`787ce70`](https://github.com/Effect-TS/effect/commit/787ce7042e35b657963473c6efe47752868cd811), [`1269641`](https://github.com/Effect-TS/effect/commit/1269641a99ae43069f7648ff79ffe8729b54b348), [`1269641`](https://github.com/Effect-TS/effect/commit/1269641a99ae43069f7648ff79ffe8729b54b348)]: + - effect@3.15.1 + - @effect/cluster@0.33.2 + - @effect/platform@0.82.1 + - @effect/platform-node-shared@0.34.2 + - @effect/rpc@0.59.2 + - @effect/sql@0.35.1 + +## 0.80.1 + +### Patch Changes + +- Updated dependencies [[`6495440`](https://github.com/Effect-TS/effect/commit/64954405eb57313722023b87c0d92761980e2713)]: + - @effect/rpc@0.59.1 + - @effect/cluster@0.33.1 + - @effect/platform-node-shared@0.34.1 + +## 0.80.0 + +### Patch Changes + +- Updated dependencies [[`c654595`](https://github.com/Effect-TS/effect/commit/c65459587b51da140b78098e81fdbfece65d53e2), [`d9f5dea`](https://github.com/Effect-TS/effect/commit/d9f5deae0f02f5de2b9fcb1cca8b142ba4bc2bba), [`49aa723`](https://github.com/Effect-TS/effect/commit/49aa7236a15e13f818c86edbca08c4af67c8dfaf), [`74c14d0`](https://github.com/Effect-TS/effect/commit/74c14d01d0cb48cf517a1b6e29a373a96ed0ff5b), [`e4f49b6`](https://github.com/Effect-TS/effect/commit/e4f49b66857e01b74ab6a9a0bc7132f44cd04cbb), [`6f02224`](https://github.com/Effect-TS/effect/commit/6f02224b3fc46a682ad2defb1a260841956c6780), [`1dcfd41`](https://github.com/Effect-TS/effect/commit/1dcfd41ff96abd706901293a00c1893cb29dd8fd), [`b21ab16`](https://github.com/Effect-TS/effect/commit/b21ab16b6f773e7ec4369db4e752c35e719f7870), [`fcf1822`](https://github.com/Effect-TS/effect/commit/fcf1822f98fcda60351d64e9d2c2c13563d7e6db), [`0061dd1`](https://github.com/Effect-TS/effect/commit/0061dd140740165e91569a684cce27a77b23229e), [`8421e6e`](https://github.com/Effect-TS/effect/commit/8421e6e49332bca8f96f482dfd48680e238b3a89), [`a9b3fb7`](https://github.com/Effect-TS/effect/commit/a9b3fb78abcfdb525318a956fd02fcadeb56143e), [`fa10f56`](https://github.com/Effect-TS/effect/commit/fa10f56b96bd9af070ba99ebc3279aa93954261e)]: + - effect@3.15.0 + - @effect/platform@0.82.0 + - @effect/cluster@0.33.0 + - @effect/platform-node-shared@0.34.0 + - @effect/rpc@0.59.0 + - @effect/sql@0.35.0 + +## 0.79.0 + +### Patch Changes + +- Updated dependencies [[`cd6cd0e`](https://github.com/Effect-TS/effect/commit/cd6cd0eacd6b09d6dd48b30b32edeb4a3c3075f9)]: + - @effect/rpc@0.58.0 + - @effect/cluster@0.32.0 + - @effect/platform-node-shared@0.33.0 + +## 0.78.1 + +### Patch Changes + +- Updated dependencies [[`24a9ebb`](https://github.com/Effect-TS/effect/commit/24a9ebbb5af598f0bfd6ecc45307e528043fe011)]: + - effect@3.14.22 + - @effect/cluster@0.31.1 + - @effect/platform@0.81.1 + - @effect/platform-node-shared@0.32.1 + - @effect/rpc@0.57.1 + - @effect/sql@0.34.1 + +## 0.78.0 + +### Patch Changes + +- Updated dependencies [[`672920f`](https://github.com/Effect-TS/effect/commit/672920f85da8abd5f9d4ad85e29248a2aca57ed8)]: + - @effect/platform@0.81.0 + - @effect/cluster@0.31.0 + - @effect/platform-node-shared@0.32.0 + - @effect/rpc@0.57.0 + - @effect/sql@0.34.0 + +## 0.77.11 + +### Patch Changes + +- Updated dependencies [[`2f3b7d4`](https://github.com/Effect-TS/effect/commit/2f3b7d4e1fa1ef8790b0ca4da22eb88872ee31df)]: + - effect@3.14.21 + - @effect/cluster@0.30.11 + - @effect/platform@0.80.21 + - @effect/platform-node-shared@0.31.11 + - @effect/rpc@0.56.9 + - @effect/sql@0.33.21 + +## 0.77.10 + +### Patch Changes + +- Updated dependencies [[`17e2f30`](https://github.com/Effect-TS/effect/commit/17e2f3091408cf0fca9414d4af3bdf7b2765b378)]: + - effect@3.14.20 + - @effect/cluster@0.30.10 + - @effect/platform@0.80.20 + - @effect/platform-node-shared@0.31.10 + - @effect/rpc@0.56.8 + - @effect/sql@0.33.20 + +## 0.77.9 + +### Patch Changes + +- Updated dependencies [[`2d55bc5`](https://github.com/Effect-TS/effect/commit/2d55bc52c596afd8381f8ad1badc69efa0be8a78), [`114dad9`](https://github.com/Effect-TS/effect/commit/114dad9a93613986eb5d306cbcfda3fb37ec1a1b)]: + - @effect/cluster@0.30.9 + - @effect/platform-node-shared@0.31.9 + +## 0.77.8 + +### Patch Changes + +- Updated dependencies [[`1b30f61`](https://github.com/Effect-TS/effect/commit/1b30f616e75580933284657cb2cefab5a7903323)]: + - @effect/cluster@0.30.8 + - @effect/platform-node-shared@0.31.8 + +## 0.77.7 + +### Patch Changes + +- Updated dependencies [[`146af39`](https://github.com/Effect-TS/effect/commit/146af39d8d3b4e82aceb13de9749e6c4120c580b), [`056a910`](https://github.com/Effect-TS/effect/commit/056a910d0a0b8b00b0dc9df4a070466b2b5c2f6c), [`e25e7bb`](https://github.com/Effect-TS/effect/commit/e25e7bbc1797733916f48f501425d9f2ef310d9f), [`3273d57`](https://github.com/Effect-TS/effect/commit/3273d572c2b3175a842677f19efeea4cd65ab016)]: + - @effect/cluster@0.30.7 + - effect@3.14.19 + - @effect/platform@0.80.19 + - @effect/platform-node-shared@0.31.7 + - @effect/rpc@0.56.7 + - @effect/sql@0.33.19 + +## 0.77.6 + +### Patch Changes + +- Updated dependencies [[`b1164d4`](https://github.com/Effect-TS/effect/commit/b1164d49a1dfdf299e9971367b6fc6be4df0ddff)]: + - effect@3.14.18 + - @effect/cluster@0.30.6 + - @effect/platform@0.80.18 + - @effect/platform-node-shared@0.31.6 + - @effect/rpc@0.56.6 + - @effect/sql@0.33.18 + +## 0.77.5 + +### Patch Changes + +- Updated dependencies [[`0b54681`](https://github.com/Effect-TS/effect/commit/0b54681cd89245e211d8f49272be0f1bf2f81813), [`41a59d5`](https://github.com/Effect-TS/effect/commit/41a59d5916a296b12b0d5ead9e859e05f40b4cce)]: + - effect@3.14.17 + - @effect/cluster@0.30.5 + - @effect/platform@0.80.17 + - @effect/platform-node-shared@0.31.5 + - @effect/rpc@0.56.5 + - @effect/sql@0.33.17 + +## 0.77.4 + +### Patch Changes + +- Updated dependencies [[`ee14444`](https://github.com/Effect-TS/effect/commit/ee144441021ec77039e43396eaf90714687bb495), [`f1c8583`](https://github.com/Effect-TS/effect/commit/f1c8583f8c3ea9415f813795ca2940a897c9ba9a)]: + - effect@3.14.16 + - @effect/platform@0.80.16 + - @effect/cluster@0.30.4 + - @effect/platform-node-shared@0.31.4 + - @effect/rpc@0.56.4 + - @effect/sql@0.33.16 + +## 0.77.3 + +### Patch Changes + +- Updated dependencies [[`239cc99`](https://github.com/Effect-TS/effect/commit/239cc995ce645946210a3c3d2cb52bd3547c0687), [`8b6c947`](https://github.com/Effect-TS/effect/commit/8b6c947eaa8e45a67ecb3c37d45cd27f3e41d165), [`c50a63b`](https://github.com/Effect-TS/effect/commit/c50a63bbecb9f560b9cae349c447eed877d1b9b6)]: + - effect@3.14.15 + - @effect/cluster@0.30.3 + - @effect/platform@0.80.15 + - @effect/platform-node-shared@0.31.3 + - @effect/rpc@0.56.3 + - @effect/sql@0.33.15 + +## 0.77.2 + +### Patch Changes + +- Updated dependencies [[`664293f`](https://github.com/Effect-TS/effect/commit/664293f975a282920a7208e966adaf4634c42ef4), [`6ed8d15`](https://github.com/Effect-TS/effect/commit/6ed8d1589beb181d30abc79afebdaabc1d101538)]: + - @effect/cluster@0.30.2 + - effect@3.14.14 + - @effect/platform-node-shared@0.31.2 + - @effect/platform@0.80.14 + - @effect/rpc@0.56.2 + - @effect/sql@0.33.14 + +## 0.77.1 + +### Patch Changes + +- Updated dependencies [[`ee77788`](https://github.com/Effect-TS/effect/commit/ee77788747e7ebbde6bfa88256cde49dbbad3608), [`5fce6ba`](https://github.com/Effect-TS/effect/commit/5fce6ba19c3cc63cc0104e737e581ad989dedbf0), [`570e45f`](https://github.com/Effect-TS/effect/commit/570e45f8cb936e42ec48f67f21bb2b7252f36c0c)]: + - effect@3.14.13 + - @effect/cluster@0.30.1 + - @effect/platform@0.80.13 + - @effect/platform-node-shared@0.31.1 + - @effect/rpc@0.56.1 + - @effect/sql@0.33.13 + +## 0.77.0 + +### Patch Changes + +- Updated dependencies [[`d6e1156`](https://github.com/Effect-TS/effect/commit/d6e115617fc1a26a846b55f407965a330145dbee), [`2c66c16`](https://github.com/Effect-TS/effect/commit/2c66c16375dc2fe128f7b4e78c5f5c27c25c0d19)]: + - @effect/rpc@0.56.0 + - @effect/cluster@0.30.0 + - @effect/platform-node-shared@0.31.0 + +## 0.76.22 + +### Patch Changes + +- Updated dependencies [[`c2ad9ee`](https://github.com/Effect-TS/effect/commit/c2ad9ee9f3c4c743390edf35ed9e85a20be33811), [`9c68654`](https://github.com/Effect-TS/effect/commit/9c686542b6eb3ea188cb70673ef2e41223633e89)]: + - effect@3.14.12 + - @effect/cluster@0.29.22 + - @effect/platform@0.80.12 + - @effect/platform-node-shared@0.30.22 + - @effect/rpc@0.55.17 + - @effect/sql@0.33.12 + +## 0.76.21 + +### Patch Changes + +- [#4758](https://github.com/Effect-TS/effect/pull/4758) [`b5ad11e`](https://github.com/Effect-TS/effect/commit/b5ad11e511424c6d5c32e34e7ee9d04f0110617d) Thanks @tim-smart! - add child_process workers to platform-node + +- Updated dependencies [[`e536127`](https://github.com/Effect-TS/effect/commit/e536127c1e6f2fb3a542c73ae919435a629a346b), [`b5ad11e`](https://github.com/Effect-TS/effect/commit/b5ad11e511424c6d5c32e34e7ee9d04f0110617d)]: + - effect@3.14.11 + - @effect/rpc@0.55.16 + - @effect/cluster@0.29.21 + - @effect/platform@0.80.11 + - @effect/platform-node-shared@0.30.21 + - @effect/sql@0.33.11 + +## 0.76.20 + +### Patch Changes + +- Updated dependencies [[`d3df84e`](https://github.com/Effect-TS/effect/commit/d3df84e8af8e00a297e2329faeae625de0a95a71)]: + - @effect/rpc@0.55.15 + - @effect/cluster@0.29.20 + - @effect/platform-node-shared@0.30.20 + +## 0.76.19 + +### Patch Changes + +- Updated dependencies [[`bc7efa3`](https://github.com/Effect-TS/effect/commit/bc7efa3b031bb25e1ed3c8f2d3fb5e8da166cadc)]: + - effect@3.14.10 + - @effect/cluster@0.29.19 + - @effect/platform@0.80.10 + - @effect/platform-node-shared@0.30.19 + - @effect/rpc@0.55.14 + - @effect/sql@0.33.10 + +## 0.76.18 + +### Patch Changes + +- Updated dependencies [[`d78249f`](https://github.com/Effect-TS/effect/commit/d78249f0b67f63cf4baf806ff090cba33293daf0)]: + - effect@3.14.9 + - @effect/cluster@0.29.18 + - @effect/platform@0.80.9 + - @effect/platform-node-shared@0.30.18 + - @effect/rpc@0.55.13 + - @effect/sql@0.33.9 + +## 0.76.17 + +### Patch Changes + +- Updated dependencies [[`58eaca9`](https://github.com/Effect-TS/effect/commit/58eaca9ef14032fc310f4a0e3c09513bac1cb50a)]: + - @effect/rpc@0.55.12 + - @effect/cluster@0.29.17 + - @effect/platform-node-shared@0.30.17 + +## 0.76.16 + +### Patch Changes + +- Updated dependencies [[`a79b732`](https://github.com/Effect-TS/effect/commit/a79b732bddea8bfca091c4fed0dd87aa0b1ab1f0), [`a79b732`](https://github.com/Effect-TS/effect/commit/a79b732bddea8bfca091c4fed0dd87aa0b1ab1f0)]: + - @effect/cluster@0.29.16 + - @effect/platform-node-shared@0.30.16 + +## 0.76.15 + +### Patch Changes + +- Updated dependencies [[`6966708`](https://github.com/Effect-TS/effect/commit/6966708a3061a3eb4bcfcb4d5877657fb41a019a)]: + - @effect/cluster@0.29.15 + - @effect/platform-node-shared@0.30.15 + +## 0.76.14 + +### Patch Changes + +- Updated dependencies [[`da21953`](https://github.com/Effect-TS/effect/commit/da21953a3831bf5974ab6add8fcc7fad1c0ba472)]: + - @effect/cluster@0.29.14 + - @effect/platform-node-shared@0.30.14 + +## 0.76.13 + +### Patch Changes + +- Updated dependencies [[`b3a2d32`](https://github.com/Effect-TS/effect/commit/b3a2d32772e6f7f20eacf2e18128e99324c4d378), [`896fbbf`](https://github.com/Effect-TS/effect/commit/896fbbf6ed6c11e099747e8aafb67b28edc4e466)]: + - effect@3.14.8 + - @effect/cluster@0.29.13 + - @effect/platform@0.80.8 + - @effect/platform-node-shared@0.30.13 + - @effect/rpc@0.55.11 + - @effect/sql@0.33.8 + +## 0.76.12 + +### Patch Changes + +- Updated dependencies [[`b542a4b`](https://github.com/Effect-TS/effect/commit/b542a4bf195be0c9af1523e1ba96c953decc4d25)]: + - effect@3.14.7 + - @effect/cluster@0.29.12 + - @effect/platform@0.80.7 + - @effect/platform-node-shared@0.30.12 + - @effect/rpc@0.55.10 + - @effect/sql@0.33.7 + +## 0.76.11 + +### Patch Changes + +- Updated dependencies [[`a1d4673`](https://github.com/Effect-TS/effect/commit/a1d4673a423dfed050c0a762664d9d64002cfa90)]: + - @effect/rpc@0.55.9 + - @effect/cluster@0.29.11 + - @effect/platform-node-shared@0.30.11 + +## 0.76.10 + +### Patch Changes + +- Updated dependencies [[`47618c1`](https://github.com/Effect-TS/effect/commit/47618c1ad84ebcc5a51133a3fff5aa5012d49d45), [`6077882`](https://github.com/Effect-TS/effect/commit/60778824a4794336c33807801f813f8751d1c7e4)]: + - effect@3.14.6 + - @effect/cluster@0.29.10 + - @effect/platform@0.80.6 + - @effect/platform-node-shared@0.30.10 + - @effect/rpc@0.55.8 + - @effect/sql@0.33.6 + +## 0.76.9 + +### Patch Changes + +- Updated dependencies [[`4414042`](https://github.com/Effect-TS/effect/commit/44140423a2fb185f92f7db4d5b383f9b62a97bf9)]: + - @effect/rpc@0.55.7 + - @effect/cluster@0.29.9 + - @effect/platform-node-shared@0.30.9 + +## 0.76.8 + +### Patch Changes + +- [#4653](https://github.com/Effect-TS/effect/pull/4653) [`868d5c2`](https://github.com/Effect-TS/effect/commit/868d5c247bf4bf5103331e4e617d0886a31d8cc1) Thanks @nuckolp-amzn! - Fix: Handle empty chunks in http server stream bodies + +- Updated dependencies [[`40dbfef`](https://github.com/Effect-TS/effect/commit/40dbfeff239b6e567706752114f31b2fce7de4e3), [`85fba81`](https://github.com/Effect-TS/effect/commit/85fba815ac07eb13d4227a69ac76a18e4b94df18), [`5a5ebdd`](https://github.com/Effect-TS/effect/commit/5a5ebdddfaddd259538b4599a6676281faca778e)]: + - effect@3.14.5 + - @effect/platform@0.80.5 + - @effect/cluster@0.29.8 + - @effect/platform-node-shared@0.30.8 + - @effect/rpc@0.55.6 + - @effect/sql@0.33.5 + +## 0.76.7 + +### Patch Changes + +- Updated dependencies [[`e4ba2c6`](https://github.com/Effect-TS/effect/commit/e4ba2c66a878e81b5e295d6d49aaf724b80a28ef), [`e3e5873`](https://github.com/Effect-TS/effect/commit/e3e5873f30080bb0e5eed8a876170acaa6ed47ff), [`26c060c`](https://github.com/Effect-TS/effect/commit/26c060c65914a623220a20356991784f974bfe18)]: + - effect@3.14.4 + - @effect/rpc@0.55.5 + - @effect/cluster@0.29.7 + - @effect/platform@0.80.4 + - @effect/platform-node-shared@0.30.7 + - @effect/sql@0.33.4 + +## 0.76.6 + +### Patch Changes + +- Updated dependencies [[`0ec5e03`](https://github.com/Effect-TS/effect/commit/0ec5e0353a1db5d27c3500deba0df61001258e76), [`05c4d77`](https://github.com/Effect-TS/effect/commit/05c4d772acc42b7425add7b22f914c5ee3ff84bd), [`37aa8e1`](https://github.com/Effect-TS/effect/commit/37aa8e137725a902e70cd1e468ea98b873aa5056), [`34f03d6`](https://github.com/Effect-TS/effect/commit/34f03d66875f21f266f102223a03cd14c2ed6ea6)]: + - @effect/rpc@0.55.4 + - effect@3.14.3 + - @effect/cluster@0.29.6 + - @effect/platform-node-shared@0.30.6 + - @effect/platform@0.80.3 + - @effect/sql@0.33.3 + +## 0.76.5 + +### Patch Changes + +- Updated dependencies [[`f87991b`](https://github.com/Effect-TS/effect/commit/f87991b6d8a2edfaf90b01cebda4b466992ae865), [`f87991b`](https://github.com/Effect-TS/effect/commit/f87991b6d8a2edfaf90b01cebda4b466992ae865), [`0a3e3e1`](https://github.com/Effect-TS/effect/commit/0a3e3e18eea5e0d1882f1a6c906198e6ef226a41)]: + - effect@3.14.2 + - @effect/cluster@0.29.5 + - @effect/platform@0.80.2 + - @effect/platform-node-shared@0.30.5 + - @effect/rpc@0.55.3 + - @effect/sql@0.33.2 + +## 0.76.4 + +### Patch Changes + +- Updated dependencies [[`d2f11e5`](https://github.com/Effect-TS/effect/commit/d2f11e557de4639762124951252170fbf4d7c906)]: + - @effect/rpc@0.55.2 + - @effect/cluster@0.29.4 + - @effect/platform-node-shared@0.30.4 + +## 0.76.3 + +### Patch Changes + +- [#4634](https://github.com/Effect-TS/effect/pull/4634) [`168e388`](https://github.com/Effect-TS/effect/commit/168e3888258d38bce9f2619ca72bc1dc35268bb9) Thanks @tim-smart! - ensure upgrade socket is closed on http response finish + +- Updated dependencies [[`18a7936`](https://github.com/Effect-TS/effect/commit/18a7936832158daa69e3c09a6caae55e3d6c0b86)]: + - @effect/cluster@0.29.3 + - @effect/platform-node-shared@0.30.3 + +## 0.76.2 + +### Patch Changes + +- Updated dependencies [[`3a99a2d`](https://github.com/Effect-TS/effect/commit/3a99a2dbaa38348c1f6e210a531fcfb99b5e73c5)]: + - @effect/cluster@0.29.2 + - @effect/platform-node-shared@0.30.2 + +## 0.76.1 + +### Patch Changes + +- Updated dependencies [[`4a274fe`](https://github.com/Effect-TS/effect/commit/4a274fe9f623182b6b902827e0e83bd89ca3b05c), [`814733f`](https://github.com/Effect-TS/effect/commit/814733fe62bb3dc91c6cd632d16a8d2076b3755b)]: + - effect@3.14.1 + - @effect/cluster@0.29.1 + - @effect/platform@0.80.1 + - @effect/platform-node-shared@0.30.1 + - @effect/rpc@0.55.1 + - @effect/sql@0.33.1 + +## 0.76.0 + +### Minor Changes + +- [#4469](https://github.com/Effect-TS/effect/pull/4469) [`3131f8f`](https://github.com/Effect-TS/effect/commit/3131f8fd12ba9eb31b90fa2f42bf88b12309133c) Thanks @tim-smart! - refactor of @effect/cluster packages + +### Patch Changes + +- [#4469](https://github.com/Effect-TS/effect/pull/4469) [`aba2d1d`](https://github.com/Effect-TS/effect/commit/aba2d1d831ea149481bd4dd755528c0afa8239ce) Thanks @tim-smart! - Move SocketServer modules to @effect/platform + +- Updated dependencies [[`1f47e4e`](https://github.com/Effect-TS/effect/commit/1f47e4e12546ab691b29bfb7b5128bb17b93baa5), [`26dd75f`](https://github.com/Effect-TS/effect/commit/26dd75f276a0d8a63eab313bd5a167d5072c9780), [`aba2d1d`](https://github.com/Effect-TS/effect/commit/aba2d1d831ea149481bd4dd755528c0afa8239ce), [`3131f8f`](https://github.com/Effect-TS/effect/commit/3131f8fd12ba9eb31b90fa2f42bf88b12309133c), [`aba2d1d`](https://github.com/Effect-TS/effect/commit/aba2d1d831ea149481bd4dd755528c0afa8239ce), [`aba2d1d`](https://github.com/Effect-TS/effect/commit/aba2d1d831ea149481bd4dd755528c0afa8239ce), [`04dff2d`](https://github.com/Effect-TS/effect/commit/04dff2d01ac68c260f29a6d4743381825c353c86), [`c7fac0c`](https://github.com/Effect-TS/effect/commit/c7fac0cd7eadcd5cc0c3a987051c5b57ad271638), [`aba2d1d`](https://github.com/Effect-TS/effect/commit/aba2d1d831ea149481bd4dd755528c0afa8239ce), [`ffaa3f3`](https://github.com/Effect-TS/effect/commit/ffaa3f3969df26610fcc02ad537340641d44e803), [`ab957c1`](https://github.com/Effect-TS/effect/commit/ab957c1fee714868f56c7ab4e802b9d449e9b666), [`35db9ce`](https://github.com/Effect-TS/effect/commit/35db9ce228f1416c8abacc6dc9c36fbd0f33ef0f), [`aba2d1d`](https://github.com/Effect-TS/effect/commit/aba2d1d831ea149481bd4dd755528c0afa8239ce), [`cf77ea9`](https://github.com/Effect-TS/effect/commit/cf77ea9ab4fc89e66a43f682a9926ccdee6c57ed), [`26dd75f`](https://github.com/Effect-TS/effect/commit/26dd75f276a0d8a63eab313bd5a167d5072c9780), [`baaab60`](https://github.com/Effect-TS/effect/commit/baaab60b737f35dfab8e4a21bce28a195d19e899)]: + - effect@3.14.0 + - @effect/platform@0.80.0 + - @effect/platform-node-shared@0.30.0 + - @effect/cluster@0.29.0 + - @effect/rpc@0.55.0 + - @effect/sql@0.33.0 + +## 0.75.4 + +### Patch Changes + +- Updated dependencies [[`5662363`](https://github.com/Effect-TS/effect/commit/566236361e270e575ef1cbf308ad1967c82a362c), [`5f1fd15`](https://github.com/Effect-TS/effect/commit/5f1fd15308ab154791580059b89877d19a2055c2), [`8bb1460`](https://github.com/Effect-TS/effect/commit/8bb1460c824f66f0f25ebd899c5e74e388089c37)]: + - @effect/platform@0.79.4 + - @effect/platform-node-shared@0.29.4 + +## 0.75.3 + +### Patch Changes + +- Updated dependencies [[`0c4803f`](https://github.com/Effect-TS/effect/commit/0c4803fcc69262d11a97ce49d0e9b4288df0651f), [`6f65ac4`](https://github.com/Effect-TS/effect/commit/6f65ac4eac1489cd6ea390e18b0908670722adad)]: + - effect@3.13.12 + - @effect/platform@0.79.3 + - @effect/platform-node-shared@0.29.3 + +## 0.75.2 + +### Patch Changes + +- Updated dependencies [[`fad8cca`](https://github.com/Effect-TS/effect/commit/fad8cca9bbfcc2eaeb44b97c15dbe0a1eda75315), [`4296293`](https://github.com/Effect-TS/effect/commit/4296293049414d0cf2d915a26c552b09f946b9a0), [`9c241ab`](https://github.com/Effect-TS/effect/commit/9c241abe47ccf7a5257b98a4a64a63054a12741d), [`082b0c1`](https://github.com/Effect-TS/effect/commit/082b0c1b9f4252bcdd69608f2e4a9226f953ac3f), [`be12983`](https://github.com/Effect-TS/effect/commit/be12983bc7e7537b41cd8910fc4eb7d1da56ab07), [`de88127`](https://github.com/Effect-TS/effect/commit/de88127a5a5906ccece98af74787b5ae0e65e431)]: + - effect@3.13.11 + - @effect/platform@0.79.2 + - @effect/platform-node-shared@0.29.2 + +## 0.75.1 + +### Patch Changes + +- Updated dependencies [[`527c964`](https://github.com/Effect-TS/effect/commit/527c9645229f5be9714a7e60a38a9e753c4bbfb1)]: + - effect@3.13.10 + - @effect/platform@0.79.1 + - @effect/platform-node-shared@0.29.1 + +## 0.75.0 + +### Minor Changes + +- [#4573](https://github.com/Effect-TS/effect/pull/4573) [`88fe129`](https://github.com/Effect-TS/effect/commit/88fe12923740765c0335a6e6203fdcc6a463edca) Thanks @tim-smart! - remove Scope from HttpClient requirements + + Before: + + ```ts + import { HttpClient } from "@effect/platform" + import { Effect } from "effect" + + Effect.gen(function* () { + const client = yield* HttpClient.HttpClient + const response = yield* client.get("https://api.github.com/users/octocat") + return yield* response.json + }).pipe(Effect.scoped) + ``` + + After: + + ```ts + import { HttpClient } from "@effect/platform" + import { Effect } from "effect" + + Effect.gen(function* () { + const client = yield* HttpClient.HttpClient + const response = yield* client.get("https://api.github.com/users/octocat") + return yield* response.json + }) // no need to add Effect.scoped + ``` + +### Patch Changes + +- [#4580](https://github.com/Effect-TS/effect/pull/4580) [`bbdc279`](https://github.com/Effect-TS/effect/commit/bbdc2795a461cb2d1fe19b2669526a6ef590c3d4) Thanks @tim-smart! - prevent worker handler interrupts from shutting down runner + +- [#4583](https://github.com/Effect-TS/effect/pull/4583) [`d630249`](https://github.com/Effect-TS/effect/commit/d630249426113088abe8b382db4f14d80f2160c2) Thanks @tim-smart! - support Layer.launch when using WorkerRunner + +- Updated dependencies [[`88fe129`](https://github.com/Effect-TS/effect/commit/88fe12923740765c0335a6e6203fdcc6a463edca), [`d630249`](https://github.com/Effect-TS/effect/commit/d630249426113088abe8b382db4f14d80f2160c2), [`2976e52`](https://github.com/Effect-TS/effect/commit/2976e52538d9dc9ffdcbc84d4ac748cff9305971)]: + - @effect/platform@0.79.0 + - effect@3.13.9 + - @effect/platform-node-shared@0.29.0 + +## 0.74.1 + +### Patch Changes + +- Updated dependencies [[`c65d336`](https://github.com/Effect-TS/effect/commit/c65d3362d07ec815ff3b46278314e8a31706ddc2), [`22d2ebb`](https://github.com/Effect-TS/effect/commit/22d2ebb4b11f5a44351a4736e65da391a3b647d0)]: + - effect@3.13.8 + - @effect/platform@0.78.1 + - @effect/platform-node-shared@0.28.1 + +## 0.74.0 + +### Patch Changes + +- Updated dependencies [[`c5bcf53`](https://github.com/Effect-TS/effect/commit/c5bcf53b7cb49dacffdd2a6cd8eb48cc452b417e)]: + - @effect/platform-node-shared@0.28.0 + - @effect/platform@0.78.0 + +## 0.73.7 + +### Patch Changes + +- [#4539](https://github.com/Effect-TS/effect/pull/4539) [`99fcbf7`](https://github.com/Effect-TS/effect/commit/99fcbf712d40a90ac5c8843237d26914146d7312) Thanks @schickling! - Fixed interruption of timeout in worker shutdown + +- Updated dependencies [[`05306d5`](https://github.com/Effect-TS/effect/commit/05306d5cc55b94a23c175de798fc6a5e93a3ab74), [`840cc73`](https://github.com/Effect-TS/effect/commit/840cc7329908db7ca693ef47b07d4f845c29cadd), [`9bf8a74`](https://github.com/Effect-TS/effect/commit/9bf8a74b967f18d931743dd5196af326c9118e9c), [`87ba23c`](https://github.com/Effect-TS/effect/commit/87ba23c41c193503ed0c612b0d32d0b253794c64), [`f910880`](https://github.com/Effect-TS/effect/commit/f91088069057f3b4529753f5bc5532b028d726df), [`0d01480`](https://github.com/Effect-TS/effect/commit/0d014803e4f688f74386a80abd65485e1a319244)]: + - @effect/platform-node-shared@0.27.7 + - @effect/platform@0.77.7 + - effect@3.13.7 + +## 0.73.6 + +### Patch Changes + +- Updated dependencies [[`3154ce4`](https://github.com/Effect-TS/effect/commit/3154ce4692fa18b804982158d3c4c8a8a5fae386)]: + - effect@3.13.6 + - @effect/platform@0.77.6 + - @effect/platform-node-shared@0.27.6 + +## 0.73.5 + +### Patch Changes + +- Updated dependencies [[`367bb35`](https://github.com/Effect-TS/effect/commit/367bb35f4c2a254e1fb211d96db2474a7aed9020), [`6cf11c3`](https://github.com/Effect-TS/effect/commit/6cf11c3a75773ceec2877c85ddc760f381f0866d), [`a0acec8`](https://github.com/Effect-TS/effect/commit/a0acec851f72e19466363d24b9cc218acd00006a)]: + - effect@3.13.5 + - @effect/platform@0.77.5 + - @effect/platform-node-shared@0.27.5 + +## 0.73.4 + +### Patch Changes + +- Updated dependencies [[`e0746f9`](https://github.com/Effect-TS/effect/commit/e0746f9aa398b69c6542e375910683bf17f49f46), [`17d9e89`](https://github.com/Effect-TS/effect/commit/17d9e89f9851663bdbb6c1e685601d97806114a4)]: + - @effect/platform@0.77.4 + - effect@3.13.4 + - @effect/platform-node-shared@0.27.4 + +## 0.73.3 + +### Patch Changes + +- Updated dependencies [[`cc5588d`](https://github.com/Effect-TS/effect/commit/cc5588df07f9103513547cb429ce041b9436a8bd), [`623c8cd`](https://github.com/Effect-TS/effect/commit/623c8cd053ed6ee3d353aaa8778d484670fca2bb), [`00b4eb1`](https://github.com/Effect-TS/effect/commit/00b4eb1ece12a16e222e6220965bb4024d6752ac), [`f2aee98`](https://github.com/Effect-TS/effect/commit/f2aee989b0a600900ce83e7f460d02908620c80f), [`fb798eb`](https://github.com/Effect-TS/effect/commit/fb798eb9061f1191badc017d1aa649360254da20), [`2251b15`](https://github.com/Effect-TS/effect/commit/2251b1528810bb695b37ce388b653cec0c5bf80c), [`2e15c1e`](https://github.com/Effect-TS/effect/commit/2e15c1e33648add0b29fe274fbcb7294b7515085), [`a4979db`](https://github.com/Effect-TS/effect/commit/a4979db021aef16e731be64df196b72088fc4376), [`b74255a`](https://github.com/Effect-TS/effect/commit/b74255a304ad49d60bedb1a260fd697f370af27a), [`d7f6a5c`](https://github.com/Effect-TS/effect/commit/d7f6a5c7d26c1963dcd864ca62360d20d08c7b49), [`9dd8979`](https://github.com/Effect-TS/effect/commit/9dd8979e940915b1cc1b1f264f3d019c77a65a02), [`477b488`](https://github.com/Effect-TS/effect/commit/477b488284f47c5469d7fba3e4065fb7e3b6556e), [`10932cb`](https://github.com/Effect-TS/effect/commit/10932cbf58fc721ada631cebec42f773ce96d3cc), [`9f6c784`](https://github.com/Effect-TS/effect/commit/9f6c78468b3b5e9ebfc38ffdfb70702901ee977b), [`2c639ec`](https://github.com/Effect-TS/effect/commit/2c639ecee332de4266e36022c989c35ae4e02105), [`886aaa8`](https://github.com/Effect-TS/effect/commit/886aaa81e06dfd3cd9391e8ea987d8cd5ada1124)]: + - effect@3.13.3 + - @effect/platform@0.77.3 + - @effect/platform-node-shared@0.27.3 + +## 0.73.2 + +### Patch Changes + +- Updated dependencies [[`31be72a`](https://github.com/Effect-TS/effect/commit/31be72ada118cb84a942e67b1663263f8db74a9f), [`3e7ce97`](https://github.com/Effect-TS/effect/commit/3e7ce97f8a41756a039cf635d0b3d9a75d781097), [`31be72a`](https://github.com/Effect-TS/effect/commit/31be72ada118cb84a942e67b1663263f8db74a9f)]: + - effect@3.13.2 + - @effect/platform@0.77.2 + - @effect/platform-node-shared@0.27.2 + +## 0.73.1 + +### Patch Changes + +- Updated dependencies [[`b56a211`](https://github.com/Effect-TS/effect/commit/b56a2110569fd0ec0b57ac137743e926d49f51cc)]: + - effect@3.13.1 + - @effect/platform@0.77.1 + - @effect/platform-node-shared@0.27.1 + +## 0.73.0 + +### Patch Changes + +- Updated dependencies [[`8baef83`](https://github.com/Effect-TS/effect/commit/8baef83e7ff0b7bc0738b680e1ef013065386cff), [`655bfe2`](https://github.com/Effect-TS/effect/commit/655bfe29e44cc3f0fb9b4e53038f50b891c188df), [`d90cbc2`](https://github.com/Effect-TS/effect/commit/d90cbc274e2742d18671fe65aa4764c057eb6cba), [`75632bd`](https://github.com/Effect-TS/effect/commit/75632bd44b8025101d652ccbaeef898c7086c91c), [`c874a2e`](https://github.com/Effect-TS/effect/commit/c874a2e4b17e9d71904ca8375bb77b020975cb1d), [`bf865e5`](https://github.com/Effect-TS/effect/commit/bf865e5833f77fd8f6c06944ca9d507b54488301), [`f98b2b7`](https://github.com/Effect-TS/effect/commit/f98b2b7592cf20f9d85313e7f1e964cb65878138), [`de8ce92`](https://github.com/Effect-TS/effect/commit/de8ce924923eaa4e1b761a97eb45ec967389f3d5), [`cf8b2dd`](https://github.com/Effect-TS/effect/commit/cf8b2dd112f8e092ed99d78fd728db0f91c29050), [`db426a5`](https://github.com/Effect-TS/effect/commit/db426a5fb41ab84d18e3c8753a7329b4de544245), [`6862444`](https://github.com/Effect-TS/effect/commit/6862444094906ad4f2cb077ff3b9cc0b73880c8c), [`5fc8a90`](https://github.com/Effect-TS/effect/commit/5fc8a90ba46a5fd9f3b643f0b5aeadc69d717339), [`546a492`](https://github.com/Effect-TS/effect/commit/546a492e60eb2b8b048a489a474b934ea0877005), [`65c4796`](https://github.com/Effect-TS/effect/commit/65c47966ce39055f02cf5c808daabb3ea6442b0b), [`9760fdc`](https://github.com/Effect-TS/effect/commit/9760fdc37bdaef9da8b150e46b86ddfbe2ad9221), [`5b471e7`](https://github.com/Effect-TS/effect/commit/5b471e7d4317e8ee5d72bbbd3e0c9775160949ab), [`4f810cc`](https://github.com/Effect-TS/effect/commit/4f810cc2770e9f1f266851d2cb6257112c12af49)]: + - effect@3.13.0 + - @effect/platform@0.77.0 + - @effect/platform-node-shared@0.27.0 + +## 0.72.1 + +### Patch Changes + +- Updated dependencies [[`4018eae`](https://github.com/Effect-TS/effect/commit/4018eaed2733241676ddb8c52416f463a8c32e35), [`543d36d`](https://github.com/Effect-TS/effect/commit/543d36d1a11452560b01ab966a82529ad5fee8c9), [`c407726`](https://github.com/Effect-TS/effect/commit/c407726f79df4a567a9631cddd8effaa16b3535d), [`f70a65a`](https://github.com/Effect-TS/effect/commit/f70a65ac80c6635d80b12beaf4d32a9cc59fa143), [`ba409f6`](https://github.com/Effect-TS/effect/commit/ba409f69c41aeaa29e475c0630735726eaf4dbac), [`3d2e356`](https://github.com/Effect-TS/effect/commit/3d2e3565e8a43d1bdb5daee8db3b90f56d71d859)]: + - effect@3.12.12 + - @effect/platform@0.76.1 + - @effect/platform-node-shared@0.26.1 + +## 0.72.0 + +### Minor Changes + +- [#4429](https://github.com/Effect-TS/effect/pull/4429) [`2473ad5`](https://github.com/Effect-TS/effect/commit/2473ad5cf23582e3a41338091fa526ffe611288d) Thanks @tim-smart! - run platform workers in a Scope, send errors or termination to a CloseLatch + +### Patch Changes + +- Updated dependencies [[`b6a032f`](https://github.com/Effect-TS/effect/commit/b6a032f07bffa020a848c813881879395134fa20), [`42ddd5f`](https://github.com/Effect-TS/effect/commit/42ddd5f144ce9f9d94a036679ebbd626446d37f5), [`2fe447c`](https://github.com/Effect-TS/effect/commit/2fe447c6354d334f9c591b8a8481818f5f0e797e), [`2473ad5`](https://github.com/Effect-TS/effect/commit/2473ad5cf23582e3a41338091fa526ffe611288d)]: + - effect@3.12.11 + - @effect/platform@0.76.0 + - @effect/platform-node-shared@0.26.0 + +## 0.71.4 + +### Patch Changes + +- Updated dependencies [[`e30f132`](https://github.com/Effect-TS/effect/commit/e30f132c336c9d0760bad39f82a55c7ce5159eb7), [`33fa667`](https://github.com/Effect-TS/effect/commit/33fa667c2623be1026e1ccee91bd44f73b09020a), [`87f5f28`](https://github.com/Effect-TS/effect/commit/87f5f2842e4196cb88d13f10f443ff0567e82832), [`7d57ecd`](https://github.com/Effect-TS/effect/commit/7d57ecdaf5da2345ebbf9c22df50317578bde0f5), [`4dbd170`](https://github.com/Effect-TS/effect/commit/4dbd170538e8fb7a36aa7c469c6f93b6c7000091)]: + - effect@3.12.10 + - @effect/platform@0.75.4 + - @effect/platform-node-shared@0.25.4 + +## 0.71.3 + +### Patch Changes + +- Updated dependencies [[`1b4a4e9`](https://github.com/Effect-TS/effect/commit/1b4a4e904ef5227ec7d9114d4e417eca19eed940)]: + - effect@3.12.9 + - @effect/platform@0.75.3 + - @effect/platform-node-shared@0.25.3 + +## 0.71.2 + +### Patch Changes + +- [#4354](https://github.com/Effect-TS/effect/pull/4354) [`62934fc`](https://github.com/Effect-TS/effect/commit/62934fc61ae870b0e86ef0711c2852743adee9db) Thanks @tim-smart! - optimize streaming response for NodeHttpServer + +- Updated dependencies [[`59b3cfb`](https://github.com/Effect-TS/effect/commit/59b3cfbbd5713dd9475998e95fad5534c0b21466), [`766113c`](https://github.com/Effect-TS/effect/commit/766113c0ea3512cdb887650ead8ba314236e22ee), [`bb05fb8`](https://github.com/Effect-TS/effect/commit/bb05fb83457355b1ca567228a9e041edfb6fd85d), [`712277f`](https://github.com/Effect-TS/effect/commit/712277f949052a24b46e4aa234063a6abf395c90), [`f269122`](https://github.com/Effect-TS/effect/commit/f269122508693b111142994dd48698ddc75f3d69), [`8f6006a`](https://github.com/Effect-TS/effect/commit/8f6006a610fb6d6c7b8d14209a7323338a8964ff), [`c45b559`](https://github.com/Effect-TS/effect/commit/c45b5592b5fd1189a5c932cfe05bd7d5f6d68508), [`430c846`](https://github.com/Effect-TS/effect/commit/430c846cbac05b187e3d24ac8dfee0cf22506f7c), [`7b03057`](https://github.com/Effect-TS/effect/commit/7b03057507d2dab5e6793beb9c578dedaaeb15fe), [`a9c94c8`](https://github.com/Effect-TS/effect/commit/a9c94c807755610831211a686d2fad849ab38eb4), [`107e6f0`](https://github.com/Effect-TS/effect/commit/107e6f0557a1e2d3b0dce25d62fa1e2601521752), [`c9175ae`](https://github.com/Effect-TS/effect/commit/c9175aef41cb1e3b689d0ac0a4f53d8107376b58), [`65c11b9`](https://github.com/Effect-TS/effect/commit/65c11b9266ec9447c31c26fe3ed35c73bd3b81fd), [`e386d2f`](https://github.com/Effect-TS/effect/commit/e386d2f1b3ab3ac2c14ee76de11f5963d32a3df4), [`9172efb`](https://github.com/Effect-TS/effect/commit/9172efba98bc6a82353e6ec2af61ac08f038ba64)]: + - @effect/platform@0.75.2 + - effect@3.12.8 + - @effect/platform-node-shared@0.25.2 + +## 0.71.1 + +### Patch Changes + +- Updated dependencies [[`8dff1d1`](https://github.com/Effect-TS/effect/commit/8dff1d1bff76cdba643cad7f0bf864300f08bc61)]: + - effect@3.12.7 + - @effect/platform@0.75.1 + - @effect/platform-node-shared@0.25.1 + +## 0.71.0 + +### Patch Changes + +- Updated dependencies [[`5e43ce5`](https://github.com/Effect-TS/effect/commit/5e43ce50bae116865906112e7f88d390739d778b), [`289c13b`](https://github.com/Effect-TS/effect/commit/289c13b38e8e35b214d46d385d05dead176c87cd), [`76eb7d0`](https://github.com/Effect-TS/effect/commit/76eb7d0fbce3c009c8f77e84c178cb15bbed9709), [`8b4e75d`](https://github.com/Effect-TS/effect/commit/8b4e75d35daea807c447ca760948a717aa66bb52), [`fc5e0f0`](https://github.com/Effect-TS/effect/commit/fc5e0f0d357a0051cfa01c1ede83ffdd3cb41ab1), [`004fd2b`](https://github.com/Effect-TS/effect/commit/004fd2bbd1459e64fb1b57f02eeb791ca5ea1ea5), [`b2a31be`](https://github.com/Effect-TS/effect/commit/b2a31be85c35d891351ce4f9a2cc93ece0c257f6), [`5514d05`](https://github.com/Effect-TS/effect/commit/5514d05b5cd586ff5868b8bd41c959e95e6c33cd), [`bf5f0ae`](https://github.com/Effect-TS/effect/commit/bf5f0ae9daa0170471678e22585e8ec14ce667bb), [`3b19bcf`](https://github.com/Effect-TS/effect/commit/3b19bcfd3aaadb6c9253428622df524537c8e626), [`b064b3b`](https://github.com/Effect-TS/effect/commit/b064b3b293615fd268cc5a5647d0981eb67750b8), [`eb264ed`](https://github.com/Effect-TS/effect/commit/eb264ed8a6e8c92a9dc7006f766c6ca2e5d29e03), [`289c13b`](https://github.com/Effect-TS/effect/commit/289c13b38e8e35b214d46d385d05dead176c87cd), [`f474678`](https://github.com/Effect-TS/effect/commit/f474678bf10b8f1c80e3dc096ddc7ecf20b2b23e), [`ee187d0`](https://github.com/Effect-TS/effect/commit/ee187d098007a402844c94d04f0cd8f07695377a)]: + - @effect/platform@0.75.0 + - effect@3.12.6 + - @effect/platform-node-shared@0.25.0 + +## 0.70.0 + +### Patch Changes + +- Updated dependencies [[`a8b0ddb`](https://github.com/Effect-TS/effect/commit/a8b0ddb84710054799fc8f57485b95d00093ada1), [`8653072`](https://github.com/Effect-TS/effect/commit/86530720d7a03e118d2c5a8bf5a997cee7e7f3d6), [`507d546`](https://github.com/Effect-TS/effect/commit/507d546bd49db31000425fb5da88c434e4291bea), [`a8b0ddb`](https://github.com/Effect-TS/effect/commit/a8b0ddb84710054799fc8f57485b95d00093ada1), [`8db239b`](https://github.com/Effect-TS/effect/commit/8db239b9c869a3707f6566b9d9dbdf53c4df03fc), [`84a0911`](https://github.com/Effect-TS/effect/commit/84a091181634c3a022c94234cec7764a3aeef1be), [`84a0911`](https://github.com/Effect-TS/effect/commit/84a091181634c3a022c94234cec7764a3aeef1be), [`3179a9f`](https://github.com/Effect-TS/effect/commit/3179a9f65d23369a6a9a1f80f7750566dd28df22), [`6cb9b76`](https://github.com/Effect-TS/effect/commit/6cb9b766396d0b2ed995cf26957359713efd202e), [`1fcbe55`](https://github.com/Effect-TS/effect/commit/1fcbe55345042d8468f6a98c84081bd00b6bcf5a), [`d9a63d9`](https://github.com/Effect-TS/effect/commit/d9a63d9d385653865954cac895065360d54cc56b), [`6cb9b76`](https://github.com/Effect-TS/effect/commit/6cb9b766396d0b2ed995cf26957359713efd202e)]: + - effect@3.12.5 + - @effect/platform@0.74.0 + - @effect/platform-node-shared@0.24.0 + +## 0.69.1 + +### Patch Changes + +- Updated dependencies [[`5b50ea4`](https://github.com/Effect-TS/effect/commit/5b50ea4a10cf9acd51f9624b2474d9d5ded74019), [`c170a68`](https://github.com/Effect-TS/effect/commit/c170a68b6266100774461fcd6c0e0fabb60112f2), [`a66c2eb`](https://github.com/Effect-TS/effect/commit/a66c2eb473245092cd41f04c2eb2b7b02cf53718), [`c9e5e1b`](https://github.com/Effect-TS/effect/commit/c9e5e1be17c0c84d3d4e2abc3c60215cdb56bbbe), [`7b3d58d`](https://github.com/Effect-TS/effect/commit/7b3d58d7aec2152ec282460871d3e9de45ed254d)]: + - effect@3.12.4 + - @effect/platform@0.73.1 + - @effect/platform-node-shared@0.23.1 + +## 0.69.0 + +### Patch Changes + +- [#4242](https://github.com/Effect-TS/effect/pull/4242) [`c1a0339`](https://github.com/Effect-TS/effect/commit/c1a0339034a291fd4463371afbcfc8adcf8994ae) Thanks @fubhy! - Add missing exports + +- Updated dependencies [[`d7dac48`](https://github.com/Effect-TS/effect/commit/d7dac48a477cdfeec509dbe9f33fce6a1b02b63d), [`c110032`](https://github.com/Effect-TS/effect/commit/c110032322450a8824ba38ae24335a538cd2ce9a), [`1d7fd2b`](https://github.com/Effect-TS/effect/commit/1d7fd2b7ee8eeecc912d27adf76ed897db236dc5), [`1d7fd2b`](https://github.com/Effect-TS/effect/commit/1d7fd2b7ee8eeecc912d27adf76ed897db236dc5), [`23ac740`](https://github.com/Effect-TS/effect/commit/23ac740c7dd4610b7d265c2071b88b0968419e9a), [`8cd7319`](https://github.com/Effect-TS/effect/commit/8cd7319b6568bfc7a30ca16c104d189e37eac3a0)]: + - effect@3.12.3 + - @effect/platform@0.73.0 + - @effect/platform-node-shared@0.23.0 + +## 0.68.2 + +### Patch Changes + +- Updated dependencies [[`734af82`](https://github.com/Effect-TS/effect/commit/734af82138e78b9c57a8355b1c6b80e80d38b222), [`b63c780`](https://github.com/Effect-TS/effect/commit/b63c78010893101520448ddda7019c487cf7eedd), [`212e784`](https://github.com/Effect-TS/effect/commit/212e78475f527147ec27c090bd13f789f55add7a), [`f852cb0`](https://github.com/Effect-TS/effect/commit/f852cb02040ea2f165e9b449615b8b1366add5d5), [`7276ae2`](https://github.com/Effect-TS/effect/commit/7276ae21062896adbb7508ac5b2dece95316322f), [`212e784`](https://github.com/Effect-TS/effect/commit/212e78475f527147ec27c090bd13f789f55add7a), [`212e784`](https://github.com/Effect-TS/effect/commit/212e78475f527147ec27c090bd13f789f55add7a), [`212e784`](https://github.com/Effect-TS/effect/commit/212e78475f527147ec27c090bd13f789f55add7a), [`c640d77`](https://github.com/Effect-TS/effect/commit/c640d77b33ad417876f4e8ffe8574ee6cbe5607f), [`0def088`](https://github.com/Effect-TS/effect/commit/0def0887cfdb6755729a64dfd52b3b9f46b0576c)]: + - effect@3.12.2 + - @effect/platform@0.72.2 + - @effect/platform-node-shared@0.22.2 + +## 0.68.1 + +### Patch Changes + +- Updated dependencies [[`302b57d`](https://github.com/Effect-TS/effect/commit/302b57d2cbf9b9ccc17450945aeebfb33cfe8d43), [`0988083`](https://github.com/Effect-TS/effect/commit/0988083d4594938590df5a287e5b27d38526dd07), [`8b46be6`](https://github.com/Effect-TS/effect/commit/8b46be6a3b8160362ab5ea9171c5e6932505125c), [`bfe8027`](https://github.com/Effect-TS/effect/commit/bfe802734b450a4b4ee069d1125dd37995db2bff), [`16dd657`](https://github.com/Effect-TS/effect/commit/16dd657033d8afac2ffea567b3c8bb27c9b249b6), [`39db211`](https://github.com/Effect-TS/effect/commit/39db211414e90c8db8fdad7dc8ce5b4661bcfaef)]: + - effect@3.12.1 + - @effect/platform@0.72.1 + - @effect/platform-node-shared@0.22.1 + +## 0.68.0 + +### Patch Changes + +- Updated dependencies [[`abb22a4`](https://github.com/Effect-TS/effect/commit/abb22a429b9c52c31e84856294f175d2064a9b4d), [`f369a89`](https://github.com/Effect-TS/effect/commit/f369a89e98bc682969803b9304adaf4557bb36c2), [`642376c`](https://github.com/Effect-TS/effect/commit/642376c63fd7d78754db991631a4d50a5dc79aa3), [`3d2b7a7`](https://github.com/Effect-TS/effect/commit/3d2b7a7e942a7157afae5b1cdbc6f3fef116428e), [`ef64c6f`](https://github.com/Effect-TS/effect/commit/ef64c6fec0d47da573c04230dde9ea729366d871), [`73f9c6f`](https://github.com/Effect-TS/effect/commit/73f9c6f2ff091512cf904cc54ab59965b86e87c8), [`17cb451`](https://github.com/Effect-TS/effect/commit/17cb4514590e8a86263f7aed009f24da8a237342), [`d801820`](https://github.com/Effect-TS/effect/commit/d80182060c2ee945d7e0e4728812abf9465a0d6a), [`e1eeb2d`](https://github.com/Effect-TS/effect/commit/e1eeb2d7064b3870041dab142f3057970699bbf1), [`c11f3a6`](https://github.com/Effect-TS/effect/commit/c11f3a60a05c3b5fc8e7ce90136728154dc505b0), [`618f7e0`](https://github.com/Effect-TS/effect/commit/618f7e092a1011e5090dca1e69b5e9285689654b), [`c0ba834`](https://github.com/Effect-TS/effect/commit/c0ba834d1995cf5a8b250e4780fd43f3e3881151), [`e1eeb2d`](https://github.com/Effect-TS/effect/commit/e1eeb2d7064b3870041dab142f3057970699bbf1)]: + - effect@3.12.0 + - @effect/platform@0.72.0 + - @effect/platform-node-shared@0.22.0 + +## 0.67.3 + +### Patch Changes + +- Updated dependencies [[`39457d4`](https://github.com/Effect-TS/effect/commit/39457d4897d9bc7df8af5c05d352866bbeae82eb), [`a475cc2`](https://github.com/Effect-TS/effect/commit/a475cc25fd7c9f26b27a8e98f8fbe43cc9e6ee3e), [`199214e`](https://github.com/Effect-TS/effect/commit/199214e21c616d8a0ccd7ed5f92e944e6c580193), [`b3c160d`](https://github.com/Effect-TS/effect/commit/b3c160d7a1fdfc2d3fb2440530f1ab80efc65133)]: + - effect@3.11.10 + - @effect/platform@0.71.7 + - @effect/platform-node-shared@0.21.7 + +## 0.67.2 + +### Patch Changes + +- Updated dependencies [[`1c08a0b`](https://github.com/Effect-TS/effect/commit/1c08a0b8505badcffb4d9cade5a746ea90c9557e), [`1ce703b`](https://github.com/Effect-TS/effect/commit/1ce703b041bbd7560c5c437c9b9be48f027937fd), [`1ce703b`](https://github.com/Effect-TS/effect/commit/1ce703b041bbd7560c5c437c9b9be48f027937fd)]: + - effect@3.11.9 + - @effect/platform@0.71.6 + - @effect/platform-node-shared@0.21.6 + +## 0.67.1 + +### Patch Changes + +- Updated dependencies [[`05d71f8`](https://github.com/Effect-TS/effect/commit/05d71f85622305705d8316817694a09762e60865), [`e66b920`](https://github.com/Effect-TS/effect/commit/e66b9205f25ab425d30640886eb3fb2c4715bc26)]: + - @effect/platform@0.71.5 + - @effect/platform-node-shared@0.21.5 + +## 0.67.0 + +### Minor Changes + +- [#4149](https://github.com/Effect-TS/effect/pull/4149) [`011e2b6`](https://github.com/Effect-TS/effect/commit/011e2b604e2e61e7b788243b0aab105fd301ec7f) Thanks @thewilkybarkid! - upgrade undici to 7.x and expose re-exports in Undici module + +### Patch Changes + +- Updated dependencies [[`909181a`](https://github.com/Effect-TS/effect/commit/909181a9ce9052a80432ccf52187e0723004bf7f), [`909181a`](https://github.com/Effect-TS/effect/commit/909181a9ce9052a80432ccf52187e0723004bf7f), [`1a6b52d`](https://github.com/Effect-TS/effect/commit/1a6b52dcf020d36e38a7bc90b648152cf5a8ccba)]: + - @effect/platform@0.71.4 + - effect@3.11.8 + - @effect/platform-node-shared@0.21.4 + +## 0.66.3 + +### Patch Changes + +- Updated dependencies [[`6984508`](https://github.com/Effect-TS/effect/commit/6984508c87f1bd91213b44c19b25ab5e2dcc1ce0), [`883639c`](https://github.com/Effect-TS/effect/commit/883639cc8ce47757f1cd39439391a8028c0812fe)]: + - @effect/platform@0.71.3 + - @effect/platform-node-shared@0.21.3 + +## 0.66.2 + +### Patch Changes + +- Updated dependencies [[`2408616`](https://github.com/Effect-TS/effect/commit/24086163b60b09cc6d0885bd565ef080dcbe866b), [`cec0b4d`](https://github.com/Effect-TS/effect/commit/cec0b4d152ef660be2ccdb0927255f2471436e6e), [`cec0b4d`](https://github.com/Effect-TS/effect/commit/cec0b4d152ef660be2ccdb0927255f2471436e6e), [`8d978c5`](https://github.com/Effect-TS/effect/commit/8d978c53f6fcc98d9d645ecba3e4b55d4297dd36), [`cec0b4d`](https://github.com/Effect-TS/effect/commit/cec0b4d152ef660be2ccdb0927255f2471436e6e), [`cec0b4d`](https://github.com/Effect-TS/effect/commit/cec0b4d152ef660be2ccdb0927255f2471436e6e)]: + - effect@3.11.7 + - @effect/platform@0.71.2 + - @effect/platform-node-shared@0.21.2 + +## 0.66.1 + +### Patch Changes + +- Updated dependencies [[`1d3df5b`](https://github.com/Effect-TS/effect/commit/1d3df5bc4324e88a392c348db35fd9d029c7b25e)]: + - @effect/platform@0.71.1 + - @effect/platform-node-shared@0.21.1 + +## 0.66.0 + +### Patch Changes + +- Updated dependencies [[`662d1ce`](https://github.com/Effect-TS/effect/commit/662d1ce6fb7da384a95888d5b2bb5605bdf3208d), [`c99a0f3`](https://github.com/Effect-TS/effect/commit/c99a0f376d049d3793ed33e146d9873b8a5e5b78), [`11fc401`](https://github.com/Effect-TS/effect/commit/11fc401f436f99bf4be95f56d50b0e4bdfe5edea), [`c99a0f3`](https://github.com/Effect-TS/effect/commit/c99a0f376d049d3793ed33e146d9873b8a5e5b78), [`31c62d8`](https://github.com/Effect-TS/effect/commit/31c62d83cbdcf9850a8b5331faa239601c60f78a)]: + - effect@3.11.6 + - @effect/platform@0.71.0 + - @effect/platform-node-shared@0.21.0 + +## 0.65.7 + +### Patch Changes + +- Updated dependencies [[`9f5a6f7`](https://github.com/Effect-TS/effect/commit/9f5a6f701bf7ba31adccd1f1bcfa8ab5614c9be8), [`22905cf`](https://github.com/Effect-TS/effect/commit/22905cf5addfb1ff3d2a6135c52036be958ae911), [`9f5a6f7`](https://github.com/Effect-TS/effect/commit/9f5a6f701bf7ba31adccd1f1bcfa8ab5614c9be8), [`1e59e4f`](https://github.com/Effect-TS/effect/commit/1e59e4fd778da18296812a2a32f36ca8ae50f60d), [`9f5a6f7`](https://github.com/Effect-TS/effect/commit/9f5a6f701bf7ba31adccd1f1bcfa8ab5614c9be8), [`8d914e5`](https://github.com/Effect-TS/effect/commit/8d914e504e7a22d0ea628e8af265ee450ff9530f), [`03bb00f`](https://github.com/Effect-TS/effect/commit/03bb00faa74f9e168a54a8cc0828a664fbb1ab05), [`9f5a6f7`](https://github.com/Effect-TS/effect/commit/9f5a6f701bf7ba31adccd1f1bcfa8ab5614c9be8), [`14e1149`](https://github.com/Effect-TS/effect/commit/14e1149f1af5a022f06eb8c2e4ba9fec17fe7426), [`9f5a6f7`](https://github.com/Effect-TS/effect/commit/9f5a6f701bf7ba31adccd1f1bcfa8ab5614c9be8), [`9f5a6f7`](https://github.com/Effect-TS/effect/commit/9f5a6f701bf7ba31adccd1f1bcfa8ab5614c9be8)]: + - effect@3.11.5 + - @effect/platform@0.70.7 + - @effect/platform-node-shared@0.20.7 + +## 0.65.6 + +### Patch Changes + +- Updated dependencies [[`9a5b8e3`](https://github.com/Effect-TS/effect/commit/9a5b8e36d184bd4967a88752cb6e755e1be263af)]: + - @effect/platform@0.70.6 + - @effect/platform-node-shared@0.20.6 + +## 0.65.5 + +### Patch Changes + +- Updated dependencies [[`415f4c9`](https://github.com/Effect-TS/effect/commit/415f4c98321868531727a83cbaad70164f5e4c40), [`518b258`](https://github.com/Effect-TS/effect/commit/518b258a8a67ecd332a9252c35cc060f8368dee2), [`6e323a3`](https://github.com/Effect-TS/effect/commit/6e323a36faaee46b328c8e3cf60a76b3aff9907f), [`518b258`](https://github.com/Effect-TS/effect/commit/518b258a8a67ecd332a9252c35cc060f8368dee2), [`6e323a3`](https://github.com/Effect-TS/effect/commit/6e323a36faaee46b328c8e3cf60a76b3aff9907f)]: + - @effect/platform@0.70.5 + - effect@3.11.4 + - @effect/platform-node-shared@0.20.5 + +## 0.65.4 + +### Patch Changes + +- Updated dependencies [[`90906f7`](https://github.com/Effect-TS/effect/commit/90906f7f154b12c7182e8f39e3c55ef3937db857), [`3862cd3`](https://github.com/Effect-TS/effect/commit/3862cd3c7f6a542ed65fb81255b3bd696ce2f567), [`3862cd3`](https://github.com/Effect-TS/effect/commit/3862cd3c7f6a542ed65fb81255b3bd696ce2f567), [`343b6aa`](https://github.com/Effect-TS/effect/commit/343b6aa6ac4a74276bfc7c63ccbf4a1d72bc1bed), [`afba339`](https://github.com/Effect-TS/effect/commit/afba339adc11dad56b5a3b7ca94487e58f34d613)]: + - effect@3.11.3 + - @effect/platform@0.70.4 + - @effect/platform-node-shared@0.20.4 + +## 0.65.3 + +### Patch Changes + +- Updated dependencies [[`7044730`](https://github.com/Effect-TS/effect/commit/70447306be1aeeb7d87c230b2a96ec87b993ede9)]: + - @effect/platform@0.70.3 + - @effect/platform-node-shared@0.20.3 + +## 0.65.2 + +### Patch Changes + +- [#4064](https://github.com/Effect-TS/effect/pull/4064) [`c2249ea`](https://github.com/Effect-TS/effect/commit/c2249ea13fd98ab7d9aa628787931356d8ec2860) Thanks @tim-smart! - HttpApi OpenApi adjustments + - Allow using transform annotation on endpoints & groups + - Preserve descriptions for "empty" schemas + +- Updated dependencies [[`01cee56`](https://github.com/Effect-TS/effect/commit/01cee560b58d94b24cc20e98083251b73e658b41), [`c2249ea`](https://github.com/Effect-TS/effect/commit/c2249ea13fd98ab7d9aa628787931356d8ec2860), [`1358aa5`](https://github.com/Effect-TS/effect/commit/1358aa5326eaa85ef13ee8d1fed0b4a4288ed3eb), [`1de3fe7`](https://github.com/Effect-TS/effect/commit/1de3fe7d1cbafd6391eaa38c2300b99e332cc2aa)]: + - effect@3.11.2 + - @effect/platform@0.70.2 + - @effect/platform-node-shared@0.20.2 + +## 0.65.1 + +### Patch Changes + +- Updated dependencies [[`dd8a2d8`](https://github.com/Effect-TS/effect/commit/dd8a2d8e80d33b16719fc69361eaedf0b59d4620), [`a71bfef`](https://github.com/Effect-TS/effect/commit/a71bfef46f5061bb2502a61a333638a987b62273)]: + - effect@3.11.1 + - @effect/platform@0.70.1 + - @effect/platform-node-shared@0.20.1 + +## 0.65.0 + +### Minor Changes + +- [#3835](https://github.com/Effect-TS/effect/pull/3835) [`672bde5`](https://github.com/Effect-TS/effect/commit/672bde5bec51c7d6f9862828e6a654cb2cb6f93d) Thanks @tim-smart! - support array of values in /platform url param schemas + +### Patch Changes + +- [#3835](https://github.com/Effect-TS/effect/pull/3835) [`5eff3f6`](https://github.com/Effect-TS/effect/commit/5eff3f6fa3aae7e86948a62cbfd63b8d6c3bdf92) Thanks @tim-smart! - fix multipart support for bun http server + +- Updated dependencies [[`147434b`](https://github.com/Effect-TS/effect/commit/147434b03d5e1fd692dd9f126e5ab0910f3b76d3), [`6e69493`](https://github.com/Effect-TS/effect/commit/6e694930048bbaf98110f35f41566aeb9752d471), [`147434b`](https://github.com/Effect-TS/effect/commit/147434b03d5e1fd692dd9f126e5ab0910f3b76d3), [`5eff3f6`](https://github.com/Effect-TS/effect/commit/5eff3f6fa3aae7e86948a62cbfd63b8d6c3bdf92), [`d9fe79b`](https://github.com/Effect-TS/effect/commit/d9fe79bb5a3fe105d8e7a3bc2922a8ad936a5d10), [`251d189`](https://github.com/Effect-TS/effect/commit/251d189420bbba71990574e91098c499065f9a9b), [`5a259f3`](https://github.com/Effect-TS/effect/commit/5a259f3711b4369f55d885b568bdb21136155261), [`b4ce4ea`](https://github.com/Effect-TS/effect/commit/b4ce4ea7fd514a7e572f2dcd879c98f334981b0e), [`15fcc5a`](https://github.com/Effect-TS/effect/commit/15fcc5a0ea4bbf40ab48fa6a04fdda74f76f4c07), [`9bc9a47`](https://github.com/Effect-TS/effect/commit/9bc9a476800dc645903c888a68bb1d3baa3383c6), [`aadb8a4`](https://github.com/Effect-TS/effect/commit/aadb8a48d2cba197c06ec9996505510e48e4e5cb), [`1e2747c`](https://github.com/Effect-TS/effect/commit/1e2747c63a4820d1459cbbc88c71212983bd68bd), [`9264162`](https://github.com/Effect-TS/effect/commit/9264162a82783a651776fb7b87604564a63e7070), [`e0b9b09`](https://github.com/Effect-TS/effect/commit/e0b9b09e70c386b2da17d1f0a15b0511861c89e8), [`c36f3b9`](https://github.com/Effect-TS/effect/commit/c36f3b95df5ce9d71b66f22f26ce12eda8d3e848), [`672bde5`](https://github.com/Effect-TS/effect/commit/672bde5bec51c7d6f9862828e6a654cb2cb6f93d), [`aadb8a4`](https://github.com/Effect-TS/effect/commit/aadb8a48d2cba197c06ec9996505510e48e4e5cb)]: + - effect@3.11.0 + - @effect/platform@0.70.0 + - @effect/platform-node-shared@0.20.0 + +## 0.64.34 + +### Patch Changes + +- Updated dependencies [[`3069614`](https://github.com/Effect-TS/effect/commit/30696149271129fc618f6f2ccd1d8f2f6c0f9cd7), [`09a5e52`](https://github.com/Effect-TS/effect/commit/09a5e522fd9b221f05d85b1d1c8a740d4973c302)]: + - effect@3.10.20 + - @effect/platform@0.69.32 + - @effect/platform-node-shared@0.19.33 + +## 0.64.33 + +### Patch Changes + +- [#4035](https://github.com/Effect-TS/effect/pull/4035) [`e6d4a37`](https://github.com/Effect-TS/effect/commit/e6d4a37c1d7e657b5ea44063a1cf586808228fe5) Thanks @tim-smart! - add template literal api for defining HttpApiEndpoint path schema + +- Updated dependencies [[`e6d4a37`](https://github.com/Effect-TS/effect/commit/e6d4a37c1d7e657b5ea44063a1cf586808228fe5)]: + - @effect/platform@0.69.31 + - @effect/platform-node-shared@0.19.32 + +## 0.64.32 + +### Patch Changes + +- [#4025](https://github.com/Effect-TS/effect/pull/4025) [`270f199`](https://github.com/Effect-TS/effect/commit/270f199b31810fd643e4c22818698adcbdb5d396) Thanks @tim-smart! - update OpenApi version to 3.1.0 + +- Updated dependencies [[`270f199`](https://github.com/Effect-TS/effect/commit/270f199b31810fd643e4c22818698adcbdb5d396)]: + - @effect/platform@0.69.30 + - @effect/platform-node-shared@0.19.31 + +## 0.64.31 + +### Patch Changes + +- [#4024](https://github.com/Effect-TS/effect/pull/4024) [`24cc35e`](https://github.com/Effect-TS/effect/commit/24cc35e26d6ed4a076470bc687ffd99cc50991b3) Thanks @tim-smart! - improve HttpApi handling of payload encoding types + +- Updated dependencies [[`24cc35e`](https://github.com/Effect-TS/effect/commit/24cc35e26d6ed4a076470bc687ffd99cc50991b3)]: + - @effect/platform@0.69.29 + - @effect/platform-node-shared@0.19.30 + +## 0.64.30 + +### Patch Changes + +- [#4007](https://github.com/Effect-TS/effect/pull/4007) [`944025b`](https://github.com/Effect-TS/effect/commit/944025bc5ce139f4a85846aa689bf30ec06a8ec1) Thanks @gcanti! - Wrap JSDoc @example tags with a TypeScript fence, closes #4002 + +- [#4016](https://github.com/Effect-TS/effect/pull/4016) [`a9e00e4`](https://github.com/Effect-TS/effect/commit/a9e00e43f0b5dd22c1f9d5b78be6383daea09c20) Thanks @tim-smart! - allow using HttpApiSchema.Multipart in a union + +- Updated dependencies [[`edd72be`](https://github.com/Effect-TS/effect/commit/edd72be57b904d60c9cbffc2537901821a9da537), [`a3e2771`](https://github.com/Effect-TS/effect/commit/a3e277170a1f7cf61fd629acb60304c7e81d9498), [`944025b`](https://github.com/Effect-TS/effect/commit/944025bc5ce139f4a85846aa689bf30ec06a8ec1), [`54addee`](https://github.com/Effect-TS/effect/commit/54addee438a644bf010646c52042c7b89c5fc0a7), [`a9e00e4`](https://github.com/Effect-TS/effect/commit/a9e00e43f0b5dd22c1f9d5b78be6383daea09c20)]: + - @effect/platform@0.69.28 + - effect@3.10.19 + - @effect/platform-node-shared@0.19.29 + +## 0.64.29 + +### Patch Changes + +- Updated dependencies [[`af409cf`](https://github.com/Effect-TS/effect/commit/af409cf1d2ff973be11cc079ea373eaeedca25de), [`beaccae`](https://github.com/Effect-TS/effect/commit/beaccae2d15931e9fe475fb50a0b3638243fe3f7)]: + - effect@3.10.18 + - @effect/platform@0.69.27 + - @effect/platform-node-shared@0.19.28 + +## 0.64.28 + +### Patch Changes + +- Updated dependencies [[`c963886`](https://github.com/Effect-TS/effect/commit/c963886d5817986fcbd6bfa4ddf50aca8b6c8184), [`42c4ce6`](https://github.com/Effect-TS/effect/commit/42c4ce6f8d8c7d847e97757650a8ad9419a829d7)]: + - @effect/platform@0.69.26 + - effect@3.10.17 + - @effect/platform-node-shared@0.19.27 + +## 0.64.27 + +### Patch Changes + +- Updated dependencies [[`320557a`](https://github.com/Effect-TS/effect/commit/320557ab18d13c5e22fc7dc0d2a157eae461012f), [`4dca30c`](https://github.com/Effect-TS/effect/commit/4dca30cfcdafe4542e236489f71d6f171a5b4e38), [`1d99867`](https://github.com/Effect-TS/effect/commit/1d998671be3cd11043f232822e91dd8c98fccfa9), [`6dae414`](https://github.com/Effect-TS/effect/commit/6dae4147991a97ec14a99289bd25fadae7541e8d), [`6b0d737`](https://github.com/Effect-TS/effect/commit/6b0d737078bf63b97891e6bc47affc04b28f9cf7), [`d8356aa`](https://github.com/Effect-TS/effect/commit/d8356aad428a0c2290db52380220f81d9ec94232), [`7b93dd6`](https://github.com/Effect-TS/effect/commit/7b93dd622e2ab79c7072d79d0d9611e446202201)]: + - @effect/platform@0.69.25 + - effect@3.10.16 + - @effect/platform-node-shared@0.19.26 + +## 0.64.26 + +### Patch Changes + +- Updated dependencies [[`3cc6514`](https://github.com/Effect-TS/effect/commit/3cc6514d2dd64e010cb760cc29bfce98c349bb10)]: + - @effect/platform@0.69.24 + - @effect/platform-node-shared@0.19.25 + +## 0.64.25 + +### Patch Changes + +- Updated dependencies [[`3aff4d3`](https://github.com/Effect-TS/effect/commit/3aff4d38837c213bb2987973dc4b98febb9f92d2)]: + - @effect/platform@0.69.23 + - @effect/platform-node-shared@0.19.24 + +## 0.64.24 + +### Patch Changes + +- Updated dependencies [[`8398b32`](https://github.com/Effect-TS/effect/commit/8398b3208242a88239d4449910b7baf923cfe3b6), [`72e55b7`](https://github.com/Effect-TS/effect/commit/72e55b7c610784fcebdbadc592c876e23e76a986)]: + - effect@3.10.15 + - @effect/platform@0.69.22 + - @effect/platform-node-shared@0.19.23 + +## 0.64.23 + +### Patch Changes + +- Updated dependencies [[`f983946`](https://github.com/Effect-TS/effect/commit/f9839467b4cad6e788297764ef9f9f0b9fd203f9), [`2d8a750`](https://github.com/Effect-TS/effect/commit/2d8a75081eb83a0a81f817fdf6f428369c5064ab)]: + - effect@3.10.14 + - @effect/platform@0.69.21 + - @effect/platform-node-shared@0.19.22 + +## 0.64.22 + +### Patch Changes + +- Updated dependencies [[`995bbdf`](https://github.com/Effect-TS/effect/commit/995bbdffea2e332f203cd5b474cd6a1c77dfa6ae)]: + - effect@3.10.13 + - @effect/platform@0.69.20 + - @effect/platform-node-shared@0.19.21 + +## 0.64.21 + +### Patch Changes + +- [#3908](https://github.com/Effect-TS/effect/pull/3908) [`eb8c52d`](https://github.com/Effect-TS/effect/commit/eb8c52d8b4c5e067ebf0a81eb742f5822e6439b5) Thanks @tim-smart! - use plain js data structures for HttpApi properties + +- Updated dependencies [[`eb8c52d`](https://github.com/Effect-TS/effect/commit/eb8c52d8b4c5e067ebf0a81eb742f5822e6439b5)]: + - @effect/platform@0.69.19 + - @effect/platform-node-shared@0.19.20 + +## 0.64.20 + +### Patch Changes + +- [#3904](https://github.com/Effect-TS/effect/pull/3904) [`dd14efe`](https://github.com/Effect-TS/effect/commit/dd14efe0ace255f571273aae876adea96267d7e6) Thanks @tim-smart! - improve platform/Worker shutdown and logging + +- Updated dependencies [[`a0584ec`](https://github.com/Effect-TS/effect/commit/a0584ece92ed784bfb139e9c5a699f02d1e71c2d), [`dd14efe`](https://github.com/Effect-TS/effect/commit/dd14efe0ace255f571273aae876adea96267d7e6), [`dd14efe`](https://github.com/Effect-TS/effect/commit/dd14efe0ace255f571273aae876adea96267d7e6)]: + - @effect/platform-node-shared@0.19.19 + - @effect/platform@0.69.18 + - effect@3.10.12 + +## 0.64.19 + +### Patch Changes + +- [#3897](https://github.com/Effect-TS/effect/pull/3897) [`a2bd4df`](https://github.com/Effect-TS/effect/commit/a2bd4dfa3d9a28a7d02ee177baf173c92a4dee7b) Thanks @patroza! - fix: NodeWorker not responding to interruption + +- Updated dependencies [[`5eef499`](https://github.com/Effect-TS/effect/commit/5eef4998b6ccb7a5404d9e4fef85e57fa35fbb8a), [`8240b1c`](https://github.com/Effect-TS/effect/commit/8240b1c10d45312fc863cb679b1a1e8441af0c1a), [`5eef499`](https://github.com/Effect-TS/effect/commit/5eef4998b6ccb7a5404d9e4fef85e57fa35fbb8a)]: + - effect@3.10.11 + - @effect/platform@0.69.17 + - @effect/platform-node-shared@0.19.18 + +## 0.64.18 + +### Patch Changes + +- Updated dependencies [[`3ff8e5b`](https://github.com/Effect-TS/effect/commit/3ff8e5b4138c89b56111c075b290e4084d7d169c)]: + - @effect/platform-node-shared@0.19.17 + +## 0.64.17 + +### Patch Changes + +- Updated dependencies [[`cd720ae`](https://github.com/Effect-TS/effect/commit/cd720aedf7f2571edec0843d6a633e84e4832b28), [`cd720ae`](https://github.com/Effect-TS/effect/commit/cd720aedf7f2571edec0843d6a633e84e4832b28), [`cd720ae`](https://github.com/Effect-TS/effect/commit/cd720aedf7f2571edec0843d6a633e84e4832b28), [`b631f40`](https://github.com/Effect-TS/effect/commit/b631f40abbe649b2a089764585b5c39f6a695ac6), [`7d89650`](https://github.com/Effect-TS/effect/commit/7d8965036cd2ea435c8441ffec3345488baebf85)]: + - effect@3.10.10 + - @effect/platform-node-shared@0.19.16 + - @effect/platform@0.69.16 + +## 0.64.16 + +### Patch Changes + +- [#3885](https://github.com/Effect-TS/effect/pull/3885) [`8a30e1d`](https://github.com/Effect-TS/effect/commit/8a30e1dfa3a7103bf5414fc6a7fca3088d8c8c00) Thanks @tim-smart! - simplify HttpApiBuilder handler logic + +- Updated dependencies [[`8a30e1d`](https://github.com/Effect-TS/effect/commit/8a30e1dfa3a7103bf5414fc6a7fca3088d8c8c00)]: + - @effect/platform@0.69.15 + - @effect/platform-node-shared@0.19.15 + +## 0.64.15 + +### Patch Changes + +- [#3884](https://github.com/Effect-TS/effect/pull/3884) [`07c493a`](https://github.com/Effect-TS/effect/commit/07c493a598e096c7810cd06def8cfa43493c46b1) Thanks @tim-smart! - add withResponse option to HttpApiClient methods + +- Updated dependencies [[`a123e80`](https://github.com/Effect-TS/effect/commit/a123e80f111a625428a5b5622b7f55ee1073566b), [`bd5fcd3`](https://github.com/Effect-TS/effect/commit/bd5fcd3e6b603b1e505af90d6a00627c8eca6d41), [`0289d3b`](https://github.com/Effect-TS/effect/commit/0289d3b6391031d00329365bab9791b355031fe3), [`7386b71`](https://github.com/Effect-TS/effect/commit/7386b710e5be570e17f468928a6ed19d549a3e12), [`07c493a`](https://github.com/Effect-TS/effect/commit/07c493a598e096c7810cd06def8cfa43493c46b1), [`257ab1b`](https://github.com/Effect-TS/effect/commit/257ab1b539fa6e930b7ae2583a188376372200d7), [`4211a23`](https://github.com/Effect-TS/effect/commit/4211a2355bb3af3f0e756e2aae9d293379f25662)]: + - effect@3.10.9 + - @effect/platform@0.69.14 + - @effect/platform-node-shared@0.19.14 + +## 0.64.14 + +### Patch Changes + +- Updated dependencies [[`68b5c9e`](https://github.com/Effect-TS/effect/commit/68b5c9e44f34192cef26e1cadda5e661a027df41), [`9c9928d`](https://github.com/Effect-TS/effect/commit/9c9928dfeacd9ac33dc37eb0ca3d7d8c39175ada), [`6306e66`](https://github.com/Effect-TS/effect/commit/6306e6656092b350d4ede5746da6f245ec9f7e07), [`361c7f3`](https://github.com/Effect-TS/effect/commit/361c7f39a2c10ede9324847c3d3ba192a6f9b20a)]: + - effect@3.10.8 + - @effect/platform@0.69.13 + - @effect/platform-node-shared@0.19.13 + +## 0.64.13 + +### Patch Changes + +- Updated dependencies [[`33f5b9f`](https://github.com/Effect-TS/effect/commit/33f5b9ffaebea4f1bd0e391b44c41fb6230e743a), [`50f0281`](https://github.com/Effect-TS/effect/commit/50f0281b0d2116726b8927a6217622d5f394f3e4)]: + - effect@3.10.7 + - @effect/platform@0.69.12 + - @effect/platform-node-shared@0.19.12 + +## 0.64.12 + +### Patch Changes + +- Updated dependencies [[`ce1c21f`](https://github.com/Effect-TS/effect/commit/ce1c21ffc11902ac9ab453a51904207859d38552), [`81ddd45`](https://github.com/Effect-TS/effect/commit/81ddd45fc074b98206fafab416d9a5a28b31e07a)]: + - effect@3.10.6 + - @effect/platform@0.69.11 + - @effect/platform-node-shared@0.19.11 + +## 0.64.11 + +### Patch Changes + +- Updated dependencies [[`3a6d757`](https://github.com/Effect-TS/effect/commit/3a6d757badeebe00d8ef4d67530d073c8264dcfa), [`59d813a`](https://github.com/Effect-TS/effect/commit/59d813aa4973d1115cfc70cc3667508335f49693)]: + - effect@3.10.5 + - @effect/platform@0.69.10 + - @effect/platform-node-shared@0.19.10 + +## 0.64.10 + +### Patch Changes + +- Updated dependencies [[`2367708`](https://github.com/Effect-TS/effect/commit/2367708be449f9526a2047e321302d7bfb16f18e)]: + - @effect/platform@0.69.9 + - effect@3.10.4 + - @effect/platform-node-shared@0.19.9 + +## 0.64.9 + +### Patch Changes + +- Updated dependencies [[`522f7c5`](https://github.com/Effect-TS/effect/commit/522f7c518a5acfb55ef96d6796869f002cc3eaf8)]: + - @effect/platform@0.69.8 + - @effect/platform-node-shared@0.19.8 + +## 0.64.8 + +### Patch Changes + +- Updated dependencies [[`690d6c5`](https://github.com/Effect-TS/effect/commit/690d6c54d2145adb0af545c447db7d4755bf3c6b), [`b9423d8`](https://github.com/Effect-TS/effect/commit/b9423d8bf8181a2389fdbce1e3c14ac6fe8d54f5), [`279fe3a`](https://github.com/Effect-TS/effect/commit/279fe3a7168fe84e520c2cc88ba189a15f03a2bc)]: + - @effect/platform@0.69.7 + - effect@3.10.3 + - @effect/platform-node-shared@0.19.7 + +## 0.64.7 + +### Patch Changes + +- Updated dependencies [[`714e119`](https://github.com/Effect-TS/effect/commit/714e11945e45e5a2554ee058e6c43f82a8e309cf), [`c1afd55`](https://github.com/Effect-TS/effect/commit/c1afd55c54e61f9c432823d21b3d016f79160a37), [`42cd72a`](https://github.com/Effect-TS/effect/commit/42cd72a44ca9593e4d81fbb50e8111625fd0fb81)]: + - effect@3.10.2 + - @effect/platform@0.69.6 + - @effect/platform-node-shared@0.19.6 + +## 0.64.6 + +### Patch Changes + +- Updated dependencies [[`9604d6b`](https://github.com/Effect-TS/effect/commit/9604d6b616435103dafea8b53637a9d1450b4750)]: + - effect@3.10.1 + - @effect/platform@0.69.5 + - @effect/platform-node-shared@0.19.5 + +## 0.64.5 + +### Patch Changes + +- Updated dependencies [[`c86b1d7`](https://github.com/Effect-TS/effect/commit/c86b1d7cd47b66df190ef9775a475467c1abdbd6)]: + - @effect/platform@0.69.4 + - @effect/platform-node-shared@0.19.4 + +## 0.64.4 + +### Patch Changes + +- Updated dependencies [[`d5fba63`](https://github.com/Effect-TS/effect/commit/d5fba6391e1005e374aa0238f13edfbd65848313), [`1eb2c30`](https://github.com/Effect-TS/effect/commit/1eb2c30ba064398db5790e376dedcfad55b7b005), [`02d413e`](https://github.com/Effect-TS/effect/commit/02d413e7b6bc1c64885969c37cc3e4e690c94d7d)]: + - @effect/platform@0.69.3 + - @effect/platform-node-shared@0.19.3 + +## 0.64.3 + +### Patch Changes + +- Updated dependencies [[`e7afc47`](https://github.com/Effect-TS/effect/commit/e7afc47ce83e381c3f4aed2b2974e3b3d86a2340)]: + - @effect/platform@0.69.2 + - @effect/platform-node-shared@0.19.2 + +## 0.64.2 + +### Patch Changes + +- [#3806](https://github.com/Effect-TS/effect/pull/3806) [`a4aa34a`](https://github.com/Effect-TS/effect/commit/a4aa34a0c32b79f7c95f3eb36ee69a8e8e23684c) Thanks @tim-smart! - fix HttpServer.layerContext access before initialization + +## 0.64.1 + +### Patch Changes + +- [#3802](https://github.com/Effect-TS/effect/pull/3802) [`7564f56`](https://github.com/Effect-TS/effect/commit/7564f56bb2844cf39d2b0d2d9e93cf9b2205e9a8) Thanks @tim-smart! - add HttpServer.layerContext to platform-node/bun + +- Updated dependencies [[`7564f56`](https://github.com/Effect-TS/effect/commit/7564f56bb2844cf39d2b0d2d9e93cf9b2205e9a8), [`7564f56`](https://github.com/Effect-TS/effect/commit/7564f56bb2844cf39d2b0d2d9e93cf9b2205e9a8)]: + - @effect/platform@0.69.1 + - @effect/platform-node-shared@0.19.1 + +## 0.64.0 + +### Patch Changes + +- Updated dependencies [[`4a01828`](https://github.com/Effect-TS/effect/commit/4a01828b66d6213e9bbe18979c893b13f7bb29bf), [`6d9de6b`](https://github.com/Effect-TS/effect/commit/6d9de6b871c5c08e6509a4e830c3d74758faa198), [`4a01828`](https://github.com/Effect-TS/effect/commit/4a01828b66d6213e9bbe18979c893b13f7bb29bf), [`c79c4c1`](https://github.com/Effect-TS/effect/commit/c79c4c178390fe61ff6dda88c9e058862349343a), [`38d30f0`](https://github.com/Effect-TS/effect/commit/38d30f08b8da62f9c3e308b9250738cb8d17bdb5), [`5821ce3`](https://github.com/Effect-TS/effect/commit/5821ce3455b47d25e0a40cae6ce22af9db5fa556)]: + - effect@3.10.0 + - @effect/platform@0.69.0 + - @effect/platform-node-shared@0.19.0 + +## 0.63.6 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform@0.68.6 + - @effect/platform-node-shared@0.18.6 + +## 0.63.5 + +### Patch Changes + +- Updated dependencies [[`2036402`](https://github.com/Effect-TS/effect/commit/20364020b8b75a684791aa93d90626758023e9e9)]: + - @effect/platform@0.68.5 + - @effect/platform-node-shared@0.18.5 + +## 0.63.4 + +### Patch Changes + +- Updated dependencies [[`1b1ef29`](https://github.com/Effect-TS/effect/commit/1b1ef29ae302322f69dc938f9337aa97b4c63266)]: + - @effect/platform@0.68.4 + - @effect/platform-node-shared@0.18.4 + +## 0.63.3 + +### Patch Changes + +- [#3769](https://github.com/Effect-TS/effect/pull/3769) [`8c33087`](https://github.com/Effect-TS/effect/commit/8c330879425e80bed2f65e407cd59e991f0d7bec) Thanks @tim-smart! - add support for WebSocket protocols option + +- [#3779](https://github.com/Effect-TS/effect/pull/3779) [`3bcdfb3`](https://github.com/Effect-TS/effect/commit/3bcdfb3b6453959f449b075130e2db941653f722) Thanks @tim-smart! - remove Scope requirement from NodeHttpServer.makeHandler + +- Updated dependencies [[`61a99b2`](https://github.com/Effect-TS/effect/commit/61a99b2bf9d757870ef0c2ec9d4c877cdd364a3d), [`8c33087`](https://github.com/Effect-TS/effect/commit/8c330879425e80bed2f65e407cd59e991f0d7bec)]: + - effect@3.9.2 + - @effect/platform@0.68.3 + - @effect/platform-node-shared@0.18.3 + +## 0.63.2 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform@0.68.2 + - @effect/platform-node-shared@0.18.2 + +## 0.63.1 + +### Patch Changes + +- Updated dependencies [[`b75ac5d`](https://github.com/Effect-TS/effect/commit/b75ac5d0909115507bedc90f18f2d34deb217769)]: + - @effect/platform@0.68.1 + - @effect/platform-node-shared@0.18.1 + +## 0.63.0 + +### Minor Changes + +- [#3756](https://github.com/Effect-TS/effect/pull/3756) [`90ceeab`](https://github.com/Effect-TS/effect/commit/90ceeab3a04051b740af18c8af8bd73ee8ec6363) Thanks @tim-smart! - remove HttpClient.Service type + +- [#3756](https://github.com/Effect-TS/effect/pull/3756) [`90ceeab`](https://github.com/Effect-TS/effect/commit/90ceeab3a04051b740af18c8af8bd73ee8ec6363) Thanks @tim-smart! - constrain HttpClient success type to HttpClientResponse + +- [#3756](https://github.com/Effect-TS/effect/pull/3756) [`90ceeab`](https://github.com/Effect-TS/effect/commit/90ceeab3a04051b740af18c8af8bd73ee8ec6363) Thanks @tim-smart! - add HttpClient accessor apis + + These apis allow you to easily send requests without first accessing the `HttpClient` service. + + Below is an example of using the `get` accessor api to send a GET request: + + ```ts + import { FetchHttpClient, HttpClient } from "@effect/platform" + import { Effect } from "effect" + + const program = HttpClient.get( + "https://jsonplaceholder.typicode.com/posts/1" + ).pipe( + Effect.andThen((response) => response.json), + Effect.scoped, + Effect.provide(FetchHttpClient.layer) + ) + + Effect.runPromise(program) + /* + Output: + { + userId: 1, + id: 1, + title: 'sunt aut facere repellat provident occaecati excepturi optio reprehenderit', + body: 'quia et suscipit\n' + + 'suscipit recusandae consequuntur expedita et cum\n' + + 'reprehenderit molestiae ut ut quas totam\n' + + 'nostrum rerum est autem sunt rem eveniet architecto' + } + */ + ``` + +### Patch Changes + +- Updated dependencies [[`90ceeab`](https://github.com/Effect-TS/effect/commit/90ceeab3a04051b740af18c8af8bd73ee8ec6363), [`90ceeab`](https://github.com/Effect-TS/effect/commit/90ceeab3a04051b740af18c8af8bd73ee8ec6363), [`90ceeab`](https://github.com/Effect-TS/effect/commit/90ceeab3a04051b740af18c8af8bd73ee8ec6363)]: + - @effect/platform@0.68.0 + - @effect/platform-node-shared@0.18.0 + +## 0.62.1 + +### Patch Changes + +- Updated dependencies [[`3b2ad1d`](https://github.com/Effect-TS/effect/commit/3b2ad1d58a2e33dc1a72b7037396bd25ca1702a9)]: + - @effect/platform@0.67.1 + - effect@3.9.1 + - @effect/platform-node-shared@0.17.1 + +## 0.62.0 + +### Patch Changes + +- Updated dependencies [[`ff3d1aa`](https://github.com/Effect-TS/effect/commit/ff3d1aab290b4d1173b2dfc7e4c76abb4babdc16), [`0ba66f2`](https://github.com/Effect-TS/effect/commit/0ba66f2451641fd6990e02ec1ed01c014db9dab0), [`bf77f51`](https://github.com/Effect-TS/effect/commit/bf77f51b323c383224ebf08adf77a7a6e8c9b3cd), [`016f9ad`](https://github.com/Effect-TS/effect/commit/016f9ad931a4b3d09a34e5caf13d87c5b8e9c984), [`0779681`](https://github.com/Effect-TS/effect/commit/07796813f07de035719728733096ba64ce333469), [`534129f`](https://github.com/Effect-TS/effect/commit/534129f8113ce1a8ec50828083e16da9c86326c6), [`d75140c`](https://github.com/Effect-TS/effect/commit/d75140c7a664ceda43142d999f4ff8dcd36d6dda), [`be0451c`](https://github.com/Effect-TS/effect/commit/be0451c149b6618af79cb839cdf04af2db1efb03), [`9237ac6`](https://github.com/Effect-TS/effect/commit/9237ac69bc07de5b3b60076a0ad2921c21de7457), [`be0451c`](https://github.com/Effect-TS/effect/commit/be0451c149b6618af79cb839cdf04af2db1efb03), [`5b36494`](https://github.com/Effect-TS/effect/commit/5b364942e9a9003fdb8217324f8a2d8369c969da), [`c716adb`](https://github.com/Effect-TS/effect/commit/c716adb250ebbea1d1048d818ef7fed4f621d186), [`4986391`](https://github.com/Effect-TS/effect/commit/49863919cd8628c962a712fb1df30d2983820933), [`d75140c`](https://github.com/Effect-TS/effect/commit/d75140c7a664ceda43142d999f4ff8dcd36d6dda), [`d1387ae`](https://github.com/Effect-TS/effect/commit/d1387aebd1ff01bbebde26be46d488956e4daef6)]: + - effect@3.9.0 + - @effect/platform@0.67.0 + - @effect/platform-node-shared@0.17.0 + +## 0.61.4 + +### Patch Changes + +- Updated dependencies [[`88e85db`](https://github.com/Effect-TS/effect/commit/88e85db34bd402526e27a323e950d053fa34d232), [`f40da15`](https://github.com/Effect-TS/effect/commit/f40da15fbeb7c491840b8f409d47de79720891c3), [`8e94585`](https://github.com/Effect-TS/effect/commit/8e94585abe62753bf3af28bfae77926a7c570ac3), [`83887ca`](https://github.com/Effect-TS/effect/commit/83887ca1b1793916913d8550a4db4450cd14a044), [`5266b6c`](https://github.com/Effect-TS/effect/commit/5266b6cd86d76c3886da041c8829bca04b1a3110), [`cdead5c`](https://github.com/Effect-TS/effect/commit/cdead5c9cfd54dc6c4f215d9732f654c4a12e991), [`f40da15`](https://github.com/Effect-TS/effect/commit/f40da15fbeb7c491840b8f409d47de79720891c3), [`766a8af`](https://github.com/Effect-TS/effect/commit/766a8af307b414aca3648d91c4eab7493a5ec862)]: + - effect@3.8.5 + - @effect/platform@0.66.3 + - @effect/platform-node-shared@0.16.4 + +## 0.61.3 + +### Patch Changes + +- Updated dependencies [[`660cd0f`](https://github.com/Effect-TS/effect/commit/660cd0f93610e5e5588f25b852ae7cf4f1dd05bc)]: + - @effect/platform-node-shared@0.16.3 + +## 0.61.2 + +### Patch Changes + +- Updated dependencies [[`fd83d0e`](https://github.com/Effect-TS/effect/commit/fd83d0e548feff9ea2d53d370a0b626c4a1d940e), [`4509656`](https://github.com/Effect-TS/effect/commit/45096569d50262275ee984f44c456f5c83b62683), [`090e41c`](https://github.com/Effect-TS/effect/commit/090e41c636d720b1c7d89684a739855765ed4382)]: + - @effect/platform@0.66.2 + - effect@3.8.4 + - @effect/platform-node-shared@0.16.2 + +## 0.61.1 + +### Patch Changes + +- Updated dependencies [[`3812788`](https://github.com/Effect-TS/effect/commit/3812788d79caaab8f559a62fd443018a04ac5647), [`0e0af6d`](https://github.com/Effect-TS/effect/commit/0e0af6d6593d041bd2cdbced9fdaf8265ce166f2)]: + - @effect/platform@0.66.1 + - @effect/platform-node-shared@0.16.1 + +## 0.61.0 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform-node-shared@0.16.0 + - @effect/platform@0.66.0 + +## 0.60.5 + +### Patch Changes + +- Updated dependencies [[`321b201`](https://github.com/Effect-TS/effect/commit/321b201adcb6bbbeb806b3467dd0b4cf063ccda8), [`bb5ec6b`](https://github.com/Effect-TS/effect/commit/bb5ec6b4b6a6f537394596c5a596faf52cb2aef4)]: + - @effect/platform@0.65.5 + - effect@3.8.3 + - @effect/platform-node-shared@0.15.5 + +## 0.60.4 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform@0.65.4 + - @effect/platform-node-shared@0.15.4 + +## 0.60.3 + +### Patch Changes + +- Updated dependencies [[`f0d8ef1`](https://github.com/Effect-TS/effect/commit/f0d8ef1ce97ec2a87b09b3e24150cfeab85d6e2f)]: + - effect@3.8.2 + - @effect/platform@0.65.3 + - @effect/platform-node-shared@0.15.3 + +## 0.60.2 + +### Patch Changes + +- Updated dependencies [[`10bf621`](https://github.com/Effect-TS/effect/commit/10bf6213f36d8ddb00f058a4609b85220f3d8334), [`cd75658`](https://github.com/Effect-TS/effect/commit/cd756584c352064cb1654be7118a925d57475d49), [`ae36fa6`](https://github.com/Effect-TS/effect/commit/ae36fa68f754eeab9a54b6dc0f8b44db513aa2b6)]: + - effect@3.8.1 + - @effect/platform-node-shared@0.15.2 + - @effect/platform@0.65.2 + +## 0.60.1 + +### Patch Changes + +- Updated dependencies [[`e44c5f2`](https://github.com/Effect-TS/effect/commit/e44c5f228215738fe4e75023c7461bf9521249cb)]: + - @effect/platform@0.65.1 + - @effect/platform-node-shared@0.15.1 + +## 0.60.0 + +### Minor Changes + +- [#3541](https://github.com/Effect-TS/effect/pull/3541) [`7041393`](https://github.com/Effect-TS/effect/commit/7041393cff132e96566d3f36da0483a6ff6195e4) Thanks @tim-smart! - refactor /platform HttpClient + + #### HttpClient.fetch removed + + The `HttpClient.fetch` client implementation has been removed. Instead, you can + access a `HttpClient` using the corresponding `Context.Tag`. + + ```ts + import { FetchHttpClient, HttpClient } from "@effect/platform" + import { Effect } from "effect" + + Effect.gen(function* () { + const client = yield* HttpClient.HttpClient + + // make a get request + yield* client.get("https://jsonplaceholder.typicode.com/todos/1") + }).pipe( + Effect.scoped, + // the fetch client has been moved to the `FetchHttpClient` module + Effect.provide(FetchHttpClient.layer) + ) + ``` + + #### `HttpClient` interface now uses methods + + Instead of being a function that returns the response, the `HttpClient` + interface now uses methods to make requests. + + Some shorthand methods have been added to the `HttpClient` interface to make + less complex requests easier. + + ```ts + import { + FetchHttpClient, + HttpClient, + HttpClientRequest + } from "@effect/platform" + import { Effect } from "effect" + + Effect.gen(function* () { + const client = yield* HttpClient.HttpClient + + // make a get request + yield* client.get("https://jsonplaceholder.typicode.com/todos/1") + // make a post request + yield* client.post("https://jsonplaceholder.typicode.com/todos") + + // execute a request instance + yield* client.execute( + HttpClientRequest.get("https://jsonplaceholder.typicode.com/todos/1") + ) + }) + ``` + + #### Scoped `HttpClientResponse` helpers removed + + The `HttpClientResponse` helpers that also supplied the `Scope` have been removed. + + Instead, you can use the `HttpClientResponse` methods directly, and explicitly + add a `Effect.scoped` to the pipeline. + + ```ts + import { FetchHttpClient, HttpClient } from "@effect/platform" + import { Effect } from "effect" + + Effect.gen(function* () { + const client = yield* HttpClient.HttpClient + + yield* client.get("https://jsonplaceholder.typicode.com/todos/1").pipe( + Effect.flatMap((response) => response.json), + Effect.scoped // supply the `Scope` + ) + }) + ``` + + #### Some apis have been renamed + + Including the `HttpClientRequest` body apis, which is to make them more + discoverable. + +### Patch Changes + +- Updated dependencies [[`fcfa6ee`](https://github.com/Effect-TS/effect/commit/fcfa6ee30ffd07d998bf22799357bf58580a116f), [`bb9931b`](https://github.com/Effect-TS/effect/commit/bb9931b62e249a3b801f2cb9d097aec0c8511af7), [`5798f76`](https://github.com/Effect-TS/effect/commit/5798f7619529de33e5ba06f551806f68fedc19db), [`5f0bfa1`](https://github.com/Effect-TS/effect/commit/5f0bfa17205398d4e4818bfbcf9e1b505b3b1fc5), [`812a4e8`](https://github.com/Effect-TS/effect/commit/812a4e86e2d1aa23b477ef5829aa0e5c07784936), [`6a128f6`](https://github.com/Effect-TS/effect/commit/6a128f63f9b41fec2db70790b3bbb96cb9afa1ab), [`273565e`](https://github.com/Effect-TS/effect/commit/273565e7901639e8d0541930ab715aea9c80fbaa), [`adf7d7a`](https://github.com/Effect-TS/effect/commit/adf7d7a7dfce3a7021e9f3b0d847dc85be89d754), [`569a801`](https://github.com/Effect-TS/effect/commit/569a8017ef0a0bc203e4312867cbdd37b0effbd7), [`aa1fa53`](https://github.com/Effect-TS/effect/commit/aa1fa5301e886b9657c8eb0d38cb87cef92a8305), [`02f6b06`](https://github.com/Effect-TS/effect/commit/02f6b0660e12bee1069532a9cc18d3ab855257be), [`12b893e`](https://github.com/Effect-TS/effect/commit/12b893e63cc6dfada4aca7773b4783940e2edf25), [`bbad27e`](https://github.com/Effect-TS/effect/commit/bbad27ec0a90860593f759405caa877e7f4a655f), [`7041393`](https://github.com/Effect-TS/effect/commit/7041393cff132e96566d3f36da0483a6ff6195e4), [`e0d21a5`](https://github.com/Effect-TS/effect/commit/e0d21a54c8323728fbb75a32f4820a9996257809), [`adf7d7a`](https://github.com/Effect-TS/effect/commit/adf7d7a7dfce3a7021e9f3b0d847dc85be89d754), [`007289a`](https://github.com/Effect-TS/effect/commit/007289a52d5877f8e90e2dacf38171ff9bf603fd), [`42a8f99`](https://github.com/Effect-TS/effect/commit/42a8f99740eefdaf2c4544d2c345313f97547a36), [`eebfd29`](https://github.com/Effect-TS/effect/commit/eebfd29633fd5d38b505c5c0842036f61f05e913), [`040703d`](https://github.com/Effect-TS/effect/commit/040703d0e100cd5511e52d812c15492414262b5e)]: + - effect@3.8.0 + - @effect/platform@0.65.0 + - @effect/platform-node-shared@0.15.0 + +## 0.59.1 + +### Patch Changes + +- Updated dependencies [[`35a0f81`](https://github.com/Effect-TS/effect/commit/35a0f813141652d696461cd5d19fd146adaf85be), [`8261c5a`](https://github.com/Effect-TS/effect/commit/8261c5ae6fe86872292ec1fc1a58ab9cea2f5f51)]: + - effect@3.7.3 + - @effect/platform@0.64.1 + - @effect/platform-node-shared@0.14.1 + +## 0.59.0 + +### Minor Changes + +- [#3565](https://github.com/Effect-TS/effect/pull/3565) [`90ac8f6`](https://github.com/Effect-TS/effect/commit/90ac8f6f6053a2e4498f8b0cc56fe12777d02e1a) Thanks @tim-smart! - move Etag implementation to /platform + +### Patch Changes + +- Updated dependencies [[`90ac8f6`](https://github.com/Effect-TS/effect/commit/90ac8f6f6053a2e4498f8b0cc56fe12777d02e1a), [`90ac8f6`](https://github.com/Effect-TS/effect/commit/90ac8f6f6053a2e4498f8b0cc56fe12777d02e1a), [`3791e24`](https://github.com/Effect-TS/effect/commit/3791e241636b1dfe924a56f380ebc9a7ff0827a9), [`3791e24`](https://github.com/Effect-TS/effect/commit/3791e241636b1dfe924a56f380ebc9a7ff0827a9)]: + - @effect/platform-node-shared@0.14.0 + - @effect/platform@0.64.0 + +## 0.58.3 + +### Patch Changes + +- Updated dependencies [[`4a701c4`](https://github.com/Effect-TS/effect/commit/4a701c406da032563fedae459536c00ae5cfe3c7), [`64c2292`](https://github.com/Effect-TS/effect/commit/64c22927aa01e0159b0fa98ff55e742069af399c)]: + - @effect/platform@0.63.3 + - @effect/platform-node-shared@0.13.3 + +## 0.58.2 + +### Patch Changes + +- Updated dependencies [[`8a601d7`](https://github.com/Effect-TS/effect/commit/8a601d7a1f8ffe52ac9e6d67e9282a1495fe59c9), [`353ba19`](https://github.com/Effect-TS/effect/commit/353ba19f9b2b9e959f0a00d058c6d40a4bc02db7)]: + - effect@3.7.2 + - @effect/platform@0.63.2 + - @effect/platform-node-shared@0.13.2 + +## 0.58.1 + +### Patch Changes + +- Updated dependencies [[`79859e7`](https://github.com/Effect-TS/effect/commit/79859e71040d8edf1868b8530b90c650f4321eff), [`f6a469c`](https://github.com/Effect-TS/effect/commit/f6a469c190b9f00eee5ea0cd4d5912a0ef8b46f5), [`dcb9ec0`](https://github.com/Effect-TS/effect/commit/dcb9ec0db443894dd204d87450f779c44b9ad7f1), [`79aa6b1`](https://github.com/Effect-TS/effect/commit/79aa6b136e1f29b36f34e88cb2ff162bff2bb4ed)]: + - effect@3.7.1 + - @effect/platform@0.63.1 + - @effect/platform-node-shared@0.13.1 + +## 0.58.0 + +### Patch Changes + +- Updated dependencies [[`db89601`](https://github.com/Effect-TS/effect/commit/db89601ee9c1050c4e762b7bd7ec65a6a2799dfe), [`2f456cc`](https://github.com/Effect-TS/effect/commit/2f456cce5012b9fcb6b4e039190d527813b75b92), [`8745e41`](https://github.com/Effect-TS/effect/commit/8745e41ed96e3765dc6048efc2a9afbe05c8a1e9), [`e557838`](https://github.com/Effect-TS/effect/commit/e55783886b046d3c5f33447f455f9ccf2fa75922), [`d6e7e40`](https://github.com/Effect-TS/effect/commit/d6e7e40b1e2ad0c59aa02f07344d28601b14ebdc), [`8356321`](https://github.com/Effect-TS/effect/commit/8356321598da04bd77c1001f45a4e447bec5591d), [`192f2eb`](https://github.com/Effect-TS/effect/commit/192f2ebb2c4ddbf4bfd8baedd32140b2376868f4), [`718cb70`](https://github.com/Effect-TS/effect/commit/718cb70038629a6d58d02e407760e341f7c94474), [`e9d0310`](https://github.com/Effect-TS/effect/commit/e9d03107acbf204d9304f3e8aea0816b7d3c7dfb), [`6bf28f7`](https://github.com/Effect-TS/effect/commit/6bf28f7e3b1e5e0608ff567205fea0581d11666f)]: + - effect@3.7.0 + - @effect/platform@0.63.0 + - @effect/platform-node-shared@0.13.0 + +## 0.57.5 + +### Patch Changes + +- Updated dependencies [[`e809286`](https://github.com/Effect-TS/effect/commit/e8092865900608c4df7a6b7991b1c13cc1e4ca2d)]: + - effect@3.6.8 + - @effect/platform@0.62.5 + - @effect/platform-node-shared@0.12.5 + +## 0.57.4 + +### Patch Changes + +- Updated dependencies [[`e7a65e3`](https://github.com/Effect-TS/effect/commit/e7a65e3c6a08636bbfce3d3af3098bf28474364d), [`50ec889`](https://github.com/Effect-TS/effect/commit/50ec8897a49b7d1fe84f63107f89d543c52f3dfc)]: + - @effect/platform-node-shared@0.12.4 + - @effect/platform@0.62.4 + - effect@3.6.7 + +## 0.57.3 + +### Patch Changes + +- Updated dependencies [[`f960bf4`](https://github.com/Effect-TS/effect/commit/f960bf45239e9badac6e0ad3a602f4174cd7bbdf), [`46a575f`](https://github.com/Effect-TS/effect/commit/46a575f48a05457b782fb21f7827d338c9b59320)]: + - effect@3.6.6 + - @effect/platform@0.62.3 + - @effect/platform-node-shared@0.12.3 + +## 0.57.2 + +### Patch Changes + +- Updated dependencies [[`413994c`](https://github.com/Effect-TS/effect/commit/413994c9792f16d9d57cca3ae6eb254bf93bd261), [`14a47a8`](https://github.com/Effect-TS/effect/commit/14a47a8c1f3cff2186b8fe7a919a1d773888fb5b), [`0c09841`](https://github.com/Effect-TS/effect/commit/0c0984173be3d58f050b300a1a8aa89d76ba49ae)]: + - @effect/platform@0.62.2 + - effect@3.6.5 + - @effect/platform-node-shared@0.12.2 + +## 0.57.1 + +### Patch Changes + +- Updated dependencies [[`9efe0e5`](https://github.com/Effect-TS/effect/commit/9efe0e5b57ac557399be620822c21cc6e9add285)]: + - @effect/platform@0.62.1 + - @effect/platform-node-shared@0.12.1 + +## 0.57.0 + +### Patch Changes + +- [#3450](https://github.com/Effect-TS/effect/pull/3450) [`0e42a8f`](https://github.com/Effect-TS/effect/commit/0e42a8f045ecb1fd3d080edf3d49fef16a9b0ca1) Thanks @tim-smart! - update dependencies + +- Updated dependencies [[`8295281`](https://github.com/Effect-TS/effect/commit/8295281ae9bd7441e680402540bf3c8682ec417b), [`c940df6`](https://github.com/Effect-TS/effect/commit/c940df63800bf3c4396d91cf28ec34938642fd2c), [`00b6c6d`](https://github.com/Effect-TS/effect/commit/00b6c6d4001f5de728b7d990a1b14560b4961a63), [`f8d95a6`](https://github.com/Effect-TS/effect/commit/f8d95a61ad0762147933c5c32bb6d7237e18eef4)]: + - effect@3.6.4 + - @effect/platform@0.62.0 + - @effect/platform-node-shared@0.12.0 + +## 0.56.9 + +### Patch Changes + +- Updated dependencies [[`04adcac`](https://github.com/Effect-TS/effect/commit/04adcace913e6fc483df266874a68005e9e04ccf)]: + - effect@3.6.3 + - @effect/platform@0.61.8 + - @effect/platform-node-shared@0.11.8 + +## 0.56.8 + +### Patch Changes + +- Updated dependencies [[`17245a4`](https://github.com/Effect-TS/effect/commit/17245a4e783c19dee51529600b3b40f164fa59bc), [`630d40e`](https://github.com/Effect-TS/effect/commit/630d40eaa7eb4d2f8b6705b16d4f426bc28a7d09), [`630d40e`](https://github.com/Effect-TS/effect/commit/630d40eaa7eb4d2f8b6705b16d4f426bc28a7d09)]: + - @effect/platform@0.61.7 + - @effect/platform-node-shared@0.11.7 + +## 0.56.7 + +### Patch Changes + +- Updated dependencies [[`d829b57`](https://github.com/Effect-TS/effect/commit/d829b576357f2e3b203ab7e107a1492de903a106), [`fd4b2f6`](https://github.com/Effect-TS/effect/commit/fd4b2f6516b325740dde615f1cf0229edf13ca0c)]: + - @effect/platform@0.61.6 + - effect@3.6.2 + - @effect/platform-node-shared@0.11.6 + +## 0.56.6 + +### Patch Changes + +- [#3428](https://github.com/Effect-TS/effect/pull/3428) [`76b0496`](https://github.com/Effect-TS/effect/commit/76b0496ff9d7670e3f4c07ae924d30ed7f613cee) Thanks @tim-smart! - fix for missing global undici dispatcher + +## 0.56.5 + +### Patch Changes + +- [#3409](https://github.com/Effect-TS/effect/pull/3409) [`056b710`](https://github.com/Effect-TS/effect/commit/056b7108978e70612176c23991916f678d947f38) Thanks @sukovanej! - Add `NodeHttpServer.layerTest`. + + ```ts + import { HttpClientRequest, HttpRouter, HttpServer } from "@effect/platform" + import { NodeHttpServer } from "@effect/platform-node" + import { expect, it } from "@effect/vitest" + import { Effect } from "effect" + + it.scoped("test", () => + Effect.gen(function* () { + yield* HttpServer.serveEffect(HttpRouter.empty) + const response = yield* HttpClientRequest.get("/") + expect(response.status, 404) + }).pipe(Effect.provide(NodeHttpServer.layerTest)) + ) + ``` + +- Updated dependencies [[`056b710`](https://github.com/Effect-TS/effect/commit/056b7108978e70612176c23991916f678d947f38)]: + - @effect/platform@0.61.5 + - @effect/platform-node-shared@0.11.5 + +## 0.56.4 + +### Patch Changes + +- Updated dependencies [[`e7cb109`](https://github.com/Effect-TS/effect/commit/e7cb109d0754207024a64d55b6bd2a674dd8ed7d)]: + - @effect/platform@0.61.4 + - @effect/platform-node-shared@0.11.4 + +## 0.56.3 + +### Patch Changes + +- Updated dependencies [[`510a34d`](https://github.com/Effect-TS/effect/commit/510a34d4cc5d2f51347a53847f6c7db84d2b17c6), [`fb9f786`](https://github.com/Effect-TS/effect/commit/fb9f7867f0c895e63f9ef23e8d0941248c42179d), [`45dbb9f`](https://github.com/Effect-TS/effect/commit/45dbb9ffeaf93d9e4df99d0cd4920e41ba9a3978)]: + - effect@3.6.1 + - @effect/platform@0.61.3 + - @effect/platform-node-shared@0.11.3 + +## 0.56.2 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform@0.61.2 + - @effect/platform-node-shared@0.11.2 + +## 0.56.1 + +### Patch Changes + +- Updated dependencies [[`11223bf`](https://github.com/Effect-TS/effect/commit/11223bf9cbf5b822e0bf9a9fb2b35b2ad88af692)]: + - @effect/platform@0.61.1 + - @effect/platform-node-shared@0.11.1 + +## 0.56.0 + +### Patch Changes + +- Updated dependencies [[`1e0fe80`](https://github.com/Effect-TS/effect/commit/1e0fe802b36c257971296617473ce0abe730e8dc), [`8135294`](https://github.com/Effect-TS/effect/commit/8135294b591ea94fde7e6f94a504608f0e630520), [`cd255a4`](https://github.com/Effect-TS/effect/commit/cd255a48872d8fb924cf713ef73f0883a9cc6987), [`3845646`](https://github.com/Effect-TS/effect/commit/3845646828e98f3c7cda1217f6cfe5f642ac0603), [`2d09078`](https://github.com/Effect-TS/effect/commit/2d09078c5948b37fc2f79ef858fe4ca3e4814085), [`4bce5a0`](https://github.com/Effect-TS/effect/commit/4bce5a0274203550ccf117d830721891b0a3d182), [`4ddbff0`](https://github.com/Effect-TS/effect/commit/4ddbff0bb4e3ffddfeb509c59835b83245fb975e), [`e74cc38`](https://github.com/Effect-TS/effect/commit/e74cc38cb420a320c4d7ef98180f19d452a8b316), [`bb069b4`](https://github.com/Effect-TS/effect/commit/bb069b49ef291c532a02c1e8e74271f6d1bb32ec), [`cd255a4`](https://github.com/Effect-TS/effect/commit/cd255a48872d8fb924cf713ef73f0883a9cc6987), [`7d02174`](https://github.com/Effect-TS/effect/commit/7d02174af3bcbf054e5cdddb821c91d0f47e8285)]: + - effect@3.6.0 + - @effect/platform@0.61.0 + - @effect/platform-node-shared@0.11.0 + +## 0.55.3 + +### Patch Changes + +- Updated dependencies [[`6359644`](https://github.com/Effect-TS/effect/commit/635964446323cf55d4060559337e710e4a24496e), [`7f41e42`](https://github.com/Effect-TS/effect/commit/7f41e428830bf3043b8be0d28dcd235d5747c942), [`f566fd1`](https://github.com/Effect-TS/effect/commit/f566fd1d7eea531a0d981dd24037f14a603a1273)]: + - effect@3.5.9 + - @effect/platform@0.60.3 + - @effect/platform-node-shared@0.10.3 + +## 0.55.2 + +### Patch Changes + +- Updated dependencies [[`eb4d014`](https://github.com/Effect-TS/effect/commit/eb4d014c559e1b4c95b3fb9295fe77593c17ed7a), [`1ba640c`](https://github.com/Effect-TS/effect/commit/1ba640c702f187a866023bf043c26e25cce941ef), [`c8c71bd`](https://github.com/Effect-TS/effect/commit/c8c71bd20eb87d23133dac6156b83bb08941597c), [`a26ce58`](https://github.com/Effect-TS/effect/commit/a26ce581ca7d407e1e81439b58c8045b3fa65231), [`fc20f73`](https://github.com/Effect-TS/effect/commit/fc20f73c69e577981cb64714de2adc97e1004dae)]: + - @effect/platform@0.60.2 + - effect@3.5.8 + - @effect/platform-node-shared@0.10.2 + +## 0.55.1 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform@0.60.1 + - @effect/platform-node-shared@0.10.1 + +## 0.55.0 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform@0.60.0 + - @effect/platform-node-shared@0.10.0 + +## 0.54.4 + +### Patch Changes + +- [#3310](https://github.com/Effect-TS/effect/pull/3310) [`99bddcf`](https://github.com/Effect-TS/effect/commit/99bddcfb3d6eab4d489d055404e26ad81afe52fc) Thanks @fubhy! - Added additional pure annotations to improve tree-shakeability + +- Updated dependencies [[`3afcc93`](https://github.com/Effect-TS/effect/commit/3afcc93413a3d910beb69e4ce9ae120e4adaffd5), [`99bddcf`](https://github.com/Effect-TS/effect/commit/99bddcfb3d6eab4d489d055404e26ad81afe52fc)]: + - effect@3.5.7 + - @effect/platform@0.59.3 + - @effect/platform-node-shared@0.9.3 + +## 0.54.3 + +### Patch Changes + +- Updated dependencies [[`cc327a1`](https://github.com/Effect-TS/effect/commit/cc327a1bccd22a4ee27ec7e58b53205e93b23e2c), [`4bfe4fb`](https://github.com/Effect-TS/effect/commit/4bfe4fb5c82f597c9beea9baa92e772593598b60), [`2b14d18`](https://github.com/Effect-TS/effect/commit/2b14d181462cad8359da4fa6bc6dfda0f742c398)]: + - effect@3.5.6 + - @effect/platform@0.59.2 + - @effect/platform-node-shared@0.9.2 + +## 0.54.2 + +### Patch Changes + +- Updated dependencies [[`a9d7800`](https://github.com/Effect-TS/effect/commit/a9d7800f6a253192b653d77778b0674f39b1ca39), [`fcecff7`](https://github.com/Effect-TS/effect/commit/fcecff7f7e12b295a252f124861b801c73072151), [`adbf753`](https://github.com/Effect-TS/effect/commit/adbf75340a9db15dc5cadc66e911a8978a195a65), [`adbf753`](https://github.com/Effect-TS/effect/commit/adbf75340a9db15dc5cadc66e911a8978a195a65)]: + - effect@3.5.5 + - @effect/platform@0.59.1 + - @effect/platform-node-shared@0.9.1 + +## 0.54.1 + +### Patch Changes + +- [#3265](https://github.com/Effect-TS/effect/pull/3265) [`07db4ac`](https://github.com/Effect-TS/effect/commit/07db4ac8da9d07ce31bd62470a73e362a4291a0c) Thanks @tim-smart! - add NodeHttpServerRequest.toServerResponse for accessing the raw node response + +## 0.54.0 + +### Minor Changes + +- [#3260](https://github.com/Effect-TS/effect/pull/3260) [`53c0db0`](https://github.com/Effect-TS/effect/commit/53c0db06872d5b5edea2a706e83249908385325c) Thanks @tim-smart! - replace /platform RefailError with use of the "cause" property + +- [#3255](https://github.com/Effect-TS/effect/pull/3255) [`ada68b3`](https://github.com/Effect-TS/effect/commit/ada68b3e61c67907c2a281c024c84d818186ca4c) Thanks @tim-smart! - refactor & simplify /platform backing workers + + Improves worker performance by 2x + +### Patch Changes + +- [#3253](https://github.com/Effect-TS/effect/pull/3253) [`ed0dde4`](https://github.com/Effect-TS/effect/commit/ed0dde4888e6f1a97ad5bba06b755d26a6a1c52e) Thanks @tim-smart! - update dependencies + +- Updated dependencies [[`53c0db0`](https://github.com/Effect-TS/effect/commit/53c0db06872d5b5edea2a706e83249908385325c), [`ed0dde4`](https://github.com/Effect-TS/effect/commit/ed0dde4888e6f1a97ad5bba06b755d26a6a1c52e), [`ada68b3`](https://github.com/Effect-TS/effect/commit/ada68b3e61c67907c2a281c024c84d818186ca4c), [`ca775ce`](https://github.com/Effect-TS/effect/commit/ca775cec53baebc1a43d9b8852a3ac6726178498), [`5be9cc0`](https://github.com/Effect-TS/effect/commit/5be9cc044025a9541b9b7acefa2d3fc05fa1301b), [`203658f`](https://github.com/Effect-TS/effect/commit/203658f8001c132b25764ab70344b171683b554c), [`eb1c4d4`](https://github.com/Effect-TS/effect/commit/eb1c4d44e54b9d8d201a366d1ff94face2a6dcd3)]: + - @effect/platform-node-shared@0.9.0 + - @effect/platform@0.59.0 + - effect@3.5.4 + +## 0.53.26 + +### Patch Changes + +- Updated dependencies [[`edb0da3`](https://github.com/Effect-TS/effect/commit/edb0da383746d760f35d8582f5fb0cc0eeca9217), [`a1db40a`](https://github.com/Effect-TS/effect/commit/a1db40a650ab842e778654f0d88e80f2ef4fd6f3), [`c8d3fb0`](https://github.com/Effect-TS/effect/commit/c8d3fb0fe23585f6efb724af51fbab3ba1ad6e83), [`dabd028`](https://github.com/Effect-TS/effect/commit/dabd028decf9b7983ca16ebe0f48c05c11a84b68), [`786b2ab`](https://github.com/Effect-TS/effect/commit/786b2ab29d525c877bb84035dac9e2d6499339d1), [`fc57354`](https://github.com/Effect-TS/effect/commit/fc573547d41667016fce05eaee75960fcc6dce4d)]: + - effect@3.5.3 + - @effect/platform@0.58.27 + - @effect/platform-node-shared@0.8.26 + +## 0.53.25 + +### Patch Changes + +- Updated dependencies [[`639208e`](https://github.com/Effect-TS/effect/commit/639208eeb8a44622994f832bc2d45d06ab636bc8), [`6684b4c`](https://github.com/Effect-TS/effect/commit/6684b4c27d77a7fcc7af2e261a450edf971b62b5), [`6684b4c`](https://github.com/Effect-TS/effect/commit/6684b4c27d77a7fcc7af2e261a450edf971b62b5), [`6684b4c`](https://github.com/Effect-TS/effect/commit/6684b4c27d77a7fcc7af2e261a450edf971b62b5)]: + - effect@3.5.2 + - @effect/platform@0.58.26 + - @effect/platform-node-shared@0.8.25 + +## 0.53.24 + +### Patch Changes + +- Updated dependencies [[`0623fca`](https://github.com/Effect-TS/effect/commit/0623fca41679b0e3c5a10dd0f8985f91670bd721)]: + - @effect/platform@0.58.25 + - @effect/platform-node-shared@0.8.24 + +## 0.53.23 + +### Patch Changes + +- Updated dependencies [[`55fdd76`](https://github.com/Effect-TS/effect/commit/55fdd761ee95afd73b6a892c13fee92b36c02837)]: + - effect@3.5.1 + - @effect/platform@0.58.24 + - @effect/platform-node-shared@0.8.23 + +## 0.53.22 + +### Patch Changes + +- Updated dependencies [[`a1f5b83`](https://github.com/Effect-TS/effect/commit/a1f5b831a1bc7535988b370d68d0b3eb1123e0ce), [`a1f5b83`](https://github.com/Effect-TS/effect/commit/a1f5b831a1bc7535988b370d68d0b3eb1123e0ce), [`a1f5b83`](https://github.com/Effect-TS/effect/commit/a1f5b831a1bc7535988b370d68d0b3eb1123e0ce), [`60bc3d0`](https://github.com/Effect-TS/effect/commit/60bc3d0867b13e48b24dc22604b4dd2e7b2c1ca4), [`5ab348f`](https://github.com/Effect-TS/effect/commit/5ab348f265db3d283aa091ddca6d2d49137c16f2), [`60bc3d0`](https://github.com/Effect-TS/effect/commit/60bc3d0867b13e48b24dc22604b4dd2e7b2c1ca4), [`3e04bf8`](https://github.com/Effect-TS/effect/commit/3e04bf8a7127e956cadb7684a8f4c661df57663b), [`e7fc45f`](https://github.com/Effect-TS/effect/commit/e7fc45f0c7002aafdaec7878149ac064cd104ea3), [`a1f5b83`](https://github.com/Effect-TS/effect/commit/a1f5b831a1bc7535988b370d68d0b3eb1123e0ce), [`4626de5`](https://github.com/Effect-TS/effect/commit/4626de59c25b384216faa0be87bf0b8cd36357d0), [`f01e7db`](https://github.com/Effect-TS/effect/commit/f01e7db317827255d7901f523f2e28b43298e8df), [`60bc3d0`](https://github.com/Effect-TS/effect/commit/60bc3d0867b13e48b24dc22604b4dd2e7b2c1ca4), [`79d2d91`](https://github.com/Effect-TS/effect/commit/79d2d91464d95dde0e9444d43e7a7f309f05d6e6), [`ac71f37`](https://github.com/Effect-TS/effect/commit/ac71f378f2413e5aa91c95f649ffe898d6a26114), [`8432360`](https://github.com/Effect-TS/effect/commit/8432360ce68614a419bb328083a4109d0fc8aa93), [`e4bf1bf`](https://github.com/Effect-TS/effect/commit/e4bf1bf2b4a970eacd77c9b77b5ea8c68bc84498), [`13cb861`](https://github.com/Effect-TS/effect/commit/13cb861a5eded15c55c6cdcf6a8acde8320367a6), [`79d2d91`](https://github.com/Effect-TS/effect/commit/79d2d91464d95dde0e9444d43e7a7f309f05d6e6), [`e7fc45f`](https://github.com/Effect-TS/effect/commit/e7fc45f0c7002aafdaec7878149ac064cd104ea3), [`9f66825`](https://github.com/Effect-TS/effect/commit/9f66825f1fce0fe8d10420c285f7dc4c71e8af8d)]: + - effect@3.5.0 + - @effect/platform@0.58.23 + - @effect/platform-node-shared@0.8.22 + +## 0.53.21 + +### Patch Changes + +- [#3209](https://github.com/Effect-TS/effect/pull/3209) [`366f2ee`](https://github.com/Effect-TS/effect/commit/366f2ee3fb6f712a44e8f84fc188612e5ecc016d) Thanks @tim-smart! - simplify /platform http response handling + +- Updated dependencies [[`7af137c`](https://github.com/Effect-TS/effect/commit/7af137c9433f6e74959b3887561ec1e6f12e10ee), [`ee4b3dc`](https://github.com/Effect-TS/effect/commit/ee4b3dc5f68d19dc3ae1c2d12901c5b8ffbebabb), [`366f2ee`](https://github.com/Effect-TS/effect/commit/366f2ee3fb6f712a44e8f84fc188612e5ecc016d), [`366f2ee`](https://github.com/Effect-TS/effect/commit/366f2ee3fb6f712a44e8f84fc188612e5ecc016d), [`097d25c`](https://github.com/Effect-TS/effect/commit/097d25cb5d13c049e01789651be56b09620186ef)]: + - effect@3.4.9 + - @effect/platform@0.58.22 + - @effect/platform-node-shared@0.8.21 + +## 0.53.20 + +### Patch Changes + +- Updated dependencies [[`a435e0f`](https://github.com/Effect-TS/effect/commit/a435e0fc5378b33a49bcec92ee235df6f16a2419), [`b5554db`](https://github.com/Effect-TS/effect/commit/b5554db36c4dd6f64fa5e6a62a29b2759c54217a), [`a9c4fb3`](https://github.com/Effect-TS/effect/commit/a9c4fb3bf3c6e92cd1c142b0605fddf7eb3c697c)]: + - effect@3.4.8 + - @effect/platform@0.58.21 + - @effect/platform-node-shared@0.8.20 + +## 0.53.19 + +### Patch Changes + +- Updated dependencies [[`a5737d6`](https://github.com/Effect-TS/effect/commit/a5737d6db2b921605c332eabbc5402ee3d17357b)]: + - effect@3.4.7 + - @effect/platform@0.58.20 + - @effect/platform-node-shared@0.8.19 + +## 0.53.18 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform@0.58.19 + - @effect/platform-node-shared@0.8.18 + +## 0.53.17 + +### Patch Changes + +- Updated dependencies [[`7f8900a`](https://github.com/Effect-TS/effect/commit/7f8900a1de9addeb0d371103a2c5c2aa3e4ff95e)]: + - @effect/platform@0.58.18 + - @effect/platform-node-shared@0.8.17 + +## 0.53.16 + +### Patch Changes + +- Updated dependencies [[`5c0ceb0`](https://github.com/Effect-TS/effect/commit/5c0ceb00826cce9e50bf9d41d83e191d5352c030), [`5c0ceb0`](https://github.com/Effect-TS/effect/commit/5c0ceb00826cce9e50bf9d41d83e191d5352c030), [`33735b1`](https://github.com/Effect-TS/effect/commit/33735b16b41bd26929d8f4754c190925db6323b7), [`5c0ceb0`](https://github.com/Effect-TS/effect/commit/5c0ceb00826cce9e50bf9d41d83e191d5352c030), [`139d4b3`](https://github.com/Effect-TS/effect/commit/139d4b39fb3bff2eeaa7c0c809c581da42425a83)]: + - effect@3.4.6 + - @effect/platform@0.58.17 + - @effect/platform-node-shared@0.8.16 + +## 0.53.15 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform@0.58.16 + - @effect/platform-node-shared@0.8.15 + +## 0.53.14 + +### Patch Changes + +- Updated dependencies [[`baa90df`](https://github.com/Effect-TS/effect/commit/baa90df9663f5f37d7b6814dad25142d53dbc720)]: + - @effect/platform@0.58.15 + - @effect/platform-node-shared@0.8.14 + +## 0.53.13 + +### Patch Changes + +- Updated dependencies [[`52a87c7`](https://github.com/Effect-TS/effect/commit/52a87c7a0b9536398deaf8ec507e53a82c607219), [`6d2280e`](https://github.com/Effect-TS/effect/commit/6d2280e9497c95cb0e965ca462c825345074eedf)]: + - @effect/platform@0.58.14 + - @effect/platform-node-shared@0.8.13 + +## 0.53.12 + +### Patch Changes + +- Updated dependencies [[`a047af9`](https://github.com/Effect-TS/effect/commit/a047af99447dfffc729e9c8ef0ca143537927e91), [`dbd53ea`](https://github.com/Effect-TS/effect/commit/dbd53ea363c71a24449cb068251054c3a1acf864)]: + - effect@3.4.5 + - @effect/platform@0.58.13 + - @effect/platform-node-shared@0.8.12 + +## 0.53.11 + +### Patch Changes + +- Updated dependencies [[`74e0ad2`](https://github.com/Effect-TS/effect/commit/74e0ad23b4c36f41b7fd10856b20f8b701bc4044), [`74e0ad2`](https://github.com/Effect-TS/effect/commit/74e0ad23b4c36f41b7fd10856b20f8b701bc4044), [`72638e3`](https://github.com/Effect-TS/effect/commit/72638e3d99f0e93a24febf6c225256ce92d4a20b), [`d7dde2b`](https://github.com/Effect-TS/effect/commit/d7dde2b4af08b37af859d4c327c1f5c6f00cf9d9), [`9b2fc3b`](https://github.com/Effect-TS/effect/commit/9b2fc3b9dfd304a2bd0508ef2313cfc54357be0c)]: + - @effect/platform@0.58.12 + - effect@3.4.4 + - @effect/platform-node-shared@0.8.11 + +## 0.53.10 + +### Patch Changes + +- Updated dependencies [[`a5b95b5`](https://github.com/Effect-TS/effect/commit/a5b95b548284e4798654ae7ce6883fa49108f0ea), [`5e29579`](https://github.com/Effect-TS/effect/commit/5e29579187cb8420ea4930b3999fec984f8999f4)]: + - @effect/platform@0.58.11 + - @effect/platform-node-shared@0.8.10 + +## 0.53.9 + +### Patch Changes + +- [#3079](https://github.com/Effect-TS/effect/pull/3079) [`bbdd365`](https://github.com/Effect-TS/effect/commit/bbdd36567706c94cdec45bacea825941c347b6cd) Thanks @tim-smart! - update dependencies + +- [#3079](https://github.com/Effect-TS/effect/pull/3079) [`bbdd365`](https://github.com/Effect-TS/effect/commit/bbdd36567706c94cdec45bacea825941c347b6cd) Thanks @tim-smart! - update to typescript 5.5 + +- Updated dependencies [[`a48ee84`](https://github.com/Effect-TS/effect/commit/a48ee845ac21bbde9baf938af9e97a98322211c9), [`c342739`](https://github.com/Effect-TS/effect/commit/c3427396226e1ad7b95b40595a23f9bdff3e3365), [`8898e5e`](https://github.com/Effect-TS/effect/commit/8898e5e238622f6337583d91ee23609c1f5ccdf7), [`a48ee84`](https://github.com/Effect-TS/effect/commit/a48ee845ac21bbde9baf938af9e97a98322211c9), [`a48ee84`](https://github.com/Effect-TS/effect/commit/a48ee845ac21bbde9baf938af9e97a98322211c9), [`ff78636`](https://github.com/Effect-TS/effect/commit/ff786367c522975f40f0f179a0ecdfcfab7ecbdb), [`c86bd4e`](https://github.com/Effect-TS/effect/commit/c86bd4e134c23146c216f9ff97e03781d55991b6), [`bbdd365`](https://github.com/Effect-TS/effect/commit/bbdd36567706c94cdec45bacea825941c347b6cd), [`ab3180f`](https://github.com/Effect-TS/effect/commit/ab3180f827041d0ea3b2d72254a1a8683e99e056), [`bbdd365`](https://github.com/Effect-TS/effect/commit/bbdd36567706c94cdec45bacea825941c347b6cd)]: + - @effect/platform@0.58.10 + - effect@3.4.3 + - @effect/platform-node-shared@0.8.9 + +## 0.53.8 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform@0.58.9 + - @effect/platform-node-shared@0.8.8 + +## 0.53.7 + +### Patch Changes + +- Updated dependencies [[`3da1497`](https://github.com/Effect-TS/effect/commit/3da1497b5c9cc886d300258bc928fd68a4fefe6f)]: + - effect@3.4.2 + - @effect/platform@0.58.8 + - @effect/platform-node-shared@0.8.7 + +## 0.53.6 + +### Patch Changes + +- Updated dependencies [[`027004a`](https://github.com/Effect-TS/effect/commit/027004a897f654791e75faa28eefb50dd0244b6e)]: + - @effect/platform@0.58.7 + - @effect/platform-node-shared@0.8.6 + +## 0.53.5 + +### Patch Changes + +- Updated dependencies [[`66a1910`](https://github.com/Effect-TS/effect/commit/66a19109ff90c4252123b8809b8c8a74681dba6a), [`2e8e252`](https://github.com/Effect-TS/effect/commit/2e8e2520cac712f0eb644553bd476429ebd674e4)]: + - effect@3.4.1 + - @effect/platform@0.58.6 + - @effect/platform-node-shared@0.8.5 + +## 0.53.4 + +### Patch Changes + +- Updated dependencies [[`37a07a2`](https://github.com/Effect-TS/effect/commit/37a07a2d8d1ce09ab965c0ada84a3fae9a6aba05)]: + - @effect/platform@0.58.5 + - @effect/platform-node-shared@0.8.4 + +## 0.53.3 + +### Patch Changes + +- Updated dependencies [[`b77fb0a`](https://github.com/Effect-TS/effect/commit/b77fb0a811ec1ad0e794917077c9a90824515db8)]: + - @effect/platform@0.58.4 + - @effect/platform-node-shared@0.8.3 + +## 0.53.2 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform@0.58.3 + - @effect/platform-node-shared@0.8.2 + +## 0.53.1 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform@0.58.2 + - @effect/platform-node-shared@0.8.1 + +## 0.53.0 + +### Minor Changes + +- [#3036](https://github.com/Effect-TS/effect/pull/3036) [`5a248aa`](https://github.com/Effect-TS/effect/commit/5a248aa5ab2db3f7131ebc79bb9871a76de57973) Thanks @tim-smart! - rename NodeSocket.fromNetSocket to .fromDuplex + +### Patch Changes + +- Updated dependencies [[`5a248aa`](https://github.com/Effect-TS/effect/commit/5a248aa5ab2db3f7131ebc79bb9871a76de57973), [`5a248aa`](https://github.com/Effect-TS/effect/commit/5a248aa5ab2db3f7131ebc79bb9871a76de57973)]: + - @effect/platform@0.58.1 + - @effect/platform-node-shared@0.8.0 + +## 0.52.0 + +### Minor Changes + +- [#2938](https://github.com/Effect-TS/effect/pull/2938) [`63dd0c3`](https://github.com/Effect-TS/effect/commit/63dd0c3af45876c1caad7d03356c74daf551c628) Thanks @tim-smart! - restructure platform http to use flattened modules + + Instead of using the previous re-exports, you now use the modules directly. + + Before: + + ```ts + import { HttpClient } from "@effect/platform" + + HttpClient.request.get("/").pipe(HttpClient.client.fetchOk) + ``` + + After: + + ```ts + import { HttpClient, HttpClientRequest } from "@effect/platform" + + HttpClientRequest.get("/").pipe(HttpClient.fetchOk) + ``` + +### Patch Changes + +- Updated dependencies [[`c0ce180`](https://github.com/Effect-TS/effect/commit/c0ce180861ad0938053c0e6145e813fa6404df3b), [`63dd0c3`](https://github.com/Effect-TS/effect/commit/63dd0c3af45876c1caad7d03356c74daf551c628), [`61707b6`](https://github.com/Effect-TS/effect/commit/61707b6ffc7397c2ba0dce22512b44955724f60f), [`9c1b5b3`](https://github.com/Effect-TS/effect/commit/9c1b5b39e6c19604ce834f072a114ad392c50a06), [`a35faf8`](https://github.com/Effect-TS/effect/commit/a35faf8d116f94899bfc03feab33b004c8ddfdf7), [`ff73c0c`](https://github.com/Effect-TS/effect/commit/ff73c0cacd66132bfad2e5211b3eae347729c667), [`984d516`](https://github.com/Effect-TS/effect/commit/984d516ccd9412dc41188f6a46b748dd20dd5848), [`8c3b8a2`](https://github.com/Effect-TS/effect/commit/8c3b8a2ce208eab753b6206a51605a424f104e98), [`017e2f9`](https://github.com/Effect-TS/effect/commit/017e2f9b371ce24ea4945e5d7390c934ad3c39cf), [`91bf8a2`](https://github.com/Effect-TS/effect/commit/91bf8a2e9d1959393b3cf7366cc1d584d3e666b7), [`c6a4a26`](https://github.com/Effect-TS/effect/commit/c6a4a266606575fd2c7165940c4072ad4c57d01f)]: + - effect@3.4.0 + - @effect/platform-node-shared@0.7.0 + - @effect/platform@0.58.0 + +## 0.51.17 + +### Patch Changes + +- Updated dependencies [[`3ba7ea1`](https://github.com/Effect-TS/effect/commit/3ba7ea1c3c2923e85bf2f17e41176f8f8796d203)]: + - @effect/platform-node-shared@0.6.17 + - @effect/platform@0.57.8 + +## 0.51.16 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform@0.57.7 + - @effect/platform-node-shared@0.6.16 + +## 0.51.15 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform@0.57.6 + - @effect/platform-node-shared@0.6.15 + +## 0.51.14 + +### Patch Changes + +- Updated dependencies [[`b8ea6aa`](https://github.com/Effect-TS/effect/commit/b8ea6aa479006358042b4256ee0a1c5cfbe57acb)]: + - @effect/platform@0.57.5 + - @effect/platform-node-shared@0.6.14 + +## 0.51.13 + +### Patch Changes + +- Updated dependencies [[`6c89408`](https://github.com/Effect-TS/effect/commit/6c89408cd7b9204ec4c5828a46cd5312d8afb5e7)]: + - effect@3.3.5 + - @effect/platform@0.57.4 + - @effect/platform-node-shared@0.6.13 + +## 0.51.12 + +### Patch Changes + +- Updated dependencies [[`a67b8fe`](https://github.com/Effect-TS/effect/commit/a67b8fe2ace08419424811b5f0d9a5378eaea352)]: + - effect@3.3.4 + - @effect/platform@0.57.3 + - @effect/platform-node-shared@0.6.12 + +## 0.51.11 + +### Patch Changes + +- Updated dependencies [[`06ede85`](https://github.com/Effect-TS/effect/commit/06ede85d6e84710e6622463be95ff3927fb30dad), [`7204ca5`](https://github.com/Effect-TS/effect/commit/7204ca5761c2b1d27999a624db23aa10b6e0504d)]: + - effect@3.3.3 + - @effect/platform@0.57.2 + - @effect/platform-node-shared@0.6.11 + +## 0.51.10 + +### Patch Changes + +- Updated dependencies [[`07e12ec`](https://github.com/Effect-TS/effect/commit/07e12ecdb0e20b9763bd9e9058e567a7c8862efc), [`3572646`](https://github.com/Effect-TS/effect/commit/3572646d5e0804f85bc7f64633fb95722533f9dd), [`1aed347`](https://github.com/Effect-TS/effect/commit/1aed347a125ed3847ec90863424810d6759cbc85), [`df4bf4b`](https://github.com/Effect-TS/effect/commit/df4bf4b62e7b316c6647da0271fc5544a84e7ba2), [`f085f92`](https://github.com/Effect-TS/effect/commit/f085f92dfa204afb41823ffc27d437225137643d)]: + - @effect/platform-node-shared@0.6.10 + - @effect/platform@0.57.1 + - effect@3.3.2 + +## 0.51.9 + +### Patch Changes + +- Updated dependencies [[`eb98c5b`](https://github.com/Effect-TS/effect/commit/eb98c5b79ab50aa0cde239bd4e660dd19dbab612), [`184fed8`](https://github.com/Effect-TS/effect/commit/184fed83ac36cba05a75a5a8013f740f9f696e3b), [`4d3fbe8`](https://github.com/Effect-TS/effect/commit/4d3fbe82e8cec13ccd0cd0b2096deac6818fb59a), [`6068e07`](https://github.com/Effect-TS/effect/commit/6068e073d4cc8b3c8583583fd5eb3efe43f7d5ba), [`3a77e20`](https://github.com/Effect-TS/effect/commit/3a77e209783933bac3aaddba1b05ff6a9ac72b36)]: + - effect@3.3.1 + - @effect/platform@0.57.0 + - @effect/platform-node-shared@0.6.9 + +## 0.51.8 + +### Patch Changes + +- Updated dependencies [[`2b9ddfc`](https://github.com/Effect-TS/effect/commit/2b9ddfcbac505d98551e764a43923854907ca5c1), [`1f4ac00`](https://github.com/Effect-TS/effect/commit/1f4ac00a91c336c9c9c9b8c3ed9ceb9920ebc9bd), [`9305b76`](https://github.com/Effect-TS/effect/commit/9305b764cceeae4f16564435ae7172f79c2bf822), [`0f40d98`](https://github.com/Effect-TS/effect/commit/0f40d989da10f68df3ecd72b36849401ad679bfb), [`188f0a5`](https://github.com/Effect-TS/effect/commit/188f0a5c57ed0d7c9e5852e0c1c998f1b95810a1), [`b761ef0`](https://github.com/Effect-TS/effect/commit/b761ef00eaf6c67b7ffe34798b98aae5347ab376), [`b53f69b`](https://github.com/Effect-TS/effect/commit/b53f69bff1452a487b21198cd83961f844e02d36), [`0f40d98`](https://github.com/Effect-TS/effect/commit/0f40d989da10f68df3ecd72b36849401ad679bfb), [`5bd549e`](https://github.com/Effect-TS/effect/commit/5bd549e4bd7144727db438ecca6b8dc9b3ef7e22), [`67f160a`](https://github.com/Effect-TS/effect/commit/67f160a213de0219a565d4bf653b3cbf24f58e8f)]: + - @effect/platform@0.56.0 + - effect@3.3.0 + - @effect/platform-node-shared@0.6.8 + +## 0.51.7 + +### Patch Changes + +- Updated dependencies [[`a67d602`](https://github.com/Effect-TS/effect/commit/a67d60276f96cd20b76145b4cee13efca6c6158a)]: + - @effect/platform@0.55.7 + - @effect/platform-node-shared@0.6.7 + +## 0.51.6 + +### Patch Changes + +- Updated dependencies [[`8c5d280`](https://github.com/Effect-TS/effect/commit/8c5d280c0402284a4e58372867a15a431cb99461), [`6ba6d26`](https://github.com/Effect-TS/effect/commit/6ba6d269f5891e6b11aa35c5281dde4bf3273004), [`3f28bf2`](https://github.com/Effect-TS/effect/commit/3f28bf274333611906175446b772243f34f1b6d5), [`5817820`](https://github.com/Effect-TS/effect/commit/58178204a770d1a78c06945ef438f9fffbb50afa), [`799aa20`](https://github.com/Effect-TS/effect/commit/799aa20b4f618736ba33a5297fda90a75d4c26c6)]: + - effect@3.2.9 + - @effect/platform@0.55.6 + - @effect/platform-node-shared@0.6.6 + +## 0.51.5 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform@0.55.5 + - @effect/platform-node-shared@0.6.5 + +## 0.51.4 + +### Patch Changes + +- Updated dependencies [[`fb91f17`](https://github.com/Effect-TS/effect/commit/fb91f17098b48497feca9ec976feb87e4a82451b)]: + - effect@3.2.8 + - @effect/platform@0.55.4 + - @effect/platform-node-shared@0.6.4 + +## 0.51.3 + +### Patch Changes + +- Updated dependencies [[`6801fca`](https://github.com/Effect-TS/effect/commit/6801fca44366be3ee1b6b99f54bd4f38a1b5e4f4)]: + - effect@3.2.7 + - @effect/platform@0.55.3 + - @effect/platform-node-shared@0.6.3 + +## 0.51.2 + +### Patch Changes + +- Updated dependencies [[`2c2280b`](https://github.com/Effect-TS/effect/commit/2c2280b98a11fc002663c55792a4fa5781cd5fb6), [`cc8ac50`](https://github.com/Effect-TS/effect/commit/cc8ac5080daba8622ca2ff5dab5c37ddfab732ba)]: + - @effect/platform@0.55.2 + - effect@3.2.6 + - @effect/platform-node-shared@0.6.2 + +## 0.51.1 + +### Patch Changes + +- Updated dependencies [[`c5c94ed`](https://github.com/Effect-TS/effect/commit/c5c94edf1ddb0abb5c0e2adbb4ec2578a98d8e07)]: + - @effect/platform-node-shared@0.6.1 + - @effect/platform@0.55.1 + +## 0.51.0 + +### Minor Changes + +- [#2835](https://github.com/Effect-TS/effect/pull/2835) [`5133ca9`](https://github.com/Effect-TS/effect/commit/5133ca9dc4b8da0e28951316da9ab55dfbe0fbb9) Thanks @tim-smart! - remove pool resizing in platform workers to enable concurrent access + +### Patch Changes + +- Updated dependencies [[`608b01f`](https://github.com/Effect-TS/effect/commit/608b01fc342dbae2a642b308a67b84ead530ecea), [`031c712`](https://github.com/Effect-TS/effect/commit/031c7122a24ac42e48d6a434646b4f5d279d7442), [`a44e532`](https://github.com/Effect-TS/effect/commit/a44e532cf3a6a498b12a5aacf8124aa267e24ba0), [`5133ca9`](https://github.com/Effect-TS/effect/commit/5133ca9dc4b8da0e28951316da9ab55dfbe0fbb9)]: + - effect@3.2.5 + - @effect/platform@0.55.0 + - @effect/platform-node-shared@0.6.0 + +## 0.50.0 + +### Minor Changes + +- [#2801](https://github.com/Effect-TS/effect/pull/2801) [`1af94df`](https://github.com/Effect-TS/effect/commit/1af94df6b74aeb4f6ebcbe80e074b4cb252e62e3) Thanks @tim-smart! - remove `permits` from workers, to prevent issues with pool resizing + +### Patch Changes + +- Updated dependencies [[`1af94df`](https://github.com/Effect-TS/effect/commit/1af94df6b74aeb4f6ebcbe80e074b4cb252e62e3), [`1af94df`](https://github.com/Effect-TS/effect/commit/1af94df6b74aeb4f6ebcbe80e074b4cb252e62e3), [`c07e0ce`](https://github.com/Effect-TS/effect/commit/c07e0cea8ce165887e2c9dfa5d669eba9b2fb798), [`e313a01`](https://github.com/Effect-TS/effect/commit/e313a01b7e80f6cb7704055a190e5623c9d22c6d), [`1af94df`](https://github.com/Effect-TS/effect/commit/1af94df6b74aeb4f6ebcbe80e074b4cb252e62e3)]: + - effect@3.2.4 + - @effect/platform@0.54.0 + - @effect/platform-node-shared@0.5.0 + +## 0.49.14 + +### Patch Changes + +- [#2803](https://github.com/Effect-TS/effect/pull/2803) [`5af633e`](https://github.com/Effect-TS/effect/commit/5af633eb5ff6560a64d87263d1692bb9c75f7b3c) Thanks @tim-smart! - update dependencies + +- Updated dependencies [[`45578e8`](https://github.com/Effect-TS/effect/commit/45578e8faa80ae33d23e08f6f19467f818b7788f)]: + - effect@3.2.3 + - @effect/platform@0.53.14 + - @effect/platform-node-shared@0.4.33 + +## 0.49.13 + +### Patch Changes + +- [#2796](https://github.com/Effect-TS/effect/pull/2796) [`7cc8020`](https://github.com/Effect-TS/effect/commit/7cc802018395804ae2fbce20f610bb7ff6081c00) Thanks @tim-smart! - wait for callback in node http server responses + +- Updated dependencies [[`5d9266e`](https://github.com/Effect-TS/effect/commit/5d9266e8c740746ac9e186c3df6090a1b57fbe2a), [`9f8122e`](https://github.com/Effect-TS/effect/commit/9f8122e78884ab47c5e5f364d86eee1d1543cc61), [`6a6f670`](https://github.com/Effect-TS/effect/commit/6a6f6706b8613c8c7c10971b8d81a0f9e440a6f2), [`c1eaef9`](https://github.com/Effect-TS/effect/commit/c1eaef910420dae416923d172ee58d219e921d0f)]: + - effect@3.2.2 + - @effect/platform@0.53.13 + - @effect/platform-node-shared@0.4.32 + +## 0.49.12 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform@0.53.12 + - @effect/platform-node-shared@0.4.31 + +## 0.49.11 + +### Patch Changes + +- Updated dependencies [[`c1e991d`](https://github.com/Effect-TS/effect/commit/c1e991dd5ba87901cd0e05697a8b4a267e7e954a)]: + - effect@3.2.1 + - @effect/platform@0.53.11 + - @effect/platform-node-shared@0.4.30 + +## 0.49.10 + +### Patch Changes + +- Updated dependencies [[`146cadd`](https://github.com/Effect-TS/effect/commit/146cadd9d004634a3ff85c480bf92cf975c853e2), [`7135748`](https://github.com/Effect-TS/effect/commit/713574813a0f64085db0b5240ba39e7a0a7c137e), [`7135748`](https://github.com/Effect-TS/effect/commit/713574813a0f64085db0b5240ba39e7a0a7c137e), [`963b4e7`](https://github.com/Effect-TS/effect/commit/963b4e7ac87e2468feb6a344f7ab4ee4ad711198), [`64c9414`](https://github.com/Effect-TS/effect/commit/64c9414e960e82058ca09bbb3976d6fbef303a8e), [`7135748`](https://github.com/Effect-TS/effect/commit/713574813a0f64085db0b5240ba39e7a0a7c137e), [`2cbb76b`](https://github.com/Effect-TS/effect/commit/2cbb76bb52500a3f4bf27d1c91482518cbea56d7), [`870c5fa`](https://github.com/Effect-TS/effect/commit/870c5fa52cd61e745e8e828d38c3f09f00737553), [`146cadd`](https://github.com/Effect-TS/effect/commit/146cadd9d004634a3ff85c480bf92cf975c853e2), [`7135748`](https://github.com/Effect-TS/effect/commit/713574813a0f64085db0b5240ba39e7a0a7c137e), [`64c9414`](https://github.com/Effect-TS/effect/commit/64c9414e960e82058ca09bbb3976d6fbef303a8e)]: + - effect@3.2.0 + - @effect/platform@0.53.10 + - @effect/platform-node-shared@0.4.29 + +## 0.49.9 + +### Patch Changes + +- Updated dependencies [[`17fc22e`](https://github.com/Effect-TS/effect/commit/17fc22e132593c5caa563705a4748ba0f04a853c), [`810f222`](https://github.com/Effect-TS/effect/commit/810f222268792b13067c7a7bf317b93a9bb8917b), [`596aaea`](https://github.com/Effect-TS/effect/commit/596aaea022648b2e06fb1ec22f1652043d6fe64e)]: + - @effect/platform@0.53.9 + - effect@3.1.6 + - @effect/platform-node-shared@0.4.28 + +## 0.49.8 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform@0.53.8 + - @effect/platform-node-shared@0.4.27 + +## 0.49.7 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform@0.53.7 + - @effect/platform-node-shared@0.4.26 + +## 0.49.6 + +### Patch Changes + +- Updated dependencies [[`6ac4847`](https://github.com/Effect-TS/effect/commit/6ac48479447c01a4f35d655552af93e47e562610), [`6ac4847`](https://github.com/Effect-TS/effect/commit/6ac48479447c01a4f35d655552af93e47e562610)]: + - @effect/platform-node-shared@0.4.25 + - @effect/platform@0.53.6 + - effect@3.1.5 + +## 0.49.5 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform@0.53.5 + - @effect/platform-node-shared@0.4.24 + +## 0.49.4 + +### Patch Changes + +- Updated dependencies [[`e41e911`](https://github.com/Effect-TS/effect/commit/e41e91122fa6dd12fc81e50dcad0db891be67146)]: + - effect@3.1.4 + - @effect/platform@0.53.4 + - @effect/platform-node-shared@0.4.23 + +## 0.49.3 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform@0.53.3 + - @effect/platform-node-shared@0.4.22 + +## 0.49.2 + +### Patch Changes + +- [#2706](https://github.com/Effect-TS/effect/pull/2706) [`1f6dc96`](https://github.com/Effect-TS/effect/commit/1f6dc96f51c7bb9c8d11415358308604ba7c7c8e) Thanks [@sukovanej](https://github.com/sukovanej)! - Attempt to close a server only if `listen` succeeds. This fixes the error reporting in case a port is already in use. + +- Updated dependencies [[`1f6dc96`](https://github.com/Effect-TS/effect/commit/1f6dc96f51c7bb9c8d11415358308604ba7c7c8e)]: + - effect@3.1.3 + - @effect/platform@0.53.2 + - @effect/platform-node-shared@0.4.21 + +## 0.49.1 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform@0.53.1 + - @effect/platform-node-shared@0.4.20 + +## 0.49.0 + +### Minor Changes + +- [#2703](https://github.com/Effect-TS/effect/pull/2703) [`d57fbbb`](https://github.com/Effect-TS/effect/commit/d57fbbbd6c466936213a671fc3cd2390064f864e) Thanks [@tim-smart](https://github.com/tim-smart)! - replace isows with WebSocketConstructor service in @effect/platform/Socket + + You now have to provide a WebSocketConstructor implementation to the `Socket.makeWebSocket` api. + + ```ts + import * as Socket from "@effect/platform/Socket" + import * as NodeSocket from "@effect/platform-node/NodeSocket" + import { Effect } from "effect" + + Socket.makeWebSocket("ws://localhost:8080").pipe( + Effect.provide(NodeSocket.layerWebSocketConstructor) // use "ws" npm package + ) + ``` + +### Patch Changes + +- Updated dependencies [[`d57fbbb`](https://github.com/Effect-TS/effect/commit/d57fbbbd6c466936213a671fc3cd2390064f864e)]: + - @effect/platform@0.53.0 + - @effect/platform-node-shared@0.4.19 + +## 0.48.4 + +### Patch Changes + +- Updated dependencies [[`5866c62`](https://github.com/Effect-TS/effect/commit/5866c621d7eb4cc84e4ba972bfdfd219734cd45d)]: + - @effect/platform@0.52.3 + - @effect/platform-node-shared@0.4.18 + +## 0.48.3 + +### Patch Changes + +- [#2689](https://github.com/Effect-TS/effect/pull/2689) [`e4b82d2`](https://github.com/Effect-TS/effect/commit/e4b82d239bf0974173b5d687b45d1b1899be615f) Thanks [@tim-smart](https://github.com/tim-smart)! - ensure http response middleware is interruptible + +- Updated dependencies [[`2e1cdf6`](https://github.com/Effect-TS/effect/commit/2e1cdf67d141281288fffe9a5c10d1379a800513)]: + - @effect/platform@0.52.2 + - effect@3.1.2 + - @effect/platform-node-shared@0.4.17 + +## 0.48.2 + +### Patch Changes + +- Updated dependencies [[`e5e56d1`](https://github.com/Effect-TS/effect/commit/e5e56d138dbed3204636f605229c6685f89659fc)]: + - effect@3.1.1 + - @effect/platform@0.52.1 + - @effect/platform-node-shared@0.4.16 + +## 0.48.1 + +### Patch Changes + +- Updated dependencies [[`9deab0a`](https://github.com/Effect-TS/effect/commit/9deab0aec9e99501f9441843e34df9afa10c5be9), [`7719b8a`](https://github.com/Effect-TS/effect/commit/7719b8a7350c14e952ffe685bfd5308773b3e271)]: + - @effect/platform@0.52.0 + - @effect/platform-node-shared@0.4.15 + +## 0.48.0 + +### Minor Changes + +- [#2543](https://github.com/Effect-TS/effect/pull/2543) [`0ec93cb`](https://github.com/Effect-TS/effect/commit/0ec93cb4f166e7401c171c2f8e8276ce958d9a57) Thanks [@github-actions](https://github.com/apps/github-actions)! - \* capitalised Http.multipart.FileSchema and Http.multipart.FilesSchema + - exported Http.multipart.FileSchema + - added Http.multipart.SingleFileSchema + +### Patch Changes + +- Updated dependencies [[`c3c12c6`](https://github.com/Effect-TS/effect/commit/c3c12c6625633fe80e79f9db75a3b8cf8ca8b11d), [`ba64ea6`](https://github.com/Effect-TS/effect/commit/ba64ea6757810c5e74cad3863a7d19d4d38af66b), [`b5de2d2`](https://github.com/Effect-TS/effect/commit/b5de2d2ce5b1afe8be90827bf898a95cec40eb2b), [`a1c7ab8`](https://github.com/Effect-TS/effect/commit/a1c7ab8ffedacd18c1fc784f4ff5844f79498b83), [`a023f28`](https://github.com/Effect-TS/effect/commit/a023f28336f3865687d9a30c1883e36909906d85), [`0ec93cb`](https://github.com/Effect-TS/effect/commit/0ec93cb4f166e7401c171c2f8e8276ce958d9a57), [`1c9454d`](https://github.com/Effect-TS/effect/commit/1c9454d532eae79b9f759aea77f59332cc6d18ed), [`a023f28`](https://github.com/Effect-TS/effect/commit/a023f28336f3865687d9a30c1883e36909906d85), [`92d56db`](https://github.com/Effect-TS/effect/commit/92d56dbb3f33e36636c2a2f1030c56492e39cf4d)]: + - effect@3.1.0 + - @effect/platform@0.51.0 + - @effect/platform-node-shared@0.4.14 + +## 0.47.8 + +### Patch Changes + +- [#2656](https://github.com/Effect-TS/effect/pull/2656) [`557707b`](https://github.com/Effect-TS/effect/commit/557707bc9e5f230c8964d2757012075c34339b5c) Thanks [@tim-smart](https://github.com/tim-smart)! - update dependencies + +- Updated dependencies [[`16039a0`](https://github.com/Effect-TS/effect/commit/16039a08f04f11545e2fdf40952788a8f9cef04f), [`557707b`](https://github.com/Effect-TS/effect/commit/557707bc9e5f230c8964d2757012075c34339b5c), [`d1d33e1`](https://github.com/Effect-TS/effect/commit/d1d33e10b25109f44b5ab1c6e4d778a59c0d3eeb), [`f4ed306`](https://github.com/Effect-TS/effect/commit/f4ed3068a70b50302d078a30d18ca3cfd2bc679c), [`661004f`](https://github.com/Effect-TS/effect/commit/661004f4bf5f8b25f5a0678c21a3a822188ce461), [`e79cb83`](https://github.com/Effect-TS/effect/commit/e79cb83d3b19098bc40a3012e2a059b8426306c2)]: + - @effect/platform@0.50.8 + - @effect/platform-node-shared@0.4.13 + - effect@3.0.8 + +## 0.47.7 + +### Patch Changes + +- Updated dependencies [[`18de56b`](https://github.com/Effect-TS/effect/commit/18de56b4a6b6d1f99230dfabf9147d59ea4dd759)]: + - effect@3.0.7 + - @effect/platform@0.50.7 + - @effect/platform-node-shared@0.4.12 + +## 0.47.6 + +### Patch Changes + +- Updated dependencies [[`ffe4f4e`](https://github.com/Effect-TS/effect/commit/ffe4f4e95db35fff6869e360b072e3837befa0a1), [`027418e`](https://github.com/Effect-TS/effect/commit/027418edaa6aa6c0ae4861b95832827b45adace4), [`ac1898e`](https://github.com/Effect-TS/effect/commit/ac1898eb7bc96880f911c276048e2ea3d6fe9c50), [`ffe4f4e`](https://github.com/Effect-TS/effect/commit/ffe4f4e95db35fff6869e360b072e3837befa0a1)]: + - effect@3.0.6 + - @effect/platform@0.50.6 + - @effect/platform-node-shared@0.4.11 + +## 0.47.5 + +### Patch Changes + +- Updated dependencies [[`6222404`](https://github.com/Effect-TS/effect/commit/62224044678751829ed2f128e05133a91c6b0569), [`868ed2a`](https://github.com/Effect-TS/effect/commit/868ed2a8fe94ee7f4206a6070f29dcf2a5ba1dc3)]: + - effect@3.0.5 + - @effect/platform@0.50.5 + - @effect/platform-node-shared@0.4.10 + +## 0.47.4 + +### Patch Changes + +- Updated dependencies [[`9a24667`](https://github.com/Effect-TS/effect/commit/9a246672008a2b668d43fbfd2fe5508c54b2b920)]: + - effect@3.0.4 + - @effect/platform@0.50.4 + - @effect/platform-node-shared@0.4.9 + +## 0.47.3 + +### Patch Changes + +- Updated dependencies [[`b3b51a2`](https://github.com/Effect-TS/effect/commit/b3b51a2ea0c6ab92a363db46ebaa7e1176d089f5)]: + - @effect/platform@0.50.3 + - @effect/platform-node-shared@0.4.8 + +## 0.47.2 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform@0.50.2 + - @effect/platform-node-shared@0.4.7 + +## 0.47.1 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform@0.50.1 + - @effect/platform-node-shared@0.4.6 + +## 0.47.0 + +### Minor Changes + +- [#2567](https://github.com/Effect-TS/effect/pull/2567) [`6f38dff`](https://github.com/Effect-TS/effect/commit/6f38dff41ffa34532cc2f25b90446550c5730bb6) Thanks [@tim-smart](https://github.com/tim-smart)! - add URL & AbortSignal to Http.client.makeDefault + +### Patch Changes + +- [#2567](https://github.com/Effect-TS/effect/pull/2567) [`6f38dff`](https://github.com/Effect-TS/effect/commit/6f38dff41ffa34532cc2f25b90446550c5730bb6) Thanks [@tim-smart](https://github.com/tim-smart)! - add more span attributes to http traces + +- Updated dependencies [[`6f38dff`](https://github.com/Effect-TS/effect/commit/6f38dff41ffa34532cc2f25b90446550c5730bb6), [`a7b4b84`](https://github.com/Effect-TS/effect/commit/a7b4b84bd5a25f51aba922f9259c3a58c98c6a4e), [`a3b0e6c`](https://github.com/Effect-TS/effect/commit/a3b0e6c490772e6d44b5d98dcf2729c4d5310ecc), [`6f38dff`](https://github.com/Effect-TS/effect/commit/6f38dff41ffa34532cc2f25b90446550c5730bb6)]: + - @effect/platform@0.50.0 + - effect@3.0.3 + - @effect/platform-node-shared@0.4.5 + +## 0.46.4 + +### Patch Changes + +- [#2562](https://github.com/Effect-TS/effect/pull/2562) [`2cecdbd`](https://github.com/Effect-TS/effect/commit/2cecdbd1cf30befce4e84796ccd953ea55ecfb86) Thanks [@fubhy](https://github.com/fubhy)! - Added provenance publishing + +- Updated dependencies [[`2cecdbd`](https://github.com/Effect-TS/effect/commit/2cecdbd1cf30befce4e84796ccd953ea55ecfb86)]: + - @effect/platform-node-shared@0.4.4 + - @effect/platform@0.49.4 + - effect@3.0.2 + +## 0.46.3 + +### Patch Changes + +- Updated dependencies [[`8d39d65`](https://github.com/Effect-TS/effect/commit/8d39d6554af548228ad767112ce2e0b1f68fa8e1)]: + - @effect/platform@0.49.3 + - @effect/platform-node-shared@0.4.3 + +## 0.46.2 + +### Patch Changes + +- Updated dependencies [[`5ef0a1a`](https://github.com/Effect-TS/effect/commit/5ef0a1ae9b773fa2481550cb0d43ff7a0e03cd44)]: + - @effect/platform-node-shared@0.4.2 + - @effect/platform@0.49.2 + +## 0.46.1 + +### Patch Changes + +- Updated dependencies [[`3da0cfa`](https://github.com/Effect-TS/effect/commit/3da0cfa12c407fd930dc480be1ecc9217a8058f8), [`570e8d8`](https://github.com/Effect-TS/effect/commit/570e8d87e7c0e9ad4cd2686462fdb9b4812f7716), [`87c5687`](https://github.com/Effect-TS/effect/commit/87c5687de0782dab177b7861217fa3b040046282), [`8edacca`](https://github.com/Effect-TS/effect/commit/8edacca37f8e37c01a63fec332b06d9361efaa7b)]: + - effect@3.0.1 + - @effect/platform-node-shared@0.4.1 + - @effect/platform@0.49.1 + +## 0.46.0 + +### Minor Changes + +- [#2207](https://github.com/Effect-TS/effect/pull/2207) [`aa4a3b5`](https://github.com/Effect-TS/effect/commit/aa4a3b550da1c1020265ac389ed3f309388994a2) Thanks [@github-actions](https://github.com/apps/github-actions)! - Swap type parameters in /platform data types + + A codemod has been released to make migration easier: + + ``` + npx @effect/codemod platform-0.49 src/**/* + ``` + +- [#2207](https://github.com/Effect-TS/effect/pull/2207) [`5a2314b`](https://github.com/Effect-TS/effect/commit/5a2314b70ec79c2c02b51cef45a5ddec8327daa1) Thanks [@github-actions](https://github.com/apps/github-actions)! - replace use of `unit` terminology with `void` + + For all the data types. + + ```ts + Effect.unit // => Effect.void + Stream.unit // => Stream.void + + // etc + ``` + +- [#2207](https://github.com/Effect-TS/effect/pull/2207) [`2fb7d9c`](https://github.com/Effect-TS/effect/commit/2fb7d9ca15037ff62a578bb9fe5732da5f4f317d) Thanks [@github-actions](https://github.com/apps/github-actions)! - Release Effect 3.0 🎉 + +### Patch Changes + +- [#2207](https://github.com/Effect-TS/effect/pull/2207) [`6c6087a`](https://github.com/Effect-TS/effect/commit/6c6087a4a897b64252346426660782d31c13f769) Thanks [@github-actions](https://github.com/apps/github-actions)! - allow specifying undici options through a fiber ref + +- [#2207](https://github.com/Effect-TS/effect/pull/2207) [`6460414`](https://github.com/Effect-TS/effect/commit/6460414351a45fb8e0a457c63f3653422efee766) Thanks [@github-actions](https://github.com/apps/github-actions)! - properly handle multiple ports in SharedWorker + +- [#2529](https://github.com/Effect-TS/effect/pull/2529) [`78b767c`](https://github.com/Effect-TS/effect/commit/78b767c2b1625186e17131761a0edbac25d21850) Thanks [@fubhy](https://github.com/fubhy)! - Renamed `ReadonlyArray` and `ReadonlyRecord` modules for better discoverability. + +- Updated dependencies [[`1b5f0c7`](https://github.com/Effect-TS/effect/commit/1b5f0c77e7fd477a0026071e82129a948227f4b3), [`d50a652`](https://github.com/Effect-TS/effect/commit/d50a652479f4d1d64f48da05c79fa847e6e51548), [`9a3bd47`](https://github.com/Effect-TS/effect/commit/9a3bd47ebd0750c7e498162734f6d21895de0cb2), [`6460414`](https://github.com/Effect-TS/effect/commit/6460414351a45fb8e0a457c63f3653422efee766), [`cf69f46`](https://github.com/Effect-TS/effect/commit/cf69f46690058d71eeada03cfb40dc744573e9e4), [`cf69f46`](https://github.com/Effect-TS/effect/commit/cf69f46690058d71eeada03cfb40dc744573e9e4), [`be9d025`](https://github.com/Effect-TS/effect/commit/be9d025e42355260ace02dd135851a8935a4deba), [`78b767c`](https://github.com/Effect-TS/effect/commit/78b767c2b1625186e17131761a0edbac25d21850), [`aa4a3b5`](https://github.com/Effect-TS/effect/commit/aa4a3b550da1c1020265ac389ed3f309388994a2), [`1499974`](https://github.com/Effect-TS/effect/commit/14999741d2e19c1747f6a7e19d68977f6429cdb8), [`1b5f0c7`](https://github.com/Effect-TS/effect/commit/1b5f0c77e7fd477a0026071e82129a948227f4b3), [`5c2b561`](https://github.com/Effect-TS/effect/commit/5c2b5614f583b88784ed68126ae939832fb3c092), [`6c6087a`](https://github.com/Effect-TS/effect/commit/6c6087a4a897b64252346426660782d31c13f769), [`a18f594`](https://github.com/Effect-TS/effect/commit/a18f5948f1439a147232448b2c443472fda0eceb), [`1499974`](https://github.com/Effect-TS/effect/commit/14999741d2e19c1747f6a7e19d68977f6429cdb8), [`2f96d93`](https://github.com/Effect-TS/effect/commit/2f96d938b90f8c19377583279e3c7afd9b509c50), [`5a2314b`](https://github.com/Effect-TS/effect/commit/5a2314b70ec79c2c02b51cef45a5ddec8327daa1), [`25d74f8`](https://github.com/Effect-TS/effect/commit/25d74f8c4d2dd4a9e5ec57ce2f20d36dedd25343), [`6c6087a`](https://github.com/Effect-TS/effect/commit/6c6087a4a897b64252346426660782d31c13f769), [`271b79f`](https://github.com/Effect-TS/effect/commit/271b79fc0b66a6c11e07a8779ff8800493a7eac2), [`1b5f0c7`](https://github.com/Effect-TS/effect/commit/1b5f0c77e7fd477a0026071e82129a948227f4b3), [`2fb7d9c`](https://github.com/Effect-TS/effect/commit/2fb7d9ca15037ff62a578bb9fe5732da5f4f317d), [`53d1c2a`](https://github.com/Effect-TS/effect/commit/53d1c2a77559081fbb89667e343346375c6d6650), [`e7e1bbe`](https://github.com/Effect-TS/effect/commit/e7e1bbe68486fdf31c8f84b0880522d39adcaad3), [`10c169e`](https://github.com/Effect-TS/effect/commit/10c169eadc874e91b4defca3f467b4e6a50fd8f3), [`6424181`](https://github.com/Effect-TS/effect/commit/64241815fe6a939e91e6947253e7dceea1306aa8)]: + - effect@3.0.0 + - @effect/platform@0.49.0 + - @effect/platform-node-shared@0.4.0 + +## 0.45.31 + +### Patch Changes + +- [#2517](https://github.com/Effect-TS/effect/pull/2517) [`b79cc59`](https://github.com/Effect-TS/effect/commit/b79cc59dbe64b9a0a7742dc9100a9d36c8e46b72) Thanks [@tim-smart](https://github.com/tim-smart)! - add uninterruptible option to http routes, for marking a route as uninterruptible + +- Updated dependencies [[`b79cc59`](https://github.com/Effect-TS/effect/commit/b79cc59dbe64b9a0a7742dc9100a9d36c8e46b72)]: + - @effect/platform@0.48.29 + - @effect/platform-node-shared@0.3.29 + +## 0.45.30 + +### Patch Changes + +- [#2504](https://github.com/Effect-TS/effect/pull/2504) [`da22adc`](https://github.com/Effect-TS/effect/commit/da22adc6507563876f1c416fd22a5f9206cc1395) Thanks [@tim-smart](https://github.com/tim-smart)! - use a FiberSet to run http server fibers + +- [#2515](https://github.com/Effect-TS/effect/pull/2515) [`d590094`](https://github.com/Effect-TS/effect/commit/d5900943489ec1e0891836aeafb5ce99fb9c75c7) Thanks [@tim-smart](https://github.com/tim-smart)! - add Http.router.uninterruptible, for marking a route as uninterruptible + +- Updated dependencies [[`41c8102`](https://github.com/Effect-TS/effect/commit/41c810228b1a50e4b41f19e735d7c62fe8d36871), [`776ef2b`](https://github.com/Effect-TS/effect/commit/776ef2bb66db9aa9f68b7beab14f6986f9c1288b), [`217147e`](https://github.com/Effect-TS/effect/commit/217147ea67c5c42c96f024775c41e5b070f81e4c), [`90776ec`](https://github.com/Effect-TS/effect/commit/90776ec8e8671d835b65fc33ead1de6c864b81b9), [`8709856`](https://github.com/Effect-TS/effect/commit/870985694ae985c3cb9360ad8a25c60e6f785f55), [`232c353`](https://github.com/Effect-TS/effect/commit/232c353c2e6f743f38e57639ee30e324ffa9c2a9), [`0ca835c`](https://github.com/Effect-TS/effect/commit/0ca835cbac8e69072a93ace83b534219faba24e8), [`8709856`](https://github.com/Effect-TS/effect/commit/870985694ae985c3cb9360ad8a25c60e6f785f55), [`d590094`](https://github.com/Effect-TS/effect/commit/d5900943489ec1e0891836aeafb5ce99fb9c75c7), [`e983740`](https://github.com/Effect-TS/effect/commit/e9837401145605aff5bc2ec7e73004f397c5d2d1), [`e3e0924`](https://github.com/Effect-TS/effect/commit/e3e09247d46a35430fc60e4aa4032cc50814f212)]: + - effect@2.4.19 + - @effect/platform@0.48.28 + - @effect/platform-node-shared@0.3.28 + +## 0.45.29 + +### Patch Changes + +- Updated dependencies [[`c6dd3c6`](https://github.com/Effect-TS/effect/commit/c6dd3c6909cafe05adc8450c5a499260e17e60d3), [`672f137`](https://github.com/Effect-TS/effect/commit/672f13747ddf6dac3ba304fd4511b1df44ab566d), [`672f137`](https://github.com/Effect-TS/effect/commit/672f13747ddf6dac3ba304fd4511b1df44ab566d)]: + - @effect/platform@0.48.27 + - @effect/platform-node-shared@0.3.27 + +## 0.45.28 + +### Patch Changes + +- Updated dependencies [[`365a486`](https://github.com/Effect-TS/effect/commit/365a4865de5e47ce09f4cfd51fc0f67438f82a57)]: + - @effect/platform@0.48.26 + - @effect/platform-node-shared@0.3.26 + +## 0.45.27 + +### Patch Changes + +- Updated dependencies [[`dadc690`](https://github.com/Effect-TS/effect/commit/dadc6906121c512bc32be22b52adbd1ada834594), [`d209171`](https://github.com/Effect-TS/effect/commit/d2091714a786820ebae4bef04a9d67d25dd08e88)]: + - effect@2.4.18 + - @effect/platform@0.48.25 + - @effect/platform-node-shared@0.3.25 + +## 0.45.26 + +### Patch Changes + +- Updated dependencies [[`f993857`](https://github.com/Effect-TS/effect/commit/f993857d5bb21ff7317ec69e481499632f0365f3), [`9c6a500`](https://github.com/Effect-TS/effect/commit/9c6a5001b467b6255c68a922f4b6e8d692b63d01), [`8fdfda6`](https://github.com/Effect-TS/effect/commit/8fdfda6618be848c01b399d13bc05a9a3adfb613), [`607b2e7`](https://github.com/Effect-TS/effect/commit/607b2e7a7fd9318c57acf4e50ec61747eea74ad7), [`8fdfda6`](https://github.com/Effect-TS/effect/commit/8fdfda6618be848c01b399d13bc05a9a3adfb613), [`35ad0ba`](https://github.com/Effect-TS/effect/commit/35ad0ba9f3ba27c60453620e514b980f819f92af), [`8206caf`](https://github.com/Effect-TS/effect/commit/8206caf7c2d22c68be4313318b61cfdacf6222b6), [`7ddd654`](https://github.com/Effect-TS/effect/commit/7ddd65415b65ccb654ad04f4dbefe39402f15117), [`7ddd654`](https://github.com/Effect-TS/effect/commit/7ddd65415b65ccb654ad04f4dbefe39402f15117), [`8fdfda6`](https://github.com/Effect-TS/effect/commit/8fdfda6618be848c01b399d13bc05a9a3adfb613), [`f456ba2`](https://github.com/Effect-TS/effect/commit/f456ba273bae21a6dcf8c966c50c97b5f0897d9f)]: + - @effect/platform-node-shared@0.3.24 + - @effect/platform@0.48.24 + - effect@2.4.17 + +## 0.45.25 + +### Patch Changes + +- Updated dependencies [[`5170ce7`](https://github.com/Effect-TS/effect/commit/5170ce708c606283e8a30d273950f1a21c7eddc2), [`63a1df2`](https://github.com/Effect-TS/effect/commit/63a1df2e4de3766f48f15676fbd0360ab9c27816), [`5170ce7`](https://github.com/Effect-TS/effect/commit/5170ce708c606283e8a30d273950f1a21c7eddc2), [`74a5dae`](https://github.com/Effect-TS/effect/commit/74a5daed0e65b32a36e026bfcf66d02269cb967a)]: + - @effect/platform@0.48.23 + - effect@2.4.16 + - @effect/platform-node-shared@0.3.23 + +## 0.45.24 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform@0.48.22 + - @effect/platform-node-shared@0.3.22 + +## 0.45.23 + +### Patch Changes + +- Updated dependencies [[`d7688c0`](https://github.com/Effect-TS/effect/commit/d7688c0c72717fe7876c871567f6946dabfc0546), [`b3a4fac`](https://github.com/Effect-TS/effect/commit/b3a4face2acaca422f0b0530436e8f13129f3b3a)]: + - effect@2.4.15 + - @effect/platform@0.48.21 + - @effect/platform-node-shared@0.3.21 + +## 0.45.22 + +### Patch Changes + +- [#2413](https://github.com/Effect-TS/effect/pull/2413) [`4789083`](https://github.com/Effect-TS/effect/commit/4789083283bdaec456982d614ebc4a496ea0e7f7) Thanks [@tim-smart](https://github.com/tim-smart)! - make /platform ClientRequest implement Effect + + ClientRequest now implements `Effect` + + This makes it easier to quickly create a request and execute it in a single line. + + ```ts + import * as Http from "@effect/platform/HttpClient" + + Http.request + .get("https://jsonplaceholder.typicode.com/todos/1") + .pipe(Http.response.json) + ``` + +- [#2413](https://github.com/Effect-TS/effect/pull/2413) [`4789083`](https://github.com/Effect-TS/effect/commit/4789083283bdaec456982d614ebc4a496ea0e7f7) Thanks [@tim-smart](https://github.com/tim-smart)! - prevent unhandled errors in undici http client + +- Updated dependencies [[`4789083`](https://github.com/Effect-TS/effect/commit/4789083283bdaec456982d614ebc4a496ea0e7f7), [`4789083`](https://github.com/Effect-TS/effect/commit/4789083283bdaec456982d614ebc4a496ea0e7f7)]: + - @effect/platform@0.48.20 + - @effect/platform-node-shared@0.3.20 + +## 0.45.21 + +### Patch Changes + +- [#2411](https://github.com/Effect-TS/effect/pull/2411) [`fb7285e`](https://github.com/Effect-TS/effect/commit/fb7285e8d6a70527df7137a6a3efdd03ae61cb8b) Thanks [@tim-smart](https://github.com/tim-smart)! - fix broken imports in /platform + +- Updated dependencies [[`fb7285e`](https://github.com/Effect-TS/effect/commit/fb7285e8d6a70527df7137a6a3efdd03ae61cb8b)]: + - @effect/platform@0.48.19 + - @effect/platform-node-shared@0.3.19 + +## 0.45.20 + +### Patch Changes + +- [#2410](https://github.com/Effect-TS/effect/pull/2410) [`26435ec`](https://github.com/Effect-TS/effect/commit/26435ecfa06569dc18d1801ccf38213a43b7c334) Thanks [@tim-smart](https://github.com/tim-smart)! - add undici http client to @effect/platform-node + +- Updated dependencies [[`26435ec`](https://github.com/Effect-TS/effect/commit/26435ecfa06569dc18d1801ccf38213a43b7c334), [`26435ec`](https://github.com/Effect-TS/effect/commit/26435ecfa06569dc18d1801ccf38213a43b7c334), [`6180c0c`](https://github.com/Effect-TS/effect/commit/6180c0cc51dee785cfce72220a52c9fc3b9bf9aa)]: + - @effect/platform-node-shared@0.3.18 + - @effect/platform@0.48.18 + - effect@2.4.14 + +## 0.45.19 + +### Patch Changes + +- [#2403](https://github.com/Effect-TS/effect/pull/2403) [`8c9abe2`](https://github.com/Effect-TS/effect/commit/8c9abe2b35c46d8891d4b2c14ff9eb46302a14f3) Thanks [@tim-smart](https://github.com/tim-smart)! - use ReadonlyRecord for storing cookies + +- [#2403](https://github.com/Effect-TS/effect/pull/2403) [`8c9abe2`](https://github.com/Effect-TS/effect/commit/8c9abe2b35c46d8891d4b2c14ff9eb46302a14f3) Thanks [@tim-smart](https://github.com/tim-smart)! - add set-cookie headers in Http.response.toWeb + +- Updated dependencies [[`47a8f1b`](https://github.com/Effect-TS/effect/commit/47a8f1b644d8294692d92cacd3c8c7543edbfabe), [`8c9abe2`](https://github.com/Effect-TS/effect/commit/8c9abe2b35c46d8891d4b2c14ff9eb46302a14f3), [`8c9abe2`](https://github.com/Effect-TS/effect/commit/8c9abe2b35c46d8891d4b2c14ff9eb46302a14f3), [`3336287`](https://github.com/Effect-TS/effect/commit/3336287ff55a25e56d759b83847bfaa21c40f499), [`54b7c00`](https://github.com/Effect-TS/effect/commit/54b7c0077fa784ad2646b812d6a44641f672edcd), [`3336287`](https://github.com/Effect-TS/effect/commit/3336287ff55a25e56d759b83847bfaa21c40f499), [`47a8f1b`](https://github.com/Effect-TS/effect/commit/47a8f1b644d8294692d92cacd3c8c7543edbfabe)]: + - @effect/platform@0.48.17 + - effect@2.4.13 + - @effect/platform-node-shared@0.3.17 + +## 0.45.18 + +### Patch Changes + +- [#2387](https://github.com/Effect-TS/effect/pull/2387) [`75a8d16`](https://github.com/Effect-TS/effect/commit/75a8d16247cc14860cdd7fd948ef542c50c2d55e) Thanks [@tim-smart](https://github.com/tim-smart)! - add Cookies module to /platform http + + To add cookies to a http response: + + ```ts + import * as Http from "@effect/platform/HttpServer" + + Http.response.empty().pipe( + Http.response.setCookies([ + ["name", "value"], + ["foo", "bar", { httpOnly: true }] + ]) + ) + ``` + + You can also use cookies with the http client: + + ```ts + import * as Http from "@effect/platform/HttpClient" + import { Effect, Ref } from "effect" + + Effect.gen(function* (_) { + const ref = yield* _(Ref.make(Http.cookies.empty)) + const defaultClient = yield* _(Http.client.Client) + const clientWithCookies = defaultClient.pipe( + Http.client.withCookiesRef(ref), + Http.client.filterStatusOk + ) + + // cookies will be stored in the ref and sent in any subsequent requests + yield* _( + Http.request.get("https://www.google.com/"), + clientWithCookies, + Effect.scoped + ) + }) + ``` + +- [#2385](https://github.com/Effect-TS/effect/pull/2385) [`3307729`](https://github.com/Effect-TS/effect/commit/3307729de162a033fa9caa8e14c111013dcf0d87) Thanks [@tim-smart](https://github.com/tim-smart)! - update typescript to 5.4 + +- Updated dependencies [[`75a8d16`](https://github.com/Effect-TS/effect/commit/75a8d16247cc14860cdd7fd948ef542c50c2d55e), [`3307729`](https://github.com/Effect-TS/effect/commit/3307729de162a033fa9caa8e14c111013dcf0d87)]: + - @effect/platform-node-shared@0.3.16 + - @effect/platform@0.48.16 + - effect@2.4.12 + +## 0.45.17 + +### Patch Changes + +- [#2384](https://github.com/Effect-TS/effect/pull/2384) [`2f488c4`](https://github.com/Effect-TS/effect/commit/2f488c436de52576562803c57ebc132ef40ccdd8) Thanks [@tim-smart](https://github.com/tim-smart)! - update dependencies + +- Updated dependencies [[`2f488c4`](https://github.com/Effect-TS/effect/commit/2f488c436de52576562803c57ebc132ef40ccdd8), [`37ca592`](https://github.com/Effect-TS/effect/commit/37ca592a4101ad90adbf8c8b3f727faf3110cae5), [`317b5b8`](https://github.com/Effect-TS/effect/commit/317b5b8e8c8c2207469b3ebfcf72bf3a9f7cbc60)]: + - @effect/platform-node-shared@0.3.15 + - effect@2.4.11 + - @effect/platform@0.48.15 + +## 0.45.16 + +### Patch Changes + +- Updated dependencies [[`9bab1f9`](https://github.com/Effect-TS/effect/commit/9bab1f9fa5b999740755e4e82485cb77c638643a), [`9bbde5b`](https://github.com/Effect-TS/effect/commit/9bbde5be9a0168d1c2a0308bfc27167ed62f3968)]: + - effect@2.4.10 + - @effect/platform@0.48.14 + - @effect/platform-node-shared@0.3.14 + +## 0.45.15 + +### Patch Changes + +- Updated dependencies [[`1879f62`](https://github.com/Effect-TS/effect/commit/1879f629d0c4815dbb5955779247cd3f3da5cd85)]: + - @effect/platform-node-shared@0.3.13 + - @effect/platform@0.48.13 + +## 0.45.14 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform@0.48.12 + - @effect/platform-node-shared@0.3.12 + +## 0.45.13 + +### Patch Changes + +- Updated dependencies [[`0f6c7b4`](https://github.com/Effect-TS/effect/commit/0f6c7b426eb3432f60e3a17f8cd92ceac91597bf)]: + - @effect/platform-node-shared@0.3.11 + - @effect/platform@0.48.11 + +## 0.45.12 + +### Patch Changes + +- [#2357](https://github.com/Effect-TS/effect/pull/2357) [`71fd528`](https://github.com/Effect-TS/effect/commit/71fd5287500f9ce155a7d9f0df6ee3e0ac3aeb99) Thanks [@tim-smart](https://github.com/tim-smart)! - make more data types in /platform implement Inspectable + +- Updated dependencies [[`71fd528`](https://github.com/Effect-TS/effect/commit/71fd5287500f9ce155a7d9f0df6ee3e0ac3aeb99)]: + - @effect/platform-node-shared@0.3.10 + - @effect/platform@0.48.10 + - effect@2.4.9 + +## 0.45.11 + +### Patch Changes + +- Updated dependencies [[`bb0b69e`](https://github.com/Effect-TS/effect/commit/bb0b69e519698c7c76aa68217de423c78ad16566), [`6b20bad`](https://github.com/Effect-TS/effect/commit/6b20badebb3a7ca4d38857753e8ecaa09d02ccfb), [`4e64e9b`](https://github.com/Effect-TS/effect/commit/4e64e9b9876de6bfcbabe39e18a91a08e5f3fbb0), [`3851a02`](https://github.com/Effect-TS/effect/commit/3851a022c481006aec1db36651e4b4fd727aa742), [`5f5fcd9`](https://github.com/Effect-TS/effect/commit/5f5fcd969ae30ed6fe61d566a571498d9e895e16), [`814e5b8`](https://github.com/Effect-TS/effect/commit/814e5b828f68210b9e8f336fd6ac688646835dd9)]: + - effect@2.4.8 + - @effect/platform@0.48.9 + - @effect/platform-node-shared@0.3.9 + +## 0.45.10 + +### Patch Changes + +- Updated dependencies [[`69d27bb`](https://github.com/Effect-TS/effect/commit/69d27bb633884b6b50f9c3d9e95c29f09b4860b5)]: + - @effect/platform-node-shared@0.3.8 + - @effect/platform@0.48.8 + +## 0.45.9 + +### Patch Changes + +- Updated dependencies [[`f908948`](https://github.com/Effect-TS/effect/commit/f908948fd05771a670c0b746e2dd9caa9408ef83)]: + - @effect/platform@0.48.7 + - @effect/platform-node-shared@0.3.7 + +## 0.45.8 + +### Patch Changes + +- Updated dependencies [[`eb93283`](https://github.com/Effect-TS/effect/commit/eb93283985913d7b04ca750e36ac8513e7b6cef6)]: + - effect@2.4.7 + - @effect/platform@0.48.6 + - @effect/platform-node-shared@0.3.6 + +## 0.45.7 + +### Patch Changes + +- Updated dependencies [[`e006e4a`](https://github.com/Effect-TS/effect/commit/e006e4a538c97bae6ca1efa74802159e8a688fcb)]: + - @effect/platform-node-shared@0.3.5 + - @effect/platform@0.48.5 + +## 0.45.6 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform@0.48.4 + - @effect/platform-node-shared@0.3.4 + +## 0.45.5 + +### Patch Changes + +- Updated dependencies [[`c362e06`](https://github.com/Effect-TS/effect/commit/c362e066550252d5a9fcbc31a4b34d0e17c50699), [`83ddd6f`](https://github.com/Effect-TS/effect/commit/83ddd6f41029724b2cbd144cf309463967ed1164)]: + - @effect/platform-node-shared@0.3.3 + - @effect/platform@0.48.3 + +## 0.45.4 + +### Patch Changes + +- Updated dependencies [[`4f35a7e`](https://github.com/Effect-TS/effect/commit/4f35a7e7c4eba598924aff24d1158b9056bb24be), [`9971186`](https://github.com/Effect-TS/effect/commit/99711862722188fbb5ed3ee75126ad5edf13f72f)]: + - @effect/platform@0.48.2 + - effect@2.4.6 + - @effect/platform-node-shared@0.3.2 + +## 0.45.3 + +### Patch Changes + +- Updated dependencies [[`bce21c5`](https://github.com/Effect-TS/effect/commit/bce21c5ded2177114666ba229bd5029fa000dee3), [`c7d3036`](https://github.com/Effect-TS/effect/commit/c7d303630b7f0825cb2e584557c5767a67214d9f)]: + - effect@2.4.5 + - @effect/platform@0.48.1 + - @effect/platform-node-shared@0.3.1 + +## 0.45.2 + +### Patch Changes + +- [#2283](https://github.com/Effect-TS/effect/pull/2283) [`509be1a`](https://github.com/Effect-TS/effect/commit/509be1a0817118489750cf028523134677e44a8a) Thanks [@tim-smart](https://github.com/tim-smart)! - add SocketCloseError with additional metadata + +- Updated dependencies [[`5d47ee0`](https://github.com/Effect-TS/effect/commit/5d47ee0855e492532085b6092879b1b952d84949), [`1cb7f9c`](https://github.com/Effect-TS/effect/commit/1cb7f9cff7c2272a32fc7a324d87b02e2cd8a2f5), [`817a04c`](https://github.com/Effect-TS/effect/commit/817a04cb2df0f4140984dc97eb3e1bb14a6c4a38), [`d90a99d`](https://github.com/Effect-TS/effect/commit/d90a99d03d074adc7cd2533f15419138264da5a2), [`dd05faa`](https://github.com/Effect-TS/effect/commit/dd05faa621555ef3585ecd914ac13ecd89b710f4), [`509be1a`](https://github.com/Effect-TS/effect/commit/509be1a0817118489750cf028523134677e44a8a), [`1cb7f9c`](https://github.com/Effect-TS/effect/commit/1cb7f9cff7c2272a32fc7a324d87b02e2cd8a2f5), [`dd05faa`](https://github.com/Effect-TS/effect/commit/dd05faa621555ef3585ecd914ac13ecd89b710f4), [`e7ca973`](https://github.com/Effect-TS/effect/commit/e7ca973c5430ae60716701e58bedd4632ff971fd), [`a1f44cb`](https://github.com/Effect-TS/effect/commit/a1f44cb5112713ff9a3ac3d91a63a2c99d6b7fc1), [`d910dd2`](https://github.com/Effect-TS/effect/commit/d910dd2ca1e8e5aa2f09d9bf3694ede745758f99), [`e7ca973`](https://github.com/Effect-TS/effect/commit/e7ca973c5430ae60716701e58bedd4632ff971fd), [`802674b`](https://github.com/Effect-TS/effect/commit/802674b379b7559ad3ff09b33388891445a9e48b), [`bdff193`](https://github.com/Effect-TS/effect/commit/bdff193365dd9ec2863573b08eb960aa8dee5c93)]: + - effect@2.4.4 + - @effect/platform@0.48.0 + - @effect/platform-node-shared@0.3.0 + +## 0.45.1 + +### Patch Changes + +- [#2276](https://github.com/Effect-TS/effect/pull/2276) [`0680545`](https://github.com/Effect-TS/effect/commit/068054540f19bb23a79c7c021ed8b2fe34f3e19f) Thanks [@tim-smart](https://github.com/tim-smart)! - improve /platform error messages + +- Updated dependencies [[`0680545`](https://github.com/Effect-TS/effect/commit/068054540f19bb23a79c7c021ed8b2fe34f3e19f), [`20e63fb`](https://github.com/Effect-TS/effect/commit/20e63fb9207210f3fe2d136ec40d0a2dbff3225e), [`20e63fb`](https://github.com/Effect-TS/effect/commit/20e63fb9207210f3fe2d136ec40d0a2dbff3225e)]: + - @effect/platform@0.47.1 + - effect@2.4.3 + - @effect/platform-node-shared@0.2.5 + +## 0.45.0 + +### Minor Changes + +- [#2261](https://github.com/Effect-TS/effect/pull/2261) [`fa9663c`](https://github.com/Effect-TS/effect/commit/fa9663cb854ca03dba672d7857ecff84f1140c9e) Thanks [@tim-smart](https://github.com/tim-smart)! - move Socket module to platform + +### Patch Changes + +- [#2261](https://github.com/Effect-TS/effect/pull/2261) [`fa9663c`](https://github.com/Effect-TS/effect/commit/fa9663cb854ca03dba672d7857ecff84f1140c9e) Thanks [@tim-smart](https://github.com/tim-smart)! - add websocket support to platform http server + + You can use the `Http.request.upgrade*` apis to access the `Socket` for the request. + + Here is an example server that handles websockets on the `/ws` path: + + ```ts + import { NodeHttpServer, NodeRuntime } from "@effect/platform-node" + import * as Http from "@effect/platform/HttpServer" + import { Console, Effect, Layer, Schedule, Stream } from "effect" + import { createServer } from "node:http" + + const ServerLive = NodeHttpServer.server.layer(() => createServer(), { + port: 3000 + }) + + const HttpLive = Http.router.empty.pipe( + Http.router.get( + "/ws", + Effect.gen(function* (_) { + yield* _( + Stream.fromSchedule(Schedule.spaced(1000)), + Stream.map(JSON.stringify), + Stream.encodeText, + Stream.pipeThroughChannel(Http.request.upgradeChannel()), + Stream.decodeText(), + Stream.runForEach(Console.log) + ) + return Http.response.empty() + }) + ), + Http.server.serve(Http.middleware.logger), + Http.server.withLogAddress, + Layer.provide(ServerLive) + ) + + NodeRuntime.runMain(Layer.launch(HttpLive)) + ``` + +- Updated dependencies [[`e03811e`](https://github.com/Effect-TS/effect/commit/e03811e80c93e986e6348b3b67ac2ed6d5fefff0), [`ac41d84`](https://github.com/Effect-TS/effect/commit/ac41d84776484cdce8165b7ca2c9c9b6377eee2d), [`0f3d99c`](https://github.com/Effect-TS/effect/commit/0f3d99c27521ec6b221b644a0fffc79199c3acca), [`6137533`](https://github.com/Effect-TS/effect/commit/613753300c7705518ab1fea2f370b032851c2750), [`f373529`](https://github.com/Effect-TS/effect/commit/f373529999f4b8bc92b634f6ea14f19271388eed), [`1bf9f31`](https://github.com/Effect-TS/effect/commit/1bf9f31f07667de677673f7c29a4e7a26ebad3c8), [`e3ff789`](https://github.com/Effect-TS/effect/commit/e3ff789226f89e71eb28ca38ce79f90af6a03f1a), [`6137533`](https://github.com/Effect-TS/effect/commit/613753300c7705518ab1fea2f370b032851c2750), [`507ba40`](https://github.com/Effect-TS/effect/commit/507ba4060ff043c1a8d541dae723fa6940633b00), [`4064ea0`](https://github.com/Effect-TS/effect/commit/4064ea04e0b3fa23108ee471cd89ab2482b2f6e5), [`e466afe`](https://github.com/Effect-TS/effect/commit/e466afe32f2de598ceafd8982bd0cfbd388e5671), [`f373529`](https://github.com/Effect-TS/effect/commit/f373529999f4b8bc92b634f6ea14f19271388eed), [`de74eb8`](https://github.com/Effect-TS/effect/commit/de74eb80a79eebde5ff645033765e7a617e92f27), [`fa9663c`](https://github.com/Effect-TS/effect/commit/fa9663cb854ca03dba672d7857ecff84f1140c9e), [`fa9663c`](https://github.com/Effect-TS/effect/commit/fa9663cb854ca03dba672d7857ecff84f1140c9e)]: + - effect@2.4.2 + - @effect/platform-node-shared@0.2.4 + - @effect/platform@0.47.0 + +## 0.44.11 + +### Patch Changes + +- Updated dependencies [[`a4a0006`](https://github.com/Effect-TS/effect/commit/a4a0006c7f19fc261df5cda16963d73457e4d6ac), [`7535080`](https://github.com/Effect-TS/effect/commit/7535080f2e2f9859711031161600c01807cc43ea), [`0a37676`](https://github.com/Effect-TS/effect/commit/0a37676aa0eb2a21e17af2e6df9f81f52bbc8831), [`bd1d7ac`](https://github.com/Effect-TS/effect/commit/bd1d7ac75eea57a94d5e2d8e1edccb3136e84899), [`6f503b7`](https://github.com/Effect-TS/effect/commit/6f503b774d893bf2af34f66202e270d8c45d5f31)]: + - effect@2.4.1 + - @effect/platform@0.46.3 + - @effect/platform-node-shared@0.2.3 + +## 0.44.10 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform@0.46.2 + - @effect/platform-node-shared@0.2.2 + +## 0.44.9 + +### Patch Changes + +- Updated dependencies [[`aa6556f`](https://github.com/Effect-TS/effect/commit/aa6556f007117caea84d6965aa30846a11879e9d)]: + - @effect/platform@0.46.1 + - @effect/platform-node-shared@0.2.1 + +## 0.44.8 + +### Patch Changes + +- Updated dependencies [[`5de7be5`](https://github.com/Effect-TS/effect/commit/5de7be5beca2e963b503e6029dcc3217848187d2), [`489fcf3`](https://github.com/Effect-TS/effect/commit/489fcf363ff2b2a953166b740cb9a62d7fc2a101), [`7d9c3bf`](https://github.com/Effect-TS/effect/commit/7d9c3bff6c18d451e0e4781042945ec5c7be1b9f), [`d8d278b`](https://github.com/Effect-TS/effect/commit/d8d278b2efb2966947029885e01f7b68348a021f), [`14c5711`](https://github.com/Effect-TS/effect/commit/14c57110078f0862b8da5c7a2c5d980f54447484), [`5de7be5`](https://github.com/Effect-TS/effect/commit/5de7be5beca2e963b503e6029dcc3217848187d2), [`b9cb3a9`](https://github.com/Effect-TS/effect/commit/b9cb3a9c9bfdd75536bd70b4e8b557c12d4923ff), [`585fcce`](https://github.com/Effect-TS/effect/commit/585fcce162d0f07a48d7cd984a9b722966fbebbe), [`93b412d`](https://github.com/Effect-TS/effect/commit/93b412d4a9ed762dc9fa5807e51fad0fc78a614a), [`55b26a6`](https://github.com/Effect-TS/effect/commit/55b26a6342b4826f1116e7a1eb660118c274458e), [`a025b12`](https://github.com/Effect-TS/effect/commit/a025b121235ba01cfce8d62a775491880c575561), [`2097739`](https://github.com/Effect-TS/effect/commit/20977393d2383bff709304e81ec7d51cafd57108)]: + - effect@2.4.0 + - @effect/platform@0.46.0 + - @effect/platform-node-shared@0.2.0 + +## 0.44.7 + +### Patch Changes + +- [#2187](https://github.com/Effect-TS/effect/pull/2187) [`e6d36c0`](https://github.com/Effect-TS/effect/commit/e6d36c0813d836f17eabb6a9c7849baffca12dbf) Thanks [@tim-smart](https://github.com/tim-smart)! - update development dependencies + +- Updated dependencies [[`5ad2eec`](https://github.com/Effect-TS/effect/commit/5ad2eece0280b6db6a749d25cac1dcf6d33659a9), [`e6d36c0`](https://github.com/Effect-TS/effect/commit/e6d36c0813d836f17eabb6a9c7849baffca12dbf)]: + - effect@2.3.8 + - @effect/platform-node-shared@0.1.14 + - @effect/platform@0.45.6 + +## 0.44.6 + +### Patch Changes + +- Updated dependencies [[`6daf084`](https://github.com/Effect-TS/effect/commit/6daf0845de008772011db8d7c75b7c37a6b4d334)]: + - @effect/platform@0.45.5 + - @effect/platform-node-shared@0.1.13 + +## 0.44.5 + +### Patch Changes + +- Updated dependencies [[`bc8404d`](https://github.com/Effect-TS/effect/commit/bc8404d54fd42072d200c0399cb39672837afa9f), [`abcb7d9`](https://github.com/Effect-TS/effect/commit/abcb7d983a4a85b43b7175e952f5b331b9019aea), [`2c5cbcd`](https://github.com/Effect-TS/effect/commit/2c5cbcd1161b4f40dab184999291e817314107de), [`6565916`](https://github.com/Effect-TS/effect/commit/6565916ef254bf910e47d25fd0ef55e7cb420241), [`abcb7d9`](https://github.com/Effect-TS/effect/commit/abcb7d983a4a85b43b7175e952f5b331b9019aea)]: + - effect@2.3.7 + - @effect/platform@0.45.4 + - @effect/platform-node-shared@0.1.12 + +## 0.44.4 + +### Patch Changes + +- Updated dependencies [[`09532a8`](https://github.com/Effect-TS/effect/commit/09532a86b7d0cc23557c89158f0342753dfce4b0)]: + - @effect/platform@0.45.3 + - @effect/platform-node-shared@0.1.11 + +## 0.44.3 + +### Patch Changes + +- [#2150](https://github.com/Effect-TS/effect/pull/2150) [`f612749`](https://github.com/Effect-TS/effect/commit/f612749ddfff40cadef3387100135f2cb9a4a9f3) Thanks [@tim-smart](https://github.com/tim-smart)! - add unsafe body accessors to node http IncomingMessage + + these can be used from debuggers to inspect the request body. + +## 0.44.2 + +### Patch Changes + +- Updated dependencies [[`b1163b2`](https://github.com/Effect-TS/effect/commit/b1163b2bd67b65bafbbb39fc4c67576e5cbaf444), [`b46b869`](https://github.com/Effect-TS/effect/commit/b46b869e59a6da5aa235a9fcc25e1e0d24e9e8f8), [`de1b226`](https://github.com/Effect-TS/effect/commit/de1b226282b5ab6c2809dd93f3bdb066f24a1333), [`a663390`](https://github.com/Effect-TS/effect/commit/a66339090ae7b960f8a8b90a0dcdc505de5aaf3e), [`ff88f80`](https://github.com/Effect-TS/effect/commit/ff88f808c4ed9947a148045849e7410b00acad0a), [`11be07b`](https://github.com/Effect-TS/effect/commit/11be07bf65d82cfdf994cdb9d8ca937f995cb4f0), [`c568645`](https://github.com/Effect-TS/effect/commit/c5686451c87d26382135a1c63b00ef171bb24f62), [`88835e5`](https://github.com/Effect-TS/effect/commit/88835e575a0bfbeff9a3696a332f32192c940e12), [`b415577`](https://github.com/Effect-TS/effect/commit/b415577f6c576073733929c858e5aac27b6d5880), [`ff8046f`](https://github.com/Effect-TS/effect/commit/ff8046f57dfd073eba60ce6d3144ab060fbf93ce), [`44c3b43`](https://github.com/Effect-TS/effect/commit/44c3b43653e64d7e425d39815d8ff405acec9b99)]: + - effect@2.3.6 + - @effect/platform@0.45.2 + - @effect/platform-node-shared@0.1.10 + +## 0.44.1 + +### Patch Changes + +- Updated dependencies [[`65895ab`](https://github.com/Effect-TS/effect/commit/65895ab982e0917ac92f0827e387e7cf61be1e69)]: + - @effect/platform@0.45.1 + - @effect/platform-node-shared@0.1.9 + +## 0.44.0 + +### Minor Changes + +- [#2119](https://github.com/Effect-TS/effect/pull/2119) [`2b62548`](https://github.com/Effect-TS/effect/commit/2b6254845882f399636d24223c483e5489e3cff4) Thanks [@tim-smart](https://github.com/tim-smart)! - add Scope to Http client + + This change adds a scope to the default http client, ensuring connections are + cleaned up if you abort the request at any point. + + Some response helpers have been added to reduce the noise. + + ```ts + import * as Http from "@effect/platform/HttpClient" + import { Effect } from "effect" + + // instead of + Http.request.get("/").pipe( + Http.client.fetchOk(), + Effect.flatMap((_) => _.json), + Effect.scoped + ) + + // you can do + Http.request.get("/").pipe(Http.client.fetchOk(), Http.response.json) + + // other helpers include + Http.response.text + Http.response.stream + Http.response.arrayBuffer + Http.response.urlParamsBody + Http.response.formData + Http.response.schema * Effect + ``` + +### Patch Changes + +- Updated dependencies [[`2b62548`](https://github.com/Effect-TS/effect/commit/2b6254845882f399636d24223c483e5489e3cff4)]: + - @effect/platform@0.45.0 + - @effect/platform-node-shared@0.1.8 + +## 0.43.7 + +### Patch Changes + +- Updated dependencies [[`b881365`](https://github.com/Effect-TS/effect/commit/b8813650355322ea2fc1fbaa4f846bd87a7a05f3)]: + - effect@2.3.5 + - @effect/platform@0.44.7 + - @effect/platform-node-shared@0.1.7 + +## 0.43.6 + +### Patch Changes + +- Updated dependencies [[`17bda66`](https://github.com/Effect-TS/effect/commit/17bda66431c999a546920c10adb205e6c8bea7d1)]: + - effect@2.3.4 + - @effect/platform@0.44.6 + - @effect/platform-node-shared@0.1.6 + +## 0.43.5 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform@0.44.5 + - @effect/platform-node-shared@0.1.5 + +## 0.43.4 + +### Patch Changes + +- Updated dependencies [[`efd41d8`](https://github.com/Effect-TS/effect/commit/efd41d8131c3d90867608969ef7c4eef490eb5e6), [`0f83515`](https://github.com/Effect-TS/effect/commit/0f83515a9c01d13c7c15a3f026e02d22c3c6bb7f), [`0f83515`](https://github.com/Effect-TS/effect/commit/0f83515a9c01d13c7c15a3f026e02d22c3c6bb7f)]: + - effect@2.3.3 + - @effect/platform@0.44.4 + - @effect/platform-node-shared@0.1.4 + +## 0.43.3 + +### Patch Changes + +- Updated dependencies [[`6654f5f`](https://github.com/Effect-TS/effect/commit/6654f5f0f6b9d97165ede5e04ca16776e2599328), [`2eb11b4`](https://github.com/Effect-TS/effect/commit/2eb11b47752cedf233ef4c4395d9c4efc9b9e180), [`56c09bd`](https://github.com/Effect-TS/effect/commit/56c09bd369279a6a7785209d172739935818cba6), [`71aa5b1`](https://github.com/Effect-TS/effect/commit/71aa5b1c180dcb8b53aefe232d12a97bd06b5447), [`1700af8`](https://github.com/Effect-TS/effect/commit/1700af8af1131602887da721914c8562b6342393)]: + - effect@2.3.2 + - @effect/platform@0.44.3 + - @effect/platform-node-shared@0.1.3 + +## 0.43.2 + +### Patch Changes + +- Updated dependencies [[`29739dd`](https://github.com/Effect-TS/effect/commit/29739dde8e6232824d49c4c7f8856de245249c5c)]: + - @effect/platform@0.44.2 + - @effect/platform-node-shared@0.1.2 + +## 0.43.1 + +### Patch Changes + +- Updated dependencies [[`b5a8215`](https://github.com/Effect-TS/effect/commit/b5a8215ee2a97a8865d69ee55ce1b9835948c922)]: + - effect@2.3.1 + - @effect/platform@0.44.1 + - @effect/platform-node-shared@0.1.1 + +## 0.43.0 + +### Minor Changes + +- [#2006](https://github.com/Effect-TS/effect/pull/2006) [`9a2d1c1`](https://github.com/Effect-TS/effect/commit/9a2d1c1468ea0789b34767ad683da074f061ea9c) Thanks [@github-actions](https://github.com/apps/github-actions)! - With this change we now require a string key to be provided for all tags and renames the dear old `Tag` to `GenericTag`, so when previously you could do: + + ```ts + import { Effect, Context } from "effect" + interface Service { + readonly _: unique symbol + } + const Service = Context.Tag< + Service, + { + number: Effect.Effect + } + >() + ``` + + you are now mandated to do: + + ```ts + import { Effect, Context } from "effect" + interface Service { + readonly _: unique symbol + } + const Service = Context.GenericTag< + Service, + { + number: Effect.Effect + } + >("Service") + ``` + + This makes by default all tags globals and ensures better debuggaility when unexpected errors arise. + + Furthermore we introduce a new way of constructing tags that should be considered the new default: + + ```ts + import { Effect, Context } from "effect" + class Service extends Context.Tag("Service")< + Service, + { + number: Effect.Effect + } + >() {} + + const program = Effect.flatMap(Service, ({ number }) => number).pipe( + Effect.flatMap((_) => Effect.log(`number: ${_}`)) + ) + ``` + + this will use "Service" as the key and will create automatically an opaque identifier (the class) to be used at the type level, it does something similar to the above in a single shot. + +- [#2006](https://github.com/Effect-TS/effect/pull/2006) [`1a77f72`](https://github.com/Effect-TS/effect/commit/1a77f72cdaf43d6cdc91b6060f82832edcdbbcb3) Thanks [@github-actions](https://github.com/apps/github-actions)! - change `Effect` type parameters order from `Effect` to `Effect` + +- [#2006](https://github.com/Effect-TS/effect/pull/2006) [`af47aa3`](https://github.com/Effect-TS/effect/commit/af47aa37196ad542c9c23a4896d8ef98147e1205) Thanks [@github-actions](https://github.com/apps/github-actions)! - move where platform worker spawn function is provided + + With this change, the point in which you provide the spawn function moves closer + to the edge, where you provide platform specific implementation. + + This seperates even more platform concerns from your business logic. Example: + + ```ts + import { Worker } from "@effect/platform" + import { BrowserWorker } from "@effect/platform-browser" + import { Effect } from "effect" + + Worker.makePool({ ... }).pipe( + Effect.provide(BrowserWorker.layer(() => new globalThis.Worker(...))) + ) + ``` + +- [#2006](https://github.com/Effect-TS/effect/pull/2006) [`a34dbdc`](https://github.com/Effect-TS/effect/commit/a34dbdc1552c73c1b612676f262a0c735ce444a7) Thanks [@github-actions](https://github.com/apps/github-actions)! - - Schema: change type parameters order from `Schema` to `Schema` + - Serializable: change type parameters order from `Serializable` to `Serializable` + - Class: change type parameters order from `Class` to `Class` + - PropertySignature: change type parameters order from `PropertySignature` to `PropertySignature` + +- [#2006](https://github.com/Effect-TS/effect/pull/2006) [`b1e2086`](https://github.com/Effect-TS/effect/commit/b1e2086ea8bf410e4ad75d71c0760825924e8f9f) Thanks [@github-actions](https://github.com/apps/github-actions)! - remove re-exports from platform packages + +- [#2006](https://github.com/Effect-TS/effect/pull/2006) [`9a2d1c1`](https://github.com/Effect-TS/effect/commit/9a2d1c1468ea0789b34767ad683da074f061ea9c) Thanks [@github-actions](https://github.com/apps/github-actions)! - This change enables `Effect.serviceConstants` and `Effect.serviceMembers` to access any constant in the service, not only the effects, namely it is now possible to do: + + ```ts + import { Effect, Context } from "effect" + + class NumberRepo extends Context.TagClass("NumberRepo")< + NumberRepo, + { + readonly numbers: Array + } + >() { + static numbers = Effect.serviceConstants(NumberRepo).numbers + } + ``` + +### Patch Changes + +- Updated dependencies [[`96bcee2`](https://github.com/Effect-TS/effect/commit/96bcee21021aecd8ffd86440a2c9be353c4668e3), [`b1e2086`](https://github.com/Effect-TS/effect/commit/b1e2086ea8bf410e4ad75d71c0760825924e8f9f), [`96bcee2`](https://github.com/Effect-TS/effect/commit/96bcee21021aecd8ffd86440a2c9be353c4668e3), [`c77f635`](https://github.com/Effect-TS/effect/commit/c77f635f8a26ca6d83cb569d911f8eee79033fd9), [`e343a74`](https://github.com/Effect-TS/effect/commit/e343a74843dd9edf879417fa94cb51de7ed5b402), [`acf1894`](https://github.com/Effect-TS/effect/commit/acf1894f45945dbe5c39451e36aabb4b5092f257), [`9a2d1c1`](https://github.com/Effect-TS/effect/commit/9a2d1c1468ea0789b34767ad683da074f061ea9c), [`1a77f72`](https://github.com/Effect-TS/effect/commit/1a77f72cdaf43d6cdc91b6060f82832edcdbbcb3), [`af47aa3`](https://github.com/Effect-TS/effect/commit/af47aa37196ad542c9c23a4896d8ef98147e1205), [`c986f0e`](https://github.com/Effect-TS/effect/commit/c986f0e0ce4d22ba08177ed351152718479ab63c), [`96bcee2`](https://github.com/Effect-TS/effect/commit/96bcee21021aecd8ffd86440a2c9be353c4668e3), [`70dde23`](https://github.com/Effect-TS/effect/commit/70dde238f81125e353fd7bde5fc24ecd8969bf97), [`a34dbdc`](https://github.com/Effect-TS/effect/commit/a34dbdc1552c73c1b612676f262a0c735ce444a7), [`81b7425`](https://github.com/Effect-TS/effect/commit/81b7425320cbbe2a6cf547a3e3ab3549cdba14cf), [`b1e2086`](https://github.com/Effect-TS/effect/commit/b1e2086ea8bf410e4ad75d71c0760825924e8f9f), [`02c3461`](https://github.com/Effect-TS/effect/commit/02c34615d02f91269ea04036d0306fccf4e39e18), [`0e56e99`](https://github.com/Effect-TS/effect/commit/0e56e998ab9815c4d096c239a553cb86a0f99af9), [`8b0ded9`](https://github.com/Effect-TS/effect/commit/8b0ded9f10ba0d96fcb9af24eff2dbd9341f85e3), [`8dd83e8`](https://github.com/Effect-TS/effect/commit/8dd83e854bfcaa6dab876994c5f813dcfb486c28), [`5127afe`](https://github.com/Effect-TS/effect/commit/5127afec1c519e0a3d7460844a9101a96272f29e), [`d75f6fe`](https://github.com/Effect-TS/effect/commit/d75f6fe6499deb0a5ee9ec94af3b5fd4eb03a2d0), [`7356e5c`](https://github.com/Effect-TS/effect/commit/7356e5cc16e9d70f18c02dee1dcb4ad539fd130a), [`3077cde`](https://github.com/Effect-TS/effect/commit/3077cde08a60246821a940964a84dd7f7c8b9f54), [`be19ce0`](https://github.com/Effect-TS/effect/commit/be19ce0b8bdf1fac80bb8d7e0b06a86986b47409), [`4a5d01a`](https://github.com/Effect-TS/effect/commit/4a5d01a409e9b6dd53893e65f8e5c9247f568021), [`78f47ab`](https://github.com/Effect-TS/effect/commit/78f47abfe3cb0a8bbde818b1c5fc603270538b47), [`6361ee2`](https://github.com/Effect-TS/effect/commit/6361ee2e83bdfead24045c3d058a7298efc18113), [`52e5d20`](https://github.com/Effect-TS/effect/commit/52e5d2077582bf51f25861c7139fc920c2c24166), [`c6137ec`](https://github.com/Effect-TS/effect/commit/c6137ec62c6b5542d5062ae1a3c936cb915dee22), [`86f665d`](https://github.com/Effect-TS/effect/commit/86f665d7bd25ba0a3f046a2384798378310dcf0c), [`f5ae081`](https://github.com/Effect-TS/effect/commit/f5ae08195e68e76faeac258c565d79da4e01e7d6), [`4a5d01a`](https://github.com/Effect-TS/effect/commit/4a5d01a409e9b6dd53893e65f8e5c9247f568021), [`60686f5`](https://github.com/Effect-TS/effect/commit/60686f5c38bef1b93a3a0dda9b6596d46aceab03), [`9a2d1c1`](https://github.com/Effect-TS/effect/commit/9a2d1c1468ea0789b34767ad683da074f061ea9c), [`5127afe`](https://github.com/Effect-TS/effect/commit/5127afec1c519e0a3d7460844a9101a96272f29e), [`8ee2931`](https://github.com/Effect-TS/effect/commit/8ee293159b4f7cb7af8558287a0a047f3a69743d), [`6727474`](https://github.com/Effect-TS/effect/commit/672747497490a30d36dd49c06db19aabf09dc7f0), [`5127afe`](https://github.com/Effect-TS/effect/commit/5127afec1c519e0a3d7460844a9101a96272f29e)]: + - effect@2.3.0 + - @effect/platform@0.44.0 + - @effect/platform-node-shared@0.1.0 + +## 0.42.11 + +### Patch Changes + +- Updated dependencies [[`3ddfdbf`](https://github.com/Effect-TS/effect/commit/3ddfdbf914edea536aef207cec6695f33496258c), [`3ddfdbf`](https://github.com/Effect-TS/effect/commit/3ddfdbf914edea536aef207cec6695f33496258c)]: + - effect@2.2.5 + - @effect/platform@0.43.11 + +## 0.42.10 + +### Patch Changes + +- Updated dependencies [[`d0b911c`](https://github.com/Effect-TS/effect/commit/d0b911c75f284c7aa87f25aa96926e6bde7690d0), [`330e1a4`](https://github.com/Effect-TS/effect/commit/330e1a4e2c1fc0af6c80c80c81dd38c3e50fab78), [`6928a2b`](https://github.com/Effect-TS/effect/commit/6928a2b0bae86a4bdfbece0aa32924207c2d5a70), [`296bc1c`](https://github.com/Effect-TS/effect/commit/296bc1c9d24986d299d2669115d584cb27b73c60)]: + - effect@2.2.4 + - @effect/platform@0.43.10 + +## 0.42.9 + +### Patch Changes + +- Updated dependencies [[`1b841a9`](https://github.com/Effect-TS/effect/commit/1b841a91fed86825cd2867cf1e68e41d8ff26b4e)]: + - @effect/platform@0.43.9 + +## 0.42.8 + +### Patch Changes + +- Updated dependencies [[`32bf796`](https://github.com/Effect-TS/effect/commit/32bf796c3e5db1b2b68e8b1b20db664295991643)]: + - @effect/platform@0.43.8 + +## 0.42.7 + +### Patch Changes + +- Updated dependencies [[`cde08f3`](https://github.com/Effect-TS/effect/commit/cde08f354ed2ff2921d1d98bd539c7d65a2ddd73)]: + - @effect/platform@0.43.7 + +## 0.42.6 + +### Patch Changes + +- Updated dependencies [[`c96bb17`](https://github.com/Effect-TS/effect/commit/c96bb17043e2cec1eaeb319614a4c2904d876beb)]: + - @effect/platform@0.43.6 + +## 0.42.5 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform@0.43.5 + +## 0.42.4 + +### Patch Changes + +- [#1999](https://github.com/Effect-TS/effect/pull/1999) [`78f5921`](https://github.com/Effect-TS/effect/commit/78f59211502ded6fcbe15a49d6fde941cccc9d52) Thanks [@tim-smart](https://github.com/tim-smart)! - ensure forked fibers are interruptible + +- Updated dependencies [[`22794e0`](https://github.com/Effect-TS/effect/commit/22794e0ba00e40281f30a22fa84412003c24877d), [`f73e6c0`](https://github.com/Effect-TS/effect/commit/f73e6c033fb0729a9cfa5eb4bc39f79d3126e247), [`78f5921`](https://github.com/Effect-TS/effect/commit/78f59211502ded6fcbe15a49d6fde941cccc9d52)]: + - effect@2.2.3 + - @effect/platform@0.43.4 + +## 0.42.3 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform@0.43.3 + +## 0.42.2 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform@0.43.2 + +## 0.42.1 + +### Patch Changes + +- Updated dependencies [[`d404561`](https://github.com/Effect-TS/effect/commit/d404561e47ec2fa5f68709a308ee5d2ee959141d), [`7b84a3c`](https://github.com/Effect-TS/effect/commit/7b84a3c7e4b9c8dc02294b0e3cc3ae3becea977b)]: + - effect@2.2.2 + - @effect/platform@0.43.1 + +## 0.42.0 + +### Minor Changes + +- [#1922](https://github.com/Effect-TS/effect/pull/1922) [`62b40e8`](https://github.com/Effect-TS/effect/commit/62b40e8479371d6663c0255aaca56a1ae0d59764) Thanks [@gcanti](https://github.com/gcanti)! - add context tracking to Schema, closes #1873 + +### Patch Changes + +- Updated dependencies [[`84da31f`](https://github.com/Effect-TS/effect/commit/84da31f0643e8651b9d311b30526b1e4edfbdfb8), [`645bea2`](https://github.com/Effect-TS/effect/commit/645bea2551129f94a5b0e38347e28067dee531bb), [`62b40e8`](https://github.com/Effect-TS/effect/commit/62b40e8479371d6663c0255aaca56a1ae0d59764)]: + - effect@2.2.1 + - @effect/platform@0.43.0 + +## 0.41.8 + +### Patch Changes + +- Updated dependencies [[`202befc`](https://github.com/Effect-TS/effect/commit/202befc2ecbeb117c4fa85ef9b12a3d3a48273d2), [`fe05ad7`](https://github.com/Effect-TS/effect/commit/fe05ad7bcb3b88d47800ab69ebf53641023676f1), [`ee4ff8a`](https://github.com/Effect-TS/effect/commit/ee4ff8a943141fcf2877af92c5877ee87a989fb9), [`ee4ff8a`](https://github.com/Effect-TS/effect/commit/ee4ff8a943141fcf2877af92c5877ee87a989fb9), [`ee4ff8a`](https://github.com/Effect-TS/effect/commit/ee4ff8a943141fcf2877af92c5877ee87a989fb9), [`ee4ff8a`](https://github.com/Effect-TS/effect/commit/ee4ff8a943141fcf2877af92c5877ee87a989fb9), [`10df798`](https://github.com/Effect-TS/effect/commit/10df798639e556f9d88265ef7fc3cf8a3bbe3874)]: + - effect@2.2.0 + - @effect/platform@0.42.7 + +## 0.41.7 + +### Patch Changes + +- Updated dependencies [[`21b9edd`](https://github.com/Effect-TS/effect/commit/21b9edde464f7c5624ef54ad1b5e264204a37625)]: + - effect@2.1.2 + - @effect/platform@0.42.6 + +## 0.41.6 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform@0.42.5 + +## 0.41.5 + +### Patch Changes + +- Updated dependencies [[`a222524`](https://github.com/Effect-TS/effect/commit/a2225247e9de2e013d287320790fde88c081dbbd)]: + - effect@2.1.1 + - @effect/platform@0.42.4 + +## 0.41.4 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform@0.42.3 + +## 0.41.3 + +### Patch Changes + +- Updated dependencies [[`05c44b3`](https://github.com/Effect-TS/effect/commit/05c44b30662554dde50b70bad79f13ae895fda02), [`05c44b3`](https://github.com/Effect-TS/effect/commit/05c44b30662554dde50b70bad79f13ae895fda02), [`05c44b3`](https://github.com/Effect-TS/effect/commit/05c44b30662554dde50b70bad79f13ae895fda02), [`05c44b3`](https://github.com/Effect-TS/effect/commit/05c44b30662554dde50b70bad79f13ae895fda02), [`05c44b3`](https://github.com/Effect-TS/effect/commit/05c44b30662554dde50b70bad79f13ae895fda02), [`05c44b3`](https://github.com/Effect-TS/effect/commit/05c44b30662554dde50b70bad79f13ae895fda02)]: + - effect@2.1.0 + - @effect/platform@0.42.2 + +## 0.41.2 + +### Patch Changes + +- Updated dependencies [[`f7f19f6`](https://github.com/Effect-TS/effect/commit/f7f19f66a5fa349baa2412c1f9f15111c437df09)]: + - effect@2.0.5 + - @effect/platform@0.42.1 + +## 0.41.1 + +### Patch Changes + +- [#1905](https://github.com/Effect-TS/effect/pull/1905) [`71ed54c`](https://github.com/Effect-TS/effect/commit/71ed54c3fbb1ead5da2776bc6207050cb073ada4) Thanks [@datner](https://github.com/datner)! - change to an actually allowed number + +- Updated dependencies [[`536c1df`](https://github.com/Effect-TS/effect/commit/536c1dfb7833961dfb2fbd6bcd2dbdfa2f208d51), [`8eec87e`](https://github.com/Effect-TS/effect/commit/8eec87e311ce55281a98517e6df0ef103b43e8a8), [`540b294`](https://github.com/Effect-TS/effect/commit/540b2941dd0a81e9688311583ce7e2e140d6e7a5), [`536c1df`](https://github.com/Effect-TS/effect/commit/536c1dfb7833961dfb2fbd6bcd2dbdfa2f208d51), [`a3f96d6`](https://github.com/Effect-TS/effect/commit/a3f96d615b8b3e238dbfa01ef713c87e6f4532be), [`8eec87e`](https://github.com/Effect-TS/effect/commit/8eec87e311ce55281a98517e6df0ef103b43e8a8), [`536c1df`](https://github.com/Effect-TS/effect/commit/536c1dfb7833961dfb2fbd6bcd2dbdfa2f208d51), [`8eec87e`](https://github.com/Effect-TS/effect/commit/8eec87e311ce55281a98517e6df0ef103b43e8a8), [`8eec87e`](https://github.com/Effect-TS/effect/commit/8eec87e311ce55281a98517e6df0ef103b43e8a8), [`25adce7`](https://github.com/Effect-TS/effect/commit/25adce7ae76ce834096dca1ed70a60ad1a349217), [`536c1df`](https://github.com/Effect-TS/effect/commit/536c1dfb7833961dfb2fbd6bcd2dbdfa2f208d51), [`48a3d40`](https://github.com/Effect-TS/effect/commit/48a3d40aed0f923f567b8911dade732ff472d981)]: + - effect@2.0.4 + - @effect/platform@0.42.0 + +## 0.41.0 + +### Minor Changes + +- [#1885](https://github.com/Effect-TS/effect/pull/1885) [`1d3a06b`](https://github.com/Effect-TS/effect/commit/1d3a06bb58ad1ac123ae8f9d42b4345f9c9c53c0) Thanks [@mikearnaldi](https://github.com/mikearnaldi)! - lift worker shutdown to /platform implementation + +### Patch Changes + +- [#1885](https://github.com/Effect-TS/effect/pull/1885) [`1d3a06b`](https://github.com/Effect-TS/effect/commit/1d3a06bb58ad1ac123ae8f9d42b4345f9c9c53c0) Thanks [@mikearnaldi](https://github.com/mikearnaldi)! - Avoid killing all fibers on interrupt + +- Updated dependencies [[`87f7ef2`](https://github.com/Effect-TS/effect/commit/87f7ef28a3c27e2e4f2fcfa465f85bb2a45a3d6b), [`1d3a06b`](https://github.com/Effect-TS/effect/commit/1d3a06bb58ad1ac123ae8f9d42b4345f9c9c53c0), [`1d3a06b`](https://github.com/Effect-TS/effect/commit/1d3a06bb58ad1ac123ae8f9d42b4345f9c9c53c0)]: + - effect@2.0.3 + - @effect/platform@0.41.0 + +## 0.40.4 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform@0.40.4 + +## 0.40.3 + +### Patch Changes + +- [#1879](https://github.com/Effect-TS/effect/pull/1879) [`92c0322`](https://github.com/Effect-TS/effect/commit/92c0322a58bf7e5b8dbb602186030839e89df5af) Thanks [@tim-smart](https://github.com/tim-smart)! - add http Multiplex module + +- Updated dependencies [[`92c0322`](https://github.com/Effect-TS/effect/commit/92c0322a58bf7e5b8dbb602186030839e89df5af)]: + - @effect/platform@0.40.3 + +## 0.40.2 + +### Patch Changes + +- Updated dependencies [[`4c90c54`](https://github.com/Effect-TS/effect/commit/4c90c54d87c91f75f3ad114926cdf3b0c25df091), [`d5a1949`](https://github.com/Effect-TS/effect/commit/d5a19499aac7c1d147674a35ac69992177c7536c), [`d3d3bda`](https://github.com/Effect-TS/effect/commit/d3d3bda74c794153def9027e0c40896e72cd5d14)]: + - @effect/platform@0.40.2 + - effect@2.0.2 + +## 0.40.1 + +### Patch Changes + +- Updated dependencies [[`16bd87d`](https://github.com/Effect-TS/effect/commit/16bd87d32611b966dc42ea4fc979764f97a49071)]: + - effect@2.0.1 + - @effect/platform@0.40.1 + +## 0.40.0 + +### Minor Changes + +- [`d0471ca`](https://github.com/Effect-TS/effect/commit/d0471ca7b544746674b9e1750202da72b0a21233) Thanks [@mikearnaldi](https://github.com/mikearnaldi)! - Switch to monorepo structure + +- [#1846](https://github.com/Effect-TS/effect/pull/1846) [`693b8f3`](https://github.com/Effect-TS/effect/commit/693b8f3a3dfd43ae61f0d9292cdf356be7329f2f) Thanks [@fubhy](https://github.com/fubhy)! - Enabled `exactOptionalPropertyTypes` throughout + +### Patch Changes + +- [`d987daa`](https://github.com/Effect-TS/effect/commit/d987daafaddd43b6ade74916a08236c19ea0a9fa) Thanks [@mikearnaldi](https://github.com/mikearnaldi)! - Switch effect dependency to caret + +- [#1848](https://github.com/Effect-TS/effect/pull/1848) [`04fb8b4`](https://github.com/Effect-TS/effect/commit/04fb8b428b19bba85a2c79910c5e363340d074e7) Thanks [@fubhy](https://github.com/fubhy)! - Avoid default parameter initilization + +- [#1796](https://github.com/Effect-TS/effect/pull/1796) [`99d22cb`](https://github.com/Effect-TS/effect/commit/99d22cbee13cc2111a4a634cbe73b9b7d7fd88c7) Thanks [@leonitousconforti](https://github.com/leonitousconforti)! - Http client treats upgrade response as successful response + +- Updated dependencies [[`d987daa`](https://github.com/Effect-TS/effect/commit/d987daafaddd43b6ade74916a08236c19ea0a9fa), [`7b5eaa3`](https://github.com/Effect-TS/effect/commit/7b5eaa3838c79bf4bdccf91b94d61bbc38a2ec95), [`0724211`](https://github.com/Effect-TS/effect/commit/072421149c36010748ff6b6ee19c15c6cffefe09), [`9f2bc5a`](https://github.com/Effect-TS/effect/commit/9f2bc5a19e0b678a0a85e84daac290922b0fd57d), [`04fb8b4`](https://github.com/Effect-TS/effect/commit/04fb8b428b19bba85a2c79910c5e363340d074e7), [`d0471ca`](https://github.com/Effect-TS/effect/commit/d0471ca7b544746674b9e1750202da72b0a21233), [`bcf0900`](https://github.com/Effect-TS/effect/commit/bcf0900b58f449262556f80bff21e771a37272aa), [`6299b84`](https://github.com/Effect-TS/effect/commit/6299b84c11e5d1fe79fa538df8935018c7613747), [`c0aeb5e`](https://github.com/Effect-TS/effect/commit/c0aeb5e302869bcd7d7627f8cc5b630d07c12d10), [`693b8f3`](https://github.com/Effect-TS/effect/commit/693b8f3a3dfd43ae61f0d9292cdf356be7329f2f)]: + - @effect/platform@0.40.0 + - effect@2.0.0 + +## 0.39.0 + +### Minor Changes + +- [#369](https://github.com/Effect-TS/platform/pull/369) [`5d5f62b`](https://github.com/Effect-TS/platform/commit/5d5f62b03ffdbca0a986d968e1dbb45886dfa827) Thanks [@tim-smart](https://github.com/tim-smart)! - rename server FormData module to Multipart + +- [#372](https://github.com/Effect-TS/platform/pull/372) [`15784c9`](https://github.com/Effect-TS/platform/commit/15784c920dcae40f328bb45ac850987135207365) Thanks [@tim-smart](https://github.com/tim-smart)! - update dependencies + +- [#373](https://github.com/Effect-TS/platform/pull/373) [`b042ba5`](https://github.com/Effect-TS/platform/commit/b042ba5ae78a1eed592e543c233fe3040d6a60da) Thanks [@tim-smart](https://github.com/tim-smart)! - update effect + +### Patch Changes + +- Updated dependencies [[`5d5f62b`](https://github.com/Effect-TS/platform/commit/5d5f62b03ffdbca0a986d968e1dbb45886dfa827), [`15784c9`](https://github.com/Effect-TS/platform/commit/15784c920dcae40f328bb45ac850987135207365), [`b042ba5`](https://github.com/Effect-TS/platform/commit/b042ba5ae78a1eed592e543c233fe3040d6a60da), [`49fb154`](https://github.com/Effect-TS/platform/commit/49fb15439f18701321db8ded839243b9dd8de71a)]: + - @effect/platform@0.39.0 + +## 0.38.0 + +### Minor Changes + +- [#367](https://github.com/Effect-TS/platform/pull/367) [`7d1584b`](https://github.com/Effect-TS/platform/commit/7d1584b23d464651c206201ff304c6eb4bebfc3a) Thanks [@tim-smart](https://github.com/tim-smart)! - update effect + +### Patch Changes + +- Updated dependencies [[`7d1584b`](https://github.com/Effect-TS/platform/commit/7d1584b23d464651c206201ff304c6eb4bebfc3a)]: + - @effect/platform@0.38.0 + +## 0.37.10 + +### Patch Changes + +- [#366](https://github.com/Effect-TS/platform/pull/366) [`1d6bf73`](https://github.com/Effect-TS/platform/commit/1d6bf730dad0a6bbb282f436ec7d5870de76ca3a) Thanks [@tim-smart](https://github.com/tim-smart)! - add Scope to every http request + +- [#365](https://github.com/Effect-TS/platform/pull/365) [`3351136`](https://github.com/Effect-TS/platform/commit/335113601c238104eb2e331d26b5e463bde80dff) Thanks [@tim-smart](https://github.com/tim-smart)! - respond with 503 on server induced interrupt + +- Updated dependencies [[`e2c545a`](https://github.com/Effect-TS/platform/commit/e2c545a328c2bccbba661540a8835b10bce4b438), [`1d6bf73`](https://github.com/Effect-TS/platform/commit/1d6bf730dad0a6bbb282f436ec7d5870de76ca3a), [`3351136`](https://github.com/Effect-TS/platform/commit/335113601c238104eb2e331d26b5e463bde80dff)]: + - @effect/platform@0.37.8 + +## 0.37.9 + +### Patch Changes + +- Updated dependencies [[`df3af6b`](https://github.com/Effect-TS/platform/commit/df3af6be61572bab15004bbca2c5739d8206f3c3)]: + - @effect/platform@0.37.7 + +## 0.37.8 + +### Patch Changes + +- [#359](https://github.com/Effect-TS/platform/pull/359) [`6dbc587`](https://github.com/Effect-TS/platform/commit/6dbc587868d2703ad9a4c9995cb9dacdfc29c364) Thanks [@tim-smart](https://github.com/tim-smart)! - use branded type for Headers + +- [#359](https://github.com/Effect-TS/platform/pull/359) [`6dbc587`](https://github.com/Effect-TS/platform/commit/6dbc587868d2703ad9a4c9995cb9dacdfc29c364) Thanks [@tim-smart](https://github.com/tim-smart)! - change UrlParams to ReadonlyArray + +- Updated dependencies [[`6dbc587`](https://github.com/Effect-TS/platform/commit/6dbc587868d2703ad9a4c9995cb9dacdfc29c364), [`6dbc587`](https://github.com/Effect-TS/platform/commit/6dbc587868d2703ad9a4c9995cb9dacdfc29c364)]: + - @effect/platform@0.37.6 + +## 0.37.7 + +### Patch Changes + +- [#357](https://github.com/Effect-TS/platform/pull/357) [`6db1c07`](https://github.com/Effect-TS/platform/commit/6db1c0768d8afd8a45c0af31cbdfc40c9319e48b) Thanks [@tim-smart](https://github.com/tim-smart)! - respond witu 499 on interrupt + +## 0.37.6 + +### Patch Changes + +- [#354](https://github.com/Effect-TS/platform/pull/354) [`190bc84`](https://github.com/Effect-TS/platform/commit/190bc84b137a729a38b6812e220085b3d12cb124) Thanks [@tim-smart](https://github.com/tim-smart)! - add Layer support to SerializedWorker + +- Updated dependencies [[`190bc84`](https://github.com/Effect-TS/platform/commit/190bc84b137a729a38b6812e220085b3d12cb124)]: + - @effect/platform@0.37.5 + +## 0.37.5 + +### Patch Changes + +- [#352](https://github.com/Effect-TS/platform/pull/352) [`1c02a35`](https://github.com/Effect-TS/platform/commit/1c02a35df2f34601b547e17ddeab98236e10f77d) Thanks [@tim-smart](https://github.com/tim-smart)! - interrupt all fibers on worker interrupt + +- Updated dependencies [[`1c02a35`](https://github.com/Effect-TS/platform/commit/1c02a35df2f34601b547e17ddeab98236e10f77d), [`1c02a35`](https://github.com/Effect-TS/platform/commit/1c02a35df2f34601b547e17ddeab98236e10f77d)]: + - @effect/platform@0.37.4 + +## 0.37.4 + +### Patch Changes + +- [#350](https://github.com/Effect-TS/platform/pull/350) [`b30e5e3`](https://github.com/Effect-TS/platform/commit/b30e5e3874f22037f92253037fff6952f537ee40) Thanks [@tim-smart](https://github.com/tim-smart)! - add decode option to worker runner + +- Updated dependencies [[`b30e5e3`](https://github.com/Effect-TS/platform/commit/b30e5e3874f22037f92253037fff6952f537ee40)]: + - @effect/platform@0.37.3 + +## 0.37.3 + +### Patch Changes + +- [#348](https://github.com/Effect-TS/platform/pull/348) [`28edc60`](https://github.com/Effect-TS/platform/commit/28edc60d2fcd30160529c677a9ffd786775e534b) Thanks [@tim-smart](https://github.com/tim-smart)! - add layer worker runner apis + +- Updated dependencies [[`28edc60`](https://github.com/Effect-TS/platform/commit/28edc60d2fcd30160529c677a9ffd786775e534b)]: + - @effect/platform@0.37.2 + +## 0.37.2 + +### Patch Changes + +- [#346](https://github.com/Effect-TS/platform/pull/346) [`c0fdc3d`](https://github.com/Effect-TS/platform/commit/c0fdc3df8d8fc057fc388f5cb1a17d707d54f3eb) Thanks [@tim-smart](https://github.com/tim-smart)! - ensure node client doesn't throw on interruption + +## 0.37.1 + +### Patch Changes + +- [#344](https://github.com/Effect-TS/platform/pull/344) [`5b7cdbd`](https://github.com/Effect-TS/platform/commit/5b7cdbdf8ded48903a9f39df800fd7a22f73f0f7) Thanks [@tim-smart](https://github.com/tim-smart)! - support error and output transfers in worker runners + +- [#344](https://github.com/Effect-TS/platform/pull/344) [`5b7cdbd`](https://github.com/Effect-TS/platform/commit/5b7cdbdf8ded48903a9f39df800fd7a22f73f0f7) Thanks [@tim-smart](https://github.com/tim-smart)! - support initialMessage in workers + +- [#344](https://github.com/Effect-TS/platform/pull/344) [`5b7cdbd`](https://github.com/Effect-TS/platform/commit/5b7cdbdf8ded48903a9f39df800fd7a22f73f0f7) Thanks [@tim-smart](https://github.com/tim-smart)! - add Schema transforms to Transferable + +- [#344](https://github.com/Effect-TS/platform/pull/344) [`5b7cdbd`](https://github.com/Effect-TS/platform/commit/5b7cdbdf8ded48903a9f39df800fd7a22f73f0f7) Thanks [@tim-smart](https://github.com/tim-smart)! - make worker encoding return Effects + +- Updated dependencies [[`5b7cdbd`](https://github.com/Effect-TS/platform/commit/5b7cdbdf8ded48903a9f39df800fd7a22f73f0f7), [`5b7cdbd`](https://github.com/Effect-TS/platform/commit/5b7cdbdf8ded48903a9f39df800fd7a22f73f0f7), [`5b7cdbd`](https://github.com/Effect-TS/platform/commit/5b7cdbdf8ded48903a9f39df800fd7a22f73f0f7), [`5b7cdbd`](https://github.com/Effect-TS/platform/commit/5b7cdbdf8ded48903a9f39df800fd7a22f73f0f7)]: + - @effect/platform@0.37.1 + +## 0.37.0 + +### Minor Changes + +- [#341](https://github.com/Effect-TS/platform/pull/341) [`649f57f`](https://github.com/Effect-TS/platform/commit/649f57fdf557eed5f8405a4a4553dfc47fd8d4b1) Thanks [@tim-smart](https://github.com/tim-smart)! - use peer deps for /platform-\* + +### Patch Changes + +- Updated dependencies [[`649f57f`](https://github.com/Effect-TS/platform/commit/649f57fdf557eed5f8405a4a4553dfc47fd8d4b1), [`649f57f`](https://github.com/Effect-TS/platform/commit/649f57fdf557eed5f8405a4a4553dfc47fd8d4b1)]: + - @effect/platform@0.37.0 + +## 0.36.0 + +### Minor Changes + +- [#338](https://github.com/Effect-TS/platform/pull/338) [`7eaa8e5`](https://github.com/Effect-TS/platform/commit/7eaa8e52b18d408688e7b4909bcf016b0c04e80a) Thanks [@tim-smart](https://github.com/tim-smart)! - change http serve api to return immediately + +- [#338](https://github.com/Effect-TS/platform/pull/338) [`7eaa8e5`](https://github.com/Effect-TS/platform/commit/7eaa8e52b18d408688e7b4909bcf016b0c04e80a) Thanks [@tim-smart](https://github.com/tim-smart)! - Http.server.serve now returns a Layer + +### Patch Changes + +- [#338](https://github.com/Effect-TS/platform/pull/338) [`7eaa8e5`](https://github.com/Effect-TS/platform/commit/7eaa8e52b18d408688e7b4909bcf016b0c04e80a) Thanks [@tim-smart](https://github.com/tim-smart)! - add Http.server.serveEffect + +- Updated dependencies [[`7eaa8e5`](https://github.com/Effect-TS/platform/commit/7eaa8e52b18d408688e7b4909bcf016b0c04e80a), [`7eaa8e5`](https://github.com/Effect-TS/platform/commit/7eaa8e52b18d408688e7b4909bcf016b0c04e80a), [`7eaa8e5`](https://github.com/Effect-TS/platform/commit/7eaa8e52b18d408688e7b4909bcf016b0c04e80a)]: + - @effect/platform@0.36.0 + +## 0.35.1 + +### Patch Changes + +- [#335](https://github.com/Effect-TS/platform/pull/335) [`4f0166e`](https://github.com/Effect-TS/platform/commit/4f0166ee2241bd9b71739c98d428b5809313e46e) Thanks [@tim-smart](https://github.com/tim-smart)! - add SerializedWorker + +- Updated dependencies [[`4f0166e`](https://github.com/Effect-TS/platform/commit/4f0166ee2241bd9b71739c98d428b5809313e46e), [`4f0166e`](https://github.com/Effect-TS/platform/commit/4f0166ee2241bd9b71739c98d428b5809313e46e)]: + - @effect/platform@0.35.0 + +## 0.35.0 + +### Minor Changes + +- [#331](https://github.com/Effect-TS/platform/pull/331) [`db1ca18`](https://github.com/Effect-TS/platform/commit/db1ca18725f9dd4be1c36ddc80faa3aa53c10eb7) Thanks [@tim-smart](https://github.com/tim-smart)! - update effect + +### Patch Changes + +- Updated dependencies [[`db1ca18`](https://github.com/Effect-TS/platform/commit/db1ca18725f9dd4be1c36ddc80faa3aa53c10eb7)]: + - @effect/platform@0.34.0 + +## 0.34.3 + +### Patch Changes + +- [#329](https://github.com/Effect-TS/platform/pull/329) [`5c75749`](https://github.com/Effect-TS/platform/commit/5c75749d451f8e79e1cb8057729691e4b3c1c6aa) Thanks [@leonitousconforti](https://github.com/leonitousconforti)! - HttpClient added another error event listener in waitForResponse + +## 0.34.2 + +### Patch Changes + +- Updated dependencies [[`162aa91`](https://github.com/Effect-TS/platform/commit/162aa915934112983c543a6be2a9d7091b86fac9)]: + - @effect/platform@0.33.1 + +## 0.34.1 + +### Patch Changes + +- [#324](https://github.com/Effect-TS/platform/pull/324) [`6b90c81`](https://github.com/Effect-TS/platform/commit/6b90c81391e613a25db564aebb9a64971ce077a5) Thanks [@tim-smart](https://github.com/tim-smart)! - improve serve api + +## 0.34.0 + +### Minor Changes + +- [#321](https://github.com/Effect-TS/platform/pull/321) [`16a5bca`](https://github.com/Effect-TS/platform/commit/16a5bca2bd4aed570ce95233a0e47350010d031f) Thanks [@tim-smart](https://github.com/tim-smart)! - update effect + +### Patch Changes + +- [#319](https://github.com/Effect-TS/platform/pull/319) [`425365e`](https://github.com/Effect-TS/platform/commit/425365ebc40c52a6e2a4bff865c3a982ce74f4ed) Thanks [@IMax153](https://github.com/IMax153)! - add Terminal.readLine to read input line-by-line from the terminal + +- [#319](https://github.com/Effect-TS/platform/pull/319) [`425365e`](https://github.com/Effect-TS/platform/commit/425365ebc40c52a6e2a4bff865c3a982ce74f4ed) Thanks [@IMax153](https://github.com/IMax153)! - make Terminal.columns an Effect to account for resizing the terminal + +- Updated dependencies [[`425365e`](https://github.com/Effect-TS/platform/commit/425365ebc40c52a6e2a4bff865c3a982ce74f4ed), [`425365e`](https://github.com/Effect-TS/platform/commit/425365ebc40c52a6e2a4bff865c3a982ce74f4ed), [`16a5bca`](https://github.com/Effect-TS/platform/commit/16a5bca2bd4aed570ce95233a0e47350010d031f)]: + - @effect/platform@0.33.0 + +## 0.33.5 + +### Patch Changes + +- [#316](https://github.com/Effect-TS/platform/pull/316) [`19431f0`](https://github.com/Effect-TS/platform/commit/19431f0b5ccb8beacd502de876962f55cabf6ed4) Thanks [@tim-smart](https://github.com/tim-smart)! - add logging to runMain + +## 0.33.4 + +### Patch Changes + +- [#314](https://github.com/Effect-TS/platform/pull/314) [`e63cf81`](https://github.com/Effect-TS/platform/commit/e63cf819dc26588e29a0177afb1665aa5fd96dfd) Thanks [@tim-smart](https://github.com/tim-smart)! - refactor node command executor + +## 0.33.3 + +### Patch Changes + +- [#312](https://github.com/Effect-TS/platform/pull/312) [`cc1f588`](https://github.com/Effect-TS/platform/commit/cc1f5886bf4188e0128b64b9e2a67f789680cab0) Thanks [@tim-smart](https://github.com/tim-smart)! - scope commands to prevent process leaks + +- Updated dependencies [[`cc1f588`](https://github.com/Effect-TS/platform/commit/cc1f5886bf4188e0128b64b9e2a67f789680cab0)]: + - @effect/platform@0.32.2 + +## 0.33.2 + +### Patch Changes + +- [#310](https://github.com/Effect-TS/platform/pull/310) [`14239fb`](https://github.com/Effect-TS/platform/commit/14239fb11ae45db1a02d9ba883d0412a9c9e6343) Thanks [@tim-smart](https://github.com/tim-smart)! - update effect + +- Updated dependencies [[`14239fb`](https://github.com/Effect-TS/platform/commit/14239fb11ae45db1a02d9ba883d0412a9c9e6343)]: + - @effect/platform@0.32.1 + +## 0.33.1 + +### Patch Changes + +- [#308](https://github.com/Effect-TS/platform/pull/308) [`4da9a1b`](https://github.com/Effect-TS/platform/commit/4da9a1b73f7644561eab5d7d0d3dcc3b1b8b9b64) Thanks [@tim-smart](https://github.com/tim-smart)! - fix mime version to preserve cjs support + +## 0.33.0 + +### Minor Changes + +- [#307](https://github.com/Effect-TS/platform/pull/307) [`746f969`](https://github.com/Effect-TS/platform/commit/746f9692e2f7133dcb413e0eea08ac7b6b97a9bd) Thanks [@tim-smart](https://github.com/tim-smart)! - update effect + +### Patch Changes + +- Updated dependencies [[`746f969`](https://github.com/Effect-TS/platform/commit/746f9692e2f7133dcb413e0eea08ac7b6b97a9bd), [`92e56a1`](https://github.com/Effect-TS/platform/commit/92e56a1f844f28f26621a1887cc4da045039066d), [`92e56a1`](https://github.com/Effect-TS/platform/commit/92e56a1f844f28f26621a1887cc4da045039066d)]: + - @effect/platform@0.32.0 + +## 0.32.3 + +### Patch Changes + +- Updated dependencies [[`7a46ec6`](https://github.com/Effect-TS/platform/commit/7a46ec679e2d4718919c407d0c6c5f0fdc35e62d)]: + - @effect/platform@0.31.2 + +## 0.32.2 + +### Patch Changes + +- [#295](https://github.com/Effect-TS/platform/pull/295) [`2f1ca0c`](https://github.com/Effect-TS/platform/commit/2f1ca0cd6d39062fef5717f322cec6767f243def) Thanks [@tim-smart](https://github.com/tim-smart)! - expose node channel apis + +## 0.32.1 + +### Patch Changes + +- [#293](https://github.com/Effect-TS/platform/pull/293) [`5a7d254`](https://github.com/Effect-TS/platform/commit/5a7d25406b0841cf6ec49218bd3324a4ddc3df5b) Thanks [@tim-smart](https://github.com/tim-smart)! - ensure http methods are uppercase + +- Updated dependencies [[`b712491`](https://github.com/Effect-TS/platform/commit/b71249168eb4623de8dbd28cd0102be688f5caa3)]: + - @effect/platform@0.31.1 + +## 0.32.0 + +### Minor Changes + +- [#291](https://github.com/Effect-TS/platform/pull/291) [`5a677f1`](https://github.com/Effect-TS/platform/commit/5a677f1062d7373e21839dfa51db26beef15dca4) Thanks [@tim-smart](https://github.com/tim-smart)! - update effect + +### Patch Changes + +- [#289](https://github.com/Effect-TS/platform/pull/289) [`624855f`](https://github.com/Effect-TS/platform/commit/624855f635162b2c1232429253477d0805e02657) Thanks [@tim-smart](https://github.com/tim-smart)! - update deps + +- Updated dependencies [[`5a677f1`](https://github.com/Effect-TS/platform/commit/5a677f1062d7373e21839dfa51db26beef15dca4), [`624855f`](https://github.com/Effect-TS/platform/commit/624855f635162b2c1232429253477d0805e02657)]: + - @effect/platform@0.31.0 + +## 0.31.9 + +### Patch Changes + +- Updated dependencies [[`d5d0932`](https://github.com/Effect-TS/platform/commit/d5d093219cde4f51afb9251d9ba4270fc70be0c1)]: + - @effect/platform@0.30.6 + +## 0.31.8 + +### Patch Changes + +- [#285](https://github.com/Effect-TS/platform/pull/285) [`a13377b`](https://github.com/Effect-TS/platform/commit/a13377b21b1369947f76d1719dd0b4acc5c64086) Thanks [@IMax153](https://github.com/IMax153)! - avoid mutating global state with Terminal service + +## 0.31.7 + +### Patch Changes + +- [#283](https://github.com/Effect-TS/platform/pull/283) [`efd464b`](https://github.com/Effect-TS/platform/commit/efd464bd0b16bb6bf3bb7507f9da835b380fb1a2) Thanks [@tim-smart](https://github.com/tim-smart)! - add WorkerManager to Node/BunContext + +- [#283](https://github.com/Effect-TS/platform/pull/283) [`efd464b`](https://github.com/Effect-TS/platform/commit/efd464bd0b16bb6bf3bb7507f9da835b380fb1a2) Thanks [@tim-smart](https://github.com/tim-smart)! - remove Terminal from Node/BunContext + +## 0.31.6 + +### Patch Changes + +- [#282](https://github.com/Effect-TS/platform/pull/282) [`534cb34`](https://github.com/Effect-TS/platform/commit/534cb3486b55e08f9c9cb3f0d955b04da128986c) Thanks [@IMax153](https://github.com/IMax153)! - add Terminal to NodeContext and BunContext + +- [#282](https://github.com/Effect-TS/platform/pull/282) [`534cb34`](https://github.com/Effect-TS/platform/commit/534cb3486b55e08f9c9cb3f0d955b04da128986c) Thanks [@IMax153](https://github.com/IMax153)! - fix bug where keypress could still be emit after terminal was quit + +- [#280](https://github.com/Effect-TS/platform/pull/280) [`d8e2234`](https://github.com/Effect-TS/platform/commit/d8e2234bc2fa0794e2a4b6a693ae1e7c1836bfb8) Thanks [@mikearnaldi](https://github.com/mikearnaldi)! - Recursive interrupt all fibers on kill + +## 0.31.5 + +### Patch Changes + +- Updated dependencies [[`36e449c`](https://github.com/Effect-TS/platform/commit/36e449c95fab80dc54505cef2071dcbecce35b4f)]: + - @effect/platform@0.30.5 + +## 0.31.4 + +### Patch Changes + +- [#275](https://github.com/Effect-TS/platform/pull/275) [`e28989e`](https://github.com/Effect-TS/platform/commit/e28989ebd1813cec7ce68f7dd8718f2254e05cad) Thanks [@tim-smart](https://github.com/tim-smart)! - add stack to WorkerError + +- Updated dependencies [[`e28989e`](https://github.com/Effect-TS/platform/commit/e28989ebd1813cec7ce68f7dd8718f2254e05cad)]: + - @effect/platform@0.30.4 + +## 0.31.3 + +### Patch Changes + +- [#272](https://github.com/Effect-TS/platform/pull/272) [`1a055ac`](https://github.com/Effect-TS/platform/commit/1a055ac959faf12e9c57768b20babea12b1f7d2d) Thanks [@tim-smart](https://github.com/tim-smart)! - add WorkerError to send api + +- Updated dependencies [[`1a055ac`](https://github.com/Effect-TS/platform/commit/1a055ac959faf12e9c57768b20babea12b1f7d2d)]: + - @effect/platform@0.30.3 + +## 0.31.2 + +### Patch Changes + +- [#270](https://github.com/Effect-TS/platform/pull/270) [`3257fd5`](https://github.com/Effect-TS/platform/commit/3257fd52016af5a38c135de5f0aa33aac7de2538) Thanks [@tim-smart](https://github.com/tim-smart)! - update multipasta + +- Updated dependencies [[`3257fd5`](https://github.com/Effect-TS/platform/commit/3257fd52016af5a38c135de5f0aa33aac7de2538)]: + - @effect/platform@0.30.2 + +## 0.31.1 + +### Patch Changes + +- [#268](https://github.com/Effect-TS/platform/pull/268) [`58f5ccc`](https://github.com/Effect-TS/platform/commit/58f5ccc07d74abe6820debc0179665e5ef76b5c4) Thanks [@tim-smart](https://github.com/tim-smart)! - update deps + +- Updated dependencies [[`58f5ccc`](https://github.com/Effect-TS/platform/commit/58f5ccc07d74abe6820debc0179665e5ef76b5c4)]: + - @effect/platform@0.30.1 + +## 0.31.0 + +### Minor Changes + +- [#267](https://github.com/Effect-TS/platform/pull/267) [`3d38b40`](https://github.com/Effect-TS/platform/commit/3d38b40a939e32c6c0e8b62dd53a844a6f389182) Thanks [@tim-smart](https://github.com/tim-smart)! - update effect + +### Patch Changes + +- Updated dependencies [[`3d38b40`](https://github.com/Effect-TS/platform/commit/3d38b40a939e32c6c0e8b62dd53a844a6f389182)]: + - @effect/platform@0.30.0 + +## 0.30.1 + +### Patch Changes + +- [#263](https://github.com/Effect-TS/platform/pull/263) [`2bbe692`](https://github.com/Effect-TS/platform/commit/2bbe6928aa5e6929e58877ba236547310bca7e2b) Thanks [@tim-smart](https://github.com/tim-smart)! - fix fieldMimeTypes fiber ref + +- Updated dependencies [[`2bbe692`](https://github.com/Effect-TS/platform/commit/2bbe6928aa5e6929e58877ba236547310bca7e2b)]: + - @effect/platform@0.29.1 + +## 0.30.0 + +### Minor Changes + +- [#250](https://github.com/Effect-TS/platform/pull/250) [`6e18090`](https://github.com/Effect-TS/platform/commit/6e18090db4686cd5564ab9dc3d8771d7b3ad97fa) Thanks [@tim-smart](https://github.com/tim-smart)! - updated FormData model and apis + +### Patch Changes + +- Updated dependencies [[`6e18090`](https://github.com/Effect-TS/platform/commit/6e18090db4686cd5564ab9dc3d8771d7b3ad97fa)]: + - @effect/platform@0.29.0 + +## 0.29.4 + +### Patch Changes + +- [#260](https://github.com/Effect-TS/platform/pull/260) [`8f5e6a2`](https://github.com/Effect-TS/platform/commit/8f5e6a2f2ced4408b0b311b0456828855e1cb958) Thanks [@IMax153](https://github.com/IMax153)! - expose available terminal columns from the Terminal service + +- Updated dependencies [[`8f5e6a2`](https://github.com/Effect-TS/platform/commit/8f5e6a2f2ced4408b0b311b0456828855e1cb958)]: + - @effect/platform@0.28.4 + +## 0.29.3 + +### Patch Changes + +- Updated dependencies [[`9f79c1f`](https://github.com/Effect-TS/platform/commit/9f79c1f5278e60b3bcbd59f08e20189bcb25a84e)]: + - @effect/platform@0.28.3 + +## 0.29.2 + +### Patch Changes + +- [#256](https://github.com/Effect-TS/platform/pull/256) [`62cbddb`](https://github.com/Effect-TS/platform/commit/62cbddb530371291123dea220bfebcc0521b54df) Thanks [@jessekelly881](https://github.com/jessekelly881)! - fix: added missing File type export + +- [#255](https://github.com/Effect-TS/platform/pull/255) [`fea76da`](https://github.com/Effect-TS/platform/commit/fea76da05190a65912911bd5b6f9cc0bef3b2edc) Thanks [@IMax153](https://github.com/IMax153)! - add basic Terminal interface for prompting user input + +- Updated dependencies [[`fea76da`](https://github.com/Effect-TS/platform/commit/fea76da05190a65912911bd5b6f9cc0bef3b2edc)]: + - @effect/platform@0.28.2 + +## 0.29.1 + +### Patch Changes + +- [#253](https://github.com/Effect-TS/platform/pull/253) [`43d2e29`](https://github.com/Effect-TS/platform/commit/43d2e2984fe88b39e907f45f089206ed88ad52d1) Thanks [@fubhy](https://github.com/fubhy)! - Update dependencies + +- Updated dependencies [[`43d2e29`](https://github.com/Effect-TS/platform/commit/43d2e2984fe88b39e907f45f089206ed88ad52d1)]: + - @effect/platform@0.28.1 + +## 0.29.0 + +### Minor Changes + +- [#251](https://github.com/Effect-TS/platform/pull/251) [`05fef78`](https://github.com/Effect-TS/platform/commit/05fef784ac975059fb6335576feadc7f34644314) Thanks [@fubhy](https://github.com/fubhy)! - Re-added exports for http module + +### Patch Changes + +- Updated dependencies [[`05fef78`](https://github.com/Effect-TS/platform/commit/05fef784ac975059fb6335576feadc7f34644314)]: + - @effect/platform@0.28.0 + +## 0.28.4 + +### Patch Changes + +- [#248](https://github.com/Effect-TS/platform/pull/248) [`8a4b1c1`](https://github.com/Effect-TS/platform/commit/8a4b1c14808d9815eb93a5b10d8a5b26c4dd027b) Thanks [@IMax153](https://github.com/IMax153)! - allow for specifying that a Command should be run in a shell + +- Updated dependencies [[`8a4b1c1`](https://github.com/Effect-TS/platform/commit/8a4b1c14808d9815eb93a5b10d8a5b26c4dd027b)]: + - @effect/platform@0.27.4 + +## 0.28.3 + +### Patch Changes + +- [#243](https://github.com/Effect-TS/platform/pull/243) [`1ac0a42`](https://github.com/Effect-TS/platform/commit/1ac0a4208184ef1d23d5ad41a7f0e32bc4d80d85) Thanks [@tim-smart](https://github.com/tim-smart)! - fix worker interruption + +- Updated dependencies [[`1ac0a42`](https://github.com/Effect-TS/platform/commit/1ac0a4208184ef1d23d5ad41a7f0e32bc4d80d85)]: + - @effect/platform@0.27.3 + +## 0.28.2 + +### Patch Changes + +- Updated dependencies [[`e2aa7cd`](https://github.com/Effect-TS/platform/commit/e2aa7cd606a735809fbf79327cfebc009e89d84d)]: + - @effect/platform@0.27.2 + +## 0.28.1 + +### Patch Changes + +- [#239](https://github.com/Effect-TS/platform/pull/239) [`4d94b9d`](https://github.com/Effect-TS/platform/commit/4d94b9d30adba2bf4f6f6e1d4cd735e6362667c5) Thanks [@tim-smart](https://github.com/tim-smart)! - update effect + +- Updated dependencies [[`4d94b9d`](https://github.com/Effect-TS/platform/commit/4d94b9d30adba2bf4f6f6e1d4cd735e6362667c5)]: + - @effect/platform@0.27.1 + +## 0.28.0 + +### Minor Changes + +- [#237](https://github.com/Effect-TS/platform/pull/237) [`1f79ed6`](https://github.com/Effect-TS/platform/commit/1f79ed6b4d2ee9ae2b59c4536854566c579e77c4) Thanks [@tim-smart](https://github.com/tim-smart)! - update effect + +### Patch Changes + +- Updated dependencies [[`1f79ed6`](https://github.com/Effect-TS/platform/commit/1f79ed6b4d2ee9ae2b59c4536854566c579e77c4)]: + - @effect/platform@0.27.0 + +## 0.27.9 + +### Patch Changes + +- [#235](https://github.com/Effect-TS/platform/pull/235) [`6e14c02`](https://github.com/Effect-TS/platform/commit/6e14c02db668f380bb92f19037685fe40592a8fe) Thanks [@tim-smart](https://github.com/tim-smart)! - fix for hanging worker shutdown + +- Updated dependencies [[`6e14c02`](https://github.com/Effect-TS/platform/commit/6e14c02db668f380bb92f19037685fe40592a8fe)]: + - @effect/platform@0.26.7 + +## 0.27.8 + +### Patch Changes + +- [#233](https://github.com/Effect-TS/platform/pull/233) [`71947e0`](https://github.com/Effect-TS/platform/commit/71947e0e0aa9dccf9aad6f63dd98a6b6c89f23b4) Thanks [@tim-smart](https://github.com/tim-smart)! - fix worker scope hanging on close + +- Updated dependencies [[`71947e0`](https://github.com/Effect-TS/platform/commit/71947e0e0aa9dccf9aad6f63dd98a6b6c89f23b4)]: + - @effect/platform@0.26.6 + +## 0.27.7 + +### Patch Changes + +- [#231](https://github.com/Effect-TS/platform/pull/231) [`a3cbba4`](https://github.com/Effect-TS/platform/commit/a3cbba4a0fa0f1ef99a6d7e54f5ab46c6813ef00) Thanks [@tim-smart](https://github.com/tim-smart)! - add onCreate and broadcast to pool options + +- Updated dependencies [[`a3cbba4`](https://github.com/Effect-TS/platform/commit/a3cbba4a0fa0f1ef99a6d7e54f5ab46c6813ef00)]: + - @effect/platform@0.26.5 + +## 0.27.6 + +### Patch Changes + +- [#229](https://github.com/Effect-TS/platform/pull/229) [`4661a8c`](https://github.com/Effect-TS/platform/commit/4661a8c63a13cc6630d5f3cbac90f4ff1d096e09) Thanks [@tim-smart](https://github.com/tim-smart)! - type worker runner success as never + +- [#229](https://github.com/Effect-TS/platform/pull/229) [`4661a8c`](https://github.com/Effect-TS/platform/commit/4661a8c63a13cc6630d5f3cbac90f4ff1d096e09) Thanks [@tim-smart](https://github.com/tim-smart)! - disable worker pool scaling + +- Updated dependencies [[`4661a8c`](https://github.com/Effect-TS/platform/commit/4661a8c63a13cc6630d5f3cbac90f4ff1d096e09), [`4661a8c`](https://github.com/Effect-TS/platform/commit/4661a8c63a13cc6630d5f3cbac90f4ff1d096e09)]: + - @effect/platform@0.26.4 + +## 0.27.5 + +### Patch Changes + +- Updated dependencies [[`abb6baa`](https://github.com/Effect-TS/platform/commit/abb6baa61346580f97d2ab91b84a7342b5becc60)]: + - @effect/platform@0.26.3 + +## 0.27.4 + +### Patch Changes + +- [#223](https://github.com/Effect-TS/platform/pull/223) [`3ab8299`](https://github.com/Effect-TS/platform/commit/3ab82991a21e15b4b7f5e53bc2d6e5a807f23698) Thanks [@tim-smart](https://github.com/tim-smart)! - add makeHandler api for node http server + +- [#223](https://github.com/Effect-TS/platform/pull/223) [`3ab8299`](https://github.com/Effect-TS/platform/commit/3ab82991a21e15b4b7f5e53bc2d6e5a807f23698) Thanks [@tim-smart](https://github.com/tim-smart)! - add apis to access underlying http request source + +## 0.27.3 + +### Patch Changes + +- [#221](https://github.com/Effect-TS/platform/pull/221) [`3e57e82`](https://github.com/Effect-TS/platform/commit/3e57e8224bf7b4474b21ef1dc25db13107d9b635) Thanks [@tim-smart](https://github.com/tim-smart)! - export WorkerRunner layers + +## 0.27.2 + +### Patch Changes + +- Updated dependencies [[`f37f58c`](https://github.com/Effect-TS/platform/commit/f37f58ca21c1d5dfedc40c01cde0ffbc954d7e32)]: + - @effect/platform@0.26.2 + +## 0.27.1 + +### Patch Changes + +- Updated dependencies [[`7471ac1`](https://github.com/Effect-TS/platform/commit/7471ac139f3c6867cd0d228ec54e88abd1384f5c)]: + - @effect/platform@0.26.1 + +## 0.27.0 + +### Minor Changes + +- [#215](https://github.com/Effect-TS/platform/pull/215) [`59da2a6`](https://github.com/Effect-TS/platform/commit/59da2a6877e219b2ca0433aeeecab4ad7487816b) Thanks [@tim-smart](https://github.com/tim-smart)! - seperate request processing in http client + +### Patch Changes + +- Updated dependencies [[`59da2a6`](https://github.com/Effect-TS/platform/commit/59da2a6877e219b2ca0433aeeecab4ad7487816b)]: + - @effect/platform@0.26.0 + +## 0.26.1 + +### Patch Changes + +- [#213](https://github.com/Effect-TS/platform/pull/213) [`38a49eb`](https://github.com/Effect-TS/platform/commit/38a49eb6ea99ef773007a98ec262898207c8f3c7) Thanks [@tim-smart](https://github.com/tim-smart)! - update effect + +- Updated dependencies [[`38a49eb`](https://github.com/Effect-TS/platform/commit/38a49eb6ea99ef773007a98ec262898207c8f3c7)]: + - @effect/platform@0.25.1 + +## 0.26.0 + +### Minor Changes + +- [#211](https://github.com/Effect-TS/platform/pull/211) [`9ec45cb`](https://github.com/Effect-TS/platform/commit/9ec45cba6b7d5016079ccad9357934f12afe8750) Thanks [@tim-smart](https://github.com/tim-smart)! - update effect + +### Patch Changes + +- Updated dependencies [[`9ec45cb`](https://github.com/Effect-TS/platform/commit/9ec45cba6b7d5016079ccad9357934f12afe8750)]: + - @effect/platform@0.25.0 + +## 0.25.0 + +### Minor Changes + +- [#209](https://github.com/Effect-TS/platform/pull/209) [`9c51aa1`](https://github.com/Effect-TS/platform/commit/9c51aa18beb7fd34023863ca069d3dde372765d8) Thanks [@tim-smart](https://github.com/tim-smart)! - update effect + +### Patch Changes + +- Updated dependencies [[`9c51aa1`](https://github.com/Effect-TS/platform/commit/9c51aa18beb7fd34023863ca069d3dde372765d8)]: + - @effect/platform@0.24.0 + +## 0.24.1 + +### Patch Changes + +- [#206](https://github.com/Effect-TS/platform/pull/206) [`b47639b`](https://github.com/Effect-TS/platform/commit/b47639b1df021beb075469921e9ef7a08c174555) Thanks [@tim-smart](https://github.com/tim-smart)! - small stream improvements + +- [#208](https://github.com/Effect-TS/platform/pull/208) [`41f8a65`](https://github.com/Effect-TS/platform/commit/41f8a650238bfbac5b8e18d58a431c3605b71aa5) Thanks [@tim-smart](https://github.com/tim-smart)! - add Http.middleware.withLoggerDisabled + +- Updated dependencies [[`b47639b`](https://github.com/Effect-TS/platform/commit/b47639b1df021beb075469921e9ef7a08c174555), [`41f8a65`](https://github.com/Effect-TS/platform/commit/41f8a650238bfbac5b8e18d58a431c3605b71aa5)]: + - @effect/platform@0.23.1 + +## 0.24.0 + +### Minor Changes + +- [#204](https://github.com/Effect-TS/platform/pull/204) [`ee0c08f`](https://github.com/Effect-TS/platform/commit/ee0c08fd9828eae32696da1bde33d50a3ad9edf3) Thanks [@tim-smart](https://github.com/tim-smart)! - update effect + +### Patch Changes + +- Updated dependencies [[`ee0c08f`](https://github.com/Effect-TS/platform/commit/ee0c08fd9828eae32696da1bde33d50a3ad9edf3)]: + - @effect/platform@0.23.0 + +## 0.23.2 + +### Patch Changes + +- [#194](https://github.com/Effect-TS/platform/pull/194) [`79b71d8`](https://github.com/Effect-TS/platform/commit/79b71d8cb3aa6520b2dcb7930850b423174e04b2) Thanks [@tim-smart](https://github.com/tim-smart)! - add Worker & WorkerRunner modules + +- Updated dependencies [[`79b71d8`](https://github.com/Effect-TS/platform/commit/79b71d8cb3aa6520b2dcb7930850b423174e04b2)]: + - @effect/platform@0.22.1 + +## 0.23.1 + +### Patch Changes + +- [#200](https://github.com/Effect-TS/platform/pull/200) [`58a002a`](https://github.com/Effect-TS/platform/commit/58a002acaafdb31e601c0de1878f4a8dee723e13) Thanks [@tim-smart](https://github.com/tim-smart)! - fix hanging node Sink fromWritable + +## 0.23.0 + +### Minor Changes + +- [#197](https://github.com/Effect-TS/platform/pull/197) [`fcc5871`](https://github.com/Effect-TS/platform/commit/fcc5871d326296334ff9a421860d69e697eea559) Thanks [@tim-smart](https://github.com/tim-smart)! - update api for Stream.toString & toUint8Array + +- [#199](https://github.com/Effect-TS/platform/pull/199) [`1e94b15`](https://github.com/Effect-TS/platform/commit/1e94b1588e51df20f9c4fc4871b246048751506c) Thanks [@tim-smart](https://github.com/tim-smart)! - enable tracing by default + +### Patch Changes + +- [#197](https://github.com/Effect-TS/platform/pull/197) [`fcc5871`](https://github.com/Effect-TS/platform/commit/fcc5871d326296334ff9a421860d69e697eea559) Thanks [@tim-smart](https://github.com/tim-smart)! - fix for non-compliant node steams + +- [#197](https://github.com/Effect-TS/platform/pull/197) [`fcc5871`](https://github.com/Effect-TS/platform/commit/fcc5871d326296334ff9a421860d69e697eea559) Thanks [@tim-smart](https://github.com/tim-smart)! - accept NodeJS.\*Stream in Stream & Sink modules + +- Updated dependencies [[`1e94b15`](https://github.com/Effect-TS/platform/commit/1e94b1588e51df20f9c4fc4871b246048751506c)]: + - @effect/platform@0.22.0 + +## 0.22.1 + +### Patch Changes + +- [#195](https://github.com/Effect-TS/platform/pull/195) [`25ce726`](https://github.com/Effect-TS/platform/commit/25ce72656a9addbb1f4d539ea69423b73fe43f46) Thanks [@tim-smart](https://github.com/tim-smart)! - add Stream.fromDuplex & pipeThroughDuplex/Simple + +## 0.22.0 + +### Minor Changes + +- [#193](https://github.com/Effect-TS/platform/pull/193) [`9ec4b1d`](https://github.com/Effect-TS/platform/commit/9ec4b1d284caa1c4f19a58c46ed7c25fb10d39a5) Thanks [@tim-smart](https://github.com/tim-smart)! - update effect + +### Patch Changes + +- [#191](https://github.com/Effect-TS/platform/pull/191) [`2711aea`](https://github.com/Effect-TS/platform/commit/2711aea855936c82c282e61fbc6d2f1a1ab6778a) Thanks [@tim-smart](https://github.com/tim-smart)! - update effect + +- Updated dependencies [[`2711aea`](https://github.com/Effect-TS/platform/commit/2711aea855936c82c282e61fbc6d2f1a1ab6778a), [`9ec4b1d`](https://github.com/Effect-TS/platform/commit/9ec4b1d284caa1c4f19a58c46ed7c25fb10d39a5)]: + - @effect/platform@0.21.0 + +## 0.21.0 + +### Minor Changes + +- [#189](https://github.com/Effect-TS/platform/pull/189) [`b07f8cd`](https://github.com/Effect-TS/platform/commit/b07f8cd50ef44d577aa981a532025aedb364df13) Thanks [@tim-smart](https://github.com/tim-smart)! - update effect + +### Patch Changes + +- Updated dependencies [[`b07f8cd`](https://github.com/Effect-TS/platform/commit/b07f8cd50ef44d577aa981a532025aedb364df13)]: + - @effect/platform@0.20.0 + +## 0.20.1 + +### Patch Changes + +- [#187](https://github.com/Effect-TS/platform/pull/187) [`26e05da`](https://github.com/Effect-TS/platform/commit/26e05dad112fa43403b23ebc815a98f0c95e0558) Thanks [@tim-smart](https://github.com/tim-smart)! - fix order of pre response access + +## 0.20.0 + +### Minor Changes + +- [#184](https://github.com/Effect-TS/platform/pull/184) [`903b599`](https://github.com/Effect-TS/platform/commit/903b5995bb407c399846e6b75e47e53098b2c80d) Thanks [@tim-smart](https://github.com/tim-smart)! - update effect + +### Patch Changes + +- [#186](https://github.com/Effect-TS/platform/pull/186) [`a3bcda4`](https://github.com/Effect-TS/platform/commit/a3bcda4c2c6655ab86769cca60bece5eb64f866e) Thanks [@tim-smart](https://github.com/tim-smart)! - add pre response handlers to http + +- Updated dependencies [[`903b599`](https://github.com/Effect-TS/platform/commit/903b5995bb407c399846e6b75e47e53098b2c80d), [`a3bcda4`](https://github.com/Effect-TS/platform/commit/a3bcda4c2c6655ab86769cca60bece5eb64f866e)]: + - @effect/platform@0.19.0 + +## 0.19.9 + +### Patch Changes + +- [#181](https://github.com/Effect-TS/platform/pull/181) [`d0d5458`](https://github.com/Effect-TS/platform/commit/d0d545869baeb91d594804ab759713f424eb7a11) Thanks [@tim-smart](https://github.com/tim-smart)! - fix error type exports + +## 0.19.8 + +### Patch Changes + +- [#179](https://github.com/Effect-TS/platform/pull/179) [`843488f`](https://github.com/Effect-TS/platform/commit/843488f79b253518f131693faf2955f5c795a1bc) Thanks [@tim-smart](https://github.com/tim-smart)! - update effect + +- Updated dependencies [[`843488f`](https://github.com/Effect-TS/platform/commit/843488f79b253518f131693faf2955f5c795a1bc)]: + - @effect/platform@0.18.7 + +## 0.19.7 + +### Patch Changes + +- [#174](https://github.com/Effect-TS/platform/pull/174) [`6116f8b`](https://github.com/Effect-TS/platform/commit/6116f8b39533c897445713c7dce531c0c60a1cbb) Thanks [@vecerek](https://github.com/vecerek)! - handle empty body when parsing responses as json + +## 0.19.6 + +### Patch Changes + +- [#175](https://github.com/Effect-TS/platform/pull/175) [`d1c2b38`](https://github.com/Effect-TS/platform/commit/d1c2b38cbb1189249c0bfd47582e00ff771428e3) Thanks [@tim-smart](https://github.com/tim-smart)! - make ServerResponse an Effect + +- Updated dependencies [[`7e4e2a5`](https://github.com/Effect-TS/platform/commit/7e4e2a5d815c677e4eb6adb2c6e9369414a79384), [`d1c2b38`](https://github.com/Effect-TS/platform/commit/d1c2b38cbb1189249c0bfd47582e00ff771428e3)]: + - @effect/platform@0.18.6 + +## 0.19.5 + +### Patch Changes + +- [#171](https://github.com/Effect-TS/platform/pull/171) [`fbbcaa9`](https://github.com/Effect-TS/platform/commit/fbbcaa9b1d4f48f204072a802fb11bcb29813664) Thanks [@tim-smart](https://github.com/tim-smart)! - remove preserveModules patch for preconstruct + +- Updated dependencies [[`fbbcaa9`](https://github.com/Effect-TS/platform/commit/fbbcaa9b1d4f48f204072a802fb11bcb29813664)]: + - @effect/platform@0.18.5 + +## 0.19.4 + +### Patch Changes + +- [#169](https://github.com/Effect-TS/platform/pull/169) [`bd8778d`](https://github.com/Effect-TS/platform/commit/bd8778d1a534f28cab4b326bb25c086fafed8101) Thanks [@tim-smart](https://github.com/tim-smart)! - fix nested modules + +- Updated dependencies [[`bd8778d`](https://github.com/Effect-TS/platform/commit/bd8778d1a534f28cab4b326bb25c086fafed8101)]: + - @effect/platform@0.18.4 + +## 0.19.3 + +### Patch Changes + +- [#167](https://github.com/Effect-TS/platform/pull/167) [`7027589`](https://github.com/Effect-TS/platform/commit/7027589d6dde621065eb8834a2b1ba4d3adc943b) Thanks [@tim-smart](https://github.com/tim-smart)! - build with preconstruct + +- Updated dependencies [[`7027589`](https://github.com/Effect-TS/platform/commit/7027589d6dde621065eb8834a2b1ba4d3adc943b)]: + - @effect/platform@0.18.3 + +## 0.19.2 + +### Patch Changes + +- [#165](https://github.com/Effect-TS/platform/pull/165) [`7e3a741`](https://github.com/Effect-TS/platform/commit/7e3a74197325566df47f9b4459e518eea0762d13) Thanks [@fubhy](https://github.com/fubhy)! - Fix peer deps version range + +- Updated dependencies [[`7e3a741`](https://github.com/Effect-TS/platform/commit/7e3a74197325566df47f9b4459e518eea0762d13)]: + - @effect/platform@0.18.2 + +## 0.19.1 + +### Patch Changes + +- [#163](https://github.com/Effect-TS/platform/pull/163) [`c957232`](https://github.com/Effect-TS/platform/commit/c9572328ee37f44e93e933da622b21df414bf5c6) Thanks [@tim-smart](https://github.com/tim-smart)! - update effect + +- Updated dependencies [[`c957232`](https://github.com/Effect-TS/platform/commit/c9572328ee37f44e93e933da622b21df414bf5c6)]: + - @effect/platform@0.18.1 + +## 0.19.0 + +### Minor Changes + +- [#160](https://github.com/Effect-TS/platform/pull/160) [`c2dc0ab`](https://github.com/Effect-TS/platform/commit/c2dc0abb20b073fd19e38b4e61a08b1edee0f37f) Thanks [@fubhy](https://github.com/fubhy)! - update to effect package + +### Patch Changes + +- Updated dependencies [[`c2dc0ab`](https://github.com/Effect-TS/platform/commit/c2dc0abb20b073fd19e38b4e61a08b1edee0f37f)]: + - @effect/platform@0.18.0 + +## 0.18.1 + +### Patch Changes + +- Updated dependencies [[`9b10bf3`](https://github.com/Effect-TS/platform/commit/9b10bf394106ba0bafd8440dc0b3fba30a5cc1ea)]: + - @effect/platform@0.17.1 + +## 0.18.0 + +### Minor Changes + +- [#156](https://github.com/Effect-TS/platform/pull/156) [`e6c4101`](https://github.com/Effect-TS/platform/commit/e6c41011e5420d90c543dd25d87036d4150f3e85) Thanks [@tim-smart](https://github.com/tim-smart)! - update dependencies + +### Patch Changes + +- Updated dependencies [[`e6c4101`](https://github.com/Effect-TS/platform/commit/e6c41011e5420d90c543dd25d87036d4150f3e85)]: + - @effect/platform@0.17.0 + +## 0.17.0 + +### Minor Changes + +- [#153](https://github.com/Effect-TS/platform/pull/153) [`be1b6f0`](https://github.com/Effect-TS/platform/commit/be1b6f036246713a55462ec76e8d999eae654cd7) Thanks [@IMax153](https://github.com/IMax153)! - make platform-node an explicit dependency of platform-bun + +- [#155](https://github.com/Effect-TS/platform/pull/155) [`937b9e5`](https://github.com/Effect-TS/platform/commit/937b9e5c00f80bea128f21c7f5bfa662ba1d45bd) Thanks [@tim-smart](https://github.com/tim-smart)! - use direct deps in sibling packages + +## 0.16.2 + +### Patch Changes + +- [#151](https://github.com/Effect-TS/platform/pull/151) [`ea877f8`](https://github.com/Effect-TS/platform/commit/ea877f8948a43a394658abf8b781a56a097624e9) Thanks [@tim-smart](https://github.com/tim-smart)! - fix exitCode for already exited processes + +## 0.16.1 + +### Patch Changes + +- [#148](https://github.com/Effect-TS/platform/pull/148) [`492f0e7`](https://github.com/Effect-TS/platform/commit/492f0e700e939ded6ff17eeca4d50a9e1ce59219) Thanks [@tim-smart](https://github.com/tim-smart)! - add IncomingMessage.remoteAddress + +- Updated dependencies [[`492f0e7`](https://github.com/Effect-TS/platform/commit/492f0e700e939ded6ff17eeca4d50a9e1ce59219)]: + - @effect/platform@0.16.1 + +## 0.16.0 + +### Minor Changes + +- [#145](https://github.com/Effect-TS/platform/pull/145) [`d0522be`](https://github.com/Effect-TS/platform/commit/d0522be6f824571d83be8c6aa16a3d7caa1b3447) Thanks [@tim-smart](https://github.com/tim-smart)! - update dependencies + +### Patch Changes + +- [#144](https://github.com/Effect-TS/platform/pull/144) [`6583ad4`](https://github.com/Effect-TS/platform/commit/6583ad4ef5b718620c873208bb11196d35733034) Thanks [@tim-smart](https://github.com/tim-smart)! - b3 header propagation in http client and server + +- Updated dependencies [[`d0522be`](https://github.com/Effect-TS/platform/commit/d0522be6f824571d83be8c6aa16a3d7caa1b3447), [`6583ad4`](https://github.com/Effect-TS/platform/commit/6583ad4ef5b718620c873208bb11196d35733034)]: + - @effect/platform@0.16.0 + +## 0.15.2 + +### Patch Changes + +- [#142](https://github.com/Effect-TS/platform/pull/142) [`8571c36`](https://github.com/Effect-TS/platform/commit/8571c36f1f8a6ab36b23ee26922cf58def15196e) Thanks [@tim-smart](https://github.com/tim-smart)! - fix mime package import + +- Updated dependencies [[`06e27ce`](https://github.com/Effect-TS/platform/commit/06e27ce29553ea8d0a234b941fa1de1a51996fbf)]: + - @effect/platform@0.15.2 + +## 0.15.1 + +### Patch Changes + +- Updated dependencies [[`2b2f658`](https://github.com/Effect-TS/platform/commit/2b2f6583a7e589a4c7ab8c22bec390ef755f54c3)]: + - @effect/platform@0.15.1 + +## 0.15.0 + +### Minor Changes + +- [#135](https://github.com/Effect-TS/platform/pull/135) [`99f2a49`](https://github.com/Effect-TS/platform/commit/99f2a49c614a5b80646f6600a170609fe7e38025) Thanks [@tim-smart](https://github.com/tim-smart)! - update dependencies + +### Patch Changes + +- Updated dependencies [[`99f2a49`](https://github.com/Effect-TS/platform/commit/99f2a49c614a5b80646f6600a170609fe7e38025)]: + - @effect/platform@0.15.0 + +## 0.14.1 + +### Patch Changes + +- [#133](https://github.com/Effect-TS/platform/pull/133) [`1d2c403`](https://github.com/Effect-TS/platform/commit/1d2c4033af11f18ba09f53dcfdf8b3fc399bd22f) Thanks [@tim-smart](https://github.com/tim-smart)! - add http platform abstraction + +- [#133](https://github.com/Effect-TS/platform/pull/133) [`1d2c403`](https://github.com/Effect-TS/platform/commit/1d2c4033af11f18ba09f53dcfdf8b3fc399bd22f) Thanks [@tim-smart](https://github.com/tim-smart)! - handle HEAD requests + +- Updated dependencies [[`1d2c403`](https://github.com/Effect-TS/platform/commit/1d2c4033af11f18ba09f53dcfdf8b3fc399bd22f), [`1d2c403`](https://github.com/Effect-TS/platform/commit/1d2c4033af11f18ba09f53dcfdf8b3fc399bd22f)]: + - @effect/platform@0.14.1 + +## 0.14.0 + +### Minor Changes + +- [#130](https://github.com/Effect-TS/platform/pull/130) [`2713c4f`](https://github.com/Effect-TS/platform/commit/2713c4f766f5493303221772368710a09033658d) Thanks [@tim-smart](https://github.com/tim-smart)! - update dependencies + +### Patch Changes + +- Updated dependencies [[`2713c4f`](https://github.com/Effect-TS/platform/commit/2713c4f766f5493303221772368710a09033658d)]: + - @effect/platform@0.14.0 + +## 0.13.18 + +### Patch Changes + +- [#127](https://github.com/Effect-TS/platform/pull/127) [`12fbbe9`](https://github.com/Effect-TS/platform/commit/12fbbe9366e3a07895326614ec911ff2601138b1) Thanks [@tim-smart](https://github.com/tim-smart)! - interrupt http app if request is aborted + +## 0.13.17 + +### Patch Changes + +- [#125](https://github.com/Effect-TS/platform/pull/125) [`eb54e53`](https://github.com/Effect-TS/platform/commit/eb54e53d95e7b863d8ffdff9de12b0abd462b217) Thanks [@tim-smart](https://github.com/tim-smart)! - restruture platform-node for platform-bun reuse + +- Updated dependencies [[`eb54e53`](https://github.com/Effect-TS/platform/commit/eb54e53d95e7b863d8ffdff9de12b0abd462b217)]: + - @effect/platform@0.13.16 + +## 0.13.16 + +### Patch Changes + +- Updated dependencies [[`07089a8`](https://github.com/Effect-TS/platform/commit/07089a877fd72b2c1b30016f92af162bbb6ff2c8)]: + - @effect/platform@0.13.15 + +## 0.13.15 + +### Patch Changes + +- [#121](https://github.com/Effect-TS/platform/pull/121) [`1b8498f`](https://github.com/Effect-TS/platform/commit/1b8498f9dee68f2a2e93ec10c62b632c65e0017a) Thanks [@tim-smart](https://github.com/tim-smart)! - export KeyValueStore in platform-node + +## 0.13.14 + +### Patch Changes + +- [#120](https://github.com/Effect-TS/platform/pull/120) [`9cda8c9`](https://github.com/Effect-TS/platform/commit/9cda8c9ce78d5a9c841a828df20401a0dc07b747) Thanks [@tim-smart](https://github.com/tim-smart)! - add KeyValueStore.SchemaStore + +- [#111](https://github.com/Effect-TS/platform/pull/111) [`6e96703`](https://github.com/Effect-TS/platform/commit/6e96703186f38bd481bffa906e0f99dee89b8e7e) Thanks [@jessekelly881](https://github.com/jessekelly881)! - add KeyValueStore module + +- [#120](https://github.com/Effect-TS/platform/pull/120) [`9cda8c9`](https://github.com/Effect-TS/platform/commit/9cda8c9ce78d5a9c841a828df20401a0dc07b747) Thanks [@tim-smart](https://github.com/tim-smart)! - add KeyValueStore.prefix + +- Updated dependencies [[`9cda8c9`](https://github.com/Effect-TS/platform/commit/9cda8c9ce78d5a9c841a828df20401a0dc07b747), [`6e96703`](https://github.com/Effect-TS/platform/commit/6e96703186f38bd481bffa906e0f99dee89b8e7e), [`9cda8c9`](https://github.com/Effect-TS/platform/commit/9cda8c9ce78d5a9c841a828df20401a0dc07b747)]: + - @effect/platform@0.13.14 + +## 0.13.13 + +### Patch Changes + +- [#117](https://github.com/Effect-TS/platform/pull/117) [`ee7e365`](https://github.com/Effect-TS/platform/commit/ee7e365eafd8b62bab5bc32dd94e3f1190f6e7d6) Thanks [@tim-smart](https://github.com/tim-smart)! - add support for File web api to http + +- Updated dependencies [[`ee7e365`](https://github.com/Effect-TS/platform/commit/ee7e365eafd8b62bab5bc32dd94e3f1190f6e7d6)]: + - @effect/platform@0.13.13 + +## 0.13.12 + +### Patch Changes + +- Updated dependencies [[`4cba795`](https://github.com/Effect-TS/platform/commit/4cba79529426483775782f2384b2194ff57f1279)]: + - @effect/platform@0.13.12 + +## 0.13.11 + +### Patch Changes + +- Updated dependencies [[`5945805`](https://github.com/Effect-TS/platform/commit/59458051ad3885d23c4657369a9a46015f4e569c)]: + - @effect/platform@0.13.11 + +## 0.13.10 + +### Patch Changes + +- [#109](https://github.com/Effect-TS/platform/pull/109) [`7031ec0`](https://github.com/Effect-TS/platform/commit/7031ec030a45a306f4fda4d3ed80796f98a7758e) Thanks [@tim-smart](https://github.com/tim-smart)! - remove Body.EffectBody + +- Updated dependencies [[`7031ec0`](https://github.com/Effect-TS/platform/commit/7031ec030a45a306f4fda4d3ed80796f98a7758e)]: + - @effect/platform@0.13.10 + +## 0.13.9 + +### Patch Changes + +- Updated dependencies [[`df3dbcf`](https://github.com/Effect-TS/platform/commit/df3dbcf468d10dca8cdb219478bb0a23bc66da0c)]: + - @effect/platform@0.13.9 + +## 0.13.8 + +### Patch Changes + +- [#105](https://github.com/Effect-TS/platform/pull/105) [`127c8f5`](https://github.com/Effect-TS/platform/commit/127c8f50f69d5cf7e4a50241fca70923f71f61a2) Thanks [@tim-smart](https://github.com/tim-smart)! - add more form data limit config + +- Updated dependencies [[`e42c3f5`](https://github.com/Effect-TS/platform/commit/e42c3f5103b7361b5162a3e9280759ecd690295f), [`127c8f5`](https://github.com/Effect-TS/platform/commit/127c8f50f69d5cf7e4a50241fca70923f71f61a2)]: + - @effect/platform@0.13.8 + +## 0.13.7 + +### Patch Changes + +- [#97](https://github.com/Effect-TS/platform/pull/97) [`e5c91eb`](https://github.com/Effect-TS/platform/commit/e5c91eb541a6f97cb759ba39732cf08b0ae4c248) Thanks [@tim-smart](https://github.com/tim-smart)! - rename IncomingMessage.urlParams to urlParamsBody + +- Updated dependencies [[`e5c91eb`](https://github.com/Effect-TS/platform/commit/e5c91eb541a6f97cb759ba39732cf08b0ae4c248)]: + - @effect/platform@0.13.7 + +## 0.13.6 + +### Patch Changes + +- Updated dependencies [[`cd3b15e`](https://github.com/Effect-TS/platform/commit/cd3b15e0cb223d2788d383caaa7c0dbc06073dc1)]: + - @effect/platform@0.13.6 + +## 0.13.5 + +### Patch Changes + +- Updated dependencies [[`a034383`](https://github.com/Effect-TS/platform/commit/a0343838bad8f37ab7fb6031084a6514103eba2b)]: + - @effect/platform@0.13.5 + +## 0.13.4 + +### Patch Changes + +- [#89](https://github.com/Effect-TS/platform/pull/89) [`30025cb`](https://github.com/Effect-TS/platform/commit/30025cbd773b4ded89ffdb20a523a4350eb0452e) Thanks [@tim-smart](https://github.com/tim-smart)! - add etag generation for http file responses + +- Updated dependencies [[`05d1765`](https://github.com/Effect-TS/platform/commit/05d1765a0606abce8a3c3d026bdcd5d8b3c64936), [`30025cb`](https://github.com/Effect-TS/platform/commit/30025cbd773b4ded89ffdb20a523a4350eb0452e)]: + - @effect/platform@0.13.4 + +## 0.13.3 + +### Patch Changes + +- Updated dependencies [[`6dfc5b0`](https://github.com/Effect-TS/platform/commit/6dfc5b0fbec0e8a057a26c009f19c9951e4b3ba4), [`d7fffeb`](https://github.com/Effect-TS/platform/commit/d7fffeb38a1c40ad3847e4e5b966f58939d1ba83)]: + - @effect/platform@0.13.3 + +## 0.13.2 + +### Patch Changes + +- [#83](https://github.com/Effect-TS/platform/pull/83) [`ce5e086`](https://github.com/Effect-TS/platform/commit/ce5e0869390d571d21f854b6c1073bf10136e602) Thanks [@tim-smart](https://github.com/tim-smart)! - update deps + +- [#81](https://github.com/Effect-TS/platform/pull/81) [`c1ec2ba`](https://github.com/Effect-TS/platform/commit/c1ec2bab2b1c134c49a82fd5dbb741b0df3d1cd9) Thanks [@tim-smart](https://github.com/tim-smart)! - use ReadonlyRecord for headers + +- [#83](https://github.com/Effect-TS/platform/pull/83) [`ce5e086`](https://github.com/Effect-TS/platform/commit/ce5e0869390d571d21f854b6c1073bf10136e602) Thanks [@tim-smart](https://github.com/tim-smart)! - performance tweaks + +- Updated dependencies [[`ce5e086`](https://github.com/Effect-TS/platform/commit/ce5e0869390d571d21f854b6c1073bf10136e602), [`c1ec2ba`](https://github.com/Effect-TS/platform/commit/c1ec2bab2b1c134c49a82fd5dbb741b0df3d1cd9), [`ce5e086`](https://github.com/Effect-TS/platform/commit/ce5e0869390d571d21f854b6c1073bf10136e602)]: + - @effect/platform@0.13.2 + +## 0.13.1 + +### Patch Changes + +- [#79](https://github.com/Effect-TS/platform/pull/79) [`3544c17`](https://github.com/Effect-TS/platform/commit/3544c17f5778ab47cb4019b6458b2543d572629a) Thanks [@TylorS](https://github.com/TylorS)! - Attempt to derive content-type from headers + +- Updated dependencies [[`3544c17`](https://github.com/Effect-TS/platform/commit/3544c17f5778ab47cb4019b6458b2543d572629a)]: + - @effect/platform@0.13.1 + +## 0.13.0 + +### Minor Changes + +- [#77](https://github.com/Effect-TS/platform/pull/77) [`e97d80b`](https://github.com/Effect-TS/platform/commit/e97d80bd69646195a65ea6dfe13c6af19589d2cf) Thanks [@tim-smart](https://github.com/tim-smart)! - remove Console module + +### Patch Changes + +- Updated dependencies [[`e97d80b`](https://github.com/Effect-TS/platform/commit/e97d80bd69646195a65ea6dfe13c6af19589d2cf)]: + - @effect/platform@0.13.0 + +## 0.12.1 + +### Patch Changes + +- Updated dependencies [[`d23ff14`](https://github.com/Effect-TS/platform/commit/d23ff14756796e945307ccfdf65252d47f99b7aa)]: + - @effect/platform@0.12.1 + +## 0.12.0 + +### Minor Changes + +- [#71](https://github.com/Effect-TS/platform/pull/71) [`139de2e`](https://github.com/Effect-TS/platform/commit/139de2e18adcf6661609909ec6afd44abe4cb1a9) Thanks [@tim-smart](https://github.com/tim-smart)! - add HttpServer module + +### Patch Changes + +- [#71](https://github.com/Effect-TS/platform/pull/71) [`139de2e`](https://github.com/Effect-TS/platform/commit/139de2e18adcf6661609909ec6afd44abe4cb1a9) Thanks [@tim-smart](https://github.com/tim-smart)! - add SizeInput type + +- Updated dependencies [[`139de2e`](https://github.com/Effect-TS/platform/commit/139de2e18adcf6661609909ec6afd44abe4cb1a9), [`139de2e`](https://github.com/Effect-TS/platform/commit/139de2e18adcf6661609909ec6afd44abe4cb1a9)]: + - @effect/platform@0.12.0 + +## 0.11.5 + +### Patch Changes + +- [#69](https://github.com/Effect-TS/platform/pull/69) [`0eb7df0`](https://github.com/Effect-TS/platform/commit/0eb7df0e2cbfb96986c3bbee4650c4036a97b1d2) Thanks [@tim-smart](https://github.com/tim-smart)! - have Command & Client implement Pipeable + +- Updated dependencies [[`0eb7df0`](https://github.com/Effect-TS/platform/commit/0eb7df0e2cbfb96986c3bbee4650c4036a97b1d2)]: + - @effect/platform@0.11.5 + +## 0.11.4 + +### Patch Changes + +- [#67](https://github.com/Effect-TS/platform/pull/67) [`c41a166`](https://github.com/Effect-TS/platform/commit/c41a16614bc4daff05956b84a6bcd01cbb5836dd) Thanks [@tim-smart](https://github.com/tim-smart)! - add node implementation of http client + +- Updated dependencies [[`c41a166`](https://github.com/Effect-TS/platform/commit/c41a16614bc4daff05956b84a6bcd01cbb5836dd)]: + - @effect/platform@0.11.4 + +## 0.11.3 + +### Patch Changes + +- Updated dependencies [[`6f2d011`](https://github.com/Effect-TS/platform/commit/6f2d011ce917d74d14b0375525f5c9805f8e44fe)]: + - @effect/platform@0.11.3 + +## 0.11.2 + +### Patch Changes + +- [#62](https://github.com/Effect-TS/platform/pull/62) [`3d44256`](https://github.com/Effect-TS/platform/commit/3d442560fee94a0c8f01f936a3f7c5b5e1ac8fc2) Thanks [@tim-smart](https://github.com/tim-smart)! - improve http client options type + +- Updated dependencies [[`3d44256`](https://github.com/Effect-TS/platform/commit/3d442560fee94a0c8f01f936a3f7c5b5e1ac8fc2)]: + - @effect/platform@0.11.2 + +## 0.11.1 + +### Patch Changes + +- [#38](https://github.com/Effect-TS/platform/pull/38) [`f70a121`](https://github.com/Effect-TS/platform/commit/f70a121b2fc9d1052434863c41657d353d21fb26) Thanks [@tim-smart](https://github.com/tim-smart)! - add HttpClient module + +- Updated dependencies [[`f70a121`](https://github.com/Effect-TS/platform/commit/f70a121b2fc9d1052434863c41657d353d21fb26)]: + - @effect/platform@0.11.1 + +## 0.11.0 + +### Minor Changes + +- [#59](https://github.com/Effect-TS/platform/pull/59) [`b2f7bc0`](https://github.com/Effect-TS/platform/commit/b2f7bc0fe7310d861d52da03fefd9bc91852e5f9) Thanks [@tim-smart](https://github.com/tim-smart)! - update dependencies + +- [#58](https://github.com/Effect-TS/platform/pull/58) [`f61aa57`](https://github.com/Effect-TS/platform/commit/f61aa57a915ee221fdf5259cbaf1e4fe208e01b8) Thanks [@tim-smart](https://github.com/tim-smart)! - update build tools + +- [#56](https://github.com/Effect-TS/platform/pull/56) [`efcf469`](https://github.com/Effect-TS/platform/commit/efcf469da368770b2f321043a8e0e33f079c169b) Thanks [@tim-smart](https://github.com/tim-smart)! - switch to peerDependencies + +- Updated dependencies [[`b2f7bc0`](https://github.com/Effect-TS/platform/commit/b2f7bc0fe7310d861d52da03fefd9bc91852e5f9), [`f61aa57`](https://github.com/Effect-TS/platform/commit/f61aa57a915ee221fdf5259cbaf1e4fe208e01b8), [`efcf469`](https://github.com/Effect-TS/platform/commit/efcf469da368770b2f321043a8e0e33f079c169b)]: + - @effect/platform@0.11.0 + +## 0.10.5 + +### Patch Changes + +- [#55](https://github.com/Effect-TS/platform/pull/55) [`67caeff`](https://github.com/Effect-TS/platform/commit/67caeffb5343b4ce428aa3c6b393feb383667fef) Thanks [@tim-smart](https://github.com/tim-smart)! - fix Path tag in node package + +- [#55](https://github.com/Effect-TS/platform/pull/55) [`67caeff`](https://github.com/Effect-TS/platform/commit/67caeffb5343b4ce428aa3c6b393feb383667fef) Thanks [@tim-smart](https://github.com/tim-smart)! - add labels to Tags + +- [#46](https://github.com/Effect-TS/platform/pull/46) [`4a4d0af`](https://github.com/Effect-TS/platform/commit/4a4d0af4832f543fc53b2ba5c9fc9739bbc78f2e) Thanks [@fubhy](https://github.com/fubhy)! - add seek method to file handles + +- Updated dependencies [[`67caeff`](https://github.com/Effect-TS/platform/commit/67caeffb5343b4ce428aa3c6b393feb383667fef), [`4a4d0af`](https://github.com/Effect-TS/platform/commit/4a4d0af4832f543fc53b2ba5c9fc9739bbc78f2e), [`b3950e1`](https://github.com/Effect-TS/platform/commit/b3950e1373673ae492106fe0cb76bcd32fbe5a2b)]: + - @effect/platform@0.10.4 + +## 0.10.4 + +### Patch Changes + +- Updated dependencies [[`9163d96`](https://github.com/Effect-TS/platform/commit/9163d96717a832e9dbf2bdd262d73034fcbe92e9)]: + - @effect/platform@0.10.3 + +## 0.10.3 + +### Patch Changes + +- Updated dependencies [[`44eaaf5`](https://github.com/Effect-TS/platform/commit/44eaaf5c182dc70c73b7da9687e9c0a81daea86c)]: + - @effect/platform@0.10.2 + +## 0.10.2 + +### Patch Changes + +- [#47](https://github.com/Effect-TS/platform/pull/47) [`24b56d5`](https://github.com/Effect-TS/platform/commit/24b56d5d6afa40df072e2db37ebd71df538e66ac) Thanks [@tim-smart](https://github.com/tim-smart)! - add exists and readFileString to FileSystem + +- Updated dependencies [[`24b56d5`](https://github.com/Effect-TS/platform/commit/24b56d5d6afa40df072e2db37ebd71df538e66ac)]: + - @effect/platform@0.10.1 + +## 0.10.1 + +### Patch Changes + +- [#43](https://github.com/Effect-TS/platform/pull/43) [`b92639a`](https://github.com/Effect-TS/platform/commit/b92639aad9bb4c5cd91ed191a5a45d26d048ac9a) Thanks [@fubhy](https://github.com/fubhy)! - Fixed `truncate` to support `undefined` as size. + +## 0.10.0 + +### Minor Changes + +- [#41](https://github.com/Effect-TS/platform/pull/41) [`68cbdca`](https://github.com/Effect-TS/platform/commit/68cbdca7e9da509c212d44101ab61c3bcf1354ad) Thanks [@tim-smart](https://github.com/tim-smart)! - update /data, /io and /stream + +### Patch Changes + +- Updated dependencies [[`68cbdca`](https://github.com/Effect-TS/platform/commit/68cbdca7e9da509c212d44101ab61c3bcf1354ad)]: + - @effect/platform@0.10.0 + +## 0.9.0 + +### Minor Changes + +- [#39](https://github.com/Effect-TS/platform/pull/39) [`3012e28`](https://github.com/Effect-TS/platform/commit/3012e289272d383fdae16af6b3ba396dec290b77) Thanks [@tim-smart](https://github.com/tim-smart)! - update dependencies + +### Patch Changes + +- Updated dependencies [[`3012e28`](https://github.com/Effect-TS/platform/commit/3012e289272d383fdae16af6b3ba396dec290b77)]: + - @effect/platform@0.9.0 + +## 0.8.0 + +### Minor Changes + +- [#36](https://github.com/Effect-TS/platform/pull/36) [`b82cbcc`](https://github.com/Effect-TS/platform/commit/b82cbcc56789c014f0a50c505497239ec220f4fd) Thanks [@tim-smart](https://github.com/tim-smart)! - update dependencies + +### Patch Changes + +- Updated dependencies [[`b82cbcc`](https://github.com/Effect-TS/platform/commit/b82cbcc56789c014f0a50c505497239ec220f4fd)]: + - @effect/platform@0.8.0 + +## 0.7.0 + +### Minor Changes + +- [#34](https://github.com/Effect-TS/platform/pull/34) [`601d045`](https://github.com/Effect-TS/platform/commit/601d04526ad0a2e3285de509fdf86c7b6809a547) Thanks [@tim-smart](https://github.com/tim-smart)! - update /stream + +### Patch Changes + +- Updated dependencies [[`601d045`](https://github.com/Effect-TS/platform/commit/601d04526ad0a2e3285de509fdf86c7b6809a547)]: + - @effect/platform@0.7.0 + +## 0.6.0 + +### Minor Changes + +- [#32](https://github.com/Effect-TS/platform/pull/32) [`ee94eae`](https://github.com/Effect-TS/platform/commit/ee94eae46aee327baf0c6960befa6c35154fa35b) Thanks [@tim-smart](https://github.com/tim-smart)! - update /io and /data + +### Patch Changes + +- Updated dependencies [[`ee94eae`](https://github.com/Effect-TS/platform/commit/ee94eae46aee327baf0c6960befa6c35154fa35b)]: + - @effect/platform@0.6.0 + +## 0.5.1 + +### Patch Changes + +- [#30](https://github.com/Effect-TS/platform/pull/30) [`aee2977`](https://github.com/Effect-TS/platform/commit/aee29776d9291f2ff8cf3379d5c6251a55343b51) Thanks [@tim-smart](https://github.com/tim-smart)! - set fromReadable buffer to 1 + +## 0.5.0 + +### Minor Changes + +- [#28](https://github.com/Effect-TS/platform/pull/28) [`f3d73f5`](https://github.com/Effect-TS/platform/commit/f3d73f587ad9b528bb1e37cf44e4928d913f56dd) Thanks [@tim-smart](https://github.com/tim-smart)! - update /io + +### Patch Changes + +- Updated dependencies [[`f3d73f5`](https://github.com/Effect-TS/platform/commit/f3d73f587ad9b528bb1e37cf44e4928d913f56dd)]: + - @effect/platform@0.5.0 + +## 0.4.0 + +### Minor Changes + +- [#26](https://github.com/Effect-TS/platform/pull/26) [`834e1a7`](https://github.com/Effect-TS/platform/commit/834e1a793365f4deb742814d9cd6df9faae9d0c2) Thanks [@tim-smart](https://github.com/tim-smart)! - update /io and /data + +### Patch Changes + +- Updated dependencies [[`834e1a7`](https://github.com/Effect-TS/platform/commit/834e1a793365f4deb742814d9cd6df9faae9d0c2)]: + - @effect/platform@0.4.0 + +## 0.3.1 + +### Patch Changes + +- [#24](https://github.com/Effect-TS/platform/pull/24) [`8f29d2a`](https://github.com/Effect-TS/platform/commit/8f29d2a2c5681044e3a0fa13dd7d107f9fe9cfae) Thanks [@tim-smart](https://github.com/tim-smart)! - fix offset option in node file system + +## 0.3.0 + +### Minor Changes + +- [#22](https://github.com/Effect-TS/platform/pull/22) [`645f10f`](https://github.com/Effect-TS/platform/commit/645f10f6d6a8600e369f068b22f3c2ef5169e867) Thanks [@tim-smart](https://github.com/tim-smart)! - update /io and /data + +### Patch Changes + +- Updated dependencies [[`645f10f`](https://github.com/Effect-TS/platform/commit/645f10f6d6a8600e369f068b22f3c2ef5169e867)]: + - @effect/platform@0.3.0 + +## 0.2.0 + +### Minor Changes + +- [#20](https://github.com/Effect-TS/platform/pull/20) [`756ccbe`](https://github.com/Effect-TS/platform/commit/756ccbe002f2e00c02b88aac126c2bc5b17a5769) Thanks [@IMax153](https://github.com/IMax153)! - upgrade to `@effect/data@0.13.5`, `@effect/io@0.31.3`, and `@effect/stream@0.25.1` + +### Patch Changes + +- Updated dependencies [[`756ccbe`](https://github.com/Effect-TS/platform/commit/756ccbe002f2e00c02b88aac126c2bc5b17a5769)]: + - @effect/platform@0.2.0 + +## 0.1.1 + +### Patch Changes + +- [#17](https://github.com/Effect-TS/platform/pull/17) [`1ea91bf`](https://github.com/Effect-TS/platform/commit/1ea91bf6ecf8778b7b063afb22041c3f75a90650) Thanks [@tim-smart](https://github.com/tim-smart)! - allow unsetting fromReadable chunkSize + +## 0.1.0 + +### Minor Changes + +- [#13](https://github.com/Effect-TS/platform/pull/13) [`b95c25f`](https://github.com/Effect-TS/platform/commit/b95c25f619b8e5ebf915f675f63de01accb1a8b8) Thanks [@tim-smart](https://github.com/tim-smart)! - initial release + +### Patch Changes + +- Updated dependencies [[`b95c25f`](https://github.com/Effect-TS/platform/commit/b95c25f619b8e5ebf915f675f63de01accb1a8b8)]: + - @effect/platform@0.1.0 diff --git a/repos/effect/packages/platform-node/LICENSE b/repos/effect/packages/platform-node/LICENSE new file mode 100644 index 0000000..be1f5c1 --- /dev/null +++ b/repos/effect/packages/platform-node/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Effectful Technologies Inc + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/repos/effect/packages/platform-node/README.md b/repos/effect/packages/platform-node/README.md new file mode 100644 index 0000000..8df2065 --- /dev/null +++ b/repos/effect/packages/platform-node/README.md @@ -0,0 +1,7 @@ +# `@effect/platform-node` + +Provides Node.js-specific implementations for the abstractions defined in [`@effect/platform`](https://github.com/Effect-TS/effect/tree/main/packages/platform), allowing you to write platform-independent code that integrates smoothly with Node.js. + +## Documentation + +- **API Reference**: [View the full documentation](https://effect-ts.github.io/effect/docs/platform-node). diff --git a/repos/effect/packages/platform-node/docgen.json b/repos/effect/packages/platform-node/docgen.json new file mode 100644 index 0000000..f806321 --- /dev/null +++ b/repos/effect/packages/platform-node/docgen.json @@ -0,0 +1,36 @@ +{ + "$schema": "../../node_modules/@effect/docgen/schema.json", + "srcLink": "https://github.com/Effect-TS/effect/tree/main/packages/platform-node/src/", + "exclude": ["src/internal/**/*.ts"], + "examplesCompilerOptions": { + "noEmit": true, + "strict": true, + "skipLibCheck": true, + "moduleResolution": "Bundler", + "module": "ES2022", + "target": "ES2022", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "paths": { + "effect": ["../../../effect/src/index.js"], + "effect/*": ["../../../effect/src/*.js"], + "@effect/cluster": ["../../../cluster/src/index.js"], + "@effect/cluster/*": ["../../../cluster/src/*.js"], + "@effect/experimental": ["../../../experimental/src/index.js"], + "@effect/experimental/*": ["../../../experimental/src/*.js"], + "@effect/platform": ["../../../platform/src/index.js"], + "@effect/platform/*": ["../../../platform/src/*.js"], + "@effect/platform-node": ["../../../platform-node/src/index.js"], + "@effect/platform-node/*": ["../../../platform-node/src/*.js"], + "@effect/platform-node-shared": [ + "../../../platform-node-shared/src/index.js" + ], + "@effect/platform-node-shared/*": [ + "../../../platform-node-shared/src/*.js" + ], + "@effect/rpc": ["../../../rpc/src/index.js"], + "@effect/rpc/*": ["../../../rpc/src/*.js"], + "@effect/sql": ["../../../sql/src/index.js"], + "@effect/sql/*": ["../../../sql/src/*.js"] + } + } +} diff --git a/repos/effect/packages/platform-node/examples/api.ts b/repos/effect/packages/platform-node/examples/api.ts new file mode 100644 index 0000000..aa676f5 --- /dev/null +++ b/repos/effect/packages/platform-node/examples/api.ts @@ -0,0 +1,237 @@ +import { + FetchHttpClient, + HttpApi, + HttpApiBuilder, + HttpApiClient, + HttpApiEndpoint, + HttpApiError, + HttpApiGroup, + HttpApiMiddleware, + HttpApiSchema, + HttpApiSecurity, + HttpApiSwagger, + HttpClient, + HttpClientRequest, + HttpMiddleware, + HttpServer, + OpenApi +} from "@effect/platform" +import { NodeHttpServer, NodeRuntime } from "@effect/platform-node" +import { Context, Effect, Layer, Redacted, Schema } from "effect" +import { createServer } from "node:http" + +class User extends Schema.Class("User")({ + id: Schema.Number, + name: Schema.String +}) {} + +class CurrentUser extends Context.Tag("CurrentUser")() {} + +class Unauthorized extends Schema.TaggedError()("Unauthorized", { + message: Schema.String +}, HttpApiSchema.annotations({ status: 401 })) {} + +export class Authentication extends HttpApiMiddleware.Tag()("Authentication", { + failure: Unauthorized, + provides: CurrentUser, + security: { + bearer: HttpApiSecurity.bearer + } +}) {} + +const idParam = HttpApiSchema.param("id", Schema.NumberFromString) + +class UsersApi extends HttpApiGroup.make("users") + .add( + HttpApiEndpoint.get("findById")`/${idParam}` + .addSuccess(User) + .setHeaders(Schema.Struct({ + page: Schema.NumberFromString.pipe( + Schema.optionalWith({ default: () => 1 }) + ) + })) + .addError(Schema.String.pipe( + HttpApiSchema.asEmpty({ status: 413, decode: () => "boom" }) + )) + ) + .add( + HttpApiEndpoint.post("create", "/") + .setPayload(HttpApiSchema.Multipart(Schema.Struct({ + name: Schema.String + }))) + .addSuccess(User) + ) + .add( + HttpApiEndpoint.get("me", "/me") + .addSuccess(User) + ) + .middleware(Authentication) + .prefix("/users") + .annotateContext(OpenApi.annotations({ + title: "Users API", + description: "API for managing users" + })) +{} + +class TopLevelApi extends HttpApiGroup.make("topLevel", { topLevel: true }) + .add( + HttpApiEndpoint.get("csv", "/csv") + .addSuccess(HttpApiSchema.Text({ + contentType: "text/csv" + })) + .addError(HttpApiError.Conflict) + ) + .add( + HttpApiEndpoint.get("binary", "/binary") + .addSuccess(HttpApiSchema.Uint8Array()) + ) + .add( + HttpApiEndpoint.get("urlParams", "/url-params") + .addSuccess( + Schema.Struct({ + id: Schema.NumberFromString, + name: Schema.String + }).pipe( + HttpApiSchema.withEncoding({ + kind: "UrlParams" + }) + ) + ) + ) + .annotateContext(OpenApi.annotations({ + title: "Top Level API", + description: "API for top level endpoints" + })) +{} + +class PeopleApi extends HttpApiGroup.make("people") + .add( + HttpApiEndpoint.get("list", "/") + .addSuccess(Schema.Array(User)) + ) + .prefix("/people") +{} + +class AnotherApi extends HttpApi.make("another").add(PeopleApi).prefix("/v2") {} + +class MyApi extends HttpApi.make("api") + .add(UsersApi) + .add(TopLevelApi) + .addHttpApi(AnotherApi) +{} + +// ------------------------------------------------ +// implementation +// ------------------------------------------------ + +const AuthenticationLive = Layer.succeed( + Authentication, + Authentication.of({ + bearer: (token) => + Effect.succeed( + new User({ + id: 1000, + name: `Authenticated with ${Redacted.value(token)}` + }) + ) + }) +) + +const UsersLive = HttpApiBuilder.group( + MyApi, + "users", + (handlers) => + handlers + .handle("create", (_) => Effect.succeed(new User({ ..._.payload, id: 123 }))) + .handle("findById", (_) => + Effect.as( + HttpApiBuilder.securitySetCookie( + HttpApiSecurity.apiKey({ + in: "cookie", + key: "token" + }), + "secret123" + ), + new User({ + id: _.path.id, + name: `John Doe (${_.headers.page})` + }) + )) + .handle("me", (_) => CurrentUser) +).pipe( + Layer.provide(AuthenticationLive) +) + +const PeopleLive = HttpApiBuilder.group( + MyApi, + "people", + (handlers) => handlers.handle("list", (_) => Effect.succeed([new User({ id: 1, name: "John" })])) +) + +const TopLevelLive = HttpApiBuilder.group( + MyApi, + "topLevel", + (handlers) => + handlers + .handle("csv", (_) => Effect.succeed("id,name\n1,John")) + .handle("urlParams", (_) => + Effect.succeed({ + id: 123, + name: "John" + })) + .handle("binary", (_) => Effect.succeed(new Uint8Array([1, 2, 3, 4, 5]))) +) + +const ApiLive = HttpApiBuilder.api(MyApi).pipe( + Layer.provide([UsersLive, TopLevelLive, PeopleLive]) +) + +// ------------------------------------------------ +// server +// ------------------------------------------------ + +HttpApiBuilder.serve(HttpMiddleware.logger).pipe( + Layer.provide(HttpApiSwagger.layer()), + Layer.provide(HttpApiBuilder.middlewareOpenApi()), + Layer.provide(ApiLive), + Layer.provide(HttpApiBuilder.middlewareCors()), + HttpServer.withLogAddress, + Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 })), + Layer.launch, + NodeRuntime.runMain +) + +Effect.gen(function*() { + yield* Effect.sleep(2000) + const client = yield* HttpApiClient.make(MyApi, { + baseUrl: "http://localhost:3000", + transformClient: HttpClient.mapRequest(HttpClientRequest.bearerToken("token")) + }) + + const data = new FormData() + data.append("name", "John") + console.log("Multipart", yield* client.users.create({ payload: data })) + + let user = yield* client.users.findById({ + path: { id: 123 }, + headers: { page: 10 } + }) + console.log("json", user) + + user = yield* client.users.me() + console.log("json me", user) + + const csv = yield* client.csv({ withResponse: true }) + console.log("csv", csv) + + const urlParams = yield* client.urlParams() + console.log("urlParams", urlParams) + + const binary = yield* client.binary() + console.log("binary", binary) + + console.log("merged api", yield* client.people.list()) +}).pipe( + Effect.provide(FetchHttpClient.layer), + NodeRuntime.runMain +) diff --git a/repos/effect/packages/platform-node/examples/cluster.ts b/repos/effect/packages/platform-node/examples/cluster.ts new file mode 100644 index 0000000..c10de88 --- /dev/null +++ b/repos/effect/packages/platform-node/examples/cluster.ts @@ -0,0 +1,71 @@ +import { Entity, RunnerAddress, Singleton } from "@effect/cluster" +import { NodeClusterSocket, NodeRuntime } from "@effect/platform-node" +import { Rpc } from "@effect/rpc" +import { Effect, Layer, Logger, LogLevel, Option, Schema } from "effect" + +const Counter = Entity.make("Counter", [ + Rpc.make("Increment", { + payload: { amount: Schema.Number }, + success: Schema.Number + }), + + Rpc.make("Decrement", { + payload: { amount: Schema.Number }, + success: Schema.Number + }) +]) + +const CounterLive = Counter.toLayer( + Effect.gen(function*() { + console.log("Creating Counter", yield* Entity.CurrentAddress) + + let state = 0 + + yield* Effect.addFinalizer(() => Effect.log("Finalizing", state)) + + return { + Increment: Effect.fnUntraced(function*({ payload: { amount }, requestId }) { + console.log("Incrementing by", amount, requestId) + state += amount + return state + }), + Decrement: Effect.fnUntraced(function*({ payload: { amount } }) { + console.log("Decrementing by", amount) + state -= amount + return state + }) + } + }), + { maxIdleTime: "10 seconds", concurrency: "unbounded" } +) + +const SendMessage = Singleton.make( + "SendMessage", + Effect.gen(function*() { + const makeClient = yield* Counter.client + const client = makeClient("test") + yield* Effect.log("Client", yield* client.Increment({ amount: 1 })) + yield* Effect.log("Client 2", yield* client.Increment({ amount: 1 })) + yield* Effect.log("Client 3", yield* client.Decrement({ amount: 1 })) + }) +) + +for (let i = 0; i < 1; i++) { + const ShardingLive = NodeClusterSocket.layer({ + storage: "local", + shardingConfig: { + runnerAddress: Option.some(RunnerAddress.make("localhost", 50000 + i)) + } + }) + + Layer.mergeAll( + CounterLive, + SendMessage + // SendMessage2 + ).pipe( + Layer.provide(ShardingLive), + Layer.provide(Logger.minimumLogLevel(LogLevel.All)), + Layer.launch, + NodeRuntime.runMain + ) +} diff --git a/repos/effect/packages/platform-node/examples/fs-watch.ts b/repos/effect/packages/platform-node/examples/fs-watch.ts new file mode 100644 index 0000000..2794735 --- /dev/null +++ b/repos/effect/packages/platform-node/examples/fs-watch.ts @@ -0,0 +1,14 @@ +import { FileSystem } from "@effect/platform" +import { NodeFileSystem, NodeRuntime } from "@effect/platform-node" +import * as ParcelWatcher from "@effect/platform-node/NodeFileSystem/ParcelWatcher" +import { Console, Effect, Layer, Stream } from "effect" + +const EnvLive = NodeFileSystem.layer.pipe(Layer.provide(ParcelWatcher.layer)) + +Effect.gen(function*() { + const fs = yield* FileSystem.FileSystem + + yield* fs.watch("src", { recursive: true }).pipe( + Stream.runForEach(Console.log) + ) +}).pipe(Effect.provide(EnvLive), NodeRuntime.runMain) diff --git a/repos/effect/packages/platform-node/examples/http-client.ts b/repos/effect/packages/platform-node/examples/http-client.ts new file mode 100644 index 0000000..5c65d45 --- /dev/null +++ b/repos/effect/packages/platform-node/examples/http-client.ts @@ -0,0 +1,62 @@ +import type { HttpBody, HttpClientError } from "@effect/platform" +import { HttpClient, HttpClientRequest, HttpClientResponse } from "@effect/platform" +import { NodeHttpClient } from "@effect/platform-node" +import { runMain } from "@effect/platform-node/NodeRuntime" +import * as Context from "effect/Context" +import * as Effect from "effect/Effect" +import * as Layer from "effect/Layer" +import type * as ParseResult from "effect/ParseResult" +import * as Schema from "effect/Schema" + +class Todo extends Schema.Class("Todo")({ + userId: Schema.Number, + id: Schema.Number, + title: Schema.String, + completed: Schema.Boolean +}) { + static decodeResponse = HttpClientResponse.schemaBodyJson(Todo) +} + +const TodoWithoutId = Schema.Struct(Todo.fields).pipe(Schema.omit("id")) +type TodoWithoutId = Schema.Schema.Type + +interface TodoService { + readonly create: ( + _: TodoWithoutId + ) => Effect.Effect +} +const TodoService = Context.GenericTag("@effect/platform-node/examples/TodoService") + +const makeTodoService = Effect.gen(function*() { + const defaultClient = yield* HttpClient.HttpClient + const clientWithBaseUrl = defaultClient.pipe( + HttpClient.filterStatusOk, + HttpClient.mapRequest(HttpClientRequest.prependUrl("https://jsonplaceholder.typicode.com")) + ) + + const addTodoWithoutIdBody = HttpClientRequest.schemaBodyJson(TodoWithoutId) + const create = (todo: TodoWithoutId) => + addTodoWithoutIdBody( + HttpClientRequest.post("/todos"), + todo + ).pipe( + Effect.flatMap(clientWithBaseUrl.execute), + Effect.flatMap(Todo.decodeResponse), + Effect.scoped + ) + + return TodoService.of({ create }) +}) + +const TodoServiceLive = Layer.effect(TodoService, makeTodoService).pipe( + Layer.provide(NodeHttpClient.layer) +) + +Effect.flatMap( + TodoService, + (todos) => todos.create({ userId: 1, title: "test", completed: false }) +).pipe( + Effect.tap(Effect.log), + Effect.provide(TodoServiceLive), + runMain +) diff --git a/repos/effect/packages/platform-node/examples/http-router.ts b/repos/effect/packages/platform-node/examples/http-router.ts new file mode 100644 index 0000000..67c0ca3 --- /dev/null +++ b/repos/effect/packages/platform-node/examples/http-router.ts @@ -0,0 +1,57 @@ +import { + HttpMiddleware, + HttpRouter, + HttpServer, + HttpServerRequest, + HttpServerResponse, + Multipart +} from "@effect/platform" +import { NodeHttpServer, NodeRuntime } from "@effect/platform-node" +import { Effect, Layer, Schedule, Stream } from "effect" +import * as Schema from "effect/Schema" +import { createServer } from "node:http" + +const ServerLive = NodeHttpServer.layer(() => createServer(), { port: 3000 }) + +const HttpLive = HttpRouter.empty.pipe( + HttpRouter.get( + "/", + Effect.map( + HttpServerRequest.HttpServerRequest, + (req) => HttpServerResponse.text(req.url) + ) + ), + HttpRouter.get( + "/healthz", + HttpServerResponse.text("ok").pipe( + HttpMiddleware.withLoggerDisabled + ) + ), + HttpRouter.post( + "/upload", + Effect.gen(function*() { + const data = yield* HttpServerRequest.schemaBodyForm(Schema.Struct({ + files: Multipart.FilesSchema + })) + console.log("got files", data.files) + return HttpServerResponse.empty() + }) + ), + HttpRouter.get( + "/ws", + Stream.fromSchedule(Schedule.spaced(1000)).pipe( + Stream.map(JSON.stringify), + Stream.encodeText, + Stream.pipeThroughChannel(HttpServerRequest.upgradeChannel()), + Stream.decodeText(), + Stream.runForEach((_) => Effect.log(_)), + Effect.annotateLogs("ws", "recv"), + Effect.as(HttpServerResponse.empty()) + ) + ), + HttpServer.serve(HttpMiddleware.logger), + HttpServer.withLogAddress, + Layer.provide(ServerLive) +) + +NodeRuntime.runMain(Layer.launch(HttpLive)) diff --git a/repos/effect/packages/platform-node/examples/http-server.ts b/repos/effect/packages/platform-node/examples/http-server.ts new file mode 100644 index 0000000..1882d0a --- /dev/null +++ b/repos/effect/packages/platform-node/examples/http-server.ts @@ -0,0 +1,11 @@ +import { HttpServer, HttpServerResponse } from "@effect/platform" +import { NodeHttpServer, NodeRuntime } from "@effect/platform-node" +import { Layer } from "effect" +import { createServer } from "node:http" + +const ServerLive = NodeHttpServer.layer(() => createServer(), { port: 3000 }) + +const HttpLive = HttpServer.serve(HttpServerResponse.text("Hello World")) + .pipe(Layer.provide(ServerLive)) + +NodeRuntime.runMain(Layer.launch(HttpLive)) diff --git a/repos/effect/packages/platform-node/examples/http-tag-router.ts b/repos/effect/packages/platform-node/examples/http-tag-router.ts new file mode 100644 index 0000000..99ea070 --- /dev/null +++ b/repos/effect/packages/platform-node/examples/http-tag-router.ts @@ -0,0 +1,44 @@ +import { HttpMiddleware, HttpRouter, HttpServer, HttpServerResponse } from "@effect/platform" +import { NodeHttpServer, NodeRuntime } from "@effect/platform-node" +import { Effect, Layer } from "effect" +import { createServer } from "http" + +// You can define router instances using `HttpRouter.Tag` +class UserRouter extends HttpRouter.Tag("UserRouter")() {} + +// Create `Layer`'s for your routes with `UserRouter.use` +const GetUsers = UserRouter.use((router) => + Effect.gen(function*() { + yield* router.get("/", HttpServerResponse.text("got users")) + }) +) + +const CreateUser = UserRouter.use((router) => + Effect.gen(function*() { + yield* router.post("/", HttpServerResponse.text("created user")) + }) +) + +// Merge all the routes together with `Layer.mergeAll` +const AllUserRoutes = Layer.mergeAll(GetUsers, CreateUser).pipe( + Layer.provideMerge(UserRouter.Live) +) + +// `HttpRouter.Default` can also be used. Here we combine our `UserRouter` with +// the default router. +const AllRoutes = HttpRouter.Default.use((router) => + Effect.gen(function*() { + yield* router.mount("/users", yield* UserRouter.router) + }) +).pipe(Layer.provide(AllUserRoutes)) + +const ServerLive = NodeHttpServer.layer(createServer, { port: 3000 }) + +// use the `.unwrap` api to turn the underlying `HttpRouter` into another layer. +// Here we use `HttpServer.serve` to create a server from the `HttpRouter`. +const HttpLive = HttpRouter.Default.unwrap(HttpServer.serve(HttpMiddleware.logger)).pipe( + Layer.provide(AllRoutes), + Layer.provide(ServerLive) +) + +NodeRuntime.runMain(Layer.launch(HttpLive)) diff --git a/repos/effect/packages/platform-node/examples/terminal.ts b/repos/effect/packages/platform-node/examples/terminal.ts new file mode 100644 index 0000000..04198a6 --- /dev/null +++ b/repos/effect/packages/platform-node/examples/terminal.ts @@ -0,0 +1,23 @@ +import { Terminal } from "@effect/platform" +import { NodeRuntime, NodeTerminal } from "@effect/platform-node" +import { Console, Effect } from "effect" + +const program = Effect.gen(function*() { + const terminal = yield* Terminal.Terminal + + const line1 = yield* terminal.readLine + yield* Console.log(`First line: ${line1}`) + + const line2 = yield* terminal.readLine + yield* Console.log(`Second line: ${line2}`) + + const line3 = yield* terminal.readLine + yield* Console.log(`Third line: ${line3}`) +}) + +const MainLive = NodeTerminal.layer + +program.pipe( + Effect.provide(MainLive), + NodeRuntime.runMain +) diff --git a/repos/effect/packages/platform-node/examples/worker.ts b/repos/effect/packages/platform-node/examples/worker.ts new file mode 100644 index 0000000..7578743 --- /dev/null +++ b/repos/effect/packages/platform-node/examples/worker.ts @@ -0,0 +1,34 @@ +import { Worker } from "@effect/platform" +import { NodeRuntime, NodeWorker } from "@effect/platform-node" +import { Console, Context, Effect, Layer, Stream } from "effect" +import * as WT from "node:worker_threads" + +interface MyWorkerPool { + readonly _: unique symbol +} +const Pool = Context.GenericTag>("@app/MyWorkerPool") +const PoolLive = Worker.makePoolLayer(Pool, { size: 3 }).pipe( + Layer.provide(NodeWorker.layer(() => tsWorker("./worker/range.ts"))) +) + +Effect.gen(function*() { + const pool = yield* Pool + yield* Effect.all([ + pool.execute(5).pipe( + Stream.runForEach((_) => Console.log("worker 1", _)) + ), + pool.execute(10).pipe( + Stream.runForEach((_) => Console.log("worker 2", _)) + ), + pool.execute(15).pipe( + Stream.runForEach((_) => Console.log("worker 3", _)) + ) + ], { concurrency: "inherit" }) +}).pipe(Effect.provide(PoolLive), NodeRuntime.runMain) + +const tsWorker = (path: string) => { + const url = new URL(path, import.meta.url) + return new WT.Worker(`import('tsx/esm/api').then(({ register }) => { register(); import('${url.pathname}') })`, { + eval: true + }) +} diff --git a/repos/effect/packages/platform-node/examples/worker/range.ts b/repos/effect/packages/platform-node/examples/worker/range.ts new file mode 100644 index 0000000..071e821 --- /dev/null +++ b/repos/effect/packages/platform-node/examples/worker/range.ts @@ -0,0 +1,11 @@ +import { WorkerRunner } from "@effect/platform" +import { NodeRuntime, NodeWorkerRunner } from "@effect/platform-node" +import { Effect, Layer, Stream } from "effect" + +const WorkerLive = Effect.gen(function*() { + yield* WorkerRunner.make((n: number) => Stream.range(0, n)) + yield* Effect.log("worker started") + yield* Effect.addFinalizer(() => Effect.log("worker closed")) +}).pipe(Layer.scopedDiscard, Layer.provide(NodeWorkerRunner.layer)) + +NodeRuntime.runMain(NodeWorkerRunner.launch(WorkerLive)) diff --git a/repos/effect/packages/platform-node/package.json b/repos/effect/packages/platform-node/package.json new file mode 100644 index 0000000..18d83a3 --- /dev/null +++ b/repos/effect/packages/platform-node/package.json @@ -0,0 +1,76 @@ +{ + "name": "@effect/platform-node", + "type": "module", + "version": "0.106.0", + "license": "MIT", + "description": "Platform specific implementations for the Node.js runtime", + "homepage": "https://effect.website", + "repository": { + "type": "git", + "url": "https://github.com/Effect-TS/effect.git", + "directory": "packages/platform-node" + }, + "bugs": { + "url": "https://github.com/Effect-TS/effect/issues" + }, + "tags": [ + "node", + "typescript", + "algebraic-data-types", + "functional-programming" + ], + "keywords": [ + "node", + "typescript", + "algebraic-data-types", + "functional-programming" + ], + "publishConfig": { + "access": "public", + "provenance": true, + "directory": "dist", + "linkDirectory": false + }, + "engines": { + "node": ">=18.0.0" + }, + "exports": { + "./package.json": "./package.json", + ".": "./src/index.ts", + "./*": "./src/*.ts", + "./internal/*": null + }, + "scripts": { + "codegen": "build-utils prepare-v3", + "build": "pnpm build-esm && pnpm build-annotate && pnpm build-cjs && build-utils pack-v3", + "build-esm": "tsc -b tsconfig.build.json", + "build-cjs": "babel build/esm --plugins @babel/transform-export-namespace-from --plugins @babel/transform-modules-commonjs --out-dir build/cjs --source-maps", + "build-annotate": "babel build/esm --plugins annotate-pure-calls --out-dir build/esm --source-maps", + "check": "tsc -b tsconfig.json", + "test": "vitest", + "coverage": "vitest --coverage" + }, + "dependencies": { + "@effect/platform-node-shared": "workspace:^", + "mime": "^3.0.0", + "undici": "^7.10.0", + "ws": "^8.18.2" + }, + "peerDependencies": { + "@effect/cluster": "workspace:^", + "@effect/platform": "workspace:^", + "@effect/rpc": "workspace:^", + "@effect/sql": "workspace:^", + "effect": "workspace:^" + }, + "devDependencies": { + "@effect/cluster": "workspace:^", + "@effect/platform": "workspace:^", + "@effect/rpc": "workspace:^", + "@effect/sql": "workspace:^", + "@types/mime": "^3.0.4", + "@types/ws": "^8.18.1", + "effect": "workspace:^", + "vitest-websocket-mock": "^0.5.0" + } +} diff --git a/repos/effect/packages/platform-node/src/NodeClusterHttp.ts b/repos/effect/packages/platform-node/src/NodeClusterHttp.ts new file mode 100644 index 0000000..3fbfcc7 --- /dev/null +++ b/repos/effect/packages/platform-node/src/NodeClusterHttp.ts @@ -0,0 +1,138 @@ +/** + * @since 1.0.0 + */ +import * as HttpRunner from "@effect/cluster/HttpRunner" +import * as MessageStorage from "@effect/cluster/MessageStorage" +import * as RunnerHealth from "@effect/cluster/RunnerHealth" +import * as Runners from "@effect/cluster/Runners" +import * as RunnerStorage from "@effect/cluster/RunnerStorage" +import type { Sharding } from "@effect/cluster/Sharding" +import * as ShardingConfig from "@effect/cluster/ShardingConfig" +import * as SqlMessageStorage from "@effect/cluster/SqlMessageStorage" +import * as SqlRunnerStorage from "@effect/cluster/SqlRunnerStorage" +import type * as Etag from "@effect/platform/Etag" +import type { HttpPlatform } from "@effect/platform/HttpPlatform" +import type { HttpServer } from "@effect/platform/HttpServer" +import type { ServeError } from "@effect/platform/HttpServerError" +import * as RpcSerialization from "@effect/rpc/RpcSerialization" +import type { SqlClient } from "@effect/sql/SqlClient" +import type { ConfigError } from "effect/ConfigError" +import * as Effect from "effect/Effect" +import * as Layer from "effect/Layer" +import * as Option from "effect/Option" +import { createServer } from "node:http" +import { layerK8sHttpClient } from "./NodeClusterSocket.js" +import type { NodeContext } from "./NodeContext.js" +import * as NodeHttpClient from "./NodeHttpClient.js" +import * as NodeHttpServer from "./NodeHttpServer.js" +import * as NodeSocket from "./NodeSocket.js" + +export { + /** + * @since 1.0.0 + * @category Re-exports + */ + layerK8sHttpClient +} from "./NodeClusterSocket.js" + +/** + * @since 1.0.0 + * @category Layers + */ +export const layer = < + const ClientOnly extends boolean = false, + const Storage extends "local" | "sql" | "byo" = never, + const Health extends "ping" | "k8s" = never +>(options: { + readonly transport: "http" | "websocket" + readonly serialization?: "msgpack" | "ndjson" | undefined + readonly clientOnly?: ClientOnly | undefined + readonly storage?: Storage | undefined + readonly runnerHealth?: Health | undefined + readonly runnerHealthK8s?: { + readonly namespace?: string | undefined + readonly labelSelector?: string | undefined + } | undefined + readonly shardingConfig?: Partial | undefined +}): ClientOnly extends true ? Layer.Layer< + Sharding | Runners.Runners | ("byo" extends Storage ? never : MessageStorage.MessageStorage), + ConfigError, + "local" extends Storage ? never + : "byo" extends Storage ? (MessageStorage.MessageStorage | RunnerStorage.RunnerStorage) + : SqlClient + > : + Layer.Layer< + Sharding | Runners.Runners | ("byo" extends Storage ? never : MessageStorage.MessageStorage), + ServeError | ConfigError, + "local" extends Storage ? never + : "byo" extends Storage ? (MessageStorage.MessageStorage | RunnerStorage.RunnerStorage) + : SqlClient + > => +{ + const layer: Layer.Layer = options.clientOnly + // client only + ? options.transport === "http" + ? Layer.provide(HttpRunner.layerHttpClientOnly, NodeHttpClient.layerUndici) + : Layer.provide(HttpRunner.layerWebsocketClientOnly, NodeSocket.layerWebSocketConstructor) + // with server + : options.transport === "http" + ? Layer.provide(HttpRunner.layerHttp, [layerHttpServer, NodeHttpClient.layerUndici]) + : Layer.provide(HttpRunner.layerWebsocket, [layerHttpServer, NodeSocket.layerWebSocketConstructor]) + + const runnerHealth: Layer.Layer = options?.clientOnly + ? Layer.empty as any + : options?.runnerHealth === "k8s" + ? RunnerHealth.layerK8s(options.runnerHealthK8s).pipe( + Layer.provide(layerK8sHttpClient) + ) + : RunnerHealth.layerPing.pipe( + Layer.provide(Runners.layerRpc), + Layer.provide( + options.transport === "http" + ? HttpRunner.layerClientProtocolHttpDefault.pipe(Layer.provide(NodeHttpClient.layerUndici)) + : HttpRunner.layerClientProtocolWebsocketDefault.pipe(Layer.provide(NodeSocket.layerWebSocketConstructor)) + ) + ) + + return layer.pipe( + Layer.provide(runnerHealth), + Layer.provideMerge( + options?.storage === "local" + ? MessageStorage.layerNoop + : options?.storage === "byo" + ? Layer.empty + : Layer.orDie(SqlMessageStorage.layer) + ), + Layer.provide( + options?.storage === "local" + ? RunnerStorage.layerMemory + : options?.storage === "byo" + ? Layer.empty + : Layer.orDie(SqlRunnerStorage.layer) + ), + Layer.provide(ShardingConfig.layerFromEnv(options?.shardingConfig)), + Layer.provide( + options?.serialization === "ndjson" ? RpcSerialization.layerNdjson : RpcSerialization.layerMsgPack + ) + ) as any +} + +/** + * @since 1.0.0 + * @category Layers + */ +export const layerHttpServer: Layer.Layer< + | HttpPlatform + | Etag.Generator + | NodeContext + | HttpServer, + ServeError, + ShardingConfig.ShardingConfig +> = Effect.gen(function*() { + const config = yield* ShardingConfig.ShardingConfig + const listenAddress = Option.orElse(config.runnerListenAddress, () => config.runnerAddress) + if (listenAddress._tag === "None") { + return yield* Effect.die("NodeClusterHttp.layerHttpServer: ShardingConfig.runnerAddress is None") + } + return NodeHttpServer.layer(createServer, listenAddress.value) +}).pipe(Layer.unwrapEffect) diff --git a/repos/effect/packages/platform-node/src/NodeClusterSocket.ts b/repos/effect/packages/platform-node/src/NodeClusterSocket.ts new file mode 100644 index 0000000..7027eb6 --- /dev/null +++ b/repos/effect/packages/platform-node/src/NodeClusterSocket.ts @@ -0,0 +1,151 @@ +/** + * @since 1.0.0 + */ +import * as K8sHttpClient from "@effect/cluster/K8sHttpClient" +import * as MessageStorage from "@effect/cluster/MessageStorage" +import * as RunnerHealth from "@effect/cluster/RunnerHealth" +import * as Runners from "@effect/cluster/Runners" +import * as RunnerStorage from "@effect/cluster/RunnerStorage" +import type { Sharding } from "@effect/cluster/Sharding" +import * as ShardingConfig from "@effect/cluster/ShardingConfig" +import * as SocketRunner from "@effect/cluster/SocketRunner" +import * as SqlMessageStorage from "@effect/cluster/SqlMessageStorage" +import * as SqlRunnerStorage from "@effect/cluster/SqlRunnerStorage" +import { layerClientProtocol, layerSocketServer } from "@effect/platform-node-shared/NodeClusterSocket" +import * as FileSystem from "@effect/platform/FileSystem" +import type * as SocketServer from "@effect/platform/SocketServer" +import * as RpcSerialization from "@effect/rpc/RpcSerialization" +import type { SqlClient } from "@effect/sql/SqlClient" +import type { ConfigError } from "effect/ConfigError" +import * as Effect from "effect/Effect" +import * as Layer from "effect/Layer" +import * as NodeFileSystem from "./NodeFileSystem.js" +import * as NodeHttpClient from "./NodeHttpClient.js" +import * as Undici from "./Undici.js" + +export { + /** + * @since 1.0.0 + * @category Re-exports + */ + layerClientProtocol, + /** + * @since 1.0.0 + * @category Re-exports + */ + layerSocketServer +} + +/** + * @since 1.0.0 + * @category Layers + */ +export const layer = < + const ClientOnly extends boolean = false, + const Storage extends "local" | "sql" | "byo" = never, + const Health extends "ping" | "k8s" = never +>( + options?: { + readonly serialization?: "msgpack" | "ndjson" | undefined + readonly clientOnly?: ClientOnly | undefined + readonly storage?: Storage | undefined + readonly runnerHealth?: Health | undefined + readonly runnerHealthK8s?: { + readonly namespace?: string | undefined + readonly labelSelector?: string | undefined + } | undefined + readonly shardingConfig?: Partial | undefined + } +): ClientOnly extends true ? Layer.Layer< + Sharding | Runners.Runners | ("byo" extends Storage ? never : MessageStorage.MessageStorage), + ConfigError, + "local" extends Storage ? never + : "byo" extends Storage ? (MessageStorage.MessageStorage | RunnerStorage.RunnerStorage) + : SqlClient + > : + Layer.Layer< + Sharding | Runners.Runners | ("byo" extends Storage ? never : MessageStorage.MessageStorage), + SocketServer.SocketServerError | ConfigError, + "local" extends Storage ? never + : "byo" extends Storage ? (MessageStorage.MessageStorage | RunnerStorage.RunnerStorage) + : SqlClient + > => +{ + const layer: Layer.Layer = options?.clientOnly + // client only + ? Layer.provide(SocketRunner.layerClientOnly, layerClientProtocol) + // with server + : Layer.provide(SocketRunner.layer, [layerSocketServer, layerClientProtocol]) + + const runnerHealth: Layer.Layer = options?.clientOnly + ? Layer.empty as any + : options?.runnerHealth === "k8s" + ? RunnerHealth.layerK8s(options.runnerHealthK8s).pipe( + Layer.provide(layerK8sHttpClient) + ) + : RunnerHealth.layerPing.pipe( + Layer.provide(Runners.layerRpc), + Layer.provide(layerClientProtocol) + ) + + return layer.pipe( + Layer.provide(runnerHealth), + Layer.provideMerge( + options?.storage === "local" + ? MessageStorage.layerNoop + : options?.storage === "byo" + ? Layer.empty + : Layer.orDie(SqlMessageStorage.layer) + ), + Layer.provide( + options?.storage === "local" + ? RunnerStorage.layerMemory + : options?.storage === "byo" + ? Layer.empty + : Layer.orDie(SqlRunnerStorage.layer) + ), + Layer.provide(ShardingConfig.layerFromEnv(options?.shardingConfig)), + Layer.provide( + options?.serialization === "ndjson" ? RpcSerialization.layerNdjson : RpcSerialization.layerMsgPack + ) + ) as any +} + +/** + * @since 1.0.0 + * @category Layers + */ +export const layerDispatcherK8s: Layer.Layer = Layer.scoped(NodeHttpClient.Dispatcher)( + Effect.gen(function*() { + const fs = yield* FileSystem.FileSystem + const caCertOption = yield* fs.readFileString("/var/run/secrets/kubernetes.io/serviceaccount/ca.crt").pipe( + Effect.option + ) + if (caCertOption._tag === "Some") { + return yield* Effect.acquireRelease( + Effect.sync(() => + new Undici.Agent({ + connect: { + ca: caCertOption.value + } + }) + ), + (agent) => Effect.promise(() => agent.destroy()) + ) + } + + return yield* NodeHttpClient.makeDispatcher + }) +).pipe( + Layer.provide(NodeFileSystem.layer) +) + +/** + * @since 1.0.0 + * @category Layers + */ +export const layerK8sHttpClient: Layer.Layer = K8sHttpClient.layer.pipe( + Layer.provide(Layer.fresh(NodeHttpClient.layerUndiciWithoutDispatcher)), + Layer.provide(layerDispatcherK8s), + Layer.provide(NodeFileSystem.layer) +) diff --git a/repos/effect/packages/platform-node/src/NodeCommandExecutor.ts b/repos/effect/packages/platform-node/src/NodeCommandExecutor.ts new file mode 100644 index 0000000..7e47b1c --- /dev/null +++ b/repos/effect/packages/platform-node/src/NodeCommandExecutor.ts @@ -0,0 +1,13 @@ +/** + * @since 1.0.0 + */ +import * as NodeCommandExecutor from "@effect/platform-node-shared/NodeCommandExecutor" +import type { CommandExecutor } from "@effect/platform/CommandExecutor" +import type { FileSystem } from "@effect/platform/FileSystem" +import type { Layer } from "effect/Layer" + +/** + * @since 1.0.0 + * @category layer + */ +export const layer: Layer = NodeCommandExecutor.layer diff --git a/repos/effect/packages/platform-node/src/NodeContext.ts b/repos/effect/packages/platform-node/src/NodeContext.ts new file mode 100644 index 0000000..1566bee --- /dev/null +++ b/repos/effect/packages/platform-node/src/NodeContext.ts @@ -0,0 +1,40 @@ +/** + * @since 1.0.0 + */ +import * as NodeCommandExecutor from "@effect/platform-node-shared/NodeCommandExecutor" +import * as NodeFileSystem from "@effect/platform-node-shared/NodeFileSystem" +import * as NodePath from "@effect/platform-node-shared/NodePath" +import * as NodeTerminal from "@effect/platform-node-shared/NodeTerminal" +import type * as CommandExecutor from "@effect/platform/CommandExecutor" +import type * as FileSystem from "@effect/platform/FileSystem" +import type * as Path from "@effect/platform/Path" +import type * as Terminal from "@effect/platform/Terminal" +import type * as Worker from "@effect/platform/Worker" +import { pipe } from "effect/Function" +import * as Layer from "effect/Layer" +import * as NodeWorker from "./NodeWorker.js" + +/** + * @since 1.0.0 + * @category models + */ +export type NodeContext = + | CommandExecutor.CommandExecutor + | FileSystem.FileSystem + | Path.Path + | Terminal.Terminal + | Worker.WorkerManager + +/** + * @since 1.0.0 + * @category layer + */ +export const layer: Layer.Layer = pipe( + Layer.mergeAll( + NodePath.layer, + NodeCommandExecutor.layer, + NodeTerminal.layer, + NodeWorker.layerManager + ), + Layer.provideMerge(NodeFileSystem.layer) +) diff --git a/repos/effect/packages/platform-node/src/NodeFileSystem.ts b/repos/effect/packages/platform-node/src/NodeFileSystem.ts new file mode 100644 index 0000000..982eefa --- /dev/null +++ b/repos/effect/packages/platform-node/src/NodeFileSystem.ts @@ -0,0 +1,12 @@ +/** + * @since 1.0.0 + */ +import * as NodeFileSystem from "@effect/platform-node-shared/NodeFileSystem" +import type { FileSystem } from "@effect/platform/FileSystem" +import type { Layer } from "effect/Layer" + +/** + * @since 1.0.0 + * @category layer + */ +export const layer: Layer = NodeFileSystem.layer diff --git a/repos/effect/packages/platform-node/src/NodeFileSystem/ParcelWatcher.ts b/repos/effect/packages/platform-node/src/NodeFileSystem/ParcelWatcher.ts new file mode 100644 index 0000000..ce126e6 --- /dev/null +++ b/repos/effect/packages/platform-node/src/NodeFileSystem/ParcelWatcher.ts @@ -0,0 +1,12 @@ +/** + * @since 1.0.0 + */ +import * as ParcelWatcher from "@effect/platform-node-shared/NodeFileSystem/ParcelWatcher" +import type { WatchBackend } from "@effect/platform/FileSystem" +import type { Layer } from "effect/Layer" + +/** + * @since 1.0.0 + * @category layer + */ +export const layer: Layer = ParcelWatcher.layer diff --git a/repos/effect/packages/platform-node/src/NodeHttpClient.ts b/repos/effect/packages/platform-node/src/NodeHttpClient.ts new file mode 100644 index 0000000..98943ce --- /dev/null +++ b/repos/effect/packages/platform-node/src/NodeHttpClient.ts @@ -0,0 +1,138 @@ +/** + * @since 1.0.0 + */ +import type * as Client from "@effect/platform/HttpClient" +import * as Context from "effect/Context" +import type * as Effect from "effect/Effect" +import type * as Layer from "effect/Layer" +import type * as Scope from "effect/Scope" +import type * as Http from "node:http" +import type * as Https from "node:https" +import * as internal from "./internal/httpClient.js" +import * as internalUndici from "./internal/httpClientUndici.js" +import type * as Undici from "./Undici.js" + +/** + * @since 1.0.0 + * @category agent + */ +export const HttpAgentTypeId: unique symbol = internal.HttpAgentTypeId + +/** + * @since 1.0.0 + * @category agent + */ +export type HttpAgentTypeId = typeof HttpAgentTypeId + +/** + * @since 1.0.0 + * @category agent + */ +export interface HttpAgent { + readonly [HttpAgentTypeId]: typeof HttpAgentTypeId + readonly http: Http.Agent + readonly https: Https.Agent +} + +/** + * @since 1.0.0 + * @category agent + */ +export const HttpAgent: Context.Tag = internal.HttpAgent + +/** + * @since 1.0.0 + * @category agent + */ +export const makeAgent: (options?: Https.AgentOptions) => Effect.Effect = + internal.makeAgent + +/** + * @since 1.0.0 + * @category agent + */ +export const agentLayer: Layer.Layer = internal.agentLayer + +/** + * @since 1.0.0 + * @category agent + */ +export const makeAgentLayer: (options?: Https.AgentOptions) => Layer.Layer = internal.makeAgentLayer + +/** + * @since 1.0.0 + * @category constructors + */ +export const make: Effect.Effect = internal.make + +/** + * @since 1.0.0 + * @category layers + */ +export const layer: Layer.Layer = internal.layer + +/** + * @since 1.0.0 + * @category layers + */ +export const layerWithoutAgent: Layer.Layer = internal.layerWithoutAgent + +/** + * @since 1.0.0 + * @category undici + */ +export interface Dispatcher { + readonly _: unique symbol +} + +/** + * @since 1.0.0 + * @category undici + */ +export const Dispatcher: Context.Tag = internalUndici.Dispatcher + +/** + * @since 1.0.0 + * @category undici + */ +export const makeDispatcher: Effect.Effect = internalUndici.makeDispatcher + +/** + * @since 1.0.0 + * @category undici + */ +export const dispatcherLayer: Layer.Layer = internalUndici.dispatcherLayer + +/** + * @since 1.0.0 + * @category undici + */ +export const dispatcherLayerGlobal: Layer.Layer = internalUndici.dispatcherLayerGlobal + +/** + * @since 1.0.0 + * @category undici + */ +export class UndiciRequestOptions extends Context.Tag(internalUndici.undiciOptionsTagKey)< + UndiciRequestOptions, + Undici.Dispatcher.RequestOptions +>() {} + +/** + * @since 1.0.0 + * @category constructors + */ +export const makeUndici: (dispatcher: Undici.Dispatcher) => Client.HttpClient = internalUndici.make + +/** + * @since 1.0.0 + * @category layers + */ +export const layerUndici: Layer.Layer = internalUndici.layer + +/** + * @since 1.0.0 + * @category layers + */ +export const layerUndiciWithoutDispatcher: Layer.Layer = + internalUndici.layerWithoutDispatcher diff --git a/repos/effect/packages/platform-node/src/NodeHttpPlatform.ts b/repos/effect/packages/platform-node/src/NodeHttpPlatform.ts new file mode 100644 index 0000000..b85156f --- /dev/null +++ b/repos/effect/packages/platform-node/src/NodeHttpPlatform.ts @@ -0,0 +1,21 @@ +/** + * @since 1.0.0 + */ +import type * as Etag from "@effect/platform/Etag" +import type * as FileSystem from "@effect/platform/FileSystem" +import type * as Platform from "@effect/platform/HttpPlatform" +import type * as Effect from "effect/Effect" +import type * as Layer from "effect/Layer" +import * as internal from "./internal/httpPlatform.js" + +/** + * @since 1.0.0 + * @category constructors + */ +export const make: Effect.Effect = internal.make + +/** + * @since 1.0.0 + * @category layers + */ +export const layer: Layer.Layer = internal.layer diff --git a/repos/effect/packages/platform-node/src/NodeHttpServer.ts b/repos/effect/packages/platform-node/src/NodeHttpServer.ts new file mode 100644 index 0000000..1ae7efc --- /dev/null +++ b/repos/effect/packages/platform-node/src/NodeHttpServer.ts @@ -0,0 +1,134 @@ +/** + * @since 1.0.0 + */ +import type * as Etag from "@effect/platform/Etag" +import type * as App from "@effect/platform/HttpApp" +import type * as HttpClient from "@effect/platform/HttpClient" +import type * as Middleware from "@effect/platform/HttpMiddleware" +import type * as Platform from "@effect/platform/HttpPlatform" +import type * as Server from "@effect/platform/HttpServer" +import type { ServeError } from "@effect/platform/HttpServerError" +import type * as ServerRequest from "@effect/platform/HttpServerRequest" +import type * as Config from "effect/Config" +import type * as ConfigError from "effect/ConfigError" +import type * as Effect from "effect/Effect" +import type { LazyArg } from "effect/Function" +import type * as Layer from "effect/Layer" +import type * as Scope from "effect/Scope" +import type * as Http from "node:http" +import type * as Net from "node:net" +import * as internal from "./internal/httpServer.js" +import type * as NodeContext from "./NodeContext.js" + +/** + * @since 1.0.0 + * @category constructors + */ +export const make: ( + evaluate: LazyArg>, + options: Net.ListenOptions +) => Effect.Effect< + Server.HttpServer, + ServeError, + Scope.Scope +> = internal.make + +/** + * @since 1.0.0 + * @category constructors + */ +export const makeHandler: { + ( + httpApp: App.Default + ): Effect.Effect< + (nodeRequest: Http.IncomingMessage, nodeResponse: Http.ServerResponse) => void, + never, + Exclude + > + >( + httpApp: App.Default, + middleware: Middleware.HttpMiddleware.Applied + ): Effect.Effect< + (nodeRequest: Http.IncomingMessage, nodeResponse: Http.ServerResponse) => void, + never, + Exclude, ServerRequest.HttpServerRequest | Scope.Scope> + > +} = internal.makeHandler + +/** + * @since 1.0.0 + * @category layers + */ +export const layerServer: ( + evaluate: LazyArg, + options: Net.ListenOptions +) => Layer.Layer = internal.layerServer + +/** + * @since 1.0.0 + * @category layers + */ +export const layer: ( + evaluate: LazyArg>, + options: Net.ListenOptions +) => Layer.Layer = + internal.layer + +/** + * @since 1.0.0 + * @category layers + */ +export const layerConfig: ( + evaluate: LazyArg>, + options: Config.Config.Wrap +) => Layer.Layer< + Platform.HttpPlatform | Etag.Generator | NodeContext.NodeContext | Server.HttpServer, + ConfigError.ConfigError | ServeError +> = internal.layerConfig + +/** + * Layer starting a server on a random port and producing an `HttpClient` + * with prepended url of the running http server. + * + * **Example** + * + * ```ts + * import * as assert from "node:assert" + * import { HttpClient, HttpRouter, HttpServer } from "@effect/platform" + * import { NodeHttpServer } from "@effect/platform-node" + * import { Effect } from "effect" + * + * Effect.gen(function*() { + * yield* HttpServer.serveEffect(HttpRouter.empty) + * const response = yield* HttpClient.get("/") + * assert.strictEqual(response.status, 404) + * }).pipe(Effect.provide(NodeHttpServer.layerTest)) + * ``` + * + * @since 1.0.0 + * @category layers + */ +export const layerTest: Layer.Layer< + | HttpClient.HttpClient + | Server.HttpServer + | Platform.HttpPlatform + | Etag.Generator + | NodeContext.NodeContext, + ServeError +> = internal.layerTest + +/** + * A Layer providing the `HttpPlatform`, `FileSystem`, `Etag.Generator`, and `Path` + * services. + * + * The `FileSystem` service is a no-op implementation, so this layer is only + * useful for platforms that have no file system. + * + * @since 1.0.0 + * @category layers + */ +export const layerContext: Layer.Layer< + | Platform.HttpPlatform + | Etag.Generator + | NodeContext.NodeContext +> = internal.layerContext diff --git a/repos/effect/packages/platform-node/src/NodeHttpServerRequest.ts b/repos/effect/packages/platform-node/src/NodeHttpServerRequest.ts new file mode 100644 index 0000000..7f013f3 --- /dev/null +++ b/repos/effect/packages/platform-node/src/NodeHttpServerRequest.ts @@ -0,0 +1,20 @@ +/** + * @since 1.0.0 + */ +import type * as ServerRequest from "@effect/platform/HttpServerRequest" +import type * as Http from "node:http" +import * as internal from "./internal/httpServer.js" + +/** + * @category conversions + * @since 1.0.0 + */ +export const toIncomingMessage: (self: ServerRequest.HttpServerRequest) => Http.IncomingMessage = + internal.toIncomingMessage + +/** + * @category conversions + * @since 1.0.0 + */ +export const toServerResponse: (self: ServerRequest.HttpServerRequest) => Http.ServerResponse = + internal.toServerResponse diff --git a/repos/effect/packages/platform-node/src/NodeKeyValueStore.ts b/repos/effect/packages/platform-node/src/NodeKeyValueStore.ts new file mode 100644 index 0000000..bf8f3a3 --- /dev/null +++ b/repos/effect/packages/platform-node/src/NodeKeyValueStore.ts @@ -0,0 +1,15 @@ +/** + * @since 1.0.0 + */ +import * as KVSN from "@effect/platform-node-shared/NodeKeyValueStore" +import type * as PlatformError from "@effect/platform/Error" +import type * as KeyValueStore from "@effect/platform/KeyValueStore" +import type * as Layer from "effect/Layer" + +/** + * @since 1.0.0 + * @category layers + */ +export const layerFileSystem: ( + directory: string +) => Layer.Layer = KVSN.layerFileSystem diff --git a/repos/effect/packages/platform-node/src/NodeMultipart.ts b/repos/effect/packages/platform-node/src/NodeMultipart.ts new file mode 100644 index 0000000..7d38dc2 --- /dev/null +++ b/repos/effect/packages/platform-node/src/NodeMultipart.ts @@ -0,0 +1,8 @@ +/** + * @since 1.0.0 + */ + +/** + * @since 1.0.0 + */ +export * from "@effect/platform-node-shared/NodeMultipart" diff --git a/repos/effect/packages/platform-node/src/NodePath.ts b/repos/effect/packages/platform-node/src/NodePath.ts new file mode 100644 index 0000000..1097c6d --- /dev/null +++ b/repos/effect/packages/platform-node/src/NodePath.ts @@ -0,0 +1,25 @@ +/** + * @since 1.0.0 + */ + +import * as NodePath from "@effect/platform-node-shared/NodePath" +import type { Path } from "@effect/platform/Path" +import type { Layer } from "effect/Layer" + +/** + * @since 1.0.0 + * @category layer + */ +export const layer: Layer = NodePath.layer + +/** + * @since 1.0.0 + * @category layer + */ +export const layerPosix: Layer = NodePath.layerPosix + +/** + * @since 1.0.0 + * @category layer + */ +export const layerWin32: Layer = NodePath.layerWin32 diff --git a/repos/effect/packages/platform-node/src/NodeRuntime.ts b/repos/effect/packages/platform-node/src/NodeRuntime.ts new file mode 100644 index 0000000..d6c012f --- /dev/null +++ b/repos/effect/packages/platform-node/src/NodeRuntime.ts @@ -0,0 +1,11 @@ +/** + * @since 1.0.0 + */ +import * as NodeRuntime from "@effect/platform-node-shared/NodeRuntime" +import type { RunMain } from "@effect/platform/Runtime" + +/** + * @since 1.0.0 + * @category runtime + */ +export const runMain: RunMain = NodeRuntime.runMain diff --git a/repos/effect/packages/platform-node/src/NodeSink.ts b/repos/effect/packages/platform-node/src/NodeSink.ts new file mode 100644 index 0000000..78c09ed --- /dev/null +++ b/repos/effect/packages/platform-node/src/NodeSink.ts @@ -0,0 +1,8 @@ +/** + * @since 1.0.0 + */ + +/** + * @since 1.0.0 + */ +export * from "@effect/platform-node-shared/NodeSink" diff --git a/repos/effect/packages/platform-node/src/NodeSocket.ts b/repos/effect/packages/platform-node/src/NodeSocket.ts new file mode 100644 index 0000000..08fc8c4 --- /dev/null +++ b/repos/effect/packages/platform-node/src/NodeSocket.ts @@ -0,0 +1,36 @@ +/** + * @since 1.0.0 + */ +import * as Socket from "@effect/platform/Socket" +import * as Layer from "effect/Layer" +import * as WS from "ws" + +/** + * @since 1.0.0 + */ +export * from "@effect/platform-node-shared/NodeSocket" + +/** + * @since 1.0.0 + * @category layers + */ +export const layerWebSocket = (url: string, options?: { + readonly closeCodeIsError?: (code: number) => boolean +}): Layer.Layer => + Layer.scoped(Socket.Socket, Socket.makeWebSocket(url, options)).pipe( + Layer.provide(layerWebSocketConstructor) + ) + +/** + * @since 1.0.0 + * @category layers + */ +export const layerWebSocketConstructor: Layer.Layer = Layer.sync( + Socket.WebSocketConstructor, + () => { + if ("WebSocket" in globalThis) { + return (url, protocols) => new globalThis.WebSocket(url, protocols) + } + return (url, protocols) => new WS.WebSocket(url, protocols) as unknown as globalThis.WebSocket + } +) diff --git a/repos/effect/packages/platform-node/src/NodeSocketServer.ts b/repos/effect/packages/platform-node/src/NodeSocketServer.ts new file mode 100644 index 0000000..3c7ec9e --- /dev/null +++ b/repos/effect/packages/platform-node/src/NodeSocketServer.ts @@ -0,0 +1,8 @@ +/** + * @since 1.0.0 + */ + +/** + * @since 1.0.0 + */ +export * from "@effect/platform-node-shared/NodeSocketServer" diff --git a/repos/effect/packages/platform-node/src/NodeStream.ts b/repos/effect/packages/platform-node/src/NodeStream.ts new file mode 100644 index 0000000..0f143b8 --- /dev/null +++ b/repos/effect/packages/platform-node/src/NodeStream.ts @@ -0,0 +1,9 @@ +/** + * @since 1.0.0 + */ + +/** + * @since 1.0.0 + * @category re-exports + */ +export * from "@effect/platform-node-shared/NodeStream" diff --git a/repos/effect/packages/platform-node/src/NodeTerminal.ts b/repos/effect/packages/platform-node/src/NodeTerminal.ts new file mode 100644 index 0000000..e128394 --- /dev/null +++ b/repos/effect/packages/platform-node/src/NodeTerminal.ts @@ -0,0 +1,20 @@ +/** + * @since 1.0.0 + */ +import * as NodeTerminal from "@effect/platform-node-shared/NodeTerminal" +import type { Terminal, UserInput } from "@effect/platform/Terminal" +import type { Effect } from "effect/Effect" +import type { Layer } from "effect/Layer" +import type { Scope } from "effect/Scope" + +/** + * @since 1.0.0 + * @category constructors + */ +export const make: (shouldQuit?: (input: UserInput) => boolean) => Effect = NodeTerminal.make + +/** + * @since 1.0.0 + * @category layer + */ +export const layer: Layer = NodeTerminal.layer diff --git a/repos/effect/packages/platform-node/src/NodeWorker.ts b/repos/effect/packages/platform-node/src/NodeWorker.ts new file mode 100644 index 0000000..99e98d6 --- /dev/null +++ b/repos/effect/packages/platform-node/src/NodeWorker.ts @@ -0,0 +1,36 @@ +/** + * @since 1.0.0 + */ +import type * as Worker from "@effect/platform/Worker" +import type * as Layer from "effect/Layer" +import type * as ChildProcess from "node:child_process" +import type * as WorkerThreads from "node:worker_threads" +import * as internal from "./internal/worker.js" + +/** + * @since 1.0.0 + * @category layers + */ +export const layerManager: Layer.Layer = internal.layerManager + +/** + * @since 1.0.0 + * @category layers + */ +export const layerWorker: Layer.Layer = internal.layerWorker + +/** + * @since 1.0.0 + * @category layers + */ +export const layer: ( + spawn: (id: number) => WorkerThreads.Worker | ChildProcess.ChildProcess +) => Layer.Layer = internal.layer + +/** + * @since 1.0.0 + * @category layers + */ +export const layerPlatform: ( + spawn: (id: number) => WorkerThreads.Worker | ChildProcess.ChildProcess +) => Layer.Layer = internal.layerPlatform diff --git a/repos/effect/packages/platform-node/src/NodeWorkerRunner.ts b/repos/effect/packages/platform-node/src/NodeWorkerRunner.ts new file mode 100644 index 0000000..4788e62 --- /dev/null +++ b/repos/effect/packages/platform-node/src/NodeWorkerRunner.ts @@ -0,0 +1,20 @@ +/** + * @since 1.0.0 + */ +import type * as Runner from "@effect/platform/WorkerRunner" +import type * as Layer from "effect/Layer" +import * as internal from "./internal/workerRunner.js" + +export { + /** + * @since 1.0.0 + * @category re-exports + */ + launch +} from "@effect/platform/WorkerRunner" + +/** + * @since 1.0.0 + * @category layers + */ +export const layer: Layer.Layer = internal.layer diff --git a/repos/effect/packages/platform-node/src/Undici.ts b/repos/effect/packages/platform-node/src/Undici.ts new file mode 100644 index 0000000..a61361d --- /dev/null +++ b/repos/effect/packages/platform-node/src/Undici.ts @@ -0,0 +1,16 @@ +/** + * @since 1.0.0 + */ + +export { + /** + * @since 1.0.0 + * @category undici + */ + default +} from "undici" +/** + * @since 1.0.0 + * @category undici + */ +export * from "undici" diff --git a/repos/effect/packages/platform-node/src/index.ts b/repos/effect/packages/platform-node/src/index.ts new file mode 100644 index 0000000..36ef345 --- /dev/null +++ b/repos/effect/packages/platform-node/src/index.ts @@ -0,0 +1,104 @@ +/** + * @since 1.0.0 + */ +export * as NodeClusterHttp from "./NodeClusterHttp.js" + +/** + * @since 1.0.0 + */ +export * as NodeClusterSocket from "./NodeClusterSocket.js" + +/** + * @since 1.0.0 + */ +export * as NodeCommandExecutor from "./NodeCommandExecutor.js" + +/** + * @since 1.0.0 + */ +export * as NodeContext from "./NodeContext.js" + +/** + * @since 1.0.0 + */ +export * as NodeFileSystem from "./NodeFileSystem.js" + +/** + * @since 1.0.0 + */ +export * as NodeHttpClient from "./NodeHttpClient.js" + +/** + * @since 1.0.0 + */ +export * as NodeHttpPlatform from "./NodeHttpPlatform.js" + +/** + * @since 1.0.0 + */ +export * as NodeHttpServer from "./NodeHttpServer.js" + +/** + * @since 1.0.0 + */ +export * as NodeHttpServerRequest from "./NodeHttpServerRequest.js" + +/** + * @since 1.0.0 + */ +export * as NodeKeyValueStore from "./NodeKeyValueStore.js" + +/** + * @since 1.0.0 + */ +export * as NodeMultipart from "./NodeMultipart.js" + +/** + * @since 1.0.0 + */ +export * as NodePath from "./NodePath.js" + +/** + * @since 1.0.0 + */ +export * as NodeRuntime from "./NodeRuntime.js" + +/** + * @since 1.0.0 + */ +export * as NodeSink from "./NodeSink.js" + +/** + * @since 1.0.0 + */ +export * as NodeSocket from "./NodeSocket.js" + +/** + * @since 1.0.0 + */ +export * as NodeSocketServer from "./NodeSocketServer.js" + +/** + * @since 1.0.0 + */ +export * as NodeStream from "./NodeStream.js" + +/** + * @since 1.0.0 + */ +export * as NodeTerminal from "./NodeTerminal.js" + +/** + * @since 1.0.0 + */ +export * as NodeWorker from "./NodeWorker.js" + +/** + * @since 1.0.0 + */ +export * as NodeWorkerRunner from "./NodeWorkerRunner.js" + +/** + * @since 1.0.0 + */ +export * as Undici from "./Undici.js" diff --git a/repos/effect/packages/platform-node/src/internal/httpClient.ts b/repos/effect/packages/platform-node/src/internal/httpClient.ts new file mode 100644 index 0000000..b9da0d7 --- /dev/null +++ b/repos/effect/packages/platform-node/src/internal/httpClient.ts @@ -0,0 +1,260 @@ +import * as Cookies from "@effect/platform/Cookies" +import type * as Body from "@effect/platform/HttpBody" +import * as Client from "@effect/platform/HttpClient" +import * as Error from "@effect/platform/HttpClientError" +import type * as ClientRequest from "@effect/platform/HttpClientRequest" +import * as ClientResponse from "@effect/platform/HttpClientResponse" +import * as IncomingMessage from "@effect/platform/HttpIncomingMessage" +import * as Context from "effect/Context" +import * as Effect from "effect/Effect" +import { pipe } from "effect/Function" +import * as Layer from "effect/Layer" +import type * as Scope from "effect/Scope" +import * as Stream from "effect/Stream" +import * as Http from "node:http" +import * as Https from "node:https" +import { Readable } from "node:stream" +import { pipeline } from "node:stream/promises" +import type * as NodeClient from "../NodeHttpClient.js" +import * as NodeSink from "../NodeSink.js" +import { HttpIncomingMessageImpl } from "./httpIncomingMessage.js" + +/** @internal */ +export const HttpAgentTypeId: NodeClient.HttpAgentTypeId = Symbol.for( + "@effect/platform-node/Http/NodeClient/HttpAgent" +) as NodeClient.HttpAgentTypeId + +/** @internal */ +export const HttpAgent = Context.GenericTag("@effect/platform-node/Http/NodeClient/HttpAgent") + +/** @internal */ +export const makeAgent = (options?: Https.AgentOptions): Effect.Effect => + Effect.map( + Effect.all([ + Effect.acquireRelease( + Effect.sync(() => new Http.Agent(options)), + (agent) => Effect.sync(() => agent.destroy()) + ), + Effect.acquireRelease( + Effect.sync(() => new Https.Agent(options)), + (agent) => Effect.sync(() => agent.destroy()) + ) + ]), + ([http, https]) => ({ + [HttpAgentTypeId]: HttpAgentTypeId, + http, + https + }) + ) + +/** @internal */ +export const makeAgentLayer = (options?: Https.AgentOptions): Layer.Layer => + Layer.scoped(HttpAgent, makeAgent(options)) + +/** @internal */ +export const agentLayer = makeAgentLayer() + +const fromAgent = (agent: NodeClient.HttpAgent): Client.HttpClient => + Client.make((request, url, signal) => { + const nodeRequest = url.protocol === "https:" ? + Https.request(url, { + agent: agent.https, + method: request.method, + headers: request.headers, + signal + }) : + Http.request(url, { + agent: agent.http, + method: request.method, + headers: request.headers, + signal + }) + return pipe( + Effect.zipRight(sendBody(nodeRequest, request, request.body), waitForResponse(nodeRequest, request), { + concurrent: true + }), + Effect.map((_) => new ClientResponseImpl(request, _)) + ) + }) + +const sendBody = ( + nodeRequest: Http.ClientRequest, + request: ClientRequest.HttpClientRequest, + body: Body.HttpBody +): Effect.Effect => + Effect.suspend((): Effect.Effect => { + switch (body._tag) { + case "Empty": { + nodeRequest.end() + return waitForFinish(nodeRequest, request) + } + case "Uint8Array": + case "Raw": { + nodeRequest.end(body.body) + return waitForFinish(nodeRequest, request) + } + case "FormData": { + const response = new Response(body.formData) + + response.headers.forEach((value, key) => { + nodeRequest.setHeader(key, value) + }) + + return Effect.tryPromise({ + try: () => pipeline(Readable.fromWeb(response.body! as any), nodeRequest), + catch: (cause) => + new Error.RequestError({ + request, + reason: "Transport", + cause + }) + }) + } + case "Stream": { + return Stream.run( + Stream.mapError(body.stream, (cause) => + new Error.RequestError({ + request, + reason: "Encode", + cause + })), + NodeSink.fromWritable(() => nodeRequest, (cause) => + new Error.RequestError({ + request, + reason: "Transport", + cause + })) + ) + } + } + }) + +const waitForResponse = (nodeRequest: Http.ClientRequest, request: ClientRequest.HttpClientRequest) => + Effect.async((resume) => { + function onError(cause: Error) { + resume(Effect.fail( + new Error.RequestError({ + request, + reason: "Transport", + cause + }) + )) + } + nodeRequest.on("error", onError) + + function onResponse(response: Http.IncomingMessage) { + nodeRequest.off("error", onError) + resume(Effect.succeed(response)) + } + nodeRequest.on("upgrade", onResponse) + nodeRequest.on("response", onResponse) + + return Effect.sync(() => { + nodeRequest.off("error", onError) + nodeRequest.off("upgrade", onResponse) + nodeRequest.off("response", onResponse) + }) + }) + +const waitForFinish = (nodeRequest: Http.ClientRequest, request: ClientRequest.HttpClientRequest) => + Effect.async((resume) => { + function onError(cause: Error) { + resume(Effect.fail( + new Error.RequestError({ + request, + reason: "Transport", + cause + }) + )) + } + nodeRequest.once("error", onError) + + function onFinish() { + nodeRequest.off("error", onError) + resume(Effect.void) + } + nodeRequest.once("finish", onFinish) + + return Effect.sync(() => { + nodeRequest.off("error", onError) + nodeRequest.off("finish", onFinish) + }) + }) + +class ClientResponseImpl extends HttpIncomingMessageImpl + implements ClientResponse.HttpClientResponse +{ + readonly [ClientResponse.TypeId]: ClientResponse.TypeId + + constructor( + readonly request: ClientRequest.HttpClientRequest, + source: Http.IncomingMessage + ) { + super(source, (cause) => + new Error.ResponseError({ + request, + response: this, + reason: "Decode", + cause + })) + this[ClientResponse.TypeId] = ClientResponse.TypeId + } + + get status() { + return this.source.statusCode! + } + + cachedCookies?: Cookies.Cookies + get cookies(): Cookies.Cookies { + if (this.cachedCookies !== undefined) { + return this.cachedCookies + } + const header = this.source.headers["set-cookie"] + if (Array.isArray(header)) { + return this.cachedCookies = Cookies.fromSetCookie(header) + } + return this.cachedCookies = Cookies.empty + } + + get formData(): Effect.Effect { + return Effect.tryPromise({ + try: () => { + const init: { + headers: HeadersInit + status?: number + statusText?: string + } = { + headers: new globalThis.Headers(this.source.headers as any) + } + + if (this.source.statusCode) { + init.status = this.source.statusCode + } + + if (this.source.statusMessage) { + init.statusText = this.source.statusMessage + } + + return new Response(Readable.toWeb(this.source) as any, init).formData() + }, + catch: this.onError + }) + } + + toJSON(): unknown { + return IncomingMessage.inspect(this, { + _id: "@effect/platform/HttpClientResponse", + request: this.request.toJSON(), + status: this.status + }) + } +} + +/** @internal */ +export const make = Effect.map(HttpAgent, fromAgent) + +/** @internal */ +export const layerWithoutAgent = Client.layerMergedContext(make) + +/** @internal */ +export const layer = Layer.provide(layerWithoutAgent, agentLayer) diff --git a/repos/effect/packages/platform-node/src/internal/httpClientUndici.ts b/repos/effect/packages/platform-node/src/internal/httpClientUndici.ts new file mode 100644 index 0000000..3985cd1 --- /dev/null +++ b/repos/effect/packages/platform-node/src/internal/httpClientUndici.ts @@ -0,0 +1,231 @@ +import * as Cookies from "@effect/platform/Cookies" +import * as Headers from "@effect/platform/Headers" +import type * as Body from "@effect/platform/HttpBody" +import * as Client from "@effect/platform/HttpClient" +import * as Error from "@effect/platform/HttpClientError" +import type * as ClientRequest from "@effect/platform/HttpClientRequest" +import * as ClientResponse from "@effect/platform/HttpClientResponse" +import * as IncomingMessage from "@effect/platform/HttpIncomingMessage" +import * as UrlParams from "@effect/platform/UrlParams" +import * as Context from "effect/Context" +import * as Effect from "effect/Effect" +import * as FiberRef from "effect/FiberRef" +import * as Inspectable from "effect/Inspectable" +import * as Layer from "effect/Layer" +import * as Option from "effect/Option" +import type * as Scope from "effect/Scope" +import type * as Stream from "effect/Stream" +import type * as NodeClient from "../NodeHttpClient.js" +import * as NodeStream from "../NodeStream.js" +import * as Undici from "../Undici.js" + +/** @internal */ +export const Dispatcher = Context.GenericTag( + "@effect/platform-node/NodeHttpClient/Dispatcher" +) + +/** @internal */ +export const makeDispatcher: Effect.Effect = Effect.acquireRelease( + Effect.sync(() => new Undici.Agent()), + (dispatcher) => Effect.promise(() => dispatcher.destroy()) +) + +/** @internal */ +export const dispatcherLayer = Layer.scoped(Dispatcher, makeDispatcher) + +/** @internal */ +export const dispatcherLayerGlobal = Layer.sync(Dispatcher, () => Undici.getGlobalDispatcher()) + +/** @internal */ +export const undiciOptionsTagKey = "@effect/platform-node/NodeHttpClient/undiciOptions" + +/** @internal */ +export const make = (dispatcher: Undici.Dispatcher): Client.HttpClient => + Client.make((request, url, signal, fiber) => { + const context = fiber.getFiberRef(FiberRef.currentContext) + const options: Undici.Dispatcher.RequestOptions = context.unsafeMap.get(undiciOptionsTagKey) ?? {} + return convertBody(request.body).pipe( + Effect.flatMap((body) => + Effect.tryPromise({ + try: () => + dispatcher.request({ + ...options, + signal, + method: request.method, + headers: request.headers, + origin: url.origin, + path: url.pathname + url.search + url.hash, + body, + // leave timeouts to Effect.timeout etc + headersTimeout: 60 * 60 * 1000, + bodyTimeout: 0 + }), + catch: (cause) => + new Error.RequestError({ + request, + reason: "Transport", + cause + }) + }) + ), + Effect.map((response) => new ClientResponseImpl(request, response)) + ) + }) + +function convertBody( + body: Body.HttpBody +): Effect.Effect> { + switch (body._tag) { + case "Empty": { + return Effect.succeed(null) + } + case "Uint8Array": + case "Raw": { + return Effect.succeed(body.body as Uint8Array) + } + case "FormData": { + return Effect.succeed(body.formData as Undici.FormData) + } + case "Stream": { + return NodeStream.toReadable(body.stream) + } + } +} + +function noopErrorHandler(_: any) {} + +class ClientResponseImpl extends Inspectable.Class implements ClientResponse.HttpClientResponse { + readonly [IncomingMessage.TypeId]: IncomingMessage.TypeId + readonly [ClientResponse.TypeId]: ClientResponse.TypeId + + constructor( + readonly request: ClientRequest.HttpClientRequest, + readonly source: Undici.Dispatcher.ResponseData + ) { + super() + this[IncomingMessage.TypeId] = IncomingMessage.TypeId + this[ClientResponse.TypeId] = ClientResponse.TypeId + source.body.on("error", noopErrorHandler) + } + + get status() { + return this.source.statusCode! + } + + get statusText() { + return undefined + } + + cachedCookies?: Cookies.Cookies + get cookies(): Cookies.Cookies { + if (this.cachedCookies !== undefined) { + return this.cachedCookies + } + const header = this.source.headers["set-cookie"] + if (header !== undefined) { + return this.cachedCookies = Cookies.fromSetCookie(Array.isArray(header) ? header : [header]) + } + return this.cachedCookies = Cookies.empty + } + + get headers(): Headers.Headers { + return Headers.fromInput(this.source.headers) + } + + get remoteAddress(): Option.Option { + return Option.none() + } + + get stream(): Stream.Stream { + return NodeStream.fromReadable(() => this.source.body, (cause) => + new Error.ResponseError({ + request: this.request, + response: this, + reason: "Decode", + cause + })) + } + + get json(): Effect.Effect { + return Effect.tryMap(this.text, { + try: (text) => text === "" ? null : JSON.parse(text) as unknown, + catch: (cause) => + new Error.ResponseError({ + request: this.request, + response: this, + reason: "Decode", + cause + }) + }) + } + + private textBody?: Effect.Effect + get text(): Effect.Effect { + return this.textBody ??= Effect.tryPromise({ + try: () => this.source.body.text(), + catch: (cause) => + new Error.ResponseError({ + request: this.request, + response: this, + reason: "Decode", + cause + }) + }).pipe(Effect.cached, Effect.runSync) + } + + get urlParamsBody(): Effect.Effect { + return Effect.flatMap(this.text, (_) => + Effect.try({ + try: () => UrlParams.fromInput(new URLSearchParams(_)), + catch: (cause) => + new Error.ResponseError({ + request: this.request, + response: this, + reason: "Decode", + cause + }) + })) + } + + private formDataBody?: Effect.Effect + get formData(): Effect.Effect { + return this.formDataBody ??= Effect.tryPromise({ + try: () => this.source.body.formData() as Promise, + catch: (cause) => + new Error.ResponseError({ + request: this.request, + response: this, + reason: "Decode", + cause + }) + }).pipe(Effect.cached, Effect.runSync) + } + + private arrayBufferBody?: Effect.Effect + get arrayBuffer(): Effect.Effect { + return this.arrayBufferBody ??= Effect.tryPromise({ + try: () => this.source.body.arrayBuffer(), + catch: (cause) => + new Error.ResponseError({ + request: this.request, + response: this, + reason: "Decode", + cause + }) + }).pipe(Effect.cached, Effect.runSync) + } + + toJSON(): unknown { + return IncomingMessage.inspect(this, { + _id: "@effect/platform/HttpClientResponse", + request: this.request.toJSON(), + status: this.status + }) + } +} + +/** @internal */ +export const layerWithoutDispatcher = Client.layerMergedContext(Effect.map(Dispatcher, make)) + +/** @internal */ +export const layer = Layer.provide(layerWithoutDispatcher, dispatcherLayer) diff --git a/repos/effect/packages/platform-node/src/internal/httpIncomingMessage.ts b/repos/effect/packages/platform-node/src/internal/httpIncomingMessage.ts new file mode 100644 index 0000000..dcfe96f --- /dev/null +++ b/repos/effect/packages/platform-node/src/internal/httpIncomingMessage.ts @@ -0,0 +1,92 @@ +import * as Headers from "@effect/platform/Headers" +import * as IncomingMessage from "@effect/platform/HttpIncomingMessage" +import * as UrlParams from "@effect/platform/UrlParams" +import * as Effect from "effect/Effect" +import * as Inspectable from "effect/Inspectable" +import * as Option from "effect/Option" +import type * as Stream from "effect/Stream" +import type * as Http from "node:http" +import * as NodeStream from "../NodeStream.js" + +/** @internal */ +export abstract class HttpIncomingMessageImpl extends Inspectable.Class + implements IncomingMessage.HttpIncomingMessage +{ + readonly [IncomingMessage.TypeId]: IncomingMessage.TypeId + + constructor( + readonly source: Http.IncomingMessage, + readonly onError: (error: unknown) => E, + readonly remoteAddressOverride?: string + ) { + super() + this[IncomingMessage.TypeId] = IncomingMessage.TypeId + } + + get headers() { + return Headers.fromInput(this.source.headers as any) + } + + get remoteAddress() { + return Option.fromNullable(this.remoteAddressOverride ?? this.source.socket.remoteAddress) + } + + private textEffect: Effect.Effect | undefined + get text(): Effect.Effect { + if (this.textEffect) { + return this.textEffect + } + this.textEffect = Effect.runSync(Effect.cached( + Effect.flatMap( + IncomingMessage.MaxBodySize, + (maxBodySize) => + NodeStream.toString(() => this.source, { + onFailure: this.onError, + maxBytes: Option.getOrUndefined(maxBodySize) + }) + ) + )) + return this.textEffect + } + + get unsafeText(): string { + return Effect.runSync(this.text) + } + + get json(): Effect.Effect { + return Effect.tryMap(this.text, { + try: (_) => _ === "" ? null : JSON.parse(_) as unknown, + catch: this.onError + }) + } + + get unsafeJson(): unknown { + return Effect.runSync(this.json) + } + + get urlParamsBody(): Effect.Effect { + return Effect.flatMap(this.text, (_) => + Effect.try({ + try: () => UrlParams.fromInput(new URLSearchParams(_)), + catch: this.onError + })) + } + + get stream(): Stream.Stream { + return NodeStream.fromReadable( + () => this.source, + this.onError + ) + } + + get arrayBuffer(): Effect.Effect { + return Effect.flatMap( + IncomingMessage.MaxBodySize, + (maxBodySize) => + NodeStream.toUint8Array(() => this.source, { + onFailure: this.onError, + maxBytes: Option.getOrUndefined(maxBodySize) + }) + ) + } +} diff --git a/repos/effect/packages/platform-node/src/internal/httpPlatform.ts b/repos/effect/packages/platform-node/src/internal/httpPlatform.ts new file mode 100644 index 0000000..58321bd --- /dev/null +++ b/repos/effect/packages/platform-node/src/internal/httpPlatform.ts @@ -0,0 +1,46 @@ +import * as EtagImpl from "@effect/platform/Etag" +import * as Headers from "@effect/platform/Headers" +import * as Platform from "@effect/platform/HttpPlatform" +import * as ServerResponse from "@effect/platform/HttpServerResponse" +import { pipe } from "effect/Function" +import * as Layer from "effect/Layer" +import Mime from "mime" +import * as Fs from "node:fs" +import { Readable } from "node:stream" +import * as FileSystem from "../NodeFileSystem.js" + +/** @internal */ +export const make = Platform.make({ + fileResponse(path, status, statusText, headers, start, end, contentLength) { + const stream = Fs.createReadStream(path, { start, end }) + return ServerResponse.raw(stream, { + headers: { + ...headers, + "content-type": headers["content-type"] ?? Mime.getType(path) ?? "application/octet-stream", + "content-length": contentLength.toString() + }, + status, + statusText + }) + }, + fileWebResponse(file, status, statusText, headers, _options) { + return ServerResponse.raw(Readable.fromWeb(file.stream() as any), { + headers: Headers.merge( + headers, + Headers.unsafeFromRecord({ + "content-type": headers["content-type"] ?? Mime.getType(file.name) ?? "application/octet-stream", + "content-length": file.size.toString() + }) + ), + status, + statusText + }) + } +}) + +/** @internal */ +export const layer = pipe( + Layer.effect(Platform.HttpPlatform, make), + Layer.provide(FileSystem.layer), + Layer.provide(EtagImpl.layer) +) diff --git a/repos/effect/packages/platform-node/src/internal/httpServer.ts b/repos/effect/packages/platform-node/src/internal/httpServer.ts new file mode 100644 index 0000000..4491b3b --- /dev/null +++ b/repos/effect/packages/platform-node/src/internal/httpServer.ts @@ -0,0 +1,517 @@ +import * as MultipartNode from "@effect/platform-node-shared/NodeMultipart" +import * as Cookies from "@effect/platform/Cookies" +import * as Etag from "@effect/platform/Etag" +import * as FileSystem from "@effect/platform/FileSystem" +import type * as Headers from "@effect/platform/Headers" +import * as App from "@effect/platform/HttpApp" +import * as IncomingMessage from "@effect/platform/HttpIncomingMessage" +import type { HttpMethod } from "@effect/platform/HttpMethod" +import type * as Middleware from "@effect/platform/HttpMiddleware" +import * as Server from "@effect/platform/HttpServer" +import * as Error from "@effect/platform/HttpServerError" +import * as ServerRequest from "@effect/platform/HttpServerRequest" +import type * as ServerResponse from "@effect/platform/HttpServerResponse" +import type * as Multipart from "@effect/platform/Multipart" +import type * as Path from "@effect/platform/Path" +import * as Socket from "@effect/platform/Socket" +import * as Cause from "effect/Cause" +import * as Chunk from "effect/Chunk" +import * as Config from "effect/Config" +import * as Effect from "effect/Effect" +import * as FiberSet from "effect/FiberSet" +import { type LazyArg, pipe } from "effect/Function" +import * as Layer from "effect/Layer" +import * as Option from "effect/Option" +import type { ReadonlyRecord } from "effect/Record" +import * as Runtime from "effect/Runtime" +import * as Scope from "effect/Scope" +import * as Stream from "effect/Stream" +import * as Http from "node:http" +import type * as Net from "node:net" +import type { Duplex } from "node:stream" +import { Readable } from "node:stream" +import { pipeline } from "node:stream/promises" +import * as WS from "ws" +import * as NodeContext from "../NodeContext.js" +import * as NodeHttpClient from "../NodeHttpClient.js" +import { HttpIncomingMessageImpl } from "./httpIncomingMessage.js" +import * as internalPlatform from "./httpPlatform.js" + +/** @internal */ +export const make = ( + evaluate: LazyArg, + options: Net.ListenOptions +): Effect.Effect => + Effect.gen(function*() { + const scope = yield* Effect.scope + const server = yield* Effect.acquireRelease( + Effect.sync(evaluate), + (server) => + Effect.async((resume) => { + if (!server.listening) { + return resume(Effect.void) + } + server.close((error) => { + if (error) { + resume(Effect.die(error)) + } else { + resume(Effect.void) + } + }) + }) + ) + + yield* Effect.async((resume) => { + function onError(cause: Error) { + resume(Effect.fail(new Error.ServeError({ cause }))) + } + server.on("error", onError) + server.listen(options, () => { + server.off("error", onError) + resume(Effect.void) + }) + }) + + const address = server.address()! + + const wss = yield* pipe( + Effect.acquireRelease( + Effect.sync(() => new WS.WebSocketServer({ noServer: true })), + (wss) => + Effect.async((resume) => { + wss.close(() => resume(Effect.void)) + }) + ), + Scope.extend(scope), + Effect.cached + ) + + return Server.make({ + address: typeof address === "string" ? + { + _tag: "UnixAddress", + path: address + } : + { + _tag: "TcpAddress", + hostname: address.address === "::" ? "0.0.0.0" : address.address, + port: address.port + }, + serve: (httpApp, middleware) => + Effect.gen(function*() { + const handler = yield* makeHandler(httpApp, middleware!) + const upgradeHandler = yield* makeUpgradeHandler(wss, httpApp, middleware!) + yield* Effect.addFinalizer(() => + Effect.sync(() => { + server.off("request", handler) + server.off("upgrade", upgradeHandler) + }) + ) + server.on("request", handler) + server.on("upgrade", upgradeHandler) + }) + }) + }).pipe( + Effect.provideService( + IncomingMessage.MaxBodySize, + Option.some(FileSystem.Size(1024 * 1024 * 10)) + ) + ) + +/** @internal */ +export const makeHandler: { + (httpApp: App.Default): Effect.Effect< + (nodeRequest: Http.IncomingMessage, nodeResponse: Http.ServerResponse) => void, + never, + Exclude + > + >( + httpApp: App.Default, + middleware: Middleware.HttpMiddleware.Applied + ): Effect.Effect< + (nodeRequest: Http.IncomingMessage, nodeResponse: Http.ServerResponse) => void, + never, + Exclude, ServerRequest.HttpServerRequest | Scope.Scope> + > +} = (httpApp: App.Default, middleware?: Middleware.HttpMiddleware) => { + const handledApp = App.toHandled(httpApp, handleResponse, middleware) + return Effect.map(Effect.runtime(), (runtime) => { + const runFork = Runtime.runFork(runtime) + return function handler( + nodeRequest: Http.IncomingMessage, + nodeResponse: Http.ServerResponse + ) { + const fiber = runFork( + Effect.provideService( + handledApp, + ServerRequest.HttpServerRequest, + new ServerRequestImpl(nodeRequest, nodeResponse) + ) + ) + nodeResponse.on("close", () => { + if (!nodeResponse.writableEnded) { + fiber.unsafeInterruptAsFork(Error.clientAbortFiberId) + } + }) + } + }) +} + +/** @internal */ +export const makeUpgradeHandler = ( + lazyWss: Effect.Effect, + httpApp: App.Default, + middleware?: Middleware.HttpMiddleware +) => { + const handledApp = App.toHandled(httpApp, handleResponse, middleware) + return Effect.map(FiberSet.makeRuntime(), (runFork) => + function handler( + nodeRequest: Http.IncomingMessage, + socket: Duplex, + head: Buffer + ) { + let nodeResponse_: Http.ServerResponse | undefined = undefined + const nodeResponse = () => { + if (nodeResponse_ === undefined) { + nodeResponse_ = new Http.ServerResponse(nodeRequest) + nodeResponse_.assignSocket(socket as any) + nodeResponse_.on("finish", () => { + socket.end() + }) + } + return nodeResponse_ + } + const upgradeEffect = Socket.fromWebSocket(Effect.flatMap( + lazyWss, + (wss) => + Effect.acquireRelease( + Effect.async((resume) => + wss.handleUpgrade(nodeRequest, socket, head, (ws) => { + resume(Effect.succeed(ws as any)) + }) + ), + (ws) => Effect.sync(() => ws.close()) + ) + )) + const fiber = runFork( + Effect.provideService( + handledApp, + ServerRequest.HttpServerRequest, + new ServerRequestImpl(nodeRequest, nodeResponse, upgradeEffect) + ) + ) + socket.on("close", () => { + if (!socket.writableEnded) { + fiber.unsafeInterruptAsFork(Error.clientAbortFiberId) + } + }) + }) +} + +class ServerRequestImpl extends HttpIncomingMessageImpl implements ServerRequest.HttpServerRequest { + readonly [ServerRequest.TypeId]: ServerRequest.TypeId + + constructor( + readonly source: Http.IncomingMessage, + readonly response: Http.ServerResponse | LazyArg, + private upgradeEffect?: Effect.Effect, + readonly url = source.url!, + private headersOverride?: Headers.Headers, + remoteAddressOverride?: string + ) { + super(source, (cause) => + new Error.RequestError({ + request: this, + reason: "Decode", + cause + }), remoteAddressOverride) + this[ServerRequest.TypeId] = ServerRequest.TypeId + } + + private cachedCookies: ReadonlyRecord | undefined + get cookies() { + if (this.cachedCookies) { + return this.cachedCookies + } + return this.cachedCookies = Cookies.parseHeader(this.headers.cookie ?? "") + } + + get resolvedResponse(): Http.ServerResponse { + return typeof this.response === "function" ? this.response() : this.response + } + + modify( + options: { + readonly url?: string | undefined + readonly headers?: Headers.Headers | undefined + readonly remoteAddress?: string | undefined + } + ) { + return new ServerRequestImpl( + this.source, + this.response, + this.upgradeEffect, + options.url ?? this.url, + options.headers ?? this.headersOverride, + options.remoteAddress ?? this.remoteAddressOverride + ) + } + + get originalUrl(): string { + return this.source.url! + } + + get method(): HttpMethod { + return this.source.method!.toUpperCase() as HttpMethod + } + + get headers(): Headers.Headers { + this.headersOverride ??= this.source.headers as Headers.Headers + return this.headersOverride + } + + private multipartEffect: + | Effect.Effect< + Multipart.Persisted, + Multipart.MultipartError, + Scope.Scope | FileSystem.FileSystem | Path.Path + > + | undefined + get multipart(): Effect.Effect< + Multipart.Persisted, + Multipart.MultipartError, + Scope.Scope | FileSystem.FileSystem | Path.Path + > { + if (this.multipartEffect) { + return this.multipartEffect + } + this.multipartEffect = Effect.runSync(Effect.cached( + MultipartNode.persisted(this.source, this.source.headers) + )) + return this.multipartEffect + } + + get multipartStream(): Stream.Stream { + return MultipartNode.stream(this.source, this.source.headers) + } + + get upgrade(): Effect.Effect { + return this.upgradeEffect ?? Effect.fail( + new Error.RequestError({ + request: this, + reason: "Decode", + description: "not an upgradeable ServerRequest" + }) + ) + } + + toString(): string { + return `ServerRequest(${this.method} ${this.url})` + } + + toJSON(): unknown { + return IncomingMessage.inspect(this, { + _id: "@effect/platform/HttpServerRequest", + method: this.method, + url: this.originalUrl + }) + } +} + +/** @internal */ +export const layerServer = ( + evaluate: LazyArg, + options: Net.ListenOptions +) => Layer.scoped(Server.HttpServer, make(evaluate, options)) + +/** @internal */ +export const layerContext = Layer.mergeAll( + internalPlatform.layer, + Etag.layerWeak, + NodeContext.layer +) + +/** @internal */ +export const layer = ( + evaluate: LazyArg, + options: Net.ListenOptions +) => + Layer.mergeAll( + Layer.scoped(Server.HttpServer, make(evaluate, options)), + layerContext + ) + +/** @internal */ +export const layerTest = Server.layerTestClient.pipe( + Layer.provide(NodeHttpClient.layerWithoutAgent), + Layer.provide(NodeHttpClient.makeAgentLayer({ keepAlive: false })), + Layer.provideMerge(layer(Http.createServer, { port: 0 })) +) + +/** @internal */ +export const layerConfig = ( + evaluate: LazyArg, + options: Config.Config.Wrap +) => + Layer.mergeAll( + Layer.scoped( + Server.HttpServer, + Effect.flatMap(Config.unwrap(options), (options) => make(evaluate, options)) + ), + internalPlatform.layer, + Etag.layerWeak, + NodeContext.layer + ) + +const handleResponse = (request: ServerRequest.HttpServerRequest, response: ServerResponse.HttpServerResponse) => + Effect.suspend((): Effect.Effect => { + const nodeResponse = (request as ServerRequestImpl).resolvedResponse + if (nodeResponse.writableEnded) { + return Effect.void + } + + let headers: Record> = response.headers + if (!Cookies.isEmpty(response.cookies)) { + headers = { ...headers } + const toSet = Cookies.toSetCookieHeaders(response.cookies) + if (headers["set-cookie"] !== undefined) { + toSet.push(headers["set-cookie"] as string) + } + headers["set-cookie"] = toSet + } + + if (request.method === "HEAD") { + nodeResponse.writeHead(response.status, headers) + return Effect.async((resume) => { + nodeResponse.end(() => resume(Effect.void)) + }) + } + const body = response.body + switch (body._tag) { + case "Empty": { + nodeResponse.writeHead(response.status, headers) + nodeResponse.end() + return Effect.void + } + case "Raw": { + nodeResponse.writeHead(response.status, headers) + if ( + typeof body.body === "object" && body.body !== null && "pipe" in body.body && + typeof body.body.pipe === "function" + ) { + return Effect.tryPromise({ + try: (signal) => pipeline(body.body as any, nodeResponse, { signal, end: true }), + catch: (cause) => + new Error.ResponseError({ + request, + response, + reason: "Decode", + cause + }) + }).pipe( + Effect.interruptible, + Effect.tapErrorCause(handleCause(nodeResponse, response)) + ) + } + return Effect.async((resume) => { + nodeResponse.end(body.body, () => resume(Effect.void)) + }) + } + case "Uint8Array": { + nodeResponse.writeHead(response.status, headers) + return Effect.async((resume) => { + nodeResponse.end(body.body, () => resume(Effect.void)) + }) + } + case "FormData": { + return Effect.suspend(() => { + const r = new Response(body.formData) + nodeResponse.writeHead(response.status, { + ...headers, + ...Object.fromEntries(r.headers) + }) + return Effect.async((resume, signal) => { + Readable.fromWeb(r.body as any, { signal }) + .pipe(nodeResponse) + .on("error", (cause) => { + resume(Effect.fail( + new Error.ResponseError({ + request, + response, + reason: "Decode", + cause + }) + )) + }) + .once("finish", () => { + resume(Effect.void) + }) + }).pipe( + Effect.interruptible, + Effect.tapErrorCause(handleCause(nodeResponse, response)) + ) + }) + } + case "Stream": { + nodeResponse.writeHead(response.status, headers) + const drainLatch = Effect.unsafeMakeLatch() + nodeResponse.on("drain", () => drainLatch.unsafeOpen()) + return body.stream.pipe( + Stream.orDie, + Stream.runForEachChunk((chunk) => { + const array = Chunk.toReadonlyArray(chunk) + if (array.length === 0) return Effect.void + let needDrain = false + for (let i = 0; i < array.length; i++) { + const written = nodeResponse.write(array[i]) + if (!written && !needDrain) { + needDrain = true + drainLatch.unsafeClose() + } else if (written && needDrain) { + needDrain = false + } + } + if (!needDrain) return Effect.void + return drainLatch.await + }), + Effect.interruptible, + Effect.matchCauseEffect({ + onSuccess: () => Effect.sync(() => nodeResponse.end()), + onFailure: handleCause(nodeResponse, response) + }) + ) + } + } + }) + +const handleCause = ( + nodeResponse: Http.ServerResponse, + original: ServerResponse.HttpServerResponse +) => +(originalCause: Cause.Cause) => + Error.causeResponse(originalCause).pipe( + Effect.flatMap(([response, cause]) => { + const headersSent = nodeResponse.headersSent + if (!headersSent) { + nodeResponse.writeHead(response.status) + } + if (!nodeResponse.writableEnded) { + nodeResponse.end() + } + return Effect.failCause( + headersSent + ? Cause.sequential(originalCause, Cause.die(original)) + : cause + ) + }) + ) + +/** @internal */ +export const toIncomingMessage = (self: ServerRequest.HttpServerRequest): Http.IncomingMessage => + (self as ServerRequestImpl).source + +/** @internal */ +export const toServerResponse = (self: ServerRequest.HttpServerRequest): Http.ServerResponse => { + const res = (self as ServerRequestImpl).response + return typeof res === "function" ? res() : res +} diff --git a/repos/effect/packages/platform-node/src/internal/worker.ts b/repos/effect/packages/platform-node/src/internal/worker.ts new file mode 100644 index 0000000..2fdf3b2 --- /dev/null +++ b/repos/effect/packages/platform-node/src/internal/worker.ts @@ -0,0 +1,86 @@ +import * as Worker from "@effect/platform/Worker" +import { WorkerError } from "@effect/platform/WorkerError" +import * as Deferred from "effect/Deferred" +import * as Effect from "effect/Effect" +import * as Exit from "effect/Exit" +import * as Layer from "effect/Layer" +import * as Scope from "effect/Scope" +import type * as ChildProcess from "node:child_process" +import type * as WorkerThreads from "node:worker_threads" + +const platformWorkerImpl = Worker.makePlatform()({ + setup({ scope, worker }) { + return Effect.flatMap(Deferred.make(), (exitDeferred) => { + const thing = "postMessage" in worker ? + { + postMessage(msg: any, t?: any) { + worker.postMessage(msg, t) + }, + kill: () => worker.terminate(), + worker + } : + { + postMessage(msg: any, _?: any) { + worker.send(msg) + }, + kill: () => worker.kill("SIGKILL"), + worker + } + worker.on("exit", () => { + Deferred.unsafeDone(exitDeferred, Exit.void) + }) + return Effect.as( + Scope.addFinalizer( + scope, + Effect.suspend(() => { + thing.postMessage([1]) + return Deferred.await(exitDeferred) + }).pipe( + Effect.interruptible, + Effect.timeout(5000), + Effect.catchAllCause(() => Effect.sync(() => thing.kill())) + ) + ), + thing + ) + }) + }, + listen({ deferred, emit, port }) { + port.worker.on("message", (message) => { + emit(message) + }) + port.worker.on("messageerror", (cause) => { + Deferred.unsafeDone( + deferred, + new WorkerError({ reason: "decode", cause }) + ) + }) + port.worker.on("error", (cause) => { + Deferred.unsafeDone(deferred, new WorkerError({ reason: "unknown", cause })) + }) + port.worker.on("exit", (code) => { + Deferred.unsafeDone( + deferred, + new WorkerError({ reason: "unknown", cause: new Error(`exited with code ${code}`) }) + ) + }) + return Effect.void + } +}) + +/** @internal */ +export const layerWorker = Layer.succeed(Worker.PlatformWorker, platformWorkerImpl) + +/** @internal */ +export const layerManager = Layer.provide(Worker.layerManager, layerWorker) + +/** @internal */ +export const layer = (spawn: (id: number) => WorkerThreads.Worker | ChildProcess.ChildProcess) => + Layer.merge( + layerManager, + Worker.layerSpawner(spawn) + ) + +/** @internal */ +export const layerPlatform = (spawn: (id: number) => WorkerThreads.Worker | ChildProcess.ChildProcess) => + Layer.merge(layerWorker, Worker.layerSpawner(spawn)) diff --git a/repos/effect/packages/platform-node/src/internal/workerRunner.ts b/repos/effect/packages/platform-node/src/internal/workerRunner.ts new file mode 100644 index 0000000..505d1f6 --- /dev/null +++ b/repos/effect/packages/platform-node/src/internal/workerRunner.ts @@ -0,0 +1,77 @@ +import { WorkerError } from "@effect/platform/WorkerError" +import * as Runner from "@effect/platform/WorkerRunner" +import * as Cause from "effect/Cause" +import * as Context from "effect/Context" +import * as Deferred from "effect/Deferred" +import * as Effect from "effect/Effect" +import * as Exit from "effect/Exit" +import * as FiberSet from "effect/FiberSet" +import * as Layer from "effect/Layer" +import * as Runtime from "effect/Runtime" +import * as Scope from "effect/Scope" +import * as WorkerThreads from "node:worker_threads" + +const platformRunnerImpl = Runner.PlatformRunner.of({ + [Runner.PlatformRunnerTypeId]: Runner.PlatformRunnerTypeId, + start(closeLatch: Deferred.Deferred) { + return Effect.gen(function*() { + if (!WorkerThreads.parentPort && !process.send) { + return yield* new WorkerError({ reason: "spawn", cause: new Error("not in a worker") }) + } + + const unsafeSend = WorkerThreads.parentPort + ? (message: any, transfers?: any) => WorkerThreads.parentPort!.postMessage(message, transfers) + : (message: any, _transfers?: any) => process.send!(message) + const send = (_portId: number, message: O, transfers?: ReadonlyArray) => + Effect.sync(() => unsafeSend([1, message], transfers as any)) + + const run = Effect.fnUntraced(function*( + handler: (portId: number, message: I) => Effect.Effect | void + ) { + const runtime = (yield* Effect.interruptible(Effect.runtime())).pipe( + Runtime.updateContext(Context.omit(Scope.Scope)) + ) as Runtime.Runtime + const fiberSet = yield* FiberSet.make() + const runFork = Runtime.runFork(runtime) + const onExit = (exit: Exit.Exit) => { + if (exit._tag === "Failure" && !Cause.isInterruptedOnly(exit.cause)) { + Deferred.unsafeDone(closeLatch, Exit.die(Cause.squash(exit.cause))) + } + } + ;(WorkerThreads.parentPort ?? process).on("message", (message: Runner.BackingRunner.Message) => { + if (message[0] === 0) { + const result = handler(0, message[1]) + if (Effect.isEffect(result)) { + const fiber = runFork(result) + fiber.addObserver(onExit) + FiberSet.unsafeAdd(fiberSet, fiber) + } + } else { + if (WorkerThreads.parentPort) { + WorkerThreads.parentPort.close() + } else { + process.channel?.unref() + } + Deferred.unsafeDone(closeLatch, Exit.void) + } + }) + + if (WorkerThreads.parentPort) { + WorkerThreads.parentPort.on("messageerror", (cause) => { + Deferred.unsafeDone(closeLatch, new WorkerError({ reason: "decode", cause })) + }) + WorkerThreads.parentPort.on("error", (cause) => { + Deferred.unsafeDone(closeLatch, new WorkerError({ reason: "unknown", cause })) + }) + } + + unsafeSend([0]) + }) + + return { run, send } + }) + } +}) + +/** @internal */ +export const layer = Layer.succeed(Runner.PlatformRunner, platformRunnerImpl) diff --git a/repos/effect/packages/platform-node/test/HttpApi.test.ts b/repos/effect/packages/platform-node/test/HttpApi.test.ts new file mode 100644 index 0000000..83c8baa --- /dev/null +++ b/repos/effect/packages/platform-node/test/HttpApi.test.ts @@ -0,0 +1,674 @@ +import { + Cookies, + FileSystem, + HttpApi, + HttpApiBuilder, + HttpApiClient, + HttpApiEndpoint, + HttpApiGroup, + HttpApiMiddleware, + HttpApiSchema, + HttpApiSecurity, + HttpClient, + HttpClientRequest, + HttpServerRequest, + HttpServerResponse, + Multipart, + OpenApi +} from "@effect/platform" +import { NodeHttpServer } from "@effect/platform-node" +import * as HttpLayerRouter from "@effect/platform/HttpLayerRouter" +import { assert, describe, it } from "@effect/vitest" +import { Chunk, Context, DateTime, Effect, Layer, Redacted, Ref, Schema, Stream, Struct } from "effect" +import OpenApiFixture from "./fixtures/openapi.json" with { type: "json" } + +describe("HttpApi", () => { + describe("payload", () => { + it.effect("is decoded / encoded", () => + Effect.gen(function*() { + const expected = new User({ + id: 123, + name: "Joe", + createdAt: DateTime.unsafeMake(0) + }) + const client = yield* HttpApiClient.make(Api) + const clientUsersGroup = yield* HttpApiClient.group(Api, { + httpClient: yield* HttpClient.HttpClient, + group: "users" + }) + const clientUsersEndpointCreate = yield* HttpApiClient.endpoint(Api, { + httpClient: yield* HttpClient.HttpClient, + group: "users", + endpoint: "create" + }) + + const apiClientUser = yield* client.users.create({ + urlParams: { id: 123 }, + payload: { name: "Joe" } + }) + assert.deepStrictEqual( + apiClientUser, + expected + ) + const groupClientUser = yield* clientUsersGroup.create({ + urlParams: { id: 123 }, + payload: { name: "Joe" } + }) + assert.deepStrictEqual( + groupClientUser, + expected + ) + const endpointClientUser = yield* clientUsersEndpointCreate({ + urlParams: { id: 123 }, + payload: { name: "Joe" } + }) + assert.deepStrictEqual( + endpointClientUser, + expected + ) + }).pipe(Effect.provide(HttpLive))) + + it.live("multipart", () => + Effect.gen(function*() { + const client = yield* HttpApiClient.make(Api) + const data = new FormData() + data.append("file", new Blob(["hello"], { type: "text/plain" }), "hello.txt") + const result = yield* client.users.upload({ payload: data, path: {} }) + assert.deepStrictEqual(result, { + contentType: "text/plain", + length: 5 + }) + }).pipe(Effect.provide(HttpLive))) + + it.live("multipart stream", () => + Effect.gen(function*() { + const client = yield* HttpApiClient.make(Api) + const data = new FormData() + data.append("file", new Blob(["hello"], { type: "text/plain" }), "hello.txt") + const result = yield* client.users.uploadStream({ payload: data }) + assert.deepStrictEqual(result, { + contentType: "text/plain", + length: 5 + }) + }).pipe(Effect.provide(HttpLive))) + }) + + describe("headers", () => { + it.effect("is decoded / encoded", () => + Effect.gen(function*() { + const client = yield* HttpApiClient.make(Api) + const users = yield* client.users.list({ + headers: { page: 1 }, + urlParams: {} + }) + const user = users[0] + assert.deepStrictEqual( + user, + new User({ + id: 1, + name: "page 1", + createdAt: DateTime.unsafeMake(0) + }) + ) + }).pipe(Effect.provide(HttpLive))) + }) + + describe("errors", () => { + it.effect("empty errors have no body", () => + Effect.gen(function*() { + const response = yield* HttpClient.get("/groups/0") + assert.strictEqual(response.status, 418) + const text = yield* response.text + assert.strictEqual(text, "") + }).pipe(Effect.provide(HttpLive))) + + it.effect("empty errors decode", () => + Effect.gen(function*() { + const client = yield* HttpApiClient.make(Api) + const error = yield* client.groups.findById({ path: { id: 0 } }).pipe( + Effect.flip + ) + assert.deepStrictEqual(error, new GroupError()) + }).pipe(Effect.provide(HttpLive))) + + it.scoped("default to 500 status code", () => + Effect.gen(function*() { + const response = yield* HttpClientRequest.get("/users").pipe( + HttpClientRequest.setHeaders({ page: "0" }), + HttpClient.execute + ) + assert.strictEqual(response.status, 500) + const body = yield* response.json + assert.deepStrictEqual(body, { + _tag: "NoStatusError" + }) + }).pipe(Effect.provide(HttpLive))) + + it.scoped("class level annotations", () => + Effect.gen(function*() { + const response = yield* HttpClientRequest.post("/users").pipe( + HttpClientRequest.setUrlParams({ id: "0" }), + HttpClientRequest.bodyUnsafeJson({ name: "boom" }), + HttpClient.execute + ) + assert.strictEqual(response.status, 400) + }).pipe(Effect.provide(HttpLive))) + + it.effect("HttpApiDecodeError", () => + Effect.gen(function*() { + const client = yield* HttpApiClient.make(Api) + const error = yield* client.users.upload({ path: {}, payload: new FormData() }).pipe( + Effect.flip + ) + assert(error._tag === "HttpApiDecodeError") + assert.deepStrictEqual(error.issues[0].path, ["file"]) + }).pipe(Effect.provide(HttpLive))) + }) + + it.effect("handler level context", () => + Effect.gen(function*() { + const client = yield* HttpApiClient.make(Api) + const users = yield* client.users.list({ headers: { page: 1 }, urlParams: {} }) + const user = users[0] + assert.strictEqual(user.name, "page 1") + assert.deepStrictEqual(user.createdAt, DateTime.unsafeMake(0)) + }).pipe(Effect.provide(HttpLive))) + + it.effect("custom client context", () => + Effect.gen(function*() { + let tapped = false + const client = yield* HttpApiClient.makeWith(Api, { + httpClient: (yield* HttpClient.HttpClient).pipe( + HttpClient.tapRequest(Effect.fnUntraced(function*(_request) { + tapped = true + yield* CurrentUser + })) + ) + }) + const users = yield* client.users.list({ headers: { page: 1 }, urlParams: {} }).pipe( + Effect.provideService( + CurrentUser, + new User({ + id: 1, + name: "foo", + createdAt: DateTime.unsafeMake(0) + }) + ) + ) + const user = users[0] + assert.strictEqual(user.name, "page 1") + assert.isTrue(tapped) + }).pipe(Effect.provide(HttpLive))) + + describe("security", () => { + it.effect("security middleware sets current user", () => + Effect.gen(function*() { + const ref = yield* Ref.make(Cookies.empty.pipe( + Cookies.unsafeSet("token", "foo") + )) + const client = yield* HttpApiClient.makeWith(Api, { + httpClient: HttpClient.withCookiesRef(yield* HttpClient.HttpClient, ref) + }) + const user = yield* client.users.findById({ path: { id: -1 } }) + assert.strictEqual(user.name, "foo") + }).pipe(Effect.provide(HttpLive))) + + it.effect("apiKey header security", () => + Effect.gen(function*() { + const decode = HttpApiBuilder.securityDecode(securityHeader).pipe( + Effect.provideService( + HttpServerRequest.HttpServerRequest, + HttpServerRequest.fromWeb( + new Request("http://localhost:3000/", { + headers: { + "x-api-key": "foo" + } + }) + ) + ), + Effect.provideService(HttpServerRequest.ParsedSearchParams, {}) + ) + const redacted = yield* decode + assert.strictEqual(Redacted.value(redacted), "foo") + }).pipe(Effect.provide(HttpLive))) + + it.effect("apiKey query security", () => + Effect.gen(function*() { + const redacted = yield* HttpApiBuilder.securityDecode(securityQuery).pipe( + Effect.provideService( + HttpServerRequest.HttpServerRequest, + HttpServerRequest.fromWeb(new Request("http://localhost:3000/")) + ), + Effect.provideService(HttpServerRequest.ParsedSearchParams, { + api_key: "foo" + }) + ) + assert.strictEqual(Redacted.value(redacted), "foo") + }).pipe(Effect.provide(HttpLive))) + }) + + it.effect("client withResponse", () => + Effect.gen(function*() { + const client = yield* HttpApiClient.make(Api) + const [users, response] = yield* client.users.list({ headers: { page: 1 }, urlParams: {}, withResponse: true }) + assert.strictEqual(users[0].name, "page 1") + assert.strictEqual(response.status, 200) + }).pipe(Effect.provide(HttpLive))) + + it.effect("multiple payload types", () => + Effect.gen(function*() { + const client = yield* HttpApiClient.make(Api) + let [group, response] = yield* client.groups.create({ + payload: { name: "Some group" }, + withResponse: true + }) + assert.deepStrictEqual(group, new Group({ id: 1, name: "Some group" })) + assert.strictEqual(response.status, 200) + + const data = new FormData() + data.set("name", "Some group") + ;[group, response] = yield* client.groups.create({ + payload: data, + withResponse: true + }) + assert.deepStrictEqual(group, new Group({ id: 1, name: "Some group" })) + assert.strictEqual(response.status, 200) + + group = yield* client.groups.create({ + payload: { foo: "Some group" } + }) + assert.deepStrictEqual(group, new Group({ id: 1, name: "Some group" })) + }).pipe(Effect.provide(HttpLive))) + + it.effect(".handle can return HttpServerResponse", () => + Effect.gen(function*() { + const client = yield* HttpApiClient.make(Api) + const response = yield* client.groups.handle({ + path: { id: 1 }, + payload: { name: "Some group" } + }) + assert.deepStrictEqual(response, { + id: 1, + name: "Some group" + }) + }).pipe(Effect.provide(HttpLive))) + + it.effect(".handleRaw can manually process body", () => + Effect.gen(function*() { + const client = yield* HttpApiClient.make(Api) + const response = yield* client.groups.handleRaw({ + path: { id: 1 }, + payload: { name: "Some group" } + }) + assert.deepStrictEqual(response, { + id: 1, + name: "Some group" + }) + }).pipe(Effect.provide(HttpLive))) + + it("OpenAPI spec", () => { + const spec = OpenApi.fromApi(Api) + assert.deepStrictEqual(spec, OpenApiFixture as any) + }) + + it.effect("error from plain text", () => { + class RateLimitError extends Schema.TaggedError("RateLimitError")( + "RateLimitError", + Schema.Struct({ message: Schema.String }) + ) {} + + const RateLimitErrorSchema = HttpApiSchema.withEncoding( + Schema.transform(Schema.String, RateLimitError, { + encode: ({ message }) => message, + decode: (message) => RateLimitError.make({ message }), + strict: true + }), + { kind: "Text" } + ).annotations(HttpApiSchema.annotations({ status: 429 })) + + const Api = HttpApi.make("api").add( + HttpApiGroup.make("group").add( + HttpApiEndpoint.get("error")`/error`.addError(RateLimitErrorSchema) + ) + ) + const ApiLive = HttpLayerRouter.addHttpApi(Api).pipe( + Layer.provide( + HttpApiBuilder.group( + Api, + "group", + (handlers) => handlers.handle("error", () => new RateLimitError({ message: "Rate limit exceeded" })) + ) + ), + HttpLayerRouter.serve, + Layer.provideMerge(NodeHttpServer.layerTest) + ) + return Effect.gen(function*() { + const client = yield* HttpApiClient.make(Api) + const response = yield* client.group.error().pipe(Effect.flip) + assert.deepStrictEqual(response, new RateLimitError({ message: "Rate limit exceeded" })) + }).pipe(Effect.provide(ApiLive)) + }) +}) + +class GlobalError extends Schema.TaggedClass()("GlobalError", {}) {} +class GroupError extends Schema.TaggedClass()("GroupError", {}) {} +class UserError extends Schema.TaggedClass()("UserError", {}, HttpApiSchema.annotations({ status: 400 })) {} +class NoStatusError extends Schema.TaggedClass()("NoStatusError", {}) {} + +class User extends Schema.Class("User")({ + id: Schema.Int, + uuid: Schema.optional(Schema.UUID), + name: Schema.String, + createdAt: Schema.DateTimeUtc +}) {} + +class Group extends Schema.Class("Group")({ + id: Schema.Int, + name: Schema.String +}) {} + +const securityHeader = HttpApiSecurity.apiKey({ + in: "header", + key: "x-api-key" +}) + +const securityQuery = HttpApiSecurity.apiKey({ + in: "query", + key: "api_key" +}) + +class CurrentUser extends Context.Tag("CurrentUser")() {} + +class Authorization extends HttpApiMiddleware.Tag()("Authorization", { + security: { + cookie: HttpApiSecurity.apiKey({ + in: "cookie", + key: "token" + }) + }, + provides: CurrentUser +}) {} + +class GroupsApi extends HttpApiGroup.make("groups") + .add( + HttpApiEndpoint.get("findById")`/${HttpApiSchema.param("id", Schema.NumberFromString)}` + .addSuccess(Group) + ) + .add( + HttpApiEndpoint.post("create")`/` + .setPayload(Schema.Union( + Schema.Struct(Struct.pick(Group.fields, "name")), + Schema.Struct({ foo: Schema.String }).pipe( + HttpApiSchema.withEncoding({ kind: "UrlParams" }) + ), + HttpApiSchema.Multipart( + Schema.Struct(Struct.pick(Group.fields, "name")) + ) + )) + .addSuccess(Group) + ) + .add( + HttpApiEndpoint.post("handle")`/handle/${HttpApiSchema.param("id", Schema.NumberFromString)}` + .setPayload(Schema.Struct({ + name: Schema.String + })) + .addSuccess(Schema.Struct({ + id: Schema.Number, + name: Schema.String + })) + ) + .add( + HttpApiEndpoint.post("handleRaw")`/handleraw/${HttpApiSchema.param("id", Schema.NumberFromString)}` + .setPayload(Schema.Struct({ + name: Schema.String + })) + .addSuccess(Schema.Struct({ + id: Schema.Number, + name: Schema.String + })) + ) + .addError(GroupError.pipe( + HttpApiSchema.asEmpty({ status: 418, decode: () => new GroupError() }) + )) + .prefix("/groups") +{} + +class UsersApi extends HttpApiGroup.make("users") + .add( + HttpApiEndpoint.get("findById")`/${HttpApiSchema.param("id", Schema.NumberFromString)}` + .addSuccess(User) + ) + .add( + HttpApiEndpoint.post("create")`/` + .setPayload(Schema.Struct(Struct.omit( + User.fields, + "id", + "createdAt" + ))) + .setUrlParams(Schema.Struct({ + id: Schema.NumberFromString + })) + .addSuccess(User) + .addError(UserError) + .addError(UserError) // ensure errors are deduplicated + ) + .add( + HttpApiEndpoint.get("list")`/` + .setHeaders(Schema.Struct({ + page: Schema.NumberFromString.pipe( + Schema.optionalWith({ default: () => 1 }) + ) + })) + .setUrlParams(Schema.Struct({ + query: Schema.optional(Schema.String).annotations({ description: "search query" }) + })) + .addSuccess(Schema.Array(User)) + .addError(NoStatusError) + .annotate(OpenApi.Deprecated, true) + .annotate(OpenApi.Summary, "test summary") + .annotateContext(OpenApi.annotations({ identifier: "listUsers" })) + ) + .add( + HttpApiEndpoint.post("upload")`/upload/${Schema.optional(Schema.String)}` + .setPayload(HttpApiSchema.Multipart(Schema.Struct({ + file: Multipart.SingleFileSchema + }))) + .addSuccess(Schema.Struct({ + contentType: Schema.String, + length: Schema.Int + })) + ) + .add( + HttpApiEndpoint.post("uploadStream")`/uploadstream` + .setPayload(HttpApiSchema.MultipartStream(Schema.Struct({ + file: Multipart.SingleFileSchema + }))) + .addSuccess(Schema.Struct({ + contentType: Schema.String, + length: Schema.Int + })) + ) + .middleware(Authorization) + .annotateContext(OpenApi.annotations({ title: "Users API" })) +{} + +class TopLevelApi extends HttpApiGroup.make("root", { topLevel: true }) + .add( + HttpApiEndpoint.get("healthz")`/healthz` + .addSuccess(HttpApiSchema.NoContent.annotations({ description: "Empty" })) + ) +{} + +class AnotherApi extends HttpApi.make("another").add(GroupsApi) {} + +class Api extends HttpApi.make("api") + .addHttpApi(AnotherApi) + .add(UsersApi.prefix("/users")) + .add(TopLevelApi) + .addError(GlobalError, { status: 413 }) + .annotateContext(OpenApi.annotations({ + title: "API", + summary: "test api summary", + transform: (openApiSpec) => ({ + ...openApiSpec, + tags: [...openApiSpec.tags ?? [], { + name: "Tag from OpenApi.Transform annotation" + }] + }) + })) + .annotate( + HttpApi.AdditionalSchemas, + [ + Schema.Struct({ + contentType: Schema.String, + length: Schema.Int + }).annotations({ + identifier: "ComponentsSchema" + }) + ] + ) +{} + +// impl + +class UserRepo extends Context.Tag("UserRepo") Effect.Effect +}>() { + static Live = Layer.succeed(this, { + findById: (id) => Effect.map(DateTime.now, (now) => ({ id, name: "foo", createdAt: now })) + }) +} + +const AuthorizationLive = Layer.succeed( + Authorization, + Authorization.of({ + cookie: (token) => + Effect.succeed( + new User({ + id: 1, + name: Redacted.value(token), + createdAt: DateTime.unsafeNow() + }) + ) + }) +) + +const HttpUsersLive = HttpApiBuilder.group( + Api, + "users", + (handlers) => + Effect.gen(function*() { + const fs = yield* FileSystem.FileSystem + const repo = yield* UserRepo + return handlers + .handle("findById", (_) => + _.path.id === -1 + ? CurrentUser : + repo.findById(_.path.id)) + .handle("create", (_) => + _.payload.name === "boom" + ? Effect.fail(new UserError()) + : Effect.map(DateTime.now, (now) => + new User({ + id: _.urlParams.id, + name: _.payload.name, + createdAt: now + }))) + .handle("list", (_) => + _.headers.page === 0 + ? Effect.fail(new NoStatusError()) + // test handler level context + : Effect.map(DateTime.nowInCurrentZone, (now) => [ + new User({ + id: 1, + name: `page ${_.headers.page}`, + createdAt: DateTime.toUtc(now) + }) + ])) + .handle("upload", (_) => + Effect.gen(function*() { + const stat = yield* fs.stat(_.payload.file.path).pipe(Effect.orDie) + return { + contentType: _.payload.file.contentType, + length: Number(stat.size) + } + })) + .handle("uploadStream", (_) => + Effect.gen(function*() { + const { content, file } = yield* _.payload.pipe( + Stream.filter((part) => part._tag === "File"), + Stream.mapEffect((file) => + file.contentEffect.pipe( + Effect.map((content) => ({ file, content })) + ) + ), + Stream.runCollect, + Effect.flatMap(Chunk.head), + Effect.orDie + ) + return { + contentType: file.contentType, + length: content.length + } + })) + }) +).pipe(Layer.provide([ + DateTime.layerCurrentZoneOffset(0), + UserRepo.Live, + AuthorizationLive +])) + +const HttpGroupsLive = HttpApiBuilder.group( + Api, + "groups", + (handlers) => + handlers + .handle("findById", ({ path }) => + path.id === 0 + ? Effect.fail(new GroupError()) + : Effect.succeed(new Group({ id: 1, name: "foo" }))) + .handle("create", ({ payload }) => + Effect.succeed( + new Group({ + id: 1, + name: "foo" in payload ? payload.foo : payload.name + }) + )) + .handle( + "handle", + Effect.fn(function*({ path, payload }) { + return HttpServerResponse.unsafeJson({ + id: path.id, + name: payload.name + }) + }) + ) + .handleRaw( + "handleRaw", + Effect.fn(function*({ path, request }) { + const body = (yield* Effect.orDie(request.json)) as { name: string } + return HttpServerResponse.unsafeJson({ + id: path.id, + name: body.name + }) + }) + ) +) + +const TopLevelLive = HttpApiBuilder.group( + Api, + "root", + (handlers) => handlers.handle("healthz", (_) => Effect.void) +) + +const HttpApiLive = Layer.provide(HttpApiBuilder.api(Api), [ + HttpGroupsLive, + HttpUsersLive, + TopLevelLive +]) + +const HttpLive = HttpApiBuilder.serve().pipe( + Layer.provide(HttpApiBuilder.middlewareCors()), + Layer.provide(HttpApiLive), + Layer.provideMerge(NodeHttpServer.layerTest) +) diff --git a/repos/effect/packages/platform-node/test/HttpClient.test.ts b/repos/effect/packages/platform-node/test/HttpClient.test.ts new file mode 100644 index 0000000..e2c0afa --- /dev/null +++ b/repos/effect/packages/platform-node/test/HttpClient.test.ts @@ -0,0 +1,155 @@ +import { HttpClient, HttpClientRequest, HttpClientResponse } from "@effect/platform" +import * as NodeClient from "@effect/platform-node/NodeHttpClient" +import { describe, expect, it } from "@effect/vitest" +import { Struct } from "effect" +import * as Context from "effect/Context" +import * as Effect from "effect/Effect" +import * as Layer from "effect/Layer" +import * as Schema from "effect/Schema" +import * as Stream from "effect/Stream" + +const Todo = Schema.Struct({ + userId: Schema.Number, + id: Schema.Number, + title: Schema.String, + completed: Schema.Boolean +}) +const TodoWithoutId = Schema.Struct({ + ...Struct.omit(Todo.fields, "id") +}) + +const makeJsonPlaceholder = Effect.gen(function*() { + const defaultClient = yield* HttpClient.HttpClient + const client = defaultClient.pipe( + HttpClient.mapRequest(HttpClientRequest.prependUrl("https://jsonplaceholder.typicode.com")) + ) + const createTodo = (todo: typeof TodoWithoutId.Type) => + HttpClientRequest.post("/todos").pipe( + HttpClientRequest.schemaBodyJson(TodoWithoutId)(todo), + Effect.flatMap(client.execute), + Effect.flatMap(HttpClientResponse.schemaBodyJson(Todo)) + ) + return { + client, + createTodo + } as const +}) +interface JsonPlaceholder extends Effect.Effect.Success {} +const JsonPlaceholder = Context.GenericTag("test/JsonPlaceholder") +const JsonPlaceholderLive = Layer.effect(JsonPlaceholder, makeJsonPlaceholder) +;[ + { + name: "node:http", + layer: NodeClient.layer + }, + { + name: "undici", + layer: NodeClient.layerUndici + } +].forEach(({ layer, name }) => { + describe(`NodeHttpClient - ${name}`, () => { + it.effect("google", () => + Effect.gen(function*() { + const response = yield* HttpClient.get("https://www.google.com/").pipe( + Effect.flatMap((_) => _.text) + ) + expect(response).toContain("Google") + }).pipe( + it.flakyTest, + Effect.provide(layer) + ), 30000) + + it.effect("google followRedirects", () => + Effect.gen(function*() { + const client = (yield* HttpClient.HttpClient).pipe( + HttpClient.followRedirects() + ) + const response = yield* client.get("http://google.com/").pipe( + Effect.flatMap((_) => _.text) + ) + expect(response).toContain("Google") + }).pipe( + it.flakyTest, + Effect.provide(layer) + ), 30000) + + it.effect("google stream", () => + Effect.gen(function*() { + const client = yield* HttpClient.HttpClient + const response = yield* client.get("https://www.google.com/").pipe( + Effect.map((_) => _.stream), + Stream.unwrapScoped, + Stream.runFold("", (a, b) => a + new TextDecoder().decode(b)) + ) + expect(response).toContain("Google") + }).pipe( + it.flakyTest, + Effect.provide(layer) + ), 30000) + + it.effect("jsonplaceholder", () => + Effect.gen(function*() { + const jp = yield* JsonPlaceholder + const response = yield* jp.client.get("/todos/1").pipe( + Effect.flatMap(HttpClientResponse.schemaBodyJson(Todo)) + ) + expect(response.id).toBe(1) + }).pipe( + it.flakyTest, + Effect.provide(JsonPlaceholderLive.pipe( + Layer.provide(layer) + )) + ), 30000) + + it.effect("jsonplaceholder schemaBodyJson", () => + Effect.gen(function*() { + const jp = yield* JsonPlaceholder + const response = yield* jp.createTodo({ + userId: 1, + title: "test", + completed: false + }) + expect(response.title).toBe("test") + }).pipe( + it.flakyTest, + Effect.provide(JsonPlaceholderLive.pipe( + Layer.provide(layer) + )) + ), 30000) + + it.effect("head request with schemaJson", () => + Effect.gen(function*() { + const client = yield* HttpClient.HttpClient + const response = yield* client.head("https://jsonplaceholder.typicode.com/todos").pipe( + Effect.flatMap( + HttpClientResponse.schemaJson(Schema.Struct({ status: Schema.Literal(200) })) + ) + ) + expect(response).toEqual({ status: 200 }) + }).pipe( + it.flakyTest, + Effect.provide(layer) + ), 30000) + + it.live("interrupt", () => + Effect.gen(function*() { + const client = yield* HttpClient.HttpClient + const response = yield* client.get("https://www.google.com/").pipe( + Effect.flatMap((_) => _.text), + Effect.timeout(1), + Effect.asSome, + Effect.catchTag("TimeoutException", () => Effect.succeedNone) + ) + expect(response._tag).toEqual("None") + }).pipe(Effect.provide(layer))) + + it.effect("close early", () => + Effect.gen(function*() { + const response = yield* HttpClient.get("https://www.google.com/") + expect(response.status).toBe(200) + }).pipe( + it.flakyTest, + Effect.provide(layer) + ), 30000) + }) +}) diff --git a/repos/effect/packages/platform-node/test/HttpServer.test.ts b/repos/effect/packages/platform-node/test/HttpServer.test.ts new file mode 100644 index 0000000..e3604f4 --- /dev/null +++ b/repos/effect/packages/platform-node/test/HttpServer.test.ts @@ -0,0 +1,796 @@ +import { + Cookies, + HttpBody, + HttpClient, + HttpClientRequest, + HttpClientResponse, + HttpLayerRouter, + HttpMultiplex, + HttpPlatform, + HttpRouter, + HttpServer, + HttpServerError, + HttpServerRequest, + HttpServerRespondable, + HttpServerResponse, + Multipart, + UrlParams +} from "@effect/platform" +import { NodeHttpServer } from "@effect/platform-node" +import { assert, describe, expect, it } from "@effect/vitest" +import { Deferred, Duration, Fiber, Stream } from "effect" +import * as Cause from "effect/Cause" +import * as Effect from "effect/Effect" +import * as Option from "effect/Option" +import * as Schema from "effect/Schema" +import * as Tracer from "effect/Tracer" +import * as Buffer from "node:buffer" + +const Todo = Schema.Struct({ + id: Schema.Number, + title: Schema.String +}) +const IdParams = Schema.Struct({ + id: Schema.NumberFromString +}) +const todoResponse = HttpServerResponse.schemaJson(Todo) + +describe("HttpServer", () => { + it.scoped("schema", () => + Effect.gen(function*() { + yield* HttpRouter.empty.pipe( + HttpRouter.get( + "/todos/:id", + Effect.flatMap( + HttpRouter.schemaParams(IdParams), + ({ id }) => todoResponse({ id, title: "test" }) + ) + ), + HttpServer.serveEffect() + ) + const todo = yield* HttpClient.get("/todos/1").pipe( + Effect.flatMap(HttpClientResponse.schemaBodyJson(Todo)) + ) + expect(todo).toEqual({ id: 1, title: "test" }) + }).pipe(Effect.provide(NodeHttpServer.layerTest))) + + it.scoped("schema HttpLayerRouter", () => + Effect.gen(function*() { + const handler = yield* HttpLayerRouter.toHttpEffect(HttpLayerRouter.use(Effect.fnUntraced(function*(router) { + yield* router.add( + "GET", + "/todos/:id", + Effect.flatMap( + HttpLayerRouter.schemaParams(IdParams), + ({ id }) => todoResponse({ id, title: "test" }) + ) + ) + }))) + + yield* HttpServer.serveEffect(handler) + + const todo = yield* HttpClient.get("/todos/1").pipe( + Effect.flatMap(HttpClientResponse.schemaBodyJson(Todo)) + ) + expect(todo).toEqual({ id: 1, title: "test" }) + }).pipe(Effect.provide(NodeHttpServer.layerTest))) + + it.scoped("formData", () => + Effect.gen(function*() { + yield* HttpRouter.empty.pipe( + HttpRouter.post( + "/upload", + Effect.gen(function*() { + const request = yield* HttpServerRequest.HttpServerRequest + const formData = yield* request.multipart + const part = formData.file + assert(typeof part !== "string") + const file = part[0] + assert(typeof file !== "string") + expect(file.path.endsWith("/test.txt")).toEqual(true) + expect(file.contentType).toEqual("text/plain") + return yield* HttpServerResponse.json({ ok: "file" in formData }) + }) + ), + HttpServer.serveEffect() + ) + const client = yield* HttpClient.HttpClient + const formData = new FormData() + formData.append("file", new Blob(["test"], { type: "text/plain" }), "test.txt") + const result = yield* client.post("/upload", { body: HttpBody.formData(formData) }).pipe( + Effect.flatMap((r) => r.json) + ) + expect(result).toEqual({ ok: true }) + }).pipe(Effect.provide(NodeHttpServer.layerTest))) + + it.scoped("schemaBodyForm", () => + Effect.gen(function*() { + yield* HttpRouter.empty.pipe( + HttpRouter.post( + "/upload", + Effect.gen(function*() { + const files = yield* HttpServerRequest.schemaBodyForm(Schema.Struct({ + file: Multipart.FilesSchema, + test: Schema.String + })) + expect(files).toHaveProperty("file") + expect(files).toHaveProperty("test") + return HttpServerResponse.empty() + }) + ), + Effect.tapErrorCause(Effect.logError), + HttpServer.serveEffect() + ) + const client = yield* HttpClient.HttpClient + const formData = new FormData() + formData.append("file", new Blob(["test"], { type: "text/plain" }), "test.txt") + formData.append("test", "test") + const response = yield* client.post("/upload", { body: HttpBody.formData(formData) }) + expect(response.status).toEqual(204) + }).pipe(Effect.provide(NodeHttpServer.layerTest))) + + it.scoped("formData withMaxFileSize", () => + Effect.gen(function*() { + yield* HttpRouter.empty.pipe( + HttpRouter.post( + "/upload", + Effect.gen(function*() { + const request = yield* HttpServerRequest.HttpServerRequest + yield* request.multipart + return HttpServerResponse.empty() + }) + ), + Effect.catchTag("MultipartError", (error) => + error.reason === "FileTooLarge" ? + HttpServerResponse.empty({ status: 413 }) : + Effect.fail(error)), + HttpServer.serveEffect(), + Multipart.withMaxFileSize(Option.some(100)) + ) + const client = yield* HttpClient.HttpClient + const formData = new FormData() + const data = new Uint8Array(1000) + formData.append("file", new Blob([data], { type: "text/plain" }), "test.txt") + const response = yield* client.post("/upload", { body: HttpBody.formData(formData) }) + expect(response.status).toEqual(413) + }).pipe(Effect.provide(NodeHttpServer.layerTest))) + + it.scoped("formData withMaxFieldSize", () => + Effect.gen(function*() { + yield* HttpRouter.empty.pipe( + HttpRouter.post( + "/upload", + Effect.gen(function*() { + const request = yield* HttpServerRequest.HttpServerRequest + yield* request.multipart + return HttpServerResponse.empty() + }) + ), + Effect.catchTag("MultipartError", (error) => + error.reason === "FieldTooLarge" ? + HttpServerResponse.empty({ status: 413 }) : + Effect.fail(error)), + HttpServer.serveEffect(), + Multipart.withMaxFieldSize(100) + ) + const client = yield* HttpClient.HttpClient + const formData = new FormData() + const data = new Uint8Array(1000).fill(1) + formData.append("file", new TextDecoder().decode(data)) + const response = yield* client.post("/upload", { body: HttpBody.formData(formData) }) + expect(response.status).toEqual(413) + }).pipe(Effect.provide(NodeHttpServer.layerTest))) + + it.scoped("mount", () => + Effect.gen(function*() { + const child = HttpRouter.empty.pipe( + HttpRouter.get("/", Effect.map(HttpServerRequest.HttpServerRequest, (_) => HttpServerResponse.text(_.url))), + HttpRouter.get("/:id", Effect.map(HttpServerRequest.HttpServerRequest, (_) => HttpServerResponse.text(_.url))) + ) + yield* HttpRouter.empty.pipe( + HttpRouter.mount("/child", child), + HttpServer.serveEffect() + ) + const client = yield* HttpClient.HttpClient + const todo = yield* client.get("/child/1").pipe(Effect.flatMap((_) => _.text)) + expect(todo).toEqual("/1") + const root = yield* client.get("/child").pipe(Effect.flatMap((_) => _.text)) + expect(root).toEqual("/") + }).pipe(Effect.provide(NodeHttpServer.layerTest))) + + it.scoped("mountApp", () => + Effect.gen(function*() { + const child = HttpRouter.empty.pipe( + HttpRouter.get("/", Effect.map(HttpServerRequest.HttpServerRequest, (_) => HttpServerResponse.text(_.url))), + HttpRouter.get("/:id", Effect.map(HttpServerRequest.HttpServerRequest, (_) => HttpServerResponse.text(_.url))) + ) + yield* HttpRouter.empty.pipe( + HttpRouter.mountApp("/child", child), + HttpServer.serveEffect() + ) + const client = yield* HttpClient.HttpClient + const todo = yield* client.get("/child/1").pipe(Effect.flatMap((_) => _.text)) + expect(todo).toEqual("/1") + const root = yield* client.get("/child").pipe(Effect.flatMap((_) => _.text)) + expect(root).toEqual("/") + const rootSearch = yield* client.get("/child?foo=bar").pipe(Effect.flatMap((_) => _.text)) + expect(rootSearch).toEqual("?foo=bar") + const rootSlash = yield* client.get("/child/").pipe(Effect.flatMap((_) => _.text)) + expect(rootSlash).toEqual("/") + const invalid = yield* client.get("/child1/", { + urlParams: { foo: "bar" } + }).pipe(Effect.map((_) => _.status)) + expect(invalid).toEqual(404) + }).pipe(Effect.provide(NodeHttpServer.layerTest))) + + it.scoped("mountApp/includePrefix", () => + Effect.gen(function*() { + const child = HttpRouter.empty.pipe( + HttpRouter.get( + "/child/", + Effect.map(HttpServerRequest.HttpServerRequest, (_) => HttpServerResponse.text(_.url)) + ), + HttpRouter.get( + "/child/:id", + Effect.map(HttpServerRequest.HttpServerRequest, (_) => HttpServerResponse.text(_.url)) + ) + ) + yield* HttpRouter.empty.pipe( + HttpRouter.mountApp("/child", child, { includePrefix: true }), + HttpServer.serveEffect() + ) + const client = yield* HttpClient.HttpClient + const todo = yield* client.get("/child/1").pipe(Effect.flatMap((_) => _.text)) + expect(todo).toEqual("/child/1") + const root = yield* client.get("/child").pipe(Effect.flatMap((_) => _.text)) + expect(root).toEqual("/child") + }).pipe(Effect.provide(NodeHttpServer.layerTest))) + + it.scoped("file", () => + Effect.gen(function*() { + yield* (yield* HttpServerResponse.file(`${__dirname}/fixtures/text.txt`).pipe( + Effect.updateService( + HttpPlatform.HttpPlatform, + (_) => ({ + ..._, + fileResponse: (path, options) => + Effect.map( + _.fileResponse(path, options), + (res) => { + ;(res as any).headers.etag = "\"etag\"" + return res + } + ) + }) + ) + )).pipe( + Effect.tapErrorCause(Effect.logError), + HttpServer.serveEffect() + ) + const client = yield* HttpClient.HttpClient + const res = yield* client.get("/") + expect(res.status).toEqual(200) + expect(res.headers["content-type"]).toEqual("text/plain") + expect(res.headers["content-length"]).toEqual("27") + expect(res.headers.etag).toEqual("\"etag\"") + const text = yield* res.text + expect(text.trim()).toEqual("lorem ipsum dolar sit amet") + }).pipe(Effect.provide(NodeHttpServer.layerTest))) + + it.scoped("fileWeb", () => + Effect.gen(function*() { + const now = new Date() + const file = new Buffer.File([new TextEncoder().encode("test")], "test.txt", { + type: "text/plain", + lastModified: now.getTime() + }) + yield* HttpServerResponse.fileWeb(file).pipe( + Effect.updateService( + HttpPlatform.HttpPlatform, + (_) => ({ + ..._, + fileWebResponse: (path, options) => + Effect.map( + _.fileWebResponse(path, options), + (res) => ({ ...res, headers: { ...res.headers, etag: "W/\"etag\"" } }) + ) + }) + ), + HttpServer.serveEffect() + ) + const client = yield* HttpClient.HttpClient + const res = yield* client.get("/") + expect(res.status).toEqual(200) + expect(res.headers["content-type"]).toEqual("text/plain") + expect(res.headers["content-length"]).toEqual("4") + expect(res.headers["last-modified"]).toEqual(now.toUTCString()) + expect(res.headers.etag).toEqual("W/\"etag\"") + const text = yield* res.text + expect(text.trim()).toEqual("test") + }).pipe(Effect.provide(NodeHttpServer.layerTest))) + + it.scoped("schemaBodyUrlParams", () => + Effect.gen(function*() { + yield* HttpRouter.empty.pipe( + HttpRouter.post( + "/todos", + Effect.flatMap( + HttpServerRequest.schemaBodyUrlParams(Schema.Struct({ + id: Schema.NumberFromString, + title: Schema.String + })), + ({ id, title }) => todoResponse({ id, title }) + ) + ), + HttpServer.serveEffect() + ) + const todo = yield* HttpClientRequest.post("/todos").pipe( + HttpClientRequest.bodyUrlParams({ id: "1", title: "test" }), + HttpClient.execute, + Effect.flatMap(HttpClientResponse.schemaBodyJson(Todo)) + ) + expect(todo).toEqual({ id: 1, title: "test" }) + }).pipe(Effect.provide(NodeHttpServer.layerTest))) + + it.scoped("schemaBodyUrlParams error", () => + Effect.gen(function*() { + yield* HttpRouter.empty.pipe( + HttpRouter.get( + "/todos", + Effect.flatMap( + HttpServerRequest.schemaBodyUrlParams(Schema.Struct({ + id: Schema.NumberFromString, + title: Schema.String + })), + ({ id, title }) => todoResponse({ id, title }) + ) + ), + HttpRouter.catchTag("ParseError", (error) => HttpServerResponse.unsafeJson({ error }, { status: 400 })), + HttpServer.serveEffect() + ) + const client = yield* HttpClient.HttpClient + const response = yield* client.get("/todos") + expect(response.status).toEqual(400) + }).pipe(Effect.provide(NodeHttpServer.layerTest))) + + it.scoped("schemaBodyFormJson", () => + Effect.gen(function*() { + yield* HttpRouter.empty.pipe( + HttpRouter.post( + "/upload", + Effect.gen(function*() { + const result = yield* HttpServerRequest.schemaBodyFormJson(Schema.Struct({ + test: Schema.String + }))("json") + expect(result.test).toEqual("content") + return HttpServerResponse.empty() + }) + ), + Effect.tapErrorCause(Effect.logError), + HttpServer.serveEffect() + ) + const client = yield* HttpClient.HttpClient + const formData = new FormData() + formData.append("json", JSON.stringify({ test: "content" })) + const response = yield* client.post("/upload", { body: HttpBody.formData(formData) }) + expect(response.status).toEqual(204) + }).pipe(Effect.provide(NodeHttpServer.layerTest))) + + it.scoped("schemaBodyFormJson file", () => + Effect.gen(function*() { + yield* HttpRouter.empty.pipe( + HttpRouter.post( + "/upload", + Effect.gen(function*() { + const result = yield* HttpServerRequest.schemaBodyFormJson(Schema.Struct({ + test: Schema.String + }))("json") + + expect(result.test).toEqual("content") + return HttpServerResponse.empty() + }) + ), + Effect.tapErrorCause(Effect.logError), + HttpServer.serveEffect() + ) + const client = yield* HttpClient.HttpClient + const formData = new FormData() + formData.append( + "json", + new Blob([JSON.stringify({ test: "content" })], { type: "application/json" }), + "test.json" + ) + const response = yield* client.post("/upload", { body: HttpBody.formData(formData) }) + expect(response.status).toEqual(204) + }).pipe(Effect.provide(NodeHttpServer.layerTest))) + + it.scoped("schemaBodyFormJson url encoded", () => + Effect.gen(function*() { + yield* HttpRouter.empty.pipe( + HttpRouter.post( + "/upload", + Effect.gen(function*() { + const result = yield* HttpServerRequest.schemaBodyFormJson(Schema.Struct({ + test: Schema.String + }))("json") + expect(result.test).toEqual("content") + return HttpServerResponse.empty() + }) + ), + Effect.tapErrorCause(Effect.logError), + HttpServer.serveEffect() + ) + const client = yield* HttpClient.HttpClient + const response = yield* client.post("/upload", { + body: HttpBody.urlParams(UrlParams.fromInput({ + json: JSON.stringify({ test: "content" }) + })) + }) + expect(response.status).toEqual(204) + }).pipe(Effect.provide(NodeHttpServer.layerTest))) + + it.scoped("tracing", () => + Effect.gen(function*() { + yield* HttpRouter.empty.pipe( + HttpRouter.get( + "/", + Effect.flatMap( + Effect.currentSpan, + (_) => HttpServerResponse.json({ spanId: _.spanId, parent: _.parent }) + ) + ), + HttpServer.serveEffect() + ) + const client = yield* HttpClient.HttpClient + const requestSpan = yield* Effect.makeSpan("client request") + const body = yield* client.get("/").pipe( + Effect.flatMap((r) => r.json), + Effect.withTracer(Tracer.make({ + span(name, parent, _, __, ___, kind) { + assert.strictEqual(name, "http.client GET") + assert.strictEqual(kind, "client") + assert(parent._tag === "Some" && parent.value._tag === "Span") + assert.strictEqual(parent.value.name, "request parent") + return requestSpan + }, + context(f, _fiber) { + return f() + } + })), + Effect.withSpan("request parent"), + Effect.repeatN(2) + ) + expect((body as any).parent.value.spanId).toEqual(requestSpan.spanId) + }).pipe(Effect.provide(NodeHttpServer.layerTest))) + + it.scopedLive("client abort", () => + Effect.gen(function*() { + const latch = yield* Deferred.make() + yield* HttpServerResponse.empty().pipe( + Effect.delay(1000), + Effect.interruptible, + HttpServer.serveEffect((app) => Effect.onExit(app, (exit) => Deferred.complete(latch, exit))) + ) + const client = yield* HttpClient.HttpClient + const fiber = yield* client.get("/").pipe(Effect.fork) + yield* Effect.sleep(100) + yield* Fiber.interrupt(fiber) + const cause = yield* Deferred.await(latch).pipe(Effect.sandbox, Effect.flip) + const [response] = HttpServerError.causeResponseStripped(cause) + expect(response.status).toEqual(499) + }).pipe(Effect.provide(NodeHttpServer.layerTest))) + + it.effect("causeResponse uses client abort when present", () => + Effect.gen(function*() { + const parentFiberId = yield* Effect.fiberId + const cause = Cause.sequential( + Cause.interrupt(parentFiberId), + Cause.interrupt(HttpServerError.clientAbortFiberId) + ) + const [response] = yield* HttpServerError.causeResponse(cause) + expect(response.status).toEqual(499) + })) + + it.scoped("multiplex", () => + Effect.gen(function*() { + yield* HttpMultiplex.empty.pipe( + HttpMultiplex.hostExact("a.example.com", HttpServerResponse.text("A")), + HttpMultiplex.hostStartsWith("b.", HttpServerResponse.text("B")), + HttpMultiplex.hostRegex(/^c\.example/, HttpServerResponse.text("C")), + HttpServer.serveEffect() + ) + const client = yield* HttpClient.HttpClient + expect( + yield* client.execute( + HttpClientRequest.get("/").pipe( + HttpClientRequest.setHeader("host", "a.example.com") + ) + ).pipe( + Effect.flatMap((r) => r.text) + ) + ).toEqual("A") + expect( + yield* client.execute( + HttpClientRequest.get("/").pipe( + HttpClientRequest.setHeader("host", "b.example.com") + ) + ).pipe( + Effect.flatMap((r) => r.text) + ) + ).toEqual("B") + expect( + yield* client.execute( + HttpClientRequest.get("/").pipe( + HttpClientRequest.setHeader("host", "b.org") + ) + ).pipe( + Effect.flatMap((r) => r.text) + ) + ).toEqual("B") + expect( + yield* client.execute( + HttpClientRequest.get("/").pipe( + HttpClientRequest.setHeader("host", "c.example.com") + ) + ).pipe( + Effect.flatMap((r) => r.text) + ) + ).toEqual("C") + }).pipe(Effect.provide(NodeHttpServer.layerTest))) + + it.scoped("html", () => + Effect.gen(function*() { + yield* HttpRouter.empty.pipe( + HttpRouter.get("/home", HttpServerResponse.html("")), + HttpRouter.get( + "/about", + HttpServerResponse.html`${Effect.succeed("")}` + ), + HttpRouter.get( + "/stream", + HttpServerResponse.htmlStream`${Stream.make("", 123, "hello")}` + ), + HttpServer.serveEffect() + ) + const client = yield* HttpClient.HttpClient + const home = yield* client.get("/home").pipe(Effect.flatMap((r) => r.text)) + expect(home).toEqual("") + const about = yield* client.get("/about").pipe(Effect.flatMap((r) => r.text)) + expect(about).toEqual("") + const stream = yield* client.get("/stream").pipe(Effect.flatMap((r) => r.text)) + expect(stream).toEqual("123hello") + }).pipe(Effect.provide(NodeHttpServer.layerTest))) + + it.scoped("setCookie", () => + Effect.gen(function*() { + yield* HttpRouter.empty.pipe( + HttpRouter.get( + "/home", + HttpServerResponse.empty().pipe( + HttpServerResponse.unsafeSetCookie("test", "value"), + HttpServerResponse.unsafeSetCookie("test2", "value2", { + httpOnly: true, + secure: true, + sameSite: "lax", + partitioned: true, + path: "/", + domain: "example.com", + expires: new Date(2022, 1, 1, 0, 0, 0, 0), + maxAge: "5 minutes" + }) + ) + ), + HttpServer.serveEffect() + ) + const client = yield* HttpClient.HttpClient + const res = yield* client.get("/home") + assert.deepStrictEqual( + res.cookies.toJSON(), + Cookies.fromReadonlyRecord({ + test: Cookies.unsafeMakeCookie("test", "value"), + test2: Cookies.unsafeMakeCookie("test2", "value2", { + httpOnly: true, + secure: true, + sameSite: "lax", + partitioned: true, + path: "/", + domain: "example.com", + expires: new Date(2022, 1, 1, 0, 0, 0, 0), + maxAge: Duration.minutes(5) + }) + }).toJSON() + ) + }).pipe(Effect.provide(NodeHttpServer.layerTest))) + + it.scopedLive("uninterruptible routes", () => + Effect.gen(function*() { + yield* HttpRouter.empty.pipe( + HttpRouter.get( + "/home", + Effect.gen(function*() { + const fiber = Option.getOrThrow(Fiber.getCurrentFiber()) + setTimeout(() => fiber.unsafeInterruptAsFork(fiber.id()), 10) + return yield* HttpServerResponse.empty().pipe(Effect.delay(50)) + }), + { uninterruptible: true } + ), + HttpServer.serveEffect() + ) + const client = yield* HttpClient.HttpClient + const res = yield* client.get("/home") + assert.strictEqual(res.status, 204) + }).pipe(Effect.provide(NodeHttpServer.layerTest))) + + describe("HttpServerRespondable", () => { + it.scoped("error/RouteNotFound", () => + Effect.gen(function*() { + yield* HttpRouter.empty.pipe(HttpServer.serveEffect()) + const client = yield* HttpClient.HttpClient + const res = yield* client.get("/") + assert.strictEqual(res.status, 404) + }).pipe(Effect.provide(NodeHttpServer.layerTest))) + + it.scoped("error/schema", () => + Effect.gen(function*() { + class CustomError extends Schema.TaggedError()("CustomError", { + name: Schema.String + }) { + [HttpServerRespondable.symbol]() { + return HttpServerResponse.schemaJson(CustomError)(this, { status: 599 }) + } + } + yield* HttpRouter.empty.pipe( + HttpRouter.get("/home", new CustomError({ name: "test" })), + HttpServer.serveEffect() + ) + const client = yield* HttpClient.HttpClient + const res = yield* client.get("/home") + assert.strictEqual(res.status, 599) + const err = yield* HttpClientResponse.schemaBodyJson(CustomError)(res) + assert.deepStrictEqual(err, new CustomError({ name: "test" })) + }).pipe(Effect.provide(NodeHttpServer.layerTest))) + + it.scoped("respondable schema", () => + Effect.gen(function*() { + class User extends Schema.Class("User")({ + name: Schema.String + }) { + [HttpServerRespondable.symbol]() { + return HttpServerResponse.schemaJson(User)(this) + } + } + yield* HttpRouter.empty.pipe( + HttpRouter.get("/user", Effect.succeed(new User({ name: "test" }))), + HttpServer.serveEffect() + ) + const client = yield* HttpClient.HttpClient + const res = yield* client.get("/user").pipe( + Effect.flatMap(HttpClientResponse.schemaBodyJson(User)) + ) + assert.deepStrictEqual(res, new User({ name: "test" })) + }).pipe(Effect.provide(NodeHttpServer.layerTest))) + }) + + it.scoped("bad middleware responds with 500", () => + Effect.gen(function*() { + yield* HttpRouter.empty.pipe( + HttpRouter.get("/", HttpServerResponse.empty()), + HttpServer.serveEffect(() => Effect.fail("boom")) + ) + const client = yield* HttpClient.HttpClient + const res = yield* client.get("/") + assert.deepStrictEqual(res.status, 500) + }).pipe(Effect.provide(NodeHttpServer.layerTest))) + + const routerA = HttpRouter.empty.pipe( + HttpRouter.get("/a", HttpServerResponse.text("a")), + HttpRouter.mountApp("/ma", HttpServerResponse.text("ma")) + ) + + const routerB = HttpRouter.empty.pipe( + HttpRouter.get("/b", HttpServerResponse.text("b")), + HttpRouter.mountApp("/mb", HttpServerResponse.text("mb")) + ) + + it.scoped("concat", () => + Effect.gen(function*() { + yield* HttpRouter.concat(routerA, routerB).pipe(HttpServer.serveEffect()) + const [responseA, responseMountA, responseB, responseMountB] = yield* Effect.all([ + HttpClient.get("/a"), + HttpClient.get("/ma"), + HttpClient.get("/b"), + HttpClient.get("/mb") + ]) + expect(yield* responseA.text).toEqual("a") + expect(yield* responseMountA.text).toEqual("ma") + expect(yield* responseB.text).toEqual("b") + expect(yield* responseMountB.text).toEqual("mb") + }).pipe(Effect.provide(NodeHttpServer.layerTest))) + + it.scoped("concatAll", () => + Effect.gen(function*() { + yield* HttpRouter.concatAll(routerA, routerB).pipe(HttpServer.serveEffect()) + const [responseA, responseMountA, responseB, responseMountB] = yield* Effect.all([ + HttpClient.get("/a"), + HttpClient.get("/ma"), + HttpClient.get("/b"), + HttpClient.get("/mb") + ]) + expect(yield* responseA.text).toEqual("a") + expect(yield* responseMountA.text).toEqual("ma") + expect(yield* responseB.text).toEqual("b") + expect(yield* responseMountB.text).toEqual("mb") + }).pipe(Effect.provide(NodeHttpServer.layerTest))) + + it.scoped("setRouterConfig", () => + Effect.gen(function*() { + yield* HttpRouter.empty.pipe( + HttpRouter.get("/:param", HttpServerResponse.empty()), + HttpServer.serveEffect() + ) + let res = yield* HttpClient.get("/123456") + assert.strictEqual(res.status, 404) + res = yield* HttpClient.get("/12345") + assert.strictEqual(res.status, 204) + }).pipe( + Effect.provide(NodeHttpServer.layerTest), + HttpRouter.withRouterConfig({ + maxParamLength: 5 + }) + )) + + it.scoped("HttpLayerRouter prefixed", () => + Effect.gen(function*() { + const handler = yield* HttpLayerRouter.toHttpEffect(HttpLayerRouter.use(Effect.fnUntraced(function*(router_) { + const router = router_.prefixed("/todos") + yield* router.add( + "GET", + "/:id", + Effect.flatMap( + HttpLayerRouter.schemaParams(IdParams), + ({ id }) => todoResponse({ id, title: "test" }) + ) + ) + yield* router.addAll([ + HttpLayerRouter.route("GET", "/", Effect.succeed(HttpServerResponse.text("root"))) + ]) + }))) + + yield* HttpServer.serveEffect(handler) + + const todo = yield* HttpClient.get("/todos/1").pipe( + Effect.flatMap(HttpClientResponse.schemaBodyJson(Todo)) + ) + expect(todo).toEqual({ id: 1, title: "test" }) + const root = yield* HttpClient.get("/todos").pipe( + Effect.flatMap((r) => r.text) + ) + expect(root).toEqual("root") + }).pipe(Effect.provide(NodeHttpServer.layerTest))) + + describe("HttpServerRequest.toWeb", () => { + it.scoped("converts POST request with body", () => + Effect.gen(function*() { + yield* HttpRouter.empty.pipe( + HttpRouter.post( + "/echo", + Effect.gen(function*() { + const request = yield* HttpServerRequest.HttpServerRequest + const webRequest = yield* HttpServerRequest.toWeb(request) + assert(webRequest !== undefined, "toWeb returned undefined") + const body = yield* Effect.promise(() => webRequest.json()) + return HttpServerResponse.unsafeJson({ received: body }) + }) + ), + HttpServer.serveEffect() + ) + const client = yield* HttpClient.HttpClient + const res = yield* client.post("/echo", { + body: HttpBody.unsafeJson({ message: "hello" }) + }) + assert.strictEqual(res.status, 200) + const json = yield* res.json + assert.deepStrictEqual(json, { received: { message: "hello" } }) + }).pipe(Effect.provide(NodeHttpServer.layerTest))) + }) +}) diff --git a/repos/effect/packages/platform-node/test/MsgPack.test.ts b/repos/effect/packages/platform-node/test/MsgPack.test.ts new file mode 100644 index 0000000..6c4876c --- /dev/null +++ b/repos/effect/packages/platform-node/test/MsgPack.test.ts @@ -0,0 +1,53 @@ +import { MsgPack } from "@effect/platform" +import { NodeSocket } from "@effect/platform-node" +import { assert, describe, test } from "@effect/vitest" +import { Chunk, Effect, Stream } from "effect" +import * as Net from "node:net" + +const server = Net.createServer((socket) => { + socket.on("data", (data) => { + socket.write(data) + }) + socket.on("end", () => { + socket.end() + }) +}) +let port = 0 +server.listen({ + port: 0 +}, () => { + port = (server.address() as Net.AddressInfo).port +}) + +describe("MsgPack", () => { + test("socket", () => + Effect.gen(function*() { + const socket = NodeSocket.makeNetChannel({ port, host: "localhost" }).pipe( + MsgPack.duplex + ) + + const outputEffect = Stream.make({ hello: "world" }, { test: 123 }).pipe( + Stream.pipeThroughChannel(socket), + Stream.runCollect + ) + const output = yield* outputEffect + + assert.deepStrictEqual(Chunk.toArray(output), [{ hello: "world" }, { test: 123 }]) + }).pipe(Effect.runPromise)) + + test("socket x10000", () => + Effect.gen(function*() { + const socket = NodeSocket.makeNetChannel({ port }).pipe( + MsgPack.duplex + ) + const msgs = Array.from({ length: 10000 }, (_, i) => ({ hello: i })) + + const outputEffect = Stream.fromIterable(msgs).pipe( + Stream.pipeThroughChannel(socket), + Stream.runCollect + ) + const output = yield* outputEffect + + assert.deepStrictEqual(Chunk.toArray(output), msgs) + }).pipe(Effect.runPromise)) +}) diff --git a/repos/effect/packages/platform-node/test/Ndjson.test.ts b/repos/effect/packages/platform-node/test/Ndjson.test.ts new file mode 100644 index 0000000..e3a3c67 --- /dev/null +++ b/repos/effect/packages/platform-node/test/Ndjson.test.ts @@ -0,0 +1,97 @@ +import { Ndjson } from "@effect/platform" +import { NodeSocket } from "@effect/platform-node" +import { assert, describe, test } from "@effect/vitest" +import { Chunk, Effect, Stream } from "effect" +import * as Net from "node:net" + +const server = Net.createServer((socket) => { + socket.on("data", (data) => { + socket.write(data) + }) + socket.on("end", () => { + socket.end() + }) +}) +let port = 0 +server.listen({ + port: 0 +}, () => { + port = (server.address() as Net.AddressInfo).port +}) + +describe("Ndjson", () => { + test("socket", () => + Effect.gen(function*() { + const socket = NodeSocket.makeNetChannel({ port, host: "localhost" }).pipe( + Ndjson.duplex() + ) + + const outputEffect = Stream.make({ hello: "world" }, { test: 123 }).pipe( + Stream.pipeThroughChannel(socket), + Stream.runCollect + ) + const output = yield* outputEffect + + assert.deepStrictEqual(Chunk.toArray(output), [{ hello: "world" }, { test: 123 }]) + }).pipe(Effect.runPromise)) + + test("socket x10000", () => + Effect.gen(function*() { + const socket = NodeSocket.makeNetChannel({ port }).pipe( + Ndjson.duplex() + ) + const msgs = Array.from({ length: 10000 }, (_, i) => ({ hello: i })) + + const outputEffect = Stream.fromIterable(msgs).pipe( + Stream.pipeThroughChannel(socket), + Stream.runCollect + ) + const output = yield* outputEffect + + assert.deepStrictEqual(Chunk.toArray(output), msgs) + }).pipe(Effect.runPromise)) + + test("should ignore empty lines", () => + Effect.gen(function*() { + const encoder = new TextEncoder() + + const ndjson = [ + "{\"id\":\"1\"}", + "{\"id\":\"2\"}", + "\n", + "{\"id\":\"3\"}", + "{\"id\":\"4\"}" + ].join("\n") + + const results = yield* Stream.succeed(encoder.encode(ndjson)).pipe( + Stream.pipeThroughChannel(Ndjson.unpack({ ignoreEmptyLines: true })), + Stream.runCollect, + Effect.map(Chunk.toReadonlyArray) + ) + + assert.deepStrictEqual(results, [{ id: "1" }, { id: "2" }, { id: "3" }, { id: "4" }]) + }).pipe(Effect.runPromise)) + + test("should not ignore empty lines", () => + Effect.gen(function*() { + const encoder = new TextEncoder() + + const ndjson = [ + "{\"id\":\"1\"}", + "{\"id\":\"2\"}", + "\n", + "{\"id\":\"3\"}", + "{\"id\":\"4\"}" + ].join("\n") + + const error = yield* Stream.succeed(encoder.encode(ndjson)).pipe( + Stream.pipeThroughChannel(Ndjson.unpack()), + Stream.runCollect, + Effect.map(Chunk.toReadonlyArray), + Effect.flip + ) + + assert.instanceOf(error, Ndjson.NdjsonError) + assert.propertyVal(error, "reason", "Unpack") + }).pipe(Effect.runPromise)) +}) diff --git a/repos/effect/packages/platform-node/test/NodeClusterSocket.test.ts b/repos/effect/packages/platform-node/test/NodeClusterSocket.test.ts new file mode 100644 index 0000000..8cf5976 --- /dev/null +++ b/repos/effect/packages/platform-node/test/NodeClusterSocket.test.ts @@ -0,0 +1,122 @@ +import { + ClusterSchema, + Entity, + MessageStorage, + RunnerAddress, + RunnerHealth, + RunnerStorage, + ShardingConfig, + SocketRunner +} from "@effect/cluster" +import { NodeClusterSocket } from "@effect/platform-node" +import { Rpc, RpcSerialization } from "@effect/rpc" +import { describe, it } from "@effect/vitest" +import { BigDecimal, Effect, Layer, Logger, LogLevel, Option, PrimaryKey, Schema } from "effect" + +class TestPayload extends Schema.Class("TestPayload")({ + id: Schema.String, + amount: Schema.BigDecimal +}) { + [PrimaryKey.symbol]() { + return this.id + } +} + +const TestEntity = Entity + .make("TestEntity", [ + Rpc.make("Process", { + payload: TestPayload, + success: Schema.Void + }) + ]) + .annotateRpcs(ClusterSchema.Persisted, true) + .annotateRpcs(ClusterSchema.Uninterruptible, true) + +const TestEntityLayer = TestEntity.toLayer( + Effect.succeed({ + Process: () => Effect.void + }) +) + +const RUNNER_PORT = 50_123 +// Build shared storage instances once, so runner and client see the same state. +// MessageStorage.layerMemory requires ShardingConfig, so we provide a minimal one. +const SharedStorage = Layer.mergeAll( + RunnerStorage.layerMemory, + MessageStorage.layerMemory +).pipe( + Layer.provide(ShardingConfig.layerDefaults) +) + +const makeRunnerLayer = (port: number) => + TestEntityLayer.pipe( + Layer.provideMerge(SocketRunner.layer), + Layer.provide(RunnerHealth.layerNoop), + Layer.provide(NodeClusterSocket.layerSocketServer), + Layer.provide(NodeClusterSocket.layerClientProtocol), + Layer.provide(ShardingConfig.layer({ + runnerAddress: Option.some(RunnerAddress.make("localhost", port)), + entityTerminationTimeout: 0, + entityMessagePollInterval: 5000, + sendRetryInterval: 100 + })), + Layer.provide(RpcSerialization.layerMsgPack) + ) + +const makeClientLayer = (port: number) => + SocketRunner.layerClientOnly.pipe( + Layer.provide(NodeClusterSocket.layerClientProtocol), + Layer.provide(ShardingConfig.layer({ + runnerAddress: Option.some(RunnerAddress.make("localhost", port)), + runnerListenAddress: Option.some(RunnerAddress.make("localhost", port)), + entityTerminationTimeout: 0, + entityMessagePollInterval: 5000, + sendRetryInterval: 100 + })), + Layer.provide(RpcSerialization.layerMsgPack) + ) + +// BigDecimal.normalize creates a circular `normalized` self-reference. +// When a persisted message is sent with discard: true, the notify path in Runners.makeRpc +// passes the raw envelope (with circular BigDecimal payload) to the runner via msgpack, +// causing RangeError: Maximum call stack size exceeded. +describe("SocketRunner", () => { + it.scopedLive( + "entity call with BigDecimal and discard should not stack overflow", + () => + Effect.gen(function*() { + // Start the runner (with socket server and entity handler) + yield* Layer.launch(makeRunnerLayer(RUNNER_PORT)).pipe(Effect.forkScoped) + + // Give the runner time to start and acquire shards + yield* Effect.sleep("2 seconds") + yield* Effect.log("Before starting the client") + + // Send a message from the client with discard: true. + // The BigDecimal is normalized to trigger the circular `normalized` self-reference. + yield* Effect.gen(function*() { + yield* Effect.log("Starting the client") + yield* Effect.sleep("2 seconds") + const makeClient = yield* TestEntity.client + // Give the client time to discover the runner + yield* Effect.sleep("3 seconds") + const client = makeClient("entity-1") + + const amount = BigDecimal.unsafeFromString("123.45") + + yield* client.Process( + TestPayload.make({ id: "req-1", amount }), + { discard: true } + ) + }).pipe( + Effect.provide(makeClientLayer(RUNNER_PORT)), + Effect.scoped + ) + }).pipe(Effect.provide( + SharedStorage.pipe(Layer.provideMerge( + Logger.minimumLogLevel(LogLevel.None) + )) + )), + 30_000 + ) +}) diff --git a/repos/effect/packages/platform-node/test/PlatformConfigProvider.test.ts b/repos/effect/packages/platform-node/test/PlatformConfigProvider.test.ts new file mode 100644 index 0000000..77c8da6 --- /dev/null +++ b/repos/effect/packages/platform-node/test/PlatformConfigProvider.test.ts @@ -0,0 +1,113 @@ +import { FileSystem, Path, PlatformConfigProvider } from "@effect/platform" +import { NodeContext } from "@effect/platform-node" +import { describe, expect, it } from "@effect/vitest" +import { Config, Effect, Either } from "effect" + +describe("dotenv", () => { + const ExampleConfig = Config.all({ + value: Config.string("VALUE"), + number: Config.number("NUMBER") + }) + + it.scopedLive.each([ + { + name: "Simple variables", + config: ExampleConfig, + content: "VALUE=hello\nNUMBER=69", + expected: { value: "hello", number: 69 } + }, + { + name: "Whitespaces", + config: ExampleConfig, + content: "VALUE= hello \n NUMBER= 69 \n\n", + expected: { value: "hello", number: 69 } + }, + { + name: "Quotes", + config: Config.all({ + value: Config.string("VALUE"), + anotherValue: Config.string("ANOTHER_VALUE") + }), + content: "VALUE=\" hello \"\nANOTHER_VALUE=' another '", + expected: { value: " hello ", anotherValue: " another " } + }, + { + name: "Expand", + config: ExampleConfig, + content: "VALUE=hello-${NUMBER}\nNUMBER=69", + expected: { value: "hello-69", number: 69 } + } + ])("parsing ($name)", ({ config, content, expected }) => + Effect.gen(function*() { + const envFile = yield* createTmpEnvFile(content) + const result = yield* (config as Config.Config).pipe( + Effect.provide(PlatformConfigProvider.layerDotEnv(envFile)) + ) + expect(result).toEqual(expected) + }).pipe(Effect.provide(NodeContext.layer))) + + it.scopedLive("load from both process env and dotenv file", () => + Effect.gen(function*() { + yield* modifyEnv("VALUE", "hello") + const envFile = yield* createTmpEnvFile("NUMBER=69") + const result = yield* ExampleConfig.pipe( + Effect.provide(PlatformConfigProvider.layerDotEnvAdd(envFile)) + ) + expect(result).toEqual({ value: "hello", number: 69 }) + }).pipe(Effect.provide(NodeContext.layer))) + + it.scopedLive("current ConfigProvider has precedence over dotenv", () => + Effect.gen(function*() { + yield* modifyEnv("VALUE", "hello") + const envFile = yield* createTmpEnvFile("NUMBER=69\nVALUE=another") + const result = yield* ExampleConfig.pipe( + Effect.provide(PlatformConfigProvider.layerDotEnvAdd(envFile)) + ) + expect(result).toEqual({ value: "hello", number: 69 }) + }).pipe(Effect.provide(NodeContext.layer))) + + it.scopedLive("fromDotEnv fails if no .env file is found", () => + Effect.gen(function*() { + const result = yield* PlatformConfigProvider.fromDotEnv(".non-existing-env-file").pipe(Effect.either) + expect(Either.isLeft(result)).toBe(true) + }).pipe(Effect.provide(NodeContext.layer))) + + it.scopedLive("layerDotEnvAdd succeeds if no .env file is found", () => + Effect.gen(function*() { + yield* modifyEnv("VALUE", "hello") + const value = yield* Config.string("VALUE") + expect(value).toEqual("hello") + }).pipe( + Effect.provide(PlatformConfigProvider.layerDotEnvAdd(".non-existing-env-file")), + Effect.provide(NodeContext.layer) + )) +}) + +// utils + +const createTmpEnvFile = (data: string) => + Effect.gen(function*() { + const fs = yield* FileSystem.FileSystem + const path = yield* Path.Path + const dir = yield* fs.makeTempDirectoryScoped({ prefix: "tmp" }) + const filename = path.join(dir, ".env") + yield* fs.writeFileString(filename, data) + return filename + }) + +const modifyEnv = (key: string, value: string) => + Effect.gen(function*() { + const isInEnv = key in process.env + const original = process.env[key] + process.env[key] = value + + yield* Effect.addFinalizer(() => + Effect.sync(() => { + if (isInEnv) { + process.env[key] = original + } else { + delete process.env[key] + } + }) + ) + }) diff --git a/repos/effect/packages/platform-node/test/RpcServer.test.ts b/repos/effect/packages/platform-node/test/RpcServer.test.ts new file mode 100644 index 0000000..374454c --- /dev/null +++ b/repos/effect/packages/platform-node/test/RpcServer.test.ts @@ -0,0 +1,185 @@ +import { HttpClient, HttpClientRequest, HttpRouter, HttpServer, SocketServer } from "@effect/platform" +import { NodeHttpServer, NodeSocket, NodeSocketServer, NodeWorker } from "@effect/platform-node" +import { RpcClient, RpcSerialization, RpcServer } from "@effect/rpc" +import { assert, describe, it } from "@effect/vitest" +import { Cause, Effect, Layer } from "effect" +import * as CP from "node:child_process" +import { RpcLive, RpcLiveDisableFatalDefects, User, UsersClient } from "./fixtures/rpc-schemas.js" +import { e2eSuite } from "./rpc-e2e.js" + +describe("RpcServer", () => { + // http ndjson + const HttpNdjsonServer = HttpRouter.Default.serve().pipe( + Layer.provide(RpcLive), + Layer.provideMerge(RpcServer.layerProtocolHttp({ path: "/rpc" })) + ) + const HttpNdjsonClient = UsersClient.layer.pipe( + Layer.provide( + RpcClient.layerProtocolHttp({ + url: "", + transformClient: HttpClient.mapRequest(HttpClientRequest.appendUrl("/rpc")) + }) + ) + ) + e2eSuite( + "e2e http ndjson", + HttpNdjsonClient.pipe( + Layer.provideMerge(HttpNdjsonServer), + Layer.provide([NodeHttpServer.layerTest, RpcSerialization.layerNdjson]) + ) + ) + e2eSuite( + "e2e http msgpack", + HttpNdjsonClient.pipe( + Layer.provideMerge(HttpNdjsonServer), + Layer.provide([NodeHttpServer.layerTest, RpcSerialization.layerMsgPack]) + ) + ) + e2eSuite( + "e2e http jsonrpc", + HttpNdjsonClient.pipe( + Layer.provideMerge(HttpNdjsonServer), + Layer.provide([NodeHttpServer.layerTest, RpcSerialization.layerNdJsonRpc()]) + ) + ) + + // websocket + const HttpWsServer = HttpRouter.Default.serve().pipe( + Layer.provide(RpcLive), + Layer.provideMerge(RpcServer.layerProtocolWebsocket({ path: "/rpc" })) + ) + const HttpWsClient = UsersClient.layer.pipe( + Layer.provide(RpcClient.layerProtocolSocket()), + Layer.provide( + Effect.gen(function*() { + const server = yield* HttpServer.HttpServer + const address = server.address as HttpServer.TcpAddress + return NodeSocket.layerWebSocket(`http://127.0.0.1:${address.port}/rpc`) + }).pipe(Layer.unwrapEffect) + ) + ) + e2eSuite( + "e2e ws ndjson", + HttpWsClient.pipe( + Layer.provideMerge(HttpWsServer), + Layer.provide([NodeHttpServer.layerTest, RpcSerialization.layerNdjson]) + ) + ) + e2eSuite( + "e2e ws json", + HttpWsClient.pipe( + Layer.provideMerge(HttpWsServer), + Layer.provide([NodeHttpServer.layerTest, RpcSerialization.layerJson]) + ) + ) + e2eSuite( + "e2e ws msgpack", + HttpWsClient.pipe( + Layer.provideMerge(HttpWsServer), + Layer.provide([NodeHttpServer.layerTest, RpcSerialization.layerMsgPack]) + ) + ) + e2eSuite( + "e2e ws jsonrpc", + HttpWsClient.pipe( + Layer.provideMerge(HttpWsServer), + Layer.provide([NodeHttpServer.layerTest, RpcSerialization.layerJsonRpc()]) + ) + ) + + // tcp + const TcpServer = RpcLive.pipe( + Layer.provideMerge(RpcServer.layerProtocolSocketServer), + Layer.provideMerge(NodeSocketServer.layer({ port: 0 })) + ) + const TcpClient = UsersClient.layer.pipe( + Layer.provide(RpcClient.layerProtocolSocket()), + Layer.provide( + Effect.gen(function*() { + const server = yield* SocketServer.SocketServer + const address = server.address as SocketServer.TcpAddress + return NodeSocket.layerNet({ port: address.port }) + }).pipe(Layer.unwrapEffect) + ) + ) + e2eSuite( + "e2e tcp ndjson", + TcpClient.pipe( + Layer.provideMerge(TcpServer), + Layer.provide([NodeHttpServer.layerTest, RpcSerialization.layerNdjson]) + ) + ) + e2eSuite( + "e2e tcp msgpack", + TcpClient.pipe( + Layer.provideMerge(TcpServer), + Layer.provide([NodeHttpServer.layerTest, RpcSerialization.layerMsgPack]) + ) + ) + e2eSuite( + "e2e tcp jsonrpc", + TcpClient.pipe( + Layer.provideMerge(TcpServer), + Layer.provide([NodeHttpServer.layerTest, RpcSerialization.layerNdJsonRpc()]) + ) + ) + + // worker + const WorkerClient = UsersClient.layer.pipe( + Layer.provide(RpcClient.layerProtocolWorker({ size: 1 })), + Layer.provide( + NodeWorker.layerPlatform(() => + CP.fork(new URL("./fixtures/rpc-worker.ts", import.meta.url), { + execPath: "tsx" + }) + ) + ), + Layer.merge(Layer.succeed(RpcServer.Protocol, { + supportsAck: true + } as any)) + ) + e2eSuite("e2e worker", WorkerClient) + + describe("RpcTest", () => { + it.effect("works", () => + Effect.gen(function*() { + const client = yield* UsersClient + const user = yield* client.GetUser({ id: "1" }) + assert.deepStrictEqual(user, new User({ id: "1", name: "Logged in user" })) + }).pipe(Effect.provide(UsersClient.layerTest))) + }) + + describe("custom defect schema", () => { + const CustomDefectServer = HttpRouter.Default.serve().pipe( + Layer.provide(RpcLiveDisableFatalDefects), + Layer.provideMerge(RpcServer.layerProtocolHttp({ path: "/rpc" })) + ) + const CustomDefectClient = UsersClient.layer.pipe( + Layer.provide( + RpcClient.layerProtocolHttp({ + url: "", + transformClient: HttpClient.mapRequest(HttpClientRequest.appendUrl("/rpc")) + }) + ) + ) + const CustomDefectLayer = CustomDefectClient.pipe( + Layer.provideMerge(CustomDefectServer), + Layer.provide([NodeHttpServer.layerTest, RpcSerialization.layerNdjson]) + ) + + it.effect("preserves full defect with Schema.Unknown", () => + Effect.gen(function*() { + const client = yield* UsersClient + const cause = yield* client.ProduceDefectCustom().pipe( + Effect.sandbox, + Effect.flip + ) + const defect = Cause.squash(cause) + assert.deepStrictEqual(defect, { + message: "detailed error", + stack: "Error: detailed error\n at handler.ts:1", + code: 42 + }) + }).pipe(Effect.provide(CustomDefectLayer))) + }) +}) diff --git a/repos/effect/packages/platform-node/test/Socket.test.ts b/repos/effect/packages/platform-node/test/Socket.test.ts new file mode 100644 index 0000000..714850e --- /dev/null +++ b/repos/effect/packages/platform-node/test/Socket.test.ts @@ -0,0 +1,119 @@ +import { Socket, type SocketServer } from "@effect/platform" +import { NodeSocket, NodeSocketServer } from "@effect/platform-node" +import { assert, describe, expect, it } from "@effect/vitest" +import { Chunk, Effect, Queue, Stream } from "effect" +import { WS } from "vitest-websocket-mock" + +const makeServer = Effect.gen(function*() { + const server = yield* NodeSocketServer.make({ port: 0 }) + + yield* server.run(Effect.fnUntraced(function*(socket) { + const write = yield* socket.writer + yield* socket.run(write) + }, Effect.scoped)).pipe(Effect.forkScoped) + + return server +}) + +describe("Socket", () => { + it.scoped("open", () => + Effect.gen(function*() { + const server = yield* makeServer + const channel = NodeSocket.makeNetChannel({ port: (server.address as SocketServer.TcpAddress).port }) + + const outputEffect = Stream.make("Hello", "World").pipe( + Stream.encodeText, + Stream.pipeThroughChannel(channel), + Stream.decodeText(), + Stream.mkString, + Stream.runCollect + ) + + const output = yield* outputEffect + assert.strictEqual(Chunk.join(output, ""), "HelloWorld") + })) + + describe("WebSocket", () => { + const url = `ws://localhost:1234` + + const makeServer = Effect.acquireRelease( + Effect.sync(() => new WS(url)), + (ws) => + Effect.sync(() => { + ws.close() + WS.clean() + }) + ) + + it.scoped("messages", () => + Effect.gen(function*() { + const server = yield* makeServer + const socket = yield* Socket.makeWebSocket(Effect.succeed(url)) + const messages = yield* Queue.unbounded() + const fiber = yield* Effect.fork(socket.run((_) => messages.offer(_))) + yield* Effect.gen(function*() { + const write = yield* socket.writer + yield* write(new TextEncoder().encode("Hello")) + yield* write(new TextEncoder().encode("World")) + }).pipe(Effect.scoped) + yield* Effect.promise(async () => { + await expect(server).toReceiveMessage(new TextEncoder().encode("Hello")) + await expect(server).toReceiveMessage(new TextEncoder().encode("World")) + }) + + server.send("Right back at you!") + let message = yield* messages.take + expect(message).toEqual(new TextEncoder().encode("Right back at you!")) + + server.send(new Blob(["A Blob message"])) + message = yield* messages.take + expect(message).toEqual(new TextEncoder().encode("A Blob message")) + + server.close() + const exit = yield* fiber.await + expect(exit._tag).toEqual("Success") + }).pipe( + Effect.provideService(Socket.WebSocketConstructor, (url) => new globalThis.WebSocket(url)) + )) + }) + + describe("TransformStream", () => { + it.scoped("works", () => + Effect.gen(function*() { + const readable = Stream.make("A", "B", "C").pipe( + Stream.tap(() => Effect.sleep(50)), + Stream.toReadableStream() + ) + const decoder = new TextDecoder() + const chunks: Array = [] + const writable = new WritableStream({ + write(chunk) { + chunks.push(decoder.decode(chunk)) + } + }) + + const socket = yield* Socket.fromTransformStream(Effect.succeed({ + readable, + writable + })) + yield* socket.writer.pipe( + Effect.tap((write) => + write("Hello").pipe( + Effect.zipRight(write("World")) + ) + ), + Effect.scoped, + Effect.forkScoped + ) + const received: Array = [] + yield* socket.run((chunk) => + Effect.sync(() => { + received.push(decoder.decode(chunk)) + }) + ).pipe(Effect.scoped) + + assert.deepStrictEqual(chunks, ["Hello", "World"]) + assert.deepStrictEqual(received, ["A", "B", "C"]) + })) + }) +}) diff --git a/repos/effect/packages/platform-node/test/fixtures/config/SHOUTING b/repos/effect/packages/platform-node/test/fixtures/config/SHOUTING new file mode 100644 index 0000000..6d4e150 --- /dev/null +++ b/repos/effect/packages/platform-node/test/fixtures/config/SHOUTING @@ -0,0 +1 @@ +value diff --git a/repos/effect/packages/platform-node/test/fixtures/config/integer b/repos/effect/packages/platform-node/test/fixtures/config/integer new file mode 100644 index 0000000..190a180 --- /dev/null +++ b/repos/effect/packages/platform-node/test/fixtures/config/integer @@ -0,0 +1 @@ +123 diff --git a/repos/effect/packages/platform-node/test/fixtures/config/secret b/repos/effect/packages/platform-node/test/fixtures/config/secret new file mode 100644 index 0000000..4a11bbc --- /dev/null +++ b/repos/effect/packages/platform-node/test/fixtures/config/secret @@ -0,0 +1 @@ +keepitsafe diff --git a/repos/effect/packages/platform-node/test/fixtures/openapi.json b/repos/effect/packages/platform-node/test/fixtures/openapi.json new file mode 100644 index 0000000..223a2fa --- /dev/null +++ b/repos/effect/packages/platform-node/test/fixtures/openapi.json @@ -0,0 +1,1002 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "API", + "version": "0.0.1", + "summary": "test api summary" + }, + "paths": { + "/groups/{id}": { + "get": { + "tags": [ + "groups" + ], + "operationId": "groups.findById", + "parameters": [ + { + "name": "id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/NumberFromString" + }, + "required": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Group", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Group" + } + } + } + }, + "400": { + "description": "The request did not match the expected schema", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HttpApiDecodeError" + } + } + } + }, + "413": { + "description": "GlobalError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GlobalError" + } + } + } + }, + "418": { + "description": "GroupError" + } + } + } + }, + "/groups": { + "post": { + "tags": [ + "groups" + ], + "operationId": "groups.create", + "parameters": [], + "security": [], + "responses": { + "200": { + "description": "Group", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Group" + } + } + } + }, + "400": { + "description": "The request did not match the expected schema", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HttpApiDecodeError" + } + } + } + }, + "413": { + "description": "GlobalError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GlobalError" + } + } + } + }, + "418": { + "description": "GroupError" + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "application/x-www-form-urlencoded": { + "schema": { + "type": "object", + "required": [ + "foo" + ], + "properties": { + "foo": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "multipart/form-data": { + "schema": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string" + } + }, + "additionalProperties": false + } + } + }, + "required": true + } + } + }, + "/groups/handle/{id}": { + "post": { + "tags": [ + "groups" + ], + "operationId": "groups.handle", + "parameters": [ + { + "name": "id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/NumberFromString" + }, + "required": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "id", + "name" + ], + "properties": { + "id": { + "type": "number" + }, + "name": { + "type": "string" + } + }, + "additionalProperties": false + } + } + } + }, + "400": { + "description": "The request did not match the expected schema", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HttpApiDecodeError" + } + } + } + }, + "413": { + "description": "GlobalError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GlobalError" + } + } + } + }, + "418": { + "description": "GroupError" + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string" + } + }, + "additionalProperties": false + } + } + }, + "required": true + } + } + }, + "/groups/handleraw/{id}": { + "post": { + "tags": [ + "groups" + ], + "operationId": "groups.handleRaw", + "parameters": [ + { + "name": "id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/NumberFromString" + }, + "required": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "id", + "name" + ], + "properties": { + "id": { + "type": "number" + }, + "name": { + "type": "string" + } + }, + "additionalProperties": false + } + } + } + }, + "400": { + "description": "The request did not match the expected schema", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HttpApiDecodeError" + } + } + } + }, + "413": { + "description": "GlobalError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GlobalError" + } + } + } + }, + "418": { + "description": "GroupError" + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string" + } + }, + "additionalProperties": false + } + } + }, + "required": true + } + } + }, + "/users/{id}": { + "get": { + "tags": [ + "Users API" + ], + "operationId": "users.findById", + "parameters": [ + { + "name": "id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/NumberFromString" + }, + "required": true + } + ], + "security": [ + { + "cookie": [] + } + ], + "responses": { + "200": { + "description": "User", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/User" + } + } + } + }, + "400": { + "description": "The request did not match the expected schema", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HttpApiDecodeError" + } + } + } + }, + "413": { + "description": "GlobalError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GlobalError" + } + } + } + } + } + } + }, + "/users": { + "post": { + "tags": [ + "Users API" + ], + "operationId": "users.create", + "parameters": [ + { + "name": "id", + "in": "query", + "schema": { + "$ref": "#/components/schemas/NumberFromString" + }, + "required": true + } + ], + "security": [ + { + "cookie": [] + } + ], + "responses": { + "200": { + "description": "User", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/User" + } + } + } + }, + "400": { + "description": "The request did not match the expected schema", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/HttpApiDecodeError" + }, + { + "$ref": "#/components/schemas/UserError" + } + ] + } + } + } + }, + "413": { + "description": "GlobalError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GlobalError" + } + } + } + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "uuid": { + "$ref": "#/components/schemas/UUID" + }, + "name": { + "type": "string" + } + }, + "additionalProperties": false + } + } + }, + "required": true + } + }, + "get": { + "tags": [ + "Users API" + ], + "operationId": "listUsers", + "parameters": [ + { + "name": "page", + "in": "header", + "schema": { + "$ref": "#/components/schemas/NumberFromString" + }, + "required": false + }, + { + "name": "query", + "in": "query", + "schema": { + "type": "string", + "description": "search query" + }, + "required": false, + "description": "search query" + } + ], + "security": [ + { + "cookie": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/User" + } + } + } + } + }, + "400": { + "description": "The request did not match the expected schema", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HttpApiDecodeError" + } + } + } + }, + "413": { + "description": "GlobalError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GlobalError" + } + } + } + }, + "500": { + "description": "NoStatusError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NoStatusError" + } + } + } + } + }, + "summary": "test summary", + "deprecated": true + } + }, + "/users/upload/{0}": { + "post": { + "tags": [ + "Users API" + ], + "operationId": "users.upload", + "parameters": [ + { + "name": "0", + "in": "path", + "schema": { + "type": "string" + }, + "required": false + } + ], + "security": [ + { + "cookie": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "contentType", + "length" + ], + "properties": { + "contentType": { + "type": "string" + }, + "length": { + "$ref": "#/components/schemas/Int" + } + }, + "additionalProperties": false + } + } + } + }, + "400": { + "description": "The request did not match the expected schema", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HttpApiDecodeError" + } + } + } + }, + "413": { + "description": "GlobalError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GlobalError" + } + } + } + } + }, + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "required": [ + "file" + ], + "properties": { + "file": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PersistedFile" + }, + "description": "an array of exactly 1 item(s)", + "title": "itemsCount(1)", + "minItems": 1, + "maxItems": 1 + } + }, + "additionalProperties": false + } + } + }, + "required": true + } + } + }, + "/users/uploadstream": { + "post": { + "tags": [ + "Users API" + ], + "operationId": "users.uploadStream", + "parameters": [], + "security": [ + { + "cookie": [] + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "contentType", + "length" + ], + "properties": { + "contentType": { + "type": "string" + }, + "length": { + "$ref": "#/components/schemas/Int" + } + }, + "additionalProperties": false + } + } + } + }, + "400": { + "description": "The request did not match the expected schema", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HttpApiDecodeError" + } + } + } + }, + "413": { + "description": "GlobalError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GlobalError" + } + } + } + } + }, + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "required": [ + "file" + ], + "properties": { + "file": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PersistedFile" + }, + "description": "an array of exactly 1 item(s)", + "title": "itemsCount(1)", + "minItems": 1, + "maxItems": 1 + } + }, + "additionalProperties": false + } + } + }, + "required": true + } + } + }, + "/healthz": { + "get": { + "tags": [ + "root" + ], + "operationId": "healthz", + "parameters": [], + "security": [], + "responses": { + "204": { + "description": "Empty" + }, + "400": { + "description": "The request did not match the expected schema", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HttpApiDecodeError" + } + } + } + }, + "413": { + "description": "GlobalError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GlobalError" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "ComponentsSchema": { + "type": "object", + "required": [ + "contentType", + "length" + ], + "properties": { + "contentType": { + "type": "string" + }, + "length": { + "$ref": "#/components/schemas/Int" + } + }, + "additionalProperties": false + }, + "Int": { + "type": "integer", + "description": "an integer", + "title": "int" + }, + "NumberFromString": { + "type": "string", + "description": "a string to be decoded into a number" + }, + "Group": { + "type": "object", + "required": [ + "id", + "name" + ], + "properties": { + "id": { + "$ref": "#/components/schemas/Int" + }, + "name": { + "type": "string" + } + }, + "additionalProperties": false + }, + "HttpApiDecodeError": { + "type": "object", + "required": [ + "issues", + "message", + "_tag" + ], + "properties": { + "issues": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Issue" + } + }, + "message": { + "type": "string" + }, + "_tag": { + "type": "string", + "enum": [ + "HttpApiDecodeError" + ] + } + }, + "additionalProperties": false, + "description": "The request did not match the expected schema" + }, + "Issue": { + "type": "object", + "required": [ + "_tag", + "path", + "message" + ], + "properties": { + "_tag": { + "type": "string", + "enum": [ + "Pointer", + "Unexpected", + "Missing", + "Composite", + "Refinement", + "Transformation", + "Type", + "Forbidden" + ], + "description": "The tag identifying the type of parse issue" + }, + "path": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PropertyKey" + }, + "description": "The path to the property where the issue occurred" + }, + "message": { + "type": "string", + "description": "A descriptive message explaining the issue" + } + }, + "additionalProperties": false, + "description": "Represents an error encountered while parsing a value to match the schema" + }, + "PropertyKey": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "object", + "required": [ + "_tag", + "key" + ], + "properties": { + "_tag": { + "type": "string", + "enum": [ + "symbol" + ] + }, + "key": { + "type": "string" + } + }, + "additionalProperties": false, + "description": "an object to be decoded into a globally shared symbol" + } + ] + }, + "GlobalError": { + "type": "object", + "required": [ + "_tag" + ], + "properties": { + "_tag": { + "type": "string", + "enum": [ + "GlobalError" + ] + } + }, + "additionalProperties": false + }, + "User": { + "type": "object", + "required": [ + "id", + "name", + "createdAt" + ], + "properties": { + "id": { + "$ref": "#/components/schemas/Int" + }, + "uuid": { + "$ref": "#/components/schemas/UUID" + }, + "name": { + "type": "string" + }, + "createdAt": { + "$ref": "#/components/schemas/DateTimeUtc" + } + }, + "additionalProperties": false + }, + "UUID": { + "type": "string", + "description": "a Universally Unique Identifier", + "format": "uuid", + "pattern": "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$" + }, + "DateTimeUtc": { + "type": "string", + "description": "a string to be decoded into a DateTime.Utc" + }, + "UserError": { + "type": "object", + "required": [ + "_tag" + ], + "properties": { + "_tag": { + "type": "string", + "enum": [ + "UserError" + ] + } + }, + "additionalProperties": false + }, + "NoStatusError": { + "type": "object", + "required": [ + "_tag" + ], + "properties": { + "_tag": { + "type": "string", + "enum": [ + "NoStatusError" + ] + } + }, + "additionalProperties": false + }, + "PersistedFile": { + "type": "string", + "format": "binary" + } + }, + "securitySchemes": { + "cookie": { + "type": "apiKey", + "name": "token", + "in": "cookie" + } + } + }, + "security": [], + "tags": [ + { + "name": "groups" + }, + { + "name": "Users API" + }, + { + "name": "root" + }, + { + "name": "Tag from OpenApi.Transform annotation" + } + ] +} diff --git a/repos/effect/packages/platform-node/test/fixtures/rpc-schemas.ts b/repos/effect/packages/platform-node/test/fixtures/rpc-schemas.ts new file mode 100644 index 0000000..150cdad --- /dev/null +++ b/repos/effect/packages/platform-node/test/fixtures/rpc-schemas.ts @@ -0,0 +1,186 @@ +import { Headers } from "@effect/platform" +import { RpcTest } from "@effect/rpc" +import * as Rpc from "@effect/rpc/Rpc" +import * as RpcClient from "@effect/rpc/RpcClient" +import type { RpcClientError } from "@effect/rpc/RpcClientError" +import * as RpcGroup from "@effect/rpc/RpcGroup" +import * as RpcMiddleware from "@effect/rpc/RpcMiddleware" +import * as RpcSchema from "@effect/rpc/RpcSchema" +import * as RpcServer from "@effect/rpc/RpcServer" +import { Context, Effect, Layer, Mailbox, Metric, Option, Schema } from "effect" + +export class User extends Schema.Class("User")({ + id: Schema.String, + name: Schema.String +}) {} + +class StreamUsers extends Schema.TaggedRequest()("StreamUsers", { + success: RpcSchema.Stream({ + success: User, + failure: Schema.Never + }), + failure: Schema.Never, + payload: { + id: Schema.String + } +}) {} + +class CurrentUser extends Context.Tag("CurrentUser")() {} + +class Unauthorized extends Schema.TaggedError("Unauthorized")("Unauthorized", {}) {} + +class AuthMiddleware extends RpcMiddleware.Tag()("AuthMiddleware", { + provides: CurrentUser, + failure: Unauthorized, + requiredForClient: true +}) {} + +class TimingMiddleware extends RpcMiddleware.Tag()("TimingMiddleware", { + wrap: true +}) {} + +class GetUser extends Rpc.make("GetUser", { + success: User, + payload: { id: Schema.String } +}) {} + +export const UserRpcs = RpcGroup.make( + GetUser, + Rpc.make("GetUserOption", { + success: Schema.Option(User), + payload: { id: Schema.String } + }), + Rpc.fromTaggedRequest(StreamUsers), + Rpc.make("GetInterrupts", { + success: Schema.Number + }), + Rpc.make("GetEmits", { + success: Schema.Number + }), + Rpc.make("ProduceDefect"), + Rpc.make("ProduceErrorDefect"), + Rpc.make("ProduceDefectCustom", { + defect: Schema.Unknown + }), + Rpc.make("Never"), + Rpc.make("nested.test"), + Rpc.make("TimedMethod", { + payload: { + shouldFail: Schema.Boolean + }, + success: Schema.Number + }).middleware(TimingMiddleware), + Rpc.make("GetTimingMiddlewareMetrics", { + success: Schema.Struct({ + success: Schema.Number, + defect: Schema.Number, + count: Schema.Number + }) + }) +).middleware(AuthMiddleware) + +const AuthLive = Layer.succeed( + AuthMiddleware, + AuthMiddleware.of((options) => + Effect.succeed( + new User({ id: options.headers.userid ?? "1", name: options.headers.name ?? "Fallback name" }) + ) + ) +) + +const rpcSuccesses = Metric.counter("rpc_middleware_success") +const rpcDefects = Metric.counter("rpc_middleware_defects") +const rpcCount = Metric.counter("rpc_middleware_count") +const TimingLive = Layer.succeed( + TimingMiddleware, + TimingMiddleware.of((options) => + options.next.pipe( + Effect.tap(Metric.increment(rpcSuccesses)), + Effect.tapDefect(() => Metric.increment(rpcDefects)), + Effect.ensuring(Metric.increment(rpcCount)) + ) + ) +) + +const UsersLive = UserRpcs.toLayer(Effect.gen(function*() { + let interrupts = 0 + let emits = 0 + return UserRpcs.of({ + GetUser: (_) => + CurrentUser.pipe( + Rpc.fork + ), + GetUserOption: Effect.fnUntraced(function*(req) { + return Option.some(new User({ id: req.id, name: "John" })) + }), + StreamUsers: Effect.fnUntraced(function*(req, _) { + const mailbox = yield* Mailbox.make(0) + + yield* Effect.addFinalizer(() => + Effect.sync(() => { + interrupts++ + }) + ) + + yield* mailbox.offer(new User({ id: req.id, name: "John" })).pipe( + Effect.tap(() => { + emits++ + }), + Effect.delay(100), + Effect.forever, + Effect.forkScoped + ) + + return mailbox + }), + GetInterrupts: () => Effect.sync(() => interrupts), + GetEmits: () => Effect.sync(() => emits), + ProduceDefect: () => Effect.die("boom"), + ProduceErrorDefect: () => Effect.die(new Error("error defect message")), + ProduceDefectCustom: () => + Effect.die({ message: "detailed error", stack: "Error: detailed error\n at handler.ts:1", code: 42 }), + Never: () => Effect.never.pipe(Effect.onInterrupt(() => Effect.sync(() => interrupts++))), + "nested.test": () => Effect.void, + TimedMethod: (_) => _.shouldFail ? Effect.die("boom") : Effect.succeed(1), + GetTimingMiddlewareMetrics: () => + Effect.all({ + defect: Metric.value(rpcDefects).pipe(Effect.map((_) => _.count)), + success: Metric.value(rpcSuccesses).pipe(Effect.map((_) => _.count)), + count: Metric.value(rpcCount).pipe(Effect.map((_) => _.count)) + }) + }) +})) + +export const RpcLive = RpcServer.layer(UserRpcs).pipe( + Layer.provide([ + UsersLive, + AuthLive, + TimingLive + ]) +) + +export const RpcLiveDisableFatalDefects = RpcServer.layer(UserRpcs, { disableFatalDefects: true }).pipe( + Layer.provide([ + UsersLive, + AuthLive, + TimingLive + ]) +) + +const AuthClient = RpcMiddleware.layerClient(AuthMiddleware, ({ request }) => + Effect.succeed({ + ...request, + headers: Headers.set(request.headers, "name", "Logged in user") + })) + +export class UsersClient extends Context.Tag("UsersClient")< + UsersClient, + RpcClient.RpcClient, RpcClientError> +>() { + static layer = Layer.scoped(UsersClient, RpcClient.make(UserRpcs)).pipe( + Layer.provide(AuthClient) + ) + static layerTest = Layer.scoped(UsersClient, RpcTest.makeClient(UserRpcs)).pipe( + Layer.provide([UsersLive, AuthLive, TimingLive, AuthClient]) + ) +} diff --git a/repos/effect/packages/platform-node/test/fixtures/rpc-worker.ts b/repos/effect/packages/platform-node/test/fixtures/rpc-worker.ts new file mode 100644 index 0000000..de567bf --- /dev/null +++ b/repos/effect/packages/platform-node/test/fixtures/rpc-worker.ts @@ -0,0 +1,11 @@ +import { NodeRuntime, NodeWorkerRunner } from "@effect/platform-node" +import { RpcServer } from "@effect/rpc" +import { Layer } from "effect" +import { RpcLive } from "./rpc-schemas.js" + +RpcLive.pipe( + Layer.provide(RpcServer.layerProtocolWorkerRunner), + Layer.provide(NodeWorkerRunner.layer), + Layer.launch, + NodeRuntime.runMain +) diff --git a/repos/effect/packages/platform-node/test/fixtures/text.txt b/repos/effect/packages/platform-node/test/fixtures/text.txt new file mode 100644 index 0000000..72b190e --- /dev/null +++ b/repos/effect/packages/platform-node/test/fixtures/text.txt @@ -0,0 +1 @@ +lorem ipsum dolar sit amet diff --git a/repos/effect/packages/platform-node/test/rpc-e2e.ts b/repos/effect/packages/platform-node/test/rpc-e2e.ts new file mode 100644 index 0000000..ee9ba38 --- /dev/null +++ b/repos/effect/packages/platform-node/test/rpc-e2e.ts @@ -0,0 +1,138 @@ +import { RpcClient, RpcServer } from "@effect/rpc" +import { assert, describe, it } from "@effect/vitest" +import type { Layer } from "effect" +import { Cause, Effect, Fiber, Option, Stream } from "effect" +import { User, UsersClient } from "./fixtures/rpc-schemas.js" + +export const e2eSuite = ( + name: string, + layer: Layer.Layer, + concurrent = true +) => { + describe(name, { concurrent, timeout: 30_000 }, () => { + it.effect("should get user", () => + Effect.gen(function*() { + const client = yield* UsersClient + const user = yield* client.GetUser({ id: "1" }) + assert.instanceOf(user, User) + assert.deepStrictEqual(user, new User({ id: "1", name: "Logged in user" })) + }).pipe(Effect.provide(layer))) + + it.effect("nested method", () => + Effect.gen(function*() { + const client = yield* UsersClient + yield* client.nested.test() + }).pipe(Effect.provide(layer))) + + it.effect("should not flatten Option", () => + Effect.gen(function*() { + const client = yield* UsersClient + const user = yield* client.GetUserOption({ id: "1" }) + assert.deepStrictEqual(user, Option.some(new User({ id: "1", name: "John" }))) + }).pipe(Effect.provide(layer))) + + it.effect("headers", () => + Effect.gen(function*() { + const client = yield* UsersClient + const user = yield* client.GetUser({ id: "1" }) + assert.instanceOf(user, User) + assert.deepStrictEqual(user, new User({ id: "123", name: "Logged in user" })) + }).pipe( + RpcClient.withHeaders({ userId: "123" }), + Effect.provide(layer) + )) + + it.live("Stream", () => + Effect.gen(function*() { + const client = yield* UsersClient + const users: Array = [] + yield* client.StreamUsers({ id: "1" }).pipe( + Stream.take(5), + Stream.runForEach((user) => + Effect.sync(() => { + users.push(user) + }) + ), + Effect.fork + ) + + yield* Effect.sleep(2000) + assert.lengthOf(users, 5) + + // test interrupts + const interrupts = yield* client.GetInterrupts() + assert.equal(interrupts, 1) + + const { supportsAck } = yield* RpcServer.Protocol + + // test backpressure + if (supportsAck) { + const emits = yield* client.GetEmits() + assert.equal(emits, 5) + } + }).pipe(Effect.provide(layer)), { timeout: 20000 }) + + it.effect("defect", () => + Effect.gen(function*() { + const client = yield* UsersClient + const cause = yield* client.ProduceDefect().pipe( + Effect.sandbox, + Effect.flip + ) + assert.deepStrictEqual(cause, Cause.die("boom")) + }).pipe( + RpcClient.withHeaders({ userId: "123" }), + Effect.provide(layer) + )) + + it.effect("defect serializes Error objects", () => + Effect.gen(function*() { + const client = yield* UsersClient + const cause = yield* client.ProduceErrorDefect().pipe( + Effect.sandbox, + Effect.flip + ) + const defect = Cause.squash(cause) + // Error details should survive serialization, not be lost as {} + assert.instanceOf(defect, Error) + assert.include(defect.message, "error defect message") + }).pipe( + RpcClient.withHeaders({ userId: "123" }), + Effect.provide(layer) + )) + + it.live("never", () => + Effect.gen(function*() { + const client = yield* UsersClient + const fiber = yield* client.Never().pipe( + Effect.fork + ) + yield* Effect.sleep(500) + assert.isNull(fiber.unsafePoll()) + + yield* Fiber.interrupt(fiber) + yield* Effect.sleep(100) + + const { supportsAck } = yield* RpcServer.Protocol + if (supportsAck) { + const interrupts = yield* client.GetInterrupts() + assert.equal(interrupts, 1) + } + }).pipe( + RpcClient.withHeaders({ userId: "123" }), + Effect.provide(layer) + )) + + it.effect("server wrap middleware", () => + Effect.gen(function*() { + const client = yield* UsersClient + const result = yield* client.TimedMethod({ shouldFail: false }) + assert.equal(result, 1) + yield* client.TimedMethod({ shouldFail: true }).pipe(Effect.exit) + const { count, defect, success } = yield* client.GetTimingMiddlewareMetrics() + assert.notEqual(count, 0) + assert.notEqual(defect, 0) + assert.notEqual(success, 0) + }).pipe(Effect.provide(layer))) + }) +} diff --git a/repos/effect/packages/platform-node/tsconfig.build.json b/repos/effect/packages/platform-node/tsconfig.build.json new file mode 100644 index 0000000..0c73ced --- /dev/null +++ b/repos/effect/packages/platform-node/tsconfig.build.json @@ -0,0 +1,17 @@ +{ + "extends": "./tsconfig.src.json", + "references": [ + { "path": "../cluster/tsconfig.build.json" }, + { "path": "../effect/tsconfig.build.json" }, + { "path": "../platform/tsconfig.build.json" }, + { "path": "../platform-node-shared/tsconfig.build.json" }, + { "path": "../rpc/tsconfig.build.json" }, + { "path": "../sql/tsconfig.build.json" } + ], + "compilerOptions": { + "tsBuildInfoFile": ".tsbuildinfo/build.tsbuildinfo", + "outDir": "build/esm", + "declarationDir": "build/dts", + "stripInternal": true + } +} diff --git a/repos/effect/packages/platform-node/tsconfig.examples.json b/repos/effect/packages/platform-node/tsconfig.examples.json new file mode 100644 index 0000000..faf5ae0 --- /dev/null +++ b/repos/effect/packages/platform-node/tsconfig.examples.json @@ -0,0 +1,12 @@ +{ + "extends": "../../tsconfig.base.json", + "include": ["examples"], + "references": [ + { "path": "tsconfig.build.json" } + ], + "compilerOptions": { + "tsBuildInfoFile": ".tsbuildinfo/examples.tsbuildinfo", + "rootDir": "examples", + "noEmit": true + } +} diff --git a/repos/effect/packages/platform-node/tsconfig.json b/repos/effect/packages/platform-node/tsconfig.json new file mode 100644 index 0000000..3edbf6b --- /dev/null +++ b/repos/effect/packages/platform-node/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.base.json", + "include": [], + "references": [ + { "path": "tsconfig.src.json" }, + { "path": "tsconfig.test.json" }, + { "path": "tsconfig.examples.json" } + ] +} diff --git a/repos/effect/packages/platform-node/tsconfig.src.json b/repos/effect/packages/platform-node/tsconfig.src.json new file mode 100644 index 0000000..0922f7a --- /dev/null +++ b/repos/effect/packages/platform-node/tsconfig.src.json @@ -0,0 +1,18 @@ +{ + "extends": "../../tsconfig.base.json", + "include": ["src"], + "references": [ + { "path": "../cluster/tsconfig.src.json" }, + { "path": "../effect/tsconfig.src.json" }, + { "path": "../platform/tsconfig.src.json" }, + { "path": "../platform-node-shared/tsconfig.src.json" }, + { "path": "../rpc/tsconfig.src.json" }, + { "path": "../sql/tsconfig.src.json" } + ], + "compilerOptions": { + "tsBuildInfoFile": ".tsbuildinfo/src.tsbuildinfo", + "rootDir": "src", + "outDir": "build/src", + "types": ["node"] + } +} diff --git a/repos/effect/packages/platform-node/tsconfig.test.json b/repos/effect/packages/platform-node/tsconfig.test.json new file mode 100644 index 0000000..500613e --- /dev/null +++ b/repos/effect/packages/platform-node/tsconfig.test.json @@ -0,0 +1,14 @@ +{ + "extends": "../../tsconfig.base.json", + "include": ["test"], + "references": [ + { "path": "tsconfig.src.json" }, + { "path": "../rpc/tsconfig.src.json" }, + { "path": "../vitest/tsconfig.src.json" } + ], + "compilerOptions": { + "tsBuildInfoFile": ".tsbuildinfo/test.tsbuildinfo", + "rootDir": "test", + "noEmit": true + } +} diff --git a/repos/effect/packages/platform-node/vitest.config.ts b/repos/effect/packages/platform-node/vitest.config.ts new file mode 100644 index 0000000..abf85c4 --- /dev/null +++ b/repos/effect/packages/platform-node/vitest.config.ts @@ -0,0 +1,10 @@ +import { mergeConfig, type ViteUserConfig } from "vitest/config" +import shared from "../../vitest.shared.js" + +const config: ViteUserConfig = { + test: { + setupFiles: ["vitest-websocket-mock"] + } +} + +export default mergeConfig(shared, config) diff --git a/repos/effect/packages/platform/CHANGELOG.md b/repos/effect/packages/platform/CHANGELOG.md new file mode 100644 index 0000000..3a8ddad --- /dev/null +++ b/repos/effect/packages/platform/CHANGELOG.md @@ -0,0 +1,6374 @@ +# @effect/platform + +## 0.96.1 + +### Patch Changes + +- [#6147](https://github.com/Effect-TS/effect/pull/6147) [`518d0e3`](https://github.com/Effect-TS/effect/commit/518d0e3f4879be6d9d9a7fa137a1820604bb3ea7) Thanks @syhstanley! - Fix `HttpLayerRouter.addHttpApi` silently skipping API-level middleware. + +- [#6191](https://github.com/Effect-TS/effect/pull/6191) [`c016642`](https://github.com/Effect-TS/effect/commit/c0166426f80b7eb8e7f7d3aecc95dcd4fdb5cb55) Thanks @IGassmann! - Update `msgpackr` to 1.11.10 to fix silent decode failures in environments that block `new Function()` at runtime (e.g. Cloudflare Workers). The new version wraps the JIT `new Function()` call in a try/catch, falling back to the interpreted path when dynamic code evaluation is blocked. + +- Updated dependencies [[`74f3267`](https://github.com/Effect-TS/effect/commit/74f3267a6cc7ed7818c4c34cc1232f7cfc7d3339)]: + - effect@3.21.2 + +## 0.96.0 + +### Patch Changes + +- Updated dependencies [[`f7bb09b`](https://github.com/Effect-TS/effect/commit/f7bb09b022f195d1f2b3c23d49e74b011ec5d109), [`bd7552a`](https://github.com/Effect-TS/effect/commit/bd7552a19cc0ed575507ac6cc0879a57e24ebd31), [`ad1a7eb`](https://github.com/Effect-TS/effect/commit/ad1a7eb7f6bebaf91c80be2443ac0439226d0098), [`0d32048`](https://github.com/Effect-TS/effect/commit/0d32048f9836e2b23a6ba3ec5f43f0a000bb92fb), [`0d32048`](https://github.com/Effect-TS/effect/commit/0d32048f9836e2b23a6ba3ec5f43f0a000bb92fb)]: + - effect@3.21.0 + +## 0.95.0 + +### Patch Changes + +- Updated dependencies [[`fc82e81`](https://github.com/Effect-TS/effect/commit/fc82e81448bd9136a37580139ce46a2c61b11b54), [`82996bc`](https://github.com/Effect-TS/effect/commit/82996bce8debffcb44feb98bb862cf2662bd56b7), [`4d97a61`](https://github.com/Effect-TS/effect/commit/4d97a61a15b9dd6a0eece65b8f0c035e16d42ada), [`f6b0960`](https://github.com/Effect-TS/effect/commit/f6b0960bf3184109920dfed16ee7dfd7d67bc0f2), [`8798a84`](https://github.com/Effect-TS/effect/commit/8798a843218e6c0c0d3a8eee83360880e370b4da)]: + - effect@3.20.0 + +## 0.94.5 + +### Patch Changes + +- [#6050](https://github.com/Effect-TS/effect/pull/6050) [`d67c708`](https://github.com/Effect-TS/effect/commit/d67c7089ba8616b2d48ef7324312267a2a6f310a) Thanks @tim-smart! - Backport Effect 4 `contentType` support for `HttpBody` JSON / URL-encoded constructors and `HttpServerResponse` JSON / URL-encoded helpers. + +- Updated dependencies [[`a8c436f`](https://github.com/Effect-TS/effect/commit/a8c436f7004cc2a8ce2daec589ea7256b91c324f)]: + - effect@3.19.17 + +## 0.94.4 + +### Patch Changes + +- [#6035](https://github.com/Effect-TS/effect/pull/6035) [`22d9d27`](https://github.com/Effect-TS/effect/commit/22d9d27bc007db86d9e4748c17324fab5f950c7d) Thanks @tim-smart! - Fix `HttpServerError.causeResponse` to prefer 499 when a client abort interrupt is present. + +## 0.94.3 + +### Patch Changes + +- [#6021](https://github.com/Effect-TS/effect/pull/6021) [`0023c19`](https://github.com/Effect-TS/effect/commit/0023c19c63c402c050d496817ba92aceea7f25b7) Thanks @codewithkenzo! - Fix `HttpClientRequest.appendUrl` to properly join URL paths. + + Previously, `appendUrl` used simple string concatenation which could produce invalid URLs: + + ```typescript + // Before (broken): + appendUrl("https://api.example.com/v1", "users") + // Result: "https://api.example.com/v1users" (missing slash!) + ``` + + Now it ensures proper path joining: + + ```typescript + // After (fixed): + appendUrl("https://api.example.com/v1", "users") + // Result: "https://api.example.com/v1/users" + ``` + +- [#6019](https://github.com/Effect-TS/effect/pull/6019) [`9a96b87`](https://github.com/Effect-TS/effect/commit/9a96b87a33a75ebc277c585e60758ab4409c0d9e) Thanks @codewithkenzo! - Fix `retryTransient` to use correct transient status codes + + Changed `isTransientResponse` from `status >= 429` to an explicit allowlist (408, 429, 500, 502, 503, 504). This correctly excludes 501 (Not Implemented) and 505+ permanent errors, while including 408 (Request Timeout) which was previously missed. + + Also aligned response retry behavior with v4: the `while` predicate now only applies to error retries, not response retries. Response retries are determined solely by `isTransientResponse`. This matches the semantic intent since `while` is typed for errors, not responses. + + Fixes #5995 + +- Updated dependencies [[`e71889f`](https://github.com/Effect-TS/effect/commit/e71889f35b081d13b7da2c04d2f81d6933056b49)]: + - effect@3.19.16 + +## 0.94.2 + +### Patch Changes + +- [#5977](https://github.com/Effect-TS/effect/pull/5977) [`118e7a4`](https://github.com/Effect-TS/effect/commit/118e7a4af5b86f6d707a40d3b03157b6bf5827e7) Thanks @scotttrinh! - Added `rows` and `isTTY` properties to `Terminal` + +- Updated dependencies [[`7e925ea`](https://github.com/Effect-TS/effect/commit/7e925eae4a9db556bcbf7e8b6a762ccf8588aa3b), [`d7e75d6`](https://github.com/Effect-TS/effect/commit/d7e75d6d15294bbcd7ac49a0e9005848379ea86f), [`4860d1e`](https://github.com/Effect-TS/effect/commit/4860d1e09b436061ea4aeca07605a669793560fc)]: + - effect@3.19.15 + +## 0.94.1 + +### Patch Changes + +- [#5936](https://github.com/Effect-TS/effect/pull/5936) [`65e9e35`](https://github.com/Effect-TS/effect/commit/65e9e35157cbdfb40826ddad34555c4ebcf7c0b0) Thanks @schickling! - Document subtle CORS middleware `allowedHeaders` behavior: when empty array (default), it reflects back the client's `Access-Control-Request-Headers` (permissive), and when non-empty array, it only allows specified headers (restrictive). Added comprehensive JSDoc with examples. + +- [#5940](https://github.com/Effect-TS/effect/pull/5940) [`ee69cd7`](https://github.com/Effect-TS/effect/commit/ee69cd796feb3d8d1046f52edd8950404cd4ed0e) Thanks @kitlangton! - HttpServerResponse: fix `fromWeb` to preserve Content-Type header when response has a body + + Previously, when converting a web `Response` to an `HttpServerResponse` via `fromWeb`, the `Content-Type` header was not passed to `Body.stream()`, causing it to default to `application/octet-stream`. This affected any code using `HttpApp.fromWebHandler` to wrap web handlers, as JSON responses would incorrectly have their Content-Type set to `application/octet-stream` instead of `application/json`. + +- Updated dependencies [[`488d6e8`](https://github.com/Effect-TS/effect/commit/488d6e870eda3dfc137f4940bb69416f61ed8fe3)]: + - effect@3.19.14 + +## 0.94.0 + +### Minor Changes + +- [#5917](https://github.com/Effect-TS/effect/pull/5917) [`ff7053f`](https://github.com/Effect-TS/effect/commit/ff7053f6d8508567b6145239f97aacc5773b0c53) Thanks @tim-smart! - support non-errors in HttpClient.retryTransient + +### Patch Changes + +- Updated dependencies [[`77eeb86`](https://github.com/Effect-TS/effect/commit/77eeb86ddf208e51ec25932af83d52d3b4700371), [`287c32c`](https://github.com/Effect-TS/effect/commit/287c32c9f10da8e96f2b9ef8424316189d9ad4b3)]: + - effect@3.19.13 + +## 0.93.8 + +### Patch Changes + +- [#5902](https://github.com/Effect-TS/effect/pull/5902) [`a0a84d8`](https://github.com/Effect-TS/effect/commit/a0a84d8df05d18023ffcb1f60af91d14c2b8db57) Thanks @tim-smart! - add HttpApp.fromWebHandler + +- Updated dependencies [[`a6dfca9`](https://github.com/Effect-TS/effect/commit/a6dfca93b676eeffe4db64945b01e2004b395cb8)]: + - effect@3.19.12 + +## 0.93.7 + +### Patch Changes + +- [#5896](https://github.com/Effect-TS/effect/pull/5896) [`65bff45`](https://github.com/Effect-TS/effect/commit/65bff451fc54d47b32995b3bc898ccc5f8b1beb6) Thanks @tim-smart! - add basic apis for converting to web Request/Response + +## 0.93.6 + +### Patch Changes + +- [#5835](https://github.com/Effect-TS/effect/pull/5835) [`25d1cb6`](https://github.com/Effect-TS/effect/commit/25d1cb60aadf8f8fdf9a4aad3dbaa31e1ca3b70d) Thanks @tim-smart! - consider clean http interrupts as successful responses + +## 0.93.5 + +### Patch Changes + +- [#5818](https://github.com/Effect-TS/effect/pull/5818) [`ebfbbd6`](https://github.com/Effect-TS/effect/commit/ebfbbd62e1daf235d1f25b825d80ae4880408df3) Thanks @KhraksMamtsov! - Support `HttpApiError` unification + +## 0.93.4 + +### Patch Changes + +- [#5797](https://github.com/Effect-TS/effect/pull/5797) [`8ebd29e`](https://github.com/Effect-TS/effect/commit/8ebd29ec10976222c200901d9b72779af743e6d5) Thanks @tim-smart! - use original status code if headers have already been sent + +## 0.93.3 + +### Patch Changes + +- [#5759](https://github.com/Effect-TS/effect/pull/5759) [`e144f02`](https://github.com/Effect-TS/effect/commit/e144f02c93258f0bb37bd10ee9849f2836914e2f) Thanks @rohovskoi! - fix scalar configuration and types + +## 0.93.2 + +### Patch Changes + +- [#5737](https://github.com/Effect-TS/effect/pull/5737) [`2bb8242`](https://github.com/Effect-TS/effect/commit/2bb8242cb094e516665116707b798fc8e2bc5837) Thanks @tim-smart! - ensure HttpApiScalar source is tree-shakable + +## 0.93.1 + +### Patch Changes + +- [#5728](https://github.com/Effect-TS/effect/pull/5728) [`1961185`](https://github.com/Effect-TS/effect/commit/1961185e502459a188216f319a38c3dfe30fc6f0) Thanks @kitlangton! - Fix UrlParams.setAll overwrite semantics + +## 0.93.0 + +### Patch Changes + +- [#5606](https://github.com/Effect-TS/effect/pull/5606) [`24a1685`](https://github.com/Effect-TS/effect/commit/24a1685c70a9ed157468650f95a5c3da3f2c2433) Thanks @tim-smart! - expose Layer output in HttpLayerRouter.serve + +- Updated dependencies [[`3c15d5f`](https://github.com/Effect-TS/effect/commit/3c15d5f99fb8d8470a00c5a33d9ba3cac89dfe4c), [`3863fa8`](https://github.com/Effect-TS/effect/commit/3863fa89f61e63e5529fd961e37333bddf7db64a), [`2a03c76`](https://github.com/Effect-TS/effect/commit/2a03c76c2781ca7e9e228e838eab2eb0d0795b1d), [`24a1685`](https://github.com/Effect-TS/effect/commit/24a1685c70a9ed157468650f95a5c3da3f2c2433)]: + - effect@3.19.0 + +## 0.92.1 + +### Patch Changes + +- [#5588](https://github.com/Effect-TS/effect/pull/5588) [`f6987c0`](https://github.com/Effect-TS/effect/commit/f6987c04ebf1386dc37729dfea1631ce364a5a96) Thanks @wmaurer! - add additional predicate typings for HttpMiddleware.cors allowOrigins + +## 0.92.0 + +### Patch Changes + +- [#5302](https://github.com/Effect-TS/effect/pull/5302) [`c60956e`](https://github.com/Effect-TS/effect/commit/c60956e18fe20841d39d0127c8c488af657ab936) Thanks @OliverJAsh! - Adjust `xForwardedHeaders` middleware to always use `x-forwarded-for` + +- Updated dependencies [[`1c6ab74`](https://github.com/Effect-TS/effect/commit/1c6ab74b314b2b6df8bb1b1a0cb9527ceda0e3fa), [`70fe803`](https://github.com/Effect-TS/effect/commit/70fe803469db3355ffbf8359b52c351f1c2dc137), [`c296e32`](https://github.com/Effect-TS/effect/commit/c296e32554143b84ae8987046984e1cf1852417c), [`a098ddf`](https://github.com/Effect-TS/effect/commit/a098ddfc551f5aa0a7c36f9b4928372a64d4d9f2)]: + - effect@3.18.0 + +## 0.91.1 + +### Patch Changes + +- [#5552](https://github.com/Effect-TS/effect/pull/5552) [`ffa494c`](https://github.com/Effect-TS/effect/commit/ffa494cbc3e62039502b09718b0a9d5e0fb4e04c) Thanks @tim-smart! - allow predicates for HttpMiddleware.cors allowOrigins + +## 0.91.0 + +### Minor Changes + +- [#5549](https://github.com/Effect-TS/effect/pull/5549) [`d4d86a8`](https://github.com/Effect-TS/effect/commit/d4d86a81f02b94e09fce8004ce2c5369c505ca5a) Thanks @tim-smart! - remove msgpackr re-exports + +## 0.90.10 + +### Patch Changes + +- [#5517](https://github.com/Effect-TS/effect/pull/5517) [`de07e58`](https://github.com/Effect-TS/effect/commit/de07e5805496b80226ba6a5efc2b4c05e1aba4b8) Thanks @tim-smart! - add onOpen option to Socket.run + +## 0.90.9 + +### Patch Changes + +- [#5492](https://github.com/Effect-TS/effect/pull/5492) [`0421c8c`](https://github.com/Effect-TS/effect/commit/0421c8ce2ee614ae46b5684c850ab6aab8fa02e9) Thanks @tim-smart! - provide http span to global middleware + +## 0.90.8 + +### Patch Changes + +- [#5481](https://github.com/Effect-TS/effect/pull/5481) [`333be04`](https://github.com/Effect-TS/effect/commit/333be046b50e8300f5cb70b871448e0628b7b37c) Thanks @jpowersdev! - Allow user to set extension of file created using `FileSystem.makeTempFile` + +## 0.90.7 + +### Patch Changes + +- [#5466](https://github.com/Effect-TS/effect/pull/5466) [`75dffc8`](https://github.com/Effect-TS/effect/commit/75dffc877b1fa8c95fc026747b9060b7eba44232) Thanks @tim-smart! - ensure HttpApiClient adds encoding contentType to headers + +## 0.90.6 + +### Patch Changes + +- [#5418](https://github.com/Effect-TS/effect/pull/5418) [`7ad7b3c`](https://github.com/Effect-TS/effect/commit/7ad7b3c7de299d8d37bfcbe23b2717b7732d490b) Thanks @tim-smart! - exclude layer services from HttpLayerRouter.toWebHandler request context + +## 0.90.5 + +### Patch Changes + +- [#5410](https://github.com/Effect-TS/effect/pull/5410) [`fef9771`](https://github.com/Effect-TS/effect/commit/fef9771eab24af6415be946df0c9f64eba01cef7) Thanks @beeman! - export isQuitExection function from @effect/platform/Terminal + +- Updated dependencies [[`84bc300`](https://github.com/Effect-TS/effect/commit/84bc3003b42ad51210e9e1248efd04c5d0e3dd1e)]: + - effect@3.17.8 + +## 0.90.4 + +### Patch Changes + +- [#5402](https://github.com/Effect-TS/effect/pull/5402) [`8c7bb52`](https://github.com/Effect-TS/effect/commit/8c7bb52dc78850be72566decba6222870e3733d0) Thanks @tim-smart! - abort HttpClientResponse.stream regardless of how stream ends + +- [#5397](https://github.com/Effect-TS/effect/pull/5397) [`0e46e24`](https://github.com/Effect-TS/effect/commit/0e46e24c24e9edb8bf2e29835a94013e9c34d034) Thanks @IMax153! - Avoid issues with ESM builds by removing dependency on `@opentelemetry/semantic-conventions` + +## 0.90.3 + +### Patch Changes + +- [#5391](https://github.com/Effect-TS/effect/pull/5391) [`786867b`](https://github.com/Effect-TS/effect/commit/786867b1a443d4965aae4b4fd6391aaa85b6573a) Thanks @tim-smart! - support multiple HttpLayerRouter.addHttpApi + +## 0.90.2 + +### Patch Changes + +- [#5357](https://github.com/Effect-TS/effect/pull/5357) [`99302f4`](https://github.com/Effect-TS/effect/commit/99302f4233029ba3f4446f284d01af501cf1f4d6) Thanks @nounder! - Add `HttpServerResponse.expireCookie` + +## 0.90.1 + +### Patch Changes + +- [#5355](https://github.com/Effect-TS/effect/pull/5355) [`27a4e02`](https://github.com/Effect-TS/effect/commit/27a4e0285226cc0c084d19b5cdc4db1f38227559) Thanks @nounder! - Use `302 Found` status in `HttpServerResponse.redirect` as default + +## 0.90.0 + +### Minor Changes + +- [#5258](https://github.com/Effect-TS/effect/pull/5258) [`7813640`](https://github.com/Effect-TS/effect/commit/7813640279d9e3a3e7fc0a29bfb5c6d5fb3c270f) Thanks @kitlangton! - Changes Terminal.readInput to return a ReadonlyMailbox of events + + This allows for more efficient handling of input events, as well as ensuring + events are not lost. + +## 0.89.0 + +### Patch Changes + +- Updated dependencies [[`40c3c87`](https://github.com/Effect-TS/effect/commit/40c3c875f724264312b43002859c82bed9ad0df9), [`ed2c74a`](https://github.com/Effect-TS/effect/commit/ed2c74ae8fa4ea0dd06ea84a3e58cd32e6916104), [`073a1b8`](https://github.com/Effect-TS/effect/commit/073a1b8be5dbfa87454393ee7346f5bc36a4fd63), [`f382e99`](https://github.com/Effect-TS/effect/commit/f382e99e409838a879246250fc3994b9bf5b3c2c), [`e8c7ba5`](https://github.com/Effect-TS/effect/commit/e8c7ba5fd3eb0c3ae3039fc24c09d69391987989), [`7e10415`](https://github.com/Effect-TS/effect/commit/7e1041599ade25103428703f5d2dfd7378a09636), [`e9bdece`](https://github.com/Effect-TS/effect/commit/e9bdececdc24f60a246be5055eca71a0d49ea7f2), [`8d95eb0`](https://github.com/Effect-TS/effect/commit/8d95eb0356b1d1736204836c275d201a547d208d)]: + - effect@3.17.0 + +## 0.88.2 + +### Patch Changes + +- [#5234](https://github.com/Effect-TS/effect/pull/5234) [`de513d9`](https://github.com/Effect-TS/effect/commit/de513d9abb8311998ca7016635f53be0ac766472) Thanks @tim-smart! - ensure duplicate paths are a defect in HttpApi + +## 0.88.1 + +### Patch Changes + +- [#5192](https://github.com/Effect-TS/effect/pull/5192) [`17a5ea8`](https://github.com/Effect-TS/effect/commit/17a5ea8fa29785fe6e4c9480f2a2e9c8c59f3f38) Thanks @nikelborm! - Updated deprecated OTel Resource attributes names and values. + + Many of the attributes have undergone the process of deprecation not once, but twice. Most of the constants holding attribute names have been renamed. These are minor changes. + + Additionally, there were numerous changes to the attribute keys themselves. These changes can be considered major. + + In the `@opentelemetry/semantic-conventions` package, new attributes having ongoing discussion about them are going through a process called incubation, until a consensus about their necessity and form is reached. Otel team [recommends](https://github.com/open-telemetry/opentelemetry-js/blob/main/semantic-conventions/README.md#unstable-semconv) devs to copy them directly into their code. Luckily, it's not necessary because all of the new attribute names and values came out of this process (some of them were changed again) and are now considered stable. + + ## Reasoning for minor version bump + + | Package | Major attribute changes | Major value changes | + | -------------------------- | ----------------------------------------------------------------------------- | --------------------------------- | + | Clickhouse client | `db.system` -> `db.system.name`
`db.name` -> `db.namespace` | | + | MsSQL client | `db.system` -> `db.system.name`
`db.name` -> `db.namespace` | `mssql` -> `microsoft.sql_server` | + | MySQL client | `db.system` -> `db.system.name`
`db.name` -> `db.namespace` | | + | Pg client | `db.system` -> `db.system.name`
`db.name` -> `db.namespace` | | + | Bun SQLite client | `db.system` -> `db.system.name` | | + | Node SQLite client | `db.system` -> `db.system.name` | | + | React.Native SQLite client | `db.system` -> `db.system.name` | | + | Wasm SQLite client | `db.system` -> `db.system.name` | | + | SQLite Do client | `db.system` -> `db.system.name` | | + | LibSQL client | `db.system` -> `db.system.name` | | + | D1 client | `db.system` -> `db.system.name` | | + | Kysely client | `db.statement` -> `db.query.text` | | + | @effect/sql | `db.statement` -> `db.query.text`
`db.operation` -> `db.operation.name` | | + +- [#5211](https://github.com/Effect-TS/effect/pull/5211) [`d25f22b`](https://github.com/Effect-TS/effect/commit/d25f22be7598abe977caf6cdac3b0dd78b438c48) Thanks @mattiamanzati! - Removed some unnecessary single-arg pipe calls + +- Updated dependencies [[`f5dfabf`](https://github.com/Effect-TS/effect/commit/f5dfabf51ba481a4468c1509c537314978ef6cec), [`17a5ea8`](https://github.com/Effect-TS/effect/commit/17a5ea8fa29785fe6e4c9480f2a2e9c8c59f3f38), [`d25f22b`](https://github.com/Effect-TS/effect/commit/d25f22be7598abe977caf6cdac3b0dd78b438c48)]: + - effect@3.16.14 + +## 0.88.0 + +### Minor Changes + +- [#5208](https://github.com/Effect-TS/effect/pull/5208) [`dbabf5e`](https://github.com/Effect-TS/effect/commit/dbabf5e76fa63b050d2b6c466713c7dc59f07d3c) Thanks @tim-smart! - consolidate Http web handler layer apis + +### Patch Changes + +- [#5206](https://github.com/Effect-TS/effect/pull/5206) [`27206d7`](https://github.com/Effect-TS/effect/commit/27206d7f0558d7fe28de57bf54f1d0cc83acc92e) Thanks @tim-smart! - lazily build HttpLayerRouter web handlers + +## 0.87.13 + +### Patch Changes + +- Updated dependencies [[`c1c05a8`](https://github.com/Effect-TS/effect/commit/c1c05a8242fb5df7445b4a12387a60eac7726eb7), [`81fe4a2`](https://github.com/Effect-TS/effect/commit/81fe4a2c81d5e30e180a60e68c52016a27b350db)]: + - effect@3.16.13 + +## 0.87.12 + +### Patch Changes + +- [#5177](https://github.com/Effect-TS/effect/pull/5177) [`32ba77a`](https://github.com/Effect-TS/effect/commit/32ba77ae304d2161362a73e8b61965332626cf2d) Thanks @johtso! - Fix KeyValueStore.make type mismatch + +- [#5174](https://github.com/Effect-TS/effect/pull/5174) [`d5e25b2`](https://github.com/Effect-TS/effect/commit/d5e25b237f05670ee42b386cb40b2cb448fc11d7) Thanks @schickling! - feat(platform): add recursive option to FileSystem.watch + + Added a `recursive` option to `FileSystem.watch` that allows watching for changes in subdirectories. When set to `true`, the watcher will monitor changes in all nested directories. + + Note: The recursive option is only supported on macOS and Windows. On other platforms, it will be ignored. + + Example: + + ```ts + import { FileSystem } from "@effect/platform" + import { Effect, Stream } from "effect" + + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem + + // Watch directory and all subdirectories + yield* fs + .watch("src", { recursive: true }) + .pipe(Stream.runForEach(console.log)) + }) + ``` + +## 0.87.11 + +### Patch Changes + +- [#5184](https://github.com/Effect-TS/effect/pull/5184) [`001392b`](https://github.com/Effect-TS/effect/commit/001392ba8bfcad101bb034348a7415012fb12f72) Thanks @tim-smart! - ensure HttpApiClient schemas are composed correctly + +- [#5181](https://github.com/Effect-TS/effect/pull/5181) [`7bfb099`](https://github.com/Effect-TS/effect/commit/7bfb099cb5528511b8d63045c4fbb4dc9cb18528) Thanks @tim-smart! - update find-my-way-ts + +## 0.87.10 + +### Patch Changes + +- [#5175](https://github.com/Effect-TS/effect/pull/5175) [`678318d`](https://github.com/Effect-TS/effect/commit/678318d2e88233156b006acda56c2d138ee3ffa0) Thanks @tim-smart! - rename HttpLayerRouter.Type to Request + +- [#5175](https://github.com/Effect-TS/effect/pull/5175) [`678318d`](https://github.com/Effect-TS/effect/commit/678318d2e88233156b006acda56c2d138ee3ffa0) Thanks @tim-smart! - propagate headers to HttpServerResponse.raw(Response) + +## 0.87.9 + +### Patch Changes + +- [#5170](https://github.com/Effect-TS/effect/pull/5170) [`54514a2`](https://github.com/Effect-TS/effect/commit/54514a2f53166de27ad7e756dbf12194691fd4af) Thanks @tim-smart! - add HttpLayerRouter.toWebHandler + +## 0.87.8 + +### Patch Changes + +- [#5166](https://github.com/Effect-TS/effect/pull/5166) [`4ce4f82`](https://github.com/Effect-TS/effect/commit/4ce4f824f6fdef492be1d35c05a490ffce518c89) Thanks @tim-smart! - add global middleware to HttpLayerRouter + +## 0.87.7 + +### Patch Changes + +- [#5153](https://github.com/Effect-TS/effect/pull/5153) [`a9b617f`](https://github.com/Effect-TS/effect/commit/a9b617f125171ed76cd79ab46d7a924daf3b0e70) Thanks @thewilkybarkid! - Fix UrlParams.toRecord when there's a **proto** key + +- [#5159](https://github.com/Effect-TS/effect/pull/5159) [`7e26e86`](https://github.com/Effect-TS/effect/commit/7e26e86524abcc93713d6ad7eee486638c98f7c2) Thanks @tim-smart! - add HttpLayerRouter.add & addAll apis + +## 0.87.6 + +### Patch Changes + +- Updated dependencies [[`905da99`](https://github.com/Effect-TS/effect/commit/905da996aad665057b4ca6dba1a4af44fb8835bd)]: + - effect@3.16.12 + +## 0.87.5 + +### Patch Changes + +- [#5142](https://github.com/Effect-TS/effect/pull/5142) [`2fd8676`](https://github.com/Effect-TS/effect/commit/2fd8676c803cd40000dfc3231f5daecaa0e0ebd2) Thanks @tim-smart! - improve type safety of HttpLayerRouter.middleware error handling + +## 0.87.4 + +### Patch Changes + +- [#5137](https://github.com/Effect-TS/effect/pull/5137) [`e82a4fd`](https://github.com/Effect-TS/effect/commit/e82a4fd60f6528d08cef1a4aba0abe0d3ba741ad) Thanks @tim-smart! - move HttpLayerRouter request errors to Layer error channel + +## 0.87.3 + +### Patch Changes + +- [#5128](https://github.com/Effect-TS/effect/pull/5128) [`1b6e396`](https://github.com/Effect-TS/effect/commit/1b6e396d699f3cbbc56b68f99055cf746529bb9e) Thanks @tim-smart! - attach http request scope to stream lifetime for stream responses + +## 0.87.2 + +### Patch Changes + +- [#5111](https://github.com/Effect-TS/effect/pull/5111) [`4fea68c`](https://github.com/Effect-TS/effect/commit/4fea68ca7a25a3c39a1ab68b3885534513ab0c81) Thanks @mlegenhausen! - `HttpRouter.mountApp` prefix matching fixed + +- [#5117](https://github.com/Effect-TS/effect/pull/5117) [`b927954`](https://github.com/Effect-TS/effect/commit/b9279543cf5688dd8a577af80456959c615217d0) Thanks @tim-smart! - add HttpLayerRouter module + + The experimental HttpLayerRouter module provides a simplified way to create HTTP servers. + + You can read more in the /platform README: + + https://github.com/Effect-TS/effect/blob/main/packages/platform/README.md#httplayerrouter + +- Updated dependencies [[`99590a6`](https://github.com/Effect-TS/effect/commit/99590a6ca9128eb1ede265b6670b655311995614), [`6c3e24c`](https://github.com/Effect-TS/effect/commit/6c3e24c2308f7d4a29b8f4270ab81bca22ac6bb4)]: + - effect@3.16.11 + +## 0.87.1 + +### Patch Changes + +- Updated dependencies [[`faad30e`](https://github.com/Effect-TS/effect/commit/faad30ec8742916be59f9db642d0fc98225b636c)]: + - effect@3.16.10 + +## 0.87.0 + +### Minor Changes + +- [#5087](https://github.com/Effect-TS/effect/pull/5087) [`b5bac9a`](https://github.com/Effect-TS/effect/commit/b5bac9ac2913fcd11b02322624f03b544eef53ba) Thanks @tim-smart! - add HttpApiClient.makeWith, for supporting passing in HttpClient with errors and requirements + +## 0.86.0 + +### Minor Changes + +- [#5081](https://github.com/Effect-TS/effect/pull/5081) [`5137c70`](https://github.com/Effect-TS/effect/commit/5137c703461d8d3b363c112140a6e7f798241d07) Thanks @tim-smart! - use Context.Reference for Multipart configuration + +### Patch Changes + +- [#5081](https://github.com/Effect-TS/effect/pull/5081) [`5137c70`](https://github.com/Effect-TS/effect/commit/5137c703461d8d3b363c112140a6e7f798241d07) Thanks @tim-smart! - allow configuring multipart limits in HttpApiSchema.Multipart + +- Updated dependencies [[`5137c70`](https://github.com/Effect-TS/effect/commit/5137c703461d8d3b363c112140a6e7f798241d07), [`c23d25c`](https://github.com/Effect-TS/effect/commit/c23d25c3e7c541f1f63b28484d8c461d86c67e99)]: + - effect@3.16.9 + +## 0.85.2 + +### Patch Changes + +- [#5053](https://github.com/Effect-TS/effect/pull/5053) [`914a191`](https://github.com/Effect-TS/effect/commit/914a191e7cb6341a3d0e965bccd27c336cf22e44) Thanks @tim-smart! - fix retrieval of HttpApiSchema.param annotations + +## 0.85.1 + +### Patch Changes + +- Updated dependencies [[`8cb98d5`](https://github.com/Effect-TS/effect/commit/8cb98d53e68330228287ce2a2e0d8a4c86bcab3b), [`db2dd3c`](https://github.com/Effect-TS/effect/commit/db2dd3c3a8a77d791eae19e66153527e1cde4e6e)]: + - effect@3.16.8 + +## 0.85.0 + +### Minor Changes + +- [#5042](https://github.com/Effect-TS/effect/pull/5042) [`93687dd`](https://github.com/Effect-TS/effect/commit/93687ddbb25ce3b324cd2b83d2ccff225e97307e) Thanks @tim-smart! - HttpApiBuilder .handleRaw no longer parses the request body + +### Patch Changes + +- [#5042](https://github.com/Effect-TS/effect/pull/5042) [`93687dd`](https://github.com/Effect-TS/effect/commit/93687ddbb25ce3b324cd2b83d2ccff225e97307e) Thanks @tim-smart! - allow return HttpServerResponse from HttpApiBuilder .handle + +- [#5042](https://github.com/Effect-TS/effect/pull/5042) [`93687dd`](https://github.com/Effect-TS/effect/commit/93687ddbb25ce3b324cd2b83d2ccff225e97307e) Thanks @tim-smart! - add HttpApiSchema.MultipartStream + +## 0.84.11 + +### Patch Changes + +- Updated dependencies [[`1bb0d8a`](https://github.com/Effect-TS/effect/commit/1bb0d8ab96782e99434356266b38251554ea0294)]: + - effect@3.16.7 + +## 0.84.10 + +### Patch Changes + +- [#5032](https://github.com/Effect-TS/effect/pull/5032) [`bf369b2`](https://github.com/Effect-TS/effect/commit/bf369b2902a0e0b195d957c18b9efd180942cf8b) Thanks @tim-smart! - allow property signatures in HttpApiSchema.param + +- Updated dependencies [[`a5f7595`](https://github.com/Effect-TS/effect/commit/a5f75956ef9a15a83c416517ef493f0ee2f5ee8a), [`a02470c`](https://github.com/Effect-TS/effect/commit/a02470c75579e91525a25adb3f21b3650d042fdd), [`f891d45`](https://github.com/Effect-TS/effect/commit/f891d45adffdafd3f94a2eca23faa354e3a409a8)]: + - effect@3.16.6 + +## 0.84.9 + +### Patch Changes + +- Updated dependencies [[`bf418ef`](https://github.com/Effect-TS/effect/commit/bf418ef14a0f2ec965535793d5cea8fa8ba177ac)]: + - effect@3.16.5 + +## 0.84.8 + +### Patch Changes + +- [#4996](https://github.com/Effect-TS/effect/pull/4996) [`8b9db77`](https://github.com/Effect-TS/effect/commit/8b9db7742846af0f58fd8e8b7acb7f4f5ff487ec) Thanks @tim-smart! - allow literals in HttpApiSchema.param + +## 0.84.7 + +### Patch Changes + +- Updated dependencies [[`74ab9a0`](https://github.com/Effect-TS/effect/commit/74ab9a0a9e16d6e019369d256e1e24175c8bc3f3), [`770008e`](https://github.com/Effect-TS/effect/commit/770008eca3aad2899a2ed951236e575793294b28)]: + - effect@3.16.4 + +## 0.84.6 + +### Patch Changes + +- [#4975](https://github.com/Effect-TS/effect/pull/4975) [`ceea77a`](https://github.com/Effect-TS/effect/commit/ceea77a13055f145520f763e3fce5b8ff15d728f) Thanks @tim-smart! - allow wrapping a web Response with HttpServerResponse.raw on some platforms + +## 0.84.5 + +### Patch Changes + +- [#4964](https://github.com/Effect-TS/effect/pull/4964) [`ec52c6a`](https://github.com/Effect-TS/effect/commit/ec52c6a2211e76972462b15b9d5a9d6d56761b7a) Thanks @tim-smart! - ensure HttpApi security middleware cache is not shared + +## 0.84.4 + +### Patch Changes + +- Updated dependencies [[`87722fc`](https://github.com/Effect-TS/effect/commit/87722fce693a9b49284bbddbf82d30714c688261), [`36217ee`](https://github.com/Effect-TS/effect/commit/36217eeb1337edd9ac3f9a635b80a6385d22ae8f)]: + - effect@3.16.3 + +## 0.84.3 + +### Patch Changes + +- [#4941](https://github.com/Effect-TS/effect/pull/4941) [`ab7684f`](https://github.com/Effect-TS/effect/commit/ab7684f1c2a0671bf091f255d220e3a4cc7f528e) Thanks @tim-smart! - decode HttpApiClient response from ArrayBuffer + +## 0.84.2 + +### Patch Changes + +- Updated dependencies [[`0ddf148`](https://github.com/Effect-TS/effect/commit/0ddf148a247aa87af043d276b8453a714a400897)]: + - effect@3.16.2 + +## 0.84.1 + +### Patch Changes + +- [#4936](https://github.com/Effect-TS/effect/pull/4936) [`71174d0`](https://github.com/Effect-TS/effect/commit/71174d09691314a9b6b66189e456fd21e3eb6543) Thanks @mattiamanzati! - Escape JSON Schema $id for empty struct + +- Updated dependencies [[`71174d0`](https://github.com/Effect-TS/effect/commit/71174d09691314a9b6b66189e456fd21e3eb6543), [`d615e6e`](https://github.com/Effect-TS/effect/commit/d615e6e5b944f6fd5e627e31752c7ca7e4e1c17d)]: + - effect@3.16.1 + +## 0.84.0 + +### Patch Changes + +- Updated dependencies [[`ee0bd5d`](https://github.com/Effect-TS/effect/commit/ee0bd5d24864752c54cb359f67a67dd903971ec4), [`5189800`](https://github.com/Effect-TS/effect/commit/51898004e11766b8cf6d95e960b636f6d5db79ec), [`58bfeaa`](https://github.com/Effect-TS/effect/commit/58bfeaa64ded8c88f772b184311c0c0dbac10960), [`194d748`](https://github.com/Effect-TS/effect/commit/194d7486943f56f3267ef415395ac220a4b3e634), [`918c9ea`](https://github.com/Effect-TS/effect/commit/918c9ea1a57facb154f0fb26792021f337054dee), [`9198e6f`](https://github.com/Effect-TS/effect/commit/9198e6fcc1a3ff4fefb3363004de558d8de01f40), [`2a370bf`](https://github.com/Effect-TS/effect/commit/2a370bf625fdeede5659721468eb0d527e403279), [`58ccb91`](https://github.com/Effect-TS/effect/commit/58ccb91328c8df5d49808b673738bc09df355201), [`fd47834`](https://github.com/Effect-TS/effect/commit/fd478348203fa89462b0a1d067ce4de034353df4)]: + - effect@3.16.0 + +## 0.83.0 + +### Minor Changes + +- [#4932](https://github.com/Effect-TS/effect/pull/4932) [`5522520`](https://github.com/Effect-TS/effect/commit/55225206ab9af0ad60b1c0654690a8a096d625cd) Thanks @tim-smart! - refactor PlatformError and make it a schema + +### Patch Changes + +- Updated dependencies [[`cc5bb2b`](https://github.com/Effect-TS/effect/commit/cc5bb2b918a9450a975f702dabcea891bda382cb)]: + - effect@3.15.5 + +## 0.82.8 + +### Patch Changes + +- [#4927](https://github.com/Effect-TS/effect/pull/4927) [`0617b9d`](https://github.com/Effect-TS/effect/commit/0617b9dc365f1963b36949ad7f9023ab6eb94524) Thanks @fubhy! - Fix package internal imports + +## 0.82.7 + +### Patch Changes + +- [#4921](https://github.com/Effect-TS/effect/pull/4921) [`c20b95a`](https://github.com/Effect-TS/effect/commit/c20b95a99ffe452b4774c844d397a905f713b6d6) Thanks @tim-smart! - update /platform dependencies + +- [#4916](https://github.com/Effect-TS/effect/pull/4916) [`94ada43`](https://github.com/Effect-TS/effect/commit/94ada430928d5685bdbef513e87562c20774a3a2) Thanks @mattiamanzati! - Fix missing encoding of path parameters in HttpApiClient + +- Updated dependencies [[`f570554`](https://github.com/Effect-TS/effect/commit/f57055459524587b041340577dad85476bb35f81), [`78047e8`](https://github.com/Effect-TS/effect/commit/78047e8dfc8005b66f87afe50bb95981fea51561)]: + - effect@3.15.4 + +## 0.82.6 + +### Patch Changes + +- [#4855](https://github.com/Effect-TS/effect/pull/4855) [`618903b`](https://github.com/Effect-TS/effect/commit/618903ba9ae96e2bfe6ee31f61c4359b915f2a36) Thanks @gcanti! - Enhance OpenAPI documentation handling by adding safe serialization and HTML escaping functions. This prevents script injection and ensures valid JSON output in the Swagger UI + +## 0.82.5 + +### Patch Changes + +- [#4912](https://github.com/Effect-TS/effect/pull/4912) [`7764a07`](https://github.com/Effect-TS/effect/commit/7764a07d960c60df81f14e1dc949518f4bbe494a) Thanks @tim-smart! - add HttpClient.withScope, for tying the lifetime of the request to a Scope + +- [#4909](https://github.com/Effect-TS/effect/pull/4909) [`30a0d9c`](https://github.com/Effect-TS/effect/commit/30a0d9cb51c84290d51b1361d72ff5cee33c13c7) Thanks @tim-smart! - add HttpClientRequest.toUrl + +- Updated dependencies [[`4577f54`](https://github.com/Effect-TS/effect/commit/4577f548d67273e576cdde423bdd34a4b910766a)]: + - effect@3.15.3 + +## 0.82.4 + +### Patch Changes + +- [#4896](https://github.com/Effect-TS/effect/pull/4896) [`d45e8a8`](https://github.com/Effect-TS/effect/commit/d45e8a8ac8227192f504e39e6d04fdcf4fb1d225) Thanks @seniorkonung! - Handle `Respondable` defects in `toResponseOrElseDefect` + +- [#4890](https://github.com/Effect-TS/effect/pull/4890) [`d13b68e`](https://github.com/Effect-TS/effect/commit/d13b68e3a9456d0bfee9bca8273a7b44a9c69087) Thanks @KhraksMamtsov! - `Url.setPassword` supports `Redacted` values + +## 0.82.3 + +### Patch Changes + +- [#4889](https://github.com/Effect-TS/effect/pull/4889) [`a328f4b`](https://github.com/Effect-TS/effect/commit/a328f4b4fe717dd53e5b04a30f387433c32f7328) Thanks @tim-smart! - add HttpBody.formDataRecord + +- Updated dependencies [[`b8722b8`](https://github.com/Effect-TS/effect/commit/b8722b817e2306fe8c8245f3f9e32d85b824b961)]: + - effect@3.15.2 + +## 0.82.2 + +### Patch Changes + +- [#4882](https://github.com/Effect-TS/effect/pull/4882) [`739a3d4`](https://github.com/Effect-TS/effect/commit/739a3d4a4565915fe2e690003f4f9085cb4422fc) Thanks @tim-smart! - remove content headers for FormData bodies + +## 0.82.1 + +### Patch Changes + +- Updated dependencies [[`787ce70`](https://github.com/Effect-TS/effect/commit/787ce7042e35b657963473c6efe47752868cd811), [`1269641`](https://github.com/Effect-TS/effect/commit/1269641a99ae43069f7648ff79ffe8729b54b348), [`1269641`](https://github.com/Effect-TS/effect/commit/1269641a99ae43069f7648ff79ffe8729b54b348)]: + - effect@3.15.1 + +## 0.82.0 + +### Minor Changes + +- [#4641](https://github.com/Effect-TS/effect/pull/4641) [`a9b3fb7`](https://github.com/Effect-TS/effect/commit/a9b3fb78abcfdb525318a956fd02fcadeb56143e) Thanks @thewilkybarkid! - Allow removing multiple Headers + +### Patch Changes + +- Updated dependencies [[`c654595`](https://github.com/Effect-TS/effect/commit/c65459587b51da140b78098e81fdbfece65d53e2), [`d9f5dea`](https://github.com/Effect-TS/effect/commit/d9f5deae0f02f5de2b9fcb1cca8b142ba4bc2bba), [`49aa723`](https://github.com/Effect-TS/effect/commit/49aa7236a15e13f818c86edbca08c4af67c8dfaf), [`74c14d0`](https://github.com/Effect-TS/effect/commit/74c14d01d0cb48cf517a1b6e29a373a96ed0ff5b), [`e4f49b6`](https://github.com/Effect-TS/effect/commit/e4f49b66857e01b74ab6a9a0bc7132f44cd04cbb), [`6f02224`](https://github.com/Effect-TS/effect/commit/6f02224b3fc46a682ad2defb1a260841956c6780), [`1dcfd41`](https://github.com/Effect-TS/effect/commit/1dcfd41ff96abd706901293a00c1893cb29dd8fd), [`b21ab16`](https://github.com/Effect-TS/effect/commit/b21ab16b6f773e7ec4369db4e752c35e719f7870), [`fcf1822`](https://github.com/Effect-TS/effect/commit/fcf1822f98fcda60351d64e9d2c2c13563d7e6db), [`0061dd1`](https://github.com/Effect-TS/effect/commit/0061dd140740165e91569a684cce27a77b23229e), [`8421e6e`](https://github.com/Effect-TS/effect/commit/8421e6e49332bca8f96f482dfd48680e238b3a89), [`fa10f56`](https://github.com/Effect-TS/effect/commit/fa10f56b96bd9af070ba99ebc3279aa93954261e)]: + - effect@3.15.0 + +## 0.81.1 + +### Patch Changes + +- Updated dependencies [[`24a9ebb`](https://github.com/Effect-TS/effect/commit/24a9ebbb5af598f0bfd6ecc45307e528043fe011)]: + - effect@3.14.22 + +## 0.81.0 + +### Minor Changes + +- [#4842](https://github.com/Effect-TS/effect/pull/4842) [`672920f`](https://github.com/Effect-TS/effect/commit/672920f85da8abd5f9d4ad85e29248a2aca57ed8) Thanks @tim-smart! - allow overriding http span names + + ```ts + import { FetchHttpClient, HttpClient } from "@effect/platform" + import { NodeRuntime } from "@effect/platform-node" + import { Effect } from "effect" + + Effect.gen(function* () { + const client = (yield* HttpClient.HttpClient).pipe( + // Customize the span names for this HttpClient + HttpClient.withSpanNameGenerator( + (request) => `http.client ${request.method} ${request.url}` + ) + ) + + yield* client.get("https://jsonplaceholder.typicode.com/posts/1") + }).pipe(Effect.provide(FetchHttpClient.layer), NodeRuntime.runMain) + ``` + + And for a server: + + ```ts + import { + HttpMiddleware, + HttpRouter, + HttpServer, + HttpServerResponse + } from "@effect/platform" + import { NodeHttpServer, NodeRuntime } from "@effect/platform-node" + import { Layer } from "effect" + import { createServer } from "http" + + HttpRouter.empty.pipe( + HttpRouter.get("/", HttpServerResponse.empty()), + HttpServer.serve(), + // Customize the span names for this HttpApp + HttpMiddleware.withSpanNameGenerator((request) => `GET ${request.url}`), + Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 })), + Layer.launch, + NodeRuntime.runMain + ) + ``` + +## 0.80.21 + +### Patch Changes + +- Updated dependencies [[`2f3b7d4`](https://github.com/Effect-TS/effect/commit/2f3b7d4e1fa1ef8790b0ca4da22eb88872ee31df)]: + - effect@3.14.21 + +## 0.80.20 + +### Patch Changes + +- Updated dependencies [[`17e2f30`](https://github.com/Effect-TS/effect/commit/17e2f3091408cf0fca9414d4af3bdf7b2765b378)]: + - effect@3.14.20 + +## 0.80.19 + +### Patch Changes + +- [#4821](https://github.com/Effect-TS/effect/pull/4821) [`e25e7bb`](https://github.com/Effect-TS/effect/commit/e25e7bbc1797733916f48f501425d9f2ef310d9f) Thanks @seniorkonung! - Ensure HttpApp defects are always 500 + +- Updated dependencies [[`056a910`](https://github.com/Effect-TS/effect/commit/056a910d0a0b8b00b0dc9df4a070466b2b5c2f6c), [`3273d57`](https://github.com/Effect-TS/effect/commit/3273d572c2b3175a842677f19efeea4cd65ab016)]: + - effect@3.14.19 + +## 0.80.18 + +### Patch Changes + +- Updated dependencies [[`b1164d4`](https://github.com/Effect-TS/effect/commit/b1164d49a1dfdf299e9971367b6fc6be4df0ddff)]: + - effect@3.14.18 + +## 0.80.17 + +### Patch Changes + +- Updated dependencies [[`0b54681`](https://github.com/Effect-TS/effect/commit/0b54681cd89245e211d8f49272be0f1bf2f81813), [`41a59d5`](https://github.com/Effect-TS/effect/commit/41a59d5916a296b12b0d5ead9e859e05f40b4cce)]: + - effect@3.14.17 + +## 0.80.16 + +### Patch Changes + +- [#4803](https://github.com/Effect-TS/effect/pull/4803) [`f1c8583`](https://github.com/Effect-TS/effect/commit/f1c8583f8c3ea9415f813795ca2940a897c9ba9a) Thanks @tim-smart! - expose uninteruptible option to HttpApiBuilder .handle apis + +- Updated dependencies [[`ee14444`](https://github.com/Effect-TS/effect/commit/ee144441021ec77039e43396eaf90714687bb495)]: + - effect@3.14.16 + +## 0.80.15 + +### Patch Changes + +- Updated dependencies [[`239cc99`](https://github.com/Effect-TS/effect/commit/239cc995ce645946210a3c3d2cb52bd3547c0687), [`8b6c947`](https://github.com/Effect-TS/effect/commit/8b6c947eaa8e45a67ecb3c37d45cd27f3e41d165), [`c50a63b`](https://github.com/Effect-TS/effect/commit/c50a63bbecb9f560b9cae349c447eed877d1b9b6)]: + - effect@3.14.15 + +## 0.80.14 + +### Patch Changes + +- Updated dependencies [[`6ed8d15`](https://github.com/Effect-TS/effect/commit/6ed8d1589beb181d30abc79afebdaabc1d101538)]: + - effect@3.14.14 + +## 0.80.13 + +### Patch Changes + +- Updated dependencies [[`ee77788`](https://github.com/Effect-TS/effect/commit/ee77788747e7ebbde6bfa88256cde49dbbad3608), [`5fce6ba`](https://github.com/Effect-TS/effect/commit/5fce6ba19c3cc63cc0104e737e581ad989dedbf0), [`570e45f`](https://github.com/Effect-TS/effect/commit/570e45f8cb936e42ec48f67f21bb2b7252f36c0c)]: + - effect@3.14.13 + +## 0.80.12 + +### Patch Changes + +- Updated dependencies [[`c2ad9ee`](https://github.com/Effect-TS/effect/commit/c2ad9ee9f3c4c743390edf35ed9e85a20be33811), [`9c68654`](https://github.com/Effect-TS/effect/commit/9c686542b6eb3ea188cb70673ef2e41223633e89)]: + - effect@3.14.12 + +## 0.80.11 + +### Patch Changes + +- Updated dependencies [[`e536127`](https://github.com/Effect-TS/effect/commit/e536127c1e6f2fb3a542c73ae919435a629a346b)]: + - effect@3.14.11 + +## 0.80.10 + +### Patch Changes + +- Updated dependencies [[`bc7efa3`](https://github.com/Effect-TS/effect/commit/bc7efa3b031bb25e1ed3c8f2d3fb5e8da166cadc)]: + - effect@3.14.10 + +## 0.80.9 + +### Patch Changes + +- Updated dependencies [[`d78249f`](https://github.com/Effect-TS/effect/commit/d78249f0b67f63cf4baf806ff090cba33293daf0)]: + - effect@3.14.9 + +## 0.80.8 + +### Patch Changes + +- Updated dependencies [[`b3a2d32`](https://github.com/Effect-TS/effect/commit/b3a2d32772e6f7f20eacf2e18128e99324c4d378)]: + - effect@3.14.8 + +## 0.80.7 + +### Patch Changes + +- Updated dependencies [[`b542a4b`](https://github.com/Effect-TS/effect/commit/b542a4bf195be0c9af1523e1ba96c953decc4d25)]: + - effect@3.14.7 + +## 0.80.6 + +### Patch Changes + +- Updated dependencies [[`47618c1`](https://github.com/Effect-TS/effect/commit/47618c1ad84ebcc5a51133a3fff5aa5012d49d45), [`6077882`](https://github.com/Effect-TS/effect/commit/60778824a4794336c33807801f813f8751d1c7e4)]: + - effect@3.14.6 + +## 0.80.5 + +### Patch Changes + +- [#4642](https://github.com/Effect-TS/effect/pull/4642) [`85fba81`](https://github.com/Effect-TS/effect/commit/85fba815ac07eb13d4227a69ac76a18e4b94df18) Thanks @nounder! - Fix options in `HttpServerResponse.raw` + +- Updated dependencies [[`40dbfef`](https://github.com/Effect-TS/effect/commit/40dbfeff239b6e567706752114f31b2fce7de4e3), [`5a5ebdd`](https://github.com/Effect-TS/effect/commit/5a5ebdddfaddd259538b4599a6676281faca778e)]: + - effect@3.14.5 + +## 0.80.4 + +### Patch Changes + +- Updated dependencies [[`e4ba2c6`](https://github.com/Effect-TS/effect/commit/e4ba2c66a878e81b5e295d6d49aaf724b80a28ef)]: + - effect@3.14.4 + +## 0.80.3 + +### Patch Changes + +- Updated dependencies [[`37aa8e1`](https://github.com/Effect-TS/effect/commit/37aa8e137725a902e70cd1e468ea98b873aa5056), [`34f03d6`](https://github.com/Effect-TS/effect/commit/34f03d66875f21f266f102223a03cd14c2ed6ea6)]: + - effect@3.14.3 + +## 0.80.2 + +### Patch Changes + +- Updated dependencies [[`f87991b`](https://github.com/Effect-TS/effect/commit/f87991b6d8a2edfaf90b01cebda4b466992ae865), [`f87991b`](https://github.com/Effect-TS/effect/commit/f87991b6d8a2edfaf90b01cebda4b466992ae865), [`0a3e3e1`](https://github.com/Effect-TS/effect/commit/0a3e3e18eea5e0d1882f1a6c906198e6ef226a41)]: + - effect@3.14.2 + +## 0.80.1 + +### Patch Changes + +- Updated dependencies [[`4a274fe`](https://github.com/Effect-TS/effect/commit/4a274fe9f623182b6b902827e0e83bd89ca3b05c)]: + - effect@3.14.1 + +## 0.80.0 + +### Minor Changes + +- [#4469](https://github.com/Effect-TS/effect/pull/4469) [`3131f8f`](https://github.com/Effect-TS/effect/commit/3131f8fd12ba9eb31b90fa2f42bf88b12309133c) Thanks @tim-smart! - refactor of @effect/cluster packages + +### Patch Changes + +- [#4469](https://github.com/Effect-TS/effect/pull/4469) [`aba2d1d`](https://github.com/Effect-TS/effect/commit/aba2d1d831ea149481bd4dd755528c0afa8239ce) Thanks @tim-smart! - move the MsgPack, Ndjson & ChannelSchema modules to @effect/platform + +- [#4469](https://github.com/Effect-TS/effect/pull/4469) [`aba2d1d`](https://github.com/Effect-TS/effect/commit/aba2d1d831ea149481bd4dd755528c0afa8239ce) Thanks @tim-smart! - add HttpRouter.Tag.serve api + +- [#4469](https://github.com/Effect-TS/effect/pull/4469) [`aba2d1d`](https://github.com/Effect-TS/effect/commit/aba2d1d831ea149481bd4dd755528c0afa8239ce) Thanks @tim-smart! - Move SocketServer modules to @effect/platform + +- Updated dependencies [[`1f47e4e`](https://github.com/Effect-TS/effect/commit/1f47e4e12546ab691b29bfb7b5128bb17b93baa5), [`26dd75f`](https://github.com/Effect-TS/effect/commit/26dd75f276a0d8a63eab313bd5a167d5072c9780), [`04dff2d`](https://github.com/Effect-TS/effect/commit/04dff2d01ac68c260f29a6d4743381825c353c86), [`c7fac0c`](https://github.com/Effect-TS/effect/commit/c7fac0cd7eadcd5cc0c3a987051c5b57ad271638), [`aba2d1d`](https://github.com/Effect-TS/effect/commit/aba2d1d831ea149481bd4dd755528c0afa8239ce), [`ffaa3f3`](https://github.com/Effect-TS/effect/commit/ffaa3f3969df26610fcc02ad537340641d44e803), [`ab957c1`](https://github.com/Effect-TS/effect/commit/ab957c1fee714868f56c7ab4e802b9d449e9b666), [`35db9ce`](https://github.com/Effect-TS/effect/commit/35db9ce228f1416c8abacc6dc9c36fbd0f33ef0f), [`cf77ea9`](https://github.com/Effect-TS/effect/commit/cf77ea9ab4fc89e66a43f682a9926ccdee6c57ed), [`26dd75f`](https://github.com/Effect-TS/effect/commit/26dd75f276a0d8a63eab313bd5a167d5072c9780), [`baaab60`](https://github.com/Effect-TS/effect/commit/baaab60b737f35dfab8e4a21bce28a195d19e899)]: + - effect@3.14.0 + +## 0.79.4 + +### Patch Changes + +- [#4592](https://github.com/Effect-TS/effect/pull/4592) [`5662363`](https://github.com/Effect-TS/effect/commit/566236361e270e575ef1cbf308ad1967c82a362c) Thanks @tim-smart! - support nested records in UrlParams module + +- [#4615](https://github.com/Effect-TS/effect/pull/4615) [`5f1fd15`](https://github.com/Effect-TS/effect/commit/5f1fd15308ab154791580059b89877d19a2055c2) Thanks @KhraksMamtsov! - - Relax `Url.setPort` constraint + - Use `URL | ...` for `baseUrl` in `HttpApiClient.make` + +- [#4614](https://github.com/Effect-TS/effect/pull/4614) [`8bb1460`](https://github.com/Effect-TS/effect/commit/8bb1460c824f66f0f25ebd899c5e74e388089c37) Thanks @gcanti! - HttpApiEndpoint: add missing `head` and `options` constructors, closes #4613. + +## 0.79.3 + +### Patch Changes + +- Updated dependencies [[`0c4803f`](https://github.com/Effect-TS/effect/commit/0c4803fcc69262d11a97ce49d0e9b4288df0651f), [`6f65ac4`](https://github.com/Effect-TS/effect/commit/6f65ac4eac1489cd6ea390e18b0908670722adad)]: + - effect@3.13.12 + +## 0.79.2 + +### Patch Changes + +- Updated dependencies [[`fad8cca`](https://github.com/Effect-TS/effect/commit/fad8cca9bbfcc2eaeb44b97c15dbe0a1eda75315), [`4296293`](https://github.com/Effect-TS/effect/commit/4296293049414d0cf2d915a26c552b09f946b9a0), [`9c241ab`](https://github.com/Effect-TS/effect/commit/9c241abe47ccf7a5257b98a4a64a63054a12741d), [`082b0c1`](https://github.com/Effect-TS/effect/commit/082b0c1b9f4252bcdd69608f2e4a9226f953ac3f), [`be12983`](https://github.com/Effect-TS/effect/commit/be12983bc7e7537b41cd8910fc4eb7d1da56ab07), [`de88127`](https://github.com/Effect-TS/effect/commit/de88127a5a5906ccece98af74787b5ae0e65e431)]: + - effect@3.13.11 + +## 0.79.1 + +### Patch Changes + +- Updated dependencies [[`527c964`](https://github.com/Effect-TS/effect/commit/527c9645229f5be9714a7e60a38a9e753c4bbfb1)]: + - effect@3.13.10 + +## 0.79.0 + +### Minor Changes + +- [#4573](https://github.com/Effect-TS/effect/pull/4573) [`88fe129`](https://github.com/Effect-TS/effect/commit/88fe12923740765c0335a6e6203fdcc6a463edca) Thanks @tim-smart! - remove Scope from HttpClient requirements + + Before: + + ```ts + import { HttpClient } from "@effect/platform" + import { Effect } from "effect" + + Effect.gen(function* () { + const client = yield* HttpClient.HttpClient + const response = yield* client.get("https://api.github.com/users/octocat") + return yield* response.json + }).pipe(Effect.scoped) + ``` + + After: + + ```ts + import { HttpClient } from "@effect/platform" + import { Effect } from "effect" + + Effect.gen(function* () { + const client = yield* HttpClient.HttpClient + const response = yield* client.get("https://api.github.com/users/octocat") + return yield* response.json + }) // no need to add Effect.scoped + ``` + +### Patch Changes + +- [#4583](https://github.com/Effect-TS/effect/pull/4583) [`d630249`](https://github.com/Effect-TS/effect/commit/d630249426113088abe8b382db4f14d80f2160c2) Thanks @tim-smart! - support Layer.launch when using WorkerRunner + +- Updated dependencies [[`2976e52`](https://github.com/Effect-TS/effect/commit/2976e52538d9dc9ffdcbc84d4ac748cff9305971)]: + - effect@3.13.9 + +## 0.78.1 + +### Patch Changes + +- Updated dependencies [[`c65d336`](https://github.com/Effect-TS/effect/commit/c65d3362d07ec815ff3b46278314e8a31706ddc2), [`22d2ebb`](https://github.com/Effect-TS/effect/commit/22d2ebb4b11f5a44351a4736e65da391a3b647d0)]: + - effect@3.13.8 + +## 0.78.0 + +### Minor Changes + +- [#4562](https://github.com/Effect-TS/effect/pull/4562) [`c5bcf53`](https://github.com/Effect-TS/effect/commit/c5bcf53b7cb49dacffdd2a6cd8eb48cc452b417e) Thanks @tim-smart! - expose ParseError in HttpApiClient + +## 0.77.7 + +### Patch Changes + +- [#4540](https://github.com/Effect-TS/effect/pull/4540) [`840cc73`](https://github.com/Effect-TS/effect/commit/840cc7329908db7ca693ef47b07d4f845c29cadd) Thanks @gcanti! - Add `additionalPropertiesStrategy` option to `OpenApi.fromApi`, closes #4531. + + This update introduces the `additionalPropertiesStrategy` option in `OpenApi.fromApi`, allowing control over how additional properties are handled in the generated OpenAPI schema. + - When `"strict"` (default), additional properties are disallowed (`"additionalProperties": false`). + - When `"allow"`, additional properties are allowed (`"additionalProperties": true`), making APIs more flexible. + + The `additionalPropertiesStrategy` option has also been added to: + - `JSONSchema.fromAST` + - `OpenApiJsonSchema.makeWithDefs` + + **Example** + + ```ts + import { + HttpApi, + HttpApiEndpoint, + HttpApiGroup, + OpenApi + } from "@effect/platform" + import { Schema } from "effect" + + const api = HttpApi.make("api").add( + HttpApiGroup.make("group").add( + HttpApiEndpoint.get("get", "/").addSuccess( + Schema.Struct({ a: Schema.String }) + ) + ) + ) + + const schema = OpenApi.fromApi(api, { + additionalPropertiesStrategy: "allow" + }) + + console.log(JSON.stringify(schema, null, 2)) + /* + { + "openapi": "3.1.0", + "info": { + "title": "Api", + "version": "0.0.1" + }, + "paths": { + "/": { + "get": { + "tags": [ + "group" + ], + "operationId": "group.get", + "parameters": [], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "a" + ], + "properties": { + "a": { + "type": "string" + } + }, + "additionalProperties": true + } + } + } + }, + "400": { + "description": "The request did not match the expected schema", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HttpApiDecodeError" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "HttpApiDecodeError": { + "type": "object", + "required": [ + "issues", + "message", + "_tag" + ], + "properties": { + "issues": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Issue" + } + }, + "message": { + "type": "string" + }, + "_tag": { + "type": "string", + "enum": [ + "HttpApiDecodeError" + ] + } + }, + "additionalProperties": true, + "description": "The request did not match the expected schema" + }, + "Issue": { + "type": "object", + "required": [ + "_tag", + "path", + "message" + ], + "properties": { + "_tag": { + "type": "string", + "enum": [ + "Pointer", + "Unexpected", + "Missing", + "Composite", + "Refinement", + "Transformation", + "Type", + "Forbidden" + ], + "description": "The tag identifying the type of parse issue" + }, + "path": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PropertyKey" + }, + "description": "The path to the property where the issue occurred" + }, + "message": { + "type": "string", + "description": "A descriptive message explaining the issue" + } + }, + "additionalProperties": true, + "description": "Represents an error encountered while parsing a value to match the schema" + }, + "PropertyKey": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "object", + "required": [ + "_tag", + "key" + ], + "properties": { + "_tag": { + "type": "string", + "enum": [ + "symbol" + ] + }, + "key": { + "type": "string" + } + }, + "additionalProperties": true, + "description": "an object to be decoded into a globally shared symbol" + } + ] + } + }, + "securitySchemes": {} + }, + "security": [], + "tags": [ + { + "name": "group" + } + ] + } + */ + ``` + +- [#4541](https://github.com/Effect-TS/effect/pull/4541) [`9bf8a74`](https://github.com/Effect-TS/effect/commit/9bf8a74b967f18d931743dd5196af326c9118e9c) Thanks @fubhy! - Disallowed excess properties for various function options + +- [#4559](https://github.com/Effect-TS/effect/pull/4559) [`f910880`](https://github.com/Effect-TS/effect/commit/f91088069057f3b4529753f5bc5532b028d726df) Thanks @tim-smart! - add additional properties options to HttpApiBuilder.middlewareOpenApi + +- [#4493](https://github.com/Effect-TS/effect/pull/4493) [`0d01480`](https://github.com/Effect-TS/effect/commit/0d014803e4f688f74386a80abd65485e1a319244) Thanks @leonitousconforti! - FetchHttpClient merge headers from request and requestInit + +- Updated dependencies [[`840cc73`](https://github.com/Effect-TS/effect/commit/840cc7329908db7ca693ef47b07d4f845c29cadd), [`9bf8a74`](https://github.com/Effect-TS/effect/commit/9bf8a74b967f18d931743dd5196af326c9118e9c), [`87ba23c`](https://github.com/Effect-TS/effect/commit/87ba23c41c193503ed0c612b0d32d0b253794c64)]: + - effect@3.13.7 + +## 0.77.6 + +### Patch Changes + +- Updated dependencies [[`3154ce4`](https://github.com/Effect-TS/effect/commit/3154ce4692fa18b804982158d3c4c8a8a5fae386)]: + - effect@3.13.6 + +## 0.77.5 + +### Patch Changes + +- Updated dependencies [[`367bb35`](https://github.com/Effect-TS/effect/commit/367bb35f4c2a254e1fb211d96db2474a7aed9020), [`6cf11c3`](https://github.com/Effect-TS/effect/commit/6cf11c3a75773ceec2877c85ddc760f381f0866d), [`a0acec8`](https://github.com/Effect-TS/effect/commit/a0acec851f72e19466363d24b9cc218acd00006a)]: + - effect@3.13.5 + +## 0.77.4 + +### Patch Changes + +- [#4525](https://github.com/Effect-TS/effect/pull/4525) [`e0746f9`](https://github.com/Effect-TS/effect/commit/e0746f9aa398b69c6542e375910683bf17f49f46) Thanks @anderssjoberg97! - Fix w3c traceparent header parsing + +- Updated dependencies [[`17d9e89`](https://github.com/Effect-TS/effect/commit/17d9e89f9851663bdbb6c1e685601d97806114a4)]: + - effect@3.13.4 + +## 0.77.3 + +### Patch Changes + +- Updated dependencies [[`cc5588d`](https://github.com/Effect-TS/effect/commit/cc5588df07f9103513547cb429ce041b9436a8bd), [`623c8cd`](https://github.com/Effect-TS/effect/commit/623c8cd053ed6ee3d353aaa8778d484670fca2bb), [`00b4eb1`](https://github.com/Effect-TS/effect/commit/00b4eb1ece12a16e222e6220965bb4024d6752ac), [`f2aee98`](https://github.com/Effect-TS/effect/commit/f2aee989b0a600900ce83e7f460d02908620c80f), [`fb798eb`](https://github.com/Effect-TS/effect/commit/fb798eb9061f1191badc017d1aa649360254da20), [`2251b15`](https://github.com/Effect-TS/effect/commit/2251b1528810bb695b37ce388b653cec0c5bf80c), [`2e15c1e`](https://github.com/Effect-TS/effect/commit/2e15c1e33648add0b29fe274fbcb7294b7515085), [`a4979db`](https://github.com/Effect-TS/effect/commit/a4979db021aef16e731be64df196b72088fc4376), [`b74255a`](https://github.com/Effect-TS/effect/commit/b74255a304ad49d60bedb1a260fd697f370af27a), [`d7f6a5c`](https://github.com/Effect-TS/effect/commit/d7f6a5c7d26c1963dcd864ca62360d20d08c7b49), [`9dd8979`](https://github.com/Effect-TS/effect/commit/9dd8979e940915b1cc1b1f264f3d019c77a65a02), [`477b488`](https://github.com/Effect-TS/effect/commit/477b488284f47c5469d7fba3e4065fb7e3b6556e), [`10932cb`](https://github.com/Effect-TS/effect/commit/10932cbf58fc721ada631cebec42f773ce96d3cc), [`9f6c784`](https://github.com/Effect-TS/effect/commit/9f6c78468b3b5e9ebfc38ffdfb70702901ee977b), [`2c639ec`](https://github.com/Effect-TS/effect/commit/2c639ecee332de4266e36022c989c35ae4e02105), [`886aaa8`](https://github.com/Effect-TS/effect/commit/886aaa81e06dfd3cd9391e8ea987d8cd5ada1124)]: + - effect@3.13.3 + +## 0.77.2 + +### Patch Changes + +- [#4456](https://github.com/Effect-TS/effect/pull/4456) [`3e7ce97`](https://github.com/Effect-TS/effect/commit/3e7ce97f8a41756a039cf635d0b3d9a75d781097) Thanks @tim-smart! - ensure key for header security is lower case + +- [#4472](https://github.com/Effect-TS/effect/pull/4472) [`31be72a`](https://github.com/Effect-TS/effect/commit/31be72ada118cb84a942e67b1663263f8db74a9f) Thanks @gcanti! - Add support for `Schema.Enums` in `HttpApiBuilder.isSingleStringType`, closes #4471. + +- Updated dependencies [[`31be72a`](https://github.com/Effect-TS/effect/commit/31be72ada118cb84a942e67b1663263f8db74a9f)]: + - effect@3.13.2 + +## 0.77.1 + +### Patch Changes + +- Updated dependencies [[`b56a211`](https://github.com/Effect-TS/effect/commit/b56a2110569fd0ec0b57ac137743e926d49f51cc)]: + - effect@3.13.1 + +## 0.77.0 + +### Patch Changes + +- Updated dependencies [[`8baef83`](https://github.com/Effect-TS/effect/commit/8baef83e7ff0b7bc0738b680e1ef013065386cff), [`655bfe2`](https://github.com/Effect-TS/effect/commit/655bfe29e44cc3f0fb9b4e53038f50b891c188df), [`d90cbc2`](https://github.com/Effect-TS/effect/commit/d90cbc274e2742d18671fe65aa4764c057eb6cba), [`75632bd`](https://github.com/Effect-TS/effect/commit/75632bd44b8025101d652ccbaeef898c7086c91c), [`c874a2e`](https://github.com/Effect-TS/effect/commit/c874a2e4b17e9d71904ca8375bb77b020975cb1d), [`bf865e5`](https://github.com/Effect-TS/effect/commit/bf865e5833f77fd8f6c06944ca9d507b54488301), [`f98b2b7`](https://github.com/Effect-TS/effect/commit/f98b2b7592cf20f9d85313e7f1e964cb65878138), [`de8ce92`](https://github.com/Effect-TS/effect/commit/de8ce924923eaa4e1b761a97eb45ec967389f3d5), [`cf8b2dd`](https://github.com/Effect-TS/effect/commit/cf8b2dd112f8e092ed99d78fd728db0f91c29050), [`db426a5`](https://github.com/Effect-TS/effect/commit/db426a5fb41ab84d18e3c8753a7329b4de544245), [`6862444`](https://github.com/Effect-TS/effect/commit/6862444094906ad4f2cb077ff3b9cc0b73880c8c), [`5fc8a90`](https://github.com/Effect-TS/effect/commit/5fc8a90ba46a5fd9f3b643f0b5aeadc69d717339), [`546a492`](https://github.com/Effect-TS/effect/commit/546a492e60eb2b8b048a489a474b934ea0877005), [`65c4796`](https://github.com/Effect-TS/effect/commit/65c47966ce39055f02cf5c808daabb3ea6442b0b), [`9760fdc`](https://github.com/Effect-TS/effect/commit/9760fdc37bdaef9da8b150e46b86ddfbe2ad9221), [`5b471e7`](https://github.com/Effect-TS/effect/commit/5b471e7d4317e8ee5d72bbbd3e0c9775160949ab), [`4f810cc`](https://github.com/Effect-TS/effect/commit/4f810cc2770e9f1f266851d2cb6257112c12af49)]: + - effect@3.13.0 + +## 0.76.1 + +### Patch Changes + +- [#4444](https://github.com/Effect-TS/effect/pull/4444) [`c407726`](https://github.com/Effect-TS/effect/commit/c407726f79df4a567a9631cddd8effaa16b3535d) Thanks @gcanti! - HttpApiBuilder: URL parameters are now automatically converted to arrays when needed, closes #4442. + + **Example** + + ```ts + import { + HttpApi, + HttpApiBuilder, + HttpApiEndpoint, + HttpApiGroup, + HttpMiddleware, + HttpServer + } from "@effect/platform" + import { NodeHttpServer, NodeRuntime } from "@effect/platform-node" + import { Effect, Layer, Schema } from "effect" + import { createServer } from "node:http" + + const api = HttpApi.make("api").add( + HttpApiGroup.make("group").add( + HttpApiEndpoint.get("get", "/") + .addSuccess(Schema.String) + .setUrlParams( + Schema.Struct({ + param: Schema.NonEmptyArray(Schema.String) + }) + ) + ) + ) + + const usersGroupLive = HttpApiBuilder.group(api, "group", (handlers) => + handlers.handle("get", (req) => + Effect.succeed(req.urlParams.param.join(", ")) + ) + ) + + const MyApiLive = HttpApiBuilder.api(api).pipe(Layer.provide(usersGroupLive)) + + const HttpLive = HttpApiBuilder.serve(HttpMiddleware.logger).pipe( + Layer.provide(MyApiLive), + HttpServer.withLogAddress, + Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 })) + ) + + Layer.launch(HttpLive).pipe(NodeRuntime.runMain) + ``` + + Previously, if a query parameter was defined as a `NonEmptyArray` (an array that requires at least one element), providing a single value would cause a parsing error. + + For example, this worked fine: + + ```sh + curl "http://localhost:3000/?param=1¶m=2" + ``` + + But this would fail: + + ```sh + curl "http://localhost:3000/?param=1" + ``` + + Resulting in an error because `"1"` was treated as a string instead of an array. + + With this update, single values are automatically wrapped in an array, so they match the expected schema without requiring manual fixes. + +- Updated dependencies [[`4018eae`](https://github.com/Effect-TS/effect/commit/4018eaed2733241676ddb8c52416f463a8c32e35), [`543d36d`](https://github.com/Effect-TS/effect/commit/543d36d1a11452560b01ab966a82529ad5fee8c9), [`f70a65a`](https://github.com/Effect-TS/effect/commit/f70a65ac80c6635d80b12beaf4d32a9cc59fa143), [`ba409f6`](https://github.com/Effect-TS/effect/commit/ba409f69c41aeaa29e475c0630735726eaf4dbac), [`3d2e356`](https://github.com/Effect-TS/effect/commit/3d2e3565e8a43d1bdb5daee8db3b90f56d71d859)]: + - effect@3.12.12 + +## 0.76.0 + +### Minor Changes + +- [#4429](https://github.com/Effect-TS/effect/pull/4429) [`2473ad5`](https://github.com/Effect-TS/effect/commit/2473ad5cf23582e3a41338091fa526ffe611288d) Thanks @tim-smart! - run platform workers in a Scope, send errors or termination to a CloseLatch + +### Patch Changes + +- Updated dependencies [[`b6a032f`](https://github.com/Effect-TS/effect/commit/b6a032f07bffa020a848c813881879395134fa20), [`42ddd5f`](https://github.com/Effect-TS/effect/commit/42ddd5f144ce9f9d94a036679ebbd626446d37f5), [`2fe447c`](https://github.com/Effect-TS/effect/commit/2fe447c6354d334f9c591b8a8481818f5f0e797e)]: + - effect@3.12.11 + +## 0.75.4 + +### Patch Changes + +- [#4416](https://github.com/Effect-TS/effect/pull/4416) [`7d57ecd`](https://github.com/Effect-TS/effect/commit/7d57ecdaf5da2345ebbf9c22df50317578bde0f5) Thanks @tim-smart! - add HttpServerResponse.mergeCookies + +- Updated dependencies [[`e30f132`](https://github.com/Effect-TS/effect/commit/e30f132c336c9d0760bad39f82a55c7ce5159eb7), [`33fa667`](https://github.com/Effect-TS/effect/commit/33fa667c2623be1026e1ccee91bd44f73b09020a), [`87f5f28`](https://github.com/Effect-TS/effect/commit/87f5f2842e4196cb88d13f10f443ff0567e82832), [`4dbd170`](https://github.com/Effect-TS/effect/commit/4dbd170538e8fb7a36aa7c469c6f93b6c7000091)]: + - effect@3.12.10 + +## 0.75.3 + +### Patch Changes + +- Updated dependencies [[`1b4a4e9`](https://github.com/Effect-TS/effect/commit/1b4a4e904ef5227ec7d9114d4e417eca19eed940)]: + - effect@3.12.9 + +## 0.75.2 + +### Patch Changes + +- [#4334](https://github.com/Effect-TS/effect/pull/4334) [`59b3cfb`](https://github.com/Effect-TS/effect/commit/59b3cfbbd5713dd9475998e95fad5534c0b21466) Thanks @gcanti! - Cookies: `unsafeMakeCookie` and `unsafeSetAll` now throw a more informative error instead of a generic one + +- [#4360](https://github.com/Effect-TS/effect/pull/4360) [`bb05fb8`](https://github.com/Effect-TS/effect/commit/bb05fb83457355b1ca567228a9e041edfb6fd85d) Thanks @IMax153! - Ensure that nested configuration values can be properly loaded from an env file + +- [#4353](https://github.com/Effect-TS/effect/pull/4353) [`8f6006a`](https://github.com/Effect-TS/effect/commit/8f6006a610fb6d6c7b8d14209a7323338a8964ff) Thanks @tim-smart! - fix HttpServerRequest.arrayBuffer for bun & web handlers + +- [#4380](https://github.com/Effect-TS/effect/pull/4380) [`c45b559`](https://github.com/Effect-TS/effect/commit/c45b5592b5fd1189a5c932cfe05bd7d5f6d68508) Thanks @fubhy! - Fixed module imports + +- [#4345](https://github.com/Effect-TS/effect/pull/4345) [`c9175ae`](https://github.com/Effect-TS/effect/commit/c9175aef41cb1e3b689d0ac0a4f53d8107376b58) Thanks @ethanniser! - Addition of `sync` property to `FileSystem.File` representing the `fsync` syscall. + +- Updated dependencies [[`766113c`](https://github.com/Effect-TS/effect/commit/766113c0ea3512cdb887650ead8ba314236e22ee), [`712277f`](https://github.com/Effect-TS/effect/commit/712277f949052a24b46e4aa234063a6abf395c90), [`f269122`](https://github.com/Effect-TS/effect/commit/f269122508693b111142994dd48698ddc75f3d69), [`430c846`](https://github.com/Effect-TS/effect/commit/430c846cbac05b187e3d24ac8dfee0cf22506f7c), [`7b03057`](https://github.com/Effect-TS/effect/commit/7b03057507d2dab5e6793beb9c578dedaaeb15fe), [`a9c94c8`](https://github.com/Effect-TS/effect/commit/a9c94c807755610831211a686d2fad849ab38eb4), [`107e6f0`](https://github.com/Effect-TS/effect/commit/107e6f0557a1e2d3b0dce25d62fa1e2601521752), [`65c11b9`](https://github.com/Effect-TS/effect/commit/65c11b9266ec9447c31c26fe3ed35c73bd3b81fd), [`e386d2f`](https://github.com/Effect-TS/effect/commit/e386d2f1b3ab3ac2c14ee76de11f5963d32a3df4), [`9172efb`](https://github.com/Effect-TS/effect/commit/9172efba98bc6a82353e6ec2af61ac08f038ba64)]: + - effect@3.12.8 + +## 0.75.1 + +### Patch Changes + +- Updated dependencies [[`8dff1d1`](https://github.com/Effect-TS/effect/commit/8dff1d1bff76cdba643cad7f0bf864300f08bc61)]: + - effect@3.12.7 + +## 0.75.0 + +### Minor Changes + +- [#4306](https://github.com/Effect-TS/effect/pull/4306) [`5e43ce5`](https://github.com/Effect-TS/effect/commit/5e43ce50bae116865906112e7f88d390739d778b) Thanks @tim-smart! - eliminate Scope by default in some layer apis + +### Patch Changes + +- [#4304](https://github.com/Effect-TS/effect/pull/4304) [`76eb7d0`](https://github.com/Effect-TS/effect/commit/76eb7d0fbce3c009c8f77e84c178cb15bbed9709) Thanks @tim-smart! - ensure toWebHandler context argument is a Context before using it + + Fixes issues with next.js where they supply a different second argument to request handlers + +- [#4286](https://github.com/Effect-TS/effect/pull/4286) [`eb264ed`](https://github.com/Effect-TS/effect/commit/eb264ed8a6e8c92a9dc7006f766c6ca2e5d29e03) Thanks @thewilkybarkid! - Fix following relative locations + +- Updated dependencies [[`289c13b`](https://github.com/Effect-TS/effect/commit/289c13b38e8e35b214d46d385d05dead176c87cd), [`8b4e75d`](https://github.com/Effect-TS/effect/commit/8b4e75d35daea807c447ca760948a717aa66bb52), [`fc5e0f0`](https://github.com/Effect-TS/effect/commit/fc5e0f0d357a0051cfa01c1ede83ffdd3cb41ab1), [`004fd2b`](https://github.com/Effect-TS/effect/commit/004fd2bbd1459e64fb1b57f02eeb791ca5ea1ea5), [`b2a31be`](https://github.com/Effect-TS/effect/commit/b2a31be85c35d891351ce4f9a2cc93ece0c257f6), [`5514d05`](https://github.com/Effect-TS/effect/commit/5514d05b5cd586ff5868b8bd41c959e95e6c33cd), [`bf5f0ae`](https://github.com/Effect-TS/effect/commit/bf5f0ae9daa0170471678e22585e8ec14ce667bb), [`3b19bcf`](https://github.com/Effect-TS/effect/commit/3b19bcfd3aaadb6c9253428622df524537c8e626), [`b064b3b`](https://github.com/Effect-TS/effect/commit/b064b3b293615fd268cc5a5647d0981eb67750b8), [`289c13b`](https://github.com/Effect-TS/effect/commit/289c13b38e8e35b214d46d385d05dead176c87cd), [`f474678`](https://github.com/Effect-TS/effect/commit/f474678bf10b8f1c80e3dc096ddc7ecf20b2b23e), [`ee187d0`](https://github.com/Effect-TS/effect/commit/ee187d098007a402844c94d04f0cd8f07695377a)]: + - effect@3.12.6 + +## 0.74.0 + +### Minor Changes + +- [#4264](https://github.com/Effect-TS/effect/pull/4264) [`6cb9b76`](https://github.com/Effect-TS/effect/commit/6cb9b766396d0b2ed995cf26957359713efd202e) Thanks @gcanti! - Add support for symbols in the `Issue` definition within `platform/HttpApiError`. + +### Patch Changes + +- [#4277](https://github.com/Effect-TS/effect/pull/4277) [`8653072`](https://github.com/Effect-TS/effect/commit/86530720d7a03e118d2c5a8bf5a997cee7e7f3d6) Thanks @tim-smart! - simplify HttpApi path regex for parameters + + HttpApi path parameters now only support the following syntax: + + `:parameterName` + + Conditional parameters are no longer supported (i.e. using `?` etc after the + parameter name). + +- Updated dependencies [[`a8b0ddb`](https://github.com/Effect-TS/effect/commit/a8b0ddb84710054799fc8f57485b95d00093ada1), [`507d546`](https://github.com/Effect-TS/effect/commit/507d546bd49db31000425fb5da88c434e4291bea), [`a8b0ddb`](https://github.com/Effect-TS/effect/commit/a8b0ddb84710054799fc8f57485b95d00093ada1), [`8db239b`](https://github.com/Effect-TS/effect/commit/8db239b9c869a3707f6566b9d9dbdf53c4df03fc), [`84a0911`](https://github.com/Effect-TS/effect/commit/84a091181634c3a022c94234cec7764a3aeef1be), [`84a0911`](https://github.com/Effect-TS/effect/commit/84a091181634c3a022c94234cec7764a3aeef1be), [`3179a9f`](https://github.com/Effect-TS/effect/commit/3179a9f65d23369a6a9a1f80f7750566dd28df22), [`6cb9b76`](https://github.com/Effect-TS/effect/commit/6cb9b766396d0b2ed995cf26957359713efd202e), [`1fcbe55`](https://github.com/Effect-TS/effect/commit/1fcbe55345042d8468f6a98c84081bd00b6bcf5a), [`d9a63d9`](https://github.com/Effect-TS/effect/commit/d9a63d9d385653865954cac895065360d54cc56b)]: + - effect@3.12.5 + +## 0.73.1 + +### Patch Changes + +- [#4250](https://github.com/Effect-TS/effect/pull/4250) [`c9e5e1b`](https://github.com/Effect-TS/effect/commit/c9e5e1be17c0c84d3d4e2abc3c60215cdb56bbbe) Thanks @thewilkybarkid! - Add isHttpMethod refinement + +- [#4225](https://github.com/Effect-TS/effect/pull/4225) [`7b3d58d`](https://github.com/Effect-TS/effect/commit/7b3d58d7aec2152ec282460871d3e9de45ed254d) Thanks @thewilkybarkid! - Add HttpClient.tapError + +- Updated dependencies [[`5b50ea4`](https://github.com/Effect-TS/effect/commit/5b50ea4a10cf9acd51f9624b2474d9d5ded74019), [`c170a68`](https://github.com/Effect-TS/effect/commit/c170a68b6266100774461fcd6c0e0fabb60112f2), [`a66c2eb`](https://github.com/Effect-TS/effect/commit/a66c2eb473245092cd41f04c2eb2b7b02cf53718)]: + - effect@3.12.4 + +## 0.73.0 + +### Minor Changes + +- [#4245](https://github.com/Effect-TS/effect/pull/4245) [`c110032`](https://github.com/Effect-TS/effect/commit/c110032322450a8824ba38ae24335a538cd2ce9a) Thanks @gcanti! - Update `HttpApi` to remove wildcard support for better OpenAPI compatibility. + + The `HttpApi*` modules previously reused the following type from `HttpRouter`: + + ```ts + type PathInput = `/${string}` | "*" + ``` + + However, the `"*"` wildcard value was not handled correctly, as OpenAPI does not support wildcards. + + This has been updated to use a more specific type: + + ```ts + type PathSegment = `/${string}` + ``` + + This change ensures better alignment with OpenAPI specifications and eliminates potential issues related to unsupported wildcard paths. + +- [#4237](https://github.com/Effect-TS/effect/pull/4237) [`23ac740`](https://github.com/Effect-TS/effect/commit/23ac740c7dd4610b7d265c2071b88b0968419e9a) Thanks @gcanti! - Make `OpenApiSpec` mutable to make handling it more convenient. + +### Patch Changes + +- [#4177](https://github.com/Effect-TS/effect/pull/4177) [`8cd7319`](https://github.com/Effect-TS/effect/commit/8cd7319b6568bfc7a30ca16c104d189e37eac3a0) Thanks @KhraksMamtsov! - `Url` module has been introduced: + - immutable setters with dual-function api + - integration with `UrlParams` + +- Updated dependencies [[`d7dac48`](https://github.com/Effect-TS/effect/commit/d7dac48a477cdfeec509dbe9f33fce6a1b02b63d), [`1d7fd2b`](https://github.com/Effect-TS/effect/commit/1d7fd2b7ee8eeecc912d27adf76ed897db236dc5), [`1d7fd2b`](https://github.com/Effect-TS/effect/commit/1d7fd2b7ee8eeecc912d27adf76ed897db236dc5)]: + - effect@3.12.3 + +## 0.72.2 + +### Patch Changes + +- [#4226](https://github.com/Effect-TS/effect/pull/4226) [`212e784`](https://github.com/Effect-TS/effect/commit/212e78475f527147ec27c090bd13f789f55add7a) Thanks @gcanti! - Ensure the encoding kind of success responses is respected in the OpenAPI spec. + + Before + + When generating an OpenAPI spec for a request with a success schema of type `HttpApiSchema.Text()``, the response content type was incorrectly set to "application/json" instead of "text/plain". + + ```ts + import { + HttpApi, + HttpApiEndpoint, + HttpApiGroup, + HttpApiSchema, + OpenApi + } from "@effect/platform" + + const api = HttpApi.make("api").add( + HttpApiGroup.make("group").add( + HttpApiEndpoint.get("get", "/").addSuccess(HttpApiSchema.Text()) + ) + ) + + const spec = OpenApi.fromApi(api) + + console.log(JSON.stringify(spec.paths, null, 2)) + /* + Output: + { + "/": { + "get": { + "tags": [ + "group" + ], + "operationId": "group.get", + "parameters": [], + "security": [], + "responses": { + "200": { + "description": "a string", + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + } + }, + "400": { + "description": "The request did not match the expected schema", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HttpApiDecodeError" + } + } + } + } + } + } + } + } + */ + ``` + + After + + ```diff + import { + HttpApi, + HttpApiEndpoint, + HttpApiGroup, + HttpApiSchema, + OpenApi + } from "@effect/platform" + + const api = HttpApi.make("api").add( + HttpApiGroup.make("group").add( + HttpApiEndpoint.get("get", "/").addSuccess(HttpApiSchema.Text()) + ) + ) + + const spec = OpenApi.fromApi(api) + + console.log(JSON.stringify(spec.paths, null, 2)) + /* + Output: + { + "/": { + "get": { + "tags": [ + "group" + ], + "operationId": "group.get", + "parameters": [], + "security": [], + "responses": { + "200": { + "description": "a string", + "content": { + - "application/json": { + + "text/plain": { + "schema": { + "type": "string" + } + } + } + }, + "400": { + "description": "The request did not match the expected schema", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HttpApiDecodeError" + } + } + } + } + } + } + } + } + */ + ``` + +- [#4234](https://github.com/Effect-TS/effect/pull/4234) [`f852cb0`](https://github.com/Effect-TS/effect/commit/f852cb02040ea2f165e9b449615b8b1366add5d5) Thanks @gcanti! - Deduplicate errors in `OpenApi.fromApi`. + + When multiple identical errors were added to the same endpoint, group, or API, they were all included in the generated OpenAPI specification, leading to redundant entries in the `anyOf` array for error schemas. + + Identical errors are now deduplicated in the OpenAPI specification. This ensures that each error schema is included only once, simplifying the generated spec and improving readability. + + **Before** + + ```ts + import { + HttpApi, + HttpApiEndpoint, + HttpApiGroup, + OpenApi + } from "@effect/platform" + import { Schema } from "effect" + + const err = Schema.String.annotations({ identifier: "err" }) + const api = HttpApi.make("api") + .add( + HttpApiGroup.make("group1") + .add( + HttpApiEndpoint.get("get1", "/1") + .addSuccess(Schema.String) + .addError(err) + .addError(err) + ) + .addError(err) + .addError(err) + ) + .addError(err) + .addError(err) + + const spec = OpenApi.fromApi(api) + + console.log(JSON.stringify(spec.paths, null, 2)) + /* + Output: + { + "/1": { + "get": { + "tags": [ + "group1" + ], + "operationId": "group1.get1", + "parameters": [], + "security": [], + "responses": { + "200": { + "description": "a string", + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + } + }, + "400": { + "description": "The request did not match the expected schema", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HttpApiDecodeError" + } + } + } + }, + "500": { + "description": "a string", + "content": { + "application/json": { + "schema": "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/err" + }, + { + "$ref": "#/components/schemas/err" + }, + { + "$ref": "#/components/schemas/err" + } + ] + } + } + } + } + } + } + } + } + */ + ``` + + **After** + + ```ts + import { + HttpApi, + HttpApiEndpoint, + HttpApiGroup, + OpenApi + } from "@effect/platform" + import { Schema } from "effect" + + const err = Schema.String.annotations({ identifier: "err" }) + const api = HttpApi.make("api") + .add( + HttpApiGroup.make("group1") + .add( + HttpApiEndpoint.get("get1", "/1") + .addSuccess(Schema.String) + .addError(err) + .addError(err) + ) + .addError(err) + .addError(err) + ) + .addError(err) + .addError(err) + + const spec = OpenApi.fromApi(api) + + console.log(JSON.stringify(spec.paths, null, 2)) + /* + Output: + { + "/1": { + "get": { + "tags": [ + "group1" + ], + "operationId": "group1.get1", + "parameters": [], + "security": [], + "responses": { + "200": { + "description": "a string", + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + } + }, + "400": { + "description": "The request did not match the expected schema", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HttpApiDecodeError" + } + } + } + }, + "500": { + "description": "a string", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/err" + } + } + } + } + } + } + } + } + */ + ``` + +- [#4233](https://github.com/Effect-TS/effect/pull/4233) [`7276ae2`](https://github.com/Effect-TS/effect/commit/7276ae21062896adbb7508ac5b2dece95316322f) Thanks @gcanti! - Ensure the encoding kind of error responses is respected in the OpenAPI spec. + + Before + + When generating an OpenAPI spec for a request with an error schema of type `HttpApiSchema.Text()``, the response content type was incorrectly set to "application/json" instead of "text/plain". + + ```ts + import { + HttpApi, + HttpApiEndpoint, + HttpApiGroup, + HttpApiSchema, + OpenApi + } from "@effect/platform" + + const api = HttpApi.make("api").add( + HttpApiGroup.make("group").add( + HttpApiEndpoint.get("get", "/").addError(HttpApiSchema.Text()) + ) + ) + + const spec = OpenApi.fromApi(api) + + console.log(JSON.stringify(spec.paths, null, 2)) + /* + Output: + { + "/": { + "get": { + "tags": [ + "group" + ], + "operationId": "group.get", + "parameters": [], + "security": [], + "responses": { + "204": { + "description": "Success" + }, + "400": { + "description": "The request did not match the expected schema", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HttpApiDecodeError" + } + } + } + }, + "500": { + "description": "a string", + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + } + } + } + } + } + } + */ + ``` + + After + + ```diff + import { HttpApi, HttpApiEndpoint, HttpApiGroup, HttpApiSchema, OpenApi } from "@effect/platform" + + const api = HttpApi.make("api").add( + HttpApiGroup.make("group").add( + HttpApiEndpoint.get("get", "/").addError(HttpApiSchema.Text()) + ) + ) + + const spec = OpenApi.fromApi(api) + + console.log(JSON.stringify(spec.paths, null, 2)) + /* + Output: + { + "/": { + "get": { + "tags": [ + "group" + ], + "operationId": "group.get", + "parameters": [], + "security": [], + "responses": { + "204": { + "description": "Success" + }, + "400": { + "description": "The request did not match the expected schema", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HttpApiDecodeError" + } + } + } + }, + "500": { + "description": "a string", + "content": { + + "text/plain": { + - "application/json": { + "schema": { + "type": "string" + } + } + } + } + } + } + } + } + */ + ``` + +- [#4226](https://github.com/Effect-TS/effect/pull/4226) [`212e784`](https://github.com/Effect-TS/effect/commit/212e78475f527147ec27c090bd13f789f55add7a) Thanks @gcanti! - Add missing `deprecated` key to `OpenApi.annotations` API. + +- [#4226](https://github.com/Effect-TS/effect/pull/4226) [`212e784`](https://github.com/Effect-TS/effect/commit/212e78475f527147ec27c090bd13f789f55add7a) Thanks @gcanti! - Fix: Prevent request body from being added to the OpenAPI spec for GET methods in `OpenApi.fromApi`. + + When creating a `GET` endpoint with a request payload, the `requestBody` was incorrectly added to the OpenAPI specification, which is invalid for `GET` methods. + + Before + + ```ts + import { + HttpApi, + HttpApiEndpoint, + HttpApiGroup, + OpenApi + } from "@effect/platform" + import { Schema } from "effect" + + const api = HttpApi.make("api").add( + HttpApiGroup.make("group").add( + HttpApiEndpoint.get("get", "/") + .addSuccess(Schema.String) + .setPayload( + Schema.Struct({ + a: Schema.String + }) + ) + ) + ) + + const spec = OpenApi.fromApi(api) + + console.log(JSON.stringify(spec.paths, null, 2)) + /* + Output: + { + "/": { + "get": { + "tags": [ + "group" + ], + "operationId": "group.get", + "parameters": [ + { + "name": "a", + "in": "query", + "schema": { + "type": "string" + }, + "required": true + } + ], + "security": [], + "responses": { + "200": { + "description": "a string", + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + } + }, + "400": { + "description": "The request did not match the expected schema", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HttpApiDecodeError" + } + } + } + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "a" + ], + "properties": { + "a": { + "type": "string" + } + }, + "additionalProperties": false + } + } + }, + "required": true + } + } + } + } + */ + ``` + + After + + ```ts + import { + HttpApi, + HttpApiEndpoint, + HttpApiGroup, + OpenApi + } from "@effect/platform" + import { Schema } from "effect" + + const api = HttpApi.make("api").add( + HttpApiGroup.make("group").add( + HttpApiEndpoint.get("get", "/") + .addSuccess(Schema.String) + .setPayload( + Schema.Struct({ + a: Schema.String + }) + ) + ) + ) + + const spec = OpenApi.fromApi(api) + + console.log(JSON.stringify(spec.paths, null, 2)) + /* + Output: + { + "/": { + "get": { + "tags": [ + "group" + ], + "operationId": "group.get", + "parameters": [ + { + "name": "a", + "in": "query", + "schema": { + "type": "string" + }, + "required": true + } + ], + "security": [], + "responses": { + "200": { + "description": "a string", + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + } + }, + "400": { + "description": "The request did not match the expected schema", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HttpApiDecodeError" + } + } + } + } + } + } + } + } + */ + ``` + +- [#4226](https://github.com/Effect-TS/effect/pull/4226) [`212e784`](https://github.com/Effect-TS/effect/commit/212e78475f527147ec27c090bd13f789f55add7a) Thanks @gcanti! - Add `"application/x-www-form-urlencoded"` to `OpenApiSpecContentType` type as it is generated by the system when using `HttpApiSchema.withEncoding({ kind: "UrlParams" })` + + **Example** + + ```ts + import { + HttpApi, + HttpApiEndpoint, + HttpApiGroup, + HttpApiSchema, + OpenApi + } from "@effect/platform" + import { Schema } from "effect" + + const api = HttpApi.make("api").add( + HttpApiGroup.make("group").add( + HttpApiEndpoint.post("post", "/") + .addSuccess(Schema.String) + .setPayload( + Schema.Struct({ foo: Schema.String }).pipe( + HttpApiSchema.withEncoding({ kind: "UrlParams" }) + ) + ) + ) + ) + + const spec = OpenApi.fromApi(api) + + console.log(JSON.stringify(spec.paths, null, 2)) + /* + Output: + { + "/": { + "post": { + "tags": [ + "group" + ], + "operationId": "group.post", + "parameters": [], + "security": [], + "responses": { + "200": { + "description": "a string", + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + } + }, + "400": { + "description": "The request did not match the expected schema", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HttpApiDecodeError" + } + } + } + } + }, + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "type": "object", + "required": [ + "foo" + ], + "properties": { + "foo": { + "type": "string" + } + }, + "additionalProperties": false + } + } + }, + "required": true + } + } + } + } + */ + ``` + +- Updated dependencies [[`734af82`](https://github.com/Effect-TS/effect/commit/734af82138e78b9c57a8355b1c6b80e80d38b222), [`b63c780`](https://github.com/Effect-TS/effect/commit/b63c78010893101520448ddda7019c487cf7eedd), [`c640d77`](https://github.com/Effect-TS/effect/commit/c640d77b33ad417876f4e8ffe8574ee6cbe5607f), [`0def088`](https://github.com/Effect-TS/effect/commit/0def0887cfdb6755729a64dfd52b3b9f46b0576c)]: + - effect@3.12.2 + +## 0.72.1 + +### Patch Changes + +- Updated dependencies [[`302b57d`](https://github.com/Effect-TS/effect/commit/302b57d2cbf9b9ccc17450945aeebfb33cfe8d43), [`0988083`](https://github.com/Effect-TS/effect/commit/0988083d4594938590df5a287e5b27d38526dd07), [`8b46be6`](https://github.com/Effect-TS/effect/commit/8b46be6a3b8160362ab5ea9171c5e6932505125c), [`bfe8027`](https://github.com/Effect-TS/effect/commit/bfe802734b450a4b4ee069d1125dd37995db2bff), [`16dd657`](https://github.com/Effect-TS/effect/commit/16dd657033d8afac2ffea567b3c8bb27c9b249b6), [`39db211`](https://github.com/Effect-TS/effect/commit/39db211414e90c8db8fdad7dc8ce5b4661bcfaef)]: + - effect@3.12.1 + +## 0.72.0 + +### Minor Changes + +- [#4068](https://github.com/Effect-TS/effect/pull/4068) [`ef64c6f`](https://github.com/Effect-TS/effect/commit/ef64c6fec0d47da573c04230dde9ea729366d871) Thanks @tim-smart! - remove generics from HttpClient tag service + + Instead you can now use `HttpClient.With` to specify the error and + requirement types. + +### Patch Changes + +- Updated dependencies [[`abb22a4`](https://github.com/Effect-TS/effect/commit/abb22a429b9c52c31e84856294f175d2064a9b4d), [`f369a89`](https://github.com/Effect-TS/effect/commit/f369a89e98bc682969803b9304adaf4557bb36c2), [`642376c`](https://github.com/Effect-TS/effect/commit/642376c63fd7d78754db991631a4d50a5dc79aa3), [`3d2b7a7`](https://github.com/Effect-TS/effect/commit/3d2b7a7e942a7157afae5b1cdbc6f3fef116428e), [`73f9c6f`](https://github.com/Effect-TS/effect/commit/73f9c6f2ff091512cf904cc54ab59965b86e87c8), [`17cb451`](https://github.com/Effect-TS/effect/commit/17cb4514590e8a86263f7aed009f24da8a237342), [`d801820`](https://github.com/Effect-TS/effect/commit/d80182060c2ee945d7e0e4728812abf9465a0d6a), [`e1eeb2d`](https://github.com/Effect-TS/effect/commit/e1eeb2d7064b3870041dab142f3057970699bbf1), [`c11f3a6`](https://github.com/Effect-TS/effect/commit/c11f3a60a05c3b5fc8e7ce90136728154dc505b0), [`618f7e0`](https://github.com/Effect-TS/effect/commit/618f7e092a1011e5090dca1e69b5e9285689654b), [`c0ba834`](https://github.com/Effect-TS/effect/commit/c0ba834d1995cf5a8b250e4780fd43f3e3881151), [`e1eeb2d`](https://github.com/Effect-TS/effect/commit/e1eeb2d7064b3870041dab142f3057970699bbf1)]: + - effect@3.12.0 + +## 0.71.7 + +### Patch Changes + +- Updated dependencies [[`39457d4`](https://github.com/Effect-TS/effect/commit/39457d4897d9bc7df8af5c05d352866bbeae82eb), [`a475cc2`](https://github.com/Effect-TS/effect/commit/a475cc25fd7c9f26b27a8e98f8fbe43cc9e6ee3e), [`199214e`](https://github.com/Effect-TS/effect/commit/199214e21c616d8a0ccd7ed5f92e944e6c580193), [`b3c160d`](https://github.com/Effect-TS/effect/commit/b3c160d7a1fdfc2d3fb2440530f1ab80efc65133)]: + - effect@3.11.10 + +## 0.71.6 + +### Patch Changes + +- Updated dependencies [[`1c08a0b`](https://github.com/Effect-TS/effect/commit/1c08a0b8505badcffb4d9cade5a746ea90c9557e), [`1ce703b`](https://github.com/Effect-TS/effect/commit/1ce703b041bbd7560c5c437c9b9be48f027937fd), [`1ce703b`](https://github.com/Effect-TS/effect/commit/1ce703b041bbd7560c5c437c9b9be48f027937fd)]: + - effect@3.11.9 + +## 0.71.5 + +### Patch Changes + +- [#4154](https://github.com/Effect-TS/effect/pull/4154) [`05d71f8`](https://github.com/Effect-TS/effect/commit/05d71f85622305705d8316817694a09762e60865) Thanks @thewilkybarkid! - Support URL objects in HttpServerResponse.redirect + +- [#4157](https://github.com/Effect-TS/effect/pull/4157) [`e66b920`](https://github.com/Effect-TS/effect/commit/e66b9205f25ab425d30640886eb3fb2c4715bc26) Thanks @tim-smart! - ensure WebSocket's are always closed with an explicit code + +## 0.71.4 + +### Patch Changes + +- [#4152](https://github.com/Effect-TS/effect/pull/4152) [`909181a`](https://github.com/Effect-TS/effect/commit/909181a9ce9052a80432ccf52187e0723004bf7f) Thanks @tim-smart! - accept Headers.Input in HttpServerResponse constructors + +- [#4152](https://github.com/Effect-TS/effect/pull/4152) [`909181a`](https://github.com/Effect-TS/effect/commit/909181a9ce9052a80432ccf52187e0723004bf7f) Thanks @tim-smart! - add HttpServerResponse.redirect api + +- Updated dependencies [[`1a6b52d`](https://github.com/Effect-TS/effect/commit/1a6b52dcf020d36e38a7bc90b648152cf5a8ccba)]: + - effect@3.11.8 + +## 0.71.3 + +### Patch Changes + +- [#4147](https://github.com/Effect-TS/effect/pull/4147) [`6984508`](https://github.com/Effect-TS/effect/commit/6984508c87f1bd91213b44c19b25ab5e2dcc1ce0) Thanks @tim-smart! - ensure HttpApi union schemas don't transfer non-api related annotations + +- [#4145](https://github.com/Effect-TS/effect/pull/4145) [`883639c`](https://github.com/Effect-TS/effect/commit/883639cc8ce47757f1cd39439391a8028c0812fe) Thanks @tim-smart! - ensure HttpApi preserves referential equality of error schemas + +## 0.71.2 + +### Patch Changes + +- [#4138](https://github.com/Effect-TS/effect/pull/4138) [`cec0b4d`](https://github.com/Effect-TS/effect/commit/cec0b4d152ef660be2ccdb0927255f2471436e6e) Thanks @gcanti! - JSONSchema: handle the `nullable` keyword for OpenAPI target, closes #4075. + + Before + + ```ts + import { OpenApiJsonSchema } from "@effect/platform" + import { Schema } from "effect" + + const schema = Schema.NullOr(Schema.String) + + console.log(JSON.stringify(OpenApiJsonSchema.make(schema), null, 2)) + /* + { + "anyOf": [ + { + "type": "string" + }, + { + "enum": [ + null + ] + } + ] + } + */ + ``` + + After + + ```ts + import { OpenApiJsonSchema } from "@effect/platform" + import { Schema } from "effect" + + const schema = Schema.NullOr(Schema.String) + + console.log(JSON.stringify(OpenApiJsonSchema.make(schema), null, 2)) + /* + { + "type": "string", + "nullable": true + } + */ + ``` + +- [#4128](https://github.com/Effect-TS/effect/pull/4128) [`8d978c5`](https://github.com/Effect-TS/effect/commit/8d978c53f6fcc98d9d645ecba3e4b55d4297dd36) Thanks @gcanti! - JSONSchema: add `type` for homogeneous enum schemas, closes #4127 + + Before + + ```ts + import { JSONSchema, Schema } from "effect" + + const schema = Schema.Literal("a", "b") + + console.log(JSON.stringify(JSONSchema.make(schema), null, 2)) + /* + { + "$schema": "http://json-schema.org/draft-07/schema#", + "enum": [ + "a", + "b" + ] + } + */ + ``` + + After + + ```ts + import { JSONSchema, Schema } from "effect" + + const schema = Schema.Literal("a", "b") + + console.log(JSON.stringify(JSONSchema.make(schema), null, 2)) + /* + { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "string", + "enum": [ + "a", + "b" + ] + } + */ + ``` + +- [#4138](https://github.com/Effect-TS/effect/pull/4138) [`cec0b4d`](https://github.com/Effect-TS/effect/commit/cec0b4d152ef660be2ccdb0927255f2471436e6e) Thanks @gcanti! - JSONSchema: use `{ "type": "null" }` to represent the `null` literal + + Before + + ```ts + import { JSONSchema, Schema } from "effect" + + const schema = Schema.NullOr(Schema.String) + + console.log(JSON.stringify(JSONSchema.make(schema), null, 2)) + /* + { + "$schema": "http://json-schema.org/draft-07/schema#", + "anyOf": [ + { + "type": "string" + }, + { + "enum": [ + null + ] + } + ] + } + */ + ``` + + After + + ```ts + import { JSONSchema, Schema } from "effect" + + const schema = Schema.NullOr(Schema.String) + + console.log(JSON.stringify(JSONSchema.make(schema), null, 2)) + /* + { + "$schema": "http://json-schema.org/draft-07/schema#", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + */ + ``` + +- Updated dependencies [[`2408616`](https://github.com/Effect-TS/effect/commit/24086163b60b09cc6d0885bd565ef080dcbe866b), [`cec0b4d`](https://github.com/Effect-TS/effect/commit/cec0b4d152ef660be2ccdb0927255f2471436e6e), [`cec0b4d`](https://github.com/Effect-TS/effect/commit/cec0b4d152ef660be2ccdb0927255f2471436e6e), [`8d978c5`](https://github.com/Effect-TS/effect/commit/8d978c53f6fcc98d9d645ecba3e4b55d4297dd36), [`cec0b4d`](https://github.com/Effect-TS/effect/commit/cec0b4d152ef660be2ccdb0927255f2471436e6e), [`cec0b4d`](https://github.com/Effect-TS/effect/commit/cec0b4d152ef660be2ccdb0927255f2471436e6e)]: + - effect@3.11.7 + +## 0.71.1 + +### Patch Changes + +- [#4132](https://github.com/Effect-TS/effect/pull/4132) [`1d3df5b`](https://github.com/Effect-TS/effect/commit/1d3df5bc4324e88a392c348db35fd9d029c7b25e) Thanks @tim-smart! - allow passing Context to HttpApp web handlers + + This allows you to pass request-scoped data to your handlers. + + ```ts + import { Context, Effect } from "effect" + import { HttpApp, HttpServerResponse } from "@effect/platform" + + class Env extends Context.Reference()("Env", { + defaultValue: () => ({ foo: "bar" }) + }) {} + + const handler = HttpApp.toWebHandler( + Effect.gen(function* () { + const env = yield* Env + return yield* HttpServerResponse.json(env) + }) + ) + + const response = await handler( + new Request("http://localhost:3000/"), + Env.context({ foo: "baz" }) + ) + + assert.deepStrictEqual(await response.json(), { + foo: "baz" + }) + ``` + +## 0.71.0 + +### Minor Changes + +- [#4129](https://github.com/Effect-TS/effect/pull/4129) [`c99a0f3`](https://github.com/Effect-TS/effect/commit/c99a0f376d049d3793ed33e146d9873b8a5e5b78) Thanks @tim-smart! - replace HttpApi.empty with HttpApi.make(identifier) + + This ensures if you have multiple HttpApi instances, the HttpApiGroup's are + implemented correctly. + + ```ts + import { HttpApi } from "@effect/platform" + + // Before + class Api extends HttpApi.empty.add(...) {} + + // After + class Api extends HttpApi.make("api").add(...) {} + ``` + +### Patch Changes + +- [#4130](https://github.com/Effect-TS/effect/pull/4130) [`11fc401`](https://github.com/Effect-TS/effect/commit/11fc401f436f99bf4be95f56d50b0e4bdfe5edea) Thanks @tim-smart! - add predefined empty errors to HttpApiError + +- [#4129](https://github.com/Effect-TS/effect/pull/4129) [`c99a0f3`](https://github.com/Effect-TS/effect/commit/c99a0f376d049d3793ed33e146d9873b8a5e5b78) Thanks @tim-smart! - add OpenApi annotation for exluding parts of the api from the spec + +- Updated dependencies [[`662d1ce`](https://github.com/Effect-TS/effect/commit/662d1ce6fb7da384a95888d5b2bb5605bdf3208d), [`31c62d8`](https://github.com/Effect-TS/effect/commit/31c62d83cbdcf9850a8b5331faa239601c60f78a)]: + - effect@3.11.6 + +## 0.70.7 + +### Patch Changes + +- [#4111](https://github.com/Effect-TS/effect/pull/4111) [`22905cf`](https://github.com/Effect-TS/effect/commit/22905cf5addfb1ff3d2a6135c52036be958ae911) Thanks @gcanti! - JSONSchema: merge refinement fragments instead of just overwriting them. + + Before + + ```ts + import { JSONSchema, Schema } from "effect" + + export const schema = Schema.String.pipe( + Schema.startsWith("a"), // <= overwritten! + Schema.endsWith("c") + ) + + console.log(JSON.stringify(JSONSchema.make(schema), null, 2)) + /* + { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "string", + "description": "a string ending with \"c\"", + "pattern": "^.*c$" // <= overwritten! + } + */ + ``` + + After + + ```ts + import { JSONSchema, Schema } from "effect" + + export const schema = Schema.String.pipe( + Schema.startsWith("a"), // <= preserved! + Schema.endsWith("c") + ) + + console.log(JSON.stringify(JSONSchema.make(schema), null, 2)) + /* + { + "type": "string", + "description": "a string ending with \"c\"", + "pattern": "^.*c$", + "allOf": [ + { + "pattern": "^a" // <= preserved! + } + ], + "$schema": "http://json-schema.org/draft-07/schema#" + } + */ + ``` + +- [#4019](https://github.com/Effect-TS/effect/pull/4019) [`9f5a6f7`](https://github.com/Effect-TS/effect/commit/9f5a6f701bf7ba31adccd1f1bcfa8ab5614c9be8) Thanks @gcanti! - OpenApiJsonSchema: Use the experimental `JSONSchema.fromAST` API for implementation. + +- Updated dependencies [[`9f5a6f7`](https://github.com/Effect-TS/effect/commit/9f5a6f701bf7ba31adccd1f1bcfa8ab5614c9be8), [`22905cf`](https://github.com/Effect-TS/effect/commit/22905cf5addfb1ff3d2a6135c52036be958ae911), [`9f5a6f7`](https://github.com/Effect-TS/effect/commit/9f5a6f701bf7ba31adccd1f1bcfa8ab5614c9be8), [`1e59e4f`](https://github.com/Effect-TS/effect/commit/1e59e4fd778da18296812a2a32f36ca8ae50f60d), [`8d914e5`](https://github.com/Effect-TS/effect/commit/8d914e504e7a22d0ea628e8af265ee450ff9530f), [`03bb00f`](https://github.com/Effect-TS/effect/commit/03bb00faa74f9e168a54a8cc0828a664fbb1ab05), [`9f5a6f7`](https://github.com/Effect-TS/effect/commit/9f5a6f701bf7ba31adccd1f1bcfa8ab5614c9be8), [`14e1149`](https://github.com/Effect-TS/effect/commit/14e1149f1af5a022f06eb8c2e4ba9fec17fe7426), [`9f5a6f7`](https://github.com/Effect-TS/effect/commit/9f5a6f701bf7ba31adccd1f1bcfa8ab5614c9be8), [`9f5a6f7`](https://github.com/Effect-TS/effect/commit/9f5a6f701bf7ba31adccd1f1bcfa8ab5614c9be8)]: + - effect@3.11.5 + +## 0.70.6 + +### Patch Changes + +- [#4097](https://github.com/Effect-TS/effect/pull/4097) [`9a5b8e3`](https://github.com/Effect-TS/effect/commit/9a5b8e36d184bd4967a88752cb6e755e1be263af) Thanks @tim-smart! - handle WebSocket's that emit ArrayBuffer instead of Uint8Array + +## 0.70.5 + +### Patch Changes + +- [#4091](https://github.com/Effect-TS/effect/pull/4091) [`415f4c9`](https://github.com/Effect-TS/effect/commit/415f4c98321868531727a83cbaad70164f5e4c40) Thanks @ryskajakub! - http api param inherits description from schema + +- [#4087](https://github.com/Effect-TS/effect/pull/4087) [`518b258`](https://github.com/Effect-TS/effect/commit/518b258a8a67ecd332a9252c35cc060f8368dee2) Thanks @tim-smart! - remove Socket write indirection + +- Updated dependencies [[`518b258`](https://github.com/Effect-TS/effect/commit/518b258a8a67ecd332a9252c35cc060f8368dee2), [`6e323a3`](https://github.com/Effect-TS/effect/commit/6e323a36faaee46b328c8e3cf60a76b3aff9907f), [`6e323a3`](https://github.com/Effect-TS/effect/commit/6e323a36faaee46b328c8e3cf60a76b3aff9907f)]: + - effect@3.11.4 + +## 0.70.4 + +### Patch Changes + +- Updated dependencies [[`90906f7`](https://github.com/Effect-TS/effect/commit/90906f7f154b12c7182e8f39e3c55ef3937db857), [`3862cd3`](https://github.com/Effect-TS/effect/commit/3862cd3c7f6a542ed65fb81255b3bd696ce2f567), [`3862cd3`](https://github.com/Effect-TS/effect/commit/3862cd3c7f6a542ed65fb81255b3bd696ce2f567), [`343b6aa`](https://github.com/Effect-TS/effect/commit/343b6aa6ac4a74276bfc7c63ccbf4a1d72bc1bed), [`afba339`](https://github.com/Effect-TS/effect/commit/afba339adc11dad56b5a3b7ca94487e58f34d613)]: + - effect@3.11.3 + +## 0.70.3 + +### Patch Changes + +- [#4065](https://github.com/Effect-TS/effect/pull/4065) [`7044730`](https://github.com/Effect-TS/effect/commit/70447306be1aeeb7d87c230b2a96ec87b993ede9) Thanks @KhraksMamtsov! - Ensure the uniqueness of the parameters at the type level + + ```ts + import { HttpApiEndpoint, HttpApiSchema } from "@effect/platform" + import { Schema } from "effect" + + HttpApiEndpoint.get( + "test" + )`/${HttpApiSchema.param("id", Schema.NumberFromString)}/${ + // @ts-expect-error: Argument of type 'Param<"id", typeof NumberFromString>' is not assignable to parameter of type '"Duplicate param :id"' + HttpApiSchema.param("id", Schema.NumberFromString) + }` + ``` + +## 0.70.2 + +### Patch Changes + +- [#4064](https://github.com/Effect-TS/effect/pull/4064) [`c2249ea`](https://github.com/Effect-TS/effect/commit/c2249ea13fd98ab7d9aa628787931356d8ec2860) Thanks @tim-smart! - HttpApi OpenApi adjustments + - Allow using transform annotation on endpoints & groups + - Preserve descriptions for "empty" schemas + +- [#4055](https://github.com/Effect-TS/effect/pull/4055) [`1358aa5`](https://github.com/Effect-TS/effect/commit/1358aa5326eaa85ef13ee8d1fed0b4a4288ed3eb) Thanks @thewilkybarkid! - Allow creating a route for all methods + +- [#4062](https://github.com/Effect-TS/effect/pull/4062) [`1de3fe7`](https://github.com/Effect-TS/effect/commit/1de3fe7d1cbafd6391eaa38c2300b99e332cc2aa) Thanks @tim-smart! - simplify HttpApiClient param regex + +- Updated dependencies [[`01cee56`](https://github.com/Effect-TS/effect/commit/01cee560b58d94b24cc20e98083251b73e658b41)]: + - effect@3.11.2 + +## 0.70.1 + +### Patch Changes + +- Updated dependencies [[`dd8a2d8`](https://github.com/Effect-TS/effect/commit/dd8a2d8e80d33b16719fc69361eaedf0b59d4620), [`a71bfef`](https://github.com/Effect-TS/effect/commit/a71bfef46f5061bb2502a61a333638a987b62273)]: + - effect@3.11.1 + +## 0.70.0 + +### Minor Changes + +- [#3835](https://github.com/Effect-TS/effect/pull/3835) [`672bde5`](https://github.com/Effect-TS/effect/commit/672bde5bec51c7d6f9862828e6a654cb2cb6f93d) Thanks @tim-smart! - support array of values in /platform url param schemas + +### Patch Changes + +- [#3835](https://github.com/Effect-TS/effect/pull/3835) [`5eff3f6`](https://github.com/Effect-TS/effect/commit/5eff3f6fa3aae7e86948a62cbfd63b8d6c3bdf92) Thanks @tim-smart! - fix multipart support for bun http server + +- [#3835](https://github.com/Effect-TS/effect/pull/3835) [`b4ce4ea`](https://github.com/Effect-TS/effect/commit/b4ce4ea7fd514a7e572f2dcd879c98f334981b0e) Thanks @SandroMaglione! - New methods `extractAll` and `extractSchema` to `UrlParams` (added `Schema.BooleanFromString`). + +- [#3835](https://github.com/Effect-TS/effect/pull/3835) [`1e2747c`](https://github.com/Effect-TS/effect/commit/1e2747c63a4820d1459cbbc88c71212983bd68bd) Thanks @KhraksMamtsov! - - JSONSchema module + - add `format?: string` optional field to `JsonSchema7String` interface + - Schema module + - add custom json schema annotation to `UUID` schema including `format: "uuid"` + - OpenApiJsonSchema module + - add `format?: string` optional field to `String` and ` Numeric` interfaces +- Updated dependencies [[`147434b`](https://github.com/Effect-TS/effect/commit/147434b03d5e1fd692dd9f126e5ab0910f3b76d3), [`6e69493`](https://github.com/Effect-TS/effect/commit/6e694930048bbaf98110f35f41566aeb9752d471), [`147434b`](https://github.com/Effect-TS/effect/commit/147434b03d5e1fd692dd9f126e5ab0910f3b76d3), [`5eff3f6`](https://github.com/Effect-TS/effect/commit/5eff3f6fa3aae7e86948a62cbfd63b8d6c3bdf92), [`d9fe79b`](https://github.com/Effect-TS/effect/commit/d9fe79bb5a3fe105d8e7a3bc2922a8ad936a5d10), [`251d189`](https://github.com/Effect-TS/effect/commit/251d189420bbba71990574e91098c499065f9a9b), [`5a259f3`](https://github.com/Effect-TS/effect/commit/5a259f3711b4369f55d885b568bdb21136155261), [`b4ce4ea`](https://github.com/Effect-TS/effect/commit/b4ce4ea7fd514a7e572f2dcd879c98f334981b0e), [`15fcc5a`](https://github.com/Effect-TS/effect/commit/15fcc5a0ea4bbf40ab48fa6a04fdda74f76f4c07), [`9bc9a47`](https://github.com/Effect-TS/effect/commit/9bc9a476800dc645903c888a68bb1d3baa3383c6), [`aadb8a4`](https://github.com/Effect-TS/effect/commit/aadb8a48d2cba197c06ec9996505510e48e4e5cb), [`1e2747c`](https://github.com/Effect-TS/effect/commit/1e2747c63a4820d1459cbbc88c71212983bd68bd), [`9264162`](https://github.com/Effect-TS/effect/commit/9264162a82783a651776fb7b87604564a63e7070), [`e0b9b09`](https://github.com/Effect-TS/effect/commit/e0b9b09e70c386b2da17d1f0a15b0511861c89e8), [`c36f3b9`](https://github.com/Effect-TS/effect/commit/c36f3b95df5ce9d71b66f22f26ce12eda8d3e848), [`aadb8a4`](https://github.com/Effect-TS/effect/commit/aadb8a48d2cba197c06ec9996505510e48e4e5cb)]: + - effect@3.11.0 + +## 0.69.32 + +### Patch Changes + +- Updated dependencies [[`3069614`](https://github.com/Effect-TS/effect/commit/30696149271129fc618f6f2ccd1d8f2f6c0f9cd7), [`09a5e52`](https://github.com/Effect-TS/effect/commit/09a5e522fd9b221f05d85b1d1c8a740d4973c302)]: + - effect@3.10.20 + +## 0.69.31 + +### Patch Changes + +- [#4035](https://github.com/Effect-TS/effect/pull/4035) [`e6d4a37`](https://github.com/Effect-TS/effect/commit/e6d4a37c1d7e657b5ea44063a1cf586808228fe5) Thanks @tim-smart! - add template literal api for defining HttpApiEndpoint path schema + +## 0.69.30 + +### Patch Changes + +- [#4025](https://github.com/Effect-TS/effect/pull/4025) [`270f199`](https://github.com/Effect-TS/effect/commit/270f199b31810fd643e4c22818698adcbdb5d396) Thanks @tim-smart! - update OpenApi version to 3.1.0 + +## 0.69.29 + +### Patch Changes + +- [#4024](https://github.com/Effect-TS/effect/pull/4024) [`24cc35e`](https://github.com/Effect-TS/effect/commit/24cc35e26d6ed4a076470bc687ffd99cc50991b3) Thanks @tim-smart! - improve HttpApi handling of payload encoding types + +## 0.69.28 + +### Patch Changes + +- [#4017](https://github.com/Effect-TS/effect/pull/4017) [`edd72be`](https://github.com/Effect-TS/effect/commit/edd72be57b904d60c9cbffc2537901821a9da537) Thanks @tim-smart! - try encode defects that match an Error schema in HttpApi + +- [#4014](https://github.com/Effect-TS/effect/pull/4014) [`a3e2771`](https://github.com/Effect-TS/effect/commit/a3e277170a1f7cf61fd629acb60304c7e81d9498) Thanks @tim-smart! - consider TimeoutException a transient error in HttpClient.retryTransient + +- [#4007](https://github.com/Effect-TS/effect/pull/4007) [`944025b`](https://github.com/Effect-TS/effect/commit/944025bc5ce139f4a85846aa689bf30ec06a8ec1) Thanks @gcanti! - Wrap JSDoc @example tags with a TypeScript fence, closes #4002 + +- [#4016](https://github.com/Effect-TS/effect/pull/4016) [`a9e00e4`](https://github.com/Effect-TS/effect/commit/a9e00e43f0b5dd22c1f9d5b78be6383daea09c20) Thanks @tim-smart! - allow using HttpApiSchema.Multipart in a union + +- Updated dependencies [[`944025b`](https://github.com/Effect-TS/effect/commit/944025bc5ce139f4a85846aa689bf30ec06a8ec1), [`54addee`](https://github.com/Effect-TS/effect/commit/54addee438a644bf010646c52042c7b89c5fc0a7)]: + - effect@3.10.19 + +## 0.69.27 + +### Patch Changes + +- [#4005](https://github.com/Effect-TS/effect/pull/4005) [`beaccae`](https://github.com/Effect-TS/effect/commit/beaccae2d15931e9fe475fb50a0b3638243fe3f7) Thanks @tim-smart! - fix HttpApiBuilder.middleware when used multiple times + +- Updated dependencies [[`af409cf`](https://github.com/Effect-TS/effect/commit/af409cf1d2ff973be11cc079ea373eaeedca25de)]: + - effect@3.10.18 + +## 0.69.26 + +### Patch Changes + +- [#3977](https://github.com/Effect-TS/effect/pull/3977) [`c963886`](https://github.com/Effect-TS/effect/commit/c963886d5817986fcbd6bfa4ddf50aca8b6c8184) Thanks @KhraksMamtsov! - `HttpApiClient.group` & `HttpApiClient.endpoint` have been added + This makes it possible to create `HttpApiClient` for some part of the `HttpApi` + This eliminates the need to provide all the dependencies for the entire `HttpApi` - but only those necessary for its specific part to work +- Updated dependencies [[`42c4ce6`](https://github.com/Effect-TS/effect/commit/42c4ce6f8d8c7d847e97757650a8ad9419a829d7)]: + - effect@3.10.17 + +## 0.69.25 + +### Patch Changes + +- [#3968](https://github.com/Effect-TS/effect/pull/3968) [`320557a`](https://github.com/Effect-TS/effect/commit/320557ab18d13c5e22fc7dc0d2a157eae461012f) Thanks @KhraksMamtsov! - `OpenApi.Transform` annotation has been added + + This customization point allows you to transform the generated specification in an arbitrary way + + ```ts + class Api extends HttpApi.empty + .annotateContext(OpenApi.annotations({ + title: "API", + summary: "test api summary", + transform: (openApiSpec) => ({ + ...openApiSpec, + tags: [...openApiSpec.tags ?? [], { + name: "Tag from OpenApi.Transform annotation" + }] + }) + })) + ``` + +- [#3962](https://github.com/Effect-TS/effect/pull/3962) [`7b93dd6`](https://github.com/Effect-TS/effect/commit/7b93dd622e2ab79c7072d79d0d9611e446202201) Thanks @KhraksMamtsov! - fix HttpApiGroup.addError signature + +- Updated dependencies [[`4dca30c`](https://github.com/Effect-TS/effect/commit/4dca30cfcdafe4542e236489f71d6f171a5b4e38), [`1d99867`](https://github.com/Effect-TS/effect/commit/1d998671be3cd11043f232822e91dd8c98fccfa9), [`6dae414`](https://github.com/Effect-TS/effect/commit/6dae4147991a97ec14a99289bd25fadae7541e8d), [`6b0d737`](https://github.com/Effect-TS/effect/commit/6b0d737078bf63b97891e6bc47affc04b28f9cf7), [`d8356aa`](https://github.com/Effect-TS/effect/commit/d8356aad428a0c2290db52380220f81d9ec94232)]: + - effect@3.10.16 + +## 0.69.24 + +### Patch Changes + +- [#3939](https://github.com/Effect-TS/effect/pull/3939) [`3cc6514`](https://github.com/Effect-TS/effect/commit/3cc6514d2dd64e010cb760cc29bfce98c349bb10) Thanks @KhraksMamtsov! - Added the ability to annotate the `HttpApi` with additional schemas + Which will be taken into account when generating `components.schemas` section of `OpenApi` schema + + ```ts + import { Schema } from "effect" + import { HttpApi } from "@effect/platform" + + HttpApi.empty.annotate(HttpApi.AdditionalSchemas, [ + Schema.Struct({ + contentType: Schema.String, + length: Schema.Int + }).annotations({ + identifier: "ComponentsSchema" + }) + ]) + /** + { + "openapi": "3.0.3", + ... + "components": { + "schemas": { + "ComponentsSchema": {...}, + ... + }, + ... + } + */ + ``` + +## 0.69.23 + +### Patch Changes + +- [#3944](https://github.com/Effect-TS/effect/pull/3944) [`3aff4d3`](https://github.com/Effect-TS/effect/commit/3aff4d38837c213bb2987973dc4b98febb9f92d2) Thanks @KhraksMamtsov! - `OpenApi.Summary` & `OpenApi.Deprecated` annotations have been added + +## 0.69.22 + +### Patch Changes + +- Updated dependencies [[`8398b32`](https://github.com/Effect-TS/effect/commit/8398b3208242a88239d4449910b7baf923cfe3b6), [`72e55b7`](https://github.com/Effect-TS/effect/commit/72e55b7c610784fcebdbadc592c876e23e76a986)]: + - effect@3.10.15 + +## 0.69.21 + +### Patch Changes + +- Updated dependencies [[`f983946`](https://github.com/Effect-TS/effect/commit/f9839467b4cad6e788297764ef9f9f0b9fd203f9), [`2d8a750`](https://github.com/Effect-TS/effect/commit/2d8a75081eb83a0a81f817fdf6f428369c5064ab)]: + - effect@3.10.14 + +## 0.69.20 + +### Patch Changes + +- Updated dependencies [[`995bbdf`](https://github.com/Effect-TS/effect/commit/995bbdffea2e332f203cd5b474cd6a1c77dfa6ae)]: + - effect@3.10.13 + +## 0.69.19 + +### Patch Changes + +- [#3908](https://github.com/Effect-TS/effect/pull/3908) [`eb8c52d`](https://github.com/Effect-TS/effect/commit/eb8c52d8b4c5e067ebf0a81eb742f5822e6439b5) Thanks @tim-smart! - use plain js data structures for HttpApi properties + +## 0.69.18 + +### Patch Changes + +- [#3906](https://github.com/Effect-TS/effect/pull/3906) [`a0584ec`](https://github.com/Effect-TS/effect/commit/a0584ece92ed784bfb139e9c5a699f02d1e71c2d) Thanks @tim-smart! - ensure Socket send queue is not ended + +- [#3904](https://github.com/Effect-TS/effect/pull/3904) [`dd14efe`](https://github.com/Effect-TS/effect/commit/dd14efe0ace255f571273aae876adea96267d7e6) Thanks @tim-smart! - improve platform/Worker shutdown and logging + +- Updated dependencies [[`dd14efe`](https://github.com/Effect-TS/effect/commit/dd14efe0ace255f571273aae876adea96267d7e6)]: + - effect@3.10.12 + +## 0.69.17 + +### Patch Changes + +- [#3898](https://github.com/Effect-TS/effect/pull/3898) [`8240b1c`](https://github.com/Effect-TS/effect/commit/8240b1c10d45312fc863cb679b1a1e8441af0c1a) Thanks @mattphillips! - Fixed handling of basic auth header values + +- [#3903](https://github.com/Effect-TS/effect/pull/3903) [`5eef499`](https://github.com/Effect-TS/effect/commit/5eef4998b6ccb7a5404d9e4fef85e57fa35fbb8a) Thanks @tim-smart! - add HttpApi.addHttpApi method, for merging two HttpApi instances + +- Updated dependencies [[`5eef499`](https://github.com/Effect-TS/effect/commit/5eef4998b6ccb7a5404d9e4fef85e57fa35fbb8a)]: + - effect@3.10.11 + +## 0.69.16 + +### Patch Changes + +- [#3893](https://github.com/Effect-TS/effect/pull/3893) [`cd720ae`](https://github.com/Effect-TS/effect/commit/cd720aedf7f2571edec0843d6a633e84e4832b28) Thanks @tim-smart! - refactor Socket internal code + +- [#3892](https://github.com/Effect-TS/effect/pull/3892) [`7d89650`](https://github.com/Effect-TS/effect/commit/7d8965036cd2ea435c8441ffec3345488baebf85) Thanks @tim-smart! - simplify HttpApiClient implementation + +- Updated dependencies [[`cd720ae`](https://github.com/Effect-TS/effect/commit/cd720aedf7f2571edec0843d6a633e84e4832b28), [`cd720ae`](https://github.com/Effect-TS/effect/commit/cd720aedf7f2571edec0843d6a633e84e4832b28), [`b631f40`](https://github.com/Effect-TS/effect/commit/b631f40abbe649b2a089764585b5c39f6a695ac6)]: + - effect@3.10.10 + +## 0.69.15 + +### Patch Changes + +- [#3885](https://github.com/Effect-TS/effect/pull/3885) [`8a30e1d`](https://github.com/Effect-TS/effect/commit/8a30e1dfa3a7103bf5414fc6a7fca3088d8c8c00) Thanks @tim-smart! - simplify HttpApiBuilder handler logic + +## 0.69.14 + +### Patch Changes + +- [#3884](https://github.com/Effect-TS/effect/pull/3884) [`07c493a`](https://github.com/Effect-TS/effect/commit/07c493a598e096c7810cd06def8cfa43493c46b1) Thanks @tim-smart! - add withResponse option to HttpApiClient methods + +- [#3882](https://github.com/Effect-TS/effect/pull/3882) [`257ab1b`](https://github.com/Effect-TS/effect/commit/257ab1b539fa6e930b7ae2583a188376372200d7) Thanks @tim-smart! - simplify Socket internal code + +- Updated dependencies [[`a123e80`](https://github.com/Effect-TS/effect/commit/a123e80f111a625428a5b5622b7f55ee1073566b), [`bd5fcd3`](https://github.com/Effect-TS/effect/commit/bd5fcd3e6b603b1e505af90d6a00627c8eca6d41), [`0289d3b`](https://github.com/Effect-TS/effect/commit/0289d3b6391031d00329365bab9791b355031fe3), [`7386b71`](https://github.com/Effect-TS/effect/commit/7386b710e5be570e17f468928a6ed19d549a3e12), [`4211a23`](https://github.com/Effect-TS/effect/commit/4211a2355bb3af3f0e756e2aae9d293379f25662)]: + - effect@3.10.9 + +## 0.69.13 + +### Patch Changes + +- Updated dependencies [[`68b5c9e`](https://github.com/Effect-TS/effect/commit/68b5c9e44f34192cef26e1cadda5e661a027df41), [`9c9928d`](https://github.com/Effect-TS/effect/commit/9c9928dfeacd9ac33dc37eb0ca3d7d8c39175ada), [`6306e66`](https://github.com/Effect-TS/effect/commit/6306e6656092b350d4ede5746da6f245ec9f7e07), [`361c7f3`](https://github.com/Effect-TS/effect/commit/361c7f39a2c10ede9324847c3d3ba192a6f9b20a)]: + - effect@3.10.8 + +## 0.69.12 + +### Patch Changes + +- Updated dependencies [[`33f5b9f`](https://github.com/Effect-TS/effect/commit/33f5b9ffaebea4f1bd0e391b44c41fb6230e743a), [`50f0281`](https://github.com/Effect-TS/effect/commit/50f0281b0d2116726b8927a6217622d5f394f3e4)]: + - effect@3.10.7 + +## 0.69.11 + +### Patch Changes + +- [#3856](https://github.com/Effect-TS/effect/pull/3856) [`81ddd45`](https://github.com/Effect-TS/effect/commit/81ddd45fc074b98206fafab416d9a5a28b31e07a) Thanks @KhraksMamtsov! - Integration with [Scalar](https://scalar.com/) has been implemented + +- Updated dependencies [[`ce1c21f`](https://github.com/Effect-TS/effect/commit/ce1c21ffc11902ac9ab453a51904207859d38552)]: + - effect@3.10.6 + +## 0.69.10 + +### Patch Changes + +- Updated dependencies [[`3a6d757`](https://github.com/Effect-TS/effect/commit/3a6d757badeebe00d8ef4d67530d073c8264dcfa), [`59d813a`](https://github.com/Effect-TS/effect/commit/59d813aa4973d1115cfc70cc3667508335f49693)]: + - effect@3.10.5 + +## 0.69.9 + +### Patch Changes + +- [#3842](https://github.com/Effect-TS/effect/pull/3842) [`2367708`](https://github.com/Effect-TS/effect/commit/2367708be449f9526a2047e321302d7bfb16f18e) Thanks @gcanti! - add support for `Schema.OptionFromUndefinedOr` in JSON Schema generation, closes #3839 + + Before + + ```ts + import { JSONSchema, Schema } from "effect" + + const schema = Schema.Struct({ + a: Schema.OptionFromUndefinedOr(Schema.Number) + }) + + console.log(JSON.stringify(JSONSchema.make(schema), null, 2)) + /* + throws: + Error: Missing annotation + at path: ["a"] + details: Generating a JSON Schema for this schema requires a "jsonSchema" annotation + schema (UndefinedKeyword): undefined + */ + ``` + + After + + ```ts + import { JSONSchema, Schema } from "effect" + + const schema = Schema.Struct({ + a: Schema.OptionFromUndefinedOr(Schema.Number) + }) + + console.log(JSON.stringify(JSONSchema.make(schema), null, 2)) + /* + Output: + { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "required": [], + "properties": { + "a": { + "type": "number" + } + }, + "additionalProperties": false + } + */ + ``` + +- Updated dependencies [[`2367708`](https://github.com/Effect-TS/effect/commit/2367708be449f9526a2047e321302d7bfb16f18e)]: + - effect@3.10.4 + +## 0.69.8 + +### Patch Changes + +- [#3837](https://github.com/Effect-TS/effect/pull/3837) [`522f7c5`](https://github.com/Effect-TS/effect/commit/522f7c518a5acfb55ef96d6796869f002cc3eaf8) Thanks @tim-smart! - eliminate HttpApiEndpoint context in .handle + +## 0.69.7 + +### Patch Changes + +- [#3836](https://github.com/Effect-TS/effect/pull/3836) [`690d6c5`](https://github.com/Effect-TS/effect/commit/690d6c54d2145adb0af545c447db7d4755bf3c6b) Thanks @tim-smart! - add HttpApiBuilder.handler, for defining a single handler + +- [#3831](https://github.com/Effect-TS/effect/pull/3831) [`279fe3a`](https://github.com/Effect-TS/effect/commit/279fe3a7168fe84e520c2cc88ba189a15f03a2bc) Thanks @tim-smart! - ensure parent annotations take precedence over surrogate annotations + +- Updated dependencies [[`b9423d8`](https://github.com/Effect-TS/effect/commit/b9423d8bf8181a2389fdbce1e3c14ac6fe8d54f5)]: + - effect@3.10.3 + +## 0.69.6 + +### Patch Changes + +- [#3826](https://github.com/Effect-TS/effect/pull/3826) [`42cd72a`](https://github.com/Effect-TS/effect/commit/42cd72a44ca9593e4d81fbb50e8111625fd0fb81) Thanks @juliusmarminge! - ensure requests & responses have headers redacted when inspecting + +- Updated dependencies [[`714e119`](https://github.com/Effect-TS/effect/commit/714e11945e45e5a2554ee058e6c43f82a8e309cf), [`c1afd55`](https://github.com/Effect-TS/effect/commit/c1afd55c54e61f9c432823d21b3d016f79160a37)]: + - effect@3.10.2 + +## 0.69.5 + +### Patch Changes + +- Updated dependencies [[`9604d6b`](https://github.com/Effect-TS/effect/commit/9604d6b616435103dafea8b53637a9d1450b4750)]: + - effect@3.10.1 + +## 0.69.4 + +### Patch Changes + +- [#3816](https://github.com/Effect-TS/effect/pull/3816) [`c86b1d7`](https://github.com/Effect-TS/effect/commit/c86b1d7cd47b66df190ef9775a475467c1abdbd6) Thanks @tim-smart! - allow Request.signal to be missing in .toWebHandler apis + +## 0.69.3 + +### Patch Changes + +- [#3814](https://github.com/Effect-TS/effect/pull/3814) [`d5fba63`](https://github.com/Effect-TS/effect/commit/d5fba6391e1005e374aa0238f13edfbd65848313) Thanks @tim-smart! - cache OpenApi schema generation + +- [#3813](https://github.com/Effect-TS/effect/pull/3813) [`1eb2c30`](https://github.com/Effect-TS/effect/commit/1eb2c30ba064398db5790e376dedcfad55b7b005) Thanks @KhraksMamtsov! - Add support for bearer format OpenApi annotation + +- [#3811](https://github.com/Effect-TS/effect/pull/3811) [`02d413e`](https://github.com/Effect-TS/effect/commit/02d413e7b6bc1c64885969c37cc3e4e690c94d7d) Thanks @KhraksMamtsov! - A bug related to the format of the security schema keys has been fixed. According to the OpenAPI specification, it must match the regular expression` ^[a-zA-Z0-9.-_]+# @effect/platform + +## 0.69.2 + +### Patch Changes + +- [#3808](https://github.com/Effect-TS/effect/pull/3808) [`e7afc47`](https://github.com/Effect-TS/effect/commit/e7afc47ce83e381c3f4aed2b2974e3b3d86a2340) Thanks @tim-smart! - ensure HttpMiddleware is only initialized once + +## 0.69.1 + +### Patch Changes + +- [#3802](https://github.com/Effect-TS/effect/pull/3802) [`7564f56`](https://github.com/Effect-TS/effect/commit/7564f56bb2844cf39d2b0d2d9e93cf9b2205e9a8) Thanks @tim-smart! - simplify HttpApiMiddleware.TagClass type + +- [#3802](https://github.com/Effect-TS/effect/pull/3802) [`7564f56`](https://github.com/Effect-TS/effect/commit/7564f56bb2844cf39d2b0d2d9e93cf9b2205e9a8) Thanks @tim-smart! - add HttpServer.layerContext to platform-node/bun + +## 0.69.0 + +### Minor Changes + +- [#3764](https://github.com/Effect-TS/effect/pull/3764) [`6d9de6b`](https://github.com/Effect-TS/effect/commit/6d9de6b871c5c08e6509a4e830c3d74758faa198) Thanks @tim-smart! - HttpApi second revision + - `HttpApi`, `HttpApiGroup` & `HttpApiEndpoint` now use a chainable api instead + of a pipeable api. + - `HttpApiMiddleware` module has been added, with a updated way of defining + security middleware. + - You can now add multiple success schemas + - A url search parameter schema has been added + - Error schemas now support `HttpApiSchema` encoding apis + - `toWebHandler` has been simplified + + For more information, see the [README](https://github.com/Effect-TS/effect/blob/main/packages/platform/README.md#http-api). + +- [#3764](https://github.com/Effect-TS/effect/pull/3764) [`5821ce3`](https://github.com/Effect-TS/effect/commit/5821ce3455b47d25e0a40cae6ce22af9db5fa556) Thanks @patroza! - feat: implement Redactable. Used by Headers to not log sensitive information + +### Patch Changes + +- Updated dependencies [[`4a01828`](https://github.com/Effect-TS/effect/commit/4a01828b66d6213e9bbe18979c893b13f7bb29bf), [`4a01828`](https://github.com/Effect-TS/effect/commit/4a01828b66d6213e9bbe18979c893b13f7bb29bf), [`c79c4c1`](https://github.com/Effect-TS/effect/commit/c79c4c178390fe61ff6dda88c9e058862349343a), [`38d30f0`](https://github.com/Effect-TS/effect/commit/38d30f08b8da62f9c3e308b9250738cb8d17bdb5), [`5821ce3`](https://github.com/Effect-TS/effect/commit/5821ce3455b47d25e0a40cae6ce22af9db5fa556)]: + - effect@3.10.0 + +## 0.68.6 + +### Patch Changes + +- Updated dependencies [[`382556f`](https://github.com/Effect-TS/effect/commit/382556f8930780c0634de681077706113a8c8239), [`97cb014`](https://github.com/Effect-TS/effect/commit/97cb0145114b2cd2f378e98f6c4ff5bf2c1865f5)]: + - @effect/schema@0.75.5 + +## 0.68.5 + +### Patch Changes + +- [#3784](https://github.com/Effect-TS/effect/pull/3784) [`2036402`](https://github.com/Effect-TS/effect/commit/20364020b8b75a684791aa93d90626758023e9e9) Thanks @patroza! - fix HttpMiddleware circular import + +## 0.68.4 + +### Patch Changes + +- [#3780](https://github.com/Effect-TS/effect/pull/3780) [`1b1ef29`](https://github.com/Effect-TS/effect/commit/1b1ef29ae302322f69dc938f9337aa97b4c63266) Thanks @tim-smart! - ensure cors middleware also affects error responses + +## 0.68.3 + +### Patch Changes + +- [#3769](https://github.com/Effect-TS/effect/pull/3769) [`8c33087`](https://github.com/Effect-TS/effect/commit/8c330879425e80bed2f65e407cd59e991f0d7bec) Thanks @tim-smart! - add support for WebSocket protocols option + +- Updated dependencies [[`61a99b2`](https://github.com/Effect-TS/effect/commit/61a99b2bf9d757870ef0c2ec9d4c877cdd364a3d)]: + - effect@3.9.2 + - @effect/schema@0.75.4 + +## 0.68.2 + +### Patch Changes + +- Updated dependencies [[`360ec14`](https://github.com/Effect-TS/effect/commit/360ec14dd4102c526aef7433a8881ad4d9beab75)]: + - @effect/schema@0.75.3 + +## 0.68.1 + +### Patch Changes + +- [#3743](https://github.com/Effect-TS/effect/pull/3743) [`b75ac5d`](https://github.com/Effect-TS/effect/commit/b75ac5d0909115507bedc90f18f2d34deb217769) Thanks @sukovanej! - Add support for `ConfigProvider` based on .env files. + + ```ts + import { PlatformConfigProvider } from "@effect/platform" + import { NodeContext } from "@effect/platform-node" + import { Config } from "effect" + + Effect.gen(function* () { + const config = yield* Config.all({ + api_url: Config.string("API_URL"), + api_key: Config.string("API_KEY") + }) + + console.log(`Api config: ${config}`) + }).pipe( + Effect.provide( + PlatformConfigProvider.layerDotEnvAdd(".env").pipe( + Layer.provide(NodeContext.layer) + ) + ) + ) + ``` + +## 0.68.0 + +### Minor Changes + +- [#3756](https://github.com/Effect-TS/effect/pull/3756) [`90ceeab`](https://github.com/Effect-TS/effect/commit/90ceeab3a04051b740af18c8af8bd73ee8ec6363) Thanks @tim-smart! - remove HttpClient.Service type + +- [#3756](https://github.com/Effect-TS/effect/pull/3756) [`90ceeab`](https://github.com/Effect-TS/effect/commit/90ceeab3a04051b740af18c8af8bd73ee8ec6363) Thanks @tim-smart! - constrain HttpClient success type to HttpClientResponse + +- [#3756](https://github.com/Effect-TS/effect/pull/3756) [`90ceeab`](https://github.com/Effect-TS/effect/commit/90ceeab3a04051b740af18c8af8bd73ee8ec6363) Thanks @tim-smart! - add HttpClient accessor apis + + These apis allow you to easily send requests without first accessing the `HttpClient` service. + + Below is an example of using the `get` accessor api to send a GET request: + + ```ts + import { FetchHttpClient, HttpClient } from "@effect/platform" + import { Effect } from "effect" + + const program = HttpClient.get( + "https://jsonplaceholder.typicode.com/posts/1" + ).pipe( + Effect.andThen((response) => response.json), + Effect.scoped, + Effect.provide(FetchHttpClient.layer) + ) + + Effect.runPromise(program) + /* + Output: + { + userId: 1, + id: 1, + title: 'sunt aut facere repellat provident occaecati excepturi optio reprehenderit', + body: 'quia et suscipit\n' + + 'suscipit recusandae consequuntur expedita et cum\n' + + 'reprehenderit molestiae ut ut quas totam\n' + + 'nostrum rerum est autem sunt rem eveniet architecto' + } + */ + ``` + +### Patch Changes + +- Updated dependencies [[`f02b354`](https://github.com/Effect-TS/effect/commit/f02b354ab5b0451143b82bb73dc866be29adec85)]: + - @effect/schema@0.75.2 + +## 0.67.1 + +### Patch Changes + +- [#3740](https://github.com/Effect-TS/effect/pull/3740) [`3b2ad1d`](https://github.com/Effect-TS/effect/commit/3b2ad1d58a2e33dc1a72b7037396bd25ca1702a9) Thanks @tim-smart! - revert deno Inspectable changes + +- Updated dependencies [[`3b2ad1d`](https://github.com/Effect-TS/effect/commit/3b2ad1d58a2e33dc1a72b7037396bd25ca1702a9)]: + - effect@3.9.1 + - @effect/schema@0.75.1 + +## 0.67.0 + +### Minor Changes + +- [#3620](https://github.com/Effect-TS/effect/pull/3620) [`0ba66f2`](https://github.com/Effect-TS/effect/commit/0ba66f2451641fd6990e02ec1ed01c014db9dab0) Thanks @tim-smart! - add deno support to Inspectable + +### Patch Changes + +- Updated dependencies [[`ff3d1aa`](https://github.com/Effect-TS/effect/commit/ff3d1aab290b4d1173b2dfc7e4c76abb4babdc16), [`0ba66f2`](https://github.com/Effect-TS/effect/commit/0ba66f2451641fd6990e02ec1ed01c014db9dab0), [`bf77f51`](https://github.com/Effect-TS/effect/commit/bf77f51b323c383224ebf08adf77a7a6e8c9b3cd), [`016f9ad`](https://github.com/Effect-TS/effect/commit/016f9ad931a4b3d09a34e5caf13d87c5b8e9c984), [`0779681`](https://github.com/Effect-TS/effect/commit/07796813f07de035719728733096ba64ce333469), [`534129f`](https://github.com/Effect-TS/effect/commit/534129f8113ce1a8ec50828083e16da9c86326c6), [`d75140c`](https://github.com/Effect-TS/effect/commit/d75140c7a664ceda43142d999f4ff8dcd36d6dda), [`be0451c`](https://github.com/Effect-TS/effect/commit/be0451c149b6618af79cb839cdf04af2db1efb03), [`9237ac6`](https://github.com/Effect-TS/effect/commit/9237ac69bc07de5b3b60076a0ad2921c21de7457), [`be0451c`](https://github.com/Effect-TS/effect/commit/be0451c149b6618af79cb839cdf04af2db1efb03), [`5b36494`](https://github.com/Effect-TS/effect/commit/5b364942e9a9003fdb8217324f8a2d8369c969da), [`c716adb`](https://github.com/Effect-TS/effect/commit/c716adb250ebbea1d1048d818ef7fed4f621d186), [`4986391`](https://github.com/Effect-TS/effect/commit/49863919cd8628c962a712fb1df30d2983820933), [`d75140c`](https://github.com/Effect-TS/effect/commit/d75140c7a664ceda43142d999f4ff8dcd36d6dda), [`d1387ae`](https://github.com/Effect-TS/effect/commit/d1387aebd1ff01bbebde26be46d488956e4daef6)]: + - effect@3.9.0 + - @effect/schema@0.75.0 + +## 0.66.3 + +### Patch Changes + +- [#3736](https://github.com/Effect-TS/effect/pull/3736) [`f40da15`](https://github.com/Effect-TS/effect/commit/f40da15fbeb7c491840b8f409d47de79720891c3) Thanks @tim-smart! - add HttpClientResponse.filterStatus apis + +- [#3732](https://github.com/Effect-TS/effect/pull/3732) [`8e94585`](https://github.com/Effect-TS/effect/commit/8e94585abe62753bf3af28bfae77926a7c570ac3) Thanks @sukovanej! - Fix: handle `Blob` message data from a websocket. + +- [#3736](https://github.com/Effect-TS/effect/pull/3736) [`f40da15`](https://github.com/Effect-TS/effect/commit/f40da15fbeb7c491840b8f409d47de79720891c3) Thanks @tim-smart! - add HttpClient.retryTransient api + +- Updated dependencies [[`88e85db`](https://github.com/Effect-TS/effect/commit/88e85db34bd402526e27a323e950d053fa34d232), [`83887ca`](https://github.com/Effect-TS/effect/commit/83887ca1b1793916913d8550a4db4450cd14a044), [`5266b6c`](https://github.com/Effect-TS/effect/commit/5266b6cd86d76c3886da041c8829bca04b1a3110), [`cdead5c`](https://github.com/Effect-TS/effect/commit/cdead5c9cfd54dc6c4f215d9732f654c4a12e991), [`766a8af`](https://github.com/Effect-TS/effect/commit/766a8af307b414aca3648d91c4eab7493a5ec862)]: + - effect@3.8.5 + - @effect/schema@0.74.2 + +## 0.66.2 + +### Patch Changes + +- [#3667](https://github.com/Effect-TS/effect/pull/3667) [`fd83d0e`](https://github.com/Effect-TS/effect/commit/fd83d0e548feff9ea2d53d370a0b626c4a1d940e) Thanks @gcanti! - Remove default json schema annotations from string, number and boolean. + + Before + + ```ts + import { JSONSchema, Schema } from "@effect/schema" + + const schema = Schema.String.annotations({ examples: ["a", "b"] }) + + console.log(JSON.stringify(JSONSchema.make(schema), null, 2)) + /* + { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "string", + "description": "a string", + "title": "string", + "examples": [ + "a", + "b" + ] + } + */ + ``` + + After + + ```ts + import { JSONSchema, Schema } from "@effect/schema" + + const schema = Schema.String.annotations({ examples: ["a", "b"] }) + + console.log(JSON.stringify(JSONSchema.make(schema), null, 2)) + /* + { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "string", + "examples": [ + "a", + "b" + ] + } + */ + ``` + +- [#3672](https://github.com/Effect-TS/effect/pull/3672) [`090e41c`](https://github.com/Effect-TS/effect/commit/090e41c636d720b1c7d89684a739855765ed4382) Thanks @gcanti! - JSON Schema: handle refinements where the 'from' part includes a transformation, closes #3662 + + Before + + ```ts + import { JSONSchema, Schema } from "@effect/schema" + + const schema = Schema.Date + + console.log(JSON.stringify(JSONSchema.make(schema), null, 2)) + /* + throws + Error: Missing annotation + details: Generating a JSON Schema for this schema requires a "jsonSchema" annotation + schema (Refinement): Date + */ + ``` + + After + + ```ts + import { JSONSchema, Schema } from "@effect/schema" + + const schema = Schema.Date + + console.log(JSON.stringify(JSONSchema.make(schema), null, 2)) + /* + { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "string", + "description": "a string that will be parsed into a Date" + } + */ + ``` + +- Updated dependencies [[`734eae6`](https://github.com/Effect-TS/effect/commit/734eae654f215e4adca457d04d2a1728b1a55c83), [`fd83d0e`](https://github.com/Effect-TS/effect/commit/fd83d0e548feff9ea2d53d370a0b626c4a1d940e), [`4509656`](https://github.com/Effect-TS/effect/commit/45096569d50262275ee984f44c456f5c83b62683), [`ad7e1de`](https://github.com/Effect-TS/effect/commit/ad7e1de948745c0751bfdac96671028ff4b7a727), [`090e41c`](https://github.com/Effect-TS/effect/commit/090e41c636d720b1c7d89684a739855765ed4382), [`090e41c`](https://github.com/Effect-TS/effect/commit/090e41c636d720b1c7d89684a739855765ed4382)]: + - @effect/schema@0.74.1 + - effect@3.8.4 + +## 0.66.1 + +### Patch Changes + +- [#3664](https://github.com/Effect-TS/effect/pull/3664) [`3812788`](https://github.com/Effect-TS/effect/commit/3812788d79caaab8f559a62fd443018a04ac5647) Thanks @tim-smart! - add OpenApiJsonSchema module + +## 0.66.0 + +### Patch Changes + +- Updated dependencies [[`de48aa5`](https://github.com/Effect-TS/effect/commit/de48aa54e98d97722a8a4c2c8f9e1fe1d4560ea2)]: + - @effect/schema@0.74.0 + +## 0.65.5 + +### Patch Changes + +- [#3640](https://github.com/Effect-TS/effect/pull/3640) [`321b201`](https://github.com/Effect-TS/effect/commit/321b201adcb6bbbeb806b3467dd0b4cf063ccda8) Thanks @tim-smart! - use HttpClientRequest.originalUrl for search params parser + +- Updated dependencies [[`bb5ec6b`](https://github.com/Effect-TS/effect/commit/bb5ec6b4b6a6f537394596c5a596faf52cb2aef4)]: + - effect@3.8.3 + - @effect/schema@0.73.4 + +## 0.65.4 + +### Patch Changes + +- Updated dependencies [[`e6440a7`](https://github.com/Effect-TS/effect/commit/e6440a74fb3f12f6422ed794c07cb44af91cbacc)]: + - @effect/schema@0.73.3 + +## 0.65.3 + +### Patch Changes + +- Updated dependencies [[`f0d8ef1`](https://github.com/Effect-TS/effect/commit/f0d8ef1ce97ec2a87b09b3e24150cfeab85d6e2f)]: + - effect@3.8.2 + - @effect/schema@0.73.2 + +## 0.65.2 + +### Patch Changes + +- Updated dependencies [[`10bf621`](https://github.com/Effect-TS/effect/commit/10bf6213f36d8ddb00f058a4609b85220f3d8334), [`f56ab78`](https://github.com/Effect-TS/effect/commit/f56ab785cbee0c1c43bd2c182c35602f486f61f0), [`ae36fa6`](https://github.com/Effect-TS/effect/commit/ae36fa68f754eeab9a54b6dc0f8b44db513aa2b6)]: + - effect@3.8.1 + - @effect/schema@0.73.1 + +## 0.65.1 + +### Patch Changes + +- [#3614](https://github.com/Effect-TS/effect/pull/3614) [`e44c5f2`](https://github.com/Effect-TS/effect/commit/e44c5f228215738fe4e75023c7461bf9521249cb) Thanks @tim-smart! - accept Redacted in HttpClientRequest.basicAuth/bearerToken + +## 0.65.0 + +### Minor Changes + +- [#3541](https://github.com/Effect-TS/effect/pull/3541) [`7041393`](https://github.com/Effect-TS/effect/commit/7041393cff132e96566d3f36da0483a6ff6195e4) Thanks @tim-smart! - refactor /platform HttpClient + + #### HttpClient.fetch removed + + The `HttpClient.fetch` client implementation has been removed. Instead, you can + access a `HttpClient` using the corresponding `Context.Tag`. + + ```ts + import { FetchHttpClient, HttpClient } from "@effect/platform" + import { Effect } from "effect" + + Effect.gen(function* () { + const client = yield* HttpClient.HttpClient + + // make a get request + yield* client.get("https://jsonplaceholder.typicode.com/todos/1") + }).pipe( + Effect.scoped, + // the fetch client has been moved to the `FetchHttpClient` module + Effect.provide(FetchHttpClient.layer) + ) + ``` + + #### `HttpClient` interface now uses methods + + Instead of being a function that returns the response, the `HttpClient` + interface now uses methods to make requests. + + Some shorthand methods have been added to the `HttpClient` interface to make + less complex requests easier. + + ```ts + import { + FetchHttpClient, + HttpClient, + HttpClientRequest + } from "@effect/platform" + import { Effect } from "effect" + + Effect.gen(function* () { + const client = yield* HttpClient.HttpClient + + // make a get request + yield* client.get("https://jsonplaceholder.typicode.com/todos/1") + // make a post request + yield* client.post("https://jsonplaceholder.typicode.com/todos") + + // execute a request instance + yield* client.execute( + HttpClientRequest.get("https://jsonplaceholder.typicode.com/todos/1") + ) + }) + ``` + + #### Scoped `HttpClientResponse` helpers removed + + The `HttpClientResponse` helpers that also supplied the `Scope` have been removed. + + Instead, you can use the `HttpClientResponse` methods directly, and explicitly + add a `Effect.scoped` to the pipeline. + + ```ts + import { FetchHttpClient, HttpClient } from "@effect/platform" + import { Effect } from "effect" + + Effect.gen(function* () { + const client = yield* HttpClient.HttpClient + + yield* client.get("https://jsonplaceholder.typicode.com/todos/1").pipe( + Effect.flatMap((response) => response.json), + Effect.scoped // supply the `Scope` + ) + }) + ``` + + #### Some apis have been renamed + + Including the `HttpClientRequest` body apis, which is to make them more + discoverable. + +### Patch Changes + +- [#3541](https://github.com/Effect-TS/effect/pull/3541) [`812a4e8`](https://github.com/Effect-TS/effect/commit/812a4e86e2d1aa23b477ef5829aa0e5c07784936) Thanks @tim-smart! - add Logger.prettyLoggerDefault, to prevent duplicate pretty loggers + +- [#3541](https://github.com/Effect-TS/effect/pull/3541) [`6a128f6`](https://github.com/Effect-TS/effect/commit/6a128f63f9b41fec2db70790b3bbb96cb9afa1ab) Thanks @tim-smart! - ensure FetchHttpClient always attempts to send a request body + +- [#3541](https://github.com/Effect-TS/effect/pull/3541) [`adf7d7a`](https://github.com/Effect-TS/effect/commit/adf7d7a7dfce3a7021e9f3b0d847dc85be89d754) Thanks @tim-smart! - use Mailbox for Workers, Socket & Rpc + +- [#3541](https://github.com/Effect-TS/effect/pull/3541) [`e0d21a5`](https://github.com/Effect-TS/effect/commit/e0d21a54c8323728fbb75a32f4820a9996257809) Thanks @fubhy! - Added refinement overloads to `HttpClient.filterOrFail` and `HttpClient.filterOrElse` + +- Updated dependencies [[`fcfa6ee`](https://github.com/Effect-TS/effect/commit/fcfa6ee30ffd07d998bf22799357bf58580a116f), [`bb9931b`](https://github.com/Effect-TS/effect/commit/bb9931b62e249a3b801f2cb9d097aec0c8511af7), [`5798f76`](https://github.com/Effect-TS/effect/commit/5798f7619529de33e5ba06f551806f68fedc19db), [`5f0bfa1`](https://github.com/Effect-TS/effect/commit/5f0bfa17205398d4e4818bfbcf9e1b505b3b1fc5), [`7fdf9d9`](https://github.com/Effect-TS/effect/commit/7fdf9d9aa1e2c1c125cbf87991e6efbf4abb7b07), [`812a4e8`](https://github.com/Effect-TS/effect/commit/812a4e86e2d1aa23b477ef5829aa0e5c07784936), [`273565e`](https://github.com/Effect-TS/effect/commit/273565e7901639e8d0541930ab715aea9c80fbaa), [`569a801`](https://github.com/Effect-TS/effect/commit/569a8017ef0a0bc203e4312867cbdd37b0effbd7), [`aa1fa53`](https://github.com/Effect-TS/effect/commit/aa1fa5301e886b9657c8eb0d38cb87cef92a8305), [`02f6b06`](https://github.com/Effect-TS/effect/commit/02f6b0660e12bee1069532a9cc18d3ab855257be), [`12b893e`](https://github.com/Effect-TS/effect/commit/12b893e63cc6dfada4aca7773b4783940e2edf25), [`bbad27e`](https://github.com/Effect-TS/effect/commit/bbad27ec0a90860593f759405caa877e7f4a655f), [`adf7d7a`](https://github.com/Effect-TS/effect/commit/adf7d7a7dfce3a7021e9f3b0d847dc85be89d754), [`007289a`](https://github.com/Effect-TS/effect/commit/007289a52d5877f8e90e2dacf38171ff9bf603fd), [`42a8f99`](https://github.com/Effect-TS/effect/commit/42a8f99740eefdaf2c4544d2c345313f97547a36), [`eebfd29`](https://github.com/Effect-TS/effect/commit/eebfd29633fd5d38b505c5c0842036f61f05e913), [`040703d`](https://github.com/Effect-TS/effect/commit/040703d0e100cd5511e52d812c15492414262b5e)]: + - effect@3.8.0 + - @effect/schema@0.73.0 + +## 0.64.1 + +### Patch Changes + +- [#3582](https://github.com/Effect-TS/effect/pull/3582) [`8261c5a`](https://github.com/Effect-TS/effect/commit/8261c5ae6fe86872292ec1fc1a58ab9cea2f5f51) Thanks @gcanti! - add missing `encoding` argument to `Command.streamLines` + +- Updated dependencies [[`35a0f81`](https://github.com/Effect-TS/effect/commit/35a0f813141652d696461cd5d19fd146adaf85be)]: + - effect@3.7.3 + - @effect/schema@0.72.4 + +## 0.64.0 + +### Minor Changes + +- [#3565](https://github.com/Effect-TS/effect/pull/3565) [`90ac8f6`](https://github.com/Effect-TS/effect/commit/90ac8f6f6053a2e4498f8b0cc56fe12777d02e1a) Thanks @tim-smart! - move Etag implementation to /platform + +### Patch Changes + +- [#3565](https://github.com/Effect-TS/effect/pull/3565) [`90ac8f6`](https://github.com/Effect-TS/effect/commit/90ac8f6f6053a2e4498f8b0cc56fe12777d02e1a) Thanks @tim-smart! - add HttpApiBuilder.toWebHandler api + +- [#3567](https://github.com/Effect-TS/effect/pull/3567) [`3791e24`](https://github.com/Effect-TS/effect/commit/3791e241636b1dfe924a56f380ebc9a7ff0827a9) Thanks @tim-smart! - reduce boxing in Socket.toChannel implementation + +- [#3567](https://github.com/Effect-TS/effect/pull/3567) [`3791e24`](https://github.com/Effect-TS/effect/commit/3791e241636b1dfe924a56f380ebc9a7ff0827a9) Thanks @tim-smart! - add Socket.toChannelString api + +- Updated dependencies [[`f6acb71`](https://github.com/Effect-TS/effect/commit/f6acb71b17a0e6b0d449e7f661c9e2c3d335fcac)]: + - @effect/schema@0.72.3 + +## 0.63.3 + +### Patch Changes + +- [#3550](https://github.com/Effect-TS/effect/pull/3550) [`4a701c4`](https://github.com/Effect-TS/effect/commit/4a701c406da032563fedae459536c00ae5cfe3c7) Thanks @tim-smart! - ensure Socket.toChannel fiber is attached to Scope + +## 0.63.2 + +### Patch Changes + +- Updated dependencies [[`8a601d7`](https://github.com/Effect-TS/effect/commit/8a601d7a1f8ffe52ac9e6d67e9282a1495fe59c9), [`353ba19`](https://github.com/Effect-TS/effect/commit/353ba19f9b2b9e959f0a00d058c6d40a4bc02db7)]: + - effect@3.7.2 + - @effect/schema@0.72.2 + +## 0.63.1 + +### Patch Changes + +- Updated dependencies [[`79859e7`](https://github.com/Effect-TS/effect/commit/79859e71040d8edf1868b8530b90c650f4321eff), [`f6a469c`](https://github.com/Effect-TS/effect/commit/f6a469c190b9f00eee5ea0cd4d5912a0ef8b46f5), [`dcb9ec0`](https://github.com/Effect-TS/effect/commit/dcb9ec0db443894dd204d87450f779c44b9ad7f1), [`79aa6b1`](https://github.com/Effect-TS/effect/commit/79aa6b136e1f29b36f34e88cb2ff162bff2bb4ed)]: + - effect@3.7.1 + - @effect/schema@0.72.1 + +## 0.63.0 + +### Patch Changes + +- [#3410](https://github.com/Effect-TS/effect/pull/3410) [`6bf28f7`](https://github.com/Effect-TS/effect/commit/6bf28f7e3b1e5e0608ff567205fea0581d11666f) Thanks @tim-smart! - add HttpApi modules + + The `HttpApi` family of modules provide a declarative way to define HTTP APIs. + + For more infomation see the README.md for the /platform package:
+ https://github.com/Effect-TS/effect/blob/main/packages/platform/README.md + +- Updated dependencies [[`db89601`](https://github.com/Effect-TS/effect/commit/db89601ee9c1050c4e762b7bd7ec65a6a2799dfe), [`2f456cc`](https://github.com/Effect-TS/effect/commit/2f456cce5012b9fcb6b4e039190d527813b75b92), [`8745e41`](https://github.com/Effect-TS/effect/commit/8745e41ed96e3765dc6048efc2a9afbe05c8a1e9), [`e557838`](https://github.com/Effect-TS/effect/commit/e55783886b046d3c5f33447f455f9ccf2fa75922), [`d6e7e40`](https://github.com/Effect-TS/effect/commit/d6e7e40b1e2ad0c59aa02f07344d28601b14ebdc), [`8356321`](https://github.com/Effect-TS/effect/commit/8356321598da04bd77c1001f45a4e447bec5591d), [`192f2eb`](https://github.com/Effect-TS/effect/commit/192f2ebb2c4ddbf4bfd8baedd32140b2376868f4), [`718cb70`](https://github.com/Effect-TS/effect/commit/718cb70038629a6d58d02e407760e341f7c94474), [`e9d0310`](https://github.com/Effect-TS/effect/commit/e9d03107acbf204d9304f3e8aea0816b7d3c7dfb), [`6bf28f7`](https://github.com/Effect-TS/effect/commit/6bf28f7e3b1e5e0608ff567205fea0581d11666f)]: + - effect@3.7.0 + - @effect/schema@0.72.0 + +## 0.62.5 + +### Patch Changes + +- Updated dependencies [[`e809286`](https://github.com/Effect-TS/effect/commit/e8092865900608c4df7a6b7991b1c13cc1e4ca2d)]: + - effect@3.6.8 + - @effect/schema@0.71.4 + +## 0.62.4 + +### Patch Changes + +- [#3506](https://github.com/Effect-TS/effect/pull/3506) [`e7a65e3`](https://github.com/Effect-TS/effect/commit/e7a65e3c6a08636bbfce3d3af3098bf28474364d) Thanks @tim-smart! - use Logger.pretty for runMain, and support dual usage + +- Updated dependencies [[`50ec889`](https://github.com/Effect-TS/effect/commit/50ec8897a49b7d1fe84f63107f89d543c52f3dfc)]: + - effect@3.6.7 + - @effect/schema@0.71.3 + +## 0.62.3 + +### Patch Changes + +- Updated dependencies [[`f960bf4`](https://github.com/Effect-TS/effect/commit/f960bf45239e9badac6e0ad3a602f4174cd7bbdf), [`46a575f`](https://github.com/Effect-TS/effect/commit/46a575f48a05457b782fb21f7827d338c9b59320)]: + - effect@3.6.6 + - @effect/schema@0.71.2 + +## 0.62.2 + +### Patch Changes + +- [#3494](https://github.com/Effect-TS/effect/pull/3494) [`413994c`](https://github.com/Effect-TS/effect/commit/413994c9792f16d9d57cca3ae6eb254bf93bd261) Thanks @tim-smart! - add binary support to KeyValueStore + +- Updated dependencies [[`14a47a8`](https://github.com/Effect-TS/effect/commit/14a47a8c1f3cff2186b8fe7a919a1d773888fb5b), [`0c09841`](https://github.com/Effect-TS/effect/commit/0c0984173be3d58f050b300a1a8aa89d76ba49ae)]: + - effect@3.6.5 + - @effect/schema@0.71.1 + +## 0.62.1 + +### Patch Changes + +- [#3469](https://github.com/Effect-TS/effect/pull/3469) [`9efe0e5`](https://github.com/Effect-TS/effect/commit/9efe0e5b57ac557399be620822c21cc6e9add285) Thanks @tim-smart! - respond with 404 for NoSuchElementException in HttpServerRespondable + +## 0.62.0 + +### Patch Changes + +- [#3454](https://github.com/Effect-TS/effect/pull/3454) [`5dcb401`](https://github.com/Effect-TS/effect/commit/5dcb401bfc52a5c8f8934b1f95adf0ad515277d6) Thanks @tim-smart! - add HttpRouter.currentRouterConfig fiber ref + +- [#3450](https://github.com/Effect-TS/effect/pull/3450) [`0e42a8f`](https://github.com/Effect-TS/effect/commit/0e42a8f045ecb1fd3d080edf3d49fef16a9b0ca1) Thanks @tim-smart! - update dependencies + +- Updated dependencies [[`c1987e2`](https://github.com/Effect-TS/effect/commit/c1987e25c8f5c48bdc9ad223d7a6f2c32f93f5a1), [`8295281`](https://github.com/Effect-TS/effect/commit/8295281ae9bd7441e680402540bf3c8682ec417b), [`c940df6`](https://github.com/Effect-TS/effect/commit/c940df63800bf3c4396d91cf28ec34938642fd2c), [`00b6c6d`](https://github.com/Effect-TS/effect/commit/00b6c6d4001f5de728b7d990a1b14560b4961a63), [`1ceed14`](https://github.com/Effect-TS/effect/commit/1ceed149dc64f4874e64b5cf2f954eba0a5a1f12), [`f8d95a6`](https://github.com/Effect-TS/effect/commit/f8d95a61ad0762147933c5c32bb6d7237e18eef4), [`0e42a8f`](https://github.com/Effect-TS/effect/commit/0e42a8f045ecb1fd3d080edf3d49fef16a9b0ca1)]: + - @effect/schema@0.71.0 + - effect@3.6.4 + +## 0.61.8 + +### Patch Changes + +- Updated dependencies [[`04adcac`](https://github.com/Effect-TS/effect/commit/04adcace913e6fc483df266874a68005e9e04ccf)]: + - effect@3.6.3 + - @effect/schema@0.70.4 + +## 0.61.7 + +### Patch Changes + +- [#3437](https://github.com/Effect-TS/effect/pull/3437) [`17245a4`](https://github.com/Effect-TS/effect/commit/17245a4e783c19dee51529600b3b40f164fa59bc) Thanks @tim-smart! - add Cookies.get/getValue apis + +- [#3439](https://github.com/Effect-TS/effect/pull/3439) [`630d40e`](https://github.com/Effect-TS/effect/commit/630d40eaa7eb4d2f8b6705b16d4f426bc28a7d09) Thanks @sukovanej! - Add `HttpRouter.concatAll`. + +- [#3439](https://github.com/Effect-TS/effect/pull/3439) [`630d40e`](https://github.com/Effect-TS/effect/commit/630d40eaa7eb4d2f8b6705b16d4f426bc28a7d09) Thanks @sukovanej! - Fix `HttpRouter.concat` - add mounts from both routers. + +## 0.61.6 + +### Patch Changes + +- [#3436](https://github.com/Effect-TS/effect/pull/3436) [`d829b57`](https://github.com/Effect-TS/effect/commit/d829b576357f2e3b203ab7e107a1492de903a106) Thanks @tim-smart! - remove host from HttpServerRequest url's + +- Updated dependencies [[`99ad841`](https://github.com/Effect-TS/effect/commit/99ad8415293a82d08bd7043c563b29e2b468ca74), [`fd4b2f6`](https://github.com/Effect-TS/effect/commit/fd4b2f6516b325740dde615f1cf0229edf13ca0c)]: + - @effect/schema@0.70.3 + - effect@3.6.2 + +## 0.61.5 + +### Patch Changes + +- [#3409](https://github.com/Effect-TS/effect/pull/3409) [`056b710`](https://github.com/Effect-TS/effect/commit/056b7108978e70612176c23991916f678d947f38) Thanks @sukovanej! - Add `HttpClient.layerTest`. + +## 0.61.4 + +### Patch Changes + +- [#3414](https://github.com/Effect-TS/effect/pull/3414) [`e7cb109`](https://github.com/Effect-TS/effect/commit/e7cb109d0754207024a64d55b6bd2a674dd8ed7d) Thanks @tim-smart! - ensure broken HttpMiddleware that doesn't fail responds + +## 0.61.3 + +### Patch Changes + +- [#3408](https://github.com/Effect-TS/effect/pull/3408) [`fb9f786`](https://github.com/Effect-TS/effect/commit/fb9f7867f0c895e63f9ef23e8d0941248c42179d) Thanks @tim-smart! - ensure failure in HttpMiddleware results in a response + +- Updated dependencies [[`510a34d`](https://github.com/Effect-TS/effect/commit/510a34d4cc5d2f51347a53847f6c7db84d2b17c6), [`45dbb9f`](https://github.com/Effect-TS/effect/commit/45dbb9ffeaf93d9e4df99d0cd4920e41ba9a3978)]: + - effect@3.6.1 + - @effect/schema@0.70.2 + +## 0.61.2 + +### Patch Changes + +- Updated dependencies [[`3dce357`](https://github.com/Effect-TS/effect/commit/3dce357efe4a4451d7d29859d08ac11713999b1a), [`657fc48`](https://github.com/Effect-TS/effect/commit/657fc48bb32daf2dc09c9335b3cbc3152bcbdd3b)]: + - @effect/schema@0.70.1 + +## 0.61.1 + +### Patch Changes + +- [#3384](https://github.com/Effect-TS/effect/pull/3384) [`11223bf`](https://github.com/Effect-TS/effect/commit/11223bf9cbf5b822e0bf9a9fb2b35b2ad88af692) Thanks @tim-smart! - use type alias for HttpApp + +## 0.61.0 + +### Patch Changes + +- [#3380](https://github.com/Effect-TS/effect/pull/3380) [`7d02174`](https://github.com/Effect-TS/effect/commit/7d02174af3bcbf054e5cdddb821c91d0f47e8285) Thanks @fubhy! - Changed various function signatures to return `Array` instead of `ReadonlyArray` + +- Updated dependencies [[`1e0fe80`](https://github.com/Effect-TS/effect/commit/1e0fe802b36c257971296617473ce0abe730e8dc), [`8135294`](https://github.com/Effect-TS/effect/commit/8135294b591ea94fde7e6f94a504608f0e630520), [`cd255a4`](https://github.com/Effect-TS/effect/commit/cd255a48872d8fb924cf713ef73f0883a9cc6987), [`3845646`](https://github.com/Effect-TS/effect/commit/3845646828e98f3c7cda1217f6cfe5f642ac0603), [`2d09078`](https://github.com/Effect-TS/effect/commit/2d09078c5948b37fc2f79ef858fe4ca3e4814085), [`4bce5a0`](https://github.com/Effect-TS/effect/commit/4bce5a0274203550ccf117d830721891b0a3d182), [`4ddbff0`](https://github.com/Effect-TS/effect/commit/4ddbff0bb4e3ffddfeb509c59835b83245fb975e), [`e74cc38`](https://github.com/Effect-TS/effect/commit/e74cc38cb420a320c4d7ef98180f19d452a8b316), [`bb069b4`](https://github.com/Effect-TS/effect/commit/bb069b49ef291c532a02c1e8e74271f6d1bb32ec), [`cd255a4`](https://github.com/Effect-TS/effect/commit/cd255a48872d8fb924cf713ef73f0883a9cc6987), [`7d02174`](https://github.com/Effect-TS/effect/commit/7d02174af3bcbf054e5cdddb821c91d0f47e8285)]: + - effect@3.6.0 + - @effect/schema@0.70.0 + +## 0.60.3 + +### Patch Changes + +- Updated dependencies [[`7c0da50`](https://github.com/Effect-TS/effect/commit/7c0da5050d30cb804f4eacb15995d0fb7f3a28d2), [`2fc0ff4`](https://github.com/Effect-TS/effect/commit/2fc0ff4c59c25977018f6ac70ced99b04a8c7b2b), [`6359644`](https://github.com/Effect-TS/effect/commit/635964446323cf55d4060559337e710e4a24496e), [`f262665`](https://github.com/Effect-TS/effect/commit/f262665c2773492c01e5dd0e8d6db235aafaaad8), [`7f41e42`](https://github.com/Effect-TS/effect/commit/7f41e428830bf3043b8be0d28dcd235d5747c942), [`9bbe7a6`](https://github.com/Effect-TS/effect/commit/9bbe7a681430ebf5c10167bb7140ba3742e46bb7), [`f566fd1`](https://github.com/Effect-TS/effect/commit/f566fd1d7eea531a0d981dd24037f14a603a1273)]: + - @effect/schema@0.69.3 + - effect@3.5.9 + +## 0.60.2 + +### Patch Changes + +- [#3339](https://github.com/Effect-TS/effect/pull/3339) [`eb4d014`](https://github.com/Effect-TS/effect/commit/eb4d014c559e1b4c95b3fb9295fe77593c17ed7a) Thanks @fubhy! - Fixed various search params related function signatures (`Array => ReadonlyArray`) + +- [#3353](https://github.com/Effect-TS/effect/pull/3353) [`fc20f73`](https://github.com/Effect-TS/effect/commit/fc20f73c69e577981cb64714de2adc97e1004dae) Thanks @tim-smart! - wait for worker ready latch before sending initial message + +- Updated dependencies [[`1ba640c`](https://github.com/Effect-TS/effect/commit/1ba640c702f187a866023bf043c26e25cce941ef), [`c8c71bd`](https://github.com/Effect-TS/effect/commit/c8c71bd20eb87d23133dac6156b83bb08941597c), [`a26ce58`](https://github.com/Effect-TS/effect/commit/a26ce581ca7d407e1e81439b58c8045b3fa65231)]: + - effect@3.5.8 + - @effect/schema@0.69.2 + +## 0.60.1 + +### Patch Changes + +- Updated dependencies [[`f241154`](https://github.com/Effect-TS/effect/commit/f241154added5d91e95866c39481f09cdb13bd4d)]: + - @effect/schema@0.69.1 + +## 0.60.0 + +### Patch Changes + +- Updated dependencies [[`20807a4`](https://github.com/Effect-TS/effect/commit/20807a45edeb4334e903dca5d708cd62a71702d8)]: + - @effect/schema@0.69.0 + +## 0.59.3 + +### Patch Changes + +- [#3310](https://github.com/Effect-TS/effect/pull/3310) [`99bddcf`](https://github.com/Effect-TS/effect/commit/99bddcfb3d6eab4d489d055404e26ad81afe52fc) Thanks @fubhy! - Added additional pure annotations to improve tree-shakeability + +- Updated dependencies [[`3afcc93`](https://github.com/Effect-TS/effect/commit/3afcc93413a3d910beb69e4ce9ae120e4adaffd5), [`99bddcf`](https://github.com/Effect-TS/effect/commit/99bddcfb3d6eab4d489d055404e26ad81afe52fc), [`6921c4f`](https://github.com/Effect-TS/effect/commit/6921c4fb8c45badff09b493043b85ca71302b560)]: + - effect@3.5.7 + - @effect/schema@0.68.27 + +## 0.59.2 + +### Patch Changes + +- Updated dependencies [[`f0285d3`](https://github.com/Effect-TS/effect/commit/f0285d3af6a18829123bc1818331c67206becbc4), [`8ec4955`](https://github.com/Effect-TS/effect/commit/8ec49555ed3b3c98093fa4d135a4c57a3f16ebd1), [`3ac2d76`](https://github.com/Effect-TS/effect/commit/3ac2d76048da09e876cf6c3aee3397febd843fe9), [`cc327a1`](https://github.com/Effect-TS/effect/commit/cc327a1bccd22a4ee27ec7e58b53205e93b23e2c), [`4bfe4fb`](https://github.com/Effect-TS/effect/commit/4bfe4fb5c82f597c9beea9baa92e772593598b60), [`2b14d18`](https://github.com/Effect-TS/effect/commit/2b14d181462cad8359da4fa6bc6dfda0f742c398)]: + - @effect/schema@0.68.26 + - effect@3.5.6 + +## 0.59.1 + +### Patch Changes + +- [#3278](https://github.com/Effect-TS/effect/pull/3278) [`fcecff7`](https://github.com/Effect-TS/effect/commit/fcecff7f7e12b295a252f124861b801c73072151) Thanks @tim-smart! - ensure /platform HttpApp.toWebHandler runs Stream's with the current runtime + + Also add runtime options to HttpServerResponse.toWeb + +- [#3281](https://github.com/Effect-TS/effect/pull/3281) [`adbf753`](https://github.com/Effect-TS/effect/commit/adbf75340a9db15dc5cadc66e911a8978a195a65) Thanks @tim-smart! - drop path-browserify dependency + +- [#3281](https://github.com/Effect-TS/effect/pull/3281) [`adbf753`](https://github.com/Effect-TS/effect/commit/adbf75340a9db15dc5cadc66e911a8978a195a65) Thanks @tim-smart! - drop fast-querystring dependency + +- Updated dependencies [[`a9d7800`](https://github.com/Effect-TS/effect/commit/a9d7800f6a253192b653d77778b0674f39b1ca39)]: + - effect@3.5.5 + - @effect/schema@0.68.25 + +## 0.59.0 + +### Minor Changes + +- [#3260](https://github.com/Effect-TS/effect/pull/3260) [`53c0db0`](https://github.com/Effect-TS/effect/commit/53c0db06872d5b5edea2a706e83249908385325c) Thanks @tim-smart! - replace /platform RefailError with use of the "cause" property + +- [#3255](https://github.com/Effect-TS/effect/pull/3255) [`ada68b3`](https://github.com/Effect-TS/effect/commit/ada68b3e61c67907c2a281c024c84d818186ca4c) Thanks @tim-smart! - refactor & simplify /platform backing workers + + Improves worker performance by 2x + +### Patch Changes + +- Updated dependencies [[`ed0dde4`](https://github.com/Effect-TS/effect/commit/ed0dde4888e6f1a97ad5bba06b755d26a6a1c52e), [`ca775ce`](https://github.com/Effect-TS/effect/commit/ca775cec53baebc1a43d9b8852a3ac6726178498), [`5be9cc0`](https://github.com/Effect-TS/effect/commit/5be9cc044025a9541b9b7acefa2d3fc05fa1301b), [`203658f`](https://github.com/Effect-TS/effect/commit/203658f8001c132b25764ab70344b171683b554c), [`eb1c4d4`](https://github.com/Effect-TS/effect/commit/eb1c4d44e54b9d8d201a366d1ff94face2a6dcd3)]: + - effect@3.5.4 + - @effect/schema@0.68.24 + +## 0.58.27 + +### Patch Changes + +- [#3241](https://github.com/Effect-TS/effect/pull/3241) [`a1db40a`](https://github.com/Effect-TS/effect/commit/a1db40a650ab842e778654f0d88e80f2ef4fd6f3) Thanks @tim-smart! - ensure interrupts are handled in WorkerRunner + +- Updated dependencies [[`edb0da3`](https://github.com/Effect-TS/effect/commit/edb0da383746d760f35d8582f5fb0cc0eeca9217), [`edb0da3`](https://github.com/Effect-TS/effect/commit/edb0da383746d760f35d8582f5fb0cc0eeca9217), [`c8d3fb0`](https://github.com/Effect-TS/effect/commit/c8d3fb0fe23585f6efb724af51fbab3ba1ad6e83), [`dabd028`](https://github.com/Effect-TS/effect/commit/dabd028decf9b7983ca16ebe0f48c05c11a84b68), [`786b2ab`](https://github.com/Effect-TS/effect/commit/786b2ab29d525c877bb84035dac9e2d6499339d1), [`fc57354`](https://github.com/Effect-TS/effect/commit/fc573547d41667016fce05eaee75960fcc6dce4d)]: + - effect@3.5.3 + - @effect/schema@0.68.23 + +## 0.58.26 + +### Patch Changes + +- Updated dependencies [[`639208e`](https://github.com/Effect-TS/effect/commit/639208eeb8a44622994f832bc2d45d06ab636bc8), [`6684b4c`](https://github.com/Effect-TS/effect/commit/6684b4c27d77a7fcc7af2e261a450edf971b62b5), [`6684b4c`](https://github.com/Effect-TS/effect/commit/6684b4c27d77a7fcc7af2e261a450edf971b62b5), [`6684b4c`](https://github.com/Effect-TS/effect/commit/6684b4c27d77a7fcc7af2e261a450edf971b62b5)]: + - effect@3.5.2 + - @effect/schema@0.68.22 + +## 0.58.25 + +### Patch Changes + +- [#3223](https://github.com/Effect-TS/effect/pull/3223) [`0623fca`](https://github.com/Effect-TS/effect/commit/0623fca41679b0e3c5a10dd0f8985f91670bd721) Thanks @tim-smart! - improve /platform/WorkerError messages + +## 0.58.24 + +### Patch Changes + +- Updated dependencies [[`55fdd76`](https://github.com/Effect-TS/effect/commit/55fdd761ee95afd73b6a892c13fee92b36c02837)]: + - effect@3.5.1 + - @effect/schema@0.68.21 + +## 0.58.23 + +### Patch Changes + +- Updated dependencies [[`a1f5b83`](https://github.com/Effect-TS/effect/commit/a1f5b831a1bc7535988b370d68d0b3eb1123e0ce), [`a1f5b83`](https://github.com/Effect-TS/effect/commit/a1f5b831a1bc7535988b370d68d0b3eb1123e0ce), [`a1f5b83`](https://github.com/Effect-TS/effect/commit/a1f5b831a1bc7535988b370d68d0b3eb1123e0ce), [`60bc3d0`](https://github.com/Effect-TS/effect/commit/60bc3d0867b13e48b24dc22604b4dd2e7b2c1ca4), [`5ab348f`](https://github.com/Effect-TS/effect/commit/5ab348f265db3d283aa091ddca6d2d49137c16f2), [`60bc3d0`](https://github.com/Effect-TS/effect/commit/60bc3d0867b13e48b24dc22604b4dd2e7b2c1ca4), [`3e04bf8`](https://github.com/Effect-TS/effect/commit/3e04bf8a7127e956cadb7684a8f4c661df57663b), [`e7fc45f`](https://github.com/Effect-TS/effect/commit/e7fc45f0c7002aafdaec7878149ac064cd104ea3), [`a1f5b83`](https://github.com/Effect-TS/effect/commit/a1f5b831a1bc7535988b370d68d0b3eb1123e0ce), [`4626de5`](https://github.com/Effect-TS/effect/commit/4626de59c25b384216faa0be87bf0b8cd36357d0), [`f01e7db`](https://github.com/Effect-TS/effect/commit/f01e7db317827255d7901f523f2e28b43298e8df), [`60bc3d0`](https://github.com/Effect-TS/effect/commit/60bc3d0867b13e48b24dc22604b4dd2e7b2c1ca4), [`79d2d91`](https://github.com/Effect-TS/effect/commit/79d2d91464d95dde0e9444d43e7a7f309f05d6e6), [`ac71f37`](https://github.com/Effect-TS/effect/commit/ac71f378f2413e5aa91c95f649ffe898d6a26114), [`8432360`](https://github.com/Effect-TS/effect/commit/8432360ce68614a419bb328083a4109d0fc8aa93), [`e4bf1bf`](https://github.com/Effect-TS/effect/commit/e4bf1bf2b4a970eacd77c9b77b5ea8c68bc84498), [`13cb861`](https://github.com/Effect-TS/effect/commit/13cb861a5eded15c55c6cdcf6a8acde8320367a6), [`79d2d91`](https://github.com/Effect-TS/effect/commit/79d2d91464d95dde0e9444d43e7a7f309f05d6e6), [`e7fc45f`](https://github.com/Effect-TS/effect/commit/e7fc45f0c7002aafdaec7878149ac064cd104ea3), [`9f66825`](https://github.com/Effect-TS/effect/commit/9f66825f1fce0fe8d10420c285f7dc4c71e8af8d)]: + - effect@3.5.0 + - @effect/schema@0.68.20 + +## 0.58.22 + +### Patch Changes + +- [#3209](https://github.com/Effect-TS/effect/pull/3209) [`366f2ee`](https://github.com/Effect-TS/effect/commit/366f2ee3fb6f712a44e8f84fc188612e5ecc016d) Thanks @tim-smart! - simplify /platform http response handling + +- [#3209](https://github.com/Effect-TS/effect/pull/3209) [`366f2ee`](https://github.com/Effect-TS/effect/commit/366f2ee3fb6f712a44e8f84fc188612e5ecc016d) Thanks @tim-smart! - support middleware in HttpApp web handler apis + +- Updated dependencies [[`7af137c`](https://github.com/Effect-TS/effect/commit/7af137c9433f6e74959b3887561ec1e6f12e10ee), [`ee4b3dc`](https://github.com/Effect-TS/effect/commit/ee4b3dc5f68d19dc3ae1c2d12901c5b8ffbebabb), [`097d25c`](https://github.com/Effect-TS/effect/commit/097d25cb5d13c049e01789651be56b09620186ef)]: + - effect@3.4.9 + - @effect/schema@0.68.19 + +## 0.58.21 + +### Patch Changes + +- Updated dependencies [[`5d5cc6c`](https://github.com/Effect-TS/effect/commit/5d5cc6cfd7d63b07081290fb189b364999201fc5), [`a435e0f`](https://github.com/Effect-TS/effect/commit/a435e0fc5378b33a49bcec92ee235df6f16a2419), [`b5554db`](https://github.com/Effect-TS/effect/commit/b5554db36c4dd6f64fa5e6a62a29b2759c54217a), [`359ff8a`](https://github.com/Effect-TS/effect/commit/359ff8aa2e4e6389bf56d759baa804e2a7674a16), [`a9c4fb3`](https://github.com/Effect-TS/effect/commit/a9c4fb3bf3c6e92cd1c142b0605fddf7eb3c697c), [`f7534b9`](https://github.com/Effect-TS/effect/commit/f7534b94cba06b143a3d4f29275d92874a939559)]: + - @effect/schema@0.68.18 + - effect@3.4.8 + +## 0.58.20 + +### Patch Changes + +- Updated dependencies [[`15967cf`](https://github.com/Effect-TS/effect/commit/15967cf18931fb6ede3083eb687a8dfff371cc56), [`2328e17`](https://github.com/Effect-TS/effect/commit/2328e17577112db17c29b7756942a0ff64a70ee0), [`a5737d6`](https://github.com/Effect-TS/effect/commit/a5737d6db2b921605c332eabbc5402ee3d17357b)]: + - @effect/schema@0.68.17 + - effect@3.4.7 + +## 0.58.19 + +### Patch Changes + +- Updated dependencies [[`d006cec`](https://github.com/Effect-TS/effect/commit/d006cec022e8524dbfd6dc6df751fe4c86b10042), [`cb22726`](https://github.com/Effect-TS/effect/commit/cb2272656881aa5878a1c3fc0b12d8fbc66eb63c), [`e911cfd`](https://github.com/Effect-TS/effect/commit/e911cfdc79418462d7e9000976fded15ea6b738d)]: + - @effect/schema@0.68.16 + +## 0.58.18 + +### Patch Changes + +- [#3136](https://github.com/Effect-TS/effect/pull/3136) [`7f8900a`](https://github.com/Effect-TS/effect/commit/7f8900a1de9addeb0d371103a2c5c2aa3e4ff95e) Thanks @tim-smart! - support undefined in http request schema apis + +## 0.58.17 + +### Patch Changes + +- Updated dependencies [[`5c0ceb0`](https://github.com/Effect-TS/effect/commit/5c0ceb00826cce9e50bf9d41d83e191d5352c030), [`5c0ceb0`](https://github.com/Effect-TS/effect/commit/5c0ceb00826cce9e50bf9d41d83e191d5352c030), [`34faeb6`](https://github.com/Effect-TS/effect/commit/34faeb6305ba52af4d6f8bdd2e633bb6a5a7a35b), [`33735b1`](https://github.com/Effect-TS/effect/commit/33735b16b41bd26929d8f4754c190925db6323b7), [`5c0ceb0`](https://github.com/Effect-TS/effect/commit/5c0ceb00826cce9e50bf9d41d83e191d5352c030), [`139d4b3`](https://github.com/Effect-TS/effect/commit/139d4b39fb3bff2eeaa7c0c809c581da42425a83)]: + - effect@3.4.6 + - @effect/schema@0.68.15 + +## 0.58.16 + +### Patch Changes + +- Updated dependencies [[`61e5964`](https://github.com/Effect-TS/effect/commit/61e59640fd993216cca8ace0ac8abd9104e213ce)]: + - @effect/schema@0.68.14 + +## 0.58.15 + +### Patch Changes + +- [#3123](https://github.com/Effect-TS/effect/pull/3123) [`baa90df`](https://github.com/Effect-TS/effect/commit/baa90df9663f5f37d7b6814dad25142d53dbc720) Thanks @tim-smart! - add HttpClient.followRedirects api + +- Updated dependencies [[`cb76bcb`](https://github.com/Effect-TS/effect/commit/cb76bcb2f8858a90db4f785efee262cea1b9844e)]: + - @effect/schema@0.68.13 + +## 0.58.14 + +### Patch Changes + +- [#3104](https://github.com/Effect-TS/effect/pull/3104) [`52a87c7`](https://github.com/Effect-TS/effect/commit/52a87c7a0b9536398deaf8ec507e53a82c607219) Thanks @tim-smart! - remove the stack from HttpServerError.RouteNotFound + +- [#3109](https://github.com/Effect-TS/effect/pull/3109) [`6d2280e`](https://github.com/Effect-TS/effect/commit/6d2280e9497c95cb0e965ca462c825345074eedf) Thanks @tim-smart! - fix assignability of HttpMiddleware in HttpRouter.use + +## 0.58.13 + +### Patch Changes + +- [#3102](https://github.com/Effect-TS/effect/pull/3102) [`dbd53ea`](https://github.com/Effect-TS/effect/commit/dbd53ea363c71a24449cb068251054c3a1acf864) Thanks @tim-smart! - filter undefined from UrlParams Input + +- Updated dependencies [[`a047af9`](https://github.com/Effect-TS/effect/commit/a047af99447dfffc729e9c8ef0ca143537927e91), [`d990544`](https://github.com/Effect-TS/effect/commit/d9905444b9e800850cb65899114ca0e502e68fe8)]: + - effect@3.4.5 + - @effect/schema@0.68.12 + +## 0.58.12 + +### Patch Changes + +- [#3094](https://github.com/Effect-TS/effect/pull/3094) [`74e0ad2`](https://github.com/Effect-TS/effect/commit/74e0ad23b4c36f41b7fd10856b20f8b701bc4044) Thanks @tim-smart! - add mount apis to HttpRouter.Service + +- [#3094](https://github.com/Effect-TS/effect/pull/3094) [`74e0ad2`](https://github.com/Effect-TS/effect/commit/74e0ad23b4c36f41b7fd10856b20f8b701bc4044) Thanks @tim-smart! - add HttpRouter.DefaultServices to all HttpRouter.Tag's + +- Updated dependencies [[`72638e3`](https://github.com/Effect-TS/effect/commit/72638e3d99f0e93a24febf6c225256ce92d4a20b), [`d7dde2b`](https://github.com/Effect-TS/effect/commit/d7dde2b4af08b37af859d4c327c1f5c6f00cf9d9), [`9b2fc3b`](https://github.com/Effect-TS/effect/commit/9b2fc3b9dfd304a2bd0508ef2313cfc54357be0c), [`d71c192`](https://github.com/Effect-TS/effect/commit/d71c192b89fd1162423acddc5fd3d6270fbf2ef6)]: + - effect@3.4.4 + - @effect/schema@0.68.11 + +## 0.58.11 + +### Patch Changes + +- [#3091](https://github.com/Effect-TS/effect/pull/3091) [`a5b95b5`](https://github.com/Effect-TS/effect/commit/a5b95b548284e4798654ae7ce6883fa49108f0ea) Thanks @tim-smart! - add some common services to HttpRouter.Default + +- [#3090](https://github.com/Effect-TS/effect/pull/3090) [`5e29579`](https://github.com/Effect-TS/effect/commit/5e29579187cb8420ea4930b3999fec984f8999f4) Thanks @tim-smart! - add HttpServerRequest.toURL api + + To try retreive the full URL for the request. + +## 0.58.10 + +### Patch Changes + +- [#3088](https://github.com/Effect-TS/effect/pull/3088) [`a48ee84`](https://github.com/Effect-TS/effect/commit/a48ee845ac21bbde9baf938af9e97a98322211c9) Thanks @tim-smart! - add HttpServerRespondable trait + + This trait allows you to define how a value should be responded to in an HTTP + server. + + You can it for both errors and success values. + + ```ts + import { Schema } from "@effect/schema" + import { + HttpRouter, + HttpServerRespondable, + HttpServerResponse + } from "@effect/platform" + + class User extends Schema.Class("User")({ + name: Schema.String + }) { + [HttpServerRespondable.symbol]() { + return HttpServerResponse.schemaJson(User)(this) + } + } + + class MyError extends Schema.TaggedError()("MyError", { + message: Schema.String + }) { + [HttpServerRespondable.symbol]() { + return HttpServerResponse.schemaJson(MyError)(this, { status: 403 }) + } + } + + HttpRouter.empty.pipe( + // responds with `{ "name": "test" }` + HttpRouter.get("/user", Effect.succeed(new User({ name: "test" }))), + // responds with a 403 status, and `{ "_tag": "MyError", "message": "boom" }` + HttpRouter.get("/fail", new MyError({ message: "boom" })) + ) + ``` + +- [#3088](https://github.com/Effect-TS/effect/pull/3088) [`a48ee84`](https://github.com/Effect-TS/effect/commit/a48ee845ac21bbde9baf938af9e97a98322211c9) Thanks @tim-smart! - swap type parameters for HttpRouter.Tag, so request context comes first + +- [#3088](https://github.com/Effect-TS/effect/pull/3088) [`a48ee84`](https://github.com/Effect-TS/effect/commit/a48ee845ac21bbde9baf938af9e97a98322211c9) Thanks @tim-smart! - add HttpRouter.Default, a default instance of HttpRouter.Tag + +- [#3089](https://github.com/Effect-TS/effect/pull/3089) [`ab3180f`](https://github.com/Effect-TS/effect/commit/ab3180f827041d0ea3b2d72254a1a8683e99e056) Thanks @tim-smart! - add HttpClientResponse.matchStatus\* apis + + Which allows you to pattern match on the status code of a response. + + ```ts + HttpClientRequest.get("/todos/1").pipe( + HttpClient.fetch, + HttpClientResponse.matchStatusScoped({ + "2xx": (_response) => Effect.succeed("ok"), + 404: (_response) => Effect.fail("not found"), + orElse: (_response) => Effect.fail("boom") + }) + ) + ``` + +- [#3079](https://github.com/Effect-TS/effect/pull/3079) [`bbdd365`](https://github.com/Effect-TS/effect/commit/bbdd36567706c94cdec45bacea825941c347b6cd) Thanks @tim-smart! - update to typescript 5.5 + +- Updated dependencies [[`c342739`](https://github.com/Effect-TS/effect/commit/c3427396226e1ad7b95b40595a23f9bdff3e3365), [`8898e5e`](https://github.com/Effect-TS/effect/commit/8898e5e238622f6337583d91ee23609c1f5ccdf7), [`ff78636`](https://github.com/Effect-TS/effect/commit/ff786367c522975f40f0f179a0ecdfcfab7ecbdb), [`c86bd4e`](https://github.com/Effect-TS/effect/commit/c86bd4e134c23146c216f9ff97e03781d55991b6), [`bbdd365`](https://github.com/Effect-TS/effect/commit/bbdd36567706c94cdec45bacea825941c347b6cd), [`bbdd365`](https://github.com/Effect-TS/effect/commit/bbdd36567706c94cdec45bacea825941c347b6cd)]: + - effect@3.4.3 + - @effect/schema@0.68.10 + +## 0.58.9 + +### Patch Changes + +- Updated dependencies [[`0b47fdf`](https://github.com/Effect-TS/effect/commit/0b47fdfe449f42de89e0e88b61ae5140f629e5c4)]: + - @effect/schema@0.68.9 + +## 0.58.8 + +### Patch Changes + +- Updated dependencies [[`192261b`](https://github.com/Effect-TS/effect/commit/192261b2aec94e9913ceed83683fdcfbc9fca66f), [`3da1497`](https://github.com/Effect-TS/effect/commit/3da1497b5c9cc886d300258bc928fd68a4fefe6f)]: + - @effect/schema@0.68.8 + - effect@3.4.2 + +## 0.58.7 + +### Patch Changes + +- [#3064](https://github.com/Effect-TS/effect/pull/3064) [`027004a`](https://github.com/Effect-TS/effect/commit/027004a897f654791e75faa28eefb50dd0244b6e) Thanks @tim-smart! - add HttpRouter.Tag.unwrap api + +## 0.58.6 + +### Patch Changes + +- [#3059](https://github.com/Effect-TS/effect/pull/3059) [`2e8e252`](https://github.com/Effect-TS/effect/commit/2e8e2520cac712f0eb644553bd476429ebd674e4) Thanks @tim-smart! - add Layer based api for creating HttpRouter's + + ```ts + import { + HttpMiddleware, + HttpRouter, + HttpServer, + HttpServerResponse + } from "@effect/platform" + import { BunHttpServer, BunRuntime } from "@effect/platform-bun" + import { Effect, Layer } from "effect" + + // create your router Context.Tag + class UserRouter extends HttpRouter.Tag("UserRouter")() {} + + // create routes with the `.use` api. + // There is also `.useScoped` + const GetUsers = UserRouter.use((router) => + Effect.gen(function* () { + yield* router.get("/", HttpServerResponse.text("got users")) + }) + ) + + const CreateUser = UserRouter.use((router) => + Effect.gen(function* () { + yield* router.post("/", HttpServerResponse.text("created user")) + }) + ) + + const AllRoutes = Layer.mergeAll(GetUsers, CreateUser) + + const ServerLive = BunHttpServer.layer({ port: 3000 }) + + // access the router with the `.router` api, to create your server + const HttpLive = Layer.unwrapEffect( + Effect.gen(function* () { + return HttpServer.serve(yield* UserRouter.router, HttpMiddleware.logger) + }) + ).pipe( + Layer.provide(UserRouter.Live), + Layer.provide(AllRoutes), + Layer.provide(ServerLive) + ) + + BunRuntime.runMain(Layer.launch(HttpLive)) + ``` + +- Updated dependencies [[`66a1910`](https://github.com/Effect-TS/effect/commit/66a19109ff90c4252123b8809b8c8a74681dba6a)]: + - effect@3.4.1 + - @effect/schema@0.68.7 + +## 0.58.5 + +### Patch Changes + +- [#3053](https://github.com/Effect-TS/effect/pull/3053) [`37a07a2`](https://github.com/Effect-TS/effect/commit/37a07a2d8d1ce09ab965c0ada84a3fae9a6aba05) Thanks @tim-smart! - coerce primitive types in UrlParams input + +## 0.58.4 + +### Patch Changes + +- [#3051](https://github.com/Effect-TS/effect/pull/3051) [`b77fb0a`](https://github.com/Effect-TS/effect/commit/b77fb0a811ec1ad0e794917077c9a90824515db8) Thanks @tim-smart! - add HttpMiddleware.cors + +## 0.58.3 + +### Patch Changes + +- Updated dependencies [[`530fa9e`](https://github.com/Effect-TS/effect/commit/530fa9e36b8532589b948fc4faa37593f36b7f42)]: + - @effect/schema@0.68.6 + +## 0.58.2 + +### Patch Changes + +- Updated dependencies [[`1d62815`](https://github.com/Effect-TS/effect/commit/1d62815a50f34115606940ffa397442d75a20c81)]: + - @effect/schema@0.68.5 + +## 0.58.1 + +### Patch Changes + +- [#3036](https://github.com/Effect-TS/effect/pull/3036) [`5a248aa`](https://github.com/Effect-TS/effect/commit/5a248aa5ab2db3f7131ebc79bb9871a76de57973) Thanks @tim-smart! - add Socket.fromTransformStream + +## 0.58.0 + +### Minor Changes + +- [#2938](https://github.com/Effect-TS/effect/pull/2938) [`63dd0c3`](https://github.com/Effect-TS/effect/commit/63dd0c3af45876c1caad7d03356c74daf551c628) Thanks @tim-smart! - restructure platform http to use flattened modules + + Instead of using the previous re-exports, you now use the modules directly. + + Before: + + ```ts + import { HttpClient } from "@effect/platform" + + HttpClient.request.get("/").pipe(HttpClient.client.fetchOk) + ``` + + After: + + ```ts + import { HttpClient, HttpClientRequest } from "@effect/platform" + + HttpClientRequest.get("/").pipe(HttpClient.fetchOk) + ``` + +### Patch Changes + +- Updated dependencies [[`c0ce180`](https://github.com/Effect-TS/effect/commit/c0ce180861ad0938053c0e6145e813fa6404df3b), [`61707b6`](https://github.com/Effect-TS/effect/commit/61707b6ffc7397c2ba0dce22512b44955724f60f), [`9c1b5b3`](https://github.com/Effect-TS/effect/commit/9c1b5b39e6c19604ce834f072a114ad392c50a06), [`a35faf8`](https://github.com/Effect-TS/effect/commit/a35faf8d116f94899bfc03feab33b004c8ddfdf7), [`ff73c0c`](https://github.com/Effect-TS/effect/commit/ff73c0cacd66132bfad2e5211b3eae347729c667), [`984d516`](https://github.com/Effect-TS/effect/commit/984d516ccd9412dc41188f6a46b748dd20dd5848), [`8c3b8a2`](https://github.com/Effect-TS/effect/commit/8c3b8a2ce208eab753b6206a51605a424f104e98), [`017e2f9`](https://github.com/Effect-TS/effect/commit/017e2f9b371ce24ea4945e5d7390c934ad3c39cf), [`91bf8a2`](https://github.com/Effect-TS/effect/commit/91bf8a2e9d1959393b3cf7366cc1d584d3e666b7), [`c6a4a26`](https://github.com/Effect-TS/effect/commit/c6a4a266606575fd2c7165940c4072ad4c57d01f)]: + - effect@3.4.0 + - @effect/schema@0.68.4 + +## 0.57.8 + +### Patch Changes + +- [#3030](https://github.com/Effect-TS/effect/pull/3030) [`3ba7ea1`](https://github.com/Effect-TS/effect/commit/3ba7ea1c3c2923e85bf2f17e41176f8f8796d203) Thanks @tim-smart! - update find-my-way-ts & multipasta + +## 0.57.7 + +### Patch Changes + +- Updated dependencies [[`d473800`](https://github.com/Effect-TS/effect/commit/d47380012c3241d7287b66968d33a2414275ce7b)]: + - @effect/schema@0.68.3 + +## 0.57.6 + +### Patch Changes + +- Updated dependencies [[`eb341b3`](https://github.com/Effect-TS/effect/commit/eb341b3eb34ad64499371bc08b7f59e429979d8a)]: + - @effect/schema@0.68.2 + +## 0.57.5 + +### Patch Changes + +- [#3021](https://github.com/Effect-TS/effect/pull/3021) [`b8ea6aa`](https://github.com/Effect-TS/effect/commit/b8ea6aa479006358042b4256ee0a1c5cfbe57acb) Thanks @tim-smart! - update find-my-way-ts to fix vercel edge support + +## 0.57.4 + +### Patch Changes + +- Updated dependencies [[`b51e266`](https://github.com/Effect-TS/effect/commit/b51e26662b879b55d2c5164b7c97742739aa9446), [`6c89408`](https://github.com/Effect-TS/effect/commit/6c89408cd7b9204ec4c5828a46cd5312d8afb5e7)]: + - @effect/schema@0.68.1 + - effect@3.3.5 + +## 0.57.3 + +### Patch Changes + +- Updated dependencies [[`f6c7977`](https://github.com/Effect-TS/effect/commit/f6c79772e632c440b7e5221bb75f0ef9d3c3b005), [`a67b8fe`](https://github.com/Effect-TS/effect/commit/a67b8fe2ace08419424811b5f0d9a5378eaea352)]: + - @effect/schema@0.68.0 + - effect@3.3.4 + +## 0.57.2 + +### Patch Changes + +- Updated dependencies [[`3b15e1b`](https://github.com/Effect-TS/effect/commit/3b15e1b505c0b0e62a03b4a3605d42a9932cc99c), [`06ede85`](https://github.com/Effect-TS/effect/commit/06ede85d6e84710e6622463be95ff3927fb30dad), [`3a750b2`](https://github.com/Effect-TS/effect/commit/3a750b25b1ed92094a7f7ebc332a6bcfb212871b), [`7204ca5`](https://github.com/Effect-TS/effect/commit/7204ca5761c2b1d27999a624db23aa10b6e0504d)]: + - @effect/schema@0.67.24 + - effect@3.3.3 + +## 0.57.1 + +### Patch Changes + +- [#2988](https://github.com/Effect-TS/effect/pull/2988) [`07e12ec`](https://github.com/Effect-TS/effect/commit/07e12ecdb0e20b9763bd9e9058e567a7c8862efc) Thanks @tim-smart! - refactor Socket to use do notation + +- Updated dependencies [[`2ee4f2b`](https://github.com/Effect-TS/effect/commit/2ee4f2be7fd63074a9cbac6dcdfb533b6683533a), [`3572646`](https://github.com/Effect-TS/effect/commit/3572646d5e0804f85bc7f64633fb95722533f9dd), [`1aed347`](https://github.com/Effect-TS/effect/commit/1aed347a125ed3847ec90863424810d6759cbc85), [`df4bf4b`](https://github.com/Effect-TS/effect/commit/df4bf4b62e7b316c6647da0271fc5544a84e7ba2), [`f085f92`](https://github.com/Effect-TS/effect/commit/f085f92dfa204afb41823ffc27d437225137643d), [`9b3b4ac`](https://github.com/Effect-TS/effect/commit/9b3b4ac639d98aae33883926bece1e31fa280d22)]: + - @effect/schema@0.67.23 + - effect@3.3.2 + +## 0.57.0 + +### Minor Changes + +- [#2966](https://github.com/Effect-TS/effect/pull/2966) [`4d3fbe8`](https://github.com/Effect-TS/effect/commit/4d3fbe82e8cec13ccd0cd0b2096deac6818fb59a) Thanks @tim-smart! - fix KeyValueStore for react native by making constructors lazy + +### Patch Changes + +- Updated dependencies [[`eb98c5b`](https://github.com/Effect-TS/effect/commit/eb98c5b79ab50aa0cde239bd4e660dd19dbab612), [`184fed8`](https://github.com/Effect-TS/effect/commit/184fed83ac36cba05a75a5a8013f740f9f696e3b), [`6068e07`](https://github.com/Effect-TS/effect/commit/6068e073d4cc8b3c8583583fd5eb3efe43f7d5ba), [`3a77e20`](https://github.com/Effect-TS/effect/commit/3a77e209783933bac3aaddba1b05ff6a9ac72b36), [`d79ca17`](https://github.com/Effect-TS/effect/commit/d79ca17d9fa432571c69714776cab5cf8fef9c34)]: + - effect@3.3.1 + - @effect/schema@0.67.22 + +## 0.56.0 + +### Minor Changes + +- [#2837](https://github.com/Effect-TS/effect/pull/2837) [`2b9ddfc`](https://github.com/Effect-TS/effect/commit/2b9ddfcbac505d98551e764a43923854907ca5c1) Thanks @tim-smart! - support new Pool options in /platform WorkerPool + +- [#2837](https://github.com/Effect-TS/effect/pull/2837) [`188f0a5`](https://github.com/Effect-TS/effect/commit/188f0a5c57ed0d7c9e5852e0c1c998f1b95810a1) Thanks @tim-smart! - parse URL instances when creating client requests + +### Patch Changes + +- Updated dependencies [[`1f4ac00`](https://github.com/Effect-TS/effect/commit/1f4ac00a91c336c9c9c9b8c3ed9ceb9920ebc9bd), [`9305b76`](https://github.com/Effect-TS/effect/commit/9305b764cceeae4f16564435ae7172f79c2bf822), [`0f40d98`](https://github.com/Effect-TS/effect/commit/0f40d989da10f68df3ecd72b36849401ad679bfb), [`b761ef0`](https://github.com/Effect-TS/effect/commit/b761ef00eaf6c67b7ffe34798b98aae5347ab376), [`b53f69b`](https://github.com/Effect-TS/effect/commit/b53f69bff1452a487b21198cd83961f844e02d36), [`0f40d98`](https://github.com/Effect-TS/effect/commit/0f40d989da10f68df3ecd72b36849401ad679bfb), [`5bd549e`](https://github.com/Effect-TS/effect/commit/5bd549e4bd7144727db438ecca6b8dc9b3ef7e22), [`67f160a`](https://github.com/Effect-TS/effect/commit/67f160a213de0219a565d4bf653b3cbf24f58e8f), [`67f160a`](https://github.com/Effect-TS/effect/commit/67f160a213de0219a565d4bf653b3cbf24f58e8f)]: + - effect@3.3.0 + - @effect/schema@0.67.21 + +## 0.55.7 + +### Patch Changes + +- [#2931](https://github.com/Effect-TS/effect/pull/2931) [`a67d602`](https://github.com/Effect-TS/effect/commit/a67d60276f96cd20b76145b4cee13efca6c6158a) Thanks @tim-smart! - ensure pre-response handler is checked after running the user-provided http app + +- Updated dependencies [[`4c6bc7f`](https://github.com/Effect-TS/effect/commit/4c6bc7f190c142dc9db70b365a2bf30715a98e62)]: + - @effect/schema@0.67.20 + +## 0.55.6 + +### Patch Changes + +- [#2903](https://github.com/Effect-TS/effect/pull/2903) [`799aa20`](https://github.com/Effect-TS/effect/commit/799aa20b4f618736ba33a5297fda90a75d4c26c6) Thanks @rocwang! - # Make baseUrl() more defensive in @effect/platform + + Sometimes, third party code may patch a missing global `location` to accommodate for non-browser JavaScript + runtimes, e.g. Cloudflare Workers, + Deno. [Such patch](https://github.com/jamsinclair/jSquash/pull/21/files#diff-322ca97cdcdd0d3b85c20a7d5cac703a2f9f3766fc762f98b9f6a9d4c5063ca3R21-R23) + might not yield a fully valid `location`. This could + break `baseUrl()`, which is called by `makeUrl()`. + + For example, the following code would log `Invalid URL: '/api/v1/users' with base 'NaN'`. + + ```js + import { makeUrl } from "@effect/platform/Http/UrlParams" + + globalThis.location = { href: "" } + + const url = makeUrl("/api/v1/users", []) + + // This would log "Invalid URL: '/api/v1/users' with base 'NaN'", + // because location.origin + location.pathname return NaN in baseUrl() + console.log(url.left.message) + ``` + + Arguably, this is not an issue of Effect per se, but it's better to be defensive and handle such cases gracefully. + So this change does that by checking if `location.orign` and `location.pathname` are available before accessing them. + +- Updated dependencies [[`8c5d280`](https://github.com/Effect-TS/effect/commit/8c5d280c0402284a4e58372867a15a431cb99461), [`6ba6d26`](https://github.com/Effect-TS/effect/commit/6ba6d269f5891e6b11aa35c5281dde4bf3273004), [`cd7496b`](https://github.com/Effect-TS/effect/commit/cd7496ba214eabac2e3c297f513fcbd5b11f0e91), [`3f28bf2`](https://github.com/Effect-TS/effect/commit/3f28bf274333611906175446b772243f34f1b6d5), [`5817820`](https://github.com/Effect-TS/effect/commit/58178204a770d1a78c06945ef438f9fffbb50afa), [`349a036`](https://github.com/Effect-TS/effect/commit/349a036ffb08351481c060655660a6ccf26473de)]: + - effect@3.2.9 + - @effect/schema@0.67.19 + +## 0.55.5 + +### Patch Changes + +- Updated dependencies [[`a0dd1c1`](https://github.com/Effect-TS/effect/commit/a0dd1c1ede2a1e856ecb0e67826ec992016fef97)]: + - @effect/schema@0.67.18 + +## 0.55.4 + +### Patch Changes + +- Updated dependencies [[`d9d22e7`](https://github.com/Effect-TS/effect/commit/d9d22e7c4d5e31d5b46644c729b027796e467c16), [`3c080f7`](https://github.com/Effect-TS/effect/commit/3c080f74b2e2290edb6143c3aa01026e57f87a2a), [`3c080f7`](https://github.com/Effect-TS/effect/commit/3c080f74b2e2290edb6143c3aa01026e57f87a2a), [`7d6d875`](https://github.com/Effect-TS/effect/commit/7d6d8750077d9c8379f37240745240d7f3b7a4f8), [`70cda70`](https://github.com/Effect-TS/effect/commit/70cda704e8e31c80737b95121c8199e726ea132f), [`fb91f17`](https://github.com/Effect-TS/effect/commit/fb91f17098b48497feca9ec976feb87e4a82451b)]: + - @effect/schema@0.67.17 + - effect@3.2.8 + +## 0.55.3 + +### Patch Changes + +- Updated dependencies [[`5745886`](https://github.com/Effect-TS/effect/commit/57458869859943410221ccc87f8cecfba7c79d92), [`6801fca`](https://github.com/Effect-TS/effect/commit/6801fca44366be3ee1b6b99f54bd4f38a1b5e4f4)]: + - @effect/schema@0.67.16 + - effect@3.2.7 + +## 0.55.2 + +### Patch Changes + +- [#2737](https://github.com/Effect-TS/effect/pull/2737) [`2c2280b`](https://github.com/Effect-TS/effect/commit/2c2280b98a11fc002663c55792a4fa5781cd5fb6) Thanks @jessekelly881! - added KeyValueStore.layerStorage to wrap instances of the `Storage` type. + +- Updated dependencies [[`e2740fc`](https://github.com/Effect-TS/effect/commit/e2740fc4e212ba85a90541e8c8d85b0bcd5c2e7c), [`cc8ac50`](https://github.com/Effect-TS/effect/commit/cc8ac5080daba8622ca2ff5dab5c37ddfab732ba), [`60fe3d5`](https://github.com/Effect-TS/effect/commit/60fe3d5fb2be168dd35c6d0cb8ac8f55deb30fc0)]: + - @effect/schema@0.67.15 + - effect@3.2.6 + +## 0.55.1 + +### Patch Changes + +- Updated dependencies [[`c5846e9`](https://github.com/Effect-TS/effect/commit/c5846e99137e9eb02efd31865e26f49f0d2c7c03)]: + - @effect/schema@0.67.14 + +## 0.55.0 + +### Minor Changes + +- [#2835](https://github.com/Effect-TS/effect/pull/2835) [`5133ca9`](https://github.com/Effect-TS/effect/commit/5133ca9dc4b8da0e28951316da9ab55dfbe0fbb9) Thanks @tim-smart! - remove pool resizing in platform workers to enable concurrent access + +### Patch Changes + +- Updated dependencies [[`608b01f`](https://github.com/Effect-TS/effect/commit/608b01fc342dbae2a642b308a67b84ead530ecea), [`031c712`](https://github.com/Effect-TS/effect/commit/031c7122a24ac42e48d6a434646b4f5d279d7442), [`a44e532`](https://github.com/Effect-TS/effect/commit/a44e532cf3a6a498b12a5aacf8124aa267e24ba0)]: + - effect@3.2.5 + - @effect/schema@0.67.13 + +## 0.54.0 + +### Minor Changes + +- [#2801](https://github.com/Effect-TS/effect/pull/2801) [`1af94df`](https://github.com/Effect-TS/effect/commit/1af94df6b74aeb4f6ebcbe80e074b4cb252e62e3) Thanks @tim-smart! - remove `permits` from workers, to prevent issues with pool resizing + +- [#2801](https://github.com/Effect-TS/effect/pull/2801) [`c07e0ce`](https://github.com/Effect-TS/effect/commit/c07e0cea8ce165887e2c9dfa5d669eba9b2fb798) Thanks @gcanti! - Revise the ordering of type parameters within the `SchemaStore` interface to enhance consistency + +### Patch Changes + +- [#2801](https://github.com/Effect-TS/effect/pull/2801) [`1af94df`](https://github.com/Effect-TS/effect/commit/1af94df6b74aeb4f6ebcbe80e074b4cb252e62e3) Thanks @tim-smart! - ensure worker pool construction errors are reported during creation + +- Updated dependencies [[`1af94df`](https://github.com/Effect-TS/effect/commit/1af94df6b74aeb4f6ebcbe80e074b4cb252e62e3), [`f8038ca`](https://github.com/Effect-TS/effect/commit/f8038cadd5f50d397469e5fdbc70dd8f69671f50), [`e376641`](https://github.com/Effect-TS/effect/commit/e3766411b60ebb45d31e9c9d94efa099121d4d58), [`e313a01`](https://github.com/Effect-TS/effect/commit/e313a01b7e80f6cb7704055a190e5623c9d22c6d)]: + - effect@3.2.4 + - @effect/schema@0.67.12 + +## 0.53.14 + +### Patch Changes + +- Updated dependencies [[`5af633e`](https://github.com/Effect-TS/effect/commit/5af633eb5ff6560a64d87263d1692bb9c75f7b3c), [`45578e8`](https://github.com/Effect-TS/effect/commit/45578e8faa80ae33d23e08f6f19467f818b7788f)]: + - @effect/schema@0.67.11 + - effect@3.2.3 + +## 0.53.13 + +### Patch Changes + +- [#2784](https://github.com/Effect-TS/effect/pull/2784) [`c1eaef9`](https://github.com/Effect-TS/effect/commit/c1eaef910420dae416923d172ee58d219e921d0f) Thanks @gcanti! - Update the definition of `Handler` to utilize `App.Default` + +- Updated dependencies [[`5d9266e`](https://github.com/Effect-TS/effect/commit/5d9266e8c740746ac9e186c3df6090a1b57fbe2a), [`9f8122e`](https://github.com/Effect-TS/effect/commit/9f8122e78884ab47c5e5f364d86eee1d1543cc61), [`6a6f670`](https://github.com/Effect-TS/effect/commit/6a6f6706b8613c8c7c10971b8d81a0f9e440a6f2), [`78ffc27`](https://github.com/Effect-TS/effect/commit/78ffc27ee3fa708433c25fa118c53d38d90d08bc)]: + - effect@3.2.2 + - @effect/schema@0.67.10 + +## 0.53.12 + +### Patch Changes + +- Updated dependencies [[`5432fff`](https://github.com/Effect-TS/effect/commit/5432fff7c9a69d43910426c1053ebfc3b73ebed6)]: + - @effect/schema@0.67.9 + +## 0.53.11 + +### Patch Changes + +- Updated dependencies [[`c1e991d`](https://github.com/Effect-TS/effect/commit/c1e991dd5ba87901cd0e05697a8b4a267e7e954a)]: + - effect@3.2.1 + - @effect/schema@0.67.8 + +## 0.53.10 + +### Patch Changes + +- [#2778](https://github.com/Effect-TS/effect/pull/2778) [`146cadd`](https://github.com/Effect-TS/effect/commit/146cadd9d004634a3ff85c480bf92cf975c853e2) Thanks [@tim-smart](https://github.com/tim-smart)! - Run client request stream with a current runtime. + +- [#2778](https://github.com/Effect-TS/effect/pull/2778) [`7135748`](https://github.com/Effect-TS/effect/commit/713574813a0f64085db0b5240ba39e7a0a7c137e) Thanks [@tim-smart](https://github.com/tim-smart)! - capture stack trace for tracing spans + +- Updated dependencies [[`146cadd`](https://github.com/Effect-TS/effect/commit/146cadd9d004634a3ff85c480bf92cf975c853e2), [`7135748`](https://github.com/Effect-TS/effect/commit/713574813a0f64085db0b5240ba39e7a0a7c137e), [`7135748`](https://github.com/Effect-TS/effect/commit/713574813a0f64085db0b5240ba39e7a0a7c137e), [`963b4e7`](https://github.com/Effect-TS/effect/commit/963b4e7ac87e2468feb6a344f7ab4ee4ad711198), [`64c9414`](https://github.com/Effect-TS/effect/commit/64c9414e960e82058ca09bbb3976d6fbef303a8e), [`7135748`](https://github.com/Effect-TS/effect/commit/713574813a0f64085db0b5240ba39e7a0a7c137e), [`2cbb76b`](https://github.com/Effect-TS/effect/commit/2cbb76bb52500a3f4bf27d1c91482518cbea56d7), [`870c5fa`](https://github.com/Effect-TS/effect/commit/870c5fa52cd61e745e8e828d38c3f09f00737553), [`7135748`](https://github.com/Effect-TS/effect/commit/713574813a0f64085db0b5240ba39e7a0a7c137e), [`64c9414`](https://github.com/Effect-TS/effect/commit/64c9414e960e82058ca09bbb3976d6fbef303a8e)]: + - effect@3.2.0 + - @effect/schema@0.67.7 + +## 0.53.9 + +### Patch Changes + +- [#2761](https://github.com/Effect-TS/effect/pull/2761) [`17fc22e`](https://github.com/Effect-TS/effect/commit/17fc22e132593c5caa563705a4748ba0f04a853c) Thanks [@KhraksMamtsov](https://github.com/KhraksMamtsov)! - Add `{ once: true }` to all `"abort"` event listeners for `AbortController` to automatically remove handlers after execution + +- Updated dependencies [[`17da864`](https://github.com/Effect-TS/effect/commit/17da864e4a6f80becdb82db7dece2ba583bfdda3), [`17fc22e`](https://github.com/Effect-TS/effect/commit/17fc22e132593c5caa563705a4748ba0f04a853c), [`810f222`](https://github.com/Effect-TS/effect/commit/810f222268792b13067c7a7bf317b93a9bb8917b), [`596aaea`](https://github.com/Effect-TS/effect/commit/596aaea022648b2e06fb1ec22f1652043d6fe64e), [`ff0efa0`](https://github.com/Effect-TS/effect/commit/ff0efa0a1415a41d4a4312a16cf7a63def86db3f)]: + - @effect/schema@0.67.6 + - effect@3.1.6 + +## 0.53.8 + +### Patch Changes + +- Updated dependencies [[`9c514de`](https://github.com/Effect-TS/effect/commit/9c514de28152696edff008324d2d7e67d55afd56)]: + - @effect/schema@0.67.5 + +## 0.53.7 + +### Patch Changes + +- Updated dependencies [[`ee08593`](https://github.com/Effect-TS/effect/commit/ee0859398ecc2589cab0d017bef6a17e00c34dfd), [`da6d7d8`](https://github.com/Effect-TS/effect/commit/da6d7d845246e9d04631d64fa7694944b6010d09)]: + - @effect/schema@0.67.4 + +## 0.53.6 + +### Patch Changes + +- [#2750](https://github.com/Effect-TS/effect/pull/2750) [`6ac4847`](https://github.com/Effect-TS/effect/commit/6ac48479447c01a4f35d655552af93e47e562610) Thanks [@tim-smart](https://github.com/tim-smart)! - fix memory leak in Socket's + +- Updated dependencies [[`6ac4847`](https://github.com/Effect-TS/effect/commit/6ac48479447c01a4f35d655552af93e47e562610)]: + - effect@3.1.5 + - @effect/schema@0.67.3 + +## 0.53.5 + +### Patch Changes + +- Updated dependencies [[`89a3afb`](https://github.com/Effect-TS/effect/commit/89a3afbe191c83b84b17bfaa95519aff0749afbe), [`992c8e2`](https://github.com/Effect-TS/effect/commit/992c8e21535db9f0c66e81d32fee8af56a96274f)]: + - @effect/schema@0.67.2 + +## 0.53.4 + +### Patch Changes + +- Updated dependencies [[`e41e911`](https://github.com/Effect-TS/effect/commit/e41e91122fa6dd12fc81e50dcad0db891be67146)]: + - effect@3.1.4 + - @effect/schema@0.67.1 + +## 0.53.3 + +### Patch Changes + +- Updated dependencies [[`d7e4997`](https://github.com/Effect-TS/effect/commit/d7e49971fe97b7ee5fb7991f3f5ac4d627a26338)]: + - @effect/schema@0.67.0 + +## 0.53.2 + +### Patch Changes + +- Updated dependencies [[`1f6dc96`](https://github.com/Effect-TS/effect/commit/1f6dc96f51c7bb9c8d11415358308604ba7c7c8e)]: + - effect@3.1.3 + - @effect/schema@0.66.16 + +## 0.53.1 + +### Patch Changes + +- Updated dependencies [[`121d6d9`](https://github.com/Effect-TS/effect/commit/121d6d93755138c7510ba3ab4f0019ec0cb91890)]: + - @effect/schema@0.66.15 + +## 0.53.0 + +### Minor Changes + +- [#2703](https://github.com/Effect-TS/effect/pull/2703) [`d57fbbb`](https://github.com/Effect-TS/effect/commit/d57fbbbd6c466936213a671fc3cd2390064f864e) Thanks [@tim-smart](https://github.com/tim-smart)! - replace isows with WebSocketConstructor service in @effect/platform/Socket + + You now have to provide a WebSocketConstructor implementation to the `Socket.makeWebSocket` api. + + ```ts + import * as Socket from "@effect/platform/Socket" + import * as NodeSocket from "@effect/platform-node/NodeSocket" + import { Effect } from "effect" + + Socket.makeWebSocket("ws://localhost:8080").pipe( + Effect.provide(NodeSocket.layerWebSocketConstructor) // use "ws" npm package + ) + ``` + +## 0.52.3 + +### Patch Changes + +- [#2698](https://github.com/Effect-TS/effect/pull/2698) [`5866c62`](https://github.com/Effect-TS/effect/commit/5866c621d7eb4cc84e4ba972bfdfd219734cd45d) Thanks [@tim-smart](https://github.com/tim-smart)! - fix http ServerResponse cookie apis + +## 0.52.2 + +### Patch Changes + +- [#2679](https://github.com/Effect-TS/effect/pull/2679) [`2e1cdf6`](https://github.com/Effect-TS/effect/commit/2e1cdf67d141281288fffe9a5c10d1379a800513) Thanks [@tim-smart](https://github.com/tim-smart)! - ensure all type ids are annotated with `unique symbol` + +- Updated dependencies [[`2e1cdf6`](https://github.com/Effect-TS/effect/commit/2e1cdf67d141281288fffe9a5c10d1379a800513)]: + - effect@3.1.2 + - @effect/schema@0.66.14 + +## 0.52.1 + +### Patch Changes + +- Updated dependencies [[`e5e56d1`](https://github.com/Effect-TS/effect/commit/e5e56d138dbed3204636f605229c6685f89659fc)]: + - effect@3.1.1 + - @effect/schema@0.66.13 + +## 0.52.0 + +### Minor Changes + +- [#2669](https://github.com/Effect-TS/effect/pull/2669) [`9deab0a`](https://github.com/Effect-TS/effect/commit/9deab0aec9e99501f9441843e34df9afa10c5be9) Thanks [@tim-smart](https://github.com/tim-smart)! - move http search params apis to ServerRequest module + + If you want to access the search params for a request, you can now use the `Http.request.ParsedSearchParams` tag. + + ```ts + import * as Http from "@effect/platform/HttpServer" + import { Effect } from "effect" + + Effect.gen(function* () { + const searchParams = yield* Http.request.ParsedSearchParams + console.log(searchParams) + }) + ``` + + The schema method has also been moved to the `ServerRequest` module. It is now available as `Http.request.schemaSearchParams`. + +### Patch Changes + +- [#2672](https://github.com/Effect-TS/effect/pull/2672) [`7719b8a`](https://github.com/Effect-TS/effect/commit/7719b8a7350c14e952ffe685bfd5308773b3e271) Thanks [@tim-smart](https://github.com/tim-smart)! - allow http client trace propagation to be controlled + + To disable trace propagation: + + ```ts + import { HttpClient as Http } from "@effect/platform" + + Http.request + .get("https://example.com") + .pipe(Http.client.fetchOk, Http.client.withTracerPropagation(false)) + ``` + +## 0.51.0 + +### Minor Changes + +- [#2543](https://github.com/Effect-TS/effect/pull/2543) [`0ec93cb`](https://github.com/Effect-TS/effect/commit/0ec93cb4f166e7401c171c2f8e8276ce958d9a57) Thanks [@github-actions](https://github.com/apps/github-actions)! - \* capitalised Http.multipart.FileSchema and Http.multipart.FilesSchema + - exported Http.multipart.FileSchema + - added Http.multipart.SingleFileSchema + +### Patch Changes + +- [#2543](https://github.com/Effect-TS/effect/pull/2543) [`a023f28`](https://github.com/Effect-TS/effect/commit/a023f28336f3865687d9a30c1883e36909906d85) Thanks [@github-actions](https://github.com/apps/github-actions)! - set span `kind` where applicable + +- Updated dependencies [[`c3c12c6`](https://github.com/Effect-TS/effect/commit/c3c12c6625633fe80e79f9db75a3b8cf8ca8b11d), [`ba64ea6`](https://github.com/Effect-TS/effect/commit/ba64ea6757810c5e74cad3863a7d19d4d38af66b), [`b5de2d2`](https://github.com/Effect-TS/effect/commit/b5de2d2ce5b1afe8be90827bf898a95cec40eb2b), [`a1c7ab8`](https://github.com/Effect-TS/effect/commit/a1c7ab8ffedacd18c1fc784f4ff5844f79498b83), [`a023f28`](https://github.com/Effect-TS/effect/commit/a023f28336f3865687d9a30c1883e36909906d85), [`1c9454d`](https://github.com/Effect-TS/effect/commit/1c9454d532eae79b9f759aea77f59332cc6d18ed), [`92d56db`](https://github.com/Effect-TS/effect/commit/92d56dbb3f33e36636c2a2f1030c56492e39cf4d)]: + - effect@3.1.0 + - @effect/schema@0.66.12 + +## 0.50.8 + +### Patch Changes + +- [#2650](https://github.com/Effect-TS/effect/pull/2650) [`16039a0`](https://github.com/Effect-TS/effect/commit/16039a08f04f11545e2fdf40952788a8f9cef04f) Thanks [@tim-smart](https://github.com/tim-smart)! - improve error messages for Http.client.filterStatus\* + +- [#2648](https://github.com/Effect-TS/effect/pull/2648) [`d1d33e1`](https://github.com/Effect-TS/effect/commit/d1d33e10b25109f44b5ab1c6e4d778a59c0d3eeb) Thanks [@floydspace](https://github.com/floydspace)! - Fixed import path for type import. + +- Updated dependencies [[`557707b`](https://github.com/Effect-TS/effect/commit/557707bc9e5f230c8964d2757012075c34339b5c), [`f4ed306`](https://github.com/Effect-TS/effect/commit/f4ed3068a70b50302d078a30d18ca3cfd2bc679c), [`661004f`](https://github.com/Effect-TS/effect/commit/661004f4bf5f8b25f5a0678c21a3a822188ce461), [`e79cb83`](https://github.com/Effect-TS/effect/commit/e79cb83d3b19098bc40a3012e2a059b8426306c2)]: + - effect@3.0.8 + - @effect/schema@0.66.11 + +## 0.50.7 + +### Patch Changes + +- Updated dependencies [[`18de56b`](https://github.com/Effect-TS/effect/commit/18de56b4a6b6d1f99230dfabf9147d59ea4dd759)]: + - effect@3.0.7 + - @effect/schema@0.66.10 + +## 0.50.6 + +### Patch Changes + +- Updated dependencies [[`ffe4f4e`](https://github.com/Effect-TS/effect/commit/ffe4f4e95db35fff6869e360b072e3837befa0a1), [`027418e`](https://github.com/Effect-TS/effect/commit/027418edaa6aa6c0ae4861b95832827b45adace4), [`ac1898e`](https://github.com/Effect-TS/effect/commit/ac1898eb7bc96880f911c276048e2ea3d6fe9c50), [`ffe4f4e`](https://github.com/Effect-TS/effect/commit/ffe4f4e95db35fff6869e360b072e3837befa0a1), [`8206529`](https://github.com/Effect-TS/effect/commit/8206529d6a7bbf3e3c6f670afb0381e83176736e)]: + - effect@3.0.6 + - @effect/schema@0.66.9 + +## 0.50.5 + +### Patch Changes + +- Updated dependencies [[`6222404`](https://github.com/Effect-TS/effect/commit/62224044678751829ed2f128e05133a91c6b0569), [`868ed2a`](https://github.com/Effect-TS/effect/commit/868ed2a8fe94ee7f4206a6070f29dcf2a5ba1dc3)]: + - effect@3.0.5 + - @effect/schema@0.66.8 + +## 0.50.4 + +### Patch Changes + +- Updated dependencies [[`dd41c6c`](https://github.com/Effect-TS/effect/commit/dd41c6c725b1c1c980683275d8fa69779902187e), [`9a24667`](https://github.com/Effect-TS/effect/commit/9a246672008a2b668d43fbfd2fe5508c54b2b920)]: + - @effect/schema@0.66.7 + - effect@3.0.4 + +## 0.50.3 + +### Patch Changes + +- [#2589](https://github.com/Effect-TS/effect/pull/2589) [`b3b51a2`](https://github.com/Effect-TS/effect/commit/b3b51a2ea0c6ab92a363db46ebaa7e1176d089f5) Thanks [@tim-smart](https://github.com/tim-smart)! - redact some common sensitive http headers names in traces + +- Updated dependencies [[`9dfc156`](https://github.com/Effect-TS/effect/commit/9dfc156dc13fb4da9c777aae3acece4b5ecf0064), [`80271bd`](https://github.com/Effect-TS/effect/commit/80271bdc648e9efa659ce66b2c255754a6a1a8b0), [`e4ba97d`](https://github.com/Effect-TS/effect/commit/e4ba97d060c16bdf4e3b5bd5db6777f121a6768c)]: + - @effect/schema@0.66.6 + +## 0.50.2 + +### Patch Changes + +- Updated dependencies [[`b3fe829`](https://github.com/Effect-TS/effect/commit/b3fe829e8b12726afe94086b5375968f41a26411), [`a58b7de`](https://github.com/Effect-TS/effect/commit/a58b7deb8bb1d3b0dd636decf5d16f115f37eb72), [`d90e8c3`](https://github.com/Effect-TS/effect/commit/d90e8c3090cbc78e2bc7b51c974df66ffefacdfa)]: + - @effect/schema@0.66.5 + +## 0.50.1 + +### Patch Changes + +- Updated dependencies [[`773b8e0`](https://github.com/Effect-TS/effect/commit/773b8e01521e8fa7c38ff15d92d21d6fd6dad56f)]: + - @effect/schema@0.66.4 + +## 0.50.0 + +### Minor Changes + +- [#2567](https://github.com/Effect-TS/effect/pull/2567) [`6f38dff`](https://github.com/Effect-TS/effect/commit/6f38dff41ffa34532cc2f25b90446550c5730bb6) Thanks [@tim-smart](https://github.com/tim-smart)! - add URL & AbortSignal to Http.client.makeDefault + +### Patch Changes + +- [#2567](https://github.com/Effect-TS/effect/pull/2567) [`6f38dff`](https://github.com/Effect-TS/effect/commit/6f38dff41ffa34532cc2f25b90446550c5730bb6) Thanks [@tim-smart](https://github.com/tim-smart)! - add more span attributes to http traces + +- [#2565](https://github.com/Effect-TS/effect/pull/2565) [`a3b0e6c`](https://github.com/Effect-TS/effect/commit/a3b0e6c490772e6d44b5d98dcf2729c4d5310ecc) Thanks [@tim-smart](https://github.com/tim-smart)! - add Http.response.void helper, for creating a http request that returns void + +- Updated dependencies [[`a7b4b84`](https://github.com/Effect-TS/effect/commit/a7b4b84bd5a25f51aba922f9259c3a58c98c6a4e)]: + - effect@3.0.3 + - @effect/schema@0.66.3 + +## 0.49.4 + +### Patch Changes + +- [#2562](https://github.com/Effect-TS/effect/pull/2562) [`2cecdbd`](https://github.com/Effect-TS/effect/commit/2cecdbd1cf30befce4e84796ccd953ea55ecfb86) Thanks [@fubhy](https://github.com/fubhy)! - Added provenance publishing + +- Updated dependencies [[`2cecdbd`](https://github.com/Effect-TS/effect/commit/2cecdbd1cf30befce4e84796ccd953ea55ecfb86)]: + - effect@3.0.2 + - @effect/schema@0.66.2 + +## 0.49.3 + +### Patch Changes + +- [#2558](https://github.com/Effect-TS/effect/pull/2558) [`8d39d65`](https://github.com/Effect-TS/effect/commit/8d39d6554af548228ad767112ce2e0b1f68fa8e1) Thanks [@tim-smart](https://github.com/tim-smart)! - add no-op FileSystem constructor for testing + +## 0.49.2 + +### Patch Changes + +- [#2556](https://github.com/Effect-TS/effect/pull/2556) [`5ef0a1a`](https://github.com/Effect-TS/effect/commit/5ef0a1ae9b773fa2481550cb0d43ff7a0e03cd44) Thanks [@tim-smart](https://github.com/tim-smart)! - fix Command stdin being closed too early + +## 0.49.1 + +### Patch Changes + +- [#2542](https://github.com/Effect-TS/effect/pull/2542) [`87c5687`](https://github.com/Effect-TS/effect/commit/87c5687de0782dab177b7861217fa3b040046282) Thanks [@tim-smart](https://github.com/tim-smart)! - allow fs.watch backend to be customized + + If you want to use the @parcel/watcher backend, you now need to provide it to + your effects. + + ```ts + import { Layer } from "effect" + import { FileSystem } from "@effect/platform" + import { NodeFileSystem } from "@effect/platform-node" + import * as ParcelWatcher from "@effect/platform-node/NodeFileSystem/ParcelWatcher" + + // create a Layer that uses the ParcelWatcher backend + NodeFileSystem.layer.pipe(Layer.provide(ParcelWatcher.layer)) + ``` + +- [#2555](https://github.com/Effect-TS/effect/pull/2555) [`8edacca`](https://github.com/Effect-TS/effect/commit/8edacca37f8e37c01a63fec332b06d9361efaa7b) Thanks [@tim-smart](https://github.com/tim-smart)! - prevent use of `Array` as import name to solve bundler issues + +- Updated dependencies [[`3da0cfa`](https://github.com/Effect-TS/effect/commit/3da0cfa12c407fd930dc480be1ecc9217a8058f8), [`570e8d8`](https://github.com/Effect-TS/effect/commit/570e8d87e7c0e9ad4cd2686462fdb9b4812f7716), [`b2b5d66`](https://github.com/Effect-TS/effect/commit/b2b5d6626b18eb5289f364ffab5240e84b04d085), [`8edacca`](https://github.com/Effect-TS/effect/commit/8edacca37f8e37c01a63fec332b06d9361efaa7b)]: + - effect@3.0.1 + - @effect/schema@0.66.1 + +## 0.49.0 + +### Minor Changes + +- [#2207](https://github.com/Effect-TS/effect/pull/2207) [`cf69f46`](https://github.com/Effect-TS/effect/commit/cf69f46690058d71eeada03cfb40dc744573e9e4) Thanks [@github-actions](https://github.com/apps/github-actions)! - make Http.middleware.withTracerDisabledWhen a Layer api + + And add Http.middleware.withTracerDisabledWhenEffect to operate on Effect's. + + Usage is now: + + ```ts + import * as Http from "@effect/platform/HttpServer" + + Http.router.empty.pipe( + Http.router.get("/health"), + Http.server.serve(), + Http.middleware.withTracerDisabledWhen( + (request) => request.url === "/no-tracing" + ) + ) + ``` + +- [#2207](https://github.com/Effect-TS/effect/pull/2207) [`aa4a3b5`](https://github.com/Effect-TS/effect/commit/aa4a3b550da1c1020265ac389ed3f309388994a2) Thanks [@github-actions](https://github.com/apps/github-actions)! - Swap type parameters in /platform data types + + A codemod has been released to make migration easier: + + ``` + npx @effect/codemod platform-0.49 src/**/* + ``` + +- [#2207](https://github.com/Effect-TS/effect/pull/2207) [`6c6087a`](https://github.com/Effect-TS/effect/commit/6c6087a4a897b64252346426660782d31c13f769) Thanks [@github-actions](https://github.com/apps/github-actions)! - rename auto-scoped ClientResponse apis from *Effect to *Scoped + +- [#2207](https://github.com/Effect-TS/effect/pull/2207) [`5a2314b`](https://github.com/Effect-TS/effect/commit/5a2314b70ec79c2c02b51cef45a5ddec8327daa1) Thanks [@github-actions](https://github.com/apps/github-actions)! - replace use of `unit` terminology with `void` + + For all the data types. + + ```ts + Effect.unit // => Effect.void + Stream.unit // => Stream.void + + // etc + ``` + +- [#2207](https://github.com/Effect-TS/effect/pull/2207) [`6c6087a`](https://github.com/Effect-TS/effect/commit/6c6087a4a897b64252346426660782d31c13f769) Thanks [@github-actions](https://github.com/apps/github-actions)! - move fetch options to a FiberRef + + This change makes adjusting options to fetch more composable. You can now do: + + ```ts + import { pipe } from "effect" + import * as Http from "@effect/platform/HttpClient" + + pipe( + Http.request.get("https://example.com"), + Http.client.fetchOk, + Http.client.withFetchOptions({ credentials: "include" }), + Http.response.text + ) + ``` + +- [#2207](https://github.com/Effect-TS/effect/pull/2207) [`2fb7d9c`](https://github.com/Effect-TS/effect/commit/2fb7d9ca15037ff62a578bb9fe5732da5f4f317d) Thanks [@github-actions](https://github.com/apps/github-actions)! - Release Effect 3.0 🎉 + +### Patch Changes + +- [#2207](https://github.com/Effect-TS/effect/pull/2207) [`6460414`](https://github.com/Effect-TS/effect/commit/6460414351a45fb8e0a457c63f3653422efee766) Thanks [@github-actions](https://github.com/apps/github-actions)! - properly handle multiple ports in SharedWorker + +- [#2207](https://github.com/Effect-TS/effect/pull/2207) [`cf69f46`](https://github.com/Effect-TS/effect/commit/cf69f46690058d71eeada03cfb40dc744573e9e4) Thanks [@github-actions](https://github.com/apps/github-actions)! - add Http.middleware.withTracerDisabledForUrls + + Allows you to disable the http server tracer for the given urls: + + ```ts + import * as Http from "@effect/platform/HttpServer" + + Http.router.empty.pipe( + Http.router.get("/health"), + Http.server.serve(), + Http.middleware.withTracerDisabledForUrls(["/health"]) + ) + ``` + +- [#2529](https://github.com/Effect-TS/effect/pull/2529) [`78b767c`](https://github.com/Effect-TS/effect/commit/78b767c2b1625186e17131761a0edbac25d21850) Thanks [@fubhy](https://github.com/fubhy)! - Renamed `ReadonlyArray` and `ReadonlyRecord` modules for better discoverability. + +- [#2514](https://github.com/Effect-TS/effect/pull/2514) [`25d74f8`](https://github.com/Effect-TS/effect/commit/25d74f8c4d2dd4a9e5ec57ce2f20d36dedd25343) Thanks [@rocwang](https://github.com/rocwang)! - Fix UrlParams.makeUrl when globalThis.location is set to `undefined` + +- Updated dependencies [[`1b5f0c7`](https://github.com/Effect-TS/effect/commit/1b5f0c77e7fd477a0026071e82129a948227f4b3), [`d50a652`](https://github.com/Effect-TS/effect/commit/d50a652479f4d1d64f48da05c79fa847e6e51548), [`9aeae46`](https://github.com/Effect-TS/effect/commit/9aeae461fdf9265389cf3dfe4e428b037215ba5f), [`9a3bd47`](https://github.com/Effect-TS/effect/commit/9a3bd47ebd0750c7e498162734f6d21895de0cb2), [`e542371`](https://github.com/Effect-TS/effect/commit/e542371981f8b4b484979feaad8a25b1f45e2df0), [`be9d025`](https://github.com/Effect-TS/effect/commit/be9d025e42355260ace02dd135851a8935a4deba), [`78b767c`](https://github.com/Effect-TS/effect/commit/78b767c2b1625186e17131761a0edbac25d21850), [`1499974`](https://github.com/Effect-TS/effect/commit/14999741d2e19c1747f6a7e19d68977f6429cdb8), [`1b5f0c7`](https://github.com/Effect-TS/effect/commit/1b5f0c77e7fd477a0026071e82129a948227f4b3), [`5c2b561`](https://github.com/Effect-TS/effect/commit/5c2b5614f583b88784ed68126ae939832fb3c092), [`a18f594`](https://github.com/Effect-TS/effect/commit/a18f5948f1439a147232448b2c443472fda0eceb), [`1499974`](https://github.com/Effect-TS/effect/commit/14999741d2e19c1747f6a7e19d68977f6429cdb8), [`2f96d93`](https://github.com/Effect-TS/effect/commit/2f96d938b90f8c19377583279e3c7afd9b509c50), [`5a2314b`](https://github.com/Effect-TS/effect/commit/5a2314b70ec79c2c02b51cef45a5ddec8327daa1), [`271b79f`](https://github.com/Effect-TS/effect/commit/271b79fc0b66a6c11e07a8779ff8800493a7eac2), [`1b5f0c7`](https://github.com/Effect-TS/effect/commit/1b5f0c77e7fd477a0026071e82129a948227f4b3), [`2fb7d9c`](https://github.com/Effect-TS/effect/commit/2fb7d9ca15037ff62a578bb9fe5732da5f4f317d), [`53d1c2a`](https://github.com/Effect-TS/effect/commit/53d1c2a77559081fbb89667e343346375c6d6650), [`e7e1bbe`](https://github.com/Effect-TS/effect/commit/e7e1bbe68486fdf31c8f84b0880522d39adcaad3), [`10c169e`](https://github.com/Effect-TS/effect/commit/10c169eadc874e91b4defca3f467b4e6a50fd8f3), [`6424181`](https://github.com/Effect-TS/effect/commit/64241815fe6a939e91e6947253e7dceea1306aa8)]: + - effect@3.0.0 + - @effect/schema@0.66.0 + +## 0.48.29 + +### Patch Changes + +- [#2517](https://github.com/Effect-TS/effect/pull/2517) [`b79cc59`](https://github.com/Effect-TS/effect/commit/b79cc59dbe64b9a0a7742dc9100a9d36c8e46b72) Thanks [@tim-smart](https://github.com/tim-smart)! - add uninterruptible option to http routes, for marking a route as uninterruptible + +## 0.48.28 + +### Patch Changes + +- [#2515](https://github.com/Effect-TS/effect/pull/2515) [`d590094`](https://github.com/Effect-TS/effect/commit/d5900943489ec1e0891836aeafb5ce99fb9c75c7) Thanks [@tim-smart](https://github.com/tim-smart)! - add Http.router.uninterruptible, for marking a route as uninterruptible + +- Updated dependencies [[`0aee906`](https://github.com/Effect-TS/effect/commit/0aee906f034539344db6fbac08919de3e28eccde), [`41c8102`](https://github.com/Effect-TS/effect/commit/41c810228b1a50e4b41f19e735d7c62fe8d36871), [`4c37001`](https://github.com/Effect-TS/effect/commit/4c370013417e18c4f564818de1341a8fccb43b4c), [`776ef2b`](https://github.com/Effect-TS/effect/commit/776ef2bb66db9aa9f68b7beab14f6986f9c1288b), [`217147e`](https://github.com/Effect-TS/effect/commit/217147ea67c5c42c96f024775c41e5b070f81e4c), [`8a69b4e`](https://github.com/Effect-TS/effect/commit/8a69b4ef6a3a06d2e21fe2e11a626038beefb4e1), [`90776ec`](https://github.com/Effect-TS/effect/commit/90776ec8e8671d835b65fc33ead1de6c864b81b9), [`b3acf47`](https://github.com/Effect-TS/effect/commit/b3acf47f9c9dfae1c99377aa906097aaa2d47d44), [`8709856`](https://github.com/Effect-TS/effect/commit/870985694ae985c3cb9360ad8a25c60e6f785f55), [`232c353`](https://github.com/Effect-TS/effect/commit/232c353c2e6f743f38e57639ee30e324ffa9c2a9), [`0d3231a`](https://github.com/Effect-TS/effect/commit/0d3231a195202635ecc0bf6bbf6a08fc017d0d69), [`0ca835c`](https://github.com/Effect-TS/effect/commit/0ca835cbac8e69072a93ace83b534219faba24e8), [`8709856`](https://github.com/Effect-TS/effect/commit/870985694ae985c3cb9360ad8a25c60e6f785f55), [`c22b019`](https://github.com/Effect-TS/effect/commit/c22b019e5eaf9d3a937a3d99cadbb8f8e9116a70), [`e983740`](https://github.com/Effect-TS/effect/commit/e9837401145605aff5bc2ec7e73004f397c5d2d1), [`e3e0924`](https://github.com/Effect-TS/effect/commit/e3e09247d46a35430fc60e4aa4032cc50814f212)]: + - @effect/schema@0.65.0 + - effect@2.4.19 + +## 0.48.27 + +### Patch Changes + +- [#2479](https://github.com/Effect-TS/effect/pull/2479) [`c6dd3c6`](https://github.com/Effect-TS/effect/commit/c6dd3c6909cafe05adc8450c5a499260e17e60d3) Thanks [@tim-smart](https://github.com/tim-smart)! - Make the file tree provider the fallback in PlatformConfigProvider.layerFileTreeAdd + +- [#2486](https://github.com/Effect-TS/effect/pull/2486) [`672f137`](https://github.com/Effect-TS/effect/commit/672f13747ddf6dac3ba304fd4511b1df44ab566d) Thanks [@tim-smart](https://github.com/tim-smart)! - accept string as a valid Socket input + +- [#2486](https://github.com/Effect-TS/effect/pull/2486) [`672f137`](https://github.com/Effect-TS/effect/commit/672f13747ddf6dac3ba304fd4511b1df44ab566d) Thanks [@tim-smart](https://github.com/tim-smart)! - add Socket.runRaw to handle strings directly + +- Updated dependencies [[`42b3651`](https://github.com/Effect-TS/effect/commit/42b36519f356bae9258a1ea1d416e2902b973e85)]: + - @effect/schema@0.64.20 + +## 0.48.26 + +### Patch Changes + +- [#2477](https://github.com/Effect-TS/effect/pull/2477) [`365a486`](https://github.com/Effect-TS/effect/commit/365a4865de5e47ce09f4cfd51fc0f67438f82a57) Thanks [@tim-smart](https://github.com/tim-smart)! - add PlatformConfigProvider module + + It contains a file tree provider, that can be used to read config values from a file tree. + + For example, if you have a file tree like this: + + ``` + config/ + secret + nested/ + value + ``` + + You could do the following: + + ```ts + import { PlatformConfigProvider } from "@effect/platform" + import { NodeContext } from "@effect/platform-node" + import { Config, Effect, Layer } from "effect" + + const ConfigProviderLive = PlatformConfigProvider.layerFileTree({ + rootDirectory: `/config` + }).pipe(Layer.provide(NodeContext.layer)) + + Effect.gen(function* (_) { + const secret = yield* _(Config.secret("secret")) + const value = yield* _(Config.string("value"), Config.nested("nested")) + }).pipe(Effect.provide(ConfigProviderLive)) + ``` + +## 0.48.25 + +### Patch Changes + +- [#2469](https://github.com/Effect-TS/effect/pull/2469) [`d209171`](https://github.com/Effect-TS/effect/commit/d2091714a786820ebae4bef04a9d67d25dd08e88) Thanks [@tim-smart](https://github.com/tim-smart)! - replace isomorphic-ws with isows + +- Updated dependencies [[`dadc690`](https://github.com/Effect-TS/effect/commit/dadc6906121c512bc32be22b52adbd1ada834594), [`58f66fe`](https://github.com/Effect-TS/effect/commit/58f66fecd4e646c6c8f10995df9faab17022eb8f), [`3cad21d`](https://github.com/Effect-TS/effect/commit/3cad21daa5d2332d33692498c87b7ffff979e304)]: + - effect@2.4.18 + - @effect/schema@0.64.19 + +## 0.48.24 + +### Patch Changes + +- [#2427](https://github.com/Effect-TS/effect/pull/2427) [`9c6a500`](https://github.com/Effect-TS/effect/commit/9c6a5001b467b6255c68a922f4b6e8d692b63d01) Thanks [@devmatteini](https://github.com/devmatteini)! - add force option to FileSystem.remove + +- [#2463](https://github.com/Effect-TS/effect/pull/2463) [`35ad0ba`](https://github.com/Effect-TS/effect/commit/35ad0ba9f3ba27c60453620e514b980f819f92af) Thanks [@tim-smart](https://github.com/tim-smart)! - fix exact optional properties type errors + +- Updated dependencies [[`8fdfda6`](https://github.com/Effect-TS/effect/commit/8fdfda6618be848c01b399d13bc05a9a3adfb613), [`607b2e7`](https://github.com/Effect-TS/effect/commit/607b2e7a7fd9318c57acf4e50ec61747eea74ad7), [`8fdfda6`](https://github.com/Effect-TS/effect/commit/8fdfda6618be848c01b399d13bc05a9a3adfb613), [`8206caf`](https://github.com/Effect-TS/effect/commit/8206caf7c2d22c68be4313318b61cfdacf6222b6), [`7ddd654`](https://github.com/Effect-TS/effect/commit/7ddd65415b65ccb654ad04f4dbefe39402f15117), [`7ddd654`](https://github.com/Effect-TS/effect/commit/7ddd65415b65ccb654ad04f4dbefe39402f15117), [`8fdfda6`](https://github.com/Effect-TS/effect/commit/8fdfda6618be848c01b399d13bc05a9a3adfb613), [`f456ba2`](https://github.com/Effect-TS/effect/commit/f456ba273bae21a6dcf8c966c50c97b5f0897d9f)]: + - effect@2.4.17 + - @effect/schema@0.64.18 + +## 0.48.23 + +### Patch Changes + +- [#2445](https://github.com/Effect-TS/effect/pull/2445) [`5170ce7`](https://github.com/Effect-TS/effect/commit/5170ce708c606283e8a30d273950f1a21c7eddc2) Thanks [@vecerek](https://github.com/vecerek)! - Add support for W3C Trace Context propagation + +- [#2454](https://github.com/Effect-TS/effect/pull/2454) [`63a1df2`](https://github.com/Effect-TS/effect/commit/63a1df2e4de3766f48f15676fbd0360ab9c27816) Thanks [@tim-smart](https://github.com/tim-smart)! - add support for binary data with XHR client + +- [#2450](https://github.com/Effect-TS/effect/pull/2450) [`74a5dae`](https://github.com/Effect-TS/effect/commit/74a5daed0e65b32a36e026bfcf66d02269cb967a) Thanks [@vecerek](https://github.com/vecerek)! - Platform: auto-instrument HTTP client + +- Updated dependencies [[`5170ce7`](https://github.com/Effect-TS/effect/commit/5170ce708c606283e8a30d273950f1a21c7eddc2), [`62a7f23`](https://github.com/Effect-TS/effect/commit/62a7f23937c0dfaca67a7b2f055b85cfde25ed11), [`7cc2b41`](https://github.com/Effect-TS/effect/commit/7cc2b41d6c551fdca2590b06681c5ad9832aba46), [`8b46fde`](https://github.com/Effect-TS/effect/commit/8b46fdebf2c075a74cd2cd29dfb69531d20fc154)]: + - effect@2.4.16 + - @effect/schema@0.64.17 + +## 0.48.22 + +### Patch Changes + +- Updated dependencies [[`a31917a`](https://github.com/Effect-TS/effect/commit/a31917aa4b05b1189b7a8e0bedb60bb3d49262ad), [`4cd2bed`](https://github.com/Effect-TS/effect/commit/4cd2bedf978f864bddd289d1c524c8e868bf587b), [`6cc6267`](https://github.com/Effect-TS/effect/commit/6cc6267026d9bfb1a9882cddf534787327e86ec1)]: + - @effect/schema@0.64.16 + +## 0.48.21 + +### Patch Changes + +- Updated dependencies [[`d7688c0`](https://github.com/Effect-TS/effect/commit/d7688c0c72717fe7876c871567f6946dabfc0546), [`b3a4fac`](https://github.com/Effect-TS/effect/commit/b3a4face2acaca422f0b0530436e8f13129f3b3a), [`5ded019`](https://github.com/Effect-TS/effect/commit/5ded019970169e3c1f2a375d0876b95fb1ff67f5)]: + - effect@2.4.15 + - @effect/schema@0.64.15 + +## 0.48.20 + +### Patch Changes + +- [#2413](https://github.com/Effect-TS/effect/pull/2413) [`4789083`](https://github.com/Effect-TS/effect/commit/4789083283bdaec456982d614ebc4a496ea0e7f7) Thanks [@tim-smart](https://github.com/tim-smart)! - make /platform ClientRequest implement Effect + + ClientRequest now implements `Effect` + + This makes it easier to quickly create a request and execute it in a single line. + + ```ts + import * as Http from "@effect/platform/HttpClient" + + Http.request + .get("https://jsonplaceholder.typicode.com/todos/1") + .pipe(Http.response.json) + ``` + +- [#2413](https://github.com/Effect-TS/effect/pull/2413) [`4789083`](https://github.com/Effect-TS/effect/commit/4789083283bdaec456982d614ebc4a496ea0e7f7) Thanks [@tim-smart](https://github.com/tim-smart)! - prevent unhandled errors in undici http client + +## 0.48.19 + +### Patch Changes + +- [#2411](https://github.com/Effect-TS/effect/pull/2411) [`fb7285e`](https://github.com/Effect-TS/effect/commit/fb7285e8d6a70527df7137a6a3efdd03ae61cb8b) Thanks [@tim-smart](https://github.com/tim-smart)! - fix broken imports in /platform + +## 0.48.18 + +### Patch Changes + +- [#2410](https://github.com/Effect-TS/effect/pull/2410) [`26435ec`](https://github.com/Effect-TS/effect/commit/26435ecfa06569dc18d1801ccf38213a43b7c334) Thanks [@tim-smart](https://github.com/tim-smart)! - add undici http client to @effect/platform-node + +- Updated dependencies [[`a76e5e1`](https://github.com/Effect-TS/effect/commit/a76e5e131a35c88a72771fb745df08f60fbc0e18), [`6180c0c`](https://github.com/Effect-TS/effect/commit/6180c0cc51dee785cfce72220a52c9fc3b9bf9aa)]: + - @effect/schema@0.64.14 + - effect@2.4.14 + +## 0.48.17 + +### Patch Changes + +- [#2400](https://github.com/Effect-TS/effect/pull/2400) [`47a8f1b`](https://github.com/Effect-TS/effect/commit/47a8f1b644d8294692d92cacd3c8c7543edbfabe) Thanks [@tim-smart](https://github.com/tim-smart)! - expose Schema ParseOptions in /platform schema apis + +- [#2403](https://github.com/Effect-TS/effect/pull/2403) [`8c9abe2`](https://github.com/Effect-TS/effect/commit/8c9abe2b35c46d8891d4b2c14ff9eb46302a14f3) Thanks [@tim-smart](https://github.com/tim-smart)! - use ReadonlyRecord for storing cookies + +- [#2403](https://github.com/Effect-TS/effect/pull/2403) [`8c9abe2`](https://github.com/Effect-TS/effect/commit/8c9abe2b35c46d8891d4b2c14ff9eb46302a14f3) Thanks [@tim-smart](https://github.com/tim-smart)! - add set-cookie headers in Http.response.toWeb + +- [#2400](https://github.com/Effect-TS/effect/pull/2400) [`47a8f1b`](https://github.com/Effect-TS/effect/commit/47a8f1b644d8294692d92cacd3c8c7543edbfabe) Thanks [@tim-smart](https://github.com/tim-smart)! - add .schemaJson / .schemaNoBody to http router apis + +- Updated dependencies [[`3336287`](https://github.com/Effect-TS/effect/commit/3336287ff55a25e56d759b83847bfaa21c40f499), [`54b7c00`](https://github.com/Effect-TS/effect/commit/54b7c0077fa784ad2646b812d6a44641f672edcd), [`3336287`](https://github.com/Effect-TS/effect/commit/3336287ff55a25e56d759b83847bfaa21c40f499)]: + - effect@2.4.13 + - @effect/schema@0.64.13 + +## 0.48.16 + +### Patch Changes + +- [#2387](https://github.com/Effect-TS/effect/pull/2387) [`75a8d16`](https://github.com/Effect-TS/effect/commit/75a8d16247cc14860cdd7fd948ef542c50c2d55e) Thanks [@tim-smart](https://github.com/tim-smart)! - add Cookies module to /platform http + + To add cookies to a http response: + + ```ts + import * as Http from "@effect/platform/HttpServer" + + Http.response.empty().pipe( + Http.response.setCookies([ + ["name", "value"], + ["foo", "bar", { httpOnly: true }] + ]) + ) + ``` + + You can also use cookies with the http client: + + ```ts + import * as Http from "@effect/platform/HttpClient" + import { Effect, Ref } from "effect" + + Effect.gen(function* (_) { + const ref = yield* _(Ref.make(Http.cookies.empty)) + const defaultClient = yield* _(Http.client.Client) + const clientWithCookies = defaultClient.pipe( + Http.client.withCookiesRef(ref), + Http.client.filterStatusOk + ) + + // cookies will be stored in the ref and sent in any subsequent requests + yield* _( + Http.request.get("https://www.google.com/"), + clientWithCookies, + Effect.scoped + ) + }) + ``` + +- [#2385](https://github.com/Effect-TS/effect/pull/2385) [`3307729`](https://github.com/Effect-TS/effect/commit/3307729de162a033fa9caa8e14c111013dcf0d87) Thanks [@tim-smart](https://github.com/tim-smart)! - update typescript to 5.4 + +- Updated dependencies [[`9392de6`](https://github.com/Effect-TS/effect/commit/9392de6baa6861662abc2bd3171897145f5ea073), [`3307729`](https://github.com/Effect-TS/effect/commit/3307729de162a033fa9caa8e14c111013dcf0d87), [`9392de6`](https://github.com/Effect-TS/effect/commit/9392de6baa6861662abc2bd3171897145f5ea073), [`3307729`](https://github.com/Effect-TS/effect/commit/3307729de162a033fa9caa8e14c111013dcf0d87), [`d17a427`](https://github.com/Effect-TS/effect/commit/d17a427c4427972fb55c45a058780716dc408631)]: + - @effect/schema@0.64.12 + - effect@2.4.12 + +## 0.48.15 + +### Patch Changes + +- Updated dependencies [[`2f488c4`](https://github.com/Effect-TS/effect/commit/2f488c436de52576562803c57ebc132ef40ccdd8), [`37ca592`](https://github.com/Effect-TS/effect/commit/37ca592a4101ad90adbf8c8b3f727faf3110cae5), [`317b5b8`](https://github.com/Effect-TS/effect/commit/317b5b8e8c8c2207469b3ebfcf72bf3a9f7cbc60)]: + - effect@2.4.11 + - @effect/schema@0.64.11 + +## 0.48.14 + +### Patch Changes + +- Updated dependencies [[`9bab1f9`](https://github.com/Effect-TS/effect/commit/9bab1f9fa5b999740755e4e82485cb77c638643a), [`9bbde5b`](https://github.com/Effect-TS/effect/commit/9bbde5be9a0168d1c2a0308bfc27167ed62f3968)]: + - effect@2.4.10 + - @effect/schema@0.64.10 + +## 0.48.13 + +### Patch Changes + +- Updated dependencies [[`dc7e497`](https://github.com/Effect-TS/effect/commit/dc7e49720df416870a7483f48adc40aeb23fe32d), [`ffaf7c3`](https://github.com/Effect-TS/effect/commit/ffaf7c36514f88496cdd2fdfdf0bc7ba5a2e5cd4)]: + - @effect/schema@0.64.9 + +## 0.48.12 + +### Patch Changes + +- Updated dependencies [[`e0af20e`](https://github.com/Effect-TS/effect/commit/e0af20ec5f6d0b19d66c5ebf610969d55bfc6c22)]: + - @effect/schema@0.64.8 + +## 0.48.11 + +### Patch Changes + +- [#2360](https://github.com/Effect-TS/effect/pull/2360) [`0f6c7b4`](https://github.com/Effect-TS/effect/commit/0f6c7b426eb3432f60e3a17f8cd92ceac91597bf) Thanks [@tim-smart](https://github.com/tim-smart)! - add support for watching single files + +## 0.48.10 + +### Patch Changes + +- [#2357](https://github.com/Effect-TS/effect/pull/2357) [`71fd528`](https://github.com/Effect-TS/effect/commit/71fd5287500f9ce155a7d9f0df6ee3e0ac3aeb99) Thanks [@tim-smart](https://github.com/tim-smart)! - make more data types in /platform implement Inspectable + +- Updated dependencies [[`71fd528`](https://github.com/Effect-TS/effect/commit/71fd5287500f9ce155a7d9f0df6ee3e0ac3aeb99)]: + - effect@2.4.9 + - @effect/schema@0.64.7 + +## 0.48.9 + +### Patch Changes + +- Updated dependencies [[`595140a`](https://github.com/Effect-TS/effect/commit/595140a13bda09bf22c669196440868e8a274599), [`5f5fcd9`](https://github.com/Effect-TS/effect/commit/5f5fcd969ae30ed6fe61d566a571498d9e895e16), [`bb0b69e`](https://github.com/Effect-TS/effect/commit/bb0b69e519698c7c76aa68217de423c78ad16566), [`7a45ad0`](https://github.com/Effect-TS/effect/commit/7a45ad0a5f715d64a69b28a8ee3573e5f86909c3), [`5c3b1cc`](https://github.com/Effect-TS/effect/commit/5c3b1ccba182d0f636a973729f9c6bfb12539dc8), [`6f7dfc9`](https://github.com/Effect-TS/effect/commit/6f7dfc9637bd641beb93b14e027dcfcb5d2c8feb), [`88b8583`](https://github.com/Effect-TS/effect/commit/88b85838e03d4f33036f9d16c9c00a487fa99bd8), [`cb20824`](https://github.com/Effect-TS/effect/commit/cb20824416cbf251188395d0aad3622e3a5d7ff2), [`6b20bad`](https://github.com/Effect-TS/effect/commit/6b20badebb3a7ca4d38857753e8ecaa09d02ccfb), [`4e64e9b`](https://github.com/Effect-TS/effect/commit/4e64e9b9876de6bfcbabe39e18a91a08e5f3fbb0), [`3851a02`](https://github.com/Effect-TS/effect/commit/3851a022c481006aec1db36651e4b4fd727aa742), [`5f5fcd9`](https://github.com/Effect-TS/effect/commit/5f5fcd969ae30ed6fe61d566a571498d9e895e16), [`814e5b8`](https://github.com/Effect-TS/effect/commit/814e5b828f68210b9e8f336fd6ac688646835dd9), [`a45a525`](https://github.com/Effect-TS/effect/commit/a45a525e7ccf07704dff1666f1e390282b5bac91)]: + - @effect/schema@0.64.6 + - effect@2.4.8 + +## 0.48.8 + +### Patch Changes + +- [#2334](https://github.com/Effect-TS/effect/pull/2334) [`69d27bb`](https://github.com/Effect-TS/effect/commit/69d27bb633884b6b50f9c3d9e95c29f09b4860b5) Thanks [@tim-smart](https://github.com/tim-smart)! - add .watch method to /platform FileSystem + + It can be used to listen for file system events. Example: + + ```ts + import { FileSystem } from "@effect/platform" + import { NodeFileSystem, NodeRuntime } from "@effect/platform-node" + import { Console, Effect, Stream } from "effect" + + Effect.gen(function* (_) { + const fs = yield* _(FileSystem.FileSystem) + yield* _(fs.watch("./"), Stream.runForEach(Console.log)) + }).pipe(Effect.provide(NodeFileSystem.layer), NodeRuntime.runMain) + ``` + +- Updated dependencies [[`d0f56c6`](https://github.com/Effect-TS/effect/commit/d0f56c68e604b1cf8dd4e761a3f3cf3631b3cec1)]: + - @effect/schema@0.64.5 + +## 0.48.7 + +### Patch Changes + +- [#2330](https://github.com/Effect-TS/effect/pull/2330) [`f908948`](https://github.com/Effect-TS/effect/commit/f908948fd05771a670c0b746e2dd9caa9408ef83) Thanks [@tim-smart](https://github.com/tim-smart)! - use Deferred.unsafeDone for websocket onclose + onerror + +## 0.48.6 + +### Patch Changes + +- Updated dependencies [[`eb93283`](https://github.com/Effect-TS/effect/commit/eb93283985913d7b04ca750e36ac8513e7b6cef6)]: + - effect@2.4.7 + - @effect/schema@0.64.4 + +## 0.48.5 + +### Patch Changes + +- [#2325](https://github.com/Effect-TS/effect/pull/2325) [`e006e4a`](https://github.com/Effect-TS/effect/commit/e006e4a538c97bae6ca1efa74802159e8a688fcb) Thanks [@tim-smart](https://github.com/tim-smart)! - ensure Socket fibers are interruptible + +## 0.48.4 + +### Patch Changes + +- Updated dependencies [[`cfef6ec`](https://github.com/Effect-TS/effect/commit/cfef6ecd1fe801cec1a3cbfb7f064fc394b0ad73)]: + - @effect/schema@0.64.3 + +## 0.48.3 + +### Patch Changes + +- [#2314](https://github.com/Effect-TS/effect/pull/2314) [`c362e06`](https://github.com/Effect-TS/effect/commit/c362e066550252d5a9fcbc31a4b34d0e17c50699) Thanks [@tim-smart](https://github.com/tim-smart)! - prevent unhandled fiber errors in Sockets + +- [#2262](https://github.com/Effect-TS/effect/pull/2262) [`83ddd6f`](https://github.com/Effect-TS/effect/commit/83ddd6f41029724b2cbd144cf309463967ed1164) Thanks [@thewilkybarkid](https://github.com/thewilkybarkid)! - Don't log an empty message when responding to a request + +## 0.48.2 + +### Patch Changes + +- [#2290](https://github.com/Effect-TS/effect/pull/2290) [`4f35a7e`](https://github.com/Effect-TS/effect/commit/4f35a7e7c4eba598924aff24d1158b9056bb24be) Thanks [@mikearnaldi](https://github.com/mikearnaldi)! - Remove function renaming from internals, introduce new cutpoint strategy + +- Updated dependencies [[`89748c9`](https://github.com/Effect-TS/effect/commit/89748c90b36cb5eb880a9ab9323b252338dee848), [`4f35a7e`](https://github.com/Effect-TS/effect/commit/4f35a7e7c4eba598924aff24d1158b9056bb24be), [`9971186`](https://github.com/Effect-TS/effect/commit/99711862722188fbb5ed3ee75126ad5edf13f72f)]: + - @effect/schema@0.64.2 + - effect@2.4.6 + +## 0.48.1 + +### Patch Changes + +- Updated dependencies [[`d10f876`](https://github.com/Effect-TS/effect/commit/d10f876cd98da275bc5dc5750a91a7fc95e97541), [`743ae6d`](https://github.com/Effect-TS/effect/commit/743ae6d12b249f0b35b31b65b2f7ec91d83ee387), [`a75bc48`](https://github.com/Effect-TS/effect/commit/a75bc48e0e3278d0f70665fedecc5ae7ec447e24), [`bce21c5`](https://github.com/Effect-TS/effect/commit/bce21c5ded2177114666ba229bd5029fa000dee3), [`c7d3036`](https://github.com/Effect-TS/effect/commit/c7d303630b7f0825cb2e584557c5767a67214d9f)]: + - @effect/schema@0.64.1 + - effect@2.4.5 + +## 0.48.0 + +### Minor Changes + +- [#2287](https://github.com/Effect-TS/effect/pull/2287) [`a1f44cb`](https://github.com/Effect-TS/effect/commit/a1f44cb5112713ff9a3ac3d91a63a2c99d6b7fc1) Thanks [@tim-smart](https://github.com/tim-smart)! - add option to /platform runMain to disable error reporting + +- [#2279](https://github.com/Effect-TS/effect/pull/2279) [`bdff193`](https://github.com/Effect-TS/effect/commit/bdff193365dd9ec2863573b08eb960aa8dee5c93) Thanks [@gcanti](https://github.com/gcanti)! - - `src/Worker.ts` + - use `CauseEncoded` in `Worker` namespace + - `src/WorkerError.ts` + - use `CauseEncoded` in `Cause` + +### Patch Changes + +- [#2284](https://github.com/Effect-TS/effect/pull/2284) [`1cb7f9c`](https://github.com/Effect-TS/effect/commit/1cb7f9cff7c2272a32fc7a324d87b02e2cd8a2f5) Thanks [@tim-smart](https://github.com/tim-smart)! - use Schema.declare for http multipart PersistedFile schema + +- [#2283](https://github.com/Effect-TS/effect/pull/2283) [`509be1a`](https://github.com/Effect-TS/effect/commit/509be1a0817118489750cf028523134677e44a8a) Thanks [@tim-smart](https://github.com/tim-smart)! - add SocketCloseError with additional metadata + +- [#2284](https://github.com/Effect-TS/effect/pull/2284) [`1cb7f9c`](https://github.com/Effect-TS/effect/commit/1cb7f9cff7c2272a32fc7a324d87b02e2cd8a2f5) Thanks [@tim-smart](https://github.com/tim-smart)! - add more http multipart data type refinements + +- [#2281](https://github.com/Effect-TS/effect/pull/2281) [`e7ca973`](https://github.com/Effect-TS/effect/commit/e7ca973c5430ae60716701e58bedd4632ff971fd) Thanks [@tim-smart](https://github.com/tim-smart)! - add OpenTimeout error to websocket client + +- [#2286](https://github.com/Effect-TS/effect/pull/2286) [`d910dd2`](https://github.com/Effect-TS/effect/commit/d910dd2ca1e8e5aa2f09d9bf3694ede745758f99) Thanks [@tim-smart](https://github.com/tim-smart)! - allow optional fields in http form schemas + +- [#2281](https://github.com/Effect-TS/effect/pull/2281) [`e7ca973`](https://github.com/Effect-TS/effect/commit/e7ca973c5430ae60716701e58bedd4632ff971fd) Thanks [@tim-smart](https://github.com/tim-smart)! - support closing a Socket by writing a CloseEvent + +- Updated dependencies [[`5d47ee0`](https://github.com/Effect-TS/effect/commit/5d47ee0855e492532085b6092879b1b952d84949), [`5d47ee0`](https://github.com/Effect-TS/effect/commit/5d47ee0855e492532085b6092879b1b952d84949), [`5d47ee0`](https://github.com/Effect-TS/effect/commit/5d47ee0855e492532085b6092879b1b952d84949), [`817a04c`](https://github.com/Effect-TS/effect/commit/817a04cb2df0f4140984dc97eb3e1bb14a6c4a38), [`d90a99d`](https://github.com/Effect-TS/effect/commit/d90a99d03d074adc7cd2533f15419138264da5a2), [`dd05faa`](https://github.com/Effect-TS/effect/commit/dd05faa621555ef3585ecd914ac13ecd89b710f4), [`dd05faa`](https://github.com/Effect-TS/effect/commit/dd05faa621555ef3585ecd914ac13ecd89b710f4), [`802674b`](https://github.com/Effect-TS/effect/commit/802674b379b7559ad3ff09b33388891445a9e48b)]: + - @effect/schema@0.64.0 + - effect@2.4.4 + +## 0.47.1 + +### Patch Changes + +- [#2276](https://github.com/Effect-TS/effect/pull/2276) [`0680545`](https://github.com/Effect-TS/effect/commit/068054540f19bb23a79c7c021ed8b2fe34f3e19f) Thanks [@tim-smart](https://github.com/tim-smart)! - improve /platform error messages + +- Updated dependencies [[`20e63fb`](https://github.com/Effect-TS/effect/commit/20e63fb9207210f3fe2d136ec40d0a2dbff3225e), [`20e63fb`](https://github.com/Effect-TS/effect/commit/20e63fb9207210f3fe2d136ec40d0a2dbff3225e)]: + - effect@2.4.3 + - @effect/schema@0.63.4 + +## 0.47.0 + +### Minor Changes + +- [#2261](https://github.com/Effect-TS/effect/pull/2261) [`fa9663c`](https://github.com/Effect-TS/effect/commit/fa9663cb854ca03dba672d7857ecff84f1140c9e) Thanks [@tim-smart](https://github.com/tim-smart)! - move Socket module to platform + +### Patch Changes + +- [#2267](https://github.com/Effect-TS/effect/pull/2267) [`0f3d99c`](https://github.com/Effect-TS/effect/commit/0f3d99c27521ec6b221b644a0fffc79199c3acca) Thanks [@tim-smart](https://github.com/tim-smart)! - propogate Socket handler errors to .run Effect + +- [#2269](https://github.com/Effect-TS/effect/pull/2269) [`4064ea0`](https://github.com/Effect-TS/effect/commit/4064ea04e0b3fa23108ee471cd89ab2482b2f6e5) Thanks [@jessekelly881](https://github.com/jessekelly881)! - added PlatformLogger module, for writing logs to a file + + If you wanted to write logfmt logs to a file, you can do the following: + + ```ts + import { PlatformLogger } from "@effect/platform" + import { NodeFileSystem, NodeRuntime } from "@effect/platform-node" + import { Effect, Layer, Logger } from "effect" + + const fileLogger = Logger.logfmtLogger.pipe(PlatformLogger.toFile("log.txt")) + const LoggerLive = Logger.replaceScoped( + Logger.defaultLogger, + fileLogger + ).pipe(Layer.provide(NodeFileSystem.layer)) + + Effect.log("a").pipe( + Effect.zipRight(Effect.log("b")), + Effect.zipRight(Effect.log("c")), + Effect.provide(LoggerLive), + NodeRuntime.runMain + ) + ``` + +- [#2261](https://github.com/Effect-TS/effect/pull/2261) [`fa9663c`](https://github.com/Effect-TS/effect/commit/fa9663cb854ca03dba672d7857ecff84f1140c9e) Thanks [@tim-smart](https://github.com/tim-smart)! - add websocket support to platform http server + + You can use the `Http.request.upgrade*` apis to access the `Socket` for the request. + + Here is an example server that handles websockets on the `/ws` path: + + ```ts + import { NodeHttpServer, NodeRuntime } from "@effect/platform-node" + import * as Http from "@effect/platform/HttpServer" + import { Console, Effect, Layer, Schedule, Stream } from "effect" + import { createServer } from "node:http" + + const ServerLive = NodeHttpServer.server.layer(() => createServer(), { + port: 3000 + }) + + const HttpLive = Http.router.empty.pipe( + Http.router.get( + "/ws", + Effect.gen(function* (_) { + yield* _( + Stream.fromSchedule(Schedule.spaced(1000)), + Stream.map(JSON.stringify), + Stream.encodeText, + Stream.pipeThroughChannel(Http.request.upgradeChannel()), + Stream.decodeText(), + Stream.runForEach(Console.log) + ) + return Http.response.empty() + }) + ), + Http.server.serve(Http.middleware.logger), + Http.server.withLogAddress, + Layer.provide(ServerLive) + ) + + NodeRuntime.runMain(Layer.launch(HttpLive)) + ``` + +- Updated dependencies [[`e03811e`](https://github.com/Effect-TS/effect/commit/e03811e80c93e986e6348b3b67ac2ed6d5fefff0), [`ac41d84`](https://github.com/Effect-TS/effect/commit/ac41d84776484cdce8165b7ca2c9c9b6377eee2d), [`6137533`](https://github.com/Effect-TS/effect/commit/613753300c7705518ab1fea2f370b032851c2750), [`f373529`](https://github.com/Effect-TS/effect/commit/f373529999f4b8bc92b634f6ea14f19271388eed), [`1bf9f31`](https://github.com/Effect-TS/effect/commit/1bf9f31f07667de677673f7c29a4e7a26ebad3c8), [`e3ff789`](https://github.com/Effect-TS/effect/commit/e3ff789226f89e71eb28ca38ce79f90af6a03f1a), [`6137533`](https://github.com/Effect-TS/effect/commit/613753300c7705518ab1fea2f370b032851c2750), [`507ba40`](https://github.com/Effect-TS/effect/commit/507ba4060ff043c1a8d541dae723fa6940633b00), [`e466afe`](https://github.com/Effect-TS/effect/commit/e466afe32f2de598ceafd8982bd0cfbd388e5671), [`465be79`](https://github.com/Effect-TS/effect/commit/465be7926afe98169837d8a4ed5ebc059a732d21), [`f373529`](https://github.com/Effect-TS/effect/commit/f373529999f4b8bc92b634f6ea14f19271388eed), [`de74eb8`](https://github.com/Effect-TS/effect/commit/de74eb80a79eebde5ff645033765e7a617e92f27), [`d8e6940`](https://github.com/Effect-TS/effect/commit/d8e694040f67da6fefc0f5c98fc8e15c0b48822e)]: + - effect@2.4.2 + - @effect/schema@0.63.3 + +## 0.46.3 + +### Patch Changes + +- [#2231](https://github.com/Effect-TS/effect/pull/2231) [`7535080`](https://github.com/Effect-TS/effect/commit/7535080f2e2f9859711031161600c01807cc43ea) Thanks [@tim-smart](https://github.com/tim-smart)! - add option to include prefix when mounting an http app to a router + + By default the prefix is removed. For example: + + ```ts + // Here a request to `/child/hello` will be mapped to `/hello` + Http.router.mountApp("/child", httpApp) + + // Here a request to `/child/hello` will be mapped to `/child/hello` + Http.router.mountApp("/child", httpApp, { includePrefix: true }) + ``` + +- [#2232](https://github.com/Effect-TS/effect/pull/2232) [`bd1d7ac`](https://github.com/Effect-TS/effect/commit/bd1d7ac75eea57a94d5e2d8e1edccb3136e84899) Thanks [@tim-smart](https://github.com/tim-smart)! - use less aggressive type exclusion in http router apis + +- Updated dependencies [[`a4a0006`](https://github.com/Effect-TS/effect/commit/a4a0006c7f19fc261df5cda16963d73457e4d6ac), [`39f583e`](https://github.com/Effect-TS/effect/commit/39f583eaeb29eecd6eaec3b113b24d9d413153df), [`f428198`](https://github.com/Effect-TS/effect/commit/f428198725d4b9e304ecd5ff8bad8f92d871dbe3), [`0a37676`](https://github.com/Effect-TS/effect/commit/0a37676aa0eb2a21e17af2e6df9f81f52bbc8831), [`c035972`](https://github.com/Effect-TS/effect/commit/c035972dfabdd3cb3372b5ab468aa2fd0d808f4d), [`6f503b7`](https://github.com/Effect-TS/effect/commit/6f503b774d893bf2af34f66202e270d8c45d5f31)]: + - effect@2.4.1 + - @effect/schema@0.63.2 + +## 0.46.2 + +### Patch Changes + +- Updated dependencies [[`5d30853`](https://github.com/Effect-TS/effect/commit/5d308534cac6f187227185393c0bac9eb27f90ab), [`6e350ed`](https://github.com/Effect-TS/effect/commit/6e350ed611feb0341e00aafd3c3905cd5ba53f07)]: + - @effect/schema@0.63.1 + +## 0.46.1 + +### Patch Changes + +- [#2196](https://github.com/Effect-TS/effect/pull/2196) [`aa6556f`](https://github.com/Effect-TS/effect/commit/aa6556f007117caea84d6965aa30846a11879e9d) Thanks [@tim-smart](https://github.com/tim-smart)! - handle defects in worker runner + +## 0.46.0 + +### Minor Changes + +- [#2101](https://github.com/Effect-TS/effect/pull/2101) [`5de7be5`](https://github.com/Effect-TS/effect/commit/5de7be5beca2e963b503e6029dcc3217848187d2) Thanks [@github-actions](https://github.com/apps/github-actions)! - add key type to ReadonlyRecord + +### Patch Changes + +- Updated dependencies [[`5de7be5`](https://github.com/Effect-TS/effect/commit/5de7be5beca2e963b503e6029dcc3217848187d2), [`489fcf3`](https://github.com/Effect-TS/effect/commit/489fcf363ff2b2a953166b740cb9a62d7fc2a101), [`7d9c3bf`](https://github.com/Effect-TS/effect/commit/7d9c3bff6c18d451e0e4781042945ec5c7be1b9f), [`d8d278b`](https://github.com/Effect-TS/effect/commit/d8d278b2efb2966947029885e01f7b68348a021f), [`14c5711`](https://github.com/Effect-TS/effect/commit/14c57110078f0862b8da5c7a2c5d980f54447484), [`5de7be5`](https://github.com/Effect-TS/effect/commit/5de7be5beca2e963b503e6029dcc3217848187d2), [`54ddbb7`](https://github.com/Effect-TS/effect/commit/54ddbb720aeeb657537b01ae221cdcd5e919c1a6), [`b9cb3a9`](https://github.com/Effect-TS/effect/commit/b9cb3a9c9bfdd75536bd70b4e8b557c12d4923ff), [`585fcce`](https://github.com/Effect-TS/effect/commit/585fcce162d0f07a48d7cd984a9b722966fbebbe), [`93b412d`](https://github.com/Effect-TS/effect/commit/93b412d4a9ed762dc9fa5807e51fad0fc78a614a), [`55b26a6`](https://github.com/Effect-TS/effect/commit/55b26a6342b4826f1116e7a1eb660118c274458e), [`136ef40`](https://github.com/Effect-TS/effect/commit/136ef40fe4a394abfa5c6a7ec103eea57251423e), [`a025b12`](https://github.com/Effect-TS/effect/commit/a025b121235ba01cfce8d62a775491880c575561), [`2097739`](https://github.com/Effect-TS/effect/commit/20977393d2383bff709304e81ec7d51cafd57108), [`f24ac9f`](https://github.com/Effect-TS/effect/commit/f24ac9f0c2c520add58f09fbdcec5defda03bd52)]: + - effect@2.4.0 + - @effect/schema@0.63.0 + +## 0.45.6 + +### Patch Changes + +- [#2187](https://github.com/Effect-TS/effect/pull/2187) [`e6d36c0`](https://github.com/Effect-TS/effect/commit/e6d36c0813d836f17eabb6a9c7849baffca12dbf) Thanks [@tim-smart](https://github.com/tim-smart)! - update development dependencies + +- Updated dependencies [[`5ad2eec`](https://github.com/Effect-TS/effect/commit/5ad2eece0280b6db6a749d25cac1dcf6d33659a9), [`e6d36c0`](https://github.com/Effect-TS/effect/commit/e6d36c0813d836f17eabb6a9c7849baffca12dbf)]: + - effect@2.3.8 + - @effect/schema@0.62.9 + +## 0.45.5 + +### Patch Changes + +- [#2177](https://github.com/Effect-TS/effect/pull/2177) [`6daf084`](https://github.com/Effect-TS/effect/commit/6daf0845de008772011db8d7c75b7c37a6b4d334) Thanks [@tim-smart](https://github.com/tim-smart)! - support Arrays in platform Template module + +## 0.45.4 + +### Patch Changes + +- [#2174](https://github.com/Effect-TS/effect/pull/2174) [`abcb7d9`](https://github.com/Effect-TS/effect/commit/abcb7d983a4a85b43b7175e952f5b331b9019aea) Thanks [@tim-smart](https://github.com/tim-smart)! - add ServerResponse.html/htmlStream api + + It uses the Template module to create html responses + + Example: + + ```ts + import { Effect } from "effect" + import * as Http from "@effect/platform/HttpServer" + + Http.response.html`${Effect.succeed(123)}` + ``` + +- [#2174](https://github.com/Effect-TS/effect/pull/2174) [`abcb7d9`](https://github.com/Effect-TS/effect/commit/abcb7d983a4a85b43b7175e952f5b331b9019aea) Thanks [@tim-smart](https://github.com/tim-smart)! - add Template module to platform + + The Template module can be used to create effectful text templates. + + Example: + + ```ts + import { Effect } from "effect" + import { Template } from "@effect/platform" + + const t = Template.make`${Effect.succeed(123)}` + + Effect.runSync(t) // returns "123" + ``` + +- Updated dependencies [[`bc8404d`](https://github.com/Effect-TS/effect/commit/bc8404d54fd42072d200c0399cb39672837afa9f), [`2c5cbcd`](https://github.com/Effect-TS/effect/commit/2c5cbcd1161b4f40dab184999291e817314107de), [`6565916`](https://github.com/Effect-TS/effect/commit/6565916ef254bf910e47d25fd0ef55e7cb420241)]: + - effect@2.3.7 + - @effect/schema@0.62.8 + +## 0.45.3 + +### Patch Changes + +- [#2152](https://github.com/Effect-TS/effect/pull/2152) [`09532a8`](https://github.com/Effect-TS/effect/commit/09532a86b7d0cc23557c89158f0342753dfce4b0) Thanks [@tim-smart](https://github.com/tim-smart)! - fix incorrect removal of scope in Client.schemaFunction + +## 0.45.2 + +### Patch Changes + +- [#2122](https://github.com/Effect-TS/effect/pull/2122) [`44c3b43`](https://github.com/Effect-TS/effect/commit/44c3b43653e64d7e425d39815d8ff405acec9b99) Thanks [@thewilkybarkid](https://github.com/thewilkybarkid)! - Add a way to redact HTTP headers + +- Updated dependencies [[`b1163b2`](https://github.com/Effect-TS/effect/commit/b1163b2bd67b65bafbbb39fc4c67576e5cbaf444), [`b46b869`](https://github.com/Effect-TS/effect/commit/b46b869e59a6da5aa235a9fcc25e1e0d24e9e8f8), [`dbff62c`](https://github.com/Effect-TS/effect/commit/dbff62c3026054350a671f6210058ec5844c285e), [`de1b226`](https://github.com/Effect-TS/effect/commit/de1b226282b5ab6c2809dd93f3bdb066f24a1333), [`a663390`](https://github.com/Effect-TS/effect/commit/a66339090ae7b960f8a8b90a0dcdc505de5aaf3e), [`ff88f80`](https://github.com/Effect-TS/effect/commit/ff88f808c4ed9947a148045849e7410b00acad0a), [`11be07b`](https://github.com/Effect-TS/effect/commit/11be07bf65d82cfdf994cdb9d8ca937f995cb4f0), [`c568645`](https://github.com/Effect-TS/effect/commit/c5686451c87d26382135a1c63b00ef171bb24f62), [`88835e5`](https://github.com/Effect-TS/effect/commit/88835e575a0bfbeff9a3696a332f32192c940e12), [`e572b07`](https://github.com/Effect-TS/effect/commit/e572b076e9b4369d9cc8e55414006eef376c93d9), [`e787a57`](https://github.com/Effect-TS/effect/commit/e787a5772e30d8b840cb98b49d36996e7d659a6c), [`b415577`](https://github.com/Effect-TS/effect/commit/b415577f6c576073733929c858e5aac27b6d5880), [`ff8046f`](https://github.com/Effect-TS/effect/commit/ff8046f57dfd073eba60ce6d3144ab060fbf93ce)]: + - effect@2.3.6 + - @effect/schema@0.62.7 + +## 0.45.1 + +### Patch Changes + +- [#2133](https://github.com/Effect-TS/effect/pull/2133) [`65895ab`](https://github.com/Effect-TS/effect/commit/65895ab982e0917ac92f0827e387e7cf61be1e69) Thanks [@tim-smart](https://github.com/tim-smart)! - use Schema.TaggedError for worker errors + +## 0.45.0 + +### Minor Changes + +- [#2119](https://github.com/Effect-TS/effect/pull/2119) [`2b62548`](https://github.com/Effect-TS/effect/commit/2b6254845882f399636d24223c483e5489e3cff4) Thanks [@tim-smart](https://github.com/tim-smart)! - add Scope to Http client + + This change adds a scope to the default http client, ensuring connections are + cleaned up if you abort the request at any point. + + Some response helpers have been added to reduce the noise. + + ```ts + import * as Http from "@effect/platform/HttpClient" + import { Effect } from "effect" + + // instead of + Http.request.get("/").pipe( + Http.client.fetchOk(), + Effect.flatMap((_) => _.json), + Effect.scoped + ) + + // you can do + Http.request.get("/").pipe(Http.client.fetchOk(), Http.response.json) + + // other helpers include + Http.response.text + Http.response.stream + Http.response.arrayBuffer + Http.response.urlParamsBody + Http.response.formData + Http.response.schema * Effect + ``` + +## 0.44.7 + +### Patch Changes + +- Updated dependencies [[`aef2b8b`](https://github.com/Effect-TS/effect/commit/aef2b8bb636ada07224dc9cf491bebe622c1aeda), [`b881365`](https://github.com/Effect-TS/effect/commit/b8813650355322ea2fc1fbaa4f846bd87a7a05f3), [`7eecb1c`](https://github.com/Effect-TS/effect/commit/7eecb1c6cebe36550df3cca85a46867adbcaa2ca)]: + - @effect/schema@0.62.6 + - effect@2.3.5 + +## 0.44.6 + +### Patch Changes + +- Updated dependencies [[`17bda66`](https://github.com/Effect-TS/effect/commit/17bda66431c999a546920c10adb205e6c8bea7d1)]: + - effect@2.3.4 + - @effect/schema@0.62.5 + +## 0.44.5 + +### Patch Changes + +- Updated dependencies [[`1c6d18b`](https://github.com/Effect-TS/effect/commit/1c6d18b422b0bd800f2ed036dba9cb78db296c03), [`13d3266`](https://github.com/Effect-TS/effect/commit/13d3266f331f7aa49b55dd244d4e749a82255274), [`a344b42`](https://github.com/Effect-TS/effect/commit/a344b420862f71532a28c72f00b7ba54776d744d)]: + - @effect/schema@0.62.4 + +## 0.44.4 + +### Patch Changes + +- Updated dependencies [[`efd41d8`](https://github.com/Effect-TS/effect/commit/efd41d8131c3d90867608969ef7c4eef490eb5e6), [`0f83515`](https://github.com/Effect-TS/effect/commit/0f83515a9c01d13c7c15a3f026e02d22c3c6bb7f), [`0f83515`](https://github.com/Effect-TS/effect/commit/0f83515a9c01d13c7c15a3f026e02d22c3c6bb7f)]: + - effect@2.3.3 + - @effect/schema@0.62.3 + +## 0.44.3 + +### Patch Changes + +- Updated dependencies [[`6654f5f`](https://github.com/Effect-TS/effect/commit/6654f5f0f6b9d97165ede5e04ca16776e2599328), [`2eb11b4`](https://github.com/Effect-TS/effect/commit/2eb11b47752cedf233ef4c4395d9c4efc9b9e180), [`56c09bd`](https://github.com/Effect-TS/effect/commit/56c09bd369279a6a7785209d172739935818cba6), [`71aa5b1`](https://github.com/Effect-TS/effect/commit/71aa5b1c180dcb8b53aefe232d12a97bd06b5447), [`1700af8`](https://github.com/Effect-TS/effect/commit/1700af8af1131602887da721914c8562b6342393)]: + - effect@2.3.2 + - @effect/schema@0.62.2 + +## 0.44.2 + +### Patch Changes + +- [#2091](https://github.com/Effect-TS/effect/pull/2091) [`29739dd`](https://github.com/Effect-TS/effect/commit/29739dde8e6232824d49c4c7f8856de245249c5c) Thanks [@tim-smart](https://github.com/tim-smart)! - improve type extraction for Router.fromIterable + +## 0.44.1 + +### Patch Changes + +- Updated dependencies [[`b5a8215`](https://github.com/Effect-TS/effect/commit/b5a8215ee2a97a8865d69ee55ce1b9835948c922)]: + - effect@2.3.1 + - @effect/schema@0.62.1 + +## 0.44.0 + +### Minor Changes + +- [#2006](https://github.com/Effect-TS/effect/pull/2006) [`9a2d1c1`](https://github.com/Effect-TS/effect/commit/9a2d1c1468ea0789b34767ad683da074f061ea9c) Thanks [@github-actions](https://github.com/apps/github-actions)! - With this change we now require a string key to be provided for all tags and renames the dear old `Tag` to `GenericTag`, so when previously you could do: + + ```ts + import { Effect, Context } from "effect" + interface Service { + readonly _: unique symbol + } + const Service = Context.Tag< + Service, + { + number: Effect.Effect + } + >() + ``` + + you are now mandated to do: + + ```ts + import { Effect, Context } from "effect" + interface Service { + readonly _: unique symbol + } + const Service = Context.GenericTag< + Service, + { + number: Effect.Effect + } + >("Service") + ``` + + This makes by default all tags globals and ensures better debuggaility when unexpected errors arise. + + Furthermore we introduce a new way of constructing tags that should be considered the new default: + + ```ts + import { Effect, Context } from "effect" + class Service extends Context.Tag("Service")< + Service, + { + number: Effect.Effect + } + >() {} + + const program = Effect.flatMap(Service, ({ number }) => number).pipe( + Effect.flatMap((_) => Effect.log(`number: ${_}`)) + ) + ``` + + this will use "Service" as the key and will create automatically an opaque identifier (the class) to be used at the type level, it does something similar to the above in a single shot. + +- [#2006](https://github.com/Effect-TS/effect/pull/2006) [`1a77f72`](https://github.com/Effect-TS/effect/commit/1a77f72cdaf43d6cdc91b6060f82832edcdbbcb3) Thanks [@github-actions](https://github.com/apps/github-actions)! - change `Effect` type parameters order from `Effect` to `Effect` + +- [#2006](https://github.com/Effect-TS/effect/pull/2006) [`af47aa3`](https://github.com/Effect-TS/effect/commit/af47aa37196ad542c9c23a4896d8ef98147e1205) Thanks [@github-actions](https://github.com/apps/github-actions)! - move where platform worker spawn function is provided + + With this change, the point in which you provide the spawn function moves closer + to the edge, where you provide platform specific implementation. + + This seperates even more platform concerns from your business logic. Example: + + ```ts + import { Worker } from "@effect/platform" + import { BrowserWorker } from "@effect/platform-browser" + import { Effect } from "effect" + + Worker.makePool({ ... }).pipe( + Effect.provide(BrowserWorker.layer(() => new globalThis.Worker(...))) + ) + ``` + +- [#2006](https://github.com/Effect-TS/effect/pull/2006) [`a34dbdc`](https://github.com/Effect-TS/effect/commit/a34dbdc1552c73c1b612676f262a0c735ce444a7) Thanks [@github-actions](https://github.com/apps/github-actions)! - - Schema: change type parameters order from `Schema` to `Schema` + - Serializable: change type parameters order from `Serializable` to `Serializable` + - Class: change type parameters order from `Class` to `Class` + - PropertySignature: change type parameters order from `PropertySignature` to `PropertySignature` + +- [#2006](https://github.com/Effect-TS/effect/pull/2006) [`b1e2086`](https://github.com/Effect-TS/effect/commit/b1e2086ea8bf410e4ad75d71c0760825924e8f9f) Thanks [@github-actions](https://github.com/apps/github-actions)! - remove re-exports from platform packages + +- [#2006](https://github.com/Effect-TS/effect/pull/2006) [`02c3461`](https://github.com/Effect-TS/effect/commit/02c34615d02f91269ea04036d0306fccf4e39e18) Thanks [@github-actions](https://github.com/apps/github-actions)! - With this change we remove the `Data.Data` type and we make `Equal.Equal` & `Hash.Hash` implicit traits. + + The main reason is that `Data.Data
` was structurally equivalent to `A & Equal.Equal` but extending `Equal.Equal` doesn't mean that the equality is implemented by-value, so the type was simply adding noise without gaining any level of safety. + + The module `Data` remains unchanged at the value level, all the functions previously available are supposed to work in exactly the same manner. + + At the type level instead the functions return `Readonly` variants, so for example we have: + + ```ts + import { Data } from "effect" + + const obj = Data.struct({ + a: 0, + b: 1 + }) + ``` + + will have the `obj` typed as: + + ```ts + declare const obj: { + readonly a: number + readonly b: number + } + ``` + +- [#2006](https://github.com/Effect-TS/effect/pull/2006) [`6361ee2`](https://github.com/Effect-TS/effect/commit/6361ee2e83bdfead24045c3d058a7298efc18113) Thanks [@github-actions](https://github.com/apps/github-actions)! - fix for encoding of Transferable schemas + +- [#2006](https://github.com/Effect-TS/effect/pull/2006) [`86f665d`](https://github.com/Effect-TS/effect/commit/86f665d7bd25ba0a3f046a2384798378310dcf0c) Thanks [@github-actions](https://github.com/apps/github-actions)! - use Context for collecting tranferables + + This changes the platform Transferable module to use Effect context to collect + tranferables when using schemas with workers etc. + + You can now use a tranferable data type anywhere in your schema without having + to wrap the outermost schema: + + ```ts + import { Transferable } from "@effect/platform" + import { Schema } from "@effect/schema" + + const structWithTransferable = Schema.struct({ + data: Transferable.Uint8Array + }) + ``` + +- [#2006](https://github.com/Effect-TS/effect/pull/2006) [`9a2d1c1`](https://github.com/Effect-TS/effect/commit/9a2d1c1468ea0789b34767ad683da074f061ea9c) Thanks [@github-actions](https://github.com/apps/github-actions)! - This change enables `Effect.serviceConstants` and `Effect.serviceMembers` to access any constant in the service, not only the effects, namely it is now possible to do: + + ```ts + import { Effect, Context } from "effect" + + class NumberRepo extends Context.TagClass("NumberRepo")< + NumberRepo, + { + readonly numbers: Array + } + >() { + static numbers = Effect.serviceConstants(NumberRepo).numbers + } + ``` + +### Patch Changes + +- [#2006](https://github.com/Effect-TS/effect/pull/2006) [`b1e2086`](https://github.com/Effect-TS/effect/commit/b1e2086ea8bf410e4ad75d71c0760825924e8f9f) Thanks [@github-actions](https://github.com/apps/github-actions)! - add server address apis + +- Updated dependencies [[`96bcee2`](https://github.com/Effect-TS/effect/commit/96bcee21021aecd8ffd86440a2c9be353c4668e3), [`4cd6e14`](https://github.com/Effect-TS/effect/commit/4cd6e144945b6c398f5f5abe3471ff7fb3372bfd), [`96bcee2`](https://github.com/Effect-TS/effect/commit/96bcee21021aecd8ffd86440a2c9be353c4668e3), [`c77f635`](https://github.com/Effect-TS/effect/commit/c77f635f8a26ca6d83cb569d911f8eee79033fd9), [`e343a74`](https://github.com/Effect-TS/effect/commit/e343a74843dd9edf879417fa94cb51de7ed5b402), [`acf1894`](https://github.com/Effect-TS/effect/commit/acf1894f45945dbe5c39451e36aabb4b5092f257), [`9dc04c8`](https://github.com/Effect-TS/effect/commit/9dc04c88a2ea9c68122cb2632a76f0f4be40329a), [`9a2d1c1`](https://github.com/Effect-TS/effect/commit/9a2d1c1468ea0789b34767ad683da074f061ea9c), [`1a77f72`](https://github.com/Effect-TS/effect/commit/1a77f72cdaf43d6cdc91b6060f82832edcdbbcb3), [`d3f9f4d`](https://github.com/Effect-TS/effect/commit/d3f9f4d4032b1131c62f4ddb21a4583e4e8d7c18), [`c986f0e`](https://github.com/Effect-TS/effect/commit/c986f0e0ce4d22ba08177ed351152718479ab63c), [`96bcee2`](https://github.com/Effect-TS/effect/commit/96bcee21021aecd8ffd86440a2c9be353c4668e3), [`d3f9f4d`](https://github.com/Effect-TS/effect/commit/d3f9f4d4032b1131c62f4ddb21a4583e4e8d7c18), [`d3f9f4d`](https://github.com/Effect-TS/effect/commit/d3f9f4d4032b1131c62f4ddb21a4583e4e8d7c18), [`d3f9f4d`](https://github.com/Effect-TS/effect/commit/d3f9f4d4032b1131c62f4ddb21a4583e4e8d7c18), [`70dde23`](https://github.com/Effect-TS/effect/commit/70dde238f81125e353fd7bde5fc24ecd8969bf97), [`a34dbdc`](https://github.com/Effect-TS/effect/commit/a34dbdc1552c73c1b612676f262a0c735ce444a7), [`d3f9f4d`](https://github.com/Effect-TS/effect/commit/d3f9f4d4032b1131c62f4ddb21a4583e4e8d7c18), [`81b7425`](https://github.com/Effect-TS/effect/commit/81b7425320cbbe2a6cf547a3e3ab3549cdba14cf), [`02c3461`](https://github.com/Effect-TS/effect/commit/02c34615d02f91269ea04036d0306fccf4e39e18), [`0e56e99`](https://github.com/Effect-TS/effect/commit/0e56e998ab9815c4d096c239a553cb86a0f99af9), [`8b0ded9`](https://github.com/Effect-TS/effect/commit/8b0ded9f10ba0d96fcb9af24eff2dbd9341f85e3), [`8dd83e8`](https://github.com/Effect-TS/effect/commit/8dd83e854bfcaa6dab876994c5f813dcfb486c28), [`5127afe`](https://github.com/Effect-TS/effect/commit/5127afec1c519e0a3d7460844a9101a96272f29e), [`d75f6fe`](https://github.com/Effect-TS/effect/commit/d75f6fe6499deb0a5ee9ec94af3b5fd4eb03a2d0), [`7356e5c`](https://github.com/Effect-TS/effect/commit/7356e5cc16e9d70f18c02dee1dcb4ad539fd130a), [`3077cde`](https://github.com/Effect-TS/effect/commit/3077cde08a60246821a940964a84dd7f7c8b9f54), [`be19ce0`](https://github.com/Effect-TS/effect/commit/be19ce0b8bdf1fac80bb8d7e0b06a86986b47409), [`4a5d01a`](https://github.com/Effect-TS/effect/commit/4a5d01a409e9b6dd53893e65f8e5c9247f568021), [`78f47ab`](https://github.com/Effect-TS/effect/commit/78f47abfe3cb0a8bbde818b1c5fc603270538b47), [`52e5d20`](https://github.com/Effect-TS/effect/commit/52e5d2077582bf51f25861c7139fc920c2c24166), [`c6137ec`](https://github.com/Effect-TS/effect/commit/c6137ec62c6b5542d5062ae1a3c936cb915dee22), [`f5ae081`](https://github.com/Effect-TS/effect/commit/f5ae08195e68e76faeac258c565d79da4e01e7d6), [`4a5d01a`](https://github.com/Effect-TS/effect/commit/4a5d01a409e9b6dd53893e65f8e5c9247f568021), [`60686f5`](https://github.com/Effect-TS/effect/commit/60686f5c38bef1b93a3a0dda9b6596d46aceab03), [`9a2d1c1`](https://github.com/Effect-TS/effect/commit/9a2d1c1468ea0789b34767ad683da074f061ea9c), [`5127afe`](https://github.com/Effect-TS/effect/commit/5127afec1c519e0a3d7460844a9101a96272f29e), [`56b8691`](https://github.com/Effect-TS/effect/commit/56b86916bf3da18002f3655d859dbc487eb5a6de), [`8ee2931`](https://github.com/Effect-TS/effect/commit/8ee293159b4f7cb7af8558287a0a047f3a69743d), [`6727474`](https://github.com/Effect-TS/effect/commit/672747497490a30d36dd49c06db19aabf09dc7f0), [`5127afe`](https://github.com/Effect-TS/effect/commit/5127afec1c519e0a3d7460844a9101a96272f29e)]: + - effect@2.3.0 + - @effect/schema@0.62.0 + +## 0.43.11 + +### Patch Changes + +- Updated dependencies [[`3ddfdbf`](https://github.com/Effect-TS/effect/commit/3ddfdbf914edea536aef207cec6695f33496258c), [`3ddfdbf`](https://github.com/Effect-TS/effect/commit/3ddfdbf914edea536aef207cec6695f33496258c)]: + - effect@2.2.5 + - @effect/schema@0.61.7 + +## 0.43.10 + +### Patch Changes + +- [#2057](https://github.com/Effect-TS/effect/pull/2057) [`6928a2b`](https://github.com/Effect-TS/effect/commit/6928a2b0bae86a4bdfbece0aa32924207c2d5a70) Thanks [@joepjoosten](https://github.com/joepjoosten)! - Fix for possible stack overflow errors when using Array.push with spread operator arguments + +- Updated dependencies [[`d0b911c`](https://github.com/Effect-TS/effect/commit/d0b911c75f284c7aa87f25aa96926e6bde7690d0), [`330e1a4`](https://github.com/Effect-TS/effect/commit/330e1a4e2c1fc0af6c80c80c81dd38c3e50fab78), [`6928a2b`](https://github.com/Effect-TS/effect/commit/6928a2b0bae86a4bdfbece0aa32924207c2d5a70), [`296bc1c`](https://github.com/Effect-TS/effect/commit/296bc1c9d24986d299d2669115d584cb27b73c60)]: + - effect@2.2.4 + - @effect/schema@0.61.6 + +## 0.43.9 + +### Patch Changes + +- [#2039](https://github.com/Effect-TS/effect/pull/2039) [`1b841a9`](https://github.com/Effect-TS/effect/commit/1b841a91fed86825cd2867cf1e68e41d8ff26b4e) Thanks [@tim-smart](https://github.com/tim-smart)! - fix ClientRequest.make signature (generic was unused) + +## 0.43.8 + +### Patch Changes + +- [#2037](https://github.com/Effect-TS/effect/pull/2037) [`32bf796`](https://github.com/Effect-TS/effect/commit/32bf796c3e5db1b2b68e8b1b20db664295991643) Thanks [@tim-smart](https://github.com/tim-smart)! - remove overloads from ClientRequest.make + + This makes it easier to programatically create client request instances: + + ``` + import * as Http from "@effect/platform/HttpClient" + + declare const method: "GET" | "POST" + declare const url: string + + Http.request.make(method)(url) + ``` + +## 0.43.7 + +### Patch Changes + +- [#2020](https://github.com/Effect-TS/effect/pull/2020) [`cde08f3`](https://github.com/Effect-TS/effect/commit/cde08f354ed2ff2921d1d98bd539c7d65a2ddd73) Thanks [@tim-smart](https://github.com/tim-smart)! - use Proxy for platform schema Transferable + +## 0.43.6 + +### Patch Changes + +- [#2016](https://github.com/Effect-TS/effect/pull/2016) [`c96bb17`](https://github.com/Effect-TS/effect/commit/c96bb17043e2cec1eaeb319614a4c2904d876beb) Thanks [@thewilkybarkid](https://github.com/thewilkybarkid)! - Support URL objects in client requests + +## 0.43.5 + +### Patch Changes + +- Updated dependencies [[`f1ff44b`](https://github.com/Effect-TS/effect/commit/f1ff44b58cdb1886b38681e8fedc309eb9ac6853), [`13785cf`](https://github.com/Effect-TS/effect/commit/13785cf4a5082d8d9cf8d7c991141dee0d2b4d31)]: + - @effect/schema@0.61.5 + +## 0.43.4 + +### Patch Changes + +- [#1999](https://github.com/Effect-TS/effect/pull/1999) [`78f5921`](https://github.com/Effect-TS/effect/commit/78f59211502ded6fcbe15a49d6fde941cccc9d52) Thanks [@tim-smart](https://github.com/tim-smart)! - ensure forked fibers are interruptible + +- Updated dependencies [[`22794e0`](https://github.com/Effect-TS/effect/commit/22794e0ba00e40281f30a22fa84412003c24877d), [`f73e6c0`](https://github.com/Effect-TS/effect/commit/f73e6c033fb0729a9cfa5eb4bc39f79d3126e247), [`6bf02c7`](https://github.com/Effect-TS/effect/commit/6bf02c70fe10a04d1b34d6666f95416e42a6225a)]: + - effect@2.2.3 + - @effect/schema@0.61.4 + +## 0.43.3 + +### Patch Changes + +- Updated dependencies [[`9863e2f`](https://github.com/Effect-TS/effect/commit/9863e2fb3561dc019965aeccd6584a418fc8b401)]: + - @effect/schema@0.61.3 + +## 0.43.2 + +### Patch Changes + +- Updated dependencies [[`64f710a`](https://github.com/Effect-TS/effect/commit/64f710aa49dec6ffcd33ee23438d0774f5489733)]: + - @effect/schema@0.61.2 + +## 0.43.1 + +### Patch Changes + +- Updated dependencies [[`c7550f9`](https://github.com/Effect-TS/effect/commit/c7550f96e1006eee832ce5025bf0c197a65935ea), [`8d1f6e4`](https://github.com/Effect-TS/effect/commit/8d1f6e4bb13e221804fb1762ef19e02bcefc8f61), [`d404561`](https://github.com/Effect-TS/effect/commit/d404561e47ec2fa5f68709a308ee5d2ee959141d), [`7b84a3c`](https://github.com/Effect-TS/effect/commit/7b84a3c7e4b9c8dc02294b0e3cc3ae3becea977b), [`1a84dee`](https://github.com/Effect-TS/effect/commit/1a84dee0e9ddbfaf2610e4d7c00c7020c427171a), [`ac30bf4`](https://github.com/Effect-TS/effect/commit/ac30bf4cd53de0663784f65ae6bee8279333df97)]: + - @effect/schema@0.61.1 + - effect@2.2.2 + +## 0.43.0 + +### Minor Changes + +- [#1922](https://github.com/Effect-TS/effect/pull/1922) [`62b40e8`](https://github.com/Effect-TS/effect/commit/62b40e8479371d6663c0255aaca56a1ae0d59764) Thanks [@gcanti](https://github.com/gcanti)! - add context tracking to Schema, closes #1873 + +### Patch Changes + +- Updated dependencies [[`84da31f`](https://github.com/Effect-TS/effect/commit/84da31f0643e8651b9d311b30526b1e4edfbdfb8), [`62b40e8`](https://github.com/Effect-TS/effect/commit/62b40e8479371d6663c0255aaca56a1ae0d59764), [`645bea2`](https://github.com/Effect-TS/effect/commit/645bea2551129f94a5b0e38347e28067dee531bb), [`62b40e8`](https://github.com/Effect-TS/effect/commit/62b40e8479371d6663c0255aaca56a1ae0d59764)]: + - effect@2.2.1 + - @effect/schema@0.61.0 + +## 0.42.7 + +### Patch Changes + +- [#1959](https://github.com/Effect-TS/effect/pull/1959) [`fe05ad7`](https://github.com/Effect-TS/effect/commit/fe05ad7bcb3b88d47800ab69ebf53641023676f1) Thanks [@tim-smart](https://github.com/tim-smart)! - fix ClientRequest stream bodies + +- Updated dependencies [[`202befc`](https://github.com/Effect-TS/effect/commit/202befc2ecbeb117c4fa85ef9b12a3d3a48273d2), [`ee4ff8a`](https://github.com/Effect-TS/effect/commit/ee4ff8a943141fcf2877af92c5877ee87a989fb9), [`ee4ff8a`](https://github.com/Effect-TS/effect/commit/ee4ff8a943141fcf2877af92c5877ee87a989fb9), [`ee4ff8a`](https://github.com/Effect-TS/effect/commit/ee4ff8a943141fcf2877af92c5877ee87a989fb9), [`ee4ff8a`](https://github.com/Effect-TS/effect/commit/ee4ff8a943141fcf2877af92c5877ee87a989fb9), [`10df798`](https://github.com/Effect-TS/effect/commit/10df798639e556f9d88265ef7fc3cf8a3bbe3874)]: + - effect@2.2.0 + - @effect/schema@0.60.7 + +## 0.42.6 + +### Patch Changes + +- Updated dependencies [[`21b9edd`](https://github.com/Effect-TS/effect/commit/21b9edde464f7c5624ef54ad1b5e264204a37625)]: + - effect@2.1.2 + - @effect/schema@0.60.6 + +## 0.42.5 + +### Patch Changes + +- Updated dependencies [[`3bf67cf`](https://github.com/Effect-TS/effect/commit/3bf67cf64ff27ffaa811b07751875cb161ac3385)]: + - @effect/schema@0.60.5 + +## 0.42.4 + +### Patch Changes + +- Updated dependencies [[`0d1af1e`](https://github.com/Effect-TS/effect/commit/0d1af1e38c11b94e152beaccd0ff7569a1b3f5b7), [`0d1af1e`](https://github.com/Effect-TS/effect/commit/0d1af1e38c11b94e152beaccd0ff7569a1b3f5b7), [`a222524`](https://github.com/Effect-TS/effect/commit/a2225247e9de2e013d287320790fde88c081dbbd)]: + - @effect/schema@0.60.4 + - effect@2.1.1 + +## 0.42.3 + +### Patch Changes + +- Updated dependencies [[`d543221`](https://github.com/Effect-TS/effect/commit/d5432213e91ab620aa66e0fd92a6593134d18940), [`2530d47`](https://github.com/Effect-TS/effect/commit/2530d470b0ad5df7e636921eedfb1cbe42821f94), [`f493929`](https://github.com/Effect-TS/effect/commit/f493929ab88d2ea137ca5fbff70bdc6c9d804d80), [`5911fa9`](https://github.com/Effect-TS/effect/commit/5911fa9c9440dd3bc1ee38542bcd15f8c75a4637)]: + - @effect/schema@0.60.3 + +## 0.42.2 + +### Patch Changes + +- [#1919](https://github.com/Effect-TS/effect/pull/1919) [`05c44b3`](https://github.com/Effect-TS/effect/commit/05c44b30662554dde50b70bad79f13ae895fda02) Thanks [@github-actions](https://github.com/apps/github-actions)! - Improve Effect.retry options + +- Updated dependencies [[`05c44b3`](https://github.com/Effect-TS/effect/commit/05c44b30662554dde50b70bad79f13ae895fda02), [`05c44b3`](https://github.com/Effect-TS/effect/commit/05c44b30662554dde50b70bad79f13ae895fda02), [`05c44b3`](https://github.com/Effect-TS/effect/commit/05c44b30662554dde50b70bad79f13ae895fda02), [`05c44b3`](https://github.com/Effect-TS/effect/commit/05c44b30662554dde50b70bad79f13ae895fda02), [`05c44b3`](https://github.com/Effect-TS/effect/commit/05c44b30662554dde50b70bad79f13ae895fda02), [`05c44b3`](https://github.com/Effect-TS/effect/commit/05c44b30662554dde50b70bad79f13ae895fda02)]: + - effect@2.1.0 + - @effect/schema@0.60.2 + +## 0.42.1 + +### Patch Changes + +- Updated dependencies [[`f7f19f6`](https://github.com/Effect-TS/effect/commit/f7f19f66a5fa349baa2412c1f9f15111c437df09)]: + - effect@2.0.5 + - @effect/schema@0.60.1 + +## 0.42.0 + +### Minor Changes + +- [#1895](https://github.com/Effect-TS/effect/pull/1895) [`48a3d40`](https://github.com/Effect-TS/effect/commit/48a3d40aed0f923f567b8911dade732ff472d981) Thanks [@tim-smart](https://github.com/tim-smart)! - make worker initial message type safe + +### Patch Changes + +- Updated dependencies [[`ec2bdfa`](https://github.com/Effect-TS/effect/commit/ec2bdfae2da717f28147b9d6820d3494cb240945), [`687e02e`](https://github.com/Effect-TS/effect/commit/687e02e7d84dc06957844160761fda90929470ab), [`536c1df`](https://github.com/Effect-TS/effect/commit/536c1dfb7833961dfb2fbd6bcd2dbdfa2f208d51), [`8eec87e`](https://github.com/Effect-TS/effect/commit/8eec87e311ce55281a98517e6df0ef103b43e8a8), [`540b294`](https://github.com/Effect-TS/effect/commit/540b2941dd0a81e9688311583ce7e2e140d6e7a5), [`8eec87e`](https://github.com/Effect-TS/effect/commit/8eec87e311ce55281a98517e6df0ef103b43e8a8), [`536c1df`](https://github.com/Effect-TS/effect/commit/536c1dfb7833961dfb2fbd6bcd2dbdfa2f208d51), [`a3f96d6`](https://github.com/Effect-TS/effect/commit/a3f96d615b8b3e238dbfa01ef713c87e6f4532be), [`0c397e7`](https://github.com/Effect-TS/effect/commit/0c397e762008a0de40c7526c9d99ff2cfe4f7a6a), [`8eec87e`](https://github.com/Effect-TS/effect/commit/8eec87e311ce55281a98517e6df0ef103b43e8a8), [`b557a10`](https://github.com/Effect-TS/effect/commit/b557a10b773e321bea77fc4951f0ef171dd193c9), [`536c1df`](https://github.com/Effect-TS/effect/commit/536c1dfb7833961dfb2fbd6bcd2dbdfa2f208d51), [`8eec87e`](https://github.com/Effect-TS/effect/commit/8eec87e311ce55281a98517e6df0ef103b43e8a8), [`8eec87e`](https://github.com/Effect-TS/effect/commit/8eec87e311ce55281a98517e6df0ef103b43e8a8), [`74b9094`](https://github.com/Effect-TS/effect/commit/74b90940e571c73a6b76cafa88ffb8a1c949cb4c), [`337e80f`](https://github.com/Effect-TS/effect/commit/337e80f69bc36966f889c439b819db2f84cae496), [`25adce7`](https://github.com/Effect-TS/effect/commit/25adce7ae76ce834096dca1ed70a60ad1a349217), [`536c1df`](https://github.com/Effect-TS/effect/commit/536c1dfb7833961dfb2fbd6bcd2dbdfa2f208d51)]: + - @effect/schema@0.60.0 + - effect@2.0.4 + +## 0.41.0 + +### Minor Changes + +- [#1885](https://github.com/Effect-TS/effect/pull/1885) [`1d3a06b`](https://github.com/Effect-TS/effect/commit/1d3a06bb58ad1ac123ae8f9d42b4345f9c9c53c0) Thanks [@mikearnaldi](https://github.com/mikearnaldi)! - lift worker shutdown to /platform implementation + +### Patch Changes + +- [#1885](https://github.com/Effect-TS/effect/pull/1885) [`1d3a06b`](https://github.com/Effect-TS/effect/commit/1d3a06bb58ad1ac123ae8f9d42b4345f9c9c53c0) Thanks [@mikearnaldi](https://github.com/mikearnaldi)! - Avoid killing all fibers on interrupt + +- Updated dependencies [[`5b46e99`](https://github.com/Effect-TS/effect/commit/5b46e996d30e2497eb23095e2c21eee04438edf5), [`87f7ef2`](https://github.com/Effect-TS/effect/commit/87f7ef28a3c27e2e4f2fcfa465f85bb2a45a3d6b), [`210d27e`](https://github.com/Effect-TS/effect/commit/210d27e999e066ea9b907301150c65f9ff080b39), [`1d3a06b`](https://github.com/Effect-TS/effect/commit/1d3a06bb58ad1ac123ae8f9d42b4345f9c9c53c0)]: + - @effect/schema@0.59.1 + - effect@2.0.3 + +## 0.40.4 + +### Patch Changes + +- Updated dependencies [[`c4b84f7`](https://github.com/Effect-TS/effect/commit/c4b84f724ae809f3450d71c3ea5d629205fc479f), [`c4b84f7`](https://github.com/Effect-TS/effect/commit/c4b84f724ae809f3450d71c3ea5d629205fc479f), [`c4b84f7`](https://github.com/Effect-TS/effect/commit/c4b84f724ae809f3450d71c3ea5d629205fc479f), [`c4b84f7`](https://github.com/Effect-TS/effect/commit/c4b84f724ae809f3450d71c3ea5d629205fc479f)]: + - @effect/schema@0.59.0 + +## 0.40.3 + +### Patch Changes + +- [#1879](https://github.com/Effect-TS/effect/pull/1879) [`92c0322`](https://github.com/Effect-TS/effect/commit/92c0322a58bf7e5b8dbb602186030839e89df5af) Thanks [@tim-smart](https://github.com/tim-smart)! - add http Multiplex module + +- Updated dependencies [[`7b2f874`](https://github.com/Effect-TS/effect/commit/7b2f8743d96753c3e24ac4cc6715a4a7f4a2ca0c), [`a904a73`](https://github.com/Effect-TS/effect/commit/a904a739459bfd0fa7844b00b902d2fa984fb014), [`7b2f874`](https://github.com/Effect-TS/effect/commit/7b2f8743d96753c3e24ac4cc6715a4a7f4a2ca0c), [`7b2f874`](https://github.com/Effect-TS/effect/commit/7b2f8743d96753c3e24ac4cc6715a4a7f4a2ca0c), [`7b2f874`](https://github.com/Effect-TS/effect/commit/7b2f8743d96753c3e24ac4cc6715a4a7f4a2ca0c), [`7b2f874`](https://github.com/Effect-TS/effect/commit/7b2f8743d96753c3e24ac4cc6715a4a7f4a2ca0c)]: + - @effect/schema@0.58.0 + +## 0.40.2 + +### Patch Changes + +- [#1870](https://github.com/Effect-TS/effect/pull/1870) [`4c90c54`](https://github.com/Effect-TS/effect/commit/4c90c54d87c91f75f3ad114926cdf3b0c25df091) Thanks [@tim-smart](https://github.com/tim-smart)! - support context propogation in platform workers + +- [#1869](https://github.com/Effect-TS/effect/pull/1869) [`d3d3bda`](https://github.com/Effect-TS/effect/commit/d3d3bda74c794153def9027e0c40896e72cd5d14) Thanks [@tim-smart](https://github.com/tim-smart)! - don't add Transferable to schema types + +- Updated dependencies [[`d5a1949`](https://github.com/Effect-TS/effect/commit/d5a19499aac7c1d147674a35ac69992177c7536c)]: + - effect@2.0.2 + - @effect/schema@0.57.2 + +## 0.40.1 + +### Patch Changes + +- Updated dependencies [[`16bd87d`](https://github.com/Effect-TS/effect/commit/16bd87d32611b966dc42ea4fc979764f97a49071)]: + - effect@2.0.1 + - @effect/schema@0.57.1 + +## 0.40.0 + +### Minor Changes + +- [`d0471ca`](https://github.com/Effect-TS/effect/commit/d0471ca7b544746674b9e1750202da72b0a21233) Thanks [@mikearnaldi](https://github.com/mikearnaldi)! - Switch to monorepo structure + +- [#1846](https://github.com/Effect-TS/effect/pull/1846) [`693b8f3`](https://github.com/Effect-TS/effect/commit/693b8f3a3dfd43ae61f0d9292cdf356be7329f2f) Thanks [@fubhy](https://github.com/fubhy)! - Enabled `exactOptionalPropertyTypes` throughout + +### Patch Changes + +- [`d987daa`](https://github.com/Effect-TS/effect/commit/d987daafaddd43b6ade74916a08236c19ea0a9fa) Thanks [@mikearnaldi](https://github.com/mikearnaldi)! - Switch effect dependency to caret + +- [#1848](https://github.com/Effect-TS/effect/pull/1848) [`04fb8b4`](https://github.com/Effect-TS/effect/commit/04fb8b428b19bba85a2c79910c5e363340d074e7) Thanks [@fubhy](https://github.com/fubhy)! - Avoid default parameter initilization + +- [#1847](https://github.com/Effect-TS/effect/pull/1847) [`bcf0900`](https://github.com/Effect-TS/effect/commit/bcf0900b58f449262556f80bff21e771a37272aa) Thanks [@fubhy](https://github.com/fubhy)! - Avoid inline creation & spreading of objects and arrays + +- [#1799](https://github.com/Effect-TS/effect/pull/1799) [`c0aeb5e`](https://github.com/Effect-TS/effect/commit/c0aeb5e302869bcd7d7627f8cc5b630d07c12d10) Thanks [@tim-smart](https://github.com/tim-smart)! - add Route to RouteContext + +- Updated dependencies [[`d987daa`](https://github.com/Effect-TS/effect/commit/d987daafaddd43b6ade74916a08236c19ea0a9fa), [`7b5eaa3`](https://github.com/Effect-TS/effect/commit/7b5eaa3838c79bf4bdccf91b94d61bbc38a2ec95), [`0724211`](https://github.com/Effect-TS/effect/commit/072421149c36010748ff6b6ee19c15c6cffefe09), [`9f2bc5a`](https://github.com/Effect-TS/effect/commit/9f2bc5a19e0b678a0a85e84daac290922b0fd57d), [`04fb8b4`](https://github.com/Effect-TS/effect/commit/04fb8b428b19bba85a2c79910c5e363340d074e7), [`d0471ca`](https://github.com/Effect-TS/effect/commit/d0471ca7b544746674b9e1750202da72b0a21233), [`bcf0900`](https://github.com/Effect-TS/effect/commit/bcf0900b58f449262556f80bff21e771a37272aa), [`6299b84`](https://github.com/Effect-TS/effect/commit/6299b84c11e5d1fe79fa538df8935018c7613747)]: + - @effect/schema@0.57.0 + - effect@2.0.0 + +## 0.39.0 + +### Minor Changes + +- [#369](https://github.com/Effect-TS/platform/pull/369) [`5d5f62b`](https://github.com/Effect-TS/platform/commit/5d5f62b03ffdbca0a986d968e1dbb45886dfa827) Thanks [@tim-smart](https://github.com/tim-smart)! - rename server FormData module to Multipart + +- [#372](https://github.com/Effect-TS/platform/pull/372) [`15784c9`](https://github.com/Effect-TS/platform/commit/15784c920dcae40f328bb45ac850987135207365) Thanks [@tim-smart](https://github.com/tim-smart)! - update dependencies + +- [#373](https://github.com/Effect-TS/platform/pull/373) [`b042ba5`](https://github.com/Effect-TS/platform/commit/b042ba5ae78a1eed592e543c233fe3040d6a60da) Thanks [@tim-smart](https://github.com/tim-smart)! - update effect + +- [#371](https://github.com/Effect-TS/platform/pull/371) [`49fb154`](https://github.com/Effect-TS/platform/commit/49fb15439f18701321db8ded839243b9dd8de71a) Thanks [@tim-smart](https://github.com/tim-smart)! - rename schemaBodyMultipartJson to schemaBodyFormJson & support url forms + +## 0.38.0 + +### Minor Changes + +- [#367](https://github.com/Effect-TS/platform/pull/367) [`7d1584b`](https://github.com/Effect-TS/platform/commit/7d1584b23d464651c206201ff304c6eb4bebfc3a) Thanks [@tim-smart](https://github.com/tim-smart)! - update effect + +## 0.37.8 + +### Patch Changes + +- [#363](https://github.com/Effect-TS/platform/pull/363) [`e2c545a`](https://github.com/Effect-TS/platform/commit/e2c545a328c2bccbba661540a8835b10bce4b438) Thanks [@tim-smart](https://github.com/tim-smart)! - fix schemaNoBody type + +- [#366](https://github.com/Effect-TS/platform/pull/366) [`1d6bf73`](https://github.com/Effect-TS/platform/commit/1d6bf730dad0a6bbb282f436ec7d5870de76ca3a) Thanks [@tim-smart](https://github.com/tim-smart)! - add Scope to every http request + +- [#365](https://github.com/Effect-TS/platform/pull/365) [`3351136`](https://github.com/Effect-TS/platform/commit/335113601c238104eb2e331d26b5e463bde80dff) Thanks [@tim-smart](https://github.com/tim-smart)! - respond with 503 on server induced interrupt + +## 0.37.7 + +### Patch Changes + +- [#361](https://github.com/Effect-TS/platform/pull/361) [`df3af6b`](https://github.com/Effect-TS/platform/commit/df3af6be61572bab15004bbca2c5739d8206f3c3) Thanks [@tim-smart](https://github.com/tim-smart)! - fix headers type for schemaJson + +## 0.37.6 + +### Patch Changes + +- [#359](https://github.com/Effect-TS/platform/pull/359) [`6dbc587`](https://github.com/Effect-TS/platform/commit/6dbc587868d2703ad9a4c9995cb9dacdfc29c364) Thanks [@tim-smart](https://github.com/tim-smart)! - use branded type for Headers + +- [#359](https://github.com/Effect-TS/platform/pull/359) [`6dbc587`](https://github.com/Effect-TS/platform/commit/6dbc587868d2703ad9a4c9995cb9dacdfc29c364) Thanks [@tim-smart](https://github.com/tim-smart)! - change UrlParams to ReadonlyArray + +## 0.37.5 + +### Patch Changes + +- [#354](https://github.com/Effect-TS/platform/pull/354) [`190bc84`](https://github.com/Effect-TS/platform/commit/190bc84b137a729a38b6812e220085b3d12cb124) Thanks [@tim-smart](https://github.com/tim-smart)! - add Layer support to SerializedWorker + +## 0.37.4 + +### Patch Changes + +- [#352](https://github.com/Effect-TS/platform/pull/352) [`1c02a35`](https://github.com/Effect-TS/platform/commit/1c02a35df2f34601b547e17ddeab98236e10f77d) Thanks [@tim-smart](https://github.com/tim-smart)! - interrupt all fibers on worker interrupt + +- [#352](https://github.com/Effect-TS/platform/pull/352) [`1c02a35`](https://github.com/Effect-TS/platform/commit/1c02a35df2f34601b547e17ddeab98236e10f77d) Thanks [@tim-smart](https://github.com/tim-smart)! - interrupt workers on all failures + +## 0.37.3 + +### Patch Changes + +- [#350](https://github.com/Effect-TS/platform/pull/350) [`b30e5e3`](https://github.com/Effect-TS/platform/commit/b30e5e3874f22037f92253037fff6952f537ee40) Thanks [@tim-smart](https://github.com/tim-smart)! - add decode option to worker runner + +## 0.37.2 + +### Patch Changes + +- [#348](https://github.com/Effect-TS/platform/pull/348) [`28edc60`](https://github.com/Effect-TS/platform/commit/28edc60d2fcd30160529c677a9ffd786775e534b) Thanks [@tim-smart](https://github.com/tim-smart)! - add layer worker runner apis + +## 0.37.1 + +### Patch Changes + +- [#344](https://github.com/Effect-TS/platform/pull/344) [`5b7cdbd`](https://github.com/Effect-TS/platform/commit/5b7cdbdf8ded48903a9f39df800fd7a22f73f0f7) Thanks [@tim-smart](https://github.com/tim-smart)! - support error and output transfers in worker runners + +- [#344](https://github.com/Effect-TS/platform/pull/344) [`5b7cdbd`](https://github.com/Effect-TS/platform/commit/5b7cdbdf8ded48903a9f39df800fd7a22f73f0f7) Thanks [@tim-smart](https://github.com/tim-smart)! - support initialMessage in workers + +- [#344](https://github.com/Effect-TS/platform/pull/344) [`5b7cdbd`](https://github.com/Effect-TS/platform/commit/5b7cdbdf8ded48903a9f39df800fd7a22f73f0f7) Thanks [@tim-smart](https://github.com/tim-smart)! - add Schema transforms to Transferable + +- [#344](https://github.com/Effect-TS/platform/pull/344) [`5b7cdbd`](https://github.com/Effect-TS/platform/commit/5b7cdbdf8ded48903a9f39df800fd7a22f73f0f7) Thanks [@tim-smart](https://github.com/tim-smart)! - make worker encoding return Effects + +## 0.37.0 + +### Minor Changes + +- [#341](https://github.com/Effect-TS/platform/pull/341) [`649f57f`](https://github.com/Effect-TS/platform/commit/649f57fdf557eed5f8405a4a4553dfc47fd8d4b1) Thanks [@tim-smart](https://github.com/tim-smart)! - use peer deps for /platform-\* + +- [#341](https://github.com/Effect-TS/platform/pull/341) [`649f57f`](https://github.com/Effect-TS/platform/commit/649f57fdf557eed5f8405a4a4553dfc47fd8d4b1) Thanks [@tim-smart](https://github.com/tim-smart)! - replace http router with find-my-way-ts + +## 0.36.0 + +### Minor Changes + +- [#338](https://github.com/Effect-TS/platform/pull/338) [`7eaa8e5`](https://github.com/Effect-TS/platform/commit/7eaa8e52b18d408688e7b4909bcf016b0c04e80a) Thanks [@tim-smart](https://github.com/tim-smart)! - change http serve api to return immediately + +- [#338](https://github.com/Effect-TS/platform/pull/338) [`7eaa8e5`](https://github.com/Effect-TS/platform/commit/7eaa8e52b18d408688e7b4909bcf016b0c04e80a) Thanks [@tim-smart](https://github.com/tim-smart)! - Http.server.serve now returns a Layer + +### Patch Changes + +- [#338](https://github.com/Effect-TS/platform/pull/338) [`7eaa8e5`](https://github.com/Effect-TS/platform/commit/7eaa8e52b18d408688e7b4909bcf016b0c04e80a) Thanks [@tim-smart](https://github.com/tim-smart)! - add Http.server.serveEffect + +## 0.35.0 + +### Minor Changes + +- [#335](https://github.com/Effect-TS/platform/pull/335) [`4f0166e`](https://github.com/Effect-TS/platform/commit/4f0166ee2241bd9b71739c98d428b5809313e46e) Thanks [@tim-smart](https://github.com/tim-smart)! - remove index module from /platform + +### Patch Changes + +- [#335](https://github.com/Effect-TS/platform/pull/335) [`4f0166e`](https://github.com/Effect-TS/platform/commit/4f0166ee2241bd9b71739c98d428b5809313e46e) Thanks [@tim-smart](https://github.com/tim-smart)! - add SerializedWorker + +## 0.34.0 + +### Minor Changes + +- [#331](https://github.com/Effect-TS/platform/pull/331) [`db1ca18`](https://github.com/Effect-TS/platform/commit/db1ca18725f9dd4be1c36ddc80faa3aa53c10eb7) Thanks [@tim-smart](https://github.com/tim-smart)! - update effect + +## 0.33.1 + +### Patch Changes + +- [#326](https://github.com/Effect-TS/platform/pull/326) [`162aa91`](https://github.com/Effect-TS/platform/commit/162aa915934112983c543a6be2a9d7091b86fac9) Thanks [@tim-smart](https://github.com/tim-smart)! - add Router.schemaSearchParams/schemaPathParams + +## 0.33.0 + +### Minor Changes + +- [#321](https://github.com/Effect-TS/platform/pull/321) [`16a5bca`](https://github.com/Effect-TS/platform/commit/16a5bca2bd4aed570ce95233a0e47350010d031f) Thanks [@tim-smart](https://github.com/tim-smart)! - update effect + +### Patch Changes + +- [#319](https://github.com/Effect-TS/platform/pull/319) [`425365e`](https://github.com/Effect-TS/platform/commit/425365ebc40c52a6e2a4bff865c3a982ce74f4ed) Thanks [@IMax153](https://github.com/IMax153)! - add Terminal.readLine to read input line-by-line from the terminal + +- [#319](https://github.com/Effect-TS/platform/pull/319) [`425365e`](https://github.com/Effect-TS/platform/commit/425365ebc40c52a6e2a4bff865c3a982ce74f4ed) Thanks [@IMax153](https://github.com/IMax153)! - make Terminal.columns an Effect to account for resizing the terminal + +## 0.32.2 + +### Patch Changes + +- [#312](https://github.com/Effect-TS/platform/pull/312) [`cc1f588`](https://github.com/Effect-TS/platform/commit/cc1f5886bf4188e0128b64b9e2a67f789680cab0) Thanks [@tim-smart](https://github.com/tim-smart)! - scope commands to prevent process leaks + +## 0.32.1 + +### Patch Changes + +- [#310](https://github.com/Effect-TS/platform/pull/310) [`14239fb`](https://github.com/Effect-TS/platform/commit/14239fb11ae45db1a02d9ba883d0412a9c9e6343) Thanks [@tim-smart](https://github.com/tim-smart)! - update effect + +## 0.32.0 + +### Minor Changes + +- [#307](https://github.com/Effect-TS/platform/pull/307) [`746f969`](https://github.com/Effect-TS/platform/commit/746f9692e2f7133dcb413e0eea08ac7b6b97a9bd) Thanks [@tim-smart](https://github.com/tim-smart)! - update effect + +### Patch Changes + +- [#304](https://github.com/Effect-TS/platform/pull/304) [`92e56a1`](https://github.com/Effect-TS/platform/commit/92e56a1f844f28f26621a1887cc4da045039066d) Thanks [@tim-smart](https://github.com/tim-smart)! - allow fetch function to be replaced + +- [#304](https://github.com/Effect-TS/platform/pull/304) [`92e56a1`](https://github.com/Effect-TS/platform/commit/92e56a1f844f28f26621a1887cc4da045039066d) Thanks [@tim-smart](https://github.com/tim-smart)! - add HttpClient.mapInputRequest apis + +## 0.31.2 + +### Patch Changes + +- [#298](https://github.com/Effect-TS/platform/pull/298) [`7a46ec6`](https://github.com/Effect-TS/platform/commit/7a46ec679e2d4718919c407d0c6c5f0fdc35e62d) Thanks [@tim-smart](https://github.com/tim-smart)! - add .toWebHandler\* to Http/App + +## 0.31.1 + +### Patch Changes + +- [#292](https://github.com/Effect-TS/platform/pull/292) [`b712491`](https://github.com/Effect-TS/platform/commit/b71249168eb4623de8dbd28cd0102be688f5caa3) Thanks [@tim-smart](https://github.com/tim-smart)! - add ability to disable http tracer with predicate + +## 0.31.0 + +### Minor Changes + +- [#291](https://github.com/Effect-TS/platform/pull/291) [`5a677f1`](https://github.com/Effect-TS/platform/commit/5a677f1062d7373e21839dfa51db26beef15dca4) Thanks [@tim-smart](https://github.com/tim-smart)! - update effect + +### Patch Changes + +- [#289](https://github.com/Effect-TS/platform/pull/289) [`624855f`](https://github.com/Effect-TS/platform/commit/624855f635162b2c1232429253477d0805e02657) Thanks [@tim-smart](https://github.com/tim-smart)! - update deps + +## 0.30.6 + +### Patch Changes + +- [#287](https://github.com/Effect-TS/platform/pull/287) [`d5d0932`](https://github.com/Effect-TS/platform/commit/d5d093219cde4f51afb9251d9ba4270fc70be0c1) Thanks [@tim-smart](https://github.com/tim-smart)! - expose ServerResponse.setStatus + +## 0.30.5 + +### Patch Changes + +- [#277](https://github.com/Effect-TS/platform/pull/277) [`36e449c`](https://github.com/Effect-TS/platform/commit/36e449c95fab80dc54505cef2071dcbecce35b4f) Thanks [@tim-smart](https://github.com/tim-smart)! - wait for ready latch in worker + +## 0.30.4 + +### Patch Changes + +- [#275](https://github.com/Effect-TS/platform/pull/275) [`e28989e`](https://github.com/Effect-TS/platform/commit/e28989ebd1813cec7ce68f7dd8718f2254e05cad) Thanks [@tim-smart](https://github.com/tim-smart)! - add stack to WorkerError + +## 0.30.3 + +### Patch Changes + +- [#272](https://github.com/Effect-TS/platform/pull/272) [`1a055ac`](https://github.com/Effect-TS/platform/commit/1a055ac959faf12e9c57768b20babea12b1f7d2d) Thanks [@tim-smart](https://github.com/tim-smart)! - add WorkerError to send api + +## 0.30.2 + +### Patch Changes + +- [#270](https://github.com/Effect-TS/platform/pull/270) [`3257fd5`](https://github.com/Effect-TS/platform/commit/3257fd52016af5a38c135de5f0aa33aac7de2538) Thanks [@tim-smart](https://github.com/tim-smart)! - update multipasta + +## 0.30.1 + +### Patch Changes + +- [#268](https://github.com/Effect-TS/platform/pull/268) [`58f5ccc`](https://github.com/Effect-TS/platform/commit/58f5ccc07d74abe6820debc0179665e5ef76b5c4) Thanks [@tim-smart](https://github.com/tim-smart)! - update deps + +## 0.30.0 + +### Minor Changes + +- [#267](https://github.com/Effect-TS/platform/pull/267) [`3d38b40`](https://github.com/Effect-TS/platform/commit/3d38b40a939e32c6c0e8b62dd53a844a6f389182) Thanks [@tim-smart](https://github.com/tim-smart)! - update effect + +## 0.29.1 + +### Patch Changes + +- [#263](https://github.com/Effect-TS/platform/pull/263) [`2bbe692`](https://github.com/Effect-TS/platform/commit/2bbe6928aa5e6929e58877ba236547310bca7e2b) Thanks [@tim-smart](https://github.com/tim-smart)! - fix fieldMimeTypes fiber ref + +## 0.29.0 + +### Minor Changes + +- [#250](https://github.com/Effect-TS/platform/pull/250) [`6e18090`](https://github.com/Effect-TS/platform/commit/6e18090db4686cd5564ab9dc3d8771d7b3ad97fa) Thanks [@tim-smart](https://github.com/tim-smart)! - updated FormData model and apis + +## 0.28.4 + +### Patch Changes + +- [#260](https://github.com/Effect-TS/platform/pull/260) [`8f5e6a2`](https://github.com/Effect-TS/platform/commit/8f5e6a2f2ced4408b0b311b0456828855e1cb958) Thanks [@IMax153](https://github.com/IMax153)! - expose available terminal columns from the Terminal service + +## 0.28.3 + +### Patch Changes + +- [#258](https://github.com/Effect-TS/platform/pull/258) [`9f79c1f`](https://github.com/Effect-TS/platform/commit/9f79c1f5278e60b3bcbd59f08e20189bcb25a84e) Thanks [@IMax153](https://github.com/IMax153)! - fix context identifier for Terminal service + +## 0.28.2 + +### Patch Changes + +- [#255](https://github.com/Effect-TS/platform/pull/255) [`fea76da`](https://github.com/Effect-TS/platform/commit/fea76da05190a65912911bd5b6f9cc0bef3b2edc) Thanks [@IMax153](https://github.com/IMax153)! - add basic Terminal interface for prompting user input + +## 0.28.1 + +### Patch Changes + +- [#253](https://github.com/Effect-TS/platform/pull/253) [`43d2e29`](https://github.com/Effect-TS/platform/commit/43d2e2984fe88b39e907f45f089206ed88ad52d1) Thanks [@fubhy](https://github.com/fubhy)! - Update dependencies + +## 0.28.0 + +### Minor Changes + +- [#251](https://github.com/Effect-TS/platform/pull/251) [`05fef78`](https://github.com/Effect-TS/platform/commit/05fef784ac975059fb6335576feadc7f34644314) Thanks [@fubhy](https://github.com/fubhy)! - Re-added exports for http module + +## 0.27.4 + +### Patch Changes + +- [#248](https://github.com/Effect-TS/platform/pull/248) [`8a4b1c1`](https://github.com/Effect-TS/platform/commit/8a4b1c14808d9815eb93a5b10d8a5b26c4dd027b) Thanks [@IMax153](https://github.com/IMax153)! - allow for specifying that a Command should be run in a shell + +## 0.27.3 + +### Patch Changes + +- [#243](https://github.com/Effect-TS/platform/pull/243) [`1ac0a42`](https://github.com/Effect-TS/platform/commit/1ac0a4208184ef1d23d5ad41a7f0e32bc4d80d85) Thanks [@tim-smart](https://github.com/tim-smart)! - fix worker interruption + +## 0.27.2 + +### Patch Changes + +- [#241](https://github.com/Effect-TS/platform/pull/241) [`e2aa7cd`](https://github.com/Effect-TS/platform/commit/e2aa7cd606a735809fbf79327cfebc009e89d84d) Thanks [@tim-smart](https://github.com/tim-smart)! - decrease bun worker close timeout + +## 0.27.1 + +### Patch Changes + +- [#239](https://github.com/Effect-TS/platform/pull/239) [`4d94b9d`](https://github.com/Effect-TS/platform/commit/4d94b9d30adba2bf4f6f6e1d4cd735e6362667c5) Thanks [@tim-smart](https://github.com/tim-smart)! - update effect + +## 0.27.0 + +### Minor Changes + +- [#237](https://github.com/Effect-TS/platform/pull/237) [`1f79ed6`](https://github.com/Effect-TS/platform/commit/1f79ed6b4d2ee9ae2b59c4536854566c579e77c4) Thanks [@tim-smart](https://github.com/tim-smart)! - update effect + +## 0.26.7 + +### Patch Changes + +- [#235](https://github.com/Effect-TS/platform/pull/235) [`6e14c02`](https://github.com/Effect-TS/platform/commit/6e14c02db668f380bb92f19037685fe40592a8fe) Thanks [@tim-smart](https://github.com/tim-smart)! - fix for hanging worker shutdown + +## 0.26.6 + +### Patch Changes + +- [#233](https://github.com/Effect-TS/platform/pull/233) [`71947e0`](https://github.com/Effect-TS/platform/commit/71947e0e0aa9dccf9aad6f63dd98a6b6c89f23b4) Thanks [@tim-smart](https://github.com/tim-smart)! - fix worker scope hanging on close + +## 0.26.5 + +### Patch Changes + +- [#231](https://github.com/Effect-TS/platform/pull/231) [`a3cbba4`](https://github.com/Effect-TS/platform/commit/a3cbba4a0fa0f1ef99a6d7e54f5ab46c6813ef00) Thanks [@tim-smart](https://github.com/tim-smart)! - add onCreate and broadcast to pool options + +## 0.26.4 + +### Patch Changes + +- [#229](https://github.com/Effect-TS/platform/pull/229) [`4661a8c`](https://github.com/Effect-TS/platform/commit/4661a8c63a13cc6630d5f3cbac90f4ff1d096e09) Thanks [@tim-smart](https://github.com/tim-smart)! - type worker runner success as never + +- [#229](https://github.com/Effect-TS/platform/pull/229) [`4661a8c`](https://github.com/Effect-TS/platform/commit/4661a8c63a13cc6630d5f3cbac90f4ff1d096e09) Thanks [@tim-smart](https://github.com/tim-smart)! - disable worker pool scaling + +## 0.26.3 + +### Patch Changes + +- [#227](https://github.com/Effect-TS/platform/pull/227) [`abb6baa`](https://github.com/Effect-TS/platform/commit/abb6baa61346580f97d2ab91b84a7342b5becc60) Thanks [@patroza](https://github.com/patroza)! - feat: cache the reading of text/urlParamsBody/formData bodies so they can be reused + +## 0.26.2 + +### Patch Changes + +- [#219](https://github.com/Effect-TS/platform/pull/219) [`f37f58c`](https://github.com/Effect-TS/platform/commit/f37f58ca21c1d5dfedc40c01cde0ffbc954d7e32) Thanks [@tim-smart](https://github.com/tim-smart)! - fix encode / transfers for effect workers + +## 0.26.1 + +### Patch Changes + +- [#217](https://github.com/Effect-TS/platform/pull/217) [`7471ac1`](https://github.com/Effect-TS/platform/commit/7471ac139f3c6867cd0d228ec54e88abd1384f5c) Thanks [@tim-smart](https://github.com/tim-smart)! - add encode option to Worker & WorkerRunner + +## 0.26.0 + +### Minor Changes + +- [#215](https://github.com/Effect-TS/platform/pull/215) [`59da2a6`](https://github.com/Effect-TS/platform/commit/59da2a6877e219b2ca0433aeeecab4ad7487816b) Thanks [@tim-smart](https://github.com/tim-smart)! - seperate request processing in http client + +## 0.25.1 + +### Patch Changes + +- [#213](https://github.com/Effect-TS/platform/pull/213) [`38a49eb`](https://github.com/Effect-TS/platform/commit/38a49eb6ea99ef773007a98ec262898207c8f3c7) Thanks [@tim-smart](https://github.com/tim-smart)! - update effect + +## 0.25.0 + +### Minor Changes + +- [#211](https://github.com/Effect-TS/platform/pull/211) [`9ec45cb`](https://github.com/Effect-TS/platform/commit/9ec45cba6b7d5016079ccad9357934f12afe8750) Thanks [@tim-smart](https://github.com/tim-smart)! - update effect + +## 0.24.0 + +### Minor Changes + +- [#209](https://github.com/Effect-TS/platform/pull/209) [`9c51aa1`](https://github.com/Effect-TS/platform/commit/9c51aa18beb7fd34023863ca069d3dde372765d8) Thanks [@tim-smart](https://github.com/tim-smart)! - update effect + +## 0.23.1 + +### Patch Changes + +- [#206](https://github.com/Effect-TS/platform/pull/206) [`b47639b`](https://github.com/Effect-TS/platform/commit/b47639b1df021beb075469921e9ef7a08c174555) Thanks [@tim-smart](https://github.com/tim-smart)! - small stream improvements + +- [#208](https://github.com/Effect-TS/platform/pull/208) [`41f8a65`](https://github.com/Effect-TS/platform/commit/41f8a650238bfbac5b8e18d58a431c3605b71aa5) Thanks [@tim-smart](https://github.com/tim-smart)! - add Http.middleware.withLoggerDisabled + +## 0.23.0 + +### Minor Changes + +- [#204](https://github.com/Effect-TS/platform/pull/204) [`ee0c08f`](https://github.com/Effect-TS/platform/commit/ee0c08fd9828eae32696da1bde33d50a3ad9edf3) Thanks [@tim-smart](https://github.com/tim-smart)! - update effect + +## 0.22.1 + +### Patch Changes + +- [#194](https://github.com/Effect-TS/platform/pull/194) [`79b71d8`](https://github.com/Effect-TS/platform/commit/79b71d8cb3aa6520b2dcb7930850b423174e04b2) Thanks [@tim-smart](https://github.com/tim-smart)! - add Worker & WorkerRunner modules + +## 0.22.0 + +### Minor Changes + +- [#199](https://github.com/Effect-TS/platform/pull/199) [`1e94b15`](https://github.com/Effect-TS/platform/commit/1e94b1588e51df20f9c4fc4871b246048751506c) Thanks [@tim-smart](https://github.com/tim-smart)! - enable tracing by default + +## 0.21.0 + +### Minor Changes + +- [#193](https://github.com/Effect-TS/platform/pull/193) [`9ec4b1d`](https://github.com/Effect-TS/platform/commit/9ec4b1d284caa1c4f19a58c46ed7c25fb10d39a5) Thanks [@tim-smart](https://github.com/tim-smart)! - update effect + +### Patch Changes + +- [#191](https://github.com/Effect-TS/platform/pull/191) [`2711aea`](https://github.com/Effect-TS/platform/commit/2711aea855936c82c282e61fbc6d2f1a1ab6778a) Thanks [@tim-smart](https://github.com/tim-smart)! - update effect + +## 0.20.0 + +### Minor Changes + +- [#189](https://github.com/Effect-TS/platform/pull/189) [`b07f8cd`](https://github.com/Effect-TS/platform/commit/b07f8cd50ef44d577aa981a532025aedb364df13) Thanks [@tim-smart](https://github.com/tim-smart)! - update effect + +## 0.19.0 + +### Minor Changes + +- [#184](https://github.com/Effect-TS/platform/pull/184) [`903b599`](https://github.com/Effect-TS/platform/commit/903b5995bb407c399846e6b75e47e53098b2c80d) Thanks [@tim-smart](https://github.com/tim-smart)! - update effect + +### Patch Changes + +- [#186](https://github.com/Effect-TS/platform/pull/186) [`a3bcda4`](https://github.com/Effect-TS/platform/commit/a3bcda4c2c6655ab86769cca60bece5eb64f866e) Thanks [@tim-smart](https://github.com/tim-smart)! - add pre response handlers to http + +## 0.18.7 + +### Patch Changes + +- [#179](https://github.com/Effect-TS/platform/pull/179) [`843488f`](https://github.com/Effect-TS/platform/commit/843488f79b253518f131693faf2955f5c795a1bc) Thanks [@tim-smart](https://github.com/tim-smart)! - update effect + +## 0.18.6 + +### Patch Changes + +- [#177](https://github.com/Effect-TS/platform/pull/177) [`7e4e2a5`](https://github.com/Effect-TS/platform/commit/7e4e2a5d815c677e4eb6adb2c6e9369414a79384) Thanks [@tim-smart](https://github.com/tim-smart)! - add ClientResponse.schemaNoBody + +- [#175](https://github.com/Effect-TS/platform/pull/175) [`d1c2b38`](https://github.com/Effect-TS/platform/commit/d1c2b38cbb1189249c0bfd47582e00ff771428e3) Thanks [@tim-smart](https://github.com/tim-smart)! - make ServerResponse an Effect + +## 0.18.5 + +### Patch Changes + +- [#171](https://github.com/Effect-TS/platform/pull/171) [`fbbcaa9`](https://github.com/Effect-TS/platform/commit/fbbcaa9b1d4f48f204072a802fb11bcb29813664) Thanks [@tim-smart](https://github.com/tim-smart)! - remove preserveModules patch for preconstruct + +## 0.18.4 + +### Patch Changes + +- [#169](https://github.com/Effect-TS/platform/pull/169) [`bd8778d`](https://github.com/Effect-TS/platform/commit/bd8778d1a534f28cab4b326bb25c086fafed8101) Thanks [@tim-smart](https://github.com/tim-smart)! - fix nested modules + +## 0.18.3 + +### Patch Changes + +- [#167](https://github.com/Effect-TS/platform/pull/167) [`7027589`](https://github.com/Effect-TS/platform/commit/7027589d6dde621065eb8834a2b1ba4d3adc943b) Thanks [@tim-smart](https://github.com/tim-smart)! - build with preconstruct + +## 0.18.2 + +### Patch Changes + +- [#165](https://github.com/Effect-TS/platform/pull/165) [`7e3a741`](https://github.com/Effect-TS/platform/commit/7e3a74197325566df47f9b4459e518eea0762d13) Thanks [@fubhy](https://github.com/fubhy)! - Fix peer deps version range + +## 0.18.1 + +### Patch Changes + +- [#163](https://github.com/Effect-TS/platform/pull/163) [`c957232`](https://github.com/Effect-TS/platform/commit/c9572328ee37f44e93e933da622b21df414bf5c6) Thanks [@tim-smart](https://github.com/tim-smart)! - update effect + +## 0.18.0 + +### Minor Changes + +- [#160](https://github.com/Effect-TS/platform/pull/160) [`c2dc0ab`](https://github.com/Effect-TS/platform/commit/c2dc0abb20b073fd19e38b4e61a08b1edee0f37f) Thanks [@fubhy](https://github.com/fubhy)! - update to effect package + +## 0.17.1 + +### Patch Changes + +- [#158](https://github.com/Effect-TS/platform/pull/158) [`9b10bf3`](https://github.com/Effect-TS/platform/commit/9b10bf394106ba0bafd8440dc0b3fba30a5cc1ea) Thanks [@tim-smart](https://github.com/tim-smart)! - add client transform apis + +## 0.17.0 + +### Minor Changes + +- [#156](https://github.com/Effect-TS/platform/pull/156) [`e6c4101`](https://github.com/Effect-TS/platform/commit/e6c41011e5420d90c543dd25d87036d4150f3e85) Thanks [@tim-smart](https://github.com/tim-smart)! - update dependencies + +## 0.16.1 + +### Patch Changes + +- [#148](https://github.com/Effect-TS/platform/pull/148) [`492f0e7`](https://github.com/Effect-TS/platform/commit/492f0e700e939ded6ff17eeca4d50a9e1ce59219) Thanks [@tim-smart](https://github.com/tim-smart)! - add IncomingMessage.remoteAddress + +## 0.16.0 + +### Minor Changes + +- [#145](https://github.com/Effect-TS/platform/pull/145) [`d0522be`](https://github.com/Effect-TS/platform/commit/d0522be6f824571d83be8c6aa16a3d7caa1b3447) Thanks [@tim-smart](https://github.com/tim-smart)! - update dependencies + +### Patch Changes + +- [#144](https://github.com/Effect-TS/platform/pull/144) [`6583ad4`](https://github.com/Effect-TS/platform/commit/6583ad4ef5b718620c873208bb11196d35733034) Thanks [@tim-smart](https://github.com/tim-smart)! - b3 header propagation in http client and server + +## 0.15.2 + +### Patch Changes + +- [#131](https://github.com/Effect-TS/platform/pull/131) [`06e27ce`](https://github.com/Effect-TS/platform/commit/06e27ce29553ea8d0a234b941fa1de1a51996fbf) Thanks [@jessekelly881](https://github.com/jessekelly881)! - add Clipboard module to /platform-browser + +## 0.15.1 + +### Patch Changes + +- [#138](https://github.com/Effect-TS/platform/pull/138) [`2b2f658`](https://github.com/Effect-TS/platform/commit/2b2f6583a7e589a4c7ab8c22bec390ef755f54c3) Thanks [@tim-smart](https://github.com/tim-smart)! - remove Router.WithoutProvided + +## 0.15.0 + +### Minor Changes + +- [#135](https://github.com/Effect-TS/platform/pull/135) [`99f2a49`](https://github.com/Effect-TS/platform/commit/99f2a49c614a5b80646f6600a170609fe7e38025) Thanks [@tim-smart](https://github.com/tim-smart)! - update dependencies + +## 0.14.1 + +### Patch Changes + +- [#133](https://github.com/Effect-TS/platform/pull/133) [`1d2c403`](https://github.com/Effect-TS/platform/commit/1d2c4033af11f18ba09f53dcfdf8b3fc399bd22f) Thanks [@tim-smart](https://github.com/tim-smart)! - add http platform abstraction + +- [#133](https://github.com/Effect-TS/platform/pull/133) [`1d2c403`](https://github.com/Effect-TS/platform/commit/1d2c4033af11f18ba09f53dcfdf8b3fc399bd22f) Thanks [@tim-smart](https://github.com/tim-smart)! - handle HEAD requests + +## 0.14.0 + +### Minor Changes + +- [#130](https://github.com/Effect-TS/platform/pull/130) [`2713c4f`](https://github.com/Effect-TS/platform/commit/2713c4f766f5493303221772368710a09033658d) Thanks [@tim-smart](https://github.com/tim-smart)! - update dependencies + +## 0.13.16 + +### Patch Changes + +- [#125](https://github.com/Effect-TS/platform/pull/125) [`eb54e53`](https://github.com/Effect-TS/platform/commit/eb54e53d95e7b863d8ffdff9de12b0abd462b217) Thanks [@tim-smart](https://github.com/tim-smart)! - restruture platform-node for platform-bun reuse + +## 0.13.15 + +### Patch Changes + +- [#123](https://github.com/Effect-TS/platform/pull/123) [`07089a8`](https://github.com/Effect-TS/platform/commit/07089a877fd72b2c1b30016f92af162bbb6ff2c8) Thanks [@tim-smart](https://github.com/tim-smart)! - add ClientResponse.schemaJson + +## 0.13.14 + +### Patch Changes + +- [#120](https://github.com/Effect-TS/platform/pull/120) [`9cda8c9`](https://github.com/Effect-TS/platform/commit/9cda8c9ce78d5a9c841a828df20401a0dc07b747) Thanks [@tim-smart](https://github.com/tim-smart)! - add KeyValueStore.SchemaStore + +- [#111](https://github.com/Effect-TS/platform/pull/111) [`6e96703`](https://github.com/Effect-TS/platform/commit/6e96703186f38bd481bffa906e0f99dee89b8e7e) Thanks [@jessekelly881](https://github.com/jessekelly881)! - add KeyValueStore module + +- [#120](https://github.com/Effect-TS/platform/pull/120) [`9cda8c9`](https://github.com/Effect-TS/platform/commit/9cda8c9ce78d5a9c841a828df20401a0dc07b747) Thanks [@tim-smart](https://github.com/tim-smart)! - add KeyValueStore.prefix + +## 0.13.13 + +### Patch Changes + +- [#117](https://github.com/Effect-TS/platform/pull/117) [`ee7e365`](https://github.com/Effect-TS/platform/commit/ee7e365eafd8b62bab5bc32dd94e3f1190f6e7d6) Thanks [@tim-smart](https://github.com/tim-smart)! - add support for File web api to http + +## 0.13.12 + +### Patch Changes + +- [#115](https://github.com/Effect-TS/platform/pull/115) [`4cba795`](https://github.com/Effect-TS/platform/commit/4cba79529426483775782f2384b2194ff57f1279) Thanks [@tim-smart](https://github.com/tim-smart)! - add Router.WithoutProvided + +## 0.13.11 + +### Patch Changes + +- [#113](https://github.com/Effect-TS/platform/pull/113) [`5945805`](https://github.com/Effect-TS/platform/commit/59458051ad3885d23c4657369a9a46015f4e569c) Thanks [@tim-smart](https://github.com/tim-smart)! - try to remove route context and requests from Router context + +## 0.13.10 + +### Patch Changes + +- [#109](https://github.com/Effect-TS/platform/pull/109) [`7031ec0`](https://github.com/Effect-TS/platform/commit/7031ec030a45a306f4fda4d3ed80796f98a7758e) Thanks [@tim-smart](https://github.com/tim-smart)! - remove Body.EffectBody + +## 0.13.9 + +### Patch Changes + +- [#106](https://github.com/Effect-TS/platform/pull/106) [`df3dbcf`](https://github.com/Effect-TS/platform/commit/df3dbcf468d10dca8cdb219478bb0a23bc66da0c) Thanks [@tim-smart](https://github.com/tim-smart)! - add count to http log span + +## 0.13.8 + +### Patch Changes + +- [#99](https://github.com/Effect-TS/platform/pull/99) [`e42c3f5`](https://github.com/Effect-TS/platform/commit/e42c3f5103b7361b5162a3e9280759ecd690295f) Thanks [@tim-smart](https://github.com/tim-smart)! - add ClientRequest.bearerToken + +- [#105](https://github.com/Effect-TS/platform/pull/105) [`127c8f5`](https://github.com/Effect-TS/platform/commit/127c8f50f69d5cf7e4a50241fca70923f71f61a2) Thanks [@tim-smart](https://github.com/tim-smart)! - add more form data limit config + +## 0.13.7 + +### Patch Changes + +- [#97](https://github.com/Effect-TS/platform/pull/97) [`e5c91eb`](https://github.com/Effect-TS/platform/commit/e5c91eb541a6f97cb759ba39732cf08b0ae4c248) Thanks [@tim-smart](https://github.com/tim-smart)! - rename IncomingMessage.urlParams to urlParamsBody + +## 0.13.6 + +### Patch Changes + +- [#94](https://github.com/Effect-TS/platform/pull/94) [`cd3b15e`](https://github.com/Effect-TS/platform/commit/cd3b15e0cb223d2788d383caaa7c0dbc06073dc1) Thanks [@tim-smart](https://github.com/tim-smart)! - only use mime module in ServerResponse + +## 0.13.5 + +### Patch Changes + +- [#92](https://github.com/Effect-TS/platform/pull/92) [`a034383`](https://github.com/Effect-TS/platform/commit/a0343838bad8f37ab7fb6031084a6514103eba2b) Thanks [@tim-smart](https://github.com/tim-smart)! - fix mime import + +## 0.13.4 + +### Patch Changes + +- [#90](https://github.com/Effect-TS/platform/pull/90) [`05d1765`](https://github.com/Effect-TS/platform/commit/05d1765a0606abce8a3c3d026bdcd5d8b3c64936) Thanks [@tim-smart](https://github.com/tim-smart)! - rename Router.transform to Router.use + +- [#89](https://github.com/Effect-TS/platform/pull/89) [`30025cb`](https://github.com/Effect-TS/platform/commit/30025cbd773b4ded89ffdb20a523a4350eb0452e) Thanks [@tim-smart](https://github.com/tim-smart)! - add etag generation for http file responses + +## 0.13.3 + +### Patch Changes + +- [#86](https://github.com/Effect-TS/platform/pull/86) [`6dfc5b0`](https://github.com/Effect-TS/platform/commit/6dfc5b0fbec0e8a057a26c009f19c9951e4b3ba4) Thanks [@tim-smart](https://github.com/tim-smart)! - add router combinators + +- [#88](https://github.com/Effect-TS/platform/pull/88) [`d7fffeb`](https://github.com/Effect-TS/platform/commit/d7fffeb38a1c40ad3847e4e5b966f58939d1ba83) Thanks [@tim-smart](https://github.com/tim-smart)! - remove Middleware.compose + +## 0.13.2 + +### Patch Changes + +- [#83](https://github.com/Effect-TS/platform/pull/83) [`ce5e086`](https://github.com/Effect-TS/platform/commit/ce5e0869390d571d21f854b6c1073bf10136e602) Thanks [@tim-smart](https://github.com/tim-smart)! - update deps + +- [#81](https://github.com/Effect-TS/platform/pull/81) [`c1ec2ba`](https://github.com/Effect-TS/platform/commit/c1ec2bab2b1c134c49a82fd5dbb741b0df3d1cd9) Thanks [@tim-smart](https://github.com/tim-smart)! - use ReadonlyRecord for headers + +- [#83](https://github.com/Effect-TS/platform/pull/83) [`ce5e086`](https://github.com/Effect-TS/platform/commit/ce5e0869390d571d21f854b6c1073bf10136e602) Thanks [@tim-smart](https://github.com/tim-smart)! - performance tweaks + +## 0.13.1 + +### Patch Changes + +- [#79](https://github.com/Effect-TS/platform/pull/79) [`3544c17`](https://github.com/Effect-TS/platform/commit/3544c17f5778ab47cb4019b6458b2543d572629a) Thanks [@TylorS](https://github.com/TylorS)! - Attempt to derive content-type from headers + +## 0.13.0 + +### Minor Changes + +- [#77](https://github.com/Effect-TS/platform/pull/77) [`e97d80b`](https://github.com/Effect-TS/platform/commit/e97d80bd69646195a65ea6dfe13c6af19589d2cf) Thanks [@tim-smart](https://github.com/tim-smart)! - remove Console module + +## 0.12.1 + +### Patch Changes + +- [#75](https://github.com/Effect-TS/platform/pull/75) [`d23ff14`](https://github.com/Effect-TS/platform/commit/d23ff14756796e945307ccfdf65252d47f99b7aa) Thanks [@tim-smart](https://github.com/tim-smart)! - add size helpers + +## 0.12.0 + +### Minor Changes + +- [#71](https://github.com/Effect-TS/platform/pull/71) [`139de2e`](https://github.com/Effect-TS/platform/commit/139de2e18adcf6661609909ec6afd44abe4cb1a9) Thanks [@tim-smart](https://github.com/tim-smart)! - add HttpServer module + +### Patch Changes + +- [#71](https://github.com/Effect-TS/platform/pull/71) [`139de2e`](https://github.com/Effect-TS/platform/commit/139de2e18adcf6661609909ec6afd44abe4cb1a9) Thanks [@tim-smart](https://github.com/tim-smart)! - add SizeInput type + +## 0.11.5 + +### Patch Changes + +- [#69](https://github.com/Effect-TS/platform/pull/69) [`0eb7df0`](https://github.com/Effect-TS/platform/commit/0eb7df0e2cbfb96986c3bbee4650c4036a97b1d2) Thanks [@tim-smart](https://github.com/tim-smart)! - have Command & Client implement Pipeable + +## 0.11.4 + +### Patch Changes + +- [#67](https://github.com/Effect-TS/platform/pull/67) [`c41a166`](https://github.com/Effect-TS/platform/commit/c41a16614bc4daff05956b84a6bcd01cbb5836dd) Thanks [@tim-smart](https://github.com/tim-smart)! - add node implementation of http client + +## 0.11.3 + +### Patch Changes + +- [#64](https://github.com/Effect-TS/platform/pull/64) [`6f2d011`](https://github.com/Effect-TS/platform/commit/6f2d011ce917d74d14b0375525f5c9805f8e44fe) Thanks [@tim-smart](https://github.com/tim-smart)! - fix ClientRequest jsonBody types + +## 0.11.2 + +### Patch Changes + +- [#62](https://github.com/Effect-TS/platform/pull/62) [`3d44256`](https://github.com/Effect-TS/platform/commit/3d442560fee94a0c8f01f936a3f7c5b5e1ac8fc2) Thanks [@tim-smart](https://github.com/tim-smart)! - improve http client options type + +## 0.11.1 + +### Patch Changes + +- [#38](https://github.com/Effect-TS/platform/pull/38) [`f70a121`](https://github.com/Effect-TS/platform/commit/f70a121b2fc9d1052434863c41657d353d21fb26) Thanks [@tim-smart](https://github.com/tim-smart)! - add HttpClient module + +## 0.11.0 + +### Minor Changes + +- [#59](https://github.com/Effect-TS/platform/pull/59) [`b2f7bc0`](https://github.com/Effect-TS/platform/commit/b2f7bc0fe7310d861d52da03fefd9bc91852e5f9) Thanks [@tim-smart](https://github.com/tim-smart)! - update dependencies + +### Patch Changes + +- [#58](https://github.com/Effect-TS/platform/pull/58) [`f61aa57`](https://github.com/Effect-TS/platform/commit/f61aa57a915ee221fdf5259cbaf1e4fe208e01b8) Thanks [@tim-smart](https://github.com/tim-smart)! - update build tools + +- [#56](https://github.com/Effect-TS/platform/pull/56) [`efcf469`](https://github.com/Effect-TS/platform/commit/efcf469da368770b2f321043a8e0e33f079c169b) Thanks [@tim-smart](https://github.com/tim-smart)! - switch to peerDependencies + +## 0.10.4 + +### Patch Changes + +- [#55](https://github.com/Effect-TS/platform/pull/55) [`67caeff`](https://github.com/Effect-TS/platform/commit/67caeffb5343b4ce428aa3c6b393feb383667fef) Thanks [@tim-smart](https://github.com/tim-smart)! - add labels to Tags + +- [#46](https://github.com/Effect-TS/platform/pull/46) [`4a4d0af`](https://github.com/Effect-TS/platform/commit/4a4d0af4832f543fc53b2ba5c9fc9739bbc78f2e) Thanks [@fubhy](https://github.com/fubhy)! - add seek method to file handles + +- [#54](https://github.com/Effect-TS/platform/pull/54) [`b3950e1`](https://github.com/Effect-TS/platform/commit/b3950e1373673ae492106fe0cb76bcd32fbe5a2b) Thanks [@tim-smart](https://github.com/tim-smart)! - add writeFileString + +## 0.10.3 + +### Patch Changes + +- [#51](https://github.com/Effect-TS/platform/pull/51) [`9163d96`](https://github.com/Effect-TS/platform/commit/9163d96717a832e9dbf2bdd262d73034fcbe92e9) Thanks [@tim-smart](https://github.com/tim-smart)! - revert exists change + +## 0.10.2 + +### Patch Changes + +- [#49](https://github.com/Effect-TS/platform/pull/49) [`44eaaf5`](https://github.com/Effect-TS/platform/commit/44eaaf5c182dc70c73b7da9687e9c0a81daea86c) Thanks [@tim-smart](https://github.com/tim-smart)! - fix exists catching wrong error + +## 0.10.1 + +### Patch Changes + +- [#47](https://github.com/Effect-TS/platform/pull/47) [`24b56d5`](https://github.com/Effect-TS/platform/commit/24b56d5d6afa40df072e2db37ebd71df538e66ac) Thanks [@tim-smart](https://github.com/tim-smart)! - add exists and readFileString to FileSystem + +## 0.10.0 + +### Minor Changes + +- [#41](https://github.com/Effect-TS/platform/pull/41) [`68cbdca`](https://github.com/Effect-TS/platform/commit/68cbdca7e9da509c212d44101ab61c3bcf1354ad) Thanks [@tim-smart](https://github.com/tim-smart)! - update /data, /io and /stream + +## 0.9.0 + +### Minor Changes + +- [#39](https://github.com/Effect-TS/platform/pull/39) [`3012e28`](https://github.com/Effect-TS/platform/commit/3012e289272d383fdae16af6b3ba396dec290b77) Thanks [@tim-smart](https://github.com/tim-smart)! - update dependencies + +## 0.8.0 + +### Minor Changes + +- [#36](https://github.com/Effect-TS/platform/pull/36) [`b82cbcc`](https://github.com/Effect-TS/platform/commit/b82cbcc56789c014f0a50c505497239ec220f4fd) Thanks [@tim-smart](https://github.com/tim-smart)! - update dependencies + +## 0.7.0 + +### Minor Changes + +- [#34](https://github.com/Effect-TS/platform/pull/34) [`601d045`](https://github.com/Effect-TS/platform/commit/601d04526ad0a2e3285de509fdf86c7b6809a547) Thanks [@tim-smart](https://github.com/tim-smart)! - update /stream + +## 0.6.0 + +### Minor Changes + +- [#32](https://github.com/Effect-TS/platform/pull/32) [`ee94eae`](https://github.com/Effect-TS/platform/commit/ee94eae46aee327baf0c6960befa6c35154fa35b) Thanks [@tim-smart](https://github.com/tim-smart)! - update /io and /data + +## 0.5.0 + +### Minor Changes + +- [#28](https://github.com/Effect-TS/platform/pull/28) [`f3d73f5`](https://github.com/Effect-TS/platform/commit/f3d73f587ad9b528bb1e37cf44e4928d913f56dd) Thanks [@tim-smart](https://github.com/tim-smart)! - update /io + +## 0.4.0 + +### Minor Changes + +- [#26](https://github.com/Effect-TS/platform/pull/26) [`834e1a7`](https://github.com/Effect-TS/platform/commit/834e1a793365f4deb742814d9cd6df9faae9d0c2) Thanks [@tim-smart](https://github.com/tim-smart)! - update /io and /data + +## 0.3.0 + +### Minor Changes + +- [#22](https://github.com/Effect-TS/platform/pull/22) [`645f10f`](https://github.com/Effect-TS/platform/commit/645f10f6d6a8600e369f068b22f3c2ef5169e867) Thanks [@tim-smart](https://github.com/tim-smart)! - update /io and /data + +## 0.2.0 + +### Minor Changes + +- [#20](https://github.com/Effect-TS/platform/pull/20) [`756ccbe`](https://github.com/Effect-TS/platform/commit/756ccbe002f2e00c02b88aac126c2bc5b17a5769) Thanks [@IMax153](https://github.com/IMax153)! - upgrade to `@effect/data@0.13.5`, `@effect/io@0.31.3`, and `@effect/stream@0.25.1` + +## 0.1.0 + +### Minor Changes + +- [#13](https://github.com/Effect-TS/platform/pull/13) [`b95c25f`](https://github.com/Effect-TS/platform/commit/b95c25f619b8e5ebf915f675f63de01accb1a8b8) Thanks [@tim-smart](https://github.com/tim-smart)! - initial release diff --git a/repos/effect/packages/platform/LICENSE b/repos/effect/packages/platform/LICENSE new file mode 100644 index 0000000..be1f5c1 --- /dev/null +++ b/repos/effect/packages/platform/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Effectful Technologies Inc + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/repos/effect/packages/platform/README.md b/repos/effect/packages/platform/README.md new file mode 100644 index 0000000..8914b2b --- /dev/null +++ b/repos/effect/packages/platform/README.md @@ -0,0 +1,5156 @@ +# Introduction + +Welcome to the documentation for `@effect/platform`, a library designed for creating platform-independent abstractions (Node.js, Bun, browsers). + +> [!WARNING] +> This documentation focuses on **unstable modules**. For stable modules, refer to the [official website documentation](https://effect.website/docs/guides/platform/introduction). + +# Running Your Main Program with runMain + +Docs for `runMain` have been moved to the [official website](https://effect.website/docs/platform/runtime/). + +# HTTP API + +## Overview + +The `HttpApi*` modules offer a flexible and declarative way to define HTTP APIs. + +To define an API, create a set of `HttpEndpoint`s. Each endpoint is described by a path, a method, and schemas for the request and response. + +Collections of endpoints are grouped in an `HttpApiGroup`, and multiple groups can be merged into a complete `HttpApi`. + +``` +HttpApi +├── HttpGroup +│ ├── HttpEndpoint +│ └── HttpEndpoint +└── HttpGroup + ├── HttpEndpoint + ├── HttpEndpoint + └── HttpEndpoint +``` + +Once your API is defined, the same definition can be reused for multiple purposes: + +- **Starting a Server**: Use the API definition to implement and serve endpoints. +- **Generating Documentation**: Create a Swagger page to document the API. +- **Deriving a Client**: Generate a fully-typed client for your API. + +Benefits of a Single API Definition: + +- **Consistency**: A single definition ensures the server, documentation, and client remain aligned. +- **Reduced Maintenance**: Changes to the API are reflected across all related components. +- **Simplified Workflow**: Avoids duplication by consolidating API details in one place. + +## Hello World + +### Defining and Implementing an API + +This example demonstrates how to define and implement a simple API with a single endpoint that returns a string response. The structure of the API is as follows: + +``` +HttpApi ("MyApi) +└── HttpGroup ("Greetings") + └── HttpEndpoint ("hello-world") +``` + +**Example** (Hello World Definition) + +```ts +import { + HttpApi, + HttpApiBuilder, + HttpApiEndpoint, + HttpApiGroup +} from "@effect/platform" +import { NodeHttpServer, NodeRuntime } from "@effect/platform-node" +import { Effect, Layer, Schema } from "effect" +import { createServer } from "node:http" + +// Define our API with one group named "Greetings" and one endpoint called "hello-world" +const MyApi = HttpApi.make("MyApi").add( + HttpApiGroup.make("Greetings").add( + HttpApiEndpoint.get("hello-world")`/`.addSuccess(Schema.String) + ) +) + +// Implement the "Greetings" group +const GreetingsLive = HttpApiBuilder.group(MyApi, "Greetings", (handlers) => + handlers.handle("hello-world", () => Effect.succeed("Hello, World!")) +) + +// Provide the implementation for the API +const MyApiLive = HttpApiBuilder.api(MyApi).pipe(Layer.provide(GreetingsLive)) + +// Set up the server using NodeHttpServer on port 3000 +const ServerLive = HttpApiBuilder.serve().pipe( + Layer.provide(MyApiLive), + Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 })) +) + +// Launch the server +Layer.launch(ServerLive).pipe(NodeRuntime.runMain) +``` + +After running the code, open a browser and navigate to http://localhost:3000. The server will respond with: + +``` +Hello, World! +``` + +### Serving The Auto Generated Swagger Documentation + +You can enhance your API by adding auto-generated Swagger documentation using the `HttpApiSwagger` module. This makes it easier for developers to explore and interact with your API. + +To include Swagger in your server setup, provide the `HttpApiSwagger.layer` when configuring the server. + +**Example** (Serving Swagger Documentation) + +```ts +import { + HttpApi, + HttpApiBuilder, + HttpApiEndpoint, + HttpApiGroup, + HttpApiSwagger +} from "@effect/platform" +import { NodeHttpServer, NodeRuntime } from "@effect/platform-node" +import { Effect, Layer, Schema } from "effect" +import { createServer } from "node:http" + +const MyApi = HttpApi.make("MyApi").add( + HttpApiGroup.make("Greetings").add( + HttpApiEndpoint.get("hello-world")`/`.addSuccess(Schema.String) + ) +) + +const GreetingsLive = HttpApiBuilder.group(MyApi, "Greetings", (handlers) => + handlers.handle("hello-world", () => Effect.succeed("Hello, World!")) +) + +const MyApiLive = HttpApiBuilder.api(MyApi).pipe(Layer.provide(GreetingsLive)) + +const ServerLive = HttpApiBuilder.serve().pipe( + // Provide the Swagger layer so clients can access auto-generated docs + Layer.provide(HttpApiSwagger.layer()), + Layer.provide(MyApiLive), + Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 })) +) + +Layer.launch(ServerLive).pipe(NodeRuntime.runMain) +``` + +After running the server, open your browser and navigate to http://localhost:3000/docs. + +This URL will display the Swagger documentation, allowing you to explore the API's endpoints, request parameters, and response structures interactively. + +![Swagger Documentation](./images/swagger-hello-world.png) + +### Deriving a Client + +Once you have defined your API, you can generate a client to interact with it using the `HttpApiClient` module. This allows you to call your API endpoints without manually handling HTTP requests. + +**Example** (Deriving and Using a Client) + +```ts +import { + FetchHttpClient, + HttpApi, + HttpApiBuilder, + HttpApiClient, + HttpApiEndpoint, + HttpApiGroup, + HttpApiSwagger +} from "@effect/platform" +import { NodeHttpServer, NodeRuntime } from "@effect/platform-node" +import { Effect, Layer, Schema } from "effect" +import { createServer } from "node:http" + +const MyApi = HttpApi.make("MyApi").add( + HttpApiGroup.make("Greetings").add( + HttpApiEndpoint.get("hello-world")`/`.addSuccess(Schema.String) + ) +) + +const GreetingsLive = HttpApiBuilder.group(MyApi, "Greetings", (handlers) => + handlers.handle("hello-world", () => Effect.succeed("Hello, World!")) +) + +const MyApiLive = HttpApiBuilder.api(MyApi).pipe(Layer.provide(GreetingsLive)) + +const ServerLive = HttpApiBuilder.serve().pipe( + Layer.provide(HttpApiSwagger.layer()), + Layer.provide(MyApiLive), + Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 })) +) + +Layer.launch(ServerLive).pipe(NodeRuntime.runMain) + +// Create a program that derives and uses the client +const program = Effect.gen(function* () { + // Derive the client + const client = yield* HttpApiClient.make(MyApi, { + baseUrl: "http://localhost:3000" + }) + // Call the "hello-world" endpoint + const hello = yield* client.Greetings["hello-world"]() + console.log(hello) +}) + +// Provide a Fetch-based HTTP client and run the program +Effect.runFork(program.pipe(Effect.provide(FetchHttpClient.layer))) +// Output: Hello, World! +``` + +## Defining a HttpApiEndpoint + +An `HttpApiEndpoint` represents a single endpoint in your API. Each endpoint is defined with a name, path, HTTP method, and optional schemas for requests and responses. This allows you to describe the structure and behavior of your API. + +Below is an example of a simple CRUD API for managing users, which includes the following endpoints: + +- `GET /users` - Retrieve all users. +- `GET /users/:userId` - Retrieve a specific user by ID. +- `POST /users` - Create a new user. +- `DELETE /users/:userId` - Delete a user by ID. +- `PATCH /users/:userId` - Update a user by ID. + +### GET + +The `HttpApiEndpoint.get` method allows you to define a GET endpoint by specifying its name, path, and optionally, a schema for the response. + +To define the structure of successful responses, use the `.addSuccess` method. If no schema is provided, the default response status is `204 No Content`. + +**Example** (Defining a GET Endpoint to Retrieve All Users) + +```ts +import { HttpApiEndpoint } from "@effect/platform" +import { Schema } from "effect" + +// Define a schema representing a User entity +const User = Schema.Struct({ + id: Schema.Number, + name: Schema.String, + createdAt: Schema.DateTimeUtc +}) + +// Define the "getUsers" endpoint, returning a list of users +const getUsers = HttpApiEndpoint + // ┌─── Endpoint name + // │ ┌─── Endpoint path + // ▼ ▼ + .get("getUsers", "/users") + // Define the success schema for the response (optional). + // If no response schema is specified, the default response is `204 No Content`. + .addSuccess(Schema.Array(User)) +``` + +### Path Parameters + +Path parameters allow you to include dynamic segments in your endpoint's path. There are two ways to define path parameters in your API. + +#### Using setPath + +The `setPath` method allows you to explicitly define path parameters by associating them with a schema. + +**Example** (Defining Parameters with setPath) + +```ts +import { HttpApiEndpoint } from "@effect/platform" +import { Schema } from "effect" + +const User = Schema.Struct({ + id: Schema.Number, + name: Schema.String, + createdAt: Schema.DateTimeUtc +}) + +// Define a GET endpoint with a path parameter ":id" +const getUser = HttpApiEndpoint.get("getUser", "/user/:id") + .setPath( + Schema.Struct({ + // Define a schema for the "id" path parameter + id: Schema.NumberFromString + }) + ) + .addSuccess(User) +``` + +#### Using Template Strings + +You can also define path parameters by embedding them in a template string with the help of `HttpApiSchema.param`. + +**Example** (Defining Parameters using a Template String) + +```ts +import { HttpApiEndpoint, HttpApiSchema } from "@effect/platform" +import { Schema } from "effect" + +const User = Schema.Struct({ + id: Schema.Number, + name: Schema.String, + createdAt: Schema.DateTimeUtc +}) + +// Create a path parameter using HttpApiSchema.param +const idParam = HttpApiSchema.param("id", Schema.NumberFromString) + +// Define the GET endpoint using a template string +const getUser = HttpApiEndpoint.get("getUser")`/user/${idParam}`.addSuccess( + User +) +``` + +### POST + +The `HttpApiEndpoint.post` method is used to define an endpoint for creating resources. You can specify a schema for the request body (payload) and a schema for the successful response. + +**Example** (Defining a POST Endpoint with Payload and Success Schemas) + +```ts +import { HttpApiEndpoint } from "@effect/platform" +import { Schema } from "effect" + +// Define a schema for the user object +const User = Schema.Struct({ + id: Schema.Number, + name: Schema.String, + createdAt: Schema.DateTimeUtc +}) + +// Define a POST endpoint for creating a new user +const createUser = HttpApiEndpoint.post("createUser", "/users") + // Define the request body schema (payload) + .setPayload( + Schema.Struct({ + name: Schema.String + }) + ) + // Define the schema for a successful response + .addSuccess(User) +``` + +### DELETE + +The `HttpApiEndpoint.del` method is used to define an endpoint for deleting a resource. + +**Example** (Defining a DELETE Endpoint with Path Parameters) + +```ts +import { HttpApiEndpoint, HttpApiSchema } from "@effect/platform" +import { Schema } from "effect" + +// Define a path parameter for the user ID +const idParam = HttpApiSchema.param("id", Schema.NumberFromString) + +// Define a DELETE endpoint to delete a user by ID +const deleteUser = HttpApiEndpoint.del("deleteUser")`/users/${idParam}` +``` + +### PATCH + +The `HttpApiEndpoint.patch` method is used to define an endpoint for partially updating a resource. This method allows you to specify a schema for the request payload and a schema for the successful response. + +**Example** (Defining a PATCH Endpoint for Updating a User) + +```ts +import { HttpApiEndpoint, HttpApiSchema } from "@effect/platform" +import { Schema } from "effect" + +// Define a schema for the user object +const User = Schema.Struct({ + id: Schema.Number, + name: Schema.String, + createdAt: Schema.DateTimeUtc +}) + +// Define a path parameter for the user ID +const idParam = HttpApiSchema.param("id", Schema.NumberFromString) + +// Define a PATCH endpoint to update a user's name by ID +const updateUser = HttpApiEndpoint.patch("updateUser")`/users/${idParam}` + // Specify the schema for the request payload + .setPayload( + Schema.Struct({ + name: Schema.String // Only the name can be updated + }) + ) + // Specify the schema for a successful response + .addSuccess(User) +``` + +### Catch-All Endpoints + +The path can also be `"*"` to match any incoming path. This is useful for defining a catch-all endpoint to handle unmatched routes or provide a fallback response. + +**Example** (Defining a Catch-All Endpoint) + +```ts +import { HttpApiEndpoint } from "@effect/platform" + +const catchAll = HttpApiEndpoint.get("catchAll", "*") +``` + +### Setting URL Parameters + +The `setUrlParams` method allows you to define the structure of URL parameters for an endpoint. You can specify the schema for each parameter and include metadata such as descriptions to provide additional context. + +**Example** (Defining URL Parameters with Metadata) + +```ts +import { HttpApiEndpoint } from "@effect/platform" +import { Schema } from "effect" + +const User = Schema.Struct({ + id: Schema.Number, + name: Schema.String, + createdAt: Schema.DateTimeUtc +}) + +const getUsers = HttpApiEndpoint.get("getUsers", "/users") + // Specify the URL parameters schema + .setUrlParams( + Schema.Struct({ + // Parameter "page" for pagination + page: Schema.NumberFromString, + // Parameter "sort" for sorting options with an added description + sort: Schema.String.annotations({ + description: "Sorting criteria (e.g., 'name', 'date')" + }) + }) + ) + .addSuccess(Schema.Array(User)) +``` + +#### Defining an Array of Values for a URL Parameter + +When defining a URL parameter that accepts multiple values, you can use the `Schema.Array` combinator. This allows the parameter to handle an array of items, with each item adhering to a specified schema. + +**Example** (Defining an Array of String Values for a URL Parameter) + +```ts +import { HttpApi, HttpApiEndpoint, HttpApiGroup } from "@effect/platform" +import { Schema } from "effect" + +const api = HttpApi.make("myApi").add( + HttpApiGroup.make("group").add( + HttpApiEndpoint.get("get", "/") + .setUrlParams( + Schema.Struct({ + // Define "a" as an array of strings + a: Schema.Array(Schema.String) + }) + ) + .addSuccess(Schema.String) + ) +) +``` + +You can test this endpoint by passing an array of values in the query string. For example: + +```sh +curl "http://localhost:3000/?a=1&a=2" +``` + +The query string sends two values (`1` and `2`) for the `a` parameter. The server will process and validate these values according to the schema. + +### Status Codes + +By default, the success status code is `200 OK`. You can change it by annotating the schema with a custom status. + +**Example** (Defining a GET Endpoint with a custom status code) + +```ts +import { HttpApiEndpoint } from "@effect/platform" +import { Schema } from "effect" + +const User = Schema.Struct({ + id: Schema.Number, + name: Schema.String, + createdAt: Schema.DateTimeUtc +}) + +const getUsers = HttpApiEndpoint.get("getUsers", "/users") + // Override the default success status + .addSuccess(Schema.Array(User), { status: 206 }) +``` + +### Handling Multipart Requests + +To support file uploads, you can use the `HttpApiSchema.Multipart` API. This allows you to define an endpoint's payload schema as a multipart request, specifying the structure of the data, including file uploads, with the `Multipart` module. + +**Example** (Defining an Endpoint for File Uploads) + +In this example, the `HttpApiSchema.Multipart` function marks the payload as a multipart request. The `files` field uses `Multipart.FilesSchema` to handle uploaded file data automatically. + +```ts +import { HttpApiEndpoint, HttpApiSchema, Multipart } from "@effect/platform" +import { Schema } from "effect" + +const upload = HttpApiEndpoint.post("upload", "/users/upload").setPayload( + // Specify that the payload is a multipart request + HttpApiSchema.Multipart( + Schema.Struct({ + // Define a "files" field to handle file uploads + files: Multipart.FilesSchema + }) + ).addSuccess(Schema.String) +) +``` + +You can test this endpoint by sending a multipart request with a file upload. For example: + +```sh +echo "Sample file content" | curl -X POST -F "files=@-" http://localhost:3000/users/upload +``` + +### Changing the Request Encoding + +By default, API requests are encoded as JSON. If your application requires a different format, you can customize the request encoding using the `HttpApiSchema.withEncoding` method. This allows you to define the encoding type and content type of the request. + +**Example** (Customizing Request Encoding) + +```ts +import { HttpApiEndpoint, HttpApiSchema } from "@effect/platform" +import { Schema } from "effect" + +const createUser = HttpApiEndpoint.post("createUser", "/users") + // Set the request payload as a string encoded with URL parameters + .setPayload( + Schema.Struct({ + a: Schema.String // Parameter "a" must be a string + }) + // Specify the encoding as URL parameters + .pipe(HttpApiSchema.withEncoding({ kind: "UrlParams" })) + ) +``` + +### Changing the Response Encoding + +By default, API responses are encoded as JSON. If your application requires a different format, you can customize the encoding using the `HttpApiSchema.withEncoding` API. This method lets you define the type and content type of the response. + +**Example** (Returning Data as `text/csv`) + +```ts +import { HttpApiEndpoint, HttpApiSchema } from "@effect/platform" +import { Schema } from "effect" + +const csv = HttpApiEndpoint.get("csv")`/users/csv` + // Set the success response as a string with CSV encoding + .addSuccess( + Schema.String.pipe( + HttpApiSchema.withEncoding({ + // Specify the type of the response + kind: "Text", + // Define the content type as text/csv + contentType: "text/csv" + }) + ) + ) +``` + +### Setting Request Headers + +Use `HttpApiEndpoint.setHeaders` to declare a single, cumulative schema that describes all expected request headers. +Provide one struct schema where each header name maps to its validator, and you can attach metadata such as descriptions. + +> [!IMPORTANT] +> All headers are normalized to lowercase. Always use lowercase keys in the headers schema. + +**Example** (Describe and validate custom headers) + +```ts +import { HttpApiEndpoint } from "@effect/platform" +import { Schema } from "effect" + +// Model for successful responses +const User = Schema.Struct({ + id: Schema.Number, + name: Schema.String, + createdAt: Schema.DateTimeUtc +}) + +const getUsers = HttpApiEndpoint.get("getUsers", "/users") + // Describe the headers the endpoint expects + .setHeaders( + // Declare a single struct schema for all headers + // Header keys MUST be lowercase in the schema + Schema.Struct({ + // This header must be a string + "x-api-key": Schema.String, + + // A human-friendly description is useful for generated docs (e.g. OpenAPI) + "x-request-id": Schema.String.annotations({ + description: "Unique identifier for the request" + }) + }) + ) + // Successful response: an array of User + .addSuccess(Schema.Array(User)) +``` + +You can test the endpoint by sending the headers: + +```sh +curl -H "X-API-Key: 1234567890" -H "X-Request-ID: 1234567890" http://localhost:3000/users +``` + +The server validates these headers against the declared schema before handling the request. + +## Defining a HttpApiGroup + +You can group related endpoints under a single entity by using `HttpApiGroup.make`. This can help organize your code and provide a clearer structure for your API. + +**Example** (Creating a Group for User-Related Endpoints) + +```ts +import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema } from "@effect/platform" +import { Schema } from "effect" + +const User = Schema.Struct({ + id: Schema.Number, + name: Schema.String, + createdAt: Schema.DateTimeUtc +}) + +const idParam = HttpApiSchema.param("id", Schema.NumberFromString) + +const getUsers = HttpApiEndpoint.get("getUsers", "/users").addSuccess( + Schema.Array(User) +) + +const getUser = HttpApiEndpoint.get("getUser")`/user/${idParam}`.addSuccess( + User +) + +const createUser = HttpApiEndpoint.post("createUser", "/users") + .setPayload( + Schema.Struct({ + name: Schema.String + }) + ) + .addSuccess(User) + +const deleteUser = HttpApiEndpoint.del("deleteUser")`/users/${idParam}` + +const updateUser = HttpApiEndpoint.patch("updateUser")`/users/${idParam}` + .setPayload( + Schema.Struct({ + name: Schema.String + }) + ) + .addSuccess(User) + +// Group all user-related endpoints +const usersGroup = HttpApiGroup.make("users") + .add(getUsers) + .add(getUser) + .add(createUser) + .add(deleteUser) + .add(updateUser) +``` + +If you would like to create a more opaque type for the group, you can extend `HttpApiGroup` with a class. + +**Example** (Creating a Group with an Opaque Type) + +```ts +// Create an opaque class extending HttpApiGroup +class UsersGroup extends HttpApiGroup.make("users").add(getUsers).add(getUser) { + // Additional endpoints or methods can be added here +} +``` + +## Creating the Top-Level HttpApi + +After defining your groups, you can combine them into one `HttpApi` representing your entire set of endpoints. + +**Example** (Combining Groups into a Top-Level API) + +```ts +import { + HttpApi, + HttpApiEndpoint, + HttpApiGroup, + HttpApiSchema +} from "@effect/platform" +import { Schema } from "effect" + +const User = Schema.Struct({ + id: Schema.Number, + name: Schema.String, + createdAt: Schema.DateTimeUtc +}) + +const idParam = HttpApiSchema.param("id", Schema.NumberFromString) + +const getUsers = HttpApiEndpoint.get("getUsers", "/users").addSuccess( + Schema.Array(User) +) + +const getUser = HttpApiEndpoint.get("getUser")`/user/${idParam}`.addSuccess( + User +) + +const createUser = HttpApiEndpoint.post("createUser", "/users") + .setPayload( + Schema.Struct({ + name: Schema.String + }) + ) + .addSuccess(User) + +const deleteUser = HttpApiEndpoint.del("deleteUser")`/users/${idParam}` + +const updateUser = HttpApiEndpoint.patch("updateUser")`/users/${idParam}` + .setPayload( + Schema.Struct({ + name: Schema.String + }) + ) + .addSuccess(User) + +const usersGroup = HttpApiGroup.make("users") + .add(getUsers) + .add(getUser) + .add(createUser) + .add(deleteUser) + .add(updateUser) + +// Combine the groups into one API +const api = HttpApi.make("myApi").add(usersGroup) + +// Alternatively, create an opaque class for your API +class MyApi extends HttpApi.make("myApi").add(usersGroup) {} +``` + +## Adding errors + +Error responses allow your API to handle different failure scenarios. These responses can be defined at various levels: + +- **Endpoint-level errors**: Use `HttpApiEndpoint.addError` to add errors specific to an endpoint. +- **Group-level errors**: Use `HttpApiGroup.addError` to add errors applicable to all endpoints in a group. +- **API-level errors**: Use `HttpApi.addError` to define errors that apply to every endpoint in the API. + +Group-level and API-level errors are useful for handling shared issues like authentication failures, especially when managed through middleware. + +**Example** (Defining Error Responses for Endpoints and Groups) + +```ts +import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema } from "@effect/platform" +import { Schema } from "effect" + +const User = Schema.Struct({ + id: Schema.Number, + name: Schema.String, + createdAt: Schema.DateTimeUtc +}) + +const idParam = HttpApiSchema.param("id", Schema.NumberFromString) + +// Define error schemas +class UserNotFound extends Schema.TaggedError()( + "UserNotFound", + {} +) {} + +class Unauthorized extends Schema.TaggedError()( + "Unauthorized", + {} +) {} + +const getUsers = HttpApiEndpoint.get("getUsers", "/users").addSuccess( + Schema.Array(User) +) + +const getUser = HttpApiEndpoint.get("getUser")`/user/${idParam}` + .addSuccess(User) + // Add a 404 error response for this endpoint + .addError(UserNotFound, { status: 404 }) + +const usersGroup = HttpApiGroup.make("users") + .add(getUsers) + .add(getUser) + // ...etc... + // Add a 401 error response for the entire group + .addError(Unauthorized, { status: 401 }) +``` + +You can assign multiple error responses to a single endpoint by calling `HttpApiEndpoint.addError` multiple times. This is useful when different types of errors might occur for a single operation. + +**Example** (Adding Multiple Errors to an Endpoint) + +```ts +const deleteUser = HttpApiEndpoint.del("deleteUser")`/users/${idParam}` + // Add a 404 error response for when the user is not found + .addError(UserNotFound, { status: 404 }) + // Add a 401 error response for unauthorized access + .addError(Unauthorized, { status: 401 }) +``` + +### Predefined Empty Error Types + +The `HttpApiError` module provides a set of predefined empty error types that you can use in your endpoints. These error types help standardize common HTTP error responses, such as `404 Not Found` or `401 Unauthorized`. Using these predefined types simplifies error handling and ensures consistency across your API. + +**Example** (Adding a Predefined Error to an Endpoint) + +```ts +import { HttpApiEndpoint, HttpApiError, HttpApiSchema } from "@effect/platform" +import { Schema } from "effect" + +const User = Schema.Struct({ + id: Schema.Number, + name: Schema.String, + createdAt: Schema.DateTimeUtc +}) + +const idParam = HttpApiSchema.param("id", Schema.NumberFromString) + +const getUser = HttpApiEndpoint.get("getUser")`/user/${idParam}` + .addSuccess(User) + .addError(HttpApiError.NotFound) +``` + +| Name | Status | Description | +| --------------------- | ------ | -------------------------------------------------------------------------------------------------- | +| `HttpApiDecodeError` | 400 | Represents an error where the request did not match the expected schema. Includes detailed issues. | +| `BadRequest` | 400 | Indicates that the request was malformed or invalid. | +| `Unauthorized` | 401 | Indicates that authentication is required but missing or invalid. | +| `Forbidden` | 403 | Indicates that the client does not have permission to access the requested resource. | +| `NotFound` | 404 | Indicates that the requested resource could not be found. | +| `MethodNotAllowed` | 405 | Indicates that the HTTP method used is not allowed for the requested resource. | +| `NotAcceptable` | 406 | Indicates that the requested resource cannot be delivered in a format acceptable to the client. | +| `RequestTimeout` | 408 | Indicates that the server timed out waiting for the client request. | +| `Conflict` | 409 | Indicates a conflict in the request, such as conflicting data. | +| `Gone` | 410 | Indicates that the requested resource is no longer available and will not return. | +| `InternalServerError` | 500 | Indicates an unexpected server error occurred. | +| `NotImplemented` | 501 | Indicates that the requested functionality is not implemented on the server. | +| `ServiceUnavailable` | 503 | Indicates that the server is temporarily unavailable, often due to maintenance or overload. | + +## Prefixing + +Prefixes can be added to endpoints, groups, or an entire API to simplify the management of common paths. This is especially useful when defining multiple related endpoints that share a common base URL. + +**Example** (Using Prefixes for Common Path Management) + +```ts +import { HttpApi, HttpApiEndpoint, HttpApiGroup } from "@effect/platform" +import { Schema } from "effect" + +const api = HttpApi.make("api") + .add( + HttpApiGroup.make("group") + .add( + HttpApiEndpoint.get("getRoot", "/") + .addSuccess(Schema.String) + // Prefix for this endpoint + .prefix("/endpointPrefix") + ) + .add(HttpApiEndpoint.get("getA", "/a").addSuccess(Schema.String)) + // Prefix for all endpoints in the group + .prefix("/groupPrefix") + ) + // Prefix for the entire API + .prefix("/apiPrefix") +``` + +## Implementing a Server + +After defining your API, you can implement a server to handle its endpoints. The `HttpApiBuilder` module provides tools to help you connect your API's structure to the logic that serves requests. + +Here, we will create a simple example with a `getUser` endpoint organized within a `users` group. + +**Example** (Defining the `users` Group and API) + +```ts +import { + HttpApi, + HttpApiEndpoint, + HttpApiGroup, + HttpApiSchema +} from "@effect/platform" +import { Schema } from "effect" + +const User = Schema.Struct({ + id: Schema.Number, + name: Schema.String, + createdAt: Schema.DateTimeUtc +}) + +const idParam = HttpApiSchema.param("id", Schema.NumberFromString) + +const usersGroup = HttpApiGroup.make("users").add( + HttpApiEndpoint.get("getUser")`/user/${idParam}`.addSuccess(User) +) + +const api = HttpApi.make("myApi").add(usersGroup) +``` + +### Implementing a HttpApiGroup + +The `HttpApiBuilder.group` API is used to implement a specific group of endpoints within an `HttpApi` definition. It requires the following inputs: + +| Input | Description | +| --------------------------------- | ----------------------------------------------------------------------- | +| The complete `HttpApi` definition | The overall API structure that includes the group you are implementing. | +| The name of the group | The specific group you are focusing on within the API. | +| A function to add handlers | A function that defines how each endpoint in the group is handled. | + +Each endpoint in the group is connected to its logic using the `HttpApiBuilder.handle` method, which maps the endpoint's definition to its corresponding implementation. + +The `HttpApiBuilder.group` API produces a `Layer` that can later be provided to the server implementation. + +**Example** (Implementing a Group with Endpoint Logic) + +```ts +import { + HttpApi, + HttpApiBuilder, + HttpApiEndpoint, + HttpApiGroup, + HttpApiSchema +} from "@effect/platform" +import { DateTime, Effect, Schema } from "effect" + +const User = Schema.Struct({ + id: Schema.Number, + name: Schema.String, + createdAt: Schema.DateTimeUtc +}) + +const idParam = HttpApiSchema.param("id", Schema.NumberFromString) + +const usersGroup = HttpApiGroup.make("users").add( + HttpApiEndpoint.get("getUser")`/user/${idParam}`.addSuccess(User) +) + +const api = HttpApi.make("myApi").add(usersGroup) + +// -------------------------------------------- +// Implementation +// -------------------------------------------- + +// ┌─── Layer> +// ▼ +const usersGroupLive = + // ┌─── The Whole API + // │ ┌─── The Group you are implementing + // ▼ ▼ + HttpApiBuilder.group(api, "users", (handlers) => + handlers.handle( + // ┌─── The Endpoint you are implementing + // ▼ + "getUser", + // Provide the handler logic for the endpoint. + // The parameters & payload are passed to the handler function. + ({ path: { id } }) => + Effect.succeed( + // Return a mock user object with the provided ID + { + id, + name: "John Doe", + createdAt: DateTime.unsafeNow() + } + ) + ) + ) +``` + +Using `HttpApiBuilder.group`, you connect the structure of your API to its logic, enabling you to focus on each endpoint's functionality in isolation. Each handler receives the parameters and payload for the request, making it easy to process input and generate a response. + +### Using Services Inside a HttpApiGroup + +If your handlers need to use services, you can easily integrate them because the `HttpApiBuilder.group` API allows you to return an `Effect`. This ensures that external services can be accessed and utilized directly within your handlers. + +**Example** (Using Services in a Group Implementation) + +```ts +import { + HttpApi, + HttpApiBuilder, + HttpApiEndpoint, + HttpApiGroup, + HttpApiSchema +} from "@effect/platform" +import { Context, Effect, Schema } from "effect" + +const User = Schema.Struct({ + id: Schema.Number, + name: Schema.String, + createdAt: Schema.DateTimeUtc +}) + +const idParam = HttpApiSchema.param("id", Schema.NumberFromString) + +const usersGroup = HttpApiGroup.make("users").add( + HttpApiEndpoint.get("getUser")`/user/${idParam}`.addSuccess(User) +) + +const api = HttpApi.make("myApi").add(usersGroup) + +// -------------------------------------------- +// Implementation +// -------------------------------------------- + +type User = typeof User.Type + +// Define the UsersRepository service +class UsersRepository extends Context.Tag("UsersRepository")< + UsersRepository, + { + readonly findById: (id: number) => Effect.Effect + } +>() {} + +// Implement the `users` group with access to the UsersRepository service +// +// ┌─── Layer, never, UsersRepository> +// ▼ +const usersGroupLive = HttpApiBuilder.group(api, "users", (handlers) => + Effect.gen(function* () { + // Access the UsersRepository service + const repository = yield* UsersRepository + return handlers.handle("getUser", ({ path: { id } }) => + repository.findById(id) + ) + }) +) +``` + +### Implementing a HttpApi + +Once all your groups are implemented, you can create a top-level implementation to combine them into a unified API. This is done using the `HttpApiBuilder.api` API, which generates a `Layer`. You then use `Layer.provide` to include the implementations of all the groups into the top-level `HttpApi`. + +**Example** (Combining Group Implementations into a Top-Level API) + +```ts +import { + HttpApi, + HttpApiBuilder, + HttpApiEndpoint, + HttpApiGroup, + HttpApiSchema +} from "@effect/platform" +import { DateTime, Effect, Layer, Schema } from "effect" + +const User = Schema.Struct({ + id: Schema.Number, + name: Schema.String, + createdAt: Schema.DateTimeUtc +}) + +const idParam = HttpApiSchema.param("id", Schema.NumberFromString) + +const usersGroup = HttpApiGroup.make("users").add( + HttpApiEndpoint.get("getUser")`/user/${idParam}`.addSuccess(User) +) + +const api = HttpApi.make("myApi").add(usersGroup) + +const usersGroupLive = HttpApiBuilder.group(api, "users", (handlers) => + handlers.handle("getUser", ({ path: { id } }) => + Effect.succeed({ + id, + name: "John Doe", + createdAt: DateTime.unsafeNow() + }) + ) +) + +// Combine all group implementations into the top-level API +// +// ┌─── Layer +// ▼ +const MyApiLive = HttpApiBuilder.api(api).pipe(Layer.provide(usersGroupLive)) +``` + +### Serving the API + +You can serve your API using the `HttpApiBuilder.serve` function. This utility builds an `HttpApp` from an `HttpApi` instance and uses an `HttpServer` to handle requests. Middleware can be added to customize or enhance the server's behavior. + +**Example** (Setting Up and Serving an API with Middleware) + +```ts +import { + HttpApi, + HttpApiBuilder, + HttpApiEndpoint, + HttpApiGroup, + HttpApiSchema, + HttpMiddleware, + HttpServer +} from "@effect/platform" +import { NodeHttpServer, NodeRuntime } from "@effect/platform-node" +import { DateTime, Effect, Layer, Schema } from "effect" +import { createServer } from "node:http" + +const User = Schema.Struct({ + id: Schema.Number, + name: Schema.String, + createdAt: Schema.DateTimeUtc +}) + +const idParam = HttpApiSchema.param("id", Schema.NumberFromString) + +const usersGroup = HttpApiGroup.make("users").add( + HttpApiEndpoint.get("getUser")`/user/${idParam}`.addSuccess(User) +) + +const api = HttpApi.make("myApi").add(usersGroup) + +const usersGroupLive = HttpApiBuilder.group(api, "users", (handlers) => + handlers.handle("getUser", ({ path: { id } }) => + Effect.succeed({ + id, + name: "John Doe", + createdAt: DateTime.unsafeNow() + }) + ) +) + +const MyApiLive = HttpApiBuilder.api(api).pipe(Layer.provide(usersGroupLive)) + +// Configure and serve the API +const HttpLive = HttpApiBuilder.serve(HttpMiddleware.logger).pipe( + // Add CORS middleware to handle cross-origin requests + Layer.provide(HttpApiBuilder.middlewareCors()), + // Provide the API implementation + Layer.provide(MyApiLive), + // Log the server's listening address + HttpServer.withLogAddress, + // Set up the Node.js HTTP server + Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 })) +) + +// Launch the server +Layer.launch(HttpLive).pipe(NodeRuntime.runMain) +``` + +### Accessing the HttpServerRequest + +In some cases, you may need to access details about the incoming `HttpServerRequest` within an endpoint handler. The HttpServerRequest module provides access to the request object, allowing you to inspect properties such as the HTTP method or headers. + +**Example** (Accessing the Request Object in a GET Endpoint) + +```ts +import { + HttpApi, + HttpApiBuilder, + HttpApiEndpoint, + HttpApiGroup, + HttpMiddleware, + HttpServer +} from "@effect/platform" +import { NodeHttpServer, NodeRuntime } from "@effect/platform-node" +import { Effect, Layer, Schema } from "effect" +import { createServer } from "node:http" + +const api = HttpApi.make("myApi").add( + HttpApiGroup.make("group").add( + HttpApiEndpoint.get("get", "/").addSuccess(Schema.String) + ) +) + +const groupLive = HttpApiBuilder.group(api, "group", (handlers) => + handlers.handle("get", ({ request }) => + Effect.gen(function* () { + // Log the HTTP method for demonstration purposes + console.log(request.method) + + // Return a response + return "Hello, World!" + }) + ) +) + +const MyApiLive = HttpApiBuilder.api(api).pipe(Layer.provide(groupLive)) + +const HttpLive = HttpApiBuilder.serve(HttpMiddleware.logger).pipe( + Layer.provide(HttpApiBuilder.middlewareCors()), + Layer.provide(MyApiLive), + HttpServer.withLogAddress, + Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 })) +) + +Layer.launch(HttpLive).pipe(NodeRuntime.runMain) +``` + +### Streaming Requests + +Streaming requests allow you to send large or continuous data streams to the server. In this example, we define an API that accepts a stream of binary data and decodes it into a string. + +**Example** (Handling Streaming Requests) + +```ts +import { + HttpApi, + HttpApiBuilder, + HttpApiEndpoint, + HttpApiGroup, + HttpApiSchema, + HttpMiddleware, + HttpServer +} from "@effect/platform" +import { NodeHttpServer, NodeRuntime } from "@effect/platform-node" +import { Effect, Layer, Schema } from "effect" +import { createServer } from "node:http" + +const api = HttpApi.make("myApi").add( + HttpApiGroup.make("group").add( + HttpApiEndpoint.post("acceptStream", "/stream") + // Define the payload as a Uint8Array with a specific encoding + .setPayload( + Schema.Uint8ArrayFromSelf.pipe( + HttpApiSchema.withEncoding({ + kind: "Uint8Array", + contentType: "application/octet-stream" + }) + ) + ) + .addSuccess(Schema.String) + ) +) + +const groupLive = HttpApiBuilder.group(api, "group", (handlers) => + handlers.handle("acceptStream", (req) => + // Decode the incoming binary data into a string + Effect.succeed(new TextDecoder().decode(req.payload)) + ) +) + +const MyApiLive = HttpApiBuilder.api(api).pipe(Layer.provide(groupLive)) + +const HttpLive = HttpApiBuilder.serve(HttpMiddleware.logger).pipe( + Layer.provide(HttpApiBuilder.middlewareCors()), + Layer.provide(MyApiLive), + HttpServer.withLogAddress, + Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 })) +) + +Layer.launch(HttpLive).pipe(NodeRuntime.runMain) +``` + +You can test the streaming request using `curl` or any tool that supports sending binary data. For example: + +```sh +echo "abc" | curl -X POST 'http://localhost:3000/stream' --data-binary @- -H "Content-Type: application/octet-stream" +# Output: abc +``` + +### Streaming Responses + +To handle streaming responses in your API, you can return a raw `HttpServerResponse`. The `HttpServerResponse.stream` function is designed to return a continuous stream of data as the response. + +**Example** (Implementing a Streaming Endpoint) + +```ts +import { + HttpApi, + HttpApiBuilder, + HttpApiEndpoint, + HttpApiGroup, + HttpApiSchema, + HttpMiddleware, + HttpServer, + HttpServerResponse +} from "@effect/platform" +import { NodeHttpServer, NodeRuntime } from "@effect/platform-node" +import { Layer, Schedule, Schema, Stream } from "effect" +import { createServer } from "node:http" + +// Define the API with a single streaming endpoint +const api = HttpApi.make("myApi").add( + HttpApiGroup.make("group").add( + HttpApiEndpoint.get("getStream", "/stream").addSuccess( + Schema.String.pipe( + HttpApiSchema.withEncoding({ + kind: "Text", + contentType: "application/octet-stream" + }) + ) + ) + ) +) + +// Simulate a stream of data +const stream = Stream.make("a", "b", "c").pipe( + Stream.schedule(Schedule.spaced("500 millis")), + Stream.map((s) => new TextEncoder().encode(s)) +) + +const groupLive = HttpApiBuilder.group(api, "group", (handlers) => + handlers.handle("getStream", () => HttpServerResponse.stream(stream)) +) + +const MyApiLive = HttpApiBuilder.api(api).pipe(Layer.provide(groupLive)) + +const HttpLive = HttpApiBuilder.serve(HttpMiddleware.logger).pipe( + Layer.provide(HttpApiBuilder.middlewareCors()), + Layer.provide(MyApiLive), + HttpServer.withLogAddress, + Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 })) +) + +Layer.launch(HttpLive).pipe(NodeRuntime.runMain) +``` + +You can test the streaming response using `curl` or any similar HTTP client that supports streaming: + +```sh +curl 'http://localhost:3000/stream' --no-buffer +``` + +The response will stream data (`a`, `b`, `c`) with a 500ms interval between each item. + +## Middlewares + +### Defining Middleware + +The `HttpApiMiddleware` module allows you to add middleware to your API. Middleware can enhance your API by introducing features like logging, authentication, or additional error handling. + +You can define middleware using the `HttpApiMiddleware.Tag` class, which lets you specify: + +| Option | Description | +| ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `failure` | A schema that describes any errors the middleware might return. | +| `provides` | A `Context.Tag` representing the resource or data the middleware will provide to subsequent handlers. | +| `security` | Definitions from `HttpApiSecurity` that the middleware will implement, such as authentication mechanisms. | +| `optional` | A boolean indicating whether the request should continue if the middleware fails with an expected error. When `optional` is set to `true`, the `provides` and `failure` options do not affect the final error type or handlers. | + +**Example** (Defining a Logger Middleware) + +```ts +import { + HttpApiEndpoint, + HttpApiGroup, + HttpApiMiddleware, + HttpApiSchema +} from "@effect/platform" +import { Schema } from "effect" + +// Define a schema for errors returned by the logger middleware +class LoggerError extends Schema.TaggedError()( + "LoggerError", + {} +) {} + +// Extend the HttpApiMiddleware.Tag class to define the logger middleware tag +class Logger extends HttpApiMiddleware.Tag()("Http/Logger", { + // Optionally define the error schema for the middleware + failure: LoggerError +}) {} + +const User = Schema.Struct({ + id: Schema.Number, + name: Schema.String, + createdAt: Schema.DateTimeUtc +}) + +const idParam = HttpApiSchema.param("id", Schema.NumberFromString) + +const usersGroup = HttpApiGroup.make("users") + .add( + HttpApiEndpoint.get("getUser")`/user/${idParam}` + .addSuccess(User) + // Apply the middleware to a single endpoint + .middleware(Logger) + ) + // Or apply the middleware to the entire group + .middleware(Logger) +``` + +### Implementing HttpApiMiddleware + +Once you have defined your `HttpApiMiddleware`, you can implement it as a `Layer`. This allows the middleware to be applied to specific API groups or endpoints, enabling modular and reusable behavior. + +**Example** (Implementing and Using Logger Middleware) + +```ts +import { HttpApiMiddleware, HttpServerRequest } from "@effect/platform" +import { Effect, Layer } from "effect" + +class Logger extends HttpApiMiddleware.Tag()("Http/Logger") {} + +const LoggerLive = Layer.effect( + Logger, + Effect.gen(function* () { + yield* Effect.log("creating Logger middleware") + + // Middleware implementation as an Effect + // that can access the `HttpServerRequest` context. + return Effect.gen(function* () { + const request = yield* HttpServerRequest.HttpServerRequest + yield* Effect.log(`Request: ${request.method} ${request.url}`) + }) + }) +) +``` + +After implementing the middleware, you can attach it to your API groups or specific endpoints using the `Layer` APIs. + +```ts +import { + HttpApi, + HttpApiBuilder, + HttpApiEndpoint, + HttpApiGroup, + HttpApiMiddleware, + HttpApiSchema, + HttpServerRequest +} from "@effect/platform" +import { DateTime, Effect, Layer, Schema } from "effect" + +// Define a schema for errors returned by the logger middleware +class LoggerError extends Schema.TaggedError()( + "LoggerError", + {} +) {} + +// Extend the HttpApiMiddleware.Tag class to define the logger middleware tag +class Logger extends HttpApiMiddleware.Tag()("Http/Logger", { + // Optionally define the error schema for the middleware + failure: LoggerError +}) {} + +const LoggerLive = Layer.effect( + Logger, + Effect.gen(function* () { + yield* Effect.log("creating Logger middleware") + + // Middleware implementation as an Effect + // that can access the `HttpServerRequest` context. + return Effect.gen(function* () { + const request = yield* HttpServerRequest.HttpServerRequest + yield* Effect.log(`Request: ${request.method} ${request.url}`) + }) + }) +) + +const User = Schema.Struct({ + id: Schema.Number, + name: Schema.String, + createdAt: Schema.DateTimeUtc +}) + +const idParam = HttpApiSchema.param("id", Schema.NumberFromString) + +const usersGroup = HttpApiGroup.make("users") + .add( + HttpApiEndpoint.get("getUser")`/user/${idParam}` + .addSuccess(User) + // Apply the middleware to a single endpoint + .middleware(Logger) + ) + // Or apply the middleware to the entire group + .middleware(Logger) + +const api = HttpApi.make("myApi").add(usersGroup) + +const usersGroupLive = HttpApiBuilder.group(api, "users", (handlers) => + handlers.handle("getUser", (req) => + Effect.succeed({ + id: req.path.id, + name: "John Doe", + createdAt: DateTime.unsafeNow() + }) + ) +).pipe( + // Provide the Logger middleware to the group + Layer.provide(LoggerLive) +) +``` + +### Defining security middleware + +The `HttpApiSecurity` module enables you to add security annotations to your API. These annotations specify the type of authorization required to access specific endpoints. + +Supported authorization types include: + +| Authorization Type | Description | +| ------------------------ | ---------------------------------------------------------------- | +| `HttpApiSecurity.apiKey` | API key authorization via headers, query parameters, or cookies. | +| `HttpApiSecurity.basic` | HTTP Basic authentication. | +| `HttpApiSecurity.bearer` | Bearer token authentication. | + +These security annotations can be used alongside `HttpApiMiddleware` to create middleware that protects your API endpoints. + +**Example** (Defining Security Middleware) + +```ts +import { + HttpApi, + HttpApiEndpoint, + HttpApiGroup, + HttpApiMiddleware, + HttpApiSchema, + HttpApiSecurity +} from "@effect/platform" +import { Context, Schema } from "effect" + +// Define a schema for the "User" +class User extends Schema.Class("User")({ id: Schema.Number }) {} + +// Define a schema for the "Unauthorized" error +class Unauthorized extends Schema.TaggedError()( + "Unauthorized", + {}, + // Specify the HTTP status code for unauthorized errors + HttpApiSchema.annotations({ status: 401 }) +) {} + +// Define a Context.Tag for the authenticated user +class CurrentUser extends Context.Tag("CurrentUser")() {} + +// Create the Authorization middleware +class Authorization extends HttpApiMiddleware.Tag()( + "Authorization", + { + // Define the error schema for unauthorized access + failure: Unauthorized, + // Specify the resource this middleware will provide + provides: CurrentUser, + // Add security definitions + security: { + // ┌─── Custom name for the security definition + // ▼ + myBearer: HttpApiSecurity.bearer + // Additional security definitions can be added here. + // They will attempt to be resolved in the order they are defined. + } + } +) {} + +const api = HttpApi.make("api") + .add( + HttpApiGroup.make("group") + .add( + HttpApiEndpoint.get("get", "/") + .addSuccess(Schema.String) + // Apply the middleware to a single endpoint + .middleware(Authorization) + ) + // Or apply the middleware to the entire group + .middleware(Authorization) + ) + // Or apply the middleware to the entire API + .middleware(Authorization) +``` + +### Implementing HttpApiSecurity middleware + +When using `HttpApiSecurity` in your middleware, the implementation involves creating a `Layer` with security handlers tailored to your requirements. Below is an example demonstrating how to implement middleware for `HttpApiSecurity.bearer` authentication. + +**Example** (Implementing Bearer Token Authentication Middleware) + +```ts +import { + HttpApiMiddleware, + HttpApiSchema, + HttpApiSecurity +} from "@effect/platform" +import { Context, Effect, Layer, Redacted, Schema } from "effect" + +class User extends Schema.Class("User")({ id: Schema.Number }) {} + +class Unauthorized extends Schema.TaggedError()( + "Unauthorized", + {}, + HttpApiSchema.annotations({ status: 401 }) +) {} + +class CurrentUser extends Context.Tag("CurrentUser")() {} + +class Authorization extends HttpApiMiddleware.Tag()( + "Authorization", + { + failure: Unauthorized, + provides: CurrentUser, + security: { + myBearer: HttpApiSecurity.bearer + } + } +) {} + +const AuthorizationLive = Layer.effect( + Authorization, + Effect.gen(function* () { + yield* Effect.log("creating Authorization middleware") + + // Return the security handlers for the middleware + return { + // Define the handler for the Bearer token + // The Bearer token is redacted for security + myBearer: (bearerToken) => + Effect.gen(function* () { + yield* Effect.log( + "checking bearer token", + Redacted.value(bearerToken) + ) + // Return a mock User object as the CurrentUser + return new User({ id: 1 }) + }) + } + }) +) +``` + +### Adding Descriptions to Security Definitions + +The `HttpApiSecurity.annotate` function allows you to add metadata, such as a description, to your security definitions. This metadata is displayed in the Swagger documentation, making it easier for developers to understand your API's security requirements. + +**Example** (Adding a Description to a Bearer Token Security Definition) + +```ts +import { + HttpApiMiddleware, + HttpApiSchema, + HttpApiSecurity, + OpenApi +} from "@effect/platform" +import { Context, Schema } from "effect" + +class User extends Schema.Class("User")({ id: Schema.Number }) {} + +class Unauthorized extends Schema.TaggedError()( + "Unauthorized", + {}, + HttpApiSchema.annotations({ status: 401 }) +) {} + +class CurrentUser extends Context.Tag("CurrentUser")() {} + +class Authorization extends HttpApiMiddleware.Tag()( + "Authorization", + { + failure: Unauthorized, + provides: CurrentUser, + security: { + myBearer: HttpApiSecurity.bearer.pipe( + // Add a description to the security definition + HttpApiSecurity.annotate(OpenApi.Description, "my description") + ) + } + } +) {} +``` + +### Setting HttpApiSecurity cookies + +To set a security cookie from within a handler, you can use the `HttpApiBuilder.securitySetCookie` API. This method sets a cookie with default properties, including the `HttpOnly` and `Secure` flags, ensuring the cookie is not accessible via JavaScript and is transmitted over secure connections. + +**Example** (Setting a Security Cookie in a Login Handler) + +```ts +// Define the security configuration for an API key stored in a cookie +const security = HttpApiSecurity.apiKey({ + // Specify that the API key is stored in a cookie + in: "cookie" + // Define the cookie name, + key: "token" +}) + +const UsersApiLive = HttpApiBuilder.group(MyApi, "users", (handlers) => + handlers.handle("login", () => + // Set the security cookie with a redacted value + HttpApiBuilder.securitySetCookie(security, Redacted.make("keep me secret")) + ) +) +``` + +## Serving Swagger documentation + +You can add Swagger documentation to your API using the `HttpApiSwagger` module. This integration provides an interactive interface for developers to explore and test your API. To enable Swagger, you simply provide the `HttpApiSwagger.layer` to your server implementation. + +**Example** (Adding Swagger Documentation to an API) + +```ts +import { + HttpApi, + HttpApiBuilder, + HttpApiEndpoint, + HttpApiGroup, + HttpApiSchema, + HttpApiSwagger, + HttpMiddleware, + HttpServer +} from "@effect/platform" +import { NodeHttpServer, NodeRuntime } from "@effect/platform-node" +import { DateTime, Effect, Layer, Schema } from "effect" +import { createServer } from "node:http" + +const User = Schema.Struct({ + id: Schema.Number, + name: Schema.String, + createdAt: Schema.DateTimeUtc +}) + +const idParam = HttpApiSchema.param("id", Schema.NumberFromString) + +const usersGroup = HttpApiGroup.make("users").add( + HttpApiEndpoint.get("getUser")`/user/${idParam}`.addSuccess(User) +) + +const api = HttpApi.make("myApi").add(usersGroup) + +const usersGroupLive = HttpApiBuilder.group(api, "users", (handlers) => + handlers.handle("getUser", ({ path: { id } }) => + Effect.succeed({ + id, + name: "John Doe", + createdAt: DateTime.unsafeNow() + }) + ) +) + +const MyApiLive = HttpApiBuilder.api(api).pipe(Layer.provide(usersGroupLive)) + +const HttpLive = HttpApiBuilder.serve(HttpMiddleware.logger).pipe( + // Add the Swagger documentation layer + Layer.provide( + HttpApiSwagger.layer({ + // Specify the Swagger documentation path. + // "/docs" is the default path. + path: "/docs" + }) + ), + Layer.provide(HttpApiBuilder.middlewareCors()), + Layer.provide(MyApiLive), + HttpServer.withLogAddress, + Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 })) +) + +Layer.launch(HttpLive).pipe(NodeRuntime.runMain) +``` + +![Swagger Documentation](./images/swagger-myapi.png) + +### Adding OpenAPI Annotations + +You can add OpenAPI annotations to your API to include metadata such as titles, descriptions, and more. These annotations help generate richer API documentation. + +#### HttpApi + +Below is a list of available annotations for a top-level `HttpApi`. They can be added using the `.annotate` method: + +| Annotation | Description | +| --------------------------- | ------------------------------------------------------------------------------------------------------------------ | +| `HttpApi.AdditionalSchemas` | Adds custom schemas to the final OpenAPI specification. Only schemas with an `identifier` annotation are included. | +| `OpenApi.Description` | Sets a general description for the API. | +| `OpenApi.License` | Defines the license used by the API. | +| `OpenApi.Summary` | Provides a brief summary of the API. | +| `OpenApi.Servers` | Lists server URLs and optional metadata such as variables. | +| `OpenApi.Override` | Merges the supplied fields into the resulting specification. | +| `OpenApi.Transform` | Allows you to modify the final specification with a custom function. | + +**Example** (Annotating the Top-Level API) + +```ts +import { HttpApi, OpenApi } from "@effect/platform" +import { Schema } from "effect" + +const api = HttpApi.make("api") + // Provide additional schemas + .annotate(HttpApi.AdditionalSchemas, [ + Schema.String.annotations({ identifier: "MyString" }) + ]) + // Add a description + .annotate(OpenApi.Description, "my description") + // Set license information + .annotate(OpenApi.License, { name: "MIT", url: "http://example.com" }) + // Provide a summary + .annotate(OpenApi.Summary, "my summary") + // Define servers + .annotate(OpenApi.Servers, [ + { + url: "http://example.com", + description: "example", + variables: { a: { default: "b", enum: ["c"], description: "d" } } + } + ]) + // Override parts of the generated specification + .annotate(OpenApi.Override, { + tags: [{ name: "a", description: "a-description" }] + }) + // Apply a transform function to the final specification + .annotate(OpenApi.Transform, (spec) => ({ + ...spec, + tags: [...spec.tags, { name: "b", description: "b-description" }] + })) + +// Generate the OpenAPI specification from the annotated API +const spec = OpenApi.fromApi(api) + +console.log(JSON.stringify(spec, null, 2)) +/* +Output: +{ + "openapi": "3.1.0", + "info": { + "title": "Api", + "version": "0.0.1", + "description": "my description", + "license": { + "name": "MIT", + "url": "http://example.com" + }, + "summary": "my summary" + }, + "paths": {}, + "tags": [ + { "name": "a", "description": "a-description" }, + { "name": "b", "description": "b-description" } + ], + "components": { + "schemas": { + "MyString": { + "type": "string" + } + }, + "securitySchemes": {} + }, + "security": [], + "servers": [ + { + "url": "http://example.com", + "description": "example", + "variables": { + "a": { + "default": "b", + "enum": [ + "c" + ], + "description": "d" + } + } + } + ] +} +*/ +``` + +#### HttpApiGroup + +The following annotations can be added to an `HttpApiGroup`: + +| Annotation | Description | +| ---------------------- | --------------------------------------------------------------------- | +| `OpenApi.Description` | Sets a description for this group. | +| `OpenApi.ExternalDocs` | Provides external documentation links for the group. | +| `OpenApi.Override` | Merges specified fields into the resulting specification. | +| `OpenApi.Transform` | Lets you modify the final group specification with a custom function. | +| `OpenApi.Exclude` | Excludes the group from the final OpenAPI specification. | + +**Example** (Annotating a Group) + +```ts +import { HttpApi, HttpApiGroup, OpenApi } from "@effect/platform" + +const api = HttpApi.make("api") + .add( + HttpApiGroup.make("group") + // Add a description for the group + .annotate(OpenApi.Description, "my description") + // Provide external documentation links + .annotate(OpenApi.ExternalDocs, { + url: "http://example.com", + description: "example" + }) + // Override parts of the final output + .annotate(OpenApi.Override, { name: "my name" }) + // Transform the final specification for this group + .annotate(OpenApi.Transform, (spec) => ({ + ...spec, + name: spec.name + "-transformed" + })) + ) + .add( + HttpApiGroup.make("excluded") + // Exclude the group from the final specification + .annotate(OpenApi.Exclude, true) + ) + +// Generate the OpenAPI spec +const spec = OpenApi.fromApi(api) + +console.log(JSON.stringify(spec, null, 2)) +/* +Output: +{ + "openapi": "3.1.0", + "info": { + "title": "Api", + "version": "0.0.1" + }, + "paths": {}, + "tags": [ + { + "name": "my name-transformed", + "description": "my description", + "externalDocs": { + "url": "http://example.com", + "description": "example" + } + } + ], + "components": { + "schemas": {}, + "securitySchemes": {} + }, + "security": [] +} +*/ +``` + +#### HttpApiEndpoint + +For an `HttpApiEndpoint`, you can use the following annotations: + +| Annotation | Description | +| ---------------------- | --------------------------------------------------------------------------- | +| `OpenApi.Description` | Adds a description for this endpoint. | +| `OpenApi.Summary` | Provides a short summary of the endpoint's purpose. | +| `OpenApi.Deprecated` | Marks the endpoint as deprecated. | +| `OpenApi.ExternalDocs` | Supplies external documentation links for the endpoint. | +| `OpenApi.Override` | Merges specified fields into the resulting specification for this endpoint. | +| `OpenApi.Transform` | Lets you modify the final endpoint specification with a custom function. | +| `OpenApi.Exclude` | Excludes the endpoint from the final OpenAPI specification. | + +**Example** (Annotating an Endpoint) + +```ts +import { + HttpApi, + HttpApiEndpoint, + HttpApiGroup, + OpenApi +} from "@effect/platform" +import { Schema } from "effect" + +const api = HttpApi.make("api").add( + HttpApiGroup.make("group") + .add( + HttpApiEndpoint.get("get", "/") + .addSuccess(Schema.String) + // Add a description + .annotate(OpenApi.Description, "my description") + // Provide a summary + .annotate(OpenApi.Summary, "my summary") + // Mark the endpoint as deprecated + .annotate(OpenApi.Deprecated, true) + // Provide external documentation + .annotate(OpenApi.ExternalDocs, { + url: "http://example.com", + description: "example" + }) + ) + .add( + HttpApiEndpoint.get("excluded", "/excluded") + .addSuccess(Schema.String) + // Exclude this endpoint from the final specification + .annotate(OpenApi.Exclude, true) + ) +) + +// Generate the OpenAPI spec +const spec = OpenApi.fromApi(api) + +console.log(JSON.stringify(spec, null, 2)) +/* +Output: +{ + "openapi": "3.1.0", + "info": { + "title": "Api", + "version": "0.0.1" + }, + "paths": { + "/": { + "get": { + "tags": [ + "group" + ], + "operationId": "my operationId-transformed", + "parameters": [], + "security": [], + "responses": { + "200": { + "description": "a string", + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + } + }, + "400": { + "description": "The request did not match the expected schema", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HttpApiDecodeError" + } + } + } + } + }, + "description": "my description", + "summary": "my summary", + "deprecated": true, + "externalDocs": { + "url": "http://example.com", + "description": "example" + } + } + } + }, + ... +} +*/ +``` + +The default response description is "Success". You can override this by annotating the schema. + +**Example** (Defining a custom response description) + +```ts +import { + HttpApi, + HttpApiEndpoint, + HttpApiGroup, + OpenApi +} from "@effect/platform" +import { Schema } from "effect" + +const User = Schema.Struct({ + id: Schema.Number, + name: Schema.String, + createdAt: Schema.DateTimeUtc +}).annotations({ identifier: "User" }) + +const api = HttpApi.make("api").add( + HttpApiGroup.make("group").add( + HttpApiEndpoint.get("getUsers", "/users").addSuccess( + Schema.Array(User).annotations({ + description: "Returns an array of users" + }) + ) + ) +) + +const spec = OpenApi.fromApi(api) + +console.log(JSON.stringify(spec.paths, null, 2)) +/* +Output: +{ + "/users": { + "get": { + "tags": [ + "group" + ], + "operationId": "group.getUsers", + "parameters": [], + "security": [], + "responses": { + "200": { + "description": "Returns an array of users", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/User" + }, + "description": "Returns an array of users" + } + } + } + }, + "400": { + "description": "The request did not match the expected schema", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HttpApiDecodeError" + } + } + } + } + } + } + } +} +*/ +``` + +### Top Level Groups + +When a group is marked as `topLevel`, the operation IDs of its endpoints do not include the group name as a prefix. This is helpful when you want to group endpoints under a shared tag without adding a redundant prefix to their operation IDs. + +**Example** (Using a Top-Level Group) + +```ts +import { + HttpApi, + HttpApiEndpoint, + HttpApiGroup, + OpenApi +} from "@effect/platform" +import { Schema } from "effect" + +const api = HttpApi.make("api").add( + // Mark the group as top-level + HttpApiGroup.make("group", { topLevel: true }).add( + HttpApiEndpoint.get("get", "/").addSuccess(Schema.String) + ) +) + +// Generate the OpenAPI spec +const spec = OpenApi.fromApi(api) + +console.log(JSON.stringify(spec.paths, null, 2)) +/* +Output: +{ + "/": { + "get": { + "tags": [ + "group" + ], + "operationId": "get", // The operation ID is not prefixed with "group" + "parameters": [], + "security": [], + "responses": { + "200": { + "description": "a string", + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + } + }, + "400": { + "description": "The request did not match the expected schema", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HttpApiDecodeError" + } + } + } + } + } + } + } +} +*/ +``` + +## Deriving a Client + +After defining your API, you can derive a client that interacts with the server. The `HttpApiClient` module simplifies the process by providing tools to generate a client based on your API definition. + +**Example** (Deriving and Using a Client) + +This example demonstrates how to create a client for an API and use it to call an endpoint. + +```ts +import { + FetchHttpClient, + HttpApi, + HttpApiBuilder, + HttpApiClient, + HttpApiEndpoint, + HttpApiGroup, + HttpApiSchema, + HttpApiSwagger, + HttpMiddleware, + HttpServer +} from "@effect/platform" +import { NodeHttpServer, NodeRuntime } from "@effect/platform-node" +import { DateTime, Effect, Layer, Schema } from "effect" +import { createServer } from "node:http" + +const User = Schema.Struct({ + id: Schema.Number, + name: Schema.String, + createdAt: Schema.DateTimeUtc +}) + +const idParam = HttpApiSchema.param("id", Schema.NumberFromString) + +const usersGroup = HttpApiGroup.make("users").add( + HttpApiEndpoint.get("getUser")`/user/${idParam}`.addSuccess(User) +) + +const api = HttpApi.make("myApi").add(usersGroup) + +const usersGroupLive = HttpApiBuilder.group(api, "users", (handlers) => + handlers.handle("getUser", ({ path: { id } }) => + Effect.succeed({ + id, + name: "John Doe", + createdAt: DateTime.unsafeNow() + }) + ) +) + +const MyApiLive = HttpApiBuilder.api(api).pipe(Layer.provide(usersGroupLive)) + +const HttpLive = HttpApiBuilder.serve(HttpMiddleware.logger).pipe( + Layer.provide(HttpApiSwagger.layer()), + Layer.provide(HttpApiBuilder.middlewareCors()), + Layer.provide(MyApiLive), + HttpServer.withLogAddress, + Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 })) +) + +Layer.launch(HttpLive).pipe(NodeRuntime.runMain) + +// Create a program that derives and uses the client +const program = Effect.gen(function* () { + // Derive the client + const client = yield* HttpApiClient.make(api, { + baseUrl: "http://localhost:3000" + }) + // Call the `getUser` endpoint + const user = yield* client.users.getUser({ path: { id: 1 } }) + console.log(user) +}) + +// Provide a Fetch-based HTTP client and run the program +Effect.runFork(program.pipe(Effect.provide(FetchHttpClient.layer))) +/* +Example Output: +User { + id: 1, + name: 'John Doe', + createdAt: DateTime.Utc(2025-01-04T15:14:49.562Z) +} +*/ +``` + +### Top Level Groups + +When a group is marked as `topLevel`, the methods on the client are not nested under the group name. This can simplify client usage by providing direct access to the endpoint methods. + +**Example** (Using a Top-Level Group in the Client) + +```ts +import { + HttpApi, + HttpApiClient, + HttpApiEndpoint, + HttpApiGroup +} from "@effect/platform" +import { Effect, Schema } from "effect" + +const api = HttpApi.make("api").add( + // Mark the group as top-level + HttpApiGroup.make("group", { topLevel: true }).add( + HttpApiEndpoint.get("get", "/").addSuccess(Schema.String) + ) +) + +const program = Effect.gen(function* () { + const client = yield* HttpApiClient.make(api, { + baseUrl: "http://localhost:3000" + }) + // The `get` method is not nested under the "group" name + const user = yield* client.get() + console.log(user) +}) +``` + +## Converting to a Web Handler + +You can convert your `HttpApi` implementation into a web handler using the `HttpApiBuilder.toWebHandler` API. This approach enables you to serve your API through a custom server setup. + +**Example** (Creating and Serving a Web Handler) + +```ts +import { + HttpApi, + HttpApiBuilder, + HttpApiEndpoint, + HttpApiGroup, + HttpApiSwagger, + HttpServer +} from "@effect/platform" +import { Effect, Layer, Schema } from "effect" +import * as http from "node:http" + +const api = HttpApi.make("myApi").add( + HttpApiGroup.make("group").add( + HttpApiEndpoint.get("get", "/").addSuccess(Schema.String) + ) +) + +const groupLive = HttpApiBuilder.group(api, "group", (handlers) => + handlers.handle("get", () => Effect.succeed("Hello, world!")) +) + +const MyApiLive = HttpApiBuilder.api(api).pipe(Layer.provide(groupLive)) + +const SwaggerLayer = HttpApiSwagger.layer().pipe(Layer.provide(MyApiLive)) + +// Convert the API to a web handler +const { dispose, handler } = HttpApiBuilder.toWebHandler( + Layer.mergeAll(MyApiLive, SwaggerLayer, HttpServer.layerContext) +) + +// Serving the handler using a custom HTTP server +http + .createServer(async (req, res) => { + const url = `http://${req.headers.host}${req.url}` + const init: RequestInit = { + method: req.method! + } + + const response = await handler(new Request(url, init)) + + res.writeHead( + response.status, + response.statusText, + Object.fromEntries(response.headers.entries()) + ) + const responseBody = await response.arrayBuffer() + res.end(Buffer.from(responseBody)) + }) + .listen(3000, () => { + console.log("Server running at http://localhost:3000/") + }) + .on("close", () => { + dispose() + }) +``` + +# HTTP Client + +## Overview + +The `@effect/platform/HttpClient*` modules provide a way to send HTTP requests, +handle responses, and abstract over the differences between platforms. + +The `HttpClient` interface has a set of methods for sending requests: + +- `.execute` - takes a [HttpClientRequest](#httpclientrequest) and returns a `HttpClientResponse` +- `.{get, del, head, options, patch, post, put}` - convenience methods for creating a request and + executing it in one step + +To access the `HttpClient`, you can use the `HttpClient.HttpClient` [tag](https://effect.website/docs/guides/context-management/services). +This will give you access to a `HttpClient` instance. + +**Example: Retrieving JSON Data (GET)** + +```ts +import { FetchHttpClient, HttpClient } from "@effect/platform" +import { Effect } from "effect" + +const program = Effect.gen(function* () { + // Access HttpClient + const client = yield* HttpClient.HttpClient + + // Create and execute a GET request + const response = yield* client.get( + "https://jsonplaceholder.typicode.com/posts/1" + ) + + const json = yield* response.json + + console.log(json) +}).pipe( + // Provide the HttpClient + Effect.provide(FetchHttpClient.layer) +) + +Effect.runPromise(program) +/* +Output: +{ + userId: 1, + id: 1, + title: 'sunt aut facere repellat provident occaecati excepturi optio reprehenderit', + body: 'quia et suscipit\n' + + 'suscipit recusandae consequuntur expedita et cum\n' + + 'reprehenderit molestiae ut ut quas totam\n' + + 'nostrum rerum est autem sunt rem eveniet architecto' +} +*/ +``` + +**Example: Retrieving JSON Data with accessor apis (GET)** + +The `HttpClient` module also provides a set of accessor apis that allow you to +easily send requests without first accessing the `HttpClient` service. + +Below is an example of using the `get` accessor api to send a GET request: + +(The following examples will continue to use the `HttpClient` service approach). + +```ts +import { FetchHttpClient, HttpClient } from "@effect/platform" +import { Effect } from "effect" + +const program = HttpClient.get( + "https://jsonplaceholder.typicode.com/posts/1" +).pipe( + Effect.andThen((response) => response.json), + Effect.provide(FetchHttpClient.layer) +) + +Effect.runPromise(program) +/* +Output: +{ + userId: 1, + id: 1, + title: 'sunt aut facere repellat provident occaecati excepturi optio reprehenderit', + body: 'quia et suscipit\n' + + 'suscipit recusandae consequuntur expedita et cum\n' + + 'reprehenderit molestiae ut ut quas totam\n' + + 'nostrum rerum est autem sunt rem eveniet architecto' +} +*/ +``` + +**Example: Creating and Executing a Custom Request** + +Using [HttpClientRequest](#httpclientrequest), you can create and then execute a request. This is useful for customizing the request further. + +```ts +import { + FetchHttpClient, + HttpClient, + HttpClientRequest +} from "@effect/platform" +import { Effect } from "effect" + +const program = Effect.gen(function* () { + // Access HttpClient + const client = yield* HttpClient.HttpClient + + // Create a GET request + const req = HttpClientRequest.get( + "https://jsonplaceholder.typicode.com/posts/1" + ) + + // Optionally customize the request + + // Execute the request and get the response + const response = yield* client.execute(req) + + const json = yield* response.json + + console.log(json) +}).pipe( + // Provide the HttpClient + Effect.provide(FetchHttpClient.layer) +) + +Effect.runPromise(program) +/* +Output: +{ + userId: 1, + id: 1, + title: 'sunt aut facere repellat provident occaecati excepturi optio reprehenderit', + body: 'quia et suscipit\n' + + 'suscipit recusandae consequuntur expedita et cum\n' + + 'reprehenderit molestiae ut ut quas totam\n' + + 'nostrum rerum est autem sunt rem eveniet architecto' +} +*/ +``` + +## Customize a HttpClient + +The `HttpClient` module allows you to customize the client in various ways. For instance, you can log details of a request before execution using the `tapRequest` function. + +**Example: Tapping** + +```ts +import { FetchHttpClient, HttpClient } from "@effect/platform" +import { Console, Effect } from "effect" + +const program = Effect.gen(function* () { + const client = (yield* HttpClient.HttpClient).pipe( + // Log the request before fetching + HttpClient.tapRequest(Console.log) + ) + + const response = yield* client.get( + "https://jsonplaceholder.typicode.com/posts/1" + ) + + const json = yield* response.json + + console.log(json) +}).pipe(Effect.provide(FetchHttpClient.layer)) + +Effect.runPromise(program) +/* +Output: +{ + _id: '@effect/platform/HttpClientRequest', + method: 'GET', + url: 'https://jsonplaceholder.typicode.com/posts/1', + urlParams: [], + hash: { _id: 'Option', _tag: 'None' }, + headers: Object <[Object: null prototype]> {}, + body: { _id: '@effect/platform/HttpBody', _tag: 'Empty' } +} +{ + userId: 1, + id: 1, + title: 'sunt aut facere repellat provident occaecati excepturi optio reprehenderit', + body: 'quia et suscipit\n' + + 'suscipit recusandae consequuntur expedita et cum\n' + + 'reprehenderit molestiae ut ut quas totam\n' + + 'nostrum rerum est autem sunt rem eveniet architecto' +} +*/ +``` + +**Operations Summary** + +| Operation | Description | +| ------------------------ | --------------------------------------------------------------------------------------- | +| `get`,`post`,`put`... | Send a request without first accessing the `HttpClient` service. | +| `filterOrElse` | Filters the result of a response, or runs an alternative effect if the predicate fails. | +| `filterOrFail` | Filters the result of a response, or throws an error if the predicate fails. | +| `filterStatus` | Filters responses by HTTP status code. | +| `filterStatusOk` | Filters responses that return a 2xx status code. | +| `followRedirects` | Follows HTTP redirects up to a specified number of times. | +| `mapRequest` | Appends a transformation of the request object before sending it. | +| `mapRequestEffect` | Appends an effectful transformation of the request object before sending it. | +| `mapRequestInput` | Prepends a transformation of the request object before sending it. | +| `mapRequestInputEffect` | Prepends an effectful transformation of the request object before sending it. | +| `retry` | Retries the request based on a provided schedule or policy. | +| `tap` | Performs an additional effect after a successful request. | +| `tapRequest` | Performs an additional effect on the request before sending it. | +| `withCookiesRef` | Associates a `Ref` of cookies with the client for handling cookies across requests. | +| `withTracerDisabledWhen` | Disables tracing for specific requests based on a provided predicate. | +| `withTracerPropagation` | Enables or disables tracing propagation for the request. | + +### Mapping Requests + +Note that `mapRequest` and `mapRequestEffect` add transformations at the end of the request chain, while `mapRequestInput` and `mapRequestInputEffect` apply transformations at the start: + +```ts +import { FetchHttpClient, HttpClient } from "@effect/platform" +import { Effect } from "effect" + +const program = Effect.gen(function* () { + const client = (yield* HttpClient.HttpClient).pipe( + // Append transformation + HttpClient.mapRequest((req) => { + console.log(1) + return req + }), + // Another append transformation + HttpClient.mapRequest((req) => { + console.log(2) + return req + }), + // Prepend transformation, this executes first + HttpClient.mapRequestInput((req) => { + console.log(3) + return req + }) + ) + + const response = yield* client.get( + "https://jsonplaceholder.typicode.com/posts/1" + ) + + const json = yield* response.json + + console.log(json) +}).pipe(Effect.provide(FetchHttpClient.layer)) + +Effect.runPromise(program) +/* +Output: +3 +1 +2 +{ + userId: 1, + id: 1, + title: 'sunt aut facere repellat provident occaecati excepturi optio reprehenderit', + body: 'quia et suscipit\n' + + 'suscipit recusandae consequuntur expedita et cum\n' + + 'reprehenderit molestiae ut ut quas totam\n' + + 'nostrum rerum est autem sunt rem eveniet architecto' +} +*/ +``` + +### Persisting Cookies + +You can manage cookies across requests using the `HttpClient.withCookiesRef` function, which associates a reference to a `Cookies` object with the client. + +```ts +import { Cookies, FetchHttpClient, HttpClient } from "@effect/platform" +import { Effect, Ref } from "effect" + +const program = Effect.gen(function* () { + // Create a reference to store cookies + const ref = yield* Ref.make(Cookies.empty) + + // Access the HttpClient and associate the cookies reference with it + const client = (yield* HttpClient.HttpClient).pipe( + HttpClient.withCookiesRef(ref) + ) + + // Make a GET request to the specified URL + yield* client.get("https://www.google.com/") + + // Log the keys of the cookies stored in the reference + console.log(Object.keys((yield* ref).cookies)) +}).pipe(Effect.provide(FetchHttpClient.layer)) + +Effect.runPromise(program) +// Output: [ 'SOCS', 'AEC', '__Secure-ENID' ] +``` + +## RequestInit Options + +You can customize the `FetchHttpClient` by passing `RequestInit` options to configure aspects of the HTTP requests, such as credentials, headers, and more. + +In this example, we customize the `FetchHttpClient` to include credentials with every request: + +```ts +import { FetchHttpClient, HttpClient } from "@effect/platform" +import { Effect, Layer } from "effect" + +const CustomFetchLive = FetchHttpClient.layer.pipe( + Layer.provide( + Layer.succeed(FetchHttpClient.RequestInit, { + credentials: "include" + }) + ) +) + +const program = Effect.gen(function* () { + const client = yield* HttpClient.HttpClient + const response = yield* client.get( + "https://jsonplaceholder.typicode.com/posts/1" + ) + const json = yield* response.json + console.log(json) +}).pipe(Effect.provide(CustomFetchLive)) +``` + +## Create a Custom HttpClient + +You can create a custom `HttpClient` using the `HttpClient.make` function. This allows you to simulate or mock server responses within your application. + +```ts +import { HttpClient, HttpClientResponse } from "@effect/platform" +import { Effect, Layer } from "effect" + +const myClient = HttpClient.make((req) => + Effect.succeed( + HttpClientResponse.fromWeb( + req, + // Simulate a response from a server + new Response( + JSON.stringify({ + userId: 1, + id: 1, + title: "title...", + body: "body..." + }) + ) + ) + ) +) + +const program = Effect.gen(function* () { + const client = yield* HttpClient.HttpClient + const response = yield* client.get( + "https://jsonplaceholder.typicode.com/posts/1" + ) + const json = yield* response.json + console.log(json) +}).pipe( + // Provide the HttpClient + Effect.provide(Layer.succeed(HttpClient.HttpClient, myClient)) +) + +Effect.runPromise(program) +/* +Output: +{ userId: 1, id: 1, title: 'title...', body: 'body...' } +*/ +``` + +## HttpClientRequest + +### Overview + +You can create a `HttpClientRequest` using the following provided constructors: + +| Constructor | Description | +| --------------------------- | ------------------------- | +| `HttpClientRequest.del` | Create a DELETE request | +| `HttpClientRequest.get` | Create a GET request | +| `HttpClientRequest.head` | Create a HEAD request | +| `HttpClientRequest.options` | Create an OPTIONS request | +| `HttpClientRequest.patch` | Create a PATCH request | +| `HttpClientRequest.post` | Create a POST request | +| `HttpClientRequest.put` | Create a PUT request | + +### Setting Headers + +When making HTTP requests, sometimes you need to include additional information in the request headers. You can set headers using the `setHeader` function for a single header or `setHeaders` for multiple headers simultaneously. + +```ts +import { HttpClientRequest } from "@effect/platform" + +const req = HttpClientRequest.get("https://api.example.com/data").pipe( + // Setting a single header + HttpClientRequest.setHeader("Authorization", "Bearer your_token_here"), + // Setting multiple headers + HttpClientRequest.setHeaders({ + "Content-Type": "application/json; charset=UTF-8", + "Custom-Header": "CustomValue" + }) +) + +console.log(JSON.stringify(req.headers, null, 2)) +/* +Output: +{ + "authorization": "Bearer your_token_here", + "content-type": "application/json; charset=UTF-8", + "custom-header": "CustomValue" +} +*/ +``` + +### basicAuth + +To include basic authentication in your HTTP request, you can use the `basicAuth` method provided by `HttpClientRequest`. + +```ts +import { HttpClientRequest } from "@effect/platform" + +const req = HttpClientRequest.get("https://api.example.com/data").pipe( + HttpClientRequest.basicAuth("your_username", "your_password") +) + +console.log(JSON.stringify(req.headers, null, 2)) +/* +Output: +{ + "authorization": "Basic eW91cl91c2VybmFtZTp5b3VyX3Bhc3N3b3Jk" +} +*/ +``` + +### bearerToken + +To include a Bearer token in your HTTP request, use the `bearerToken` method provided by `HttpClientRequest`. + +```ts +import { HttpClientRequest } from "@effect/platform" + +const req = HttpClientRequest.get("https://api.example.com/data").pipe( + HttpClientRequest.bearerToken("your_token") +) + +console.log(JSON.stringify(req.headers, null, 2)) +/* +Output: +{ + "authorization": "Bearer your_token" +} +*/ +``` + +### accept + +To specify the media types that are acceptable for the response, use the `accept` method provided by `HttpClientRequest`. + +```ts +import { HttpClientRequest } from "@effect/platform" + +const req = HttpClientRequest.get("https://api.example.com/data").pipe( + HttpClientRequest.accept("application/xml") +) + +console.log(JSON.stringify(req.headers, null, 2)) +/* +Output: +{ + "accept": "application/xml" +} +*/ +``` + +### acceptJson + +To indicate that the client accepts JSON responses, use the `acceptJson` method provided by `HttpClientRequest`. + +```ts +import { HttpClientRequest } from "@effect/platform" + +const req = HttpClientRequest.get("https://api.example.com/data").pipe( + HttpClientRequest.acceptJson +) + +console.log(JSON.stringify(req.headers, null, 2)) +/* +Output: +{ + "accept": "application/json" +} +*/ +``` + +## GET + +### Converting the Response + +The `HttpClientResponse` provides several methods to convert a response into different formats. + +**Example: Converting to JSON** + +```ts +import { FetchHttpClient, HttpClient } from "@effect/platform" +import { NodeRuntime } from "@effect/platform-node" +import { Console, Effect } from "effect" + +const getPostAsJson = Effect.gen(function* () { + const client = yield* HttpClient.HttpClient + const response = yield* client.get( + "https://jsonplaceholder.typicode.com/posts/1" + ) + return yield* response.json +}).pipe(Effect.provide(FetchHttpClient.layer)) + +getPostAsJson.pipe( + Effect.andThen((post) => Console.log(typeof post, post)), + NodeRuntime.runMain +) +/* +Output: +object { + userId: 1, + id: 1, + title: 'sunt aut facere repellat provident occaecati excepturi optio reprehenderit', + body: 'quia et suscipit\n' + + 'suscipit recusandae consequuntur expedita et cum\n' + + 'reprehenderit molestiae ut ut quas totam\n' + + 'nostrum rerum est autem sunt rem eveniet architecto' +} +*/ +``` + +**Example: Converting to Text** + +```ts +import { FetchHttpClient, HttpClient } from "@effect/platform" +import { NodeRuntime } from "@effect/platform-node" +import { Console, Effect } from "effect" + +const getPostAsText = Effect.gen(function* () { + const client = yield* HttpClient.HttpClient + const response = yield* client.get( + "https://jsonplaceholder.typicode.com/posts/1" + ) + return yield* response.text +}).pipe(Effect.provide(FetchHttpClient.layer)) + +getPostAsText.pipe( + Effect.andThen((post) => Console.log(typeof post, post)), + NodeRuntime.runMain +) +/* +Output: +string { + userId: 1, + id: 1, + title: 'sunt aut facere repellat provident occaecati excepturi optio reprehenderit', + body: 'quia et suscipit\n' + + 'suscipit recusandae consequuntur expedita et cum\n' + + 'reprehenderit molestiae ut ut quas totam\n' + + 'nostrum rerum est autem sunt rem eveniet architecto' +} +*/ +``` + +**Methods Summary** + +| Method | Description | +| --------------- | ------------------------------------- | +| `arrayBuffer` | Convert to `ArrayBuffer` | +| `formData` | Convert to `FormData` | +| `json` | Convert to JSON | +| `stream` | Convert to a `Stream` of `Uint8Array` | +| `text` | Convert to text | +| `urlParamsBody` | Convert to `UrlParams` | + +### Decoding Data with Schemas + +A common use case when fetching data is to validate the received format. For this purpose, the `HttpClientResponse` module is integrated with `effect/Schema`. + +```ts +import { + FetchHttpClient, + HttpClient, + HttpClientResponse +} from "@effect/platform" +import { NodeRuntime } from "@effect/platform-node" +import { Console, Effect, Schema } from "effect" + +const Post = Schema.Struct({ + id: Schema.Number, + title: Schema.String +}) + +const getPostAndValidate = Effect.gen(function* () { + const client = yield* HttpClient.HttpClient + const response = yield* client.get( + "https://jsonplaceholder.typicode.com/posts/1" + ) + return yield* HttpClientResponse.schemaBodyJson(Post)(response) +}).pipe(Effect.provide(FetchHttpClient.layer)) + +getPostAndValidate.pipe(Effect.andThen(Console.log), NodeRuntime.runMain) +/* +Output: +{ + id: 1, + title: 'sunt aut facere repellat provident occaecati excepturi optio reprehenderit' +} +*/ +``` + +In this example, we define a schema for a post object with properties `id` and `title`. Then, we fetch the data and validate it against this schema using `HttpClientResponse.schemaBodyJson`. Finally, we log the validated post object. + +### Filtering And Error Handling + +It's important to note that `HttpClient.get` doesn't consider non-`200` status codes as errors by default. This design choice allows for flexibility in handling different response scenarios. For instance, you might have a schema union where the status code serves as the discriminator, enabling you to define a schema that encompasses all possible response cases. + +You can use `HttpClient.filterStatusOk` to ensure only `2xx` responses are treated as successes. + +In this example, we attempt to fetch a non-existent page and don't receive any error: + +```ts +import { FetchHttpClient, HttpClient } from "@effect/platform" +import { NodeRuntime } from "@effect/platform-node" +import { Console, Effect } from "effect" + +const getText = Effect.gen(function* () { + const client = yield* HttpClient.HttpClient + const response = yield* client.get( + "https://jsonplaceholder.typicode.com/non-existing-page" + ) + return yield* response.text +}).pipe(Effect.provide(FetchHttpClient.layer)) + +getText.pipe(Effect.andThen(Console.log), NodeRuntime.runMain) +/* +Output: +{} +*/ +``` + +However, if we use `HttpClient.filterStatusOk`, an error is logged: + +```ts +import { FetchHttpClient, HttpClient } from "@effect/platform" +import { NodeRuntime } from "@effect/platform-node" +import { Console, Effect } from "effect" + +const getText = Effect.gen(function* () { + const client = (yield* HttpClient.HttpClient).pipe(HttpClient.filterStatusOk) + const response = yield* client.get( + "https://jsonplaceholder.typicode.com/non-existing-page" + ) + return yield* response.text +}).pipe(Effect.provide(FetchHttpClient.layer)) + +getText.pipe(Effect.andThen(Console.log), NodeRuntime.runMain) +/* +Output: +[17:37:59.923] ERROR (#0): + ResponseError: StatusCode: non 2xx status code (404 GET https://jsonplaceholder.typicode.com/non-existing-page) + ... stack trace ... +*/ +``` + +## POST + +To make a POST request, you can use the `HttpClientRequest.post` function provided by the `HttpClientRequest` module. Here's an example of how to create and send a POST request: + +```ts +import { + FetchHttpClient, + HttpClient, + HttpClientRequest +} from "@effect/platform" +import { NodeRuntime } from "@effect/platform-node" +import { Console, Effect } from "effect" + +const addPost = Effect.gen(function* () { + const client = yield* HttpClient.HttpClient + return yield* HttpClientRequest.post( + "https://jsonplaceholder.typicode.com/posts" + ).pipe( + HttpClientRequest.bodyJson({ + title: "foo", + body: "bar", + userId: 1 + }), + Effect.flatMap(client.execute), + Effect.flatMap((res) => res.json) + ) +}).pipe(Effect.provide(FetchHttpClient.layer)) + +addPost.pipe(Effect.andThen(Console.log), NodeRuntime.runMain) +/* +Output: +{ title: 'foo', body: 'bar', userId: 1, id: 101 } +*/ +``` + +If you need to send data in a format other than JSON, such as plain text, you can use different APIs provided by `HttpClientRequest`. + +In the following example, we send the data as text: + +```ts +import { + FetchHttpClient, + HttpClient, + HttpClientRequest +} from "@effect/platform" +import { NodeRuntime } from "@effect/platform-node" +import { Console, Effect } from "effect" + +const addPost = Effect.gen(function* () { + const client = yield* HttpClient.HttpClient + return yield* HttpClientRequest.post( + "https://jsonplaceholder.typicode.com/posts" + ).pipe( + HttpClientRequest.bodyText( + JSON.stringify({ + title: "foo", + body: "bar", + userId: 1 + }), + "application/json; charset=UTF-8" + ), + client.execute, + Effect.flatMap((res) => res.json) + ) +}).pipe(Effect.provide(FetchHttpClient.layer)) + +addPost.pipe(Effect.andThen(Console.log), NodeRuntime.runMain) +/* +Output: +{ title: 'foo', body: 'bar', userId: 1, id: 101 } +*/ +``` + +### Decoding Data with Schemas + +A common use case when fetching data is to validate the received format. For this purpose, the `HttpClientResponse` module is integrated with `effect/Schema`. + +```ts +import { + FetchHttpClient, + HttpClient, + HttpClientRequest, + HttpClientResponse +} from "@effect/platform" +import { NodeRuntime } from "@effect/platform-node" +import { Console, Effect, Schema } from "effect" + +const Post = Schema.Struct({ + id: Schema.Number, + title: Schema.String +}) + +const addPost = Effect.gen(function* () { + const client = yield* HttpClient.HttpClient + return yield* HttpClientRequest.post( + "https://jsonplaceholder.typicode.com/posts" + ).pipe( + HttpClientRequest.bodyText( + JSON.stringify({ + title: "foo", + body: "bar", + userId: 1 + }), + "application/json; charset=UTF-8" + ), + client.execute, + Effect.flatMap(HttpClientResponse.schemaBodyJson(Post)) + ) +}).pipe(Effect.provide(FetchHttpClient.layer)) + +addPost.pipe(Effect.andThen(Console.log), NodeRuntime.runMain) +/* +Output: +{ id: 101, title: 'foo' } +*/ +``` + +## Testing + +### Injecting Fetch + +To test HTTP requests, you can inject a mock fetch implementation. + +```ts +import { FetchHttpClient, HttpClient } from "@effect/platform" +import { Effect, Layer } from "effect" +import * as assert from "node:assert" + +// Mock fetch implementation +const FetchTest = Layer.succeed(FetchHttpClient.Fetch, () => + Promise.resolve(new Response("not found", { status: 404 })) +) + +const TestLayer = FetchHttpClient.layer.pipe(Layer.provide(FetchTest)) + +const program = Effect.gen(function* () { + const client = yield* HttpClient.HttpClient + + return yield* client + .get("https://www.google.com/") + .pipe(Effect.flatMap((res) => res.text)) +}) + +// Test +Effect.gen(function* () { + const response = yield* program + assert.equal(response, "not found") +}).pipe(Effect.provide(TestLayer), Effect.runPromise) +``` + +# HTTP Server + +## Overview + +This section provides a simplified explanation of key concepts within the `@effect/platform` TypeScript library, focusing on components used to build HTTP servers. Understanding these terms and their relationships helps in structuring and managing server applications effectively. + +### Core Concepts + +- **HttpApp**: This is an `Effect` which results in a value `A`. It can utilize `ServerRequest` to produce the outcome `A`. Essentially, an `HttpApp` represents an application component that handles HTTP requests and generates responses based on those requests. + +- **Default** (HttpApp): A special type of `HttpApp` that specifically produces a `ServerResponse` as its output `A`. This is the most common form of application where each interaction is expected to result in an HTTP response. + +- **Server**: A construct that takes a `Default` app and converts it into an `Effect`. This serves as the execution layer where the `Default` app is operated, handling incoming requests and serving responses. + +- **Router**: A type of `Default` app where the possible error outcome is `RouteNotFound`. Routers are used to direct incoming requests to appropriate handlers based on the request path and method. + +- **Handler**: Another form of `Default` app, which has access to both `RouteContext` and `ServerRequest.ParsedSearchParams`. Handlers are specific functions designed to process requests and generate responses. + +- **Middleware**: Functions that transform a `Default` app into another `Default` app. Middleware can be used to modify requests, responses, or handle tasks like logging, authentication, and more. Middleware can be applied in two ways: + - On a `Router` using `router.use: Handler -> Default` which applies the middleware to specific routes. + - On a `Server` using `server.serve: () -> Layer | Middleware -> Layer` which applies the middleware globally to all routes handled by the server. + +### Applying Concepts + +These components are designed to work together in a modular and flexible way, allowing developers to build complex server applications with reusable components. Here's how you might typically use these components in a project: + +1. **Create Handlers**: Define functions that process specific types of requests (e.g., GET, POST) and return responses. + +2. **Set Up Routers**: Organize handlers into routers, where each router manages a subset of application routes. + +3. **Apply Middleware**: Enhance routers or entire servers with middleware to add extra functionality like error handling or request logging. + +4. **Initialize the Server**: Wrap the main router with server functionality, applying any server-wide middleware, and start listening for requests. + +## Getting Started + +### Hello world example + +In this example, we will create a simple HTTP server that listens on port `3000`. The server will respond with "Hello World!" when a request is made to the root URL (/) and return a `500` error for all other paths. + +Node.js Example + +```ts +import { HttpRouter, HttpServer, HttpServerResponse } from "@effect/platform" +import { NodeHttpServer, NodeRuntime } from "@effect/platform-node" +import { Layer } from "effect" +import { createServer } from "node:http" + +// Define the router with a single route for the root URL +const router = HttpRouter.empty.pipe( + HttpRouter.get("/", HttpServerResponse.text("Hello World")) +) + +// Set up the application server with logging +const app = router.pipe(HttpServer.serve(), HttpServer.withLogAddress) + +// Specify the port +const port = 3000 + +// Create a server layer with the specified port +const ServerLive = NodeHttpServer.layer(() => createServer(), { port }) + +// Run the application +NodeRuntime.runMain(Layer.launch(Layer.provide(app, ServerLive))) + +/* +Output: +timestamp=... level=INFO fiber=#0 message="Listening on http://localhost:3000" +*/ +``` + +> [!NOTE] +> The `HttpServer.withLogAddress` middleware logs the address and port where the server is listening, helping to confirm that the server is running correctly and accessible on the expected endpoint. + +Bun Example + +```ts +import { HttpRouter, HttpServer, HttpServerResponse } from "@effect/platform" +import { BunHttpServer, BunRuntime } from "@effect/platform-bun" +import { Layer } from "effect" + +// Define the router with a single route for the root URL +const router = HttpRouter.empty.pipe( + HttpRouter.get("/", HttpServerResponse.text("Hello World")) +) + +// Set up the application server with logging +const app = router.pipe(HttpServer.serve(), HttpServer.withLogAddress) + +// Specify the port +const port = 3000 + +// Create a server layer with the specified port +const ServerLive = BunHttpServer.layer({ port }) + +// Run the application +BunRuntime.runMain(Layer.launch(Layer.provide(app, ServerLive))) + +/* +Output: +timestamp=... level=INFO fiber=#0 message="Listening on http://localhost:3000" +*/ +``` + +To avoid boilerplate code for the final server setup, we'll use a helper function from the `listen.ts` file: + +Node.js Example + +```ts +import type { HttpPlatform, HttpServer } from "@effect/platform" +import { NodeHttpServer, NodeRuntime } from "@effect/platform-node" +import { Layer } from "effect" +import { createServer } from "node:http" + +export const listen = ( + app: Layer.Layer< + never, + never, + HttpPlatform.HttpPlatform | HttpServer.HttpServer + >, + port: number +) => + NodeRuntime.runMain( + Layer.launch( + Layer.provide( + app, + NodeHttpServer.layer(() => createServer(), { port }) + ) + ) + ) +``` + +Bun Example + +```ts +import type { HttpPlatform, HttpServer } from "@effect/platform" +import { BunHttpServer, BunRuntime } from "@effect/platform-bun" +import { Layer } from "effect" + +export const listen = ( + app: Layer.Layer< + never, + never, + HttpPlatform.HttpPlatform | HttpServer.HttpServer + >, + port: number +) => + BunRuntime.runMain( + Layer.launch(Layer.provide(app, BunHttpServer.layer({ port }))) + ) +``` + +### Basic routing + +Routing refers to determining how an application responds to a client request to a particular endpoint, which is a URI (or path) and a specific HTTP request method (GET, POST, and so on). + +Route definition takes the following structure: + +``` +router.pipe(HttpRouter.METHOD(PATH, HANDLER)) +``` + +Where: + +- **router** is an instance of `Router` (`import type { Router } from "@effect/platform/Http/Router"`). +- **METHOD** is an HTTP request method, in lowercase (e.g., get, post, put, del). +- **PATH** is the path on the server (e.g., "/", "/user"). +- **HANDLER** is the action that gets executed when the route is matched. + +The following examples illustrate defining simple routes. + +Respond with `"Hello World!"` on the homepage: + +```ts +router.pipe(HttpRouter.get("/", HttpServerResponse.text("Hello World"))) +``` + +Respond to POST request on the root route (/), the application's home page: + +```ts +router.pipe(HttpRouter.post("/", HttpServerResponse.text("Got a POST request"))) +``` + +Respond to a PUT request to the `/user` route: + +```ts +router.pipe( + HttpRouter.put("/user", HttpServerResponse.text("Got a PUT request at /user")) +) +``` + +Respond to a DELETE request to the `/user` route: + +```ts +router.pipe( + HttpRouter.del( + "/user", + HttpServerResponse.text("Got a DELETE request at /user") + ) +) +``` + +### Serving static files + +To serve static files such as images, CSS files, and JavaScript files, use the `HttpServerResponse.file` built-in action. + +```ts +import { HttpRouter, HttpServer, HttpServerResponse } from "@effect/platform" +import { listen } from "./listen.js" + +const router = HttpRouter.empty.pipe( + HttpRouter.get("/", HttpServerResponse.file("index.html")) +) + +const app = router.pipe(HttpServer.serve()) + +listen(app, 3000) +``` + +Create an `index.html` file in your project directory: + +```html filename="index.html" + + + + + + index.html + + + + index.html + + +``` + +## Routing + +Routing refers to how an application's endpoints (URIs) respond to client requests. + +You define routing using methods of the `HttpRouter` object that correspond to HTTP methods; for example, `HttpRouter.get()` to handle GET requests and `HttpRouter.post` to handle POST requests. You can also use `HttpRouter.all()` to handle all HTTP methods. + +These routing methods specify a `Route.Handler` called when the application receives a request to the specified route (endpoint) and HTTP method. In other words, the application “listens” for requests that match the specified route(s) and method(s), and when it detects a match, it calls the specified handler. + +The following code is an example of a very basic route. + +```ts +// respond with "hello world" when a GET request is made to the homepage +HttpRouter.get("/", HttpServerResponse.text("Hello World")) +``` + +### Route methods + +A route method is derived from one of the HTTP methods, and is attached to an instance of the `HttpRouter` object. + +The following code is an example of routes that are defined for the GET and the POST methods to the root of the app. + +```ts +// GET method route +HttpRouter.get("/", HttpServerResponse.text("GET request to the homepage")) + +// POST method route +HttpRouter.post("/", HttpServerResponse.text("POST request to the homepage")) +``` + +`HttpRouter` supports methods that correspond to all HTTP request methods: `get`, `post`, and so on. + +There is a special routing method, `HttpRouter.all()`, used to load middleware functions at a path for **all** HTTP request methods. For example, the following handler is executed for requests to the route “/secret” whether using GET, POST, PUT, DELETE. + +```ts +HttpRouter.all( + "/secret", + HttpServerResponse.empty().pipe( + Effect.tap(Console.log("Accessing the secret section ...")) + ) +) +``` + +### Route paths + +Route paths, when combined with a request method, define the endpoints where requests can be made. Route paths can be specified as strings according to the following type: + +```ts +type PathInput = `/${string}` | "*" +``` + +> [!NOTE] +> Query strings are not part of the route path. + +Here are some examples of route paths based on strings. + +This route path will match requests to the root route, /. + +```ts +HttpRouter.get("/", HttpServerResponse.text("root")) +``` + +This route path will match requests to `/user`. + +```ts +HttpRouter.get("/user", HttpServerResponse.text("user")) +``` + +This route path matches requests to any path starting with `/user` (e.g., `/user`, `/users`, etc.) + +```ts +HttpRouter.get( + "/user*", + Effect.map(HttpServerRequest.HttpServerRequest, (req) => + HttpServerResponse.text(req.url) + ) +) +``` + +### Route parameters + +Route parameters are named URL segments that are used to capture the values specified at their position in the URL. By using a schema the captured values are populated in an object, with the name of the route parameter specified in the path as their respective keys. + +Route parameters are named segments in a URL that capture the values specified at those positions. These captured values are stored in an object, with the parameter names used as keys. + +For example: + +``` +Route path: /users/:userId/books/:bookId +Request URL: http://localhost:3000/users/34/books/8989 +params: { "userId": "34", "bookId": "8989" } +``` + +To define routes with parameters, include the parameter names in the path and use a schema to validate and parse these parameters, as shown below. + +```ts +import { HttpRouter, HttpServer, HttpServerResponse } from "@effect/platform" +import { Effect, Schema } from "effect" +import { listen } from "./listen.js" + +// Define the schema for route parameters +const Params = Schema.Struct({ + userId: Schema.String, + bookId: Schema.String +}) + +// Create a router with a route that captures parameters +const router = HttpRouter.empty.pipe( + HttpRouter.get( + "/users/:userId/books/:bookId", + HttpRouter.schemaPathParams(Params).pipe( + Effect.flatMap((params) => HttpServerResponse.json(params)) + ) + ) +) + +const app = router.pipe(HttpServer.serve()) + +listen(app, 3000) +``` + +### Response methods + +The methods on `HttpServerResponse` object in the following table can send a response to the client, and terminate the request-response cycle. If none of these methods are called from a route handler, the client request will be left hanging. + +| Method | Description | +| ------------ | ------------------------------ | +| **empty** | Sends an empty response. | +| **formData** | Sends form data. | +| **html** | Sends an HTML response. | +| **raw** | Sends a raw response. | +| **setBody** | Sets the body of the response. | +| **stream** | Sends a streaming response. | +| **text** | Sends a plain text response. | + +### Router + +Use the `HttpRouter` object to create modular, mountable route handlers. A `Router` instance is a complete middleware and routing system, often referred to as a "mini-app." + +The following example shows how to create a router as a module, define some routes, and mount the router module on a path in the main app. + +Create a file named `birds.ts` in your app directory with the following content: + +```ts +import { HttpRouter, HttpServerResponse } from "@effect/platform" + +export const birds = HttpRouter.empty.pipe( + HttpRouter.get("/", HttpServerResponse.text("Birds home page")), + HttpRouter.get("/about", HttpServerResponse.text("About birds")) +) +``` + +In your main application file, load the router module and mount it. + +```ts +import { HttpRouter, HttpServer } from "@effect/platform" +import { birds } from "./birds.js" +import { listen } from "./listen.js" + +// Create the main router and mount the birds router +const router = HttpRouter.empty.pipe(HttpRouter.mount("/birds", birds)) + +const app = router.pipe(HttpServer.serve()) + +listen(app, 3000) +``` + +When you run this code, your application will be able to handle requests to `/birds` and `/birds/about`, serving the respective responses defined in the `birds` router module. + +## Writing Middleware + +In this section, we'll build a simple "Hello World" application and demonstrate how to add three middleware functions: `myLogger` for logging, `requestTime` for displaying request timestamps, and `validateCookies` for validating incoming cookies. + +### Example Application + +Here is an example of a basic "Hello World" application with middleware. + +### Middleware `myLogger` + +This middleware logs "LOGGED" whenever a request passes through it. + +```ts +const myLogger = HttpMiddleware.make((app) => + Effect.gen(function* () { + console.log("LOGGED") + return yield* app + }) +) +``` + +To use the middleware, add it to the router using `HttpRouter.use()`: + +```ts +import { + HttpMiddleware, + HttpRouter, + HttpServer, + HttpServerResponse +} from "@effect/platform" +import { Effect } from "effect" +import { listen } from "./listen.js" + +const myLogger = HttpMiddleware.make((app) => + Effect.gen(function* () { + console.log("LOGGED") + return yield* app + }) +) + +const router = HttpRouter.empty.pipe( + HttpRouter.get("/", HttpServerResponse.text("Hello World")) +) + +const app = router.pipe(HttpRouter.use(myLogger), HttpServer.serve()) + +listen(app, 3000) +``` + +With this setup, every request to the app will log "LOGGED" to the terminal. Middleware execute in the order they are loaded. + +### Middleware `requestTime` + +Next, we'll create a middleware that records the timestamp of each HTTP request and provides it via a service called `RequestTime`. + +```ts +class RequestTime extends Context.Tag("RequestTime")() {} + +const requestTime = HttpMiddleware.make((app) => + Effect.gen(function* () { + return yield* app.pipe(Effect.provideService(RequestTime, Date.now())) + }) +) +``` + +Update the app to use this middleware and display the timestamp in the response: + +```ts +import { + HttpMiddleware, + HttpRouter, + HttpServer, + HttpServerResponse +} from "@effect/platform" +import { Context, Effect } from "effect" +import { listen } from "./listen.js" + +class RequestTime extends Context.Tag("RequestTime")() {} + +const requestTime = HttpMiddleware.make((app) => + Effect.gen(function* () { + return yield* app.pipe(Effect.provideService(RequestTime, Date.now())) + }) +) + +const router = HttpRouter.empty.pipe( + HttpRouter.get( + "/", + Effect.gen(function* () { + const requestTime = yield* RequestTime + const responseText = `Hello World
Requested at: ${requestTime}` + return yield* HttpServerResponse.html(responseText) + }) + ) +) + +const app = router.pipe(HttpRouter.use(requestTime), HttpServer.serve()) + +listen(app, 3000) +``` + +Now, when you make a request to the root path, the response will include the timestamp of the request. + +### Middleware `validateCookies` + +Finally, we'll create a middleware that validates incoming cookies. If the cookies are invalid, it sends a 400 response. + +Here's an example that validates cookies using an external service: + +```ts +class CookieError { + readonly _tag = "CookieError" +} + +const externallyValidateCookie = (testCookie: string | undefined) => + testCookie && testCookie.length > 0 + ? Effect.succeed(testCookie) + : Effect.fail(new CookieError()) + +const cookieValidator = HttpMiddleware.make((app) => + Effect.gen(function* () { + const req = yield* HttpServerRequest.HttpServerRequest + yield* externallyValidateCookie(req.cookies.testCookie) + return yield* app + }).pipe( + Effect.catchTag("CookieError", () => + HttpServerResponse.text("Invalid cookie") + ) + ) +) +``` + +Update the app to use the `cookieValidator` middleware: + +```ts +import { + HttpMiddleware, + HttpRouter, + HttpServer, + HttpServerRequest, + HttpServerResponse +} from "@effect/platform" +import { Effect } from "effect" +import { listen } from "./listen.js" + +class CookieError { + readonly _tag = "CookieError" +} + +const externallyValidateCookie = (testCookie: string | undefined) => + testCookie && testCookie.length > 0 + ? Effect.succeed(testCookie) + : Effect.fail(new CookieError()) + +const cookieValidator = HttpMiddleware.make((app) => + Effect.gen(function* () { + const req = yield* HttpServerRequest.HttpServerRequest + yield* externallyValidateCookie(req.cookies.testCookie) + return yield* app + }).pipe( + Effect.catchTag("CookieError", () => + HttpServerResponse.text("Invalid cookie") + ) + ) +) + +const router = HttpRouter.empty.pipe( + HttpRouter.get("/", HttpServerResponse.text("Hello World")) +) + +const app = router.pipe(HttpRouter.use(cookieValidator), HttpServer.serve()) + +listen(app, 3000) +``` + +Test the middleware with the following commands: + +```sh +curl -i http://localhost:3000 +curl -i http://localhost:3000 --cookie "testCookie=myvalue" +curl -i http://localhost:3000 --cookie "testCookie=" +``` + +This setup validates the `testCookie` and returns "Invalid cookie" if the validation fails, or "Hello World" if it passes. + +## Applying Middleware in Your Application + +Middleware functions are powerful tools that allow you to modify the request-response cycle. Middlewares can be applied at various levels to achieve different scopes of influence: + +- **Route Level**: Apply middleware to individual routes. +- **Router Level**: Apply middleware to a group of routes within a single router. +- **Server Level**: Apply middleware across all routes managed by a server. + +### Applying Middleware at the Route Level + +At the route level, middlewares are applied to specific endpoints, allowing for targeted modifications or enhancements such as logging, authentication, or parameter validation for a particular route. + +**Example** + +Here's a practical example showing how to apply middleware at the route level: + +```ts +import { + HttpMiddleware, + HttpRouter, + HttpServer, + HttpServerResponse +} from "@effect/platform" +import { Effect } from "effect" +import { listen } from "./listen.js" + +// Middleware constructor that logs the name of the middleware +const withMiddleware = (name: string) => + HttpMiddleware.make((app) => + Effect.gen(function* () { + console.log(name) // Log the middleware name when the route is accessed + return yield* app // Continue with the original application flow + }) + ) + +const router = HttpRouter.empty.pipe( + // Applying middleware to route "/a" + HttpRouter.get("/a", HttpServerResponse.text("a").pipe(withMiddleware("M1"))), + // Applying middleware to route "/b" + HttpRouter.get("/b", HttpServerResponse.text("b").pipe(withMiddleware("M2"))) +) + +const app = router.pipe(HttpServer.serve()) + +listen(app, 3000) +``` + +**Testing the Middleware** + +You can test the middleware by making requests to the respective routes and observing the console output: + +```sh +# Test route /a +curl -i http://localhost:3000/a +# Expected console output: M1 + +# Test route /b +curl -i http://localhost:3000/b +# Expected console output: M2 +``` + +### Applying Middleware at the Router Level + +Applying middleware at the router level is an efficient way to manage common functionalities across multiple routes within your application. Middleware can handle tasks such as logging, authentication, and response modifications before reaching the actual route handlers. + +**Example** + +Here's how you can structure and apply middleware across different routers using the `@effect/platform` library: + +```ts +import { + HttpMiddleware, + HttpRouter, + HttpServer, + HttpServerResponse +} from "@effect/platform" +import { Effect } from "effect" +import { listen } from "./listen.js" + +// Middleware constructor that logs the name of the middleware +const withMiddleware = (name: string) => + HttpMiddleware.make((app) => + Effect.gen(function* () { + console.log(name) // Log the middleware name when a route is accessed + return yield* app // Continue with the original application flow + }) + ) + +// Define Router1 with specific routes +const router1 = HttpRouter.empty.pipe( + HttpRouter.get("/a", HttpServerResponse.text("a")), // Middleware M4, M3, M1 will apply + HttpRouter.get("/b", HttpServerResponse.text("b")), // Middleware M4, M3, M1 will apply + // Apply Middleware at the router level + HttpRouter.use(withMiddleware("M1")), + HttpRouter.get("/c", HttpServerResponse.text("c")) // Middleware M4, M3 will apply +) + +// Define Router2 with specific routes +const router2 = HttpRouter.empty.pipe( + HttpRouter.get("/d", HttpServerResponse.text("d")), // Middleware M4, M2 will apply + HttpRouter.get("/e", HttpServerResponse.text("e")), // Middleware M4, M2 will apply + HttpRouter.get("/f", HttpServerResponse.text("f")), // Middleware M4, M2 will apply + // Apply Middleware at the router level + HttpRouter.use(withMiddleware("M2")) +) + +// Main router combining Router1 and Router2 +const router = HttpRouter.empty.pipe( + HttpRouter.mount("/r1", router1), + // Apply Middleware affecting all routes under /r1 + HttpRouter.use(withMiddleware("M3")), + HttpRouter.get("/g", HttpServerResponse.text("g")), // Only Middleware M4 will apply + HttpRouter.mount("/r2", router2), + // Apply Middleware affecting all routes + HttpRouter.use(withMiddleware("M4")) +) + +// Configure the application with the server middleware +const app = router.pipe(HttpServer.serve()) + +listen(app, 3000) +``` + +**Testing the Middleware** + +To ensure that the middleware is working as expected, you can test it by making HTTP requests to the defined routes and checking the console output for middleware logs: + +```sh +# Test route /a under router1 +curl -i http://localhost:3000/r1/a +# Expected console output: M4 M3 M1 + +# Test route /c under router1 +curl -i http://localhost:3000/r1/c +# Expected console output: M4 M3 + +# Test route /d under router2 +curl -i http://localhost:3000/r2/d +# Expected console output: M4 M2 + +# Test route /g under the main router +curl -i http://localhost:3000/g +# Expected console output: M4 +``` + +### Applying Middleware at the Server Level + +Applying middleware at the server level allows you to introduce certain functionalities, such as logging, authentication, or general request processing, that affect every request handled by the server. This ensures that all incoming requests, regardless of the route, pass through the applied middleware, making it an essential feature for global error handling, logging, or authentication. + +**Example** + +```ts +import { + HttpMiddleware, + HttpRouter, + HttpServer, + HttpServerResponse +} from "@effect/platform" +import { Effect } from "effect" +import { listen } from "./listen.js" + +// Middleware constructor that logs the name of the middleware +const withMiddleware = (name: string) => + HttpMiddleware.make((app) => + Effect.gen(function* () { + console.log(name) // Log the middleware name when the route is accessed + return yield* app // Continue with the original application flow + }) + ) + +const router = HttpRouter.empty.pipe( + HttpRouter.get("/a", HttpServerResponse.text("a").pipe(withMiddleware("M1"))), + HttpRouter.get("/b", HttpServerResponse.text("b")), + HttpRouter.use(withMiddleware("M2")), + HttpRouter.get("/", HttpServerResponse.text("root")) +) + +const app = router.pipe(HttpServer.serve(withMiddleware("M3"))) + +listen(app, 3000) +``` + +**Testing the Middleware** + +To confirm the middleware is functioning as intended, you can send HTTP requests to the defined routes and check the console for middleware logs: + +```sh +# Test route /a and observe the middleware logs +curl -i http://localhost:3000/a +# Expected console output: M3 M2 M1 - Middleware M3 (server-level), M2 (router-level), and M1 (route-level) apply. + +# Test route /b and observe the middleware logs +curl -i http://localhost:3000/b +# Expected console output: M3 M2 - Middleware M3 (server-level) and M2 (router-level) apply. + +# Test route / and observe the middleware logs +curl -i http://localhost:3000/ +# Expected console output: M3 M2 - Middleware M3 (server-level) and M2 (router-level) apply. +``` + +### Applying Multiple Middlewares + +Middleware functions are simply functions that transform a `Default` app into another `Default` app. This flexibility allows for stacking multiple middleware functions, much like composing functions in functional programming. The `flow` function from the `Effect` library facilitates this by enabling function composition. + +**Example** + +```ts +import { + HttpMiddleware, + HttpRouter, + HttpServer, + HttpServerResponse +} from "@effect/platform" +import { Effect, flow } from "effect" +import { listen } from "./listen.js" + +// Middleware constructor that logs the middleware's name when a route is accessed +const withMiddleware = (name: string) => + HttpMiddleware.make((app) => + Effect.gen(function* () { + console.log(name) // Log the middleware name + return yield* app // Continue with the original application flow + }) + ) + +// Setup routes and apply multiple middlewares using flow for function composition +const router = HttpRouter.empty.pipe( + HttpRouter.get( + "/a", + HttpServerResponse.text("a").pipe( + flow(withMiddleware("M1"), withMiddleware("M2")) + ) + ), + HttpRouter.get("/b", HttpServerResponse.text("b")), + // Apply combined middlewares to the entire router + HttpRouter.use(flow(withMiddleware("M3"), withMiddleware("M4"))), + HttpRouter.get("/", HttpServerResponse.text("root")) +) + +// Apply combined middlewares at the server level +const app = router.pipe( + HttpServer.serve(flow(withMiddleware("M5"), withMiddleware("M6"))) +) + +listen(app, 3000) +``` + +**Testing the Middleware Composition** + +To verify that the middleware is functioning as expected, you can send HTTP requests to the routes and check the console for the expected middleware log output: + +```sh +# Test route /a to see the output from multiple middleware layers +curl -i http://localhost:3000/a +# Expected console output: M6 M5 M4 M3 M2 M1 + +# Test route /b where fewer middleware are applied +curl -i http://localhost:3000/b +# Expected console output: M6 M5 M4 M3 + +# Test the root route to confirm top-level middleware application +curl -i http://localhost:3000/ +# Expected console output: M6 M5 +``` + +## Built-in middleware + +### Middleware Summary + +| Middleware | Description | +| --------------------- | --------------------------------------------------------------------------------------------------------------------------------- | +| **Logger** | Provides detailed logging of all requests and responses, aiding in debugging and monitoring application activities. | +| **xForwardedHeaders** | Manages `X-Forwarded-*` headers to accurately maintain client information such as IP addresses and host names in proxy scenarios. | + +### logger + +The `HttpMiddleware.logger` middleware enables logging for your entire application, providing insights into each request and response. Here's how to set it up: + +```ts +import { + HttpMiddleware, + HttpRouter, + HttpServer, + HttpServerResponse +} from "@effect/platform" +import { listen } from "./listen.js" + +const router = HttpRouter.empty.pipe( + HttpRouter.get("/", HttpServerResponse.text("Hello World")) +) + +// Apply the logger middleware globally +const app = router.pipe(HttpServer.serve(HttpMiddleware.logger)) + +listen(app, 3000) +/* +curl -i http://localhost:3000 +timestamp=... level=INFO fiber=#0 message="Listening on http://0.0.0.0:3000" +timestamp=... level=INFO fiber=#19 message="Sent HTTP response" http.span.1=8ms http.status=200 http.method=GET http.url=/ +timestamp=... level=INFO fiber=#20 cause="RouteNotFound: GET /favicon.ico not found + at ... + at http.server GET" http.span.2=4ms http.status=500 http.method=GET http.url=/favicon.ico +*/ +``` + +To disable the logger for specific routes, you can use `HttpMiddleware.withLoggerDisabled`: + +```ts +import { + HttpMiddleware, + HttpRouter, + HttpServer, + HttpServerResponse +} from "@effect/platform" +import { listen } from "./listen.js" + +// Create the router with routes that will and will not have logging +const router = HttpRouter.empty.pipe( + HttpRouter.get("/", HttpServerResponse.text("Hello World")), + HttpRouter.get( + "/no-logger", + HttpServerResponse.text("no-logger").pipe(HttpMiddleware.withLoggerDisabled) + ) +) + +// Apply the logger middleware globally +const app = router.pipe(HttpServer.serve(HttpMiddleware.logger)) + +listen(app, 3000) +/* +curl -i http://localhost:3000/no-logger +timestamp=2024-05-19T09:53:29.877Z level=INFO fiber=#0 message="Listening on http://0.0.0.0:3000" +*/ +``` + +### xForwardedHeaders + +This middleware handles `X-Forwarded-*` headers, useful when your app is behind a reverse proxy or load balancer and you need to retrieve the original client's IP and host information. +**WARNING:** The `X-Forwarded-*` headers are untrustworthy when no trusted reverse proxy or load balancer is between the client and server. + +```ts +import { + HttpMiddleware, + HttpRouter, + HttpServer, + HttpServerRequest, + HttpServerResponse +} from "@effect/platform" +import { Effect } from "effect" +import { listen } from "./listen.js" + +// Create a router and a route that logs request headers and remote address +const router = HttpRouter.empty.pipe( + HttpRouter.get( + "/", + Effect.gen(function* () { + const req = yield* HttpServerRequest.HttpServerRequest + console.log(req.headers) + console.log(req.remoteAddress) + return yield* HttpServerResponse.text("Hello World") + }) + ) +) + +// Set up the server with xForwardedHeaders middleware +const app = router.pipe(HttpServer.serve(HttpMiddleware.xForwardedHeaders)) + +listen(app, 3000) +/* +curl -H "X-Forwarded-Host: 192.168.1.1" -H "X-Forwarded-For: 192.168.1.1" http://localhost:3000 +timestamp=... level=INFO fiber=#0 message="Listening on http://0.0.0.0:3000" +{ + host: '192.168.1.1', + 'user-agent': 'curl/8.6.0', + accept: '*\/*', + 'x-forwarded-host': '192.168.1.1', + 'x-forwarded-for': '192.168.1.1' +} +{ _id: 'Option', _tag: 'Some', value: '192.168.1.1' } +*/ +``` + +## Error Handling + +### Catching Errors + +Below is an example illustrating how to catch and manage errors that occur during the execution of route handlers: + +```ts +import { HttpRouter, HttpServer, HttpServerResponse } from "@effect/platform" +import { Effect } from "effect" +import { listen } from "./listen.js" + +// Define routes that might throw errors or fail +const router = HttpRouter.empty.pipe( + HttpRouter.get( + "/throw", + Effect.sync(() => { + throw new Error("BROKEN") // This will intentionally throw an error + }) + ), + HttpRouter.get("/fail", Effect.fail("Uh oh!")) // This will intentionally fail +) + +// Configure the application to handle different types of errors +const app = router.pipe( + Effect.catchTags({ + RouteNotFound: () => + HttpServerResponse.text("Route Not Found", { status: 404 }) + }), + Effect.catchAllCause((cause) => + HttpServerResponse.text(cause.toString(), { status: 500 }) + ), + HttpServer.serve() +) + +listen(app, 3000) +``` + +You can test the error handling setup with `curl` commands by trying to access routes that trigger errors: + +```sh +# Accessing a route that does not exist +curl -i http://localhost:3000/nonexistent + +# Accessing the route that throws an error +curl -i http://localhost:3000/throw + +# Accessing the route that fails +curl -i http://localhost:3000/fail +``` + +## Validations + +Validation is a critical aspect of handling HTTP requests to ensure that the data your server receives is as expected. We'll explore how to validate headers and cookies using the `@effect/platform` and `effect/Schema` libraries, which provide structured and robust methods for these tasks. + +### Headers + +Headers often contain important information needed by your application, such as content types, authentication tokens, or session data. Validating these headers ensures that your application can trust and correctly process the information it receives. + +```ts +import { + HttpRouter, + HttpServer, + HttpServerRequest, + HttpServerResponse +} from "@effect/platform" +import { Effect, Schema } from "effect" +import { listen } from "./listen.js" + +const router = HttpRouter.empty.pipe( + HttpRouter.get( + "/", + Effect.gen(function* () { + // Define the schema for expected headers and validate them + const headers = yield* HttpServerRequest.schemaHeaders( + Schema.Struct({ test: Schema.String }) + ) + return yield* HttpServerResponse.text("header: " + headers.test) + }).pipe( + // Handle parsing errors + Effect.catchTag("ParseError", (e) => + HttpServerResponse.text(`Invalid header: ${e.message}`) + ) + ) + ) +) + +const app = router.pipe(HttpServer.serve()) + +listen(app, 3000) +``` + +You can test header validation using the following `curl` commands: + +```sh +# Request without the required header +curl -i http://localhost:3000 + +# Request with the valid header +curl -i -H "test: myvalue" http://localhost:3000 +``` + +### Cookies + +Cookies are commonly used to maintain session state or user preferences. Validating cookies ensures that the data they carry is intact and as expected, enhancing security and application integrity. + +Here's how you can validate cookies received in HTTP requests: + +```ts +import { + Cookies, + HttpRouter, + HttpServer, + HttpServerRequest, + HttpServerResponse +} from "@effect/platform" +import { Effect, Schema } from "effect" +import { listen } from "./listen.js" + +const router = HttpRouter.empty.pipe( + HttpRouter.get( + "/", + Effect.gen(function* () { + const cookies = yield* HttpServerRequest.schemaCookies( + Schema.Struct({ test: Schema.String }) + ) + return yield* HttpServerResponse.text("cookie: " + cookies.test) + }).pipe( + Effect.catchTag("ParseError", (e) => + HttpServerResponse.text(`Invalid cookie: ${e.message}`) + ) + ) + ) +) + +const app = router.pipe(HttpServer.serve()) + +listen(app, 3000) +``` + +Validate the cookie handling with the following `curl` commands: + +```sh +# Request without any cookies +curl -i http://localhost:3000 + +# Request with the valid cookie +curl -i http://localhost:3000 --cookie "test=myvalue" +``` + +## ServerRequest + +### How do I get the raw request? + +The native request object depends on the platform you are using, and it is not directly modeled in `@effect/platform`. Instead, you need to refer to the specific platform package you are working with, such as `@effect/platform-node` or `@effect/platform-bun`. + +Here is an example using Node.js: + +```ts +import { + HttpRouter, + HttpServer, + HttpServerRequest, + HttpServerResponse +} from "@effect/platform" +import { NodeHttpServer, NodeHttpServerRequest } from "@effect/platform-node" +import { Effect } from "effect" +import { listen } from "./listen.js" + +const router = HttpRouter.empty.pipe( + HttpRouter.get( + "/", + Effect.gen(function* () { + const req = yield* HttpServerRequest.HttpServerRequest + const raw = NodeHttpServerRequest.toIncomingMessage(req) + console.log(raw) + return HttpServerResponse.empty() + }) + ) +) + +listen(HttpServer.serve(router), 3000) +``` + +## Conversions + +### toWebHandler + +The `toWebHandler` function converts a `Default` (i.e. a type of `HttpApp` that specifically produces a `ServerResponse` as its output) into a web handler that can process `Request` objects and return `Response` objects. + +```ts +import { HttpApp, HttpRouter, HttpServerResponse } from "@effect/platform" + +// Define the router with some routes +const router = HttpRouter.empty.pipe( + HttpRouter.get("/", HttpServerResponse.text("content 1")), + HttpRouter.get("/foo", HttpServerResponse.text("content 2")) +) + +// Convert the router to a web handler +// const handler: (request: Request) => Promise +const handler = HttpApp.toWebHandler(router) + +// Test the handler with a request +const response = await handler(new Request("http://localhost:3000/foo")) +console.log(await response.text()) // Output: content 2 +``` + +# Url + +The `Url` module provides utilities for constructing and working with `URL` objects in a functional style. It includes: + +- A safe constructor for parsing URLs from strings. +- Functions for immutably updating `URL` properties like `host`, `href`, and `search`. +- Tools for reading and modifying URL parameters using the `UrlParams` module. +- A focus on immutability, creating new `URL` instances for every change. + +## Creating a URL + +### fromString + +This function takes a string and attempts to parse it into a `URL` object. If the string is invalid, it returns an `Either.Left` containing an `IllegalArgumentException` with the error details. Otherwise, it returns an `Either.Right` containing the parsed `URL`. + +You can optionally provide a `base` parameter to resolve relative URLs. When supplied, the function treats the input `url` as relative to the `base`. + +**Example** (Parsing a URL with Optional Base) + +```ts +import { Url } from "@effect/platform" +import { Either } from "effect" + +// Parse an absolute URL +// +// ┌─── Either +// ▼ +const parsed = Url.fromString("https://example.com/path") + +if (Either.isRight(parsed)) { + console.log("Parsed URL:", parsed.right.toString()) +} else { + console.log("Error:", parsed.left.message) +} +// Output: Parsed URL: https://example.com/path + +// Parse a relative URL with a base +const relativeParsed = Url.fromString("/relative-path", "https://example.com") + +if (Either.isRight(relativeParsed)) { + console.log("Parsed relative URL:", relativeParsed.right.toString()) +} else { + console.log("Error:", relativeParsed.left.message) +} +// Output: Parsed relative URL: https://example.com/relative-path +``` + +## Immutably Changing URL Properties + +The `Url` module offers a set of functions for updating properties of a `URL` object without modifying the original instance. These functions create and return a new `URL` with the specified updates, preserving the immutability of the original. + +### Available Setters + +| Setter | Description | +| ------------- | --------------------------------------------------------- | +| `setHash` | Updates the hash fragment of the URL. | +| `setHost` | Updates the host (domain and port) of the URL. | +| `setHostname` | Updates the domain of the URL without modifying the port. | +| `setHref` | Replaces the entire URL string. | +| `setPassword` | Updates the password used for authentication. | +| `setPathname` | Updates the path of the URL. | +| `setPort` | Updates the port of the URL. | +| `setProtocol` | Updates the protocol (e.g., `http`, `https`). | +| `setSearch` | Updates the query string of the URL. | +| `setUsername` | Updates the username used for authentication. | + +**Example** (Using Setters to Modify URL Properties) + +```ts +import { Url } from "@effect/platform" +import { pipe } from "effect" + +const myUrl = new URL("https://example.com") + +// Changing protocol, host, and port +const newUrl = pipe( + myUrl, + Url.setProtocol("http:"), + Url.setHost("google.com"), + Url.setPort("8080") +) + +console.log("Original:", myUrl.toString()) +// Output: Original: https://example.com/ + +console.log("New:", newUrl.toString()) +// Output: New: http://google.com:8080/ +``` + +### mutate + +For more advanced modifications, use the `mutate` function. It clones the original `URL` object and applies a callback to the clone, allowing multiple updates at once. + +**Example** (Applying Multiple Changes with `mutate`) + +```ts +import { Url } from "@effect/platform" + +const myUrl = new URL("https://example.com") + +const mutatedUrl = Url.mutate(myUrl, (url) => { + url.username = "user" + url.password = "pass" +}) + +console.log("Mutated:", mutatedUrl.toString()) +// Output: Mutated: https://user:pass@example.com/ +``` + +## Reading and Writing URL Parameters + +The `Url` module provides utilities for working with URL query parameters. These utilities allow you to read existing parameters and write new ones, all while maintaining immutability. This functionality is supported by the `UrlParams` module. + +You can extract the query parameters from a `URL` object using the `urlParams` function. + +To modify or add query parameters, use the `setUrlParams` function. This function creates a new `URL` with the updated query string. + +**Example** (Reading and Writing Parameters) + +```ts +import { Url, UrlParams } from "@effect/platform" + +const myUrl = new URL("https://example.com?foo=bar") + +// Read parameters +const params = Url.urlParams(myUrl) + +console.log(params) +// Output: [ [ 'foo', 'bar' ] ] + +// Write parameters +const updatedUrl = Url.setUrlParams( + myUrl, + UrlParams.fromInput([["key", "value"]]) +) + +console.log(updatedUrl.toString()) +// Output: https://example.com/?key=value +``` + +### Modifying URL Parameters + +The `modifyUrlParams` function allows you to read, modify, and overwrite URL parameters in a single operation. + +**Example** (Appending a Parameter to a URL) + +```ts +import { Url, UrlParams } from "@effect/platform" + +const myUrl = new URL("https://example.com?foo=bar") + +const changedUrl = Url.modifyUrlParams(myUrl, UrlParams.append("key", "value")) + +console.log(changedUrl.toString()) +// Output: https://example.com/?foo=bar&key=value +``` + +# OpenApiJsonSchema + +The `OpenApiJsonSchema` module provides utilities to transform `Schema` objects into JSON schemas that comply with the OpenAPI Specification. These utilities are especially helpful for generating OpenAPI documentation or working with tools that require OpenAPI-compliant schemas. + +## Creating a JSON Schema from a Schema + +This module enables you to convert `Schema` objects into OpenAPI-compatible JSON schemas, making it easy to integrate with tools like Swagger or other OpenAPI-based frameworks. + +**Example** (Generating a JSON Schema from a String Schema) + +```ts +import { OpenApiJsonSchema } from "@effect/platform" +import { Schema } from "effect" + +const schema = Schema.String + +// Convert the schema to OpenAPI JSON Schema +const openApiSchema = OpenApiJsonSchema.make(schema) + +console.log(JSON.stringify(openApiSchema, null, 2)) +/* +Output: +{ + "type": "string" +} +*/ +``` + +## Differences from JSONSchema + +The `OpenApiJsonSchema` module differs from the `JSONSchema` module in several ways. These differences are tailored to align with the OpenAPI Specification. + +### `$schema` Property Omission + +OpenAPI schemas do not include the `$schema` property, while JSON schemas do. + +**Example** (Comparison of `$schema` Property) + +```ts +import { OpenApiJsonSchema } from "@effect/platform" +import { JSONSchema, Schema } from "effect" + +const schema = Schema.String + +const openApiSchema = OpenApiJsonSchema.make(schema) +const jsonSchema = JSONSchema.make(schema) + +console.log(JSON.stringify(openApiSchema, null, 2)) +/* +Output: +{ + "type": "string" +} +*/ + +console.log(JSON.stringify(jsonSchema, null, 2)) +/* +Output: +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "string" +} +*/ +``` + +### Handling of `null` Values + +OpenAPI does not support `{ "type": "null" }`. Instead, it uses an `enum` containing `null` to represent nullable values. + +**Example** (Representation of `null` Values) + +```ts +import { OpenApiJsonSchema } from "@effect/platform" +import { JSONSchema, Schema } from "effect" + +const schema = Schema.Null + +const openApiSchema = OpenApiJsonSchema.make(schema) +const jsonSchema = JSONSchema.make(schema) + +console.log(JSON.stringify(openApiSchema, null, 2)) +/* +Output: +{ + "enum": [ + null + ] +} +*/ + +console.log(JSON.stringify(jsonSchema, null, 2)) +/* +Output: +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "null" +} +*/ +``` + +### Nullable Values + +OpenAPI uses the `nullable` property to indicate that a value can be `null`, whereas JSON schemas use an `anyOf` structure. + +**Example** (Nullable Property Representation) + +```ts +import { OpenApiJsonSchema } from "@effect/platform" +import { JSONSchema, Schema } from "effect" + +const schema = Schema.NullOr(Schema.String) + +const openApiSchema = OpenApiJsonSchema.make(schema) +const jsonSchema = JSONSchema.make(schema) + +console.log(JSON.stringify(openApiSchema, null, 2)) +/* +Output: +{ + "type": "string", + "nullable": true +} +*/ + +console.log(JSON.stringify(jsonSchema, null, 2)) +/* +Output: +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] +} +*/ +``` + +### `contentSchema` Support + +OpenAPI schemas include a `contentSchema` property, which allows you to describe the structure of the content for a media type (e.g., `application/json`). This feature is not available in JSON schemas (Draft 7), making `contentSchema` particularly useful for defining structured payloads in OpenAPI documentation. + +**Note**: Use `contentSchema` to define the internal structure of media types like `application/json` in OpenAPI specifications. This property provides clarity and detail for tools and users interacting with the API, especially when handling structured payloads. + +**Example** (Defining a Schema with `contentSchema` for JSON Content) + +```ts +import { OpenApiJsonSchema } from "@effect/platform" +import { JSONSchema, Schema } from "effect" + +// Define a schema for parsing JSON content +const schema = Schema.parseJson(Schema.Struct({ a: Schema.String })) + +const openApiSchema = OpenApiJsonSchema.make(schema) +const jsonSchema = JSONSchema.make(schema) + +console.log(JSON.stringify(openApiSchema, null, 2)) +/* +Output: +{ + "type": "string", + "contentMediaType": "application/json", + "contentSchema": { + "type": "object", + "required": [ + "a" + ], + "properties": { + "a": { + "type": "string" + } + }, + "additionalProperties": false + } +} +*/ + +console.log(JSON.stringify(jsonSchema, null, 2)) +/* +Output: +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "required": [ + "a" + ], + "properties": { + "a": { + "type": "string" + } + }, + "additionalProperties": false +} +*/ +``` + +### makeWithDefs + +The `makeWithDefs` function generates OpenAPI-compatible JSON schemas and collects schema definitions in a shared object. This is especially useful for consolidating multiple schemas into a single OpenAPI specification, enabling schema reuse across your API. + +**Example** (Generating OpenAPI Schema with Definitions) + +```ts +import { OpenApiJsonSchema } from "@effect/platform" +import { Schema } from "effect" + +// Define a schema with an identifier annotation +const schema = Schema.Struct({ a: Schema.String }).annotations({ + identifier: "MyStruct" +}) + +// Create a definitions object +const defs = {} + +// Generate the OpenAPI schema while collecting definitions +const openApiSchema = OpenApiJsonSchema.makeWithDefs(schema, { defs }) + +console.log(JSON.stringify(openApiSchema, null, 2)) +/* +Output: +{ + "$ref": "#/components/schemas/MyStruct" +} +*/ + +console.log(JSON.stringify(defs, null, 2)) +/* +Output: +{ + "MyStruct": { + "type": "object", + "required": [ + "a" + ], + "properties": { + "a": { + "type": "string" + } + }, + "additionalProperties": false + } +} +*/ +``` + +**Example** (Combining Multiple Schemas into One OpenAPI Specification) + +```ts +import { OpenApiJsonSchema } from "@effect/platform" +import { Schema } from "effect" + +// Define multiple schemas with unique identifiers +const schema1 = Schema.Struct({ a: Schema.String }).annotations({ + identifier: "MyStruct1" +}) +const schema2 = Schema.Struct({ b: Schema.Number }).annotations({ + identifier: "MyStruct2" +}) + +// Create a shared definitions object +const defs = {} + +// Use `makeWithDefs` to generate schemas for API paths +const paths = { + paths: { + "/path1": { + get: { + responses: { + "200": { + content: { + "application/json": { + schema: OpenApiJsonSchema.makeWithDefs(schema1, { defs }) + } + } + } + } + } + }, + "/path2": { + get: { + responses: { + "200": { + content: { + "application/json": { + schema: OpenApiJsonSchema.makeWithDefs(schema2, { defs }) + } + } + } + } + } + } + } +} + +// Combine paths and definitions into a single OpenAPI schema +const openApiSchema = { + components: { + schemas: defs + }, + paths +} + +console.log(JSON.stringify(openApiSchema, null, 2)) +/* +Output: +{ + "components": { + "schemas": { + "MyStruct1": { + "type": "object", + "required": [ + "a" + ], + "properties": { + "a": { + "type": "string" + } + }, + "additionalProperties": false + }, + "MyStruct2": { + "type": "object", + "required": [ + "b" + ], + "properties": { + "b": { + "type": "number" + } + }, + "additionalProperties": false + } + } + }, + "paths": { + "paths": { + "/path1": { + "get": { + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MyStruct1" + } + } + } + } + } + } + }, + "/path2": { + "get": { + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MyStruct2" + } + } + } + } + } + } + } + } + } +} +*/ +``` + +# HttpLayerRouter + +The experimental `HttpLayerRouter` module provides a simplified way to create HTTP servers. +It aims to simplify the process of defining routes and registering other HTTP +services like `HttpApi` or `RpcServer`'s. + +## Registering routes + +```ts +import * as NodeHttpServer from "@effect/platform-node/NodeHttpServer" +import * as NodeRuntime from "@effect/platform-node/NodeRuntime" +import * as HttpLayerRouter from "@effect/platform/HttpLayerRouter" +import * as HttpServerResponse from "@effect/platform/HttpServerResponse" +import * as Effect from "effect/Effect" +import * as Layer from "effect/Layer" +import { createServer } from "http" + +// Here is how you can register a simple GET route +const HelloRoute = Layer.effectDiscard( + Effect.gen(function* () { + // First, we need to access the `HttpRouter` service + const router = yield* HttpLayerRouter.HttpRouter + + // Then, we can add a new route to the router + yield* router.add("GET", "/hello", HttpServerResponse.text("Hello, World!")) + }) +) + +// You can also use the `HttpLayerRouter.use` function to register a route +const GoodbyeRoute = HttpLayerRouter.use( + Effect.fn(function* (router) { + // The `router` parameter is the `HttpRouter` service + yield* router.add( + "GET", + "/goodbye", + HttpServerResponse.text("Goodbye, World!") + ) + }) +) +// Or use `HttpLayerRouter.add/addAll` for simple routes +const SimpleRoute = HttpLayerRouter.add( + "GET", + "/simple", + HttpServerResponse.text("Simply fantastic!") +) + +const AllRoutes = Layer.mergeAll(HelloRoute, GoodbyeRoute, SimpleRoute) + +// To start the server, we use `HttpLayerRouter.serve` with the routes layer +HttpLayerRouter.serve(AllRoutes).pipe( + Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 })), + Layer.launch, + NodeRuntime.runMain +) +``` + +## Applying middleware + +```ts +import * as HttpLayerRouter from "@effect/platform/HttpLayerRouter" +import * as HttpMiddleware from "@effect/platform/HttpMiddleware" +import * as HttpServerResponse from "@effect/platform/HttpServerResponse" +import * as Context from "effect/Context" +import * as Effect from "effect/Effect" +import * as Layer from "effect/Layer" + +// Here is a service that we want to provide to every HTTP request +class CurrentSession extends Context.Tag("CurrentSession")< + CurrentSession, + { + readonly token: string + } +>() {} + +// Using the `HttpLayerRouter.middleware` function, we can create a middleware +// that provides the `CurrentSession` service to every HTTP request. +const SessionMiddleware = HttpLayerRouter.middleware<{ + provides: CurrentSession +}>()( + Effect.gen(function* () { + yield* Effect.log("SessionMiddleware initialized") + + return (httpEffect) => + Effect.provideService(httpEffect, CurrentSession, { + token: "dummy-token" + }) + }) +) + +// And here is an example of global middleware, that modifies the HTTP response. +// Global middleware directly returns a `Layer`. +const CorsMiddleware = HttpLayerRouter.middleware(HttpMiddleware.cors(), { + global: true +}) +// You can also use `HttpLayerRouter.cors()` to create a CORS middleware + +const HelloRoute = HttpLayerRouter.add( + "GET", + "/hello", + Effect.gen(function* () { + // We can now access the `CurrentSession` service in our route handler + const session = yield* CurrentSession + return HttpServerResponse.text( + `Hello, World! Your session token is: ${session.token}` + ) + }) +).pipe( + // We can provide the `SessionMiddleware.layer` to the `HelloRoute` layer + Layer.provide(SessionMiddleware.layer), + // And we can also provide the `CorsMiddleware` layer to handle CORS + Layer.provide(CorsMiddleware) +) +``` + +## Interdependent middleware + +If middleware depends on another middleware, you can use the `.combine` api to +combine them. + +```ts +import * as HttpLayerRouter from "@effect/platform/HttpLayerRouter" +import * as HttpServerResponse from "@effect/platform/HttpServerResponse" +import * as Context from "effect/Context" +import * as Effect from "effect/Effect" +import * as Layer from "effect/Layer" + +class CurrentSession extends Context.Tag("CurrentSession")< + CurrentSession, + { + readonly token: string + } +>() {} + +const SessionMiddleware = HttpLayerRouter.middleware<{ + provides: CurrentSession +}>()( + Effect.gen(function* () { + yield* Effect.log("SessionMiddleware initialized") + + return (httpEffect) => + Effect.provideService(httpEffect, CurrentSession, { + token: "dummy-token" + }) + }) +) + +// Here is a middleware that uses the `CurrentSession` service +const LogMiddleware = HttpLayerRouter.middleware( + Effect.gen(function* () { + yield* Effect.log("LogMiddleware initialized") + + return Effect.fn(function* (httpEffect) { + const session = yield* CurrentSession + yield* Effect.log(`Current session token: ${session.token}`) + return yield* httpEffect + }) + }) +) + +// We can then use the .combine method to combine the middlewares +const LogAndSessionMiddleware = LogMiddleware.combine(SessionMiddleware) + +const HelloRoute = HttpLayerRouter.add( + "GET", + "/hello", + Effect.gen(function* () { + const session = yield* CurrentSession + return HttpServerResponse.text( + `Hello, World! Your session token is: ${session.token}` + ) + }) +).pipe(Layer.provide(LogAndSessionMiddleware.layer)) +``` + +## Registering a HttpApi + +```ts +import { + HttpApi, + HttpApiBuilder, + HttpApiEndpoint, + HttpApiGroup, + HttpApiScalar, + HttpLayerRouter +} from "@effect/platform" +import { NodeHttpServer, NodeRuntime } from "@effect/platform-node" +import { Effect, Layer } from "effect" +import { createServer } from "http" + +// First, we define our HttpApi +class MyApi extends HttpApi.make("api").add( + HttpApiGroup.make("users") + .add(HttpApiEndpoint.get("me", "/me")) + .prefix("/users") +) {} + +// Implement the handlers for the API +const UsersApiLayer = HttpApiBuilder.group(MyApi, "users", (handers) => + handers.handle("me", () => Effect.void) +) + +// Use `HttpLayerRouter.addHttpApi` to register the API with the router +const HttpApiRoutes = HttpLayerRouter.addHttpApi(MyApi, { + openapiPath: "/docs/openapi.json" +}).pipe( + // Provide the api handlers layer + Layer.provide(UsersApiLayer) +) + +// Create a /docs route for the API documentation +const DocsRoute = HttpApiScalar.layerHttpLayerRouter({ + api: MyApi, + path: "/docs" +}) + +// Finally, we merge all routes and serve them using the Node HTTP server +const AllRoutes = Layer.mergeAll(HttpApiRoutes, DocsRoute).pipe( + Layer.provide(HttpLayerRouter.cors()) +) + +HttpLayerRouter.serve(AllRoutes).pipe( + Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 })), + Layer.launch, + NodeRuntime.runMain +) +``` + +## Registering a RpcServer + +```ts +import { HttpLayerRouter } from "@effect/platform" +import { NodeHttpServer, NodeRuntime } from "@effect/platform-node" +import { Rpc, RpcGroup, RpcSerialization, RpcServer } from "@effect/rpc" +import { Effect, Layer, Schema } from "effect" +import { createServer } from "http" + +export class User extends Schema.Class("User")({ + id: Schema.String, + name: Schema.String +}) {} + +// Define a group of RPCs +export class UserRpcs extends RpcGroup.make( + Rpc.make("UserById", { + success: User, + error: Schema.String, // Indicates that errors, if any, will be returned as strings + payload: { + id: Schema.String + } + }) +) {} + +const UserHandlers = UserRpcs.toLayer({ + UserById: ({ id }) => Effect.succeed(new User({ id, name: "John Doe" })) +}) + +// Use `HttpLayerRouter` to register the rpc server +const RpcRoute = RpcServer.layerHttpRouter({ + group: UserRpcs, + path: "/rpc" +}).pipe( + Layer.provide(UserHandlers), + Layer.provide(RpcSerialization.layerJson), + Layer.provide(HttpLayerRouter.cors()) // provide CORS middleware +) + +// Start the HTTP server with the RPC route +HttpLayerRouter.serve(RpcRoute).pipe( + Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 })), + Layer.launch, + NodeRuntime.runMain +) +``` + +## Create a web handler + +```ts +import * as HttpLayerRouter from "@effect/platform/HttpLayerRouter" +import * as HttpServerResponse from "@effect/platform/HttpServerResponse" +import * as Effect from "effect/Effect" + +const HelloRoute = HttpLayerRouter.use( + Effect.fn(function* (router) { + yield* router.add( + "GET", + "/hello", + HttpServerResponse.text("Hellow, World!") + ) + }) +) + +const { dispose, handler } = HttpLayerRouter.toWebHandler(HelloRoute) + +// When the process is interrupted, we want to clean up resources +process.on("SIGINT", () => { + dispose().then( + () => { + process.exit(0) + }, + () => { + process.exit(1) + } + ) +}) + +// Use the handler in your server setup +export { handler } +``` diff --git a/repos/effect/packages/platform/docgen.json b/repos/effect/packages/platform/docgen.json new file mode 100644 index 0000000..b6cccb0 --- /dev/null +++ b/repos/effect/packages/platform/docgen.json @@ -0,0 +1,36 @@ +{ + "$schema": "../../node_modules/@effect/docgen/schema.json", + "srcLink": "https://github.com/Effect-TS/effect/tree/main/packages/platform/src/", + "exclude": ["src/internal/**/*.ts"], + "examplesCompilerOptions": { + "noEmit": true, + "strict": true, + "skipLibCheck": true, + "moduleResolution": "Bundler", + "module": "ES2022", + "target": "ES2022", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "paths": { + "effect": ["../../../effect/src/index.js"], + "effect/*": ["../../../effect/src/*.js"], + "@effect/cluster": ["../../../cluster/src/index.js"], + "@effect/cluster/*": ["../../../cluster/src/*.js"], + "@effect/experimental": ["../../../experimental/src/index.js"], + "@effect/experimental/*": ["../../../experimental/src/*.js"], + "@effect/platform": ["../../../platform/src/index.js"], + "@effect/platform/*": ["../../../platform/src/*.js"], + "@effect/platform-node": ["../../../platform-node/src/index.js"], + "@effect/platform-node/*": ["../../../platform-node/src/*.js"], + "@effect/platform-node-shared": [ + "../../../platform-node-shared/src/index.js" + ], + "@effect/platform-node-shared/*": [ + "../../../platform-node-shared/src/*.js" + ], + "@effect/rpc": ["../../../rpc/src/index.js"], + "@effect/rpc/*": ["../../../rpc/src/*.js"], + "@effect/sql": ["../../../sql/src/index.js"], + "@effect/sql/*": ["../../../sql/src/*.js"] + } + } +} diff --git a/repos/effect/packages/platform/dtslint/HttpApiClient.tst.ts b/repos/effect/packages/platform/dtslint/HttpApiClient.tst.ts new file mode 100644 index 0000000..278ab09 --- /dev/null +++ b/repos/effect/packages/platform/dtslint/HttpApiClient.tst.ts @@ -0,0 +1,201 @@ +import type { HttpApiError, HttpClientError } from "@effect/platform" +import { HttpApi, HttpApiClient, HttpApiEndpoint, HttpApiGroup, HttpApiMiddleware, HttpClient } from "@effect/platform" +import type { Schema } from "effect" +import { Effect } from "effect" +import type { ParseError } from "effect/ParseResult" +import { describe, expect, it } from "tstyche" + +declare const ApiError: Schema.Schema<"ApiError", "ApiErrorEncoded", "ApiErrorR"> + +declare const Group1Error: Schema.Schema<"Group1Error", "Group1ErrorEncoded", "Group1ErrorR"> +declare const EndpointAError: Schema.Schema<"EndpointAError", "EndpointAErrorEncoded", "EndpointAErrorR"> +declare const EndpointASuccess: Schema.Schema<"EndpointASuccess", "EndpointASuccessEncoded", "EndpointASuccessR"> +declare const EndpointBError: Schema.Schema<"EndpointBError", "EndpointBErrorEncoded", "EndpointBErrorR"> +declare const EndpointBSuccess: Schema.Schema<"EndpointBSuccess", "EndpointBSuccessEncoded", "EndpointBSuccessR"> + +declare const EndpointASecurityError: Schema.Schema< + "EndpointASecurityError", + "EndpointASecurityErrorEncoded", + "EndpointASecurityErrorR" +> +class EndpointASecurity extends HttpApiMiddleware.Tag()("EndpointASecurity", { + failure: EndpointASecurityError +}) {} +declare const EndpointBSecurityError: Schema.Schema< + "EndpointBSecurityError", + "EndpointBSecurityErrorEncoded", + "EndpointBSecurityErrorR" +> +class EndpointBSecurity extends HttpApiMiddleware.Tag()("EndpointBSecurity", { + failure: EndpointBSecurityError +}) {} +declare const Group1SecurityError: Schema.Schema< + "Group1SecurityError", + "Group1SecurityErrorEncoded", + "Group1SecurityErrorR" +> +class Group1Security extends HttpApiMiddleware.Tag()("Group1Security", { + failure: Group1SecurityError +}) {} +declare const ApiSecurityError: Schema.Schema< + "ApiSecurityError", + "ApiSecurityErrorEncoded", + "ApiSecurityErrorR" +> +class ApiSecurity extends HttpApiMiddleware.Tag()("ApiSecurity", { + failure: ApiSecurityError +}) {} + +const EndpointA = HttpApiEndpoint.post("EndpointA", "/endpoint_a") + .middleware(EndpointASecurity) + .addError(EndpointAError) + .addSuccess(EndpointASuccess) +const EndpointB = HttpApiEndpoint.post("EndpointB", "/endpoint_b") + .middleware(EndpointBSecurity) + .addError(EndpointBError) + .addSuccess(EndpointBSuccess) + +const Group1 = HttpApiGroup.make("Group1") + .middleware(Group1Security) + .addError(Group1Error) + .add(EndpointA) + .add(EndpointB) + +declare const Group2Error: Schema.Schema<"Group2Error", "Group2ErrorEncoded", "Group2ErrorR"> +declare const EndpointCError: Schema.Schema<"EndpointCError", "EndpointCErrorEncoded", "EndpointCErrorR"> +declare const EndpointCSuccess: Schema.Schema<"EndpointCSuccess", "EndpointCSuccessEncoded", "EndpointCSuccessR"> + +const EndpointC = HttpApiEndpoint.post("EndpointC", "/endpoint_c") + .addError(EndpointCError) + .addSuccess(EndpointCSuccess) +const Group2 = HttpApiGroup.make("Group2") + .addError(Group2Error) + .add(EndpointC) + +const TestApi = HttpApi.make("test") + .middleware(ApiSecurity) + .addError(ApiError) + .add(Group1) + .add(Group2) + +describe("HttpApiClient", () => { + it("endpoint", () => { + Effect.gen(function*() { + const clientEndpointEffect = HttpApiClient.endpoint(TestApi, { + httpClient: yield* HttpClient.HttpClient, + group: "Group1", + endpoint: "EndpointA" + }) + expect>().type.toBe() + expect>().type.toBe< + | "ApiErrorR" + | "Group1ErrorR" + | "EndpointAErrorR" + | "EndpointASuccessR" + | "EndpointASecurityErrorR" + | "Group1SecurityErrorR" + | "ApiSecurityErrorR" + >() + + const clientEndpoint = yield* clientEndpointEffect + + expect(clientEndpoint({ withResponse: false })).type.toBe< + Effect.Effect< + "EndpointASuccess", + | "ApiError" + | "Group1Error" + | "EndpointAError" + | "EndpointASecurityError" + | "Group1SecurityError" + | "ApiSecurityError" + | HttpApiError.HttpApiDecodeError + | HttpClientError.HttpClientError + | ParseError + > + >() + }) + }) + + it("group", () => { + Effect.gen(function*() { + const clientGroupEffect = HttpApiClient.group(TestApi, { + httpClient: yield* HttpClient.HttpClient, + group: "Group1" + }) + + expect>().type.toBe() + + expect>().type.toBe< + | "ApiErrorR" + | "Group1ErrorR" + | "EndpointAErrorR" + | "EndpointASuccessR" + | "EndpointBErrorR" + | "EndpointBSuccessR" + | "EndpointASecurityErrorR" + | "EndpointBSecurityErrorR" + | "Group1SecurityErrorR" + | "ApiSecurityErrorR" + >() + + const clientGroup = yield* clientGroupEffect + + expect(clientGroup.EndpointA({ withResponse: false })).type.toBe< + Effect.Effect< + "EndpointASuccess", + | "ApiError" + | "Group1Error" + | "EndpointAError" + | "EndpointASecurityError" + | "Group1SecurityError" + | "ApiSecurityError" + | HttpApiError.HttpApiDecodeError + | HttpClientError.HttpClientError + | ParseError + > + >() + }) + }) + + it("make", () => { + Effect.gen(function*() { + const clientApiEffect = HttpApiClient.make(TestApi) + + expect>().type.toBe() + + expect>().type.toBe< + | "ApiErrorR" + | "Group1ErrorR" + | "EndpointAErrorR" + | "EndpointASuccessR" + | "EndpointBErrorR" + | "EndpointBSuccessR" + | "EndpointASecurityErrorR" + | "EndpointBSecurityErrorR" + | "Group1SecurityErrorR" + | "ApiSecurityErrorR" + | "Group2ErrorR" + | "EndpointCErrorR" + | "EndpointCSuccessR" + | HttpClient.HttpClient + >() + + const clientApi = yield* clientApiEffect + + expect(clientApi.Group1.EndpointA({ withResponse: false })).type.toBe< + Effect.Effect< + "EndpointASuccess", + | "ApiError" + | "Group1Error" + | "EndpointAError" + | "EndpointASecurityError" + | "Group1SecurityError" + | "ApiSecurityError" + | HttpApiError.HttpApiDecodeError + | HttpClientError.HttpClientError + | ParseError + > + >() + }) + }) +}) diff --git a/repos/effect/packages/platform/dtslint/HttpApiEndpoint.tst.ts b/repos/effect/packages/platform/dtslint/HttpApiEndpoint.tst.ts new file mode 100644 index 0000000..147d99d --- /dev/null +++ b/repos/effect/packages/platform/dtslint/HttpApiEndpoint.tst.ts @@ -0,0 +1,13 @@ +/* eslint @typescript-eslint/no-unused-expressions: "off" */ + +import { HttpApiEndpoint, HttpApiSchema } from "@effect/platform" +import { Schema } from "effect" +import { describe, it } from "tstyche" + +describe("HttpApiEndpoint", () => { + it("should prevent duplicated params", () => { + HttpApiEndpoint.get("test")`/${HttpApiSchema.param("id", Schema.NumberFromString)}/${ + // @ts-expect-error: Argument of type 'Param<"id", typeof NumberFromString>' is not assignable to parameter of type '"Duplicate param: id"' + HttpApiSchema.param("id", Schema.NumberFromString)}` + }) +}) diff --git a/repos/effect/packages/platform/dtslint/HttpApiError.tst.ts b/repos/effect/packages/platform/dtslint/HttpApiError.tst.ts new file mode 100644 index 0000000..539a206 --- /dev/null +++ b/repos/effect/packages/platform/dtslint/HttpApiError.tst.ts @@ -0,0 +1,15 @@ +import type { HttpApiError } from "@effect/platform" + +import type { Unify } from "effect" + +import { describe, expect, it } from "tstyche" + +describe("HttpApiError", () => { + describe("Unify", () => { + it("should unify error types", () => { + type testType = Unify.Unify + expect() + .type.toBe() + }) + }) +}) diff --git a/repos/effect/packages/platform/dtslint/HttpRouter.tst.ts b/repos/effect/packages/platform/dtslint/HttpRouter.tst.ts new file mode 100644 index 0000000..a02deb2 --- /dev/null +++ b/repos/effect/packages/platform/dtslint/HttpRouter.tst.ts @@ -0,0 +1,14 @@ +import { HttpRouter } from "@effect/platform" +import { describe, expect, it } from "tstyche" + +declare const router1: HttpRouter.HttpRouter<"E1", "R1"> +declare const router2: HttpRouter.HttpRouter<"E2", "R2"> +declare const router3: HttpRouter.HttpRouter<"E3", "R3"> + +describe("HttpApiRouter", () => { + it("concatAll", () => { + expect(HttpRouter.concatAll(router1, router2, router3)).type.toBe< + HttpRouter.HttpRouter<"E1" | "E2" | "E3", "R1" | "R2" | "R3"> + >() + }) +}) diff --git a/repos/effect/packages/platform/dtslint/tsconfig.json b/repos/effect/packages/platform/dtslint/tsconfig.json new file mode 100644 index 0000000..4d1caf1 --- /dev/null +++ b/repos/effect/packages/platform/dtslint/tsconfig.json @@ -0,0 +1,18 @@ +{ + "extends": "../../../tsconfig.base.json", + "include": ["."], + "references": [ + { "path": "../tsconfig.src.json" }, + ], + "compilerOptions": { + "incremental": false, + "composite": false, + "noUnusedLocals": false, + "plugins": [ + { + "name": "@effect/language-server", + "diagnostics": false + } + ] + } +} diff --git a/repos/effect/packages/platform/images/swagger-hello-world.png b/repos/effect/packages/platform/images/swagger-hello-world.png new file mode 100644 index 0000000..62461e6 Binary files /dev/null and b/repos/effect/packages/platform/images/swagger-hello-world.png differ diff --git a/repos/effect/packages/platform/images/swagger-myapi.png b/repos/effect/packages/platform/images/swagger-myapi.png new file mode 100644 index 0000000..e7219ca Binary files /dev/null and b/repos/effect/packages/platform/images/swagger-myapi.png differ diff --git a/repos/effect/packages/platform/package.json b/repos/effect/packages/platform/package.json new file mode 100644 index 0000000..5111033 --- /dev/null +++ b/repos/effect/packages/platform/package.json @@ -0,0 +1,61 @@ +{ + "name": "@effect/platform", + "type": "module", + "version": "0.96.1", + "license": "MIT", + "description": "Unified interfaces for common platform-specific services", + "homepage": "https://effect.website", + "repository": { + "type": "git", + "url": "https://github.com/Effect-TS/effect.git", + "directory": "packages/platform" + }, + "bugs": { + "url": "https://github.com/Effect-TS/effect/issues" + }, + "tags": [ + "typescript", + "algebraic-data-types", + "functional-programming" + ], + "keywords": [ + "typescript", + "algebraic-data-types", + "functional-programming" + ], + "publishConfig": { + "access": "public", + "provenance": true, + "directory": "dist", + "linkDirectory": false + }, + "exports": { + "./package.json": "./package.json", + ".": "./src/index.ts", + "./*": "./src/*.ts", + "./internal/*": null + }, + "scripts": { + "codegen": "build-utils prepare-v3", + "build": "pnpm build-esm && pnpm build-annotate && pnpm build-cjs && build-utils pack-v3", + "build-esm": "tsc -b tsconfig.build.json", + "build-cjs": "babel build/esm --plugins @babel/transform-export-namespace-from --plugins @babel/transform-modules-commonjs --out-dir build/cjs --source-maps", + "build-annotate": "babel build/esm --plugins annotate-pure-calls --out-dir build/esm --source-maps", + "test-types": "tstyche", + "check": "tsc -b tsconfig.json", + "test": "vitest", + "coverage": "vitest --coverage" + }, + "dependencies": { + "find-my-way-ts": "^0.1.6", + "msgpackr": "^1.11.10", + "multipasta": "^0.2.7" + }, + "peerDependencies": { + "effect": "workspace:^" + }, + "devDependencies": { + "ajv": "^8.17.1", + "effect": "workspace:^" + } +} diff --git a/repos/effect/packages/platform/src/ChannelSchema.ts b/repos/effect/packages/platform/src/ChannelSchema.ts new file mode 100644 index 0000000..033d980 --- /dev/null +++ b/repos/effect/packages/platform/src/ChannelSchema.ts @@ -0,0 +1,239 @@ +/** + * @since 1.0.0 + */ +import type * as Cause from "effect/Cause" +import * as Channel from "effect/Channel" +import type * as Chunk from "effect/Chunk" +import { dual, pipe } from "effect/Function" +import type { ParseError } from "effect/ParseResult" +import * as Schema from "effect/Schema" + +/** + * @since 1.0.0 + * @category constructors + */ +export const encode = ( + schema: Schema.Schema +) => +(): Channel.Channel< + Chunk.Chunk, + Chunk.Chunk
, + IE | ParseError, + IE, + Done, + Done, + R +> => { + const encode = Schema.encode(Schema.ChunkFromSelf(schema)) + const loop: Channel.Channel, Chunk.Chunk, IE | ParseError, IE, Done, Done, R> = Channel + .readWithCause({ + onInput: (input: Chunk.Chunk) => + Channel.zipRight( + Channel.flatMap(encode(input), Channel.write), + loop + ), + onFailure: (cause: Cause.Cause) => Channel.failCause(cause), + onDone: Channel.succeed + }) + return loop +} + +/** + * @since 1.0.0 + * @category constructors + */ +export const encodeUnknown: ( + schema: Schema.Schema +) => () => Channel.Channel< + Chunk.Chunk, + Chunk.Chunk, + IE | ParseError, + IE, + Done, + Done, + R +> = encode as any + +/** + * @since 1.0.0 + * @category constructors + */ +export const decode = ( + schema: Schema.Schema +) => +(): Channel.Channel< + Chunk.Chunk, + Chunk.Chunk, + ParseError | IE, + IE, + Done, + Done, + R +> => { + const decode = Schema.decode(Schema.ChunkFromSelf(schema)) + const loop: Channel.Channel, Chunk.Chunk, ParseError | IE, IE, Done, Done, R> = Channel + .readWithCause({ + onInput(chunk: Chunk.Chunk) { + return decode(chunk).pipe( + Channel.flatMap(Channel.write), + Channel.zipRight(loop) + ) + }, + onFailure(cause: Cause.Cause) { + return Channel.failCause(cause) + }, + onDone(done: Done) { + return Channel.succeed(done) + } + }) + return loop +} + +/** + * @since 1.0.0 + * @category constructors + */ +export const decodeUnknown: ( + schema: Schema.Schema +) => () => Channel.Channel< + Chunk.Chunk, + Chunk.Chunk, + ParseError | IE, + IE, + Done, + Done, + R +> = decode as any + +/** + * @since 1.0.0 + * @category combinators + */ +export const duplex: { + (options: { + readonly inputSchema: Schema.Schema + readonly outputSchema: Schema.Schema + }): ( + self: Channel.Channel< + Chunk.Chunk, + Chunk.Chunk, + OutErr, + ParseError | InErr, + OutDone, + InDone, + R + > + ) => Channel.Channel< + Chunk.Chunk, + Chunk.Chunk, + ParseError | OutErr, + InErr, + OutDone, + InDone, + R | IR | OR + > + ( + self: Channel.Channel< + Chunk.Chunk, + Chunk.Chunk, + OutErr, + ParseError | InErr, + OutDone, + InDone, + R + >, + options: { + readonly inputSchema: Schema.Schema + readonly outputSchema: Schema.Schema + } + ): Channel.Channel< + Chunk.Chunk, + Chunk.Chunk, + ParseError | OutErr, + InErr, + OutDone, + InDone, + R | IR | OR + > +} = dual(2, ( + self: Channel.Channel< + Chunk.Chunk, + Chunk.Chunk, + OutErr, + ParseError | InErr, + OutDone, + InDone, + R + >, + options: { + readonly inputSchema: Schema.Schema + readonly outputSchema: Schema.Schema + } +): Channel.Channel< + Chunk.Chunk, + Chunk.Chunk, + ParseError | OutErr, + InErr, + OutDone, + InDone, + R | IR | OR +> => { + const decode = Schema.decode(Schema.ChunkFromSelf(options.outputSchema)) + return pipe( + encode(options.inputSchema)(), + Channel.pipeTo(self), + Channel.mapOutEffect(decode) + ) +}) + +/** + * @since 1.0.0 + * @category combinators + */ +export const duplexUnknown: { + (options: { + readonly inputSchema: Schema.Schema + readonly outputSchema: Schema.Schema + }): ( + self: Channel.Channel< + Chunk.Chunk, + Chunk.Chunk, + OutErr, + ParseError | InErr, + OutDone, + InDone, + R + > + ) => Channel.Channel< + Chunk.Chunk, + Chunk.Chunk, + ParseError | OutErr, + InErr, + OutDone, + InDone, + R | IR | OR + > + ( + self: Channel.Channel< + Chunk.Chunk, + Chunk.Chunk, + OutErr, + ParseError | InErr, + OutDone, + InDone, + R + >, + options: { + readonly inputSchema: Schema.Schema + readonly outputSchema: Schema.Schema + } + ): Channel.Channel< + Chunk.Chunk, + Chunk.Chunk, + ParseError | OutErr, + InErr, + OutDone, + InDone, + R | IR | OR + > +} = duplex as any diff --git a/repos/effect/packages/platform/src/Command.ts b/repos/effect/packages/platform/src/Command.ts new file mode 100644 index 0000000..01445ef --- /dev/null +++ b/repos/effect/packages/platform/src/Command.ts @@ -0,0 +1,298 @@ +/** + * @since 1.0.0 + */ +import type { NonEmptyReadonlyArray } from "effect/Array" +import type { Effect } from "effect/Effect" +import type { HashMap } from "effect/HashMap" +import type { Inspectable } from "effect/Inspectable" +import type { Option } from "effect/Option" +import type { Pipeable } from "effect/Pipeable" +import type { Scope } from "effect/Scope" +import type { Sink } from "effect/Sink" +import type { Stream } from "effect/Stream" +import type { CommandExecutor, ExitCode, Process } from "./CommandExecutor.js" +import type { PlatformError } from "./Error.js" +import * as internal from "./internal/command.js" + +/** + * @since 1.0.0 + */ +export const CommandTypeId: unique symbol = internal.CommandTypeId + +/** + * @since 1.0.0 + */ +export type CommandTypeId = typeof CommandTypeId + +/** + * @since 1.0.0 + * @category models + */ +export type Command = StandardCommand | PipedCommand + +/** + * @since 1.0.0 + */ +export declare namespace Command { + /** + * @since 1.0.0 + * @category models + */ + export interface Proto extends Pipeable, Inspectable { + readonly [CommandTypeId]: CommandTypeId + readonly _tag: string + } + /** + * Configures the pipe that is established between the parent and child + * processes' `stdin` stream. + * + * @since 1.0.0 + * @category models + */ + export type Input = CommandInput + /** + * Configures the pipes that are established between the parent and child + * processes `stderr` and `stdout` streams. + * + * @since 1.0.0 + * @category models + */ + export type Output = CommandOutput +} + +/** + * Configures the pipe that is established between the parent and child + * processes' `stdin` stream. + * + * Defaults to "pipe" + * + * @since 1.0.0 + * @category models + */ +export type CommandInput = "inherit" | "pipe" | Stream + +/** + * Configures the pipes that are established between the parent and child + * processes `stderr` and `stdout` streams. + * + * Defaults to "pipe" + * + * @since 1.0.0 + * @category models + */ +export type CommandOutput = "inherit" | "pipe" | Sink + +/** + * @since 1.0.0 + * @category models + */ +export interface StandardCommand extends Command.Proto { + readonly _tag: "StandardCommand" + readonly command: string + readonly args: ReadonlyArray + readonly env: HashMap + readonly cwd: Option + readonly shell: boolean | string + readonly stdin: Command.Input + readonly stdout: Command.Output + readonly stderr: Command.Output + readonly gid: Option + readonly uid: Option +} + +/** + * @since 1.0.0 + * @category models + */ +export interface PipedCommand extends Command.Proto { + readonly _tag: "PipedCommand" + readonly left: Command + readonly right: Command +} + +/** + * Returns `true` if the specified value is a `Command`, otherwise returns + * `false`. + * + * @since 1.0.0 + * @category refinements + */ +export const isCommand: (u: unknown) => u is Command = internal.isCommand + +/** + * Specify the environment variables that will be used when running this command. + * + * @since 1.0.0 + * @category combinators + */ +export const env: { + (environment: Record): (self: Command) => Command + (self: Command, environment: Record): Command +} = internal.env + +/** + * Returns the exit code of the command after the process has completed + * execution. + * + * @since 1.0.0 + * @category execution + */ +export const exitCode: (self: Command) => Effect = internal.exitCode + +/** + * Feed a string to standard input (default encoding of UTF-8). + * + * @since 1.0.0 + * @category combinators + */ +export const feed: { + (input: string): (self: Command) => Command + (self: Command, input: string): Command +} = internal.feed + +/** + * Flatten this command to a non-empty array of standard commands. + * + * For a `StandardCommand`, this simply returns a `1` element array + * For a `PipedCommand`, all commands in the pipe will be extracted out into + * a array from left to right + * + * @since 1.0.0 + * @category combinators + */ +export const flatten: (self: Command) => NonEmptyReadonlyArray = internal.flatten + +/** + * Runs the command returning the output as an array of lines with the specified + * encoding. + * + * @since 1.0.0 + * @category execution + */ +export const lines: (command: Command, encoding?: string) => Effect, PlatformError, CommandExecutor> = + internal.lines + +/** + * Create a command with the specified process name and an optional list of + * arguments. + * + * @since 1.0.0 + * @category constructors + */ +export const make: (command: string, ...args: Array) => Command = internal.make + +/** + * Pipe one command to another command from left to right. + * + * Conceptually, the equivalent of piping one shell command to another: + * + * ```sh + * command1 | command2 + * ``` + * + * @since 1.0.0 + * @category combinators + */ +export const pipeTo: { + (into: Command): (self: Command) => Command + (self: Command, into: Command): Command +} = internal.pipeTo + +/** + * Allows for specifying whether or not a `Command` should be run inside a + * shell. + * + * @since 1.0.0 + * @category combinators + */ +export const runInShell: { + (shell: string | boolean): (self: Command) => Command + (self: Command, shell: string | boolean): Command +} = internal.runInShell + +/** + * Start running the command and return a handle to the running process. + * + * @since 1.0.0 + * @category execution + */ +export const start: (command: Command) => Effect = internal.start + +/** + * Start running the command and return the output as a `Stream`. + * + * @since 1.0.0 + * @category execution + */ +export const stream: (command: Command) => Stream = internal.stream + +/** + * Runs the command returning the output as an stream of lines with the + * specified encoding. + * + * @since 1.0.0 + * @category execution + */ +export const streamLines: (command: Command, encoding?: string) => Stream = + internal.streamLines + +/** + * Runs the command returning the entire output as a string with the + * specified encoding. + * + * If an encoding is not specified, the encoding will default to `utf-8`. + * + * @since 1.0.0 + * @category execution + */ +export const string: { + (encoding?: string): (command: Command) => Effect + (command: Command, encoding?: string): Effect +} = internal.string + +/** + * Specify the standard error stream for a command. + * + * @since 1.0.0 + * @category combinators + */ +export const stderr: { + (stderr: Command.Output): (self: Command) => Command + (self: Command, stderr: Command.Output): Command +} = internal.stderr + +/** + * Specify the standard input stream for a command. + * + * @since 1.0.0 + * @category combinators + */ +export const stdin: { + (stdin: Command.Input): (self: Command) => Command + (self: Command, stdin: Command.Input): Command +} = internal.stdin + +/** + * Specify the standard output stream for a command. + * + * @since 1.0.0 + * @category combinators + */ +export const stdout: { + (stdout: Command.Output): (self: Command) => Command + (self: Command, stdout: Command.Output): Command +} = internal.stdout + +/** + * Set the working directory that will be used when this command will be run. + * + * For piped commands, the working directory of each command will be set to the + * specified working directory. + * + * @since 1.0.0 + * @category combinators + */ +export const workingDirectory: { + (cwd: string): (self: Command) => Command + (self: Command, cwd: string): Command +} = internal.workingDirectory diff --git a/repos/effect/packages/platform/src/CommandExecutor.ts b/repos/effect/packages/platform/src/CommandExecutor.ts new file mode 100644 index 0000000..85ff4ae --- /dev/null +++ b/repos/effect/packages/platform/src/CommandExecutor.ts @@ -0,0 +1,207 @@ +/** + * @since 1.0.0 + */ +import type * as Brand from "effect/Brand" +import type { Tag } from "effect/Context" +import type { Effect } from "effect/Effect" +import type { Inspectable } from "effect/Inspectable" +import type { Scope } from "effect/Scope" +import type { Sink } from "effect/Sink" +import type { Stream } from "effect/Stream" +import type { Command } from "./Command.js" +import type { PlatformError } from "./Error.js" +import * as internal from "./internal/commandExecutor.js" + +/** + * @since 1.0.0 + * @category type ids + */ +export const TypeId: unique symbol = internal.TypeId + +/** + * @since 1.0.0 + * @category type ids + */ +export type TypeId = typeof TypeId + +/** + * @since 1.0.0 + * @category models + */ +export interface CommandExecutor { + readonly [TypeId]: TypeId + + /** + * Returns the exit code of the command after the process has completed + * execution. + */ + readonly exitCode: (command: Command) => Effect + /** + * Start running the command and return a handle to the running process. + */ + readonly start: (command: Command) => Effect + /** + * Runs the command returning the entire output as a string with the + * specified encoding. + * + * If an encoding is not specified, the encoding will default to `utf-8`. + */ + readonly string: (command: Command, encoding?: string) => Effect + /** + * Runs the command returning the entire output as an array of lines. + * + * If an encoding is not specified, the encoding will default to `utf-8`. + */ + readonly lines: (command: Command, encoding?: string) => Effect, PlatformError> + /** + * Runs the command returning the output as a `Stream`. + */ + readonly stream: (command: Command) => Stream + /** + * Runs the command returning the output as a `Stream` of lines. + */ + readonly streamLines: (command: Command, encoding?: string) => Stream +} + +/** + * @since 1.0.0 + * @category tags + */ +export const CommandExecutor: Tag = internal.CommandExecutor + +/** + * @since 1.0.0 + * @category symbols + */ +export const ProcessTypeId: unique symbol = internal.ProcessTypeId + +/** + * @since 1.0.0 + * @category symbols + */ +export type ProcessTypeId = typeof ProcessTypeId + +/** + * @since 1.0.0 + * @category models + */ +export interface Process extends Inspectable { + readonly [ProcessTypeId]: ProcessTypeId + /** + * The process identifier. + */ + readonly pid: ProcessId + /** + * Waits for the process to exit and returns the `ExitCode` of the command + * that was run. + */ + readonly exitCode: Effect + /** + * Returns `true` if the process is still running, otherwise returns `false`. + */ + readonly isRunning: Effect + /** + * Kills the running process with the provided signal. + * + * If no signal is provided, the signal will defaults to `SIGTERM`. + */ + readonly kill: (signal?: Signal) => Effect + /** + * The standard error stream of the process. + */ + readonly stderr: Stream + /** + * The standard input sink of the process. + */ + readonly stdin: Sink + /** + * The standard output stream of the process. + */ + readonly stdout: Stream +} + +/** + * @since 1.0.0 + * @category models + */ +export type ProcessId = Brand.Branded + +/** + * @since 1.0.0 + */ +export declare namespace Process { + /** + * @since 1.0.0 + * @category models + */ + export type Id = ProcessId +} + +/** + * @since 1.0.0 + * @category models + */ +export type Signal = + | "SIGABRT" + | "SIGALRM" + | "SIGBUS" + | "SIGCHLD" + | "SIGCONT" + | "SIGFPE" + | "SIGHUP" + | "SIGILL" + | "SIGINT" + | "SIGIO" + | "SIGIOT" + | "SIGKILL" + | "SIGPIPE" + | "SIGPOLL" + | "SIGPROF" + | "SIGPWR" + | "SIGQUIT" + | "SIGSEGV" + | "SIGSTKFLT" + | "SIGSTOP" + | "SIGSYS" + | "SIGTERM" + | "SIGTRAP" + | "SIGTSTP" + | "SIGTTIN" + | "SIGTTOU" + | "SIGUNUSED" + | "SIGURG" + | "SIGUSR1" + | "SIGUSR2" + | "SIGVTALRM" + | "SIGWINCH" + | "SIGXCPU" + | "SIGXFSZ" + | "SIGBREAK" + | "SIGLOST" + | "SIGINFO" + +/** + * @since 1.0.0 + * @category models + */ +export type ExitCode = Brand.Branded + +/** + * @since 1.0.0 + * @category constructors + */ +export const ExitCode: Brand.Brand.Constructor = internal.ExitCode + +/** + * @since 1.0.0 + * @category constructors + */ +export const ProcessId: Brand.Brand.Constructor = internal.ProcessId + +/** + * @since 1.0.0 + * @category constructors + */ +export const makeExecutor: ( + start: (command: Command) => Effect +) => CommandExecutor = internal.makeExecutor diff --git a/repos/effect/packages/platform/src/Cookies.ts b/repos/effect/packages/platform/src/Cookies.ts new file mode 100644 index 0000000..2f4e5d3 --- /dev/null +++ b/repos/effect/packages/platform/src/Cookies.ts @@ -0,0 +1,747 @@ +/** + * @since 1.0.0 + */ +import * as Duration from "effect/Duration" +import * as Either from "effect/Either" +import { dual, identity } from "effect/Function" +import * as Inspectable from "effect/Inspectable" +import * as Option from "effect/Option" +import { type Pipeable, pipeArguments } from "effect/Pipeable" +import * as Predicate from "effect/Predicate" +import * as Record from "effect/Record" +import type * as Types from "effect/Types" +import { TypeIdError } from "./Error.js" + +/** + * @since 1.0.0 + * @category type ids + */ +export const TypeId: unique symbol = Symbol.for("@effect/platform/Cookies") + +/** + * @since 1.0.0 + * @category type ids + */ +export type TypeId = typeof TypeId + +/** + * @since 1.0.0 + * @category refinements + */ +export const isCookies = (u: unknown): u is Cookies => Predicate.hasProperty(u, TypeId) + +/** + * @since 1.0.0 + * @category models + */ +export interface Cookies extends Pipeable, Inspectable.Inspectable { + readonly [TypeId]: TypeId + readonly cookies: Record.ReadonlyRecord +} + +/** + * @since 1.0.0 + * @category type ids + */ +export const CookieTypeId: unique symbol = Symbol.for("@effect/platform/Cookies/Cookie") + +/** + * @since 1.0.0 + * @category type ids + */ +export type CookieTypeId = typeof CookieTypeId + +/** + * @since 1.0.0 + * @category cookie + */ +export interface Cookie extends Inspectable.Inspectable { + readonly [CookieTypeId]: CookieTypeId + readonly name: string + readonly value: string + readonly valueEncoded: string + readonly options?: { + readonly domain?: string | undefined + readonly expires?: Date | undefined + readonly maxAge?: Duration.DurationInput | undefined + readonly path?: string | undefined + readonly priority?: "low" | "medium" | "high" | undefined + readonly httpOnly?: boolean | undefined + readonly secure?: boolean | undefined + readonly partitioned?: boolean | undefined + readonly sameSite?: "lax" | "strict" | "none" | undefined + } | undefined +} + +/** + * @since 1.0.0 + * @category type ids + */ +export const ErrorTypeId: unique symbol = Symbol.for("@effect/platform/Cookies/CookieError") + +/** + * @since 1.0.0 + * @category type ids + */ +export type ErrorTypeId = typeof ErrorTypeId + +/** + * @since 1.0.0 + * @category errors + */ +export class CookiesError extends TypeIdError(ErrorTypeId, "CookieError")<{ + readonly reason: "InvalidName" | "InvalidValue" | "InvalidDomain" | "InvalidPath" | "InfinityMaxAge" +}> { + get message() { + return this.reason + } +} + +const Proto: Omit = { + [TypeId]: TypeId, + ...Inspectable.BaseProto, + toJSON(this: Cookies) { + return { + _id: "@effect/platform/Cookies", + cookies: Record.map(this.cookies, (cookie) => cookie.toJSON()) + } + }, + pipe() { + return pipeArguments(this, arguments) + } +} + +/** + * Create a Cookies object from an Iterable + * + * @since 1.0.0 + * @category constructors + */ +export const fromReadonlyRecord = (cookies: Record.ReadonlyRecord): Cookies => { + const self = Object.create(Proto) + self.cookies = cookies + return self +} + +/** + * Create a Cookies object from an Iterable + * + * @since 1.0.0 + * @category constructors + */ +export const fromIterable = (cookies: Iterable): Cookies => { + const record: Record = {} + for (const cookie of cookies) { + record[cookie.name] = cookie + } + return fromReadonlyRecord(record) +} + +/** + * Create a Cookies object from a set of Set-Cookie headers + * + * @since 1.0.0 + * @category constructors + */ +export const fromSetCookie = (headers: Iterable | string): Cookies => { + const arrayHeaders = typeof headers === "string" ? [headers] : headers + const cookies: Array = [] + for (const header of arrayHeaders) { + const cookie = parseSetCookie(header.trim()) + if (Option.isSome(cookie)) { + cookies.push(cookie.value) + } + } + + return fromIterable(cookies) +} + +function parseSetCookie(header: string): Option.Option { + const parts = header.split(";").map((_) => _.trim()).filter((_) => _ !== "") + if (parts.length === 0) { + return Option.none() + } + + const firstEqual = parts[0].indexOf("=") + if (firstEqual === -1) { + return Option.none() + } + const name = parts[0].slice(0, firstEqual) + if (!fieldContentRegExp.test(name)) { + return Option.none() + } + + const valueEncoded = parts[0].slice(firstEqual + 1) + const value = tryDecodeURIComponent(valueEncoded) + + if (parts.length === 1) { + return Option.some(Object.assign(Object.create(CookieProto), { + name, + value, + valueEncoded + })) + } + + const options: Types.Mutable = {} + + for (let i = 1; i < parts.length; i++) { + const part = parts[i] + const equalIndex = part.indexOf("=") + const key = equalIndex === -1 ? part : part.slice(0, equalIndex).trim() + const value = equalIndex === -1 ? undefined : part.slice(equalIndex + 1).trim() + + switch (key.toLowerCase()) { + case "domain": { + if (value === undefined) { + break + } + const domain = value.trim().replace(/^\./, "") + if (domain) { + options.domain = domain + } + break + } + case "expires": { + if (value === undefined) { + break + } + const date = new Date(value) + if (!isNaN(date.getTime())) { + options.expires = date + } + break + } + case "max-age": { + if (value === undefined) { + break + } + const maxAge = parseInt(value, 10) + if (!isNaN(maxAge)) { + options.maxAge = Duration.seconds(maxAge) + } + break + } + case "path": { + if (value === undefined) { + break + } + if (value[0] === "/") { + options.path = value + } + break + } + case "priority": { + if (value === undefined) { + break + } + switch (value.toLowerCase()) { + case "low": + options.priority = "low" + break + case "medium": + options.priority = "medium" + break + case "high": + options.priority = "high" + break + } + break + } + case "httponly": { + options.httpOnly = true + break + } + case "secure": { + options.secure = true + break + } + case "partitioned": { + options.partitioned = true + break + } + case "samesite": { + if (value === undefined) { + break + } + switch (value.toLowerCase()) { + case "lax": + options.sameSite = "lax" + break + case "strict": + options.sameSite = "strict" + break + case "none": + options.sameSite = "none" + break + } + break + } + } + } + + return Option.some(Object.assign(Object.create(CookieProto), { + name, + value, + valueEncoded, + options: Object.keys(options).length > 0 ? options : undefined + })) +} + +/** + * An empty Cookies object + * + * @since 1.0.0 + * @category constructors + */ +export const empty: Cookies = fromIterable([]) + +/** + * @since 1.0.0 + * @category refinements + */ +export const isEmpty = (self: Cookies): boolean => Record.isEmptyRecord(self.cookies) + +// eslint-disable-next-line no-control-regex +const fieldContentRegExp = /^[\u0009\u0020-\u007e\u0080-\u00ff]+$/ + +const CookieProto = { + [CookieTypeId]: CookieTypeId, + ...Inspectable.BaseProto, + toJSON(this: Cookie) { + return { + _id: "@effect/platform/Cookies/Cookie", + name: this.name, + value: this.value, + options: this.options + } + } +} + +/** + * Create a new cookie + * + * @since 1.0.0 + * @category constructors + */ +export function makeCookie( + name: string, + value: string, + options?: Cookie["options"] | undefined +): Either.Either { + if (!fieldContentRegExp.test(name)) { + return Either.left(new CookiesError({ reason: "InvalidName" })) + } + const encodedValue = encodeURIComponent(value) + if (encodedValue && !fieldContentRegExp.test(encodedValue)) { + return Either.left(new CookiesError({ reason: "InvalidValue" })) + } + + if (options !== undefined) { + if (options.domain !== undefined && !fieldContentRegExp.test(options.domain)) { + return Either.left(new CookiesError({ reason: "InvalidDomain" })) + } + + if (options.path !== undefined && !fieldContentRegExp.test(options.path)) { + return Either.left(new CookiesError({ reason: "InvalidPath" })) + } + + if (options.maxAge !== undefined && !Duration.isFinite(Duration.decode(options.maxAge))) { + return Either.left(new CookiesError({ reason: "InfinityMaxAge" })) + } + } + + return Either.right(Object.assign(Object.create(CookieProto), { + name, + value, + valueEncoded: encodedValue, + options + })) +} + +/** + * Create a new cookie, throwing an error if invalid + * + * @since 1.0.0 + * @category constructors + */ +export const unsafeMakeCookie = ( + name: string, + value: string, + options?: Cookie["options"] | undefined +): Cookie => Either.getOrThrowWith(makeCookie(name, value, options), identity) + +/** + * Add a cookie to a Cookies object + * + * @since 1.0.0 + * @category combinators + */ +export const setCookie: { + (cookie: Cookie): (self: Cookies) => Cookies + ( + self: Cookies, + cookie: Cookie + ): Cookies +} = dual( + 2, + (self: Cookies, cookie: Cookie) => + fromReadonlyRecord(Record.set( + self.cookies, + cookie.name, + cookie + )) +) + +/** + * Add multiple cookies to a Cookies object + * + * @since 1.0.0 + * @category combinators + */ +export const setAllCookie: { + (cookies: Iterable): (self: Cookies) => Cookies + ( + self: Cookies, + cookies: Iterable + ): Cookies +} = dual(2, (self: Cookies, cookies: Iterable) => { + const record = { ...self.cookies } + for (const cookie of cookies) { + record[cookie.name] = cookie + } + return fromReadonlyRecord(record) +}) + +/** + * Combine two Cookies objects, removing duplicates from the first + * + * @since 1.0.0 + * @category combinators + */ +export const merge: { + (that: Cookies): (self: Cookies) => Cookies + ( + self: Cookies, + that: Cookies + ): Cookies +} = dual(2, (self: Cookies, that: Cookies) => + fromReadonlyRecord({ + ...self.cookies, + ...that.cookies + })) + +/** + * Remove a cookie by name + * + * @since 1.0.0 + * @category combinators + */ +export const remove: { + (name: string): (self: Cookies) => Cookies + ( + self: Cookies, + name: string + ): Cookies +} = dual(2, (self: Cookies, name: string) => fromReadonlyRecord(Record.remove(self.cookies, name))) + +/** + * Get a cookie from a Cookies object + * + * @since 1.0.0 + * @category combinators + */ +export const get: { + (name: string): (self: Cookies) => Option.Option + (self: Cookies, name: string): Option.Option +} = dual( + (args) => isCookies(args[0]), + (self: Cookies, name: string): Option.Option => Record.get(self.cookies, name) +) + +/** + * Get a cookie from a Cookies object + * + * @since 1.0.0 + * @category combinators + */ +export const getValue: { + (name: string): (self: Cookies) => Option.Option + (self: Cookies, name: string): Option.Option +} = dual( + (args) => isCookies(args[0]), + (self: Cookies, name: string): Option.Option => + Option.map(Record.get(self.cookies, name), (cookie) => cookie.value) +) + +/** + * Add a cookie to a Cookies object + * + * @since 1.0.0 + * @category combinators + */ +export const set: { + ( + name: string, + value: string, + options?: Cookie["options"] + ): (self: Cookies) => Either.Either + ( + self: Cookies, + name: string, + value: string, + options?: Cookie["options"] + ): Either.Either +} = dual( + (args) => isCookies(args[0]), + (self: Cookies, name: string, value: string, options?: Cookie["options"]) => + Either.map( + makeCookie(name, value, options), + (cookie) => fromReadonlyRecord(Record.set(self.cookies, name, cookie)) + ) +) + +/** + * Add a cookie to a Cookies object + * + * @since 1.0.0 + * @category combinators + */ +export const unsafeSet: { + ( + name: string, + value: string, + options?: Cookie["options"] + ): (self: Cookies) => Cookies + ( + self: Cookies, + name: string, + value: string, + options?: Cookie["options"] + ): Cookies +} = dual( + (args) => isCookies(args[0]), + (self: Cookies, name: string, value: string, options?: Cookie["options"]) => + fromReadonlyRecord(Record.set( + self.cookies, + name, + unsafeMakeCookie(name, value, options) + )) +) + +/** + * Add multiple cookies to a Cookies object + * + * @since 1.0.0 + * @category combinators + */ +export const setAll: { + ( + cookies: Iterable + ): (self: Cookies) => Either.Either + ( + self: Cookies, + cookies: Iterable + ): Either.Either +} = dual( + 2, + ( + self: Cookies, + cookies: Iterable + ): Either.Either => { + const record: Record = { ...self.cookies } + for (const [name, value, options] of cookies) { + const either = makeCookie(name, value, options) + if (Either.isLeft(either)) { + return either as Either.Left + } + record[name] = either.right + } + return Either.right(fromReadonlyRecord(record)) + } +) + +/** + * Add multiple cookies to a Cookies object, throwing an error if invalid + * + * @since 1.0.0 + * @category combinators + */ +export const unsafeSetAll: { + ( + cookies: Iterable + ): (self: Cookies) => Cookies + ( + self: Cookies, + cookies: Iterable + ): Cookies +} = dual( + 2, + ( + self: Cookies, + cookies: Iterable + ): Cookies => Either.getOrThrowWith(setAll(self, cookies), identity) +) + +/** + * Serialize a cookie into a string + * + * Adapted from https://github.com/fastify/fastify-cookie under MIT License + * + * @since 1.0.0 + * @category encoding + */ +export function serializeCookie(self: Cookie): string { + let str = self.name + "=" + self.valueEncoded + + if (self.options === undefined) { + return str + } + const options = self.options + + if (options.maxAge !== undefined) { + const maxAge = Duration.toSeconds(options.maxAge) + str += "; Max-Age=" + Math.trunc(maxAge) + } + + if (options.domain !== undefined) { + str += "; Domain=" + options.domain + } + + if (options.path !== undefined) { + str += "; Path=" + options.path + } + + if (options.priority !== undefined) { + switch (options.priority) { + case "low": + str += "; Priority=Low" + break + case "medium": + str += "; Priority=Medium" + break + case "high": + str += "; Priority=High" + break + } + } + + if (options.expires !== undefined) { + str += "; Expires=" + options.expires.toUTCString() + } + + if (options.httpOnly) { + str += "; HttpOnly" + } + + if (options.secure) { + str += "; Secure" + } + + // Draft implementation to support Chrome from 2024-Q1 forward. + // See https://datatracker.ietf.org/doc/html/draft-cutler-httpbis-partitioned-cookies#section-2.1 + if (options.partitioned) { + str += "; Partitioned" + } + + if (options.sameSite !== undefined) { + switch (options.sameSite) { + case "lax": + str += "; SameSite=Lax" + break + case "strict": + str += "; SameSite=Strict" + break + case "none": + str += "; SameSite=None" + break + } + } + + return str +} + +/** + * Serialize a Cookies object into a Cookie header + * + * @since 1.0.0 + * @category encoding + */ +export const toCookieHeader = (self: Cookies): string => + Object.values(self.cookies).map((cookie) => `${cookie.name}=${cookie.valueEncoded}`).join("; ") + +/** + * To record + * + * @since 1.0.0 + * @category encoding + */ +export const toRecord = (self: Cookies): Record => { + const record: Record = {} + const cookies = Object.values(self.cookies) + for (let index = 0; index < cookies.length; index++) { + const cookie = cookies[index] + record[cookie.name] = cookie.value + } + return record +} + +/** + * Serialize a Cookies object into Headers object containing one or more Set-Cookie headers + * + * @since 1.0.0 + * @category encoding + */ +export const toSetCookieHeaders = (self: Cookies): Array => Object.values(self.cookies).map(serializeCookie) + +/** + * Parse a cookie header into a record of key-value pairs + * + * Adapted from https://github.com/fastify/fastify-cookie under MIT License + * + * @since 1.0.0 + * @category decoding + */ +export function parseHeader(header: string): Record { + const result: Record = {} + + const strLen = header.length + let pos = 0 + let terminatorPos = 0 + + while (true) { + if (terminatorPos === strLen) break + terminatorPos = header.indexOf(";", pos) + if (terminatorPos === -1) terminatorPos = strLen // This is the last pair + + let eqIdx = header.indexOf("=", pos) + if (eqIdx === -1) break // No key-value pairs left + if (eqIdx > terminatorPos) { + // Malformed key-value pair + pos = terminatorPos + 1 + continue + } + + const key = header.substring(pos, eqIdx++).trim() + if (result[key] === undefined) { + const val = header.charCodeAt(eqIdx) === 0x22 + ? header.substring(eqIdx + 1, terminatorPos - 1).trim() + : header.substring(eqIdx, terminatorPos).trim() + + result[key] = !(val.indexOf("%") === -1) + ? tryDecodeURIComponent(val) + : val + } + + pos = terminatorPos + 1 + } + + return result +} + +const tryDecodeURIComponent = (str: string): string => { + try { + return decodeURIComponent(str) + } catch { + return str + } +} diff --git a/repos/effect/packages/platform/src/Effectify.ts b/repos/effect/packages/platform/src/Effectify.ts new file mode 100644 index 0000000..e1f3eeb --- /dev/null +++ b/repos/effect/packages/platform/src/Effectify.ts @@ -0,0 +1,257 @@ +/** + * @since 1.0.0 + */ +import type * as Effect from "effect/Effect" +import * as internal from "./internal/effectify.js" + +interface Callback { + (err: E, a?: A): void +} + +type ArgsWithCallback, E, A> = [...args: Args, cb: Callback] + +type WithoutNull = unknown extends A ? void : Exclude + +/** + * Converts a callback-based function to a function that returns an `Effect`. + * + * @since 1.0.0 + */ +export type Effectify = T extends { + (...args: ArgsWithCallback): infer _R1 + (...args: ArgsWithCallback): infer _R2 + (...args: ArgsWithCallback): infer _R3 + (...args: ArgsWithCallback): infer _R4 + (...args: ArgsWithCallback): infer _R5 + (...args: ArgsWithCallback): infer _R6 + (...args: ArgsWithCallback): infer _R7 + (...args: ArgsWithCallback): infer _R8 + (...args: ArgsWithCallback): infer _R9 + (...args: ArgsWithCallback): infer _R10 +} ? { + (...args: Args1): Effect.Effect, E> + (...args: Args2): Effect.Effect, E> + (...args: Args3): Effect.Effect, E> + (...args: Args4): Effect.Effect, E> + (...args: Args5): Effect.Effect, E> + (...args: Args6): Effect.Effect, E> + (...args: Args7): Effect.Effect, E> + (...args: Args8): Effect.Effect, E> + (...args: Args9): Effect.Effect, E> + (...args: Args10): Effect.Effect, E> + } + : T extends { + (...args: ArgsWithCallback): infer _R1 + (...args: ArgsWithCallback): infer _R2 + (...args: ArgsWithCallback): infer _R3 + (...args: ArgsWithCallback): infer _R4 + (...args: ArgsWithCallback): infer _R5 + (...args: ArgsWithCallback): infer _R6 + (...args: ArgsWithCallback): infer _R7 + (...args: ArgsWithCallback): infer _R8 + (...args: ArgsWithCallback): infer _R9 + } ? { + (...args: Args1): Effect.Effect, E> + (...args: Args2): Effect.Effect, E> + (...args: Args3): Effect.Effect, E> + (...args: Args4): Effect.Effect, E> + (...args: Args5): Effect.Effect, E> + (...args: Args6): Effect.Effect, E> + (...args: Args7): Effect.Effect, E> + (...args: Args8): Effect.Effect, E> + (...args: Args9): Effect.Effect, E> + } + : T extends { + (...args: ArgsWithCallback): infer _R1 + (...args: ArgsWithCallback): infer _R2 + (...args: ArgsWithCallback): infer _R3 + (...args: ArgsWithCallback): infer _R4 + (...args: ArgsWithCallback): infer _R5 + (...args: ArgsWithCallback): infer _R6 + (...args: ArgsWithCallback): infer _R7 + (...args: ArgsWithCallback): infer _R8 + } ? { + (...args: Args1): Effect.Effect, E> + (...args: Args2): Effect.Effect, E> + (...args: Args3): Effect.Effect, E> + (...args: Args4): Effect.Effect, E> + (...args: Args5): Effect.Effect, E> + (...args: Args6): Effect.Effect, E> + (...args: Args7): Effect.Effect, E> + (...args: Args8): Effect.Effect, E> + } + : T extends { + (...args: ArgsWithCallback): infer _R1 + (...args: ArgsWithCallback): infer _R2 + (...args: ArgsWithCallback): infer _R3 + (...args: ArgsWithCallback): infer _R4 + (...args: ArgsWithCallback): infer _R5 + (...args: ArgsWithCallback): infer _R6 + (...args: ArgsWithCallback): infer _R7 + } ? { + (...args: Args1): Effect.Effect, E> + (...args: Args2): Effect.Effect, E> + (...args: Args3): Effect.Effect, E> + (...args: Args4): Effect.Effect, E> + (...args: Args5): Effect.Effect, E> + (...args: Args6): Effect.Effect, E> + (...args: Args7): Effect.Effect, E> + } + : T extends { + (...args: ArgsWithCallback): infer _R1 + (...args: ArgsWithCallback): infer _R2 + (...args: ArgsWithCallback): infer _R3 + (...args: ArgsWithCallback): infer _R4 + (...args: ArgsWithCallback): infer _R5 + (...args: ArgsWithCallback): infer _R6 + } ? { + (...args: Args1): Effect.Effect, E> + (...args: Args2): Effect.Effect, E> + (...args: Args3): Effect.Effect, E> + (...args: Args4): Effect.Effect, E> + (...args: Args5): Effect.Effect, E> + (...args: Args6): Effect.Effect, E> + } + : T extends { + (...args: ArgsWithCallback): infer _R1 + (...args: ArgsWithCallback): infer _R2 + (...args: ArgsWithCallback): infer _R3 + (...args: ArgsWithCallback): infer _R4 + (...args: ArgsWithCallback): infer _R5 + } ? { + (...args: Args1): Effect.Effect, E> + (...args: Args2): Effect.Effect, E> + (...args: Args3): Effect.Effect, E> + (...args: Args4): Effect.Effect, E> + (...args: Args5): Effect.Effect, E> + } + : T extends { + (...args: ArgsWithCallback): infer _R1 + (...args: ArgsWithCallback): infer _R2 + (...args: ArgsWithCallback): infer _R3 + (...args: ArgsWithCallback): infer _R4 + } ? { + (...args: Args1): Effect.Effect, E> + (...args: Args2): Effect.Effect, E> + (...args: Args3): Effect.Effect, E> + (...args: Args4): Effect.Effect, E> + } + : T extends { + (...args: ArgsWithCallback): infer _R1 + (...args: ArgsWithCallback): infer _R2 + (...args: ArgsWithCallback): infer _R3 + } ? { + (...args: Args1): Effect.Effect, E> + (...args: Args2): Effect.Effect, E> + (...args: Args3): Effect.Effect, E> + } + : T extends { + (...args: ArgsWithCallback): infer _R1 + (...args: ArgsWithCallback): infer _R2 + } ? { + (...args: Args1): Effect.Effect, E> + (...args: Args2): Effect.Effect, E> + } + : T extends { + (...args: ArgsWithCallback): infer _R1 + } ? { + (...args: Args1): Effect.Effect, E> + } + : never + +/** + * @category util + * @since 1.0.0 + */ +export type EffectifyError = T extends { + (...args: ArgsWithCallback): infer _R1 + (...args: ArgsWithCallback): infer _R2 + (...args: ArgsWithCallback): infer _R3 + (...args: ArgsWithCallback): infer _R4 + (...args: ArgsWithCallback): infer _R5 + (...args: ArgsWithCallback): infer _R6 + (...args: ArgsWithCallback): infer _R7 + (...args: ArgsWithCallback): infer _R8 + (...args: ArgsWithCallback): infer _R9 + (...args: ArgsWithCallback): infer _R10 +} ? NonNullable + : T extends { + (...args: ArgsWithCallback): infer _R1 + (...args: ArgsWithCallback): infer _R2 + (...args: ArgsWithCallback): infer _R3 + (...args: ArgsWithCallback): infer _R4 + (...args: ArgsWithCallback): infer _R5 + (...args: ArgsWithCallback): infer _R6 + (...args: ArgsWithCallback): infer _R7 + (...args: ArgsWithCallback): infer _R8 + (...args: ArgsWithCallback): infer _R9 + } ? NonNullable + : T extends { + (...args: ArgsWithCallback): infer _R1 + (...args: ArgsWithCallback): infer _R2 + (...args: ArgsWithCallback): infer _R3 + (...args: ArgsWithCallback): infer _R4 + (...args: ArgsWithCallback): infer _R5 + (...args: ArgsWithCallback): infer _R6 + (...args: ArgsWithCallback): infer _R7 + (...args: ArgsWithCallback): infer _R8 + } ? NonNullable + : T extends { + (...args: ArgsWithCallback): infer _R1 + (...args: ArgsWithCallback): infer _R2 + (...args: ArgsWithCallback): infer _R3 + (...args: ArgsWithCallback): infer _R4 + (...args: ArgsWithCallback): infer _R5 + (...args: ArgsWithCallback): infer _R6 + (...args: ArgsWithCallback): infer _R7 + } ? NonNullable + : T extends { + (...args: ArgsWithCallback): infer _R1 + (...args: ArgsWithCallback): infer _R2 + (...args: ArgsWithCallback): infer _R3 + (...args: ArgsWithCallback): infer _R4 + (...args: ArgsWithCallback): infer _R5 + (...args: ArgsWithCallback): infer _R6 + } ? NonNullable + : T extends { + (...args: ArgsWithCallback): infer _R1 + (...args: ArgsWithCallback): infer _R2 + (...args: ArgsWithCallback): infer _R3 + (...args: ArgsWithCallback): infer _R4 + (...args: ArgsWithCallback): infer _R5 + } ? NonNullable + : T extends { + (...args: ArgsWithCallback): infer _R1 + (...args: ArgsWithCallback): infer _R2 + (...args: ArgsWithCallback): infer _R3 + (...args: ArgsWithCallback): infer _R4 + } ? NonNullable + : T extends { + (...args: ArgsWithCallback): infer _R1 + (...args: ArgsWithCallback): infer _R2 + (...args: ArgsWithCallback): infer _R3 + } ? NonNullable + : T extends { + (...args: ArgsWithCallback): infer _R1 + (...args: ArgsWithCallback): infer _R2 + } ? NonNullable + : T extends { + (...args: ArgsWithCallback): infer _R1 + } ? NonNullable + : never + +/** + * @since 1.0.0 + */ +export const effectify: { + ) => any>(fn: F): Effectify> + ) => any, E>( + fn: F, + onError: (error: EffectifyError, args: Parameters) => E + ): Effectify + ) => any, E, E2>( + fn: F, + onError: (error: EffectifyError, args: Parameters) => E, + onSyncError: (error: unknown, args: Parameters) => E2 + ): Effectify +} = internal.effectify diff --git a/repos/effect/packages/platform/src/Error.ts b/repos/effect/packages/platform/src/Error.ts new file mode 100644 index 0000000..11d9834 --- /dev/null +++ b/repos/effect/packages/platform/src/Error.ts @@ -0,0 +1,153 @@ +/** + * @since 1.0.0 + */ +import type * as Cause from "effect/Cause" +import * as Data from "effect/Data" +import * as Predicate from "effect/Predicate" +import * as Schema from "effect/Schema" +import type { Simplify } from "effect/Types" + +/** + * @since 1.0.0 + * @category type id + */ +export const TypeId: unique symbol = Symbol.for("@effect/platform/Error") + +/** + * @since 1.0.0 + * @category type id + */ +export type TypeId = typeof TypeId + +/** + * @since 1.0.0 + * @category refinements + */ +export const isPlatformError = (u: unknown): u is PlatformError => Predicate.hasProperty(u, TypeId) + +/** + * @since 1.0.0 + * @category error + */ +export const TypeIdError = ( + typeId: TypeId, + tag: Tag +): new>( + args: Simplify +) => + & Cause.YieldableError + & Record + & { readonly _tag: Tag } + & Readonly => +{ + class Base extends Data.Error<{}> { + readonly _tag = tag + } + ;(Base.prototype as any)[typeId] = typeId + ;(Base.prototype as any).name = tag + return Base as any +} + +/** + * @since 1.0.0 + * @category Models + */ +export const Module = Schema.Literal( + "Clipboard", + "Command", + "FileSystem", + "KeyValueStore", + "Path", + "Stream", + "Terminal" +) + +/** + * @since 1.0.0 + * @category Models + */ +export class BadArgument extends Schema.TaggedError("@effect/platform/Error/BadArgument")("BadArgument", { + module: Module, + method: Schema.String, + description: Schema.optional(Schema.String), + cause: Schema.optional(Schema.Defect) +}) { + /** + * @since 1.0.0 + */ + readonly [TypeId]: typeof TypeId = TypeId + + /** + * @since 1.0.0 + */ + get message(): string { + return `${this.module}.${this.method}${this.description ? `: ${this.description}` : ""}` + } +} + +/** + * @since 1.0.0 + * @category Model + */ +export const SystemErrorReason = Schema.Literal( + "AlreadyExists", + "BadResource", + "Busy", + "InvalidData", + "NotFound", + "PermissionDenied", + "TimedOut", + "UnexpectedEof", + "Unknown", + "WouldBlock", + "WriteZero" +) + +/** + * @since 1.0.0 + * @category Model + */ +export type SystemErrorReason = typeof SystemErrorReason.Type + +/** + * @since 1.0.0 + * @category models + */ +export class SystemError extends Schema.TaggedError("@effect/platform/Error/SystemError")("SystemError", { + reason: SystemErrorReason, + module: Module, + method: Schema.String, + description: Schema.optional(Schema.String), + syscall: Schema.optional(Schema.String), + pathOrDescriptor: Schema.optional(Schema.Union(Schema.String, Schema.Number)), + cause: Schema.optional(Schema.Defect) +}) { + /** + * @since 1.0.0 + */ + readonly [TypeId]: typeof TypeId = TypeId + + /** + * @since 1.0.0 + */ + get message(): string { + return `${this.reason}: ${this.module}.${this.method}${ + this.pathOrDescriptor !== undefined ? ` (${this.pathOrDescriptor})` : "" + }${this.description ? `: ${this.description}` : ""}` + } +} + +/** + * @since 1.0.0 + * @category Models + */ +export type PlatformError = BadArgument | SystemError + +/** + * @since 1.0.0 + * @category Models + */ +export const PlatformError: Schema.Union<[ + typeof BadArgument, + typeof SystemError +]> = Schema.Union(BadArgument, SystemError) diff --git a/repos/effect/packages/platform/src/Etag.ts b/repos/effect/packages/platform/src/Etag.ts new file mode 100644 index 0000000..34141e0 --- /dev/null +++ b/repos/effect/packages/platform/src/Etag.ts @@ -0,0 +1,79 @@ +/** + * @since 1.0.0 + */ +import type * as Context from "effect/Context" +import type * as Effect from "effect/Effect" +import type { Layer } from "effect/Layer" +import type * as FileSystem from "./FileSystem.js" +import type * as Body from "./HttpBody.js" +import * as internal from "./internal/etag.js" + +/** + * @since 1.0.0 + * @category models + */ +export type Etag = Weak | Strong + +/** + * @since 1.0.0 + * @category models + */ +export interface Weak { + readonly _tag: "Weak" + readonly value: string +} + +/** + * @since 1.0.0 + * @category models + */ +export interface Strong { + readonly _tag: "Strong" + readonly value: string +} + +/** + * @since 1.0.0 + * @category convertions + */ +export const toString: (self: Etag) => string = internal.toString + +/** + * @since 1.0.0 + * @category type ids + */ +export const GeneratorTypeId: unique symbol = internal.GeneratorTypeId + +/** + * @since 1.0.0 + * @category type ids + */ +export type GeneratorTypeId = typeof GeneratorTypeId + +/** + * @since 1.0.0 + * @category models + */ +export interface Generator { + readonly [GeneratorTypeId]: GeneratorTypeId + readonly fromFileInfo: (info: FileSystem.File.Info) => Effect.Effect + readonly fromFileWeb: (file: Body.HttpBody.FileLike) => Effect.Effect +} + +/** + * @since 1.0.0 + * @category tags + */ +export const Generator: Context.Tag = internal.tag + +/** + * @since 1.0.0 + * @category layers + */ +export const layer: Layer = internal.layer + +/** + * @since 1.0.0 + * @category layers + */ +export const layerWeak: Layer = internal.layerWeak diff --git a/repos/effect/packages/platform/src/FetchHttpClient.ts b/repos/effect/packages/platform/src/FetchHttpClient.ts new file mode 100644 index 0000000..0f1a546 --- /dev/null +++ b/repos/effect/packages/platform/src/FetchHttpClient.ts @@ -0,0 +1,25 @@ +/** + * @since 1.0.0 + */ +import * as Context from "effect/Context" +import type * as Layer from "effect/Layer" +import type { HttpClient } from "./HttpClient.js" +import * as internal from "./internal/fetchHttpClient.js" + +/** + * @since 1.0.0 + * @category tags + */ +export class Fetch extends Context.Tag(internal.fetchTagKey)() {} + +/** + * @since 1.0.0 + * @category tags + */ +export class RequestInit extends Context.Tag(internal.requestInitTagKey)() {} + +/** + * @since 1.0.0 + * @category layers + */ +export const layer: Layer.Layer = internal.layer diff --git a/repos/effect/packages/platform/src/FileSystem.ts b/repos/effect/packages/platform/src/FileSystem.ts new file mode 100644 index 0000000..3871f80 --- /dev/null +++ b/repos/effect/packages/platform/src/FileSystem.ts @@ -0,0 +1,658 @@ +/** + * @since 1.0.0 + */ +import * as Brand from "effect/Brand" +import type { Tag } from "effect/Context" +import * as Context from "effect/Context" +import * as Data from "effect/Data" +import type * as Effect from "effect/Effect" +import type { Layer } from "effect/Layer" +import type { Option } from "effect/Option" +import type { Scope } from "effect/Scope" +import type { Sink } from "effect/Sink" +import type { Stream } from "effect/Stream" +import type { PlatformError } from "./Error.js" +import * as internal from "./internal/fileSystem.js" + +/** + * @since 1.0.0 + * @category model + */ +export interface FileSystem { + /** + * Check if a file can be accessed. + * You can optionally specify the level of access to check for. + */ + readonly access: ( + path: string, + options?: AccessFileOptions + ) => Effect.Effect + /** + * Copy a file or directory from `fromPath` to `toPath`. + * + * Equivalent to `cp -r`. + */ + readonly copy: ( + fromPath: string, + toPath: string, + options?: CopyOptions + ) => Effect.Effect + /** + * Copy a file from `fromPath` to `toPath`. + */ + readonly copyFile: ( + fromPath: string, + toPath: string + ) => Effect.Effect + /** + * Change the permissions of a file. + */ + readonly chmod: ( + path: string, + mode: number + ) => Effect.Effect + /** + * Change the owner and group of a file. + */ + readonly chown: ( + path: string, + uid: number, + gid: number + ) => Effect.Effect + /** + * Check if a path exists. + */ + readonly exists: ( + path: string + ) => Effect.Effect + /** + * Create a hard link from `fromPath` to `toPath`. + */ + readonly link: ( + fromPath: string, + toPath: string + ) => Effect.Effect + /** + * Create a directory at `path`. You can optionally specify the mode and + * whether to recursively create nested directories. + */ + readonly makeDirectory: ( + path: string, + options?: MakeDirectoryOptions + ) => Effect.Effect + /** + * Create a temporary directory. + * + * By default the directory will be created inside the system's default + * temporary directory, but you can specify a different location by setting + * the `directory` option. + * + * You can also specify a prefix for the directory name by setting the + * `prefix` option. + */ + readonly makeTempDirectory: ( + options?: MakeTempDirectoryOptions + ) => Effect.Effect + /** + * Create a temporary directory inside a scope. + * + * Functionally equivalent to `makeTempDirectory`, but the directory will be + * automatically deleted when the scope is closed. + */ + readonly makeTempDirectoryScoped: ( + options?: MakeTempDirectoryOptions + ) => Effect.Effect + /** + * Create a temporary file. + * The directory creation is functionally equivalent to `makeTempDirectory`. + * The file name will be a randomly generated string. + */ + readonly makeTempFile: ( + options?: MakeTempFileOptions + ) => Effect.Effect + /** + * Create a temporary file inside a scope. + * + * Functionally equivalent to `makeTempFile`, but the file will be + * automatically deleted when the scope is closed. + */ + readonly makeTempFileScoped: ( + options?: MakeTempFileOptions + ) => Effect.Effect + /** + * Open a file at `path` with the specified `options`. + * + * The file handle will be automatically closed when the scope is closed. + */ + readonly open: ( + path: string, + options?: OpenFileOptions + ) => Effect.Effect + /** + * List the contents of a directory. + * + * You can recursively list the contents of nested directories by setting the + * `recursive` option. + */ + readonly readDirectory: ( + path: string, + options?: ReadDirectoryOptions + ) => Effect.Effect, PlatformError> + /** + * Read the contents of a file. + */ + readonly readFile: ( + path: string + ) => Effect.Effect + /** + * Read the contents of a file. + */ + readonly readFileString: ( + path: string, + encoding?: string + ) => Effect.Effect + /** + * Read the destination of a symbolic link. + */ + readonly readLink: ( + path: string + ) => Effect.Effect + /** + * Resolve a path to its canonicalized absolute pathname. + */ + readonly realPath: ( + path: string + ) => Effect.Effect + /** + * Remove a file or directory. + */ + readonly remove: ( + path: string, + options?: RemoveOptions + ) => Effect.Effect + /** + * Rename a file or directory. + */ + readonly rename: ( + oldPath: string, + newPath: string + ) => Effect.Effect + /** + * Create a writable `Sink` for the specified `path`. + */ + readonly sink: ( + path: string, + options?: SinkOptions + ) => Sink + /** + * Get information about a file at `path`. + */ + readonly stat: ( + path: string + ) => Effect.Effect + /** + * Create a readable `Stream` for the specified `path`. + * + * Changing the `bufferSize` option will change the internal buffer size of + * the stream. It defaults to `4`. + * + * The `chunkSize` option will change the size of the chunks emitted by the + * stream. It defaults to 64kb. + * + * Changing `offset` and `bytesToRead` will change the offset and the number + * of bytes to read from the file. + */ + readonly stream: ( + path: string, + options?: StreamOptions + ) => Stream + /** + * Create a symbolic link from `fromPath` to `toPath`. + */ + readonly symlink: ( + fromPath: string, + toPath: string + ) => Effect.Effect + /** + * Truncate a file to a specified length. If the `length` is not specified, + * the file will be truncated to length `0`. + */ + readonly truncate: ( + path: string, + length?: SizeInput + ) => Effect.Effect + /** + * Change the file system timestamps of the file at `path`. + */ + readonly utimes: ( + path: string, + atime: Date | number, + mtime: Date | number + ) => Effect.Effect + /** + * Watch a directory or file for changes. + * + * By default, only changes to the direct children of the directory are reported. + * Set the `recursive` option to `true` to watch for changes in subdirectories as well. + * + * Note: The `recursive` option behavior depends on the backend implementation: + * - When using the default Node.js `fs.watch()` backend: The `recursive` + * option is supported on all platforms (Node.js v20+). + * - When using `@parcel/watcher` (via `NodeFileSystem/ParcelWatcher` layer): + * Watching is always recursive on all platforms. This option is ignored. + */ + readonly watch: (path: string, options?: WatchOptions) => Stream + /** + * Write data to a file at `path`. + */ + readonly writeFile: ( + path: string, + data: Uint8Array, + options?: WriteFileOptions + ) => Effect.Effect + /** + * Write a string to a file at `path`. + */ + readonly writeFileString: ( + path: string, + data: string, + options?: WriteFileStringOptions + ) => Effect.Effect +} + +/** + * Represents a size in bytes. + * + * @since 1.0.0 + * @category sizes + */ +export type Size = Brand.Branded + +/** + * Represents a size in bytes. + * + * @since 1.0.0 + * @category sizes + */ +export type SizeInput = bigint | number | Size + +/** + * @since 1.0.0 + * @category sizes + */ +export const Size: (bytes: SizeInput) => Size = internal.Size + +/** + * @since 1.0.0 + * @category sizes + */ +export const KiB: (n: number) => Size = internal.KiB + +/** + * @since 1.0.0 + * @category sizes + */ +export const MiB: (n: number) => Size = internal.MiB + +/** + * @since 1.0.0 + * @category sizes + */ +export const GiB: (n: number) => Size = internal.GiB + +/** + * @since 1.0.0 + * @category sizes + */ +export const TiB: (n: number) => Size = internal.TiB + +/** + * @since 1.0.0 + * @category sizes + */ +export const PiB: (n: number) => Size = internal.PiB + +/** + * @since 1.0.0 + * @category model + */ +export type OpenFlag = + | "r" + | "r+" + | "w" + | "wx" + | "w+" + | "wx+" + | "a" + | "ax" + | "a+" + | "ax+" + +/** + * @since 1.0.0 + * @category options + */ +export interface AccessFileOptions { + readonly ok?: boolean + readonly readable?: boolean + readonly writable?: boolean +} + +/** + * @since 1.0.0 + * @category options + */ +export interface MakeDirectoryOptions { + readonly recursive?: boolean + readonly mode?: number +} + +/** + * @since 1.0.0 + * @category options + */ +export interface CopyOptions { + readonly overwrite?: boolean + readonly preserveTimestamps?: boolean +} + +/** + * @since 1.0.0 + * @category options + */ +export interface MakeTempDirectoryOptions { + readonly directory?: string + readonly prefix?: string +} + +/** + * @since 1.0.0 + * @category options + */ +export interface MakeTempFileOptions { + readonly directory?: string + readonly prefix?: string + readonly suffix?: string +} + +/** + * @since 1.0.0 + * @category options + */ +export interface OpenFileOptions { + readonly flag?: OpenFlag + readonly mode?: number +} + +/** + * @since 1.0.0 + * @category options + */ +export interface ReadDirectoryOptions { + readonly recursive?: boolean +} + +/** + * @since 1.0.0 + * @category options + */ +export interface RemoveOptions { + /** + * When `true`, you can recursively remove nested directories. + */ + readonly recursive?: boolean + /** + * When `true`, exceptions will be ignored if `path` does not exist. + */ + readonly force?: boolean +} + +/** + * @since 1.0.0 + * @category options + */ +export interface SinkOptions extends OpenFileOptions {} + +/** + * @since 1.0.0 + * @category options + */ +export interface StreamOptions { + readonly bufferSize?: number + readonly bytesToRead?: SizeInput + readonly chunkSize?: SizeInput + readonly offset?: SizeInput +} + +/** + * @since 1.0.0 + * @category options + */ +export interface WriteFileOptions { + readonly flag?: OpenFlag + readonly mode?: number +} + +/** + * @since 1.0.0 + * @category options + */ +export interface WriteFileStringOptions { + readonly flag?: OpenFlag + readonly mode?: number +} + +/** + * @since 1.0.0 + * @category options + */ +export interface WatchOptions { + /** + * When `true`, the watcher will also watch for changes in subdirectories. + */ + readonly recursive?: boolean +} + +/** + * @since 1.0.0 + * @category tag + */ +export const FileSystem: Tag = internal.tag + +/** + * @since 1.0.0 + * @category constructor + */ +export const make: ( + impl: Omit +) => FileSystem = internal.make + +/** + * Create a no-op file system that can be used for testing. + * + * @since 1.0.0 + * @category constructor + */ +export const makeNoop: (fileSystem: Partial) => FileSystem = internal.makeNoop + +/** + * Create a no-op file system that can be used for testing. + * + * @since 1.0.0 + * @category layers + */ +export const layerNoop: (fileSystem: Partial) => Layer = internal.layerNoop + +/** + * @since 1.0.0 + * @category type id + */ +export const FileTypeId: unique symbol = Symbol.for( + "@effect/platform/FileSystem/File" +) + +/** + * @since 1.0.0 + * @category type id + */ +export type FileTypeId = typeof FileTypeId + +/** + * @since 1.0.0 + * @category guard + */ +export const isFile = (u: unknown): u is File => typeof u === "object" && u !== null && FileTypeId in u + +/** + * @since 1.0.0 + * @category model + */ +export interface File { + readonly [FileTypeId]: FileTypeId + readonly fd: File.Descriptor + readonly stat: Effect.Effect + readonly seek: (offset: SizeInput, from: SeekMode) => Effect.Effect + readonly sync: Effect.Effect + readonly read: (buffer: Uint8Array) => Effect.Effect + readonly readAlloc: (size: SizeInput) => Effect.Effect, PlatformError> + readonly truncate: (length?: SizeInput) => Effect.Effect + readonly write: (buffer: Uint8Array) => Effect.Effect + readonly writeAll: (buffer: Uint8Array) => Effect.Effect +} + +/** + * @since 1.0.0 + */ +export declare namespace File { + /** + * @since 1.0.0 + * @category model + */ + export type Descriptor = Brand.Branded + + /** + * @since 1.0.0 + * @category model + */ + export type Type = + | "File" + | "Directory" + | "SymbolicLink" + | "BlockDevice" + | "CharacterDevice" + | "FIFO" + | "Socket" + | "Unknown" + + /** + * @since 1.0.0 + * @category model + */ + export interface Info { + readonly type: Type + readonly mtime: Option + readonly atime: Option + readonly birthtime: Option + readonly dev: number + readonly ino: Option + readonly mode: number + readonly nlink: Option + readonly uid: Option + readonly gid: Option + readonly rdev: Option + readonly size: Size + readonly blksize: Option + readonly blocks: Option + } +} + +/** + * @since 1.0.0 + * @category constructor + */ +export const FileDescriptor = Brand.nominal() + +/** + * @since 1.0.0 + * @category model + */ +export type SeekMode = "start" | "current" + +/** + * @since 1.0.0 + * @category model + */ +export type WatchEvent = WatchEvent.Create | WatchEvent.Update | WatchEvent.Remove + +/** + * @since 1.0.0 + * @category model + */ +export declare namespace WatchEvent { + /** + * @since 1.0.0 + * @category model + */ + export interface Create { + readonly _tag: "Create" + readonly path: string + } + + /** + * @since 1.0.0 + * @category model + */ + export interface Update { + readonly _tag: "Update" + readonly path: string + } + + /** + * @since 1.0.0 + * @category model + */ + export interface Remove { + readonly _tag: "Remove" + readonly path: string + } +} + +/** + * @since 1.0.0 + * @category constructor + */ +export const WatchEventCreate: Data.Case.Constructor = Data.tagged( + "Create" +) + +/** + * @since 1.0.0 + * @category constructor + */ +export const WatchEventUpdate: Data.Case.Constructor = Data.tagged( + "Update" +) + +/** + * @since 1.0.0 + * @category constructor + */ +export const WatchEventRemove: Data.Case.Constructor = Data.tagged( + "Remove" +) + +/** + * @since 1.0.0 + * @category file watcher + */ +export class WatchBackend extends Context.Tag("@effect/platform/FileSystem/WatchBackend")< + WatchBackend, + { + readonly register: ( + path: string, + stat: File.Info, + options?: WatchOptions + ) => Option> + } +>() { +} diff --git a/repos/effect/packages/platform/src/Headers.ts b/repos/effect/packages/platform/src/Headers.ts new file mode 100644 index 0000000..cf97be6 --- /dev/null +++ b/repos/effect/packages/platform/src/Headers.ts @@ -0,0 +1,289 @@ +/** + * @since 1.0.0 + */ +import * as FiberRef from "effect/FiberRef" +import * as FiberRefs from "effect/FiberRefs" +import { dual, identity } from "effect/Function" +import { globalValue } from "effect/GlobalValue" +import { type Redactable, symbolRedactable } from "effect/Inspectable" +import type * as Option from "effect/Option" +import * as Predicate from "effect/Predicate" +import * as Record from "effect/Record" +import * as Redacted from "effect/Redacted" +import * as Schema from "effect/Schema" +import * as String from "effect/String" +import type { Mutable } from "effect/Types" + +/** + * @since 1.0.0 + * @category type ids + */ +export const HeadersTypeId: unique symbol = Symbol.for("@effect/platform/Headers") + +/** + * @since 1.0.0 + * @category type ids + */ +export type HeadersTypeId = typeof HeadersTypeId + +/** + * @since 1.0.0 + * @category refinements + */ +export const isHeaders = (u: unknown): u is Headers => Predicate.hasProperty(u, HeadersTypeId) + +/** + * @since 1.0.0 + * @category models + */ +export interface Headers extends Redactable { + readonly [HeadersTypeId]: HeadersTypeId + readonly [key: string]: string +} + +const Proto = Object.assign(Object.create(null), { + [HeadersTypeId]: HeadersTypeId, + [symbolRedactable]( + this: Headers, + fiberRefs: FiberRefs.FiberRefs + ): Record> { + return redact(this, FiberRefs.getOrDefault(fiberRefs, currentRedactedNames)) + } +}) + +const make = (input: Record.ReadonlyRecord): Mutable => + Object.assign(Object.create(Proto), input) as Headers + +/** + * @since 1.0.0 + * @category schemas + */ +export const schemaFromSelf: Schema.Schema = Schema.declare(isHeaders, { + typeConstructor: { _tag: "effect/platform/Headers" }, + identifier: "Headers", + equivalence: () => Record.getEquivalence(String.Equivalence) +}) + +/** + * @since 1.0.0 + * @category schemas + */ +export const schema: Schema.Schema> = Schema + .transform( + Schema.Record({ key: Schema.String, value: Schema.String }), + schemaFromSelf, + { strict: true, decode: (record) => fromInput(record), encode: identity } + ) + +/** + * @since 1.0.0 + * @category models + */ +export type Input = + | Record.ReadonlyRecord | undefined> + | Iterable + +/** + * @since 1.0.0 + * @category constructors + */ +export const empty: Headers = Object.create(Proto) + +/** + * @since 1.0.0 + * @category constructors + */ +export const fromInput: (input?: Input) => Headers = (input) => { + if (input === undefined) { + return empty + } else if (Symbol.iterator in input) { + const out: Record = Object.create(Proto) + for (const [k, v] of input) { + out[k.toLowerCase()] = v + } + return out as Headers + } + const out: Record = Object.create(Proto) + for (const [k, v] of Object.entries(input)) { + if (Array.isArray(v)) { + out[k.toLowerCase()] = v.join(", ") + } else if (v !== undefined) { + out[k.toLowerCase()] = v as string + } + } + return out as Headers +} + +/** + * @since 1.0.0 + * @category constructors + */ +export const unsafeFromRecord = (input: Record.ReadonlyRecord): Headers => + Object.setPrototypeOf(input, Proto) as Headers + +/** + * @since 1.0.0 + * @category combinators + */ +export const has: { + (key: string): (self: Headers) => boolean + (self: Headers, key: string): boolean +} = dual< + (key: string) => (self: Headers) => boolean, + (self: Headers, key: string) => boolean +>(2, (self, key) => key.toLowerCase() in self) + +/** + * @since 1.0.0 + * @category combinators + */ +export const get: { + (key: string): (self: Headers) => Option.Option + (self: Headers, key: string): Option.Option +} = dual< + (key: string) => (self: Headers) => Option.Option, + (self: Headers, key: string) => Option.Option +>(2, (self, key) => Record.get(self as Record, key.toLowerCase())) + +/** + * @since 1.0.0 + * @category combinators + */ +export const set: { + (key: string, value: string): (self: Headers) => Headers + (self: Headers, key: string, value: string): Headers +} = dual< + (key: string, value: string) => (self: Headers) => Headers, + (self: Headers, key: string, value: string) => Headers +>(3, (self, key, value) => { + const out = make(self) + out[key.toLowerCase()] = value + return out +}) + +/** + * @since 1.0.0 + * @category combinators + */ +export const setAll: { + (headers: Input): (self: Headers) => Headers + (self: Headers, headers: Input): Headers +} = dual< + (headers: Input) => (self: Headers) => Headers, + (self: Headers, headers: Input) => Headers +>(2, (self, headers) => + make({ + ...self, + ...fromInput(headers) + })) + +/** + * @since 1.0.0 + * @category combinators + */ +export const merge: { + (headers: Headers): (self: Headers) => Headers + (self: Headers, headers: Headers): Headers +} = dual< + (headers: Headers) => (self: Headers) => Headers, + (self: Headers, headers: Headers) => Headers +>(2, (self, headers) => { + const out = make(self) + Object.assign(out, headers) + return out +}) + +/** + * @since 1.0.0 + * @category combinators + */ +export const remove: { + (key: string | RegExp | ReadonlyArray): (self: Headers) => Headers + (self: Headers, key: string | RegExp | ReadonlyArray): Headers +} = dual< + (key: string | RegExp | ReadonlyArray) => (self: Headers) => Headers, + (self: Headers, key: string | RegExp | ReadonlyArray) => Headers +>(2, (self, key) => { + const out = make(self) + const modify = (key: string | RegExp) => { + if (typeof key === "string") { + const k = key.toLowerCase() + if (k in self) { + delete out[k] + } + } else { + for (const name in self) { + if (key.test(name)) { + delete out[name] + } + } + } + } + if (Array.isArray(key)) { + for (let i = 0; i < key.length; i++) { + modify(key[i]) + } + } else { + modify(key as string | RegExp) + } + return out +}) + +/** + * @since 1.0.0 + * @category combinators + */ +export const redact: { + ( + key: string | RegExp | ReadonlyArray + ): (self: Headers) => Record + ( + self: Headers, + key: string | RegExp | ReadonlyArray + ): Record +} = dual( + 2, + ( + self: Headers, + key: string | RegExp | ReadonlyArray + ): Record => { + const out: Record = { ...self } + const modify = (key: string | RegExp) => { + if (typeof key === "string") { + const k = key.toLowerCase() + if (k in self) { + out[k] = Redacted.make(self[k]) + } + } else { + for (const name in self) { + if (key.test(name)) { + out[name] = Redacted.make(self[name]) + } + } + } + } + if (Array.isArray(key)) { + for (let i = 0; i < key.length; i++) { + modify(key[i]) + } + } else { + modify(key as string | RegExp) + } + return out + } +) + +/** + * @since 1.0.0 + * @category fiber refs + */ +export const currentRedactedNames: FiberRef.FiberRef> = globalValue( + "@effect/platform/Headers/currentRedactedNames", + () => + FiberRef.unsafeMake>([ + "authorization", + "cookie", + "set-cookie", + "x-api-key" + ]) +) diff --git a/repos/effect/packages/platform/src/HttpApi.ts b/repos/effect/packages/platform/src/HttpApi.ts new file mode 100644 index 0000000..2cbcf25 --- /dev/null +++ b/repos/effect/packages/platform/src/HttpApi.ts @@ -0,0 +1,455 @@ +/** + * @since 1.0.0 + */ +import * as Context from "effect/Context" +import * as Option from "effect/Option" +import { type Pipeable, pipeArguments } from "effect/Pipeable" +import * as Predicate from "effect/Predicate" +import * as Record from "effect/Record" +import type * as Schema from "effect/Schema" +import * as AST from "effect/SchemaAST" +import type { Mutable } from "effect/Types" +import type * as HttpApiEndpoint from "./HttpApiEndpoint.js" +import { HttpApiDecodeError } from "./HttpApiError.js" +import type * as HttpApiGroup from "./HttpApiGroup.js" +import type * as HttpApiMiddleware from "./HttpApiMiddleware.js" +import * as HttpApiSchema from "./HttpApiSchema.js" +import type { HttpMethod } from "./HttpMethod.js" + +/** + * @since 1.0.0 + * @category type ids + */ +export const TypeId: unique symbol = Symbol.for("@effect/platform/HttpApi") + +/** + * @since 1.0.0 + * @category type ids + */ +export type TypeId = typeof TypeId + +/** + * @since 1.0.0 + * @category guards + */ +export const isHttpApi = (u: unknown): u is HttpApi.Any => Predicate.hasProperty(u, TypeId) + +/** + * An `HttpApi` is a collection of `HttpApiEndpoint`s. You can use an `HttpApi` to + * represent a portion of your domain. + * + * The endpoints can be implemented later using the `HttpApiBuilder.make` api. + * + * @since 1.0.0 + * @category models + */ +export interface HttpApi< + out Id extends string, + out Groups extends HttpApiGroup.HttpApiGroup.Any = never, + in out E = never, + out R = never +> extends Pipeable { + new(_: never): {} + readonly [TypeId]: TypeId + readonly identifier: Id + readonly groups: Record.ReadonlyRecord + readonly annotations: Context.Context + readonly errorSchema: Schema.Schema + readonly middlewares: ReadonlySet + + /** + * Add a `HttpApiGroup` to the `HttpApi`. + */ + add(group: A): HttpApi + /** + * Add another `HttpApi` to the `HttpApi`. + */ + addHttpApi( + api: HttpApi + ): HttpApi< + Id, + Groups | HttpApiGroup.HttpApiGroup.AddContext, + E | E2, + R + > + /** + * Add an global error to the `HttpApi`. + */ + addError( + schema: Schema.Schema, + annotations?: { + readonly status?: number | undefined + } + ): HttpApi + /** + * Prefix all endpoints in the `HttpApi`. + */ + prefix(prefix: HttpApiEndpoint.PathSegment): HttpApi + /** + * Add a middleware to a `HttpApi`. It will be applied to all endpoints in the + * `HttpApi`. + */ + middleware( + middleware: Context.Tag + ): HttpApi< + Id, + Groups, + E | HttpApiMiddleware.HttpApiMiddleware.Error, + R | I | HttpApiMiddleware.HttpApiMiddleware.ErrorContext + > + /** + * Annotate the `HttpApi`. + */ + annotate(tag: Context.Tag, value: S): HttpApi + /** + * Annotate the `HttpApi` with a Context. + */ + annotateContext(context: Context.Context): HttpApi +} + +/** + * @since 1.0.0 + * @category tags + */ +export class Api extends Context.Tag("@effect/platform/HttpApi/Api")< + Api, + { + readonly api: HttpApi + readonly context: Context.Context + } +>() {} + +/** + * @since 1.0.0 + * @category models + */ +export declare namespace HttpApi { + /** + * @since 1.0.0 + * @category models + */ + export interface Any { + readonly [TypeId]: TypeId + } + + /** + * @since 1.0.0 + * @category models + */ + export type AnyWithProps = HttpApi +} + +const Proto = { + [TypeId]: TypeId, + pipe() { + return pipeArguments(this, arguments) + }, + add( + this: HttpApi.AnyWithProps, + group: HttpApiGroup.HttpApiGroup.AnyWithProps + ) { + return makeProto({ + identifier: this.identifier, + groups: Record.set(this.groups, group.identifier, group), + errorSchema: this.errorSchema, + annotations: this.annotations, + middlewares: this.middlewares + }) + }, + addHttpApi( + this: HttpApi.AnyWithProps, + api: HttpApi.AnyWithProps + ) { + const newGroups = { ...this.groups } + for (const key in api.groups) { + const newGroup: Mutable = api.groups[key].annotateContext(Context.empty()) + newGroup.annotations = Context.merge(api.annotations, newGroup.annotations) + newGroup.middlewares = new Set([...api.middlewares, ...newGroup.middlewares]) + newGroups[key] = newGroup as any + } + return makeProto({ + identifier: this.identifier, + groups: newGroups, + errorSchema: HttpApiSchema.UnionUnify(this.errorSchema, api.errorSchema), + annotations: this.annotations, + middlewares: this.middlewares + }) + }, + addError( + this: HttpApi.AnyWithProps, + schema: Schema.Schema.Any, + annotations?: { readonly status?: number } + ) { + return makeProto({ + identifier: this.identifier, + groups: this.groups, + errorSchema: HttpApiSchema.UnionUnify( + this.errorSchema, + annotations?.status + ? schema.annotations(HttpApiSchema.annotations({ status: annotations.status })) + : schema + ), + annotations: this.annotations, + middlewares: this.middlewares + }) + }, + prefix(this: HttpApi.AnyWithProps, prefix: HttpApiEndpoint.PathSegment) { + return makeProto({ + identifier: this.identifier, + groups: Record.map(this.groups, (group) => group.prefix(prefix)), + errorSchema: this.errorSchema, + annotations: this.annotations, + middlewares: this.middlewares + }) + }, + middleware(this: HttpApi.AnyWithProps, tag: HttpApiMiddleware.TagClassAny) { + return makeProto({ + identifier: this.identifier, + groups: this.groups, + errorSchema: HttpApiSchema.UnionUnify(this.errorSchema, tag.failure), + annotations: this.annotations, + middlewares: new Set([...this.middlewares, tag]) + }) + }, + annotate(this: HttpApi.AnyWithProps, tag: Context.Tag, value: any) { + return makeProto({ + identifier: this.identifier, + groups: this.groups, + errorSchema: this.errorSchema, + annotations: Context.add(this.annotations, tag, value), + middlewares: this.middlewares + }) + }, + annotateContext(this: HttpApi.AnyWithProps, context: Context.Context) { + return makeProto({ + identifier: this.identifier, + groups: this.groups, + errorSchema: this.errorSchema, + annotations: Context.merge(this.annotations, context), + middlewares: this.middlewares + }) + } +} + +const makeProto = ( + options: { + readonly identifier: Id + readonly groups: Record.ReadonlyRecord + readonly errorSchema: Schema.Schema + readonly annotations: Context.Context + readonly middlewares: ReadonlySet + } +): HttpApi => { + function HttpApi() {} + Object.setPrototypeOf(HttpApi, Proto) + HttpApi.groups = options.groups + HttpApi.errorSchema = options.errorSchema + HttpApi.annotations = options.annotations + HttpApi.middlewares = options.middlewares + return HttpApi as any +} + +/** + * An `HttpApi` is a collection of `HttpApiEndpoint`s. You can use an `HttpApi` to + * represent a portion of your domain. + * + * The endpoints can be implemented later using the `HttpApiBuilder.make` api. + * + * @since 1.0.0 + * @category constructors + */ +export const make = (identifier: Id): HttpApi => + makeProto({ + identifier, + groups: new Map() as any, + errorSchema: HttpApiDecodeError, + annotations: Context.empty(), + middlewares: new Set() + }) + +/** + * Extract metadata from an `HttpApi`, which can be used to generate documentation + * or other tooling. + * + * See the `OpenApi` & `HttpApiClient` modules for examples of how to use this function. + * + * @since 1.0.0 + * @category reflection + */ +export const reflect = ( + self: HttpApi, + options: { + readonly predicate?: Predicate.Predicate<{ + readonly endpoint: HttpApiEndpoint.HttpApiEndpoint.AnyWithProps + readonly group: HttpApiGroup.HttpApiGroup.AnyWithProps + }> + readonly onGroup: (options: { + readonly group: HttpApiGroup.HttpApiGroup.AnyWithProps + readonly mergedAnnotations: Context.Context + }) => void + readonly onEndpoint: (options: { + readonly group: HttpApiGroup.HttpApiGroup.AnyWithProps + readonly endpoint: HttpApiEndpoint.HttpApiEndpoint + readonly mergedAnnotations: Context.Context + readonly middleware: ReadonlySet + readonly payloads: ReadonlyMap + readonly successes: ReadonlyMap + readonly description: Option.Option + }> + readonly errors: ReadonlyMap + readonly description: Option.Option + }> + }) => void + } +) => { + const apiErrors = extractMembers(self.errorSchema.ast, new Map(), HttpApiSchema.getStatusErrorAST) + const groups = Object.values(self.groups) as any as Array + for (const group of groups) { + const groupErrors = extractMembers(group.errorSchema.ast, apiErrors, HttpApiSchema.getStatusErrorAST) + const groupAnnotations = Context.merge(self.annotations, group.annotations) + options.onGroup({ + group, + mergedAnnotations: groupAnnotations + }) + const endpoints = Object.values(group.endpoints) as Iterable> + for (const endpoint of endpoints) { + if ( + options.predicate && !options.predicate({ + endpoint, + group + } as any) + ) continue + + const errors = extractMembers(endpoint.errorSchema.ast, groupErrors, HttpApiSchema.getStatusErrorAST) + options.onEndpoint({ + group, + endpoint, + middleware: new Set([...group.middlewares, ...endpoint.middlewares]), + mergedAnnotations: Context.merge(groupAnnotations, endpoint.annotations), + payloads: endpoint.payloadSchema._tag === "Some" ? extractPayloads(endpoint.payloadSchema.value.ast) : emptyMap, + successes: extractMembers(endpoint.successSchema.ast, new Map(), HttpApiSchema.getStatusSuccessAST), + errors + }) + } + } +} + +// ------------------------------------------------------------------------------------- + +const emptyMap = new Map() + +const extractMembers = ( + ast: AST.AST, + inherited: ReadonlyMap + readonly description: Option.Option + }>, + getStatus: (ast: AST.AST) => number +): ReadonlyMap + readonly description: Option.Option +}> => { + const members = new Map(inherited) + function process(type: AST.AST) { + if (AST.isNeverKeyword(type)) { + return + } + const annotations = HttpApiSchema.extractAnnotations(ast.annotations) + // Avoid changing the reference unless necessary + // Otherwise, deduplication of the ASTs below will not be possible + if (!Record.isEmptyRecord(annotations)) { + type = AST.annotations(type, { + ...annotations, + ...type.annotations + }) + } + const status = getStatus(type) + const emptyDecodeable = HttpApiSchema.getEmptyDecodeable(type) + const current = members.get(status) + members.set( + status, + { + description: (current ? current.description : Option.none()).pipe( + Option.orElse(() => getDescriptionOrIdentifier(type)) + ), + ast: (current ? current.ast : Option.none()).pipe( + // Deduplicate the ASTs + Option.map((current) => HttpApiSchema.UnionUnifyAST(current, type)), + Option.orElse(() => + !emptyDecodeable && AST.isVoidKeyword(AST.encodedAST(type)) ? Option.none() : Option.some(type) + ) + ) + } + ) + } + + HttpApiSchema.extractUnionTypes(ast).forEach(process) + return members +} + +const extractPayloads = (topAst: AST.AST): ReadonlyMap => { + const members = new Map() + function process(ast: AST.AST) { + if (ast._tag === "NeverKeyword") { + return + } + ast = AST.annotations(ast, { + ...HttpApiSchema.extractAnnotations(topAst.annotations), + ...ast.annotations + }) + const encoding = HttpApiSchema.getEncoding(ast) + const contentType = HttpApiSchema.getMultipart(ast) || HttpApiSchema.getMultipartStream(ast) + ? "multipart/form-data" + : encoding.contentType + const current = members.get(contentType) + if (current === undefined) { + members.set(contentType, { + encoding, + ast + }) + } else { + current.ast = AST.Union.make([current.ast, ast]) + } + } + if (topAst._tag === "Union") { + for (const type of topAst.types) { + process(type) + } + } else { + process(topAst) + } + return members +} + +const getDescriptionOrIdentifier = (ast: AST.PropertySignature | AST.AST): Option.Option => { + const annotations = "to" in ast ? + { + ...ast.to.annotations, + ...ast.annotations + } : + ast.annotations + return Option.fromNullable(annotations[AST.DescriptionAnnotationId] ?? annotations[AST.IdentifierAnnotationId] as any) +} + +/** + * Adds additional schemas to components/schemas. + * The provided schemas must have a `identifier` annotation. + * + * @since 1.0.0 + * @category tags + */ +export class AdditionalSchemas extends Context.Tag("@effect/platform/HttpApi/AdditionalSchemas")< + AdditionalSchemas, + ReadonlyArray +>() {} diff --git a/repos/effect/packages/platform/src/HttpApiBuilder.ts b/repos/effect/packages/platform/src/HttpApiBuilder.ts new file mode 100644 index 0000000..6cf5685 --- /dev/null +++ b/repos/effect/packages/platform/src/HttpApiBuilder.ts @@ -0,0 +1,1095 @@ +/** + * @since 1.0.0 + */ +import * as Cause from "effect/Cause" +import * as Chunk from "effect/Chunk" +import * as Context from "effect/Context" +import * as Effect from "effect/Effect" +import * as Encoding from "effect/Encoding" +import * as Fiber from "effect/Fiber" +import { constFalse, identity } from "effect/Function" +import { globalValue } from "effect/GlobalValue" +import * as Layer from "effect/Layer" +import * as Option from "effect/Option" +import * as ParseResult from "effect/ParseResult" +import { type Pipeable, pipeArguments } from "effect/Pipeable" +import type * as Predicate from "effect/Predicate" +import type { ReadonlyRecord } from "effect/Record" +import * as Redacted from "effect/Redacted" +import * as Schema from "effect/Schema" +import type * as AST from "effect/SchemaAST" +import type { Scope } from "effect/Scope" +import type { Covariant, NoInfer } from "effect/Types" +import { unify } from "effect/Unify" +import type { Cookie } from "./Cookies.js" +import type { FileSystem } from "./FileSystem.js" +import * as HttpApi from "./HttpApi.js" +import type * as HttpApiEndpoint from "./HttpApiEndpoint.js" +import { HttpApiDecodeError } from "./HttpApiError.js" +import type * as HttpApiGroup from "./HttpApiGroup.js" +import * as HttpApiMiddleware from "./HttpApiMiddleware.js" +import * as HttpApiSchema from "./HttpApiSchema.js" +import type * as HttpApiSecurity from "./HttpApiSecurity.js" +import * as HttpApp from "./HttpApp.js" +import * as HttpMethod from "./HttpMethod.js" +import * as HttpMiddleware from "./HttpMiddleware.js" +import * as HttpRouter from "./HttpRouter.js" +import * as HttpServer from "./HttpServer.js" +import * as HttpServerRequest from "./HttpServerRequest.js" +import * as HttpServerResponse from "./HttpServerResponse.js" +import * as Multipart from "./Multipart.js" +import * as OpenApi from "./OpenApi.js" +import type { Path } from "./Path.js" +import * as UrlParams from "./UrlParams.js" + +/** + * The router that the API endpoints are attached to. + * + * @since 1.0.0 + * @category router + */ +export class Router extends HttpRouter.Tag("@effect/platform/HttpApiBuilder/Router")() {} + +/** + * Create a top-level `HttpApi` layer. + * + * @since 1.0.0 + * @category constructors + */ +export const api = ( + api: HttpApi.HttpApi +): Layer.Layer< + HttpApi.Api, + never, + HttpApiGroup.HttpApiGroup.ToService | R | HttpApiGroup.HttpApiGroup.ErrorContext +> => + Layer.effect( + HttpApi.Api, + Effect.map(Effect.context(), (context) => ({ api: api as any, context })) + ) + +/** + * Build an `HttpApp` from an `HttpApi` instance, and serve it using an + * `HttpServer`. + * + * Optionally, you can provide a middleware function that will be applied to + * the `HttpApp` before serving. + * + * @since 1.0.0 + * @category constructors + */ +export const serve = ( + middleware?: (httpApp: HttpApp.Default) => HttpApp.Default +): Layer.Layer< + never, + never, + | HttpServer.HttpServer + | HttpRouter.HttpRouter.DefaultServices + | Exclude + | HttpApi.Api +> => + httpApp.pipe( + Effect.map((app) => HttpServer.serve(app as any, middleware!)), + Layer.unwrapEffect, + Layer.provide([Router.Live, Middleware.layer]) + ) + +/** + * Construct an `HttpApp` from an `HttpApi` instance. + * + * @since 1.0.0 + * @category constructors + */ +export const httpApp: Effect.Effect< + HttpApp.Default, + never, + Router | HttpApi.Api | Middleware +> = Effect.gen(function*() { + const { api, context } = yield* HttpApi.Api + const middleware = makeMiddlewareMap(api.middlewares, context) + const router = applyMiddleware(middleware, yield* HttpRouter.toHttpApp(yield* Router.router)) + const apiMiddlewareService = yield* Middleware + const apiMiddleware = yield* apiMiddlewareService.retrieve + const errorSchema = makeErrorSchema(api as any) + const encodeError = Schema.encodeUnknown(errorSchema) + return router.pipe( + apiMiddleware, + Effect.catchAllCause((cause) => + Effect.matchEffect(Effect.provide(encodeError(Cause.squash(cause)), context), { + onFailure: () => Effect.failCause(cause), + onSuccess: Effect.succeed + }) + ) + ) as any +}) + +/** + * @since 1.0.0 + * @category constructors + */ +export const buildMiddleware: ( + api: HttpApi.HttpApi +) => Effect.Effect< + ( + effect: Effect.Effect + ) => Effect.Effect +> = Effect.fnUntraced( + function*( + api: HttpApi.HttpApi + ) { + const context = yield* Effect.context() + const middlewareMap = makeMiddlewareMap(api.middlewares, context) + const errorSchema = makeErrorSchema(api as any) + const encodeError = Schema.encodeUnknown(errorSchema) + return (effect: Effect.Effect) => + Effect.catchAllCause( + applyMiddleware(middlewareMap, effect), + (cause) => + Effect.matchEffect(Effect.provide(encodeError(Cause.squash(cause)), context), { + onFailure: () => Effect.failCause(cause), + onSuccess: Effect.succeed + }) + ) + } +) + +/** + * Construct an http web handler from an `HttpApi` instance. + * + * **Example** + * + * ```ts + * import { HttpApi, HttpApiBuilder, HttpServer } from "@effect/platform" + * import { Layer } from "effect" + * + * class MyApi extends HttpApi.make("api") {} + * + * const MyApiLive = HttpApiBuilder.api(MyApi) + * + * const { dispose, handler } = HttpApiBuilder.toWebHandler( + * Layer.mergeAll( + * MyApiLive, + * // you could also use NodeHttpServer.layerContext, depending on your + * // server's platform + * HttpServer.layerContext + * ) + * ) + * ``` + * + * @since 1.0.0 + * @category constructors + */ +export const toWebHandler = ( + layer: Layer.Layer, + options?: { + readonly middleware?: ( + httpApp: HttpApp.Default + ) => HttpApp.Default< + never, + HttpApi.Api | Router | HttpRouter.HttpRouter.DefaultServices + > + readonly memoMap?: Layer.MemoMap + } +): { + readonly handler: (request: Request, context?: Context.Context | undefined) => Promise + readonly dispose: () => Promise +} => { + const layerMerged = Layer.mergeAll(layer, Router.Live, Middleware.layer) + return HttpApp.toWebHandlerLayerWith(layerMerged, { + memoMap: options?.memoMap, + middleware: options?.middleware as any, + toHandler: (r) => Effect.provide(httpApp, r) + }) +} + +/** + * @since 1.0.0 + * @category handlers + */ +export const HandlersTypeId: unique symbol = Symbol.for("@effect/platform/HttpApiBuilder/Handlers") + +/** + * @since 1.0.0 + * @category handlers + */ +export type HandlersTypeId = typeof HandlersTypeId + +/** + * Represents a handled `HttpApi`. + * + * @since 1.0.0 + * @category handlers + */ +export interface Handlers< + E, + Provides, + R, + Endpoints extends HttpApiEndpoint.HttpApiEndpoint.Any = never +> extends Pipeable { + readonly [HandlersTypeId]: { + _Endpoints: Covariant + } + readonly group: HttpApiGroup.HttpApiGroup.AnyWithProps + readonly handlers: Chunk.Chunk> + + /** + * Add the implementation for an `HttpApiEndpoint` to a `Handlers` group. + */ + handle, R1>( + name: Name, + handler: HttpApiEndpoint.HttpApiEndpoint.HandlerWithName, + options?: { readonly uninterruptible?: boolean | undefined } | undefined + ): Handlers< + E, + Provides, + | R + | Exclude< + HttpApiEndpoint.HttpApiEndpoint.ExcludeProvided< + Endpoints, + Name, + R1 | HttpApiEndpoint.HttpApiEndpoint.ContextWithName + >, + Provides + >, + HttpApiEndpoint.HttpApiEndpoint.ExcludeName + > + + /** + * Add the implementation for an `HttpApiEndpoint` to a `Handlers` group. + * This version of the api allows you to return the full response object. + */ + handleRaw, R1>( + name: Name, + handler: HttpApiEndpoint.HttpApiEndpoint.HandlerRawWithName, + options?: { readonly uninterruptible?: boolean | undefined } | undefined + ): Handlers< + E, + Provides, + | R + | Exclude< + HttpApiEndpoint.HttpApiEndpoint.ExcludeProvided< + Endpoints, + Name, + R1 | HttpApiEndpoint.HttpApiEndpoint.ContextWithName + >, + Provides + >, + HttpApiEndpoint.HttpApiEndpoint.ExcludeName + > +} + +/** + * @since 1.0.0 + * @category handlers + */ +export declare namespace Handlers { + /** + * @since 1.0.0 + * @category handlers + */ + export interface Any { + readonly [HandlersTypeId]: any + } + + /** + * @since 1.0.0 + * @category handlers + */ + export type Middleware = (self: HttpRouter.Route.Middleware) => HttpApp.Default + + /** + * @since 1.0.0 + * @category handlers + */ + export type Item = { + readonly endpoint: HttpApiEndpoint.HttpApiEndpoint.Any + readonly handler: HttpApiEndpoint.HttpApiEndpoint.Handler + readonly withFullRequest: boolean + readonly uninterruptible: boolean + } + + /** + * @since 1.0.0 + * @category handlers + */ + export type FromGroup< + ApiError, + ApiR, + Group extends HttpApiGroup.HttpApiGroup.Any + > = Handlers< + | ApiError + | HttpApiGroup.HttpApiGroup.Error, + | HttpApiMiddleware.HttpApiMiddleware.ExtractProvides + | HttpApiGroup.HttpApiGroup.Provides, + never, + HttpApiGroup.HttpApiGroup.Endpoints + > + + /** + * @since 1.0.0 + * @category handlers + */ + export type ValidateReturn = A extends ( + | Handlers< + infer _E, + infer _Provides, + infer _R, + infer _Endpoints + > + | Effect.Effect< + Handlers< + infer _E, + infer _Provides, + infer _R, + infer _Endpoints + >, + infer _EX, + infer _RX + > + ) ? [_Endpoints] extends [never] ? A + : `Endpoint not handled: ${HttpApiEndpoint.HttpApiEndpoint.Name<_Endpoints>}` : + `Must return the implemented handlers` + + /** + * @since 1.0.0 + * @category handlers + */ + export type Error = A extends Effect.Effect< + Handlers< + infer _E, + infer _Provides, + infer _R, + infer _Endpoints + >, + infer _EX, + infer _RX + > ? _EX : + never + + /** + * @since 1.0.0 + * @category handlers + */ + export type Context = A extends Handlers< + infer _E, + infer _Provides, + infer _R, + infer _Endpoints + > ? _R : + A extends Effect.Effect< + Handlers< + infer _E, + infer _Provides, + infer _R, + infer _Endpoints + >, + infer _EX, + infer _RX + > ? _R | _RX : + never +} + +const HandlersProto = { + [HandlersTypeId]: { + _Endpoints: identity + }, + pipe() { + return pipeArguments(this, arguments) + }, + handle( + this: Handlers, + name: string, + handler: HttpApiEndpoint.HttpApiEndpoint.Handler, + options?: { readonly uninterruptible?: boolean | undefined } | undefined + ) { + const endpoint = this.group.endpoints[name] + return makeHandlers({ + group: this.group, + handlers: Chunk.append(this.handlers, { + endpoint, + handler, + withFullRequest: false, + uninterruptible: options?.uninterruptible ?? false + }) as any + }) + }, + handleRaw( + this: Handlers, + name: string, + handler: HttpApiEndpoint.HttpApiEndpoint.Handler, + options?: { readonly uninterruptible?: boolean | undefined } | undefined + ) { + const endpoint = this.group.endpoints[name] + return makeHandlers({ + group: this.group, + handlers: Chunk.append(this.handlers, { + endpoint, + handler, + withFullRequest: true, + uninterruptible: options?.uninterruptible ?? false + }) as any + }) + } +} + +const makeHandlers = ( + options: { + readonly group: HttpApiGroup.HttpApiGroup.Any + readonly handlers: Chunk.Chunk> + } +): Handlers => { + const self = Object.create(HandlersProto) + self.group = options.group + self.handlers = options.handlers + return self +} + +/** + * Create a `Layer` that will implement all the endpoints in an `HttpApi`. + * + * An unimplemented `Handlers` instance is passed to the `build` function, which + * you can use to add handlers to the group. + * + * You can implement endpoints using the `handlers.handle` api. + * + * @since 1.0.0 + * @category handlers + */ +export const group = < + ApiId extends string, + Groups extends HttpApiGroup.HttpApiGroup.Any, + ApiError, + ApiR, + const Name extends HttpApiGroup.HttpApiGroup.Name, + Return +>( + api: HttpApi.HttpApi, + groupName: Name, + build: ( + handlers: Handlers.FromGroup> + ) => Handlers.ValidateReturn +): Layer.Layer< + HttpApiGroup.ApiGroup, + Handlers.Error, + Exclude< + | Handlers.Context + | HttpApiGroup.HttpApiGroup.MiddlewareWithName, + Scope + > +> => + Router.use((router) => + Effect.gen(function*() { + const context = yield* Effect.context() + const group = api.groups[groupName]! + const result = build(makeHandlers({ group, handlers: Chunk.empty() })) + const handlers: Handlers = Effect.isEffect(result) + ? (yield* result as Effect.Effect) + : result + const groupMiddleware = makeMiddlewareMap((group as any).middlewares, context) + const routes: Array> = [] + for (const item of handlers.handlers) { + const middleware = makeMiddlewareMap((item as any).endpoint.middlewares, context, groupMiddleware) + routes.push(handlerToRoute( + item.endpoint, + middleware, + function(request) { + return Effect.mapInputContext( + item.handler(request), + (input) => Context.merge(context, input) + ) + }, + item.withFullRequest, + item.uninterruptible + )) + } + yield* router.concat(HttpRouter.fromIterable(routes)) + }) + ) as any + +/** + * Create a `Handler` for a single endpoint. + * + * @since 1.0.0 + * @category handlers + */ +export const handler = < + ApiId extends string, + Groups extends HttpApiGroup.HttpApiGroup.Any, + ApiError, + ApiR, + const GroupName extends Groups["identifier"], + const Name extends HttpApiGroup.HttpApiGroup.EndpointsWithName["name"], + R +>( + _api: HttpApi.HttpApi, + _groupName: GroupName, + _name: Name, + f: HttpApiEndpoint.HttpApiEndpoint.HandlerWithName< + HttpApiGroup.HttpApiGroup.EndpointsWithName, + Name, + | ApiError + | HttpApiGroup.HttpApiGroup.ErrorWithName, + R + > +): HttpApiEndpoint.HttpApiEndpoint.HandlerWithName< + HttpApiGroup.HttpApiGroup.EndpointsWithName, + Name, + | ApiError + | HttpApiGroup.HttpApiGroup.ErrorWithName, + R +> => f + +// internal + +const requestPayload = ( + request: HttpServerRequest.HttpServerRequest, + urlParams: ReadonlyRecord>, + multipartLimits: Option.Option +): Effect.Effect< + unknown, + never, + | FileSystem + | Path + | Scope +> => { + if (!HttpMethod.hasBody(request.method)) { + return Effect.succeed(urlParams) + } + const contentType = request.headers["content-type"] + ? request.headers["content-type"].toLowerCase().trim() + : "application/json" + if (contentType.includes("application/json")) { + return Effect.orDie(request.json) + } else if (contentType.includes("multipart/form-data")) { + return Effect.orDie(Option.match(multipartLimits, { + onNone: () => request.multipart, + onSome: (limits) => Multipart.withLimits(request.multipart, limits) + })) + } else if (contentType.includes("x-www-form-urlencoded")) { + return Effect.map(Effect.orDie(request.urlParamsBody), UrlParams.toRecord) + } else if (contentType.startsWith("text/")) { + return Effect.orDie(request.text) + } + return Effect.map(Effect.orDie(request.arrayBuffer), (buffer) => new Uint8Array(buffer)) +} + +type MiddlewareMap = Map +}> + +const makeMiddlewareMap = ( + middleware: ReadonlySet, + context: Context.Context, + initial?: MiddlewareMap +): MiddlewareMap => { + const map = new Map + }>(initial) + middleware.forEach((tag) => { + map.set(tag.key, { + tag, + effect: Context.unsafeGet(context, tag as any) + }) + }) + return map +} + +function isSingleStringType(ast: AST.AST, key?: PropertyKey): boolean { + switch (ast._tag) { + case "StringKeyword": + case "Literal": + case "TemplateLiteral": + case "Enums": + return true + case "TypeLiteral": { + if (key !== undefined) { + const ps = ast.propertySignatures.find((ps) => ps.name === key) + return ps !== undefined + ? isSingleStringType(ps.type, key) + : ast.indexSignatures.some((is) => Schema.is(Schema.make(is.parameter))(key) && isSingleStringType(is.type)) + } + return false + } + case "Union": + return ast.types.some((type) => isSingleStringType(type, key)) + case "Suspend": + return isSingleStringType(ast.f(), key) + case "Refinement": + case "Transformation": + return isSingleStringType(ast.from, key) + } + return false +} + +/** + * Normalizes the url parameters so that if a key is expected to be an array, + * a single string value is wrapped in an array. + * + * @internal + */ +export function normalizeUrlParams( + params: ReadonlyRecord>, + ast: AST.AST +): ReadonlyRecord> { + const out: Record> = {} + for (const key in params) { + const value = params[key] + out[key] = Array.isArray(value) || isSingleStringType(ast, key) ? value : [value] + } + return out +} + +const handlerToRoute = ( + endpoint_: HttpApiEndpoint.HttpApiEndpoint.Any, + middleware: MiddlewareMap, + handler: HttpApiEndpoint.HttpApiEndpoint.Handler, + isFullRequest: boolean, + uninterruptible: boolean +): HttpRouter.Route => { + const endpoint = endpoint_ as HttpApiEndpoint.HttpApiEndpoint.AnyWithProps + const isMultipartStream = endpoint.payloadSchema.pipe( + Option.map(({ ast }) => HttpApiSchema.getMultipartStream(ast) !== undefined), + Option.getOrElse(constFalse) + ) + const multipartLimits = endpoint.payloadSchema.pipe( + Option.flatMapNullable(({ ast }) => HttpApiSchema.getMultipart(ast) || HttpApiSchema.getMultipartStream(ast)) + ) + const decodePath = Option.map(endpoint.pathSchema, Schema.decodeUnknown) + const decodePayload = isFullRequest || isMultipartStream + ? Option.none() + : Option.map(endpoint.payloadSchema, Schema.decodeUnknown) + const decodeHeaders = Option.map(endpoint.headersSchema, Schema.decodeUnknown) + const encodeSuccess = Schema.encode(makeSuccessSchema(endpoint.successSchema)) + return HttpRouter.makeRoute( + endpoint.method, + endpoint.path, + applyMiddleware( + middleware, + Effect.gen(function*() { + const fiber = Option.getOrThrow(Fiber.getCurrentFiber()) + const context = fiber.currentContext + const httpRequest = Context.unsafeGet(context, HttpServerRequest.HttpServerRequest) + const routeContext = Context.unsafeGet(context, HttpRouter.RouteContext) + const urlParams = Context.unsafeGet(context, HttpServerRequest.ParsedSearchParams) + const request: any = { request: httpRequest } + if (decodePath._tag === "Some") { + request.path = yield* decodePath.value(routeContext.params) + } + if (decodePayload._tag === "Some") { + request.payload = yield* Effect.flatMap( + requestPayload(httpRequest, urlParams, multipartLimits), + decodePayload.value + ) + } else if (isMultipartStream) { + request.payload = Option.match(multipartLimits, { + onNone: () => httpRequest.multipartStream, + onSome: (limits) => Multipart.withLimitsStream(httpRequest.multipartStream, limits) + }) + } + if (decodeHeaders._tag === "Some") { + request.headers = yield* decodeHeaders.value(httpRequest.headers) + } + if (endpoint.urlParamsSchema._tag === "Some") { + const schema = endpoint.urlParamsSchema.value + request.urlParams = yield* Schema.decodeUnknown(schema)(normalizeUrlParams(urlParams, schema.ast)) + } + const response = yield* handler(request) + return HttpServerResponse.isServerResponse(response) ? response : yield* encodeSuccess(response) + }).pipe( + Effect.catchIf(ParseResult.isParseError, HttpApiDecodeError.refailParseError) + ) + ), + { uninterruptible } + ) +} + +const applyMiddleware = >( + middleware: MiddlewareMap, + handler: A +) => { + for (const entry of middleware.values()) { + const effect = HttpApiMiddleware.SecurityTypeId in entry.tag ? makeSecurityMiddleware(entry as any) : entry.effect + if (entry.tag.optional) { + const previous = handler + handler = Effect.matchEffect(effect, { + onFailure: () => previous, + onSuccess: entry.tag.provides !== undefined + ? (value) => Effect.provideService(previous, entry.tag.provides as any, value) + : (_) => previous + }) as any + } else { + handler = entry.tag.provides !== undefined + ? Effect.provideServiceEffect(handler, entry.tag.provides as any, effect) as any + : Effect.zipRight(effect, handler) as any + } + } + return handler +} + +const securityMiddlewareCache = globalValue>>( + "securityMiddlewareCache", + () => new WeakMap() +) + +const makeSecurityMiddleware = ( + entry: { + readonly tag: HttpApiMiddleware.TagClassSecurityAny + readonly effect: Record Effect.Effect> + } +): Effect.Effect => { + if (securityMiddlewareCache.has(entry)) { + return securityMiddlewareCache.get(entry)! + } + + let effect: Effect.Effect | undefined + for (const [key, security] of Object.entries(entry.tag.security)) { + const decode = securityDecode(security) + const handler = entry.effect[key] + const middleware = Effect.flatMap(decode, handler) + effect = effect === undefined ? middleware : Effect.catchAll(effect, () => middleware) + } + if (effect === undefined) { + effect = Effect.void + } + securityMiddlewareCache.set(entry, effect) + return effect +} + +const responseSchema = Schema.declare(HttpServerResponse.isServerResponse) + +const makeSuccessSchema = ( + schema: Schema.Schema.Any +): Schema.Schema => { + const schemas = new Set() + HttpApiSchema.deunionize(schemas, schema) + return Schema.Union(...Array.from(schemas, toResponseSuccess)) as any +} + +const makeErrorSchema = ( + api: HttpApi.HttpApi.AnyWithProps +): Schema.Schema => { + const schemas = new Set() + HttpApiSchema.deunionize(schemas, api.errorSchema) + for (const group of Object.values(api.groups)) { + for (const endpoint of Object.values(group.endpoints)) { + HttpApiSchema.deunionize(schemas, endpoint.errorSchema) + } + HttpApiSchema.deunionize(schemas, group.errorSchema) + } + return Schema.Union(...Array.from(schemas, toResponseError)) as any +} + +const decodeForbidden = (_: A, __: AST.ParseOptions, ast: AST.Transformation) => + ParseResult.fail(new ParseResult.Forbidden(ast, _, "Encode only schema")) + +const toResponseSchema = (getStatus: (ast: AST.AST) => number) => { + const cache = new WeakMap() + const schemaToResponse = ( + data: any, + _: AST.ParseOptions, + ast: AST.Transformation + ): Effect.Effect => { + const isEmpty = HttpApiSchema.isVoid(ast.to) + const status = getStatus(ast.to) + if (isEmpty) { + return HttpServerResponse.empty({ status }) + } + const encoding = HttpApiSchema.getEncoding(ast.to) + switch (encoding.kind) { + case "Json": { + return Effect.mapError( + HttpServerResponse.json(data, { + status, + contentType: encoding.contentType + }), + (error) => new ParseResult.Type(ast, error, "Could not encode to JSON") + ) + } + case "Text": { + return ParseResult.succeed(HttpServerResponse.text(data as any, { + status, + contentType: encoding.contentType + })) + } + case "Uint8Array": { + return ParseResult.succeed(HttpServerResponse.uint8Array(data as any, { + status, + contentType: encoding.contentType + })) + } + case "UrlParams": { + return ParseResult.succeed(HttpServerResponse.urlParams(data as any, { + status, + contentType: encoding.contentType + })) + } + } + } + return (schema: Schema.Schema): Schema.Schema => { + if (cache.has(schema.ast)) { + return cache.get(schema.ast)! as any + } + const transform = Schema.transformOrFail(responseSchema, schema, { + decode: decodeForbidden, + encode: schemaToResponse + }) + cache.set(transform.ast, transform) + return transform + } +} + +const toResponseSuccess = toResponseSchema(HttpApiSchema.getStatusSuccessAST) +const toResponseError = toResponseSchema(HttpApiSchema.getStatusErrorAST) + +// ---------------------------------------------------------------------------- +// Global middleware +// ---------------------------------------------------------------------------- + +/** + * @since 1.0.0 + * @category middleware + */ +export class Middleware extends Context.Tag("@effect/platform/HttpApiBuilder/Middleware")< + Middleware, + { + readonly add: (middleware: HttpMiddleware.HttpMiddleware) => Effect.Effect + readonly retrieve: Effect.Effect + } +>() { + /** + * @since 1.0.0 + */ + static readonly layer = Layer.sync(Middleware, () => { + let middleware: HttpMiddleware.HttpMiddleware = identity + return Middleware.of({ + add: (f) => + Effect.sync(() => { + const prev = middleware + middleware = (app) => f(prev(app)) + }), + retrieve: Effect.sync(() => middleware) + }) + }) +} + +/** + * @since 1.0.0 + * @category global + */ +export type MiddlewareFn = ( + httpApp: HttpApp.Default +) => HttpApp.Default + +const middlewareAdd = ( + middleware: HttpMiddleware.HttpMiddleware +): Effect.Effect => + Effect.gen(function*() { + const context = yield* Effect.context() + const service = yield* Middleware + yield* service.add((httpApp) => + Effect.mapInputContext(middleware(httpApp), (input) => Context.merge(context, input)) + ) + }) + +const middlewareAddNoContext = ( + middleware: HttpMiddleware.HttpMiddleware +): Effect.Effect => + Effect.gen(function*() { + const service = yield* Middleware + yield* service.add(middleware) + }) + +/** + * Create an `HttpApi` level middleware `Layer`. + * + * @since 1.0.0 + * @category middleware + */ +export const middleware: { + ( + middleware: MiddlewareFn | Effect.Effect, EX, RX>, + options?: { + readonly withContext?: false | undefined + } + ): Layer.Layer> + ( + middleware: MiddlewareFn | Effect.Effect, EX, RX>, + options: { + readonly withContext: true + } + ): Layer.Layer | RX, Scope>> + ( + api: HttpApi.HttpApi, + middleware: MiddlewareFn> | Effect.Effect>, EX, RX>, + options?: { + readonly withContext?: false | undefined + } + ): Layer.Layer> + ( + api: HttpApi.HttpApi, + middleware: MiddlewareFn, R> | Effect.Effect, R>, EX, RX>, + options: { + readonly withContext: true + } + ): Layer.Layer | RX, Scope>> +} = ( + ...args: [ + middleware: MiddlewareFn | Effect.Effect, any, any>, + options?: { + readonly withContext?: boolean | undefined + } | undefined + ] | [ + api: HttpApi.HttpApi.Any, + middleware: MiddlewareFn | Effect.Effect, any, any>, + options?: { + readonly withContext?: boolean | undefined + } | undefined + ] +): any => { + const apiFirst = HttpApi.isHttpApi(args[0]) + const withContext = apiFirst ? args[2]?.withContext === true : (args as any)[1]?.withContext === true + const add = withContext ? middlewareAdd : middlewareAddNoContext + const middleware = apiFirst ? args[1] : args[0] + return (Effect.isEffect(middleware) + ? Layer.scopedDiscard(Effect.flatMap(middleware as any, add)) + : Layer.scopedDiscard(add(middleware as any))).pipe(Layer.provide(Middleware.layer)) +} + +/** + * A CORS middleware layer that can be provided to the `HttpApiBuilder.serve` layer. + * + * @since 1.0.0 + * @category middleware + */ +export const middlewareCors = ( + options?: { + readonly allowedOrigins?: ReadonlyArray | Predicate.Predicate | undefined + readonly allowedMethods?: ReadonlyArray | undefined + readonly allowedHeaders?: ReadonlyArray | undefined + readonly exposedHeaders?: ReadonlyArray | undefined + readonly maxAge?: number | undefined + readonly credentials?: boolean | undefined + } | undefined +): Layer.Layer => middleware(HttpMiddleware.cors(options)) + +/** + * A middleware that adds an openapi.json endpoint to the API. + * + * @since 1.0.0 + * @category middleware + */ +export const middlewareOpenApi = ( + options?: { + readonly path?: HttpApiEndpoint.PathSegment | undefined + readonly additionalPropertiesStrategy?: OpenApi.AdditionalPropertiesStrategy | undefined + } | undefined +): Layer.Layer => + Router.use((router) => + Effect.gen(function*() { + const { api } = yield* HttpApi.Api + const spec = OpenApi.fromApi(api, { + additionalPropertiesStrategy: options?.additionalPropertiesStrategy + }) + const response = yield* HttpServerResponse.json(spec).pipe( + Effect.orDie + ) + yield* router.get(options?.path ?? "/openapi.json", Effect.succeed(response)) + }) + ) + +const bearerLen = `Bearer `.length +const basicLen = `Basic `.length + +/** + * @since 1.0.0 + * @category security + */ +export const securityDecode = ( + self: Security +): Effect.Effect< + HttpApiSecurity.HttpApiSecurity.Type, + never, + HttpServerRequest.HttpServerRequest | HttpServerRequest.ParsedSearchParams +> => { + switch (self._tag) { + case "Bearer": { + return Effect.map( + HttpServerRequest.HttpServerRequest, + (request) => Redacted.make((request.headers.authorization ?? "").slice(bearerLen)) as any + ) + } + case "ApiKey": { + const key = self.in === "header" ? self.key.toLowerCase() : self.key + const schema = Schema.Struct({ + [key]: Schema.String + }) + const decode = unify( + self.in === "query" + ? HttpServerRequest.schemaSearchParams(schema) + : self.in === "cookie" + ? HttpServerRequest.schemaCookies(schema) + : HttpServerRequest.schemaHeaders(schema) + ) + return Effect.match(decode, { + onFailure: () => Redacted.make("") as any, + onSuccess: (match) => Redacted.make(match[key]) + }) + } + case "Basic": { + const empty: HttpApiSecurity.HttpApiSecurity.Type = { + username: "", + password: Redacted.make("") + } as any + return HttpServerRequest.HttpServerRequest.pipe( + Effect.flatMap((request) => Encoding.decodeBase64String((request.headers.authorization ?? "").slice(basicLen))), + Effect.match({ + onFailure: () => empty, + onSuccess: (header) => { + const parts = header.split(":") + if (parts.length !== 2) { + return empty + } + return { + username: parts[0], + password: Redacted.make(parts[1]) + } as any + } + }) + ) + } + } +} + +/** + * Set a cookie from an `HttpApiSecurity.HttpApiKey` instance. + * + * You can use this api before returning a response from an endpoint handler. + * + * ```ts skip-type-checking + * handlers.handle( + * "authenticate", + * (_) => HttpApiBuilder.securitySetCookie(security, "secret123") + * ) + * ``` + * + * @since 1.0.0 + * @category middleware + */ +export const securitySetCookie = ( + self: HttpApiSecurity.ApiKey, + value: string | Redacted.Redacted, + options?: Cookie["options"] +): Effect.Effect => { + const stringValue = typeof value === "string" ? value : Redacted.value(value) + return HttpApp.appendPreResponseHandler((_req, response) => + Effect.orDie( + HttpServerResponse.setCookie(response, self.key, stringValue, { + secure: true, + httpOnly: true, + ...options + }) + ) + ) +} diff --git a/repos/effect/packages/platform/src/HttpApiClient.ts b/repos/effect/packages/platform/src/HttpApiClient.ts new file mode 100644 index 0000000..3c589a6 --- /dev/null +++ b/repos/effect/packages/platform/src/HttpApiClient.ts @@ -0,0 +1,578 @@ +/** + * @since 1.0.0 + */ +import * as Context from "effect/Context" +import * as Effect from "effect/Effect" +import { identity } from "effect/Function" +import { globalValue } from "effect/GlobalValue" +import * as Option from "effect/Option" +import * as ParseResult from "effect/ParseResult" +import type * as Predicate from "effect/Predicate" +import * as Schema from "effect/Schema" +import type * as AST from "effect/SchemaAST" +import type { Simplify } from "effect/Types" +import * as HttpApi from "./HttpApi.js" +import type { HttpApiEndpoint } from "./HttpApiEndpoint.js" +import type { HttpApiGroup } from "./HttpApiGroup.js" +import type * as HttpApiMiddleware from "./HttpApiMiddleware.js" +import * as HttpApiSchema from "./HttpApiSchema.js" +import * as HttpBody from "./HttpBody.js" +import * as HttpClient from "./HttpClient.js" +import * as HttpClientError from "./HttpClientError.js" +import * as HttpClientRequest from "./HttpClientRequest.js" +import * as HttpClientResponse from "./HttpClientResponse.js" +import * as HttpMethod from "./HttpMethod.js" +import * as UrlParams from "./UrlParams.js" + +/** + * @since 1.0.0 + * @category models + */ +export type Client = Simplify< + & { + readonly [Group in Extract as HttpApiGroup.Name]: Client.Group< + Group, + Group["identifier"], + E, + R + > + } + & { + readonly [Method in Client.TopLevelMethods as Method[0]]: Method[1] + } +> + +/** + * @since 1.0.0 + * @category models + */ +export declare namespace Client { + /** + * @since 1.0.0 + * @category models + */ + export type Group = + [HttpApiGroup.WithName] extends + [HttpApiGroup] ? { + readonly [Endpoint in _Endpoints as HttpApiEndpoint.Name]: Method< + Endpoint, + E, + _GroupError, + R + > + } : + never + + /** + * @since 1.0.0 + * @category models + */ + export type Method = [Endpoint] extends [ + HttpApiEndpoint< + infer _Name, + infer _Method, + infer _Path, + infer _UrlParams, + infer _Payload, + infer _Headers, + infer _Success, + infer _Error, + infer _R, + infer _RE + > + ] ? ( + request: Simplify> + ) => Effect.Effect< + WithResponse extends true ? [_Success, HttpClientResponse.HttpClientResponse] : _Success, + _Error | GroupError | E | HttpClientError.HttpClientError | ParseResult.ParseError, + R + > : + never + + /** + * @since 1.0.0 + * @category models + */ + export type TopLevelMethods = + Extract extends + HttpApiGroup ? + _Endpoints extends infer Endpoint ? [HttpApiEndpoint.Name, Method] + : never : + never +} + +/** + * @internal + */ +const makeClient = ( + api: HttpApi.HttpApi, + options: { + readonly httpClient: HttpClient.HttpClient.With + readonly predicate?: Predicate.Predicate<{ + readonly endpoint: HttpApiEndpoint.AnyWithProps + readonly group: HttpApiGroup.AnyWithProps + }> + readonly onGroup?: (options: { + readonly group: HttpApiGroup.AnyWithProps + readonly mergedAnnotations: Context.Context + }) => void + readonly onEndpoint: (options: { + readonly group: HttpApiGroup.AnyWithProps + readonly endpoint: HttpApiEndpoint + readonly mergedAnnotations: Context.Context + readonly middleware: ReadonlySet + readonly successes: ReadonlyMap + readonly description: Option.Option + }> + readonly errors: ReadonlyMap + readonly description: Option.Option + }> + readonly endpointFn: Function + }) => void + readonly transformResponse?: + | ((effect: Effect.Effect) => Effect.Effect) + | undefined + readonly baseUrl?: URL | string | undefined + } +): Effect.Effect< + void, + never, + HttpApiMiddleware.HttpApiMiddleware.Without> +> => + Effect.gen(function*() { + const context = yield* Effect.context() + const httpClient = options.httpClient.pipe( + options?.baseUrl === undefined + ? identity + : HttpClient.mapRequest( + HttpClientRequest.prependUrl(options.baseUrl.toString()) + ) + ) + HttpApi.reflect(api as any, { + predicate: options?.predicate, + onGroup(onGroupOptions) { + options.onGroup?.(onGroupOptions) + }, + onEndpoint(onEndpointOptions) { + const { endpoint, errors, successes } = onEndpointOptions + const makeUrl = compilePath(endpoint.path) + const decodeMap: Record< + number | "orElse", + (response: HttpClientResponse.HttpClientResponse) => Effect.Effect + > = { orElse: statusOrElse } + const decodeResponse = HttpClientResponse.matchStatus(decodeMap) + errors.forEach(({ ast }, status) => { + if (ast._tag === "None") { + decodeMap[status] = statusCodeError + return + } + const decode = schemaToResponse(ast.value) + decodeMap[status] = (response) => Effect.flatMap(decode(response), Effect.fail) + }) + successes.forEach(({ ast }, status) => { + decodeMap[status] = ast._tag === "None" ? responseAsVoid : schemaToResponse(ast.value) + }) + const encodePath = endpoint.pathSchema.pipe( + Option.map(Schema.encodeUnknown) + ) + const encodePayloadBody = endpoint.payloadSchema.pipe( + Option.map((schema) => { + if (HttpMethod.hasBody(endpoint.method)) { + return Schema.encodeUnknown(payloadSchemaBody(schema as any)) + } + return Schema.encodeUnknown(schema) + }) + ) + const encodeHeaders = endpoint.headersSchema.pipe( + Option.map(Schema.encodeUnknown) + ) + const encodeUrlParams = endpoint.urlParamsSchema.pipe( + Option.map(Schema.encodeUnknown) + ) + const endpointFn = Effect.fnUntraced(function*(request: { + readonly path: any + readonly urlParams: any + readonly payload: any + readonly headers: any + readonly withResponse?: boolean + }) { + let httpRequest = HttpClientRequest.make(endpoint.method)(endpoint.path) + if (request && request.path) { + const encodedPathParams = encodePath._tag === "Some" + ? yield* encodePath.value(request.path) + : request.path + httpRequest = HttpClientRequest.setUrl(httpRequest, makeUrl(encodedPathParams)) + } + if (request && request.payload instanceof FormData) { + httpRequest = HttpClientRequest.bodyFormData(httpRequest, request.payload) + } else if (encodePayloadBody._tag === "Some") { + if (HttpMethod.hasBody(endpoint.method)) { + const body = (yield* encodePayloadBody.value(request.payload)) as HttpBody.HttpBody + httpRequest = HttpClientRequest.setBody(httpRequest, body) + } else { + const urlParams = (yield* encodePayloadBody.value(request.payload)) as Record + httpRequest = HttpClientRequest.setUrlParams(httpRequest, urlParams) + } + } + if (encodeHeaders._tag === "Some") { + httpRequest = HttpClientRequest.setHeaders( + httpRequest, + (yield* encodeHeaders.value(request.headers)) as any + ) + } + if (encodeUrlParams._tag === "Some") { + httpRequest = HttpClientRequest.appendUrlParams( + httpRequest, + (yield* encodeUrlParams.value(request.urlParams)) as any + ) + } + const response = yield* httpClient.execute(httpRequest) + const value = yield* (options.transformResponse === undefined + ? decodeResponse(response) + : options.transformResponse(decodeResponse(response))) + return request?.withResponse === true ? [value, response] : value + }, Effect.mapInputContext((input) => Context.merge(context, input))) + + options.onEndpoint({ + ...onEndpointOptions, + endpointFn + }) + } + }) + }) + +/** + * @since 1.0.0 + * @category constructors + */ +export const make = ( + api: HttpApi.HttpApi, + options?: { + readonly transformClient?: ((client: HttpClient.HttpClient) => HttpClient.HttpClient) | undefined + readonly transformResponse?: + | ((effect: Effect.Effect) => Effect.Effect) + | undefined + readonly baseUrl?: URL | string | undefined + } +): Effect.Effect< + Simplify>, + never, + HttpApiMiddleware.HttpApiMiddleware.Without> | HttpClient.HttpClient +> => + Effect.flatMap(HttpClient.HttpClient, (httpClient) => + makeWith(api, { + ...options, + httpClient: options?.transformClient ? options.transformClient(httpClient) : httpClient + })) + +/** + * @since 1.0.0 + * @category constructors + */ +export const makeWith = ( + api: HttpApi.HttpApi, + options: { + readonly httpClient: HttpClient.HttpClient.With + readonly transformResponse?: + | ((effect: Effect.Effect) => Effect.Effect) + | undefined + readonly baseUrl?: URL | string | undefined + } +): Effect.Effect< + Simplify>, + never, + HttpApiMiddleware.HttpApiMiddleware.Without> +> => { + const client: Record> = {} + return makeClient(api, { + ...options, + onGroup({ group }) { + if (group.topLevel) return + client[group.identifier] = {} + }, + onEndpoint({ endpoint, endpointFn, group }) { + ;(group.topLevel ? client : client[group.identifier])[endpoint.name] = endpointFn + } + }).pipe(Effect.map(() => client)) as any +} + +/** + * @since 1.0.0 + * @category constructors + */ +export const group = < + ApiId extends string, + Groups extends HttpApiGroup.Any, + ApiError, + ApiR, + const GroupName extends HttpApiGroup.Name, + E, + R +>( + api: HttpApi.HttpApi, + options: { + readonly group: GroupName + readonly httpClient: HttpClient.HttpClient.With + readonly transformResponse?: + | ((effect: Effect.Effect) => Effect.Effect) + | undefined + readonly baseUrl?: URL | string | undefined + } +): Effect.Effect< + Client.Group, + never, + HttpApiMiddleware.HttpApiMiddleware.Without< + | ApiR + | HttpApiGroup.ClientContext< + HttpApiGroup.WithName + > + > +> => { + const client: Record = {} + return makeClient(api, { + ...options, + predicate: ({ group }) => group.identifier === options.group, + onEndpoint({ endpoint, endpointFn }) { + client[endpoint.name] = endpointFn + } + }).pipe(Effect.map(() => client)) as any +} + +/** + * @since 1.0.0 + * @category constructors + */ +export const endpoint = < + ApiId extends string, + Groups extends HttpApiGroup.Any, + ApiError, + ApiR, + const GroupName extends HttpApiGroup.Name, + const EndpointName extends HttpApiEndpoint.Name>, + E, + R +>( + api: HttpApi.HttpApi, + options: { + readonly group: GroupName + readonly endpoint: EndpointName + readonly httpClient: HttpClient.HttpClient.With + readonly transformClient?: ((client: HttpClient.HttpClient) => HttpClient.HttpClient) | undefined + readonly transformResponse?: + | ((effect: Effect.Effect) => Effect.Effect) + | undefined + readonly baseUrl?: URL | string | undefined + } +): Effect.Effect< + Client.Method< + HttpApiEndpoint.WithName>, EndpointName>, + HttpApiGroup.Error>, + ApiError | E, + R + >, + never, + HttpApiMiddleware.HttpApiMiddleware.Without< + | ApiR + | HttpApiGroup.Context> + | HttpApiEndpoint.ContextWithName, EndpointName> + | HttpApiEndpoint.ErrorContextWithName, EndpointName> + > +> => { + let client: any = undefined + return makeClient(api, { + ...options, + predicate: ({ endpoint, group }) => group.identifier === options.group && endpoint.name === options.endpoint, + onEndpoint({ endpointFn }) { + client = endpointFn + } + }).pipe(Effect.map(() => client)) as any +} + +// ---------------------------------------------------------------------------- + +const paramsRegex = /:(\w+)\??/g + +const compilePath = (path: string) => { + const segments = path.split(paramsRegex) + const len = segments.length + if (len === 1) { + return (_: any) => path + } + return (params: Record) => { + let url = segments[0] + for (let i = 1; i < len; i++) { + if (i % 2 === 0) { + url += segments[i] + } else { + url += params[segments[i]] + } + } + return url + } +} + +const schemaToResponse = ( + ast: AST.AST +): (response: HttpClientResponse.HttpClientResponse) => Effect.Effect => { + const encoding = HttpApiSchema.getEncoding(ast) + const decode = Schema.decode(schemaFromArrayBuffer(ast, encoding)) + return (response) => Effect.flatMap(response.arrayBuffer, decode) +} + +const Uint8ArrayFromArrayBuffer = Schema.transform( + Schema.Unknown as Schema.Schema, + Schema.Uint8ArrayFromSelf, + { + decode(fromA) { + return new Uint8Array(fromA) + }, + encode(arr) { + return arr.byteLength === arr.buffer.byteLength ? + arr.buffer : + arr.buffer.slice(arr.byteOffset, arr.byteOffset + arr.byteLength) + } + } +) + +const StringFromArrayBuffer = Schema.transform( + Schema.Unknown as Schema.Schema, + Schema.String, + { + decode(fromA) { + return new TextDecoder().decode(fromA) + }, + encode(toI) { + const arr = new TextEncoder().encode(toI) + return arr.byteLength === arr.buffer.byteLength ? + arr.buffer : + arr.buffer.slice(arr.byteOffset, arr.byteOffset + arr.byteLength) + } + } +) + +const parseJsonOrVoid = Schema.transformOrFail( + Schema.String, + Schema.Unknown, + { + strict: true, + decode: (i, _, ast) => { + if (i === "") return ParseResult.succeed(void 0) + return ParseResult.try({ + try: () => JSON.parse(i), + catch: () => new ParseResult.Type(ast, i, "Could not parse JSON") + }) + }, + encode: (a, _, ast) => { + if (a === undefined) return ParseResult.succeed("") + return ParseResult.try({ + try: () => JSON.stringify(a), + catch: () => new ParseResult.Type(ast, a, "Could not encode as JSON") + }) + } + } +) + +const parseJsonArrayBuffer = Schema.compose(StringFromArrayBuffer, parseJsonOrVoid) + +const schemaFromArrayBuffer = ( + ast: AST.AST, + encoding: HttpApiSchema.Encoding +): Schema.Schema => { + if (ast._tag === "Union") { + return Schema.Union(...ast.types.map((ast) => schemaFromArrayBuffer(ast, HttpApiSchema.getEncoding(ast, encoding)))) + } + const schema = Schema.make(ast) + switch (encoding.kind) { + case "Json": { + return Schema.compose(parseJsonArrayBuffer, schema) + } + case "UrlParams": { + return Schema.compose(StringFromArrayBuffer, UrlParams.schemaParse(schema as any)) as any + } + case "Uint8Array": { + return Schema.compose(Uint8ArrayFromArrayBuffer, schema) + } + case "Text": { + return Schema.compose(StringFromArrayBuffer, schema) + } + } +} + +const statusOrElse = (response: HttpClientResponse.HttpClientResponse) => + Effect.fail( + new HttpClientError.ResponseError({ + reason: "Decode", + request: response.request, + response + }) + ) + +const statusCodeError = (response: HttpClientResponse.HttpClientResponse) => + Effect.fail( + new HttpClientError.ResponseError({ + reason: "StatusCode", + request: response.request, + response + }) + ) + +const responseAsVoid = (_response: HttpClientResponse.HttpClientResponse) => Effect.void + +const HttpBodyFromSelf = Schema.declare(HttpBody.isHttpBody) + +const payloadSchemaBody = (schema: Schema.Schema.All): Schema.Schema => { + const members = schema.ast._tag === "Union" ? schema.ast.types : [schema.ast] + return Schema.Union(...members.map(bodyFromPayload)) as any +} + +const bodyFromPayloadCache = globalValue( + "@effect/platform/HttpApiClient/bodyFromPayloadCache", + () => new WeakMap() +) + +const bodyFromPayload = (ast: AST.AST) => { + if (bodyFromPayloadCache.has(ast)) { + return bodyFromPayloadCache.get(ast)! + } + const schema = Schema.make(ast) + const encoding = HttpApiSchema.getEncoding(ast) + const transform = Schema.transformOrFail( + HttpBodyFromSelf, + schema, + { + decode(fromA, _, ast) { + return ParseResult.fail(new ParseResult.Forbidden(ast, fromA, "encode only schema")) + }, + encode(toI, _, ast) { + switch (encoding.kind) { + case "Json": { + try { + return ParseResult.succeed(HttpBody.text(JSON.stringify(toI), encoding.contentType)) + } catch { + return ParseResult.fail(new ParseResult.Type(ast, toI, "Could not encode as JSON")) + } + } + case "Text": { + if (typeof toI !== "string") { + return ParseResult.fail(new ParseResult.Type(ast, toI, "Expected a string")) + } + return ParseResult.succeed(HttpBody.text(toI, encoding.contentType)) + } + case "UrlParams": { + return ParseResult.succeed(HttpBody.urlParams(UrlParams.fromInput(toI as any))) + } + case "Uint8Array": { + if (!(toI instanceof Uint8Array)) { + return ParseResult.fail(new ParseResult.Type(ast, toI, "Expected a Uint8Array")) + } + return ParseResult.succeed(HttpBody.uint8Array(toI, encoding.contentType)) + } + } + } + } + ) + bodyFromPayloadCache.set(ast, transform) + return transform +} diff --git a/repos/effect/packages/platform/src/HttpApiEndpoint.ts b/repos/effect/packages/platform/src/HttpApiEndpoint.ts new file mode 100644 index 0000000..7b383fd --- /dev/null +++ b/repos/effect/packages/platform/src/HttpApiEndpoint.ts @@ -0,0 +1,996 @@ +/** + * @since 1.0.0 + */ +import type { Brand } from "effect/Brand" +import * as Context from "effect/Context" +import type { Effect } from "effect/Effect" +import * as Option from "effect/Option" +import { type Pipeable, pipeArguments } from "effect/Pipeable" +import * as Predicate from "effect/Predicate" +import * as Schema from "effect/Schema" +import type * as Stream from "effect/Stream" +import type * as Types from "effect/Types" +import type * as HttpApiMiddleware from "./HttpApiMiddleware.js" +import * as HttpApiSchema from "./HttpApiSchema.js" +import type { HttpMethod } from "./HttpMethod.js" +import * as HttpRouter from "./HttpRouter.js" +import type { HttpServerRequest } from "./HttpServerRequest.js" +import type { HttpServerResponse } from "./HttpServerResponse.js" +import type * as Multipart from "./Multipart.js" + +/** + * @since 1.0.0 + * @category type ids + */ +export const TypeId: unique symbol = Symbol.for("@effect/platform/HttpApiEndpoint") + +/** + * @since 1.0.0 + * @category type ids + */ +export type TypeId = typeof TypeId + +/** + * @since 1.0.0 + * @category guards + */ +export const isHttpApiEndpoint = (u: unknown): u is HttpApiEndpoint => Predicate.hasProperty(u, TypeId) + +/** + * Represents a path segment. A path segment is a string that represents a + * segment of a URL path. + * + * @since 1.0.0 + * @category models + */ +export type PathSegment = `/${string}` + +/** + * Represents an API endpoint. An API endpoint is mapped to a single route on + * the underlying `HttpRouter`. + * + * @since 1.0.0 + * @category models + */ +export interface HttpApiEndpoint< + out Name extends string, + out Method extends HttpMethod, + in out Path = never, + in out UrlParams = never, + in out Payload = never, + in out Headers = never, + in out Success = void, + in out Error = never, + out R = never, + out RE = never +> extends Pipeable { + readonly [TypeId]: TypeId + readonly name: Name + readonly path: PathSegment + readonly method: Method + readonly pathSchema: Option.Option> + readonly urlParamsSchema: Option.Option> + readonly payloadSchema: Option.Option> + readonly headersSchema: Option.Option> + readonly successSchema: Schema.Schema + readonly errorSchema: Schema.Schema + readonly annotations: Context.Context + readonly middlewares: ReadonlySet + + /** + * Add a schema for the success response of the endpoint. The status code + * will be inferred from the schema, otherwise it will default to 200. + */ + addSuccess( + schema: S, + annotations?: { + readonly status?: number | undefined + } + ): HttpApiEndpoint< + Name, + Method, + Path, + UrlParams, + Payload, + Headers, + Exclude | Schema.Schema.Type, + Error, + R | Schema.Schema.Context, + RE + > + + /** + * Add an error response schema to the endpoint. The status code + * will be inferred from the schema, otherwise it will default to 500. + */ + addError( + schema: E, + annotations?: { + readonly status?: number | undefined + } + ): HttpApiEndpoint< + Name, + Method, + Path, + UrlParams, + Payload, + Headers, + Success, + Error | Schema.Schema.Type, + R, + RE | Schema.Schema.Context + > + + /** + * Set the schema for the request body of the endpoint. The schema will be + * used to validate the request body before the handler is called. + * + * For endpoints with no request body, the payload will use the url search + * parameters. + * + * You can set a multipart schema to handle file uploads by using the + * `HttpApiSchema.Multipart` combinator. + */ + setPayload

( + schema: P & HttpApiEndpoint.ValidatePayload + ): HttpApiEndpoint< + Name, + Method, + Path, + UrlParams, + Schema.Schema.Type

, + Headers, + Success, + Error, + R | Schema.Schema.Context

, + RE + > + + /** + * Set the schema for the path parameters of the endpoint. The schema will be + * used to validate the path parameters before the handler is called. + */ + setPath( + schema: Path & HttpApiEndpoint.ValidatePath + ): HttpApiEndpoint< + Name, + Method, + Schema.Schema.Type, + UrlParams, + Payload, + Headers, + Success, + Error, + R | Schema.Schema.Context, + RE + > + + /** + * Set the schema for the url search parameters of the endpoint. + */ + setUrlParams( + schema: UrlParams & HttpApiEndpoint.ValidateUrlParams + ): HttpApiEndpoint< + Name, + Method, + Path, + Schema.Schema.Type, + Payload, + Headers, + Success, + Error, + R | Schema.Schema.Context, + RE + > + + /** + * Set the schema for the headers of the endpoint. The schema will be + * used to validate the headers before the handler is called. + */ + setHeaders( + schema: H & HttpApiEndpoint.ValidateHeaders + ): HttpApiEndpoint< + Name, + Method, + Path, + UrlParams, + Payload, + Schema.Schema.Type, + Success, + Error, + R | Schema.Schema.Context, + RE + > + + /** + * Add a prefix to the path of the endpoint. + */ + prefix( + prefix: PathSegment + ): HttpApiEndpoint + + /** + * Add an `HttpApiMiddleware` to the endpoint. + */ + middleware(middleware: Context.Tag): HttpApiEndpoint< + Name, + Method, + Path, + UrlParams, + Payload, + Headers, + Success, + Error | HttpApiMiddleware.HttpApiMiddleware.Error, + R | I, + RE | HttpApiMiddleware.HttpApiMiddleware.ErrorContext + > + + /** + * Add an annotation on the endpoint. + */ + annotate( + tag: Context.Tag, + value: S + ): HttpApiEndpoint + + /** + * Merge the annotations of the endpoint with the provided context. + */ + annotateContext( + context: Context.Context + ): HttpApiEndpoint +} + +/** + * @since 1.0.0 + * @category models + */ +export declare namespace HttpApiEndpoint { + /** + * @since 1.0.0 + * @category models + */ + export interface Any extends Pipeable { + readonly [TypeId]: TypeId + readonly name: string + } + + /** + * @since 1.0.0 + * @category models + */ + export interface AnyWithProps extends HttpApiEndpoint {} + + /** + * @since 1.0.0 + * @category models + */ + export type Name = Endpoint extends HttpApiEndpoint< + infer _Name, + infer _Method, + infer _Path, + infer _UrlParams, + infer _Payload, + infer _Headers, + infer _Success, + infer _Error, + infer _R, + infer _RE + > ? _Name + : never + + /** + * @since 1.0.0 + * @category models + */ + export type Success = Endpoint extends HttpApiEndpoint< + infer _Name, + infer _Method, + infer _Path, + infer _UrlParams, + infer _Payload, + infer _Headers, + infer _Success, + infer _Error, + infer _R, + infer _RE + > ? _Success + : never + + /** + * @since 1.0.0 + * @category models + */ + export type Error = Endpoint extends HttpApiEndpoint< + infer _Name, + infer _Method, + infer _Path, + infer _UrlParams, + infer _Payload, + infer _Headers, + infer _Success, + infer _Error, + infer _R, + infer _RE + > ? _Error + : never + + /** + * @since 1.0.0 + * @category models + */ + export type PathParsed = Endpoint extends HttpApiEndpoint< + infer _Name, + infer _Method, + infer _Path, + infer _UrlParams, + infer _Payload, + infer _Headers, + infer _Success, + infer _Error, + infer _R, + infer _RE + > ? _Path + : never + + /** + * @since 1.0.0 + * @category models + */ + export type UrlParams = Endpoint extends HttpApiEndpoint< + infer _Name, + infer _Method, + infer _Path, + infer _UrlParams, + infer _Payload, + infer _Headers, + infer _Success, + infer _Error, + infer _R, + infer _RE + > ? _UrlParams + : never + + /** + * @since 1.0.0 + * @category models + */ + export type Payload = Endpoint extends HttpApiEndpoint< + infer _Name, + infer _Method, + infer _Path, + infer _UrlParams, + infer _Payload, + infer _Headers, + infer _Success, + infer _Error, + infer _R, + infer _RE + > ? _Payload + : never + + /** + * @since 1.0.0 + * @category models + */ + export type Headers = Endpoint extends HttpApiEndpoint< + infer _Name, + infer _Method, + infer _Path, + infer _UrlParams, + infer _Payload, + infer _Headers, + infer _Success, + infer _Error, + infer _R, + infer _RE + > ? _Headers + : never + + /** + * @since 1.0.0 + * @category models + */ + export type Request = Endpoint extends HttpApiEndpoint< + infer _Name, + infer _Method, + infer _Path, + infer _UrlParams, + infer _Payload, + infer _Headers, + infer _Success, + infer _Error, + infer _R, + infer _RE + > ? + & ([_Path] extends [never] ? {} : { readonly path: _Path }) + & ([_UrlParams] extends [never] ? {} : { readonly urlParams: _UrlParams }) + & ([_Payload] extends [never] ? {} + : _Payload extends Brand ? + { readonly payload: Stream.Stream } + : { readonly payload: _Payload }) + & ([_Headers] extends [never] ? {} : { readonly headers: _Headers }) + & { readonly request: HttpServerRequest } + : {} + + /** + * @since 1.0.0 + * @category models + */ + export type RequestRaw = Endpoint extends HttpApiEndpoint< + infer _Name, + infer _Method, + infer _Path, + infer _UrlParams, + infer _Payload, + infer _Headers, + infer _Success, + infer _Error, + infer _R, + infer _RE + > ? + & ([_Path] extends [never] ? {} : { readonly path: _Path }) + & ([_UrlParams] extends [never] ? {} : { readonly urlParams: _UrlParams }) + & ([_Headers] extends [never] ? {} : { readonly headers: _Headers }) + & { readonly request: HttpServerRequest } + : {} + + /** + * @since 1.0.0 + * @category models + */ + export type ClientRequest = ( + & ([Path] extends [void] ? {} : { readonly path: Path }) + & ([UrlParams] extends [never] ? {} : { readonly urlParams: UrlParams }) + & ([Headers] extends [never] ? {} : { readonly headers: Headers }) + & ([Payload] extends [never] ? {} + : Payload extends infer P ? + P extends Brand | Brand + ? { readonly payload: FormData } + : { readonly payload: P } + : { readonly payload: Payload }) + ) extends infer Req ? keyof Req extends never ? (void | { readonly withResponse?: WithResponse }) : + Req & { readonly withResponse?: WithResponse } : + void + + /** + * @since 1.0.0 + * @category models + */ + export type Context = Endpoint extends HttpApiEndpoint< + infer _Name, + infer _Method, + infer _Path, + infer _UrlParams, + infer _Payload, + infer _Headers, + infer _Success, + infer _Error, + infer _R, + infer _RE + > ? _R + : never + + /** + * @since 1.0.0 + * @category models + */ + export type ErrorContext = Endpoint extends HttpApiEndpoint< + infer _Name, + infer _Method, + infer _Path, + infer _UrlParams, + infer _Payload, + infer _Headers, + infer _Success, + infer _Error, + infer _R, + infer _RE + > ? _RE + : never + + /** + * @since 1.0.0 + * @category models + */ + export type Handler = ( + request: Types.Simplify> + ) => Effect | HttpServerResponse, Error | E, R> + + /** + * @since 1.0.0 + * @category models + */ + export type HandlerRaw = ( + request: Types.Simplify> + ) => Effect | HttpServerResponse, Error | E, R> + + /** + * @since 1.0.0 + * @category models + */ + export type WithName = Extract + + /** + * @since 1.0.0 + * @category models + */ + export type ExcludeName = Exclude + + /** + * @since 1.0.0 + * @category models + */ + export type HandlerWithName = Handler< + WithName, + E, + R + > + + /** + * @since 1.0.0 + * @category models + */ + export type HandlerRawWithName = HandlerRaw< + WithName, + E, + R + > + + /** + * @since 1.0.0 + * @category models + */ + export type SuccessWithName = Success> + + /** + * @since 1.0.0 + * @category models + */ + export type ErrorWithName = Error> + + /** + * @since 1.0.0 + * @category models + */ + export type ContextWithName = Context> + + /** + * @since 1.0.0 + * @category models + */ + export type ErrorContextWithName = ErrorContext> + + /** + * @since 1.0.0 + * @category models + */ + export type ExcludeProvided = Exclude< + R, + | HttpRouter.HttpRouter.DefaultServices + | HttpRouter.HttpRouter.Provided + | HttpApiMiddleware.HttpApiMiddleware.ExtractProvides> + > + + /** + * @since 1.0.0 + * @category models + */ + export type ValidatePath = S extends Schema.Schema + ? [_I] extends [Readonly>] ? {} + : `Path schema must be encodeable to strings` + : {} + + /** + * @since 1.0.0 + * @category models + */ + export type ValidateUrlParams = S extends Schema.Schema + ? [_I] extends [Readonly | undefined>>] ? {} + : `UrlParams schema must be encodeable to strings` + : {} + + /** + * @since 1.0.0 + * @category models + */ + export type ValidateHeaders = S extends Schema.Schema + ? [_I] extends [Readonly>] ? {} + : `Headers schema must be encodeable to strings` + : {} + + /** + * @since 1.0.0 + * @category models + */ + export type ValidatePayload = Method extends + HttpMethod.NoBody ? + P extends Schema.Schema + ? [_I] extends [Readonly | undefined>>] ? {} + : `'${Method}' payload must be encodeable to strings` + : {} + : {} + + /** + * @since 1.0.0 + * @category models + */ + export type ValidateParams< + Schemas extends ReadonlyArray, + Prev extends (Schema.Schema.Any | Schema.PropertySignature.Any) = never + > = Schemas extends [ + infer Head extends (Schema.Schema.Any | Schema.PropertySignature.Any), + ...infer Tail extends ReadonlyArray + ] ? [ + Head extends HttpApiSchema.Param + ? HttpApiSchema.Param<_Name, any> extends Prev ? `Duplicate param: ${_Name}` + : [Schema.Schema.Encoded & {}] extends [string] ? Head + : `Must be encodeable to string: ${_Name}` : + Head, + ...ValidateParams + ] + : Schemas + + /** + * @since 1.0.0 + * @category models + */ + export type AddError = Endpoint extends HttpApiEndpoint< + infer _Name, + infer _Method, + infer _Path, + infer _UrlParams, + infer _Payload, + infer _Headers, + infer _Success, + infer _Error, + infer _R, + infer _RE + > ? HttpApiEndpoint< + _Name, + _Method, + _Path, + _UrlParams, + _Payload, + _Headers, + _Success, + _Error | E, + _R, + _RE | R + > : + never + + /** + * @since 1.0.0 + * @category models + */ + export type AddContext = Endpoint extends HttpApiEndpoint< + infer _Name, + infer _Method, + infer _Path, + infer _UrlParams, + infer _Payload, + infer _Headers, + infer _Success, + infer _Error, + infer _R, + infer _RE + > ? HttpApiEndpoint< + _Name, + _Method, + _Path, + _UrlParams, + _Payload, + _Headers, + _Success, + _Error | HttpApiMiddleware.HttpApiMiddleware.Error, + _R | R, + _RE | HttpApiMiddleware.HttpApiMiddleware.ErrorContext + > : + never + + /** + * @since 1.0.0 + * @category models + */ + export type PathEntries> = + Extract extends infer K ? + K extends keyof Schemas ? Schemas[K] extends HttpApiSchema.Param ? [_Name, _S] : + Schemas[K] extends (Schema.Schema.Any | Schema.PropertySignature.Any) ? [K, Schemas[K]] + : never + : never + : never + + type OptionalTypePropertySignature = + | Schema.PropertySignature<"?:", any, PropertyKey, Schema.PropertySignature.Token, any, boolean, unknown> + | Schema.PropertySignature<"?:", any, PropertyKey, Schema.PropertySignature.Token, never, boolean, unknown> + | Schema.PropertySignature<"?:", never, PropertyKey, Schema.PropertySignature.Token, any, boolean, unknown> + | Schema.PropertySignature<"?:", never, PropertyKey, Schema.PropertySignature.Token, never, boolean, unknown> + + /** + * @since 1.0.0 + * @category models + */ + export type ExtractPath> = + Schema.Simplify< + & { + readonly [ + Entry in Extract, [any, OptionalTypePropertySignature]> as Entry[0] + ]?: Schema.Schema.Type + } + & { + readonly [ + Entry in Exclude, [any, OptionalTypePropertySignature]> as Entry[0] + ]: Schema.Schema.Type + } + > + + /** + * @since 1.0.0 + * @category models + */ + export type Constructor = < + const Schemas extends ReadonlyArray + >( + segments: TemplateStringsArray, + ...schemas: ValidateParams + ) => HttpApiEndpoint< + Name, + Method, + Schemas["length"] extends 0 ? never : Types.Simplify>, + never, + never, + never, + void, + never, + Schema.Schema.Context + > +} + +const Proto = { + [TypeId]: TypeId, + pipe() { + return pipeArguments(this, arguments) + }, + addSuccess( + this: HttpApiEndpoint.AnyWithProps, + schema: Schema.Schema.Any, + annotations?: { readonly status?: number } + ) { + schema = annotations?.status ? + schema.annotations(HttpApiSchema.annotations({ status: annotations.status })) : + schema + return makeProto({ + ...this, + successSchema: this.successSchema === HttpApiSchema.NoContent ? + schema : + HttpApiSchema.UnionUnify(this.successSchema, schema) + }) + }, + addError(this: HttpApiEndpoint.AnyWithProps, schema: Schema.Schema.Any, annotations?: { readonly status?: number }) { + return makeProto({ + ...this, + errorSchema: HttpApiSchema.UnionUnify( + this.errorSchema, + annotations?.status ? schema.annotations(HttpApiSchema.annotations({ status: annotations.status })) : schema + ) + }) + }, + setPayload(this: HttpApiEndpoint.AnyWithProps, schema: Schema.Schema.Any) { + return makeProto({ + ...this, + payloadSchema: Option.some(schema) + }) + }, + setPath(this: HttpApiEndpoint.AnyWithProps, schema: Schema.Schema.Any) { + return makeProto({ + ...this, + pathSchema: Option.some(schema) + }) + }, + setUrlParams(this: HttpApiEndpoint.AnyWithProps, schema: Schema.Schema.Any) { + return makeProto({ + ...this, + urlParamsSchema: Option.some(schema) + }) + }, + setHeaders(this: HttpApiEndpoint.AnyWithProps, schema: Schema.Schema.Any) { + return makeProto({ + ...this, + headersSchema: Option.some(schema) + }) + }, + prefix(this: HttpApiEndpoint.AnyWithProps, prefix: PathSegment) { + return makeProto({ + ...this, + path: HttpRouter.prefixPath(this.path, prefix) as PathSegment + }) + }, + middleware(this: HttpApiEndpoint.AnyWithProps, middleware: HttpApiMiddleware.TagClassAny) { + return makeProto({ + ...this, + errorSchema: HttpApiSchema.UnionUnify(this.errorSchema, middleware.failure), + middlewares: new Set([...this.middlewares, middleware]) + }) + }, + annotate(this: HttpApiEndpoint.AnyWithProps, tag: Context.Tag, value: any) { + return makeProto({ + ...this, + annotations: Context.add(this.annotations, tag, value) + }) + }, + annotateContext(this: HttpApiEndpoint.AnyWithProps, context: Context.Context) { + return makeProto({ + ...this, + annotations: Context.merge(this.annotations, context) + }) + } +} + +const makeProto = < + Name extends string, + Method extends HttpMethod, + Path, + UrlParams, + Payload, + Headers, + Success, + Error, + R, + RE +>(options: { + readonly name: Name + readonly path: PathSegment + readonly method: Method + readonly pathSchema: Option.Option> + readonly urlParamsSchema: Option.Option> + readonly payloadSchema: Option.Option> + readonly headersSchema: Option.Option> + readonly successSchema: Schema.Schema + readonly errorSchema: Schema.Schema + readonly annotations: Context.Context + readonly middlewares: ReadonlySet +}): HttpApiEndpoint => + Object.assign(Object.create(Proto), options) + +/** + * @since 1.0.0 + * @category constructors + */ +export const make = (method: Method): { + (name: Name): HttpApiEndpoint.Constructor + (name: Name, path: PathSegment): HttpApiEndpoint +} => + ((name: string, ...args: [PathSegment]) => { + if (args.length === 1) { + return makeProto({ + name, + path: args[0], + method, + pathSchema: Option.none(), + urlParamsSchema: Option.none(), + payloadSchema: Option.none(), + headersSchema: Option.none(), + successSchema: HttpApiSchema.NoContent as any, + errorSchema: Schema.Never as any, + annotations: Context.empty(), + middlewares: new Set() + }) + } + return ( + segments: TemplateStringsArray, + ...schemas: ReadonlyArray + ) => { + let path = segments[0].replace(":", "::") as PathSegment + let pathSchema = Option.none() + if (schemas.length > 0) { + const obj: Record = {} + for (let i = 0; i < schemas.length; i++) { + const schema = schemas[i] + const key = HttpApiSchema.getParam(schema.ast) ?? String(i) + const optional = schema.ast._tag === "PropertySignatureTransformation" && schema.ast.from.isOptional || + schema.ast._tag === "PropertySignatureDeclaration" && schema.ast.isOptional + obj[key] = schema + path += `:${key}${optional ? "?" : ""}${segments[i + 1].replace(":", "::")}` + } + pathSchema = Option.some(Schema.Struct(obj)) + } + return makeProto({ + name, + path, + method, + pathSchema, + urlParamsSchema: Option.none(), + payloadSchema: Option.none(), + headersSchema: Option.none(), + successSchema: HttpApiSchema.NoContent as any, + errorSchema: Schema.Never as any, + annotations: Context.empty(), + middlewares: new Set() + }) + } + }) as any + +/** + * @since 1.0.0 + * @category constructors + */ +export const get: { + (name: Name): HttpApiEndpoint.Constructor + ( + name: Name, + path: PathSegment + ): HttpApiEndpoint +} = make("GET") + +/** + * @since 1.0.0 + * @category constructors + */ +export const post: { + (name: Name): HttpApiEndpoint.Constructor + ( + name: Name, + path: PathSegment + ): HttpApiEndpoint +} = make("POST") + +/** + * @since 1.0.0 + * @category constructors + */ +export const put: { + (name: Name): HttpApiEndpoint.Constructor + ( + name: Name, + path: PathSegment + ): HttpApiEndpoint +} = make("PUT") + +/** + * @since 1.0.0 + * @category constructors + */ +export const patch: { + (name: Name): HttpApiEndpoint.Constructor + ( + name: Name, + path: PathSegment + ): HttpApiEndpoint +} = make("PATCH") + +/** + * @since 1.0.0 + * @category constructors + */ +export const del: { + (name: Name): HttpApiEndpoint.Constructor + ( + name: Name, + path: PathSegment + ): HttpApiEndpoint +} = make("DELETE") + +/** + * @since 1.0.0 + * @category constructors + */ +export const head: { + (name: Name): HttpApiEndpoint.Constructor + ( + name: Name, + path: PathSegment + ): HttpApiEndpoint +} = make("HEAD") + +/** + * @since 1.0.0 + * @category constructors + */ +export const options: { + (name: Name): HttpApiEndpoint.Constructor + ( + name: Name, + path: PathSegment + ): HttpApiEndpoint +} = make("OPTIONS") diff --git a/repos/effect/packages/platform/src/HttpApiError.ts b/repos/effect/packages/platform/src/HttpApiError.ts new file mode 100644 index 0000000..5dd1515 --- /dev/null +++ b/repos/effect/packages/platform/src/HttpApiError.ts @@ -0,0 +1,168 @@ +/** + * @since 1.0.0 + */ +import * as Effect from "effect/Effect" +import * as ParseResult from "effect/ParseResult" +import * as Schema from "effect/Schema" +import * as HttpApiSchema from "./HttpApiSchema.js" + +/** + * @since 1.0.0 + * @category type ids + */ +export const TypeId: unique symbol = Symbol.for("@effect/platform/HttpApiError") + +/** + * @since 1.0.0 + * @category type ids + */ +export type TypeId = typeof TypeId + +/** + * @since 1.0.0 + * @category schemas + */ +export class Issue extends Schema.ArrayFormatterIssue.annotations({ + identifier: "Issue", + description: "Represents an error encountered while parsing a value to match the schema" +}) {} + +/** + * @since 1.0.0 + * @category errors + */ +export class HttpApiDecodeError extends Schema.TaggedError()( + "HttpApiDecodeError", + { + issues: Schema.Array(Issue), + message: Schema.String + }, + HttpApiSchema.annotations({ + status: 400, + description: "The request did not match the expected schema" + }) +) { + /** + * @since 1.0.0 + */ + static fromParseError(error: ParseResult.ParseError): Effect.Effect { + return ParseResult.ArrayFormatter.formatError(error).pipe( + Effect.zip(ParseResult.TreeFormatter.formatError(error)), + Effect.map(([issues, message]) => new HttpApiDecodeError({ issues, message })) + ) + } + /** + * @since 1.0.0 + */ + static refailParseError(error: ParseResult.ParseError): Effect.Effect { + return Effect.flatMap(HttpApiDecodeError.fromParseError(error), Effect.fail) + } +} + +/** + * @since 1.0.0 + * @category empty errors + */ +export class BadRequest extends HttpApiSchema.EmptyError()({ + tag: "BadRequest", + status: 400 +}) {} + +/** + * @since 1.0.0 + * @category empty errors + */ +export class Unauthorized extends HttpApiSchema.EmptyError()({ + tag: "Unauthorized", + status: 401 +}) {} + +/** + * @since 1.0.0 + * @category empty errors + */ +export class Forbidden extends HttpApiSchema.EmptyError()({ + tag: "Forbidden", + status: 403 +}) {} + +/** + * @since 1.0.0 + * @category empty errors + */ +export class NotFound extends HttpApiSchema.EmptyError()({ + tag: "NotFound", + status: 404 +}) {} + +/** + * @since 1.0.0 + * @category empty errors + */ +export class MethodNotAllowed extends HttpApiSchema.EmptyError()({ + tag: "MethodNotAllowed", + status: 405 +}) {} + +/** + * @since 1.0.0 + * @category empty errors + */ +export class NotAcceptable extends HttpApiSchema.EmptyError()({ + tag: "NotAcceptable", + status: 406 +}) {} + +/** + * @since 1.0.0 + * @category empty errors + */ +export class RequestTimeout extends HttpApiSchema.EmptyError()({ + tag: "RequestTimeout", + status: 408 +}) {} + +/** + * @since 1.0.0 + * @category empty errors + */ +export class Conflict extends HttpApiSchema.EmptyError()({ + tag: "Conflict", + status: 409 +}) {} + +/** + * @since 1.0.0 + * @category empty errors + */ +export class Gone extends HttpApiSchema.EmptyError()({ + tag: "Gone", + status: 410 +}) {} + +/** + * @since 1.0.0 + * @category empty errors + */ +export class InternalServerError extends HttpApiSchema.EmptyError()({ + tag: "InternalServerError", + status: 500 +}) {} + +/** + * @since 1.0.0 + * @category empty errors + */ +export class NotImplemented extends HttpApiSchema.EmptyError()({ + tag: "NotImplemented", + status: 501 +}) {} + +/** + * @since 1.0.0 + * @category empty errors + */ +export class ServiceUnavailable extends HttpApiSchema.EmptyError()({ + tag: "ServiceUnavailable", + status: 503 +}) {} diff --git a/repos/effect/packages/platform/src/HttpApiGroup.ts b/repos/effect/packages/platform/src/HttpApiGroup.ts new file mode 100644 index 0000000..60877ab --- /dev/null +++ b/repos/effect/packages/platform/src/HttpApiGroup.ts @@ -0,0 +1,430 @@ +/** + * @since 1.0.0 + */ +import * as Context from "effect/Context" +import { type Pipeable, pipeArguments } from "effect/Pipeable" +import * as Predicate from "effect/Predicate" +import * as Record from "effect/Record" +import * as Schema from "effect/Schema" +import type * as HttpApiEndpoint from "./HttpApiEndpoint.js" +import type { HttpApiDecodeError } from "./HttpApiError.js" +import type * as HttpApiMiddleware from "./HttpApiMiddleware.js" +import * as HttpApiSchema from "./HttpApiSchema.js" + +/** + * @since 1.0.0 + * @category type ids + */ +export const TypeId: unique symbol = Symbol.for("@effect/platform/HttpApiGroup") + +/** + * @since 1.0.0 + * @category type ids + */ +export type TypeId = typeof TypeId + +/** + * @since 1.0.0 + * @category guards + */ +export const isHttpApiGroup = (u: unknown): u is HttpApiGroup.Any => Predicate.hasProperty(u, TypeId) + +/** + * An `HttpApiGroup` is a collection of `HttpApiEndpoint`s. You can use an `HttpApiGroup` to + * represent a portion of your domain. + * + * The endpoints can be implemented later using the `HttpApiBuilder.group` api. + * + * @since 1.0.0 + * @category models + */ +export interface HttpApiGroup< + out Id extends string, + out Endpoints extends HttpApiEndpoint.HttpApiEndpoint.Any = never, + in out Error = HttpApiDecodeError, + out R = never, + out TopLevel extends (true | false) = false +> extends Pipeable { + new(_: never): {} + readonly [TypeId]: TypeId + readonly identifier: Id + readonly topLevel: TopLevel + readonly endpoints: Record.ReadonlyRecord + readonly errorSchema: Schema.Schema + readonly annotations: Context.Context + readonly middlewares: ReadonlySet + + /** + * Add an `HttpApiEndpoint` to an `HttpApiGroup`. + */ + add( + endpoint: A + ): HttpApiGroup + + /** + * Add an error schema to an `HttpApiGroup`, which is shared by all endpoints in the + * group. + */ + addError( + schema: Schema.Schema, + annotations?: { + readonly status?: number | undefined + } + ): HttpApiGroup + + /** + * Add a path prefix to all endpoints in an `HttpApiGroup`. Note that this will only + * add the prefix to the endpoints before this api is called. + */ + prefix(prefix: HttpApiEndpoint.PathSegment): HttpApiGroup + + /** + * Add an `HttpApiMiddleware` to the `HttpApiGroup`. + * + * It will be applied to all endpoints in the group. + */ + middleware(middleware: Context.Tag): HttpApiGroup< + Id, + Endpoints, + Error | HttpApiMiddleware.HttpApiMiddleware.Error, + R | I | HttpApiMiddleware.HttpApiMiddleware.ErrorContext, + TopLevel + > + + /** + * Add an `HttpApiMiddleware` to each endpoint in the `HttpApiGroup`. + * + * Endpoints added after this api is called will not have the middleware + * applied. + */ + middlewareEndpoints( + middleware: Context.Tag + ): HttpApiGroup< + Id, + HttpApiEndpoint.HttpApiEndpoint.AddContext, + Error, + R, + TopLevel + > + + /** + * Merge the annotations of an `HttpApiGroup` with a new context. + */ + annotateContext(context: Context.Context): HttpApiGroup + + /** + * Add an annotation to an `HttpApiGroup`. + */ + annotate(tag: Context.Tag, value: S): HttpApiGroup + + /** + * For each endpoint in an `HttpApiGroup`, update the annotations with a new + * context. + * + * Note that this will only update the annotations before this api is called. + */ + annotateEndpointsContext(context: Context.Context): HttpApiGroup + + /** + * For each endpoint in an `HttpApiGroup`, add an annotation. + * + * Note that this will only add the annotation to the endpoints before this api + * is called. + */ + annotateEndpoints(tag: Context.Tag, value: S): HttpApiGroup +} + +/** + * @since 1.0.0 + * @category models + */ +export interface ApiGroup { + readonly _: unique symbol + readonly apiId: ApiId + readonly name: Name +} + +/** + * @since 1.0.0 + * @category models + */ +export declare namespace HttpApiGroup { + /** + * @since 1.0.0 + * @category models + */ + export interface Any { + readonly [TypeId]: TypeId + readonly identifier: string + } + + /** + * @since 1.0.0 + * @category models + */ + export type AnyWithProps = HttpApiGroup + + /** + * @since 1.0.0 + * @category models + */ + export type ToService = A extends + HttpApiGroup ? ApiGroup + : never + + /** + * @since 1.0.0 + * @category models + */ + export type WithName = Extract + + /** + * @since 1.0.0 + * @category models + */ + export type Name = Group extends + HttpApiGroup ? _Name + : never + + /** + * @since 1.0.0 + * @category models + */ + export type Endpoints = Group extends + HttpApiGroup ? _Endpoints + : never + + /** + * @since 1.0.0 + * @category models + */ + export type EndpointsWithName = Endpoints> + + /** + * @since 1.0.0 + * @category models + */ + export type Error = Group extends + HttpApiGroup ? _Error + : never + + /** + * @since 1.0.0 + * @category models + */ + export type AddContext = [R] extends [never] ? Group : + Group extends HttpApiGroup ? + HttpApiGroup<_Name, _Endpoints, _Error, _R | R, _TopLevel> + : never + + /** + * @since 1.0.0 + * @category models + */ + export type Provides = HttpApiMiddleware.HttpApiMiddleware.ExtractProvides> + + /** + * @since 1.0.0 + * @category models + */ + export type ErrorWithName = Error> + + /** + * @since 1.0.0 + * @category models + */ + export type Context = Group extends + HttpApiGroup ? + HttpApiMiddleware.HttpApiMiddleware.Without<_R> : + never + + /** + * @since 1.0.0 + * @category models + */ + export type Middleware = Group extends + HttpApiGroup ? + HttpApiMiddleware.HttpApiMiddleware.Only<_R> : + never + + /** + * @since 1.0.0 + * @category models + */ + export type ClientContext = Group extends + HttpApiGroup ? + | _R + | HttpApiEndpoint.HttpApiEndpoint.Context<_Endpoints> + | HttpApiEndpoint.HttpApiEndpoint.ErrorContext<_Endpoints> + : never + + /** + * @since 1.0.0 + * @category models + */ + export type ErrorContext = Group extends + HttpApiGroup + ? HttpApiMiddleware.HttpApiMiddleware.Without<_R> | HttpApiEndpoint.HttpApiEndpoint.ErrorContext<_Endpoints> + : never + + /** + * @since 1.0.0 + * @category models + */ + export type ContextWithName = Context> + + /** + * @since 1.0.0 + * @category models + */ + export type MiddlewareWithName = Middleware< + WithName + > +} + +const Proto = { + [TypeId]: TypeId, + add(this: HttpApiGroup.AnyWithProps, endpoint: A) { + return makeProto({ + identifier: this.identifier, + topLevel: this.topLevel, + endpoints: { + ...this.endpoints, + [endpoint.name]: endpoint + }, + errorSchema: this.errorSchema, + annotations: this.annotations, + middlewares: this.middlewares + }) + }, + addError( + this: HttpApiGroup.AnyWithProps, + schema: Schema.Schema, + annotations?: { readonly status?: number } + ) { + return makeProto({ + identifier: this.identifier, + topLevel: this.topLevel, + endpoints: this.endpoints, + errorSchema: HttpApiSchema.UnionUnify( + this.errorSchema, + annotations?.status ? schema.annotations(HttpApiSchema.annotations({ status: annotations.status })) : schema + ), + annotations: this.annotations, + middlewares: this.middlewares + }) + }, + prefix(this: HttpApiGroup.AnyWithProps, prefix: HttpApiEndpoint.PathSegment) { + return makeProto({ + identifier: this.identifier, + topLevel: this.topLevel, + endpoints: Record.map(this.endpoints, (endpoint) => endpoint.prefix(prefix)), + errorSchema: this.errorSchema, + annotations: this.annotations, + middlewares: this.middlewares + }) + }, + middleware(this: HttpApiGroup.AnyWithProps, middleware: HttpApiMiddleware.TagClassAny) { + return makeProto({ + identifier: this.identifier, + topLevel: this.topLevel, + endpoints: this.endpoints, + errorSchema: HttpApiSchema.UnionUnify(this.errorSchema, middleware.failure), + annotations: this.annotations, + middlewares: new Set([...this.middlewares, middleware]) + }) + }, + middlewareEndpoints(this: HttpApiGroup.AnyWithProps, middleware: HttpApiMiddleware.TagClassAny) { + return makeProto({ + identifier: this.identifier, + topLevel: this.topLevel, + endpoints: Record.map(this.endpoints, (endpoint) => endpoint.middleware(middleware)), + errorSchema: this.errorSchema, + annotations: this.annotations, + middlewares: this.middlewares + }) + }, + annotateContext(this: HttpApiGroup.AnyWithProps, context: Context.Context) { + return makeProto({ + identifier: this.identifier, + topLevel: this.topLevel, + endpoints: this.endpoints, + errorSchema: this.errorSchema, + annotations: Context.merge(this.annotations, context), + middlewares: this.middlewares + }) + }, + annotate(this: HttpApiGroup.AnyWithProps, tag: Context.Tag, value: S) { + return makeProto({ + identifier: this.identifier, + topLevel: this.topLevel, + endpoints: this.endpoints, + errorSchema: this.errorSchema, + annotations: Context.add(this.annotations, tag, value), + middlewares: this.middlewares + }) + }, + annotateEndpointsContext(this: HttpApiGroup.AnyWithProps, context: Context.Context) { + return makeProto({ + identifier: this.identifier, + topLevel: this.topLevel, + endpoints: Record.map(this.endpoints, (endpoint) => endpoint.annotateContext(context)), + errorSchema: this.errorSchema, + annotations: this.annotations, + middlewares: this.middlewares + }) + }, + annotateEndpoints(this: HttpApiGroup.AnyWithProps, tag: Context.Tag, value: S) { + return makeProto({ + identifier: this.identifier, + topLevel: this.topLevel, + endpoints: Record.map(this.endpoints, (endpoint) => endpoint.annotate(tag, value)), + errorSchema: this.errorSchema, + annotations: this.annotations, + middlewares: this.middlewares + }) + }, + pipe() { + return pipeArguments(this, arguments) + } +} + +const makeProto = < + Id extends string, + Endpoints extends HttpApiEndpoint.HttpApiEndpoint.Any, + Error, + R, + TopLevel extends (true | false) +>(options: { + readonly identifier: Id + readonly topLevel: TopLevel + readonly endpoints: Record.ReadonlyRecord + readonly errorSchema: Schema.Schema + readonly annotations: Context.Context + readonly middlewares: ReadonlySet +}): HttpApiGroup => { + function HttpApiGroup() {} + Object.setPrototypeOf(HttpApiGroup, Proto) + return Object.assign(HttpApiGroup, options) as any +} + +/** + * An `HttpApiGroup` is a collection of `HttpApiEndpoint`s. You can use an `HttpApiGroup` to + * represent a portion of your domain. + * + * The endpoints can be implemented later using the `HttpApiBuilder.group` api. + * + * @since 1.0.0 + * @category constructors + */ +export const make = (identifier: Id, options?: { + readonly topLevel?: TopLevel | undefined +}): HttpApiGroup => + makeProto({ + identifier, + topLevel: options?.topLevel ?? false as any, + endpoints: Record.empty(), + errorSchema: Schema.Never as any, + annotations: Context.empty(), + middlewares: new Set() + }) diff --git a/repos/effect/packages/platform/src/HttpApiMiddleware.ts b/repos/effect/packages/platform/src/HttpApiMiddleware.ts new file mode 100644 index 0000000..4804f41 --- /dev/null +++ b/repos/effect/packages/platform/src/HttpApiMiddleware.ts @@ -0,0 +1,317 @@ +/** + * @since 1.0.0 + */ +import * as Context from "effect/Context" +import type * as Effect from "effect/Effect" +import { hasProperty } from "effect/Predicate" +import * as Schema from "effect/Schema" +import type { Mutable, Simplify } from "effect/Types" +import type * as HttpApiSecurity from "./HttpApiSecurity.js" +import type * as HttpRouter from "./HttpRouter.js" + +/** + * @since 1.0.0 + * @category type ids + */ +export const TypeId: unique symbol = Symbol.for("@effect/platform/HttpApiMiddleware") + +/** + * @since 1.0.0 + * @category type ids + */ +export type TypeId = typeof TypeId + +/** + * @since 1.0.0 + * @category type ids + */ +export const SecurityTypeId: unique symbol = Symbol.for("@effect/platform/HttpApiMiddleware/Security") + +/** + * @since 1.0.0 + * @category type ids + */ +export type SecurityTypeId = typeof SecurityTypeId + +/** + * @since 1.0.0 + * @category guards + */ +export const isSecurity = (u: TagClassAny): u is TagClassSecurityAny => hasProperty(u, SecurityTypeId) + +/** + * @since 1.0.0 + * @category models + */ +export interface HttpApiMiddleware extends Effect.Effect {} + +/** + * @since 1.0.0 + * @category models + */ +export type HttpApiMiddlewareSecurity, Provides, E> = { + readonly [K in keyof Security]: ( + _: HttpApiSecurity.HttpApiSecurity.Type + ) => Effect.Effect +} + +/** + * @since 1.0.0 + * @category models + */ +export declare namespace HttpApiMiddleware { + /** + * @since 1.0.0 + * @category models + */ + export interface Any { + readonly [TypeId]: TypeId + } + + /** + * @since 1.0.0 + * @category models + */ + export interface AnyId { + readonly [TypeId]: { + readonly provides: any + } + } + + /** + * @since 1.0.0 + * @category models + */ + export type Provides = A extends { readonly [TypeId]: { readonly provides: infer P } } ? P : never + + /** + * @since 1.0.0 + * @category models + */ + export type ExtractProvides = Provides> + + /** + * @since 1.0.0 + * @category models + */ + export type Error = A extends { readonly [TypeId]: { readonly failure: infer E } } ? E : never + + /** + * @since 1.0.0 + * @category models + */ + export type ErrorContext = A extends { readonly [TypeId]: { readonly failureContext: infer R } } ? R : never + + /** + * @since 1.0.0 + * @category models + */ + export type Only = Extract + + /** + * @since 1.0.0 + * @category models + */ + export type Without = Exclude +} + +/** + * @since 1.0.0 + * @category models + */ +export type TagClass< + Self, + Name extends string, + Options +> = Options extends { readonly security: Record } ? TagClass.BaseSecurity< + Self, + Name, + Options, + Simplify< + HttpApiMiddlewareSecurity< + Options["security"], + TagClass.Service, + TagClass.FailureService + > + >, + Options["security"] + > + : TagClass.Base< + Self, + Name, + Options, + HttpApiMiddleware< + TagClass.Service, + TagClass.FailureService + > + > + +/** + * @since 1.0.0 + * @category models + */ +export declare namespace TagClass { + /** + * @since 1.0.0 + * @category models + */ + export type Provides = Options extends { + readonly provides: Context.Tag + readonly optional?: false + } ? Context.Tag.Identifier + : never + + /** + * @since 1.0.0 + * @category models + */ + export type Service = Options extends { readonly provides: Context.Tag } + ? Context.Tag.Service + : void + + /** + * @since 1.0.0 + * @category models + */ + export type FailureSchema = Options extends + { readonly failure: Schema.Schema.All; readonly optional?: false } ? Options["failure"] + : typeof Schema.Never + + /** + * @since 1.0.0 + * @category models + */ + export type Failure = Options extends + { readonly failure: Schema.Schema; readonly optional?: false } ? _A + : never + + /** + * @since 1.0.0 + * @category models + */ + export type FailureContext = Schema.Schema.Context> + + /** + * @since 1.0.0 + * @category models + */ + export type FailureService = Optional extends true ? unknown : Failure + + /** + * @since 1.0.0 + * @category models + */ + export type Optional = Options extends { readonly optional: true } ? true : false + + /** + * @since 1.0.0 + * @category models + */ + export interface Base extends Context.Tag { + new(_: never): + & Context.TagClassShape + & { + readonly [TypeId]: { + readonly provides: Provides + readonly failure: Failure + readonly failureContext: FailureContext + } + } + readonly [TypeId]: TypeId + readonly optional: Optional + readonly failure: FailureSchema + readonly provides: Options extends { readonly provides: Context.Tag } ? Options["provides"] + : undefined + } + + /** + * @since 1.0.0 + * @category models + */ + export interface BaseSecurity< + Self, + Name extends string, + Options, + Service, + Security extends Record + > extends Base { + readonly [SecurityTypeId]: SecurityTypeId + readonly security: Security + } +} + +/** + * @since 1.0.0 + * @category models + */ +export interface TagClassAny extends Context.Tag { + readonly [TypeId]: TypeId + readonly optional: boolean + readonly provides?: Context.Tag + readonly failure: Schema.Schema.All +} + +/** + * @since 1.0.0 + * @category models + */ +export interface TagClassSecurityAny extends TagClassAny { + readonly [SecurityTypeId]: SecurityTypeId + readonly security: Record +} + +/** + * @since 1.0.0 + * @category tags + */ +export const Tag = (): < + const Name extends string, + const Options extends { + readonly optional?: boolean + readonly failure?: Schema.Schema.All + readonly provides?: Context.Tag + readonly security?: Record + } +>( + id: Name, + options?: Options | undefined +) => TagClass => +( + id: string, + options?: { + readonly optional?: boolean + readonly security?: Record + readonly failure?: Schema.Schema.All + readonly provides?: Context.Tag + } +) => { + const Err = globalThis.Error as any + const limit = Err.stackTraceLimit + Err.stackTraceLimit = 2 + const creationError = new Err() + Err.stackTraceLimit = limit + + function TagClass() {} + const TagClass_ = TagClass as any as Mutable + Object.setPrototypeOf(TagClass, Object.getPrototypeOf(Context.GenericTag(id))) + TagClass.key = id + Object.defineProperty(TagClass, "stack", { + get() { + return creationError.stack + } + }) + TagClass_[TypeId] = TypeId + TagClass_.failure = options?.optional === true || options?.failure === undefined ? Schema.Never : options.failure + if (options?.provides) { + TagClass_.provides = options.provides + } + TagClass_.optional = options?.optional ?? false + if (options?.security) { + if (Object.keys(options.security).length === 0) { + throw new Error("HttpApiMiddleware.Tag: security object must not be empty") + } + TagClass_[SecurityTypeId] = SecurityTypeId + TagClass_.security = options.security + } + return TagClass as any +} diff --git a/repos/effect/packages/platform/src/HttpApiScalar.ts b/repos/effect/packages/platform/src/HttpApiScalar.ts new file mode 100644 index 0000000..d1cc216 --- /dev/null +++ b/repos/effect/packages/platform/src/HttpApiScalar.ts @@ -0,0 +1,266 @@ +/** + * @since 1.0.0 + */ +import * as Effect from "effect/Effect" +import * as Layer from "effect/Layer" +import { Api } from "./HttpApi.js" +import type * as HttpApi from "./HttpApi.js" +import { Router } from "./HttpApiBuilder.js" +import * as HttpLayerRouter from "./HttpLayerRouter.js" +import * as HttpServerResponse from "./HttpServerResponse.js" +import * as Html from "./internal/html.js" +import * as internal from "./internal/httpApiScalar.js" +import * as OpenApi from "./OpenApi.js" + +/** + * @since 1.0.0 + * @category model + */ +export type ScalarThemeId = + | "alternate" + | "default" + | "moon" + | "purple" + | "solarized" + | "bluePlanet" + | "saturn" + | "kepler" + | "mars" + | "deepSpace" + | "laserwave" + | "none" + +/** + * @see https://github.com/scalar/scalar/blob/main/documentation/configuration.md + * + * @since 1.0.0 + * @category model + */ +export type ScalarConfig = { + /** A string to use one of the color presets */ + theme?: ScalarThemeId + /** The layout to use for the references */ + layout?: "modern" | "classic" + /** URL to a request proxy for the API client */ + proxyUrl?: string + /** Whether to show the sidebar */ + showSidebar?: boolean + /** + * Whether to show models in the sidebar, search, and content. + * + * Default: `false` + */ + hideModels?: boolean + /** + * Whether to show the “Test Request” button + * + * Default: `false` + */ + hideTestRequestButton?: boolean + /** + * Whether to show the sidebar search bar + * + * Default: `false` + */ + hideSearch?: boolean + /** Whether dark mode is on or off initially (light mode) */ + darkMode?: boolean + /** forceDarkModeState makes it always this state no matter what*/ + forceDarkModeState?: "dark" | "light" + /** Whether to show the dark mode toggle */ + hideDarkModeToggle?: boolean + /** + * Path to a favicon image + * + * Default: `undefined` + * Example: '/favicon.svg' + */ + favicon?: string + /** Custom CSS to be added to the page */ + customCss?: string + /** + * The baseServerURL is used when the spec servers are relative paths and we are using SSR. + * On the client we can grab the window.location.origin but on the server we need + * to use this prop. + * + * Default: `undefined` + * Example: 'http://localhost:3000' + */ + baseServerURL?: string + /** + * We’re using Inter and JetBrains Mono as the default fonts. If you want to use your own fonts, set this to false. + * + * Default: `true` + */ + withDefaultFonts?: boolean + /** + * By default we only open the relevant tag based on the url, however if you want all the tags open by default then set this configuration option :) + * + * Default: `false` + */ + defaultOpenAllTags?: boolean +} + +const makeHandler = (options: { + readonly api: HttpApi.HttpApi.Any + readonly source: { + readonly _tag: "Cdn" + readonly version?: string | undefined + } | { + readonly _tag: "Inline" + readonly source: string + } + readonly scalar?: ScalarConfig +}) => { + const spec = OpenApi.fromApi(options.api as any) + + const source = options?.source + + const scalarConfig = { + _integration: "html", + ...options?.scalar + } + + const response = HttpServerResponse.html(` + + + + ${Html.escape(spec.info.title)} + ${ + !spec.info.description + ? "" + : `` + } + ${ + !spec.info.description + ? "" + : `` + } + + + + + + ${ + source._tag === "Cdn" + ? `` + : `` + } + +`) + + return Effect.succeed(response) +} + +/** + * @since 1.0.0 + * @category layers + */ +export const layer = (options?: { + readonly path?: `/${string}` | undefined + readonly scalar?: ScalarConfig +}): Layer.Layer => + Router.use(Effect.fnUntraced(function*(router) { + const { api } = yield* Api + const handler = makeHandler({ + ...options, + api, + source: { + _tag: "Inline", + source: internal.javascript + } + }) + yield* router.get(options?.path ?? "/docs", handler) + })) + +/** + * @since 1.0.0 + * @category layers + */ +export const layerCdn = (options?: { + readonly path?: `/${string}` | undefined + readonly scalar?: ScalarConfig + readonly version?: string | undefined +}): Layer.Layer => + Router.use(Effect.fnUntraced(function*(router) { + const { api } = yield* Api + const handler = makeHandler({ + ...options, + api, + source: { + _tag: "Cdn", + version: options?.version + } + }) + yield* router.get(options?.path ?? "/docs", handler) + })) + +/** + * @since 1.0.0 + * @category layers + */ +export const layerHttpLayerRouter: ( + options: { + readonly api: HttpApi.HttpApi.Any + readonly path: `/${string}` + readonly scalar?: ScalarConfig + } +) => Layer.Layer< + never, + never, + HttpLayerRouter.HttpRouter +> = Effect.fnUntraced(function*(options: { + readonly api: HttpApi.HttpApi.Any + readonly path: `/${string}` + readonly scalar?: ScalarConfig +}) { + const router = yield* HttpLayerRouter.HttpRouter + const handler = makeHandler({ + ...options, + source: { + _tag: "Inline", + source: internal.javascript + } + }) + yield* router.add("GET", options.path, handler) +}, Layer.effectDiscard) + +/** + * @since 1.0.0 + * @category layers + */ +export const layerHttpLayerRouterCdn: ( + options: { + readonly api: HttpApi.HttpApi.Any + readonly path: `/${string}` + readonly version?: string | undefined + readonly scalar?: ScalarConfig + } +) => Layer.Layer< + never, + never, + HttpLayerRouter.HttpRouter +> = Effect.fnUntraced(function*(options: { + readonly api: HttpApi.HttpApi.Any + readonly path: `/${string}` + readonly version?: string | undefined + readonly scalar?: ScalarConfig +}) { + const router = yield* HttpLayerRouter.HttpRouter + const handler = makeHandler({ + ...options, + source: { + _tag: "Cdn", + version: options?.version + } + }) + yield* router.add("GET", options.path, handler) +}, Layer.effectDiscard) diff --git a/repos/effect/packages/platform/src/HttpApiSchema.ts b/repos/effect/packages/platform/src/HttpApiSchema.ts new file mode 100644 index 0000000..5001e3d --- /dev/null +++ b/repos/effect/packages/platform/src/HttpApiSchema.ts @@ -0,0 +1,686 @@ +/** + * @since 1.0.0 + */ +import type { Brand } from "effect/Brand" +import * as Effect from "effect/Effect" +import * as Effectable from "effect/Effectable" +import type { LazyArg } from "effect/Function" +import { constant, constVoid, dual } from "effect/Function" +import { globalValue } from "effect/GlobalValue" +import * as Option from "effect/Option" +import { hasProperty } from "effect/Predicate" +import * as Schema from "effect/Schema" +import * as AST from "effect/SchemaAST" +import * as Struct from "effect/Struct" +import type * as Unify from "effect/Unify" +import type * as FileSystem from "./FileSystem.js" +import type * as Multipart_ from "./Multipart.js" + +/** + * @since 1.0.0 + * @category annotations + */ +export const AnnotationMultipart: unique symbol = Symbol.for( + "@effect/platform/HttpApiSchema/AnnotationMultipart" +) + +/** + * @since 1.0.0 + * @category annotations + */ +export const AnnotationMultipartStream: unique symbol = Symbol.for( + "@effect/platform/HttpApiSchema/AnnotationMultipartStream" +) + +/** + * @since 1.0.0 + * @category annotations + */ +export const AnnotationStatus: unique symbol = Symbol.for("@effect/platform/HttpApiSchema/AnnotationStatus") + +/** + * @since 1.0.0 + * @category annotations + */ +export const AnnotationEmptyDecodeable: unique symbol = Symbol.for( + "@effect/platform/HttpApiSchema/AnnotationEmptyDecodeable" +) + +/** + * @since 1.0.0 + * @category annotations + */ +export const AnnotationEncoding: unique symbol = Symbol.for("@effect/platform/HttpApiSchema/AnnotationEncoding") + +/** + * @since 1.0.0 + * @category annotations + */ +export const AnnotationParam: unique symbol = Symbol.for( + "@effect/platform/HttpApiSchema/AnnotationParam" +) + +/** + * @since 1.0.0 + * @category annotations + */ +export const extractAnnotations = (ast: AST.Annotations): AST.Annotations => { + const result: Record = {} + if (AnnotationStatus in ast) { + result[AnnotationStatus] = ast[AnnotationStatus] + } + if (AnnotationEmptyDecodeable in ast) { + result[AnnotationEmptyDecodeable] = ast[AnnotationEmptyDecodeable] + } + if (AnnotationEncoding in ast) { + result[AnnotationEncoding] = ast[AnnotationEncoding] + } + if (AnnotationParam in ast) { + result[AnnotationParam] = ast[AnnotationParam] + } + if (AnnotationMultipart in ast) { + result[AnnotationMultipart] = ast[AnnotationMultipart] + } + if (AnnotationMultipartStream in ast) { + result[AnnotationMultipartStream] = ast[AnnotationMultipartStream] + } + return result +} + +const mergedAnnotations = (ast: AST.AST): Record => + ast._tag === "Transformation" ? + { + ...ast.to.annotations, + ...ast.annotations + } : + ast.annotations + +const getAnnotation = (ast: AST.AST, key: symbol): A | undefined => mergedAnnotations(ast)[key] as A + +/** + * @since 1.0.0 + * @category annotations + */ +export const getStatus = (ast: AST.AST, defaultStatus: number): number => + getAnnotation(ast, AnnotationStatus) ?? defaultStatus + +/** + * @since 1.0.0 + * @category annotations + */ +export const getEmptyDecodeable = (ast: AST.AST): boolean => + getAnnotation(ast, AnnotationEmptyDecodeable) ?? false + +/** + * @since 1.0.0 + * @category annotations + */ +export const getMultipart = (ast: AST.AST): Multipart_.withLimits.Options | undefined => + getAnnotation(ast, AnnotationMultipart) + +/** + * @since 1.0.0 + * @category annotations + */ +export const getMultipartStream = (ast: AST.AST): Multipart_.withLimits.Options | undefined => + getAnnotation(ast, AnnotationMultipartStream) + +const encodingJson: Encoding = { + kind: "Json", + contentType: "application/json" +} + +/** + * @since 1.0.0 + * @category annotations + */ +export const getEncoding = (ast: AST.AST, fallback = encodingJson): Encoding => + getAnnotation(ast, AnnotationEncoding) ?? fallback + +/** + * @since 1.0.0 + * @category annotations + */ +export const getParam = (ast: AST.AST | Schema.PropertySignature.AST): string | undefined => { + const annotations = ast._tag === "PropertySignatureTransformation" ? ast.to.annotations : ast.annotations + return (annotations[AnnotationParam] as any)?.name as string | undefined +} + +/** + * @since 1.0.0 + * @category annotations + */ +export const annotations = ( + annotations: Schema.Annotations.Schema> & { + readonly status?: number | undefined + } +): Schema.Annotations.Schema => { + const result: Record = Struct.omit(annotations, "status") + if (annotations.status !== undefined) { + result[AnnotationStatus] = annotations.status + } + return result +} + +/** + * @since 1.0.0 + * @category reflection + */ +export const isVoid = (ast: AST.AST): boolean => { + switch (ast._tag) { + case "VoidKeyword": { + return true + } + case "Transformation": { + return isVoid(ast.from) + } + case "Suspend": { + return isVoid(ast.f()) + } + default: { + return false + } + } +} + +/** + * @since 1.0.0 + * @category reflection + */ +export const getStatusSuccessAST = (ast: AST.AST): number => getStatus(ast, isVoid(ast) ? 204 : 200) + +/** + * @since 1.0.0 + * @category reflection + */ +export const getStatusSuccess = (self: A): number => getStatusSuccessAST(self.ast) + +/** + * @since 1.0.0 + * @category reflection + */ +export const getStatusErrorAST = (ast: AST.AST): number => getStatus(ast, 500) + +/** + * @since 1.0.0 + * @category reflection + */ +export const getStatusError = (self: A): number => getStatusErrorAST(self.ast) + +/** + * Extracts all individual types from a union type recursively. + * + * **Details** + * + * This function traverses an AST and collects all the types within a union, + * even if they are nested. It ensures that every type in a union (including + * deeply nested unions) is included in the resulting array. The returned array + * contains each type as an individual AST node, preserving the order in which + * they appear. + * + * @internal + */ +export const extractUnionTypes = (ast: AST.AST): ReadonlyArray => { + function process(ast: AST.AST): void { + if (AST.isUnion(ast)) { + for (const type of ast.types) { + process(type) + } + } else { + out.push(ast) + } + } + const out: Array = [] + process(ast) + return out +} + +/** @internal */ +export const UnionUnifyAST = (self: AST.AST, that: AST.AST): AST.AST => + AST.Union.make(Array.from(new Set([...extractUnionTypes(self), ...extractUnionTypes(that)]))) + +/** + * @since 1.0.0 + */ +export const UnionUnify = (self: A, that: B): Schema.Schema< + A["Type"] | B["Type"], + A["Encoded"] | B["Encoded"], + A["Context"] | B["Context"] +> => Schema.make(UnionUnifyAST(self.ast, that.ast)) + +type Void$ = typeof Schema.Void + +/** + * @since 1.0.0 + * @category path params + */ +export interface Param + extends Schema.Schema, Schema.Schema.Encoded, Schema.Schema.Context> +{ + readonly [AnnotationParam]: { + readonly name: Name + readonly schema: S + } +} + +/** + * @since 1.0.0 + * @category path params + */ +export const param: { + ( + name: Name + ): ( + schema: + & S + & ([Schema.Schema.Encoded & {}] extends [string] ? unknown : "Schema must be encodable to a string") + ) => Param + ( + name: Name, + schema: + & S + & ([Schema.Schema.Encoded & {}] extends [string] ? unknown : "Schema must be encodable to a string") + ): Param +} = dual( + 2, + ( + name: Name, + schema: S + ): Param => { + const annotations: Record = { + [AnnotationParam]: { name, schema } + } + if (Schema.isSchema(schema)) { + const identifier = AST.getIdentifierAnnotation(schema.ast) + if (Option.isSome(identifier)) { + annotations[AST.IdentifierAnnotationId] = identifier.value + } + } + return schema.annotations(annotations) as any + } +) + +/** + * @since 1.0.0 + * @category empty response + */ +export const Empty = (status: number): typeof Schema.Void => Schema.Void.annotations(annotations({ status })) + +/** + * @since 1.0.0 + * @category empty response + */ +export interface asEmpty< + S extends Schema.Schema.Any +> extends Schema.transform {} + +/** + * @since 1.0.0 + * @category empty response + */ +export const asEmpty: { + (options: { + readonly status: number + readonly decode: LazyArg> + }): (self: S) => asEmpty + ( + self: S, + options: { + readonly status: number + readonly decode: LazyArg> + } + ): asEmpty +} = dual( + 2, + ( + self: S, + options: { + readonly status: number + readonly decode: LazyArg> + } + ): asEmpty => + Schema.transform( + Schema.Void.annotations(self.ast.annotations), + Schema.typeSchema(self), + { + decode: options.decode, + encode: constVoid + } + ).annotations(annotations({ + status: options.status, + [AnnotationEmptyDecodeable]: true + })) as any +) + +/** + * @since 1.0.0 + * @category empty response + */ +export interface Created extends Void$ { + readonly _: unique symbol +} + +/** + * @since 1.0.0 + * @category empty response + */ +export const Created: Created = Empty(201) as any + +/** + * @since 1.0.0 + * @category empty response + */ +export interface Accepted extends Void$ { + readonly _: unique symbol +} + +/** + * @since 1.0.0 + * @category empty response + */ +export const Accepted: Accepted = Empty(202) as any + +/** + * @since 1.0.0 + * @category empty response + */ +export interface NoContent extends Void$ { + readonly _: unique symbol +} + +/** + * @since 1.0.0 + * @category empty response + */ +export const NoContent: NoContent = Empty(204) as any + +/** + * @since 1.0.0 + * @category multipart + */ +export const MultipartTypeId: unique symbol = Symbol.for("@effect/platform/HttpApiSchema/Multipart") + +/** + * @since 1.0.0 + * @category multipart + */ +export type MultipartTypeId = typeof MultipartTypeId + +/** + * @since 1.0.0 + * @category multipart + */ +export interface Multipart + extends + Schema.Schema & Brand, Schema.Schema.Encoded, Schema.Schema.Context> +{} + +/** + * @since 1.0.0 + * @category multipart + */ +export const Multipart = (self: S, options?: { + readonly maxParts?: Option.Option | undefined + readonly maxFieldSize?: FileSystem.SizeInput | undefined + readonly maxFileSize?: Option.Option | undefined + readonly maxTotalSize?: Option.Option | undefined + readonly fieldMimeTypes?: ReadonlyArray | undefined +}): Multipart => + self.annotations({ + [AnnotationMultipart]: options ?? {} + }) as any + +/** + * @since 1.0.0 + * @category multipart + */ +export const MultipartStreamTypeId: unique symbol = Symbol.for("@effect/platform/HttpApiSchema/MultipartStream") + +/** + * @since 1.0.0 + * @category multipart + */ +export type MultipartStreamTypeId = typeof MultipartStreamTypeId + +/** + * @since 1.0.0 + * @category multipart + */ +export interface MultipartStream extends + Schema.Schema< + Schema.Schema.Type & Brand, + Schema.Schema.Encoded, + Schema.Schema.Context + > +{} + +/** + * @since 1.0.0 + * @category multipart + */ +export const MultipartStream = (self: S, options?: { + readonly maxParts?: Option.Option | undefined + readonly maxFieldSize?: FileSystem.SizeInput | undefined + readonly maxFileSize?: Option.Option | undefined + readonly maxTotalSize?: Option.Option | undefined + readonly fieldMimeTypes?: ReadonlyArray | undefined +}): MultipartStream => + self.annotations({ + [AnnotationMultipartStream]: options ?? {} + }) as any + +const defaultContentType = (encoding: Encoding["kind"]) => { + switch (encoding) { + case "Json": { + return "application/json" + } + case "UrlParams": { + return "application/x-www-form-urlencoded" + } + case "Uint8Array": { + return "application/octet-stream" + } + case "Text": { + return "text/plain" + } + } +} + +/** + * @since 1.0.0 + * @category encoding + */ +export interface Encoding { + readonly kind: "Json" | "UrlParams" | "Uint8Array" | "Text" + readonly contentType: string +} + +/** + * @since 1.0.0 + * @category encoding + */ +export declare namespace Encoding { + /** + * @since 1.0.0 + * @category encoding + */ + export type Validate = Kind extends "Json" ? {} + : Kind extends "UrlParams" ? [A["Encoded"]] extends [Readonly>] ? {} + : `'UrlParams' kind can only be encoded to 'Record'` + : Kind extends "Uint8Array" ? + [A["Encoded"]] extends [Uint8Array] ? {} : `'Uint8Array' kind can only be encoded to 'Uint8Array'` + : Kind extends "Text" ? [A["Encoded"]] extends [string] ? {} : `'Text' kind can only be encoded to 'string'` + : never +} + +/** + * @since 1.0.0 + * @category encoding + */ +export const withEncoding: { + ( + options: { + readonly kind: Kind + readonly contentType?: string | undefined + } & Encoding.Validate + ): (self: A) => A + ( + self: A, + options: { + readonly kind: Kind + readonly contentType?: string | undefined + } & Encoding.Validate + ): A +} = dual(2, (self: A, options: { + readonly kind: Encoding["kind"] + readonly contentType?: string | undefined +}): A => + self.annotations({ + [AnnotationEncoding]: { + kind: options.kind, + contentType: options.contentType ?? defaultContentType(options.kind) + }, + ...(options.kind === "Uint8Array" ? + { + jsonSchema: { + type: "string", + format: "binary" + } + } : + undefined) + }) as any) + +/** + * @since 1.0.0 + * @category encoding + */ +export const Text = (options?: { + readonly contentType?: string +}): typeof Schema.String => withEncoding(Schema.String, { kind: "Text", ...options }) + +/** + * @since 1.0.0 + * @category encoding + */ +export const Uint8Array = (options?: { + readonly contentType?: string +}): typeof Schema.Uint8ArrayFromSelf => withEncoding(Schema.Uint8ArrayFromSelf, { kind: "Uint8Array", ...options }) + +const astCache = globalValue( + "@effect/platform/HttpApiSchema/astCache", + () => new WeakMap() +) + +/** + * @since 1.0.0 + */ +export const deunionize = ( + schemas: Set, + schema: Schema.Schema.Any +): void => { + if (astCache.has(schema.ast)) { + schemas.add(astCache.get(schema.ast)!) + return + } + const ast = schema.ast + if (ast._tag === "Union") { + for (const astType of ast.types) { + if (astCache.has(astType)) { + schemas.add(astCache.get(astType)!) + continue + } + const memberSchema = Schema.make(AST.annotations(astType, { + ...ast.annotations, + ...astType.annotations + })) + astCache.set(astType, memberSchema) + schemas.add(memberSchema) + } + } else { + astCache.set(ast, schema) + schemas.add(schema) + } +} + +/** + * @since 1.0.0 + * @category empty errors + */ +export interface EmptyError extends Effect.Effect { + readonly _tag: Tag + [Unify.typeSymbol]?: unknown + [Unify.unifySymbol]?: EmptyErrorUnify + [Unify.ignoreSymbol]?: EmptyErrorUnifyIgnore +} + +/** + * @category models + * @since 1.0.0 + */ +export interface EmptyErrorUnify extends Effect.EffectUnify { + EmptyError?: () => A[Unify.typeSymbol] extends EmptyError | infer _ ? Self + : never +} + +/** + * @since 1.0.0 + * @category empty errors + */ +export interface EmptyErrorClass extends Schema.Schema { + new(_: void): EmptyError +} + +/** + * @category models + * @since 1.0.0 + */ +export interface EmptyErrorUnifyIgnore extends Effect.EffectUnifyIgnore { + Effect?: true +} + +/** + * @since 1.0.0 + * @category empty errors + */ +export const EmptyError = () => +(options: { + readonly tag: Tag + readonly status: number +}): EmptyErrorClass => { + const symbol = Symbol.for(`@effect/platform/HttpApiSchema/EmptyError/${options.tag}`) + class EmptyError extends Effectable.StructuralClass { + readonly _tag: Tag = options.tag + commit(): Effect.Effect { + return Effect.fail(this) as any + } + } + ;(EmptyError as any).prototype[symbol] = symbol + Object.assign(EmptyError, { + [Schema.TypeId]: Schema.Void[Schema.TypeId], + pipe: Schema.Void.pipe, + annotations(this: any, annotations: any) { + return Schema.make(this.ast).annotations(annotations) + } + }) + let transform: Schema.Schema.Any | undefined + Object.defineProperty(EmptyError, "ast", { + get() { + if (transform) { + return transform.ast + } + const self = this as any + transform = asEmpty( + Schema.declare((u) => hasProperty(u, symbol), { + identifier: options.tag, + title: options.tag + }), + { + status: options.status, + decode: constant(new self()) + } + ) + return transform.ast + } + }) + return EmptyError as any +} diff --git a/repos/effect/packages/platform/src/HttpApiSecurity.ts b/repos/effect/packages/platform/src/HttpApiSecurity.ts new file mode 100644 index 0000000..9f89533 --- /dev/null +++ b/repos/effect/packages/platform/src/HttpApiSecurity.ts @@ -0,0 +1,171 @@ +/** + * @since 1.0.0 + */ +import * as Context from "effect/Context" +import { dual } from "effect/Function" +import { type Pipeable, pipeArguments } from "effect/Pipeable" +import type { Redacted } from "effect/Redacted" +import type { Covariant } from "effect/Types" + +/** + * @since 1.0.0 + * @category type ids + */ +export const TypeId: unique symbol = Symbol.for("@effect/platform/HttpApiSecurity") + +/** + * @since 1.0.0 + * @category type ids + */ +export type TypeId = typeof TypeId + +/** + * @since 1.0.0 + * @category models + */ +export type HttpApiSecurity = Bearer | ApiKey | Basic + +/** + * @since 1.0.0 + * @category models + */ +export declare namespace HttpApiSecurity { + /** + * @since 1.0.0 + * @category models + */ + export interface Proto extends Pipeable { + readonly [TypeId]: { + readonly _A: Covariant + } + readonly annotations: Context.Context + } + + /** + * @since 1.0.0 + * @category models + */ + export type Type = A extends Proto ? Out : never +} + +/** + * @since 1.0.0 + * @category models + */ +export interface Bearer extends HttpApiSecurity.Proto { + readonly _tag: "Bearer" +} + +/** + * @since 1.0.0 + * @category models + */ +export interface ApiKey extends HttpApiSecurity.Proto { + readonly _tag: "ApiKey" + readonly in: "header" | "query" | "cookie" + readonly key: string +} + +/** + * @since 1.0.0 + * @category models + */ +export interface Basic extends HttpApiSecurity.Proto { + readonly _tag: "Basic" +} + +/** + * @since 1.0.0 + * @category models + */ +export interface Credentials { + readonly username: string + readonly password: Redacted +} + +const Proto = { + [TypeId]: TypeId, + pipe() { + return pipeArguments(this, arguments) + } +} + +/** + * Create an Bearer token security scheme. + * + * You can implement some api middleware for this security scheme using + * `HttpApiBuilder.middlewareSecurity`. + * + * @since 1.0.0 + * @category constructors + */ +export const bearer: Bearer = Object.assign(Object.create(Proto), { + _tag: "Bearer", + annotations: Context.empty() +}) + +/** + * Create an API key security scheme. + * + * You can implement some api middleware for this security scheme using + * `HttpApiBuilder.middlewareSecurity`. + * + * To set the correct cookie in a handler, you can use + * `HttpApiBuilder.securitySetCookie`. + * + * The default value for `in` is "header". + * + * @since 1.0.0 + * @category constructors + */ +export const apiKey = (options: { + readonly key: string + readonly in?: "header" | "query" | "cookie" | undefined +}): ApiKey => + Object.assign(Object.create(Proto), { + _tag: "ApiKey", + key: options.key, + in: options.in ?? "header", + annotations: Context.empty() + }) + +/** + * @since 1.0.0 + * @category constructors + */ +export const basic: Basic = Object.assign(Object.create(Proto), { + _tag: "Basic", + annotations: Context.empty() +}) + +/** + * @since 1.0.0 + * @category annotations + */ +export const annotateContext: { + (context: Context.Context): (self: A) => A + (self: A, context: Context.Context): A +} = dual( + 2, + (self: A, context: Context.Context): A => + Object.assign(Object.create(Proto), { + ...self, + annotations: Context.merge(self.annotations, context) + }) +) + +/** + * @since 1.0.0 + * @category annotations + */ +export const annotate: { + (tag: Context.Tag, value: S): (self: A) => A + (self: A, tag: Context.Tag, value: S): A +} = dual( + 3, + (self: A, tag: Context.Tag, value: S): A => + Object.assign(Object.create(Proto), { + ...self, + annotations: Context.add(self.annotations, tag, value) + }) +) diff --git a/repos/effect/packages/platform/src/HttpApiSwagger.ts b/repos/effect/packages/platform/src/HttpApiSwagger.ts new file mode 100644 index 0000000..6f00d06 --- /dev/null +++ b/repos/effect/packages/platform/src/HttpApiSwagger.ts @@ -0,0 +1,85 @@ +/** + * @since 1.0.0 + */ +import * as Effect from "effect/Effect" +import * as Layer from "effect/Layer" +import { Api } from "./HttpApi.js" +import type * as HttpApi from "./HttpApi.js" +import { Router } from "./HttpApiBuilder.js" +import * as HttpLayerRouter from "./HttpLayerRouter.js" +import * as HttpServerResponse from "./HttpServerResponse.js" +import * as Html from "./internal/html.js" +import * as internal from "./internal/httpApiSwagger.js" +import * as OpenApi from "./OpenApi.js" + +const makeHandler = (options: { + readonly api: HttpApi.HttpApi.Any +}) => { + const spec = OpenApi.fromApi(options.api as any) + const response = HttpServerResponse.html(` + + + + + ${Html.escape(spec.info.title)} Documentation + + + +

+ + + +`) + return Effect.succeed(response) +} + +/** + * Exported layer mounting Swagger/OpenAPI documentation UI. + * + * @param options.path Optional mount path (default "/docs"). + * + * @since 1.0.0 + * @category layers + */ +export const layer = (options?: { + readonly path?: `/${string}` | undefined +}): Layer.Layer => + Router.use((router) => + Effect.gen(function*() { + const { api } = yield* Api + const handler = makeHandler({ api }) + yield* router.get(options?.path ?? "/docs", handler) + }) + ) + +/** + * @since 1.0.0 + * @category layers + */ +export const layerHttpLayerRouter: ( + options: { + readonly api: HttpApi.HttpApi.Any + readonly path: `/${string}` + } +) => Layer.Layer< + never, + never, + HttpLayerRouter.HttpRouter +> = Effect.fnUntraced(function*(options: { + readonly api: HttpApi.HttpApi.Any + readonly path: `/${string}` +}) { + const router = yield* HttpLayerRouter.HttpRouter + const handler = makeHandler(options) + yield* router.add("GET", options.path, handler) +}, Layer.effectDiscard) diff --git a/repos/effect/packages/platform/src/HttpApp.ts b/repos/effect/packages/platform/src/HttpApp.ts new file mode 100644 index 0000000..31d44c8 --- /dev/null +++ b/repos/effect/packages/platform/src/HttpApp.ts @@ -0,0 +1,355 @@ +/** + * @since 1.0.0 + */ +import * as Cause from "effect/Cause" +import * as Context from "effect/Context" +import * as Effect from "effect/Effect" +import * as Exit from "effect/Exit" +import * as Fiber from "effect/Fiber" +import type * as FiberRef from "effect/FiberRef" +import * as GlobalValue from "effect/GlobalValue" +import * as Layer from "effect/Layer" +import * as Option from "effect/Option" +import * as Runtime from "effect/Runtime" +import * as Scope from "effect/Scope" +import * as Stream from "effect/Stream" +import type * as Types from "effect/Types" +import { unify } from "effect/Unify" +import * as HttpBody from "./HttpBody.js" +import type { HttpMiddleware } from "./HttpMiddleware.js" +import * as ServerError from "./HttpServerError.js" +import * as ServerRequest from "./HttpServerRequest.js" +import * as ServerResponse from "./HttpServerResponse.js" +import * as internal from "./internal/httpApp.js" +import * as internalMiddleware from "./internal/httpMiddleware.js" + +/** + * @since 1.0.0 + * @category models + */ +export type HttpApp = Effect.Effect< + A, + E, + R | ServerRequest.HttpServerRequest +> + +/** + * @since 1.0.0 + * @category models + */ +export type Default = HttpApp + +const handledSymbol = Symbol.for("@effect/platform/HttpApp/handled") + +/** + * @since 1.0.0 + * @category combinators + */ +export const toHandled = ( + self: Default, + handleResponse: ( + request: ServerRequest.HttpServerRequest, + response: ServerResponse.HttpServerResponse + ) => Effect.Effect<_, EH, RH>, + middleware?: HttpMiddleware | undefined +): Effect.Effect> => { + const responded = Effect.withFiberRuntime< + ServerResponse.HttpServerResponse, + E | EH | ServerError.ResponseError, + R | RH | ServerRequest.HttpServerRequest + >((fiber) => + Effect.flatMap(self, (response) => { + const request = Context.unsafeGet(fiber.currentContext, ServerRequest.HttpServerRequest) + const handler = fiber.getFiberRef(currentPreResponseHandlers) + if (handler._tag === "None") { + ;(request as any)[handledSymbol] = true + return Effect.as(handleResponse(request, response), response) + } + return Effect.tap(handler.value(request, response), (response) => { + ;(request as any)[handledSymbol] = true + return handleResponse(request, response) + }) + }) + ) + + const withErrorHandling = Effect.catchAllCause( + responded, + (cause) => + Effect.withFiberRuntime< + ServerResponse.HttpServerResponse, + E | EH | ServerError.ResponseError, + ServerRequest.HttpServerRequest | RH + >((fiber) => + Effect.flatMap(ServerError.causeResponse(cause), ([response, cause]) => { + const request = Context.unsafeGet(fiber.currentContext, ServerRequest.HttpServerRequest) + const handler = fiber.getFiberRef(currentPreResponseHandlers) + if (handler._tag === "None") { + ;(request as any)[handledSymbol] = true + return Effect.zipRight( + handleResponse(request, response), + Cause.isEmptyType(cause) ? Effect.succeed(response) : Effect.failCause(cause) + ) + } + return Effect.zipRight( + Effect.tap(handler.value(request, response), (response) => { + ;(request as any)[handledSymbol] = true + return handleResponse(request, response) + }), + Cause.isEmptyType(cause) ? Effect.succeed(response) : Effect.failCause(cause) + ) + }) + ) + ) + + const withMiddleware = unify( + middleware === undefined ? + internalMiddleware.tracer(withErrorHandling) : + Effect.matchCauseEffect(internalMiddleware.tracer(middleware(withErrorHandling)), { + onFailure: (cause): Effect.Effect => + Effect.withFiberRuntime((fiber) => { + const request = Context.unsafeGet(fiber.currentContext, ServerRequest.HttpServerRequest) + if (handledSymbol in request) { + return Effect.void + } + return Effect.matchCauseEffect(ServerError.causeResponse(cause), { + onFailure: (_cause) => handleResponse(request, ServerResponse.empty({ status: 500 })), + onSuccess: ([response]) => handleResponse(request, response) + }) + }), + onSuccess: (response): Effect.Effect => + Effect.withFiberRuntime((fiber) => { + const request = Context.unsafeGet(fiber.currentContext, ServerRequest.HttpServerRequest) + return handledSymbol in request ? Effect.void : handleResponse(request, response) + }) + }) + ) + + return Effect.uninterruptible(scoped(withMiddleware)) as any +} + +/** + * If you want to finalize the http request scope elsewhere, you can use this + * function to eject from the default scope closure. + * + * @since 1.0.0 + * @category Scope + */ +export const ejectDefaultScopeClose = (scope: Scope.Scope): void => { + ejectedScopes.add(scope) +} + +/** + * @since 1.0.0 + * @category Scope + */ +export const unsafeEjectStreamScope = ( + response: ServerResponse.HttpServerResponse +): ServerResponse.HttpServerResponse => { + if (response.body._tag !== "Stream") { + return response + } + const fiber = Option.getOrThrow(Fiber.getCurrentFiber()) + const scope = Context.unsafeGet(fiber.currentContext, Scope.Scope) as Scope.CloseableScope + ejectDefaultScopeClose(scope) + return ServerResponse.setBody( + response, + HttpBody.stream( + Stream.ensuring(response.body.stream, Scope.close(scope, Exit.void)), + response.body.contentType, + response.body.contentLength + ) + ) +} + +const ejectedScopes = GlobalValue.globalValue( + "@effect/platform/HttpApp/ejectedScopes", + () => new WeakSet() +) + +const scoped = (effect: Effect.Effect) => + Effect.flatMap(Scope.make(), (scope) => + Effect.onExit(Scope.extend(effect, scope), (exit) => { + if (ejectedScopes.has(scope)) { + return Effect.void + } + return Scope.close(scope, exit) + })) + +/** + * @since 1.0.0 + * @category models + */ +export type PreResponseHandler = ( + request: ServerRequest.HttpServerRequest, + response: ServerResponse.HttpServerResponse +) => Effect.Effect + +/** + * @since 1.0.0 + * @category fiber refs + */ +export const currentPreResponseHandlers: FiberRef.FiberRef> = + internal.currentPreResponseHandlers + +/** + * @since 1.0.0 + * @category fiber refs + */ +export const appendPreResponseHandler: (handler: PreResponseHandler) => Effect.Effect = + internal.appendPreResponseHandler + +/** + * @since 1.0.0 + * @category fiber refs + */ +export const withPreResponseHandler = internal.withPreResponseHandler + +/** + * @since 1.0.0 + * @category conversions + */ +export const toWebHandlerRuntime = (runtime: Runtime.Runtime) => { + const httpRuntime: Types.Mutable> = Runtime.make(runtime) + const run = Runtime.runFork(httpRuntime) + return (self: Default, middleware?: HttpMiddleware | undefined) => { + const resolveSymbol = Symbol.for("@effect/platform/HttpApp/resolve") + const httpApp = toHandled(self, (request, response) => { + response = unsafeEjectStreamScope(response) + ;(request as any)[resolveSymbol]( + ServerResponse.toWeb(response, { withoutBody: request.method === "HEAD", runtime }) + ) + return Effect.void + }, middleware) + return (request: Request, context?: Context.Context | undefined): Promise => + new Promise((resolve) => { + const contextMap = new Map(runtime.context.unsafeMap) + if (Context.isContext(context)) { + for (const [key, value] of context.unsafeMap) { + contextMap.set(key, value) + } + } + const httpServerRequest = ServerRequest.fromWeb(request) + contextMap.set(ServerRequest.HttpServerRequest.key, httpServerRequest) + ;(httpServerRequest as any)[resolveSymbol] = resolve + httpRuntime.context = Context.unsafeMake(contextMap) + const fiber = run(httpApp as any) + request.signal?.addEventListener("abort", () => { + fiber.unsafeInterruptAsFork(ServerError.clientAbortFiberId) + }, { once: true }) + }) + } +} + +/** + * @since 1.0.0 + * @category conversions + */ +export const toWebHandler: ( + self: Default, + middleware?: HttpMiddleware | undefined +) => (request: Request, context?: Context.Context | undefined) => Promise = toWebHandlerRuntime( + Runtime.defaultRuntime +) + +/** + * @since 1.0.0 + * @category conversions + */ +export const toWebHandlerLayerWith = ( + layer: Layer.Layer, + options: { + readonly toHandler: ( + runtime: Runtime.Runtime + ) => Effect.Effect< + Effect.Effect, + EX + > + readonly middleware?: HttpMiddleware | undefined + readonly memoMap?: Layer.MemoMap | undefined + } +): { + readonly dispose: () => Promise + readonly handler: (request: Request, context?: Context.Context | undefined) => Promise +} => { + const scope = Effect.runSync(Scope.make()) + const dispose = () => Effect.runPromise(Scope.close(scope, Exit.void)) + + let handlerCache: ((request: Request, context?: Context.Context | undefined) => Promise) | undefined + let handlerPromise: + | Promise<(request: Request, context?: Context.Context | undefined) => Promise> + | undefined + function handler(request: Request, context?: Context.Context | undefined): Promise { + if (handlerCache) { + return handlerCache(request, context) + } + handlerPromise ??= Effect.gen(function*() { + const runtime = yield* (options.memoMap + ? Layer.toRuntimeWithMemoMap(layer, options.memoMap) + : Layer.toRuntime(layer)) + return handlerCache = toWebHandlerRuntime(runtime)( + yield* options.toHandler(runtime), + options.middleware + ) + }).pipe( + Scope.extend(scope), + Effect.runPromise + ) + return handlerPromise.then((f) => f(request, context)) + } + return { dispose, handler } as const +} + +/** + * @since 1.0.0 + * @category conversions + */ +export const toWebHandlerLayer = ( + self: Default, + layer: Layer.Layer, + options?: { + readonly memoMap?: Layer.MemoMap | undefined + readonly middleware?: HttpMiddleware | undefined + } +): { + readonly dispose: () => Promise + readonly handler: (request: Request, context?: Context.Context | undefined) => Promise +} => + toWebHandlerLayerWith(layer, { + ...options, + toHandler: () => Effect.succeed(self) + }) + +/** + * @since 1.0.0 + * @category conversions + */ +export const fromWebHandler = ( + handler: (request: Request) => Promise +): Default => + Effect.async((resume, signal) => { + const fiber = Option.getOrThrow(Fiber.getCurrentFiber()) + const request = Context.unsafeGet(fiber.currentContext, ServerRequest.HttpServerRequest) + const requestResult = ServerRequest.toWebEither(request, { + signal, + runtime: Runtime.make({ + context: fiber.currentContext, + fiberRefs: fiber.getFiberRefs(), + runtimeFlags: Runtime.defaultRuntimeFlags + }) + }) + if (requestResult._tag === "Left") { + return resume(Effect.fail(requestResult.left)) + } + handler(requestResult.right).then( + (response) => resume(Effect.succeed(ServerResponse.fromWeb(response))), + (cause) => + resume(Effect.fail( + new ServerError.RequestError({ + cause, + request, + reason: "Transport", + description: "HttpApp.fromWebHandler: Error in handler" + }) + )) + ) + }) diff --git a/repos/effect/packages/platform/src/HttpBody.ts b/repos/effect/packages/platform/src/HttpBody.ts new file mode 100644 index 0000000..0b59c45 --- /dev/null +++ b/repos/effect/packages/platform/src/HttpBody.ts @@ -0,0 +1,267 @@ +/** + * @since 1.0.0 + */ +import type * as Effect from "effect/Effect" +import type { Inspectable } from "effect/Inspectable" +import type * as ParseResult from "effect/ParseResult" +import * as Predicate from "effect/Predicate" +import type * as Schema from "effect/Schema" +import type * as Stream_ from "effect/Stream" +import type * as PlatformError from "./Error.js" +import type * as FileSystem from "./FileSystem.js" +import * as internal from "./internal/httpBody.js" +import type * as UrlParams from "./UrlParams.js" + +/** + * @since 1.0.0 + * @category type ids + */ +export const TypeId: unique symbol = internal.TypeId + +/** + * @since 1.0.0 + * @category type ids + */ +export type TypeId = typeof TypeId + +/** + * @since 1.0.0 + * @category refinements + */ +export const isHttpBody = (u: unknown): u is HttpBody => Predicate.hasProperty(u, TypeId) + +/** + * @since 1.0.0 + * @category models + */ +export type HttpBody = Empty | Raw | Uint8Array | FormData | Stream + +/** + * @since 1.0.0 + */ +export declare namespace HttpBody { + /** + * @since 1.0.0 + * @category models + */ + export interface Proto extends Inspectable { + readonly [TypeId]: TypeId + readonly _tag: string + readonly contentType?: string | undefined + readonly contentLength?: number | undefined + } + + /** + * @since 1.0.0 + * @category models + */ + export interface FileLike { + readonly name: string + readonly lastModified: number + readonly size: number + readonly stream: () => unknown + readonly type: string + } +} + +/** + * @since 1.0.0 + * @category type ids + */ +export const ErrorTypeId: unique symbol = internal.ErrorTypeId + +/** + * @since 1.0.0 + * @category type ids + */ +export type ErrorTypeId = typeof ErrorTypeId + +/** + * @since 1.0.0 + * @category errors + */ +export interface HttpBodyError { + readonly [ErrorTypeId]: ErrorTypeId + readonly _tag: "HttpBodyError" + readonly reason: ErrorReason +} + +/** + * @since 1.0.0 + * @category errors + */ +export const HttpBodyError: (reason: ErrorReason) => HttpBodyError = internal.HttpBodyError + +/** + * @since 1.0.0 + * @category errors + */ +export type ErrorReason = { + readonly _tag: "JsonError" + readonly error: unknown +} | { + readonly _tag: "SchemaError" + readonly error: ParseResult.ParseError +} +/** + * @since 1.0.0 + * @category models + */ +export interface Empty extends HttpBody.Proto { + readonly _tag: "Empty" +} + +/** + * @since 1.0.0 + * @category constructors + */ +export const empty: Empty = internal.empty + +/** + * @since 1.0.0 + * @category models + */ +export interface Raw extends HttpBody.Proto { + readonly _tag: "Raw" + readonly body: unknown +} + +/** + * @since 1.0.0 + * @category constructors + */ +export const raw: ( + body: unknown, + options?: { + readonly contentType?: string | undefined + readonly contentLength?: number | undefined + } | undefined +) => Raw = internal.raw + +/** + * @since 1.0.0 + * @category models + */ +export interface Uint8Array extends HttpBody.Proto { + readonly _tag: "Uint8Array" + readonly body: globalThis.Uint8Array + readonly contentType: string + readonly contentLength: number +} + +/** + * @since 1.0.0 + * @category constructors + */ +export const uint8Array: (body: globalThis.Uint8Array, contentType?: string) => Uint8Array = internal.uint8Array + +/** + * @since 1.0.0 + * @category constructors + */ +export const text: (body: string, contentType?: string) => Uint8Array = internal.text + +/** + * @since 1.0.0 + * @category constructors + */ +export const unsafeJson: (body: unknown, contentType?: string) => Uint8Array = internal.unsafeJson + +/** + * @since 1.0.0 + * @category constructors + */ +export const json: (body: unknown, contentType?: string) => Effect.Effect = internal.json + +/** + * @since 1.0.0 + * @category constructors + */ +export const jsonSchema: ( + schema: Schema.Schema +) => (body: A, contentType?: string) => Effect.Effect = internal.jsonSchema + +/** + * @since 1.0.0 + * @category constructors + */ +export const urlParams: (urlParams: UrlParams.UrlParams, contentType?: string) => Uint8Array = internal.urlParams + +/** + * @since 1.0.0 + * @category models + */ +export interface FormData extends HttpBody.Proto { + readonly _tag: "FormData" + readonly formData: globalThis.FormData +} + +/** + * @since 1.0.0 + * @category FormData + */ +export const formData: (body: globalThis.FormData) => FormData = internal.formData + +/** + * @since 1.0.0 + * @category FormData + */ +export type FormDataInput = Record> + +/** + * @since 1.0.0 + * @category FormData + */ +export type FormDataCoercible = string | number | boolean | File | Blob | null | undefined + +/** + * @since 1.0.0 + * @category FormData + */ +export const formDataRecord: (entries: FormDataInput) => FormData = internal.formDataRecord + +/** + * @since 1.0.0 + * @category models + */ +export interface Stream extends HttpBody.Proto { + readonly _tag: "Stream" + readonly stream: Stream_.Stream + readonly contentType: string + readonly contentLength?: number | undefined +} + +/** + * @since 1.0.0 + * @category constructors + */ +export const stream: ( + body: Stream_.Stream, + contentType?: string, + contentLength?: number +) => Stream = internal.stream + +/** + * @since 1.0.0 + * @category constructors + */ +export const file: ( + path: string, + options?: FileSystem.StreamOptions & { readonly contentType?: string } +) => Effect.Effect = internal.file + +/** + * @since 1.0.0 + * @category constructors + */ +export const fileInfo: ( + path: string, + info: FileSystem.File.Info, + options?: FileSystem.StreamOptions & { readonly contentType?: string } +) => Effect.Effect = internal.fileInfo + +/** + * @since 1.0.0 + * @category constructors + */ +export const fileWeb: (file: HttpBody.FileLike) => Stream = internal.fileWeb diff --git a/repos/effect/packages/platform/src/HttpClient.ts b/repos/effect/packages/platform/src/HttpClient.ts new file mode 100644 index 0000000..628e9e5 --- /dev/null +++ b/repos/effect/packages/platform/src/HttpClient.ts @@ -0,0 +1,734 @@ +/** + * @since 1.0.0 + */ +import type * as Context from "effect/Context" +import type * as Effect from "effect/Effect" +import type { RuntimeFiber } from "effect/Fiber" +import type * as FiberRef from "effect/FiberRef" +import type { Inspectable } from "effect/Inspectable" +import type { Layer } from "effect/Layer" +import type { Pipeable } from "effect/Pipeable" +import type * as Predicate from "effect/Predicate" +import type { Ref } from "effect/Ref" +import type * as Schedule from "effect/Schedule" +import type { Scope } from "effect/Scope" +import type { NoExcessProperties, NoInfer } from "effect/Types" +import type { Cookies } from "./Cookies.js" +import type * as Error from "./HttpClientError.js" +import type * as ClientRequest from "./HttpClientRequest.js" +import type * as ClientResponse from "./HttpClientResponse.js" +import * as internal from "./internal/httpClient.js" + +/** + * @since 1.0.0 + * @category type ids + */ +export const TypeId: unique symbol = internal.TypeId + +/** + * @since 1.0.0 + * @category type ids + */ +export type TypeId = typeof TypeId + +/** + * @since 1.0.0 + * @category models + */ +export interface HttpClient extends HttpClient.With {} + +/** + * @since 1.0.0 + */ +export declare namespace HttpClient { + /** + * @since 1.0.0 + * @category models + */ + export interface With extends Pipeable, Inspectable { + readonly [TypeId]: TypeId + readonly execute: ( + request: ClientRequest.HttpClientRequest + ) => Effect.Effect + + readonly get: ( + url: string | URL, + options?: ClientRequest.Options.NoBody + ) => Effect.Effect + readonly head: ( + url: string | URL, + options?: ClientRequest.Options.NoBody + ) => Effect.Effect + readonly post: ( + url: string | URL, + options?: ClientRequest.Options.NoUrl + ) => Effect.Effect + readonly patch: ( + url: string | URL, + options?: ClientRequest.Options.NoUrl + ) => Effect.Effect + readonly put: ( + url: string | URL, + options?: ClientRequest.Options.NoUrl + ) => Effect.Effect + readonly del: ( + url: string | URL, + options?: ClientRequest.Options.NoUrl + ) => Effect.Effect + readonly options: ( + url: string | URL, + options?: ClientRequest.Options.NoUrl + ) => Effect.Effect + } + + /** + * @since 1.0.0 + * @category models + */ + export type Preprocess = ( + request: ClientRequest.HttpClientRequest + ) => Effect.Effect + + /** + * @since 1.0.0 + * @category models + */ + export type Postprocess = ( + request: Effect.Effect + ) => Effect.Effect +} + +/** + * @since 1.0.0 + * @category tags + */ +export const HttpClient: Context.Tag = internal.tag + +/** + * @since 1.0.0 + * @category accessors + */ +export const execute: ( + request: ClientRequest.HttpClientRequest +) => Effect.Effect = internal.execute + +/** + * @since 1.0.0 + * @category accessors + */ +export const get: ( + url: string | URL, + options?: ClientRequest.Options.NoBody | undefined +) => Effect.Effect< + ClientResponse.HttpClientResponse, + Error.HttpClientError, + HttpClient +> = internal.get + +/** + * @since 1.0.0 + * @category accessors + */ +export const head: ( + url: string | URL, + options?: ClientRequest.Options.NoBody | undefined +) => Effect.Effect< + ClientResponse.HttpClientResponse, + Error.HttpClientError, + HttpClient +> = internal.head + +/** + * @since 1.0.0 + * @category accessors + */ +export const post: ( + url: string | URL, + options?: ClientRequest.Options.NoUrl | undefined +) => Effect.Effect< + ClientResponse.HttpClientResponse, + Error.HttpClientError, + HttpClient +> = internal.post + +/** + * @since 1.0.0 + * @category accessors + */ +export const patch: ( + url: string | URL, + options?: ClientRequest.Options.NoUrl | undefined +) => Effect.Effect< + ClientResponse.HttpClientResponse, + Error.HttpClientError, + HttpClient +> = internal.patch + +/** + * @since 1.0.0 + * @category accessors + */ +export const put: ( + url: string | URL, + options?: ClientRequest.Options.NoUrl | undefined +) => Effect.Effect< + ClientResponse.HttpClientResponse, + Error.HttpClientError, + HttpClient +> = internal.put + +/** + * @since 1.0.0 + * @category accessors + */ +export const del: ( + url: string | URL, + options?: ClientRequest.Options.NoUrl | undefined +) => Effect.Effect< + ClientResponse.HttpClientResponse, + Error.HttpClientError, + HttpClient +> = internal.del + +/** + * @since 1.0.0 + * @category accessors + */ +export const options: ( + url: string | URL, + options?: ClientRequest.Options.NoUrl | undefined +) => Effect.Effect< + ClientResponse.HttpClientResponse, + Error.HttpClientError, + HttpClient +> = internal.options + +/** + * @since 1.0.0 + * @category error handling + */ +export const catchAll: { + ( + f: (e: E) => Effect.Effect + ): (self: HttpClient.With) => HttpClient.With + ( + self: HttpClient.With, + f: (e: E) => Effect.Effect + ): HttpClient.With +} = internal.catchAll + +/** + * @since 1.0.0 + * @category error handling + */ +export const catchTag: { + ( + tag: K, + f: (e: Extract) => Effect.Effect + ): (self: HttpClient.With) => HttpClient.With, R1 | R> + ( + self: HttpClient.With, + tag: K, + f: (e: Extract) => Effect.Effect + ): HttpClient.With, R1 | R> +} = internal.catchTag + +/** + * @since 1.0.0 + * @category error handling + */ +export const catchTags: { + < + E, + Cases extends + & { + [K in Extract["_tag"]]+?: ( + error: Extract + ) => Effect.Effect + } + & (unknown extends E ? {} : { [K in Exclude["_tag"]>]: never }) + >( + cases: Cases + ): ( + self: HttpClient.With + ) => HttpClient.With< + | Exclude + | { + [K in keyof Cases]: Cases[K] extends (...args: Array) => Effect.Effect ? E : never + }[keyof Cases], + | R + | { + [K in keyof Cases]: Cases[K] extends (...args: Array) => Effect.Effect ? R : never + }[keyof Cases] + > + < + E extends { _tag: string }, + R, + Cases extends + & { + [K in Extract["_tag"]]+?: ( + error: Extract + ) => Effect.Effect + } + & (unknown extends E ? {} : { [K in Exclude["_tag"]>]: never }) + >( + self: HttpClient.With, + cases: Cases + ): HttpClient.With< + | Exclude + | { + [K in keyof Cases]: Cases[K] extends (...args: Array) => Effect.Effect ? E : never + }[keyof Cases], + | R + | { + [K in keyof Cases]: Cases[K] extends (...args: Array) => Effect.Effect ? R : never + }[keyof Cases] + > +} = internal.catchTags + +/** + * Filters the result of a response, or runs an alternative effect if the predicate fails. + * + * @since 1.0.0 + * @category filters + */ +export const filterOrElse: { + ( + predicate: Predicate.Predicate, + orElse: (response: ClientResponse.HttpClientResponse) => Effect.Effect + ): (self: HttpClient.With) => HttpClient.With + ( + self: HttpClient.With, + predicate: Predicate.Predicate, + orElse: (response: ClientResponse.HttpClientResponse) => Effect.Effect + ): HttpClient.With +} = internal.filterOrElse + +/** + * Filters the result of a response, or throws an error if the predicate fails. + * + * @since 1.0.0 + * @category filters + */ +export const filterOrFail: { + ( + predicate: Predicate.Predicate, + orFailWith: (response: ClientResponse.HttpClientResponse) => E2 + ): (self: HttpClient.With) => HttpClient.With + ( + self: HttpClient.With, + predicate: Predicate.Predicate, + orFailWith: (response: ClientResponse.HttpClientResponse) => E2 + ): HttpClient.With +} = internal.filterOrFail + +/** + * Filters responses by HTTP status code. + * + * @since 1.0.0 + * @category filters + */ +export const filterStatus: { + (f: (status: number) => boolean): (self: HttpClient.With) => HttpClient.With + (self: HttpClient.With, f: (status: number) => boolean): HttpClient.With +} = internal.filterStatus + +/** + * Filters responses that return a 2xx status code. + * + * @since 1.0.0 + * @category filters + */ +export const filterStatusOk: (self: HttpClient.With) => HttpClient.With = + internal.filterStatusOk + +/** + * @since 1.0.0 + * @category constructors + */ +export const makeWith: ( + postprocess: ( + request: Effect.Effect + ) => Effect.Effect, + preprocess: HttpClient.Preprocess +) => HttpClient.With = internal.makeWith + +/** + * @since 1.0.0 + * @category constructors + */ +export const make: ( + f: ( + request: ClientRequest.HttpClientRequest, + url: URL, + signal: AbortSignal, + fiber: RuntimeFiber + ) => Effect.Effect +) => HttpClient = internal.make + +/** + * @since 1.0.0 + * @category mapping & sequencing + */ +export const transform: { + ( + f: ( + effect: Effect.Effect, + request: ClientRequest.HttpClientRequest + ) => Effect.Effect + ): (self: HttpClient.With) => HttpClient.With + ( + self: HttpClient.With, + f: ( + effect: Effect.Effect, + request: ClientRequest.HttpClientRequest + ) => Effect.Effect + ): HttpClient.With +} = internal.transform + +/** + * @since 1.0.0 + * @category mapping & sequencing + */ +export const transformResponse: { + ( + f: ( + effect: Effect.Effect + ) => Effect.Effect + ): (self: HttpClient.With) => HttpClient.With + ( + self: HttpClient.With, + f: ( + effect: Effect.Effect + ) => Effect.Effect + ): HttpClient.With +} = internal.transformResponse + +/** + * Appends a transformation of the request object before sending it. + * + * @since 1.0.0 + * @category mapping & sequencing + */ +export const mapRequest: { + ( + f: (a: ClientRequest.HttpClientRequest) => ClientRequest.HttpClientRequest + ): (self: HttpClient.With) => HttpClient.With + ( + self: HttpClient.With, + f: (a: ClientRequest.HttpClientRequest) => ClientRequest.HttpClientRequest + ): HttpClient.With +} = internal.mapRequest + +/** + * Appends an effectful transformation of the request object before sending it. + * + * @since 1.0.0 + * @category mapping & sequencing + */ +export const mapRequestEffect: { + ( + f: (a: ClientRequest.HttpClientRequest) => Effect.Effect + ): (self: HttpClient.With) => HttpClient.With + ( + self: HttpClient.With, + f: (a: ClientRequest.HttpClientRequest) => Effect.Effect + ): HttpClient.With +} = internal.mapRequestEffect + +/** + * Prepends a transformation of the request object before sending it. + * + * @since 1.0.0 + * @category mapping & sequencing + */ +export const mapRequestInput: { + ( + f: (a: ClientRequest.HttpClientRequest) => ClientRequest.HttpClientRequest + ): (self: HttpClient.With) => HttpClient.With + ( + self: HttpClient.With, + f: (a: ClientRequest.HttpClientRequest) => ClientRequest.HttpClientRequest + ): HttpClient.With +} = internal.mapRequestInput + +/** + * Prepends an effectful transformation of the request object before sending it. + * + * @since 1.0.0 + * @category mapping & sequencing + */ +export const mapRequestInputEffect: { + ( + f: (a: ClientRequest.HttpClientRequest) => Effect.Effect + ): (self: HttpClient.With) => HttpClient.With + ( + self: HttpClient.With, + f: (a: ClientRequest.HttpClientRequest) => Effect.Effect + ): HttpClient.With +} = internal.mapRequestInputEffect + +/** + * @since 1.0.0 + * @category error handling + */ +export declare namespace Retry { + /** + * @since 1.0.0 + * @category error handling + */ + export type Return, O>> = HttpClient.With< + | (O extends { schedule: Schedule.Schedule } ? E + : O extends { until: Predicate.Refinement } ? E2 + : E) + | (O extends { while: (...args: Array) => Effect.Effect } ? E : never) + | (O extends { until: (...args: Array) => Effect.Effect } ? E : never), + | R + | (O extends { schedule: Schedule.Schedule } ? R : never) + | (O extends { while: (...args: Array) => Effect.Effect } ? R : never) + | (O extends { until: (...args: Array) => Effect.Effect } ? R : never) + > extends infer Z ? Z : never +} + +/** + * Retries the request based on a provided schedule or policy. + * + * @since 1.0.0 + * @category error handling + */ +export const retry: { + , O>>( + options: O + ): (self: HttpClient.With) => Retry.Return + ( + policy: Schedule.Schedule, R1> + ): (self: HttpClient.With) => HttpClient.With + , O>>( + self: HttpClient.With, + options: O + ): Retry.Return + ( + self: HttpClient.With, + policy: Schedule.Schedule + ): HttpClient.With +} = internal.retry + +/** + * Retries common transient errors, such as rate limiting, timeouts or network issues. + * + * Specifying a `while` predicate allows you to consider other errors as + * transient. + * + * @since 1.0.0 + * @category error handling + */ +export const retryTransient: { + < + B, + E, + R1 = never, + const Mode extends "errors-only" | "response-only" | "both" = never, + Input = "errors-only" extends Mode ? E + : "response-only" extends Mode ? ClientResponse.HttpClientResponse + : ClientResponse.HttpClientResponse | E + >( + options: { + readonly mode?: Mode | undefined + readonly while?: Predicate.Predicate> + readonly schedule?: Schedule.Schedule, R1> + readonly times?: number + } | Schedule.Schedule, R1> + ): (self: HttpClient.With) => HttpClient.With + < + E, + R, + B, + R1 = never, + const Mode extends "errors-only" | "response-only" | "both" = never, + Input = "errors-only" extends Mode ? E + : "response-only" extends Mode ? ClientResponse.HttpClientResponse + : ClientResponse.HttpClientResponse | E + >( + self: HttpClient.With, + options: { + readonly mode?: Mode | undefined + readonly while?: Predicate.Predicate> + readonly schedule?: Schedule.Schedule, R1> + readonly times?: number + } | Schedule.Schedule, R1> + ): HttpClient.With +} = internal.retryTransient + +/** + * Performs an additional effect after a successful request. + * + * @since 1.0.0 + * @category mapping & sequencing + */ +export const tap: { + <_, E2, R2>( + f: (response: ClientResponse.HttpClientResponse) => Effect.Effect<_, E2, R2> + ): (self: HttpClient.With) => HttpClient.With + ( + self: HttpClient.With, + f: (response: ClientResponse.HttpClientResponse) => Effect.Effect<_, E2, R2> + ): HttpClient.With +} = internal.tap + +/** + * Performs an additional effect after an unsuccessful request. + * + * @since 1.0.0 + * @category mapping & sequencing + */ +export const tapError: { + <_, E, E2, R2>( + f: (e: NoInfer) => Effect.Effect<_, E2, R2> + ): (self: HttpClient.With) => HttpClient.With + ( + self: HttpClient.With, + f: (e: NoInfer) => Effect.Effect<_, E2, R2> + ): HttpClient.With +} = internal.tapError + +/** + * Performs an additional effect on the request before sending it. + * + * @since 1.0.0 + * @category mapping & sequencing + */ +export const tapRequest: { + <_, E2, R2>( + f: (a: ClientRequest.HttpClientRequest) => Effect.Effect<_, E2, R2> + ): (self: HttpClient.With) => HttpClient.With + ( + self: HttpClient.With, + f: (a: ClientRequest.HttpClientRequest) => Effect.Effect<_, E2, R2> + ): HttpClient.With +} = internal.tapRequest + +/** + * Associates a `Ref` of cookies with the client for handling cookies across requests. + * + * @since 1.0.0 + * @category cookies + */ +export const withCookiesRef: { + (ref: Ref): (self: HttpClient.With) => HttpClient.With + (self: HttpClient.With, ref: Ref): HttpClient.With +} = internal.withCookiesRef + +/** + * Follows HTTP redirects up to a specified number of times. + * + * @since 1.0.0 + * @category redirects + */ +export const followRedirects: { + (maxRedirects?: number | undefined): (self: HttpClient.With) => HttpClient.With + (self: HttpClient.With, maxRedirects?: number | undefined): HttpClient.With +} = internal.followRedirects + +/** + * @since 1.0.0 + * @category Tracing + */ +export const currentTracerDisabledWhen: FiberRef.FiberRef> = + internal.currentTracerDisabledWhen + +/** + * Disables tracing for specific requests based on a provided predicate. + * + * @since 1.0.0 + * @category Tracing + */ +export const withTracerDisabledWhen: { + ( + predicate: Predicate.Predicate + ): (self: HttpClient.With) => HttpClient.With + ( + self: HttpClient.With, + predicate: Predicate.Predicate + ): HttpClient.With +} = internal.withTracerDisabledWhen + +/** + * @since 1.0.0 + * @category Tracing + */ +export const currentTracerPropagation: FiberRef.FiberRef = internal.currentTracerPropagation + +/** + * Enables or disables tracing propagation for the request. + * + * @since 1.0.0 + * @category Tracing + */ +export const withTracerPropagation: { + (enabled: boolean): (self: HttpClient.With) => HttpClient.With + (self: HttpClient.With, enabled: boolean): HttpClient.With +} = internal.withTracerPropagation + +/** + * @since 1.0.0 + */ +export const layerMergedContext: ( + effect: Effect.Effect +) => Layer = internal.layerMergedContext + +/** + * @since 1.0.0 + * @category Tracing + */ +export interface SpanNameGenerator { + readonly _: unique symbol +} + +/** + * @since 1.0.0 + * @category Tracing + */ +export const SpanNameGenerator: Context.Reference< + SpanNameGenerator, + (request: ClientRequest.HttpClientRequest) => string +> = internal.SpanNameGenerator + +/** + * Customizes the span names for tracing. + * + * ```ts + * import { FetchHttpClient, HttpClient } from "@effect/platform" + * import { NodeRuntime } from "@effect/platform-node" + * import { Effect } from "effect" + * + * Effect.gen(function* () { + * const client = (yield* HttpClient.HttpClient).pipe( + * // Customize the span names for this HttpClient + * HttpClient.withSpanNameGenerator( + * (request) => `http.client ${request.method} ${request.url}` + * ) + * ) + * + * yield* client.get("https://jsonplaceholder.typicode.com/posts/1") + * }).pipe(Effect.provide(FetchHttpClient.layer), NodeRuntime.runMain) + * ``` + * + * @since 1.0.0 + * @category Tracing + */ +export const withSpanNameGenerator: { + ( + f: (request: ClientRequest.HttpClientRequest) => string + ): (self: HttpClient.With) => HttpClient.With + (self: HttpClient.With, f: (request: ClientRequest.HttpClientRequest) => string): HttpClient.With +} = internal.withSpanNameGenerator + +/** + * Ties the lifetime of the `HttpClientRequest` to a `Scope`. + * + * @since 1.0.0 + * @category Scope + */ +export const withScope: ( + self: HttpClient.With +) => HttpClient.With = internal.withScope diff --git a/repos/effect/packages/platform/src/HttpClientError.ts b/repos/effect/packages/platform/src/HttpClientError.ts new file mode 100644 index 0000000..64c408a --- /dev/null +++ b/repos/effect/packages/platform/src/HttpClientError.ts @@ -0,0 +1,76 @@ +/** + * @since 1.0.0 + */ +import { hasProperty } from "effect/Predicate" +import * as Error from "./Error.js" +import type * as ClientRequest from "./HttpClientRequest.js" +import type * as ClientResponse from "./HttpClientResponse.js" +import * as internal from "./internal/httpClientError.js" + +/** + * @since 1.0.0 + * @category type id + */ +export const TypeId: unique symbol = internal.TypeId + +/** + * @since 1.0.0 + * @category type id + */ +export type TypeId = typeof TypeId + +/** + * @since 1.0.0 + * @category guards + */ +export const isHttpClientError = (u: unknown): u is HttpClientError => hasProperty(u, TypeId) + +/** + * @since 1.0.0 + * @category error + */ +export type HttpClientError = RequestError | ResponseError + +/** + * @since 1.0.0 + * @category error + */ +export class RequestError extends Error.TypeIdError(TypeId, "RequestError")<{ + readonly request: ClientRequest.HttpClientRequest + readonly reason: "Transport" | "Encode" | "InvalidUrl" + readonly cause?: unknown + readonly description?: string +}> { + get methodAndUrl() { + return `${this.request.method} ${this.request.url}` + } + + get message() { + return this.description ? + `${this.reason}: ${this.description} (${this.methodAndUrl})` : + `${this.reason} error (${this.methodAndUrl})` + } +} + +/** + * @since 1.0.0 + * @category error + */ +export class ResponseError extends Error.TypeIdError(TypeId, "ResponseError")<{ + readonly request: ClientRequest.HttpClientRequest + readonly response: ClientResponse.HttpClientResponse + readonly reason: "StatusCode" | "Decode" | "EmptyBody" + readonly cause?: unknown + readonly description?: string +}> { + get methodAndUrl() { + return `${this.request.method} ${this.request.url}` + } + + get message() { + const info = `${this.response.status} ${this.methodAndUrl}` + return this.description ? + `${this.reason}: ${this.description} (${info})` : + `${this.reason} error (${info})` + } +} diff --git a/repos/effect/packages/platform/src/HttpClientRequest.ts b/repos/effect/packages/platform/src/HttpClientRequest.ts new file mode 100644 index 0000000..c79db13 --- /dev/null +++ b/repos/effect/packages/platform/src/HttpClientRequest.ts @@ -0,0 +1,416 @@ +/** + * @since 1.0.0 + */ +import type * as Effect from "effect/Effect" +import type { Inspectable } from "effect/Inspectable" +import type * as Option from "effect/Option" +import type { Pipeable } from "effect/Pipeable" +import type { Redacted } from "effect/Redacted" +import type * as Schema from "effect/Schema" +import type { ParseOptions } from "effect/SchemaAST" +import type * as Stream from "effect/Stream" +import type * as PlatformError from "./Error.js" +import type * as FileSystem from "./FileSystem.js" +import type * as Headers from "./Headers.js" +import type * as Body from "./HttpBody.js" +import type { HttpMethod } from "./HttpMethod.js" +import * as internal from "./internal/httpClientRequest.js" +import type * as UrlParams from "./UrlParams.js" + +/** + * @since 1.0.0 + * @category type ids + */ +export const TypeId: unique symbol = Symbol.for("@effect/platform/HttpClientRequest") + +/** + * @since 1.0.0 + * @category type ids + */ +export type TypeId = typeof TypeId + +/** + * @since 1.0.0 + * @category models + */ +export interface HttpClientRequest extends Inspectable, Pipeable { + readonly [TypeId]: TypeId + readonly method: HttpMethod + readonly url: string + readonly urlParams: UrlParams.UrlParams + readonly hash: Option.Option + readonly headers: Headers.Headers + readonly body: Body.HttpBody +} + +/** + * @since 1.0.0 + * @category models + */ +export interface Options { + readonly method?: HttpMethod | undefined + readonly url?: string | URL | undefined + readonly urlParams?: UrlParams.Input | undefined + readonly hash?: string | undefined + readonly headers?: Headers.Input | undefined + readonly body?: Body.HttpBody | undefined + readonly accept?: string | undefined + readonly acceptJson?: boolean | undefined +} + +/** + * @since 1.0.0 + */ +export declare namespace Options { + /** + * @since 1.0.0 + * @category models + */ + export interface NoBody extends Omit {} + + /** + * @since 1.0.0 + * @category models + */ + export interface NoUrl extends Omit {} +} + +/** + * @since 1.0.0 + * @category constructors + */ +export const make: ( + method: M +) => ( + url: string | URL, + options?: (M extends "GET" | "HEAD" ? Options.NoBody : Options.NoUrl) | undefined +) => HttpClientRequest = internal.make + +/** + * @since 1.0.0 + * @category constructors + */ +export const get: (url: string | URL, options?: Options.NoBody) => HttpClientRequest = internal.get + +/** + * @since 1.0.0 + * @category constructors + */ +export const post: (url: string | URL, options?: Options.NoUrl) => HttpClientRequest = internal.post + +/** + * @since 1.0.0 + * @category constructors + */ +export const patch: (url: string | URL, options?: Options.NoUrl) => HttpClientRequest = internal.patch + +/** + * @since 1.0.0 + * @category constructors + */ +export const put: (url: string | URL, options?: Options.NoUrl) => HttpClientRequest = internal.put + +/** + * @since 1.0.0 + * @category constructors + */ +export const del: (url: string | URL, options?: Options.NoUrl) => HttpClientRequest = internal.del + +/** + * @since 1.0.0 + * @category constructors + */ +export const head: (url: string | URL, options?: Options.NoBody) => HttpClientRequest = internal.head + +/** + * @since 1.0.0 + * @category constructors + */ +export const options: (url: string | URL, options?: Options.NoUrl) => HttpClientRequest = internal.options + +/** + * @since 1.0.0 + * @category combinators + */ +export const modify: { + (options: Options): (self: HttpClientRequest) => HttpClientRequest + (self: HttpClientRequest, options: Options): HttpClientRequest +} = internal.modify + +/** + * @since 1.0.0 + * @category combinators + */ +export const setMethod: { + (method: HttpMethod): (self: HttpClientRequest) => HttpClientRequest + (self: HttpClientRequest, method: HttpMethod): HttpClientRequest +} = internal.setMethod + +/** + * @since 1.0.0 + * @category combinators + */ +export const setHeader: { + (key: string, value: string): (self: HttpClientRequest) => HttpClientRequest + (self: HttpClientRequest, key: string, value: string): HttpClientRequest +} = internal.setHeader + +/** + * @since 1.0.0 + * @category combinators + */ +export const setHeaders: { + (input: Headers.Input): (self: HttpClientRequest) => HttpClientRequest + (self: HttpClientRequest, input: Headers.Input): HttpClientRequest +} = internal.setHeaders + +/** + * @since 1.0.0 + * @category combinators + */ +export const basicAuth: { + (username: string | Redacted, password: string | Redacted): (self: HttpClientRequest) => HttpClientRequest + (self: HttpClientRequest, username: string | Redacted, password: string | Redacted): HttpClientRequest +} = internal.basicAuth + +/** + * @since 1.0.0 + * @category combinators + */ +export const bearerToken: { + (token: string | Redacted): (self: HttpClientRequest) => HttpClientRequest + (self: HttpClientRequest, token: string | Redacted): HttpClientRequest +} = internal.bearerToken + +/** + * @since 1.0.0 + * @category combinators + */ +export const accept: { + (mediaType: string): (self: HttpClientRequest) => HttpClientRequest + (self: HttpClientRequest, mediaType: string): HttpClientRequest +} = internal.accept + +/** + * @since 1.0.0 + * @category combinators + */ +export const acceptJson: (self: HttpClientRequest) => HttpClientRequest = internal.acceptJson + +/** + * @since 1.0.0 + * @category combinators + */ +export const setUrl: { + (url: string | URL): (self: HttpClientRequest) => HttpClientRequest + (self: HttpClientRequest, url: string | URL): HttpClientRequest +} = internal.setUrl + +/** + * @since 1.0.0 + * @category combinators + */ +export const prependUrl: { + (path: string): (self: HttpClientRequest) => HttpClientRequest + (self: HttpClientRequest, path: string): HttpClientRequest +} = internal.prependUrl + +/** + * @since 1.0.0 + * @category combinators + */ +export const appendUrl: { + (path: string): (self: HttpClientRequest) => HttpClientRequest + (self: HttpClientRequest, path: string): HttpClientRequest +} = internal.appendUrl + +/** + * @since 1.0.0 + * @category combinators + */ +export const updateUrl: { + (f: (url: string) => string): (self: HttpClientRequest) => HttpClientRequest + (self: HttpClientRequest, f: (url: string) => string): HttpClientRequest +} = internal.updateUrl + +/** + * @since 1.0.0 + * @category combinators + */ +export const setUrlParam: { + (key: string, value: string): (self: HttpClientRequest) => HttpClientRequest + (self: HttpClientRequest, key: string, value: string): HttpClientRequest +} = internal.setUrlParam + +/** + * @since 1.0.0 + * @category combinators + */ +export const setUrlParams: { + (input: UrlParams.Input): (self: HttpClientRequest) => HttpClientRequest + (self: HttpClientRequest, input: UrlParams.Input): HttpClientRequest +} = internal.setUrlParams + +/** + * @since 1.0.0 + * @category combinators + */ +export const appendUrlParam: { + (key: string, value: string): (self: HttpClientRequest) => HttpClientRequest + (self: HttpClientRequest, key: string, value: string): HttpClientRequest +} = internal.appendUrlParam + +/** + * @since 1.0.0 + * @category combinators + */ +export const appendUrlParams: { + (input: UrlParams.Input): (self: HttpClientRequest) => HttpClientRequest + (self: HttpClientRequest, input: UrlParams.Input): HttpClientRequest +} = internal.appendUrlParams + +/** + * @since 1.0.0 + * @category combinators + */ +export const setHash: { + (hash: string): (self: HttpClientRequest) => HttpClientRequest + (self: HttpClientRequest, hash: string): HttpClientRequest +} = internal.setHash + +/** + * @since 1.0.0 + * @category combinators + */ +export const removeHash: (self: HttpClientRequest) => HttpClientRequest = internal.removeHash + +/** + * @since 1.0.0 + * @category combinators + */ +export const toUrl: (self: HttpClientRequest) => Option.Option = internal.toUrl + +/** + * @since 1.0.0 + * @category combinators + */ +export const setBody: { + (body: Body.HttpBody): (self: HttpClientRequest) => HttpClientRequest + (self: HttpClientRequest, body: Body.HttpBody): HttpClientRequest +} = internal.setBody + +/** + * @since 1.0.0 + * @category combinators + */ +export const bodyUint8Array: { + (body: Uint8Array, contentType?: string): (self: HttpClientRequest) => HttpClientRequest + (self: HttpClientRequest, body: Uint8Array, contentType?: string): HttpClientRequest +} = internal.bodyUint8Array + +/** + * @since 1.0.0 + * @category combinators + */ +export const bodyText: { + (body: string, contentType?: string): (self: HttpClientRequest) => HttpClientRequest + (self: HttpClientRequest, body: string, contentType?: string): HttpClientRequest +} = internal.bodyText + +/** + * @since 1.0.0 + * @category combinators + */ +export const bodyJson: { + (body: unknown): (self: HttpClientRequest) => Effect.Effect + (self: HttpClientRequest, body: unknown): Effect.Effect +} = internal.bodyJson + +/** + * @since 1.0.0 + * @category combinators + */ +export const bodyUnsafeJson: { + (body: unknown): (self: HttpClientRequest) => HttpClientRequest + (self: HttpClientRequest, body: unknown): HttpClientRequest +} = internal.bodyUnsafeJson + +/** + * @since 1.0.0 + * @category combinators + */ +export const schemaBodyJson: ( + schema: Schema.Schema, + options?: ParseOptions | undefined +) => { + (body: A): (self: HttpClientRequest) => Effect.Effect + (self: HttpClientRequest, body: A): Effect.Effect +} = internal.schemaBodyJson + +/** + * @since 1.0.0 + * @category combinators + */ +export const bodyUrlParams: { + (input: UrlParams.Input): (self: HttpClientRequest) => HttpClientRequest + (self: HttpClientRequest, input: UrlParams.Input): HttpClientRequest +} = internal.bodyUrlParams + +/** + * @since 1.0.0 + * @category combinators + */ +export const bodyFormData: { + (body: FormData): (self: HttpClientRequest) => HttpClientRequest + (self: HttpClientRequest, body: FormData): HttpClientRequest +} = internal.bodyFormData + +/** + * @since 1.0.0 + * @category combinators + */ +export const bodyFormDataRecord: { + (entries: Body.FormDataInput): (self: HttpClientRequest) => HttpClientRequest + (self: HttpClientRequest, entries: Body.FormDataInput): HttpClientRequest +} = internal.bodyFormDataRecord + +/** + * @since 1.0.0 + * @category combinators + */ +export const bodyStream: { + ( + body: Stream.Stream, + options?: { readonly contentType?: string | undefined; readonly contentLength?: number | undefined } | undefined + ): (self: HttpClientRequest) => HttpClientRequest + ( + self: HttpClientRequest, + body: Stream.Stream, + options?: { readonly contentType?: string | undefined; readonly contentLength?: number | undefined } | undefined + ): HttpClientRequest +} = internal.bodyStream + +/** + * @since 1.0.0 + * @category combinators + */ +export const bodyFile: { + ( + path: string, + options?: FileSystem.StreamOptions & { readonly contentType?: string } + ): (self: HttpClientRequest) => Effect.Effect + ( + self: HttpClientRequest, + path: string, + options?: FileSystem.StreamOptions & { readonly contentType?: string } + ): Effect.Effect +} = internal.bodyFile + +/** + * @since 1.0.0 + * @category combinators + */ +export const bodyFileWeb: { + (file: Body.HttpBody.FileLike): (self: HttpClientRequest) => HttpClientRequest + (self: HttpClientRequest, file: Body.HttpBody.FileLike): HttpClientRequest +} = internal.bodyFileWeb diff --git a/repos/effect/packages/platform/src/HttpClientResponse.ts b/repos/effect/packages/platform/src/HttpClientResponse.ts new file mode 100644 index 0000000..94c8f02 --- /dev/null +++ b/repos/effect/packages/platform/src/HttpClientResponse.ts @@ -0,0 +1,148 @@ +/** + * @since 1.0.0 + */ +import type * as Effect from "effect/Effect" +import type * as ParseResult from "effect/ParseResult" +import type * as Schema from "effect/Schema" +import type { ParseOptions } from "effect/SchemaAST" +import type * as Stream from "effect/Stream" +import type { Unify } from "effect/Unify" +import type * as Cookies from "./Cookies.js" +import type * as Error from "./HttpClientError.js" +import type * as ClientRequest from "./HttpClientRequest.js" +import type * as IncomingMessage from "./HttpIncomingMessage.js" +import * as internal from "./internal/httpClientResponse.js" + +export { + /** + * @since 1.0.0 + * @category schema + */ + schemaBodyJson, + /** + * @since 1.0.0 + * @category schema + */ + schemaBodyUrlParams, + /** + * @since 1.0.0 + * @category schema + */ + schemaHeaders +} from "./HttpIncomingMessage.js" + +/** + * @since 1.0.0 + * @category type ids + */ +export const TypeId: unique symbol = internal.TypeId + +/** + * @since 1.0.0 + * @category type ids + */ +export type TypeId = typeof TypeId + +/** + * @since 1.0.0 + * @category models + */ +export interface HttpClientResponse extends IncomingMessage.HttpIncomingMessage { + readonly [TypeId]: TypeId + readonly request: ClientRequest.HttpClientRequest + readonly status: number + readonly cookies: Cookies.Cookies + readonly formData: Effect.Effect +} + +/** + * @since 1.0.0 + * @category constructors + */ +export const fromWeb: (request: ClientRequest.HttpClientRequest, source: Response) => HttpClientResponse = + internal.fromWeb + +/** + * @since 1.0.0 + * @category schema + */ +export const schemaJson: < + R, + I extends { + readonly status?: number | undefined + readonly headers?: Readonly> | undefined + readonly body?: unknown + }, + A +>( + schema: Schema.Schema, + options?: ParseOptions | undefined +) => (self: HttpClientResponse) => Effect.Effect = + internal.schemaJson + +/** + * @since 1.0.0 + * @category schema + */ +export const schemaNoBody: < + R, + I extends { + readonly status?: number | undefined + readonly headers?: Readonly> | undefined + }, + A +>( + schema: Schema.Schema, + options?: ParseOptions | undefined +) => (self: HttpClientResponse) => Effect.Effect = internal.schemaNoBody + +/** + * @since 1.0.0 + * @category accessors + */ +export const stream: ( + effect: Effect.Effect +) => Stream.Stream = internal.stream + +/** + * @since 1.0.0 + * @category pattern matching + */ +export const matchStatus: { + < + const Cases extends { + readonly [status: number]: (_: HttpClientResponse) => any + readonly "2xx"?: (_: HttpClientResponse) => any + readonly "3xx"?: (_: HttpClientResponse) => any + readonly "4xx"?: (_: HttpClientResponse) => any + readonly "5xx"?: (_: HttpClientResponse) => any + readonly orElse: (_: HttpClientResponse) => any + } + >(cases: Cases): (self: HttpClientResponse) => Cases[keyof Cases] extends (_: any) => infer R ? Unify : never + < + const Cases extends { + readonly [status: number]: (_: HttpClientResponse) => any + readonly "2xx"?: (_: HttpClientResponse) => any + readonly "3xx"?: (_: HttpClientResponse) => any + readonly "4xx"?: (_: HttpClientResponse) => any + readonly "5xx"?: (_: HttpClientResponse) => any + readonly orElse: (_: HttpClientResponse) => any + } + >(self: HttpClientResponse, cases: Cases): Cases[keyof Cases] extends (_: any) => infer R ? Unify : never +} = internal.matchStatus + +/** + * @since 1.0.0 + * @category filters + */ +export const filterStatus: { + (f: (status: number) => boolean): (self: HttpClientResponse) => Effect.Effect + (self: HttpClientResponse, f: (status: number) => boolean): Effect.Effect +} = internal.filterStatus + +/** + * @since 1.0.0 + * @category filters + */ +export const filterStatusOk: (self: HttpClientResponse) => Effect.Effect = + internal.filterStatusOk diff --git a/repos/effect/packages/platform/src/HttpIncomingMessage.ts b/repos/effect/packages/platform/src/HttpIncomingMessage.ts new file mode 100644 index 0000000..b7417a8 --- /dev/null +++ b/repos/effect/packages/platform/src/HttpIncomingMessage.ts @@ -0,0 +1,128 @@ +/** + * @since 1.0.0 + */ +import * as Context from "effect/Context" +import * as Effect from "effect/Effect" +import { dual } from "effect/Function" +import * as Inspectable from "effect/Inspectable" +import * as Option from "effect/Option" +import type * as ParseResult from "effect/ParseResult" +import * as Schema from "effect/Schema" +import type { ParseOptions } from "effect/SchemaAST" +import type * as Stream from "effect/Stream" +import * as FileSystem from "./FileSystem.js" +import type * as Headers from "./Headers.js" +import * as UrlParams from "./UrlParams.js" + +/** + * @since 1.0.0 + * @category type ids + */ +export const TypeId: unique symbol = Symbol.for("@effect/platform/HttpIncomingMessage") + +/** + * @since 1.0.0 + * @category type ids + */ +export type TypeId = typeof TypeId + +/** + * @since 1.0.0 + * @category models + */ +export interface HttpIncomingMessage extends Inspectable.Inspectable { + readonly [TypeId]: TypeId + readonly headers: Headers.Headers + readonly remoteAddress: Option.Option + readonly json: Effect.Effect + readonly text: Effect.Effect + readonly urlParamsBody: Effect.Effect + readonly arrayBuffer: Effect.Effect + readonly stream: Stream.Stream +} + +/** + * @since 1.0.0 + * @category schema + */ +export const schemaBodyJson = (schema: Schema.Schema, options?: ParseOptions | undefined) => { + const parse = Schema.decodeUnknown(schema, options) + return (self: HttpIncomingMessage): Effect.Effect => + Effect.flatMap(self.json, parse) +} + +/** + * @since 1.0.0 + * @category schema + */ +export const schemaBodyUrlParams = < + A, + I extends Readonly | undefined>>, + R +>( + schema: Schema.Schema, + options?: ParseOptions | undefined +) => { + const decode = UrlParams.schemaStruct(schema, options) + return (self: HttpIncomingMessage): Effect.Effect => + Effect.flatMap(self.urlParamsBody, decode) +} + +/** + * @since 1.0.0 + * @category schema + */ +export const schemaHeaders = >, R>( + schema: Schema.Schema, + options?: ParseOptions | undefined +) => { + const parse = Schema.decodeUnknown(schema, options) + return (self: HttpIncomingMessage): Effect.Effect => parse(self.headers) +} + +/** + * @since 1.0.0 + * @category fiber refs + */ +export class MaxBodySize extends Context.Reference()("@effect/platform/HttpIncomingMessage/MaxBodySize", { + defaultValue: Option.none +}) {} + +/** + * @since 1.0.0 + * @category fiber refs + */ +export const withMaxBodySize = dual< + (size: Option.Option) => (effect: Effect.Effect) => Effect.Effect, + (effect: Effect.Effect, size: Option.Option) => Effect.Effect +>(2, (effect, size) => Effect.provideService(effect, MaxBodySize, Option.map(size, FileSystem.Size))) + +/** + * @since 1.0.0 + */ +export const inspect = (self: HttpIncomingMessage, that: object): object => { + const contentType = self.headers["content-type"] ?? "" + let body: unknown + if (contentType.includes("application/json")) { + try { + body = Effect.runSync(self.json) + } catch { + // + } + } else if (contentType.includes("text/") || contentType.includes("urlencoded")) { + try { + body = Effect.runSync(self.text) + } catch { + // + } + } + const obj: any = { + ...that, + headers: Inspectable.redact(self.headers), + remoteAddress: self.remoteAddress.toJSON() + } + if (body !== undefined) { + obj.body = body + } + return obj +} diff --git a/repos/effect/packages/platform/src/HttpLayerRouter.ts b/repos/effect/packages/platform/src/HttpLayerRouter.ts new file mode 100644 index 0000000..6403247 --- /dev/null +++ b/repos/effect/packages/platform/src/HttpLayerRouter.ts @@ -0,0 +1,1174 @@ +/** + * @since 1.0.0 + */ +import * as HttpServerRequest from "@effect/platform/HttpServerRequest" +import * as HttpServerResponse from "@effect/platform/HttpServerResponse" +import * as Arr from "effect/Array" +import * as Context from "effect/Context" +import * as Effect from "effect/Effect" +import * as FiberRef from "effect/FiberRef" +import { compose, constant, dual, identity } from "effect/Function" +import * as Layer from "effect/Layer" +import * as Option from "effect/Option" +import type * as Predicate from "effect/Predicate" +import * as Scope from "effect/Scope" +import * as Tracer from "effect/Tracer" +import type * as Types from "effect/Types" +import * as FindMyWay from "find-my-way-ts" +import type * as Etag from "./Etag.js" +import type { FileSystem } from "./FileSystem.js" +import type * as HttpApi from "./HttpApi.js" +import * as HttpApiBuilder from "./HttpApiBuilder.js" +import type * as HttpApiGroup from "./HttpApiGroup.js" +import * as HttpApp from "./HttpApp.js" +import type * as HttpMethod from "./HttpMethod.js" +import * as HttpMiddleware from "./HttpMiddleware.js" +import type { HttpPlatform } from "./HttpPlatform.js" +import { RouteContext, RouteContextTypeId } from "./HttpRouter.js" +import * as HttpServer from "./HttpServer.js" +import * as HttpServerError from "./HttpServerError.js" +import * as OpenApi from "./OpenApi.js" +import type { Path } from "./Path.js" + +/** + * @since 1.0.0 + * @category Re-exports + */ +export * as FindMyWay from "find-my-way-ts" + +/** + * @since 1.0.0 + * @category HttpRouter + */ +export const TypeId: unique symbol = Symbol.for("@effect/platform/HttpLayerRouter/HttpRouter") + +/** + * @since 1.0.0 + * @category HttpRouter + */ +export type TypeId = typeof TypeId + +/** + * @since 1.0.0 + * @category HttpRouter + */ +export interface HttpRouter { + readonly [TypeId]: TypeId + + readonly prefixed: (prefix: string) => HttpRouter + + readonly add: ( + method: "*" | "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "OPTIONS", + path: PathInput, + handler: + | Effect.Effect + | ((request: HttpServerRequest.HttpServerRequest) => Effect.Effect), + options?: { readonly uninterruptible?: boolean | undefined } | undefined + ) => Effect.Effect< + void, + never, + Request.From<"Requires", Exclude> | Request.From<"Error", E> + > + + readonly addAll: >>( + routes: Routes + ) => Effect.Effect< + void, + never, + | Request.From<"Requires", Exclude, Provided>> + | Request.From<"Error", Route.Error> + > + + readonly addGlobalMiddleware: ( + middleware: + & (( + effect: Effect.Effect + ) => Effect.Effect) + & (unhandled extends E ? unknown : "You cannot handle any errors") + ) => Effect.Effect< + void, + never, + | Request.From<"GlobalRequires", Exclude> + | Request.From<"GlobalError", Exclude> + > + + readonly asHttpEffect: () => Effect.Effect< + HttpServerResponse.HttpServerResponse, + unknown, + HttpServerRequest.HttpServerRequest | Scope.Scope + > +} + +/** + * @since 1.0.0 + * @category HttpRouter + */ +export const HttpRouter: Context.Tag = Context.GenericTag( + "@effect/platform/HttpLayerRouter" +) + +/** + * @since 1.0.0 + * @category HttpRouter + */ +export const make = Effect.gen(function*() { + const router = FindMyWay.make>(yield* RouterConfig) + const middleware = new Set() + + const addAll = >>( + routes: Routes + ): Effect.Effect< + void, + never, + | Request.From<"Requires", Exclude, Provided>> + | Request.From<"Error", Route.Error> + > => + Effect.contextWith((context: Context.Context) => { + const middleware = getMiddleware(context) + const applyMiddleware = (effect: Effect.Effect) => { + for (let i = 0; i < middleware.length; i++) { + effect = middleware[i](effect) + } + return effect + } + for (let i = 0; i < routes.length; i++) { + const route = middleware.length === 0 ? routes[i] : makeRoute({ + ...routes[i], + handler: applyMiddleware(routes[i].handler as Effect.Effect) + }) + if (route.method === "*") { + router.all(route.path, route as any) + } else { + router.on(route.method, route.path, route as any) + } + } + }) + + return HttpRouter.of({ + [TypeId]: TypeId, + prefixed(this: HttpRouter, prefix: string) { + return HttpRouter.of({ + ...this, + prefixed: (newPrefix: string) => this.prefixed(prefixPath(prefix, newPrefix)), + addAll: (routes) => addAll(routes.map(prefixRoute(prefix))) as any, + add: (method, path, handler, options) => + addAll([ + makeRoute({ + method, + path: prefixPath(path, prefix) as PathInput, + handler: Effect.isEffect(handler) + ? handler + : Effect.flatMap(HttpServerRequest.HttpServerRequest, handler), + uninterruptible: options?.uninterruptible ?? false, + prefix: Option.some(prefix) + }) + ]) + }) + }, + addAll, + add: (method, path, handler, options) => addAll([route(method, path, handler, options)]), + addGlobalMiddleware: (middleware_) => + Effect.sync(() => { + middleware.add(middleware_ as any) + }), + asHttpEffect() { + let handler = Effect.withFiberRuntime((fiber) => { + const contextMap = new Map(fiber.currentContext.unsafeMap) + const request = contextMap.get(HttpServerRequest.HttpServerRequest.key) as HttpServerRequest.HttpServerRequest + let result = router.find(request.method, request.url) + if (result === undefined && request.method === "HEAD") { + result = router.find("GET", request.url) + } + if (result === undefined) { + return Effect.fail(new HttpServerError.RouteNotFound({ request })) + } + const route = result.handler + if (route.prefix._tag === "Some") { + contextMap.set(HttpServerRequest.HttpServerRequest.key, sliceRequestUrl(request, route.prefix.value)) + } + contextMap.set(HttpServerRequest.ParsedSearchParams.key, result.searchParams) + contextMap.set(RouteContext.key, { + [RouteContextTypeId]: RouteContextTypeId, + route, + params: result.params + }) + + const span = contextMap.get(Tracer.ParentSpan.key) as Tracer.Span | undefined + if (span && span._tag === "Span") { + span.attribute("http.route", route.path) + } + return Effect.locally( + (route.uninterruptible ? + route.handler : + Effect.interruptible(route.handler)) as Effect.Effect< + HttpServerResponse.HttpServerResponse, + unknown + >, + FiberRef.currentContext, + Context.unsafeMake(contextMap) + ) + }) + if (middleware.size === 0) return handler + for (const fn of Arr.reverse(middleware)) { + handler = fn(handler as any) + } + return handler + } + }) +}) + +function sliceRequestUrl(request: HttpServerRequest.HttpServerRequest, prefix: string) { + const prefexLen = prefix.length + return request.modify({ url: request.url.length <= prefexLen ? "/" : request.url.slice(prefexLen) }) +} + +/** + * @since 1.0.0 + * @category Configuration + */ +export class RouterConfig extends Context.Reference()("@effect/platform/HttpLayerRouter/RouterConfig", { + defaultValue: constant>({}) +}) {} + +export { + /** + * @since 1.0.0 + * @category Route context + */ + params, + /** + * @since 1.0.0 + * @category Route context + */ + RouteContext, + /** + * @since 1.0.0 + * @category Route context + */ + schemaJson, + /** + * @since 1.0.0 + * @category Route context + */ + schemaNoBody, + /** + * @since 1.0.0 + * @category Route context + */ + schemaParams, + /** + * @since 1.0.0 + * @category Route context + */ + schemaPathParams +} from "./HttpRouter.js" + +/** + * A helper function that is the equivalent of: + * + * ```ts + * import * as HttpLayerRouter from "@effect/platform/HttpLayerRouter" + * import * as Effect from "effect/Effect" + * import * as Layer from "effect/Layer" + * + * const MyRoute = Layer.scopedDiscard(Effect.gen(function*() { + * const router = yield* HttpLayerRouter.HttpRouter + * + * // then use `yield* router.add(...)` to add a route + * })) + * ``` + * + * @since 1.0.0 + * @category HttpRouter + */ +export const use = ( + f: (router: HttpRouter) => Effect.Effect +): Layer.Layer> => Layer.scopedDiscard(Effect.flatMap(HttpRouter, f)) + +/** + * Create a layer that adds a single route to the HTTP router. + * + * ```ts + * import * as HttpLayerRouter from "@effect/platform/HttpLayerRouter" + * import * as HttpServerResponse from "@effect/platform/HttpServerResponse" + * + * const Route = HttpLayerRouter.add("GET", "/hello", HttpServerResponse.text("Hello, World!")) + * ``` + * + * @since 1.0.0 + * @category HttpRouter + */ +export const add = ( + method: "*" | "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "OPTIONS", + path: PathInput, + handler: + | Effect.Effect + | ((request: HttpServerRequest.HttpServerRequest) => Effect.Effect), + options?: { + readonly uninterruptible?: boolean | undefined + } +): Layer.Layer> | Request.From<"Error", E>> => + use((router) => router.add(method, path, handler, options)) + +/** + * Create a layer that adds multiple routes to the HTTP router. + * + * ```ts + * import * as HttpLayerRouter from "@effect/platform/HttpLayerRouter" + * import * as HttpServerResponse from "@effect/platform/HttpServerResponse" + * + * const Routes = HttpLayerRouter.addAll([ + * HttpLayerRouter.route("GET", "/hello", HttpServerResponse.text("Hello, World!")) + * ]) + * ``` + * + * @since 1.0.0 + * @category HttpRouter + */ +export const addAll = >, EX = never, RX = never>( + routes: Routes | Effect.Effect, + options?: { + readonly prefix?: string | undefined + } +): Layer.Layer< + never, + EX, + | HttpRouter + | Exclude + | Request.From<"Requires", Exclude, Provided>> + | Request.From<"Error", Route.Error> +> => + Layer.scopedDiscard(Effect.gen(function*() { + const toAdd = Effect.isEffect(routes) ? yield* routes : routes + let router = yield* HttpRouter + if (options?.prefix) { + router = router.prefixed(options.prefix) + } + yield* router.addAll(toAdd) + })) + +/** + * @since 1.0.0 + * @category HttpRouter + */ +export const layer: Layer.Layer = Layer.effect(HttpRouter, make) + +/** + * @since 1.0.0 + * @category HttpRouter + */ +export const toHttpEffect = ( + appLayer: Layer.Layer +): Effect.Effect< + Effect.Effect< + HttpServerResponse.HttpServerResponse, + Request.Only<"Error", R> | Request.Only<"GlobalRequires", R> | HttpServerError.RouteNotFound, + Scope.Scope | HttpServerRequest.HttpServerRequest | Request.Only<"Requires", R> | Request.Only<"GlobalRequires", R> + >, + Request.Without, + Exclude, HttpRouter> | Scope.Scope +> => + Effect.gen(function*() { + const scope = yield* Effect.scope + const memoMap = yield* Layer.CurrentMemoMap + const context = yield* Layer.buildWithMemoMap( + Layer.provideMerge(appLayer, layer), + memoMap, + scope + ) + const router = Context.get(context, HttpRouter) + return router.asHttpEffect() + }) as any + +/** + * @since 1.0.0 + * @category Route + */ +export const RouteTypeId: unique symbol = Symbol.for("@effect/platform/HttpLayerRouter/Route") + +/** + * @since 1.0.0 + * @category Route + */ +export type RouteTypeId = typeof RouteTypeId + +/** + * @since 1.0.0 + * @category Route + */ +export interface Route { + readonly [RouteTypeId]: RouteTypeId + readonly method: HttpMethod.HttpMethod | "*" + readonly path: PathInput + readonly handler: Effect.Effect + readonly uninterruptible: boolean + readonly prefix: Option.Option +} + +/** + * @since 1.0.0 + * @category Route + */ +export declare namespace Route { + /** + * @since 1.0.0 + * @category Route + */ + export type Error> = R extends Route ? E : never + + /** + * @since 1.0.0 + * @category Route + */ + export type Context> = T extends Route ? R : never +} + +const makeRoute = (options: { + readonly method: HttpMethod.HttpMethod | "*" + readonly path: PathInput + readonly handler: Effect.Effect + readonly uninterruptible?: boolean | undefined + readonly prefix?: Option.Option | undefined +}): Route> => + ({ + ...options, + uninterruptible: options.uninterruptible ?? false, + prefix: options.prefix ?? Option.none(), + [RouteTypeId]: RouteTypeId + }) as Route> + +/** + * @since 1.0.0 + * @category Route + */ +export const route = ( + method: "*" | "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "OPTIONS", + path: PathInput, + handler: + | Effect.Effect + | ((request: HttpServerRequest.HttpServerRequest) => Effect.Effect), + options?: { + readonly uninterruptible?: boolean | undefined + } +): Route> => + makeRoute({ + ...options, + method, + path, + handler: Effect.isEffect(handler) ? handler : Effect.flatMap(HttpServerRequest.HttpServerRequest, handler), + uninterruptible: options?.uninterruptible ?? false + }) + +/** + * @since 1.0.0 + * @category PathInput + */ +export type PathInput = `/${string}` | "*" + +const removeTrailingSlash = ( + path: PathInput +): PathInput => (path.endsWith("/") ? path.slice(0, -1) : path) as any + +/** + * @since 1.0.0 + * @category PathInput + */ +export const prefixPath: { + (prefix: string): (self: string) => string + (self: string, prefix: string): string +} = dual(2, (self: string, prefix: string) => { + prefix = removeTrailingSlash(prefix as PathInput) + return self === "/" ? prefix : prefix + self +}) + +/** + * @since 1.0.0 + * @category Route + */ +export const prefixRoute: { + (prefix: string): (self: Route) => Route + (self: Route, prefix: string): Route +} = dual(2, (self: Route, prefix: string): Route => + makeRoute({ + ...self, + path: prefixPath(self.path, prefix) as PathInput, + prefix: Option.match(self.prefix, { + onNone: () => Option.some(prefix as string), + onSome: (existingPrefix) => Option.some(prefixPath(existingPrefix, prefix) as string) + }) + })) + +/** + * Represents a request-level dependency, that needs to be provided by + * middleware. + * + * @since 1.0.0 + * @category Request types + */ +export interface Request { + readonly _: unique symbol + readonly kind: Kind + readonly type: T +} + +/** + * @since 1.0.0 + * @category Request types + */ +export declare namespace Request { + /** + * @since 1.0.0 + * @category Request types + */ + export type From = R extends infer T ? Request : never + + /** + * @since 1.0.0 + * @category Request types + */ + export type Only = A extends Request ? T : never + + /** + * @since 1.0.0 + * @category Request types + */ + export type Without = A extends Request ? never : A +} + +/** + * Services provided by the HTTP router, which are available in the + * request context. + * + * @since 1.0.0 + * @category Request types + */ +export type Provided = + | HttpServerRequest.HttpServerRequest + | Scope.Scope + | HttpServerRequest.ParsedSearchParams + | RouteContext + +/** + * Services provided to global middleware. + * + * @since 1.0.0 + * @category Request types + */ +export type GlobalProvided = + | HttpServerRequest.HttpServerRequest + | Scope.Scope + +/** + * @since 1.0.0 + * @category Middleware + */ +export const MiddlewareTypeId: unique symbol = Symbol.for("@effect/platform/HttpLayerRouter/Middleware") + +/** + * @since 1.0.0 + * @category Middleware + */ +export type MiddlewareTypeId = typeof MiddlewareTypeId + +/** + * A pseudo-error type that represents an error that should be not handled by + * the middleware. + * + * @since 1.0.0 + * @category Middleware + */ +export interface unhandled { + readonly _: unique symbol +} + +/** + * @since 1.0.0 + * @category Middleware + */ +export interface Middleware< + Config extends { + provides: any + handles: any + error: any + requires: any + layerError: any + layerRequires: any + } +> { + readonly [MiddlewareTypeId]: Config + + readonly layer: [Config["requires"]] extends [never] ? Layer.Layer< + Request.From<"Requires", Config["provides"]>, + Config["layerError"], + | Config["layerRequires"] + | Request.From<"Requires", Config["requires"]> + | Request.From<"Error", Config["error"]> + > + : "Need to .combine(middleware) that satisfy the missing request dependencies" + + readonly combine: < + Config2 extends { + provides: any + handles: any + error: any + requires: any + layerError: any + layerRequires: any + } + >(other: Middleware) => Middleware<{ + provides: Config2["provides"] | Config["provides"] + handles: Config2["handles"] | Config["handles"] + error: Config2["error"] | Exclude + requires: Exclude | Config2["requires"] + layerError: Config["layerError"] | Config2["layerError"] + layerRequires: Config["layerRequires"] | Config2["layerRequires"] + }> +} + +/** + * Create a middleware layer that can be used to modify requests and responses. + * + * By default, the middleware only affects the routes that it is provided to. + * + * If you want to create a middleware that applies globally to all routes, pass + * the `global` option as `true`. + * + * ```ts + * import * as HttpLayerRouter from "@effect/platform/HttpLayerRouter" + * import * as HttpMiddleware from "@effect/platform/HttpMiddleware" + * import * as HttpServerResponse from "@effect/platform/HttpServerResponse" + * import * as Context from "effect/Context" + * import * as Effect from "effect/Effect" + * import * as Layer from "effect/Layer" + * + * // Here we are defining a CORS middleware + * const CorsMiddleware = HttpLayerRouter.middleware(HttpMiddleware.cors()).layer + * // You can also use HttpLayerRouter.cors() to create a CORS middleware + * + * class CurrentSession extends Context.Tag("CurrentSession")() {} + * + * // You can create middleware that provides a service to the HTTP requests. + * const SessionMiddleware = HttpLayerRouter.middleware<{ + * provides: CurrentSession + * }>()( + * Effect.gen(function*() { + * yield* Effect.log("SessionMiddleware initialized") + * + * return (httpEffect) => + * Effect.provideService(httpEffect, CurrentSession, { + * token: "dummy-token" + * }) + * }) + * ).layer + * + * Effect.gen(function*() { + * const router = yield* HttpLayerRouter.HttpRouter + * yield* router.add( + * "GET", + * "/hello", + * Effect.gen(function*() { + * // Requests can now access the current session + * const session = yield* CurrentSession + * return HttpServerResponse.text(`Hello, World! Your token is ${session.token}`) + * }) + * ) + * }).pipe( + * Layer.effectDiscard, + * // Provide the SessionMiddleware & CorsMiddleware to some routes + * Layer.provide([SessionMiddleware, CorsMiddleware]) + * ) + * ``` + * + * @since 1.0.0 + * @category Middleware + */ +export const middleware: + & middleware.Make + & (< + Config extends { + provides?: any + handles?: any + } = {} + >() => middleware.Make< + Config extends { provides: infer R } ? R : never, + Config extends { handles: infer E } ? E : never + >) = function() { + if (arguments.length === 0) { + return makeMiddleware as any + } + return makeMiddleware(arguments[0], arguments[1]) as any + } + +const makeMiddleware = (middleware: any, options?: { + readonly global?: boolean | undefined +}) => + options?.global ? + Layer.scopedDiscard(Effect.gen(function*() { + const router = yield* HttpRouter + const fn = Effect.isEffect(middleware) ? yield* middleware : middleware + yield* router.addGlobalMiddleware(fn) + })) + : new MiddlewareImpl( + Effect.isEffect(middleware) ? + Layer.scopedContext(Effect.map(middleware, (fn) => Context.unsafeMake(new Map([[fnContextKey, fn]])))) : + Layer.succeedContext(Context.unsafeMake(new Map([[fnContextKey, middleware]]))) as any + ) + +let middlewareId = 0 +const fnContextKey = "@effect/platform/HttpLayerRouter/MiddlewareFn" + +class MiddlewareImpl< + Config extends { + provides: any + handles: any + error: any + requires: any + layerError: any + layerRequires: any + } +> implements Middleware { + readonly [MiddlewareTypeId]: Config = {} as any + + constructor( + readonly layerFn: Layer.Layer, + readonly dependencies?: Layer.Layer + ) { + const contextKey = `@effect/platform/HttpLayerRouter/Middleware-${++middlewareId}` as const + this.layer = Layer.scopedContext(Effect.gen(this, function*() { + const context = yield* Effect.context() + const stack = [context.unsafeMap.get(fnContextKey)] + if (this.dependencies) { + const memoMap = yield* Layer.CurrentMemoMap + const scope = Context.get(context, Scope.Scope) + const depsContext = yield* Layer.buildWithMemoMap(this.dependencies, memoMap, scope) + // eslint-disable-next-line no-restricted-syntax + stack.push(...getMiddleware(depsContext)) + } + return Context.unsafeMake(new Map([[contextKey, stack]])) + })).pipe(Layer.provide(this.layerFn)) + } + + layer: any + + combine< + Config2 extends { + provides: any + handles: any + error: any + requires: any + layerError: any + layerRequires: any + } + >(other: Middleware): Middleware { + return new MiddlewareImpl( + this.layerFn, + this.dependencies ? Layer.provideMerge(this.dependencies, other.layer as any) : other.layer as any + ) as any + } +} + +const middlewareCache = new WeakMap, any>() +const getMiddleware = (context: Context.Context): Array => { + let arr = middlewareCache.get(context) + if (arr) return arr + const topLevel = Arr.empty>() + let maxLength = 0 + for (const [key, value] of context.unsafeMap) { + if (key.startsWith("@effect/platform/HttpLayerRouter/Middleware-")) { + topLevel.push(value) + if (value.length > maxLength) { + maxLength = value.length + } + } + } + if (topLevel.length === 0) { + arr = [] + } else { + const middleware = new Set() + for (let i = maxLength - 1; i >= 0; i--) { + for (const arr of topLevel) { + if (i < arr.length) { + middleware.add(arr[i]) + } + } + } + arr = Arr.fromIterable(middleware).reverse() + } + middlewareCache.set(context, arr) + return arr +} + +/** + * @since 1.0.0 + * @category Middleware + */ +export declare namespace middleware { + /** + * @since 1.0.0 + * @category Middleware + */ + export type Make = { + ( + middleware: Effect.Effect< + ( + effect: Effect.Effect< + HttpServerResponse.HttpServerResponse, + Types.NoInfer, + Types.NoInfer + > + ) => + & Effect.Effect< + HttpServerResponse.HttpServerResponse, + E, + R + > + & (unhandled extends E ? unknown : "You must only handle the configured errors"), + EX, + RX + >, + options?: { + readonly global?: Global | undefined + } + ): Global extends true ? Layer.Layer< + | Request.From<"Requires", Provides> + | Request.From<"Error", Handles> + | Request.From<"GlobalRequires", Provides> + | Request.From<"GlobalError", Handles>, + EX, + | HttpRouter + | Exclude + | Request.From<"GlobalRequires", Exclude> + | Request.From<"GlobalError", Exclude> + > : + Middleware<{ + provides: Provides + handles: Handles + error: Exclude + requires: Exclude + layerError: EX + layerRequires: Exclude + }> + ( + middleware: + & (( + effect: Effect.Effect< + HttpServerResponse.HttpServerResponse, + Types.NoInfer, + Types.NoInfer + > + ) => Effect.Effect< + HttpServerResponse.HttpServerResponse, + E, + R + >) + & (unhandled extends E ? unknown : "You must only handle the configured errors"), + options?: { + readonly global?: Global | undefined + } + ): Global extends true ? Layer.Layer< + | Request.From<"Requires", Provides> + | Request.From<"Error", Handles> + | Request.From<"GlobalRequires", Provides> + | Request.From<"GlobalError", Handles>, + never, + | HttpRouter + | Request.From<"GlobalRequires", Exclude> + | Request.From<"GlobalError", Exclude> + > : + Middleware<{ + provides: Provides + handles: Handles + error: Exclude + requires: Exclude + layerError: never + layerRequires: never + }> + } + + /** + * @since 1.0.0 + * @category Middleware + */ + export type Fn = ( + effect: Effect.Effect + ) => Effect.Effect +} + +/** + * A middleware that applies CORS headers to the HTTP response. + * + * @since 1.0.0 + * @category Middleware + */ +export const cors = ( + options?: { + readonly allowedOrigins?: ReadonlyArray | Predicate.Predicate | undefined + readonly allowedMethods?: ReadonlyArray | undefined + readonly allowedHeaders?: ReadonlyArray | undefined + readonly exposedHeaders?: ReadonlyArray | undefined + readonly maxAge?: number | undefined + readonly credentials?: boolean | undefined + } | undefined +): Layer.Layer => middleware(HttpMiddleware.cors(options), { global: true }) + +/** + * A middleware that disables the logger for some routes. + * + * ```ts + * import * as HttpLayerRouter from "@effect/platform/HttpLayerRouter" + * import * as HttpServerResponse from "@effect/platform/HttpServerResponse" + * import * as Layer from "effect/Layer" + * + * const Route = HttpLayerRouter.add("GET", "/hello", HttpServerResponse.text("Hello, World!")).pipe( + * // disable the logger for this route + * Layer.provide(HttpLayerRouter.disableLogger) + * ) + * ``` + * + * @since 1.0.0 + * @category Middleware + */ +export const disableLogger: Layer.Layer = middleware(HttpMiddleware.withLoggerDisabled).layer + +/** + * ```ts + * import * as NodeHttpServer from "@effect/platform-node/NodeHttpServer" + * import * as NodeRuntime from "@effect/platform-node/NodeRuntime" + * import * as HttpApi from "@effect/platform/HttpApi" + * import * as HttpApiBuilder from "@effect/platform/HttpApiBuilder" + * import * as HttpApiEndpoint from "@effect/platform/HttpApiEndpoint" + * import * as HttpApiGroup from "@effect/platform/HttpApiGroup" + * import * as HttpApiScalar from "@effect/platform/HttpApiScalar" + * import * as HttpLayerRouter from "@effect/platform/HttpLayerRouter" + * import * as HttpMiddleware from "@effect/platform/HttpMiddleware" + * import * as Effect from "effect/Effect" + * import * as Layer from "effect/Layer" + * import { createServer } from "http" + * + * // First, we define our HttpApi + * class MyApi extends HttpApi.make("api").add( + * HttpApiGroup.make("users").add( + * HttpApiEndpoint.get("me", "/me") + * ).prefix("/users") + * ) {} + * + * // Implement the handlers for the API + * const UsersApiLayer = HttpApiBuilder.group(MyApi, "users", (handers) => handers.handle("me", () => Effect.void)) + * + * // Use `HttpLayerRouter.addHttpApi` to register the API with the router + * const HttpApiRoutes = HttpLayerRouter.addHttpApi(MyApi, { + * openapiPath: "/docs/openapi.json" + * }).pipe( + * // Provide the api handlers layer + * Layer.provide(UsersApiLayer) + * ) + * + * // Create a /docs route for the API documentation + * const DocsRoute = HttpApiScalar.layerHttpLayerRouter({ + * api: MyApi, + * path: "/docs" + * }) + * + * const CorsMiddleware = HttpLayerRouter.middleware(HttpMiddleware.cors()) + * // You can also use HttpLayerRouter.cors() to create a CORS middleware + * + * // Finally, we merge all routes and serve them using the Node HTTP server + * const AllRoutes = Layer.mergeAll( + * HttpApiRoutes, + * DocsRoute + * ).pipe( + * Layer.provide(CorsMiddleware.layer) + * ) + * + * HttpLayerRouter.serve(AllRoutes).pipe( + * Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 })), + * Layer.launch, + * NodeRuntime.runMain + * ) + * ``` + * + * @since 1.0.0 + * @category HttpApi + */ +export const addHttpApi = ( + api: HttpApi.HttpApi, + options?: { + readonly openapiPath?: `/${string}` | undefined + } +): Layer.Layer< + never, + never, + | Etag.Generator + | HttpRouter + | FileSystem + | HttpPlatform + | Path + | R + | HttpApiGroup.HttpApiGroup.ToService + | HttpApiGroup.HttpApiGroup.ErrorContext +> => { + const ApiMiddleware = middleware(HttpApiBuilder.buildMiddleware(api)).layer as Layer.Layer + return HttpApiBuilder.Router.unwrap(Effect.fnUntraced(function*(router_) { + const router = yield* HttpRouter + let existing = existingRoutesMap.get(router) + if (!existing) { + existing = new Set() + existingRoutesMap.set(router, existing) + } + const context = yield* Effect.context< + | Etag.Generator + | HttpRouter + | FileSystem + | HttpPlatform + | Path + >() + const routes = Arr.empty>() + for (const route of router_.routes) { + if (existing.has(route)) { + continue + } + existing.add(route) + routes.push(makeRoute({ + ...route as any, + handler: Effect.mapInputContext(route.handler, (input) => Context.merge(context, input)) + })) + } + + yield* (router.addAll(routes) as Effect.Effect) + + if (options?.openapiPath) { + const spec = OpenApi.fromApi(api) + yield* router.add("GET", options.openapiPath, Effect.succeed(HttpServerResponse.unsafeJson(spec))) + } + }, Layer.effectDiscard)).pipe( + Layer.provide(ApiMiddleware) + ) +} + +const existingRoutesMap = new WeakMap>() + +/** + * Serves the provided application layer as an HTTP server. + * + * @since 1.0.0 + * @category Server + */ +export const serve = | Request.Only<"GlobalRequires", R>>( + appLayer: Layer.Layer, + options?: { + readonly routerConfig?: Partial | undefined + readonly disableLogger?: boolean | undefined + readonly disableListenLog?: boolean + /** + * Middleware to apply to the HTTP server. + * + * NOTE: This middleware is applied to the entire HTTP server chain, + * including the sending of the response. This means that modifications + * to the response **WILL NOT** be reflected in the final response sent to the + * client. + * + * Use HttpLayerRouter.middleware to create middleware that can modify the + * response. + */ + readonly middleware?: ( + effect: Effect.Effect< + HttpServerResponse.HttpServerResponse, + Request.Only<"Error", R> | Request.Only<"GlobalError", R> | HttpServerError.RouteNotFound, + | Scope.Scope + | HttpServerRequest.HttpServerRequest + | Request.Only<"Requires", R> + | Request.Only<"GlobalRequires", R> + > + ) => Effect.Effect + } +): Layer.Layer< + A, + Request.Without, + HttpServer.HttpServer | Exclude | Exclude, HttpRouter> +> => { + let middleware: any = options?.middleware + if (options?.disableLogger !== true) { + middleware = middleware ? compose(middleware, HttpMiddleware.logger) : HttpMiddleware.logger + } + const RouterLayer = options?.routerConfig + ? Layer.provide(layer, Layer.succeed(RouterConfig, options.routerConfig)) + : layer + return Effect.gen(function*() { + const router = yield* HttpRouter + const handler = router.asHttpEffect() + return middleware ? HttpServer.serve(handler, middleware) : HttpServer.serve(handler) + }).pipe( + Layer.unwrapScoped, + Layer.provideMerge(appLayer), + Layer.provide(RouterLayer), + options?.disableListenLog ? identity : HttpServer.withLogAddress + ) as any +} + +/** + * @since 1.0.0 + * @category Server + */ +export const toWebHandler = < + A, + E, + R extends + | HttpRouter + | Request<"Requires", any> + | Request<"GlobalRequires", any> + | Request<"Error", any> + | Request<"GlobalError", any>, + HE, + HR = Exclude | Request.Only<"GlobalRequires", R>, A> +>( + appLayer: Layer.Layer, + options?: { + readonly memoMap?: Layer.MemoMap | undefined + readonly routerConfig?: Partial | undefined + readonly disableLogger?: boolean | undefined + /** + * Middleware to apply to the HTTP server. + * + * NOTE: This middleware is applied to the entire HTTP server chain, + * including the sending of the response. This means that modifications + * to the response **WILL NOT** be reflected in the final response sent to the + * client. + * + * Use HttpLayerRouter.middleware to create middleware that can modify the + * response. + */ + readonly middleware?: ( + effect: Effect.Effect< + HttpServerResponse.HttpServerResponse, + Request.Only<"Error", R> | Request.Only<"GlobalError", R> | HttpServerError.RouteNotFound, + | Scope.Scope + | HttpServerRequest.HttpServerRequest + | Request.Only<"Requires", R> + | Request.Only<"GlobalRequires", R> + > + ) => Effect.Effect + } +): { + readonly handler: [HR] extends [never] + ? ((request: globalThis.Request, context?: Context.Context | undefined) => Promise) + : ((request: globalThis.Request, context: Context.Context
) => Promise) + readonly dispose: () => Promise +} => { + let middleware: any = options?.middleware + if (options?.disableLogger !== true) { + middleware = middleware ? compose(middleware, HttpMiddleware.logger) : HttpMiddleware.logger + } + const RouterLayer: Layer.Layer = Layer.provideMerge( + appLayer, + options?.routerConfig + ? Layer.provide(layer, Layer.succeed(RouterConfig, options.routerConfig)) + : layer + ) as any + return HttpApp.toWebHandlerLayerWith(RouterLayer, { + toHandler: (r) => Effect.succeed(Context.get(r.context, HttpRouter).asHttpEffect()), + middleware, + memoMap: options?.memoMap + }) +} diff --git a/repos/effect/packages/platform/src/HttpMethod.ts b/repos/effect/packages/platform/src/HttpMethod.ts new file mode 100644 index 0000000..e071ece --- /dev/null +++ b/repos/effect/packages/platform/src/HttpMethod.ts @@ -0,0 +1,61 @@ +/** + * @since 1.0.0 + * @category models + */ +export type HttpMethod = + | "GET" + | "POST" + | "PUT" + | "DELETE" + | "PATCH" + | "HEAD" + | "OPTIONS" + +/** + * @since 1.0.0 + * @category models + */ +export declare namespace HttpMethod { + /** + * @since 1.0.0 + * @category models + */ + export type NoBody = "GET" | "HEAD" | "OPTIONS" + + /** + * @since 1.0.0 + * @category models + */ + export type WithBody = Exclude +} + +/** + * @since 1.0.0 + */ +export const hasBody = (method: HttpMethod): boolean => method !== "GET" && method !== "HEAD" && method !== "OPTIONS" + +/** + * @since 1.0.0 + */ +export const all: ReadonlySet = new Set(["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"]) + +/** + * Tests if a value is a `HttpMethod`. + * + * **Example** + * + * ```ts + * import { HttpMethod } from "@effect/platform" + * + * console.log(HttpMethod.isHttpMethod("GET")) + * // true + * console.log(HttpMethod.isHttpMethod("get")) + * // false + * console.log(HttpMethod.isHttpMethod(1)) + * // false + * ``` + * + * @since 1.0.0 + * @category refinements + */ +export const isHttpMethod = (u: unknown): u is HttpMethod => all.has(u as HttpMethod) diff --git a/repos/effect/packages/platform/src/HttpMiddleware.ts b/repos/effect/packages/platform/src/HttpMiddleware.ts new file mode 100644 index 0000000..707318a --- /dev/null +++ b/repos/effect/packages/platform/src/HttpMiddleware.ts @@ -0,0 +1,234 @@ +/** + * @since 1.0.0 + */ +import type * as Context from "effect/Context" +import type * as Effect from "effect/Effect" +import type * as FiberRef from "effect/FiberRef" +import type * as Layer from "effect/Layer" +import type * as Predicate from "effect/Predicate" +import type * as App from "./HttpApp.js" +import type * as ServerRequest from "./HttpServerRequest.js" +import * as internal from "./internal/httpMiddleware.js" + +/** + * @since 1.0.0 + * @category models + */ +export interface HttpMiddleware { + (self: App.Default): App.Default +} + +/** + * @since 1.0.0 + */ +export declare namespace HttpMiddleware { + /** + * @since 1.0.0 + */ + export interface Applied
, E, R> { + (self: App.Default): A + } +} + +/** + * @since 1.0.0 + * @category constructors + */ +export const make: (middleware: M) => M = internal.make + +/** + * @since 1.0.0 + * @category constructors + */ +export const logger: (httpApp: App.Default) => App.Default = internal.logger + +/** + * @since 1.0.0 + * @category fiber refs + */ +export const loggerDisabled: FiberRef.FiberRef = internal.loggerDisabled + +/** + * @since 1.0.0 + * @category fiber refs + */ +export const withLoggerDisabled: (self: Effect.Effect) => Effect.Effect = + internal.withLoggerDisabled + +/** + * @since 1.0.0 + * @category fiber refs + */ +export const currentTracerDisabledWhen: FiberRef.FiberRef> = + internal.currentTracerDisabledWhen + +/** + * @since 1.0.0 + * @category fiber refs + */ +export const withTracerDisabledWhen: { + ( + predicate: Predicate.Predicate + ): (layer: Layer.Layer) => Layer.Layer + ( + layer: Layer.Layer, + predicate: Predicate.Predicate + ): Layer.Layer +} = internal.withTracerDisabledWhen + +/** + * @since 1.0.0 + * @category fiber refs + */ +export const withTracerDisabledWhenEffect: { + ( + predicate: Predicate.Predicate + ): (effect: Effect.Effect) => Effect.Effect + ( + effect: Effect.Effect, + predicate: Predicate.Predicate + ): Effect.Effect +} = internal.withTracerDisabledWhenEffect + +/** + * @since 1.0.0 + * @category fiber refs + */ +export const withTracerDisabledForUrls: { + (urls: ReadonlyArray): (layer: Layer.Layer) => Layer.Layer + (layer: Layer.Layer, urls: ReadonlyArray): Layer.Layer +} = internal.withTracerDisabledForUrls + +/** + * @since 1.0.0 + * @category constructors + */ +export const xForwardedHeaders: (httpApp: App.Default) => App.Default = internal.xForwardedHeaders + +/** + * @since 1.0.0 + * @category constructors + */ +export const searchParamsParser: ( + httpApp: App.Default +) => App.Default< + E, + Exclude +> = internal.searchParamsParser + +/** + * Creates a CORS (Cross-Origin Resource Sharing) middleware for HTTP applications. + * + * @param options - CORS configuration options + * @param options.allowedOrigins - Origins allowed to access the resource. Can be: + * - An array of origin strings (e.g., `["https://example.com", "https://api.example.com"]`) + * - A predicate function to dynamically allow origins + * - If empty array (default): allows all origins with `Access-Control-Allow-Origin: *` + * @param options.allowedMethods - HTTP methods allowed for CORS requests. + * Default: `["GET", "HEAD", "PUT", "PATCH", "POST", "DELETE"]` + * @param options.allowedHeaders - Headers allowed in CORS requests. **Important behavior**: + * - If empty array (default): reflects back the client's `Access-Control-Request-Headers`, + * effectively allowing all headers requested by the client + * - If non-empty array: only the specified headers are allowed + * - This means the default behavior is permissive, not restrictive + * @param options.exposedHeaders - Headers exposed to the client in the response. + * Default: `[]` + * @param options.maxAge - Maximum time (in seconds) that preflight request results can be cached. + * If not specified, no `Access-Control-Max-Age` header is sent + * @param options.credentials - Whether to allow credentials (cookies, authorization headers, etc.). + * Default: `false` + * + * @example + * ```ts + * import { HttpMiddleware, HttpRouter, HttpServerResponse } from "@effect/platform" + * + * // Allow all origins and reflect requested headers (default behavior) + * HttpRouter.empty.pipe( + * HttpRouter.get("/", HttpServerResponse.empty()), + * HttpMiddleware.cors() + * ) + * + * // Restrict to specific origins and headers + * HttpRouter.empty.pipe( + * HttpRouter.get("/", HttpServerResponse.empty()), + * HttpMiddleware.cors({ + * allowedOrigins: ["https://example.com"], + * allowedHeaders: ["Content-Type", "Authorization"], + * credentials: true + * }) + * ) + * + * // Dynamic origin checking with predicate + * HttpRouter.empty.pipe( + * HttpRouter.get("/", HttpServerResponse.empty()), + * HttpMiddleware.cors({ + * allowedOrigins: (origin) => origin.endsWith(".example.com") + * }) + * ) + * ``` + * + * @since 1.0.0 + * @category constructors + */ +export const cors: ( + options?: { + readonly allowedOrigins?: ReadonlyArray | Predicate.Predicate | undefined + readonly allowedMethods?: ReadonlyArray | undefined + readonly allowedHeaders?: ReadonlyArray | undefined + readonly exposedHeaders?: ReadonlyArray | undefined + readonly maxAge?: number | undefined + readonly credentials?: boolean | undefined + } | undefined +) => (httpApp: App.Default) => App.Default = internal.cors + +/** + * @since 1.0.0 + * @category Tracing + */ +export interface SpanNameGenerator { + readonly _: unique symbol +} + +/** + * @since 1.0.0 + * @category Tracing + */ +export const SpanNameGenerator: Context.Reference< + SpanNameGenerator, + (request: ServerRequest.HttpServerRequest) => string +> = internal.SpanNameGenerator + +/** + * Customizes the span name for the http app. + * + * ```ts + * import { + * HttpMiddleware, + * HttpRouter, + * HttpServer, + * HttpServerResponse + * } from "@effect/platform" + * import { NodeHttpServer, NodeRuntime } from "@effect/platform-node" + * import { Layer } from "effect" + * import { createServer } from "http" + * + * HttpRouter.empty.pipe( + * HttpRouter.get("/", HttpServerResponse.empty()), + * HttpServer.serve(), + * // Customize the span names for this HttpApp + * HttpMiddleware.withSpanNameGenerator((request) => `GET ${request.url}`), + * Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 })), + * Layer.launch, + * NodeRuntime.runMain + * ) + * ``` + * + * @since 1.0.0 + * @category Tracing + */ +export const withSpanNameGenerator: { + ( + f: (request: ServerRequest.HttpServerRequest) => string + ): (layer: Layer.Layer) => Layer.Layer + (layer: Layer.Layer, f: (request: ServerRequest.HttpServerRequest) => string): Layer.Layer +} = internal.withSpanNameGenerator diff --git a/repos/effect/packages/platform/src/HttpMultiplex.ts b/repos/effect/packages/platform/src/HttpMultiplex.ts new file mode 100644 index 0000000..823e912 --- /dev/null +++ b/repos/effect/packages/platform/src/HttpMultiplex.ts @@ -0,0 +1,178 @@ +/** + * @since 1.0.0 + */ +import type * as Effect from "effect/Effect" +import type { Inspectable } from "effect/Inspectable" +import type * as App from "./HttpApp.js" +import type * as Error from "./HttpServerError.js" +import type * as ServerRequest from "./HttpServerRequest.js" +import * as internal from "./internal/httpMultiplex.js" + +/** + * @since 1.0.0 + * @category type ids + */ +export const TypeId: unique symbol = internal.TypeId + +/** + * @since 1.0.0 + * @category type ids + */ +export type TypeId = typeof TypeId + +/** + * @since 1.0.0 + * @category models + */ +export interface HttpMultiplex extends App.Default, Inspectable { + readonly [TypeId]: TypeId + readonly apps: ReadonlyArray< + readonly [ + predicate: (request: ServerRequest.HttpServerRequest) => Effect.Effect, + app: App.Default + ] + > +} + +/** + * @since 1.0.0 + * @category constructors + */ +export const empty: HttpMultiplex = internal.empty + +/** + * @since 1.0.0 + * @category constructors + */ +export const make: ( + apps: Iterable< + readonly [ + predicate: (request: ServerRequest.HttpServerRequest) => Effect.Effect, + app: App.Default + ] + > +) => HttpMultiplex = internal.make + +/** + * @since 1.0.0 + * @category combinators + */ +export const add: { + ( + predicate: (request: ServerRequest.HttpServerRequest) => Effect.Effect, + app: App.Default + ): (self: HttpMultiplex) => HttpMultiplex + ( + self: HttpMultiplex, + predicate: (request: ServerRequest.HttpServerRequest) => Effect.Effect, + app: App.Default + ): HttpMultiplex +} = internal.add + +/** + * @since 1.0.0 + * @category combinators + */ +export const headerExact: { + ( + header: string, + value: string, + app: App.Default + ): (self: HttpMultiplex) => HttpMultiplex + ( + self: HttpMultiplex, + header: string, + value: string, + app: App.Default + ): HttpMultiplex +} = internal.headerExact + +/** + * @since 1.0.0 + * @category combinators + */ +export const headerRegex: { + ( + header: string, + regex: RegExp, + app: App.Default + ): (self: HttpMultiplex) => HttpMultiplex + ( + self: HttpMultiplex, + header: string, + regex: RegExp, + app: App.Default + ): HttpMultiplex +} = internal.headerRegex + +/** + * @since 1.0.0 + * @category combinators + */ +export const headerStartsWith: { + ( + header: string, + prefix: string, + app: App.Default + ): (self: HttpMultiplex) => HttpMultiplex + ( + self: HttpMultiplex, + header: string, + prefix: string, + app: App.Default + ): HttpMultiplex +} = internal.headerStartsWith + +/** + * @since 1.0.0 + * @category combinators + */ +export const headerEndsWith: { + ( + header: string, + suffix: string, + app: App.Default + ): (self: HttpMultiplex) => HttpMultiplex + ( + self: HttpMultiplex, + header: string, + suffix: string, + app: App.Default + ): HttpMultiplex +} = internal.headerEndsWith + +/** + * @since 1.0.0 + * @category combinators + */ +export const hostExact: { + (host: string, app: App.Default): (self: HttpMultiplex) => HttpMultiplex + (self: HttpMultiplex, host: string, app: App.Default): HttpMultiplex +} = internal.hostExact + +/** + * @since 1.0.0 + * @category combinators + */ +export const hostRegex: { + (regex: RegExp, app: App.Default): (self: HttpMultiplex) => HttpMultiplex + (self: HttpMultiplex, regex: RegExp, app: App.Default): HttpMultiplex +} = internal.hostRegex + +/** + * @since 1.0.0 + * @category combinators + */ +export const hostStartsWith: { + (prefix: string, app: App.Default): (self: HttpMultiplex) => HttpMultiplex + (self: HttpMultiplex, prefix: string, app: App.Default): HttpMultiplex +} = internal.hostStartsWith + +/** + * @since 1.0.0 + * @category combinators + */ +export const hostEndsWith: { + (suffix: string, app: App.Default): (self: HttpMultiplex) => HttpMultiplex + (self: HttpMultiplex, suffix: string, app: App.Default): HttpMultiplex +} = internal.hostEndsWith diff --git a/repos/effect/packages/platform/src/HttpPlatform.ts b/repos/effect/packages/platform/src/HttpPlatform.ts new file mode 100644 index 0000000..fed43b1 --- /dev/null +++ b/repos/effect/packages/platform/src/HttpPlatform.ts @@ -0,0 +1,78 @@ +/** + * @since 1.0.0 + */ +import type * as Context from "effect/Context" +import type * as Effect from "effect/Effect" +import type { Layer } from "effect/Layer" +import type * as Error from "./Error.js" +import type * as Etag from "./Etag.js" +import type * as FileSystem from "./FileSystem.js" +import type * as Headers from "./Headers.js" +import type * as Body from "./HttpBody.js" +import type * as ServerResponse from "./HttpServerResponse.js" +import * as internal from "./internal/httpPlatform.js" + +/** + * @since 1.0.0 + * @category type ids + */ +export const TypeId: unique symbol = internal.TypeId + +/** + * @since 1.0.0 + * @category type ids + */ +export type TypeId = typeof TypeId + +/** + * @since 1.0.0 + * @category tags + */ +export const HttpPlatform: Context.Tag = internal.tag + +/** + * @since 1.0.0 + * @category models + */ +export interface HttpPlatform { + readonly [TypeId]: TypeId + readonly fileResponse: ( + path: string, + options?: ServerResponse.Options.WithContent & FileSystem.StreamOptions + ) => Effect.Effect + readonly fileWebResponse: ( + file: Body.HttpBody.FileLike, + options?: ServerResponse.Options.WithContent & FileSystem.StreamOptions + ) => Effect.Effect +} + +/** + * @since 1.0.0 + * @category constructors + */ +export const make: ( + impl: { + readonly fileResponse: ( + path: string, + status: number, + statusText: string | undefined, + headers: Headers.Headers, + start: number, + end: number | undefined, + contentLength: number + ) => ServerResponse.HttpServerResponse + readonly fileWebResponse: ( + file: Body.HttpBody.FileLike, + status: number, + statusText: string | undefined, + headers: Headers.Headers, + options?: FileSystem.StreamOptions | undefined + ) => ServerResponse.HttpServerResponse + } +) => Effect.Effect = internal.make + +/** + * @since 1.0.0 + * @category layers + */ +export const layer: Layer = internal.layer diff --git a/repos/effect/packages/platform/src/HttpRouter.ts b/repos/effect/packages/platform/src/HttpRouter.ts new file mode 100644 index 0000000..57006c8 --- /dev/null +++ b/repos/effect/packages/platform/src/HttpRouter.ts @@ -0,0 +1,840 @@ +/** + * @since 1.0.0 + */ +import type * as Cause from "effect/Cause" +import type * as Chunk from "effect/Chunk" +import type * as Context from "effect/Context" +import type * as Effect from "effect/Effect" +import type { FiberRef } from "effect/FiberRef" +import type { Inspectable } from "effect/Inspectable" +import type * as Layer from "effect/Layer" +import type * as Option from "effect/Option" +import type * as ParseResult from "effect/ParseResult" +import type * as Schema from "effect/Schema" +import type { ParseOptions } from "effect/SchemaAST" +import type * as Scope from "effect/Scope" +import type { RouterConfig } from "find-my-way-ts" +import type * as Etag from "./Etag.js" +import type { FileSystem } from "./FileSystem.js" +import type * as App from "./HttpApp.js" +import type * as Method from "./HttpMethod.js" +import type * as Middleware from "./HttpMiddleware.js" +import type * as Platform from "./HttpPlatform.js" +import type * as HttpServer from "./HttpServer.js" +import type * as Error from "./HttpServerError.js" +import type * as ServerRequest from "./HttpServerRequest.js" +import type * as Respondable from "./HttpServerRespondable.js" +import type * as ServerResponse from "./HttpServerResponse.js" +import * as internal from "./internal/httpRouter.js" +import type { Path } from "./Path.js" + +/** + * @since 1.0.0 + * @category type ids + */ +export const TypeId: unique symbol = internal.TypeId + +/** + * @since 1.0.0 + * @category type ids + */ +export type TypeId = typeof TypeId + +/** + * @since 1.0.0 + * @category models + */ +export interface HttpRouter + extends App.Default>, Inspectable +{ + readonly [TypeId]: TypeId + readonly routes: Chunk.Chunk> + readonly mounts: Chunk.Chunk< + readonly [ + prefix: string, + httpApp: App.Default, + options?: { readonly includePrefix?: boolean | undefined } | undefined + ] + > +} + +/** + * @since 1.0.0 + */ +export declare namespace HttpRouter { + /** + * @since 1.0.0 + */ + export type Provided = RouteContext | ServerRequest.HttpServerRequest | ServerRequest.ParsedSearchParams | Scope.Scope + + /** + * @since 1.0.0 + */ + export type ExcludeProvided = Exclude + + /** + * @since 1.0.0 + */ + export interface Service { + readonly router: Effect.Effect> + readonly addRoute: (route: Route) => Effect.Effect + readonly all: ( + path: PathInput, + handler: Route.Handler, + options?: { readonly uninterruptible?: boolean | undefined } | undefined + ) => Effect.Effect + readonly get: ( + path: PathInput, + handler: Route.Handler, + options?: { readonly uninterruptible?: boolean | undefined } | undefined + ) => Effect.Effect + readonly post: ( + path: PathInput, + handler: Route.Handler, + options?: { readonly uninterruptible?: boolean | undefined } | undefined + ) => Effect.Effect + readonly put: ( + path: PathInput, + handler: Route.Handler, + options?: { readonly uninterruptible?: boolean | undefined } | undefined + ) => Effect.Effect + readonly patch: ( + path: PathInput, + handler: Route.Handler, + options?: { readonly uninterruptible?: boolean | undefined } | undefined + ) => Effect.Effect + readonly del: ( + path: PathInput, + handler: Route.Handler, + options?: { readonly uninterruptible?: boolean | undefined } | undefined + ) => Effect.Effect + readonly head: ( + path: PathInput, + handler: Route.Handler, + options?: { readonly uninterruptible?: boolean | undefined } | undefined + ) => Effect.Effect + readonly options: ( + path: PathInput, + handler: Route.Handler, + options?: { readonly uninterruptible?: boolean | undefined } | undefined + ) => Effect.Effect + readonly mount: ( + path: `/${string}`, + router: HttpRouter + ) => Effect.Effect + readonly mountApp: ( + path: `/${string}`, + router: App.Default, + options?: { readonly includePrefix?: boolean | undefined } | undefined + ) => Effect.Effect + readonly concat: (router: HttpRouter) => Effect.Effect + } + + /** + * @since 1.0.0 + */ + export type DefaultServices = Platform.HttpPlatform | Etag.Generator | FileSystem | Path + + /** + * @since 1.0.0 + */ + export interface TagClass extends Context.Tag> { + new(_: never): Context.TagClassShape> + readonly Live: Layer.Layer + readonly router: Effect.Effect, never, Self> + readonly use: ( + f: (router: Service) => Effect.Effect + ) => Layer.Layer> + readonly unwrap: (f: (router: HttpRouter) => Layer.Layer) => Layer.Layer + readonly serve: ( + middleware?: Middleware.HttpMiddleware.Applied + ) => Layer.Layer< + never, + never, + HttpServer.HttpServer | Exclude + > + } +} + +/** + * @since 1.0.0 + * @category type ids + */ +export const RouteTypeId: unique symbol = internal.RouteTypeId + +/** + * @since 1.0.0 + * @category type ids + */ +export type RouteTypeId = typeof RouteTypeId + +/** + * @since 1.0.0 + * @category models + */ +export type PathInput = `/${string}` | "*" + +/** + * @since 1.0.0 + * @category models + */ +export interface Route extends Inspectable { + readonly [RouteTypeId]: RouteTypeId + readonly method: Method.HttpMethod | "*" + readonly path: PathInput + readonly handler: Route.Handler + readonly prefix: Option.Option + readonly uninterruptible: boolean +} + +/** + * @since 1.0.0 + */ +export declare namespace Route { + /** + * @since 1.0.0 + */ + export type Handler = App.HttpApp< + Respondable.Respondable, + E, + R | RouteContext | ServerRequest.ParsedSearchParams + > + + /** + * @since 1.0.0 + */ + export type Middleware = App.HttpApp< + ServerResponse.HttpServerResponse, + E, + R | RouteContext | ServerRequest.ParsedSearchParams + > +} + +/** + * @since 1.0.0 + * @category type ids + */ +export const RouteContextTypeId: unique symbol = internal.RouteContextTypeId + +/** + * @since 1.0.0 + * @category type ids + */ +export type RouteContextTypeId = typeof RouteContextTypeId + +/** + * @since 1.0.0 + * @category models + */ +export interface RouteContext { + readonly [RouteContextTypeId]: RouteContextTypeId + readonly params: Readonly> + readonly route: Route +} + +/** + * @since 1.0.0 + * @category route context + */ +export const RouteContext: Context.Tag = internal.RouteContext + +/** + * @since 1.0.0 + * @category route context + */ +export const params: Effect.Effect< + Readonly>, + never, + RouteContext +> = internal.params + +/** + * @since 1.0.0 + * @category route context + */ +export const schemaJson: < + R, + I extends Partial<{ + readonly method: Method.HttpMethod + readonly url: string + readonly cookies: Readonly> + readonly headers: Readonly> + readonly pathParams: Readonly> + readonly searchParams: Readonly | undefined>> + readonly body: any + }>, + A +>( + schema: Schema.Schema, + options?: ParseOptions | undefined +) => Effect.Effect< + A, + Error.RequestError | ParseResult.ParseError, + RouteContext | R | ServerRequest.HttpServerRequest | ServerRequest.ParsedSearchParams +> = internal.schemaJson + +/** + * @since 1.0.0 + * @category route context + */ +export const schemaNoBody: < + R, + I extends Partial< + { + readonly method: Method.HttpMethod + readonly url: string + readonly cookies: Readonly> + readonly headers: Readonly> + readonly pathParams: Readonly> + readonly searchParams: Readonly | undefined>> + } + >, + A +>( + schema: Schema.Schema, + options?: ParseOptions | undefined +) => Effect.Effect< + A, + ParseResult.ParseError, + R | RouteContext | ServerRequest.HttpServerRequest | ServerRequest.ParsedSearchParams +> = internal.schemaNoBody + +/** + * @since 1.0.0 + * @category route context + */ +export const schemaParams: | undefined>>, R>( + schema: Schema.Schema, + options?: ParseOptions | undefined +) => Effect.Effect = + internal.schemaParams + +/** + * @since 1.0.0 + * @category route context + */ +export const schemaPathParams: >, R>( + schema: Schema.Schema, + options?: ParseOptions | undefined +) => Effect.Effect = internal.schemaPathParams + +/** + * @since 1.0.0 + * @category router config + */ +export const currentRouterConfig: FiberRef> = internal.currentRouterConfig + +/** + * @since 1.0.0 + * @category router config + */ +export const withRouterConfig: { + (config: Partial): (effect: Effect.Effect) => Effect.Effect + (effect: Effect.Effect, config: Partial): Effect.Effect +} = internal.withRouterConfig + +/** + * @since 1.0.0 + * @category router config + */ +export const setRouterConfig: (config: Partial) => Layer.Layer = internal.setRouterConfig + +/** + * @since 1.0.0 + * @category constructors + */ +export const empty: HttpRouter = internal.empty + +/** + * @since 1.0.0 + * @category constructors + */ +export const fromIterable: >( + routes: Iterable +) => HttpRouter ? E : never, R extends Route ? Env : never> = + internal.fromIterable + +/** + * @since 1.0.0 + * @category constructors + */ +export const makeRoute: ( + method: Method.HttpMethod | "*", + path: PathInput, + handler: Route.Handler, + options?: { readonly prefix?: string | undefined; readonly uninterruptible?: boolean | undefined } | undefined +) => Route> = internal.makeRoute + +/** + * @since 1.0.0 + * @category utils + */ +export const prefixPath: { + (prefix: string): (self: string) => string + (self: string, prefix: string): string +} = internal.prefixPath + +/** + * @since 1.0.0 + * @category combinators + */ +export const prefixAll: { + (prefix: PathInput): (self: HttpRouter) => HttpRouter + (self: HttpRouter, prefix: PathInput): HttpRouter +} = internal.prefixAll + +/** + * @since 1.0.0 + * @category combinators + */ +export const append: { + ( + route: Route + ): ( + self: HttpRouter + ) => HttpRouter< + E1 | E, + R | HttpRouter.ExcludeProvided + > + ( + self: HttpRouter, + route: Route + ): HttpRouter< + E | E1, + R | HttpRouter.ExcludeProvided + > +} = internal.append + +/** + * @since 1.0.0 + * @category combinators + */ +export const concat: { + (that: HttpRouter): ( + self: HttpRouter + ) => HttpRouter + (self: HttpRouter, that: HttpRouter): HttpRouter< + E | E1, + R | R1 + > +} = internal.concat + +/** + * @since 1.0.0 + * @category combinators + */ +export const concatAll: >>( + ...routers: Routers +) => [Routers[number]] extends [HttpRouter] ? HttpRouter : never = internal.concatAll + +/** + * @since 1.0.0 + * @category routing + */ +export const mount: { + (path: `/${string}`, that: HttpRouter): (self: HttpRouter) => HttpRouter + (self: HttpRouter, path: `/${string}`, that: HttpRouter): HttpRouter +} = internal.mount + +/** + * @since 1.0.0 + * @category routing + */ +export const mountApp: { + ( + path: `/${string}`, + that: App.Default, + options?: { readonly includePrefix?: boolean | undefined } | undefined + ): ( + self: HttpRouter + ) => HttpRouter< + E1 | E, + | HttpRouter.ExcludeProvided + | HttpRouter.ExcludeProvided + > + ( + self: HttpRouter, + path: `/${string}`, + that: App.Default, + options?: { readonly includePrefix?: boolean | undefined } | undefined + ): HttpRouter< + E | E1, + | HttpRouter.ExcludeProvided + | HttpRouter.ExcludeProvided + > +} = internal.mountApp + +/** + * @since 1.0.0 + * @category routing + */ +export const route: ( + method: Method.HttpMethod | "*" +) => { + ( + path: PathInput, + handler: Route.Handler, + options?: { readonly uninterruptible?: boolean | undefined } | undefined + ): ( + self: HttpRouter + ) => HttpRouter> + ( + self: HttpRouter, + path: PathInput, + handler: Route.Handler, + options?: { readonly uninterruptible?: boolean | undefined } | undefined + ): HttpRouter> +} = internal.route + +/** + * @since 1.0.0 + * @category routing + */ +export const all: { + ( + path: PathInput, + handler: Route.Handler, + options?: { readonly uninterruptible?: boolean | undefined } | undefined + ): ( + self: HttpRouter + ) => HttpRouter< + E1 | E, + R | HttpRouter.ExcludeProvided + > + ( + self: HttpRouter, + path: PathInput, + handler: Route.Handler, + options?: { readonly uninterruptible?: boolean | undefined } | undefined + ): HttpRouter< + E | E1, + R | HttpRouter.ExcludeProvided + > +} = internal.all + +/** + * @since 1.0.0 + * @category routing + */ +export const get: { + ( + path: PathInput, + handler: Route.Handler, + options?: { readonly uninterruptible?: boolean | undefined } | undefined + ): ( + self: HttpRouter + ) => HttpRouter> + ( + self: HttpRouter, + path: PathInput, + handler: Route.Handler, + options?: { readonly uninterruptible?: boolean | undefined } | undefined + ): HttpRouter> +} = internal.get + +/** + * @since 1.0.0 + * @category routing + */ +export const post: { + ( + path: PathInput, + handler: Route.Handler, + options?: { readonly uninterruptible?: boolean | undefined } | undefined + ): ( + self: HttpRouter + ) => HttpRouter> + ( + self: HttpRouter, + path: PathInput, + handler: Route.Handler, + options?: { readonly uninterruptible?: boolean | undefined } | undefined + ): HttpRouter> +} = internal.post + +/** + * @since 1.0.0 + * @category routing + */ +export const patch: { + ( + path: PathInput, + handler: Route.Handler, + options?: { readonly uninterruptible?: boolean | undefined } | undefined + ): ( + self: HttpRouter + ) => HttpRouter> + ( + self: HttpRouter, + path: PathInput, + handler: Route.Handler, + options?: { readonly uninterruptible?: boolean | undefined } | undefined + ): HttpRouter> +} = internal.patch + +/** + * @since 1.0.0 + * @category routing + */ +export const put: { + ( + path: PathInput, + handler: Route.Handler, + options?: { readonly uninterruptible?: boolean | undefined } | undefined + ): ( + self: HttpRouter + ) => HttpRouter> + ( + self: HttpRouter, + path: PathInput, + handler: Route.Handler, + options?: { readonly uninterruptible?: boolean | undefined } | undefined + ): HttpRouter> +} = internal.put + +/** + * @since 1.0.0 + * @category routing + */ +export const del: { + ( + path: PathInput, + handler: Route.Handler, + options?: { readonly uninterruptible?: boolean | undefined } | undefined + ): ( + self: HttpRouter + ) => HttpRouter> + ( + self: HttpRouter, + path: PathInput, + handler: Route.Handler, + options?: { readonly uninterruptible?: boolean | undefined } | undefined + ): HttpRouter> +} = internal.del + +/** + * @since 1.0.0 + * @category routing + */ +export const head: { + ( + path: PathInput, + handler: Route.Handler, + options?: { readonly uninterruptible?: boolean | undefined } | undefined + ): ( + self: HttpRouter + ) => HttpRouter> + ( + self: HttpRouter, + path: PathInput, + handler: Route.Handler, + options?: { readonly uninterruptible?: boolean | undefined } | undefined + ): HttpRouter> +} = internal.head + +/** + * @since 1.0.0 + * @category routing + */ +export const options: { + ( + path: PathInput, + handler: Route.Handler, + options?: { readonly uninterruptible?: boolean | undefined } | undefined + ): ( + self: HttpRouter + ) => HttpRouter> + ( + self: HttpRouter, + path: PathInput, + handler: Route.Handler, + options?: { readonly uninterruptible?: boolean | undefined } | undefined + ): HttpRouter> +} = internal.options + +/** + * @since 1.0.0 + * @category combinators + */ +export const use: { + ( + f: (self: Route.Middleware) => App.Default + ): (self: HttpRouter) => HttpRouter> + ( + self: HttpRouter, + f: (self: Route.Middleware) => App.Default + ): HttpRouter> +} = internal.use + +/** + * @since 1.0.0 + * @category combinators + */ +export const transform: { + ( + f: (self: Route.Handler) => App.HttpApp + ): (self: HttpRouter) => HttpRouter> + ( + self: HttpRouter, + f: (self: Route.Handler) => App.HttpApp + ): HttpRouter> +} = internal.transform + +/** + * @since 1.0.0 + * @category combinators + */ +export const catchAll: { + ( + f: (e: E) => Route.Handler + ): (self: HttpRouter) => HttpRouter> + ( + self: HttpRouter, + f: (e: E) => Route.Handler + ): HttpRouter> +} = internal.catchAll + +/** + * @since 1.0.0 + * @category combinators + */ +export const catchAllCause: { + ( + f: (e: Cause.Cause) => Route.Handler + ): (self: HttpRouter) => HttpRouter> + ( + self: HttpRouter, + f: (e: Cause.Cause) => Route.Handler + ): HttpRouter> +} = internal.catchAllCause + +/** + * @since 1.0.0 + * @category combinators + */ +export const catchTag: { + ( + k: K, + f: (e: Extract) => Route.Handler + ): ( + self: HttpRouter + ) => HttpRouter, R | HttpRouter.ExcludeProvided> + ( + self: HttpRouter, + k: K, + f: (e: Extract) => Route.Handler + ): HttpRouter, R | HttpRouter.ExcludeProvided> +} = internal.catchTag + +/** + * @since 1.0.0 + * @category combinators + */ +export const catchTags: { + < + E, + Cases extends E extends { _tag: string } + ? { [K in E["_tag"]]+?: ((error: Extract) => Route.Handler) | undefined } + : {} + >( + cases: Cases + ): ( + self: HttpRouter + ) => HttpRouter< + | Exclude + | { + [K in keyof Cases]: Cases[K] extends (...args: Array) => Effect.Effect ? E : never + }[keyof Cases], + | R + | HttpRouter.ExcludeProvided< + { + [K in keyof Cases]: Cases[K] extends (...args: Array) => Effect.Effect ? R : never + }[keyof Cases] + > + > + < + R, + E, + Cases extends E extends { _tag: string } + ? { [K in E["_tag"]]+?: ((error: Extract) => Route.Handler) | undefined } : + {} + >( + self: HttpRouter, + cases: Cases + ): HttpRouter< + | Exclude + | { + [K in keyof Cases]: Cases[K] extends (...args: Array) => Effect.Effect ? E : never + }[keyof Cases], + | R + | HttpRouter.ExcludeProvided< + { + [K in keyof Cases]: Cases[K] extends (...args: Array) => Effect.Effect ? R : never + }[keyof Cases] + > + > +} = internal.catchTags + +/** + * @since 1.0.0 + * @category combinators + */ +export const provideService: { + >( + tag: T, + service: Context.Tag.Service + ): (self: HttpRouter) => HttpRouter>> + >( + self: HttpRouter, + tag: T, + service: Context.Tag.Service + ): HttpRouter>> +} = internal.provideService + +/** + * @since 1.0.0 + * @category combinators + */ +export const provideServiceEffect: { + , R1, E1>( + tag: T, + effect: Effect.Effect, E1, R1> + ): ( + self: HttpRouter + ) => HttpRouter< + E1 | E, + | Exclude> + | Exclude, Context.Tag.Identifier> + > + , R1, E1>( + self: HttpRouter, + tag: T, + effect: Effect.Effect, E1, R1> + ): HttpRouter< + E | E1, + | Exclude> + | Exclude, Context.Tag.Identifier> + > +} = internal.provideServiceEffect + +/** + * @since 1.0.0 + * @category tags + */ +export const Tag: ( + id: Name +) => () => HttpRouter.TagClass = + internal.Tag + +/** + * @since 1.0.0 + * @category tags + */ +export class Default extends Tag("@effect/platform/HttpRouter/Default")() {} + +/** + * @since 1.0.0 + * @category utils + */ +export const toHttpApp: (self: HttpRouter) => Effect.Effect> = + internal.toHttpApp diff --git a/repos/effect/packages/platform/src/HttpServer.ts b/repos/effect/packages/platform/src/HttpServer.ts new file mode 100644 index 0000000..120590b --- /dev/null +++ b/repos/effect/packages/platform/src/HttpServer.ts @@ -0,0 +1,226 @@ +/** + * @since 1.0.0 + */ +import type * as Context from "effect/Context" +import type * as Effect from "effect/Effect" +import type * as Layer from "effect/Layer" +import type * as Scope from "effect/Scope" +import type { Generator } from "./Etag.js" +import type { FileSystem } from "./FileSystem.js" +import type * as App from "./HttpApp.js" +import type * as Client from "./HttpClient.js" +import type * as Middleware from "./HttpMiddleware.js" +import type { HttpPlatform } from "./HttpPlatform.js" +import type * as ServerRequest from "./HttpServerRequest.js" +import * as internal from "./internal/httpServer.js" +import type { Path } from "./Path.js" + +/** + * @since 1.0.0 + * @category type ids + */ +export const TypeId: unique symbol = internal.TypeId + +/** + * @since 1.0.0 + * @category type ids + */ +export type TypeId = typeof TypeId + +/** + * @since 1.0.0 + * @category models + */ +export interface HttpServer { + readonly [TypeId]: TypeId + readonly serve: { + (httpApp: App.Default): Effect.Effect< + void, + never, + Exclude | Scope.Scope + > + >( + httpApp: App.Default, + middleware: Middleware.HttpMiddleware.Applied + ): Effect.Effect< + void, + never, + Exclude | Scope.Scope + > + } + readonly address: Address +} + +/** + * @since 1.0.0 + * @category models + */ +export interface ServeOptions { + readonly respond: boolean +} + +/** + * @since 1.0.0 + * @category address + */ +export type Address = UnixAddress | TcpAddress + +/** + * @since 1.0.0 + * @category address + */ +export interface TcpAddress { + readonly _tag: "TcpAddress" + readonly hostname: string + readonly port: number +} + +/** + * @since 1.0.0 + * @category address + */ +export interface UnixAddress { + readonly _tag: "UnixAddress" + readonly path: string +} + +/** + * @since 1.0.0 + * @category constructors + */ +export const HttpServer: Context.Tag = internal.serverTag + +/** + * @since 1.0.0 + * @category constructors + */ +export const make: ( + options: { + readonly serve: ( + httpApp: App.Default, + middleware?: Middleware.HttpMiddleware + ) => Effect.Effect + readonly address: Address + } +) => HttpServer = internal.make + +/** + * @since 1.0.0 + * @category accessors + */ +export const serve: { + (): ( + httpApp: App.Default + ) => Layer.Layer> + >( + middleware: Middleware.HttpMiddleware.Applied + ): ( + httpApp: App.Default + ) => Layer.Layer< + never, + never, + HttpServer | Exclude, ServerRequest.HttpServerRequest | Scope.Scope> + > + ( + httpApp: App.Default + ): Layer.Layer> + >( + httpApp: App.Default, + middleware: Middleware.HttpMiddleware.Applied + ): Layer.Layer< + never, + never, + HttpServer | Exclude, ServerRequest.HttpServerRequest | Scope.Scope> + > +} = internal.serve + +/** + * @since 1.0.0 + * @category accessors + */ +export const serveEffect: { + (): ( + httpApp: App.Default + ) => Effect.Effect> + >( + middleware: Middleware.HttpMiddleware.Applied + ): ( + httpApp: App.Default + ) => Effect.Effect< + void, + never, + Scope.Scope | HttpServer | Exclude, ServerRequest.HttpServerRequest> + > + ( + httpApp: App.Default + ): Effect.Effect> + >( + httpApp: App.Default, + middleware: Middleware.HttpMiddleware.Applied + ): Effect.Effect< + void, + never, + Scope.Scope | HttpServer | Exclude, ServerRequest.HttpServerRequest> + > +} = internal.serveEffect + +/** + * @since 1.0.0 + * @category address + */ +export const formatAddress: (address: Address) => string = internal.formatAddress + +/** + * @since 1.0.0 + * @category address + */ +export const addressWith: ( + effect: (address: Address) => Effect.Effect +) => Effect.Effect = internal.addressWith + +/** + * @since 1.0.0 + * @category address + */ +export const addressFormattedWith: ( + effect: (address: string) => Effect.Effect +) => Effect.Effect = internal.addressFormattedWith + +/** + * @since 1.0.0 + * @category address + */ +export const logAddress: Effect.Effect = internal.logAddress + +/** + * @since 1.0.0 + * @category address + */ +export const withLogAddress: (layer: Layer.Layer) => Layer.Layer> = + internal.withLogAddress + +/** + * Layer producing an `HttpClient` with prepended url of the running http server. + * + * @since 1.0.0 + * @category layers + */ +export const layerTestClient: Layer.Layer = + internal.layerTestClient + +/** + * A Layer providing the `HttpPlatform`, `FileSystem`, `Etag.Generator`, and `Path` + * services. + * + * The `FileSystem` service is a no-op implementation, so this layer is only + * useful for platforms that have no file system. + * + * @since 1.0.0 + * @category layers + */ +export const layerContext: Layer.Layer< + | HttpPlatform + | FileSystem + | Generator + | Path +> = internal.layerContext diff --git a/repos/effect/packages/platform/src/HttpServerError.ts b/repos/effect/packages/platform/src/HttpServerError.ts new file mode 100644 index 0000000..5c9a6aa --- /dev/null +++ b/repos/effect/packages/platform/src/HttpServerError.ts @@ -0,0 +1,152 @@ +/** + * @since 1.0.0 + */ +import type * as Cause from "effect/Cause" +import type * as Effect from "effect/Effect" +import type * as Exit from "effect/Exit" +import type * as FiberId from "effect/FiberId" +import type * as Option from "effect/Option" +import { TypeIdError } from "./Error.js" +import type * as ServerRequest from "./HttpServerRequest.js" +import * as Respondable from "./HttpServerRespondable.js" +import * as ServerResponse from "./HttpServerResponse.js" +import * as internal from "./internal/httpServerError.js" + +/** + * @since 1.0.0 + * @category type id + */ +export const TypeId: unique symbol = internal.TypeId + +/** + * @since 1.0.0 + * @category type id + */ +export type TypeId = typeof TypeId + +/** + * @since 1.0.0 + * @category error + */ +export type HttpServerError = RequestError | ResponseError | RouteNotFound | ServeError + +/** + * @since 1.0.0 + * @category error + */ +export class RequestError extends TypeIdError(TypeId, "RequestError")<{ + readonly request: ServerRequest.HttpServerRequest + readonly reason: "Transport" | "Decode" + readonly cause?: unknown + readonly description?: string +}> implements Respondable.Respondable { + /** + * @since 1.0.0 + */ + [Respondable.symbol]() { + return ServerResponse.empty({ status: 400 }) + } + + get methodAndUrl() { + return `${this.request.method} ${this.request.url}` + } + + get message() { + return this.description ? + `${this.reason}: ${this.description} (${this.methodAndUrl})` : + `${this.reason} error (${this.methodAndUrl})` + } +} + +/** + * @since 1.0.0 + * @category predicates + */ +export const isServerError: (u: unknown) => u is HttpServerError = internal.isServerError + +/** + * @since 1.0.0 + * @category error + */ +export class RouteNotFound extends TypeIdError(TypeId, "RouteNotFound")<{ + readonly request: ServerRequest.HttpServerRequest +}> { + constructor(options: { request: ServerRequest.HttpServerRequest }) { + super(options) + ;(this as any).stack = `${this.name}: ${this.message}` + } + /** + * @since 1.0.0 + */ + [Respondable.symbol]() { + return ServerResponse.empty({ status: 404 }) + } + get message() { + return `${this.request.method} ${this.request.url} not found` + } +} + +/** + * @since 1.0.0 + * @category error + */ +export class ResponseError extends TypeIdError(TypeId, "ResponseError")<{ + readonly request: ServerRequest.HttpServerRequest + readonly response: ServerResponse.HttpServerResponse + readonly reason: "Decode" + readonly cause?: unknown + readonly description?: string +}> { + /** + * @since 1.0.0 + */ + [Respondable.symbol]() { + return ServerResponse.empty({ status: 500 }) + } + + get methodAndUrl() { + return `${this.request.method} ${this.request.url}` + } + + get message() { + const info = `${this.response.status} ${this.methodAndUrl}` + return this.description ? + `${this.description} (${info})` : + `${this.reason} error (${info})` + } +} + +/** + * @since 1.0.0 + * @category error + */ +export class ServeError extends TypeIdError(TypeId, "ServeError")<{ + readonly cause: unknown +}> {} + +/** + * @since 1.0.0 + */ +export const clientAbortFiberId: FiberId.FiberId = internal.clientAbortFiberId + +/** + * @since 1.0.0 + */ +export const causeResponse: ( + cause: Cause.Cause +) => Effect.Effect]> = internal.causeResponse + +/** + * @since 1.0.0 + */ +export const causeResponseStripped: ( + cause: Cause.Cause +) => readonly [response: ServerResponse.HttpServerResponse, cause: Option.Option>] = + internal.causeResponseStripped + +/** + * @since 1.0.0 + */ +export const exitResponse: ( + exit: Exit.Exit +) => ServerResponse.HttpServerResponse = internal.exitResponse diff --git a/repos/effect/packages/platform/src/HttpServerRequest.ts b/repos/effect/packages/platform/src/HttpServerRequest.ts new file mode 100644 index 0000000..ca39ccc --- /dev/null +++ b/repos/effect/packages/platform/src/HttpServerRequest.ts @@ -0,0 +1,286 @@ +/** + * @since 1.0.0 + */ +import type { Channel } from "effect/Channel" +import type { Chunk } from "effect/Chunk" +import type * as Context from "effect/Context" +import * as Effect from "effect/Effect" +import * as Either from "effect/Either" +import * as Option from "effect/Option" +import type * as ParseResult from "effect/ParseResult" +import type { ReadonlyRecord } from "effect/Record" +import * as Runtime from "effect/Runtime" +import type * as Schema from "effect/Schema" +import type { ParseOptions } from "effect/SchemaAST" +import type * as Scope from "effect/Scope" +import * as Stream from "effect/Stream" +import type * as FileSystem from "./FileSystem.js" +import type * as Headers from "./Headers.js" +import type * as IncomingMessage from "./HttpIncomingMessage.js" +import { hasBody, type HttpMethod } from "./HttpMethod.js" +import * as Error from "./HttpServerError.js" +import * as internal from "./internal/httpServerRequest.js" +import type * as Multipart from "./Multipart.js" +import type * as Path from "./Path.js" +import type * as Socket from "./Socket.js" + +export { + /** + * @since 1.0.0 + * @category fiber refs + */ + MaxBodySize, + /** + * @since 1.0.0 + * @category fiber refs + */ + withMaxBodySize +} from "./HttpIncomingMessage.js" + +/** + * @since 1.0.0 + * @category type ids + */ +export const TypeId: unique symbol = internal.TypeId + +/** + * @since 1.0.0 + * @category type ids + */ +export type TypeId = typeof TypeId + +/** + * @since 1.0.0 + * @category models + */ +export interface HttpServerRequest extends IncomingMessage.HttpIncomingMessage { + readonly [TypeId]: TypeId + readonly source: unknown + readonly url: string + readonly originalUrl: string + readonly method: HttpMethod + readonly cookies: ReadonlyRecord + + readonly multipart: Effect.Effect< + Multipart.Persisted, + Multipart.MultipartError, + Scope.Scope | FileSystem.FileSystem | Path.Path + > + readonly multipartStream: Stream.Stream + + readonly upgrade: Effect.Effect + + readonly modify: ( + options: { + readonly url?: string + readonly headers?: Headers.Headers + readonly remoteAddress?: string + } + ) => HttpServerRequest +} + +/** + * @since 1.0.0 + * @category context + */ +export const HttpServerRequest: Context.Tag = internal.serverRequestTag + +/** + * @since 1.0.0 + * @category search params + */ +export interface ParsedSearchParams { + readonly _: unique symbol +} + +/** + * @since 1.0.0 + * @category search params + */ +export const ParsedSearchParams: Context.Tag>> = + internal.parsedSearchParamsTag + +/** + * @since 1.0.0 + * @category search params + */ +export const searchParamsFromURL: (url: URL) => ReadonlyRecord> = + internal.searchParamsFromURL + +/** + * @since 1.0.0 + * @category accessors + */ +export const persistedMultipart: Effect.Effect< + unknown, + Multipart.MultipartError, + Scope.Scope | FileSystem.FileSystem | Path.Path | HttpServerRequest +> = internal.multipartPersisted + +/** + * @since 1.0.0 + * @category accessors + */ +export const upgrade: Effect.Effect = internal.upgrade + +/** + * @since 1.0.0 + * @category accessors + */ +export const upgradeChannel: () => Channel< + Chunk, + Chunk, + Error.RequestError | IE | Socket.SocketError, + IE, + void, + unknown, + HttpServerRequest +> = internal.upgradeChannel + +/** + * @since 1.0.0 + * @category schema + */ +export const schemaCookies: >, R>( + schema: Schema.Schema, + options?: ParseOptions | undefined +) => Effect.Effect = internal.schemaCookies + +/** + * @since 1.0.0 + * @category schema + */ +export const schemaHeaders: >, R>( + schema: Schema.Schema, + options?: ParseOptions | undefined +) => Effect.Effect = internal.schemaHeaders + +/** + * @since 1.0.0 + * @category schema + */ +export const schemaSearchParams: | undefined>>, R>( + schema: Schema.Schema, + options?: ParseOptions | undefined +) => Effect.Effect = internal.schemaSearchParams + +/** + * @since 1.0.0 + * @category schema + */ +export const schemaBodyJson: ( + schema: Schema.Schema, + options?: ParseOptions | undefined +) => Effect.Effect = internal.schemaBodyJson + +/** + * @since 1.0.0 + * @category schema + */ +export const schemaBodyForm: , R>( + schema: Schema.Schema, + options?: ParseOptions | undefined +) => Effect.Effect< + A, + Multipart.MultipartError | ParseResult.ParseError | Error.RequestError, + R | HttpServerRequest | Scope.Scope | FileSystem.FileSystem | Path.Path +> = internal.schemaBodyForm + +/** + * @since 1.0.0 + * @category schema + */ +export const schemaBodyUrlParams: < + A, + I extends Readonly | undefined>>, + R +>( + schema: Schema.Schema, + options?: ParseOptions | undefined +) => Effect.Effect = internal.schemaBodyUrlParams + +/** + * @since 1.0.0 + * @category schema + */ +export const schemaBodyMultipart: , R>( + schema: Schema.Schema, + options?: ParseOptions | undefined +) => Effect.Effect< + A, + Multipart.MultipartError | ParseResult.ParseError, + R | HttpServerRequest | Scope.Scope | FileSystem.FileSystem | Path.Path +> = internal.schemaBodyMultipart + +/** + * @since 1.0.0 + * @category schema + */ +export const schemaBodyFormJson: ( + schema: Schema.Schema, + options?: ParseOptions | undefined +) => ( + field: string +) => Effect.Effect< + A, + ParseResult.ParseError | Error.RequestError, + R | HttpServerRequest | FileSystem.FileSystem | Path.Path | Scope.Scope +> = internal.schemaBodyFormJson + +/** + * @since 1.0.0 + * @category conversions + */ +export const fromWeb: (request: Request) => HttpServerRequest = internal.fromWeb + +/** + * @since 1.0.0 + * @category conversions + */ +export const toWebEither = (self: HttpServerRequest, options?: { + readonly signal?: AbortSignal | undefined + readonly runtime?: Runtime.Runtime | undefined +}): Either.Either => { + if (self.source instanceof Request) { + return Either.right(self.source) + } + const ourl = toURL(self) + if (Option.isNone(ourl)) { + return Either.left( + new Error.RequestError({ + request: self, + reason: "Decode", + description: "Invalid URL" + }) + ) + } + const requestInit: RequestInit = { + method: self.method, + headers: self.headers, + signal: options?.signal + } + if (hasBody(self.method)) { + requestInit.body = Stream.toReadableStreamRuntime(self.stream, options?.runtime ?? Runtime.defaultRuntime) + ;(requestInit as any).duplex = "half" + } + return Either.right(new Request(ourl.value, requestInit)) +} + +/** + * @since 1.0.0 + * @category conversions + */ +export const toWeb = (self: HttpServerRequest, options?: { + readonly signal?: AbortSignal | undefined +}): Effect.Effect => + Effect.flatMap(Effect.runtime(), (runtime) => + toWebEither(self, { + signal: options?.signal, + runtime + })) + +/** + * @since 1.0.0 + * @category conversions + */ +export const toURL: (self: HttpServerRequest) => Option.Option = internal.toURL diff --git a/repos/effect/packages/platform/src/HttpServerRespondable.ts b/repos/effect/packages/platform/src/HttpServerRespondable.ts new file mode 100644 index 0000000..4cdd7e5 --- /dev/null +++ b/repos/effect/packages/platform/src/HttpServerRespondable.ts @@ -0,0 +1,74 @@ +/** + * @since 1.0.0 + */ +import * as Cause from "effect/Cause" +import * as Effect from "effect/Effect" +import * as ParseResult from "effect/ParseResult" +import { hasProperty } from "effect/Predicate" +import type { HttpServerResponse } from "./HttpServerResponse.js" +import * as ServerResponse from "./HttpServerResponse.js" + +/** + * @since 1.0.0 + * @category symbols + */ +export const symbol: unique symbol = Symbol.for("@effect/platform/HttpServerRespondable") + +/** + * @since 1.0.0 + * @category models + */ +export interface Respondable { + readonly [symbol]: () => Effect.Effect +} + +/** + * @since 1.0.0 + * @category guards + */ +export const isRespondable = (u: unknown): u is Respondable => hasProperty(u, symbol) + +const badRequest = ServerResponse.empty({ status: 400 }) +const notFound = ServerResponse.empty({ status: 404 }) + +/** + * @since 1.0.0 + * @category accessors + */ +export const toResponse = (self: Respondable): Effect.Effect => { + if (ServerResponse.isServerResponse(self)) { + return Effect.succeed(self) + } + return Effect.orDie(self[symbol]()) +} + +/** + * @since 1.0.0 + * @category accessors + */ +export const toResponseOrElse = (u: unknown, orElse: HttpServerResponse): Effect.Effect => { + if (ServerResponse.isServerResponse(u)) { + return Effect.succeed(u) + } else if (isRespondable(u)) { + return Effect.catchAllCause(u[symbol](), () => Effect.succeed(orElse)) + // add support for some commmon types + } else if (ParseResult.isParseError(u)) { + return Effect.succeed(badRequest) + } else if (Cause.isNoSuchElementException(u)) { + return Effect.succeed(notFound) + } + return Effect.succeed(orElse) +} + +/** + * @since 1.0.0 + * @category accessors + */ +export const toResponseOrElseDefect = (u: unknown, orElse: HttpServerResponse): Effect.Effect => { + if (ServerResponse.isServerResponse(u)) { + return Effect.succeed(u) + } else if (isRespondable(u)) { + return Effect.catchAllCause(u[symbol](), () => Effect.succeed(orElse)) + } + return Effect.succeed(orElse) +} diff --git a/repos/effect/packages/platform/src/HttpServerResponse.ts b/repos/effect/packages/platform/src/HttpServerResponse.ts new file mode 100644 index 0000000..72b7c64 --- /dev/null +++ b/repos/effect/packages/platform/src/HttpServerResponse.ts @@ -0,0 +1,428 @@ +/** + * @since 1.0.0 + */ +import type * as Effect from "effect/Effect" +import type { Inspectable } from "effect/Inspectable" +import type * as Runtime from "effect/Runtime" +import type * as Schema from "effect/Schema" +import type { ParseOptions } from "effect/SchemaAST" +import * as Stream from "effect/Stream" +import type { Cookie, CookiesError } from "./Cookies.js" +import * as Cookies from "./Cookies.js" +import type * as PlatformError from "./Error.js" +import type * as FileSystem from "./FileSystem.js" +import type * as Headers from "./Headers.js" +import * as Body from "./HttpBody.js" +import type * as Platform from "./HttpPlatform.js" +import type { Respondable } from "./HttpServerRespondable.js" +import * as internal from "./internal/httpServerResponse.js" +import type * as Template from "./Template.js" +import type * as UrlParams from "./UrlParams.js" + +/** + * @since 1.0.0 + * @category type ids + */ +export const TypeId: unique symbol = Symbol.for("@effect/platform/HttpServerResponse") + +/** + * @since 1.0.0 + * @category type ids + */ +export type TypeId = typeof TypeId + +/** + * @since 1.0.0 + * @category models + */ +export interface HttpServerResponse extends Effect.Effect, Inspectable, Respondable { + readonly [TypeId]: TypeId + readonly status: number + readonly statusText?: string | undefined + readonly headers: Headers.Headers + readonly cookies: Cookies.Cookies + readonly body: Body.HttpBody +} + +/** + * @since 1.0.0 + * @category models + */ +export interface Options { + readonly status?: number | undefined + readonly statusText?: string | undefined + readonly headers?: Headers.Input | undefined + readonly cookies?: Cookies.Cookies | undefined + readonly contentType?: string | undefined + readonly contentLength?: number | undefined +} + +/** + * @since 1.0.0 + */ +export declare namespace Options { + /** + * @since 1.0.0 + * @category models + */ + export interface WithContent extends Omit {} + + /** + * @since 1.0.0 + * @category models + */ + export interface WithContentType extends Omit {} +} + +/** + * @since 1.0.0 + */ +export const isServerResponse: (u: unknown) => u is HttpServerResponse = internal.isServerResponse + +/** + * @since 1.0.0 + * @category constructors + */ +export const empty: (options?: Options.WithContent | undefined) => HttpServerResponse = internal.empty + +/** + * @since 1.0.0 + * @category constructors + */ +export const redirect: (location: string | URL, options?: Options.WithContentType | undefined) => HttpServerResponse = + internal.redirect + +/** + * @since 1.0.0 + * @category constructors + */ +export const uint8Array: (body: Uint8Array, options?: Options.WithContentType | undefined) => HttpServerResponse = + internal.uint8Array + +/** + * @since 1.0.0 + * @category constructors + */ +export const text: (body: string, options?: Options.WithContentType | undefined) => HttpServerResponse = internal.text + +/** + * @since 1.0.0 + * @category constructors + */ +export const html: { + >( + strings: TemplateStringsArray, + ...args: A + ): Effect.Effect, Template.Interpolated.Context> + (html: string): HttpServerResponse +} = internal.html + +/** + * @since 1.0.0 + * @category constructors + */ +export const htmlStream: >( + strings: TemplateStringsArray, + ...args: A +) => Effect.Effect> = internal.htmlStream + +/** + * @since 1.0.0 + * @category constructors + */ +export const json: ( + body: unknown, + options?: Options.WithContentType | undefined +) => Effect.Effect = internal.json + +/** + * @since 1.0.0 + * @category constructors + */ +export const schemaJson: ( + schema: Schema.Schema, + options?: ParseOptions | undefined +) => ( + body: A, + options?: Options.WithContentType | undefined +) => Effect.Effect = internal.schemaJson + +/** + * @since 1.0.0 + * @category constructors + */ +export const unsafeJson: (body: unknown, options?: Options.WithContentType | undefined) => HttpServerResponse = + internal.unsafeJson + +/** + * @since 1.0.0 + * @category constructors + */ +export const urlParams: (body: UrlParams.Input, options?: Options.WithContentType | undefined) => HttpServerResponse = + internal.urlParams + +/** + * @since 1.0.0 + * @category constructors + */ +export const raw: (body: unknown, options?: Options | undefined) => HttpServerResponse = internal.raw + +/** + * @since 1.0.0 + * @category constructors + */ +export const formData: (body: FormData, options?: Options.WithContent | undefined) => HttpServerResponse = + internal.formData + +/** + * @since 1.0.0 + * @category constructors + */ +export const stream: ( + body: Stream.Stream, + options?: Options | undefined +) => HttpServerResponse = internal.stream + +/** + * @since 1.0.0 + * @category constructors + */ +export const file: ( + path: string, + options?: (Options & FileSystem.StreamOptions) | undefined +) => Effect.Effect = internal.file + +/** + * @since 1.0.0 + * @category constructors + */ +export const fileWeb: ( + file: Body.HttpBody.FileLike, + options?: (Options.WithContent & FileSystem.StreamOptions) | undefined +) => Effect.Effect = internal.fileWeb + +/** + * @since 1.0.0 + * @category combinators + */ +export const setHeader: { + (key: string, value: string): (self: HttpServerResponse) => HttpServerResponse + (self: HttpServerResponse, key: string, value: string): HttpServerResponse +} = internal.setHeader + +/** + * @since 1.0.0 + * @category combinators + */ +export const setHeaders: { + (input: Headers.Input): (self: HttpServerResponse) => HttpServerResponse + (self: HttpServerResponse, input: Headers.Input): HttpServerResponse +} = internal.setHeaders + +/** + * @since 1.0.0 + * @category combinators + */ +export const removeCookie: { + (name: string): (self: HttpServerResponse) => HttpServerResponse + (self: HttpServerResponse, name: string): HttpServerResponse +} = internal.removeCookie + +/** + * @since 1.0.0 + * @category combinators + */ +export const expireCookie: { + ( + name: string, + options?: Omit + ): (self: HttpServerResponse) => HttpServerResponse + ( + self: HttpServerResponse, + name: string, + options?: Omit + ): HttpServerResponse +} = internal.expireCookie + +/** + * @since 1.0.0 + * @category combinators + */ +export const replaceCookies: { + (cookies: Cookies.Cookies): (self: HttpServerResponse) => HttpServerResponse + (self: HttpServerResponse, cookies: Cookies.Cookies): HttpServerResponse +} = internal.replaceCookies + +/** + * @since 1.0.0 + * @category combinators + */ +export const setCookie: { + ( + name: string, + value: string, + options?: Cookie["options"] + ): ( + self: HttpServerResponse + ) => Effect.Effect< + HttpServerResponse, + CookiesError + > + ( + self: HttpServerResponse, + name: string, + value: string, + options?: Cookie["options"] + ): Effect.Effect< + HttpServerResponse, + CookiesError + > +} = internal.setCookie + +/** + * @since 1.0.0 + * @category combinators + */ +export const unsafeSetCookie: { + ( + name: string, + value: string, + options?: Cookie["options"] + ): (self: HttpServerResponse) => HttpServerResponse + ( + self: HttpServerResponse, + name: string, + value: string, + options?: Cookie["options"] + ): HttpServerResponse +} = internal.unsafeSetCookie + +/** + * @since 1.0.0 + * @category combinators + */ +export const updateCookies: { + (f: (cookies: Cookies.Cookies) => Cookies.Cookies): (self: HttpServerResponse) => HttpServerResponse + (self: HttpServerResponse, f: (cookies: Cookies.Cookies) => Cookies.Cookies): HttpServerResponse +} = internal.updateCookies + +/** + * @since 1.0.0 + * @category combinators + */ +export const mergeCookies: { + (cookies: Cookies.Cookies): (self: HttpServerResponse) => HttpServerResponse + (self: HttpServerResponse, cookies: Cookies.Cookies): HttpServerResponse +} = internal.mergeCookies + +/** + * @since 1.0.0 + * @category combinators + */ +export const setCookies: { + ( + cookies: Iterable< + readonly [ + name: string, + value: string, + options?: Cookie["options"] + ] + > + ): (self: HttpServerResponse) => Effect.Effect + ( + self: HttpServerResponse, + cookies: Iterable< + readonly [ + name: string, + value: string, + options?: Cookie["options"] + ] + > + ): Effect.Effect +} = internal.setCookies + +/** + * @since 1.0.0 + * @category combinators + */ +export const unsafeSetCookies: { + ( + cookies: Iterable< + readonly [ + name: string, + value: string, + options?: Cookie["options"] + ] + > + ): (self: HttpServerResponse) => HttpServerResponse + ( + self: HttpServerResponse, + cookies: Iterable< + readonly [ + name: string, + value: string, + options?: Cookie["options"] + ] + > + ): HttpServerResponse +} = internal.unsafeSetCookies + +/** + * @since 1.0.0 + * @category combinators + */ +export const setBody: { + (body: Body.HttpBody): (self: HttpServerResponse) => HttpServerResponse + (self: HttpServerResponse, body: Body.HttpBody): HttpServerResponse +} = internal.setBody + +/** + * @since 1.0.0 + * @category combinators + */ +export const setStatus: { + (status: number, statusText?: string | undefined): (self: HttpServerResponse) => HttpServerResponse + (self: HttpServerResponse, status: number, statusText?: string | undefined): HttpServerResponse +} = internal.setStatus + +/** + * @since 1.0.0 + * @category conversions + */ +export const toWeb: ( + response: HttpServerResponse, + options?: { + readonly withoutBody?: boolean | undefined + readonly runtime?: Runtime.Runtime | undefined + } +) => Response = internal.toWeb + +/** + * @since 1.0.0 + * @category conversions + */ +export const fromWeb = (response: Response): HttpServerResponse => { + const headers = new globalThis.Headers(response.headers) + const setCookieHeaders = headers.getSetCookie() + headers.delete("set-cookie") + let self = empty({ + status: response.status, + statusText: response.statusText, + headers: headers as any, + cookies: Cookies.fromSetCookie(setCookieHeaders) + }) + if (response.body) { + const contentType = headers.get("content-type") + self = setBody( + self, + Body.stream( + Stream.fromReadableStream({ + evaluate: () => response.body!, + onError: (e) => e + }), + contentType ?? undefined + ) + ) + } + return self +} diff --git a/repos/effect/packages/platform/src/HttpTraceContext.ts b/repos/effect/packages/platform/src/HttpTraceContext.ts new file mode 100644 index 0000000..1387fe1 --- /dev/null +++ b/repos/effect/packages/platform/src/HttpTraceContext.ts @@ -0,0 +1,109 @@ +/** + * @since 1.0.0 + */ +import * as Option from "effect/Option" +import * as Tracer from "effect/Tracer" +import * as Headers from "./Headers.js" + +/** + * @since 1.0.0 + * @category models + */ +export interface FromHeaders { + (headers: Headers.Headers): Option.Option +} + +/** + * @since 1.0.0 + * @category encoding + */ +export const toHeaders = (span: Tracer.Span): Headers.Headers => + Headers.unsafeFromRecord({ + b3: `${span.traceId}-${span.spanId}-${span.sampled ? "1" : "0"}${ + span.parent._tag === "Some" ? `-${span.parent.value.spanId}` : "" + }`, + traceparent: `00-${span.traceId}-${span.spanId}-${span.sampled ? "01" : "00"}` + }) + +/** + * @since 1.0.0 + * @category decoding + */ +export const fromHeaders = (headers: Headers.Headers): Option.Option => { + let span = w3c(headers) + if (span._tag === "Some") { + return span + } + span = b3(headers) + if (span._tag === "Some") { + return span + } + return xb3(headers) +} + +/** + * @since 1.0.0 + * @category decoding + */ +export const b3: FromHeaders = (headers) => { + if (!("b3" in headers)) { + return Option.none() + } + const parts = headers["b3"].split("-") + if (parts.length < 2) { + return Option.none() + } + return Option.some(Tracer.externalSpan({ + traceId: parts[0], + spanId: parts[1], + sampled: parts[2] ? parts[2] === "1" : true + })) +} + +/** + * @since 1.0.0 + * @category decoding + */ +export const xb3: FromHeaders = (headers) => { + if (!(headers["x-b3-traceid"]) || !(headers["x-b3-spanid"])) { + return Option.none() + } + return Option.some(Tracer.externalSpan({ + traceId: headers["x-b3-traceid"], + spanId: headers["x-b3-spanid"], + sampled: headers["x-b3-sampled"] ? headers["x-b3-sampled"] === "1" : true + })) +} + +const w3cTraceId = /^[0-9a-f]{32}$/i +const w3cSpanId = /^[0-9a-f]{16}$/i + +/** + * @since 1.0.0 + * @category decoding + */ +export const w3c: FromHeaders = (headers) => { + if (!(headers["traceparent"])) { + return Option.none() + } + const parts = headers["traceparent"].split("-") + if (parts.length !== 4) { + return Option.none() + } + const [version, traceId, spanId, flags] = parts + switch (version) { + case "00": { + if (w3cTraceId.test(traceId) === false || w3cSpanId.test(spanId) === false) { + return Option.none() + } + return Option.some(Tracer.externalSpan({ + traceId, + spanId, + sampled: (parseInt(flags, 16) & 1) === 1 + })) + } + default: { + return Option.none() + } + } +} diff --git a/repos/effect/packages/platform/src/KeyValueStore.ts b/repos/effect/packages/platform/src/KeyValueStore.ts new file mode 100644 index 0000000..f0a607f --- /dev/null +++ b/repos/effect/packages/platform/src/KeyValueStore.ts @@ -0,0 +1,245 @@ +/** + * @since 1.0.0 + */ +import type * as Context from "effect/Context" +import type * as Effect from "effect/Effect" +import type { LazyArg } from "effect/Function" +import type * as Layer from "effect/Layer" +import type * as Option from "effect/Option" +import type * as ParseResult from "effect/ParseResult" +import type * as Schema from "effect/Schema" +import type * as PlatformError from "./Error.js" +import type * as FileSystem from "./FileSystem.js" +import * as internal from "./internal/keyValueStore.js" +import type * as Path from "./Path.js" + +/** + * @since 1.0.0 + * @category type id + */ +export const TypeId: unique symbol = internal.TypeId + +/** + * @since 1.0.0 + * @category type id + */ +export type TypeId = typeof TypeId + +/** + * @since 1.0.0 + * @category models + */ +export interface KeyValueStore { + readonly [TypeId]: TypeId + /** + * Returns the value of the specified key if it exists. + */ + readonly get: (key: string) => Effect.Effect, PlatformError.PlatformError> + + /** + * Returns the value of the specified key if it exists. + */ + readonly getUint8Array: (key: string) => Effect.Effect, PlatformError.PlatformError> + + /** + * Sets the value of the specified key. + */ + readonly set: (key: string, value: string | Uint8Array) => Effect.Effect + + /** + * Removes the specified key. + */ + readonly remove: (key: string) => Effect.Effect + + /** + * Removes all entries. + */ + readonly clear: Effect.Effect + + /** + * Returns the number of entries. + */ + readonly size: Effect.Effect + + /** + * Updates the value of the specified key if it exists. + */ + readonly modify: ( + key: string, + f: (value: string) => string + ) => Effect.Effect, PlatformError.PlatformError> + + /** + * Updates the value of the specified key if it exists. + */ + readonly modifyUint8Array: ( + key: string, + f: (value: Uint8Array) => Uint8Array + ) => Effect.Effect, PlatformError.PlatformError> + + /** + * Returns true if the KeyValueStore contains the specified key. + */ + readonly has: (key: string) => Effect.Effect + + /** + * Checks if the KeyValueStore contains any entries. + */ + readonly isEmpty: Effect.Effect + + /** + * Create a SchemaStore for the specified schema. + */ + readonly forSchema: (schema: Schema.Schema) => SchemaStore +} + +/** + * @since 1.0.0 + */ +export declare namespace KeyValueStore { + /** + * @since 1.0.0 + */ + export type AnyStore = KeyValueStore | SchemaStore +} + +/** + * @since 1.0.0 + * @category tags + */ +export const KeyValueStore: Context.Tag = internal.keyValueStoreTag + +/** + * @since 1.0.0 + * @category constructors + */ +export const make: ( + impl: + & Omit + & Partial +) => KeyValueStore = internal.make + +/** + * @since 1.0.0 + * @category constructors + */ +export const makeStringOnly: ( + impl: Pick & Partial> & { + readonly set: (key: string, value: string) => Effect.Effect + } +) => KeyValueStore = internal.makeStringOnly + +/** + * @since 1.0.0 + * @category combinators + */ +export const prefix: { + (prefix: string): (self: S) => S + (self: S, prefix: string): S +} = internal.prefix + +/** + * @since 1.0.0 + * @category layers + */ +export const layerMemory: Layer.Layer = internal.layerMemory + +/** + * @since 1.0.0 + * @category layers + */ +export const layerFileSystem: ( + directory: string +) => Layer.Layer = + internal.layerFileSystem + +/** + * @since 1.0.0 + * @category type id + */ +export const SchemaStoreTypeId: unique symbol = internal.SchemaStoreTypeId + +/** + * @since 1.0.0 + * @category type id + */ +export type SchemaStoreTypeId = typeof SchemaStoreTypeId + +/** + * @since 1.0.0 + * @category models + */ +export interface SchemaStore { + readonly [SchemaStoreTypeId]: SchemaStoreTypeId + /** + * Returns the value of the specified key if it exists. + */ + readonly get: ( + key: string + ) => Effect.Effect, PlatformError.PlatformError | ParseResult.ParseError, R> + + /** + * Sets the value of the specified key. + */ + readonly set: ( + key: string, + value: A + ) => Effect.Effect + + /** + * Removes the specified key. + */ + readonly remove: (key: string) => Effect.Effect + + /** + * Removes all entries. + */ + readonly clear: Effect.Effect + + /** + * Returns the number of entries. + */ + readonly size: Effect.Effect + + /** + * Updates the value of the specified key if it exists. + */ + readonly modify: ( + key: string, + f: (value: A) => A + ) => Effect.Effect, PlatformError.PlatformError | ParseResult.ParseError, R> + + /** + * Returns true if the KeyValueStore contains the specified key. + */ + readonly has: (key: string) => Effect.Effect + + /** + * Checks if the KeyValueStore contains any entries. + */ + readonly isEmpty: Effect.Effect +} + +/** + * @since 1.0.0 + * @category layers + */ +export const layerSchema: ( + schema: Schema.Schema, + tagIdentifier: string +) => { + readonly tag: Context.Tag, SchemaStore> + readonly layer: Layer.Layer, never, KeyValueStore> +} = internal.layerSchema + +/** + * Creates an KeyValueStorage from an instance of the `Storage` api. + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API + * + * @since 1.0.0 + * @category layers + */ +export const layerStorage: ( + evaluate: LazyArg +) => Layer.Layer = internal.layerStorage diff --git a/repos/effect/packages/platform/src/MsgPack.ts b/repos/effect/packages/platform/src/MsgPack.ts new file mode 100644 index 0000000..81ad0f7 --- /dev/null +++ b/repos/effect/packages/platform/src/MsgPack.ts @@ -0,0 +1,348 @@ +/** + * @since 1.0.0 + */ +import * as Channel from "effect/Channel" +import * as Chunk from "effect/Chunk" +import * as Data from "effect/Data" +import * as Effect from "effect/Effect" +import { dual } from "effect/Function" +import type { ParseError } from "effect/ParseResult" +import * as ParseResult from "effect/ParseResult" +import * as Predicate from "effect/Predicate" +import * as Schema from "effect/Schema" +import { Packr, Unpackr } from "msgpackr" +import * as Msgpackr from "msgpackr" +import * as ChannelSchema from "./ChannelSchema.js" + +/** + * @since 1.0.0 + * @category errors + */ +export const ErrorTypeId: unique symbol = Symbol.for("@effect/platform/MsgPack/MsgPackError") + +/** + * @since 1.0.0 + * @category errors + */ +export type ErrorTypeId = typeof ErrorTypeId + +/** + * @since 1.0.0 + * @category errors + */ +export class MsgPackError extends Data.TaggedError("MsgPackError")<{ + readonly reason: "Pack" | "Unpack" + readonly cause: unknown +}> { + /** + * @since 1.0.0 + */ + readonly [ErrorTypeId]: ErrorTypeId = ErrorTypeId + + /** + * @since 1.0.0 + */ + get message() { + return this.reason + } +} + +/** + * @since 1.0.0 + * @category constructors + */ +export const pack = (): Channel.Channel< + Chunk.Chunk, + Chunk.Chunk, + IE | MsgPackError, + IE, + Done, + Done +> => + Channel.suspend(() => { + const packr = new Packr() + const loop: Channel.Channel, Chunk.Chunk, IE | MsgPackError, IE, Done, Done> = + Channel + .readWithCause({ + onInput: (input) => + Channel.zipRight( + Channel.flatMap( + Effect.try({ + try: () => Chunk.of(packr.pack(Chunk.toReadonlyArray(input))), + catch: (cause) => new MsgPackError({ reason: "Pack", cause }) + }), + Channel.write + ), + loop + ), + onFailure: (cause) => Channel.failCause(cause), + onDone: Channel.succeed + }) + return loop + }) + +/** + * @since 1.0.0 + * @category constructors + */ +export const packSchema = ( + schema: Schema.Schema +) => +(): Channel.Channel< + Chunk.Chunk, + Chunk.Chunk, + IE | MsgPackError | ParseError, + IE, + Done, + Done, + R +> => Channel.pipeTo(ChannelSchema.encode(schema)(), pack()) + +/** + * @since 1.0.0 + * @category constructors + */ +export const unpack = (): Channel.Channel< + Chunk.Chunk, + Chunk.Chunk, + IE | MsgPackError, + IE, + Done, + Done +> => + Channel.flatMap( + Channel.sync(() => new Unpackr()), + (packr) => { + let incomplete: Uint8Array | undefined = undefined + const unpack = (value: Chunk.Chunk) => + Effect.try({ + try: () => + Chunk.flatMap(value, (buf) => { + if (incomplete !== undefined) { + const chunk = new Uint8Array(incomplete.length + buf.length) + chunk.set(incomplete) + chunk.set(buf, incomplete.length) + buf = chunk + incomplete = undefined + } + try { + return Chunk.unsafeFromArray(packr.unpackMultiple(buf).flat()) + } catch (error_) { + const error: any = error_ + if (error.incomplete) { + incomplete = buf.subarray(error.lastPosition) + return Chunk.unsafeFromArray(error.values ?? []) + } + throw error + } + }), + catch: (cause) => new MsgPackError({ reason: "Unpack", cause }) + }) + + const loop: Channel.Channel, Chunk.Chunk, IE | MsgPackError, IE, Done, Done> = + Channel.readWithCause({ + onInput: (input: Chunk.Chunk) => + Channel.zipRight( + Channel.flatMap(unpack(input), Channel.write), + loop + ), + onFailure: (cause) => Channel.failCause(cause), + onDone: Channel.succeed + }) + + return loop + } + ) + +/** + * @since 1.0.0 + * @category constructors + */ +export const unpackSchema = ( + schema: Schema.Schema +) => +(): Channel.Channel< + Chunk.Chunk, + Chunk.Chunk, + MsgPackError | ParseError | IE, + IE, + Done, + Done, + R +> => Channel.pipeTo(unpack(), ChannelSchema.decodeUnknown(schema)()) + +/** + * @since 1.0.0 + * @category combinators + */ +export const duplex = ( + self: Channel.Channel, Chunk.Chunk, OE, IE | MsgPackError, OutDone, InDone, R> +): Channel.Channel, Chunk.Chunk, MsgPackError | OE, IE, OutDone, InDone, R> => + Channel.pipeTo( + Channel.pipeTo(pack(), self), + unpack() + ) + +/** + * @since 1.0.0 + * @category combinators + */ +export const duplexSchema: { + ( + options: { + readonly inputSchema: Schema.Schema + readonly outputSchema: Schema.Schema + } + ): ( + self: Channel.Channel< + Chunk.Chunk, + Chunk.Chunk, + OutErr, + MsgPackError | ParseError | InErr, + OutDone, + InDone, + R + > + ) => Channel.Channel< + Chunk.Chunk, + Chunk.Chunk, + MsgPackError | ParseError | OutErr, + InErr, + OutDone, + InDone, + IR | OR | R + > + ( + self: Channel.Channel< + Chunk.Chunk, + Chunk.Chunk, + OutErr, + MsgPackError | ParseError | InErr, + OutDone, + InDone, + R + >, + options: { + readonly inputSchema: Schema.Schema + readonly outputSchema: Schema.Schema + } + ): Channel.Channel< + Chunk.Chunk, + Chunk.Chunk, + MsgPackError | ParseError | OutErr, + InErr, + OutDone, + InDone, + R | IR | OR + > +} = dual< + ( + options: { + readonly inputSchema: Schema.Schema + readonly outputSchema: Schema.Schema + } + ) => ( + self: Channel.Channel< + Chunk.Chunk, + Chunk.Chunk, + OutErr, + MsgPackError | ParseError | InErr, + OutDone, + InDone, + R + > + ) => Channel.Channel< + Chunk.Chunk, + Chunk.Chunk, + MsgPackError | ParseError | OutErr, + InErr, + OutDone, + InDone, + R | IR | OR + >, + ( + self: Channel.Channel< + Chunk.Chunk, + Chunk.Chunk, + OutErr, + MsgPackError | ParseError | InErr, + OutDone, + InDone, + R + >, + options: { + readonly inputSchema: Schema.Schema + readonly outputSchema: Schema.Schema + } + ) => Channel.Channel< + Chunk.Chunk, + Chunk.Chunk, + MsgPackError | ParseError | OutErr, + InErr, + OutDone, + InDone, + R | IR | OR + > +>(2, ( + self: Channel.Channel< + Chunk.Chunk, + Chunk.Chunk, + OutErr, + MsgPackError | ParseError | InErr, + OutDone, + InDone, + R + >, + options: { + readonly inputSchema: Schema.Schema + readonly outputSchema: Schema.Schema + } +): Channel.Channel< + Chunk.Chunk, + Chunk.Chunk, + MsgPackError | ParseError | OutErr, + InErr, + OutDone, + InDone, + R | IR | OR +> => ChannelSchema.duplexUnknown(duplex(self), options)) + +/** + * @since 1.0.0 + * @category schemas + */ +export interface schema extends Schema.transformOrFail, S> {} + +/** + * @since 1.0.0 + * @category schemas + */ +export const schema = (schema: S): schema => + Schema.transformOrFail( + Schema.Uint8ArrayFromSelf, + schema, + { + decode(fromA, _, ast) { + return ParseResult.try({ + try: () => Msgpackr.decode(fromA) as Schema.Schema.Encoded, + catch: (cause) => + new ParseResult.Type( + ast, + fromA, + Predicate.hasProperty(cause, "message") ? String(cause.message) : String(cause) + ) + }) + }, + encode(toI, _, ast) { + return ParseResult.try({ + try: () => Msgpackr.encode(toI), + catch: (cause) => + new ParseResult.Type( + ast, + toI, + Predicate.hasProperty(cause, "message") ? String(cause.message) : String(cause) + ) + }) + } + } + ) diff --git a/repos/effect/packages/platform/src/Multipart.ts b/repos/effect/packages/platform/src/Multipart.ts new file mode 100644 index 0000000..9c44369 --- /dev/null +++ b/repos/effect/packages/platform/src/Multipart.ts @@ -0,0 +1,777 @@ +/** + * @since 1.0.0 + */ +import type * as Cause from "effect/Cause" +import * as Channel from "effect/Channel" +import * as Chunk from "effect/Chunk" +import * as Context from "effect/Context" +import * as Effect from "effect/Effect" +import * as Exit from "effect/Exit" +import { constant, dual } from "effect/Function" +import * as Inspectable from "effect/Inspectable" +import * as Mailbox from "effect/Mailbox" +import * as Option from "effect/Option" +import type * as ParseResult from "effect/ParseResult" +import * as Predicate from "effect/Predicate" +import * as Schema from "effect/Schema" +import type { ParseOptions } from "effect/SchemaAST" +import type * as Scope from "effect/Scope" +import type * as AsyncInput from "effect/SingleProducerAsyncInput" +import * as Stream from "effect/Stream" +import * as MP from "multipasta" +import * as FileSystem from "./FileSystem.js" +import * as IncomingMessage from "./HttpIncomingMessage.js" +import * as Path from "./Path.js" + +/** + * @since 1.0.0 + * @category type ids + */ +export const TypeId: unique symbol = Symbol.for("@effect/platform/Multipart") + +/** + * @since 1.0.0 + * @category type ids + */ +export type TypeId = typeof TypeId + +/** + * @since 1.0.0 + * @category models + */ +export type Part = Field | File + +/** + * @since 1.0.0 + */ +export declare namespace Part { + /** + * @since 1.0.0 + * @category models + */ + export interface Proto extends Inspectable.Inspectable { + readonly [TypeId]: TypeId + readonly _tag: string + } +} + +/** + * @since 1.0.0 + * @category models + */ +export interface Field extends Part.Proto { + readonly _tag: "Field" + readonly key: string + readonly contentType: string + readonly value: string +} + +/** + * @since 1.0.0 + * @category Guards + */ +export const isPart = (u: unknown): u is Part => Predicate.hasProperty(u, TypeId) + +/** + * @since 1.0.0 + * @category Guards + */ +export const isField = (u: unknown): u is Field => isPart(u) && u._tag === "Field" + +/** + * @since 1.0.0 + * @category models + */ +export interface File extends Part.Proto { + readonly _tag: "File" + readonly key: string + readonly name: string + readonly contentType: string + readonly content: Stream.Stream + readonly contentEffect: Effect.Effect +} + +/** + * @since 1.0.0 + * @category Guards + */ +export const isFile = (u: unknown): u is File => isPart(u) && u._tag === "File" + +/** + * @since 1.0.0 + * @category models + */ +export interface PersistedFile extends Part.Proto { + readonly _tag: "PersistedFile" + readonly key: string + readonly name: string + readonly contentType: string + readonly path: string +} + +/** + * @since 1.0.0 + * @category Guards + */ +export const isPersistedFile = (u: unknown): u is PersistedFile => + Predicate.hasProperty(u, TypeId) && Predicate.isTagged(u, "PersistedFile") + +/** + * @since 1.0.0 + * @category models + */ +export interface Persisted { + readonly [key: string]: ReadonlyArray | ReadonlyArray | string +} + +/** + * @since 1.0.0 + * @category Errors + */ +export const ErrorTypeId: unique symbol = Symbol.for( + "@effect/platform/Multipart/MultipartError" +) + +/** + * @since 1.0.0 + * @category Errors + */ +export type ErrorTypeId = typeof ErrorTypeId + +/** + * @since 1.0.0 + * @category Errors + */ +export class MultipartError extends Schema.TaggedError()("MultipartError", { + reason: Schema.Literal("FileTooLarge", "FieldTooLarge", "BodyTooLarge", "TooManyParts", "InternalError", "Parse"), + cause: Schema.Defect +}) { + /** + * @since 1.0.0 + */ + readonly [ErrorTypeId]: ErrorTypeId = ErrorTypeId + + /** + * @since 1.0.0 + */ + get message(): string { + return this.reason + } +} + +/** + * @since 1.0.0 + * @category Schemas + */ +export const FileSchema: Schema.Schema = Schema.declare(isPersistedFile, { + typeConstructor: { _tag: "effect/platform/Multipart.PersistedFile" }, + identifier: "PersistedFile", + jsonSchema: { + type: "string", + format: "binary" + } +}) + +/** + * @since 1.0.0 + * @category Schemas + */ +export const FilesSchema: Schema.Schema> = Schema.Array(FileSchema) + +/** + * @since 1.0.0 + * @category Schemas + */ +export const SingleFileSchema: Schema.transform< + Schema.Schema>, + Schema.Schema +> = Schema.transform(FilesSchema.pipe(Schema.itemsCount(1)), FileSchema, { + strict: true, + decode: ([file]) => file, + encode: (file) => [file] +}) + +/** + * @since 1.0.0 + * @category Schemas + */ +export const schemaPersisted = , R>( + schema: Schema.Schema, + options?: ParseOptions | undefined +): (persisted: Persisted) => Effect.Effect< + A, + ParseResult.ParseError, + R +> => Schema.decodeUnknown(schema, options) + +/** + * @since 1.0.0 + * @category Schemas + */ +export const schemaJson = (schema: Schema.Schema, options?: ParseOptions | undefined): { + ( + field: string + ): (persisted: Persisted) => Effect.Effect + ( + persisted: Persisted, + field: string + ): Effect.Effect +} => { + const fromJson = Schema.parseJson(schema) + return dual< + ( + field: string + ) => ( + persisted: Persisted + ) => Effect.Effect, + ( + persisted: Persisted, + field: string + ) => Effect.Effect + >(2, (persisted, field) => + Effect.map( + Schema.decodeUnknown( + Schema.Struct({ + [field]: fromJson + }), + options + )(persisted), + (_) => _[field] + )) +} + +/** + * @since 1.0.0 + * @category Config + */ +export const makeConfig = ( + headers: Record +): Effect.Effect => + Effect.withFiberRuntime((fiber) => { + const mimeTypes = Context.get(fiber.currentContext, FieldMimeTypes) + return Effect.succeed({ + headers, + maxParts: Option.getOrUndefined(Context.get(fiber.currentContext, MaxParts)), + maxFieldSize: Number(Context.get(fiber.currentContext, MaxFieldSize)), + maxPartSize: Context.get(fiber.currentContext, MaxFileSize).pipe(Option.map(Number), Option.getOrUndefined), + maxTotalSize: Context.get(fiber.currentContext, IncomingMessage.MaxBodySize).pipe( + Option.map(Number), + Option.getOrUndefined + ), + isFile: mimeTypes.length === 0 ? undefined : (info: MP.PartInfo): boolean => + !Chunk.some( + mimeTypes, + (_) => info.contentType.includes(_) + ) && MP.defaultIsFile(info) + }) + }) + +/** + * @since 1.0.0 + * @category Parsers + */ +export const makeChannel = ( + headers: Record, + bufferSize = 16 +): Channel.Channel< + Chunk.Chunk, + Chunk.Chunk, + MultipartError | IE, + IE, + unknown, + unknown +> => + Channel.acquireUseRelease( + Effect.all([ + makeConfig(headers), + Mailbox.make>(bufferSize) + ]), + ([config, mailbox]) => { + let partsBuffer: Array = [] + let exit = Option.none>() + + const input: AsyncInput.AsyncInputProducer, unknown> = { + awaitRead: () => Effect.void, + emit(element) { + return mailbox.offer(element) + }, + error(cause) { + exit = Option.some(Exit.failCause(cause)) + return mailbox.end + }, + done(_value) { + return mailbox.end + } + } + + const parser = MP.make({ + ...config, + onField(info, value) { + partsBuffer.push(new FieldImpl(info.name, info.contentType, MP.decodeField(info, value))) + }, + onFile(info) { + let chunks: Array = [] + let finished = false + const take: Channel.Channel> = Channel.suspend(() => { + if (chunks.length === 0) { + return finished ? Channel.void : Channel.zipRight(pump, take) + } + const chunk = Chunk.unsafeFromArray(chunks) + chunks = [] + return finished ? Channel.write(chunk) : Channel.zipRight( + Channel.write(chunk), + Channel.zipRight(pump, take) + ) + }) + partsBuffer.push(new FileImpl(info, take)) + return function(chunk) { + if (chunk === null) { + finished = true + } else { + chunks.push(chunk) + } + } + }, + onError(error_) { + exit = Option.some(Exit.fail(convertError(error_))) + }, + onDone() { + exit = Option.some(Exit.void) + } + }) + + const pump = Channel.flatMap( + mailbox.takeAll, + ([chunks, done]) => + Channel.sync(() => { + Chunk.forEach(chunks, Chunk.forEach(parser.write)) + if (done) { + parser.end() + } + }) + ) + + const partsChannel: Channel.Channel< + Chunk.Chunk, + unknown, + IE | MultipartError + > = Channel.flatMap( + pump, + () => { + if (partsBuffer.length === 0) { + return exit._tag === "None" ? partsChannel : writeExit(exit.value) + } + const chunk = Chunk.unsafeFromArray(partsBuffer) + partsBuffer = [] + return Channel.zipRight( + Channel.write(chunk), + exit._tag === "None" ? partsChannel : writeExit(exit.value) + ) + } + ) + + return Channel.embedInput(partsChannel, input) + }, + ([, mailbox]) => mailbox.shutdown + ) + +const writeExit = ( + self: Exit.Exit +): Channel.Channel => self._tag === "Success" ? Channel.void : Channel.failCause(self.cause) + +function convertError(cause: MP.MultipartError): MultipartError { + switch (cause._tag) { + case "ReachedLimit": { + switch (cause.limit) { + case "MaxParts": { + return new MultipartError({ reason: "TooManyParts", cause }) + } + case "MaxFieldSize": { + return new MultipartError({ reason: "FieldTooLarge", cause }) + } + case "MaxPartSize": { + return new MultipartError({ reason: "FileTooLarge", cause }) + } + case "MaxTotalSize": { + return new MultipartError({ reason: "BodyTooLarge", cause }) + } + } + } + default: { + return new MultipartError({ reason: "Parse", cause }) + } + } +} + +abstract class PartBase extends Inspectable.Class { + readonly [TypeId]: TypeId + constructor() { + super() + this[TypeId] = TypeId + } +} + +class FieldImpl extends PartBase implements Field { + readonly _tag = "Field" + + constructor( + readonly key: string, + readonly contentType: string, + readonly value: string + ) { + super() + } + + toJSON(): unknown { + return { + _id: "@effect/platform/Multipart/Part", + _tag: "Field", + key: this.key, + contentType: this.contentType, + value: this.value + } + } +} + +class FileImpl extends PartBase implements File { + readonly _tag = "File" + readonly key: string + readonly name: string + readonly contentType: string + readonly content: Stream.Stream + readonly contentEffect: Effect.Effect + + constructor( + info: MP.PartInfo, + channel: Channel.Channel, unknown, never, unknown, void, unknown> + ) { + super() + this.key = info.name + this.name = info.filename ?? info.name + this.contentType = info.contentType + this.content = Stream.fromChannel(channel) + this.contentEffect = channel.pipe( + Channel.pipeTo(collectUint8Array), + Channel.run, + Effect.mapError((cause) => new MultipartError({ reason: "InternalError", cause })) + ) + } + + toJSON(): unknown { + return { + _id: "@effect/platform/Multipart/Part", + _tag: "File", + key: this.key, + name: this.name, + contentType: this.contentType + } + } +} + +const defaultWriteFile = (path: string, file: File) => + Effect.flatMap( + FileSystem.FileSystem, + (fs) => + Effect.mapError( + Stream.run(file.content, fs.sink(path)), + (cause) => new MultipartError({ reason: "InternalError", cause }) + ) + ) + +/** + * @since 1.0.0 + */ +export const collectUint8Array = Channel.suspend(() => { + let accumulator = new Uint8Array(0) + const loop: Channel.Channel< + never, + Chunk.Chunk, + unknown, + unknown, + Uint8Array + > = Channel.readWithCause({ + onInput(chunk: Chunk.Chunk) { + for (const element of chunk) { + const newAccumulator = new Uint8Array(accumulator.length + element.length) + newAccumulator.set(accumulator, 0) + newAccumulator.set(element, accumulator.length) + accumulator = newAccumulator + } + return loop + }, + onFailure: (cause: Cause.Cause) => Channel.failCause(cause), + onDone: () => Channel.succeed(accumulator) + }) + return loop +}) + +/** + * @since 1.0.0 + * @category Conversions + */ +export const toPersisted = ( + stream: Stream.Stream, + writeFile = defaultWriteFile +): Effect.Effect => + Effect.gen(function*() { + const fs = yield* FileSystem.FileSystem + const path_ = yield* Path.Path + const dir = yield* fs.makeTempDirectoryScoped() + const persisted: Record | Array | string> = Object.create(null) + yield* Stream.runForEach(stream, (part) => { + if (part._tag === "Field") { + if (!(part.key in persisted)) { + persisted[part.key] = part.value + } else if (typeof persisted[part.key] === "string") { + persisted[part.key] = [persisted[part.key] as string, part.value] + } else { + ;(persisted[part.key] as Array).push(part.value) + } + return Effect.void + } else if (part.name === "") { + return Effect.void + } + const file = part + const path = path_.join(dir, path_.basename(file.name).slice(-128)) + const filePart = new PersistedFileImpl( + file.key, + file.name, + file.contentType, + path + ) + if (Array.isArray(persisted[part.key])) { + ;(persisted[part.key] as Array).push(filePart) + } else { + persisted[part.key] = [filePart] + } + return writeFile(path, file) + }) + return persisted + }).pipe( + Effect.catchTags({ + SystemError: (cause) => Effect.fail(new MultipartError({ reason: "InternalError", cause })), + BadArgument: (cause) => Effect.fail(new MultipartError({ reason: "InternalError", cause })) + }) + ) + +class PersistedFileImpl extends PartBase implements PersistedFile { + readonly _tag = "PersistedFile" + + constructor( + readonly key: string, + readonly name: string, + readonly contentType: string, + readonly path: string + ) { + super() + } + + toJSON(): unknown { + return { + _id: "@effect/platform/Multipart/Part", + _tag: "PersistedFile", + key: this.key, + name: this.name, + contentType: this.contentType, + path: this.path + } + } +} + +/** + * @since 1.0.0 + * @category fiber refs + */ +export const withLimits: { + (options: { + readonly maxParts?: Option.Option | undefined + readonly maxFieldSize?: FileSystem.SizeInput | undefined + readonly maxFileSize?: Option.Option | undefined + readonly maxTotalSize?: Option.Option | undefined + readonly fieldMimeTypes?: ReadonlyArray | undefined + }): (effect: Effect.Effect) => Effect.Effect + ( + effect: Effect.Effect, + options: { + readonly maxParts?: Option.Option | undefined + readonly maxFieldSize?: FileSystem.SizeInput | undefined + readonly maxFileSize?: Option.Option | undefined + readonly maxTotalSize?: Option.Option | undefined + readonly fieldMimeTypes?: ReadonlyArray | undefined + } + ): Effect.Effect +} = dual(2, ( + effect: Effect.Effect, + options: { + readonly maxParts?: Option.Option | undefined + readonly maxFieldSize?: FileSystem.SizeInput | undefined + readonly maxFileSize?: Option.Option | undefined + readonly maxTotalSize?: Option.Option | undefined + readonly fieldMimeTypes?: ReadonlyArray | undefined + } +): Effect.Effect => Effect.provide(effect, withLimitsContext(options))) + +const withLimitsContext = (options: { + readonly maxParts?: Option.Option | undefined + readonly maxFieldSize?: FileSystem.SizeInput | undefined + readonly maxFileSize?: Option.Option | undefined + readonly maxTotalSize?: Option.Option | undefined + readonly fieldMimeTypes?: ReadonlyArray | undefined +}) => { + const contextMap = new Map() + if (options.maxParts !== undefined) { + contextMap.set(MaxParts.key, options.maxParts) + } + if (options.maxFieldSize !== undefined) { + contextMap.set(MaxFieldSize.key, FileSystem.Size(options.maxFieldSize)) + } + if (options.maxFileSize !== undefined) { + contextMap.set(MaxFileSize.key, Option.map(options.maxFileSize, FileSystem.Size)) + } + if (options.maxTotalSize !== undefined) { + contextMap.set(IncomingMessage.MaxBodySize.key, Option.map(options.maxTotalSize, FileSystem.Size)) + } + if (options.fieldMimeTypes !== undefined) { + contextMap.set(FieldMimeTypes.key, Chunk.fromIterable(options.fieldMimeTypes)) + } + return Context.unsafeMake(contextMap) +} + +/** + * @since 1.0.0 + * @category fiber refs + */ +export const withLimitsStream: { + (options: { + readonly maxParts?: Option.Option | undefined + readonly maxFieldSize?: FileSystem.SizeInput | undefined + readonly maxFileSize?: Option.Option | undefined + readonly maxTotalSize?: Option.Option | undefined + readonly fieldMimeTypes?: ReadonlyArray | undefined + }): (stream: Stream.Stream) => Stream.Stream + ( + stream: Stream.Stream, + options: { + readonly maxParts?: Option.Option | undefined + readonly maxFieldSize?: FileSystem.SizeInput | undefined + readonly maxFileSize?: Option.Option | undefined + readonly maxTotalSize?: Option.Option | undefined + readonly fieldMimeTypes?: ReadonlyArray | undefined + } + ): Stream.Stream +} = dual(2, ( + stream: Stream.Stream, + options: { + readonly maxParts?: Option.Option | undefined + readonly maxFieldSize?: FileSystem.SizeInput | undefined + readonly maxFileSize?: Option.Option | undefined + readonly maxTotalSize?: Option.Option | undefined + readonly fieldMimeTypes?: ReadonlyArray | undefined + } +): Stream.Stream => Stream.provideSomeContext(stream, withLimitsContext(options))) + +/** + * @since 1.0.0 + * @category fiber refs + */ +export declare namespace withLimits { + /** + * @since 1.0.0 + * @category fiber refs + */ + export type Options = { + readonly maxParts?: Option.Option | undefined + readonly maxFieldSize?: FileSystem.SizeInput | undefined + readonly maxFileSize?: Option.Option | undefined + readonly maxTotalSize?: Option.Option | undefined + readonly fieldMimeTypes?: ReadonlyArray | undefined + } +} + +/** + * @since 1.0.0 + * @category fiber refs + */ +export class MaxParts extends Context.Reference()("@effect/platform/Multipart/MaxParts", { + defaultValue: Option.none +}) {} + +/** + * @since 1.0.0 + * @category fiber refs + */ +export const withMaxParts: { + (count: Option.Option): (effect: Effect.Effect) => Effect.Effect + (effect: Effect.Effect, count: Option.Option): Effect.Effect +} = dual( + 2, + (effect: Effect.Effect, count: Option.Option): Effect.Effect => + Effect.provideService(effect, MaxParts, count) +) + +/** + * @since 1.0.0 + * @category fiber refs + */ +export class MaxFieldSize extends Context.Reference()("@effect/platform/Multipart/MaxFieldSize", { + defaultValue: constant(FileSystem.Size(10 * 1024 * 1024)) +}) {} + +/** + * @since 1.0.0 + * @category fiber refs + */ +export const withMaxFieldSize: { + (size: FileSystem.SizeInput): (effect: Effect.Effect) => Effect.Effect + (effect: Effect.Effect, size: FileSystem.SizeInput): Effect.Effect +} = dual( + 2, + (effect: Effect.Effect, size: FileSystem.SizeInput): Effect.Effect => + Effect.provideService(effect, MaxFieldSize, FileSystem.Size(size)) +) + +/** + * @since 1.0.0 + * @category fiber refs + */ +export class MaxFileSize extends Context.Reference()("@effect/platform/Multipart/MaxFileSize", { + defaultValue: Option.none +}) {} + +/** + * @since 1.0.0 + * @category fiber refs + */ +export const withMaxFileSize: { + (size: Option.Option): (effect: Effect.Effect) => Effect.Effect + (effect: Effect.Effect, size: Option.Option): Effect.Effect +} = dual( + 2, + (effect: Effect.Effect, size: Option.Option): Effect.Effect => + Effect.provideService( + effect, + MaxFileSize, + Option.map(size, FileSystem.Size) + ) +) + +/** + * @since 1.0.0 + * @category fiber refs + */ +export class FieldMimeTypes extends Context.Reference()("@effect/platform/Multipart/FieldMimeTypes", { + defaultValue: constant>(Chunk.make("application/json")) +}) {} + +/** + * @since 1.0.0 + * @category fiber refs + */ +export const withFieldMimeTypes: { + (mimeTypes: ReadonlyArray): (effect: Effect.Effect) => Effect.Effect + (effect: Effect.Effect, mimeTypes: ReadonlyArray): Effect.Effect +} = dual( + 2, + (effect: Effect.Effect, mimeTypes: ReadonlyArray): Effect.Effect => + Effect.provideService(effect, FieldMimeTypes, Chunk.fromIterable(mimeTypes)) +) diff --git a/repos/effect/packages/platform/src/Ndjson.ts b/repos/effect/packages/platform/src/Ndjson.ts new file mode 100644 index 0000000..256b296 --- /dev/null +++ b/repos/effect/packages/platform/src/Ndjson.ts @@ -0,0 +1,461 @@ +/** + * @since 1.0.0 + */ +import type * as Cause from "effect/Cause" +import * as Channel from "effect/Channel" +import * as Chunk from "effect/Chunk" +import * as Effect from "effect/Effect" +import { dual, identity } from "effect/Function" +import type { ParseError } from "effect/ParseResult" +import type * as Schema from "effect/Schema" +import * as ChannelSchema from "./ChannelSchema.js" +import { TypeIdError } from "./Error.js" + +/** + * @since 1.0.0 + * @category type ids + */ +export const ErrorTypeId: unique symbol = Symbol.for("@effect/platform/Ndjson/NdjsonError") + +/** + * @since 1.0.0 + * @category type ids + */ +export type NdjsonErrorTypeId = typeof ErrorTypeId + +const encoder = new TextEncoder() + +/** + * @since 1.0.0 + * @category errors + */ +export class NdjsonError extends TypeIdError(ErrorTypeId, "NdjsonError")<{ + readonly reason: "Pack" | "Unpack" + readonly cause: unknown +}> { + get message() { + return this.reason + } +} + +/** + * Represents a set of options which can be used to control how the newline + * delimited JSON is handled. + * + * @since 1.0.0 + * @category models + */ +export interface NdjsonOptions { + /** + * Whether or not the newline delimited JSON parser should ignore empty lines. + * + * Defaults to `false`. + * + * From the [newline delimited JSON spec](https://github.com/ndjson/ndjson-spec): + * ```text + * The parser MAY silently ignore empty lines, e.g. \n\n. This behavior MUST + * be documented and SHOULD be configurable by the user of the parser. + * ``` + * + * @since 1.0.0 + */ + readonly ignoreEmptyLines?: boolean +} + +/** + * @since 1.0.0 + * @category constructors + */ +export const packString = (): Channel.Channel< + Chunk.Chunk, + Chunk.Chunk, + IE | NdjsonError, + IE, + Done, + Done +> => { + const loop: Channel.Channel< + Chunk.Chunk, + Chunk.Chunk, + IE | NdjsonError, + IE, + Done, + Done + > = Channel.readWithCause({ + onInput: (input: Chunk.Chunk) => + Channel.zipRight( + Channel.flatMap( + Effect.try({ + try: () => Chunk.of(Chunk.toReadonlyArray(input).map((_) => JSON.stringify(_)).join("\n") + "\n"), + catch: (cause) => new NdjsonError({ reason: "Pack", cause }) + }), + Channel.write + ), + loop + ), + onFailure: Channel.failCause, + onDone: Channel.succeed + }) + return loop +} + +/** + * @since 1.0.0 + * @category constructors + */ +export const pack = (): Channel.Channel< + Chunk.Chunk, + Chunk.Chunk, + IE | NdjsonError, + IE, + Done, + Done +> => Channel.mapOut(packString(), Chunk.map((_) => encoder.encode(_))) + +/** + * @since 1.0.0 + * @category constructors + */ +export const packSchema = ( + schema: Schema.Schema +) => +(): Channel.Channel< + Chunk.Chunk, + Chunk.Chunk, + IE | NdjsonError | ParseError, + IE, + Done, + Done, + R +> => Channel.pipeTo(ChannelSchema.encode(schema)(), pack()) + +/** + * @since 1.0.0 + * @category constructors + */ +export const packSchemaString = ( + schema: Schema.Schema +) => +(): Channel.Channel< + Chunk.Chunk, + Chunk.Chunk, + IE | NdjsonError | ParseError, + IE, + Done, + Done, + R +> => Channel.pipeTo(ChannelSchema.encode(schema)(), packString()) + +const filterEmpty = Chunk.filter((line) => line.length > 0) +const filterEmptyChannel = () => { + const loop: Channel.Channel< + Chunk.Chunk, + Chunk.Chunk, + IE, + IE, + Done, + Done, + never + > = Channel.readWithCause({ + onInput(input: Chunk.Chunk) { + const filtered = filterEmpty(input) + return Channel.zipRight(Chunk.isEmpty(filtered) ? Channel.void : Channel.write(filtered), loop) + }, + onFailure(cause: Cause.Cause) { + return Channel.failCause(cause) + }, + onDone(done: Done) { + return Channel.succeed(done) + } + }) + return loop +} + +/** + * @since 1.0.0 + * @category constructors + */ +export const unpackString = (options?: NdjsonOptions): Channel.Channel< + Chunk.Chunk, + Chunk.Chunk, + IE | NdjsonError, + IE, + Done, + Done +> => { + const lines = Channel.splitLines().pipe( + options?.ignoreEmptyLines === true ? + Channel.pipeTo(filterEmptyChannel()) : + identity + ) + return Channel.mapOutEffect(lines, (chunk) => + Effect.try({ + try: () => Chunk.map(chunk, (_) => JSON.parse(_)), + catch: (cause) => new NdjsonError({ reason: "Unpack", cause }) + })) +} + +const decodeString = () => { + const decoder = new TextDecoder() + const loop: Channel.Channel< + Chunk.Chunk, + Chunk.Chunk, + IE, + IE, + Done, + Done, + never + > = Channel.readWithCause({ + onInput: (input) => + Channel.zipRight( + Channel.write(Chunk.map(input, (_) => decoder.decode(_))), + loop + ), + onFailure: Channel.failCause, + onDone: Channel.succeed + }) + return loop +} + +/** + * @since 1.0.0 + * @category constructors + */ +export const unpack = (options?: NdjsonOptions): Channel.Channel< + Chunk.Chunk, + Chunk.Chunk, + IE | NdjsonError, + IE, + Done, + Done +> => { + return Channel.pipeTo(decodeString(), unpackString(options)) +} + +/** + * @since 1.0.0 + * @category constructors + */ +export const unpackSchema = ( + schema: Schema.Schema +) => +(options?: NdjsonOptions): Channel.Channel< + Chunk.Chunk, + Chunk.Chunk, + NdjsonError | ParseError | IE, + IE, + Done, + Done, + R +> => Channel.pipeTo(unpack(options), ChannelSchema.decodeUnknown(schema)()) + +/** + * @since 1.0.0 + * @category constructors + */ +export const unpackSchemaString = ( + schema: Schema.Schema +) => +(options?: NdjsonOptions): Channel.Channel< + Chunk.Chunk, + Chunk.Chunk, + NdjsonError | ParseError | IE, + IE, + Done, + Done, + R +> => Channel.pipeTo(unpackString(options), ChannelSchema.decodeUnknown(schema)()) + +/** + * @since 1.0.0 + * @category combinators + */ +export const duplex: { + (options?: NdjsonOptions): ( + self: Channel.Channel, Chunk.Chunk, OE, IE | NdjsonError, OutDone, InDone, R> + ) => Channel.Channel, Chunk.Chunk, NdjsonError | OE, IE, OutDone, InDone, R> + ( + self: Channel.Channel, Chunk.Chunk, OE, IE | NdjsonError, OutDone, InDone, R>, + options?: NdjsonOptions + ): Channel.Channel, Chunk.Chunk, NdjsonError | OE, IE, OutDone, InDone, R> +} = dual((args) => Channel.isChannel(args[0]), ( + self: Channel.Channel, Chunk.Chunk, OE, IE | NdjsonError, OutDone, InDone, R>, + options?: NdjsonOptions +): Channel.Channel, Chunk.Chunk, NdjsonError | OE, IE, OutDone, InDone, R> => + Channel.pipeTo( + Channel.pipeTo(pack(), self), + unpack(options) + )) + +/** + * @since 1.0.0 + * @category combinators + */ +export const duplexString: { + (options?: NdjsonOptions): ( + self: Channel.Channel, Chunk.Chunk, OE, IE | NdjsonError, OutDone, InDone, R> + ) => Channel.Channel, Chunk.Chunk, NdjsonError | OE, IE, OutDone, InDone, R> + ( + self: Channel.Channel, Chunk.Chunk, OE, IE | NdjsonError, OutDone, InDone, R>, + options?: NdjsonOptions + ): Channel.Channel, Chunk.Chunk, NdjsonError | OE, IE, OutDone, InDone, R> +} = dual((args) => Channel.isChannel(args[0]), ( + self: Channel.Channel, Chunk.Chunk, OE, IE | NdjsonError, OutDone, InDone, R>, + options?: NdjsonOptions +): Channel.Channel, Chunk.Chunk, NdjsonError | OE, IE, OutDone, InDone, R> => + Channel.pipeTo( + Channel.pipeTo(packString(), self), + unpackString(options) + )) + +/** + * @since 1.0.0 + * @category combinators + */ +export const duplexSchema: { + ( + options: Partial & { + readonly inputSchema: Schema.Schema + readonly outputSchema: Schema.Schema + } + ): ( + self: Channel.Channel< + Chunk.Chunk, + Chunk.Chunk, + OutErr, + NdjsonError | ParseError | InErr, + OutDone, + InDone, + R + > + ) => Channel.Channel< + Chunk.Chunk, + Chunk.Chunk, + NdjsonError | ParseError | OutErr, + InErr, + OutDone, + InDone, + R | IR | OR + > + ( + self: Channel.Channel< + Chunk.Chunk, + Chunk.Chunk, + OutErr, + NdjsonError | ParseError | InErr, + OutDone, + InDone, + R + >, + options: Partial & { + readonly inputSchema: Schema.Schema + readonly outputSchema: Schema.Schema + } + ): Channel.Channel< + Chunk.Chunk, + Chunk.Chunk, + NdjsonError | ParseError | OutErr, + InErr, + OutDone, + InDone, + R | IR | OR + > +} = dual(2, ( + self: Channel.Channel< + Chunk.Chunk, + Chunk.Chunk, + OutErr, + NdjsonError | ParseError | InErr, + OutDone, + InDone, + R + >, + options: Partial & { + readonly inputSchema: Schema.Schema + readonly outputSchema: Schema.Schema + } +): Channel.Channel< + Chunk.Chunk, + Chunk.Chunk, + NdjsonError | ParseError | OutErr, + InErr, + OutDone, + InDone, + R | IR | OR +> => ChannelSchema.duplexUnknown(duplex(self, options), options)) + +/** + * @since 1.0.0 + * @category combinators + */ +export const duplexSchemaString: { + ( + options: Partial & { + readonly inputSchema: Schema.Schema + readonly outputSchema: Schema.Schema + } + ): ( + self: Channel.Channel< + Chunk.Chunk, + Chunk.Chunk, + OutErr, + NdjsonError | ParseError | InErr, + OutDone, + InDone, + R + > + ) => Channel.Channel< + Chunk.Chunk, + Chunk.Chunk, + NdjsonError | ParseError | OutErr, + InErr, + OutDone, + InDone, + R | IR | OR + > + ( + self: Channel.Channel< + Chunk.Chunk, + Chunk.Chunk, + OutErr, + NdjsonError | ParseError | InErr, + OutDone, + InDone, + R + >, + options: Partial & { + readonly inputSchema: Schema.Schema + readonly outputSchema: Schema.Schema + } + ): Channel.Channel< + Chunk.Chunk, + Chunk.Chunk, + NdjsonError | ParseError | OutErr, + InErr, + OutDone, + InDone, + R | IR | OR + > +} = dual(2, ( + self: Channel.Channel< + Chunk.Chunk, + Chunk.Chunk, + OutErr, + NdjsonError | ParseError | InErr, + OutDone, + InDone, + R + >, + options: Partial & { + readonly inputSchema: Schema.Schema + readonly outputSchema: Schema.Schema + } +): Channel.Channel< + Chunk.Chunk, + Chunk.Chunk, + NdjsonError | ParseError | OutErr, + InErr, + OutDone, + InDone, + R | IR | OR +> => ChannelSchema.duplexUnknown(duplexString(self, options), options)) diff --git a/repos/effect/packages/platform/src/OpenApi.ts b/repos/effect/packages/platform/src/OpenApi.ts new file mode 100644 index 0000000..5bb7b37 --- /dev/null +++ b/repos/effect/packages/platform/src/OpenApi.ts @@ -0,0 +1,719 @@ +/** + * @since 1.0.0 + */ +import type { NonEmptyArray } from "effect/Array" +import * as Context from "effect/Context" +import { constFalse } from "effect/Function" +import { globalValue } from "effect/GlobalValue" +import * as Option from "effect/Option" +import type * as Schema from "effect/Schema" +import type * as AST from "effect/SchemaAST" +import * as HttpApi from "./HttpApi.js" +import type { HttpApiGroup } from "./HttpApiGroup.js" +import * as HttpApiMiddleware from "./HttpApiMiddleware.js" +import * as HttpApiSchema from "./HttpApiSchema.js" +import type { HttpApiSecurity } from "./HttpApiSecurity.js" +import * as HttpMethod from "./HttpMethod.js" +import * as JsonSchema from "./OpenApiJsonSchema.js" + +/** + * @since 1.0.0 + * @category annotations + */ +export class Identifier extends Context.Tag("@effect/platform/OpenApi/Identifier")() {} + +/** + * @since 1.0.0 + * @category annotations + */ +export class Title extends Context.Tag("@effect/platform/OpenApi/Title")() {} + +/** + * @since 1.0.0 + * @category annotations + */ +export class Version extends Context.Tag("@effect/platform/OpenApi/Version")() {} + +/** + * @since 1.0.0 + * @category annotations + */ +export class Description extends Context.Tag("@effect/platform/OpenApi/Description")() {} + +/** + * @since 1.0.0 + * @category annotations + */ +export class License extends Context.Tag("@effect/platform/OpenApi/License")() {} + +/** + * @since 1.0.0 + * @category annotations + */ +export class ExternalDocs + extends Context.Tag("@effect/platform/OpenApi/ExternalDocs")() +{} + +/** + * @since 1.0.0 + * @category annotations + */ +export class Servers + extends Context.Tag("@effect/platform/OpenApi/Servers")>() +{} + +/** + * @since 1.0.0 + * @category annotations + */ +export class Format extends Context.Tag("@effect/platform/OpenApi/Format")() {} + +/** + * @since 1.0.0 + * @category annotations + */ +export class Summary extends Context.Tag("@effect/platform/OpenApi/Summary")() {} + +/** + * @since 1.0.0 + * @category annotations + */ +export class Deprecated extends Context.Tag("@effect/platform/OpenApi/Deprecated")() {} + +/** + * @since 1.0.0 + * @category annotations + */ +export class Override extends Context.Tag("@effect/platform/OpenApi/Override")>() {} + +/** + * @since 1.0.0 + * @category annotations + */ +export class Exclude extends Context.Reference()("@effect/platform/OpenApi/Exclude", { + defaultValue: constFalse +}) {} + +/** + * Transforms the generated OpenAPI specification + * @since 1.0.0 + * @category annotations + */ +export class Transform extends Context.Tag("@effect/platform/OpenApi/Transform")< + Transform, + (openApiSpec: Record) => Record +>() {} + +const contextPartial = >>(tags: Tags): ( + options: { + readonly [K in keyof Tags]?: Context.Tag.Service | undefined + } +) => Context.Context => { + const entries = Object.entries(tags) + return (options) => { + let context = Context.empty() + for (const [key, tag] of entries) { + if (options[key] !== undefined) { + context = Context.add(context, tag, options[key]!) + } + } + return context + } +} + +/** + * @since 1.0.0 + * @category annotations + */ +export const annotations: ( + options: { + readonly identifier?: string | undefined + readonly title?: string | undefined + readonly version?: string | undefined + readonly description?: string | undefined + readonly license?: OpenAPISpecLicense | undefined + readonly summary?: string | undefined + readonly deprecated?: boolean | undefined + readonly externalDocs?: OpenAPISpecExternalDocs | undefined + readonly servers?: ReadonlyArray | undefined + readonly format?: string | undefined + readonly override?: Record | undefined + readonly exclude?: boolean | undefined + readonly transform?: ((openApiSpec: Record) => Record) | undefined + } +) => Context.Context = contextPartial({ + identifier: Identifier, + title: Title, + version: Version, + description: Description, + license: License, + summary: Summary, + deprecated: Deprecated, + externalDocs: ExternalDocs, + servers: Servers, + format: Format, + override: Override, + exclude: Exclude, + transform: Transform +}) + +const apiCache = globalValue("@effect/platform/OpenApi/apiCache", () => new WeakMap()) + +/** + * This function checks if a given tag exists within the provided context. If + * the tag is present, it retrieves the associated value and applies the given + * callback function to it. If the tag is not found, the function does nothing. + */ +function processAnnotation( + ctx: Context.Context, + tag: Context.Tag, + f: (s: S) => void +) { + const o = Context.getOption(ctx, tag) + if (Option.isSome(o)) { + f(o.value) + } +} + +/** + * @since 1.0.0 + * @category models + */ +export type AdditionalPropertiesStrategy = "allow" | "strict" + +/** + * Converts an `HttpApi` instance into an OpenAPI Specification object. + * + * **Details** + * + * This function takes an `HttpApi` instance, which defines a structured API, + * and generates an OpenAPI Specification (`OpenAPISpec`). The resulting spec + * adheres to the OpenAPI 3.1.0 standard and includes detailed metadata such as + * paths, operations, security schemes, and components. The function processes + * the API's annotations, middleware, groups, and endpoints to build a complete + * and accurate representation of the API in OpenAPI format. + * + * The function also deduplicates schemas, applies transformations, and + * integrates annotations like descriptions, summaries, external documentation, + * and overrides. Cached results are used for better performance when the same + * `HttpApi` instance is processed multiple times. + * + * **Options** + * + * - `additionalPropertiesStrategy`: Controls the handling of additional properties. Possible values are: + * - `"strict"`: Disallow additional properties (default behavior). + * - `"allow"`: Allow additional properties. + * + * **Example** + * + * ```ts + * import { HttpApi, HttpApiEndpoint, HttpApiGroup, OpenApi } from "@effect/platform" + * import { Schema } from "effect" + * + * const api = HttpApi.make("api").add( + * HttpApiGroup.make("group").add( + * HttpApiEndpoint.get("get", "/items") + * .addSuccess(Schema.Array(Schema.String)) + * ) + * ) + * + * const spec = OpenApi.fromApi(api) + * + * console.log(JSON.stringify(spec, null, 2)) + * // Output: OpenAPI specification in JSON format + * ``` + * + * @category constructors + * @since 1.0.0 + */ +export const fromApi = ( + api: HttpApi.HttpApi, + options?: { + readonly additionalPropertiesStrategy?: AdditionalPropertiesStrategy | undefined + } | undefined +): OpenAPISpec => { + const cached = apiCache.get(api) + if (cached !== undefined) { + return cached + } + const jsonSchemaDefs: Record = {} + let spec: OpenAPISpec = { + openapi: "3.1.0", + info: { + title: Context.getOrElse(api.annotations, Title, () => "Api"), + version: Context.getOrElse(api.annotations, Version, () => "0.0.1") + }, + paths: {}, + components: { + schemas: jsonSchemaDefs, + securitySchemes: {} + }, + security: [], + tags: [] + } + + function processAST(ast: AST.AST): JsonSchema.JsonSchema { + return JsonSchema.fromAST(ast, { + defs: jsonSchemaDefs, + additionalPropertiesStrategy: options?.additionalPropertiesStrategy + }) + } + + function processHttpApiSecurity( + name: string, + security: HttpApiSecurity + ) { + if (spec.components.securitySchemes[name] !== undefined) { + return + } + spec.components.securitySchemes[name] = makeSecurityScheme(security) + } + + processAnnotation(api.annotations, HttpApi.AdditionalSchemas, (componentSchemas) => { + componentSchemas.forEach((componentSchema) => processAST(componentSchema.ast)) + }) + processAnnotation(api.annotations, Description, (description) => { + spec.info.description = description + }) + processAnnotation(api.annotations, License, (license) => { + spec.info.license = license + }) + processAnnotation(api.annotations, Summary, (summary) => { + spec.info.summary = summary + }) + processAnnotation(api.annotations, Servers, (servers) => { + spec.servers = [...servers] + }) + + api.middlewares.forEach((middleware) => { + if (!HttpApiMiddleware.isSecurity(middleware)) { + return + } + for (const [name, security] of Object.entries(middleware.security)) { + processHttpApiSecurity(name, security) + spec.security.push({ [name]: [] }) + } + }) + HttpApi.reflect(api, { + onGroup({ group }) { + if (Context.get(group.annotations, Exclude)) { + return + } + let tag: OpenAPISpecTag = { + name: Context.getOrElse(group.annotations, Title, () => group.identifier) + } + + processAnnotation(group.annotations, Description, (description) => { + tag.description = description + }) + processAnnotation(group.annotations, ExternalDocs, (externalDocs) => { + tag.externalDocs = externalDocs + }) + processAnnotation(group.annotations, Override, (override) => { + Object.assign(tag, override) + }) + processAnnotation(group.annotations, Transform, (transformFn) => { + tag = transformFn(tag) as OpenAPISpecTag + }) + + spec.tags.push(tag) + }, + onEndpoint({ endpoint, errors, group, mergedAnnotations, middleware, payloads, successes }) { + if (Context.get(mergedAnnotations, Exclude)) { + return + } + let op: OpenAPISpecOperation = { + tags: [Context.getOrElse(group.annotations, Title, () => group.identifier)], + operationId: Context.getOrElse( + endpoint.annotations, + Identifier, + () => group.topLevel ? endpoint.name : `${group.identifier}.${endpoint.name}` + ), + parameters: [], + security: [], + responses: {} + } + + function processResponseMap( + map: ReadonlyMap + readonly description: Option.Option + }>, + defaultDescription: () => string + ) { + for (const [status, { ast, description }] of map) { + if (op.responses[status]) continue + op.responses[status] = { + description: Option.getOrElse(description, defaultDescription) + } + ast.pipe( + Option.filter((ast) => !HttpApiSchema.getEmptyDecodeable(ast)), + Option.map((ast) => { + const encoding = HttpApiSchema.getEncoding(ast) + op.responses[status].content = { + [encoding.contentType]: { + schema: processAST(ast) + } + } + }) + ) + } + } + + function processParameters(schema: Option.Option, i: OpenAPISpecParameter["in"]) { + if (Option.isSome(schema)) { + const jsonSchema = processAST(schema.value.ast) + if ("properties" in jsonSchema) { + Object.entries(jsonSchema.properties).forEach(([name, psJsonSchema]) => { + op.parameters.push({ + name, + in: i, + schema: psJsonSchema, + required: jsonSchema.required.includes(name), + ...(psJsonSchema.description !== undefined ? { description: psJsonSchema.description } : undefined) + }) + }) + } + } + } + + processAnnotation(endpoint.annotations, Description, (description) => { + op.description = description + }) + processAnnotation(endpoint.annotations, Summary, (summary) => { + op.summary = summary + }) + processAnnotation(endpoint.annotations, Deprecated, (deprecated) => { + op.deprecated = deprecated + }) + processAnnotation(endpoint.annotations, ExternalDocs, (externalDocs) => { + op.externalDocs = externalDocs + }) + + middleware.forEach((middleware) => { + if (!HttpApiMiddleware.isSecurity(middleware)) { + return + } + for (const [name, security] of Object.entries(middleware.security)) { + processHttpApiSecurity(name, security) + op.security.push({ [name]: [] }) + } + }) + const hasBody = HttpMethod.hasBody(endpoint.method) + if (hasBody && payloads.size > 0) { + const content: OpenApiSpecContent = {} + payloads.forEach(({ ast }, contentType) => { + content[contentType as OpenApiSpecContentType] = { + schema: processAST(ast) + } + }) + op.requestBody = { content, required: true } + } + + processParameters(endpoint.pathSchema, "path") + if (!hasBody) { + processParameters(endpoint.payloadSchema, "query") + } + processParameters(endpoint.headersSchema, "header") + processParameters(endpoint.urlParamsSchema, "query") + + processResponseMap(successes, () => "Success") + processResponseMap(errors, () => "Error") + + const path = endpoint.path.replace(/:(\w+)\??/g, "{$1}") + const method = endpoint.method.toLowerCase() as OpenAPISpecMethodName + if (!spec.paths[path]) { + spec.paths[path] = {} + } + + processAnnotation(endpoint.annotations, Override, (override) => { + Object.assign(op, override) + }) + processAnnotation(endpoint.annotations, Transform, (transformFn) => { + op = transformFn(op) as OpenAPISpecOperation + }) + + spec.paths[path][method] = op + } + }) + + processAnnotation(api.annotations, Override, (override) => { + Object.assign(spec, override) + }) + processAnnotation(api.annotations, Transform, (transformFn) => { + spec = transformFn(spec) as OpenAPISpec + }) + + apiCache.set(api, spec) + + return spec +} + +const makeSecurityScheme = (security: HttpApiSecurity): OpenAPISecurityScheme => { + const meta: Partial = {} + processAnnotation(security.annotations, Description, (description) => { + meta.description = description + }) + switch (security._tag) { + case "Basic": { + return { + ...meta, + type: "http", + scheme: "basic" + } + } + case "Bearer": { + const format = Context.getOption(security.annotations, Format).pipe( + Option.map((format) => ({ bearerFormat: format })), + Option.getOrUndefined + ) + return { + ...meta, + type: "http", + scheme: "bearer", + ...format + } + } + case "ApiKey": { + return { + ...meta, + type: "apiKey", + name: security.key, + in: security.in + } + } + } +} + +/** + * This model describes the OpenAPI specification (version 3.1.0) returned by + * {@link fromApi}. It is not intended to describe the entire OpenAPI + * specification, only the output of `fromApi`. + * + * @category models + * @since 1.0.0 + */ +export interface OpenAPISpec { + openapi: "3.1.0" + info: OpenAPISpecInfo + paths: OpenAPISpecPaths + components: OpenAPIComponents + security: Array + tags: Array + servers?: Array +} + +/** + * @category models + * @since 1.0.0 + */ +export interface OpenAPISpecInfo { + title: string + version: string + description?: string + license?: OpenAPISpecLicense + summary?: string +} + +/** + * @category models + * @since 1.0.0 + */ +export interface OpenAPISpecTag { + name: string + description?: string + externalDocs?: OpenAPISpecExternalDocs +} + +/** + * @category models + * @since 1.0.0 + */ +export interface OpenAPISpecExternalDocs { + url: string + description?: string +} + +/** + * @category models + * @since 1.0.0 + */ +export interface OpenAPISpecLicense { + name: string + url?: string +} + +/** + * @category models + * @since 1.0.0 + */ +export interface OpenAPISpecServer { + url: string + description?: string + variables?: Record +} + +/** + * @category models + * @since 1.0.0 + */ +export interface OpenAPISpecServerVariable { + default: string + enum?: NonEmptyArray + description?: string +} + +/** + * @category models + * @since 1.0.0 + */ +export type OpenAPISpecPaths = Record + +/** + * @category models + * @since 1.0.0 + */ +export type OpenAPISpecMethodName = + | "get" + | "put" + | "post" + | "delete" + | "options" + | "head" + | "patch" + | "trace" + +/** + * @category models + * @since 1.0.0 + */ +export type OpenAPISpecPathItem = { + [K in OpenAPISpecMethodName]?: OpenAPISpecOperation +} + +/** + * @category models + * @since 1.0.0 + */ +export interface OpenAPISpecParameter { + name: string + in: "query" | "header" | "path" | "cookie" + schema: JsonSchema.JsonSchema + required: boolean + description?: string +} + +/** + * @category models + * @since 1.0.0 + */ +export type OpenAPISpecResponses = Record + +/** + * @category models + * @since 1.0.0 + */ +export type OpenApiSpecContentType = + | "application/json" + | "application/xml" + | "application/x-www-form-urlencoded" + | "multipart/form-data" + | "text/plain" + +/** + * @category models + * @since 1.0.0 + */ +export type OpenApiSpecContent = { + [K in OpenApiSpecContentType]?: OpenApiSpecMediaType +} + +/** + * @category models + * @since 1.0.0 + */ +export interface OpenApiSpecResponse { + description: string + content?: OpenApiSpecContent +} + +/** + * @category models + * @since 1.0.0 + */ +export interface OpenApiSpecMediaType { + schema: JsonSchema.JsonSchema +} + +/** + * @category models + * @since 1.0.0 + */ +export interface OpenAPISpecRequestBody { + content: OpenApiSpecContent + required: true +} + +/** + * @category models + * @since 1.0.0 + */ +export interface OpenAPIComponents { + schemas: Record + securitySchemes: Record +} + +/** + * @category models + * @since 1.0.0 + */ +export interface OpenAPIHTTPSecurityScheme { + readonly type: "http" + scheme: "bearer" | "basic" | string + description?: string + /* only for scheme: 'bearer' */ + bearerFormat?: string +} + +/** + * @category models + * @since 1.0.0 + */ +export interface OpenAPIApiKeySecurityScheme { + readonly type: "apiKey" + name: string + in: "query" | "header" | "cookie" + description?: string +} + +/** + * @category models + * @since 1.0.0 + */ +export type OpenAPISecurityScheme = + | OpenAPIHTTPSecurityScheme + | OpenAPIApiKeySecurityScheme + +/** + * @category models + * @since 1.0.0 + */ +export type OpenAPISecurityRequirement = Record> + +/** + * @category models + * @since 1.0.0 + */ +export interface OpenAPISpecOperation { + operationId: string + parameters: Array + responses: OpenAPISpecResponses + /** Always contains at least the title annotation or the group identifier */ + tags: NonEmptyArray + security: Array + requestBody?: OpenAPISpecRequestBody + description?: string + summary?: string + deprecated?: boolean + externalDocs?: OpenAPISpecExternalDocs +} diff --git a/repos/effect/packages/platform/src/OpenApiJsonSchema.ts b/repos/effect/packages/platform/src/OpenApiJsonSchema.ts new file mode 100644 index 0000000..32669b9 --- /dev/null +++ b/repos/effect/packages/platform/src/OpenApiJsonSchema.ts @@ -0,0 +1,301 @@ +/** + * @since 1.0.0 + */ +import * as JSONSchema from "effect/JSONSchema" +import * as Record from "effect/Record" +import type * as Schema from "effect/Schema" +import type * as AST from "effect/SchemaAST" + +/** + * @category model + * @since 1.0.0 + */ +export interface Annotations { + title?: string + description?: string + default?: unknown + examples?: globalThis.Array +} + +/** + * @category model + * @since 1.0.0 + */ +export interface Never extends Annotations { + $id: "/schemas/never" + not: {} + nullable?: boolean +} + +/** + * @category model + * @since 1.0.0 + */ +export interface Any extends Annotations { + $id: "/schemas/any" +} + +/** + * @category model + * @since 1.0.0 + */ +export interface Unknown extends Annotations { + $id: "/schemas/unknown" +} + +/** + * @category model + * @since 0.69.0 + */ +export interface Void extends Annotations { + $id: "/schemas/void" +} + +/** + * @category model + * @since 0.71.0 + */ +export interface AnyObject extends Annotations { + $id: "/schemas/object" + anyOf: [ + { type: "object" }, + { type: "array" } + ] + nullable?: boolean +} + +/** + * @category model + * @since 0.71.0 + */ +export interface Empty extends Annotations { + $id: "/schemas/%7B%7D" + anyOf: [ + { type: "object" }, + { type: "array" } + ] + nullable?: boolean +} + +/** + * @category model + * @since 1.0.0 + */ +export interface Ref extends Annotations { + $ref: string + nullable?: boolean +} + +/** + * @category model + * @since 1.0.0 + */ +export interface String extends Annotations { + type: "string" + minLength?: number + maxLength?: number + pattern?: string + format?: string + contentMediaType?: string + contentSchema?: JsonSchema + allOf?: globalThis.Array<{ + minLength?: number + maxLength?: number + pattern?: string + }> + nullable?: boolean +} + +/** + * @category model + * @since 1.0.0 + */ +export interface Numeric extends Annotations { + minimum?: number + exclusiveMinimum?: number + maximum?: number + exclusiveMaximum?: number + multipleOf?: number + format?: string + allOf?: globalThis.Array<{ + minimum?: number + exclusiveMinimum?: number + maximum?: number + exclusiveMaximum?: number + multipleOf?: number + }> + nullable?: boolean +} + +/** + * @category model + * @since 1.0.0 + */ +export interface Number extends Numeric { + type: "number" +} + +/** + * @category model + * @since 1.0.0 + */ +export interface Integer extends Numeric { + type: "integer" +} + +/** + * @category model + * @since 1.0.0 + */ +export interface Boolean extends Annotations { + type: "boolean" + nullable?: boolean +} + +/** + * @category model + * @since 1.0.0 + */ +export interface Array extends Annotations { + type: "array" + items?: JsonSchema | globalThis.Array + minItems?: number + maxItems?: number + additionalItems?: JsonSchema | boolean + nullable?: boolean +} + +/** + * @category model + * @since 1.0.0 + */ +export interface Enum extends Annotations { + type?: "string" | "number" | "boolean" + enum: globalThis.Array + nullable?: boolean +} + +/** + * @category model + * @since 0.71.0 + */ +export interface Enums extends Annotations { + $comment: "/schemas/enums" + anyOf: globalThis.Array<{ + type: "string" | "number" + title: string + enum: [string | number] + }> + nullable?: boolean +} + +/** + * @category model + * @since 1.0.0 + */ +export interface AnyOf extends Annotations { + anyOf: globalThis.Array + nullable?: boolean +} + +/** + * @category model + * @since 1.0.0 + */ +export interface Object extends Annotations { + type: "object" + required: globalThis.Array + properties: Record + additionalProperties?: boolean | JsonSchema + patternProperties?: Record + propertyNames?: JsonSchema + nullable?: boolean +} + +/** + * @category model + * @since 0.71.0 + */ +export type JsonSchema = + | Never + | Any + | Unknown + | Void + | AnyObject + | Empty + | Ref + | String + | Number + | Integer + | Boolean + | Array + | Enum + | Enums + | AnyOf + | Object + +/** + * @category model + * @since 1.0.0 + */ +export type Root = JsonSchema & { + $defs?: Record +} + +/** + * @category encoding + * @since 1.0.0 + */ +export const make = (schema: Schema.Schema): Root => { + const defs: Record = {} + const out: Root = makeWithDefs(schema, { defs }) + if (!Record.isEmptyRecord(defs)) { + out.$defs = defs + } + return out +} + +type TopLevelReferenceStrategy = "skip" | "keep" + +type AdditionalPropertiesStrategy = "allow" | "strict" + +/** + * Creates a schema with additional options and definitions. + * + * **Options** + * + * - `defs`: A record of definitions that are included in the schema. + * - `defsPath`: The path to the definitions within the schema (defaults to "#/$defs/"). + * - `topLevelReferenceStrategy`: Controls the handling of the top-level reference. Possible values are: + * - `"keep"`: Keep the top-level reference (default behavior). + * - `"skip"`: Skip the top-level reference. + * - `additionalPropertiesStrategy`: Controls the handling of additional properties. Possible values are: + * - `"strict"`: Disallow additional properties (default behavior). + * - `"allow"`: Allow additional properties. + * + * @category encoding + * @since 1.0.0 + */ +export const makeWithDefs = (schema: Schema.Schema, options: { + readonly defs: Record + readonly defsPath?: string | undefined + readonly topLevelReferenceStrategy?: TopLevelReferenceStrategy | undefined + readonly additionalPropertiesStrategy?: AdditionalPropertiesStrategy | undefined +}): JsonSchema => fromAST(schema.ast, options) + +/** @internal */ +export const fromAST = (ast: AST.AST, options: { + readonly defs: Record + readonly defsPath?: string | undefined + readonly topLevelReferenceStrategy?: TopLevelReferenceStrategy | undefined + readonly additionalPropertiesStrategy?: AdditionalPropertiesStrategy | undefined +}): JsonSchema => { + const jsonSchema = JSONSchema.fromAST(ast, { + definitions: options.defs, + definitionPath: options.defsPath ?? "#/components/schemas/", + target: "openApi3.1", + topLevelReferenceStrategy: options.topLevelReferenceStrategy, + additionalPropertiesStrategy: options.additionalPropertiesStrategy + }) + return jsonSchema as JsonSchema +} diff --git a/repos/effect/packages/platform/src/Path.ts b/repos/effect/packages/platform/src/Path.ts new file mode 100644 index 0000000..f790eb4 --- /dev/null +++ b/repos/effect/packages/platform/src/Path.ts @@ -0,0 +1,77 @@ +/** + * @since 1.0.0 + */ + +import type { Tag } from "effect/Context" +import type { Effect } from "effect/Effect" +import type { Layer } from "effect/Layer" +import type { BadArgument } from "./Error.js" +import * as internal from "./internal/path.js" + +/** + * @since 1.0.0 + * @category type ids + */ +export const TypeId: unique symbol = internal.TypeId + +/** + * @since 1.0.0 + * @category type ids + */ +export type TypeId = typeof TypeId + +/** + * @since 1.0.0 + * @category model + */ +export interface Path { + readonly [TypeId]: TypeId + readonly sep: string + readonly basename: (path: string, suffix?: string) => string + readonly dirname: (path: string) => string + readonly extname: (path: string) => string + readonly format: (pathObject: Partial) => string + readonly fromFileUrl: (url: URL) => Effect + readonly isAbsolute: (path: string) => boolean + readonly join: (...paths: ReadonlyArray) => string + readonly normalize: (path: string) => string + readonly parse: (path: string) => Path.Parsed + readonly relative: (from: string, to: string) => string + readonly resolve: (...pathSegments: ReadonlyArray) => string + readonly toFileUrl: (path: string) => Effect + readonly toNamespacedPath: (path: string) => string +} + +/** + * @since 1.0.0 + */ +export declare namespace Path { + /** + * @since 1.0.0 + * @category model + */ + export interface Parsed { + readonly root: string + readonly dir: string + readonly base: string + readonly ext: string + readonly name: string + } +} + +/** + * @since 1.0.0 + * @category tag + */ +export const Path: Tag = internal.Path + +/** + * An implementation of the Path interface that can be used in all environments + * (including browsers). + * + * It uses the POSIX standard for paths. + * + * @since 1.0.0 + * @category layer + */ +export const layer: Layer = internal.layer diff --git a/repos/effect/packages/platform/src/PlatformConfigProvider.ts b/repos/effect/packages/platform/src/PlatformConfigProvider.ts new file mode 100644 index 0000000..9e49b69 --- /dev/null +++ b/repos/effect/packages/platform/src/PlatformConfigProvider.ts @@ -0,0 +1,143 @@ +/** + * @since 1.0.0 + */ +import * as Arr from "effect/Array" +import * as Cause from "effect/Cause" +import type * as Config from "effect/Config" +import * as ConfigError from "effect/ConfigError" +import * as ConfigProvider from "effect/ConfigProvider" +import * as PathPatch from "effect/ConfigProviderPathPatch" +import * as Context from "effect/Context" +import * as DefaultServices from "effect/DefaultServices" +import * as Effect from "effect/Effect" +import * as Either from "effect/Either" +import * as HashSet from "effect/HashSet" +import * as Layer from "effect/Layer" +import { isPlatformError, type PlatformError } from "./Error.js" +import * as FileSystem from "./FileSystem.js" +import * as internal from "./internal/platformConfigProvider.js" +import * as Path from "./Path.js" + +/** + * @since 1.0.0 + * @category constructors + */ +export const fromFileTree = (options?: { + readonly rootDirectory?: string +}): Effect.Effect => + Effect.Do.pipe( + Effect.bind("path", () => Path.Path), + Effect.bind("fs", () => FileSystem.FileSystem), + Effect.map(({ fs, path }) => { + const rootDirectory = options?.rootDirectory ?? "/" + + const parseConfig = (primitive: Config.Config.Primitive) => (value: string) => + Either.map(primitive.parse(value.trim()), Arr.of) + + const readConfig = (filePath: string, primitive: Config.Config.Primitive) => + Effect.flatMap( + fs.readFileString(filePath), + parseConfig(primitive) + ) + + const resolveEnumerableDirs = (segments: ReadonlyArray) => + segments.length === 0 ? [] : [path.join(...segments)] + + const resolveFilePath = (pathSegments: ReadonlyArray) => path.join(rootDirectory, ...pathSegments) + + const sourceError = (pathSegments: ReadonlyArray, error: PlatformError) => + ConfigError.SourceUnavailable( + [...pathSegments], + error.description ?? error.message, + Cause.fail(error) + ) + const pathNotFoundError = (pathSegments: ReadonlyArray) => + ConfigError.MissingData( + [...pathSegments], + `Path ${resolveFilePath(pathSegments)} not found` + ) + const handlePlatformError = (pathSegments: ReadonlyArray) => (error: PlatformError) => + error._tag === "SystemError" && error.reason === "NotFound" + ? Effect.fail(pathNotFoundError(pathSegments)) + : Effect.fail(sourceError(pathSegments, error)) + + return ConfigProvider.fromFlat( + ConfigProvider.makeFlat({ + load: (pathSegments, config) => + Effect.catchIf( + readConfig(resolveFilePath(pathSegments), config), + isPlatformError, + handlePlatformError(pathSegments) + ), + enumerateChildren: (pathSegments) => + Effect.forEach(resolveEnumerableDirs(pathSegments), (dir) => fs.readDirectory(dir)).pipe( + Effect.map((files) => HashSet.fromIterable(files.flat())), + Effect.catchIf(isPlatformError, handlePlatformError(pathSegments)) + ), + patch: PathPatch.empty + }) + ) + }) + ) + +/** + * Add the file tree ConfigProvider to the environment, as a fallback to the current ConfigProvider. + * + * @since 1.0.0 + * @category layers + */ +export const layerFileTreeAdd = (options?: { + readonly rootDirectory?: string +}): Layer.Layer => + fromFileTree(options).pipe( + Effect.map((provider) => + Layer.fiberRefLocallyScopedWith(DefaultServices.currentServices, (services) => { + const current = Context.get(services, ConfigProvider.ConfigProvider) + return Context.add(services, ConfigProvider.ConfigProvider, ConfigProvider.orElse(current, () => provider)) + }) + ), + Layer.unwrapEffect + ) + +/** + * Add the file tree ConfigProvider to the environment, replacing the current ConfigProvider. + * + * @since 1.0.0 + * @category layers + */ +export const layerFileTree = (options?: { + readonly rootDirectory?: string +}): Layer.Layer => + fromFileTree(options).pipe( + Effect.map(Layer.setConfigProvider), + Layer.unwrapEffect + ) + +/** + * Create a dotenv ConfigProvider. + * + * @category constructors + * @since 1.0.0 + */ +export const fromDotEnv: ( + paths: string +) => Effect.Effect = internal.fromDotEnv + +/** + * Add the dotenv ConfigProvider to the environment, as a fallback to the current ConfigProvider. + * If the file is not found, a debug log is produced and empty layer is returned. + * + * @since 1.0.0 + * @category layers + */ +export const layerDotEnvAdd: (path: string) => Layer.Layer = + internal.layerDotEnvAdd + +/** + * Add the dotenv ConfigProvider to the environment, replacing the current ConfigProvider. + * + * @since 1.0.0 + * @category layers + */ +export const layerDotEnv: (path: string) => Layer.Layer = + internal.layerDotEnv diff --git a/repos/effect/packages/platform/src/PlatformLogger.ts b/repos/effect/packages/platform/src/PlatformLogger.ts new file mode 100644 index 0000000..c3049ca --- /dev/null +++ b/repos/effect/packages/platform/src/PlatformLogger.ts @@ -0,0 +1,63 @@ +/** + * @since 1.0.0 + */ +import type { DurationInput } from "effect/Duration" +import type { Effect } from "effect/Effect" +import type * as Logger from "effect/Logger" +import type { Scope } from "effect/Scope" +import type { PlatformError } from "./Error.js" +import type { FileSystem, OpenFileOptions } from "./FileSystem.js" +import * as internal from "./internal/platformLogger.js" + +/** + * Create a Logger from another string Logger that writes to the specified file. + * + * **Example** + * + * ```ts + * import { PlatformLogger } from "@effect/platform" + * import { NodeFileSystem, NodeRuntime } from "@effect/platform-node" + * import { Effect, Layer, Logger } from "effect" + * + * const fileLogger = Logger.logfmtLogger.pipe( + * PlatformLogger.toFile("/tmp/log.txt") + * ) + * const LoggerLive = Logger.replaceScoped(Logger.defaultLogger, fileLogger).pipe( + * Layer.provide(NodeFileSystem.layer) + * ) + * + * Effect.log("a").pipe( + * Effect.zipRight(Effect.log("b")), + * Effect.zipRight(Effect.log("c")), + * Effect.provide(LoggerLive), + * NodeRuntime.runMain + * ) + * ``` + * + * @since 1.0.0 + */ +export const toFile: { + ( + path: string, + options?: + | (OpenFileOptions & { + readonly batchWindow?: + | DurationInput + | undefined + }) + | undefined + ): ( + self: Logger.Logger + ) => Effect, PlatformError, Scope | FileSystem> + ( + self: Logger.Logger, + path: string, + options?: + | (OpenFileOptions & { + readonly batchWindow?: + | DurationInput + | undefined + }) + | undefined + ): Effect, PlatformError, Scope | FileSystem> +} = internal.toFile diff --git a/repos/effect/packages/platform/src/Runtime.ts b/repos/effect/packages/platform/src/Runtime.ts new file mode 100644 index 0000000..a678ba9 --- /dev/null +++ b/repos/effect/packages/platform/src/Runtime.ts @@ -0,0 +1,153 @@ +/** + * @since 1.0.0 + */ +import * as Cause from "effect/Cause" +import * as Effect from "effect/Effect" +import * as Exit from "effect/Exit" +import type * as Fiber from "effect/Fiber" +import type * as FiberId from "effect/FiberId" +import * as FiberRef from "effect/FiberRef" +import * as FiberRefs from "effect/FiberRefs" +import { dual } from "effect/Function" +import * as HashSet from "effect/HashSet" +import * as Logger from "effect/Logger" + +/** + * @category model + * @since 1.0.0 + */ +export interface Teardown { + (exit: Exit.Exit, onExit: (code: number) => void): void +} + +/** + * @category teardown + * @since 1.0.0 + */ +export const defaultTeardown: Teardown = ( + exit: Exit.Exit, + onExit: (code: number) => void +) => { + onExit(Exit.isFailure(exit) && !Cause.isInterruptedOnly(exit.cause) ? 1 : 0) +} + +/** + * @category model + * @since 1.0.0 + */ +export interface RunMain { + /** + * Helps you run a main effect with built-in error handling, logging, and signal management. + * + * **Details** + * + * This function launches an Effect as the main entry point, setting exit codes + * based on success or failure, handling interrupts (e.g., Ctrl+C), and optionally + * logging errors. By default, it logs errors and uses a "pretty" format, but both + * behaviors can be turned off. You can also provide custom teardown logic to + * finalize resources or produce different exit codes. + * + * **Options** + * + * An optional object that can include: + * - `disableErrorReporting`: Turn off automatic error logging. + * - `disablePrettyLogger`: Avoid adding the pretty logger. + * - `teardown`: Provide custom finalization logic. + * + * **When to Use** + * + * Use this function to run an Effect as your application’s main program, especially + * when you need structured error handling, log management, interrupt support, + * or advanced teardown capabilities. + */ + ( + options?: { + readonly disableErrorReporting?: boolean | undefined + readonly disablePrettyLogger?: boolean | undefined + readonly teardown?: Teardown | undefined + } + ): (effect: Effect.Effect) => void + /** + * Helps you run a main effect with built-in error handling, logging, and signal management. + * + * **Details** + * + * This function launches an Effect as the main entry point, setting exit codes + * based on success or failure, handling interrupts (e.g., Ctrl+C), and optionally + * logging errors. By default, it logs errors and uses a "pretty" format, but both + * behaviors can be turned off. You can also provide custom teardown logic to + * finalize resources or produce different exit codes. + * + * **Options** + * + * An optional object that can include: + * - `disableErrorReporting`: Turn off automatic error logging. + * - `disablePrettyLogger`: Avoid adding the pretty logger. + * - `teardown`: Provide custom finalization logic. + * + * **When to Use** + * + * Use this function to run an Effect as your application’s main program, especially + * when you need structured error handling, log management, interrupt support, + * or advanced teardown capabilities. + */ + ( + effect: Effect.Effect, + options?: { + readonly disableErrorReporting?: boolean | undefined + readonly disablePrettyLogger?: boolean | undefined + readonly teardown?: Teardown | undefined + } + ): void +} + +const addPrettyLogger = (refs: FiberRefs.FiberRefs, fiberId: FiberId.Runtime) => { + const loggers = FiberRefs.getOrDefault(refs, FiberRef.currentLoggers) + if (!HashSet.has(loggers, Logger.defaultLogger)) { + return refs + } + return FiberRefs.updateAs(refs, { + fiberId, + fiberRef: FiberRef.currentLoggers, + value: loggers.pipe( + HashSet.remove(Logger.defaultLogger), + HashSet.add(Logger.prettyLoggerDefault) + ) + }) +} + +/** + * @category constructors + * @since 1.0.0 + */ +export const makeRunMain = ( + f: ( + options: { + readonly fiber: Fiber.RuntimeFiber + readonly teardown: Teardown + } + ) => void +): RunMain => + dual((args) => Effect.isEffect(args[0]), (effect: Effect.Effect, options?: { + readonly disableErrorReporting?: boolean | undefined + readonly disablePrettyLogger?: boolean | undefined + readonly teardown?: Teardown | undefined + }) => { + const fiber = options?.disableErrorReporting === true + ? Effect.runFork(effect, { + updateRefs: options?.disablePrettyLogger === true ? undefined : addPrettyLogger + }) + : Effect.runFork( + Effect.tapErrorCause(effect, (cause) => { + if (Cause.isInterruptedOnly(cause)) { + return Effect.void + } + return Effect.logError(cause) + }), + { + updateRefs: options?.disablePrettyLogger === true ? undefined : addPrettyLogger + } + ) + const teardown = options?.teardown ?? defaultTeardown + return f({ fiber, teardown }) + }) diff --git a/repos/effect/packages/platform/src/Socket.ts b/repos/effect/packages/platform/src/Socket.ts new file mode 100644 index 0000000..bc2192f --- /dev/null +++ b/repos/effect/packages/platform/src/Socket.ts @@ -0,0 +1,682 @@ +/** + * @since 1.0.0 + */ +import * as Channel from "effect/Channel" +import type * as Chunk from "effect/Chunk" +import * as Context from "effect/Context" +import * as Deferred from "effect/Deferred" +import type { DurationInput } from "effect/Duration" +import * as Effect from "effect/Effect" +import * as ExecutionStrategy from "effect/ExecutionStrategy" +import * as Exit from "effect/Exit" +import * as FiberRef from "effect/FiberRef" +import * as FiberSet from "effect/FiberSet" +import { dual } from "effect/Function" +import { globalValue } from "effect/GlobalValue" +import * as Layer from "effect/Layer" +import * as Mailbox from "effect/Mailbox" +import * as Predicate from "effect/Predicate" +import * as Scope from "effect/Scope" +import type * as AsyncProducer from "effect/SingleProducerAsyncInput" +import { TypeIdError } from "./Error.js" + +/** + * @since 1.0.0 + * @category type ids + */ +export const TypeId: unique symbol = Symbol.for("@effect/platform/Socket") + +/** + * @since 1.0.0 + * @category type ids + */ +export type TypeId = typeof TypeId + +/** + * @since 1.0.0 + * @category guards + */ +export const isSocket = (u: unknown): u is Socket => Predicate.hasProperty(u, TypeId) + +/** + * @since 1.0.0 + * @category tags + */ +export const Socket: Context.Tag = Context.GenericTag( + "@effect/platform/Socket" +) + +/** + * @since 1.0.0 + * @category models + */ +export interface Socket { + readonly [TypeId]: TypeId + readonly run: <_, E = never, R = never>( + handler: (_: Uint8Array) => Effect.Effect<_, E, R> | void, + options?: { + readonly onOpen?: Effect.Effect | undefined + } + ) => Effect.Effect + readonly runRaw: <_, E = never, R = never>( + handler: (_: string | Uint8Array) => Effect.Effect<_, E, R> | void, + options?: { + readonly onOpen?: Effect.Effect | undefined + } + ) => Effect.Effect + readonly writer: Effect.Effect< + (chunk: Uint8Array | string | CloseEvent) => Effect.Effect, + never, + Scope.Scope + > +} + +/** + * @since 1.0.0 + * @category type ids + */ +export const CloseEventTypeId: unique symbol = Symbol.for("@effect/platform/Socket/CloseEvent") + +/** + * @since 1.0.0 + * @category type ids + */ +export type CloseEventTypeId = typeof CloseEventTypeId + +/** + * @since 1.0.0 + * @category models + */ +export class CloseEvent { + /** + * @since 1.0.0 + */ + readonly [CloseEventTypeId]: CloseEventTypeId + constructor(readonly code = 1000, readonly reason?: string) { + this[CloseEventTypeId] = CloseEventTypeId + } + /** + * @since 1.0.0 + */ + toString() { + return this.reason ? `${this.code}: ${this.reason}` : `${this.code}` + } +} + +/** + * @since 1.0.0 + * @category refinements + */ +export const isCloseEvent = (u: unknown): u is CloseEvent => Predicate.hasProperty(u, CloseEventTypeId) + +/** + * @since 1.0.0 + * @category type ids + */ +export const SocketErrorTypeId: unique symbol = Symbol.for("@effect/platform/Socket/SocketError") + +/** + * @since 1.0.0 + * @category type ids + */ +export type SocketErrorTypeId = typeof SocketErrorTypeId + +/** + * @since 1.0.0 + * @category refinements + */ +export const isSocketError = (u: unknown): u is SocketError => Predicate.hasProperty(u, SocketErrorTypeId) + +/** + * @since 1.0.0 + * @category errors + */ +export type SocketError = SocketGenericError | SocketCloseError + +/** + * @since 1.0.0 + * @category errors + */ +export class SocketGenericError extends TypeIdError(SocketErrorTypeId, "SocketError")<{ + readonly reason: "Write" | "Read" | "Open" | "OpenTimeout" + readonly cause: unknown +}> { + get message() { + return `An error occurred during ${this.reason}` + } +} + +/** + * @since 1.0.0 + * @category errors + */ +export class SocketCloseError extends TypeIdError(SocketErrorTypeId, "SocketError")<{ + readonly reason: "Close" + readonly code: number + readonly closeReason?: string | undefined +}> { + /** + * @since 1.0.0 + */ + static is(u: unknown): u is SocketCloseError { + return isSocketError(u) && u.reason === "Close" + } + + /** + * @since 1.0.0 + */ + static isClean(isClean: (code: number) => boolean) { + return function(u: unknown): u is SocketCloseError { + return SocketCloseError.is(u) && isClean(u.code) + } + } + + get message() { + if (this.closeReason) { + return `${this.reason}: ${this.code}: ${this.closeReason}` + } + return `${this.reason}: ${this.code}` + } +} + +/** + * @since 1.0.0 + * @category combinators + */ +export const toChannelMap = ( + self: Socket, + f: (data: Uint8Array | string) => A +): Channel.Channel< + Chunk.Chunk, + Chunk.Chunk, + SocketError | IE, + IE, + void, + unknown +> => + Effect.gen(function*() { + const scope = yield* Effect.scope + const mailbox = yield* Mailbox.make() + const writeScope = yield* Scope.fork(scope, ExecutionStrategy.sequential) + const write = yield* Scope.extend(self.writer, writeScope) + function* emit(chunk: Chunk.Chunk) { + for (const data of chunk) { + yield* write(data) + } + } + const input: AsyncProducer.AsyncInputProducer, unknown> = { + awaitRead: () => Effect.void, + emit(chunk) { + return Effect.catchAllCause( + Effect.gen(() => emit(chunk)), + (cause) => mailbox.failCause(cause) + ) + }, + error(error) { + return Effect.zipRight( + Scope.close(writeScope, Exit.void), + mailbox.failCause(error) + ) + }, + done() { + return Scope.close(writeScope, Exit.void) + } + } + + yield* self.runRaw((data) => { + mailbox.unsafeOffer(f(data)) + }).pipe( + Mailbox.into(mailbox), + Effect.forkIn(scope), + Effect.interruptible + ) + + return Channel.embedInput(Mailbox.toChannel(mailbox), input) + }).pipe(Channel.unwrapScoped) + +/** + * @since 1.0.0 + * @category combinators + */ +export const toChannel = ( + self: Socket +): Channel.Channel< + Chunk.Chunk, + Chunk.Chunk, + SocketError | IE, + IE, + void, + unknown +> => { + const encoder = new TextEncoder() + return toChannelMap(self, (data) => typeof data === "string" ? encoder.encode(data) : data) +} + +/** + * @since 1.0.0 + * @category combinators + */ +export const toChannelString: { + (encoding?: string | undefined): (self: Socket) => Channel.Channel< + Chunk.Chunk, + Chunk.Chunk, + SocketError | IE, + IE, + void, + unknown + > + ( + self: Socket, + encoding?: string | undefined + ): Channel.Channel< + Chunk.Chunk, + Chunk.Chunk, + SocketError | IE, + IE, + void, + unknown + > +} = dual((args) => isSocket(args[0]), ( + self: Socket, + encoding?: string | undefined +): Channel.Channel< + Chunk.Chunk, + Chunk.Chunk, + SocketError | IE, + IE, + void, + unknown +> => { + const decoder = new TextDecoder(encoding) + return toChannelMap(self, (data) => typeof data === "string" ? data : decoder.decode(data)) +}) + +/** + * @since 1.0.0 + * @category combinators + */ +export const toChannelWith = () => +( + self: Socket +): Channel.Channel< + Chunk.Chunk, + Chunk.Chunk, + SocketError | IE, + IE, + void, + unknown +> => toChannel(self) + +/** + * @since 1.0.0 + * @category constructors + */ +export const makeChannel = (): Channel.Channel< + Chunk.Chunk, + Chunk.Chunk, + SocketError | IE, + IE, + void, + unknown, + Socket +> => Channel.unwrap(Effect.map(Socket, toChannelWith())) + +/** + * @since 1.0.0 + */ +export const defaultCloseCodeIsError = (code: number) => code !== 1000 && code !== 1006 + +/** + * @since 1.0.0 + * @category tags + */ +export interface WebSocket { + readonly _: unique symbol +} + +/** + * @since 1.0.0 + * @category tags + */ +export const WebSocket: Context.Tag = Context.GenericTag( + "@effect/platform/Socket/WebSocket" +) + +/** + * @since 1.0.0 + * @category tags + */ +export interface WebSocketConstructor { + readonly _: unique symbol +} + +/** + * @since 1.0.0 + * @category tags + */ +export const WebSocketConstructor: Context.Tag< + WebSocketConstructor, + (url: string, protocols?: string | Array | undefined) => globalThis.WebSocket +> = Context + .GenericTag("@effect/platform/Socket/WebSocketConstructor") + +/** + * @since 1.0.0 + * @category layers + */ +export const layerWebSocketConstructorGlobal: Layer.Layer = Layer.succeed( + WebSocketConstructor, + (url, protocols) => new globalThis.WebSocket(url, protocols) +) + +/** + * @since 1.0.0 + * @category constructors + */ +export const makeWebSocket = (url: string | Effect.Effect, options?: { + readonly closeCodeIsError?: ((code: number) => boolean) | undefined + readonly openTimeout?: DurationInput | undefined + readonly protocols?: string | Array | undefined +}): Effect.Effect => + fromWebSocket( + Effect.acquireRelease( + (typeof url === "string" ? Effect.succeed(url) : url).pipe( + Effect.flatMap((url) => Effect.map(WebSocketConstructor, (f) => f(url, options?.protocols))) + ), + (ws) => Effect.sync(() => ws.close(1000)) + ), + options + ) + +/** + * @since 1.0.0 + * @category constructors + */ +export const fromWebSocket = ( + acquire: Effect.Effect, + options?: { + readonly closeCodeIsError?: (code: number) => boolean + readonly openTimeout?: DurationInput + } +): Effect.Effect> => + Effect.withFiberRuntime((fiber) => { + let currentWS: globalThis.WebSocket | undefined + const latch = Effect.unsafeMakeLatch(false) + const acquireContext = fiber.currentContext as Context.Context + const closeCodeIsError = options?.closeCodeIsError ?? defaultCloseCodeIsError + + const runRaw = <_, E, R>(handler: (_: string | Uint8Array) => Effect.Effect<_, E, R> | void, opts?: { + readonly onOpen?: Effect.Effect | undefined + }) => + Effect.scopedWith(Effect.fnUntraced(function*(scope) { + const fiberSet = yield* FiberSet.make().pipe( + Scope.extend(scope) + ) + const ws = yield* Scope.extend(acquire, scope) + const run = yield* Effect.provideService(FiberSet.runtime(fiberSet)(), WebSocket, ws) + let open = false + + function onMessage(event: MessageEvent) { + if (event.data instanceof Blob) { + return Effect.promise(() => event.data.arrayBuffer() as Promise).pipe( + Effect.andThen((buffer) => handler(new Uint8Array(buffer))), + run + ) + } + const result = handler(event.data) + if (Effect.isEffect(result)) { + run(result) + } + } + function onError(cause: Event) { + ws.removeEventListener("message", onMessage) + ws.removeEventListener("close", onClose) + Deferred.unsafeDone( + fiberSet.deferred, + Effect.fail(new SocketGenericError({ reason: open ? "Read" : "Open", cause })) + ) + } + function onClose(event: globalThis.CloseEvent) { + ws.removeEventListener("message", onMessage) + ws.removeEventListener("error", onError) + Deferred.unsafeDone( + fiberSet.deferred, + Effect.fail( + new SocketCloseError({ + reason: "Close", + code: event.code, + closeReason: event.reason + }) + ) + ) + } + + ws.addEventListener("close", onClose, { once: true }) + ws.addEventListener("error", onError, { once: true }) + ws.addEventListener("message", onMessage) + + if (ws.readyState !== 1) { + const openDeferred = Deferred.unsafeMake(fiber.id()) + ws.addEventListener("open", () => { + open = true + Deferred.unsafeDone(openDeferred, Effect.void) + }, { once: true }) + yield* Deferred.await(openDeferred).pipe( + Effect.timeoutFail({ + duration: options?.openTimeout ?? 10000, + onTimeout: () => new SocketGenericError({ reason: "OpenTimeout", cause: "timeout waiting for \"open\"" }) + }), + Effect.raceFirst(FiberSet.join(fiberSet)) + ) + } + open = true + currentWS = ws + yield* latch.open + if (opts?.onOpen) yield* opts.onOpen + return yield* FiberSet.join(fiberSet).pipe( + Effect.catchIf( + SocketCloseError.isClean((_) => !closeCodeIsError(_)), + (_) => Effect.void + ) + ) + })).pipe( + Effect.mapInputContext((input: Context.Context) => Context.merge(acquireContext, input)), + Effect.ensuring(Effect.sync(() => { + latch.unsafeClose() + currentWS = undefined + })), + Effect.interruptible + ) + + const encoder = new TextEncoder() + const run = <_, E, R>(handler: (_: Uint8Array) => Effect.Effect<_, E, R> | void, opts?: { + readonly onOpen?: Effect.Effect | undefined + }) => + runRaw((data) => + typeof data === "string" + ? handler(encoder.encode(data)) + : data instanceof Uint8Array + ? handler(data) + : handler(new Uint8Array(data)), opts) + + const write = (chunk: Uint8Array | string | CloseEvent) => + latch.whenOpen(Effect.sync(() => { + const ws = currentWS! + if (isCloseEvent(chunk)) { + ws.close(chunk.code, chunk.reason) + } else { + ws.send(chunk) + } + })) + const writer = Effect.succeed(write) + + return Effect.succeed(Socket.of({ + [TypeId]: TypeId, + run, + runRaw, + writer + })) + }) + +/** + * @since 1.0.0 + * @category constructors + */ +export const makeWebSocketChannel = ( + url: string, + options?: { + readonly closeCodeIsError?: (code: number) => boolean + } +): Channel.Channel< + Chunk.Chunk, + Chunk.Chunk, + SocketError | IE, + IE, + void, + unknown, + WebSocketConstructor +> => + Channel.unwrapScoped( + Effect.map(makeWebSocket(url, options), toChannelWith()) + ) + +/** + * @since 1.0.0 + * @category layers + */ +export const layerWebSocket = (url: string, options?: { + readonly closeCodeIsError?: (code: number) => boolean +}): Layer.Layer => + Layer.effect( + Socket, + makeWebSocket(url, options) + ) + +/** + * @since 1.0.0 + * @category fiber refs + */ +export const currentSendQueueCapacity: FiberRef.FiberRef = globalValue( + "@effect/platform/Socket/currentSendQueueCapacity", + () => FiberRef.unsafeMake(16) +) + +/** + * @since 1.0.0 + * @category models + */ +export interface InputTransformStream { + readonly readable: ReadableStream | ReadableStream | ReadableStream + readonly writable: WritableStream +} + +/** + * @since 1.0.0 + * @category constructors + */ +export const fromTransformStream = (acquire: Effect.Effect, options?: { + readonly closeCodeIsError?: (code: number) => boolean +}): Effect.Effect> => + Effect.withFiberRuntime((fiber) => { + const latch = Effect.unsafeMakeLatch(false) + let currentStream: { + readonly stream: InputTransformStream + readonly fiberSet: FiberSet.FiberSet + } | undefined + const acquireContext = fiber.currentContext as Context.Context + const closeCodeIsError = options?.closeCodeIsError ?? defaultCloseCodeIsError + const runRaw = <_, E, R>(handler: (_: string | Uint8Array) => Effect.Effect<_, E, R> | void, opts?: { + readonly onOpen?: Effect.Effect | undefined + }) => + Effect.scopedWith(Effect.fnUntraced(function*(scope) { + const stream = yield* Scope.extend(acquire, scope) + const reader = stream.readable.getReader() + yield* Scope.addFinalizer(scope, Effect.promise(() => reader.cancel())) + const fiberSet = yield* FiberSet.make().pipe( + Scope.extend(scope) + ) + const runFork = yield* FiberSet.runtime(fiberSet)() + + yield* Effect.tryPromise({ + try: async () => { + while (true) { + const { done, value } = await reader.read() + if (done) { + throw new SocketCloseError({ reason: "Close", code: 1000 }) + } + const result = handler(value) + if (Effect.isEffect(result)) { + runFork(result) + } + } + }, + catch: (cause) => isSocketError(cause) ? cause : new SocketGenericError({ reason: "Read", cause }) + }).pipe( + FiberSet.run(fiberSet) + ) + + currentStream = { stream, fiberSet } + yield* latch.open + if (opts?.onOpen) yield* opts.onOpen + + return yield* FiberSet.join(fiberSet).pipe( + Effect.catchIf( + SocketCloseError.isClean((_) => !closeCodeIsError(_)), + (_) => Effect.void + ) + ) + })).pipe( + (_) => _, + Effect.mapInputContext((input: Context.Context) => Context.merge(acquireContext, input)), + Effect.ensuring(Effect.sync(() => { + latch.unsafeClose() + currentStream = undefined + })), + Effect.interruptible + ) + + const encoder = new TextEncoder() + const run = <_, E, R>(handler: (_: Uint8Array) => Effect.Effect<_, E, R> | void, opts?: { + readonly onOpen?: Effect.Effect | undefined + }) => + runRaw((data) => + typeof data === "string" + ? handler(encoder.encode(data)) + : handler(data), opts) + + const writers = new WeakMap>() + const getWriter = (stream: InputTransformStream) => { + let writer = writers.get(stream) + if (!writer) { + writer = stream.writable.getWriter() + writers.set(stream, writer) + } + return writer + } + const write = (chunk: Uint8Array | string | CloseEvent) => + latch.whenOpen(Effect.suspend(() => { + const { fiberSet, stream } = currentStream! + if (isCloseEvent(chunk)) { + return Deferred.fail( + fiberSet.deferred, + new SocketCloseError({ reason: "Close", code: chunk.code, closeReason: chunk.reason }) + ) + } + return Effect.promise(() => getWriter(stream).write(typeof chunk === "string" ? encoder.encode(chunk) : chunk)) + })) + const writer = Effect.acquireRelease( + Effect.succeed(write), + () => + Effect.promise(async () => { + if (!currentStream) return + await getWriter(currentStream.stream).close() + }) + ) + + return Effect.succeed(Socket.of({ + [TypeId]: TypeId, + run, + runRaw, + writer + })) + }) diff --git a/repos/effect/packages/platform/src/SocketServer.ts b/repos/effect/packages/platform/src/SocketServer.ts new file mode 100644 index 0000000..0a28e0c --- /dev/null +++ b/repos/effect/packages/platform/src/SocketServer.ts @@ -0,0 +1,79 @@ +/** + * @since 1.0.0 + */ +import * as Context from "effect/Context" +import * as Data from "effect/Data" +import type * as Effect from "effect/Effect" +import type * as Socket from "./Socket.js" + +/** + * @since 1.0.0 + * @category tags + */ +export class SocketServer extends Context.Tag("@effect/platform/SocketServer")< + SocketServer, + { + readonly address: Address + readonly run: ( + handler: (socket: Socket.Socket) => Effect.Effect<_, E, R> + ) => Effect.Effect + } +>() {} + +/** + * @since 1.0.0 + * @category errors + */ +export const ErrorTypeId: unique symbol = Symbol.for("@effect/platform/SocketServer/SocketServerError") + +/** + * @since 1.0.0 + * @category errors + */ +export type ErrorTypeId = typeof ErrorTypeId + +/** + * @since 1.0.0 + * @category errors + */ +export class SocketServerError extends Data.TaggedError("SocketServerError")<{ + readonly reason: "Open" | "Unknown" + readonly cause: unknown +}> { + /** + * @since 1.0.0 + */ + readonly [ErrorTypeId]: ErrorTypeId = ErrorTypeId + + /** + * @since 1.0.0 + */ + get message(): string { + return this.reason + } +} + +/** + * @since 1.0.0 + * @category models + */ +export type Address = UnixAddress | TcpAddress + +/** + * @since 1.0.0 + * @category models + */ +export interface TcpAddress { + readonly _tag: "TcpAddress" + readonly hostname: string + readonly port: number +} + +/** + * @since 1.0.0 + * @category models + */ +export interface UnixAddress { + readonly _tag: "UnixAddress" + readonly path: string +} diff --git a/repos/effect/packages/platform/src/Template.ts b/repos/effect/packages/platform/src/Template.ts new file mode 100644 index 0000000..f54cba1 --- /dev/null +++ b/repos/effect/packages/platform/src/Template.ts @@ -0,0 +1,198 @@ +/** + * @since 1.0.0 + */ +import * as Effect from "effect/Effect" +import * as Option from "effect/Option" +import * as Predicate from "effect/Predicate" +import * as Stream from "effect/Stream" + +/** + * @category models + * @since 1.0.0 + */ +export type PrimitiveValue = string | number | bigint | boolean | null | undefined + +/** + * @category models + * @since 1.0.0 + */ +export type Primitive = PrimitiveValue | ReadonlyArray + +/** + * @category models + * @since 1.0.0 + */ +export type Interpolated = + | Primitive + | Option.Option + | Effect.Effect + +/** + * @category models + * @since 1.0.0 + */ +export type InterpolatedWithStream = Interpolated | Stream.Stream + +/** + * @category models + * @since 1.0.0 + */ +export declare namespace Interpolated { + /** + * @category models + * @since 1.0.0 + */ + export type Context = A extends infer T ? T extends Option.Option ? never + : T extends Stream.Stream ? R + : never + : never + + /** + * @category models + * @since 1.0.0 + */ + export type Error = A extends infer T ? T extends Option.Option ? never + : T extends Stream.Stream ? E + : never + : never +} + +/** + * @category constructors + * @since 1.0.0 + */ +export function make>( + strings: TemplateStringsArray, + ...args: A +): Effect.Effect< + string, + Interpolated.Error, + Interpolated.Context +> { + const argsLength = args.length + const values = new Array(argsLength) + const effects: Array< + [index: number, effect: Effect.Effect] + > = [] + + for (let i = 0; i < argsLength; i++) { + const arg = args[i] + + if (Option.isOption(arg)) { + values[i] = arg._tag === "Some" ? primitiveToString(arg.value) : "" + } else if (isSuccess(arg)) { + values[i] = primitiveToString((arg as any).effect_instruction_i0) + } else if (Effect.isEffect(arg)) { + effects.push([i, arg]) + } else { + values[i] = primitiveToString(arg) + } + } + + if (effects.length === 0) { + return Effect.succeed(consolidate(strings, values)) + } + + return Effect.map( + Effect.forEach( + effects, + ([index, effect]) => + Effect.tap(effect, (value) => { + values[index] = primitiveToString(value) + }), + { + concurrency: "inherit", + discard: true + } + ), + (_) => consolidate(strings, values) + ) +} + +/** + * @category constructors + * @since 1.0.0 + */ +export function stream>( + strings: TemplateStringsArray, + ...args: A +): Stream.Stream< + string, + Interpolated.Error, + Interpolated.Context +> { + const chunks: Array> = [] + let buffer = "" + + for (let i = 0, len = args.length; i < len; i++) { + buffer += strings[i] + const arg = args[i] + if (Option.isOption(arg)) { + buffer += arg._tag === "Some" ? primitiveToString(arg.value) : "" + } else if (isSuccess(arg)) { + buffer += primitiveToString((arg as any).effect_instruction_i0) + } else if (Predicate.hasProperty(arg, Stream.StreamTypeId)) { + if (buffer.length > 0) { + chunks.push(buffer) + buffer = "" + } + if (Effect.isEffect(arg)) { + chunks.push(Effect.map(arg, primitiveToString)) + } else { + chunks.push(Stream.map(arg, primitiveToString)) + } + } else { + buffer += primitiveToString(arg) + } + } + + buffer += strings[strings.length - 1] + if (buffer.length > 0) { + chunks.push(buffer) + buffer = "" + } + + return Stream.flatMap( + Stream.fromIterable(chunks), + (chunk) => typeof chunk === "string" ? Stream.succeed(chunk) : chunk, + { concurrency: "unbounded" } + ) +} + +function primitiveToString(value: Primitive): string { + if (Array.isArray(value)) { + return value.map(primitiveToString).join("") + } + + switch (typeof value) { + case "string": { + return value + } + case "number": + case "bigint": { + return value.toString() + } + case "boolean": { + return value ? "true" : "false" + } + default: { + return "" + } + } +} + +function consolidate( + strings: ReadonlyArray, + values: ReadonlyArray +): string { + let out = "" + for (let i = 0, len = values.length; i < len; i++) { + out += strings[i] + out += values[i] + } + return out + strings[strings.length - 1] +} + +function isSuccess(u: unknown) { + return Effect.isEffect(u) && (u as any)._op === "Success" +} diff --git a/repos/effect/packages/platform/src/Terminal.ts b/repos/effect/packages/platform/src/Terminal.ts new file mode 100644 index 0000000..8897720 --- /dev/null +++ b/repos/effect/packages/platform/src/Terminal.ts @@ -0,0 +1,105 @@ +/** + * @since 1.0.0 + */ +import type { Tag } from "effect/Context" +import { TaggedError } from "effect/Data" +import type { Effect } from "effect/Effect" +import type { ReadonlyMailbox } from "effect/Mailbox" +import type { Option } from "effect/Option" +import type * as Scope from "effect/Scope" +import type { PlatformError } from "./Error.js" +import * as InternalTerminal from "./internal/terminal.js" + +/** + * A `Terminal` represents a command-line interface which can read input from a + * user and display messages to a user. + * + * @since 1.0.0 + * @category models + */ +export interface Terminal { + /** + * The number of columns available on the platform's terminal interface. + */ + readonly columns: Effect + /** + * The number of rows available on the platform's terminal interface. + */ + readonly rows: Effect + /** + * Determines if the current terminal interface is interactive. + */ + readonly isTTY: Effect + /** + * Reads input events from the default standard input. + */ + readonly readInput: Effect, never, Scope.Scope> + /** + * Reads a single line from the default standard input. + */ + readonly readLine: Effect + /** + * Displays text to the the default standard output. + */ + readonly display: (text: string) => Effect +} + +/** + * @since 1.0.0 + * @category model + */ +export interface Key { + /** + * The name of the key being pressed. + */ + readonly name: string + /** + * If set to `true`, then the user is also holding down the `Ctrl` key. + */ + readonly ctrl: boolean + /** + * If set to `true`, then the user is also holding down the `Meta` key. + */ + readonly meta: boolean + /** + * If set to `true`, then the user is also holding down the `Shift` key. + */ + readonly shift: boolean +} + +/** + * @since 1.0.0 + * @category model + */ +export interface UserInput { + /** + * The character read from the user (if any). + */ + readonly input: Option + /** + * The key that the user pressed. + */ + readonly key: Key +} + +/** + * A `QuitException` represents an exception that occurs when a user attempts to + * quit out of a `Terminal` prompt for input (usually by entering `ctrl`+`c`). + * + * @since 1.0.0 + * @category model + */ +export class QuitException extends TaggedError("QuitException")<{}> {} + +/** + * @since 1.0.0 + * @category refinements + */ +export const isQuitException = (u: unknown): u is QuitException => + typeof u === "object" && u != null && "_tag" in u && u._tag === "QuitException" + +/** + * @since 1.0.0 + * @category tag + */ +export const Terminal: Tag = InternalTerminal.tag diff --git a/repos/effect/packages/platform/src/Transferable.ts b/repos/effect/packages/platform/src/Transferable.ts new file mode 100644 index 0000000..4c78b26 --- /dev/null +++ b/repos/effect/packages/platform/src/Transferable.ts @@ -0,0 +1,125 @@ +/** + * @since 1.0.0 + */ +import * as Context from "effect/Context" +import * as Effect from "effect/Effect" +import { dual } from "effect/Function" +import * as Option from "effect/Option" +import * as ParseResult from "effect/ParseResult" +import * as Schema from "effect/Schema" + +/** + * @since 1.0.0 + * @category models + */ +export interface CollectorService { + readonly addAll: (_: Iterable) => Effect.Effect + readonly unsafeAddAll: (_: Iterable) => void + readonly read: Effect.Effect> + readonly unsafeRead: () => Array + readonly unsafeClear: () => Array + readonly clear: Effect.Effect> +} + +/** + * @since 1.0.0 + * @category tags + */ +export class Collector extends Context.Tag("@effect/platform/Transferable/Collector")< + Collector, + CollectorService +>() {} + +/** + * @since 1.0.0 + * @category constructors + */ +export const unsafeMakeCollector = (): CollectorService => { + let tranferables: Array = [] + const unsafeAddAll = (transfers: Iterable): void => { + // eslint-disable-next-line no-restricted-syntax + tranferables.push(...transfers) + } + const unsafeRead = (): Array => tranferables + const unsafeClear = (): Array => { + const prev = tranferables + tranferables = [] + return prev + } + return Collector.of({ + unsafeAddAll, + addAll: (transferables) => Effect.sync(() => unsafeAddAll(transferables)), + unsafeRead, + read: Effect.sync(unsafeRead), + unsafeClear, + clear: Effect.sync(unsafeClear) + }) +} + +/** + * @since 1.0.0 + * @category constructors + */ +export const makeCollector: Effect.Effect = Effect.sync(unsafeMakeCollector) + +/** + * @since 1.0.0 + * @category accessors + */ +export const addAll = (tranferables: Iterable): Effect.Effect => + Effect.flatMap( + Effect.serviceOption(Collector), + Option.match({ + onNone: () => Effect.void, + onSome: (_) => _.addAll(tranferables) + }) + ) + +/** + * @since 1.0.0 + * @category schema + */ +export const schema: { + ( + f: (_: I) => Iterable + ): (self: Schema.Schema) => Schema.Schema + ( + self: Schema.Schema, + f: (_: I) => Iterable + ): Schema.Schema +} = dual(2, ( + self: Schema.Schema, + f: (_: I) => Iterable +) => + Schema.transformOrFail( + Schema.encodedSchema(self), + self, + { strict: true, decode: ParseResult.succeed, encode: (i) => Effect.as(addAll(f(i)), i) } + )) + +/** + * @since 1.0.0 + * @category schema + */ +export const ImageData: Schema.Schema = schema( + Schema.Any, + (_) => [(_ as ImageData).data.buffer] +) + +/** + * @since 1.0.0 + * @category schema + */ +export const MessagePort: Schema.Schema = schema( + Schema.Any, + (_) => [_ as MessagePort] +) + +/** + * @since 1.0.0 + * @category schema + */ +export const Uint8Array: Schema.Schema = schema( + Schema.Uint8ArrayFromSelf, + (_) => [_.buffer] +) diff --git a/repos/effect/packages/platform/src/Url.ts b/repos/effect/packages/platform/src/Url.ts new file mode 100644 index 0000000..bb93045 --- /dev/null +++ b/repos/effect/packages/platform/src/Url.ts @@ -0,0 +1,324 @@ +/** + * @since 1.0.0 + */ +import * as Cause from "effect/Cause" +import * as Either from "effect/Either" +import { dual } from "effect/Function" +import * as Redacted from "effect/Redacted" +import * as UrlParams from "./UrlParams.js" + +/** + * Parses a URL string into a `URL` object, returning an `Either` type for safe + * error handling. + * + * **Details** + * + * This function converts a string into a `URL` object, enabling safe URL + * parsing with built-in error handling. If the string is invalid or fails to + * parse, this function does not throw an error; instead, it wraps the error in + * a `IllegalArgumentException` and returns it as the `Left` value of an + * `Either`. The `Right` value contains the successfully parsed `URL`. + * + * An optional `base` parameter can be provided to resolve relative URLs. If + * specified, the function interprets the input `url` as relative to this + * `base`. This is especially useful when dealing with URLs that might not be + * fully qualified. + * + * **Example** + * + * ```ts + * import { Url } from "@effect/platform" + * import { Either } from "effect" + * + * // Parse an absolute URL + * // + * // ┌─── Either + * // ▼ + * const parsed = Url.fromString("https://example.com/path") + * + * if (Either.isRight(parsed)) { + * console.log("Parsed URL:", parsed.right.toString()) + * } else { + * console.log("Error:", parsed.left.message) + * } + * // Output: Parsed URL: https://example.com/path + * + * // Parse a relative URL with a base + * const relativeParsed = Url.fromString("/relative-path", "https://example.com") + * + * if (Either.isRight(relativeParsed)) { + * console.log("Parsed relative URL:", relativeParsed.right.toString()) + * } else { + * console.log("Error:", relativeParsed.left.message) + * } + * // Output: Parsed relative URL: https://example.com/relative-path + * ``` + * + * @since 1.0.0 + * @category Constructors + */ +export const fromString: { + (url: string, base?: string | URL | undefined): Either.Either +} = (url, base) => + Either.try({ + try: () => new URL(url, base), + catch: (cause) => + new Cause.IllegalArgumentException(cause instanceof globalThis.Error ? cause.message : "Invalid input") + }) + +/** + * This function clones the original `URL` object and applies a callback to the + * clone, allowing multiple updates at once. + * + * **Example** + * + * ```ts + * import { Url } from "@effect/platform" + * + * const myUrl = new URL("https://example.com") + * + * const mutatedUrl = Url.mutate(myUrl, (url) => { + * url.username = "user" + * url.password = "pass" + * }) + * + * console.log("Mutated:", mutatedUrl.toString()) + * // Output: Mutated: https://user:pass@example.com/ + * ``` + * + * @since 1.0.0 + * @category Modifiers + */ +export const mutate: { + (f: (url: URL) => void): (self: URL) => URL + (self: URL, f: (url: URL) => void): URL +} = dual(2, (self: URL, f: (url: URL) => void) => { + const copy = new URL(self) + f(copy) + return copy +}) + +/** @internal */ +const immutableURLSetter =

(property: P): { + (value: URL[P] | A): (url: URL) => URL + (url: URL, value: URL[P] | A): URL +} => + dual(2, (url: URL, value: URL[P]) => + mutate(url, (url) => { + url[property] = value + })) + +/** + * Updates the hash fragment of the URL. + * + * @since 1.0.0 + * @category Setters + */ +export const setHash: { + (hash: string): (url: URL) => URL + (url: URL, hash: string): URL +} = immutableURLSetter("hash") + +/** + * Updates the host (domain and port) of the URL. + * + * @since 1.0.0 + * @category Setters + */ +export const setHost: { + (host: string): (url: URL) => URL + (url: URL, host: string): URL +} = immutableURLSetter("host") + +/** + * Updates the domain of the URL without modifying the port. + * + * @since 1.0.0 + * @category Setters + */ +export const setHostname: { + (hostname: string): (url: URL) => URL + (url: URL, hostname: string): URL +} = immutableURLSetter("hostname") + +/** + * Replaces the entire URL string. + * + * @since 1.0.0 + * @category Setters + */ +export const setHref: { + (href: string): (url: URL) => URL + (url: URL, href: string): URL +} = immutableURLSetter("href") + +/** + * Updates the password used for authentication. + * + * @since 1.0.0 + * @category Setters + */ +export const setPassword: { + (password: string | Redacted.Redacted): (url: URL) => URL + (url: URL, password: string | Redacted.Redacted): URL +} = dual(2, (url: URL, password: string | Redacted.Redacted) => + mutate(url, (url) => { + url.password = typeof password === "string" + ? password : + Redacted.value(password) + })) + +/** + * Updates the path of the URL. + * + * @since 1.0.0 + * @category Setters + */ +export const setPathname: { + (pathname: string): (url: URL) => URL + (url: URL, pathname: string): URL +} = immutableURLSetter("pathname") + +/** + * Updates the port of the URL. + * + * @since 1.0.0 + * @category Setters + */ +export const setPort: { + (port: string | number): (url: URL) => URL + (url: URL, port: string | number): URL +} = immutableURLSetter("port") + +/** + * Updates the protocol (e.g., `http`, `https`). + * + * @since 1.0.0 + * @category Setters + */ +export const setProtocol: { + (protocol: string): (url: URL) => URL + (url: URL, protocol: string): URL +} = immutableURLSetter("protocol") + +/** + * Updates the query string of the URL. + * + * @since 1.0.0 + * @category Setters + */ +export const setSearch: { + (search: string): (url: URL) => URL + (url: URL, search: string): URL +} = immutableURLSetter("search") + +/** + * Updates the username used for authentication. + * + * @since 1.0.0 + * @category Setters + */ +export const setUsername: { + (username: string): (url: URL) => URL + (url: URL, username: string): URL +} = immutableURLSetter("username") + +/** + * Updates the query parameters of a URL. + * + * **Details** + * + * This function allows you to set or replace the query parameters of a `URL` + * object using the provided `UrlParams`. It creates a new `URL` object with the + * updated parameters, leaving the original object unchanged. + * + * **Example** + * + * ```ts + * import { Url, UrlParams } from "@effect/platform" + * + * const myUrl = new URL("https://example.com?foo=bar") + * + * // Write parameters + * const updatedUrl = Url.setUrlParams( + * myUrl, + * UrlParams.fromInput([["key", "value"]]) + * ) + * + * console.log(updatedUrl.toString()) + * // Output: https://example.com/?key=value + * ``` + * + * @since 1.0.0 + * @category Setters + */ +export const setUrlParams: { + (urlParams: UrlParams.UrlParams): (url: URL) => URL + (url: URL, urlParams: UrlParams.UrlParams): URL +} = dual(2, (url: URL, searchParams: UrlParams.UrlParams) => + mutate(url, (url) => { + url.search = UrlParams.toString(searchParams) + })) + +/** + * Retrieves the query parameters from a URL. + * + * **Details** + * + * This function extracts the query parameters from a `URL` object and returns + * them as `UrlParams`. The resulting structure can be easily manipulated or + * inspected. + * + * **Example** + * + * ```ts + * import { Url } from "@effect/platform" + * + * const myUrl = new URL("https://example.com?foo=bar") + * + * // Read parameters + * const params = Url.urlParams(myUrl) + * + * console.log(params) + * // Output: [ [ 'foo', 'bar' ] ] + * ``` + * + * @since 1.0.0 + * @category Getters + */ +export const urlParams = (url: URL): UrlParams.UrlParams => UrlParams.fromInput(url.searchParams) + +/** + * Reads, modifies, and updates the query parameters of a URL. + * + * **Details** + * + * This function provides a functional way to interact with query parameters by + * reading the current parameters, applying a transformation function, and then + * writing the updated parameters back to the URL. It returns a new `URL` object + * with the modified parameters, ensuring immutability. + * + * **Example** + * + * ```ts + * import { Url, UrlParams } from "@effect/platform" + * + * const myUrl = new URL("https://example.com?foo=bar") + * + * const changedUrl = Url.modifyUrlParams(myUrl, UrlParams.append("key", "value")) + * + * console.log(changedUrl.toString()) + * // Output: https://example.com/?foo=bar&key=value + * ``` + * + * @since 1.0.0 + * @category Modifiers + */ +export const modifyUrlParams: { + (f: (urlParams: UrlParams.UrlParams) => UrlParams.UrlParams): (url: URL) => URL + (url: URL, f: (urlParams: UrlParams.UrlParams) => UrlParams.UrlParams): URL +} = dual(2, (url: URL, f: (urlParams: UrlParams.UrlParams) => UrlParams.UrlParams) => + mutate(url, (url) => { + const params = f(UrlParams.fromInput(url.searchParams)) + url.search = UrlParams.toString(params) + })) diff --git a/repos/effect/packages/platform/src/UrlParams.ts b/repos/effect/packages/platform/src/UrlParams.ts new file mode 100644 index 0000000..0ca9081 --- /dev/null +++ b/repos/effect/packages/platform/src/UrlParams.ts @@ -0,0 +1,385 @@ +/** + * @since 1.0.0 + */ +import * as Arr from "effect/Array" +import type * as Effect from "effect/Effect" +import * as Either from "effect/Either" +import { dual } from "effect/Function" +import * as Option from "effect/Option" +import type * as ParseResult from "effect/ParseResult" +import * as Schema from "effect/Schema" +import type { ParseOptions } from "effect/SchemaAST" + +/** + * @since 1.0.0 + * @category models + */ +export interface UrlParams extends ReadonlyArray {} + +/** + * @since 1.0.0 + * @category models + */ +export type Input = + | CoercibleRecord + | Iterable + | URLSearchParams + +/** + * @since 1.0.0 + * @category models + */ +export type Coercible = string | number | bigint | boolean | null | undefined + +/** + * @since 1.0.0 + * @category models + */ +export interface CoercibleRecord { + readonly [key: string]: Coercible | ReadonlyArray | CoercibleRecord +} + +/** + * @since 1.0.0 + * @category constructors + */ +export const fromInput = (input: Input): UrlParams => { + const parsed = fromInputNested(input) + const out: Array<[string, string]> = [] + for (let i = 0; i < parsed.length; i++) { + if (Array.isArray(parsed[i][0])) { + const [keys, value] = parsed[i] as [Array, string] + out.push([`${keys[0]}[${keys.slice(1).join("][")}]`, value]) + } else { + out.push(parsed[i] as [string, string]) + } + } + return out +} + +const fromInputNested = (input: Input): Array<[string | Array, any]> => { + const entries = Symbol.iterator in input ? Arr.fromIterable(input) : Object.entries(input) + const out: Array<[string | Array, string]> = [] + for (const [key, value] of entries) { + if (Array.isArray(value)) { + for (let i = 0; i < value.length; i++) { + if (value[i] !== undefined) { + out.push([key, String(value[i])]) + } + } + } else if (typeof value === "object") { + const nested = fromInputNested(value as CoercibleRecord) + for (const [k, v] of nested) { + out.push([[key, ...(typeof k === "string" ? [k] : k)], v]) + } + } else if (value !== undefined) { + out.push([key, String(value)]) + } + } + return out +} + +/** + * @since 1.0.0 + * @category schemas + */ +export const schemaFromSelf: Schema.Schema = Schema.Array( + Schema.Tuple(Schema.String, Schema.String) +).annotations({ identifier: "UrlParams" }) + +/** + * @since 1.0.0 + * @category constructors + */ +export const empty: UrlParams = [] + +/** + * @since 1.0.0 + * @category combinators + */ +export const getAll: { + (key: string): (self: UrlParams) => ReadonlyArray + (self: UrlParams, key: string): ReadonlyArray +} = dual( + 2, + (self: UrlParams, key: string): ReadonlyArray => + Arr.reduce(self, [] as Array, (acc, [k, value]) => { + if (k === key) { + acc.push(value) + } + return acc + }) +) + +/** + * @since 1.0.0 + * @category combinators + */ +export const getFirst: { + (key: string): (self: UrlParams) => Option.Option + (self: UrlParams, key: string): Option.Option +} = dual(2, (self: UrlParams, key: string): Option.Option => + Option.map( + Arr.findFirst(self, ([k]) => k === key), + ([, value]) => value + )) + +/** + * @since 1.0.0 + * @category combinators + */ +export const getLast: { + (key: string): (self: UrlParams) => Option.Option + (self: UrlParams, key: string): Option.Option +} = dual(2, (self: UrlParams, key: string): Option.Option => + Option.map( + Arr.findLast(self, ([k]) => k === key), + ([, value]) => value + )) + +/** + * @since 1.0.0 + * @category combinators + */ +export const set: { + (key: string, value: Coercible): (self: UrlParams) => UrlParams + (self: UrlParams, key: string, value: Coercible): UrlParams +} = dual(3, (self: UrlParams, key: string, value: Coercible): UrlParams => + Arr.append( + Arr.filter(self, ([k]) => k !== key), + [key, String(value)] + )) + +/** + * @since 1.0.0 + * @category combinators + */ +export const setAll: { + (input: Input): (self: UrlParams) => UrlParams + (self: UrlParams, input: Input): UrlParams +} = dual(2, (self: UrlParams, input: Input): UrlParams => { + const out = fromInput(input) as Array + const keys = new Set() + for (let i = 0; i < out.length; i++) { + keys.add(out[i][0]) + } + for (let i = 0; i < self.length; i++) { + if (keys.has(self[i][0])) continue + out.push(self[i]) + } + return out +}) + +/** + * @since 1.0.0 + * @category combinators + */ +export const append: { + (key: string, value: Coercible): (self: UrlParams) => UrlParams + (self: UrlParams, key: string, value: Coercible): UrlParams +} = dual(3, (self: UrlParams, key: string, value: Coercible): UrlParams => + Arr.append( + self, + [key, String(value)] + )) + +/** + * @since 1.0.0 + * @category combinators + */ +export const appendAll: { + (input: Input): (self: UrlParams) => UrlParams + (self: UrlParams, input: Input): UrlParams +} = dual(2, (self: UrlParams, input: Input): UrlParams => Arr.appendAll(self, fromInput(input))) + +/** + * @since 1.0.0 + * @category combinators + */ +export const remove: { + (key: string): (self: UrlParams) => UrlParams + (self: UrlParams, key: string): UrlParams +} = dual(2, (self: UrlParams, key: string): UrlParams => Arr.filter(self, ([k]) => k !== key)) + +/** + * @since 1.0.0 + * @category conversions + */ +export const makeUrl = (url: string, params: UrlParams, hash: Option.Option): Either.Either => { + try { + const urlInstance = new URL(url, baseUrl()) + for (let i = 0; i < params.length; i++) { + const [key, value] = params[i] + if (value !== undefined) { + urlInstance.searchParams.append(key, value) + } + } + if (hash._tag === "Some") { + urlInstance.hash = hash.value + } + return Either.right(urlInstance) + } catch (e) { + return Either.left(e as Error) + } +} + +/** + * @since 1.0.0 + * @category conversions + */ +export const toString = (self: UrlParams): string => new URLSearchParams(self as any).toString() + +const baseUrl = (): string | undefined => { + if ( + "location" in globalThis && + globalThis.location !== undefined && + globalThis.location.origin !== undefined && + globalThis.location.pathname !== undefined + ) { + return location.origin + location.pathname + } + return undefined +} + +/** + * Builds a `Record` containing all the key-value pairs in the given `UrlParams` + * as `string` (if only one value for a key) or a `NonEmptyArray` + * (when more than one value for a key) + * + * **Example** + * + * ```ts + * import * as assert from "node:assert" + * import { UrlParams } from "@effect/platform" + * + * const urlParams = UrlParams.fromInput({ a: 1, b: true, c: "string", e: [1, 2, 3] }) + * const result = UrlParams.toRecord(urlParams) + * + * assert.deepStrictEqual( + * result, + * { "a": "1", "b": "true", "c": "string", "e": ["1", "2", "3"] } + * ) + * ``` + * + * @since 1.0.0 + * @category conversions + */ +export const toRecord = (self: UrlParams): Record> => { + const out: Record> = Object.create(null) + for (const [k, value] of self) { + const curr = out[k] + if (curr === undefined) { + out[k] = value + } else if (typeof curr === "string") { + out[k] = [curr, value] + } else { + curr.push(value) + } + } + return { ...out } +} + +/** + * @since 1.0.0 + * @category schema + */ +export const schemaJson = (schema: Schema.Schema, options?: ParseOptions | undefined): { + ( + field: string + ): (self: UrlParams) => Effect.Effect + ( + self: UrlParams, + field: string + ): Effect.Effect +} => { + const parse = Schema.decodeUnknown(Schema.parseJson(schema), options) + return dual< + (field: string) => (self: UrlParams) => Effect.Effect, + (self: UrlParams, field: string) => Effect.Effect + >(2, (self, field) => parse(Option.getOrElse(getLast(self, field), () => ""))) +} + +/** + * Extract schema from all key-value pairs in the given `UrlParams`. + * + * **Example** + * + * ```ts + * import * as assert from "node:assert" + * import { Effect, Schema } from "effect" + * import { UrlParams } from "@effect/platform" + * + * Effect.gen(function* () { + * const urlParams = UrlParams.fromInput({ "a": [10, "string"], "b": false }) + * const result = yield* UrlParams.schemaStruct(Schema.Struct({ + * a: Schema.Tuple(Schema.NumberFromString, Schema.String), + * b: Schema.BooleanFromString + * }))(urlParams) + * + * assert.deepStrictEqual(result, { + * a: [10, "string"], + * b: false + * }) + * }) + * ``` + * + * @since 1.0.0 + * @category schema + */ +export const schemaStruct = | undefined>, R>( + schema: Schema.Schema, + options?: ParseOptions | undefined +) => +(self: UrlParams): Effect.Effect => { + const parse = Schema.decodeUnknown(schema, options) + return parse(toRecord(self)) +} + +/** + * @since 1.0.0 + * @category schema + */ +export const schemaFromString: Schema.Schema = Schema.transform( + Schema.String, + schemaFromSelf, + { + decode(fromA) { + return fromInput(new URLSearchParams(fromA)) + }, + encode(toI) { + return toString(toI) + } + } +) + +/** + * @since 1.0.0 + * @category schema + */ +export const schemaRecord = | undefined>, R>( + schema: Schema.Schema +): Schema.Schema => + Schema.transform( + schemaFromSelf, + schema, + { + decode(fromA) { + return toRecord(fromA) as I + }, + encode(toI) { + return fromInput(toI as Input) as UrlParams + } + } + ) + +/** + * @since 1.0.0 + * @category schema + */ +export const schemaParse = | undefined>, R>( + schema: Schema.Schema +): Schema.Schema => + Schema.compose( + schemaFromString, + schemaRecord(schema) + ) diff --git a/repos/effect/packages/platform/src/Worker.ts b/repos/effect/packages/platform/src/Worker.ts new file mode 100644 index 0000000..0f6a3d2 --- /dev/null +++ b/repos/effect/packages/platform/src/Worker.ts @@ -0,0 +1,369 @@ +/** + * @since 1.0.0 + */ +import type * as Context from "effect/Context" +import type * as Deferred from "effect/Deferred" +import type * as Duration from "effect/Duration" +import type * as Effect from "effect/Effect" +import type { LazyArg } from "effect/Function" +import type * as Layer from "effect/Layer" +import type * as ParseResult from "effect/ParseResult" +import type * as Pool from "effect/Pool" +import type * as Schema from "effect/Schema" +import type * as Scope from "effect/Scope" +import type * as Stream from "effect/Stream" +import * as internal from "./internal/worker.js" +import type { WorkerError, WorkerErrorFrom } from "./WorkerError.js" + +/** + * @since 1.0.0 + * @category models + */ +export interface BackingWorker { + readonly send: (message: I, transfers?: ReadonlyArray) => Effect.Effect + readonly run: ( + handler: (_: BackingWorker.Message) => Effect.Effect + ) => Effect.Effect +} + +/** + * @since 1.0.0 + * @category models + */ +export declare namespace BackingWorker { + /** + * @since 1.0.0 + * @category models + */ + export type Message = readonly [ready: 0] | readonly [data: 1, O] +} + +/** + * @since 1.0.0 + * @category type ids + */ +export const PlatformWorkerTypeId: unique symbol = internal.PlatformWorkerTypeId + +/** + * @since 1.0.0 + * @category type ids + */ +export type PlatformWorkerTypeId = typeof PlatformWorkerTypeId + +/** + * @since 1.0.0 + * @category models + */ +export interface PlatformWorker { + readonly [PlatformWorkerTypeId]: PlatformWorkerTypeId + readonly spawn: (id: number) => Effect.Effect, WorkerError, Spawner> +} + +/** + * @since 1.0.0 + */ +export const makePlatform: () => < + P extends { readonly postMessage: (message: any, transfers?: any | undefined) => void } +>( + options: { + readonly setup: (options: { readonly worker: W; readonly scope: Scope.Scope }) => Effect.Effect + readonly listen: ( + options: { + readonly port: P + readonly emit: (data: any) => void + readonly deferred: Deferred.Deferred + readonly scope: Scope.Scope + } + ) => Effect.Effect + } +) => PlatformWorker = internal.makePlatform + +/** + * @since 1.0.0 + * @category tags + */ +export const PlatformWorker: Context.Tag = internal.PlatformWorker + +/** + * @since 1.0.0 + * @category models + */ +export interface Worker { + readonly id: number + readonly execute: (message: I) => Stream.Stream + readonly executeEffect: (message: I) => Effect.Effect +} + +/** + * @since 1.0.0 + * @category models + */ +export interface Spawner { + readonly _: unique symbol +} + +/** + * @since 1.0.0 + * @category tags + */ +export const Spawner: Context.Tag> = internal.Spawner + +/** + * @since 1.0.0 + * @category models + */ +export interface SpawnerFn { + (id: number): W +} + +/** + * @since 1.0.0 + * @category models + */ +export declare namespace Worker { + /** + * @since 1.0.0 + * @category models + */ + export interface Options { + readonly encode?: ((message: I) => Effect.Effect) | undefined + readonly initialMessage?: LazyArg | undefined + } + + /** + * @since 1.0.0 + * @category models + */ + export type Request = + | readonly [id: number, data: 0, I, trace: Span | undefined] + | readonly [id: number, interrupt: 1] + + /** + * @since 1.0.0 + * @category models + */ + export type Span = readonly [traceId: string, spanId: string, sampled: boolean] + + /** + * @since 1.0.0 + * @category models + */ + export type Response = + | readonly [id: number, data: 0, ReadonlyArray] + | readonly [id: number, end: 1] + | readonly [id: number, end: 1, ReadonlyArray] + | readonly [id: number, error: 2, E] + | readonly [id: number, defect: 3, Schema.CauseEncoded] +} + +/** + * @since 1.0.0 + * @category models + */ +export interface WorkerPool { + readonly backing: Pool.Pool, WorkerError> + readonly broadcast: (message: I) => Effect.Effect + readonly execute: (message: I) => Stream.Stream + readonly executeEffect: (message: I) => Effect.Effect +} + +/** + * @since 1.0.0 + * @category models + */ +export declare namespace WorkerPool { + /** + * @since 1.0.0 + * @category models + */ + export type Options = + & Worker.Options + & ({ + readonly onCreate?: (worker: Worker) => Effect.Effect + readonly size: number + readonly concurrency?: number | undefined + readonly targetUtilization?: number | undefined + } | { + readonly onCreate?: (worker: Worker) => Effect.Effect + readonly minSize: number + readonly maxSize: number + readonly concurrency?: number | undefined + readonly targetUtilization?: number | undefined + readonly timeToLive: Duration.DurationInput + }) +} + +/** + * @since 1.0.0 + * @category type ids + */ +export const WorkerManagerTypeId: unique symbol = internal.WorkerManagerTypeId + +/** + * @since 1.0.0 + * @category type ids + */ +export type WorkerManagerTypeId = typeof WorkerManagerTypeId + +/** + * @since 1.0.0 + * @category models + */ +export interface WorkerManager { + readonly [WorkerManagerTypeId]: WorkerManagerTypeId + readonly spawn: ( + options: Worker.Options + ) => Effect.Effect, WorkerError, Scope.Scope | Spawner> +} + +/** + * @since 1.0.0 + * @category tags + */ +export const WorkerManager: Context.Tag = internal.WorkerManager + +/** + * @since 1.0.0 + * @category constructors + */ +export const makeManager: Effect.Effect = internal.makeManager + +/** + * @since 1.0.0 + * @category layers + */ +export const layerManager: Layer.Layer = internal.layerManager + +/** + * @since 1.0.0 + * @category constructors + */ +export const makePool: ( + options: WorkerPool.Options +) => Effect.Effect, WorkerError, WorkerManager | Spawner | Scope.Scope> = internal.makePool + +/** + * @since 1.0.0 + * @category constructors + */ +export const makePoolLayer: ( + tag: Context.Tag>, + options: WorkerPool.Options +) => Layer.Layer = internal.makePoolLayer + +/** + * @since 1.0.0 + * @category models + */ +export interface SerializedWorker { + readonly id: number + readonly execute: ( + message: Req + ) => Req extends Schema.WithResult + ? Stream.Stream + : never + readonly executeEffect: ( + message: Req + ) => Req extends Schema.WithResult + ? Effect.Effect + : never +} + +/** + * @since 1.0.0 + * @category models + */ +export declare namespace SerializedWorker { + /** + * @since 1.0.0 + * @category models + */ + export type Options = Extract extends never ? { + readonly initialMessage?: LazyArg + } + : { + readonly initialMessage: LazyArg> + } +} + +/** + * @since 1.0.0 + * @category models + */ +export interface SerializedWorkerPool { + readonly backing: Pool.Pool, WorkerError> + readonly broadcast: ( + message: Req + ) => Req extends Schema.WithResult + ? Effect.Effect + : never + readonly execute: ( + message: Req + ) => Req extends Schema.WithResult + ? Stream.Stream + : never + readonly executeEffect: ( + message: Req + ) => Req extends Schema.WithResult + ? Effect.Effect + : never +} + +/** + * @since 1.0.0 + * @category models + */ +export declare namespace SerializedWorkerPool { + /** + * @since 1.0.0 + * @category models + */ + export type Options = + & SerializedWorker.Options + & ({ + readonly onCreate?: (worker: Worker) => Effect.Effect + readonly size: number + readonly concurrency?: number | undefined + readonly targetUtilization?: number | undefined + } | { + readonly onCreate?: (worker: Worker) => Effect.Effect + readonly minSize: number + readonly maxSize: number + readonly concurrency?: number | undefined + readonly targetUtilization?: number | undefined + readonly timeToLive: Duration.DurationInput + }) +} + +/** + * @since 1.0.0 + * @category constructors + */ +export const makeSerialized: ( + options: SerializedWorker.Options +) => Effect.Effect, WorkerError, WorkerManager | Spawner | Scope.Scope> = internal.makeSerialized + +/** + * @since 1.0.0 + * @category constructors + */ +export const makePoolSerialized: ( + options: SerializedWorkerPool.Options +) => Effect.Effect, WorkerError, WorkerManager | Spawner | Scope.Scope> = + internal.makePoolSerialized + +/** + * @since 1.0.0 + * @category layers + */ +export const makePoolSerializedLayer: ( + tag: Context.Tag>, + options: SerializedWorkerPool.Options +) => Layer.Layer = internal.makePoolSerializedLayer + +/** + * @since 1.0.0 + * @category layers + */ +export const layerSpawner: (spawner: SpawnerFn) => Layer.Layer = + internal.layerSpawner diff --git a/repos/effect/packages/platform/src/WorkerError.ts b/repos/effect/packages/platform/src/WorkerError.ts new file mode 100644 index 0000000..79c4497 --- /dev/null +++ b/repos/effect/packages/platform/src/WorkerError.ts @@ -0,0 +1,87 @@ +/** + * @since 1.0.0 + */ +import type * as Cause from "effect/Cause" +import * as Predicate from "effect/Predicate" +import * as Schema from "effect/Schema" +import * as internal from "./internal/workerError.js" + +/** + * @since 1.0.0 + * @category type ids + */ +export const WorkerErrorTypeId: unique symbol = internal.WorkerErrorTypeId + +/** + * @since 1.0.0 + * @category type ids + */ +export type WorkerErrorTypeId = typeof WorkerErrorTypeId + +/** + * @since 1.0.0 + * @category predicates + */ +export const isWorkerError = (u: unknown): u is WorkerError => Predicate.hasProperty(u, WorkerErrorTypeId) + +/** + * @since 1.0.0 + * @category errors + */ +export class WorkerError extends Schema.TaggedError()("WorkerError", { + reason: Schema.Literal("spawn", "decode", "send", "unknown", "encode"), + cause: Schema.Defect +}) { + /** + * @since 1.0.0 + */ + readonly [WorkerErrorTypeId]: WorkerErrorTypeId = WorkerErrorTypeId + + /** + * @since 1.0.0 + */ + static readonly Cause: Schema.Schema< + Cause.Cause, + Schema.CauseEncoded + > = Schema.Cause({ error: this, defect: Schema.Defect }) + + /** + * @since 1.0.0 + */ + static readonly encodeCause: (a: Cause.Cause) => Schema.CauseEncoded = Schema + .encodeSync(this.Cause) + + /** + * @since 1.0.0 + */ + static readonly decodeCause: (u: Schema.CauseEncoded) => Cause.Cause = Schema + .decodeSync(this.Cause) + + /** + * @since 1.0.0 + */ + get message(): string { + switch (this.reason) { + case "send": + return "An error occurred calling .postMessage" + case "spawn": + return "An error occurred while spawning a worker" + case "decode": + return "An error occurred during decoding" + case "encode": + return "An error occurred during encoding" + case "unknown": + return "An unexpected error occurred" + } + } +} + +/** + * @since 1.0.0 + * @category errors + */ +export interface WorkerErrorFrom { + readonly _tag: "WorkerError" + readonly reason: "spawn" | "decode" | "send" | "unknown" | "encode" + readonly cause: unknown +} diff --git a/repos/effect/packages/platform/src/WorkerRunner.ts b/repos/effect/packages/platform/src/WorkerRunner.ts new file mode 100644 index 0000000..91402cb --- /dev/null +++ b/repos/effect/packages/platform/src/WorkerRunner.ts @@ -0,0 +1,249 @@ +/** + * @since 1.0.0 + */ +import type * as Context from "effect/Context" +import type * as Deferred from "effect/Deferred" +import type * as Effect from "effect/Effect" +import type * as Layer from "effect/Layer" +import type * as Mailbox from "effect/Mailbox" +import type * as Schema from "effect/Schema" +import type * as Scope from "effect/Scope" +import type * as Stream from "effect/Stream" +import * as internal from "./internal/workerRunner.js" +import type { WorkerError } from "./WorkerError.js" + +/** + * @since 1.0.0 + * @category models + */ +export interface BackingRunner { + readonly run: ( + handler: (portId: number, message: I) => Effect.Effect | void + ) => Effect.Effect + readonly send: ( + portId: number, + message: O, + transfers?: ReadonlyArray + ) => Effect.Effect + readonly disconnects?: Mailbox.ReadonlyMailbox +} + +/** + * @since 1.0.0 + * @category models + */ +export declare namespace BackingRunner { + /** + * @since 1.0.0 + * @category models + */ + export type Message = readonly [request: 0, I] | readonly [close: 1] +} + +/** + * @since 1.0.0 + * @category type ids + */ +export const PlatformRunnerTypeId: unique symbol = internal.PlatformRunnerTypeId + +/** + * @since 1.0.0 + * @category type ids + */ +export type PlatformRunnerTypeId = typeof PlatformRunnerTypeId + +/** + * @since 1.0.0 + * @category models + */ +export interface PlatformRunner { + readonly [PlatformRunnerTypeId]: PlatformRunnerTypeId + readonly start: (closeLatch: typeof CloseLatch.Service) => Effect.Effect, WorkerError> +} + +/** + * @since 1.0.0 + * @category tags + */ +export const PlatformRunner: Context.Tag = internal.PlatformRunner + +/** + * The worker close latch is used by platform runners to signal that the worker + * has been closed. + * + * @since 1.0.0 + * @category CloseLatch + */ +export interface CloseLatch { + readonly _: unique symbol +} + +/** + * The worker close latch is used by platform runners to signal that the worker + * has been closed. + * + * @since 1.0.0 + * @category CloseLatch + */ +export const CloseLatch: Context.Reference> = internal.CloseLatch + +/** + * @since 1.0.0 + * @category CloseLatch + */ +export const layerCloseLatch: Layer.Layer = internal.layerCloseLatch + +/** + * @since 1.0.0 + * @category models + */ +export declare namespace Runner { + /** + * @since 1.0.0 + * @category models + */ + export interface Options { + readonly decode?: ( + message: unknown + ) => Effect.Effect + readonly encodeOutput?: ( + request: I, + message: O + ) => Effect.Effect + readonly encodeError?: ( + request: I, + error: E + ) => Effect.Effect + } +} + +/** + * @since 1.0.0 + * @category constructors + */ +export const make: ( + process: (request: I) => Stream.Stream | Effect.Effect, + options?: Runner.Options +) => Effect.Effect = internal.make + +/** + * @since 1.0.0 + * @category layers + */ +export const layer: ( + process: (request: I) => Stream.Stream | Effect.Effect, + options?: Runner.Options | undefined +) => Layer.Layer = internal.layer + +/** + * @since 1.0.0 + * @category models + */ +export declare namespace SerializedRunner { + /** + * @since 1.0.0 + */ + export type Handlers = { + readonly [K in A["_tag"]]: Extract< + A, + { readonly _tag: K } + > extends Schema.SerializableWithResult< + infer S, + infer _SI, + infer _SR, + infer A, + infer _AI, + infer E, + infer _EI, + infer _RR + > ? ( + _: S + ) => + | Stream.Stream + | Effect.Effect + | Layer.Layer + | Layer.Layer + : never + } + + /** + * @since 1.0.0 + */ + export type HandlersContext< + Handlers extends Record) => any> + > = + | Exclude< + { + [K in keyof Handlers]: ReturnType extends Stream.Stream< + infer _A, + infer _E, + infer R + > ? R + : never + }[keyof Handlers], + InitialContext + > + | InitialEnv + + /** + * @since 1.0.0 + */ + type InitialContext< + Handlers extends Record) => any> + > = Handlers["InitialMessage"] extends ( + ...args: ReadonlyArray + ) => Layer.Layer ? A + : never + + /** + * @since 1.0.0 + */ + type InitialEnv< + Handlers extends Record) => any> + > = Handlers["InitialMessage"] extends ( + ...args: ReadonlyArray + ) => Layer.Layer ? R + : never +} + +/** + * @since 1.0.0 + * @category constructors + */ +export const makeSerialized: < + R, + I, + A extends Schema.TaggedRequest.All, + const Handlers extends SerializedRunner.Handlers +>( + schema: Schema.Schema, + handlers: Handlers +) => Effect.Effect< + void, + WorkerError, + PlatformRunner | Scope.Scope | R | SerializedRunner.HandlersContext +> = internal.makeSerialized + +/** + * @since 1.0.0 + * @category layers + */ +export const layerSerialized: < + R, + I, + A extends Schema.TaggedRequest.All, + const Handlers extends SerializedRunner.Handlers +>( + schema: Schema.Schema, + handlers: Handlers +) => Layer.Layer> = + internal.layerSerialized + +/** + * Launch the specified layer, interrupting the fiber when the CloseLatch is + * triggered. + * + * @since 1.0.0 + * @category Execution + */ +export const launch: (layer: Layer.Layer) => Effect.Effect = internal.launch diff --git a/repos/effect/packages/platform/src/index.ts b/repos/effect/packages/platform/src/index.ts new file mode 100644 index 0000000..06f07da --- /dev/null +++ b/repos/effect/packages/platform/src/index.ts @@ -0,0 +1,300 @@ +/** + * @since 1.0.0 + */ +export * as ChannelSchema from "./ChannelSchema.js" + +/** + * @since 1.0.0 + */ +export * as Command from "./Command.js" + +/** + * @since 1.0.0 + */ +export * as CommandExecutor from "./CommandExecutor.js" + +/** + * @since 1.0.0 + */ +export * as Cookies from "./Cookies.js" + +/** + * @since 1.0.0 + */ +export * as Effectify from "./Effectify.js" + +/** + * @since 1.0.0 + */ +export * as Error from "./Error.js" + +/** + * @since 1.0.0 + */ +export * as Etag from "./Etag.js" + +/** + * @since 1.0.0 + */ +export * as FetchHttpClient from "./FetchHttpClient.js" + +/** + * @since 1.0.0 + */ +export * as FileSystem from "./FileSystem.js" + +/** + * @since 1.0.0 + */ +export * as Headers from "./Headers.js" + +/** + * @since 1.0.0 + */ +export * as HttpApi from "./HttpApi.js" + +/** + * @since 1.0.0 + */ +export * as HttpApiBuilder from "./HttpApiBuilder.js" + +/** + * @since 1.0.0 + */ +export * as HttpApiClient from "./HttpApiClient.js" + +/** + * @since 1.0.0 + */ +export * as HttpApiEndpoint from "./HttpApiEndpoint.js" + +/** + * @since 1.0.0 + */ +export * as HttpApiError from "./HttpApiError.js" + +/** + * @since 1.0.0 + */ +export * as HttpApiGroup from "./HttpApiGroup.js" + +/** + * @since 1.0.0 + */ +export * as HttpApiMiddleware from "./HttpApiMiddleware.js" + +/** + * @since 1.0.0 + */ +export * as HttpApiScalar from "./HttpApiScalar.js" + +/** + * @since 1.0.0 + */ +export * as HttpApiSchema from "./HttpApiSchema.js" + +/** + * @since 1.0.0 + */ +export * as HttpApiSecurity from "./HttpApiSecurity.js" + +/** + * @since 1.0.0 + */ +export * as HttpApiSwagger from "./HttpApiSwagger.js" + +/** + * @since 1.0.0 + */ +export * as HttpApp from "./HttpApp.js" + +/** + * @since 1.0.0 + */ +export * as HttpBody from "./HttpBody.js" + +/** + * @since 1.0.0 + */ +export * as HttpClient from "./HttpClient.js" + +/** + * @since 1.0.0 + */ +export * as HttpClientError from "./HttpClientError.js" + +/** + * @since 1.0.0 + */ +export * as HttpClientRequest from "./HttpClientRequest.js" + +/** + * @since 1.0.0 + */ +export * as HttpClientResponse from "./HttpClientResponse.js" + +/** + * @since 1.0.0 + */ +export * as HttpIncomingMessage from "./HttpIncomingMessage.js" + +/** + * @since 1.0.0 + */ +export * as HttpLayerRouter from "./HttpLayerRouter.js" + +/** + * @since 1.0.0 + * @category models + */ +export * as HttpMethod from "./HttpMethod.js" + +/** + * @since 1.0.0 + */ +export * as HttpMiddleware from "./HttpMiddleware.js" + +/** + * @since 1.0.0 + */ +export * as HttpMultiplex from "./HttpMultiplex.js" + +/** + * @since 1.0.0 + */ +export * as HttpPlatform from "./HttpPlatform.js" + +/** + * @since 1.0.0 + */ +export * as HttpRouter from "./HttpRouter.js" + +/** + * @since 1.0.0 + */ +export * as HttpServer from "./HttpServer.js" + +/** + * @since 1.0.0 + */ +export * as HttpServerError from "./HttpServerError.js" + +/** + * @since 1.0.0 + */ +export * as HttpServerRequest from "./HttpServerRequest.js" + +/** + * @since 1.0.0 + */ +export * as HttpServerRespondable from "./HttpServerRespondable.js" + +/** + * @since 1.0.0 + */ +export * as HttpServerResponse from "./HttpServerResponse.js" + +/** + * @since 1.0.0 + */ +export * as HttpTraceContext from "./HttpTraceContext.js" + +/** + * @since 1.0.0 + */ +export * as KeyValueStore from "./KeyValueStore.js" + +/** + * @since 1.0.0 + */ +export * as MsgPack from "./MsgPack.js" + +/** + * @since 1.0.0 + */ +export * as Multipart from "./Multipart.js" + +/** + * @since 1.0.0 + */ +export * as Ndjson from "./Ndjson.js" + +/** + * @since 1.0.0 + */ +export * as OpenApi from "./OpenApi.js" + +/** + * @since 1.0.0 + */ +export * as OpenApiJsonSchema from "./OpenApiJsonSchema.js" + +/** + * @since 1.0.0 + */ +export * as Path from "./Path.js" + +/** + * @since 1.0.0 + */ +export * as PlatformConfigProvider from "./PlatformConfigProvider.js" + +/** + * @since 1.0.0 + */ +export * as PlatformLogger from "./PlatformLogger.js" + +/** + * @since 1.0.0 + */ +export * as Runtime from "./Runtime.js" + +/** + * @since 1.0.0 + */ +export * as Socket from "./Socket.js" + +/** + * @since 1.0.0 + */ +export * as SocketServer from "./SocketServer.js" + +/** + * @since 1.0.0 + */ +export * as Template from "./Template.js" + +/** + * @since 1.0.0 + */ +export * as Terminal from "./Terminal.js" + +/** + * @since 1.0.0 + */ +export * as Transferable from "./Transferable.js" + +/** + * @since 1.0.0 + */ +export * as Url from "./Url.js" + +/** + * @since 1.0.0 + */ +export * as UrlParams from "./UrlParams.js" + +/** + * @since 1.0.0 + */ +export * as Worker from "./Worker.js" + +/** + * @since 1.0.0 + */ +export * as WorkerError from "./WorkerError.js" + +/** + * @since 1.0.0 + */ +export * as WorkerRunner from "./WorkerRunner.js" diff --git a/repos/effect/packages/platform/src/internal/command.ts b/repos/effect/packages/platform/src/internal/command.ts new file mode 100644 index 0000000..f61b7bb --- /dev/null +++ b/repos/effect/packages/platform/src/internal/command.ts @@ -0,0 +1,281 @@ +import type * as ReadonlyArray from "effect/Array" +import * as Chunk from "effect/Chunk" +import * as Effect from "effect/Effect" +import { dual } from "effect/Function" +import * as HashMap from "effect/HashMap" +import * as Inspectable from "effect/Inspectable" +import * as Option from "effect/Option" +import { pipeArguments } from "effect/Pipeable" +import type { Scope } from "effect/Scope" +import * as Stream from "effect/Stream" +import type * as Command from "../Command.js" +import type * as CommandExecutor from "../CommandExecutor.js" +import type { PlatformError } from "../Error.js" +import * as commandExecutor from "./commandExecutor.js" + +/** @internal */ +export const CommandTypeId: Command.CommandTypeId = Symbol.for("@effect/platform/Command") as Command.CommandTypeId + +/** @internal */ +export const isCommand = (u: unknown): u is Command.Command => typeof u === "object" && u != null && CommandTypeId in u + +/** @internal */ +export const env: { + (environment: Record): (self: Command.Command) => Command.Command + (self: Command.Command, environment: Record): Command.Command +} = dual< + (environment: Record) => (self: Command.Command) => Command.Command, + (self: Command.Command, environment: Record) => Command.Command +>(2, (self, environment) => { + switch (self._tag) { + case "StandardCommand": { + return makeStandard({ + ...self, + env: HashMap.union( + self.env, + HashMap.fromIterable(Object.entries(environment).filter(([v]) => v !== undefined)) + ) as HashMap.HashMap + }) + } + case "PipedCommand": { + return pipeTo(env(self.left, environment), env(self.right, environment)) + } + } +}) + +/** @internal */ +export const exitCode = ( + self: Command.Command +): Effect.Effect => + Effect.flatMap(commandExecutor.CommandExecutor, (executor) => executor.exitCode(self)) + +/** @internal */ +export const feed = dual< + (input: string) => (self: Command.Command) => Command.Command, + (self: Command.Command, input: string) => Command.Command +>(2, (self, input) => stdin(self, Stream.fromChunk(Chunk.of(new TextEncoder().encode(input))))) + +/** @internal */ +export const flatten = (self: Command.Command): ReadonlyArray.NonEmptyReadonlyArray => + Array.from(flattenLoop(self)) as unknown as ReadonlyArray.NonEmptyReadonlyArray< + Command.StandardCommand + > + +/** @internal */ +const flattenLoop = (self: Command.Command): Chunk.NonEmptyChunk => { + switch (self._tag) { + case "StandardCommand": { + return Chunk.of(self) + } + case "PipedCommand": { + return Chunk.appendAll( + flattenLoop(self.left), + flattenLoop(self.right) + ) as Chunk.NonEmptyChunk + } + } +} + +/** @internal */ +export const runInShell = dual< + (shell: boolean | string) => (self: Command.Command) => Command.Command, + (self: Command.Command, shell: boolean | string) => Command.Command +>(2, (self: Command.Command, shell: boolean | string): Command.Command => { + switch (self._tag) { + case "StandardCommand": { + return makeStandard({ ...self, shell }) + } + case "PipedCommand": { + return pipeTo( + runInShell(self.left, shell), + runInShell(self.right, shell) + ) + } + } +}) + +/** @internal */ +export const lines = ( + command: Command.Command, + encoding = "utf-8" +): Effect.Effect, PlatformError, CommandExecutor.CommandExecutor> => + Effect.flatMap(commandExecutor.CommandExecutor, (executor) => executor.lines(command, encoding)) + +const Proto = { + [CommandTypeId]: CommandTypeId, + pipe() { + return pipeArguments(this, arguments) + }, + ...Inspectable.BaseProto +} + +const StandardProto = { + ...Proto, + _tag: "StandardCommand", + toJSON(this: Command.StandardCommand) { + return { + _id: "@effect/platform/Command", + _tag: this._tag, + command: this.command, + args: this.args, + env: Object.fromEntries(this.env), + cwd: this.cwd.toJSON(), + shell: this.shell, + gid: this.gid.toJSON(), + uid: this.uid.toJSON() + } + } +} + +const makeStandard = (options: Omit): Command.StandardCommand => + Object.assign(Object.create(StandardProto), options) + +const PipedProto = { + ...Proto, + _tag: "PipedCommand", + toJSON(this: Command.PipedCommand) { + return { + _id: "@effect/platform/Command", + _tag: this._tag, + left: this.left.toJSON(), + right: this.right.toJSON() + } + } +} + +const makePiped = (options: Omit): Command.PipedCommand => + Object.assign(Object.create(PipedProto), options) + +/** @internal */ +export const make = (command: string, ...args: Array): Command.Command => + makeStandard({ + command, + args, + env: HashMap.empty(), + cwd: Option.none(), + shell: false, + stdin: "pipe", + stdout: "pipe", + stderr: "pipe", + gid: Option.none(), + uid: Option.none() + }) + +/** @internal */ +export const pipeTo = dual< + (into: Command.Command) => (self: Command.Command) => Command.Command, + (self: Command.Command, into: Command.Command) => Command.Command +>(2, (self, into) => + makePiped({ + left: self, + right: into + })) + +/** @internal */ +export const stderr: { + (stderr: Command.Command.Output): (self: Command.Command) => Command.Command + (self: Command.Command, stderr: Command.Command.Output): Command.Command +} = dual< + (stderr: Command.Command.Output) => (self: Command.Command) => Command.Command, + (self: Command.Command, stderr: Command.Command.Output) => Command.Command +>(2, (self, output) => { + switch (self._tag) { + case "StandardCommand": { + return makeStandard({ ...self, stderr: output }) + } + // For piped commands it only makes sense to provide `stderr` for the + // right-most command as the rest will be piped in. + case "PipedCommand": { + return makePiped({ ...self, right: stderr(self.right, output) }) + } + } +}) + +/** @internal */ +export const stdin: { + (stdin: Command.Command.Input): (self: Command.Command) => Command.Command + (self: Command.Command, stdin: Command.Command.Input): Command.Command +} = dual< + (stdin: Command.Command.Input) => (self: Command.Command) => Command.Command, + (self: Command.Command, stdin: Command.Command.Input) => Command.Command +>(2, (self, input) => { + switch (self._tag) { + case "StandardCommand": { + return makeStandard({ ...self, stdin: input }) + } + // For piped commands it only makes sense to provide `stdin` for the + // left-most command as the rest will be piped in. + case "PipedCommand": { + return makePiped({ ...self, left: stdin(self.left, input) }) + } + } +}) + +/** @internal */ +export const stdout: { + (stdout: Command.Command.Output): (self: Command.Command) => Command.Command + (self: Command.Command, stdout: Command.Command.Output): Command.Command +} = dual< + (stdout: Command.Command.Output) => (self: Command.Command) => Command.Command, + (self: Command.Command, stdout: Command.Command.Output) => Command.Command +>(2, (self, output) => { + switch (self._tag) { + case "StandardCommand": { + return makeStandard({ ...self, stdout: output }) + } + // For piped commands it only makes sense to provide `stderr` for the + // right-most command as the rest will be piped in. + case "PipedCommand": { + return makePiped({ ...self, right: stdout(self.right, output) }) + } + } +}) + +/** @internal */ +export const start = ( + command: Command.Command +): Effect.Effect => + Effect.flatMap(commandExecutor.CommandExecutor, (executor) => executor.start(command)) + +/** @internal */ +export const stream = ( + command: Command.Command +): Stream.Stream => + Stream.flatMap(commandExecutor.CommandExecutor, (executor) => executor.stream(command)) + +/** @internal */ +export const streamLines = ( + command: Command.Command, + encoding?: string +): Stream.Stream => + Stream.flatMap(commandExecutor.CommandExecutor, (executor) => executor.streamLines(command, encoding)) + +/** @internal */ +export const string = dual< + ( + encoding?: string + ) => (command: Command.Command) => Effect.Effect, + (command: Command.Command, encoding?: string) => Effect.Effect +>( + (args) => isCommand(args[0]), + (command, encoding) => + Effect.flatMap(commandExecutor.CommandExecutor, (executor) => executor.string(command, encoding)) +) + +/** @internal */ +export const workingDirectory: { + (cwd: string): (self: Command.Command) => Command.Command + (self: Command.Command, cwd: string): Command.Command +} = dual< + (cwd: string) => (self: Command.Command) => Command.Command, + (self: Command.Command, cwd: string) => Command.Command +>(2, (self, cwd) => { + switch (self._tag) { + case "StandardCommand": { + return makeStandard({ ...self, cwd: Option.some(cwd) }) + } + case "PipedCommand": { + return pipeTo(workingDirectory(self.left, cwd), workingDirectory(self.right, cwd)) + } + } +}) diff --git a/repos/effect/packages/platform/src/internal/commandExecutor.ts b/repos/effect/packages/platform/src/internal/commandExecutor.ts new file mode 100644 index 0000000..1ae82c8 --- /dev/null +++ b/repos/effect/packages/platform/src/internal/commandExecutor.ts @@ -0,0 +1,71 @@ +import * as Brand from "effect/Brand" +import * as Chunk from "effect/Chunk" +import { GenericTag } from "effect/Context" +import * as Effect from "effect/Effect" +import { pipe } from "effect/Function" +import * as Sink from "effect/Sink" +import * as Stream from "effect/Stream" +import type * as _CommandExecutor from "../CommandExecutor.js" + +/** @internal */ +export const TypeId: _CommandExecutor.TypeId = Symbol.for("@effect/platform/CommandExecutor") as _CommandExecutor.TypeId + +/** @internal */ +export const ProcessTypeId: _CommandExecutor.ProcessTypeId = Symbol.for( + "@effect/platform/Process" +) as _CommandExecutor.ProcessTypeId + +/** @internal */ +export const ExitCode = Brand.nominal<_CommandExecutor.ExitCode>() + +/** @internal */ +export const ProcessId = Brand.nominal<_CommandExecutor.Process.Id>() + +/** @internal */ +export const CommandExecutor = GenericTag<_CommandExecutor.CommandExecutor>("@effect/platform/CommandExecutor") + +/** @internal */ +export const makeExecutor = (start: _CommandExecutor.CommandExecutor["start"]): _CommandExecutor.CommandExecutor => { + const stream: _CommandExecutor.CommandExecutor["stream"] = (command) => + Stream.unwrapScoped(Effect.map(start(command), (process) => process.stdout)) + const streamLines: _CommandExecutor.CommandExecutor["streamLines"] = (command, encoding) => { + const decoder = new TextDecoder(encoding) + return Stream.splitLines( + Stream.mapChunks(stream(command), Chunk.map((bytes) => decoder.decode(bytes))) + ) + } + return { + [TypeId]: TypeId, + start, + exitCode: (command) => Effect.scoped(Effect.flatMap(start(command), (process) => process.exitCode)), + stream, + string: (command, encoding = "utf-8") => { + const decoder = new TextDecoder(encoding) + return pipe( + start(command), + Effect.flatMap((process) => Stream.run(process.stdout, collectUint8Array)), + Effect.map((bytes) => decoder.decode(bytes)), + Effect.scoped + ) + }, + lines: (command, encoding = "utf-8") => { + return pipe( + streamLines(command, encoding), + Stream.runCollect, + Effect.map(Chunk.toArray) + ) + }, + streamLines + } +} + +const collectUint8Array: Sink.Sink = Sink.foldLeftChunks( + new Uint8Array(), + (bytes, chunk: Chunk.Chunk) => + Chunk.reduce(chunk, bytes, (acc, curr) => { + const newArray = new Uint8Array(acc.length + curr.length) + newArray.set(acc) + newArray.set(curr, acc.length) + return newArray + }) +) diff --git a/repos/effect/packages/platform/src/internal/effectify.ts b/repos/effect/packages/platform/src/internal/effectify.ts new file mode 100644 index 0000000..7680b43 --- /dev/null +++ b/repos/effect/packages/platform/src/internal/effectify.ts @@ -0,0 +1,31 @@ +import * as Effect from "effect/Effect" +import type { Effectify, EffectifyError } from "../Effectify.js" + +/** @internal */ +export const effectify: { + ) => any>(fn: F): Effectify> + ) => any, E>( + fn: F, + onError: (error: EffectifyError, args: Parameters) => E + ): Effectify + ) => any, E, E2>( + fn: F, + onError: (error: EffectifyError, args: Parameters) => E, + onSyncError: (error: unknown, args: Parameters) => E2 + ): Effectify +} = + ((fn: Function, onError?: (e: any, args: any) => any, onSyncError?: (e: any, args: any) => any) => + (...args: Array) => + Effect.async((resume) => { + try { + fn(...args, (err: Error | null, result: A) => { + if (err) { + resume(Effect.fail(onError ? onError(err, args) : err)) + } else { + resume(Effect.succeed(result)) + } + }) + } catch (err) { + resume(onSyncError ? Effect.fail(onSyncError(err, args)) : Effect.die(err)) + } + })) as any diff --git a/repos/effect/packages/platform/src/internal/etag.ts b/repos/effect/packages/platform/src/internal/etag.ts new file mode 100644 index 0000000..b11b6e2 --- /dev/null +++ b/repos/effect/packages/platform/src/internal/etag.ts @@ -0,0 +1,63 @@ +import * as Context from "effect/Context" +import * as Effect from "effect/Effect" +import * as Layer from "effect/Layer" +import type * as Etag from "../Etag.js" +import type * as FileSystem from "../FileSystem.js" +import type * as Body from "../HttpBody.js" + +/** @internal */ +export const GeneratorTypeId: Etag.GeneratorTypeId = Symbol.for( + "@effect/platform/Etag/Generator" +) as Etag.GeneratorTypeId + +/** @internal */ +export const tag = Context.GenericTag("@effect/platform/Etag/Generator") + +/** @internal */ +export const toString = (self: Etag.Etag): string => { + switch (self._tag) { + case "Weak": + return `W/"${self.value}"` + case "Strong": + return `"${self.value}"` + } +} + +const fromFileInfo = (info: FileSystem.File.Info) => { + const mtime = info.mtime._tag === "Some" + ? info.mtime.value.getTime().toString(16) + : "0" + return `${info.size.toString(16)}-${mtime}` +} + +const fromFileWeb = (file: Body.HttpBody.FileLike) => { + return `${file.size.toString(16)}-${file.lastModified.toString(16)}` +} + +/** @internal */ +export const layer = Layer.succeed( + tag, + tag.of({ + [GeneratorTypeId]: GeneratorTypeId, + fromFileInfo(info) { + return Effect.sync(() => ({ _tag: "Strong", value: fromFileInfo(info) })) + }, + fromFileWeb(file) { + return Effect.sync(() => ({ _tag: "Strong", value: fromFileWeb(file) })) + } + }) +) + +/** @internal */ +export const layerWeak = Layer.succeed( + tag, + tag.of({ + [GeneratorTypeId]: GeneratorTypeId, + fromFileInfo(info) { + return Effect.sync(() => ({ _tag: "Weak", value: fromFileInfo(info) })) + }, + fromFileWeb(file) { + return Effect.sync(() => ({ _tag: "Weak", value: fromFileWeb(file) })) + } + }) +) diff --git a/repos/effect/packages/platform/src/internal/fetchHttpClient.ts b/repos/effect/packages/platform/src/internal/fetchHttpClient.ts new file mode 100644 index 0000000..434c143 --- /dev/null +++ b/repos/effect/packages/platform/src/internal/fetchHttpClient.ts @@ -0,0 +1,54 @@ +import * as Effect from "effect/Effect" +import * as FiberRef from "effect/FiberRef" +import * as Stream from "effect/Stream" +import * as Headers from "../Headers.js" +import type * as Client from "../HttpClient.js" +import * as Error from "../HttpClientError.js" +import * as client from "./httpClient.js" +import * as internalResponse from "./httpClientResponse.js" + +/** @internal */ +export const fetchTagKey = "@effect/platform/FetchHttpClient/Fetch" +/** @internal */ +export const requestInitTagKey = "@effect/platform/FetchHttpClient/FetchOptions" + +const fetch: Client.HttpClient = client.make((request, url, signal, fiber) => { + const context = fiber.getFiberRef(FiberRef.currentContext) + const fetch: typeof globalThis.fetch = context.unsafeMap.get(fetchTagKey) ?? globalThis.fetch + const options: RequestInit = context.unsafeMap.get(requestInitTagKey) ?? {} + const headers = options.headers ? Headers.merge(Headers.fromInput(options.headers), request.headers) : request.headers + const send = (body: BodyInit | undefined) => + Effect.map( + Effect.tryPromise({ + try: () => + fetch(url, { + ...options, + method: request.method, + headers, + body, + duplex: request.body._tag === "Stream" ? "half" : undefined, + signal + } as any), + catch: (cause) => + new Error.RequestError({ + request, + reason: "Transport", + cause + }) + }), + (response) => internalResponse.fromWeb(request, response) + ) + switch (request.body._tag) { + case "Raw": + case "Uint8Array": + return send(request.body.body as any) + case "FormData": + return send(request.body.formData) + case "Stream": + return Effect.flatMap(Stream.toReadableStreamEffect(request.body.stream), send) + } + return send(undefined) +}) + +/** @internal */ +export const layer = client.layerMergedContext(Effect.succeed(fetch)) diff --git a/repos/effect/packages/platform/src/internal/fileSystem.ts b/repos/effect/packages/platform/src/internal/fileSystem.ts new file mode 100644 index 0000000..89bf627 --- /dev/null +++ b/repos/effect/packages/platform/src/internal/fileSystem.ts @@ -0,0 +1,239 @@ +import * as Channel from "effect/Channel" +import * as Chunk from "effect/Chunk" +import { GenericTag } from "effect/Context" +import * as Effect from "effect/Effect" +import { identity, pipe } from "effect/Function" +import * as Layer from "effect/Layer" +import * as Option from "effect/Option" +import * as Sink from "effect/Sink" +import * as Stream from "effect/Stream" +import * as Error from "../Error.js" +import type { File, FileSystem, Size as Size_, SizeInput, StreamOptions } from "../FileSystem.js" + +/** @internal */ +export const tag = GenericTag("@effect/platform/FileSystem") + +/** @internal */ +export const Size = (bytes: SizeInput) => typeof bytes === "bigint" ? bytes as Size_ : BigInt(bytes) as Size_ + +/** @internal */ +export const KiB = (n: number) => Size(n * 1024) + +/** @internal */ +export const MiB = (n: number) => Size(n * 1024 * 1024) + +/** @internal */ +export const GiB = (n: number) => Size(n * 1024 * 1024 * 1024) + +/** @internal */ +export const TiB = (n: number) => Size(n * 1024 * 1024 * 1024 * 1024) + +const bigint1024 = BigInt(1024) +const bigintPiB = bigint1024 * bigint1024 * bigint1024 * bigint1024 * bigint1024 + +/** @internal */ +export const PiB = (n: number) => Size(BigInt(n) * bigintPiB) + +/** @internal */ +export const make = ( + impl: Omit +): FileSystem => { + return tag.of({ + ...impl, + exists: (path) => + pipe( + impl.access(path), + Effect.as(true), + Effect.catchTag("SystemError", (e) => e.reason === "NotFound" ? Effect.succeed(false) : Effect.fail(e)) + ), + readFileString: (path, encoding) => + Effect.tryMap(impl.readFile(path), { + try: (_) => new TextDecoder(encoding).decode(_), + catch: (cause) => + new Error.BadArgument({ + module: "FileSystem", + method: "readFileString", + description: "invalid encoding", + cause + }) + }), + stream: (path, options) => + pipe( + impl.open(path, { flag: "r" }), + options?.offset ? + Effect.tap((file) => file.seek(options.offset!, "start")) : + identity, + Effect.map((file) => stream(file, options)), + Stream.unwrapScoped + ), + sink: (path, options) => + pipe( + impl.open(path, { flag: "w", ...options }), + Effect.map((file) => Sink.forEach((_: Uint8Array) => file.writeAll(_))), + Sink.unwrapScoped + ), + writeFileString: (path, data, options) => + Effect.flatMap( + Effect.try({ + try: () => new TextEncoder().encode(data), + catch: (cause) => + new Error.BadArgument({ + module: "FileSystem", + method: "writeFileString", + description: "could not encode string", + cause + }) + }), + (_) => impl.writeFile(path, _, options) + ) + }) +} + +const notFound = (method: string, path: string) => + new Error.SystemError({ + module: "FileSystem", + method, + reason: "NotFound", + description: "No such file or directory", + pathOrDescriptor: path + }) + +/** @internal */ +export const makeNoop = ( + fileSystem: Partial +): FileSystem => { + return { + access(path) { + return Effect.fail(notFound("access", path)) + }, + chmod(path) { + return Effect.fail(notFound("chmod", path)) + }, + chown(path) { + return Effect.fail(notFound("chown", path)) + }, + copy(path) { + return Effect.fail(notFound("copy", path)) + }, + copyFile(path) { + return Effect.fail(notFound("copyFile", path)) + }, + exists() { + return Effect.succeed(false) + }, + link(path) { + return Effect.fail(notFound("link", path)) + }, + makeDirectory() { + return Effect.die("not implemented") + }, + makeTempDirectory() { + return Effect.die("not implemented") + }, + makeTempDirectoryScoped() { + return Effect.die("not implemented") + }, + makeTempFile() { + return Effect.die("not implemented") + }, + makeTempFileScoped() { + return Effect.die("not implemented") + }, + open(path) { + return Effect.fail(notFound("open", path)) + }, + readDirectory(path) { + return Effect.fail(notFound("readDirectory", path)) + }, + readFile(path) { + return Effect.fail(notFound("readFile", path)) + }, + readFileString(path) { + return Effect.fail(notFound("readFileString", path)) + }, + readLink(path) { + return Effect.fail(notFound("readLink", path)) + }, + realPath(path) { + return Effect.fail(notFound("realPath", path)) + }, + remove() { + return Effect.void + }, + rename(oldPath) { + return Effect.fail(notFound("rename", oldPath)) + }, + sink(path) { + return Sink.fail(notFound("sink", path)) + }, + stat(path) { + return Effect.fail(notFound("stat", path)) + }, + stream(path) { + return Stream.fail(notFound("stream", path)) + }, + symlink(fromPath) { + return Effect.fail(notFound("symlink", fromPath)) + }, + truncate(path) { + return Effect.fail(notFound("truncate", path)) + }, + utimes(path) { + return Effect.fail(notFound("utimes", path)) + }, + watch(path) { + return Stream.fail(notFound("watch", path)) + }, + writeFile(path) { + return Effect.fail(notFound("writeFile", path)) + }, + writeFileString(path) { + return Effect.fail(notFound("writeFileString", path)) + }, + ...fileSystem + } +} + +/** @internal */ +export const layerNoop = ( + fileSystem: Partial +): Layer.Layer => Layer.succeed(tag, makeNoop(fileSystem)) + +/** @internal */ +const stream = (file: File, { + bufferSize = 16, + bytesToRead: bytesToRead_, + chunkSize: chunkSize_ = Size(64 * 1024) +}: StreamOptions = {}) => { + const bytesToRead = bytesToRead_ !== undefined ? Size(bytesToRead_) : undefined + const chunkSize = Size(chunkSize_) + + function loop( + totalBytesRead: bigint + ): Channel.Channel, unknown, Error.PlatformError, unknown, void, unknown> { + if (bytesToRead !== undefined && bytesToRead <= totalBytesRead) { + return Channel.void + } + + const toRead = bytesToRead !== undefined && (bytesToRead - totalBytesRead) < chunkSize + ? bytesToRead - totalBytesRead + : chunkSize + + return Channel.flatMap( + file.readAlloc(toRead), + Option.match({ + onNone: () => Channel.void, + onSome: (buf) => + Channel.flatMap( + Channel.write(Chunk.of(buf)), + (_) => loop(totalBytesRead + BigInt(buf.length)) + ) + }) + ) + } + + return Stream.bufferChunks( + Stream.fromChannel(loop(BigInt(0))), + { capacity: bufferSize } + ) +} diff --git a/repos/effect/packages/platform/src/internal/html.ts b/repos/effect/packages/platform/src/internal/html.ts new file mode 100644 index 0000000..bda17e6 --- /dev/null +++ b/repos/effect/packages/platform/src/internal/html.ts @@ -0,0 +1,32 @@ +// Regex to find closing tags in JSON to prevent breaking out of script context +const ESCAPE_SCRIPT_END = /<\/script>/gi + +// Regex to find Unicode line terminators that are valid JSON but break JS string literals +const ESCAPE_LINE_TERMS = /[\u2028\u2029]/g + +/** + * Safely serialize an object to a JSON string + * and escape any sequences that could break ` with `<\/script>` to avoid premature tag closing. + * - Escapes U+2028 and U+2029 as literal \u2028 / \u2029. + * + * @internal + */ +export function escapeJson(spec: unknown): string { + return JSON.stringify(spec) + .replace(ESCAPE_SCRIPT_END, "<\\/script>") + .replace(ESCAPE_LINE_TERMS, (c) => c === "\u2028" ? "\\u2028" : "\\u2029") +} + +/** + * HTML-escape text content to prevent injection in text nodes or attributes. + * + * @internal + */ +export function escape(str: string): string { + return str + .replace(/&/g, "&") + .replace(//g, ">") +} diff --git a/repos/effect/packages/platform/src/internal/httpApiScalar.ts b/repos/effect/packages/platform/src/internal/httpApiScalar.ts new file mode 100644 index 0000000..e08bce7 --- /dev/null +++ b/repos/effect/packages/platform/src/internal/httpApiScalar.ts @@ -0,0 +1,4 @@ +/* eslint-disable */ + +/** @internal */ +export const javascript = "/**\n * Minified by jsDelivr using Terser v5.39.0.\n * Original file: /npm/@scalar/api-reference@1.39.3/dist/browser/standalone.js\n *\n * Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files\n */\n!function(){\"use strict\";try{if(\"undefined\"!=typeof document){var e=document.createElement(\"style\");e.appendChild(document.createTextNode('.references-classic-header[data-v-9198d025]{display:flex;align-items:center;gap:12px;max-width:var(--refs-content-max-width);margin:auto;padding:12px 0}.references-classic-header-content[data-v-9198d025]{display:flex;gap:12px;flex-grow:1}.references-classic-header-container[data-v-9198d025]{padding:0 60px}@container narrow-references-container (max-width: 900px){.references-classic-header[data-v-9198d025]{padding:12px 24px}.references-classic-header-container[data-v-9198d025]{padding:0}}.references-classic-header-icon[data-v-9198d025]{height:24px;color:var(--scalar-color-1)}.auth-combobox-position[data-v-a85af23e]{margin-left:120px}.scroll-timeline-x[data-v-a85af23e]{overflow:auto;scroll-timeline:--scroll-timeline x;scroll-timeline:--scroll-timeline horizontal;scrollbar-width:none;-ms-overflow-style:none}.fade-left[data-v-a85af23e],.fade-right[data-v-a85af23e]{position:sticky;content:\"\";height:100%;animation-name:fadein-a85af23e;animation-duration:1ms;animation-direction:reverse;animation-timeline:--scroll-timeline;min-height:24px;pointer-events:none}.fade-left[data-v-a85af23e]{background:linear-gradient(-90deg,color-mix(in srgb,var(--scalar-background-1),transparent 100%) 0%,color-mix(in srgb,var(--scalar-background-1),transparent 20%) 60%,var(--scalar-background-1) 100%);min-width:3px;left:-1px;animation-direction:normal}.fade-right[data-v-a85af23e]{background:linear-gradient(90deg,color-mix(in srgb,var(--scalar-background-1),transparent 100%) 0%,color-mix(in srgb,var(--scalar-background-1),transparent 20%) 60%,var(--scalar-background-1) 100%);margin-left:-20px;min-width:24px;right:-1px;top:0}@keyframes fadein-a85af23e{0%{opacity:0}15%{opacity:1}}.auth-combobox-position[data-v-43114fd7]{margin-left:120px}.scroll-timeline-x[data-v-43114fd7]{overflow:auto;scroll-timeline:--scroll-timeline x;scroll-timeline:--scroll-timeline horizontal;scrollbar-width:none;-ms-overflow-style:none}.fade-left[data-v-43114fd7],.fade-right[data-v-43114fd7]{position:sticky;content:\"\";height:100%;animation-name:fadein-43114fd7;animation-duration:1ms;animation-direction:reverse;animation-timeline:--scroll-timeline;min-height:24px;pointer-events:none}.fade-left[data-v-43114fd7]{background:linear-gradient(-90deg,color-mix(in srgb,var(--scalar-background-1),transparent 100%) 0%,color-mix(in srgb,var(--scalar-background-1),transparent 20%) 60%,var(--scalar-background-1) 100%);min-width:3px;left:-1px;animation-direction:normal}.fade-right[data-v-43114fd7]{background:linear-gradient(90deg,color-mix(in srgb,var(--scalar-background-1),transparent 100%) 0%,color-mix(in srgb,var(--scalar-background-1),transparent 20%) 60%,var(--scalar-background-1) 100%);margin-left:-20px;min-width:24px;right:-1px;top:0}@keyframes fadein-43114fd7{0%{opacity:0}15%{opacity:1}}.client-libraries-content[data-v-b7785507]{container:client-libraries-content / inline-size;display:flex;justify-content:center;overflow:hidden;padding:0 12px;background-color:var(--scalar-background-1);border-left:var(--scalar-border-width) solid var(--scalar-border-color);border-right:var(--scalar-border-width) solid var(--scalar-border-color)}.client-libraries[data-v-b7785507]{display:flex;align-items:center;justify-content:center;width:100%;position:relative;cursor:pointer;white-space:nowrap;padding:8px 2px;gap:6px;color:var(--scalar-color-3);border-bottom:1px solid transparent;-webkit-user-select:none;user-select:none}.client-libraries[data-v-b7785507]:not(.client-libraries__active):hover:before{content:\"\";position:absolute;width:calc(100% - 4px);height:calc(100% - 4px);background:var(--scalar-background-2);left:2px;top:2px;z-index:0;border-radius:var(--scalar-radius)}.client-libraries[data-v-b7785507]:active{color:var(--scalar-color-1)}.client-libraries[data-v-b7785507]:focus-visible{outline:none;box-shadow:inset 0 0 0 1px var(--scalar-color-accent)}@media screen and (max-width: 450px){.client-libraries[data-v-b7785507]:nth-of-type(4),.client-libraries[data-v-b7785507]:nth-of-type(5){display:none}}.client-libraries-icon[data-v-b7785507]{max-width:14px;max-height:14px;min-width:14px;width:100%;aspect-ratio:1;display:flex;align-items:center;justify-content:center;position:relative;box-sizing:border-box;color:currentColor}.client-libraries-icon__more svg[data-v-b7785507]{height:initial}@container client-libraries-content (width < 400px){.client-libraries__select[data-v-b7785507]{width:fit-content}.client-libraries__select .client-libraries-icon__more+span[data-v-b7785507]{display:none}}@container client-libraries-content (width < 380px){.client-libraries[data-v-b7785507]{width:100%}.client-libraries span[data-v-b7785507]{display:none}}.client-libraries__active[data-v-b7785507]{color:var(--scalar-color-1);border-bottom:1px solid var(--scalar-color-1)}@keyframes codeloader-b7785507{0%{transform:rotate(0)}to{transform:rotate(1turn)}}.client-libraries .client-libraries-text[data-v-b7785507]{font-size:var(--scalar-small);position:relative;display:flex;align-items:center}.client-libraries__active .client-libraries-text[data-v-b7785507]{color:var(--scalar-color-1);font-weight:var(--scalar-semibold)}@media screen and (max-width: 600px){.references-classic .client-libraries[data-v-b7785507]{flex-direction:column}}.selected-client[data-v-abfa446f]{color:var(--scalar-color-1);font-size:var(--scalar-small);font-family:var(--scalar-font-code);padding:9px 12px;border-top:none;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;background:var(--scalar-background-1);border:var(--scalar-border-width) solid var(--scalar-border-color);border-bottom-left-radius:var(--scalar-radius-lg);border-bottom-right-radius:var(--scalar-radius-lg);min-height:fit-content}.client-libraries-heading[data-v-abfa446f]{font-size:var(--scalar-small);font-weight:var(--scalar-font-medium);color:var(--scalar-color-1);padding:9px 12px;background-color:var(--scalar-background-2);display:flex;align-items:center;max-height:32px;border:var(--scalar-border-width) solid var(--scalar-border-color);border-top-left-radius:var(--scalar-radius-lg);border-top-right-radius:var(--scalar-radius-lg)}[data-v-abfa446f] .scalar-codeblock-pre .hljs{margin-top:8px}.badge[data-v-3dedb7e4]{color:var(--badge-text-color, var(--scalar-color-2));font-size:var(--scalar-mini);background:var(--badge-background-color, var(--scalar-background-2));border:var(--scalar-border-width) solid var(--badge-border-color, var(--scalar-border-color));padding:2px 6px;border-radius:12px;display:inline-block}.badge.text-orange[data-v-3dedb7e4]{background:color-mix(in srgb,var(--scalar-color-orange),transparent 90%);border:transparent}.badge.text-yellow[data-v-3dedb7e4]{background:color-mix(in srgb,var(--scalar-color-yellow),transparent 90%);border:transparent}.badge.text-red[data-v-3dedb7e4]{background:color-mix(in srgb,var(--scalar-color-red),transparent 90%);border:transparent}.badge.text-purple[data-v-3dedb7e4]{background:color-mix(in srgb,var(--scalar-color-purple),transparent 90%);border:transparent}.badge.text-green[data-v-3dedb7e4]{background:color-mix(in srgb,var(--scalar-color-green),transparent 90%);border:transparent}@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){[data-v-767248c2],[data-v-767248c2]:before,[data-v-767248c2]:after,[data-v-767248c2]::backdrop{--tw-outline-style:solid}}}.download-container[data-v-767248c2]{z-index:1;flex-direction:column;gap:16px;width:fit-content;margin:0 .5px 8px;display:flex;position:relative}.download-container[data-v-767248c2]:has(:focus-visible):before,.download-container.download-both[data-v-767248c2]:hover:before{content:\"\";border-radius:var(--scalar-radius-lg);width:calc(100% + 24px);height:90px;box-shadow:var(--scalar-shadow-2);pointer-events:none;background:var(--scalar-background-1);position:absolute;top:-11px;left:-12px}.download-button[data-v-767248c2]{color:var(--scalar-link-color);cursor:pointer;outline:none;justify-content:center;align-items:center;gap:4px;height:fit-content;padding:0;display:flex;position:relative;white-space:nowrap!important}.download-button[data-v-767248c2]:before{border-radius:var(--scalar-radius);content:\"\";width:calc(100% + 18px);height:calc(100% + 16px);position:absolute;top:-8px;left:-9px}.download-button[data-v-767248c2]:last-of-type:before{width:calc(100% + 15px)}.download-button[data-v-767248c2]:hover:before{background:var(--scalar-background-2);border:var(--scalar-border-width)solid var(--scalar-border-color)}.download-button[data-v-767248c2]:focus-visible:before{background:var(--scalar-background-2);border:var(--scalar-border-width)solid var(--scalar-border-color);outline-style:var(--tw-outline-style);outline-width:1px}.download-button span[data-v-767248c2]{--font-color:var(--scalar-link-color,var(--scalar-color-accent));--font-visited:var(--scalar-link-color-visited,var(--scalar-color-2));-webkit-text-decoration:var(--scalar-text-decoration);text-decoration:var(--scalar-text-decoration);color:var(--font-color);font-weight:var(--scalar-link-font-weight,var(--scalar-semibold));text-underline-offset:.25rem;text-decoration-thickness:1px;-webkit-text-decoration-color:var(--font-color);text-decoration-color:var(--font-color)}@supports (color:color-mix(in lab,red,red)){.download-button span[data-v-767248c2]{-webkit-text-decoration-color:color-mix(in srgb,var(--font-color)30%,transparent);text-decoration-color:color-mix(in srgb,var(--font-color)30%,transparent)}}.download-button span[data-v-767248c2]{z-index:1;align-items:center;gap:6px;line-height:1.625;display:flex}.download-button:hover span[data-v-767248c2]{color:var(--scalar-link-color-hover,var(--scalar-color-accent));-webkit-text-decoration:var(--scalar-text-decoration-hover);text-decoration:var(--scalar-text-decoration-hover)}.download-button[data-v-767248c2]:nth-of-type(2){clip:rect(0,0,0,0);white-space:nowrap;border-width:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.download-container:has(:focus-visible) .download-button[data-v-767248c2]:nth-of-type(2),.download-container:hover .download-button[data-v-767248c2]:nth-of-type(2){clip:auto;white-space:normal;width:auto;height:auto;margin:0;padding:0;position:absolute;top:42px;overflow:visible}.extension[data-v-767248c2]{z-index:1;background:var(--scalar-link-color,var(--scalar-color-accent));color:var(--scalar-background-1)}.download-container:has(:focus-visible) .extension[data-v-767248c2],.download-container:hover .extension[data-v-767248c2]{opacity:1}.download-link[data-v-767248c2]{--font-color:var(--scalar-link-color,var(--scalar-color-accent));--font-visited:var(--scalar-link-color-visited,var(--scalar-color-2));-webkit-text-decoration:var(--scalar-text-decoration);text-decoration:var(--scalar-text-decoration);color:var(--font-color);font-weight:var(--scalar-link-font-weight,var(--scalar-semibold));text-underline-offset:.25rem;text-decoration-thickness:1px;-webkit-text-decoration-color:var(--font-color);text-decoration-color:var(--font-color)}@supports (color:color-mix(in lab,red,red)){.download-link[data-v-767248c2]{-webkit-text-decoration-color:color-mix(in srgb,var(--font-color)30%,transparent);text-decoration-color:color-mix(in srgb,var(--font-color)30%,transparent)}}.download-link[data-v-767248c2]:hover{--font-color:var(--scalar-link-color,var(--scalar-color-accent));-webkit-text-decoration-color:var(--font-color);text-decoration-color:var(--font-color)}.introduction-card[data-v-a8605b85]{display:flex;flex-direction:column}.introduction-card-row[data-v-a8605b85]{gap:24px}@media (min-width: 600px){.introduction-card-row[data-v-a8605b85]{flex-flow:row wrap}}.introduction-card-row[data-v-a8605b85]>*{flex:1}@media (min-width: 600px){.introduction-card-row[data-v-a8605b85]>*{min-width:min-content}}@media (max-width: 600px){.introduction-card-row[data-v-a8605b85]>*{max-width:100%}}@container (max-width: 900px){.introduction-card-row[data-v-a8605b85]{flex-direction:column;align-items:stretch;gap:0px}}.introduction-card[data-v-a8605b85] .security-scheme-label{text-transform:uppercase;font-weight:var(--scalar-semibold)}.introduction-card-row[data-v-a8605b85] .scalar-card:nth-of-type(2) .scalar-card-header{display:none}.introduction-card-row[data-v-a8605b85] .scalar-card:nth-of-type(2) .scalar-card-header.scalar-card--borderless+.scalar-card-content{margin-top:0}.section[data-v-be4443e9]{position:relative;display:flex;flex-direction:column;max-width:var(--refs-content-max-width);margin:auto;padding:90px 0;scroll-margin-top:var(--refs-viewport-offset)}.section[data-v-be4443e9]:has(~div.contents){border-bottom:var(--scalar-border-width) solid var(--scalar-border-color)}.references-classic .section[data-v-be4443e9]{padding:48px 0;gap:24px}@container narrow-references-container (max-width: 900px){.references-classic .section[data-v-be4443e9],.section[data-v-be4443e9]{padding:48px 24px}}.section[data-v-be4443e9]:not(:last-of-type){border-bottom:var(--scalar-border-width) solid var(--scalar-border-color)}.section-wrapper[data-v-ee08c887]{color:var(--scalar-color-1);padding-top:12px;margin-top:-12px}.section-accordion[data-v-ee08c887]{display:flex;flex-direction:column;border-radius:var(--scalar-radius-lg);background:var(--scalar-background-2)}.section-accordion-transparent[data-v-ee08c887]{background:transparent;border:var(--scalar-border-width) solid var(--scalar-border-color)}.section-accordion-button[data-v-ee08c887]{padding:6px}.section-accordion-button[data-v-ee08c887]{display:flex;align-items:center;gap:6px;cursor:pointer}.section-accordion-button-content[data-v-ee08c887]{flex:1;min-width:0}.section-accordion-button-actions[data-v-ee08c887]{display:flex;align-items:center;gap:6px;color:var(--scalar-color-3)}.section-accordion-chevron[data-v-ee08c887]{margin-right:4px;cursor:pointer;opacity:1;color:var(--scalar-color-3)}.section-accordion-button:hover .section-accordion-chevron[data-v-ee08c887]{color:var(--scalar-color-1)}.section-accordion-content[data-v-ee08c887]{border-top:var(--scalar-border-width) solid var(--scalar-border-color);display:flex;flex-direction:column}.section-accordion-description[data-v-ee08c887]{font-weight:var(--scalar-semibold);font-size:var(--scalar-mini);color:var(--scalar-color--1);padding:10px 12px 0}.section-accordion-content-card[data-v-ee08c887] .property:last-of-type{padding-bottom:9px}.section-column[data-v-699c28e3]{flex:1;min-width:0}@container narrow-references-container (max-width: 900px){.section-column[data-v-699c28e3]:nth-of-type(2){padding-top:0}}.section-columns[data-v-8b9602bf]{display:flex;gap:48px}@container narrow-references-container (max-width: 900px){.section-columns[data-v-8b9602bf]{flex-direction:column;gap:24px}}.section-container[data-v-20a1472a]{position:relative;padding:0 60px;width:100%;border-top:var(--scalar-border-width) solid var(--scalar-border-color)}.section-container[data-v-20a1472a]:has(.introduction-section){border-top:none}@container narrow-references-container (max-width: 900px){.section-container[data-v-20a1472a]{padding:0}}.section-accordion-wrapper[data-v-9419dd23]{padding:0 60px}.section-accordion[data-v-9419dd23]{position:relative;width:100%;max-width:var(--refs-content-max-width);margin:auto}.section-accordion-content[data-v-9419dd23]{display:flex;flex-direction:column;gap:12px;padding-top:12px}.section-accordion-button[data-v-9419dd23]{width:100%;display:flex;cursor:pointer;padding:6px 0;margin:-6px 0;border-radius:var(--scalar-radius)}.section-accordion-chevron[data-v-9419dd23]{position:absolute;left:-22px;top:12px;color:var(--scalar-color-3)}.section-accordion-button:hover .section-accordion-chevron[data-v-9419dd23]{color:var(--scalar-color-1)}.section-accordion-title[data-v-9419dd23]{display:flex;flex-direction:column;align-items:flex-start;flex:1;padding:0 6px}.section-accordion-title[data-v-9419dd23] .section-header-wrapper{grid-template-columns:1fr}.section-accordion-title[data-v-9419dd23] .section-header{margin-bottom:0}@container narrow-references-container (max-width: 900px){.section-accordion-chevron[data-v-9419dd23]{width:16px;left:-16px;top:14px}.section-accordion-wrapper[data-v-9419dd23]{padding:calc(var(--refs-viewport-offset)) 24px 0 24px}}.loading[data-v-8e0226d7]{background:var(--scalar-background-3);animation:loading-skeleton-8e0226d7 1.5s infinite alternate;border-radius:var(--scalar-radius-lg);min-height:1.6em;margin:.6em 0;max-width:100%}.loading[data-v-8e0226d7]:first-of-type{min-height:3em;margin-bottom:24px;margin-top:0}.loading[data-v-8e0226d7]:last-of-type{width:60%}.loading.single-line[data-v-8e0226d7]{min-height:3em;margin:.6em 0;max-width:80%}@keyframes loading-skeleton-8e0226d7{0%{opacity:1}to{opacity:.33}}@container narrow-references-container (max-width: 900px){.section-content--with-columns[data-v-9735459e]{flex-direction:column;gap:24px}}.section-header-wrapper[data-v-465a7a78]{grid-template-columns:1fr;display:grid}@media (min-width:1200px){.section-header-wrapper[data-v-465a7a78]{grid-template-columns:repeat(2,1fr)}}.section-header[data-v-465a7a78]{font-size:var(--font-size,var(--scalar-heading-1));font-weight:var(--font-weight,var(--scalar-bold));color:var(--scalar-color-1);word-wrap:break-word;margin-top:0;margin-bottom:12px;line-height:1.45}.section-header.tight[data-v-465a7a78]{margin-bottom:6px}.section-header.loading[data-v-465a7a78]{width:80%}.section-header-label[data-v-f1ac6c38]{display:inline}.screenreader-only[data-v-df2e1026]{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0}.collapsible-section[data-v-999a158a]{border-top:var(--scalar-border-width) solid var(--scalar-border-color);position:relative}.collapsible-section-header[data-v-999a158a]{color:var(--scalar-color-1)}.collapsible-section .collapsible-section-trigger[data-v-999a158a]{display:flex;align-items:center;cursor:pointer;padding:10px 0;font-size:var(--scalar-font-size-3);z-index:1;position:relative}.collapsible-section-trigger svg[data-v-999a158a]{color:var(--scalar-color-3);position:absolute;left:-19px}.collapsible-section:hover .collapsible-section-trigger svg[data-v-999a158a]{color:var(--scalar-color-1)}.collapsible-section .collapsible-section-trigger[data-v-999a158a] .anchor-copy{line-height:18.5px}.collapsible-section-content[data-v-999a158a]{padding:0;margin:0 0 10px;scroll-margin-top:140px}.references-classic .introduction-description[data-v-62f8a5d1] img{max-width:720px}.icons-only[data-v-0939d4d9] span{display:none}.sticky-cards[data-v-06ff5db3]{display:flex;flex-direction:column;position:sticky;top:calc(var(--refs-viewport-offset) + 24px)}.introduction-card-item[data-v-708aae59]{display:flex;margin-bottom:12px;flex-direction:column;justify-content:start}.introduction-card-item[data-v-708aae59]:has(.description) .server-form-container{border-bottom-left-radius:0;border-bottom-right-radius:0}.introduction-card-item[data-v-708aae59] .request-item{border-bottom:0}.schema-type-icon[data-v-d7e5e7ec]{color:var(--scalar-color-1);display:none}.schema-type[data-v-d7e5e7ec]{font-family:var(--scalar-font-code);color:var(--scalar-color-1)}.property-enum-value[data-v-8a5eccde]{color:var(--scalar-color-3);line-height:1.5;overflow-wrap:break-word;display:flex;align-items:stretch;position:relative;--decorator-width: 1px;--decorator-color: color-mix( in srgb, var(--scalar-background-1), var(--scalar-color-1) 25% )}.property-enum-value-content[data-v-8a5eccde]{display:flex;flex-direction:column;padding:3px 0}.property-enum-value-label[data-v-8a5eccde]{display:flex;font-family:var(--scalar-font-code);color:var(--scalar-color-1);position:relative}.property-enum-value:last-of-type .property-enum-value-label[data-v-8a5eccde]{padding-bottom:0}.property-enum-value[data-v-8a5eccde]:before{content:\"\";margin-right:12px;width:var(--decorator-width);display:block;background-color:var(--decorator-color)}.property-enum-value[data-v-8a5eccde]:last-of-type:before,.property-enum-values:has(.enum-toggle-button) .property-enum-value[data-v-8a5eccde]:nth-last-child(2):before{height:calc(.5lh + 4px)}.property-enum-value-label[data-v-8a5eccde]:after{content:\"\";position:absolute;top:.5lh;left:-12px;width:8px;height:var(--decorator-width);background-color:var(--decorator-color)}.property-enum-value[data-v-8a5eccde]:last-of-type:after{bottom:0;height:50%;background:var(--scalar-background-1);border-top:var(--scalar-border-width) solid var(--decorator-color)}.property-enum-value-description[data-v-8a5eccde]{color:var(--scalar-color-3)}.property-heading:empty+.property-description[data-v-d4946030]:last-of-type,.property-description[data-v-d4946030]:first-of-type:last-of-type{margin-top:0}.property-list[data-v-d4946030]{border:var(--scalar-border-width) solid var(--scalar-border-color);border-radius:var(--scalar-radius);margin-top:10px}.property-list .property[data-v-d4946030]:last-of-type{padding-bottom:10px}.property-enum-values[data-v-d4946030]{font-size:var(--scalar-font-size-3);list-style:none;margin-top:8px;padding-left:2px}.enum-toggle-button[data-v-d4946030]:hover{color:var(--scalar-color-1)}.property-detail[data-v-827ea49d]{display:inline-flex}.property-detail+.property-detail[data-v-827ea49d]:before{display:block;content:\"·\";margin:0 .5ch}.property-detail-truncate[data-v-827ea49d]{overflow:hidden}.property-detail-truncate>.property-detail-value[data-v-827ea49d]{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.property-detail-prefix[data-v-827ea49d]{color:var(--scalar-color-2)}code.property-detail-value[data-v-827ea49d]{font-family:var(--scalar-font-code);font-size:var(--scalar-font-size-3);color:var(--scalar-color-2);background:var(--scalar-background-3);padding:0 4px;border:.5px solid var(--scalar-border-color);border-radius:var(--scalar-radius)}.property-example[data-v-dd79da55]{display:flex;flex-direction:column;font-size:var(--scalar-mini);position:relative}.property-example[data-v-dd79da55]:hover:before{content:\"\";position:absolute;top:0;left:0;width:100%;height:20px;border-radius:var(--scalar-radius)}.property-example:hover .property-example-label span[data-v-dd79da55]{color:var(--scalar-color-1)}.property-example-label span[data-v-dd79da55]{color:var(--scalar-color-3);position:relative;border-bottom:var(--scalar-border-width) dotted currentColor}.property-example-value[data-v-dd79da55]{font-family:var(--scalar-font-code);display:flex;gap:8px;align-items:center;width:100%;padding:6px}.property-example-value span[data-v-dd79da55]{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.property-example-value[data-v-dd79da55] svg{color:var(--scalar-color-3)}.property-example-value[data-v-dd79da55]:hover svg{color:var(--scalar-color-1)}.property-example-value[data-v-dd79da55]{background:var(--scalar-background-2);border:var(--scalar-border-width) solid var(--scalar-border-color);border-radius:var(--scalar-radius)}.property-example-value-list[data-v-dd79da55]{position:absolute;top:18px;left:50%;transform:translate3d(-50%,0,0);overflow:auto;background-color:var(--scalar-background-1);box-shadow:var(--scalar-shadow-1);border-radius:var(--scalar-radius-lg);border:var(--scalar-border-width) solid var(--scalar-border-color);padding:9px;min-width:200px;max-width:300px;flex-direction:column;gap:3px;display:none;z-index:10}.property-example:hover .property-example-value-list[data-v-dd79da55],.property-example:focus-within .property-example-value-list[data-v-dd79da55]{display:flex}.property-heading[data-v-5d03e993]{display:flex;flex-wrap:wrap;align-items:baseline;row-gap:9px;white-space:nowrap}.property-heading[data-v-5d03e993]:has(+.children),.property-heading[data-v-5d03e993]:has(+.property-rule){margin-bottom:9px}.property-heading[data-v-5d03e993]>*{margin-right:9px}.property-heading[data-v-5d03e993]:last-child{margin-right:0}.property-heading>.property-detail[data-v-5d03e993]:not(:last-of-type){margin-right:0}.property-name[data-v-5d03e993]{max-width:100%;font-family:var(--scalar-font-code);font-weight:var(--scalar-semibold);font-size:var(--scalar-font-size-3);white-space:normal;overflow-wrap:break-word}.property-additional[data-v-5d03e993]{font-family:var(--scalar-font-code)}.property-required[data-v-5d03e993],.property-optional[data-v-5d03e993]{color:var(--scalar-color-2)}.property-required[data-v-5d03e993]{font-size:var(--scalar-mini);color:var(--scalar-color-orange)}.property-read-only[data-v-5d03e993]{font-size:var(--scalar-mini);color:var(--scalar-color-blue)}.property-write-only[data-v-5d03e993]{font-size:var(--scalar-mini);color:var(--scalar-color-green)}.property-discriminator[data-v-5d03e993]{font-size:var(--scalar-mini);color:var(--scalar-color-purple)}.property-detail[data-v-5d03e993]{font-size:var(--scalar-mini);color:var(--scalar-color-2);display:flex;align-items:center;min-width:0}.property-const[data-v-5d03e993]{color:var(--scalar-color-1)}.deprecated[data-v-5d03e993]{text-decoration:line-through}.property[data-v-0b17418b]{color:var(--scalar-color-1);display:flex;flex-direction:column;padding:8px;font-size:var(--scalar-small);position:relative}.property.property--level-0[data-v-0b17418b]:has(>.property-rule>.schema-card>.schema-properties.schema-properties-open>ul>li.property){padding-top:0}.property[data-v-0b17418b]:hover{z-index:1}.property--compact.property--level-0[data-v-0b17418b],.property--compact.property--level-1[data-v-0b17418b]{padding:8px 0}.composition-panel .property.property.property.property--level-0[data-v-0b17418b]{padding:0}.property--compact.property--level-0 .composition-panel .property--compact.property--level-1[data-v-0b17418b]{padding:8px}.property[data-v-0b17418b]:has(>.property-rule:nth-of-type(1)):not(.property--compact){padding-top:8px;padding-bottom:8px}.property--deprecated[data-v-0b17418b]{background:repeating-linear-gradient(-45deg,var(--scalar-background-2) 0,var(--scalar-background-2) 2px,transparent 2px,transparent 5px);background-size:100%}.property--deprecated[data-v-0b17418b]>*{opacity:.75}.property-description[data-v-0b17418b]{margin-top:6px;line-height:1.4;font-size:var(--scalar-small)}.property-description[data-v-0b17418b]:has(+.property-rule){margin-bottom:9px}[data-v-0b17418b] .property-description *{color:var(--scalar-color-2)!important}.property[data-v-0b17418b]:not(:last-of-type){border-bottom:var(--scalar-border-width) solid var(--scalar-border-color)}.property-description+.children[data-v-0b17418b],.children+.property-rule[data-v-0b17418b]{margin-top:9px}.children[data-v-0b17418b]{display:flex;flex-direction:column}.children .property--compact.property--level-1[data-v-0b17418b]{padding:12px}.property-example-value[data-v-0b17418b]{all:unset;font-family:var(--scalar-font-code);padding:6px;border-top:var(--scalar-border-width) solid var(--scalar-border-color)}.property-rule[data-v-0b17418b]{border-radius:var(--scalar-radius-lg);display:flex;flex-direction:column}.property-rule[data-v-0b17418b] .composition-panel .schema-card .schema-properties.schema-properties-open{border-top-left-radius:0;border-top-right-radius:0}.property-rule[data-v-0b17418b] .composition-panel>.schema-card>.schema-card-description{padding-left:8px;padding-right:8px;border-left:1px solid var(--scalar-border-color);border-right:1px solid var(--scalar-border-color)}.property-rule[data-v-0b17418b] .composition-panel>.schema-card>.schema-card-description+.schema-properties{margin-top:0}.property-example[data-v-0b17418b]{background:transparent;border:none;display:flex;flex-direction:row;gap:8px}.property-example-label[data-v-0b17418b],.property-example-value[data-v-0b17418b]{padding:3px 0 0}.property-example-value[data-v-0b17418b]{background:var(--scalar-background-2);border-top:0;border-radius:var(--scalar-radius);padding:3px 4px}.property-name[data-v-0b17418b]{font-family:var(--scalar-font-code);font-weight:var(--scalar-semibold)}.property-name-additional-properties[data-v-0b17418b]:before,.property-name-pattern-properties[data-v-0b17418b]:before{text-transform:uppercase;font-size:var(--scalar-micro);display:inline-block;padding:2px 4px;border-radius:var(--scalar-radius);color:var(--scalar-color-1);border:1px solid var(--scalar-border-color);background-color:var(--scalar-background-2);margin-right:4px}.property-name-pattern-properties[data-v-0b17418b]:before{content:\"regex\"}.property-name-additional-properties[data-v-0b17418b]:before{content:\"unknown\"}.error[data-v-40568e30]{background-color:var(--scalar-color-red)}.schema-card[data-v-40568e30]{z-index:0;font-size:var(--scalar-font-size-4);color:var(--scalar-color-1)}.schema-card-title[data-v-40568e30]{height:var(--schema-title-height);padding:6px 8px;display:flex;align-items:center;gap:4px;color:var(--scalar-color-2);font-weight:var(--scalar-semibold);font-size:var(--scalar-mini);border-bottom:var(--scalar-border-width) solid transparent}button.schema-card-title[data-v-40568e30]{cursor:pointer}button.schema-card-title[data-v-40568e30]:hover{color:var(--scalar-color-1)}.schema-card-title-icon--open[data-v-40568e30]{transform:rotate(45deg)}.schema-properties-open>.schema-card-title[data-v-40568e30]{border-bottom-left-radius:0;border-bottom-right-radius:0;border-bottom:var(--scalar-border-width) solid var(--scalar-border-color)}.schema-properties-open>.schema-properties[data-v-40568e30]{width:fit-content}.schema-card-description[data-v-40568e30]{color:var(--scalar-color-2)}.schema-card-description+.schema-properties[data-v-40568e30]{width:fit-content}.schema-card-description+.schema-properties[data-v-40568e30]{margin-top:8px}.schema-properties-open.schema-properties[data-v-40568e30],.schema-properties-open>.schema-card--open[data-v-40568e30]{width:100%}.schema-properties[data-v-40568e30]{display:flex;flex-direction:column;border:var(--scalar-border-width) solid var(--scalar-border-color);border-radius:var(--scalar-radius-lg);width:fit-content}.schema-properties-name[data-v-40568e30]{width:100%}.schema-properties .schema-properties[data-v-40568e30]{border-radius:13.5px}.schema-properties .schema-properties.schema-properties-open[data-v-40568e30]{border-radius:var(--scalar-radius-lg)}.schema-properties-open[data-v-40568e30]{width:100%}.schema-card--compact[data-v-40568e30]{align-self:flex-start}.schema-card--compact.schema-card--open[data-v-40568e30]{align-self:initial}.schema-card-title--compact[data-v-40568e30]{color:var(--scalar-color-2);padding:6px 10px 6px 8px;height:auto;border-bottom:none}.schema-card-title--compact>.schema-card-title-icon[data-v-40568e30]{margin:0}.schema-card-title--compact>.schema-card-title-icon--open[data-v-40568e30]{transform:rotate(45deg)}.schema-properties-open>.schema-card-title--compact[data-v-40568e30]{position:static}.property--level-0>.schema-properties>.schema-card--level-0>.schema-properties[data-v-40568e30]{border:none}.property--level-0 .schema-card--level-0:not(.schema-card--compact) .property--level-1[data-v-40568e30]{padding:0 0 8px}:not(.composition-panel)>.schema-card--compact.schema-card--level-0>.schema-properties[data-v-40568e30]{border:none}[data-v-40568e30] .schema-card-description p{font-size:var(--scalar-small, var(--scalar-paragraph));color:var(--scalar-color-2);line-height:1.5;display:block;margin-bottom:6px}.children .schema-card-description[data-v-40568e30]:first-of-type{padding-top:0}.reference-models-anchor[data-v-e9f2f7bc]{display:flex;align-items:center;font-size:20px;padding-left:6px;color:var(--scalar-color-1)}.reference-models-label[data-v-e9f2f7bc]{display:block;font-size:var(--scalar-mini)}.reference-models-label[data-v-e9f2f7bc] em{font-weight:var(--scalar-bold)}.show-more[data-v-3cd30981]{appearance:none;border:none;border:var(--scalar-border-width) solid var(--scalar-border-color);margin:auto;padding:8px 12px 8px 16px;border-radius:30px;color:var(--scalar-color-1);font-weight:var(--scalar-semibold);font-size:var(--scalar-small);display:flex;align-items:center;justify-content:center;position:relative;gap:6px;top:-48px}.show-more[data-v-3cd30981]:hover{background:var(--scalar-background-2);cursor:pointer}.show-more[data-v-3cd30981]:active{box-shadow:0 0 0 1px var(--scalar-border-color)}@container narrow-references-container (max-width: 900px){.show-more[data-v-3cd30981]{top:-24px}}.tag-section[data-v-1124be5d]{margin-bottom:48px}.tag-name[data-v-1124be5d]{text-transform:capitalize}.tag-description[data-v-1124be5d]{padding-bottom:4px;text-align:left}.endpoint[data-v-ad8530a6]{display:flex;white-space:nowrap;cursor:pointer;text-decoration:none}.endpoint:hover .endpoint-path[data-v-ad8530a6],.endpoint:focus-visible .endpoint-path[data-v-ad8530a6]{text-decoration:underline}.endpoint .post[data-v-ad8530a6],.endpoint .get[data-v-ad8530a6],.endpoint .delete[data-v-ad8530a6],.endpoint .put[data-v-ad8530a6]{white-space:nowrap}.endpoint-method[data-v-ad8530a6],.endpoint-path[data-v-ad8530a6]{color:var(--scalar-color-1);min-width:62px;display:inline-flex;line-height:1.55;font-family:var(--scalar-font-code);font-size:var(--scalar-small);cursor:pointer}.endpoint-method[data-v-ad8530a6]{text-align:right}.endpoint-path[data-v-ad8530a6]{margin-left:12px;text-transform:initial}.deprecated[data-v-ad8530a6]{text-decoration:line-through}.endpoints-card[data-v-f726f753]{position:sticky;top:calc(var(--refs-viewport-offset) + 24px);font-size:var(--scalar-font-size-3)}.endpoints[data-v-f726f753]{overflow:auto;background:var(--scalar-background-2);padding:10px 12px;width:100%}.section-container[data-v-3eabdf4c]{border-top:var(--scalar-border-width) solid var(--scalar-border-color)}.section-container[data-v-3eabdf4c]:has(.show-more){background-color:color-mix(in srgb,var(--scalar-background-2),transparent)}.operation-path[data-v-ec6c8861]{overflow:hidden;word-wrap:break-word;font-weight:var(--scalar-semibold);line-break:anywhere}.deprecated[data-v-ec6c8861]{text-decoration:line-through}.empty-state[data-v-0fa97c76]{margin:10px 0 10px 12px;text-align:center;font-size:var(--scalar-mini);min-height:56px;display:flex;align-items:center;justify-content:center;border-radius:var(--scalar-radius-lg);color:var(--scalar-color-2)}.rule-title[data-v-0fa97c76]{font-family:var(--scalar-font-code);color:var(--scalar-color-1);display:inline-block;margin:12px 0 6px;border-radius:var(--scalar-radius)}.rule[data-v-0fa97c76]{margin:0 12px;border-radius:var(--scalar-radius-lg)}.rule-items[data-v-0fa97c76]{counter-reset:list-number;display:flex;flex-direction:column;gap:12px;border-left:1px solid var(--scalar-border-color);padding:12px 0}.rule-item[data-v-0fa97c76]{counter-increment:list-number;border:1px solid var(--scalar-border-color);border-radius:var(--scalar-radius-lg);overflow:hidden;margin-left:24px}.rule-item[data-v-0fa97c76]:before{border:1px solid var(--scalar-border-color);border-top:0;border-right:0;content:\" \";display:block;width:24px;height:6px;border-radius:0 0 0 var(--scalar-radius-lg);margin-top:6px;color:var(--scalar-color-2);transform:translate(-25px);color:var(--scalar-color-1);position:absolute}.tab[data-v-c8207e48]{background:none;border:none;font-size:var(--scalar-small);font-family:var(--scalar-font);font-weight:var(--scalar-font-normal);color:var(--scalar-color-2);line-height:calc(var(--scalar-small) + 2px);white-space:nowrap;cursor:pointer;padding:0;margin-right:3px;text-transform:uppercase;position:relative;line-height:22px}.tab[data-v-c8207e48]:before{content:\"\";position:absolute;z-index:0;left:-6px;top:-6px;width:calc(100% + 12px);height:calc(100% + 12px);border-radius:var(--scalar-radius);background:var(--scalar-background-3);opacity:0}.tab[data-v-c8207e48]:hover:before,.tab[data-v-c8207e48]:focus-visible:before{opacity:1}.tab[data-v-c8207e48]:focus-visible:before{outline:1px solid var(--scalar-color-accent)}.tab span[data-v-c8207e48]{z-index:1;position:relative}.tab-selected[data-v-c8207e48]{color:var(--scalar-color-1);font-weight:var(--scalar-semibold)}.tab-selected[data-v-c8207e48]:after{content:\"\";position:absolute;background:currentColor;width:100%;left:0;height:1px;bottom:calc(var(--tab-list-padding-y) * -1)}.tab-list[data-v-fec8fbbb]{display:flex;gap:6px;position:relative;flex:1;--tab-list-padding-y: 7px;--tab-list-padding-x: 12px;padding:var(--tab-list-padding-y) var(--tab-list-padding-x);overflow:auto}.scalar-card-header.scalar-card-header-tabs[data-v-fec8fbbb]{padding:0}.response-card[data-v-bf753e60]{font-size:var(--scalar-font-size-3)}.markdown[data-v-bf753e60] *{margin:0}.code-copy[data-v-bf753e60]{display:flex;align-items:center;justify-content:center;appearance:none;-webkit-appearance:none;outline:none;background:transparent;cursor:pointer;color:var(--scalar-color-3);border:none;padding:0;margin-right:12px}.code-copy[data-v-bf753e60]:hover{color:var(--scalar-color-1)}.code-copy svg[data-v-bf753e60]{width:13px;height:13px}.response-card-footer[data-v-bf753e60]{display:flex;flex-direction:row;justify-content:space-between;flex-shrink:0;padding:7px 12px;gap:8px}.response-example-selector[data-v-bf753e60]{align-self:flex-start;margin:-4px}.response-description[data-v-bf753e60]{font-weight:var(--scalar-semibold);font-size:var(--scalar-small);color:var(--scalar-color--1);display:flex;align-items:center;box-sizing:border-box}.schema-type[data-v-bf753e60]{font-size:var(--scalar-micro);color:var(--scalar-color-2);font-weight:var(--scalar-semibold);background:var(--scalar-background-3);padding:2px 4px;border-radius:4px;margin-right:4px}.schema-example[data-v-bf753e60]{font-size:var(--scalar-micro);color:var(--scalar-color-2);font-weight:var(--scalar-semibold)}.example-response-tab[data-v-bf753e60]{display:block;margin:6px}.scalar-card-checkbox[data-v-bf753e60]{display:flex;align-items:center;justify-content:center;position:relative;min-height:17px;cursor:pointer;-webkit-user-select:none;user-select:none;font-size:var(--scalar-small);font-weight:var(--scalar-font-normal);color:var(--scalar-color-2);width:fit-content;white-space:nowrap;gap:6px;padding:7px 6px}.scalar-card-checkbox:has(.scalar-card-checkbox-input:focus-visible) .scalar-card-checkbox-checkmark[data-v-bf753e60]{outline:1px solid var(--scalar-color-accent)}.scalar-card-checkbox[data-v-bf753e60]:hover{color:var(--scalar-color--1)}.scalar-card-checkbox .scalar-card-checkbox-input[data-v-bf753e60]{position:absolute;opacity:0;cursor:pointer;height:0;width:0}.scalar-card-checkbox-checkmark[data-v-bf753e60]{height:16px;width:16px;border-radius:var(--scalar-radius);background-color:transparent;background-color:var(--scalar-background-3);box-shadow:inset 0 0 0 var(--scalar-border-width) var(--scalar-border-color)}.scalar-card-checkbox[data-v-bf753e60]:has(.scalar-card-checkbox-input:checked){color:var(--scalar-color-1);font-weight:var(--scalar-semibold)}.scalar-card-checkbox .scalar-card-checkbox-input:checked~.scalar-card-checkbox-checkmark[data-v-bf753e60]{background-color:var(--scalar-button-1);box-shadow:none}.scalar-card-checkbox-checkmark[data-v-bf753e60]:after{content:\"\";position:absolute;display:none}.scalar-card-checkbox .scalar-card-checkbox-input:checked~.scalar-card-checkbox-checkmark[data-v-bf753e60]:after{display:block}.scalar-card-checkbox .scalar-card-checkbox-checkmark[data-v-bf753e60]:after{right:11.5px;top:12.5px;width:5px;height:9px;border:solid 1px var(--scalar-button-1-color);border-width:0 1.5px 1.5px 0;transform:rotate(45deg)}.headers-card[data-v-6fb09984]{z-index:0;margin-top:12px;margin-bottom:6px;position:relative;font-size:var(--scalar-font-size-4);color:var(--scalar-color-1);align-self:flex-start}.headers-card.headers-card--open[data-v-6fb09984]{align-self:initial}.headers-card-title[data-v-6fb09984]{padding:6px 10px;display:flex;align-items:center;gap:4px;color:var(--scalar-color-3);font-weight:var(--scalar-semibold);font-size:var(--scalar-micro);border-radius:13.5px}button.headers-card-title[data-v-6fb09984]{cursor:pointer}button.headers-card-title[data-v-6fb09984]:hover{color:var(--scalar-color-1)}.headers-card-title-icon--open[data-v-6fb09984]{transform:rotate(45deg)}.headers-properties[data-v-6fb09984]{display:flex;flex-direction:column;border:var(--scalar-border-width) solid var(--scalar-border-color);border-radius:13.5px;width:fit-content}.headers-properties-open>.headers-card-title[data-v-6fb09984]{border-bottom-left-radius:0;border-bottom-right-radius:0;border-bottom:var(--scalar-border-width) solid var(--scalar-border-color)}.headers-properties-open[data-v-6fb09984]{border-radius:var(--scalar-radius-lg);width:100%}.headers-card .property[data-v-6fb09984]:last-of-type{padding-bottom:10px}.headers-card-title>.headers-card-title-icon[data-v-6fb09984]{width:10px;height:10px;margin:0}.headers-card-title>.headers-card-title-icon--open[data-v-6fb09984]{transform:rotate(45deg)}.parameter-item[data-v-8c1d456c]{display:flex;flex-direction:column;border-top:var(--scalar-border-width) solid var(--scalar-border-color)}.parameter-item:last-of-type .parameter-schema[data-v-8c1d456c]{padding-bottom:0}.parameter-item-container[data-v-8c1d456c]{padding:0}.parameter-item-headers[data-v-8c1d456c]{border:var(--scalar-border-width) solid var(--scalar-border-color)}.parameter-item-name[data-v-8c1d456c]{position:relative;font-weight:var(--scalar-semibold);font-size:var(--scalar-font-size-3);font-family:var(--scalar-font-code);color:var(--scalar-color-1);overflow-wrap:break-word}.parameter-item-description[data-v-8c1d456c],.parameter-item-description-summary[data-v-8c1d456c]{font-size:var(--scalar-mini);color:var(--scalar-color-2)}.parameter-item-description-summary.parameter-item-description-summary[data-v-8c1d456c]>*{--markdown-line-height: 1}.parameter-item-trigger+.parameter-item-container[data-v-8c1d456c] .property--level-0>.property-heading .property-detail-value{font-size:var(--scalar-micro)}.parameter-item-required-optional[data-v-8c1d456c]{color:var(--scalar-color-2);font-weight:var(--scalar-semibold);margin-right:6px;position:relative}.parameter-item--required[data-v-8c1d456c]{text-transform:uppercase;font-size:var(--scalar-micro);font-weight:var(--scalar-semibold);color:var(--scalar-color-orange)}.parameter-item-description[data-v-8c1d456c],.parameter-item-description[data-v-8c1d456c] p{margin-top:4px;font-size:var(--scalar-small);color:var(--scalar-color-2);line-height:1.4}.parameter-schema[data-v-8c1d456c]{padding-bottom:9px;margin-top:3px}.parameter-item-trigger[data-v-8c1d456c]{display:flex;align-items:baseline;gap:6px;flex-wrap:wrap;padding:12px 0;outline:none}.parameter-item-trigger-open[data-v-8c1d456c]{padding-bottom:0}.parameter-item-trigger[data-v-8c1d456c]:after{content:\"\";position:absolute;height:10px;width:100%;bottom:0}.parameter-item-icon[data-v-8c1d456c]{color:var(--scalar-color-3);left:-19px;top:.5lh;translate:0 -50%;position:absolute}.parameter-item-trigger:hover .parameter-item-icon[data-v-8c1d456c],.parameter-item-trigger:focus-visible .parameter-item-icon[data-v-8c1d456c]{color:var(--scalar-color-1)}.parameter-item-trigger:focus-visible .parameter-item-icon[data-v-8c1d456c]{outline:1px solid var(--scalar-color-accent);outline-offset:2px;border-radius:var(--scalar-radius)}.request-body[data-v-3e73fda7]{margin-top:24px}.request-body-header[data-v-3e73fda7]{display:flex;align-items:center;justify-content:space-between;padding-bottom:12px;border-bottom:var(--scalar-border-width) solid var(--scalar-border-color);flex-flow:wrap}.request-body-title[data-v-3e73fda7]{display:flex;align-items:center;gap:8px;font-size:var(--scalar-font-size-2);font-weight:var(--scalar-semibold);color:var(--scalar-color-1)}.request-body-required[data-v-3e73fda7]{font-size:var(--scalar-micro);color:var(--scalar-color-orange);font-weight:400}.request-body-description[data-v-3e73fda7]{margin-top:6px;font-size:var(--scalar-small);width:100%}.request-body-header+.request-body-schema[data-v-3e73fda7]:has(>.schema-card>.schema-card-description),.request-body-header+.request-body-schema[data-v-3e73fda7]:has(>.schema-card>.schema-properties>*>.property--level-0){padding-top:8px}.request-body-description[data-v-3e73fda7] .markdown *{color:var(--scalar-color-2)!important}.callback-sticky-offset[data-v-3c2f3b42]{top:var(--refs-viewport-offset, 0px);z-index:1}.callback-operation-container[data-v-3c2f3b42] .request-body,.callback-operation-container[data-v-3c2f3b42] .request-body-description,.callback-operation-container[data-v-3c2f3b42] .request-body-header{margin-top:0}.callback-operation-container[data-v-3c2f3b42] .request-body-header{--scalar-font-size-2: var(--scalar-font-size-4);padding:8px;border-bottom:none;border:.5px solid var(--scalar-border-color);border-radius:var(--scalar-radius-lg) var(--scalar-radius-lg) 0 0;background:color-mix(in srgb,var(--scalar-background-2) 50%,transparent)}.callback-operation-container[data-v-3c2f3b42] ul li.property.property--level-1{padding:8px}.callback-operation-container[data-v-3c2f3b42] .request-body-schema{background-color:var(--scalar-background-1);border:var(--scalar-border-width) solid var(--scalar-border-color);border-top:none;overflow:hidden;border-radius:0 0 var(--scalar-radius-lg) var(--scalar-radius-lg)}.callback-operation-container[data-v-3c2f3b42] .parameter-list{margin-top:0}.callback-operation-container[data-v-3c2f3b42] .parameter-list-title{background:color-mix(in srgb,var(--scalar-background-2) 50%,transparent);border-radius:var(--scalar-radius-lg) var(--scalar-radius-lg) 0 0;padding:8px;margin-bottom:0;border:var(--scalar-border-width) solid var(--scalar-border-color);border-bottom:none;--scalar-font-size-2: var(--scalar-font-size-4)}.callback-operation-container[data-v-3c2f3b42] .parameter-list-items{border:var(--scalar-border-width) solid var(--scalar-border-color);border-radius:0 0 var(--scalar-radius-lg) var(--scalar-radius-lg)}.callback-operation-container[data-v-3c2f3b42] .parameter-list-items>li:first-of-type{border-top:none}.callback-operation-container[data-v-3c2f3b42] .parameter-list-items>li{padding:0 8px}.show-api-client-button[data-v-f7468f9c]{appearance:none;border:none;padding:1px 6px;white-space:nowrap;border-radius:var(--scalar-radius);display:flex;justify-content:center;align-items:center;font-weight:var(--scalar-semibold);font-size:var(--scalar-small);line-height:22px;color:var(--scalar-background-2);font-family:var(--scalar-font);background:var(--scalar-button-1);position:relative;cursor:pointer;box-sizing:border-box;box-shadow:inset 0 0 0 1px #0000001a;outline-offset:2px}.show-api-client-button span[data-v-f7468f9c],.show-api-client-button svg[data-v-f7468f9c]{fill:currentColor;color:var(--scalar-button-1-color);z-index:1}.show-api-client-button[data-v-f7468f9c]:hover{background:var(--scalar-button-1-hover)}.show-api-client-button svg[data-v-f7468f9c]{margin-right:4px}.operation-title[data-v-86cb6452]{justify-content:space-between;display:flex}.operation-details[data-v-86cb6452]{flex-shrink:1;align-items:center;gap:9px;min-width:0;margin-top:0;display:flex}.operation-details[data-v-86cb6452] .endpoint-anchor .scalar-button svg{width:16px;height:16px}.endpoint-type[data-v-86cb6452]{z-index:0;width:60px;font-size:var(--scalar-small);text-transform:uppercase;font-weight:var(--scalar-bold);font-family:var(--scalar-font);flex-shrink:0;justify-content:center;align-items:center;gap:6px;padding:6px;display:flex;position:relative}.endpoint-type[data-v-86cb6452]:after{content:\"\";z-index:-1;opacity:.15;border-radius:var(--scalar-radius);background:currentColor;position:absolute;inset:0}.endpoint-anchor[data-v-86cb6452]{flex-shrink:1;align-items:center;min-width:0;display:flex}.endpoint-anchor.label[data-v-86cb6452]{display:flex}.endpoint-label[data-v-86cb6452]{min-width:0;color:var(--scalar-color-1);flex-shrink:1;align-items:baseline;gap:9px;display:flex}.endpoint-label-path[data-v-86cb6452]{font-family:var(--scalar-font-code);font-size:var(--scalar-mini);text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.endpoint-label-path[data-v-86cb6452] em{color:var(--scalar-color-2)}.endpoint-label-name[data-v-86cb6452]{color:var(--scalar-color-2);font-size:var(--scalar-small);text-overflow:ellipsis;white-space:nowrap;flex-shrink:1000000000;overflow:hidden}.endpoint-try-hint[data-v-86cb6452]{flex-shrink:0;padding:2px}.endpoint-copy[data-v-86cb6452]{color:currentColor}.endpoint-copy[data-v-86cb6452] svg{stroke-width:2px}.endpoint-content[data-v-86cb6452]{grid-auto-columns:1fr;grid-auto-flow:row;gap:9px;padding:9px;display:grid}@media (min-width:1000px){.endpoint-content[data-v-86cb6452]{grid-auto-flow:column}}@container (max-width:900px){.endpoint-content[data-v-86cb6452]{grid-template-columns:1fr}}.endpoint-content[data-v-86cb6452]>*{min-width:0}.operation-details-card[data-v-86cb6452]{flex-direction:column;gap:12px;min-width:0;display:flex}:is(.operation-details-card-item[data-v-86cb6452] .parameter-list,.operation-details-card-item[data-v-86cb6452] .callbacks-list){border:var(--scalar-border-width)solid var(--scalar-border-color);border-radius:var(--scalar-radius-lg);margin-top:0}.operation-details-card-item[data-v-86cb6452]{flex-direction:column;gap:12px;display:flex}.operation-details-card-item[data-v-86cb6452] .parameter-list-items{margin-bottom:0}.operation-details-card[data-v-86cb6452] .parameter-item:last-of-type .parameter-schema{padding-bottom:12px}.operation-details-card[data-v-86cb6452] .parameter-list .parameter-list{margin-bottom:12px}.operation-details-card[data-v-86cb6452] .parameter-item{margin:0;padding:0}.operation-details-card[data-v-86cb6452] .property{margin:0;padding:9px}:is(.operation-details-card[data-v-86cb6452] .parameter-list-title,.operation-details-card[data-v-86cb6452] .request-body-title,.operation-details-card[data-v-86cb6452] .callbacks-title){text-transform:uppercase;font-weight:var(--scalar-bold);font-size:var(--scalar-mini);color:var(--scalar-color-2);margin:0;padding:9px;line-height:1.33}.operation-details-card[data-v-86cb6452] .callback-list-item-title{padding-left:28px;padding-right:12px}.operation-details-card[data-v-86cb6452] .callback-list-item-icon{left:6px}.operation-details-card[data-v-86cb6452] .callback-operation-container{padding-inline:9px;padding-bottom:9px}:is(.operation-details-card[data-v-86cb6452] .callback-operation-container>.request-body,.operation-details-card[data-v-86cb6452] .callback-operation-container>.parameter-list){border:none}.operation-details-card[data-v-86cb6452] .callback-operation-container>.request-body>.request-body-header{border-bottom:var(--scalar-border-width)solid var(--scalar-border-color);padding:0 0 9px}.operation-details-card[data-v-86cb6452] .request-body-description{border-top:var(--scalar-border-width)solid var(--scalar-border-color);margin-top:0;padding:9px 9px 0}.operation-details-card[data-v-86cb6452] .request-body{border-radius:var(--scalar-radius-lg);border:var(--scalar-border-width)solid var(--scalar-border-color);margin-top:0}.operation-details-card[data-v-86cb6452] .request-body-header{border-bottom:0;padding-bottom:0}.operation-details-card[data-v-86cb6452] .contents button{margin-right:9px}.operation-details-card[data-v-86cb6452] .schema-card--open+.schema-card:not(.schema-card--open){margin-inline:9px;margin-bottom:9px}.operation-details-card[data-v-86cb6452] .request-body-schema .property--level-0{padding:0}.operation-details-card[data-v-86cb6452] .selected-content-type{margin-right:9px}.operation-example-card[data-v-86cb6452]{top:calc(var(--refs-viewport-offset) + 24px);max-height:calc(var(--refs-viewport-height) - 48px);position:sticky}@media (max-width:600px){.operation-example-card[data-v-86cb6452]{max-height:unset;position:static}}.examples[data-v-21dac38e]{position:sticky;top:calc(var(--refs-viewport-offset) + 24px)}.examples[data-v-21dac38e]>*{max-height:calc((var(--refs-viewport-height) - 60px) / 2);position:relative}@media (max-width: 600px){.examples[data-v-21dac38e]>*{max-height:unset}}.deprecated[data-v-21dac38e] *{text-decoration:line-through}.section-flare[data-v-2a9c8c02]{top:0;right:0;pointer-events:none}.narrow-references-container{container-name:narrow-references-container;container-type:inline-size}.ref-search-meta[data-v-c1c368f9]{background:var(--scalar-background-1);border-bottom-left-radius:var(--scalar-radius-lg);border-bottom-right-radius:var(--scalar-radius-lg);padding:6px 12px;font-size:var(--scalar-font-size-4);color:var(--scalar-color-3);font-weight:var(--scalar-semibold);display:flex;gap:12px;border-top:var(--scalar-border-width) solid var(--scalar-border-color)}/*! tailwindcss v4.1.8 | MIT License | https://tailwindcss.com */@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-divide-y-reverse:0;--tw-border-style:solid;--tw-gradient-position:initial;--tw-gradient-from:#0000;--tw-gradient-via:#0000;--tw-gradient-to:#0000;--tw-gradient-stops:initial;--tw-gradient-via-stops:initial;--tw-gradient-from-position:0%;--tw-gradient-via-position:50%;--tw-gradient-to-position:100%;--tw-leading:initial;--tw-font-weight:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-outline-style:solid;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-duration:initial;--tw-ease:initial;--tw-scale-x:1;--tw-scale-y:1;--tw-scale-z:1;--tw-space-x-reverse:0;--tw-content:\"\"}}}@layer scalar-base{@supports (color:color-mix(in lab,red,red)){.light-mode{--scalar-color-alert:color-mix(in srgb,var(--scalar-color-orange),var(--scalar-color-1)20%)}}@supports (color:color-mix(in lab,red,red)){.light-mode{--scalar-color-danger:color-mix(in srgb,var(--scalar-color-red),var(--scalar-color-1)20%)}}@supports (color:color-mix(in lab,red,red)){.light-mode{--scalar-background-alert:color-mix(in srgb,var(--scalar-color-orange),var(--scalar-background-1)95%)}}@supports (color:color-mix(in lab,red,red)){.light-mode{--scalar-background-danger:color-mix(in srgb,var(--scalar-color-red),var(--scalar-background-1)95%)}}@supports (color:color-mix(in lab,red,red)){.dark-mode{--scalar-tooltip-background:color-mix(in srgb,var(--scalar-background-1),#fff 10%)}}@supports (color:color-mix(in lab,red,red)){.dark-mode{--scalar-color-danger:color-mix(in srgb,var(--scalar-color-red),var(--scalar-background-1)20%)}}@supports (color:color-mix(in lab,red,red)){.dark-mode{--scalar-background-alert:color-mix(in srgb,var(--scalar-color-orange),var(--scalar-background-1)95%)}}@supports (color:color-mix(in lab,red,red)){.dark-mode{--scalar-background-danger:color-mix(in srgb,var(--scalar-color-red),var(--scalar-background-1)95%)}}:root,:host{--leading-snug:1.375;--leading-normal:1.5;--leading-relaxed:1.625;--ease-in-out:cubic-bezier(.4,0,.2,1);--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4,0,.2,1)}@supports (color:color-mix(in lab,red,red)){.light-mode{--scalar-color-alert:var(--scalar-color-orange)}@supports (color:color-mix(in lab,red,red)){.light-mode{--scalar-color-alert:color-mix(in srgb,var(--scalar-color-orange),var(--scalar-color-1)20%)}}.light-mode{--scalar-color-danger:var(--scalar-color-red)}@supports (color:color-mix(in lab,red,red)){.light-mode{--scalar-color-danger:color-mix(in srgb,var(--scalar-color-red),var(--scalar-color-1)20%)}}.light-mode{--scalar-background-alert:var(--scalar-color-orange)}@supports (color:color-mix(in lab,red,red)){.light-mode{--scalar-background-alert:color-mix(in srgb,var(--scalar-color-orange),var(--scalar-background-1)95%)}}.light-mode{--scalar-background-danger:var(--scalar-color-red)}@supports (color:color-mix(in lab,red,red)){.light-mode{--scalar-background-danger:color-mix(in srgb,var(--scalar-color-red),var(--scalar-background-1)95%)}}.dark-mode{--scalar-tooltip-background:var(--scalar-background-1)}@supports (color:color-mix(in lab,red,red)){.dark-mode{--scalar-tooltip-background:color-mix(in srgb,var(--scalar-background-1),#fff 10%)}}.dark-mode{--scalar-color-danger:var(--scalar-color-red)}@supports (color:color-mix(in lab,red,red)){.dark-mode{--scalar-color-danger:color-mix(in srgb,var(--scalar-color-red),var(--scalar-background-1)20%)}}.dark-mode{--scalar-background-alert:var(--scalar-color-orange)}@supports (color:color-mix(in lab,red,red)){.dark-mode{--scalar-background-alert:color-mix(in srgb,var(--scalar-color-orange),var(--scalar-background-1)95%)}}.dark-mode{--scalar-background-danger:var(--scalar-color-red)}@supports (color:color-mix(in lab,red,red)){.dark-mode{--scalar-background-danger:color-mix(in srgb,var(--scalar-color-red),var(--scalar-background-1)95%)}}}@supports (color:color-mix(in lab,red,red)){.light-mode{--scalar-color-alert:var(--scalar-color-orange)}@supports (color:color-mix(in lab,red,red)){.light-mode{--scalar-color-alert:var(--scalar-color-orange)}@supports (color:color-mix(in lab,red,red)){.light-mode{--scalar-color-alert:color-mix(in srgb,var(--scalar-color-orange),var(--scalar-color-1)20%)}}}.light-mode{--scalar-color-danger:var(--scalar-color-red)}@supports (color:color-mix(in lab,red,red)){.light-mode{--scalar-color-danger:var(--scalar-color-red)}@supports (color:color-mix(in lab,red,red)){.light-mode{--scalar-color-danger:color-mix(in srgb,var(--scalar-color-red),var(--scalar-color-1)20%)}}}.light-mode{--scalar-background-alert:var(--scalar-color-orange)}@supports (color:color-mix(in lab,red,red)){.light-mode{--scalar-background-alert:var(--scalar-color-orange)}@supports (color:color-mix(in lab,red,red)){.light-mode{--scalar-background-alert:color-mix(in srgb,var(--scalar-color-orange),var(--scalar-background-1)95%)}}}.light-mode{--scalar-background-danger:var(--scalar-color-red)}@supports (color:color-mix(in lab,red,red)){.light-mode{--scalar-background-danger:var(--scalar-color-red)}@supports (color:color-mix(in lab,red,red)){.light-mode{--scalar-background-danger:color-mix(in srgb,var(--scalar-color-red),var(--scalar-background-1)95%)}}}.dark-mode{--scalar-tooltip-background:var(--scalar-background-1)}@supports (color:color-mix(in lab,red,red)){.dark-mode{--scalar-tooltip-background:var(--scalar-background-1)}@supports (color:color-mix(in lab,red,red)){.dark-mode{--scalar-tooltip-background:color-mix(in srgb,var(--scalar-background-1),#fff 10%)}}}.dark-mode{--scalar-color-danger:var(--scalar-color-red)}@supports (color:color-mix(in lab,red,red)){.dark-mode{--scalar-color-danger:var(--scalar-color-red)}@supports (color:color-mix(in lab,red,red)){.dark-mode{--scalar-color-danger:color-mix(in srgb,var(--scalar-color-red),var(--scalar-background-1)20%)}}}.dark-mode{--scalar-background-alert:var(--scalar-color-orange)}@supports (color:color-mix(in lab,red,red)){.dark-mode{--scalar-background-alert:var(--scalar-color-orange)}@supports (color:color-mix(in lab,red,red)){.dark-mode{--scalar-background-alert:color-mix(in srgb,var(--scalar-color-orange),var(--scalar-background-1)95%)}}}.dark-mode{--scalar-background-danger:var(--scalar-color-red)}@supports (color:color-mix(in lab,red,red)){.dark-mode{--scalar-background-danger:var(--scalar-color-red)}@supports (color:color-mix(in lab,red,red)){.dark-mode{--scalar-background-danger:color-mix(in srgb,var(--scalar-color-red),var(--scalar-background-1)95%)}}}}:root,:host{--leading-snug:1.375;--ease-in-out:cubic-bezier(.4,0,.2,1);--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4,0,.2,1)}@supports (color:color-mix(in lab,red,red)){@supports (color:color-mix(in lab,red,red)){.light-mode{--scalar-color-alert:color-mix(in srgb,var(--scalar-color-orange),var(--scalar-color-1)20%)}}@supports (color:color-mix(in lab,red,red)){.light-mode{--scalar-color-danger:color-mix(in srgb,var(--scalar-color-red),var(--scalar-color-1)20%)}}@supports (color:color-mix(in lab,red,red)){.light-mode{--scalar-background-alert:color-mix(in srgb,var(--scalar-color-orange),var(--scalar-background-1)95%)}}@supports (color:color-mix(in lab,red,red)){.light-mode{--scalar-background-danger:color-mix(in srgb,var(--scalar-color-red),var(--scalar-background-1)95%)}}@supports (color:color-mix(in lab,red,red)){.dark-mode{--scalar-tooltip-background:color-mix(in srgb,var(--scalar-background-1),#fff 10%)}}@supports (color:color-mix(in lab,red,red)){.dark-mode{--scalar-color-danger:color-mix(in srgb,var(--scalar-color-red),var(--scalar-background-1)20%)}}@supports (color:color-mix(in lab,red,red)){.dark-mode{--scalar-background-alert:color-mix(in srgb,var(--scalar-color-orange),var(--scalar-background-1)95%)}}@supports (color:color-mix(in lab,red,red)){.dark-mode{--scalar-background-danger:color-mix(in srgb,var(--scalar-color-red),var(--scalar-background-1)95%)}}.light-mode{--scalar-color-alert:var(--scalar-color-orange)}@supports (color:color-mix(in lab,red,red)){.light-mode{--scalar-color-alert:var(--scalar-color-orange)}@supports (color:color-mix(in lab,red,red)){.light-mode{--scalar-color-alert:color-mix(in srgb,var(--scalar-color-orange),var(--scalar-color-1)20%)}}}.light-mode{--scalar-color-danger:var(--scalar-color-red)}@supports (color:color-mix(in lab,red,red)){.light-mode{--scalar-color-danger:var(--scalar-color-red)}@supports (color:color-mix(in lab,red,red)){.light-mode{--scalar-color-danger:color-mix(in srgb,var(--scalar-color-red),var(--scalar-color-1)20%)}}}.light-mode{--scalar-background-alert:var(--scalar-color-orange)}@supports (color:color-mix(in lab,red,red)){.light-mode{--scalar-background-alert:var(--scalar-color-orange)}@supports (color:color-mix(in lab,red,red)){.light-mode{--scalar-background-alert:color-mix(in srgb,var(--scalar-color-orange),var(--scalar-background-1)95%)}}}.light-mode{--scalar-background-danger:var(--scalar-color-red)}@supports (color:color-mix(in lab,red,red)){.light-mode{--scalar-background-danger:var(--scalar-color-red)}@supports (color:color-mix(in lab,red,red)){.light-mode{--scalar-background-danger:color-mix(in srgb,var(--scalar-color-red),var(--scalar-background-1)95%)}}}.dark-mode{--scalar-tooltip-background:var(--scalar-background-1)}@supports (color:color-mix(in lab,red,red)){.dark-mode{--scalar-tooltip-background:var(--scalar-background-1)}@supports (color:color-mix(in lab,red,red)){.dark-mode{--scalar-tooltip-background:color-mix(in srgb,var(--scalar-background-1),#fff 10%)}}}.dark-mode{--scalar-color-danger:var(--scalar-color-red)}@supports (color:color-mix(in lab,red,red)){.dark-mode{--scalar-color-danger:var(--scalar-color-red)}@supports (color:color-mix(in lab,red,red)){.dark-mode{--scalar-color-danger:color-mix(in srgb,var(--scalar-color-red),var(--scalar-background-1)20%)}}}.dark-mode{--scalar-background-alert:var(--scalar-color-orange)}@supports (color:color-mix(in lab,red,red)){.dark-mode{--scalar-background-alert:var(--scalar-color-orange)}@supports (color:color-mix(in lab,red,red)){.dark-mode{--scalar-background-alert:color-mix(in srgb,var(--scalar-color-orange),var(--scalar-background-1)95%)}}}.dark-mode{--scalar-background-danger:var(--scalar-color-red)}@supports (color:color-mix(in lab,red,red)){.dark-mode{--scalar-background-danger:var(--scalar-color-red)}@supports (color:color-mix(in lab,red,red)){.dark-mode{--scalar-background-danger:color-mix(in srgb,var(--scalar-color-red),var(--scalar-background-1)95%)}}}}body{line-height:inherit;margin:0}:root{--scalar-border-width:.5px;--scalar-radius:3px;--scalar-radius-lg:6px;--scalar-radius-xl:8px;--scalar-font:\"Inter\",-apple-system,BlinkMacSystemFont,\"Segoe UI\",Roboto,Oxygen,Ubuntu,Cantarell,\"Open Sans\",\"Helvetica Neue\",sans-serif;--scalar-font-code:\"JetBrains Mono\",ui-monospace,Menlo,Monaco,\"Cascadia Mono\",\"Segoe UI Mono\",\"Roboto Mono\",\"Oxygen Mono\",\"Ubuntu Monospace\",\"Source Code Pro\",\"Fira Mono\",\"Droid Sans Mono\",\"Courier New\",monospace;--scalar-heading-1:24px;--scalar-page-description:16px;--scalar-heading-2:20px;--scalar-heading-3:16px;--scalar-heading-4:16px;--scalar-heading-5:16px;--scalar-heading-6:16px;--scalar-paragraph:16px;--scalar-small:14px;--scalar-mini:13px;--scalar-micro:12px;--scalar-bold:600;--scalar-semibold:500;--scalar-regular:400;--scalar-font-size-1:21px;--scalar-font-size-2:16px;--scalar-font-size-3:14px;--scalar-font-size-4:13px;--scalar-font-size-5:12px;--scalar-font-size-6:12px;--scalar-font-size-7:10px;--scalar-line-height-1:32px;--scalar-line-height-2:24px;--scalar-line-height-3:20px;--scalar-line-height-4:18px;--scalar-line-height-5:16px;--scalar-font-normal:400;--scalar-font-medium:500;--scalar-font-bold:700;--scalar-text-decoration:none;--scalar-text-decoration-hover:underline;--scalar-link-font-weight:inherit;--scalar-sidebar-indent:20px}.dark-mode{color-scheme:dark;--scalar-scrollbar-color:#ffffff2e;--scalar-scrollbar-color-active:#ffffff5c;--scalar-button-1:#fff;--scalar-button-1-hover:#ffffffe6;--scalar-button-1-color:black;--scalar-shadow-1:0 1px 3px 0 #0000001a;--scalar-shadow-2:0 0 0 .5px var(--scalar-border-color),#0f0f0f33 0px 3px 6px,#0f0f0f66 0px 9px 24px;--scalar-lifted-brightness:1.45;--scalar-backdrop-brightness:.5;--scalar-text-decoration-color:currentColor;--scalar-text-decoration-color-hover:currentColor}.light-mode{color-scheme:light;--scalar-scrollbar-color-active:#0000005c;--scalar-scrollbar-color:#0000002e;--scalar-button-1:#000;--scalar-button-1-hover:#000c;--scalar-button-1-color:#ffffffe6;--scalar-shadow-1:0 1px 3px 0 #0000001c;--scalar-shadow-2:#00000014 0px 13px 20px 0px,#00000014 0px 3px 8px 0px,#eeeeed 0px 0 0 .5px;--scalar-lifted-brightness:1;--scalar-backdrop-brightness:1;--scalar-text-decoration-color:currentColor;--scalar-text-decoration-color-hover:currentColor}.light-mode .dark-mode{color-scheme:dark!important}@media (max-width:460px){:root{--scalar-font-size-1:22px;--scalar-font-size-2:14px;--scalar-font-size-3:12px}}@media (max-width:720px){:root{--scalar-heading-1:24px;--scalar-page-description:20px}}:root{--scalar-text-decoration:underline;--scalar-text-decoration-hover:underline}.light-mode{--scalar-background-1:#fff;--scalar-background-2:#f6f6f6;--scalar-background-3:#e7e7e7;--scalar-background-accent:#8ab4f81f;--scalar-color-1:#1b1b1b;--scalar-color-2:#757575;--scalar-color-3:#8e8e8e;--scalar-color-accent:#09f;--scalar-border-color:#dfdfdf}.dark-mode{--scalar-background-1:#0f0f0f;--scalar-background-2:#1a1a1a;--scalar-background-3:#272727;--scalar-color-1:#e7e7e7;--scalar-color-2:#a4a4a4;--scalar-color-3:#797979;--scalar-color-accent:#00aeff;--scalar-background-accent:#3ea6ff1f;--scalar-border-color:#2d2d2d}.light-mode .t-doc__sidebar,.dark-mode .t-doc__sidebar{--scalar-sidebar-background-1:var(--scalar-background-1);--scalar-sidebar-color-1:var(--scalar-color-1);--scalar-sidebar-color-2:var(--scalar-color-2);--scalar-sidebar-border-color:var(--scalar-border-color);--scalar-sidebar-item-hover-background:var(--scalar-background-2);--scalar-sidebar-item-hover-color:currentColor;--scalar-sidebar-item-active-background:var(--scalar-background-2);--scalar-sidebar-color-active:var(--scalar-color-1);--scalar-sidebar-indent-border:var(--scalar-sidebar-border-color);--scalar-sidebar-indent-border-hover:var(--scalar-sidebar-border-color);--scalar-sidebar-indent-border-active:var(--scalar-sidebar-border-color);--scalar-sidebar-search-background:transparent;--scalar-sidebar-search-color:var(--scalar-color-3);--scalar-sidebar-search-border-color:var(--scalar-border-color)}.light-mode{--scalar-color-green:#069061;--scalar-color-red:#ef0006;--scalar-color-yellow:#edbe20;--scalar-color-blue:#0082d0;--scalar-color-orange:#ff5800;--scalar-color-purple:#5203d1;--scalar-link-color:var(--scalar-color-1);--scalar-link-color-hover:var(--scalar-link-color);--scalar-button-1:#000;--scalar-button-1-hover:#000c;--scalar-button-1-color:#ffffffe6;--scalar-tooltip-background:#1a1a1ae6;--scalar-tooltip-color:#ffffffd9;--scalar-color-alert:var(--scalar-color-orange)}@supports (color:color-mix(in lab,red,red)){.light-mode{--scalar-color-alert:var(--scalar-color-orange)}@supports (color:color-mix(in lab,red,red)){.light-mode{--scalar-color-alert:var(--scalar-color-orange)}@supports (color:color-mix(in lab,red,red)){.light-mode{--scalar-color-alert:var(--scalar-color-orange)}@supports (color:color-mix(in lab,red,red)){.light-mode{--scalar-color-alert:color-mix(in srgb,var(--scalar-color-orange),var(--scalar-color-1)20%)}}}}}.light-mode{--scalar-color-danger:var(--scalar-color-red)}@supports (color:color-mix(in lab,red,red)){.light-mode{--scalar-color-danger:var(--scalar-color-red)}@supports (color:color-mix(in lab,red,red)){.light-mode{--scalar-color-danger:var(--scalar-color-red)}@supports (color:color-mix(in lab,red,red)){.light-mode{--scalar-color-danger:var(--scalar-color-red)}@supports (color:color-mix(in lab,red,red)){.light-mode{--scalar-color-danger:color-mix(in srgb,var(--scalar-color-red),var(--scalar-color-1)20%)}}}}}.light-mode{--scalar-background-alert:var(--scalar-color-orange)}@supports (color:color-mix(in lab,red,red)){.light-mode{--scalar-background-alert:var(--scalar-color-orange)}@supports (color:color-mix(in lab,red,red)){.light-mode{--scalar-background-alert:var(--scalar-color-orange)}@supports (color:color-mix(in lab,red,red)){.light-mode{--scalar-background-alert:var(--scalar-color-orange)}@supports (color:color-mix(in lab,red,red)){.light-mode{--scalar-background-alert:color-mix(in srgb,var(--scalar-color-orange),var(--scalar-background-1)95%)}}}}}.light-mode{--scalar-background-danger:var(--scalar-color-red)}@supports (color:color-mix(in lab,red,red)){.light-mode{--scalar-background-danger:var(--scalar-color-red)}@supports (color:color-mix(in lab,red,red)){.light-mode{--scalar-background-danger:var(--scalar-color-red)}@supports (color:color-mix(in lab,red,red)){.light-mode{--scalar-background-danger:var(--scalar-color-red)}@supports (color:color-mix(in lab,red,red)){.light-mode{--scalar-background-danger:color-mix(in srgb,var(--scalar-color-red),var(--scalar-background-1)95%)}}}}}.dark-mode{--scalar-color-green:#00b648;--scalar-color-red:#dc1b19;--scalar-color-yellow:#ffc90d;--scalar-color-blue:#4eb3ec;--scalar-color-orange:#ff8d4d;--scalar-color-purple:#b191f9;--scalar-link-color:var(--scalar-color-1);--scalar-link-color-hover:var(--scalar-link-color);--scalar-button-1:#fff;--scalar-button-1-hover:#ffffffe6;--scalar-button-1-color:black;--scalar-tooltip-background:var(--scalar-background-1)}@supports (color:color-mix(in lab,red,red)){.dark-mode{--scalar-tooltip-background:var(--scalar-background-1)}@supports (color:color-mix(in lab,red,red)){.dark-mode{--scalar-tooltip-background:var(--scalar-background-1)}@supports (color:color-mix(in lab,red,red)){.dark-mode{--scalar-tooltip-background:var(--scalar-background-1)}@supports (color:color-mix(in lab,red,red)){.dark-mode{--scalar-tooltip-background:color-mix(in srgb,var(--scalar-background-1),#fff 10%)}}}}}.dark-mode{--scalar-tooltip-color:#fffffff2;--scalar-color-danger:var(--scalar-color-red)}@supports (color:color-mix(in lab,red,red)){.dark-mode{--scalar-color-danger:var(--scalar-color-red)}@supports (color:color-mix(in lab,red,red)){.dark-mode{--scalar-color-danger:var(--scalar-color-red)}@supports (color:color-mix(in lab,red,red)){.dark-mode{--scalar-color-danger:var(--scalar-color-red)}@supports (color:color-mix(in lab,red,red)){.dark-mode{--scalar-color-danger:color-mix(in srgb,var(--scalar-color-red),var(--scalar-background-1)20%)}}}}}.dark-mode{--scalar-background-alert:var(--scalar-color-orange)}@supports (color:color-mix(in lab,red,red)){.dark-mode{--scalar-background-alert:var(--scalar-color-orange)}@supports (color:color-mix(in lab,red,red)){.dark-mode{--scalar-background-alert:var(--scalar-color-orange)}@supports (color:color-mix(in lab,red,red)){.dark-mode{--scalar-background-alert:var(--scalar-color-orange)}@supports (color:color-mix(in lab,red,red)){.dark-mode{--scalar-background-alert:color-mix(in srgb,var(--scalar-color-orange),var(--scalar-background-1)95%)}}}}}.dark-mode{--scalar-background-danger:var(--scalar-color-red)}@supports (color:color-mix(in lab,red,red)){.dark-mode{--scalar-background-danger:var(--scalar-color-red)}@supports (color:color-mix(in lab,red,red)){.dark-mode{--scalar-background-danger:var(--scalar-color-red)}@supports (color:color-mix(in lab,red,red)){.dark-mode{--scalar-background-danger:var(--scalar-color-red)}@supports (color:color-mix(in lab,red,red)){.dark-mode{--scalar-background-danger:color-mix(in srgb,var(--scalar-color-red),var(--scalar-background-1)95%)}}}}}@supports (color:color(display-p3 1 1 1)){.light-mode{--scalar-color-accent:color(display-p3 0 .6 1);--scalar-color-green:color(display-p3 .023529 .564706 .380392);--scalar-color-red:color(display-p3 .937255 0 .023529);--scalar-color-yellow:color(display-p3 .929412 .745098 .12549);--scalar-color-blue:color(display-p3 0 .509804 .815686);--scalar-color-orange:color(display-p3 1 .4 .02);--scalar-color-purple:color(display-p3 .321569 .011765 .819608)}.dark-mode{--scalar-color-accent:color(display-p3 .07 .67 1);--scalar-color-green:color(display-p3 0 .713725 .282353);--scalar-color-red:color(display-p3 .862745 .105882 .098039);--scalar-color-yellow:color(display-p3 1 .788235 .05098);--scalar-color-blue:color(display-p3 .305882 .701961 .92549);--scalar-color-orange:color(display-p3 1 .552941 .301961);--scalar-color-purple:color(display-p3 .694118 .568627 .976471)}}:root,:host{--leading-snug:1.375;--ease-in-out:cubic-bezier(.4,0,.2,1);--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4,0,.2,1);--leading-normal:1.5}body{background-color:var(--scalar-background-1);margin:0}}@layer scalar-theme;.scalar-app .\\\\@container{container-type:inline-size}.scalar-app .-top-2{top:-8px}.scalar-app .top-3\\\\.5{top:14px}.scalar-app .-left-4\\\\.5{left:-18px}.scalar-app .-left-5{left:-20px}.scalar-app .z-1000{z-index:1000}.scalar-app .order-789{order:789}.scalar-app .-m-1{margin:-4px}.scalar-app .-m-2{margin:-8px}.scalar-app .-mx-2{margin-inline:-8px}.scalar-app .my-2{margin-block:8px}.scalar-app .my-3{margin-block:12px}.scalar-app .-mt-1{margin-top:-4px}.scalar-app .mt-6{margin-top:24px}.scalar-app .mb-3{margin-bottom:12px}.scalar-app .size-4\\\\.5{width:18px;height:18px}.scalar-app .h-\\\\[calc\\\\(100\\\\%\\\\+16px\\\\)\\\\]{height:calc(100% + 16px)}.scalar-app .h-\\\\[var\\\\(--scalar-header-height\\\\)\\\\]{height:var(--scalar-header-height)}.scalar-app .max-h-\\\\[60vh\\\\]{max-height:60vh}.scalar-app .min-h-3{min-height:12px}.scalar-app .min-h-7{min-height:28px}.scalar-app .min-h-dvh{min-height:100dvh}.scalar-app .w-0{width:0}.scalar-app .w-4\\\\.5{width:18px}.scalar-app .w-96{width:384px}.scalar-app .w-110{width:440px}.scalar-app .w-120{width:480px}.scalar-app .max-w-\\\\(--refs-content-max-width\\\\){max-width:var(--refs-content-max-width)}.scalar-app .max-w-64{max-width:256px}.scalar-app .min-w-3{min-width:12px}.scalar-app .min-w-7{min-width:28px}.scalar-app .rotate-45{rotate:45deg}.scalar-app .scroll-mt-16{scroll-margin-top:64px}.scalar-app .scroll-mt-24{scroll-margin-top:96px}.scalar-app .list-none{list-style-type:none}.scalar-app .content-end{align-content:flex-end}.scalar-app .gap-7{gap:28px}.scalar-app .overflow-x-scroll{overflow-x:scroll}.scalar-app .rounded-b-none{border-bottom-right-radius:0;border-bottom-left-radius:0}@supports (color:color-mix(in lab,red,red)){.scalar-app .bg-b-1\\\\.5{background-color:color-mix(in srgb,var(--scalar-background-1),var(--scalar-background-2))}}.scalar-app .p-7{padding:28px}.scalar-app .px-15{padding-inline:60px}.scalar-app .py-2\\\\.25{padding-block:9px}.scalar-app .pt-1\\\\.5{padding-top:6px}.scalar-app .pb-12{padding-bottom:48px}.scalar-app .leading-\\\\[1\\\\.45\\\\]{--tw-leading:1.45;line-height:1.45}.scalar-app .leading-relaxed{--tw-leading:var(--leading-relaxed);line-height:var(--leading-relaxed)}.scalar-app .text-current{color:currentColor}.scalar-app .italic{font-style:italic}.scalar-app .line-through{text-decoration-line:line-through}.scalar-app .\\\\[--scalar-address-bar-height\\\\:0px\\\\]{--scalar-address-bar-height:0px}.scalar-app .\\\\[grid-area\\\\:header\\\\]{grid-area:header}.scalar-app .\\\\[grid-area\\\\:navigation\\\\]{grid-area:navigation}:is(.scalar-app .\\\\*\\\\:\\\\!p-0>*){padding:0!important}.scalar-app .group-last\\\\:mr-0:is(:where(.group):last-child *){margin-right:0}.scalar-app .group-open\\\\:rotate-90:is(:where(.group):is([open],:popover-open,:open) *){rotate:90deg}.scalar-app .group-open\\\\:flex-wrap:is(:where(.group):is([open],:popover-open,:open) *){flex-wrap:wrap}.scalar-app .group-open\\\\:whitespace-normal:is(:where(.group):is([open],:popover-open,:open) *){white-space:normal}.scalar-app .group-focus-within\\\\/parameter-item\\\\:w-auto:is(:where(.group\\\\/parameter-item):focus-within *){width:auto}@media (hover:hover){.scalar-app .group-hover\\\\:flex:is(:where(.group):hover *){display:flex}.scalar-app .group-hover\\\\:text-c-1:is(:where(.group):hover *){color:var(--scalar-color-1)}.scalar-app .group-hover\\\\:opacity-100:is(:where(.group):hover *){opacity:1}.scalar-app .group-hover\\\\/auth\\\\:absolute:is(:where(.group\\\\/auth):hover *){position:absolute}.scalar-app .group-hover\\\\/auth\\\\:h-auto:is(:where(.group\\\\/auth):hover *){height:auto}.scalar-app .group-hover\\\\/auth\\\\:border-b:is(:where(.group\\\\/auth):hover *){border-bottom-style:var(--tw-border-style);border-bottom-width:var(--scalar-border-width)}.scalar-app .group-hover\\\\/heading\\\\:opacity-100:is(:where(.group\\\\/heading):hover *),.scalar-app .group-hover\\\\/item\\\\:opacity-100:is(:where(.group\\\\/item):hover *){opacity:1}.scalar-app .group-hover\\\\/parameter-item\\\\:w-auto:is(:where(.group\\\\/parameter-item):hover *){width:auto}.scalar-app .group-hover\\\\/scopes-accordion\\\\:text-c-2:is(:where(.group\\\\/scopes-accordion):hover *){color:var(--scalar-color-2)}}.scalar-app .group-has-focus-visible\\\\/heading\\\\:opacity-100:is(:where(.group\\\\/heading):has(:focus-visible) *){opacity:1}.scalar-app .group-aria-expanded\\\\/combobox-button\\\\:rotate-180:is(:where(.group\\\\/combobox-button)[aria-expanded=true] *){rotate:180deg}.scalar-app .empty\\\\:hidden:empty{display:none}@media (hover:hover){.scalar-app .hover\\\\:bg-b-2:hover{background-color:var(--scalar-background-2)}.scalar-app .hover\\\\:bg-b-3:hover{background-color:var(--scalar-background-3)}.scalar-app .hover\\\\:whitespace-normal:hover{white-space:normal}.scalar-app .hover\\\\:text-c-1:hover{color:var(--scalar-color-1)}}.scalar-app .has-focus\\\\:outline:has(:focus){outline-style:var(--tw-outline-style);outline-width:1px}@media (min-width:1200px){.scalar-app .xl\\\\:mb-1\\\\.5{margin-bottom:6px}.scalar-app .xl\\\\:gap-12{gap:48px}.scalar-app .xl\\\\:border-r{border-right-style:var(--tw-border-style);border-right-width:var(--scalar-border-width)}.scalar-app .xl\\\\:border-none{--tw-border-style:none;border-style:none}.scalar-app .xl\\\\:first\\\\:ml-auto:first-child{margin-left:auto}}.scalar-app .\\\\[\\\\&_a\\\\]\\\\:underline a{text-decoration-line:underline}.scalar-app .\\\\[\\\\&_a\\\\:hover\\\\]\\\\:text-c-1 a:hover{color:var(--scalar-color-1)}.scalar-app .\\\\[\\\\&_code\\\\]\\\\:font-code code{font-family:var(--scalar-font-code)}.scalar-app .\\\\[\\\\&_em\\\\]\\\\:text-c-1 em{color:var(--scalar-color-1)}.scalar-app .\\\\[\\\\&_em\\\\]\\\\:not-italic em{font-style:normal}@supports (color:color-mix(in lab,red,red)){.scalar-app .bg-b-1\\\\.5{background-color:var(--scalar-background-1)}@supports (color:color-mix(in lab,red,red)){.scalar-app .bg-b-1\\\\.5{background-color:color-mix(in srgb,var(--scalar-background-1),var(--scalar-background-2))}}}@supports (color:color-mix(in lab,red,red)){.scalar-app .to-b-1\\\\.5{--tw-gradient-to:var(--scalar-background-1)}@supports (color:color-mix(in lab,red,red)){.scalar-app .to-b-1\\\\.5{--tw-gradient-to:color-mix(in srgb,var(--scalar-background-1),var(--scalar-background-2))}}:is(.scalar-app .\\\\*\\\\:border-border-tooltip>*){border-color:var(--scalar-tooltip-color)}@supports (color:color-mix(in lab,red,red)){:is(.scalar-app .\\\\*\\\\:border-border-tooltip>*){border-color:color-mix(in srgb,var(--scalar-tooltip-color),var(--scalar-tooltip-background))}}}@property --tw-divide-y-reverse{syntax:\"*\";inherits:false;initial-value:0}@property --tw-gradient-position{syntax:\"*\";inherits:false}@property --tw-gradient-from{syntax:\"\";inherits:false;initial-value:#0000}@property --tw-gradient-via{syntax:\"\";inherits:false;initial-value:#0000}@property --tw-gradient-to{syntax:\"\";inherits:false;initial-value:#0000}@property --tw-gradient-stops{syntax:\"*\";inherits:false}@property --tw-gradient-via-stops{syntax:\"*\";inherits:false}@property --tw-gradient-from-position{syntax:\"\";inherits:false;initial-value:0%}@property --tw-gradient-via-position{syntax:\"\";inherits:false;initial-value:50%}@property --tw-gradient-to-position{syntax:\"\";inherits:false;initial-value:100%}@property --tw-ease{syntax:\"*\";inherits:false}@keyframes fade-in-f525638b{0%{opacity:0}70%{opacity:0}to{opacity:1}}@keyframes rotate-f525638b{0%{transform:scale(3.5)rotate(0)}to{transform:scale(3.5)rotate(360deg)}}@supports (color:color-mix(in lab,red,red)){.scalar-app .markdown a{-webkit-text-decoration-color:color-mix(in srgb,var(--font-color)30%,transparent);text-decoration-color:color-mix(in srgb,var(--font-color)30%,transparent)}}@supports (color:color-mix(in lab,red,red)){.scalar-app .markdown .markdown-alert{background-color:color-mix(in srgb,var(--scalar-background-2),transparent)}}@supports (color:color-mix(in lab,red,red)){.scalar-app .markdown .markdown-alert.markdown-alert-note{background-color:color-mix(in srgb,var(--scalar-color-blue),transparent 97%)}}@supports (color:color-mix(in lab,red,red)){.scalar-app .markdown .markdown-alert.markdown-alert-note{border:var(--scalar-border-width)solid color-mix(in srgb,var(--scalar-color-blue),transparent 50%)}}@supports (color:color-mix(in lab,red,red)){.scalar-app .markdown .markdown-alert.markdown-alert-tip{background-color:color-mix(in srgb,var(--scalar-color-2),transparent 97%)}}@supports (color:color-mix(in lab,red,red)){.scalar-app .markdown .markdown-alert.markdown-alert-tip{border:var(--scalar-border-width)solid color-mix(in srgb,var(--scalar-color-2),transparent 50%)}}@supports (color:color-mix(in lab,red,red)){.scalar-app .markdown .markdown-alert.markdown-alert-important,.scalar-app .markdown .markdown-alert.markdown-alert-warning{background-color:color-mix(in srgb,var(--scalar-color-orange),transparent 97%)}}@supports (color:color-mix(in lab,red,red)){.scalar-app .markdown .markdown-alert.markdown-alert-important,.scalar-app .markdown .markdown-alert.markdown-alert-warning{border:var(--scalar-border-width)solid color-mix(in srgb,var(--scalar-color-orange),transparent 50%)}}@supports (color:color-mix(in lab,red,red)){.scalar-app .markdown .markdown-alert.markdown-alert-caution{background-color:color-mix(in srgb,var(--scalar-color-red),transparent 97%)}}@supports (color:color-mix(in lab,red,red)){.scalar-app .markdown .markdown-alert.markdown-alert-caution{border:var(--scalar-border-width)solid color-mix(in srgb,var(--scalar-color-red),transparent 50%)}}@supports (color:color-mix(in lab,red,red)){.scalar-app .markdown .markdown-alert.markdown-alert-success{background-color:color-mix(in srgb,var(--scalar-color-green),transparent 97%)}}@supports (color:color-mix(in lab,red,red)){.scalar-app .markdown .markdown-alert.markdown-alert-success{border:var(--scalar-border-width)solid color-mix(in srgb,var(--scalar-color-green),transparent 50%)}}@property --tw-backdrop-blur{syntax:\"*\";inherits:false}@property --tw-backdrop-brightness{syntax:\"*\";inherits:false}@property --tw-backdrop-contrast{syntax:\"*\";inherits:false}@property --tw-backdrop-grayscale{syntax:\"*\";inherits:false}@property --tw-backdrop-hue-rotate{syntax:\"*\";inherits:false}@property --tw-backdrop-invert{syntax:\"*\";inherits:false}@property --tw-backdrop-opacity{syntax:\"*\";inherits:false}@property --tw-backdrop-saturate{syntax:\"*\";inherits:false}@property --tw-backdrop-sepia{syntax:\"*\";inherits:false}@keyframes fadein-layout-1319c63c{0%{opacity:0}to{opacity:1}}@keyframes fadein-modal-1319c63c{0%{opacity:0;transform:translateY(10px)}to{opacity:1;transform:translate(0)}}@supports (color:color-mix(in lab,red,red)){.scalar-app .bg-b-1\\\\.5{background-color:var(--scalar-background-1)}@supports (color:color-mix(in lab,red,red)){.scalar-app .bg-b-1\\\\.5{background-color:var(--scalar-background-1)}@supports (color:color-mix(in lab,red,red)){.scalar-app .bg-b-1\\\\.5{background-color:color-mix(in srgb,var(--scalar-background-1),var(--scalar-background-2))}}}}@supports (color:color-mix(in lab,red,red)){.scalar-app .to-b-1\\\\.5{--tw-gradient-to:var(--scalar-background-1)}@supports (color:color-mix(in lab,red,red)){.scalar-app .to-b-1\\\\.5{--tw-gradient-to:var(--scalar-background-1)}@supports (color:color-mix(in lab,red,red)){.scalar-app .to-b-1\\\\.5{--tw-gradient-to:color-mix(in srgb,var(--scalar-background-1),var(--scalar-background-2))}}}:is(.scalar-app .\\\\*\\\\:border-border-tooltip>*){border-color:var(--scalar-tooltip-color)}@supports (color:color-mix(in lab,red,red)){:is(.scalar-app .\\\\*\\\\:border-border-tooltip>*){border-color:var(--scalar-tooltip-color)}@supports (color:color-mix(in lab,red,red)){:is(.scalar-app .\\\\*\\\\:border-border-tooltip>*){border-color:color-mix(in srgb,var(--scalar-tooltip-color),var(--scalar-tooltip-background))}}}}@supports (color:color-mix(in lab,red,red)){.dragover-above[data-v-a89d6a6e]:after,.dragover-below[data-v-a89d6a6e]:after{background:var(--scalar-color-blue)}@supports (color:color-mix(in lab,red,red)){.dragover-above[data-v-a89d6a6e]:after,.dragover-below[data-v-a89d6a6e]:after{background:color-mix(in srgb,var(--scalar-color-blue),transparent 85%)}}.dragover-asChild[data-v-a89d6a6e]:after{background:var(--scalar-color-blue)}@supports (color:color-mix(in lab,red,red)){.dragover-asChild[data-v-a89d6a6e]:after{background:color-mix(in srgb,var(--scalar-color-blue),transparent 85%)}}.scalar-app .bg-b-1\\\\.5{background-color:var(--scalar-background-1)}@supports (color:color-mix(in lab,red,red)){.scalar-app .bg-b-1\\\\.5{background-color:var(--scalar-background-1)}@supports (color:color-mix(in lab,red,red)){.scalar-app .bg-b-1\\\\.5{background-color:color-mix(in srgb,var(--scalar-background-1),var(--scalar-background-2))}}}.scalar-app .to-b-1\\\\.5{--tw-gradient-to:var(--scalar-background-1)}@supports (color:color-mix(in lab,red,red)){.scalar-app .to-b-1\\\\.5{--tw-gradient-to:var(--scalar-background-1)}@supports (color:color-mix(in lab,red,red)){.scalar-app .to-b-1\\\\.5{--tw-gradient-to:color-mix(in srgb,var(--scalar-background-1),var(--scalar-background-2))}}}:is(.scalar-app .\\\\*\\\\:border-border-tooltip>*){border-color:var(--scalar-tooltip-color)}@supports (color:color-mix(in lab,red,red)){:is(.scalar-app .\\\\*\\\\:border-border-tooltip>*){border-color:var(--scalar-tooltip-color)}@supports (color:color-mix(in lab,red,red)){:is(.scalar-app .\\\\*\\\\:border-border-tooltip>*){border-color:color-mix(in srgb,var(--scalar-tooltip-color),var(--scalar-tooltip-background))}}}}@media (hover:hover){.scalar-app .group-hover\\\\:text-c-1:is(:where(.group):hover *){color:var(--scalar-color-1)}.scalar-app .group-hover\\\\/button\\\\:bg-sidebar-indent-border-hover:is(:where(.group\\\\/button):hover *){background-color:var(--scalar-sidebar-indent-border-hover,var(--scalar-border-color))}.scalar-app .group-hover\\\\/button\\\\:text-c-1:is(:where(.group\\\\/button):hover *){color:var(--scalar-color-1)}.scalar-app .group-hover\\\\/code-block\\\\:opacity-100:is(:where(.group\\\\/code-block):hover *){opacity:1}.scalar-app .hover\\\\:bg-b-2:hover{background-color:var(--scalar-background-2)}.scalar-app .hover\\\\:bg-b-3:hover{background-color:var(--scalar-background-3)}.scalar-app .hover\\\\:bg-h-btn:hover{background-color:var(--scalar-button-1-hover)}.scalar-app .hover\\\\:bg-linear-to-b:hover{--tw-gradient-position:to bottom}@supports (background-image:linear-gradient(in lab,red,red)){.scalar-app .hover\\\\:bg-linear-to-b:hover{--tw-gradient-position:to bottom in oklab}}.scalar-app .hover\\\\:bg-linear-to-b:hover{background-image:linear-gradient(var(--tw-gradient-stops))}.scalar-app .hover\\\\:bg-linear-to-t:hover{--tw-gradient-position:to top}@supports (background-image:linear-gradient(in lab,red,red)){.scalar-app .hover\\\\:bg-linear-to-t:hover{--tw-gradient-position:to top in oklab}}.scalar-app .hover\\\\:bg-linear-to-t:hover{background-image:linear-gradient(var(--tw-gradient-stops))}.scalar-app .hover\\\\:text-c-1:hover{color:var(--scalar-color-1)}.scalar-app .hover\\\\:underline:hover{text-decoration-line:underline}.scalar-app .hover\\\\:brightness-90:hover{--tw-brightness:brightness(90%);filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}}@supports (color:color-mix(in lab,red,red)){.scalar-app .markdown a{-webkit-text-decoration-color:var(--font-color)}@supports (color:color-mix(in lab,red,red)){.scalar-app .markdown a{-webkit-text-decoration-color:color-mix(in srgb,var(--font-color)30%,transparent)}}.scalar-app .markdown a{-webkit-text-decoration-color:var(--font-color);text-decoration-color:var(--font-color)}@supports (color:color-mix(in lab,red,red)){.scalar-app .markdown a{-webkit-text-decoration-color:color-mix(in srgb,var(--font-color)30%,transparent);text-decoration-color:color-mix(in srgb,var(--font-color)30%,transparent)}}.scalar-app .markdown .markdown-alert{background-color:var(--scalar-background-2)}@supports (color:color-mix(in lab,red,red)){.scalar-app .markdown .markdown-alert{background-color:color-mix(in srgb,var(--scalar-background-2),transparent)}}.scalar-app .markdown .markdown-alert.markdown-alert-note{background-color:var(--scalar-color-blue)}@supports (color:color-mix(in lab,red,red)){.scalar-app .markdown .markdown-alert.markdown-alert-note{background-color:color-mix(in srgb,var(--scalar-color-blue),transparent 97%)}}.scalar-app .markdown .markdown-alert.markdown-alert-note{border:var(--scalar-border-width)solid var(--scalar-color-blue)}@supports (color:color-mix(in lab,red,red)){.scalar-app .markdown .markdown-alert.markdown-alert-note{border:var(--scalar-border-width)solid color-mix(in srgb,var(--scalar-color-blue),transparent 50%)}}.scalar-app .markdown .markdown-alert.markdown-alert-tip{background-color:var(--scalar-color-2)}@supports (color:color-mix(in lab,red,red)){.scalar-app .markdown .markdown-alert.markdown-alert-tip{background-color:color-mix(in srgb,var(--scalar-color-2),transparent 97%)}}.scalar-app .markdown .markdown-alert.markdown-alert-tip{border:var(--scalar-border-width)solid var(--scalar-color-2)}@supports (color:color-mix(in lab,red,red)){.scalar-app .markdown .markdown-alert.markdown-alert-tip{border:var(--scalar-border-width)solid color-mix(in srgb,var(--scalar-color-2),transparent 50%)}}.scalar-app .markdown .markdown-alert.markdown-alert-important,.scalar-app .markdown .markdown-alert.markdown-alert-warning{background-color:var(--scalar-color-orange)}@supports (color:color-mix(in lab,red,red)){.scalar-app .markdown .markdown-alert.markdown-alert-important,.scalar-app .markdown .markdown-alert.markdown-alert-warning{background-color:color-mix(in srgb,var(--scalar-color-orange),transparent 97%)}}.scalar-app .markdown .markdown-alert.markdown-alert-important,.scalar-app .markdown .markdown-alert.markdown-alert-warning{border:var(--scalar-border-width)solid var(--scalar-color-orange)}@supports (color:color-mix(in lab,red,red)){.scalar-app .markdown .markdown-alert.markdown-alert-important,.scalar-app .markdown .markdown-alert.markdown-alert-warning{border:var(--scalar-border-width)solid color-mix(in srgb,var(--scalar-color-orange),transparent 50%)}}.scalar-app .markdown .markdown-alert.markdown-alert-caution{background-color:var(--scalar-color-red)}@supports (color:color-mix(in lab,red,red)){.scalar-app .markdown .markdown-alert.markdown-alert-caution{background-color:color-mix(in srgb,var(--scalar-color-red),transparent 97%)}}.scalar-app .markdown .markdown-alert.markdown-alert-caution{border:var(--scalar-border-width)solid var(--scalar-color-red)}@supports (color:color-mix(in lab,red,red)){.scalar-app .markdown .markdown-alert.markdown-alert-caution{border:var(--scalar-border-width)solid color-mix(in srgb,var(--scalar-color-red),transparent 50%)}}.scalar-app .markdown .markdown-alert.markdown-alert-success{background-color:var(--scalar-color-green)}@supports (color:color-mix(in lab,red,red)){.scalar-app .markdown .markdown-alert.markdown-alert-success{background-color:color-mix(in srgb,var(--scalar-color-green),transparent 97%)}}.scalar-app .markdown .markdown-alert.markdown-alert-success{border:var(--scalar-border-width)solid var(--scalar-color-green)}@supports (color:color-mix(in lab,red,red)){.scalar-app .markdown .markdown-alert.markdown-alert-success{border:var(--scalar-border-width)solid color-mix(in srgb,var(--scalar-color-green),transparent 50%)}}}.scalar-app .ml-2{margin-left:8px}.scalar-app .self-start{align-self:flex-start}@media (hover:hover){.scalar-app .group-hover\\\\/group-button\\\\:block:is(:where(.group\\\\/group-button):hover *){display:block}.scalar-app .group-hover\\\\/group-button\\\\:hidden:is(:where(.group\\\\/group-button):hover *){display:none}}:where(.scalar-app){font-family:var(--scalar-font);color:var(--scalar-color-1);-webkit-text-size-adjust:100%;tab-size:4;line-height:1.15}:where(.scalar-app) *,:where(.scalar-app) :before,:where(.scalar-app) :after{box-sizing:border-box;border-style:solid;border-width:0;border-color:var(--scalar-border-color);outline-width:1px;outline-style:none;outline-color:var(--scalar-color-accent);font-feature-settings:inherit;font-variation-settings:inherit;font-family:inherit;font-size:inherit;font-weight:inherit;font-style:inherit;-webkit-text-decoration:inherit;text-decoration:inherit;text-align:inherit;line-height:inherit;color:inherit;margin:unset;padding:unset;text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}:where(.scalar-app) :before,:where(.scalar-app) :after{--tw-content:\"\"}:where(.scalar-app) button,:where(.scalar-app) input,:where(.scalar-app) optgroup,:where(.scalar-app) select,:where(.scalar-app) textarea{background:0 0}:where(.scalar-app) ::file-selector-button{background:0 0}:where(.scalar-app) ol,:where(.scalar-app) ul,:where(.scalar-app) menu{list-style:none}:where(.scalar-app) input:where(:not([type=button],[type=reset],[type=submit])),:where(.scalar-app) select,:where(.scalar-app) textarea{border-radius:var(--scalar-radius);border-width:1px}:where(.scalar-app) input::placeholder{color:var(--scalar-color-3);font-family:var(--scalar-font)}:where(.scalar-app) input[type=search]::-webkit-search-cancel-button{appearance:none}:where(.scalar-app) input[type=search]::-webkit-search-decoration{appearance:none}:where(.scalar-app) summary::-webkit-details-marker{display:none}:where(.scalar-app) input:-webkit-autofill{-webkit-background-clip:text!important;background-clip:text!important}:where(.scalar-app) :focus-visible{border-radius:var(--scalar-radius);outline-style:solid}:where(.scalar-app) button:focus-visible,:where(.scalar-app) [role=button]:focus-visible{outline-offset:-1px}:where(.scalar-app) button,:where(.scalar-app) [role=button]{cursor:pointer}:where(.scalar-app) :disabled{cursor:default}:where(.scalar-app) img,:where(.scalar-app) svg,:where(.scalar-app) video,:where(.scalar-app) canvas,:where(.scalar-app) audio,:where(.scalar-app) iframe,:where(.scalar-app) embed,:where(.scalar-app) object{vertical-align:middle;display:block}:where(.scalar-app) [hidden]{display:none}.scalar-app .cm-scroller,.scalar-app .custom-scroll{scrollbar-color:transparent transparent;scrollbar-width:thin;-webkit-overflow-scrolling:touch;overflow-y:auto}.scalar-app .custom-scroll-self-contain-overflow{overscroll-behavior:contain}.scalar-app .cm-scroller:hover,.scalar-app .custom-scroll:hover,.scalar-app.scalar-scrollbars-obtrusive .cm-scroller,.scalar-app.scalar-scrollbars-obtrusive .custom-scroll{scrollbar-color:var(--scalar-scrollbar-color,transparent)transparent}.scalar-app .cm-scroller:hover::-webkit-scrollbar-thumb{background:var(--scalar-scrollbar-color);background-clip:content-box;border:3px solid #0000}.scalar-app .custom-scroll:hover::-webkit-scrollbar-thumb{background:var(--scalar-scrollbar-color);background-clip:content-box;border:3px solid #0000}.scalar-app .cm-scroller::-webkit-scrollbar-thumb:active{background:var(--scalar-scrollbar-color-active);background-clip:content-box;border:3px solid #0000}.scalar-app .custom-scroll::-webkit-scrollbar-thumb:active{background:var(--scalar-scrollbar-color-active);background-clip:content-box;border:3px solid #0000}.scalar-app .cm-scroller::-webkit-scrollbar-corner{background:0 0}.scalar-app .custom-scroll::-webkit-scrollbar-corner{background:0 0}.scalar-app .cm-scroller::-webkit-scrollbar{width:12px;height:12px}.scalar-app .custom-scroll::-webkit-scrollbar{width:12px;height:12px}.scalar-app .cm-scroller::-webkit-scrollbar-track{background:0 0}.scalar-app .custom-scroll::-webkit-scrollbar-track{background:0 0}.scalar-app .cm-scroller::-webkit-scrollbar-thumb{background:padding-box content-box;border:3px solid #0000;border-radius:20px}.scalar-app .custom-scroll::-webkit-scrollbar-thumb{background:padding-box content-box;border:3px solid #0000;border-radius:20px}@media (pointer:coarse){.scalar-app .cm-scroller,.scalar-app .custom-scroll{padding-right:12px}}.scalar-app .invisible{visibility:hidden}.scalar-app .inset-y-0{inset-block:0}.scalar-app .top-\\\\(--nested-items-offset\\\\){top:var(--nested-items-offset)}.scalar-app .top-0\\\\.5{top:2px}.scalar-app .top-1\\\\/2{top:50%}.scalar-app .top-22{top:88px}.scalar-app .top-px{top:1px}.scalar-app .left-2{left:8px}.scalar-app .left-2\\\\.5{left:10px}.scalar-app .left-4{left:16px}.scalar-app .left-10{left:40px}.scalar-app .left-border{left:var(--scalar-border-width)}.scalar-app .left-px{left:1px}.scalar-app .z-\\\\[1001\\\\]{z-index:1001}.scalar-app .z-tooltip{z-index:99999}.scalar-app .-m-1\\\\.5{margin:-6px}.scalar-app .-m-px{margin:-1px}.scalar-app .m-1{margin:4px}.scalar-app .-mx-0\\\\.75{margin-inline:-3px}.scalar-app .-mx-px{margin-inline:-1px}.scalar-app .mx-px{margin-inline:1px}.scalar-app .-my-1\\\\.5{margin-block:-6px}.scalar-app .-my-2{margin-block:-8px}.scalar-app .my-0\\\\.75{margin-block:3px}.scalar-app .-mt-1\\\\.5{margin-top:-6px}.scalar-app .mt-0{margin-top:0}.scalar-app .mt-\\\\[15svh\\\\]{margin-top:15svh}.scalar-app .mt-\\\\[20svh\\\\]{margin-top:20svh}.scalar-app .-mr-0\\\\.25{margin-right:-1px}.scalar-app .mr-0{margin-right:0}.scalar-app .mr-\\\\[calc\\\\(20px-var\\\\(--scalar-sidebar-indent\\\\)\\\\)\\\\]{margin-right:calc(20px - var(--scalar-sidebar-indent))}.scalar-app .-mb-1{margin-bottom:-4px}.scalar-app .-ml-0\\\\.75{margin-left:-3px}.scalar-app .line-clamp-\\\\(--markdown-clamp\\\\){-webkit-line-clamp:var(--markdown-clamp);-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden}.scalar-app .\\\\!hidden{display:none!important}.scalar-app .size-2{width:8px;height:8px}.scalar-app .size-2\\\\.75{width:11px;height:11px}.scalar-app .size-3\\\\.25{width:13px;height:13px}.scalar-app .size-60{width:240px;height:240px}.scalar-app .size-\\\\[23px\\\\]{width:23px;height:23px}.scalar-app .h-0{height:0}.scalar-app .h-1{height:4px}.scalar-app .h-24{height:96px}.scalar-app .h-32{height:128px}.scalar-app .h-\\\\[1lh\\\\]{height:1lh}.scalar-app .h-\\\\[100dvh\\\\]{height:100dvh}.scalar-app .h-border{height:var(--scalar-border-width)}.scalar-app .h-dvh{height:100dvh}.scalar-app .max-h-20{max-height:80px}.scalar-app .max-h-\\\\[80svh\\\\]{max-height:80svh}.scalar-app .max-h-\\\\[90svh\\\\]{max-height:90svh}.scalar-app .max-h-dvh{max-height:100dvh}.scalar-app .max-h-radix-popper{max-height:calc(var(--radix-popper-available-height) - 8px)}.scalar-app .min-h-96{min-height:384px}.scalar-app .min-h-header{min-height:48px}.scalar-app .w-12{width:48px}.scalar-app .w-16{width:64px}.scalar-app .w-24{width:96px}.scalar-app .w-32{width:128px}.scalar-app .w-40{width:160px}.scalar-app .w-48{width:192px}.scalar-app .w-\\\\[38px\\\\]{width:38px}.scalar-app .w-\\\\[100dvw\\\\]{width:100dvw}.scalar-app .w-\\\\[calc\\\\(100vw-12px\\\\)\\\\]{width:calc(100vw - 12px)}.scalar-app .w-\\\\[var\\\\(--scalar-sidebar-indent\\\\)\\\\]{width:var(--scalar-sidebar-indent)}.scalar-app .w-border{width:var(--scalar-border-width)}.scalar-app .w-min{width:min-content}.scalar-app .w-screen{width:100vw}.scalar-app .max-w-\\\\[360px\\\\]{max-width:360px}.scalar-app .max-w-\\\\[480px\\\\]{max-width:480px}.scalar-app .max-w-\\\\[540px\\\\]{max-width:540px}.scalar-app .max-w-\\\\[640px\\\\]{max-width:640px}.scalar-app .max-w-\\\\[800px\\\\]{max-width:800px}.scalar-app .max-w-\\\\[1000px\\\\]{max-width:1000px}.scalar-app .max-w-\\\\[inherit\\\\]{max-width:inherit}.scalar-app .max-w-xs{max-width:320px}.scalar-app .min-w-6{min-width:24px}.scalar-app .min-w-min{min-width:min-content}.scalar-app .flex-shrink,.scalar-app .shrink{flex-shrink:1}.scalar-app .-translate-x-full{--tw-translate-x:-100%;translate:var(--tw-translate-x)var(--tw-translate-y)}.scalar-app .translate-x-2\\\\.5{--tw-translate-x:10px;translate:var(--tw-translate-x)var(--tw-translate-y)}.scalar-app .translate-x-\\\\[14px\\\\]{--tw-translate-x:14px;translate:var(--tw-translate-x)var(--tw-translate-y)}.scalar-app .translate-x-full{--tw-translate-x:100%;translate:var(--tw-translate-x)var(--tw-translate-y)}.scalar-app .-translate-y-1\\\\/2{--tw-translate-y:-50%;translate:var(--tw-translate-x)var(--tw-translate-y)}.scalar-app .rotate-180{rotate:180deg}.scalar-app .appearance-none{appearance:none}.scalar-app .grid-flow-col{grid-auto-flow:column}.scalar-app .\\\\!items-end{align-items:flex-end!important}.scalar-app .\\\\!items-start{align-items:flex-start!important}.scalar-app .items-baseline{align-items:baseline}.scalar-app .\\\\!justify-end{justify-content:flex-end!important}.scalar-app .\\\\!justify-start{justify-content:flex-start!important}.scalar-app .gap-2\\\\.25{gap:9px}.scalar-app .gap-16{gap:64px}.scalar-app .gap-x-4{column-gap:16px}.scalar-app .gap-y-8{row-gap:32px}.scalar-app .self-end{align-self:flex-end}.scalar-app .overflow-x-clip{overflow-x:clip}.scalar-app .overflow-y-scroll{overflow-y:scroll}.scalar-app .overscroll-contain{overscroll-behavior:contain}.scalar-app .rounded-none{border-radius:0}.scalar-app .border-1{border-style:var(--tw-border-style);border-width:1px}.scalar-app .border-solid{--tw-border-style:solid;border-style:solid}.scalar-app .border-\\\\(--scalar-background-3\\\\){border-color:var(--scalar-background-3)}.scalar-app .border-border{border-color:var(--scalar-border-color)}.scalar-app .border-c-alert{border-color:var(--scalar-color-alert)}.scalar-app .border-c-danger{border-color:var(--scalar-color-danger)}.scalar-app .border-red{border-color:var(--scalar-color-red)}.scalar-app .border-sidebar-border{border-color:var(--scalar-sidebar-border-color,var(--scalar-border-color))}.scalar-app .border-sidebar-border-search{border-color:var(--scalar-sidebar-search-border-color,var(--scalar-border-color))}.scalar-app .bg-b-1,.scalar-app .bg-b-1\\\\.5{background-color:var(--scalar-background-1)}@supports (color:color-mix(in lab,red,red)){.scalar-app .bg-b-1\\\\.5{background-color:var(--scalar-background-1)}@supports (color:color-mix(in lab,red,red)){.scalar-app .bg-b-1\\\\.5{background-color:var(--scalar-background-1)}@supports (color:color-mix(in lab,red,red)){.scalar-app .bg-b-1\\\\.5{background-color:var(--scalar-background-1)}@supports (color:color-mix(in lab,red,red)){.scalar-app .bg-b-1\\\\.5{background-color:color-mix(in srgb,var(--scalar-background-1),var(--scalar-background-2))}}}}}.scalar-app .bg-b-alert{background-color:var(--scalar-background-alert)}.scalar-app .bg-b-btn{background-color:var(--scalar-button-1)}.scalar-app .bg-b-tooltip{background-color:var(--scalar-tooltip-background)}.scalar-app .bg-backdrop{background-color:#00000038}.scalar-app .bg-border{background-color:var(--scalar-border-color)}.scalar-app .bg-c-danger{background-color:var(--scalar-color-danger)}.scalar-app .bg-inherit{background-color:inherit}.scalar-app .bg-sidebar-b-search{background-color:var(--scalar-sidebar-search-background,var(--scalar-background-2))}.scalar-app .bg-sidebar-indent-border{background-color:var(--scalar-sidebar-indent-border,var(--scalar-border-color))}.scalar-app .bg-sidebar-indent-border-active{background-color:var(--scalar-sidebar-indent-border-active,var(--scalar-color-accent))}.scalar-app .bg-transparent{background-color:#0000}.scalar-app .bg-linear-to-b{--tw-gradient-position:to bottom}@supports (background-image:linear-gradient(in lab,red,red)){.scalar-app .bg-linear-to-b{--tw-gradient-position:to bottom in oklab}}.scalar-app .bg-linear-to-b{background-image:linear-gradient(var(--tw-gradient-stops))}.scalar-app .from-b-1{--tw-gradient-from:var(--scalar-background-1);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.scalar-app .to-b-1\\\\.5{--tw-gradient-to:var(--scalar-background-1)}@supports (color:color-mix(in lab,red,red)){.scalar-app .to-b-1\\\\.5{--tw-gradient-to:var(--scalar-background-1)}@supports (color:color-mix(in lab,red,red)){.scalar-app .to-b-1\\\\.5{--tw-gradient-to:var(--scalar-background-1)}@supports (color:color-mix(in lab,red,red)){.scalar-app .to-b-1\\\\.5{--tw-gradient-to:var(--scalar-background-1)}@supports (color:color-mix(in lab,red,red)){.scalar-app .to-b-1\\\\.5{--tw-gradient-to:color-mix(in srgb,var(--scalar-background-1),var(--scalar-background-2))}}}}}.scalar-app .to-b-1\\\\.5{--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.scalar-app .to-b-2{--tw-gradient-to:var(--scalar-background-2);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.scalar-app .mask-repeat{-webkit-mask-repeat:repeat;mask-repeat:repeat}.scalar-app .p-0\\\\.25{padding:1px}.scalar-app .p-2\\\\.5{padding:10px}.scalar-app .p-6{padding:24px}.scalar-app .px-3\\\\.5{padding-inline:14px}.scalar-app .px-9{padding-inline:36px}.scalar-app .py-4{padding-block:16px}.scalar-app .py-\\\\[6\\\\.75px\\\\]{padding-block:6.75px}.scalar-app .pl-8{padding-left:32px}.scalar-app .text-lg{font-size:var(--scalar-font-size-2)}.scalar-app .leading-5{--tw-leading:var(--scalar-line-height-5);line-height:var(--scalar-line-height-5)}.scalar-app .font-sidebar{--tw-font-weight:var(--scalar-sidebar-font-weight,var(--scalar-regular));font-weight:var(--scalar-sidebar-font-weight,var(--scalar-regular))}.scalar-app .font-sidebar-active{--tw-font-weight:var(--scalar-sidebar-font-weight-active,var(--scalar-semibold));font-weight:var(--scalar-sidebar-font-weight-active,var(--scalar-semibold))}.scalar-app .text-nowrap{text-wrap:nowrap}.scalar-app .text-wrap{text-wrap:wrap}.scalar-app .break-words,.scalar-app .wrap-break-word{overflow-wrap:break-word}.scalar-app .text-c-accent{color:var(--scalar-color-accent)}.scalar-app .text-c-alert{color:var(--scalar-color-alert)}.scalar-app .text-c-danger{color:var(--scalar-color-danger)}.scalar-app .text-c-tooltip{color:var(--scalar-tooltip-color)}.scalar-app .text-sidebar-c-search{color:var(--scalar-sidebar-search-color,var(--scalar-color-3))}.scalar-app .text-white{color:#fff}.scalar-app .opacity-40{opacity:.4}.scalar-app .outline-offset-1{outline-offset:1px}.scalar-app .outline-offset-\\\\[-1px\\\\]{outline-offset:-1px}.scalar-app .backdrop-blur{--tw-backdrop-blur:blur(8px);-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}:is(.scalar-app .\\\\*\\\\:size-4>*){width:16px;height:16px}:is(.scalar-app .\\\\*\\\\:h-5>*){height:20px}:is(.scalar-app .\\\\*\\\\:min-w-5>*){min-width:20px}:is(.scalar-app .\\\\*\\\\:flex-1>*){flex:1}:is(.scalar-app .\\\\*\\\\:justify-center>*){justify-content:center}:is(.scalar-app .\\\\*\\\\:gap-px>*){gap:1px}:is(.scalar-app .\\\\*\\\\:rounded>*){border-radius:var(--scalar-radius)}:is(.scalar-app .\\\\*\\\\:border>*){border-style:var(--tw-border-style);border-width:var(--scalar-border-width)}:is(.scalar-app .\\\\*\\\\:border-border-tooltip>*){border-color:var(--scalar-tooltip-color)}@supports (color:color-mix(in lab,red,red)){:is(.scalar-app .\\\\*\\\\:border-border-tooltip>*){border-color:var(--scalar-tooltip-color)}@supports (color:color-mix(in lab,red,red)){:is(.scalar-app .\\\\*\\\\:border-border-tooltip>*){border-color:var(--scalar-tooltip-color)}@supports (color:color-mix(in lab,red,red)){:is(.scalar-app .\\\\*\\\\:border-border-tooltip>*){border-color:var(--scalar-tooltip-color)}@supports (color:color-mix(in lab,red,red)){:is(.scalar-app .\\\\*\\\\:border-border-tooltip>*){border-color:color-mix(in srgb,var(--scalar-tooltip-color),var(--scalar-tooltip-background))}}}}}:is(.scalar-app .\\\\*\\\\:px-1>*){padding-inline:4px}@media (hover:hover){.scalar-app .group-hover\\\\:text-c-1:is(:where(.group):hover *){color:var(--scalar-color-1)}.scalar-app .group-hover\\\\/button\\\\:bg-sidebar-indent-border-hover:is(:where(.group\\\\/button):hover *){background-color:var(--scalar-sidebar-indent-border-hover,var(--scalar-border-color))}.scalar-app .group-hover\\\\/button\\\\:text-c-1:is(:where(.group\\\\/button):hover *){color:var(--scalar-color-1)}.scalar-app .group-hover\\\\/code-block\\\\:opacity-100:is(:where(.group\\\\/code-block):hover *){opacity:1}}.scalar-app .group-focus-visible\\\\/toggle\\\\:outline:is(:where(.group\\\\/toggle):focus-visible *){outline-style:var(--tw-outline-style);outline-width:1px}.scalar-app .placeholder\\\\:font-\\\\[inherit\\\\]::placeholder{font-family:inherit}.scalar-app .first\\\\:rounded-t-\\\\[inherit\\\\]:first-child,:is(.scalar-app .\\\\*\\\\:first\\\\:rounded-t-\\\\[inherit\\\\]>*):first-child{border-top-left-radius:inherit;border-top-right-radius:inherit}.scalar-app .last\\\\:rounded-b-\\\\[inherit\\\\]:last-child,:is(.scalar-app .\\\\*\\\\:last\\\\:rounded-b-\\\\[inherit\\\\]>*):last-child{border-bottom-right-radius:inherit;border-bottom-left-radius:inherit}.scalar-app .focus-within\\\\:outline-none:focus-within{--tw-outline-style:none;outline-style:none}@media (hover:hover){.scalar-app .hover\\\\:bg-b-2:hover{background-color:var(--scalar-background-2)}.scalar-app .hover\\\\:bg-b-3:hover{background-color:var(--scalar-background-3)}.scalar-app .hover\\\\:bg-h-btn:hover{background-color:var(--scalar-button-1-hover)}.scalar-app .hover\\\\:bg-linear-to-b:hover{--tw-gradient-position:to bottom}@supports (background-image:linear-gradient(in lab,red,red)){.scalar-app .hover\\\\:bg-linear-to-b:hover{--tw-gradient-position:to bottom in oklab}}.scalar-app .hover\\\\:bg-linear-to-b:hover{background-image:linear-gradient(var(--tw-gradient-stops))}.scalar-app .hover\\\\:bg-linear-to-t:hover{--tw-gradient-position:to top}@supports (background-image:linear-gradient(in lab,red,red)){.scalar-app .hover\\\\:bg-linear-to-t:hover{--tw-gradient-position:to top in oklab}}.scalar-app .hover\\\\:bg-linear-to-t:hover{background-image:linear-gradient(var(--tw-gradient-stops))}.scalar-app .hover\\\\:text-c-1:hover{color:var(--scalar-color-1)}.scalar-app .hover\\\\:underline:hover{text-decoration-line:underline}.scalar-app .hover\\\\:brightness-90:hover{--tw-brightness:brightness(90%);filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}}.scalar-app .focus-visible\\\\:border-c-btn:focus-visible{border-color:var(--scalar-button-1-color)}.scalar-app .focus-visible\\\\:opacity-100:focus-visible{opacity:1}.scalar-app .focus-visible\\\\:outline:focus-visible{outline-style:var(--tw-outline-style);outline-width:1px}.scalar-app .active\\\\:bg-b-btn:active{background-color:var(--scalar-button-1)}.scalar-app .active\\\\:brightness-90:active{--tw-brightness:brightness(90%);filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.scalar-app .has-\\\\[\\\\:focus-visible\\\\]\\\\:outline:has(:focus-visible),.scalar-app .has-\\\\[input\\\\:focus-visible\\\\]\\\\:outline:has(:is(input:focus-visible)){outline-style:var(--tw-outline-style);outline-width:1px}@media (min-width:800px){.scalar-app .md\\\\:w-\\\\[calc\\\\(100vw-16px\\\\)\\\\]{width:calc(100vw - 16px)}}@media (min-width:1000px){.scalar-app .lg\\\\:w-\\\\[calc\\\\(100vw-32px\\\\)\\\\]{width:calc(100vw - 32px)}.scalar-app .lg\\\\:w-full{width:100%}}.scalar-app .dark\\\\:bg-b-3:where(.dark-mode,.dark-mode *){background-color:var(--scalar-background-3)}.scalar-app .dark\\\\:bg-backdrop-dark:where(.dark-mode,.dark-mode *){background-color:#00000073}.scalar-app .dark\\\\:bg-linear-to-t:where(.dark-mode,.dark-mode *){--tw-gradient-position:to top}@supports (background-image:linear-gradient(in lab,red,red)){.scalar-app .dark\\\\:bg-linear-to-t:where(.dark-mode,.dark-mode *){--tw-gradient-position:to top in oklab}}.scalar-app .dark\\\\:bg-linear-to-t:where(.dark-mode,.dark-mode *){background-image:linear-gradient(var(--tw-gradient-stops))}@media (hover:hover){.scalar-app .dark\\\\:hover\\\\:bg-b-3:where(.dark-mode,.dark-mode *):hover{background-color:var(--scalar-background-3)}.scalar-app .dark\\\\:hover\\\\:bg-linear-to-b:where(.dark-mode,.dark-mode *):hover{--tw-gradient-position:to bottom}@supports (background-image:linear-gradient(in lab,red,red)){.scalar-app .dark\\\\:hover\\\\:bg-linear-to-b:where(.dark-mode,.dark-mode *):hover{--tw-gradient-position:to bottom in oklab}}.scalar-app .dark\\\\:hover\\\\:bg-linear-to-b:where(.dark-mode,.dark-mode *):hover{background-image:linear-gradient(var(--tw-gradient-stops))}.scalar-app .dark\\\\:hover\\\\:bg-linear-to-t:where(.dark-mode,.dark-mode *):hover{--tw-gradient-position:to top}@supports (background-image:linear-gradient(in lab,red,red)){.scalar-app .dark\\\\:hover\\\\:bg-linear-to-t:where(.dark-mode,.dark-mode *):hover{--tw-gradient-position:to top in oklab}}.scalar-app .dark\\\\:hover\\\\:bg-linear-to-t:where(.dark-mode,.dark-mode *):hover{background-image:linear-gradient(var(--tw-gradient-stops))}}@media (max-width:720px) and (max-height:480px){.scalar-app .zoomed\\\\:\\\\!whitespace-normal{white-space:normal!important}}.loader-wrapper[data-v-f525638b]{--loader-size:50%;justify-content:center;align-items:center;display:flex;position:relative}.svg-loader[data-v-f525638b]{width:var(--loader-size);height:var(--loader-size);fill:none;stroke:currentColor;background-color:#0000;top:1rem;right:.9rem;overflow:visible}.svg-path[data-v-f525638b]{stroke-width:12px;fill:none;transition:all .3s}.svg-x-mark[data-v-f525638b]{stroke-dasharray:57;stroke-dashoffset:57px;transition-delay:0s}.svg-check-mark[data-v-f525638b]{stroke-dasharray:149;stroke-dashoffset:149px;transition-delay:0s}.icon-is-invalid .svg-x-mark[data-v-f525638b],.icon-is-valid .svg-check-mark[data-v-f525638b]{stroke-dashoffset:0;transition-delay:.3s}.circular-loader[data-v-f525638b]{transform-origin:50%;background:0 0;animation:.7s linear infinite rotate-f525638b,.4s fade-in-f525638b;transform:scale(3.5)}.loader-path[data-v-f525638b]{stroke-dasharray:50 200;stroke-dashoffset:-100px;stroke-linecap:round}.loader-path-off[data-v-f525638b]{stroke-dasharray:50 200;stroke-dashoffset:-100px;opacity:0;transition:opacity .3s}.scalar-code-block:hover .scalar-code-copy[data-v-0295860a]{opacity:100}.copy-icon[data-v-0295860a],.check-icon[data-v-0295860a]{transition:transform .15s ease-in-out;position:absolute;top:50%;left:50%;transform:translate(-50%,-50%)scale(1)}.copy-icon.copied[data-v-0295860a],.check-icon[data-v-0295860a]{transform:translate(-50%,-50%)scale(0)}.check-icon.visible[data-v-0295860a]{transform:translate(-50%,-50%)scale(1)}.scalar-code-block{background:inherit;padding:10px 8px 12px 12px;position:relative;overflow:auto}.scalar-codeblock-pre{all:unset;text-wrap:nowrap;white-space-collapse:preserve;background:0 0;border-radius:0;width:fit-content;margin:0}.toggle-icon-ellipse[data-v-60be8692]{background:var(--scalar-background-1);border-radius:50%;width:7px;height:7px;transition:width .3s ease-in-out,height .3s ease-in-out;display:inline-block;position:relative;overflow:hidden;box-shadow:inset 0 0 0 1px}.toggle-icon-moon-mask[data-v-60be8692]{background:var(--scalar-background-1);border:1px solid;border-radius:50%;width:100%;height:100%;transition:transform .3s ease-in-out;display:block;position:absolute;bottom:2.5px;left:2.5px;transform:translate(4px,-4px)}.toggle-icon-sun-ray[data-v-60be8692]{background:currentColor;border-radius:8px;width:12px;height:1px;transition:transform .3s ease-in-out;position:absolute}.toggle-icon-sun-ray[data-v-60be8692]:nth-of-type(2){transform:rotate(90deg)}.toggle-icon-sun-ray[data-v-60be8692]:nth-of-type(3){transform:rotate(45deg)}.toggle-icon-sun-ray[data-v-60be8692]:nth-of-type(4){transform:rotate(-45deg)}.toggle-icon-dark .toggle-icon-ellipse[data-v-60be8692]{width:10px;height:10px;-webkit-mask-image:radial-gradient(circle at 0 100%,pink 10px,#0000 12px);mask-image:radial-gradient(circle at 0 100%,pink 10px,#0000 12px)}.toggle-icon-dark .toggle-icon-sun-ray[data-v-60be8692]{transform:scale(0)}.toggle-icon-dark .toggle-icon-moon-mask[data-v-60be8692]{transform:translateZ(0)}.scalar-icon[data-v-b651bb23],.scalar-icon[data-v-b651bb23] *{stroke-width:var(--c07589c2)}.scalar-app :where(code.hljs) *{font-size:inherit;font-family:var(--scalar-font-code);text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;tab-size:4;line-height:1.4}.scalar-app code.hljs{all:unset;font-size:inherit;color:var(--scalar-color-2);font-family:var(--scalar-font-code);counter-reset:linenumber}.scalar-app .hljs{color:var(--scalar-color-2);background:0 0}.scalar-app .hljs .line:before{color:var(--scalar-color-3);counter-increment:linenumber;content:counter(linenumber);min-width:calc(var(--line-digits)*1ch);text-align:right;margin-right:.875rem;display:inline-block}.scalar-app .hljs-comment,.scalar-app .hljs-quote{color:var(--scalar-color-3);font-style:italic}.scalar-app .hljs-number{color:var(--scalar-color-orange)}.scalar-app .hljs-regexp,.scalar-app .hljs-string,.scalar-app .hljs-built_in{color:var(--scalar-color-blue)}.scalar-app .hljs-title.class_{color:var(--scalar-color-1)}.scalar-app .hljs-keyword{color:var(--scalar-color-purple)}.scalar-app .hljs-title.function_{color:var(--scalar-color-orange)}.scalar-app .hljs-subst,.scalar-app .hljs-name{color:var(--scalar-color-blue)}.scalar-app .hljs-attr,.scalar-app .hljs-attribute{color:var(--scalar-color-1)}.scalar-app .hljs-addition,.scalar-app .hljs-literal,.scalar-app .hljs-selector-tag,.scalar-app .hljs-type{color:var(--scalar-color-green)}.scalar-app .hljs-selector-attr,.scalar-app .hljs-selector-pseudo{color:var(--scalar-color-orange)}.scalar-app .hljs-doctag,.scalar-app .hljs-section,.scalar-app .hljs-title{color:var(--scalar-color-blue)}.scalar-app .hljs-selector-id,.scalar-app .hljs-template-variable,.scalar-app .hljs-variable{color:var(--scalar-color-1)}.scalar-app .hljs-name,.scalar-app .hljs-section,.scalar-app .hljs-strong{font-weight:var(--scalar-semibold)}.scalar-app .hljs-bullet,.scalar-app .hljs-link,.scalar-app .hljs-meta,.scalar-app .hljs-symbol{color:var(--scalar-color-blue)}.scalar-app .hljs-deletion{color:var(--scalar-color-red)}.scalar-app .hljs-formula{background:var(--scalar-color-1)}.scalar-app .hljs-emphasis{font-style:italic}.scalar-app .credential .credential-value{color:#0000;font-size:0}.scalar-app .credential:after{content:\"·····\";color:var(--scalar-color-3);-webkit-user-select:none;user-select:none}.hljs.language-html{color:var(--scalar-color-1)}.hljs.language-html .hljs-attr{color:var(--scalar-color-2)}.hljs.language-curl .hljs-string{color:var(--scalar-color-blue)}.hljs.language-curl .hljs-literal{color:var(--scalar-color-1)}.hljs.language-php .hljs-variable{color:var(--scalar-color-blue)}.hljs.language-objectivec .hljs-meta{color:var(--scalar-color-1)}.hljs.language-objectivec .hljs-built_in,.hljs-built_in{color:var(--scalar-color-orange)}.scalar-app .markdown{--scalar-refs-heading-spacing:24px;--markdown-border:var(--scalar-border-width)solid var(--scalar-border-color);--markdown-spacing-sm:12px;--markdown-spacing-md:16px;--markdown-line-height:1.625;font-family:var(--scalar-font);word-break:break-word}.scalar-app .markdown>*{margin-bottom:var(--markdown-spacing-md)}.scalar-app .markdown>:not(h1):not(h2):not(h3):not(h4):not(h5):not(h6):last-child{margin-bottom:0}.scalar-app .markdown h1{--font-size:1.5rem;--markdown-line-height:32px}.scalar-app .markdown h2,.scalar-app .markdown h3{--font-size:1.25rem;--markdown-line-height:1.3}.scalar-app .markdown h4,.scalar-app .markdown h5,.scalar-app .markdown h6{--font-size:1rem}.scalar-app .markdown h1,.scalar-app .markdown h2,.scalar-app .markdown h3,.scalar-app .markdown h4,.scalar-app .markdown h5,.scalar-app .markdown h6{font-size:var(--font-size);font-weight:var(--scalar-bold);margin-top:var(--scalar-refs-heading-spacing);margin-bottom:var(--markdown-spacing-sm);scroll-margin-top:1rem;display:block}.scalar-app .markdown b,.scalar-app .markdown strong{font-weight:var(--scalar-bold)}.scalar-app .markdown p{color:inherit;line-height:var(--markdown-line-height);display:block}.scalar-app .markdown img{border-radius:var(--scalar-radius);max-width:100%;display:inline-block;overflow:hidden}.scalar-app .markdown ul,.scalar-app .markdown ol{line-height:var(--markdown-line-height);flex-direction:column;gap:2px;padding-left:1.6em;display:flex}.scalar-app .markdown li{margin-top:2px;padding-left:7px}.scalar-app ol>li::marker{font:var(--scalar-font);font-variant-numeric:tabular-nums;font-weight:var(--scalar-semibold);white-space:nowrap}.scalar-app ol>*>li::marker{font:var(--scalar-font);font-variant-numeric:tabular-nums;font-weight:var(--scalar-semibold);white-space:nowrap}.scalar-app .markdown ol{list-style-type:decimal}.scalar-app .markdown ol ol{list-style-type:lower-alpha}.scalar-app .markdown ol ol ol ol,.scalar-app .markdown ol ol ol ol ol ol ol{list-style-type:decimal}.scalar-app .markdown ol ol ol ol ol,.scalar-app .markdown ol ol ol ol ol ol ol ol{list-style-type:lower-alpha}.scalar-app .markdown ol ol ol,.scalar-app .markdown ol ol ol ol ol ol,.scalar-app .markdown ol ol ol ol ol ol ol ol ol{list-style-type:lower-roman}.scalar-app .markdown ul>li,.scalar-app .markdown ul>*>li{list-style-type:disc}.scalar-app .markdown table{table-layout:fixed;border:var(--scalar-border-width)solid var(--scalar-border-color);border-radius:var(--scalar-radius);border-spacing:0;width:100%;margin:1em 0;display:table;position:relative;overflow-x:auto}.scalar-app .markdown tbody,.scalar-app .markdown thead{vertical-align:middle}.scalar-app .markdown tbody{display:table-row-group}.scalar-app .markdown thead{display:table-header-group}.scalar-app .markdown tr{border-color:inherit;vertical-align:inherit;display:table-row}.scalar-app .markdown td,.scalar-app .markdown th{vertical-align:top;min-width:1em;line-height:var(--markdown-line-height);word-break:break-word;font-size:var(--scalar-small);color:var(--scalar-color-1);border-right:var(--markdown-border);border-bottom:var(--markdown-border);padding:8.5px 16px;display:table-cell;position:relative}.scalar-app .markdown td>*,.scalar-app .markdown th>*{margin-bottom:0}.scalar-app .markdown th:empty{display:none}.scalar-app .markdown td:first-of-type,.scalar-app .markdown th:first-of-type{border-left:none}.scalar-app .markdown td:last-of-type,.scalar-app .markdown th:last-of-type{border-right:none}.scalar-app .markdown tr:last-of-type td{border-bottom:none}.scalar-app .markdown th{font-weight:var(--scalar-bold);text-align:left;background:var(--scalar-background-2);border-left-color:#0000}.scalar-app .markdown th:first-of-type{border-top-left-radius:var(--scalar-radius)}.scalar-app .markdown th:last-of-type{border-top-right-radius:var(--scalar-radius)}.scalar-app .markdown tr>[align=left]{text-align:left}.scalar-app .markdown tr>[align=right]{text-align:right}.scalar-app .markdown tr>[align=center]{text-align:center}.scalar-app .markdown details{border:var(--markdown-border);border-radius:var(--scalar-radius);color:var(--scalar-color-1)}.scalar-app .markdown details>:not(summary){margin:var(--markdown-spacing-md);margin-bottom:0}.scalar-app .markdown details>p:has(>strong):not(:has(:not(strong))){margin-bottom:8px}.scalar-app .markdown details>p:has(>strong):not(:has(:not(strong)))+*{margin-top:0}.scalar-app .markdown details>table{width:calc(100% - calc(var(--markdown-spacing-md)*2))}.scalar-app .markdown summary{min-height:40px;font-weight:var(--scalar-semibold);line-height:var(--markdown-line-height);cursor:pointer;-webkit-user-select:none;user-select:none;border-radius:2.5px;align-items:flex-start;gap:8px;padding:7px 14px;display:flex;position:relative}.scalar-app .markdown summary:hover{background-color:var(--scalar-background-2)}.scalar-app .markdown details[open]{padding-bottom:var(--markdown-spacing-md)}.scalar-app .markdown details[open]>summary{border-bottom:var(--markdown-border);border-bottom-right-radius:0;border-bottom-left-radius:0}.scalar-app .markdown summary:before{content:\"\";width:var(--markdown-spacing-md);height:var(--markdown-spacing-md);background-color:var(--scalar-color-3);flex-shrink:0;margin-top:5px;display:block;-webkit-mask-image:url(\\'data:image/svg+xml,\\');mask-image:url(\\'data:image/svg+xml,\\')}.scalar-app .markdown summary:hover:before{background-color:var(--scalar-color-1)}.scalar-app .markdown details[open]>summary:before{transition:transform .1s ease-in-out;transform:rotate(90deg)}.scalar-app .markdown details:has(+details){border-bottom:0;border-bottom-right-radius:0;border-bottom-left-radius:0;margin-bottom:0}.scalar-app .markdown details:has(+details)+details,.scalar-app .markdown details:has(+details)+details>summary{border-top-left-radius:0;border-top-right-radius:0}.scalar-app .markdown a{--font-color:var(--scalar-link-color,var(--scalar-color-accent));--font-visited:var(--scalar-link-color-visited,var(--scalar-color-2));-webkit-text-decoration:var(--scalar-text-decoration);text-decoration:var(--scalar-text-decoration);color:var(--font-color);font-weight:var(--scalar-link-font-weight,var(--scalar-semibold));text-underline-offset:.25rem;text-decoration-thickness:1px;-webkit-text-decoration-color:var(--font-color);text-decoration-color:var(--font-color)}@supports (color:color-mix(in lab,red,red)){.scalar-app .markdown a{-webkit-text-decoration-color:var(--font-color)}@supports (color:color-mix(in lab,red,red)){.scalar-app .markdown a{-webkit-text-decoration-color:var(--font-color)}@supports (color:color-mix(in lab,red,red)){.scalar-app .markdown a{-webkit-text-decoration-color:color-mix(in srgb,var(--font-color)30%,transparent)}}}.scalar-app .markdown a{-webkit-text-decoration-color:var(--font-color);text-decoration-color:var(--font-color)}@supports (color:color-mix(in lab,red,red)){.scalar-app .markdown a{-webkit-text-decoration-color:var(--font-color)}@supports (color:color-mix(in lab,red,red)){.scalar-app .markdown a{-webkit-text-decoration-color:color-mix(in srgb,var(--font-color)30%,transparent)}}.scalar-app .markdown a{-webkit-text-decoration-color:var(--font-color);text-decoration-color:var(--font-color)}@supports (color:color-mix(in lab,red,red)){.scalar-app .markdown a{-webkit-text-decoration-color:color-mix(in srgb,var(--font-color)30%,transparent);text-decoration-color:color-mix(in srgb,var(--font-color)30%,transparent)}}}}.scalar-app .markdown a:hover{color:var(--scalar-link-color-hover,var(--scalar-color-accent));-webkit-text-decoration:var(--scalar-text-decoration-hover);text-decoration:var(--scalar-text-decoration-hover)}.scalar-app .markdown a:visited{color:var(--font-visited)}.scalar-app .markdown em{font-style:italic}.scalar-app .markdown sup,.scalar-app .markdown sub{font-size:var(--scalar-micro);font-weight:450}.scalar-app .markdown sup{vertical-align:super}.scalar-app .markdown sub{vertical-align:sub}.scalar-app .markdown del{text-decoration:line-through}.scalar-app .markdown code{font-family:var(--scalar-font-code);background-color:var(--scalar-background-2);box-shadow:0 0 0 var(--scalar-border-width) var(--scalar-border-color);font-size:var(--scalar-micro);border-radius:2px;padding:0 3px}.scalar-app .markdown .hljs{font-size:var(--scalar-small)}.scalar-app .markdown pre code{white-space:pre;padding:var(--markdown-spacing-sm);margin:var(--markdown-spacing-sm)0;-webkit-overflow-scrolling:touch;min-width:100px;max-width:100%;line-height:1.5;display:block;overflow-x:auto}.scalar-app .markdown hr{border:none;border-bottom:var(--markdown-border)}.scalar-app .markdown blockquote{border-left:1px solid var(--scalar-color-1);padding-left:var(--markdown-spacing-md);font-weight:var(--scalar-bold);font-size:var(--scalar-font-size-2);margin:0;display:block}.scalar-app .markdown li.task-list-item{list-style:none;position:relative}.scalar-app .markdown li.task-list-item>input{appearance:none;width:var(--markdown-spacing-md);height:var(--markdown-spacing-md);border:1px solid var(--scalar-color-3);border-radius:var(--scalar-radius);display:inline;position:absolute;top:.225em;left:-1.4em}.scalar-app .markdown li.task-list-item>input[type=checkbox]:checked{background-color:var(--scalar-color-1);border-color:var(--scalar-color-1)}.scalar-app .markdown li.task-list-item>input[type=checkbox]:before{content:\"\";border:solid var(--scalar-background-1);opacity:0;border-width:0 1.5px 1.5px 0;width:5px;height:10px;position:absolute;top:1px;left:5px;transform:rotate(45deg)}.scalar-app .markdown li.task-list-item>input[type=checkbox]:checked:before{opacity:1}.scalar-app .markdown .markdown-alert{border-radius:var(--scalar-radius);background-color:var(--scalar-background-2);align-items:stretch}@supports (color:color-mix(in lab,red,red)){.scalar-app .markdown .markdown-alert{background-color:var(--scalar-background-2)}@supports (color:color-mix(in lab,red,red)){.scalar-app .markdown .markdown-alert{background-color:var(--scalar-background-2)}@supports (color:color-mix(in lab,red,red)){.scalar-app .markdown .markdown-alert{background-color:color-mix(in srgb,var(--scalar-background-2),transparent)}}}}.scalar-app .markdown .markdown-alert{border:var(--markdown-border);gap:var(--markdown-spacing-sm);padding:10px 14px;display:flex;position:relative}.scalar-app .markdown .markdown-alert .markdown-alert-icon:before{content:\"\";background-color:currentColor;flex-shrink:0;width:18px;height:18px;margin-top:3px;display:block;-webkit-mask-position:50%;mask-position:50%;-webkit-mask-size:contain;mask-size:contain;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat}.scalar-app .markdown .markdown-alert.markdown-alert-note{background-color:var(--scalar-color-blue)}@supports (color:color-mix(in lab,red,red)){.scalar-app .markdown .markdown-alert.markdown-alert-note{background-color:var(--scalar-color-blue)}@supports (color:color-mix(in lab,red,red)){.scalar-app .markdown .markdown-alert.markdown-alert-note{background-color:var(--scalar-color-blue)}@supports (color:color-mix(in lab,red,red)){.scalar-app .markdown .markdown-alert.markdown-alert-note{background-color:color-mix(in srgb,var(--scalar-color-blue),transparent 97%)}}}}.scalar-app .markdown .markdown-alert.markdown-alert-note{border:var(--scalar-border-width)solid var(--scalar-color-blue)}@supports (color:color-mix(in lab,red,red)){.scalar-app .markdown .markdown-alert.markdown-alert-note{border:var(--scalar-border-width)solid var(--scalar-color-blue)}@supports (color:color-mix(in lab,red,red)){.scalar-app .markdown .markdown-alert.markdown-alert-note{border:var(--scalar-border-width)solid var(--scalar-color-blue)}@supports (color:color-mix(in lab,red,red)){.scalar-app .markdown .markdown-alert.markdown-alert-note{border:var(--scalar-border-width)solid color-mix(in srgb,var(--scalar-color-blue),transparent 50%)}}}}.scalar-app .markdown .markdown-alert.markdown-alert-tip{background-color:var(--scalar-color-2)}@supports (color:color-mix(in lab,red,red)){.scalar-app .markdown .markdown-alert.markdown-alert-tip{background-color:var(--scalar-color-2)}@supports (color:color-mix(in lab,red,red)){.scalar-app .markdown .markdown-alert.markdown-alert-tip{background-color:var(--scalar-color-2)}@supports (color:color-mix(in lab,red,red)){.scalar-app .markdown .markdown-alert.markdown-alert-tip{background-color:color-mix(in srgb,var(--scalar-color-2),transparent 97%)}}}}.scalar-app .markdown .markdown-alert.markdown-alert-tip{border:var(--scalar-border-width)solid var(--scalar-color-2)}@supports (color:color-mix(in lab,red,red)){.scalar-app .markdown .markdown-alert.markdown-alert-tip{border:var(--scalar-border-width)solid var(--scalar-color-2)}@supports (color:color-mix(in lab,red,red)){.scalar-app .markdown .markdown-alert.markdown-alert-tip{border:var(--scalar-border-width)solid var(--scalar-color-2)}@supports (color:color-mix(in lab,red,red)){.scalar-app .markdown .markdown-alert.markdown-alert-tip{border:var(--scalar-border-width)solid color-mix(in srgb,var(--scalar-color-2),transparent 50%)}}}}.scalar-app .markdown .markdown-alert.markdown-alert-note .markdown-alert-icon:before,.scalar-app .markdown .markdown-alert.markdown-alert-tip .markdown-alert-icon:before{color:var(--scalar-color-blue);-webkit-mask-image:url(\\'data:image/svg+xml,\\');mask-image:url(\\'data:image/svg+xml,\\')}.scalar-app .markdown .markdown-alert.markdown-alert-important,.scalar-app .markdown .markdown-alert.markdown-alert-warning{background-color:var(--scalar-color-orange)}@supports (color:color-mix(in lab,red,red)){.scalar-app .markdown .markdown-alert.markdown-alert-important,.scalar-app .markdown .markdown-alert.markdown-alert-warning{background-color:var(--scalar-color-orange)}@supports (color:color-mix(in lab,red,red)){.scalar-app .markdown .markdown-alert.markdown-alert-important,.scalar-app .markdown .markdown-alert.markdown-alert-warning{background-color:var(--scalar-color-orange)}@supports (color:color-mix(in lab,red,red)){.scalar-app .markdown .markdown-alert.markdown-alert-important,.scalar-app .markdown .markdown-alert.markdown-alert-warning{background-color:color-mix(in srgb,var(--scalar-color-orange),transparent 97%)}}}}.scalar-app .markdown .markdown-alert.markdown-alert-important,.scalar-app .markdown .markdown-alert.markdown-alert-warning{border:var(--scalar-border-width)solid var(--scalar-color-orange)}@supports (color:color-mix(in lab,red,red)){.scalar-app .markdown .markdown-alert.markdown-alert-important,.scalar-app .markdown .markdown-alert.markdown-alert-warning{border:var(--scalar-border-width)solid var(--scalar-color-orange)}@supports (color:color-mix(in lab,red,red)){.scalar-app .markdown .markdown-alert.markdown-alert-important,.scalar-app .markdown .markdown-alert.markdown-alert-warning{border:var(--scalar-border-width)solid var(--scalar-color-orange)}@supports (color:color-mix(in lab,red,red)){.scalar-app .markdown .markdown-alert.markdown-alert-important,.scalar-app .markdown .markdown-alert.markdown-alert-warning{border:var(--scalar-border-width)solid color-mix(in srgb,var(--scalar-color-orange),transparent 50%)}}}}.scalar-app .markdown .markdown-alert.markdown-alert-important .markdown-alert-icon:before,.scalar-app .markdown .markdown-alert.markdown-alert-warning .markdown-alert-icon:before{-webkit-mask-image:url(\\'data:image/svg+xml,\\');mask-image:url(\\'data:image/svg+xml,\\')}.scalar-app .markdown .markdown-alert.markdown-alert-caution{background-color:var(--scalar-color-red)}@supports (color:color-mix(in lab,red,red)){.scalar-app .markdown .markdown-alert.markdown-alert-caution{background-color:var(--scalar-color-red)}@supports (color:color-mix(in lab,red,red)){.scalar-app .markdown .markdown-alert.markdown-alert-caution{background-color:var(--scalar-color-red)}@supports (color:color-mix(in lab,red,red)){.scalar-app .markdown .markdown-alert.markdown-alert-caution{background-color:color-mix(in srgb,var(--scalar-color-red),transparent 97%)}}}}.scalar-app .markdown .markdown-alert.markdown-alert-caution{border:var(--scalar-border-width)solid var(--scalar-color-red)}@supports (color:color-mix(in lab,red,red)){.scalar-app .markdown .markdown-alert.markdown-alert-caution{border:var(--scalar-border-width)solid var(--scalar-color-red)}@supports (color:color-mix(in lab,red,red)){.scalar-app .markdown .markdown-alert.markdown-alert-caution{border:var(--scalar-border-width)solid var(--scalar-color-red)}@supports (color:color-mix(in lab,red,red)){.scalar-app .markdown .markdown-alert.markdown-alert-caution{border:var(--scalar-border-width)solid color-mix(in srgb,var(--scalar-color-red),transparent 50%)}}}}.scalar-app .markdown .markdown-alert.markdown-alert-caution .markdown-alert-icon:before{color:var(--scalar-color-red);-webkit-mask-image:url(\\'data:image/svg+xml,\\');mask-image:url(\\'data:image/svg+xml,\\')}.scalar-app .markdown .markdown-alert.markdown-alert-success{background-color:var(--scalar-color-green)}@supports (color:color-mix(in lab,red,red)){.scalar-app .markdown .markdown-alert.markdown-alert-success{background-color:var(--scalar-color-green)}@supports (color:color-mix(in lab,red,red)){.scalar-app .markdown .markdown-alert.markdown-alert-success{background-color:var(--scalar-color-green)}@supports (color:color-mix(in lab,red,red)){.scalar-app .markdown .markdown-alert.markdown-alert-success{background-color:color-mix(in srgb,var(--scalar-color-green),transparent 97%)}}}}.scalar-app .markdown .markdown-alert.markdown-alert-success{border:var(--scalar-border-width)solid var(--scalar-color-green)}@supports (color:color-mix(in lab,red,red)){.scalar-app .markdown .markdown-alert.markdown-alert-success{border:var(--scalar-border-width)solid var(--scalar-color-green)}@supports (color:color-mix(in lab,red,red)){.scalar-app .markdown .markdown-alert.markdown-alert-success{border:var(--scalar-border-width)solid var(--scalar-color-green)}@supports (color:color-mix(in lab,red,red)){.scalar-app .markdown .markdown-alert.markdown-alert-success{border:var(--scalar-border-width)solid color-mix(in srgb,var(--scalar-color-green),transparent 50%)}}}}.scalar-app .markdown .markdown-alert.markdown-alert-success .markdown-alert-icon:before{color:var(--scalar-color-green);-webkit-mask-image:url(\\'data:image/svg+xml,\\');mask-image:url(\\'data:image/svg+xml,\\')}.scalar-app .markdown .markdown-alert.markdown-alert-note .markdown-alert-icon:before{color:var(--scalar-color-blue)}.scalar-app .markdown .markdown-alert.markdown-alert-tip .markdown-alert-icon:before{color:var(--scalar-color-2)}.scalar-app .markdown .markdown-alert.markdown-alert-important .markdown-alert-icon:before{color:var(--scalar-color-purple)}.scalar-app .markdown .markdown-alert.markdown-alert-warning .markdown-alert-icon:before{color:var(--scalar-color-orange)}.scalar-app .markdown .markdown-alert .markdown-alert-content{line-height:var(--markdown-line-height);margin:0}.scalar-app .markdown.markdown-summary.markdown-summary :before,.scalar-app .markdown.markdown-summary.markdown-summary :after{content:none}.scalar-app .markdown.markdown-summary.markdown-summary :not(strong,em,a){font-size:inherit;font-weight:inherit;line-height:var(--markdown-line-height);display:contents}.scalar-app .markdown.markdown-summary.markdown-summary img,.scalar-app .markdown.markdown-summary.markdown-summary svg,.scalar-app .markdown.markdown-summary.markdown-summary hr,.scalar-app .markdown.markdown-summary.markdown-summary pre{display:none}.dark-mode .scalar-dropdown-item[data-v-7e7bf818]:hover{filter:brightness(1.1)}.group\\\\/item>*>.scalar-sidebar-indent .scalar-sidebar-indent-border[data-v-48a21042]{inset-block:-1px}.group\\\\/item:first-child>*>.scalar-sidebar-indent .scalar-sidebar-indent-border[data-v-48a21042]{top:0}.group\\\\/item:last-child>*>.scalar-sidebar-indent .scalar-sidebar-indent-border[data-v-48a21042]{bottom:0}.group\\\\/items.-translate-x-full .group\\\\/button{transition-behavior:allow-discrete;max-height:0;transition-property:display,max-height;transition-duration:0s;transition-delay:.3s;display:none}.group\\\\/item.group\\\\/nested-items-open>*>.group\\\\/items.translate-x-0 .group\\\\/button{max-height:3.40282e38px;display:flex}.group\\\\/sidebar-section:first-of-type>.group\\\\/spacer-before,.group\\\\/sidebar-section:last-of-type>.group\\\\/spacer-after{height:0}.group\\\\/sidebar-section:has(+.group\\\\/sidebar-section)>.group\\\\/spacer-after{height:0;margin-bottom:-1px}:where(body)>.scalar-tooltip{--scalar-tooltip-padding:8px;padding:calc(var(--scalar-tooltip-padding) + var(--scalar-tooltip-offset));z-index:99999;max-width:320px;font-size:var(--scalar-font-size-5);--tw-leading:var(--scalar-line-height-5);line-height:var(--scalar-line-height-5);--tw-font-weight:var(--scalar-semibold);font-weight:var(--scalar-semibold);overflow-wrap:break-word;color:var(--scalar-tooltip-color)}:where(body)>.scalar-tooltip:before{content:\"\";inset:var(--scalar-tooltip-offset);z-index:-1;border-radius:var(--scalar-radius);background-color:var(--scalar-tooltip-background);--tw-backdrop-blur:blur(8px);-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);position:absolute}:where(body.dark-mode)>.scalar-tooltip:before{--tw-shadow:inset 0 0 0 var(--tw-shadow-color,calc(var(--scalar-border-width)*2))var(--scalar-border-color);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.dark-mode .scalar-dropdown-item[data-v-788cdfbc]:hover{filter:brightness(1.1)}.scalar-modal-layout[data-v-1319c63c]{animation:.3s ease-in-out forwards fadein-layout-1319c63c}.scalar-modal[data-v-1319c63c]{box-shadow:var(--scalar-shadow-2);animation:.3s ease-in-out .1s forwards fadein-modal-1319c63c;transform:translateY(10px)}.scalar-modal-layout-full[data-v-1319c63c]{opacity:1!important;background:0 0!important}.modal-content-search .modal-body[data-v-1319c63c]{flex-direction:column;max-height:440px;padding:0;display:flex;overflow:hidden}@media (max-width:720px) and (max-height:480px){.scalar-modal-layout .scalar-modal[data-v-1319c63c]{max-height:90svh;margin-top:5svh}}.full-size-styles[data-v-1319c63c]{margin:initial;border-right:var(--scalar-border-width)solid var(--scalar-border-color);animation:.3s ease-in-out forwards fadein-layout-1319c63c;left:0;transform:translate(0);background-color:var(--scalar-background-1)!important;max-height:100%!important;box-shadow:none!important;border-radius:0!important;position:absolute!important;top:0!important}@media (min-width:800px){.full-size-styles[data-v-1319c63c]{width:50dvw!important}}.full-size-styles[data-v-1319c63c]:after{content:\"\";width:50dvw;height:100dvh;position:absolute;top:0;right:-50dvw}@supports (color:color-mix(in lab,red,red)){.dragover-above[data-v-a89d6a6e]:after,.dragover-below[data-v-a89d6a6e]:after{background:var(--scalar-color-blue)}@supports (color:color-mix(in lab,red,red)){.dragover-above[data-v-a89d6a6e]:after,.dragover-below[data-v-a89d6a6e]:after{background:var(--scalar-color-blue)}@supports (color:color-mix(in lab,red,red)){.dragover-above[data-v-a89d6a6e]:after,.dragover-below[data-v-a89d6a6e]:after{background:color-mix(in srgb,var(--scalar-color-blue),transparent 85%)}}}}@supports (color:color-mix(in lab,red,red)){.dragover-asChild[data-v-a89d6a6e]:after{background:var(--scalar-color-blue)}@supports (color:color-mix(in lab,red,red)){.dragover-asChild[data-v-a89d6a6e]:after{background:var(--scalar-color-blue)}@supports (color:color-mix(in lab,red,red)){.dragover-asChild[data-v-a89d6a6e]:after{background:color-mix(in srgb,var(--scalar-color-blue),transparent 85%)}}}}.sidebar-heading-type[data-v-1857170e]{text-transform:uppercase;color:var(--method-color,var(--scalar-color-1));font-size:10px;line-height:14px;font-weight:var(--scalar-bold);font-family:var(--scalar-font-code);white-space:nowrap;flex-shrink:0;align-items:center;gap:4px;display:inline-flex;overflow:hidden}.scalar-app .pointer-events-auto{pointer-events:auto}.scalar-app .pointer-events-none{pointer-events:none}.scalar-app .collapse{visibility:collapse}.scalar-app .visible{visibility:visible}.scalar-app .floating-bg:before{background-color:var(--scalar-background-2);border-radius:var(--scalar-radius);content:\"\";opacity:0;z-index:1;width:calc(100% + 8px);height:calc(100% - 4px);transition:opacity .2s ease-in-out;position:absolute;top:2.5px;left:-4px}.scalar-app .floating-bg:hover:before{opacity:1}.scalar-app .centered{--tw-translate-y:-50%;--tw-translate-x:-50%;translate:var(--tw-translate-x)var(--tw-translate-y);position:absolute;top:50%;left:50%}.scalar-app .centered-y{--tw-translate-y:-50%;translate:var(--tw-translate-x)var(--tw-translate-y);position:absolute;top:50%}.scalar-app .centered-x{--tw-translate-x:-50%;translate:var(--tw-translate-x)var(--tw-translate-y);position:absolute;left:50%}.scalar-app .sr-only{clip:rect(0,0,0,0);white-space:nowrap;border-width:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.scalar-app .absolute{position:absolute}.scalar-app .fixed{position:fixed}.scalar-app .relative{position:relative}.scalar-app .static{position:static}.scalar-app .sticky{position:sticky}.scalar-app .inset-0{inset:0}.scalar-app .inset-x-1{inset-inline:4px}.scalar-app .-top-\\\\(--scalar-address-bar-height\\\\){top:calc(var(--scalar-address-bar-height)*-1)}.scalar-app .-top-\\\\[104px\\\\]{top:-104px}.scalar-app .top-0{top:0}.scalar-app .top-2{top:8px}.scalar-app .top-12{top:48px}.scalar-app .-right-\\\\[30px\\\\]{right:-30px}.scalar-app .right-0{right:0}.scalar-app .right-1{right:4px}.scalar-app .right-1\\\\.5{right:6px}.scalar-app .right-1\\\\/2{right:50%}.scalar-app .right-2{right:8px}.scalar-app .right-4{right:16px}.scalar-app .right-7{right:28px}.scalar-app .right-14{right:56px}.scalar-app .right-16{right:64px}.scalar-app .bottom-0{bottom:0}.scalar-app .bottom-1{bottom:4px}.scalar-app .bottom-1\\\\/2{bottom:50%}.scalar-app .bottom-\\\\[var\\\\(--scalar-border-width\\\\)\\\\]{bottom:var(--scalar-border-width)}.scalar-app .left-0{left:0}.scalar-app .left-1\\\\/2{left:50%}.scalar-app .left-3{left:12px}.scalar-app .-z-1{z-index:-1}.scalar-app .z-0{z-index:0}.scalar-app .z-1{z-index:1}.scalar-app .z-10{z-index:10}.scalar-app .z-20{z-index:20}.scalar-app .z-50{z-index:50}.scalar-app .z-\\\\[1\\\\]{z-index:1}.scalar-app .z-\\\\[1002\\\\]{z-index:1002}.scalar-app .z-context{z-index:1000}.scalar-app .z-context-plus{z-index:1001}.scalar-app .z-overlay{z-index:10000}.scalar-app .order-last{order:9999}.scalar-app .col-span-full{grid-column:1/-1}.scalar-app .container{width:100%}@media (min-width:400px){.scalar-app .container{max-width:400px}}@media (min-width:600px){.scalar-app .container{max-width:600px}}@media (min-width:800px){.scalar-app .container{max-width:800px}}@media (min-width:1000px){.scalar-app .container{max-width:1000px}}@media (min-width:1200px){.scalar-app .container{max-width:1200px}}@media (min-width:96rem){.scalar-app .container{max-width:96rem}}.scalar-app .\\\\!m-0{margin:0!important}.scalar-app .-m-0\\\\.5{margin:-2px}.scalar-app .m-0{margin:0}.scalar-app .m-4{margin:16px}.scalar-app .m-auto{margin:auto}.scalar-app .-mx-0\\\\.25{margin-inline:-1px}.scalar-app .mx-auto{margin-inline:auto}.scalar-app .-my-1{margin-block:-4px}.scalar-app .my-12{margin-block:48px}.scalar-app .-mt-\\\\[\\\\.5px\\\\]{margin-top:-.5px}.scalar-app .mt-0\\\\.25{margin-top:1px}.scalar-app .mt-1{margin-top:4px}.scalar-app .mt-1\\\\.5{margin-top:6px}.scalar-app .mt-2{margin-top:8px}.scalar-app .mt-3{margin-top:12px}.scalar-app .mt-5{margin-top:20px}.scalar-app .mt-10{margin-top:40px}.scalar-app .mt-\\\\[0\\\\.5px\\\\]{margin-top:.5px}.scalar-app .mt-auto{margin-top:auto}.scalar-app .\\\\!mr-0{margin-right:0!important}.scalar-app .-mr-0\\\\.5{margin-right:-2px}.scalar-app .-mr-1{margin-right:-4px}.scalar-app .-mr-1\\\\.5{margin-right:-6px}.scalar-app .-mr-3{margin-right:-12px}.scalar-app .mr-0\\\\.5{margin-right:2px}.scalar-app .mr-0\\\\.75{margin-right:3px}.scalar-app .mr-1{margin-right:4px}.scalar-app .mr-1\\\\.5{margin-right:6px}.scalar-app .mr-1\\\\.25{margin-right:5px}.scalar-app .mr-2{margin-right:8px}.scalar-app .mr-2\\\\.5{margin-right:10px}.scalar-app .mr-3{margin-right:12px}.scalar-app .mr-\\\\[6\\\\.25px\\\\]{margin-right:6.25px}.scalar-app .mr-auto{margin-right:auto}.scalar-app .\\\\!mb-0{margin-bottom:0!important}.scalar-app .-mb-\\\\[var\\\\(--scalar-border-width\\\\)\\\\]{margin-bottom:calc(var(--scalar-border-width)*-1)}.scalar-app .mb-0{margin-bottom:0}.scalar-app .mb-1{margin-bottom:4px}.scalar-app .mb-1\\\\.5{margin-bottom:6px}.scalar-app .mb-2{margin-bottom:8px}.scalar-app .mb-4{margin-bottom:16px}.scalar-app .mb-\\\\[\\\\.5px\\\\]{margin-bottom:.5px}.scalar-app .-ml-0\\\\.5{margin-left:-2px}.scalar-app .-ml-0\\\\.25{margin-left:-1px}.scalar-app .-ml-1{margin-left:-4px}.scalar-app .-ml-2{margin-left:-8px}.scalar-app .-ml-12{margin-left:-48px}.scalar-app .ml-0\\\\.5{margin-left:2px}.scalar-app .ml-0\\\\.75{margin-left:3px}.scalar-app .ml-1{margin-left:4px}.scalar-app .ml-1\\\\.25{margin-left:5px}.scalar-app .ml-3{margin-left:12px}.scalar-app .ml-auto{margin-left:auto}.scalar-app .box-border{box-sizing:border-box}.scalar-app .box-content{box-sizing:content-box}.scalar-app .flex-center{justify-content:center;align-items:center;display:flex}.scalar-app .line-clamp-1{-webkit-line-clamp:1;-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden}.scalar-app .\\\\!block{display:block!important}.scalar-app .\\\\!flex{display:flex!important}.scalar-app .block{display:block}.scalar-app .contents{display:contents}.scalar-app .flex{display:flex}.scalar-app .grid{display:grid}.scalar-app .hidden{display:none}.scalar-app .inline{display:inline}.scalar-app .inline-block{display:inline-block}.scalar-app .inline-flex{display:inline-flex}.scalar-app .table{display:table}.scalar-app .aspect-\\\\[4\\\\/3\\\\]{aspect-ratio:4/3}.scalar-app .aspect-square{aspect-ratio:1}.scalar-app .size-2\\\\.5{width:10px;height:10px}.scalar-app .size-3{width:12px;height:12px}.scalar-app .size-3\\\\.5{width:14px;height:14px}.scalar-app .size-3\\\\/4{width:75%;height:75%}.scalar-app .size-4{width:16px;height:16px}.scalar-app .size-5{width:20px;height:20px}.scalar-app .size-6{width:24px;height:24px}.scalar-app .size-7{width:28px;height:28px}.scalar-app .size-8{width:32px;height:32px}.scalar-app .size-10{width:40px;height:40px}.scalar-app .size-full{width:100%;height:100%}.scalar-app .h-\\\\(--scalar-address-bar-height\\\\){height:var(--scalar-address-bar-height)}.scalar-app .h-1\\\\.5{height:6px}.scalar-app .h-2\\\\.5{height:10px}.scalar-app .h-2\\\\.25{height:9px}.scalar-app .h-3{height:12px}.scalar-app .h-3\\\\.5{height:14px}.scalar-app .h-4{height:16px}.scalar-app .h-5{height:20px}.scalar-app .h-6{height:24px}.scalar-app .h-7{height:28px}.scalar-app .h-8{height:32px}.scalar-app .h-9{height:36px}.scalar-app .h-10{height:40px}.scalar-app .h-12{height:48px}.scalar-app .h-64{height:256px}.scalar-app .h-\\\\[68px\\\\]{height:68px}.scalar-app .h-\\\\[calc\\\\(100\\\\%-273\\\\.5px\\\\)\\\\]{height:calc(100% - 273.5px)}.scalar-app .h-\\\\[calc\\\\(100\\\\%_-_50px\\\\)\\\\]{height:calc(100% - 50px)}.scalar-app .h-auto{height:auto}.scalar-app .h-fit{height:fit-content}.scalar-app .h-full{height:100%}.scalar-app .h-header{height:48px}.scalar-app .h-min{height:min-content}.scalar-app .h-px{height:1px}.scalar-app .h-screen{height:100vh}.scalar-app .\\\\!max-h-\\\\[initial\\\\]{max-height:initial!important}.scalar-app .max-h-8{max-height:32px}.scalar-app .max-h-40{max-height:160px}.scalar-app .max-h-80{max-height:320px}.scalar-app .max-h-\\\\[40dvh\\\\]{max-height:40dvh}.scalar-app .max-h-\\\\[50dvh\\\\]{max-height:50dvh}.scalar-app .max-h-\\\\[60svh\\\\]{max-height:60svh}.scalar-app .max-h-\\\\[auto\\\\]{max-height:auto}.scalar-app .max-h-\\\\[calc\\\\(100\\\\%-32px\\\\)\\\\]{max-height:calc(100% - 32px)}.scalar-app .max-h-\\\\[inherit\\\\]{max-height:inherit}.scalar-app .max-h-fit{max-height:fit-content}.scalar-app .max-h-full{max-height:100%}.scalar-app .max-h-screen{max-height:100vh}.scalar-app .\\\\!min-h-full{min-height:100%!important}.scalar-app .min-h-0{min-height:0}.scalar-app .min-h-8{min-height:32px}.scalar-app .min-h-10{min-height:40px}.scalar-app .min-h-11{min-height:44px}.scalar-app .min-h-12{min-height:48px}.scalar-app .min-h-16{min-height:64px}.scalar-app .min-h-20{min-height:80px}.scalar-app .min-h-\\\\[65px\\\\]{min-height:65px}.scalar-app .min-h-\\\\[calc\\\\(1rem\\\\*4\\\\)\\\\]{min-height:4rem}.scalar-app .min-h-\\\\[calc\\\\(4rem\\\\+0\\\\.5px\\\\)\\\\]{min-height:calc(4rem + .5px)}.scalar-app .min-h-\\\\[calc\\\\(4rem\\\\+1px\\\\)\\\\]{min-height:calc(4rem + 1px)}.scalar-app .min-h-fit{min-height:fit-content}.scalar-app .\\\\!w-fit{width:fit-content!important}.scalar-app .w-0\\\\.5{width:2px}.scalar-app .w-1\\\\.5{width:6px}.scalar-app .w-1\\\\/2{width:50%}.scalar-app .w-2\\\\.5{width:10px}.scalar-app .w-2\\\\.25{width:9px}.scalar-app .w-3{width:12px}.scalar-app .w-3\\\\.5{width:14px}.scalar-app .w-4{width:16px}.scalar-app .w-5{width:20px}.scalar-app .w-6{width:24px}.scalar-app .w-7{width:28px}.scalar-app .w-8{width:32px}.scalar-app .w-10{width:40px}.scalar-app .w-20{width:80px}.scalar-app .w-56{width:224px}.scalar-app .w-64{width:256px}.scalar-app .w-72{width:288px}.scalar-app .w-\\\\[60px\\\\]{width:60px}.scalar-app .w-\\\\[calc\\\\(100\\\\%-10px\\\\)\\\\]{width:calc(100% - 10px)}.scalar-app .w-\\\\[calc\\\\(100\\\\%_-_8px\\\\)\\\\]{width:calc(100% - 8px)}.scalar-app .w-\\\\[inherit\\\\]{width:inherit}.scalar-app .w-auto{width:auto}.scalar-app .w-dvw{width:100dvw}.scalar-app .w-fit{width:fit-content}.scalar-app .w-full{width:100%}.scalar-app .w-max{width:max-content}.scalar-app .max-w-8{max-width:32px}.scalar-app .max-w-40{max-width:160px}.scalar-app .max-w-\\\\[14px\\\\]{max-width:14px}.scalar-app .max-w-\\\\[37px\\\\]{max-width:37px}.scalar-app .max-w-\\\\[100\\\\%\\\\]{max-width:100%}.scalar-app .max-w-\\\\[150px\\\\]{max-width:150px}.scalar-app .max-w-\\\\[380px\\\\]{max-width:380px}.scalar-app .max-w-\\\\[420px\\\\]{max-width:420px}.scalar-app .max-w-\\\\[720px\\\\]{max-width:720px}.scalar-app .max-w-\\\\[calc\\\\(100dvw-24px\\\\)\\\\]{max-width:calc(100dvw - 24px)}.scalar-app .max-w-full{max-width:100%}.scalar-app .min-w-0{min-width:0}.scalar-app .min-w-2\\\\.25{min-width:9px}.scalar-app .min-w-3\\\\.5{min-width:14px}.scalar-app .min-w-4{min-width:16px}.scalar-app .min-w-8{min-width:32px}.scalar-app .min-w-32{min-width:128px}.scalar-app .min-w-48{min-width:192px}.scalar-app .min-w-\\\\[37px\\\\]{min-width:37px}.scalar-app .min-w-\\\\[296px\\\\]{min-width:296px}.scalar-app .min-w-fit{min-width:fit-content}.scalar-app .min-w-full{min-width:100%}.scalar-app .flex-1{flex:1}.scalar-app .flex-shrink{flex-shrink:1}.scalar-app .shrink-0{flex-shrink:0}.scalar-app .flex-grow{flex-grow:1}.scalar-app .-translate-x-1\\\\/2{--tw-translate-x:-50%;translate:var(--tw-translate-x)var(--tw-translate-y)}.scalar-app .translate-x-0{--tw-translate-x:0px;translate:var(--tw-translate-x)var(--tw-translate-y)}.scalar-app .translate-x-1\\\\/2{--tw-translate-x:50%;translate:var(--tw-translate-x)var(--tw-translate-y)}.scalar-app .translate-y-1\\\\/2{--tw-translate-y:50%;translate:var(--tw-translate-x)var(--tw-translate-y)}.scalar-app .scale-75{--tw-scale-x:75%;--tw-scale-y:75%;--tw-scale-z:75%;scale:var(--tw-scale-x)var(--tw-scale-y)}.scalar-app .rotate-90{rotate:90deg}.scalar-app .transform{transform:var(--tw-rotate-x,)var(--tw-rotate-y,)var(--tw-rotate-z,)var(--tw-skew-x,)var(--tw-skew-y,)}.scalar-app .cursor-auto{cursor:auto}.scalar-app .cursor-default{cursor:default}.scalar-app .cursor-grab{cursor:grab}.scalar-app .cursor-help{cursor:help}.scalar-app .cursor-not-allowed{cursor:not-allowed}.scalar-app .cursor-pointer{cursor:pointer}.scalar-app .cursor-text{cursor:text}.scalar-app .resize{resize:both}.scalar-app .resize-none{resize:none}.scalar-app .auto-rows-\\\\[32px\\\\]{grid-auto-rows:32px}.scalar-app .auto-rows-auto{grid-auto-rows:auto}.scalar-app .grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.scalar-app .grid-cols-\\\\[44px_1fr_repeat\\\\(3\\\\,auto\\\\)\\\\]{grid-template-columns:44px 1fr repeat(3,auto)}.scalar-app .grid-cols-\\\\[auto_1fr\\\\]{grid-template-columns:auto 1fr}.scalar-app .grid-cols-\\\\[repeat\\\\(auto-fill\\\\,minmax\\\\(32px\\\\,1fr\\\\)\\\\)\\\\]{grid-template-columns:repeat(auto-fill,minmax(32px,1fr))}.scalar-app .\\\\!flex-col{flex-direction:column!important}.scalar-app .flex-col{flex-direction:column}.scalar-app .flex-row{flex-direction:row}.scalar-app .flex-wrap{flex-wrap:wrap}.scalar-app .content-between{align-content:space-between}.scalar-app .content-start{align-content:flex-start}.scalar-app .items-center{align-items:center}.scalar-app .items-end{align-items:flex-end}.scalar-app .items-start{align-items:flex-start}.scalar-app .items-stretch{align-items:stretch}.scalar-app .justify-between{justify-content:space-between}.scalar-app .justify-center{justify-content:center}.scalar-app .justify-end{justify-content:flex-end}.scalar-app .justify-start{justify-content:flex-start}.scalar-app .justify-stretch{justify-content:stretch}.scalar-app .\\\\!gap-2{gap:8px!important}.scalar-app .gap-0\\\\.5{gap:2px}.scalar-app .gap-0\\\\.75{gap:3px}.scalar-app .gap-1{gap:4px}.scalar-app .gap-1\\\\.5{gap:6px}.scalar-app .gap-1\\\\.75{gap:7px}.scalar-app .gap-2{gap:8px}.scalar-app .gap-2\\\\.5{gap:10px}.scalar-app .gap-3{gap:12px}.scalar-app .gap-4{gap:16px}.scalar-app .gap-6{gap:24px}.scalar-app .gap-8{gap:32px}.scalar-app .gap-10{gap:40px}.scalar-app .gap-12{gap:48px}.scalar-app .gap-\\\\[1\\\\.5px\\\\]{gap:1.5px}.scalar-app .gap-px{gap:1px}.scalar-app .gap-x-2\\\\.5{column-gap:10px}:where(.scalar-app .space-x-1>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(4px*var(--tw-space-x-reverse));margin-inline-end:calc(4px*calc(1 - var(--tw-space-x-reverse)))}:where(.scalar-app .divide-y>:not(:last-child)){--tw-divide-y-reverse:0;border-bottom-style:var(--tw-border-style);border-top-style:var(--tw-border-style);border-top-width:calc(var(--scalar-border-width)*var(--tw-divide-y-reverse));border-bottom-width:calc(var(--scalar-border-width)*calc(1 - var(--tw-divide-y-reverse)))}.scalar-app .self-center{align-self:center}.scalar-app .truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.scalar-app .overflow-auto{overflow:auto}.scalar-app .overflow-hidden{overflow:hidden}.scalar-app .overflow-visible{overflow:visible}.scalar-app .overflow-x-auto{overflow-x:auto}.scalar-app .overflow-y-auto{overflow-y:auto}.scalar-app .overflow-y-hidden{overflow-y:hidden}.scalar-app .\\\\!rounded-none{border-radius:0!important}.scalar-app .rounded{border-radius:var(--scalar-radius)}.scalar-app .rounded-\\\\[10px\\\\]{border-radius:10px}.scalar-app .rounded-full{border-radius:9999px}.scalar-app .rounded-lg{border-radius:var(--scalar-radius-lg)}.scalar-app .rounded-md{border-radius:var(--scalar-radius)}.scalar-app .rounded-px{border-radius:1px}.scalar-app .rounded-xl{border-radius:var(--scalar-radius-xl)}.scalar-app .rounded-t{border-top-left-radius:var(--scalar-radius);border-top-right-radius:var(--scalar-radius)}.scalar-app .rounded-t-lg{border-top-left-radius:var(--scalar-radius-lg);border-top-right-radius:var(--scalar-radius-lg)}.scalar-app .rounded-t-none{border-top-left-radius:0;border-top-right-radius:0}.scalar-app .rounded-b{border-bottom-right-radius:var(--scalar-radius);border-bottom-left-radius:var(--scalar-radius)}.scalar-app .rounded-b-lg{border-bottom-right-radius:var(--scalar-radius-lg);border-bottom-left-radius:var(--scalar-radius-lg)}.scalar-app .\\\\!border-0{border-style:var(--tw-border-style)!important;border-width:0!important}.scalar-app .border{border-style:var(--tw-border-style);border-width:var(--scalar-border-width)}.scalar-app .border-0{border-style:var(--tw-border-style);border-width:0}.scalar-app .border-\\\\[1\\\\.5px\\\\]{border-style:var(--tw-border-style);border-width:1.5px}.scalar-app .border-\\\\[1px\\\\]{border-style:var(--tw-border-style);border-width:1px}.scalar-app .border-x{border-inline-style:var(--tw-border-style);border-inline-width:var(--scalar-border-width)}.scalar-app .border-x-0{border-inline-style:var(--tw-border-style);border-inline-width:0}.scalar-app .border-y{border-block-style:var(--tw-border-style);border-block-width:var(--scalar-border-width)}.scalar-app .border-t{border-top-style:var(--tw-border-style);border-top-width:var(--scalar-border-width)}.scalar-app .border-t-0{border-top-style:var(--tw-border-style);border-top-width:0}.scalar-app .\\\\!border-r{border-right-style:var(--tw-border-style)!important;border-right-width:var(--scalar-border-width)!important}.scalar-app .border-r{border-right-style:var(--tw-border-style);border-right-width:var(--scalar-border-width)}.scalar-app .border-r-0{border-right-style:var(--tw-border-style);border-right-width:0}.scalar-app .border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:var(--scalar-border-width)}.scalar-app .border-b-0{border-bottom-style:var(--tw-border-style);border-bottom-width:0}.scalar-app .border-b-\\\\[1px\\\\]{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.scalar-app .border-l{border-left-style:var(--tw-border-style);border-left-width:var(--scalar-border-width)}.scalar-app .border-l-0{border-left-style:var(--tw-border-style);border-left-width:0}.scalar-app .\\\\!border-none{--tw-border-style:none!important;border-style:none!important}.scalar-app .border-dashed{--tw-border-style:dashed;border-style:dashed}.scalar-app .border-none{--tw-border-style:none;border-style:none}.scalar-app .\\\\!border-current{border-color:currentColor!important}.scalar-app .border-c-1{border-color:var(--scalar-color-1)}.scalar-app .border-c-3{border-color:var(--scalar-color-3)}.scalar-app .border-transparent{border-color:#0000}.scalar-app .border-r-transparent{border-right-color:#0000}.scalar-app .bg-b-1{background-color:var(--scalar-background-1)}.scalar-app .bg-b-2{background-color:var(--scalar-background-2)}.scalar-app .bg-b-3{background-color:var(--scalar-background-3)}.scalar-app .bg-b-danger{background-color:var(--scalar-background-danger)}.scalar-app .bg-c-3\\\\/5{background-color:var(--scalar-color-3)}@supports (color:color-mix(in lab,red,red)){.scalar-app .bg-c-3\\\\/5{background-color:var(--scalar-color-3)}@supports (color:color-mix(in lab,red,red)){.scalar-app .bg-c-3\\\\/5{background-color:color-mix(in oklab,var(--scalar-color-3)5%,transparent)}}}.scalar-app .bg-c-accent{background-color:var(--scalar-color-accent)}.scalar-app .bg-current{background-color:currentColor}.scalar-app .bg-grey{background-color:var(--scalar-color-3)}.scalar-app .bg-red{background-color:var(--scalar-color-red)}.scalar-app .bg-sidebar-b-1{background-color:var(--scalar-sidebar-background-1,var(--scalar-background-1))}.scalar-app .bg-sidebar-b-active{background-color:var(--scalar-sidebar-item-active-background,var(--scalar-background-2))}.scalar-app .bg-none{background-image:none}.scalar-app .fill-current{fill:currentColor}.scalar-app .stroke-2{stroke-width:2px}.scalar-app .stroke-\\\\[1\\\\.5\\\\]{stroke-width:1.5px}.scalar-app .stroke-\\\\[1\\\\.75\\\\]{stroke-width:1.75px}.scalar-app .stroke-\\\\[2\\\\.25\\\\]{stroke-width:2.25px}.scalar-app .object-contain{object-fit:contain}.scalar-app .\\\\!p-0{padding:0!important}.scalar-app .p-0{padding:0}.scalar-app .p-0\\\\.5{padding:2px}.scalar-app .p-0\\\\.75{padding:3px}.scalar-app .p-1{padding:4px}.scalar-app .p-1\\\\.5{padding:6px}.scalar-app .p-1\\\\.25{padding:5px}.scalar-app .p-1\\\\.75{padding:7px}.scalar-app .p-2{padding:8px}.scalar-app .p-3{padding:12px}.scalar-app .p-4{padding:16px}.scalar-app .p-\\\\[3px\\\\]{padding:3px}.scalar-app .p-\\\\[5px\\\\]{padding:5px}.scalar-app .p-px{padding:1px}.scalar-app .\\\\!px-3{padding-inline:12px!important}.scalar-app .px-0{padding-inline:0}.scalar-app .px-0\\\\.5{padding-inline:2px}.scalar-app .px-0\\\\.75{padding-inline:3px}.scalar-app .px-1{padding-inline:4px}.scalar-app .px-1\\\\.5{padding-inline:6px}.scalar-app .px-1\\\\.25{padding-inline:5px}.scalar-app .px-2{padding-inline:8px}.scalar-app .px-2\\\\.5{padding-inline:10px}.scalar-app .px-3{padding-inline:12px}.scalar-app .px-4{padding-inline:16px}.scalar-app .px-5{padding-inline:20px}.scalar-app .px-6{padding-inline:24px}.scalar-app .px-8{padding-inline:32px}.scalar-app .\\\\!py-1\\\\.5{padding-block:6px!important}.scalar-app .py-0{padding-block:0}.scalar-app .py-0\\\\.5{padding-block:2px}.scalar-app .py-0\\\\.25{padding-block:1px}.scalar-app .py-0\\\\.75{padding-block:3px}.scalar-app .py-1{padding-block:4px}.scalar-app .py-1\\\\.5{padding-block:6px}.scalar-app .py-1\\\\.25{padding-block:5px}.scalar-app .py-1\\\\.75{padding-block:7px}.scalar-app .py-2{padding-block:8px}.scalar-app .py-2\\\\.5{padding-block:10px}.scalar-app .py-3{padding-block:12px}.scalar-app .py-5{padding-block:20px}.scalar-app .py-8{padding-block:32px}.scalar-app .py-15{padding-block:60px}.scalar-app .py-px{padding-block:1px}.scalar-app .\\\\!pt-0{padding-top:0!important}.scalar-app .pt-0{padding-top:0}.scalar-app .pt-2{padding-top:8px}.scalar-app .pt-3{padding-top:12px}.scalar-app .pt-4{padding-top:16px}.scalar-app .pt-6{padding-top:24px}.scalar-app .pt-8{padding-top:32px}.scalar-app .pt-px{padding-top:1px}.scalar-app .pr-0{padding-right:0}.scalar-app .pr-0\\\\.75{padding-right:3px}.scalar-app .pr-1{padding-right:4px}.scalar-app .pr-1\\\\.5{padding-right:6px}.scalar-app .pr-2{padding-right:8px}.scalar-app .pr-2\\\\.5{padding-right:10px}.scalar-app .pr-2\\\\.25{padding-right:9px}.scalar-app .pr-3{padding-right:12px}.scalar-app .pr-6{padding-right:24px}.scalar-app .pr-8{padding-right:32px}.scalar-app .pr-9{padding-right:36px}.scalar-app .pr-10{padding-right:40px}.scalar-app .pr-12{padding-right:48px}.scalar-app .pr-\\\\[26px\\\\]{padding-right:26px}.scalar-app .pb-0{padding-bottom:0}.scalar-app .pb-1\\\\.5{padding-bottom:6px}.scalar-app .pb-2{padding-bottom:8px}.scalar-app .pb-3{padding-bottom:12px}.scalar-app .pb-5{padding-bottom:20px}.scalar-app .pb-6{padding-bottom:24px}.scalar-app .pb-8{padding-bottom:32px}.scalar-app .pb-14{padding-bottom:56px}.scalar-app .pb-\\\\[75px\\\\]{padding-bottom:75px}.scalar-app .\\\\!pl-3{padding-left:12px!important}.scalar-app .pl-1{padding-left:4px}.scalar-app .pl-1\\\\.5{padding-left:6px}.scalar-app .pl-1\\\\.25{padding-left:5px}.scalar-app .pl-2{padding-left:8px}.scalar-app .pl-3{padding-left:12px}.scalar-app .pl-5{padding-left:20px}.scalar-app .pl-6{padding-left:24px}.scalar-app .pl-8\\\\.5{padding-left:34px}.scalar-app .pl-9{padding-left:36px}.scalar-app .pl-12{padding-left:48px}.scalar-app .pl-px{padding-left:1px}.scalar-app .text-center{text-align:center}.scalar-app .text-left{text-align:left}.scalar-app .text-right{text-align:right}.scalar-app .font-code{font-family:var(--scalar-font-code)}.scalar-app .font-sans{font-family:var(--scalar-font)}.scalar-app .text-3xs{font-size:var(--scalar-font-size-7)}.scalar-app .text-\\\\[6px\\\\]{font-size:6px}.scalar-app .text-\\\\[11px\\\\]{font-size:11px}.scalar-app .text-\\\\[21px\\\\]{font-size:21px}.scalar-app .text-base{font-size:var(--scalar-font-size-3)}.scalar-app .text-sm{font-size:var(--scalar-font-size-4)}.scalar-app .text-xl{font-size:var(--scalar-font-size-1)}.scalar-app .text-xs{font-size:var(--scalar-font-size-5)}.scalar-app .text-xxs{font-size:var(--scalar-font-size-6)}.scalar-app .\\\\!leading-\\\\[6px\\\\]{--tw-leading:6px!important;line-height:6px!important}.scalar-app .leading-2{--tw-leading:var(--scalar-line-height-2);line-height:var(--scalar-line-height-2)}.scalar-app .leading-3{--tw-leading:var(--scalar-line-height-3);line-height:var(--scalar-line-height-3)}.scalar-app .leading-\\\\[1\\\\.44\\\\]{--tw-leading:1.44;line-height:1.44}.scalar-app .leading-\\\\[7px\\\\]{--tw-leading:7px;line-height:7px}.scalar-app .leading-\\\\[20px\\\\]{--tw-leading:20px;line-height:20px}.scalar-app .leading-\\\\[21px\\\\]{--tw-leading:21px;line-height:21px}.scalar-app .leading-\\\\[22px\\\\]{--tw-leading:22px;line-height:22px}.scalar-app .leading-\\\\[normal\\\\]{--tw-leading:normal;line-height:normal}.scalar-app .leading-none{--tw-leading:1;line-height:1}.scalar-app .leading-normal{--tw-leading:var(--leading-normal);line-height:var(--leading-normal)}.scalar-app .leading-snug{--tw-leading:var(--leading-snug);line-height:var(--leading-snug)}.scalar-app .font-bold{--tw-font-weight:var(--scalar-bold);font-weight:var(--scalar-bold)}.scalar-app .font-medium{--tw-font-weight:var(--scalar-semibold);font-weight:var(--scalar-semibold)}.scalar-app .font-normal{--tw-font-weight:var(--scalar-regular);font-weight:var(--scalar-regular)}.scalar-app .text-balance{text-wrap:balance}.scalar-app .text-pretty{text-wrap:pretty}.scalar-app .break-words{overflow-wrap:break-word}.scalar-app .break-all{word-break:break-all}.scalar-app .text-ellipsis{text-overflow:ellipsis}.scalar-app .whitespace-nowrap{white-space:nowrap}.scalar-app .whitespace-pre{white-space:pre}.scalar-app .whitespace-pre-wrap{white-space:pre-wrap}.scalar-app .\\\\!text-c-1{color:var(--scalar-color-1)!important}.scalar-app .text-b-1{color:var(--scalar-background-1)}.scalar-app .text-blue{color:var(--scalar-color-blue)}.scalar-app .text-border{color:var(--scalar-border-color)}.scalar-app .text-c-1{color:var(--scalar-color-1)}.scalar-app .text-c-2{color:var(--scalar-color-2)}.scalar-app .text-c-3{color:var(--scalar-color-3)}.scalar-app .text-c-btn{color:var(--scalar-button-1-color)}.scalar-app .text-green{color:var(--scalar-color-green)}.scalar-app .text-grey{color:var(--scalar-color-3)}.scalar-app .text-orange{color:var(--scalar-color-orange)}.scalar-app .text-purple{color:var(--scalar-color-purple)}.scalar-app .text-red{color:var(--scalar-color-red)}.scalar-app .text-sidebar-c-2{color:var(--scalar-sidebar-color-2,var(--scalar-color-2))}.scalar-app .text-sidebar-c-active{color:var(--scalar-sidebar-color-active,currentColor)}.scalar-app .text-transparent{color:#0000}.scalar-app .text-yellow{color:var(--scalar-color-yellow)}.scalar-app .capitalize{text-transform:capitalize}.scalar-app .lowercase{text-transform:lowercase}.scalar-app .uppercase{text-transform:uppercase}.scalar-app .no-underline{text-decoration-line:none}.scalar-app .underline{text-decoration-line:underline}.scalar-app .decoration-c-3{-webkit-text-decoration-color:var(--scalar-color-3);text-decoration-color:var(--scalar-color-3)}.scalar-app .underline-offset-2{text-underline-offset:2px}.scalar-app .opacity-0{opacity:0}.scalar-app .opacity-50{opacity:.5}.scalar-app .opacity-100{opacity:1}.scalar-app .bg-blend-normal{background-blend-mode:normal}.scalar-app .mix-blend-luminosity{mix-blend-mode:luminosity}.scalar-app .shadow{--tw-shadow:var(--scalar-shadow-1);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.scalar-app .shadow-\\\\[-8px_0_4px_var\\\\(--scalar-background-1\\\\)\\\\]{--tw-shadow:-8px 0 4px var(--tw-shadow-color,var(--scalar-background-1));box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.scalar-app .shadow-\\\\[0_-8px_0_8px_var\\\\(--scalar-background-1\\\\)\\\\,0_0_8px_8px_var\\\\(--scalar-background-1\\\\)\\\\]{--tw-shadow:0 -8px 0 8px var(--tw-shadow-color,var(--scalar-background-1)),0 0 8px 8px var(--tw-shadow-color,var(--scalar-background-1));box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.scalar-app .shadow-border{--tw-shadow:inset 0 0 0 var(--tw-shadow-color,calc(var(--scalar-border-width)*2))var(--scalar-border-color);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.scalar-app .shadow-lg{--tw-shadow:var(--scalar-shadow-2);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.scalar-app .shadow-none{--tw-shadow:0 0 #0000;box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.scalar-app .outline{outline-style:var(--tw-outline-style);outline-width:1px}.scalar-app .-outline-offset-1{outline-offset:-1px}.scalar-app .-outline-offset-2{outline-offset:-2px}.scalar-app .outline-offset-2{outline-offset:2px}.scalar-app .outline-b-3{outline-color:var(--scalar-background-3)}.scalar-app .blur{--tw-blur:blur(8px);filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.scalar-app .brightness-90{--tw-brightness:brightness(90%);filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.scalar-app .brightness-\\\\[\\\\.9\\\\]{--tw-brightness:brightness(.9);filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.scalar-app .brightness-lifted{--tw-brightness:brightness(var(--scalar-lifted-brightness));filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.scalar-app .filter{filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.scalar-app .backdrop-filter{-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.scalar-app .transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,visibility,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.scalar-app .transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.scalar-app .transition-opacity{transition-property:opacity;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.scalar-app .transition-transform{transition-property:transform,translate,scale,rotate;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.scalar-app .transition-none{transition-property:none}.scalar-app .duration-100{--tw-duration:.1s;transition-duration:.1s}.scalar-app .duration-150{--tw-duration:.15s;transition-duration:.15s}.scalar-app .duration-200{--tw-duration:.2s;transition-duration:.2s}.scalar-app .duration-300{--tw-duration:.3s;transition-duration:.3s}.scalar-app .ease-in-out{--tw-ease:var(--ease-in-out);transition-timing-function:var(--ease-in-out)}.scalar-app .outline-none{--tw-outline-style:none;outline-style:none}.scalar-app .select-none{-webkit-user-select:none;user-select:none}.scalar-app .\\\\[--scalar-address-bar-height\\\\:32px\\\\]{--scalar-address-bar-height:32px}.scalar-app .app-drag-region{-webkit-app-region:drag}.scalar-app .app-no-drag-region{-webkit-app-region:no-drag}:is(.scalar-app .\\\\*\\\\:flex>*){display:flex}:is(.scalar-app .\\\\*\\\\:h-8>*){height:32px}:is(.scalar-app .\\\\*\\\\:cursor-pointer>*){cursor:pointer}:is(.scalar-app .\\\\*\\\\:items-center>*){align-items:center}:is(.scalar-app .\\\\*\\\\:rounded-none>*){border-radius:0}:is(.scalar-app .\\\\*\\\\:border-t>*){border-top-style:var(--tw-border-style);border-top-width:var(--scalar-border-width)}:is(.scalar-app .\\\\*\\\\:border-b-0>*){border-bottom-style:var(--tw-border-style);border-bottom-width:0}:is(.scalar-app .\\\\*\\\\:px-1\\\\.5>*){padding-inline:6px}:is(.scalar-app .\\\\*\\\\:pl-4>*){padding-left:16px}.scalar-app .group-first\\\\/row\\\\:border-t-0:is(:where(.group\\\\/row):first-child *){border-top-style:var(--tw-border-style);border-top-width:0}.scalar-app .group-last\\\\:border-b-transparent:is(:where(.group):last-child *){border-bottom-color:#0000}.scalar-app .group-last\\\\/label\\\\:rounded-br-lg:is(:where(.group\\\\/label):last-child *){border-bottom-right-radius:var(--scalar-radius-lg)}.scalar-app .group-focus-within\\\\:flex:is(:where(.group):focus-within *){display:flex}@media (hover:hover){.scalar-app .group-hover\\\\:block:is(:where(.group):hover *){display:block}.scalar-app .group-hover\\\\:flex:is(:where(.group):hover *){display:flex}.scalar-app .group-hover\\\\:hidden:is(:where(.group):hover *){display:none}.scalar-app .group-hover\\\\:inline:is(:where(.group):hover *){display:inline}.scalar-app .group-hover\\\\:pr-5:is(:where(.group):hover *){padding-right:20px}.scalar-app .group-hover\\\\:pr-6:is(:where(.group):hover *){padding-right:24px}.scalar-app .group-hover\\\\:pr-10:is(:where(.group):hover *){padding-right:40px}.scalar-app .group-hover\\\\:text-c-1:is(:where(.group):hover *){color:var(--scalar-color-1)}.scalar-app .group-hover\\\\:opacity-80:is(:where(.group):hover *){opacity:.8}.scalar-app .group-hover\\\\:opacity-100:is(:where(.group):hover *){opacity:1}.scalar-app .group-hover\\\\/auth\\\\:absolute:is(:where(.group\\\\/auth):hover *){position:absolute}.scalar-app .group-hover\\\\/auth\\\\:h-auto:is(:where(.group\\\\/auth):hover *){height:auto}.scalar-app .group-hover\\\\/auth\\\\:border-b:is(:where(.group\\\\/auth):hover *){border-bottom-style:var(--tw-border-style);border-bottom-width:var(--scalar-border-width)}.scalar-app .group-hover\\\\/cell\\\\:opacity-100:is(:where(.group\\\\/cell):hover *){opacity:1}.scalar-app .group-hover\\\\/item\\\\:flex:is(:where(.group\\\\/item):hover *){display:flex}.scalar-app .group-hover\\\\/item\\\\:opacity-100:is(:where(.group\\\\/item):hover *),.scalar-app .group-hover\\\\/params\\\\:opacity-100:is(:where(.group\\\\/params):hover *){opacity:1}.scalar-app .group-hover\\\\/row\\\\:flex:is(:where(.group\\\\/row):hover *){display:flex}.scalar-app .group-hover\\\\/scopes-accordion\\\\:text-c-2:is(:where(.group\\\\/scopes-accordion):hover *){color:var(--scalar-color-2)}.scalar-app .group-hover\\\\/upload\\\\:block:is(:where(.group\\\\/upload):hover *){display:block}}.scalar-app .group-focus-visible\\\\:opacity-100:is(:where(.group):focus-visible *){opacity:1}.scalar-app .group-focus-visible\\\\:outline:is(:where(.group):focus-visible *){outline-style:var(--tw-outline-style);outline-width:1px}.scalar-app .group-has-\\\\[\\\\.cm-focused\\\\]\\\\:z-1:is(:where(.group):has(.cm-focused) *){z-index:1}.scalar-app .group-has-\\\\[\\\\.cm-focused\\\\]\\\\:flex:is(:where(.group):has(.cm-focused) *){display:flex}.scalar-app .group-has-\\\\[\\\\.cm-focused\\\\]\\\\:pr-6:is(:where(.group):has(.cm-focused) *){padding-right:24px}.scalar-app .group-has-\\\\[\\\\.cm-focused\\\\]\\\\:pr-10:is(:where(.group):has(.cm-focused) *){padding-right:40px}.scalar-app .group-has-\\\\[\\\\:focus-visible\\\\]\\\\:hidden:is(:where(.group):has(:focus-visible) *){display:none}.scalar-app .group-has-\\\\[\\\\:focus-visible\\\\]\\\\:opacity-100:is(:where(.group):has(:focus-visible) *){opacity:1}.scalar-app .group-has-\\\\[\\\\:focus-visible\\\\]\\\\/cell\\\\:border-c-accent:is(:where(.group\\\\/cell):has(:focus-visible) *){border-color:var(--scalar-color-accent)}.scalar-app .group-has-\\\\[\\\\:focus-visible\\\\]\\\\/cell\\\\:opacity-100:is(:where(.group\\\\/cell):has(:focus-visible) *){opacity:1}.scalar-app .group-has-\\\\[\\\\:focus-visible\\\\]\\\\/input\\\\:block:is(:where(.group\\\\/input):has(:focus-visible) *){display:block}.scalar-app .group-has-\\\\[input\\\\]\\\\/label\\\\:mr-0:is(:where(.group\\\\/label):has(:is(input)) *){margin-right:0}.scalar-app .group-aria-expanded\\\\/button\\\\:rotate-180:is(:where(.group\\\\/button)[aria-expanded=true] *),.scalar-app .group-aria-expanded\\\\/combobox-button\\\\:rotate-180:is(:where(.group\\\\/combobox-button)[aria-expanded=true] *){rotate:180deg}.scalar-app .group-\\\\[\\\\.alert\\\\]\\\\:bg-b-alert:is(:where(.group).alert *){background-color:var(--scalar-background-alert)}.scalar-app .group-\\\\[\\\\.alert\\\\]\\\\:bg-transparent:is(:where(.group).alert *){background-color:#0000}.scalar-app .group-\\\\[\\\\.alert\\\\]\\\\:shadow-none:is(:where(.group).alert *){--tw-shadow:0 0 #0000;box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.scalar-app .group-\\\\[\\\\.alert\\\\]\\\\:outline-orange:is(:where(.group).alert *){outline-color:var(--scalar-color-orange)}.scalar-app .group-\\\\[\\\\.error\\\\]\\\\:bg-b-danger:is(:where(.group).error *){background-color:var(--scalar-background-danger)}.scalar-app .group-\\\\[\\\\.error\\\\]\\\\:bg-transparent:is(:where(.group).error *){background-color:#0000}.scalar-app .group-\\\\[\\\\.error\\\\]\\\\:text-red:is(:where(.group).error *){color:var(--scalar-color-red)}.scalar-app .group-\\\\[\\\\.error\\\\]\\\\:shadow-none:is(:where(.group).error *){--tw-shadow:0 0 #0000;box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.scalar-app .group-\\\\[\\\\.error\\\\]\\\\:outline-red:is(:where(.group).error *){outline-color:var(--scalar-color-red)}.scalar-app .peer-checked\\\\:text-c-1:is(:where(.peer):checked~*){color:var(--scalar-color-1)}.scalar-app .peer-has-\\\\[\\\\.cm-focused\\\\]\\\\:opacity-0:is(:where(.peer):has(.cm-focused)~*){opacity:0}.scalar-app .peer-has-\\\\[\\\\.color-selector\\\\]\\\\:hidden:is(:where(.peer):has(.color-selector)~*){display:none}.scalar-app .before\\\\:pointer-events-none:before{content:var(--tw-content);pointer-events:none}.scalar-app .before\\\\:absolute:before{content:var(--tw-content);position:absolute}.scalar-app .before\\\\:top-0:before{content:var(--tw-content);top:0}.scalar-app .before\\\\:left-3:before{content:var(--tw-content);left:12px}.scalar-app .before\\\\:left-\\\\[calc\\\\(\\\\.75rem_\\\\+_\\\\.5px\\\\)\\\\]:before{content:var(--tw-content);left:calc(.75rem + .5px)}.scalar-app .before\\\\:z-1:before{content:var(--tw-content);z-index:1}.scalar-app .before\\\\:h-\\\\[calc\\\\(100\\\\%_\\\\+_\\\\.5px\\\\)\\\\]:before{content:var(--tw-content);height:calc(100% + .5px)}.scalar-app .before\\\\:w-\\\\[\\\\.5px\\\\]:before{content:var(--tw-content);width:.5px}.scalar-app .before\\\\:bg-border:before{content:var(--tw-content);background-color:var(--scalar-border-color)}.scalar-app .after\\\\:content-\\\\[\\\\\\'\\\\:\\\\\\'\\\\]:after{--tw-content:\":\";content:var(--tw-content)}:is(.scalar-app .\\\\*\\\\:first\\\\:line-clamp-1>*):first-child{-webkit-line-clamp:1;-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden}:is(.scalar-app .\\\\*\\\\:first\\\\:rounded-l>*):first-child{border-top-left-radius:var(--scalar-radius);border-bottom-left-radius:var(--scalar-radius)}:is(.scalar-app .\\\\*\\\\:first\\\\:border-t-0>*):first-child,:is(.scalar-app .first\\\\:\\\\*\\\\:border-t-0:first-child>*){border-top-style:var(--tw-border-style);border-top-width:0}:is(.scalar-app .\\\\*\\\\:first\\\\:text-ellipsis>*):first-child{text-overflow:ellipsis}@media (hover:hover){:is(.scalar-app .group-hover\\\\/auth\\\\:\\\\*\\\\:first\\\\:line-clamp-none:is(:where(.group\\\\/auth):hover *)>*):first-child{-webkit-line-clamp:unset;-webkit-box-orient:horizontal;display:block;overflow:visible}}.scalar-app .last\\\\:mb-0:last-child{margin-bottom:0}.scalar-app .last\\\\:rounded-b-lg:last-child{border-bottom-right-radius:var(--scalar-radius-lg);border-bottom-left-radius:var(--scalar-radius-lg)}.scalar-app .last\\\\:border-r-0:last-child{border-right-style:var(--tw-border-style);border-right-width:0}:is(.scalar-app .\\\\*\\\\:last\\\\:rounded-r>*):last-child{border-top-right-radius:var(--scalar-radius);border-bottom-right-radius:var(--scalar-radius)}.scalar-app .last\\\\:before\\\\:h-full:last-child:before{content:var(--tw-content);height:100%}.scalar-app .last-of-type\\\\:first-of-type\\\\:border-b-0:last-of-type:first-of-type{border-bottom-style:var(--tw-border-style);border-bottom-width:0}.scalar-app .focus-within\\\\:z-20:focus-within{z-index:20}.scalar-app .focus-within\\\\:border-\\\\(--scalar-background-3\\\\):focus-within{border-color:var(--scalar-background-3)}.scalar-app .focus-within\\\\:bg-b-1:focus-within{background-color:var(--scalar-background-1)}.scalar-app .focus-within\\\\:text-c-1:focus-within{color:var(--scalar-color-1)}@media (hover:hover){.scalar-app .hover\\\\:cursor-default:hover{cursor:default}.scalar-app .hover\\\\:border-\\\\(--scalar-background-3\\\\):hover{border-color:var(--scalar-background-3)}.scalar-app .hover\\\\:border-inherit:hover{border-color:inherit}.scalar-app .hover\\\\:bg-b-2:hover{background-color:var(--scalar-background-2)}.scalar-app .hover\\\\:bg-b-3:hover{background-color:var(--scalar-background-3)}.scalar-app .hover\\\\:bg-inherit:hover{background-color:inherit}.scalar-app .hover\\\\:bg-sidebar-b-active:hover{background-color:var(--scalar-sidebar-item-active-background,var(--scalar-background-2))}.scalar-app .hover\\\\:whitespace-normal:hover{white-space:normal}.scalar-app .hover\\\\:text-c-1:hover{color:var(--scalar-color-1)}.scalar-app .hover\\\\:text-c-2:hover{color:var(--scalar-color-2)}.scalar-app .hover\\\\:underline:hover{text-decoration-line:underline}.scalar-app .hover\\\\:brightness-75:hover{--tw-brightness:brightness(75%);filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}}.scalar-app .focus\\\\:border-b-1:focus{border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:var(--scalar-background-1)}.scalar-app .focus\\\\:text-c-1:focus{color:var(--scalar-color-1)}.scalar-app .focus\\\\:outline-none:focus{--tw-outline-style:none;outline-style:none}.scalar-app .focus-visible\\\\:z-10:focus-visible{z-index:10}.scalar-app .active\\\\:text-c-1:active{color:var(--scalar-color-1)}.scalar-app .disabled\\\\:cursor-default:disabled{cursor:default}.scalar-app .disabled\\\\:text-c-2:disabled{color:var(--scalar-color-2)}.scalar-app .has-\\\\[\\\\.empty-sidebar-item\\\\]\\\\:border-t:has(.empty-sidebar-item){border-top-style:var(--tw-border-style);border-top-width:var(--scalar-border-width)}.scalar-app .has-\\\\[\\\\:focus-visible\\\\]\\\\:absolute:has(:focus-visible){position:absolute}.scalar-app .has-\\\\[\\\\:focus-visible\\\\]\\\\:z-1:has(:focus-visible){z-index:1}.scalar-app .has-\\\\[\\\\:focus-visible\\\\]\\\\:rounded-\\\\[4px\\\\]:has(:focus-visible){border-radius:4px}.scalar-app .has-\\\\[\\\\:focus-visible\\\\]\\\\:bg-b-1:has(:focus-visible){background-color:var(--scalar-background-1)}.scalar-app .has-\\\\[\\\\:focus-visible\\\\]\\\\:opacity-100:has(:focus-visible){opacity:1}.scalar-app .has-\\\\[\\\\:focus-visible\\\\]\\\\:outline:has(:focus-visible){outline-style:var(--tw-outline-style);outline-width:1px}@media (min-width:600px){.scalar-app .sm\\\\:not-sr-only{clip:auto;white-space:normal;width:auto;height:auto;margin:0;padding:0;position:static;overflow:visible}.scalar-app .sm\\\\:order-none{order:0}.scalar-app .sm\\\\:mr-1\\\\.5{margin-right:6px}.scalar-app .sm\\\\:mb-1\\\\.5{margin-bottom:6px}.scalar-app .sm\\\\:ml-1\\\\.5{margin-left:6px}.scalar-app .sm\\\\:flex{display:flex}.scalar-app .sm\\\\:hidden{display:none}.scalar-app .sm\\\\:max-w-max{max-width:max-content}.scalar-app .sm\\\\:min-w-max{min-width:max-content}.scalar-app .sm\\\\:flex-col{flex-direction:column}.scalar-app .sm\\\\:flex-row{flex-direction:row}.scalar-app .sm\\\\:justify-between{justify-content:space-between}.scalar-app .sm\\\\:gap-px{gap:1px}.scalar-app .sm\\\\:rounded{border-radius:var(--scalar-radius)}.scalar-app .sm\\\\:rounded-lg{border-radius:var(--scalar-radius-lg)}.scalar-app .sm\\\\:px-2{padding-inline:8px}.scalar-app .sm\\\\:px-3{padding-inline:12px}.scalar-app .sm\\\\:py-1\\\\.5{padding-block:6px}:is(.scalar-app .sm\\\\:\\\\*\\\\:rounded-lg>*){border-radius:var(--scalar-radius-lg)}}@media (min-width:800px){.scalar-app .md\\\\:right-10{right:40px}.scalar-app .md\\\\:bottom-10{bottom:40px}.scalar-app .md\\\\:mx-auto{margin-inline:auto}.scalar-app .md\\\\:-ml-1\\\\.25{margin-left:-5px}.scalar-app .md\\\\:ml-1\\\\.5{margin-left:6px}.scalar-app .md\\\\:block{display:block}.scalar-app .md\\\\:flex{display:flex}.scalar-app .md\\\\:grid{display:grid}.scalar-app .md\\\\:w-full{width:100%}.scalar-app .md\\\\:max-w-\\\\[720px\\\\]{max-width:720px}.scalar-app .md\\\\:min-w-fit{min-width:fit-content}.scalar-app .md\\\\:flex-none{flex:none}.scalar-app .md\\\\:translate-x-0{--tw-translate-x:0px;translate:var(--tw-translate-x)var(--tw-translate-y)}.scalar-app .md\\\\:translate-y-0{--tw-translate-y:0px;translate:var(--tw-translate-x)var(--tw-translate-y)}.scalar-app .md\\\\:grid-cols-\\\\[1fr_720px_1fr\\\\]{grid-template-columns:1fr 720px 1fr}.scalar-app .md\\\\:flex-row{flex-direction:row}.scalar-app .md\\\\:border-r{border-right-style:var(--tw-border-style);border-right-width:var(--scalar-border-width)}.scalar-app .md\\\\:border-b-0{border-bottom-style:var(--tw-border-style);border-bottom-width:0}.scalar-app .md\\\\:p-1\\\\.5{padding:6px}.scalar-app .md\\\\:px-0{padding-inline:0}.scalar-app .md\\\\:px-1\\\\.5{padding-inline:6px}.scalar-app .md\\\\:px-2{padding-inline:8px}.scalar-app .md\\\\:px-2\\\\.5{padding-inline:10px}.scalar-app .md\\\\:px-4{padding-inline:16px}.scalar-app .md\\\\:px-\\\\[18px\\\\]{padding-inline:18px}.scalar-app .md\\\\:py-2\\\\.5{padding-block:10px}.scalar-app .md\\\\:pb-2\\\\.5{padding-bottom:10px}.scalar-app .md\\\\:pb-\\\\[37px\\\\]{padding-bottom:37px}.scalar-app .md\\\\:pl-0{padding-left:0}:is(.scalar-app .md\\\\:\\\\*\\\\:border-t-0>*){border-top-style:var(--tw-border-style);border-top-width:0}}@media (min-width:1000px){.scalar-app .lg\\\\:order-none{order:0}.scalar-app .lg\\\\:-mr-1{margin-right:-4px}.scalar-app .lg\\\\:mb-0{margin-bottom:0}.scalar-app .lg\\\\:flex{display:flex}.scalar-app .lg\\\\:min-h-header{min-height:48px}.scalar-app .lg\\\\:w-auto{width:auto}.scalar-app .lg\\\\:max-w-\\\\[580px\\\\]{max-width:580px}.scalar-app .lg\\\\:min-w-\\\\[580px\\\\]{min-width:580px}.scalar-app .lg\\\\:flex-1{flex:1}.scalar-app .lg\\\\:p-1{padding:4px}.scalar-app .lg\\\\:px-1{padding-inline:4px}.scalar-app .lg\\\\:px-2\\\\.5{padding-inline:10px}.scalar-app .lg\\\\:pt-1{padding-top:4px}.scalar-app .lg\\\\:pr-24{padding-right:96px}}@media (min-width:1200px){.scalar-app .xl\\\\:\\\\!flex{display:flex!important}.scalar-app .xl\\\\:flex{display:flex}.scalar-app .xl\\\\:hidden{display:none}.scalar-app .xl\\\\:h-fit{height:fit-content}.scalar-app .xl\\\\:h-full{height:100%}.scalar-app .xl\\\\:min-h-header{min-height:48px}.scalar-app .xl\\\\:max-w-\\\\[720px\\\\]{max-width:720px}.scalar-app .xl\\\\:min-w-0{min-width:0}.scalar-app .xl\\\\:min-w-\\\\[720px\\\\]{min-width:720px}.scalar-app .xl\\\\:flex-row{flex-direction:row}.scalar-app .xl\\\\:overflow-auto{overflow:auto}.scalar-app .xl\\\\:overflow-hidden{overflow:hidden}.scalar-app .xl\\\\:rounded-none{border-radius:0}.scalar-app .xl\\\\:pr-0\\\\.5{padding-right:2px}.scalar-app .xl\\\\:pl-2{padding-left:8px}:is(.scalar-app .\\\\*\\\\:xl\\\\:border-t-0>*){border-top-style:var(--tw-border-style);border-top-width:0}:is(.scalar-app .\\\\*\\\\:xl\\\\:border-l>*){border-left-style:var(--tw-border-style);border-left-width:var(--scalar-border-width)}:is(.scalar-app .\\\\*\\\\:first\\\\:xl\\\\:border-l-0>*):first-child{border-left-style:var(--tw-border-style);border-left-width:0}}.scalar-app .dark\\\\:bg-b-2:where(.dark-mode,.dark-mode *){background-color:var(--scalar-background-2)}@media (hover:hover){.scalar-app .hover\\\\:dark\\\\:bg-b-2:hover:where(.dark-mode,.dark-mode *){background-color:var(--scalar-background-2)}}.scalar-app .ui-open\\\\:rotate-90[data-headlessui-state~=open],:where([data-headlessui-state~=open]) :is(.scalar-app .ui-open\\\\:rotate-90){rotate:90deg}.scalar-app .ui-open\\\\:rotate-180[data-headlessui-state~=open],:where([data-headlessui-state~=open]) :is(.scalar-app .ui-open\\\\:rotate-180){rotate:180deg}.scalar-app .last\\\\:ui-open\\\\:border-b-0:last-child[data-headlessui-state~=open],:where([data-headlessui-state~=open]) .scalar-app .last\\\\:ui-open\\\\:border-b-0:last-child{border-bottom-style:var(--tw-border-style);border-bottom-width:0}.scalar-app .ui-not-open\\\\:hidden[data-headlessui-state]:not([data-headlessui-state~=open]),:where([data-headlessui-state]:not([data-headlessui-state~=open])) :is(.scalar-app .ui-not-open\\\\:hidden):not([data-headlessui-state]){display:none}.scalar-app .ui-not-open\\\\:rotate-0[data-headlessui-state]:not([data-headlessui-state~=open]),:where([data-headlessui-state]:not([data-headlessui-state~=open])) :is(.scalar-app .ui-not-open\\\\:rotate-0):not([data-headlessui-state]){rotate:none}.scalar-app .ui-checked\\\\:bg-b-3[data-headlessui-state~=checked],:where([data-headlessui-state~=checked]) :is(.scalar-app .ui-checked\\\\:bg-b-3){background-color:var(--scalar-background-3)}.scalar-app .ui-active\\\\:bg-b-2[data-headlessui-state~=active],:where([data-headlessui-state~=active]) :is(.scalar-app .ui-active\\\\:bg-b-2),:is(.scalar-app .ui-active\\\\:\\\\*\\\\:bg-b-2[data-headlessui-state~=active]>*),:is(:where([data-headlessui-state~=active]) :is(.scalar-app .ui-active\\\\:\\\\*\\\\:bg-b-2)>*){background-color:var(--scalar-background-2)}@media (max-width:720px) and (max-height:480px){.scalar-app .zoomed\\\\:static{position:static}.scalar-app .zoomed\\\\:p-1{padding:4px}}.app-platform-mac :is(.scalar-app .mac\\\\:pl-\\\\[72px\\\\]){padding-left:72px}@property --tw-scale-x{syntax:\"*\";inherits:false;initial-value:1}@property --tw-scale-y{syntax:\"*\";inherits:false;initial-value:1}@property --tw-scale-z{syntax:\"*\";inherits:false;initial-value:1}@property --tw-space-x-reverse{syntax:\"*\";inherits:false;initial-value:0}@property --tw-content{syntax:\"*\";inherits:false;initial-value:\"\"}.nav-item[data-v-507381a3]{cursor:pointer;border-radius:var(--scalar-radius-lg);background:var(--scalar-background-3);border:var(--scalar-border-width)solid var(--scalar-background-2);color:var(--scalar-color-3);flex:1;justify-content:center;align-items:center;min-width:0;padding:4.5px;display:flex;position:relative;overflow:hidden}.dark-mode .nav-item[data-v-507381a3]{background:var(--scalar-background-2)}@supports (color:color-mix(in lab,red,red)){.dark-mode .nav-item[data-v-507381a3]{background:color-mix(in srgb,var(--scalar-background-2),transparent)}}.nav-item-icon-copy[data-v-507381a3]{white-space:nowrap;max-width:100%;-webkit-mask-image:linear-gradient(to left,transparent 0,var(--scalar-background-2)20px);mask-image:linear-gradient(to left,transparent 0,var(--scalar-background-2)20px);overflow:hidden}.nav-item:hover .nav-item-icon-copy[data-v-507381a3]{-webkit-mask-image:linear-gradient(to left,transparent 20px,var(--scalar-background-2)40px);mask-image:linear-gradient(to left,transparent 20px,var(--scalar-background-2)40px)}.nav-item-copy[data-v-507381a3]{max-width:calc(100% - 20px)}.nav-item[data-v-507381a3]:hover{color:var(--scalar-color-1)}.nav-item__active[data-v-507381a3]{background-color:var(--scalar-background-1);color:var(--scalar-color-1);border-color:var(--scalar-border-color)}.dark-mode .nav-item__active[data-v-507381a3]{background-color:var(--scalar-background-2)}.nav-item-close[data-v-507381a3]{border-radius:var(--scalar-radius);stroke-width:1.5px;max-width:20px;color:var(--scalar-color-3);opacity:0;background:0 0;margin-left:-20px;padding:2px;position:absolute;right:3px}.nav-item:hover .nav-item-close[data-v-507381a3]{opacity:1}.nav-item-close[data-v-507381a3]:hover{background-color:var(--scalar-background-4)}.nav-item__active .nav-item-close[data-v-507381a3]:hover{background-color:var(--scalar-background-2)}.download-app-button[data-v-cb45fa05]{box-shadow:0 0 0 .5px var(--scalar-border-color);background:linear-gradient(#ffffffbf,#00000009)}.dark-mode .download-app-button[data-v-cb45fa05]{background:linear-gradient(#ffffff1a,#00000026)}.download-app-button[data-v-cb45fa05]:hover{background:linear-gradient(#00000009,#ffffffbf)}.dark-mode .download-app-button[data-v-cb45fa05]:hover{background:linear-gradient(#00000026,#ffffff1a)}.http-bg-gradient[data-v-076b14a1]{background:linear-gradient(#ffffffbf,#00000009)}.http-bg-gradient[data-v-076b14a1]:hover{background:linear-gradient(#00000009,#ffffffbf)}.dark-mode .http-bg-gradient[data-v-076b14a1]{background:linear-gradient(#ffffff09,#00000026)}.dark-mode .http-bg-gradient[data-v-076b14a1]:hover{background:linear-gradient(#00000026,#ffffff09)}.scroll-timeline-x[data-v-e0578855]{scroll-timeline:--scroll-timeline x;scroll-timeline:--scroll-timeline horizontal;-ms-overflow-style:none;scrollbar-width:none;overflow:auto}.commandmenu[data-v-f2bbd082]{box-shadow:var(--scalar-shadow-2);border-radius:var(--scalar-radius-lg);background-color:var(--scalar-background-1);opacity:0;width:100%;max-width:580px;max-height:60dvh;margin:12px;animation:.3s ease-in-out .1s forwards fadeincommandmenu-f2bbd082;position:fixed;top:150px;left:50%;transform:translate(-50%,10px)}.commandmenu-overlay[data-v-f2bbd082]{cursor:pointer;background:#0003;animation:.3s ease-in-out forwards fadeincommand-f2bbd082;position:fixed;inset:0}@keyframes fadeincommand-f2bbd082{0%{opacity:0}to{opacity:1}}@keyframes fadeincommandmenu-f2bbd082{0%{opacity:0;transform:translate(-50%,10px)}to{opacity:1;transform:translate(-50%)}}.scalar .scalar-app-layout[data-v-45e9730e]{background:var(--scalar-background-1);opacity:0;border:var(--scalar-border-width)solid var(--scalar-border-color);border-radius:8px;width:100%;max-width:1390px;height:calc(100% - 120px);margin:auto;animation:.35s forwards scalarapiclientfadein-45e9730e;position:relative;overflow:hidden}@media (max-width:720px) and (max-height:480px){.scalar .scalar-app-layout[data-v-45e9730e]{height:100%;max-height:90svh}}@keyframes scalarapiclientfadein-45e9730e{0%{opacity:0}to{opacity:1}}.scalar .scalar-app-exit[data-v-45e9730e]{cursor:pointer;z-index:-1;background:#00000038;width:100vw;height:100vh;transition:all .3s ease-in-out;animation:.35s forwards scalardrawerexitfadein-45e9730e;position:fixed;top:0;left:0}.dark-mode .scalar .scalar-app-exit[data-v-45e9730e]{background:#00000073}.scalar .scalar-app-exit[data-v-45e9730e]:before{text-align:center;color:#fff;opacity:.6;font-family:sans-serif;font-size:30px;font-weight:100;line-height:50px;position:absolute;top:0;right:12px}.scalar .scalar-app-exit[data-v-45e9730e]:hover:before{opacity:1}@keyframes scalardrawerexitfadein-45e9730e{0%{opacity:0}to{opacity:1}}.scalar-container[data-v-45e9730e]{visibility:visible;z-index:10000;justify-content:center;align-items:center;width:100%;height:100%;display:flex;position:fixed;top:0;bottom:0;left:0;overflow:hidden}.scalar .url-form-input[data-v-45e9730e]{min-height:auto!important}.scalar .scalar-container[data-v-45e9730e]{line-height:normal}.scalar .scalar-app-header span[data-v-45e9730e]{color:var(--scalar-color-3)}.scalar .scalar-app-header a[data-v-45e9730e]{color:var(--scalar-color-1)}.scalar .scalar-app-header a[data-v-45e9730e]:hover{text-decoration:underline}.scalar-activate[data-v-45e9730e]{cursor:pointer;align-items:center;gap:6px;width:fit-content;margin:0 .75rem .75rem auto;font-size:.875rem;font-weight:600;line-height:24px;display:flex}.scalar-activate-button[data-v-45e9730e]{color:var(--scalar-color-blue);appearance:none;background:0 0;border:none;outline:none;align-items:center;gap:6px;padding:0 .5rem;display:flex}.scalar-activate:hover .scalar-activate-button[data-v-45e9730e]{background:var(--scalar-background-3);border-radius:3px}.scalar-modal-layout .scalar-button-danger[data-v-98703c3c]{background:var(--scalar-color-red)}@supports (color:color-mix(in lab,red,red)){.scalar-modal-layout .scalar-button-danger[data-v-98703c3c]{background:color-mix(in srgb,var(--scalar-color-red),transparent 95%)}}.scalar-modal-layout .scalar-button-danger[data-v-98703c3c]{color:var(--scalar-color-red)}.scalar-modal-layout .scalar-button-danger[data-v-98703c3c]:hover,.scalar-modal-layout .scalar-button-danger[data-v-98703c3c]:focus{background:var(--scalar-color-red)}@supports (color:color-mix(in lab,red,red)){.scalar-modal-layout .scalar-button-danger[data-v-98703c3c]:hover,.scalar-modal-layout .scalar-button-danger[data-v-98703c3c]:focus{background:color-mix(in srgb,var(--scalar-color-red),transparent 90%)}}.fade-request-section-content[data-v-f97cc68c]{background:linear-gradient(to left,var(--scalar-background-1)64%,transparent)}.filter-hover[data-v-f97cc68c]{height:100%;padding-left:24px;padding-right:39px;transition:width 0s ease-in-out .2s;position:absolute;right:0;overflow:hidden}.filter-hover[data-v-f97cc68c]:hover,.filter-hover[data-v-f97cc68c]:has(:focus-visible){z-index:10;width:100%}.filter-hover[data-v-f97cc68c]:before{content:\"\";background-color:var(--scalar-background-1);opacity:0;pointer-events:none;width:100%;height:fit-content;transition:all .3s ease-in-out;position:absolute;top:0;left:0}.filter-hover-item[data-v-f97cc68c]{opacity:0}.filter-hover-item[data-v-f97cc68c]:not(:last-of-type){transform:translateY(3px)}.filter-hover:hover .filter-hover-item[data-v-f97cc68c]{transition:opacity .2s ease-in-out,transform .2s ease-in-out}.filter-hover:hover .filter-hover-item[data-v-f97cc68c]:last-of-type{transition-delay:50ms}.filter-hover:hover .filter-hover-item[data-v-f97cc68c]:nth-last-of-type(2){transition-delay:.1s}.filter-hover:hover .filter-hover-item[data-v-f97cc68c]:nth-last-of-type(3){transition-delay:.15s}.filter-hover:hover .filter-hover-item[data-v-f97cc68c]:nth-last-of-type(4){transition-delay:.2s}.filter-hover:hover .filter-hover-item[data-v-f97cc68c]:nth-last-of-type(5){transition-delay:.25s}.filter-hover:hover .filter-hover-item[data-v-f97cc68c]:nth-last-of-type(6){transition-delay:.3s}.filter-hover:hover .filter-hover-item[data-v-f97cc68c]:nth-last-of-type(7){transition-delay:.35s}.filter-hover:hover .filter-hover-item[data-v-f97cc68c],.filter-hover:has(:focus-visible) .filter-hover-item[data-v-f97cc68c]{opacity:1;transform:translateZ(0)}.filter-hover[data-v-f97cc68c]:hover:before,.filter-hover[data-v-f97cc68c]:has(:focus-visible):before{opacity:.9;-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px)}.filter-button[data-v-f97cc68c]{top:50%;transform:translateY(-50%)}.context-bar-group:hover .context-bar-group-hover\\\\:text-c-1[data-v-f97cc68c],.context-bar-group:has(:focus-visible) .context-bar-group-hover\\\\:text-c-1[data-v-f97cc68c]{--tw-text-opacity:1;color:rgb(var(--scalar-color-1)/var(--tw-text-opacity))}.context-bar-group:hover .context-bar-group-hover\\\\:hidden[data-v-f97cc68c],.context-bar-group:has(:focus-visible) .context-bar-group-hover\\\\:hidden[data-v-f97cc68c]{display:none}.light-mode .bg-preview[data-v-0956ad2d]{background-image:url(\"data:image/svg+xml,%3Csvg xmlns=\\'http://www.w3.org/2000/svg\\' width=\\'16\\' height=\\'16\\' fill=\\'%23000\\' fill-opacity=\\'10%25\\'%3E%3Crect width=\\'8\\' height=\\'8\\' /%3E%3Crect x=\\'8\\' y=\\'8\\' width=\\'8\\' height=\\'8\\' /%3E%3C/svg%3E\")}.dark-mode .bg-preview[data-v-0956ad2d]{background-image:url(\"data:image/svg+xml,%3Csvg xmlns=\\'http://www.w3.org/2000/svg\\' width=\\'16\\' height=\\'16\\' fill=\\'%23FFF\\' fill-opacity=\\'10%25\\'%3E%3Crect width=\\'8\\' height=\\'8\\' /%3E%3Crect x=\\'8\\' y=\\'8\\' width=\\'8\\' height=\\'8\\' /%3E%3C/svg%3E\")}[data-v-85d2902e] .cm-editor{font-size:var(--scalar-small);background-color:#0000;outline:none}[data-v-85d2902e] .cm-gutters{background-color:var(--scalar-background-1);border-radius:var(--scalar-radius)0 0 var(--scalar-radius)}.body-raw[data-v-85d2902e] .cm-scroller{min-width:100%;overflow:auto}.scalar-code-block[data-v-17966bf4] .hljs *{font-size:var(--scalar-small)}.ascii-art-animate .ascii-art-line[data-v-69ebd973]{border-right:1ch solid #0000;animation:4s step-end 1s both typewriter-69ebd973,.5s step-end infinite blinkTextCursor-69ebd973}@keyframes typewriter-69ebd973{0%{width:0}to{width:100%}}@keyframes blinkTextCursor-69ebd973{0%{border-right-color:currentColor}50%{border-right-color:#0000}}.keycap-n[data-v-45a9fc44]{background:-webkit-linear-gradient(5deg,transparent 30%,var(--scalar-color-3)50%);-webkit-text-fill-color:transparent;-webkit-background-clip:text}.keycap-hotkey[data-v-45a9fc44]{line-height:26px;position:absolute;top:32px}.scalar-version-number[data-v-6d2bdb61]{width:76px;height:76px;font-size:8px;font-family:var(--scalar-font-code);box-shadow:inset 2px 0 0 2px var(--scalar-background-2);text-align:center;text-transform:initial;-webkit-text-decoration-color:var(--scalar-color-3);text-decoration-color:var(--scalar-color-3);border-radius:9px 9px 16px 12px;flex-direction:column;justify-content:center;align-items:center;margin-top:-113px;margin-left:-36px;line-height:11px;display:flex;position:absolute;transform:skewY(13deg)}.scalar-version-number a[data-v-6d2bdb61]{background:var(--scalar-background-2);border:.5px solid var(--scalar-border-color);border-radius:3px;padding:2px 4px;font-weight:700;text-decoration:none}.gitbook-show[data-v-6d2bdb61]{display:none}.v-enter-active[data-v-5d3b84e1]{transition:opacity .5s}.v-enter-from[data-v-5d3b84e1]{opacity:0}.animate-response-heading .response-heading[data-v-7138ed84]{opacity:1;animation:.2s ease-in-out forwards push-response-7138ed84}@keyframes push-response-7138ed84{0%{opacity:1;transform:translateY(0)}to{opacity:0;transform:translateY(-4px)}}.animate-response-heading .animate-response-children[data-v-7138ed84]{opacity:0;animation:.2s ease-in-out 50ms forwards response-spans-7138ed84}@keyframes response-spans-7138ed84{0%{opacity:0;transform:translateY(4px)}to{opacity:1;transform:translateY(0)}}[data-v-103d9d56] .cm-editor{background:0 0;outline:none;height:100%;padding:0}[data-v-103d9d56] .cm-placeholder{color:var(--scalar-color-3)}[data-v-103d9d56] .cm-content{font-family:var(--scalar-font-code);font-size:var(--scalar-small);max-height:20px;padding:8px 0}[data-v-103d9d56] .cm-tooltip{filter:brightness(var(--scalar-lifted-brightness));border-radius:var(--scalar-radius);box-shadow:var(--scalar-shadow-2);background:0 0!important;border:none!important;outline:none!important;overflow:hidden!important}[data-v-103d9d56] .cm-tooltip-autocomplete ul li{padding:3px 6px!important}[data-v-103d9d56] .cm-completionIcon-type:after{color:var(--scalar-color-3)!important}[data-v-103d9d56] .cm-tooltip-autocomplete ul li[aria-selected]{background:var(--scalar-background-2)!important;color:var(--scalar-color-1)!important}[data-v-103d9d56] .cm-tooltip-autocomplete ul{position:relative;padding:6px!important}[data-v-103d9d56] .cm-tooltip-autocomplete ul li:hover{border-radius:3px;color:var(--scalar-color-1)!important;background:var(--scalar-background-3)!important}[data-v-103d9d56] .cm-activeLine,[data-v-103d9d56] .cm-activeLineGutter{background-color:#0000}[data-v-103d9d56] .cm-selectionMatch,[data-v-103d9d56] .cm-matchingBracket{border-radius:var(--scalar-radius);background:var(--scalar-background-4)!important}[data-v-103d9d56] .cm-css-color-picker-wrapper{outline:1px solid var(--scalar-background-3);border-radius:3px;display:inline-flex;overflow:hidden}[data-v-103d9d56] .cm-gutters{color:var(--scalar-color-3);font-size:var(--scalar-small);background-color:#0000;border-right:none;border-radius:0 0 0 3px;line-height:22px}[data-v-103d9d56] .cm-gutters:before{content:\"\";border-radius:var(--scalar-radius)0 0 var(--scalar-radius);background-color:var(--scalar-background-1);width:calc(100% - 2px);height:calc(100% - 4px);position:absolute;top:2px;left:2px}[data-v-103d9d56] .cm-gutterElement{justify-content:flex-end;align-items:center;display:flex;position:relative;font-family:var(--scalar-font-code)!important;padding-left:0!important;padding-right:6px!important}[data-v-103d9d56] .cm-lineNumbers .cm-gutterElement{min-width:fit-content}[data-v-103d9d56] .cm-gutter+.cm-gutter :not(.cm-foldGutter) .cm-gutterElement{padding-left:0!important}[data-v-103d9d56] .cm-scroller{overflow:auto}.line-wrapping[data-v-103d9d56]:focus-within .cm-content{white-space:break-spaces;word-break:break-all;min-height:fit-content;padding:3px 6px;display:inline-table}.schema>span[data-v-4df72868]:not(:first-child):before{content:\"·\";margin:0 .5ch;display:block}.schema>span[data-v-4df72868]{white-space:nowrap;display:flex}[data-v-04661eb4] .cm-editor{padding:0}[data-v-04661eb4] .cm-content{font-family:var(--scalar-font);font-size:var(--scalar-small);background-color:#0000;align-items:center;width:100%;padding:5px 8px;display:flex}[data-v-04661eb4] .cm-content:has(.cm-pill){padding:5px 8px}[data-v-04661eb4] .cm-content .cm-pill:not(:last-of-type){margin-right:.5px}[data-v-04661eb4] .cm-content .cm-pill:not(:first-of-type){margin-left:.5px}[data-v-04661eb4] .cm-line{text-overflow:ellipsis;padding:0;overflow:hidden}.filemask[data-v-04661eb4]{-webkit-mask-image:linear-gradient(to right,transparent 0,var(--scalar-background-2)20px);mask-image:linear-gradient(to right,transparent 0,var(--scalar-background-2)20px)}[data-v-9adcfa05] .cm-content{font-size:var(--scalar-small)}[data-v-2b299aed] .cm-editor{padding:0}[data-v-2b299aed] .cm-content{font-family:var(--scalar-font);font-size:var(--scalar-small);background-color:#0000;align-items:center;width:100%;padding:5px 8px;display:flex}[data-v-2b299aed] .cm-content:has(.cm-pill){padding:5px 8px}[data-v-2b299aed] .cm-content .cm-pill:not(:last-of-type){margin-right:.5px}[data-v-2b299aed] .cm-content .cm-pill:not(:first-of-type){margin-left:.5px}[data-v-2b299aed] .cm-line{text-overflow:ellipsis;word-break:break-word;padding:0;overflow:hidden}.required[data-v-2b299aed]:after{content:\"Required\"}input[data-v-2b299aed]::placeholder{color:var(--scalar-color-3)}.scalar-password-input[data-v-2b299aed]{text-security:disc;-webkit-text-security:disc;-moz-text-security:disc}.auth-combobox-position[data-v-0bb98074]{margin-left:120px}.scroll-timeline-x[data-v-0bb98074]{scroll-timeline:--scroll-timeline x;scroll-timeline:--scroll-timeline horizontal;scrollbar-width:none;-ms-overflow-style:none;overflow:auto}.fade-left[data-v-0bb98074],.fade-right[data-v-0bb98074]{content:\"\";pointer-events:none;height:100%;min-height:24px;animation-name:fadein-0bb98074;animation-duration:1ms;animation-direction:reverse;animation-timeline:--scroll-timeline;position:sticky}.fade-left[data-v-0bb98074]{background:linear-gradient(-90deg,var(--scalar-background-1)0%,var(--scalar-background-1)60%,var(--scalar-background-1)100%)}@supports (color:color-mix(in lab,red,red)){.fade-left[data-v-0bb98074]{background:linear-gradient(-90deg,color-mix(in srgb,var(--scalar-background-1),transparent 100%)0%,color-mix(in srgb,var(--scalar-background-1),transparent 20%)60%,var(--scalar-background-1)100%)}}.fade-left[data-v-0bb98074]{min-width:3px;animation-direction:normal;left:-1px}.fade-right[data-v-0bb98074]{background:linear-gradient(90deg,var(--scalar-background-1)0%,var(--scalar-background-1)60%,var(--scalar-background-1)100%)}@supports (color:color-mix(in lab,red,red)){.fade-right[data-v-0bb98074]{background:linear-gradient(90deg,color-mix(in srgb,var(--scalar-background-1),transparent 100%)0%,color-mix(in srgb,var(--scalar-background-1),transparent 20%)60%,var(--scalar-background-1)100%)}}.fade-right[data-v-0bb98074]{min-width:24px;margin-left:-20px;top:0;right:-1px}@keyframes fadein-0bb98074{0%{opacity:0}15%{opacity:1}}.auth-combobox-position[data-v-3f1067a4]{margin-left:120px}.scroll-timeline-x[data-v-3f1067a4]{scroll-timeline:--scroll-timeline x;scroll-timeline:--scroll-timeline horizontal;scrollbar-width:none;-ms-overflow-style:none;overflow:auto}.fade-left[data-v-3f1067a4],.fade-right[data-v-3f1067a4]{content:\"\";pointer-events:none;height:100%;min-height:24px;animation-name:fadein-3f1067a4;animation-duration:1ms;animation-direction:reverse;animation-timeline:--scroll-timeline;position:sticky}.fade-left[data-v-3f1067a4]{background:linear-gradient(-90deg,var(--scalar-background-1)0%,var(--scalar-background-1)60%,var(--scalar-background-1)100%)}@supports (color:color-mix(in lab,red,red)){.fade-left[data-v-3f1067a4]{background:linear-gradient(-90deg,color-mix(in srgb,var(--scalar-background-1),transparent 100%)0%,color-mix(in srgb,var(--scalar-background-1),transparent 20%)60%,var(--scalar-background-1)100%)}}.fade-left[data-v-3f1067a4]{min-width:3px;animation-direction:normal;left:-1px}.fade-right[data-v-3f1067a4]{background:linear-gradient(90deg,var(--scalar-background-1)0%,var(--scalar-background-1)60%,var(--scalar-background-1)100%)}@supports (color:color-mix(in lab,red,red)){.fade-right[data-v-3f1067a4]{background:linear-gradient(90deg,color-mix(in srgb,var(--scalar-background-1),transparent 100%)0%,color-mix(in srgb,var(--scalar-background-1),transparent 20%)60%,var(--scalar-background-1)100%)}}.fade-right[data-v-3f1067a4]{min-width:24px;margin-left:-20px;top:0;right:-1px}@keyframes fadein-3f1067a4{0%{opacity:0}15%{opacity:1}}[data-v-2891f052] code.hljs *{font-size:var(--scalar-small)}.request-section-content[data-v-e85e2882]{--scalar-border-width:.5px}.request-section-content-filter[data-v-e85e2882]{box-shadow:0 -10px 0 10px var(--scalar-background-1)}.request-item:focus-within .request-meta-buttons[data-v-e85e2882]{opacity:1}.group-hover-input[data-v-e85e2882]{border-width:var(--scalar-border-width);border-color:#0000}.group:hover .group-hover-input[data-v-e85e2882]{background:var(--scalar-background-1)}@supports (color:color-mix(in lab,red,red)){.group:hover .group-hover-input[data-v-e85e2882]{background:color-mix(in srgb,var(--scalar-background-1),var(--scalar-background-2))}}.group:hover .group-hover-input[data-v-e85e2882]{border-color:var(--scalar-border-color)}.group-hover-input[data-v-e85e2882]:focus{border-color:var(--scalar-border-color)!important;background:0 0!important}.address-bar-history-button[data-v-8cf04803]:hover{background:var(--scalar-background-3)}.address-bar-history-button[data-v-8cf04803]:focus-within{background:var(--scalar-background-2)}.description[data-v-b503e183] .markdown{font-weight:var(--scalar-semibold);color:var(--scalar-color--1);padding:0;display:block}.description[data-v-b503e183] .markdown>:first-child{margin-top:0}[data-v-d30c143b] .cm-editor{background:0 0;outline:none;height:100%;padding:0}[data-v-d30c143b] .cm-placeholder{color:var(--scalar-color-3)}[data-v-d30c143b] .cm-content{font-family:var(--scalar-font-code);font-size:var(--scalar-small);max-height:20px;padding:8px 0}[data-v-d30c143b] .cm-tooltip{filter:brightness(var(--scalar-lifted-brightness));border-radius:var(--scalar-radius);box-shadow:var(--scalar-shadow-2);background:0 0!important;border:none!important;outline:none!important;overflow:hidden!important}[data-v-d30c143b] .cm-tooltip-autocomplete ul li{padding:3px 6px!important}[data-v-d30c143b] .cm-completionIcon-type:after{color:var(--scalar-color-3)!important}[data-v-d30c143b] .cm-tooltip-autocomplete ul li[aria-selected]{background:var(--scalar-background-2)!important;color:var(--scalar-color-1)!important}[data-v-d30c143b] .cm-tooltip-autocomplete ul{position:relative;padding:6px!important}[data-v-d30c143b] .cm-tooltip-autocomplete ul li:hover{border-radius:3px;color:var(--scalar-color-1)!important;background:var(--scalar-background-3)!important}[data-v-d30c143b] .cm-activeLine,[data-v-d30c143b] .cm-activeLineGutter{background-color:#0000}[data-v-d30c143b] .cm-selectionMatch,[data-v-d30c143b] .cm-matchingBracket{border-radius:var(--scalar-radius);background:var(--scalar-background-4)!important}[data-v-d30c143b] .cm-css-color-picker-wrapper{outline:1px solid var(--scalar-background-3);border-radius:3px;display:inline-flex;overflow:hidden}[data-v-d30c143b] .cm-gutters{color:var(--scalar-color-3);font-size:var(--scalar-small);background-color:#0000;border-right:none;border-radius:0 0 0 3px;line-height:22px}[data-v-d30c143b] .cm-gutters:before{content:\"\";border-radius:var(--scalar-radius)0 0 var(--scalar-radius);background-color:var(--scalar-background-1);width:calc(100% - 2px);height:calc(100% - 4px);position:absolute;top:2px;left:2px}[data-v-d30c143b] .cm-gutterElement{justify-content:flex-end;align-items:center;display:flex;position:relative;font-family:var(--scalar-font-code)!important;padding-left:0!important;padding-right:6px!important}[data-v-d30c143b] .cm-lineNumbers .cm-gutterElement{min-width:fit-content}[data-v-d30c143b] .cm-gutter+.cm-gutter :not(.cm-foldGutter) .cm-gutterElement{padding-left:0!important}[data-v-d30c143b] .cm-scroller{overflow:auto}.line-wrapping[data-v-d30c143b]:focus-within .cm-content{white-space:break-spaces;word-break:break-all;min-height:fit-content;padding:3px 6px;display:inline-table}.cm-pill{--tw-bg-base:var(--scalar-color-1);color:var(--tw-bg-base);font-size:var(--scalar-small);border-radius:30px;padding:0 9px;display:inline-block;background:var(--tw-bg-base)!important}@supports (color:color-mix(in lab,red,red)){.cm-pill{background:color-mix(in srgb,var(--tw-bg-base),transparent 94%)!important}}.cm-pill.bg-grey{background:var(--scalar-background-3)!important}.dark-mode .cm-pill{background:var(--tw-bg-base)!important}@supports (color:color-mix(in lab,red,red)){.dark-mode .cm-pill{background:color-mix(in srgb,var(--tw-bg-base),transparent 90%)!important}}.cm-pill:first-of-type{margin-left:0}.cm-editor .cm-widgetBuffer{display:none}.cm-foldPlaceholder:hover{color:var(--scalar-color-1)}.cm-foldGutter .cm-gutterElement{font-size:var(--scalar-heading-4);padding:2px!important}.cm-foldGutter .cm-gutterElement:first-of-type{display:none}.cm-foldGutter .cm-gutterElement .cm-foldMarker{padding:2px}.cm-foldGutter .cm-gutterElement:hover .cm-foldMarker{background:var(--scalar-background-2);border-radius:var(--scalar-radius);color:var(--scalar-color-1)}[data-v-79f90dcf] .cm-editor{outline:none;width:100%;height:100%}[data-v-79f90dcf] .cm-line{padding:0}[data-v-79f90dcf] .cm-content{font-size:var(--scalar-small);align-items:center;padding:0;display:flex}.scroll-timeline-x[data-v-79f90dcf]{scroll-timeline:--scroll-timeline x;scroll-timeline:--scroll-timeline horizontal;-ms-overflow-style:none}.scroll-timeline-x-hidden[data-v-79f90dcf]{overflow-x:auto}.scroll-timeline-x-hidden[data-v-79f90dcf] .cm-scroller{scrollbar-width:none;-ms-overflow-style:none;padding-right:20px;overflow:auto}.scroll-timeline-x-hidden[data-v-79f90dcf]::-webkit-scrollbar{width:0;height:0;display:none}.scroll-timeline-x-hidden[data-v-79f90dcf] .cm-scroller::-webkit-scrollbar{width:0;height:0;display:none}.scroll-timeline-x-address[data-v-79f90dcf]{scrollbar-width:none;line-height:27px}.scroll-timeline-x-address[data-v-79f90dcf]:after{content:\"\";cursor:text;width:24px;height:100%;position:absolute;right:0}.scroll-timeline-x-address[data-v-79f90dcf]:empty:before{content:\"Enter URL or cURL request\";color:var(--scalar-color-3);pointer-events:none}.fade-left[data-v-79f90dcf],.fade-right[data-v-79f90dcf]{content:\"\";pointer-events:none;z-index:1;height:100%;animation-name:fadein-79f90dcf;animation-duration:1ms;animation-direction:reverse;animation-timeline:--scroll-timeline;position:sticky}.fade-left[data-v-79f90dcf]{background:linear-gradient(-90deg,var(--scalar-address-bar-bg)0%,var(--scalar-address-bar-bg)30%,var(--scalar-address-bar-bg)100%)}@supports (color:color-mix(in lab,red,red)){.fade-left[data-v-79f90dcf]{background:linear-gradient(-90deg,color-mix(in srgb,var(--scalar-address-bar-bg),transparent 100%)0%,color-mix(in srgb,var(--scalar-address-bar-bg),transparent 20%)30%,var(--scalar-address-bar-bg)100%)}}.fade-left[data-v-79f90dcf]{min-width:6px;animation-direction:normal;left:-1px}.fade-right[data-v-79f90dcf]{background:linear-gradient(90deg,var(--scalar-address-bar-bg)0%,var(--scalar-address-bar-bg)30%,var(--scalar-address-bar-bg)100%)}@supports (color:color-mix(in lab,red,red)){.fade-right[data-v-79f90dcf]{background:linear-gradient(90deg,color-mix(in srgb,var(--scalar-address-bar-bg),transparent 100%)0%,color-mix(in srgb,var(--scalar-address-bar-bg),transparent 20%)30%,var(--scalar-address-bar-bg)100%)}}.fade-right[data-v-79f90dcf]{min-width:24px;right:-1px}@keyframes fadein-79f90dcf{0%{opacity:0}1%{opacity:1}}.address-bar-bg-states[data-v-79f90dcf]{--scalar-address-bar-bg:var(--scalar-background-1)}@supports (color:color-mix(in lab,red,red)){.address-bar-bg-states[data-v-79f90dcf]{--scalar-address-bar-bg:color-mix(in srgb,var(--scalar-background-1),var(--scalar-background-2))}}.address-bar-bg-states[data-v-79f90dcf]{background:var(--scalar-address-bar-bg)}.address-bar-bg-states[data-v-79f90dcf]:has(.cm-focused){--scalar-address-bar-bg:var(--scalar-background-1);border-color:var(--scalar-border-color);outline:1px solid var(--scalar-color-accent)}.address-bar-bg-states:has(.cm-focused) .fade-left[data-v-79f90dcf],.address-bar-bg-states:has(.cm-focused) .fade-right[data-v-79f90dcf]{--scalar-address-bar-bg:var(--scalar-background-1)}.open-api-client-button[data-v-f016469d]{cursor:pointer;text-align:center;white-space:nowrap;width:100%;height:31px;font-size:var(--scalar-mini);font-weight:var(--scalar-semibold);border-radius:var(--scalar-radius);box-shadow:0 0 0 .5px var(--scalar-border-color);color:var(--scalar-sidebar-color-1);justify-content:center;align-items:center;gap:6px;padding:9px 12px;line-height:1.385;text-decoration:none;display:flex}.open-api-client-button[data-v-f016469d]:hover{background:var(--scalar-sidebar-item-hover-background,var(--scalar-background-2))}.gitbook-show[data-v-0b45d144]{display:none}.app-exit-button[data-v-0b45d144]{color:#fff;background:#0000001a}.app-exit-button[data-v-0b45d144]:hover{background:#ffffff1a}.schema>span[data-v-05ca9351]:not(:first-child):before{content:\"·\";margin:0 .5ch;display:block}.schema>span[data-v-05ca9351]{white-space:nowrap;display:flex}[data-v-46e149be] .cm-editor{padding:0}[data-v-46e149be] .cm-content{font-family:var(--scalar-font);font-size:var(--scalar-small);background-color:#0000;align-items:center;width:100%;padding:5px 8px;display:flex}[data-v-46e149be] .cm-content:has(.cm-pill){padding:5px 8px}[data-v-46e149be] .cm-content .cm-pill:not(:last-of-type){margin-right:.5px}[data-v-46e149be] .cm-content .cm-pill:not(:first-of-type){margin-left:.5px}[data-v-46e149be] .cm-line{text-overflow:ellipsis;padding:0;overflow:hidden}.filemask[data-v-46e149be]{-webkit-mask-image:linear-gradient(to right,transparent 0,var(--scalar-background-2)20px);mask-image:linear-gradient(to right,transparent 0,var(--scalar-background-2)20px)}[data-v-01b90115] .cm-content{font-size:var(--scalar-small)}[data-v-25ff4902] .cm-editor{padding:0}[data-v-25ff4902] .cm-content{font-family:var(--scalar-font);font-size:var(--scalar-small);background-color:#0000;align-items:center;width:100%;padding:5px 8px;display:flex}[data-v-25ff4902] .cm-content:has(.cm-pill){padding:5px 8px}[data-v-25ff4902] .cm-content .cm-pill:not(:last-of-type){margin-right:.5px}[data-v-25ff4902] .cm-content .cm-pill:not(:first-of-type){margin-left:.5px}[data-v-25ff4902] .cm-line{text-overflow:ellipsis;word-break:break-word;padding:0;overflow:hidden}.required[data-v-25ff4902]:after{content:\"Required\"}input[data-v-25ff4902]::placeholder{color:var(--scalar-color-3)}.scalar-password-input[data-v-25ff4902]{text-security:disc;-webkit-text-security:disc;-moz-text-security:disc}.request-section-content[data-v-5b361e3c]{--scalar-border-width:.5px}.request-section-content-filter[data-v-5b361e3c]{box-shadow:0 -10px 0 10px var(--scalar-background-1)}.request-item:focus-within .request-meta-buttons[data-v-5b361e3c]{opacity:1}.group-hover-input[data-v-5b361e3c]{border-width:var(--scalar-border-width);border-color:#0000}.group:hover .group-hover-input[data-v-5b361e3c]{background:var(--scalar-background-1)}@supports (color:color-mix(in lab,red,red)){.group:hover .group-hover-input[data-v-5b361e3c]{background:color-mix(in srgb,var(--scalar-background-1),var(--scalar-background-2))}}.group:hover .group-hover-input[data-v-5b361e3c]{border-color:var(--scalar-border-color)}.group-hover-input[data-v-5b361e3c]:focus{border-color:var(--scalar-border-color)!important;background:0 0!important}.light-mode .bg-preview[data-v-f72c7443]{background-image:url(\"data:image/svg+xml,%3Csvg xmlns=\\'http://www.w3.org/2000/svg\\' width=\\'16\\' height=\\'16\\' fill=\\'%23000\\' fill-opacity=\\'10%25\\'%3E%3Crect width=\\'8\\' height=\\'8\\' /%3E%3Crect x=\\'8\\' y=\\'8\\' width=\\'8\\' height=\\'8\\' /%3E%3C/svg%3E\")}.dark-mode .bg-preview[data-v-f72c7443]{background-image:url(\"data:image/svg+xml,%3Csvg xmlns=\\'http://www.w3.org/2000/svg\\' width=\\'16\\' height=\\'16\\' fill=\\'%23FFF\\' fill-opacity=\\'10%25\\'%3E%3Crect width=\\'8\\' height=\\'8\\' /%3E%3Crect x=\\'8\\' y=\\'8\\' width=\\'8\\' height=\\'8\\' /%3E%3C/svg%3E\")}[data-v-9041436f] .cm-editor{font-size:var(--scalar-small);background-color:#0000;outline:none}[data-v-9041436f] .cm-gutters{background-color:var(--scalar-background-1);border-radius:var(--scalar-radius)0 0 var(--scalar-radius)}.body-raw[data-v-9041436f] .cm-scroller{min-width:100%;overflow:auto}.scalar-code-block[data-v-4f20aa18] .hljs *{font-size:var(--scalar-small)}.response-body-virtual[data-headlessui-state=open],.response-body-virtual[data-headlessui-state=open] .diclosure-panel{flex-direction:column;flex-grow:1;display:flex}.scalar-version-number[data-v-dc5b68d4]{width:76px;height:76px;font-size:8px;font-family:var(--scalar-font-code);box-shadow:inset 2px 0 0 2px var(--scalar-background-2);text-align:center;text-transform:initial;-webkit-text-decoration-color:var(--scalar-color-3);text-decoration-color:var(--scalar-color-3);border-radius:9px 9px 16px 12px;flex-direction:column;justify-content:center;align-items:center;margin-top:-113px;margin-left:-36px;line-height:11px;display:flex;position:absolute;transform:skewY(13deg)}.scalar-version-number a[data-v-dc5b68d4]{background:var(--scalar-background-2);border:.5px solid var(--scalar-border-color);border-radius:3px;padding:2px 4px;font-weight:700;text-decoration:none}.gitbook-show[data-v-dc5b68d4]{display:none}.v-enter-active[data-v-6f5e8f67]{transition:opacity .5s}.v-enter-from[data-v-6f5e8f67]{opacity:0}.animate-response-heading .response-heading[data-v-fd0c4365]{opacity:1;animation:.2s ease-in-out forwards push-response-fd0c4365}@keyframes push-response-fd0c4365{0%{opacity:1;transform:translateY(0)}to{opacity:0;transform:translateY(-4px)}}.animate-response-heading .animate-response-children[data-v-fd0c4365]{opacity:0;animation:.2s ease-in-out 50ms forwards response-spans-fd0c4365}@keyframes response-spans-fd0c4365{0%{opacity:0;transform:translateY(4px)}to{opacity:1;transform:translateY(0)}}.resizer[data-v-bbcd6c0c]{cursor:col-resize;border-right:2px solid #0000;width:5px;transition:border-right-color .3s;position:absolute;top:0;bottom:0;right:0}.scalar-dragging{cursor:col-resize}.resizer:hover,.scalar-dragging .resizer{border-right-color:var(--scalar-background-3)}.scalar-dragging:after{content:\"\";display:block;position:absolute;inset:0}.scalar .scalar-app-layout[data-v-ddfe1d6d]{background:var(--scalar-background-1);opacity:0;border:var(--scalar-border-width)solid var(--scalar-border-color);border-radius:8px;width:100%;max-width:1390px;height:calc(100% - 120px);margin:auto;animation:.35s forwards scalarapiclientfadein-ddfe1d6d;position:relative;overflow:hidden}@media (max-width:720px) and (max-height:480px){.scalar .scalar-app-layout[data-v-ddfe1d6d]{height:100%;max-height:90svh}}@keyframes scalarapiclientfadein-ddfe1d6d{0%{opacity:0}to{opacity:1}}.scalar .scalar-app-exit[data-v-ddfe1d6d]{cursor:pointer;z-index:-1;background:#00000038;width:100vw;height:100vh;transition:all .3s ease-in-out;animation:.35s forwards scalardrawerexitfadein-ddfe1d6d;position:fixed;top:0;left:0}.dark-mode .scalar .scalar-app-exit[data-v-ddfe1d6d]{background:#00000073}.scalar .scalar-app-exit[data-v-ddfe1d6d]:before{text-align:center;color:#fff;opacity:.6;font-family:sans-serif;font-size:30px;font-weight:100;line-height:50px;position:absolute;top:0;right:12px}.scalar .scalar-app-exit[data-v-ddfe1d6d]:hover:before{opacity:1}@keyframes scalardrawerexitfadein-ddfe1d6d{0%{opacity:0}to{opacity:1}}.scalar-container[data-v-ddfe1d6d]{visibility:visible;z-index:10000;justify-content:center;align-items:center;width:100%;height:100%;display:flex;position:fixed;top:0;bottom:0;left:0;overflow:hidden}.scalar .url-form-input[data-v-ddfe1d6d]{min-height:auto!important}.scalar .scalar-container[data-v-ddfe1d6d]{line-height:normal}.scalar .scalar-app-header span[data-v-ddfe1d6d]{color:var(--scalar-color-3)}.scalar .scalar-app-header a[data-v-ddfe1d6d]{color:var(--scalar-color-1)}.scalar .scalar-app-header a[data-v-ddfe1d6d]:hover{text-decoration:underline}.scalar-activate[data-v-ddfe1d6d]{cursor:pointer;align-items:center;gap:6px;width:fit-content;margin:0 .75rem .75rem auto;font-size:.875rem;font-weight:600;line-height:24px;display:flex}.scalar-activate-button[data-v-ddfe1d6d]{color:var(--scalar-color-blue);appearance:none;background:0 0;border:none;outline:none;align-items:center;gap:6px;padding:0 .5rem;display:flex}.scalar-activate:hover .scalar-activate-button[data-v-ddfe1d6d]{background:var(--scalar-background-3);border-radius:3px}.data-table tr:nth-child(2) td[data-v-ed778005]{border-top:none!important}[data-v-675b20d5] .cm-editor{padding:0}[data-v-675b20d5] .cm-content{font-family:var(--scalar-font);font-size:var(--scalar-small);background-color:#0000;align-items:center;width:100%;padding:5px 8px;display:flex}[data-v-675b20d5] .cm-content:has(.cm-pill){padding:5px 8px}[data-v-675b20d5] .cm-content .cm-pill:not(:last-of-type){margin-right:.5px}[data-v-675b20d5] .cm-content .cm-pill:not(:first-of-type){margin-left:.5px}[data-v-675b20d5] .cm-line{text-overflow:ellipsis;padding:0;overflow:hidden}[data-v-28c8509c] .cm-editor{padding:0}[data-v-28c8509c] .cm-content{font-family:var(--scalar-font);font-size:var(--scalar-small);background-color:#0000;align-items:center;width:100%;padding:5px 8px;display:flex}[data-v-28c8509c] .cm-content:has(.cm-pill){padding:5px 8px}[data-v-28c8509c] .cm-content .cm-pill:not(:last-of-type){margin-right:.5px}[data-v-28c8509c] .cm-content .cm-pill:not(:first-of-type){margin-left:.5px}[data-v-28c8509c] .cm-line{text-overflow:ellipsis;padding:0;overflow:hidden}.empty-sidebar-item-content[data-v-87ac8466]{display:none}.empty-sidebar-item .empty-sidebar-item-content[data-v-87ac8466]{display:block}.rabbitjump[data-v-87ac8466]{opacity:0}.empty-sidebar-item:hover .rabbitjump[data-v-87ac8466]{opacity:1;animation:.5s step-end infinite rabbitAnimation-87ac8466}.empty-sidebar-item:hover .rabbitsit[data-v-87ac8466]{opacity:0;animation:.5s step-end infinite rabbitAnimation2-87ac8466}.empty-sidebar-item:hover .rabbit-ascii[data-v-87ac8466]{animation:8s linear infinite rabbitRun-87ac8466}@keyframes rabbitRun-87ac8466{0%{transform:translateZ(0)}25%{transform:translate(250px)}25.01%{transform:translate(-250px)}75%{transform:translate(250px)}75.01%{transform:translate(-250px)}to{transform:translateZ(0)}}@keyframes rabbitAnimation-87ac8466{0%,to{opacity:1}50%{opacity:0}}@keyframes rabbitAnimation2-87ac8466{0%,to{opacity:0}50%{opacity:1;transform:translateY(-8px)}}.download-app-button[data-v-d9bec97b]{box-shadow:0 0 0 .5px var(--scalar-border-color);background:linear-gradient(#ffffffbf,#00000009)}.dark-mode .download-app-button[data-v-d9bec97b]{background:linear-gradient(#ffffff1a,#00000026)}.download-app-button[data-v-d9bec97b]:hover{background:linear-gradient(#00000009,#ffffffbf)}.dark-mode .download-app-button[data-v-d9bec97b]:hover{background:linear-gradient(#00000026,#ffffff1a)}.scalar-modal-layout .scalar-button-danger[data-v-b4497d3d]{background:var(--scalar-color-red)}@supports (color:color-mix(in lab,red,red)){.scalar-modal-layout .scalar-button-danger[data-v-b4497d3d]{background:color-mix(in srgb,var(--scalar-color-red),transparent 95%)}}.scalar-modal-layout .scalar-button-danger[data-v-b4497d3d]{color:var(--scalar-color-red)}.scalar-modal-layout .scalar-button-danger[data-v-b4497d3d]:hover,.scalar-modal-layout .scalar-button-danger[data-v-b4497d3d]:focus{background:var(--scalar-color-red)}@supports (color:color-mix(in lab,red,red)){.scalar-modal-layout .scalar-button-danger[data-v-b4497d3d]:hover,.scalar-modal-layout .scalar-button-danger[data-v-b4497d3d]:focus{background:color-mix(in srgb,var(--scalar-color-red),transparent 90%)}}.request-card[data-v-13b785c0]{font-size:var(--scalar-font-size-3)}.request-method[data-v-13b785c0]{font-family:var(--scalar-font-code);text-transform:uppercase;margin-right:6px}.request-card-footer[data-v-13b785c0]{flex-shrink:0;justify-content:flex-end;padding:6px;display:flex}.request-card-footer-addon[data-v-13b785c0]{flex:1;align-items:center;min-width:0;display:flex}.request-editor-section[data-v-13b785c0]{flex:1;display:flex}.request-card-simple[data-v-13b785c0]{font-size:var(--scalar-small);justify-content:space-between;align-items:center;padding:8px 8px 8px 12px;display:flex}.code-snippet[data-v-13b785c0]{flex-direction:column;width:100%;display:flex}@media (min-width:800px){.has-no-import-url,.has-import-url{contain:paint;max-width:100dvw;overflow-x:hidden}.has-no-import-url .scalar-client>main{opacity:1;background:var(--scalar-background-1);animation:.3s ease-in-out forwards transform-restore-layout}.has-import-url .scalar-client>main{opacity:0;border:var(--scalar-border-width)solid var(--scalar-border-color);z-index:10000;border-radius:12px;animation:.3s ease-in-out forwards transform-fade-layout;overflow:hidden;transform:scale(.85)translate(calc(50dvw + 80px))}.has-import-url .scalar-client .sidenav{display:none}.has-no-import-url .scalar-app,.has-import-url .scalar-app{background:var(--scalar-background-1)!important}}@keyframes transform-fade-layout{0%{opacity:0;transform:scale(.85)translate(calc(50dvw + 80px),10px)}to{opacity:1;transform:scale(.85)translate(calc(50dvw + 80px))}}@keyframes transform-restore-layout{0%{opacity:1;transform:scale(.85)translate(calc(50dvw + 80px))}to{opacity:1;transform:scale(1)translate(0)}}.openapi-color{color:var(--scalar-color-green)}.section-flare{position:fixed;top:0;right:-50dvw}#scalar-client{background-color:var(--scalar-background-2);flex-direction:column;width:100dvw;height:100dvh;display:flex;position:relative}.dark-mode #scalar-client{background-color:var(--scalar-background-1)}@supports (color:color-mix(in lab,red,red)){.dark-mode #scalar-client{background-color:color-mix(in srgb,var(--scalar-background-1)65%,black)}}.address-bar-history-button[data-v-a93fa60f]:hover{background:var(--scalar-background-3)}.address-bar-history-button[data-v-a93fa60f]:focus-within{background:var(--scalar-background-2)}.description[data-v-92012388] .markdown{font-weight:var(--scalar-semibold);color:var(--scalar-color--1);padding:0;display:block}.description[data-v-92012388] .markdown>:first-child{margin-top:0}[data-v-78c9dbb9] .cm-editor{outline:none;width:100%;height:100%}[data-v-78c9dbb9] .cm-line{padding:0}[data-v-78c9dbb9] .cm-content{font-size:var(--scalar-small);align-items:center;padding:0;display:flex}.scroll-timeline-x[data-v-78c9dbb9]{scroll-timeline:--scroll-timeline x;scroll-timeline:--scroll-timeline horizontal;-ms-overflow-style:none}.scroll-timeline-x-hidden[data-v-78c9dbb9]{overflow-x:auto}.scroll-timeline-x-hidden[data-v-78c9dbb9] .cm-scroller{scrollbar-width:none;-ms-overflow-style:none;padding-right:20px;overflow:auto}.scroll-timeline-x-hidden[data-v-78c9dbb9]::-webkit-scrollbar{width:0;height:0;display:none}.scroll-timeline-x-hidden[data-v-78c9dbb9] .cm-scroller::-webkit-scrollbar{width:0;height:0;display:none}.scroll-timeline-x-address[data-v-78c9dbb9]{scrollbar-width:none;line-height:27px}.scroll-timeline-x-address[data-v-78c9dbb9]:after{content:\"\";cursor:text;width:24px;height:100%;position:absolute;right:0}.scroll-timeline-x-address[data-v-78c9dbb9]:empty:before{content:\"Enter URL or cURL request\";color:var(--scalar-color-3);pointer-events:none}.fade-left[data-v-78c9dbb9],.fade-right[data-v-78c9dbb9]{content:\"\";pointer-events:none;z-index:1;height:100%;animation-name:fadein-78c9dbb9;animation-duration:1ms;animation-direction:reverse;animation-timeline:--scroll-timeline;position:sticky}.fade-left[data-v-78c9dbb9]{background:linear-gradient(-90deg,var(--scalar-address-bar-bg)0%,var(--scalar-address-bar-bg)30%,var(--scalar-address-bar-bg)100%)}@supports (color:color-mix(in lab,red,red)){.fade-left[data-v-78c9dbb9]{background:linear-gradient(-90deg,color-mix(in srgb,var(--scalar-address-bar-bg),transparent 100%)0%,color-mix(in srgb,var(--scalar-address-bar-bg),transparent 20%)30%,var(--scalar-address-bar-bg)100%)}}.fade-left[data-v-78c9dbb9]{min-width:6px;animation-direction:normal;left:-1px}.fade-right[data-v-78c9dbb9]{background:linear-gradient(90deg,var(--scalar-address-bar-bg)0%,var(--scalar-address-bar-bg)30%,var(--scalar-address-bar-bg)100%)}@supports (color:color-mix(in lab,red,red)){.fade-right[data-v-78c9dbb9]{background:linear-gradient(90deg,color-mix(in srgb,var(--scalar-address-bar-bg),transparent 100%)0%,color-mix(in srgb,var(--scalar-address-bar-bg),transparent 20%)30%,var(--scalar-address-bar-bg)100%)}}.fade-right[data-v-78c9dbb9]{min-width:24px;right:-1px}@keyframes fadein-78c9dbb9{0%{opacity:0}1%{opacity:1}}.address-bar-bg-states[data-v-78c9dbb9]{--scalar-address-bar-bg:var(--scalar-background-1)}@supports (color:color-mix(in lab,red,red)){.address-bar-bg-states[data-v-78c9dbb9]{--scalar-address-bar-bg:color-mix(in srgb,var(--scalar-background-1),var(--scalar-background-2))}}.address-bar-bg-states[data-v-78c9dbb9]{background:var(--scalar-address-bar-bg)}.address-bar-bg-states[data-v-78c9dbb9]:has(.cm-focused){--scalar-address-bar-bg:var(--scalar-background-1);border-color:var(--scalar-border-color);outline:1px solid var(--scalar-color-accent)}.address-bar-bg-states:has(.cm-focused) .fade-left[data-v-78c9dbb9],.address-bar-bg-states:has(.cm-focused) .fade-right[data-v-78c9dbb9]{--scalar-address-bar-bg:var(--scalar-background-1)}.sidebar-height[data-v-dcff7b49]{min-height:100%}@media (min-width:800px){.sidebar-mask[data-v-dcff7b49]{-webkit-mask-image:linear-gradient(0,transparent 0,transparent 0,var(--scalar-background-2)30px);mask-image:linear-gradient(0,transparent 0,transparent 0,var(--scalar-background-2)30px)}}.resizer[data-v-dcff7b49]{cursor:col-resize;border-right:2px solid #0000;width:5px;transition:border-right-color .3s;position:absolute;top:0;bottom:0;right:0}.resizer[data-v-dcff7b49]:hover,.dragging .resizer[data-v-dcff7b49]{border-right-color:var(--scalar-background-3)}.dragging[data-v-dcff7b49]{cursor:col-resize}.dragging[data-v-dcff7b49]:before{content:\"\";width:100%;height:100%;display:block;position:absolute}.ellipsis-position[data-v-01a1ab71]{transform:translate(calc(-100% - 4.5px))}.indent-border-line-offset[data-v-4f5a9d1f]:before{left:var(--0bed2d4e)}.indent-padding-left[data-v-4f5a9d1f]{padding-left:calc(var(--57ee1db0) + 6px)}.sidebar-folderitem[data-v-4f5a9d1f] .ellipsis-position{right:6px;transform:none}.search-button-fade[data-v-bca9c474]{background:linear-gradient(var(--scalar-background-1)32px,var(--scalar-background-1)38px,transparent)}@supports (color:color-mix(in lab,red,red)){.search-button-fade[data-v-bca9c474]{background:linear-gradient(var(--scalar-background-1)32px,color-mix(in srgb,var(--scalar-background-1),transparent)38px,transparent)}}.empty-sidebar-item-content[data-v-bca9c474]{display:none}.empty-sidebar-item .empty-sidebar-item-content[data-v-bca9c474]{display:block}.rabbitjump[data-v-bca9c474]{opacity:0}.empty-sidebar-item:hover .rabbitjump[data-v-bca9c474]{opacity:1;animation:.5s step-end infinite rabbitAnimation-bca9c474}.empty-sidebar-item:hover .rabbitsit[data-v-bca9c474]{opacity:0;animation:.5s step-end infinite rabbitAnimation2-bca9c474}.empty-sidebar-item:hover .rabbit-ascii[data-v-bca9c474]{animation:8s linear infinite rabbitRun-bca9c474}@keyframes rabbitRun-bca9c474{0%{transform:translateZ(0)}25%{transform:translate(250px)}25.01%{transform:translate(-250px)}75%{transform:translate(250px)}75.01%{transform:translate(-250px)}to{transform:translateZ(0)}}@keyframes rabbitAnimation-bca9c474{0%,to{opacity:1}50%{opacity:0}}@keyframes rabbitAnimation2-bca9c474{0%,to{opacity:0}50%{opacity:1;transform:translateY(-8px)}}.request-text-color-text[data-v-f141e3af]{color:var(--scalar-color-1);background:linear-gradient(var(--scalar-background-1),var(--scalar-background-3));box-shadow:0 0 0 1px var(--scalar-border-color)}@media screen and (max-width:800px){.sidebar-active-hide-layout[data-v-f141e3af]{display:none}.sidebar-active-width[data-v-f141e3af]{width:100%}}.gitbook-show[data-v-c8df97c6]{display:none}.app-exit-button[data-v-c8df97c6]{color:#fff;background:#0000001a}.app-exit-button[data-v-c8df97c6]:hover{background:#ffffff1a}.request-text-color-text[data-v-57ae0d10]{color:var(--scalar-color-1);background:linear-gradient(var(--scalar-background-1),var(--scalar-background-3));box-shadow:0 0 0 1px var(--scalar-border-color)}@media screen and (max-width:800px){.sidebar-active-hide-layout[data-v-57ae0d10]{display:none}.sidebar-active-width[data-v-57ae0d10]{width:100%}}.group-hover-input[data-v-fced736a]{border-width:var(--scalar-border-width);border-color:#0000}.group:hover .group-hover-input[data-v-fced736a]{background:var(--scalar-background-1)}@supports (color:color-mix(in lab,red,red)){.group:hover .group-hover-input[data-v-fced736a]{background:color-mix(in srgb,var(--scalar-background-1),var(--scalar-background-2))}}.group:hover .group-hover-input[data-v-fced736a]{border-color:var(--scalar-border-color)}.group-hover-input[data-v-fced736a]:focus{border-color:var(--scalar-border-color)!important;background:0 0!important}[data-v-68d5218e] .markdown h2{font-size:var(--scalar-font-size-2)}[data-v-5997a667] .cm-content{min-height:fit-content}[data-v-5997a667] .cm-scroller{max-width:100%;overflow:auto hidden}[data-v-83bfcc8a] .cm-editor{padding:0}[data-v-83bfcc8a] .cm-content{font-family:var(--scalar-font);font-size:var(--scalar-small);background-color:#0000;align-items:center;width:100%;padding:5px 8px;display:flex}[data-v-83bfcc8a] .cm-content:has(.cm-pill){padding:5px 8px}[data-v-83bfcc8a] .cm-content .cm-pill:not(:last-of-type){margin-right:.5px}[data-v-83bfcc8a] .cm-content .cm-pill:not(:first-of-type){margin-left:.5px}[data-v-83bfcc8a] .cm-line{text-overflow:ellipsis;padding:0;overflow:hidden}.scalar-collection-auth[data-v-cc87292e]{border:var(--scalar-border-width)solid var(--scalar-border-color);border-radius:var(--scalar-radius-lg);overflow:hidden}.scalar-button-danger[data-v-f353959a]{background:var(--scalar-color-red)}@supports (color:color-mix(in lab,red,red)){.scalar-button-danger[data-v-f353959a]{background:color-mix(in srgb,var(--scalar-color-red),transparent 95%)}}.scalar-button-danger[data-v-f353959a]{color:var(--scalar-color-red)}.scalar-button-danger[data-v-f353959a]:hover,.scalar-button-danger[data-v-f353959a]:focus{background:var(--scalar-color-red)}@supports (color:color-mix(in lab,red,red)){.scalar-button-danger[data-v-f353959a]:hover,.scalar-button-danger[data-v-f353959a]:focus{background:color-mix(in srgb,var(--scalar-color-red),transparent 90%)}}.dragover-asChild[data-v-a89d6a6e],.dragover-above[data-v-a89d6a6e],.dragover-below[data-v-a89d6a6e]{position:relative}.dragover-above[data-v-a89d6a6e]:after,.dragover-below[data-v-a89d6a6e]:after{content:\"\";background:var(--scalar-color-blue);width:100%;height:3px;display:block;position:absolute;top:-1.5px}@supports (color:color-mix(in lab,red,red)){.dragover-above[data-v-a89d6a6e]:after,.dragover-below[data-v-a89d6a6e]:after{background:color-mix(in srgb,var(--scalar-color-blue),transparent 85%)}}.dragover-above[data-v-a89d6a6e]:after,.dragover-below[data-v-a89d6a6e]:after{pointer-events:none;border-radius:var(--scalar-radius)}.dragover-below[data-v-a89d6a6e]:after{top:initial;bottom:-1.5px}.dragover-asChild[data-v-a89d6a6e]:after{content:\"\";background:var(--scalar-color-blue);width:100%;height:100%;display:block;position:absolute;top:0;left:0}@supports (color:color-mix(in lab,red,red)){.dragover-asChild[data-v-a89d6a6e]:after{background:color-mix(in srgb,var(--scalar-color-blue),transparent 85%)}}.dragover-asChild[data-v-a89d6a6e]:after{pointer-events:none;border-radius:var(--scalar-radius)}.empty-variable-name[data-v-0b6c70e4]:empty:before{content:\"Untitled\";color:var(--scalar-color-3)}.form-group[data-v-694018d6]{margin-bottom:1rem}.modal-actions[data-v-694018d6]{justify-content:flex-end;gap:1rem;display:flex}.group-hover-input[data-v-5a23cb87]{border-width:var(--scalar-border-width);border-color:#0000}.group:hover .group-hover-input[data-v-5a23cb87]{background:var(--scalar-background-1)}@supports (color:color-mix(in lab,red,red)){.group:hover .group-hover-input[data-v-5a23cb87]{background:color-mix(in srgb,var(--scalar-background-1),var(--scalar-background-2))}}.group:hover .group-hover-input[data-v-5a23cb87]{border-color:var(--scalar-border-color)}.group-hover-input[data-v-5a23cb87]:focus{border-color:var(--scalar-border-color)!important;background:0 0!important}[data-v-7c1a2f6c] .cm-content{min-height:fit-content}[data-v-7c1a2f6c] .cm-scroller{max-width:100%;overflow:auto hidden}.scalar-collection-auth[data-v-8dd529fe]{border:var(--scalar-border-width)solid var(--scalar-border-color);border-radius:var(--scalar-radius-lg);overflow:hidden}.scalar-button-danger[data-v-369fbcff]{background:var(--scalar-color-red)}@supports (color:color-mix(in lab,red,red)){.scalar-button-danger[data-v-369fbcff]{background:color-mix(in srgb,var(--scalar-color-red),transparent 95%)}}.scalar-button-danger[data-v-369fbcff]{color:var(--scalar-color-red)}.scalar-button-danger[data-v-369fbcff]:hover,.scalar-button-danger[data-v-369fbcff]:focus{background:var(--scalar-color-red)}@supports (color:color-mix(in lab,red,red)){.scalar-button-danger[data-v-369fbcff]:hover,.scalar-button-danger[data-v-369fbcff]:focus{background:color-mix(in srgb,var(--scalar-color-red),transparent 90%)}}:root{--scalar-loaded-api-reference:true}@property --tw-translate-x{syntax:\"*\";inherits:false;initial-value:0}@property --tw-translate-y{syntax:\"*\";inherits:false;initial-value:0}@property --tw-translate-z{syntax:\"*\";inherits:false;initial-value:0}@property --tw-rotate-x{syntax:\"*\";inherits:false}@property --tw-rotate-y{syntax:\"*\";inherits:false}@property --tw-rotate-z{syntax:\"*\";inherits:false}@property --tw-skew-x{syntax:\"*\";inherits:false}@property --tw-skew-y{syntax:\"*\";inherits:false}@property --tw-border-style{syntax:\"*\";inherits:false;initial-value:solid}@property --tw-leading{syntax:\"*\";inherits:false}@property --tw-font-weight{syntax:\"*\";inherits:false}@property --tw-shadow{syntax:\"*\";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:\"*\";inherits:false}@property --tw-shadow-alpha{syntax:\"\";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:\"*\";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:\"*\";inherits:false}@property --tw-inset-shadow-alpha{syntax:\"\";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:\"*\";inherits:false}@property --tw-ring-shadow{syntax:\"*\";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:\"*\";inherits:false}@property --tw-inset-ring-shadow{syntax:\"*\";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:\"*\";inherits:false}@property --tw-ring-offset-width{syntax:\"\";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:\"*\";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:\"*\";inherits:false;initial-value:0 0 #0000}@property --tw-outline-style{syntax:\"*\";inherits:false;initial-value:solid}@property --tw-blur{syntax:\"*\";inherits:false}@property --tw-brightness{syntax:\"*\";inherits:false}@property --tw-contrast{syntax:\"*\";inherits:false}@property --tw-grayscale{syntax:\"*\";inherits:false}@property --tw-hue-rotate{syntax:\"*\";inherits:false}@property --tw-invert{syntax:\"*\";inherits:false}@property --tw-opacity{syntax:\"*\";inherits:false}@property --tw-saturate{syntax:\"*\";inherits:false}@property --tw-sepia{syntax:\"*\";inherits:false}@property --tw-drop-shadow{syntax:\"*\";inherits:false}@property --tw-drop-shadow-color{syntax:\"*\";inherits:false}@property --tw-drop-shadow-alpha{syntax:\"\";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:\"*\";inherits:false}@property --tw-duration{syntax:\"*\";inherits:false}@layer scalar-config{.scalar-api-reference[data-v-cc258b2c]{--refs-sidebar-width: var(--scalar-sidebar-width, 0px);--refs-header-height: calc( var(--scalar-custom-header-height) + var(--scalar-header-height, 0px) );--refs-viewport-offset: calc( var(--refs-header-height, 0px) + var(--refs-content-offset, 0px) );--refs-viewport-height: calc( var(--full-height, 100dvh) - var(--refs-viewport-offset, 0px) );--refs-content-max-width: var(--scalar-content-max-width, 1540px)}.scalar-api-reference.references-classic[data-v-cc258b2c]{--refs-content-max-width: var(--scalar-content-max-width, 1420px);min-height:100dvh;--refs-sidebar-width: 0}.scalar-api-reference[data-v-cc258b2c]:has(.api-reference-toolbar){--refs-content-offset: 48px}}.references-layout[data-v-cc258b2c]{min-height:100dvh;min-width:100%;max-width:100%;flex:1;--full-height: 100dvh;display:grid;grid-template-rows:var(--scalar-header-height, 0px) repeat(2,auto);grid-template-columns:auto 1fr;grid-template-areas:\"header header\" \"navigation rendered\" \"footer footer\";background:var(--scalar-background-1)}.references-editor[data-v-cc258b2c]{grid-area:editor;display:flex;min-width:0;background:var(--scalar-background-1)}.references-rendered[data-v-cc258b2c]{position:relative;grid-area:rendered;min-width:0;background:var(--scalar-background-1)}.scalar-api-reference.references-classic[data-v-cc258b2c],.references-classic .references-rendered[data-v-cc258b2c]{height:initial!important;max-height:initial!important}@layer scalar-config{.references-sidebar[data-v-cc258b2c]{--refs-sidebar-width: var(--scalar-sidebar-width, 280px)}}.references-footer[data-v-cc258b2c]{grid-area:footer}@media (max-width: 1000px){.references-layout[data-v-cc258b2c]{grid-template-columns:auto;grid-template-rows:var(--scalar-header-height, 0px) 0px auto auto;grid-template-areas:\"header\" \"navigation\" \"rendered\" \"footer\"}.references-editable[data-v-cc258b2c]{grid-template-areas:\"header\" \"navigation\" \"editor\"}.references-rendered[data-v-cc258b2c]{position:static}}@media (max-width: 1000px){.scalar-api-references-standalone-mobile[data-v-cc258b2c]{--scalar-header-height: 50px}}.darklight-reference[data-v-cc258b2c]{width:100%;margin-top:auto}')),document.head.appendChild(e)}}catch(e){console.error(\"vite-plugin-css-injected-by-js\",e)}}(),function(e){\"function\"==typeof define&&define.amd?define(e):e()}((function(){\"use strict\";const e=Object.freeze({status:\"aborted\"});function t(e,t,r){function n(r,n){var a;Object.defineProperty(r,\"_zod\",{value:r._zod??{},enumerable:!1}),(a=r._zod).traits??(a.traits=new Set),r._zod.traits.add(e),t(r,n);for(const e in i.prototype)e in r||Object.defineProperty(r,e,{value:i.prototype[e].bind(r)});r._zod.constr=i,r._zod.def=n}const a=r?.Parent??Object;class o extends a{}function i(e){var t;const a=r?.Parent?new o:this;n(a,e),(t=a._zod).deferred??(t.deferred=[]);for(const e of a._zod.deferred)e();return a}return Object.defineProperty(o,\"name\",{value:e}),Object.defineProperty(i,\"init\",{value:n}),Object.defineProperty(i,Symbol.hasInstance,{value:t=>!!(r?.Parent&&t instanceof r.Parent)||t?._zod?.traits?.has(e)}),Object.defineProperty(i,\"name\",{value:e}),i}const r=Symbol(\"zod_brand\");class n extends Error{constructor(){super(\"Encountered Promise during synchronous parse. Use .parseAsync() instead.\")}}class a extends Error{constructor(e){super(`Encountered unidirectional transform during encode: ${e}`),this.name=\"ZodEncodeError\"}}const o={};function i(e){return e&&Object.assign(o,e),o}function s(e){const t=Object.values(e).filter((e=>\"number\"==typeof e));return Object.entries(e).filter((([e,r])=>-1===t.indexOf(+e))).map((([e,t])=>t))}function l(e,t=\"|\"){return e.map((e=>$(e))).join(t)}function c(e,t){return\"bigint\"==typeof t?t.toString():t}function u(e){return{get value(){{const t=e();return Object.defineProperty(this,\"value\",{value:t}),t}}}}function d(e){return null==e}function p(e){const t=e.startsWith(\"^\")?1:0,r=e.endsWith(\"$\")?e.length-1:e.length;return e.slice(t,r)}function h(e,t){const r=(e.toString().split(\".\")[1]||\"\").length,n=t.toString();let a=(n.split(\".\")[1]||\"\").length;if(0===a&&/\\d?e-\\d?/.test(n)){const e=n.match(/\\d?e-(\\d?)/);e?.[1]&&(a=Number.parseInt(e[1]))}const o=r>a?r:a;return Number.parseInt(e.toFixed(o).replace(\".\",\"\"))%Number.parseInt(t.toFixed(o).replace(\".\",\"\"))/10**o}const f=Symbol(\"evaluating\");function m(e,t,r){let n;Object.defineProperty(e,t,{get(){if(n!==f)return void 0===n&&(n=f,n=r()),n},set(r){Object.defineProperty(e,t,{value:r})},configurable:!0})}function g(e,t,r){Object.defineProperty(e,t,{value:r,writable:!0,enumerable:!0,configurable:!0})}function v(...e){const t={};for(const r of e){const e=Object.getOwnPropertyDescriptors(r);Object.assign(t,e)}return Object.defineProperties({},t)}function b(e){return JSON.stringify(e)}const y=\"captureStackTrace\"in Error?Error.captureStackTrace:(...e)=>{};function O(e){return\"object\"==typeof e&&null!==e&&!Array.isArray(e)}const w=u((()=>{if(\"undefined\"!=typeof navigator&&navigator?.userAgent?.includes(\"Cloudflare\"))return!1;try{return new Function(\"\"),!0}catch(e){return!1}}));function x(e){if(!1===O(e))return!1;const t=e.constructor;if(void 0===t)return!0;const r=t.prototype;return!1!==O(r)&&!1!==Object.prototype.hasOwnProperty.call(r,\"isPrototypeOf\")}function k(e){return x(e)?{...e}:Array.isArray(e)?[...e]:e}const S=new Set([\"string\",\"number\",\"symbol\"]),_=new Set([\"string\",\"number\",\"bigint\",\"boolean\",\"symbol\",\"undefined\"]);function E(e){return e.replace(/[.*+?^${}()|[\\]\\\\]/g,\"\\\\$&\")}function T(e,t,r){const n=new e._zod.constr(t??e._zod.def);return t&&!r?.parent||(n._zod.parent=e),n}function A(e){const t=e;if(!t)return{};if(\"string\"==typeof t)return{error:()=>t};if(void 0!==t?.message){if(void 0!==t?.error)throw new Error(\"Cannot specify both `message` and `error` params\");t.error=t.message}return delete t.message,\"string\"==typeof t.error?{...t,error:()=>t.error}:t}function $(e){return\"bigint\"==typeof e?e.toString()+\"n\":\"string\"==typeof e?`\"${e}\"`:`${e}`}function C(e){return Object.keys(e).filter((t=>\"optional\"===e[t]._zod.optin&&\"optional\"===e[t]._zod.optout))}const P={safeint:[Number.MIN_SAFE_INTEGER,Number.MAX_SAFE_INTEGER],int32:[-2147483648,2147483647],uint32:[0,4294967295],float32:[-34028234663852886e22,34028234663852886e22],float64:[-Number.MAX_VALUE,Number.MAX_VALUE]},D={int64:[BigInt(\"-9223372036854775808\"),BigInt(\"9223372036854775807\")],uint64:[BigInt(0),BigInt(\"18446744073709551615\")]};function I(e,t){const r=e._zod.def;return T(e,v(e._zod.def,{get shape(){const e={};for(const n in t){if(!(n in r.shape))throw new Error(`Unrecognized key: \"${n}\"`);t[n]&&(e[n]=r.shape[n])}return g(this,\"shape\",e),e},checks:[]}))}function M(e,t){const r=e._zod.def,n=v(e._zod.def,{get shape(){const n={...e._zod.def.shape};for(const e in t){if(!(e in r.shape))throw new Error(`Unrecognized key: \"${e}\"`);t[e]&&delete n[e]}return g(this,\"shape\",n),n},checks:[]});return T(e,n)}function N(e,t){if(!x(t))throw new Error(\"Invalid input to extend: expected a plain object\");const r=e._zod.def.checks;if(r&&r.length>0)throw new Error(\"Object schemas containing refinements cannot be extended. Use `.safeExtend()` instead.\");const n=v(e._zod.def,{get shape(){const r={...e._zod.def.shape,...t};return g(this,\"shape\",r),r},checks:[]});return T(e,n)}function R(e,t){if(!x(t))throw new Error(\"Invalid input to safeExtend: expected a plain object\");const r={...e._zod.def,get shape(){const r={...e._zod.def.shape,...t};return g(this,\"shape\",r),r},checks:e._zod.def.checks};return T(e,r)}function j(e,t){const r=v(e._zod.def,{get shape(){const r={...e._zod.def.shape,...t._zod.def.shape};return g(this,\"shape\",r),r},get catchall(){return t._zod.def.catchall},checks:[]});return T(e,r)}function L(e,t,r){const n=v(t._zod.def,{get shape(){const n=t._zod.def.shape,a={...n};if(r)for(const t in r){if(!(t in n))throw new Error(`Unrecognized key: \"${t}\"`);r[t]&&(a[t]=e?new e({type:\"optional\",innerType:n[t]}):n[t])}else for(const t in n)a[t]=e?new e({type:\"optional\",innerType:n[t]}):n[t];return g(this,\"shape\",a),a},checks:[]});return T(t,n)}function U(e,t,r){const n=v(t._zod.def,{get shape(){const n=t._zod.def.shape,a={...n};if(r)for(const t in r){if(!(t in a))throw new Error(`Unrecognized key: \"${t}\"`);r[t]&&(a[t]=new e({type:\"nonoptional\",innerType:n[t]}))}else for(const t in n)a[t]=new e({type:\"nonoptional\",innerType:n[t]});return g(this,\"shape\",a),a},checks:[]});return T(t,n)}function B(e,t=0){if(!0===e.aborted)return!0;for(let r=t;r{var r;return(r=t).path??(r.path=[]),t.path.unshift(e),t}))}function F(e){return\"string\"==typeof e?e:e?.message}function Q(e,t,r){const n={...e,path:e.path??[]};if(!e.message){const a=F(e.inst?._zod.def?.error?.(e))??F(t?.error?.(e))??F(r.customError?.(e))??F(r.localeError?.(e))??\"Invalid input\";n.message=a}return delete n.inst,delete n.continue,t?.reportInput||delete n.input,n}function q(e){return e instanceof Set?\"set\":e instanceof Map?\"map\":e instanceof File?\"file\":\"unknown\"}function Z(e){return Array.isArray(e)?\"array\":\"string\"==typeof e?\"string\":\"unknown\"}function V(...e){const[t,r,n]=e;return\"string\"==typeof t?{message:t,code:\"custom\",input:r,inst:n}:{...t}}function H(e){const t=atob(e),r=new Uint8Array(t.length);for(let e=0;eNumber.isNaN(Number.parseInt(e,10)))).map((e=>e[1]))},cleanRegex:p,clone:T,cloneDef:function(e){return v(e._zod.def)},createTransparentProxy:function(e){let t;return new Proxy({},{get:(r,n,a)=>(t??(t=e()),Reflect.get(t,n,a)),set:(r,n,a,o)=>(t??(t=e()),Reflect.set(t,n,a,o)),has:(r,n)=>(t??(t=e()),Reflect.has(t,n)),deleteProperty:(r,n)=>(t??(t=e()),Reflect.deleteProperty(t,n)),ownKeys:r=>(t??(t=e()),Reflect.ownKeys(t)),getOwnPropertyDescriptor:(r,n)=>(t??(t=e()),Reflect.getOwnPropertyDescriptor(t,n)),defineProperty:(r,n,a)=>(t??(t=e()),Reflect.defineProperty(t,n,a))})},defineLazy:m,esc:b,escapeRegex:E,extend:N,finalizeIssue:Q,floatSafeRemainder:h,getElementAtPath:function(e,t){return t?t.reduce(((e,t)=>e?.[t]),e):e},getEnumValues:s,getLengthableOrigin:Z,getParsedType:e=>{const t=typeof e;switch(t){case\"undefined\":return\"undefined\";case\"string\":return\"string\";case\"number\":return Number.isNaN(e)?\"nan\":\"number\";case\"boolean\":return\"boolean\";case\"function\":return\"function\";case\"bigint\":return\"bigint\";case\"symbol\":return\"symbol\";case\"object\":return Array.isArray(e)?\"array\":null===e?\"null\":e.then&&\"function\"==typeof e.then&&e.catch&&\"function\"==typeof e.catch?\"promise\":\"undefined\"!=typeof Map&&e instanceof Map?\"map\":\"undefined\"!=typeof Set&&e instanceof Set?\"set\":\"undefined\"!=typeof Date&&e instanceof Date?\"date\":\"undefined\"!=typeof File&&e instanceof File?\"file\":\"object\";default:throw new Error(`Unknown data type: ${t}`)}},getSizableOrigin:q,hexToUint8Array:function(e){const t=e.replace(/^0x/,\"\");if(t.length%2!=0)throw new Error(\"Invalid hex string length\");const r=new Uint8Array(t.length/2);for(let e=0;ee[t]));return Promise.all(r).then((e=>{const r={};for(let n=0;ne.toString(16).padStart(2,\"0\"))).join(\"\")},unwrapMessage:F},Symbol.toStringTag,{value:\"Module\"})),G=(e,t)=>{e.name=\"$ZodError\",Object.defineProperty(e,\"_zod\",{value:e._zod,enumerable:!1}),Object.defineProperty(e,\"issues\",{value:t,enumerable:!1}),e.message=JSON.stringify(t,c,2),Object.defineProperty(e,\"toString\",{value:()=>e.message,enumerable:!1})},Y=t(\"$ZodError\",G),K=t(\"$ZodError\",G,{Parent:Error});function J(e,t=e=>e.message){const r={},n=[];for(const a of e.issues)a.path.length>0?(r[a.path[0]]=r[a.path[0]]||[],r[a.path[0]].push(t(a))):n.push(t(a));return{formErrors:n,fieldErrors:r}}function ee(e,t){const r=t||function(e){return e.message},n={_errors:[]},a=e=>{for(const t of e.issues)if(\"invalid_union\"===t.code&&t.errors.length)t.errors.map((e=>a({issues:e})));else if(\"invalid_key\"===t.code)a({issues:t.issues});else if(\"invalid_element\"===t.code)a({issues:t.issues});else if(0===t.path.length)n._errors.push(r(t));else{let e=n,a=0;for(;a{var o,i;for(const s of e.issues)if(\"invalid_union\"===s.code&&s.errors.length)s.errors.map((e=>a({issues:e},s.path)));else if(\"invalid_key\"===s.code)a({issues:s.issues},s.path);else if(\"invalid_element\"===s.code)a({issues:s.issues},s.path);else{const e=[...t,...s.path];if(0===e.length){n.errors.push(r(s));continue}let a=n,l=0;for(;l\"object\"==typeof e?e.key:e));for(const e of r)\"number\"==typeof e?t.push(`[${e}]`):\"symbol\"==typeof e?t.push(`[${JSON.stringify(String(e))}]`):/[^\\w$]/.test(e)?t.push(`[${JSON.stringify(e)}]`):(t.length&&t.push(\".\"),t.push(e));return t.join(\"\")}function ne(e){const t=[],r=[...e.issues].sort(((e,t)=>(e.path??[]).length-(t.path??[]).length));for(const e of r)t.push(`✖ ${e.message}`),e.path?.length&&t.push(` → at ${re(e.path)}`);return t.join(\"\\n\")}const ae=e=>(t,r,a,o)=>{const s=a?Object.assign(a,{async:!1}):{async:!1},l=t._zod.run({value:r,issues:[]},s);if(l instanceof Promise)throw new n;if(l.issues.length){const t=new(o?.Err??e)(l.issues.map((e=>Q(e,s,i()))));throw y(t,o?.callee),t}return l.value},oe=ae(K),ie=e=>async(t,r,n,a)=>{const o=n?Object.assign(n,{async:!0}):{async:!0};let s=t._zod.run({value:r,issues:[]},o);if(s instanceof Promise&&(s=await s),s.issues.length){const t=new(a?.Err??e)(s.issues.map((e=>Q(e,o,i()))));throw y(t,a?.callee),t}return s.value},se=ie(K),le=e=>(t,r,a)=>{const o=a?{...a,async:!1}:{async:!1},s=t._zod.run({value:r,issues:[]},o);if(s instanceof Promise)throw new n;return s.issues.length?{success:!1,error:new(e??Y)(s.issues.map((e=>Q(e,o,i()))))}:{success:!0,data:s.value}},ce=le(K),ue=e=>async(t,r,n)=>{const a=n?Object.assign(n,{async:!0}):{async:!0};let o=t._zod.run({value:r,issues:[]},a);return o instanceof Promise&&(o=await o),o.issues.length?{success:!1,error:new e(o.issues.map((e=>Q(e,a,i()))))}:{success:!0,data:o.value}},de=ue(K),pe=e=>(t,r,n)=>{const a=n?Object.assign(n,{direction:\"backward\"}):{direction:\"backward\"};return ae(e)(t,r,a)},he=pe(K),fe=e=>(t,r,n)=>ae(e)(t,r,n),me=fe(K),ge=e=>async(t,r,n)=>{const a=n?Object.assign(n,{direction:\"backward\"}):{direction:\"backward\"};return ie(e)(t,r,a)},ve=ge(K),be=e=>async(t,r,n)=>ie(e)(t,r,n),ye=be(K),Oe=e=>(t,r,n)=>{const a=n?Object.assign(n,{direction:\"backward\"}):{direction:\"backward\"};return le(e)(t,r,a)},we=Oe(K),xe=e=>(t,r,n)=>le(e)(t,r,n),ke=xe(K),Se=e=>async(t,r,n)=>{const a=n?Object.assign(n,{direction:\"backward\"}):{direction:\"backward\"};return ue(e)(t,r,a)},_e=Se(K),Ee=e=>async(t,r,n)=>ue(e)(t,r,n),Te=Ee(K),Ae=/^[cC][^\\s-]{8,}$/,$e=/^[0-9a-z]+$/,Ce=/^[0-9A-HJKMNP-TV-Za-hjkmnp-tv-z]{26}$/,Pe=/^[0-9a-vA-V]{20}$/,De=/^[A-Za-z0-9]{27}$/,Ie=/^[a-zA-Z0-9_-]{21}$/,Me=/^P(?:(\\d+W)|(?!.*W)(?=\\d|T\\d)(\\d+Y)?(\\d+M)?(\\d+D)?(T(?=\\d)(\\d+H)?(\\d+M)?(\\d+([.,]\\d+)?S)?)?)$/,Ne=/^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$/,Re=e=>e?new RegExp(`^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-${e}[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$`):/^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$/,je=Re(4),Le=Re(6),Ue=Re(7),Be=/^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$/,ze=/^[^\\s@\"]{1,64}@[^\\s@]{1,255}$/u,Fe=ze;function Qe(){return new RegExp(\"^(\\\\p{Extended_Pictographic}|\\\\p{Emoji_Component})+$\",\"u\")}const qe=/^(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])$/,Ze=/^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:))$/,Ve=/^((25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\\.){3}(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\\/([0-9]|[1-2][0-9]|3[0-2])$/,He=/^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|::|([0-9a-fA-F]{1,4})?::([0-9a-fA-F]{1,4}:?){0,6})\\/(12[0-8]|1[01][0-9]|[1-9]?[0-9])$/,We=/^$|^(?:[0-9a-zA-Z+/]{4})*(?:(?:[0-9a-zA-Z+/]{2}==)|(?:[0-9a-zA-Z+/]{3}=))?$/,Xe=/^[A-Za-z0-9_-]*$/,Ge=/^(?=.{1,253}\\.?$)[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[-0-9a-zA-Z]{0,61}[0-9a-zA-Z])?)*\\.?$/,Ye=/^([a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\\.)+[a-zA-Z]{2,}$/,Ke=/^\\+(?:[0-9]){6,14}[0-9]$/,Je=\"(?:(?:\\\\d\\\\d[2468][048]|\\\\d\\\\d[13579][26]|\\\\d\\\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\\\d|30)|(?:02)-(?:0[1-9]|1\\\\d|2[0-8])))\",et=new RegExp(`^${Je}$`);function tt(e){const t=\"(?:[01]\\\\d|2[0-3]):[0-5]\\\\d\";return\"number\"==typeof e.precision?-1===e.precision?`${t}`:0===e.precision?`${t}:[0-5]\\\\d`:`${t}:[0-5]\\\\d\\\\.\\\\d{${e.precision}}`:`${t}(?::[0-5]\\\\d(?:\\\\.\\\\d+)?)?`}function rt(e){return new RegExp(`^${tt(e)}$`)}function nt(e){const t=tt({precision:e.precision}),r=[\"Z\"];e.local&&r.push(\"\"),e.offset&&r.push(\"([+-](?:[01]\\\\d|2[0-3]):[0-5]\\\\d)\");const n=`${t}(?:${r.join(\"|\")})`;return new RegExp(`^${Je}T(?:${n})$`)}const at=e=>new RegExp(`^${e?`[\\\\s\\\\S]{${e?.minimum??0},${e?.maximum??\"\"}}`:\"[\\\\s\\\\S]*\"}$`),ot=/^-?\\d+n?$/,it=/^-?\\d+$/,st=/^-?\\d+(?:\\.\\d+)?/,lt=/^(?:true|false)$/i,ct=/^null$/i,ut=/^undefined$/i,dt=/^[^A-Z]*$/,pt=/^[^a-z]*$/,ht=/^[0-9a-fA-F]*$/;function ft(e,t){return new RegExp(`^[A-Za-z0-9+/]{${e}}${t}$`)}function mt(e){return new RegExp(`^[A-Za-z0-9_-]{${e}}$`)}const gt=ft(22,\"==\"),vt=mt(22),bt=ft(27,\"=\"),yt=mt(27),Ot=ft(43,\"=\"),wt=mt(43),xt=ft(64,\"\"),kt=mt(64),St=ft(86,\"==\"),_t=mt(86),Et=Object.freeze(Object.defineProperty({__proto__:null,base64:We,base64url:Xe,bigint:ot,boolean:lt,browserEmail:/^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/,cidrv4:Ve,cidrv6:He,cuid:Ae,cuid2:$e,date:et,datetime:nt,domain:Ye,duration:Me,e164:Ke,email:Be,emoji:Qe,extendedDuration:/^[-+]?P(?!$)(?:(?:[-+]?\\d+Y)|(?:[-+]?\\d+[.,]\\d+Y$))?(?:(?:[-+]?\\d+M)|(?:[-+]?\\d+[.,]\\d+M$))?(?:(?:[-+]?\\d+W)|(?:[-+]?\\d+[.,]\\d+W$))?(?:(?:[-+]?\\d+D)|(?:[-+]?\\d+[.,]\\d+D$))?(?:T(?=[\\d+-])(?:(?:[-+]?\\d+H)|(?:[-+]?\\d+[.,]\\d+H$))?(?:(?:[-+]?\\d+M)|(?:[-+]?\\d+[.,]\\d+M$))?(?:[-+]?\\d+(?:[.,]\\d+)?S)?)??$/,guid:Ne,hex:ht,hostname:Ge,html5Email:/^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/,idnEmail:Fe,integer:it,ipv4:qe,ipv6:Ze,ksuid:De,lowercase:dt,md5_base64:gt,md5_base64url:vt,md5_hex:/^[0-9a-fA-F]{32}$/,nanoid:Ie,null:ct,number:st,rfc5322Email:/^(([^<>()\\[\\]\\\\.,;:\\s@\"]+(\\.[^<>()\\[\\]\\\\.,;:\\s@\"]+)*)|(\".+\"))@((\\[[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}])|(([a-zA-Z\\-0-9]+\\.)+[a-zA-Z]{2,}))$/,sha1_base64:bt,sha1_base64url:yt,sha1_hex:/^[0-9a-fA-F]{40}$/,sha256_base64:Ot,sha256_base64url:wt,sha256_hex:/^[0-9a-fA-F]{64}$/,sha384_base64:xt,sha384_base64url:kt,sha384_hex:/^[0-9a-fA-F]{96}$/,sha512_base64:St,sha512_base64url:_t,sha512_hex:/^[0-9a-fA-F]{128}$/,string:at,time:rt,ulid:Ce,undefined:ut,unicodeEmail:ze,uppercase:pt,uuid:Re,uuid4:je,uuid6:Le,uuid7:Ue,xid:Pe},Symbol.toStringTag,{value:\"Module\"})),Tt=t(\"$ZodCheck\",((e,t)=>{var r;e._zod??(e._zod={}),e._zod.def=t,(r=e._zod).onattach??(r.onattach=[])})),At={number:\"number\",bigint:\"bigint\",object:\"date\"},$t=t(\"$ZodCheckLessThan\",((e,t)=>{Tt.init(e,t);const r=At[typeof t.value];e._zod.onattach.push((e=>{const r=e._zod.bag,n=(t.inclusive?r.maximum:r.exclusiveMaximum)??Number.POSITIVE_INFINITY;t.value{(t.inclusive?n.value<=t.value:n.value{Tt.init(e,t);const r=At[typeof t.value];e._zod.onattach.push((e=>{const r=e._zod.bag,n=(t.inclusive?r.minimum:r.exclusiveMinimum)??Number.NEGATIVE_INFINITY;t.value>n&&(t.inclusive?r.minimum=t.value:r.exclusiveMinimum=t.value)})),e._zod.check=n=>{(t.inclusive?n.value>=t.value:n.value>t.value)||n.issues.push({origin:r,code:\"too_small\",minimum:t.value,input:n.value,inclusive:t.inclusive,inst:e,continue:!t.abort})}})),Pt=t(\"$ZodCheckMultipleOf\",((e,t)=>{Tt.init(e,t),e._zod.onattach.push((e=>{var r;(r=e._zod.bag).multipleOf??(r.multipleOf=t.value)})),e._zod.check=r=>{if(typeof r.value!=typeof t.value)throw new Error(\"Cannot mix number and bigint in multiple_of check.\");(\"bigint\"==typeof r.value?r.value%t.value===BigInt(0):0===h(r.value,t.value))||r.issues.push({origin:typeof r.value,code:\"not_multiple_of\",divisor:t.value,input:r.value,inst:e,continue:!t.abort})}})),Dt=t(\"$ZodCheckNumberFormat\",((e,t)=>{Tt.init(e,t),t.format=t.format||\"float64\";const r=t.format?.includes(\"int\"),n=r?\"int\":\"number\",[a,o]=P[t.format];e._zod.onattach.push((e=>{const n=e._zod.bag;n.format=t.format,n.minimum=a,n.maximum=o,r&&(n.pattern=it)})),e._zod.check=i=>{const s=i.value;if(r){if(!Number.isInteger(s))return void i.issues.push({expected:n,format:t.format,code:\"invalid_type\",continue:!1,input:s,inst:e});if(!Number.isSafeInteger(s))return void(s>0?i.issues.push({input:s,code:\"too_big\",maximum:Number.MAX_SAFE_INTEGER,note:\"Integers must be within the safe integer range.\",inst:e,origin:n,continue:!t.abort}):i.issues.push({input:s,code:\"too_small\",minimum:Number.MIN_SAFE_INTEGER,note:\"Integers must be within the safe integer range.\",inst:e,origin:n,continue:!t.abort}))}so&&i.issues.push({origin:\"number\",input:s,code:\"too_big\",maximum:o,inst:e})}})),It=t(\"$ZodCheckBigIntFormat\",((e,t)=>{Tt.init(e,t);const[r,n]=D[t.format];e._zod.onattach.push((e=>{const a=e._zod.bag;a.format=t.format,a.minimum=r,a.maximum=n})),e._zod.check=a=>{const o=a.value;on&&a.issues.push({origin:\"bigint\",input:o,code:\"too_big\",maximum:n,inst:e})}})),Mt=t(\"$ZodCheckMaxSize\",((e,t)=>{var r;Tt.init(e,t),(r=e._zod.def).when??(r.when=e=>{const t=e.value;return!d(t)&&void 0!==t.size}),e._zod.onattach.push((e=>{const r=e._zod.bag.maximum??Number.POSITIVE_INFINITY;t.maximum{const n=r.value;n.size<=t.maximum||r.issues.push({origin:q(n),code:\"too_big\",maximum:t.maximum,inclusive:!0,input:n,inst:e,continue:!t.abort})}})),Nt=t(\"$ZodCheckMinSize\",((e,t)=>{var r;Tt.init(e,t),(r=e._zod.def).when??(r.when=e=>{const t=e.value;return!d(t)&&void 0!==t.size}),e._zod.onattach.push((e=>{const r=e._zod.bag.minimum??Number.NEGATIVE_INFINITY;t.minimum>r&&(e._zod.bag.minimum=t.minimum)})),e._zod.check=r=>{const n=r.value;n.size>=t.minimum||r.issues.push({origin:q(n),code:\"too_small\",minimum:t.minimum,inclusive:!0,input:n,inst:e,continue:!t.abort})}})),Rt=t(\"$ZodCheckSizeEquals\",((e,t)=>{var r;Tt.init(e,t),(r=e._zod.def).when??(r.when=e=>{const t=e.value;return!d(t)&&void 0!==t.size}),e._zod.onattach.push((e=>{const r=e._zod.bag;r.minimum=t.size,r.maximum=t.size,r.size=t.size})),e._zod.check=r=>{const n=r.value,a=n.size;if(a===t.size)return;const o=a>t.size;r.issues.push({origin:q(n),...o?{code:\"too_big\",maximum:t.size}:{code:\"too_small\",minimum:t.size},inclusive:!0,exact:!0,input:r.value,inst:e,continue:!t.abort})}})),jt=t(\"$ZodCheckMaxLength\",((e,t)=>{var r;Tt.init(e,t),(r=e._zod.def).when??(r.when=e=>{const t=e.value;return!d(t)&&void 0!==t.length}),e._zod.onattach.push((e=>{const r=e._zod.bag.maximum??Number.POSITIVE_INFINITY;t.maximum{const n=r.value;if(n.length<=t.maximum)return;const a=Z(n);r.issues.push({origin:a,code:\"too_big\",maximum:t.maximum,inclusive:!0,input:n,inst:e,continue:!t.abort})}})),Lt=t(\"$ZodCheckMinLength\",((e,t)=>{var r;Tt.init(e,t),(r=e._zod.def).when??(r.when=e=>{const t=e.value;return!d(t)&&void 0!==t.length}),e._zod.onattach.push((e=>{const r=e._zod.bag.minimum??Number.NEGATIVE_INFINITY;t.minimum>r&&(e._zod.bag.minimum=t.minimum)})),e._zod.check=r=>{const n=r.value;if(n.length>=t.minimum)return;const a=Z(n);r.issues.push({origin:a,code:\"too_small\",minimum:t.minimum,inclusive:!0,input:n,inst:e,continue:!t.abort})}})),Ut=t(\"$ZodCheckLengthEquals\",((e,t)=>{var r;Tt.init(e,t),(r=e._zod.def).when??(r.when=e=>{const t=e.value;return!d(t)&&void 0!==t.length}),e._zod.onattach.push((e=>{const r=e._zod.bag;r.minimum=t.length,r.maximum=t.length,r.length=t.length})),e._zod.check=r=>{const n=r.value,a=n.length;if(a===t.length)return;const o=Z(n),i=a>t.length;r.issues.push({origin:o,...i?{code:\"too_big\",maximum:t.length}:{code:\"too_small\",minimum:t.length},inclusive:!0,exact:!0,input:r.value,inst:e,continue:!t.abort})}})),Bt=t(\"$ZodCheckStringFormat\",((e,t)=>{var r,n;Tt.init(e,t),e._zod.onattach.push((e=>{const r=e._zod.bag;r.format=t.format,t.pattern&&(r.patterns??(r.patterns=new Set),r.patterns.add(t.pattern))})),t.pattern?(r=e._zod).check??(r.check=r=>{t.pattern.lastIndex=0,t.pattern.test(r.value)||r.issues.push({origin:\"string\",code:\"invalid_format\",format:t.format,input:r.value,...t.pattern?{pattern:t.pattern.toString()}:{},inst:e,continue:!t.abort})}):(n=e._zod).check??(n.check=()=>{})})),zt=t(\"$ZodCheckRegex\",((e,t)=>{Bt.init(e,t),e._zod.check=r=>{t.pattern.lastIndex=0,t.pattern.test(r.value)||r.issues.push({origin:\"string\",code:\"invalid_format\",format:\"regex\",input:r.value,pattern:t.pattern.toString(),inst:e,continue:!t.abort})}})),Ft=t(\"$ZodCheckLowerCase\",((e,t)=>{t.pattern??(t.pattern=dt),Bt.init(e,t)})),Qt=t(\"$ZodCheckUpperCase\",((e,t)=>{t.pattern??(t.pattern=pt),Bt.init(e,t)})),qt=t(\"$ZodCheckIncludes\",((e,t)=>{Tt.init(e,t);const r=E(t.includes),n=new RegExp(\"number\"==typeof t.position?`^.{${t.position}}${r}`:r);t.pattern=n,e._zod.onattach.push((e=>{const t=e._zod.bag;t.patterns??(t.patterns=new Set),t.patterns.add(n)})),e._zod.check=r=>{r.value.includes(t.includes,t.position)||r.issues.push({origin:\"string\",code:\"invalid_format\",format:\"includes\",includes:t.includes,input:r.value,inst:e,continue:!t.abort})}})),Zt=t(\"$ZodCheckStartsWith\",((e,t)=>{Tt.init(e,t);const r=new RegExp(`^${E(t.prefix)}.*`);t.pattern??(t.pattern=r),e._zod.onattach.push((e=>{const t=e._zod.bag;t.patterns??(t.patterns=new Set),t.patterns.add(r)})),e._zod.check=r=>{r.value.startsWith(t.prefix)||r.issues.push({origin:\"string\",code:\"invalid_format\",format:\"starts_with\",prefix:t.prefix,input:r.value,inst:e,continue:!t.abort})}})),Vt=t(\"$ZodCheckEndsWith\",((e,t)=>{Tt.init(e,t);const r=new RegExp(`.*${E(t.suffix)}$`);t.pattern??(t.pattern=r),e._zod.onattach.push((e=>{const t=e._zod.bag;t.patterns??(t.patterns=new Set),t.patterns.add(r)})),e._zod.check=r=>{r.value.endsWith(t.suffix)||r.issues.push({origin:\"string\",code:\"invalid_format\",format:\"ends_with\",suffix:t.suffix,input:r.value,inst:e,continue:!t.abort})}}));function Ht(e,t,r){e.issues.length&&t.issues.push(...z(r,e.issues))}const Wt=t(\"$ZodCheckProperty\",((e,t)=>{Tt.init(e,t),e._zod.check=e=>{const r=t.schema._zod.run({value:e.value[t.property],issues:[]},{});if(r instanceof Promise)return r.then((r=>Ht(r,e,t.property)));Ht(r,e,t.property)}})),Xt=t(\"$ZodCheckMimeType\",((e,t)=>{Tt.init(e,t);const r=new Set(t.mime);e._zod.onattach.push((e=>{e._zod.bag.mime=t.mime})),e._zod.check=n=>{r.has(n.value.type)||n.issues.push({code:\"invalid_value\",values:t.mime,input:n.value.type,inst:e,continue:!t.abort})}})),Gt=t(\"$ZodCheckOverwrite\",((e,t)=>{Tt.init(e,t),e._zod.check=e=>{e.value=t.tx(e.value)}}));class Yt{constructor(e=[]){this.content=[],this.indent=0,this&&(this.args=e)}indented(e){this.indent+=1,e(this),this.indent-=1}write(e){if(\"function\"==typeof e)return e(this,{execution:\"sync\"}),void e(this,{execution:\"async\"});const t=e.split(\"\\n\").filter((e=>e)),r=Math.min(...t.map((e=>e.length-e.trimStart().length))),n=t.map((e=>e.slice(r))).map((e=>\" \".repeat(2*this.indent)+e));for(const e of n)this.content.push(e)}compile(){const e=Function,t=this?.args;return new e(...t,[...(this?.content??[\"\"]).map((e=>` ${e}`))].join(\"\\n\"))}}const Kt={major:4,minor:1,patch:11},Jt=t(\"$ZodType\",((e,t)=>{var r;e??(e={}),e._zod.def=t,e._zod.bag=e._zod.bag||{},e._zod.version=Kt;const a=[...e._zod.def.checks??[]];e._zod.traits.has(\"$ZodCheck\")&&a.unshift(e);for(const t of a)for(const r of t._zod.onattach)r(e);if(0===a.length)(r=e._zod).deferred??(r.deferred=[]),e._zod.deferred?.push((()=>{e._zod.run=e._zod.parse}));else{const t=(e,t,r)=>{let a,o=B(e);for(const i of t){if(i._zod.def.when){if(!i._zod.def.when(e))continue}else if(o)continue;const t=e.issues.length,s=i._zod.check(e);if(s instanceof Promise&&!1===r?.async)throw new n;if(a||s instanceof Promise)a=(a??Promise.resolve()).then((async()=>{await s,e.issues.length!==t&&(o||(o=B(e,t)))}));else{if(e.issues.length===t)continue;o||(o=B(e,t))}}return a?a.then((()=>e)):e},r=(r,o,i)=>{if(B(r))return r.aborted=!0,r;const s=t(o,a,i);if(s instanceof Promise){if(!1===i.async)throw new n;return s.then((t=>e._zod.parse(t,i)))}return e._zod.parse(s,i)};e._zod.run=(o,i)=>{if(i.skipChecks)return e._zod.parse(o,i);if(\"backward\"===i.direction){const t=e._zod.parse({value:o.value,issues:[]},{...i,skipChecks:!0});return t instanceof Promise?t.then((e=>r(e,o,i))):r(t,o,i)}const s=e._zod.parse(o,i);if(s instanceof Promise){if(!1===i.async)throw new n;return s.then((e=>t(e,a,i)))}return t(s,a,i)}}e[\"~standard\"]={validate:t=>{try{const r=ce(e,t);return r.success?{value:r.data}:{issues:r.error?.issues}}catch(r){return de(e,t).then((e=>e.success?{value:e.data}:{issues:e.error?.issues}))}},vendor:\"zod\",version:1}})),er=t(\"$ZodString\",((e,t)=>{Jt.init(e,t),e._zod.pattern=[...e?._zod.bag?.patterns??[]].pop()??at(e._zod.bag),e._zod.parse=(r,n)=>{if(t.coerce)try{r.value=String(r.value)}catch(e){}return\"string\"==typeof r.value||r.issues.push({expected:\"string\",code:\"invalid_type\",input:r.value,inst:e}),r}})),tr=t(\"$ZodStringFormat\",((e,t)=>{Bt.init(e,t),er.init(e,t)})),rr=t(\"$ZodGUID\",((e,t)=>{t.pattern??(t.pattern=Ne),tr.init(e,t)})),nr=t(\"$ZodUUID\",((e,t)=>{if(t.version){const e={v1:1,v2:2,v3:3,v4:4,v5:5,v6:6,v7:7,v8:8}[t.version];if(void 0===e)throw new Error(`Invalid UUID version: \"${t.version}\"`);t.pattern??(t.pattern=Re(e))}else t.pattern??(t.pattern=Re());tr.init(e,t)})),ar=t(\"$ZodEmail\",((e,t)=>{t.pattern??(t.pattern=Be),tr.init(e,t)})),or=t(\"$ZodURL\",((e,t)=>{tr.init(e,t),e._zod.check=r=>{try{const n=r.value.trim(),a=new URL(n);return t.hostname&&(t.hostname.lastIndex=0,t.hostname.test(a.hostname)||r.issues.push({code:\"invalid_format\",format:\"url\",note:\"Invalid hostname\",pattern:Ge.source,input:r.value,inst:e,continue:!t.abort})),t.protocol&&(t.protocol.lastIndex=0,t.protocol.test(a.protocol.endsWith(\":\")?a.protocol.slice(0,-1):a.protocol)||r.issues.push({code:\"invalid_format\",format:\"url\",note:\"Invalid protocol\",pattern:t.protocol.source,input:r.value,inst:e,continue:!t.abort})),void(t.normalize?r.value=a.href:r.value=n)}catch(n){r.issues.push({code:\"invalid_format\",format:\"url\",input:r.value,inst:e,continue:!t.abort})}}})),ir=t(\"$ZodEmoji\",((e,t)=>{t.pattern??(t.pattern=Qe()),tr.init(e,t)})),sr=t(\"$ZodNanoID\",((e,t)=>{t.pattern??(t.pattern=Ie),tr.init(e,t)})),lr=t(\"$ZodCUID\",((e,t)=>{t.pattern??(t.pattern=Ae),tr.init(e,t)})),cr=t(\"$ZodCUID2\",((e,t)=>{t.pattern??(t.pattern=$e),tr.init(e,t)})),ur=t(\"$ZodULID\",((e,t)=>{t.pattern??(t.pattern=Ce),tr.init(e,t)})),dr=t(\"$ZodXID\",((e,t)=>{t.pattern??(t.pattern=Pe),tr.init(e,t)})),pr=t(\"$ZodKSUID\",((e,t)=>{t.pattern??(t.pattern=De),tr.init(e,t)})),hr=t(\"$ZodISODateTime\",((e,t)=>{t.pattern??(t.pattern=nt(t)),tr.init(e,t)})),fr=t(\"$ZodISODate\",((e,t)=>{t.pattern??(t.pattern=et),tr.init(e,t)})),mr=t(\"$ZodISOTime\",((e,t)=>{t.pattern??(t.pattern=rt(t)),tr.init(e,t)})),gr=t(\"$ZodISODuration\",((e,t)=>{t.pattern??(t.pattern=Me),tr.init(e,t)})),vr=t(\"$ZodIPv4\",((e,t)=>{t.pattern??(t.pattern=qe),tr.init(e,t),e._zod.onattach.push((e=>{e._zod.bag.format=\"ipv4\"}))})),br=t(\"$ZodIPv6\",((e,t)=>{t.pattern??(t.pattern=Ze),tr.init(e,t),e._zod.onattach.push((e=>{e._zod.bag.format=\"ipv6\"})),e._zod.check=r=>{try{new URL(`http://[${r.value}]`)}catch{r.issues.push({code:\"invalid_format\",format:\"ipv6\",input:r.value,inst:e,continue:!t.abort})}}})),yr=t(\"$ZodCIDRv4\",((e,t)=>{t.pattern??(t.pattern=Ve),tr.init(e,t)})),Or=t(\"$ZodCIDRv6\",((e,t)=>{t.pattern??(t.pattern=He),tr.init(e,t),e._zod.check=r=>{const n=r.value.split(\"/\");try{if(2!==n.length)throw new Error;const[e,t]=n;if(!t)throw new Error;const r=Number(t);if(`${r}`!==t)throw new Error;if(r<0||r>128)throw new Error;new URL(`http://[${e}]`)}catch{r.issues.push({code:\"invalid_format\",format:\"cidrv6\",input:r.value,inst:e,continue:!t.abort})}}}));function wr(e){if(\"\"===e)return!0;if(e.length%4!=0)return!1;try{return atob(e),!0}catch{return!1}}const xr=t(\"$ZodBase64\",((e,t)=>{t.pattern??(t.pattern=We),tr.init(e,t),e._zod.onattach.push((e=>{e._zod.bag.contentEncoding=\"base64\"})),e._zod.check=r=>{wr(r.value)||r.issues.push({code:\"invalid_format\",format:\"base64\",input:r.value,inst:e,continue:!t.abort})}}));function kr(e){if(!Xe.test(e))return!1;const t=e.replace(/[-_]/g,(e=>\"-\"===e?\"+\":\"/\"));return wr(t.padEnd(4*Math.ceil(t.length/4),\"=\"))}const Sr=t(\"$ZodBase64URL\",((e,t)=>{t.pattern??(t.pattern=Xe),tr.init(e,t),e._zod.onattach.push((e=>{e._zod.bag.contentEncoding=\"base64url\"})),e._zod.check=r=>{kr(r.value)||r.issues.push({code:\"invalid_format\",format:\"base64url\",input:r.value,inst:e,continue:!t.abort})}})),_r=t(\"$ZodE164\",((e,t)=>{t.pattern??(t.pattern=Ke),tr.init(e,t)}));function Er(e,t=null){try{const r=e.split(\".\");if(3!==r.length)return!1;const[n]=r;if(!n)return!1;const a=JSON.parse(atob(n));return!(\"typ\"in a&&\"JWT\"!==a?.typ||!a.alg||t&&(!(\"alg\"in a)||a.alg!==t))}catch{return!1}}const Tr=t(\"$ZodJWT\",((e,t)=>{tr.init(e,t),e._zod.check=r=>{Er(r.value,t.alg)||r.issues.push({code:\"invalid_format\",format:\"jwt\",input:r.value,inst:e,continue:!t.abort})}})),Ar=t(\"$ZodCustomStringFormat\",((e,t)=>{tr.init(e,t),e._zod.check=r=>{t.fn(r.value)||r.issues.push({code:\"invalid_format\",format:t.format,input:r.value,inst:e,continue:!t.abort})}})),$r=t(\"$ZodNumber\",((e,t)=>{Jt.init(e,t),e._zod.pattern=e._zod.bag.pattern??st,e._zod.parse=(r,n)=>{if(t.coerce)try{r.value=Number(r.value)}catch(e){}const a=r.value;if(\"number\"==typeof a&&!Number.isNaN(a)&&Number.isFinite(a))return r;const o=\"number\"==typeof a?Number.isNaN(a)?\"NaN\":Number.isFinite(a)?void 0:\"Infinity\":void 0;return r.issues.push({expected:\"number\",code:\"invalid_type\",input:a,inst:e,...o?{received:o}:{}}),r}})),Cr=t(\"$ZodNumber\",((e,t)=>{Dt.init(e,t),$r.init(e,t)})),Pr=t(\"$ZodBoolean\",((e,t)=>{Jt.init(e,t),e._zod.pattern=lt,e._zod.parse=(r,n)=>{if(t.coerce)try{r.value=Boolean(r.value)}catch(e){}const a=r.value;return\"boolean\"==typeof a||r.issues.push({expected:\"boolean\",code:\"invalid_type\",input:a,inst:e}),r}})),Dr=t(\"$ZodBigInt\",((e,t)=>{Jt.init(e,t),e._zod.pattern=ot,e._zod.parse=(r,n)=>{if(t.coerce)try{r.value=BigInt(r.value)}catch(e){}return\"bigint\"==typeof r.value||r.issues.push({expected:\"bigint\",code:\"invalid_type\",input:r.value,inst:e}),r}})),Ir=t(\"$ZodBigInt\",((e,t)=>{It.init(e,t),Dr.init(e,t)})),Mr=t(\"$ZodSymbol\",((e,t)=>{Jt.init(e,t),e._zod.parse=(t,r)=>{const n=t.value;return\"symbol\"==typeof n||t.issues.push({expected:\"symbol\",code:\"invalid_type\",input:n,inst:e}),t}})),Nr=t(\"$ZodUndefined\",((e,t)=>{Jt.init(e,t),e._zod.pattern=ut,e._zod.values=new Set([void 0]),e._zod.optin=\"optional\",e._zod.optout=\"optional\",e._zod.parse=(t,r)=>{const n=t.value;return void 0===n||t.issues.push({expected:\"undefined\",code:\"invalid_type\",input:n,inst:e}),t}})),Rr=t(\"$ZodNull\",((e,t)=>{Jt.init(e,t),e._zod.pattern=ct,e._zod.values=new Set([null]),e._zod.parse=(t,r)=>{const n=t.value;return null===n||t.issues.push({expected:\"null\",code:\"invalid_type\",input:n,inst:e}),t}})),jr=t(\"$ZodAny\",((e,t)=>{Jt.init(e,t),e._zod.parse=e=>e})),Lr=t(\"$ZodUnknown\",((e,t)=>{Jt.init(e,t),e._zod.parse=e=>e})),Ur=t(\"$ZodNever\",((e,t)=>{Jt.init(e,t),e._zod.parse=(t,r)=>(t.issues.push({expected:\"never\",code:\"invalid_type\",input:t.value,inst:e}),t)})),Br=t(\"$ZodVoid\",((e,t)=>{Jt.init(e,t),e._zod.parse=(t,r)=>{const n=t.value;return void 0===n||t.issues.push({expected:\"void\",code:\"invalid_type\",input:n,inst:e}),t}})),zr=t(\"$ZodDate\",((e,t)=>{Jt.init(e,t),e._zod.parse=(r,n)=>{if(t.coerce)try{r.value=new Date(r.value)}catch(e){}const a=r.value,o=a instanceof Date;return o&&!Number.isNaN(a.getTime())||r.issues.push({expected:\"date\",code:\"invalid_type\",input:a,...o?{received:\"Invalid Date\"}:{},inst:e}),r}}));function Fr(e,t,r){e.issues.length&&t.issues.push(...z(r,e.issues)),t.value[r]=e.value}const Qr=t(\"$ZodArray\",((e,t)=>{Jt.init(e,t),e._zod.parse=(r,n)=>{const a=r.value;if(!Array.isArray(a))return r.issues.push({expected:\"array\",code:\"invalid_type\",input:a,inst:e}),r;r.value=Array(a.length);const o=[];for(let e=0;eFr(t,r,e)))):Fr(s,r,e)}return o.length?Promise.all(o).then((()=>r)):r}}));function qr(e,t,r,n){e.issues.length&&t.issues.push(...z(r,e.issues)),void 0===e.value?r in n&&(t.value[r]=void 0):t.value[r]=e.value}function Zr(e){const t=Object.keys(e.shape);for(const r of t)if(!e.shape?.[r]?._zod?.traits?.has(\"$ZodType\"))throw new Error(`Invalid element at key \"${r}\": expected a Zod schema`);const r=C(e.shape);return{...e,keys:t,keySet:new Set(t),numKeys:t.length,optionalKeys:new Set(r)}}function Vr(e,t,r,n,a,o){const i=[],s=a.keySet,l=a.catchall._zod,c=l.def.type;for(const a of Object.keys(t)){if(s.has(a))continue;if(\"never\"===c){i.push(a);continue}const o=l.run({value:t[a],issues:[]},n);o instanceof Promise?e.push(o.then((e=>qr(e,r,a,t)))):qr(o,r,a,t)}return i.length&&r.issues.push({code:\"unrecognized_keys\",keys:i,input:t,inst:o}),e.length?Promise.all(e).then((()=>r)):r}const Hr=t(\"$ZodObject\",((e,t)=>{Jt.init(e,t);const r=Object.getOwnPropertyDescriptor(t,\"shape\");if(!r?.get){const e=t.shape;Object.defineProperty(t,\"shape\",{get:()=>{const r={...e};return Object.defineProperty(t,\"shape\",{value:r}),r}})}const n=u((()=>Zr(t)));m(e._zod,\"propValues\",(()=>{const e=t.shape,r={};for(const t in e){const n=e[t]._zod;if(n.values){r[t]??(r[t]=new Set);for(const e of n.values)r[t].add(e)}}return r}));const a=O,o=t.catchall;let i;e._zod.parse=(t,r)=>{i??(i=n.value);const s=t.value;if(!a(s))return t.issues.push({expected:\"object\",code:\"invalid_type\",input:s,inst:e}),t;t.value={};const l=[],c=i.shape;for(const e of i.keys){const n=c[e]._zod.run({value:s[e],issues:[]},r);n instanceof Promise?l.push(n.then((r=>qr(r,t,e,s)))):qr(n,t,e,s)}return o?Vr(l,s,t,r,n.value,e):l.length?Promise.all(l).then((()=>t)):t}})),Wr=t(\"$ZodObjectJIT\",((e,t)=>{Hr.init(e,t);const r=e._zod.parse,n=u((()=>Zr(t)));let a;const i=O,s=!o.jitless,l=s&&w.value,c=t.catchall;let d;e._zod.parse=(o,u)=>{d??(d=n.value);const p=o.value;return i(p)?s&&l&&!1===u?.async&&!0!==u.jitless?(a||(a=(e=>{const t=new Yt([\"shape\",\"payload\",\"ctx\"]),r=n.value,a=e=>{const t=b(e);return`shape[${t}]._zod.run({ value: input[${t}], issues: [] }, ctx)`};t.write(\"const input = payload.value;\");const o=Object.create(null);let i=0;for(const e of r.keys)o[e]=\"key_\"+i++;t.write(\"const newResult = {};\");for(const e of r.keys){const r=o[e],n=b(e);t.write(`const ${r} = ${a(e)};`),t.write(`\\n if (${r}.issues.length) {\\n payload.issues = payload.issues.concat(${r}.issues.map(iss => ({\\n ...iss,\\n path: iss.path ? [${n}, ...iss.path] : [${n}]\\n })));\\n }\\n \\n \\n if (${r}.value === undefined) {\\n if (${n} in input) {\\n newResult[${n}] = undefined;\\n }\\n } else {\\n newResult[${n}] = ${r}.value;\\n }\\n \\n `)}t.write(\"payload.value = newResult;\"),t.write(\"return payload;\");const s=t.compile();return(t,r)=>s(e,t,r)})(t.shape)),o=a(o,u),c?Vr([],p,o,u,d,e):o):r(o,u):(o.issues.push({expected:\"object\",code:\"invalid_type\",input:p,inst:e}),o)}}));function Xr(e,t,r,n){for(const r of e)if(0===r.issues.length)return t.value=r.value,t;const a=e.filter((e=>!B(e)));return 1===a.length?(t.value=a[0].value,a[0]):(t.issues.push({code:\"invalid_union\",input:t.value,inst:r,errors:e.map((e=>e.issues.map((e=>Q(e,n,i())))))}),t)}const Gr=t(\"$ZodUnion\",((e,t)=>{Jt.init(e,t),m(e._zod,\"optin\",(()=>t.options.some((e=>\"optional\"===e._zod.optin))?\"optional\":void 0)),m(e._zod,\"optout\",(()=>t.options.some((e=>\"optional\"===e._zod.optout))?\"optional\":void 0)),m(e._zod,\"values\",(()=>{if(t.options.every((e=>e._zod.values)))return new Set(t.options.flatMap((e=>Array.from(e._zod.values))))})),m(e._zod,\"pattern\",(()=>{if(t.options.every((e=>e._zod.pattern))){const e=t.options.map((e=>e._zod.pattern));return new RegExp(`^(${e.map((e=>p(e.source))).join(\"|\")})$`)}}));const r=1===t.options.length,n=t.options[0]._zod.run;e._zod.parse=(a,o)=>{if(r)return n(a,o);let i=!1;const s=[];for(const e of t.options){const t=e._zod.run({value:a.value,issues:[]},o);if(t instanceof Promise)s.push(t),i=!0;else{if(0===t.issues.length)return t;s.push(t)}}return i?Promise.all(s).then((t=>Xr(t,a,e,o))):Xr(s,a,e,o)}})),Yr=t(\"$ZodDiscriminatedUnion\",((e,t)=>{Gr.init(e,t);const r=e._zod.parse;m(e._zod,\"propValues\",(()=>{const e={};for(const r of t.options){const n=r._zod.propValues;if(!n||0===Object.keys(n).length)throw new Error(`Invalid discriminated union option at index \"${t.options.indexOf(r)}\"`);for(const[t,r]of Object.entries(n)){e[t]||(e[t]=new Set);for(const n of r)e[t].add(n)}}return e}));const n=u((()=>{const e=t.options,r=new Map;for(const n of e){const e=n._zod.propValues?.[t.discriminator];if(!e||0===e.size)throw new Error(`Invalid discriminated union option at index \"${t.options.indexOf(n)}\"`);for(const t of e){if(r.has(t))throw new Error(`Duplicate discriminator value \"${String(t)}\"`);r.set(t,n)}}return r}));e._zod.parse=(a,o)=>{const i=a.value;if(!O(i))return a.issues.push({code:\"invalid_type\",expected:\"object\",input:i,inst:e}),a;const s=n.value.get(i?.[t.discriminator]);return s?s._zod.run(a,o):t.unionFallback?r(a,o):(a.issues.push({code:\"invalid_union\",errors:[],note:\"No matching discriminator\",discriminator:t.discriminator,input:i,path:[t.discriminator],inst:e}),a)}})),Kr=t(\"$ZodIntersection\",((e,t)=>{Jt.init(e,t),e._zod.parse=(e,r)=>{const n=e.value,a=t.left._zod.run({value:n,issues:[]},r),o=t.right._zod.run({value:n,issues:[]},r);return a instanceof Promise||o instanceof Promise?Promise.all([a,o]).then((([t,r])=>en(e,t,r))):en(e,a,o)}}));function Jr(e,t){if(e===t)return{valid:!0,data:e};if(e instanceof Date&&t instanceof Date&&+e==+t)return{valid:!0,data:e};if(x(e)&&x(t)){const r=Object.keys(t),n=Object.keys(e).filter((e=>-1!==r.indexOf(e))),a={...e,...t};for(const r of n){const n=Jr(e[r],t[r]);if(!n.valid)return{valid:!1,mergeErrorPath:[r,...n.mergeErrorPath]};a[r]=n.data}return{valid:!0,data:a}}if(Array.isArray(e)&&Array.isArray(t)){if(e.length!==t.length)return{valid:!1,mergeErrorPath:[]};const r=[];for(let n=0;n{Jt.init(e,t);const r=t.items,n=r.length-[...r].reverse().findIndex((e=>\"optional\"!==e._zod.optin));e._zod.parse=(a,o)=>{const i=a.value;if(!Array.isArray(i))return a.issues.push({input:i,inst:e,expected:\"tuple\",code:\"invalid_type\"}),a;a.value=[];const s=[];if(!t.rest){const t=i.length>r.length,o=i.length=i.length&&l>=n)continue;const t=e._zod.run({value:i[l],issues:[]},o);t instanceof Promise?s.push(t.then((e=>rn(e,a,l)))):rn(t,a,l)}if(t.rest){const e=i.slice(r.length);for(const r of e){l++;const e=t.rest._zod.run({value:r,issues:[]},o);e instanceof Promise?s.push(e.then((e=>rn(e,a,l)))):rn(e,a,l)}}return s.length?Promise.all(s).then((()=>a)):a}}));function rn(e,t,r){e.issues.length&&t.issues.push(...z(r,e.issues)),t.value[r]=e.value}const nn=t(\"$ZodRecord\",((e,t)=>{Jt.init(e,t),e._zod.parse=(r,n)=>{const a=r.value;if(!x(a))return r.issues.push({expected:\"record\",code:\"invalid_type\",input:a,inst:e}),r;const o=[];if(t.keyType._zod.values){const i=t.keyType._zod.values;r.value={};for(const e of i)if(\"string\"==typeof e||\"number\"==typeof e||\"symbol\"==typeof e){const i=t.valueType._zod.run({value:a[e],issues:[]},n);i instanceof Promise?o.push(i.then((t=>{t.issues.length&&r.issues.push(...z(e,t.issues)),r.value[e]=t.value}))):(i.issues.length&&r.issues.push(...z(e,i.issues)),r.value[e]=i.value)}let s;for(const e in a)i.has(e)||(s=s??[],s.push(e));s&&s.length>0&&r.issues.push({code:\"unrecognized_keys\",input:a,inst:e,keys:s})}else{r.value={};for(const s of Reflect.ownKeys(a)){if(\"__proto__\"===s)continue;const l=t.keyType._zod.run({value:s,issues:[]},n);if(l instanceof Promise)throw new Error(\"Async schemas not supported in object keys currently\");if(l.issues.length){r.issues.push({code:\"invalid_key\",origin:\"record\",issues:l.issues.map((e=>Q(e,n,i()))),input:s,path:[s],inst:e}),r.value[l.value]=l.value;continue}const c=t.valueType._zod.run({value:a[s],issues:[]},n);c instanceof Promise?o.push(c.then((e=>{e.issues.length&&r.issues.push(...z(s,e.issues)),r.value[l.value]=e.value}))):(c.issues.length&&r.issues.push(...z(s,c.issues)),r.value[l.value]=c.value)}}return o.length?Promise.all(o).then((()=>r)):r}})),an=t(\"$ZodMap\",((e,t)=>{Jt.init(e,t),e._zod.parse=(r,n)=>{const a=r.value;if(!(a instanceof Map))return r.issues.push({expected:\"map\",code:\"invalid_type\",input:a,inst:e}),r;const o=[];r.value=new Map;for(const[i,s]of a){const l=t.keyType._zod.run({value:i,issues:[]},n),c=t.valueType._zod.run({value:s,issues:[]},n);l instanceof Promise||c instanceof Promise?o.push(Promise.all([l,c]).then((([t,o])=>{on(t,o,r,i,a,e,n)}))):on(l,c,r,i,a,e,n)}return o.length?Promise.all(o).then((()=>r)):r}}));function on(e,t,r,n,a,o,s){e.issues.length&&(S.has(typeof n)?r.issues.push(...z(n,e.issues)):r.issues.push({code:\"invalid_key\",origin:\"map\",input:a,inst:o,issues:e.issues.map((e=>Q(e,s,i())))})),t.issues.length&&(S.has(typeof n)?r.issues.push(...z(n,t.issues)):r.issues.push({origin:\"map\",code:\"invalid_element\",input:a,inst:o,key:n,issues:t.issues.map((e=>Q(e,s,i())))})),r.value.set(e.value,t.value)}const sn=t(\"$ZodSet\",((e,t)=>{Jt.init(e,t),e._zod.parse=(r,n)=>{const a=r.value;if(!(a instanceof Set))return r.issues.push({input:a,inst:e,expected:\"set\",code:\"invalid_type\"}),r;const o=[];r.value=new Set;for(const e of a){const a=t.valueType._zod.run({value:e,issues:[]},n);a instanceof Promise?o.push(a.then((e=>ln(e,r)))):ln(a,r)}return o.length?Promise.all(o).then((()=>r)):r}}));function ln(e,t){e.issues.length&&t.issues.push(...e.issues),t.value.add(e.value)}const cn=t(\"$ZodEnum\",((e,t)=>{Jt.init(e,t);const r=s(t.entries),n=new Set(r);e._zod.values=n,e._zod.pattern=new RegExp(`^(${r.filter((e=>S.has(typeof e))).map((e=>\"string\"==typeof e?E(e):e.toString())).join(\"|\")})$`),e._zod.parse=(t,a)=>{const o=t.value;return n.has(o)||t.issues.push({code:\"invalid_value\",values:r,input:o,inst:e}),t}})),un=t(\"$ZodLiteral\",((e,t)=>{if(Jt.init(e,t),0===t.values.length)throw new Error(\"Cannot create literal schema with no valid values\");e._zod.values=new Set(t.values),e._zod.pattern=new RegExp(`^(${t.values.map((e=>\"string\"==typeof e?E(e):e?E(e.toString()):String(e))).join(\"|\")})$`),e._zod.parse=(r,n)=>{const a=r.value;return e._zod.values.has(a)||r.issues.push({code:\"invalid_value\",values:t.values,input:a,inst:e}),r}})),dn=t(\"$ZodFile\",((e,t)=>{Jt.init(e,t),e._zod.parse=(t,r)=>{const n=t.value;return n instanceof File||t.issues.push({expected:\"file\",code:\"invalid_type\",input:n,inst:e}),t}})),pn=t(\"$ZodTransform\",((e,t)=>{Jt.init(e,t),e._zod.parse=(r,o)=>{if(\"backward\"===o.direction)throw new a(e.constructor.name);const i=t.transform(r.value,r);if(o.async)return(i instanceof Promise?i:Promise.resolve(i)).then((e=>(r.value=e,r)));if(i instanceof Promise)throw new n;return r.value=i,r}}));function hn(e,t){return e.issues.length&&void 0===t?{issues:[],value:void 0}:e}const fn=t(\"$ZodOptional\",((e,t)=>{Jt.init(e,t),e._zod.optin=\"optional\",e._zod.optout=\"optional\",m(e._zod,\"values\",(()=>t.innerType._zod.values?new Set([...t.innerType._zod.values,void 0]):void 0)),m(e._zod,\"pattern\",(()=>{const e=t.innerType._zod.pattern;return e?new RegExp(`^(${p(e.source)})?$`):void 0})),e._zod.parse=(e,r)=>{if(\"optional\"===t.innerType._zod.optin){const n=t.innerType._zod.run(e,r);return n instanceof Promise?n.then((t=>hn(t,e.value))):hn(n,e.value)}return void 0===e.value?e:t.innerType._zod.run(e,r)}})),mn=t(\"$ZodNullable\",((e,t)=>{Jt.init(e,t),m(e._zod,\"optin\",(()=>t.innerType._zod.optin)),m(e._zod,\"optout\",(()=>t.innerType._zod.optout)),m(e._zod,\"pattern\",(()=>{const e=t.innerType._zod.pattern;return e?new RegExp(`^(${p(e.source)}|null)$`):void 0})),m(e._zod,\"values\",(()=>t.innerType._zod.values?new Set([...t.innerType._zod.values,null]):void 0)),e._zod.parse=(e,r)=>null===e.value?e:t.innerType._zod.run(e,r)})),gn=t(\"$ZodDefault\",((e,t)=>{Jt.init(e,t),e._zod.optin=\"optional\",m(e._zod,\"values\",(()=>t.innerType._zod.values)),e._zod.parse=(e,r)=>{if(\"backward\"===r.direction)return t.innerType._zod.run(e,r);if(void 0===e.value)return e.value=t.defaultValue,e;const n=t.innerType._zod.run(e,r);return n instanceof Promise?n.then((e=>vn(e,t))):vn(n,t)}}));function vn(e,t){return void 0===e.value&&(e.value=t.defaultValue),e}const bn=t(\"$ZodPrefault\",((e,t)=>{Jt.init(e,t),e._zod.optin=\"optional\",m(e._zod,\"values\",(()=>t.innerType._zod.values)),e._zod.parse=(e,r)=>(\"backward\"===r.direction||void 0===e.value&&(e.value=t.defaultValue),t.innerType._zod.run(e,r))})),yn=t(\"$ZodNonOptional\",((e,t)=>{Jt.init(e,t),m(e._zod,\"values\",(()=>{const e=t.innerType._zod.values;return e?new Set([...e].filter((e=>void 0!==e))):void 0})),e._zod.parse=(r,n)=>{const a=t.innerType._zod.run(r,n);return a instanceof Promise?a.then((t=>On(t,e))):On(a,e)}}));function On(e,t){return e.issues.length||void 0!==e.value||e.issues.push({code:\"invalid_type\",expected:\"nonoptional\",input:e.value,inst:t}),e}const wn=t(\"$ZodSuccess\",((e,t)=>{Jt.init(e,t),e._zod.parse=(e,r)=>{if(\"backward\"===r.direction)throw new a(\"ZodSuccess\");const n=t.innerType._zod.run(e,r);return n instanceof Promise?n.then((t=>(e.value=0===t.issues.length,e))):(e.value=0===n.issues.length,e)}})),xn=t(\"$ZodCatch\",((e,t)=>{Jt.init(e,t),m(e._zod,\"optin\",(()=>t.innerType._zod.optin)),m(e._zod,\"optout\",(()=>t.innerType._zod.optout)),m(e._zod,\"values\",(()=>t.innerType._zod.values)),e._zod.parse=(e,r)=>{if(\"backward\"===r.direction)return t.innerType._zod.run(e,r);const n=t.innerType._zod.run(e,r);return n instanceof Promise?n.then((n=>(e.value=n.value,n.issues.length&&(e.value=t.catchValue({...e,error:{issues:n.issues.map((e=>Q(e,r,i())))},input:e.value}),e.issues=[]),e))):(e.value=n.value,n.issues.length&&(e.value=t.catchValue({...e,error:{issues:n.issues.map((e=>Q(e,r,i())))},input:e.value}),e.issues=[]),e)}})),kn=t(\"$ZodNaN\",((e,t)=>{Jt.init(e,t),e._zod.parse=(t,r)=>(\"number\"==typeof t.value&&Number.isNaN(t.value)||t.issues.push({input:t.value,inst:e,expected:\"nan\",code:\"invalid_type\"}),t)})),Sn=t(\"$ZodPipe\",((e,t)=>{Jt.init(e,t),m(e._zod,\"values\",(()=>t.in._zod.values)),m(e._zod,\"optin\",(()=>t.in._zod.optin)),m(e._zod,\"optout\",(()=>t.out._zod.optout)),m(e._zod,\"propValues\",(()=>t.in._zod.propValues)),e._zod.parse=(e,r)=>{if(\"backward\"===r.direction){const n=t.out._zod.run(e,r);return n instanceof Promise?n.then((e=>_n(e,t.in,r))):_n(n,t.in,r)}const n=t.in._zod.run(e,r);return n instanceof Promise?n.then((e=>_n(e,t.out,r))):_n(n,t.out,r)}}));function _n(e,t,r){return e.issues.length?(e.aborted=!0,e):t._zod.run({value:e.value,issues:e.issues},r)}const En=t(\"$ZodCodec\",((e,t)=>{Jt.init(e,t),m(e._zod,\"values\",(()=>t.in._zod.values)),m(e._zod,\"optin\",(()=>t.in._zod.optin)),m(e._zod,\"optout\",(()=>t.out._zod.optout)),m(e._zod,\"propValues\",(()=>t.in._zod.propValues)),e._zod.parse=(e,r)=>{if(\"forward\"===(r.direction||\"forward\")){const n=t.in._zod.run(e,r);return n instanceof Promise?n.then((e=>Tn(e,t,r))):Tn(n,t,r)}{const n=t.out._zod.run(e,r);return n instanceof Promise?n.then((e=>Tn(e,t,r))):Tn(n,t,r)}}}));function Tn(e,t,r){if(e.issues.length)return e.aborted=!0,e;if(\"forward\"===(r.direction||\"forward\")){const n=t.transform(e.value,e);return n instanceof Promise?n.then((n=>An(e,n,t.out,r))):An(e,n,t.out,r)}{const n=t.reverseTransform(e.value,e);return n instanceof Promise?n.then((n=>An(e,n,t.in,r))):An(e,n,t.in,r)}}function An(e,t,r,n){return e.issues.length?(e.aborted=!0,e):r._zod.run({value:t,issues:e.issues},n)}const $n=t(\"$ZodReadonly\",((e,t)=>{Jt.init(e,t),m(e._zod,\"propValues\",(()=>t.innerType._zod.propValues)),m(e._zod,\"values\",(()=>t.innerType._zod.values)),m(e._zod,\"optin\",(()=>t.innerType._zod.optin)),m(e._zod,\"optout\",(()=>t.innerType._zod.optout)),e._zod.parse=(e,r)=>{if(\"backward\"===r.direction)return t.innerType._zod.run(e,r);const n=t.innerType._zod.run(e,r);return n instanceof Promise?n.then(Cn):Cn(n)}}));function Cn(e){return e.value=Object.freeze(e.value),e}const Pn=t(\"$ZodTemplateLiteral\",((e,t)=>{Jt.init(e,t);const r=[];for(const e of t.parts)if(\"object\"==typeof e&&null!==e){if(!e._zod.pattern)throw new Error(`Invalid template literal part, no pattern found: ${[...e._zod.traits].shift()}`);const t=e._zod.pattern instanceof RegExp?e._zod.pattern.source:e._zod.pattern;if(!t)throw new Error(`Invalid template literal part: ${e._zod.traits}`);const n=t.startsWith(\"^\")?1:0,a=t.endsWith(\"$\")?t.length-1:t.length;r.push(t.slice(n,a))}else{if(null!==e&&!_.has(typeof e))throw new Error(`Invalid template literal part: ${e}`);r.push(E(`${e}`))}e._zod.pattern=new RegExp(`^${r.join(\"\")}$`),e._zod.parse=(r,n)=>\"string\"!=typeof r.value?(r.issues.push({input:r.value,inst:e,expected:\"template_literal\",code:\"invalid_type\"}),r):(e._zod.pattern.lastIndex=0,e._zod.pattern.test(r.value)||r.issues.push({input:r.value,inst:e,code:\"invalid_format\",format:t.format??\"template_literal\",pattern:e._zod.pattern.source}),r)})),Dn=t(\"$ZodFunction\",((e,t)=>(Jt.init(e,t),e._def=t,e._zod.def=t,e.implement=t=>{if(\"function\"!=typeof t)throw new Error(\"implement() must be called with a function\");return function(...r){const n=e._def.input?oe(e._def.input,r):r,a=Reflect.apply(t,this,n);return e._def.output?oe(e._def.output,a):a}},e.implementAsync=t=>{if(\"function\"!=typeof t)throw new Error(\"implementAsync() must be called with a function\");return async function(...r){const n=e._def.input?await se(e._def.input,r):r,a=await Reflect.apply(t,this,n);return e._def.output?await se(e._def.output,a):a}},e._zod.parse=(t,r)=>{if(\"function\"!=typeof t.value)return t.issues.push({code:\"invalid_type\",expected:\"function\",input:t.value,inst:e}),t;const n=e._def.output&&\"promise\"===e._def.output._zod.def.type;return t.value=n?e.implementAsync(t.value):e.implement(t.value),t},e.input=(...t)=>{const r=e.constructor;return Array.isArray(t[0])?new r({type:\"function\",input:new tn({type:\"tuple\",items:t[0],rest:t[1]}),output:e._def.output}):new r({type:\"function\",input:t[0],output:e._def.output})},e.output=t=>new(0,e.constructor)({type:\"function\",input:e._def.input,output:t}),e))),In=t(\"$ZodPromise\",((e,t)=>{Jt.init(e,t),e._zod.parse=(e,r)=>Promise.resolve(e.value).then((e=>t.innerType._zod.run({value:e,issues:[]},r)))})),Mn=t(\"$ZodLazy\",((e,t)=>{Jt.init(e,t),m(e._zod,\"innerType\",(()=>t.getter())),m(e._zod,\"pattern\",(()=>e._zod.innerType._zod.pattern)),m(e._zod,\"propValues\",(()=>e._zod.innerType._zod.propValues)),m(e._zod,\"optin\",(()=>e._zod.innerType._zod.optin??void 0)),m(e._zod,\"optout\",(()=>e._zod.innerType._zod.optout??void 0)),e._zod.parse=(t,r)=>e._zod.innerType._zod.run(t,r)})),Nn=t(\"$ZodCustom\",((e,t)=>{Tt.init(e,t),Jt.init(e,t),e._zod.parse=(e,t)=>e,e._zod.check=r=>{const n=r.value,a=t.fn(n);if(a instanceof Promise)return a.then((t=>Rn(t,r,n,e)));Rn(a,r,n,e)}}));function Rn(e,t,r,n){if(!e){const e={code:\"custom\",input:r,inst:n,path:[...n._zod.def.path??[]],continue:!n._zod.def.abort};n._zod.def.params&&(e.params=n._zod.def.params),t.issues.push(V(e))}}const jn=()=>{const e={string:{unit:\"حرف\",verb:\"أن يحوي\"},file:{unit:\"بايت\",verb:\"أن يحوي\"},array:{unit:\"عنصر\",verb:\"أن يحوي\"},set:{unit:\"عنصر\",verb:\"أن يحوي\"}};function t(t){return e[t]??null}const r={regex:\"مدخل\",email:\"بريد إلكتروني\",url:\"رابط\",emoji:\"إيموجي\",uuid:\"UUID\",uuidv4:\"UUIDv4\",uuidv6:\"UUIDv6\",nanoid:\"nanoid\",guid:\"GUID\",cuid:\"cuid\",cuid2:\"cuid2\",ulid:\"ULID\",xid:\"XID\",ksuid:\"KSUID\",datetime:\"تاريخ ووقت بمعيار ISO\",date:\"تاريخ بمعيار ISO\",time:\"وقت بمعيار ISO\",duration:\"مدة بمعيار ISO\",ipv4:\"عنوان IPv4\",ipv6:\"عنوان IPv6\",cidrv4:\"مدى عناوين بصيغة IPv4\",cidrv6:\"مدى عناوين بصيغة IPv6\",base64:\"نَص بترميز base64-encoded\",base64url:\"نَص بترميز base64url-encoded\",json_string:\"نَص على هيئة JSON\",e164:\"رقم هاتف بمعيار E.164\",jwt:\"JWT\",template_literal:\"مدخل\"};return e=>{switch(e.code){case\"invalid_type\":return`مدخلات غير مقبولة: يفترض إدخال ${e.expected}، ولكن تم إدخال ${(e=>{const t=typeof e;switch(t){case\"number\":return Number.isNaN(e)?\"NaN\":\"number\";case\"object\":if(Array.isArray(e))return\"array\";if(null===e)return\"null\";if(Object.getPrototypeOf(e)!==Object.prototype&&e.constructor)return e.constructor.name}return t})(e.input)}`;case\"invalid_value\":return 1===e.values.length?`مدخلات غير مقبولة: يفترض إدخال ${$(e.values[0])}`:`اختيار غير مقبول: يتوقع انتقاء أحد هذه الخيارات: ${l(e.values,\"|\")}`;case\"too_big\":{const r=e.inclusive?\"<=\":\"<\",n=t(e.origin);return n?` أكبر من اللازم: يفترض أن تكون ${e.origin??\"القيمة\"} ${r} ${e.maximum.toString()} ${n.unit??\"عنصر\"}`:`أكبر من اللازم: يفترض أن تكون ${e.origin??\"القيمة\"} ${r} ${e.maximum.toString()}`}case\"too_small\":{const r=e.inclusive?\">=\":\">\",n=t(e.origin);return n?`أصغر من اللازم: يفترض لـ ${e.origin} أن يكون ${r} ${e.minimum.toString()} ${n.unit}`:`أصغر من اللازم: يفترض لـ ${e.origin} أن يكون ${r} ${e.minimum.toString()}`}case\"invalid_format\":{const t=e;return\"starts_with\"===t.format?`نَص غير مقبول: يجب أن يبدأ بـ \"${e.prefix}\"`:\"ends_with\"===t.format?`نَص غير مقبول: يجب أن ينتهي بـ \"${t.suffix}\"`:\"includes\"===t.format?`نَص غير مقبول: يجب أن يتضمَّن \"${t.includes}\"`:\"regex\"===t.format?`نَص غير مقبول: يجب أن يطابق النمط ${t.pattern}`:`${r[t.format]??e.format} غير مقبول`}case\"not_multiple_of\":return`رقم غير مقبول: يجب أن يكون من مضاعفات ${e.divisor}`;case\"unrecognized_keys\":return`معرف${e.keys.length>1?\"ات\":\"\"} غريب${e.keys.length>1?\"ة\":\"\"}: ${l(e.keys,\"، \")}`;case\"invalid_key\":return`معرف غير مقبول في ${e.origin}`;case\"invalid_union\":default:return\"مدخل غير مقبول\";case\"invalid_element\":return`مدخل غير مقبول في ${e.origin}`}}},Ln=()=>{const e={string:{unit:\"simvol\",verb:\"olmalıdır\"},file:{unit:\"bayt\",verb:\"olmalıdır\"},array:{unit:\"element\",verb:\"olmalıdır\"},set:{unit:\"element\",verb:\"olmalıdır\"}};function t(t){return e[t]??null}const r={regex:\"input\",email:\"email address\",url:\"URL\",emoji:\"emoji\",uuid:\"UUID\",uuidv4:\"UUIDv4\",uuidv6:\"UUIDv6\",nanoid:\"nanoid\",guid:\"GUID\",cuid:\"cuid\",cuid2:\"cuid2\",ulid:\"ULID\",xid:\"XID\",ksuid:\"KSUID\",datetime:\"ISO datetime\",date:\"ISO date\",time:\"ISO time\",duration:\"ISO duration\",ipv4:\"IPv4 address\",ipv6:\"IPv6 address\",cidrv4:\"IPv4 range\",cidrv6:\"IPv6 range\",base64:\"base64-encoded string\",base64url:\"base64url-encoded string\",json_string:\"JSON string\",e164:\"E.164 number\",jwt:\"JWT\",template_literal:\"input\"};return e=>{switch(e.code){case\"invalid_type\":return`Yanlış dəyər: gözlənilən ${e.expected}, daxil olan ${(e=>{const t=typeof e;switch(t){case\"number\":return Number.isNaN(e)?\"NaN\":\"number\";case\"object\":if(Array.isArray(e))return\"array\";if(null===e)return\"null\";if(Object.getPrototypeOf(e)!==Object.prototype&&e.constructor)return e.constructor.name}return t})(e.input)}`;case\"invalid_value\":return 1===e.values.length?`Yanlış dəyər: gözlənilən ${$(e.values[0])}`:`Yanlış seçim: aşağıdakılardan biri olmalıdır: ${l(e.values,\"|\")}`;case\"too_big\":{const r=e.inclusive?\"<=\":\"<\",n=t(e.origin);return n?`Çox böyük: gözlənilən ${e.origin??\"dəyər\"} ${r}${e.maximum.toString()} ${n.unit??\"element\"}`:`Çox böyük: gözlənilən ${e.origin??\"dəyər\"} ${r}${e.maximum.toString()}`}case\"too_small\":{const r=e.inclusive?\">=\":\">\",n=t(e.origin);return n?`Çox kiçik: gözlənilən ${e.origin} ${r}${e.minimum.toString()} ${n.unit}`:`Çox kiçik: gözlənilən ${e.origin} ${r}${e.minimum.toString()}`}case\"invalid_format\":{const t=e;return\"starts_with\"===t.format?`Yanlış mətn: \"${t.prefix}\" ilə başlamalıdır`:\"ends_with\"===t.format?`Yanlış mətn: \"${t.suffix}\" ilə bitməlidir`:\"includes\"===t.format?`Yanlış mətn: \"${t.includes}\" daxil olmalıdır`:\"regex\"===t.format?`Yanlış mətn: ${t.pattern} şablonuna uyğun olmalıdır`:`Yanlış ${r[t.format]??e.format}`}case\"not_multiple_of\":return`Yanlış ədəd: ${e.divisor} ilə bölünə bilən olmalıdır`;case\"unrecognized_keys\":return`Tanınmayan açar${e.keys.length>1?\"lar\":\"\"}: ${l(e.keys,\", \")}`;case\"invalid_key\":return`${e.origin} daxilində yanlış açar`;case\"invalid_union\":default:return\"Yanlış dəyər\";case\"invalid_element\":return`${e.origin} daxilində yanlış dəyər`}}};function Un(e,t,r,n){const a=Math.abs(e),o=a%10,i=a%100;return i>=11&&i<=19?n:1===o?t:o>=2&&o<=4?r:n}const Bn=()=>{const e={string:{unit:{one:\"сімвал\",few:\"сімвалы\",many:\"сімвалаў\"},verb:\"мець\"},array:{unit:{one:\"элемент\",few:\"элементы\",many:\"элементаў\"},verb:\"мець\"},set:{unit:{one:\"элемент\",few:\"элементы\",many:\"элементаў\"},verb:\"мець\"},file:{unit:{one:\"байт\",few:\"байты\",many:\"байтаў\"},verb:\"мець\"}};function t(t){return e[t]??null}const r={regex:\"увод\",email:\"email адрас\",url:\"URL\",emoji:\"эмодзі\",uuid:\"UUID\",uuidv4:\"UUIDv4\",uuidv6:\"UUIDv6\",nanoid:\"nanoid\",guid:\"GUID\",cuid:\"cuid\",cuid2:\"cuid2\",ulid:\"ULID\",xid:\"XID\",ksuid:\"KSUID\",datetime:\"ISO дата і час\",date:\"ISO дата\",time:\"ISO час\",duration:\"ISO працягласць\",ipv4:\"IPv4 адрас\",ipv6:\"IPv6 адрас\",cidrv4:\"IPv4 дыяпазон\",cidrv6:\"IPv6 дыяпазон\",base64:\"радок у фармаце base64\",base64url:\"радок у фармаце base64url\",json_string:\"JSON радок\",e164:\"нумар E.164\",jwt:\"JWT\",template_literal:\"увод\"};return e=>{switch(e.code){case\"invalid_type\":return`Няправільны ўвод: чакаўся ${e.expected}, атрымана ${(e=>{const t=typeof e;switch(t){case\"number\":return Number.isNaN(e)?\"NaN\":\"лік\";case\"object\":if(Array.isArray(e))return\"масіў\";if(null===e)return\"null\";if(Object.getPrototypeOf(e)!==Object.prototype&&e.constructor)return e.constructor.name}return t})(e.input)}`;case\"invalid_value\":return 1===e.values.length?`Няправільны ўвод: чакалася ${$(e.values[0])}`:`Няправільны варыянт: чакаўся адзін з ${l(e.values,\"|\")}`;case\"too_big\":{const r=e.inclusive?\"<=\":\"<\",n=t(e.origin);if(n){const t=Un(Number(e.maximum),n.unit.one,n.unit.few,n.unit.many);return`Занадта вялікі: чакалася, што ${e.origin??\"значэнне\"} павінна ${n.verb} ${r}${e.maximum.toString()} ${t}`}return`Занадта вялікі: чакалася, што ${e.origin??\"значэнне\"} павінна быць ${r}${e.maximum.toString()}`}case\"too_small\":{const r=e.inclusive?\">=\":\">\",n=t(e.origin);if(n){const t=Un(Number(e.minimum),n.unit.one,n.unit.few,n.unit.many);return`Занадта малы: чакалася, што ${e.origin} павінна ${n.verb} ${r}${e.minimum.toString()} ${t}`}return`Занадта малы: чакалася, што ${e.origin} павінна быць ${r}${e.minimum.toString()}`}case\"invalid_format\":{const t=e;return\"starts_with\"===t.format?`Няправільны радок: павінен пачынацца з \"${t.prefix}\"`:\"ends_with\"===t.format?`Няправільны радок: павінен заканчвацца на \"${t.suffix}\"`:\"includes\"===t.format?`Няправільны радок: павінен змяшчаць \"${t.includes}\"`:\"regex\"===t.format?`Няправільны радок: павінен адпавядаць шаблону ${t.pattern}`:`Няправільны ${r[t.format]??e.format}`}case\"not_multiple_of\":return`Няправільны лік: павінен быць кратным ${e.divisor}`;case\"unrecognized_keys\":return`Нераспазнаны ${e.keys.length>1?\"ключы\":\"ключ\"}: ${l(e.keys,\", \")}`;case\"invalid_key\":return`Няправільны ключ у ${e.origin}`;case\"invalid_union\":default:return\"Няправільны ўвод\";case\"invalid_element\":return`Няправільнае значэнне ў ${e.origin}`}}},zn=()=>{const e={string:{unit:\"caràcters\",verb:\"contenir\"},file:{unit:\"bytes\",verb:\"contenir\"},array:{unit:\"elements\",verb:\"contenir\"},set:{unit:\"elements\",verb:\"contenir\"}};function t(t){return e[t]??null}const r={regex:\"entrada\",email:\"adreça electrònica\",url:\"URL\",emoji:\"emoji\",uuid:\"UUID\",uuidv4:\"UUIDv4\",uuidv6:\"UUIDv6\",nanoid:\"nanoid\",guid:\"GUID\",cuid:\"cuid\",cuid2:\"cuid2\",ulid:\"ULID\",xid:\"XID\",ksuid:\"KSUID\",datetime:\"data i hora ISO\",date:\"data ISO\",time:\"hora ISO\",duration:\"durada ISO\",ipv4:\"adreça IPv4\",ipv6:\"adreça IPv6\",cidrv4:\"rang IPv4\",cidrv6:\"rang IPv6\",base64:\"cadena codificada en base64\",base64url:\"cadena codificada en base64url\",json_string:\"cadena JSON\",e164:\"número E.164\",jwt:\"JWT\",template_literal:\"entrada\"};return e=>{switch(e.code){case\"invalid_type\":return`Tipus invàlid: s'esperava ${e.expected}, s'ha rebut ${(e=>{const t=typeof e;switch(t){case\"number\":return Number.isNaN(e)?\"NaN\":\"number\";case\"object\":if(Array.isArray(e))return\"array\";if(null===e)return\"null\";if(Object.getPrototypeOf(e)!==Object.prototype&&e.constructor)return e.constructor.name}return t})(e.input)}`;case\"invalid_value\":return 1===e.values.length?`Valor invàlid: s'esperava ${$(e.values[0])}`:`Opció invàlida: s'esperava una de ${l(e.values,\" o \")}`;case\"too_big\":{const r=e.inclusive?\"com a màxim\":\"menys de\",n=t(e.origin);return n?`Massa gran: s'esperava que ${e.origin??\"el valor\"} contingués ${r} ${e.maximum.toString()} ${n.unit??\"elements\"}`:`Massa gran: s'esperava que ${e.origin??\"el valor\"} fos ${r} ${e.maximum.toString()}`}case\"too_small\":{const r=e.inclusive?\"com a mínim\":\"més de\",n=t(e.origin);return n?`Massa petit: s'esperava que ${e.origin} contingués ${r} ${e.minimum.toString()} ${n.unit}`:`Massa petit: s'esperava que ${e.origin} fos ${r} ${e.minimum.toString()}`}case\"invalid_format\":{const t=e;return\"starts_with\"===t.format?`Format invàlid: ha de començar amb \"${t.prefix}\"`:\"ends_with\"===t.format?`Format invàlid: ha d'acabar amb \"${t.suffix}\"`:\"includes\"===t.format?`Format invàlid: ha d'incloure \"${t.includes}\"`:\"regex\"===t.format?`Format invàlid: ha de coincidir amb el patró ${t.pattern}`:`Format invàlid per a ${r[t.format]??e.format}`}case\"not_multiple_of\":return`Número invàlid: ha de ser múltiple de ${e.divisor}`;case\"unrecognized_keys\":return`Clau${e.keys.length>1?\"s\":\"\"} no reconeguda${e.keys.length>1?\"s\":\"\"}: ${l(e.keys,\", \")}`;case\"invalid_key\":return`Clau invàlida a ${e.origin}`;case\"invalid_union\":default:return\"Entrada invàlida\";case\"invalid_element\":return`Element invàlid a ${e.origin}`}}},Fn=()=>{const e={string:{unit:\"znaků\",verb:\"mít\"},file:{unit:\"bajtů\",verb:\"mít\"},array:{unit:\"prvků\",verb:\"mít\"},set:{unit:\"prvků\",verb:\"mít\"}};function t(t){return e[t]??null}const r={regex:\"regulární výraz\",email:\"e-mailová adresa\",url:\"URL\",emoji:\"emoji\",uuid:\"UUID\",uuidv4:\"UUIDv4\",uuidv6:\"UUIDv6\",nanoid:\"nanoid\",guid:\"GUID\",cuid:\"cuid\",cuid2:\"cuid2\",ulid:\"ULID\",xid:\"XID\",ksuid:\"KSUID\",datetime:\"datum a čas ve formátu ISO\",date:\"datum ve formátu ISO\",time:\"čas ve formátu ISO\",duration:\"doba trvání ISO\",ipv4:\"IPv4 adresa\",ipv6:\"IPv6 adresa\",cidrv4:\"rozsah IPv4\",cidrv6:\"rozsah IPv6\",base64:\"řetězec zakódovaný ve formátu base64\",base64url:\"řetězec zakódovaný ve formátu base64url\",json_string:\"řetězec ve formátu JSON\",e164:\"číslo E.164\",jwt:\"JWT\",template_literal:\"vstup\"};return e=>{switch(e.code){case\"invalid_type\":return`Neplatný vstup: očekáváno ${e.expected}, obdrženo ${(e=>{const t=typeof e;switch(t){case\"number\":return Number.isNaN(e)?\"NaN\":\"číslo\";case\"string\":return\"řetězec\";case\"boolean\":return\"boolean\";case\"bigint\":return\"bigint\";case\"function\":return\"funkce\";case\"symbol\":return\"symbol\";case\"undefined\":return\"undefined\";case\"object\":if(Array.isArray(e))return\"pole\";if(null===e)return\"null\";if(Object.getPrototypeOf(e)!==Object.prototype&&e.constructor)return e.constructor.name}return t})(e.input)}`;case\"invalid_value\":return 1===e.values.length?`Neplatný vstup: očekáváno ${$(e.values[0])}`:`Neplatná možnost: očekávána jedna z hodnot ${l(e.values,\"|\")}`;case\"too_big\":{const r=e.inclusive?\"<=\":\"<\",n=t(e.origin);return n?`Hodnota je příliš velká: ${e.origin??\"hodnota\"} musí mít ${r}${e.maximum.toString()} ${n.unit??\"prvků\"}`:`Hodnota je příliš velká: ${e.origin??\"hodnota\"} musí být ${r}${e.maximum.toString()}`}case\"too_small\":{const r=e.inclusive?\">=\":\">\",n=t(e.origin);return n?`Hodnota je příliš malá: ${e.origin??\"hodnota\"} musí mít ${r}${e.minimum.toString()} ${n.unit??\"prvků\"}`:`Hodnota je příliš malá: ${e.origin??\"hodnota\"} musí být ${r}${e.minimum.toString()}`}case\"invalid_format\":{const t=e;return\"starts_with\"===t.format?`Neplatný řetězec: musí začínat na \"${t.prefix}\"`:\"ends_with\"===t.format?`Neplatný řetězec: musí končit na \"${t.suffix}\"`:\"includes\"===t.format?`Neplatný řetězec: musí obsahovat \"${t.includes}\"`:\"regex\"===t.format?`Neplatný řetězec: musí odpovídat vzoru ${t.pattern}`:`Neplatný formát ${r[t.format]??e.format}`}case\"not_multiple_of\":return`Neplatné číslo: musí být násobkem ${e.divisor}`;case\"unrecognized_keys\":return`Neznámé klíče: ${l(e.keys,\", \")}`;case\"invalid_key\":return`Neplatný klíč v ${e.origin}`;case\"invalid_union\":default:return\"Neplatný vstup\";case\"invalid_element\":return`Neplatná hodnota v ${e.origin}`}}},Qn=()=>{const e={string:{unit:\"tegn\",verb:\"havde\"},file:{unit:\"bytes\",verb:\"havde\"},array:{unit:\"elementer\",verb:\"indeholdt\"},set:{unit:\"elementer\",verb:\"indeholdt\"}},t={string:\"streng\",number:\"tal\",boolean:\"boolean\",array:\"liste\",object:\"objekt\",set:\"sæt\",file:\"fil\"};function r(t){return e[t]??null}function n(e){return t[e]??e}const a={regex:\"input\",email:\"e-mailadresse\",url:\"URL\",emoji:\"emoji\",uuid:\"UUID\",uuidv4:\"UUIDv4\",uuidv6:\"UUIDv6\",nanoid:\"nanoid\",guid:\"GUID\",cuid:\"cuid\",cuid2:\"cuid2\",ulid:\"ULID\",xid:\"XID\",ksuid:\"KSUID\",datetime:\"ISO dato- og klokkeslæt\",date:\"ISO-dato\",time:\"ISO-klokkeslæt\",duration:\"ISO-varighed\",ipv4:\"IPv4-område\",ipv6:\"IPv6-område\",cidrv4:\"IPv4-spektrum\",cidrv6:\"IPv6-spektrum\",base64:\"base64-kodet streng\",base64url:\"base64url-kodet streng\",json_string:\"JSON-streng\",e164:\"E.164-nummer\",jwt:\"JWT\",template_literal:\"input\"};return e=>{switch(e.code){case\"invalid_type\":return`Ugyldigt input: forventede ${n(e.expected)}, fik ${n((e=>{const t=typeof e;switch(t){case\"number\":return Number.isNaN(e)?\"NaN\":\"tal\";case\"object\":return Array.isArray(e)?\"liste\":null===e?\"null\":Object.getPrototypeOf(e)!==Object.prototype&&e.constructor?e.constructor.name:\"objekt\"}return t})(e.input))}`;case\"invalid_value\":return 1===e.values.length?`Ugyldig værdi: forventede ${$(e.values[0])}`:`Ugyldigt valg: forventede en af følgende ${l(e.values,\"|\")}`;case\"too_big\":{const t=e.inclusive?\"<=\":\"<\",a=r(e.origin),o=n(e.origin);return a?`For stor: forventede ${o??\"value\"} ${a.verb} ${t} ${e.maximum.toString()} ${a.unit??\"elementer\"}`:`For stor: forventede ${o??\"value\"} havde ${t} ${e.maximum.toString()}`}case\"too_small\":{const t=e.inclusive?\">=\":\">\",a=r(e.origin),o=n(e.origin);return a?`For lille: forventede ${o} ${a.verb} ${t} ${e.minimum.toString()} ${a.unit}`:`For lille: forventede ${o} havde ${t} ${e.minimum.toString()}`}case\"invalid_format\":{const t=e;return\"starts_with\"===t.format?`Ugyldig streng: skal starte med \"${t.prefix}\"`:\"ends_with\"===t.format?`Ugyldig streng: skal ende med \"${t.suffix}\"`:\"includes\"===t.format?`Ugyldig streng: skal indeholde \"${t.includes}\"`:\"regex\"===t.format?`Ugyldig streng: skal matche mønsteret ${t.pattern}`:`Ugyldig ${a[t.format]??e.format}`}case\"not_multiple_of\":return`Ugyldigt tal: skal være deleligt med ${e.divisor}`;case\"unrecognized_keys\":return`${e.keys.length>1?\"Ukendte nøgler\":\"Ukendt nøgle\"}: ${l(e.keys,\", \")}`;case\"invalid_key\":return`Ugyldig nøgle i ${e.origin}`;case\"invalid_union\":return\"Ugyldigt input: matcher ingen af de tilladte typer\";case\"invalid_element\":return`Ugyldig værdi i ${e.origin}`;default:return\"Ugyldigt input\"}}},qn=()=>{const e={string:{unit:\"Zeichen\",verb:\"zu haben\"},file:{unit:\"Bytes\",verb:\"zu haben\"},array:{unit:\"Elemente\",verb:\"zu haben\"},set:{unit:\"Elemente\",verb:\"zu haben\"}};function t(t){return e[t]??null}const r={regex:\"Eingabe\",email:\"E-Mail-Adresse\",url:\"URL\",emoji:\"Emoji\",uuid:\"UUID\",uuidv4:\"UUIDv4\",uuidv6:\"UUIDv6\",nanoid:\"nanoid\",guid:\"GUID\",cuid:\"cuid\",cuid2:\"cuid2\",ulid:\"ULID\",xid:\"XID\",ksuid:\"KSUID\",datetime:\"ISO-Datum und -Uhrzeit\",date:\"ISO-Datum\",time:\"ISO-Uhrzeit\",duration:\"ISO-Dauer\",ipv4:\"IPv4-Adresse\",ipv6:\"IPv6-Adresse\",cidrv4:\"IPv4-Bereich\",cidrv6:\"IPv6-Bereich\",base64:\"Base64-codierter String\",base64url:\"Base64-URL-codierter String\",json_string:\"JSON-String\",e164:\"E.164-Nummer\",jwt:\"JWT\",template_literal:\"Eingabe\"};return e=>{switch(e.code){case\"invalid_type\":return`Ungültige Eingabe: erwartet ${e.expected}, erhalten ${(e=>{const t=typeof e;switch(t){case\"number\":return Number.isNaN(e)?\"NaN\":\"Zahl\";case\"object\":if(Array.isArray(e))return\"Array\";if(null===e)return\"null\";if(Object.getPrototypeOf(e)!==Object.prototype&&e.constructor)return e.constructor.name}return t})(e.input)}`;case\"invalid_value\":return 1===e.values.length?`Ungültige Eingabe: erwartet ${$(e.values[0])}`:`Ungültige Option: erwartet eine von ${l(e.values,\"|\")}`;case\"too_big\":{const r=e.inclusive?\"<=\":\"<\",n=t(e.origin);return n?`Zu groß: erwartet, dass ${e.origin??\"Wert\"} ${r}${e.maximum.toString()} ${n.unit??\"Elemente\"} hat`:`Zu groß: erwartet, dass ${e.origin??\"Wert\"} ${r}${e.maximum.toString()} ist`}case\"too_small\":{const r=e.inclusive?\">=\":\">\",n=t(e.origin);return n?`Zu klein: erwartet, dass ${e.origin} ${r}${e.minimum.toString()} ${n.unit} hat`:`Zu klein: erwartet, dass ${e.origin} ${r}${e.minimum.toString()} ist`}case\"invalid_format\":{const t=e;return\"starts_with\"===t.format?`Ungültiger String: muss mit \"${t.prefix}\" beginnen`:\"ends_with\"===t.format?`Ungültiger String: muss mit \"${t.suffix}\" enden`:\"includes\"===t.format?`Ungültiger String: muss \"${t.includes}\" enthalten`:\"regex\"===t.format?`Ungültiger String: muss dem Muster ${t.pattern} entsprechen`:`Ungültig: ${r[t.format]??e.format}`}case\"not_multiple_of\":return`Ungültige Zahl: muss ein Vielfaches von ${e.divisor} sein`;case\"unrecognized_keys\":return`${e.keys.length>1?\"Unbekannte Schlüssel\":\"Unbekannter Schlüssel\"}: ${l(e.keys,\", \")}`;case\"invalid_key\":return`Ungültiger Schlüssel in ${e.origin}`;case\"invalid_union\":default:return\"Ungültige Eingabe\";case\"invalid_element\":return`Ungültiger Wert in ${e.origin}`}}},Zn=()=>{const e={string:{unit:\"characters\",verb:\"to have\"},file:{unit:\"bytes\",verb:\"to have\"},array:{unit:\"items\",verb:\"to have\"},set:{unit:\"items\",verb:\"to have\"}};function t(t){return e[t]??null}const r={regex:\"input\",email:\"email address\",url:\"URL\",emoji:\"emoji\",uuid:\"UUID\",uuidv4:\"UUIDv4\",uuidv6:\"UUIDv6\",nanoid:\"nanoid\",guid:\"GUID\",cuid:\"cuid\",cuid2:\"cuid2\",ulid:\"ULID\",xid:\"XID\",ksuid:\"KSUID\",datetime:\"ISO datetime\",date:\"ISO date\",time:\"ISO time\",duration:\"ISO duration\",ipv4:\"IPv4 address\",ipv6:\"IPv6 address\",cidrv4:\"IPv4 range\",cidrv6:\"IPv6 range\",base64:\"base64-encoded string\",base64url:\"base64url-encoded string\",json_string:\"JSON string\",e164:\"E.164 number\",jwt:\"JWT\",template_literal:\"input\"};return e=>{switch(e.code){case\"invalid_type\":return`Invalid input: expected ${e.expected}, received ${(e=>{const t=typeof e;switch(t){case\"number\":return Number.isNaN(e)?\"NaN\":\"number\";case\"object\":if(Array.isArray(e))return\"array\";if(null===e)return\"null\";if(Object.getPrototypeOf(e)!==Object.prototype&&e.constructor)return e.constructor.name}return t})(e.input)}`;case\"invalid_value\":return 1===e.values.length?`Invalid input: expected ${$(e.values[0])}`:`Invalid option: expected one of ${l(e.values,\"|\")}`;case\"too_big\":{const r=e.inclusive?\"<=\":\"<\",n=t(e.origin);return n?`Too big: expected ${e.origin??\"value\"} to have ${r}${e.maximum.toString()} ${n.unit??\"elements\"}`:`Too big: expected ${e.origin??\"value\"} to be ${r}${e.maximum.toString()}`}case\"too_small\":{const r=e.inclusive?\">=\":\">\",n=t(e.origin);return n?`Too small: expected ${e.origin} to have ${r}${e.minimum.toString()} ${n.unit}`:`Too small: expected ${e.origin} to be ${r}${e.minimum.toString()}`}case\"invalid_format\":{const t=e;return\"starts_with\"===t.format?`Invalid string: must start with \"${t.prefix}\"`:\"ends_with\"===t.format?`Invalid string: must end with \"${t.suffix}\"`:\"includes\"===t.format?`Invalid string: must include \"${t.includes}\"`:\"regex\"===t.format?`Invalid string: must match pattern ${t.pattern}`:`Invalid ${r[t.format]??e.format}`}case\"not_multiple_of\":return`Invalid number: must be a multiple of ${e.divisor}`;case\"unrecognized_keys\":return`Unrecognized key${e.keys.length>1?\"s\":\"\"}: ${l(e.keys,\", \")}`;case\"invalid_key\":return`Invalid key in ${e.origin}`;case\"invalid_union\":default:return\"Invalid input\";case\"invalid_element\":return`Invalid value in ${e.origin}`}}};function Vn(){return{localeError:Zn()}}const Hn=()=>{const e={string:{unit:\"karaktrojn\",verb:\"havi\"},file:{unit:\"bajtojn\",verb:\"havi\"},array:{unit:\"elementojn\",verb:\"havi\"},set:{unit:\"elementojn\",verb:\"havi\"}};function t(t){return e[t]??null}const r={regex:\"enigo\",email:\"retadreso\",url:\"URL\",emoji:\"emoĝio\",uuid:\"UUID\",uuidv4:\"UUIDv4\",uuidv6:\"UUIDv6\",nanoid:\"nanoid\",guid:\"GUID\",cuid:\"cuid\",cuid2:\"cuid2\",ulid:\"ULID\",xid:\"XID\",ksuid:\"KSUID\",datetime:\"ISO-datotempo\",date:\"ISO-dato\",time:\"ISO-tempo\",duration:\"ISO-daŭro\",ipv4:\"IPv4-adreso\",ipv6:\"IPv6-adreso\",cidrv4:\"IPv4-rango\",cidrv6:\"IPv6-rango\",base64:\"64-ume kodita karaktraro\",base64url:\"URL-64-ume kodita karaktraro\",json_string:\"JSON-karaktraro\",e164:\"E.164-nombro\",jwt:\"JWT\",template_literal:\"enigo\"};return e=>{switch(e.code){case\"invalid_type\":return`Nevalida enigo: atendiĝis ${e.expected}, riceviĝis ${(e=>{const t=typeof e;switch(t){case\"number\":return Number.isNaN(e)?\"NaN\":\"nombro\";case\"object\":if(Array.isArray(e))return\"tabelo\";if(null===e)return\"senvalora\";if(Object.getPrototypeOf(e)!==Object.prototype&&e.constructor)return e.constructor.name}return t})(e.input)}`;case\"invalid_value\":return 1===e.values.length?`Nevalida enigo: atendiĝis ${$(e.values[0])}`:`Nevalida opcio: atendiĝis unu el ${l(e.values,\"|\")}`;case\"too_big\":{const r=e.inclusive?\"<=\":\"<\",n=t(e.origin);return n?`Tro granda: atendiĝis ke ${e.origin??\"valoro\"} havu ${r}${e.maximum.toString()} ${n.unit??\"elementojn\"}`:`Tro granda: atendiĝis ke ${e.origin??\"valoro\"} havu ${r}${e.maximum.toString()}`}case\"too_small\":{const r=e.inclusive?\">=\":\">\",n=t(e.origin);return n?`Tro malgranda: atendiĝis ke ${e.origin} havu ${r}${e.minimum.toString()} ${n.unit}`:`Tro malgranda: atendiĝis ke ${e.origin} estu ${r}${e.minimum.toString()}`}case\"invalid_format\":{const t=e;return\"starts_with\"===t.format?`Nevalida karaktraro: devas komenciĝi per \"${t.prefix}\"`:\"ends_with\"===t.format?`Nevalida karaktraro: devas finiĝi per \"${t.suffix}\"`:\"includes\"===t.format?`Nevalida karaktraro: devas inkluzivi \"${t.includes}\"`:\"regex\"===t.format?`Nevalida karaktraro: devas kongrui kun la modelo ${t.pattern}`:`Nevalida ${r[t.format]??e.format}`}case\"not_multiple_of\":return`Nevalida nombro: devas esti oblo de ${e.divisor}`;case\"unrecognized_keys\":return`Nekonata${e.keys.length>1?\"j\":\"\"} ŝlosilo${e.keys.length>1?\"j\":\"\"}: ${l(e.keys,\", \")}`;case\"invalid_key\":return`Nevalida ŝlosilo en ${e.origin}`;case\"invalid_union\":default:return\"Nevalida enigo\";case\"invalid_element\":return`Nevalida valoro en ${e.origin}`}}},Wn=()=>{const e={string:{unit:\"caracteres\",verb:\"tener\"},file:{unit:\"bytes\",verb:\"tener\"},array:{unit:\"elementos\",verb:\"tener\"},set:{unit:\"elementos\",verb:\"tener\"}},t={string:\"texto\",number:\"número\",boolean:\"booleano\",array:\"arreglo\",object:\"objeto\",set:\"conjunto\",file:\"archivo\",date:\"fecha\",bigint:\"número grande\",symbol:\"símbolo\",undefined:\"indefinido\",null:\"nulo\",function:\"función\",map:\"mapa\",record:\"registro\",tuple:\"tupla\",enum:\"enumeración\",union:\"unión\",literal:\"literal\",promise:\"promesa\",void:\"vacío\",never:\"nunca\",unknown:\"desconocido\",any:\"cualquiera\"};function r(t){return e[t]??null}function n(e){return t[e]??e}const a={regex:\"entrada\",email:\"dirección de correo electrónico\",url:\"URL\",emoji:\"emoji\",uuid:\"UUID\",uuidv4:\"UUIDv4\",uuidv6:\"UUIDv6\",nanoid:\"nanoid\",guid:\"GUID\",cuid:\"cuid\",cuid2:\"cuid2\",ulid:\"ULID\",xid:\"XID\",ksuid:\"KSUID\",datetime:\"fecha y hora ISO\",date:\"fecha ISO\",time:\"hora ISO\",duration:\"duración ISO\",ipv4:\"dirección IPv4\",ipv6:\"dirección IPv6\",cidrv4:\"rango IPv4\",cidrv6:\"rango IPv6\",base64:\"cadena codificada en base64\",base64url:\"URL codificada en base64\",json_string:\"cadena JSON\",e164:\"número E.164\",jwt:\"JWT\",template_literal:\"entrada\"};return e=>{switch(e.code){case\"invalid_type\":return`Entrada inválida: se esperaba ${n(e.expected)}, recibido ${n((e=>{const t=typeof e;switch(t){case\"number\":return Number.isNaN(e)?\"NaN\":\"number\";case\"object\":return Array.isArray(e)?\"array\":null===e?\"null\":Object.getPrototypeOf(e)!==Object.prototype?e.constructor.name:\"object\"}return t})(e.input))}`;case\"invalid_value\":return 1===e.values.length?`Entrada inválida: se esperaba ${$(e.values[0])}`:`Opción inválida: se esperaba una de ${l(e.values,\"|\")}`;case\"too_big\":{const t=e.inclusive?\"<=\":\"<\",a=r(e.origin),o=n(e.origin);return a?`Demasiado grande: se esperaba que ${o??\"valor\"} tuviera ${t}${e.maximum.toString()} ${a.unit??\"elementos\"}`:`Demasiado grande: se esperaba que ${o??\"valor\"} fuera ${t}${e.maximum.toString()}`}case\"too_small\":{const t=e.inclusive?\">=\":\">\",a=r(e.origin),o=n(e.origin);return a?`Demasiado pequeño: se esperaba que ${o} tuviera ${t}${e.minimum.toString()} ${a.unit}`:`Demasiado pequeño: se esperaba que ${o} fuera ${t}${e.minimum.toString()}`}case\"invalid_format\":{const t=e;return\"starts_with\"===t.format?`Cadena inválida: debe comenzar con \"${t.prefix}\"`:\"ends_with\"===t.format?`Cadena inválida: debe terminar en \"${t.suffix}\"`:\"includes\"===t.format?`Cadena inválida: debe incluir \"${t.includes}\"`:\"regex\"===t.format?`Cadena inválida: debe coincidir con el patrón ${t.pattern}`:`Inválido ${a[t.format]??e.format}`}case\"not_multiple_of\":return`Número inválido: debe ser múltiplo de ${e.divisor}`;case\"unrecognized_keys\":return`Llave${e.keys.length>1?\"s\":\"\"} desconocida${e.keys.length>1?\"s\":\"\"}: ${l(e.keys,\", \")}`;case\"invalid_key\":return`Llave inválida en ${n(e.origin)}`;case\"invalid_union\":default:return\"Entrada inválida\";case\"invalid_element\":return`Valor inválido en ${n(e.origin)}`}}},Xn=()=>{const e={string:{unit:\"کاراکتر\",verb:\"داشته باشد\"},file:{unit:\"بایت\",verb:\"داشته باشد\"},array:{unit:\"آیتم\",verb:\"داشته باشد\"},set:{unit:\"آیتم\",verb:\"داشته باشد\"}};function t(t){return e[t]??null}const r={regex:\"ورودی\",email:\"آدرس ایمیل\",url:\"URL\",emoji:\"ایموجی\",uuid:\"UUID\",uuidv4:\"UUIDv4\",uuidv6:\"UUIDv6\",nanoid:\"nanoid\",guid:\"GUID\",cuid:\"cuid\",cuid2:\"cuid2\",ulid:\"ULID\",xid:\"XID\",ksuid:\"KSUID\",datetime:\"تاریخ و زمان ایزو\",date:\"تاریخ ایزو\",time:\"زمان ایزو\",duration:\"مدت زمان ایزو\",ipv4:\"IPv4 آدرس\",ipv6:\"IPv6 آدرس\",cidrv4:\"IPv4 دامنه\",cidrv6:\"IPv6 دامنه\",base64:\"base64-encoded رشته\",base64url:\"base64url-encoded رشته\",json_string:\"JSON رشته\",e164:\"E.164 عدد\",jwt:\"JWT\",template_literal:\"ورودی\"};return e=>{switch(e.code){case\"invalid_type\":return`ورودی نامعتبر: می‌بایست ${e.expected} می‌بود، ${(e=>{const t=typeof e;switch(t){case\"number\":return Number.isNaN(e)?\"NaN\":\"عدد\";case\"object\":if(Array.isArray(e))return\"آرایه\";if(null===e)return\"null\";if(Object.getPrototypeOf(e)!==Object.prototype&&e.constructor)return e.constructor.name}return t})(e.input)} دریافت شد`;case\"invalid_value\":return 1===e.values.length?`ورودی نامعتبر: می‌بایست ${$(e.values[0])} می‌بود`:`گزینه نامعتبر: می‌بایست یکی از ${l(e.values,\"|\")} می‌بود`;case\"too_big\":{const r=e.inclusive?\"<=\":\"<\",n=t(e.origin);return n?`خیلی بزرگ: ${e.origin??\"مقدار\"} باید ${r}${e.maximum.toString()} ${n.unit??\"عنصر\"} باشد`:`خیلی بزرگ: ${e.origin??\"مقدار\"} باید ${r}${e.maximum.toString()} باشد`}case\"too_small\":{const r=e.inclusive?\">=\":\">\",n=t(e.origin);return n?`خیلی کوچک: ${e.origin} باید ${r}${e.minimum.toString()} ${n.unit} باشد`:`خیلی کوچک: ${e.origin} باید ${r}${e.minimum.toString()} باشد`}case\"invalid_format\":{const t=e;return\"starts_with\"===t.format?`رشته نامعتبر: باید با \"${t.prefix}\" شروع شود`:\"ends_with\"===t.format?`رشته نامعتبر: باید با \"${t.suffix}\" تمام شود`:\"includes\"===t.format?`رشته نامعتبر: باید شامل \"${t.includes}\" باشد`:\"regex\"===t.format?`رشته نامعتبر: باید با الگوی ${t.pattern} مطابقت داشته باشد`:`${r[t.format]??e.format} نامعتبر`}case\"not_multiple_of\":return`عدد نامعتبر: باید مضرب ${e.divisor} باشد`;case\"unrecognized_keys\":return`کلید${e.keys.length>1?\"های\":\"\"} ناشناس: ${l(e.keys,\", \")}`;case\"invalid_key\":return`کلید ناشناس در ${e.origin}`;case\"invalid_union\":default:return\"ورودی نامعتبر\";case\"invalid_element\":return`مقدار نامعتبر در ${e.origin}`}}},Gn=()=>{const e={string:{unit:\"merkkiä\",subject:\"merkkijonon\"},file:{unit:\"tavua\",subject:\"tiedoston\"},array:{unit:\"alkiota\",subject:\"listan\"},set:{unit:\"alkiota\",subject:\"joukon\"},number:{unit:\"\",subject:\"luvun\"},bigint:{unit:\"\",subject:\"suuren kokonaisluvun\"},int:{unit:\"\",subject:\"kokonaisluvun\"},date:{unit:\"\",subject:\"päivämäärän\"}};function t(t){return e[t]??null}const r={regex:\"säännöllinen lauseke\",email:\"sähköpostiosoite\",url:\"URL-osoite\",emoji:\"emoji\",uuid:\"UUID\",uuidv4:\"UUIDv4\",uuidv6:\"UUIDv6\",nanoid:\"nanoid\",guid:\"GUID\",cuid:\"cuid\",cuid2:\"cuid2\",ulid:\"ULID\",xid:\"XID\",ksuid:\"KSUID\",datetime:\"ISO-aikaleima\",date:\"ISO-päivämäärä\",time:\"ISO-aika\",duration:\"ISO-kesto\",ipv4:\"IPv4-osoite\",ipv6:\"IPv6-osoite\",cidrv4:\"IPv4-alue\",cidrv6:\"IPv6-alue\",base64:\"base64-koodattu merkkijono\",base64url:\"base64url-koodattu merkkijono\",json_string:\"JSON-merkkijono\",e164:\"E.164-luku\",jwt:\"JWT\",template_literal:\"templaattimerkkijono\"};return e=>{switch(e.code){case\"invalid_type\":return`Virheellinen tyyppi: odotettiin ${e.expected}, oli ${(e=>{const t=typeof e;switch(t){case\"number\":return Number.isNaN(e)?\"NaN\":\"number\";case\"object\":if(Array.isArray(e))return\"array\";if(null===e)return\"null\";if(Object.getPrototypeOf(e)!==Object.prototype&&e.constructor)return e.constructor.name}return t})(e.input)}`;case\"invalid_value\":return 1===e.values.length?`Virheellinen syöte: täytyy olla ${$(e.values[0])}`:`Virheellinen valinta: täytyy olla yksi seuraavista: ${l(e.values,\"|\")}`;case\"too_big\":{const r=e.inclusive?\"<=\":\"<\",n=t(e.origin);return n?`Liian suuri: ${n.subject} täytyy olla ${r}${e.maximum.toString()} ${n.unit}`.trim():`Liian suuri: arvon täytyy olla ${r}${e.maximum.toString()}`}case\"too_small\":{const r=e.inclusive?\">=\":\">\",n=t(e.origin);return n?`Liian pieni: ${n.subject} täytyy olla ${r}${e.minimum.toString()} ${n.unit}`.trim():`Liian pieni: arvon täytyy olla ${r}${e.minimum.toString()}`}case\"invalid_format\":{const t=e;return\"starts_with\"===t.format?`Virheellinen syöte: täytyy alkaa \"${t.prefix}\"`:\"ends_with\"===t.format?`Virheellinen syöte: täytyy loppua \"${t.suffix}\"`:\"includes\"===t.format?`Virheellinen syöte: täytyy sisältää \"${t.includes}\"`:\"regex\"===t.format?`Virheellinen syöte: täytyy vastata säännöllistä lauseketta ${t.pattern}`:`Virheellinen ${r[t.format]??e.format}`}case\"not_multiple_of\":return`Virheellinen luku: täytyy olla luvun ${e.divisor} monikerta`;case\"unrecognized_keys\":return`${e.keys.length>1?\"Tuntemattomat avaimet\":\"Tuntematon avain\"}: ${l(e.keys,\", \")}`;case\"invalid_key\":return\"Virheellinen avain tietueessa\";case\"invalid_union\":return\"Virheellinen unioni\";case\"invalid_element\":return\"Virheellinen arvo joukossa\";default:return\"Virheellinen syöte\"}}},Yn=()=>{const e={string:{unit:\"caractères\",verb:\"avoir\"},file:{unit:\"octets\",verb:\"avoir\"},array:{unit:\"éléments\",verb:\"avoir\"},set:{unit:\"éléments\",verb:\"avoir\"}};function t(t){return e[t]??null}const r={regex:\"entrée\",email:\"adresse e-mail\",url:\"URL\",emoji:\"emoji\",uuid:\"UUID\",uuidv4:\"UUIDv4\",uuidv6:\"UUIDv6\",nanoid:\"nanoid\",guid:\"GUID\",cuid:\"cuid\",cuid2:\"cuid2\",ulid:\"ULID\",xid:\"XID\",ksuid:\"KSUID\",datetime:\"date et heure ISO\",date:\"date ISO\",time:\"heure ISO\",duration:\"durée ISO\",ipv4:\"adresse IPv4\",ipv6:\"adresse IPv6\",cidrv4:\"plage IPv4\",cidrv6:\"plage IPv6\",base64:\"chaîne encodée en base64\",base64url:\"chaîne encodée en base64url\",json_string:\"chaîne JSON\",e164:\"numéro E.164\",jwt:\"JWT\",template_literal:\"entrée\"};return e=>{switch(e.code){case\"invalid_type\":return`Entrée invalide : ${e.expected} attendu, ${(e=>{const t=typeof e;switch(t){case\"number\":return Number.isNaN(e)?\"NaN\":\"nombre\";case\"object\":if(Array.isArray(e))return\"tableau\";if(null===e)return\"null\";if(Object.getPrototypeOf(e)!==Object.prototype&&e.constructor)return e.constructor.name}return t})(e.input)} reçu`;case\"invalid_value\":return 1===e.values.length?`Entrée invalide : ${$(e.values[0])} attendu`:`Option invalide : une valeur parmi ${l(e.values,\"|\")} attendue`;case\"too_big\":{const r=e.inclusive?\"<=\":\"<\",n=t(e.origin);return n?`Trop grand : ${e.origin??\"valeur\"} doit ${n.verb} ${r}${e.maximum.toString()} ${n.unit??\"élément(s)\"}`:`Trop grand : ${e.origin??\"valeur\"} doit être ${r}${e.maximum.toString()}`}case\"too_small\":{const r=e.inclusive?\">=\":\">\",n=t(e.origin);return n?`Trop petit : ${e.origin} doit ${n.verb} ${r}${e.minimum.toString()} ${n.unit}`:`Trop petit : ${e.origin} doit être ${r}${e.minimum.toString()}`}case\"invalid_format\":{const t=e;return\"starts_with\"===t.format?`Chaîne invalide : doit commencer par \"${t.prefix}\"`:\"ends_with\"===t.format?`Chaîne invalide : doit se terminer par \"${t.suffix}\"`:\"includes\"===t.format?`Chaîne invalide : doit inclure \"${t.includes}\"`:\"regex\"===t.format?`Chaîne invalide : doit correspondre au modèle ${t.pattern}`:`${r[t.format]??e.format} invalide`}case\"not_multiple_of\":return`Nombre invalide : doit être un multiple de ${e.divisor}`;case\"unrecognized_keys\":return`Clé${e.keys.length>1?\"s\":\"\"} non reconnue${e.keys.length>1?\"s\":\"\"} : ${l(e.keys,\", \")}`;case\"invalid_key\":return`Clé invalide dans ${e.origin}`;case\"invalid_union\":default:return\"Entrée invalide\";case\"invalid_element\":return`Valeur invalide dans ${e.origin}`}}},Kn=()=>{const e={string:{unit:\"caractères\",verb:\"avoir\"},file:{unit:\"octets\",verb:\"avoir\"},array:{unit:\"éléments\",verb:\"avoir\"},set:{unit:\"éléments\",verb:\"avoir\"}};function t(t){return e[t]??null}const r={regex:\"entrée\",email:\"adresse courriel\",url:\"URL\",emoji:\"emoji\",uuid:\"UUID\",uuidv4:\"UUIDv4\",uuidv6:\"UUIDv6\",nanoid:\"nanoid\",guid:\"GUID\",cuid:\"cuid\",cuid2:\"cuid2\",ulid:\"ULID\",xid:\"XID\",ksuid:\"KSUID\",datetime:\"date-heure ISO\",date:\"date ISO\",time:\"heure ISO\",duration:\"durée ISO\",ipv4:\"adresse IPv4\",ipv6:\"adresse IPv6\",cidrv4:\"plage IPv4\",cidrv6:\"plage IPv6\",base64:\"chaîne encodée en base64\",base64url:\"chaîne encodée en base64url\",json_string:\"chaîne JSON\",e164:\"numéro E.164\",jwt:\"JWT\",template_literal:\"entrée\"};return e=>{switch(e.code){case\"invalid_type\":return`Entrée invalide : attendu ${e.expected}, reçu ${(e=>{const t=typeof e;switch(t){case\"number\":return Number.isNaN(e)?\"NaN\":\"number\";case\"object\":if(Array.isArray(e))return\"array\";if(null===e)return\"null\";if(Object.getPrototypeOf(e)!==Object.prototype&&e.constructor)return e.constructor.name}return t})(e.input)}`;case\"invalid_value\":return 1===e.values.length?`Entrée invalide : attendu ${$(e.values[0])}`:`Option invalide : attendu l'une des valeurs suivantes ${l(e.values,\"|\")}`;case\"too_big\":{const r=e.inclusive?\"≤\":\"<\",n=t(e.origin);return n?`Trop grand : attendu que ${e.origin??\"la valeur\"} ait ${r}${e.maximum.toString()} ${n.unit}`:`Trop grand : attendu que ${e.origin??\"la valeur\"} soit ${r}${e.maximum.toString()}`}case\"too_small\":{const r=e.inclusive?\"≥\":\">\",n=t(e.origin);return n?`Trop petit : attendu que ${e.origin} ait ${r}${e.minimum.toString()} ${n.unit}`:`Trop petit : attendu que ${e.origin} soit ${r}${e.minimum.toString()}`}case\"invalid_format\":{const t=e;return\"starts_with\"===t.format?`Chaîne invalide : doit commencer par \"${t.prefix}\"`:\"ends_with\"===t.format?`Chaîne invalide : doit se terminer par \"${t.suffix}\"`:\"includes\"===t.format?`Chaîne invalide : doit inclure \"${t.includes}\"`:\"regex\"===t.format?`Chaîne invalide : doit correspondre au motif ${t.pattern}`:`${r[t.format]??e.format} invalide`}case\"not_multiple_of\":return`Nombre invalide : doit être un multiple de ${e.divisor}`;case\"unrecognized_keys\":return`Clé${e.keys.length>1?\"s\":\"\"} non reconnue${e.keys.length>1?\"s\":\"\"} : ${l(e.keys,\", \")}`;case\"invalid_key\":return`Clé invalide dans ${e.origin}`;case\"invalid_union\":default:return\"Entrée invalide\";case\"invalid_element\":return`Valeur invalide dans ${e.origin}`}}},Jn=()=>{const e={string:{unit:\"אותיות\",verb:\"לכלול\"},file:{unit:\"בייטים\",verb:\"לכלול\"},array:{unit:\"פריטים\",verb:\"לכלול\"},set:{unit:\"פריטים\",verb:\"לכלול\"}};function t(t){return e[t]??null}const r={regex:\"קלט\",email:\"כתובת אימייל\",url:\"כתובת רשת\",emoji:\"אימוג'י\",uuid:\"UUID\",uuidv4:\"UUIDv4\",uuidv6:\"UUIDv6\",nanoid:\"nanoid\",guid:\"GUID\",cuid:\"cuid\",cuid2:\"cuid2\",ulid:\"ULID\",xid:\"XID\",ksuid:\"KSUID\",datetime:\"תאריך וזמן ISO\",date:\"תאריך ISO\",time:\"זמן ISO\",duration:\"משך זמן ISO\",ipv4:\"כתובת IPv4\",ipv6:\"כתובת IPv6\",cidrv4:\"טווח IPv4\",cidrv6:\"טווח IPv6\",base64:\"מחרוזת בבסיס 64\",base64url:\"מחרוזת בבסיס 64 לכתובות רשת\",json_string:\"מחרוזת JSON\",e164:\"מספר E.164\",jwt:\"JWT\",template_literal:\"קלט\"};return e=>{switch(e.code){case\"invalid_type\":return`קלט לא תקין: צריך ${e.expected}, התקבל ${(e=>{const t=typeof e;switch(t){case\"number\":return Number.isNaN(e)?\"NaN\":\"number\";case\"object\":if(Array.isArray(e))return\"array\";if(null===e)return\"null\";if(Object.getPrototypeOf(e)!==Object.prototype&&e.constructor)return e.constructor.name}return t})(e.input)}`;case\"invalid_value\":return 1===e.values.length?`קלט לא תקין: צריך ${$(e.values[0])}`:`קלט לא תקין: צריך אחת מהאפשרויות ${l(e.values,\"|\")}`;case\"too_big\":{const r=e.inclusive?\"<=\":\"<\",n=t(e.origin);return n?`גדול מדי: ${e.origin??\"value\"} צריך להיות ${r}${e.maximum.toString()} ${n.unit??\"elements\"}`:`גדול מדי: ${e.origin??\"value\"} צריך להיות ${r}${e.maximum.toString()}`}case\"too_small\":{const r=e.inclusive?\">=\":\">\",n=t(e.origin);return n?`קטן מדי: ${e.origin} צריך להיות ${r}${e.minimum.toString()} ${n.unit}`:`קטן מדי: ${e.origin} צריך להיות ${r}${e.minimum.toString()}`}case\"invalid_format\":{const t=e;return\"starts_with\"===t.format?`מחרוזת לא תקינה: חייבת להתחיל ב\"${t.prefix}\"`:\"ends_with\"===t.format?`מחרוזת לא תקינה: חייבת להסתיים ב \"${t.suffix}\"`:\"includes\"===t.format?`מחרוזת לא תקינה: חייבת לכלול \"${t.includes}\"`:\"regex\"===t.format?`מחרוזת לא תקינה: חייבת להתאים לתבנית ${t.pattern}`:`${r[t.format]??e.format} לא תקין`}case\"not_multiple_of\":return`מספר לא תקין: חייב להיות מכפלה של ${e.divisor}`;case\"unrecognized_keys\":return`מפתח${e.keys.length>1?\"ות\":\"\"} לא מזוה${e.keys.length>1?\"ים\":\"ה\"}: ${l(e.keys,\", \")}`;case\"invalid_key\":return`מפתח לא תקין ב${e.origin}`;case\"invalid_union\":default:return\"קלט לא תקין\";case\"invalid_element\":return`ערך לא תקין ב${e.origin}`}}},ea=()=>{const e={string:{unit:\"karakter\",verb:\"legyen\"},file:{unit:\"byte\",verb:\"legyen\"},array:{unit:\"elem\",verb:\"legyen\"},set:{unit:\"elem\",verb:\"legyen\"}};function t(t){return e[t]??null}const r={regex:\"bemenet\",email:\"email cím\",url:\"URL\",emoji:\"emoji\",uuid:\"UUID\",uuidv4:\"UUIDv4\",uuidv6:\"UUIDv6\",nanoid:\"nanoid\",guid:\"GUID\",cuid:\"cuid\",cuid2:\"cuid2\",ulid:\"ULID\",xid:\"XID\",ksuid:\"KSUID\",datetime:\"ISO időbélyeg\",date:\"ISO dátum\",time:\"ISO idő\",duration:\"ISO időintervallum\",ipv4:\"IPv4 cím\",ipv6:\"IPv6 cím\",cidrv4:\"IPv4 tartomány\",cidrv6:\"IPv6 tartomány\",base64:\"base64-kódolt string\",base64url:\"base64url-kódolt string\",json_string:\"JSON string\",e164:\"E.164 szám\",jwt:\"JWT\",template_literal:\"bemenet\"};return e=>{switch(e.code){case\"invalid_type\":return`Érvénytelen bemenet: a várt érték ${e.expected}, a kapott érték ${(e=>{const t=typeof e;switch(t){case\"number\":return Number.isNaN(e)?\"NaN\":\"szám\";case\"object\":if(Array.isArray(e))return\"tömb\";if(null===e)return\"null\";if(Object.getPrototypeOf(e)!==Object.prototype&&e.constructor)return e.constructor.name}return t})(e.input)}`;case\"invalid_value\":return 1===e.values.length?`Érvénytelen bemenet: a várt érték ${$(e.values[0])}`:`Érvénytelen opció: valamelyik érték várt ${l(e.values,\"|\")}`;case\"too_big\":{const r=e.inclusive?\"<=\":\"<\",n=t(e.origin);return n?`Túl nagy: ${e.origin??\"érték\"} mérete túl nagy ${r}${e.maximum.toString()} ${n.unit??\"elem\"}`:`Túl nagy: a bemeneti érték ${e.origin??\"érték\"} túl nagy: ${r}${e.maximum.toString()}`}case\"too_small\":{const r=e.inclusive?\">=\":\">\",n=t(e.origin);return n?`Túl kicsi: a bemeneti érték ${e.origin} mérete túl kicsi ${r}${e.minimum.toString()} ${n.unit}`:`Túl kicsi: a bemeneti érték ${e.origin} túl kicsi ${r}${e.minimum.toString()}`}case\"invalid_format\":{const t=e;return\"starts_with\"===t.format?`Érvénytelen string: \"${t.prefix}\" értékkel kell kezdődnie`:\"ends_with\"===t.format?`Érvénytelen string: \"${t.suffix}\" értékkel kell végződnie`:\"includes\"===t.format?`Érvénytelen string: \"${t.includes}\" értéket kell tartalmaznia`:\"regex\"===t.format?`Érvénytelen string: ${t.pattern} mintának kell megfelelnie`:`Érvénytelen ${r[t.format]??e.format}`}case\"not_multiple_of\":return`Érvénytelen szám: ${e.divisor} többszörösének kell lennie`;case\"unrecognized_keys\":return`Ismeretlen kulcs${e.keys.length>1?\"s\":\"\"}: ${l(e.keys,\", \")}`;case\"invalid_key\":return`Érvénytelen kulcs ${e.origin}`;case\"invalid_union\":default:return\"Érvénytelen bemenet\";case\"invalid_element\":return`Érvénytelen érték: ${e.origin}`}}},ta=()=>{const e={string:{unit:\"karakter\",verb:\"memiliki\"},file:{unit:\"byte\",verb:\"memiliki\"},array:{unit:\"item\",verb:\"memiliki\"},set:{unit:\"item\",verb:\"memiliki\"}};function t(t){return e[t]??null}const r={regex:\"input\",email:\"alamat email\",url:\"URL\",emoji:\"emoji\",uuid:\"UUID\",uuidv4:\"UUIDv4\",uuidv6:\"UUIDv6\",nanoid:\"nanoid\",guid:\"GUID\",cuid:\"cuid\",cuid2:\"cuid2\",ulid:\"ULID\",xid:\"XID\",ksuid:\"KSUID\",datetime:\"tanggal dan waktu format ISO\",date:\"tanggal format ISO\",time:\"jam format ISO\",duration:\"durasi format ISO\",ipv4:\"alamat IPv4\",ipv6:\"alamat IPv6\",cidrv4:\"rentang alamat IPv4\",cidrv6:\"rentang alamat IPv6\",base64:\"string dengan enkode base64\",base64url:\"string dengan enkode base64url\",json_string:\"string JSON\",e164:\"angka E.164\",jwt:\"JWT\",template_literal:\"input\"};return e=>{switch(e.code){case\"invalid_type\":return`Input tidak valid: diharapkan ${e.expected}, diterima ${(e=>{const t=typeof e;switch(t){case\"number\":return Number.isNaN(e)?\"NaN\":\"number\";case\"object\":if(Array.isArray(e))return\"array\";if(null===e)return\"null\";if(Object.getPrototypeOf(e)!==Object.prototype&&e.constructor)return e.constructor.name}return t})(e.input)}`;case\"invalid_value\":return 1===e.values.length?`Input tidak valid: diharapkan ${$(e.values[0])}`:`Pilihan tidak valid: diharapkan salah satu dari ${l(e.values,\"|\")}`;case\"too_big\":{const r=e.inclusive?\"<=\":\"<\",n=t(e.origin);return n?`Terlalu besar: diharapkan ${e.origin??\"value\"} memiliki ${r}${e.maximum.toString()} ${n.unit??\"elemen\"}`:`Terlalu besar: diharapkan ${e.origin??\"value\"} menjadi ${r}${e.maximum.toString()}`}case\"too_small\":{const r=e.inclusive?\">=\":\">\",n=t(e.origin);return n?`Terlalu kecil: diharapkan ${e.origin} memiliki ${r}${e.minimum.toString()} ${n.unit}`:`Terlalu kecil: diharapkan ${e.origin} menjadi ${r}${e.minimum.toString()}`}case\"invalid_format\":{const t=e;return\"starts_with\"===t.format?`String tidak valid: harus dimulai dengan \"${t.prefix}\"`:\"ends_with\"===t.format?`String tidak valid: harus berakhir dengan \"${t.suffix}\"`:\"includes\"===t.format?`String tidak valid: harus menyertakan \"${t.includes}\"`:\"regex\"===t.format?`String tidak valid: harus sesuai pola ${t.pattern}`:`${r[t.format]??e.format} tidak valid`}case\"not_multiple_of\":return`Angka tidak valid: harus kelipatan dari ${e.divisor}`;case\"unrecognized_keys\":return`Kunci tidak dikenali ${e.keys.length>1?\"s\":\"\"}: ${l(e.keys,\", \")}`;case\"invalid_key\":return`Kunci tidak valid di ${e.origin}`;case\"invalid_union\":default:return\"Input tidak valid\";case\"invalid_element\":return`Nilai tidak valid di ${e.origin}`}}},ra=()=>{const e={string:{unit:\"stafi\",verb:\"að hafa\"},file:{unit:\"bæti\",verb:\"að hafa\"},array:{unit:\"hluti\",verb:\"að hafa\"},set:{unit:\"hluti\",verb:\"að hafa\"}};function t(t){return e[t]??null}const r={regex:\"gildi\",email:\"netfang\",url:\"vefslóð\",emoji:\"emoji\",uuid:\"UUID\",uuidv4:\"UUIDv4\",uuidv6:\"UUIDv6\",nanoid:\"nanoid\",guid:\"GUID\",cuid:\"cuid\",cuid2:\"cuid2\",ulid:\"ULID\",xid:\"XID\",ksuid:\"KSUID\",datetime:\"ISO dagsetning og tími\",date:\"ISO dagsetning\",time:\"ISO tími\",duration:\"ISO tímalengd\",ipv4:\"IPv4 address\",ipv6:\"IPv6 address\",cidrv4:\"IPv4 range\",cidrv6:\"IPv6 range\",base64:\"base64-encoded strengur\",base64url:\"base64url-encoded strengur\",json_string:\"JSON strengur\",e164:\"E.164 tölugildi\",jwt:\"JWT\",template_literal:\"gildi\"};return e=>{switch(e.code){case\"invalid_type\":return`Rangt gildi: Þú slóst inn ${(e=>{const t=typeof e;switch(t){case\"number\":return Number.isNaN(e)?\"NaN\":\"númer\";case\"object\":if(Array.isArray(e))return\"fylki\";if(null===e)return\"null\";if(Object.getPrototypeOf(e)!==Object.prototype&&e.constructor)return e.constructor.name}return t})(e.input)} þar sem á að vera ${e.expected}`;case\"invalid_value\":return 1===e.values.length?`Rangt gildi: gert ráð fyrir ${$(e.values[0])}`:`Ógilt val: má vera eitt af eftirfarandi ${l(e.values,\"|\")}`;case\"too_big\":{const r=e.inclusive?\"<=\":\"<\",n=t(e.origin);return n?`Of stórt: gert er ráð fyrir að ${e.origin??\"gildi\"} hafi ${r}${e.maximum.toString()} ${n.unit??\"hluti\"}`:`Of stórt: gert er ráð fyrir að ${e.origin??\"gildi\"} sé ${r}${e.maximum.toString()}`}case\"too_small\":{const r=e.inclusive?\">=\":\">\",n=t(e.origin);return n?`Of lítið: gert er ráð fyrir að ${e.origin} hafi ${r}${e.minimum.toString()} ${n.unit}`:`Of lítið: gert er ráð fyrir að ${e.origin} sé ${r}${e.minimum.toString()}`}case\"invalid_format\":{const t=e;return\"starts_with\"===t.format?`Ógildur strengur: verður að byrja á \"${t.prefix}\"`:\"ends_with\"===t.format?`Ógildur strengur: verður að enda á \"${t.suffix}\"`:\"includes\"===t.format?`Ógildur strengur: verður að innihalda \"${t.includes}\"`:\"regex\"===t.format?`Ógildur strengur: verður að fylgja mynstri ${t.pattern}`:`Rangt ${r[t.format]??e.format}`}case\"not_multiple_of\":return`Röng tala: verður að vera margfeldi af ${e.divisor}`;case\"unrecognized_keys\":return`Óþekkt ${e.keys.length>1?\"ir lyklar\":\"ur lykill\"}: ${l(e.keys,\", \")}`;case\"invalid_key\":return`Rangur lykill í ${e.origin}`;case\"invalid_union\":default:return\"Rangt gildi\";case\"invalid_element\":return`Rangt gildi í ${e.origin}`}}},na=()=>{const e={string:{unit:\"caratteri\",verb:\"avere\"},file:{unit:\"byte\",verb:\"avere\"},array:{unit:\"elementi\",verb:\"avere\"},set:{unit:\"elementi\",verb:\"avere\"}};function t(t){return e[t]??null}const r={regex:\"input\",email:\"indirizzo email\",url:\"URL\",emoji:\"emoji\",uuid:\"UUID\",uuidv4:\"UUIDv4\",uuidv6:\"UUIDv6\",nanoid:\"nanoid\",guid:\"GUID\",cuid:\"cuid\",cuid2:\"cuid2\",ulid:\"ULID\",xid:\"XID\",ksuid:\"KSUID\",datetime:\"data e ora ISO\",date:\"data ISO\",time:\"ora ISO\",duration:\"durata ISO\",ipv4:\"indirizzo IPv4\",ipv6:\"indirizzo IPv6\",cidrv4:\"intervallo IPv4\",cidrv6:\"intervallo IPv6\",base64:\"stringa codificata in base64\",base64url:\"URL codificata in base64\",json_string:\"stringa JSON\",e164:\"numero E.164\",jwt:\"JWT\",template_literal:\"input\"};return e=>{switch(e.code){case\"invalid_type\":return`Input non valido: atteso ${e.expected}, ricevuto ${(e=>{const t=typeof e;switch(t){case\"number\":return Number.isNaN(e)?\"NaN\":\"numero\";case\"object\":if(Array.isArray(e))return\"vettore\";if(null===e)return\"null\";if(Object.getPrototypeOf(e)!==Object.prototype&&e.constructor)return e.constructor.name}return t})(e.input)}`;case\"invalid_value\":return 1===e.values.length?`Input non valido: atteso ${$(e.values[0])}`:`Opzione non valida: atteso uno tra ${l(e.values,\"|\")}`;case\"too_big\":{const r=e.inclusive?\"<=\":\"<\",n=t(e.origin);return n?`Troppo grande: ${e.origin??\"valore\"} deve avere ${r}${e.maximum.toString()} ${n.unit??\"elementi\"}`:`Troppo grande: ${e.origin??\"valore\"} deve essere ${r}${e.maximum.toString()}`}case\"too_small\":{const r=e.inclusive?\">=\":\">\",n=t(e.origin);return n?`Troppo piccolo: ${e.origin} deve avere ${r}${e.minimum.toString()} ${n.unit}`:`Troppo piccolo: ${e.origin} deve essere ${r}${e.minimum.toString()}`}case\"invalid_format\":{const t=e;return\"starts_with\"===t.format?`Stringa non valida: deve iniziare con \"${t.prefix}\"`:\"ends_with\"===t.format?`Stringa non valida: deve terminare con \"${t.suffix}\"`:\"includes\"===t.format?`Stringa non valida: deve includere \"${t.includes}\"`:\"regex\"===t.format?`Stringa non valida: deve corrispondere al pattern ${t.pattern}`:`Invalid ${r[t.format]??e.format}`}case\"not_multiple_of\":return`Numero non valido: deve essere un multiplo di ${e.divisor}`;case\"unrecognized_keys\":return`Chiav${e.keys.length>1?\"i\":\"e\"} non riconosciut${e.keys.length>1?\"e\":\"a\"}: ${l(e.keys,\", \")}`;case\"invalid_key\":return`Chiave non valida in ${e.origin}`;case\"invalid_union\":default:return\"Input non valido\";case\"invalid_element\":return`Valore non valido in ${e.origin}`}}},aa=()=>{const e={string:{unit:\"文字\",verb:\"である\"},file:{unit:\"バイト\",verb:\"である\"},array:{unit:\"要素\",verb:\"である\"},set:{unit:\"要素\",verb:\"である\"}};function t(t){return e[t]??null}const r={regex:\"入力値\",email:\"メールアドレス\",url:\"URL\",emoji:\"絵文字\",uuid:\"UUID\",uuidv4:\"UUIDv4\",uuidv6:\"UUIDv6\",nanoid:\"nanoid\",guid:\"GUID\",cuid:\"cuid\",cuid2:\"cuid2\",ulid:\"ULID\",xid:\"XID\",ksuid:\"KSUID\",datetime:\"ISO日時\",date:\"ISO日付\",time:\"ISO時刻\",duration:\"ISO期間\",ipv4:\"IPv4アドレス\",ipv6:\"IPv6アドレス\",cidrv4:\"IPv4範囲\",cidrv6:\"IPv6範囲\",base64:\"base64エンコード文字列\",base64url:\"base64urlエンコード文字列\",json_string:\"JSON文字列\",e164:\"E.164番号\",jwt:\"JWT\",template_literal:\"入力値\"};return e=>{switch(e.code){case\"invalid_type\":return`無効な入力: ${e.expected}が期待されましたが、${(e=>{const t=typeof e;switch(t){case\"number\":return Number.isNaN(e)?\"NaN\":\"数値\";case\"object\":if(Array.isArray(e))return\"配列\";if(null===e)return\"null\";if(Object.getPrototypeOf(e)!==Object.prototype&&e.constructor)return e.constructor.name}return t})(e.input)}が入力されました`;case\"invalid_value\":return 1===e.values.length?`無効な入力: ${$(e.values[0])}が期待されました`:`無効な選択: ${l(e.values,\"、\")}のいずれかである必要があります`;case\"too_big\":{const r=e.inclusive?\"以下である\":\"より小さい\",n=t(e.origin);return n?`大きすぎる値: ${e.origin??\"値\"}は${e.maximum.toString()}${n.unit??\"要素\"}${r}必要があります`:`大きすぎる値: ${e.origin??\"値\"}は${e.maximum.toString()}${r}必要があります`}case\"too_small\":{const r=e.inclusive?\"以上である\":\"より大きい\",n=t(e.origin);return n?`小さすぎる値: ${e.origin}は${e.minimum.toString()}${n.unit}${r}必要があります`:`小さすぎる値: ${e.origin}は${e.minimum.toString()}${r}必要があります`}case\"invalid_format\":{const t=e;return\"starts_with\"===t.format?`無効な文字列: \"${t.prefix}\"で始まる必要があります`:\"ends_with\"===t.format?`無効な文字列: \"${t.suffix}\"で終わる必要があります`:\"includes\"===t.format?`無効な文字列: \"${t.includes}\"を含む必要があります`:\"regex\"===t.format?`無効な文字列: パターン${t.pattern}に一致する必要があります`:`無効な${r[t.format]??e.format}`}case\"not_multiple_of\":return`無効な数値: ${e.divisor}の倍数である必要があります`;case\"unrecognized_keys\":return`認識されていないキー${e.keys.length>1?\"群\":\"\"}: ${l(e.keys,\"、\")}`;case\"invalid_key\":return`${e.origin}内の無効なキー`;case\"invalid_union\":default:return\"無効な入力\";case\"invalid_element\":return`${e.origin}内の無効な値`}}},oa=()=>{const e={string:{unit:\"სიმბოლო\",verb:\"უნდა შეიცავდეს\"},file:{unit:\"ბაიტი\",verb:\"უნდა შეიცავდეს\"},array:{unit:\"ელემენტი\",verb:\"უნდა შეიცავდეს\"},set:{unit:\"ელემენტი\",verb:\"უნდა შეიცავდეს\"}};function t(t){return e[t]??null}const r={regex:\"შეყვანა\",email:\"ელ-ფოსტის მისამართი\",url:\"URL\",emoji:\"ემოჯი\",uuid:\"UUID\",uuidv4:\"UUIDv4\",uuidv6:\"UUIDv6\",nanoid:\"nanoid\",guid:\"GUID\",cuid:\"cuid\",cuid2:\"cuid2\",ulid:\"ULID\",xid:\"XID\",ksuid:\"KSUID\",datetime:\"თარიღი-დრო\",date:\"თარიღი\",time:\"დრო\",duration:\"ხანგრძლივობა\",ipv4:\"IPv4 მისამართი\",ipv6:\"IPv6 მისამართი\",cidrv4:\"IPv4 დიაპაზონი\",cidrv6:\"IPv6 დიაპაზონი\",base64:\"base64-კოდირებული სტრინგი\",base64url:\"base64url-კოდირებული სტრინგი\",json_string:\"JSON სტრინგი\",e164:\"E.164 ნომერი\",jwt:\"JWT\",template_literal:\"შეყვანა\"};return e=>{switch(e.code){case\"invalid_type\":return`არასწორი შეყვანა: მოსალოდნელი ${e.expected}, მიღებული ${(e=>{const t=typeof e;switch(t){case\"number\":return Number.isNaN(e)?\"NaN\":\"რიცხვი\";case\"object\":if(Array.isArray(e))return\"მასივი\";if(null===e)return\"null\";if(Object.getPrototypeOf(e)!==Object.prototype&&e.constructor)return e.constructor.name}return{string:\"სტრინგი\",boolean:\"ბულეანი\",undefined:\"undefined\",bigint:\"bigint\",symbol:\"symbol\",function:\"ფუნქცია\"}[t]??t})(e.input)}`;case\"invalid_value\":return 1===e.values.length?`არასწორი შეყვანა: მოსალოდნელი ${$(e.values[0])}`:`არასწორი ვარიანტი: მოსალოდნელია ერთ-ერთი ${l(e.values,\"|\")}-დან`;case\"too_big\":{const r=e.inclusive?\"<=\":\"<\",n=t(e.origin);return n?`ზედმეტად დიდი: მოსალოდნელი ${e.origin??\"მნიშვნელობა\"} ${n.verb} ${r}${e.maximum.toString()} ${n.unit}`:`ზედმეტად დიდი: მოსალოდნელი ${e.origin??\"მნიშვნელობა\"} იყოს ${r}${e.maximum.toString()}`}case\"too_small\":{const r=e.inclusive?\">=\":\">\",n=t(e.origin);return n?`ზედმეტად პატარა: მოსალოდნელი ${e.origin} ${n.verb} ${r}${e.minimum.toString()} ${n.unit}`:`ზედმეტად პატარა: მოსალოდნელი ${e.origin} იყოს ${r}${e.minimum.toString()}`}case\"invalid_format\":{const t=e;return\"starts_with\"===t.format?`არასწორი სტრინგი: უნდა იწყებოდეს \"${t.prefix}\"-ით`:\"ends_with\"===t.format?`არასწორი სტრინგი: უნდა მთავრდებოდეს \"${t.suffix}\"-ით`:\"includes\"===t.format?`არასწორი სტრინგი: უნდა შეიცავდეს \"${t.includes}\"-ს`:\"regex\"===t.format?`არასწორი სტრინგი: უნდა შეესაბამებოდეს შაბლონს ${t.pattern}`:`არასწორი ${r[t.format]??e.format}`}case\"not_multiple_of\":return`არასწორი რიცხვი: უნდა იყოს ${e.divisor}-ის ჯერადი`;case\"unrecognized_keys\":return`უცნობი გასაღებ${e.keys.length>1?\"ები\":\"ი\"}: ${l(e.keys,\", \")}`;case\"invalid_key\":return`არასწორი გასაღები ${e.origin}-ში`;case\"invalid_union\":default:return\"არასწორი შეყვანა\";case\"invalid_element\":return`არასწორი მნიშვნელობა ${e.origin}-ში`}}},ia=()=>{const e={string:{unit:\"តួអក្សរ\",verb:\"គួរមាន\"},file:{unit:\"បៃ\",verb:\"គួរមាន\"},array:{unit:\"ធាតុ\",verb:\"គួរមាន\"},set:{unit:\"ធាតុ\",verb:\"គួរមាន\"}};function t(t){return e[t]??null}const r={regex:\"ទិន្នន័យបញ្ចូល\",email:\"អាសយដ្ឋានអ៊ីមែល\",url:\"URL\",emoji:\"សញ្ញាអារម្មណ៍\",uuid:\"UUID\",uuidv4:\"UUIDv4\",uuidv6:\"UUIDv6\",nanoid:\"nanoid\",guid:\"GUID\",cuid:\"cuid\",cuid2:\"cuid2\",ulid:\"ULID\",xid:\"XID\",ksuid:\"KSUID\",datetime:\"កាលបរិច្ឆេទ និងម៉ោង ISO\",date:\"កាលបរិច្ឆេទ ISO\",time:\"ម៉ោង ISO\",duration:\"រយៈពេល ISO\",ipv4:\"អាសយដ្ឋាន IPv4\",ipv6:\"អាសយដ្ឋាន IPv6\",cidrv4:\"ដែនអាសយដ្ឋាន IPv4\",cidrv6:\"ដែនអាសយដ្ឋាន IPv6\",base64:\"ខ្សែអក្សរអ៊ិកូដ base64\",base64url:\"ខ្សែអក្សរអ៊ិកូដ base64url\",json_string:\"ខ្សែអក្សរ JSON\",e164:\"លេខ E.164\",jwt:\"JWT\",template_literal:\"ទិន្នន័យបញ្ចូល\"};return e=>{switch(e.code){case\"invalid_type\":return`ទិន្នន័យបញ្ចូលមិនត្រឹមត្រូវ៖ ត្រូវការ ${e.expected} ប៉ុន្តែទទួលបាន ${(e=>{const t=typeof e;switch(t){case\"number\":return Number.isNaN(e)?\"មិនមែនជាលេខ (NaN)\":\"លេខ\";case\"object\":if(Array.isArray(e))return\"អារេ (Array)\";if(null===e)return\"គ្មានតម្លៃ (null)\";if(Object.getPrototypeOf(e)!==Object.prototype&&e.constructor)return e.constructor.name}return t})(e.input)}`;case\"invalid_value\":return 1===e.values.length?`ទិន្នន័យបញ្ចូលមិនត្រឹមត្រូវ៖ ត្រូវការ ${$(e.values[0])}`:`ជម្រើសមិនត្រឹមត្រូវ៖ ត្រូវជាមួយក្នុងចំណោម ${l(e.values,\"|\")}`;case\"too_big\":{const r=e.inclusive?\"<=\":\"<\",n=t(e.origin);return n?`ធំពេក៖ ត្រូវការ ${e.origin??\"តម្លៃ\"} ${r} ${e.maximum.toString()} ${n.unit??\"ធាតុ\"}`:`ធំពេក៖ ត្រូវការ ${e.origin??\"តម្លៃ\"} ${r} ${e.maximum.toString()}`}case\"too_small\":{const r=e.inclusive?\">=\":\">\",n=t(e.origin);return n?`តូចពេក៖ ត្រូវការ ${e.origin} ${r} ${e.minimum.toString()} ${n.unit}`:`តូចពេក៖ ត្រូវការ ${e.origin} ${r} ${e.minimum.toString()}`}case\"invalid_format\":{const t=e;return\"starts_with\"===t.format?`ខ្សែអក្សរមិនត្រឹមត្រូវ៖ ត្រូវចាប់ផ្តើមដោយ \"${t.prefix}\"`:\"ends_with\"===t.format?`ខ្សែអក្សរមិនត្រឹមត្រូវ៖ ត្រូវបញ្ចប់ដោយ \"${t.suffix}\"`:\"includes\"===t.format?`ខ្សែអក្សរមិនត្រឹមត្រូវ៖ ត្រូវមាន \"${t.includes}\"`:\"regex\"===t.format?`ខ្សែអក្សរមិនត្រឹមត្រូវ៖ ត្រូវតែផ្គូផ្គងនឹងទម្រង់ដែលបានកំណត់ ${t.pattern}`:`មិនត្រឹមត្រូវ៖ ${r[t.format]??e.format}`}case\"not_multiple_of\":return`លេខមិនត្រឹមត្រូវ៖ ត្រូវតែជាពហុគុណនៃ ${e.divisor}`;case\"unrecognized_keys\":return`រកឃើញសោមិនស្គាល់៖ ${l(e.keys,\", \")}`;case\"invalid_key\":return`សោមិនត្រឹមត្រូវនៅក្នុង ${e.origin}`;case\"invalid_union\":default:return\"ទិន្នន័យមិនត្រឹមត្រូវ\";case\"invalid_element\":return`ទិន្នន័យមិនត្រឹមត្រូវនៅក្នុង ${e.origin}`}}};function sa(){return{localeError:ia()}}const la=()=>{const e={string:{unit:\"문자\",verb:\"to have\"},file:{unit:\"바이트\",verb:\"to have\"},array:{unit:\"개\",verb:\"to have\"},set:{unit:\"개\",verb:\"to have\"}};function t(t){return e[t]??null}const r={regex:\"입력\",email:\"이메일 주소\",url:\"URL\",emoji:\"이모지\",uuid:\"UUID\",uuidv4:\"UUIDv4\",uuidv6:\"UUIDv6\",nanoid:\"nanoid\",guid:\"GUID\",cuid:\"cuid\",cuid2:\"cuid2\",ulid:\"ULID\",xid:\"XID\",ksuid:\"KSUID\",datetime:\"ISO 날짜시간\",date:\"ISO 날짜\",time:\"ISO 시간\",duration:\"ISO 기간\",ipv4:\"IPv4 주소\",ipv6:\"IPv6 주소\",cidrv4:\"IPv4 범위\",cidrv6:\"IPv6 범위\",base64:\"base64 인코딩 문자열\",base64url:\"base64url 인코딩 문자열\",json_string:\"JSON 문자열\",e164:\"E.164 번호\",jwt:\"JWT\",template_literal:\"입력\"};return e=>{switch(e.code){case\"invalid_type\":return`잘못된 입력: 예상 타입은 ${e.expected}, 받은 타입은 ${(e=>{const t=typeof e;switch(t){case\"number\":return Number.isNaN(e)?\"NaN\":\"number\";case\"object\":if(Array.isArray(e))return\"array\";if(null===e)return\"null\";if(Object.getPrototypeOf(e)!==Object.prototype&&e.constructor)return e.constructor.name}return t})(e.input)}입니다`;case\"invalid_value\":return 1===e.values.length?`잘못된 입력: 값은 ${$(e.values[0])} 이어야 합니다`:`잘못된 옵션: ${l(e.values,\"또는 \")} 중 하나여야 합니다`;case\"too_big\":{const r=e.inclusive?\"이하\":\"미만\",n=\"미만\"===r?\"이어야 합니다\":\"여야 합니다\",a=t(e.origin),o=a?.unit??\"요소\";return a?`${e.origin??\"값\"}이 너무 큽니다: ${e.maximum.toString()}${o} ${r}${n}`:`${e.origin??\"값\"}이 너무 큽니다: ${e.maximum.toString()} ${r}${n}`}case\"too_small\":{const r=e.inclusive?\"이상\":\"초과\",n=\"이상\"===r?\"이어야 합니다\":\"여야 합니다\",a=t(e.origin),o=a?.unit??\"요소\";return a?`${e.origin??\"값\"}이 너무 작습니다: ${e.minimum.toString()}${o} ${r}${n}`:`${e.origin??\"값\"}이 너무 작습니다: ${e.minimum.toString()} ${r}${n}`}case\"invalid_format\":{const t=e;return\"starts_with\"===t.format?`잘못된 문자열: \"${t.prefix}\"(으)로 시작해야 합니다`:\"ends_with\"===t.format?`잘못된 문자열: \"${t.suffix}\"(으)로 끝나야 합니다`:\"includes\"===t.format?`잘못된 문자열: \"${t.includes}\"을(를) 포함해야 합니다`:\"regex\"===t.format?`잘못된 문자열: 정규식 ${t.pattern} 패턴과 일치해야 합니다`:`잘못된 ${r[t.format]??e.format}`}case\"not_multiple_of\":return`잘못된 숫자: ${e.divisor}의 배수여야 합니다`;case\"unrecognized_keys\":return`인식할 수 없는 키: ${l(e.keys,\", \")}`;case\"invalid_key\":return`잘못된 키: ${e.origin}`;case\"invalid_union\":default:return\"잘못된 입력\";case\"invalid_element\":return`잘못된 값: ${e.origin}`}}},ca=(e,t=void 0)=>{switch(e){case\"number\":return Number.isNaN(t)?\"NaN\":\"skaičius\";case\"bigint\":return\"sveikasis skaičius\";case\"string\":return\"eilutė\";case\"boolean\":return\"loginė reikšmė\";case\"undefined\":case\"void\":return\"neapibrėžta reikšmė\";case\"function\":return\"funkcija\";case\"symbol\":return\"simbolis\";case\"object\":return void 0===t?\"nežinomas objektas\":null===t?\"nulinė reikšmė\":Array.isArray(t)?\"masyvas\":Object.getPrototypeOf(t)!==Object.prototype&&t.constructor?t.constructor.name:\"objektas\";case\"null\":return\"nulinė reikšmė\"}return e},ua=e=>e.charAt(0).toUpperCase()+e.slice(1);function da(e){const t=Math.abs(e),r=t%10,n=t%100;return n>=11&&n<=19||0===r?\"many\":1===r?\"one\":\"few\"}const pa=()=>{const e={string:{unit:{one:\"simbolis\",few:\"simboliai\",many:\"simbolių\"},verb:{smaller:{inclusive:\"turi būti ne ilgesnė kaip\",notInclusive:\"turi būti trumpesnė kaip\"},bigger:{inclusive:\"turi būti ne trumpesnė kaip\",notInclusive:\"turi būti ilgesnė kaip\"}}},file:{unit:{one:\"baitas\",few:\"baitai\",many:\"baitų\"},verb:{smaller:{inclusive:\"turi būti ne didesnis kaip\",notInclusive:\"turi būti mažesnis kaip\"},bigger:{inclusive:\"turi būti ne mažesnis kaip\",notInclusive:\"turi būti didesnis kaip\"}}},array:{unit:{one:\"elementą\",few:\"elementus\",many:\"elementų\"},verb:{smaller:{inclusive:\"turi turėti ne daugiau kaip\",notInclusive:\"turi turėti mažiau kaip\"},bigger:{inclusive:\"turi turėti ne mažiau kaip\",notInclusive:\"turi turėti daugiau kaip\"}}},set:{unit:{one:\"elementą\",few:\"elementus\",many:\"elementų\"},verb:{smaller:{inclusive:\"turi turėti ne daugiau kaip\",notInclusive:\"turi turėti mažiau kaip\"},bigger:{inclusive:\"turi turėti ne mažiau kaip\",notInclusive:\"turi turėti daugiau kaip\"}}}};function t(t,r,n,a){const o=e[t]??null;return null===o?o:{unit:o.unit[r],verb:o.verb[a][n?\"inclusive\":\"notInclusive\"]}}const r={regex:\"įvestis\",email:\"el. pašto adresas\",url:\"URL\",emoji:\"jaustukas\",uuid:\"UUID\",uuidv4:\"UUIDv4\",uuidv6:\"UUIDv6\",nanoid:\"nanoid\",guid:\"GUID\",cuid:\"cuid\",cuid2:\"cuid2\",ulid:\"ULID\",xid:\"XID\",ksuid:\"KSUID\",datetime:\"ISO data ir laikas\",date:\"ISO data\",time:\"ISO laikas\",duration:\"ISO trukmė\",ipv4:\"IPv4 adresas\",ipv6:\"IPv6 adresas\",cidrv4:\"IPv4 tinklo prefiksas (CIDR)\",cidrv6:\"IPv6 tinklo prefiksas (CIDR)\",base64:\"base64 užkoduota eilutė\",base64url:\"base64url užkoduota eilutė\",json_string:\"JSON eilutė\",e164:\"E.164 numeris\",jwt:\"JWT\",template_literal:\"įvestis\"};return e=>{switch(e.code){case\"invalid_type\":return`Gautas tipas ${n=e.input,ca(typeof n,n)}, o tikėtasi - ${ca(e.expected)}`;case\"invalid_value\":return 1===e.values.length?`Privalo būti ${$(e.values[0])}`:`Privalo būti vienas iš ${l(e.values,\"|\")} pasirinkimų`;case\"too_big\":{const r=ca(e.origin),n=t(e.origin,da(Number(e.maximum)),e.inclusive??!1,\"smaller\");if(n?.verb)return`${ua(r??e.origin??\"reikšmė\")} ${n.verb} ${e.maximum.toString()} ${n.unit??\"elementų\"}`;const a=e.inclusive?\"ne didesnis kaip\":\"mažesnis kaip\";return`${ua(r??e.origin??\"reikšmė\")} turi būti ${a} ${e.maximum.toString()} ${n?.unit}`}case\"too_small\":{const r=ca(e.origin),n=t(e.origin,da(Number(e.minimum)),e.inclusive??!1,\"bigger\");if(n?.verb)return`${ua(r??e.origin??\"reikšmė\")} ${n.verb} ${e.minimum.toString()} ${n.unit??\"elementų\"}`;const a=e.inclusive?\"ne mažesnis kaip\":\"didesnis kaip\";return`${ua(r??e.origin??\"reikšmė\")} turi būti ${a} ${e.minimum.toString()} ${n?.unit}`}case\"invalid_format\":{const t=e;return\"starts_with\"===t.format?`Eilutė privalo prasidėti \"${t.prefix}\"`:\"ends_with\"===t.format?`Eilutė privalo pasibaigti \"${t.suffix}\"`:\"includes\"===t.format?`Eilutė privalo įtraukti \"${t.includes}\"`:\"regex\"===t.format?`Eilutė privalo atitikti ${t.pattern}`:`Neteisingas ${r[t.format]??e.format}`}case\"not_multiple_of\":return`Skaičius privalo būti ${e.divisor} kartotinis.`;case\"unrecognized_keys\":return`Neatpažint${e.keys.length>1?\"i\":\"as\"} rakt${e.keys.length>1?\"ai\":\"as\"}: ${l(e.keys,\", \")}`;case\"invalid_key\":return\"Rastas klaidingas raktas\";case\"invalid_union\":default:return\"Klaidinga įvestis\";case\"invalid_element\":{const t=ca(e.origin);return`${ua(t??e.origin??\"reikšmė\")} turi klaidingą įvestį`}}var n}},ha=()=>{const e={string:{unit:\"знаци\",verb:\"да имаат\"},file:{unit:\"бајти\",verb:\"да имаат\"},array:{unit:\"ставки\",verb:\"да имаат\"},set:{unit:\"ставки\",verb:\"да имаат\"}};function t(t){return e[t]??null}const r={regex:\"внес\",email:\"адреса на е-пошта\",url:\"URL\",emoji:\"емоџи\",uuid:\"UUID\",uuidv4:\"UUIDv4\",uuidv6:\"UUIDv6\",nanoid:\"nanoid\",guid:\"GUID\",cuid:\"cuid\",cuid2:\"cuid2\",ulid:\"ULID\",xid:\"XID\",ksuid:\"KSUID\",datetime:\"ISO датум и време\",date:\"ISO датум\",time:\"ISO време\",duration:\"ISO времетраење\",ipv4:\"IPv4 адреса\",ipv6:\"IPv6 адреса\",cidrv4:\"IPv4 опсег\",cidrv6:\"IPv6 опсег\",base64:\"base64-енкодирана низа\",base64url:\"base64url-енкодирана низа\",json_string:\"JSON низа\",e164:\"E.164 број\",jwt:\"JWT\",template_literal:\"внес\"};return e=>{switch(e.code){case\"invalid_type\":return`Грешен внес: се очекува ${e.expected}, примено ${(e=>{const t=typeof e;switch(t){case\"number\":return Number.isNaN(e)?\"NaN\":\"број\";case\"object\":if(Array.isArray(e))return\"низа\";if(null===e)return\"null\";if(Object.getPrototypeOf(e)!==Object.prototype&&e.constructor)return e.constructor.name}return t})(e.input)}`;case\"invalid_value\":return 1===e.values.length?`Invalid input: expected ${$(e.values[0])}`:`Грешана опција: се очекува една ${l(e.values,\"|\")}`;case\"too_big\":{const r=e.inclusive?\"<=\":\"<\",n=t(e.origin);return n?`Премногу голем: се очекува ${e.origin??\"вредноста\"} да има ${r}${e.maximum.toString()} ${n.unit??\"елементи\"}`:`Премногу голем: се очекува ${e.origin??\"вредноста\"} да биде ${r}${e.maximum.toString()}`}case\"too_small\":{const r=e.inclusive?\">=\":\">\",n=t(e.origin);return n?`Премногу мал: се очекува ${e.origin} да има ${r}${e.minimum.toString()} ${n.unit}`:`Премногу мал: се очекува ${e.origin} да биде ${r}${e.minimum.toString()}`}case\"invalid_format\":{const t=e;return\"starts_with\"===t.format?`Неважечка низа: мора да започнува со \"${t.prefix}\"`:\"ends_with\"===t.format?`Неважечка низа: мора да завршува со \"${t.suffix}\"`:\"includes\"===t.format?`Неважечка низа: мора да вклучува \"${t.includes}\"`:\"regex\"===t.format?`Неважечка низа: мора да одгоара на патернот ${t.pattern}`:`Invalid ${r[t.format]??e.format}`}case\"not_multiple_of\":return`Грешен број: мора да биде делив со ${e.divisor}`;case\"unrecognized_keys\":return`${e.keys.length>1?\"Непрепознаени клучеви\":\"Непрепознаен клуч\"}: ${l(e.keys,\", \")}`;case\"invalid_key\":return`Грешен клуч во ${e.origin}`;case\"invalid_union\":default:return\"Грешен внес\";case\"invalid_element\":return`Грешна вредност во ${e.origin}`}}},fa=()=>{const e={string:{unit:\"aksara\",verb:\"mempunyai\"},file:{unit:\"bait\",verb:\"mempunyai\"},array:{unit:\"elemen\",verb:\"mempunyai\"},set:{unit:\"elemen\",verb:\"mempunyai\"}};function t(t){return e[t]??null}const r={regex:\"input\",email:\"alamat e-mel\",url:\"URL\",emoji:\"emoji\",uuid:\"UUID\",uuidv4:\"UUIDv4\",uuidv6:\"UUIDv6\",nanoid:\"nanoid\",guid:\"GUID\",cuid:\"cuid\",cuid2:\"cuid2\",ulid:\"ULID\",xid:\"XID\",ksuid:\"KSUID\",datetime:\"tarikh masa ISO\",date:\"tarikh ISO\",time:\"masa ISO\",duration:\"tempoh ISO\",ipv4:\"alamat IPv4\",ipv6:\"alamat IPv6\",cidrv4:\"julat IPv4\",cidrv6:\"julat IPv6\",base64:\"string dikodkan base64\",base64url:\"string dikodkan base64url\",json_string:\"string JSON\",e164:\"nombor E.164\",jwt:\"JWT\",template_literal:\"input\"};return e=>{switch(e.code){case\"invalid_type\":return`Input tidak sah: dijangka ${e.expected}, diterima ${(e=>{const t=typeof e;switch(t){case\"number\":return Number.isNaN(e)?\"NaN\":\"nombor\";case\"object\":if(Array.isArray(e))return\"array\";if(null===e)return\"null\";if(Object.getPrototypeOf(e)!==Object.prototype&&e.constructor)return e.constructor.name}return t})(e.input)}`;case\"invalid_value\":return 1===e.values.length?`Input tidak sah: dijangka ${$(e.values[0])}`:`Pilihan tidak sah: dijangka salah satu daripada ${l(e.values,\"|\")}`;case\"too_big\":{const r=e.inclusive?\"<=\":\"<\",n=t(e.origin);return n?`Terlalu besar: dijangka ${e.origin??\"nilai\"} ${n.verb} ${r}${e.maximum.toString()} ${n.unit??\"elemen\"}`:`Terlalu besar: dijangka ${e.origin??\"nilai\"} adalah ${r}${e.maximum.toString()}`}case\"too_small\":{const r=e.inclusive?\">=\":\">\",n=t(e.origin);return n?`Terlalu kecil: dijangka ${e.origin} ${n.verb} ${r}${e.minimum.toString()} ${n.unit}`:`Terlalu kecil: dijangka ${e.origin} adalah ${r}${e.minimum.toString()}`}case\"invalid_format\":{const t=e;return\"starts_with\"===t.format?`String tidak sah: mesti bermula dengan \"${t.prefix}\"`:\"ends_with\"===t.format?`String tidak sah: mesti berakhir dengan \"${t.suffix}\"`:\"includes\"===t.format?`String tidak sah: mesti mengandungi \"${t.includes}\"`:\"regex\"===t.format?`String tidak sah: mesti sepadan dengan corak ${t.pattern}`:`${r[t.format]??e.format} tidak sah`}case\"not_multiple_of\":return`Nombor tidak sah: perlu gandaan ${e.divisor}`;case\"unrecognized_keys\":return`Kunci tidak dikenali: ${l(e.keys,\", \")}`;case\"invalid_key\":return`Kunci tidak sah dalam ${e.origin}`;case\"invalid_union\":default:return\"Input tidak sah\";case\"invalid_element\":return`Nilai tidak sah dalam ${e.origin}`}}},ma=()=>{const e={string:{unit:\"tekens\"},file:{unit:\"bytes\"},array:{unit:\"elementen\"},set:{unit:\"elementen\"}};function t(t){return e[t]??null}const r={regex:\"invoer\",email:\"emailadres\",url:\"URL\",emoji:\"emoji\",uuid:\"UUID\",uuidv4:\"UUIDv4\",uuidv6:\"UUIDv6\",nanoid:\"nanoid\",guid:\"GUID\",cuid:\"cuid\",cuid2:\"cuid2\",ulid:\"ULID\",xid:\"XID\",ksuid:\"KSUID\",datetime:\"ISO datum en tijd\",date:\"ISO datum\",time:\"ISO tijd\",duration:\"ISO duur\",ipv4:\"IPv4-adres\",ipv6:\"IPv6-adres\",cidrv4:\"IPv4-bereik\",cidrv6:\"IPv6-bereik\",base64:\"base64-gecodeerde tekst\",base64url:\"base64 URL-gecodeerde tekst\",json_string:\"JSON string\",e164:\"E.164-nummer\",jwt:\"JWT\",template_literal:\"invoer\"};return e=>{switch(e.code){case\"invalid_type\":return`Ongeldige invoer: verwacht ${e.expected}, ontving ${(e=>{const t=typeof e;switch(t){case\"number\":return Number.isNaN(e)?\"NaN\":\"getal\";case\"object\":if(Array.isArray(e))return\"array\";if(null===e)return\"null\";if(Object.getPrototypeOf(e)!==Object.prototype&&e.constructor)return e.constructor.name}return t})(e.input)}`;case\"invalid_value\":return 1===e.values.length?`Ongeldige invoer: verwacht ${$(e.values[0])}`:`Ongeldige optie: verwacht één van ${l(e.values,\"|\")}`;case\"too_big\":{const r=e.inclusive?\"<=\":\"<\",n=t(e.origin);return n?`Te lang: verwacht dat ${e.origin??\"waarde\"} ${r}${e.maximum.toString()} ${n.unit??\"elementen\"} bevat`:`Te lang: verwacht dat ${e.origin??\"waarde\"} ${r}${e.maximum.toString()} is`}case\"too_small\":{const r=e.inclusive?\">=\":\">\",n=t(e.origin);return n?`Te kort: verwacht dat ${e.origin} ${r}${e.minimum.toString()} ${n.unit} bevat`:`Te kort: verwacht dat ${e.origin} ${r}${e.minimum.toString()} is`}case\"invalid_format\":{const t=e;return\"starts_with\"===t.format?`Ongeldige tekst: moet met \"${t.prefix}\" beginnen`:\"ends_with\"===t.format?`Ongeldige tekst: moet op \"${t.suffix}\" eindigen`:\"includes\"===t.format?`Ongeldige tekst: moet \"${t.includes}\" bevatten`:\"regex\"===t.format?`Ongeldige tekst: moet overeenkomen met patroon ${t.pattern}`:`Ongeldig: ${r[t.format]??e.format}`}case\"not_multiple_of\":return`Ongeldig getal: moet een veelvoud van ${e.divisor} zijn`;case\"unrecognized_keys\":return`Onbekende key${e.keys.length>1?\"s\":\"\"}: ${l(e.keys,\", \")}`;case\"invalid_key\":return`Ongeldige key in ${e.origin}`;case\"invalid_union\":default:return\"Ongeldige invoer\";case\"invalid_element\":return`Ongeldige waarde in ${e.origin}`}}},ga=()=>{const e={string:{unit:\"tegn\",verb:\"å ha\"},file:{unit:\"bytes\",verb:\"å ha\"},array:{unit:\"elementer\",verb:\"å inneholde\"},set:{unit:\"elementer\",verb:\"å inneholde\"}};function t(t){return e[t]??null}const r={regex:\"input\",email:\"e-postadresse\",url:\"URL\",emoji:\"emoji\",uuid:\"UUID\",uuidv4:\"UUIDv4\",uuidv6:\"UUIDv6\",nanoid:\"nanoid\",guid:\"GUID\",cuid:\"cuid\",cuid2:\"cuid2\",ulid:\"ULID\",xid:\"XID\",ksuid:\"KSUID\",datetime:\"ISO dato- og klokkeslett\",date:\"ISO-dato\",time:\"ISO-klokkeslett\",duration:\"ISO-varighet\",ipv4:\"IPv4-område\",ipv6:\"IPv6-område\",cidrv4:\"IPv4-spekter\",cidrv6:\"IPv6-spekter\",base64:\"base64-enkodet streng\",base64url:\"base64url-enkodet streng\",json_string:\"JSON-streng\",e164:\"E.164-nummer\",jwt:\"JWT\",template_literal:\"input\"};return e=>{switch(e.code){case\"invalid_type\":return`Ugyldig input: forventet ${e.expected}, fikk ${(e=>{const t=typeof e;switch(t){case\"number\":return Number.isNaN(e)?\"NaN\":\"tall\";case\"object\":if(Array.isArray(e))return\"liste\";if(null===e)return\"null\";if(Object.getPrototypeOf(e)!==Object.prototype&&e.constructor)return e.constructor.name}return t})(e.input)}`;case\"invalid_value\":return 1===e.values.length?`Ugyldig verdi: forventet ${$(e.values[0])}`:`Ugyldig valg: forventet en av ${l(e.values,\"|\")}`;case\"too_big\":{const r=e.inclusive?\"<=\":\"<\",n=t(e.origin);return n?`For stor(t): forventet ${e.origin??\"value\"} til å ha ${r}${e.maximum.toString()} ${n.unit??\"elementer\"}`:`For stor(t): forventet ${e.origin??\"value\"} til å ha ${r}${e.maximum.toString()}`}case\"too_small\":{const r=e.inclusive?\">=\":\">\",n=t(e.origin);return n?`For lite(n): forventet ${e.origin} til å ha ${r}${e.minimum.toString()} ${n.unit}`:`For lite(n): forventet ${e.origin} til å ha ${r}${e.minimum.toString()}`}case\"invalid_format\":{const t=e;return\"starts_with\"===t.format?`Ugyldig streng: må starte med \"${t.prefix}\"`:\"ends_with\"===t.format?`Ugyldig streng: må ende med \"${t.suffix}\"`:\"includes\"===t.format?`Ugyldig streng: må inneholde \"${t.includes}\"`:\"regex\"===t.format?`Ugyldig streng: må matche mønsteret ${t.pattern}`:`Ugyldig ${r[t.format]??e.format}`}case\"not_multiple_of\":return`Ugyldig tall: må være et multiplum av ${e.divisor}`;case\"unrecognized_keys\":return`${e.keys.length>1?\"Ukjente nøkler\":\"Ukjent nøkkel\"}: ${l(e.keys,\", \")}`;case\"invalid_key\":return`Ugyldig nøkkel i ${e.origin}`;case\"invalid_union\":default:return\"Ugyldig input\";case\"invalid_element\":return`Ugyldig verdi i ${e.origin}`}}},va=()=>{const e={string:{unit:\"harf\",verb:\"olmalıdır\"},file:{unit:\"bayt\",verb:\"olmalıdır\"},array:{unit:\"unsur\",verb:\"olmalıdır\"},set:{unit:\"unsur\",verb:\"olmalıdır\"}};function t(t){return e[t]??null}const r={regex:\"giren\",email:\"epostagâh\",url:\"URL\",emoji:\"emoji\",uuid:\"UUID\",uuidv4:\"UUIDv4\",uuidv6:\"UUIDv6\",nanoid:\"nanoid\",guid:\"GUID\",cuid:\"cuid\",cuid2:\"cuid2\",ulid:\"ULID\",xid:\"XID\",ksuid:\"KSUID\",datetime:\"ISO hengâmı\",date:\"ISO tarihi\",time:\"ISO zamanı\",duration:\"ISO müddeti\",ipv4:\"IPv4 nişânı\",ipv6:\"IPv6 nişânı\",cidrv4:\"IPv4 menzili\",cidrv6:\"IPv6 menzili\",base64:\"base64-şifreli metin\",base64url:\"base64url-şifreli metin\",json_string:\"JSON metin\",e164:\"E.164 sayısı\",jwt:\"JWT\",template_literal:\"giren\"};return e=>{switch(e.code){case\"invalid_type\":return`Fâsit giren: umulan ${e.expected}, alınan ${(e=>{const t=typeof e;switch(t){case\"number\":return Number.isNaN(e)?\"NaN\":\"numara\";case\"object\":if(Array.isArray(e))return\"saf\";if(null===e)return\"gayb\";if(Object.getPrototypeOf(e)!==Object.prototype&&e.constructor)return e.constructor.name}return t})(e.input)}`;case\"invalid_value\":return 1===e.values.length?`Fâsit giren: umulan ${$(e.values[0])}`:`Fâsit tercih: mûteberler ${l(e.values,\"|\")}`;case\"too_big\":{const r=e.inclusive?\"<=\":\"<\",n=t(e.origin);return n?`Fazla büyük: ${e.origin??\"value\"}, ${r}${e.maximum.toString()} ${n.unit??\"elements\"} sahip olmalıydı.`:`Fazla büyük: ${e.origin??\"value\"}, ${r}${e.maximum.toString()} olmalıydı.`}case\"too_small\":{const r=e.inclusive?\">=\":\">\",n=t(e.origin);return n?`Fazla küçük: ${e.origin}, ${r}${e.minimum.toString()} ${n.unit} sahip olmalıydı.`:`Fazla küçük: ${e.origin}, ${r}${e.minimum.toString()} olmalıydı.`}case\"invalid_format\":{const t=e;return\"starts_with\"===t.format?`Fâsit metin: \"${t.prefix}\" ile başlamalı.`:\"ends_with\"===t.format?`Fâsit metin: \"${t.suffix}\" ile bitmeli.`:\"includes\"===t.format?`Fâsit metin: \"${t.includes}\" ihtivâ etmeli.`:\"regex\"===t.format?`Fâsit metin: ${t.pattern} nakşına uymalı.`:`Fâsit ${r[t.format]??e.format}`}case\"not_multiple_of\":return`Fâsit sayı: ${e.divisor} katı olmalıydı.`;case\"unrecognized_keys\":return`Tanınmayan anahtar ${e.keys.length>1?\"s\":\"\"}: ${l(e.keys,\", \")}`;case\"invalid_key\":return`${e.origin} için tanınmayan anahtar var.`;case\"invalid_union\":return\"Giren tanınamadı.\";case\"invalid_element\":return`${e.origin} için tanınmayan kıymet var.`;default:return\"Kıymet tanınamadı.\"}}},ba=()=>{const e={string:{unit:\"توکي\",verb:\"ولري\"},file:{unit:\"بایټس\",verb:\"ولري\"},array:{unit:\"توکي\",verb:\"ولري\"},set:{unit:\"توکي\",verb:\"ولري\"}};function t(t){return e[t]??null}const r={regex:\"ورودي\",email:\"بریښنالیک\",url:\"یو آر ال\",emoji:\"ایموجي\",uuid:\"UUID\",uuidv4:\"UUIDv4\",uuidv6:\"UUIDv6\",nanoid:\"nanoid\",guid:\"GUID\",cuid:\"cuid\",cuid2:\"cuid2\",ulid:\"ULID\",xid:\"XID\",ksuid:\"KSUID\",datetime:\"نیټه او وخت\",date:\"نېټه\",time:\"وخت\",duration:\"موده\",ipv4:\"د IPv4 پته\",ipv6:\"د IPv6 پته\",cidrv4:\"د IPv4 ساحه\",cidrv6:\"د IPv6 ساحه\",base64:\"base64-encoded متن\",base64url:\"base64url-encoded متن\",json_string:\"JSON متن\",e164:\"د E.164 شمېره\",jwt:\"JWT\",template_literal:\"ورودي\"};return e=>{switch(e.code){case\"invalid_type\":return`ناسم ورودي: باید ${e.expected} وای, مګر ${(e=>{const t=typeof e;switch(t){case\"number\":return Number.isNaN(e)?\"NaN\":\"عدد\";case\"object\":if(Array.isArray(e))return\"ارې\";if(null===e)return\"null\";if(Object.getPrototypeOf(e)!==Object.prototype&&e.constructor)return e.constructor.name}return t})(e.input)} ترلاسه شو`;case\"invalid_value\":return 1===e.values.length?`ناسم ورودي: باید ${$(e.values[0])} وای`:`ناسم انتخاب: باید یو له ${l(e.values,\"|\")} څخه وای`;case\"too_big\":{const r=e.inclusive?\"<=\":\"<\",n=t(e.origin);return n?`ډیر لوی: ${e.origin??\"ارزښت\"} باید ${r}${e.maximum.toString()} ${n.unit??\"عنصرونه\"} ولري`:`ډیر لوی: ${e.origin??\"ارزښت\"} باید ${r}${e.maximum.toString()} وي`}case\"too_small\":{const r=e.inclusive?\">=\":\">\",n=t(e.origin);return n?`ډیر کوچنی: ${e.origin} باید ${r}${e.minimum.toString()} ${n.unit} ولري`:`ډیر کوچنی: ${e.origin} باید ${r}${e.minimum.toString()} وي`}case\"invalid_format\":{const t=e;return\"starts_with\"===t.format?`ناسم متن: باید د \"${t.prefix}\" سره پیل شي`:\"ends_with\"===t.format?`ناسم متن: باید د \"${t.suffix}\" سره پای ته ورسيږي`:\"includes\"===t.format?`ناسم متن: باید \"${t.includes}\" ولري`:\"regex\"===t.format?`ناسم متن: باید د ${t.pattern} سره مطابقت ولري`:`${r[t.format]??e.format} ناسم دی`}case\"not_multiple_of\":return`ناسم عدد: باید د ${e.divisor} مضرب وي`;case\"unrecognized_keys\":return`ناسم ${e.keys.length>1?\"کلیډونه\":\"کلیډ\"}: ${l(e.keys,\", \")}`;case\"invalid_key\":return`ناسم کلیډ په ${e.origin} کې`;case\"invalid_union\":default:return\"ناسمه ورودي\";case\"invalid_element\":return`ناسم عنصر په ${e.origin} کې`}}},ya=()=>{const e={string:{unit:\"znaków\",verb:\"mieć\"},file:{unit:\"bajtów\",verb:\"mieć\"},array:{unit:\"elementów\",verb:\"mieć\"},set:{unit:\"elementów\",verb:\"mieć\"}};function t(t){return e[t]??null}const r={regex:\"wyrażenie\",email:\"adres email\",url:\"URL\",emoji:\"emoji\",uuid:\"UUID\",uuidv4:\"UUIDv4\",uuidv6:\"UUIDv6\",nanoid:\"nanoid\",guid:\"GUID\",cuid:\"cuid\",cuid2:\"cuid2\",ulid:\"ULID\",xid:\"XID\",ksuid:\"KSUID\",datetime:\"data i godzina w formacie ISO\",date:\"data w formacie ISO\",time:\"godzina w formacie ISO\",duration:\"czas trwania ISO\",ipv4:\"adres IPv4\",ipv6:\"adres IPv6\",cidrv4:\"zakres IPv4\",cidrv6:\"zakres IPv6\",base64:\"ciąg znaków zakodowany w formacie base64\",base64url:\"ciąg znaków zakodowany w formacie base64url\",json_string:\"ciąg znaków w formacie JSON\",e164:\"liczba E.164\",jwt:\"JWT\",template_literal:\"wejście\"};return e=>{switch(e.code){case\"invalid_type\":return`Nieprawidłowe dane wejściowe: oczekiwano ${e.expected}, otrzymano ${(e=>{const t=typeof e;switch(t){case\"number\":return Number.isNaN(e)?\"NaN\":\"liczba\";case\"object\":if(Array.isArray(e))return\"tablica\";if(null===e)return\"null\";if(Object.getPrototypeOf(e)!==Object.prototype&&e.constructor)return e.constructor.name}return t})(e.input)}`;case\"invalid_value\":return 1===e.values.length?`Nieprawidłowe dane wejściowe: oczekiwano ${$(e.values[0])}`:`Nieprawidłowa opcja: oczekiwano jednej z wartości ${l(e.values,\"|\")}`;case\"too_big\":{const r=e.inclusive?\"<=\":\"<\",n=t(e.origin);return n?`Za duża wartość: oczekiwano, że ${e.origin??\"wartość\"} będzie mieć ${r}${e.maximum.toString()} ${n.unit??\"elementów\"}`:`Zbyt duż(y/a/e): oczekiwano, że ${e.origin??\"wartość\"} będzie wynosić ${r}${e.maximum.toString()}`}case\"too_small\":{const r=e.inclusive?\">=\":\">\",n=t(e.origin);return n?`Za mała wartość: oczekiwano, że ${e.origin??\"wartość\"} będzie mieć ${r}${e.minimum.toString()} ${n.unit??\"elementów\"}`:`Zbyt mał(y/a/e): oczekiwano, że ${e.origin??\"wartość\"} będzie wynosić ${r}${e.minimum.toString()}`}case\"invalid_format\":{const t=e;return\"starts_with\"===t.format?`Nieprawidłowy ciąg znaków: musi zaczynać się od \"${t.prefix}\"`:\"ends_with\"===t.format?`Nieprawidłowy ciąg znaków: musi kończyć się na \"${t.suffix}\"`:\"includes\"===t.format?`Nieprawidłowy ciąg znaków: musi zawierać \"${t.includes}\"`:\"regex\"===t.format?`Nieprawidłowy ciąg znaków: musi odpowiadać wzorcowi ${t.pattern}`:`Nieprawidłow(y/a/e) ${r[t.format]??e.format}`}case\"not_multiple_of\":return`Nieprawidłowa liczba: musi być wielokrotnością ${e.divisor}`;case\"unrecognized_keys\":return`Nierozpoznane klucze${e.keys.length>1?\"s\":\"\"}: ${l(e.keys,\", \")}`;case\"invalid_key\":return`Nieprawidłowy klucz w ${e.origin}`;case\"invalid_union\":default:return\"Nieprawidłowe dane wejściowe\";case\"invalid_element\":return`Nieprawidłowa wartość w ${e.origin}`}}},Oa=()=>{const e={string:{unit:\"caracteres\",verb:\"ter\"},file:{unit:\"bytes\",verb:\"ter\"},array:{unit:\"itens\",verb:\"ter\"},set:{unit:\"itens\",verb:\"ter\"}};function t(t){return e[t]??null}const r={regex:\"padrão\",email:\"endereço de e-mail\",url:\"URL\",emoji:\"emoji\",uuid:\"UUID\",uuidv4:\"UUIDv4\",uuidv6:\"UUIDv6\",nanoid:\"nanoid\",guid:\"GUID\",cuid:\"cuid\",cuid2:\"cuid2\",ulid:\"ULID\",xid:\"XID\",ksuid:\"KSUID\",datetime:\"data e hora ISO\",date:\"data ISO\",time:\"hora ISO\",duration:\"duração ISO\",ipv4:\"endereço IPv4\",ipv6:\"endereço IPv6\",cidrv4:\"faixa de IPv4\",cidrv6:\"faixa de IPv6\",base64:\"texto codificado em base64\",base64url:\"URL codificada em base64\",json_string:\"texto JSON\",e164:\"número E.164\",jwt:\"JWT\",template_literal:\"entrada\"};return e=>{switch(e.code){case\"invalid_type\":return`Tipo inválido: esperado ${e.expected}, recebido ${(e=>{const t=typeof e;switch(t){case\"number\":return Number.isNaN(e)?\"NaN\":\"número\";case\"object\":if(Array.isArray(e))return\"array\";if(null===e)return\"nulo\";if(Object.getPrototypeOf(e)!==Object.prototype&&e.constructor)return e.constructor.name}return t})(e.input)}`;case\"invalid_value\":return 1===e.values.length?`Entrada inválida: esperado ${$(e.values[0])}`:`Opção inválida: esperada uma das ${l(e.values,\"|\")}`;case\"too_big\":{const r=e.inclusive?\"<=\":\"<\",n=t(e.origin);return n?`Muito grande: esperado que ${e.origin??\"valor\"} tivesse ${r}${e.maximum.toString()} ${n.unit??\"elementos\"}`:`Muito grande: esperado que ${e.origin??\"valor\"} fosse ${r}${e.maximum.toString()}`}case\"too_small\":{const r=e.inclusive?\">=\":\">\",n=t(e.origin);return n?`Muito pequeno: esperado que ${e.origin} tivesse ${r}${e.minimum.toString()} ${n.unit}`:`Muito pequeno: esperado que ${e.origin} fosse ${r}${e.minimum.toString()}`}case\"invalid_format\":{const t=e;return\"starts_with\"===t.format?`Texto inválido: deve começar com \"${t.prefix}\"`:\"ends_with\"===t.format?`Texto inválido: deve terminar com \"${t.suffix}\"`:\"includes\"===t.format?`Texto inválido: deve incluir \"${t.includes}\"`:\"regex\"===t.format?`Texto inválido: deve corresponder ao padrão ${t.pattern}`:`${r[t.format]??e.format} inválido`}case\"not_multiple_of\":return`Número inválido: deve ser múltiplo de ${e.divisor}`;case\"unrecognized_keys\":return`Chave${e.keys.length>1?\"s\":\"\"} desconhecida${e.keys.length>1?\"s\":\"\"}: ${l(e.keys,\", \")}`;case\"invalid_key\":return`Chave inválida em ${e.origin}`;case\"invalid_union\":return\"Entrada inválida\";case\"invalid_element\":return`Valor inválido em ${e.origin}`;default:return\"Campo inválido\"}}};function wa(e,t,r,n){const a=Math.abs(e),o=a%10,i=a%100;return i>=11&&i<=19?n:1===o?t:o>=2&&o<=4?r:n}const xa=()=>{const e={string:{unit:{one:\"символ\",few:\"символа\",many:\"символов\"},verb:\"иметь\"},file:{unit:{one:\"байт\",few:\"байта\",many:\"байт\"},verb:\"иметь\"},array:{unit:{one:\"элемент\",few:\"элемента\",many:\"элементов\"},verb:\"иметь\"},set:{unit:{one:\"элемент\",few:\"элемента\",many:\"элементов\"},verb:\"иметь\"}};function t(t){return e[t]??null}const r={regex:\"ввод\",email:\"email адрес\",url:\"URL\",emoji:\"эмодзи\",uuid:\"UUID\",uuidv4:\"UUIDv4\",uuidv6:\"UUIDv6\",nanoid:\"nanoid\",guid:\"GUID\",cuid:\"cuid\",cuid2:\"cuid2\",ulid:\"ULID\",xid:\"XID\",ksuid:\"KSUID\",datetime:\"ISO дата и время\",date:\"ISO дата\",time:\"ISO время\",duration:\"ISO длительность\",ipv4:\"IPv4 адрес\",ipv6:\"IPv6 адрес\",cidrv4:\"IPv4 диапазон\",cidrv6:\"IPv6 диапазон\",base64:\"строка в формате base64\",base64url:\"строка в формате base64url\",json_string:\"JSON строка\",e164:\"номер E.164\",jwt:\"JWT\",template_literal:\"ввод\"};return e=>{switch(e.code){case\"invalid_type\":return`Неверный ввод: ожидалось ${e.expected}, получено ${(e=>{const t=typeof e;switch(t){case\"number\":return Number.isNaN(e)?\"NaN\":\"число\";case\"object\":if(Array.isArray(e))return\"массив\";if(null===e)return\"null\";if(Object.getPrototypeOf(e)!==Object.prototype&&e.constructor)return e.constructor.name}return t})(e.input)}`;case\"invalid_value\":return 1===e.values.length?`Неверный ввод: ожидалось ${$(e.values[0])}`:`Неверный вариант: ожидалось одно из ${l(e.values,\"|\")}`;case\"too_big\":{const r=e.inclusive?\"<=\":\"<\",n=t(e.origin);if(n){const t=wa(Number(e.maximum),n.unit.one,n.unit.few,n.unit.many);return`Слишком большое значение: ожидалось, что ${e.origin??\"значение\"} будет иметь ${r}${e.maximum.toString()} ${t}`}return`Слишком большое значение: ожидалось, что ${e.origin??\"значение\"} будет ${r}${e.maximum.toString()}`}case\"too_small\":{const r=e.inclusive?\">=\":\">\",n=t(e.origin);if(n){const t=wa(Number(e.minimum),n.unit.one,n.unit.few,n.unit.many);return`Слишком маленькое значение: ожидалось, что ${e.origin} будет иметь ${r}${e.minimum.toString()} ${t}`}return`Слишком маленькое значение: ожидалось, что ${e.origin} будет ${r}${e.minimum.toString()}`}case\"invalid_format\":{const t=e;return\"starts_with\"===t.format?`Неверная строка: должна начинаться с \"${t.prefix}\"`:\"ends_with\"===t.format?`Неверная строка: должна заканчиваться на \"${t.suffix}\"`:\"includes\"===t.format?`Неверная строка: должна содержать \"${t.includes}\"`:\"regex\"===t.format?`Неверная строка: должна соответствовать шаблону ${t.pattern}`:`Неверный ${r[t.format]??e.format}`}case\"not_multiple_of\":return`Неверное число: должно быть кратным ${e.divisor}`;case\"unrecognized_keys\":return`Нераспознанн${e.keys.length>1?\"ые\":\"ый\"} ключ${e.keys.length>1?\"и\":\"\"}: ${l(e.keys,\", \")}`;case\"invalid_key\":return`Неверный ключ в ${e.origin}`;case\"invalid_union\":default:return\"Неверные входные данные\";case\"invalid_element\":return`Неверное значение в ${e.origin}`}}},ka=()=>{const e={string:{unit:\"znakov\",verb:\"imeti\"},file:{unit:\"bajtov\",verb:\"imeti\"},array:{unit:\"elementov\",verb:\"imeti\"},set:{unit:\"elementov\",verb:\"imeti\"}};function t(t){return e[t]??null}const r={regex:\"vnos\",email:\"e-poštni naslov\",url:\"URL\",emoji:\"emoji\",uuid:\"UUID\",uuidv4:\"UUIDv4\",uuidv6:\"UUIDv6\",nanoid:\"nanoid\",guid:\"GUID\",cuid:\"cuid\",cuid2:\"cuid2\",ulid:\"ULID\",xid:\"XID\",ksuid:\"KSUID\",datetime:\"ISO datum in čas\",date:\"ISO datum\",time:\"ISO čas\",duration:\"ISO trajanje\",ipv4:\"IPv4 naslov\",ipv6:\"IPv6 naslov\",cidrv4:\"obseg IPv4\",cidrv6:\"obseg IPv6\",base64:\"base64 kodiran niz\",base64url:\"base64url kodiran niz\",json_string:\"JSON niz\",e164:\"E.164 številka\",jwt:\"JWT\",template_literal:\"vnos\"};return e=>{switch(e.code){case\"invalid_type\":return`Neveljaven vnos: pričakovano ${e.expected}, prejeto ${(e=>{const t=typeof e;switch(t){case\"number\":return Number.isNaN(e)?\"NaN\":\"število\";case\"object\":if(Array.isArray(e))return\"tabela\";if(null===e)return\"null\";if(Object.getPrototypeOf(e)!==Object.prototype&&e.constructor)return e.constructor.name}return t})(e.input)}`;case\"invalid_value\":return 1===e.values.length?`Neveljaven vnos: pričakovano ${$(e.values[0])}`:`Neveljavna možnost: pričakovano eno izmed ${l(e.values,\"|\")}`;case\"too_big\":{const r=e.inclusive?\"<=\":\"<\",n=t(e.origin);return n?`Preveliko: pričakovano, da bo ${e.origin??\"vrednost\"} imelo ${r}${e.maximum.toString()} ${n.unit??\"elementov\"}`:`Preveliko: pričakovano, da bo ${e.origin??\"vrednost\"} ${r}${e.maximum.toString()}`}case\"too_small\":{const r=e.inclusive?\">=\":\">\",n=t(e.origin);return n?`Premajhno: pričakovano, da bo ${e.origin} imelo ${r}${e.minimum.toString()} ${n.unit}`:`Premajhno: pričakovano, da bo ${e.origin} ${r}${e.minimum.toString()}`}case\"invalid_format\":{const t=e;return\"starts_with\"===t.format?`Neveljaven niz: mora se začeti z \"${t.prefix}\"`:\"ends_with\"===t.format?`Neveljaven niz: mora se končati z \"${t.suffix}\"`:\"includes\"===t.format?`Neveljaven niz: mora vsebovati \"${t.includes}\"`:\"regex\"===t.format?`Neveljaven niz: mora ustrezati vzorcu ${t.pattern}`:`Neveljaven ${r[t.format]??e.format}`}case\"not_multiple_of\":return`Neveljavno število: mora biti večkratnik ${e.divisor}`;case\"unrecognized_keys\":return`Neprepoznan${e.keys.length>1?\"i ključi\":\" ključ\"}: ${l(e.keys,\", \")}`;case\"invalid_key\":return`Neveljaven ključ v ${e.origin}`;case\"invalid_union\":default:return\"Neveljaven vnos\";case\"invalid_element\":return`Neveljavna vrednost v ${e.origin}`}}},Sa=()=>{const e={string:{unit:\"tecken\",verb:\"att ha\"},file:{unit:\"bytes\",verb:\"att ha\"},array:{unit:\"objekt\",verb:\"att innehålla\"},set:{unit:\"objekt\",verb:\"att innehålla\"}};function t(t){return e[t]??null}const r={regex:\"reguljärt uttryck\",email:\"e-postadress\",url:\"URL\",emoji:\"emoji\",uuid:\"UUID\",uuidv4:\"UUIDv4\",uuidv6:\"UUIDv6\",nanoid:\"nanoid\",guid:\"GUID\",cuid:\"cuid\",cuid2:\"cuid2\",ulid:\"ULID\",xid:\"XID\",ksuid:\"KSUID\",datetime:\"ISO-datum och tid\",date:\"ISO-datum\",time:\"ISO-tid\",duration:\"ISO-varaktighet\",ipv4:\"IPv4-intervall\",ipv6:\"IPv6-intervall\",cidrv4:\"IPv4-spektrum\",cidrv6:\"IPv6-spektrum\",base64:\"base64-kodad sträng\",base64url:\"base64url-kodad sträng\",json_string:\"JSON-sträng\",e164:\"E.164-nummer\",jwt:\"JWT\",template_literal:\"mall-literal\"};return e=>{switch(e.code){case\"invalid_type\":return`Ogiltig inmatning: förväntat ${e.expected}, fick ${(e=>{const t=typeof e;switch(t){case\"number\":return Number.isNaN(e)?\"NaN\":\"antal\";case\"object\":if(Array.isArray(e))return\"lista\";if(null===e)return\"null\";if(Object.getPrototypeOf(e)!==Object.prototype&&e.constructor)return e.constructor.name}return t})(e.input)}`;case\"invalid_value\":return 1===e.values.length?`Ogiltig inmatning: förväntat ${$(e.values[0])}`:`Ogiltigt val: förväntade en av ${l(e.values,\"|\")}`;case\"too_big\":{const r=e.inclusive?\"<=\":\"<\",n=t(e.origin);return n?`För stor(t): förväntade ${e.origin??\"värdet\"} att ha ${r}${e.maximum.toString()} ${n.unit??\"element\"}`:`För stor(t): förväntat ${e.origin??\"värdet\"} att ha ${r}${e.maximum.toString()}`}case\"too_small\":{const r=e.inclusive?\">=\":\">\",n=t(e.origin);return n?`För lite(t): förväntade ${e.origin??\"värdet\"} att ha ${r}${e.minimum.toString()} ${n.unit}`:`För lite(t): förväntade ${e.origin??\"värdet\"} att ha ${r}${e.minimum.toString()}`}case\"invalid_format\":{const t=e;return\"starts_with\"===t.format?`Ogiltig sträng: måste börja med \"${t.prefix}\"`:\"ends_with\"===t.format?`Ogiltig sträng: måste sluta med \"${t.suffix}\"`:\"includes\"===t.format?`Ogiltig sträng: måste innehålla \"${t.includes}\"`:\"regex\"===t.format?`Ogiltig sträng: måste matcha mönstret \"${t.pattern}\"`:`Ogiltig(t) ${r[t.format]??e.format}`}case\"not_multiple_of\":return`Ogiltigt tal: måste vara en multipel av ${e.divisor}`;case\"unrecognized_keys\":return`${e.keys.length>1?\"Okända nycklar\":\"Okänd nyckel\"}: ${l(e.keys,\", \")}`;case\"invalid_key\":return`Ogiltig nyckel i ${e.origin??\"värdet\"}`;case\"invalid_union\":default:return\"Ogiltig input\";case\"invalid_element\":return`Ogiltigt värde i ${e.origin??\"värdet\"}`}}},_a=()=>{const e={string:{unit:\"எழுத்துக்கள்\",verb:\"கொண்டிருக்க வேண்டும்\"},file:{unit:\"பைட்டுகள்\",verb:\"கொண்டிருக்க வேண்டும்\"},array:{unit:\"உறுப்புகள்\",verb:\"கொண்டிருக்க வேண்டும்\"},set:{unit:\"உறுப்புகள்\",verb:\"கொண்டிருக்க வேண்டும்\"}};function t(t){return e[t]??null}const r={regex:\"உள்ளீடு\",email:\"மின்னஞ்சல் முகவரி\",url:\"URL\",emoji:\"emoji\",uuid:\"UUID\",uuidv4:\"UUIDv4\",uuidv6:\"UUIDv6\",nanoid:\"nanoid\",guid:\"GUID\",cuid:\"cuid\",cuid2:\"cuid2\",ulid:\"ULID\",xid:\"XID\",ksuid:\"KSUID\",datetime:\"ISO தேதி நேரம்\",date:\"ISO தேதி\",time:\"ISO நேரம்\",duration:\"ISO கால அளவு\",ipv4:\"IPv4 முகவரி\",ipv6:\"IPv6 முகவரி\",cidrv4:\"IPv4 வரம்பு\",cidrv6:\"IPv6 வரம்பு\",base64:\"base64-encoded சரம்\",base64url:\"base64url-encoded சரம்\",json_string:\"JSON சரம்\",e164:\"E.164 எண்\",jwt:\"JWT\",template_literal:\"input\"};return e=>{switch(e.code){case\"invalid_type\":return`தவறான உள்ளீடு: எதிர்பார்க்கப்பட்டது ${e.expected}, பெறப்பட்டது ${(e=>{const t=typeof e;switch(t){case\"number\":return Number.isNaN(e)?\"எண் அல்லாதது\":\"எண்\";case\"object\":if(Array.isArray(e))return\"அணி\";if(null===e)return\"வெறுமை\";if(Object.getPrototypeOf(e)!==Object.prototype&&e.constructor)return e.constructor.name}return t})(e.input)}`;case\"invalid_value\":return 1===e.values.length?`தவறான உள்ளீடு: எதிர்பார்க்கப்பட்டது ${$(e.values[0])}`:`தவறான விருப்பம்: எதிர்பார்க்கப்பட்டது ${l(e.values,\"|\")} இல் ஒன்று`;case\"too_big\":{const r=e.inclusive?\"<=\":\"<\",n=t(e.origin);return n?`மிக பெரியது: எதிர்பார்க்கப்பட்டது ${e.origin??\"மதிப்பு\"} ${r}${e.maximum.toString()} ${n.unit??\"உறுப்புகள்\"} ஆக இருக்க வேண்டும்`:`மிக பெரியது: எதிர்பார்க்கப்பட்டது ${e.origin??\"மதிப்பு\"} ${r}${e.maximum.toString()} ஆக இருக்க வேண்டும்`}case\"too_small\":{const r=e.inclusive?\">=\":\">\",n=t(e.origin);return n?`மிகச் சிறியது: எதிர்பார்க்கப்பட்டது ${e.origin} ${r}${e.minimum.toString()} ${n.unit} ஆக இருக்க வேண்டும்`:`மிகச் சிறியது: எதிர்பார்க்கப்பட்டது ${e.origin} ${r}${e.minimum.toString()} ஆக இருக்க வேண்டும்`}case\"invalid_format\":{const t=e;return\"starts_with\"===t.format?`தவறான சரம்: \"${t.prefix}\" இல் தொடங்க வேண்டும்`:\"ends_with\"===t.format?`தவறான சரம்: \"${t.suffix}\" இல் முடிவடைய வேண்டும்`:\"includes\"===t.format?`தவறான சரம்: \"${t.includes}\" ஐ உள்ளடக்க வேண்டும்`:\"regex\"===t.format?`தவறான சரம்: ${t.pattern} முறைபாட்டுடன் பொருந்த வேண்டும்`:`தவறான ${r[t.format]??e.format}`}case\"not_multiple_of\":return`தவறான எண்: ${e.divisor} இன் பலமாக இருக்க வேண்டும்`;case\"unrecognized_keys\":return`அடையாளம் தெரியாத விசை${e.keys.length>1?\"கள்\":\"\"}: ${l(e.keys,\", \")}`;case\"invalid_key\":return`${e.origin} இல் தவறான விசை`;case\"invalid_union\":default:return\"தவறான உள்ளீடு\";case\"invalid_element\":return`${e.origin} இல் தவறான மதிப்பு`}}},Ea=()=>{const e={string:{unit:\"ตัวอักษร\",verb:\"ควรมี\"},file:{unit:\"ไบต์\",verb:\"ควรมี\"},array:{unit:\"รายการ\",verb:\"ควรมี\"},set:{unit:\"รายการ\",verb:\"ควรมี\"}};function t(t){return e[t]??null}const r={regex:\"ข้อมูลที่ป้อน\",email:\"ที่อยู่อีเมล\",url:\"URL\",emoji:\"อิโมจิ\",uuid:\"UUID\",uuidv4:\"UUIDv4\",uuidv6:\"UUIDv6\",nanoid:\"nanoid\",guid:\"GUID\",cuid:\"cuid\",cuid2:\"cuid2\",ulid:\"ULID\",xid:\"XID\",ksuid:\"KSUID\",datetime:\"วันที่เวลาแบบ ISO\",date:\"วันที่แบบ ISO\",time:\"เวลาแบบ ISO\",duration:\"ช่วงเวลาแบบ ISO\",ipv4:\"ที่อยู่ IPv4\",ipv6:\"ที่อยู่ IPv6\",cidrv4:\"ช่วง IP แบบ IPv4\",cidrv6:\"ช่วง IP แบบ IPv6\",base64:\"ข้อความแบบ Base64\",base64url:\"ข้อความแบบ Base64 สำหรับ URL\",json_string:\"ข้อความแบบ JSON\",e164:\"เบอร์โทรศัพท์ระหว่างประเทศ (E.164)\",jwt:\"โทเคน JWT\",template_literal:\"ข้อมูลที่ป้อน\"};return e=>{switch(e.code){case\"invalid_type\":return`ประเภทข้อมูลไม่ถูกต้อง: ควรเป็น ${e.expected} แต่ได้รับ ${(e=>{const t=typeof e;switch(t){case\"number\":return Number.isNaN(e)?\"ไม่ใช่ตัวเลข (NaN)\":\"ตัวเลข\";case\"object\":if(Array.isArray(e))return\"อาร์เรย์ (Array)\";if(null===e)return\"ไม่มีค่า (null)\";if(Object.getPrototypeOf(e)!==Object.prototype&&e.constructor)return e.constructor.name}return t})(e.input)}`;case\"invalid_value\":return 1===e.values.length?`ค่าไม่ถูกต้อง: ควรเป็น ${$(e.values[0])}`:`ตัวเลือกไม่ถูกต้อง: ควรเป็นหนึ่งใน ${l(e.values,\"|\")}`;case\"too_big\":{const r=e.inclusive?\"ไม่เกิน\":\"น้อยกว่า\",n=t(e.origin);return n?`เกินกำหนด: ${e.origin??\"ค่า\"} ควรมี${r} ${e.maximum.toString()} ${n.unit??\"รายการ\"}`:`เกินกำหนด: ${e.origin??\"ค่า\"} ควรมี${r} ${e.maximum.toString()}`}case\"too_small\":{const r=e.inclusive?\"อย่างน้อย\":\"มากกว่า\",n=t(e.origin);return n?`น้อยกว่ากำหนด: ${e.origin} ควรมี${r} ${e.minimum.toString()} ${n.unit}`:`น้อยกว่ากำหนด: ${e.origin} ควรมี${r} ${e.minimum.toString()}`}case\"invalid_format\":{const t=e;return\"starts_with\"===t.format?`รูปแบบไม่ถูกต้อง: ข้อความต้องขึ้นต้นด้วย \"${t.prefix}\"`:\"ends_with\"===t.format?`รูปแบบไม่ถูกต้อง: ข้อความต้องลงท้ายด้วย \"${t.suffix}\"`:\"includes\"===t.format?`รูปแบบไม่ถูกต้อง: ข้อความต้องมี \"${t.includes}\" อยู่ในข้อความ`:\"regex\"===t.format?`รูปแบบไม่ถูกต้อง: ต้องตรงกับรูปแบบที่กำหนด ${t.pattern}`:`รูปแบบไม่ถูกต้อง: ${r[t.format]??e.format}`}case\"not_multiple_of\":return`ตัวเลขไม่ถูกต้อง: ต้องเป็นจำนวนที่หารด้วย ${e.divisor} ได้ลงตัว`;case\"unrecognized_keys\":return`พบคีย์ที่ไม่รู้จัก: ${l(e.keys,\", \")}`;case\"invalid_key\":return`คีย์ไม่ถูกต้องใน ${e.origin}`;case\"invalid_union\":return\"ข้อมูลไม่ถูกต้อง: ไม่ตรงกับรูปแบบยูเนียนที่กำหนดไว้\";case\"invalid_element\":return`ข้อมูลไม่ถูกต้องใน ${e.origin}`;default:return\"ข้อมูลไม่ถูกต้อง\"}}},Ta=()=>{const e={string:{unit:\"karakter\",verb:\"olmalı\"},file:{unit:\"bayt\",verb:\"olmalı\"},array:{unit:\"öğe\",verb:\"olmalı\"},set:{unit:\"öğe\",verb:\"olmalı\"}};function t(t){return e[t]??null}const r={regex:\"girdi\",email:\"e-posta adresi\",url:\"URL\",emoji:\"emoji\",uuid:\"UUID\",uuidv4:\"UUIDv4\",uuidv6:\"UUIDv6\",nanoid:\"nanoid\",guid:\"GUID\",cuid:\"cuid\",cuid2:\"cuid2\",ulid:\"ULID\",xid:\"XID\",ksuid:\"KSUID\",datetime:\"ISO tarih ve saat\",date:\"ISO tarih\",time:\"ISO saat\",duration:\"ISO süre\",ipv4:\"IPv4 adresi\",ipv6:\"IPv6 adresi\",cidrv4:\"IPv4 aralığı\",cidrv6:\"IPv6 aralığı\",base64:\"base64 ile şifrelenmiş metin\",base64url:\"base64url ile şifrelenmiş metin\",json_string:\"JSON dizesi\",e164:\"E.164 sayısı\",jwt:\"JWT\",template_literal:\"Şablon dizesi\"};return e=>{switch(e.code){case\"invalid_type\":return`Geçersiz değer: beklenen ${e.expected}, alınan ${(e=>{const t=typeof e;switch(t){case\"number\":return Number.isNaN(e)?\"NaN\":\"number\";case\"object\":if(Array.isArray(e))return\"array\";if(null===e)return\"null\";if(Object.getPrototypeOf(e)!==Object.prototype&&e.constructor)return e.constructor.name}return t})(e.input)}`;case\"invalid_value\":return 1===e.values.length?`Geçersiz değer: beklenen ${$(e.values[0])}`:`Geçersiz seçenek: aşağıdakilerden biri olmalı: ${l(e.values,\"|\")}`;case\"too_big\":{const r=e.inclusive?\"<=\":\"<\",n=t(e.origin);return n?`Çok büyük: beklenen ${e.origin??\"değer\"} ${r}${e.maximum.toString()} ${n.unit??\"öğe\"}`:`Çok büyük: beklenen ${e.origin??\"değer\"} ${r}${e.maximum.toString()}`}case\"too_small\":{const r=e.inclusive?\">=\":\">\",n=t(e.origin);return n?`Çok küçük: beklenen ${e.origin} ${r}${e.minimum.toString()} ${n.unit}`:`Çok küçük: beklenen ${e.origin} ${r}${e.minimum.toString()}`}case\"invalid_format\":{const t=e;return\"starts_with\"===t.format?`Geçersiz metin: \"${t.prefix}\" ile başlamalı`:\"ends_with\"===t.format?`Geçersiz metin: \"${t.suffix}\" ile bitmeli`:\"includes\"===t.format?`Geçersiz metin: \"${t.includes}\" içermeli`:\"regex\"===t.format?`Geçersiz metin: ${t.pattern} desenine uymalı`:`Geçersiz ${r[t.format]??e.format}`}case\"not_multiple_of\":return`Geçersiz sayı: ${e.divisor} ile tam bölünebilmeli`;case\"unrecognized_keys\":return`Tanınmayan anahtar${e.keys.length>1?\"lar\":\"\"}: ${l(e.keys,\", \")}`;case\"invalid_key\":return`${e.origin} içinde geçersiz anahtar`;case\"invalid_union\":default:return\"Geçersiz değer\";case\"invalid_element\":return`${e.origin} içinde geçersiz değer`}}},Aa=()=>{const e={string:{unit:\"символів\",verb:\"матиме\"},file:{unit:\"байтів\",verb:\"матиме\"},array:{unit:\"елементів\",verb:\"матиме\"},set:{unit:\"елементів\",verb:\"матиме\"}};function t(t){return e[t]??null}const r={regex:\"вхідні дані\",email:\"адреса електронної пошти\",url:\"URL\",emoji:\"емодзі\",uuid:\"UUID\",uuidv4:\"UUIDv4\",uuidv6:\"UUIDv6\",nanoid:\"nanoid\",guid:\"GUID\",cuid:\"cuid\",cuid2:\"cuid2\",ulid:\"ULID\",xid:\"XID\",ksuid:\"KSUID\",datetime:\"дата та час ISO\",date:\"дата ISO\",time:\"час ISO\",duration:\"тривалість ISO\",ipv4:\"адреса IPv4\",ipv6:\"адреса IPv6\",cidrv4:\"діапазон IPv4\",cidrv6:\"діапазон IPv6\",base64:\"рядок у кодуванні base64\",base64url:\"рядок у кодуванні base64url\",json_string:\"рядок JSON\",e164:\"номер E.164\",jwt:\"JWT\",template_literal:\"вхідні дані\"};return e=>{switch(e.code){case\"invalid_type\":return`Неправильні вхідні дані: очікується ${e.expected}, отримано ${(e=>{const t=typeof e;switch(t){case\"number\":return Number.isNaN(e)?\"NaN\":\"число\";case\"object\":if(Array.isArray(e))return\"масив\";if(null===e)return\"null\";if(Object.getPrototypeOf(e)!==Object.prototype&&e.constructor)return e.constructor.name}return t})(e.input)}`;case\"invalid_value\":return 1===e.values.length?`Неправильні вхідні дані: очікується ${$(e.values[0])}`:`Неправильна опція: очікується одне з ${l(e.values,\"|\")}`;case\"too_big\":{const r=e.inclusive?\"<=\":\"<\",n=t(e.origin);return n?`Занадто велике: очікується, що ${e.origin??\"значення\"} ${n.verb} ${r}${e.maximum.toString()} ${n.unit??\"елементів\"}`:`Занадто велике: очікується, що ${e.origin??\"значення\"} буде ${r}${e.maximum.toString()}`}case\"too_small\":{const r=e.inclusive?\">=\":\">\",n=t(e.origin);return n?`Занадто мале: очікується, що ${e.origin} ${n.verb} ${r}${e.minimum.toString()} ${n.unit}`:`Занадто мале: очікується, що ${e.origin} буде ${r}${e.minimum.toString()}`}case\"invalid_format\":{const t=e;return\"starts_with\"===t.format?`Неправильний рядок: повинен починатися з \"${t.prefix}\"`:\"ends_with\"===t.format?`Неправильний рядок: повинен закінчуватися на \"${t.suffix}\"`:\"includes\"===t.format?`Неправильний рядок: повинен містити \"${t.includes}\"`:\"regex\"===t.format?`Неправильний рядок: повинен відповідати шаблону ${t.pattern}`:`Неправильний ${r[t.format]??e.format}`}case\"not_multiple_of\":return`Неправильне число: повинно бути кратним ${e.divisor}`;case\"unrecognized_keys\":return`Нерозпізнаний ключ${e.keys.length>1?\"і\":\"\"}: ${l(e.keys,\", \")}`;case\"invalid_key\":return`Неправильний ключ у ${e.origin}`;case\"invalid_union\":default:return\"Неправильні вхідні дані\";case\"invalid_element\":return`Неправильне значення у ${e.origin}`}}};function $a(){return{localeError:Aa()}}const Ca=()=>{const e={string:{unit:\"حروف\",verb:\"ہونا\"},file:{unit:\"بائٹس\",verb:\"ہونا\"},array:{unit:\"آئٹمز\",verb:\"ہونا\"},set:{unit:\"آئٹمز\",verb:\"ہونا\"}};function t(t){return e[t]??null}const r={regex:\"ان پٹ\",email:\"ای میل ایڈریس\",url:\"یو آر ایل\",emoji:\"ایموجی\",uuid:\"یو یو آئی ڈی\",uuidv4:\"یو یو آئی ڈی وی 4\",uuidv6:\"یو یو آئی ڈی وی 6\",nanoid:\"نینو آئی ڈی\",guid:\"جی یو آئی ڈی\",cuid:\"سی یو آئی ڈی\",cuid2:\"سی یو آئی ڈی 2\",ulid:\"یو ایل آئی ڈی\",xid:\"ایکس آئی ڈی\",ksuid:\"کے ایس یو آئی ڈی\",datetime:\"آئی ایس او ڈیٹ ٹائم\",date:\"آئی ایس او تاریخ\",time:\"آئی ایس او وقت\",duration:\"آئی ایس او مدت\",ipv4:\"آئی پی وی 4 ایڈریس\",ipv6:\"آئی پی وی 6 ایڈریس\",cidrv4:\"آئی پی وی 4 رینج\",cidrv6:\"آئی پی وی 6 رینج\",base64:\"بیس 64 ان کوڈڈ سٹرنگ\",base64url:\"بیس 64 یو آر ایل ان کوڈڈ سٹرنگ\",json_string:\"جے ایس او این سٹرنگ\",e164:\"ای 164 نمبر\",jwt:\"جے ڈبلیو ٹی\",template_literal:\"ان پٹ\"};return e=>{switch(e.code){case\"invalid_type\":return`غلط ان پٹ: ${e.expected} متوقع تھا، ${(e=>{const t=typeof e;switch(t){case\"number\":return Number.isNaN(e)?\"NaN\":\"نمبر\";case\"object\":if(Array.isArray(e))return\"آرے\";if(null===e)return\"نل\";if(Object.getPrototypeOf(e)!==Object.prototype&&e.constructor)return e.constructor.name}return t})(e.input)} موصول ہوا`;case\"invalid_value\":return 1===e.values.length?`غلط ان پٹ: ${$(e.values[0])} متوقع تھا`:`غلط آپشن: ${l(e.values,\"|\")} میں سے ایک متوقع تھا`;case\"too_big\":{const r=e.inclusive?\"<=\":\"<\",n=t(e.origin);return n?`بہت بڑا: ${e.origin??\"ویلیو\"} کے ${r}${e.maximum.toString()} ${n.unit??\"عناصر\"} ہونے متوقع تھے`:`بہت بڑا: ${e.origin??\"ویلیو\"} کا ${r}${e.maximum.toString()} ہونا متوقع تھا`}case\"too_small\":{const r=e.inclusive?\">=\":\">\",n=t(e.origin);return n?`بہت چھوٹا: ${e.origin} کے ${r}${e.minimum.toString()} ${n.unit} ہونے متوقع تھے`:`بہت چھوٹا: ${e.origin} کا ${r}${e.minimum.toString()} ہونا متوقع تھا`}case\"invalid_format\":{const t=e;return\"starts_with\"===t.format?`غلط سٹرنگ: \"${t.prefix}\" سے شروع ہونا چاہیے`:\"ends_with\"===t.format?`غلط سٹرنگ: \"${t.suffix}\" پر ختم ہونا چاہیے`:\"includes\"===t.format?`غلط سٹرنگ: \"${t.includes}\" شامل ہونا چاہیے`:\"regex\"===t.format?`غلط سٹرنگ: پیٹرن ${t.pattern} سے میچ ہونا چاہیے`:`غلط ${r[t.format]??e.format}`}case\"not_multiple_of\":return`غلط نمبر: ${e.divisor} کا مضاعف ہونا چاہیے`;case\"unrecognized_keys\":return`غیر تسلیم شدہ کی${e.keys.length>1?\"ز\":\"\"}: ${l(e.keys,\"، \")}`;case\"invalid_key\":return`${e.origin} میں غلط کی`;case\"invalid_union\":default:return\"غلط ان پٹ\";case\"invalid_element\":return`${e.origin} میں غلط ویلیو`}}},Pa=()=>{const e={string:{unit:\"ký tự\",verb:\"có\"},file:{unit:\"byte\",verb:\"có\"},array:{unit:\"phần tử\",verb:\"có\"},set:{unit:\"phần tử\",verb:\"có\"}};function t(t){return e[t]??null}const r={regex:\"đầu vào\",email:\"địa chỉ email\",url:\"URL\",emoji:\"emoji\",uuid:\"UUID\",uuidv4:\"UUIDv4\",uuidv6:\"UUIDv6\",nanoid:\"nanoid\",guid:\"GUID\",cuid:\"cuid\",cuid2:\"cuid2\",ulid:\"ULID\",xid:\"XID\",ksuid:\"KSUID\",datetime:\"ngày giờ ISO\",date:\"ngày ISO\",time:\"giờ ISO\",duration:\"khoảng thời gian ISO\",ipv4:\"địa chỉ IPv4\",ipv6:\"địa chỉ IPv6\",cidrv4:\"dải IPv4\",cidrv6:\"dải IPv6\",base64:\"chuỗi mã hóa base64\",base64url:\"chuỗi mã hóa base64url\",json_string:\"chuỗi JSON\",e164:\"số E.164\",jwt:\"JWT\",template_literal:\"đầu vào\"};return e=>{switch(e.code){case\"invalid_type\":return`Đầu vào không hợp lệ: mong đợi ${e.expected}, nhận được ${(e=>{const t=typeof e;switch(t){case\"number\":return Number.isNaN(e)?\"NaN\":\"số\";case\"object\":if(Array.isArray(e))return\"mảng\";if(null===e)return\"null\";if(Object.getPrototypeOf(e)!==Object.prototype&&e.constructor)return e.constructor.name}return t})(e.input)}`;case\"invalid_value\":return 1===e.values.length?`Đầu vào không hợp lệ: mong đợi ${$(e.values[0])}`:`Tùy chọn không hợp lệ: mong đợi một trong các giá trị ${l(e.values,\"|\")}`;case\"too_big\":{const r=e.inclusive?\"<=\":\"<\",n=t(e.origin);return n?`Quá lớn: mong đợi ${e.origin??\"giá trị\"} ${n.verb} ${r}${e.maximum.toString()} ${n.unit??\"phần tử\"}`:`Quá lớn: mong đợi ${e.origin??\"giá trị\"} ${r}${e.maximum.toString()}`}case\"too_small\":{const r=e.inclusive?\">=\":\">\",n=t(e.origin);return n?`Quá nhỏ: mong đợi ${e.origin} ${n.verb} ${r}${e.minimum.toString()} ${n.unit}`:`Quá nhỏ: mong đợi ${e.origin} ${r}${e.minimum.toString()}`}case\"invalid_format\":{const t=e;return\"starts_with\"===t.format?`Chuỗi không hợp lệ: phải bắt đầu bằng \"${t.prefix}\"`:\"ends_with\"===t.format?`Chuỗi không hợp lệ: phải kết thúc bằng \"${t.suffix}\"`:\"includes\"===t.format?`Chuỗi không hợp lệ: phải bao gồm \"${t.includes}\"`:\"regex\"===t.format?`Chuỗi không hợp lệ: phải khớp với mẫu ${t.pattern}`:`${r[t.format]??e.format} không hợp lệ`}case\"not_multiple_of\":return`Số không hợp lệ: phải là bội số của ${e.divisor}`;case\"unrecognized_keys\":return`Khóa không được nhận dạng: ${l(e.keys,\", \")}`;case\"invalid_key\":return`Khóa không hợp lệ trong ${e.origin}`;case\"invalid_union\":default:return\"Đầu vào không hợp lệ\";case\"invalid_element\":return`Giá trị không hợp lệ trong ${e.origin}`}}},Da=()=>{const e={string:{unit:\"字符\",verb:\"包含\"},file:{unit:\"字节\",verb:\"包含\"},array:{unit:\"项\",verb:\"包含\"},set:{unit:\"项\",verb:\"包含\"}};function t(t){return e[t]??null}const r={regex:\"输入\",email:\"电子邮件\",url:\"URL\",emoji:\"表情符号\",uuid:\"UUID\",uuidv4:\"UUIDv4\",uuidv6:\"UUIDv6\",nanoid:\"nanoid\",guid:\"GUID\",cuid:\"cuid\",cuid2:\"cuid2\",ulid:\"ULID\",xid:\"XID\",ksuid:\"KSUID\",datetime:\"ISO日期时间\",date:\"ISO日期\",time:\"ISO时间\",duration:\"ISO时长\",ipv4:\"IPv4地址\",ipv6:\"IPv6地址\",cidrv4:\"IPv4网段\",cidrv6:\"IPv6网段\",base64:\"base64编码字符串\",base64url:\"base64url编码字符串\",json_string:\"JSON字符串\",e164:\"E.164号码\",jwt:\"JWT\",template_literal:\"输入\"};return e=>{switch(e.code){case\"invalid_type\":return`无效输入:期望 ${e.expected},实际接收 ${(e=>{const t=typeof e;switch(t){case\"number\":return Number.isNaN(e)?\"非数字(NaN)\":\"数字\";case\"object\":if(Array.isArray(e))return\"数组\";if(null===e)return\"空值(null)\";if(Object.getPrototypeOf(e)!==Object.prototype&&e.constructor)return e.constructor.name}return t})(e.input)}`;case\"invalid_value\":return 1===e.values.length?`无效输入:期望 ${$(e.values[0])}`:`无效选项:期望以下之一 ${l(e.values,\"|\")}`;case\"too_big\":{const r=e.inclusive?\"<=\":\"<\",n=t(e.origin);return n?`数值过大:期望 ${e.origin??\"值\"} ${r}${e.maximum.toString()} ${n.unit??\"个元素\"}`:`数值过大:期望 ${e.origin??\"值\"} ${r}${e.maximum.toString()}`}case\"too_small\":{const r=e.inclusive?\">=\":\">\",n=t(e.origin);return n?`数值过小:期望 ${e.origin} ${r}${e.minimum.toString()} ${n.unit}`:`数值过小:期望 ${e.origin} ${r}${e.minimum.toString()}`}case\"invalid_format\":{const t=e;return\"starts_with\"===t.format?`无效字符串:必须以 \"${t.prefix}\" 开头`:\"ends_with\"===t.format?`无效字符串:必须以 \"${t.suffix}\" 结尾`:\"includes\"===t.format?`无效字符串:必须包含 \"${t.includes}\"`:\"regex\"===t.format?`无效字符串:必须满足正则表达式 ${t.pattern}`:`无效${r[t.format]??e.format}`}case\"not_multiple_of\":return`无效数字:必须是 ${e.divisor} 的倍数`;case\"unrecognized_keys\":return`出现未知的键(key): ${l(e.keys,\", \")}`;case\"invalid_key\":return`${e.origin} 中的键(key)无效`;case\"invalid_union\":default:return\"无效输入\";case\"invalid_element\":return`${e.origin} 中包含无效值(value)`}}},Ia=()=>{const e={string:{unit:\"字元\",verb:\"擁有\"},file:{unit:\"位元組\",verb:\"擁有\"},array:{unit:\"項目\",verb:\"擁有\"},set:{unit:\"項目\",verb:\"擁有\"}};function t(t){return e[t]??null}const r={regex:\"輸入\",email:\"郵件地址\",url:\"URL\",emoji:\"emoji\",uuid:\"UUID\",uuidv4:\"UUIDv4\",uuidv6:\"UUIDv6\",nanoid:\"nanoid\",guid:\"GUID\",cuid:\"cuid\",cuid2:\"cuid2\",ulid:\"ULID\",xid:\"XID\",ksuid:\"KSUID\",datetime:\"ISO 日期時間\",date:\"ISO 日期\",time:\"ISO 時間\",duration:\"ISO 期間\",ipv4:\"IPv4 位址\",ipv6:\"IPv6 位址\",cidrv4:\"IPv4 範圍\",cidrv6:\"IPv6 範圍\",base64:\"base64 編碼字串\",base64url:\"base64url 編碼字串\",json_string:\"JSON 字串\",e164:\"E.164 數值\",jwt:\"JWT\",template_literal:\"輸入\"};return e=>{switch(e.code){case\"invalid_type\":return`無效的輸入值:預期為 ${e.expected},但收到 ${(e=>{const t=typeof e;switch(t){case\"number\":return Number.isNaN(e)?\"NaN\":\"number\";case\"object\":if(Array.isArray(e))return\"array\";if(null===e)return\"null\";if(Object.getPrototypeOf(e)!==Object.prototype&&e.constructor)return e.constructor.name}return t})(e.input)}`;case\"invalid_value\":return 1===e.values.length?`無效的輸入值:預期為 ${$(e.values[0])}`:`無效的選項:預期為以下其中之一 ${l(e.values,\"|\")}`;case\"too_big\":{const r=e.inclusive?\"<=\":\"<\",n=t(e.origin);return n?`數值過大:預期 ${e.origin??\"值\"} 應為 ${r}${e.maximum.toString()} ${n.unit??\"個元素\"}`:`數值過大:預期 ${e.origin??\"值\"} 應為 ${r}${e.maximum.toString()}`}case\"too_small\":{const r=e.inclusive?\">=\":\">\",n=t(e.origin);return n?`數值過小:預期 ${e.origin} 應為 ${r}${e.minimum.toString()} ${n.unit}`:`數值過小:預期 ${e.origin} 應為 ${r}${e.minimum.toString()}`}case\"invalid_format\":{const t=e;return\"starts_with\"===t.format?`無效的字串:必須以 \"${t.prefix}\" 開頭`:\"ends_with\"===t.format?`無效的字串:必須以 \"${t.suffix}\" 結尾`:\"includes\"===t.format?`無效的字串:必須包含 \"${t.includes}\"`:\"regex\"===t.format?`無效的字串:必須符合格式 ${t.pattern}`:`無效的 ${r[t.format]??e.format}`}case\"not_multiple_of\":return`無效的數字:必須為 ${e.divisor} 的倍數`;case\"unrecognized_keys\":return`無法識別的鍵值${e.keys.length>1?\"們\":\"\"}:${l(e.keys,\"、\")}`;case\"invalid_key\":return`${e.origin} 中有無效的鍵值`;case\"invalid_union\":default:return\"無效的輸入值\";case\"invalid_element\":return`${e.origin} 中有無效的值`}}},Ma=()=>{const e={string:{unit:\"àmi\",verb:\"ní\"},file:{unit:\"bytes\",verb:\"ní\"},array:{unit:\"nkan\",verb:\"ní\"},set:{unit:\"nkan\",verb:\"ní\"}};function t(t){return e[t]??null}const r={regex:\"ẹ̀rọ ìbáwọlé\",email:\"àdírẹ́sì ìmẹ́lì\",url:\"URL\",emoji:\"emoji\",uuid:\"UUID\",uuidv4:\"UUIDv4\",uuidv6:\"UUIDv6\",nanoid:\"nanoid\",guid:\"GUID\",cuid:\"cuid\",cuid2:\"cuid2\",ulid:\"ULID\",xid:\"XID\",ksuid:\"KSUID\",datetime:\"àkókò ISO\",date:\"ọjọ́ ISO\",time:\"àkókò ISO\",duration:\"àkókò tó pé ISO\",ipv4:\"àdírẹ́sì IPv4\",ipv6:\"àdírẹ́sì IPv6\",cidrv4:\"àgbègbè IPv4\",cidrv6:\"àgbègbè IPv6\",base64:\"ọ̀rọ̀ tí a kọ́ ní base64\",base64url:\"ọ̀rọ̀ base64url\",json_string:\"ọ̀rọ̀ JSON\",e164:\"nọ́mbà E.164\",jwt:\"JWT\",template_literal:\"ẹ̀rọ ìbáwọlé\"};return e=>{switch(e.code){case\"invalid_type\":return`Ìbáwọlé aṣìṣe: a ní láti fi ${e.expected}, àmọ̀ a rí ${(e=>{const t=typeof e;switch(t){case\"number\":return Number.isNaN(e)?\"NaN\":\"nọ́mbà\";case\"object\":if(Array.isArray(e))return\"akopọ\";if(null===e)return\"null\";if(Object.getPrototypeOf(e)!==Object.prototype&&e.constructor)return e.constructor.name}return t})(e.input)}`;case\"invalid_value\":return 1===e.values.length?`Ìbáwọlé aṣìṣe: a ní láti fi ${$(e.values[0])}`:`Àṣàyàn aṣìṣe: yan ọ̀kan lára ${l(e.values,\"|\")}`;case\"too_big\":{const r=e.inclusive?\"<=\":\"<\",n=t(e.origin);return n?`Tó pọ̀ jù: a ní láti jẹ́ pé ${e.origin??\"iye\"} ${n.verb} ${r}${e.maximum} ${n.unit}`:`Tó pọ̀ jù: a ní láti jẹ́ ${r}${e.maximum}`}case\"too_small\":{const r=e.inclusive?\">=\":\">\",n=t(e.origin);return n?`Kéré ju: a ní láti jẹ́ pé ${e.origin} ${n.verb} ${r}${e.minimum} ${n.unit}`:`Kéré ju: a ní láti jẹ́ ${r}${e.minimum}`}case\"invalid_format\":{const t=e;return\"starts_with\"===t.format?`Ọ̀rọ̀ aṣìṣe: gbọ́dọ̀ bẹ̀rẹ̀ pẹ̀lú \"${t.prefix}\"`:\"ends_with\"===t.format?`Ọ̀rọ̀ aṣìṣe: gbọ́dọ̀ parí pẹ̀lú \"${t.suffix}\"`:\"includes\"===t.format?`Ọ̀rọ̀ aṣìṣe: gbọ́dọ̀ ní \"${t.includes}\"`:\"regex\"===t.format?`Ọ̀rọ̀ aṣìṣe: gbọ́dọ̀ bá àpẹẹrẹ mu ${t.pattern}`:`Aṣìṣe: ${r[t.format]??e.format}`}case\"not_multiple_of\":return`Nọ́mbà aṣìṣe: gbọ́dọ̀ jẹ́ èyà pípín ti ${e.divisor}`;case\"unrecognized_keys\":return`Bọtìnì àìmọ̀: ${l(e.keys,\", \")}`;case\"invalid_key\":return`Bọtìnì aṣìṣe nínú ${e.origin}`;case\"invalid_union\":default:return\"Ìbáwọlé aṣìṣe\";case\"invalid_element\":return`Iye aṣìṣe nínú ${e.origin}`}}},Na=Object.freeze(Object.defineProperty({__proto__:null,ar:function(){return{localeError:jn()}},az:function(){return{localeError:Ln()}},be:function(){return{localeError:Bn()}},ca:function(){return{localeError:zn()}},cs:function(){return{localeError:Fn()}},da:function(){return{localeError:Qn()}},de:function(){return{localeError:qn()}},en:Vn,eo:function(){return{localeError:Hn()}},es:function(){return{localeError:Wn()}},fa:function(){return{localeError:Xn()}},fi:function(){return{localeError:Gn()}},fr:function(){return{localeError:Yn()}},frCA:function(){return{localeError:Kn()}},he:function(){return{localeError:Jn()}},hu:function(){return{localeError:ea()}},id:function(){return{localeError:ta()}},is:function(){return{localeError:ra()}},it:function(){return{localeError:na()}},ja:function(){return{localeError:aa()}},ka:function(){return{localeError:oa()}},kh:function(){return sa()},km:sa,ko:function(){return{localeError:la()}},lt:function(){return{localeError:pa()}},mk:function(){return{localeError:ha()}},ms:function(){return{localeError:fa()}},nl:function(){return{localeError:ma()}},no:function(){return{localeError:ga()}},ota:function(){return{localeError:va()}},pl:function(){return{localeError:ya()}},ps:function(){return{localeError:ba()}},pt:function(){return{localeError:Oa()}},ru:function(){return{localeError:xa()}},sl:function(){return{localeError:ka()}},sv:function(){return{localeError:Sa()}},ta:function(){return{localeError:_a()}},th:function(){return{localeError:Ea()}},tr:function(){return{localeError:Ta()}},ua:function(){return $a()},uk:$a,ur:function(){return{localeError:Ca()}},vi:function(){return{localeError:Pa()}},yo:function(){return{localeError:Ma()}},zhCN:function(){return{localeError:Da()}},zhTW:function(){return{localeError:Ia()}}},Symbol.toStringTag,{value:\"Module\"})),Ra=Symbol(\"ZodOutput\"),ja=Symbol(\"ZodInput\");class La{constructor(){this._map=new WeakMap,this._idmap=new Map}add(e,...t){const r=t[0];if(this._map.set(e,r),r&&\"object\"==typeof r&&\"id\"in r){if(this._idmap.has(r.id))throw new Error(`ID ${r.id} already exists in the registry`);this._idmap.set(r.id,e)}return this}clear(){return this._map=new WeakMap,this._idmap=new Map,this}remove(e){const t=this._map.get(e);return t&&\"object\"==typeof t&&\"id\"in t&&this._idmap.delete(t.id),this._map.delete(e),this}get(e){const t=e._zod.parent;if(t){const r={...this.get(t)??{}};delete r.id;const n={...r,...this._map.get(e)};return Object.keys(n).length?n:void 0}return this._map.get(e)}has(e){return this._map.has(e)}}function Ua(){return new La}const Ba=Ua();function za(e,t){return new e({type:\"string\",...A(t)})}function Fa(e,t){return new e({type:\"string\",coerce:!0,...A(t)})}function Qa(e,t){return new e({type:\"string\",format:\"email\",check:\"string_format\",abort:!1,...A(t)})}function qa(e,t){return new e({type:\"string\",format:\"guid\",check:\"string_format\",abort:!1,...A(t)})}function Za(e,t){return new e({type:\"string\",format:\"uuid\",check:\"string_format\",abort:!1,...A(t)})}function Va(e,t){return new e({type:\"string\",format:\"uuid\",check:\"string_format\",abort:!1,version:\"v4\",...A(t)})}function Ha(e,t){return new e({type:\"string\",format:\"uuid\",check:\"string_format\",abort:!1,version:\"v6\",...A(t)})}function Wa(e,t){return new e({type:\"string\",format:\"uuid\",check:\"string_format\",abort:!1,version:\"v7\",...A(t)})}function Xa(e,t){return new e({type:\"string\",format:\"url\",check:\"string_format\",abort:!1,...A(t)})}function Ga(e,t){return new e({type:\"string\",format:\"emoji\",check:\"string_format\",abort:!1,...A(t)})}function Ya(e,t){return new e({type:\"string\",format:\"nanoid\",check:\"string_format\",abort:!1,...A(t)})}function Ka(e,t){return new e({type:\"string\",format:\"cuid\",check:\"string_format\",abort:!1,...A(t)})}function Ja(e,t){return new e({type:\"string\",format:\"cuid2\",check:\"string_format\",abort:!1,...A(t)})}function eo(e,t){return new e({type:\"string\",format:\"ulid\",check:\"string_format\",abort:!1,...A(t)})}function to(e,t){return new e({type:\"string\",format:\"xid\",check:\"string_format\",abort:!1,...A(t)})}function ro(e,t){return new e({type:\"string\",format:\"ksuid\",check:\"string_format\",abort:!1,...A(t)})}function no(e,t){return new e({type:\"string\",format:\"ipv4\",check:\"string_format\",abort:!1,...A(t)})}function ao(e,t){return new e({type:\"string\",format:\"ipv6\",check:\"string_format\",abort:!1,...A(t)})}function oo(e,t){return new e({type:\"string\",format:\"cidrv4\",check:\"string_format\",abort:!1,...A(t)})}function io(e,t){return new e({type:\"string\",format:\"cidrv6\",check:\"string_format\",abort:!1,...A(t)})}function so(e,t){return new e({type:\"string\",format:\"base64\",check:\"string_format\",abort:!1,...A(t)})}function lo(e,t){return new e({type:\"string\",format:\"base64url\",check:\"string_format\",abort:!1,...A(t)})}function co(e,t){return new e({type:\"string\",format:\"e164\",check:\"string_format\",abort:!1,...A(t)})}function uo(e,t){return new e({type:\"string\",format:\"jwt\",check:\"string_format\",abort:!1,...A(t)})}const po={Any:null,Minute:-1,Second:0,Millisecond:3,Microsecond:6};function ho(e,t){return new e({type:\"string\",format:\"datetime\",check:\"string_format\",offset:!1,local:!1,precision:null,...A(t)})}function fo(e,t){return new e({type:\"string\",format:\"date\",check:\"string_format\",...A(t)})}function mo(e,t){return new e({type:\"string\",format:\"time\",check:\"string_format\",precision:null,...A(t)})}function go(e,t){return new e({type:\"string\",format:\"duration\",check:\"string_format\",...A(t)})}function vo(e,t){return new e({type:\"number\",checks:[],...A(t)})}function bo(e,t){return new e({type:\"number\",coerce:!0,checks:[],...A(t)})}function yo(e,t){return new e({type:\"number\",check:\"number_format\",abort:!1,format:\"safeint\",...A(t)})}function Oo(e,t){return new e({type:\"number\",check:\"number_format\",abort:!1,format:\"float32\",...A(t)})}function wo(e,t){return new e({type:\"number\",check:\"number_format\",abort:!1,format:\"float64\",...A(t)})}function xo(e,t){return new e({type:\"number\",check:\"number_format\",abort:!1,format:\"int32\",...A(t)})}function ko(e,t){return new e({type:\"number\",check:\"number_format\",abort:!1,format:\"uint32\",...A(t)})}function So(e,t){return new e({type:\"boolean\",...A(t)})}function _o(e,t){return new e({type:\"boolean\",coerce:!0,...A(t)})}function Eo(e,t){return new e({type:\"bigint\",...A(t)})}function To(e,t){return new e({type:\"bigint\",coerce:!0,...A(t)})}function Ao(e,t){return new e({type:\"bigint\",check:\"bigint_format\",abort:!1,format:\"int64\",...A(t)})}function $o(e,t){return new e({type:\"bigint\",check:\"bigint_format\",abort:!1,format:\"uint64\",...A(t)})}function Co(e,t){return new e({type:\"symbol\",...A(t)})}function Po(e,t){return new e({type:\"undefined\",...A(t)})}function Do(e,t){return new e({type:\"null\",...A(t)})}function Io(e){return new e({type:\"any\"})}function Mo(e){return new e({type:\"unknown\"})}function No(e,t){return new e({type:\"never\",...A(t)})}function Ro(e,t){return new e({type:\"void\",...A(t)})}function jo(e,t){return new e({type:\"date\",...A(t)})}function Lo(e,t){return new e({type:\"date\",coerce:!0,...A(t)})}function Uo(e,t){return new e({type:\"nan\",...A(t)})}function Bo(e,t){return new $t({check:\"less_than\",...A(t),value:e,inclusive:!1})}function zo(e,t){return new $t({check:\"less_than\",...A(t),value:e,inclusive:!0})}function Fo(e,t){return new Ct({check:\"greater_than\",...A(t),value:e,inclusive:!1})}function Qo(e,t){return new Ct({check:\"greater_than\",...A(t),value:e,inclusive:!0})}function qo(e){return Fo(0,e)}function Zo(e){return Bo(0,e)}function Vo(e){return zo(0,e)}function Ho(e){return Qo(0,e)}function Wo(e,t){return new Pt({check:\"multiple_of\",...A(t),value:e})}function Xo(e,t){return new Mt({check:\"max_size\",...A(t),maximum:e})}function Go(e,t){return new Nt({check:\"min_size\",...A(t),minimum:e})}function Yo(e,t){return new Rt({check:\"size_equals\",...A(t),size:e})}function Ko(e,t){return new jt({check:\"max_length\",...A(t),maximum:e})}function Jo(e,t){return new Lt({check:\"min_length\",...A(t),minimum:e})}function ei(e,t){return new Ut({check:\"length_equals\",...A(t),length:e})}function ti(e,t){return new zt({check:\"string_format\",format:\"regex\",...A(t),pattern:e})}function ri(e){return new Ft({check:\"string_format\",format:\"lowercase\",...A(e)})}function ni(e){return new Qt({check:\"string_format\",format:\"uppercase\",...A(e)})}function ai(e,t){return new qt({check:\"string_format\",format:\"includes\",...A(t),includes:e})}function oi(e,t){return new Zt({check:\"string_format\",format:\"starts_with\",...A(t),prefix:e})}function ii(e,t){return new Vt({check:\"string_format\",format:\"ends_with\",...A(t),suffix:e})}function si(e,t,r){return new Wt({check:\"property\",property:e,schema:t,...A(r)})}function li(e,t){return new Xt({check:\"mime_type\",mime:e,...A(t)})}function ci(e){return new Gt({check:\"overwrite\",tx:e})}function ui(e){return ci((t=>t.normalize(e)))}function di(){return ci((e=>e.trim()))}function pi(){return ci((e=>e.toLowerCase()))}function hi(){return ci((e=>e.toUpperCase()))}function fi(e,t,r){return new e({type:\"array\",element:t,...A(r)})}function mi(e,t){return new e({type:\"file\",...A(t)})}function gi(e,t,r){const n=A(r);return n.abort??(n.abort=!0),new e({type:\"custom\",check:\"custom\",fn:t,...n})}function vi(e,t,r){return new e({type:\"custom\",check:\"custom\",fn:t,...A(r)})}function bi(e){const t=yi((r=>(r.addIssue=e=>{if(\"string\"==typeof e)r.issues.push(V(e,r.value,t._zod.def));else{const n=e;n.fatal&&(n.continue=!1),n.code??(n.code=\"custom\"),n.input??(n.input=r.value),n.inst??(n.inst=t),n.continue??(n.continue=!t._zod.def.abort),r.issues.push(V(n))}},e(r.value,r))));return t}function yi(e,t){const r=new Tt({check:\"custom\",...A(t)});return r._zod.check=e,r}function Oi(e,t){const r=A(t);let n=r.truthy??[\"true\",\"1\",\"yes\",\"on\",\"y\",\"enabled\"],a=r.falsy??[\"false\",\"0\",\"no\",\"off\",\"n\",\"disabled\"];\"sensitive\"!==r.case&&(n=n.map((e=>\"string\"==typeof e?e.toLowerCase():e)),a=a.map((e=>\"string\"==typeof e?e.toLowerCase():e)));const o=new Set(n),i=new Set(a),s=e.Codec??En,l=e.Boolean??Pr,c=new s({type:\"pipe\",in:new(e.String??er)({type:\"string\",error:r.error}),out:new l({type:\"boolean\",error:r.error}),transform:(e,t)=>{let n=e;return\"sensitive\"!==r.case&&(n=n.toLowerCase()),!!o.has(n)||!i.has(n)&&(t.issues.push({code:\"invalid_value\",expected:\"stringbool\",values:[...o,...i],input:t.value,inst:c,continue:!1}),{})},reverseTransform:(e,t)=>!0===e?n[0]||\"true\":a[0]||\"false\",error:r.error});return c}function wi(e,t,r,n={}){const a=A(n),o={...A(n),check:\"string_format\",type:\"string\",format:t,fn:\"function\"==typeof r?r:e=>r.test(e),...a};return r instanceof RegExp&&(o.pattern=r),new e(o)}class xi{constructor(e){this.counter=0,this.metadataRegistry=e?.metadata??Ba,this.target=e?.target??\"draft-2020-12\",this.unrepresentable=e?.unrepresentable??\"throw\",this.override=e?.override??(()=>{}),this.io=e?.io??\"output\",this.seen=new Map}process(e,t={path:[],schemaPath:[]}){var r;const n=e._zod.def,a={guid:\"uuid\",url:\"uri\",datetime:\"date-time\",json_string:\"json-string\",regex:\"\"},o=this.seen.get(e);if(o)return o.count++,t.schemaPath.includes(e)&&(o.cycle=t.path),o.schema;const i={schema:{},count:1,cycle:void 0,path:t.path};this.seen.set(e,i);const l=e._zod.toJSONSchema?.();if(l)i.schema=l;else{const r={...t,schemaPath:[...t.schemaPath,e],path:t.path},o=e._zod.parent;if(o)i.ref=o,this.process(o,r),this.seen.get(o).isParent=!0;else{const t=i.schema;switch(n.type){case\"string\":{const r=t;r.type=\"string\";const{minimum:n,maximum:o,format:s,patterns:l,contentEncoding:c}=e._zod.bag;if(\"number\"==typeof n&&(r.minLength=n),\"number\"==typeof o&&(r.maxLength=o),s&&(r.format=a[s]??s,\"\"===r.format&&delete r.format),c&&(r.contentEncoding=c),l&&l.size>0){const e=[...l];1===e.length?r.pattern=e[0].source:e.length>1&&(i.schema.allOf=[...e.map((e=>({...\"draft-7\"===this.target||\"draft-4\"===this.target||\"openapi-3.0\"===this.target?{type:\"string\"}:{},pattern:e.source})))])}break}case\"number\":{const r=t,{minimum:n,maximum:a,format:o,multipleOf:i,exclusiveMaximum:s,exclusiveMinimum:l}=e._zod.bag;\"string\"==typeof o&&o.includes(\"int\")?r.type=\"integer\":r.type=\"number\",\"number\"==typeof l&&(\"draft-4\"===this.target||\"openapi-3.0\"===this.target?(r.minimum=l,r.exclusiveMinimum=!0):r.exclusiveMinimum=l),\"number\"==typeof n&&(r.minimum=n,\"number\"==typeof l&&\"draft-4\"!==this.target&&(l>=n?delete r.minimum:delete r.exclusiveMinimum)),\"number\"==typeof s&&(\"draft-4\"===this.target||\"openapi-3.0\"===this.target?(r.maximum=s,r.exclusiveMaximum=!0):r.exclusiveMaximum=s),\"number\"==typeof a&&(r.maximum=a,\"number\"==typeof s&&\"draft-4\"!==this.target&&(s<=a?delete r.maximum:delete r.exclusiveMaximum)),\"number\"==typeof i&&(r.multipleOf=i);break}case\"boolean\":case\"success\":t.type=\"boolean\";break;case\"bigint\":if(\"throw\"===this.unrepresentable)throw new Error(\"BigInt cannot be represented in JSON Schema\");break;case\"symbol\":if(\"throw\"===this.unrepresentable)throw new Error(\"Symbols cannot be represented in JSON Schema\");break;case\"null\":\"openapi-3.0\"===this.target?(t.type=\"string\",t.nullable=!0,t.enum=[null]):t.type=\"null\";break;case\"any\":case\"unknown\":break;case\"undefined\":if(\"throw\"===this.unrepresentable)throw new Error(\"Undefined cannot be represented in JSON Schema\");break;case\"void\":if(\"throw\"===this.unrepresentable)throw new Error(\"Void cannot be represented in JSON Schema\");break;case\"never\":t.not={};break;case\"date\":if(\"throw\"===this.unrepresentable)throw new Error(\"Date cannot be represented in JSON Schema\");break;case\"array\":{const a=t,{minimum:o,maximum:i}=e._zod.bag;\"number\"==typeof o&&(a.minItems=o),\"number\"==typeof i&&(a.maxItems=i),a.type=\"array\",a.items=this.process(n.element,{...r,path:[...r.path,\"items\"]});break}case\"object\":{const e=t;e.type=\"object\",e.properties={};const a=n.shape;for(const t in a)e.properties[t]=this.process(a[t],{...r,path:[...r.path,\"properties\",t]});const o=new Set(Object.keys(a)),i=new Set([...o].filter((e=>{const t=n.shape[e]._zod;return\"input\"===this.io?void 0===t.optin:void 0===t.optout})));i.size>0&&(e.required=Array.from(i)),\"never\"===n.catchall?._zod.def.type?e.additionalProperties=!1:n.catchall?n.catchall&&(e.additionalProperties=this.process(n.catchall,{...r,path:[...r.path,\"additionalProperties\"]})):\"output\"===this.io&&(e.additionalProperties=!1);break}case\"union\":{const e=t,a=n.options.map(((e,t)=>this.process(e,{...r,path:[...r.path,\"anyOf\",t]})));e.anyOf=a;break}case\"intersection\":{const e=t,a=this.process(n.left,{...r,path:[...r.path,\"allOf\",0]}),o=this.process(n.right,{...r,path:[...r.path,\"allOf\",1]}),i=e=>\"allOf\"in e&&1===Object.keys(e).length,s=[...i(a)?a.allOf:[a],...i(o)?o.allOf:[o]];e.allOf=s;break}case\"tuple\":{const a=t;a.type=\"array\";const o=\"draft-2020-12\"===this.target?\"prefixItems\":\"items\",i=\"draft-2020-12\"===this.target||\"openapi-3.0\"===this.target?\"items\":\"additionalItems\",s=n.items.map(((e,t)=>this.process(e,{...r,path:[...r.path,o,t]}))),l=n.rest?this.process(n.rest,{...r,path:[...r.path,i,...\"openapi-3.0\"===this.target?[n.items.length]:[]]}):null;\"draft-2020-12\"===this.target?(a.prefixItems=s,l&&(a.items=l)):\"openapi-3.0\"===this.target?(a.items={anyOf:s},l&&a.items.anyOf.push(l),a.minItems=s.length,l||(a.maxItems=s.length)):(a.items=s,l&&(a.additionalItems=l));const{minimum:c,maximum:u}=e._zod.bag;\"number\"==typeof c&&(a.minItems=c),\"number\"==typeof u&&(a.maxItems=u);break}case\"record\":{const e=t;e.type=\"object\",\"draft-7\"!==this.target&&\"draft-2020-12\"!==this.target||(e.propertyNames=this.process(n.keyType,{...r,path:[...r.path,\"propertyNames\"]})),e.additionalProperties=this.process(n.valueType,{...r,path:[...r.path,\"additionalProperties\"]});break}case\"map\":if(\"throw\"===this.unrepresentable)throw new Error(\"Map cannot be represented in JSON Schema\");break;case\"set\":if(\"throw\"===this.unrepresentable)throw new Error(\"Set cannot be represented in JSON Schema\");break;case\"enum\":{const e=t,r=s(n.entries);r.every((e=>\"number\"==typeof e))&&(e.type=\"number\"),r.every((e=>\"string\"==typeof e))&&(e.type=\"string\"),e.enum=r;break}case\"literal\":{const e=t,r=[];for(const e of n.values)if(void 0===e){if(\"throw\"===this.unrepresentable)throw new Error(\"Literal `undefined` cannot be represented in JSON Schema\")}else if(\"bigint\"==typeof e){if(\"throw\"===this.unrepresentable)throw new Error(\"BigInt literals cannot be represented in JSON Schema\");r.push(Number(e))}else r.push(e);if(0===r.length);else if(1===r.length){const t=r[0];e.type=null===t?\"null\":typeof t,\"draft-4\"===this.target||\"openapi-3.0\"===this.target?e.enum=[t]:e.const=t}else r.every((e=>\"number\"==typeof e))&&(e.type=\"number\"),r.every((e=>\"string\"==typeof e))&&(e.type=\"string\"),r.every((e=>\"boolean\"==typeof e))&&(e.type=\"string\"),r.every((e=>null===e))&&(e.type=\"null\"),e.enum=r;break}case\"file\":{const r=t,n={type:\"string\",format:\"binary\",contentEncoding:\"binary\"},{minimum:a,maximum:o,mime:i}=e._zod.bag;void 0!==a&&(n.minLength=a),void 0!==o&&(n.maxLength=o),i?1===i.length?(n.contentMediaType=i[0],Object.assign(r,n)):r.anyOf=i.map((e=>({...n,contentMediaType:e}))):Object.assign(r,n);break}case\"transform\":if(\"throw\"===this.unrepresentable)throw new Error(\"Transforms cannot be represented in JSON Schema\");break;case\"nullable\":{const e=this.process(n.innerType,r);\"openapi-3.0\"===this.target?(i.ref=n.innerType,t.nullable=!0):t.anyOf=[e,{type:\"null\"}];break}case\"nonoptional\":case\"promise\":case\"optional\":this.process(n.innerType,r),i.ref=n.innerType;break;case\"default\":this.process(n.innerType,r),i.ref=n.innerType,t.default=JSON.parse(JSON.stringify(n.defaultValue));break;case\"prefault\":this.process(n.innerType,r),i.ref=n.innerType,\"input\"===this.io&&(t._prefault=JSON.parse(JSON.stringify(n.defaultValue)));break;case\"catch\":{let e;this.process(n.innerType,r),i.ref=n.innerType;try{e=n.catchValue(void 0)}catch{throw new Error(\"Dynamic catch values are not supported in JSON Schema\")}t.default=e;break}case\"nan\":if(\"throw\"===this.unrepresentable)throw new Error(\"NaN cannot be represented in JSON Schema\");break;case\"template_literal\":{const r=t,n=e._zod.pattern;if(!n)throw new Error(\"Pattern not found in template literal\");r.type=\"string\",r.pattern=n.source;break}case\"pipe\":{const e=\"input\"===this.io?\"transform\"===n.in._zod.def.type?n.out:n.in:n.out;this.process(e,r),i.ref=e;break}case\"readonly\":this.process(n.innerType,r),i.ref=n.innerType,t.readOnly=!0;break;case\"lazy\":{const t=e._zod.innerType;this.process(t,r),i.ref=t;break}case\"custom\":if(\"throw\"===this.unrepresentable)throw new Error(\"Custom types cannot be represented in JSON Schema\");break;case\"function\":if(\"throw\"===this.unrepresentable)throw new Error(\"Function types cannot be represented in JSON Schema\")}}}const c=this.metadataRegistry.get(e);return c&&Object.assign(i.schema,c),\"input\"===this.io&&Si(e)&&(delete i.schema.examples,delete i.schema.default),\"input\"===this.io&&i.schema._prefault&&((r=i.schema).default??(r.default=i.schema._prefault)),delete i.schema._prefault,this.seen.get(e).schema}emit(e,t){const r={cycles:t?.cycles??\"ref\",reused:t?.reused??\"inline\",external:t?.external??void 0},n=this.seen.get(e);if(!n)throw new Error(\"Unprocessed schema. This is a bug in Zod.\");const a=e=>{const t=\"draft-2020-12\"===this.target?\"$defs\":\"definitions\";if(r.external){const n=r.external.registry.get(e[0])?.id,a=r.external.uri??(e=>e);if(n)return{ref:a(n)};const o=e[1].defId??e[1].schema.id??\"schema\"+this.counter++;return e[1].defId=o,{defId:o,ref:`${a(\"__shared\")}#/${t}/${o}`}}if(e[1]===n)return{ref:\"#\"};const a=`#/${t}/`,o=e[1].schema.id??\"__schema\"+this.counter++;return{defId:o,ref:a+o}},o=e=>{if(e[1].schema.$ref)return;const t=e[1],{ref:r,defId:n}=a(e);t.def={...t.schema},n&&(t.defId=n);const o=t.schema;for(const e in o)delete o[e];o.$ref=r};if(\"throw\"===r.cycles)for(const e of this.seen.entries()){const t=e[1];if(t.cycle)throw new Error(`Cycle detected: #/${t.cycle?.join(\"/\")}/\\n\\nSet the \\`cycles\\` parameter to \\`\"ref\"\\` to resolve cyclical schemas with defs.`)}for(const t of this.seen.entries()){const n=t[1];if(e===t[0]){o(t);continue}if(r.external){const n=r.external.registry.get(t[0])?.id;if(e!==t[0]&&n){o(t);continue}}const a=this.metadataRegistry.get(t[0])?.id;(a||n.cycle||n.count>1&&\"ref\"===r.reused)&&o(t)}const i=(e,t)=>{const r=this.seen.get(e),n=r.def??r.schema,a={...n};if(null===r.ref)return;const o=r.ref;if(r.ref=null,o){i(o,t);const e=this.seen.get(o).schema;!e.$ref||\"draft-7\"!==t.target&&\"draft-4\"!==t.target&&\"openapi-3.0\"!==t.target?(Object.assign(n,e),Object.assign(n,a)):(n.allOf=n.allOf??[],n.allOf.push(e))}r.isParent||this.override({zodSchema:e,jsonSchema:n,path:r.path??[]})};for(const e of[...this.seen.entries()].reverse())i(e[0],{target:this.target});const s={};if(\"draft-2020-12\"===this.target?s.$schema=\"https://json-schema.org/draft/2020-12/schema\":\"draft-7\"===this.target?s.$schema=\"http://json-schema.org/draft-07/schema#\":\"draft-4\"===this.target?s.$schema=\"http://json-schema.org/draft-04/schema#\":\"openapi-3.0\"===this.target||console.warn(`Invalid target: ${this.target}`),r.external?.uri){const t=r.external.registry.get(e)?.id;if(!t)throw new Error(\"Schema is missing an `id` property\");s.$id=r.external.uri(t)}Object.assign(s,n.def);const l=r.external?.defs??{};for(const e of this.seen.entries()){const t=e[1];t.def&&t.defId&&(l[t.defId]=t.def)}r.external||Object.keys(l).length>0&&(\"draft-2020-12\"===this.target?s.$defs=l:s.definitions=l);try{return JSON.parse(JSON.stringify(s))}catch(e){throw new Error(\"Error converting schema to JSON.\")}}}function ki(e,t){if(e instanceof La){const r=new xi(t),n={};for(const t of e._idmap.entries()){const[e,n]=t;r.process(n)}const a={},o={registry:e,uri:t?.uri,defs:n};for(const n of e._idmap.entries()){const[e,i]=n;a[e]=r.emit(i,{...t,external:o})}if(Object.keys(n).length>0){const e=\"draft-2020-12\"===r.target?\"$defs\":\"definitions\";a.__shared={[e]:n}}return{schemas:a}}const r=new xi(t);return r.process(e),r.emit(e,t)}function Si(e,t){const r=t??{seen:new Set};if(r.seen.has(e))return!1;r.seen.add(e);const n=e._zod.def;switch(n.type){case\"string\":case\"number\":case\"bigint\":case\"boolean\":case\"date\":case\"symbol\":case\"undefined\":case\"null\":case\"any\":case\"unknown\":case\"never\":case\"void\":case\"literal\":case\"enum\":case\"nan\":case\"file\":case\"template_literal\":case\"custom\":case\"success\":case\"catch\":case\"function\":return!1;case\"array\":return Si(n.element,r);case\"object\":for(const e in n.shape)if(Si(n.shape[e],r))return!0;return!1;case\"union\":for(const e of n.options)if(Si(e,r))return!0;return!1;case\"intersection\":return Si(n.left,r)||Si(n.right,r);case\"tuple\":for(const e of n.items)if(Si(e,r))return!0;return!(!n.rest||!Si(n.rest,r));case\"record\":case\"map\":return Si(n.keyType,r)||Si(n.valueType,r);case\"set\":return Si(n.valueType,r);case\"promise\":case\"optional\":case\"nonoptional\":case\"nullable\":case\"readonly\":case\"default\":case\"prefault\":return Si(n.innerType,r);case\"lazy\":return Si(n.getter(),r);case\"transform\":return!0;case\"pipe\":return Si(n.in,r)||Si(n.out,r)}throw new Error(`Unknown schema type: ${n.type}`)}const _i=Object.freeze(Object.defineProperty({__proto__:null},Symbol.toStringTag,{value:\"Module\"})),Ei=Object.freeze(Object.defineProperty({__proto__:null,$ZodAny:jr,$ZodArray:Qr,$ZodAsyncError:n,$ZodBase64:xr,$ZodBase64URL:Sr,$ZodBigInt:Dr,$ZodBigIntFormat:Ir,$ZodBoolean:Pr,$ZodCIDRv4:yr,$ZodCIDRv6:Or,$ZodCUID:lr,$ZodCUID2:cr,$ZodCatch:xn,$ZodCheck:Tt,$ZodCheckBigIntFormat:It,$ZodCheckEndsWith:Vt,$ZodCheckGreaterThan:Ct,$ZodCheckIncludes:qt,$ZodCheckLengthEquals:Ut,$ZodCheckLessThan:$t,$ZodCheckLowerCase:Ft,$ZodCheckMaxLength:jt,$ZodCheckMaxSize:Mt,$ZodCheckMimeType:Xt,$ZodCheckMinLength:Lt,$ZodCheckMinSize:Nt,$ZodCheckMultipleOf:Pt,$ZodCheckNumberFormat:Dt,$ZodCheckOverwrite:Gt,$ZodCheckProperty:Wt,$ZodCheckRegex:zt,$ZodCheckSizeEquals:Rt,$ZodCheckStartsWith:Zt,$ZodCheckStringFormat:Bt,$ZodCheckUpperCase:Qt,$ZodCodec:En,$ZodCustom:Nn,$ZodCustomStringFormat:Ar,$ZodDate:zr,$ZodDefault:gn,$ZodDiscriminatedUnion:Yr,$ZodE164:_r,$ZodEmail:ar,$ZodEmoji:ir,$ZodEncodeError:a,$ZodEnum:cn,$ZodError:Y,$ZodFile:dn,$ZodFunction:Dn,$ZodGUID:rr,$ZodIPv4:vr,$ZodIPv6:br,$ZodISODate:fr,$ZodISODateTime:hr,$ZodISODuration:gr,$ZodISOTime:mr,$ZodIntersection:Kr,$ZodJWT:Tr,$ZodKSUID:pr,$ZodLazy:Mn,$ZodLiteral:un,$ZodMap:an,$ZodNaN:kn,$ZodNanoID:sr,$ZodNever:Ur,$ZodNonOptional:yn,$ZodNull:Rr,$ZodNullable:mn,$ZodNumber:$r,$ZodNumberFormat:Cr,$ZodObject:Hr,$ZodObjectJIT:Wr,$ZodOptional:fn,$ZodPipe:Sn,$ZodPrefault:bn,$ZodPromise:In,$ZodReadonly:$n,$ZodRealError:K,$ZodRecord:nn,$ZodRegistry:La,$ZodSet:sn,$ZodString:er,$ZodStringFormat:tr,$ZodSuccess:wn,$ZodSymbol:Mr,$ZodTemplateLiteral:Pn,$ZodTransform:pn,$ZodTuple:tn,$ZodType:Jt,$ZodULID:ur,$ZodURL:or,$ZodUUID:nr,$ZodUndefined:Nr,$ZodUnion:Gr,$ZodUnknown:Lr,$ZodVoid:Br,$ZodXID:dr,$brand:r,$constructor:t,$input:ja,$output:Ra,Doc:Yt,JSONSchema:_i,JSONSchemaGenerator:xi,NEVER:e,TimePrecision:po,_any:Io,_array:fi,_base64:so,_base64url:lo,_bigint:Eo,_boolean:So,_catch:function(e,t,r){return new e({type:\"catch\",innerType:t,catchValue:\"function\"==typeof r?r:()=>r})},_check:yi,_cidrv4:oo,_cidrv6:io,_coercedBigint:To,_coercedBoolean:_o,_coercedDate:Lo,_coercedNumber:bo,_coercedString:Fa,_cuid:Ka,_cuid2:Ja,_custom:gi,_date:jo,_decode:fe,_decodeAsync:be,_default:function(e,t,r){return new e({type:\"default\",innerType:t,get defaultValue(){return\"function\"==typeof r?r():k(r)}})},_discriminatedUnion:function(e,t,r,n){return new e({type:\"union\",options:r,discriminator:t,...A(n)})},_e164:co,_email:Qa,_emoji:Ga,_encode:pe,_encodeAsync:ge,_endsWith:ii,_enum:function(e,t,r){return new e({type:\"enum\",entries:Array.isArray(t)?Object.fromEntries(t.map((e=>[e,e]))):t,...A(r)})},_file:mi,_float32:Oo,_float64:wo,_gt:Fo,_gte:Qo,_guid:qa,_includes:ai,_int:yo,_int32:xo,_int64:Ao,_intersection:function(e,t,r){return new e({type:\"intersection\",left:t,right:r})},_ipv4:no,_ipv6:ao,_isoDate:fo,_isoDateTime:ho,_isoDuration:go,_isoTime:mo,_jwt:uo,_ksuid:ro,_lazy:function(e,t){return new e({type:\"lazy\",getter:t})},_length:ei,_literal:function(e,t,r){return new e({type:\"literal\",values:Array.isArray(t)?t:[t],...A(r)})},_lowercase:ri,_lt:Bo,_lte:zo,_map:function(e,t,r,n){return new e({type:\"map\",keyType:t,valueType:r,...A(n)})},_max:zo,_maxLength:Ko,_maxSize:Xo,_mime:li,_min:Qo,_minLength:Jo,_minSize:Go,_multipleOf:Wo,_nan:Uo,_nanoid:Ya,_nativeEnum:function(e,t,r){return new e({type:\"enum\",entries:t,...A(r)})},_negative:Zo,_never:No,_nonnegative:Ho,_nonoptional:function(e,t,r){return new e({type:\"nonoptional\",innerType:t,...A(r)})},_nonpositive:Vo,_normalize:ui,_null:Do,_nullable:function(e,t){return new e({type:\"nullable\",innerType:t})},_number:vo,_optional:function(e,t){return new e({type:\"optional\",innerType:t})},_overwrite:ci,_parse:ae,_parseAsync:ie,_pipe:function(e,t,r){return new e({type:\"pipe\",in:t,out:r})},_positive:qo,_promise:function(e,t){return new e({type:\"promise\",innerType:t})},_property:si,_readonly:function(e,t){return new e({type:\"readonly\",innerType:t})},_record:function(e,t,r,n){return new e({type:\"record\",keyType:t,valueType:r,...A(n)})},_refine:vi,_regex:ti,_safeDecode:xe,_safeDecodeAsync:Ee,_safeEncode:Oe,_safeEncodeAsync:Se,_safeParse:le,_safeParseAsync:ue,_set:function(e,t,r){return new e({type:\"set\",valueType:t,...A(r)})},_size:Yo,_startsWith:oi,_string:za,_stringFormat:wi,_stringbool:Oi,_success:function(e,t){return new e({type:\"success\",innerType:t})},_superRefine:bi,_symbol:Co,_templateLiteral:function(e,t,r){return new e({type:\"template_literal\",parts:t,...A(r)})},_toLowerCase:pi,_toUpperCase:hi,_transform:function(e,t){return new e({type:\"transform\",transform:t})},_trim:di,_tuple:function(e,t,r,n){const a=r instanceof Jt;return new e({type:\"tuple\",items:t,rest:a?r:null,...A(a?n:r)})},_uint32:ko,_uint64:$o,_ulid:eo,_undefined:Po,_union:function(e,t,r){return new e({type:\"union\",options:t,...A(r)})},_unknown:Mo,_uppercase:ni,_url:Xa,_uuid:Za,_uuidv4:Va,_uuidv6:Ha,_uuidv7:Wa,_void:Ro,_xid:to,clone:T,config:i,decode:me,decodeAsync:ye,encode:he,encodeAsync:ve,flattenError:J,formatError:ee,globalConfig:o,globalRegistry:Ba,isValidBase64:wr,isValidBase64URL:kr,isValidJWT:Er,locales:Na,parse:oe,parseAsync:se,prettifyError:ne,regexes:Et,registry:Ua,safeDecode:ke,safeDecodeAsync:Te,safeEncode:we,safeEncodeAsync:_e,safeParse:ce,safeParseAsync:de,toDotPath:re,toJSONSchema:ki,treeifyError:te,util:X,version:Kt},Symbol.toStringTag,{value:\"Module\"})),Ti=t(\"ZodISODateTime\",((e,t)=>{hr.init(e,t),ts.init(e,t)}));function Ai(e){return ho(Ti,e)}const $i=t(\"ZodISODate\",((e,t)=>{fr.init(e,t),ts.init(e,t)}));function Ci(e){return fo($i,e)}const Pi=t(\"ZodISOTime\",((e,t)=>{mr.init(e,t),ts.init(e,t)}));function Di(e){return mo(Pi,e)}const Ii=t(\"ZodISODuration\",((e,t)=>{gr.init(e,t),ts.init(e,t)}));function Mi(e){return go(Ii,e)}const Ni=Object.freeze(Object.defineProperty({__proto__:null,ZodISODate:$i,ZodISODateTime:Ti,ZodISODuration:Ii,ZodISOTime:Pi,date:Ci,datetime:Ai,duration:Mi,time:Di},Symbol.toStringTag,{value:\"Module\"})),Ri=(e,t)=>{Y.init(e,t),e.name=\"ZodError\",Object.defineProperties(e,{format:{value:t=>ee(e,t)},flatten:{value:t=>J(e,t)},addIssue:{value:t=>{e.issues.push(t),e.message=JSON.stringify(e.issues,c,2)}},addIssues:{value:t=>{e.issues.push(...t),e.message=JSON.stringify(e.issues,c,2)}},isEmpty:{get:()=>0===e.issues.length}})},ji=t(\"ZodError\",Ri),Li=t(\"ZodError\",Ri,{Parent:Error}),Ui=ae(Li),Bi=ie(Li),zi=le(Li),Fi=ue(Li),Qi=pe(Li),qi=fe(Li),Zi=ge(Li),Vi=be(Li),Hi=Oe(Li),Wi=xe(Li),Xi=Se(Li),Gi=Ee(Li),Yi=t(\"ZodType\",((e,t)=>(Jt.init(e,t),e.def=t,e.type=t.type,Object.defineProperty(e,\"_def\",{value:t}),e.check=(...r)=>e.clone(v(t,{checks:[...t.checks??[],...r.map((e=>\"function\"==typeof e?{_zod:{check:e,def:{check:\"custom\"},onattach:[]}}:e))]})),e.clone=(t,r)=>T(e,t,r),e.brand=()=>e,e.register=(t,r)=>(t.add(e,r),e),e.parse=(t,r)=>Ui(e,t,r,{callee:e.parse}),e.safeParse=(t,r)=>zi(e,t,r),e.parseAsync=async(t,r)=>Bi(e,t,r,{callee:e.parseAsync}),e.safeParseAsync=async(t,r)=>Fi(e,t,r),e.spa=e.safeParseAsync,e.encode=(t,r)=>Qi(e,t,r),e.decode=(t,r)=>qi(e,t,r),e.encodeAsync=async(t,r)=>Zi(e,t,r),e.decodeAsync=async(t,r)=>Vi(e,t,r),e.safeEncode=(t,r)=>Hi(e,t,r),e.safeDecode=(t,r)=>Wi(e,t,r),e.safeEncodeAsync=async(t,r)=>Xi(e,t,r),e.safeDecodeAsync=async(t,r)=>Gi(e,t,r),e.refine=(t,r)=>e.check(zl(t,r)),e.superRefine=t=>e.check(Fl(t)),e.overwrite=t=>e.check(ci(t)),e.optional=()=>ml(e),e.nullable=()=>vl(e),e.nullish=()=>ml(vl(e)),e.nonoptional=t=>kl(e,t),e.array=()=>Zs(e),e.or=t=>Xs([e,t]),e.and=t=>Js(e,t),e.transform=t=>$l(e,hl(t)),e.default=t=>yl(e,t),e.prefault=t=>wl(e,t),e.catch=t=>El(e,t),e.pipe=t=>$l(e,t),e.readonly=()=>Dl(e),e.describe=t=>{const r=e.clone();return Ba.add(r,{description:t}),r},Object.defineProperty(e,\"description\",{get:()=>Ba.get(e)?.description,configurable:!0}),e.meta=(...t)=>{if(0===t.length)return Ba.get(e);const r=e.clone();return Ba.add(r,t[0]),r},e.isOptional=()=>e.safeParse(void 0).success,e.isNullable=()=>e.safeParse(null).success,e))),Ki=t(\"_ZodString\",((e,t)=>{er.init(e,t),Yi.init(e,t);const r=e._zod.bag;e.format=r.format??null,e.minLength=r.minimum??null,e.maxLength=r.maximum??null,e.regex=(...t)=>e.check(ti(...t)),e.includes=(...t)=>e.check(ai(...t)),e.startsWith=(...t)=>e.check(oi(...t)),e.endsWith=(...t)=>e.check(ii(...t)),e.min=(...t)=>e.check(Jo(...t)),e.max=(...t)=>e.check(Ko(...t)),e.length=(...t)=>e.check(ei(...t)),e.nonempty=(...t)=>e.check(Jo(1,...t)),e.lowercase=t=>e.check(ri(t)),e.uppercase=t=>e.check(ni(t)),e.trim=()=>e.check(di()),e.normalize=(...t)=>e.check(ui(...t)),e.toLowerCase=()=>e.check(pi()),e.toUpperCase=()=>e.check(hi())})),Ji=t(\"ZodString\",((e,t)=>{er.init(e,t),Ki.init(e,t),e.email=t=>e.check(Qa(rs,t)),e.url=t=>e.check(Xa(os,t)),e.jwt=t=>e.check(uo(Os,t)),e.emoji=t=>e.check(Ga(is,t)),e.guid=t=>e.check(qa(ns,t)),e.uuid=t=>e.check(Za(as,t)),e.uuidv4=t=>e.check(Va(as,t)),e.uuidv6=t=>e.check(Ha(as,t)),e.uuidv7=t=>e.check(Wa(as,t)),e.nanoid=t=>e.check(Ya(ss,t)),e.guid=t=>e.check(qa(ns,t)),e.cuid=t=>e.check(Ka(ls,t)),e.cuid2=t=>e.check(Ja(cs,t)),e.ulid=t=>e.check(eo(us,t)),e.base64=t=>e.check(so(vs,t)),e.base64url=t=>e.check(lo(bs,t)),e.xid=t=>e.check(to(ds,t)),e.ksuid=t=>e.check(ro(ps,t)),e.ipv4=t=>e.check(no(hs,t)),e.ipv6=t=>e.check(ao(fs,t)),e.cidrv4=t=>e.check(oo(ms,t)),e.cidrv6=t=>e.check(io(gs,t)),e.e164=t=>e.check(co(ys,t)),e.datetime=t=>e.check(Ai(t)),e.date=t=>e.check(Ci(t)),e.time=t=>e.check(Di(t)),e.duration=t=>e.check(Mi(t))}));function es(e){return za(Ji,e)}const ts=t(\"ZodStringFormat\",((e,t)=>{tr.init(e,t),Ki.init(e,t)})),rs=t(\"ZodEmail\",((e,t)=>{ar.init(e,t),ts.init(e,t)})),ns=t(\"ZodGUID\",((e,t)=>{rr.init(e,t),ts.init(e,t)})),as=t(\"ZodUUID\",((e,t)=>{nr.init(e,t),ts.init(e,t)})),os=t(\"ZodURL\",((e,t)=>{or.init(e,t),ts.init(e,t)})),is=t(\"ZodEmoji\",((e,t)=>{ir.init(e,t),ts.init(e,t)})),ss=t(\"ZodNanoID\",((e,t)=>{sr.init(e,t),ts.init(e,t)})),ls=t(\"ZodCUID\",((e,t)=>{lr.init(e,t),ts.init(e,t)})),cs=t(\"ZodCUID2\",((e,t)=>{cr.init(e,t),ts.init(e,t)})),us=t(\"ZodULID\",((e,t)=>{ur.init(e,t),ts.init(e,t)})),ds=t(\"ZodXID\",((e,t)=>{dr.init(e,t),ts.init(e,t)})),ps=t(\"ZodKSUID\",((e,t)=>{pr.init(e,t),ts.init(e,t)})),hs=t(\"ZodIPv4\",((e,t)=>{vr.init(e,t),ts.init(e,t)})),fs=t(\"ZodIPv6\",((e,t)=>{br.init(e,t),ts.init(e,t)})),ms=t(\"ZodCIDRv4\",((e,t)=>{yr.init(e,t),ts.init(e,t)})),gs=t(\"ZodCIDRv6\",((e,t)=>{Or.init(e,t),ts.init(e,t)})),vs=t(\"ZodBase64\",((e,t)=>{xr.init(e,t),ts.init(e,t)})),bs=t(\"ZodBase64URL\",((e,t)=>{Sr.init(e,t),ts.init(e,t)})),ys=t(\"ZodE164\",((e,t)=>{_r.init(e,t),ts.init(e,t)})),Os=t(\"ZodJWT\",((e,t)=>{Tr.init(e,t),ts.init(e,t)})),ws=t(\"ZodCustomStringFormat\",((e,t)=>{Ar.init(e,t),ts.init(e,t)})),xs=t(\"ZodNumber\",((e,t)=>{$r.init(e,t),Yi.init(e,t),e.gt=(t,r)=>e.check(Fo(t,r)),e.gte=(t,r)=>e.check(Qo(t,r)),e.min=(t,r)=>e.check(Qo(t,r)),e.lt=(t,r)=>e.check(Bo(t,r)),e.lte=(t,r)=>e.check(zo(t,r)),e.max=(t,r)=>e.check(zo(t,r)),e.int=t=>e.check(_s(t)),e.safe=t=>e.check(_s(t)),e.positive=t=>e.check(Fo(0,t)),e.nonnegative=t=>e.check(Qo(0,t)),e.negative=t=>e.check(Bo(0,t)),e.nonpositive=t=>e.check(zo(0,t)),e.multipleOf=(t,r)=>e.check(Wo(t,r)),e.step=(t,r)=>e.check(Wo(t,r)),e.finite=()=>e;const r=e._zod.bag;e.minValue=Math.max(r.minimum??Number.NEGATIVE_INFINITY,r.exclusiveMinimum??Number.NEGATIVE_INFINITY)??null,e.maxValue=Math.min(r.maximum??Number.POSITIVE_INFINITY,r.exclusiveMaximum??Number.POSITIVE_INFINITY)??null,e.isInt=(r.format??\"\").includes(\"int\")||Number.isSafeInteger(r.multipleOf??.5),e.isFinite=!0,e.format=r.format??null}));function ks(e){return vo(xs,e)}const Ss=t(\"ZodNumberFormat\",((e,t)=>{Cr.init(e,t),xs.init(e,t)}));function _s(e){return yo(Ss,e)}const Es=t(\"ZodBoolean\",((e,t)=>{Pr.init(e,t),Yi.init(e,t)}));function Ts(e){return So(Es,e)}const As=t(\"ZodBigInt\",((e,t)=>{Dr.init(e,t),Yi.init(e,t),e.gte=(t,r)=>e.check(Qo(t,r)),e.min=(t,r)=>e.check(Qo(t,r)),e.gt=(t,r)=>e.check(Fo(t,r)),e.gte=(t,r)=>e.check(Qo(t,r)),e.min=(t,r)=>e.check(Qo(t,r)),e.lt=(t,r)=>e.check(Bo(t,r)),e.lte=(t,r)=>e.check(zo(t,r)),e.max=(t,r)=>e.check(zo(t,r)),e.positive=t=>e.check(Fo(BigInt(0),t)),e.negative=t=>e.check(Bo(BigInt(0),t)),e.nonpositive=t=>e.check(zo(BigInt(0),t)),e.nonnegative=t=>e.check(Qo(BigInt(0),t)),e.multipleOf=(t,r)=>e.check(Wo(t,r));const r=e._zod.bag;e.minValue=r.minimum??null,e.maxValue=r.maximum??null,e.format=r.format??null})),$s=t(\"ZodBigIntFormat\",((e,t)=>{Ir.init(e,t),As.init(e,t)})),Cs=t(\"ZodSymbol\",((e,t)=>{Mr.init(e,t),Yi.init(e,t)})),Ps=t(\"ZodUndefined\",((e,t)=>{Nr.init(e,t),Yi.init(e,t)}));function Ds(e){return Po(Ps,e)}const Is=t(\"ZodNull\",((e,t)=>{Rr.init(e,t),Yi.init(e,t)}));function Ms(e){return Do(Is,e)}const Ns=t(\"ZodAny\",((e,t)=>{jr.init(e,t),Yi.init(e,t)}));function Rs(){return Io(Ns)}const js=t(\"ZodUnknown\",((e,t)=>{Lr.init(e,t),Yi.init(e,t)}));function Ls(){return Mo(js)}const Us=t(\"ZodNever\",((e,t)=>{Ur.init(e,t),Yi.init(e,t)}));function Bs(e){return No(Us,e)}const zs=t(\"ZodVoid\",((e,t)=>{Br.init(e,t),Yi.init(e,t)}));function Fs(e){return Ro(zs,e)}const Qs=t(\"ZodDate\",((e,t)=>{zr.init(e,t),Yi.init(e,t),e.min=(t,r)=>e.check(Qo(t,r)),e.max=(t,r)=>e.check(zo(t,r));const r=e._zod.bag;e.minDate=r.minimum?new Date(r.minimum):null,e.maxDate=r.maximum?new Date(r.maximum):null})),qs=t(\"ZodArray\",((e,t)=>{Qr.init(e,t),Yi.init(e,t),e.element=t.element,e.min=(t,r)=>e.check(Jo(t,r)),e.nonempty=t=>e.check(Jo(1,t)),e.max=(t,r)=>e.check(Ko(t,r)),e.length=(t,r)=>e.check(ei(t,r)),e.unwrap=()=>e.element}));function Zs(e,t){return fi(qs,e,t)}const Vs=t(\"ZodObject\",((e,t)=>{Wr.init(e,t),Yi.init(e,t),m(e,\"shape\",(()=>t.shape)),e.keyof=()=>ll(Object.keys(e._zod.def.shape)),e.catchall=t=>e.clone({...e._zod.def,catchall:t}),e.passthrough=()=>e.clone({...e._zod.def,catchall:Ls()}),e.loose=()=>e.clone({...e._zod.def,catchall:Ls()}),e.strict=()=>e.clone({...e._zod.def,catchall:Bs()}),e.strip=()=>e.clone({...e._zod.def,catchall:void 0}),e.extend=t=>N(e,t),e.safeExtend=t=>R(e,t),e.merge=t=>j(e,t),e.pick=t=>I(e,t),e.omit=t=>M(e,t),e.partial=(...t)=>L(fl,e,t[0]),e.required=(...t)=>U(xl,e,t[0])}));function Hs(e,t){const r={type:\"object\",shape:e??{},...A(t)};return new Vs(r)}const Ws=t(\"ZodUnion\",((e,t)=>{Gr.init(e,t),Yi.init(e,t),e.options=t.options}));function Xs(e,t){return new Ws({type:\"union\",options:e,...A(t)})}const Gs=t(\"ZodDiscriminatedUnion\",((e,t)=>{Ws.init(e,t),Yr.init(e,t)}));function Ys(e,t,r){return new Gs({type:\"union\",options:t,discriminator:e,...A(r)})}const Ks=t(\"ZodIntersection\",((e,t)=>{Kr.init(e,t),Yi.init(e,t)}));function Js(e,t){return new Ks({type:\"intersection\",left:e,right:t})}const el=t(\"ZodTuple\",((e,t)=>{tn.init(e,t),Yi.init(e,t),e.rest=t=>e.clone({...e._zod.def,rest:t})}));function tl(e,t,r){const n=t instanceof Jt;return new el({type:\"tuple\",items:e,rest:n?t:null,...A(n?r:t)})}const rl=t(\"ZodRecord\",((e,t)=>{nn.init(e,t),Yi.init(e,t),e.keyType=t.keyType,e.valueType=t.valueType}));function nl(e,t,r){return new rl({type:\"record\",keyType:e,valueType:t,...A(r)})}function al(e,t,r){const n=T(e);return n._zod.values=void 0,new rl({type:\"record\",keyType:n,valueType:t,...A(r)})}const ol=t(\"ZodMap\",((e,t)=>{an.init(e,t),Yi.init(e,t),e.keyType=t.keyType,e.valueType=t.valueType})),il=t(\"ZodSet\",((e,t)=>{sn.init(e,t),Yi.init(e,t),e.min=(...t)=>e.check(Go(...t)),e.nonempty=t=>e.check(Go(1,t)),e.max=(...t)=>e.check(Xo(...t)),e.size=(...t)=>e.check(Yo(...t))})),sl=t(\"ZodEnum\",((e,t)=>{cn.init(e,t),Yi.init(e,t),e.enum=t.entries,e.options=Object.values(t.entries);const r=new Set(Object.keys(t.entries));e.extract=(e,n)=>{const a={};for(const n of e){if(!r.has(n))throw new Error(`Key ${n} not found in enum`);a[n]=t.entries[n]}return new sl({...t,checks:[],...A(n),entries:a})},e.exclude=(e,n)=>{const a={...t.entries};for(const t of e){if(!r.has(t))throw new Error(`Key ${t} not found in enum`);delete a[t]}return new sl({...t,checks:[],...A(n),entries:a})}}));function ll(e,t){const r=Array.isArray(e)?Object.fromEntries(e.map((e=>[e,e]))):e;return new sl({type:\"enum\",entries:r,...A(t)})}const cl=t(\"ZodLiteral\",((e,t)=>{un.init(e,t),Yi.init(e,t),e.values=new Set(t.values),Object.defineProperty(e,\"value\",{get(){if(t.values.length>1)throw new Error(\"This schema contains multiple valid literal values. Use `.values` instead.\");return t.values[0]}})}));function ul(e,t){return new cl({type:\"literal\",values:Array.isArray(e)?e:[e],...A(t)})}const dl=t(\"ZodFile\",((e,t)=>{dn.init(e,t),Yi.init(e,t),e.min=(t,r)=>e.check(Go(t,r)),e.max=(t,r)=>e.check(Xo(t,r)),e.mime=(t,r)=>e.check(li(Array.isArray(t)?t:[t],r))})),pl=t(\"ZodTransform\",((e,t)=>{pn.init(e,t),Yi.init(e,t),e._zod.parse=(r,n)=>{if(\"backward\"===n.direction)throw new a(e.constructor.name);r.addIssue=n=>{if(\"string\"==typeof n)r.issues.push(V(n,r.value,t));else{const t=n;t.fatal&&(t.continue=!1),t.code??(t.code=\"custom\"),t.input??(t.input=r.value),t.inst??(t.inst=e),r.issues.push(V(t))}};const o=t.transform(r.value,r);return o instanceof Promise?o.then((e=>(r.value=e,r))):(r.value=o,r)}}));function hl(e){return new pl({type:\"transform\",transform:e})}const fl=t(\"ZodOptional\",((e,t)=>{fn.init(e,t),Yi.init(e,t),e.unwrap=()=>e._zod.def.innerType}));function ml(e){return new fl({type:\"optional\",innerType:e})}const gl=t(\"ZodNullable\",((e,t)=>{mn.init(e,t),Yi.init(e,t),e.unwrap=()=>e._zod.def.innerType}));function vl(e){return new gl({type:\"nullable\",innerType:e})}const bl=t(\"ZodDefault\",((e,t)=>{gn.init(e,t),Yi.init(e,t),e.unwrap=()=>e._zod.def.innerType,e.removeDefault=e.unwrap}));function yl(e,t){return new bl({type:\"default\",innerType:e,get defaultValue(){return\"function\"==typeof t?t():k(t)}})}const Ol=t(\"ZodPrefault\",((e,t)=>{bn.init(e,t),Yi.init(e,t),e.unwrap=()=>e._zod.def.innerType}));function wl(e,t){return new Ol({type:\"prefault\",innerType:e,get defaultValue(){return\"function\"==typeof t?t():k(t)}})}const xl=t(\"ZodNonOptional\",((e,t)=>{yn.init(e,t),Yi.init(e,t),e.unwrap=()=>e._zod.def.innerType}));function kl(e,t){return new xl({type:\"nonoptional\",innerType:e,...A(t)})}const Sl=t(\"ZodSuccess\",((e,t)=>{wn.init(e,t),Yi.init(e,t),e.unwrap=()=>e._zod.def.innerType})),_l=t(\"ZodCatch\",((e,t)=>{xn.init(e,t),Yi.init(e,t),e.unwrap=()=>e._zod.def.innerType,e.removeCatch=e.unwrap}));function El(e,t){return new _l({type:\"catch\",innerType:e,catchValue:\"function\"==typeof t?t:()=>t})}const Tl=t(\"ZodNaN\",((e,t)=>{kn.init(e,t),Yi.init(e,t)})),Al=t(\"ZodPipe\",((e,t)=>{Sn.init(e,t),Yi.init(e,t),e.in=t.in,e.out=t.out}));function $l(e,t){return new Al({type:\"pipe\",in:e,out:t})}const Cl=t(\"ZodCodec\",((e,t)=>{Al.init(e,t),En.init(e,t)})),Pl=t(\"ZodReadonly\",((e,t)=>{$n.init(e,t),Yi.init(e,t),e.unwrap=()=>e._zod.def.innerType}));function Dl(e){return new Pl({type:\"readonly\",innerType:e})}const Il=t(\"ZodTemplateLiteral\",((e,t)=>{Pn.init(e,t),Yi.init(e,t)})),Ml=t(\"ZodLazy\",((e,t)=>{Mn.init(e,t),Yi.init(e,t),e.unwrap=()=>e._zod.def.getter()}));function Nl(e){return new Ml({type:\"lazy\",getter:e})}const Rl=t(\"ZodPromise\",((e,t)=>{In.init(e,t),Yi.init(e,t),e.unwrap=()=>e._zod.def.innerType})),jl=t(\"ZodFunction\",((e,t)=>{Dn.init(e,t),Yi.init(e,t)}));function Ll(e){return new jl({type:\"function\",input:Array.isArray(e?.input)?tl(e?.input):e?.input??Zs(Ls()),output:e?.output??Ls()})}const Ul=t(\"ZodCustom\",((e,t)=>{Nn.init(e,t),Yi.init(e,t)}));function Bl(e,t){return gi(Ul,e??(()=>!0),t)}function zl(e,t={}){return vi(Ul,e,t)}function Fl(e){return bi(e)}function Ql(e,t={error:`Input not instance of ${e.name}`}){const r=new Ul({type:\"custom\",check:\"custom\",fn:t=>t instanceof e,abort:!0,...A(t)});return r._zod.bag.Class=e,r}var ql;function Zl(e){return Fa(Ji,e)}ql||(ql={});const Vl=Object.freeze(Object.defineProperty({__proto__:null,bigint:function(e){return To(As,e)},boolean:function(e){return _o(Es,e)},date:function(e){return Lo(Qs,e)},number:function(e){return bo(xs,e)},string:Zl},Symbol.toStringTag,{value:\"Module\"}));i(Vn());const Hl=Object.freeze(Object.defineProperty({__proto__:null,$brand:r,$input:ja,$output:Ra,NEVER:e,TimePrecision:po,ZodAny:Ns,ZodArray:qs,ZodBase64:vs,ZodBase64URL:bs,ZodBigInt:As,ZodBigIntFormat:$s,ZodBoolean:Es,ZodCIDRv4:ms,ZodCIDRv6:gs,ZodCUID:ls,ZodCUID2:cs,ZodCatch:_l,ZodCodec:Cl,ZodCustom:Ul,ZodCustomStringFormat:ws,ZodDate:Qs,ZodDefault:bl,ZodDiscriminatedUnion:Gs,ZodE164:ys,ZodEmail:rs,ZodEmoji:is,ZodEnum:sl,ZodError:ji,ZodFile:dl,get ZodFirstPartyTypeKind(){return ql},ZodFunction:jl,ZodGUID:ns,ZodIPv4:hs,ZodIPv6:fs,ZodISODate:$i,ZodISODateTime:Ti,ZodISODuration:Ii,ZodISOTime:Pi,ZodIntersection:Ks,ZodIssueCode:{invalid_type:\"invalid_type\",too_big:\"too_big\",too_small:\"too_small\",invalid_format:\"invalid_format\",not_multiple_of:\"not_multiple_of\",unrecognized_keys:\"unrecognized_keys\",invalid_union:\"invalid_union\",invalid_key:\"invalid_key\",invalid_element:\"invalid_element\",invalid_value:\"invalid_value\",custom:\"custom\"},ZodJWT:Os,ZodKSUID:ps,ZodLazy:Ml,ZodLiteral:cl,ZodMap:ol,ZodNaN:Tl,ZodNanoID:ss,ZodNever:Us,ZodNonOptional:xl,ZodNull:Is,ZodNullable:gl,ZodNumber:xs,ZodNumberFormat:Ss,ZodObject:Vs,ZodOptional:fl,ZodPipe:Al,ZodPrefault:Ol,ZodPromise:Rl,ZodReadonly:Pl,ZodRealError:Li,ZodRecord:rl,ZodSet:il,ZodString:Ji,ZodStringFormat:ts,ZodSuccess:Sl,ZodSymbol:Cs,ZodTemplateLiteral:Il,ZodTransform:pl,ZodTuple:el,ZodType:Yi,ZodULID:us,ZodURL:os,ZodUUID:as,ZodUndefined:Ps,ZodUnion:Ws,ZodUnknown:js,ZodVoid:zs,ZodXID:ds,_ZodString:Ki,_default:yl,_function:Ll,any:Rs,array:Zs,base64:function(e){return so(vs,e)},base64url:function(e){return lo(bs,e)},bigint:function(e){return Eo(As,e)},boolean:Ts,catch:El,check:function(e){const t=new Tt({check:\"custom\"});return t._zod.check=e,t},cidrv4:function(e){return oo(ms,e)},cidrv6:function(e){return io(gs,e)},clone:T,codec:function(e,t,r){return new Cl({type:\"pipe\",in:e,out:t,transform:r.decode,reverseTransform:r.encode})},coerce:Vl,config:i,core:Ei,cuid:function(e){return Ka(ls,e)},cuid2:function(e){return Ja(cs,e)},custom:Bl,date:function(e){return jo(Qs,e)},decode:qi,decodeAsync:Vi,discriminatedUnion:Ys,e164:function(e){return co(ys,e)},email:function(e){return Qa(rs,e)},emoji:function(e){return Ga(is,e)},encode:Qi,encodeAsync:Zi,endsWith:ii,enum:ll,file:function(e){return mi(dl,e)},flattenError:J,float32:function(e){return Oo(Ss,e)},float64:function(e){return wo(Ss,e)},formatError:ee,function:Ll,getErrorMap:function(){return i().customError},globalRegistry:Ba,gt:Fo,gte:Qo,guid:function(e){return qa(ns,e)},hash:function(e,t){const r=`${e}_${t?.enc??\"hex\"}`,n=Et[r];if(!n)throw new Error(`Unrecognized hash format: ${r}`);return wi(ws,r,n,t)},hex:function(e){return wi(ws,\"hex\",ht,e)},hostname:function(e){return wi(ws,\"hostname\",Ge,e)},httpUrl:function(e){return Xa(os,{protocol:/^https?$/,hostname:Ye,...A(e)})},includes:ai,instanceof:Ql,int:_s,int32:function(e){return xo(Ss,e)},int64:function(e){return Ao($s,e)},intersection:Js,ipv4:function(e){return no(hs,e)},ipv6:function(e){return ao(fs,e)},iso:Ni,json:function(e){const t=Nl((()=>Xs([es(e),ks(),Ts(),Ms(),Zs(t),nl(es(),t)])));return t},jwt:function(e){return uo(Os,e)},keyof:function(e){const t=e._zod.def.shape;return ll(Object.keys(t))},ksuid:function(e){return ro(ps,e)},lazy:Nl,length:ei,literal:ul,locales:Na,looseObject:function(e,t){return new Vs({type:\"object\",shape:e,catchall:Ls(),...A(t)})},lowercase:ri,lt:Bo,lte:zo,map:function(e,t,r){return new ol({type:\"map\",keyType:e,valueType:t,...A(r)})},maxLength:Ko,maxSize:Xo,mime:li,minLength:Jo,minSize:Go,multipleOf:Wo,nan:function(e){return Uo(Tl,e)},nanoid:function(e){return Ya(ss,e)},nativeEnum:function(e,t){return new sl({type:\"enum\",entries:e,...A(t)})},negative:Zo,never:Bs,nonnegative:Ho,nonoptional:kl,nonpositive:Vo,normalize:ui,null:Ms,nullable:vl,nullish:function(e){return ml(vl(e))},number:ks,object:Hs,optional:ml,overwrite:ci,parse:Ui,parseAsync:Bi,partialRecord:al,pipe:$l,positive:qo,prefault:wl,preprocess:function(e,t){return $l(hl(e),t)},prettifyError:ne,promise:function(e){return new Rl({type:\"promise\",innerType:e})},property:si,readonly:Dl,record:nl,refine:zl,regex:ti,regexes:Et,registry:Ua,safeDecode:Wi,safeDecodeAsync:Gi,safeEncode:Hi,safeEncodeAsync:Xi,safeParse:zi,safeParseAsync:Fi,set:function(e,t){return new il({type:\"set\",valueType:e,...A(t)})},setErrorMap:function(e){i({customError:e})},size:Yo,startsWith:oi,strictObject:function(e,t){return new Vs({type:\"object\",shape:e,catchall:Bs(),...A(t)})},string:es,stringFormat:function(e,t,r={}){return wi(ws,e,t,r)},stringbool:(...e)=>Oi({Codec:Cl,Boolean:Es,String:Ji},...e),success:function(e){return new Sl({type:\"success\",innerType:e})},superRefine:Fl,symbol:function(e){return Co(Cs,e)},templateLiteral:function(e,t){return new Il({type:\"template_literal\",parts:e,...A(t)})},toJSONSchema:ki,toLowerCase:pi,toUpperCase:hi,transform:hl,treeifyError:te,trim:di,tuple:tl,uint32:function(e){return ko(Ss,e)},uint64:function(e){return $o($s,e)},ulid:function(e){return eo(us,e)},undefined:Ds,union:Xs,unknown:Ls,uppercase:ni,url:function(e){return Xa(os,e)},util:X,uuid:function(e){return Za(as,e)},uuidv4:function(e){return Va(as,e)},uuidv6:function(e){return Ha(as,e)},uuidv7:function(e){return Wa(as,e)},void:Fs,xid:function(e){return to(ds,e)}},Symbol.toStringTag,{value:\"Module\"})),Wl=Hs({title:es().optional(),component:Ls(),props:nl(es(),Rs()).optional()}),Xl=Hs({\"request.section\":Zs(Wl).optional(),\"response.section\":Zs(Wl).optional()}),Gl=Hs({onBeforeRequest:Ll({input:[Hs({request:Ql(Request)})]}).optional(),onResponseReceived:Ll({input:[Hs({response:Ql(Response),operation:nl(es(),Rs())})]}).optional()}),Yl=Ll({input:[],output:Hs({name:es(),views:Xl.optional(),hooks:Gl.optional()})}),Kl=\"https://api.scalar.com/request-proxy\",Jl=\"https://proxy.scalar.com\",ec=Hl.object({title:Hl.string().optional(),slug:Hl.string().optional(),authentication:Hl.any().optional(),baseServerURL:Hl.string().optional(),hideClientButton:Hl.boolean().optional().default(!1).catch(!1),proxyUrl:Hl.string().optional(),searchHotKey:Hl.enum([\"a\",\"b\",\"c\",\"d\",\"e\",\"f\",\"g\",\"h\",\"i\",\"j\",\"k\",\"l\",\"m\",\"n\",\"o\",\"p\",\"q\",\"r\",\"s\",\"t\",\"u\",\"v\",\"w\",\"x\",\"y\",\"z\"]).optional(),servers:Hl.array(Hl.any()).optional(),showSidebar:Hl.boolean().optional().default(!0).catch(!0),showToolbar:Hl.enum([\"always\",\"localhost\",\"never\"]).optional().default(\"localhost\").catch(\"localhost\"),operationTitleSource:Hl.enum([\"summary\",\"path\"]).optional().default(\"summary\").catch(\"summary\"),theme:Hl.enum([\"alternate\",\"default\",\"moon\",\"purple\",\"solarized\",\"bluePlanet\",\"deepSpace\",\"saturn\",\"kepler\",\"elysiajs\",\"fastify\",\"mars\",\"laserwave\",\"none\"]).optional().default(\"default\").catch(\"default\"),_integration:Hl.enum([\"adonisjs\",\"astro\",\"docusaurus\",\"dotnet\",\"elysiajs\",\"express\",\"fastapi\",\"fastify\",\"go\",\"hono\",\"html\",\"laravel\",\"litestar\",\"nestjs\",\"nextjs\",\"nitro\",\"nuxt\",\"platformatic\",\"react\",\"rust\",\"svelte\",\"vue\"]).nullable().optional(),onRequestSent:Hl.function({input:[Hl.string()],output:Hl.void()}).optional(),persistAuth:Hl.boolean().optional().default(!1).catch(!1),plugins:Hl.array(Yl).optional(),telemetry:Hl.boolean().optional().default(!0)}),tc=Hl.object({default:Hl.boolean().default(!1).optional().catch(!1),url:Hl.string().optional(),content:Hl.union([Hl.string(),Hl.null(),Hl.record(Hl.string(),Hl.any()),Hl.function({input:[],output:Hl.record(Hl.string(),Hl.any())})]).optional(),title:Hl.string().optional(),slug:Hl.string().optional(),spec:Hl.object({url:Hl.string().optional(),content:Hl.union([Hl.string(),Hl.null(),Hl.record(Hl.string(),Hl.any()),Hl.function({input:[],output:Hl.record(Hl.string(),Hl.any())})]).optional()}).optional()}),rc=ec.extend(tc.shape),nc=Hs({name:es().regex(/^x-/),component:Ls(),renderer:Ls().optional()}),ac=Hs({component:Ls(),renderer:Ls().optional(),props:nl(es(),Rs()).optional()}),oc=Hs({\"content.end\":Zs(ac).optional()}),ic=Ll({input:[],output:Hs({name:es(),extensions:Zs(nc),views:oc.optional()})}),sc=Bl(),lc=ec.extend({layout:ll([\"modern\",\"classic\"]).optional().default(\"modern\").catch(\"modern\"),proxy:es().optional(),fetch:sc.optional(),plugins:Zs(ic).optional(),isEditable:Ts().optional().default(!1).catch(!1),isLoading:Ts().optional().default(!1).catch(!1),hideModels:Ts().optional().default(!1).catch(!1),documentDownloadType:ll([\"yaml\",\"json\",\"both\",\"direct\",\"none\"]).optional().default(\"both\").catch(\"both\"),hideDownloadButton:Ts().optional(),hideTestRequestButton:Ts().optional().default(!1).catch(!1),hideSearch:Ts().optional().default(!1).catch(!1),showOperationId:Ts().optional().default(!1).catch(!1),darkMode:Ts().optional(),forceDarkModeState:ll([\"dark\",\"light\"]).optional(),hideDarkModeToggle:Ts().optional().default(!1).catch(!1),metaData:Rs().optional(),favicon:es().optional(),hiddenClients:Xs([nl(es(),Xs([Ts(),Zs(es())])),Zs(es()),ul(!0)]).optional(),defaultHttpClient:Hs({targetKey:Bl(),clientKey:es()}).optional(),customCss:es().optional(),onSpecUpdate:Ll({input:[es()],output:Fs()}).optional(),onServerChange:Ll({input:[es()],output:Fs()}).optional(),onDocumentSelect:Ll({input:[]}).optional(),onLoaded:Ll().optional(),onBeforeRequest:Ll({input:[Hs({request:Ql(Request)})]}).optional(),onShowMore:Ll({input:[es()]}).optional(),onSidebarClick:Ll({input:[es()]}).optional(),pathRouting:Hs({basePath:es()}).optional(),generateHeadingSlug:Ll({input:[Hs({slug:es().default(\"headingSlug\")})],output:es()}).optional(),generateModelSlug:Ll({input:[Hs({name:es().default(\"modelName\")})],output:es()}).optional(),generateTagSlug:Ll({input:[Hs({name:es().default(\"tagName\")})],output:es()}).optional(),generateOperationSlug:Ll({input:[Hs({path:es(),operationId:es().optional(),method:es(),summary:es().optional()})],output:es()}).optional(),generateWebhookSlug:Ll({input:[Hs({name:es(),method:es().optional()})],output:es()}).optional(),redirect:Ll({input:[es()],output:es().nullable().optional()}).optional(),withDefaultFonts:Ts().optional().default(!0).catch(!0),defaultOpenAllTags:Ts().optional().default(!1).catch(!1),expandAllModelSections:Ts().optional().default(!1).catch(!1),expandAllResponses:Ts().optional().default(!1).catch(!1),tagsSorter:Xs([ul(\"alpha\"),Ll({input:[Rs(),Rs()],output:ks()})]).optional(),operationsSorter:Xs([ul(\"alpha\"),ul(\"method\"),Ll({input:[Rs(),Rs()],output:ks()})]).optional(),orderSchemaPropertiesBy:Xs([ul(\"alpha\"),ul(\"preserve\")]).optional().default(\"alpha\").catch(\"alpha\"),orderRequiredPropertiesFirst:Ts().optional().default(!0).catch(!0)}),cc=lc.extend(tc.shape).transform((e=>(e.hideDownloadButton&&(console.warn(\"[DEPRECATED] You're using the deprecated 'hideDownloadButton' attribute. Use 'documentDownloadType: 'none'' instead.\"),e.documentDownloadType=\"none\"),e.spec?.url&&(console.warn(\"[DEPRECATED] You're using the deprecated 'spec.url' attribute. Remove the spec prefix and move the 'url' attribute to the top level.\"),e.url=e.spec.url,delete e.spec),e.spec?.content&&(console.warn(\"[DEPRECATED] You're using the deprecated 'spec.content' attribute. Remove the spec prefix and move the 'content' attribute to the top level.\"),e.content=e.spec.content,delete e.spec),e.proxy&&(console.warn(\"[DEPRECATED] You're using the deprecated 'proxy' attribute, rename it to 'proxyUrl' or update the package.\"),e.proxyUrl||(e.proxyUrl=e.proxy),delete e.proxy),e.proxyUrl===Kl&&(console.warn(`[DEPRECATED] Warning: configuration.proxyUrl points to our old proxy (${Kl}).`),console.warn(`[DEPRECATED] We are overwriting the value and use the new proxy URL (${Jl}) instead.`),console.warn(`[DEPRECATED] Action Required: You should manually update your configuration to use the new URL (${Jl}). Read more: https://github.com/scalar/scalar`),e.proxyUrl=Jl),e)));Hs({cdn:es().optional().default(\"https://cdn.jsdelivr.net/npm/@scalar/api-reference\"),pageTitle:es().optional().default(\"Scalar API Reference\")});var uc=(e=>(e.Deprecated=\"deprecated\",e.Experimental=\"experimental\",e.Stable=\"stable\",e))(uc||{});function dc(e,t){const r=[],n=t.resolveKeyData||(e=>e.key),a=t.resolveValueData||(e=>e.value);for(const[o,i]of Object.entries(e))r.push(...(Array.isArray(i)?i:[i]).map((e=>{const r={key:o,value:e},i=a(r);return\"object\"==typeof i?dc(i,t):Array.isArray(i)?i:{[\"function\"==typeof t.key?t.key(r):t.key]:n(r),[\"function\"==typeof t.value?t.value(r):t.value]:i}})).flat());return r}function pc(e,t){return Object.entries(e).map((([e,r])=>{if(\"object\"==typeof r&&(r=pc(r,t)),t.resolve){const n=t.resolve({key:e,value:r});if(void 0!==n)return n}return\"number\"==typeof r&&(r=r.toString()),\"string\"==typeof r&&t.wrapValue&&(r=r.replace(new RegExp(t.wrapValue,\"g\"),`\\\\${t.wrapValue}`),r=`${t.wrapValue}${r}${t.wrapValue}`),`${e}${t.keyValueSeparator||\"\"}${r}`})).join(t.entrySeparator||\"\")}const hc=new Set([\"title\",\"titleTemplate\",\"script\",\"style\",\"noscript\"]),fc=new Set([\"base\",\"meta\",\"link\",\"style\",\"script\",\"noscript\"]),mc=new Set([\"title\",\"titleTemplate\",\"templateParams\",\"base\",\"htmlAttrs\",\"bodyAttrs\",\"meta\",\"link\",\"style\",\"script\",\"noscript\"]),gc=new Set([\"base\",\"title\",\"titleTemplate\",\"bodyAttrs\",\"htmlAttrs\",\"templateParams\"]),vc=new Set([\"tagPosition\",\"tagPriority\",\"tagDuplicateStrategy\",\"children\",\"innerHTML\",\"textContent\",\"processTemplateParams\"]),bc=\"undefined\"!=typeof window;function yc(e){let t=9;for(let r=0;r>>9)).toString(16).substring(1,8).toLowerCase()}function Oc(e){if(e._h)return e._h;if(e._d)return yc(e._d);let t=`${e.tag}:${e.textContent||e.innerHTML||\"\"}:`;for(const r in e.props)t+=`${r}:${String(e.props[r])},`;return yc(t)}const wc=e=>({keyValue:e,metaKey:\"property\"}),xc=e=>({keyValue:e}),kc={appleItunesApp:{unpack:{entrySeparator:\", \",resolve:({key:e,value:t})=>`${Ec(e)}=${t}`}},articleExpirationTime:wc(\"article:expiration_time\"),articleModifiedTime:wc(\"article:modified_time\"),articlePublishedTime:wc(\"article:published_time\"),bookReleaseDate:wc(\"book:release_date\"),charset:{metaKey:\"charset\"},contentSecurityPolicy:{unpack:{entrySeparator:\"; \",resolve:({key:e,value:t})=>`${Ec(e)} ${t}`},metaKey:\"http-equiv\"},contentType:{metaKey:\"http-equiv\"},defaultStyle:{metaKey:\"http-equiv\"},fbAppId:wc(\"fb:app_id\"),msapplicationConfig:xc(\"msapplication-Config\"),msapplicationTileColor:xc(\"msapplication-TileColor\"),msapplicationTileImage:xc(\"msapplication-TileImage\"),ogAudioSecureUrl:wc(\"og:audio:secure_url\"),ogAudioUrl:wc(\"og:audio\"),ogImageSecureUrl:wc(\"og:image:secure_url\"),ogImageUrl:wc(\"og:image\"),ogSiteName:wc(\"og:site_name\"),ogVideoSecureUrl:wc(\"og:video:secure_url\"),ogVideoUrl:wc(\"og:video\"),profileFirstName:wc(\"profile:first_name\"),profileLastName:wc(\"profile:last_name\"),profileUsername:wc(\"profile:username\"),refresh:{metaKey:\"http-equiv\",unpack:{entrySeparator:\";\",resolve({key:e,value:t}){if(\"seconds\"===e)return`${t}`}}},robots:{unpack:{entrySeparator:\", \",resolve:({key:e,value:t})=>\"boolean\"==typeof t?`${Ec(e)}`:`${Ec(e)}:${t}`}},xUaCompatible:{metaKey:\"http-equiv\"}},Sc=new Set([\"og\",\"book\",\"article\",\"profile\"]);function _c(e){const t=Ec(e),r=t.indexOf(\":\");return Sc.has(t.substring(0,r))?\"property\":kc[e]?.metaKey||\"name\"}function Ec(e){const t=e.replace(/([A-Z])/g,\"-$1\").toLowerCase(),r=t.indexOf(\"-\"),n=t.substring(0,r);return\"twitter\"===n||Sc.has(n)?e.replace(/([A-Z])/g,\":$1\").toLowerCase():t}function Tc(e){if(Array.isArray(e))return e.map((e=>Tc(e)));if(\"object\"!=typeof e||Array.isArray(e))return e;const t={};for(const r in e)Object.prototype.hasOwnProperty.call(e,r)&&(t[Ec(r)]=Tc(e[r]));return t}function Ac(e,t){const r=kc[t];return\"refresh\"===t?`${e.seconds};url=${e.url}`:pc(Tc(e),{keyValueSeparator:\"=\",entrySeparator:\", \",resolve:({value:e,key:t})=>null===e?\"\":\"boolean\"==typeof e?`${t}`:void 0,...r?.unpack})}const $c=new Set([\"og:image\",\"og:video\",\"og:audio\",\"twitter:image\"]);function Cc(e){const t={};for(const r in e){if(!Object.prototype.hasOwnProperty.call(e,r))continue;const n=e[r];\"false\"!==String(n)&&r&&(t[r]=n)}return t}function Pc(e,t){const r=Cc(t),n=Ec(e),a=_c(n);if($c.has(n)){const t={};for(const n in r)Object.prototype.hasOwnProperty.call(r,n)&&(t[`${e}${\"url\"===n?\"\":`${n[0].toUpperCase()}${n.slice(1)}`}`]=r[n]);return Dc(t).sort(((e,t)=>(e[a]?.length||0)-(t[a]?.length||0)))}return[{[a]:n,...r}]}function Dc(e){const t=[],r={};for(const n in e){if(!Object.prototype.hasOwnProperty.call(e,n))continue;const a=e[n];if(Array.isArray(a))for(const e of a)t.push(...\"string\"==typeof e?Dc({[n]:e}):Pc(n,e));else if(\"object\"==typeof a&&a){if($c.has(Ec(n))){t.push(...Pc(n,a));continue}r[n]=Cc(a)}else r[n]=a}const n=dc(r,{key:({key:e})=>_c(e),value:({key:e})=>\"charset\"===e?\"charset\":\"content\",resolveKeyData:({key:e})=>function(e){return kc[e]?.keyValue||Ec(e)}(e),resolveValueData:({value:e,key:t})=>null===e?\"_null\":\"object\"==typeof e?Ac(e,t):\"number\"==typeof e?e.toString():e});return[...t,...n].map((e=>(\"_null\"===e.content&&(e.content=null),e)))}function Ic(e,t,r,n){const a=n||Rc(\"object\"!=typeof t||\"function\"==typeof t||t instanceof Promise?{[\"script\"===e||\"noscript\"===e||\"style\"===e?\"innerHTML\":\"textContent\"]:t}:{...t},\"templateParams\"===e||\"titleTemplate\"===e);if(a instanceof Promise)return a.then((n=>Ic(e,t,r,n)));const o={tag:e,props:a};for(const e of vc){const t=void 0!==o.props[e]?o.props[e]:r[e];void 0!==t&&((\"innerHTML\"!==e&&\"textContent\"!==e&&\"children\"!==e||hc.has(o.tag))&&(o[\"children\"===e?\"innerHTML\":e]=t),delete o.props[e])}return o.props.body&&(o.tagPosition=\"bodyClose\",delete o.props.body),\"script\"===o.tag&&\"object\"==typeof o.innerHTML&&(o.innerHTML=JSON.stringify(o.innerHTML),o.props.type=o.props.type||\"application/json\"),Array.isArray(o.props.content)?o.props.content.map((e=>({...o,props:{...o.props,content:e}}))):o}function Mc(e,t){const r=\"class\"===e?\" \":\";\";return t&&\"object\"==typeof t&&!Array.isArray(t)&&(t=Object.entries(t).filter((([,e])=>e)).map((([t,r])=>\"style\"===e?`${t}:${r}`:t))),String(Array.isArray(t)?t.join(r):t)?.split(r).filter((e=>Boolean(e.trim()))).join(r)}function Nc(e,t,r,n){for(let a=n;a(e[n]=o,Nc(e,t,r,a))));if(!t&&!vc.has(n)){const t=String(e[n]),r=n.startsWith(\"data-\");\"true\"===t||\"\"===t?e[n]=!r||\"true\":e[n]||(r&&\"false\"===t?e[n]=\"false\":delete e[n])}}else e[n]=Mc(n,e[n])}}function Rc(e,t=!1){const r=Nc(e,t,Object.keys(e),0);return r instanceof Promise?r.then((()=>e)):e}function jc(e,t,r){for(let n=r;n(t[n]=r,jc(e,t,n))));Array.isArray(r)?e.push(...r):e.push(r)}}function Lc(e){const t=[],r=e.resolvedInput;for(const n in r){if(!Object.prototype.hasOwnProperty.call(r,n))continue;const a=r[n];if(void 0!==a&&mc.has(n))if(Array.isArray(a))for(const r of a)t.push(Ic(n,r,e));else t.push(Ic(n,a,e))}if(0===t.length)return[];const n=[];return o=()=>n.map(((t,r)=>(t._e=e._i,e.mode&&(t._m=e.mode),t._p=(e._i<<10)+r,t))),(a=jc(n,t,0))instanceof Promise?a.then(o):o();var a,o}const Uc=new Set([\"onload\",\"onerror\",\"onabort\",\"onprogress\",\"onloadstart\"]),Bc={base:-10,title:10},zc={critical:-80,high:-10,low:20};function Fc(e){const t=e.tagPriority;if(\"number\"==typeof t)return t;let r=100;return\"meta\"===e.tag?\"content-security-policy\"===e.props[\"http-equiv\"]?r=-30:e.props.charset?r=-20:\"viewport\"===e.props.name&&(r=-15):\"link\"===e.tag&&\"preconnect\"===e.props.rel?r=20:e.tag in Bc&&(r=Bc[e.tag]),t&&t in zc?r+zc[t]:r}const Qc=[{prefix:\"before:\",offset:-1},{prefix:\"after:\",offset:1}],qc=[\"name\",\"property\",\"http-equiv\"];function Zc(e){const{props:t,tag:r}=e;if(gc.has(r))return r;if(\"link\"===r&&\"canonical\"===t.rel)return\"canonical\";if(t.charset)return\"charset\";if(t.id)return`${r}:id:${t.id}`;for(const e of qc)if(void 0!==t[e])return`${r}:${e}:${t[e]}`;return!1}const Vc=\"%separator\",Hc=new RegExp(`${Vc}(?:\\\\s*${Vc})*`,\"g\");function Wc(e,t,r,n=!1){if(\"string\"!=typeof e||!e.includes(\"%\"))return e;let a=e;try{a=decodeURI(e)}catch{}const o=a.match(/%\\w+(?:\\.\\w+)?/g);if(!o)return e;const i=e.includes(Vc);return e=e.replace(/%\\w+(?:\\.\\w+)?/g,(e=>{if(e===Vc||!o.includes(e))return e;const r=function(e,t,r=!1){let n;if(\"s\"===t||\"pageTitle\"===t)n=e.pageTitle;else if(t.includes(\".\")){const r=t.indexOf(\".\");n=e[t.substring(0,r)]?.[t.substring(r+1)]}else n=e[t];if(void 0!==n)return r?(n||\"\").replace(/\"/g,'\\\\\"'):n||\"\"}(t,e.slice(1),n);return void 0!==r?r:e})).trim(),i&&(e.endsWith(Vc)&&(e=e.slice(0,-10)),e.startsWith(Vc)&&(e=e.slice(10)),e=e.replace(Hc,r).trim()),e}function Xc(e,t){return null==e?t||null:\"function\"==typeof e?e(t):e}function Gc(e){return t=>{const r=t.resolvedOptions.document?.head.querySelector('script[id=\"unhead:payload\"]')?.innerHTML||!1;return r&&t.push(JSON.parse(r)),{mode:\"client\",hooks:{\"entries:updated\":t=>{!function(e,t={}){const r=t.delayFn||(e=>setTimeout(e,10));e._domDebouncedUpdatePromise=e._domDebouncedUpdatePromise||new Promise((n=>r((()=>async function(e,t={}){const r=t.document||e.resolvedOptions.document;if(!r||!e.dirty)return;const n={shouldRender:!0,tags:[]};return await e.hooks.callHook(\"dom:beforeRender\",n),n.shouldRender?(e._domUpdatePromise||(e._domUpdatePromise=new Promise((async t=>{const n=(await e.resolveTags()).map((e=>({tag:e,id:fc.has(e.tag)?Oc(e):e.tag,shouldRender:!0})));let a=e._dom;if(!a){a={elMap:{htmlAttrs:r.documentElement,bodyAttrs:r.body}};const e=new Set;for(const t of[\"body\",\"head\"]){const n=r[t]?.children;for(const t of n){const r=t.tagName.toLowerCase();if(!fc.has(r))continue;const n={tag:r,props:await Rc(t.getAttributeNames().reduce(((e,r)=>({...e,[r]:t.getAttribute(r)})),{})),innerHTML:t.innerHTML},o=Zc(n);let i=o,s=1;for(;i&&e.has(i);)i=`${o}:${s++}`;i&&(n._d=i,e.add(i)),a.elMap[t.getAttribute(\"data-hid\")||Oc(n)]=t}}}function o(e,t,r){const n=`${e}:${t}`;a.sideEffects[n]=r,delete a.pendingSideEffects[n]}function i({id:e,$el:t,tag:n}){const i=n.tag.endsWith(\"Attrs\");if(a.elMap[e]=t,i||(n.textContent&&n.textContent!==t.textContent&&(t.textContent=n.textContent),n.innerHTML&&n.innerHTML!==t.innerHTML&&(t.innerHTML=n.innerHTML),o(e,\"el\",(()=>{a.elMap[e]?.remove(),delete a.elMap[e]}))),n._eventHandlers)for(const e in n._eventHandlers)Object.prototype.hasOwnProperty.call(n._eventHandlers,e)&&\"\"!==t.getAttribute(`data-${e}`)&&((\"bodyAttrs\"===n.tag?r.defaultView:t).addEventListener(e.substring(2),n._eventHandlers[e].bind(t)),t.setAttribute(`data-${e}`,\"\"));for(const r in n.props){if(!Object.prototype.hasOwnProperty.call(n.props,r))continue;const a=n.props[r],s=`attr:${r}`;if(\"class\"===r){if(!a)continue;for(const r of a.split(\" \"))i&&o(e,`${s}:${r}`,(()=>t.classList.remove(r))),!t.classList.contains(r)&&t.classList.add(r)}else if(\"style\"===r){if(!a)continue;for(const r of a.split(\";\")){const n=r.indexOf(\":\"),a=r.substring(0,n).trim(),i=r.substring(n+1).trim();o(e,`${s}:${a}`,(()=>{t.style.removeProperty(a)})),t.style.setProperty(a,i)}}else t.getAttribute(r)!==a&&t.setAttribute(r,!0===a?\"\":String(a)),i&&o(e,s,(()=>t.removeAttribute(r)))}}a.pendingSideEffects={...a.sideEffects},a.sideEffects={};const s=[],l={bodyClose:void 0,bodyOpen:void 0,head:void 0};for(const e of n){const{tag:t,shouldRender:n,id:o}=e;n&&(\"title\"!==t.tag?(e.$el=e.$el||a.elMap[o],e.$el?i(e):fc.has(t.tag)&&s.push(e)):r.title=t.textContent)}for(const e of s){const t=e.tag.tagPosition||\"head\";e.$el=r.createElement(e.tag.tag),i(e),l[t]=l[t]||r.createDocumentFragment(),l[t].appendChild(e.$el)}for(const t of n)await e.hooks.callHook(\"dom:renderTag\",t,r,o);l.head&&r.head.appendChild(l.head),l.bodyOpen&&r.body.insertBefore(l.bodyOpen,r.body.firstChild),l.bodyClose&&r.body.appendChild(l.bodyClose);for(const e in a.pendingSideEffects)a.pendingSideEffects[e]();e._dom=a,await e.hooks.callHook(\"dom:rendered\",{renders:n}),t()})).finally((()=>{e._domUpdatePromise=void 0,e.dirty=!1}))),e._domUpdatePromise):void 0}(e,t).then((()=>{delete e._domDebouncedUpdatePromise,n()}))))))}(t,e)}}}}}function Yc(e,t={},r){for(const n in e){const a=e[n],o=r?`${r}:${n}`:n;\"object\"==typeof a&&null!==a?Yc(a,t,o):\"function\"==typeof a&&(t[o]=a)}return t}const Kc={run:e=>e()},Jc=void 0!==console.createTask?console.createTask:()=>Kc;function eu(e,t){const r=t.shift(),n=Jc(r);return e.reduce(((e,r)=>e.then((()=>n.run((()=>r(...t)))))),Promise.resolve())}function tu(e,t){const r=t.shift(),n=Jc(r);return Promise.all(e.map((e=>n.run((()=>e(...t))))))}function ru(e,t){for(const r of[...e])r(t)}class nu{constructor(){this._hooks={},this._before=void 0,this._after=void 0,this._deprecatedMessages=void 0,this._deprecatedHooks={},this.hook=this.hook.bind(this),this.callHook=this.callHook.bind(this),this.callHookWith=this.callHookWith.bind(this)}hook(e,t,r={}){if(!e||\"function\"!=typeof t)return()=>{};const n=e;let a;for(;this._deprecatedHooks[e];)a=this._deprecatedHooks[e],e=a.to;if(a&&!r.allowDeprecated){let e=a.message;e||(e=`${n} hook has been deprecated`+(a.to?`, please use ${a.to}`:\"\")),this._deprecatedMessages||(this._deprecatedMessages=new Set),this._deprecatedMessages.has(e)||(console.warn(e),this._deprecatedMessages.add(e))}if(!t.name)try{Object.defineProperty(t,\"name\",{get:()=>\"_\"+e.replace(/\\W+/g,\"_\")+\"_hook_cb\",configurable:!0})}catch{}return this._hooks[e]=this._hooks[e]||[],this._hooks[e].push(t),()=>{t&&(this.removeHook(e,t),t=void 0)}}hookOnce(e,t){let r,n=(...e)=>(\"function\"==typeof r&&r(),r=void 0,n=void 0,t(...e));return r=this.hook(e,n),r}removeHook(e,t){if(this._hooks[e]){const r=this._hooks[e].indexOf(t);-1!==r&&this._hooks[e].splice(r,1),0===this._hooks[e].length&&delete this._hooks[e]}}deprecateHook(e,t){this._deprecatedHooks[e]=\"string\"==typeof t?{to:t}:t;const r=this._hooks[e]||[];delete this._hooks[e];for(const t of r)this.hook(e,t)}deprecateHooks(e){Object.assign(this._deprecatedHooks,e);for(const t in e)this.deprecateHook(t,e[t])}addHooks(e){const t=Yc(e),r=Object.keys(t).map((e=>this.hook(e,t[e])));return()=>{for(const e of r.splice(0,r.length))e()}}removeHooks(e){const t=Yc(e);for(const e in t)this.removeHook(e,t[e])}removeAllHooks(){for(const e in this._hooks)delete this._hooks[e]}callHook(e,...t){return t.unshift(e),this.callHookWith(eu,e,...t)}callHookParallel(e,...t){return t.unshift(e),this.callHookWith(tu,e,...t)}callHookWith(e,t,...r){const n=this._before||this._after?{name:t,args:r,context:{}}:void 0;this._before&&ru(this._before,n);const a=e(t in this._hooks?[...this._hooks[t]]:[],r);return a instanceof Promise?a.finally((()=>{this._after&&n&&ru(this._after,n)})):(this._after&&n&&ru(this._after,n),a)}beforeEach(e){return this._before=this._before||[],this._before.push(e),()=>{if(void 0!==this._before){const t=this._before.indexOf(e);-1!==t&&this._before.splice(t,1)}}}afterEach(e){return this._after=this._after||[],this._after.push(e),()=>{if(void 0!==this._after){const t=this._after.indexOf(e);-1!==t&&this._after.splice(t,1)}}}}const au=new Set([\"templateParams\",\"htmlAttrs\",\"bodyAttrs\"]),ou={hooks:{\"tag:normalise\":({tag:e})=>{e.props.hid&&(e.key=e.props.hid,delete e.props.hid),e.props.vmid&&(e.key=e.props.vmid,delete e.props.vmid),e.props.key&&(e.key=e.props.key,delete e.props.key);const t=Zc(e);!t||t.startsWith(\"meta:og:\")||t.startsWith(\"meta:twitter:\")||delete e.key;const r=t||!!e.key&&`${e.tag}:${e.key}`;r&&(e._d=r)},\"tags:resolve\":e=>{const t=Object.create(null);for(const r of e.tags){const e=(r.key?`${r.tag}:${r.key}`:r._d)||Oc(r),n=t[e];if(n){let a=r?.tagDuplicateStrategy;if(!a&&au.has(r.tag)&&(a=\"merge\"),\"merge\"===a){const a=n.props;a.style&&r.props.style&&(\";\"!==a.style[a.style.length-1]&&(a.style+=\";\"),r.props.style=`${a.style} ${r.props.style}`),a.class&&r.props.class?r.props.class=`${a.class} ${r.props.class}`:a.class&&(r.props.class=a.class),t[e].props={...a,...r.props};continue}if(r._e===n._e){n._duped=n._duped||[],r._d=`${n._d}:${n._duped.length+1}`,n._duped.push(r);continue}if(Fc(r)>Fc(n))continue}r.innerHTML||r.textContent||0!==Object.keys(r.props).length||!fc.has(r.tag)?t[e]=r:delete t[e]}const r=[];for(const e in t){const n=t[e],a=n._duped;r.push(n),a&&(delete n._duped,r.push(...a))}e.tags=r,e.tags=e.tags.filter((e=>!(\"meta\"===e.tag&&(e.props.name||e.props.property)&&!e.props.content)))}}},iu=new Set([\"script\",\"link\",\"bodyAttrs\"]),su=e=>({hooks:{\"tags:resolve\":t=>{for(const r of t.tags){if(!iu.has(r.tag))continue;const t=r.props;for(const n in t){if(\"o\"!==n[0]||\"n\"!==n[1])continue;if(!Object.prototype.hasOwnProperty.call(t,n))continue;const a=t[n];\"function\"==typeof a&&(e.ssr&&Uc.has(n)?t[n]=`this.dataset.${n}fired = true`:delete t[n],r._eventHandlers=r._eventHandlers||{},r._eventHandlers[n]=a)}e.ssr&&r._eventHandlers&&(r.props.src||r.props.href)&&(r.key=r.key||yc(r.props.src||r.props.href))}},\"dom:renderTag\":({$el:e,tag:t})=>{const r=e?.dataset;if(r)for(const n in r){if(!n.endsWith(\"fired\"))continue;const r=n.slice(0,-5);Uc.has(r)&&t._eventHandlers?.[r]?.call(e,new Event(r.substring(2)))}}}}),lu=new Set([\"link\",\"style\",\"script\",\"noscript\"]),cu={hooks:{\"tag:normalise\":({tag:e})=>{e.key&&lu.has(e.tag)&&(e.props[\"data-hid\"]=e._h=yc(e.key))}}},uu={mode:\"server\",hooks:{\"tags:beforeResolve\":e=>{const t={};let r=!1;for(const n of e.tags)\"server\"!==n._m||\"titleTemplate\"!==n.tag&&\"templateParams\"!==n.tag&&\"title\"!==n.tag||(t[n.tag]=\"title\"===n.tag||\"titleTemplate\"===n.tag?n.textContent:n.props,r=!0);r&&e.tags.push({tag:\"script\",innerHTML:JSON.stringify(t),props:{id:\"unhead:payload\",type:\"application/json\"}})}}},du={hooks:{\"tags:resolve\":e=>{for(const t of e.tags)if(\"string\"==typeof t.tagPriority)for(const{prefix:r,offset:n}of Qc){if(!t.tagPriority.startsWith(r))continue;const a=t.tagPriority.substring(r.length),o=e.tags.find((e=>e._d===a))?._p;if(void 0!==o){t._p=o+n;break}}e.tags.sort(((e,t)=>{const r=Fc(e),n=Fc(t);return rn?1:e._p-t._p}))}}},pu={meta:\"content\",link:\"href\",htmlAttrs:\"lang\"},hu=[\"innerHTML\",\"textContent\"],fu=e=>({hooks:{\"tags:resolve\":t=>{const{tags:r}=t;let n;for(let e=0;e\"title\"===e.tag))?.textContent||\"\",a,o);for(const e of r){if(!1===e.processTemplateParams)continue;const t=pu[e.tag];if(t&&\"string\"==typeof e.props[t])e.props[t]=Wc(e.props[t],a,o);else if(e.processTemplateParams||\"titleTemplate\"===e.tag||\"title\"===e.tag)for(const t of hu)\"string\"==typeof e[t]&&(e[t]=Wc(e[t],a,o,\"script\"===e.tag&&e.props.type.endsWith(\"json\")))}e._templateParams=a,e._separator=o},\"tags:afterResolve\":({tags:t})=>{let r;for(let e=0;e{const{tags:t}=e;let r,n;for(let e=0;e{for(const t of e.tags)\"string\"==typeof t.innerHTML&&(!t.innerHTML||\"application/ld+json\"!==t.props.type&&\"application/json\"!==t.props.type?t.innerHTML=t.innerHTML.replace(new RegExp(`e in t}const Ou={},wu=[],xu=()=>{},ku=()=>!1,Su=e=>111===e.charCodeAt(0)&&110===e.charCodeAt(1)&&(e.charCodeAt(2)>122||e.charCodeAt(2)<97),_u=e=>e.startsWith(\"onUpdate:\"),Eu=Object.assign,Tu=(e,t)=>{const r=e.indexOf(t);r>-1&&e.splice(r,1)},Au=Object.prototype.hasOwnProperty,$u=(e,t)=>Au.call(e,t),Cu=Array.isArray,Pu=e=>\"[object Map]\"===Bu(e),Du=e=>\"[object Set]\"===Bu(e),Iu=e=>\"[object Date]\"===Bu(e),Mu=e=>\"function\"==typeof e,Nu=e=>\"string\"==typeof e,Ru=e=>\"symbol\"==typeof e,ju=e=>null!==e&&\"object\"==typeof e,Lu=e=>(ju(e)||Mu(e))&&Mu(e.then)&&Mu(e.catch),Uu=Object.prototype.toString,Bu=e=>Uu.call(e),zu=e=>\"[object Object]\"===Bu(e),Fu=e=>Nu(e)&&\"NaN\"!==e&&\"-\"!==e[0]&&\"\"+parseInt(e,10)===e,Qu=yu(\",key,ref,ref_for,ref_key,onVnodeBeforeMount,onVnodeMounted,onVnodeBeforeUpdate,onVnodeUpdated,onVnodeBeforeUnmount,onVnodeUnmounted\"),qu=e=>{const t=Object.create(null);return r=>t[r]||(t[r]=e(r))},Zu=/-\\w/g,Vu=qu((e=>e.replace(Zu,(e=>e.slice(1).toUpperCase())))),Hu=/\\B([A-Z])/g,Wu=qu((e=>e.replace(Hu,\"-$1\").toLowerCase())),Xu=qu((e=>e.charAt(0).toUpperCase()+e.slice(1))),Gu=qu((e=>e?`on${Xu(e)}`:\"\")),Yu=(e,t)=>!Object.is(e,t),Ku=(e,...t)=>{for(let r=0;r{Object.defineProperty(e,t,{configurable:!0,enumerable:!1,writable:n,value:r})},ed=e=>{const t=parseFloat(e);return isNaN(t)?e:t};let td;const rd=()=>td||(td=\"undefined\"!=typeof globalThis?globalThis:\"undefined\"!=typeof self?self:\"undefined\"!=typeof window?window:\"undefined\"!=typeof global?global:{});function nd(e){if(Cu(e)){const t={};for(let r=0;r{if(e){const r=e.split(od);r.length>1&&(t[r[0].trim()]=r[1].trim())}})),t}function ld(e){let t=\"\";if(Nu(e))t=e;else if(Cu(e))for(let r=0;rpd(e,t)))}const fd=e=>!(!e||!0!==e.__v_isRef),md=e=>Nu(e)?e:null==e?\"\":Cu(e)||ju(e)&&(e.toString===Uu||!Mu(e.toString))?fd(e)?md(e.value):JSON.stringify(e,gd,2):String(e),gd=(e,t)=>fd(t)?gd(e,t.value):Pu(t)?{[`Map(${t.size})`]:[...t.entries()].reduce(((e,[t,r],n)=>(e[vd(t,n)+\" =>\"]=r,e)),{})}:Du(t)?{[`Set(${t.size})`]:[...t.values()].map((e=>vd(e)))}:Ru(t)?vd(t):!ju(t)||Cu(t)||zu(t)?t:String(t),vd=(e,t=\"\")=>{var r;return Ru(e)?`Symbol(${null!=(r=e.description)?r:t})`:e};function bd(e){return null==e?\"initial\":\"string\"==typeof e?\"\"===e?\" \":e:String(e)}\n/**\n * @vue/reactivity v3.5.21\n * (c) 2018-present Yuxi (Evan) You and Vue contributors\n * @license MIT\n **/let yd,Od;class wd{constructor(e=!1){this.detached=e,this._active=!0,this._on=0,this.effects=[],this.cleanups=[],this._isPaused=!1,this.parent=yd,!e&&yd&&(this.index=(yd.scopes||(yd.scopes=[])).push(this)-1)}get active(){return this._active}pause(){if(this._active){let e,t;if(this._isPaused=!0,this.scopes)for(e=0,t=this.scopes.length;e0&&0==--this._on&&(yd=this.prevScope,this.prevScope=void 0)}stop(e){if(this._active){let t,r;for(this._active=!1,t=0,r=this.effects.length;t0)return;if(Td){let e=Td;for(Td=void 0;e;){const t=e.next;e.next=void 0,e.flags&=-9,e=t}}let e;for(;Ed;){let t=Ed;for(Ed=void 0;t;){const r=t.next;if(t.next=void 0,t.flags&=-9,1&t.flags)try{t.trigger()}catch(t){e||(e=t)}t=r}}if(e)throw e}function Dd(e){for(let t=e.deps;t;t=t.nextDep)t.version=-1,t.prevActiveLink=t.dep.activeLink,t.dep.activeLink=t}function Id(e){let t,r=e.depsTail,n=r;for(;n;){const e=n.prevDep;-1===n.version?(n===r&&(r=e),Rd(n),jd(n)):t=n,n.dep.activeLink=n.prevActiveLink,n.prevActiveLink=void 0,n=e}e.deps=t,e.depsTail=r}function Md(e){for(let t=e.deps;t;t=t.nextDep)if(t.dep.version!==t.version||t.dep.computed&&(Nd(t.dep.computed)||t.dep.version!==t.version))return!0;return!!e._dirty}function Nd(e){if(4&e.flags&&!(16&e.flags))return;if(e.flags&=-17,e.globalVersion===Qd)return;if(e.globalVersion=Qd,!e.isSSR&&128&e.flags&&(!e.deps&&!e._dirty||!Md(e)))return;e.flags|=2;const t=e.dep,r=Od,n=Ld;Od=e,Ld=!0;try{Dd(e);const r=e.fn(e._value);(0===t.version||Yu(r,e._value))&&(e.flags|=128,e._value=r,t.version++)}catch(e){throw t.version++,e}finally{Od=r,Ld=n,Id(e),e.flags&=-3}}function Rd(e,t=!1){const{dep:r,prevSub:n,nextSub:a}=e;if(n&&(n.nextSub=a,e.prevSub=void 0),a&&(a.prevSub=n,e.nextSub=void 0),r.subs===e&&(r.subs=n,!n&&r.computed)){r.computed.flags&=-5;for(let e=r.computed.deps;e;e=e.nextDep)Rd(e,!0)}t||--r.sc||!r.map||r.map.delete(r.key)}function jd(e){const{prevDep:t,nextDep:r}=e;t&&(t.nextDep=r,e.prevDep=void 0),r&&(r.prevDep=t,e.nextDep=void 0)}let Ld=!0;const Ud=[];function Bd(){Ud.push(Ld),Ld=!1}function zd(){const e=Ud.pop();Ld=void 0===e||e}function Fd(e){const{cleanup:t}=e;if(e.cleanup=void 0,t){const e=Od;Od=void 0;try{t()}finally{Od=e}}}let Qd=0;class qd{constructor(e,t){this.sub=e,this.dep=t,this.version=t.version,this.nextDep=this.prevDep=this.nextSub=this.prevSub=this.prevActiveLink=void 0}}class Zd{constructor(e){this.computed=e,this.version=0,this.activeLink=void 0,this.subs=void 0,this.map=void 0,this.key=void 0,this.sc=0,this.__v_skip=!0}track(e){if(!Od||!Ld||Od===this.computed)return;let t=this.activeLink;if(void 0===t||t.sub!==Od)t=this.activeLink=new qd(Od,this),Od.deps?(t.prevDep=Od.depsTail,Od.depsTail.nextDep=t,Od.depsTail=t):Od.deps=Od.depsTail=t,Vd(t);else if(-1===t.version&&(t.version=this.version,t.nextDep)){const e=t.nextDep;e.prevDep=t.prevDep,t.prevDep&&(t.prevDep.nextDep=e),t.prevDep=Od.depsTail,t.nextDep=void 0,Od.depsTail.nextDep=t,Od.depsTail=t,Od.deps===t&&(Od.deps=e)}return t}trigger(e){this.version++,Qd++,this.notify(e)}notify(e){Cd();try{for(let e=this.subs;e;e=e.prevSub)e.sub.notify()&&e.sub.dep.notify()}finally{Pd()}}}function Vd(e){if(e.dep.sc++,4&e.sub.flags){const t=e.dep.computed;if(t&&!e.dep.subs){t.flags|=20;for(let e=t.deps;e;e=e.nextDep)Vd(e)}const r=e.dep.subs;r!==e&&(e.prevSub=r,r&&(r.nextSub=e)),e.dep.subs=e}}const Hd=new WeakMap,Wd=Symbol(\"\"),Xd=Symbol(\"\"),Gd=Symbol(\"\");function Yd(e,t,r){if(Ld&&Od){let t=Hd.get(e);t||Hd.set(e,t=new Map);let n=t.get(r);n||(t.set(r,n=new Zd),n.map=t,n.key=r),n.track()}}function Kd(e,t,r,n,a,o){const i=Hd.get(e);if(!i)return void Qd++;const s=e=>{e&&e.trigger()};if(Cd(),\"clear\"===t)i.forEach(s);else{const a=Cu(e),o=a&&Fu(r);if(a&&\"length\"===r){const e=Number(n);i.forEach(((t,r)=>{(\"length\"===r||r===Gd||!Ru(r)&&r>=e)&&s(t)}))}else switch((void 0!==r||i.has(void 0))&&s(i.get(r)),o&&s(i.get(Gd)),t){case\"add\":a?o&&s(i.get(\"length\")):(s(i.get(Wd)),Pu(e)&&s(i.get(Xd)));break;case\"delete\":a||(s(i.get(Wd)),Pu(e)&&s(i.get(Xd)));break;case\"set\":Pu(e)&&s(i.get(Wd))}}Pd()}function Jd(e){const t=Up(e);return t===e?t:(Yd(t,0,Gd),jp(e)?t:t.map(Bp))}function ep(e){return Yd(e=Up(e),0,Gd),e}const tp={__proto__:null,[Symbol.iterator](){return rp(this,Symbol.iterator,Bp)},concat(...e){return Jd(this).concat(...e.map((e=>Cu(e)?Jd(e):e)))},entries(){return rp(this,\"entries\",(e=>(e[1]=Bp(e[1]),e)))},every(e,t){return ap(this,\"every\",e,t,void 0,arguments)},filter(e,t){return ap(this,\"filter\",e,t,(e=>e.map(Bp)),arguments)},find(e,t){return ap(this,\"find\",e,t,Bp,arguments)},findIndex(e,t){return ap(this,\"findIndex\",e,t,void 0,arguments)},findLast(e,t){return ap(this,\"findLast\",e,t,Bp,arguments)},findLastIndex(e,t){return ap(this,\"findLastIndex\",e,t,void 0,arguments)},forEach(e,t){return ap(this,\"forEach\",e,t,void 0,arguments)},includes(...e){return ip(this,\"includes\",e)},indexOf(...e){return ip(this,\"indexOf\",e)},join(e){return Jd(this).join(e)},lastIndexOf(...e){return ip(this,\"lastIndexOf\",e)},map(e,t){return ap(this,\"map\",e,t,void 0,arguments)},pop(){return sp(this,\"pop\")},push(...e){return sp(this,\"push\",e)},reduce(e,...t){return op(this,\"reduce\",e,t)},reduceRight(e,...t){return op(this,\"reduceRight\",e,t)},shift(){return sp(this,\"shift\")},some(e,t){return ap(this,\"some\",e,t,void 0,arguments)},splice(...e){return sp(this,\"splice\",e)},toReversed(){return Jd(this).toReversed()},toSorted(e){return Jd(this).toSorted(e)},toSpliced(...e){return Jd(this).toSpliced(...e)},unshift(...e){return sp(this,\"unshift\",e)},values(){return rp(this,\"values\",Bp)}};function rp(e,t,r){const n=ep(e),a=n[t]();return n===e||jp(e)||(a._next=a.next,a.next=()=>{const e=a._next();return e.value&&(e.value=r(e.value)),e}),a}const np=Array.prototype;function ap(e,t,r,n,a,o){const i=ep(e),s=i!==e&&!jp(e),l=i[t];if(l!==np[t]){const t=l.apply(e,o);return s?Bp(t):t}let c=r;i!==e&&(s?c=function(t,n){return r.call(this,Bp(t),n,e)}:r.length>2&&(c=function(t,n){return r.call(this,t,n,e)}));const u=l.call(i,c,n);return s&&a?a(u):u}function op(e,t,r,n){const a=ep(e);let o=r;return a!==e&&(jp(e)?r.length>3&&(o=function(t,n,a){return r.call(this,t,n,a,e)}):o=function(t,n,a){return r.call(this,t,Bp(n),a,e)}),a[t](o,...n)}function ip(e,t,r){const n=Up(e);Yd(n,0,Gd);const a=n[t](...r);return-1!==a&&!1!==a||!Lp(r[0])?a:(r[0]=Up(r[0]),n[t](...r))}function sp(e,t,r=[]){Bd(),Cd();const n=Up(e)[t].apply(e,r);return Pd(),zd(),n}const lp=yu(\"__proto__,__v_isRef,__isVue\"),cp=new Set(Object.getOwnPropertyNames(Symbol).filter((e=>\"arguments\"!==e&&\"caller\"!==e)).map((e=>Symbol[e])).filter(Ru));function up(e){Ru(e)||(e=String(e));const t=Up(this);return Yd(t,0,e),t.hasOwnProperty(e)}class dp{constructor(e=!1,t=!1){this._isReadonly=e,this._isShallow=t}get(e,t,r){if(\"__v_skip\"===t)return e.__v_skip;const n=this._isReadonly,a=this._isShallow;if(\"__v_isReactive\"===t)return!n;if(\"__v_isReadonly\"===t)return n;if(\"__v_isShallow\"===t)return a;if(\"__v_raw\"===t)return r===(n?a?$p:Ap:a?Tp:Ep).get(e)||Object.getPrototypeOf(e)===Object.getPrototypeOf(r)?e:void 0;const o=Cu(e);if(!n){let e;if(o&&(e=tp[t]))return e;if(\"hasOwnProperty\"===t)return up}const i=Reflect.get(e,t,Fp(e)?e:r);return(Ru(t)?cp.has(t):lp(t))?i:(n||Yd(e,0,t),a?i:Fp(i)?o&&Fu(t)?i:i.value:ju(i)?n?Dp(i):Cp(i):i)}}class pp extends dp{constructor(e=!1){super(!1,e)}set(e,t,r,n){let a=e[t];if(!this._isShallow){const t=Rp(a);if(jp(r)||Rp(r)||(a=Up(a),r=Up(r)),!Cu(e)&&Fp(a)&&!Fp(r))return t||(a.value=r),!0}const o=Cu(e)&&Fu(t)?Number(t)e,yp=e=>Reflect.getPrototypeOf(e);function Op(e){return function(...t){return\"delete\"!==e&&(\"clear\"===e?void 0:this)}}function wp(e,t){const r=function(e,t){const r={get(r){const n=this.__v_raw,a=Up(n),o=Up(r);e||(Yu(r,o)&&Yd(a,0,r),Yd(a,0,o));const{has:i}=yp(a),s=t?bp:e?zp:Bp;return i.call(a,r)?s(n.get(r)):i.call(a,o)?s(n.get(o)):void(n!==a&&n.get(r))},get size(){const t=this.__v_raw;return!e&&Yd(Up(t),0,Wd),t.size},has(t){const r=this.__v_raw,n=Up(r),a=Up(t);return e||(Yu(t,a)&&Yd(n,0,t),Yd(n,0,a)),t===a?r.has(t):r.has(t)||r.has(a)},forEach(r,n){const a=this,o=a.__v_raw,i=Up(o),s=t?bp:e?zp:Bp;return!e&&Yd(i,0,Wd),o.forEach(((e,t)=>r.call(n,s(e),s(t),a)))}};return Eu(r,e?{add:Op(\"add\"),set:Op(\"set\"),delete:Op(\"delete\"),clear:Op(\"clear\")}:{add(e){t||jp(e)||Rp(e)||(e=Up(e));const r=Up(this);return yp(r).has.call(r,e)||(r.add(e),Kd(r,\"add\",e,e)),this},set(e,r){t||jp(r)||Rp(r)||(r=Up(r));const n=Up(this),{has:a,get:o}=yp(n);let i=a.call(n,e);i||(e=Up(e),i=a.call(n,e));const s=o.call(n,e);return n.set(e,r),i?Yu(r,s)&&Kd(n,\"set\",e,r):Kd(n,\"add\",e,r),this},delete(e){const t=Up(this),{has:r,get:n}=yp(t);let a=r.call(t,e);a||(e=Up(e),a=r.call(t,e)),n&&n.call(t,e);const o=t.delete(e);return a&&Kd(t,\"delete\",e,void 0),o},clear(){const e=Up(this),t=0!==e.size,r=e.clear();return t&&Kd(e,\"clear\",void 0,void 0),r}}),[\"keys\",\"values\",\"entries\",Symbol.iterator].forEach((n=>{r[n]=function(e,t,r){return function(...n){const a=this.__v_raw,o=Up(a),i=Pu(o),s=\"entries\"===e||e===Symbol.iterator&&i,l=\"keys\"===e&&i,c=a[e](...n),u=r?bp:t?zp:Bp;return!t&&Yd(o,0,l?Xd:Wd),{next(){const{value:e,done:t}=c.next();return t?{value:e,done:t}:{value:s?[u(e[0]),u(e[1])]:u(e),done:t}},[Symbol.iterator](){return this}}}}(n,e,t)})),r}(e,t);return(t,n,a)=>\"__v_isReactive\"===n?!e:\"__v_isReadonly\"===n?e:\"__v_raw\"===n?t:Reflect.get($u(r,n)&&n in t?r:t,n,a)}const xp={get:wp(!1,!1)},kp={get:wp(!1,!0)},Sp={get:wp(!0,!1)},_p={get:wp(!0,!0)},Ep=new WeakMap,Tp=new WeakMap,Ap=new WeakMap,$p=new WeakMap;function Cp(e){return Rp(e)?e:Mp(e,!1,fp,xp,Ep)}function Pp(e){return Mp(e,!1,gp,kp,Tp)}function Dp(e){return Mp(e,!0,mp,Sp,Ap)}function Ip(e){return Mp(e,!0,vp,_p,$p)}function Mp(e,t,r,n,a){if(!ju(e))return e;if(e.__v_raw&&(!t||!e.__v_isReactive))return e;const o=(i=e).__v_skip||!Object.isExtensible(i)?0:function(e){switch(e){case\"Object\":case\"Array\":return 1;case\"Map\":case\"Set\":case\"WeakMap\":case\"WeakSet\":return 2;default:return 0}}((e=>Bu(e).slice(8,-1))(i));var i;if(0===o)return e;const s=a.get(e);if(s)return s;const l=new Proxy(e,2===o?n:r);return a.set(e,l),l}function Np(e){return Rp(e)?Np(e.__v_raw):!(!e||!e.__v_isReactive)}function Rp(e){return!(!e||!e.__v_isReadonly)}function jp(e){return!(!e||!e.__v_isShallow)}function Lp(e){return!!e&&!!e.__v_raw}function Up(e){const t=e&&e.__v_raw;return t?Up(t):e}const Bp=e=>ju(e)?Cp(e):e,zp=e=>ju(e)?Dp(e):e;function Fp(e){return!!e&&!0===e.__v_isRef}function Qp(e){return Zp(e,!1)}function qp(e){return Zp(e,!0)}function Zp(e,t){return Fp(e)?e:new Vp(e,t)}class Vp{constructor(e,t){this.dep=new Zd,this.__v_isRef=!0,this.__v_isShallow=!1,this._rawValue=t?e:Up(e),this._value=t?e:Bp(e),this.__v_isShallow=t}get value(){return this.dep.track(),this._value}set value(e){const t=this._rawValue,r=this.__v_isShallow||jp(e)||Rp(e);e=r?e:Up(e),Yu(e,t)&&(this._rawValue=e,this._value=r?e:Bp(e),this.dep.trigger())}}function Hp(e){return Fp(e)?e.value:e}function Wp(e){return Mu(e)?e():Hp(e)}const Xp={get:(e,t,r)=>\"__v_raw\"===t?e:Hp(Reflect.get(e,t,r)),set:(e,t,r,n)=>{const a=e[t];return Fp(a)&&!Fp(r)?(a.value=r,!0):Reflect.set(e,t,r,n)}};function Gp(e){return Np(e)?e:new Proxy(e,Xp)}class Yp{constructor(e){this.__v_isRef=!0,this._value=void 0;const t=this.dep=new Zd,{get:r,set:n}=e(t.track.bind(t),t.trigger.bind(t));this._get=r,this._set=n}get value(){return this._value=this._get()}set value(e){this._set(e)}}function Kp(e){return new Yp(e)}class Jp{constructor(e,t,r){this._object=e,this._key=t,this._defaultValue=r,this.__v_isRef=!0,this._value=void 0}get value(){const e=this._object[this._key];return this._value=void 0===e?this._defaultValue:e}set value(e){this._object[this._key]=e}get dep(){return function(e,t){const r=Hd.get(e);return r&&r.get(t)}(Up(this._object),this._key)}}class eh{constructor(e){this._getter=e,this.__v_isRef=!0,this.__v_isReadonly=!0,this._value=void 0}get value(){return this._value=this._getter()}}function th(e,t,r){return Fp(e)?e:Mu(e)?new eh(e):ju(e)&&arguments.length>1?function(e,t,r){const n=e[t];return Fp(n)?n:new Jp(e,t,r)}(e,t,r):Qp(e)}class rh{constructor(e,t,r){this.fn=e,this.setter=t,this._value=void 0,this.dep=new Zd(this),this.__v_isRef=!0,this.deps=void 0,this.depsTail=void 0,this.flags=16,this.globalVersion=Qd-1,this.next=void 0,this.effect=this,this.__v_isReadonly=!t,this.isSSR=r}notify(){if(this.flags|=16,!(8&this.flags)&&Od!==this)return $d(this,!0),!0}get value(){const e=this.dep.track();return Nd(this),e&&(e.version=this.dep.version),this._value}set value(e){this.setter&&this.setter(e)}}const nh={},ah=new WeakMap;let oh;function ih(e,t=1/0,r){if(t<=0||!ju(e)||e.__v_skip)return e;if(((r=r||new Map).get(e)||0)>=t)return e;if(r.set(e,t),t--,Fp(e))ih(e.value,t,r);else if(Cu(e))for(let n=0;n{ih(e,t,r)}));else if(zu(e)){for(const n in e)ih(e[n],t,r);for(const n of Object.getOwnPropertySymbols(e))Object.prototype.propertyIsEnumerable.call(e,n)&&ih(e[n],t,r)}return e}\n/**\n * @vue/runtime-core v3.5.21\n * (c) 2018-present Yuxi (Evan) You and Vue contributors\n * @license MIT\n **/function sh(e,t,r,n){try{return n?e(...n):e()}catch(e){ch(e,t,r)}}function lh(e,t,r,n){if(Mu(e)){const a=sh(e,t,r,n);return a&&Lu(a)&&a.catch((e=>{ch(e,t,r)})),a}if(Cu(e)){const a=[];for(let o=0;o=kh(r)?uh.push(e):uh.splice(function(e){let t=dh+1,r=uh.length;for(;t>>1,a=uh[n],o=kh(a);okh(e)-kh(t)));if(ph.length=0,hh)return void hh.push(...e);for(hh=e,fh=0;fhnull==e.id?2&e.flags?-1:1/0:e.id;function Sh(e){try{for(dh=0;dh{n._d&&ag(-1);const a=Th(t);let o;try{o=e(...r)}finally{Th(a),n._d&&ag(1)}return o};return n._n=!0,n._c=!0,n._d=!0,n}function $h(e,t){if(null===_h)return e;const r=Lg(_h),n=e.dirs||(e.dirs=[]);for(let e=0;ee.__isTeleport,Ih=e=>e&&(e.disabled||\"\"===e.disabled),Mh=e=>e&&(e.defer||\"\"===e.defer),Nh=e=>\"undefined\"!=typeof SVGElement&&e instanceof SVGElement,Rh=e=>\"function\"==typeof MathMLElement&&e instanceof MathMLElement,jh=(e,t)=>{const r=e&&e.to;return Nu(r)?t?t(r):null:r},Lh={name:\"Teleport\",__isTeleport:!0,process(e,t,r,n,a,o,i,s,l,c){const{mc:u,pc:d,pbc:p,o:{insert:h,querySelector:f,createText:m,createComment:g}}=c,v=Ih(t.props);let{shapeFlag:b,children:y,dynamicChildren:O}=t;if(null==e){const e=t.el=m(\"\"),c=t.anchor=m(\"\");h(e,r,n),h(c,r,n);const d=(e,t)=>{16&b&&(a&&a.isCE&&(a.ce._teleportTarget=e),u(y,e,t,a,o,i,s,l))},p=()=>{const e=t.target=jh(t.props,f),r=Fh(e,t,m,h);e&&(\"svg\"!==i&&Nh(e)?i=\"svg\":\"mathml\"!==i&&Rh(e)&&(i=\"mathml\"),v||(d(e,r),zh(t,!1)))};v&&(d(r,c),zh(t,!0)),Mh(t.props)?(t.el.__isMounted=!1,_m((()=>{p(),delete t.el.__isMounted}),o)):p()}else{if(Mh(t.props)&&!1===e.el.__isMounted)return void _m((()=>{Lh.process(e,t,r,n,a,o,i,s,l,c)}),o);t.el=e.el,t.targetStart=e.targetStart;const u=t.anchor=e.anchor,h=t.target=e.target,m=t.targetAnchor=e.targetAnchor,g=Ih(e.props),b=g?r:h,y=g?u:m;if(\"svg\"===i||Nh(h)?i=\"svg\":(\"mathml\"===i||Rh(h))&&(i=\"mathml\"),O?(p(e.dynamicChildren,O,b,a,o,i,s),$m(e,t,!0)):l||d(e,t,b,y,a,o,i,s,!1),v)g?t.props&&e.props&&t.props.to!==e.props.to&&(t.props.to=e.props.to):Uh(t,r,u,c,1);else if((t.props&&t.props.to)!==(e.props&&e.props.to)){const e=t.target=jh(t.props,f);e&&Uh(t,e,null,c,0)}else g&&Uh(t,h,m,c,1);zh(t,v)}},remove(e,t,r,{um:n,o:{remove:a}},o){const{shapeFlag:i,children:s,anchor:l,targetStart:c,targetAnchor:u,target:d,props:p}=e;if(d&&(a(c),a(u)),o&&a(l),16&i){const e=o||!Ih(p);for(let a=0;a{const t=e.subTree;return t.component?Hh(t.component):t};function Wh(e){let t=e[0];if(e.length>1)for(const r of e)if(r.type!==Km){t=r;break}return t}const Xh={name:\"BaseTransition\",props:Vh,setup(e,{slots:t}){const r=Tg(),n=function(){const e={isMounted:!1,isLeaving:!1,isUnmounting:!1,leavingVNodes:new Map};return yf((()=>{e.isMounted=!0})),xf((()=>{e.isUnmounting=!0})),e}();return()=>{const a=t.default&&tf(t.default(),!0);if(!a||!a.length)return;const o=Wh(a),i=Up(e),{mode:s}=i;if(n.isLeaving)return Kh(o);const l=Jh(o);if(!l)return Kh(o);let c=Yh(l,i,n,r,(e=>c=e));l.type!==Km&&ef(l,c);let u=r.subTree&&Jh(r.subTree);if(u&&u.type!==Km&&!cg(u,l)&&Hh(r).type!==Km){let e=Yh(u,i,n,r);if(ef(u,e),\"out-in\"===s&&l.type!==Km)return n.isLeaving=!0,e.afterLeave=()=>{n.isLeaving=!1,8&r.job.flags||r.update(),delete e.afterLeave,u=void 0},Kh(o);\"in-out\"===s&&l.type!==Km?e.delayLeave=(e,t,r)=>{Gh(n,u)[String(u.key)]=u,e[Qh]=()=>{t(),e[Qh]=void 0,delete c.delayedLeave,u=void 0},c.delayedLeave=()=>{r(),delete c.delayedLeave,u=void 0}}:u=void 0}else u&&(u=void 0);return o}}};function Gh(e,t){const{leavingVNodes:r}=e;let n=r.get(t.type);return n||(n=Object.create(null),r.set(t.type,n)),n}function Yh(e,t,r,n,a){const{appear:o,mode:i,persisted:s=!1,onBeforeEnter:l,onEnter:c,onAfterEnter:u,onEnterCancelled:d,onBeforeLeave:p,onLeave:h,onAfterLeave:f,onLeaveCancelled:m,onBeforeAppear:g,onAppear:v,onAfterAppear:b,onAppearCancelled:y}=t,O=String(e.key),w=Gh(r,e),x=(e,t)=>{e&&lh(e,n,9,t)},k=(e,t)=>{const r=t[1];x(e,t),Cu(e)?e.every((e=>e.length<=1))&&r():e.length<=1&&r()},S={mode:i,persisted:s,beforeEnter(t){let n=l;if(!r.isMounted){if(!o)return;n=g||l}t[Qh]&&t[Qh](!0);const a=w[O];a&&cg(e,a)&&a.el[Qh]&&a.el[Qh](),x(n,[t])},enter(e){let t=c,n=u,a=d;if(!r.isMounted){if(!o)return;t=v||c,n=b||u,a=y||d}let i=!1;const s=e[qh]=t=>{i||(i=!0,x(t?a:n,[e]),S.delayedLeave&&S.delayedLeave(),e[qh]=void 0)};t?k(t,[e,s]):s()},leave(t,n){const a=String(e.key);if(t[qh]&&t[qh](!0),r.isUnmounting)return n();x(p,[t]);let o=!1;const i=t[Qh]=r=>{o||(o=!0,n(),x(r?m:f,[t]),t[Qh]=void 0,w[a]===e&&delete w[a])};w[a]=e,h?k(h,[t,i]):i()},clone(e){const o=Yh(e,t,r,n,a);return a&&a(o),o}};return S}function Kh(e){if(df(e))return(e=mg(e)).children=null,e}function Jh(e){if(!df(e))return Dh(e.type)&&e.children?Wh(e.children):e;if(e.component)return e.component.subTree;const{shapeFlag:t,children:r}=e;if(r){if(16&t)return r[0];if(32&t&&Mu(r.default))return r.default()}}function ef(e,t){6&e.shapeFlag&&e.component?(e.transition=t,ef(e.component.subTree,t)):128&e.shapeFlag?(e.ssContent.transition=t.clone(e.ssContent),e.ssFallback.transition=t.clone(e.ssFallback)):e.transition=t}function tf(e,t=!1,r){let n=[],a=0;for(let o=0;o1)for(let e=0;er.value,set:e=>r.value=e})}return r}const sf=new WeakMap;function lf(e,t,r,n,a=!1){if(Cu(e))return void e.forEach(((e,o)=>lf(e,t&&(Cu(t)?t[o]:t),r,n,a)));if(uf(n)&&!a)return void(512&n.shapeFlag&&n.type.__asyncResolved&&n.component.subTree.component&&lf(e,t,r,n.component.subTree));const o=4&n.shapeFlag?Lg(n.component):n.el,i=a?null:o,{i:s,r:l}=e,c=t&&t.r,u=s.refs===Ou?s.refs={}:s.refs,d=s.setupState,p=Up(d),h=d===Ou?ku:e=>$u(p,e);if(null!=c&&c!==l)if(cf(t),Nu(c))u[c]=null,h(c)&&(d[c]=null);else if(Fp(c)){c.value=null;const e=t;e.k&&(u[e.k]=null)}if(Mu(l))sh(l,s,12,[i,u]);else{const t=Nu(l),n=Fp(l);if(t||n){const s=()=>{if(e.f){const r=t?h(l)?d[l]:u[l]:l.value;if(a)Cu(r)&&Tu(r,o);else if(Cu(r))r.includes(o)||r.push(o);else if(t)u[l]=[o],h(l)&&(d[l]=u[l]);else{const t=[o];l.value=t,e.k&&(u[e.k]=t)}}else t?(u[l]=i,h(l)&&(d[l]=i)):n&&(l.value=i,e.k&&(u[e.k]=i))};if(i){const t=()=>{s(),sf.delete(e)};t.id=-1,sf.set(e,t),_m(t,r)}else cf(e),s()}}}function cf(e){const t=sf.get(e);t&&(t.flags|=8,sf.delete(e))}rd().requestIdleCallback,rd().cancelIdleCallback;const uf=e=>!!e.type.__asyncLoader,df=e=>e.type.__isKeepAlive;function pf(e,t){ff(e,\"a\",t)}function hf(e,t){ff(e,\"da\",t)}function ff(e,t,r=Eg){const n=e.__wdc||(e.__wdc=()=>{let t=r;for(;t;){if(t.isDeactivated)return;t=t.parent}return e()});if(gf(t,n,r),r){let e=r.parent;for(;e&&e.parent;)df(e.parent.vnode)&&mf(n,t,r,e),e=e.parent}}function mf(e,t,r,n){const a=gf(t,e,n,!0);kf((()=>{Tu(n[t],a)}),r)}function gf(e,t,r=Eg,n=!1){if(r){const a=r[e]||(r[e]=[]),o=t.__weh||(t.__weh=(...n)=>{Bd();const a=Cg(r),o=lh(t,r,e,n);return a(),zd(),o});return n?a.unshift(o):a.push(o),o}}const vf=e=>(t,r=Eg)=>{Ig&&\"sp\"!==e||gf(e,((...e)=>t(...e)),r)},bf=vf(\"bm\"),yf=vf(\"m\"),Of=vf(\"bu\"),wf=vf(\"u\"),xf=vf(\"bum\"),kf=vf(\"um\"),Sf=vf(\"sp\"),_f=vf(\"rtg\"),Ef=vf(\"rtc\");function Tf(e,t=Eg){gf(\"ec\",e,t)}const Af=\"components\";function $f(e,t){return Df(Af,e,!0,t)||e}const Cf=Symbol.for(\"v-ndc\");function Pf(e){return Nu(e)?Df(Af,e,!1)||e:e||Cf}function Df(e,t,r=!0,n=!1){const a=_h||Eg;if(a){const r=a.type;{const e=function(e,t=!0){return Mu(e)?e.displayName||e.name:e.name||t&&e.__name}(r,!1);if(e&&(e===t||e===Vu(t)||e===Xu(Vu(t))))return r}const o=If(a[e]||r[e],t)||If(a.appContext[e],t);return!o&&n?r:o}}function If(e,t){return e&&(e[t]||e[Vu(t)]||e[Xu(Vu(t))])}function Mf(e,t,r,n){let a;const o=r,i=Cu(e);if(i||Nu(e)){let r=!1,n=!1;i&&Np(e)&&(r=!jp(e),n=Rp(e),e=ep(e)),a=new Array(e.length);for(let i=0,s=e.length;it(e,r,void 0,o)));else{const r=Object.keys(e);a=new Array(r.length);for(let n=0,i=r.length;n{const t=n.fn(...e);return t&&(t.key=n.key),t}:n.fn)}return e}function Rf(e,t,r={},n,a){if(_h.ce||_h.parent&&uf(_h.parent)&&_h.parent.ce)return\"default\"!==t&&(r.name=t),rg(),sg(Gm,null,[hg(\"slot\",r,n&&n())],64);let o=e[t];o&&o._c&&(o._d=!1),rg();const i=o&&jf(o(r)),s=r.key||i&&i.key,l=sg(Gm,{key:(s&&!Ru(s)?s:`_${t}`)+(!i&&n?\"_fb\":\"\")},i||(n?n():[]),i&&1===e._?64:-2);return!a&&l.scopeId&&(l.slotScopeIds=[l.scopeId+\"-s\"]),o&&o._c&&(o._d=!0),l}function jf(e){return e.some((e=>!lg(e)||e.type!==Km&&!(e.type===Gm&&!jf(e.children))))?e:null}const Lf=e=>e?Dg(e)?Lg(e):Lf(e.parent):null,Uf=Eu(Object.create(null),{$:e=>e,$el:e=>e.vnode.el,$data:e=>e.data,$props:e=>e.props,$attrs:e=>e.attrs,$slots:e=>e.slots,$refs:e=>e.refs,$parent:e=>Lf(e.parent),$root:e=>Lf(e.root),$host:e=>e.ce,$emit:e=>e.emit,$options:e=>Gf(e),$forceUpdate:e=>e.f||(e.f=()=>{bh(e.update)}),$nextTick:e=>e.n||(e.n=vh.bind(e.proxy)),$watch:e=>jm.bind(e)}),Bf=(e,t)=>e!==Ou&&!e.__isScriptSetup&&$u(e,t),zf={get({_:e},t){if(\"__v_skip\"===t)return!0;const{ctx:r,setupState:n,data:a,props:o,accessCache:i,type:s,appContext:l}=e;let c;if(\"$\"!==t[0]){const s=i[t];if(void 0!==s)switch(s){case 1:return n[t];case 2:return a[t];case 4:return r[t];case 3:return o[t]}else{if(Bf(n,t))return i[t]=1,n[t];if(a!==Ou&&$u(a,t))return i[t]=2,a[t];if((c=e.propsOptions[0])&&$u(c,t))return i[t]=3,o[t];if(r!==Ou&&$u(r,t))return i[t]=4,r[t];Hf&&(i[t]=0)}}const u=Uf[t];let d,p;return u?(\"$attrs\"===t&&Yd(e.attrs,0,\"\"),u(e)):(d=s.__cssModules)&&(d=d[t])?d:r!==Ou&&$u(r,t)?(i[t]=4,r[t]):(p=l.config.globalProperties,$u(p,t)?p[t]:void 0)},set({_:e},t,r){const{data:n,setupState:a,ctx:o}=e;return Bf(a,t)?(a[t]=r,!0):n!==Ou&&$u(n,t)?(n[t]=r,!0):!($u(e.props,t)||\"$\"===t[0]&&t.slice(1)in e||(o[t]=r,0))},has({_:{data:e,setupState:t,accessCache:r,ctx:n,appContext:a,propsOptions:o,type:i}},s){let l,c;return!!(r[s]||e!==Ou&&\"$\"!==s[0]&&$u(e,s)||Bf(t,s)||(l=o[0])&&$u(l,s)||$u(n,s)||$u(Uf,s)||$u(a.config.globalProperties,s)||(c=i.__cssModules)&&c[s])},defineProperty(e,t,r){return null!=r.get?e._.accessCache[t]=0:$u(r,\"value\")&&this.set(e,t,r.value,null),Reflect.defineProperty(e,t,r)}};function Ff(){return qf().slots}function Qf(){return qf().attrs}function qf(e){const t=Tg();return t.setupContext||(t.setupContext=jg(t))}function Zf(e){return Cu(e)?e.reduce(((e,t)=>(e[t]=null,e)),{}):e}function Vf(e,t){return e&&t?Cu(e)&&Cu(t)?e.concat(t):Eu({},Zf(e),Zf(t)):e||t}let Hf=!0;function Wf(e,t,r){lh(Cu(e)?e.map((e=>e.bind(t.proxy))):e.bind(t.proxy),t,r)}function Xf(e,t,r,n){let a=n.includes(\".\")?Lm(r,n):()=>r[n];if(Nu(e)){const r=t[e];Mu(r)&&Nm(a,r)}else if(Mu(e))Nm(a,e.bind(r));else if(ju(e))if(Cu(e))e.forEach((e=>Xf(e,t,r,n)));else{const n=Mu(e.handler)?e.handler.bind(r):t[e.handler];Mu(n)&&Nm(a,n,e)}}function Gf(e){const t=e.type,{mixins:r,extends:n}=t,{mixins:a,optionsCache:o,config:{optionMergeStrategies:i}}=e.appContext,s=o.get(t);let l;return s?l=s:a.length||r||n?(l={},a.length&&a.forEach((e=>Yf(l,e,i,!0))),Yf(l,t,i)):l=t,ju(t)&&o.set(t,l),l}function Yf(e,t,r,n=!1){const{mixins:a,extends:o}=t;o&&Yf(e,o,r,!0),a&&a.forEach((t=>Yf(e,t,r,!0)));for(const a in t)if(n&&\"expose\"===a);else{const n=Kf[a]||r&&r[a];e[a]=n?n(e[a],t[a]):t[a]}return e}const Kf={data:Jf,props:nm,emits:nm,methods:rm,computed:rm,beforeCreate:tm,created:tm,beforeMount:tm,mounted:tm,beforeUpdate:tm,updated:tm,beforeDestroy:tm,beforeUnmount:tm,destroyed:tm,unmounted:tm,activated:tm,deactivated:tm,errorCaptured:tm,serverPrefetch:tm,components:rm,directives:rm,watch:function(e,t){if(!e)return t;if(!t)return e;const r=Eu(Object.create(null),e);for(const n in t)r[n]=tm(e[n],t[n]);return r},provide:Jf,inject:function(e,t){return rm(em(e),em(t))}};function Jf(e,t){return t?e?function(){return Eu(Mu(e)?e.call(this,this):e,Mu(t)?t.call(this,this):t)}:t:e}function em(e){if(Cu(e)){const t={};for(let r=0;r(a.has(e)||(e&&Mu(e.install)?(a.add(e),e.install(s,...t)):Mu(e)&&(a.add(e),e(s,...t))),s),mixin:e=>(n.mixins.includes(e)||n.mixins.push(e),s),component:(e,t)=>t?(n.components[e]=t,s):n.components[e],directive:(e,t)=>t?(n.directives[e]=t,s):n.directives[e],mount(a,o,l){if(!i){const o=s._ceVNode||hg(t,r);return o.appContext=n,!0===l?l=\"svg\":!1===l&&(l=void 0),e(o,a,l),i=!0,s._container=a,a.__vue_app__=s,Lg(o.component)}},onUnmount(e){o.push(e)},unmount(){i&&(lh(o,s._instance,16),e(null,s._container),delete s._container.__vue_app__)},provide:(e,t)=>(n.provides[e]=t,s),runWithContext(e){const t=sm;sm=s;try{return e()}finally{sm=t}}};return s}}let sm=null;function lm(e,t){if(Eg){let r=Eg.provides;const n=Eg.parent&&Eg.parent.provides;n===r&&(r=Eg.provides=Object.create(n)),r[e]=t}}function cm(e,t,r=!1){const n=Tg();if(n||sm){let a=sm?sm._context.provides:n?null==n.parent||n.ce?n.vnode.appContext&&n.vnode.appContext.provides:n.parent.provides:void 0;if(a&&e in a)return a[e];if(arguments.length>1)return r&&Mu(t)?t.call(n&&n.proxy):t}}function um(){return!(!Tg()&&!sm)}const dm={},pm=()=>Object.create(dm),hm=e=>Object.getPrototypeOf(e)===dm;function fm(e,t,r,n){const[a,o]=e.propsOptions;let i,s=!1;if(t)for(let l in t){if(Qu(l))continue;const c=t[l];let u;a&&$u(a,u=Vu(l))?o&&o.includes(u)?(i||(i={}))[u]=c:r[u]=c:qm(e.emitsOptions,l)||l in n&&c===n[l]||(n[l]=c,s=!0)}if(o){const t=Up(r),n=i||Ou;for(let i=0;i{l=!0;const[r,n]=vm(e,t,!0);Eu(i,r),n&&s.push(...n)};!r&&t.mixins.length&&t.mixins.forEach(n),e.extends&&n(e.extends),e.mixins&&e.mixins.forEach(n)}if(!o&&!l)return ju(e)&&n.set(e,wu),wu;if(Cu(o))for(let e=0;e\"_\"===e||\"_ctx\"===e||\"$stable\"===e,Om=e=>Cu(e)?e.map(yg):[yg(e)],wm=(e,t,r)=>{if(t._n)return t;const n=Ah(((...e)=>Om(t(...e))),r);return n._c=!1,n},xm=(e,t,r)=>{const n=e._ctx;for(const r in e){if(ym(r))continue;const a=e[r];if(Mu(a))t[r]=wm(0,a,n);else if(null!=a){const e=Om(a);t[r]=()=>e}}},km=(e,t)=>{const r=Om(t);e.slots.default=()=>r},Sm=(e,t,r)=>{for(const n in t)!r&&ym(n)||(e[n]=t[n])},_m=function(e,t){t&&t.pendingBranch?Cu(e)?t.effects.push(...e):t.effects.push(e):Oh(e)};function Em(e){return function(e){rd().__VUE__=!0;const{insert:t,remove:r,patchProp:n,createElement:a,createText:o,createComment:i,setText:s,setElementText:l,parentNode:c,nextSibling:u,setScopeId:d=xu,insertStaticContent:p}=e,h=(e,t,r,n=null,a=null,o=null,i=void 0,s=null,l=!!t.dynamicChildren)=>{if(e===t)return;e&&!cg(e,t)&&(n=B(e),N(e,a,o,!0),e=null),-2===t.patchFlag&&(l=!1,t.dynamicChildren=null);const{type:c,ref:u,shapeFlag:d}=t;switch(c){case Ym:f(e,t,r,n);break;case Km:m(e,t,r,n);break;case Jm:null==e&&g(t,r,n,i);break;case Gm:_(e,t,r,n,a,o,i,s,l);break;default:1&d?b(e,t,r,n,a,o,i,s,l):6&d?E(e,t,r,n,a,o,i,s,l):(64&d||128&d)&&c.process(e,t,r,n,a,o,i,s,l,Q)}null!=u&&a?lf(u,e&&e.ref,o,t||e,!t):null==u&&e&&null!=e.ref&&lf(e.ref,null,o,e,!0)},f=(e,r,n,a)=>{if(null==e)t(r.el=o(r.children),n,a);else{const t=r.el=e.el;r.children!==e.children&&s(t,r.children)}},m=(e,r,n,a)=>{null==e?t(r.el=i(r.children||\"\"),n,a):r.el=e.el},g=(e,t,r,n)=>{[e.el,e.anchor]=p(e.children,t,r,n,e.el,e.anchor)},v=({el:e,anchor:r},n,a)=>{let o;for(;e&&e!==r;)o=u(e),t(e,n,a),e=o;t(r,n,a)},b=(e,t,r,n,a,o,i,s,l)=>{\"svg\"===t.type?i=\"svg\":\"math\"===t.type&&(i=\"mathml\"),null==e?y(t,r,n,a,o,i,s,l):x(e,t,a,o,i,s,l)},y=(e,r,o,i,s,c,u,d)=>{let p,h;const{props:f,shapeFlag:m,transition:g,dirs:v}=e;if(p=e.el=a(e.type,c,f&&f.is,f),8&m?l(p,e.children):16&m&&w(e.children,p,null,i,s,Tm(e,c),u,d),v&&Ch(e,null,i,\"created\"),O(p,e,e.scopeId,u,i),f){for(const e in f)\"value\"===e||Qu(e)||n(p,e,null,f[e],c,i);\"value\"in f&&n(p,\"value\",null,f.value,c),(h=f.onVnodeBeforeMount)&&kg(h,i,e)}v&&Ch(e,null,i,\"beforeMount\");const b=function(e,t){return(!e||e&&!e.pendingBranch)&&t&&!t.persisted}(s,g);b&&g.beforeEnter(p),t(p,r,o),((h=f&&f.onVnodeMounted)||b||v)&&_m((()=>{h&&kg(h,i,e),b&&g.enter(p),v&&Ch(e,null,i,\"mounted\")}),s)},O=(e,t,r,n,a)=>{if(r&&d(e,r),n)for(let t=0;t{for(let c=l;c{const c=t.el=e.el;let{patchFlag:u,dynamicChildren:d,dirs:p}=t;u|=16&e.patchFlag;const h=e.props||Ou,f=t.props||Ou;let m;if(r&&Am(r,!1),(m=f.onVnodeBeforeUpdate)&&kg(m,r,t,e),p&&Ch(t,e,r,\"beforeUpdate\"),r&&Am(r,!0),(h.innerHTML&&null==f.innerHTML||h.textContent&&null==f.textContent)&&l(c,\"\"),d?k(e.dynamicChildren,d,c,r,a,Tm(t,o),i):s||P(e,t,c,null,r,a,Tm(t,o),i,!1),u>0){if(16&u)S(c,h,f,r,o);else if(2&u&&h.class!==f.class&&n(c,\"class\",null,f.class,o),4&u&&n(c,\"style\",h.style,f.style,o),8&u){const e=t.dynamicProps;for(let t=0;t{m&&kg(m,r,t,e),p&&Ch(t,e,r,\"updated\")}),a)},k=(e,t,r,n,a,o,i)=>{for(let s=0;s{if(t!==r){if(t!==Ou)for(const i in t)Qu(i)||i in r||n(e,i,t[i],null,o,a);for(const i in r){if(Qu(i))continue;const s=r[i],l=t[i];s!==l&&\"value\"!==i&&n(e,i,l,s,o,a)}\"value\"in r&&n(e,\"value\",t.value,r.value,o)}},_=(e,r,n,a,i,s,l,c,u)=>{const d=r.el=e?e.el:o(\"\"),p=r.anchor=e?e.anchor:o(\"\");let{patchFlag:h,dynamicChildren:f,slotScopeIds:m}=r;m&&(c=c?c.concat(m):m),null==e?(t(d,n,a),t(p,n,a),w(r.children||[],n,p,i,s,l,c,u)):h>0&&64&h&&f&&e.dynamicChildren?(k(e.dynamicChildren,f,n,i,s,l,c),(null!=r.key||i&&r===i.subTree)&&$m(e,r,!0)):P(e,r,n,p,i,s,l,c,u)},E=(e,t,r,n,a,o,i,s,l)=>{t.slotScopeIds=s,null==e?512&t.shapeFlag?a.ctx.activate(t,r,n,i,l):T(t,r,n,a,o,i,l):A(e,t,l)},T=(e,t,r,n,a,o,i)=>{const s=e.component=function(e,t,r){const n=e.type,a=(t?t.appContext:e.appContext)||Sg,o={uid:_g++,vnode:e,type:n,parent:t,appContext:a,root:null,next:null,subTree:null,effect:null,update:null,job:null,scope:new wd(!0),render:null,proxy:null,exposed:null,exposeProxy:null,withProxy:null,provides:t?t.provides:Object.create(a.provides),ids:t?t.ids:[\"\",0,0],accessCache:null,renderCache:[],components:null,directives:null,propsOptions:vm(n,a),emitsOptions:Qm(n,a),emit:null,emitted:null,propsDefaults:Ou,inheritAttrs:n.inheritAttrs,ctx:Ou,data:Ou,props:Ou,attrs:Ou,slots:Ou,refs:Ou,setupState:Ou,setupContext:null,suspense:r,suspenseId:r?r.pendingId:0,asyncDep:null,asyncResolved:!1,isMounted:!1,isUnmounted:!1,isDeactivated:!1,bc:null,c:null,bm:null,m:null,bu:null,u:null,um:null,bum:null,da:null,a:null,rtg:null,rtc:null,ec:null,sp:null};return o.ctx={_:o},o.root=t?t.root:o,o.emit=zm.bind(null,o),e.ce&&e.ce(o),o}(e,n,a);if(df(e)&&(s.ctx.renderer=Q),function(e,t=!1,r=!1){t&&$g(t);const{props:n,children:a}=e.vnode,o=Dg(e);(function(e,t,r,n=!1){const a={},o=pm();e.propsDefaults=Object.create(null),fm(e,t,a,o);for(const t in e.propsOptions[0])t in a||(a[t]=void 0);r?e.props=n?a:Pp(a):e.type.props?e.props=a:e.props=o,e.attrs=o})(e,n,o,t),((e,t,r)=>{const n=e.slots=pm();if(32&e.vnode.shapeFlag){const e=t._;e?(Sm(n,t,r),r&&Ju(n,\"_\",e,!0)):xm(t,n)}else t&&km(e,t)})(e,a,r||t);o&&function(e,t){const r=e.type;e.accessCache=Object.create(null),e.proxy=new Proxy(e.ctx,zf);const{setup:n}=r;if(n){Bd();const r=e.setupContext=n.length>1?jg(e):null,a=Cg(e),o=sh(n,e,0,[e.props,r]),i=Lu(o);if(zd(),a(),!i&&!e.sp||uf(e)||af(e),i){if(o.then(Pg,Pg),t)return o.then((t=>{Mg(e,t)})).catch((t=>{ch(t,e,0)}));e.asyncDep=o}else Mg(e,o)}else Ng(e)}(e,t);t&&$g(!1)}(s,!1,i),s.asyncDep){if(a&&a.registerDep(s,$,i),!e.el){const n=s.subTree=hg(Km);m(null,n,t,r),e.placeholder=n.el}}else $(s,e,t,r,a,o,i)},A=(e,t,r)=>{const n=t.component=e.component;if(function(e,t,r){const{props:n,children:a,component:o}=e,{props:i,children:s,patchFlag:l}=t,c=o.emitsOptions;if(t.dirs||t.transition)return!0;if(!(r&&l>=0))return!(!a&&!s||s&&s.$stable)||n!==i&&(n?!i||Wm(n,i,c):!!i);if(1024&l)return!0;if(16&l)return n?Wm(n,i,c):!!i;if(8&l){const e=t.dynamicProps;for(let t=0;t{const s=()=>{if(e.isMounted){let{next:t,bu:r,u:n,parent:l,vnode:u}=e;{const r=Cm(e);if(r)return t&&(t.el=u.el,C(e,t,i)),void r.asyncDep.then((()=>{e.isUnmounted||s()}))}let d,p=t;Am(e,!1),t?(t.el=u.el,C(e,t,i)):t=u,r&&Ku(r),(d=t.props&&t.props.onVnodeBeforeUpdate)&&kg(d,l,t,u),Am(e,!0);const f=Zm(e),m=e.subTree;e.subTree=f,h(m,f,c(m.el),B(m),e,a,o),t.el=f.el,null===p&&function({vnode:e,parent:t},r){for(;t;){const n=t.subTree;if(n.suspense&&n.suspense.activeBranch===e&&(n.el=e.el),n!==e)break;(e=t.vnode).el=r,t=t.parent}}(e,f.el),n&&_m(n,a),(d=t.props&&t.props.onVnodeUpdated)&&_m((()=>kg(d,l,t,u)),a)}else{let i;const{el:s,props:l}=t,{bm:c,m:u,parent:d,root:p,type:f}=e,m=uf(t);Am(e,!1),c&&Ku(c),!m&&(i=l&&l.onVnodeBeforeMount)&&kg(i,d,t),Am(e,!0);{p.ce&&!1!==p.ce._def.shadowRoot&&p.ce._injectChildStyle(f);const i=e.subTree=Zm(e);h(null,i,r,n,e,a,o),t.el=i.el}if(u&&_m(u,a),!m&&(i=l&&l.onVnodeMounted)){const e=t;_m((()=>kg(i,d,e)),a)}(256&t.shapeFlag||d&&uf(d.vnode)&&256&d.vnode.shapeFlag)&&e.a&&_m(e.a,a),e.isMounted=!0,t=r=n=null}};e.scope.on();const l=e.effect=new _d(s);e.scope.off();const u=e.update=l.run.bind(l),d=e.job=l.runIfDirty.bind(l);d.i=e,d.id=e.uid,l.scheduler=()=>bh(d),Am(e,!0),u()},C=(e,t,r)=>{t.component=e;const n=e.vnode.props;e.vnode=t,e.next=null,function(e,t,r,n){const{props:a,attrs:o,vnode:{patchFlag:i}}=e,s=Up(a),[l]=e.propsOptions;let c=!1;if(!(n||i>0)||16&i){let n;fm(e,t,a,o)&&(c=!0);for(const o in s)t&&($u(t,o)||(n=Wu(o))!==o&&$u(t,n))||(l?!r||void 0===r[o]&&void 0===r[n]||(a[o]=mm(l,s,o,void 0,e,!0)):delete a[o]);if(o!==s)for(const e in o)t&&$u(t,e)||(delete o[e],c=!0)}else if(8&i){const r=e.vnode.dynamicProps;for(let n=0;n{const{vnode:n,slots:a}=e;let o=!0,i=Ou;if(32&n.shapeFlag){const e=t._;e?r&&1===e?o=!1:Sm(a,t,r):(o=!t.$stable,xm(t,a)),i=t}else t&&(km(e,t),i={default:1});if(o)for(const e in a)ym(e)||null!=i[e]||delete a[e]})(e,t.children,r),Bd(),wh(e),zd()},P=(e,t,r,n,a,o,i,s,c=!1)=>{const u=e&&e.children,d=e?e.shapeFlag:0,p=t.children,{patchFlag:h,shapeFlag:f}=t;if(h>0){if(128&h)return void I(u,p,r,n,a,o,i,s,c);if(256&h)return void D(u,p,r,n,a,o,i,s,c)}8&f?(16&d&&U(u,a,o),p!==u&&l(r,p)):16&d?16&f?I(u,p,r,n,a,o,i,s,c):U(u,a,o,!0):(8&d&&l(r,\"\"),16&f&&w(p,r,n,a,o,i,s,c))},D=(e,t,r,n,a,o,i,s,l)=>{t=t||wu;const c=(e=e||wu).length,u=t.length,d=Math.min(c,u);let p;for(p=0;pu?U(e,a,o,!0,!1,d):w(t,r,n,a,o,i,s,l,d)},I=(e,t,r,n,a,o,i,s,l)=>{let c=0;const u=t.length;let d=e.length-1,p=u-1;for(;c<=d&&c<=p;){const n=e[c],u=t[c]=l?Og(t[c]):yg(t[c]);if(!cg(n,u))break;h(n,u,r,null,a,o,i,s,l),c++}for(;c<=d&&c<=p;){const n=e[d],c=t[p]=l?Og(t[p]):yg(t[p]);if(!cg(n,c))break;h(n,c,r,null,a,o,i,s,l),d--,p--}if(c>d){if(c<=p){const e=p+1,d=ep)for(;c<=d;)N(e[c],a,o,!0),c++;else{const f=c,m=c,g=new Map;for(c=m;c<=p;c++){const e=t[c]=l?Og(t[c]):yg(t[c]);null!=e.key&&g.set(e.key,c)}let v,b=0;const y=p-m+1;let O=!1,w=0;const x=new Array(y);for(c=0;c=y){N(n,a,o,!0);continue}let u;if(null!=n.key)u=g.get(n.key);else for(v=m;v<=p;v++)if(0===x[v-m]&&cg(n,t[v])){u=v;break}void 0===u?N(n,a,o,!0):(x[u-m]=c+1,u>=w?w=u:O=!0,h(n,t[u],r,null,a,o,i,s,l),b++)}const k=O?function(e){const t=e.slice(),r=[0];let n,a,o,i,s;const l=e.length;for(n=0;n>1,e[r[s]]0&&(t[n]=r[o-1]),r[o]=n)}}for(o=r.length,i=r[o-1];o-- >0;)r[o]=i,i=t[i];return r}(x):wu;for(v=k.length-1,c=y-1;c>=0;c--){const e=m+c,d=t[e],p=t[e+1],f=e+1{const{el:s,type:l,transition:c,children:u,shapeFlag:d}=e;if(6&d)M(e.component.subTree,n,a,o);else if(128&d)e.suspense.move(n,a,o);else if(64&d)l.move(e,n,a,Q);else if(l!==Gm)if(l!==Jm)if(2!==o&&1&d&&c)if(0===o)c.beforeEnter(s),t(s,n,a),_m((()=>c.enter(s)),i);else{const{leave:o,delayLeave:i,afterLeave:l}=c,u=()=>{e.ctx.isUnmounted?r(s):t(s,n,a)},d=()=>{s._isLeaving&&s[Qh](!0),o(s,(()=>{u(),l&&l()}))};i?i(s,u,d):d()}else t(s,n,a);else v(e,n,a);else{t(s,n,a);for(let e=0;e{const{type:o,props:i,ref:s,children:l,dynamicChildren:c,shapeFlag:u,patchFlag:d,dirs:p,cacheIndex:h}=e;if(-2===d&&(a=!1),null!=s&&(Bd(),lf(s,null,r,e,!0),zd()),null!=h&&(t.renderCache[h]=void 0),256&u)return void t.ctx.deactivate(e);const f=1&u&&p,m=!uf(e);let g;if(m&&(g=i&&i.onVnodeBeforeUnmount)&&kg(g,t,e),6&u)L(e.component,r,n);else{if(128&u)return void e.suspense.unmount(r,n);f&&Ch(e,null,t,\"beforeUnmount\"),64&u?e.type.remove(e,t,r,Q,n):c&&!c.hasOnce&&(o!==Gm||d>0&&64&d)?U(c,t,r,!1,!0):(o===Gm&&384&d||!a&&16&u)&&U(l,t,r),n&&R(e)}(m&&(g=i&&i.onVnodeUnmounted)||f)&&_m((()=>{g&&kg(g,t,e),f&&Ch(e,null,t,\"unmounted\")}),r)},R=e=>{const{type:t,el:n,anchor:a,transition:o}=e;if(t===Gm)return void j(n,a);if(t===Jm)return void(({el:e,anchor:t})=>{let n;for(;e&&e!==t;)n=u(e),r(e),e=n;r(t)})(e);const i=()=>{r(n),o&&!o.persisted&&o.afterLeave&&o.afterLeave()};if(1&e.shapeFlag&&o&&!o.persisted){const{leave:t,delayLeave:r}=o,a=()=>t(n,i);r?r(e.el,i,a):a()}else i()},j=(e,t)=>{let n;for(;e!==t;)n=u(e),r(e),e=n;r(t)},L=(e,t,r)=>{const{bum:n,scope:a,job:o,subTree:i,um:s,m:l,a:c}=e;Pm(l),Pm(c),n&&Ku(n),a.stop(),o&&(o.flags|=8,N(i,e,t,r)),s&&_m(s,t),_m((()=>{e.isUnmounted=!0}),t)},U=(e,t,r,n=!1,a=!1,o=0)=>{for(let i=o;i{if(6&e.shapeFlag)return B(e.component.subTree);if(128&e.shapeFlag)return e.suspense.next();const t=u(e.anchor||e.el),r=t&&t[Ph];return r?u(r):t};let z=!1;const F=(e,t,r)=>{null==e?t._vnode&&N(t._vnode,null,null,!0):h(t._vnode||null,e,t,null,null,null,r),t._vnode=e,z||(z=!0,wh(),xh(),z=!1)},Q={p:h,um:N,m:M,r:R,mt:T,mc:w,pc:P,pbc:k,n:B,o:e};return{render:F,hydrate:undefined,createApp:im(F)}}(e)}function Tm({type:e,props:t},r){return\"svg\"===r&&\"foreignObject\"===e||\"mathml\"===r&&\"annotation-xml\"===e&&t&&t.encoding&&t.encoding.includes(\"html\")?void 0:r}function Am({effect:e,job:t},r){r?(e.flags|=32,t.flags|=4):(e.flags&=-33,t.flags&=-5)}function $m(e,t,r=!1){const n=e.children,a=t.children;if(Cu(n)&&Cu(a))for(let e=0;ecm(Dm);function Mm(e,t){return Rm(e,null,t)}function Nm(e,t,r){return Rm(e,t,r)}function Rm(e,t,r=Ou){const{immediate:n,deep:a,flush:o,once:i}=r,s=Eu({},r),l=t&&n||!t&&\"post\"!==o;let c;if(Ig)if(\"sync\"===o){const e=Im();c=e.__watcherHandles||(e.__watcherHandles=[])}else if(!l){const e=()=>{};return e.stop=xu,e.resume=xu,e.pause=xu,e}const u=Eg;s.call=(e,t,r)=>lh(e,u,t,r);let d=!1;\"post\"===o?s.scheduler=e=>{_m(e,u&&u.suspense)}:\"sync\"!==o&&(d=!0,s.scheduler=(e,t)=>{t?e():bh(e)}),s.augmentJob=e=>{t&&(e.flags|=4),d&&(e.flags|=2,u&&(e.id=u.uid,e.i=u))};const p=function(e,t,r=Ou){const{immediate:n,deep:a,once:o,scheduler:i,augmentJob:s,call:l}=r,c=e=>a?e:jp(e)||!1===a||0===a?ih(e,1):ih(e);let u,d,p,h,f=!1,m=!1;if(Fp(e)?(d=()=>e.value,f=jp(e)):Np(e)?(d=()=>c(e),f=!0):Cu(e)?(m=!0,f=e.some((e=>Np(e)||jp(e))),d=()=>e.map((e=>Fp(e)?e.value:Np(e)?c(e):Mu(e)?l?l(e,2):e():void 0))):d=Mu(e)?t?l?()=>l(e,2):e:()=>{if(p){Bd();try{p()}finally{zd()}}const t=oh;oh=u;try{return l?l(e,3,[h]):e(h)}finally{oh=t}}:xu,t&&a){const e=d,t=!0===a?1/0:a;d=()=>ih(e(),t)}const g=xd(),v=()=>{u.stop(),g&&g.active&&Tu(g.effects,u)};if(o&&t){const e=t;t=(...t)=>{e(...t),v()}}let b=m?new Array(e.length).fill(nh):nh;const y=e=>{if(1&u.flags&&(u.dirty||e))if(t){const e=u.run();if(a||f||(m?e.some(((e,t)=>Yu(e,b[t]))):Yu(e,b))){p&&p();const r=oh;oh=u;try{const r=[e,b===nh?void 0:m&&b[0]===nh?[]:b,h];b=e,l?l(t,3,r):t(...r)}finally{oh=r}}}else u.run()};return s&&s(y),u=new _d(d),u.scheduler=i?()=>i(y,!1):y,h=e=>function(e,t=!1,r=oh){if(r){let t=ah.get(r);t||ah.set(r,t=[]),t.push(e)}}(e,!1,u),p=u.onStop=()=>{const e=ah.get(u);if(e){if(l)l(e,4);else for(const t of e)t();ah.delete(u)}},t?n?y(!0):b=u.run():i?i(y.bind(null,!0),!0):u.run(),v.pause=u.pause.bind(u),v.resume=u.resume.bind(u),v.stop=v,v}(e,t,s);return Ig&&(c?c.push(p):l&&p()),p}function jm(e,t,r){const n=this.proxy,a=Nu(e)?e.includes(\".\")?Lm(n,e):()=>n[e]:e.bind(n,n);let o;Mu(t)?o=t:(o=t.handler,r=t);const i=Cg(this),s=Rm(a,o.bind(n),r);return i(),s}function Lm(e,t){const r=t.split(\".\");return()=>{let t=e;for(let e=0;e{let l,c,u=Ou;return Rm((()=>{const t=e[a];Yu(l,t)&&(l=t,s())}),null,{flush:\"sync\"}),{get:()=>(i(),r.get?r.get(l):l),set(e){const i=r.set?r.set(e):e;if(!(Yu(i,l)||u!==Ou&&Yu(e,u)))return;const d=n.vnode.props;d&&(t in d||a in d||o in d)&&(`onUpdate:${t}`in d||`onUpdate:${a}`in d||`onUpdate:${o}`in d)||(l=e,s()),n.emit(`update:${t}`,i),Yu(e,i)&&Yu(e,u)&&!Yu(i,c)&&s(),u=e,c=i}}}));return s[Symbol.iterator]=()=>{let e=0;return{next:()=>e<2?{value:e++?i||Ou:s,done:!1}:{done:!0}}},s}const Bm=(e,t)=>\"modelValue\"===t||\"model-value\"===t?e.modelModifiers:e[`${t}Modifiers`]||e[`${Vu(t)}Modifiers`]||e[`${Wu(t)}Modifiers`];function zm(e,t,...r){if(e.isUnmounted)return;const n=e.vnode.props||Ou;let a=r;const o=t.startsWith(\"update:\"),i=o&&Bm(n,t.slice(7));let s;i&&(i.trim&&(a=r.map((e=>Nu(e)?e.trim():e))),i.number&&(a=r.map(ed)));let l=n[s=Gu(t)]||n[s=Gu(Vu(t))];!l&&o&&(l=n[s=Gu(Wu(t))]),l&&lh(l,e,6,a);const c=n[s+\"Once\"];if(c){if(e.emitted){if(e.emitted[s])return}else e.emitted={};e.emitted[s]=!0,lh(c,e,6,a)}}const Fm=new WeakMap;function Qm(e,t,r=!1){const n=r?Fm:t.emitsCache,a=n.get(e);if(void 0!==a)return a;const o=e.emits;let i={},s=!1;if(!Mu(e)){const n=e=>{const r=Qm(e,t,!0);r&&(s=!0,Eu(i,r))};!r&&t.mixins.length&&t.mixins.forEach(n),e.extends&&n(e.extends),e.mixins&&e.mixins.forEach(n)}return o||s?(Cu(o)?o.forEach((e=>i[e]=null)):Eu(i,o),ju(e)&&n.set(e,i),i):(ju(e)&&n.set(e,null),null)}function qm(e,t){return!(!e||!Su(t))&&(t=t.slice(2).replace(/Once$/,\"\"),$u(e,t[0].toLowerCase()+t.slice(1))||$u(e,Wu(t))||$u(e,t))}function Zm(e){const{type:t,vnode:r,proxy:n,withProxy:a,propsOptions:[o],slots:i,attrs:s,emit:l,render:c,renderCache:u,props:d,data:p,setupState:h,ctx:f,inheritAttrs:m}=e,g=Th(e);let v,b;try{if(4&r.shapeFlag){const e=a||n,t=e;v=yg(c.call(t,e,u,d,h,p,f)),b=s}else{const e=t;v=yg(e.length>1?e(d,{attrs:s,slots:i,emit:l}):e(d,null)),b=t.props?s:Vm(s)}}catch(t){eg.length=0,ch(t,e,1),v=hg(Km)}let y=v;if(b&&!1!==m){const e=Object.keys(b),{shapeFlag:t}=y;e.length&&7&t&&(o&&e.some(_u)&&(b=Hm(b,o)),y=mg(y,b,!1,!0))}return r.dirs&&(y=mg(y,null,!1,!0),y.dirs=y.dirs?y.dirs.concat(r.dirs):r.dirs),r.transition&&ef(y,r.transition),v=y,Th(g),v}const Vm=e=>{let t;for(const r in e)(\"class\"===r||\"style\"===r||Su(r))&&((t||(t={}))[r]=e[r]);return t},Hm=(e,t)=>{const r={};for(const n in e)_u(n)&&n.slice(9)in t||(r[n]=e[n]);return r};function Wm(e,t,r){const n=Object.keys(t);if(n.length!==Object.keys(e).length)return!0;for(let a=0;ae.__isSuspense,Gm=Symbol.for(\"v-fgt\"),Ym=Symbol.for(\"v-txt\"),Km=Symbol.for(\"v-cmt\"),Jm=Symbol.for(\"v-stc\"),eg=[];let tg=null;function rg(e=!1){eg.push(tg=e?null:[])}let ng=1;function ag(e,t=!1){ng+=e,e<0&&tg&&t&&(tg.hasOnce=!0)}function og(e){return e.dynamicChildren=ng>0?tg||wu:null,eg.pop(),tg=eg[eg.length-1]||null,ng>0&&tg&&tg.push(e),e}function ig(e,t,r,n,a,o){return og(pg(e,t,r,n,a,o,!0))}function sg(e,t,r,n,a){return og(hg(e,t,r,n,a,!0))}function lg(e){return!!e&&!0===e.__v_isVNode}function cg(e,t){return e.type===t.type&&e.key===t.key}const ug=({key:e})=>null!=e?e:null,dg=({ref:e,ref_key:t,ref_for:r})=>(\"number\"==typeof e&&(e=\"\"+e),null!=e?Nu(e)||Fp(e)||Mu(e)?{i:_h,r:e,k:t,f:!!r}:e:null);function pg(e,t=null,r=null,n=0,a=null,o=(e===Gm?0:1),i=!1,s=!1){const l={__v_isVNode:!0,__v_skip:!0,type:e,props:t,key:t&&ug(t),ref:t&&dg(t),scopeId:Eh,slotScopeIds:null,children:r,component:null,suspense:null,ssContent:null,ssFallback:null,dirs:null,transition:null,el:null,anchor:null,target:null,targetStart:null,targetAnchor:null,staticCount:0,shapeFlag:o,patchFlag:n,dynamicProps:a,dynamicChildren:null,appContext:null,ctx:_h};return s?(wg(l,r),128&o&&e.normalize(l)):r&&(l.shapeFlag|=Nu(r)?8:16),ng>0&&!i&&tg&&(l.patchFlag>0||6&o)&&32!==l.patchFlag&&tg.push(l),l}const hg=function(e,t=null,r=null,n=0,a=null,o=!1){if(e&&e!==Cf||(e=Km),lg(e)){const n=mg(e,t,!0);return r&&wg(n,r),ng>0&&!o&&tg&&(6&n.shapeFlag?tg[tg.indexOf(e)]=n:tg.push(n)),n.patchFlag=-2,n}var i;if(Mu(i=e)&&\"__vccOpts\"in i&&(e=e.__vccOpts),t){t=fg(t);let{class:e,style:r}=t;e&&!Nu(e)&&(t.class=ld(e)),ju(r)&&(Lp(r)&&!Cu(r)&&(r=Eu({},r)),t.style=nd(r))}return pg(e,t,r,n,a,Nu(e)?1:Xm(e)?128:Dh(e)?64:ju(e)?4:Mu(e)?2:0,o,!0)};function fg(e){return e?Lp(e)||hm(e)?Eu({},e):e:null}function mg(e,t,r=!1,n=!1){const{props:a,ref:o,patchFlag:i,children:s,transition:l}=e,c=t?xg(a||{},t):a,u={__v_isVNode:!0,__v_skip:!0,type:e.type,props:c,key:c&&ug(c),ref:t&&t.ref?r&&o?Cu(o)?o.concat(dg(t)):[o,dg(t)]:dg(t):o,scopeId:e.scopeId,slotScopeIds:e.slotScopeIds,children:s,target:e.target,targetStart:e.targetStart,targetAnchor:e.targetAnchor,staticCount:e.staticCount,shapeFlag:e.shapeFlag,patchFlag:t&&e.type!==Gm?-1===i?16:16|i:i,dynamicProps:e.dynamicProps,dynamicChildren:e.dynamicChildren,appContext:e.appContext,dirs:e.dirs,transition:l,component:e.component,suspense:e.suspense,ssContent:e.ssContent&&mg(e.ssContent),ssFallback:e.ssFallback&&mg(e.ssFallback),placeholder:e.placeholder,el:e.el,anchor:e.anchor,ctx:e.ctx,ce:e.ce};return l&&n&&ef(u,l.clone(u)),u}function gg(e=\" \",t=0){return hg(Ym,null,e,t)}function vg(e,t){const r=hg(Jm,null,e);return r.staticCount=t,r}function bg(e=\"\",t=!1){return t?(rg(),sg(Km,null,e)):hg(Km,null,e)}function yg(e){return null==e||\"boolean\"==typeof e?hg(Km):Cu(e)?hg(Gm,null,e.slice()):lg(e)?Og(e):hg(Ym,null,String(e))}function Og(e){return null===e.el&&-1!==e.patchFlag||e.memo?e:mg(e)}function wg(e,t){let r=0;const{shapeFlag:n}=e;if(null==t)t=null;else if(Cu(t))r=16;else if(\"object\"==typeof t){if(65&n){const r=t.default;return void(r&&(r._c&&(r._d=!1),wg(e,r()),r._c&&(r._d=!0)))}{r=32;const n=t._;n||hm(t)?3===n&&_h&&(1===_h.slots._?t._=1:(t._=2,e.patchFlag|=1024)):t._ctx=_h}}else Mu(t)?(t={default:t,_ctx:_h},r=32):(t=String(t),64&n?(r=16,t=[gg(t)]):r=8);e.children=t,e.shapeFlag|=r}function xg(...e){const t={};for(let r=0;rEg||_h;let Ag,$g;{const e=rd(),t=(t,r)=>{let n;return(n=e[t])||(n=e[t]=[]),n.push(r),e=>{n.length>1?n.forEach((t=>t(e))):n[0](e)}};Ag=t(\"__VUE_INSTANCE_SETTERS__\",(e=>Eg=e)),$g=t(\"__VUE_SSR_SETTERS__\",(e=>Ig=e))}const Cg=e=>{const t=Eg;return Ag(e),e.scope.on(),()=>{e.scope.off(),Ag(t)}},Pg=()=>{Eg&&Eg.scope.off(),Ag(null)};function Dg(e){return 4&e.vnode.shapeFlag}let Ig=!1;function Mg(e,t,r){Mu(t)?e.type.__ssrInlineRender?e.ssrRender=t:e.render=t:ju(t)&&(e.setupState=Gp(t)),Ng(e)}function Ng(e,t,r){const n=e.type;e.render||(e.render=n.render||xu);{const t=Cg(e);Bd();try{!function(e){const t=Gf(e),r=e.proxy,n=e.ctx;Hf=!1,t.beforeCreate&&Wf(t.beforeCreate,e,\"bc\");const{data:a,computed:o,methods:i,watch:s,provide:l,inject:c,created:u,beforeMount:d,mounted:p,beforeUpdate:h,updated:f,activated:m,deactivated:g,beforeDestroy:v,beforeUnmount:b,destroyed:y,unmounted:O,render:w,renderTracked:x,renderTriggered:k,errorCaptured:S,serverPrefetch:_,expose:E,inheritAttrs:T,components:A,directives:$,filters:C}=t;if(c&&function(e,t){Cu(e)&&(e=em(e));for(const r in e){const n=e[r];let a;a=ju(n)?\"default\"in n?cm(n.from||r,n.default,!0):cm(n.from||r):cm(n),Fp(a)?Object.defineProperty(t,r,{enumerable:!0,configurable:!0,get:()=>a.value,set:e=>a.value=e}):t[r]=a}}(c,n),i)for(const e in i){const t=i[e];Mu(t)&&(n[e]=t.bind(r))}if(a){const t=a.call(r,r);ju(t)&&(e.data=Cp(t))}if(Hf=!0,o)for(const e in o){const t=o[e],a=Mu(t)?t.bind(r,r):Mu(t.get)?t.get.bind(r,r):xu,i=!Mu(t)&&Mu(t.set)?t.set.bind(r):xu,s=Ug({get:a,set:i});Object.defineProperty(n,e,{enumerable:!0,configurable:!0,get:()=>s.value,set:e=>s.value=e})}if(s)for(const e in s)Xf(s[e],n,r,e);if(l){const e=Mu(l)?l.call(r):l;Reflect.ownKeys(e).forEach((t=>{lm(t,e[t])}))}function P(e,t){Cu(t)?t.forEach((t=>e(t.bind(r)))):t&&e(t.bind(r))}if(u&&Wf(u,e,\"c\"),P(bf,d),P(yf,p),P(Of,h),P(wf,f),P(pf,m),P(hf,g),P(Tf,S),P(Ef,x),P(_f,k),P(xf,b),P(kf,O),P(Sf,_),Cu(E))if(E.length){const t=e.exposed||(e.exposed={});E.forEach((e=>{Object.defineProperty(t,e,{get:()=>r[e],set:t=>r[e]=t,enumerable:!0})}))}else e.exposed||(e.exposed={});w&&e.render===xu&&(e.render=w),null!=T&&(e.inheritAttrs=T),A&&(e.components=A),$&&(e.directives=$),_&&af(e)}(e)}finally{zd(),t()}}}const Rg={get:(e,t)=>(Yd(e,0,\"\"),e[t])};function jg(e){return{attrs:new Proxy(e.attrs,Rg),slots:e.slots,emit:e.emit,expose:t=>{e.exposed=t||{}}}}function Lg(e){return e.exposed?e.exposeProxy||(e.exposeProxy=new Proxy(Gp((t=e.exposed,!$u(t,\"__v_skip\")&&Object.isExtensible(t)&&Ju(t,\"__v_skip\",!0),t)),{get:(t,r)=>r in t?t[r]:r in Uf?Uf[r](e):void 0,has:(e,t)=>t in e||t in Uf})):e.proxy;var t}const Ug=(e,t)=>{const r=function(e,t,r=!1){let n,a;return Mu(e)?n=e:(n=e.get,a=e.set),new rh(n,a,r)}(e,0,Ig);return r};function Bg(e,t,r){const n=(e,t,r)=>{ag(-1);try{return hg(e,t,r)}finally{ag(1)}},a=arguments.length;return 2===a?ju(t)&&!Cu(t)?lg(t)?n(e,null,[t]):n(e,t):n(e,null,t):(a>3?r=Array.prototype.slice.call(arguments,2):3===a&&lg(r)&&(r=[r]),n(e,t,r))}const zg=\"3.5.21\";\n/**\n * @vue/runtime-dom v3.5.21\n * (c) 2018-present Yuxi (Evan) You and Vue contributors\n * @license MIT\n **/let Fg;const Qg=\"undefined\"!=typeof window&&window.trustedTypes;if(Qg)try{Fg=Qg.createPolicy(\"vue\",{createHTML:e=>e})}catch(Gb){}const qg=Fg?e=>Fg.createHTML(e):e=>e,Zg=\"undefined\"!=typeof document?document:null,Vg=Zg&&Zg.createElement(\"template\"),Hg={insert:(e,t,r)=>{t.insertBefore(e,r||null)},remove:e=>{const t=e.parentNode;t&&t.removeChild(e)},createElement:(e,t,r,n)=>{const a=\"svg\"===t?Zg.createElementNS(\"http://www.w3.org/2000/svg\",e):\"mathml\"===t?Zg.createElementNS(\"http://www.w3.org/1998/Math/MathML\",e):r?Zg.createElement(e,{is:r}):Zg.createElement(e);return\"select\"===e&&n&&null!=n.multiple&&a.setAttribute(\"multiple\",n.multiple),a},createText:e=>Zg.createTextNode(e),createComment:e=>Zg.createComment(e),setText:(e,t)=>{e.nodeValue=t},setElementText:(e,t)=>{e.textContent=t},parentNode:e=>e.parentNode,nextSibling:e=>e.nextSibling,querySelector:e=>Zg.querySelector(e),setScopeId(e,t){e.setAttribute(t,\"\")},insertStaticContent(e,t,r,n,a,o){const i=r?r.previousSibling:t.lastChild;if(a&&(a===o||a.nextSibling))for(;t.insertBefore(a.cloneNode(!0),r),a!==o&&(a=a.nextSibling););else{Vg.innerHTML=qg(\"svg\"===n?`${e}`:\"mathml\"===n?`${e}`:e);const a=Vg.content;if(\"svg\"===n||\"mathml\"===n){const e=a.firstChild;for(;e.firstChild;)a.appendChild(e.firstChild);a.removeChild(e)}t.insertBefore(a,r)}return[i?i.nextSibling:t.firstChild,r?r.previousSibling:t.lastChild]}},Wg=\"transition\",Xg=\"animation\",Gg=Symbol(\"_vtc\"),Yg={name:String,type:String,css:{type:Boolean,default:!0},duration:[String,Number,Object],enterFromClass:String,enterActiveClass:String,enterToClass:String,appearFromClass:String,appearActiveClass:String,appearToClass:String,leaveFromClass:String,leaveActiveClass:String,leaveToClass:String},Kg=Eu({},Vh,Yg),Jg=(e=>(e.displayName=\"Transition\",e.props=Kg,e))(((e,{slots:t})=>Bg(Xh,function(e){const t={};for(const r in e)r in Yg||(t[r]=e[r]);if(!1===e.css)return t;const{name:r=\"v\",type:n,duration:a,enterFromClass:o=`${r}-enter-from`,enterActiveClass:i=`${r}-enter-active`,enterToClass:s=`${r}-enter-to`,appearFromClass:l=o,appearActiveClass:c=i,appearToClass:u=s,leaveFromClass:d=`${r}-leave-from`,leaveActiveClass:p=`${r}-leave-active`,leaveToClass:h=`${r}-leave-to`}=e,f=function(e){if(null==e)return null;if(ju(e))return[rv(e.enter),rv(e.leave)];{const t=rv(e);return[t,t]}}(a),m=f&&f[0],g=f&&f[1],{onBeforeEnter:v,onEnter:b,onEnterCancelled:y,onLeave:O,onLeaveCancelled:w,onBeforeAppear:x=v,onAppear:k=b,onAppearCancelled:S=y}=t,_=(e,t,r,n)=>{e._enterCancelled=n,av(e,t?u:s),av(e,t?c:i),r&&r()},E=(e,t)=>{e._isLeaving=!1,av(e,d),av(e,h),av(e,p),t&&t()},T=e=>(t,r)=>{const a=e?k:b,i=()=>_(t,e,r);ev(a,[t,i]),ov((()=>{av(t,e?l:o),nv(t,e?u:s),tv(a)||sv(t,n,m,i)}))};return Eu(t,{onBeforeEnter(e){ev(v,[e]),nv(e,o),nv(e,i)},onBeforeAppear(e){ev(x,[e]),nv(e,l),nv(e,c)},onEnter:T(!1),onAppear:T(!0),onLeave(e,t){e._isLeaving=!0;const r=()=>E(e,t);nv(e,d),e._enterCancelled?(nv(e,p),uv()):(uv(),nv(e,p)),ov((()=>{e._isLeaving&&(av(e,d),nv(e,h),tv(O)||sv(e,n,g,r))})),ev(O,[e,r])},onEnterCancelled(e){_(e,!1,void 0,!0),ev(y,[e])},onAppearCancelled(e){_(e,!0,void 0,!0),ev(S,[e])},onLeaveCancelled(e){E(e),ev(w,[e])}})}(e),t))),ev=(e,t=[])=>{Cu(e)?e.forEach((e=>e(...t))):e&&e(...t)},tv=e=>!!e&&(Cu(e)?e.some((e=>e.length>1)):e.length>1);function rv(e){return(e=>{const t=Nu(e)?Number(e):NaN;return isNaN(t)?e:t})(e)}function nv(e,t){t.split(/\\s+/).forEach((t=>t&&e.classList.add(t))),(e[Gg]||(e[Gg]=new Set)).add(t)}function av(e,t){t.split(/\\s+/).forEach((t=>t&&e.classList.remove(t)));const r=e[Gg];r&&(r.delete(t),r.size||(e[Gg]=void 0))}function ov(e){requestAnimationFrame((()=>{requestAnimationFrame(e)}))}let iv=0;function sv(e,t,r,n){const a=e._endId=++iv,o=()=>{a===e._endId&&n()};if(null!=r)return setTimeout(o,r);const{type:i,timeout:s,propCount:l}=function(e,t){const r=window.getComputedStyle(e),n=e=>(r[e]||\"\").split(\", \"),a=n(`${Wg}Delay`),o=n(`${Wg}Duration`),i=lv(a,o),s=n(`${Xg}Delay`),l=n(`${Xg}Duration`),c=lv(s,l);let u=null,d=0,p=0;t===Wg?i>0&&(u=Wg,d=i,p=o.length):t===Xg?c>0&&(u=Xg,d=c,p=l.length):(d=Math.max(i,c),u=d>0?i>c?Wg:Xg:null,p=u?u===Wg?o.length:l.length:0);return{type:u,timeout:d,propCount:p,hasTransform:u===Wg&&/\\b(?:transform|all)(?:,|$)/.test(n(`${Wg}Property`).toString())}}(e,t);if(!i)return n();const c=i+\"end\";let u=0;const d=()=>{e.removeEventListener(c,p),o()},p=t=>{t.target===e&&++u>=l&&d()};setTimeout((()=>{ucv(t)+cv(e[r]))))}function cv(e){return\"auto\"===e?0:1e3*Number(e.slice(0,-1).replace(\",\",\".\"))}function uv(){return document.body.offsetHeight}const dv=Symbol(\"_vod\"),pv=Symbol(\"_vsh\"),hv={name:\"show\",beforeMount(e,{value:t},{transition:r}){e[dv]=\"none\"===e.style.display?\"\":e.style.display,r&&t?r.beforeEnter(e):fv(e,t)},mounted(e,{value:t},{transition:r}){r&&t&&r.enter(e)},updated(e,{value:t,oldValue:r},{transition:n}){!t!=!r&&(n?t?(n.beforeEnter(e),fv(e,!0),n.enter(e)):n.leave(e,(()=>{fv(e,!1)})):fv(e,t))},beforeUnmount(e,{value:t}){fv(e,t)}};function fv(e,t){e.style.display=t?e[dv]:\"none\",e[pv]=!t}const mv=Symbol(\"\");function gv(e){const t=Tg();if(!t)return;const r=t.ut=(r=e(t.proxy))=>{Array.from(document.querySelectorAll(`[data-v-owner=\"${t.uid}\"]`)).forEach((e=>bv(e,r)))},n=()=>{const n=e(t.proxy);t.ce?bv(t.ce,n):vv(t.subTree,n),r(n)};Of((()=>{Oh(n)})),yf((()=>{Nm(n,xu,{flush:\"post\"});const e=new MutationObserver(n);e.observe(t.subTree.el.parentNode,{childList:!0}),kf((()=>e.disconnect()))}))}function vv(e,t){if(128&e.shapeFlag){const r=e.suspense;e=r.activeBranch,r.pendingBranch&&!r.isHydrating&&r.effects.push((()=>{vv(r.activeBranch,t)}))}for(;e.component;)e=e.component.subTree;if(1&e.shapeFlag&&e.el)bv(e.el,t);else if(e.type===Gm)e.children.forEach((e=>vv(e,t)));else if(e.type===Jm){let{el:r,anchor:n}=e;for(;r&&(bv(r,t),r!==n);)r=r.nextSibling}}function bv(e,t){if(1===e.nodeType){const r=e.style;let n=\"\";for(const e in t){const a=bd(t[e]);r.setProperty(`--${e}`,a),n+=`--${e}: ${a};`}r[mv]=n}}const yv=/(?:^|;)\\s*display\\s*:/,Ov=/\\s*!important$/;function wv(e,t,r){if(Cu(r))r.forEach((r=>wv(e,t,r)));else if(null==r&&(r=\"\"),t.startsWith(\"--\"))e.setProperty(t,r);else{const n=function(e,t){const r=kv[t];if(r)return r;let n=Vu(t);if(\"filter\"!==n&&n in e)return kv[t]=n;n=Xu(n);for(let r=0;rCv||(Pv.then((()=>Cv=0)),Cv=Date.now()),Iv=e=>111===e.charCodeAt(0)&&110===e.charCodeAt(1)&&e.charCodeAt(2)>96&&e.charCodeAt(2)<123,Mv=e=>{const t=e.props[\"onUpdate:modelValue\"]||!1;return Cu(t)?e=>Ku(t,e):t};function Nv(e){e.target.composing=!0}function Rv(e){const t=e.target;t.composing&&(t.composing=!1,t.dispatchEvent(new Event(\"input\")))}const jv=Symbol(\"_assign\"),Lv={created(e,{modifiers:{lazy:t,trim:r,number:n}},a){e[jv]=Mv(a);const o=n||a.props&&\"number\"===a.props.type;Tv(e,t?\"change\":\"input\",(t=>{if(t.target.composing)return;let n=e.value;r&&(n=n.trim()),o&&(n=ed(n)),e[jv](n)})),r&&Tv(e,\"change\",(()=>{e.value=e.value.trim()})),t||(Tv(e,\"compositionstart\",Nv),Tv(e,\"compositionend\",Rv),Tv(e,\"change\",Rv))},mounted(e,{value:t}){e.value=null==t?\"\":t},beforeUpdate(e,{value:t,oldValue:r,modifiers:{lazy:n,trim:a,number:o}},i){if(e[jv]=Mv(i),e.composing)return;const s=null==t?\"\":t;if((!o&&\"number\"!==e.type||/^0\\d/.test(e.value)?e.value:ed(e.value))!==s){if(document.activeElement===e&&\"range\"!==e.type){if(n&&t===r)return;if(a&&e.value.trim()===s)return}e.value=s}}},Uv={deep:!0,created(e,t,r){e[jv]=Mv(r),Tv(e,\"change\",(()=>{const t=e._modelValue,r=qv(e),n=e.checked,a=e[jv];if(Cu(t)){const e=hd(t,r),o=-1!==e;if(n&&!o)a(t.concat(r));else if(!n&&o){const r=[...t];r.splice(e,1),a(r)}}else if(Du(t)){const e=new Set(t);n?e.add(r):e.delete(r),a(e)}else a(Zv(e,n))}))},mounted:Bv,beforeUpdate(e,t,r){e[jv]=Mv(r),Bv(e,t,r)}};function Bv(e,{value:t,oldValue:r},n){let a;if(e._modelValue=t,Cu(t))a=hd(t,n.props.value)>-1;else if(Du(t))a=t.has(n.props.value);else{if(t===r)return;a=pd(t,Zv(e,!0))}e.checked!==a&&(e.checked=a)}const zv={created(e,{value:t},r){e.checked=pd(t,r.props.value),e[jv]=Mv(r),Tv(e,\"change\",(()=>{e[jv](qv(e))}))},beforeUpdate(e,{value:t,oldValue:r},n){e[jv]=Mv(n),t!==r&&(e.checked=pd(t,n.props.value))}},Fv={deep:!0,created(e,{value:t,modifiers:{number:r}},n){const a=Du(t);Tv(e,\"change\",(()=>{const t=Array.prototype.filter.call(e.options,(e=>e.selected)).map((e=>r?ed(qv(e)):qv(e)));e[jv](e.multiple?a?new Set(t):t:t[0]),e._assigning=!0,vh((()=>{e._assigning=!1}))})),e[jv]=Mv(n)},mounted(e,{value:t}){Qv(e,t)},beforeUpdate(e,t,r){e[jv]=Mv(r)},updated(e,{value:t}){e._assigning||Qv(e,t)}};function Qv(e,t){const r=e.multiple,n=Cu(t);if(!r||n||Du(t)){for(let a=0,o=e.options.length;aString(e)===String(i))):hd(t,i)>-1}else o.selected=t.has(i);else if(pd(qv(o),t))return void(e.selectedIndex!==a&&(e.selectedIndex=a))}r||-1===e.selectedIndex||(e.selectedIndex=-1)}}function qv(e){return\"_value\"in e?e._value:e.value}function Zv(e,t){const r=t?\"_trueValue\":\"_falseValue\";return r in e?e[r]:t}const Vv={created(e,t,r){Hv(e,t,r,null,\"created\")},mounted(e,t,r){Hv(e,t,r,null,\"mounted\")},beforeUpdate(e,t,r,n){Hv(e,t,r,n,\"beforeUpdate\")},updated(e,t,r,n){Hv(e,t,r,n,\"updated\")}};function Hv(e,t,r,n,a){const o=function(e,t){switch(e){case\"SELECT\":return Fv;case\"TEXTAREA\":return Lv;default:switch(t){case\"checkbox\":return Uv;case\"radio\":return zv;default:return Lv}}}(e.tagName,r.props&&r.props.type)[a];o&&o(e,t,r,n)}const Wv=[\"ctrl\",\"shift\",\"alt\",\"meta\"],Xv={stop:e=>e.stopPropagation(),prevent:e=>e.preventDefault(),self:e=>e.target!==e.currentTarget,ctrl:e=>!e.ctrlKey,shift:e=>!e.shiftKey,alt:e=>!e.altKey,meta:e=>!e.metaKey,left:e=>\"button\"in e&&0!==e.button,middle:e=>\"button\"in e&&1!==e.button,right:e=>\"button\"in e&&2!==e.button,exact:(e,t)=>Wv.some((r=>e[`${r}Key`]&&!t.includes(r)))},Gv=(e,t)=>{const r=e._withMods||(e._withMods={}),n=t.join(\".\");return r[n]||(r[n]=(r,...n)=>{for(let e=0;e{const r=e._withKeys||(e._withKeys={}),n=t.join(\".\");return r[n]||(r[n]=r=>{if(!(\"key\"in r))return;const n=Wu(r.key);return t.some((e=>e===n||Yv[e]===n))?e(r):void 0})},Jv=Eu({patchProp:(e,t,r,n,a,o)=>{const i=\"svg\"===a;\"class\"===t?function(e,t,r){const n=e[Gg];n&&(t=(t?[t,...n]:[...n]).join(\" \")),null==t?e.removeAttribute(\"class\"):r?e.setAttribute(\"class\",t):e.className=t}(e,n,i):\"style\"===t?function(e,t,r){const n=e.style,a=Nu(r);let o=!1;if(r&&!a){if(t)if(Nu(t))for(const e of t.split(\";\")){const t=e.slice(0,e.indexOf(\":\")).trim();null==r[t]&&wv(n,t,\"\")}else for(const e in t)null==r[e]&&wv(n,e,\"\");for(const e in r)\"display\"===e&&(o=!0),wv(n,e,r[e])}else if(a){if(t!==r){const e=n[mv];e&&(r+=\";\"+e),n.cssText=r,o=yv.test(r)}}else t&&e.removeAttribute(\"style\");dv in e&&(e[dv]=o?n.display:\"\",e[pv]&&(n.display=\"none\"))}(e,r,n):Su(t)?_u(t)||function(e,t,r,n,a=null){const o=e[Av]||(e[Av]={}),i=o[t];if(n&&i)i.value=n;else{const[r,s]=function(e){let t;if($v.test(e)){let r;for(t={};r=e.match($v);)e=e.slice(0,e.length-r[0].length),t[r[0].toLowerCase()]=!0}return[\":\"===e[2]?e.slice(3):Wu(e.slice(2)),t]}(t);if(n){const i=o[t]=function(e,t){const r=e=>{if(e._vts){if(e._vts<=r.attached)return}else e._vts=Date.now();lh(function(e,t){if(Cu(t)){const r=e.stopImmediatePropagation;return e.stopImmediatePropagation=()=>{r.call(e),e._stopped=!0},t.map((e=>t=>!t._stopped&&e&&e(t)))}return t}(e,r.value),t,5,[e])};return r.value=e,r.attached=Dv(),r}(n,a);Tv(e,r,i,s)}else i&&(function(e,t,r,n){e.removeEventListener(t,r,n)}(e,r,i,s),o[t]=void 0)}}(e,t,0,n,o):(\".\"===t[0]?(t=t.slice(1),1):\"^\"===t[0]?(t=t.slice(1),0):function(e,t,r,n){if(n)return\"innerHTML\"===t||\"textContent\"===t||!!(t in e&&Iv(t)&&Mu(r));if(\"spellcheck\"===t||\"draggable\"===t||\"translate\"===t||\"autocorrect\"===t)return!1;if(\"form\"===t)return!1;if(\"list\"===t&&\"INPUT\"===e.tagName)return!1;if(\"type\"===t&&\"TEXTAREA\"===e.tagName)return!1;if(\"width\"===t||\"height\"===t){const t=e.tagName;if(\"IMG\"===t||\"VIDEO\"===t||\"CANVAS\"===t||\"SOURCE\"===t)return!1}return(!Iv(t)||!Nu(r))&&t in e}(e,t,n,i))?(Ev(e,t,n),e.tagName.includes(\"-\")||\"value\"!==t&&\"checked\"!==t&&\"selected\"!==t||_v(e,t,n,i,0,\"value\"!==t)):!e._isVueCE||!/[A-Z]/.test(t)&&Nu(n)?(\"true-value\"===t?e._trueValue=n:\"false-value\"===t&&(e._falseValue=n),_v(e,t,n,i)):Ev(e,Vu(t),n,0,t)}},Hg);let eb;function tb(){return eb||(eb=Em(Jv))}const rb=(...e)=>{tb().render(...e)},nb=(...e)=>{const t=tb().createApp(...e),{mount:r}=t;return t.mount=e=>{const n=function(e){return Nu(e)?document.querySelector(e):e}(e);if(!n)return;const a=t._component;Mu(a)||a.render||a.template||(a.template=n.innerHTML),1===n.nodeType&&(n.textContent=\"\");const o=r(n,!1,function(e){return e instanceof SVGElement?\"svg\":\"function\"==typeof MathMLElement&&e instanceof MathMLElement?\"mathml\":void 0}(n));return n instanceof Element&&(n.removeAttribute(\"v-cloak\"),n.setAttribute(\"data-v-app\",\"\")),o},t},ab=\"3\"===zg[0];function ob(e){if(e instanceof Promise||e instanceof Date||e instanceof RegExp)return e;const t=\"function\"==typeof(r=e)?r():Hp(r);var r;if(!e||!t)return t;if(Array.isArray(t))return t.map((e=>ob(e)));if(\"object\"==typeof t){const e={};for(const r in t)Object.prototype.hasOwnProperty.call(t,r)&&(\"titleTemplate\"===r||\"o\"===r[0]&&\"n\"===r[1]?e[r]=Hp(t[r]):e[r]=ob(t[r]));return e}return t}const ib={hooks:{\"entries:resolve\":e=>{for(const t of e.entries)t.resolvedInput=ob(t.input)}}},sb=\"usehead\";function lb(e={}){e.domDelayFn=e.domDelayFn||(e=>vh((()=>setTimeout((()=>e()),0))));const t=function(e={}){const t=function(e={}){const t=new nu;t.addHooks(e.hooks||{}),e.document=e.document||(bc?document:void 0);const r=!e.document,n=()=>{s.dirty=!0,t.callHook(\"entries:updated\",s)};let a=0,o=[];const i=[],s={plugins:i,dirty:!1,resolvedOptions:e,hooks:t,headEntries:()=>o,use(e){const n=\"function\"==typeof e?e(s):e;n.key&&i.some((e=>e.key===n.key))||(i.push(n),bu(n.mode,r)&&t.addHooks(n.hooks||{}))},push(e,t){delete t?.head;const i={_i:a++,input:e,...t};return bu(i.mode,r)&&(o.push(i),n()),{dispose(){o=o.filter((e=>e._i!==i._i)),n()},patch(e){for(const t of o)t._i===i._i&&(t.input=i.input=e);n()}}},async resolveTags(){const e={tags:[],entries:[...o]};await t.callHook(\"entries:resolve\",e);for(const r of e.entries){const n=r.resolvedInput||r.input;if(r.resolvedInput=await(r.transform?r.transform(n):n),r.resolvedInput)for(const n of await Lc(r)){const a={tag:n,entry:r,resolvedOptions:s.resolvedOptions};await t.callHook(\"tag:normalise\",a),e.tags.push(a.tag)}}return await t.callHook(\"tags:beforeResolve\",e),await t.callHook(\"tags:resolve\",e),await t.callHook(\"tags:afterResolve\",e),e.tags},ssr:r};return[ou,uu,su,cu,du,fu,mu,gu,...e?.plugins||[]].forEach((e=>s.use(e))),s.hooks.callHook(\"init\",s),s}(e);return t.use(Gc()),vu=t}(e);return t.use(ib),t.install=function(e){return{install(t){ab&&(t.config.globalProperties.$unhead=e,t.config.globalProperties.$head=e,t.provide(sb,e))}}.install}(t),t}const cb=\"undefined\"!=typeof globalThis?globalThis:\"undefined\"!=typeof window?window:\"undefined\"!=typeof global?global:\"undefined\"!=typeof self?self:{},ub=\"__unhead_injection_handler__\";function db(e,t={}){const r=t.head||(ub in cb?cb[ub]():cm(sb)||vu);if(r)return r.ssr?r.push(e,t):function(e,t,r={}){const n=Qp(!1),a=Qp({});Mm((()=>{a.value=n.value?{}:ob(t)}));const o=e.push(a.value,r);return Nm(a,(e=>{o.patch(e)})),Tg()&&(xf((()=>{o.dispose()})),hf((()=>{n.value=!0})),pf((()=>{n.value=!1}))),o}(r,e,t)}function pb(e,t,r){let n=Qp(null==r?void 0:r.value),a=Ug((()=>void 0!==e.value));return[Ug((()=>a.value?e.value:n.value)),function(e){return a.value||(n.value=e),null==t?void 0:t(e)}]}function hb(e){\"function\"==typeof queueMicrotask?queueMicrotask(e):Promise.resolve().then(e).catch((e=>setTimeout((()=>{throw e}))))}function fb(){let e=[],t={addEventListener:(e,r,n,a)=>(e.addEventListener(r,n,a),t.add((()=>e.removeEventListener(r,n,a)))),requestAnimationFrame(...e){let r=requestAnimationFrame(...e);t.add((()=>cancelAnimationFrame(r)))},nextFrame(...e){t.requestAnimationFrame((()=>{t.requestAnimationFrame(...e)}))},setTimeout(...e){let r=setTimeout(...e);t.add((()=>clearTimeout(r)))},microTask(...e){let r={current:!0};return hb((()=>{r.current&&e[0]()})),t.add((()=>{r.current=!1}))},style(e,t,r){let n=e.style.getPropertyValue(t);return Object.assign(e.style,{[t]:r}),this.add((()=>{Object.assign(e.style,{[t]:n})}))},group(e){let t=fb();return e(t),this.add((()=>t.dispose()))},add:t=>(e.push(t),()=>{let r=e.indexOf(t);if(r>=0)for(let t of e.splice(r,1))t()}),dispose(){for(let t of e.splice(0))t()}};return t}var mb;let gb=Symbol(\"headlessui.useid\"),vb=0;const bb=null!=(mb=nf)?mb:function(){return cm(gb,(()=>\"\"+ ++vb))()};function yb(e){var t;if(null==e||null==e.value)return null;let r=null!=(t=e.value.$el)?t:e.value;return r instanceof Node?r:null}function Ob(e,t,...r){if(e in t){let n=t[e];return\"function\"==typeof n?n(...r):n}let n=new Error(`Tried to handle \"${e}\" but there is no handler defined. Only defined handlers are: ${Object.keys(t).map((e=>`\"${e}\"`)).join(\", \")}.`);throw Error.captureStackTrace&&Error.captureStackTrace(n,Ob),n}var wb=Object.defineProperty,xb=(e,t,r)=>(((e,t,r)=>{t in e?wb(e,t,{enumerable:!0,configurable:!0,writable:!0,value:r}):e[t]=r})(e,\"symbol\"!=typeof t?t+\"\":t,r),r);let kb=new class{constructor(){xb(this,\"current\",this.detect()),xb(this,\"currentId\",0)}set(e){this.current!==e&&(this.currentId=0,this.current=e)}reset(){this.set(this.detect())}nextId(){return++this.currentId}get isServer(){return\"server\"===this.current}get isClient(){return\"client\"===this.current}detect(){return\"undefined\"==typeof window||\"undefined\"==typeof document?\"server\":\"client\"}};function Sb(e){if(kb.isServer)return null;if(e instanceof Node)return e.ownerDocument;if(null!=e&&e.hasOwnProperty(\"value\")){let t=yb(e);if(t)return t.ownerDocument}return document}let _b=[\"[contentEditable=true]\",\"[tabindex]\",\"a[href]\",\"area[href]\",\"button:not([disabled])\",\"iframe\",\"input:not([disabled])\",\"select:not([disabled])\",\"textarea:not([disabled])\"].map((e=>`${e}:not([tabindex='-1'])`)).join(\",\");var Eb,Tb,Ab,$b=((Ab=$b||{})[Ab.First=1]=\"First\",Ab[Ab.Previous=2]=\"Previous\",Ab[Ab.Next=4]=\"Next\",Ab[Ab.Last=8]=\"Last\",Ab[Ab.WrapAround=16]=\"WrapAround\",Ab[Ab.NoScroll=32]=\"NoScroll\",Ab),Cb=((Tb=Cb||{})[Tb.Error=0]=\"Error\",Tb[Tb.Overflow=1]=\"Overflow\",Tb[Tb.Success=2]=\"Success\",Tb[Tb.Underflow=3]=\"Underflow\",Tb),Pb=((Eb=Pb||{})[Eb.Previous=-1]=\"Previous\",Eb[Eb.Next=1]=\"Next\",Eb);function Db(e=document.body){return null==e?[]:Array.from(e.querySelectorAll(_b)).sort(((e,t)=>Math.sign((e.tabIndex||Number.MAX_SAFE_INTEGER)-(t.tabIndex||Number.MAX_SAFE_INTEGER))))}var Ib=(e=>(e[e.Strict=0]=\"Strict\",e[e.Loose=1]=\"Loose\",e))(Ib||{});function Mb(e,t=0){var r;return e!==(null==(r=Sb(e))?void 0:r.body)&&Ob(t,{0:()=>e.matches(_b),1(){let t=e;for(;null!==t;){if(t.matches(_b))return!0;t=t.parentElement}return!1}})}function Nb(e){let t=Sb(e);vh((()=>{t&&!Mb(t.activeElement,0)&&jb(e)}))}var Rb=(e=>(e[e.Keyboard=0]=\"Keyboard\",e[e.Mouse=1]=\"Mouse\",e))(Rb||{});function jb(e){null==e||e.focus({preventScroll:!0})}\"undefined\"!=typeof window&&\"undefined\"!=typeof document&&(document.addEventListener(\"keydown\",(e=>{e.metaKey||e.altKey||e.ctrlKey||(document.documentElement.dataset.headlessuiFocusVisible=\"\")}),!0),document.addEventListener(\"click\",(e=>{1===e.detail?delete document.documentElement.dataset.headlessuiFocusVisible:0===e.detail&&(document.documentElement.dataset.headlessuiFocusVisible=\"\")}),!0));let Lb=[\"textarea\",\"input\"].join(\",\");function Ub(e,t=e=>e){return e.slice().sort(((e,r)=>{let n=t(e),a=t(r);if(null===n||null===a)return 0;let o=n.compareDocumentPosition(a);return o&Node.DOCUMENT_POSITION_FOLLOWING?-1:o&Node.DOCUMENT_POSITION_PRECEDING?1:0}))}function Bb(e,t,{sorted:r=!0,relativeTo:n=null,skipElements:a=[]}={}){var o;let i=null!=(o=Array.isArray(e)?e.length>0?e[0].ownerDocument:document:null==e?void 0:e.ownerDocument)?o:document,s=Array.isArray(e)?r?Ub(e):e:Db(e);a.length>0&&s.length>1&&(s=s.filter((e=>!a.includes(e)))),n=null!=n?n:i.activeElement;let l,c=(()=>{if(5&t)return 1;if(10&t)return-1;throw new Error(\"Missing Focus.First, Focus.Previous, Focus.Next or Focus.Last\")})(),u=(()=>{if(1&t)return 0;if(2&t)return Math.max(0,s.indexOf(n))-1;if(4&t)return Math.max(0,s.indexOf(n))+1;if(8&t)return s.length-1;throw new Error(\"Missing Focus.First, Focus.Previous, Focus.Next or Focus.Last\")})(),d=32&t?{preventScroll:!0}:{},p=0,h=s.length;do{if(p>=h||p+h<=0)return 0;let e=u+p;if(16&t)e=(e+h)%h;else{if(e<0)return 3;if(e>=h)return 1}l=s[e],null==l||l.focus(d),p+=c}while(l!==i.activeElement);return 6&t&&function(e){var t,r;return null!=(r=null==(t=null==e?void 0:e.matches)?void 0:t.call(e,Lb))&&r}(l)&&l.select(),2}function zb(){return/iPhone/gi.test(window.navigator.platform)||/Mac/gi.test(window.navigator.platform)&&window.navigator.maxTouchPoints>0}function Fb(e,t,r){kb.isServer||Mm((n=>{document.addEventListener(e,t,r),n((()=>document.removeEventListener(e,t,r)))}))}function Qb(e,t,r){kb.isServer||Mm((n=>{window.addEventListener(e,t,r),n((()=>window.removeEventListener(e,t,r)))}))}function qb(e,t,r=Ug((()=>!0))){function n(n,a){if(!r.value||n.defaultPrevented)return;let o=a(n);if(null===o||!o.getRootNode().contains(o))return;let i=function e(t){return\"function\"==typeof t?e(t()):Array.isArray(t)||t instanceof Set?t:[t]}(e);for(let e of i){if(null===e)continue;let t=e instanceof HTMLElement?e:yb(e);if(null!=t&&t.contains(o)||n.composed&&n.composedPath().includes(t))return}return!Mb(o,Ib.Loose)&&-1!==o.tabIndex&&n.preventDefault(),t(n,o)}let a=Qp(null);Fb(\"pointerdown\",(e=>{var t,n;r.value&&(a.value=(null==(n=null==(t=e.composedPath)?void 0:t.call(e))?void 0:n[0])||e.target)}),!0),Fb(\"mousedown\",(e=>{var t,n;r.value&&(a.value=(null==(n=null==(t=e.composedPath)?void 0:t.call(e))?void 0:n[0])||e.target)}),!0),Fb(\"click\",(e=>{zb()||/Android/gi.test(window.navigator.userAgent)||a.value&&(n(e,(()=>a.value)),a.value=null)}),!0),Fb(\"touchend\",(e=>n(e,(()=>e.target instanceof HTMLElement?e.target:null))),!0),Qb(\"blur\",(e=>n(e,(()=>window.document.activeElement instanceof HTMLIFrameElement?window.document.activeElement:null))),!0)}function Zb(e,t){if(e)return e;let r=null!=t?t:\"button\";return\"string\"==typeof r&&\"button\"===r.toLowerCase()?\"button\":void 0}function Vb(e,t){let r=Qp(Zb(e.value.type,e.value.as));return yf((()=>{r.value=Zb(e.value.type,e.value.as)})),Mm((()=>{var e;r.value||yb(t)&&yb(t)instanceof HTMLButtonElement&&(null==(e=yb(t))||!e.hasAttribute(\"type\"))&&(r.value=\"button\")})),r}function Hb(e){return[e.screenX,e.screenY]}function Wb(){let e=Qp([-1,-1]);return{wasMoved(t){let r=Hb(t);return(e.value[0]!==r[0]||e.value[1]!==r[1])&&(e.value=r,!0)},update(t){e.value=Hb(t)}}}function Xb({container:e,accept:t,walk:r,enabled:n}){Mm((()=>{let a=e.value;if(!a||void 0!==n&&!n.value)return;let o=Sb(e);if(!o)return;let i=Object.assign((e=>t(e)),{acceptNode:t}),s=o.createTreeWalker(a,NodeFilter.SHOW_ELEMENT,i,!1);for(;s.nextNode();)r(s.currentNode)}))}var Gb,Yb=(e=>(e[e.None=0]=\"None\",e[e.RenderStrategy=1]=\"RenderStrategy\",e[e.Static=2]=\"Static\",e))(Yb||{}),Kb=((Gb=Kb||{})[Gb.Unmount=0]=\"Unmount\",Gb[Gb.Hidden=1]=\"Hidden\",Gb);function Jb({visible:e=!0,features:t=0,ourProps:r,theirProps:n,...a}){var o;let i=ry(n,r),s=Object.assign(a,{props:i});return e||2&t&&i.static?ey(s):1&t?Ob(null==(o=i.unmount)||o?0:1,{0:()=>null,1:()=>ey({...a,props:{...i,hidden:!0,style:{display:\"none\"}}})}):ey(s)}function ey({props:e,attrs:t,slots:r,slot:n,name:a}){var o,i;let{as:s,...l}=ay(e,[\"unmount\",\"static\"]),c=null==(o=r.default)?void 0:o.call(r,n),u={};if(n){let e=!1,t=[];for(let[r,a]of Object.entries(n))\"boolean\"==typeof a&&(e=!0),!0===a&&t.push(r);e&&(u[\"data-headlessui-state\"]=t.join(\" \"))}if(\"template\"===s){if(c=ty(null!=c?c:[]),Object.keys(l).length>0||Object.keys(t).length>0){let[e,...r]=null!=c?c:[];if(!function(e){return null!=e&&(\"string\"==typeof e.type||\"object\"==typeof e.type||\"function\"==typeof e.type)}(e)||r.length>0)throw new Error(['Passing props on \"template\"!',\"\",`The current component <${a} /> is rendering a \"template\".`,\"However we need to passthrough the following props:\",Object.keys(l).concat(Object.keys(t)).map((e=>e.trim())).filter(((e,t,r)=>r.indexOf(e)===t)).sort(((e,t)=>e.localeCompare(t))).map((e=>` - ${e}`)).join(\"\\n\"),\"\",\"You can apply a few solutions:\",['Add an `as=\"...\"` prop, to ensure that we render an actual element instead of a \"template\".',\"Render a single element as the child so that we can forward the props onto that element.\"].map((e=>` - ${e}`)).join(\"\\n\")].join(\"\\n\"));let n=ry(null!=(i=e.props)?i:{},l,u),o=mg(e,n,!0);for(let e in n)e.startsWith(\"on\")&&(o.props||(o.props={}),o.props[e]=n[e]);return o}return Array.isArray(c)&&1===c.length?c[0]:c}return Bg(s,Object.assign({},l,u),{default:()=>c})}function ty(e){return e.flatMap((e=>e.type===Gm?ty(e.children):[e]))}function ry(...e){if(0===e.length)return{};if(1===e.length)return e[0];let t={},r={};for(let n of e)for(let e in n)e.startsWith(\"on\")&&\"function\"==typeof n[e]?(null!=r[e]||(r[e]=[]),r[e].push(n[e])):t[e]=n[e];if(t.disabled||t[\"aria-disabled\"])return Object.assign(t,Object.fromEntries(Object.keys(r).map((e=>[e,void 0]))));for(let e in r)Object.assign(t,{[e](t,...n){let a=r[e];for(let e of a){if(t instanceof Event&&t.defaultPrevented)return;e(t,...n)}}});return t}function ny(e){let t=Object.assign({},e);for(let e in t)void 0===t[e]&&delete t[e];return t}function ay(e,t=[]){let r=Object.assign({},e);for(let e of t)e in r&&delete r[e];return r}var oy=(e=>(e[e.None=1]=\"None\",e[e.Focusable=2]=\"Focusable\",e[e.Hidden=4]=\"Hidden\",e))(oy||{});let iy=rf({name:\"Hidden\",props:{as:{type:[Object,String],default:\"div\"},features:{type:Number,default:1}},setup:(e,{slots:t,attrs:r})=>()=>{var n;let{features:a,...o}=e;return Jb({ourProps:{\"aria-hidden\":!(2&~a)||(null!=(n=o[\"aria-hidden\"])?n:void 0),hidden:!(4&~a)||void 0,style:{position:\"fixed\",top:1,left:1,width:1,height:0,padding:0,margin:-1,overflow:\"hidden\",clip:\"rect(0, 0, 0, 0)\",whiteSpace:\"nowrap\",borderWidth:\"0\",...!(4&~a)&&!!(2&~a)&&{display:\"none\"}}},theirProps:o,slot:{},attrs:r,slots:t,name:\"Hidden\"})}}),sy=Symbol(\"Context\");var ly=(e=>(e[e.Open=1]=\"Open\",e[e.Closed=2]=\"Closed\",e[e.Closing=4]=\"Closing\",e[e.Opening=8]=\"Opening\",e))(ly||{});function cy(){return cm(sy,null)}function uy(e){lm(sy,e)}var dy,py=((dy=py||{}).Space=\" \",dy.Enter=\"Enter\",dy.Escape=\"Escape\",dy.Backspace=\"Backspace\",dy.Delete=\"Delete\",dy.ArrowLeft=\"ArrowLeft\",dy.ArrowUp=\"ArrowUp\",dy.ArrowRight=\"ArrowRight\",dy.ArrowDown=\"ArrowDown\",dy.Home=\"Home\",dy.End=\"End\",dy.PageUp=\"PageUp\",dy.PageDown=\"PageDown\",dy.Tab=\"Tab\",dy);let hy=[];!function(){function e(){\"loading\"!==document.readyState&&((()=>{function e(e){e.target instanceof HTMLElement&&e.target!==document.body&&hy[0]!==e.target&&(hy.unshift(e.target),hy=hy.filter((e=>null!=e&&e.isConnected)),hy.splice(10))}window.addEventListener(\"click\",e,{capture:!0}),window.addEventListener(\"mousedown\",e,{capture:!0}),window.addEventListener(\"focus\",e,{capture:!0}),document.body.addEventListener(\"click\",e,{capture:!0}),document.body.addEventListener(\"mousedown\",e,{capture:!0}),document.body.addEventListener(\"focus\",e,{capture:!0})})(),document.removeEventListener(\"DOMContentLoaded\",e))}\"undefined\"!=typeof window&&\"undefined\"!=typeof document&&(document.addEventListener(\"DOMContentLoaded\",e),e())}();var fy,my=((fy=my||{})[fy.First=0]=\"First\",fy[fy.Previous=1]=\"Previous\",fy[fy.Next=2]=\"Next\",fy[fy.Last=3]=\"Last\",fy[fy.Specific=4]=\"Specific\",fy[fy.Nothing=5]=\"Nothing\",fy);function gy(e,t){let r=t.resolveItems();if(r.length<=0)return null;let n=t.resolveActiveIndex(),a=null!=n?n:-1;switch(e.focus){case 0:for(let e=0;e=0;--e)if(!t.resolveDisabled(r[e],e,r))return e;return n;case 2:for(let e=a+1;e=0;--e)if(!t.resolveDisabled(r[e],e,r))return e;return n;case 4:for(let n=0;n{(e=null!=e?e:window).addEventListener(t,r,n),a((()=>e.removeEventListener(t,r,n)))}))}var wy=(e=>(e[e.Forwards=0]=\"Forwards\",e[e.Backwards=1]=\"Backwards\",e))(wy||{});function xy(){let e=Qp(0);return Qb(\"keydown\",(t=>{\"Tab\"===t.key&&(e.value=t.shiftKey?1:0)})),e}function ky(e){if(!e)return new Set;if(\"function\"==typeof e)return new Set(e());let t=new Set;for(let r of e.value){let e=yb(r);e instanceof HTMLElement&&t.add(e)}return t}var Sy=(e=>(e[e.None=1]=\"None\",e[e.InitialFocus=2]=\"InitialFocus\",e[e.TabLock=4]=\"TabLock\",e[e.FocusLock=8]=\"FocusLock\",e[e.RestoreFocus=16]=\"RestoreFocus\",e[e.All=30]=\"All\",e))(Sy||{});let _y=Object.assign(rf({name:\"FocusTrap\",props:{as:{type:[Object,String],default:\"div\"},initialFocus:{type:Object,default:null},features:{type:Number,default:30},containers:{type:[Object,Function],default:Qp(new Set)}},inheritAttrs:!1,setup(e,{attrs:t,slots:r,expose:n}){let a=Qp(null);n({el:a,$el:a});let o=Ug((()=>Sb(a))),i=Qp(!1);yf((()=>i.value=!0)),kf((()=>i.value=!1)),function({ownerDocument:e},t){let r=function(e){let t=Qp(hy.slice());return Nm([e],(([e],[r])=>{!0===r&&!1===e?hb((()=>{t.value.splice(0)})):!1===r&&!0===e&&(t.value=hy.slice())}),{flush:\"post\"}),()=>{var e;return null!=(e=t.value.find((e=>null!=e&&e.isConnected)))?e:null}}(t);yf((()=>{Mm((()=>{var n,a;t.value||(null==(n=e.value)?void 0:n.activeElement)===(null==(a=e.value)?void 0:a.body)&&jb(r())}),{flush:\"post\"})})),kf((()=>{t.value&&jb(r())}))}({ownerDocument:o},Ug((()=>i.value&&Boolean(16&e.features))));let s=function({ownerDocument:e,container:t,initialFocus:r},n){let a=Qp(null),o=Qp(!1);return yf((()=>o.value=!0)),kf((()=>o.value=!1)),yf((()=>{Nm([t,r,n],((i,s)=>{if(i.every(((e,t)=>(null==s?void 0:s[t])===e))||!n.value)return;let l=yb(t);l&&hb((()=>{var t,n;if(!o.value)return;let i=yb(r),s=null==(t=e.value)?void 0:t.activeElement;if(i){if(i===s)return void(a.value=s)}else if(l.contains(s))return void(a.value=s);i?jb(i):Bb(l,$b.First|$b.NoScroll)===Cb.Error&&console.warn(\"There are no focusable elements inside the \"),a.value=null==(n=e.value)?void 0:n.activeElement}))}),{immediate:!0,flush:\"post\"})})),a}({ownerDocument:o,container:a,initialFocus:Ug((()=>e.initialFocus))},Ug((()=>i.value&&Boolean(2&e.features))));!function({ownerDocument:e,container:t,containers:r,previousActiveElement:n},a){var o;Oy(null==(o=e.value)?void 0:o.defaultView,\"focus\",(e=>{if(!a.value)return;let o=ky(r);yb(t)instanceof HTMLElement&&o.add(yb(t));let i=n.value;if(!i)return;let s=e.target;s&&s instanceof HTMLElement?Ey(o,s)?(n.value=s,jb(s)):(e.preventDefault(),e.stopPropagation(),jb(i)):jb(n.value)}),!0)}({ownerDocument:o,container:a,containers:e.containers,previousActiveElement:s},Ug((()=>i.value&&Boolean(8&e.features))));let l=xy();function c(e){let t=yb(a);t&&Ob(l.value,{[wy.Forwards]:()=>{Bb(t,$b.First,{skipElements:[e.relatedTarget]})},[wy.Backwards]:()=>{Bb(t,$b.Last,{skipElements:[e.relatedTarget]})}})}let u=Qp(!1);function d(e){\"Tab\"===e.key&&(u.value=!0,requestAnimationFrame((()=>{u.value=!1})))}function p(t){if(!i.value)return;let r=ky(e.containers);yb(a)instanceof HTMLElement&&r.add(yb(a));let n=t.relatedTarget;n instanceof HTMLElement&&\"true\"!==n.dataset.headlessuiFocusGuard&&(Ey(r,n)||(u.value?Bb(yb(a),Ob(l.value,{[wy.Forwards]:()=>$b.Next,[wy.Backwards]:()=>$b.Previous})|$b.WrapAround,{relativeTo:t.target}):t.target instanceof HTMLElement&&jb(t.target)))}return()=>{let n={ref:a,onKeydown:d,onFocusout:p},{features:o,initialFocus:i,containers:s,...l}=e;return Bg(Gm,[Boolean(4&o)&&Bg(iy,{as:\"button\",type:\"button\",\"data-headlessui-focus-guard\":!0,onFocus:c,features:oy.Focusable}),Jb({ourProps:n,theirProps:{...t,...l},slot:{},attrs:t,slots:r,name:\"FocusTrap\"}),Boolean(4&o)&&Bg(iy,{as:\"button\",type:\"button\",\"data-headlessui-focus-guard\":!0,onFocus:c,features:oy.Focusable})])}}}),{features:Sy});function Ey(e,t){for(let r of e)if(r.contains(t))return!0;return!1}function Ty(){let e;return{before({doc:t}){var r;let n=t.documentElement;e=(null!=(r=t.defaultView)?r:window).innerWidth-n.clientWidth},after({doc:t,d:r}){let n=t.documentElement,a=n.clientWidth-n.offsetWidth,o=e-a;r.style(n,\"paddingRight\",`${o}px`)}}}function Ay(e){let t={};for(let r of e)Object.assign(t,r(t));return t}let $y=function(e,t){let r=new Map,n=new Set;return{getSnapshot:()=>r,subscribe:e=>(n.add(e),()=>n.delete(e)),dispatch(e,...a){let o=t[e].call(r,...a);o&&(r=o,n.forEach((e=>e())))}}}(0,{PUSH(e,t){var r;let n=null!=(r=this.get(e))?r:{doc:e,count:0,d:fb(),meta:new Set};return n.count++,n.meta.add(t),this.set(e,n),this},POP(e,t){let r=this.get(e);return r&&(r.count--,r.meta.delete(t)),this},SCROLL_PREVENT({doc:e,d:t,meta:r}){let n={doc:e,d:t,meta:Ay(r)},a=[zb()?{before({doc:e,d:t,meta:r}){function n(e){return r.containers.flatMap((e=>e())).some((t=>t.contains(e)))}t.microTask((()=>{var r;if(\"auto\"!==window.getComputedStyle(e.documentElement).scrollBehavior){let r=fb();r.style(e.documentElement,\"scrollBehavior\",\"auto\"),t.add((()=>t.microTask((()=>r.dispose()))))}let a=null!=(r=window.scrollY)?r:window.pageYOffset,o=null;t.addEventListener(e,\"click\",(t=>{if(t.target instanceof HTMLElement)try{let r=t.target.closest(\"a\");if(!r)return;let{hash:a}=new URL(r.href),i=e.querySelector(a);i&&!n(i)&&(o=i)}catch{}}),!0),t.addEventListener(e,\"touchstart\",(e=>{if(e.target instanceof HTMLElement)if(n(e.target)){let r=e.target;for(;r.parentElement&&n(r.parentElement);)r=r.parentElement;t.style(r,\"overscrollBehavior\",\"contain\")}else t.style(e.target,\"touchAction\",\"none\")})),t.addEventListener(e,\"touchmove\",(e=>{if(e.target instanceof HTMLElement){if(\"INPUT\"===e.target.tagName)return;if(n(e.target)){let t=e.target;for(;t.parentElement&&\"\"!==t.dataset.headlessuiPortal&&!(t.scrollHeight>t.clientHeight||t.scrollWidth>t.clientWidth);)t=t.parentElement;\"\"===t.dataset.headlessuiPortal&&e.preventDefault()}else e.preventDefault()}}),{passive:!1}),t.add((()=>{var e;let t=null!=(e=window.scrollY)?e:window.pageYOffset;a!==t&&window.scrollTo(0,a),o&&o.isConnected&&(o.scrollIntoView({block:\"nearest\"}),o=null)}))}))}}:{},Ty(),{before({doc:e,d:t}){t.style(e.documentElement,\"overflow\",\"hidden\")}}];a.forEach((({before:e})=>null==e?void 0:e(n))),a.forEach((({after:e})=>null==e?void 0:e(n)))},SCROLL_ALLOW({d:e}){e.dispose()},TEARDOWN({doc:e}){this.delete(e)}});$y.subscribe((()=>{let e=$y.getSnapshot(),t=new Map;for(let[r]of e)t.set(r,r.documentElement.style.overflow);for(let r of e.values()){let e=\"hidden\"===t.get(r.doc),n=0!==r.count;(n&&!e||!n&&e)&&$y.dispatch(r.count>0?\"SCROLL_PREVENT\":\"SCROLL_ALLOW\",r),0===r.count&&$y.dispatch(\"TEARDOWN\",r)}}));let Cy=new Map,Py=new Map;function Dy(e,t=Qp(!0)){Mm((r=>{var n;if(!t.value)return;let a=yb(e);if(!a)return;r((function(){var e;if(!a)return;let t=null!=(e=Py.get(a))?e:1;if(1===t?Py.delete(a):Py.set(a,t-1),1!==t)return;let r=Cy.get(a);r&&(null===r[\"aria-hidden\"]?a.removeAttribute(\"aria-hidden\"):a.setAttribute(\"aria-hidden\",r[\"aria-hidden\"]),a.inert=r.inert,Cy.delete(a))}));let o=null!=(n=Py.get(a))?n:0;Py.set(a,o+1),0===o&&(Cy.set(a,{\"aria-hidden\":a.getAttribute(\"aria-hidden\"),inert:a.inert}),a.setAttribute(\"aria-hidden\",\"true\"),a.inert=!0)}))}function Iy({defaultContainers:e=[],portals:t,mainTreeNodeRef:r}={}){let n=Qp(null),a=Sb(n);function o(){var r,o,i;let s=[];for(let t of e)null!==t&&(t instanceof HTMLElement?s.push(t):\"value\"in t&&t.value instanceof HTMLElement&&s.push(t.value));if(null!=t&&t.value)for(let e of t.value)s.push(e);for(let e of null!=(r=null==a?void 0:a.querySelectorAll(\"html > *, body > *\"))?r:[])e!==document.body&&e!==document.head&&e instanceof HTMLElement&&\"headlessui-portal-root\"!==e.id&&(e.contains(yb(n))||e.contains(null==(i=null==(o=yb(n))?void 0:o.getRootNode())?void 0:i.host)||s.some((t=>e.contains(t)))||s.push(e));return s}return{resolveContainers:o,contains:e=>o().some((t=>t.contains(e))),mainTreeNodeRef:n,MainTreeNode:()=>null!=r?null:Bg(iy,{features:oy.Hidden,ref:n})}}let My=Symbol(\"ForcePortalRootContext\"),Ny=rf({name:\"ForcePortalRoot\",props:{as:{type:[Object,String],default:\"template\"},force:{type:Boolean,default:!1}},setup:(e,{slots:t,attrs:r})=>(lm(My,e.force),()=>{let{force:n,...a}=e;return Jb({theirProps:a,ourProps:{},slot:{},slots:t,attrs:r,name:\"ForcePortalRoot\"})})}),Ry=Symbol(\"StackContext\");var jy=(e=>(e[e.Add=0]=\"Add\",e[e.Remove=1]=\"Remove\",e))(jy||{});let Ly=Symbol(\"DescriptionContext\");function Uy({slot:e=Qp({}),name:t=\"Description\",props:r={}}={}){let n=Qp([]);return lm(Ly,{register:function(e){return n.value.push(e),()=>{let t=n.value.indexOf(e);-1!==t&&n.value.splice(t,1)}},slot:e,name:t,props:r}),Ug((()=>n.value.length>0?n.value.join(\" \"):void 0))}const By=new WeakMap;function zy(e,t){let r=t(function(e){var t;return null!=(t=By.get(e))?t:0}(e));return r<=0?By.delete(e):By.set(e,r),r}let Fy=rf({name:\"Portal\",props:{as:{type:[Object,String],default:\"div\"}},setup(e,{slots:t,attrs:r}){let n=Qp(null),a=Ug((()=>Sb(n))),o=cm(My,!1),i=cm(Zy,null),s=Qp(!0===o||null==i?function(e){let t=Sb(e);if(!t){if(null===e)return null;throw new Error(`[Headless UI]: Cannot find ownerDocument for contextElement: ${e}`)}let r=t.getElementById(\"headlessui-portal-root\");if(r)return r;let n=t.createElement(\"div\");return n.setAttribute(\"id\",\"headlessui-portal-root\"),t.body.appendChild(n)}(n.value):i.resolveTarget());s.value&&zy(s.value,(e=>e+1));let l=Qp(!1);yf((()=>{l.value=!0})),Mm((()=>{o||null!=i&&(s.value=i.resolveTarget())}));let c=cm(Qy,null),u=!1,d=Tg();return Nm(n,(()=>{if(u||!c)return;let e=yb(n);e&&(kf(c.register(e),d),u=!0)})),kf((()=>{var e,t;let r=null==(e=a.value)?void 0:e.getElementById(\"headlessui-portal-root\");!r||s.value!==r||zy(s.value,(e=>e-1))||s.value.children.length>0||null==(t=s.value.parentElement)||t.removeChild(s.value)})),()=>{if(!l.value||null===s.value)return null;let a={ref:n,\"data-headlessui-portal\":\"\"};return Bg(Bh,{to:s.value},Jb({ourProps:a,theirProps:e,slot:{},attrs:r,slots:t,name:\"Portal\"}))}}}),Qy=Symbol(\"PortalParentContext\");function qy(){let e=cm(Qy,null),t=Qp([]);function r(r){let n=t.value.indexOf(r);-1!==n&&t.value.splice(n,1),e&&e.unregister(r)}let n={register:function(n){return t.value.push(n),e&&e.register(n),()=>r(n)},unregister:r,portals:t};return[t,rf({name:\"PortalWrapper\",setup:(e,{slots:t})=>(lm(Qy,n),()=>{var e;return null==(e=t.default)?void 0:e.call(t)})})]}let Zy=Symbol(\"PortalGroupContext\"),Vy=rf({name:\"PortalGroup\",props:{as:{type:[Object,String],default:\"template\"},target:{type:Object,default:null}},setup(e,{attrs:t,slots:r}){let n=Cp({resolveTarget:()=>e.target});return lm(Zy,n),()=>{let{target:n,...a}=e;return Jb({theirProps:a,ourProps:{},slot:{},attrs:t,slots:r,name:\"PortalGroup\"})}}});var Hy,Wy=((Hy=Wy||{})[Hy.Open=0]=\"Open\",Hy[Hy.Closed=1]=\"Closed\",Hy);let Xy=Symbol(\"DialogContext\");function Gy(e){let t=cm(Xy,null);if(null===t){let t=new Error(`<${e} /> is missing a parent

component.`);throw Error.captureStackTrace&&Error.captureStackTrace(t,Gy),t}return t}let Yy=\"DC8F892D-2EBD-447C-A4C8-A03058436FF4\",Ky=rf({name:\"Dialog\",inheritAttrs:!1,props:{as:{type:[Object,String],default:\"div\"},static:{type:Boolean,default:!1},unmount:{type:Boolean,default:!0},open:{type:[Boolean,String],default:Yy},initialFocus:{type:Object,default:null},id:{type:String,default:null},role:{type:String,default:\"dialog\"}},emits:{close:e=>!0},setup(e,{emit:t,attrs:r,slots:n,expose:a}){var o,i;let s=null!=(o=e.id)?o:`headlessui-dialog-${bb()}`,l=Qp(!1);yf((()=>{l.value=!0}));let c=!1,u=Ug((()=>\"dialog\"===e.role||\"alertdialog\"===e.role?e.role:(c||(c=!0,console.warn(`Invalid role [${u}] passed to . Only \\`dialog\\` and and \\`alertdialog\\` are supported. Using \\`dialog\\` instead.`)),\"dialog\"))),d=Qp(0),p=cy(),h=Ug((()=>e.open===Yy&&null!==p?(p.value&ly.Open)===ly.Open:e.open)),f=Qp(null),m=Ug((()=>Sb(f)));if(a({el:f,$el:f}),e.open===Yy&&null===p)throw new Error(\"You forgot to provide an `open` prop to the `Dialog`.\");if(\"boolean\"!=typeof h.value)throw new Error(`You provided an \\`open\\` prop to the \\`Dialog\\`, but the value is not a boolean. Received: ${h.value===Yy?void 0:e.open}`);let g=Ug((()=>l.value&&h.value?0:1)),v=Ug((()=>0===g.value)),b=Ug((()=>d.value>1)),y=null!==cm(Xy,null),[O,w]=qy(),{resolveContainers:x,mainTreeNodeRef:k,MainTreeNode:S}=Iy({portals:O,defaultContainers:[Ug((()=>{var e;return null!=(e=I.panelRef.value)?e:f.value}))]}),_=Ug((()=>b.value?\"parent\":\"leaf\")),E=Ug((()=>null!==p&&(p.value&ly.Closing)===ly.Closing)),T=Ug((()=>!y&&!E.value&&v.value)),A=Ug((()=>{var e,t,r;return null!=(r=Array.from(null!=(t=null==(e=m.value)?void 0:e.querySelectorAll(\"body > *\"))?t:[]).find((e=>\"headlessui-portal-root\"!==e.id&&e.contains(yb(k))&&e instanceof HTMLElement)))?r:null}));Dy(A,T);let $=Ug((()=>!!b.value||v.value)),C=Ug((()=>{var e,t,r;return null!=(r=Array.from(null!=(t=null==(e=m.value)?void 0:e.querySelectorAll(\"[data-headlessui-portal]\"))?t:[]).find((e=>e.contains(yb(k))&&e instanceof HTMLElement)))?r:null}));Dy(C,$),function({type:e,enabled:t,element:r,onUpdate:n}){let a=cm(Ry,(()=>{}));function o(...e){null==n||n(...e),a(...e)}yf((()=>{Nm(t,((t,n)=>{t?o(0,e,r):!0===n&&o(1,e,r)}),{immediate:!0,flush:\"sync\"})})),kf((()=>{t.value&&o(1,e,r)})),lm(Ry,o)}({type:\"Dialog\",enabled:Ug((()=>0===g.value)),element:f,onUpdate:(e,t)=>{if(\"Dialog\"===t)return Ob(e,{[jy.Add]:()=>d.value+=1,[jy.Remove]:()=>d.value-=1})}});let P=Uy({name:\"DialogDescription\",slot:Ug((()=>({open:h.value})))}),D=Qp(null),I={titleId:D,panelRef:Qp(null),dialogState:g,setTitleId(e){D.value!==e&&(D.value=e)},close(){t(\"close\",!1)}};lm(Xy,I);let M=Ug((()=>!(!v.value||b.value)));qb(x,((e,t)=>{e.preventDefault(),I.close(),vh((()=>null==t?void 0:t.focus()))}),M);let N=Ug((()=>!(b.value||0!==g.value)));Oy(null==(i=m.value)?void 0:i.defaultView,\"keydown\",(e=>{N.value&&(e.defaultPrevented||e.key===py.Escape&&(e.preventDefault(),e.stopPropagation(),I.close()))}));let R=Ug((()=>!(E.value||0!==g.value||y)));return function(e,t,r){let n=function(e){let t=qp(e.getSnapshot());return kf(e.subscribe((()=>{t.value=e.getSnapshot()}))),t}($y),a=Ug((()=>{let t=e.value?n.value.get(e.value):void 0;return!!t&&t.count>0}));Nm([e,t],(([e,t],[n],a)=>{if(!e||!t)return;$y.dispatch(\"PUSH\",e,r);let o=!1;a((()=>{o||($y.dispatch(\"POP\",null!=n?n:e,r),o=!0)}))}),{immediate:!0})}(m,R,(e=>{var t;return{containers:[...null!=(t=e.containers)?t:[],x]}})),Mm((e=>{if(0!==g.value)return;let t=yb(f);if(!t)return;let r=new ResizeObserver((e=>{for(let t of e){let e=t.target.getBoundingClientRect();0===e.x&&0===e.y&&0===e.width&&0===e.height&&I.close()}}));r.observe(t),e((()=>r.disconnect()))})),()=>{let{open:t,initialFocus:a,...o}=e,i={...r,ref:f,id:s,role:u.value,\"aria-modal\":0===g.value||void 0,\"aria-labelledby\":D.value,\"aria-describedby\":P.value},l={open:0===g.value};return Bg(Ny,{force:!0},(()=>[Bg(Fy,(()=>Bg(Vy,{target:f.value},(()=>Bg(Ny,{force:!1},(()=>Bg(_y,{initialFocus:a,containers:x,features:v.value?Ob(_.value,{parent:_y.features.RestoreFocus,leaf:_y.features.All&~_y.features.FocusLock}):_y.features.None},(()=>Bg(w,{},(()=>Jb({ourProps:i,theirProps:{...o,...r},slot:l,attrs:r,slots:n,visible:0===g.value,features:Yb.RenderStrategy|Yb.Static,name:\"Dialog\"}))))))))))),Bg(S)]))}}}),Jy=rf({name:\"DialogPanel\",props:{as:{type:[Object,String],default:\"div\"},id:{type:String,default:null}},setup(e,{attrs:t,slots:r,expose:n}){var a;let o=null!=(a=e.id)?a:`headlessui-dialog-panel-${bb()}`,i=Gy(\"DialogPanel\");function s(e){e.stopPropagation()}return n({el:i.panelRef,$el:i.panelRef}),()=>{let{...n}=e;return Jb({ourProps:{id:o,ref:i.panelRef,onClick:s},theirProps:n,slot:{open:0===i.dialogState.value},attrs:t,slots:r,name:\"DialogPanel\"})}}}),eO=rf({name:\"DialogTitle\",props:{as:{type:[Object,String],default:\"h2\"},id:{type:String,default:null}},setup(e,{attrs:t,slots:r}){var n;let a=null!=(n=e.id)?n:`headlessui-dialog-title-${bb()}`,o=Gy(\"DialogTitle\");return yf((()=>{o.setTitleId(a),kf((()=>o.setTitleId(null)))})),()=>{let{...n}=e;return Jb({ourProps:{id:a},theirProps:n,slot:{open:0===o.dialogState.value},attrs:t,slots:r,name:\"DialogTitle\"})}}});var tO=(e=>(e[e.Open=0]=\"Open\",e[e.Closed=1]=\"Closed\",e))(tO||{});let rO=Symbol(\"DisclosureContext\");function nO(e){let t=cm(rO,null);if(null===t){let t=new Error(`<${e} /> is missing a parent component.`);throw Error.captureStackTrace&&Error.captureStackTrace(t,nO),t}return t}let aO=Symbol(\"DisclosurePanelContext\"),oO=rf({name:\"Disclosure\",props:{as:{type:[Object,String],default:\"template\"},defaultOpen:{type:[Boolean],default:!1}},setup(e,{slots:t,attrs:r}){let n=Qp(e.defaultOpen?0:1),a=Qp(null),o=Qp(null),i={buttonId:Qp(`headlessui-disclosure-button-${bb()}`),panelId:Qp(`headlessui-disclosure-panel-${bb()}`),disclosureState:n,panel:a,button:o,toggleDisclosure(){n.value=Ob(n.value,{0:1,1:0})},closeDisclosure(){1!==n.value&&(n.value=1)},close(e){i.closeDisclosure();let t=e?e instanceof HTMLElement?e:e.value instanceof HTMLElement?yb(e):yb(i.button):yb(i.button);null==t||t.focus()}};return lm(rO,i),uy(Ug((()=>Ob(n.value,{0:ly.Open,1:ly.Closed})))),()=>{let{defaultOpen:a,...o}=e;return Jb({theirProps:o,ourProps:{},slot:{open:0===n.value,close:i.close},slots:t,attrs:r,name:\"Disclosure\"})}}}),iO=rf({name:\"DisclosureButton\",props:{as:{type:[Object,String],default:\"button\"},disabled:{type:[Boolean],default:!1},id:{type:String,default:null}},setup(e,{attrs:t,slots:r,expose:n}){let a=nO(\"DisclosureButton\"),o=cm(aO,null),i=Ug((()=>null!==o&&o.value===a.panelId.value));yf((()=>{i.value||null!==e.id&&(a.buttonId.value=e.id)})),kf((()=>{i.value||(a.buttonId.value=null)}));let s=Qp(null);n({el:s,$el:s}),i.value||Mm((()=>{a.button.value=s.value}));let l=Vb(Ug((()=>({as:e.as,type:t.type}))),s);function c(){var t;e.disabled||(i.value?(a.toggleDisclosure(),null==(t=yb(a.button))||t.focus()):a.toggleDisclosure())}function u(t){var r;if(!e.disabled)if(i.value)switch(t.key){case py.Space:case py.Enter:t.preventDefault(),t.stopPropagation(),a.toggleDisclosure(),null==(r=yb(a.button))||r.focus()}else switch(t.key){case py.Space:case py.Enter:t.preventDefault(),t.stopPropagation(),a.toggleDisclosure()}}function d(e){e.key===py.Space&&e.preventDefault()}return()=>{var n;let o={open:0===a.disclosureState.value},{id:p,...h}=e;return Jb({ourProps:i.value?{ref:s,type:l.value,onClick:c,onKeydown:u}:{id:null!=(n=a.buttonId.value)?n:p,ref:s,type:l.value,\"aria-expanded\":0===a.disclosureState.value,\"aria-controls\":0===a.disclosureState.value||yb(a.panel)?a.panelId.value:void 0,disabled:!!e.disabled||void 0,onClick:c,onKeydown:u,onKeyup:d},theirProps:h,slot:o,attrs:t,slots:r,name:\"DisclosureButton\"})}}}),sO=rf({name:\"DisclosurePanel\",props:{as:{type:[Object,String],default:\"div\"},static:{type:Boolean,default:!1},unmount:{type:Boolean,default:!0},id:{type:String,default:null}},setup(e,{attrs:t,slots:r,expose:n}){let a=nO(\"DisclosurePanel\");yf((()=>{null!==e.id&&(a.panelId.value=e.id)})),kf((()=>{a.panelId.value=null})),n({el:a.panel,$el:a.panel}),lm(aO,a.panelId);let o=cy(),i=Ug((()=>null!==o?(o.value&ly.Open)===ly.Open:0===a.disclosureState.value));return()=>{var n;let o={open:0===a.disclosureState.value,close:a.close},{id:s,...l}=e;return Jb({ourProps:{id:null!=(n=a.panelId.value)?n:s,ref:a.panel},theirProps:l,slot:o,attrs:t,slots:r,features:Yb.RenderStrategy|Yb.Static,visible:i.value,name:\"DisclosurePanel\"})}}}),lO=/([\\u2700-\\u27BF]|[\\uE000-\\uF8FF]|\\uD83C[\\uDC00-\\uDFFF]|\\uD83D[\\uDC00-\\uDFFF]|[\\u2011-\\u26FF]|\\uD83E[\\uDD10-\\uDDFF])/g;function cO(e){var t,r;let n=null!=(t=e.innerText)?t:\"\",a=e.cloneNode(!0);if(!(a instanceof HTMLElement))return n;let o=!1;for(let e of a.querySelectorAll('[hidden],[aria-hidden],[role=\"img\"]'))e.remove(),o=!0;let i=o?null!=(r=a.innerText)?r:\"\":n;return lO.test(i)&&(i=i.replace(lO,\"\")),i}function uO(e){let t=Qp(\"\"),r=Qp(\"\");return()=>{let n=yb(e);if(!n)return\"\";let a=n.innerText;if(t.value===a)return r.value;let o=function(e){let t=e.getAttribute(\"aria-label\");if(\"string\"==typeof t)return t.trim();let r=e.getAttribute(\"aria-labelledby\");if(r){let e=r.split(\" \").map((e=>{let t=document.getElementById(e);if(t){let e=t.getAttribute(\"aria-label\");return\"string\"==typeof e?e.trim():cO(t).trim()}return null})).filter(Boolean);if(e.length>0)return e.join(\", \")}return cO(e).trim()}(n).trim().toLowerCase();return t.value=a,r.value=o,o}}function dO(e,t){return e===t}var pO=(e=>(e[e.Open=0]=\"Open\",e[e.Closed=1]=\"Closed\",e))(pO||{}),hO=(e=>(e[e.Single=0]=\"Single\",e[e.Multi=1]=\"Multi\",e))(hO||{}),fO=(e=>(e[e.Pointer=0]=\"Pointer\",e[e.Other=1]=\"Other\",e))(fO||{});let mO=Symbol(\"ListboxContext\");function gO(e){let t=cm(mO,null);if(null===t){let t=new Error(`<${e} /> is missing a parent component.`);throw Error.captureStackTrace&&Error.captureStackTrace(t,gO),t}return t}let vO=rf({name:\"Listbox\",emits:{\"update:modelValue\":e=>!0},props:{as:{type:[Object,String],default:\"template\"},disabled:{type:[Boolean],default:!1},by:{type:[String,Function],default:()=>dO},horizontal:{type:[Boolean],default:!1},modelValue:{type:[Object,String,Number,Boolean],default:void 0},defaultValue:{type:[Object,String,Number,Boolean],default:void 0},form:{type:String,optional:!0},name:{type:String,optional:!0},multiple:{type:[Boolean],default:!1}},inheritAttrs:!1,setup(e,{slots:t,attrs:r,emit:n}){let a=Qp(1),o=Qp(null),i=Qp(null),s=Qp(null),l=Qp([]),c=Qp(\"\"),u=Qp(null),d=Qp(1);function p(e=e=>e){let t=null!==u.value?l.value[u.value]:null,r=Ub(e(l.value.slice()),(e=>yb(e.dataRef.domRef))),n=t?r.indexOf(t):null;return-1===n&&(n=null),{options:r,activeOptionIndex:n}}let h=Ug((()=>e.multiple?1:0)),[f,m]=pb(Ug((()=>e.modelValue)),(e=>n(\"update:modelValue\",e)),Ug((()=>e.defaultValue))),g=Ug((()=>void 0===f.value?Ob(h.value,{1:[],0:void 0}):f.value)),v={listboxState:a,value:g,mode:h,compare(t,r){if(\"string\"==typeof e.by){let n=e.by;return(null==t?void 0:t[n])===(null==r?void 0:r[n])}return e.by(t,r)},orientation:Ug((()=>e.horizontal?\"horizontal\":\"vertical\")),labelRef:o,buttonRef:i,optionsRef:s,disabled:Ug((()=>e.disabled)),options:l,searchQuery:c,activeOptionIndex:u,activationTrigger:d,closeListbox(){e.disabled||1!==a.value&&(a.value=1,u.value=null)},openListbox(){e.disabled||0!==a.value&&(a.value=0)},goToOption(t,r,n){if(e.disabled||1===a.value)return;let o=p(),i=gy(t===my.Specific?{focus:my.Specific,id:r}:{focus:t},{resolveItems:()=>o.options,resolveActiveIndex:()=>o.activeOptionIndex,resolveId:e=>e.id,resolveDisabled:e=>e.dataRef.disabled});c.value=\"\",u.value=i,d.value=null!=n?n:1,l.value=o.options},search(t){if(e.disabled||1===a.value)return;let r=\"\"!==c.value?0:1;c.value+=t.toLowerCase();let n=(null!==u.value?l.value.slice(u.value+r).concat(l.value.slice(0,u.value+r)):l.value).find((e=>e.dataRef.textValue.startsWith(c.value)&&!e.dataRef.disabled)),o=n?l.value.indexOf(n):-1;-1===o||o===u.value||(u.value=o,d.value=1)},clearSearch(){e.disabled||1!==a.value&&\"\"!==c.value&&(c.value=\"\")},registerOption(e,t){let r=p((r=>[...r,{id:e,dataRef:t}]));l.value=r.options,u.value=r.activeOptionIndex},unregisterOption(e){let t=p((t=>{let r=t.findIndex((t=>t.id===e));return-1!==r&&t.splice(r,1),t}));l.value=t.options,u.value=t.activeOptionIndex,d.value=1},theirOnChange(t){e.disabled||m(t)},select(t){e.disabled||m(Ob(h.value,{0:()=>t,1:()=>{let e=Up(v.value.value).slice(),r=Up(t),n=e.findIndex((e=>v.compare(r,Up(e))));return-1===n?e.push(r):e.splice(n,1),e}}))}};qb([i,s],((e,t)=>{var r;v.closeListbox(),Mb(t,Ib.Loose)||(e.preventDefault(),null==(r=yb(i))||r.focus())}),Ug((()=>0===a.value))),lm(mO,v),uy(Ug((()=>Ob(a.value,{0:ly.Open,1:ly.Closed}))));let b=Ug((()=>{var e;return null==(e=yb(i))?void 0:e.closest(\"form\")}));return yf((()=>{Nm([b],(()=>{if(b.value&&void 0!==e.defaultValue)return b.value.addEventListener(\"reset\",t),()=>{var e;null==(e=b.value)||e.removeEventListener(\"reset\",t)};function t(){v.theirOnChange(e.defaultValue)}}),{immediate:!0})})),()=>{let{name:n,modelValue:o,disabled:i,form:s,...l}=e,c={open:0===a.value,disabled:i,value:g.value};return Bg(Gm,[...null!=n&&null!=g.value?vy({[n]:g.value}).map((([e,t])=>Bg(iy,ny({features:oy.Hidden,key:e,as:\"input\",type:\"hidden\",hidden:!0,readOnly:!0,form:s,disabled:i,name:e,value:t})))):[],Jb({ourProps:{},theirProps:{...r,...ay(l,[\"defaultValue\",\"onUpdate:modelValue\",\"horizontal\",\"multiple\",\"by\"])},slot:c,slots:t,attrs:r,name:\"Listbox\"})])}}}),bO=rf({name:\"ListboxLabel\",props:{as:{type:[Object,String],default:\"label\"},id:{type:String,default:null}},setup(e,{attrs:t,slots:r}){var n;let a=null!=(n=e.id)?n:`headlessui-listbox-label-${bb()}`,o=gO(\"ListboxLabel\");function i(){var e;null==(e=yb(o.buttonRef))||e.focus({preventScroll:!0})}return()=>{let n={open:0===o.listboxState.value,disabled:o.disabled.value},{...s}=e;return Jb({ourProps:{id:a,ref:o.labelRef,onClick:i},theirProps:s,slot:n,attrs:t,slots:r,name:\"ListboxLabel\"})}}}),yO=rf({name:\"ListboxButton\",props:{as:{type:[Object,String],default:\"button\"},id:{type:String,default:null}},setup(e,{attrs:t,slots:r,expose:n}){var a;let o=null!=(a=e.id)?a:`headlessui-listbox-button-${bb()}`,i=gO(\"ListboxButton\");function s(e){switch(e.key){case py.Space:case py.Enter:case py.ArrowDown:e.preventDefault(),i.openListbox(),vh((()=>{var e;null==(e=yb(i.optionsRef))||e.focus({preventScroll:!0}),i.value.value||i.goToOption(my.First)}));break;case py.ArrowUp:e.preventDefault(),i.openListbox(),vh((()=>{var e;null==(e=yb(i.optionsRef))||e.focus({preventScroll:!0}),i.value.value||i.goToOption(my.Last)}))}}function l(e){e.key===py.Space&&e.preventDefault()}function c(e){i.disabled.value||(0===i.listboxState.value?(i.closeListbox(),vh((()=>{var e;return null==(e=yb(i.buttonRef))?void 0:e.focus({preventScroll:!0})}))):(e.preventDefault(),i.openListbox(),function(e){requestAnimationFrame((()=>requestAnimationFrame(e)))}((()=>{var e;return null==(e=yb(i.optionsRef))?void 0:e.focus({preventScroll:!0})}))))}n({el:i.buttonRef,$el:i.buttonRef});let u=Vb(Ug((()=>({as:e.as,type:t.type}))),i.buttonRef);return()=>{var n,a;let d={open:0===i.listboxState.value,disabled:i.disabled.value,value:i.value.value},{...p}=e;return Jb({ourProps:{ref:i.buttonRef,id:o,type:u.value,\"aria-haspopup\":\"listbox\",\"aria-controls\":null==(n=yb(i.optionsRef))?void 0:n.id,\"aria-expanded\":0===i.listboxState.value,\"aria-labelledby\":i.labelRef.value?[null==(a=yb(i.labelRef))?void 0:a.id,o].join(\" \"):void 0,disabled:!0===i.disabled.value||void 0,onKeydown:s,onKeyup:l,onClick:c},theirProps:p,slot:d,attrs:t,slots:r,name:\"ListboxButton\"})}}}),OO=rf({name:\"ListboxOptions\",props:{as:{type:[Object,String],default:\"ul\"},static:{type:Boolean,default:!1},unmount:{type:Boolean,default:!0},id:{type:String,default:null}},setup(e,{attrs:t,slots:r,expose:n}){var a;let o=null!=(a=e.id)?a:`headlessui-listbox-options-${bb()}`,i=gO(\"ListboxOptions\"),s=Qp(null);function l(e){switch(s.value&&clearTimeout(s.value),e.key){case py.Space:if(\"\"!==i.searchQuery.value)return e.preventDefault(),e.stopPropagation(),i.search(e.key);case py.Enter:if(e.preventDefault(),e.stopPropagation(),null!==i.activeOptionIndex.value){let e=i.options.value[i.activeOptionIndex.value];i.select(e.dataRef.value)}0===i.mode.value&&(i.closeListbox(),vh((()=>{var e;return null==(e=yb(i.buttonRef))?void 0:e.focus({preventScroll:!0})})));break;case Ob(i.orientation.value,{vertical:py.ArrowDown,horizontal:py.ArrowRight}):return e.preventDefault(),e.stopPropagation(),i.goToOption(my.Next);case Ob(i.orientation.value,{vertical:py.ArrowUp,horizontal:py.ArrowLeft}):return e.preventDefault(),e.stopPropagation(),i.goToOption(my.Previous);case py.Home:case py.PageUp:return e.preventDefault(),e.stopPropagation(),i.goToOption(my.First);case py.End:case py.PageDown:return e.preventDefault(),e.stopPropagation(),i.goToOption(my.Last);case py.Escape:e.preventDefault(),e.stopPropagation(),i.closeListbox(),vh((()=>{var e;return null==(e=yb(i.buttonRef))?void 0:e.focus({preventScroll:!0})}));break;case py.Tab:e.preventDefault(),e.stopPropagation();break;default:1===e.key.length&&(i.search(e.key),s.value=setTimeout((()=>i.clearSearch()),350))}}n({el:i.optionsRef,$el:i.optionsRef});let c=cy(),u=Ug((()=>null!==c?(c.value&ly.Open)===ly.Open:0===i.listboxState.value));return()=>{var n,a;let s={open:0===i.listboxState.value},{...c}=e;return Jb({ourProps:{\"aria-activedescendant\":null===i.activeOptionIndex.value||null==(n=i.options.value[i.activeOptionIndex.value])?void 0:n.id,\"aria-multiselectable\":1===i.mode.value||void 0,\"aria-labelledby\":null==(a=yb(i.buttonRef))?void 0:a.id,\"aria-orientation\":i.orientation.value,id:o,onKeydown:l,role:\"listbox\",tabIndex:0,ref:i.optionsRef},theirProps:c,slot:s,attrs:t,slots:r,features:Yb.RenderStrategy|Yb.Static,visible:u.value,name:\"ListboxOptions\"})}}}),wO=rf({name:\"ListboxOption\",props:{as:{type:[Object,String],default:\"li\"},value:{type:[Object,String,Number,Boolean]},disabled:{type:Boolean,default:!1},id:{type:String,default:null}},setup(e,{slots:t,attrs:r,expose:n}){var a;let o=null!=(a=e.id)?a:`headlessui-listbox-option-${bb()}`,i=gO(\"ListboxOption\"),s=Qp(null);n({el:s,$el:s});let l=Ug((()=>null!==i.activeOptionIndex.value&&i.options.value[i.activeOptionIndex.value].id===o)),c=Ug((()=>Ob(i.mode.value,{0:()=>i.compare(Up(i.value.value),Up(e.value)),1:()=>Up(i.value.value).some((t=>i.compare(Up(t),Up(e.value))))}))),u=Ug((()=>Ob(i.mode.value,{1:()=>{var e;let t=Up(i.value.value);return(null==(e=i.options.value.find((e=>t.some((t=>i.compare(Up(t),Up(e.dataRef.value)))))))?void 0:e.id)===o},0:()=>c.value}))),d=uO(s),p=Ug((()=>({disabled:e.disabled,value:e.value,get textValue(){return d()},domRef:s})));function h(t){if(e.disabled)return t.preventDefault();i.select(e.value),0===i.mode.value&&(i.closeListbox(),vh((()=>{var e;return null==(e=yb(i.buttonRef))?void 0:e.focus({preventScroll:!0})})))}function f(){if(e.disabled)return i.goToOption(my.Nothing);i.goToOption(my.Specific,o)}yf((()=>i.registerOption(o,p))),kf((()=>i.unregisterOption(o))),yf((()=>{Nm([i.listboxState,c],(()=>{0===i.listboxState.value&&c.value&&Ob(i.mode.value,{1:()=>{u.value&&i.goToOption(my.Specific,o)},0:()=>{i.goToOption(my.Specific,o)}})}),{immediate:!0})})),Mm((()=>{0===i.listboxState.value&&l.value&&0!==i.activationTrigger.value&&vh((()=>{var e,t;return null==(t=null==(e=yb(s))?void 0:e.scrollIntoView)?void 0:t.call(e,{block:\"nearest\"})}))}));let m=Wb();function g(e){m.update(e)}function v(t){m.wasMoved(t)&&(e.disabled||l.value||i.goToOption(my.Specific,o,0))}function b(t){m.wasMoved(t)&&(e.disabled||l.value&&i.goToOption(my.Nothing))}return()=>{let{disabled:n}=e,a={active:l.value,selected:c.value,disabled:n},{value:i,disabled:u,...d}=e;return Jb({ourProps:{id:o,ref:s,role:\"option\",tabIndex:!0===n?void 0:-1,\"aria-disabled\":!0===n||void 0,\"aria-selected\":c.value,disabled:void 0,onClick:h,onFocus:f,onPointerenter:g,onMouseenter:g,onPointermove:v,onMousemove:v,onPointerleave:b,onMouseleave:b},theirProps:d,slot:a,attrs:r,slots:t,name:\"ListboxOption\"})}}});var xO=(e=>(e[e.Open=0]=\"Open\",e[e.Closed=1]=\"Closed\",e))(xO||{}),kO=(e=>(e[e.Pointer=0]=\"Pointer\",e[e.Other=1]=\"Other\",e))(kO||{});let SO=Symbol(\"MenuContext\");function _O(e){let t=cm(SO,null);if(null===t){let t=new Error(`<${e} /> is missing a parent component.`);throw Error.captureStackTrace&&Error.captureStackTrace(t,_O),t}return t}let EO=rf({name:\"Menu\",props:{as:{type:[Object,String],default:\"template\"}},setup(e,{slots:t,attrs:r}){let n=Qp(1),a=Qp(null),o=Qp(null),i=Qp([]),s=Qp(\"\"),l=Qp(null),c=Qp(1);function u(e=e=>e){let t=null!==l.value?i.value[l.value]:null,r=Ub(e(i.value.slice()),(e=>yb(e.dataRef.domRef))),n=t?r.indexOf(t):null;return-1===n&&(n=null),{items:r,activeItemIndex:n}}let d={menuState:n,buttonRef:a,itemsRef:o,items:i,searchQuery:s,activeItemIndex:l,activationTrigger:c,closeMenu:()=>{n.value=1,l.value=null},openMenu:()=>n.value=0,goToItem(e,t,r){let n=u(),a=gy(e===my.Specific?{focus:my.Specific,id:t}:{focus:e},{resolveItems:()=>n.items,resolveActiveIndex:()=>n.activeItemIndex,resolveId:e=>e.id,resolveDisabled:e=>e.dataRef.disabled});s.value=\"\",l.value=a,c.value=null!=r?r:1,i.value=n.items},search(e){let t=\"\"!==s.value?0:1;s.value+=e.toLowerCase();let r=(null!==l.value?i.value.slice(l.value+t).concat(i.value.slice(0,l.value+t)):i.value).find((e=>e.dataRef.textValue.startsWith(s.value)&&!e.dataRef.disabled)),n=r?i.value.indexOf(r):-1;-1===n||n===l.value||(l.value=n,c.value=1)},clearSearch(){s.value=\"\"},registerItem(e,t){let r=u((r=>[...r,{id:e,dataRef:t}]));i.value=r.items,l.value=r.activeItemIndex,c.value=1},unregisterItem(e){let t=u((t=>{let r=t.findIndex((t=>t.id===e));return-1!==r&&t.splice(r,1),t}));i.value=t.items,l.value=t.activeItemIndex,c.value=1}};return qb([a,o],((e,t)=>{var r;d.closeMenu(),Mb(t,Ib.Loose)||(e.preventDefault(),null==(r=yb(a))||r.focus())}),Ug((()=>0===n.value))),lm(SO,d),uy(Ug((()=>Ob(n.value,{0:ly.Open,1:ly.Closed})))),()=>{let a={open:0===n.value,close:d.closeMenu};return Jb({ourProps:{},theirProps:e,slot:a,slots:t,attrs:r,name:\"Menu\"})}}}),TO=rf({name:\"MenuButton\",props:{disabled:{type:Boolean,default:!1},as:{type:[Object,String],default:\"button\"},id:{type:String,default:null}},setup(e,{attrs:t,slots:r,expose:n}){var a;let o=null!=(a=e.id)?a:`headlessui-menu-button-${bb()}`,i=_O(\"MenuButton\");function s(e){switch(e.key){case py.Space:case py.Enter:case py.ArrowDown:e.preventDefault(),e.stopPropagation(),i.openMenu(),vh((()=>{var e;null==(e=yb(i.itemsRef))||e.focus({preventScroll:!0}),i.goToItem(my.First)}));break;case py.ArrowUp:e.preventDefault(),e.stopPropagation(),i.openMenu(),vh((()=>{var e;null==(e=yb(i.itemsRef))||e.focus({preventScroll:!0}),i.goToItem(my.Last)}))}}function l(e){e.key===py.Space&&e.preventDefault()}function c(t){e.disabled||(0===i.menuState.value?(i.closeMenu(),vh((()=>{var e;return null==(e=yb(i.buttonRef))?void 0:e.focus({preventScroll:!0})}))):(t.preventDefault(),i.openMenu(),function(e){requestAnimationFrame((()=>requestAnimationFrame(e)))}((()=>{var e;return null==(e=yb(i.itemsRef))?void 0:e.focus({preventScroll:!0})}))))}n({el:i.buttonRef,$el:i.buttonRef});let u=Vb(Ug((()=>({as:e.as,type:t.type}))),i.buttonRef);return()=>{var n;let a={open:0===i.menuState.value},{...d}=e;return Jb({ourProps:{ref:i.buttonRef,id:o,type:u.value,\"aria-haspopup\":\"menu\",\"aria-controls\":null==(n=yb(i.itemsRef))?void 0:n.id,\"aria-expanded\":0===i.menuState.value,onKeydown:s,onKeyup:l,onClick:c},theirProps:d,slot:a,attrs:t,slots:r,name:\"MenuButton\"})}}}),AO=rf({name:\"MenuItems\",props:{as:{type:[Object,String],default:\"div\"},static:{type:Boolean,default:!1},unmount:{type:Boolean,default:!0},id:{type:String,default:null}},setup(e,{attrs:t,slots:r,expose:n}){var a;let o=null!=(a=e.id)?a:`headlessui-menu-items-${bb()}`,i=_O(\"MenuItems\"),s=Qp(null);function l(e){var t;switch(s.value&&clearTimeout(s.value),e.key){case py.Space:if(\"\"!==i.searchQuery.value)return e.preventDefault(),e.stopPropagation(),i.search(e.key);case py.Enter:e.preventDefault(),e.stopPropagation(),null!==i.activeItemIndex.value&&(null==(t=yb(i.items.value[i.activeItemIndex.value].dataRef.domRef))||t.click()),i.closeMenu(),Nb(yb(i.buttonRef));break;case py.ArrowDown:return e.preventDefault(),e.stopPropagation(),i.goToItem(my.Next);case py.ArrowUp:return e.preventDefault(),e.stopPropagation(),i.goToItem(my.Previous);case py.Home:case py.PageUp:return e.preventDefault(),e.stopPropagation(),i.goToItem(my.First);case py.End:case py.PageDown:return e.preventDefault(),e.stopPropagation(),i.goToItem(my.Last);case py.Escape:e.preventDefault(),e.stopPropagation(),i.closeMenu(),vh((()=>{var e;return null==(e=yb(i.buttonRef))?void 0:e.focus({preventScroll:!0})}));break;case py.Tab:e.preventDefault(),e.stopPropagation(),i.closeMenu(),vh((()=>function(e,t){return Bb(Db(),t,{relativeTo:e})}(yb(i.buttonRef),e.shiftKey?$b.Previous:$b.Next)));break;default:1===e.key.length&&(i.search(e.key),s.value=setTimeout((()=>i.clearSearch()),350))}}function c(e){e.key===py.Space&&e.preventDefault()}n({el:i.itemsRef,$el:i.itemsRef}),Xb({container:Ug((()=>yb(i.itemsRef))),enabled:Ug((()=>0===i.menuState.value)),accept:e=>\"menuitem\"===e.getAttribute(\"role\")?NodeFilter.FILTER_REJECT:e.hasAttribute(\"role\")?NodeFilter.FILTER_SKIP:NodeFilter.FILTER_ACCEPT,walk(e){e.setAttribute(\"role\",\"none\")}});let u=cy(),d=Ug((()=>null!==u?(u.value&ly.Open)===ly.Open:0===i.menuState.value));return()=>{var n,a;let s={open:0===i.menuState.value},{...u}=e;return Jb({ourProps:{\"aria-activedescendant\":null===i.activeItemIndex.value||null==(n=i.items.value[i.activeItemIndex.value])?void 0:n.id,\"aria-labelledby\":null==(a=yb(i.buttonRef))?void 0:a.id,id:o,onKeydown:l,onKeyup:c,role:\"menu\",tabIndex:0,ref:i.itemsRef},theirProps:u,slot:s,attrs:t,slots:r,features:Yb.RenderStrategy|Yb.Static,visible:d.value,name:\"MenuItems\"})}}}),$O=rf({name:\"MenuItem\",inheritAttrs:!1,props:{as:{type:[Object,String],default:\"template\"},disabled:{type:Boolean,default:!1},id:{type:String,default:null}},setup(e,{slots:t,attrs:r,expose:n}){var a;let o=null!=(a=e.id)?a:`headlessui-menu-item-${bb()}`,i=_O(\"MenuItem\"),s=Qp(null);n({el:s,$el:s});let l=Ug((()=>null!==i.activeItemIndex.value&&i.items.value[i.activeItemIndex.value].id===o)),c=uO(s),u=Ug((()=>({disabled:e.disabled,get textValue(){return c()},domRef:s})));function d(t){if(e.disabled)return t.preventDefault();i.closeMenu(),Nb(yb(i.buttonRef))}function p(){if(e.disabled)return i.goToItem(my.Nothing);i.goToItem(my.Specific,o)}yf((()=>i.registerItem(o,u))),kf((()=>i.unregisterItem(o))),Mm((()=>{0===i.menuState.value&&l.value&&0!==i.activationTrigger.value&&vh((()=>{var e,t;return null==(t=null==(e=yb(s))?void 0:e.scrollIntoView)?void 0:t.call(e,{block:\"nearest\"})}))}));let h=Wb();function f(e){h.update(e)}function m(t){h.wasMoved(t)&&(e.disabled||l.value||i.goToItem(my.Specific,o,0))}function g(t){h.wasMoved(t)&&(e.disabled||l.value&&i.goToItem(my.Nothing))}return()=>{let{disabled:n,...a}=e,c={active:l.value,disabled:n,close:i.closeMenu};return Jb({ourProps:{id:o,ref:s,role:\"menuitem\",tabIndex:!0===n?void 0:-1,\"aria-disabled\":!0===n||void 0,onClick:d,onFocus:p,onPointerenter:f,onMouseenter:f,onPointermove:m,onMousemove:m,onPointerleave:g,onMouseleave:g},theirProps:{...r,...a},slot:c,attrs:r,slots:t,name:\"MenuItem\"})}}});var CO,PO=((CO=PO||{})[CO.Open=0]=\"Open\",CO[CO.Closed=1]=\"Closed\",CO);let DO=Symbol(\"PopoverContext\");function IO(e){let t=cm(DO,null);if(null===t){let t=new Error(`<${e} /> is missing a parent <${jO.name} /> component.`);throw Error.captureStackTrace&&Error.captureStackTrace(t,IO),t}return t}let MO=Symbol(\"PopoverGroupContext\");function NO(){return cm(MO,null)}let RO=Symbol(\"PopoverPanelContext\"),jO=rf({name:\"Popover\",inheritAttrs:!1,props:{as:{type:[Object,String],default:\"div\"}},setup(e,{slots:t,attrs:r,expose:n}){var a;let o=Qp(null);n({el:o,$el:o});let i=Qp(1),s=Qp(null),l=Qp(null),c=Qp(null),u=Qp(null),d=Ug((()=>Sb(o))),p=Ug((()=>{var e,t;if(!yb(s)||!yb(u))return!1;for(let e of document.querySelectorAll(\"body > *\"))if(Number(null==e?void 0:e.contains(yb(s)))^Number(null==e?void 0:e.contains(yb(u))))return!0;let r=Db(),n=r.indexOf(yb(s)),a=(n+r.length-1)%r.length,o=(n+1)%r.length,i=r[a],l=r[o];return!(null!=(e=yb(u))&&e.contains(i)||null!=(t=yb(u))&&t.contains(l))})),h={popoverState:i,buttonId:Qp(null),panelId:Qp(null),panel:u,button:s,isPortalled:p,beforePanelSentinel:l,afterPanelSentinel:c,togglePopover(){i.value=Ob(i.value,{0:1,1:0})},closePopover(){1!==i.value&&(i.value=1)},close(e){h.closePopover();let t=e?e instanceof HTMLElement?e:e.value instanceof HTMLElement?yb(e):yb(h.button):yb(h.button);null==t||t.focus()}};lm(DO,h),uy(Ug((()=>Ob(i.value,{0:ly.Open,1:ly.Closed}))));let f={buttonId:h.buttonId,panelId:h.panelId,close(){h.closePopover()}},m=NO(),g=null==m?void 0:m.registerPopover,[v,b]=qy(),y=Iy({mainTreeNodeRef:null==m?void 0:m.mainTreeNodeRef,portals:v,defaultContainers:[s,u]});return Mm((()=>null==g?void 0:g(f))),Oy(null==(a=d.value)?void 0:a.defaultView,\"focus\",(e=>{var t,r;e.target!==window&&e.target instanceof HTMLElement&&0===i.value&&(function(){var e,t,r,n;return null!=(n=null==m?void 0:m.isFocusWithinPopoverGroup())?n:(null==(e=d.value)?void 0:e.activeElement)&&((null==(t=yb(s))?void 0:t.contains(d.value.activeElement))||(null==(r=yb(u))?void 0:r.contains(d.value.activeElement)))}()||s&&u&&(y.contains(e.target)||null!=(t=yb(h.beforePanelSentinel))&&t.contains(e.target)||null!=(r=yb(h.afterPanelSentinel))&&r.contains(e.target)||h.closePopover()))}),!0),qb(y.resolveContainers,((e,t)=>{var r;h.closePopover(),Mb(t,Ib.Loose)||(e.preventDefault(),null==(r=yb(s))||r.focus())}),Ug((()=>0===i.value))),()=>{let n={open:0===i.value,close:h.close};return Bg(Gm,[Bg(b,{},(()=>Jb({theirProps:{...e,...r},ourProps:{ref:o},slot:n,slots:t,attrs:r,name:\"Popover\"}))),Bg(y.MainTreeNode)])}}}),LO=rf({name:\"PopoverButton\",props:{as:{type:[Object,String],default:\"button\"},disabled:{type:[Boolean],default:!1},id:{type:String,default:null}},inheritAttrs:!1,setup(e,{attrs:t,slots:r,expose:n}){var a;let o=null!=(a=e.id)?a:`headlessui-popover-button-${bb()}`,i=IO(\"PopoverButton\"),s=Ug((()=>Sb(i.button)));n({el:i.button,$el:i.button}),yf((()=>{i.buttonId.value=o})),kf((()=>{i.buttonId.value=null}));let l=NO(),c=null==l?void 0:l.closeOthers,u=cm(RO,null),d=Ug((()=>null!==u&&u.value===i.panelId.value)),p=Qp(null),h=`headlessui-focus-sentinel-${bb()}`;d.value||Mm((()=>{i.button.value=yb(p)}));let f=Vb(Ug((()=>({as:e.as,type:t.type}))),p);function m(e){var t,r,n,a,o;if(d.value){if(1===i.popoverState.value)return;switch(e.key){case py.Space:case py.Enter:e.preventDefault(),null==(r=(t=e.target).click)||r.call(t),i.closePopover(),null==(n=yb(i.button))||n.focus()}}else switch(e.key){case py.Space:case py.Enter:e.preventDefault(),e.stopPropagation(),1===i.popoverState.value&&(null==c||c(i.buttonId.value)),i.togglePopover();break;case py.Escape:if(0!==i.popoverState.value)return null==c?void 0:c(i.buttonId.value);if(!yb(i.button)||null!=(a=s.value)&&a.activeElement&&(null==(o=yb(i.button))||!o.contains(s.value.activeElement)))return;e.preventDefault(),e.stopPropagation(),i.closePopover()}}function g(e){d.value||e.key===py.Space&&e.preventDefault()}function v(t){var r,n;e.disabled||(d.value?(i.closePopover(),null==(r=yb(i.button))||r.focus()):(t.preventDefault(),t.stopPropagation(),1===i.popoverState.value&&(null==c||c(i.buttonId.value)),i.togglePopover(),null==(n=yb(i.button))||n.focus()))}function b(e){e.preventDefault(),e.stopPropagation()}let y=xy();function O(){let e=yb(i.panel);e&&Ob(y.value,{[wy.Forwards]:()=>Bb(e,$b.First),[wy.Backwards]:()=>Bb(e,$b.Last)})===Cb.Error&&Bb(Db().filter((e=>\"true\"!==e.dataset.headlessuiFocusGuard)),Ob(y.value,{[wy.Forwards]:$b.Next,[wy.Backwards]:$b.Previous}),{relativeTo:yb(i.button)})}return()=>{let n=0===i.popoverState.value,a={open:n},{...s}=e,l=d.value?{ref:p,type:f.value,onKeydown:m,onClick:v}:{ref:p,id:o,type:f.value,\"aria-expanded\":0===i.popoverState.value,\"aria-controls\":yb(i.panel)?i.panelId.value:void 0,disabled:!!e.disabled||void 0,onKeydown:m,onKeyup:g,onClick:v,onMousedown:b};return Bg(Gm,[Jb({ourProps:l,theirProps:{...t,...s},slot:a,attrs:t,slots:r,name:\"PopoverButton\"}),n&&!d.value&&i.isPortalled.value&&Bg(iy,{id:h,features:oy.Focusable,\"data-headlessui-focus-guard\":!0,as:\"button\",type:\"button\",onFocus:O})])}}}),UO=rf({name:\"PopoverPanel\",props:{as:{type:[Object,String],default:\"div\"},static:{type:Boolean,default:!1},unmount:{type:Boolean,default:!0},focus:{type:Boolean,default:!1},id:{type:String,default:null}},inheritAttrs:!1,setup(e,{attrs:t,slots:r,expose:n}){var a;let o=null!=(a=e.id)?a:`headlessui-popover-panel-${bb()}`,{focus:i}=e,s=IO(\"PopoverPanel\"),l=Ug((()=>Sb(s.panel))),c=`headlessui-focus-sentinel-before-${bb()}`,u=`headlessui-focus-sentinel-after-${bb()}`;n({el:s.panel,$el:s.panel}),yf((()=>{s.panelId.value=o})),kf((()=>{s.panelId.value=null})),lm(RO,s.panelId),Mm((()=>{var e,t;if(!i||0!==s.popoverState.value||!s.panel)return;let r=null==(e=l.value)?void 0:e.activeElement;null!=(t=yb(s.panel))&&t.contains(r)||Bb(yb(s.panel),$b.First)}));let d=cy(),p=Ug((()=>null!==d?(d.value&ly.Open)===ly.Open:0===s.popoverState.value));function h(e){var t,r;if(e.key===py.Escape){if(0!==s.popoverState.value||!yb(s.panel)||l.value&&(null==(t=yb(s.panel))||!t.contains(l.value.activeElement)))return;e.preventDefault(),e.stopPropagation(),s.closePopover(),null==(r=yb(s.button))||r.focus()}}function f(e){var t,r,n,a,o;let i=e.relatedTarget;i&&yb(s.panel)&&(null!=(t=yb(s.panel))&&t.contains(i)||(s.closePopover(),(null!=(n=null==(r=yb(s.beforePanelSentinel))?void 0:r.contains)&&n.call(r,i)||null!=(o=null==(a=yb(s.afterPanelSentinel))?void 0:a.contains)&&o.call(a,i))&&i.focus({preventScroll:!0})))}let m=xy();function g(){let e=yb(s.panel);e&&Ob(m.value,{[wy.Forwards]:()=>{var t;Bb(e,$b.First)===Cb.Error&&(null==(t=yb(s.afterPanelSentinel))||t.focus())},[wy.Backwards]:()=>{var e;null==(e=yb(s.button))||e.focus({preventScroll:!0})}})}function v(){let e=yb(s.panel);e&&Ob(m.value,{[wy.Forwards]:()=>{let e=yb(s.button),t=yb(s.panel);if(!e)return;let r=Db(),n=r.indexOf(e),a=r.slice(0,n+1),o=[...r.slice(n+1),...a];for(let e of o.slice())if(\"true\"===e.dataset.headlessuiFocusGuard||null!=t&&t.contains(e)){let t=o.indexOf(e);-1!==t&&o.splice(t,1)}Bb(o,$b.First,{sorted:!1})},[wy.Backwards]:()=>{var t;Bb(e,$b.Previous)===Cb.Error&&(null==(t=yb(s.button))||t.focus())}})}return()=>{let n={open:0===s.popoverState.value,close:s.close},{focus:a,...l}=e;return Jb({ourProps:{ref:s.panel,id:o,onKeydown:h,onFocusout:i&&0===s.popoverState.value?f:void 0,tabIndex:-1},theirProps:{...t,...l},attrs:t,slot:n,slots:{...r,default:(...e)=>{var t;return[Bg(Gm,[p.value&&s.isPortalled.value&&Bg(iy,{id:c,ref:s.beforePanelSentinel,features:oy.Focusable,\"data-headlessui-focus-guard\":!0,as:\"button\",type:\"button\",onFocus:g}),null==(t=r.default)?void 0:t.call(r,...e),p.value&&s.isPortalled.value&&Bg(iy,{id:u,ref:s.afterPanelSentinel,features:oy.Focusable,\"data-headlessui-focus-guard\":!0,as:\"button\",type:\"button\",onFocus:v})])]}},features:Yb.RenderStrategy|Yb.Static,visible:p.value,name:\"PopoverPanel\"})}}}),BO=Symbol(\"LabelContext\");function zO(){let e=cm(BO,null);if(null===e){let e=new Error(\"You used a

= P extends + SerializableWithResult ? SR | RR + : never + /** + * @since 3.10.0 + */ + export type Any = SerializableWithResult + /** + * @since 3.10.0 + */ + export type All = + | Any + | SerializableWithResult +} + +/** + * @since 3.10.0 + */ +export const asSerializableWithResult = ( + procedure: SWR +): SerializableWithResult< + Serializable.Type, + Serializable.Encoded, + Serializable.Context, + WithResult.Success, + WithResult.SuccessEncoded, + WithResult.Failure, + WithResult.FailureEncoded, + WithResult.Context +> => procedure as any + +/** + * @since 3.10.0 + */ +export interface TaggedRequest< + Tag extends string, + A, + I, + R, + SuccessType, + SuccessEncoded, + FailureType, + FailureEncoded, + ResultR +> extends + Request.Request, + SerializableWithResult< + A, + I, + R, + SuccessType, + SuccessEncoded, + FailureType, + FailureEncoded, + ResultR + > +{ + readonly _tag: Tag +} + +/** + * @since 3.10.0 + */ +export declare namespace TaggedRequest { + /** + * @since 3.10.0 + */ + export type Any = TaggedRequest + /** + * @since 3.10.0 + */ + export type All = + | Any + | TaggedRequest +} + +/** + * @category api interface + * @since 3.10.0 + */ +export interface TaggedRequestClass< + Self, + Tag extends string, + Payload extends Struct.Fields, + Success extends Schema.All, + Failure extends Schema.All +> extends + Class< + Self, + Payload, + Struct.Encoded, + Struct.Context, + Struct.Constructor>, + TaggedRequest< + Tag, + Self, + Struct.Encoded, + Struct.Context, + Schema.Type, + Schema.Encoded, + Schema.Type, + Schema.Encoded, + Schema.Context | Schema.Context + >, + {} + > +{ + readonly _tag: Tag + readonly success: Success + readonly failure: Failure +} + +/** + * @example + * ```ts + * import { Schema } from "effect" + * + * class MyRequest extends Schema.TaggedRequest("MyRequest")("MyRequest", { + * failure: Schema.String, + * success: Schema.Number, + * payload: { id: Schema.String } + * }) {} + * ``` + * + * @category classes + * @since 3.10.0 + */ +export const TaggedRequest = + (identifier?: string) => + ( + tag: Tag, + options: { + failure: Failure + success: Success + payload: Payload + }, + annotations?: ClassAnnotations } & Payload>>> + ): [Self] extends [never] ? MissingSelfGeneric<"TaggedRequest", `"Tag", SuccessSchema, FailureSchema, `> + : TaggedRequestClass< + Self, + Tag, + { readonly _tag: tag } & Payload, + Success, + Failure + > => + { + const taggedFields = extendFields({ _tag: getClassTag(tag) }, options.payload) + return class TaggedRequestClass extends makeClass({ + kind: "TaggedRequest", + identifier: identifier ?? tag, + schema: Struct(taggedFields), + fields: taggedFields, + Base: Request.Class, + annotations + }) { + static _tag = tag + static success = options.success + static failure = options.failure + get [symbolSerializable]() { + return this.constructor + } + get [symbolWithResult]() { + return { + failure: options.failure, + success: options.success + } + } + } as any + } + +// ------------------------------------------------------------------------------------------------- +// Equivalence compiler +// ------------------------------------------------------------------------------------------------- + +/** + * Given a schema `Schema`, returns an `Equivalence` instance for `A`. + * + * @category Equivalence + * @since 3.10.0 + */ +export const equivalence = (schema: Schema): Equivalence.Equivalence => go(schema.ast, []) + +const getEquivalenceAnnotation = AST.getAnnotation>(AST.EquivalenceAnnotationId) + +const go = (ast: AST.AST, path: ReadonlyArray): Equivalence.Equivalence => { + const hook = getEquivalenceAnnotation(ast) + if (option_.isSome(hook)) { + switch (ast._tag) { + case "Declaration": + return hook.value(...ast.typeParameters.map((tp) => go(tp, path))) + case "Refinement": + return hook.value(go(ast.from, path)) + default: + return hook.value() + } + } + switch (ast._tag) { + case "NeverKeyword": + throw new Error(errors_.getEquivalenceUnsupportedErrorMessage(ast, path)) + case "Transformation": + return go(ast.to, path) + case "Declaration": + case "Literal": + case "StringKeyword": + case "TemplateLiteral": + case "UniqueSymbol": + case "SymbolKeyword": + case "UnknownKeyword": + case "AnyKeyword": + case "NumberKeyword": + case "BooleanKeyword": + case "BigIntKeyword": + case "UndefinedKeyword": + case "VoidKeyword": + case "Enums": + case "ObjectKeyword": + return Equal.equals + case "Refinement": + return go(ast.from, path) + case "Suspend": { + const get = util_.memoizeThunk(() => go(ast.f(), path)) + return (a, b) => get()(a, b) + } + case "TupleType": { + const elements = ast.elements.map((element, i) => go(element.type, path.concat(i))) + const rest = ast.rest.map((annotatedAST) => go(annotatedAST.type, path)) + return Equivalence.make((a, b) => { + if (!Array.isArray(a) || !Array.isArray(b)) { + return false + } + const len = a.length + if (len !== b.length) { + return false + } + // --------------------------------------------- + // handle elements + // --------------------------------------------- + let i = 0 + for (; i < Math.min(len, ast.elements.length); i++) { + if (!elements[i](a[i], b[i])) { + return false + } + } + // --------------------------------------------- + // handle rest element + // --------------------------------------------- + if (array_.isNonEmptyReadonlyArray(rest)) { + const [head, ...tail] = rest + for (; i < len - tail.length; i++) { + if (!head(a[i], b[i])) { + return false + } + } + // --------------------------------------------- + // handle post rest elements + // --------------------------------------------- + for (let j = 0; j < tail.length; j++) { + i += j + if (!tail[j](a[i], b[i])) { + return false + } + } + } + return true + }) + } + case "TypeLiteral": { + if (ast.propertySignatures.length === 0 && ast.indexSignatures.length === 0) { + return Equal.equals + } + const propertySignatures = ast.propertySignatures.map((ps) => go(ps.type, path.concat(ps.name))) + const indexSignatures = ast.indexSignatures.map((is) => go(is.type, path)) + return Equivalence.make((a, b) => { + if (!Predicate.isRecord(a) || !Predicate.isRecord(b)) { + return false + } + const aStringKeys = Object.keys(a) + const aSymbolKeys = Object.getOwnPropertySymbols(a) + // --------------------------------------------- + // handle property signatures + // --------------------------------------------- + for (let i = 0; i < propertySignatures.length; i++) { + const ps = ast.propertySignatures[i] + const name = ps.name + const aHas = Object.prototype.hasOwnProperty.call(a, name) + const bHas = Object.prototype.hasOwnProperty.call(b, name) + if (ps.isOptional) { + if (aHas !== bHas) { + return false + } + } + if (aHas && bHas && !propertySignatures[i](a[name], b[name])) { + return false + } + } + // --------------------------------------------- + // handle index signatures + // --------------------------------------------- + let bSymbolKeys: Array | undefined + let bStringKeys: Array | undefined + for (let i = 0; i < indexSignatures.length; i++) { + const is = ast.indexSignatures[i] + const encodedParameter = AST.getEncodedParameter(is.parameter) + const isSymbol = AST.isSymbolKeyword(encodedParameter) + if (isSymbol) { + bSymbolKeys = bSymbolKeys || Object.getOwnPropertySymbols(b) + if (aSymbolKeys.length !== bSymbolKeys.length) { + return false + } + } else { + bStringKeys = bStringKeys || Object.keys(b) + if (aStringKeys.length !== bStringKeys.length) { + return false + } + } + const aKeys = isSymbol ? aSymbolKeys : aStringKeys + for (let j = 0; j < aKeys.length; j++) { + const key = aKeys[j] + if ( + !Object.prototype.hasOwnProperty.call(b, key) || !indexSignatures[i](a[key], b[key]) + ) { + return false + } + } + } + return true + }) + } + case "Union": { + const searchTree = ParseResult.getSearchTree(ast.types, true) + const ownKeys = Reflect.ownKeys(searchTree.keys) + const len = ownKeys.length + return Equivalence.make((a, b) => { + let candidates: Array = [] + if (len > 0 && Predicate.isRecordOrArray(a)) { + for (let i = 0; i < len; i++) { + const name = ownKeys[i] + const buckets = searchTree.keys[name].buckets + if (Object.prototype.hasOwnProperty.call(a, name)) { + const literal = String(a[name]) + if (Object.prototype.hasOwnProperty.call(buckets, literal)) { + candidates = candidates.concat(buckets[literal]) + } + } + } + } + if (searchTree.otherwise.length > 0) { + candidates = candidates.concat(searchTree.otherwise) + } + const tuples = candidates.map((ast) => [go(ast, path), ParseResult.is({ ast } as any)] as const) + for (let i = 0; i < tuples.length; i++) { + const [equivalence, is] = tuples[i] + if (is(a) && is(b)) { + if (equivalence(a, b)) { + return true + } + } + } + return false + }) + } + } +} + +const SymbolStruct = TaggedStruct("symbol", { + key: String$ +}).annotations({ description: "an object to be decoded into a globally shared symbol" }) + +const SymbolFromStruct = transformOrFail( + SymbolStruct, + SymbolFromSelf, + { + strict: true, + decode: (i) => decodeSymbol(i.key), + encode: (a, _, ast) => ParseResult.map(encodeSymbol(a, ast), (key) => SymbolStruct.make({ key })) + } +) + +/** @ignore */ +class PropertyKey$ extends Union(String$, Number$, SymbolFromStruct).annotations({ identifier: "PropertyKey" }) {} + +export { + /** + * @since 3.12.5 + */ + PropertyKey$ as PropertyKey +} + +/** + * @category ArrayFormatter + * @since 3.12.5 + */ +export class ArrayFormatterIssue extends Struct({ + _tag: propertySignature(Literal( + "Pointer", + "Unexpected", + "Missing", + "Composite", + "Refinement", + "Transformation", + "Type", + "Forbidden" + )).annotations({ description: "The tag identifying the type of parse issue" }), + path: propertySignature(Array$(PropertyKey$)).annotations({ + description: "The path to the property where the issue occurred" + }), + message: propertySignature(String$).annotations({ description: "A descriptive message explaining the issue" }) +}).annotations({ + identifier: "ArrayFormatterIssue", + description: "Represents an issue returned by the ArrayFormatter formatter" +}) {} diff --git a/repos/effect/packages/effect/src/SchemaAST.ts b/repos/effect/packages/effect/src/SchemaAST.ts new file mode 100644 index 0000000..1f9efb8 --- /dev/null +++ b/repos/effect/packages/effect/src/SchemaAST.ts @@ -0,0 +1,3047 @@ +/** + * @since 3.10.0 + */ + +import * as Arr from "./Array.js" +import type { Effect } from "./Effect.js" +import type { Equivalence } from "./Equivalence.js" +import { dual, identity } from "./Function.js" +import { globalValue } from "./GlobalValue.js" +import * as Inspectable from "./Inspectable.js" +import * as errors_ from "./internal/schema/errors.js" +import * as util_ from "./internal/schema/util.js" +import * as Number from "./Number.js" +import * as Option from "./Option.js" +import * as Order from "./Order.js" +import type { ParseIssue } from "./ParseResult.js" +import * as Predicate from "./Predicate.js" +import * as regexp from "./RegExp.js" +import type { Concurrency } from "./Types.js" + +/** + * @category model + * @since 3.10.0 + */ +export type AST = + | Declaration + | Literal + | UniqueSymbol + | UndefinedKeyword + | VoidKeyword + | NeverKeyword + | UnknownKeyword + | AnyKeyword + | StringKeyword + | NumberKeyword + | BooleanKeyword + | BigIntKeyword + | SymbolKeyword + | ObjectKeyword + | Enums + | TemplateLiteral + // possible transformations + | Refinement + | TupleType + | TypeLiteral + | Union + | Suspend + // transformations + | Transformation + +// ------------------------------------------------------------------------------------- +// annotations +// ------------------------------------------------------------------------------------- + +/** + * @category annotations + * @since 3.19.0 + * @experimental + */ +export type TypeConstructorAnnotation = { + readonly _tag: string + [key: PropertyKey]: unknown +} + +/** + * @category annotations + * @since 3.19.0 + * @experimental + */ +export const TypeConstructorAnnotationId: unique symbol = Symbol.for("effect/annotation/TypeConstructor") + +/** + * @category annotations + * @since 3.10.0 + */ +export type BrandAnnotation = Arr.NonEmptyReadonlyArray + +/** + * @category annotations + * @since 3.10.0 + */ +export const BrandAnnotationId: unique symbol = Symbol.for("effect/annotation/Brand") + +/** + * @category annotations + * @since 3.10.0 + */ +export type SchemaIdAnnotation = string | symbol + +/** + * @category annotations + * @since 3.10.0 + */ +export const SchemaIdAnnotationId: unique symbol = Symbol.for("effect/annotation/SchemaId") + +/** + * @category annotations + * @since 3.10.0 + */ +export type MessageAnnotation = (issue: ParseIssue) => string | Effect | { + readonly message: string | Effect + readonly override: boolean +} + +/** + * @category annotations + * @since 3.10.0 + */ +export const MessageAnnotationId: unique symbol = Symbol.for("effect/annotation/Message") + +/** + * @category annotations + * @since 3.10.0 + */ +export type MissingMessageAnnotation = () => string | Effect + +/** + * @category annotations + * @since 3.10.0 + */ +export const MissingMessageAnnotationId: unique symbol = Symbol.for("effect/annotation/MissingMessage") + +/** + * @category annotations + * @since 3.10.0 + */ +export type IdentifierAnnotation = string + +/** + * @category annotations + * @since 3.10.0 + */ +export const IdentifierAnnotationId: unique symbol = Symbol.for("effect/annotation/Identifier") + +/** + * @category annotations + * @since 3.10.0 + */ +export type TitleAnnotation = string + +/** + * @category annotations + * @since 3.10.0 + */ +export const TitleAnnotationId: unique symbol = Symbol.for("effect/annotation/Title") + +/** @internal */ +export const AutoTitleAnnotationId: unique symbol = Symbol.for("effect/annotation/AutoTitle") + +/** + * @category annotations + * @since 3.10.0 + */ +export type DescriptionAnnotation = string + +/** + * @category annotations + * @since 3.10.0 + */ +export const DescriptionAnnotationId: unique symbol = Symbol.for("effect/annotation/Description") + +/** + * @category annotations + * @since 3.10.0 + */ +export type ExamplesAnnotation = Arr.NonEmptyReadonlyArray + +/** + * @category annotations + * @since 3.10.0 + */ +export const ExamplesAnnotationId: unique symbol = Symbol.for("effect/annotation/Examples") + +/** + * @category annotations + * @since 3.10.0 + */ +export type DefaultAnnotation = A + +/** + * @category annotations + * @since 3.10.0 + */ +export const DefaultAnnotationId: unique symbol = Symbol.for("effect/annotation/Default") + +/** + * @category annotations + * @since 3.10.0 + */ +export type JSONSchemaAnnotation = object + +/** + * @category annotations + * @since 3.10.0 + */ +export const JSONSchemaAnnotationId: unique symbol = Symbol.for("effect/annotation/JSONSchema") + +/** + * @category annotations + * @since 3.10.0 + */ +export const ArbitraryAnnotationId: unique symbol = Symbol.for("effect/annotation/Arbitrary") + +/** + * @category annotations + * @since 3.10.0 + */ +export const PrettyAnnotationId: unique symbol = Symbol.for("effect/annotation/Pretty") + +/** + * @category annotations + * @since 3.10.0 + */ +export type EquivalenceAnnotation = readonly []> = ( + ...equivalences: { readonly [K in keyof TypeParameters]: Equivalence } +) => Equivalence + +/** + * @category annotations + * @since 3.10.0 + */ +export const EquivalenceAnnotationId: unique symbol = Symbol.for("effect/annotation/Equivalence") + +/** + * @category annotations + * @since 3.10.0 + */ +export type DocumentationAnnotation = string + +/** + * @category annotations + * @since 3.10.0 + */ +export const DocumentationAnnotationId: unique symbol = Symbol.for("effect/annotation/Documentation") + +/** + * @category annotations + * @since 3.10.0 + */ +export type ConcurrencyAnnotation = Concurrency | undefined + +/** + * @category annotations + * @since 3.10.0 + */ +export const ConcurrencyAnnotationId: unique symbol = Symbol.for("effect/annotation/Concurrency") + +/** + * @category annotations + * @since 3.10.0 + */ +export type BatchingAnnotation = boolean | "inherit" | undefined + +/** + * @category annotations + * @since 3.10.0 + */ +export const BatchingAnnotationId: unique symbol = Symbol.for("effect/annotation/Batching") + +/** + * @category annotations + * @since 3.10.0 + */ +export type ParseIssueTitleAnnotation = (issue: ParseIssue) => string | undefined + +/** + * @category annotations + * @since 3.10.0 + */ +export const ParseIssueTitleAnnotationId: unique symbol = Symbol.for("effect/annotation/ParseIssueTitle") + +/** + * @category annotations + * @since 3.10.0 + */ +export const ParseOptionsAnnotationId: unique symbol = Symbol.for("effect/annotation/ParseOptions") + +/** + * @category annotations + * @since 3.10.0 + */ +export type DecodingFallbackAnnotation = (issue: ParseIssue) => Effect + +/** + * @category annotations + * @since 3.10.0 + */ +export const DecodingFallbackAnnotationId: unique symbol = Symbol.for("effect/annotation/DecodingFallback") + +/** + * @category annotations + * @since 3.10.0 + */ +export const SurrogateAnnotationId: unique symbol = Symbol.for("effect/annotation/Surrogate") + +/** + * @category annotations + * @since 3.10.0 + */ +export type SurrogateAnnotation = AST + +/** @internal */ +export const StableFilterAnnotationId: unique symbol = Symbol.for("effect/annotation/StableFilter") + +/** + * A stable filter consistently applies fixed validation rules, such as + * 'minItems', 'maxItems', and 'itemsCount', to ensure array length complies + * with set criteria regardless of the input data's content. + * + * @internal + */ +export type StableFilterAnnotation = boolean + +/** + * @category annotations + * @since 3.10.0 + */ +export interface Annotations { + readonly [_: string]: unknown + readonly [_: symbol]: unknown +} + +/** + * @category annotations + * @since 3.10.0 + */ +export interface Annotated { + readonly annotations: Annotations +} + +/** + * @category annotations + * @since 3.10.0 + */ +export const getAnnotation: { + (key: symbol): (annotated: Annotated) => Option.Option + (annotated: Annotated, key: symbol): Option.Option +} = dual( + 2, + (annotated: Annotated, key: symbol): Option.Option => + Object.prototype.hasOwnProperty.call(annotated.annotations, key) ? + Option.some(annotated.annotations[key] as any) : + Option.none() +) + +/** + * @category annotations + * @since 3.19.0 + * @experimental + */ +export const getTypeConstructorAnnotation = getAnnotation(TypeConstructorAnnotationId) + +/** + * @category annotations + * @since 3.10.0 + */ +export const getBrandAnnotation = getAnnotation(BrandAnnotationId) + +/** + * @category annotations + * @since 3.14.2 + */ +export const getSchemaIdAnnotation = getAnnotation(SchemaIdAnnotationId) + +/** + * @category annotations + * @since 3.10.0 + */ +export const getMessageAnnotation = getAnnotation(MessageAnnotationId) + +/** + * @category annotations + * @since 3.10.0 + */ +export const getMissingMessageAnnotation = getAnnotation(MissingMessageAnnotationId) + +/** + * @category annotations + * @since 3.10.0 + */ +export const getTitleAnnotation = getAnnotation(TitleAnnotationId) + +/** @internal */ +export const getAutoTitleAnnotation = getAnnotation(AutoTitleAnnotationId) + +/** + * @category annotations + * @since 3.10.0 + */ +export const getIdentifierAnnotation = getAnnotation(IdentifierAnnotationId) + +/** + * @category annotations + * @since 3.10.0 + */ +export const getDescriptionAnnotation = getAnnotation(DescriptionAnnotationId) + +/** + * @category annotations + * @since 3.10.0 + */ +export const getExamplesAnnotation = getAnnotation>(ExamplesAnnotationId) + +/** + * @category annotations + * @since 3.10.0 + */ +export const getDefaultAnnotation = getAnnotation>(DefaultAnnotationId) + +/** + * @category annotations + * @since 3.10.0 + */ +export const getJSONSchemaAnnotation = getAnnotation(JSONSchemaAnnotationId) + +/** + * @category annotations + * @since 3.10.0 + */ +export const getDocumentationAnnotation = getAnnotation(DocumentationAnnotationId) + +/** + * @category annotations + * @since 3.10.0 + */ +export const getConcurrencyAnnotation = getAnnotation(ConcurrencyAnnotationId) + +/** + * @category annotations + * @since 3.10.0 + */ +export const getBatchingAnnotation = getAnnotation(BatchingAnnotationId) + +/** + * @category annotations + * @since 3.10.0 + */ +export const getParseIssueTitleAnnotation = getAnnotation(ParseIssueTitleAnnotationId) + +/** + * @category annotations + * @since 3.10.0 + */ +export const getParseOptionsAnnotation = getAnnotation(ParseOptionsAnnotationId) + +/** + * @category annotations + * @since 3.10.0 + */ +export const getDecodingFallbackAnnotation = getAnnotation>( + DecodingFallbackAnnotationId +) + +/** + * @category annotations + * @since 3.10.0 + */ +export const getSurrogateAnnotation = getAnnotation(SurrogateAnnotationId) + +const getStableFilterAnnotation = getAnnotation(StableFilterAnnotationId) + +/** @internal */ +export const hasStableFilter = (annotated: Annotated) => + Option.exists(getStableFilterAnnotation(annotated), (b) => b === true) + +/** + * @category annotations + * @since 3.10.0 + */ +export const JSONIdentifierAnnotationId: unique symbol = Symbol.for("effect/annotation/JSONIdentifier") + +/** + * @category annotations + * @since 3.10.0 + */ +export const getJSONIdentifierAnnotation = getAnnotation(JSONIdentifierAnnotationId) + +/** + * @category annotations + * @since 3.10.0 + */ +export const getJSONIdentifier = (annotated: Annotated) => + Option.orElse(getJSONIdentifierAnnotation(annotated), () => getIdentifierAnnotation(annotated)) + +// ------------------------------------------------------------------------------------- +// schema ids +// ------------------------------------------------------------------------------------- + +/** + * @category schema id + * @since 3.10.0 + */ +export const ParseJsonSchemaId: unique symbol = Symbol.for("effect/schema/ParseJson") + +/** + * @category model + * @since 3.10.0 + */ +export class Declaration implements Annotated { + /** + * @since 3.10.0 + */ + readonly _tag = "Declaration" + constructor( + readonly typeParameters: ReadonlyArray, + readonly decodeUnknown: ( + ...typeParameters: ReadonlyArray + ) => (input: unknown, options: ParseOptions, self: Declaration) => Effect, + readonly encodeUnknown: ( + ...typeParameters: ReadonlyArray + ) => (input: unknown, options: ParseOptions, self: Declaration) => Effect, + readonly annotations: Annotations = {} + ) {} + /** + * @since 3.10.0 + */ + toString() { + return Option.getOrElse(getExpected(this), () => "") + } + /** + * @since 3.10.0 + */ + toJSON(): object { + return { + _tag: this._tag, + typeParameters: this.typeParameters.map((ast) => ast.toJSON()), + annotations: toJSONAnnotations(this.annotations) + } + } +} + +const createASTGuard = (tag: T) => (ast: AST): ast is Extract => + ast._tag === tag + +/** + * @category guards + * @since 3.10.0 + */ +export const isDeclaration: (ast: AST) => ast is Declaration = createASTGuard("Declaration") + +/** + * @category model + * @since 3.10.0 + */ +export type LiteralValue = string | number | boolean | null | bigint + +/** + * @category model + * @since 3.10.0 + */ +export class Literal implements Annotated { + /** + * @since 3.10.0 + */ + readonly _tag = "Literal" + constructor(readonly literal: LiteralValue, readonly annotations: Annotations = {}) {} + /** + * @since 3.10.0 + */ + toString() { + return Option.getOrElse(getExpected(this), () => Inspectable.formatUnknown(this.literal)) + } + /** + * @since 3.10.0 + */ + toJSON(): object { + return { + _tag: this._tag, + literal: Predicate.isBigInt(this.literal) ? String(this.literal) : this.literal, + annotations: toJSONAnnotations(this.annotations) + } + } +} + +/** + * @category guards + * @since 3.10.0 + */ +export const isLiteral: (ast: AST) => ast is Literal = createASTGuard("Literal") + +const $null = new Literal(null) + +export { + /** + * @category constructors + * @since 3.10.0 + */ + $null as null +} + +/** + * @category model + * @since 3.10.0 + */ +export class UniqueSymbol implements Annotated { + /** + * @since 3.10.0 + */ + readonly _tag = "UniqueSymbol" + constructor(readonly symbol: symbol, readonly annotations: Annotations = {}) {} + /** + * @since 3.10.0 + */ + toString() { + return Option.getOrElse(getExpected(this), () => Inspectable.formatUnknown(this.symbol)) + } + /** + * @since 3.10.0 + */ + toJSON(): object { + return { + _tag: this._tag, + symbol: String(this.symbol), + annotations: toJSONAnnotations(this.annotations) + } + } +} + +/** + * @category guards + * @since 3.10.0 + */ +export const isUniqueSymbol: (ast: AST) => ast is UniqueSymbol = createASTGuard("UniqueSymbol") + +/** + * @category model + * @since 3.10.0 + */ +export class UndefinedKeyword implements Annotated { + /** + * @since 3.10.0 + */ + readonly _tag = "UndefinedKeyword" + constructor(readonly annotations: Annotations = {}) {} + /** + * @since 3.10.0 + */ + toString() { + return formatKeyword(this) + } + /** + * @since 3.10.0 + */ + toJSON(): object { + return { + _tag: this._tag, + annotations: toJSONAnnotations(this.annotations) + } + } +} + +/** + * @category constructors + * @since 3.10.0 + */ +export const undefinedKeyword: UndefinedKeyword = new UndefinedKeyword({ + [TitleAnnotationId]: "undefined" +}) + +/** + * @category guards + * @since 3.10.0 + */ +export const isUndefinedKeyword: (ast: AST) => ast is UndefinedKeyword = createASTGuard("UndefinedKeyword") + +/** + * @category model + * @since 3.10.0 + */ +export class VoidKeyword implements Annotated { + /** + * @since 3.10.0 + */ + readonly _tag = "VoidKeyword" + constructor(readonly annotations: Annotations = {}) {} + /** + * @since 3.10.0 + */ + toString() { + return formatKeyword(this) + } + /** + * @since 3.10.0 + */ + toJSON(): object { + return { + _tag: this._tag, + annotations: toJSONAnnotations(this.annotations) + } + } +} + +/** + * @category constructors + * @since 3.10.0 + */ +export const voidKeyword: VoidKeyword = new VoidKeyword({ + [TitleAnnotationId]: "void" +}) + +/** + * @category guards + * @since 3.10.0 + */ +export const isVoidKeyword: (ast: AST) => ast is VoidKeyword = createASTGuard("VoidKeyword") + +/** + * @category model + * @since 3.10.0 + */ +export class NeverKeyword implements Annotated { + /** + * @since 3.10.0 + */ + readonly _tag = "NeverKeyword" + constructor(readonly annotations: Annotations = {}) {} + /** + * @since 3.10.0 + */ + toString() { + return formatKeyword(this) + } + /** + * @since 3.10.0 + */ + toJSON(): object { + return { + _tag: this._tag, + annotations: toJSONAnnotations(this.annotations) + } + } +} + +/** + * @category constructors + * @since 3.10.0 + */ +export const neverKeyword: NeverKeyword = new NeverKeyword({ + [TitleAnnotationId]: "never" +}) + +/** + * @category guards + * @since 3.10.0 + */ +export const isNeverKeyword: (ast: AST) => ast is NeverKeyword = createASTGuard("NeverKeyword") + +/** + * @category model + * @since 3.10.0 + */ +export class UnknownKeyword implements Annotated { + /** + * @since 3.10.0 + */ + readonly _tag = "UnknownKeyword" + constructor(readonly annotations: Annotations = {}) {} + /** + * @since 3.10.0 + */ + toString() { + return formatKeyword(this) + } + /** + * @since 3.10.0 + */ + toJSON(): object { + return { + _tag: this._tag, + annotations: toJSONAnnotations(this.annotations) + } + } +} + +/** + * @category constructors + * @since 3.10.0 + */ +export const unknownKeyword: UnknownKeyword = new UnknownKeyword({ + [TitleAnnotationId]: "unknown" +}) + +/** + * @category guards + * @since 3.10.0 + */ +export const isUnknownKeyword: (ast: AST) => ast is UnknownKeyword = createASTGuard("UnknownKeyword") + +/** + * @category model + * @since 3.10.0 + */ +export class AnyKeyword implements Annotated { + /** + * @since 3.10.0 + */ + readonly _tag = "AnyKeyword" + constructor(readonly annotations: Annotations = {}) {} + /** + * @since 3.10.0 + */ + toString() { + return formatKeyword(this) + } + /** + * @since 3.10.0 + */ + toJSON(): object { + return { + _tag: this._tag, + annotations: toJSONAnnotations(this.annotations) + } + } +} + +/** + * @category constructors + * @since 3.10.0 + */ +export const anyKeyword: AnyKeyword = new AnyKeyword({ + [TitleAnnotationId]: "any" +}) + +/** + * @category guards + * @since 3.10.0 + */ +export const isAnyKeyword: (ast: AST) => ast is AnyKeyword = createASTGuard("AnyKeyword") + +/** + * @category model + * @since 3.10.0 + */ +export class StringKeyword implements Annotated { + /** + * @since 3.10.0 + */ + readonly _tag = "StringKeyword" + constructor(readonly annotations: Annotations = {}) {} + /** + * @since 3.10.0 + */ + toString() { + return formatKeyword(this) + } + /** + * @since 3.10.0 + */ + toJSON(): object { + return { + _tag: this._tag, + annotations: toJSONAnnotations(this.annotations) + } + } +} + +/** + * @category constructors + * @since 3.10.0 + */ +export const stringKeyword: StringKeyword = new StringKeyword({ + [TitleAnnotationId]: "string", + [DescriptionAnnotationId]: "a string" +}) + +/** + * @category guards + * @since 3.10.0 + */ +export const isStringKeyword: (ast: AST) => ast is StringKeyword = createASTGuard("StringKeyword") + +/** + * @category model + * @since 3.10.0 + */ +export class NumberKeyword implements Annotated { + /** + * @since 3.10.0 + */ + readonly _tag = "NumberKeyword" + constructor(readonly annotations: Annotations = {}) {} + /** + * @since 3.10.0 + */ + toString() { + return formatKeyword(this) + } + /** + * @since 3.10.0 + */ + toJSON(): object { + return { + _tag: this._tag, + annotations: toJSONAnnotations(this.annotations) + } + } +} + +/** + * @category constructors + * @since 3.10.0 + */ +export const numberKeyword: NumberKeyword = new NumberKeyword({ + [TitleAnnotationId]: "number", + [DescriptionAnnotationId]: "a number" +}) + +/** + * @category guards + * @since 3.10.0 + */ +export const isNumberKeyword: (ast: AST) => ast is NumberKeyword = createASTGuard("NumberKeyword") + +/** + * @category model + * @since 3.10.0 + */ +export class BooleanKeyword implements Annotated { + /** + * @since 3.10.0 + */ + readonly _tag = "BooleanKeyword" + constructor(readonly annotations: Annotations = {}) {} + /** + * @since 3.10.0 + */ + toString() { + return formatKeyword(this) + } + /** + * @since 3.10.0 + */ + toJSON(): object { + return { + _tag: this._tag, + annotations: toJSONAnnotations(this.annotations) + } + } +} + +/** + * @category constructors + * @since 3.10.0 + */ +export const booleanKeyword: BooleanKeyword = new BooleanKeyword({ + [TitleAnnotationId]: "boolean", + [DescriptionAnnotationId]: "a boolean" +}) + +/** + * @category guards + * @since 3.10.0 + */ +export const isBooleanKeyword: (ast: AST) => ast is BooleanKeyword = createASTGuard("BooleanKeyword") + +/** + * @category model + * @since 3.10.0 + */ +export class BigIntKeyword implements Annotated { + /** + * @since 3.10.0 + */ + readonly _tag = "BigIntKeyword" + constructor(readonly annotations: Annotations = {}) {} + /** + * @since 3.10.0 + */ + toString() { + return formatKeyword(this) + } + /** + * @since 3.10.0 + */ + toJSON(): object { + return { + _tag: this._tag, + annotations: toJSONAnnotations(this.annotations) + } + } +} + +/** + * @category constructors + * @since 3.10.0 + */ +export const bigIntKeyword: BigIntKeyword = new BigIntKeyword({ + [TitleAnnotationId]: "bigint", + [DescriptionAnnotationId]: "a bigint" +}) + +/** + * @category guards + * @since 3.10.0 + */ +export const isBigIntKeyword: (ast: AST) => ast is BigIntKeyword = createASTGuard("BigIntKeyword") + +/** + * @category model + * @since 3.10.0 + */ +export class SymbolKeyword implements Annotated { + /** + * @since 3.10.0 + */ + readonly _tag = "SymbolKeyword" + constructor(readonly annotations: Annotations = {}) {} + /** + * @since 3.10.0 + */ + toString() { + return formatKeyword(this) + } + /** + * @since 3.10.0 + */ + toJSON(): object { + return { + _tag: this._tag, + annotations: toJSONAnnotations(this.annotations) + } + } +} + +/** + * @category constructors + * @since 3.10.0 + */ +export const symbolKeyword: SymbolKeyword = new SymbolKeyword({ + [TitleAnnotationId]: "symbol", + [DescriptionAnnotationId]: "a symbol" +}) + +/** + * @category guards + * @since 3.10.0 + */ +export const isSymbolKeyword: (ast: AST) => ast is SymbolKeyword = createASTGuard("SymbolKeyword") + +/** + * @category model + * @since 3.10.0 + */ +export class ObjectKeyword implements Annotated { + /** + * @since 3.10.0 + */ + readonly _tag = "ObjectKeyword" + constructor(readonly annotations: Annotations = {}) {} + /** + * @since 3.10.0 + */ + toString() { + return formatKeyword(this) + } + /** + * @since 3.10.0 + */ + toJSON(): object { + return { + _tag: this._tag, + annotations: toJSONAnnotations(this.annotations) + } + } +} + +/** + * @category constructors + * @since 3.10.0 + */ +export const objectKeyword: ObjectKeyword = new ObjectKeyword({ + [TitleAnnotationId]: "object", + [DescriptionAnnotationId]: "an object in the TypeScript meaning, i.e. the `object` type" +}) + +/** + * @category guards + * @since 3.10.0 + */ +export const isObjectKeyword: (ast: AST) => ast is ObjectKeyword = createASTGuard("ObjectKeyword") + +/** + * @category model + * @since 3.10.0 + */ +export class Enums implements Annotated { + /** + * @since 3.10.0 + */ + readonly _tag = "Enums" + constructor( + readonly enums: ReadonlyArray, + readonly annotations: Annotations = {} + ) {} + /** + * @since 3.10.0 + */ + toString() { + return Option.getOrElse( + getExpected(this), + () => ` JSON.stringify(value)).join(" | ")}>` + ) + } + /** + * @since 3.10.0 + */ + toJSON(): object { + return { + _tag: this._tag, + enums: this.enums, + annotations: toJSONAnnotations(this.annotations) + } + } +} + +/** + * @category guards + * @since 3.10.0 + */ +export const isEnums: (ast: AST) => ast is Enums = createASTGuard("Enums") + +type TemplateLiteralSpanBaseType = StringKeyword | NumberKeyword | Literal | TemplateLiteral + +type TemplateLiteralSpanType = TemplateLiteralSpanBaseType | Union + +const isTemplateLiteralSpanType = (ast: AST): ast is TemplateLiteralSpanType => { + switch (ast._tag) { + case "Literal": + case "NumberKeyword": + case "StringKeyword": + case "TemplateLiteral": + return true + case "Union": + return ast.types.every(isTemplateLiteralSpanType) + } + return false +} + +const templateLiteralSpanUnionTypeToString = (type: TemplateLiteralSpanType): string => { + switch (type._tag) { + case "Literal": + return JSON.stringify(String(type.literal)) + case "StringKeyword": + return "string" + case "NumberKeyword": + return "number" + case "TemplateLiteral": + return String(type) + case "Union": + return type.types.map(templateLiteralSpanUnionTypeToString).join(" | ") + } +} + +const templateLiteralSpanTypeToString = (type: TemplateLiteralSpanType): string => { + switch (type._tag) { + case "Literal": + return String(type.literal) + case "StringKeyword": + return "${string}" + case "NumberKeyword": + return "${number}" + case "TemplateLiteral": + return "${" + String(type) + "}" + case "Union": + return "${" + type.types.map(templateLiteralSpanUnionTypeToString).join(" | ") + "}" + } +} + +/** + * @category model + * @since 3.10.0 + */ +export class TemplateLiteralSpan { + /** + * @since 3.10.0 + */ + readonly type: TemplateLiteralSpanType + constructor(type: AST, readonly literal: string) { + if (isTemplateLiteralSpanType(type)) { + this.type = type + } else { + throw new Error(errors_.getSchemaUnsupportedLiteralSpanErrorMessage(type)) + } + } + /** + * @since 3.10.0 + */ + toString() { + return templateLiteralSpanTypeToString(this.type) + this.literal + } + /** + * @since 3.10.0 + */ + toJSON(): object { + return { + type: this.type.toJSON(), + literal: this.literal + } + } +} + +/** + * @category model + * @since 3.10.0 + */ +export class TemplateLiteral implements Annotated { + /** + * @since 3.10.0 + */ + readonly _tag = "TemplateLiteral" + constructor( + readonly head: string, + readonly spans: Arr.NonEmptyReadonlyArray, + readonly annotations: Annotations = {} + ) {} + /** + * @since 3.10.0 + */ + toString() { + return Option.getOrElse(getExpected(this), () => formatTemplateLiteral(this)) + } + /** + * @since 3.10.0 + */ + toJSON(): object { + return { + _tag: this._tag, + head: this.head, + spans: this.spans.map((span) => span.toJSON()), + annotations: toJSONAnnotations(this.annotations) + } + } +} + +const formatTemplateLiteral = (ast: TemplateLiteral): string => + "`" + ast.head + ast.spans.map(String).join("") + + "`" + +/** + * @category guards + * @since 3.10.0 + */ +export const isTemplateLiteral: (ast: AST) => ast is TemplateLiteral = createASTGuard("TemplateLiteral") + +/** + * @category model + * @since 3.10.0 + */ +export class Type implements Annotated { + constructor( + readonly type: AST, + readonly annotations: Annotations = {} + ) {} + /** + * @since 3.10.0 + */ + toJSON(): object { + return { + type: this.type.toJSON(), + annotations: toJSONAnnotations(this.annotations) + } + } + /** + * @since 3.10.0 + */ + toString() { + return String(this.type) + } +} + +/** + * @category model + * @since 3.10.0 + */ +export class OptionalType extends Type { + constructor( + type: AST, + readonly isOptional: boolean, + annotations: Annotations = {} + ) { + super(type, annotations) + } + /** + * @since 3.10.0 + */ + toJSON(): object { + return { + type: this.type.toJSON(), + isOptional: this.isOptional, + annotations: toJSONAnnotations(this.annotations) + } + } + /** + * @since 3.10.0 + */ + toString() { + return String(this.type) + (this.isOptional ? "?" : "") + } +} + +const getRestASTs = (rest: ReadonlyArray): ReadonlyArray => rest.map((annotatedAST) => annotatedAST.type) + +/** + * @category model + * @since 3.10.0 + */ +export class TupleType implements Annotated { + /** + * @since 3.10.0 + */ + readonly _tag = "TupleType" + constructor( + readonly elements: ReadonlyArray, + readonly rest: ReadonlyArray, + readonly isReadonly: boolean, + readonly annotations: Annotations = {} + ) { + let hasOptionalElement = false + let hasIllegalRequiredElement = false + for (const e of elements) { + if (e.isOptional) { + hasOptionalElement = true + } else if (hasOptionalElement) { + hasIllegalRequiredElement = true + break + } + } + if (hasIllegalRequiredElement || (hasOptionalElement && rest.length > 1)) { + throw new Error(errors_.getASTRequiredElementFollowinAnOptionalElementErrorMessage) + } + } + /** + * @since 3.10.0 + */ + toString() { + return Option.getOrElse(getExpected(this), () => formatTuple(this)) + } + /** + * @since 3.10.0 + */ + toJSON(): object { + return { + _tag: this._tag, + elements: this.elements.map((e) => e.toJSON()), + rest: this.rest.map((ast) => ast.toJSON()), + isReadonly: this.isReadonly, + annotations: toJSONAnnotations(this.annotations) + } + } +} + +const formatTuple = (ast: TupleType): string => { + const formattedElements = ast.elements.map(String) + .join(", ") + return Arr.matchLeft(ast.rest, { + onEmpty: () => `readonly [${formattedElements}]`, + onNonEmpty: (head, tail) => { + const formattedHead = String(head) + const wrappedHead = formattedHead.includes(" | ") ? `(${formattedHead})` : formattedHead + + if (tail.length > 0) { + const formattedTail = tail.map(String).join(", ") + if (ast.elements.length > 0) { + return `readonly [${formattedElements}, ...${wrappedHead}[], ${formattedTail}]` + } else { + return `readonly [...${wrappedHead}[], ${formattedTail}]` + } + } else { + if (ast.elements.length > 0) { + return `readonly [${formattedElements}, ...${wrappedHead}[]]` + } else { + return `ReadonlyArray<${formattedHead}>` + } + } + } + }) +} + +/** + * @category guards + * @since 3.10.0 + */ +export const isTupleType: (ast: AST) => ast is TupleType = createASTGuard("TupleType") + +/** + * @category model + * @since 3.10.0 + */ +export class PropertySignature extends OptionalType { + constructor( + readonly name: PropertyKey, + type: AST, + isOptional: boolean, + readonly isReadonly: boolean, + annotations?: Annotations + ) { + super(type, isOptional, annotations) + } + /** + * @since 3.10.0 + */ + toString(): string { + return (this.isReadonly ? "readonly " : "") + String(this.name) + (this.isOptional ? "?" : "") + ": " + + this.type + } + /** + * @since 3.10.0 + */ + toJSON(): object { + return { + name: String(this.name), + type: this.type.toJSON(), + isOptional: this.isOptional, + isReadonly: this.isReadonly, + annotations: toJSONAnnotations(this.annotations) + } + } +} + +/** + * @since 3.10.0 + */ +export type Parameter = StringKeyword | SymbolKeyword | TemplateLiteral | Refinement + +/** + * @since 3.10.0 + */ +export const isParameter = (ast: AST): ast is Parameter => { + switch (ast._tag) { + case "StringKeyword": + case "SymbolKeyword": + case "TemplateLiteral": + return true + case "Refinement": + return isParameter(ast.from) + } + return false +} + +/** + * @category model + * @since 3.10.0 + */ +export class IndexSignature { + /** + * @since 3.10.0 + */ + readonly parameter: Parameter + constructor( + parameter: AST, + readonly type: AST, + readonly isReadonly: boolean + ) { + if (isParameter(parameter)) { + this.parameter = parameter + } else { + throw new Error(errors_.getASTIndexSignatureParameterErrorMessage) + } + } + /** + * @since 3.10.0 + */ + toString(): string { + return (this.isReadonly ? "readonly " : "") + `[x: ${this.parameter}]: ${this.type}` + } + /** + * @since 3.10.0 + */ + toJSON(): object { + return { + parameter: this.parameter.toJSON(), + type: this.type.toJSON(), + isReadonly: this.isReadonly + } + } +} + +/** + * @category model + * @since 3.10.0 + */ +export class TypeLiteral implements Annotated { + /** + * @since 3.10.0 + */ + readonly _tag = "TypeLiteral" + /** + * @since 3.10.0 + */ + readonly propertySignatures: ReadonlyArray + /** + * @since 3.10.0 + */ + readonly indexSignatures: ReadonlyArray + constructor( + propertySignatures: ReadonlyArray, + indexSignatures: ReadonlyArray, + readonly annotations: Annotations = {} + ) { + // check for duplicate property signatures + const keys: Record = {} + for (let i = 0; i < propertySignatures.length; i++) { + const name = propertySignatures[i].name + if (Object.prototype.hasOwnProperty.call(keys, name)) { + throw new Error(errors_.getASTDuplicatePropertySignatureErrorMessage(name)) + } + keys[name] = null + } + // check for duplicate index signatures + const parameters = { + string: false, + symbol: false + } + for (let i = 0; i < indexSignatures.length; i++) { + const encodedParameter = getEncodedParameter(indexSignatures[i].parameter) + if (isStringKeyword(encodedParameter)) { + if (parameters.string) { + throw new Error(errors_.getASTDuplicateIndexSignatureErrorMessage("string")) + } + parameters.string = true + } else if (isSymbolKeyword(encodedParameter)) { + if (parameters.symbol) { + throw new Error(errors_.getASTDuplicateIndexSignatureErrorMessage("symbol")) + } + parameters.symbol = true + } + } + + this.propertySignatures = propertySignatures + this.indexSignatures = indexSignatures + } + /** + * @since 3.10.0 + */ + toString() { + return Option.getOrElse(getExpected(this), () => formatTypeLiteral(this)) + } + /** + * @since 3.10.0 + */ + toJSON(): object { + return { + _tag: this._tag, + propertySignatures: this.propertySignatures.map((ps) => ps.toJSON()), + indexSignatures: this.indexSignatures.map((ps) => ps.toJSON()), + annotations: toJSONAnnotations(this.annotations) + } + } +} + +const formatIndexSignatures = (iss: ReadonlyArray): string => iss.map(String).join("; ") + +const formatTypeLiteral = (ast: TypeLiteral): string => { + if (ast.propertySignatures.length > 0) { + const pss = ast.propertySignatures.map(String).join("; ") + if (ast.indexSignatures.length > 0) { + return `{ ${pss}; ${formatIndexSignatures(ast.indexSignatures)} }` + } else { + return `{ ${pss} }` + } + } else { + if (ast.indexSignatures.length > 0) { + return `{ ${formatIndexSignatures(ast.indexSignatures)} }` + } else { + return "{}" + } + } +} + +/** + * @category guards + * @since 3.10.0 + */ +export const isTypeLiteral: (ast: AST) => ast is TypeLiteral = createASTGuard("TypeLiteral") + +/** + * @since 3.10.0 + */ +export type Members = readonly [A, A, ...Array] + +const sortCandidates = Arr.sort( + Order.mapInput(Number.Order, (ast: AST) => { + switch (ast._tag) { + case "AnyKeyword": + return 0 + case "UnknownKeyword": + return 1 + case "ObjectKeyword": + return 2 + case "StringKeyword": + case "NumberKeyword": + case "BooleanKeyword": + case "BigIntKeyword": + case "SymbolKeyword": + return 3 + } + return 4 + }) +) + +const literalMap = { + string: "StringKeyword", + number: "NumberKeyword", + boolean: "BooleanKeyword", + bigint: "BigIntKeyword" +} as const + +/** @internal */ +export const flatten = (candidates: ReadonlyArray): Array => + Arr.flatMap(candidates, (ast) => isUnion(ast) ? flatten(ast.types) : [ast]) + +/** @internal */ +export const unify = (candidates: ReadonlyArray): Array => { + const cs = sortCandidates(candidates) + const out: Array = [] + const uniques: { [K in AST["_tag"] | "{}"]?: AST } = {} + const literals: Array = [] + for (const ast of cs) { + switch (ast._tag) { + case "NeverKeyword": + break + case "AnyKeyword": + return [anyKeyword] + case "UnknownKeyword": + return [unknownKeyword] + // uniques + case "ObjectKeyword": + case "UndefinedKeyword": + case "VoidKeyword": + case "StringKeyword": + case "NumberKeyword": + case "BooleanKeyword": + case "BigIntKeyword": + case "SymbolKeyword": { + if (!uniques[ast._tag]) { + uniques[ast._tag] = ast + out.push(ast) + } + break + } + case "Literal": { + const type = typeof ast.literal + switch (type) { + case "string": + case "number": + case "bigint": + case "boolean": { + const _tag = literalMap[type] + if (!uniques[_tag] && !literals.includes(ast.literal)) { + literals.push(ast.literal) + out.push(ast) + } + break + } + // null + case "object": { + if (!literals.includes(ast.literal)) { + literals.push(ast.literal) + out.push(ast) + } + break + } + } + break + } + case "UniqueSymbol": { + if (!uniques["SymbolKeyword"] && !literals.includes(ast.symbol)) { + literals.push(ast.symbol) + out.push(ast) + } + break + } + case "TupleType": { + if (!uniques["ObjectKeyword"]) { + out.push(ast) + } + break + } + case "TypeLiteral": { + if (ast.propertySignatures.length === 0 && ast.indexSignatures.length === 0) { + if (!uniques["{}"]) { + uniques["{}"] = ast + out.push(ast) + } + } else if (!uniques["ObjectKeyword"]) { + out.push(ast) + } + break + } + default: + out.push(ast) + } + } + return out +} + +/** + * @category model + * @since 3.10.0 + */ +export class Union implements Annotated { + static make = (types: ReadonlyArray, annotations?: Annotations): AST => { + return isMembers(types) ? new Union(types, annotations) : types.length === 1 ? types[0] : neverKeyword + } + /** @internal */ + static unify = (candidates: ReadonlyArray, annotations?: Annotations): AST => { + return Union.make(unify(flatten(candidates)), annotations) + } + /** + * @since 3.10.0 + */ + readonly _tag = "Union" + private constructor(readonly types: Members, readonly annotations: Annotations = {}) {} + /** + * @since 3.10.0 + */ + toString() { + return Option.getOrElse(getExpected(this), () => this.types.map(String).join(" | ")) + } + /** + * @since 3.10.0 + */ + toJSON(): object { + return { + _tag: this._tag, + types: this.types.map((ast) => ast.toJSON()), + annotations: toJSONAnnotations(this.annotations) + } + } +} + +/** @internal */ +export const mapMembers = (members: Members, f: (a: A) => B): Members => members.map(f) as any + +/** @internal */ +export const isMembers = (as: ReadonlyArray): as is Members => as.length > 1 + +/** + * @category guards + * @since 3.10.0 + */ +export const isUnion: (ast: AST) => ast is Union = createASTGuard("Union") + +const toJSONMemoMap = globalValue( + Symbol.for("effect/Schema/AST/toJSONMemoMap"), + () => new WeakMap() +) + +/** + * @category model + * @since 3.10.0 + */ +export class Suspend implements Annotated { + /** + * @since 3.10.0 + */ + readonly _tag = "Suspend" + constructor(readonly f: () => AST, readonly annotations: Annotations = {}) { + this.f = util_.memoizeThunk(f) + } + /** + * @since 3.10.0 + */ + toString() { + return getExpected(this).pipe( + Option.orElse(() => + Option.flatMap( + Option.liftThrowable(this.f)(), + (ast) => getExpected(ast) + ) + ), + Option.getOrElse(() => "") + ) + } + /** + * @since 3.10.0 + */ + toJSON(): object { + const ast = this.f() + let out = toJSONMemoMap.get(ast) + if (out) { + return out + } + toJSONMemoMap.set(ast, { _tag: this._tag }) + out = { + _tag: this._tag, + ast: ast.toJSON(), + annotations: toJSONAnnotations(this.annotations) + } + toJSONMemoMap.set(ast, out) + return out + } +} + +/** + * @category guards + * @since 3.10.0 + */ +export const isSuspend: (ast: AST) => ast is Suspend = createASTGuard("Suspend") + +/** + * @category model + * @since 3.10.0 + */ +export class Refinement implements Annotated { + /** + * @since 3.10.0 + */ + readonly _tag = "Refinement" + constructor( + readonly from: From, + readonly filter: ( + input: any, + options: ParseOptions, + self: Refinement + ) => Option.Option, + readonly annotations: Annotations = {} + ) {} + /** + * @since 3.10.0 + */ + toString() { + return getIdentifierAnnotation(this).pipe(Option.getOrElse(() => + Option.match(getOrElseExpected(this), { + onNone: () => `{ ${this.from} | filter }`, + onSome: (expected) => isRefinement(this.from) ? String(this.from) + " & " + expected : expected + }) + )) + } + /** + * @since 3.10.0 + */ + toJSON(): object { + return { + _tag: this._tag, + from: this.from.toJSON(), + annotations: toJSONAnnotations(this.annotations) + } + } +} + +/** + * @category guards + * @since 3.10.0 + */ +export const isRefinement: (ast: AST) => ast is Refinement = createASTGuard("Refinement") + +/** + * @category model + * @since 3.10.0 + */ +export interface ParseOptions { + /** + * The `errors` option allows you to receive all parsing errors when + * attempting to parse a value using a schema. By default only the first error + * is returned, but by setting the `errors` option to `"all"`, you can receive + * all errors that occurred during the parsing process. This can be useful for + * debugging or for providing more comprehensive error messages to the user. + * + * default: "first" + * + * @since 3.10.0 + */ + readonly errors?: "first" | "all" | undefined + /** + * When using a `Schema` to parse a value, by default any properties that are + * not specified in the `Schema` will be stripped out from the output. This is + * because the `Schema` is expecting a specific shape for the parsed value, + * and any excess properties do not conform to that shape. + * + * However, you can use the `onExcessProperty` option (default value: + * `"ignore"`) to trigger a parsing error. This can be particularly useful in + * cases where you need to detect and handle potential errors or unexpected + * values. + * + * If you want to allow excess properties to remain, you can use + * `onExcessProperty` set to `"preserve"`. + * + * default: "ignore" + * + * @since 3.10.0 + */ + readonly onExcessProperty?: "ignore" | "error" | "preserve" | undefined + /** + * The `propertyOrder` option provides control over the order of object fields + * in the output. This feature is particularly useful when the sequence of + * keys is important for the consuming processes or when maintaining the input + * order enhances readability and usability. + * + * By default, the `propertyOrder` option is set to `"none"`. This means that + * the internal system decides the order of keys to optimize parsing speed. + * The order of keys in this mode should not be considered stable, and it's + * recommended not to rely on key ordering as it may change in future updates + * without notice. + * + * Setting `propertyOrder` to `"original"` ensures that the keys are ordered + * as they appear in the input during the decoding/encoding process. + * + * default: "none" + * + * @since 3.10.0 + */ + readonly propertyOrder?: "none" | "original" | undefined + /** + * Handles missing properties in data structures. By default, missing + * properties are treated as if present with an `undefined` value. To treat + * missing properties as errors, set the `exact` option to `true`. This + * setting is already enabled by default for `is` and `asserts` functions, + * treating absent properties strictly unless overridden. + * + * default: false + * + * @since 3.10.0 + */ + readonly exact?: boolean | undefined +} + +/** + * @since 3.10.0 + */ +export const defaultParseOption: ParseOptions = {} + +/** + * @category model + * @since 3.10.0 + */ +export class Transformation implements Annotated { + /** + * @since 3.10.0 + */ + readonly _tag = "Transformation" + constructor( + readonly from: AST, + readonly to: AST, + readonly transformation: TransformationKind, + readonly annotations: Annotations = {} + ) {} + /** + * @since 3.10.0 + */ + toString() { + return Option.getOrElse( + getExpected(this), + () => `(${String(this.from)} <-> ${String(this.to)})` + ) + } + /** + * @since 3.10.0 + */ + toJSON(): object { + return { + _tag: this._tag, + from: this.from.toJSON(), + to: this.to.toJSON(), + annotations: toJSONAnnotations(this.annotations) + } + } +} + +/** + * @category guards + * @since 3.10.0 + */ +export const isTransformation: (ast: AST) => ast is Transformation = createASTGuard("Transformation") + +/** + * @category model + * @since 3.10.0 + */ +export type TransformationKind = + | FinalTransformation + | ComposeTransformation + | TypeLiteralTransformation + +/** + * @category model + * @since 3.10.0 + */ +export class FinalTransformation { + /** + * @since 3.10.0 + */ + readonly _tag = "FinalTransformation" + constructor( + readonly decode: ( + fromA: any, + options: ParseOptions, + self: Transformation, + fromI: any + ) => Effect, + readonly encode: (toI: any, options: ParseOptions, self: Transformation, toA: any) => Effect + ) {} +} + +const createTransformationGuard = + (tag: T) => + (ast: TransformationKind): ast is Extract => ast._tag === tag + +/** + * @category guards + * @since 3.10.0 + */ +export const isFinalTransformation: (ast: TransformationKind) => ast is FinalTransformation = createTransformationGuard( + "FinalTransformation" +) + +/** + * @category model + * @since 3.10.0 + */ +export class ComposeTransformation { + /** + * @since 3.10.0 + */ + readonly _tag = "ComposeTransformation" +} + +/** + * @category constructors + * @since 3.10.0 + */ +export const composeTransformation: ComposeTransformation = new ComposeTransformation() + +/** + * @category guards + * @since 3.10.0 + */ +export const isComposeTransformation: (ast: TransformationKind) => ast is ComposeTransformation = + createTransformationGuard( + "ComposeTransformation" + ) + +/** + * Represents a `PropertySignature -> PropertySignature` transformation + * + * The semantic of `decode` is: + * - `none()` represents the absence of the key/value pair + * - `some(value)` represents the presence of the key/value pair + * + * The semantic of `encode` is: + * - `none()` you don't want to output the key/value pair + * - `some(value)` you want to output the key/value pair + * + * @category model + * @since 3.10.0 + */ +export class PropertySignatureTransformation { + constructor( + readonly from: PropertyKey, + readonly to: PropertyKey, + readonly decode: (o: Option.Option) => Option.Option, + readonly encode: (o: Option.Option) => Option.Option + ) {} +} + +const isRenamingPropertySignatureTransformation = (t: PropertySignatureTransformation) => + t.decode === identity && t.encode === identity + +/** + * @category model + * @since 3.10.0 + */ +export class TypeLiteralTransformation { + /** + * @since 3.10.0 + */ + readonly _tag = "TypeLiteralTransformation" + constructor( + readonly propertySignatureTransformations: ReadonlyArray< + PropertySignatureTransformation + > + ) { + // check for duplicate property signature transformations + const fromKeys: Record = {} + const toKeys: Record = {} + for (const pst of propertySignatureTransformations) { + const from = pst.from + if (fromKeys[from]) { + throw new Error(errors_.getASTDuplicatePropertySignatureTransformationErrorMessage(from)) + } + fromKeys[from] = true + const to = pst.to + if (toKeys[to]) { + throw new Error(errors_.getASTDuplicatePropertySignatureTransformationErrorMessage(to)) + } + toKeys[to] = true + } + } +} + +/** + * @category guards + * @since 3.10.0 + */ +export const isTypeLiteralTransformation: (ast: TransformationKind) => ast is TypeLiteralTransformation = + createTransformationGuard("TypeLiteralTransformation") + +// ------------------------------------------------------------------------------------- +// API +// ------------------------------------------------------------------------------------- + +/** + * Merges a set of new annotations with existing ones, potentially overwriting + * any duplicates. + * + * Any previously existing identifier annotations are deleted. + * + * @since 3.10.0 + */ +export const annotations = (ast: AST, overrides: Annotations): AST => { + const d = Object.getOwnPropertyDescriptors(ast) + const base: any = { ...ast.annotations } + delete base[IdentifierAnnotationId] + const value = { ...base, ...overrides } + const surrogate = getSurrogateAnnotation(ast) + if (Option.isSome(surrogate)) { + value[SurrogateAnnotationId] = annotations(surrogate.value, overrides) + } + d.annotations.value = value + return Object.create(Object.getPrototypeOf(ast), d) +} + +/** + * Equivalent at runtime to the TypeScript type-level `keyof` operator. + * + * @since 3.10.0 + */ +export const keyof = (ast: AST): AST => Union.unify(_keyof(ast)) + +const STRING_KEYWORD_PATTERN = "[\\s\\S]*?" // any string, including newlines +const NUMBER_KEYWORD_PATTERN = "[+-]?\\d*\\.?\\d+(?:[Ee][+-]?\\d+)?" + +const getTemplateLiteralSpanTypePattern = (type: TemplateLiteralSpanType, capture: boolean): string => { + switch (type._tag) { + case "Literal": + return regexp.escape(String(type.literal)) + case "StringKeyword": + return STRING_KEYWORD_PATTERN + case "NumberKeyword": + return NUMBER_KEYWORD_PATTERN + case "TemplateLiteral": + return getTemplateLiteralPattern(type, capture, false) + case "Union": + return type.types.map((type) => getTemplateLiteralSpanTypePattern(type, capture)).join("|") + } +} + +const handleTemplateLiteralSpanTypeParens = ( + type: TemplateLiteralSpanType, + s: string, + capture: boolean, + top: boolean +) => { + if (isUnion(type)) { + if (capture && !top) { + return `(?:${s})` + } + } else if (!capture || !top) { + return s + } + return `(${s})` +} + +const getTemplateLiteralPattern = (ast: TemplateLiteral, capture: boolean, top: boolean): string => { + let pattern = `` + if (ast.head !== "") { + const head = regexp.escape(ast.head) + pattern += capture && top ? `(${head})` : head + } + + for (const span of ast.spans) { + const spanPattern = getTemplateLiteralSpanTypePattern(span.type, capture) + pattern += handleTemplateLiteralSpanTypeParens(span.type, spanPattern, capture, top) + if (span.literal !== "") { + const literal = regexp.escape(span.literal) + pattern += capture && top ? `(${literal})` : literal + } + } + + return pattern +} + +/** + * Generates a regular expression from a `TemplateLiteral` AST node. + * + * @see {@link getTemplateLiteralCapturingRegExp} for a variant that captures the pattern. + * + * @since 3.10.0 + */ +export const getTemplateLiteralRegExp = (ast: TemplateLiteral): RegExp => + new RegExp(`^${getTemplateLiteralPattern(ast, false, true)}$`) + +/** + * Generates a regular expression that captures the pattern defined by the given `TemplateLiteral` AST. + * + * @see {@link getTemplateLiteralRegExp} for a variant that does not capture the pattern. + * + * @since 3.10.0 + */ +export const getTemplateLiteralCapturingRegExp = (ast: TemplateLiteral): RegExp => + new RegExp(`^${getTemplateLiteralPattern(ast, true, true)}$`) + +/** + * @since 3.10.0 + */ +export const getPropertySignatures = (ast: AST): Array => { + const annotation = getSurrogateAnnotation(ast) + if (Option.isSome(annotation)) { + return getPropertySignatures(annotation.value) + } + switch (ast._tag) { + case "TypeLiteral": + return ast.propertySignatures.slice() + case "Suspend": + return getPropertySignatures(ast.f()) + case "Refinement": + return getPropertySignatures(ast.from) + } + return getPropertyKeys(ast).map((name) => getPropertyKeyIndexedAccess(ast, name)) +} + +const getIndexSignatures = (ast: AST): Array => { + const annotation = getSurrogateAnnotation(ast) + if (Option.isSome(annotation)) { + return getIndexSignatures(annotation.value) + } + switch (ast._tag) { + case "TypeLiteral": + return ast.indexSignatures.slice() + case "Suspend": + return getIndexSignatures(ast.f()) + case "Refinement": + return getIndexSignatures(ast.from) + case "Transformation": + return getIndexSignatures(ast.to) + } + return [] +} + +/** @internal */ +export const getNumberIndexedAccess = (ast: AST): AST => { + switch (ast._tag) { + case "TupleType": { + let hasOptional = false + let out: Array = [] + for (const e of ast.elements) { + if (e.isOptional) { + hasOptional = true + } + out.push(e.type) + } + if (hasOptional) { + out.push(undefinedKeyword) + } + out = out.concat(getRestASTs(ast.rest)) + return Union.make(out) + } + case "Refinement": + return getNumberIndexedAccess(ast.from) + case "Union": + return Union.make(ast.types.map(getNumberIndexedAccess)) + case "Suspend": + return getNumberIndexedAccess(ast.f()) + } + throw new Error(errors_.getASTUnsupportedSchemaErrorMessage(ast)) +} + +const getTypeLiteralPropertySignature = (ast: TypeLiteral, name: PropertyKey): PropertySignature | undefined => { + // from property signatures... + const ops = Arr.findFirst(ast.propertySignatures, (ps) => ps.name === name) + if (Option.isSome(ops)) { + return ops.value + } + + // from index signatures... + if (Predicate.isString(name)) { + let out: PropertySignature | undefined = undefined + for (const is of ast.indexSignatures) { + const encodedParameter = getEncodedParameter(is.parameter) + switch (encodedParameter._tag) { + case "TemplateLiteral": { + const regex = getTemplateLiteralRegExp(encodedParameter) + if (regex.test(name)) { + return new PropertySignature(name, is.type, false, true) + } + break + } + case "StringKeyword": { + if (out === undefined) { + out = new PropertySignature(name, is.type, false, true) + } + } + } + } + if (out) { + return out + } + } else if (Predicate.isSymbol(name)) { + for (const is of ast.indexSignatures) { + const encodedParameter = getEncodedParameter(is.parameter) + if (isSymbolKeyword(encodedParameter)) { + return new PropertySignature(name, is.type, false, true) + } + } + } +} + +/** @internal */ +export const getPropertyKeyIndexedAccess = (ast: AST, name: PropertyKey): PropertySignature => { + const annotation = getSurrogateAnnotation(ast) + if (Option.isSome(annotation)) { + return getPropertyKeyIndexedAccess(annotation.value, name) + } + switch (ast._tag) { + case "TypeLiteral": { + const ps = getTypeLiteralPropertySignature(ast, name) + if (ps) { + return ps + } + break + } + case "Union": + return new PropertySignature( + name, + Union.make(ast.types.map((ast) => getPropertyKeyIndexedAccess(ast, name).type)), + false, + true + ) + case "Suspend": + return getPropertyKeyIndexedAccess(ast.f(), name) + case "Refinement": + return getPropertyKeyIndexedAccess(ast.from, name) + case "Transformation": + return getPropertyKeyIndexedAccess(ast.to, name) + } + throw new Error(errors_.getASTUnsupportedSchemaErrorMessage(ast)) +} + +const getPropertyKeys = (ast: AST): Array => { + const annotation = getSurrogateAnnotation(ast) + if (Option.isSome(annotation)) { + return getPropertyKeys(annotation.value) + } + switch (ast._tag) { + case "TypeLiteral": + return ast.propertySignatures.map((ps) => ps.name) + case "Union": + return ast.types.slice(1).reduce( + (out: Array, ast) => Arr.intersection(out, getPropertyKeys(ast)), + getPropertyKeys(ast.types[0]) + ) + case "Suspend": + return getPropertyKeys(ast.f()) + case "Refinement": + return getPropertyKeys(ast.from) + case "Transformation": + return getPropertyKeys(ast.to) + } + return [] +} + +/** @internal */ +export const record = (key: AST, value: AST): { + propertySignatures: Array + indexSignatures: Array +} => { + const propertySignatures: Array = [] + const indexSignatures: Array = [] + const go = (key: AST): void => { + switch (key._tag) { + case "NeverKeyword": + break + case "StringKeyword": + case "SymbolKeyword": + case "TemplateLiteral": + case "Refinement": + indexSignatures.push(new IndexSignature(key, value, true)) + break + case "Literal": + if (Predicate.isString(key.literal) || Predicate.isNumber(key.literal)) { + propertySignatures.push(new PropertySignature(key.literal, value, false, true)) + } else { + throw new Error(errors_.getASTUnsupportedLiteralErrorMessage(key.literal)) + } + break + case "Enums": { + for (const [_, name] of key.enums) { + propertySignatures.push(new PropertySignature(name, value, false, true)) + } + break + } + case "UniqueSymbol": + propertySignatures.push(new PropertySignature(key.symbol, value, false, true)) + break + case "Union": + key.types.forEach(go) + break + default: + throw new Error(errors_.getASTUnsupportedKeySchemaErrorMessage(key)) + } + } + go(key) + return { propertySignatures, indexSignatures } +} + +/** + * Equivalent at runtime to the built-in TypeScript utility type `Pick`. + * + * @since 3.10.0 + */ +export const pick = (ast: AST, keys: ReadonlyArray): TypeLiteral | Transformation => { + const annotation = getSurrogateAnnotation(ast) + if (Option.isSome(annotation)) { + return pick(annotation.value, keys) + } + switch (ast._tag) { + case "TypeLiteral": { + const pss: Array = [] + const names: Record = {} + for (const ps of ast.propertySignatures) { + names[ps.name] = null + if (keys.includes(ps.name)) { + pss.push(ps) + } + } + for (const key of keys) { + if (!(key in names)) { + const ps = getTypeLiteralPropertySignature(ast, key) + if (ps) { + pss.push(ps) + } + } + } + return new TypeLiteral(pss, []) + } + case "Union": + return new TypeLiteral(keys.map((name) => getPropertyKeyIndexedAccess(ast, name)), []) + case "Suspend": + return pick(ast.f(), keys) + case "Refinement": + return pick(ast.from, keys) + case "Transformation": { + switch (ast.transformation._tag) { + case "ComposeTransformation": + return new Transformation( + pick(ast.from, keys), + pick(ast.to, keys), + composeTransformation + ) + case "TypeLiteralTransformation": { + const ts: Array = [] + const fromKeys: Array = [] + for (const k of keys) { + const t = ast.transformation.propertySignatureTransformations.find((t) => t.to === k) + if (t) { + ts.push(t) + fromKeys.push(t.from) + } else { + fromKeys.push(k) + } + } + return Arr.isNonEmptyReadonlyArray(ts) ? + new Transformation( + pick(ast.from, fromKeys), + pick(ast.to, keys), + new TypeLiteralTransformation(ts) + ) : + pick(ast.from, fromKeys) + } + } + } + } + throw new Error(errors_.getASTUnsupportedSchemaErrorMessage(ast)) +} + +/** + * Equivalent at runtime to the built-in TypeScript utility type `Omit`. + * + * @since 3.10.0 + */ +export const omit = (ast: AST, keys: ReadonlyArray): TypeLiteral | Transformation => { + let indexSignatures = getIndexSignatures(ast) + if (indexSignatures.length > 0) { + if (indexSignatures.some((is) => isStringKeyword(getEncodedParameter(is.parameter)))) { + indexSignatures = indexSignatures.filter((is) => !isTemplateLiteral(getEncodedParameter(is.parameter))) + } + return new TypeLiteral([], indexSignatures) + } + return pick(ast, getPropertyKeys(ast).filter((name) => !keys.includes(name))) +} + +/** @internal */ +export const orUndefined = (ast: AST): AST => Union.make([ast, undefinedKeyword]) + +/** + * Equivalent at runtime to the built-in TypeScript utility type `Partial`. + * + * @since 3.10.0 + */ +export const partial = (ast: AST, options?: { readonly exact: true }): AST => { + const exact = options?.exact === true + switch (ast._tag) { + case "TupleType": + return new TupleType( + ast.elements.map((e) => new OptionalType(exact ? e.type : orUndefined(e.type), true)), + Arr.match(ast.rest, { + onEmpty: () => ast.rest, + onNonEmpty: (rest) => [new Type(Union.make([...getRestASTs(rest), undefinedKeyword]))] + }), + ast.isReadonly + ) + case "TypeLiteral": + return new TypeLiteral( + ast.propertySignatures.map((ps) => + new PropertySignature(ps.name, exact ? ps.type : orUndefined(ps.type), true, ps.isReadonly, ps.annotations) + ), + ast.indexSignatures.map((is) => new IndexSignature(is.parameter, orUndefined(is.type), is.isReadonly)) + ) + case "Union": + return Union.make(ast.types.map((member) => partial(member, options))) + case "Suspend": + return new Suspend(() => partial(ast.f(), options)) + case "Declaration": + case "Refinement": + throw new Error(errors_.getASTUnsupportedSchemaErrorMessage(ast)) + case "Transformation": { + if ( + isTypeLiteralTransformation(ast.transformation) && + ast.transformation.propertySignatureTransformations.every(isRenamingPropertySignatureTransformation) + ) { + return new Transformation(partial(ast.from, options), partial(ast.to, options), ast.transformation) + } + throw new Error(errors_.getASTUnsupportedSchemaErrorMessage(ast)) + } + } + return ast +} + +/** + * Equivalent at runtime to the built-in TypeScript utility type `Required`. + * + * @since 3.10.0 + */ +export const required = (ast: AST): AST => { + switch (ast._tag) { + case "TupleType": + return new TupleType( + ast.elements.map((e) => new OptionalType(e.type, false)), + ast.rest, + ast.isReadonly + ) + case "TypeLiteral": + return new TypeLiteral( + ast.propertySignatures.map((f) => new PropertySignature(f.name, f.type, false, f.isReadonly, f.annotations)), + ast.indexSignatures + ) + case "Union": + return Union.make(ast.types.map((member) => required(member))) + case "Suspend": + return new Suspend(() => required(ast.f())) + case "Declaration": + case "Refinement": + throw new Error(errors_.getASTUnsupportedSchemaErrorMessage(ast)) + case "Transformation": { + if ( + isTypeLiteralTransformation(ast.transformation) && + ast.transformation.propertySignatureTransformations.every(isRenamingPropertySignatureTransformation) + ) { + return new Transformation(required(ast.from), required(ast.to), ast.transformation) + } + throw new Error(errors_.getASTUnsupportedSchemaErrorMessage(ast)) + } + } + return ast +} + +/** + * Creates a new AST with shallow mutability applied to its properties. + * + * @since 3.10.0 + */ +export const mutable = (ast: AST): AST => { + switch (ast._tag) { + case "TupleType": + return ast.isReadonly === false ? ast : new TupleType(ast.elements, ast.rest, false, ast.annotations) + case "TypeLiteral": { + const propertySignatures = changeMap( + ast.propertySignatures, + (ps) => + ps.isReadonly === false ? ps : new PropertySignature(ps.name, ps.type, ps.isOptional, false, ps.annotations) + ) + const indexSignatures = changeMap( + ast.indexSignatures, + (is) => is.isReadonly === false ? is : new IndexSignature(is.parameter, is.type, false) + ) + return propertySignatures === ast.propertySignatures && indexSignatures === ast.indexSignatures ? + ast : + new TypeLiteral(propertySignatures, indexSignatures, ast.annotations) + } + case "Union": { + const types = changeMap(ast.types, mutable) + return types === ast.types ? ast : Union.make(types, ast.annotations) + } + case "Suspend": + return new Suspend(() => mutable(ast.f()), ast.annotations) + case "Refinement": { + const from = mutable(ast.from) + return from === ast.from ? ast : new Refinement(from, ast.filter, ast.annotations) + } + case "Transformation": { + const from = mutable(ast.from) + const to = mutable(ast.to) + return from === ast.from && to === ast.to ? + ast : + new Transformation(from, to, ast.transformation, ast.annotations) + } + } + return ast +} + +// ------------------------------------------------------------------------------------- +// compiler harness +// ------------------------------------------------------------------------------------- + +/** + * @since 3.10.0 + */ +export type Compiler = (ast: AST, path: ReadonlyArray) => A + +/** + * @since 3.10.0 + */ +export type Match = { + [K in AST["_tag"]]: (ast: Extract, compile: Compiler, path: ReadonlyArray) => A +} + +/** + * @since 3.10.0 + */ +export const getCompiler = (match: Match): Compiler => { + const compile = (ast: AST, path: ReadonlyArray): A => match[ast._tag](ast as any, compile, path) + return compile +} + +/** @internal */ +export const pickAnnotations = + (annotationIds: ReadonlyArray) => (annotated: Annotated): Annotations | undefined => { + let out: { [_: symbol]: unknown } | undefined = undefined + for (const id of annotationIds) { + if (Object.prototype.hasOwnProperty.call(annotated.annotations, id)) { + if (out === undefined) { + out = {} + } + out[id] = annotated.annotations[id] + } + } + return out + } + +/** @internal */ +export const omitAnnotations = + (annotationIds: ReadonlyArray) => (annotated: Annotated): Annotations | undefined => { + const out = { ...annotated.annotations } + for (const id of annotationIds) { + delete out[id] + } + return out + } + +const preserveTransformationAnnotations = pickAnnotations([ + ExamplesAnnotationId, + DefaultAnnotationId, + JSONSchemaAnnotationId, + ArbitraryAnnotationId, + PrettyAnnotationId, + EquivalenceAnnotationId +]) + +/** + * @since 3.10.0 + */ +export const typeAST = (ast: AST): AST => { + switch (ast._tag) { + case "Declaration": { + const typeParameters = changeMap(ast.typeParameters, typeAST) + return typeParameters === ast.typeParameters ? + ast : + new Declaration(typeParameters, ast.decodeUnknown, ast.encodeUnknown, ast.annotations) + } + case "TupleType": { + const elements = changeMap(ast.elements, (e) => { + const type = typeAST(e.type) + return type === e.type ? e : new OptionalType(type, e.isOptional) + }) + const restASTs = getRestASTs(ast.rest) + const rest = changeMap(restASTs, typeAST) + return elements === ast.elements && rest === restASTs ? + ast : + new TupleType(elements, rest.map((type) => new Type(type)), ast.isReadonly, ast.annotations) + } + case "TypeLiteral": { + const propertySignatures = changeMap(ast.propertySignatures, (p) => { + const type = typeAST(p.type) + return type === p.type ? p : new PropertySignature(p.name, type, p.isOptional, p.isReadonly) + }) + const indexSignatures = changeMap(ast.indexSignatures, (is) => { + const type = typeAST(is.type) + return type === is.type ? is : new IndexSignature(is.parameter, type, is.isReadonly) + }) + return propertySignatures === ast.propertySignatures && indexSignatures === ast.indexSignatures ? + ast : + new TypeLiteral(propertySignatures, indexSignatures, ast.annotations) + } + case "Union": { + const types = changeMap(ast.types, typeAST) + return types === ast.types ? ast : Union.make(types, ast.annotations) + } + case "Suspend": + return new Suspend(() => typeAST(ast.f()), ast.annotations) + case "Refinement": { + const from = typeAST(ast.from) + return from === ast.from ? + ast : + new Refinement(from, ast.filter, ast.annotations) + } + case "Transformation": { + const preserve = preserveTransformationAnnotations(ast) + return typeAST( + preserve !== undefined ? + annotations(ast.to, preserve) : + ast.to + ) + } + } + return ast +} + +function changeMap( + as: Arr.NonEmptyReadonlyArray, + f: (a: A) => A +): Arr.NonEmptyReadonlyArray +function changeMap(as: ReadonlyArray, f: (a: A) => A): ReadonlyArray +function changeMap(as: ReadonlyArray, f: (a: A) => A): ReadonlyArray { + let changed = false + const out = Arr.allocate(as.length) as Array + for (let i = 0; i < as.length; i++) { + const a = as[i] + const fa = f(a) + if (fa !== a) { + changed = true + } + out[i] = fa + } + return changed ? out : as +} + +/** + * Returns the from part of a transformation if it exists + * + * @internal + */ +export const getTransformationFrom = (ast: AST): AST | undefined => { + switch (ast._tag) { + case "Transformation": + return ast.from + case "Refinement": + return getTransformationFrom(ast.from) + case "Suspend": + return getTransformationFrom(ast.f()) + } +} + +const encodedAST_ = (ast: AST, isBound: boolean): AST => { + switch (ast._tag) { + case "Declaration": { + const typeParameters = changeMap(ast.typeParameters, (ast) => encodedAST_(ast, isBound)) + return typeParameters === ast.typeParameters ? + ast : + new Declaration(typeParameters, ast.decodeUnknown, ast.encodeUnknown) + } + case "TupleType": { + const elements = changeMap(ast.elements, (e) => { + const type = encodedAST_(e.type, isBound) + return type === e.type ? e : new OptionalType(type, e.isOptional) + }) + const restASTs = getRestASTs(ast.rest) + const rest = changeMap(restASTs, (ast) => encodedAST_(ast, isBound)) + return elements === ast.elements && rest === restASTs ? + ast : + new TupleType(elements, rest.map((ast) => new Type(ast)), ast.isReadonly) + } + case "TypeLiteral": { + const propertySignatures = changeMap(ast.propertySignatures, (ps) => { + const type = encodedAST_(ps.type, isBound) + return type === ps.type + ? ps + : new PropertySignature(ps.name, type, ps.isOptional, ps.isReadonly) + }) + const indexSignatures = changeMap(ast.indexSignatures, (is) => { + const type = encodedAST_(is.type, isBound) + return type === is.type ? is : new IndexSignature(is.parameter, type, is.isReadonly) + }) + return propertySignatures === ast.propertySignatures && indexSignatures === ast.indexSignatures ? + ast : + new TypeLiteral(propertySignatures, indexSignatures) + } + case "Union": { + const types = changeMap(ast.types, (ast) => encodedAST_(ast, isBound)) + return types === ast.types ? ast : Union.make(types) + } + case "Suspend": { + let borrowedAnnotations = undefined + const identifier = getJSONIdentifier(ast) + if (Option.isSome(identifier)) { + const suffix = isBound ? "Bound" : "" + borrowedAnnotations = { [JSONIdentifierAnnotationId]: `${identifier.value}Encoded${suffix}` } + } + return new Suspend(() => encodedAST_(ast.f(), isBound), borrowedAnnotations) + } + case "Refinement": { + const from = encodedAST_(ast.from, isBound) + if (isBound) { + if (from === ast.from) return ast + if (getTransformationFrom(ast.from) === undefined && hasStableFilter(ast)) { + return new Refinement(from, ast.filter, ast.annotations) + } + return from + } else { + return from + } + } + case "Transformation": + return encodedAST_(ast.from, isBound) + } + return ast +} + +/** + * @since 3.10.0 + */ +export const encodedAST = (ast: AST): AST => encodedAST_(ast, false) + +/** + * @since 3.10.0 + */ +export const encodedBoundAST = (ast: AST): AST => encodedAST_(ast, true) + +const toJSONAnnotations = (annotations: Annotations): object => { + const out: Record = {} + for (const k of Object.getOwnPropertySymbols(annotations)) { + out[String(k)] = annotations[k] + } + return out +} + +/** @internal */ +export const getEncodedParameter = ( + ast: Parameter +): StringKeyword | SymbolKeyword | TemplateLiteral => { + switch (ast._tag) { + case "StringKeyword": + case "SymbolKeyword": + case "TemplateLiteral": + return ast + case "Refinement": + return getEncodedParameter(ast.from) + } +} + +/** @internal */ +export const equals = (self: AST, that: AST): boolean => { + switch (self._tag) { + case "Literal": + return isLiteral(that) && that.literal === self.literal + case "UniqueSymbol": + return isUniqueSymbol(that) && that.symbol === self.symbol + case "UndefinedKeyword": + case "VoidKeyword": + case "NeverKeyword": + case "UnknownKeyword": + case "AnyKeyword": + case "StringKeyword": + case "NumberKeyword": + case "BooleanKeyword": + case "BigIntKeyword": + case "SymbolKeyword": + case "ObjectKeyword": + return that._tag === self._tag + case "TemplateLiteral": + return isTemplateLiteral(that) && that.head === self.head && equalsTemplateLiteralSpan(that.spans, self.spans) + case "Enums": + return isEnums(that) && equalsEnums(that.enums, self.enums) + case "Union": + return isUnion(that) && equalsUnion(self.types, that.types) + case "Refinement": + case "TupleType": + case "TypeLiteral": + case "Suspend": + case "Transformation": + case "Declaration": + return self === that + } +} + +const equalsTemplateLiteralSpan = Arr.getEquivalence((self, that): boolean => { + return self.literal === that.literal && equals(self.type, that.type) +}) + +const equalsEnums = Arr.getEquivalence((self, that) => + that[0] === self[0] && that[1] === self[1] +) + +const equalsUnion = Arr.getEquivalence(equals) + +const intersection = Arr.intersectionWith(equals) + +const _keyof = (ast: AST): Array => { + switch (ast._tag) { + case "Declaration": { + const annotation = getSurrogateAnnotation(ast) + if (Option.isSome(annotation)) { + return _keyof(annotation.value) + } + break + } + case "TypeLiteral": + return ast.propertySignatures.map((p): AST => + Predicate.isSymbol(p.name) ? new UniqueSymbol(p.name) : new Literal(p.name) + ).concat(ast.indexSignatures.map((is) => getEncodedParameter(is.parameter))) + case "Suspend": + return _keyof(ast.f()) + case "Union": + return ast.types.slice(1).reduce( + (out: Array, ast) => intersection(out, _keyof(ast)), + _keyof(ast.types[0]) + ) + case "Transformation": + return _keyof(ast.to) + } + throw new Error(errors_.getASTUnsupportedSchemaErrorMessage(ast)) +} + +/** @internal */ +export const compose = (ab: AST, cd: AST): AST => new Transformation(ab, cd, composeTransformation) + +/** @internal */ +export const rename = (ast: AST, mapping: { readonly [K in PropertyKey]?: PropertyKey }): AST => { + switch (ast._tag) { + case "TypeLiteral": { + const propertySignatureTransformations: Array = [] + for (const key of Reflect.ownKeys(mapping)) { + const name = mapping[key] + if (name !== undefined) { + propertySignatureTransformations.push( + new PropertySignatureTransformation( + key, + name, + identity, + identity + ) + ) + } + } + if (propertySignatureTransformations.length === 0) { + return ast + } + return new Transformation( + ast, + new TypeLiteral( + ast.propertySignatures.map((ps) => { + const name = mapping[ps.name] + return new PropertySignature( + name === undefined ? ps.name : name, + typeAST(ps.type), + ps.isOptional, + ps.isReadonly, + ps.annotations + ) + }), + ast.indexSignatures + ), + new TypeLiteralTransformation(propertySignatureTransformations) + ) + } + case "Union": + return Union.make(ast.types.map((ast) => rename(ast, mapping))) + case "Suspend": + return new Suspend(() => rename(ast.f(), mapping)) + case "Transformation": + return compose(ast, rename(typeAST(ast), mapping)) + } + throw new Error(errors_.getASTUnsupportedRenameSchemaErrorMessage(ast)) +} + +const formatKeyword = (ast: AST): string => Option.getOrElse(getExpected(ast), () => ast._tag) + +function getBrands(ast: Annotated): string { + return Option.match(getBrandAnnotation(ast), { + onNone: () => "", + onSome: (brands) => brands.map((brand) => ` & Brand<${Inspectable.formatUnknown(brand)}>`).join("") + }) +} + +const getOrElseExpected = (ast: Annotated): Option.Option => + getTitleAnnotation(ast).pipe( + Option.orElse(() => getDescriptionAnnotation(ast)), + Option.orElse(() => getAutoTitleAnnotation(ast)), + Option.map((s) => s + getBrands(ast)) + ) + +const getExpected = (ast: Annotated): Option.Option => + Option.orElse(getIdentifierAnnotation(ast), () => getOrElseExpected(ast)) + +/** @internal */ +export const pruneUndefined = ( + ast: AST, + self: (ast: AST) => AST | undefined, + onTransformation: (ast: Transformation) => AST | undefined +): AST | undefined => { + switch (ast._tag) { + case "UndefinedKeyword": + return neverKeyword + case "Union": { + const types: Array = [] + let hasUndefined = false + for (const type of ast.types) { + const pruned = self(type) + if (pruned) { + hasUndefined = true + if (!isNeverKeyword(pruned)) { + types.push(pruned) + } + } else { + types.push(type) + } + } + if (hasUndefined) { + return Union.make(types) + } + break + } + case "Suspend": + return self(ast.f()) + case "Transformation": + return onTransformation(ast) + } +} diff --git a/repos/effect/packages/effect/src/Scope.ts b/repos/effect/packages/effect/src/Scope.ts new file mode 100644 index 0000000..de0bd15 --- /dev/null +++ b/repos/effect/packages/effect/src/Scope.ts @@ -0,0 +1,204 @@ +/** + * @since 2.0.0 + */ + +import type * as Context from "./Context.js" +import type * as Effect from "./Effect.js" +import type * as ExecutionStrategy from "./ExecutionStrategy.js" +import type * as Exit from "./Exit.js" +import * as core from "./internal/core.js" +import * as fiberRuntime from "./internal/fiberRuntime.js" +import type { Pipeable } from "./Pipeable.js" + +/** + * A unique identifier for the `Scope` type. + * + * @since 2.0.0 + * @category symbols + */ +export const ScopeTypeId: unique symbol = core.ScopeTypeId + +/** + * The type of the unique identifier for `Scope`. + * + * @since 2.0.0 + * @category symbols + */ +export type ScopeTypeId = typeof ScopeTypeId + +/** + * A unique identifier for the `CloseableScope` type. + * + * @since 2.0.0 + * @category symbols + */ +export const CloseableScopeTypeId: unique symbol = core.CloseableScopeTypeId + +/** + * The type of the unique identifier for `CloseableScope`. + * + * @since 2.0.0 + * @category symbols + */ +export type CloseableScopeTypeId = typeof CloseableScopeTypeId + +/** + * Represents a scope that manages finalizers and can fork child scopes. + * + * @since 2.0.0 + * @category models + */ +export interface Scope extends Pipeable { + readonly [ScopeTypeId]: ScopeTypeId + /** + * The execution strategy for running finalizers in this scope. + */ + readonly strategy: ExecutionStrategy.ExecutionStrategy + /** + * Forks a new child scope with the specified execution strategy. The child scope + * will automatically be closed when this scope is closed. + * + * @internal + */ + fork(strategy: ExecutionStrategy.ExecutionStrategy): Effect.Effect + /** + * Adds a finalizer to this scope. The finalizer will be run when the scope is closed. + * + * @internal + */ + addFinalizer(finalizer: Scope.Finalizer): Effect.Effect +} + +/** + * A scope that can be explicitly closed with a specified exit value. + * + * @since 2.0.0 + * @category models + */ +export interface CloseableScope extends Scope, Pipeable { + readonly [CloseableScopeTypeId]: CloseableScopeTypeId + + /** + * Closes this scope with the given exit value, running all finalizers. + * + * @internal + */ + close(exit: Exit.Exit): Effect.Effect +} + +/** + * A tag representing the current `Scope` in the environment. + * + * @since 2.0.0 + * @category context + */ +export const Scope: Context.Tag = fiberRuntime.scopeTag + +/** + * @since 2.0.0 + */ +export declare namespace Scope { + /** + * A finalizer function that takes an `Exit` value and returns an `Effect`. + * + * @since 2.0.0 + * @category model + */ + export type Finalizer = (exit: Exit.Exit) => Effect.Effect + /** + * A closeable scope that can be explicitly closed. + * + * @since 2.0.0 + * @category model + */ + export type Closeable = CloseableScope +} + +/** + * Adds a finalizer to this scope. The finalizer is guaranteed to be run when + * the scope is closed. Use this when the finalizer does not need to know the + * `Exit` value that the scope is closed with. + * + * @see {@link addFinalizerExit} + * + * @since 2.0.0 + * @category utils + */ +export const addFinalizer: ( + self: Scope, + finalizer: Effect.Effect +) => Effect.Effect = core.scopeAddFinalizer + +/** + * Adds a finalizer to this scope. The finalizer receives the `Exit` value + * when the scope is closed, allowing it to perform different actions based + * on the exit status. + * + * @see {@link addFinalizer} + * + * @since 2.0.0 + * @category utils + */ +export const addFinalizerExit: (self: Scope, finalizer: Scope.Finalizer) => Effect.Effect = + core.scopeAddFinalizerExit + +/** + * Closes this scope with the specified exit value, running all finalizers that + * have been added to the scope. + * + * @since 2.0.0 + * @category destructors + */ +export const close: (self: CloseableScope, exit: Exit.Exit) => Effect.Effect = core.scopeClose + +/** + * Extends the scope of an `Effect` that requires a scope into this scope. + * It provides this scope to the effect but does not close the scope when the + * effect completes execution. This allows extending a scoped value into a + * larger scope. + * + * @since 2.0.0 + * @category utils + */ +export const extend: { + (scope: Scope): (effect: Effect.Effect) => Effect.Effect> + (effect: Effect.Effect, scope: Scope): Effect.Effect> +} = fiberRuntime.scopeExtend + +/** + * Forks a new child scope with the specified execution strategy. The child scope + * will automatically be closed when this scope is closed. + * + * @since 2.0.0 + * @category utils + */ +export const fork: ( + self: Scope, + strategy: ExecutionStrategy.ExecutionStrategy +) => Effect.Effect = core.scopeFork + +/** + * Provides this closeable scope to an `Effect` that requires a scope, + * guaranteeing that the scope is closed with the result of that effect as + * soon as the effect completes execution, whether by success, failure, or + * interruption. + * + * @since 2.0.0 + * @category destructors + */ +export const use: { + (scope: CloseableScope): (effect: Effect.Effect) => Effect.Effect> + (effect: Effect.Effect, scope: CloseableScope): Effect.Effect> +} = fiberRuntime.scopeUse + +/** + * Creates a new closeable scope where finalizers will run according to the + * specified `ExecutionStrategy`. If no execution strategy is provided, `sequential` + * will be used by default. + * + * @since 2.0.0 + * @category constructors + */ +export const make: ( + executionStrategy?: ExecutionStrategy.ExecutionStrategy +) => Effect.Effect = fiberRuntime.scopeMake diff --git a/repos/effect/packages/effect/src/ScopedCache.ts b/repos/effect/packages/effect/src/ScopedCache.ts new file mode 100644 index 0000000..19cf996 --- /dev/null +++ b/repos/effect/packages/effect/src/ScopedCache.ts @@ -0,0 +1,151 @@ +/** + * @since 2.0.0 + */ +import type * as Cache from "./Cache.js" +import type * as Duration from "./Duration.js" +import type * as Effect from "./Effect.js" +import type * as Exit from "./Exit.js" +import * as internal from "./internal/scopedCache.js" +import type * as Option from "./Option.js" +import type { Pipeable } from "./Pipeable.js" +import type * as Scope from "./Scope.js" +import type * as Types from "./Types.js" + +/** + * @since 2.0.0 + * @category symbols + */ +export const ScopedCacheTypeId: unique symbol = internal.ScopedCacheTypeId + +/** + * @since 2.0.0 + * @category symbols + */ +export type ScopedCacheTypeId = typeof ScopedCacheTypeId + +/** + * @since 2.0.0 + * @category models + */ +export interface ScopedCache + extends ScopedCache.Variance, Pipeable +{ + /** + * Retrieves the value associated with the specified key if it exists. + * Otherwise returns `Option.none`. + */ + getOption(key: Key): Effect.Effect, Error, Scope.Scope> + + /** + * Retrieves the value associated with the specified key if it exists and the + * lookup function has completed. Otherwise returns `Option.none`. + */ + getOptionComplete(key: Key): Effect.Effect, never, Scope.Scope> + + /** + * Returns statistics for this cache. + */ + readonly cacheStats: Effect.Effect + + /** + * Return whether a resource associated with the specified key exists in the + * cache. Sometime `contains` can return true if the resource is currently + * being created but not yet totally created. + */ + contains(key: Key): Effect.Effect + + /** + * Return statistics for the specified entry. + */ + entryStats(key: Key): Effect.Effect> + + /** + * Gets the value from the cache if it exists or otherwise computes it, the + * release action signals to the cache that the value is no longer being used + * and can potentially be finalized subject to the policies of the cache. + */ + get(key: Key): Effect.Effect + + /** + * Invalidates the resource associated with the specified key. + */ + invalidate(key: Key): Effect.Effect + + /** + * Invalidates all values in the cache. + */ + readonly invalidateAll: Effect.Effect + + /** + * Force the reuse of the lookup function to compute the returned scoped + * effect associated with the specified key immediately. Once the new resource + * is recomputed, the old resource associated to the key is cleaned (once all + * fiber using it are done with it). During the time the new resource is + * computed, concurrent call the .get will use the old resource if this one is + * not expired. + */ + refresh(key: Key): Effect.Effect + + /** + * Returns the approximate number of values in the cache. + */ + readonly size: Effect.Effect +} + +/** + * @since 2.0.0 + */ +export declare namespace ScopedCache { + /** + * @since 2.0.0 + * @category models + */ + export interface Variance { + readonly [ScopedCacheTypeId]: { + _Key: Types.Contravariant + _Error: Types.Covariant + _Value: Types.Covariant + } + } +} + +/** + * Constructs a new cache with the specified capacity, time to live, and + * lookup function. + * + * @since 2.0.0 + * @category constructors + */ +export const make: ( + options: { + readonly lookup: Lookup + readonly capacity: number + readonly timeToLive: Duration.DurationInput + } +) => Effect.Effect, never, Scope.Scope | Environment> = internal.make + +/** + * Constructs a new cache with the specified capacity, time to live, and + * lookup function, where the time to live can depend on the `Exit` value + * returned by the lookup function. + * + * @since 2.0.0 + * @category constructors + */ +export const makeWith: ( + options: { + readonly capacity: number + readonly lookup: Lookup + readonly timeToLive: (exit: Exit.Exit) => Duration.DurationInput + } +) => Effect.Effect, never, Scope.Scope | Environment> = internal.makeWith + +/** + * Similar to `Cache.Lookup`, but executes the lookup function within a `Scope`. + * + * @since 2.0.0 + * @category models + */ +export type Lookup = ( + key: Key +) => Effect.Effect diff --git a/repos/effect/packages/effect/src/ScopedRef.ts b/repos/effect/packages/effect/src/ScopedRef.ts new file mode 100644 index 0000000..8d44654 --- /dev/null +++ b/repos/effect/packages/effect/src/ScopedRef.ts @@ -0,0 +1,117 @@ +/** + * @since 2.0.0 + */ +import type * as Effect from "./Effect.js" +import type { LazyArg } from "./Function.js" +import * as internal from "./internal/scopedRef.js" +import type { Pipeable } from "./Pipeable.js" +import type * as Scope from "./Scope.js" +import type * as Synchronized from "./SynchronizedRef.js" +import type * as Types from "./Types.js" +import type * as Unify from "./Unify.js" + +/** + * @since 2.0.0 + * @category symbols + */ +export const ScopedRefTypeId: unique symbol = internal.ScopedRefTypeId + +/** + * @since 2.0.0 + * @category symbols + */ +export type ScopedRefTypeId = typeof ScopedRefTypeId + +/** + * A `ScopedRef` is a reference whose value is associated with resources, + * which must be released properly. You can both get the current value of any + * `ScopedRef`, as well as set it to a new value (which may require new + * resources). The reference itself takes care of properly releasing resources + * for the old value whenever a new value is obtained. + * + * @since 2.0.0 + * @category models + */ +export interface ScopedRef extends Effect.Effect, ScopedRef.Variance, Pipeable { + /** @internal */ + readonly ref: Synchronized.SynchronizedRef + + readonly [Unify.typeSymbol]?: unknown + readonly [Unify.unifySymbol]?: ScopedRefUnify + readonly [Unify.ignoreSymbol]?: ScopedRefUnifyIgnore +} + +/** + * @category models + * @since 3.9.0 + */ +export interface ScopedRefUnify extends Effect.EffectUnify { + ScopedRef?: () => Extract> +} + +/** + * @category models + * @since 3.9.0 + */ +export interface ScopedRefUnifyIgnore extends Effect.EffectUnifyIgnore { + Effect?: true +} + +/** + * @since 2.0.0 + */ +export declare namespace ScopedRef { + /** + * @since 2.0.0 + * @category models + */ + export interface Variance { + readonly [ScopedRefTypeId]: { + readonly _A: Types.Invariant + } + } +} + +/** + * Creates a new `ScopedRef` from an effect that resourcefully produces a + * value. + * + * @since 2.0.0 + * @category constructors + */ +export const fromAcquire: ( + acquire: Effect.Effect +) => Effect.Effect, E, Scope.Scope | R> = internal.fromAcquire + +/** + * Retrieves the current value of the scoped reference. + * + * @since 2.0.0 + * @category getters + */ +export const get: (self: ScopedRef) => Effect.Effect = internal.get + +/** + * Creates a new `ScopedRef` from the specified value. This method should + * not be used for values whose creation require the acquisition of resources. + * + * @since 2.0.0 + * @category constructors + */ +export const make: (evaluate: LazyArg) => Effect.Effect, never, Scope.Scope> = internal.make + +/** + * Sets the value of this reference to the specified resourcefully-created + * value. Any resources associated with the old value will be released. + * + * This method will not return until either the reference is successfully + * changed to the new value, with old resources released, or until the attempt + * to acquire a new value fails. + * + * @since 2.0.0 + * @category getters + */ +export const set: { + (acquire: Effect.Effect): (self: ScopedRef) => Effect.Effect> + (self: ScopedRef, acquire: Effect.Effect): Effect.Effect> +} = internal.set diff --git a/repos/effect/packages/effect/src/Secret.ts b/repos/effect/packages/effect/src/Secret.ts new file mode 100644 index 0000000..c19270f --- /dev/null +++ b/repos/effect/packages/effect/src/Secret.ts @@ -0,0 +1,88 @@ +/** + * @since 2.0.0 + * @deprecated + */ +import type * as Equal from "./Equal.js" +import * as InternalSecret from "./internal/secret.js" +import type * as Redacted from "./Redacted.js" + +/** + * @since 2.0.0 + * @category symbols + * @deprecated + */ +export const SecretTypeId: unique symbol = InternalSecret.SecretTypeId + +/** + * @since 2.0.0 + * @category symbols + * @deprecated + */ +export type SecretTypeId = typeof SecretTypeId + +/** + * @since 2.0.0 + * @category models + * @deprecated + */ +export interface Secret extends Redacted.Redacted, Secret.Proto, Equal.Equal { + /** @internal */ + readonly raw: Array +} + +/** + * @since 2.0.0 + * @deprecated + */ +export declare namespace Secret { + /** + * @since 2.0.0 + * @category models + * @deprecated + */ + export interface Proto { + readonly [SecretTypeId]: SecretTypeId + } +} + +/** + * @since 2.0.0 + * @category refinements + * @deprecated + */ +export const isSecret: (u: unknown) => u is Secret = InternalSecret.isSecret + +/** + * @since 2.0.0 + * @category constructors + * @deprecated + */ +export const make: (bytes: Array) => Secret = InternalSecret.make + +/** + * @since 2.0.0 + * @category constructors + * @deprecated + */ +export const fromIterable: (iterable: Iterable) => Secret = InternalSecret.fromIterable + +/** + * @since 2.0.0 + * @category constructors + * @deprecated + */ +export const fromString: (text: string) => Secret = InternalSecret.fromString + +/** + * @since 2.0.0 + * @category getters + * @deprecated + */ +export const value: (self: Secret) => string = InternalSecret.value + +/** + * @since 2.0.0 + * @category unsafe + * @deprecated + */ +export const unsafeWipe: (self: Secret) => void = InternalSecret.unsafeWipe diff --git a/repos/effect/packages/effect/src/SingleProducerAsyncInput.ts b/repos/effect/packages/effect/src/SingleProducerAsyncInput.ts new file mode 100644 index 0000000..43e07dd --- /dev/null +++ b/repos/effect/packages/effect/src/SingleProducerAsyncInput.ts @@ -0,0 +1,67 @@ +/** + * @since 2.0.0 + */ +import type * as Cause from "./Cause.js" +import type * as Effect from "./Effect.js" +import type * as Either from "./Either.js" +import type * as Exit from "./Exit.js" +import * as internal from "./internal/channel/singleProducerAsyncInput.js" + +/** + * An MVar-like abstraction for sending data to channels asynchronously which is + * designed for one producer and multiple consumers. + * + * Features the following semantics: + * - Buffer of size 1. + * - When emitting, the producer waits for a consumer to pick up the value to + * prevent "reading ahead" too much. + * - Once an emitted element is read by a consumer, it is cleared from the + * buffer, so that at most one consumer sees every emitted element. + * - When sending a done or error signal, the producer does not wait for a + * consumer to pick up the signal. The signal stays in the buffer after + * being read by a consumer, so it can be propagated to multiple consumers. + * - Trying to publish another emit/error/done after an error/done have + * already been published results in an interruption. + * + * @since 2.0.0 + * @category models + */ +export interface SingleProducerAsyncInput + extends AsyncInputProducer, AsyncInputConsumer +{ + readonly close: Effect.Effect + readonly take: Effect.Effect>> +} + +/** + * Producer-side view of `SingleProducerAsyncInput` for variance purposes. + * + * @since 2.0.0 + * @category models + */ +export interface AsyncInputProducer { + awaitRead(): Effect.Effect + done(value: Done): Effect.Effect + emit(element: Elem): Effect.Effect + error(cause: Cause.Cause): Effect.Effect +} + +/** + * Consumer-side view of `SingleProducerAsyncInput` for variance purposes. + * + * @since 2.0.0 + * @category models + */ +export interface AsyncInputConsumer { + takeWith( + onError: (cause: Cause.Cause) => A, + onElement: (element: Elem) => A, + onDone: (value: Done) => A + ): Effect.Effect +} + +/** + * @since 2.0.0 + * @category constructors + */ +export const make: () => Effect.Effect> = internal.make diff --git a/repos/effect/packages/effect/src/Sink.ts b/repos/effect/packages/effect/src/Sink.ts new file mode 100644 index 0000000..61756be --- /dev/null +++ b/repos/effect/packages/effect/src/Sink.ts @@ -0,0 +1,1461 @@ +/** + * @since 2.0.0 + */ +import type * as Cause from "./Cause.js" +import type * as Channel from "./Channel.js" +import type * as Chunk from "./Chunk.js" +import type * as Context from "./Context.js" +import type * as Duration from "./Duration.js" +import type * as Effect from "./Effect.js" +import type * as Either from "./Either.js" +import type * as Exit from "./Exit.js" +import type { LazyArg } from "./Function.js" +import type * as HashMap from "./HashMap.js" +import type * as HashSet from "./HashSet.js" +import * as internal from "./internal/sink.js" +import type * as MergeDecision from "./MergeDecision.js" +import type * as Option from "./Option.js" +import type { Pipeable } from "./Pipeable.js" +import type { Predicate, Refinement } from "./Predicate.js" +import type * as PubSub from "./PubSub.js" +import type * as Queue from "./Queue.js" +import type * as Scope from "./Scope.js" +import type * as Types from "./Types.js" +import type * as Unify from "./Unify.js" + +/** + * @since 2.0.0 + * @category symbols + */ +export const SinkTypeId: unique symbol = internal.SinkTypeId + +/** + * @since 2.0.0 + * @category symbols + */ +export type SinkTypeId = typeof SinkTypeId + +/** + * A `Sink` is used to consume elements produced by a `Stream`. + * You can think of a sink as a function that will consume a variable amount of + * `In` elements (could be 0, 1, or many), might fail with an error of type `E`, + * and will eventually yield a value of type `A` together with a remainder of + * type `L` (i.e. any leftovers). + * + * @since 2.0.0 + * @category models + */ +export interface Sink + extends Sink.Variance, Pipeable +{} + +/** + * @since 2.0.0 + * @category models + */ +export interface SinkUnify extends Effect.EffectUnify { + Sink?: () => A[Unify.typeSymbol] extends + | Sink< + infer A, + infer In, + infer L, + infer E, + infer R + > + | infer _ ? Sink + : never +} + +/** + * @category models + * @since 2.0.0 + */ +export interface SinkUnifyIgnore extends Effect.EffectUnifyIgnore { + Sink?: true +} + +/** + * @since 2.0.0 + * @category models + */ +declare module "./Effect.js" { + interface Effect extends Sink {} + interface EffectUnifyIgnore { + Sink?: true + } +} + +/** + * @since 2.0.0 + */ +export declare namespace Sink { + /** + * @since 2.0.0 + * @category models + */ + export interface Variance { + readonly [SinkTypeId]: VarianceStruct + } + /** + * @since 2.0.0 + * @category models + */ + export interface VarianceStruct { + _A: Types.Covariant + _In: Types.Contravariant + _L: Types.Covariant + _E: Types.Covariant + _R: Types.Covariant + } +} + +/** + * Replaces this sink's result with the provided value. + * + * @since 2.0.0 + * @category mapping + */ +export const as: { + (a: A2): (self: Sink) => Sink + (self: Sink, a: A2): Sink +} = internal.as + +/** + * A sink that collects all elements into a `Chunk`. + * + * @since 2.0.0 + * @category constructors + */ +export const collectAll: () => Sink, In> = internal.collectAll + +/** + * A sink that collects first `n` elements into a chunk. + * + * @since 2.0.0 + * @category constructors + */ +export const collectAllN: (n: number) => Sink, In, In> = internal.collectAllN + +/** + * Repeatedly runs the sink and accumulates its results into a `Chunk`. + * + * @since 2.0.0 + * @category utils + */ +export const collectAllFrom: ( + self: Sink +) => Sink, In, L, E, R> = internal.collectAllFrom + +/** + * A sink that collects all of its inputs into a map. The keys are extracted + * from inputs using the keying function `key`; if multiple inputs use the + * same key, they are merged using the `merge` function. + * + * @since 2.0.0 + * @category constructors + */ +export const collectAllToMap: ( + key: (input: In) => K, + merge: (x: In, y: In) => In +) => Sink, In> = internal.collectAllToMap + +/** + * A sink that collects first `n` keys into a map. The keys are calculated + * from inputs using the keying function `key`; if multiple inputs use the the + * same key, they are merged using the `merge` function. + * + * @since 2.0.0 + * @category constructors + */ +export const collectAllToMapN: ( + n: number, + key: (input: In) => K, + merge: (x: In, y: In) => In +) => Sink, In, In> = internal.collectAllToMapN + +/** + * A sink that collects all of its inputs into a set. + * + * @since 2.0.0 + * @category constructors + */ +export const collectAllToSet: () => Sink, In> = internal.collectAllToSet + +/** + * A sink that collects first `n` distinct inputs into a set. + * + * @since 2.0.0 + * @category constructors + */ +export const collectAllToSetN: (n: number) => Sink, In, In> = internal.collectAllToSetN + +/** + * Accumulates incoming elements into a chunk until predicate `p` is + * satisfied. + * + * @since 2.0.0 + * @category constructors + */ +export const collectAllUntil: (p: Predicate) => Sink, In, In> = internal.collectAllUntil + +/** + * Accumulates incoming elements into a chunk until effectful predicate `p` is + * satisfied. + * + * @since 2.0.0 + * @category constructors + */ +export const collectAllUntilEffect: ( + p: (input: In) => Effect.Effect +) => Sink, In, In, E, R> = internal.collectAllUntilEffect + +/** + * Accumulates incoming elements into a chunk as long as they verify predicate + * `p`. + * + * @since 2.0.0 + * @category constructors + */ +export const collectAllWhile: { + (refinement: Refinement): Sink, In, In> + (predicate: Predicate): Sink, In, In> +} = internal.collectAllWhile + +/** + * Accumulates incoming elements into a chunk as long as they verify effectful + * predicate `p`. + * + * @since 2.0.0 + * @category constructors + */ +export const collectAllWhileEffect: ( + predicate: (input: In) => Effect.Effect +) => Sink, In, In, E, R> = internal.collectAllWhileEffect + +/** + * Repeatedly runs the sink for as long as its results satisfy the predicate + * `p`. The sink's results will be accumulated using the stepping function `f`. + * + * @since 2.0.0 + * @category utils + */ +export const collectAllWhileWith: { + ( + options: { readonly initial: S; readonly while: Predicate; readonly body: (s: S, a: A) => S } + ): (self: Sink) => Sink + ( + self: Sink, + options: { readonly initial: S; readonly while: Predicate; readonly body: (s: S, a: A) => S } + ): Sink +} = internal.collectAllWhileWith as any + +/** + * Collects the leftovers from the stream when the sink succeeds and returns + * them as part of the sink's result. + * + * @since 2.0.0 + * @category utils + */ +export const collectLeftover: ( + self: Sink +) => Sink<[A, Chunk.Chunk], In, never, E, R> = internal.collectLeftover + +/** + * Transforms this sink's input elements. + * + * @since 2.0.0 + * @category mapping + */ +export const mapInput: { + (f: (input: In0) => In): (self: Sink) => Sink + (self: Sink, f: (input: In0) => In): Sink +} = internal.mapInput + +/** + * Effectfully transforms this sink's input elements. + * + * @since 2.0.0 + * @category mapping + */ +export const mapInputEffect: { + ( + f: (input: In0) => Effect.Effect + ): (self: Sink) => Sink + ( + self: Sink, + f: (input: In0) => Effect.Effect + ): Sink +} = internal.mapInputEffect + +/** + * Transforms this sink's input chunks. `f` must preserve chunking-invariance. + * + * @since 2.0.0 + * @category mapping + */ +export const mapInputChunks: { + ( + f: (chunk: Chunk.Chunk) => Chunk.Chunk + ): (self: Sink) => Sink + ( + self: Sink, + f: (chunk: Chunk.Chunk) => Chunk.Chunk + ): Sink +} = internal.mapInputChunks + +/** + * Effectfully transforms this sink's input chunks. `f` must preserve + * chunking-invariance. + * + * @since 2.0.0 + * @category mapping + */ +export const mapInputChunksEffect: { + ( + f: (chunk: Chunk.Chunk) => Effect.Effect, E2, R2> + ): (self: Sink) => Sink + ( + self: Sink, + f: (chunk: Chunk.Chunk) => Effect.Effect, E2, R2> + ): Sink +} = internal.mapInputChunksEffect + +/** + * A sink that counts the number of elements fed to it. + * + * @since 2.0.0 + * @category constructors + */ +export const count: Sink = internal.count + +/** + * Creates a sink halting with the specified defect. + * + * @since 2.0.0 + * @category constructors + */ +export const die: (defect: unknown) => Sink = internal.die + +/** + * Creates a sink halting with the specified message, wrapped in a + * `RuntimeException`. + * + * @since 2.0.0 + * @category constructors + */ +export const dieMessage: (message: string) => Sink = internal.dieMessage + +/** + * Creates a sink halting with the specified defect. + * + * @since 2.0.0 + * @category constructors + */ +export const dieSync: (evaluate: LazyArg) => Sink = internal.dieSync + +/** + * Transforms both inputs and result of this sink using the provided + * functions. + * + * @since 2.0.0 + * @category mapping + */ +export const dimap: { + ( + options: { readonly onInput: (input: In0) => In; readonly onDone: (a: A) => A2 } + ): (self: Sink) => Sink + ( + self: Sink, + options: { readonly onInput: (input: In0) => In; readonly onDone: (a: A) => A2 } + ): Sink +} = internal.dimap + +/** + * Effectfully transforms both inputs and result of this sink using the + * provided functions. + * + * @since 2.0.0 + * @category mapping + */ +export const dimapEffect: { + ( + options: { + readonly onInput: (input: In0) => Effect.Effect + readonly onDone: (a: A) => Effect.Effect + } + ): (self: Sink) => Sink + ( + self: Sink, + options: { + readonly onInput: (input: In0) => Effect.Effect + readonly onDone: (a: A) => Effect.Effect + } + ): Sink +} = internal.dimapEffect + +/** + * Transforms both input chunks and result of this sink using the provided + * functions. + * + * @since 2.0.0 + * @category mapping + */ +export const dimapChunks: { + ( + options: { readonly onInput: (chunk: Chunk.Chunk) => Chunk.Chunk; readonly onDone: (a: A) => A2 } + ): (self: Sink) => Sink + ( + self: Sink, + options: { readonly onInput: (chunk: Chunk.Chunk) => Chunk.Chunk; readonly onDone: (a: A) => A2 } + ): Sink +} = internal.dimapChunks + +/** + * Effectfully transforms both input chunks and result of this sink using the + * provided functions. `f` and `g` must preserve chunking-invariance. + * + * @since 2.0.0 + * @category mapping + */ +export const dimapChunksEffect: { + ( + options: { + readonly onInput: (chunk: Chunk.Chunk) => Effect.Effect, E2, R2> + readonly onDone: (a: A) => Effect.Effect + } + ): (self: Sink) => Sink + ( + self: Sink, + options: { + readonly onInput: (chunk: Chunk.Chunk) => Effect.Effect, E2, R2> + readonly onDone: (a: A) => Effect.Effect + } + ): Sink +} = internal.dimapChunksEffect + +/** + * A sink that ignores its inputs. + * + * @since 2.0.0 + * @category constructors + */ +export const drain: Sink = internal.drain + +/** + * Creates a sink that drops `n` elements. + * + * @since 2.0.0 + * @category constructors + */ +export const drop: (n: number) => Sink = internal.drop + +/** + * Drops incoming elements until the predicate is satisfied. + * + * @since 2.0.0 + * @category constructors + */ +export const dropUntil: (predicate: Predicate) => Sink = internal.dropUntil + +/** + * Drops incoming elements until the effectful predicate is satisfied. + * + * @since 2.0.0 + * @category constructors + */ +export const dropUntilEffect: ( + predicate: (input: In) => Effect.Effect +) => Sink = internal.dropUntilEffect + +/** + * Drops incoming elements as long as the predicate is satisfied. + * + * @since 2.0.0 + * @category constructors + */ +export const dropWhile: (predicate: Predicate) => Sink = internal.dropWhile + +/** + * Drops incoming elements as long as the effectful predicate is satisfied. + * + * @since 2.0.0 + * @category constructors + */ +export const dropWhileEffect: ( + predicate: (input: In) => Effect.Effect +) => Sink = internal.dropWhileEffect + +/** + * Returns a new sink with an attached finalizer. The finalizer is guaranteed + * to be executed so long as the sink begins execution (and regardless of + * whether or not it completes). + * + * @since 2.0.0 + * @category finalization + */ +export const ensuring: { + ( + finalizer: Effect.Effect + ): (self: Sink) => Sink + (self: Sink, finalizer: Effect.Effect): Sink +} = internal.ensuring + +/** + * Returns a new sink with an attached finalizer. The finalizer is guaranteed + * to be executed so long as the sink begins execution (and regardless of + * whether or not it completes). + * + * @since 2.0.0 + * @category finalization + */ +export const ensuringWith: { + ( + finalizer: (exit: Exit.Exit) => Effect.Effect + ): (self: Sink) => Sink + ( + self: Sink, + finalizer: (exit: Exit.Exit) => Effect.Effect + ): Sink +} = internal.ensuringWith + +/** + * Accesses the whole context of the sink. + * + * @since 2.0.0 + * @category constructors + */ +export const context: () => Sink, unknown, never, never, R> = internal.context + +/** + * Accesses the context of the sink. + * + * @since 2.0.0 + * @category constructors + */ +export const contextWith: (f: (context: Context.Context) => Z) => Sink = + internal.contextWith + +/** + * Accesses the context of the sink in the context of an effect. + * + * @since 2.0.0 + * @category constructors + */ +export const contextWithEffect: ( + f: (context: Context.Context) => Effect.Effect +) => Sink = internal.contextWithEffect + +/** + * Accesses the context of the sink in the context of a sink. + * + * @since 2.0.0 + * @category constructors + */ +export const contextWithSink: ( + f: (context: Context.Context) => Sink +) => Sink = internal.contextWithSink + +/** + * A sink that returns whether all elements satisfy the specified predicate. + * + * @since 2.0.0 + * @category constructors + */ +export const every: (predicate: Predicate) => Sink = internal.every + +/** + * A sink that always fails with the specified error. + * + * @since 2.0.0 + * @category constructors + */ +export const fail: (e: E) => Sink = internal.fail + +/** + * A sink that always fails with the specified lazily evaluated error. + * + * @since 2.0.0 + * @category constructors + */ +export const failSync: (evaluate: LazyArg) => Sink = internal.failSync + +/** + * Creates a sink halting with a specified `Cause`. + * + * @since 2.0.0 + * @category constructors + */ +export const failCause: (cause: Cause.Cause) => Sink = internal.failCause + +/** + * Creates a sink halting with a specified lazily evaluated `Cause`. + * + * @since 2.0.0 + * @category constructors + */ +export const failCauseSync: (evaluate: LazyArg>) => Sink = + internal.failCauseSync + +/** + * Filters the sink's input with the given predicate. + * + * @since 2.0.0 + * @category filtering + */ +export const filterInput: { + ( + f: Refinement + ): (self: Sink) => Sink + (f: Predicate): (self: Sink) => Sink +} = internal.filterInput + +/** + * Effectfully filter the input of this sink using the specified predicate. + * + * @since 2.0.0 + * @category filtering + */ +export const filterInputEffect: { + ( + f: (input: In1) => Effect.Effect + ): (self: Sink) => Sink + ( + self: Sink, + f: (input: In1) => Effect.Effect + ): Sink +} = internal.filterInputEffect + +/** + * Creates a sink that produces values until one verifies the predicate `f`. + * + * @since 2.0.0 + * @category elements + */ +export const findEffect: { + ( + f: (a: A) => Effect.Effect + ): (self: Sink) => Sink, In, L, E2 | E, R2 | R> + ( + self: Sink, + f: (a: A) => Effect.Effect + ): Sink, In, L, E | E2, R | R2> +} = internal.findEffect as any + +/** + * A sink that folds its inputs with the provided function, termination + * predicate and initial state. + * + * @since 2.0.0 + * @category folding + */ +export const fold: (s: S, contFn: Predicate, f: (s: S, input: In) => S) => Sink = internal.fold + +/** + * Folds over the result of the sink + * + * @since 2.0.0 + * @category folding + */ +export const foldSink: { + ( + options: { + readonly onFailure: (err: E) => Sink + readonly onSuccess: (a: A) => Sink + } + ): (self: Sink) => Sink + ( + self: Sink, + options: { + readonly onFailure: (err: E) => Sink + readonly onSuccess: (a: A) => Sink + } + ): Sink +} = internal.foldSink + +/** + * A sink that folds its input chunks with the provided function, termination + * predicate and initial state. `contFn` condition is checked only for the + * initial value and at the end of processing of each chunk. `f` and `contFn` + * must preserve chunking-invariance. + * + * @since 2.0.0 + * @category constructors + */ +export const foldChunks: ( + s: S, + contFn: Predicate, + f: (s: S, chunk: Chunk.Chunk) => S +) => Sink = internal.foldChunks + +/** + * A sink that effectfully folds its input chunks with the provided function, + * termination predicate and initial state. `contFn` condition is checked only + * for the initial value and at the end of processing of each chunk. `f` and + * `contFn` must preserve chunking-invariance. + * + * @since 2.0.0 + * @category constructors + */ +export const foldChunksEffect: ( + s: S, + contFn: Predicate, + f: (s: S, chunk: Chunk.Chunk) => Effect.Effect +) => Sink = internal.foldChunksEffect + +/** + * A sink that effectfully folds its inputs with the provided function, + * termination predicate and initial state. + * + * @since 2.0.0 + * @category constructors + */ +export const foldEffect: ( + s: S, + contFn: Predicate, + f: (s: S, input: In) => Effect.Effect +) => Sink = internal.foldEffect + +/** + * A sink that folds its inputs with the provided function and initial state. + * + * @since 2.0.0 + * @category constructors + */ +export const foldLeft: (s: S, f: (s: S, input: In) => S) => Sink = internal.foldLeft + +/** + * A sink that folds its input chunks with the provided function and initial + * state. `f` must preserve chunking-invariance. + * + * @since 2.0.0 + * @category constructors + */ +export const foldLeftChunks: (s: S, f: (s: S, chunk: Chunk.Chunk) => S) => Sink = + internal.foldLeftChunks + +/** + * A sink that effectfully folds its input chunks with the provided function + * and initial state. `f` must preserve chunking-invariance. + * + * @since 2.0.0 + * @category constructors + */ +export const foldLeftChunksEffect: ( + s: S, + f: (s: S, chunk: Chunk.Chunk) => Effect.Effect +) => Sink = internal.foldLeftChunksEffect + +/** + * A sink that effectfully folds its inputs with the provided function and + * initial state. + * + * @since 2.0.0 + * @category constructors + */ +export const foldLeftEffect: ( + s: S, + f: (s: S, input: In) => Effect.Effect +) => Sink = internal.foldLeftEffect + +/** + * Creates a sink that folds elements of type `In` into a structure of type + * `S` until `max` elements have been folded. + * + * Like `Sink.foldWeighted`, but with a constant cost function of `1`. + * + * @since 2.0.0 + * @category constructors + */ +export const foldUntil: (s: S, max: number, f: (s: S, input: In) => S) => Sink = internal.foldUntil + +/** + * Creates a sink that effectfully folds elements of type `In` into a + * structure of type `S` until `max` elements have been folded. + * + * Like `Sink.foldWeightedEffect` but with a constant cost function of `1`. + * + * @since 2.0.0 + * @category constructors + */ +export const foldUntilEffect: ( + s: S, + max: number, + f: (s: S, input: In) => Effect.Effect +) => Sink = internal.foldUntilEffect + +/** + * Creates a sink that folds elements of type `In` into a structure of type `S`, + * until `max` worth of elements (determined by the `costFn`) have been folded. + * + * **Note** + * + * Elements that have an individual cost larger than `max` will force the sink + * to cross the `max` cost. See `Sink.foldWeightedDecompose` for a variant + * that can handle these cases. + * + * @since 2.0.0 + * @category constructors + */ +export const foldWeighted: ( + options: { + readonly initial: S + readonly maxCost: number + readonly cost: (s: S, input: In) => number + readonly body: (s: S, input: In) => S + } +) => Sink = internal.foldWeighted + +/** + * Creates a sink that folds elements of type `In` into a structure of type + * `S`, until `max` worth of elements (determined by the `costFn`) have been + * folded. + * + * The `decompose` function will be used for decomposing elements that cause + * an `S` aggregate to cross `max` into smaller elements. For example: + * + * ```ts skip-type-checking + * pipe( + * Stream.make(1, 5, 1), + * Stream.transduce( + * Sink.foldWeightedDecompose( + * Chunk.empty(), + * 4, + * (n: number) => n, + * (n: number) => Chunk.make(n - 1, 1), + * (acc, el) => pipe(acc, Chunk.append(el)) + * ) + * ), + * Stream.runCollect + * ) + * ``` + * + * The stream would emit the elements `Chunk(1), Chunk(4), Chunk(1, 1)`. + * + * Be vigilant with this function, it has to generate "simpler" values or the + * fold may never end. A value is considered indivisible if `decompose` yields + * the empty chunk or a single-valued chunk. In these cases, there is no other + * choice than to yield a value that will cross the threshold. + * + * `Sink.foldWeightedDecomposeEffect` allows the decompose function to return an + * effect value, and consequently it allows the sink to fail. + * + * @since 2.0.0 + * @category constructors + */ +export const foldWeightedDecompose: ( + options: { + readonly initial: S + readonly maxCost: number + readonly cost: (s: S, input: In) => number + readonly decompose: (input: In) => Chunk.Chunk + readonly body: (s: S, input: In) => S + } +) => Sink = internal.foldWeightedDecompose + +/** + * Creates a sink that effectfully folds elements of type `In` into a + * structure of type `S`, until `max` worth of elements (determined by the + * `costFn`) have been folded. + * + * The `decompose` function will be used for decomposing elements that cause + * an `S` aggregate to cross `max` into smaller elements. Be vigilant with + * this function, it has to generate "simpler" values or the fold may never + * end. A value is considered indivisible if `decompose` yields the empty + * chunk or a single-valued chunk. In these cases, there is no other choice + * than to yield a value that will cross the threshold. + * + * See `Sink.foldWeightedDecompose` for an example. + * + * @since 2.0.0 + * @category constructors + */ +export const foldWeightedDecomposeEffect: ( + options: { + readonly initial: S + readonly maxCost: number + readonly cost: (s: S, input: In) => Effect.Effect + readonly decompose: (input: In) => Effect.Effect, E2, R2> + readonly body: (s: S, input: In) => Effect.Effect + } +) => Sink = internal.foldWeightedDecomposeEffect + +/** + * Creates a sink that effectfully folds elements of type `In` into a + * structure of type `S`, until `max` worth of elements (determined by the + * `costFn`) have been folded. + * + * @note + * Elements that have an individual cost larger than `max` will force the + * sink to cross the `max` cost. See `Sink.foldWeightedDecomposeEffect` for + * a variant that can handle these cases. + * + * @since 2.0.0 + * @category constructors + */ +export const foldWeightedEffect: ( + options: { + readonly initial: S + readonly maxCost: number + readonly cost: (s: S, input: In) => Effect.Effect + readonly body: (s: S, input: In) => Effect.Effect + } +) => Sink = internal.foldWeightedEffect + +/** + * A sink that executes the provided effectful function for every element fed + * to it. + * + * @since 2.0.0 + * @category constructors + */ +export const forEach: (f: (input: In) => Effect.Effect) => Sink = + internal.forEach + +/** + * A sink that executes the provided effectful function for every chunk fed to + * it. + * + * @since 2.0.0 + * @category constructors + */ +export const forEachChunk: ( + f: (input: Chunk.Chunk) => Effect.Effect +) => Sink = internal.forEachChunk + +/** + * A sink that executes the provided effectful function for every chunk fed to + * it until `f` evaluates to `false`. + * + * @since 2.0.0 + * @category constructors + */ +export const forEachChunkWhile: ( + f: (input: Chunk.Chunk) => Effect.Effect +) => Sink = internal.forEachChunkWhile + +/** + * A sink that executes the provided effectful function for every element fed + * to it until `f` evaluates to `false`. + * + * @since 2.0.0 + * @category constructors + */ +export const forEachWhile: (f: (input: In) => Effect.Effect) => Sink = + internal.forEachWhile + +/** + * Runs this sink until it yields a result, then uses that result to create + * another sink from the provided function which will continue to run until it + * yields a result. + * + * This function essentially runs sinks in sequence. + * + * @since 2.0.0 + * @category sequencing + */ +export const flatMap: { + ( + f: (a: A) => Sink + ): (self: Sink) => Sink + ( + self: Sink, + f: (a: A) => Sink + ): Sink +} = internal.flatMap + +/** + * Creates a sink from a `Channel`. + * + * @since 2.0.0 + * @category constructors + */ +export const fromChannel: ( + channel: Channel.Channel, Chunk.Chunk, E, never, A, unknown, R> +) => Sink = internal.fromChannel + +/** + * Creates a `Channel` from a Sink. + * + * @since 2.0.0 + * @category constructors + */ +export const toChannel: ( + self: Sink +) => Channel.Channel, Chunk.Chunk, E, never, A, unknown, R> = internal.toChannel + +/** + * Creates a single-value sink produced from an effect. + * + * @since 2.0.0 + * @category constructors + */ +export const fromEffect: (effect: Effect.Effect) => Sink = + internal.fromEffect + +/** + * Create a sink which publishes each element to the specified `PubSub`. + * + * If the `shutdown` parameter is `true`, the `PubSub` will be shutdown after + * the sink is evaluated (defaults to `false`). + * + * @since 2.0.0 + * @category constructors + */ +export const fromPubSub: ( + pubsub: PubSub.PubSub, + options?: { + readonly shutdown?: boolean | undefined + } +) => Sink = internal.fromPubSub + +/** + * Creates a sink from a chunk processing function. + * + * @since 2.0.0 + * @category constructors + */ +export const fromPush: ( + push: Effect.Effect< + (_: Option.Option>) => Effect.Effect, Chunk.Chunk], R>, + never, + R + > +) => Sink> = internal.fromPush + +/** + * Create a sink which enqueues each element into the specified queue. + * + * If the `shutdown` parameter is `true`, the queue will be shutdown after the + * sink is evaluated (defaults to `false`). + * + * @since 2.0.0 + * @category constructors + */ +export const fromQueue: ( + queue: Queue.Enqueue, + options?: { + readonly shutdown?: boolean | undefined + } +) => Sink = internal.fromQueue + +/** + * Creates a sink containing the first value. + * + * @since 2.0.0 + * @category constructors + */ +export const head: () => Sink, In, In> = internal.head + +/** + * Drains the remaining elements from the stream after the sink finishes + * + * @since 2.0.0 + * @category utils + */ +export const ignoreLeftover: (self: Sink) => Sink = + internal.ignoreLeftover + +/** + * Creates a sink containing the last value. + * + * @since 2.0.0 + * @category constructors + */ +export const last: () => Sink, In, In> = internal.last + +/** + * Creates a sink that does not consume any input but provides the given chunk + * as its leftovers + * + * @since 2.0.0 + * @category constructors + */ +export const leftover: (chunk: Chunk.Chunk) => Sink = internal.leftover + +/** + * Transforms this sink's result. + * + * @since 2.0.0 + * @category mapping + */ +export const map: { + (f: (a: A) => A2): (self: Sink) => Sink + (self: Sink, f: (a: A) => A2): Sink +} = internal.map + +/** + * Effectfully transforms this sink's result. + * + * @since 2.0.0 + * @category mapping + */ +export const mapEffect: { + ( + f: (a: A) => Effect.Effect + ): (self: Sink) => Sink + ( + self: Sink, + f: (a: A) => Effect.Effect + ): Sink +} = internal.mapEffect + +/** + * Transforms the errors emitted by this sink using `f`. + * + * @since 2.0.0 + * @category mapping + */ +export const mapError: { + (f: (error: E) => E2): (self: Sink) => Sink + (self: Sink, f: (error: E) => E2): Sink +} = internal.mapError + +/** + * Transforms the leftovers emitted by this sink using `f`. + * + * @since 2.0.0 + * @category mapping + */ +export const mapLeftover: { + (f: (leftover: L) => L2): (self: Sink) => Sink + (self: Sink, f: (leftover: L) => L2): Sink +} = internal.mapLeftover + +/** + * Creates a sink which transforms it's inputs into a string. + * + * @since 2.0.0 + * @category constructors + */ +export const mkString: Sink = internal.mkString + +/** + * Creates a sink which never terminates. + * + * @since 2.0.0 + * @category constructors + */ +export const never: Sink = internal.never + +/** + * Switch to another sink in case of failure + * + * @since 2.0.0 + * @category error handling + */ +export const orElse: { + ( + that: LazyArg> + ): (self: Sink) => Sink + ( + self: Sink, + that: LazyArg> + ): Sink +} = internal.orElse + +/** + * Provides the sink with its required context, which eliminates its + * dependency on `R`. + * + * @since 2.0.0 + * @category context + */ +export const provideContext: { + (context: Context.Context): (self: Sink) => Sink + (self: Sink, context: Context.Context): Sink +} = internal.provideContext + +/** + * Runs both sinks in parallel on the input, , returning the result or the + * error from the one that finishes first. + * + * @since 2.0.0 + * @category utils + */ +export const race: { + ( + that: Sink + ): (self: Sink) => Sink + ( + self: Sink, + that: Sink + ): Sink +} = internal.race + +/** + * Runs both sinks in parallel on the input, returning the result or the error + * from the one that finishes first. + * + * @since 2.0.0 + * @category utils + */ +export const raceBoth: { + ( + that: Sink, + options?: { readonly capacity?: number | undefined } | undefined + ): (self: Sink) => Sink, In & In1, L1 | L, E1 | E, R1 | R> + ( + self: Sink, + that: Sink, + options?: { readonly capacity?: number | undefined } | undefined + ): Sink, In & In1, L | L1, E | E1, R | R1> +} = internal.raceBoth + +/** + * Runs both sinks in parallel on the input, using the specified merge + * function as soon as one result or the other has been computed. + * + * @since 2.0.0 + * @category utils + */ +export const raceWith: { + ( + options: { + readonly other: Sink + readonly onSelfDone: (exit: Exit.Exit) => MergeDecision.MergeDecision + readonly onOtherDone: (exit: Exit.Exit) => MergeDecision.MergeDecision + readonly capacity?: number | undefined + } + ): (self: Sink) => Sink + ( + self: Sink, + options: { + readonly other: Sink + readonly onSelfDone: (exit: Exit.Exit) => MergeDecision.MergeDecision + readonly onOtherDone: (exit: Exit.Exit) => MergeDecision.MergeDecision + readonly capacity?: number | undefined + } + ): Sink +} = internal.raceWith + +/** + * @since 2.0.0 + * @category error handling + */ +export const refineOrDie: { + (pf: (error: E) => Option.Option): (self: Sink) => Sink + (self: Sink, pf: (error: E) => Option.Option): Sink +} = internal.refineOrDie + +/** + * @since 2.0.0 + * @category error handling + */ +export const refineOrDieWith: { + ( + pf: (error: E) => Option.Option, + f: (error: E) => unknown + ): (self: Sink) => Sink + ( + self: Sink, + pf: (error: E) => Option.Option, + f: (error: E) => unknown + ): Sink +} = internal.refineOrDieWith + +/** + * A sink that returns whether an element satisfies the specified predicate. + * + * @since 2.0.0 + * @category constructors + */ +export const some: (predicate: Predicate) => Sink = internal.some + +/** + * Splits the sink on the specified predicate, returning a new sink that + * consumes elements until an element after the first satisfies the specified + * predicate. + * + * @since 2.0.0 + * @category utils + */ +export const splitWhere: { + (f: Predicate): (self: Sink) => Sink + (self: Sink, f: Predicate): Sink +} = internal.splitWhere + +/** + * A sink that immediately ends with the specified value. + * + * @since 2.0.0 + * @category constructors + */ +export const succeed: (a: A) => Sink = internal.succeed + +/** + * A sink that sums incoming numeric values. + * + * @since 2.0.0 + * @category constructors + */ +export const sum: Sink = internal.sum + +/** + * Summarize a sink by running an effect when the sink starts and again when + * it completes. + * + * @since 2.0.0 + * @category utils + */ +export const summarized: { + ( + summary: Effect.Effect, + f: (start: A2, end: A2) => A3 + ): (self: Sink) => Sink<[A, A3], In, L, E2 | E, R2 | R> + ( + self: Sink, + summary: Effect.Effect, + f: (start: A2, end: A2) => A3 + ): Sink<[A, A3], In, L, E | E2, R | R2> +} = internal.summarized + +/** + * Returns a lazily constructed sink that may require effects for its + * creation. + * + * @since 2.0.0 + * @category constructors + */ +export const suspend: (evaluate: LazyArg>) => Sink = + internal.suspend + +/** + * A sink that immediately ends with the specified lazy value. + * + * @since 2.0.0 + * @category constructors + */ +export const sync: (evaluate: LazyArg) => Sink = internal.sync + +/** + * A sink that takes the specified number of values. + * + * @since 2.0.0 + * @category constructors + */ +export const take: (n: number) => Sink, In, In> = internal.take + +/** + * @since 2.0.0 + * @category constructors + */ +export const timed: Sink = internal.timed + +/** + * Creates a sink produced from an effect. + * + * @since 2.0.0 + * @category constructors + */ +export const unwrap: ( + effect: Effect.Effect, E, R> +) => Sink = internal.unwrap + +/** + * Creates a sink produced from a scoped effect. + * + * @since 2.0.0 + * @category constructors + */ +export const unwrapScoped: ( + effect: Effect.Effect, E, R> +) => Sink> = internal.unwrapScoped + +/** + * Constructs a `Sink` from a function which receives a `Scope` and returns + * an effect that will result in a `Sink` if successful. + * + * @since 3.11.0 + * @category constructors + */ +export const unwrapScopedWith: ( + f: (scope: Scope.Scope) => Effect.Effect, E, R> +) => Sink = internal.unwrapScopedWith + +/** + * Returns the sink that executes this one and times its execution. + * + * @since 2.0.0 + * @category utils + */ +export const withDuration: ( + self: Sink +) => Sink<[A, Duration.Duration], In, L, E, R> = internal.withDuration + +/** + * Feeds inputs to this sink until it yields a result, then switches over to + * the provided sink until it yields a result, finally combining the two + * results into a tuple. + * + * @since 2.0.0 + * @category zipping + */ +export const zip: { + ( + that: Sink, + options?: { readonly concurrent?: boolean | undefined } | undefined + ): (self: Sink) => Sink<[A, A2], In & In2, L2 | L, E2 | E, R2 | R> + ( + self: Sink, + that: Sink, + options?: { readonly concurrent?: boolean | undefined } | undefined + ): Sink<[A, A2], In & In2, L | L2, E | E2, R | R2> +} = internal.zip + +/** + * Like `Sink.zip` but keeps only the result from this sink. + * + * @since 2.0.0 + * @category zipping + */ +export const zipLeft: { + ( + that: Sink, + options?: { readonly concurrent?: boolean | undefined } | undefined + ): (self: Sink) => Sink + ( + self: Sink, + that: Sink, + options?: { readonly concurrent?: boolean | undefined } | undefined + ): Sink +} = internal.zipLeft + +/** + * Like `Sink.zip` but keeps only the result from `that` sink. + * + * @since 2.0.0 + * @category zipping + */ +export const zipRight: { + ( + that: Sink, + options?: { readonly concurrent?: boolean | undefined } | undefined + ): (self: Sink) => Sink + ( + self: Sink, + that: Sink, + options?: { readonly concurrent?: boolean | undefined } | undefined + ): Sink +} = internal.zipRight + +/** + * Feeds inputs to this sink until it yields a result, then switches over to + * the provided sink until it yields a result, finally combining the two + * results with `f`. + * + * @since 2.0.0 + * @category zipping + */ +export const zipWith: { + ( + that: Sink, + f: (a: A, a2: A2) => A3, + options?: { readonly concurrent?: boolean | undefined } | undefined + ): (self: Sink) => Sink + ( + self: Sink, + that: Sink, + f: (a: A, a2: A2) => A3, + options?: { readonly concurrent?: boolean | undefined } | undefined + ): Sink +} = internal.zipWith diff --git a/repos/effect/packages/effect/src/SortedMap.ts b/repos/effect/packages/effect/src/SortedMap.ts new file mode 100644 index 0000000..61d23cb --- /dev/null +++ b/repos/effect/packages/effect/src/SortedMap.ts @@ -0,0 +1,287 @@ +/** + * @since 2.0.0 + */ +import * as Equal from "./Equal.js" +import * as Dual from "./Function.js" +import { pipe } from "./Function.js" +import * as Hash from "./Hash.js" +import { format, type Inspectable, NodeInspectSymbol, toJSON } from "./Inspectable.js" +import * as Option from "./Option.js" +import type { Order } from "./Order.js" +import type { Pipeable } from "./Pipeable.js" +import { pipeArguments } from "./Pipeable.js" +import { hasProperty } from "./Predicate.js" +import * as RBT from "./RedBlackTree.js" +import type * as Types from "./Types.js" + +const TypeId: unique symbol = Symbol.for("effect/SortedMap") + +/** + * @since 2.0.0 + * @category symbol + */ +export type TypeId = typeof TypeId + +/** + * @since 2.0.0 + * @category models + */ +export interface SortedMap extends Iterable<[K, V]>, Equal.Equal, Pipeable, Inspectable { + readonly [TypeId]: { + readonly _K: Types.Invariant + readonly _V: Types.Covariant + } + /** @internal */ + readonly tree: RBT.RedBlackTree +} + +const SortedMapProto: Omit, "tree"> = { + [TypeId]: { + _K: (_: any) => _, + _V: (_: never) => _ + }, + [Hash.symbol](this: SortedMap): number { + return pipe( + Hash.hash(this.tree), + Hash.combine(Hash.hash("effect/SortedMap")), + Hash.cached(this) + ) + }, + [Equal.symbol](this: SortedMap, that: unknown): boolean { + return isSortedMap(that) && Equal.equals(this.tree, that.tree) + }, + [Symbol.iterator](this: SortedMap): Iterator<[K, V]> { + return this.tree[Symbol.iterator]() + }, + toString() { + return format(this.toJSON()) + }, + toJSON() { + return { + _id: "SortedMap", + values: Array.from(this).map(toJSON) + } + }, + [NodeInspectSymbol]() { + return this.toJSON() + }, + pipe() { + return pipeArguments(this, arguments) + } +} + +const makeImpl = (tree: RBT.RedBlackTree): SortedMap => { + const self = Object.create(SortedMapProto) + self.tree = tree + return self +} + +/** + * @since 2.0.0 + * @category refinements + */ +export const isSortedMap: { + (u: Iterable): u is SortedMap + (u: unknown): u is SortedMap +} = (u: unknown): u is SortedMap => hasProperty(u, TypeId) + +/** + * @since 2.0.0 + * @category constructors + */ +export const empty = (ord: Order): SortedMap => makeImpl(RBT.empty(ord)) + +/** + * Creates a new `SortedMap` from an iterable collection of key/value pairs. + * + * @since 2.0.0 + * @category constructors + */ +export const fromIterable: { + (ord: Order): (iterable: Iterable) => SortedMap + (iterable: Iterable, ord: Order): SortedMap +} = Dual.dual( + 2, + (iterable: Iterable, ord: Order): SortedMap => + makeImpl(RBT.fromIterable(iterable, ord)) +) + +/** + * @since 2.0.0 + * @category constructors + */ +export const make = + (ord: Order) => + >(...entries: Entries): SortedMap< + K, + Entries[number] extends (readonly [any, infer V]) ? V : never + > => fromIterable(ord)(entries) + +/** + * @since 2.0.0 + * @category predicates + */ +export const isEmpty = (self: SortedMap): boolean => size(self) === 0 + +/** + * @since 2.0.0 + * @category predicates + */ +export const isNonEmpty = (self: SortedMap): boolean => size(self) > 0 + +/** + * @since 2.0.0 + * @category elements + */ +export const get: { + (key: K): (self: SortedMap) => Option.Option + (self: SortedMap, key: K): Option.Option +} = Dual.dual< + (key: K) => (self: SortedMap) => Option.Option, + (self: SortedMap, key: K) => Option.Option +>(2, (self, key) => RBT.findFirst(self.tree, key)) + +/** + * Gets the `Order` that the `SortedMap` is using. + * + * @since 2.0.0 + * @category getters + */ +export const getOrder = (self: SortedMap): Order => RBT.getOrder(self.tree) + +/** + * @since 2.0.0 + * @category elements + */ +export const has: { + (key: K): (self: SortedMap) => boolean + (self: SortedMap, key: K): boolean +} = Dual.dual< + (key: K) => (self: SortedMap) => boolean, + (self: SortedMap, key: K) => boolean +>(2, (self, key) => Option.isSome(get(self, key))) + +/** + * @since 2.0.0 + * @category elements + */ +export const headOption = (self: SortedMap): Option.Option<[K, V]> => RBT.first(self.tree) + +/** + * @since 2.0.0 + * @category mapping + */ +export const map: { + (f: (a: A, k: K) => B): (self: SortedMap) => SortedMap + (self: SortedMap, f: (a: A, k: K) => B): SortedMap +} = Dual.dual< + (f: (a: A, k: K) => B) => (self: SortedMap) => SortedMap, + (self: SortedMap, f: (a: A, k: K) => B) => SortedMap +>(2, (self: SortedMap, f: (a: A, k: K) => B) => + reduce( + self, + empty(RBT.getOrder(self.tree)), + (acc, v, k) => set(acc, k, f(v, k)) + )) + +/** + * @since 2.0.0 + * @category folding + */ +export const reduce: { + (zero: B, f: (acc: B, value: A, key: K) => B): (self: SortedMap) => B + (self: SortedMap, zero: B, f: (acc: B, value: A, key: K) => B): B +} = Dual.dual< + (zero: B, f: (acc: B, value: A, key: K) => B) => (self: SortedMap) => B, + (self: SortedMap, zero: B, f: (acc: B, value: A, key: K) => B) => B +>(3, (self, zero, f) => RBT.reduce(self.tree, zero, f)) + +/** + * @since 2.0.0 + * @category elements + */ +export const remove: { + (key: K): (self: SortedMap) => SortedMap + (self: SortedMap, key: K): SortedMap +} = Dual.dual< + (key: K) => (self: SortedMap) => SortedMap, + (self: SortedMap, key: K) => SortedMap +>(2, (self, key) => makeImpl(RBT.removeFirst(self.tree, key))) + +/** + * @since 2.0.0 + * @category elements + */ +export const set: { + (key: K, value: V): (self: SortedMap) => SortedMap + (self: SortedMap, key: K, value: V): SortedMap +} = Dual.dual< + (key: K, value: V) => (self: SortedMap) => SortedMap, + (self: SortedMap, key: K, value: V) => SortedMap +>(3, (self, key, value) => + RBT.has(self.tree, key) + ? makeImpl(RBT.insert(RBT.removeFirst(self.tree, key), key, value)) + : makeImpl(RBT.insert(self.tree, key, value))) + +/** + * @since 2.0.0 + * @category getters + */ +export const size = (self: SortedMap): number => RBT.size(self.tree) + +/** + * @since 2.0.0 + * @category getters + */ +export const keys = (self: SortedMap): IterableIterator => RBT.keys(self.tree) + +/** + * @since 2.0.0 + * @category getters + */ +export const values = (self: SortedMap): IterableIterator => RBT.values(self.tree) + +/** + * @since 2.0.0 + * @category getters + */ +export const entries = (self: SortedMap): IterableIterator<[K, V]> => { + const iterator: any = self.tree[Symbol.iterator]() + iterator[Symbol.iterator] = () => entries(self) + return iterator +} + +/** + * @since 3.1.0 + * @category elements + */ +export const lastOption = (self: SortedMap): Option.Option<[K, V]> => RBT.last(self.tree) + +/** + * @since 3.1.0 + * @category filtering + */ +export const partition: { + ( + predicate: (a: Types.NoInfer) => boolean + ): (self: SortedMap) => [excluded: SortedMap, satisfying: SortedMap] + (self: SortedMap, predicate: (a: K) => boolean): [excluded: SortedMap, satisfying: SortedMap] +} = Dual.dual( + 2, + ( + self: SortedMap, + predicate: (a: K) => boolean + ): [excluded: SortedMap, satisfying: SortedMap] => { + const ord = RBT.getOrder(self.tree) + let right = empty(ord) + let left = empty(ord) + for (const value of self) { + if (predicate(value[0])) { + right = set(right, value[0], value[1]) + } else { + left = set(left, value[0], value[1]) + } + } + return [left, right] + } +) diff --git a/repos/effect/packages/effect/src/SortedSet.ts b/repos/effect/packages/effect/src/SortedSet.ts new file mode 100644 index 0000000..2bc50e5 --- /dev/null +++ b/repos/effect/packages/effect/src/SortedSet.ts @@ -0,0 +1,390 @@ +/** + * @since 2.0.0 + */ +import * as Equal from "./Equal.js" +import type * as Equivalence from "./Equivalence.js" +import * as Dual from "./Function.js" +import { pipe } from "./Function.js" +import * as Hash from "./Hash.js" +import type { Inspectable } from "./Inspectable.js" +import { format, NodeInspectSymbol, toJSON } from "./Inspectable.js" +import type { Order } from "./Order.js" +import type { Pipeable } from "./Pipeable.js" +import { pipeArguments } from "./Pipeable.js" +import type { Predicate } from "./Predicate.js" +import { hasProperty } from "./Predicate.js" +import * as RBT from "./RedBlackTree.js" +import type { Invariant, NoInfer } from "./Types.js" + +const TypeId: unique symbol = Symbol.for("effect/SortedSet") + +/** + * @since 2.0.0 + * @category symbol + */ +export type TypeId = typeof TypeId + +/** + * @since 2.0.0 + * @category models + */ +export interface SortedSet extends Iterable, Equal.Equal, Pipeable, Inspectable { + readonly [TypeId]: { + readonly _A: Invariant + } + /** @internal */ + readonly keyTree: RBT.RedBlackTree +} + +const SortedSetProto: Omit, "keyTree"> = { + [TypeId]: { + _A: (_: any) => _ + }, + [Hash.symbol](this: SortedSet): number { + return pipe( + Hash.hash(this.keyTree), + Hash.combine(Hash.hash(TypeId)), + Hash.cached(this) + ) + }, + [Equal.symbol](this: SortedSet, that: unknown): boolean { + return isSortedSet(that) && Equal.equals(this.keyTree, that.keyTree) + }, + [Symbol.iterator](this: SortedSet): Iterator { + return RBT.keys(this.keyTree) + }, + toString(this: SortedSet) { + return format(this.toJSON()) + }, + toJSON() { + return { + _id: "SortedSet", + values: Array.from(this).map(toJSON) + } + }, + [NodeInspectSymbol]() { + return this.toJSON() + }, + pipe() { + return pipeArguments(this, arguments) + } +} + +const fromTree = (keyTree: RBT.RedBlackTree): SortedSet => { + const a = Object.create(SortedSetProto) + a.keyTree = keyTree + return a +} + +/** + * @since 2.0.0 + * @category refinements + */ +export const isSortedSet: { + (u: Iterable): u is SortedSet + (u: unknown): u is SortedSet +} = (u: unknown): u is SortedSet => hasProperty(u, TypeId) + +/** + * @since 2.0.0 + * @category constructors + */ +export const empty = (O: Order): SortedSet => fromTree(RBT.empty(O)) + +/** + * Creates a new `SortedSet` from an iterable collection of values. + * + * @since 2.0.0 + * @category constructors + */ +export const fromIterable: { + (ord: Order): (iterable: Iterable) => SortedSet + (iterable: Iterable, ord: Order): SortedSet +} = Dual.dual( + 2, + (iterable: Iterable, ord: Order): SortedSet => + fromTree(RBT.fromIterable(Array.from(iterable).map((k) => [k, true]), ord)) +) + +/** + * @since 2.0.0 + * @category constructors + */ +export const make = + (ord: Order) => >(...entries: Entries): SortedSet => + fromIterable(entries, ord) + +/** + * @since 2.0.0 + * @category elements + */ +export const add: { + (value: A): (self: SortedSet) => SortedSet + (self: SortedSet, value: A): SortedSet +} = Dual.dual< + (value: A) => (self: SortedSet) => SortedSet, + (self: SortedSet, value: A) => SortedSet +>(2, (self, value) => + RBT.has(self.keyTree, value) + ? self + : fromTree(RBT.insert(self.keyTree, value, true))) + +/** + * @since 2.0.0 + */ +export const difference: { + (that: Iterable): (self: SortedSet) => SortedSet + (self: SortedSet, that: Iterable): SortedSet +} = Dual.dual< + (that: Iterable) => (self: SortedSet) => SortedSet, + (self: SortedSet, that: Iterable) => SortedSet +>(2, (self: SortedSet, that: Iterable) => { + let out = self + for (const value of that) { + out = remove(out, value) + } + return out +}) + +/** + * Check if a predicate holds true for every `SortedSet` element. + * + * @since 2.0.0 + * @category elements + */ +export const every: { + (predicate: Predicate): (self: SortedSet) => boolean + (self: SortedSet, predicate: Predicate): boolean +} = Dual.dual(2, (self: SortedSet, predicate: Predicate): boolean => { + for (const value of self) { + if (!predicate(value)) { + return false + } + } + return true +}) + +/** + * @since 2.0.0 + * @category filtering + */ +export const filter: { + (predicate: Predicate): (self: SortedSet) => SortedSet + (self: SortedSet, predicate: Predicate): SortedSet +} = Dual.dual(2, (self: SortedSet, predicate: Predicate): SortedSet => { + const ord = RBT.getOrder(self.keyTree) + let out = empty(ord) + for (const value of self) { + if (predicate(value)) { + out = add(out, value) + } + } + return out +}) + +/** + * @since 2.0.0 + * @category sequencing + */ +export const flatMap: { + (O: Order, f: (a: A) => Iterable): (self: SortedSet) => SortedSet + (self: SortedSet, O: Order, f: (a: A) => Iterable): SortedSet +} = Dual.dual< + (O: Order, f: (a: A) => Iterable) => (self: SortedSet) => SortedSet, + (self: SortedSet, O: Order, f: (a: A) => Iterable) => SortedSet +>(3, (self, O, f) => { + let out = empty(O) + forEach(self, (a) => { + for (const b of f(a)) { + out = add(out, b) + } + }) + return out +}) + +/** + * @since 2.0.0 + * @category traversing + */ +export const forEach: { + (f: (a: A) => void): (self: SortedSet) => void + (self: SortedSet, f: (a: A) => void): void +} = Dual.dual< + (f: (a: A) => void) => (self: SortedSet) => void, + (self: SortedSet, f: (a: A) => void) => void +>(2, (self, f) => RBT.forEach(self.keyTree, f)) + +/** + * @since 2.0.0 + * @category elements + */ +export const has: { + (value: A): (self: SortedSet) => boolean + (self: SortedSet, value: A): boolean +} = Dual.dual< + (value: A) => (self: SortedSet) => boolean, + (self: SortedSet, value: A) => boolean +>(2, (self, value) => RBT.has(self.keyTree, value)) + +/** + * @since 2.0.0 + */ +export const intersection: { + (that: Iterable): (self: SortedSet) => SortedSet + (self: SortedSet, that: Iterable): SortedSet +} = Dual.dual< + (that: Iterable) => (self: SortedSet) => SortedSet, + (self: SortedSet, that: Iterable) => SortedSet +>(2, (self, that) => { + const ord = RBT.getOrder(self.keyTree) + let out = empty(ord) + for (const value of that) { + if (has(self, value)) { + out = add(out, value) + } + } + return out +}) + +/** + * @since 2.0.0 + * @category elements + */ +export const isSubset: { + (that: SortedSet): (self: SortedSet) => boolean + (self: SortedSet, that: SortedSet): boolean +} = Dual.dual< + (that: SortedSet) => (self: SortedSet) => boolean, + (self: SortedSet, that: SortedSet) => boolean +>(2, (self, that) => every(self, (a) => has(that, a))) + +/** + * @since 2.0.0 + * @category mapping + */ +export const map: { + (O: Order, f: (a: A) => B): (self: SortedSet) => SortedSet + (self: SortedSet, O: Order, f: (a: A) => B): SortedSet +} = Dual.dual< + (O: Order, f: (a: A) => B) => (self: SortedSet) => SortedSet, + (self: SortedSet, O: Order, f: (a: A) => B) => SortedSet +>(3, (self, O, f) => { + let out = empty(O) + forEach(self, (a) => { + const b = f(a) + if (!has(out, b)) { + out = add(out, b) + } + }) + return out +}) + +/** + * @since 2.0.0 + * @category filtering + */ +export const partition: { + ( + predicate: (a: NoInfer) => boolean + ): (self: SortedSet) => [excluded: SortedSet, satisfying: SortedSet] + (self: SortedSet, predicate: (a: A) => boolean): [excluded: SortedSet, satisfying: SortedSet] +} = Dual.dual( + 2, + (self: SortedSet, predicate: (a: A) => boolean): [excluded: SortedSet, satisfying: SortedSet] => { + const ord = RBT.getOrder(self.keyTree) + let right = empty(ord) + let left = empty(ord) + for (const value of self) { + if (predicate(value)) { + right = add(right, value) + } else { + left = add(left, value) + } + } + return [left, right] + } +) + +/** + * @since 2.0.0 + * @category elements + */ +export const remove: { + (value: A): (self: SortedSet) => SortedSet + (self: SortedSet, value: A): SortedSet +} = Dual.dual< + (value: A) => (self: SortedSet) => SortedSet, + (self: SortedSet, value: A) => SortedSet +>(2, (self, value) => fromTree(RBT.removeFirst(self.keyTree, value))) + +/** + * @since 2.0.0 + * @category getters + */ +export const size = (self: SortedSet): number => RBT.size(self.keyTree) + +/** + * Check if a predicate holds true for some `SortedSet` element. + * + * @since 2.0.0 + * @category elements + */ +export const some: { + (predicate: Predicate): (self: SortedSet) => boolean + (self: SortedSet, predicate: Predicate): boolean +} = Dual.dual< + (predicate: Predicate) => (self: SortedSet) => boolean, + (self: SortedSet, predicate: Predicate) => boolean +>(2, (self, predicate) => { + for (const value of self) { + if (predicate(value)) { + return true + } + } + return false +}) + +/** + * @since 2.0.0 + * @category elements + */ +export const toggle: { + (value: A): (self: SortedSet) => SortedSet + (self: SortedSet, value: A): SortedSet +} = Dual.dual< + (value: A) => (self: SortedSet) => SortedSet, + (self: SortedSet, value: A) => SortedSet +>(2, (self, value) => has(self, value) ? remove(self, value) : add(self, value)) + +/** + * @since 2.0.0 + */ +export const union: { + (that: Iterable): (self: SortedSet) => SortedSet + (self: SortedSet, that: Iterable): SortedSet +} = Dual.dual< + (that: Iterable) => (self: SortedSet) => SortedSet, + (self: SortedSet, that: Iterable) => SortedSet +>(2, (self: SortedSet, that: Iterable) => { + const ord = RBT.getOrder(self.keyTree) + let out = empty(ord) + for (const value of self) { + out = add(value)(out) + } + for (const value of that) { + out = add(value)(out) + } + return out +}) + +/** + * @since 2.0.0 + * @category getters + */ +export const values = (self: SortedSet): IterableIterator => RBT.keys(self.keyTree) + +/** + * @since 2.0.0 + * @category equivalence + */ +export const getEquivalence = (): Equivalence.Equivalence> => (a, b) => isSubset(a, b) && isSubset(b, a) diff --git a/repos/effect/packages/effect/src/Stream.ts b/repos/effect/packages/effect/src/Stream.ts new file mode 100644 index 0000000..3863a8e --- /dev/null +++ b/repos/effect/packages/effect/src/Stream.ts @@ -0,0 +1,6468 @@ +/** + * @since 2.0.0 + */ +import type * as Cause from "./Cause.js" +import type * as Channel from "./Channel.js" +import type * as Chunk from "./Chunk.js" +import type * as Context from "./Context.js" +import type * as Deferred from "./Deferred.js" +import type * as Duration from "./Duration.js" +import type * as Effect from "./Effect.js" +import type * as Either from "./Either.js" +import type { ExecutionPlan } from "./ExecutionPlan.js" +import type * as Exit from "./Exit.js" +import type { LazyArg } from "./Function.js" +import type * as GroupBy from "./GroupBy.js" +import type { TypeLambda } from "./HKT.js" +import * as groupBy_ from "./internal/groupBy.js" +import * as internal from "./internal/stream.js" +import type * as Layer from "./Layer.js" +import type * as Option from "./Option.js" +import type * as Order from "./Order.js" +import type { Pipeable } from "./Pipeable.js" +import type { Predicate, Refinement } from "./Predicate.js" +import type * as PubSub from "./PubSub.js" +import type * as Queue from "./Queue.js" +import type { Runtime } from "./Runtime.js" +import type * as Schedule from "./Schedule.js" +import type * as Scope from "./Scope.js" +import type * as Sink from "./Sink.js" +import type * as Emit from "./StreamEmit.js" +import type * as HaltStrategy from "./StreamHaltStrategy.js" +import type * as Take from "./Take.js" +import type { TPubSub } from "./TPubSub.js" +import type { TDequeue } from "./TQueue.js" +import type * as Tracer from "./Tracer.js" +import type { Covariant, NoInfer, TupleOf } from "./Types.js" +import type * as Unify from "./Unify.js" + +/** + * @since 2.0.0 + * @category symbols + */ +export const StreamTypeId: unique symbol = internal.StreamTypeId + +/** + * @since 2.0.0 + * @category symbols + */ +export type StreamTypeId = typeof StreamTypeId + +/** + * A `Stream` is a description of a program that, when evaluated, may + * emit zero or more values of type `A`, may fail with errors of type `E`, and + * uses an context of type `R`. One way to think of `Stream` is as a + * `Effect` program that could emit multiple values. + * + * `Stream` is a purely functional *pull* based stream. Pull based streams offer + * inherent laziness and backpressure, relieving users of the need to manage + * buffers between operators. As an optimization, `Stream` does not emit + * single values, but rather an array of values. This allows the cost of effect + * evaluation to be amortized. + * + * `Stream` forms a monad on its `A` type parameter, and has error management + * facilities for its `E` type parameter, modeled similarly to `Effect` (with + * some adjustments for the multiple-valued nature of `Stream`). These aspects + * allow for rich and expressive composition of streams. + * + * @since 2.0.0 + * @category models + */ +export interface Stream extends Stream.Variance, Pipeable { + [Unify.typeSymbol]?: unknown + [Unify.unifySymbol]?: StreamUnify + [Unify.ignoreSymbol]?: StreamUnifyIgnore +} + +/** + * @since 2.0.0 + * @category models + */ +export interface StreamUnify extends Effect.EffectUnify { + Stream?: () => A[Unify.typeSymbol] extends Stream | infer _ ? Stream : never +} + +/** + * @category models + * @since 2.0.0 + */ +export interface StreamUnifyIgnore extends Effect.EffectUnifyIgnore { + Effect?: true +} + +/** + * @since 2.0.0 + * @category models + */ +declare module "./Effect.js" { + interface Effect extends Stream {} +} + +/** + * @category type lambdas + * @since 2.0.0 + */ +export interface StreamTypeLambda extends TypeLambda { + readonly type: Stream +} + +/** + * @since 2.0.0 + */ +export declare namespace Stream { + /** + * @since 2.0.0 + * @category models + */ + export interface Variance { + readonly [StreamTypeId]: VarianceStruct + } + + /** + * @since 3.4.0 + * @category models + */ + export interface VarianceStruct { + readonly _A: Covariant + readonly _E: Covariant + readonly _R: Covariant + } + + /** + * @since 3.4.0 + * @category type-level + */ + export type Success> = [T] extends [Stream] ? _A : never + + /** + * @since 3.4.0 + * @category type-level + */ + export type Error> = [T] extends [Stream] ? _E : never + + /** + * @since 3.4.0 + * @category type-level + */ + export type Context> = [T] extends [Stream] ? _R : never + + /** + * @since 2.0.0 + * @category models + * @deprecated use Types.TupleOf instead + */ + export type DynamicTuple = N extends N ? number extends N ? Array : DynamicTupleOf + : never + + /** + * @since 2.0.0 + * @category models + * @deprecated use Types.TupleOf instead + */ + export type DynamicTupleOf> = R["length"] extends N ? R + : DynamicTupleOf +} + +/** + * The default chunk size used by the various combinators and constructors of + * `Stream`. + * + * @since 2.0.0 + * @category constants + */ +export const DefaultChunkSize: number = internal.DefaultChunkSize + +/** + * Collects each underlying Chunk of the stream into a new chunk, and emits it + * on each pull. + * + * @since 2.0.0 + * @category utils + */ +export const accumulate: (self: Stream) => Stream, E, R> = internal.accumulate + +/** + * Re-chunks the elements of the stream by accumulating each underlying chunk. + * + * @since 2.0.0 + * @category utils + */ +export const accumulateChunks: (self: Stream) => Stream = internal.accumulateChunks + +/** + * Creates a stream from a single value that will get cleaned up after the + * stream is consumed. + * + * @example + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * // Simulating File operations + * const open = (filename: string) => + * Effect.gen(function*() { + * yield* Console.log(`Opening ${filename}`) + * return { + * getLines: Effect.succeed(["Line 1", "Line 2", "Line 3"]), + * close: Console.log(`Closing ${filename}`) + * } + * }) + * + * const stream = Stream.acquireRelease( + * open("file.txt"), + * (file) => file.close + * ).pipe(Stream.flatMap((file) => file.getLines)) + * + * Effect.runPromise(Stream.runCollect(stream)).then(console.log) + * // Opening file.txt + * // Closing file.txt + * // { _id: 'Chunk', values: [ [ 'Line 1', 'Line 2', 'Line 3' ] ] } + * ``` + * + * @since 2.0.0 + * @category constructors + */ +export const acquireRelease: ( + acquire: Effect.Effect, + release: (resource: A, exit: Exit.Exit) => Effect.Effect +) => Stream = internal.acquireRelease + +/** + * Aggregates elements of this stream using the provided sink for as long as + * the downstream operators on the stream are busy. + * + * This operator divides the stream into two asynchronous "islands". Operators + * upstream of this operator run on one fiber, while downstream operators run + * on another. Whenever the downstream fiber is busy processing elements, the + * upstream fiber will feed elements into the sink until it signals + * completion. + * + * Any sink can be used here, but see `Sink.foldWeightedEffect` and + * `Sink.foldUntilEffect` for sinks that cover the common usecases. + * + * @since 2.0.0 + * @category utils + */ +export const aggregate: { + (sink: Sink.Sink): (self: Stream) => Stream + (self: Stream, sink: Sink.Sink): Stream +} = internal.aggregate + +/** + * Like {@link aggregateWithinEither}, but only returns the `Right` results. + * + * @since 2.0.0 + * @category utils + */ +export const aggregateWithin: { + ( + sink: Sink.Sink, + schedule: Schedule.Schedule, R3> + ): (self: Stream) => Stream + ( + self: Stream, + sink: Sink.Sink, + schedule: Schedule.Schedule, R3> + ): Stream +} = internal.aggregateWithin + +/** + * Aggregates elements using the provided sink until it completes, or until + * the delay signalled by the schedule has passed. + * + * This operator divides the stream into two asynchronous islands. Operators + * upstream of this operator run on one fiber, while downstream operators run + * on another. Elements will be aggregated by the sink until the downstream + * fiber pulls the aggregated value, or until the schedule's delay has passed. + * + * Aggregated elements will be fed into the schedule to determine the delays + * between pulls. + * + * @since 2.0.0 + * @category utils + */ +export const aggregateWithinEither: { + ( + sink: Sink.Sink, + schedule: Schedule.Schedule, R3> + ): (self: Stream) => Stream, E2 | E, R2 | R3 | R> + ( + self: Stream, + sink: Sink.Sink, + schedule: Schedule.Schedule, R3> + ): Stream, E | E2, R | R2 | R3> +} = internal.aggregateWithinEither + +/** + * Maps the success values of this stream to the specified constant value. + * + * @example + * ```ts + * import { Effect, Stream } from "effect" + * + * const stream = Stream.range(1, 5).pipe(Stream.as(null)) + * + * Effect.runPromise(Stream.runCollect(stream)).then(console.log) + * // { _id: 'Chunk', values: [ null, null, null, null, null ] } + * ``` + * + * @since 2.0.0 + * @category mapping + */ +export const as: { + (value: B): (self: Stream) => Stream + (self: Stream, value: B): Stream +} = internal.as + +const _async: ( + register: (emit: Emit.Emit) => Effect.Effect | void, + bufferSize?: number | "unbounded" | { + readonly bufferSize?: number | undefined + readonly strategy?: "dropping" | "sliding" | "suspend" | undefined + } | undefined +) => Stream = internal._async + +export { + /** + * Creates a stream from an asynchronous callback that can be called multiple + * times. The optionality of the error type `E` in `Emit` can be used to + * signal the end of the stream by setting it to `None`. + * + * The registration function can optionally return an `Effect`, which will be + * executed if the `Fiber` executing this Effect is interrupted. + * + * @example + * ```ts + * import type { StreamEmit } from "effect" + * import { Chunk, Effect, Option, Stream } from "effect" + * + * const events = [1, 2, 3, 4] + * + * const stream = Stream.async( + * (emit: StreamEmit.Emit) => { + * events.forEach((n) => { + * setTimeout(() => { + * if (n === 3) { + * emit(Effect.fail(Option.none())) // Terminate the stream + * } else { + * emit(Effect.succeed(Chunk.of(n))) // Add the current item to the stream + * } + * }, 100 * n) + * }) + * } + * ) + * + * Effect.runPromise(Stream.runCollect(stream)).then(console.log) + * // { _id: 'Chunk', values: [ 1, 2 ] } + * + * ``` + * @since 2.0.0 + * @category constructors + */ + _async as async +} + +/** + * Creates a stream from an asynchronous callback that can be called multiple + * times The registration of the callback itself returns an effect. The + * optionality of the error type `E` can be used to signal the end of the + * stream, by setting it to `None`. + * + * @since 2.0.0 + * @category constructors + */ +export const asyncEffect: ( + register: (emit: Emit.Emit) => Effect.Effect, + bufferSize?: number | "unbounded" | { + readonly bufferSize?: number | undefined + readonly strategy?: "dropping" | "sliding" | "suspend" | undefined + } | undefined +) => Stream = internal.asyncEffect + +/** + * Creates a stream from an external push-based resource. + * + * You can use the `emit` helper to emit values to the stream. The `emit` helper + * returns a boolean indicating whether the value was emitted or not. + * + * You can also use the `emit` helper to signal the end of the stream by + * using apis such as `emit.end` or `emit.fail`. + * + * By default it uses an "unbounded" buffer size. + * You can customize the buffer size and strategy by passing an object as the + * second argument with the `bufferSize` and `strategy` fields. + * + * @example + * ```ts + * import { Effect, Stream } from "effect" + * + * Stream.asyncPush((emit) => + * Effect.acquireRelease( + * Effect.gen(function*() { + * yield* Effect.log("subscribing") + * return setInterval(() => emit.single("tick"), 1000) + * }), + * (handle) => + * Effect.gen(function*() { + * yield* Effect.log("unsubscribing") + * clearInterval(handle) + * }) + * ), { bufferSize: 16, strategy: "dropping" }) + * ``` + * + * @since 3.6.0 + * @category constructors + */ +export const asyncPush: ( + register: (emit: Emit.EmitOpsPush) => Effect.Effect, + options?: { readonly bufferSize: "unbounded" } | { + readonly bufferSize?: number | undefined + readonly strategy?: "dropping" | "sliding" | undefined + } | undefined +) => Stream> = internal.asyncPush + +/** + * Creates a stream from an asynchronous callback that can be called multiple + * times. The registration of the callback itself returns an a scoped + * resource. The optionality of the error type `E` can be used to signal the + * end of the stream, by setting it to `None`. + * + * @since 2.0.0 + * @category constructors + */ +export const asyncScoped: ( + register: (emit: Emit.Emit) => Effect.Effect, + bufferSize?: number | "unbounded" | { + readonly bufferSize?: number | undefined + readonly strategy?: "dropping" | "sliding" | "suspend" | undefined + } | undefined +) => Stream> = internal.asyncScoped + +/** + * Returns a `Stream` that first collects `n` elements from the input `Stream`, + * and then creates a new `Stream` using the specified function, and sends all + * the following elements through that. + * + * @since 2.0.0 + * @category sequencing + */ +export const branchAfter: { + ( + n: number, + f: (input: Chunk.Chunk) => Stream + ): (self: Stream) => Stream + ( + self: Stream, + n: number, + f: (input: Chunk.Chunk) => Stream + ): Stream +} = internal.branchAfter + +/** + * Fan out the stream, producing a list of streams that have the same elements + * as this stream. The driver stream will only ever advance the `maximumLag` + * chunks before the slowest downstream stream. + * + * @example + * ```ts + * import { Console, Effect, Fiber, Schedule, Stream } from "effect" + * + * const numbers = Effect.scoped( + * Stream.range(1, 20).pipe( + * Stream.tap((n) => Console.log(`Emit ${n} element before broadcasting`)), + * Stream.broadcast(2, 5), + * Stream.flatMap(([first, second]) => + * Effect.gen(function*() { + * const fiber1 = yield* Stream.runFold(first, 0, (acc, e) => Math.max(acc, e)).pipe( + * Effect.andThen((max) => Console.log(`Maximum: ${max}`)), + * Effect.fork + * ) + * const fiber2 = yield* second.pipe( + * Stream.schedule(Schedule.spaced("1 second")), + * Stream.runForEach((n) => Console.log(`Logging to the Console: ${n}`)), + * Effect.fork + * ) + * yield* Fiber.join(fiber1).pipe( + * Effect.zip(Fiber.join(fiber2), { concurrent: true }) + * ) + * }) + * ), + * Stream.runCollect + * ) + * ) + * + * Effect.runPromise(numbers).then(console.log) + * // Emit 1 element before broadcasting + * // Emit 2 element before broadcasting + * // Emit 3 element before broadcasting + * // Emit 4 element before broadcasting + * // Emit 5 element before broadcasting + * // Emit 6 element before broadcasting + * // Emit 7 element before broadcasting + * // Emit 8 element before broadcasting + * // Emit 9 element before broadcasting + * // Emit 10 element before broadcasting + * // Emit 11 element before broadcasting + * // Logging to the Console: 1 + * // Logging to the Console: 2 + * // Logging to the Console: 3 + * // Logging to the Console: 4 + * // Logging to the Console: 5 + * // Emit 12 element before broadcasting + * // Emit 13 element before broadcasting + * // Emit 14 element before broadcasting + * // Emit 15 element before broadcasting + * // Emit 16 element before broadcasting + * // Logging to the Console: 6 + * // Logging to the Console: 7 + * // Logging to the Console: 8 + * // Logging to the Console: 9 + * // Logging to the Console: 10 + * // Emit 17 element before broadcasting + * // Emit 18 element before broadcasting + * // Emit 19 element before broadcasting + * // Emit 20 element before broadcasting + * // Logging to the Console: 11 + * // Logging to the Console: 12 + * // Logging to the Console: 13 + * // Logging to the Console: 14 + * // Logging to the Console: 15 + * // Maximum: 20 + * // Logging to the Console: 16 + * // Logging to the Console: 17 + * // Logging to the Console: 18 + * // Logging to the Console: 19 + * // Logging to the Console: 20 + * // { _id: 'Chunk', values: [ undefined ] } + * ``` + * + * @since 2.0.0 + * @category utils + */ +export const broadcast: { + ( + n: N, + maximumLag: number | { readonly capacity: "unbounded"; readonly replay?: number | undefined } | { + readonly capacity: number + readonly strategy?: "sliding" | "dropping" | "suspend" | undefined + readonly replay?: number | undefined + } + ): (self: Stream) => Effect.Effect>, never, Scope.Scope | R> + ( + self: Stream, + n: N, + maximumLag: number | { readonly capacity: "unbounded"; readonly replay?: number | undefined } | { + readonly capacity: number + readonly strategy?: "sliding" | "dropping" | "suspend" | undefined + readonly replay?: number | undefined + } + ): Effect.Effect>, never, Scope.Scope | R> +} = internal.broadcast + +/** + * Returns a new Stream that multicasts the original Stream, subscribing to it as soon as the first consumer subscribes. + * As long as there is at least one consumer, the upstream will continue running and emitting data. + * When all consumers have exited, the upstream will be finalized. + * + * @since 3.8.0 + * @category utils + */ +export const share: { + ( + config: { + readonly capacity: "unbounded" + readonly replay?: number | undefined + readonly idleTimeToLive?: Duration.DurationInput | undefined + } | { + readonly capacity: number + readonly strategy?: "sliding" | "dropping" | "suspend" | undefined + readonly replay?: number | undefined + readonly idleTimeToLive?: Duration.DurationInput | undefined + } + ): (self: Stream) => Effect.Effect, never, R | Scope.Scope> + ( + self: Stream, + config: { + readonly capacity: "unbounded" + readonly replay?: number | undefined + readonly idleTimeToLive?: Duration.DurationInput | undefined + } | { + readonly capacity: number + readonly strategy?: "sliding" | "dropping" | "suspend" | undefined + readonly replay?: number | undefined + readonly idleTimeToLive?: Duration.DurationInput | undefined + } + ): Effect.Effect, never, R | Scope.Scope> +} = internal.share + +/** + * Fan out the stream, producing a dynamic number of streams that have the + * same elements as this stream. The driver stream will only ever advance the + * `maximumLag` chunks before the slowest downstream stream. + * + * @since 2.0.0 + * @category utils + */ +export const broadcastDynamic: { + ( + maximumLag: number | { readonly capacity: "unbounded"; readonly replay?: number | undefined } | { + readonly capacity: number + readonly strategy?: "sliding" | "dropping" | "suspend" | undefined + readonly replay?: number | undefined + } + ): (self: Stream) => Effect.Effect, never, Scope.Scope | R> + ( + self: Stream, + maximumLag: number | { readonly capacity: "unbounded"; readonly replay?: number | undefined } | { + readonly capacity: number + readonly strategy?: "sliding" | "dropping" | "suspend" | undefined + readonly replay?: number | undefined + } + ): Effect.Effect, never, Scope.Scope | R> +} = internal.broadcastDynamic + +/** + * Converts the stream to a scoped list of queues. Every value will be + * replicated to every queue with the slowest queue being allowed to buffer + * `maximumLag` chunks before the driver is back pressured. + * + * Queues can unsubscribe from upstream by shutting down. + * + * @since 2.0.0 + * @category utils + */ +export const broadcastedQueues: { + ( + n: N, + maximumLag: number | { readonly capacity: "unbounded"; readonly replay?: number | undefined } | { + readonly capacity: number + readonly strategy?: "sliding" | "dropping" | "suspend" | undefined + readonly replay?: number | undefined + } + ): ( + self: Stream + ) => Effect.Effect>>, never, Scope.Scope | R> + ( + self: Stream, + n: N, + maximumLag: number | { readonly capacity: "unbounded"; readonly replay?: number | undefined } | { + readonly capacity: number + readonly strategy?: "sliding" | "dropping" | "suspend" | undefined + readonly replay?: number | undefined + } + ): Effect.Effect>>, never, Scope.Scope | R> +} = internal.broadcastedQueues + +/** + * Converts the stream to a scoped dynamic amount of queues. Every chunk will + * be replicated to every queue with the slowest queue being allowed to buffer + * `maximumLag` chunks before the driver is back pressured. + * + * Queues can unsubscribe from upstream by shutting down. + * + * @since 2.0.0 + * @category utils + */ +export const broadcastedQueuesDynamic: { + ( + maximumLag: number | { readonly capacity: "unbounded"; readonly replay?: number | undefined } | { + readonly capacity: number + readonly strategy?: "sliding" | "dropping" | "suspend" | undefined + readonly replay?: number | undefined + } + ): ( + self: Stream + ) => Effect.Effect>, never, Scope.Scope>, never, Scope.Scope | R> + ( + self: Stream, + maximumLag: number | { readonly capacity: "unbounded"; readonly replay?: number | undefined } | { + readonly capacity: number + readonly strategy?: "sliding" | "dropping" | "suspend" | undefined + readonly replay?: number | undefined + } + ): Effect.Effect>, never, Scope.Scope>, never, Scope.Scope | R> +} = internal.broadcastedQueuesDynamic + +/** + * Allows a faster producer to progress independently of a slower consumer by + * buffering up to `capacity` elements in a queue. + * + * Note: This combinator destroys the chunking structure. It's recommended to + * use rechunk afterwards. Additionally, prefer capacities that are powers + * of 2 for better performance. + * + * @example + * ```ts + * import { Console, Effect, Schedule, Stream } from "effect" + * + * const stream = Stream.range(1, 10).pipe( + * Stream.tap((n) => Console.log(`before buffering: ${n}`)), + * Stream.buffer({ capacity: 4 }), + * Stream.tap((n) => Console.log(`after buffering: ${n}`)), + * Stream.schedule(Schedule.spaced("5 seconds")) + * ) + * + * Effect.runPromise(Stream.runCollect(stream)).then(console.log) + * // before buffering: 1 + * // before buffering: 2 + * // before buffering: 3 + * // before buffering: 4 + * // before buffering: 5 + * // before buffering: 6 + * // after buffering: 1 + * // after buffering: 2 + * // before buffering: 7 + * // after buffering: 3 + * // before buffering: 8 + * // after buffering: 4 + * // before buffering: 9 + * // after buffering: 5 + * // before buffering: 10 + * // ... + * ``` + * + * @since 2.0.0 + * @category utils + */ +export const buffer: { + ( + options: { readonly capacity: "unbounded" } | { + readonly capacity: number + readonly strategy?: "dropping" | "sliding" | "suspend" | undefined + } + ): (self: Stream) => Stream + ( + self: Stream, + options: { readonly capacity: "unbounded" } | { + readonly capacity: number + readonly strategy?: "dropping" | "sliding" | "suspend" | undefined + } + ): Stream +} = internal.buffer + +/** + * Allows a faster producer to progress independently of a slower consumer by + * buffering up to `capacity` chunks in a queue. + * + * @note Prefer capacities that are powers of 2 for better performance. + * @since 2.0.0 + * @category utils + */ +export const bufferChunks: { + ( + options: { readonly capacity: number; readonly strategy?: "dropping" | "sliding" | "suspend" | undefined } + ): (self: Stream) => Stream + ( + self: Stream, + options: { readonly capacity: number; readonly strategy?: "dropping" | "sliding" | "suspend" | undefined } + ): Stream +} = internal.bufferChunks + +/** + * Switches over to the stream produced by the provided function in case this + * one fails with a typed error. + * + * @since 2.0.0 + * @category error handling + */ +export const catchAll: { + (f: (error: E) => Stream): (self: Stream) => Stream + (self: Stream, f: (error: E) => Stream): Stream +} = internal.catchAll + +/** + * Switches over to the stream produced by the provided function in case this + * one fails. Allows recovery from all causes of failure, including + * interruption if the stream is uninterruptible. + * + * @since 2.0.0 + * @category error handling + */ +export const catchAllCause: { + ( + f: (cause: Cause.Cause) => Stream + ): (self: Stream) => Stream + ( + self: Stream, + f: (cause: Cause.Cause) => Stream + ): Stream +} = internal.catchAllCause + +/** + * Switches over to the stream produced by the provided function in case this + * one fails with some typed error. + * + * @since 2.0.0 + * @category error handling + */ +export const catchSome: { + ( + pf: (error: E) => Option.Option> + ): (self: Stream) => Stream + ( + self: Stream, + pf: (error: E) => Option.Option> + ): Stream +} = internal.catchSome + +/** + * Switches over to the stream produced by the provided function in case this + * one fails with an error matching the given `_tag`. + * + * @since 2.0.0 + * @category error handling + */ +export const catchTag: { + ( + k: K, + f: (e: Extract) => Stream + ): (self: Stream) => Stream, R1 | R> + ( + self: Stream, + k: K, + f: (e: Extract) => Stream + ): Stream, R | R1> +} = internal.catchTag + +/** + * Switches over to the stream produced by one of the provided functions, in + * case this one fails with an error matching one of the given `_tag`'s. + * + * @since 2.0.0 + * @category error handling + */ +export const catchTags: { + < + E extends { _tag: string }, + Cases extends { [K in E["_tag"]]+?: (error: Extract) => Stream } + >( + cases: Cases + ): ( + self: Stream + ) => Stream< + | A + | { + [K in keyof Cases]: Cases[K] extends (...args: Array) => Stream.Variance ? A + : never + }[keyof Cases], + | Exclude + | { + [K in keyof Cases]: Cases[K] extends (...args: Array) => Stream.Variance ? E + : never + }[keyof Cases], + | R + | { + [K in keyof Cases]: Cases[K] extends (...args: Array) => Stream.Variance ? R + : never + }[keyof Cases] + > + < + A, + E extends { _tag: string }, + R, + Cases extends { [K in E["_tag"]]+?: (error: Extract) => Stream } + >( + self: Stream, + cases: Cases + ): Stream< + | A + | { + [K in keyof Cases]: Cases[K] extends (...args: Array) => Stream.Variance ? A + : never + }[keyof Cases], + | Exclude + | { + [K in keyof Cases]: Cases[K] extends (...args: Array) => Stream.Variance ? E + : never + }[keyof Cases], + | R + | { + [K in keyof Cases]: Cases[K] extends (...args: Array) => Stream.Variance ? R + : never + }[keyof Cases] + > +} = internal.catchTags + +/** + * Switches over to the stream produced by the provided function in case this + * one fails with some errors. Allows recovery from all causes of failure, + * including interruption if the stream is uninterruptible. + * + * @since 2.0.0 + * @category error handling + */ +export const catchSomeCause: { + ( + pf: (cause: Cause.Cause) => Option.Option> + ): (self: Stream) => Stream + ( + self: Stream, + pf: (cause: Cause.Cause) => Option.Option> + ): Stream +} = internal.catchSomeCause + +/** + * Returns a new stream that only emits elements that are not equal to the + * previous element emitted, using natural equality to determine whether two + * elements are equal. + * + * @example + * ```ts + * import { Effect, Stream } from "effect" + * + * const stream = Stream.make(1, 1, 1, 2, 2, 3, 4).pipe(Stream.changes) + * + * Effect.runPromise(Stream.runCollect(stream)).then(console.log) + * // { _id: 'Chunk', values: [ 1, 2, 3, 4 ] } + * ``` + * + * @since 2.0.0 + * @category utils + */ +export const changes: (self: Stream) => Stream = internal.changes + +/** + * Returns a new stream that only emits elements that are not equal to the + * previous element emitted, using the specified function to determine whether + * two elements are equal. + * + * @since 2.0.0 + * @category utils + */ +export const changesWith: { + (f: (x: A, y: A) => boolean): (self: Stream) => Stream + (self: Stream, f: (x: A, y: A) => boolean): Stream +} = internal.changesWith + +/** + * Returns a new stream that only emits elements that are not equal to the + * previous element emitted, using the specified effectual function to + * determine whether two elements are equal. + * + * @since 2.0.0 + * @category utils + */ +export const changesWithEffect: { + ( + f: (x: A, y: A) => Effect.Effect + ): (self: Stream) => Stream + (self: Stream, f: (x: A, y: A) => Effect.Effect): Stream +} = internal.changesWithEffect + +/** + * Exposes the underlying chunks of the stream as a stream of chunks of + * elements. + * + * @since 2.0.0 + * @category utils + */ +export const chunks: (self: Stream) => Stream, E, R> = internal.chunks + +/** + * Performs the specified stream transformation with the chunk structure of + * the stream exposed. + * + * @since 2.0.0 + * @category utils + */ +export const chunksWith: { + ( + f: (stream: Stream, E, R>) => Stream, E2, R2> + ): (self: Stream) => Stream + ( + self: Stream, + f: (stream: Stream, E, R>) => Stream, E2, R2> + ): Stream +} = internal.chunksWith + +/** + * Combines the elements from this stream and the specified stream by + * repeatedly applying the function `f` to extract an element using both sides + * and conceptually "offer" it to the destination stream. `f` can maintain + * some internal state to control the combining process, with the initial + * state being specified by `s`. + * + * Where possible, prefer `Stream.combineChunks` for a more efficient + * implementation. + * + * @since 2.0.0 + * @category utils + */ +export const combine: { + ( + that: Stream, + s: S, + f: ( + s: S, + pullLeft: Effect.Effect, R3>, + pullRight: Effect.Effect, R4> + ) => Effect.Effect>, never, R5> + ): (self: Stream) => Stream + ( + self: Stream, + that: Stream, + s: S, + f: ( + s: S, + pullLeft: Effect.Effect, R3>, + pullRight: Effect.Effect, R4> + ) => Effect.Effect>, never, R5> + ): Stream +} = internal.combine + +/** + * Combines the chunks from this stream and the specified stream by repeatedly + * applying the function `f` to extract a chunk using both sides and + * conceptually "offer" it to the destination stream. `f` can maintain some + * internal state to control the combining process, with the initial state + * being specified by `s`. + * + * @since 2.0.0 + * @category utils + */ +export const combineChunks: { + ( + that: Stream, + s: S, + f: ( + s: S, + pullLeft: Effect.Effect, Option.Option, R3>, + pullRight: Effect.Effect, Option.Option, R4> + ) => Effect.Effect, S], Option.Option>, never, R5> + ): (self: Stream) => Stream + ( + self: Stream, + that: Stream, + s: S, + f: ( + s: S, + pullLeft: Effect.Effect, Option.Option, R3>, + pullRight: Effect.Effect, Option.Option, R4> + ) => Effect.Effect, S], Option.Option>, never, R5> + ): Stream +} = internal.combineChunks + +/** + * Concatenates the specified stream with this stream, resulting in a stream + * that emits the elements from this stream and then the elements from the + * specified stream. + * + * @example + * ```ts + * import { Effect, Stream } from "effect" + * + * const s1 = Stream.make(1, 2, 3) + * const s2 = Stream.make(4, 5) + * + * const stream = Stream.concat(s1, s2) + * + * Effect.runPromise(Stream.runCollect(stream)).then(console.log) + * // { _id: 'Chunk', values: [ 1, 2, 3, 4, 5 ] } + * ``` + * + * @since 2.0.0 + * @category utils + */ +export const concat: { + (that: Stream): (self: Stream) => Stream + (self: Stream, that: Stream): Stream +} = internal.concat + +/** + * Concatenates all of the streams in the chunk to one stream. + * + * @example + * ```ts + * import { Chunk, Effect, Stream } from "effect" + * + * const s1 = Stream.make(1, 2, 3) + * const s2 = Stream.make(4, 5) + * const s3 = Stream.make(6, 7, 8) + * + * const stream = Stream.concatAll(Chunk.make(s1, s2, s3)) + * + * Effect.runPromise(Stream.runCollect(stream)).then(console.log) + * // { + * // _id: 'Chunk', + * // values: [ + * // 1, 2, 3, 4, + * // 5, 6, 7, 8 + * // ] + * // } + * ``` + * + * @since 2.0.0 + * @category constructors + */ +export const concatAll: (streams: Chunk.Chunk>) => Stream = internal.concatAll + +/** + * Composes this stream with the specified stream to create a cartesian + * product of elements. The `right` stream would be run multiple times, for + * every element in the `left` stream. + * + * See also `Stream.zip` for the more common point-wise variant. + * + * @example + * ```ts + * import { Effect, Stream } from "effect" + * + * const s1 = Stream.make(1, 2, 3) + * const s2 = Stream.make("a", "b") + * + * const product = Stream.cross(s1, s2) + * + * Effect.runPromise(Stream.runCollect(product)).then(console.log) + * // { + * // _id: "Chunk", + * // values: [ + * // [ 1, "a" ], [ 1, "b" ], [ 2, "a" ], [ 2, "b" ], [ 3, "a" ], [ 3, "b" ] + * // ] + * // } + * ``` + * + * @since 2.0.0 + * @category utils + */ +export const cross: { + (right: Stream): (left: Stream) => Stream<[AL, AR], EL | ER, RL | RR> + (left: Stream, right: Stream): Stream<[AL, AR], EL | ER, RL | RR> +} = internal.cross + +/** + * Composes this stream with the specified stream to create a cartesian + * product of elements, but keeps only elements from `left` stream. The `right` + * stream would be run multiple times, for every element in the `left` stream. + * + * See also `Stream.zipLeft` for the more common point-wise variant. + * + * @since 2.0.0 + * @category utils + */ +export const crossLeft: { + (right: Stream): (left: Stream) => Stream + (left: Stream, right: Stream): Stream +} = internal.crossLeft + +/** + * Composes this stream with the specified stream to create a cartesian + * product of elements, but keeps only elements from the `right` stream. The + * `left` stream would be run multiple times, for every element in the `right` + * stream. + * + * See also `Stream.zipRight` for the more common point-wise variant. + * + * @since 2.0.0 + * @category utils + */ +export const crossRight: { + (right: Stream): (left: Stream) => Stream + (left: Stream, right: Stream): Stream +} = internal.crossRight + +/** + * Composes this stream with the specified stream to create a cartesian + * product of elements with a specified function. The `right` stream would be + * run multiple times, for every element in the `left` stream. + * + * See also `Stream.zipWith` for the more common point-wise variant. + * + * @since 2.0.0 + * @category utils + */ +export const crossWith: { + ( + right: Stream, + f: (left: AL, right: AR) => A + ): (left: Stream) => Stream + ( + left: Stream, + right: Stream, + f: (left: AL, right: AR) => A + ): Stream +} = internal.crossWith + +/** + * Delays the emission of values by holding new values for a set duration. If + * no new values arrive during that time the value is emitted, however if a + * new value is received during the holding period the previous value is + * discarded and the process is repeated with the new value. + * + * This operator is useful if you have a stream of "bursty" events which + * eventually settle down and you only need the final event of the burst. For + * example, a search engine may only want to initiate a search after a user + * has paused typing so as to not prematurely recommend results. + * + * @example + * ```ts + * import { Effect, Stream } from "effect" + * + * let last = Date.now() + * const log = (message: string) => + * Effect.sync(() => { + * const end = Date.now() + * console.log(`${message} after ${end - last}ms`) + * last = end + * }) + * + * const stream = Stream.make(1, 2, 3).pipe( + * Stream.concat( + * Stream.fromEffect(Effect.sleep("200 millis").pipe(Effect.as(4))) // Emit 4 after 200 ms + * ), + * Stream.concat(Stream.make(5, 6)), // Continue with more rapid values + * Stream.concat( + * Stream.fromEffect(Effect.sleep("150 millis").pipe(Effect.as(7))) // Emit 7 after 150 ms + * ), + * Stream.concat(Stream.make(8)), + * Stream.tap((n) => log(`Received ${n}`)), + * Stream.debounce("100 millis"), // Only emit values after a pause of at least 100 milliseconds, + * Stream.tap((n) => log(`> Emitted ${n}`)) + * ) + * + * Effect.runPromise(Stream.runCollect(stream)).then(console.log) + * // Received 1 after 5ms + * // Received 2 after 2ms + * // Received 3 after 0ms + * // > Emitted 3 after 104ms + * // Received 4 after 99ms + * // Received 5 after 1ms + * // Received 6 after 0ms + * // > Emitted 6 after 101ms + * // Received 7 after 50ms + * // Received 8 after 1ms + * // > Emitted 8 after 101ms + * // { _id: 'Chunk', values: [ 3, 6, 8 ] } + * ``` + * + * @since 2.0.0 + * @category utils + */ +export const debounce: { + (duration: Duration.DurationInput): (self: Stream) => Stream + (self: Stream, duration: Duration.DurationInput): Stream +} = internal.debounce + +/** + * The stream that dies with the specified defect. + * + * @since 2.0.0 + * @category constructors + */ +export const die: (defect: unknown) => Stream = internal.die + +/** + * The stream that dies with the specified lazily evaluated defect. + * + * @since 2.0.0 + * @category constructors + */ +export const dieSync: (evaluate: LazyArg) => Stream = internal.dieSync + +/** + * The stream that dies with an exception described by `message`. + * + * @since 2.0.0 + * @category constructors + */ +export const dieMessage: (message: string) => Stream = internal.dieMessage + +/** + * More powerful version of `Stream.broadcast`. Allows to provide a function + * that determines what queues should receive which elements. The decide + * function will receive the indices of the queues in the resulting list. + * + * @since 2.0.0 + * @category utils + */ +export const distributedWith: { + ( + options: { + readonly size: N + readonly maximumLag: number + readonly decide: (a: A) => Effect.Effect> + } + ): ( + self: Stream + ) => Effect.Effect>>>, never, Scope.Scope | R> + ( + self: Stream, + options: { + readonly size: N + readonly maximumLag: number + readonly decide: (a: A) => Effect.Effect> + } + ): Effect.Effect>>>, never, Scope.Scope | R> +} = internal.distributedWith + +/** + * More powerful version of `Stream.distributedWith`. This returns a function + * that will produce new queues and corresponding indices. You can also + * provide a function that will be executed after the final events are + * enqueued in all queues. Shutdown of the queues is handled by the driver. + * Downstream users can also shutdown queues manually. In this case the driver + * will continue but no longer backpressure on them. + * + * @since 2.0.0 + * @category utils + */ +export const distributedWithDynamic: { + ( + options: { readonly maximumLag: number; readonly decide: (a: A) => Effect.Effect, never, never> } + ): ( + self: Stream + ) => Effect.Effect< + Effect.Effect<[number, Queue.Dequeue>>], never, never>, + never, + Scope.Scope | R + > + ( + self: Stream, + options: { readonly maximumLag: number; readonly decide: (a: A) => Effect.Effect, never, never> } + ): Effect.Effect< + Effect.Effect<[number, Queue.Dequeue>>], never, never>, + never, + Scope.Scope | R + > +} = internal.distributedWithDynamic + +/** + * Converts this stream to a stream that executes its effects but emits no + * elements. Useful for sequencing effects using streams: + * + * @example + * ```ts + * import { Effect, Stream } from "effect" + * + * // We create a stream and immediately drain it. + * const stream = Stream.range(1, 6).pipe(Stream.drain) + * + * Effect.runPromise(Stream.runCollect(stream)).then(console.log) + * // { _id: 'Chunk', values: [] } + * ``` + * + * @since 2.0.0 + * @category utils + */ +export const drain: (self: Stream) => Stream = internal.drain + +/** + * Drains the provided stream in the background for as long as this stream is + * running. If this stream ends before `other`, `other` will be interrupted. + * If `other` fails, this stream will fail with that error. + * + * @since 2.0.0 + * @category utils + */ +export const drainFork: { + (that: Stream): (self: Stream) => Stream + (self: Stream, that: Stream): Stream +} = internal.drainFork + +/** + * Drops the specified number of elements from this stream. + * + * @since 2.0.0 + * @category utils + */ +export const drop: { + (n: number): (self: Stream) => Stream + (self: Stream, n: number): Stream +} = internal.drop + +/** + * Drops the last specified number of elements from this stream. + * + * @note This combinator keeps `n` elements in memory. Be careful with big + * numbers. + * @since 2.0.0 + * @category utils + */ +export const dropRight: { + (n: number): (self: Stream) => Stream + (self: Stream, n: number): Stream +} = internal.dropRight + +/** + * Drops all elements of the stream until the specified predicate evaluates to + * `true`. + * + * @since 2.0.0 + * @category utils + */ +export const dropUntil: { + (predicate: Predicate>): (self: Stream) => Stream + (self: Stream, predicate: Predicate): Stream +} = internal.dropUntil + +/** + * Drops all elements of the stream until the specified effectful predicate + * evaluates to `true`. + * + * @since 2.0.0 + * @category utils + */ +export const dropUntilEffect: { + ( + predicate: (a: NoInfer) => Effect.Effect + ): (self: Stream) => Stream + ( + self: Stream, + predicate: (a: NoInfer) => Effect.Effect + ): Stream +} = internal.dropUntilEffect + +/** + * Drops all elements of the stream for as long as the specified predicate + * evaluates to `true`. + * + * @since 2.0.0 + * @category utils + */ +export const dropWhile: { + (predicate: Predicate>): (self: Stream) => Stream + (self: Stream, predicate: Predicate): Stream +} = internal.dropWhile + +/** + * Drops all elements of the stream for as long as the specified predicate + * produces an effect that evalutates to `true` + * + * @since 2.0.0 + * @category utils + */ +export const dropWhileEffect: { + ( + predicate: (a: NoInfer) => Effect.Effect + ): (self: Stream) => Stream + ( + self: Stream, + predicate: (a: A) => Effect.Effect + ): Stream +} = internal.dropWhileEffect + +/** + * Returns a stream whose failures and successes have been lifted into an + * `Either`. The resulting stream cannot fail, because the failures have been + * exposed as part of the `Either` success case. + * + * @note The stream will end as soon as the first error occurs. + * + * @since 2.0.0 + * @category utils + */ +export const either: (self: Stream) => Stream, never, R> = internal.either + +/** + * The empty stream. + * + * @example + * ```ts + * import { Effect, Stream } from "effect" + * + * const stream = Stream.empty + * + * Effect.runPromise(Stream.runCollect(stream)).then(console.log) + * // { _id: 'Chunk', values: [] } + * ``` + * + * @since 2.0.0 + * @category constructors + */ +export const empty: Stream = internal.empty + +/** + * Executes the provided finalizer after this stream's finalizers run. + * + * @example + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * const program = Stream.fromEffect(Console.log("Application Logic.")).pipe( + * Stream.concat(Stream.finalizer(Console.log("Finalizing the stream"))), + * Stream.ensuring( + * Console.log("Doing some other works after stream's finalization") + * ) + * ) + * + * Effect.runPromise(Stream.runCollect(program)).then(console.log) + * // Application Logic. + * // Finalizing the stream + * // Doing some other works after stream's finalization + * // { _id: 'Chunk', values: [ undefined, undefined ] } + * ``` + * + * @since 2.0.0 + * @category utils + */ +export const ensuring: { + (finalizer: Effect.Effect): (self: Stream) => Stream + (self: Stream, finalizer: Effect.Effect): Stream +} = internal.ensuring + +/** + * Executes the provided finalizer after this stream's finalizers run. + * + * @since 2.0.0 + * @category utils + */ +export const ensuringWith: { + ( + finalizer: (exit: Exit.Exit) => Effect.Effect + ): (self: Stream) => Stream + ( + self: Stream, + finalizer: (exit: Exit.Exit) => Effect.Effect + ): Stream +} = internal.ensuringWith + +/** + * Accesses the whole context of the stream. + * + * @since 2.0.0 + * @category context + */ +export const context: () => Stream, never, R> = internal.context + +/** + * Accesses the context of the stream. + * + * @since 2.0.0 + * @category context + */ +export const contextWith: (f: (env: Context.Context) => A) => Stream = internal.contextWith + +/** + * Accesses the context of the stream in the context of an effect. + * + * @since 2.0.0 + * @category context + */ +export const contextWithEffect: ( + f: (env: Context.Context) => Effect.Effect +) => Stream = internal.contextWithEffect + +/** + * Accesses the context of the stream in the context of a stream. + * + * @since 2.0.0 + * @category context + */ +export const contextWithStream: ( + f: (env: Context.Context) => Stream +) => Stream = internal.contextWithStream + +/** + * Creates a stream that executes the specified effect but emits no elements. + * + * @since 2.0.0 + * @category constructors + */ +export const execute: (effect: Effect.Effect) => Stream = internal.execute + +/** + * Terminates with the specified error. + * + * @example + * ```ts + * import { Effect, Stream } from "effect" + * + * const stream = Stream.fail("Uh oh!") + * + * Effect.runPromiseExit(Stream.runCollect(stream)).then(console.log) + * // { + * // _id: 'Exit', + * // _tag: 'Failure', + * // cause: { _id: 'Cause', _tag: 'Fail', failure: 'Uh oh!' } + * // } + * ``` + * + * @since 2.0.0 + * @category constructors + */ +export const fail: (error: E) => Stream = internal.fail + +/** + * Terminates with the specified lazily evaluated error. + * + * @since 2.0.0 + * @category constructors + */ +export const failSync: (evaluate: LazyArg) => Stream = internal.failSync + +/** + * The stream that always fails with the specified `Cause`. + * + * @since 2.0.0 + * @category constructors + */ +export const failCause: (cause: Cause.Cause) => Stream = internal.failCause + +/** + * The stream that always fails with the specified lazily evaluated `Cause`. + * + * @since 2.0.0 + * @category constructors + */ +export const failCauseSync: (evaluate: LazyArg>) => Stream = internal.failCauseSync + +/** + * Filters the elements emitted by this stream using the provided function. + * + * @example + * ```ts + * import { Effect, Stream } from "effect" + * + * const stream = Stream.range(1, 11).pipe(Stream.filter((n) => n % 2 === 0)) + * + * Effect.runPromise(Stream.runCollect(stream)).then(console.log) + * // { _id: 'Chunk', values: [ 2, 4, 6, 8, 10 ] } + * ``` + * + * @since 2.0.0 + * @category filtering + */ +export const filter: { + (refinement: Refinement, B>): (self: Stream) => Stream + (predicate: Predicate): (self: Stream) => Stream + (self: Stream, refinement: Refinement): Stream + (self: Stream, predicate: Predicate): Stream +} = internal.filter + +/** + * Effectfully filters the elements emitted by this stream. + * + * @since 2.0.0 + * @category filtering + */ +export const filterEffect: { + ( + f: (a: NoInfer) => Effect.Effect + ): (self: Stream) => Stream + (self: Stream, f: (a: A) => Effect.Effect): Stream +} = internal.filterEffect + +/** + * Performs a filter and map in a single step. + * + * @since 2.0.0 + * @category utils + */ +export const filterMap: { + (pf: (a: A) => Option.Option): (self: Stream) => Stream + (self: Stream, pf: (a: A) => Option.Option): Stream +} = internal.filterMap + +/** + * Performs an effectful filter and map in a single step. + * + * @since 2.0.0 + * @category utils + */ +export const filterMapEffect: { + ( + pf: (a: A) => Option.Option> + ): (self: Stream) => Stream + ( + self: Stream, + pf: (a: A) => Option.Option> + ): Stream +} = internal.filterMapEffect + +/** + * Transforms all elements of the stream for as long as the specified partial + * function is defined. + * + * @since 2.0.0 + * @category utils + */ +export const filterMapWhile: { + (pf: (a: A) => Option.Option): (self: Stream) => Stream + (self: Stream, pf: (a: A) => Option.Option): Stream +} = internal.filterMapWhile + +/** + * Effectfully transforms all elements of the stream for as long as the + * specified partial function is defined. + * + * @since 2.0.0 + * @category utils + */ +export const filterMapWhileEffect: { + ( + pf: (a: A) => Option.Option> + ): (self: Stream) => Stream + ( + self: Stream, + pf: (a: A) => Option.Option> + ): Stream +} = internal.filterMapWhileEffect + +/** + * Creates a one-element stream that never fails and executes the finalizer + * when it ends. + * + * @example + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * const application = Stream.fromEffect(Console.log("Application Logic.")) + * + * const deleteDir = (dir: string) => Console.log(`Deleting dir: ${dir}`) + * + * const program = application.pipe( + * Stream.concat( + * Stream.finalizer( + * deleteDir("tmp").pipe( + * Effect.andThen(Console.log("Temporary directory was deleted.")) + * ) + * ) + * ) + * ) + * + * Effect.runPromise(Stream.runCollect(program)).then(console.log) + * // Application Logic. + * // Deleting dir: tmp + * // Temporary directory was deleted. + * // { _id: 'Chunk', values: [ undefined, undefined ] } + * ``` + * + * @since 2.0.0 + * @category constructors + */ +export const finalizer: (finalizer: Effect.Effect) => Stream = internal.finalizer + +/** + * Finds the first element emitted by this stream that satisfies the provided + * predicate. + * + * @since 2.0.0 + * @category elements + */ +export const find: { + (refinement: Refinement, B>): (self: Stream) => Stream + (predicate: Predicate>): (self: Stream) => Stream + (self: Stream, refinement: Refinement): Stream + (self: Stream, predicate: Predicate): Stream +} = internal.find + +/** + * Finds the first element emitted by this stream that satisfies the provided + * effectful predicate. + * + * @since 2.0.0 + * @category elements + */ +export const findEffect: { + ( + predicate: (a: NoInfer) => Effect.Effect + ): (self: Stream) => Stream + ( + self: Stream, + predicate: (a: NoInfer) => Effect.Effect + ): Stream +} = internal.findEffect + +/** + * Returns a stream made of the concatenation in strict order of all the + * streams produced by passing each element of this stream to `f0` + * + * @since 2.0.0 + * @category sequencing + */ +export const flatMap: { + ( + f: (a: A) => Stream, + options?: { + readonly concurrency?: number | "unbounded" | undefined + readonly bufferSize?: number | undefined + readonly switch?: boolean | undefined + } | undefined + ): (self: Stream) => Stream + ( + self: Stream, + f: (a: A) => Stream, + options?: { + readonly concurrency?: number | "unbounded" | undefined + readonly bufferSize?: number | undefined + readonly switch?: boolean | undefined + } | undefined + ): Stream +} = internal.flatMap + +/** + * Flattens this stream-of-streams into a stream made of the concatenation in + * strict order of all the streams. + * + * @since 2.0.0 + * @category sequencing + */ +export const flatten: { + ( + options?: + | { readonly concurrency?: number | "unbounded" | undefined; readonly bufferSize?: number | undefined } + | undefined + ): (self: Stream, E, R>) => Stream + ( + self: Stream, E, R>, + options?: + | { readonly concurrency?: number | "unbounded" | undefined; readonly bufferSize?: number | undefined } + | undefined + ): Stream +} = internal.flatten + +/** + * Submerges the chunks carried by this stream into the stream's structure, + * while still preserving them. + * + * @since 2.0.0 + * @category sequencing + */ +export const flattenChunks: (self: Stream, E, R>) => Stream = internal.flattenChunks + +/** + * Flattens `Effect` values into the stream's structure, preserving all + * information about the effect. + * + * @since 2.0.0 + * @category sequencing + */ +export const flattenEffect: { + ( + options?: + | { readonly concurrency?: number | "unbounded" | undefined; readonly unordered?: boolean | undefined } + | undefined + ): (self: Stream, E, R>) => Stream + ( + self: Stream, E, R>, + options?: + | { readonly concurrency?: number | "unbounded" | undefined; readonly unordered?: boolean | undefined } + | undefined + ): Stream +} = internal.flattenEffect + +/** + * Unwraps `Exit` values that also signify end-of-stream by failing with `None`. + * + * @since 2.0.0 + * @category sequencing + */ +export const flattenExitOption: ( + self: Stream>, E, R> +) => Stream = internal.flattenExitOption + +/** + * Submerges the iterables carried by this stream into the stream's structure, + * while still preserving them. + * + * @since 2.0.0 + * @category sequencing + */ +export const flattenIterables: (self: Stream, E, R>) => Stream = internal.flattenIterables + +/** + * Unwraps `Exit` values and flatten chunks that also signify end-of-stream + * by failing with `None`. + * + * @since 2.0.0 + * @category sequencing + */ +export const flattenTake: (self: Stream, E, R>) => Stream = + internal.flattenTake + +/** + * Repeats this stream forever. + * + * @since 2.0.0 + * @category utils + */ +export const forever: (self: Stream) => Stream = internal.forever + +/** + * Creates a stream from an `AsyncIterable`. + * + * @example + * ```ts + * import { Effect, Stream } from "effect" + * + * const myAsyncIterable = async function*() { + * yield 1 + * yield 2 + * } + * + * const stream = Stream.fromAsyncIterable( + * myAsyncIterable(), + * (e) => new Error(String(e)) // Error Handling + * ) + * + * Effect.runPromise(Stream.runCollect(stream)).then(console.log) + * // { _id: 'Chunk', values: [ 1, 2 ] } + * ``` + * + * @since 2.0.0 + * @category constructors + */ +export const fromAsyncIterable: (iterable: AsyncIterable, onError: (e: unknown) => E) => Stream = + internal.fromAsyncIterable + +/** + * Creates a stream from a `Channel`. + * + * @since 2.0.0 + * @category constructors + */ +export const fromChannel: ( + channel: Channel.Channel, unknown, E, unknown, unknown, unknown, R> +) => Stream = internal.fromChannel + +/** + * Creates a channel from a `Stream`. + * + * @since 2.0.0 + * @category constructors + */ +export const toChannel: ( + stream: Stream +) => Channel.Channel, unknown, E, unknown, unknown, unknown, R> = internal.toChannel + +/** + * Creates a stream from a `Chunk` of values. + * + * @example + * ```ts + * import { Chunk, Effect, Stream } from "effect" + * + * // Creating a stream with values from a single Chunk + * const stream = Stream.fromChunk(Chunk.make(1, 2, 3)) + * + * Effect.runPromise(Stream.runCollect(stream)).then(console.log) + * // { _id: 'Chunk', values: [ 1, 2, 3 ] } + * ``` + * + * @since 2.0.0 + * @category constructors + */ +export const fromChunk: (chunk: Chunk.Chunk) => Stream = internal.fromChunk + +/** + * Creates a stream from a subscription to a `PubSub`. + * + * **Options** + * + * - `shutdown`: If `true`, the `PubSub` will be shutdown after the stream is evaluated (defaults to `false`) + * + * @since 2.0.0 + * @category constructors + */ +export const fromChunkPubSub: { + ( + pubsub: PubSub.PubSub>, + options: { readonly scoped: true; readonly shutdown?: boolean | undefined } + ): Effect.Effect, never, Scope.Scope> + ( + pubsub: PubSub.PubSub>, + options?: { readonly scoped?: false | undefined; readonly shutdown?: boolean | undefined } | undefined + ): Stream +} = internal.fromChunkPubSub + +/** + * Creates a stream from a `Queue` of values. + * + * **Options** + * + * - `shutdown`: If `true`, the queue will be shutdown after the stream is evaluated (defaults to `false`) + * + * @since 2.0.0 + * @category constructors + */ +export const fromChunkQueue: ( + queue: Queue.Dequeue>, + options?: { + readonly shutdown?: boolean | undefined + } +) => Stream = internal.fromChunkQueue + +/** + * Creates a stream from an arbitrary number of chunks. + * + * @example + * ```ts + * import { Chunk, Effect, Stream } from "effect" + * + * // Creating a stream with values from multiple Chunks + * const stream = Stream.fromChunks(Chunk.make(1, 2, 3), Chunk.make(4, 5, 6)) + * + * Effect.runPromise(Stream.runCollect(stream)).then(console.log) + * // { _id: 'Chunk', values: [ 1, 2, 3, 4, 5, 6 ] } + * ``` + * + * @since 2.0.0 + * @category constructors + */ +export const fromChunks: (...chunks: Array>) => Stream = internal.fromChunks + +/** + * Either emits the success value of this effect or terminates the stream + * with the failure value of this effect. + * + * @example + * ```ts + * import { Effect, Random, Stream } from "effect" + * + * const stream = Stream.fromEffect(Random.nextInt) + * + * Effect.runPromise(Stream.runCollect(stream)).then(console.log) + * // Example Output: { _id: 'Chunk', values: [ 922694024 ] } + * ``` + * + * @since 2.0.0 + * @category constructors + */ +export const fromEffect: (effect: Effect.Effect) => Stream = internal.fromEffect + +/** + * Creates a stream from an effect producing a value of type `A` or an empty + * `Stream`. + * + * @since 2.0.0 + * @category constructors + */ +export const fromEffectOption: (effect: Effect.Effect, R>) => Stream = + internal.fromEffectOption + +/** + * Creates a stream from a subscription to a `PubSub`. + * + * **Options** + * + * - `shutdown`: If `true`, the `PubSub` will be shutdown after the stream is evaluated (defaults to `false`) + * + * @since 2.0.0 + * @category constructors + */ +export const fromPubSub: { + ( + pubsub: PubSub.PubSub, + options: { + readonly scoped: true + readonly maxChunkSize?: number | undefined + readonly shutdown?: boolean | undefined + } + ): Effect.Effect, never, Scope.Scope> + ( + pubsub: PubSub.PubSub, + options?: { + readonly scoped?: false | undefined + readonly maxChunkSize?: number | undefined + readonly shutdown?: boolean | undefined + } | undefined + ): Stream +} = internal.fromPubSub + +/** + * Creates a stream from a subscription to a `TPubSub`. + * + * @since 3.10.0 + * @category constructors + */ +export const fromTPubSub: (pubsub: TPubSub) => Stream = internal.fromTPubSub + +/** + * Creates a new `Stream` from an iterable collection of values. + * + * @example + * ```ts + * import { Effect, Stream } from "effect" + * + * const numbers = [1, 2, 3] + * + * const stream = Stream.fromIterable(numbers) + * + * Effect.runPromise(Stream.runCollect(stream)).then(console.log) + * // { _id: 'Chunk', values: [ 1, 2, 3 ] } + * ``` + * + * @since 2.0.0 + * @category constructors + */ +export const fromIterable: (iterable: Iterable) => Stream = internal.fromIterable + +/** + * Creates a stream from an effect producing a value of type `Iterable`. + * + * @example + * ```ts + * import { Context, Effect, Stream } from "effect" + * + * class Database extends Context.Tag("Database")< + * Database, + * { readonly getUsers: Effect.Effect> } + * >() {} + * + * const getUsers = Database.pipe(Effect.andThen((_) => _.getUsers)) + * + * const stream = Stream.fromIterableEffect(getUsers) + * + * Effect.runPromise( + * Stream.runCollect(stream.pipe(Stream.provideService(Database, { getUsers: Effect.succeed(["user1", "user2"]) }))) + * ).then(console.log) + * // { _id: 'Chunk', values: [ 'user1', 'user2' ] } + * ``` + * + * @since 2.0.0 + * @category constructors + */ +export const fromIterableEffect: (effect: Effect.Effect, E, R>) => Stream = + internal.fromIterableEffect + +/** + * Creates a stream from an iterator + * + * @since 2.0.0 + * @category constructors + */ +export const fromIteratorSucceed: (iterator: IterableIterator, maxChunkSize?: number) => Stream = + internal.fromIteratorSucceed + +/** + * Creates a stream from an effect that pulls elements from another stream. + * + * See `Stream.toPull` for reference. + * + * @since 2.0.0 + * @category constructors + */ +export const fromPull: ( + effect: Effect.Effect, Option.Option, R2>, never, Scope.Scope | R> +) => Stream> = internal.fromPull + +/** + * Creates a stream from a queue of values + * + * **Options** + * + * - `maxChunkSize`: The maximum number of queued elements to put in one chunk in the stream + * - `shutdown`: If `true`, the queue will be shutdown after the stream is evaluated (defaults to `false`) + * + * @since 2.0.0 + * @category constructors + */ +export const fromQueue: ( + queue: Queue.Dequeue, + options?: { + readonly maxChunkSize?: number | undefined + readonly shutdown?: boolean | undefined + } +) => Stream = internal.fromQueue + +/** + * Creates a stream from a TQueue of values + * + * @since 3.10.0 + * @category constructors + */ +export const fromTQueue: (queue: TDequeue) => Stream = internal.fromTQueue + +/** + * Creates a stream from a `ReadableStream`. + * + * See https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream. + * + * @since 2.0.0 + * @category constructors + */ +export const fromReadableStream: { + ( + options: { + readonly evaluate: LazyArg> + readonly onError: (error: unknown) => E + readonly releaseLockOnEnd?: boolean | undefined + } + ): Stream + (evaluate: LazyArg>, onError: (error: unknown) => E): Stream +} = internal.fromReadableStream + +/** + * Creates a stream from a `ReadableStreamBYOBReader`. + * + * See https://developer.mozilla.org/en-US/docs/Web/API/ReadableStreamBYOBReader. + * + * @since 2.0.0 + * @category constructors + */ +export const fromReadableStreamByob: { + ( + options: { + readonly evaluate: LazyArg> + readonly onError: (error: unknown) => E + readonly bufferSize?: number | undefined + readonly releaseLockOnEnd?: boolean | undefined + } + ): Stream + ( + evaluate: LazyArg>, + onError: (error: unknown) => E, + /** Controls the size of the underlying `ArrayBuffer` (defaults to `4096`) */ + allocSize?: number + ): Stream +} = internal.fromReadableStreamByob + +/** + * Creates a stream from a `Schedule` that does not require any further + * input. The stream will emit an element for each value output from the + * schedule, continuing for as long as the schedule continues. + * + * @example + * ```ts + * import { Effect, Schedule, Stream } from "effect" + * + * // Emits values every 1 second for a total of 5 emissions + * const schedule = Schedule.spaced("1 second").pipe( + * Schedule.compose(Schedule.recurs(5)) + * ) + * + * const stream = Stream.fromSchedule(schedule) + * + * Effect.runPromise(Stream.runCollect(stream)).then(console.log) + * // { _id: 'Chunk', values: [ 0, 1, 2, 3, 4 ] } + * ``` + * + * @since 2.0.0 + * @category constructors + */ +export const fromSchedule: (schedule: Schedule.Schedule) => Stream = + internal.fromSchedule + +/** + * Creates a pipeline that groups on adjacent keys, calculated by the + * specified function. + * + * @since 2.0.0 + * @category grouping + */ +export const groupAdjacentBy: { + (f: (a: A) => K): (self: Stream) => Stream<[K, Chunk.NonEmptyChunk], E, R> + (self: Stream, f: (a: A) => K): Stream<[K, Chunk.NonEmptyChunk], E, R> +} = internal.groupAdjacentBy + +/** + * More powerful version of `Stream.groupByKey`. + * + * @example + * ```ts + * import { Chunk, Effect, GroupBy, Stream } from "effect" + * + * const groupByKeyResult = Stream.fromIterable([ + * "Mary", + * "James", + * "Robert", + * "Patricia", + * "John", + * "Jennifer", + * "Rebecca", + * "Peter" + * ]).pipe( + * Stream.groupBy((name) => Effect.succeed([name.substring(0, 1), name])) + * ) + * + * const stream = GroupBy.evaluate(groupByKeyResult, (key, stream) => + * Stream.fromEffect( + * Stream.runCollect(stream).pipe( + * Effect.andThen((chunk) => [key, Chunk.size(chunk)] as const) + * ) + * )) + * + * Effect.runPromise(Stream.runCollect(stream)).then(console.log) + * // { + * // _id: 'Chunk', + * // values: [ [ 'M', 1 ], [ 'J', 3 ], [ 'R', 2 ], [ 'P', 2 ] ] + * // } + * ``` + * + * @since 2.0.0 + * @category grouping + */ +export const groupBy: { + ( + f: (a: A) => Effect.Effect, + options?: { readonly bufferSize?: number | undefined } | undefined + ): (self: Stream) => GroupBy.GroupBy + ( + self: Stream, + f: (a: A) => Effect.Effect, + options?: { readonly bufferSize?: number | undefined } | undefined + ): GroupBy.GroupBy +} = groupBy_.groupBy + +/** + * Partition a stream using a function and process each stream individually. + * This returns a data structure that can be used to further filter down which + * groups shall be processed. + * + * After calling apply on the GroupBy object, the remaining groups will be + * processed in parallel and the resulting streams merged in a + * nondeterministic fashion. + * + * Up to `buffer` elements may be buffered in any group stream before the + * producer is backpressured. Take care to consume from all streams in order + * to prevent deadlocks. + * + * For example, to collect the first 2 words for every starting letter from a + * stream of words: + * + * ```ts + * import { pipe, GroupBy, Stream } from "effect" + * + * pipe( + * Stream.fromIterable(["hello", "world", "hi", "holla"]), + * Stream.groupByKey((word) => word[0]), + * GroupBy.evaluate((key, stream) => + * pipe( + * stream, + * Stream.take(2), + * Stream.map((words) => [key, words] as const) + * ) + * ) + * ) + * ``` + * + * @since 2.0.0 + * @category grouping + */ +export const groupByKey: { + ( + f: (a: A) => K, + options?: { + readonly bufferSize?: number | undefined + } + ): (self: Stream) => GroupBy.GroupBy + ( + self: Stream, + f: (a: A) => K, + options?: { + readonly bufferSize?: number | undefined + } + ): GroupBy.GroupBy +} = groupBy_.groupByKey + +/** + * Partitions the stream with specified `chunkSize`. + * + * @example + * ```ts + * import { Effect, Stream } from "effect" + * + * const stream = Stream.range(0, 8).pipe(Stream.grouped(3)) + * + * Effect.runPromise(Stream.runCollect(stream)).then((chunks) => console.log("%o", chunks)) + * // { + * // _id: 'Chunk', + * // values: [ + * // { _id: 'Chunk', values: [ 0, 1, 2, [length]: 3 ] }, + * // { _id: 'Chunk', values: [ 3, 4, 5, [length]: 3 ] }, + * // { _id: 'Chunk', values: [ 6, 7, 8, [length]: 3 ] }, + * // [length]: 3 + * // ] + * // } + * ``` + * + * @since 2.0.0 + * @category grouping + */ +export const grouped: { + (chunkSize: number): (self: Stream) => Stream, E, R> + (self: Stream, chunkSize: number): Stream, E, R> +} = internal.grouped + +/** + * Partitions the stream with the specified `chunkSize` or until the specified + * `duration` has passed, whichever is satisfied first. + * + * @example + * ```ts + * import { Chunk, Effect, Schedule, Stream } from "effect" + * + * const stream = Stream.range(0, 9).pipe( + * Stream.repeat(Schedule.spaced("1 second")), + * Stream.groupedWithin(18, "1.5 seconds"), + * Stream.take(3) + * ) + * + * Effect.runPromise(Stream.runCollect(stream)).then((chunks) => console.log(Chunk.toArray(chunks))) + * // [ + * // { + * // _id: 'Chunk', + * // values: [ + * // 0, 1, 2, 3, 4, 5, 6, + * // 7, 8, 9, 0, 1, 2, 3, + * // 4, 5, 6, 7 + * // ] + * // }, + * // { + * // _id: 'Chunk', + * // values: [ + * // 8, 9, 0, 1, 2, + * // 3, 4, 5, 6, 7, + * // 8, 9 + * // ] + * // }, + * // { + * // _id: 'Chunk', + * // values: [ + * // 0, 1, 2, 3, 4, 5, 6, + * // 7, 8, 9, 0, 1, 2, 3, + * // 4, 5, 6, 7 + * // ] + * // } + * // ] + * ``` + * + * @since 2.0.0 + * @category grouping + */ +export const groupedWithin: { + ( + chunkSize: number, + duration: Duration.DurationInput + ): (self: Stream) => Stream, E, R> + (self: Stream, chunkSize: number, duration: Duration.DurationInput): Stream, E, R> +} = internal.groupedWithin + +/** + * Specialized version of haltWhen which halts the evaluation of this stream + * after the given duration. + * + * An element in the process of being pulled will not be interrupted when the + * given duration completes. See `interruptAfter` for this behavior. + * + * @since 2.0.0 + * @category utils + */ +export const haltAfter: { + (duration: Duration.DurationInput): (self: Stream) => Stream + (self: Stream, duration: Duration.DurationInput): Stream +} = internal.haltAfter + +/** + * Halts the evaluation of this stream when the provided effect completes. The + * given effect will be forked as part of the returned stream, and its success + * will be discarded. + * + * An element in the process of being pulled will not be interrupted when the + * effect completes. See `interruptWhen` for this behavior. + * + * If the effect completes with a failure, the stream will emit that failure. + * + * @since 2.0.0 + * @category utils + */ +export const haltWhen: { + (effect: Effect.Effect): (self: Stream) => Stream + (self: Stream, effect: Effect.Effect): Stream +} = internal.haltWhen + +/** + * Halts the evaluation of this stream when the provided promise resolves. + * + * If the promise completes with a failure, the stream will emit that failure. + * + * @since 2.0.0 + * @category utils + */ +export const haltWhenDeferred: { + (deferred: Deferred.Deferred): (self: Stream) => Stream + (self: Stream, deferred: Deferred.Deferred): Stream +} = internal.haltWhenDeferred + +/** + * The identity pipeline, which does not modify streams in any way. + * + * @since 2.0.0 + * @category utils + */ +export const identity: () => Stream = internal.identityStream + +/** + * Interleaves this stream and the specified stream deterministically by + * alternating pulling values from this stream and the specified stream. When + * one stream is exhausted all remaining values in the other stream will be + * pulled. + * + * @example + * ```ts + * import { Effect, Stream } from "effect" + * + * const s1 = Stream.make(1, 2, 3) + * const s2 = Stream.make(4, 5, 6) + * + * const stream = Stream.interleave(s1, s2) + * + * Effect.runPromise(Stream.runCollect(stream)).then(console.log) + * // { _id: 'Chunk', values: [ 1, 4, 2, 5, 3, 6 ] } + * ``` + * @since 2.0.0 + * @category utils + */ +export const interleave: { + (that: Stream): (self: Stream) => Stream + (self: Stream, that: Stream): Stream +} = internal.interleave + +/** + * Combines this stream and the specified stream deterministically using the + * stream of boolean values `pull` to control which stream to pull from next. + * A value of `true` indicates to pull from this stream and a value of `false` + * indicates to pull from the specified stream. Only consumes as many elements + * as requested by the `pull` stream. If either this stream or the specified + * stream are exhausted further requests for values from that stream will be + * ignored. + * + * @example + * ```ts + * import { Effect, Stream } from "effect" + * + * const s1 = Stream.make(1, 3, 5, 7, 9) + * const s2 = Stream.make(2, 4, 6, 8, 10) + * + * const booleanStream = Stream.make(true, false, false).pipe(Stream.forever) + * + * const stream = Stream.interleaveWith(s1, s2, booleanStream) + * + * Effect.runPromise(Stream.runCollect(stream)).then(console.log) + * // { + * // _id: 'Chunk', + * // values: [ + * // 1, 2, 4, 3, 6, + * // 8, 5, 10, 7, 9 + * // ] + * // } + * ``` + * + * @since 2.0.0 + * @category utils + */ +export const interleaveWith: { + ( + that: Stream, + decider: Stream + ): (self: Stream) => Stream + ( + self: Stream, + that: Stream, + decider: Stream + ): Stream +} = internal.interleaveWith + +/** + * Intersperse stream with provided `element`. + * + * @example + * ```ts + * import { Effect, Stream } from "effect" + * + * const stream = Stream.make(1, 2, 3, 4, 5).pipe(Stream.intersperse(0)) + * + * Effect.runPromise(Stream.runCollect(stream)).then(console.log) + * // { + * // _id: 'Chunk', + * // values: [ + * // 1, 0, 2, 0, 3, + * // 0, 4, 0, 5 + * // ] + * // } + * ``` + * + * @since 2.0.0 + * @category utils + */ +export const intersperse: { + (element: A2): (self: Stream) => Stream + (self: Stream, element: A2): Stream +} = internal.intersperse + +/** + * Intersperse the specified element, also adding a prefix and a suffix. + * + * @example + * ```ts + * import { Effect, Stream } from "effect" + * + * const stream = Stream.make(1, 2, 3, 4, 5).pipe( + * Stream.intersperseAffixes({ + * start: "[", + * middle: "-", + * end: "]" + * }) + * ) + * + * Effect.runPromise(Stream.runCollect(stream)).then(console.log) + * // { + * // _id: 'Chunk', + * // values: [ + * // '[', 1, '-', 2, '-', + * // 3, '-', 4, '-', 5, + * // ']' + * // ] + * // } + * ``` + * + * @since 2.0.0 + * @category utils + */ +export const intersperseAffixes: { + ( + options: { readonly start: A2; readonly middle: A3; readonly end: A4 } + ): (self: Stream) => Stream + ( + self: Stream, + options: { readonly start: A2; readonly middle: A3; readonly end: A4 } + ): Stream +} = internal.intersperseAffixes + +/** + * Specialized version of `Stream.interruptWhen` which interrupts the + * evaluation of this stream after the given `Duration`. + * + * @since 2.0.0 + * @category utils + */ +export const interruptAfter: { + (duration: Duration.DurationInput): (self: Stream) => Stream + (self: Stream, duration: Duration.DurationInput): Stream +} = internal.interruptAfter + +/** + * Interrupts the evaluation of this stream when the provided effect + * completes. The given effect will be forked as part of this stream, and its + * success will be discarded. This combinator will also interrupt any + * in-progress element being pulled from upstream. + * + * If the effect completes with a failure before the stream completes, the + * returned stream will emit that failure. + * + * @since 2.0.0 + * @category utils + */ +export const interruptWhen: { + (effect: Effect.Effect): (self: Stream) => Stream + (self: Stream, effect: Effect.Effect): Stream +} = internal.interruptWhen + +/** + * Interrupts the evaluation of this stream when the provided promise + * resolves. This combinator will also interrupt any in-progress element being + * pulled from upstream. + * + * If the promise completes with a failure, the stream will emit that failure. + * + * @since 2.0.0 + * @category utils + */ +export const interruptWhenDeferred: { + (deferred: Deferred.Deferred): (self: Stream) => Stream + (self: Stream, deferred: Deferred.Deferred): Stream +} = internal.interruptWhenDeferred + +/** + * The infinite stream of iterative function application: a, f(a), f(f(a)), + * f(f(f(a))), ... + * + * @example + * ```ts + * import { Effect, Stream } from "effect" + * + * // An infinite Stream of numbers starting from 1 and incrementing + * const stream = Stream.iterate(1, (n) => n + 1) + * + * Effect.runPromise(Stream.runCollect(stream.pipe(Stream.take(10)))).then(console.log) + * // { _id: 'Chunk', values: [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ] } + * ``` + * + * @since 2.0.0 + * @category constructors + */ +export const iterate: (value: A, next: (value: A) => A) => Stream = internal.iterate + +/** + * Creates a stream from an sequence of values. + * + * @example + * ```ts + * import { Effect, Stream } from "effect" + * + * const stream = Stream.make(1, 2, 3) + * + * Effect.runPromise(Stream.runCollect(stream)).then(console.log) + * // { _id: 'Chunk', values: [ 1, 2, 3 ] } + * ``` + * + * @since 2.0.0 + * @category constructors + */ +export const make: >(...as: As) => Stream = internal.make + +/** + * Transforms the elements of this stream using the supplied function. + * + * @example + * ```ts + * import { Effect, Stream } from "effect" + * + * const stream = Stream.make(1, 2, 3).pipe(Stream.map((n) => n + 1)) + * + * Effect.runPromise(Stream.runCollect(stream)).then(console.log) + * // { _id: 'Chunk', values: [ 2, 3, 4 ] } + * ``` + * + * @since 2.0.0 + * @category mapping + */ +export const map: { + (f: (a: A) => B): (self: Stream) => Stream + (self: Stream, f: (a: A) => B): Stream +} = internal.map + +/** + * Statefully maps over the elements of this stream to produce new elements. + * + * @example + * ```ts + * import { Effect, Stream } from "effect" + * + * const runningTotal = (stream: Stream.Stream): Stream.Stream => + * stream.pipe(Stream.mapAccum(0, (s, a) => [s + a, s + a])) + * + * // input: 0, 1, 2, 3, 4, 5, 6 + * Effect.runPromise(Stream.runCollect(runningTotal(Stream.range(0, 6)))).then( + * console.log + * ) + * // { _id: "Chunk", values: [ 0, 1, 3, 6, 10, 15, 21 ] } + * ``` + * + * @since 2.0.0 + * @category mapping + */ +export const mapAccum: { + (s: S, f: (s: S, a: A) => readonly [S, A2]): (self: Stream) => Stream + (self: Stream, s: S, f: (s: S, a: A) => readonly [S, A2]): Stream +} = internal.mapAccum + +/** + * Statefully and effectfully maps over the elements of this stream to produce + * new elements. + * + * @since 2.0.0 + * @category mapping + */ +export const mapAccumEffect: { + ( + s: S, + f: (s: S, a: A) => Effect.Effect + ): (self: Stream) => Stream + ( + self: Stream, + s: S, + f: (s: S, a: A) => Effect.Effect + ): Stream +} = internal.mapAccumEffect + +/** + * Returns a stream whose failure and success channels have been mapped by the + * specified `onFailure` and `onSuccess` functions. + * + * @since 2.0.0 + * @category utils + */ +export const mapBoth: { + ( + options: { readonly onFailure: (e: E) => E2; readonly onSuccess: (a: A) => A2 } + ): (self: Stream) => Stream + ( + self: Stream, + options: { readonly onFailure: (e: E) => E2; readonly onSuccess: (a: A) => A2 } + ): Stream +} = internal.mapBoth + +/** + * Transforms the chunks emitted by this stream. + * + * @since 2.0.0 + * @category mapping + */ +export const mapChunks: { + (f: (chunk: Chunk.Chunk) => Chunk.Chunk): (self: Stream) => Stream + (self: Stream, f: (chunk: Chunk.Chunk) => Chunk.Chunk): Stream +} = internal.mapChunks + +/** + * Effectfully transforms the chunks emitted by this stream. + * + * @since 2.0.0 + * @category mapping + */ +export const mapChunksEffect: { + ( + f: (chunk: Chunk.Chunk) => Effect.Effect, E2, R2> + ): (self: Stream) => Stream + ( + self: Stream, + f: (chunk: Chunk.Chunk) => Effect.Effect, E2, R2> + ): Stream +} = internal.mapChunksEffect + +/** + * Maps each element to an iterable, and flattens the iterables into the + * output of this stream. + * + * @example + * ```ts + * import { Effect, Stream } from "effect" + * + * const numbers = Stream.make("1-2-3", "4-5", "6").pipe( + * Stream.mapConcat((s) => s.split("-")), + * Stream.map((s) => parseInt(s)) + * ) + * + * Effect.runPromise(Stream.runCollect(numbers)).then(console.log) + * // { _id: 'Chunk', values: [ 1, 2, 3, 4, 5, 6 ] } + * ``` + * + * @since 2.0.0 + * @category mapping + */ +export const mapConcat: { + (f: (a: A) => Iterable): (self: Stream) => Stream + (self: Stream, f: (a: A) => Iterable): Stream +} = internal.mapConcat + +/** + * Maps each element to a chunk, and flattens the chunks into the output of + * this stream. + * + * @since 2.0.0 + * @category mapping + */ +export const mapConcatChunk: { + (f: (a: A) => Chunk.Chunk): (self: Stream) => Stream + (self: Stream, f: (a: A) => Chunk.Chunk): Stream +} = internal.mapConcatChunk + +/** + * Effectfully maps each element to a chunk, and flattens the chunks into the + * output of this stream. + * + * @since 2.0.0 + * @category mapping + */ +export const mapConcatChunkEffect: { + ( + f: (a: A) => Effect.Effect, E2, R2> + ): (self: Stream) => Stream + ( + self: Stream, + f: (a: A) => Effect.Effect, E2, R2> + ): Stream +} = internal.mapConcatChunkEffect + +/** + * Effectfully maps each element to an iterable, and flattens the iterables + * into the output of this stream. + * + * @since 2.0.0 + * @category mapping + */ +export const mapConcatEffect: { + ( + f: (a: A) => Effect.Effect, E2, R2> + ): (self: Stream) => Stream + ( + self: Stream, + f: (a: A) => Effect.Effect, E2, R2> + ): Stream +} = internal.mapConcatEffect + +/** + * Maps over elements of the stream with the specified effectful function. + * + * @example + * ```ts + * import { Effect, Random, Stream } from "effect" + * + * const stream = Stream.make(10, 20, 30).pipe( + * Stream.mapEffect((n) => Random.nextIntBetween(0, n)) + * ) + * + * Effect.runPromise(Stream.runCollect(stream)).then(console.log) + * // Example Output: { _id: 'Chunk', values: [ 7, 19, 8 ] } + * ``` + * + * @since 2.0.0 + * @category mapping + */ +export const mapEffect: { + ( + f: (a: A) => Effect.Effect, + options?: + | { readonly concurrency?: number | "unbounded" | undefined; readonly unordered?: boolean | undefined } + | undefined + ): (self: Stream) => Stream + ( + f: (a: A) => Effect.Effect, + options: { readonly key: (a: A) => K; readonly bufferSize?: number | undefined } + ): (self: Stream) => Stream + ( + self: Stream, + f: (a: A) => Effect.Effect, + options?: + | { readonly concurrency?: number | "unbounded" | undefined; readonly unordered?: boolean | undefined } + | undefined + ): Stream + ( + self: Stream, + f: (a: A) => Effect.Effect, + options: { readonly key: (a: A) => K; readonly bufferSize?: number | undefined } + ): Stream +} = groupBy_.mapEffectOptions + +/** + * Transforms the errors emitted by this stream using `f`. + * + * @since 2.0.0 + * @category mapping + */ +export const mapError: { + (f: (error: E) => E2): (self: Stream) => Stream + (self: Stream, f: (error: E) => E2): Stream +} = internal.mapError + +/** + * Transforms the full causes of failures emitted by this stream. + * + * @since 2.0.0 + * @category mapping + */ +export const mapErrorCause: { + (f: (cause: Cause.Cause) => Cause.Cause): (self: Stream) => Stream + (self: Stream, f: (cause: Cause.Cause) => Cause.Cause): Stream +} = internal.mapErrorCause + +/** + * Merges this stream and the specified stream together. + * + * New produced stream will terminate when both specified stream terminate if + * no termination strategy is specified. + * + * @example + * ```ts + * import { Effect, Schedule, Stream } from "effect" + * + * const s1 = Stream.make(1, 2, 3).pipe( + * Stream.schedule(Schedule.spaced("100 millis")) + * ) + * const s2 = Stream.make(4, 5, 6).pipe( + * Stream.schedule(Schedule.spaced("200 millis")) + * ) + * + * const stream = Stream.merge(s1, s2) + * + * Effect.runPromise(Stream.runCollect(stream)).then(console.log) + * // { _id: 'Chunk', values: [ 1, 4, 2, 3, 5, 6 ] } + * ``` + * + * @since 2.0.0 + * @category utils + */ +export const merge: { + ( + that: Stream, + options?: { readonly haltStrategy?: HaltStrategy.HaltStrategyInput | undefined } | undefined + ): (self: Stream) => Stream + ( + self: Stream, + that: Stream, + options?: { readonly haltStrategy?: HaltStrategy.HaltStrategyInput | undefined } | undefined + ): Stream +} = internal.merge + +/** + * Merges a variable list of streams in a non-deterministic fashion. Up to `n` + * streams may be consumed in parallel and up to `outputBuffer` chunks may be + * buffered by this operator. + * + * @since 2.0.0 + * @category utils + */ +export const mergeAll: { + ( + options: { + readonly concurrency: number | "unbounded" + readonly bufferSize?: number | undefined + } + ): (streams: Iterable>) => Stream + ( + streams: Iterable>, + options: { + readonly concurrency: number | "unbounded" + readonly bufferSize?: number | undefined + } + ): Stream +} = internal.mergeAll + +/** + * Merges a struct of streams into a single stream of tagged values. + * @category combinators + * @since 3.8.5 + * + * @example + * ```ts + * import { Stream } from "effect" + * // Stream.Stream<{ _tag: "a"; value: number; } | { _tag: "b"; value: string; }> + * const res = Stream.mergeWithTag({ + * a: Stream.make(0), + * b: Stream.make("") + * }, { concurrency: "unbounded" }) + * ``` + */ +export const mergeWithTag: { + }>( + streams: S, + options: { + readonly concurrency: number | "unbounded" + readonly bufferSize?: number | undefined + } + ): Stream< + { [K in keyof S]: { _tag: K; value: Stream.Success } }[keyof S], + Stream.Error, + Stream.Context + > + (options: { + readonly concurrency: number | "unbounded" + readonly bufferSize?: number | undefined + }): }>(streams: S) => Stream< + { [K in keyof S]: { _tag: K; value: Stream.Success } }[keyof S], + Stream.Error, + Stream.Context + > +} = internal.mergeWithTag + +/** + * Merges this stream and the specified stream together to a common element + * type with the specified mapping functions. + * + * New produced stream will terminate when both specified stream terminate if + * no termination strategy is specified. + * + * @example + * ```ts + * import { Effect, Schedule, Stream } from "effect" + * + * const s1 = Stream.make("1", "2", "3").pipe( + * Stream.schedule(Schedule.spaced("100 millis")) + * ) + * const s2 = Stream.make(4.1, 5.3, 6.2).pipe( + * Stream.schedule(Schedule.spaced("200 millis")) + * ) + * + * const stream = Stream.mergeWith(s1, s2, { + * onSelf: (s) => parseInt(s), + * onOther: (n) => Math.floor(n) + * }) + * + * Effect.runPromise(Stream.runCollect(stream)).then(console.log) + * // { _id: 'Chunk', values: [ 1, 4, 2, 3, 5, 6 ] } + * ``` + * + * @since 2.0.0 + * @category utils + */ +export const mergeWith: { + ( + other: Stream, + options: { + readonly onSelf: (a: A) => A3 + readonly onOther: (a2: A2) => A4 + readonly haltStrategy?: HaltStrategy.HaltStrategyInput | undefined + } + ): (self: Stream) => Stream + ( + self: Stream, + other: Stream, + options: { + readonly onSelf: (a: A) => A3 + readonly onOther: (a2: A2) => A4 + readonly haltStrategy?: HaltStrategy.HaltStrategyInput | undefined + } + ): Stream +} = internal.mergeWith + +/** + * Merges this stream and the specified stream together to produce a stream of + * eithers. + * + * @since 2.0.0 + * @category utils + */ +export const mergeEither: { + ( + that: Stream + ): (self: Stream) => Stream, E2 | E, R2 | R> + (self: Stream, that: Stream): Stream, E | E2, R | R2> +} = internal.mergeEither + +/** + * Merges this stream and the specified stream together, discarding the values + * from the right stream. + * + * @since 2.0.0 + * @category utils + */ +export const mergeLeft: { + (right: Stream): (left: Stream) => Stream + (left: Stream, right: Stream): Stream +} = internal.mergeLeft + +/** + * Merges this stream and the specified stream together, discarding the values + * from the left stream. + * + * @since 2.0.0 + * @category utils + */ +export const mergeRight: { + (right: Stream): (left: Stream) => Stream + (left: Stream, right: Stream): Stream +} = internal.mergeRight + +/** + * Returns a combined string resulting from concatenating each of the values + * from the stream. + * + * @since 2.0.0 + * @category utils + */ +export const mkString: (self: Stream) => Effect.Effect = internal.mkString + +/** + * The stream that never produces any value or fails with any error. + * + * @since 2.0.0 + * @category constructors + */ +export const never: Stream = internal.never + +/** + * Adds an effect to be executed at the end of the stream. + * + * @example + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * const stream = Stream.make(1, 2, 3).pipe( + * Stream.map((n) => n * 2), + * Stream.tap((n) => Console.log(`after mapping: ${n}`)), + * Stream.onEnd(Console.log("Stream ended")) + * ) + * + * Effect.runPromise(Stream.runCollect(stream)).then(console.log) + * // after mapping: 2 + * // after mapping: 4 + * // after mapping: 6 + * // Stream ended + * // { _id: 'Chunk', values: [ 2, 4, 6 ] } + * ``` + * + * @since 3.6.0 + * @category sequencing + */ +export const onEnd: { + <_, E2, R2>( + effect: Effect.Effect<_, E2, R2> + ): (self: Stream) => Stream + ( + self: Stream, + effect: Effect.Effect<_, E2, R2> + ): Stream +} = internal.onEnd + +/** + * Runs the specified effect if this stream fails, providing the error to the + * effect if it exists. + * + * Note: Unlike `Effect.onError` there is no guarantee that the provided + * effect will not be interrupted. + * + * @since 2.0.0 + * @category utils + */ +export const onError: { + ( + cleanup: (cause: Cause.Cause) => Effect.Effect + ): (self: Stream) => Stream + ( + self: Stream, + cleanup: (cause: Cause.Cause) => Effect.Effect + ): Stream +} = internal.onError + +/** + * Runs the specified effect if this stream ends. + * + * @since 2.0.0 + * @category utils + */ +export const onDone: { + (cleanup: () => Effect.Effect): (self: Stream) => Stream + (self: Stream, cleanup: () => Effect.Effect): Stream +} = internal.onDone + +/** + * Adds an effect to be executed at the start of the stream. + * + * @example + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * const stream = Stream.make(1, 2, 3).pipe( + * Stream.onStart(Console.log("Stream started")), + * Stream.map((n) => n * 2), + * Stream.tap((n) => Console.log(`after mapping: ${n}`)) + * ) + * + * Effect.runPromise(Stream.runCollect(stream)).then(console.log) + * // Stream started + * // after mapping: 2 + * // after mapping: 4 + * // after mapping: 6 + * // { _id: 'Chunk', values: [ 2, 4, 6 ] } + * ``` + * + * @since 3.6.0 + * @category sequencing + */ +export const onStart: { + <_, E2, R2>( + effect: Effect.Effect<_, E2, R2> + ): (self: Stream) => Stream + ( + self: Stream, + effect: Effect.Effect<_, E2, R2> + ): Stream +} = internal.onStart + +/** + * Translates any failure into a stream termination, making the stream + * infallible and all failures unchecked. + * + * @since 2.0.0 + * @category error handling + */ +export const orDie: (self: Stream) => Stream = internal.orDie + +/** + * Keeps none of the errors, and terminates the stream with them, using the + * specified function to convert the `E` into a defect. + * + * @since 2.0.0 + * @category error handling + */ +export const orDieWith: { + (f: (e: E) => unknown): (self: Stream) => Stream + (self: Stream, f: (e: E) => unknown): Stream +} = internal.orDieWith + +/** + * Switches to the provided stream in case this one fails with a typed error. + * + * See also `Stream.catchAll`. + * + * @since 2.0.0 + * @category error handling + */ +export const orElse: { + (that: LazyArg>): (self: Stream) => Stream + (self: Stream, that: LazyArg>): Stream +} = internal.orElse + +/** + * Switches to the provided stream in case this one fails with a typed error. + * + * See also `Stream.catchAll`. + * + * @since 2.0.0 + * @category error handling + */ +export const orElseEither: { + ( + that: LazyArg> + ): (self: Stream) => Stream, E2, R2 | R> + ( + self: Stream, + that: LazyArg> + ): Stream, E2, R | R2> +} = internal.orElseEither + +/** + * Fails with given error in case this one fails with a typed error. + * + * See also `Stream.catchAll`. + * + * @since 2.0.0 + * @category error handling + */ +export const orElseFail: { + (error: LazyArg): (self: Stream) => Stream + (self: Stream, error: LazyArg): Stream +} = internal.orElseFail + +/** + * Produces the specified element if this stream is empty. + * + * @since 2.0.0 + * @category error handling + */ +export const orElseIfEmpty: { + (element: LazyArg): (self: Stream) => Stream + (self: Stream, element: LazyArg): Stream +} = internal.orElseIfEmpty + +/** + * Produces the specified chunk if this stream is empty. + * + * @since 2.0.0 + * @category error handling + */ +export const orElseIfEmptyChunk: { + (chunk: LazyArg>): (self: Stream) => Stream + (self: Stream, chunk: LazyArg>): Stream +} = internal.orElseIfEmptyChunk + +/** + * Switches to the provided stream in case this one is empty. + * + * @since 2.0.0 + * @category error handling + */ +export const orElseIfEmptyStream: { + (stream: LazyArg>): (self: Stream) => Stream + (self: Stream, stream: LazyArg>): Stream +} = internal.orElseIfEmptyStream + +/** + * Succeeds with the specified value if this one fails with a typed error. + * + * @since 2.0.0 + * @category error handling + */ +export const orElseSucceed: { + (value: LazyArg): (self: Stream) => Stream + (self: Stream, value: LazyArg): Stream +} = internal.orElseSucceed + +/** + * Like `Stream.unfold`, but allows the emission of values to end one step further + * than the unfolding of the state. This is useful for embedding paginated + * APIs, hence the name. + * + * @example + * ```ts + * import { Effect, Option, Stream } from "effect" + * + * const stream = Stream.paginate(0, (n) => [ + * n, + * n < 3 ? Option.some(n + 1) : Option.none() + * ]) + * + * Effect.runPromise(Stream.runCollect(stream)).then(console.log) + * // { _id: 'Chunk', values: [ 0, 1, 2, 3 ] } + * ``` + * + * @since 2.0.0 + * @category constructors + */ +export const paginate: (s: S, f: (s: S) => readonly [A, Option.Option]) => Stream = internal.paginate + +/** + * Like `Stream.unfoldChunk`, but allows the emission of values to end one step + * further than the unfolding of the state. This is useful for embedding + * paginated APIs, hence the name. + * + * @since 2.0.0 + * @category constructors + */ +export const paginateChunk: ( + s: S, + f: (s: S) => readonly [Chunk.Chunk, Option.Option] +) => Stream = internal.paginateChunk + +/** + * Like `Stream.unfoldChunkEffect`, but allows the emission of values to end one step + * further than the unfolding of the state. This is useful for embedding + * paginated APIs, hence the name. + * + * @since 2.0.0 + * @category constructors + */ +export const paginateChunkEffect: ( + s: S, + f: (s: S) => Effect.Effect, Option.Option], E, R> +) => Stream = internal.paginateChunkEffect + +/** + * Like `Stream.unfoldEffect` but allows the emission of values to end one step + * further than the unfolding of the state. This is useful for embedding + * paginated APIs, hence the name. + * + * @since 2.0.0 + * @category constructors + */ +export const paginateEffect: ( + s: S, + f: (s: S) => Effect.Effect], E, R> +) => Stream = internal.paginateEffect + +/** + * Splits a stream into two substreams based on a predicate. + * + * **Details** + * + * The `Stream.partition` function splits a stream into two parts: one for + * elements that satisfy the predicate (evaluated to `true`) and another for + * those that do not (evaluated to `false`). + * + * The faster stream may advance up to `bufferSize` elements ahead of the slower + * one. + * + * **Example** (Partitioning a Stream into Even and Odd Numbers) + * + * ```ts + * import { Effect, Stream } from "effect" + * + * const partition = Stream.range(1, 9).pipe( + * Stream.partition((n) => n % 2 === 0, { bufferSize: 5 }) + * ) + * + * const program = Effect.scoped( + * Effect.gen(function*() { + * const [odds, evens] = yield* partition + * console.log(yield* Stream.runCollect(odds)) + * console.log(yield* Stream.runCollect(evens)) + * }) + * ) + * + * Effect.runPromise(program) + * // { _id: 'Chunk', values: [ 1, 3, 5, 7, 9 ] } + * // { _id: 'Chunk', values: [ 2, 4, 6, 8 ] } + * ``` + * + * @see {@link partitionEither} for partitioning a stream based on effectful + * conditions. + * + * @since 2.0.0 + * @category utils + */ +export const partition: { + ( + refinement: Refinement, B>, + options?: { bufferSize?: number | undefined } | undefined + ): ( + self: Stream + ) => Effect.Effect<[excluded: Stream, E, never>, satisfying: Stream], E, R | Scope.Scope> + ( + predicate: Predicate, + options?: { bufferSize?: number | undefined } | undefined + ): ( + self: Stream + ) => Effect.Effect<[excluded: Stream, satisfying: Stream], E, Scope.Scope | R> + ( + self: Stream, + refinement: Refinement, + options?: { bufferSize?: number | undefined } | undefined + ): Effect.Effect<[excluded: Stream, E, never>, satisfying: Stream], E, R | Scope.Scope> + ( + self: Stream, + predicate: Predicate, + options?: { bufferSize?: number | undefined } | undefined + ): Effect.Effect<[excluded: Stream, satisfying: Stream], E, R | Scope.Scope> +} = internal.partition + +/** + * Splits a stream into two substreams based on an effectful condition. + * + * **Details** + * + * The `Stream.partitionEither` function is used to divide a stream into two + * parts: one for elements that satisfy a condition producing `Either.left` + * values, and another for those that produce `Either.right` values. This + * function applies an effectful predicate to each element in the stream to + * determine which substream it belongs to. + * + * The faster stream may advance up to `bufferSize` elements ahead of the slower + * one. + * + * **Example** (Partitioning a Stream with an Effectful Predicate) + * + * ```ts + * import { Effect, Either, Stream } from "effect" + * + * const partition = Stream.range(1, 9).pipe( + * Stream.partitionEither( + * (n) => Effect.succeed(n % 2 === 0 ? Either.right(n) : Either.left(n)), + * { bufferSize: 5 } + * ) + * ) + * + * const program = Effect.scoped( + * Effect.gen(function*() { + * const [evens, odds] = yield* partition + * console.log(yield* Stream.runCollect(evens)) + * console.log(yield* Stream.runCollect(odds)) + * }) + * ) + * + * Effect.runPromise(program) + * // { _id: 'Chunk', values: [ 1, 3, 5, 7, 9 ] } + * // { _id: 'Chunk', values: [ 2, 4, 6, 8 ] } + * ``` + * + * @see {@link partition} for partitioning a stream based on simple conditions. + * + * @since 2.0.0 + * @category utils + */ +export const partitionEither: { + ( + predicate: (a: NoInfer) => Effect.Effect, E2, R2>, + options?: { readonly bufferSize?: number | undefined } | undefined + ): ( + self: Stream + ) => Effect.Effect<[left: Stream, right: Stream], E2 | E, Scope.Scope | R2 | R> + ( + self: Stream, + predicate: (a: A) => Effect.Effect, E2, R2>, + options?: { readonly bufferSize?: number | undefined } | undefined + ): Effect.Effect<[left: Stream, right: Stream], E | E2, Scope.Scope | R | R2> +} = internal.partitionEither + +/** + * Peels off enough material from the stream to construct a `Z` using the + * provided `Sink` and then returns both the `Z` and the rest of the + * `Stream` in a scope. Like all scoped values, the provided stream is + * valid only within the scope. + * + * @since 2.0.0 + * @category utils + */ +export const peel: { + ( + sink: Sink.Sink + ): (self: Stream) => Effect.Effect<[A2, Stream], E2 | E, Scope.Scope | R2 | R> + ( + self: Stream, + sink: Sink.Sink + ): Effect.Effect<[A2, Stream], E | E2, Scope.Scope | R | R2> +} = internal.peel + +/** + * Pipes all of the values from this stream through the provided sink. + * + * See also `Stream.transduce`. + * + * @since 2.0.0 + * @category utils + */ +export const pipeThrough: { + (sink: Sink.Sink): (self: Stream) => Stream + (self: Stream, sink: Sink.Sink): Stream +} = internal.pipeThrough + +/** + * Pipes all the values from this stream through the provided channel. + * + * @since 2.0.0 + * @category utils + */ +export const pipeThroughChannel: { + ( + channel: Channel.Channel, Chunk.Chunk, E2, E, unknown, unknown, R2> + ): (self: Stream) => Stream + ( + self: Stream, + channel: Channel.Channel, Chunk.Chunk, E2, E, unknown, unknown, R2> + ): Stream +} = internal.pipeThroughChannel + +/** + * Pipes all values from this stream through the provided channel, passing + * through any error emitted by this stream unchanged. + * + * @since 2.0.0 + * @category utils + */ +export const pipeThroughChannelOrFail: { + ( + chan: Channel.Channel, Chunk.Chunk, E2, E, unknown, unknown, R2> + ): (self: Stream) => Stream + ( + self: Stream, + chan: Channel.Channel, Chunk.Chunk, E2, E, unknown, unknown, R2> + ): Stream +} = internal.pipeThroughChannelOrFail + +/** + * Emits the provided chunk before emitting any other value. + * + * @since 2.0.0 + * @category utils + */ +export const prepend: { + (values: Chunk.Chunk): (self: Stream) => Stream + (self: Stream, values: Chunk.Chunk): Stream +} = internal.prepend + +/** + * Provides the stream with its required context, which eliminates its + * dependency on `R`. + * + * @since 2.0.0 + * @category context + */ +export const provideContext: { + (context: Context.Context): (self: Stream) => Stream + (self: Stream, context: Context.Context): Stream +} = internal.provideContext + +/** + * Provides the stream with some of its required context, which eliminates its + * dependency on `R`. + * + * @since 3.16.9 + * @category context + */ +export const provideSomeContext: { + (context: Context.Context): (self: Stream) => Stream> + (self: Stream, context: Context.Context): Stream> +} = internal.provideSomeContext + +/** + * Provides a `Layer` to the stream, which translates it to another level. + * + * @since 2.0.0 + * @category context + */ +export const provideLayer: { + (layer: Layer.Layer): (self: Stream) => Stream + (self: Stream, layer: Layer.Layer): Stream +} = internal.provideLayer + +/** + * Provides the stream with the single service it requires. If the stream + * requires more than one service use `Stream.provideContext` instead. + * + * @since 2.0.0 + * @category context + */ +export const provideService: { + (tag: Context.Tag, resource: NoInfer): (self: Stream) => Stream> + (self: Stream, tag: Context.Tag, resource: NoInfer): Stream> +} = internal.provideService + +/** + * Provides the stream with the single service it requires. If the stream + * requires more than one service use `Stream.provideContext` instead. + * + * @since 2.0.0 + * @category context + */ +export const provideServiceEffect: { + ( + tag: Context.Tag, + effect: Effect.Effect, E2, R2> + ): (self: Stream) => Stream> + ( + self: Stream, + tag: Context.Tag, + effect: Effect.Effect, E2, R2> + ): Stream> +} = internal.provideServiceEffect + +/** + * Provides the stream with the single service it requires. If the stream + * requires more than one service use `Stream.provideContext` instead. + * + * @since 2.0.0 + * @category context + */ +export const provideServiceStream: { + ( + tag: Context.Tag, + stream: Stream, E2, R2> + ): (self: Stream) => Stream> + ( + self: Stream, + tag: Context.Tag, + stream: Stream, E2, R2> + ): Stream> +} = internal.provideServiceStream + +/** + * Transforms the context being provided to the stream with the specified + * function. + * + * @since 2.0.0 + * @category context + */ +export const mapInputContext: { + (f: (env: Context.Context) => Context.Context): (self: Stream) => Stream + (self: Stream, f: (env: Context.Context) => Context.Context): Stream +} = internal.mapInputContext + +/** + * Splits the context into two parts, providing one part using the + * specified layer and leaving the remainder `R0`. + * + * @since 2.0.0 + * @category context + */ +export const provideSomeLayer: { + ( + layer: Layer.Layer + ): (self: Stream) => Stream> + ( + self: Stream, + layer: Layer.Layer + ): Stream> +} = internal.provideSomeLayer + +/** + * Returns a stream that mirrors the first upstream to emit an item. + * As soon as one of the upstream emits a first value, the other is interrupted. + * The resulting stream will forward all items from the "winning" source stream. + * Any upstream failures will cause the returned stream to fail. + * + * @example + * ```ts + * import { Stream, Schedule, Console, Effect } from "effect" + * + * const stream = Stream.fromSchedule(Schedule.spaced('2 millis')).pipe( + * Stream.race(Stream.fromSchedule(Schedule.spaced('1 millis'))), + * Stream.take(6), + * Stream.tap(Console.log) + * ) + * + * Effect.runPromise(Stream.runDrain(stream)) + * // Output each millisecond from the first stream, the rest streams are interrupted + * // 0 + * // 1 + * // 2 + * // 3 + * // 4 + * // 5 + * ``` + * @since 3.7.0 + * @category racing + */ +export const race: { + ( + right: Stream + ): (left: Stream) => Stream + ( + left: Stream, + right: Stream + ): Stream +} = internal.race + +/** + * Returns a stream that mirrors the first upstream to emit an item. + * As soon as one of the upstream emits a first value, all the others are interrupted. + * The resulting stream will forward all items from the "winning" source stream. + * Any upstream failures will cause the returned stream to fail. + * + * @example + * ```ts + * import { Stream, Schedule, Console, Effect } from "effect" + * + * const stream = Stream.raceAll( + * Stream.fromSchedule(Schedule.spaced('1 millis')), + * Stream.fromSchedule(Schedule.spaced('2 millis')), + * Stream.fromSchedule(Schedule.spaced('4 millis')), + * ).pipe(Stream.take(6), Stream.tap(Console.log)) + * + * Effect.runPromise(Stream.runDrain(stream)) + * // Output each millisecond from the first stream, the rest streams are interrupted + * // 0 + * // 1 + * // 2 + * // 3 + * // 4 + * // 5 + * ``` + * @since 3.5.0 + * @category racing + */ +export const raceAll: >>( + ...streams: S +) => Stream< + Stream.Success, + Stream.Error, + Stream.Context +> = internal.raceAll + +/** + * Constructs a stream from a range of integers, including both endpoints. + * + * @example + * ```ts + * import { Effect, Stream } from "effect" + * + * // A Stream with a range of numbers from 1 to 5 + * const stream = Stream.range(1, 5) + * + * Effect.runPromise(Stream.runCollect(stream)).then(console.log) + * // { _id: 'Chunk', values: [ 1, 2, 3, 4, 5 ] } + * ``` + * + * @since 2.0.0 + * @category constructors + */ +export const range: (min: number, max: number, chunkSize?: number) => Stream = internal.range + +/** + * Re-chunks the elements of the stream into chunks of `n` elements each. The + * last chunk might contain less than `n` elements. + * + * @since 2.0.0 + * @category utils + */ +export const rechunk: { + (n: number): (self: Stream) => Stream + (self: Stream, n: number): Stream +} = internal.rechunk + +/** + * Keeps some of the errors, and terminates the fiber with the rest + * + * @since 2.0.0 + * @category error handling + */ +export const refineOrDie: { + (pf: (error: E) => Option.Option): (self: Stream) => Stream + (self: Stream, pf: (error: E) => Option.Option): Stream +} = internal.refineOrDie + +/** + * Keeps some of the errors, and terminates the fiber with the rest, using the + * specified function to convert the `E` into a defect. + * + * @since 2.0.0 + * @category error handling + */ +export const refineOrDieWith: { + ( + pf: (error: E) => Option.Option, + f: (error: E) => unknown + ): (self: Stream) => Stream + (self: Stream, pf: (error: E) => Option.Option, f: (error: E) => unknown): Stream +} = internal.refineOrDieWith + +/** + * Repeats the entire stream using the specified schedule. The stream will + * execute normally, and then repeat again according to the provided schedule. + * + * @example + * ```ts + * import { Effect, Schedule, Stream } from "effect" + * + * const stream = Stream.repeat(Stream.succeed(1), Schedule.forever) + * + * Effect.runPromise(Stream.runCollect(stream.pipe(Stream.take(5)))).then(console.log) + * // { _id: 'Chunk', values: [ 1, 1, 1, 1, 1 ] } + * ``` + * + * @since 2.0.0 + * @category utils + */ +export const repeat: { + (schedule: Schedule.Schedule): (self: Stream) => Stream + (self: Stream, schedule: Schedule.Schedule): Stream +} = internal.repeat + +/** + * Creates a stream from an effect producing a value of type `A` which repeats + * forever. + * + * @example + * ```ts + * import { Effect, Random, Stream } from "effect" + * + * const stream = Stream.repeatEffect(Random.nextInt) + * + * Effect.runPromise(Stream.runCollect(stream.pipe(Stream.take(5)))).then(console.log) + * // Example Output: { _id: 'Chunk', values: [ 3891571149, 4239494205, 2352981603, 2339111046, 1488052210 ] } + * ``` + * + * @since 2.0.0 + * @category constructors + */ +export const repeatEffect: (effect: Effect.Effect) => Stream = internal.repeatEffect + +/** + * Creates a stream from an effect producing chunks of `A` values which + * repeats forever. + * + * @since 2.0.0 + * @category constructors + */ +export const repeatEffectChunk: (effect: Effect.Effect, E, R>) => Stream = + internal.repeatEffectChunk + +/** + * Creates a stream from an effect producing chunks of `A` values until it + * fails with `None`. + * + * @since 2.0.0 + * @category constructors + */ +export const repeatEffectChunkOption: ( + effect: Effect.Effect, Option.Option, R> +) => Stream = internal.repeatEffectChunkOption + +/** + * Creates a stream from an effect producing values of type `A` until it fails + * with `None`. + * + * @example + * ```ts + * // In this example, we're draining an Iterator to create a stream from it + * import { Stream, Effect, Option } from "effect" + * + * const drainIterator = (it: Iterator): Stream.Stream => + * Stream.repeatEffectOption( + * Effect.sync(() => it.next()).pipe( + * Effect.andThen((res) => { + * if (res.done) { + * return Effect.fail(Option.none()) + * } + * return Effect.succeed(res.value) + * }) + * ) + * ) + * ``` + * + * @since 2.0.0 + * @category constructors + */ +export const repeatEffectOption: (effect: Effect.Effect, R>) => Stream = + internal.repeatEffectOption + +/** + * Creates a stream from an effect producing a value of type `A`, which is + * repeated using the specified schedule. + * + * @since 2.0.0 + * @category constructors + */ +export const repeatEffectWithSchedule: ( + effect: Effect.Effect, + schedule: Schedule.Schedule +) => Stream = internal.repeatEffectWithSchedule + +/** + * Repeats the entire stream using the specified schedule. The stream will + * execute normally, and then repeat again according to the provided schedule. + * The schedule output will be emitted at the end of each repetition. + * + * @since 2.0.0 + * @category utils + */ +export const repeatEither: { + ( + schedule: Schedule.Schedule + ): (self: Stream) => Stream, E, R2 | R> + ( + self: Stream, + schedule: Schedule.Schedule + ): Stream, E, R | R2> +} = internal.repeatEither + +/** + * Repeats each element of the stream using the provided schedule. Repetitions + * are done in addition to the first execution, which means using + * `Schedule.recurs(1)` actually results in the original effect, plus an + * additional recurrence, for a total of two repetitions of each value in the + * stream. + * + * @since 2.0.0 + * @category utils + */ +export const repeatElements: { + (schedule: Schedule.Schedule): (self: Stream) => Stream + (self: Stream, schedule: Schedule.Schedule): Stream +} = internal.repeatElements + +/** + * Repeats each element of the stream using the provided schedule. When the + * schedule is finished, then the output of the schedule will be emitted into + * the stream. Repetitions are done in addition to the first execution, which + * means using `Schedule.recurs(1)` actually results in the original effect, + * plus an additional recurrence, for a total of two repetitions of each value + * in the stream. + * + * This function accepts two conversion functions, which allow the output of + * this stream and the output of the provided schedule to be unified into a + * single type. For example, `Either` or similar data type. + * + * @since 2.0.0 + * @category utils + */ +export const repeatElementsWith: { + ( + schedule: Schedule.Schedule, + options: { readonly onElement: (a: A) => C; readonly onSchedule: (b: B) => C } + ): (self: Stream) => Stream + ( + self: Stream, + schedule: Schedule.Schedule, + options: { readonly onElement: (a: A) => C; readonly onSchedule: (b: B) => C } + ): Stream +} = internal.repeatElementsWith + +/** + * Repeats the provided value infinitely. + * + * @example + * ```ts + * import { Effect, Stream } from "effect" + * + * const stream = Stream.repeatValue(0) + * + * Effect.runPromise(Stream.runCollect(stream.pipe(Stream.take(5)))).then(console.log) + * // { _id: 'Chunk', values: [ 0, 0, 0, 0, 0 ] } + * ``` + * + * @since 2.0.0 + * @category constructors + */ +export const repeatValue: (value: A) => Stream = internal.repeatValue + +/** + * Repeats the entire stream using the specified schedule. The stream will + * execute normally, and then repeat again according to the provided schedule. + * The schedule output will be emitted at the end of each repetition and can + * be unified with the stream elements using the provided functions. + * + * @since 2.0.0 + * @category utils + */ +export const repeatWith: { + ( + schedule: Schedule.Schedule, + options: { readonly onElement: (a: A) => C; readonly onSchedule: (b: B) => C } + ): (self: Stream) => Stream + ( + self: Stream, + schedule: Schedule.Schedule, + options: { readonly onElement: (a: A) => C; readonly onSchedule: (b: B) => C } + ): Stream +} = internal.repeatWith + +/** + * When the stream fails, retry it according to the given schedule + * + * This retries the entire stream, so will re-execute all of the stream's + * acquire operations. + * + * The schedule is reset as soon as the first element passes through the + * stream again. + * + * @since 2.0.0 + * @category utils + */ +export const retry: { + (policy: Schedule.Schedule, R2>): (self: Stream) => Stream + (self: Stream, policy: Schedule.Schedule, R2>): Stream +} = internal.retry + +/** + * Apply an `ExecutionPlan` to the stream, which allows you to fallback to + * different resources in case of failure. + * + * If you have a stream that could fail with partial results, you can use + * the `preventFallbackOnPartialStream` option to prevent contamination of + * the final stream with partial results. + * + * @since 3.16.0 + * @category Error handling + * @experimental + */ +export const withExecutionPlan: { + ( + policy: ExecutionPlan<{ provides: Provides; input: Input; error: PolicyE; requirements: R2 }>, + options?: { readonly preventFallbackOnPartialStream?: boolean | undefined } + ): (self: Stream) => Stream> + ( + self: Stream, + policy: ExecutionPlan<{ provides: Provides; input: Input; error: PolicyE; requirements: R2 }>, + options?: { readonly preventFallbackOnPartialStream?: boolean | undefined } + ): Stream> +} = internal.withExecutionPlan + +/** + * Runs the sink on the stream to produce either the sink's result or an error. + * + * @since 2.0.0 + * @category destructors + */ +export const run: { + ( + sink: Sink.Sink + ): (self: Stream) => Effect.Effect> + ( + self: Stream, + sink: Sink.Sink + ): Effect.Effect> +} = internal.run + +/** + * Runs the stream and collects all of its elements to a chunk. + * + * @since 2.0.0 + * @category destructors + */ +export const runCollect: (self: Stream) => Effect.Effect, E, R> = internal.runCollect + +/** + * Runs the stream and emits the number of elements processed + * + * @since 2.0.0 + * @category destructors + */ +export const runCount: (self: Stream) => Effect.Effect = internal.runCount + +/** + * Runs the stream only for its effects. The emitted elements are discarded. + * + * @since 2.0.0 + * @category destructors + */ +export const runDrain: (self: Stream) => Effect.Effect = internal.runDrain + +/** + * Executes a pure fold over the stream of values - reduces all elements in + * the stream to a value of type `S`. + * + * @since 2.0.0 + * @category destructors + */ +export const runFold: { + (s: S, f: (s: S, a: A) => S): (self: Stream) => Effect.Effect + (self: Stream, s: S, f: (s: S, a: A) => S): Effect.Effect +} = internal.runFold + +/** + * Executes an effectful fold over the stream of values. + * + * @since 2.0.0 + * @category destructors + */ +export const runFoldEffect: { + ( + s: S, + f: (s: S, a: A) => Effect.Effect + ): (self: Stream) => Effect.Effect> + ( + self: Stream, + s: S, + f: (s: S, a: A) => Effect.Effect + ): Effect.Effect> +} = internal.runFoldEffect + +/** + * Executes a pure fold over the stream of values. Returns a scoped value that + * represents the scope of the stream. + * + * @since 2.0.0 + * @category destructors + */ +export const runFoldScoped: { + (s: S, f: (s: S, a: A) => S): (self: Stream) => Effect.Effect + (self: Stream, s: S, f: (s: S, a: A) => S): Effect.Effect +} = internal.runFoldScoped + +/** + * Executes an effectful fold over the stream of values. Returns a scoped + * value that represents the scope of the stream. + * + * @since 2.0.0 + * @category destructors + */ +export const runFoldScopedEffect: { + ( + s: S, + f: (s: S, a: A) => Effect.Effect + ): (self: Stream) => Effect.Effect + ( + self: Stream, + s: S, + f: (s: S, a: A) => Effect.Effect + ): Effect.Effect +} = internal.runFoldScopedEffect + +/** + * Reduces the elements in the stream to a value of type `S`. Stops the fold + * early when the condition is not fulfilled. Example: + * + * @since 2.0.0 + * @category destructors + */ +export const runFoldWhile: { + (s: S, cont: Predicate, f: (s: S, a: A) => S): (self: Stream) => Effect.Effect + (self: Stream, s: S, cont: Predicate, f: (s: S, a: A) => S): Effect.Effect +} = internal.runFoldWhile + +/** + * Executes an effectful fold over the stream of values. Stops the fold early + * when the condition is not fulfilled. + * + * @since 2.0.0 + * @category destructors + */ +export const runFoldWhileEffect: { + ( + s: S, + cont: Predicate, + f: (s: S, a: A) => Effect.Effect + ): (self: Stream) => Effect.Effect> + ( + self: Stream, + s: S, + cont: Predicate, + f: (s: S, a: A) => Effect.Effect + ): Effect.Effect> +} = internal.runFoldWhileEffect + +/** + * Executes a pure fold over the stream of values. Returns a scoped value that + * represents the scope of the stream. Stops the fold early when the condition + * is not fulfilled. + * + * @since 2.0.0 + * @category destructors + */ +export const runFoldWhileScoped: { + ( + s: S, + cont: Predicate, + f: (s: S, a: A) => S + ): (self: Stream) => Effect.Effect + ( + self: Stream, + s: S, + cont: Predicate, + f: (s: S, a: A) => S + ): Effect.Effect +} = internal.runFoldWhileScoped + +/** + * Executes an effectful fold over the stream of values. Returns a scoped + * value that represents the scope of the stream. Stops the fold early when + * the condition is not fulfilled. + * + * @since 2.0.0 + * @category destructors + */ +export const runFoldWhileScopedEffect: { + ( + s: S, + cont: Predicate, + f: (s: S, a: A) => Effect.Effect + ): (self: Stream) => Effect.Effect + ( + self: Stream, + s: S, + cont: Predicate, + f: (s: S, a: A) => Effect.Effect + ): Effect.Effect +} = internal.runFoldWhileScopedEffect + +/** + * Consumes all elements of the stream, passing them to the specified + * callback. + * + * @since 2.0.0 + * @category destructors + */ +export const runForEach: { + ( + f: (a: A) => Effect.Effect + ): (self: Stream) => Effect.Effect + ( + self: Stream, + f: (a: A) => Effect.Effect + ): Effect.Effect +} = internal.runForEach + +/** + * Consumes all elements of the stream, passing them to the specified + * callback. + * + * @since 2.0.0 + * @category destructors + */ +export const runForEachChunk: { + ( + f: (a: Chunk.Chunk) => Effect.Effect + ): (self: Stream) => Effect.Effect + ( + self: Stream, + f: (a: Chunk.Chunk) => Effect.Effect + ): Effect.Effect +} = internal.runForEachChunk + +/** + * Like `Stream.runForEachChunk`, but returns a scoped effect so the + * finalization order can be controlled. + * + * @since 2.0.0 + * @category destructors + */ +export const runForEachChunkScoped: { + ( + f: (a: Chunk.Chunk) => Effect.Effect + ): (self: Stream) => Effect.Effect + ( + self: Stream, + f: (a: Chunk.Chunk) => Effect.Effect + ): Effect.Effect +} = internal.runForEachChunkScoped + +/** + * Like `Stream.forEach`, but returns a scoped effect so the finalization + * order can be controlled. + * + * @since 2.0.0 + * @category destructors + */ +export const runForEachScoped: { + ( + f: (a: A) => Effect.Effect + ): (self: Stream) => Effect.Effect + ( + self: Stream, + f: (a: A) => Effect.Effect + ): Effect.Effect +} = internal.runForEachScoped + +/** + * Consumes elements of the stream, passing them to the specified callback, + * and terminating consumption when the callback returns `false`. + * + * @since 2.0.0 + * @category destructors + */ +export const runForEachWhile: { + ( + f: (a: A) => Effect.Effect + ): (self: Stream) => Effect.Effect + ( + self: Stream, + f: (a: A) => Effect.Effect + ): Effect.Effect +} = internal.runForEachWhile + +/** + * Like `Stream.runForEachWhile`, but returns a scoped effect so the + * finalization order can be controlled. + * + * @since 2.0.0 + * @category destructors + */ +export const runForEachWhileScoped: { + ( + f: (a: A) => Effect.Effect + ): (self: Stream) => Effect.Effect + ( + self: Stream, + f: (a: A) => Effect.Effect + ): Effect.Effect +} = internal.runForEachWhileScoped + +/** + * Runs the stream to completion and yields the first value emitted by it, + * discarding the rest of the elements. + * + * @since 2.0.0 + * @category destructors + */ +export const runHead: (self: Stream) => Effect.Effect, E, R> = internal.runHead + +/** + * Publishes elements of this stream to a `PubSub`. Stream failure and ending will + * also be signalled. + * + * @since 2.0.0 + * @category destructors + */ +export const runIntoPubSub: { + (pubsub: PubSub.PubSub>): (self: Stream) => Effect.Effect + (self: Stream, pubsub: PubSub.PubSub>): Effect.Effect +} = internal.runIntoPubSub + +/** + * Like `Stream.runIntoPubSub`, but provides the result as a scoped effect to + * allow for scope composition. + * + * @since 2.0.0 + * @category destructors + */ +export const runIntoPubSubScoped: { + ( + pubsub: PubSub.PubSub> + ): (self: Stream) => Effect.Effect + (self: Stream, pubsub: PubSub.PubSub>): Effect.Effect +} = internal.runIntoPubSubScoped + +/** + * Enqueues elements of this stream into a queue. Stream failure and ending + * will also be signalled. + * + * @since 2.0.0 + * @category destructors + */ +export const runIntoQueue: { + (queue: Queue.Enqueue>): (self: Stream) => Effect.Effect + (self: Stream, queue: Queue.Enqueue>): Effect.Effect +} = internal.runIntoQueue + +/** + * Like `Stream.runIntoQueue`, but provides the result as a scoped Effect + * to allow for scope composition. + * + * @since 2.0.0 + * @category destructors + */ +export const runIntoQueueElementsScoped: { + ( + queue: Queue.Enqueue>> + ): (self: Stream) => Effect.Effect + ( + self: Stream, + queue: Queue.Enqueue>> + ): Effect.Effect +} = internal.runIntoQueueElementsScoped + +/** + * Like `Stream.runIntoQueue`, but provides the result as a scoped effect + * to allow for scope composition. + * + * @since 2.0.0 + * @category destructors + */ +export const runIntoQueueScoped: { + ( + queue: Queue.Enqueue> + ): (self: Stream) => Effect.Effect + (self: Stream, queue: Queue.Enqueue>): Effect.Effect +} = internal.runIntoQueueScoped + +/** + * Runs the stream to completion and yields the last value emitted by it, + * discarding the rest of the elements. + * + * @since 2.0.0 + * @category destructors + */ +export const runLast: (self: Stream) => Effect.Effect, E, R> = internal.runLast + +/** + * @since 2.0.0 + * @category destructors + */ +export const runScoped: { + ( + sink: Sink.Sink + ): (self: Stream) => Effect.Effect + ( + self: Stream, + sink: Sink.Sink + ): Effect.Effect +} = internal.runScoped + +/** + * Runs the stream to a sink which sums elements, provided they are Numeric. + * + * @since 2.0.0 + * @category destructors + */ +export const runSum: (self: Stream) => Effect.Effect = internal.runSum + +/** + * Statefully maps over the elements of this stream to produce all + * intermediate results of type `S` given an initial S. + * + * @example + * ```ts + * import { Effect, Stream } from "effect" + * + * const stream = Stream.range(1, 6).pipe(Stream.scan(0, (a, b) => a + b)) + * + * Effect.runPromise(Stream.runCollect(stream)).then(console.log) + * // { _id: 'Chunk', values: [ 0, 1, 3, 6, 10, 15, 21 ] } + * ``` + * + * @since 2.0.0 + * @category utils + */ +export const scan: { + (s: S, f: (s: S, a: A) => S): (self: Stream) => Stream + (self: Stream, s: S, f: (s: S, a: A) => S): Stream +} = internal.scan + +/** + * Statefully and effectfully maps over the elements of this stream to produce + * all intermediate results of type `S` given an initial S. + * + * @since 2.0.0 + * @category utils + */ +export const scanEffect: { + ( + s: S, + f: (s: S, a: A) => Effect.Effect + ): (self: Stream) => Stream + ( + self: Stream, + s: S, + f: (s: S, a: A) => Effect.Effect + ): Stream +} = internal.scanEffect + +/** + * Statefully maps over the elements of this stream to produce all + * intermediate results. + * + * See also `Stream.scan`. + * + * @since 2.0.0 + * @category utils + */ +export const scanReduce: { + (f: (a2: A2 | A, a: A) => A2): (self: Stream) => Stream + (self: Stream, f: (a2: A | A2, a: A) => A2): Stream +} = internal.scanReduce + +/** + * Statefully and effectfully maps over the elements of this stream to produce + * all intermediate results. + * + * See also `Stream.scanEffect`. + * + * @since 2.0.0 + * @category utils + */ +export const scanReduceEffect: { + ( + f: (a2: A2 | A, a: A) => Effect.Effect + ): (self: Stream) => Stream + ( + self: Stream, + f: (a2: A | A2, a: A) => Effect.Effect + ): Stream +} = internal.scanReduceEffect + +/** + * Schedules the output of the stream using the provided `schedule`. + * + * @since 2.0.0 + * @category utils + */ +export const schedule: { + ( + schedule: Schedule.Schedule + ): (self: Stream) => Stream + (self: Stream, schedule: Schedule.Schedule): Stream +} = internal.schedule + +/** + * Schedules the output of the stream using the provided `schedule` and emits + * its output at the end (if `schedule` is finite). Uses the provided function + * to align the stream and schedule outputs on the same type. + * + * @since 2.0.0 + * @category utils + */ +export const scheduleWith: { + ( + schedule: Schedule.Schedule, + options: { readonly onElement: (a: A) => C; readonly onSchedule: (b: B) => C } + ): (self: Stream) => Stream + ( + self: Stream, + schedule: Schedule.Schedule, + options: { readonly onElement: (a: A) => C; readonly onSchedule: (b: B) => C } + ): Stream +} = internal.scheduleWith + +/** + * Creates a single-valued stream from a scoped resource. + * + * @example + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * // Creating a single-valued stream from a scoped resource + * const stream = Stream.scoped( + * Effect.acquireRelease( + * Console.log("acquire"), + * () => Console.log("release") + * ) + * ).pipe( + * Stream.flatMap(() => Console.log("use")) + * ) + * + * Effect.runPromise(Stream.runCollect(stream)).then(console.log) + * // acquire + * // use + * // release + * // { _id: 'Chunk', values: [ undefined ] } + * ``` + * + * @since 2.0.0 + * @category constructors + */ +export const scoped: (effect: Effect.Effect) => Stream> = + internal.scoped + +/** + * Use a function that receives a scope and returns an effect to emit an output + * element. The output element will be the result of the returned effect, if + * successful. + * + * @since 3.11.0 + * @category constructors + */ +export const scopedWith: (f: (scope: Scope.Scope) => Effect.Effect) => Stream = + internal.scopedWith + +/** + * Emits a sliding window of `n` elements. + * + * ```ts + * import { pipe, Stream } from "effect" + * + * pipe( + * Stream.make(1, 2, 3, 4), + * Stream.sliding(2), + * Stream.runCollect + * ) + * // => Chunk(Chunk(1, 2), Chunk(2, 3), Chunk(3, 4)) + * ``` + * + * @since 2.0.0 + * @category utils + */ +export const sliding: { + (chunkSize: number): (self: Stream) => Stream, E, R> + (self: Stream, chunkSize: number): Stream, E, R> +} = internal.sliding + +/** + * Like `sliding`, but with a configurable `stepSize` parameter. + * + * @since 2.0.0 + * @category utils + */ +export const slidingSize: { + (chunkSize: number, stepSize: number): (self: Stream) => Stream, E, R> + (self: Stream, chunkSize: number, stepSize: number): Stream, E, R> +} = internal.slidingSize + +/** + * Converts an option on values into an option on errors. + * + * @since 2.0.0 + * @category utils + */ +export const some: (self: Stream, E, R>) => Stream, R> = internal.some + +/** + * Extracts the optional value, or returns the given 'default'. + * + * @since 2.0.0 + * @category utils + */ +export const someOrElse: { + (fallback: LazyArg): (self: Stream, E, R>) => Stream + (self: Stream, E, R>, fallback: LazyArg): Stream +} = internal.someOrElse + +/** + * Extracts the optional value, or fails with the given error 'e'. + * + * @since 2.0.0 + * @category utils + */ +export const someOrFail: { + (error: LazyArg): (self: Stream, E, R>) => Stream + (self: Stream, E, R>, error: LazyArg): Stream +} = internal.someOrFail + +/** + * Splits elements based on a predicate or refinement. + * + * ```ts + * import { pipe, Stream } from "effect" + * + * pipe( + * Stream.range(1, 10), + * Stream.split((n) => n % 4 === 0), + * Stream.runCollect + * ) + * // => Chunk(Chunk(1, 2, 3), Chunk(5, 6, 7), Chunk(9)) + * ``` + * + * @since 2.0.0 + * @category utils + */ +export const split: { + ( + refinement: Refinement, B> + ): (self: Stream) => Stream>, E, R> + (predicate: Predicate>): (self: Stream) => Stream, E, R> + (self: Stream, refinement: Refinement): Stream>, E, R> + (self: Stream, predicate: Predicate): Stream, E, R> +} = internal.split + +/** + * Splits elements on a delimiter and transforms the splits into desired output. + * + * @since 2.0.0 + * @category utils + */ +export const splitOnChunk: { + (delimiter: Chunk.Chunk): (self: Stream) => Stream, E, R> + (self: Stream, delimiter: Chunk.Chunk): Stream, E, R> +} = internal.splitOnChunk + +/** + * Splits strings on newlines. Handles both Windows newlines (`\r\n`) and UNIX + * newlines (`\n`). + * + * @since 2.0.0 + * @category combinators + */ +export const splitLines: (self: Stream) => Stream = internal.splitLines + +/** + * Creates a single-valued pure stream. + * + * @example + * ```ts + * import { Effect, Stream } from "effect" + * + * // A Stream with a single number + * const stream = Stream.succeed(3) + * + * Effect.runPromise(Stream.runCollect(stream)).then(console.log) + * // { _id: 'Chunk', values: [ 3 ] } + * ``` + * + * @since 2.0.0 + * @category constructors + */ +export const succeed: (value: A) => Stream = internal.succeed + +/** + * Creates a single-valued pure stream. + * + * @since 2.0.0 + * @category constructors + */ +export const sync: (evaluate: LazyArg) => Stream = internal.sync + +/** + * Returns a lazily constructed stream. + * + * @since 2.0.0 + * @category constructors + */ +export const suspend: (stream: LazyArg>) => Stream = internal.suspend + +/** + * Takes the specified number of elements from this stream. + * + * @example + * ```ts + * import { Effect, Stream } from "effect" + * + * const stream = Stream.take(Stream.iterate(0, (n) => n + 1), 5) + * + * Effect.runPromise(Stream.runCollect(stream)).then(console.log) + * // { _id: 'Chunk', values: [ 0, 1, 2, 3, 4 ] } + * ``` + * + * @since 2.0.0 + * @category utils + */ +export const take: { + (n: number): (self: Stream) => Stream + (self: Stream, n: number): Stream +} = internal.take + +/** + * Takes the last specified number of elements from this stream. + * + * @example + * ```ts + * import { Effect, Stream } from "effect" + * + * const stream = Stream.takeRight(Stream.make(1, 2, 3, 4, 5, 6), 3) + * + * Effect.runPromise(Stream.runCollect(stream)).then(console.log) + * // { _id: 'Chunk', values: [ 4, 5, 6 ] } + * ``` + * + * @since 2.0.0 + * @category utils + */ +export const takeRight: { + (n: number): (self: Stream) => Stream + (self: Stream, n: number): Stream +} = internal.takeRight + +/** + * Takes all elements of the stream until the specified predicate evaluates to + * `true`. + * + * @example + * ```ts + * import { Effect, Stream } from "effect" + * + * const stream = Stream.takeUntil(Stream.iterate(0, (n) => n + 1), (n) => n === 4) + * + * Effect.runPromise(Stream.runCollect(stream)).then(console.log) + * // { _id: 'Chunk', values: [ 0, 1, 2, 3, 4 ] } + * ``` + * + * @since 2.0.0 + * @category utils + */ +export const takeUntil: { + (predicate: Predicate>): (self: Stream) => Stream + (self: Stream, predicate: Predicate): Stream +} = internal.takeUntil + +/** + * Takes all elements of the stream until the specified effectual predicate + * evaluates to `true`. + * + * @since 2.0.0 + * @category utils + */ +export const takeUntilEffect: { + ( + predicate: (a: NoInfer) => Effect.Effect + ): (self: Stream) => Stream + ( + self: Stream, + predicate: (a: A) => Effect.Effect + ): Stream +} = internal.takeUntilEffect + +/** + * Takes all elements of the stream for as long as the specified predicate + * evaluates to `true`. + * + * @example + * ```ts + * import { Effect, Stream } from "effect" + * + * const stream = Stream.takeWhile(Stream.iterate(0, (n) => n + 1), (n) => n < 5) + * + * Effect.runPromise(Stream.runCollect(stream)).then(console.log) + * // { _id: 'Chunk', values: [ 0, 1, 2, 3, 4 ] } + * ``` + * + * @since 2.0.0 + * @category utils + */ +export const takeWhile: { + (refinement: Refinement, B>): (self: Stream) => Stream + (predicate: Predicate>): (self: Stream) => Stream + (self: Stream, refinement: Refinement): Stream + (self: Stream, predicate: Predicate): Stream +} = internal.takeWhile + +/** + * Adds an effect to consumption of every element of the stream. + * + * @example + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * const stream = Stream.make(1, 2, 3).pipe( + * Stream.tap((n) => Console.log(`before mapping: ${n}`)), + * Stream.map((n) => n * 2), + * Stream.tap((n) => Console.log(`after mapping: ${n}`)) + * ) + * + * Effect.runPromise(Stream.runCollect(stream)).then(console.log) + * // before mapping: 1 + * // after mapping: 2 + * // before mapping: 2 + * // after mapping: 4 + * // before mapping: 3 + * // after mapping: 6 + * // { _id: 'Chunk', values: [ 2, 4, 6 ] } + * ``` + * + * @since 2.0.0 + * @category sequencing + */ +export const tap: { + ( + f: (a: NoInfer) => Effect.Effect + ): (self: Stream) => Stream + (self: Stream, f: (a: NoInfer) => Effect.Effect): Stream +} = internal.tap + +/** + * Returns a stream that effectfully "peeks" at the failure or success of + * the stream. + * + * @since 2.0.0 + * @category sequencing + */ +export const tapBoth: { + ( + options: { + readonly onFailure: (e: NoInfer) => Effect.Effect + readonly onSuccess: (a: NoInfer) => Effect.Effect + } + ): (self: Stream) => Stream + ( + self: Stream, + options: { + readonly onFailure: (e: NoInfer) => Effect.Effect + readonly onSuccess: (a: NoInfer) => Effect.Effect + } + ): Stream +} = internal.tapBoth + +/** + * Returns a stream that effectfully "peeks" at the failure of the stream. + * + * @since 2.0.0 + * @category sequencing + */ +export const tapError: { + ( + f: (error: NoInfer) => Effect.Effect + ): (self: Stream) => Stream + (self: Stream, f: (error: E) => Effect.Effect): Stream +} = internal.tapError + +/** + * Returns a stream that effectfully "peeks" at the cause of failure of the + * stream. + * + * @since 2.0.0 + * @category utils + */ +export const tapErrorCause: { + ( + f: (cause: Cause.Cause>) => Effect.Effect + ): (self: Stream) => Stream + ( + self: Stream, + f: (cause: Cause.Cause) => Effect.Effect + ): Stream +} = internal.tapErrorCause + +/** + * Sends all elements emitted by this stream to the specified sink in addition + * to emitting them. + * + * @since 2.0.0 + * @category sequencing + */ +export const tapSink: { + (sink: Sink.Sink): (self: Stream) => Stream + (self: Stream, sink: Sink.Sink): Stream +} = internal.tapSink + +/** + * Delays the chunks of this stream according to the given bandwidth + * parameters using the token bucket algorithm. Allows for burst in the + * processing of elements by allowing the token bucket to accumulate tokens up + * to a `units + burst` threshold. The weight of each chunk is determined by + * the `cost` function. + * + * If using the "enforce" strategy, chunks that do not meet the bandwidth + * constraints are dropped. If using the "shape" strategy, chunks are delayed + * until they can be emitted without exceeding the bandwidth constraints. + * + * Defaults to the "shape" strategy. + * + * @example + * ```ts + * import { Chunk, Effect, Schedule, Stream } from "effect" + * + * let last = Date.now() + * const log = (message: string) => + * Effect.sync(() => { + * const end = Date.now() + * console.log(`${message} after ${end - last}ms`) + * last = end + * }) + * + * const stream = Stream.fromSchedule(Schedule.spaced("50 millis")).pipe( + * Stream.take(6), + * Stream.tap((n) => log(`Received ${n}`)), + * Stream.throttle({ + * cost: Chunk.size, + * duration: "100 millis", + * units: 1 + * }), + * Stream.tap((n) => log(`> Emitted ${n}`)) + * ) + * + * Effect.runPromise(Stream.runCollect(stream)).then(console.log) + * // Received 0 after 56ms + * // > Emitted 0 after 0ms + * // Received 1 after 52ms + * // > Emitted 1 after 48ms + * // Received 2 after 52ms + * // > Emitted 2 after 49ms + * // Received 3 after 52ms + * // > Emitted 3 after 48ms + * // Received 4 after 52ms + * // > Emitted 4 after 47ms + * // Received 5 after 52ms + * // > Emitted 5 after 49ms + * // { _id: 'Chunk', values: [ 0, 1, 2, 3, 4, 5 ] } + * ``` + * + * @since 2.0.0 + * @category utils + */ +export const throttle: { + ( + options: { + readonly cost: (chunk: Chunk.Chunk) => number + readonly units: number + readonly duration: Duration.DurationInput + readonly burst?: number | undefined + readonly strategy?: "enforce" | "shape" | undefined + } + ): (self: Stream) => Stream + ( + self: Stream, + options: { + readonly cost: (chunk: Chunk.Chunk) => number + readonly units: number + readonly duration: Duration.DurationInput + readonly burst?: number | undefined + readonly strategy?: "enforce" | "shape" | undefined + } + ): Stream +} = internal.throttle + +/** + * Delays the chunks of this stream according to the given bandwidth + * parameters using the token bucket algorithm. Allows for burst in the + * processing of elements by allowing the token bucket to accumulate tokens up + * to a `units + burst` threshold. The weight of each chunk is determined by + * the effectful `costFn` function. + * + * If using the "enforce" strategy, chunks that do not meet the bandwidth + * constraints are dropped. If using the "shape" strategy, chunks are delayed + * until they can be emitted without exceeding the bandwidth constraints. + * + * Defaults to the "shape" strategy. + * + * @since 2.0.0 + * @category utils + */ +export const throttleEffect: { + ( + options: { + readonly cost: (chunk: Chunk.Chunk) => Effect.Effect + readonly units: number + readonly duration: Duration.DurationInput + readonly burst?: number | undefined + readonly strategy?: "enforce" | "shape" | undefined + } + ): (self: Stream) => Stream + ( + self: Stream, + options: { + readonly cost: (chunk: Chunk.Chunk) => Effect.Effect + readonly units: number + readonly duration: Duration.DurationInput + readonly burst?: number | undefined + readonly strategy?: "enforce" | "shape" | undefined + } + ): Stream +} = internal.throttleEffect + +/** + * A stream that emits void values spaced by the specified duration. + * + * @example + * ```ts + * import { Effect, Stream } from "effect" + * + * let last = Date.now() + * const log = (message: string) => + * Effect.sync(() => { + * const end = Date.now() + * console.log(`${message} after ${end - last}ms`) + * last = end + * }) + * + * const stream = Stream.tick("1 seconds").pipe(Stream.tap(() => log("tick"))) + * + * Effect.runPromise(Stream.runCollect(stream.pipe(Stream.take(5)))).then(console.log) + * // tick after 4ms + * // tick after 1003ms + * // tick after 1001ms + * // tick after 1002ms + * // tick after 1002ms + * // { _id: 'Chunk', values: [ undefined, undefined, undefined, undefined, undefined ] } + * ``` + * + * @since 2.0.0 + * @category constructors + */ +export const tick: (interval: Duration.DurationInput) => Stream = internal.tick + +/** + * Ends the stream if it does not produce a value after the specified duration. + * + * @since 2.0.0 + * @category utils + */ +export const timeout: { + (duration: Duration.DurationInput): (self: Stream) => Stream + (self: Stream, duration: Duration.DurationInput): Stream +} = internal.timeout + +/** + * Fails the stream with given error if it does not produce a value after d + * duration. + * + * @since 2.0.0 + * @category utils + */ +export const timeoutFail: { + (error: LazyArg, duration: Duration.DurationInput): (self: Stream) => Stream + (self: Stream, error: LazyArg, duration: Duration.DurationInput): Stream +} = internal.timeoutFail + +/** + * Fails the stream with given cause if it does not produce a value after d + * duration. + * + * @since 2.0.0 + * @category utils + */ +export const timeoutFailCause: { + ( + cause: LazyArg>, + duration: Duration.DurationInput + ): (self: Stream) => Stream + ( + self: Stream, + cause: LazyArg>, + duration: Duration.DurationInput + ): Stream +} = internal.timeoutFailCause + +/** + * Switches the stream if it does not produce a value after the specified + * duration. + * + * @since 2.0.0 + * @category utils + */ +export const timeoutTo: { + ( + duration: Duration.DurationInput, + that: Stream + ): (self: Stream) => Stream + ( + self: Stream, + duration: Duration.DurationInput, + that: Stream + ): Stream +} = internal.timeoutTo + +/** + * Converts the stream to a scoped `PubSub` of chunks. After the scope is closed, + * the `PubSub` will never again produce values and should be discarded. + * + * @since 2.0.0 + * @category destructors + */ +export const toPubSub: { + ( + capacity: number | { readonly capacity: "unbounded"; readonly replay?: number | undefined } | { + readonly capacity: number + readonly strategy?: "sliding" | "dropping" | "suspend" | undefined + readonly replay?: number | undefined + } + ): (self: Stream) => Effect.Effect>, never, Scope.Scope | R> + ( + self: Stream, + capacity: number | { readonly capacity: "unbounded"; readonly replay?: number | undefined } | { + readonly capacity: number + readonly strategy?: "sliding" | "dropping" | "suspend" | undefined + readonly replay?: number | undefined + } + ): Effect.Effect>, never, Scope.Scope | R> +} = internal.toPubSub + +/** + * Returns in a scope an Effect that can be used to repeatedly pull chunks + * from the stream. The pull effect fails with None when the stream is + * finished, or with Some error if it fails, otherwise it returns a chunk of + * the stream's output. + * + * @example + * ```ts + * import { Effect, Stream } from "effect" + * + * // Simulate a chunked stream + * const stream = Stream.fromIterable([1, 2, 3, 4, 5]).pipe(Stream.rechunk(2)) + * + * const program = Effect.gen(function*() { + * // Create an effect to get data chunks from the stream + * const getChunk = yield* Stream.toPull(stream) + * + * // Continuously fetch and process chunks + * while (true) { + * const chunk = yield* getChunk + * console.log(chunk) + * } + * }) + * + * Effect.runPromise(Effect.scoped(program)).then(console.log, console.error) + * // { _id: 'Chunk', values: [ 1, 2 ] } + * // { _id: 'Chunk', values: [ 3, 4 ] } + * // { _id: 'Chunk', values: [ 5 ] } + * // (FiberFailure) Error: { + * // "_id": "Option", + * // "_tag": "None" + * // } + * ``` + * + * @since 2.0.0 + * @category destructors + */ +export const toPull: ( + self: Stream +) => Effect.Effect, Option.Option, R>, never, Scope.Scope | R> = internal.toPull + +/** + * Converts the stream to a scoped queue of chunks. After the scope is closed, + * the queue will never again produce values and should be discarded. + * + * Defaults to the "suspend" back pressure strategy with a capacity of 2. + * + * @since 2.0.0 + * @category destructors + */ +export const toQueue: { + ( + options?: + | { readonly strategy?: "dropping" | "sliding" | "suspend" | undefined; readonly capacity?: number | undefined } + | { readonly strategy: "unbounded" } + | undefined + ): (self: Stream) => Effect.Effect>, never, Scope.Scope | R> + ( + self: Stream, + options?: + | { readonly strategy?: "dropping" | "sliding" | "suspend" | undefined; readonly capacity?: number | undefined } + | { readonly strategy: "unbounded" } + | undefined + ): Effect.Effect>, never, Scope.Scope | R> +} = internal.toQueue + +/** + * Converts the stream to a scoped queue of elements. After the scope is + * closed, the queue will never again produce values and should be discarded. + * + * Defaults to a capacity of 2. + * + * @since 2.0.0 + * @category destructors + */ +export const toQueueOfElements: { + ( + options?: { readonly capacity?: number | undefined } | undefined + ): ( + self: Stream + ) => Effect.Effect>>, never, Scope.Scope | R> + ( + self: Stream, + options?: { readonly capacity?: number | undefined } | undefined + ): Effect.Effect>>, never, Scope.Scope | R> +} = internal.toQueueOfElements + +/** + * Converts the stream to a `ReadableStream`. + * + * See https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream. + * + * @since 2.0.0 + * @category destructors + */ +export const toReadableStream: { + ( + options?: { readonly strategy?: QueuingStrategy | undefined } + ): ( + self: Stream + ) => ReadableStream + ( + self: Stream, + options?: { readonly strategy?: QueuingStrategy | undefined } + ): ReadableStream +} = internal.toReadableStream + +/** + * Converts the stream to a `Effect`. + * + * See https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream. + * + * @since 2.0.0 + * @category destructors + */ +export const toReadableStreamEffect: { + ( + options?: { readonly strategy?: QueuingStrategy | undefined } + ): ( + self: Stream + ) => Effect.Effect, never, R> + ( + self: Stream, + options?: { readonly strategy?: QueuingStrategy | undefined } + ): Effect.Effect, never, R> +} = internal.toReadableStreamEffect + +/** + * Converts the stream to a `ReadableStream` using the provided runtime. + * + * See https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream. + * + * @since 2.0.0 + * @category destructors + */ +export const toReadableStreamRuntime: { + ( + runtime: Runtime, + options?: { readonly strategy?: QueuingStrategy | undefined } + ): (self: Stream) => ReadableStream + ( + self: Stream, + runtime: Runtime, + options?: { readonly strategy?: QueuingStrategy | undefined } + ): ReadableStream +} = internal.toReadableStreamRuntime + +/** + * Converts the stream to a `AsyncIterable` using the provided runtime. + * + * @since 3.15.0 + * @category destructors + */ +export const toAsyncIterableRuntime: { + (runtime: Runtime): (self: Stream) => AsyncIterable + (self: Stream, runtime: Runtime): AsyncIterable +} = internal.toAsyncIterableRuntime + +/** + * Converts the stream to a `AsyncIterable` capturing the required dependencies. + * + * @since 3.15.0 + * @category destructors + */ +export const toAsyncIterableEffect: (self: Stream) => Effect.Effect, never, R> = + internal.toAsyncIterableEffect + +/** + * Converts the stream to a `AsyncIterable`. + * + * @since 3.15.0 + * @category destructors + */ +export const toAsyncIterable: (self: Stream) => AsyncIterable = internal.toAsyncIterable + +/** + * Applies the transducer to the stream and emits its outputs. + * + * @since 2.0.0 + * @category utils + */ +export const transduce: { + (sink: Sink.Sink): (self: Stream) => Stream + (self: Stream, sink: Sink.Sink): Stream +} = internal.transduce + +/** + * Creates a stream by peeling off the "layers" of a value of type `S`. + * + * @example + * ```ts + * import { Effect, Option, Stream } from "effect" + * + * const stream = Stream.unfold(1, (n) => Option.some([n, n + 1])) + * + * Effect.runPromise(Stream.runCollect(stream.pipe(Stream.take(5)))).then(console.log) + * // { _id: 'Chunk', values: [ 1, 2, 3, 4, 5 ] } + * ``` + * + * @since 2.0.0 + * @category constructors + */ +export const unfold: (s: S, f: (s: S) => Option.Option) => Stream = internal.unfold + +/** + * Creates a stream by peeling off the "layers" of a value of type `S`. + * + * @since 2.0.0 + * @category constructors + */ +export const unfoldChunk: ( + s: S, + f: (s: S) => Option.Option, S]> +) => Stream = internal.unfoldChunk + +/** + * Creates a stream by effectfully peeling off the "layers" of a value of type + * `S`. + * + * @since 2.0.0 + * @category constructors + */ +export const unfoldChunkEffect: ( + s: S, + f: (s: S) => Effect.Effect, S]>, E, R> +) => Stream = internal.unfoldChunkEffect + +/** + * Creates a stream by effectfully peeling off the "layers" of a value of type + * `S`. + * + * @example + * ```ts + * import { Effect, Option, Random, Stream } from "effect" + * + * const stream = Stream.unfoldEffect(1, (n) => + * Random.nextBoolean.pipe( + * Effect.map((b) => (b ? Option.some([n, -n]) : Option.some([n, n]))) + * )) + * + * Effect.runPromise(Stream.runCollect(stream.pipe(Stream.take(5)))).then(console.log) + * // { _id: 'Chunk', values: [ 1, -1, -1, -1, -1 ] } + * ``` + * + * @since 2.0.0 + * @category constructors + */ +export const unfoldEffect: ( + s: S, + f: (s: S) => Effect.Effect, E, R> +) => Stream = internal.unfoldEffect + +const void_: Stream = internal.void +export { + /** + * A stream that contains a single `void` value. + * + * @example + * ```ts + * import { Effect, Stream } from "effect" + * + * const stream = Stream.void + * + * Effect.runPromise(Stream.runCollect(stream)).then(console.log) + * // { _id: 'Chunk', values: [ undefined ] } + * + * ``` + * @since 2.0.0 + * @category constructors + */ + void_ as void +} + +/** + * Creates a stream produced from an `Effect`. + * + * @since 2.0.0 + * @category constructors + */ +export const unwrap: (effect: Effect.Effect, E, R>) => Stream = + internal.unwrap + +/** + * Creates a stream produced from a scoped `Effect`. + * + * @since 2.0.0 + * @category constructors + */ +export const unwrapScoped: ( + effect: Effect.Effect, E, R> +) => Stream> = internal.unwrapScoped + +/** + * Creates a stream produced from a function which receives a `Scope` and + * returns an `Effect`. The resulting stream will emit a single element, which + * will be the result of the returned effect, if successful. + * + * @since 3.11.0 + * @category constructors + */ +export const unwrapScopedWith: ( + f: (scope: Scope.Scope) => Effect.Effect, E, R> +) => Stream = internal.unwrapScopedWith + +/** + * Updates the specified service within the context of the `Stream`. + * + * @since 2.0.0 + * @category context + */ +export const updateService: { + ( + tag: Context.Tag, + f: (service: NoInfer) => NoInfer + ): (self: Stream) => Stream + ( + self: Stream, + tag: Context.Tag, + f: (service: NoInfer) => NoInfer + ): Stream +} = internal.updateService + +/** + * Returns the specified stream if the given condition is satisfied, otherwise + * returns an empty stream. + * + * @since 2.0.0 + * @category utils + */ +export const when: { + (test: LazyArg): (self: Stream) => Stream + (self: Stream, test: LazyArg): Stream +} = internal.when + +/** + * Returns the resulting stream when the given `PartialFunction` is defined + * for the given value, otherwise returns an empty stream. + * + * @since 2.0.0 + * @category constructors + */ +export const whenCase: ( + evaluate: LazyArg, + pf: (a: A) => Option.Option> +) => Stream = internal.whenCase + +/** + * Returns the stream when the given partial function is defined for the given + * effectful value, otherwise returns an empty stream. + * + * @since 2.0.0 + * @category utils + */ +export const whenCaseEffect: { + ( + pf: (a: A) => Option.Option> + ): (self: Effect.Effect) => Stream + ( + self: Effect.Effect, + pf: (a: A) => Option.Option> + ): Stream +} = internal.whenCaseEffect + +/** + * Returns the stream if the given effectful condition is satisfied, otherwise + * returns an empty stream. + * + * @since 2.0.0 + * @category utils + */ +export const whenEffect: { + (effect: Effect.Effect): (self: Stream) => Stream + (self: Stream, effect: Effect.Effect): Stream +} = internal.whenEffect + +/** + * Wraps the stream with a new span for tracing. + * + * @since 2.0.0 + * @category tracing + */ +export const withSpan: { + ( + name: string, + options?: Tracer.SpanOptions | undefined + ): (self: Stream) => Stream> + ( + self: Stream, + name: string, + options?: Tracer.SpanOptions | undefined + ): Stream> +} = internal.withSpan + +/** + * Zips this stream with another point-wise and emits tuples of elements from + * both streams. + * + * The new stream will end when one of the sides ends. + * + * @example + * ```ts + * import { Effect, Stream } from "effect" + * + * // We create two streams and zip them together. + * const stream = Stream.zip( + * Stream.make(1, 2, 3, 4, 5, 6), + * Stream.make("a", "b", "c") + * ) + * + * Effect.runPromise(Stream.runCollect(stream)).then(console.log) + * // { _id: 'Chunk', values: [ [ 1, 'a' ], [ 2, 'b' ], [ 3, 'c' ] ] } + * ``` + * + * @since 2.0.0 + * @category zipping + */ +export const zip: { + (that: Stream): (self: Stream) => Stream<[A, A2], E2 | E, R2 | R> + (self: Stream, that: Stream): Stream<[A, A2], E | E2, R | R2> +} = internal.zip + +/** + * Zips this stream with another point-wise and emits tuples of elements from + * both streams. + * + * The new stream will end when one of the sides ends. + * + * @since 2.0.0 + * @category zipping + */ +export const zipFlatten: { + ( + that: Stream + ): , E, R>(self: Stream) => Stream<[...A, A2], E2 | E, R2 | R> + , E, R, A2, E2, R2>( + self: Stream, + that: Stream + ): Stream<[...A, A2], E | E2, R | R2> +} = internal.zipFlatten + +/** + * Zips this stream with another point-wise, creating a new stream of pairs of + * elements from both sides. + * + * The defaults `defaultLeft` and `defaultRight` will be used if the streams + * have different lengths and one of the streams has ended before the other. + * + * @example + * ```ts + * import { Effect, Stream } from "effect" + * + * const stream = Stream.zipAll(Stream.make(1, 2, 3, 4, 5, 6), { + * other: Stream.make("a", "b", "c"), + * defaultSelf: 0, + * defaultOther: "x" + * }) + * + * Effect.runPromise(Stream.runCollect(stream)).then(console.log) + * // { _id: "Chunk", values: [ [ 1, "a" ], [ 2, "b" ], [ 3, "c" ], [ 4, "x" ], [ 5, "x" ], [ 6, "x" ] ] } + * ``` + * + * @since 2.0.0 + * @category zipping + */ +export const zipAll: { + ( + options: { readonly other: Stream; readonly defaultSelf: A; readonly defaultOther: A2 } + ): (self: Stream) => Stream<[A, A2], E2 | E, R2 | R> + ( + self: Stream, + options: { readonly other: Stream; readonly defaultSelf: A; readonly defaultOther: A2 } + ): Stream<[A, A2], E | E2, R | R2> +} = internal.zipAll + +/** + * Zips this stream with another point-wise, and keeps only elements from this + * stream. + * + * The provided default value will be used if the other stream ends before + * this one. + * + * @since 2.0.0 + * @category zipping + */ +export const zipAllLeft: { + (that: Stream, defaultLeft: A): (self: Stream) => Stream + (self: Stream, that: Stream, defaultLeft: A): Stream +} = internal.zipAllLeft + +/** + * Zips this stream with another point-wise, and keeps only elements from the + * other stream. + * + * The provided default value will be used if this stream ends before the + * other one. + * + * @since 2.0.0 + * @category zipping + */ +export const zipAllRight: { + ( + that: Stream, + defaultRight: A2 + ): (self: Stream) => Stream + (self: Stream, that: Stream, defaultRight: A2): Stream +} = internal.zipAllRight + +/** + * Zips this stream that is sorted by distinct keys and the specified stream + * that is sorted by distinct keys to produce a new stream that is sorted by + * distinct keys. Combines values associated with each key into a tuple, + * using the specified values `defaultLeft` and `defaultRight` to fill in + * missing values. + * + * This allows zipping potentially unbounded streams of data by key in + * constant space but the caller is responsible for ensuring that the + * streams are sorted by distinct keys. + * + * @since 2.0.0 + * @category zipping + */ +export const zipAllSortedByKey: { + ( + options: { + readonly other: Stream + readonly defaultSelf: A + readonly defaultOther: A2 + readonly order: Order.Order + } + ): (self: Stream) => Stream<[K, [A, A2]], E2 | E, R2 | R> + ( + self: Stream, + options: { + readonly other: Stream + readonly defaultSelf: A + readonly defaultOther: A2 + readonly order: Order.Order + } + ): Stream<[K, [A, A2]], E | E2, R | R2> +} = internal.zipAllSortedByKey + +/** + * Zips this stream that is sorted by distinct keys and the specified stream + * that is sorted by distinct keys to produce a new stream that is sorted by + * distinct keys. Keeps only values from this stream, using the specified + * value `default` to fill in missing values. + * + * This allows zipping potentially unbounded streams of data by key in + * constant space but the caller is responsible for ensuring that the + * streams are sorted by distinct keys. + * + * @since 2.0.0 + * @category zipping + */ +export const zipAllSortedByKeyLeft: { + ( + options: { + readonly other: Stream + readonly defaultSelf: A + readonly order: Order.Order + } + ): (self: Stream) => Stream<[K, A], E2 | E, R2 | R> + ( + self: Stream, + options: { + readonly other: Stream + readonly defaultSelf: A + readonly order: Order.Order + } + ): Stream<[K, A], E | E2, R | R2> +} = internal.zipAllSortedByKeyLeft + +/** + * Zips this stream that is sorted by distinct keys and the specified stream + * that is sorted by distinct keys to produce a new stream that is sorted by + * distinct keys. Keeps only values from that stream, using the specified + * value `default` to fill in missing values. + * + * This allows zipping potentially unbounded streams of data by key in + * constant space but the caller is responsible for ensuring that the + * streams are sorted by distinct keys. + * + * @since 2.0.0 + * @category zipping + */ +export const zipAllSortedByKeyRight: { + ( + options: { + readonly other: Stream + readonly defaultOther: A2 + readonly order: Order.Order + } + ): (self: Stream) => Stream<[K, A2], E2 | E, R2 | R> + ( + self: Stream, + options: { + readonly other: Stream + readonly defaultOther: A2 + readonly order: Order.Order + } + ): Stream<[K, A2], E | E2, R | R2> +} = internal.zipAllSortedByKeyRight + +/** + * Zips this stream that is sorted by distinct keys and the specified stream + * that is sorted by distinct keys to produce a new stream that is sorted by + * distinct keys. Uses the functions `left`, `right`, and `both` to handle + * the cases where a key and value exist in this stream, that stream, or + * both streams. + * + * This allows zipping potentially unbounded streams of data by key in + * constant space but the caller is responsible for ensuring that the + * streams are sorted by distinct keys. + * + * @since 2.0.0 + * @category zipping + */ +export const zipAllSortedByKeyWith: { + ( + options: { + readonly other: Stream + readonly onSelf: (a: A) => A3 + readonly onOther: (a2: A2) => A3 + readonly onBoth: (a: A, a2: A2) => A3 + readonly order: Order.Order + } + ): (self: Stream) => Stream<[K, A3], E2 | E, R2 | R> + ( + self: Stream, + options: { + readonly other: Stream + readonly onSelf: (a: A) => A3 + readonly onOther: (a2: A2) => A3 + readonly onBoth: (a: A, a2: A2) => A3 + readonly order: Order.Order + } + ): Stream<[K, A3], E | E2, R | R2> +} = internal.zipAllSortedByKeyWith + +/** + * Zips this stream with another point-wise. The provided functions will be + * used to create elements for the composed stream. + * + * The functions `left` and `right` will be used if the streams have different + * lengths and one of the streams has ended before the other. + * + * @example + * ```ts + * import { Effect, Stream } from "effect" + * + * const stream = Stream.zipAllWith(Stream.make(1, 2, 3, 4, 5, 6), { + * other: Stream.make("a", "b", "c"), + * onSelf: (n) => [n, "x"], + * onOther: (s) => [0, s], + * onBoth: (n, s) => [n - s.length, s] + * }) + * + * Effect.runPromise(Stream.runCollect(stream)).then(console.log) + * // { _id: "Chunk", values: [ [ 0, "a" ], [ 1, "b" ], [ 2, "c" ], [ 4, "x" ], [ 5, "x" ], [ 6, "x" ] ] } + * ``` + * + * @since 2.0.0 + * @category zipping + */ +export const zipAllWith: { + ( + options: { + readonly other: Stream + readonly onSelf: (a: A) => A3 + readonly onOther: (a2: A2) => A3 + readonly onBoth: (a: A, a2: A2) => A3 + } + ): (self: Stream) => Stream + ( + self: Stream, + options: { + readonly other: Stream + readonly onSelf: (a: A) => A3 + readonly onOther: (a2: A2) => A3 + readonly onBoth: (a: A, a2: A2) => A3 + } + ): Stream +} = internal.zipAllWith + +/** + * Zips the two streams so that when a value is emitted by either of the two + * streams, it is combined with the latest value from the other stream to + * produce a result. + * + * Note: tracking the latest value is done on a per-chunk basis. That means + * that emitted elements that are not the last value in chunks will never be + * used for zipping. + * + * @example + * ```ts + * import { Effect, Schedule, Stream } from "effect" + * + * const s1 = Stream.make(1, 2, 3).pipe( + * Stream.schedule(Schedule.spaced("1 second")) + * ) + * + * const s2 = Stream.make("a", "b", "c", "d").pipe( + * Stream.schedule(Schedule.spaced("500 millis")) + * ) + * + * const stream = Stream.zipLatest(s1, s2) + * + * Effect.runPromise(Stream.runCollect(stream)).then(console.log) + * // { _id: "Chunk", values: [ [ 1, "a" ], [ 1, "b" ], [ 2, "b" ], [ 2, "c" ], [ 2, "d" ], [ 3, "d" ] ] } + * ``` + * + * @since 2.0.0 + * @category zipping + */ +export const zipLatest: { + (right: Stream): (left: Stream) => Stream<[AL, AR], EL | ER, RL | RR> + (left: Stream, right: Stream): Stream<[AL, AR], EL | ER, RL | RR> +} = internal.zipLatest + +/** + * Zips multiple streams so that when a value is emitted by any of the streams, + * it is combined with the latest values from the other streams to produce a result. + * + * Note: tracking the latest value is done on a per-chunk basis. That means + * that emitted elements that are not the last value in chunks will never be + * used for zipping. + * + * @example + * ```ts + * import { Stream, Schedule, Console, Effect } from "effect" + * + * const stream = Stream.zipLatestAll( + * Stream.fromSchedule(Schedule.spaced('1 millis')), + * Stream.fromSchedule(Schedule.spaced('2 millis')), + * Stream.fromSchedule(Schedule.spaced('4 millis')), + * ).pipe(Stream.take(6), Stream.tap(Console.log)) + * + * Effect.runPromise(Stream.runDrain(stream)) + * // Output: + * // [ 0, 0, 0 ] + * // [ 1, 0, 0 ] + * // [ 1, 1, 0 ] + * // [ 2, 1, 0 ] + * // [ 3, 1, 0 ] + * // [ 3, 1, 1 ] + * // ..... + * ``` + * + * @since 3.3.0 + * @category zipping + */ +export const zipLatestAll: >>( + ...streams: T +) => Stream< + [T[number]] extends [never] ? never + : { [K in keyof T]: T[K] extends Stream ? A : never }, + [T[number]] extends [never] ? never : T[number] extends Stream ? _E : never, + [T[number]] extends [never] ? never : T[number] extends Stream ? _R : never +> = internal.zipLatestAll + +/** + * Zips the two streams so that when a value is emitted by either of the two + * streams, it is combined with the latest value from the other stream to + * produce a result. + * + * Note: tracking the latest value is done on a per-chunk basis. That means + * that emitted elements that are not the last value in chunks will never be + * used for zipping. + * + * @since 2.0.0 + * @category zipping + */ +export const zipLatestWith: { + ( + right: Stream, + f: (left: AL, right: AR) => A + ): (left: Stream) => Stream + ( + left: Stream, + right: Stream, + f: (left: AL, right: AR) => A + ): Stream +} = internal.zipLatestWith + +/** + * Zips this stream with another point-wise, but keeps only the outputs of + * `left` stream. + * + * The new stream will end when one of the sides ends. + * + * @since 2.0.0 + * @category zipping + */ +export const zipLeft: { + (right: Stream): (left: Stream) => Stream + (left: Stream, right: Stream): Stream +} = internal.zipLeft + +/** + * Zips this stream with another point-wise, but keeps only the outputs of the + * `right` stream. + * + * The new stream will end when one of the sides ends. + * + * @since 2.0.0 + * @category zipping + */ +export const zipRight: { + (right: Stream): (left: Stream) => Stream + (left: Stream, right: Stream): Stream +} = internal.zipRight + +/** + * Zips this stream with another point-wise and applies the function to the + * paired elements. + * + * The new stream will end when one of the sides ends. + * + * @example + * ```ts + * import { Effect, Stream } from "effect" + * + * // We create two streams and zip them with custom logic. + * const stream = Stream.zipWith( + * Stream.make(1, 2, 3, 4, 5, 6), + * Stream.make("a", "b", "c"), + * (n, s) => [n - s.length, s] + * ) + * + * Effect.runPromise(Stream.runCollect(stream)).then(console.log) + * // { _id: 'Chunk', values: [ [ 0, 'a' ], [ 1, 'b' ], [ 2, 'c' ] ] } + * ``` + * + * @since 2.0.0 + * @category zipping + */ +export const zipWith: { + ( + right: Stream, + f: (left: AL, right: AR) => A + ): (left: Stream) => Stream + ( + left: Stream, + right: Stream, + f: (left: AL, right: AR) => A + ): Stream +} = internal.zipWith + +/** + * Zips this stream with another point-wise and applies the function to the + * paired elements. + * + * The new stream will end when one of the sides ends. + * + * @since 2.0.0 + * @category zipping + */ +export const zipWithChunks: { + ( + that: Stream, + f: ( + left: Chunk.Chunk, + right: Chunk.Chunk + ) => readonly [Chunk.Chunk, Either.Either, Chunk.Chunk>] + ): (self: Stream) => Stream + ( + self: Stream, + that: Stream, + f: ( + left: Chunk.Chunk, + right: Chunk.Chunk + ) => readonly [Chunk.Chunk, Either.Either, Chunk.Chunk>] + ): Stream +} = internal.zipWithChunks + +/** + * Zips each element with the next element if present. + * + * @example + * ```ts + * import { Chunk, Effect, Stream } from "effect" + * + * const stream = Stream.zipWithNext(Stream.make(1, 2, 3, 4)) + * + * Effect.runPromise(Stream.runCollect(stream)).then((chunk) => console.log(Chunk.toArray(chunk))) + * // [ + * // [ 1, { _id: 'Option', _tag: 'Some', value: 2 } ], + * // [ 2, { _id: 'Option', _tag: 'Some', value: 3 } ], + * // [ 3, { _id: 'Option', _tag: 'Some', value: 4 } ], + * // [ 4, { _id: 'Option', _tag: 'None' } ] + * // ] + * ``` + * + * @since 2.0.0 + * @category zipping + */ +export const zipWithNext: (self: Stream) => Stream<[A, Option.Option], E, R> = internal.zipWithNext + +/** + * Zips each element with the previous element. Initially accompanied by + * `None`. + * + * @example + * ```ts + * import { Chunk, Effect, Stream } from "effect" + * + * const stream = Stream.zipWithPrevious(Stream.make(1, 2, 3, 4)) + * + * Effect.runPromise(Stream.runCollect(stream)).then((chunk) => console.log(Chunk.toArray(chunk))) + * // [ + * // [ { _id: 'Option', _tag: 'None' }, 1 ], + * // [ { _id: 'Option', _tag: 'Some', value: 1 }, 2 ], + * // [ { _id: 'Option', _tag: 'Some', value: 2 }, 3 ], + * // [ { _id: 'Option', _tag: 'Some', value: 3 }, 4 ] + * // ] + * ``` + * + * @since 2.0.0 + * @category zipping + */ +export const zipWithPrevious: (self: Stream) => Stream<[Option.Option, A], E, R> = + internal.zipWithPrevious + +/** + * Zips each element with both the previous and next element. + * + * @example + * ```ts + * import { Chunk, Effect, Stream } from "effect" + * + * const stream = Stream.zipWithPreviousAndNext(Stream.make(1, 2, 3, 4)) + * + * Effect.runPromise(Stream.runCollect(stream)).then((chunk) => console.log(Chunk.toArray(chunk))) + * // [ + * // [ + * // { _id: 'Option', _tag: 'None' }, + * // 1, + * // { _id: 'Option', _tag: 'Some', value: 2 } + * // ], + * // [ + * // { _id: 'Option', _tag: 'Some', value: 1 }, + * // 2, + * // { _id: 'Option', _tag: 'Some', value: 3 } + * // ], + * // [ + * // { _id: 'Option', _tag: 'Some', value: 2 }, + * // 3, + * // { _id: 'Option', _tag: 'Some', value: 4 } + * // ], + * // [ + * // { _id: 'Option', _tag: 'Some', value: 3 }, + * // 4, + * // { _id: 'Option', _tag: 'None' } + * // ] + * // ] + * ``` + * + * @since 2.0.0 + * @category zipping + */ +export const zipWithPreviousAndNext: ( + self: Stream +) => Stream<[Option.Option, A, Option.Option], E, R> = internal.zipWithPreviousAndNext + +/** + * Zips this stream together with the index of elements. + * + * @example + * ```ts + * import { Effect, Stream } from "effect" + * + * const stream = Stream.make("Mary", "James", "Robert", "Patricia") + * + * const indexedStream = Stream.zipWithIndex(stream) + * + * Effect.runPromise(Stream.runCollect(indexedStream)).then(console.log) + * // { + * // _id: 'Chunk', + * // values: [ [ 'Mary', 0 ], [ 'James', 1 ], [ 'Robert', 2 ], [ 'Patricia', 3 ] ] + * // } + * ``` + * + * @since 2.0.0 + * @category zipping + */ +export const zipWithIndex: (self: Stream) => Stream<[A, number], E, R> = internal.zipWithIndex + +// ------------------------------------------------------------------------------------- +// do notation +// ------------------------------------------------------------------------------------- + +/** + * The "do simulation" in Effect allows you to write code in a more declarative style, similar to the "do notation" in other programming languages. It provides a way to define variables and perform operations on them using functions like `bind` and `let`. + * + * Here's how the do simulation works: + * + * 1. Start the do simulation using the `Do` value + * 2. Within the do simulation scope, you can use the `bind` function to define variables and bind them to `Stream` values + * 3. You can accumulate multiple `bind` statements to define multiple variables within the scope + * 4. Inside the do simulation scope, you can also use the `let` function to define variables and bind them to simple values + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { Chunk, Effect, pipe, Stream } from "effect" + * + * const result = pipe( + * Stream.Do, + * Stream.bind("x", () => Stream.succeed(2)), + * Stream.bind("y", () => Stream.succeed(3)), + * Stream.let("sum", ({ x, y }) => x + y) + * ) + * assert.deepStrictEqual(Effect.runSync(Stream.runCollect(result)), Chunk.of({ x: 2, y: 3, sum: 5 })) + * ``` + * + * @see {@link bindTo} + * @see {@link bind} + * @see {@link bindEffect} + * @see {@link let_ let} + * + * @category do notation + * @since 2.0.0 + */ +export const Do: Stream<{}> = internal.Do + +/** + * The "do simulation" in Effect allows you to write code in a more declarative style, similar to the "do notation" in other programming languages. It provides a way to define variables and perform operations on them using functions like `bind` and `let`. + * + * Here's how the do simulation works: + * + * 1. Start the do simulation using the `Do` value + * 2. Within the do simulation scope, you can use the `bind` function to define variables and bind them to `Stream` values + * 3. You can accumulate multiple `bind` statements to define multiple variables within the scope + * 4. Inside the do simulation scope, you can also use the `let` function to define variables and bind them to simple values + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { Chunk, Effect, pipe, Stream } from "effect" + * + * const result = pipe( + * Stream.Do, + * Stream.bind("x", () => Stream.succeed(2)), + * Stream.bind("y", () => Stream.succeed(3)), + * Stream.let("sum", ({ x, y }) => x + y) + * ) + * assert.deepStrictEqual(Effect.runSync(Stream.runCollect(result)), Chunk.of({ x: 2, y: 3, sum: 5 })) + * ``` + * + * @see {@link Do} + * @see {@link bindTo} + * @see {@link bindEffect} + * @see {@link let_ let} + * + * @category do notation + * @since 2.0.0 + */ +export const bind: { + ( + tag: Exclude, + f: (_: NoInfer) => Stream, + options?: + | { readonly concurrency?: number | "unbounded" | undefined; readonly bufferSize?: number | undefined } + | undefined + ): (self: Stream) => Stream<{ [K in N | keyof A]: K extends keyof A ? A[K] : B }, E2 | E, R2 | R> + ( + self: Stream, + tag: Exclude, + f: (_: NoInfer) => Stream, + options?: + | { readonly concurrency?: number | "unbounded" | undefined; readonly bufferSize?: number | undefined } + | undefined + ): Stream<{ [K in N | keyof A]: K extends keyof A ? A[K] : B }, E | E2, R | R2> +} = internal.bind + +/** + * Binds an effectful value in a `do` scope + * + * @see {@link Do} + * @see {@link bindTo} + * @see {@link bind} + * @see {@link let_ let} + * + * @since 2.0.0 + * @category do notation + */ +export const bindEffect: { + ( + tag: Exclude, + f: (_: NoInfer) => Effect.Effect, + options?: { readonly concurrency?: number | "unbounded" | undefined; readonly bufferSize?: number | undefined } + ): (self: Stream) => Stream<{ [K in keyof A | N]: K extends keyof A ? A[K] : B }, E | E2, R | R2> + ( + self: Stream, + tag: Exclude, + f: (_: NoInfer) => Effect.Effect, + options?: { readonly concurrency?: number | "unbounded" | undefined; readonly unordered?: boolean | undefined } + ): Stream<{ [K in keyof A | N]: K extends keyof A ? A[K] : B }, E | E2, R | R2> +} = groupBy_.bindEffect + +/** + * The "do simulation" in Effect allows you to write code in a more declarative style, similar to the "do notation" in other programming languages. It provides a way to define variables and perform operations on them using functions like `bind` and `let`. + * + * Here's how the do simulation works: + * + * 1. Start the do simulation using the `Do` value + * 2. Within the do simulation scope, you can use the `bind` function to define variables and bind them to `Stream` values + * 3. You can accumulate multiple `bind` statements to define multiple variables within the scope + * 4. Inside the do simulation scope, you can also use the `let` function to define variables and bind them to simple values + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { Chunk, Effect, pipe, Stream } from "effect" + * + * const result = pipe( + * Stream.Do, + * Stream.bind("x", () => Stream.succeed(2)), + * Stream.bind("y", () => Stream.succeed(3)), + * Stream.let("sum", ({ x, y }) => x + y) + * ) + * assert.deepStrictEqual(Effect.runSync(Stream.runCollect(result)), Chunk.of({ x: 2, y: 3, sum: 5 })) + * ``` + * + * @see {@link Do} + * @see {@link bind} + * @see {@link bindEffect} + * @see {@link let_ let} + * + * @category do notation + * @since 2.0.0 + */ +export const bindTo: { + (name: N): (self: Stream) => Stream<{ [K in N]: A }, E, R> + (self: Stream, name: N): Stream<{ [K in N]: A }, E, R> +} = internal.bindTo + +const let_: { + ( + name: Exclude, + f: (a: NoInfer) => B + ): (self: Stream) => Stream<{ [K in N | keyof A]: K extends keyof A ? A[K] : B }, E, R> + ( + self: Stream, + name: Exclude, + f: (a: NoInfer) => B + ): Stream<{ [K in N | keyof A]: K extends keyof A ? A[K] : B }, E, R> +} = internal.let_ + +export { + /** + * The "do simulation" in Effect allows you to write code in a more declarative style, similar to the "do notation" in other programming languages. It provides a way to define variables and perform operations on them using functions like `bind` and `let`. + * + * Here's how the do simulation works: + * + * 1. Start the do simulation using the `Do` value + * 2. Within the do simulation scope, you can use the `bind` function to define variables and bind them to `Stream` values + * 3. You can accumulate multiple `bind` statements to define multiple variables within the scope + * 4. Inside the do simulation scope, you can also use the `let` function to define variables and bind them to simple values + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { Chunk, Effect, pipe, Stream } from "effect" + * + * const result = pipe( + * Stream.Do, + * Stream.bind("x", () => Stream.succeed(2)), + * Stream.bind("y", () => Stream.succeed(3)), + * Stream.let("sum", ({ x, y }) => x + y) + * ) + * assert.deepStrictEqual(Effect.runSync(Stream.runCollect(result)), Chunk.of({ x: 2, y: 3, sum: 5 })) + * ``` + * + * @see {@link Do} + * @see {@link bindTo} + * @see {@link bind} + * @see {@link bindEffect} + * + * @category do notation + * @since 2.0.0 + */ + let_ as let +} + +// ------------------------------------------------------------------------------------- +// encoding +// ------------------------------------------------------------------------------------- + +/** + * Decode Uint8Array chunks into a stream of strings using the specified encoding. + * + * @since 2.0.0 + * @category encoding + */ +export const decodeText: { + (encoding?: string | undefined): (self: Stream) => Stream + (self: Stream, encoding?: string | undefined): Stream +} = internal.decodeText + +/** + * Encode a stream of strings into a stream of Uint8Array chunks using the specified encoding. + * + * @since 2.0.0 + * @category encoding + */ +export const encodeText: (self: Stream) => Stream = internal.encodeText + +/** + * @since 3.4.0 + * @category models + */ +export interface EventListener { + addEventListener( + event: string, + f: (event: A) => void, + options?: { + readonly capture?: boolean + readonly passive?: boolean + readonly once?: boolean + readonly signal?: AbortSignal + } | boolean + ): void + removeEventListener( + event: string, + f: (event: A) => void, + options?: { + readonly capture?: boolean + } | boolean + ): void +} + +/** + * Creates a `Stream` using addEventListener. + * @since 3.1.0 + */ +export const fromEventListener: ( + target: EventListener, + type: string, + options?: boolean | { + readonly capture?: boolean + readonly passive?: boolean + readonly once?: boolean + readonly bufferSize?: number | "unbounded" | undefined + } | undefined +) => Stream = internal.fromEventListener diff --git a/repos/effect/packages/effect/src/StreamEmit.ts b/repos/effect/packages/effect/src/StreamEmit.ts new file mode 100644 index 0000000..fdb935d --- /dev/null +++ b/repos/effect/packages/effect/src/StreamEmit.ts @@ -0,0 +1,136 @@ +/** + * @since 2.0.0 + */ +import type * as Cause from "./Cause.js" +import type * as Chunk from "./Chunk.js" +import type * as Effect from "./Effect.js" +import type * as Exit from "./Exit.js" +import type * as Option from "./Option.js" + +/** + * An `Emit` represents an asynchronous callback that can be + * called multiple times. The callback can be called with a value of type + * `Effect, Option, R>`, where succeeding with a `Chunk` + * indicates to emit those elements, failing with `Some` indicates to + * terminate with that error, and failing with `None` indicates to terminate + * with an end of stream signal. + * + * @since 2.0.0 + * @category models + */ +export interface Emit extends EmitOps { + (f: Effect.Effect, Option.Option, R>): Promise +} + +/** + * @since 2.0.0 + * @category models + */ +export interface EmitOps { + /** + * Emits a chunk containing the specified values. + */ + chunk(chunk: Chunk.Chunk): Promise + + /** + * Terminates with a cause that dies with the specified defect. + */ + die(defect: Err): Promise + + /** + * Terminates with a cause that dies with a `Throwable` with the specified + * message. + */ + dieMessage(message: string): Promise + + /** + * Either emits the specified value if this `Exit` is a `Success` or else + * terminates with the specified cause if this `Exit` is a `Failure`. + */ + done(exit: Exit.Exit): Promise + + /** + * Terminates with an end of stream signal. + */ + end(): Promise + + /** + * Terminates with the specified error. + */ + fail(error: E): Promise + + /** + * Either emits the success value of this effect or terminates the stream + * with the failure value of this effect. + */ + fromEffect(effect: Effect.Effect): Promise + + /** + * Either emits the success value of this effect or terminates the stream + * with the failure value of this effect. + */ + fromEffectChunk(effect: Effect.Effect, E, R>): Promise + + /** + * Terminates the stream with the specified cause. + */ + halt(cause: Cause.Cause): Promise + + /** + * Emits a chunk containing the specified value. + */ + single(value: A): Promise +} + +/** + * @since 3.6.0 + * @category models + */ +export interface EmitOpsPush { + /** + * Emits a chunk containing the specified values. + */ + chunk(chunk: Chunk.Chunk): boolean + + /** + * Emits a chunk containing the specified values. + */ + array(chunk: ReadonlyArray): boolean + + /** + * Terminates with a cause that dies with the specified defect. + */ + die(defect: Err): void + + /** + * Terminates with a cause that dies with a `Throwable` with the specified + * message. + */ + dieMessage(message: string): void + + /** + * Either emits the specified value if this `Exit` is a `Success` or else + * terminates with the specified cause if this `Exit` is a `Failure`. + */ + done(exit: Exit.Exit): void + + /** + * Terminates with an end of stream signal. + */ + end(): void + + /** + * Terminates with the specified error. + */ + fail(error: E): void + + /** + * Terminates the stream with the specified cause. + */ + halt(cause: Cause.Cause): void + + /** + * Emits a chunk containing the specified value. + */ + single(value: A): boolean +} diff --git a/repos/effect/packages/effect/src/StreamHaltStrategy.ts b/repos/effect/packages/effect/src/StreamHaltStrategy.ts new file mode 100644 index 0000000..5d7b055 --- /dev/null +++ b/repos/effect/packages/effect/src/StreamHaltStrategy.ts @@ -0,0 +1,123 @@ +/** + * @since 2.0.0 + */ +import * as internal from "./internal/stream/haltStrategy.js" + +/** + * @since 2.0.0 + * @category models + */ +export type HaltStrategy = Left | Right | Both | Either + +/** + * @since 2.0.0 + * @category models + */ +export type HaltStrategyInput = HaltStrategy | "left" | "right" | "both" | "either" + +/** + * @since 2.0.0 + * @category models + */ +export interface Left { + readonly _tag: "Left" +} + +/** + * @since 2.0.0 + * @category models + */ +export interface Right { + readonly _tag: "Right" +} + +/** + * @since 2.0.0 + * @category models + */ +export interface Both { + readonly _tag: "Both" +} + +/** + * @since 2.0.0 + * @category models + */ +export interface Either { + readonly _tag: "Either" +} + +/** + * @since 2.0.0 + * @category constructors + */ +export const Left: HaltStrategy = internal.Left + +/** + * @since 2.0.0 + * @category constructors + */ +export const Right: HaltStrategy = internal.Right + +/** + * @since 2.0.0 + * @category constructors + */ +export const Both: HaltStrategy = internal.Both + +/** + * @since 2.0.0 + * @category constructors + */ +export const Either: HaltStrategy = internal.Either + +/** + * @since 2.0.0 + * @category constructors + */ +export const fromInput: (input: HaltStrategyInput) => HaltStrategy = internal.fromInput + +/** + * @since 2.0.0 + * @category refinements + */ +export const isLeft: (self: HaltStrategy) => self is Left = internal.isLeft + +/** + * @since 2.0.0 + * @category refinements + */ +export const isRight: (self: HaltStrategy) => self is Right = internal.isRight + +/** + * @since 2.0.0 + * @category refinements + */ +export const isBoth: (self: HaltStrategy) => self is Both = internal.isBoth + +/** + * @since 2.0.0 + * @category refinements + */ +export const isEither: (self: HaltStrategy) => self is Either = internal.isEither + +/** + * Folds over the specified `HaltStrategy` using the provided case functions. + * + * @since 2.0.0 + * @category folding + */ +export const match: { + (options: { + readonly onLeft: () => Z + readonly onRight: () => Z + readonly onBoth: () => Z + readonly onEither: () => Z + }): (self: HaltStrategy) => Z + (self: HaltStrategy, options: { + readonly onLeft: () => Z + readonly onRight: () => Z + readonly onBoth: () => Z + readonly onEither: () => Z + }): Z +} = internal.match diff --git a/repos/effect/packages/effect/src/Streamable.ts b/repos/effect/packages/effect/src/Streamable.ts new file mode 100644 index 0000000..059952c --- /dev/null +++ b/repos/effect/packages/effect/src/Streamable.ts @@ -0,0 +1,45 @@ +/** + * @since 2.0.0 + */ + +import { pipeArguments } from "./Pipeable.js" +import * as Stream from "./Stream.js" + +const streamVariance = { + /* c8 ignore next */ + _R: (_: never) => _, + /* c8 ignore next */ + _E: (_: never) => _, + /* c8 ignore next */ + _A: (_: never) => _ +} + +/** + * @since 2.0.0 + * @category constructors + */ +export abstract class Class implements Stream.Stream { + /** + * @since 2.0.0 + */ + readonly [Stream.StreamTypeId] = streamVariance + + /** + * @since 2.0.0 + */ + pipe() { + return pipeArguments(this, arguments) + } + + /** + * @since 2.0.0 + */ + abstract toStream(): Stream.Stream + + /** + * @internal + */ + get channel() { + return Stream.toChannel(this.toStream()) + } +} diff --git a/repos/effect/packages/effect/src/String.ts b/repos/effect/packages/effect/src/String.ts new file mode 100644 index 0000000..5b2c1ef --- /dev/null +++ b/repos/effect/packages/effect/src/String.ts @@ -0,0 +1,778 @@ +/** + * This module provides utility functions and type class instances for working with the `string` type in TypeScript. + * It includes functions for basic string manipulation, as well as type class instances for + * `Equivalence` and `Order`. + * + * @since 2.0.0 + */ + +import type { NonEmptyArray } from "./Array.js" +import * as equivalence from "./Equivalence.js" +import { dual } from "./Function.js" +import * as readonlyArray from "./internal/array.js" +import * as number from "./Number.js" +import * as Option from "./Option.js" +import * as order from "./Order.js" +import type * as Ordering from "./Ordering.js" +import type { Refinement } from "./Predicate.js" +import * as predicate from "./Predicate.js" + +/** + * Tests if a value is a `string`. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { String } from "effect" + * + * assert.deepStrictEqual(String.isString("a"), true) + * assert.deepStrictEqual(String.isString(1), false) + * ``` + * + * @category guards + * @since 2.0.0 + */ +export const isString: Refinement = predicate.isString + +/** + * @category instances + * @since 2.0.0 + */ +export const Equivalence: equivalence.Equivalence = equivalence.string + +/** + * @category instances + * @since 2.0.0 + */ +export const Order: order.Order = order.string + +/** + * The empty string `""`. + * + * @since 2.0.0 + */ +export const empty: "" = "" as const + +/** + * Concatenates two strings at the type level. + * + * @since 2.0.0 + */ +export type Concat = `${A}${B}` + +/** + * Concatenates two strings at runtime. + * + * @since 2.0.0 + */ +export const concat: { + (that: B): (self: A) => Concat + (self: A, that: B): Concat +} = dual(2, (self: string, that: string): string => self + that) + +/** + * @example + * ```ts + * import * as assert from "node:assert" + * import { pipe, String } from "effect" + * + * assert.deepStrictEqual(pipe('a', String.toUpperCase), 'A') + * ``` + * + * @since 2.0.0 + */ +export const toUpperCase = (self: S): Uppercase => self.toUpperCase() as Uppercase + +/** + * @example + * ```ts + * import * as assert from "node:assert" + * import { pipe, String } from "effect" + * + * assert.deepStrictEqual(pipe('A', String.toLowerCase), 'a') + * ``` + * + * @since 2.0.0 + */ +export const toLowerCase = (self: T): Lowercase => self.toLowerCase() as Lowercase + +/** + * @example + * ```ts + * import * as assert from "node:assert" + * import { pipe, String } from "effect" + * + * assert.deepStrictEqual(pipe('abc', String.capitalize), 'Abc') + * ``` + * + * @since 2.0.0 + */ +export const capitalize = (self: T): Capitalize => { + if (self.length === 0) return self as Capitalize + + return (toUpperCase(self[0]) + self.slice(1)) as Capitalize +} + +/** + * @example + * ```ts + * import * as assert from "node:assert" + * import { pipe, String } from "effect" + * + * assert.deepStrictEqual(pipe('ABC', String.uncapitalize), 'aBC') + * ``` + * + * @since 2.0.0 + */ +export const uncapitalize = (self: T): Uncapitalize => { + if (self.length === 0) return self as Uncapitalize + + return (toLowerCase(self[0]) + self.slice(1)) as Uncapitalize +} + +/** + * @example + * ```ts + * import * as assert from "node:assert" + * import { pipe, String } from "effect" + * + * assert.deepStrictEqual(pipe('abc', String.replace('b', 'd')), 'adc') + * ``` + * + * @since 2.0.0 + */ +export const replace = (searchValue: string | RegExp, replaceValue: string) => (self: string): string => + self.replace(searchValue, replaceValue) + +/** + * @since 2.0.0 + */ +export type Trim = TrimEnd> + +/** + * @example + * ```ts + * import * as assert from "node:assert" + * import { String } from "effect" + * + * assert.deepStrictEqual(String.trim(' a '), 'a') + * ``` + * + * @since 2.0.0 + */ +export const trim = (self: A): Trim => self.trim() as Trim + +/** + * @since 2.0.0 + */ +export type TrimStart = A extends `${" " | "\n" | "\t" | "\r"}${infer B}` ? TrimStart : A + +/** + * @example + * ```ts + * import * as assert from "node:assert" + * import { String } from "effect" + * + * assert.deepStrictEqual(String.trimStart(' a '), 'a ') + * ``` + * + * @since 2.0.0 + */ +export const trimStart = (self: A): TrimStart => self.trimStart() as TrimStart + +/** + * @since 2.0.0 + */ +export type TrimEnd = A extends `${infer B}${" " | "\n" | "\t" | "\r"}` ? TrimEnd : A + +/** + * @example + * ```ts + * import * as assert from "node:assert" + * import { String } from "effect" + * + * assert.deepStrictEqual(String.trimEnd(' a '), ' a') + * ``` + * + * @since 2.0.0 + */ +export const trimEnd = (self: A): TrimEnd => self.trimEnd() as TrimEnd + +/** + * @example + * ```ts + * import * as assert from "node:assert" + * import { pipe, String } from "effect" + * + * assert.deepStrictEqual(pipe('abcd', String.slice(1, 3)), 'bc') + * ``` + * + * @since 2.0.0 + */ +export const slice = (start?: number, end?: number) => (self: string): string => self.slice(start, end) + +/** + * Test whether a `string` is empty. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { String } from "effect" + * + * assert.deepStrictEqual(String.isEmpty(''), true) + * assert.deepStrictEqual(String.isEmpty('a'), false) + * ``` + * + * @since 2.0.0 + */ +export const isEmpty = (self: string): self is "" => self.length === 0 + +/** + * Test whether a `string` is non empty. + * + * @since 2.0.0 + */ +export const isNonEmpty = (self: string): boolean => self.length > 0 + +/** + * Calculate the number of characters in a `string`. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { String } from "effect" + * + * assert.deepStrictEqual(String.length('abc'), 3) + * ``` + * + * @since 2.0.0 + */ +export const length = (self: string): number => self.length + +/** + * @example + * ```ts + * import * as assert from "node:assert" + * import { pipe, String } from "effect" + * + * assert.deepStrictEqual(pipe('abc', String.split('')), ['a', 'b', 'c']) + * assert.deepStrictEqual(pipe('', String.split('')), ['']) + * ``` + * + * @since 2.0.0 + */ +export const split: { + (separator: string | RegExp): (self: string) => NonEmptyArray + (self: string, separator: string | RegExp): NonEmptyArray +} = dual(2, (self: string, separator: string | RegExp): NonEmptyArray => { + const out = self.split(separator) + return readonlyArray.isNonEmptyArray(out) ? out : [self] +}) + +/** + * Returns `true` if `searchString` appears as a substring of `self`, at one or more positions that are + * greater than or equal to `position`; otherwise, returns `false`. + * + * @since 2.0.0 + */ +export const includes = (searchString: string, position?: number) => (self: string): boolean => + self.includes(searchString, position) + +/** + * @since 2.0.0 + */ +export const startsWith = (searchString: string, position?: number) => (self: string): boolean => + self.startsWith(searchString, position) + +/** + * @since 2.0.0 + */ +export const endsWith = (searchString: string, position?: number) => (self: string): boolean => + self.endsWith(searchString, position) + +/** + * @example + * ```ts + * import * as assert from "node:assert" + * import { pipe, String, Option } from "effect" + * + * assert.deepStrictEqual(pipe("abc", String.charCodeAt(1)), Option.some(98)) + * assert.deepStrictEqual(pipe("abc", String.charCodeAt(4)), Option.none()) + * ``` + * + * @since 2.0.0 + */ +export const charCodeAt: { + (index: number): (self: string) => Option.Option + (self: string, index: number): Option.Option +} = dual( + 2, + (self: string, index: number): Option.Option => + Option.filter(Option.some(self.charCodeAt(index)), (charCode) => !isNaN(charCode)) +) + +/** + * @example + * ```ts + * import * as assert from "node:assert" + * import { pipe, String, Option } from "effect" + * + * assert.deepStrictEqual(pipe("abcd", String.substring(1)), "bcd") + * assert.deepStrictEqual(pipe("abcd", String.substring(1, 3)), "bc") + * ``` + * + * @since 2.0.0 + */ +export const substring = (start: number, end?: number) => (self: string): string => self.substring(start, end) + +/** + * @example + * ```ts + * import * as assert from "node:assert" + * import { pipe, String, Option } from "effect" + * + * assert.deepStrictEqual(pipe("abc", String.at(1)), Option.some("b")) + * assert.deepStrictEqual(pipe("abc", String.at(4)), Option.none()) + * ``` + * + * @since 2.0.0 + */ +export const at: { + (index: number): (self: string) => Option.Option + (self: string, index: number): Option.Option +} = dual(2, (self: string, index: number): Option.Option => Option.fromNullable(self.at(index))) + +/** + * @example + * ```ts + * import * as assert from "node:assert" + * import { pipe, String, Option } from "effect" + * + * assert.deepStrictEqual(pipe("abc", String.charAt(1)), Option.some("b")) + * assert.deepStrictEqual(pipe("abc", String.charAt(4)), Option.none()) + * ``` + * + * @since 2.0.0 + */ +export const charAt: { + (index: number): (self: string) => Option.Option + (self: string, index: number): Option.Option +} = dual( + 2, + (self: string, index: number): Option.Option => Option.filter(Option.some(self.charAt(index)), isNonEmpty) +) + +/** + * @example + * ```ts + * import * as assert from "node:assert" + * import { pipe, String, Option } from "effect" + * + * assert.deepStrictEqual(pipe("abc", String.codePointAt(1)), Option.some(98)) + * ``` + * + * @since 2.0.0 + */ +export const codePointAt: { + (index: number): (self: string) => Option.Option + (self: string, index: number): Option.Option +} = dual(2, (self: string, index: number): Option.Option => Option.fromNullable(self.codePointAt(index))) + +/** + * @example + * ```ts + * import * as assert from "node:assert" + * import { pipe, String, Option } from "effect" + * + * assert.deepStrictEqual(pipe("abbbc", String.indexOf("b")), Option.some(1)) + * ``` + * + * @since 2.0.0 + */ +export const indexOf = (searchString: string) => (self: string): Option.Option => + Option.filter(Option.some(self.indexOf(searchString)), number.greaterThanOrEqualTo(0)) + +/** + * @example + * ```ts + * import * as assert from "node:assert" + * import { pipe, String, Option } from "effect" + * + * assert.deepStrictEqual(pipe("abbbc", String.lastIndexOf("b")), Option.some(3)) + * assert.deepStrictEqual(pipe("abbbc", String.lastIndexOf("d")), Option.none()) + * ``` + * + * @since 2.0.0 + */ +export const lastIndexOf = (searchString: string) => (self: string): Option.Option => + Option.filter(Option.some(self.lastIndexOf(searchString)), number.greaterThanOrEqualTo(0)) + +/** + * @example + * ```ts + * import * as assert from "node:assert" + * import { pipe, String } from "effect" + * + * assert.deepStrictEqual(pipe("a", String.localeCompare("b")), -1) + * assert.deepStrictEqual(pipe("b", String.localeCompare("a")), 1) + * assert.deepStrictEqual(pipe("a", String.localeCompare("a")), 0) + * ``` + * + * @since 2.0.0 + */ +export const localeCompare = + (that: string, locales?: Intl.LocalesArgument, options?: Intl.CollatorOptions) => (self: string): Ordering.Ordering => + number.sign(self.localeCompare(that, locales, options)) + +/** + * It is the `pipe`-able version of the native `match` method. + * + * @since 2.0.0 + */ +export const match = (regexp: RegExp | string) => (self: string): Option.Option => + Option.fromNullable(self.match(regexp)) + +/** + * It is the `pipe`-able version of the native `matchAll` method. + * + * @since 2.0.0 + */ +export const matchAll = (regexp: RegExp) => (self: string): IterableIterator => self.matchAll(regexp) + +/** + * @example + * ```ts + * import * as assert from "node:assert" + * import { pipe, String } from "effect" + * + * const str = "\u1E9B\u0323"; + * assert.deepStrictEqual(pipe(str, String.normalize()), "\u1E9B\u0323") + * assert.deepStrictEqual(pipe(str, String.normalize("NFC")), "\u1E9B\u0323") + * assert.deepStrictEqual(pipe(str, String.normalize("NFD")), "\u017F\u0323\u0307") + * assert.deepStrictEqual(pipe(str, String.normalize("NFKC")), "\u1E69") + * assert.deepStrictEqual(pipe(str, String.normalize("NFKD")), "\u0073\u0323\u0307") + * ``` + * + * @since 2.0.0 + */ +export const normalize = (form?: "NFC" | "NFD" | "NFKC" | "NFKD") => (self: string): string => self.normalize(form) + +/** + * @example + * ```ts + * import * as assert from "node:assert" + * import { pipe, String } from "effect" + * + * assert.deepStrictEqual(pipe("a", String.padEnd(5)), "a ") + * assert.deepStrictEqual(pipe("a", String.padEnd(5, "_")), "a____") + * ``` + * + * @since 2.0.0 + */ +export const padEnd = (maxLength: number, fillString?: string) => (self: string): string => + self.padEnd(maxLength, fillString) + +/** + * @example + * ```ts + * import * as assert from "node:assert" + * import { pipe, String } from "effect" + * + * assert.deepStrictEqual(pipe("a", String.padStart(5)), " a") + * assert.deepStrictEqual(pipe("a", String.padStart(5, "_")), "____a") + * ``` + * + * @since 2.0.0 + */ +export const padStart = (maxLength: number, fillString?: string) => (self: string): string => + self.padStart(maxLength, fillString) + +/** + * @example + * ```ts + * import * as assert from "node:assert" + * import { pipe, String } from "effect" + * + * assert.deepStrictEqual(pipe("a", String.repeat(5)), "aaaaa") + * ``` + * + * @since 2.0.0 + */ +export const repeat = (count: number) => (self: string): string => self.repeat(count) + +/** + * @example + * ```ts + * import * as assert from "node:assert" + * import { pipe, String } from "effect" + * + * assert.deepStrictEqual(pipe("ababb", String.replaceAll("b", "c")), "acacc") + * assert.deepStrictEqual(pipe("ababb", String.replaceAll(/ba/g, "cc")), "accbb") + * ``` + * + * @since 2.0.0 + */ +export const replaceAll = (searchValue: string | RegExp, replaceValue: string) => (self: string): string => + self.replaceAll(searchValue, replaceValue) + +/** + * @example + * ```ts + * import * as assert from "node:assert" + * import { pipe, String, Option } from "effect" + * + * assert.deepStrictEqual(pipe("ababb", String.search("b")), Option.some(1)) + * assert.deepStrictEqual(pipe("ababb", String.search(/abb/)), Option.some(2)) + * assert.deepStrictEqual(pipe("ababb", String.search("d")), Option.none()) + * ``` + * + * @since 2.0.0 + */ +export const search: { + (regexp: RegExp | string): (self: string) => Option.Option + (self: string, regexp: RegExp | string): Option.Option +} = dual( + 2, + (self: string, regexp: RegExp | string): Option.Option => + Option.filter(Option.some(self.search(regexp)), number.greaterThanOrEqualTo(0)) +) + +/** + * @example + * ```ts + * import * as assert from "node:assert" + * import { pipe, String } from "effect" + * + * const str = "\u0130" + * assert.deepStrictEqual(pipe(str, String.toLocaleLowerCase("tr")), "i") + * ``` + * + * @since 2.0.0 + */ +export const toLocaleLowerCase = (locale?: Intl.LocalesArgument) => (self: string): string => + self.toLocaleLowerCase(locale) + +/** + * @example + * ```ts + * import * as assert from "node:assert" + * import { pipe, String } from "effect" + * + * const str = "i\u0307" + * assert.deepStrictEqual(pipe(str, String.toLocaleUpperCase("lt-LT")), "I") + * ``` + * + * @since 2.0.0 + */ +export const toLocaleUpperCase = (locale?: Intl.LocalesArgument) => (self: string): string => + self.toLocaleUpperCase(locale) + +/** + * Keep the specified number of characters from the start of a string. + * + * If `n` is larger than the available number of characters, the string will + * be returned whole. + * + * If `n` is not a positive number, an empty string will be returned. + * + * If `n` is a float, it will be rounded down to the nearest integer. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { String } from "effect" + * + * assert.deepStrictEqual(String.takeLeft("Hello World", 5), "Hello") + * ``` + * + * @since 2.0.0 + */ +export const takeLeft: { + (n: number): (self: string) => string + (self: string, n: number): string +} = dual(2, (self: string, n: number): string => self.slice(0, Math.max(n, 0))) + +/** + * Keep the specified number of characters from the end of a string. + * + * If `n` is larger than the available number of characters, the string will + * be returned whole. + * + * If `n` is not a positive number, an empty string will be returned. + * + * If `n` is a float, it will be rounded down to the nearest integer. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { String } from "effect" + * + * assert.deepStrictEqual(String.takeRight("Hello World", 5), "World") + * ``` + * + * @since 2.0.0 + */ +export const takeRight: { + (n: number): (self: string) => string + (self: string, n: number): string +} = dual( + 2, + (self: string, n: number): string => self.slice(Math.max(0, self.length - Math.floor(n)), Infinity) +) + +const CR = 0x0d +const LF = 0x0a + +/** + * Returns an `IterableIterator` which yields each line contained within the + * string, trimming off the trailing newline character. + * + * @since 2.0.0 + */ +export const linesIterator = (self: string): LinesIterator => linesSeparated(self, true) + +/** + * Returns an `IterableIterator` which yields each line contained within the + * string as well as the trailing newline character. + * + * @since 2.0.0 + */ +export const linesWithSeparators = (s: string): LinesIterator => linesSeparated(s, false) + +/** + * For every line in this string, strip a leading prefix consisting of blanks + * or control characters followed by the character specified by `marginChar` + * from the line. + * + * @since 2.0.0 + */ +export const stripMarginWith: { + (marginChar: string): (self: string) => string + (self: string, marginChar: string): string +} = dual(2, (self: string, marginChar: string): string => { + let out = "" + + for (const line of linesWithSeparators(self)) { + let index = 0 + + while (index < line.length && line.charAt(index) <= " ") { + index = index + 1 + } + + const stripped = index < line.length && line.charAt(index) === marginChar + ? line.substring(index + 1) + : line + + out = out + stripped + } + + return out +}) + +/** + * For every line in this string, strip a leading prefix consisting of blanks + * or control characters followed by the `"|"` character from the line. + * + * @since 2.0.0 + */ +export const stripMargin = (self: string): string => stripMarginWith(self, "|") + +/** + * @since 2.0.0 + */ +export const snakeToCamel = (self: string): string => { + let str = self[0] + for (let i = 1; i < self.length; i++) { + str += self[i] === "_" ? self[++i].toUpperCase() : self[i] + } + return str +} + +/** + * @since 2.0.0 + */ +export const snakeToPascal = (self: string): string => { + let str = self[0].toUpperCase() + for (let i = 1; i < self.length; i++) { + str += self[i] === "_" ? self[++i].toUpperCase() : self[i] + } + return str +} + +/** + * @since 2.0.0 + */ +export const snakeToKebab = (self: string): string => self.replace(/_/g, "-") + +/** + * @since 2.0.0 + */ +export const camelToSnake = (self: string): string => self.replace(/([A-Z])/g, "_$1").toLowerCase() + +/** + * @since 2.0.0 + */ +export const pascalToSnake = (self: string): string => + (self.slice(0, 1) + self.slice(1).replace(/([A-Z])/g, "_$1")).toLowerCase() + +/** + * @since 2.0.0 + */ +export const kebabToSnake = (self: string): string => self.replace(/-/g, "_") + +class LinesIterator implements IterableIterator { + private index: number + private readonly length: number + + constructor(readonly s: string, readonly stripped: boolean = false) { + this.index = 0 + this.length = s.length + } + + next(): IteratorResult { + if (this.done) { + return { done: true, value: undefined } + } + const start = this.index + while (!this.done && !isLineBreak(this.s[this.index]!)) { + this.index = this.index + 1 + } + let end = this.index + if (!this.done) { + const char = this.s[this.index]! + this.index = this.index + 1 + if (!this.done && isLineBreak2(char, this.s[this.index]!)) { + this.index = this.index + 1 + } + if (!this.stripped) { + end = this.index + } + } + return { done: false, value: this.s.substring(start, end) } + } + + [Symbol.iterator](): IterableIterator { + return new LinesIterator(this.s, this.stripped) + } + + private get done(): boolean { + return this.index >= this.length + } +} + +/** + * Test if the provided character is a line break character (i.e. either `"\r"` + * or `"\n"`). + */ +const isLineBreak = (char: string): boolean => { + const code = char.charCodeAt(0) + return code === CR || code === LF +} + +/** + * Test if the provided characters combine to form a carriage return/line-feed + * (i.e. `"\r\n"`). + */ +const isLineBreak2 = (char0: string, char1: string): boolean => char0.charCodeAt(0) === CR && char1.charCodeAt(0) === LF + +const linesSeparated = (self: string, stripped: boolean): LinesIterator => new LinesIterator(self, stripped) diff --git a/repos/effect/packages/effect/src/Struct.ts b/repos/effect/packages/effect/src/Struct.ts new file mode 100644 index 0000000..a85ca30 --- /dev/null +++ b/repos/effect/packages/effect/src/Struct.ts @@ -0,0 +1,243 @@ +/** + * This module provides utility functions for working with structs in TypeScript. + * + * @since 2.0.0 + */ + +import * as Equivalence from "./Equivalence.js" +import { dual } from "./Function.js" +import * as order from "./Order.js" +import * as Predicate from "./Predicate.js" +import type { MatchRecord, Simplify } from "./Types.js" + +/** + * Create a new object by picking properties of an existing object. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { pipe, Struct } from "effect" + * + * assert.deepStrictEqual(pipe({ a: "a", b: 1, c: true }, Struct.pick("a", "b")), { a: "a", b: 1 }) + * assert.deepStrictEqual(Struct.pick({ a: "a", b: 1, c: true }, "a", "b"), { a: "a", b: 1 }) + * ``` + * + * @since 2.0.0 + */ +export const pick: { + >( + ...keys: Keys + ): ( + s: S + ) => MatchRecord>> + >( + s: S, + ...keys: Keys + ): MatchRecord>> +} = dual( + (args) => Predicate.isObject(args[0]), + >(s: S, ...keys: Keys) => { + const out: any = {} + for (const k of keys) { + if (k in s) { + out[k] = (s as any)[k] + } + } + return out + } +) + +/** + * Create a new object by omitting properties of an existing object. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { pipe, Struct } from "effect" + * + * assert.deepStrictEqual(pipe({ a: "a", b: 1, c: true }, Struct.omit("c")), { a: "a", b: 1 }) + * assert.deepStrictEqual(Struct.omit({ a: "a", b: 1, c: true }, "c"), { a: "a", b: 1 }) + * ``` + * + * @since 2.0.0 + */ +export const omit: { + >( + ...keys: Keys + ): (s: S) => Simplify> + >( + s: S, + ...keys: Keys + ): Simplify> +} = dual( + (args) => Predicate.isObject(args[0]), + >(s: S, ...keys: Keys) => { + const out: any = { ...s } + for (const k of keys) { + delete out[k] + } + return out + } +) + +/** + * Given a struct of `Equivalence`s returns a new `Equivalence` that compares values of a struct + * by applying each `Equivalence` to the corresponding property of the struct. + * + * Alias of {@link Equivalence.struct}. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { Struct, String, Number } from "effect" + * + * const PersonEquivalence = Struct.getEquivalence({ + * name: String.Equivalence, + * age: Number.Equivalence + * }) + * + * assert.deepStrictEqual( + * PersonEquivalence({ name: "John", age: 25 }, { name: "John", age: 25 }), + * true + * ) + * assert.deepStrictEqual( + * PersonEquivalence({ name: "John", age: 25 }, { name: "John", age: 40 }), + * false + * ) + * ``` + * + * @category combinators + * @since 2.0.0 + */ +export const getEquivalence: >>( + isEquivalents: R +) => Equivalence.Equivalence< + { readonly [K in keyof R]: [R[K]] extends [Equivalence.Equivalence] ? A : never } +> = Equivalence.struct + +/** + * This function creates and returns a new `Order` for a struct of values based on the given `Order`s + * for each property in the struct. + * + * Alias of {@link order.struct}. + * + * @category combinators + * @since 2.0.0 + */ +export const getOrder: }>( + fields: R +) => order.Order<{ [K in keyof R]: [R[K]] extends [order.Order] ? A : never }> = order.struct + +type Transformed = + & unknown + & { + [K in keyof O]: K extends keyof T ? (T[K] extends (...a: any) => any ? ReturnType : O[K]) : O[K] + } +type PartialTransform = { + [K in keyof T]: T[K] extends (a: O[K & keyof O]) => any ? T[K] : (a: O[K & keyof O]) => unknown +} +/** + * Transforms the values of a Struct provided a transformation function for each key. + * If no transformation function is provided for a key, it will return the original value for that key. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { pipe, Struct } from "effect" + * + * assert.deepStrictEqual( + * pipe( + * { a: 'a', b: 1, c: 3 }, + * Struct.evolve({ + * a: (a) => a.length, + * b: (b) => b * 2 + * }) + * ), + * { a: 1, b: 2, c: 3 } + * ) + * ``` + * + * @since 2.0.0 + */ +export const evolve: { + (t: PartialTransform): (obj: O) => Transformed + (obj: O, t: PartialTransform): Transformed +} = dual( + 2, + (obj: O, t: PartialTransform): Transformed => { + const out = { ...obj } + for (const k in t) { + if (Object.prototype.hasOwnProperty.call(obj, k)) { + // @ts-expect-error + out[k] = t[k](obj[k]) + } + } + return out as any + } +) + +/** + * Retrieves the value associated with the specified key from a struct. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { pipe, Struct } from "effect" + * + * const value = pipe({ a: 1, b: 2 }, Struct.get("a")) + * + * assert.deepStrictEqual(value, 1) + * ``` + * + * @since 2.0.0 + */ +export const get = + (key: K) => (s: S): MatchRecord => + s[key] + +/** + * Retrieves the object keys that are strings in a typed manner + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { Struct } from "effect" + * + * const symbol: unique symbol = Symbol() + * + * const value = { + * a: 1, + * b: 2, + * [symbol]: 3 + * } + * + * const keys: Array<"a" | "b"> = Struct.keys(value) + * + * assert.deepStrictEqual(keys, ["a", "b"]) + * ``` + * + * @since 3.6.0 + */ +export const keys = (o: T): Array<(keyof T) & string> => Object.keys(o) as Array<(keyof T) & string> + +/** + * Retrieves the entries (key-value pairs) of an object, where keys are strings, + * in a type-safe manner. Symbol keys are excluded from the result. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { Struct } from "effect" + * + * const c = Symbol("c") + * const value = { a: "foo", b: 1, [c]: true } + * + * const entries: Array<["a" | "b", string | number]> = Struct.entries(value) + * + * assert.deepStrictEqual(entries, [["a", "foo"], ["b", 1]]) + * ``` + * + * @since 3.17.0 + */ +export const entries = (obj: R): Array<[keyof R & string, R[keyof R & string]]> => + Object.entries(obj as any) as any diff --git a/repos/effect/packages/effect/src/Subscribable.ts b/repos/effect/packages/effect/src/Subscribable.ts new file mode 100644 index 0000000..dab27ea --- /dev/null +++ b/repos/effect/packages/effect/src/Subscribable.ts @@ -0,0 +1,100 @@ +/** + * @since 2.0.0 + */ +import * as Effect from "./Effect.js" +import { dual } from "./Function.js" +import { pipeArguments } from "./Pipeable.js" +import { hasProperty } from "./Predicate.js" +import * as Readable from "./Readable.js" +import * as Stream from "./Stream.js" +import type { NoInfer } from "./Types.js" + +/** + * @since 2.0.0 + * @category type ids + */ +export const TypeId: unique symbol = Symbol.for("effect/Subscribable") + +/** + * @since 2.0.0 + * @category type ids + */ +export type TypeId = typeof TypeId + +/** + * @since 2.0.0 + * @category models + */ +export interface Subscribable extends Readable.Readable { + readonly [TypeId]: TypeId + readonly changes: Stream.Stream +} + +/** + * @since 2.0.0 + * @category refinements + */ +export const isSubscribable = (u: unknown): u is Subscribable => hasProperty(u, TypeId) + +const Proto: Omit, "get" | "changes"> = { + [Readable.TypeId]: Readable.TypeId, + [TypeId]: TypeId, + pipe() { + return pipeArguments(this, arguments) + } +} + +/** + * @since 2.0.0 + * @category constructors + */ +export const make = (options: { + readonly get: Effect.Effect + readonly changes: Stream.Stream +}): Subscribable => Object.assign(Object.create(Proto), options) + +/** + * @since 2.0.0 + * @category combinators + */ +export const map: { + (f: (a: NoInfer) => B): (fa: Subscribable) => Subscribable + (self: Subscribable, f: (a: NoInfer) => B): Subscribable +} = dual(2, (self: Subscribable, f: (a: NoInfer) => B): Subscribable => + make({ + get: Effect.map(self.get, f), + changes: Stream.map(self.changes, f) + })) + +/** + * @since 2.0.0 + * @category combinators + */ +export const mapEffect: { + ( + f: (a: NoInfer) => Effect.Effect + ): (fa: Subscribable) => Subscribable + ( + self: Subscribable, + f: (a: NoInfer) => Effect.Effect + ): Subscribable +} = dual(2, ( + self: Subscribable, + f: (a: NoInfer) => Effect.Effect +): Subscribable => + make({ + get: Effect.flatMap(self.get, f), + changes: Stream.mapEffect(self.changes, f) + })) + +/** + * @since 2.0.0 + * @category constructors + */ +export const unwrap = ( + effect: Effect.Effect, E1, R1> +): Subscribable => + make({ + get: Effect.flatMap(effect, (s) => s.get), + changes: Stream.unwrap(Effect.map(effect, (s) => s.changes)) + }) diff --git a/repos/effect/packages/effect/src/SubscriptionRef.ts b/repos/effect/packages/effect/src/SubscriptionRef.ts new file mode 100644 index 0000000..aac1045 --- /dev/null +++ b/repos/effect/packages/effect/src/SubscriptionRef.ts @@ -0,0 +1,298 @@ +/** + * @since 2.0.0 + */ +import type * as Effect from "./Effect.js" +import * as internal from "./internal/subscriptionRef.js" +import type * as Option from "./Option.js" +import type * as PubSub from "./PubSub.js" +import * as Ref from "./Ref.js" +import type * as Stream from "./Stream.js" +import type { Subscribable } from "./Subscribable.js" +import * as Synchronized from "./SynchronizedRef.js" +import type * as Types from "./Types.js" +import type * as Unify from "./Unify.js" + +/** + * @since 2.0.0 + * @category symbols + */ +export const SubscriptionRefTypeId: unique symbol = internal.SubscriptionRefTypeId + +/** + * @since 2.0.0 + * @category symbols + */ +export type SubscriptionRefTypeId = typeof SubscriptionRefTypeId + +/** + * A `SubscriptionRef` is a `Ref` that can be subscribed to in order to + * receive the current value as well as all changes to the value. + * + * @since 2.0.0 + * @category models + */ +export interface SubscriptionRef + extends SubscriptionRef.Variance, Synchronized.SynchronizedRef, Subscribable +{ + /** @internal */ + readonly ref: Ref.Ref + /** @internal */ + readonly pubsub: PubSub.PubSub + /** @internal */ + readonly semaphore: Effect.Semaphore + /** + * A stream containing the current value of the `Ref` as well as all changes + * to that value. + */ + readonly changes: Stream.Stream + readonly [Unify.typeSymbol]?: unknown + readonly [Unify.unifySymbol]?: SubscriptionRefUnify + readonly [Unify.ignoreSymbol]?: SubscriptionRefUnifyIgnore +} + +/** + * @category models + * @since 3.8.0 + */ +export interface SubscriptionRefUnify + extends Synchronized.SynchronizedRefUnify +{ + SubscriptionRef?: () => Extract> +} + +/** + * @category models + * @since 3.8.0 + */ +export interface SubscriptionRefUnifyIgnore extends Synchronized.SynchronizedRefUnifyIgnore { + SynchronizedRef?: true +} + +/** + * @since 2.0.0 + */ +export declare namespace SubscriptionRef { + /** + * @since 2.0.0 + * @category models + */ + export interface Variance { + readonly [SubscriptionRefTypeId]: { + readonly _A: Types.Invariant + } + } +} + +/** + * @since 2.0.0 + * @category getters + */ +export const get: (self: SubscriptionRef) => Effect.Effect = internal.get + +/** + * @since 2.0.0 + * @category utils + */ +export const getAndSet: { + (value: A): (self: SubscriptionRef) => Effect.Effect + (self: SubscriptionRef, value: A): Effect.Effect +} = Ref.getAndSet + +/** + * @since 2.0.0 + * @category utils + */ +export const getAndUpdate: { + (f: (a: A) => A): (self: SubscriptionRef) => Effect.Effect + (self: SubscriptionRef, f: (a: A) => A): Effect.Effect +} = Ref.getAndUpdate + +/** + * @since 2.0.0 + * @category utils + */ +export const getAndUpdateEffect: { + (f: (a: A) => Effect.Effect): (self: SubscriptionRef) => Effect.Effect + (self: SubscriptionRef, f: (a: A) => Effect.Effect): Effect.Effect +} = Synchronized.getAndUpdateEffect + +/** + * @since 2.0.0 + * @category utils + */ +export const getAndUpdateSome: { + (pf: (a: A) => Option.Option): (self: SubscriptionRef) => Effect.Effect + (self: SubscriptionRef, pf: (a: A) => Option.Option): Effect.Effect +} = Ref.getAndUpdateSome + +/** + * @since 2.0.0 + * @category utils + */ +export const getAndUpdateSomeEffect: { + ( + pf: (a: A) => Option.Option> + ): (self: SubscriptionRef) => Effect.Effect + ( + self: SubscriptionRef, + pf: (a: A) => Option.Option> + ): Effect.Effect +} = Synchronized.getAndUpdateSomeEffect + +/** + * Creates a new `SubscriptionRef` with the specified value. + * + * @since 2.0.0 + * @category constructors + */ +export const make: (value: A) => Effect.Effect> = internal.make + +/** + * @since 2.0.0 + * @category utils + */ +export const modify: { + (f: (a: A) => readonly [B, A]): (self: SubscriptionRef) => Effect.Effect + (self: SubscriptionRef, f: (a: A) => readonly [B, A]): Effect.Effect +} = internal.modify + +/** + * @since 2.0.0 + * @category utils + */ +export const modifyEffect: { + (f: (a: A) => Effect.Effect): (self: SubscriptionRef) => Effect.Effect + (self: SubscriptionRef, f: (a: A) => Effect.Effect): Effect.Effect +} = internal.modifyEffect + +/** + * @since 2.0.0 + * @category utils + */ +export const modifySome: { + ( + fallback: B, + pf: (a: A) => Option.Option + ): (self: SubscriptionRef) => Effect.Effect + ( + self: SubscriptionRef, + fallback: B, + pf: (a: A) => Option.Option + ): Effect.Effect +} = Ref.modifySome + +/** + * @since 2.0.0 + * @category utils + */ +export const modifySomeEffect: { + ( + fallback: B, + pf: (a: A) => Option.Option> + ): (self: Synchronized.SynchronizedRef) => Effect.Effect + ( + self: Synchronized.SynchronizedRef, + fallback: B, + pf: (a: A) => Option.Option> + ): Effect.Effect +} = Synchronized.modifySomeEffect + +/** + * @since 2.0.0 + * @category utils + */ +export const set: { + (value: A): (self: SubscriptionRef) => Effect.Effect + (self: SubscriptionRef, value: A): Effect.Effect +} = internal.set + +/** + * @since 2.0.0 + * @category utils + */ +export const setAndGet: { + (value: A): (self: SubscriptionRef) => Effect.Effect + (self: SubscriptionRef, value: A): Effect.Effect +} = Ref.setAndGet + +/** + * @since 2.0.0 + * @category utils + */ +export const update: { + (f: (a: A) => A): (self: SubscriptionRef) => Effect.Effect + (self: SubscriptionRef, f: (a: A) => A): Effect.Effect +} = Ref.update + +/** + * @since 2.0.0 + * @category utils + */ +export const updateEffect: { + (f: (a: A) => Effect.Effect): (self: Synchronized.SynchronizedRef) => Effect.Effect + (self: Synchronized.SynchronizedRef, f: (a: A) => Effect.Effect): Effect.Effect +} = Synchronized.updateEffect + +/** + * @since 2.0.0 + * @category utils + */ +export const updateAndGet: { + (f: (a: A) => A): (self: SubscriptionRef) => Effect.Effect + (self: SubscriptionRef, f: (a: A) => A): Effect.Effect +} = Ref.updateAndGet + +/** + * @since 2.0.0 + * @category utils + */ +export const updateAndGetEffect: { + (f: (a: A) => Effect.Effect): (self: SubscriptionRef) => Effect.Effect + (self: SubscriptionRef, f: (a: A) => Effect.Effect): Effect.Effect +} = Synchronized.updateAndGetEffect + +/** + * @since 2.0.0 + * @category utils + */ +export const updateSome: { + (f: (a: A) => Option.Option): (self: SubscriptionRef) => Effect.Effect + (self: SubscriptionRef, f: (a: A) => Option.Option): Effect.Effect +} = Ref.updateSome + +/** + * @since 2.0.0 + * @category utils + */ +export const updateSomeEffect: { + ( + pf: (a: A) => Option.Option> + ): (self: Synchronized.SynchronizedRef) => Effect.Effect + ( + self: Synchronized.SynchronizedRef, + pf: (a: A) => Option.Option> + ): Effect.Effect +} = Synchronized.updateSomeEffect + +/** + * @since 2.0.0 + * @category utils + */ +export const updateSomeAndGet: { + (pf: (a: A) => Option.Option): (self: SubscriptionRef) => Effect.Effect + (self: SubscriptionRef, pf: (a: A) => Option.Option): Effect.Effect +} = Ref.updateSomeAndGet + +/** + * @since 2.0.0 + * @category utils + */ +export const updateSomeAndGetEffect: { + ( + pf: (a: A) => Option.Option> + ): (self: SubscriptionRef) => Effect.Effect + ( + self: SubscriptionRef, + pf: (a: A) => Option.Option> + ): Effect.Effect +} = Synchronized.updateSomeAndGetEffect diff --git a/repos/effect/packages/effect/src/Supervisor.ts b/repos/effect/packages/effect/src/Supervisor.ts new file mode 100644 index 0000000..b323dca --- /dev/null +++ b/repos/effect/packages/effect/src/Supervisor.ts @@ -0,0 +1,240 @@ +/** + * A `Supervisor` is allowed to supervise the launching and termination of + * fibers, producing some visible value of type `T` from the supervision. + * + * @since 2.0.0 + */ +import type * as Context from "./Context.js" +import type * as Effect from "./Effect.js" +import type * as Exit from "./Exit.js" +import type * as Fiber from "./Fiber.js" +import * as core from "./internal/core.js" +import * as circular from "./internal/layer/circular.js" +import * as internal from "./internal/supervisor.js" +import type * as Layer from "./Layer.js" +import type * as MutableRef from "./MutableRef.js" +import type * as Option from "./Option.js" +import type * as SortedSet from "./SortedSet.js" +import type * as Types from "./Types.js" + +/** + * @since 2.0.0 + * @category symbols + */ +export const SupervisorTypeId: unique symbol = internal.SupervisorTypeId + +/** + * @since 2.0.0 + * @category symbols + */ +export type SupervisorTypeId = typeof SupervisorTypeId + +/** + * @since 2.0.0 + * @category models + */ +export interface Supervisor extends Supervisor.Variance { + /** + * Returns an `Effect` that succeeds with the value produced by this + * supervisor. This value may change over time, reflecting what the supervisor + * produces as it supervises fibers. + */ + readonly value: Effect.Effect + + /** + * Supervises the start of a `Fiber`. + */ + onStart( + context: Context.Context, + effect: Effect.Effect, + parent: Option.Option>, + fiber: Fiber.RuntimeFiber + ): void + + /** + * Supervises the end of a `Fiber`. + */ + onEnd(value: Exit.Exit, fiber: Fiber.RuntimeFiber): void + + /** + * Supervises the execution of an `Effect` by a `Fiber`. + */ + onEffect(fiber: Fiber.RuntimeFiber, effect: Effect.Effect): void + + /** + * Supervises the suspension of a computation running within a `Fiber`. + */ + onSuspend(fiber: Fiber.RuntimeFiber): void + + /** + * Supervises the resumption of a computation running within a `Fiber`. + */ + onResume(fiber: Fiber.RuntimeFiber): void + + /** + * Maps this supervisor to another one, which has the same effect, but whose + * value has been transformed by the specified function. + */ + map(f: (a: T) => B): Supervisor + + /** + * Returns a new supervisor that performs the function of this supervisor, and + * the function of the specified supervisor, producing a tuple of the outputs + * produced by both supervisors. + */ + zip(right: Supervisor): Supervisor<[T, A]> +} + +/** + * @since 2.0.0 + */ +export declare namespace Supervisor { + /** + * @since 2.0.0 + * @category models + */ + export interface Variance { + readonly [SupervisorTypeId]: { + readonly _T: Types.Covariant + } + } +} + +/** + * @since 2.0.0 + * @category context + */ +export const addSupervisor: (supervisor: Supervisor) => Layer.Layer = circular.addSupervisor + +/** + * Creates a new supervisor that tracks children in a set. + * + * @since 2.0.0 + * @category constructors + */ +export const fibersIn: ( + ref: MutableRef.MutableRef>> +) => Effect.Effect>>> = internal.fibersIn + +/** + * Creates a new supervisor that constantly yields effect when polled + * + * @since 2.0.0 + * @category constructors + */ +export const fromEffect: (effect: Effect.Effect) => Supervisor = internal.fromEffect + +/** + * A supervisor that doesn't do anything in response to supervision events. + * + * @since 2.0.0 + * @category constructors + */ +export const none: Supervisor = internal.none + +/** + * Creates a new supervisor that tracks children in a set. + * + * @since 2.0.0 + * @category constructors + */ +export const track: Effect.Effect>>> = internal.track + +/** + * Unsafely creates a new supervisor that tracks children in a set. + * + * @since 2.0.0 + * @category unsafe + */ +export const unsafeTrack: () => Supervisor>> = internal.unsafeTrack + +/** + * @since 2.0.0 + * @category constructors + */ +export abstract class AbstractSupervisor implements Supervisor { + /** + * @since 2.0.0 + */ + abstract value: Effect.Effect + + /** + * @since 2.0.0 + */ + onStart( + _context: Context.Context, + _effect: Effect.Effect, + _parent: Option.Option>, + _fiber: Fiber.RuntimeFiber + ): void { + // + } + + /** + * @since 2.0.0 + */ + onEnd( + _value: Exit.Exit, + _fiber: Fiber.RuntimeFiber + ): void { + // + } + + /** + * @since 2.0.0 + */ + onEffect( + _fiber: Fiber.RuntimeFiber, + _effect: Effect.Effect + ): void { + // + } + + /** + * @since 2.0.0 + */ + onSuspend( + _fiber: Fiber.RuntimeFiber + ): void { + // + } + + /** + * @since 2.0.0 + */ + onResume( + _fiber: Fiber.RuntimeFiber + ): void { + // + } + + /** + * @since 2.0.0 + */ + map(f: (a: T) => B): Supervisor { + return new internal.ProxySupervisor(this, core.map(this.value, f)) + } + + /** + * @since 2.0.0 + */ + zip( + right: Supervisor + ): Supervisor<[T, A]> { + return new internal.Zip(this, right) + } + + /** + * @since 2.0.0 + */ + onRun(execution: () => X, _fiber: Fiber.RuntimeFiber): X { + return execution() + } + + /** + * @since 2.0.0 + */ + readonly [SupervisorTypeId]: { + _T: (_: never) => never + } = internal.supervisorVariance +} diff --git a/repos/effect/packages/effect/src/Symbol.ts b/repos/effect/packages/effect/src/Symbol.ts new file mode 100644 index 0000000..eaf4527 --- /dev/null +++ b/repos/effect/packages/effect/src/Symbol.ts @@ -0,0 +1,29 @@ +/** + * @since 2.0.0 + */ + +import * as equivalence from "./Equivalence.js" +import * as predicate from "./Predicate.js" + +/** + * Tests if a value is a `symbol`. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { Predicate } from "effect" + * + * assert.deepStrictEqual(Predicate.isSymbol(Symbol.for("a")), true) + * assert.deepStrictEqual(Predicate.isSymbol("a"), false) + * ``` + * + * @category guards + * @since 2.0.0 + */ +export const isSymbol: (u: unknown) => u is symbol = predicate.isSymbol + +/** + * @category instances + * @since 2.0.0 + */ +export const Equivalence: equivalence.Equivalence = equivalence.symbol diff --git a/repos/effect/packages/effect/src/SynchronizedRef.ts b/repos/effect/packages/effect/src/SynchronizedRef.ts new file mode 100644 index 0000000..cf763ec --- /dev/null +++ b/repos/effect/packages/effect/src/SynchronizedRef.ts @@ -0,0 +1,270 @@ +/** + * @since 2.0.0 + */ +import type * as Effect from "./Effect.js" +import * as circular from "./internal/effect/circular.js" +import * as ref from "./internal/ref.js" +import * as internal from "./internal/synchronizedRef.js" +import type * as Option from "./Option.js" +import type * as Ref from "./Ref.js" +import type * as Types from "./Types.js" +import type * as Unify from "./Unify.js" + +/** + * @since 2.0.0 + * @category symbols + */ +export const SynchronizedRefTypeId: unique symbol = circular.SynchronizedTypeId + +/** + * @since 2.0.0 + * @category symbols + */ +export type SynchronizedRefTypeId = typeof SynchronizedRefTypeId + +/** + * @since 2.0.0 + * @category models + */ +export interface SynchronizedRef extends SynchronizedRef.Variance, Ref.Ref { + modifyEffect(f: (a: A) => Effect.Effect): Effect.Effect + readonly [Unify.typeSymbol]?: unknown + readonly [Unify.unifySymbol]?: SynchronizedRefUnify + readonly [Unify.ignoreSymbol]?: SynchronizedRefUnifyIgnore +} + +/** + * @category models + * @since 3.8.0 + */ +export interface SynchronizedRefUnify extends Ref.RefUnify { + SynchronizedRef?: () => Extract> +} + +/** + * @category models + * @since 3.8.0 + */ +export interface SynchronizedRefUnifyIgnore extends Ref.RefUnifyIgnore { + Ref?: true +} + +/** + * @since 2.0.0 + */ +export declare namespace SynchronizedRef { + /** + * @since 2.0.0 + * @category models + */ + export interface Variance { + readonly [SynchronizedRefTypeId]: { + readonly _A: Types.Invariant + } + } +} + +/** + * @since 2.0.0 + * @category constructors + */ +export const make: (value: A) => Effect.Effect> = circular.makeSynchronized + +/** + * @since 2.0.0 + * @category getters + */ +export const get: (self: SynchronizedRef) => Effect.Effect = ref.get + +/** + * @since 2.0.0 + * @category utils + */ +export const getAndSet: { + (value: A): (self: Ref.Ref) => Effect.Effect + (self: Ref.Ref, value: A): Effect.Effect +} = ref.getAndSet + +/** + * @since 2.0.0 + * @category utils + */ +export const getAndUpdate: { + (f: (a: A) => A): (self: Ref.Ref) => Effect.Effect + (self: Ref.Ref, f: (a: A) => A): Effect.Effect +} = ref.getAndUpdate + +/** + * @since 2.0.0 + * @category utils + */ +export const getAndUpdateEffect: { + (f: (a: A) => Effect.Effect): (self: SynchronizedRef) => Effect.Effect + (self: SynchronizedRef, f: (a: A) => Effect.Effect): Effect.Effect +} = internal.getAndUpdateEffect + +/** + * @since 2.0.0 + * @category utils + */ +export const getAndUpdateSome: { + (pf: (a: A) => Option.Option): (self: Ref.Ref) => Effect.Effect + (self: Ref.Ref, pf: (a: A) => Option.Option): Effect.Effect +} = ref.getAndUpdateSome + +/** + * @since 2.0.0 + * @category utils + */ +export const getAndUpdateSomeEffect: { + (pf: (a: A) => Option.Option>): (self: SynchronizedRef) => Effect.Effect + (self: SynchronizedRef, pf: (a: A) => Option.Option>): Effect.Effect +} = internal.getAndUpdateSomeEffect + +/** + * @since 2.0.0 + * @category utils + */ +export const modify: { + (f: (a: A) => readonly [B, A]): (self: SynchronizedRef) => Effect.Effect + (self: SynchronizedRef, f: (a: A) => readonly [B, A]): Effect.Effect +} = internal.modify + +/** + * @since 2.0.0 + * @category utils + */ +export const modifyEffect: { + (f: (a: A) => Effect.Effect): (self: SynchronizedRef) => Effect.Effect + (self: SynchronizedRef, f: (a: A) => Effect.Effect): Effect.Effect +} = internal.modifyEffect + +/** + * @since 2.0.0 + * @category utils + */ +export const modifySome: { + ( + fallback: B, + pf: (a: A) => Option.Option + ): (self: Ref.Ref) => Effect.Effect + ( + self: Ref.Ref, + fallback: B, + pf: (a: A) => Option.Option + ): Effect.Effect +} = ref.modifySome + +/** + * @since 2.0.0 + * @category utils + */ +export const modifySomeEffect: { + ( + fallback: B, + pf: (a: A) => Option.Option> + ): (self: SynchronizedRef) => Effect.Effect + ( + self: SynchronizedRef, + fallback: B, + pf: (a: A) => Option.Option> + ): Effect.Effect +} = internal.modifySomeEffect + +/** + * @since 2.0.0 + * @category utils + */ +export const set: { + (value: A): (self: Ref.Ref) => Effect.Effect + (self: Ref.Ref, value: A): Effect.Effect +} = ref.set + +/** + * @since 2.0.0 + * @category utils + */ +export const setAndGet: { + (value: A): (self: Ref.Ref) => Effect.Effect + (self: Ref.Ref, value: A): Effect.Effect +} = ref.setAndGet + +/** + * @since 2.0.0 + * @category utils + */ +export const update: { + (f: (a: A) => A): (self: Ref.Ref) => Effect.Effect + (self: Ref.Ref, f: (a: A) => A): Effect.Effect +} = ref.update + +/** + * @since 2.0.0 + * @category utils + */ +export const updateEffect: { + (f: (a: A) => Effect.Effect): (self: SynchronizedRef) => Effect.Effect + (self: SynchronizedRef, f: (a: A) => Effect.Effect): Effect.Effect +} = internal.updateEffect + +/** + * @since 2.0.0 + * @category utils + */ +export const updateAndGet: { + (f: (a: A) => A): (self: Ref.Ref) => Effect.Effect + (self: Ref.Ref, f: (a: A) => A): Effect.Effect +} = ref.updateAndGet + +/** + * @since 2.0.0 + * @category utils + */ +export const updateAndGetEffect: { + (f: (a: A) => Effect.Effect): (self: SynchronizedRef) => Effect.Effect + (self: SynchronizedRef, f: (a: A) => Effect.Effect): Effect.Effect +} = internal.updateAndGetEffect + +/** + * @since 2.0.0 + * @category utils + */ +export const updateSome: { + (f: (a: A) => Option.Option): (self: Ref.Ref) => Effect.Effect + (self: Ref.Ref, f: (a: A) => Option.Option): Effect.Effect +} = ref.updateSome + +/** + * @since 2.0.0 + * @category utils + */ +export const updateSomeEffect: { + ( + pf: (a: A) => Option.Option> + ): (self: SynchronizedRef) => Effect.Effect + (self: SynchronizedRef, pf: (a: A) => Option.Option>): Effect.Effect +} = internal.updateSomeEffect + +/** + * @since 2.0.0 + * @category utils + */ +export const updateSomeAndGet: { + (pf: (a: A) => Option.Option): (self: Ref.Ref) => Effect.Effect + (self: Ref.Ref, pf: (a: A) => Option.Option): Effect.Effect +} = ref.updateSomeAndGet + +/** + * @since 2.0.0 + * @category utils + */ +export const updateSomeAndGetEffect: { + (pf: (a: A) => Option.Option>): (self: SynchronizedRef) => Effect.Effect + (self: SynchronizedRef, pf: (a: A) => Option.Option>): Effect.Effect +} = circular.updateSomeAndGetEffectSynchronized + +/** + * @since 2.0.0 + * @category unsafe + */ +export const unsafeMake: (value: A) => SynchronizedRef = circular.unsafeMakeSynchronized diff --git a/repos/effect/packages/effect/src/TArray.ts b/repos/effect/packages/effect/src/TArray.ts new file mode 100644 index 0000000..95bc51d --- /dev/null +++ b/repos/effect/packages/effect/src/TArray.ts @@ -0,0 +1,495 @@ +/** + * @since 2.0.0 + */ +import * as internal from "./internal/stm/tArray.js" +import type * as Option from "./Option.js" +import type * as Order from "./Order.js" +import type { Predicate } from "./Predicate.js" +import type * as STM from "./STM.js" +import type * as TRef from "./TRef.js" +import type * as Types from "./Types.js" + +/** + * @since 2.0.0 + * @category symbols + */ +export const TArrayTypeId: unique symbol = internal.TArrayTypeId + +/** + * @since 2.0.0 + * @category symbols + */ +export type TArrayTypeId = typeof TArrayTypeId + +/** + * @since 2.0.0 + * @category models + */ +export interface TArray extends TArray.Variance {} +/** + * @internal + * @since 2.0.0 + */ +export interface TArray { + /** @internal */ + readonly chunk: Array> +} + +/** + * @since 2.0.0 + */ +export declare namespace TArray { + /** + * @since 2.0.0 + * @category models + */ + export interface Variance { + readonly [TArrayTypeId]: { + readonly _A: Types.Invariant + } + } +} + +/** + * Finds the result of applying a partial function to the first value in its + * domain. + * + * @since 2.0.0 + * @category elements + */ +export const collectFirst: { + (pf: (a: A) => Option.Option): (self: TArray) => STM.STM> + (self: TArray, pf: (a: A) => Option.Option): STM.STM> +} = internal.collectFirst + +/** + * Finds the result of applying an transactional partial function to the first + * value in its domain. + * + * @since 2.0.0 + * @category elements + */ +export const collectFirstSTM: { + (pf: (a: A) => Option.Option>): (self: TArray) => STM.STM, E, R> + (self: TArray, pf: (a: A) => Option.Option>): STM.STM, E, R> +} = internal.collectFirstSTM + +/** + * Determine if the array contains a specified value. + * + * @macro trace + * @since 2.0.0 + * @category elements + */ +export const contains: { + (value: A): (self: TArray) => STM.STM + (self: TArray, value: A): STM.STM +} = internal.contains + +/** + * Count the values in the array matching a predicate. + * + * @macro trace + * @since 2.0.0 + * @category folding + */ +export const count: { + (predicate: Predicate): (self: TArray) => STM.STM + (self: TArray, predicate: Predicate): STM.STM +} = internal.count + +/** + * Count the values in the array matching a transactional predicate. + * + * @macro trace + * @since 2.0.0 + * @category folding + */ +export const countSTM: { + (predicate: (value: A) => STM.STM): (self: TArray) => STM.STM + (self: TArray, predicate: (value: A) => STM.STM): STM.STM +} = internal.countSTM + +/** + * Makes an empty `TArray`. + * + * @since 2.0.0 + * @category constructors + */ +export const empty: () => STM.STM> = internal.empty + +/** + * Atomically evaluate the conjunction of a predicate across the members of + * the array. + * + * @since 2.0.0 + * @category elements + */ +export const every: { + (predicate: Predicate): (self: TArray) => STM.STM + (self: TArray, predicate: Predicate): STM.STM +} = internal.every + +/** + * Atomically evaluate the conjunction of a transactional predicate across the + * members of the array. + * + * @since 2.0.0 + * @category elements + */ +export const everySTM: { + (predicate: (value: A) => STM.STM): (self: TArray) => STM.STM + (self: TArray, predicate: (value: A) => STM.STM): STM.STM +} = internal.everySTM + +/** + * Find the first element in the array matching the specified predicate. + * + * @since 2.0.0 + * @category elements + */ +export const findFirst: { + (predicate: Predicate): (self: TArray) => STM.STM> + (self: TArray, predicate: Predicate): STM.STM> +} = internal.findFirst + +/** + * Get the first index of a specific value in the array. + * + * @since 2.0.0 + * @category elements + */ +export const findFirstIndex: { + (value: A): (self: TArray) => STM.STM> + (self: TArray, value: A): STM.STM> +} = internal.findFirstIndex + +/** + * Get the first index of a specific value in the array starting from the + * specified index. + * + * @since 2.0.0 + * @category elements + */ +export const findFirstIndexFrom: { + (value: A, from: number): (self: TArray) => STM.STM> + (self: TArray, value: A, from: number): STM.STM> +} = internal.findFirstIndexFrom + +/** + * Get the index of the first entry in the array matching a predicate. + * + * @since 2.0.0 + * @category elements + */ +export const findFirstIndexWhere: { + (predicate: Predicate): (self: TArray) => STM.STM> + (self: TArray, predicate: Predicate): STM.STM> +} = internal.findFirstIndexWhere + +/** + * Get the index of the first entry in the array starting from the specified + * index, matching a predicate. + * + * @since 2.0.0 + * @category elements + */ +export const findFirstIndexWhereFrom: { + (predicate: Predicate, from: number): (self: TArray) => STM.STM> + (self: TArray, predicate: Predicate, from: number): STM.STM> +} = internal.findFirstIndexWhereFrom + +/** + * Get the index of the next entry that matches a transactional predicate. + * + * @since 2.0.0 + * @category elements + */ +export const findFirstIndexWhereSTM: { + (predicate: (value: A) => STM.STM): (self: TArray) => STM.STM, E, R> + (self: TArray, predicate: (value: A) => STM.STM): STM.STM, E, R> +} = internal.findFirstIndexWhereSTM + +/** + * Starting at specified index, get the index of the next entry that matches a + * transactional predicate. + * + * @since 2.0.0 + * @category elements + */ +export const findFirstIndexWhereFromSTM: { + ( + predicate: (value: A) => STM.STM, + from: number + ): (self: TArray) => STM.STM, E, R> + ( + self: TArray, + predicate: (value: A) => STM.STM, + from: number + ): STM.STM, E, R> +} = internal.findFirstIndexWhereFromSTM + +/** + * Find the first element in the array matching a transactional predicate. + * + * @since 2.0.0 + * @category elements + */ +export const findFirstSTM: { + (predicate: (value: A) => STM.STM): (self: TArray) => STM.STM, E, R> + (self: TArray, predicate: (value: A) => STM.STM): STM.STM, E, R> +} = internal.findFirstSTM + +/** + * Find the last element in the array matching a predicate. + * + * @since 2.0.0 + * @category elements + */ +export const findLast: { + (predicate: Predicate): (self: TArray) => STM.STM> + (self: TArray, predicate: Predicate): STM.STM> +} = internal.findLast + +/** + * Get the last index of a specific value in the array bounded above by a + * specific index. + * + * @since 2.0.0 + * @category elements + */ +export const findLastIndex: { + (value: A): (self: TArray) => STM.STM> + (self: TArray, value: A): STM.STM> +} = internal.findLastIndex + +/** + * Get the last index of a specific value in the array bounded above by a + * specific index. + * + * @since 2.0.0 + * @category elements + */ +export const findLastIndexFrom: { + (value: A, end: number): (self: TArray) => STM.STM> + (self: TArray, value: A, end: number): STM.STM> +} = internal.findLastIndexFrom + +/** + * Find the last element in the array matching a transactional predicate. + * + * @since 2.0.0 + * @category elements + */ +export const findLastSTM: { + (predicate: (value: A) => STM.STM): (self: TArray) => STM.STM, E, R> + (self: TArray, predicate: (value: A) => STM.STM): STM.STM, E, R> +} = internal.findLastSTM + +/** + * Atomically performs transactional effect for each item in array. + * + * @since 2.0.0 + * @category elements + */ +export const forEach: { + (f: (value: A) => STM.STM): (self: TArray) => STM.STM + (self: TArray, f: (value: A) => STM.STM): STM.STM +} = internal.forEach + +/** + * Creates a new `TArray` from an iterable collection of values. + * + * @since 2.0.0 + * @category constructors + */ +export const fromIterable: (iterable: Iterable) => STM.STM> = internal.fromIterable + +/** + * Extracts value from ref in array. + * + * @since 2.0.0 + * @category elements + */ +export const get: { + (index: number): (self: TArray) => STM.STM + (self: TArray, index: number): STM.STM +} = internal.get + +/** + * The first entry of the array, if it exists. + * + * @since 2.0.0 + * @category elements + */ +export const headOption: (self: TArray) => STM.STM> = internal.headOption + +/** + * The last entry in the array, if it exists. + * + * @since 2.0.0 + * @category elements + */ +export const lastOption: (self: TArray) => STM.STM> = internal.lastOption + +/** + * Makes a new `TArray` that is initialized with specified values. + * + * @since 2.0.0 + * @category constructors + */ +export const make: ]>( + ...elements: Elements +) => STM.STM> = internal.make + +/** + * Atomically compute the greatest element in the array, if it exists. + * + * @since 2.0.0 + * @category elements + */ +export const maxOption: { + (order: Order.Order): (self: TArray) => STM.STM> + (self: TArray, order: Order.Order): STM.STM> +} = internal.maxOption + +/** + * Atomically compute the least element in the array, if it exists. + * + * @since 2.0.0 + * @category elements + */ +export const minOption: { + (order: Order.Order): (self: TArray) => STM.STM> + (self: TArray, order: Order.Order): STM.STM> +} = internal.minOption + +/** + * Atomically folds using a pure function. + * + * @since 2.0.0 + * @category folding + */ +export const reduce: { + (zero: Z, f: (accumulator: Z, current: A) => Z): (self: TArray) => STM.STM + (self: TArray, zero: Z, f: (accumulator: Z, current: A) => Z): STM.STM +} = internal.reduce + +/** + * Atomically reduce the array, if non-empty, by a binary operator. + * + * @since 2.0.0 + * @category elements + */ +export const reduceOption: { + (f: (x: A, y: A) => A): (self: TArray) => STM.STM> + (self: TArray, f: (x: A, y: A) => A): STM.STM> +} = internal.reduceOption + +/** + * Atomically reduce the non-empty array using a transactional binary + * operator. + * + * @since 2.0.0 + * @category elements + */ +export const reduceOptionSTM: { + (f: (x: A, y: A) => STM.STM): (self: TArray) => STM.STM, E, R> + (self: TArray, f: (x: A, y: A) => STM.STM): STM.STM, E, R> +} = internal.reduceOptionSTM + +/** + * Atomically folds using a transactional function. + * + * @macro trace + * @since 2.0.0 + * @category folding + */ +export const reduceSTM: { + (zero: Z, f: (accumulator: Z, current: A) => STM.STM): (self: TArray) => STM.STM + (self: TArray, zero: Z, f: (accumulator: Z, current: A) => STM.STM): STM.STM +} = internal.reduceSTM + +/** + * Returns the size of the `TArray`. + * + * @since 2.0.0 + * @category getters + */ +export const size: (self: TArray) => number = internal.size + +/** + * Determine if the array contains a value satisfying a predicate. + * + * @since 2.0.0 + * @category elements + */ +export const some: { + (predicate: Predicate): (self: TArray) => STM.STM + (self: TArray, predicate: Predicate): STM.STM +} = internal.some + +/** + * Determine if the array contains a value satisfying a transactional + * predicate. + * + * @since 2.0.0 + * @category elements + */ +export const someSTM: { + (predicate: (value: A) => STM.STM): (self: TArray) => STM.STM + (self: TArray, predicate: (value: A) => STM.STM): STM.STM +} = internal.someSTM + +/** + * Collects all elements into a chunk. + * + * @since 2.0.0 + * @since 2.0.0 + * @category destructors + */ +export const toArray: (self: TArray) => STM.STM> = internal.toArray + +/** + * Atomically updates all elements using a pure function. + * + * @since 2.0.0 + * @category elements + */ +export const transform: { + (f: (value: A) => A): (self: TArray) => STM.STM + (self: TArray, f: (value: A) => A): STM.STM +} = internal.transform + +/** + * Atomically updates all elements using a transactional effect. + * + * @since 2.0.0 + * @category elements + */ +export const transformSTM: { + (f: (value: A) => STM.STM): (self: TArray) => STM.STM + (self: TArray, f: (value: A) => STM.STM): STM.STM +} = internal.transformSTM + +/** + * Updates element in the array with given function. + * + * @since 2.0.0 + * @category elements + */ +export const update: { + (index: number, f: (value: A) => A): (self: TArray) => STM.STM + (self: TArray, index: number, f: (value: A) => A): STM.STM +} = internal.update + +/** + * Atomically updates element in the array with given transactional effect. + * + * @since 2.0.0 + * @category elements + */ +export const updateSTM: { + (index: number, f: (value: A) => STM.STM): (self: TArray) => STM.STM + (self: TArray, index: number, f: (value: A) => STM.STM): STM.STM +} = internal.updateSTM diff --git a/repos/effect/packages/effect/src/TDeferred.ts b/repos/effect/packages/effect/src/TDeferred.ts new file mode 100644 index 0000000..44536d7 --- /dev/null +++ b/repos/effect/packages/effect/src/TDeferred.ts @@ -0,0 +1,100 @@ +/** + * @since 2.0.0 + */ +import type * as Either from "./Either.js" +import * as internal from "./internal/stm/tDeferred.js" +import type * as Option from "./Option.js" +import type * as STM from "./STM.js" +import type * as TRef from "./TRef.js" +import type * as Types from "./Types.js" + +/** + * @since 2.0.0 + * @category symbols + */ +export const TDeferredTypeId: unique symbol = internal.TDeferredTypeId + +/** + * @since 2.0.0 + * @category symbols + */ +export type TDeferredTypeId = typeof TDeferredTypeId + +/** + * @since 2.0.0 + * @category models + */ +export interface TDeferred extends TDeferred.Variance {} +/** + * @internal + * @since 2.0.0 + */ +export interface TDeferred { + /** @internal */ + readonly ref: TRef.TRef>> +} + +/** + * @since 2.0.0 + */ +export declare namespace TDeferred { + /** + * @since 2.0.0 + * @category models + */ + export interface Variance { + readonly [TDeferredTypeId]: { + readonly _A: Types.Invariant + readonly _E: Types.Invariant + } + } +} + +const _await: (self: TDeferred) => STM.STM = internal._await + +export { + /** + * @since 2.0.0 + * @category getters + */ + _await as await +} + +/** + * @since 2.0.0 + * @category mutations + */ +export const done: { + (either: Either.Either): (self: TDeferred) => STM.STM + (self: TDeferred, either: Either.Either): STM.STM +} = internal.done + +/** + * @since 2.0.0 + * @category mutations + */ +export const fail: { + (error: E): (self: TDeferred) => STM.STM + (self: TDeferred, error: E): STM.STM +} = internal.fail + +/** + * @since 2.0.0 + * @category constructors + */ +export const make: () => STM.STM> = internal.make + +/** + * @since 2.0.0 + * @category getters + */ +export const poll: (self: TDeferred) => STM.STM>> = internal.poll + +/** + * @since 2.0.0 + * @category mutations + */ +export const succeed: { + (value: A): (self: TDeferred) => STM.STM + (self: TDeferred, value: A): STM.STM +} = internal.succeed diff --git a/repos/effect/packages/effect/src/TMap.ts b/repos/effect/packages/effect/src/TMap.ts new file mode 100644 index 0000000..d83c842 --- /dev/null +++ b/repos/effect/packages/effect/src/TMap.ts @@ -0,0 +1,515 @@ +/** + * @since 2.0.0 + */ +import type * as Chunk from "./Chunk.js" +import type { LazyArg } from "./Function.js" +import type * as HashMap from "./HashMap.js" +import * as internal from "./internal/stm/tMap.js" +import type * as Option from "./Option.js" +import type * as STM from "./STM.js" +import type * as TArray from "./TArray.js" +import type * as TRef from "./TRef.js" +import type * as Types from "./Types.js" + +/** + * @since 2.0.0 + * @category symbols + */ +export const TMapTypeId: unique symbol = internal.TMapTypeId + +/** + * @since 2.0.0 + * @category symbols + */ +export type TMapTypeId = typeof TMapTypeId + +/** + * Transactional map implemented on top of `TRef` and `TArray`. Resolves + * conflicts via chaining. + * + * @since 2.0.0 + * @category models + */ +export interface TMap extends TMap.Variance {} +/** + * @internal + * @since 2.0.0 + */ +export interface TMap { + /** @internal */ + readonly tBuckets: TRef.TRef>> + /** @internal */ + readonly tSize: TRef.TRef +} + +/** + * @since 2.0.0 + */ +export declare namespace TMap { + /** + * @since 2.0.0 + * @category models + */ + export interface Variance { + readonly [TMapTypeId]: { + readonly _K: Types.Invariant + readonly _V: Types.Invariant + } + } +} + +/** + * Makes an empty `TMap`. + * + * @since 2.0.0 + * @category constructors + */ +export const empty: () => STM.STM> = internal.empty + +/** + * Finds the key/value pair matching the specified predicate, and uses the + * provided function to extract a value out of it. + * + * @since 2.0.0 + * @category elements + */ +export const find: { + (pf: (key: K, value: V) => Option.Option): (self: TMap) => STM.STM> + (self: TMap, pf: (key: K, value: V) => Option.Option): STM.STM> +} = internal.find + +/** + * Finds the key/value pair matching the specified predicate, and uses the + * provided effectful function to extract a value out of it. + * + * @since 2.0.0 + * @category elements + */ +export const findSTM: { + ( + f: (key: K, value: V) => STM.STM, R> + ): (self: TMap) => STM.STM, E, R> + ( + self: TMap, + f: (key: K, value: V) => STM.STM, R> + ): STM.STM, E, R> +} = internal.findSTM + +/** + * Finds all the key/value pairs matching the specified predicate, and uses + * the provided function to extract values out them. + * + * @since 2.0.0 + * @category elements + */ +export const findAll: { + (pf: (key: K, value: V) => Option.Option): (self: TMap) => STM.STM> + (self: TMap, pf: (key: K, value: V) => Option.Option): STM.STM> +} = internal.findAll + +/** + * Finds all the key/value pairs matching the specified predicate, and uses + * the provided effectful function to extract values out of them.. + * + * @since 2.0.0 + * @category elements + */ +export const findAllSTM: { + ( + pf: (key: K, value: V) => STM.STM, R> + ): (self: TMap) => STM.STM, E, R> + (self: TMap, pf: (key: K, value: V) => STM.STM, R>): STM.STM, E, R> +} = internal.findAllSTM + +/** + * Atomically performs transactional-effect for each binding present in map. + * + * @since 2.0.0 + * @category elements + */ +export const forEach: { + (f: (key: K, value: V) => STM.STM): (self: TMap) => STM.STM + (self: TMap, f: (key: K, value: V) => STM.STM): STM.STM +} = internal.forEach + +/** + * Creates a new `TMap` from an iterable collection of key/value pairs. + * + * @since 2.0.0 + * @category constructors + */ +export const fromIterable: ( + iterable: Iterable +) => STM.STM> = internal.fromIterable + +/** + * Retrieves value associated with given key. + * + * @since 2.0.0 + * @category elements + */ +export const get: { + (key: K): (self: TMap) => STM.STM> + (self: TMap, key: K): STM.STM> +} = internal.get + +/** + * Retrieves value associated with given key or default value, in case the key + * isn't present. + * + * @since 2.0.0 + * @category elements + */ +export const getOrElse: { + (key: K, fallback: LazyArg): (self: TMap) => STM.STM + (self: TMap, key: K, fallback: LazyArg): STM.STM +} = internal.getOrElse + +/** + * Tests whether or not map contains a key. + * + * @since 2.0.0 + * @category elements + */ +export const has: { + (key: K): (self: TMap) => STM.STM + (self: TMap, key: K): STM.STM +} = internal.has + +/** + * Tests if the map is empty or not. + * + * @since 2.0.0 + * @category getters + */ +export const isEmpty: (self: TMap) => STM.STM = internal.isEmpty + +/** + * Collects all keys stored in map. + * + * @since 2.0.0 + * @category elements + */ +export const keys: (self: TMap) => STM.STM> = internal.keys + +/** + * Makes a new `TMap` that is initialized with specified values. + * + * @since 2.0.0 + * @category constructors + */ +export const make: (...entries: Array) => STM.STM> = internal.make + +/** + * If the key is not already associated with a value, stores the provided value, + * otherwise merge the existing value with the new one using function `f` and + * store the result. + * + * @since 2.0.0 + * @category mutations + */ +export const merge: { + (key: K, value: V, f: (x: V, y: V) => V): (self: TMap) => STM.STM + (self: TMap, key: K, value: V, f: (x: V, y: V) => V): STM.STM +} = internal.merge + +/** + * Atomically folds using a pure function. + * + * @since 2.0.0 + * @category folding + */ +export const reduce: { + (zero: Z, f: (acc: Z, value: V, key: K) => Z): (self: TMap) => STM.STM + (self: TMap, zero: Z, f: (acc: Z, value: V, key: K) => Z): STM.STM +} = internal.reduce + +/** + * Atomically folds using a transactional function. + * + * @since 2.0.0 + * @category folding + */ +export const reduceSTM: { + (zero: Z, f: (acc: Z, value: V, key: K) => STM.STM): (self: TMap) => STM.STM + (self: TMap, zero: Z, f: (acc: Z, value: V, key: K) => STM.STM): STM.STM +} = internal.reduceSTM + +/** + * Removes binding for given key. + * + * @since 2.0.0 + * @category mutations + */ +export const remove: { + (key: K): (self: TMap) => STM.STM + (self: TMap, key: K): STM.STM +} = internal.remove + +/** + * Deletes all entries associated with the specified keys. + * + * @since 2.0.0 + * @category mutations + */ +export const removeAll: { + (keys: Iterable): (self: TMap) => STM.STM + (self: TMap, keys: Iterable): STM.STM +} = internal.removeAll + +/** + * Removes entries from a `TMap` that satisfy the specified predicate and returns the removed entries + * (or `void` if `discard = true`). + * + * @since 2.0.0 + * @category mutations + */ +export const removeIf: { + ( + predicate: (key: K, value: V) => boolean, + options: { + readonly discard: true + } + ): (self: TMap) => STM.STM + ( + predicate: (key: K, value: V) => boolean, + options?: { + readonly discard: false + } + ): (self: TMap) => STM.STM> + ( + self: TMap, + predicate: (key: K, value: V) => boolean, + options: { + readonly discard: true + } + ): STM.STM + ( + self: TMap, + predicate: (key: K, value: V) => boolean, + options?: { + readonly discard: false + } + ): STM.STM> +} = internal.removeIf + +/** + * Retains entries in a `TMap` that satisfy the specified predicate and returns the removed entries + * (or `void` if `discard = true`). + * + * @since 2.0.0 + * @category mutations + */ +export const retainIf: { + ( + predicate: (key: K, value: V) => boolean, + options: { + readonly discard: true + } + ): (self: TMap) => STM.STM + ( + predicate: (key: K, value: V) => boolean, + options?: { + readonly discard: false + } + ): (self: TMap) => STM.STM> + ( + self: TMap, + predicate: (key: K, value: V) => boolean, + options: { + readonly discard: true + } + ): STM.STM + ( + self: TMap, + predicate: (key: K, value: V) => boolean, + options?: { + readonly discard: false + } + ): STM.STM> +} = internal.retainIf + +/** + * Stores new binding into the map. + * + * @since 2.0.0 + * @category mutations + */ +export const set: { + (key: K, value: V): (self: TMap) => STM.STM + (self: TMap, key: K, value: V): STM.STM +} = internal.set + +/** + * Stores new binding in the map if it does not already exist. + * + * @since 2.0.0 + * @category mutations + */ +export const setIfAbsent: { + (key: K, value: V): (self: TMap) => STM.STM + (self: TMap, key: K, value: V): STM.STM +} = internal.setIfAbsent + +/** + * Returns the number of bindings. + * + * @since 2.0.0 + * @category getters + */ +export const size: (self: TMap) => STM.STM = internal.size + +/** + * Takes the first matching value, or retries until there is one. + * + * @since 2.0.0 + * @category mutations + */ +export const takeFirst: { + (pf: (key: K, value: V) => Option.Option): (self: TMap) => STM.STM + (self: TMap, pf: (key: K, value: V) => Option.Option): STM.STM +} = internal.takeFirst + +/** + * Takes the first matching value, or retries until there is one. + * + * @since 2.0.0 + * @category mutations + */ +export const takeFirstSTM: { + (pf: (key: K, value: V) => STM.STM, R>): (self: TMap) => STM.STM + (self: TMap, pf: (key: K, value: V) => STM.STM, R>): STM.STM +} = internal.takeFirstSTM + +/** + * Takes all matching values, or retries until there is at least one. + * + * @since 2.0.0 + * @category mutations + */ +export const takeSome: { + (pf: (key: K, value: V) => Option.Option): (self: TMap) => STM.STM<[A, ...Array]> + (self: TMap, pf: (key: K, value: V) => Option.Option): STM.STM<[A, ...Array]> +} = internal.takeSome + +/** + * Takes all matching values, or retries until there is at least one. + * + * @since 2.0.0 + * @category mutations + */ +export const takeSomeSTM: { + ( + pf: (key: K, value: V) => STM.STM, R> + ): (self: TMap) => STM.STM<[A, ...Array], E, R> + ( + self: TMap, + pf: (key: K, value: V) => STM.STM, R> + ): STM.STM<[A, ...Array], E, R> +} = internal.takeSomeSTM + +/** + * Collects all bindings into a `Chunk`. + * + * @since 2.0.0 + * @category destructors + */ +export const toChunk: (self: TMap) => STM.STM> = internal.toChunk + +/** + * Collects all bindings into a `HashMap`. + * + * @since 2.0.0 + * @category destructors + */ +export const toHashMap: (self: TMap) => STM.STM> = internal.toHashMap + +/** + * Collects all bindings into an `Array`. + * + * @since 2.0.0 + * @category destructors + */ +export const toArray: (self: TMap) => STM.STM> = internal.toArray + +/** + * Collects all bindings into a `Map`. + * + * @since 2.0.0 + * @category destructors + */ +export const toMap: (self: TMap) => STM.STM> = internal.toMap + +/** + * Atomically updates all bindings using a pure function. + * + * @since 2.0.0 + * @category mutations + */ +export const transform: { + (f: (key: K, value: V) => readonly [K, V]): (self: TMap) => STM.STM + (self: TMap, f: (key: K, value: V) => readonly [K, V]): STM.STM +} = internal.transform + +/** + * Atomically updates all bindings using a transactional function. + * + * @since 2.0.0 + * @category mutations + */ +export const transformSTM: { + (f: (key: K, value: V) => STM.STM): (self: TMap) => STM.STM + (self: TMap, f: (key: K, value: V) => STM.STM): STM.STM +} = internal.transformSTM + +/** + * Atomically updates all values using a pure function. + * + * @since 2.0.0 + * @category mutations + */ +export const transformValues: { + (f: (value: V) => V): (self: TMap) => STM.STM + (self: TMap, f: (value: V) => V): STM.STM +} = internal.transformValues + +/** + * Atomically updates all values using a transactional function. + * + * @since 2.0.0 + * @category mutations + */ +export const transformValuesSTM: { + (f: (value: V) => STM.STM): (self: TMap) => STM.STM + (self: TMap, f: (value: V) => STM.STM): STM.STM +} = internal.transformValuesSTM + +/** + * Updates the mapping for the specified key with the specified function, + * which takes the current value of the key as an input, if it exists, and + * either returns `Some` with a new value to indicate to update the value in + * the map or `None` to remove the value from the map. Returns `Some` with the + * updated value or `None` if the value was removed from the map. + * + * @since 2.0.0 + * @category mutations + */ +export const updateWith: { + ( + key: K, + f: (value: Option.Option) => Option.Option + ): (self: TMap) => STM.STM> + ( + self: TMap, + key: K, + f: (value: Option.Option) => Option.Option + ): STM.STM> +} = internal.updateWith + +/** + * Collects all values stored in map. + * + * @since 2.0.0 + * @category elements + */ +export const values: (self: TMap) => STM.STM> = internal.values diff --git a/repos/effect/packages/effect/src/TPriorityQueue.ts b/repos/effect/packages/effect/src/TPriorityQueue.ts new file mode 100644 index 0000000..90f37ac --- /dev/null +++ b/repos/effect/packages/effect/src/TPriorityQueue.ts @@ -0,0 +1,223 @@ +/** + * @since 2.0.0 + */ +import type * as Chunk from "./Chunk.js" +import * as internal from "./internal/stm/tPriorityQueue.js" +import type * as Option from "./Option.js" +import type * as Order from "./Order.js" +import type { Predicate } from "./Predicate.js" +import type * as SortedMap from "./SortedMap.js" +import type * as STM from "./STM.js" +import type * as TRef from "./TRef.js" +import type * as Types from "./Types.js" + +/** + * @since 2.0.0 + * @category symbols + */ +export const TPriorityQueueTypeId: unique symbol = internal.TPriorityQueueTypeId + +/** + * @since 2.0.0 + * @category symbols + */ +export type TPriorityQueueTypeId = typeof TPriorityQueueTypeId + +/** + * A `TPriorityQueue` contains values of type `A` that an `Order` is defined + * on. Unlike a `TQueue`, `take` returns the highest priority value (the value + * that is first in the specified ordering) as opposed to the first value + * offered to the queue. The ordering that elements with the same priority will + * be taken from the queue is not guaranteed. + * + * @since 2.0.0 + * @category models + */ +export interface TPriorityQueue extends TPriorityQueue.Variance {} +/** + * @internal + * @since 2.0.0 + */ +export interface TPriorityQueue { + /** @internal */ + readonly ref: TRef.TRef]>> +} + +/** + * @since 2.0.0 + */ +export declare namespace TPriorityQueue { + /** + * @since 2.0.0 + * @category models + */ + export interface Variance { + readonly [TPriorityQueueTypeId]: { + readonly _A: Types.Invariant + } + } +} + +/** + * Constructs a new empty `TPriorityQueue` with the specified `Order`. + * + * @since 2.0.0 + * @category constructors + */ +export const empty: (order: Order.Order) => STM.STM> = internal.empty + +/** + * Creates a new `TPriorityQueue` from an iterable collection of values. + * + * @since 2.0.0 + * @category constructors + */ +export const fromIterable: ( + order: Order.Order +) => (iterable: Iterable) => STM.STM> = internal.fromIterable + +/** + * Checks whether the queue is empty. + * + * @since 2.0.0 + * @category getters + */ +export const isEmpty: (self: TPriorityQueue) => STM.STM = internal.isEmpty + +/** + * Checks whether the queue is not empty. + * + * @since 2.0.0 + * @category getters + */ +export const isNonEmpty: (self: TPriorityQueue) => STM.STM = internal.isNonEmpty + +/** + * Makes a new `TPriorityQueue` that is initialized with specified values. + * + * @since 2.0.0 + * @category constructors + */ +export const make: (order: Order.Order) => (...elements: Array) => STM.STM> = internal.make + +/** + * Offers the specified value to the queue. + * + * @since 2.0.0 + * @category mutations + */ +export const offer: { + (value: A): (self: TPriorityQueue) => STM.STM + (self: TPriorityQueue, value: A): STM.STM +} = internal.offer + +/** + * Offers all of the elements in the specified collection to the queue. + * + * @since 2.0.0 + * @category mutations + */ +export const offerAll: { + (values: Iterable): (self: TPriorityQueue) => STM.STM + (self: TPriorityQueue, values: Iterable): STM.STM +} = internal.offerAll + +/** + * Peeks at the first value in the queue without removing it, retrying until a + * value is in the queue. + * + * @since 2.0.0 + * @category getters + */ +export const peek: (self: TPriorityQueue) => STM.STM = internal.peek + +/** + * Peeks at the first value in the queue without removing it, returning `None` + * if there is not a value in the queue. + * + * @since 2.0.0 + * @category getters + */ +export const peekOption: (self: TPriorityQueue) => STM.STM> = internal.peekOption + +/** + * Removes all elements from the queue matching the specified predicate. + * + * @since 2.0.0 + * @category getters + */ +export const removeIf: { + (predicate: Predicate): (self: TPriorityQueue) => STM.STM + (self: TPriorityQueue, predicate: Predicate): STM.STM +} = internal.removeIf + +/** + * Retains only elements from the queue matching the specified predicate. + * + * @since 2.0.0 + * @category getters + */ +export const retainIf: { + (predicate: Predicate): (self: TPriorityQueue) => STM.STM + (self: TPriorityQueue, predicate: Predicate): STM.STM +} = internal.retainIf + +/** + * Returns the size of the queue. + * + * @since 2.0.0 + * @category getters + */ +export const size: (self: TPriorityQueue) => STM.STM = internal.size + +/** + * Takes a value from the queue, retrying until a value is in the queue. + * + * @since 2.0.0 + * @category mutations + */ +export const take: (self: TPriorityQueue) => STM.STM = internal.take + +/** + * Takes all values from the queue. + * + * @since 2.0.0 + * @category mutations + */ +export const takeAll: (self: TPriorityQueue) => STM.STM> = internal.takeAll + +/** + * Takes a value from the queue, returning `None` if there is not a value in + * the queue. + * + * @since 2.0.0 + * @category mutations + */ +export const takeOption: (self: TPriorityQueue) => STM.STM> = internal.takeOption + +/** + * Takes up to the specified maximum number of elements from the queue. + * + * @since 2.0.0 + * @category mutations + */ +export const takeUpTo: { + (n: number): (self: TPriorityQueue) => STM.STM> + (self: TPriorityQueue, n: number): STM.STM> +} = internal.takeUpTo + +/** + * Collects all values into a `Chunk`. + * + * @since 2.0.0 + * @category destructors + */ +export const toChunk: (self: TPriorityQueue) => STM.STM> = internal.toChunk + +/** + * Collects all values into an array. + * + * @since 2.0.0 + * @category destructors + */ +export const toArray: (self: TPriorityQueue) => STM.STM> = internal.toArray diff --git a/repos/effect/packages/effect/src/TPubSub.ts b/repos/effect/packages/effect/src/TPubSub.ts new file mode 100644 index 0000000..f6b7b04 --- /dev/null +++ b/repos/effect/packages/effect/src/TPubSub.ts @@ -0,0 +1,200 @@ +/** + * @since 2.0.0 + */ +import type * as Effect from "./Effect.js" +import type * as HashSet from "./HashSet.js" +import * as internal from "./internal/stm/tPubSub.js" +import type * as tQueue from "./internal/stm/tQueue.js" +import type * as Scope from "./Scope.js" +import type * as STM from "./STM.js" +import type * as TQueue from "./TQueue.js" +import type * as TRef from "./TRef.js" +import type * as Types from "./Types.js" + +/** + * @since 2.0.0 + * @category symbols + */ +export const TPubSubTypeId: unique symbol = internal.TPubSubTypeId + +/** + * @since 2.0.0 + * @category symbols + */ +export type TPubSubTypeId = typeof TPubSubTypeId + +/** + * @since 2.0.0 + * @category models + */ +export interface TPubSub extends TQueue.TEnqueue { + readonly [TPubSubTypeId]: { + readonly _A: Types.Invariant + } +} +/** + * @internal + * @since 2.0.0 + */ +export interface TPubSub { + /** @internal */ + readonly pubsubSize: TRef.TRef + /** @internal */ + readonly publisherHead: TRef.TRef | undefined>> + /** @internal */ + readonly publisherTail: TRef.TRef | undefined> | undefined> + /** @internal */ + readonly requestedCapacity: number + /** @internal */ + readonly strategy: tQueue.TQueueStrategy + /** @internal */ + readonly subscriberCount: TRef.TRef + /** @internal */ + readonly subscribers: TRef.TRef> | undefined>>> +} + +/** + * Waits until the `TPubSub` is shutdown. The `STM` returned by this method will + * not resume until the queue has been shutdown. If the `TPubSub` is already + * shutdown, the `STM` will resume right away. + * + * @since 2.0.0 + * @category mutations + */ +export const awaitShutdown: (self: TPubSub) => STM.STM = internal.awaitShutdown + +/** + * Creates a bounded `TPubSub` with the back pressure strategy. The `TPubSub` will retain + * messages until they have been taken by all subscribers, applying back + * pressure to publishers if the `TPubSub` is at capacity. + * + * @since 2.0.0 + * @category constructors + */ +export const bounded: (requestedCapacity: number) => STM.STM> = internal.bounded + +/** + * Returns the number of elements the `TPubSub` can hold. + * + * @since 2.0.0 + * @category getters + */ +export const capacity: (self: TPubSub) => number = internal.capacity + +/** + * Creates a bounded `TPubSub` with the dropping strategy. The `TPubSub` will drop new + * messages if the `TPubSub` is at capacity. + * + * @since 2.0.0 + * @category constructors + */ +export const dropping: (requestedCapacity: number) => STM.STM> = internal.dropping + +/** + * Returns `true` if the `TPubSub` contains zero elements, `false` otherwise. + * + * @since 2.0.0 + * @category getters + */ +export const isEmpty: (self: TPubSub) => STM.STM = internal.isEmpty + +/** + * Returns `true` if the `TPubSub` contains at least one element, `false` + * otherwise. + * + * @since 2.0.0 + * @category getters + */ +export const isFull: (self: TPubSub) => STM.STM = internal.isFull + +/** + * Interrupts any fibers that are suspended on `offer` or `take`. Future calls + * to `offer*` and `take*` will be interrupted immediately. + * + * @since 2.0.0 + * @category utils + */ +export const shutdown: (self: TPubSub) => STM.STM = internal.shutdown + +/** + * Returns `true` if `shutdown` has been called, otherwise returns `false`. + * + * @since 2.0.0 + * @category getters + */ +export const isShutdown: (self: TPubSub) => STM.STM = internal.isShutdown + +/** + * Publishes a message to the `TPubSub`, returning whether the message was published + * to the `TPubSub`. + * + * @since 2.0.0 + * @category mutations + */ +export const publish: { + (value: A): (self: TPubSub) => STM.STM + (self: TPubSub, value: A): STM.STM +} = internal.publish + +/** + * Publishes all of the specified messages to the `TPubSub`, returning whether they + * were published to the `TPubSub`. + * + * @since 2.0.0 + * @category mutations + */ +export const publishAll: { + (iterable: Iterable): (self: TPubSub) => STM.STM + (self: TPubSub, iterable: Iterable): STM.STM +} = internal.publishAll + +/** + * Retrieves the size of the `TPubSub`, which is equal to the number of elements + * in the `TPubSub`. This may be negative if fibers are suspended waiting for + * elements to be added to the `TPubSub`. + * + * @since 2.0.0 + * @category getters + */ +export const size: (self: TPubSub) => STM.STM = internal.size + +/** + * Creates a bounded `TPubSub` with the sliding strategy. The `TPubSub` will add new + * messages and drop old messages if the `TPubSub` is at capacity. + * + * For best performance use capacities that are powers of two. + * + * @since 2.0.0 + * @category constructors + */ +export const sliding: (requestedCapacity: number) => STM.STM> = internal.sliding + +/** + * Subscribes to receive messages from the `TPubSub`. The resulting subscription can + * be evaluated multiple times to take a message from the `TPubSub` each time. The + * caller is responsible for unsubscribing from the `TPubSub` by shutting down the + * queue. + * + * @since 2.0.0 + * @category mutations + */ +export const subscribe: (self: TPubSub) => STM.STM> = internal.subscribe + +/** + * Subscribes to receive messages from the `TPubSub`. The resulting subscription can + * be evaluated multiple times within the scope to take a message from the `TPubSub` + * each time. + * + * @since 2.0.0 + * @category mutations + */ +export const subscribeScoped: (self: TPubSub) => Effect.Effect, never, Scope.Scope> = + internal.subscribeScoped + +/** + * Creates an unbounded `TPubSub`. + * + * @since 2.0.0 + * @category constructors + */ +export const unbounded: () => STM.STM> = internal.unbounded diff --git a/repos/effect/packages/effect/src/TQueue.ts b/repos/effect/packages/effect/src/TQueue.ts new file mode 100644 index 0000000..e8b9b46 --- /dev/null +++ b/repos/effect/packages/effect/src/TQueue.ts @@ -0,0 +1,432 @@ +/** + * @since 2.0.0 + */ +import * as internal from "./internal/stm/tQueue.js" +import type * as Option from "./Option.js" +import type { Predicate } from "./Predicate.js" +import type * as STM from "./STM.js" +import type * as Types from "./Types.js" + +/** + * @since 2.0.0 + * @category symbols + */ +export const TDequeueTypeId: unique symbol = internal.TDequeueTypeId + +/** + * @since 2.0.0 + * @category symbols + */ +export type TDequeueTypeId = typeof TDequeueTypeId + +/** + * @since 2.0.0 + * @category symbols + */ +export const TEnqueueTypeId: unique symbol = internal.TEnqueueTypeId + +/** + * @since 2.0.0 + * @category symbols + */ +export type TEnqueueTypeId = typeof TEnqueueTypeId + +/** + * @since 2.0.0 + * @category models + */ +export interface TQueue extends TEnqueue, TDequeue {} + +/** + * @since 2.0.0 + * @category models + */ +export interface TEnqueue extends TQueue.TEnqueueVariance, BaseTQueue { + /** + * Places one value in the queue. + */ + offer(value: A): STM.STM + + /** + * For Bounded TQueue: uses the `BackPressure` Strategy, places the values in + * the queue and always returns true. If the queue has reached capacity, then + * the fiber performing the `offerAll` will be suspended until there is room + * in the queue. + * + * For Unbounded TQueue: Places all values in the queue and returns true. + * + * For Sliding TQueue: uses `Sliding` Strategy If there is room in the queue, + * it places the values otherwise it removes the old elements and enqueues the + * new ones. Always returns true. + * + * For Dropping TQueue: uses `Dropping` Strategy, It places the values in the + * queue but if there is no room it will not enqueue them and return false. + */ + offerAll(iterable: Iterable): STM.STM +} + +/** + * @since 2.0.0 + * @category models + */ +export interface TDequeue extends TQueue.TDequeueVariance, BaseTQueue { + /** + * Views the next element in the queue without removing it, retrying if the + * queue is empty. + */ + readonly peek: STM.STM + + /** + * Views the next element in the queue without removing it, returning `None` + * if the queue is empty. + */ + readonly peekOption: STM.STM> + + /** + * Takes the oldest value in the queue. If the queue is empty, this will return + * a computation that resumes when an item has been added to the queue. + */ + readonly take: STM.STM + + /** + * Takes all the values in the queue and returns the values. If the queue is + * empty returns an empty collection. + */ + readonly takeAll: STM.STM> + + /** + * Takes up to max number of values from the queue. + */ + takeUpTo(max: number): STM.STM> +} + +/** + * The base interface that all `TQueue`s must implement. + * + * @since 2.0.0 + * @category models + */ +export interface BaseTQueue { + /** + * Returns the number of elements the queue can hold. + */ + capacity(): number + + /** + * Retrieves the size of the queue, which is equal to the number of elements + * in the queue. This may be negative if fibers are suspended waiting for + * elements to be added to the queue. + */ + readonly size: STM.STM + + /** + * Returns `true` if the `TQueue` contains at least one element, `false` + * otherwise. + */ + readonly isFull: STM.STM + + /** + * Returns `true` if the `TQueue` contains zero elements, `false` otherwise. + */ + readonly isEmpty: STM.STM + + /** + * Interrupts any fibers that are suspended on `offer` or `take`. Future calls + * to `offer*` and `take*` will be interrupted immediately. + */ + readonly shutdown: STM.STM + + /** + * Returns `true` if `shutdown` has been called, otherwise returns `false`. + */ + readonly isShutdown: STM.STM + + /** + * Waits until the queue is shutdown. The `STM` returned by this method will + * not resume until the queue has been shutdown. If the queue is already + * shutdown, the `STM` will resume right away. + */ + readonly awaitShutdown: STM.STM +} + +/** + * @since 2.0.0 + */ +export declare namespace TQueue { + /** + * @since 2.0.0 + * @category models + */ + export interface TEnqueueVariance { + readonly [TEnqueueTypeId]: { + readonly _In: Types.Contravariant + } + } + + /** + * @since 2.0.0 + * @category models + */ + export interface TDequeueVariance { + readonly [TDequeueTypeId]: { + readonly _Out: Types.Covariant + } + } +} + +/** + * Returns `true` if the specified value is a `TQueue`, `false` otherwise. + * + * @since 2.0.0 + * @category refinements + */ +export const isTQueue: (u: unknown) => u is TQueue = internal.isTQueue + +/** + * Returns `true` if the specified value is a `TDequeue`, `false` otherwise. + * + * @since 2.0.0 + * @category refinements + */ +export const isTDequeue: (u: unknown) => u is TDequeue = internal.isTDequeue + +/** + * Returns `true` if the specified value is a `TEnqueue`, `false` otherwise. + * + * @since 2.0.0 + * @category refinements + */ +export const isTEnqueue: (u: unknown) => u is TEnqueue = internal.isTEnqueue + +/** + * Waits until the queue is shutdown. The `STM` returned by this method will + * not resume until the queue has been shutdown. If the queue is already + * shutdown, the `STM` will resume right away. + * + * @since 2.0.0 + * @category mutations + */ +export const awaitShutdown: (self: TDequeue | TEnqueue) => STM.STM = internal.awaitShutdown + +/** + * Creates a bounded queue with the back pressure strategy. The queue will + * retain values until they have been taken, applying back pressure to + * offerors if the queue is at capacity. + * + * For best performance use capacities that are powers of two. + * + * @since 2.0.0 + * @category constructors + */ +export const bounded: (requestedCapacity: number) => STM.STM> = internal.bounded + +/** + * Returns the number of elements the queue can hold. + * + * @since 2.0.0 + * @category getters + */ +export const capacity: (self: TDequeue | TEnqueue) => number = internal.capacity + +/** + * Creates a bounded queue with the dropping strategy. The queue will drop new + * values if the queue is at capacity. + * + * For best performance use capacities that are powers of two. + * + * @since 2.0.0 + * @category constructors + */ +export const dropping: (requestedCapacity: number) => STM.STM> = internal.dropping + +/** + * Returns `true` if the `TQueue` contains zero elements, `false` otherwise. + * + * @since 2.0.0 + * @category getters + */ +export const isEmpty: (self: TDequeue | TEnqueue) => STM.STM = internal.isEmpty + +/** + * Returns `true` if the `TQueue` contains at least one element, `false` + * otherwise. + * + * @since 2.0.0 + * @category getters + */ +export const isFull: (self: TDequeue | TEnqueue) => STM.STM = internal.isFull + +/** + * Returns `true` if `shutdown` has been called, otherwise returns `false`. + * + * @since 2.0.0 + * @category getters + */ +export const isShutdown: (self: TDequeue | TEnqueue) => STM.STM = internal.isShutdown + +/** + * Places one value in the queue. + * + * @since 2.0.0 + * @category mutations + */ +export const offer: { + (value: A): (self: TEnqueue) => STM.STM + (self: TEnqueue, value: A): STM.STM +} = internal.offer + +/** + * For Bounded TQueue: uses the `BackPressure` Strategy, places the values in + * the queue and always returns true. If the queue has reached capacity, then + * the fiber performing the `offerAll` will be suspended until there is room + * in the queue. + * + * For Unbounded TQueue: Places all values in the queue and returns true. + * + * For Sliding TQueue: uses `Sliding` Strategy If there is room in the queue, + * it places the values otherwise it removes the old elements and enqueues the + * new ones. Always returns true. + * + * For Dropping TQueue: uses `Dropping` Strategy, It places the values in the + * queue but if there is no room it will not enqueue them and return false. + * + * @since 2.0.0 + * @category mutations + */ +export const offerAll: { + (iterable: Iterable): (self: TEnqueue) => STM.STM + (self: TEnqueue, iterable: Iterable): STM.STM +} = internal.offerAll + +/** + * Views the next element in the queue without removing it, retrying if the + * queue is empty. + * + * @since 2.0.0 + * @category getters + */ +export const peek: (self: TDequeue) => STM.STM = internal.peek + +/** + * Views the next element in the queue without removing it, returning `None` + * if the queue is empty. + * + * @since 2.0.0 + * @category getters + */ +export const peekOption: (self: TDequeue) => STM.STM> = internal.peekOption + +/** + * Takes a single element from the queue, returning `None` if the queue is + * empty. + * + * @since 2.0.0 + * @category getters + */ +export const poll: (self: TDequeue) => STM.STM> = internal.poll + +/** + * Drops elements from the queue while they do not satisfy the predicate, + * taking and returning the first element that does satisfy the predicate. + * Retries if no elements satisfy the predicate. + * + * @since 2.0.0 + * @category mutations + */ +export const seek: { + (predicate: Predicate): (self: TDequeue) => STM.STM + (self: TDequeue, predicate: Predicate): STM.STM +} = internal.seek + +/** + * Interrupts any fibers that are suspended on `offer` or `take`. Future calls + * to `offer*` and `take*` will be interrupted immediately. + * + * @since 2.0.0 + * @category mutations + */ +export const shutdown: (self: TDequeue | TEnqueue) => STM.STM = internal.shutdown + +/** + * Retrieves the size of the queue, which is equal to the number of elements + * in the queue. This may be negative if fibers are suspended waiting for + * elements to be added to the queue. + * + * @since 2.0.0 + * @category getters + */ +export const size: (self: TDequeue | TEnqueue) => STM.STM = internal.size + +/** + * Creates a bounded queue with the sliding strategy. The queue will add new + * values and drop old values if the queue is at capacity. + * + * For best performance use capacities that are powers of two. + * + * @since 2.0.0 + * @category constructors + */ +export const sliding: (requestedCapacity: number) => STM.STM> = internal.sliding + +/** + * Takes the oldest value in the queue. If the queue is empty, this will return + * a computation that resumes when an item has been added to the queue. + * + * @since 2.0.0 + * @category mutations + */ +export const take: (self: TDequeue) => STM.STM = internal.take + +/** + * Takes all the values in the queue and returns the values. If the queue is + * empty returns an empty collection. + * + * @since 2.0.0 + * @category mutations + */ +export const takeAll: (self: TDequeue) => STM.STM> = internal.takeAll + +/** + * Takes a number of elements from the queue between the specified minimum and + * maximum. If there are fewer than the minimum number of elements available, + * retries until at least the minimum number of elements have been collected. + * + * @since 2.0.0 + * @category mutations + */ +export const takeBetween: { + (min: number, max: number): (self: TDequeue) => STM.STM> + (self: TDequeue, min: number, max: number): STM.STM> +} = internal.takeBetween + +/** + * Takes the specified number of elements from the queue. If there are fewer + * than the specified number of elements available, it retries until they + * become available. + * + * @since 2.0.0 + * @category mutations + */ +export const takeN: { + (n: number): (self: TDequeue) => STM.STM> + (self: TDequeue, n: number): STM.STM> +} = internal.takeN + +/** + * Takes up to max number of values from the queue. + * + * @since 2.0.0 + * @category mutations + */ +export const takeUpTo: { + (max: number): (self: TDequeue) => STM.STM> + (self: TDequeue, max: number): STM.STM> +} = internal.takeUpTo + +/** + * Creates an unbounded queue. + * + * @since 2.0.0 + * @category constructors + */ +export const unbounded: () => STM.STM> = internal.unbounded diff --git a/repos/effect/packages/effect/src/TRandom.ts b/repos/effect/packages/effect/src/TRandom.ts new file mode 100644 index 0000000..cb9e4bf --- /dev/null +++ b/repos/effect/packages/effect/src/TRandom.ts @@ -0,0 +1,129 @@ +/** + * @since 2.0.0 + */ +import type * as Context from "./Context.js" +import * as internal from "./internal/stm/tRandom.js" +import type * as Layer from "./Layer.js" +import type * as STM from "./STM.js" +import type * as TRef from "./TRef.js" +import type * as Random from "./Utils.js" + +/** + * @since 2.0.0 + * @category symbols + */ +export const TRandomTypeId: unique symbol = internal.TRandomTypeId + +/** + * @since 2.0.0 + * @category symbols + */ +export type TRandomTypeId = typeof TRandomTypeId + +/** + * @since 2.0.0 + * @category models + */ +export interface TRandom { + readonly [TRandomTypeId]: TRandomTypeId + /** + * Returns the next numeric value from the pseudo-random number generator. + */ + readonly next: STM.STM + /** + * Returns the next boolean value from the pseudo-random number generator. + */ + readonly nextBoolean: STM.STM + /** + * Returns the next integer value from the pseudo-random number generator. + */ + readonly nextInt: STM.STM + /** + * Returns the next numeric value in the specified range from the + * pseudo-random number generator. + */ + nextRange(min: number, max: number): STM.STM + /** + * Returns the next integer value in the specified range from the + * pseudo-random number generator. + */ + nextIntBetween(min: number, max: number): STM.STM + /** + * Uses the pseudo-random number generator to shuffle the specified iterable. + */ + shuffle(elements: Iterable): STM.STM> +} +/** + * @internal + * @since 2.0.0 + */ +export interface TRandom { + /** @internal */ + readonly state: TRef.TRef +} + +/** + * The service tag used to access `TRandom` in the environment of an effect. + * + * @since 2.0.0 + * @category context + */ +export const Tag: Context.Tag = internal.Tag + +/** + * The "live" `TRandom` service wrapped into a `Layer`. + * + * @since 2.0.0 + * @category context + */ +export const live: Layer.Layer = internal.live + +/** + * Returns the next number from the pseudo-random number generator. + * + * @since 2.0.0 + * @category random + */ +export const next: STM.STM = internal.next + +/** + * Returns the next boolean value from the pseudo-random number generator. + * + * @since 2.0.0 + * @category random + */ +export const nextBoolean: STM.STM = internal.nextBoolean + +/** + * Returns the next integer from the pseudo-random number generator. + * + * @since 2.0.0 + * @category random + */ +export const nextInt: STM.STM = internal.nextInt + +/** + * Returns the next integer in the specified range from the pseudo-random number + * generator. + * + * @since 2.0.0 + * @category random + */ +export const nextIntBetween: (low: number, high: number) => STM.STM = internal.nextIntBetween + +/** + * Returns the next number in the specified range from the pseudo-random number + * generator. + * + * @since 2.0.0 + * @category random + */ +export const nextRange: (min: number, max: number) => STM.STM = internal.nextRange + +/** + * Uses the pseudo-random number generator to shuffle the specified iterable. + * + * @since 2.0.0 + * @category random + */ +export const shuffle: (elements: Iterable) => STM.STM, never, TRandom> = internal.shuffle diff --git a/repos/effect/packages/effect/src/TReentrantLock.ts b/repos/effect/packages/effect/src/TReentrantLock.ts new file mode 100644 index 0000000..0bcb604 --- /dev/null +++ b/repos/effect/packages/effect/src/TReentrantLock.ts @@ -0,0 +1,224 @@ +/** + * @since 2.0.0 + */ +import type * as Effect from "./Effect.js" +import * as internal from "./internal/stm/tReentrantLock.js" +import type * as Scope from "./Scope.js" +import type * as STM from "./STM.js" +import type * as TRef from "./TRef.js" + +/** + * @since 2.0.0 + * @category symbols + */ +export const TReentrantLockTypeId: unique symbol = internal.TReentrantLockTypeId + +/** + * @since 2.0.0 + * @category symbols + */ +export type TReentrantLockTypeId = typeof TReentrantLockTypeId + +/** + * A `TReentrantLock` is a reentrant read/write lock. Multiple readers may all + * concurrently acquire read locks. Only one writer is allowed to acquire a + * write lock at any given time. Read locks may be upgraded into write locks. A + * fiber that has a write lock may acquire other write locks or read locks. + * + * The two primary methods of this structure are `readLock`, which acquires a + * read lock in a scoped context, and `writeLock`, which acquires a write lock + * in a scoped context. + * + * Although located in the STM package, there is no need for locks within STM + * transactions. However, this lock can be quite useful in effectful code, to + * provide consistent read/write access to mutable state; and being in STM + * allows this structure to be composed into more complicated concurrent + * structures that are consumed from effectful code. + * + * @since 2.0.0 + * @category models + */ +export interface TReentrantLock extends TReentrantLock.Proto {} +/** + * @internal + * @since 2.0.0 + */ +export interface TReentrantLock { + /** @internal */ + readonly state: TRef.TRef +} + +/** + * @since 2.0.0 + */ +export declare namespace TReentrantLock { + /** + * @since 2.0.0 + * @category models + */ + export interface Proto { + readonly [TReentrantLockTypeId]: TReentrantLockTypeId + } +} + +/** + * Acquires a read lock. The transaction will suspend until no other fiber is + * holding a write lock. Succeeds with the number of read locks held by this + * fiber. + * + * @since 2.0.0 + * @category mutations + */ +export const acquireRead: (self: TReentrantLock) => STM.STM = internal.acquireRead + +/** + * Acquires a write lock. The transaction will suspend until no other fibers + * are holding read or write locks. Succeeds with the number of write locks + * held by this fiber. + * + * @since 2.0.0 + * @category mutations + */ +export const acquireWrite: (self: TReentrantLock) => STM.STM = internal.acquireWrite + +/** + * Retrieves the number of acquired read locks for this fiber. + * + * @since 2.0.0 + * @category mutations + */ +export const fiberReadLocks: (self: TReentrantLock) => STM.STM = internal.fiberReadLocks + +/** + * Retrieves the number of acquired write locks for this fiber. + * + * @since 2.0.0 + * @category mutations + */ +export const fiberWriteLocks: (self: TReentrantLock) => STM.STM = internal.fiberWriteLocks + +/** + * Just a convenience method for applications that only need reentrant locks, + * without needing a distinction between readers / writers. + * + * See `TReentrantLock.writeLock`. + * + * @since 2.0.0 + * @category mutations + */ +export const lock: (self: TReentrantLock) => Effect.Effect = internal.lock + +/** + * Determines if any fiber has a read or write lock. + * + * @since 2.0.0 + * @category mutations + */ +export const locked: (self: TReentrantLock) => STM.STM = internal.locked + +/** + * Makes a new reentrant read/write lock. + * + * @since 2.0.0 + * @category constructors + */ +export const make: STM.STM = internal.make + +/** + * Obtains a read lock in a scoped context. + * + * @since 2.0.0 + * @category mutations + */ +export const readLock: (self: TReentrantLock) => Effect.Effect = internal.readLock + +/** + * Retrieves the total number of acquired read locks. + * + * @since 2.0.0 + * @category mutations + */ +export const readLocks: (self: TReentrantLock) => STM.STM = internal.readLocks + +/** + * Determines if any fiber has a read lock. + * + * @since 2.0.0 + * @category mutations + */ +export const readLocked: (self: TReentrantLock) => STM.STM = internal.readLocked + +/** + * Releases a read lock held by this fiber. Succeeds with the outstanding + * number of read locks held by this fiber. + * + * @since 2.0.0 + * @category mutations + */ +export const releaseRead: (self: TReentrantLock) => STM.STM = internal.releaseRead + +/** + * Releases a write lock held by this fiber. Succeeds with the outstanding + * number of write locks held by this fiber. + * + * @since 2.0.0 + * @category mutations + */ +export const releaseWrite: (self: TReentrantLock) => STM.STM = internal.releaseWrite + +/** + * Runs the specified workflow with a lock. + * + * @since 2.0.0 + * @category mutations + */ +export const withLock: { + (self: TReentrantLock): (effect: Effect.Effect) => Effect.Effect + (effect: Effect.Effect, self: TReentrantLock): Effect.Effect +} = internal.withLock + +/** + * Runs the specified workflow with a read lock. + * + * @since 2.0.0 + * @category mutations + */ +export const withReadLock: { + (self: TReentrantLock): (effect: Effect.Effect) => Effect.Effect + (effect: Effect.Effect, self: TReentrantLock): Effect.Effect +} = internal.withReadLock + +/** + * Runs the specified workflow with a write lock. + * + * @since 2.0.0 + * @category mutations + */ +export const withWriteLock: { + (self: TReentrantLock): (effect: Effect.Effect) => Effect.Effect + (effect: Effect.Effect, self: TReentrantLock): Effect.Effect +} = internal.withWriteLock + +/** + * Obtains a write lock in a scoped context. + * + * @since 2.0.0 + * @category mutations + */ +export const writeLock: (self: TReentrantLock) => Effect.Effect = internal.writeLock + +/** + * Determines if a write lock is held by some fiber. + * + * @since 2.0.0 + * @category mutations + */ +export const writeLocked: (self: TReentrantLock) => STM.STM = internal.writeLocked + +/** + * Computes the number of write locks held by fibers. + * + * @since 2.0.0 + * @category mutations + */ +export const writeLocks: (self: TReentrantLock) => STM.STM = internal.writeLocks diff --git a/repos/effect/packages/effect/src/TRef.ts b/repos/effect/packages/effect/src/TRef.ts new file mode 100644 index 0000000..445359a --- /dev/null +++ b/repos/effect/packages/effect/src/TRef.ts @@ -0,0 +1,178 @@ +/** + * @since 2.0.0 + */ + +import type * as Journal from "./internal/stm/journal.js" +import * as internal from "./internal/stm/tRef.js" +import type * as TxnId from "./internal/stm/txnId.js" +import type * as Versioned from "./internal/stm/versioned.js" +import type * as Option from "./Option.js" +import type { Pipeable } from "./Pipeable.js" +import type * as STM from "./STM.js" +import type * as Types from "./Types.js" + +/** + * @since 2.0.0 + * @category symbols + */ +export const TRefTypeId: unique symbol = internal.TRefTypeId + +/** + * @since 2.0.0 + * @category symbols + */ +export type TRefTypeId = typeof TRefTypeId + +/** + * A `TRef` is a purely functional description of a mutable reference that can + * be modified as part of a transactional effect. The fundamental operations of + * a `TRef` are `set` and `get`. `set` transactionally sets the reference to a + * new value. `get` gets the current value of the reference. + * + * NOTE: While `TRef` provides the transactional equivalent of a mutable + * reference, the value inside the `TRef` should be immutable. + * + * @since 2.0.0 + * @category models + */ +export interface TRef extends TRef.Variance, Pipeable { + /** + * Note: the method is unbound, exposed only for potential extensions. + */ + modify(f: (a: A) => readonly [B, A]): STM.STM +} +/** + * @internal + * @since 2.0.0 + */ +export interface TRef { + /** @internal */ + todos: Map + /** @internal */ + versioned: Versioned.Versioned +} + +/** + * @since 2.0.0 + */ +export declare namespace TRef { + /** + * @since 2.0.0 + */ + export interface Variance { + readonly [TRefTypeId]: { + readonly _A: Types.Invariant + } + } +} + +/** + * @since 2.0.0 + * @category mutations + */ +export const get: (self: TRef) => STM.STM = internal.get + +/** + * @since 2.0.0 + * @category mutations + */ +export const getAndSet: { + (value: A): (self: TRef) => STM.STM + (self: TRef, value: A): STM.STM +} = internal.getAndSet + +/** + * @since 2.0.0 + * @category mutations + */ +export const getAndUpdate: { + (f: (a: A) => A): (self: TRef) => STM.STM + (self: TRef, f: (a: A) => A): STM.STM +} = internal.getAndUpdate + +/** + * @since 2.0.0 + * @category mutations + */ +export const getAndUpdateSome: { + (f: (a: A) => Option.Option): (self: TRef) => STM.STM + (self: TRef, f: (a: A) => Option.Option): STM.STM +} = internal.getAndUpdateSome + +/** + * @since 2.0.0 + * @category constructors + */ +export const make: (value: A) => STM.STM> = internal.make + +/** + * @since 2.0.0 + * @category mutations + */ +export const modify: { + (f: (a: A) => readonly [B, A]): (self: TRef) => STM.STM + (self: TRef, f: (a: A) => readonly [B, A]): STM.STM +} = internal.modify + +/** + * @since 2.0.0 + * @category mutations + */ +export const modifySome: { + (fallback: B, f: (a: A) => Option.Option): (self: TRef) => STM.STM + (self: TRef, fallback: B, f: (a: A) => Option.Option): STM.STM +} = internal.modifySome + +/** + * @since 2.0.0 + * @category mutations + */ +export const set: { + (value: A): (self: TRef) => STM.STM + (self: TRef, value: A): STM.STM +} = internal.set + +/** + * @since 2.0.0 + * @category mutations + */ +export const setAndGet: { + (value: A): (self: TRef) => STM.STM + (self: TRef, value: A): STM.STM +} = internal.setAndGet + +/** + * @since 2.0.0 + * @category mutations + */ +export const update: { + (f: (a: A) => A): (self: TRef) => STM.STM + (self: TRef, f: (a: A) => A): STM.STM +} = internal.update + +/** + * @since 2.0.0 + * @category mutations + */ +export const updateAndGet: { + (f: (a: A) => A): (self: TRef) => STM.STM + (self: TRef, f: (a: A) => A): STM.STM +} = internal.updateAndGet + +/** + * @since 2.0.0 + * @category mutations + */ +export const updateSome: { + (f: (a: A) => Option.Option): (self: TRef) => STM.STM + (self: TRef, f: (a: A) => Option.Option): STM.STM +} = internal.updateSome + +/** + * @since 2.0.0 + * @category mutations + */ +export const updateSomeAndGet: { + (f: (a: A) => Option.Option): (self: TRef) => STM.STM + (self: TRef, f: (a: A) => Option.Option): STM.STM +} = internal.updateSomeAndGet diff --git a/repos/effect/packages/effect/src/TSemaphore.ts b/repos/effect/packages/effect/src/TSemaphore.ts new file mode 100644 index 0000000..0db52f5 --- /dev/null +++ b/repos/effect/packages/effect/src/TSemaphore.ts @@ -0,0 +1,129 @@ +/** + * @since 2.0.0 + */ + +import type * as Effect from "./Effect.js" +import * as internal from "./internal/stm/tSemaphore.js" +import type * as Scope from "./Scope.js" +import type * as STM from "./STM.js" +import type * as TRef from "./TRef.js" + +/** + * @since 2.0.0 + * @category symbols + */ +export const TSemaphoreTypeId: unique symbol = internal.TSemaphoreTypeId + +/** + * @since 2.0.0 + * @category symbols + */ +export type TSemaphoreTypeId = typeof TSemaphoreTypeId + +/** + * @since 2.0.0 + * @category models + */ +export interface TSemaphore extends TSemaphore.Proto {} +/** + * @internal + * @since 2.0.0 + */ +export interface TSemaphore { + /** @internal */ + readonly permits: TRef.TRef +} + +/** + * @since 2.0.0 + */ +export declare namespace TSemaphore { + /** + * @since 2.0.0 + * @category models + */ + export interface Proto { + readonly [TSemaphoreTypeId]: TSemaphoreTypeId + } +} + +/** + * @since 2.0.0 + * @category mutations + */ +export const acquire: (self: TSemaphore) => STM.STM = internal.acquire + +/** + * @since 2.0.0 + * @category mutations + */ +export const acquireN: { + (n: number): (self: TSemaphore) => STM.STM + (self: TSemaphore, n: number): STM.STM +} = internal.acquireN + +/** + * @since 2.0.0 + * @category getters + */ +export const available: (self: TSemaphore) => STM.STM = internal.available + +/** + * @since 2.0.0 + * @category constructors + */ +export const make: (permits: number) => STM.STM = internal.make + +/** + * @since 2.0.0 + * @category mutations + */ +export const release: (self: TSemaphore) => STM.STM = internal.release + +/** + * @since 2.0.0 + * @category mutations + */ +export const releaseN: { + (n: number): (self: TSemaphore) => STM.STM + (self: TSemaphore, n: number): STM.STM +} = internal.releaseN + +/** + * @since 2.0.0 + * @category mutations + */ +export const withPermit: { + (semaphore: TSemaphore): (self: Effect.Effect) => Effect.Effect + (self: Effect.Effect, semaphore: TSemaphore): Effect.Effect +} = internal.withPermit + +/** + * @since 2.0.0 + * @category mutations + */ +export const withPermits: { + (semaphore: TSemaphore, permits: number): (self: Effect.Effect) => Effect.Effect + (self: Effect.Effect, semaphore: TSemaphore, permits: number): Effect.Effect +} = internal.withPermits + +/** + * @since 2.0.0 + * @category mutations + */ +export const withPermitScoped: (self: TSemaphore) => Effect.Effect = internal.withPermitScoped + +/** + * @since 2.0.0 + * @category mutations + */ +export const withPermitsScoped: { + (permits: number): (self: TSemaphore) => Effect.Effect + (self: TSemaphore, permits: number): Effect.Effect +} = internal.withPermitsScoped + +/** + * @since 2.0.0 + * @category unsafe + */ +export const unsafeMake: (permits: number) => TSemaphore = internal.unsafeMakeSemaphore diff --git a/repos/effect/packages/effect/src/TSet.ts b/repos/effect/packages/effect/src/TSet.ts new file mode 100644 index 0000000..88fdbde --- /dev/null +++ b/repos/effect/packages/effect/src/TSet.ts @@ -0,0 +1,365 @@ +/** + * @since 2.0.0 + */ +import type * as Chunk from "./Chunk.js" +import type * as HashSet from "./HashSet.js" +import * as internal from "./internal/stm/tSet.js" +import type * as Option from "./Option.js" +import type { Predicate } from "./Predicate.js" +import type * as STM from "./STM.js" +import type * as TMap from "./TMap.js" +import type * as Types from "./Types.js" + +/** + * @since 2.0.0 + * @category symbols + */ +export const TSetTypeId: unique symbol = internal.TSetTypeId + +/** + * @since 2.0.0 + * @category symbols + */ +export type TSetTypeId = typeof TSetTypeId + +/** + * Transactional set implemented on top of `TMap`. + * + * @since 2.0.0 + * @category models + */ +export interface TSet extends TSet.Variance {} +/** + * @internal + * @since 2.0.0 + */ +export interface TSet { + /** @internal */ + readonly tMap: TMap.TMap +} + +/** + * @since 2.0.0 + */ +export declare namespace TSet { + /** + * @since 2.0.0 + * @category models + */ + export interface Variance { + readonly [TSetTypeId]: { + readonly _A: Types.Invariant + } + } +} + +/** + * Stores new element in the set. + * + * @since 2.0.0 + * @category mutations + */ +export const add: { + (value: A): (self: TSet) => STM.STM + (self: TSet, value: A): STM.STM +} = internal.add + +/** + * Atomically transforms the set into the difference of itself and the + * provided set. + * + * @since 2.0.0 + * @category mutations + */ +export const difference: { + (other: TSet): (self: TSet) => STM.STM + (self: TSet, other: TSet): STM.STM +} = internal.difference + +/** + * Makes an empty `TSet`. + * + * @since 2.0.0 + * @category constructors + */ +export const empty: () => STM.STM> = internal.empty + +/** + * Atomically performs transactional-effect for each element in set. + * + * @since 2.0.0 + * @category elements + */ +export const forEach: { + (f: (value: A) => STM.STM): (self: TSet) => STM.STM + (self: TSet, f: (value: A) => STM.STM): STM.STM +} = internal.forEach + +/** + * Creates a new `TSet` from an iterable collection of values. + * + * @since 2.0.0 + * @category constructors + */ +export const fromIterable: (iterable: Iterable) => STM.STM> = internal.fromIterable + +/** + * Tests whether or not set contains an element. + * + * @since 2.0.0 + * @category elements + */ +export const has: { + (value: A): (self: TSet) => STM.STM + (self: TSet, value: A): STM.STM +} = internal.has + +/** + * Atomically transforms the set into the intersection of itself and the + * provided set. + * + * @since 2.0.0 + * @category mutations + */ +export const intersection: { + (other: TSet): (self: TSet) => STM.STM + (self: TSet, other: TSet): STM.STM +} = internal.intersection + +/** + * Tests if the set is empty or not + * + * @since 2.0.0 + * @category getters + */ +export const isEmpty: (self: TSet) => STM.STM = internal.isEmpty + +/** + * Makes a new `TSet` that is initialized with specified values. + * + * @since 2.0.0 + * @category constructors + */ +export const make: >( + ...elements: Elements +) => STM.STM> = internal.make + +/** + * Atomically folds using a pure function. + * + * @since 2.0.0 + * @category folding + */ +export const reduce: { + (zero: Z, f: (accumulator: Z, value: A) => Z): (self: TSet) => STM.STM + (self: TSet, zero: Z, f: (accumulator: Z, value: A) => Z): STM.STM +} = internal.reduce + +/** + * Atomically folds using a transactional function. + * + * @since 2.0.0 + * @category folding + */ +export const reduceSTM: { + (zero: Z, f: (accumulator: Z, value: A) => STM.STM): (self: TSet) => STM.STM + (self: TSet, zero: Z, f: (accumulator: Z, value: A) => STM.STM): STM.STM +} = internal.reduceSTM + +/** + * Removes a single element from the set. + * + * @since 2.0.0 + * @category mutations + */ +export const remove: { + (value: A): (self: TSet) => STM.STM + (self: TSet, value: A): STM.STM +} = internal.remove + +/** + * Removes elements from the set. + * + * @since 2.0.0 + * @category mutations + */ +export const removeAll: { + (iterable: Iterable): (self: TSet) => STM.STM + (self: TSet, iterable: Iterable): STM.STM +} = internal.removeAll + +/** + * Removes entries from a `TSet` that satisfy the specified predicate and returns the removed entries + * (or `void` if `discard = true`). + * + * @since 2.0.0 + * @category mutations + */ +export const removeIf: { + (predicate: Predicate, options: { + readonly discard: true + }): (self: TSet) => STM.STM + ( + predicate: Predicate, + options?: { + readonly discard: false + } + ): (self: TSet) => STM.STM> + (self: TSet, predicate: Predicate, options: { + readonly discard: true + }): STM.STM + ( + self: TSet, + predicate: Predicate, + options?: { + readonly discard: false + } + ): STM.STM> +} = internal.removeIf + +/** + * Retains entries in a `TSet` that satisfy the specified predicate and returns the removed entries + * (or `void` if `discard = true`). + * + * @since 2.0.0 + * @category mutations + */ +export const retainIf: { + (predicate: Predicate, options: { + readonly discard: true + }): (self: TSet) => STM.STM + ( + predicate: Predicate, + options?: { + readonly discard: false + } + ): (self: TSet) => STM.STM> + (self: TSet, predicate: Predicate, options: { + readonly discard: true + }): STM.STM + ( + self: TSet, + predicate: Predicate, + options?: { + readonly discard: false + } + ): STM.STM> +} = internal.retainIf + +/** + * Returns the set's cardinality. + * + * @since 2.0.0 + * @category getters + */ +export const size: (self: TSet) => STM.STM = internal.size + +/** + * Takes the first matching value, or retries until there is one. + * + * @since 2.0.0 + * @category mutations + */ +export const takeFirst: { + (pf: (a: A) => Option.Option): (self: TSet) => STM.STM + (self: TSet, pf: (a: A) => Option.Option): STM.STM +} = internal.takeFirst + +/** + * Takes the first matching value, or retries until there is one. + * + * @since 2.0.0 + * @category mutations + */ +export const takeFirstSTM: { + (pf: (a: A) => STM.STM, R>): (self: TSet) => STM.STM + (self: TSet, pf: (a: A) => STM.STM, R>): STM.STM +} = internal.takeFirstSTM + +/** + * Takes all matching values, or retries until there is at least one. + * + * @since 2.0.0 + * @category mutations + */ +export const takeSome: { + (pf: (a: A) => Option.Option): (self: TSet) => STM.STM<[B, ...Array]> + (self: TSet, pf: (a: A) => Option.Option): STM.STM<[B, ...Array]> +} = internal.takeSome + +/** + * Takes all matching values, or retries until there is at least one. + * + * @since 2.0.0 + * @category mutations + */ +export const takeSomeSTM: { + (pf: (a: A) => STM.STM, R>): (self: TSet) => STM.STM<[B, ...Array], E, R> + (self: TSet, pf: (a: A) => STM.STM, R>): STM.STM<[B, ...Array], E, R> +} = internal.takeSomeSTM + +/** + * Collects all elements into a `Chunk`. + * + * @since 2.0.0 + * @category destructors + */ +export const toChunk: (self: TSet) => STM.STM> = internal.toChunk + +/** + * Collects all elements into a `HashSet`. + * + * @since 2.0.0 + * @category destructors + */ +export const toHashSet: (self: TSet) => STM.STM> = internal.toHashSet + +/** + * Collects all elements into a `Array`. + * + * @since 2.0.0 + * @category destructors + */ +export const toArray: (self: TSet) => STM.STM> = internal.toArray + +/** + * Collects all elements into a `ReadonlySet`. + * + * @since 2.0.0 + * @category destructors + */ +export const toReadonlySet: (self: TSet) => STM.STM> = internal.toReadonlySet + +/** + * Atomically updates all elements using a pure function. + * + * @since 2.0.0 + * @category mutations + */ +export const transform: { + (f: (a: A) => A): (self: TSet) => STM.STM + (self: TSet, f: (a: A) => A): STM.STM +} = internal.transform + +/** + * Atomically updates all elements using a transactional function. + * + * @since 2.0.0 + * @category mutations + */ +export const transformSTM: { + (f: (a: A) => STM.STM): (self: TSet) => STM.STM + (self: TSet, f: (a: A) => STM.STM): STM.STM +} = internal.transformSTM + +/** + * Atomically transforms the set into the union of itself and the provided + * set. + * + * @since 2.0.0 + * @category mutations + */ +export const union: { + (other: TSet): (self: TSet) => STM.STM + (self: TSet, other: TSet): STM.STM +} = internal.union diff --git a/repos/effect/packages/effect/src/TSubscriptionRef.ts b/repos/effect/packages/effect/src/TSubscriptionRef.ts new file mode 100644 index 0000000..dfc6ccb --- /dev/null +++ b/repos/effect/packages/effect/src/TSubscriptionRef.ts @@ -0,0 +1,192 @@ +/** + * @since 3.10.0 + */ +import type * as Effect from "./Effect.js" +import * as internal from "./internal/stm/tSubscriptionRef.js" +import type * as Option from "./Option.js" +import type * as Scope from "./Scope.js" +import type * as STM from "./STM.js" +import type * as Stream from "./Stream.js" +import type * as TPubSub from "./TPubSub.js" +import type * as TQueue from "./TQueue.js" +import type * as TRef from "./TRef.js" +import type * as Types from "./Types.js" + +/** + * @since 3.10.0 + * @category symbols + */ +export const TSubscriptionRefTypeId: unique symbol = internal.TSubscriptionRefTypeId + +/** + * @since 3.10.0 + * @category symbols + */ +export type TSubscriptionRefTypeId = typeof TSubscriptionRefTypeId + +/** + * A `TSubscriptionRef` is a `TRef` that can be subscribed to in order to + * receive a `TDequeue` of the current value and all committed changes to the value. + * + * @since 3.10.0 + * @category models + */ +export interface TSubscriptionRef extends TSubscriptionRef.Variance, TRef.TRef { + /** @internal */ + readonly ref: TRef.TRef + /** @internal */ + readonly pubsub: TPubSub.TPubSub + /** @internal */ + modify(f: (a: A) => readonly [B, A]): STM.STM + + /** + * A TDequeue containing the current value of the `Ref` as well as all changes + * to that value. + */ + readonly changes: STM.STM> +} + +/** + * @since 3.10.0 + */ +export declare namespace TSubscriptionRef { + /** + * @since 3.10.0 + * @category models + */ + export interface Variance { + readonly [TSubscriptionRefTypeId]: { + readonly _A: Types.Invariant + } + } +} + +/** + * @since 3.10.0 + * @category mutations + */ +export const get: (self: TSubscriptionRef) => STM.STM = internal.get + +/** + * @since 3.10.0 + * @category mutations + */ +export const getAndSet: { + (value: A): (self: TSubscriptionRef) => STM.STM + (self: TSubscriptionRef, value: A): STM.STM +} = internal.getAndSet + +/** + * @since 3.10.0 + * @category mutations + */ +export const getAndUpdate: { + (f: (a: A) => A): (self: TSubscriptionRef) => STM.STM + (self: TSubscriptionRef, f: (a: A) => A): STM.STM +} = internal.getAndUpdate + +/** + * @since 3.10.0 + * @category mutations + */ +export const getAndUpdateSome: { + (f: (a: A) => Option.Option): (self: TSubscriptionRef) => STM.STM + (self: TSubscriptionRef, f: (a: A) => Option.Option): STM.STM +} = internal.getAndUpdateSome + +/** + * @since 3.10.0 + * @category constructors + */ +export const make: (value: A) => STM.STM> = internal.make + +/** + * @since 3.10.0 + * @category mutations + */ +export const modify: { + (f: (a: A) => readonly [B, A]): (self: TSubscriptionRef) => STM.STM + (self: TSubscriptionRef, f: (a: A) => readonly [B, A]): STM.STM +} = internal.modify + +/** + * @since 3.10.0 + * @category mutations + */ +export const modifySome: { + (fallback: B, f: (a: A) => Option.Option): (self: TSubscriptionRef) => STM.STM + (self: TSubscriptionRef, fallback: B, f: (a: A) => Option.Option): STM.STM +} = internal.modifySome + +/** + * @since 3.10.0 + * @category mutations + */ +export const set: { + (value: A): (self: TSubscriptionRef) => STM.STM + (self: TSubscriptionRef, value: A): STM.STM +} = internal.set + +/** + * @since 3.10.0 + * @category mutations + */ +export const setAndGet: { + (value: A): (self: TSubscriptionRef) => STM.STM + (self: TSubscriptionRef, value: A): STM.STM +} = internal.setAndGet + +/** + * @since 3.10.0 + * @category mutations + */ +export const update: { + (f: (a: A) => A): (self: TSubscriptionRef) => STM.STM + (self: TSubscriptionRef, f: (a: A) => A): STM.STM +} = internal.update + +/** + * @since 3.10.0 + * @category mutations + */ +export const updateAndGet: { + (f: (a: A) => A): (self: TSubscriptionRef) => STM.STM + (self: TSubscriptionRef, f: (a: A) => A): STM.STM +} = internal.updateAndGet + +/** + * @since 3.10.0 + * @category mutations + */ +export const updateSome: { + (f: (a: A) => Option.Option): (self: TSubscriptionRef) => STM.STM + (self: TSubscriptionRef, f: (a: A) => Option.Option): STM.STM +} = internal.updateSome + +/** + * @since 3.10.0 + * @category mutations + */ +export const updateSomeAndGet: { + (f: (a: A) => Option.Option): (self: TSubscriptionRef) => STM.STM + (self: TSubscriptionRef, f: (a: A) => Option.Option): STM.STM +} = internal.updateSomeAndGet + +/** + * @since 3.10.0 + * @category mutations + */ +export const changesScoped: (self: TSubscriptionRef) => Effect.Effect, never, Scope.Scope> = + internal.changesScoped + +/** + * @since 3.10.0 + * @category mutations + */ +export const changesStream: (self: TSubscriptionRef) => Stream.Stream = internal.changesStream + +/** + * @since 3.10.0 + * @category mutations + */ +export const changes: (self: TSubscriptionRef) => STM.STM> = (self) => self.changes diff --git a/repos/effect/packages/effect/src/Take.ts b/repos/effect/packages/effect/src/Take.ts new file mode 100644 index 0000000..04a6a23 --- /dev/null +++ b/repos/effect/packages/effect/src/Take.ts @@ -0,0 +1,258 @@ +/** + * @since 2.0.0 + */ +import type * as Cause from "./Cause.js" +import type * as Chunk from "./Chunk.js" +import type * as Effect from "./Effect.js" +import type * as Exit from "./Exit.js" +import * as internal from "./internal/take.js" +import type * as Option from "./Option.js" +import type { Pipeable } from "./Pipeable.js" +import type * as Types from "./Types.js" + +/** + * @since 2.0.0 + * @category symbols + */ +export const TakeTypeId: unique symbol = internal.TakeTypeId + +/** + * @since 2.0.0 + * @category symbols + */ +export type TakeTypeId = typeof TakeTypeId + +/** + * A `Take` represents a single `take` from a queue modeling a stream of + * values. A `Take` may be a failure cause `Cause`, a chunk value `Chunk`, + * or an end-of-stream marker. + * + * @since 2.0.0 + * @category models + */ +export interface Take extends Take.Variance, Pipeable { + /** @internal */ + readonly exit: Exit.Exit, Option.Option> +} + +/** + * @since 2.0.0 + */ +export declare namespace Take { + /** + * @since 2.0.0 + * @category models + */ + export interface Variance { + readonly [TakeTypeId]: { + readonly _A: Types.Covariant + readonly _E: Types.Covariant + } + } +} + +/** + * Creates a `Take` with the specified chunk. + * + * @since 2.0.0 + * @category constructors + */ +export const chunk: (chunk: Chunk.Chunk) => Take = internal.chunk + +/** + * Creates a failing `Take` with the specified defect. + * + * @since 2.0.0 + * @category constructors + */ +export const die: (defect: unknown) => Take = internal.die + +/** + * Creates a failing `Take` with the specified error message. + * + * @since 2.0.0 + * @category constructors + */ +export const dieMessage: (message: string) => Take = internal.dieMessage + +/** + * Transforms a `Take` to an `Effect`. + * + * @since 2.0.0 + * @category destructors + */ +export const done: (self: Take) => Effect.Effect, Option.Option> = internal.done + +/** + * Represents the end-of-stream marker. + * + * @since 2.0.0 + * @category constructors + */ +export const end: Take = internal.end + +/** + * Creates a failing `Take` with the specified error. + * + * @since 2.0.0 + * @category constructors + */ +export const fail: (error: E) => Take = internal.fail + +/** + * Creates a failing `Take` with the specified cause. + * + * @since 2.0.0 + * @category constructors + */ +export const failCause: (cause: Cause.Cause) => Take = internal.failCause + +/** + * Creates an effect from `Effect` that does not fail, but succeeds with + * the `Take`. Error from stream when pulling is converted to + * `Take.failCause`. Creates a single value chunk. + * + * @since 2.0.0 + * @category constructors + */ +export const fromEffect: (effect: Effect.Effect) => Effect.Effect, never, R> = + internal.fromEffect + +/** + * Creates a `Take` from an `Exit`. + * + * @since 2.0.0 + * @category constructors + */ +export const fromExit: (exit: Exit.Exit) => Take = internal.fromExit + +/** + * Creates effect from `Effect, Option, R>` that does not fail, but + * succeeds with the `Take`. Errors from stream when pulling are converted + * to `Take.failCause`, and the end-of-stream is converted to `Take.end`. + * + * @since 2.0.0 + * @category constructors + */ +export const fromPull: ( + pull: Effect.Effect, Option.Option, R> +) => Effect.Effect, never, R> = internal.fromPull + +/** + * Checks if this `take` is done (`Take.end`). + * + * @since 2.0.0 + * @category getters + */ +export const isDone: (self: Take) => boolean = internal.isDone + +/** + * Checks if this `take` is a failure. + * + * @since 2.0.0 + * @category getters + */ +export const isFailure: (self: Take) => boolean = internal.isFailure + +/** + * Checks if this `take` is a success. + * + * @since 2.0.0 + * @category getters + */ +export const isSuccess: (self: Take) => boolean = internal.isSuccess + +/** + * Constructs a `Take`. + * + * @since 2.0.0 + * @category constructors + */ +export const make: (exit: Exit.Exit, Option.Option>) => Take = internal.make + +/** + * Transforms `Take` to `Take` by applying function `f`. + * + * @since 2.0.0 + * @category mapping + */ +export const map: { + (f: (a: A) => B): (self: Take) => Take + (self: Take, f: (a: A) => B): Take +} = internal.map + +/** + * Folds over the failure cause, success value and end-of-stream marker to + * yield a value. + * + * @since 2.0.0 + * @category destructors + */ +export const match: { + ( + options: { + readonly onEnd: () => Z + readonly onFailure: (cause: Cause.Cause) => Z2 + readonly onSuccess: (chunk: Chunk.Chunk) => Z3 + } + ): (self: Take) => Z | Z2 | Z3 + ( + self: Take, + options: { + readonly onEnd: () => Z + readonly onFailure: (cause: Cause.Cause) => Z2 + readonly onSuccess: (chunk: Chunk.Chunk) => Z3 + } + ): Z | Z2 | Z3 +} = internal.match + +/** + * Effectful version of `Take.fold`. + * + * Folds over the failure cause, success value and end-of-stream marker to + * yield an effect. + * + * @since 2.0.0 + * @category destructors + */ +export const matchEffect: { + ( + options: { + readonly onEnd: Effect.Effect + readonly onFailure: (cause: Cause.Cause) => Effect.Effect + readonly onSuccess: (chunk: Chunk.Chunk) => Effect.Effect + } + ): (self: Take) => Effect.Effect + ( + self: Take, + options: { + readonly onEnd: Effect.Effect + readonly onFailure: (cause: Cause.Cause) => Effect.Effect + readonly onSuccess: (chunk: Chunk.Chunk) => Effect.Effect + } + ): Effect.Effect +} = internal.matchEffect + +/** + * Creates a `Take` with a single value chunk. + * + * @since 2.0.0 + * @category constructors + */ +export const of: (value: A) => Take = internal.of + +/** + * Returns an effect that effectfully "peeks" at the success of this take. + * + * @since 2.0.0 + * @category sequencing + */ +export const tap: { + ( + f: (chunk: Chunk.Chunk) => Effect.Effect + ): (self: Take) => Effect.Effect + ( + self: Take, + f: (chunk: Chunk.Chunk) => Effect.Effect + ): Effect.Effect +} = internal.tap diff --git a/repos/effect/packages/effect/src/TestAnnotation.ts b/repos/effect/packages/effect/src/TestAnnotation.ts new file mode 100644 index 0000000..76be7f6 --- /dev/null +++ b/repos/effect/packages/effect/src/TestAnnotation.ts @@ -0,0 +1,158 @@ +/** + * @since 2.0.0 + */ +import * as Chunk from "./Chunk.js" +import * as Either from "./Either.js" +import * as Equal from "./Equal.js" +import type * as Fiber from "./Fiber.js" +import { pipe } from "./Function.js" +import * as Hash from "./Hash.js" +import * as HashSet from "./HashSet.js" +import { getBugErrorMessage } from "./internal/errors.js" +import type * as MutableRef from "./MutableRef.js" +import { hasProperty } from "./Predicate.js" +import type * as SortedSet from "./SortedSet.js" +import type * as Types from "./Types.js" + +/** @internal */ +const TestAnnotationSymbolKey = "effect/TestAnnotation" + +/** + * @since 2.0.0 + */ +export const TestAnnotationTypeId: unique symbol = Symbol.for(TestAnnotationSymbolKey) + +/** + * @since 2.0.0 + */ +export type TestAnnotationTypeId = typeof TestAnnotationTypeId + +/** + * @since 2.0.0 + */ +export interface TestAnnotation extends Equal.Equal { + readonly [TestAnnotationTypeId]: { + readonly _A: Types.Invariant + } + readonly identifier: string + readonly initial: A + combine(a: A, b: A): A +} + +/** @internal */ +class TestAnnotationImpl implements Equal.Equal { + readonly [TestAnnotationTypeId] = { + _A: (_: any) => _ + } + constructor( + readonly identifier: string, + readonly initial: A, + readonly combine: (a: A, b: A) => A + ) {} + [Hash.symbol](): number { + return pipe( + Hash.hash(TestAnnotationSymbolKey), + Hash.combine(Hash.hash(this.identifier)), + Hash.cached(this) + ) + } + [Equal.symbol](that: unknown): boolean { + return isTestAnnotation(that) && + this.identifier === that.identifier + } +} + +/** + * @since 2.0.0 + */ +export const isTestAnnotation = (u: unknown): u is TestAnnotation => hasProperty(u, TestAnnotationTypeId) + +/** + * @since 2.0.0 + */ +export const make = ( + identifier: string, + initial: A, + combine: (a: A, b: A) => A +): TestAnnotation => { + return new TestAnnotationImpl(identifier, initial, combine) +} + +/** + * @since 2.0.0 + */ +export const compose = ( + left: Either.Either, number>, + right: Either.Either, number> +): Either.Either, number> => { + if (Either.isLeft(left) && Either.isLeft(right)) { + return Either.left(left.left + right.left) + } + if (Either.isRight(left) && Either.isRight(right)) { + return Either.right(pipe(left.right, Chunk.appendAll(right.right))) + } + if (Either.isRight(left) && Either.isLeft(right)) { + return right + } + if (Either.isLeft(left) && Either.isRight(right)) { + return right + } + throw new Error(getBugErrorMessage("TestAnnotation.compose")) +} + +/** + * @since 2.0.0 + */ +export const fibers: TestAnnotation< + Either.Either>>>, number> +> = make< + Either.Either>>>, number> +>( + "fibers", + Either.left(0), + compose +) + +/** + * An annotation which counts ignored tests. + * + * @since 2.0.0 + */ +export const ignored: TestAnnotation = make( + "ignored", + 0, + (a, b) => a + b +) + +/** + * An annotation which counts repeated tests. + * + * @since 2.0.0 + */ +export const repeated: TestAnnotation = make( + "repeated", + 0, + (a, b) => a + b +) + +/** + * An annotation which counts retried tests. + * + * @since 2.0.0 + */ +export const retried: TestAnnotation = make( + "retried", + 0, + (a, b) => a + b +) + +/** + * An annotation which tags tests with strings. + * + * @since 2.0.0 + */ +export const tagged: TestAnnotation> = make( + "tagged", + HashSet.empty(), + (a, b) => pipe(a, HashSet.union(b)) +) diff --git a/repos/effect/packages/effect/src/TestAnnotationMap.ts b/repos/effect/packages/effect/src/TestAnnotationMap.ts new file mode 100644 index 0000000..3bcac97 --- /dev/null +++ b/repos/effect/packages/effect/src/TestAnnotationMap.ts @@ -0,0 +1,119 @@ +/** + * @since 2.0.0 + */ +import { dual } from "./Function.js" +import * as HashMap from "./HashMap.js" +import { hasProperty } from "./Predicate.js" +import type * as TestAnnotation from "./TestAnnotation.js" + +/** + * @since 2.0.0 + */ +export const TestAnnotationMapTypeId: unique symbol = Symbol.for("effect/TestAnnotationMap") + +/** + * @since 2.0.0 + */ +export type TestAnnotationMapTypeId = typeof TestAnnotationMapTypeId + +/** + * An annotation map keeps track of annotations of different types. + * + * @since 2.0.0 + */ +export interface TestAnnotationMap { + readonly [TestAnnotationMapTypeId]: TestAnnotationMapTypeId + /** @internal */ + readonly map: HashMap.HashMap, any> +} + +/** @internal */ +class TestAnnotationMapImpl implements TestAnnotationMap { + readonly [TestAnnotationMapTypeId]: TestAnnotationMapTypeId = TestAnnotationMapTypeId + constructor(readonly map: HashMap.HashMap, any>) { + } +} + +/** + * @since 2.0.0 + */ +export const isTestAnnotationMap = (u: unknown): u is TestAnnotationMap => hasProperty(u, TestAnnotationMapTypeId) + +/** + * @since 2.0.0 + */ +export const empty: (_: void) => TestAnnotationMap = () => new TestAnnotationMapImpl(HashMap.empty()) + +/** + * @since 2.0.0 + */ +export const make = (map: HashMap.HashMap, any>): TestAnnotationMap => { + return new TestAnnotationMapImpl(map) +} + +/** + * @since 2.0.0 + */ +export const overwrite = dual< + (key: TestAnnotation.TestAnnotation, value: A) => (self: TestAnnotationMap) => TestAnnotationMap, + (self: TestAnnotationMap, key: TestAnnotation.TestAnnotation, value: A) => TestAnnotationMap +>(3, (self, key, value) => make(HashMap.set(self.map, key, value))) + +/** + * @since 2.0.0 + */ +export const update = dual< + (key: TestAnnotation.TestAnnotation, f: (value: A) => A) => (self: TestAnnotationMap) => TestAnnotationMap, + (self: TestAnnotationMap, key: TestAnnotation.TestAnnotation, f: (value: A) => A) => TestAnnotationMap +>(3, (self: TestAnnotationMap, key: TestAnnotation.TestAnnotation, f: (value: A) => A) => { + let value = key.initial + if (HashMap.has(self.map, key)) { + value = HashMap.unsafeGet(self.map, key) as A + } + return overwrite(self, key, f(value)) +}) + +/** + * Retrieves the annotation of the specified type, or its default value if + * there is none. + * + * @since 2.0.0 + */ +export const get = dual< + (key: TestAnnotation.TestAnnotation) => (self: TestAnnotationMap) => A, + (self: TestAnnotationMap, key: TestAnnotation.TestAnnotation) => A +>(2, (self: TestAnnotationMap, key: TestAnnotation.TestAnnotation) => { + if (HashMap.has(self.map, key)) { + return HashMap.unsafeGet(self.map, key) as A + } + return key.initial +}) + +/** + * Appends the specified annotation to the annotation map. + * + * @since 2.0.0 + */ +export const annotate = dual< + (key: TestAnnotation.TestAnnotation, value: A) => (self: TestAnnotationMap) => TestAnnotationMap, + (self: TestAnnotationMap, key: TestAnnotation.TestAnnotation, value: A) => TestAnnotationMap +>(3, (self, key, value) => update(self, key, (_) => key.combine(_, value))) + +/** + * @since 2.0.0 + */ +export const combine = dual< + (that: TestAnnotationMap) => (self: TestAnnotationMap) => TestAnnotationMap, + (self: TestAnnotationMap, that: TestAnnotationMap) => TestAnnotationMap +>(2, (self, that) => { + let result = self.map + for (const entry of that.map) { + if (HashMap.has(result, entry[0])) { + const value = HashMap.get(result, entry[0])! + result = HashMap.set(result, entry[0], entry[0].combine(value, entry[1])) + } else { + result = HashMap.set(result, entry[0], entry[1]) + } + } + return make(result) +}) diff --git a/repos/effect/packages/effect/src/TestAnnotations.ts b/repos/effect/packages/effect/src/TestAnnotations.ts new file mode 100644 index 0000000..745ca7c --- /dev/null +++ b/repos/effect/packages/effect/src/TestAnnotations.ts @@ -0,0 +1,117 @@ +/** + * @since 2.0.0 + */ +import * as RA from "./Array.js" +import * as Context from "./Context.js" +import type * as Effect from "./Effect.js" +import * as Equal from "./Equal.js" +import type * as Fiber from "./Fiber.js" +import { pipe } from "./Function.js" +import * as effect from "./internal/core-effect.js" +import * as core from "./internal/core.js" +import * as fiber from "./internal/fiber.js" +import * as MutableRef from "./MutableRef.js" +import { hasProperty } from "./Predicate.js" +import * as Ref from "./Ref.js" +import * as SortedSet from "./SortedSet.js" +import * as TestAnnotation from "./TestAnnotation.js" +import * as TestAnnotationMap from "./TestAnnotationMap.js" + +/** + * @since 2.0.0 + */ +export const TestAnnotationsTypeId: unique symbol = Symbol.for("effect/TestAnnotations") + +/** + * @since 2.0.0 + */ +export type TestAnnotationsTypeId = typeof TestAnnotationsTypeId + +/** + * The `Annotations` trait provides access to an annotation map that tests can + * add arbitrary annotations to. Each annotation consists of a string + * identifier, an initial value, and a function for combining two values. + * Annotations form monoids and you can think of `Annotations` as a more + * structured logging service or as a super polymorphic version of the writer + * monad effect. + * + * @since 2.0.0 + */ +export interface TestAnnotations { + readonly [TestAnnotationsTypeId]: TestAnnotationsTypeId + + /** + * A ref containing the bacnking map for all annotations + */ + readonly ref: Ref.Ref + + /** + * Accesses an `Annotations` instance in the context and retrieves the + * annotation of the specified type, or its default value if there is none. + */ + get(key: TestAnnotation.TestAnnotation): Effect.Effect + + /** + * Accesses an `Annotations` instance in the context and appends the + * specified annotation to the annotation map. + */ + annotate(key: TestAnnotation.TestAnnotation, value: A): Effect.Effect + + /** + * Returns the set of all fibers in this test. + */ + readonly supervisedFibers: Effect.Effect< + SortedSet.SortedSet> + > +} + +/** @internal */ +class AnnotationsImpl implements TestAnnotations { + readonly [TestAnnotationsTypeId]: TestAnnotationsTypeId = TestAnnotationsTypeId + constructor(readonly ref: Ref.Ref) { + } + get(key: TestAnnotation.TestAnnotation): Effect.Effect { + return core.map(Ref.get(this.ref), TestAnnotationMap.get(key)) + } + annotate(key: TestAnnotation.TestAnnotation, value: A): Effect.Effect { + return Ref.update(this.ref, TestAnnotationMap.annotate(key, value)) + } + get supervisedFibers(): Effect.Effect>> { + return effect.descriptorWith((descriptor) => + core.flatMap(this.get(TestAnnotation.fibers), (either) => { + switch (either._tag) { + case "Left": { + return core.succeed(SortedSet.empty(fiber.Order)) + } + case "Right": { + return pipe( + either.right, + core.forEachSequential((ref) => core.sync(() => MutableRef.get(ref))), + core.map(RA.reduce(SortedSet.empty(fiber.Order), (a, b) => SortedSet.union(a, b))), + core.map(SortedSet.filter((fiber) => !Equal.equals(fiber.id(), descriptor.id))) + ) + } + } + }) + ) + } +} + +/** + * @since 2.0.0 + */ +export const TestAnnotations: Context.Tag = Context.GenericTag( + "effect/Annotations" +) + +/** + * @since 2.0.0 + */ +export const isTestAnnotations = (u: unknown): u is TestAnnotations => hasProperty(u, TestAnnotationsTypeId) + +/** + * @since 2.0.0 + */ +export const make = ( + ref: Ref.Ref +): TestAnnotations => new AnnotationsImpl(ref) diff --git a/repos/effect/packages/effect/src/TestClock.ts b/repos/effect/packages/effect/src/TestClock.ts new file mode 100644 index 0000000..81bf45e --- /dev/null +++ b/repos/effect/packages/effect/src/TestClock.ts @@ -0,0 +1,556 @@ +/** + * @since 2.0.0 + */ +import * as Chunk from "./Chunk.js" +import type * as Clock from "./Clock.js" +import * as Context from "./Context.js" +import * as DateTime from "./DateTime.js" +import type * as Deferred from "./Deferred.js" +import * as Duration from "./Duration.js" +import type * as Effect from "./Effect.js" +import * as Equal from "./Equal.js" +import type * as Fiber from "./Fiber.js" +import type * as FiberId from "./FiberId.js" +import * as FiberStatus from "./FiberStatus.js" +import { constVoid, dual, identity, pipe } from "./Function.js" +import * as HashMap from "./HashMap.js" +import * as clock from "./internal/clock.js" +import * as effect from "./internal/core-effect.js" +import * as core from "./internal/core.js" +import * as defaultServices from "./internal/defaultServices.js" +import * as circular from "./internal/effect/circular.js" +import * as fiberRuntime from "./internal/fiberRuntime.js" +import * as layer from "./internal/layer.js" +import * as ref from "./internal/ref.js" +import * as synchronized from "./internal/synchronizedRef.js" +import * as SuspendedWarningData from "./internal/testing/suspendedWarningData.js" +import * as WarningData from "./internal/testing/warningData.js" +import type * as Layer from "./Layer.js" +import * as number from "./Number.js" +import * as Option from "./Option.js" +import * as Order from "./Order.js" +import type * as Ref from "./Ref.js" +import type * as SortedSet from "./SortedSet.js" +import type * as Synchronized from "./SynchronizedRef.js" +import * as Annotations from "./TestAnnotations.js" +import * as Live from "./TestLive.js" + +/** + * A `TestClock` makes it easy to deterministically and efficiently test effects + * involving the passage of time. + * + * Instead of waiting for actual time to pass, `sleep` and methods implemented + * in terms of it schedule effects to take place at a given clock time. Users + * can adjust the clock time using the `adjust` and `setTime` methods, and all + * effects scheduled to take place on or before that time will automatically be + * run in order. + * + * For example, here is how we can test `Effect.timeout` using `TestClock`: + * + * ```ts + * import * as assert from "node:assert" + * import { Duration, Effect, Fiber, TestClock, Option, pipe } from "effect" + * + * Effect.gen(function*() { + * const fiber = yield* pipe( + * Effect.sleep(Duration.minutes(5)), + * Effect.timeout(Duration.minutes(1)), + * Effect.fork + * ) + * yield* TestClock.adjust(Duration.minutes(1)) + * const result = yield* Fiber.join(fiber) + * assert.deepStrictEqual(result, Option.none()) + * }) + * ``` + * + * Note how we forked the fiber that `sleep` was invoked on. Calls to `sleep` + * and methods derived from it will semantically block until the time is set to + * on or after the time they are scheduled to run. If we didn't fork the fiber + * on which we called sleep we would never get to set the time on the line + * below. Thus, a useful pattern when using `TestClock` is to fork the effect + * being tested, then adjust the clock time, and finally verify that the + * expected effects have been performed. + * + * @since 2.0.0 + */ +export interface TestClock extends Clock.Clock { + adjust(duration: Duration.DurationInput): Effect.Effect + adjustWith(duration: Duration.DurationInput): (effect: Effect.Effect) => Effect.Effect + readonly save: Effect.Effect> + setTime(time: number): Effect.Effect + readonly sleeps: Effect.Effect> +} + +/** + * `Data` represents the state of the `TestClock`, including the clock time. + * + * @since 2.0.1 + */ +export interface Data { + readonly instant: number + readonly sleeps: Chunk.Chunk]> +} + +/** + * @since 2.0.0 + */ +export const makeData = ( + instant: number, + sleeps: Chunk.Chunk]> +): Data => ({ + instant, + sleeps +}) + +/** + * @since 2.0.0 + */ +export const TestClock: Context.Tag = Context.GenericTag("effect/TestClock") + +/** + * The warning message that will be displayed if a test is using time but is + * not advancing the `TestClock`. + * + * @internal + */ +const warning = "Warning: A test is using time, but is not advancing " + + "the test clock, which may result in the test hanging. Use TestClock.adjust to " + + "manually advance the time." + +/** + * The warning message that will be displayed if a test is advancing the clock + * but a fiber is still running. + * + * @internal + */ +const suspendedWarning = "Warning: A test is advancing the test clock, " + + "but a fiber is not suspending, which may result in the test hanging. Use " + + "TestAspect.diagnose to identity the fiber that is not suspending." + +/** @internal */ +export class TestClockImpl implements TestClock { + [clock.ClockTypeId]: Clock.ClockTypeId = clock.ClockTypeId + constructor( + readonly clockState: Ref.Ref, + readonly live: Live.TestLive, + readonly annotations: Annotations.TestAnnotations, + readonly warningState: Synchronized.SynchronizedRef, + readonly suspendedWarningState: Synchronized.SynchronizedRef + ) { + this.currentTimeMillis = core.map( + ref.get(this.clockState), + (data) => data.instant + ) + this.currentTimeNanos = core.map( + ref.get(this.clockState), + (data) => BigInt(data.instant * 1000000) + ) + } + + /** + * Unsafely returns the current time in milliseconds. + */ + unsafeCurrentTimeMillis(): number { + return ref.unsafeGet(this.clockState).instant + } + + /** + * Unsafely returns the current time in nanoseconds. + */ + unsafeCurrentTimeNanos(): bigint { + return BigInt(Math.floor(this.unsafeCurrentTimeMillis() * 1000000)) + } + + /** + * Returns the current clock time in milliseconds. + */ + currentTimeMillis: Effect.Effect + + /** + * Returns the current clock time in nanoseconds. + */ + currentTimeNanos: Effect.Effect + + /** + * Saves the `TestClock`'s current state in an effect which, when run, will + * restore the `TestClock` state to the saved state. + */ + get save(): Effect.Effect> { + return core.map(ref.get(this.clockState), (data) => ref.set(this.clockState, data)) + } + /** + * Sets the current clock time to the specified instant. Any effects that + * were scheduled to occur on or before the new time will be run in order. + */ + setTime(instant: number): Effect.Effect { + return core.zipRight(this.warningDone(), this.run(() => instant)) + } + /** + * Semantically blocks the current fiber until the clock time is equal to or + * greater than the specified duration. Once the clock time is adjusted to + * on or after the duration, the fiber will automatically be resumed. + */ + sleep(durationInput: Duration.DurationInput): Effect.Effect { + const duration = Duration.decode(durationInput) + return core.flatMap(core.deferredMake(), (deferred) => + pipe( + ref.modify(this.clockState, (data) => { + const end = data.instant + Duration.toMillis(duration) + if (end > data.instant) { + return [ + true, + makeData(data.instant, pipe(data.sleeps, Chunk.prepend([end, deferred] as const))) + ] as const + } + return [false, data] as const + }), + core.flatMap((shouldAwait) => + shouldAwait ? + pipe(this.warningStart(), core.zipRight(core.deferredAwait(deferred))) : + pipe(core.deferredSucceed(deferred, void 0), core.asVoid) + ) + )) + } + /** + * Returns a list of the times at which all queued effects are scheduled to + * resume. + */ + get sleeps(): Effect.Effect> { + return core.map( + ref.get(this.clockState), + (data) => Chunk.map(data.sleeps, (_) => _[0]) + ) + } + /** + * Increments the current clock time by the specified duration. Any effects + * that were scheduled to occur on or before the new time will be run in + * order. + */ + adjust(durationInput: Duration.DurationInput): Effect.Effect { + const duration = Duration.decode(durationInput) + return core.zipRight(this.warningDone(), this.run((n) => n + Duration.toMillis(duration))) + } + /** + * Increments the current clock time by the specified duration. Any effects + * that were scheduled to occur on or before the new time will be run in + * order. + */ + adjustWith(durationInput: Duration.DurationInput) { + const duration = Duration.decode(durationInput) + return (effect: Effect.Effect): Effect.Effect => + fiberRuntime.zipLeftOptions(effect, this.adjust(duration), { concurrent: true }) + } + /** + * Returns a set of all fibers in this test. + */ + supervisedFibers(): Effect.Effect>> { + return this.annotations.supervisedFibers + } + /** + * Captures a "snapshot" of the identifier and status of all fibers in this + * test other than the current fiber. Fails with the `void` value if any of + * these fibers are not done or suspended. Note that because we cannot + * synchronize on the status of multiple fibers at the same time this + * snapshot may not be fully consistent. + */ + freeze(): Effect.Effect, void> { + return core.flatMap(this.supervisedFibers(), (fibers) => + pipe( + fibers, + effect.reduce(HashMap.empty(), (map, fiber) => + pipe( + fiber.status, + core.flatMap((status) => { + if (FiberStatus.isDone(status)) { + return core.succeed(HashMap.set(map, fiber.id() as FiberId.FiberId, status as FiberStatus.FiberStatus)) + } + if (FiberStatus.isSuspended(status)) { + return core.succeed(HashMap.set(map, fiber.id() as FiberId.FiberId, status as FiberStatus.FiberStatus)) + } + return core.fail(void 0) + }) + )) + )) + } + /** + * Forks a fiber that will display a warning message if a test is using time + * but is not advancing the `TestClock`. + */ + warningStart(): Effect.Effect { + return synchronized.updateSomeEffect(this.warningState, (data) => + WarningData.isStart(data) ? + Option.some( + pipe( + this.live.provide( + pipe(effect.logWarning(warning), effect.delay(Duration.seconds(5))) + ), + core.interruptible, + fiberRuntime.fork, + core.map((fiber) => WarningData.pending(fiber)) + ) + ) : + Option.none()) + } + /** + * Cancels the warning message that is displayed if a test is using time but + * is not advancing the `TestClock`. + */ + warningDone(): Effect.Effect { + return synchronized.updateSomeEffect(this.warningState, (warningData) => { + if (WarningData.isStart(warningData)) { + return Option.some(core.succeed(WarningData.done)) + } + if (WarningData.isPending(warningData)) { + return Option.some(pipe(core.interruptFiber(warningData.fiber), core.as(WarningData.done))) + } + return Option.none() + }) + } + + private yieldTimer = core.async((resume) => { + const timer = setTimeout(() => { + resume(core.void) + }, 0) + return core.sync(() => clearTimeout(timer)) + }) + + /** + * Returns whether all descendants of this fiber are done or suspended. + */ + suspended(): Effect.Effect, void> { + return pipe( + this.freeze(), + core.zip(pipe(this.yieldTimer, core.zipRight(this.freeze()))), + core.flatMap(([first, last]) => + Equal.equals(first, last) ? + core.succeed(first) : + core.fail(void 0) + ) + ) + } + /** + * Polls until all descendants of this fiber are done or suspended. + */ + awaitSuspended(): Effect.Effect { + return pipe( + this.suspendedWarningStart(), + core.zipRight( + pipe( + this.suspended(), + core.zipWith( + pipe(this.yieldTimer, core.zipRight(this.suspended())), + Equal.equals + ), + effect.filterOrFail(identity, constVoid), + effect.eventually + ) + ), + core.zipRight(this.suspendedWarningDone()) + ) + } + /** + * Forks a fiber that will display a warning message if a test is advancing + * the `TestClock` but a fiber is not suspending. + */ + suspendedWarningStart(): Effect.Effect { + return synchronized.updateSomeEffect(this.suspendedWarningState, (suspendedWarningData) => { + if (SuspendedWarningData.isStart(suspendedWarningData)) { + return Option.some( + pipe( + this.live.provide( + pipe( + effect.logWarning(suspendedWarning), + core.zipRight(ref.set(this.suspendedWarningState, SuspendedWarningData.done)), + effect.delay(Duration.seconds(5)) + ) + ), + core.interruptible, + fiberRuntime.fork, + core.map((fiber) => SuspendedWarningData.pending(fiber)) + ) + ) + } + return Option.none() + }) + } + /** + * Cancels the warning message that is displayed if a test is advancing the + * `TestClock` but a fiber is not suspending. + */ + suspendedWarningDone(): Effect.Effect { + return synchronized.updateSomeEffect(this.suspendedWarningState, (suspendedWarningData) => { + if (SuspendedWarningData.isPending(suspendedWarningData)) { + return Option.some(pipe(core.interruptFiber(suspendedWarningData.fiber), core.as(SuspendedWarningData.start))) + } + return Option.none() + }) + } + /** + * Runs all effects scheduled to occur on or before the specified instant, + * which may depend on the current time, in order. + */ + run(f: (instant: number) => number): Effect.Effect { + return pipe( + this.awaitSuspended(), + core.zipRight(pipe( + ref.modify(this.clockState, (data) => { + const end = f(data.instant) + const sorted = pipe( + data.sleeps, + Chunk.sort]>( + pipe(number.Order, Order.mapInput((_) => _[0])) + ) + ) + if (Chunk.isNonEmpty(sorted)) { + const [instant, deferred] = Chunk.headNonEmpty(sorted) + if (instant <= end) { + return [ + Option.some([end, deferred] as const), + makeData(instant, Chunk.tailNonEmpty(sorted)) + ] as const + } + } + return [Option.none(), makeData(end, data.sleeps)] as const + }), + core.flatMap((option) => { + switch (option._tag) { + case "None": { + return core.void + } + case "Some": { + const [end, deferred] = option.value + return pipe( + core.deferredSucceed(deferred, void 0), + core.zipRight(core.yieldNow()), + core.zipRight(this.run(() => end)) + ) + } + } + }) + )) + ) + } +} + +/** + * @since 2.0.0 + */ +export const live = (data: Data): Layer.Layer => + layer.scoped( + TestClock, + core.gen(function*() { + const live = yield* Live.TestLive + const annotations = yield* Annotations.TestAnnotations + const clockState = yield* core.sync(() => ref.unsafeMake(data)) + const warningState = yield* circular.makeSynchronized(WarningData.start) + const suspendedWarningState = yield* circular.makeSynchronized(SuspendedWarningData.start) + const testClock = new TestClockImpl(clockState, live, annotations, warningState, suspendedWarningState) + yield* fiberRuntime.withClockScoped(testClock) + yield* fiberRuntime.addFinalizer( + () => core.zipRight(testClock.warningDone(), testClock.suspendedWarningDone()) + ) + return testClock + }) + ) + +/** + * @since 2.0.0 + */ +export const defaultTestClock: Layer.Layer = live( + makeData(new Date(0).getTime(), Chunk.empty()) +) + +/** + * Accesses a `TestClock` instance in the context and increments the time + * by the specified duration, running any actions scheduled for on or before + * the new time in order. + * + * @since 2.0.0 + */ +export const adjust = (durationInput: Duration.DurationInput): Effect.Effect => { + const duration = Duration.decode(durationInput) + return testClockWith((testClock) => testClock.adjust(duration)) +} + +/** + * @since 2.0.0 + */ +export const adjustWith = dual< + (duration: Duration.DurationInput) => (effect: Effect.Effect) => Effect.Effect, + (effect: Effect.Effect, duration: Duration.DurationInput) => Effect.Effect +>(2, (effect, durationInput) => { + const duration = Duration.decode(durationInput) + return testClockWith((testClock) => testClock.adjustWith(duration)(effect)) +}) + +/** + * Accesses a `TestClock` instance in the context and saves the clock + * state in an effect which, when run, will restore the `TestClock` to the + * saved state. + * + * @since 2.0.0 + */ +export const save = (): Effect.Effect> => testClockWith((testClock) => testClock.save) + +/** + * Accesses a `TestClock` instance in the context and sets the clock time + * to the specified `Instant` or `Date`, running any actions scheduled for on or before + * the new time in order. + * + * @since 2.0.0 + */ +export const setTime = (input: DateTime.DateTime.Input): Effect.Effect => + testClockWith((testClock) => + testClock.setTime( + typeof input === "number" + ? input + : DateTime.unsafeMake(input).epochMillis + ) + ) + +/** + * Semantically blocks the current fiber until the clock time is equal to or + * greater than the specified duration. Once the clock time is adjusted to + * on or after the duration, the fiber will automatically be resumed. + * + * @since 2.0.0 + */ +export const sleep = (durationInput: Duration.DurationInput): Effect.Effect => { + const duration = Duration.decode(durationInput) + return testClockWith((testClock) => testClock.sleep(duration)) +} + +/** + * Accesses a `TestClock` instance in the context and returns a list of + * times that effects are scheduled to run. + * + * @since 2.0.0 + */ +export const sleeps = (): Effect.Effect> => testClockWith((testClock) => testClock.sleeps) + +/** + * Retrieves the `TestClock` service for this test. + * + * @since 2.0.0 + */ +export const testClock = (): Effect.Effect => testClockWith(core.succeed) + +/** + * Retrieves the `TestClock` service for this test and uses it to run the + * specified workflow. + * + * @since 2.0.0 + */ +export const testClockWith = (f: (testClock: TestClock) => Effect.Effect): Effect.Effect => + core.fiberRefGetWith( + defaultServices.currentServices, + (services) => f(pipe(services, Context.get(clock.clockTag)) as TestClock) + ) + +/** + * Accesses the current time of a `TestClock` instance in the context in + * milliseconds. + * + * @since 2.0.0 + */ +export const currentTimeMillis: Effect.Effect = testClockWith((testClock) => testClock.currentTimeMillis) diff --git a/repos/effect/packages/effect/src/TestConfig.ts b/repos/effect/packages/effect/src/TestConfig.ts new file mode 100644 index 0000000..fae2172 --- /dev/null +++ b/repos/effect/packages/effect/src/TestConfig.ts @@ -0,0 +1,47 @@ +/** + * @since 2.0.0 + */ +import * as Context from "./Context.js" + +/** + * The `TestConfig` service provides access to default configuration settings + * used by tests, including the number of times to repeat tests to ensure + * they are stable, the number of times to retry flaky tests, the sufficient + * number of samples to check from a random variable, and the maximum number of + * shrinkings to minimize large failures. + * + * @since 2.0.0 + */ +export interface TestConfig { + /** + * The number of times to repeat tests to ensure they are stable. + */ + readonly repeats: number + /** + * The number of times to retry flaky tests. + */ + readonly retries: number + /** + * The number of sufficient samples to check for a random variable. + */ + readonly samples: number + /** + * The maximum number of shrinkings to minimize large failures + */ + readonly shrinks: number +} + +/** + * @since 2.0.0 + */ +export const TestConfig: Context.Tag = Context.GenericTag("effect/TestConfig") + +/** + * @since 2.0.0 + */ +export const make = (params: { + readonly repeats: number + readonly retries: number + readonly samples: number + readonly shrinks: number +}): TestConfig => params diff --git a/repos/effect/packages/effect/src/TestContext.ts b/repos/effect/packages/effect/src/TestContext.ts new file mode 100644 index 0000000..01e7853 --- /dev/null +++ b/repos/effect/packages/effect/src/TestContext.ts @@ -0,0 +1,36 @@ +/** + * @since 2.0.0 + */ +import type * as DefaultServices from "./DefaultServices.js" +import { pipe } from "./Function.js" +import * as defaultServices from "./internal/defaultServices.js" +import * as layer from "./internal/layer.js" +import type * as Layer from "./Layer.js" +import * as TestClock from "./TestClock.js" +import * as TestServices from "./TestServices.js" + +/** @internal */ +export const live: Layer.Layer = pipe( + TestServices.annotationsLayer(), + layer.merge(TestServices.liveLayer()), + layer.merge(TestServices.sizedLayer(100)), + layer.merge(pipe( + TestClock.defaultTestClock, + layer.provideMerge( + layer.merge(TestServices.liveLayer(), TestServices.annotationsLayer()) + ) + )), + layer.merge(TestServices.testConfigLayer({ repeats: 100, retries: 100, samples: 200, shrinks: 1000 })) +) + +/** + * @since 2.0.0 + */ +export const LiveContext: Layer.Layer = layer.syncContext(() => + defaultServices.liveServices +) + +/** + * @since 2.0.0 + */ +export const TestContext: Layer.Layer = layer.provideMerge(live, LiveContext) diff --git a/repos/effect/packages/effect/src/TestLive.ts b/repos/effect/packages/effect/src/TestLive.ts new file mode 100644 index 0000000..f9ae5e0 --- /dev/null +++ b/repos/effect/packages/effect/src/TestLive.ts @@ -0,0 +1,53 @@ +/** + * @since 2.0.0 + */ +import * as Context from "./Context.js" +import type * as DefaultServices from "./DefaultServices.js" +import type * as Effect from "./Effect.js" +import * as core from "./internal/core.js" +import * as defaultServices from "./internal/defaultServices.js" + +/** + * @since 2.0.0 + */ +export const TestLiveTypeId: unique symbol = Symbol.for("effect/TestLive") + +/** + * @since 2.0.0 + */ +export type TestLiveTypeId = typeof TestLiveTypeId + +/** + * The `Live` trait provides access to the "live" default Effect services from + * within tests for workflows such as printing test results to the console or + * timing out tests where it is necessary to access the real implementations of + * these services. + * + * @since 2.0.0 + */ +export interface TestLive { + readonly [TestLiveTypeId]: TestLiveTypeId + provide(effect: Effect.Effect): Effect.Effect +} + +/** + * @since 2.0.0 + */ +export const TestLive: Context.Tag = Context.GenericTag("effect/TestLive") + +/** @internal */ +class LiveImpl implements TestLive { + readonly [TestLiveTypeId]: TestLiveTypeId = TestLiveTypeId + constructor(readonly services: Context.Context) {} + provide(effect: Effect.Effect): Effect.Effect { + return core.fiberRefLocallyWith( + defaultServices.currentServices, + Context.merge(this.services) + )(effect) + } +} + +/** + * @since 2.0.0 + */ +export const make = (services: Context.Context): TestLive => new LiveImpl(services) diff --git a/repos/effect/packages/effect/src/TestServices.ts b/repos/effect/packages/effect/src/TestServices.ts new file mode 100644 index 0000000..108bc01 --- /dev/null +++ b/repos/effect/packages/effect/src/TestServices.ts @@ -0,0 +1,390 @@ +/** + * @since 2.0.0 + */ +import * as Context from "./Context.js" +import type * as DefaultServices from "./DefaultServices.js" +import * as Effect from "./Effect.js" +import type * as Fiber from "./Fiber.js" +import type * as FiberRef from "./FiberRef.js" +import { dual, pipe } from "./Function.js" +import * as core from "./internal/core.js" +import * as defaultServices from "./internal/defaultServices.js" +import * as fiberRuntime from "./internal/fiberRuntime.js" +import * as layer from "./internal/layer.js" +import * as ref from "./internal/ref.js" +import type * as Layer from "./Layer.js" +import type * as Scope from "./Scope.js" +import type * as SortedSet from "./SortedSet.js" +import type * as TestAnnotation from "./TestAnnotation.js" +import * as TestAnnotationMap from "./TestAnnotationMap.js" +import * as Annotations from "./TestAnnotations.js" +import * as TestConfig from "./TestConfig.js" +import * as Live from "./TestLive.js" +import * as Sized from "./TestSized.js" + +/** + * @since 2.0.0 + */ +export type TestServices = + | Annotations.TestAnnotations + | Live.TestLive + | Sized.TestSized + | TestConfig.TestConfig + +/** + * The default Effect test services. + * + * @since 2.0.0 + */ +export const liveServices: Context.Context = pipe( + Context.make(Annotations.TestAnnotations, Annotations.make(ref.unsafeMake(TestAnnotationMap.empty()))), + Context.add(Live.TestLive, Live.make(defaultServices.liveServices)), + Context.add(Sized.TestSized, Sized.make(100)), + Context.add(TestConfig.TestConfig, TestConfig.make({ repeats: 100, retries: 100, samples: 200, shrinks: 1000 })) +) + +/** + * @since 2.0.0 + */ +export const currentServices: FiberRef.FiberRef> = core.fiberRefUnsafeMakeContext( + liveServices +) + +/** + * Retrieves the `Annotations` service for this test. + * + * @since 2.0.0 + */ +export const annotations = (): Effect.Effect => annotationsWith(core.succeed) + +/** + * Retrieves the `Annotations` service for this test and uses it to run the + * specified workflow. + * + * @since 2.0.0 + */ +export const annotationsWith = ( + f: (annotations: Annotations.TestAnnotations) => Effect.Effect +): Effect.Effect => + core.fiberRefGetWith( + currentServices, + (services) => f(Context.get(services, Annotations.TestAnnotations)) + ) + +/** + * Executes the specified workflow with the specified implementation of the + * annotations service. + * + * @since 2.0.0 + */ +export const withAnnotations = dual< + (annotations: Annotations.TestAnnotations) => (effect: Effect.Effect) => Effect.Effect, + (effect: Effect.Effect, annotations: Annotations.TestAnnotations) => Effect.Effect +>(2, (effect, annotations) => + core.fiberRefLocallyWith( + currentServices, + Context.add(Annotations.TestAnnotations, annotations) + )(effect)) + +/** + * Sets the implementation of the annotations service to the specified value + * and restores it to its original value when the scope is closed. + * + * @since 2.0.0 + */ +export const withAnnotationsScoped = ( + annotations: Annotations.TestAnnotations +): Effect.Effect => + fiberRuntime.fiberRefLocallyScopedWith( + currentServices, + Context.add(Annotations.TestAnnotations, annotations) + ) + +/** + * Constructs a new `Annotations` service wrapped in a layer. + * + * @since 2.0.0 + */ +export const annotationsLayer = (): Layer.Layer => + layer.scoped( + Annotations.TestAnnotations, + pipe( + core.sync(() => ref.unsafeMake(TestAnnotationMap.empty())), + core.map(Annotations.make), + core.tap(withAnnotationsScoped) + ) + ) + +/** + * Accesses an `Annotations` instance in the context and retrieves the + * annotation of the specified type, or its default value if there is none. + * + * @since 2.0.0 + */ +export const get = (key: TestAnnotation.TestAnnotation): Effect.Effect => + annotationsWith((annotations) => annotations.get(key)) + +/** + * Accesses an `Annotations` instance in the context and appends the + * specified annotation to the annotation map. + * + * @since 2.0.0 + */ +export const annotate = (key: TestAnnotation.TestAnnotation, value: A): Effect.Effect => + annotationsWith((annotations) => annotations.annotate(key, value)) + +/** + * Returns the set of all fibers in this test. + * + * @since 2.0.0 + */ +export const supervisedFibers = (): Effect.Effect< + SortedSet.SortedSet> +> => annotationsWith((annotations) => annotations.supervisedFibers) + +/** + * Retrieves the `Live` service for this test and uses it to run the specified + * workflow. + * + * @since 2.0.0 + */ +export const liveWith = (f: (live: Live.TestLive) => Effect.Effect): Effect.Effect => + core.fiberRefGetWith(currentServices, (services) => f(Context.get(services, Live.TestLive))) + +/** + * Retrieves the `Live` service for this test. + * + * @since 2.0.0 + */ +export const live: Effect.Effect = liveWith(core.succeed) + +/** + * Executes the specified workflow with the specified implementation of the + * live service. + * + * @since 2.0.0 + */ +export const withLive = dual< + (live: Live.TestLive) => (effect: Effect.Effect) => Effect.Effect, + (effect: Effect.Effect, live: Live.TestLive) => Effect.Effect +>(2, (effect, live) => + core.fiberRefLocallyWith( + currentServices, + Context.add(Live.TestLive, live) + )(effect)) + +/** + * Sets the implementation of the live service to the specified value and + * restores it to its original value when the scope is closed. + * + * @since 2.0.0 + */ +export const withLiveScoped = (live: Live.TestLive): Effect.Effect => + fiberRuntime.fiberRefLocallyScopedWith(currentServices, Context.add(Live.TestLive, live)) + +/** + * Constructs a new `Live` service wrapped in a layer. + * + * @since 2.0.0 + */ +export const liveLayer = (): Layer.Layer => + layer.scoped( + Live.TestLive, + pipe( + core.context(), + core.map(Live.make), + core.tap(withLiveScoped) + ) + ) + +/** + * Provides a workflow with the "live" default Effect services. + * + * @since 2.0.0 + */ +export const provideLive = (effect: Effect.Effect): Effect.Effect => + liveWith((live) => live.provide(effect)) + +/** + * Runs a transformation function with the live default Effect services while + * ensuring that the workflow itself is run with the test services. + * + * @since 2.0.0 + */ +export const provideWithLive = dual< + ( + f: (effect: Effect.Effect) => Effect.Effect + ) => (self: Effect.Effect) => Effect.Effect, + ( + self: Effect.Effect, + f: (effect: Effect.Effect) => Effect.Effect + ) => Effect.Effect +>(2, (self, f) => + core.fiberRefGetWith( + defaultServices.currentServices, + (services) => provideLive(f(core.fiberRefLocally(defaultServices.currentServices, services)(self))) + )) + +/** + * Retrieves the `Sized` service for this test and uses it to run the + * specified workflow. + * + * @since 2.0.0 + */ +export const sizedWith = (f: (sized: Sized.TestSized) => Effect.Effect): Effect.Effect => + core.fiberRefGetWith( + currentServices, + (services) => f(Context.get(services, Sized.TestSized)) + ) + +/** + * Retrieves the `Sized` service for this test. + * + * @since 2.0.0 + */ +export const sized: Effect.Effect = sizedWith(core.succeed) + +/** + * Executes the specified workflow with the specified implementation of the + * sized service. + * + * @since 2.0.0 + */ +export const withSized = dual< + (sized: Sized.TestSized) => (effect: Effect.Effect) => Effect.Effect, + (effect: Effect.Effect, sized: Sized.TestSized) => Effect.Effect +>(2, (effect, sized) => + core.fiberRefLocallyWith( + currentServices, + Context.add(Sized.TestSized, sized) + )(effect)) + +/** + * Sets the implementation of the sized service to the specified value and + * restores it to its original value when the scope is closed. + * + * @since 2.0.0 + */ +export const withSizedScoped = (sized: Sized.TestSized): Effect.Effect => + fiberRuntime.fiberRefLocallyScopedWith(currentServices, Context.add(Sized.TestSized, sized)) + +/** + * @since 2.0.0 + */ +export const sizedLayer = (size: number): Layer.Layer => + layer.scoped( + Sized.TestSized, + pipe( + fiberRuntime.fiberRefMake(size), + core.map(Sized.fromFiberRef), + core.tap(withSizedScoped) + ) + ) + +/** + * @since 2.0.0 + */ +export const size: Effect.Effect = sizedWith((sized) => sized.size) + +/** + * @since 2.0.0 + */ +export const withSize = dual< + (size: number) => (effect: Effect.Effect) => Effect.Effect, + (effect: Effect.Effect, size: number) => Effect.Effect +>(2, (effect, size) => sizedWith((sized) => sized.withSize(size)(effect))) + +/** + * Retrieves the `TestConfig` service for this test and uses it to run the + * specified workflow. + * + * @since 2.0.0 + */ +export const testConfigWith = ( + f: (config: TestConfig.TestConfig) => Effect.Effect +): Effect.Effect => + core.fiberRefGetWith( + currentServices, + (services) => f(Context.get(services, TestConfig.TestConfig)) + ) + +/** + * Retrieves the `TestConfig` service for this test. + * + * @since 2.0.0 + */ +export const testConfig: Effect.Effect = testConfigWith(core.succeed) + +/** + * Executes the specified workflow with the specified implementation of the + * config service. + * + * @since 2.0.0 + */ +export const withTestConfig = dual< + (config: TestConfig.TestConfig) => (effect: Effect.Effect) => Effect.Effect, + (effect: Effect.Effect, config: TestConfig.TestConfig) => Effect.Effect +>(2, (effect, config) => + core.fiberRefLocallyWith( + currentServices, + Context.add(TestConfig.TestConfig, config) + )(effect)) + +/** + * Sets the implementation of the config service to the specified value and + * restores it to its original value when the scope is closed. + * + * @since 2.0.0 + */ +export const withTestConfigScoped = (config: TestConfig.TestConfig): Effect.Effect => + fiberRuntime.fiberRefLocallyScopedWith(currentServices, Context.add(TestConfig.TestConfig, config)) + +/** + * Constructs a new `TestConfig` service with the specified settings. + * + * @since 2.0.0 + */ +export const testConfigLayer = (params: { + readonly repeats: number + readonly retries: number + readonly samples: number + readonly shrinks: number +}): Layer.Layer => + layer.scoped( + TestConfig.TestConfig, + Effect.suspend(() => { + const testConfig = TestConfig.make(params) + return pipe( + withTestConfigScoped(testConfig), + core.as(testConfig) + ) + }) + ) + +/** + * The number of times to repeat tests to ensure they are stable. + * + * @since 2.0.0 + */ +export const repeats: Effect.Effect = testConfigWith((config) => core.succeed(config.repeats)) + +/** + * The number of times to retry flaky tests. + * + * @since 2.0.0 + */ +export const retries: Effect.Effect = testConfigWith((config) => core.succeed(config.retries)) + +/** + * The number of sufficient samples to check for a random variable. + * + * @since 2.0.0 + */ +export const samples: Effect.Effect = testConfigWith((config) => core.succeed(config.samples)) + +/** + * The maximum number of shrinkings to minimize large failures. + * + * @since 2.0.0 + */ +export const shrinks: Effect.Effect = testConfigWith((config) => core.succeed(config.shrinks)) diff --git a/repos/effect/packages/effect/src/TestSized.ts b/repos/effect/packages/effect/src/TestSized.ts new file mode 100644 index 0000000..21ab387 --- /dev/null +++ b/repos/effect/packages/effect/src/TestSized.ts @@ -0,0 +1,55 @@ +/** + * @since 2.0.0 + */ +import * as Context from "./Context.js" +import type * as Effect from "./Effect.js" +import type * as FiberRef from "./FiberRef.js" +import * as core from "./internal/core.js" + +/** + * @since 2.0.0 + */ +export const TestSizedTypeId: unique symbol = Symbol.for("effect/TestSized") + +/** + * @since 2.0.0 + */ +export type TestSizedTypeId = typeof TestSizedTypeId + +/** + * @since 2.0.0 + */ +export interface TestSized { + readonly [TestSizedTypeId]: TestSizedTypeId + readonly fiberRef: FiberRef.FiberRef + readonly size: Effect.Effect + withSize(size: number): (effect: Effect.Effect) => Effect.Effect +} + +/** + * @since 2.0.0 + */ +export const TestSized: Context.Tag = Context.GenericTag("effect/TestSized") + +/** @internal */ +class SizedImpl implements TestSized { + readonly [TestSizedTypeId]: TestSizedTypeId = TestSizedTypeId + constructor(readonly fiberRef: FiberRef.FiberRef) {} + get size(): Effect.Effect { + return core.fiberRefGet(this.fiberRef) + } + withSize(size: number) { + return (effect: Effect.Effect): Effect.Effect => + core.fiberRefLocally(this.fiberRef, size)(effect) + } +} + +/** + * @since 2.0.0 + */ +export const make = (size: number): TestSized => new SizedImpl(core.fiberRefUnsafeMake(size)) + +/** + * @since 2.0.0 + */ +export const fromFiberRef = (fiberRef: FiberRef.FiberRef): TestSized => new SizedImpl(fiberRef) diff --git a/repos/effect/packages/effect/src/Tracer.ts b/repos/effect/packages/effect/src/Tracer.ts new file mode 100644 index 0000000..c8c27ef --- /dev/null +++ b/repos/effect/packages/effect/src/Tracer.ts @@ -0,0 +1,182 @@ +/** + * @since 2.0.0 + */ +import type * as Context from "./Context.js" +import type * as Effect from "./Effect.js" +import type * as Exit from "./Exit.js" +import type * as Fiber from "./Fiber.js" +import type { LazyArg } from "./Function.js" +import * as defaultServices from "./internal/defaultServices.js" +import * as internal from "./internal/tracer.js" +import type * as Option from "./Option.js" + +/** + * @since 2.0.0 + */ +export const TracerTypeId: unique symbol = internal.TracerTypeId + +/** + * @since 2.0.0 + */ +export type TracerTypeId = typeof TracerTypeId + +/** + * @since 2.0.0 + */ +export interface Tracer { + readonly [TracerTypeId]: TracerTypeId + span( + name: string, + parent: Option.Option, + context: Context.Context, + links: ReadonlyArray, + startTime: bigint, + kind: SpanKind, + options?: SpanOptions + ): Span + context(f: () => X, fiber: Fiber.RuntimeFiber): X +} + +/** + * @since 2.0.0 + * @category models + */ +export type SpanStatus = { + _tag: "Started" + startTime: bigint +} | { + _tag: "Ended" + startTime: bigint + endTime: bigint + exit: Exit.Exit +} + +/** + * @since 2.0.0 + * @category models + */ +export type AnySpan = Span | ExternalSpan + +/** + * @since 2.0.0 + * @category tags + */ +export interface ParentSpan { + readonly _: unique symbol +} + +/** + * @since 2.0.0 + * @category tags + */ +export const ParentSpan: Context.Tag = internal.spanTag + +/** + * @since 2.0.0 + * @category models + */ +export interface ExternalSpan { + readonly _tag: "ExternalSpan" + readonly spanId: string + readonly traceId: string + readonly sampled: boolean + readonly context: Context.Context +} + +/** + * @since 3.1.0 + * @category models + */ +export interface SpanOptions { + readonly attributes?: Record | undefined + readonly links?: ReadonlyArray | undefined + readonly parent?: AnySpan | undefined + readonly root?: boolean | undefined + readonly context?: Context.Context | undefined + readonly kind?: SpanKind | undefined + readonly captureStackTrace?: boolean | LazyArg | undefined +} + +/** + * @since 3.1.0 + * @category models + */ +export type SpanKind = "internal" | "server" | "client" | "producer" | "consumer" + +/** + * @since 2.0.0 + * @category models + */ +export interface Span { + readonly _tag: "Span" + readonly name: string + readonly spanId: string + readonly traceId: string + readonly parent: Option.Option + readonly context: Context.Context + readonly status: SpanStatus + readonly attributes: ReadonlyMap + readonly links: ReadonlyArray + readonly sampled: boolean + readonly kind: SpanKind + end(endTime: bigint, exit: Exit.Exit): void + attribute(key: string, value: unknown): void + event(name: string, startTime: bigint, attributes?: Record): void + addLinks(links: ReadonlyArray): void +} + +/** + * @since 2.0.0 + * @category models + */ +export interface SpanLink { + readonly _tag: "SpanLink" + readonly span: AnySpan + readonly attributes: Readonly> +} + +/** + * @since 2.0.0 + * @category tags + */ +export const Tracer: Context.Tag = internal.tracerTag + +/** + * @since 2.0.0 + * @category constructors + */ +export const make: (options: Omit) => Tracer = internal.make + +/** + * @since 2.0.0 + * @category constructors + */ +export const externalSpan: ( + options: { + readonly spanId: string + readonly traceId: string + readonly sampled?: boolean | undefined + readonly context?: Context.Context | undefined + } +) => ExternalSpan = internal.externalSpan + +/** + * @since 2.0.0 + * @category constructors + */ +export const tracerWith: (f: (tracer: Tracer) => Effect.Effect) => Effect.Effect = + defaultServices.tracerWith + +/** + * @since 3.12.0 + * @category annotations + */ +export interface DisablePropagation { + readonly _: unique symbol +} + +/** + * @since 3.12.0 + * @category annotations + */ +export const DisablePropagation: Context.Reference = internal.DisablePropagation diff --git a/repos/effect/packages/effect/src/Trie.ts b/repos/effect/packages/effect/src/Trie.ts new file mode 100644 index 0000000..1f3ef8e --- /dev/null +++ b/repos/effect/packages/effect/src/Trie.ts @@ -0,0 +1,840 @@ +/** + * A `Trie` is used for locating specific `string` keys from within a set. + * + * It works similar to `HashMap`, but with keys required to be `string`. + * This constraint unlocks some performance optimizations and new methods to get string prefixes (e.g. `keysWithPrefix`, `longestPrefixOf`). + * + * Prefix search is also the main feature that makes a `Trie` more suited than `HashMap` for certain usecases. + * + * A `Trie` is often used to store a dictionary (list of words) that can be searched + * in a manner that allows for efficient generation of completion lists + * (e.g. predict the rest of a word a user is typing). + * + * A `Trie` has O(n) lookup time where `n` is the size of the key, + * or even less than `n` on search misses. + * + * @since 2.0.0 + */ +import type { Equal } from "./Equal.js" +import type { Inspectable } from "./Inspectable.js" +import * as TR from "./internal/trie.js" +import type { Option } from "./Option.js" +import type { Pipeable } from "./Pipeable.js" +import type { Covariant, NoInfer } from "./Types.js" + +const TypeId: unique symbol = TR.TrieTypeId as TypeId + +/** + * @since 2.0.0 + * @category symbol + */ +export type TypeId = typeof TypeId + +/** + * @since 2.0.0 + * @category models + */ +export interface Trie extends Iterable<[string, Value]>, Equal, Pipeable, Inspectable { + readonly [TypeId]: { + readonly _Value: Covariant + } +} + +/** + * Creates an empty `Trie`. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { Trie, Equal } from "effect" + * + * const trie = Trie.empty() + * + * assert.equal(Trie.size(trie), 0) + * assert.deepStrictEqual(Array.from(trie), []) + * ``` + * + * @since 2.0.0 + * @category constructors + */ +export const empty: () => Trie = TR.empty + +/** + * Creates a new `Trie` from an iterable collection of key/value pairs (e.g. `Array<[string, V]>`). + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { Trie, Equal } from "effect" + * + * const iterable: Array = [["call", 0], ["me", 1], ["mind", 2], ["mid", 3]] + * const trie = Trie.fromIterable(iterable) + * + * // The entries in the `Trie` are extracted in alphabetical order, regardless of the insertion order + * assert.deepStrictEqual(Array.from(trie), [["call", 0], ["me", 1], ["mid", 3], ["mind", 2]]) + * assert.equal(Equal.equals(Trie.make(["call", 0], ["me", 1], ["mind", 2], ["mid", 3]), trie), true) + * ``` + * + * @since 2.0.0 + * @category constructors + */ +export const fromIterable: (entries: Iterable) => Trie = TR.fromIterable + +/** + * Constructs a new `Trie` from the specified entries (`[string, V]`). + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { Trie, Equal } from "effect" + * + * const trie = Trie.make(["ca", 0], ["me", 1]) + * + * assert.deepStrictEqual(Array.from(trie), [["ca", 0], ["me", 1]]) + * assert.equal(Equal.equals(Trie.fromIterable([["ca", 0], ["me", 1]]), trie), true) + * ``` + * + * @since 2.0.0 + * @category constructors + */ +export const make: >( + ...entries: Entries +) => Trie = TR.make + +/** + * Insert a new entry in the `Trie`. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { Trie } from "effect" + * + * const trie1 = Trie.empty().pipe( + * Trie.insert("call", 0) + * ) + * const trie2 = trie1.pipe(Trie.insert("me", 1)) + * const trie3 = trie2.pipe(Trie.insert("mind", 2)) + * const trie4 = trie3.pipe(Trie.insert("mid", 3)) + * + * assert.deepStrictEqual(Array.from(trie1), [["call", 0]]) + * assert.deepStrictEqual(Array.from(trie2), [["call", 0], ["me", 1]]) + * assert.deepStrictEqual(Array.from(trie3), [["call", 0], ["me", 1], ["mind", 2]]) + * assert.deepStrictEqual(Array.from(trie4), [["call", 0], ["me", 1], ["mid", 3], ["mind", 2]]) + * ``` + * + * @since 2.0.0 + * @category mutations + */ +export const insert: { + (key: string, value: V1): (self: Trie) => Trie + (self: Trie, key: string, value: V1): Trie +} = TR.insert + +/** + * Returns an `IterableIterator` of the keys within the `Trie`. + * + * The keys are returned in alphabetical order, regardless of insertion order. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { Trie } from "effect" + * + * const trie = Trie.empty().pipe( + * Trie.insert("cab", 0), + * Trie.insert("abc", 1), + * Trie.insert("bca", 2) + * ) + * + * const result = Array.from(Trie.keys(trie)) + * assert.deepStrictEqual(result, ["abc", "bca", "cab"]) + * ``` + * + * @since 2.0.0 + * @category getters + */ +export const keys: (self: Trie) => IterableIterator = TR.keys + +/** + * Returns an `IterableIterator` of the values within the `Trie`. + * + * Values are ordered based on their key in alphabetical order, regardless of insertion order. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { Trie } from "effect" + * + * const trie = Trie.empty().pipe( + * Trie.insert("call", 0), + * Trie.insert("me", 1), + * Trie.insert("and", 2) + * ) + * + * const result = Array.from(Trie.values(trie)) + * assert.deepStrictEqual(result, [2, 0, 1]) + * ``` + * + * @since 2.0.0 + * @category getters + */ +export const values: (self: Trie) => IterableIterator = TR.values + +/** + * Returns an `IterableIterator` of the entries within the `Trie`. + * + * The entries are returned by keys in alphabetical order, regardless of insertion order. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { Trie } from "effect" + * + * const trie = Trie.empty().pipe( + * Trie.insert("call", 0), + * Trie.insert("me", 1) + * ) + * + * const result = Array.from(Trie.entries(trie)) + * assert.deepStrictEqual(result, [["call", 0], ["me", 1]]) + * ``` + * + * @since 2.0.0 + * @category getters + */ +export const entries: (self: Trie) => IterableIterator<[string, V]> = TR.entries + +/** + * Returns an `Array<[K, V]>` of the entries within the `Trie`. + * + * Equivalent to `Array.from(Trie.entries(trie))`. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { Trie } from "effect" + * + * const trie = Trie.empty().pipe( + * Trie.insert("call", 0), + * Trie.insert("me", 1) + * ) + * const result = Trie.toEntries(trie) + * + * assert.deepStrictEqual(result, [["call", 0], ["me", 1]]) + * ``` + * + * @since 2.0.0 + * @category getters + */ +export const toEntries = (self: Trie): Array<[string, V]> => Array.from(entries(self)) + +/** + * Returns an `IterableIterator` of the keys within the `Trie` + * that have `prefix` as prefix (`prefix` included if it exists). + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { Trie } from "effect" + * + * const trie = Trie.empty().pipe( + * Trie.insert("she", 0), + * Trie.insert("shells", 1), + * Trie.insert("sea", 2), + * Trie.insert("shore", 3) + * ) + * + * const result = Array.from(Trie.keysWithPrefix(trie, "she")) + * assert.deepStrictEqual(result, ["she", "shells"]) + * ``` + * + * @since 2.0.0 + * @category getters + */ +export const keysWithPrefix: { + (prefix: string): (self: Trie) => IterableIterator + (self: Trie, prefix: string): IterableIterator +} = TR.keysWithPrefix + +/** + * Returns an `IterableIterator` of the values within the `Trie` + * that have `prefix` as prefix (`prefix` included if it exists). + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { Trie } from "effect" + * + * const trie = Trie.empty().pipe( + * Trie.insert("she", 0), + * Trie.insert("shells", 1), + * Trie.insert("sea", 2), + * Trie.insert("shore", 3) + * ) + * + * const result = Array.from(Trie.valuesWithPrefix(trie, "she")) + * + * // 0: "she", 1: "shells" + * assert.deepStrictEqual(result, [0, 1]) + * ``` + * + * @since 2.0.0 + * @category getters + */ +export const valuesWithPrefix: { + (prefix: string): (self: Trie) => IterableIterator + (self: Trie, prefix: string): IterableIterator +} = TR.valuesWithPrefix + +/** + * Returns an `IterableIterator` of the entries within the `Trie` + * that have `prefix` as prefix (`prefix` included if it exists). + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { Trie } from "effect" + * + * const trie = Trie.empty().pipe( + * Trie.insert("she", 0), + * Trie.insert("shells", 1), + * Trie.insert("sea", 2), + * Trie.insert("shore", 3) + * ) + * + * const result = Array.from(Trie.entriesWithPrefix(trie, "she")) + * assert.deepStrictEqual(result, [["she", 0], ["shells", 1]]) + * ``` + * + * @since 2.0.0 + * @category getters + */ +export const entriesWithPrefix: { + (prefix: string): (self: Trie) => IterableIterator<[string, V]> + (self: Trie, prefix: string): IterableIterator<[string, V]> +} = TR.entriesWithPrefix + +/** + * Returns `Array<[K, V]>` of the entries within the `Trie` + * that have `prefix` as prefix (`prefix` included if it exists). + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { Trie } from "effect" + * + * const trie = Trie.empty().pipe( + * Trie.insert("shells", 0), + * Trie.insert("sells", 1), + * Trie.insert("sea", 2), + * Trie.insert("she", 3) + * ) + * + * const result = Trie.toEntriesWithPrefix(trie, "she") + * assert.deepStrictEqual(result, [["she", 3], ["shells", 0]]) + * ``` + * + * @since 2.0.0 + * @category getters + */ +export const toEntriesWithPrefix: { + (prefix: string): (self: Trie) => Array<[string, V]> + (self: Trie, prefix: string): Array<[string, V]> +} = TR.toEntriesWithPrefix + +/** + * Returns the longest key/value in the `Trie` + * that is a prefix of that `key` if it exists, `None` otherwise. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { Trie, Option } from "effect" + * + * const trie = Trie.empty().pipe( + * Trie.insert("shells", 0), + * Trie.insert("sells", 1), + * Trie.insert("she", 2) + * ) + * + * assert.deepStrictEqual(Trie.longestPrefixOf(trie, "sell"), Option.none()) + * assert.deepStrictEqual(Trie.longestPrefixOf(trie, "sells"), Option.some(["sells", 1])) + * assert.deepStrictEqual(Trie.longestPrefixOf(trie, "shell"), Option.some(["she", 2])) + * assert.deepStrictEqual(Trie.longestPrefixOf(trie, "shellsort"), Option.some(["shells", 0])) + * ``` + * + * @since 2.0.0 + * @category getters + */ +export const longestPrefixOf: { + (key: string): (self: Trie) => Option<[string, V]> + (self: Trie, key: string): Option<[string, V]> +} = TR.longestPrefixOf + +/** + * Returns the size of the `Trie` (number of entries in the `Trie`). + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { Trie } from "effect" + * + * const trie = Trie.empty().pipe( + * Trie.insert("a", 0), + * Trie.insert("b", 1) + * ) + * + * assert.equal(Trie.size(trie), 2) + * ``` + * + * @since 2.0.0 + * @category getters + */ +export const size: (self: Trie) => number = TR.size + +/** + * Safely lookup the value for the specified key in the `Trie`. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { Trie, Option } from "effect" + * + * const trie = Trie.empty().pipe( + * Trie.insert("call", 0), + * Trie.insert("me", 1), + * Trie.insert("mind", 2), + * Trie.insert("mid", 3) + * ) + * + * assert.deepStrictEqual(Trie.get(trie, "call"), Option.some(0)) + * assert.deepStrictEqual(Trie.get(trie, "me"), Option.some(1)) + * assert.deepStrictEqual(Trie.get(trie, "mind"), Option.some(2)) + * assert.deepStrictEqual(Trie.get(trie, "mid"), Option.some(3)) + * assert.deepStrictEqual(Trie.get(trie, "cale"), Option.none()) + * assert.deepStrictEqual(Trie.get(trie, "ma"), Option.none()) + * assert.deepStrictEqual(Trie.get(trie, "midn"), Option.none()) + * assert.deepStrictEqual(Trie.get(trie, "mea"), Option.none()) + * ``` + * + * @since 2.0.0 + * @category elements + */ +export const get: { + (key: string): (self: Trie) => Option + (self: Trie, key: string): Option +} = TR.get + +/** + * Check if the given key exists in the `Trie`. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { Trie } from "effect" + * + * const trie = Trie.empty().pipe( + * Trie.insert("call", 0), + * Trie.insert("me", 1), + * Trie.insert("mind", 2), + * Trie.insert("mid", 3) + * ) + * + * assert.equal(Trie.has(trie, "call"), true) + * assert.equal(Trie.has(trie, "me"), true) + * assert.equal(Trie.has(trie, "mind"), true) + * assert.equal(Trie.has(trie, "mid"), true) + * assert.equal(Trie.has(trie, "cale"), false) + * assert.equal(Trie.has(trie, "ma"), false) + * assert.equal(Trie.has(trie, "midn"), false) + * assert.equal(Trie.has(trie, "mea"), false) + * ``` + * + * @since 2.0.0 + * @category elements + */ +export const has: { + (key: string): (self: Trie) => boolean + (self: Trie, key: string): boolean +} = TR.has + +/** + * Checks if the `Trie` contains any entries. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { Trie } from "effect" + * + * const trie = Trie.empty() + * const trie1 = trie.pipe(Trie.insert("ma", 0)) + * + * assert.equal(Trie.isEmpty(trie), true) + * assert.equal(Trie.isEmpty(trie1), false) + * ``` + * + * @since 2.0.0 + * @category elements + */ +export const isEmpty: (self: Trie) => boolean = TR.isEmpty + +/** + * Unsafely lookup the value for the specified key in the `Trie`. + * + * `unsafeGet` will throw if the key is not found. Use `get` instead to safely + * get a value from the `Trie`. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { Trie } from "effect" + * + * const trie = Trie.empty().pipe( + * Trie.insert("call", 0), + * Trie.insert("me", 1) + * ) + * + * assert.throws(() => Trie.unsafeGet(trie, "mae")) + * ``` + * + * @since 2.0.0 + * @category unsafe + */ +export const unsafeGet: { + (key: string): (self: Trie) => V + (self: Trie, key: string): V +} = TR.unsafeGet + +/** + * Remove the entry for the specified key in the `Trie`. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { Trie, Option } from "effect" + * + * const trie = Trie.empty().pipe( + * Trie.insert("call", 0), + * Trie.insert("me", 1), + * Trie.insert("mind", 2), + * Trie.insert("mid", 3) + * ) + * + * const trie1 = trie.pipe(Trie.remove("call")) + * const trie2 = trie1.pipe(Trie.remove("mea")) + * + * assert.deepStrictEqual(Trie.get(trie, "call"), Option.some(0)) + * assert.deepStrictEqual(Trie.get(trie1, "call"), Option.none()) + * assert.deepStrictEqual(Trie.get(trie2, "call"), Option.none()) + * ``` + * + * @since 2.0.0 + * @category mutations + */ +export const remove: { + (key: string): (self: Trie) => Trie + (self: Trie, key: string): Trie +} = TR.remove + +/** + * Reduce a state over the entries of the `Trie`. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { Trie } from "effect" + * + * const trie = Trie.empty().pipe( + * Trie.insert("shells", 0), + * Trie.insert("sells", 1), + * Trie.insert("she", 2) + * ) + * + * assert.equal( + * trie.pipe( + * Trie.reduce(0, (acc, n) => acc + n) + * ), + * 3 + * ) + * assert.equal( + * trie.pipe( + * Trie.reduce(10, (acc, n) => acc + n) + * ), + * 13 + * ) + * assert.equal( + * trie.pipe( + * Trie.reduce("", (acc, _, key) => acc + key) + * ), + * "sellssheshells" + * ) + * ``` + * + * @since 2.0.0 + * @category folding + */ +export const reduce: { + (zero: Z, f: (accumulator: Z, value: V, key: string) => Z): (self: Trie) => Z + (self: Trie, zero: Z, f: (accumulator: Z, value: V, key: string) => Z): Z +} = TR.reduce + +/** + * Maps over the entries of the `Trie` using the specified function. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { Trie, Equal } from "effect" + * + * const trie = Trie.empty().pipe( + * Trie.insert("shells", 0), + * Trie.insert("sells", 1), + * Trie.insert("she", 2) + * ) + * + * const trieMapV = Trie.empty().pipe( + * Trie.insert("shells", 1), + * Trie.insert("sells", 2), + * Trie.insert("she", 3) + * ) + * + * const trieMapK = Trie.empty().pipe( + * Trie.insert("shells", 6), + * Trie.insert("sells", 5), + * Trie.insert("she", 3) + * ) + * + * assert.equal(Equal.equals(Trie.map(trie, (v) => v + 1), trieMapV), true) + * assert.equal(Equal.equals(Trie.map(trie, (_, k) => k.length), trieMapK), true) + * ``` + * + * @since 2.0.0 + * @category folding + */ +export const map: { + (f: (value: V, key: string) => A): (self: Trie) => Trie + (self: Trie, f: (value: V, key: string) => A): Trie +} = TR.map + +/** + * Filters entries out of a `Trie` using the specified predicate. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { Trie, Equal } from "effect" + * + * const trie = Trie.empty().pipe( + * Trie.insert("shells", 0), + * Trie.insert("sells", 1), + * Trie.insert("she", 2) + * ) + * + * const trieMapV = Trie.empty().pipe( + * Trie.insert("she", 2) + * ) + * + * const trieMapK = Trie.empty().pipe( + * Trie.insert("shells", 0), + * Trie.insert("sells", 1) + * ) + * + * assert.equal(Equal.equals(Trie.filter(trie, (v) => v > 1), trieMapV), true) + * assert.equal(Equal.equals(Trie.filter(trie, (_, k) => k.length > 3), trieMapK), true) + * ``` + * + * @since 2.0.0 + * @category filtering + */ +export const filter: { + (f: (a: NoInfer, k: string) => a is B): (self: Trie) => Trie + (f: (a: NoInfer, k: string) => boolean): (self: Trie) => Trie + (self: Trie, f: (a: A, k: string) => a is B): Trie + (self: Trie, f: (a: A, k: string) => boolean): Trie +} = TR.filter + +/** + * Maps over the entries of the `Trie` using the specified partial function + * and filters out `None` values. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { Trie, Equal, Option } from "effect" + * + * const trie = Trie.empty().pipe( + * Trie.insert("shells", 0), + * Trie.insert("sells", 1), + * Trie.insert("she", 2) + * ) + * + * const trieMapV = Trie.empty().pipe( + * Trie.insert("she", 2) + * ) + * + * const trieMapK = Trie.empty().pipe( + * Trie.insert("shells", 0), + * Trie.insert("sells", 1) + * ) + * + * assert.equal(Equal.equals(Trie.filterMap(trie, (v) => v > 1 ? Option.some(v) : Option.none()), trieMapV), true) + * assert.equal( + * Equal.equals(Trie.filterMap(trie, (v, k) => k.length > 3 ? Option.some(v) : Option.none()), trieMapK), + * true + * ) + * ``` + * + * @since 2.0.0 + * @category filtering + */ +export const filterMap: { + (f: (value: A, key: string) => Option): (self: Trie) => Trie + (self: Trie, f: (value: A, key: string) => Option): Trie +} = TR.filterMap + +/** + * Filters out `None` values from a `Trie` of `Options`s. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { Trie, Equal, Option } from "effect" + * + * const trie = Trie.empty>().pipe( + * Trie.insert("shells", Option.some(0)), + * Trie.insert("sells", Option.none()), + * Trie.insert("she", Option.some(2)) + * ) + * + * const trieMapV = Trie.empty().pipe( + * Trie.insert("shells", 0), + * Trie.insert("she", 2) + * ) + * + * assert.equal(Equal.equals(Trie.compact(trie), trieMapV), true) + * ``` + * + * @since 2.0.0 + * @category filtering + */ +export const compact: (self: Trie>) => Trie = TR.compact + +/** + * Applies the specified function to the entries of the `Trie`. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { Trie } from "effect" + * + * let value = 0 + * + * Trie.empty().pipe( + * Trie.insert("shells", 0), + * Trie.insert("sells", 1), + * Trie.insert("she", 2), + * Trie.forEach((n, key) => { + * value += n + key.length + * }) + * ) + * + * assert.equal(value, 17) + * ``` + * + * @since 2.0.0 + * @category traversing + */ +export const forEach: { + (f: (value: V, key: string) => void): (self: Trie) => void + (self: Trie, f: (value: V, key: string) => void): void +} = TR.forEach + +/** + * Updates the value of the specified key within the `Trie` if it exists. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { Trie, Equal, Option } from "effect" + * + * const trie = Trie.empty().pipe( + * Trie.insert("shells", 0), + * Trie.insert("sells", 1), + * Trie.insert("she", 2) + * ) + * + * assert.deepStrictEqual(trie.pipe(Trie.modify("she", (v) => v + 10), Trie.get("she")), Option.some(12)) + * + * assert.equal(Equal.equals(trie.pipe(Trie.modify("me", (v) => v)), trie), true) + * ``` + * + * @since 2.0.0 + * @category mutations + */ +export const modify: { + (key: string, f: (v: V) => V1): (self: Trie) => Trie + (self: Trie, key: string, f: (v: V) => V1): Trie +} = TR.modify + +/** + * Removes all entries in the `Trie` which have the specified keys. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { Trie, Equal } from "effect" + * + * const trie = Trie.empty().pipe( + * Trie.insert("shells", 0), + * Trie.insert("sells", 1), + * Trie.insert("she", 2) + * ) + * + * assert.equal( + * Equal.equals(trie.pipe(Trie.removeMany(["she", "sells"])), Trie.empty().pipe(Trie.insert("shells", 0))), + * true + * ) + * ``` + * + * @since 2.0.0 + * @category mutations + */ +export const removeMany: { + (keys: Iterable): (self: Trie) => Trie + (self: Trie, keys: Iterable): Trie +} = TR.removeMany + +/** + * Insert multiple entries in the `Trie` at once. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { Trie, Equal } from "effect" + * + * const trie = Trie.empty().pipe( + * Trie.insert("shells", 0), + * Trie.insert("sells", 1), + * Trie.insert("she", 2) + * ) + * + * const trieInsert = Trie.empty().pipe( + * Trie.insert("shells", 0), + * Trie.insertMany( + * [["sells", 1], ["she", 2]] + * ) + * ) + * + * assert.equal( + * Equal.equals(trie, trieInsert), + * true + * ) + * ``` + * + * @since 2.0.0 + * @category mutations + */ +export const insertMany: { + (iter: Iterable<[string, V1]>): (self: Trie) => Trie + (self: Trie, iter: Iterable<[string, V1]>): Trie +} = TR.insertMany diff --git a/repos/effect/packages/effect/src/Tuple.ts b/repos/effect/packages/effect/src/Tuple.ts new file mode 100644 index 0000000..ede2b47 --- /dev/null +++ b/repos/effect/packages/effect/src/Tuple.ts @@ -0,0 +1,305 @@ +/** + * This module provides utility functions for working with tuples in TypeScript. + * + * @since 2.0.0 + */ +import * as Equivalence from "./Equivalence.js" +import { dual } from "./Function.js" +import type { TypeLambda } from "./HKT.js" +import * as order from "./Order.js" +import type { TupleOf } from "./Types.js" + +/** + * @category type lambdas + * @since 2.0.0 + */ +export interface TupleTypeLambda extends TypeLambda { + readonly type: [this["Out1"], this["Target"]] +} + +/** + * Constructs a new tuple from the provided values. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { make } from "effect/Tuple" + * + * assert.deepStrictEqual(make(1, 'hello', true), [1, 'hello', true]) + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const make = >(...elements: A): A => elements + +/** + * Return the first element from a tuple with two elements. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { getFirst } from "effect/Tuple" + * + * assert.deepStrictEqual(getFirst(["hello", 42]), "hello") + * ``` + * + * @category getters + * @since 2.0.0 + */ +export const getFirst = (self: readonly [L, R]): L => self[0] + +/** + * Return the second element from a tuple with two elements. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { getSecond } from "effect/Tuple" + * + * assert.deepStrictEqual(getSecond(["hello", 42]), 42) + * ``` + * + * @category getters + * @since 2.0.0 + */ +export const getSecond = (self: readonly [L, R]): R => self[1] + +/** + * Transforms each element of tuple using the given function, treating tuple homomorphically + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { pipe, Tuple } from "effect" + * + * const result = pipe( + * ["a", 1, false] as const, + * Tuple.map((el) => el.toString().toUpperCase()) + * ) + * assert.deepStrictEqual(result, ['A', '1', 'FALSE']) + * ``` + * + * @category mapping + * @since 3.9.0 + */ +export const map: { + | [], B>( + fn: (element: T[number]) => B + ): (self: T) => TupleOf + | []>( + self: T, + fn: (element: T[number]) => B + ): TupleOf +} = dual( + 2, + ( + self: TupleOf, + fn: (element: A) => B + ): TupleOf => self.map((element) => fn(element)) as TupleOf +) + +/** + * Transforms both elements of a tuple with two elements using the given functions. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { mapBoth } from "effect/Tuple" + * + * assert.deepStrictEqual( + * mapBoth(["hello", 42], { onFirst: s => s.toUpperCase(), onSecond: n => n.toString() }), + * ["HELLO", "42"] + * ) + * ``` + * + * @category mapping + * @since 2.0.0 + */ +export const mapBoth: { + (options: { + readonly onFirst: (e: L1) => L2 + readonly onSecond: (a: R1) => R2 + }): (self: readonly [L1, R1]) => [L2, R2] + (self: readonly [L1, R1], options: { + readonly onFirst: (e: L1) => L2 + readonly onSecond: (a: R1) => R2 + }): [L2, R2] +} = dual( + 2, + ( + self: readonly [L1, R1], + { onFirst, onSecond }: { + readonly onFirst: (e: L1) => L2 + readonly onSecond: (a: R1) => R2 + } + ): [L2, R2] => [onFirst(self[0]), onSecond(self[1])] +) + +/** + * Transforms the first component of a tuple with two elements using a given function. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { mapFirst } from "effect/Tuple" + * + * assert.deepStrictEqual( + * mapFirst(["hello", 42], s => s.toUpperCase()), + * ["HELLO", 42] + * ) + * ``` + * + * @category mapping + * @since 2.0.0 + */ +export const mapFirst: { + (f: (left: L1) => L2): (self: readonly [L1, R]) => [L2, R] + (self: readonly [L1, R], f: (left: L1) => L2): [L2, R] +} = dual(2, (self: readonly [L1, R], f: (left: L1) => L2): [L2, R] => [f(self[0]), self[1]]) + +/** + * Transforms the second component of a tuple with two elements using a given function. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { mapSecond } from "effect/Tuple" + * + * assert.deepStrictEqual( + * mapSecond(["hello", 42], n => n.toString()), + * ["hello", "42"] + * ) + * ``` + * + * @category mapping + * @since 2.0.0 + */ +export const mapSecond: { + (f: (right: R1) => R2): (self: readonly [L, R1]) => [L, R2] + (self: readonly [L, R1], f: (right: R1) => R2): [L, R2] +} = dual(2, (self: readonly [L, R1], f: (right: R1) => R2): [L, R2] => [self[0], f(self[1])]) + +/** + * Swaps the elements of a tuple with two elements. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { swap } from "effect/Tuple" + * + * assert.deepStrictEqual(swap(["hello", 42]), [42, "hello"]) + * ``` + * + * @since 2.0.0 + */ +export const swap = (self: readonly [L, R]): [R, L] => [self[1], self[0]] + +/** + * Given a tuple of `Equivalence`s returns a new `Equivalence` that compares values of a tuple + * by applying each `Equivalence` to the corresponding element of the tuple. + * + * @category combinators + * @since 2.0.0 + */ +export const getEquivalence: >>( + ...isEquivalents: T +) => Equivalence.Equivalence< + Readonly<{ [I in keyof T]: [T[I]] extends [Equivalence.Equivalence] ? A : never }> +> = Equivalence.tuple + +/** + * This function creates and returns a new `Order` for a tuple of values based on the given `Order`s for each element in the tuple. + * The returned `Order` compares two tuples of the same type by applying the corresponding `Order` to each element in the tuple. + * It is useful when you need to compare two tuples of the same type and you have a specific way of comparing each element + * of the tuple. + * + * @category combinators + * @since 2.0.0 + */ +export const getOrder: >>( + ...elements: T +) => order.Order<{ [I in keyof T]: [T[I]] extends [order.Order] ? A : never }> = order.tuple + +/** + * Appends an element to the end of a tuple. + * + * @category concatenating + * @since 2.0.0 + */ +export const appendElement: { + (that: B): >(self: A) => [...A, B] + , B>(self: A, that: B): [...A, B] +} = dual(2, , B>(self: A, that: B): [...A, B] => [...self, that]) + +/** + * Retrieves the element at a specified index from a tuple. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { Tuple } from "effect" + * + * assert.deepStrictEqual(Tuple.at([1, 'hello', true], 1), 'hello') + * ``` + * + * @category getters + * @since 3.4.0 + */ +export const at: { + (index: N): >(self: A) => A[N] + , N extends number>(self: A, index: N): A[N] +} = dual(2, , N extends number>(self: A, index: N): A[N] => self[index]) + +export { + /** + * Determine if an `Array` is a tuple with exactly `N` elements, narrowing down the type to `TupleOf`. + * + * An `Array` is considered to be a `TupleOf` if its length is exactly `N`. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { isTupleOf } from "effect/Tuple" + * + * assert.deepStrictEqual(isTupleOf([1, 2, 3], 3), true); + * assert.deepStrictEqual(isTupleOf([1, 2, 3], 2), false); + * assert.deepStrictEqual(isTupleOf([1, 2, 3], 4), false); + * + * const arr: number[] = [1, 2, 3]; + * if (isTupleOf(arr, 3)) { + * console.log(arr); + * // ^? [number, number, number] + * } + * + * ``` + * @category guards + * @since 3.3.0 + */ + isTupleOf, + /** + * Determine if an `Array` is a tuple with at least `N` elements, narrowing down the type to `TupleOfAtLeast`. + * + * An `Array` is considered to be a `TupleOfAtLeast` if its length is at least `N`. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { isTupleOfAtLeast } from "effect/Tuple" + * + * assert.deepStrictEqual(isTupleOfAtLeast([1, 2, 3], 3), true); + * assert.deepStrictEqual(isTupleOfAtLeast([1, 2, 3], 2), true); + * assert.deepStrictEqual(isTupleOfAtLeast([1, 2, 3], 4), false); + * + * const arr: number[] = [1, 2, 3, 4]; + * if (isTupleOfAtLeast(arr, 3)) { + * console.log(arr); + * // ^? [number, number, number, ...number[]] + * } + * + * ``` + * @category guards + * @since 3.3.0 + */ + isTupleOfAtLeast +} from "./Predicate.js" diff --git a/repos/effect/packages/effect/src/Types.ts b/repos/effect/packages/effect/src/Types.ts new file mode 100644 index 0000000..90d6476 --- /dev/null +++ b/repos/effect/packages/effect/src/Types.ts @@ -0,0 +1,361 @@ +/** + * A collection of types that are commonly used types. + * + * @since 2.0.0 + */ + +type _TupleOf> = `${N}` extends `-${number}` ? never + : R["length"] extends N ? R + : _TupleOf + +/** + * Represents a tuple with a fixed number of elements of type `T`. + * + * This type constructs a tuple that has exactly `N` elements of type `T`. + * + * @typeParam N - The number of elements in the tuple. + * @typeParam T - The type of elements in the tuple. + * + * @example + * ```ts + * import { TupleOf } from "effect/Types" + * + * // A tuple with exactly 3 numbers + * const example1: TupleOf<3, number> = [1, 2, 3]; // valid + * // @ts-expect-error + * const example2: TupleOf<3, number> = [1, 2]; // invalid + * // @ts-expect-error + * const example3: TupleOf<3, number> = [1, 2, 3, 4]; // invalid + * ``` + * + * @category tuples + * @since 3.3.0 + */ +export type TupleOf = N extends N ? number extends N ? Array : _TupleOf : never + +/** + * Represents a tuple with at least `N` elements of type `T`. + * + * This type constructs a tuple that has a fixed number of elements `N` of type `T` at the start, + * followed by any number (including zero) of additional elements of the same type `T`. + * + * @typeParam N - The minimum number of elements in the tuple. + * @typeParam T - The type of elements in the tuple. + * + * @example + * ```ts + * import { TupleOfAtLeast } from "effect/Types" + * + * // A tuple with at least 3 numbers + * const example1: TupleOfAtLeast<3, number> = [1, 2, 3]; // valid + * const example2: TupleOfAtLeast<3, number> = [1, 2, 3, 4, 5]; // valid + * // @ts-expect-error + * const example3: TupleOfAtLeast<3, number> = [1, 2]; // invalid + * ``` + * + * @category tuples + * @since 3.3.0 + */ +export type TupleOfAtLeast = [...TupleOf, ...Array] + +/** + * Returns the tags in a type. + * @example + * ```ts + * import type { Types } from "effect" + * + * type Res = Types.Tags // "a" | "b" + * ``` + * + * @category types + * @since 2.0.0 + */ +export type Tags = E extends { _tag: string } ? E["_tag"] : never + +/** + * Excludes the tagged object from the type. + * @example + * ```ts + * import type { Types } from "effect" + * + * type Res = Types.ExcludeTag // string | { _tag: "b" } + * ``` + * + * @category types + * @since 2.0.0 + */ +export type ExcludeTag> = Exclude + +/** + * Extracts the type of the given tag. + * + * @example + * ```ts + * import type { Types } from "effect" + * + * type Res = Types.ExtractTag<{ _tag: "a", a: number } | { _tag: "b", b: number }, "b"> // { _tag: "b", b: number } + * ``` + * + * @category types + * @since 2.0.0 + */ +export type ExtractTag> = Extract + +/** + * A utility type that transforms a union type `T` into an intersection type. + * + * @since 2.0.0 + * @category types + */ +export type UnionToIntersection = (T extends any ? (x: T) => any : never) extends (x: infer R) => any ? R + : never + +/** + * Simplifies the type signature of a type. + * + * @example + * ```ts + * import type { Types } from "effect" + * + * type Res = Types.Simplify<{ a: number } & { b: number }> // { a: number; b: number; } + * ``` + * + * @since 2.0.0 + * @category types + */ +export type Simplify = { + [K in keyof A]: A[K] +} extends infer B ? B : never + +/** + * Determines if two types are equal. + * + * @example + * ```ts + * import type { Types } from "effect" + * + * type Res1 = Types.Equals<{ a: number }, { a: number }> // true + * type Res2 = Types.Equals<{ a: number }, { b: number }> // false + * ``` + * + * @since 2.0.0 + * @category models + */ +export type Equals = (() => T extends X ? 1 : 2) extends < + T +>() => T extends Y ? 1 : 2 ? true + : false + +/** + * Determines if two types are equal, allowing to specify the return types. + * + * @since 3.15.0 + * @category models + */ +export type EqualsWith = (() => T extends A ? 1 : 2) extends (() => T extends B ? 1 : 2) ? Y : N + +/** + * Determines if a record contains any of the given keys. + * + * @example + * ```ts + * import type { Types } from "effect" + * + * type Res1 = Types.Has<{ a: number }, "a" | "b"> // true + * type Res2 = Types.Has<{ c: number }, "a" | "b"> // false + * ``` + * + * @since 2.0.0 + * @category models + */ +export type Has = (Key extends infer K ? K extends keyof A ? true : never : never) extends never + ? false + : true + +/** + * Merges two object where the keys of the left object take precedence in the case of a conflict. + * + * @example + * ```ts + * import type { Types } from "effect" + * type MergeLeft = Types.MergeLeft<{ a: number, b: number; }, { a: string }> // { a: number; b: number; } + * ``` + * + * @since 2.0.0 + * @category models + */ +export type MergeLeft = MergeRight + +/** + * Merges two object where the keys of the right object take precedence in the case of a conflict. + * + * @example + * ```ts + * import type { Types } from "effect" + * type MergeRight = Types.MergeRight<{ a: number, b: number; }, { a: string }> // { a: string; b: number; } + * ``` + * + * @since 2.0.0 + * @category models + */ +export type MergeRight = Simplify< + & Source + & { + [Key in keyof Target as Key extends keyof Source ? never : Key]: Target[Key] + } +> + +/** + * @since 2.0.0 + * @category models + */ +export type MergeRecord = MergeLeft + +/** + * Describes the concurrency to use when executing multiple Effect's. + * + * @since 2.0.0 + * @category models + */ +export type Concurrency = number | "unbounded" | "inherit" + +/** + * Make all properties in `T` mutable. Supports arrays, tuples, and records as well. + * + * @example + * ```ts + * import type { Types } from "effect" + * + * type MutableStruct = Types.Mutable<{ readonly a: string; readonly b: number }> // { a: string; b: number; } + * + * type MutableArray = Types.Mutable> // string[] + * + * type MutableTuple = Types.Mutable // [string, number] + * + * type MutableRecord = Types.Mutable<{ readonly [_: string]: number }> // { [x: string]: number; } + * ``` + * + * @since 2.0.0 + * @category types + */ +export type Mutable = { + -readonly [P in keyof T]: T[P] +} + +/** + * Like `Types.Mutable`, but works recursively. + * + * @example + * ```ts + * import type { Types } from "effect" + * + * type DeepMutableStruct = Types.DeepMutable<{ + * readonly a: string; + * readonly b: readonly string[] + * }> + * // { a: string; b: string[] } + * ``` + * + * @since 3.1.0 + * @category types + */ +export type DeepMutable = T extends ReadonlyMap ? Map, DeepMutable> + : T extends ReadonlySet ? Set> + : T extends string | number | boolean | bigint | symbol | Function ? T + : { -readonly [K in keyof T]: DeepMutable } + +/** + * Avoid inference on a specific parameter + * + * @since 2.0.0 + * @category models + */ +export type NoInfer = [A][A extends any ? 0 : never] + +/** + * Invariant helper. + * + * @since 2.0.0 + * @category models + */ +export type Invariant = (_: A) => A + +/** + * @since 3.9.0 + * @category models + */ +export declare namespace Invariant { + /** + * @since 3.9.0 + * @category models + */ + export type Type = A extends Invariant ? U : never +} + +/** + * Covariant helper. + * + * @since 2.0.0 + * @category models + */ +export type Covariant = (_: never) => A + +/** + * @since 3.9.0 + * @category models + */ +export declare namespace Covariant { + /** + * @since 3.9.0 + * @category models + */ + export type Type = A extends Covariant ? U : never +} + +/** + * Contravariant helper. + * + * @since 2.0.0 + * @category models + */ +export type Contravariant = (_: A) => void + +/** + * @since 3.9.0 + * @category models + */ +export declare namespace Contravariant { + /** + * @since 3.9.0 + * @category models + */ + export type Type = A extends Contravariant ? U : never +} + +/** + * @since 2.0.0 + */ +export type MatchRecord = {} extends S ? onTrue : onFalse + +/** + * @since 2.0.0 + */ +export type NotFunction = T extends Function ? never : T + +/** + * @since 3.9.0 + */ +export type NoExcessProperties = T & { readonly [K in Exclude]: never } + +/** + * @since 3.15.0 + */ +export type Ctor = new(...args: Array) => T + +/** + * Conditional type that returns `void` if `S` is an empty object type, + * otherwise returns `S`. + * + * @since 3.19.20 + */ +export type VoidIfEmpty = keyof S extends never ? void : S diff --git a/repos/effect/packages/effect/src/Unify.ts b/repos/effect/packages/effect/src/Unify.ts new file mode 100644 index 0000000..a024a5b --- /dev/null +++ b/repos/effect/packages/effect/src/Unify.ts @@ -0,0 +1,113 @@ +/** + * @since 2.0.0 + */ + +import { identity } from "./Function.js" + +/** + * @since 2.0.0 + */ +export declare const unifySymbol: unique symbol + +/** + * @since 2.0.0 + */ +export type unifySymbol = typeof unifySymbol + +/** + * @since 2.0.0 + */ +export declare const typeSymbol: unique symbol + +/** + * @since 2.0.0 + */ +export type typeSymbol = typeof typeSymbol + +/** + * @since 2.0.0 + */ +export declare const ignoreSymbol: unique symbol + +/** + * @since 2.0.0 + */ +export type ignoreSymbol = typeof ignoreSymbol + +type MaybeReturn = F extends () => infer R ? R : NonNullable + +type Values = X extends [infer A, infer Ignore] + ? Exclude extends infer k ? k extends keyof A ? MaybeReturn : never : never + : never + +type Ignore = X extends { [ignoreSymbol]?: infer Obj } ? keyof NonNullable + : never + +type ExtractTypes< + X +> = X extends { + [typeSymbol]?: infer _Type + [unifySymbol]?: infer _Unify +} ? [NonNullable<_Unify>, Ignore] + : never + +type FilterIn = A extends any ? typeSymbol extends keyof A ? A : never : never + +type FilterOut = A extends any ? typeSymbol extends keyof A ? never : A : never + +/** + * @since 2.0.0 + */ +export type Unify = Values< + ExtractTypes< + ( + & FilterIn + & { [typeSymbol]: A } + ) + > +> extends infer Z ? Z | Exclude | FilterOut : never + +/** + * @since 2.0.0 + */ +export const unify: { + < + Args extends Array, + Args2 extends Array, + Args3 extends Array, + Args4 extends Array, + Args5 extends Array, + T + >( + x: (...args: Args) => (...args: Args2) => (...args: Args3) => (...args: Args4) => (...args: Args5) => T + ): (...args: Args) => (...args: Args2) => (...args: Args3) => (...args: Args4) => (...args: Args5) => Unify + < + Args extends Array, + Args2 extends Array, + Args3 extends Array, + Args4 extends Array, + T + >( + x: (...args: Args) => (...args: Args2) => (...args: Args3) => (...args: Args4) => T + ): (...args: Args) => (...args: Args2) => (...args: Args3) => (...args: Args4) => Unify + < + Args extends Array, + Args2 extends Array, + Args3 extends Array, + T + >( + x: (...args: Args) => (...args: Args2) => (...args: Args3) => T + ): (...args: Args) => (...args: Args2) => (...args: Args3) => Unify + < + Args extends Array, + Args2 extends Array, + T + >( + x: (...args: Args) => (...args: Args2) => T + ): (...args: Args) => (...args: Args2) => Unify + < + Args extends Array, + T + >(x: (...args: Args) => T): (...args: Args) => Unify + (x: T): Unify +} = identity as any diff --git a/repos/effect/packages/effect/src/UpstreamPullRequest.ts b/repos/effect/packages/effect/src/UpstreamPullRequest.ts new file mode 100644 index 0000000..06a363d --- /dev/null +++ b/repos/effect/packages/effect/src/UpstreamPullRequest.ts @@ -0,0 +1,117 @@ +/** + * @since 2.0.0 + */ +import * as internal from "./internal/channel/upstreamPullRequest.js" +import type * as Types from "./Types.js" + +/** + * @since 2.0.0 + * @category symbols + */ +export const UpstreamPullRequestTypeId: unique symbol = internal.UpstreamPullRequestTypeId + +/** + * @since 2.0.0 + * @category symbols + */ +export type UpstreamPullRequestTypeId = typeof UpstreamPullRequestTypeId + +/** + * @since 2.0.0 + * @category models + */ +export type UpstreamPullRequest = Pulled | NoUpstream + +/** + * @since 2.0.0 + */ +export declare namespace UpstreamPullRequest { + /** + * @since 2.0.0 + * @category models + */ + export interface Variance { + readonly [UpstreamPullRequestTypeId]: { + readonly _A: Types.Covariant + } + } +} + +/** + * @since 2.0.0 + * @category models + */ +export interface Pulled extends UpstreamPullRequest.Variance { + readonly _tag: "Pulled" + readonly value: A +} + +/** + * @since 2.0.0 + * @category models + */ +export interface NoUpstream extends UpstreamPullRequest.Variance { + readonly _tag: "NoUpstream" + readonly activeDownstreamCount: number +} + +/** + * @since 2.0.0 + * @category constructors + */ +export const Pulled: (value: A) => UpstreamPullRequest = internal.Pulled + +/** + * @since 2.0.0 + * @category constructors + */ +export const NoUpstream: (activeDownstreamCount: number) => UpstreamPullRequest = internal.NoUpstream + +/** + * Returns `true` if the specified value is an `UpstreamPullRequest`, `false` + * otherwise. + * + * @since 2.0.0 + * @category refinements + */ +export const isUpstreamPullRequest: (u: unknown) => u is UpstreamPullRequest = internal.isUpstreamPullRequest + +/** + * Returns `true` if the specified `UpstreamPullRequest` is a `Pulled`, `false` + * otherwise. + * + * @since 2.0.0 + * @category refinements + */ +export const isPulled: (self: UpstreamPullRequest) => self is Pulled = internal.isPulled + +/** + * Returns `true` if the specified `UpstreamPullRequest` is a `NoUpstream`, + * `false` otherwise. + * + * @since 2.0.0 + * @category refinements + */ +export const isNoUpstream: (self: UpstreamPullRequest) => self is NoUpstream = internal.isNoUpstream + +/** + * Folds an `UpstreamPullRequest` into a value of type `Z`. + * + * @since 2.0.0 + * @category folding + */ +export const match: { + ( + options: { + readonly onPulled: (value: A) => Z + readonly onNoUpstream: (activeDownstreamCount: number) => Z + } + ): (self: UpstreamPullRequest) => Z + ( + self: UpstreamPullRequest, + options: { + readonly onPulled: (value: A) => Z + readonly onNoUpstream: (activeDownstreamCount: number) => Z + } + ): Z +} = internal.match diff --git a/repos/effect/packages/effect/src/UpstreamPullStrategy.ts b/repos/effect/packages/effect/src/UpstreamPullStrategy.ts new file mode 100644 index 0000000..fc6ea45 --- /dev/null +++ b/repos/effect/packages/effect/src/UpstreamPullStrategy.ts @@ -0,0 +1,121 @@ +/** + * @since 2.0.0 + */ +import * as internal from "./internal/channel/upstreamPullStrategy.js" +import type * as Option from "./Option.js" +import type * as Types from "./Types.js" + +/** + * @since 2.0.0 + * @category symbols + */ +export const UpstreamPullStrategyTypeId: unique symbol = internal.UpstreamPullStrategyTypeId + +/** + * @since 2.0.0 + * @category symbols + */ +export type UpstreamPullStrategyTypeId = typeof UpstreamPullStrategyTypeId + +/** + * @since 2.0.0 + * @category models + */ +export type UpstreamPullStrategy = PullAfterNext | PullAfterAllEnqueued + +/** + * @since 2.0.0 + */ +export declare namespace UpstreamPullStrategy { + /** + * @since 2.0.0 + * @category models + */ + export interface Variance { + readonly [UpstreamPullStrategyTypeId]: { + readonly _A: Types.Covariant + } + } +} + +/** + * @since 2.0.0 + * @category models + */ +export interface PullAfterNext extends UpstreamPullStrategy.Variance { + readonly _tag: "PullAfterNext" + readonly emitSeparator: Option.Option +} + +/** + * @since 2.0.0 + * @category models + */ +export interface PullAfterAllEnqueued extends UpstreamPullStrategy.Variance { + readonly _tag: "PullAfterAllEnqueued" + readonly emitSeparator: Option.Option +} + +/** + * @since 2.0.0 + * @category constructors + */ +export const PullAfterNext: (emitSeparator: Option.Option) => UpstreamPullStrategy = internal.PullAfterNext + +/** + * @since 2.0.0 + * @category constructors + */ +export const PullAfterAllEnqueued: (emitSeparator: Option.Option) => UpstreamPullStrategy = + internal.PullAfterAllEnqueued + +/** + * Returns `true` if the specified value is an `UpstreamPullStrategy`, `false` + * otherwise. + * + * @since 2.0.0 + * @category refinements + */ +export const isUpstreamPullStrategy: (u: unknown) => u is UpstreamPullStrategy = + internal.isUpstreamPullStrategy + +/** + * Returns `true` if the specified `UpstreamPullStrategy` is a `PullAfterNext`, + * `false` otherwise. + * + * @since 2.0.0 + * @category refinements + */ +export const isPullAfterNext: (self: UpstreamPullStrategy) => self is PullAfterNext = internal.isPullAfterNext + +/** + * Returns `true` if the specified `UpstreamPullStrategy` is a + * `PullAfterAllEnqueued`, `false` otherwise. + * + * @since 2.0.0 + * @category refinements + */ +export const isPullAfterAllEnqueued: (self: UpstreamPullStrategy) => self is PullAfterAllEnqueued = + internal.isPullAfterAllEnqueued + +/** + * Folds an `UpstreamPullStrategy` into a value of type `Z`. + * + * @since 2.0.0 + * @category folding + */ +export const match: { + ( + options: { + readonly onNext: (emitSeparator: Option.Option) => Z + readonly onAllEnqueued: (emitSeparator: Option.Option) => Z + } + ): (self: UpstreamPullStrategy) => Z + ( + self: UpstreamPullStrategy, + options: { + readonly onNext: (emitSeparator: Option.Option) => Z + readonly onAllEnqueued: (emitSeparator: Option.Option) => Z + } + ): Z +} = internal.match diff --git a/repos/effect/packages/effect/src/Utils.ts b/repos/effect/packages/effect/src/Utils.ts new file mode 100644 index 0000000..ef4a070 --- /dev/null +++ b/repos/effect/packages/effect/src/Utils.ts @@ -0,0 +1,809 @@ +/** + * @since 2.0.0 + */ +import { identity } from "./Function.js" +import { globalValue } from "./GlobalValue.js" +import type { Kind, TypeLambda } from "./HKT.js" +import { getBugErrorMessage } from "./internal/errors.js" +import { isNullable, isObject } from "./Predicate.js" +import type * as Types from "./Types.js" + +/* + * Copyright 2014 Thom Chiovoloni, released under the MIT license. + * + * A random number generator based on the basic implementation of the PCG algorithm, + * as described here: http://www.pcg-random.org/ + * + * Adapted for TypeScript from Thom's original code at https://github.com/thomcc/pcg-random + * + * forked from https://github.com/frptools + * + * @since 2.0.0 + */ + +/** + * @category symbols + * @since 2.0.0 + */ +export const GenKindTypeId: unique symbol = Symbol.for("effect/Gen/GenKind") + +/** + * @category symbols + * @since 2.0.0 + */ +export type GenKindTypeId = typeof GenKindTypeId + +/** + * @category models + * @since 2.0.0 + */ +export interface GenKind extends Variance { + readonly value: Kind + + [Symbol.iterator](): IterableIterator, A> +} + +/** + * @category predicates + * @since 3.0.6 + */ +export const isGenKind = (u: unknown): u is GenKind => isObject(u) && GenKindTypeId in u + +/** + * @category constructors + * @since 2.0.0 + */ +export class GenKindImpl implements GenKind { + constructor( + /** + * @since 2.0.0 + */ + readonly value: Kind + ) {} + + /** + * @since 2.0.0 + */ + get _F() { + return identity + } + + /** + * @since 2.0.0 + */ + get _R() { + return (_: R) => _ + } + + /** + * @since 2.0.0 + */ + get _O() { + return (_: never): O => _ + } + + /** + * @since 2.0.0 + */ + get _E() { + return (_: never): E => _ + } + + /** + * @since 2.0.0 + */ + readonly [GenKindTypeId]: typeof GenKindTypeId = GenKindTypeId; + + /** + * @since 2.0.0 + */ + [Symbol.iterator](): IterableIterator, A> { + return new SingleShotGen, A>(this as any) + } +} + +/** + * @category constructors + * @since 2.0.0 + */ +export class SingleShotGen implements IterableIterator { + private called = false + + constructor(readonly self: T) {} + + /** + * @since 2.0.0 + */ + next(a: A): IteratorResult { + return this.called ? + ({ + value: a, + done: true + }) : + (this.called = true, + ({ + value: this.self, + done: false + })) + } + + /** + * @since 2.0.0 + */ + return(a: A): IteratorResult { + return ({ + value: a, + done: true + }) + } + + /** + * @since 2.0.0 + */ + throw(e: unknown): IteratorResult { + throw e + } + + /** + * @since 2.0.0 + */ + [Symbol.iterator](): IterableIterator { + return new SingleShotGen(this.self) + } +} + +/** + * @category constructors + * @since 2.0.0 + */ +export const makeGenKind = ( + kind: Kind +): GenKind => new GenKindImpl(kind) + +/** + * @category models + * @since 2.0.0 + */ +export interface Variance { + readonly [GenKindTypeId]: GenKindTypeId + readonly _F: Types.Invariant + readonly _R: Types.Contravariant + readonly _O: Types.Covariant + readonly _E: Types.Covariant +} + +/** + * @category models + * @since 2.0.0 + */ +export interface Gen { + | YieldWrap>, A>( + ...args: + | [ + self: Self, + body: (this: Self, resume: Z) => Generator + ] + | [ + body: (resume: Z) => Generator + ] + ): Kind< + F, + [K] extends [Variance] ? R + : [K] extends [YieldWrap>] ? R + : never, + [K] extends [Variance] ? O + : [K] extends [YieldWrap>] ? O + : never, + [K] extends [Variance] ? E + : [K] extends [YieldWrap>] ? E + : never, + A + > +} + +/** + * @category models + * @since 2.0.0 + */ +export interface Adapter { + <_R, _O, _E, _A>( + self: Kind + ): GenKind + (a: A, ab: (a: A) => Kind): GenKind + (a: A, ab: (a: A) => B, bc: (b: B) => Kind): GenKind + ( + a: A, + ab: (a: A) => B, + bc: (b: B) => C, + cd: (c: C) => Kind + ): GenKind + ( + a: A, + ab: (a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D, + de: (d: D) => Kind + ): GenKind + ( + a: A, + ab: (a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D, + de: (d: D) => E, + ef: (e: E) => Kind + ): GenKind + ( + a: A, + ab: (a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D, + de: (d: D) => E, + ef: (e: E) => F, + fg: (f: F) => Kind + ): GenKind + ( + a: A, + ab: (a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D, + de: (d: D) => E, + ef: (e: E) => F, + fg: (f: F) => G, + gh: (g: F) => Kind + ): GenKind + ( + a: A, + ab: (a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D, + de: (d: D) => E, + ef: (e: E) => F, + fg: (f: F) => G, + gh: (g: G) => H, + hi: (g: H) => Kind + ): GenKind + ( + a: A, + ab: (a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D, + de: (d: D) => E, + ef: (e: E) => F, + fg: (f: F) => G, + gh: (g: G) => H, + hi: (h: H) => I, + ij: (i: I) => Kind + ): GenKind + ( + a: A, + ab: (a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D, + de: (d: D) => E, + ef: (e: E) => F, + fg: (f: F) => G, + gh: (g: G) => H, + hi: (h: H) => I, + ij: (i: I) => J, + jk: (j: J) => Kind + ): GenKind + ( + a: A, + ab: (a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D, + de: (d: D) => E, + ef: (e: E) => F, + fg: (f: F) => G, + gh: (g: G) => H, + hi: (h: H) => I, + ij: (i: I) => J, + jk: (j: J) => K, + kl: (k: K) => Kind + ): GenKind + ( + a: A, + ab: (a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D, + de: (d: D) => E, + ef: (e: E) => F, + fg: (f: F) => G, + gh: (g: G) => H, + hi: (h: H) => I, + ij: (i: I) => J, + jk: (j: J) => K, + kl: (k: K) => L, + lm: (l: L) => Kind + ): GenKind + ( + a: A, + ab: (a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D, + de: (d: D) => E, + ef: (e: E) => F, + fg: (f: F) => G, + gh: (g: G) => H, + hi: (h: H) => I, + ij: (i: I) => J, + jk: (j: J) => K, + kl: (k: K) => L, + lm: (l: L) => M, + mn: (m: M) => Kind + ): GenKind + ( + a: A, + ab: (a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D, + de: (d: D) => E, + ef: (e: E) => F, + fg: (f: F) => G, + gh: (g: G) => H, + hi: (h: H) => I, + ij: (i: I) => J, + jk: (j: J) => K, + kl: (k: K) => L, + lm: (l: L) => M, + mn: (m: M) => N, + no: (n: N) => Kind + ): GenKind + ( + a: A, + ab: (a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D, + de: (d: D) => E, + ef: (e: E) => F, + fg: (f: F) => G, + gh: (g: G) => H, + hi: (h: H) => I, + ij: (i: I) => J, + jk: (j: J) => K, + kl: (k: K) => L, + lm: (l: L) => M, + mn: (m: M) => N, + no: (n: N) => O, + op: (o: O) => Kind + ): GenKind + ( + a: A, + ab: (a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D, + de: (d: D) => E, + ef: (e: E) => F, + fg: (f: F) => G, + gh: (g: G) => H, + hi: (h: H) => I, + ij: (i: I) => J, + jk: (j: J) => K, + kl: (k: K) => L, + lm: (l: L) => M, + mn: (m: M) => N, + no: (n: N) => O, + op: (o: O) => P, + pq: (p: P) => Kind + ): GenKind + ( + a: A, + ab: (a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D, + de: (d: D) => E, + ef: (e: E) => F, + fg: (f: F) => G, + gh: (g: G) => H, + hi: (h: H) => I, + ij: (i: I) => J, + jk: (j: J) => K, + kl: (k: K) => L, + lm: (l: L) => M, + mn: (m: M) => N, + no: (n: N) => O, + op: (o: O) => P, + pq: (p: P) => Q, + qr: (q: Q) => Kind + ): GenKind + ( + a: A, + ab: (a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D, + de: (d: D) => E, + ef: (e: E) => F, + fg: (f: F) => G, + gh: (g: G) => H, + hi: (h: H) => I, + ij: (i: I) => J, + jk: (j: J) => K, + kl: (k: K) => L, + lm: (l: L) => M, + mn: (m: M) => N, + no: (n: N) => O, + op: (o: O) => P, + pq: (p: P) => Q, + qr: (q: Q) => R, + rs: (r: R) => Kind + ): GenKind + ( + a: A, + ab: (a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D, + de: (d: D) => E, + ef: (e: E) => F, + fg: (f: F) => G, + gh: (g: G) => H, + hi: (h: H) => I, + ij: (i: I) => J, + jk: (j: J) => K, + kl: (k: K) => L, + lm: (l: L) => M, + mn: (m: M) => N, + no: (n: N) => O, + op: (o: O) => P, + pq: (p: P) => Q, + qr: (q: Q) => R, + rs: (r: R) => S, + st: (s: S) => Kind + ): GenKind + ( + a: A, + ab: (a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D, + de: (d: D) => E, + ef: (e: E) => F, + fg: (f: F) => G, + gh: (g: G) => H, + hi: (h: H) => I, + ij: (i: I) => J, + jk: (j: J) => K, + kl: (k: K) => L, + lm: (l: L) => M, + mn: (m: M) => N, + no: (n: N) => O, + op: (o: O) => P, + pq: (p: P) => Q, + qr: (q: Q) => R, + rs: (r: R) => S, + st: (s: S) => T, + tu: (s: T) => Kind + ): GenKind +} + +/** + * @category adapters + * @since 2.0.0 + */ +export const adapter: () => Adapter = () => (function() { + let x = arguments[0] + for (let i = 1; i < arguments.length; i++) { + x = arguments[i](x) + } + return new GenKindImpl(x) as any +}) + +const defaultIncHi = 0x14057b7e +const defaultIncLo = 0xf767814f +const MUL_HI = 0x5851f42d >>> 0 +const MUL_LO = 0x4c957f2d >>> 0 +const BIT_53 = 9007199254740992.0 +const BIT_27 = 134217728.0 + +/** + * @category model + * @since 2.0.0 + */ +export type PCGRandomState = [number, number, number, number] + +/** + * @category model + * @since 2.0.0 + */ +export type OptionalNumber = number | null | undefined + +/** + * PCG is a family of simple fast space-efficient statistically good algorithms + * for random number generation. Unlike many general-purpose RNGs, they are also + * hard to predict. + * + * @category model + * @since 2.0.0 + */ +export class PCGRandom { + private _state!: Int32Array + + /** + * Creates an instance of PCGRandom. + * + * - `seed` - The low 32 bits of the seed (0 is used for high 32 bits). + * + * @memberOf PCGRandom + */ + constructor(seed?: OptionalNumber) + /** + * Creates an instance of PCGRandom. + * + * - `seedHi` - The high 32 bits of the seed. + * - `seedLo` - The low 32 bits of the seed. + * - `inc` - The low 32 bits of the incrementer (0 is used for high 32 bits). + * + * @memberOf PCGRandom + */ + constructor(seedHi: OptionalNumber, seedLo: OptionalNumber, inc?: OptionalNumber) + /** + * Creates an instance of PCGRandom. + * + * - `seedHi` - The high 32 bits of the seed. + * - `seedLo` - The low 32 bits of the seed. + * - `incHi` - The high 32 bits of the incrementer. + * - `incLo` - The low 32 bits of the incrementer. + * + * @memberOf PCGRandom + */ + constructor( + seedHi: OptionalNumber, + seedLo: OptionalNumber, + incHi: OptionalNumber, + incLo: OptionalNumber + ) + constructor( + seedHi?: OptionalNumber, + seedLo?: OptionalNumber, + incHi?: OptionalNumber, + incLo?: OptionalNumber + ) { + if (isNullable(seedLo) && isNullable(seedHi)) { + seedLo = (Math.random() * 0xffffffff) >>> 0 + seedHi = 0 + } else if (isNullable(seedLo)) { + seedLo = seedHi + seedHi = 0 + } + if (isNullable(incLo) && isNullable(incHi)) { + incLo = this._state ? this._state[3] : defaultIncLo + incHi = this._state ? this._state[2] : defaultIncHi + } else if (isNullable(incLo)) { + incLo = incHi + incHi = 0 + } + + this._state = new Int32Array([0, 0, ( incHi) >>> 0, ((incLo || 0) | 1) >>> 0]) + this._next() + add64( + this._state, + this._state[0]!, + this._state[1]!, + ( seedHi) >>> 0, + ( seedLo) >>> 0 + ) + this._next() + return this + } + + /** + * Returns a copy of the internal state of this random number generator as a + * JavaScript Array. + * + * @category getters + * @since 2.0.0 + */ + getState(): PCGRandomState { + return [this._state[0]!, this._state[1]!, this._state[2]!, this._state[3]!] + } + + /** + * Restore state previously retrieved using `getState()`. + * + * @since 2.0.0 + */ + setState(state: PCGRandomState) { + this._state[0] = state[0] + this._state[1] = state[1] + this._state[2] = state[2] + this._state[3] = state[3] | 1 + } + + /** + * Get a uniformly distributed 32 bit integer between [0, max). + * + * @category getter + * @since 2.0.0 + */ + integer(max: number) { + return Math.round(this.number() * Number.MAX_SAFE_INTEGER) % max + } + + /** + * Get a uniformly distributed IEEE-754 double between 0.0 and 1.0, with + * 53 bits of precision (every bit of the mantissa is randomized). + * + * @category getters + * @since 2.0.0 + */ + number() { + const hi = (this._next() & 0x03ffffff) * 1.0 + const lo = (this._next() & 0x07ffffff) * 1.0 + return (hi * BIT_27 + lo) / BIT_53 + } + + /** @internal */ + private _next() { + // save current state (what we'll use for this number) + const oldHi = this._state[0]! >>> 0 + const oldLo = this._state[1]! >>> 0 + + // churn LCG. + mul64(this._state, oldHi, oldLo, MUL_HI, MUL_LO) + add64(this._state, this._state[0]!, this._state[1]!, this._state[2]!, this._state[3]!) + + // get least sig. 32 bits of ((oldstate >> 18) ^ oldstate) >> 27 + let xsHi = oldHi >>> 18 + let xsLo = ((oldLo >>> 18) | (oldHi << 14)) >>> 0 + xsHi = (xsHi ^ oldHi) >>> 0 + xsLo = (xsLo ^ oldLo) >>> 0 + const xorshifted = ((xsLo >>> 27) | (xsHi << 5)) >>> 0 + // rotate xorshifted right a random amount, based on the most sig. 5 bits + // bits of the old state. + const rot = oldHi >>> 27 + const rot2 = ((-rot >>> 0) & 31) >>> 0 + return ((xorshifted >>> rot) | (xorshifted << rot2)) >>> 0 + } +} + +function mul64( + out: Int32Array, + aHi: number, + aLo: number, + bHi: number, + bLo: number +): void { + let c1 = ((aLo >>> 16) * (bLo & 0xffff)) >>> 0 + let c0 = ((aLo & 0xffff) * (bLo >>> 16)) >>> 0 + + let lo = ((aLo & 0xffff) * (bLo & 0xffff)) >>> 0 + let hi = ((aLo >>> 16) * (bLo >>> 16) + ((c0 >>> 16) + (c1 >>> 16))) >>> 0 + + c0 = (c0 << 16) >>> 0 + lo = (lo + c0) >>> 0 + if ((lo >>> 0) < (c0 >>> 0)) { + hi = (hi + 1) >>> 0 + } + + c1 = (c1 << 16) >>> 0 + lo = (lo + c1) >>> 0 + if ((lo >>> 0) < (c1 >>> 0)) { + hi = (hi + 1) >>> 0 + } + + hi = (hi + Math.imul(aLo, bHi)) >>> 0 + hi = (hi + Math.imul(aHi, bLo)) >>> 0 + + out[0] = hi + out[1] = lo +} + +// add two 64 bit numbers (given in parts), and store the result in `out`. +function add64( + out: Int32Array, + aHi: number, + aLo: number, + bHi: number, + bLo: number +): void { + let hi = (aHi + bHi) >>> 0 + const lo = (aLo + bLo) >>> 0 + if ((lo >>> 0) < (aLo >>> 0)) { + hi = (hi + 1) | 0 + } + out[0] = hi + out[1] = lo +} + +/** + * @since 3.0.6 + */ +export const YieldWrapTypeId: unique symbol = Symbol.for("effect/Utils/YieldWrap") + +/** + * @since 3.0.6 + */ +export class YieldWrap { + /** + * @since 3.0.6 + */ + readonly #value: T + constructor(value: T) { + this.#value = value + } + /** + * @since 3.0.6 + */ + [YieldWrapTypeId](): T { + return this.#value + } +} + +/** + * @since 3.0.6 + */ +export function yieldWrapGet(self: YieldWrap): T { + if (typeof self === "object" && self !== null && YieldWrapTypeId in self) { + return self[YieldWrapTypeId]() + } + throw new Error(getBugErrorMessage("yieldWrapGet")) +} + +/** + * Note: this is an experimental feature made available to allow custom matchers in tests, not to be directly used yet in user code + * + * @since 3.1.1 + * @status experimental + * @category modifiers + */ +export const structuralRegionState = globalValue( + "effect/Utils/isStructuralRegion", + (): { enabled: boolean; tester: ((a: unknown, b: unknown) => boolean) | undefined } => ({ + enabled: false, + tester: undefined + }) +) + +/** + * Note: this is an experimental feature made available to allow custom matchers in tests, not to be directly used yet in user code + * + * @since 3.1.1 + * @status experimental + * @category modifiers + */ +export const structuralRegion = (body: () => A, tester?: (a: unknown, b: unknown) => boolean): A => { + const current = structuralRegionState.enabled + const currentTester = structuralRegionState.tester + structuralRegionState.enabled = true + if (tester) { + structuralRegionState.tester = tester + } + try { + return body() + } finally { + structuralRegionState.enabled = current + structuralRegionState.tester = currentTester + } +} + +const standard = { + effect_internal_function: (body: () => A) => { + return body() + } +} + +const forced = { + effect_internal_function: (body: () => A) => { + try { + return body() + } finally { + // + } + } +} + +const isNotOptimizedAway = + standard.effect_internal_function(() => new Error().stack)?.includes("effect_internal_function") === true + +/** + * @since 3.2.2 + * @status experimental + * @category tracing + */ +export const internalCall = isNotOptimizedAway ? standard.effect_internal_function : forced.effect_internal_function + +const genConstructor = (function*() {}).constructor + +/** + * @since 3.11.0 + */ +export const isGeneratorFunction = (u: unknown): u is (...args: Array) => Generator => + isObject(u) && u.constructor === genConstructor diff --git a/repos/effect/packages/effect/src/index.ts b/repos/effect/packages/effect/src/index.ts new file mode 100644 index 0000000..d1a7c98 --- /dev/null +++ b/repos/effect/packages/effect/src/index.ts @@ -0,0 +1,1561 @@ +/** + * @since 2.0.0 + */ + +export { + /** + * @since 2.0.0 + */ + absurd, + /** + * @since 2.0.0 + */ + flow, + /** + * @since 2.0.0 + */ + hole, + /** + * @since 2.0.0 + */ + identity, + /** + * @since 2.0.0 + */ + pipe, + /** + * @since 2.0.0 + */ + unsafeCoerce +} from "./Function.js" + +/** + * @since 3.10.0 + */ +export * as Arbitrary from "./Arbitrary.js" + +/** + * This module provides utility functions for working with arrays in TypeScript. + * + * @since 2.0.0 + */ +export * as Array from "./Array.js" + +/** + * This module provides utility functions and type class instances for working with the `BigDecimal` type in TypeScript. + * It includes functions for basic arithmetic operations, as well as type class instances for `Equivalence` and `Order`. + * + * A `BigDecimal` allows storing any real number to arbitrary precision; which avoids common floating point errors + * (such as 0.1 + 0.2 ≠ 0.3) at the cost of complexity. + * + * Internally, `BigDecimal` uses a `BigInt` object, paired with a 64-bit integer which determines the position of the + * decimal point. Therefore, the precision *is not* actually arbitrary, but limited to 263 decimal places. + * + * It is not recommended to convert a floating point number to a decimal directly, as the floating point representation + * may be unexpected. + * + * @module BigDecimal + * @since 2.0.0 + * @see {@link module:BigInt} for more similar operations on `bigint` types + * @see {@link module:Number} for more similar operations on `number` types + */ +export * as BigDecimal from "./BigDecimal.js" + +/** + * This module provides utility functions and type class instances for working with the `bigint` type in TypeScript. + * It includes functions for basic arithmetic operations, as well as type class instances for + * `Equivalence` and `Order`. + * + * @module BigInt + * @since 2.0.0 + * @see {@link module:BigDecimal} for more similar operations on `BigDecimal` types + * @see {@link module:Number} for more similar operations on `number` types + */ +export * as BigInt from "./BigInt.js" + +/** + * This module provides utility functions and type class instances for working with the `boolean` type in TypeScript. + * It includes functions for basic boolean operations, as well as type class instances for + * `Equivalence` and `Order`. + * + * @since 2.0.0 + */ +export * as Boolean from "./Boolean.js" + +/** + * This module provides types and utility functions to create and work with branded types, + * which are TypeScript types with an added type tag to prevent accidental usage of a value in the wrong context. + * + * The `refined` and `nominal` functions are both used to create branded types in TypeScript. + * The main difference between them is that `refined` allows for validation of the data, while `nominal` does not. + * + * The `nominal` function is used to create a new branded type that has the same underlying type as the input, but with a different name. + * This is useful when you want to distinguish between two values of the same type that have different meanings. + * The `nominal` function does not perform any validation of the input data. + * + * On the other hand, the `refined` function is used to create a new branded type that has the same underlying type as the input, + * but with a different name, and it also allows for validation of the input data. + * The `refined` function takes a predicate that is used to validate the input data. + * If the input data fails the validation, a `BrandErrors` is returned, which provides information about the specific validation failure. + * + * @since 2.0.0 + */ +export * as Brand from "./Brand.js" + +/** + * @since 2.0.0 + */ +export * as Cache from "./Cache.js" + +/** + * The `Effect` type is polymorphic in values of type `E` and we can + * work with any error type that we want. However, there is a lot of information + * that is not inside an arbitrary `E` value. So as a result, an `Effect` needs + * somewhere to store things like unexpected errors or defects, stack and + * execution traces, causes of fiber interruptions, and so forth. + * + * Effect-TS is very strict about preserving the full information related to a + * failure. It captures all type of errors into the `Cause` data type. `Effect` + * uses the `Cause` data type to store the full story of failure. So its + * error model is lossless. It doesn't throw information related to the failure + * result. So we can figure out exactly what happened during the operation of + * our effects. + * + * It is important to note that `Cause` is an underlying data type representing + * errors occuring within an `Effect` workflow. Thus, we don't usually deal with + * `Cause`s directly. Even though it is not a data type that we deal with very + * often, the `Cause` of a failing `Effect` workflow can be accessed at any + * time, which gives us total access to all parallel and sequential errors in + * occurring within our codebase. + * + * @since 2.0.0 + */ +export * as Cause from "./Cause.js" + +/** + * @since 2.0.0 + */ +export * as Channel from "./Channel.js" + +/** + * @since 2.0.0 + */ +export * as ChildExecutorDecision from "./ChildExecutorDecision.js" + +/** + * @since 2.0.0 + */ +export * as Chunk from "./Chunk.js" + +/** + * @since 2.0.0 + */ +export * as Clock from "./Clock.js" + +/** + * @since 2.0.0 + */ +export * as Config from "./Config.js" + +/** + * @since 2.0.0 + */ +export * as ConfigError from "./ConfigError.js" + +/** + * @since 2.0.0 + */ +export * as ConfigProvider from "./ConfigProvider.js" + +/** + * @since 2.0.0 + */ +export * as ConfigProviderPathPatch from "./ConfigProviderPathPatch.js" + +/** + * @since 2.0.0 + */ +export * as Console from "./Console.js" + +/** + * This module provides a data structure called `Context` that can be used for dependency injection in effectful + * programs. It is essentially a table mapping `Tag`s to their implementations (called `Service`s), and can be used to + * manage dependencies in a type-safe way. The `Context` data structure is essentially a way of providing access to a set + * of related services that can be passed around as a single unit. This module provides functions to create, modify, and + * query the contents of a `Context`, as well as a number of utility types for working with tags and services. + * + * @since 2.0.0 + */ +export * as Context from "./Context.js" + +/** + * @since 2.0.0 + */ +export * as Cron from "./Cron.js" + +/** + * @since 2.0.0 + */ +export * as Data from "./Data.js" + +/** + * @since 3.6.0 + */ +export * as DateTime from "./DateTime.js" + +/** + * @since 2.0.0 + */ +export * as DefaultServices from "./DefaultServices.js" + +/** + * @since 2.0.0 + */ +export * as Deferred from "./Deferred.js" + +/** + * @since 2.0.0 + */ +export * as Differ from "./Differ.js" + +/** + * @since 2.0.0 + */ +export * as Duration from "./Duration.js" + +/** + * @since 2.0.0 + */ +export * as Effect from "./Effect.js" + +/** + * @since 2.0.0 + */ +export * as Effectable from "./Effectable.js" + +/** + * @since 2.0.0 + */ +export * as Either from "./Either.js" + +/** + * This module provides encoding & decoding functionality for: + * + * - base64 (RFC4648) + * - base64 (URL) + * - hex + * + * @since 2.0.0 + */ +export * as Encoding from "./Encoding.js" + +/** + * @since 2.0.0 + */ +export * as Equal from "./Equal.js" + +/** + * This module provides an implementation of the `Equivalence` type class, which defines a binary relation + * that is reflexive, symmetric, and transitive. In other words, it defines a notion of equivalence between values of a certain type. + * These properties are also known in mathematics as an "equivalence relation". + * + * @since 2.0.0 + */ +export * as Equivalence from "./Equivalence.js" + +/** + * @since 3.16.0 + * @experimental + */ +export * as ExecutionPlan from "./ExecutionPlan.js" + +/** + * @since 2.0.0 + */ +export * as ExecutionStrategy from "./ExecutionStrategy.js" + +/** + * @since 2.0.0 + */ +export * as Exit from "./Exit.js" + +/** + * @since 3.10.0 + */ +export * as FastCheck from "./FastCheck.js" + +/** + * @since 2.0.0 + */ +export * as Fiber from "./Fiber.js" + +/** + * @since 2.0.0 + */ +export * as FiberHandle from "./FiberHandle.js" + +/** + * @since 2.0.0 + */ +export * as FiberId from "./FiberId.js" + +/** + * @since 2.0.0 + */ +export * as FiberMap from "./FiberMap.js" + +/** + * @since 2.0.0 + */ +export * as FiberRef from "./FiberRef.js" + +/** + * @since 2.0.0 + */ +export * as FiberRefs from "./FiberRefs.js" + +/** + * @since 2.0.0 + */ +export * as FiberRefsPatch from "./FiberRefsPatch.js" + +/** + * @since 2.0.0 + */ +export * as FiberSet from "./FiberSet.js" + +/** + * @since 2.0.0 + */ +export * as FiberStatus from "./FiberStatus.js" + +/** + * @since 2.0.0 + */ +export * as Function from "./Function.js" + +/** + * The `GlobalValue` module ensures that a single instance of a value is created globally, + * even when modules are imported multiple times (e.g., due to mixing CommonJS and ESM builds) + * or during hot-reloading in development environments like Next.js or Remix. + * + * It achieves this by using a versioned global store, identified by a unique `Symbol` tied to + * the current version of the `effect` library. The store holds values that are keyed by an identifier, + * allowing the reuse of previously computed instances across imports or reloads. + * + * This pattern is particularly useful in scenarios where frequent reloading can cause services or + * single-instance objects to be recreated unnecessarily, such as in development environments with hot-reloading. + * + * @since 2.0.0 + */ +export * as GlobalValue from "./GlobalValue.js" + +/** + * @experimental + * @since 3.18.0 + */ +export * as Graph from "./Graph.js" + +/** + * @since 2.0.0 + */ +export * as GroupBy from "./GroupBy.js" + +/** + * @since 2.0.0 + */ +export * as HKT from "./HKT.js" + +/** + * @since 2.0.0 + */ +export * as Hash from "./Hash.js" + +/** + * @since 2.0.0 + */ +export * as HashMap from "./HashMap.js" + +/** + * @since 3.19.0 + * @experimental + */ +export * as HashRing from "./HashRing.js" + +/** + * # HashSet + * + * An immutable `HashSet` provides a collection of unique values with efficient + * lookup, insertion and removal. Once created, a `HashSet` cannot be modified; + * any operation that would alter the set instead returns a new `HashSet` with + * the changes. This immutability offers benefits like predictable state + * management and easier reasoning about your code. + * + * ## What Problem Does It Solve? + * + * `HashSet` solves the problem of maintaining an unsorted collection where each + * value appears exactly once, with fast operations for checking membership and + * adding/removing values. + * + * ## When to Use + * + * Use `HashSet` when you need: + * + * - A collection with no duplicate values + * - Efficient membership testing (**`O(1)`** average complexity) + * - Set operations like union, intersection, and difference + * - An immutable data structure that preserves functional programming patterns + * + * ## Advanced Features + * + * HashSet provides operations for: + * + * - Transforming sets with map and flatMap + * - Filtering elements with filter + * - Combining sets with union, intersection and difference + * - Performance optimizations via mutable operations in controlled contexts + * + * ## Performance Characteristics + * + * - **Lookup** operations ({@link module:HashSet.has}): **`O(1)`** average time + * complexity + * - **Insertion** operations ({@link module:HashSet.add}): **`O(1)`** average time + * complexity + * - **Removal** operations ({@link module:HashSet.remove}): **`O(1)`** average + * time complexity + * - **Set** operations ({@link module:HashSet.union}, + * {@link module:HashSet.intersection}): **`O(n)`** where n is the size of the + * smaller set + * - **Iteration**: **`O(n)`** where n is the size of the set + * + * The HashSet data structure implements the following traits: + * + * - {@link Iterable}: allows iterating over the values in the set + * - {@link Equal}: allows comparing two sets for value-based equality + * - {@link Pipeable}: allows chaining operations with the pipe operator + * - {@link Inspectable}: allows inspecting the contents of the set + * + * ## Operations Reference + * + * | Category | Operation | Description | Complexity | + * | ------------ | ----------------------------------- | ------------------------------------------- | ---------- | + * | constructors | {@link module:HashSet.empty} | Creates an empty HashSet | O(1) | + * | constructors | {@link module:HashSet.fromIterable} | Creates a HashSet from an iterable | O(n) | + * | constructors | {@link module:HashSet.make} | Creates a HashSet from multiple values | O(n) | + * | | | | | + * | elements | {@link module:HashSet.has} | Checks if a value exists in the set | O(1) avg | + * | elements | {@link module:HashSet.some} | Checks if any element satisfies a predicate | O(n) | + * | elements | {@link module:HashSet.every} | Checks if all elements satisfy a predicate | O(n) | + * | elements | {@link module:HashSet.isSubset} | Checks if a set is a subset of another | O(n) | + * | | | | | + * | getters | {@link module:HashSet.values} | Gets an iterator of all values | O(1) | + * | getters | {@link module:HashSet.toValues} | Gets an array of all values | O(n) | + * | getters | {@link module:HashSet.size} | Gets the number of elements | O(1) | + * | | | | | + * | mutations | {@link module:HashSet.add} | Adds a value to the set | O(1) avg | + * | mutations | {@link module:HashSet.remove} | Removes a value from the set | O(1) avg | + * | mutations | {@link module:HashSet.toggle} | Toggles a value's presence | O(1) avg | + * | | | | | + * | operations | {@link module:HashSet.difference} | Computes set difference (A - B) | O(n) | + * | operations | {@link module:HashSet.intersection} | Computes set intersection (A ∩ B) | O(n) | + * | operations | {@link module:HashSet.union} | Computes set union (A ∪ B) | O(n) | + * | | | | | + * | mapping | {@link module:HashSet.map} | Transforms each element | O(n) | + * | | | | | + * | sequencing | {@link module:HashSet.flatMap} | Transforms and flattens elements | O(n) | + * | | | | | + * | traversing | {@link module:HashSet.forEach} | Applies a function to each element | O(n) | + * | | | | | + * | folding | {@link module:HashSet.reduce} | Reduces the set to a single value | O(n) | + * | | | | | + * | filtering | {@link module:HashSet.filter} | Keeps elements that satisfy a predicate | O(n) | + * | | | | | + * | partitioning | {@link module:HashSet.partition} | Splits into two sets by a predicate | O(n) | + * + * ## Notes + * + * ### Composability with the Effect Ecosystem: + * + * This `HashSet` is designed to work seamlessly within the Effect ecosystem. It + * implements the {@link Iterable}, {@link Equal}, {@link Pipeable}, and + * {@link Inspectable} traits from Effect. This ensures compatibility with other + * Effect data structures and functionalities. For example, you can easily use + * Effect's `pipe` method to chain operations on the `HashSet`. + * + * **Equality of Elements with Effect's {@link Equal `Equal`} Trait:** + * + * This `HashSet` relies on Effect's {@link Equal} trait to determine the + * uniqueness of elements within the set. The way equality is checked depends on + * the type of the elements: + * + * - **Primitive Values:** For primitive JavaScript values like strings, numbers, + * booleans, `null`, and `undefined`, equality is determined by their value + * (similar to the `===` operator). + * - **Objects and Custom Types:** For objects and other custom types, equality is + * determined by whether those types implement the {@link Equal} interface + * themselves. If an element type implements `Equal`, the `HashSet` will + * delegate to that implementation to perform the equality check. This allows + * you to define custom logic for determining when two instances of your + * objects should be considered equal based on their properties, rather than + * just their object identity. + * + * ```ts + * import { Equal, Hash, HashSet } from "effect" + * + * class Person implements Equal.Equal { + * constructor( + * readonly id: number, // Unique identifier + * readonly name: string, + * readonly age: number + * ) {} + * + * // Define equality based on id, name, and age + * [Equal.symbol](that: Equal.Equal): boolean { + * if (that instanceof Person) { + * return ( + * Equal.equals(this.id, that.id) && + * Equal.equals(this.name, that.name) && + * Equal.equals(this.age, that.age) + * ) + * } + * return false + * } + * + * // Generate a hash code based on the unique id + * [Hash.symbol](): number { + * return Hash.hash(this.id) + * } + * } + * + * // Creating a HashSet with objects that implement the Equal interface + * const set = HashSet.empty().pipe( + * HashSet.add(new Person(1, "Alice", 30)), + * HashSet.add(new Person(1, "Alice", 30)) + * ) + * + * // HashSet recognizes them as equal, so only one element is stored + * console.log(HashSet.size(set)) + * // Output: 1 + * ``` + * + * **Simplifying Equality and Hashing with `Data` and `Schema`:** + * + * Effect's {@link Data} and {@link Schema `Schema.Data`} modules offer powerful + * ways to automatically handle the implementation of both the {@link Equal} and + * {@link Hash} traits for your custom data structures. + * + * - **`Data` Module:** By using constructors like `Data.struct`, `Data.tuple`, + * `Data.array`, or `Data.case` to define your data types, Effect + * automatically generates the necessary implementations for value-based + * equality and consistent hashing. This significantly reduces boilerplate and + * ensures correctness. + * + * ```ts + * import { HashSet, Data, Equal } from "effect" + * import assert from "node:assert/strict" + * + * // Data.* implements the `Equal` traits for us + * const person1 = Data.struct({ id: 1, name: "Alice", age: 30 }) + * const person2 = Data.struct({ id: 1, name: "Alice", age: 30 }) + * + * assert(Equal.equals(person1, person2)) + * + * const set = HashSet.empty().pipe( + * HashSet.add(person1), + * HashSet.add(person2) + * ) + * + * // HashSet recognizes them as equal, so only one element is stored + * console.log(HashSet.size(set)) // Output: 1 + * ``` + * + * - **`Schema` Module:** When defining data schemas using the {@link Schema} + * module, you can use `Schema.Data` to automatically include the `Equal` and + * `Hash` traits in the decoded objects. This is particularly important when + * working with `HashSet`. **For decoded objects to be correctly recognized as + * equal within a `HashSet`, ensure that the schema for those objects is + * defined using `Schema.Data`.** + * + * ```ts + * import { Equal, HashSet, Schema } from "effect" + * import assert from "node:assert/strict" + * + * // Schema.Data implements the `Equal` traits for us + * const PersonSchema = Schema.Data( + * Schema.Struct({ + * id: Schema.Number, + * name: Schema.String, + * age: Schema.Number + * }) + * ) + * + * const Person = Schema.decode(PersonSchema) + * + * const person1 = Person({ id: 1, name: "Alice", age: 30 }) + * const person2 = Person({ id: 1, name: "Alice", age: 30 }) + * + * assert(Equal.equals(person1, person2)) // Output: true + * + * const set = HashSet.empty().pipe( + * HashSet.add(person1), + * HashSet.add(person2) + * ) + * + * // HashSet thanks to Schema.Data implementation of the `Equal` trait, recognizes the two Person as equal, so only one element is stored + * console.log(HashSet.size(set)) // Output: 1 + * ``` + * + * ### Interoperability with the JavaScript Runtime: + * + * To interoperate with the regular JavaScript runtime, Effect's `HashSet` + * provides methods to access its elements in formats readily usable by + * JavaScript APIs: {@link values `HashSet.values`}, + * {@link toValues `HashSet.toValues`} + * + * ```ts + * import { HashSet } from "effect" + * + * const hashSet: HashSet.HashSet = HashSet.make(1, 2, 3) + * + * // Using HashSet.values to convert HashSet.HashSet to IterableIterator + * const iterable: IterableIterator = HashSet.values(hashSet) + * + * console.log(...iterable) // Logs: 1 2 3 + * + * // Using HashSet.toValues to convert HashSet.HashSet to Array + * const array: Array = HashSet.toValues(hashSet) + * + * console.log(array) // Logs: [ 1, 2, 3 ] + * ``` + * + * Be mindful of performance implications (both time and space complexity) when + * frequently converting between Effect's immutable HashSet and mutable + * JavaScript data structures, especially for large collections. + * + * @module HashSet + * @since 2.0.0 + */ +export * as HashSet from "./HashSet.js" + +/** + * @since 2.0.0 + */ +export * as Inspectable from "./Inspectable.js" + +/** + * This module provides utility functions for working with Iterables in TypeScript. + * + * @since 2.0.0 + */ +export * as Iterable from "./Iterable.js" + +/** + * @since 3.10.0 + */ +export * as JSONSchema from "./JSONSchema.js" + +/** + * @since 2.0.0 + */ +export * as KeyedPool from "./KeyedPool.js" + +/** + * A `Layer` describes how to build one or more services in your + * application. Services can be injected into effects via + * `Effect.provideService`. Effects can require services via `Effect.service`. + * + * Layer can be thought of as recipes for producing bundles of services, given + * their dependencies (other services). + * + * Construction of services can be effectful and utilize resources that must be + * acquired and safely released when the services are done being utilized. + * + * By default layers are shared, meaning that if the same layer is used twice + * the layer will only be allocated a single time. + * + * Because of their excellent composition properties, layers are the idiomatic + * way in Effect-TS to create services that depend on other services. + * + * @since 2.0.0 + */ +export * as Layer from "./Layer.js" + +/** + * @since 3.14.0 + * @experimental + */ +export * as LayerMap from "./LayerMap.js" + +/** + * A data type for immutable linked lists representing ordered collections of elements of type `A`. + * + * This data type is optimal for last-in-first-out (LIFO), stack-like access patterns. If you need another access pattern, for example, random access or FIFO, consider using a collection more suited to this than `List`. + * + * **Performance** + * + * - Time: `List` has `O(1)` prepend and head/tail access. Most other operations are `O(n)` on the number of elements in the list. This includes the index-based lookup of elements, `length`, `append` and `reverse`. + * - Space: `List` implements structural sharing of the tail list. This means that many operations are either zero- or constant-memory cost. + * + * @since 2.0.0 + */ +export * as List from "./List.js" + +/** + * @since 2.0.0 + */ +export * as LogLevel from "./LogLevel.js" + +/** + * @since 2.0.0 + */ +export * as LogSpan from "./LogSpan.js" + +/** + * @since 2.0.0 + */ +export * as Logger from "./Logger.js" + +/** + * @since 3.8.0 + * @experimental + */ +export * as Mailbox from "./Mailbox.js" + +/** + * @since 2.0.0 + */ +export * as ManagedRuntime from "./ManagedRuntime.js" + +/** + * The `effect/match` module provides a type-safe pattern matching system for + * TypeScript. Inspired by functional programming, it simplifies conditional + * logic by replacing verbose if/else or switch statements with a structured and + * expressive API. + * + * This module supports matching against types, values, and discriminated unions + * while enforcing exhaustiveness checking to ensure all cases are handled. + * + * Although pattern matching is not yet a native JavaScript feature, + * `effect/match` offers a reliable implementation that is available today. + * + * **How Pattern Matching Works** + * + * Pattern matching follows a structured process: + * + * - **Creating a matcher**: Define a `Matcher` that operates on either a + * specific `Match.type` or `Match.value`. + * + * - **Defining patterns**: Use combinators such as `Match.when`, `Match.not`, + * and `Match.tag` to specify matching conditions. + * + * - **Completing the match**: Apply a finalizer such as `Match.exhaustive`, + * `Match.orElse`, or `Match.option` to determine how unmatched cases should + * be handled. + * + * @since 1.0.0 + */ +export * as Match from "./Match.js" + +/** + * @since 2.0.0 + */ +export * as MergeDecision from "./MergeDecision.js" + +/** + * @since 2.0.0 + */ +export * as MergeState from "./MergeState.js" + +/** + * @since 2.0.0 + */ +export * as MergeStrategy from "./MergeStrategy.js" + +/** + * @since 2.0.0 + */ +export * as Metric from "./Metric.js" + +/** + * @since 2.0.0 + */ +export * as MetricBoundaries from "./MetricBoundaries.js" + +/** + * @since 2.0.0 + */ +export * as MetricHook from "./MetricHook.js" + +/** + * @since 2.0.0 + */ +export * as MetricKey from "./MetricKey.js" + +/** + * @since 2.0.0 + */ +export * as MetricKeyType from "./MetricKeyType.js" + +/** + * @since 2.0.0 + */ +export * as MetricLabel from "./MetricLabel.js" + +/** + * @since 2.0.0 + */ +export * as MetricPair from "./MetricPair.js" + +/** + * @since 2.0.0 + */ +export * as MetricPolling from "./MetricPolling.js" + +/** + * @since 2.0.0 + */ +export * as MetricRegistry from "./MetricRegistry.js" + +/** + * @since 2.0.0 + */ +export * as MetricState from "./MetricState.js" + +/** + * A lightweight alternative to the `Effect` data type, with a subset of the functionality. + * + * @since 3.4.0 + * @experimental + */ +export * as Micro from "./Micro.js" + +/** + * @since 2.0.0 + * + * Enables low level framework authors to run on their own isolated effect version + */ +export * as ModuleVersion from "./ModuleVersion.js" + +/** + * @since 2.0.0 + */ +export * as MutableHashMap from "./MutableHashMap.js" + +/** + * # MutableHashSet + * + * A mutable `MutableHashSet` provides a collection of unique values with + * efficient lookup, insertion and removal. Unlike its immutable sibling + * {@link module:HashSet}, a `MutableHashSet` can be modified in-place; + * operations like add, remove, and clear directly modify the original set + * rather than creating a new one. This mutability offers benefits like improved + * performance in scenarios where you need to build or modify a set + * incrementally. + * + * ## What Problem Does It Solve? + * + * `MutableHashSet` solves the problem of maintaining an unsorted collection + * where each value appears exactly once, with fast operations for checking + * membership and adding/removing values, in contexts where mutability is + * preferred for performance or implementation simplicity. + * + * ## When to Use + * + * Use `MutableHashSet` when you need: + * + * - A collection with no duplicate values + * - Efficient membership testing (**`O(1)`** average complexity) + * - In-place modifications for better performance + * - A set that will be built or modified incrementally + * - Local mutability in otherwise immutable code + * + * ## Advanced Features + * + * MutableHashSet provides operations for: + * + * - Adding and removing elements with direct mutation + * - Checking for element existence + * - Clearing all elements at once + * - Converting to/from other collection types + * + * ## Performance Characteristics + * + * - **Lookup** operations ({@link module:MutableHashSet.has}): **`O(1)`** average + * time complexity + * - **Insertion** operations ({@link module:MutableHashSet.add}): **`O(1)`** + * average time complexity + * - **Removal** operations ({@link module:MutableHashSet.remove}): **`O(1)`** + * average time complexity + * - **Iteration**: **`O(n)`** where n is the size of the set + * + * The MutableHashSet data structure implements the following traits: + * + * - {@link Iterable}: allows iterating over the values in the set + * - {@link Pipeable}: allows chaining operations with the pipe operator + * - {@link Inspectable}: allows inspecting the contents of the set + * + * ## Operations Reference + * + * | Category | Operation | Description | Complexity | + * | ------------ | ------------------------------------------ | ----------------------------------- | ---------- | + * | constructors | {@link module:MutableHashSet.empty} | Creates an empty MutableHashSet | O(1) | + * | constructors | {@link module:MutableHashSet.fromIterable} | Creates a set from an iterable | O(n) | + * | constructors | {@link module:MutableHashSet.make} | Creates a set from multiple values | O(n) | + * | | | | | + * | elements | {@link module:MutableHashSet.has} | Checks if a value exists in the set | O(1) avg | + * | elements | {@link module:MutableHashSet.add} | Adds a value to the set | O(1) avg | + * | elements | {@link module:MutableHashSet.remove} | Removes a value from the set | O(1) avg | + * | elements | {@link module:MutableHashSet.size} | Gets the number of elements | O(1) | + * | elements | {@link module:MutableHashSet.clear} | Removes all values from the set | O(1) | + * + * ## Notes + * + * ### Mutability Considerations: + * + * Unlike most data structures in the Effect ecosystem, `MutableHashSet` is + * mutable. This means that operations like `add`, `remove`, and `clear` modify + * the original set rather than creating a new one. This can lead to more + * efficient code in some scenarios, but requires careful handling to avoid + * unexpected side effects. + * + * ### When to Choose `MutableHashSet` vs {@link module:HashSet}: + * + * - Use `MutableHashSet` when you need to build or modify a set incrementally and + * performance is a priority + * - Use `HashSet` when you want immutability guarantees and functional + * programming patterns + * - Consider using {@link module:HashSet}'s bounded mutation context (via + * {@link module:HashSet.beginMutation}, {@link module:HashSet.endMutation}, and + * {@link module:HashSet.mutate} methods) when you need temporary mutability + * within an otherwise immutable context - this approach might be sufficient + * for many use cases without requiring a separate `MutableHashSet` + * - `MutableHashSet` is often useful for local operations where the mutability is + * contained and doesn't leak into the broader application + * + * @module MutableHashSet + * @since 2.0.0 + */ +export * as MutableHashSet from "./MutableHashSet.js" + +/** + * @since 2.0.0 + */ +export * as MutableList from "./MutableList.js" + +/** + * @since 2.0.0 + */ +export * as MutableQueue from "./MutableQueue.js" + +/** + * @since 2.0.0 + */ +export * as MutableRef from "./MutableRef.js" + +/** + * @since 2.0.0 + */ +export * as NonEmptyIterable from "./NonEmptyIterable.js" + +/** + * # Number + * + * This module provides utility functions and type class instances for working + * with the `number` type in TypeScript. It includes functions for basic + * arithmetic operations, as well as type class instances for `Equivalence` and + * `Order`. + * + * ## Operations Reference + * + * | Category | Operation | Description | Domain | Co-domain | + * | ------------ | ------------------------------------------ | ------------------------------------------------------- | ------------------------------ | --------------------- | + * | constructors | {@link module:Number.parse} | Safely parses a string to a number | `string` | `Option` | + * | | | | | | + * | math | {@link module:Number.sum} | Adds two numbers | `number`, `number` | `number` | + * | math | {@link module:Number.sumAll} | Sums all numbers in a collection | `Iterable` | `number` | + * | math | {@link module:Number.subtract} | Subtracts one number from another | `number`, `number` | `number` | + * | math | {@link module:Number.multiply} | Multiplies two numbers | `number`, `number` | `number` | + * | math | {@link module:Number.multiplyAll} | Multiplies all numbers in a collection | `Iterable` | `number` | + * | math | {@link module:Number.divide} | Safely divides handling division by zero | `number`, `number` | `Option` | + * | math | {@link module:Number.unsafeDivide} | Divides but misbehaves for division by zero | `number`, `number` | `number` | + * | math | {@link module:Number.remainder} | Calculates remainder of division | `number`, `number` | `number` | + * | math | {@link module:Number.increment} | Adds 1 to a number | `number` | `number` | + * | math | {@link module:Number.decrement} | Subtracts 1 from a number | `number` | `number` | + * | math | {@link module:Number.sign} | Determines the sign of a number | `number` | `Ordering` | + * | math | {@link module:Number.nextPow2} | Finds the next power of 2 | `number` | `number` | + * | math | {@link module:Number.round} | Rounds a number with specified precision | `number`, `number` | `number` | + * | | | | | | + * | predicates | {@link module:Number.between} | Checks if a number is in a range | `number`, `{minimum, maximum}` | `boolean` | + * | predicates | {@link module:Number.lessThan} | Checks if one number is less than another | `number`, `number` | `boolean` | + * | predicates | {@link module:Number.lessThanOrEqualTo} | Checks if one number is less than or equal | `number`, `number` | `boolean` | + * | predicates | {@link module:Number.greaterThan} | Checks if one number is greater than another | `number`, `number` | `boolean` | + * | predicates | {@link module:Number.greaterThanOrEqualTo} | Checks if one number is greater or equal | `number`, `number` | `boolean` | + * | | | | | | + * | guards | {@link module:Number.isNumber} | Type guard for JavaScript numbers | `unknown` | `boolean` | + * | | | | | | + * | comparison | {@link module:Number.min} | Returns the minimum of two numbers | `number`, `number` | `number` | + * | comparison | {@link module:Number.max} | Returns the maximum of two numbers | `number`, `number` | `number` | + * | comparison | {@link module:Number.clamp} | Restricts a number to a range | `number`, `{minimum, maximum}` | `number` | + * | | | | | | + * | instances | {@link module:Number.Equivalence} | Equivalence instance for numbers | | `Equivalence` | + * | instances | {@link module:Number.Order} | Order instance for numbers | | `Order` | + * | | | | | | + * | errors | {@link module:Number.DivisionByZeroError} | Error thrown by unsafeDivide | | | + * + * ## Composition Patterns and Type Safety + * + * When building function pipelines, understanding how types flow through + * operations is critical: + * + * ### Composing with type-preserving operations + * + * Most operations in this module are type-preserving (`number → number`), + * making them easily composable in pipelines: + * + * ```ts + * import { pipe } from "effect" + * import * as Number from "effect/Number" + * + * const result = pipe( + * 10, + * Number.increment, // number → number + * Number.multiply(2), // number → number + * Number.round(1) // number → number + * ) // Result: number (21) + * ``` + * + * ### Working with Option results + * + * Operations that might fail (like division by zero) return Option types and + * require Option combinators: + * + * ```ts + * import { pipe, Option } from "effect" + * import * as Number from "effect/Number" + * + * const result = pipe( + * 10, + * Number.divide(0), // number → Option + * Option.getOrElse(() => 0) // Option → number + * ) // Result: number (0) + * ``` + * + * ### Composition best practices + * + * - Chain type-preserving operations for maximum composability + * - Use Option combinators when working with potentially failing operations + * - Consider using Effect for operations that might fail with specific errors + * - Remember that all operations maintain JavaScript's floating-point precision + * limitations + * + * @module Number + * @since 2.0.0 + * @see {@link module:BigInt} for more similar operations on `bigint` types + * @see {@link module:BigDecimal} for more similar operations on `BigDecimal` types + */ +export * as Number from "./Number.js" + +/** + * @since 2.0.0 + */ +export * as Option from "./Option.js" + +/** + * This module provides an implementation of the `Order` type class which is used to define a total ordering on some type `A`. + * An order is defined by a relation `<=`, which obeys the following laws: + * + * - either `x <= y` or `y <= x` (totality) + * - if `x <= y` and `y <= x`, then `x == y` (antisymmetry) + * - if `x <= y` and `y <= z`, then `x <= z` (transitivity) + * + * The truth table for compare is defined as follows: + * + * | `x <= y` | `x >= y` | Ordering | | + * | -------- | -------- | -------- | --------------------- | + * | `true` | `true` | `0` | corresponds to x == y | + * | `true` | `false` | `< 0` | corresponds to x < y | + * | `false` | `true` | `> 0` | corresponds to x > y | + * + * @since 2.0.0 + */ +export * as Order from "./Order.js" + +/** + * @since 2.0.0 + */ +export * as Ordering from "./Ordering.js" + +/** + * @since 3.10.0 + */ +export * as ParseResult from "./ParseResult.js" + +/** + * @since 3.19.4 + * @experimental + */ +export * as PartitionedSemaphore from "./PartitionedSemaphore.js" + +/** + * @since 2.0.0 + */ +export * as Pipeable from "./Pipeable.js" + +/** + * @since 2.0.0 + */ +export * as Pool from "./Pool.js" + +/** + * This module provides a collection of functions for working with predicates and refinements. + * + * A `Predicate` is a function that takes a value of type `A` and returns a boolean. + * It is used to check if a value satisfies a certain condition. + * + * A `Refinement` is a special type of predicate that not only checks a condition + * but also provides a type guard, allowing TypeScript to narrow the type of the input + * value from `A` to a more specific type `B` within a conditional block. + * + * The module includes: + * - Basic predicates and refinements for common types (e.g., `isString`, `isNumber`). + * - Combinators to create new predicates from existing ones (e.g., `and`, `or`, `not`). + * - Advanced combinators for working with data structures (e.g., `tuple`, `struct`). + * - Type-level utilities for inspecting predicate and refinement types. + * + * @since 2.0.0 + */ +export * as Predicate from "./Predicate.js" + +/** + * @since 3.10.0 + */ +export * as Pretty from "./Pretty.js" + +/** + * @since 2.0.0 + */ +export * as PrimaryKey from "./PrimaryKey.js" + +/** + * @since 2.0.0 + */ +export * as PubSub from "./PubSub.js" + +/** + * @since 2.0.0 + */ +export * as Queue from "./Queue.js" + +/** + * @since 2.0.0 + */ +export * as Random from "./Random.js" + +/** + * Limits the number of calls to a resource to a maximum amount in some interval. + * + * @since 2.0.0 + */ +export * as RateLimiter from "./RateLimiter.js" + +/** + * @since 3.5.0 + */ +export * as RcMap from "./RcMap.js" + +/** + * @since 3.5.0 + */ +export * as RcRef from "./RcRef.js" + +/** + * @since 2.0.0 + */ +export * as Readable from "./Readable.js" + +/** + * This module provides utility functions for working with records in TypeScript. + * + * @since 2.0.0 + */ +export * as Record from "./Record.js" + +/** + * @since 2.0.0 + */ +export * as RedBlackTree from "./RedBlackTree.js" + +/** + * The Redacted module provides functionality for handling sensitive information + * securely within your application. By using the `Redacted` data type, you can + * ensure that sensitive values are not accidentally exposed in logs or error + * messages. + * + * @since 3.3.0 + */ +export * as Redacted from "./Redacted.js" + +/** + * @since 2.0.0 + */ +export * as Ref from "./Ref.js" + +/** + * This module provides utility functions for working with RegExp in TypeScript. + * + * @since 2.0.0 + */ +export * as RegExp from "./RegExp.js" + +/** + * @since 2.0.0 + */ +export * as Reloadable from "./Reloadable.js" + +/** + * @since 2.0.0 + */ +export * as Request from "./Request.js" + +/** + * @since 2.0.0 + */ +export * as RequestBlock from "./RequestBlock.js" + +/** + * @since 2.0.0 + */ +export * as RequestResolver from "./RequestResolver.js" + +/** + * @since 2.0.0 + */ +export * as Resource from "./Resource.js" + +/** + * @since 2.0.0 + */ +export * as Runtime from "./Runtime.js" + +/** + * @since 2.0.0 + */ +export * as RuntimeFlags from "./RuntimeFlags.js" + +/** + * @since 2.0.0 + */ +export * as RuntimeFlagsPatch from "./RuntimeFlagsPatch.js" + +/** + * @since 2.0.0 + */ +export * as STM from "./STM.js" + +/** + * @since 2.0.0 + */ +export * as Schedule from "./Schedule.js" + +/** + * @since 2.0.0 + */ +export * as ScheduleDecision from "./ScheduleDecision.js" + +/** + * @since 2.0.0 + */ +export * as ScheduleInterval from "./ScheduleInterval.js" + +/** + * @since 2.0.0 + */ +export * as ScheduleIntervals from "./ScheduleIntervals.js" + +/** + * @since 2.0.0 + */ +export * as Scheduler from "./Scheduler.js" + +/** + * @since 3.10.0 + */ +export * as Schema from "./Schema.js" + +/** + * @since 3.10.0 + */ +export * as SchemaAST from "./SchemaAST.js" + +/** + * @since 2.0.0 + */ +export * as Scope from "./Scope.js" + +/** + * @since 2.0.0 + */ +export * as ScopedCache from "./ScopedCache.js" + +/** + * @since 2.0.0 + */ +export * as ScopedRef from "./ScopedRef.js" + +/** + * @since 2.0.0 + * @deprecated + */ +export * as Secret from "./Secret.js" + +/** + * @since 2.0.0 + */ +export * as SingleProducerAsyncInput from "./SingleProducerAsyncInput.js" + +/** + * @since 2.0.0 + */ +export * as Sink from "./Sink.js" + +/** + * @since 2.0.0 + */ +export * as SortedMap from "./SortedMap.js" + +/** + * @since 2.0.0 + */ +export * as SortedSet from "./SortedSet.js" + +/** + * @since 2.0.0 + */ +export * as Stream from "./Stream.js" + +/** + * @since 2.0.0 + */ +export * as StreamEmit from "./StreamEmit.js" + +/** + * @since 2.0.0 + */ +export * as StreamHaltStrategy from "./StreamHaltStrategy.js" + +/** + * @since 2.0.0 + */ +export * as Streamable from "./Streamable.js" + +/** + * This module provides utility functions and type class instances for working with the `string` type in TypeScript. + * It includes functions for basic string manipulation, as well as type class instances for + * `Equivalence` and `Order`. + * + * @since 2.0.0 + */ +export * as String from "./String.js" + +/** + * This module provides utility functions for working with structs in TypeScript. + * + * @since 2.0.0 + */ +export * as Struct from "./Struct.js" + +/** + * @since 2.0.0 + */ +export * as Subscribable from "./Subscribable.js" + +/** + * @since 2.0.0 + */ +export * as SubscriptionRef from "./SubscriptionRef.js" + +/** + * A `Supervisor` is allowed to supervise the launching and termination of + * fibers, producing some visible value of type `T` from the supervision. + * + * @since 2.0.0 + */ +export * as Supervisor from "./Supervisor.js" + +/** + * @since 2.0.0 + */ +export * as Symbol from "./Symbol.js" + +/** + * @since 2.0.0 + */ +export * as SynchronizedRef from "./SynchronizedRef.js" + +/** + * @since 2.0.0 + */ +export * as TArray from "./TArray.js" + +/** + * @since 2.0.0 + */ +export * as TDeferred from "./TDeferred.js" + +/** + * @since 2.0.0 + */ +export * as TMap from "./TMap.js" + +/** + * @since 2.0.0 + */ +export * as TPriorityQueue from "./TPriorityQueue.js" + +/** + * @since 2.0.0 + */ +export * as TPubSub from "./TPubSub.js" + +/** + * @since 2.0.0 + */ +export * as TQueue from "./TQueue.js" + +/** + * @since 2.0.0 + */ +export * as TRandom from "./TRandom.js" + +/** + * @since 2.0.0 + */ +export * as TReentrantLock from "./TReentrantLock.js" + +/** + * @since 2.0.0 + */ +export * as TRef from "./TRef.js" + +/** + * @since 2.0.0 + */ +export * as TSemaphore from "./TSemaphore.js" + +/** + * @since 2.0.0 + */ +export * as TSet from "./TSet.js" + +/** + * @since 3.10.0 + */ +export * as TSubscriptionRef from "./TSubscriptionRef.js" + +/** + * @since 2.0.0 + */ +export * as Take from "./Take.js" + +/** + * @since 2.0.0 + */ +export * as TestAnnotation from "./TestAnnotation.js" + +/** + * @since 2.0.0 + */ +export * as TestAnnotationMap from "./TestAnnotationMap.js" + +/** + * @since 2.0.0 + */ +export * as TestAnnotations from "./TestAnnotations.js" + +/** + * @since 2.0.0 + */ +export * as TestClock from "./TestClock.js" + +/** + * @since 2.0.0 + */ +export * as TestConfig from "./TestConfig.js" + +/** + * @since 2.0.0 + */ +export * as TestContext from "./TestContext.js" + +/** + * @since 2.0.0 + */ +export * as TestLive from "./TestLive.js" + +/** + * @since 2.0.0 + */ +export * as TestServices from "./TestServices.js" + +/** + * @since 2.0.0 + */ +export * as TestSized from "./TestSized.js" + +/** + * @since 2.0.0 + */ +export * as Tracer from "./Tracer.js" + +/** + * A `Trie` is used for locating specific `string` keys from within a set. + * + * It works similar to `HashMap`, but with keys required to be `string`. + * This constraint unlocks some performance optimizations and new methods to get string prefixes (e.g. `keysWithPrefix`, `longestPrefixOf`). + * + * Prefix search is also the main feature that makes a `Trie` more suited than `HashMap` for certain usecases. + * + * A `Trie` is often used to store a dictionary (list of words) that can be searched + * in a manner that allows for efficient generation of completion lists + * (e.g. predict the rest of a word a user is typing). + * + * A `Trie` has O(n) lookup time where `n` is the size of the key, + * or even less than `n` on search misses. + * + * @since 2.0.0 + */ +export * as Trie from "./Trie.js" + +/** + * This module provides utility functions for working with tuples in TypeScript. + * + * @since 2.0.0 + */ +export * as Tuple from "./Tuple.js" + +/** + * A collection of types that are commonly used types. + * + * @since 2.0.0 + */ +export * as Types from "./Types.js" + +/** + * @since 2.0.0 + */ +export * as Unify from "./Unify.js" + +/** + * @since 2.0.0 + */ +export * as UpstreamPullRequest from "./UpstreamPullRequest.js" + +/** + * @since 2.0.0 + */ +export * as UpstreamPullStrategy from "./UpstreamPullStrategy.js" + +/** + * @since 2.0.0 + */ +export * as Utils from "./Utils.js" diff --git a/repos/effect/packages/effect/src/internal/array.ts b/repos/effect/packages/effect/src/internal/array.ts new file mode 100644 index 0000000..24d7ee2 --- /dev/null +++ b/repos/effect/packages/effect/src/internal/array.ts @@ -0,0 +1,8 @@ +/** + * @since 2.0.0 + */ + +import type { NonEmptyArray } from "../Array.js" + +/** @internal */ +export const isNonEmptyArray = (self: ReadonlyArray): self is NonEmptyArray => self.length > 0 diff --git a/repos/effect/packages/effect/src/internal/blockedRequests.ts b/repos/effect/packages/effect/src/internal/blockedRequests.ts new file mode 100644 index 0000000..baa627a --- /dev/null +++ b/repos/effect/packages/effect/src/internal/blockedRequests.ts @@ -0,0 +1,520 @@ +import * as Chunk from "../Chunk.js" +import type * as Deferred from "../Deferred.js" +import * as Either from "../Either.js" +import * as Equal from "../Equal.js" +import type { FiberId } from "../FiberId.js" +import * as HashMap from "../HashMap.js" +import * as List from "../List.js" +import * as Option from "../Option.js" +import { hasProperty } from "../Predicate.js" +import type * as Request from "../Request.js" +import type * as RequestBlock from "../RequestBlock.js" +import type * as RequestResolver from "../RequestResolver.js" + +/** @internal */ +export const empty: RequestBlock.RequestBlock = { + _tag: "Empty" +} + +/** + * Combines this collection of blocked requests with the specified collection + * of blocked requests, in parallel. + * + * @internal + */ +export const par = ( + self: RequestBlock.RequestBlock, + that: RequestBlock.RequestBlock +): RequestBlock.RequestBlock => ({ + _tag: "Par", + left: self, + right: that +}) + +/** + * Combines this collection of blocked requests with the specified collection + * of blocked requests, in sequence. + * + * @internal + */ +export const seq = ( + self: RequestBlock.RequestBlock, + that: RequestBlock.RequestBlock +): RequestBlock.RequestBlock => ({ + _tag: "Seq", + left: self, + right: that +}) + +/** + * Constructs a collection of blocked requests from the specified blocked + * request and data source. + * + * @internal + */ +export const single = ( + dataSource: RequestResolver.RequestResolver, + blockedRequest: Request.Entry +): RequestBlock.RequestBlock => ({ + _tag: "Single", + dataSource: dataSource as any, + blockedRequest +}) + +/** @internal */ +export const MapRequestResolversReducer = ( + f: (dataSource: RequestResolver.RequestResolver) => RequestResolver.RequestResolver +): RequestBlock.RequestBlock.Reducer => ({ + emptyCase: () => empty, + parCase: (left, right) => par(left, right), + seqCase: (left, right) => seq(left, right), + singleCase: (dataSource, blockedRequest) => single(f(dataSource), blockedRequest as any) +}) + +type BlockedRequestsCase = ParCase | SeqCase + +interface ParCase { + readonly _tag: "ParCase" +} + +interface SeqCase { + readonly _tag: "SeqCase" +} + +/** + * Transforms all data sources with the specified data source aspect, which + * can change the environment type of data sources but must preserve the + * request type of each data source. + * + * @internal + */ +export const mapRequestResolvers = ( + self: RequestBlock.RequestBlock, + f: (dataSource: RequestResolver.RequestResolver) => RequestResolver.RequestResolver +): RequestBlock.RequestBlock => reduce(self, MapRequestResolversReducer(f)) + +/** + * Folds over the cases of this collection of blocked requests with the + * specified functions. + * + * @internal + */ +export const reduce = ( + self: RequestBlock.RequestBlock, + reducer: RequestBlock.RequestBlock.Reducer +): Z => { + let input: List.List = List.of(self) + let output = List.empty>() + while (List.isCons(input)) { + const current: RequestBlock.RequestBlock = input.head + switch (current._tag) { + case "Empty": { + output = List.cons(Either.right(reducer.emptyCase()), output) + input = input.tail + break + } + case "Par": { + output = List.cons(Either.left({ _tag: "ParCase" }), output) + input = List.cons(current.left, List.cons(current.right, input.tail)) + break + } + case "Seq": { + output = List.cons(Either.left({ _tag: "SeqCase" }), output) + input = List.cons(current.left, List.cons(current.right, input.tail)) + break + } + case "Single": { + const result = reducer.singleCase(current.dataSource, current.blockedRequest) + output = List.cons(Either.right(result), output) + input = input.tail + break + } + } + } + const result = List.reduce(output, List.empty(), (acc, current) => { + switch (current._tag) { + case "Left": { + const left = List.unsafeHead(acc) + const right = List.unsafeHead(List.unsafeTail(acc)) + const tail = List.unsafeTail(List.unsafeTail(acc)) + switch (current.left._tag) { + case "ParCase": { + return List.cons(reducer.parCase(left, right), tail) + } + case "SeqCase": { + return List.cons(reducer.seqCase(left, right), tail) + } + } + } + case "Right": { + return List.cons(current.right, acc) + } + } + }) + if (List.isNil(result)) { + throw new Error( + "BUG: BlockedRequests.reduce - please report an issue at https://github.com/Effect-TS/effect/issues" + ) + } + return result.head +} + +/** + * Flattens a collection of blocked requests into a collection of pipelined + * and batched requests that can be submitted for execution. + * + * @internal + */ +export const flatten = ( + self: RequestBlock.RequestBlock +): List.List => { + let current = List.of(self) + let updated = List.empty() + // eslint-disable-next-line no-constant-condition + while (1) { + const [parallel, sequential] = List.reduce( + current, + [parallelCollectionEmpty(), List.empty()] as const, + ([parallel, sequential], blockedRequest) => { + const [par, seq] = step(blockedRequest) + return [ + parallelCollectionCombine(parallel, par), + List.appendAll(sequential, seq) + ] + } + ) + updated = merge(updated, parallel) + if (List.isNil(sequential)) { + return List.reverse(updated) + } + current = sequential + } + throw new Error( + "BUG: BlockedRequests.flatten - please report an issue at https://github.com/Effect-TS/effect/issues" + ) +} + +/** + * Takes one step in evaluating a collection of blocked requests, returning a + * collection of blocked requests that can be performed in parallel and a list + * of blocked requests that must be performed sequentially after those + * requests. + */ +const step = ( + requests: RequestBlock.RequestBlock +): [ParallelCollection, List.List] => { + let current: RequestBlock.RequestBlock = requests + let parallel = parallelCollectionEmpty() + let stack = List.empty() + let sequential = List.empty() + // eslint-disable-next-line no-constant-condition + while (1) { + switch (current._tag) { + case "Empty": { + if (List.isNil(stack)) { + return [parallel, sequential] + } + current = stack.head + stack = stack.tail + break + } + case "Par": { + stack = List.cons(current.right, stack) + current = current.left + break + } + case "Seq": { + const left = current.left + const right = current.right + switch (left._tag) { + case "Empty": { + current = right + break + } + case "Par": { + const l = left.left + const r = left.right + current = par(seq(l, right), seq(r, right)) + break + } + case "Seq": { + const l = left.left + const r = left.right + current = seq(l, seq(r, right)) + break + } + case "Single": { + current = left + sequential = List.cons(right, sequential) + break + } + } + break + } + case "Single": { + parallel = parallelCollectionAdd( + parallel, + current + ) + if (List.isNil(stack)) { + return [parallel, sequential] + } + current = stack.head + stack = stack.tail + break + } + } + } + throw new Error( + "BUG: BlockedRequests.step - please report an issue at https://github.com/Effect-TS/effect/issues" + ) +} + +/** + * Merges a collection of requests that must be executed sequentially with a + * collection of requests that can be executed in parallel. If the collections + * are both from the same single data source then the requests can be + * pipelined while preserving ordering guarantees. + */ +const merge = ( + sequential: List.List, + parallel: ParallelCollection +): List.List => { + if (List.isNil(sequential)) { + return List.of(parallelCollectionToSequentialCollection(parallel)) + } + if (parallelCollectionIsEmpty(parallel)) { + return sequential + } + const seqHeadKeys = sequentialCollectionKeys(sequential.head) + const parKeys = parallelCollectionKeys(parallel) + if ( + seqHeadKeys.length === 1 && + parKeys.length === 1 && + Equal.equals(seqHeadKeys[0], parKeys[0]) + ) { + return List.cons( + sequentialCollectionCombine( + sequential.head, + parallelCollectionToSequentialCollection(parallel) + ), + sequential.tail + ) + } + return List.cons(parallelCollectionToSequentialCollection(parallel), sequential) +} + +// +// circular +// + +/** @internal */ +export const EntryTypeId: Request.EntryTypeId = Symbol.for( + "effect/RequestBlock/Entry" +) as Request.EntryTypeId + +/** @internal */ +class EntryImpl> implements Request.Entry { + readonly [EntryTypeId] = blockedRequestVariance + constructor( + readonly request: A, + readonly result: Deferred.Deferred, Request.Request.Error>, + readonly listeners: Request.Listeners, + readonly ownerId: FiberId, + readonly state: { + completed: boolean + } + ) {} +} + +const blockedRequestVariance = { + /* c8 ignore next */ + _R: (_: never) => _ +} + +/** @internal */ +export const isEntry = (u: unknown): u is Request.Entry => hasProperty(u, EntryTypeId) + +/** @internal */ +export const makeEntry = >( + options: { + readonly request: A + readonly result: Deferred.Deferred, Request.Request.Error> + readonly listeners: Request.Listeners + readonly ownerId: FiberId + readonly state: { completed: boolean } + } +): Request.Entry => new EntryImpl(options.request, options.result, options.listeners, options.ownerId, options.state) + +/** @internal */ +export const RequestBlockParallelTypeId = Symbol.for( + "effect/RequestBlock/RequestBlockParallel" +) + +const parallelVariance = { + /* c8 ignore next */ + _R: (_: never) => _ +} + +class ParallelImpl implements ParallelCollection { + readonly [RequestBlockParallelTypeId] = parallelVariance + constructor( + readonly map: HashMap.HashMap< + RequestResolver.RequestResolver, + Chunk.Chunk> + > + ) {} +} + +/** @internal */ +export const parallelCollectionEmpty = (): ParallelCollection => new ParallelImpl(HashMap.empty()) + +/** @internal */ +export const parallelCollectionMake = ( + dataSource: RequestResolver.RequestResolver, + blockedRequest: Request.Entry +): ParallelCollection => new ParallelImpl(HashMap.make([dataSource, Chunk.of(blockedRequest)]) as any) + +/** @internal */ +export const parallelCollectionAdd = ( + self: ParallelCollection, + blockedRequest: RequestBlock.Single +): ParallelCollection => + new ParallelImpl(HashMap.modifyAt( + self.map, + blockedRequest.dataSource, + (_) => + Option.orElseSome( + Option.map(_, Chunk.append(blockedRequest.blockedRequest)), + () => Chunk.of(blockedRequest.blockedRequest) + ) + )) + +/** @internal */ +export const parallelCollectionCombine = ( + self: ParallelCollection, + that: ParallelCollection +): ParallelCollection => + new ParallelImpl(HashMap.reduce(self.map, that.map, (map, value, key) => + HashMap.set( + map, + key, + Option.match(HashMap.get(map, key), { + onNone: () => value, + onSome: (other) => Chunk.appendAll(value, other) + }) + ))) + +/** @internal */ +export const parallelCollectionIsEmpty = (self: ParallelCollection): boolean => HashMap.isEmpty(self.map) + +/** @internal */ +export const parallelCollectionKeys = ( + self: ParallelCollection +): Array> => Array.from(HashMap.keys(self.map)) as any + +/** @internal */ +export const parallelCollectionToSequentialCollection = ( + self: ParallelCollection +): SequentialCollection => sequentialCollectionMake(HashMap.map(self.map, (x) => Chunk.of(x)) as any) + +// TODO +// /** @internal */ +// export const parallelCollectionToChunk = ( +// self: ParallelCollection +// ): Array<[RequestResolver.RequestResolver, Array>]> => Array.from(self.map) as any + +/** @internal */ +export const SequentialCollectionTypeId = Symbol.for( + "effect/RequestBlock/RequestBlockSequential" +) + +const sequentialVariance = { + /* c8 ignore next */ + _R: (_: never) => _ +} + +class SequentialImpl implements SequentialCollection { + readonly [SequentialCollectionTypeId] = sequentialVariance + constructor( + readonly map: HashMap.HashMap< + RequestResolver.RequestResolver, + Chunk.Chunk>> + > + ) {} +} + +/** @internal */ +export const sequentialCollectionMake = ( + map: HashMap.HashMap< + RequestResolver.RequestResolver, + Chunk.Chunk>> + > +): SequentialCollection => new SequentialImpl(map as any) + +/** @internal */ +export const sequentialCollectionCombine = ( + self: SequentialCollection, + that: SequentialCollection +): SequentialCollection => + new SequentialImpl(HashMap.reduce(that.map, self.map, (map, value, key) => + HashMap.set( + map, + key, + Option.match(HashMap.get(map, key), { + onNone: () => Chunk.empty(), + onSome: (a) => Chunk.appendAll(a, value) + }) + ))) + +/** @internal */ +export const sequentialCollectionIsEmpty = (self: SequentialCollection): boolean => HashMap.isEmpty(self.map) + +/** @internal */ +export const sequentialCollectionKeys = ( + self: SequentialCollection +): Array> => Array.from(HashMap.keys(self.map)) as any + +/** @internal */ +export const sequentialCollectionToChunk = ( + self: SequentialCollection +): Array<[RequestResolver.RequestResolver, Chunk.Chunk>>]> => + Array.from(self.map) as any + +/** @internal */ +export type RequestBlockParallelTypeId = typeof RequestBlockParallelTypeId + +/** @internal */ +export interface ParallelCollection extends ParallelCollection.Variance { + readonly map: HashMap.HashMap< + RequestResolver.RequestResolver, + Chunk.Chunk> + > +} + +/** @internal */ +export declare namespace ParallelCollection { + /** @internal */ + export interface Variance { + readonly [RequestBlockParallelTypeId]: {} + } +} + +/** @internal */ +export type SequentialCollectionTypeId = typeof SequentialCollectionTypeId + +/** @internal */ +export interface SequentialCollection extends SequentialCollection.Variance { + readonly map: HashMap.HashMap< + RequestResolver.RequestResolver, + Chunk.Chunk>> + > +} + +/** @internal */ +export declare namespace SequentialCollection { + /** @internal */ + export interface Variance { + readonly [SequentialCollectionTypeId]: {} + } +} diff --git a/repos/effect/packages/effect/src/internal/cache.ts b/repos/effect/packages/effect/src/internal/cache.ts new file mode 100644 index 0000000..480b749 --- /dev/null +++ b/repos/effect/packages/effect/src/internal/cache.ts @@ -0,0 +1,733 @@ +import type * as Cache from "../Cache.js" +import type * as Clock from "../Clock.js" +import * as Context from "../Context.js" +import * as Deferred from "../Deferred.js" +import * as Duration from "../Duration.js" +import type * as Effect from "../Effect.js" +import * as Either from "../Either.js" +import * as Equal from "../Equal.js" +import * as Exit from "../Exit.js" +import type * as FiberId from "../FiberId.js" +import { pipe } from "../Function.js" +import * as Hash from "../Hash.js" +import * as MutableHashMap from "../MutableHashMap.js" +import * as MutableQueue from "../MutableQueue.js" +import * as MutableRef from "../MutableRef.js" +import * as Option from "../Option.js" +import { hasProperty } from "../Predicate.js" +import * as effect from "./core-effect.js" +import * as core from "./core.js" +import * as Data from "./data.js" +import { none } from "./fiberId.js" +import * as fiberRuntime from "./fiberRuntime.js" + +/** + * A `MapValue` represents a value in the cache. A value may either be + * `Pending` with a `Promise` that will contain the result of computing the + * lookup function, when it is available, or `Complete` with an `Exit` value + * that contains the result of computing the lookup function. + * + * @internal + */ +export type MapValue = + | Complete + | Pending + | Refreshing + +/** @internal */ +export interface Complete { + readonly _tag: "Complete" + readonly key: MapKey + readonly exit: Exit.Exit + readonly entryStats: Cache.EntryStats + readonly timeToLiveMillis: number +} + +/** @internal */ +export interface Pending { + readonly _tag: "Pending" + readonly key: MapKey + readonly deferred: Deferred.Deferred +} + +/** @internal */ +export interface Refreshing { + readonly _tag: "Refreshing" + readonly deferred: Deferred.Deferred + readonly complete: Complete +} + +/** @internal */ +export const complete = ( + key: MapKey, + exit: Exit.Exit, + entryStats: Cache.EntryStats, + timeToLiveMillis: number +): MapValue => + Data.struct({ + _tag: "Complete" as const, + key, + exit, + entryStats, + timeToLiveMillis + }) + +/** @internal */ +export const pending = ( + key: MapKey, + deferred: Deferred.Deferred +): MapValue => + Data.struct({ + _tag: "Pending" as const, + key, + deferred + }) + +/** @internal */ +export const refreshing = ( + deferred: Deferred.Deferred, + complete: Complete +): MapValue => + Data.struct({ + _tag: "Refreshing" as const, + deferred, + complete + }) + +/** @internal */ +export const MapKeyTypeId = Symbol.for("effect/Cache/MapKey") + +/** @internal */ +export type MapKeyTypeId = typeof MapKeyTypeId + +/** + * A `MapKey` represents a key in the cache. It contains mutable references + * to the previous key and next key in the `KeySet` to support an efficient + * implementation of a sorted set of keys. + * + * @internal + */ +export interface MapKey extends Equal.Equal { + readonly [MapKeyTypeId]: MapKeyTypeId + readonly current: K + previous: MapKey | undefined // mutable by design + next: MapKey | undefined // mutable by design +} + +class MapKeyImpl implements MapKey { + readonly [MapKeyTypeId]: MapKeyTypeId = MapKeyTypeId + previous: MapKey | undefined = undefined + next: MapKey | undefined = undefined + constructor(readonly current: K) {} + [Hash.symbol](): number { + return pipe( + Hash.hash(this.current), + Hash.combine(Hash.hash(this.previous)), + Hash.combine(Hash.hash(this.next)), + Hash.cached(this) + ) + } + [Equal.symbol](that: unknown): boolean { + if (this === that) { + return true + } + return isMapKey(that) && + Equal.equals(this.current, that.current) && + Equal.equals(this.previous, that.previous) && + Equal.equals(this.next, that.next) + } +} + +/** @internal */ +export const makeMapKey = (current: K): MapKey => new MapKeyImpl(current) + +/** @internal */ +export const isMapKey = (u: unknown): u is MapKey => hasProperty(u, MapKeyTypeId) + +/** + * A `KeySet` is a sorted set of keys in the cache ordered by last access. + * For efficiency, the set is implemented in terms of a doubly linked list + * and is not safe for concurrent access. + * + * @internal + */ +export interface KeySet { + head: MapKey | undefined // mutable by design + tail: MapKey | undefined // mutable by design + /** + * Adds the specified key to the set. + */ + add(key: MapKey): void + /** + * Removes the lowest priority key from the set. + */ + remove(): MapKey | undefined +} + +class KeySetImpl implements KeySet { + head: MapKey | undefined = undefined + tail: MapKey | undefined = undefined + add(key: MapKey): void { + if (key !== this.tail) { + if (this.tail === undefined) { + this.head = key + this.tail = key + } else { + const previous = key.previous + const next = key.next + if (next !== undefined) { + key.next = undefined + if (previous !== undefined) { + previous.next = next + next.previous = previous + } else { + this.head = next + this.head.previous = undefined + } + } + this.tail.next = key + key.previous = this.tail + this.tail = key + } + } + } + remove(): MapKey | undefined { + const key = this.head + if (key !== undefined) { + const next = key.next + if (next !== undefined) { + key.next = undefined + this.head = next + this.head.previous = undefined + } else { + this.head = undefined + this.tail = undefined + } + } + return key + } +} + +/** @internal */ +export const makeKeySet = (): KeySet => new KeySetImpl() + +/** + * The `CacheState` represents the mutable state underlying the cache. + * + * @internal + */ +export interface CacheState { + map: MutableHashMap.MutableHashMap> // mutable by design + keys: KeySet // mutable by design + accesses: MutableQueue.MutableQueue> // mutable by design + updating: MutableRef.MutableRef // mutable by design + hits: number // mutable by design + misses: number // mutable by design +} + +/** + * Constructs a new `CacheState` from the specified values. + * + * @internal + */ +export const makeCacheState = ( + map: MutableHashMap.MutableHashMap>, + keys: KeySet, + accesses: MutableQueue.MutableQueue>, + updating: MutableRef.MutableRef, + hits: number, + misses: number +): CacheState => ({ + map, + keys, + accesses, + updating, + hits, + misses +}) + +/** + * Constructs an initial cache state. + * + * @internal + */ +export const initialCacheState = (): CacheState => + makeCacheState( + MutableHashMap.empty(), + makeKeySet(), + MutableQueue.unbounded(), + MutableRef.make(false), + 0, + 0 + ) + +/** @internal */ +const CacheSymbolKey = "effect/Cache" + +/** @internal */ +export const CacheTypeId: Cache.CacheTypeId = Symbol.for( + CacheSymbolKey +) as Cache.CacheTypeId + +const cacheVariance = { + /* c8 ignore next */ + _Key: (_: any) => _, + /* c8 ignore next */ + _Error: (_: never) => _, + /* c8 ignore next */ + _Value: (_: any) => _ +} + +/** @internal */ +const ConsumerCacheSymbolKey = "effect/ConsumerCache" + +/** @internal */ +export const ConsumerCacheTypeId: Cache.ConsumerCacheTypeId = Symbol.for( + ConsumerCacheSymbolKey +) as Cache.ConsumerCacheTypeId + +const consumerCacheVariance = { + /* c8 ignore next */ + _Key: (_: any) => _, + /* c8 ignore next */ + _Error: (_: never) => _, + /* c8 ignore next */ + _Value: (_: never) => _ +} + +/** @internal */ +export const makeCacheStats = ( + options: { + readonly hits: number + readonly misses: number + readonly size: number + } +): Cache.CacheStats => options + +/** @internal */ +export const makeEntryStats = (loadedMillis: number): Cache.EntryStats => ({ + loadedMillis +}) + +class CacheImpl implements Cache.Cache { + readonly [CacheTypeId] = cacheVariance + readonly [ConsumerCacheTypeId] = consumerCacheVariance + readonly cacheState: CacheState + constructor( + readonly capacity: number, + readonly context: Context.Context, + readonly fiberId: FiberId.FiberId, + readonly lookup: Cache.Lookup, + readonly timeToLive: (exit: Exit.Exit) => Duration.DurationInput + ) { + this.cacheState = initialCacheState() + } + + get(key: Key): Effect.Effect { + return core.map(this.getEither(key), Either.merge) + } + + get cacheStats(): Effect.Effect { + return core.sync(() => + makeCacheStats({ + hits: this.cacheState.hits, + misses: this.cacheState.misses, + size: MutableHashMap.size(this.cacheState.map) + }) + ) + } + + getOption(key: Key): Effect.Effect, Error> { + return core.suspend(() => + Option.match(MutableHashMap.get(this.cacheState.map, key), { + onNone: () => { + const mapKey = makeMapKey(key) + this.trackAccess(mapKey) + this.trackMiss() + return core.succeed(Option.none()) + }, + onSome: (value) => this.resolveMapValue(value) + }) + ) + } + + getOptionComplete(key: Key): Effect.Effect> { + return core.suspend(() => + Option.match(MutableHashMap.get(this.cacheState.map, key), { + onNone: () => { + const mapKey = makeMapKey(key) + this.trackAccess(mapKey) + this.trackMiss() + return core.succeed(Option.none()) + }, + onSome: (value) => this.resolveMapValue(value, true) as Effect.Effect> + }) + ) + } + + contains(key: Key): Effect.Effect { + return core.sync(() => MutableHashMap.has(this.cacheState.map, key)) + } + + entryStats(key: Key): Effect.Effect> { + return core.sync(() => { + const option = MutableHashMap.get(this.cacheState.map, key) + if (Option.isSome(option)) { + switch (option.value._tag) { + case "Complete": { + const loaded = option.value.entryStats.loadedMillis + return Option.some(makeEntryStats(loaded)) + } + case "Pending": { + return Option.none() + } + case "Refreshing": { + const loaded = option.value.complete.entryStats.loadedMillis + return Option.some(makeEntryStats(loaded)) + } + } + } + return Option.none() + }) + } + + getEither(key: Key): Effect.Effect, Error> { + return core.suspend((): Effect.Effect, Error> => { + const k = key + let mapKey: MapKey | undefined = undefined + let deferred: Deferred.Deferred | undefined = undefined + let value = Option.getOrUndefined(MutableHashMap.get(this.cacheState.map, k)) + if (value === undefined) { + deferred = Deferred.unsafeMake(this.fiberId) + mapKey = makeMapKey(k) + if (MutableHashMap.has(this.cacheState.map, k)) { + value = Option.getOrUndefined(MutableHashMap.get(this.cacheState.map, k)) + } else { + MutableHashMap.set(this.cacheState.map, k, pending(mapKey, deferred)) + } + } + if (value === undefined) { + this.trackAccess(mapKey!) + this.trackMiss() + return core.map(this.lookupValueOf(key, deferred!), Either.right) + } else { + return core.flatMap( + this.resolveMapValue(value), + Option.match({ + onNone: () => this.getEither(key), + onSome: (value) => core.succeed(Either.left(value)) + }) + ) + } + }) + } + + invalidate(key: Key): Effect.Effect { + return core.sync(() => { + MutableHashMap.remove(this.cacheState.map, key) + }) + } + + invalidateWhen(key: Key, when: (value: Value) => boolean): Effect.Effect { + return core.sync(() => { + const value = MutableHashMap.get(this.cacheState.map, key) + if (Option.isSome(value) && value.value._tag === "Complete") { + if (value.value.exit._tag === "Success") { + if (when(value.value.exit.value)) { + MutableHashMap.remove(this.cacheState.map, key) + } + } + } + }) + } + + get invalidateAll(): Effect.Effect { + return core.sync(() => { + this.cacheState.map = MutableHashMap.empty() + }) + } + + refresh(key: Key): Effect.Effect { + return effect.clockWith((clock) => + core.suspend(() => { + const k = key + const deferred: Deferred.Deferred = Deferred.unsafeMake(this.fiberId) + let value = Option.getOrUndefined(MutableHashMap.get(this.cacheState.map, k)) + if (value === undefined) { + if (MutableHashMap.has(this.cacheState.map, k)) { + value = Option.getOrUndefined(MutableHashMap.get(this.cacheState.map, k)) + } else { + MutableHashMap.set(this.cacheState.map, k, pending(makeMapKey(k), deferred)) + } + } + if (value === undefined) { + return core.asVoid(this.lookupValueOf(key, deferred)) + } else { + switch (value._tag) { + case "Complete": { + if (this.hasExpired(clock, value.timeToLiveMillis)) { + const found = Option.getOrUndefined(MutableHashMap.get(this.cacheState.map, k)) + if (Equal.equals(found, value)) { + MutableHashMap.remove(this.cacheState.map, k) + } + return core.asVoid(this.get(key)) + } + // Only trigger the lookup if we're still the current value, `completedResult` + return pipe( + this.lookupValueOf(key, deferred), + effect.when(() => { + const current = Option.getOrUndefined(MutableHashMap.get(this.cacheState.map, k)) + if (Equal.equals(current, value)) { + const mapValue = refreshing(deferred, value as Complete) + MutableHashMap.set(this.cacheState.map, k, mapValue) + return true + } + return false + }), + core.asVoid + ) + } + case "Pending": { + return Deferred.await(value.deferred) + } + case "Refreshing": { + return Deferred.await(value.deferred) + } + } + } + }) + ) + } + + set(key: Key, value: Value): Effect.Effect { + return effect.clockWith((clock) => + core.sync(() => { + const now = clock.unsafeCurrentTimeMillis() + const k = key + const lookupResult = Exit.succeed(value) + const mapValue = complete( + makeMapKey(k), + lookupResult, + makeEntryStats(now), + now + Duration.toMillis(Duration.decode(this.timeToLive(lookupResult))) + ) + MutableHashMap.set( + this.cacheState.map, + k, + mapValue as Complete + ) + }) + ) + } + + get size(): Effect.Effect { + return core.sync(() => { + return MutableHashMap.size(this.cacheState.map) + }) + } + + get values(): Effect.Effect> { + return core.sync(() => { + const values: Array = [] + for (const entry of this.cacheState.map) { + if (entry[1]._tag === "Complete" && entry[1].exit._tag === "Success") { + values.push(entry[1].exit.value) + } + } + return values + }) + } + + get entries(): Effect.Effect> { + return core.sync(() => { + const values: Array<[Key, Value]> = [] + for (const entry of this.cacheState.map) { + if (entry[1]._tag === "Complete" && entry[1].exit._tag === "Success") { + values.push([entry[0], entry[1].exit.value]) + } + } + return values + }) + } + + get keys(): Effect.Effect> { + return core.sync(() => { + const keys: Array = [] + for (const entry of this.cacheState.map) { + if (entry[1]._tag === "Complete" && entry[1].exit._tag === "Success") { + keys.push(entry[0]) + } + } + return keys + }) + } + + resolveMapValue( + value: MapValue, + ignorePending = false + ): Effect.Effect, Error> { + return effect.clockWith((clock) => { + switch (value._tag) { + case "Complete": { + this.trackAccess(value.key) + if (this.hasExpired(clock, value.timeToLiveMillis)) { + MutableHashMap.remove(this.cacheState.map, value.key.current) + return core.succeed(Option.none()) + } + this.trackHit() + return core.map(value.exit, Option.some) + } + case "Pending": { + this.trackAccess(value.key) + this.trackHit() + if (ignorePending) { + return core.succeed(Option.none()) + } + return core.map(Deferred.await(value.deferred), Option.some) + } + case "Refreshing": { + this.trackAccess(value.complete.key) + this.trackHit() + if (this.hasExpired(clock, value.complete.timeToLiveMillis)) { + if (ignorePending) { + return core.succeed(Option.none()) + } + return core.map(Deferred.await(value.deferred), Option.some) + } + return core.map(value.complete.exit, Option.some) + } + } + }) + } + + trackHit(): void { + this.cacheState.hits = this.cacheState.hits + 1 + } + + trackMiss(): void { + this.cacheState.misses = this.cacheState.misses + 1 + } + + trackAccess(key: MapKey): void { + MutableQueue.offer(this.cacheState.accesses, key) + if (MutableRef.compareAndSet(this.cacheState.updating, false, true)) { + let loop = true + while (loop) { + const key = MutableQueue.poll(this.cacheState.accesses, MutableQueue.EmptyMutableQueue) + if (key === MutableQueue.EmptyMutableQueue) { + loop = false + } else { + this.cacheState.keys.add(key) + } + } + let size = MutableHashMap.size(this.cacheState.map) + loop = size > this.capacity + while (loop) { + const key = this.cacheState.keys.remove() + if (key !== undefined) { + if (MutableHashMap.has(this.cacheState.map, key.current)) { + MutableHashMap.remove(this.cacheState.map, key.current) + size = size - 1 + loop = size > this.capacity + } + } else { + loop = false + } + } + MutableRef.set(this.cacheState.updating, false) + } + } + + hasExpired(clock: Clock.Clock, timeToLiveMillis: number): boolean { + return clock.unsafeCurrentTimeMillis() > timeToLiveMillis + } + + lookupValueOf( + input: Key, + deferred: Deferred.Deferred + ): Effect.Effect { + return effect.clockWith((clock) => + core.suspend(() => { + const key = input + return pipe( + this.lookup(input), + core.provideContext(this.context), + core.exit, + core.flatMap((exit) => { + const now = clock.unsafeCurrentTimeMillis() + const stats = makeEntryStats(now) + const value = complete( + makeMapKey(key), + exit, + stats, + now + Duration.toMillis(Duration.decode(this.timeToLive(exit))) + ) + MutableHashMap.set(this.cacheState.map, key, value) + return core.zipRight( + Deferred.done(deferred, exit), + exit + ) + }), + core.onInterrupt(() => + core.zipRight( + Deferred.interrupt(deferred), + core.sync(() => { + MutableHashMap.remove(this.cacheState.map, key) + }) + ) + ) + ) + }) + ) + } +} + +/** @internal */ +export const make = ( + options: { + readonly capacity: number + readonly timeToLive: Duration.DurationInput + readonly lookup: Cache.Lookup + } +): Effect.Effect, never, Environment> => { + const timeToLive = Duration.decode(options.timeToLive) + return makeWith({ + capacity: options.capacity, + lookup: options.lookup, + timeToLive: () => timeToLive + }) +} + +/** @internal */ +export const makeWith = ( + options: { + readonly capacity: number + readonly lookup: Cache.Lookup + readonly timeToLive: (exit: Exit.Exit) => Duration.DurationInput + } +): Effect.Effect, never, Environment> => + core.map( + fiberRuntime.all([core.context(), core.fiberId]), + ([context, fiberId]) => + new CacheImpl( + options.capacity, + context, + fiberId, + options.lookup, + (exit) => Duration.decode(options.timeToLive(exit)) + ) + ) + +/** @internal */ +export const unsafeMakeWith = ( + capacity: number, + lookup: Cache.Lookup, + timeToLive: (exit: Exit.Exit) => Duration.DurationInput +): Cache.Cache => + new CacheImpl( + capacity, + Context.empty() as Context.Context, + none, + lookup, + (exit) => Duration.decode(timeToLive(exit)) + ) diff --git a/repos/effect/packages/effect/src/internal/cause.ts b/repos/effect/packages/effect/src/internal/cause.ts new file mode 100644 index 0000000..e557a91 --- /dev/null +++ b/repos/effect/packages/effect/src/internal/cause.ts @@ -0,0 +1,1049 @@ +import * as Arr from "../Array.js" +import type * as Cause from "../Cause.js" +import * as Chunk from "../Chunk.js" +import * as Either from "../Either.js" +import * as Equal from "../Equal.js" +import type * as FiberId from "../FiberId.js" +import { constFalse, constTrue, dual, identity, pipe } from "../Function.js" +import { globalValue } from "../GlobalValue.js" +import * as Hash from "../Hash.js" +import * as HashSet from "../HashSet.js" +import { NodeInspectSymbol, stringifyCircular, toJSON } from "../Inspectable.js" +import * as Option from "../Option.js" +import { pipeArguments } from "../Pipeable.js" +import type { Predicate, Refinement } from "../Predicate.js" +import { hasProperty, isFunction } from "../Predicate.js" +import type { AnySpan, Span } from "../Tracer.js" +import type * as Types from "../Types.js" +import { getBugErrorMessage } from "./errors.js" +import * as OpCodes from "./opCodes/cause.js" + +// ----------------------------------------------------------------------------- +// Models +// ----------------------------------------------------------------------------- + +/** @internal */ +const CauseSymbolKey = "effect/Cause" + +/** @internal */ +export const CauseTypeId: Cause.CauseTypeId = Symbol.for( + CauseSymbolKey +) as Cause.CauseTypeId + +const variance = { + /* c8 ignore next */ + _E: (_: never) => _ +} + +/** @internal */ +const proto = { + [CauseTypeId]: variance, + [Hash.symbol](this: Cause.Cause): number { + return pipe( + Hash.hash(CauseSymbolKey), + Hash.combine(Hash.hash(flattenCause(this))), + Hash.cached(this) + ) + }, + [Equal.symbol](this: Cause.Cause, that: unknown): boolean { + return isCause(that) && causeEquals(this, that) + }, + pipe() { + return pipeArguments(this, arguments) + }, + toJSON(this: Cause.Cause) { + switch (this._tag) { + case "Empty": + return { _id: "Cause", _tag: this._tag } + case "Die": + return { _id: "Cause", _tag: this._tag, defect: toJSON(this.defect) } + case "Interrupt": + return { _id: "Cause", _tag: this._tag, fiberId: this.fiberId.toJSON() } + case "Fail": + return { _id: "Cause", _tag: this._tag, failure: toJSON(this.error) } + case "Sequential": + case "Parallel": + return { _id: "Cause", _tag: this._tag, left: toJSON(this.left), right: toJSON(this.right) } + } + }, + toString(this: Cause.Cause) { + return pretty(this) + }, + [NodeInspectSymbol](this: Cause.Cause) { + return this.toJSON() + } +} + +// ----------------------------------------------------------------------------- +// Constructors +// ----------------------------------------------------------------------------- + +/** @internal */ +export const empty: Cause.Cause = (() => { + const o = Object.create(proto) + o._tag = OpCodes.OP_EMPTY + return o +})() + +/** @internal */ +export const fail = (error: E): Cause.Cause => { + const o = Object.create(proto) + o._tag = OpCodes.OP_FAIL + o.error = error + return o +} + +/** @internal */ +export const die = (defect: unknown): Cause.Cause => { + const o = Object.create(proto) + o._tag = OpCodes.OP_DIE + o.defect = defect + return o +} + +/** @internal */ +export const interrupt = (fiberId: FiberId.FiberId): Cause.Cause => { + const o = Object.create(proto) + o._tag = OpCodes.OP_INTERRUPT + o.fiberId = fiberId + return o +} + +/** @internal */ +export const parallel = (left: Cause.Cause, right: Cause.Cause): Cause.Cause => { + const o = Object.create(proto) + o._tag = OpCodes.OP_PARALLEL + o.left = left + o.right = right + return o +} + +/** @internal */ +export const sequential = (left: Cause.Cause, right: Cause.Cause): Cause.Cause => { + const o = Object.create(proto) + o._tag = OpCodes.OP_SEQUENTIAL + o.left = left + o.right = right + return o +} + +// ----------------------------------------------------------------------------- +// Refinements +// ----------------------------------------------------------------------------- + +/** @internal */ +export const isCause = (u: unknown): u is Cause.Cause => hasProperty(u, CauseTypeId) + +/** @internal */ +export const isEmptyType = (self: Cause.Cause): self is Cause.Empty => self._tag === OpCodes.OP_EMPTY + +/** @internal */ +export const isFailType = (self: Cause.Cause): self is Cause.Fail => self._tag === OpCodes.OP_FAIL + +/** @internal */ +export const isDieType = (self: Cause.Cause): self is Cause.Die => self._tag === OpCodes.OP_DIE + +/** @internal */ +export const isInterruptType = (self: Cause.Cause): self is Cause.Interrupt => self._tag === OpCodes.OP_INTERRUPT + +/** @internal */ +export const isSequentialType = (self: Cause.Cause): self is Cause.Sequential => + self._tag === OpCodes.OP_SEQUENTIAL + +/** @internal */ +export const isParallelType = (self: Cause.Cause): self is Cause.Parallel => self._tag === OpCodes.OP_PARALLEL + +// ----------------------------------------------------------------------------- +// Getters +// ----------------------------------------------------------------------------- + +/** @internal */ +export const size = (self: Cause.Cause): number => reduceWithContext(self, void 0, SizeCauseReducer) + +/** @internal */ +export const isEmpty = (self: Cause.Cause): boolean => { + if (self._tag === OpCodes.OP_EMPTY) { + return true + } + return reduce(self, true, (acc, cause) => { + switch (cause._tag) { + case OpCodes.OP_EMPTY: { + return Option.some(acc) + } + case OpCodes.OP_DIE: + case OpCodes.OP_FAIL: + case OpCodes.OP_INTERRUPT: { + return Option.some(false) + } + default: { + return Option.none() + } + } + }) +} + +/** @internal */ +export const isFailure = (self: Cause.Cause): boolean => Option.isSome(failureOption(self)) + +/** @internal */ +export const isDie = (self: Cause.Cause): boolean => Option.isSome(dieOption(self)) + +/** @internal */ +export const isInterrupted = (self: Cause.Cause): boolean => Option.isSome(interruptOption(self)) + +/** @internal */ +export const isInterruptedOnly = (self: Cause.Cause): boolean => + reduceWithContext(undefined, IsInterruptedOnlyCauseReducer)(self) + +/** @internal */ +export const failures = (self: Cause.Cause): Chunk.Chunk => + Chunk.reverse( + reduce, E>( + self, + Chunk.empty(), + (list, cause) => + cause._tag === OpCodes.OP_FAIL ? + Option.some(pipe(list, Chunk.prepend(cause.error))) : + Option.none() + ) + ) + +/** @internal */ +export const defects = (self: Cause.Cause): Chunk.Chunk => + Chunk.reverse( + reduce, E>( + self, + Chunk.empty(), + (list, cause) => + cause._tag === OpCodes.OP_DIE ? + Option.some(pipe(list, Chunk.prepend(cause.defect))) : + Option.none() + ) + ) + +/** @internal */ +export const interruptors = (self: Cause.Cause): HashSet.HashSet => + reduce(self, HashSet.empty(), (set, cause) => + cause._tag === OpCodes.OP_INTERRUPT ? + Option.some(pipe(set, HashSet.add(cause.fiberId))) : + Option.none()) + +/** @internal */ +export const failureOption = (self: Cause.Cause): Option.Option => + find(self, (cause) => + cause._tag === OpCodes.OP_FAIL ? + Option.some(cause.error) : + Option.none()) + +/** @internal */ +export const failureOrCause = (self: Cause.Cause): Either.Either, E> => { + const option = failureOption(self) + switch (option._tag) { + case "None": { + // no `E` inside this `Cause`, so it can be safely cast to `never` + return Either.right(self as Cause.Cause) + } + case "Some": { + return Either.left(option.value) + } + } +} + +/** @internal */ +export const dieOption = (self: Cause.Cause): Option.Option => + find(self, (cause) => + cause._tag === OpCodes.OP_DIE ? + Option.some(cause.defect) : + Option.none()) + +/** @internal */ +export const flipCauseOption = (self: Cause.Cause>): Option.Option> => + match(self, { + onEmpty: Option.some>(empty), + onFail: Option.map(fail), + onDie: (defect) => Option.some(die(defect)), + onInterrupt: (fiberId) => Option.some(interrupt(fiberId)), + onSequential: Option.mergeWith(sequential), + onParallel: Option.mergeWith(parallel) + }) + +/** @internal */ +export const interruptOption = (self: Cause.Cause): Option.Option => + find(self, (cause) => + cause._tag === OpCodes.OP_INTERRUPT ? + Option.some(cause.fiberId) : + Option.none()) + +/** @internal */ +export const keepDefects = (self: Cause.Cause): Option.Option> => + match(self, { + onEmpty: Option.none(), + onFail: () => Option.none(), + onDie: (defect) => Option.some(die(defect)), + onInterrupt: () => Option.none(), + onSequential: Option.mergeWith(sequential), + onParallel: Option.mergeWith(parallel) + }) + +/** @internal */ +export const keepDefectsAndElectFailures = (self: Cause.Cause): Option.Option> => + match(self, { + onEmpty: Option.none(), + onFail: (failure) => Option.some(die(failure)), + onDie: (defect) => Option.some(die(defect)), + onInterrupt: () => Option.none(), + onSequential: Option.mergeWith(sequential), + onParallel: Option.mergeWith(parallel) + }) + +/** @internal */ +export const linearize = (self: Cause.Cause): HashSet.HashSet> => + match(self, { + onEmpty: HashSet.empty(), + onFail: (error) => HashSet.make(fail(error)), + onDie: (defect) => HashSet.make(die(defect)), + onInterrupt: (fiberId) => HashSet.make(interrupt(fiberId)), + onSequential: (leftSet, rightSet) => + HashSet.flatMap(leftSet, (leftCause) => HashSet.map(rightSet, (rightCause) => sequential(leftCause, rightCause))), + onParallel: (leftSet, rightSet) => + HashSet.flatMap(leftSet, (leftCause) => HashSet.map(rightSet, (rightCause) => parallel(leftCause, rightCause))) + }) + +/** @internal */ +export const stripFailures = (self: Cause.Cause): Cause.Cause => + match(self, { + onEmpty: empty, + onFail: () => empty, + onDie: die, + onInterrupt: interrupt, + onSequential: sequential, + onParallel: parallel + }) + +/** @internal */ +export const electFailures = (self: Cause.Cause): Cause.Cause => + match(self, { + onEmpty: empty, + onFail: die, + onDie: die, + onInterrupt: interrupt, + onSequential: sequential, + onParallel: parallel + }) + +/** @internal */ +export const stripSomeDefects = dual< + (pf: (defect: unknown) => Option.Option) => (self: Cause.Cause) => Option.Option>, + (self: Cause.Cause, pf: (defect: unknown) => Option.Option) => Option.Option> +>( + 2, + (self: Cause.Cause, pf: (defect: unknown) => Option.Option): Option.Option> => + match(self, { + onEmpty: Option.some>(empty), + onFail: (error) => Option.some(fail(error)), + onDie: (defect) => { + const option = pf(defect) + return Option.isSome(option) ? Option.none() : Option.some(die(defect)) + }, + onInterrupt: (fiberId) => Option.some(interrupt(fiberId)), + onSequential: Option.mergeWith(sequential), + onParallel: Option.mergeWith(parallel) + }) +) + +// ----------------------------------------------------------------------------- +// Mapping +// ----------------------------------------------------------------------------- + +/** @internal */ +export const as = dual< + (error: E2) => (self: Cause.Cause) => Cause.Cause, + (self: Cause.Cause, error: E2) => Cause.Cause +>(2, (self, error) => map(self, () => error)) + +/** @internal */ +export const map = dual< + (f: (e: E) => E2) => (self: Cause.Cause) => Cause.Cause, + (self: Cause.Cause, f: (e: E) => E2) => Cause.Cause +>(2, (self, f) => flatMap(self, (e) => fail(f(e)))) + +// ----------------------------------------------------------------------------- +// Sequencing +// ----------------------------------------------------------------------------- + +/** @internal */ +export const flatMap = dual< + (f: (e: E) => Cause.Cause) => (self: Cause.Cause) => Cause.Cause, + (self: Cause.Cause, f: (e: E) => Cause.Cause) => Cause.Cause +>(2, (self, f) => + match(self, { + onEmpty: empty, + onFail: (error) => f(error), + onDie: (defect) => die(defect), + onInterrupt: (fiberId) => interrupt(fiberId), + onSequential: (left, right) => sequential(left, right), + onParallel: (left, right) => parallel(left, right) + })) + +/** @internal */ +export const flatten = (self: Cause.Cause>): Cause.Cause => flatMap(self, identity) + +/** @internal */ +export const andThen: { + (f: (e: E) => Cause.Cause): (self: Cause.Cause) => Cause.Cause + (f: Cause.Cause): (self: Cause.Cause) => Cause.Cause + (self: Cause.Cause, f: (e: E) => Cause.Cause): Cause.Cause + (self: Cause.Cause, f: Cause.Cause): Cause.Cause +} = dual( + 2, + (self: Cause.Cause, f: ((e: E) => Cause.Cause) | Cause.Cause): Cause.Cause => + isFunction(f) ? flatMap(self, f) : flatMap(self, () => f) +) + +// ----------------------------------------------------------------------------- +// Equality +// ----------------------------------------------------------------------------- + +/** @internal */ +export const contains = dual< + (that: Cause.Cause) => (self: Cause.Cause) => boolean, + (self: Cause.Cause, that: Cause.Cause) => boolean +>(2, (self, that) => { + if (that._tag === OpCodes.OP_EMPTY || self === that) { + return true + } + return reduce(self, false, (accumulator, cause) => { + return Option.some(accumulator || causeEquals(cause, that)) + }) +}) + +/** @internal */ +const causeEquals = (left: Cause.Cause, right: Cause.Cause): boolean => { + let leftStack: Chunk.Chunk> = Chunk.of(left) + let rightStack: Chunk.Chunk> = Chunk.of(right) + while (Chunk.isNonEmpty(leftStack) && Chunk.isNonEmpty(rightStack)) { + const [leftParallel, leftSequential] = pipe( + Chunk.headNonEmpty(leftStack), + reduce( + [HashSet.empty(), Chunk.empty>()] as const, + ([parallel, sequential], cause) => { + const [par, seq] = evaluateCause(cause) + return Option.some( + [ + pipe(parallel, HashSet.union(par)), + pipe(sequential, Chunk.appendAll(seq)) + ] as const + ) + } + ) + ) + const [rightParallel, rightSequential] = pipe( + Chunk.headNonEmpty(rightStack), + reduce( + [HashSet.empty(), Chunk.empty>()] as const, + ([parallel, sequential], cause) => { + const [par, seq] = evaluateCause(cause) + return Option.some( + [ + pipe(parallel, HashSet.union(par)), + pipe(sequential, Chunk.appendAll(seq)) + ] as const + ) + } + ) + ) + if (!Equal.equals(leftParallel, rightParallel)) { + return false + } + leftStack = leftSequential + rightStack = rightSequential + } + return true +} + +// ----------------------------------------------------------------------------- +// Flattening +// ----------------------------------------------------------------------------- + +/** + * Flattens a cause to a sequence of sets of causes, where each set represents + * causes that fail in parallel and sequential sets represent causes that fail + * after each other. + * + * @internal + */ +const flattenCause = (cause: Cause.Cause): Chunk.Chunk> => { + return flattenCauseLoop(Chunk.of(cause), Chunk.empty()) +} + +/** @internal */ +const flattenCauseLoop = ( + causes: Chunk.Chunk>, + flattened: Chunk.Chunk> +): Chunk.Chunk> => { + // eslint-disable-next-line no-constant-condition + while (1) { + const [parallel, sequential] = pipe( + causes, + Arr.reduce( + [HashSet.empty(), Chunk.empty>()] as const, + ([parallel, sequential], cause) => { + const [par, seq] = evaluateCause(cause) + return [ + pipe(parallel, HashSet.union(par)), + pipe(sequential, Chunk.appendAll(seq)) + ] + } + ) + ) + const updated = HashSet.size(parallel) > 0 ? + pipe(flattened, Chunk.prepend(parallel)) : + flattened + if (Chunk.isEmpty(sequential)) { + return Chunk.reverse(updated) + } + causes = sequential + flattened = updated + } + throw new Error(getBugErrorMessage("Cause.flattenCauseLoop")) +} + +// ----------------------------------------------------------------------------- +// Finding +// ----------------------------------------------------------------------------- + +/** @internal */ +export const find = dual< + (pf: (cause: Cause.Cause) => Option.Option) => (self: Cause.Cause) => Option.Option, + (self: Cause.Cause, pf: (cause: Cause.Cause) => Option.Option) => Option.Option +>(2, (self: Cause.Cause, pf: (cause: Cause.Cause) => Option.Option) => { + const stack: Array> = [self] + while (stack.length > 0) { + const item = stack.pop()! + const option = pf(item) + switch (option._tag) { + case "None": { + switch (item._tag) { + case OpCodes.OP_SEQUENTIAL: + case OpCodes.OP_PARALLEL: { + stack.push(item.right) + stack.push(item.left) + break + } + } + break + } + case "Some": { + return option + } + } + } + return Option.none() +}) + +// ----------------------------------------------------------------------------- +// Filtering +// ----------------------------------------------------------------------------- + +/** @internal */ +export const filter: { + ( + refinement: Refinement>, Cause.Cause> + ): (self: Cause.Cause) => Cause.Cause + (predicate: Predicate>>): (self: Cause.Cause) => Cause.Cause + (self: Cause.Cause, refinement: Refinement, Cause.Cause>): Cause.Cause + (self: Cause.Cause, predicate: Predicate>): Cause.Cause +} = dual( + 2, + (self: Cause.Cause, predicate: Predicate>): Cause.Cause => + reduceWithContext(self, void 0, FilterCauseReducer(predicate)) +) + +// ----------------------------------------------------------------------------- +// Evaluation +// ----------------------------------------------------------------------------- + +/** + * Takes one step in evaluating a cause, returning a set of causes that fail + * in parallel and a list of causes that fail sequentially after those causes. + * + * @internal + */ +const evaluateCause = ( + self: Cause.Cause +): [HashSet.HashSet, Chunk.Chunk>] => { + let cause: Cause.Cause | undefined = self + const stack: Array> = [] + let _parallel = HashSet.empty() + let _sequential = Chunk.empty>() + while (cause !== undefined) { + switch (cause._tag) { + case OpCodes.OP_EMPTY: { + if (stack.length === 0) { + return [_parallel, _sequential] + } + cause = stack.pop() + break + } + case OpCodes.OP_FAIL: { + _parallel = HashSet.add(_parallel, Chunk.make(cause._tag, cause.error)) + if (stack.length === 0) { + return [_parallel, _sequential] + } + cause = stack.pop() + break + } + case OpCodes.OP_DIE: { + _parallel = HashSet.add(_parallel, Chunk.make(cause._tag, cause.defect)) + if (stack.length === 0) { + return [_parallel, _sequential] + } + cause = stack.pop() + break + } + case OpCodes.OP_INTERRUPT: { + _parallel = HashSet.add(_parallel, Chunk.make(cause._tag, cause.fiberId as unknown)) + if (stack.length === 0) { + return [_parallel, _sequential] + } + cause = stack.pop() + break + } + case OpCodes.OP_SEQUENTIAL: { + switch (cause.left._tag) { + case OpCodes.OP_EMPTY: { + cause = cause.right + break + } + case OpCodes.OP_SEQUENTIAL: { + cause = sequential(cause.left.left, sequential(cause.left.right, cause.right)) + break + } + case OpCodes.OP_PARALLEL: { + cause = parallel( + sequential(cause.left.left, cause.right), + sequential(cause.left.right, cause.right) + ) + break + } + default: { + _sequential = Chunk.prepend(_sequential, cause.right) + cause = cause.left + break + } + } + break + } + case OpCodes.OP_PARALLEL: { + stack.push(cause.right) + cause = cause.left + break + } + } + } + throw new Error(getBugErrorMessage("Cause.evaluateCauseLoop")) +} + +// ----------------------------------------------------------------------------- +// Reducing +// ----------------------------------------------------------------------------- + +/** @internal */ +const SizeCauseReducer: Cause.CauseReducer = { + emptyCase: () => 0, + failCase: () => 1, + dieCase: () => 1, + interruptCase: () => 1, + sequentialCase: (_, left, right) => left + right, + parallelCase: (_, left, right) => left + right +} + +/** @internal */ +const IsInterruptedOnlyCauseReducer: Cause.CauseReducer = { + emptyCase: constTrue, + failCase: constFalse, + dieCase: constFalse, + interruptCase: constTrue, + sequentialCase: (_, left, right) => left && right, + parallelCase: (_, left, right) => left && right +} + +/** @internal */ +const FilterCauseReducer = ( + predicate: Predicate> +): Cause.CauseReducer> => ({ + emptyCase: () => empty, + failCase: (_, error) => fail(error), + dieCase: (_, defect) => die(defect), + interruptCase: (_, fiberId) => interrupt(fiberId), + sequentialCase: (_, left, right) => { + if (predicate(left)) { + if (predicate(right)) { + return sequential(left, right) + } + return left + } + if (predicate(right)) { + return right + } + return empty + }, + parallelCase: (_, left, right) => { + if (predicate(left)) { + if (predicate(right)) { + return parallel(left, right) + } + return left + } + if (predicate(right)) { + return right + } + return empty + } +}) + +/** @internal */ +type CauseCase = SequentialCase | ParallelCase + +const OP_SEQUENTIAL_CASE = "SequentialCase" + +const OP_PARALLEL_CASE = "ParallelCase" + +/** @internal */ +interface SequentialCase { + readonly _tag: typeof OP_SEQUENTIAL_CASE +} + +/** @internal */ +interface ParallelCase { + readonly _tag: typeof OP_PARALLEL_CASE +} + +/** @internal */ +export const match = dual< + ( + options: { + readonly onEmpty: Z + readonly onFail: (error: E) => Z + readonly onDie: (defect: unknown) => Z + readonly onInterrupt: (fiberId: FiberId.FiberId) => Z + readonly onSequential: (left: Z, right: Z) => Z + readonly onParallel: (left: Z, right: Z) => Z + } + ) => (self: Cause.Cause) => Z, + ( + self: Cause.Cause, + options: { + readonly onEmpty: Z + readonly onFail: (error: E) => Z + readonly onDie: (defect: unknown) => Z + readonly onInterrupt: (fiberId: FiberId.FiberId) => Z + readonly onSequential: (left: Z, right: Z) => Z + readonly onParallel: (left: Z, right: Z) => Z + } + ) => Z +>(2, (self, { onDie, onEmpty, onFail, onInterrupt, onParallel, onSequential }) => { + return reduceWithContext(self, void 0, { + emptyCase: () => onEmpty, + failCase: (_, error) => onFail(error), + dieCase: (_, defect) => onDie(defect), + interruptCase: (_, fiberId) => onInterrupt(fiberId), + sequentialCase: (_, left, right) => onSequential(left, right), + parallelCase: (_, left, right) => onParallel(left, right) + }) +}) + +/** @internal */ +export const reduce = dual< + (zero: Z, pf: (accumulator: Z, cause: Cause.Cause) => Option.Option) => (self: Cause.Cause) => Z, + (self: Cause.Cause, zero: Z, pf: (accumulator: Z, cause: Cause.Cause) => Option.Option) => Z +>(3, (self: Cause.Cause, zero: Z, pf: (accumulator: Z, cause: Cause.Cause) => Option.Option) => { + let accumulator: Z = zero + let cause: Cause.Cause | undefined = self + const causes: Array> = [] + while (cause !== undefined) { + const option = pf(accumulator, cause) + accumulator = Option.isSome(option) ? option.value : accumulator + switch (cause._tag) { + case OpCodes.OP_SEQUENTIAL: { + causes.push(cause.right) + cause = cause.left + break + } + case OpCodes.OP_PARALLEL: { + causes.push(cause.right) + cause = cause.left + break + } + default: { + cause = undefined + break + } + } + if (cause === undefined && causes.length > 0) { + cause = causes.pop()! + } + } + return accumulator +}) + +/** @internal */ +export const reduceWithContext = dual< + (context: C, reducer: Cause.CauseReducer) => (self: Cause.Cause) => Z, + (self: Cause.Cause, context: C, reducer: Cause.CauseReducer) => Z +>(3, (self: Cause.Cause, context: C, reducer: Cause.CauseReducer) => { + const input: Array> = [self] + const output: Array> = [] + while (input.length > 0) { + const cause = input.pop()! + switch (cause._tag) { + case OpCodes.OP_EMPTY: { + output.push(Either.right(reducer.emptyCase(context))) + break + } + case OpCodes.OP_FAIL: { + output.push(Either.right(reducer.failCase(context, cause.error))) + break + } + case OpCodes.OP_DIE: { + output.push(Either.right(reducer.dieCase(context, cause.defect))) + break + } + case OpCodes.OP_INTERRUPT: { + output.push(Either.right(reducer.interruptCase(context, cause.fiberId))) + break + } + case OpCodes.OP_SEQUENTIAL: { + input.push(cause.right) + input.push(cause.left) + output.push(Either.left({ _tag: OP_SEQUENTIAL_CASE })) + break + } + case OpCodes.OP_PARALLEL: { + input.push(cause.right) + input.push(cause.left) + output.push(Either.left({ _tag: OP_PARALLEL_CASE })) + break + } + } + } + const accumulator: Array = [] + while (output.length > 0) { + const either = output.pop()! + switch (either._tag) { + case "Left": { + switch (either.left._tag) { + case OP_SEQUENTIAL_CASE: { + const left = accumulator.pop()! + const right = accumulator.pop()! + const value = reducer.sequentialCase(context, left, right) + accumulator.push(value) + break + } + case OP_PARALLEL_CASE: { + const left = accumulator.pop()! + const right = accumulator.pop()! + const value = reducer.parallelCase(context, left, right) + accumulator.push(value) + break + } + } + break + } + case "Right": { + accumulator.push(either.right) + break + } + } + } + if (accumulator.length === 0) { + throw new Error( + "BUG: Cause.reduceWithContext - please report an issue at https://github.com/Effect-TS/effect/issues" + ) + } + return accumulator.pop()! +}) + +// ----------------------------------------------------------------------------- +// Pretty Printing +// ----------------------------------------------------------------------------- + +/** @internal */ +export const pretty = (cause: Cause.Cause, options?: { + readonly renderErrorCause?: boolean | undefined +}): string => { + if (isInterruptedOnly(cause)) { + return "All fibers interrupted without errors." + } + return prettyErrors(cause).map(function(e) { + if (options?.renderErrorCause !== true || e.cause === undefined) { + return e.stack + } + return `${e.stack} {\n${renderErrorCause(e.cause as Cause.PrettyError, " ")}\n}` + }).join("\n") +} + +const renderErrorCause = (cause: Cause.PrettyError, prefix: string) => { + const lines = cause.stack!.split("\n") + let stack = `${prefix}[cause]: ${lines[0]}` + for (let i = 1, len = lines.length; i < len; i++) { + stack += `\n${prefix}${lines[i]}` + } + if (cause.cause) { + stack += ` {\n${renderErrorCause(cause.cause as Cause.PrettyError, `${prefix} `)}\n${prefix}}` + } + return stack +} + +/** @internal */ +export const makePrettyError = (originalError: unknown): Cause.PrettyError => { + const originalErrorIsObject = typeof originalError === "object" && originalError !== null + const prevLimit = Error.stackTraceLimit + Error.stackTraceLimit = 1 + const error = new Error( + prettyErrorMessage(originalError), + originalErrorIsObject && "cause" in originalError && typeof originalError.cause !== "undefined" + ? { cause: makePrettyError(originalError.cause) } + : undefined + ) as Types.Mutable + Error.stackTraceLimit = prevLimit + if (error.message === "") { + error.message = "An error has occurred" + } + Error.stackTraceLimit = prevLimit + error.name = originalError instanceof Error ? originalError.name : "Error" + if (originalErrorIsObject) { + if (spanSymbol in originalError) { + error.span = originalError[spanSymbol] as Span + } + Object.keys(originalError).forEach((key) => { + if (!(key in error)) { + // @ts-expect-error + error[key] = originalError[key] + } + }) + } + error.stack = prettyErrorStack( + `${error.name}: ${error.message}`, + originalError instanceof Error && originalError.stack + ? originalError.stack + : "", + error.span + ) + return error +} + +/** + * A utility function for generating human-readable error messages from a generic error of type `unknown`. + * + * Rules: + * + * 1) If the input `u` is already a string, it's considered a message. + * 2) If `u` is an Error instance with a message defined, it uses the message. + * 3) If `u` has a user-defined `toString()` method, it uses that method. + * 4) Otherwise, it uses `Inspectable.stringifyCircular` to produce a string representation and uses it as the error message, + * with "Error" added as a prefix. + * + * @internal + */ +export const prettyErrorMessage = (u: unknown): string => { + // 1) + if (typeof u === "string") { + return u + } + // 2) + if (typeof u === "object" && u !== null && u instanceof Error) { + return u.message + } + // 3) + try { + if ( + hasProperty(u, "toString") && + isFunction(u["toString"]) && + u["toString"] !== Object.prototype.toString && + u["toString"] !== globalThis.Array.prototype.toString + ) { + return u["toString"]() + } + } catch { + // something's off, rollback to json + } + // 4) + return stringifyCircular(u) +} + +const locationRegex = /\((.*)\)/g + +/** @internal */ +export const spanToTrace = globalValue("effect/Tracer/spanToTrace", () => new WeakMap()) + +const prettyErrorStack = (message: string, stack: string, span?: Span | undefined): string => { + const out: Array = [message] + const lines = stack.startsWith(message) ? stack.slice(message.length).split("\n") : stack.split("\n") + + for (let i = 1; i < lines.length; i++) { + if (lines[i].includes(" at new BaseEffectError") || lines[i].includes(" at new YieldableError")) { + i++ + continue + } + if (lines[i].includes("Generator.next")) { + break + } + if (lines[i].includes("effect_internal_function")) { + break + } + out.push( + lines[i] + .replace(/at .*effect_instruction_i.*\((.*)\)/, "at $1") + .replace(/EffectPrimitive\.\w+/, "") + ) + } + + if (span) { + let current: Span | AnySpan | undefined = span + let i = 0 + while (current && current._tag === "Span" && i < 10) { + const stackFn = spanToTrace.get(current) + if (typeof stackFn === "function") { + const stack = stackFn() + if (typeof stack === "string") { + const locationMatchAll = stack.matchAll(locationRegex) + let match = false + for (const [, location] of locationMatchAll) { + match = true + out.push(` at ${current.name} (${location})`) + } + if (!match) { + out.push(` at ${current.name} (${stack.replace(/^at /, "")})`) + } + } else { + out.push(` at ${current.name}`) + } + } else { + out.push(` at ${current.name}`) + } + current = Option.getOrUndefined(current.parent) + i++ + } + } + + return out.join("\n") +} + +/** @internal */ +export const spanSymbol = Symbol.for("effect/SpanAnnotation") + +/** @internal */ +export const prettyErrors = (cause: Cause.Cause): Array => + reduceWithContext(cause, void 0, { + emptyCase: (): Array => [], + dieCase: (_, unknownError) => { + return [makePrettyError(unknownError)] + }, + failCase: (_, error) => { + return [makePrettyError(error)] + }, + interruptCase: () => [], + parallelCase: (_, l, r) => [...l, ...r], + sequentialCase: (_, l, r) => [...l, ...r] + }) diff --git a/repos/effect/packages/effect/src/internal/channel.ts b/repos/effect/packages/effect/src/internal/channel.ts new file mode 100644 index 0000000..f0710db --- /dev/null +++ b/repos/effect/packages/effect/src/internal/channel.ts @@ -0,0 +1,2603 @@ +import * as Cause from "../Cause.js" +import type * as Channel from "../Channel.js" +import * as Chunk from "../Chunk.js" +import * as Context from "../Context.js" +import * as Deferred from "../Deferred.js" +import * as Effect from "../Effect.js" +import * as Either from "../Either.js" +import * as Equal from "../Equal.js" +import * as Exit from "../Exit.js" +import * as Fiber from "../Fiber.js" +import * as FiberRef from "../FiberRef.js" +import { constVoid, dual, identity, pipe } from "../Function.js" +import type { LazyArg } from "../Function.js" +import * as Layer from "../Layer.js" +import type * as MergeDecision from "../MergeDecision.js" +import type * as MergeState from "../MergeState.js" +import type * as MergeStrategy from "../MergeStrategy.js" +import * as Option from "../Option.js" +import { hasProperty, type Predicate } from "../Predicate.js" +import * as PubSub from "../PubSub.js" +import * as Queue from "../Queue.js" +import * as Ref from "../Ref.js" +import * as Scope from "../Scope.js" +import type * as SingleProducerAsyncInput from "../SingleProducerAsyncInput.js" +import type * as Tracer from "../Tracer.js" +import type * as Types from "../Types.js" +import * as executor from "./channel/channelExecutor.js" +import type * as ChannelState from "./channel/channelState.js" +import * as mergeDecision from "./channel/mergeDecision.js" +import * as mergeState from "./channel/mergeState.js" +import * as mergeStrategy_ from "./channel/mergeStrategy.js" +import * as singleProducerAsyncInput from "./channel/singleProducerAsyncInput.js" +import * as coreEffect from "./core-effect.js" +import * as core from "./core-stream.js" +import * as MergeDecisionOpCodes from "./opCodes/channelMergeDecision.js" +import * as MergeStateOpCodes from "./opCodes/channelMergeState.js" +import * as ChannelStateOpCodes from "./opCodes/channelState.js" +import * as tracer from "./tracer.js" + +/** @internal */ +export const acquireUseRelease = ( + acquire: Effect.Effect, + use: (a: Acquired) => Channel.Channel, + release: (a: Acquired, exit: Exit.Exit) => Effect.Effect +): Channel.Channel => + core.flatMap( + core.fromEffect( + Ref.make< + (exit: Exit.Exit) => Effect.Effect + >(() => Effect.void) + ), + (ref) => + pipe( + core.fromEffect( + Effect.uninterruptible( + Effect.tap( + acquire, + (a) => Ref.set(ref, (exit) => release(a, exit)) + ) + ) + ), + core.flatMap(use), + core.ensuringWith((exit) => Effect.flatMap(Ref.get(ref), (f) => f(exit))) + ) + ) + +/** @internal */ +export const as = dual< + ( + value: OutDone2 + ) => ( + self: Channel.Channel + ) => Channel.Channel, + ( + self: Channel.Channel, + value: OutDone2 + ) => Channel.Channel +>(2, ( + self: Channel.Channel, + value: OutDone2 +): Channel.Channel => map(self, () => value)) + +/** @internal */ +export const asVoid = ( + self: Channel.Channel +): Channel.Channel => map(self, constVoid) + +/** @internal */ +export const buffer = ( + options: { + readonly empty: InElem + readonly isEmpty: Predicate + readonly ref: Ref.Ref + } +): Channel.Channel => + core.suspend(() => { + const doBuffer = ( + empty: InElem, + isEmpty: Predicate, + ref: Ref.Ref + ): Channel.Channel => + unwrap( + Ref.modify(ref, (inElem) => + isEmpty(inElem) ? + [ + core.readWith({ + onInput: (input: InElem) => + core.flatMap( + core.write(input), + () => doBuffer(empty, isEmpty, ref) + ), + onFailure: (error: InErr) => core.fail(error), + onDone: (done: InDone) => core.succeedNow(done) + }), + inElem + ] as const : + [ + core.flatMap( + core.write(inElem), + () => doBuffer(empty, isEmpty, ref) + ), + empty + ] as const) + ) + return doBuffer(options.empty, options.isEmpty, options.ref) + }) + +/** @internal */ +export const bufferChunk = ( + ref: Ref.Ref> +): Channel.Channel, Chunk.Chunk, InErr, InErr, InDone, InDone> => + buffer({ + empty: Chunk.empty(), + isEmpty: Chunk.isEmpty, + ref + }) + +/** @internal */ +export const catchAll = dual< + ( + f: (error: OutErr) => Channel.Channel + ) => ( + self: Channel.Channel + ) => Channel.Channel< + OutElem1 | OutElem, + InElem & InElem1, + OutErr1, + InErr & InErr1, + OutDone1 | OutDone, + InDone & InDone1, + Env1 | Env + >, + ( + self: Channel.Channel, + f: (error: OutErr) => Channel.Channel + ) => Channel.Channel< + OutElem1 | OutElem, + InElem & InElem1, + OutErr1, + InErr & InErr1, + OutDone1 | OutDone, + InDone & InDone1, + Env1 | Env + > +>( + 2, + ( + self: Channel.Channel, + f: (error: OutErr) => Channel.Channel + ): Channel.Channel< + OutElem | OutElem1, + InElem & InElem1, + OutErr1, + InErr & InErr1, + OutDone | OutDone1, + InDone & InDone1, + Env | Env1 + > => + core.catchAllCause(self, (cause) => + Either.match(Cause.failureOrCause(cause), { + onLeft: f, + onRight: core.failCause + })) +) + +/** @internal */ +export const concatMap = dual< + ( + f: (o: OutElem) => Channel.Channel + ) => ( + self: Channel.Channel + ) => Channel.Channel< + OutElem2, + InElem & InElem2, + OutErr2 | OutErr, + InErr & InErr2, + unknown, + InDone & InDone2, + Env2 | Env + >, + ( + self: Channel.Channel, + f: (o: OutElem) => Channel.Channel + ) => Channel.Channel< + OutElem2, + InElem & InElem2, + OutErr2 | OutErr, + InErr & InErr2, + unknown, + InDone & InDone2, + Env2 | Env + > +>(2, ( + self: Channel.Channel, + f: (o: OutElem) => Channel.Channel +): Channel.Channel< + OutElem2, + InElem & InElem2, + OutErr | OutErr2, + InErr & InErr2, + unknown, + InDone & InDone2, + Env | Env2 +> => core.concatMapWith(self, f, () => void 0, () => void 0)) + +/** @internal */ +export const collect = dual< + ( + pf: (o: OutElem) => Option.Option + ) => ( + self: Channel.Channel + ) => Channel.Channel, + ( + self: Channel.Channel, + pf: (o: OutElem) => Option.Option + ) => Channel.Channel +>(2, ( + self: Channel.Channel, + pf: (o: OutElem) => Option.Option +): Channel.Channel => { + const collector: Channel.Channel = core + .readWith({ + onInput: (out) => + Option.match(pf(out), { + onNone: () => collector, + onSome: (out2) => core.flatMap(core.write(out2), () => collector) + }), + onFailure: core.fail, + onDone: core.succeedNow + }) + return core.pipeTo(self, collector) +}) + +/** @internal */ +export const concatOut = ( + self: Channel.Channel< + Channel.Channel, + InElem, + OutErr, + InErr, + OutDone, + InDone, + Env + > +): Channel.Channel => core.concatAll(self) + +/** @internal */ +export const mapInput = dual< + ( + f: (a: InDone0) => InDone + ) => ( + self: Channel.Channel + ) => Channel.Channel, + ( + self: Channel.Channel, + f: (a: InDone0) => InDone + ) => Channel.Channel +>(2, ( + self: Channel.Channel, + f: (a: InDone0) => InDone +): Channel.Channel => { + const reader: Channel.Channel = core.readWith({ + onInput: (inElem: InElem) => core.flatMap(core.write(inElem), () => reader), + onFailure: core.fail, + onDone: (done: InDone0) => core.succeedNow(f(done)) + }) + return core.pipeTo(reader, self) +}) + +/** @internal */ +export const mapInputEffect = dual< + ( + f: (i: InDone0) => Effect.Effect + ) => ( + self: Channel.Channel + ) => Channel.Channel, + ( + self: Channel.Channel, + f: (i: InDone0) => Effect.Effect + ) => Channel.Channel +>(2, ( + self: Channel.Channel, + f: (i: InDone0) => Effect.Effect +): Channel.Channel => { + const reader: Channel.Channel = core.readWith({ + onInput: (inElem) => core.flatMap(core.write(inElem), () => reader), + onFailure: core.fail, + onDone: (done) => core.fromEffect(f(done)) + }) + return core.pipeTo(reader, self) +}) + +/** @internal */ +export const mapInputError = dual< + ( + f: (a: InErr0) => InErr + ) => ( + self: Channel.Channel + ) => Channel.Channel, + ( + self: Channel.Channel, + f: (a: InErr0) => InErr + ) => Channel.Channel +>(2, ( + self: Channel.Channel, + f: (a: InErr0) => InErr +): Channel.Channel => { + const reader: Channel.Channel = core.readWith({ + onInput: (inElem: InElem) => core.flatMap(core.write(inElem), () => reader), + onFailure: (error) => core.fail(f(error)), + onDone: core.succeedNow + }) + return core.pipeTo(reader, self) +}) + +/** @internal */ +export const mapInputErrorEffect = dual< + ( + f: (error: InErr0) => Effect.Effect + ) => ( + self: Channel.Channel + ) => Channel.Channel, + ( + self: Channel.Channel, + f: (error: InErr0) => Effect.Effect + ) => Channel.Channel +>(2, ( + self: Channel.Channel, + f: (error: InErr0) => Effect.Effect +): Channel.Channel => { + const reader: Channel.Channel = core.readWith({ + onInput: (inElem) => core.flatMap(core.write(inElem), () => reader), + onFailure: (error) => core.fromEffect(f(error)), + onDone: core.succeedNow + }) + return core.pipeTo(reader, self) +}) + +/** @internal */ +export const mapInputIn = dual< + ( + f: (a: InElem0) => InElem + ) => ( + self: Channel.Channel + ) => Channel.Channel, + ( + self: Channel.Channel, + f: (a: InElem0) => InElem + ) => Channel.Channel +>(2, ( + self: Channel.Channel, + f: (a: InElem0) => InElem +): Channel.Channel => { + const reader: Channel.Channel = core.readWith({ + onInput: (inElem) => core.flatMap(core.write(f(inElem)), () => reader), + onFailure: core.fail, + onDone: core.succeedNow + }) + return core.pipeTo(reader, self) +}) + +/** @internal */ +export const mapInputInEffect = dual< + ( + f: (a: InElem0) => Effect.Effect + ) => ( + self: Channel.Channel + ) => Channel.Channel, + ( + self: Channel.Channel, + f: (a: InElem0) => Effect.Effect + ) => Channel.Channel +>(2, ( + self: Channel.Channel, + f: (a: InElem0) => Effect.Effect +): Channel.Channel => { + const reader: Channel.Channel = core.readWith({ + onInput: (inElem) => core.flatMap(core.flatMap(core.fromEffect(f(inElem)), core.write), () => reader), + onFailure: core.fail, + onDone: core.succeedNow + }) + return core.pipeTo(reader, self) +}) + +/** @internal */ +export const doneCollect = ( + self: Channel.Channel +): Channel.Channel, OutDone], InDone, Env> => + core.suspend(() => { + const builder: Array = [] + return pipe( + core.pipeTo(self, doneCollectReader(builder)), + core.flatMap((outDone) => core.succeed([Chunk.unsafeFromArray(builder), outDone])) + ) + }) + +/** @internal */ +const doneCollectReader = ( + builder: Array +): Channel.Channel => { + return core.readWith({ + onInput: (outElem) => + core.flatMap( + core.sync(() => { + builder.push(outElem) + }), + () => doneCollectReader(builder) + ), + onFailure: core.fail, + onDone: core.succeed + }) +} + +/** @internal */ +export const drain = ( + self: Channel.Channel +): Channel.Channel => { + const drainer: Channel.Channel = core + .readWithCause({ + onInput: () => drainer, + onFailure: core.failCause, + onDone: core.succeed + }) + return core.pipeTo(self, drainer) +} + +/** @internal */ +export const emitCollect = ( + self: Channel.Channel +): Channel.Channel<[Chunk.Chunk, OutDone], InElem, OutErr, InErr, void, InDone, Env> => + core.flatMap(doneCollect(self), core.write) + +/** @internal */ +export const ensuring = dual< + ( + finalizer: Effect.Effect + ) => ( + self: Channel.Channel + ) => Channel.Channel, + ( + self: Channel.Channel, + finalizer: Effect.Effect + ) => Channel.Channel +>(2, ( + self: Channel.Channel, + finalizer: Effect.Effect +): Channel.Channel => + core.ensuringWith(self, () => finalizer)) + +/** @internal */ +export const context = (): Channel.Channel, unknown, Env> => + core.fromEffect(Effect.context()) + +/** @internal */ +export const contextWith = ( + f: (env: Context.Context) => OutDone +): Channel.Channel => map(context(), f) + +/** @internal */ +export const contextWithChannel = < + Env, + OutElem, + InElem, + OutErr, + InErr, + OutDone, + InDone, + Env1 +>( + f: (env: Context.Context) => Channel.Channel +): Channel.Channel => core.flatMap(context(), f) + +/** @internal */ +export const contextWithEffect = ( + f: (env: Context.Context) => Effect.Effect +): Channel.Channel => mapEffect(context(), f) + +/** @internal */ +export const flatten = < + OutElem, + InElem, + OutErr, + InErr, + OutElem1, + InElem1, + OutErr1, + InErr1, + OutDone2, + InDone1, + Env1, + InDone, + Env +>( + self: Channel.Channel< + OutElem, + InElem, + OutErr, + InErr, + Channel.Channel, + InDone, + Env + > +): Channel.Channel< + OutElem | OutElem1, + InElem & InElem1, + OutErr | OutErr1, + InErr & InErr1, + OutDone2, + InDone & InDone1, + Env | Env1 +> => core.flatMap(self, identity) + +/** @internal */ +export const foldChannel = dual< + < + OutErr, + OutElem1, + InElem1, + OutErr1, + InErr1, + OutDone1, + InDone1, + Env1, + OutDone, + OutElem2, + InElem2, + OutErr2, + InErr2, + OutDone2, + InDone2, + Env2 + >( + options: { + readonly onFailure: ( + error: OutErr + ) => Channel.Channel + readonly onSuccess: ( + done: OutDone + ) => Channel.Channel + } + ) => ( + self: Channel.Channel + ) => Channel.Channel< + OutElem1 | OutElem2 | OutElem, + InElem & InElem1 & InElem2, + OutErr1 | OutErr2, + InErr & InErr1 & InErr2, + OutDone1 | OutDone2, + InDone & InDone1 & InDone2, + Env1 | Env2 | Env + >, + < + OutElem, + InElem, + OutErr, + InErr, + OutDone, + InDone, + Env, + OutElem1, + InElem1, + OutErr1, + InErr1, + OutDone1, + InDone1, + Env1, + OutElem2, + InElem2, + OutErr2, + InErr2, + OutDone2, + InDone2, + Env2 + >( + self: Channel.Channel, + options: { + readonly onFailure: ( + error: OutErr + ) => Channel.Channel + readonly onSuccess: ( + done: OutDone + ) => Channel.Channel + } + ) => Channel.Channel< + OutElem1 | OutElem2 | OutElem, + InElem & InElem1 & InElem2, + OutErr1 | OutErr2, + InErr & InErr1 & InErr2, + OutDone1 | OutDone2, + InDone & InDone1 & InDone2, + Env1 | Env2 | Env + > +>(2, < + OutElem, + InElem, + OutErr, + InErr, + OutDone, + InDone, + Env, + OutElem1, + InElem1, + OutErr1, + InErr1, + OutDone1, + InDone1, + Env1, + OutElem2, + InElem2, + OutErr2, + InErr2, + OutDone2, + InDone2, + Env2 +>( + self: Channel.Channel, + options: { + readonly onFailure: (error: OutErr) => Channel.Channel + readonly onSuccess: (done: OutDone) => Channel.Channel + } +): Channel.Channel< + OutElem | OutElem2 | OutElem1, + InElem & InElem1 & InElem2, + OutErr2 | OutErr1, + InErr & InErr1 & InErr2, + OutDone2 | OutDone1, + InDone & InDone1 & InDone2, + Env | Env1 | Env2 +> => + core.foldCauseChannel(self, { + onFailure: (cause) => { + const either = Cause.failureOrCause(cause) + switch (either._tag) { + case "Left": { + return options.onFailure(either.left) + } + case "Right": { + return core.failCause(either.right) + } + } + }, + onSuccess: options.onSuccess + })) + +/** @internal */ +export const fromEither = ( + either: Either.Either +): Channel.Channel => + core.suspend(() => Either.match(either, { onLeft: core.fail, onRight: core.succeed })) + +/** @internal */ +export const fromInput = ( + input: SingleProducerAsyncInput.AsyncInputConsumer +): Channel.Channel => + unwrap( + input.takeWith( + core.failCause, + (elem) => core.flatMap(core.write(elem), () => fromInput(input)), + core.succeed + ) + ) + +/** @internal */ +export const fromPubSub = ( + pubsub: PubSub.PubSub>> +): Channel.Channel => + unwrapScoped(Effect.map(PubSub.subscribe(pubsub), fromQueue)) + +/** @internal */ +export const fromPubSubScoped = ( + pubsub: PubSub.PubSub>> +): Effect.Effect, never, Scope.Scope> => + Effect.map(PubSub.subscribe(pubsub), fromQueue) + +/** @internal */ +export const fromOption = ( + option: Option.Option +): Channel.Channel, unknown, A, unknown> => + core.suspend(() => + Option.match(option, { + onNone: () => core.fail(Option.none()), + onSome: core.succeed + }) + ) + +/** @internal */ +export const fromQueue = ( + queue: Queue.Dequeue>> +): Channel.Channel => core.suspend(() => fromQueueInternal(queue)) + +/** @internal */ +const fromQueueInternal = ( + queue: Queue.Dequeue>> +): Channel.Channel => + pipe( + core.fromEffect(Queue.take(queue)), + core.flatMap(Either.match({ + onLeft: Exit.match({ + onFailure: core.failCause, + onSuccess: core.succeedNow + }), + onRight: (elem) => + core.flatMap( + core.write(elem), + () => fromQueueInternal(queue) + ) + })) + ) + +/** @internal */ +export const identityChannel = (): Channel.Channel => + core.readWith({ + onInput: (input: Elem) => core.flatMap(core.write(input), () => identityChannel()), + onFailure: core.fail, + onDone: core.succeedNow + }) + +/** @internal */ +export const interruptWhen = dual< + ( + effect: Effect.Effect + ) => ( + self: Channel.Channel + ) => Channel.Channel, + ( + self: Channel.Channel, + effect: Effect.Effect + ) => Channel.Channel +>(2, ( + self: Channel.Channel, + effect: Effect.Effect +): Channel.Channel => + mergeWith(self, { + other: core.fromEffect(effect), + onSelfDone: (selfDone) => mergeDecision.Done(Effect.suspend(() => selfDone)), + onOtherDone: (effectDone) => mergeDecision.Done(Effect.suspend(() => effectDone)) + })) + +/** @internal */ +export const interruptWhenDeferred = dual< + ( + deferred: Deferred.Deferred + ) => ( + self: Channel.Channel + ) => Channel.Channel, + ( + self: Channel.Channel, + deferred: Deferred.Deferred + ) => Channel.Channel +>(2, ( + self: Channel.Channel, + deferred: Deferred.Deferred +): Channel.Channel => + interruptWhen(self, Deferred.await(deferred))) + +/** @internal */ +export const map = dual< + ( + f: (out: OutDone) => OutDone2 + ) => ( + self: Channel.Channel + ) => Channel.Channel, + ( + self: Channel.Channel, + f: (out: OutDone) => OutDone2 + ) => Channel.Channel +>(2, ( + self: Channel.Channel, + f: (out: OutDone) => OutDone2 +): Channel.Channel => + core.flatMap(self, (a) => core.sync(() => f(a)))) + +/** @internal */ +export const mapEffect = dual< + ( + f: (o: OutDone) => Effect.Effect + ) => ( + self: Channel.Channel + ) => Channel.Channel, + ( + self: Channel.Channel, + f: (o: OutDone) => Effect.Effect + ) => Channel.Channel +>(2, ( + self: Channel.Channel, + f: (o: OutDone) => Effect.Effect +): Channel.Channel => + core.flatMap(self, (z) => core.fromEffect(f(z)))) + +/** @internal */ +export const mapError = dual< + ( + f: (err: OutErr) => OutErr2 + ) => ( + self: Channel.Channel + ) => Channel.Channel, + ( + self: Channel.Channel, + f: (err: OutErr) => OutErr2 + ) => Channel.Channel +>(2, ( + self: Channel.Channel, + f: (err: OutErr) => OutErr2 +): Channel.Channel => mapErrorCause(self, Cause.map(f))) + +/** @internal */ +export const mapErrorCause = dual< + ( + f: (cause: Cause.Cause) => Cause.Cause + ) => ( + self: Channel.Channel + ) => Channel.Channel, + ( + self: Channel.Channel, + f: (cause: Cause.Cause) => Cause.Cause + ) => Channel.Channel +>(2, ( + self: Channel.Channel, + f: (cause: Cause.Cause) => Cause.Cause +): Channel.Channel => + core.catchAllCause(self, (cause) => core.failCause(f(cause)))) + +/** @internal */ +export const mapOut = dual< + ( + f: (o: OutElem) => OutElem2 + ) => ( + self: Channel.Channel + ) => Channel.Channel, + ( + self: Channel.Channel, + f: (o: OutElem) => OutElem2 + ) => Channel.Channel +>(2, ( + self: Channel.Channel, + f: (o: OutElem) => OutElem2 +): Channel.Channel => { + const reader: Channel.Channel = core + .readWith({ + onInput: (outElem) => core.flatMap(core.write(f(outElem)), () => reader), + onFailure: core.fail, + onDone: core.succeedNow + }) + return core.pipeTo(self, reader) +}) + +/** @internal */ +export const mapOutEffect = dual< + ( + f: (o: OutElem) => Effect.Effect + ) => ( + self: Channel.Channel + ) => Channel.Channel, + ( + self: Channel.Channel, + f: (o: OutElem) => Effect.Effect + ) => Channel.Channel +>(2, ( + self: Channel.Channel, + f: (o: OutElem) => Effect.Effect +): Channel.Channel => { + const reader: Channel.Channel = core + .readWithCause({ + onInput: (outElem) => + pipe( + core.fromEffect(f(outElem)), + core.flatMap(core.write), + core.flatMap(() => reader) + ), + onFailure: core.failCause, + onDone: core.succeedNow + }) + return core.pipeTo(self, reader) +}) + +/** @internal */ +export const mapOutEffectPar = dual< + ( + f: (o: OutElem) => Effect.Effect, + n: number + ) => ( + self: Channel.Channel + ) => Channel.Channel, + ( + self: Channel.Channel, + f: (o: OutElem) => Effect.Effect, + n: number + ) => Channel.Channel +>(3, ( + self: Channel.Channel, + f: (o: OutElem) => Effect.Effect, + n: number +): Channel.Channel => + unwrapScopedWith( + (scope) => + Effect.gen(function*() { + const input = yield* singleProducerAsyncInput.make() + const queueReader = fromInput(input) + const queue = yield* Queue.bounded, OutErr | OutErr1, Env1>>(n) + yield* Scope.addFinalizer(scope, Queue.shutdown(queue)) + const errorSignal = yield* Deferred.make() + const withPermits = n === Number.POSITIVE_INFINITY ? + ((_: number) => identity) : + (yield* Effect.makeSemaphore(n)).withPermits + const pull = yield* queueReader.pipe(core.pipeTo(self), toPullIn(scope)) + yield* pull.pipe( + Effect.matchCauseEffect({ + onFailure: (cause) => Queue.offer(queue, Effect.failCause(cause)), + onSuccess: Either.match({ + onLeft: (outDone) => + Effect.zipRight( + Effect.interruptible(withPermits(n)(Effect.void)), + Effect.asVoid(Queue.offer(queue, Effect.succeed(Either.left(outDone)))) + ), + onRight: (outElem) => + Effect.gen(function*() { + const deferred = yield* Deferred.make() + const latch = yield* Deferred.make() + yield* Queue.offer(queue, Effect.map(Deferred.await(deferred), Either.right)) + yield* Deferred.succeed(latch, void 0).pipe( + Effect.zipRight( + Effect.uninterruptibleMask((restore) => + Effect.exit(restore(Deferred.await(errorSignal))).pipe( + Effect.raceFirst(Effect.exit(restore(f(outElem)))), + Effect.flatMap(identity) + ) + ).pipe( + Effect.tapErrorCause((cause) => Deferred.failCause(errorSignal, cause)), + Effect.intoDeferred(deferred) + ) + ), + withPermits(1), + Effect.forkIn(scope) + ) + yield* Deferred.await(latch) + }) + }) + }), + Effect.forever, + Effect.interruptible, + Effect.forkIn(scope) + ) + const consumer: Channel.Channel = unwrap( + Effect.matchCause(Effect.flatten(Queue.take(queue)), { + onFailure: core.failCause, + onSuccess: Either.match({ + onLeft: core.succeedNow, + onRight: (outElem) => core.flatMap(core.write(outElem), () => consumer) + }) + }) + ) + return core.embedInput(consumer, input) + }) + )) + +/** @internal */ +export const mergeAll = ( + options: { + readonly concurrency: number | "unbounded" + readonly bufferSize?: number | undefined + readonly mergeStrategy?: MergeStrategy.MergeStrategy | undefined + } +) => { + return < + OutElem, + InElem1, + OutErr1, + InErr1, + InDone1, + Env1, + InElem, + OutErr, + InErr, + InDone, + Env + >( + channels: Channel.Channel< + Channel.Channel, + InElem, + OutErr, + InErr, + unknown, + InDone, + Env + > + ): Channel.Channel< + OutElem, + InElem & InElem1, + OutErr | OutErr1, + InErr & InErr1, + unknown, + InDone & InDone1, + Env | Env1 + > => mergeAllWith(options)(channels, constVoid) +} + +/** @internal */ +export const mergeAllUnbounded = < + OutElem, + InElem1, + OutErr1, + InErr1, + InDone1, + Env1, + InElem, + OutErr, + InErr, + InDone, + Env +>( + channels: Channel.Channel< + Channel.Channel, + InElem, + OutErr, + InErr, + unknown, + InDone, + Env + > +): Channel.Channel< + OutElem, + InElem & InElem1, + OutErr | OutErr1, + InErr & InErr1, + unknown, + InDone & InDone1, + Env | Env1 +> => mergeAllWith({ concurrency: "unbounded" })(channels, constVoid) + +/** @internal */ +export const mergeAllUnboundedWith = < + OutElem, + InElem1, + OutErr1, + InErr1, + OutDone, + InDone1, + Env1, + InElem, + OutErr, + InErr, + InDone, + Env +>( + channels: Channel.Channel< + Channel.Channel, + InElem, + OutErr, + InErr, + OutDone, + InDone, + Env + >, + f: (o1: OutDone, o2: OutDone) => OutDone +): Channel.Channel< + OutElem, + InElem & InElem1, + OutErr | OutErr1, + InErr & InErr1, + OutDone, + InDone & InDone1, + Env | Env1 +> => mergeAllWith({ concurrency: "unbounded" })(channels, f) + +/** @internal */ +export const mergeAllWith = ( + { + bufferSize = 16, + concurrency, + mergeStrategy = mergeStrategy_.BackPressure() + }: { + readonly concurrency: number | "unbounded" + readonly bufferSize?: number | undefined + readonly mergeStrategy?: MergeStrategy.MergeStrategy | undefined + } +) => +( + channels: Channel.Channel< + Channel.Channel, + InElem, + OutErr, + InErr, + OutDone, + InDone, + Env + >, + f: (o1: OutDone, o2: OutDone) => OutDone +): Channel.Channel< + OutElem, + InElem & InElem1, + OutErr | OutErr1, + InErr & InErr1, + OutDone, + InDone & InDone1, + Env | Env1 +> => + unwrapScopedWith( + (scope) => + Effect.gen(function*() { + const concurrencyN = concurrency === "unbounded" ? Number.MAX_SAFE_INTEGER : concurrency + const input = yield* singleProducerAsyncInput.make< + InErr & InErr1, + InElem & InElem1, + InDone & InDone1 + >() + const queueReader = fromInput(input) + const queue = yield* Queue.bounded, OutErr | OutErr1, Env>>( + bufferSize + ) + yield* Scope.addFinalizer(scope, Queue.shutdown(queue)) + const cancelers = yield* Queue.unbounded>() + yield* Scope.addFinalizer(scope, Queue.shutdown(cancelers)) + const lastDone = yield* Ref.make>(Option.none()) + const errorSignal = yield* Deferred.make() + const withPermits = (yield* Effect.makeSemaphore(concurrencyN)).withPermits + const pull = yield* toPullIn(core.pipeTo(queueReader, channels), scope) + + function evaluatePull( + pull: Effect.Effect< + Either.Either, + OutErr | OutErr1, + Env | Env1 + > + ) { + return pull.pipe( + Effect.flatMap(Either.match({ + onLeft: (done) => Effect.succeed(Option.some(done)), + onRight: (outElem) => + Effect.as( + Queue.offer(queue, Effect.succeed(Either.right(outElem))), + Option.none() + ) + })), + Effect.repeat({ until: (_): _ is Option.Some => Option.isSome(_) }), + Effect.flatMap((outDone) => + Ref.update( + lastDone, + Option.match({ + onNone: () => Option.some(outDone.value), + onSome: (lastDone) => Option.some(f(lastDone, outDone.value)) + }) + ) + ), + Effect.catchAllCause((cause) => + Cause.isInterrupted(cause) + ? Effect.failCause(cause) + : Queue.offer(queue, Effect.failCause(cause)).pipe( + Effect.zipRight(Deferred.succeed(errorSignal, void 0)), + Effect.asVoid + ) + ) + ) + } + + yield* pull.pipe( + Effect.matchCauseEffect({ + onFailure: (cause) => + Queue.offer(queue, Effect.failCause(cause)).pipe( + Effect.zipRight(Effect.succeed(false)) + ), + onSuccess: Either.match({ + onLeft: (outDone) => + Effect.raceWith( + Effect.interruptible(Deferred.await(errorSignal)), + Effect.interruptible(withPermits(concurrencyN)(Effect.void)), + { + onSelfDone: (_, permitAcquisition) => Effect.as(Fiber.interrupt(permitAcquisition), false), + onOtherDone: (_, failureAwait) => + Effect.zipRight( + Fiber.interrupt(failureAwait), + Ref.get(lastDone).pipe( + Effect.flatMap(Option.match({ + onNone: () => Queue.offer(queue, Effect.succeed(Either.left(outDone))), + onSome: (lastDone) => Queue.offer(queue, Effect.succeed(Either.left(f(lastDone, outDone)))) + })), + Effect.as(false) + ) + ) + } + ), + onRight: (channel) => + mergeStrategy_.match(mergeStrategy, { + onBackPressure: () => + Effect.gen(function*() { + const latch = yield* Deferred.make() + const raceEffects = Effect.scopedWith((scope) => + toPullIn(core.pipeTo(queueReader, channel), scope).pipe( + Effect.flatMap((pull) => + Effect.race( + Effect.exit(evaluatePull(pull)), + Effect.exit(Effect.interruptible(Deferred.await(errorSignal))) + ) + ), + Effect.flatMap(identity) + ) + ) + yield* Deferred.succeed(latch, void 0).pipe( + Effect.zipRight(raceEffects), + withPermits(1), + Effect.forkIn(scope) + ) + yield* Deferred.await(latch) + const errored = yield* Deferred.isDone(errorSignal) + return !errored + }), + onBufferSliding: () => + Effect.gen(function*() { + const canceler = yield* Deferred.make() + const latch = yield* Deferred.make() + const size = yield* Queue.size(cancelers) + yield* Queue.take(cancelers).pipe( + Effect.flatMap((canceler) => Deferred.succeed(canceler, void 0)), + Effect.when(() => size >= concurrencyN) + ) + yield* Queue.offer(cancelers, canceler) + const raceEffects = Effect.scopedWith((scope) => + toPullIn(core.pipeTo(queueReader, channel), scope).pipe( + Effect.flatMap((pull) => + Effect.exit(evaluatePull(pull)).pipe( + Effect.race(Effect.exit(Effect.interruptible(Deferred.await(errorSignal)))), + Effect.race(Effect.exit(Effect.interruptible(Deferred.await(canceler)))) + ) + ), + Effect.flatMap(identity) + ) + ) + yield* Deferred.succeed(latch, void 0).pipe( + Effect.zipRight(raceEffects), + withPermits(1), + Effect.forkIn(scope) + ) + yield* Deferred.await(latch) + const errored = yield* Deferred.isDone(errorSignal) + return !errored + }) + }) + }) + }), + Effect.repeat({ while: (_) => _ }), + Effect.forkIn(scope) + ) + + const consumer: Channel.Channel = + pipe( + Queue.take(queue), + Effect.flatten, + Effect.matchCause({ + onFailure: core.failCause, + onSuccess: Either.match({ + onLeft: core.succeedNow, + onRight: (outElem) => core.flatMap(core.write(outElem), () => consumer) + }) + }), + unwrap + ) + + return core.embedInput(consumer, input) + }) + ) + +/** @internal */ +export const mergeMap = dual< + ( + f: (outElem: OutElem) => Channel.Channel, + options: { + readonly concurrency: number | "unbounded" + readonly bufferSize?: number | undefined + readonly mergeStrategy?: MergeStrategy.MergeStrategy | undefined + } + ) => ( + self: Channel.Channel + ) => Channel.Channel< + OutElem1, + InElem & InElem1, + OutErr1 | OutErr, + InErr & InErr1, + unknown, + InDone & InDone1, + Env1 | Env + >, + ( + self: Channel.Channel, + f: (outElem: OutElem) => Channel.Channel, + options: { + readonly concurrency: number | "unbounded" + readonly bufferSize?: number | undefined + readonly mergeStrategy?: MergeStrategy.MergeStrategy | undefined + } + ) => Channel.Channel< + OutElem1, + InElem & InElem1, + OutErr1 | OutErr, + InErr & InErr1, + unknown, + InDone & InDone1, + Env1 | Env + > +>(3, ( + self: Channel.Channel, + f: (outElem: OutElem) => Channel.Channel, + options: { + readonly concurrency: number | "unbounded" + readonly bufferSize?: number | undefined + readonly mergeStrategy?: MergeStrategy.MergeStrategy | undefined + } +): Channel.Channel< + OutElem1, + InElem & InElem1, + OutErr | OutErr1, + InErr & InErr1, + unknown, + InDone & InDone1, + Env | Env1 +> => mergeAll(options)(mapOut(self, f))) + +/** @internal */ +export const mergeOut = dual< + ( + n: number + ) => ( + self: Channel.Channel< + Channel.Channel, + InElem, + OutErr, + InErr, + OutDone, + InDone, + Env + > + ) => Channel.Channel< + OutElem1, + InElem & InElem1, + OutErr | OutErr1, + InErr & InErr1, + unknown, + InDone & InDone1, + Env | Env1 + >, + ( + self: Channel.Channel< + Channel.Channel, + InElem, + OutErr, + InErr, + OutDone, + InDone, + Env + >, + n: number + ) => Channel.Channel< + OutElem1, + InElem & InElem1, + OutErr | OutErr1, + InErr & InErr1, + unknown, + InDone & InDone1, + Env | Env1 + > +>(2, ( + self: Channel.Channel< + Channel.Channel, + InElem, + OutErr, + InErr, + OutDone, + InDone, + Env + >, + n: number +): Channel.Channel< + OutElem1, + InElem & InElem1, + OutErr | OutErr1, + InErr & InErr1, + unknown, + InDone & InDone1, + Env | Env1 +> => mergeAll({ concurrency: n })(mapOut(self, identity))) + +/** @internal */ +export const mergeOutWith = dual< + ( + n: number, + f: (o1: OutDone1, o2: OutDone1) => OutDone1 + ) => ( + self: Channel.Channel< + Channel.Channel, + InElem, + OutErr, + InErr, + OutDone1, + InDone, + Env + > + ) => Channel.Channel< + OutElem1, + InElem & InElem1, + OutErr | OutErr1, + InErr & InErr1, + OutDone1, + InDone & InDone1, + Env | Env1 + >, + ( + self: Channel.Channel< + Channel.Channel, + InElem, + OutErr, + InErr, + OutDone1, + InDone, + Env + >, + n: number, + f: (o1: OutDone1, o2: OutDone1) => OutDone1 + ) => Channel.Channel< + OutElem1, + InElem & InElem1, + OutErr | OutErr1, + InErr & InErr1, + OutDone1, + InDone & InDone1, + Env | Env1 + > +>(3, ( + self: Channel.Channel< + Channel.Channel, + InElem, + OutErr, + InErr, + OutDone1, + InDone, + Env + >, + n: number, + f: (o1: OutDone1, o2: OutDone1) => OutDone1 +): Channel.Channel< + OutElem1, + InElem & InElem1, + OutErr | OutErr1, + InErr & InErr1, + OutDone1, + InDone & InDone1, + Env | Env1 +> => mergeAllWith({ concurrency: n })(mapOut(self, identity), f)) + +/** @internal */ +export const mergeWith = dual< + ( + options: { + readonly other: Channel.Channel + readonly onSelfDone: ( + exit: Exit.Exit + ) => MergeDecision.MergeDecision + readonly onOtherDone: ( + ex: Exit.Exit + ) => MergeDecision.MergeDecision + } + ) => ( + self: Channel.Channel + ) => Channel.Channel< + OutElem1 | OutElem, + InElem & InElem1, + OutErr2 | OutErr3, + InErr & InErr1, + OutDone2 | OutDone3, + InDone & InDone1, + Env1 | Env + >, + < + OutElem, + InElem, + OutErr, + InErr, + OutDone, + InDone, + Env, + OutElem1, + InElem1, + OutErr1, + InErr1, + OutDone1, + InDone1, + Env1, + OutErr2, + OutDone2, + OutErr3, + OutDone3 + >( + self: Channel.Channel, + options: { + readonly other: Channel.Channel + readonly onSelfDone: ( + exit: Exit.Exit + ) => MergeDecision.MergeDecision + readonly onOtherDone: ( + ex: Exit.Exit + ) => MergeDecision.MergeDecision + } + ) => Channel.Channel< + OutElem1 | OutElem, + InElem & InElem1, + OutErr2 | OutErr3, + InErr & InErr1, + OutDone2 | OutDone3, + InDone & InDone1, + Env1 | Env + > +>(2, < + OutElem, + InElem, + OutErr, + InErr, + OutDone, + InDone, + Env, + OutElem1, + InElem1, + OutErr1, + InErr1, + OutDone1, + InDone1, + Env1, + OutErr2, + OutDone2, + OutErr3, + OutDone3 +>( + self: Channel.Channel, + options: { + readonly other: Channel.Channel + readonly onSelfDone: ( + exit: Exit.Exit + ) => MergeDecision.MergeDecision + readonly onOtherDone: ( + ex: Exit.Exit + ) => MergeDecision.MergeDecision + } +): Channel.Channel< + OutElem | OutElem1, + InElem & InElem1, + OutErr2 | OutErr3, + InErr & InErr1, + OutDone2 | OutDone3, + InDone & InDone1, + Env1 | Env +> => { + function merge(scope: Scope.Scope) { + return Effect.gen(function*() { + type State = MergeState.MergeState< + Env | Env1, + OutErr, + OutErr1, + OutErr2 | OutErr3, + OutElem | OutElem1, + OutDone, + OutDone1, + OutDone2 | OutDone3 + > + + const input = yield* singleProducerAsyncInput.make< + InErr & InErr1, + InElem & InElem1, + InDone & InDone1 + >() + const queueReader = fromInput(input) + const pullL = yield* toPullIn(core.pipeTo(queueReader, self), scope) + const pullR = yield* toPullIn(core.pipeTo(queueReader, options.other), scope) + + function handleSide( + exit: Exit.Exit, Err>, + fiber: Fiber.Fiber, Err2>, + pull: Effect.Effect, Err, Env | Env1> + ) { + return ( + done: ( + ex: Exit.Exit + ) => MergeDecision.MergeDecision< + Env | Env1, + Err2, + Done2, + OutErr2 | OutErr3, + OutDone2 | OutDone3 + >, + both: ( + f1: Fiber.Fiber, Err>, + f2: Fiber.Fiber, Err2> + ) => State, + single: ( + f: ( + ex: Exit.Exit + ) => Effect.Effect + ) => State + ): Effect.Effect< + Channel.Channel< + OutElem | OutElem1, + unknown, + OutErr2 | OutErr3, + unknown, + OutDone2 | OutDone3, + unknown, + Env | Env1 + >, + never, + Env | Env1 + > => { + function onDecision( + decision: MergeDecision.MergeDecision< + Env | Env1, + Err2, + Done2, + OutErr2 | OutErr3, + OutDone2 | OutDone3 + > + ): Effect.Effect< + Channel.Channel< + OutElem | OutElem1, + unknown, + OutErr2 | OutErr3, + unknown, + OutDone2 | OutDone3, + unknown, + Env | Env1 + > + > { + const op = decision as mergeDecision.Primitive + if (op._tag === MergeDecisionOpCodes.OP_DONE) { + return Effect.succeed( + core.fromEffect( + Effect.zipRight( + Fiber.interrupt(fiber), + op.effect + ) + ) + ) + } + return Effect.map( + Fiber.await(fiber), + Exit.match({ + onFailure: (cause) => core.fromEffect(op.f(Exit.failCause(cause))), + onSuccess: Either.match({ + onLeft: (done) => core.fromEffect(op.f(Exit.succeed(done))), + onRight: (elem) => zipRight(core.write(elem), go(single(op.f))) + }) + }) + ) + } + + return Exit.match(exit, { + onFailure: (cause) => onDecision(done(Exit.failCause(cause))), + onSuccess: Either.match({ + onLeft: (z) => onDecision(done(Exit.succeed(z))), + onRight: (elem) => + Effect.succeed( + core.flatMap(core.write(elem), () => + core.flatMap( + core.fromEffect(Effect.forkIn(Effect.interruptible(pull), scope)), + (leftFiber) => go(both(leftFiber, fiber)) + )) + ) + }) + }) + } + } + + function go( + state: State + ): Channel.Channel< + OutElem | OutElem1, + unknown, + OutErr2 | OutErr3, + unknown, + OutDone2 | OutDone3, + unknown, + Env | Env1 + > { + switch (state._tag) { + case MergeStateOpCodes.OP_BOTH_RUNNING: { + const leftJoin = Effect.interruptible(Fiber.join(state.left)) + const rightJoin = Effect.interruptible(Fiber.join(state.right)) + return unwrap( + Effect.raceWith(leftJoin, rightJoin, { + onSelfDone: (leftExit, rf) => + Effect.zipRight( + Fiber.interrupt(rf), + handleSide(leftExit, state.right, pullL)( + options.onSelfDone, + mergeState.BothRunning, + (f) => mergeState.LeftDone(f) + ) + ), + onOtherDone: (rightExit, lf) => + Effect.zipRight( + Fiber.interrupt(lf), + handleSide(rightExit, state.left, pullR)( + options.onOtherDone as ( + ex: Exit.Exit + ) => MergeDecision.MergeDecision< + Env1 | Env, + OutErr, + OutDone, + OutErr2 | OutErr3, + OutDone2 | OutDone3 + >, + (left, right) => mergeState.BothRunning(right, left), + (f) => mergeState.RightDone(f) + ) + ) + }) + ) + } + case MergeStateOpCodes.OP_LEFT_DONE: { + return unwrap( + Effect.map( + Effect.exit(pullR), + Exit.match({ + onFailure: (cause) => core.fromEffect(state.f(Exit.failCause(cause))), + onSuccess: Either.match({ + onLeft: (done) => core.fromEffect(state.f(Exit.succeed(done))), + onRight: (elem) => + core.flatMap( + core.write(elem), + () => go(mergeState.LeftDone(state.f)) + ) + }) + }) + ) + ) + } + case MergeStateOpCodes.OP_RIGHT_DONE: { + return unwrap( + Effect.map( + Effect.exit(pullL), + Exit.match({ + onFailure: (cause) => core.fromEffect(state.f(Exit.failCause(cause))), + onSuccess: Either.match({ + onLeft: (done) => core.fromEffect(state.f(Exit.succeed(done))), + onRight: (elem) => + core.flatMap( + core.write(elem), + () => go(mergeState.RightDone(state.f)) + ) + }) + }) + ) + ) + } + } + } + + return core.fromEffect( + Effect.withFiberRuntime< + MergeState.MergeState< + Env | Env1, + OutErr, + OutErr1, + OutErr2 | OutErr3, + OutElem | OutElem1, + OutDone, + OutDone1, + OutDone2 | OutDone3 + >, + never, + Env | Env1 + >((parent) => { + const inherit = Effect.withFiberRuntime((state) => { + ;(state as any).transferChildren((parent as any).scope()) + return Effect.void + }) + const leftFiber = Effect.interruptible(pullL).pipe( + Effect.ensuring(inherit), + Effect.forkIn(scope) + ) + const rightFiber = Effect.interruptible(pullR).pipe( + Effect.ensuring(inherit), + Effect.forkIn(scope) + ) + return Effect.zipWith( + leftFiber, + rightFiber, + (left, right): State => + mergeState.BothRunning< + Env | Env1, + OutErr, + OutErr1, + OutErr2 | OutErr3, + OutElem | OutElem1, + OutDone, + OutDone1, + OutDone2 | OutDone3 + >(left, right) + ) + }) + ).pipe( + core.flatMap(go), + core.embedInput(input) + ) + }) + } + return unwrapScopedWith(merge) +}) + +/** @internal */ +export const never: Channel.Channel = core.fromEffect( + Effect.never +) + +/** @internal */ +export const orDie = dual< + ( + error: LazyArg + ) => ( + self: Channel.Channel + ) => Channel.Channel, + ( + self: Channel.Channel, + error: LazyArg + ) => Channel.Channel +>(2, ( + self: Channel.Channel, + error: LazyArg +): Channel.Channel => orDieWith(self, error)) + +/** @internal */ +export const orDieWith = dual< + ( + f: (e: OutErr) => unknown + ) => ( + self: Channel.Channel + ) => Channel.Channel, + ( + self: Channel.Channel, + f: (e: OutErr) => unknown + ) => Channel.Channel +>(2, ( + self: Channel.Channel, + f: (e: OutErr) => unknown +): Channel.Channel => + catchAll(self, (e) => core.failCauseSync(() => Cause.die(f(e)))) as Channel.Channel< + OutElem, + InElem, + never, + InErr, + OutDone, + InDone, + Env + >) + +/** @internal */ +export const orElse = dual< + ( + that: LazyArg> + ) => ( + self: Channel.Channel + ) => Channel.Channel< + OutElem1 | OutElem, + InElem & InElem1, + OutErr1, + InErr & InErr1, + OutDone1 | OutDone, + InDone & InDone1, + Env1 | Env + >, + ( + self: Channel.Channel, + that: LazyArg> + ) => Channel.Channel< + OutElem1 | OutElem, + InElem & InElem1, + OutErr1, + InErr & InErr1, + OutDone1 | OutDone, + InDone & InDone1, + Env1 | Env + > +>( + 2, + ( + self: Channel.Channel, + that: LazyArg> + ): Channel.Channel< + OutElem | OutElem1, + InElem & InElem1, + OutErr1, + InErr & InErr1, + OutDone | OutDone1, + InDone & InDone1, + Env | Env1 + > => catchAll(self, that) +) + +/** @internal */ +export const pipeToOrFail = dual< + ( + that: Channel.Channel + ) => ( + self: Channel.Channel + ) => Channel.Channel, + ( + self: Channel.Channel, + that: Channel.Channel + ) => Channel.Channel +>(2, ( + self: Channel.Channel, + that: Channel.Channel +): Channel.Channel => + core.suspend(() => { + let channelException: Channel.ChannelException | undefined = undefined + + const reader: Channel.Channel = core + .readWith({ + onInput: (outElem) => core.flatMap(core.write(outElem), () => reader), + onFailure: (outErr) => { + channelException = ChannelException(outErr) + return core.failCause(Cause.die(channelException)) + }, + onDone: core.succeedNow + }) + + const writer: Channel.Channel< + OutElem2, + OutElem2, + OutErr2, + OutErr2, + OutDone2, + OutDone2, + Env2 + > = core.readWithCause({ + onInput: (outElem) => pipe(core.write(outElem), core.flatMap(() => writer)), + onFailure: (cause) => + Cause.isDieType(cause) && + isChannelException(cause.defect) && + Equal.equals(cause.defect, channelException) + ? core.fail(cause.defect.error as OutErr2) + : core.failCause(cause), + onDone: core.succeedNow + }) + + return core.pipeTo(core.pipeTo(core.pipeTo(self, reader), that), writer) + })) + +/** @internal */ +export const provideService = dual< + ( + tag: Context.Tag, + service: Types.NoInfer + ) => ( + self: Channel.Channel + ) => Channel.Channel>, + ( + self: Channel.Channel, + tag: Context.Tag, + service: Types.NoInfer + ) => Channel.Channel> +>(3, ( + self: Channel.Channel, + tag: Context.Tag, + service: Types.NoInfer +): Channel.Channel> => { + return core.flatMap( + context(), + (context) => core.provideContext(self, Context.add(context, tag, service)) + ) +}) + +/** @internal */ +export const provideLayer = dual< + ( + layer: Layer.Layer + ) => ( + self: Channel.Channel + ) => Channel.Channel, + ( + self: Channel.Channel, + layer: Layer.Layer + ) => Channel.Channel +>(2, ( + self: Channel.Channel, + layer: Layer.Layer +): Channel.Channel => + unwrapScopedWith((scope) => + Effect.map(Layer.buildWithScope(layer, scope), (context) => core.provideContext(self, context)) + )) + +/** @internal */ +export const mapInputContext = dual< + ( + f: (env: Context.Context) => Context.Context + ) => ( + self: Channel.Channel + ) => Channel.Channel, + ( + self: Channel.Channel, + f: (env: Context.Context) => Context.Context + ) => Channel.Channel +>(2, ( + self: Channel.Channel, + f: (env: Context.Context) => Context.Context +): Channel.Channel => + contextWithChannel((context: Context.Context) => core.provideContext(self, f(context)))) + +/** @internal */ +export const provideSomeLayer = dual< + ( + layer: Layer.Layer + ) => ( + self: Channel.Channel + ) => Channel.Channel>, + ( + self: Channel.Channel, + layer: Layer.Layer + ) => Channel.Channel> +>(2, ( + self: Channel.Channel, + layer: Layer.Layer +): Channel.Channel> => + // @ts-expect-error + provideLayer(self, Layer.merge(Layer.context>(), layer))) + +/** @internal */ +export const read = (): Channel.Channel, unknown, In, unknown> => + core.readOrFail, In>(Option.none()) + +/** @internal */ +export const repeated = ( + self: Channel.Channel +): Channel.Channel => core.flatMap(self, () => repeated(self)) + +/** @internal */ +export const run = ( + self: Channel.Channel +): Effect.Effect => Effect.scopedWith((scope) => executor.runIn(self, scope)) + +/** @internal */ +export const runCollect = ( + self: Channel.Channel +): Effect.Effect<[Chunk.Chunk, OutDone], OutErr, Env> => run(core.collectElements(self)) + +/** @internal */ +export const runDrain = ( + self: Channel.Channel +): Effect.Effect => run(drain(self)) + +/** @internal */ +export const runScoped = ( + self: Channel.Channel +): Effect.Effect => Effect.scopeWith((scope) => executor.runIn(self, scope)) + +/** @internal */ +export const scoped = ( + effect: Effect.Effect +): Channel.Channel> => + unwrap( + Effect.uninterruptibleMask((restore) => + Effect.map(Scope.make(), (scope) => + core.acquireReleaseOut( + Effect.tapErrorCause( + restore(Scope.extend(effect, scope)), + (cause) => Scope.close(scope, Exit.failCause(cause)) + ), + (_, exit) => Scope.close(scope, exit) + )) + ) + ) + +/** @internal */ +export const scopedWith = ( + f: (scope: Scope.Scope) => Effect.Effect +): Channel.Channel => + unwrapScoped(Effect.map(Effect.scope, (scope) => core.flatMap(core.fromEffect(f(scope)), core.write))) + +/** @internal */ +export const service = ( + tag: Context.Tag +): Channel.Channel => core.fromEffect(tag) + +/** @internal */ +export const serviceWith = (tag: Context.Tag) => +( + f: (resource: Types.NoInfer) => OutDone +): Channel.Channel => map(service(tag), f) + +/** @internal */ +export const serviceWithChannel = + (tag: Context.Tag) => + ( + f: (resource: Types.NoInfer) => Channel.Channel + ): Channel.Channel => core.flatMap(service(tag), f) + +/** @internal */ +export const serviceWithEffect = (tag: Context.Tag) => +( + f: (resource: Types.NoInfer) => Effect.Effect +): Channel.Channel => mapEffect(service(tag), f) + +/** @internal */ +export const splitLines = (): Channel.Channel< + Chunk.Chunk, + Chunk.Chunk, + Err, + Err, + Done, + Done, + never +> => + core.suspend(() => { + let stringBuilder = "" + let midCRLF = false + const splitLinesChunk = (chunk: Chunk.Chunk): Chunk.Chunk => { + const chunkBuilder: Array = [] + Chunk.map(chunk, (str) => { + if (str.length !== 0) { + let from = 0 + let indexOfCR = str.indexOf("\r") + let indexOfLF = str.indexOf("\n") + if (midCRLF) { + if (indexOfLF === 0) { + chunkBuilder.push(stringBuilder) + stringBuilder = "" + from = 1 + indexOfLF = str.indexOf("\n", from) + } else { + stringBuilder = stringBuilder + "\r" + } + midCRLF = false + } + while (indexOfCR !== -1 || indexOfLF !== -1) { + if (indexOfCR === -1 || (indexOfLF !== -1 && indexOfLF < indexOfCR)) { + if (stringBuilder.length === 0) { + chunkBuilder.push(str.substring(from, indexOfLF)) + } else { + chunkBuilder.push(stringBuilder + str.substring(from, indexOfLF)) + stringBuilder = "" + } + from = indexOfLF + 1 + indexOfLF = str.indexOf("\n", from) + } else { + if (str.length === indexOfCR + 1) { + midCRLF = true + indexOfCR = -1 + } else { + if (indexOfLF === indexOfCR + 1) { + if (stringBuilder.length === 0) { + chunkBuilder.push(str.substring(from, indexOfCR)) + } else { + stringBuilder = stringBuilder + str.substring(from, indexOfCR) + chunkBuilder.push(stringBuilder) + stringBuilder = "" + } + from = indexOfCR + 2 + indexOfCR = str.indexOf("\r", from) + indexOfLF = str.indexOf("\n", from) + } else { + indexOfCR = str.indexOf("\r", indexOfCR + 1) + } + } + } + } + if (midCRLF) { + stringBuilder = stringBuilder + str.substring(from, str.length - 1) + } else { + stringBuilder = stringBuilder + str.substring(from, str.length) + } + } + }) + return Chunk.unsafeFromArray(chunkBuilder) + } + const loop: Channel.Channel, Chunk.Chunk, Err, Err, Done, Done, never> = core + .readWithCause({ + onInput: (input: Chunk.Chunk) => { + const out = splitLinesChunk(input) + return Chunk.isEmpty(out) + ? loop + : core.flatMap(core.write(out), () => loop) + }, + onFailure: (cause) => + stringBuilder.length === 0 + ? core.failCause(cause) + : core.flatMap(core.write(Chunk.of(stringBuilder)), () => core.failCause(cause)), + onDone: (done) => + stringBuilder.length === 0 + ? core.succeed(done) + : core.flatMap(core.write(Chunk.of(stringBuilder)), () => core.succeed(done)) + }) + return loop + }) + +/** @internal */ +export const toPubSub = ( + pubsub: PubSub.PubSub>> +): Channel.Channel => toQueue(pubsub) + +/** @internal */ +export const toPull = ( + self: Channel.Channel +): Effect.Effect, OutErr, Env>, never, Env | Scope.Scope> => + Effect.flatMap(Effect.scope, (scope) => toPullIn(self, scope)) + +/** @internal */ +export const toPullIn = dual< + (scope: Scope.Scope) => ( + self: Channel.Channel + ) => Effect.Effect, OutErr, Env>, never, Env>, + ( + self: Channel.Channel, + scope: Scope.Scope + ) => Effect.Effect, OutErr, Env>, never, Env> +>(2, ( + self: Channel.Channel, + scope: Scope.Scope +) => + Effect.zip( + Effect.sync(() => new executor.ChannelExecutor(self, void 0, identity)), + Effect.runtime() + ).pipe( + Effect.tap(([executor, runtime]) => + Scope.addFinalizerExit(scope, (exit) => { + const finalizer = executor.close(exit) + return finalizer !== undefined + ? Effect.provide(finalizer, runtime) + : Effect.void + }) + ), + Effect.uninterruptible, + Effect.map(([executor]) => + Effect.suspend(() => + interpretToPull( + executor.run() as ChannelState.ChannelState, + executor + ) + ) + ) + )) + +/** @internal */ +const interpretToPull = ( + channelState: ChannelState.ChannelState, + exec: executor.ChannelExecutor +): Effect.Effect, OutErr, Env> => { + const state = channelState as ChannelState.Primitive + switch (state._tag) { + case ChannelStateOpCodes.OP_DONE: { + return Exit.match(exec.getDone(), { + onFailure: Effect.failCause, + onSuccess: (done): Effect.Effect, OutErr, Env> => + Effect.succeed(Either.left(done)) + }) + } + case ChannelStateOpCodes.OP_EMIT: { + return Effect.succeed(Either.right(exec.getEmit())) + } + case ChannelStateOpCodes.OP_FROM_EFFECT: { + return pipe( + state.effect as Effect.Effect, OutErr, Env>, + Effect.flatMap(() => interpretToPull(exec.run() as ChannelState.ChannelState, exec)) + ) + } + case ChannelStateOpCodes.OP_READ: { + return executor.readUpstream( + state, + () => interpretToPull(exec.run() as ChannelState.ChannelState, exec), + (cause) => Effect.failCause(cause) as Effect.Effect, OutErr, Env> + ) + } + } +} + +/** @internal */ +export const toQueue = ( + queue: Queue.Enqueue>> +): Channel.Channel => core.suspend(() => toQueueInternal(queue)) + +/** @internal */ +const toQueueInternal = ( + queue: Queue.Enqueue>> +): Channel.Channel => { + return core.readWithCause({ + onInput: (elem) => + core.flatMap( + core.fromEffect(Queue.offer(queue, Either.right(elem))), + () => toQueueInternal(queue) + ), + onFailure: (cause) => core.fromEffect(Queue.offer(queue, Either.left(Exit.failCause(cause)))), + onDone: (done) => core.fromEffect(Queue.offer(queue, Either.left(Exit.succeed(done)))) + }) +} + +/** @internal */ +export const unwrap = ( + channel: Effect.Effect, E, R> +): Channel.Channel => flatten(core.fromEffect(channel)) + +/** @internal */ +export const unwrapScoped = ( + self: Effect.Effect, E, R> +): Channel.Channel> => + core.concatAllWith( + scoped(self), + (d, _) => d, + (d, _) => d + ) + +/** @internal */ +export const unwrapScopedWith = ( + f: (scope: Scope.Scope) => Effect.Effect, E, R> +): Channel.Channel => + core.concatAllWith( + scopedWith(f), + (d, _) => d, + (d, _) => d + ) + +/** @internal */ +export const updateService = dual< + ( + tag: Context.Tag, + f: (resource: Types.NoInfer) => Types.NoInfer + ) => ( + self: Channel.Channel + ) => Channel.Channel, + ( + self: Channel.Channel, + tag: Context.Tag, + f: (resource: Types.NoInfer) => Types.NoInfer + ) => Channel.Channel +>(3, ( + self: Channel.Channel, + tag: Context.Tag, + f: (resource: Types.NoInfer) => Types.NoInfer +): Channel.Channel => + mapInputContext(self, (context: Context.Context) => + Context.merge( + context, + Context.make(tag, f(Context.unsafeGet(context, tag))) + ))) + +/** @internal */ +export const withSpan: { + ( + name: string, + options?: Tracer.SpanOptions + ): ( + self: Channel.Channel + ) => Channel.Channel> + ( + self: Channel.Channel, + name: string, + options?: Tracer.SpanOptions + ): Channel.Channel> +} = function() { + const dataFirst = typeof arguments[0] !== "string" + const name = dataFirst ? arguments[1] : arguments[0] + const options = tracer.addSpanStackTrace(dataFirst ? arguments[2] : arguments[1]) + const acquire = Effect.all([ + Effect.makeSpan(name, options), + Effect.context(), + Effect.clock, + FiberRef.get(FiberRef.currentTracerTimingEnabled) + ]) + if (dataFirst) { + const self = arguments[0] + return acquireUseRelease( + acquire, + ([span, context]) => core.provideContext(self, Context.add(context, tracer.spanTag, span)), + ([span, , clock, timingEnabled], exit) => coreEffect.endSpan(span, exit, clock, timingEnabled) + ) + } + return (self: Channel.Channel) => + acquireUseRelease( + acquire, + ([span, context]) => core.provideContext(self, Context.add(context, tracer.spanTag, span)), + ([span, , clock, timingEnabled], exit) => coreEffect.endSpan(span, exit, clock, timingEnabled) + ) +} as any + +/** @internal */ +export const writeAll = ( + ...outs: Array +): Channel.Channel => writeChunk(Chunk.fromIterable(outs)) + +/** @internal */ +export const writeChunk = ( + outs: Chunk.Chunk +): Channel.Channel => writeChunkWriter(0, outs.length, outs) + +/** @internal */ +const writeChunkWriter = ( + idx: number, + len: number, + chunk: Chunk.Chunk +): Channel.Channel => { + return idx === len + ? core.void + : pipe( + core.write(pipe(chunk, Chunk.unsafeGet(idx))), + core.flatMap(() => writeChunkWriter(idx + 1, len, chunk)) + ) +} + +/** @internal */ +export const zip = dual< + ( + that: Channel.Channel, + options?: { + readonly concurrent?: boolean | undefined + } + ) => ( + self: Channel.Channel + ) => Channel.Channel< + OutElem1 | OutElem, + InElem & InElem1, + OutErr1 | OutErr, + InErr & InErr1, + readonly [OutDone, OutDone1], + InDone & InDone1, + Env1 | Env + >, + ( + self: Channel.Channel, + that: Channel.Channel, + options?: { + readonly concurrent?: boolean | undefined + } + ) => Channel.Channel< + OutElem1 | OutElem, + InElem & InElem1, + OutErr1 | OutErr, + InErr & InErr1, + readonly [OutDone, OutDone1], + InDone & InDone1, + Env1 | Env + > +>( + (args) => core.isChannel(args[1]), + ( + self: Channel.Channel, + that: Channel.Channel, + options?: { + readonly concurrent?: boolean | undefined + } + ): Channel.Channel< + OutElem | OutElem1, + InElem & InElem1, + OutErr | OutErr1, + InErr & InErr1, + readonly [OutDone, OutDone1], + InDone & InDone1, + Env | Env1 + > => + options?.concurrent ? + mergeWith(self, { + other: that, + onSelfDone: (exit1) => mergeDecision.Await((exit2) => Effect.suspend(() => Exit.zip(exit1, exit2))), + onOtherDone: (exit2) => mergeDecision.Await((exit1) => Effect.suspend(() => Exit.zip(exit1, exit2))) + }) : + core.flatMap(self, (a) => map(that, (b) => [a, b] as const)) +) + +/** @internal */ +export const zipLeft = dual< + ( + that: Channel.Channel, + options?: { + readonly concurrent?: boolean | undefined + } + ) => ( + self: Channel.Channel + ) => Channel.Channel< + OutElem1 | OutElem, + InElem & InElem1, + OutErr1 | OutErr, + InErr & InErr1, + OutDone, + InDone & InDone1, + Env1 | Env + >, + ( + self: Channel.Channel, + that: Channel.Channel, + options?: { + readonly concurrent?: boolean | undefined + } + ) => Channel.Channel< + OutElem1 | OutElem, + InElem & InElem1, + OutErr1 | OutErr, + InErr & InErr1, + OutDone, + InDone & InDone1, + Env1 | Env + > +>( + (args) => core.isChannel(args[1]), + ( + self: Channel.Channel, + that: Channel.Channel, + options?: { + readonly concurrent?: boolean | undefined + } + ): Channel.Channel< + OutElem | OutElem1, + InElem & InElem1, + OutErr | OutErr1, + InErr & InErr1, + OutDone, + InDone & InDone1, + Env | Env1 + > => + options?.concurrent ? + map(zip(self, that, { concurrent: true }), (tuple) => tuple[0]) : + core.flatMap(self, (z) => as(that, z)) +) + +/** @internal */ +export const zipRight = dual< + ( + that: Channel.Channel, + options?: { + readonly concurrent?: boolean | undefined + } + ) => ( + self: Channel.Channel + ) => Channel.Channel< + OutElem1 | OutElem, + InElem & InElem1, + OutErr1 | OutErr, + InErr & InErr1, + OutDone1, + InDone & InDone1, + Env1 | Env + >, + ( + self: Channel.Channel, + that: Channel.Channel, + options?: { + readonly concurrent?: boolean | undefined + } + ) => Channel.Channel< + OutElem1 | OutElem, + InElem & InElem1, + OutErr1 | OutErr, + InErr & InErr1, + OutDone1, + InDone & InDone1, + Env1 | Env + > +>( + (args) => core.isChannel(args[1]), + ( + self: Channel.Channel, + that: Channel.Channel, + options?: { + readonly concurrent?: boolean | undefined + } + ): Channel.Channel< + OutElem | OutElem1, + InElem & InElem1, + OutErr | OutErr1, + InErr & InErr1, + OutDone1, + InDone & InDone1, + Env | Env1 + > => + options?.concurrent ? + map(zip(self, that, { concurrent: true }), (tuple) => tuple[1]) : + core.flatMap(self, () => that) +) + +/** @internal */ +export const ChannelExceptionTypeId: Channel.ChannelExceptionTypeId = Symbol.for( + "effect/Channel/ChannelException" +) as Channel.ChannelExceptionTypeId + +/** @internal */ +export const ChannelException = (error: E): Channel.ChannelException => ({ + _tag: "ChannelException", + [ChannelExceptionTypeId]: ChannelExceptionTypeId, + error +}) + +/** @internal */ +export const isChannelException = (u: unknown): u is Channel.ChannelException => + hasProperty(u, ChannelExceptionTypeId) diff --git a/repos/effect/packages/effect/src/internal/channel/channelExecutor.ts b/repos/effect/packages/effect/src/internal/channel/channelExecutor.ts new file mode 100644 index 0000000..275e4de --- /dev/null +++ b/repos/effect/packages/effect/src/internal/channel/channelExecutor.ts @@ -0,0 +1,1200 @@ +import * as Cause from "../../Cause.js" +import type * as Channel from "../../Channel.js" +import type * as ChildExecutorDecision from "../../ChildExecutorDecision.js" +import type * as Context from "../../Context.js" +import * as Deferred from "../../Deferred.js" +import * as Effect from "../../Effect.js" +import * as ExecutionStrategy from "../../ExecutionStrategy.js" +import * as Exit from "../../Exit.js" +import * as Fiber from "../../Fiber.js" +import * as FiberId from "../../FiberId.js" +import { dual, identity, pipe } from "../../Function.js" +import * as HashSet from "../../HashSet.js" +import * as Option from "../../Option.js" +import * as Scope from "../../Scope.js" +import type * as UpstreamPullStrategy from "../../UpstreamPullStrategy.js" +import * as core from "../core-stream.js" +import * as ChannelOpCodes from "../opCodes/channel.js" +import * as ChildExecutorDecisionOpCodes from "../opCodes/channelChildExecutorDecision.js" +import * as ChannelStateOpCodes from "../opCodes/channelState.js" +import * as UpstreamPullStrategyOpCodes from "../opCodes/channelUpstreamPullStrategy.js" +import * as ContinuationOpCodes from "../opCodes/continuation.js" +import * as ChannelState from "./channelState.js" +import * as Continuation from "./continuation.js" +import * as Subexecutor from "./subexecutor.js" +import * as upstreamPullRequest from "./upstreamPullRequest.js" + +export type ErasedChannel = Channel.Channel + +/** @internal */ +export type ErasedExecutor = ChannelExecutor + +/** @internal */ +export type ErasedContinuation = Continuation.Continuation< + R, + unknown, + unknown, + unknown, + unknown, + unknown, + unknown, + unknown, + unknown +> + +/** @internal */ +export type ErasedFinalizer = (exit: Exit.Exit) => Effect.Effect + +/** @internal */ +export class ChannelExecutor< + out OutElem, + in InElem = unknown, + out OutErr = never, + in InErr = unknown, + out OutDone = void, + in InDone = unknown, + in out Env = never +> { + private _activeSubexecutor: Subexecutor.Subexecutor | undefined = undefined + + private _cancelled: Exit.Exit | undefined = undefined + + private _closeLastSubstream: Effect.Effect | undefined = undefined + + private _currentChannel: core.Primitive | undefined + + private _done: Exit.Exit | undefined = undefined + + private _doneStack: Array> = [] + + private _emitted: unknown | undefined = undefined + + private _executeCloseLastSubstream: ( + effect: Effect.Effect + ) => Effect.Effect + + private _input: ErasedExecutor | undefined = undefined + + private _inProgressFinalizer: Effect.Effect | undefined = undefined + + private _providedEnv: Context.Context | undefined + + constructor( + initialChannel: Channel.Channel, + providedEnv: Context.Context | undefined, + executeCloseLastSubstream: (effect: Effect.Effect) => Effect.Effect + ) { + this._currentChannel = initialChannel as core.Primitive + this._executeCloseLastSubstream = executeCloseLastSubstream + this._providedEnv = providedEnv + } + + run(): ChannelState.ChannelState { + let result: ChannelState.ChannelState | undefined = undefined + while (result === undefined) { + if (this._cancelled !== undefined) { + result = this.processCancellation() + } else if (this._activeSubexecutor !== undefined) { + result = this.runSubexecutor() + } else { + try { + if (this._currentChannel === undefined) { + result = ChannelState.Done() + } else { + if (Effect.isEffect(this._currentChannel)) { + this._currentChannel = core.fromEffect(this._currentChannel) as core.Primitive + } + switch (this._currentChannel._tag) { + case ChannelOpCodes.OP_BRACKET_OUT: { + result = this.runBracketOut(this._currentChannel) + break + } + + case ChannelOpCodes.OP_BRIDGE: { + const bridgeInput = this._currentChannel.input + + // PipeTo(left, Bridge(queue, channel)) + // In a fiber: repeatedly run left and push its outputs to the queue + // Add a finalizer to interrupt the fiber and close the executor + this._currentChannel = this._currentChannel.channel as core.Primitive + + if (this._input !== undefined) { + const inputExecutor = this._input + this._input = undefined + + const drainer = (): Effect.Effect => + Effect.flatMap(bridgeInput.awaitRead(), () => + Effect.suspend(() => { + const state = inputExecutor.run() as ChannelState.Primitive + switch (state._tag) { + case ChannelStateOpCodes.OP_DONE: { + return Exit.match(inputExecutor.getDone(), { + onFailure: (cause) => bridgeInput.error(cause), + onSuccess: (value) => bridgeInput.done(value) + }) + } + case ChannelStateOpCodes.OP_EMIT: { + return Effect.flatMap( + bridgeInput.emit(inputExecutor.getEmit()), + () => drainer() + ) + } + case ChannelStateOpCodes.OP_FROM_EFFECT: { + return Effect.matchCauseEffect(state.effect, { + onFailure: (cause) => bridgeInput.error(cause), + onSuccess: () => drainer() + }) + } + case ChannelStateOpCodes.OP_READ: { + return readUpstream( + state, + () => drainer(), + (cause) => bridgeInput.error(cause) + ) + } + } + })) as Effect.Effect + + result = ChannelState.fromEffect( + Effect.flatMap( + Effect.forkDaemon(Effect.interruptible(drainer())), + (fiber) => + Effect.sync(() => + this.addFinalizer((exit) => + Effect.flatMap(Fiber.interrupt(fiber), () => + Effect.suspend(() => { + const effect = this.restorePipe(exit, inputExecutor) + return effect !== undefined ? effect : Effect.void + })) + ) + ) + ) + ) + } + + break + } + + case ChannelOpCodes.OP_CONCAT_ALL: { + const executor: ErasedExecutor = new ChannelExecutor( + this._currentChannel.value() as Channel.Channel< + never, + unknown, + never, + unknown, + never, + unknown, + Env + >, + this._providedEnv, + (effect) => + Effect.sync(() => { + const prevLastClose = this._closeLastSubstream === undefined + ? Effect.void + : this._closeLastSubstream + this._closeLastSubstream = pipe(prevLastClose, Effect.zipRight(effect)) + }) + ) + executor._input = this._input + + const channel = this._currentChannel + this._activeSubexecutor = new Subexecutor.PullFromUpstream( + executor, + (value) => channel.k(value), + undefined, + [], + (x, y) => channel.combineInners(x, y), + (x, y) => channel.combineAll(x, y), + (request) => channel.onPull(request), + (value) => channel.onEmit(value) + ) + + this._closeLastSubstream = undefined + this._currentChannel = undefined + + break + } + + case ChannelOpCodes.OP_EMIT: { + this._emitted = this._currentChannel.out + this._currentChannel = (this._activeSubexecutor !== undefined ? + undefined : + core.void) as core.Primitive | undefined + result = ChannelState.Emit() + break + } + + case ChannelOpCodes.OP_ENSURING: { + this.runEnsuring(this._currentChannel) + break + } + + case ChannelOpCodes.OP_FAIL: { + result = this.doneHalt(this._currentChannel.error()) + break + } + + case ChannelOpCodes.OP_FOLD: { + this._doneStack.push(this._currentChannel.k as ErasedContinuation) + this._currentChannel = this._currentChannel.channel as core.Primitive + break + } + + case ChannelOpCodes.OP_FROM_EFFECT: { + const effect = this._providedEnv === undefined ? + this._currentChannel.effect() : + pipe( + this._currentChannel.effect(), + Effect.provide(this._providedEnv) + ) + + result = ChannelState.fromEffect( + Effect.matchCauseEffect(effect, { + onFailure: (cause) => { + const state = this.doneHalt(cause) + return state !== undefined && ChannelState.isFromEffect(state) ? + state.effect : + Effect.void + }, + onSuccess: (value) => { + const state = this.doneSucceed(value) + return state !== undefined && ChannelState.isFromEffect(state) ? + state.effect : + Effect.void + } + }) + ) as ChannelState.ChannelState | undefined + + break + } + + case ChannelOpCodes.OP_PIPE_TO: { + const previousInput = this._input + + const leftExec: ErasedExecutor = new ChannelExecutor( + this._currentChannel.left() as Channel.Channel, + this._providedEnv, + (effect) => this._executeCloseLastSubstream(effect) + ) + leftExec._input = previousInput + this._input = leftExec + + this.addFinalizer((exit) => { + const effect = this.restorePipe(exit, previousInput) + return effect !== undefined ? effect : Effect.void + }) + + this._currentChannel = this._currentChannel.right() as core.Primitive + + break + } + + case ChannelOpCodes.OP_PROVIDE: { + const previousEnv = this._providedEnv + this._providedEnv = this._currentChannel.context() + this._currentChannel = this._currentChannel.inner as core.Primitive + this.addFinalizer(() => + Effect.sync(() => { + this._providedEnv = previousEnv + }) + ) + break + } + + case ChannelOpCodes.OP_READ: { + const read = this._currentChannel + result = ChannelState.Read( + this._input!, + identity, + (emitted) => { + try { + this._currentChannel = read.more(emitted) as core.Primitive + } catch (error) { + this._currentChannel = read.done.onExit(Exit.die(error)) as core.Primitive + } + return undefined + }, + (exit) => { + const onExit = (exit: Exit.Exit): core.Primitive => { + return read.done.onExit(exit) as core.Primitive + } + this._currentChannel = onExit(exit) + return undefined + } + ) + break + } + + case ChannelOpCodes.OP_SUCCEED: { + result = this.doneSucceed(this._currentChannel.evaluate()) + break + } + + case ChannelOpCodes.OP_SUCCEED_NOW: { + result = this.doneSucceed(this._currentChannel.terminal) + break + } + + case ChannelOpCodes.OP_SUSPEND: { + this._currentChannel = this._currentChannel.channel() as core.Primitive + break + } + } + } + } catch (error) { + this._currentChannel = core.failCause(Cause.die(error)) as core.Primitive + } + } + } + return result + } + + getDone(): Exit.Exit { + return this._done as Exit.Exit + } + + getEmit(): OutElem { + return this._emitted as OutElem + } + + cancelWith(exit: Exit.Exit): void { + this._cancelled = exit + } + + clearInProgressFinalizer(): void { + this._inProgressFinalizer = undefined + } + + storeInProgressFinalizer(finalizer: Effect.Effect | undefined): void { + this._inProgressFinalizer = finalizer + } + + popAllFinalizers(exit: Exit.Exit): Effect.Effect { + const finalizers: Array> = [] + let next = this._doneStack.pop() as Continuation.Primitive | undefined + while (next) { + if (next._tag === "ContinuationFinalizer") { + finalizers.push(next.finalizer as ErasedFinalizer) + } + next = this._doneStack.pop() as Continuation.Primitive | undefined + } + const effect = (finalizers.length === 0 ? Effect.void : runFinalizers(finalizers, exit)) as Effect.Effect< + unknown, + never, + Env + > + this.storeInProgressFinalizer(effect) + return effect + } + + popNextFinalizers(): Array> { + const builder: Array> = [] + while (this._doneStack.length !== 0) { + const cont = this._doneStack[this._doneStack.length - 1] as Continuation.Primitive + if (cont._tag === ContinuationOpCodes.OP_CONTINUATION_K) { + return builder + } + builder.push(cont as Continuation.ContinuationFinalizer) + this._doneStack.pop() + } + return builder + } + + restorePipe( + exit: Exit.Exit, + prev: ErasedExecutor | undefined + ): Effect.Effect | undefined { + const currInput = this._input + this._input = prev + if (currInput !== undefined) { + const effect = currInput.close(exit) + return effect + } + return Effect.void + } + + close(exit: Exit.Exit): Effect.Effect | undefined { + let runInProgressFinalizers: Effect.Effect | undefined = undefined + const finalizer = this._inProgressFinalizer + if (finalizer !== undefined) { + runInProgressFinalizers = pipe( + finalizer, + Effect.ensuring(Effect.sync(() => this.clearInProgressFinalizer())) + ) + } + + let closeSelf: Effect.Effect | undefined = undefined + const selfFinalizers = this.popAllFinalizers(exit) + if (selfFinalizers !== undefined) { + closeSelf = pipe( + selfFinalizers, + Effect.ensuring(Effect.sync(() => this.clearInProgressFinalizer())) + ) + } + + const closeSubexecutors = this._activeSubexecutor === undefined ? + undefined : + this._activeSubexecutor.close(exit) + + if ( + closeSubexecutors === undefined && + runInProgressFinalizers === undefined && + closeSelf === undefined + ) { + return undefined + } + + return pipe( + Effect.exit(ifNotNull(closeSubexecutors)), + Effect.zip(Effect.exit(ifNotNull(runInProgressFinalizers))), + Effect.zip(Effect.exit(ifNotNull(closeSelf))), + Effect.map(([[exit1, exit2], exit3]) => pipe(exit1, Exit.zipRight(exit2), Exit.zipRight(exit3))), + Effect.uninterruptible, + // TODO: remove + Effect.flatMap((exit) => Effect.suspend(() => exit)) + ) + } + + doneSucceed(value: unknown): ChannelState.ChannelState | undefined { + if (this._doneStack.length === 0) { + this._done = Exit.succeed(value) + this._currentChannel = undefined + return ChannelState.Done() + } + + const head = this._doneStack[this._doneStack.length - 1] as Continuation.Primitive + if (head._tag === ContinuationOpCodes.OP_CONTINUATION_K) { + this._doneStack.pop() + this._currentChannel = head.onSuccess(value) as core.Primitive + return undefined + } + + const finalizers = this.popNextFinalizers() + if (this._doneStack.length === 0) { + this._doneStack = finalizers.reverse() + this._done = Exit.succeed(value) + this._currentChannel = undefined + return ChannelState.Done() + } + + const finalizerEffect = runFinalizers(finalizers.map((f) => f.finalizer), Exit.succeed(value))! + this.storeInProgressFinalizer(finalizerEffect) + + const effect = pipe( + finalizerEffect, + Effect.ensuring(Effect.sync(() => this.clearInProgressFinalizer())), + Effect.uninterruptible, + Effect.flatMap(() => Effect.sync(() => this.doneSucceed(value))) + ) + + return ChannelState.fromEffect(effect) + } + + doneHalt(cause: Cause.Cause): ChannelState.ChannelState | undefined { + if (this._doneStack.length === 0) { + this._done = Exit.failCause(cause) + this._currentChannel = undefined + return ChannelState.Done() + } + + const head = this._doneStack[this._doneStack.length - 1] as Continuation.Primitive + if (head._tag === ContinuationOpCodes.OP_CONTINUATION_K) { + this._doneStack.pop() + try { + this._currentChannel = head.onHalt(cause) as core.Primitive + } catch (error) { + this._currentChannel = core.failCause(Cause.die(error)) as core.Primitive + } + return undefined + } + + const finalizers = this.popNextFinalizers() + if (this._doneStack.length === 0) { + this._doneStack = finalizers.reverse() + this._done = Exit.failCause(cause) + this._currentChannel = undefined + return ChannelState.Done() + } + + const finalizerEffect = runFinalizers(finalizers.map((f) => f.finalizer), Exit.failCause(cause))! + this.storeInProgressFinalizer(finalizerEffect) + + const effect = pipe( + finalizerEffect, + Effect.ensuring(Effect.sync(() => this.clearInProgressFinalizer())), + Effect.uninterruptible, + Effect.flatMap(() => Effect.sync(() => this.doneHalt(cause))) + ) + + return ChannelState.fromEffect(effect) + } + + processCancellation(): ChannelState.ChannelState { + this._currentChannel = undefined + this._done = this._cancelled + this._cancelled = undefined + return ChannelState.Done() + } + + runBracketOut(bracketOut: core.BracketOut): ChannelState.ChannelState { + const effect = Effect.uninterruptible( + Effect.matchCauseEffect(this.provide(bracketOut.acquire() as Effect.Effect), { + onFailure: (cause) => + Effect.sync(() => { + this._currentChannel = core.failCause(cause) as core.Primitive + }), + onSuccess: (out) => + Effect.sync(() => { + this.addFinalizer((exit) => + this.provide(bracketOut.finalizer(out, exit)) as Effect.Effect + ) + this._currentChannel = core.write(out) as core.Primitive + }) + }) + ) + return ChannelState.fromEffect(effect) as ChannelState.ChannelState + } + + provide(effect: Effect.Effect): Effect.Effect { + if (this._providedEnv === undefined) { + return effect + } + return pipe(effect, Effect.provide(this._providedEnv)) + } + + runEnsuring(ensuring: core.Ensuring): void { + this.addFinalizer(ensuring.finalizer as ErasedFinalizer) + this._currentChannel = ensuring.channel as core.Primitive + } + + addFinalizer(f: ErasedFinalizer): void { + this._doneStack.push(new Continuation.ContinuationFinalizerImpl(f)) + } + + runSubexecutor(): ChannelState.ChannelState | undefined { + const subexecutor = this._activeSubexecutor as Subexecutor.Primitive + switch (subexecutor._tag) { + case Subexecutor.OP_PULL_FROM_CHILD: { + return this.pullFromChild( + subexecutor.childExecutor, + subexecutor.parentSubexecutor, + subexecutor.onEmit, + subexecutor + ) + } + case Subexecutor.OP_PULL_FROM_UPSTREAM: { + return this.pullFromUpstream(subexecutor) + } + case Subexecutor.OP_DRAIN_CHILD_EXECUTORS: { + return this.drainChildExecutors(subexecutor) + } + case Subexecutor.OP_EMIT: { + this._emitted = subexecutor.value + this._activeSubexecutor = subexecutor.next + return ChannelState.Emit() + } + } + } + + replaceSubexecutor(nextSubExec: Subexecutor.Subexecutor): void { + this._currentChannel = undefined + this._activeSubexecutor = nextSubExec + } + + finishWithExit(exit: Exit.Exit): Effect.Effect { + const state = Exit.match(exit, { + onFailure: (cause) => this.doneHalt(cause), + onSuccess: (value) => this.doneSucceed(value) + }) + this._activeSubexecutor = undefined + return state === undefined ? + Effect.void : + ChannelState.effect(state) + } + + finishSubexecutorWithCloseEffect( + subexecutorDone: Exit.Exit, + ...closeFuncs: Array<(exit: Exit.Exit) => Effect.Effect | undefined> + ): ChannelState.ChannelState | undefined { + this.addFinalizer(() => + pipe( + closeFuncs, + Effect.forEach((closeFunc) => + pipe( + Effect.sync(() => closeFunc(subexecutorDone)), + Effect.flatMap((closeEffect) => closeEffect !== undefined ? closeEffect : Effect.void) + ), { discard: true }) + ) + ) + const state = pipe( + subexecutorDone, + Exit.match({ + onFailure: (cause) => this.doneHalt(cause), + onSuccess: (value) => this.doneSucceed(value) + }) + ) + this._activeSubexecutor = undefined + return state + } + + applyUpstreamPullStrategy( + upstreamFinished: boolean, + queue: ReadonlyArray | undefined>, + strategy: UpstreamPullStrategy.UpstreamPullStrategy + ): [Option.Option, ReadonlyArray | undefined>] { + switch (strategy._tag) { + case UpstreamPullStrategyOpCodes.OP_PULL_AFTER_NEXT: { + const shouldPrepend = !upstreamFinished || queue.some((subexecutor) => subexecutor !== undefined) + return [strategy.emitSeparator, shouldPrepend ? [undefined, ...queue] : queue] + } + case UpstreamPullStrategyOpCodes.OP_PULL_AFTER_ALL_ENQUEUED: { + const shouldEnqueue = !upstreamFinished || queue.some((subexecutor) => subexecutor !== undefined) + return [strategy.emitSeparator, shouldEnqueue ? [...queue, undefined] : queue] + } + } + } + + pullFromChild( + childExecutor: ErasedExecutor, + parentSubexecutor: Subexecutor.Subexecutor, + onEmitted: (emitted: unknown) => ChildExecutorDecision.ChildExecutorDecision, + subexecutor: Subexecutor.PullFromChild + ): ChannelState.ChannelState | undefined { + return ChannelState.Read( + childExecutor, + identity, + (emitted) => { + const childExecutorDecision = onEmitted(emitted) + switch (childExecutorDecision._tag) { + case ChildExecutorDecisionOpCodes.OP_CONTINUE: { + break + } + case ChildExecutorDecisionOpCodes.OP_CLOSE: { + this.finishWithDoneValue(childExecutor, parentSubexecutor, childExecutorDecision.value) + break + } + case ChildExecutorDecisionOpCodes.OP_YIELD: { + const modifiedParent = parentSubexecutor.enqueuePullFromChild(subexecutor) + this.replaceSubexecutor(modifiedParent) + break + } + } + this._activeSubexecutor = new Subexecutor.Emit(emitted, this._activeSubexecutor!) + return undefined + }, + Exit.match({ + onFailure: (cause) => { + const state = this.handleSubexecutorFailure(childExecutor, parentSubexecutor, cause) + return state === undefined ? + undefined : + ChannelState.effectOrUndefinedIgnored(state) as Effect.Effect + }, + onSuccess: (doneValue) => { + this.finishWithDoneValue(childExecutor, parentSubexecutor, doneValue) + return undefined + } + }) + ) + } + + finishWithDoneValue( + childExecutor: ErasedExecutor, + parentSubexecutor: Subexecutor.Subexecutor, + doneValue: unknown + ): void { + const subexecutor = parentSubexecutor as Subexecutor.Primitive + switch (subexecutor._tag) { + case Subexecutor.OP_PULL_FROM_UPSTREAM: { + const modifiedParent = new Subexecutor.PullFromUpstream( + subexecutor.upstreamExecutor, + subexecutor.createChild, + subexecutor.lastDone !== undefined + ? subexecutor.combineChildResults( + subexecutor.lastDone, + doneValue + ) + : doneValue, + subexecutor.activeChildExecutors, + subexecutor.combineChildResults, + subexecutor.combineWithChildResult, + subexecutor.onPull, + subexecutor.onEmit + ) + this._closeLastSubstream = childExecutor.close(Exit.succeed(doneValue)) + this.replaceSubexecutor(modifiedParent) + break + } + case Subexecutor.OP_DRAIN_CHILD_EXECUTORS: { + const modifiedParent = new Subexecutor.DrainChildExecutors( + subexecutor.upstreamExecutor, + subexecutor.lastDone !== undefined + ? subexecutor.combineChildResults( + subexecutor.lastDone, + doneValue + ) + : doneValue, + subexecutor.activeChildExecutors, + subexecutor.upstreamDone, + subexecutor.combineChildResults, + subexecutor.combineWithChildResult, + subexecutor.onPull + ) + this._closeLastSubstream = childExecutor.close(Exit.succeed(doneValue)) + this.replaceSubexecutor(modifiedParent) + break + } + default: { + break + } + } + } + + handleSubexecutorFailure( + childExecutor: ErasedExecutor, + parentSubexecutor: Subexecutor.Subexecutor, + cause: Cause.Cause + ): ChannelState.ChannelState | undefined { + return this.finishSubexecutorWithCloseEffect( + Exit.failCause(cause), + (exit) => parentSubexecutor.close(exit), + (exit) => childExecutor.close(exit) + ) + } + + pullFromUpstream( + subexecutor: Subexecutor.PullFromUpstream + ): ChannelState.ChannelState | undefined { + if (subexecutor.activeChildExecutors.length === 0) { + return this.performPullFromUpstream(subexecutor) + } + + const activeChild = subexecutor.activeChildExecutors[0] + + const parentSubexecutor = new Subexecutor.PullFromUpstream( + subexecutor.upstreamExecutor, + subexecutor.createChild, + subexecutor.lastDone, + subexecutor.activeChildExecutors.slice(1), + subexecutor.combineChildResults, + subexecutor.combineWithChildResult, + subexecutor.onPull, + subexecutor.onEmit + ) + + if (activeChild === undefined) { + return this.performPullFromUpstream(parentSubexecutor) + } + + this.replaceSubexecutor( + new Subexecutor.PullFromChild( + activeChild.childExecutor, + parentSubexecutor, + activeChild.onEmit + ) + ) + + return undefined + } + + performPullFromUpstream( + subexecutor: Subexecutor.PullFromUpstream + ): ChannelState.ChannelState | undefined { + return ChannelState.Read( + subexecutor.upstreamExecutor, + (effect) => { + const closeLastSubstream = this._closeLastSubstream === undefined ? Effect.void : this._closeLastSubstream + this._closeLastSubstream = undefined + return pipe( + this._executeCloseLastSubstream(closeLastSubstream), + Effect.zipRight(effect) + ) + }, + (emitted) => { + if (this._closeLastSubstream !== undefined) { + const closeLastSubstream = this._closeLastSubstream + this._closeLastSubstream = undefined + return pipe( + this._executeCloseLastSubstream(closeLastSubstream), + Effect.map(() => { + const childExecutor: ErasedExecutor = new ChannelExecutor( + subexecutor.createChild(emitted), + this._providedEnv, + this._executeCloseLastSubstream + ) + + childExecutor._input = this._input + + const [emitSeparator, updatedChildExecutors] = this.applyUpstreamPullStrategy( + false, + subexecutor.activeChildExecutors, + subexecutor.onPull(upstreamPullRequest.Pulled(emitted)) + ) + + this._activeSubexecutor = new Subexecutor.PullFromChild( + childExecutor, + new Subexecutor.PullFromUpstream( + subexecutor.upstreamExecutor, + subexecutor.createChild, + subexecutor.lastDone, + updatedChildExecutors, + subexecutor.combineChildResults, + subexecutor.combineWithChildResult, + subexecutor.onPull, + subexecutor.onEmit + ), + subexecutor.onEmit + ) + + if (Option.isSome(emitSeparator)) { + this._activeSubexecutor = new Subexecutor.Emit(emitSeparator.value, this._activeSubexecutor) + } + + return undefined + }) + ) + } + + const childExecutor: ErasedExecutor = new ChannelExecutor( + subexecutor.createChild(emitted), + this._providedEnv, + this._executeCloseLastSubstream + ) + + childExecutor._input = this._input + + const [emitSeparator, updatedChildExecutors] = this.applyUpstreamPullStrategy( + false, + subexecutor.activeChildExecutors, + subexecutor.onPull(upstreamPullRequest.Pulled(emitted)) + ) + + this._activeSubexecutor = new Subexecutor.PullFromChild( + childExecutor, + new Subexecutor.PullFromUpstream( + subexecutor.upstreamExecutor, + subexecutor.createChild, + subexecutor.lastDone, + updatedChildExecutors, + subexecutor.combineChildResults, + subexecutor.combineWithChildResult, + subexecutor.onPull, + subexecutor.onEmit + ), + subexecutor.onEmit + ) + + if (Option.isSome(emitSeparator)) { + this._activeSubexecutor = new Subexecutor.Emit(emitSeparator.value, this._activeSubexecutor) + } + + return undefined + }, + (exit) => { + if (subexecutor.activeChildExecutors.some((subexecutor) => subexecutor !== undefined)) { + const drain = new Subexecutor.DrainChildExecutors( + subexecutor.upstreamExecutor, + subexecutor.lastDone, + [undefined, ...subexecutor.activeChildExecutors], + subexecutor.upstreamExecutor.getDone(), + subexecutor.combineChildResults, + subexecutor.combineWithChildResult, + subexecutor.onPull + ) + + if (this._closeLastSubstream !== undefined) { + const closeLastSubstream = this._closeLastSubstream + this._closeLastSubstream = undefined + return pipe( + this._executeCloseLastSubstream(closeLastSubstream), + Effect.map(() => this.replaceSubexecutor(drain)) + ) + } + + this.replaceSubexecutor(drain) + + return undefined + } + + const closeLastSubstream = this._closeLastSubstream + const state = this.finishSubexecutorWithCloseEffect( + pipe(exit, Exit.map((a) => subexecutor.combineWithChildResult(subexecutor.lastDone, a))), + () => closeLastSubstream, + (exit) => subexecutor.upstreamExecutor.close(exit) + ) + return state === undefined ? + undefined : + // NOTE: assuming finalizers cannot fail + ChannelState.effectOrUndefinedIgnored(state as ChannelState.ChannelState) + } + ) + } + + drainChildExecutors( + subexecutor: Subexecutor.DrainChildExecutors + ): ChannelState.ChannelState | undefined { + if (subexecutor.activeChildExecutors.length === 0) { + const lastClose = this._closeLastSubstream + if (lastClose !== undefined) { + this.addFinalizer(() => Effect.succeed(lastClose)) + } + return this.finishSubexecutorWithCloseEffect( + subexecutor.upstreamDone, + () => lastClose, + (exit) => subexecutor.upstreamExecutor.close(exit) + ) + } + + const activeChild = subexecutor.activeChildExecutors[0] + const rest = subexecutor.activeChildExecutors.slice(1) + + if (activeChild === undefined) { + const [emitSeparator, remainingExecutors] = this.applyUpstreamPullStrategy( + true, + rest, + subexecutor.onPull( + upstreamPullRequest.NoUpstream(rest.reduce((n, curr) => curr !== undefined ? n + 1 : n, 0)) + ) + ) + + this.replaceSubexecutor( + new Subexecutor.DrainChildExecutors( + subexecutor.upstreamExecutor, + subexecutor.lastDone, + remainingExecutors, + subexecutor.upstreamDone, + subexecutor.combineChildResults, + subexecutor.combineWithChildResult, + subexecutor.onPull + ) + ) + + if (Option.isSome(emitSeparator)) { + this._emitted = emitSeparator.value + return ChannelState.Emit() + } + + return undefined + } + + const parentSubexecutor = new Subexecutor.DrainChildExecutors( + subexecutor.upstreamExecutor, + subexecutor.lastDone, + rest, + subexecutor.upstreamDone, + subexecutor.combineChildResults, + subexecutor.combineWithChildResult, + subexecutor.onPull + ) + + this.replaceSubexecutor( + new Subexecutor.PullFromChild( + activeChild.childExecutor, + parentSubexecutor, + activeChild.onEmit + ) + ) + + return undefined + } +} + +const ifNotNull = (effect: Effect.Effect | undefined): Effect.Effect => + effect !== undefined ? effect : Effect.void + +const runFinalizers = ( + finalizers: Array>, + exit: Exit.Exit +): Effect.Effect => { + return pipe( + Effect.forEach(finalizers, (fin) => Effect.exit(fin(exit))), + Effect.map((exits) => pipe(Exit.all(exits), Option.getOrElse(() => Exit.void))), + Effect.flatMap((exit) => Effect.suspend(() => exit as Exit.Exit)) + ) +} + +/** + * @internal + */ +export const readUpstream = ( + r: ChannelState.Read, + onSuccess: () => Effect.Effect, + onFailure: (cause: Cause.Cause) => Effect.Effect +): Effect.Effect => { + const readStack = [r as ChannelState.Read] + const read = (): Effect.Effect => { + const current = readStack.pop() + if (current === undefined || current.upstream === undefined) { + return Effect.dieMessage("Unexpected end of input for channel execution") + } + const state = current.upstream.run() as ChannelState.Primitive + switch (state._tag) { + case ChannelStateOpCodes.OP_EMIT: { + const emitEffect = current.onEmit(current.upstream.getEmit()) + if (readStack.length === 0) { + if (emitEffect === undefined) { + return Effect.suspend(onSuccess) + } + return pipe( + emitEffect as Effect.Effect, + Effect.matchCauseEffect({ onFailure, onSuccess }) + ) + } + if (emitEffect === undefined) { + return Effect.suspend(() => read()) + } + return pipe( + emitEffect as Effect.Effect, + Effect.matchCauseEffect({ onFailure, onSuccess: () => read() }) + ) + } + + case ChannelStateOpCodes.OP_DONE: { + const doneEffect = current.onDone(current.upstream.getDone()) + if (readStack.length === 0) { + if (doneEffect === undefined) { + return Effect.suspend(onSuccess) + } + return pipe( + doneEffect as Effect.Effect, + Effect.matchCauseEffect({ onFailure, onSuccess }) + ) + } + if (doneEffect === undefined) { + return Effect.suspend(() => read()) + } + return pipe( + doneEffect as Effect.Effect, + Effect.matchCauseEffect({ onFailure, onSuccess: () => read() }) + ) + } + + case ChannelStateOpCodes.OP_FROM_EFFECT: { + readStack.push(current) + return pipe( + current.onEffect(state.effect as Effect.Effect) as Effect.Effect, + Effect.catchAllCause((cause) => + Effect.suspend(() => { + const doneEffect = current.onDone(Exit.failCause(cause)) as Effect.Effect + return doneEffect === undefined ? Effect.void : doneEffect + }) + ), + Effect.matchCauseEffect({ onFailure, onSuccess: () => read() }) + ) + } + + case ChannelStateOpCodes.OP_READ: { + readStack.push(current) + readStack.push(state) + return Effect.suspend(() => read()) + } + } + } + return read() +} + +/** @internal */ +export const runIn = dual< + (scope: Scope.Scope) => ( + self: Channel.Channel + ) => Effect.Effect, + ( + self: Channel.Channel, + scope: Scope.Scope + ) => Effect.Effect +>(2, ( + self: Channel.Channel, + scope: Scope.Scope +) => { + const run = ( + channelDeferred: Deferred.Deferred, + scopeDeferred: Deferred.Deferred, + scope: Scope.Scope + ) => + Effect.acquireUseRelease( + Effect.sync(() => new ChannelExecutor(self, void 0, identity)), + (exec) => + Effect.suspend(() => + runScopedInterpret(exec.run() as ChannelState.ChannelState, exec).pipe( + Effect.intoDeferred(channelDeferred), + Effect.zipRight(Deferred.await(channelDeferred)), + Effect.zipLeft(Deferred.await(scopeDeferred)) + ) + ), + (exec, exit) => { + const finalize = exec.close(exit) + if (finalize === undefined) { + return Effect.void + } + return Effect.tapErrorCause( + finalize, + (cause) => Scope.addFinalizer(scope, Effect.failCause(cause)) + ) + } + ) + return Effect.uninterruptibleMask((restore) => + Effect.all([ + Scope.fork(scope, ExecutionStrategy.sequential), + Deferred.make(), + Deferred.make() + ]).pipe(Effect.flatMap(([child, channelDeferred, scopeDeferred]) => + restore(run(channelDeferred, scopeDeferred, child)).pipe( + Effect.forkIn(scope), + Effect.flatMap((fiber) => + scope.addFinalizer((exit) => { + const interruptors = Exit.isFailure(exit) ? Cause.interruptors(exit.cause) : undefined + return Deferred.isDone(channelDeferred).pipe( + Effect.flatMap((isDone) => + isDone + ? Deferred.succeed(scopeDeferred, void 0).pipe( + Effect.zipRight(Fiber.await(fiber)), + Effect.zipRight(Fiber.inheritAll(fiber)) + ) + : Deferred.succeed(scopeDeferred, void 0).pipe( + Effect.zipRight( + interruptors && HashSet.size(interruptors) > 0 + ? Fiber.interruptAs(fiber, FiberId.combineAll(interruptors)) + : Fiber.interrupt(fiber) + ), + Effect.zipRight(Fiber.inheritAll(fiber)) + ) + ) + ) + }).pipe(Effect.zipRight(restore(Deferred.await(channelDeferred)))) + ) + ) + )) + ) +}) + +/** @internal */ +const runScopedInterpret = ( + channelState: ChannelState.ChannelState, + exec: ChannelExecutor +): Effect.Effect => { + const op = channelState as ChannelState.Primitive + switch (op._tag) { + case ChannelStateOpCodes.OP_FROM_EFFECT: { + return pipe( + op.effect as Effect.Effect, + Effect.flatMap(() => runScopedInterpret(exec.run() as ChannelState.ChannelState, exec)) + ) + } + case ChannelStateOpCodes.OP_EMIT: { + // Can't really happen because Out <:< Nothing. So just skip ahead. + return runScopedInterpret( + exec.run() as ChannelState.ChannelState, + exec + ) + } + case ChannelStateOpCodes.OP_DONE: { + return Effect.suspend(() => exec.getDone()) + } + case ChannelStateOpCodes.OP_READ: { + return readUpstream( + op, + () => runScopedInterpret(exec.run() as ChannelState.ChannelState, exec), + Effect.failCause + ) as Effect.Effect + } + } +} diff --git a/repos/effect/packages/effect/src/internal/channel/channelState.ts b/repos/effect/packages/effect/src/internal/channel/channelState.ts new file mode 100644 index 0000000..20c425b --- /dev/null +++ b/repos/effect/packages/effect/src/internal/channel/channelState.ts @@ -0,0 +1,134 @@ +import * as Effect from "../../Effect.js" +import type * as Exit from "../../Exit.js" +import { hasProperty } from "../../Predicate.js" +import type * as Types from "../../Types.js" +import * as OpCodes from "../opCodes/channelState.js" +import type { ErasedExecutor } from "./channelExecutor.js" + +/** @internal */ +export const ChannelStateTypeId = Symbol.for("effect/ChannelState") + +/** @internal */ +export type ChannelStateTypeId = typeof ChannelStateTypeId + +/** @internal */ +export interface ChannelState extends ChannelState.Variance {} + +/** @internal */ +export declare namespace ChannelState { + export interface Variance { + readonly [ChannelStateTypeId]: { + readonly _E: Types.Covariant + readonly _R: Types.Covariant + } + } +} + +const channelStateVariance = { + /* c8 ignore next */ + _E: (_: never) => _, + /* c8 ignore next */ + _R: (_: never) => _ +} + +/** @internal */ +const proto = { + [ChannelStateTypeId]: channelStateVariance +} + +/** @internal */ +export type Primitive = + | Done + | Emit + | FromEffect + | Read + +/** @internal */ +export type Op = ChannelState & Body & { + readonly _tag: Tag +} + +/** @internal */ +export interface Done extends Op {} + +/** @internal */ +export interface Emit extends Op {} + +/** @internal */ +export interface FromEffect extends + Op + }> +{} + +/** @internal */ +export interface Read extends + Op + onEffect(effect: Effect.Effect): Effect.Effect + onEmit(value: unknown): Effect.Effect + onDone(exit: Exit.Exit): Effect.Effect + }> +{} + +/** @internal */ +export const Done = (): ChannelState => { + const op = Object.create(proto) + op._tag = OpCodes.OP_DONE + return op +} + +/** @internal */ +export const Emit = (): ChannelState => { + const op = Object.create(proto) + op._tag = OpCodes.OP_EMIT + return op +} + +/** @internal */ +export const fromEffect = (effect: Effect.Effect): ChannelState => { + const op = Object.create(proto) + op._tag = OpCodes.OP_FROM_EFFECT + op.effect = effect + return op +} + +/** @internal */ +export const Read = ( + upstream: ErasedExecutor, + onEffect: (effect: Effect.Effect) => Effect.Effect, + onEmit: (value: unknown) => Effect.Effect | undefined, + onDone: (exit: Exit.Exit) => Effect.Effect | undefined +): ChannelState => { + const op = Object.create(proto) + op._tag = OpCodes.OP_READ + op.upstream = upstream + op.onEffect = onEffect + op.onEmit = onEmit + op.onDone = onDone + return op +} + +/** @internal */ +export const isChannelState = (u: unknown): u is ChannelState => hasProperty(u, ChannelStateTypeId) + +/** @internal */ +export const isDone = (self: ChannelState): self is Done => (self as Primitive)._tag === OpCodes.OP_DONE + +/** @internal */ +export const isEmit = (self: ChannelState): self is Emit => (self as Primitive)._tag === OpCodes.OP_EMIT + +/** @internal */ +export const isFromEffect = (self: ChannelState): self is FromEffect => + (self as Primitive)._tag === OpCodes.OP_FROM_EFFECT + +/** @internal */ +export const isRead = (self: ChannelState): self is Read => (self as Primitive)._tag === OpCodes.OP_READ + +/** @internal */ +export const effect = (self: ChannelState): Effect.Effect => + isFromEffect(self) ? self.effect as Effect.Effect : Effect.void + +/** @internal */ +export const effectOrUndefinedIgnored = (self: ChannelState): Effect.Effect | undefined => + isFromEffect(self) ? Effect.ignore(self.effect as Effect.Effect) : undefined diff --git a/repos/effect/packages/effect/src/internal/channel/childExecutorDecision.ts b/repos/effect/packages/effect/src/internal/channel/childExecutorDecision.ts new file mode 100644 index 0000000..930ec74 --- /dev/null +++ b/repos/effect/packages/effect/src/internal/channel/childExecutorDecision.ts @@ -0,0 +1,96 @@ +import type * as ChildExecutorDecision from "../../ChildExecutorDecision.js" +import { dual } from "../../Function.js" +import { hasProperty } from "../../Predicate.js" +import * as OpCodes from "../opCodes/channelChildExecutorDecision.js" + +/** @internal */ +const ChildExecutorDecisionSymbolKey = "effect/ChannelChildExecutorDecision" + +/** @internal */ +export const ChildExecutorDecisionTypeId: ChildExecutorDecision.ChildExecutorDecisionTypeId = Symbol.for( + ChildExecutorDecisionSymbolKey +) as ChildExecutorDecision.ChildExecutorDecisionTypeId + +/** @internal */ +const proto = { + [ChildExecutorDecisionTypeId]: ChildExecutorDecisionTypeId +} + +/** @internal */ +export const Continue = (_: void): ChildExecutorDecision.ChildExecutorDecision => { + const op = Object.create(proto) + op._tag = OpCodes.OP_CONTINUE + return op +} + +/** @internal */ +export const Close = (value: unknown): ChildExecutorDecision.ChildExecutorDecision => { + const op = Object.create(proto) + op._tag = OpCodes.OP_CLOSE + op.value = value + return op +} + +/** @internal */ +export const Yield = (_: void): ChildExecutorDecision.ChildExecutorDecision => { + const op = Object.create(proto) + op._tag = OpCodes.OP_YIELD + return op +} + +/** @internal */ +export const isChildExecutorDecision = (u: unknown): u is ChildExecutorDecision.ChildExecutorDecision => + hasProperty(u, ChildExecutorDecisionTypeId) + +/** @internal */ +export const isContinue = ( + self: ChildExecutorDecision.ChildExecutorDecision +): self is ChildExecutorDecision.Continue => self._tag === OpCodes.OP_CONTINUE + +/** @internal */ +export const isClose = ( + self: ChildExecutorDecision.ChildExecutorDecision +): self is ChildExecutorDecision.Close => self._tag === OpCodes.OP_CLOSE + +/** @internal */ +export const isYield = ( + self: ChildExecutorDecision.ChildExecutorDecision +): self is ChildExecutorDecision.Yield => self._tag === OpCodes.OP_YIELD + +/** @internal */ +export const match = dual< + ( + options: { + readonly onContinue: () => A + readonly onClose: (value: unknown) => A + readonly onYield: () => A + } + ) => (self: ChildExecutorDecision.ChildExecutorDecision) => A, + ( + self: ChildExecutorDecision.ChildExecutorDecision, + options: { + readonly onContinue: () => A + readonly onClose: (value: unknown) => A + readonly onYield: () => A + } + ) => A +>(2, ( + self: ChildExecutorDecision.ChildExecutorDecision, + { onClose, onContinue, onYield }: { + readonly onContinue: () => A + readonly onClose: (value: unknown) => A + readonly onYield: () => A + } +): A => { + switch (self._tag) { + case OpCodes.OP_CONTINUE: { + return onContinue() + } + case OpCodes.OP_CLOSE: { + return onClose(self.value) + } + case OpCodes.OP_YIELD: { + return onYield() + } + } +}) diff --git a/repos/effect/packages/effect/src/internal/channel/continuation.ts b/repos/effect/packages/effect/src/internal/channel/continuation.ts new file mode 100644 index 0000000..5578ecd --- /dev/null +++ b/repos/effect/packages/effect/src/internal/channel/continuation.ts @@ -0,0 +1,200 @@ +import type * as Cause from "../../Cause.js" +import type * as Channel from "../../Channel.js" +import type * as Effect from "../../Effect.js" +import * as Exit from "../../Exit.js" +import type * as Types from "../../Types.js" +import * as OpCodes from "../opCodes/continuation.js" + +/** @internal */ +export const ContinuationTypeId = Symbol.for("effect/ChannelContinuation") + +/** @internal */ +export type ContinuationTypeId = typeof ContinuationTypeId + +/** @internal */ +export interface Continuation< + out Env, + in InErr, + in InElem, + in InDone, + out OutErr, + out OutErr2, + out OutElem, + out OutDone, + out OutDone2 +> extends Continuation.Variance {} + +/** @internal */ +export declare namespace Continuation { + /** @internal */ + export interface Variance< + out Env, + in InErr, + in InElem, + in InDone, + out OutErr, + out OutErr2, + out OutElem, + out OutDone, + out OutDone2 + > { + readonly [ContinuationTypeId]: { + readonly _Env: Types.Covariant + readonly _InErr: Types.Contravariant + readonly _InElem: Types.Contravariant + readonly _InDone: Types.Contravariant + readonly _OutErr: Types.Covariant + readonly _OutDone: Types.Covariant + readonly _OutErr2: Types.Covariant + readonly _OutElem: Types.Covariant + readonly _OutDone2: Types.Covariant + } + } +} + +/** @internal */ +export type Primitive = ErasedContinuationK | ErasedContinuationFinalizer + +/** @internal */ +export type ErasedContinuationK = ContinuationK< + unknown, + unknown, + unknown, + unknown, + unknown, + unknown, + unknown, + unknown, + unknown +> + +/** @internal */ +export type ErasedContinuationFinalizer = ContinuationFinalizer + +/** @internal */ +export interface ContinuationK< + out Env, + in InErr, + in InElem, + in InDone, + out OutErr, + out OutErr2, + out OutElem, + out OutDone, + out OutDone2 +> extends + Continuation< + Env, + InErr, + InElem, + InDone, + OutErr, + OutErr2, + OutElem, + OutDone, + OutDone2 + > +{ + readonly _tag: OpCodes.OP_CONTINUATION_K + onSuccess( + o: OutDone + ): Channel.Channel + onHalt( + c: Cause.Cause + ): Channel.Channel + onExit( + exit: Exit.Exit + ): Channel.Channel +} + +/** @internal */ +export interface ContinuationFinalizer extends + Continuation< + Env, + unknown, + unknown, + unknown, + OutErr, + never, + never, + OutDone, + never + > +{ + readonly _tag: OpCodes.OP_CONTINUATION_FINALIZER + finalizer(exit: Exit.Exit): Effect.Effect +} + +const continuationVariance = { + /* c8 ignore next */ + _Env: (_: never) => _, + /* c8 ignore next */ + _InErr: (_: unknown) => _, + /* c8 ignore next */ + _InElem: (_: unknown) => _, + /* c8 ignore next */ + _InDone: (_: unknown) => _, + /* c8 ignore next */ + _OutErr: (_: never) => _, + /* c8 ignore next */ + _OutDone: (_: never) => _, + /* c8 ignore next */ + _OutErr2: (_: never) => _, + /* c8 ignore next */ + _OutElem: (_: never) => _, + /* c8 ignore next */ + _OutDone2: (_: never) => _ +} + +/** @internal */ +export class ContinuationKImpl< + out Env, + out Env2, + in InErr, + in InElem, + in InDone, + in out OutErr, + out OutErr2, + out OutElem, + in out OutDone, + out OutDone2 +> implements + ContinuationK< + Env | Env2, + InErr, + InElem, + InDone, + OutErr, + OutErr2, + OutElem, + OutDone, + OutDone2 + > +{ + readonly _tag = OpCodes.OP_CONTINUATION_K + readonly [ContinuationTypeId] = continuationVariance + constructor( + readonly onSuccess: ( + o: OutDone + ) => Channel.Channel, + readonly onHalt: ( + c: Cause.Cause + ) => Channel.Channel + ) { + } + onExit( + exit: Exit.Exit + ): Channel.Channel { + return Exit.isFailure(exit) ? this.onHalt(exit.cause) : this.onSuccess(exit.value) + } +} + +/** @internal */ +export class ContinuationFinalizerImpl + implements ContinuationFinalizer +{ + readonly _tag = OpCodes.OP_CONTINUATION_FINALIZER + readonly [ContinuationTypeId] = continuationVariance + constructor(readonly finalizer: (exit: Exit.Exit) => Effect.Effect) { + } +} diff --git a/repos/effect/packages/effect/src/internal/channel/mergeDecision.ts b/repos/effect/packages/effect/src/internal/channel/mergeDecision.ts new file mode 100644 index 0000000..9a85a82 --- /dev/null +++ b/repos/effect/packages/effect/src/internal/channel/mergeDecision.ts @@ -0,0 +1,113 @@ +import type * as Effect from "../../Effect.js" +import type * as Exit from "../../Exit.js" +import { dual } from "../../Function.js" +import type * as MergeDecision from "../../MergeDecision.js" +import { hasProperty } from "../../Predicate.js" +import * as OpCodes from "../opCodes/channelMergeDecision.js" + +/** @internal */ +const MergeDecisionSymbolKey = "effect/ChannelMergeDecision" + +/** @internal */ +export const MergeDecisionTypeId: MergeDecision.MergeDecisionTypeId = Symbol.for( + MergeDecisionSymbolKey +) as MergeDecision.MergeDecisionTypeId + +/** @internal */ +const proto = { + [MergeDecisionTypeId]: { + _R: (_: never) => _, + _E0: (_: unknown) => _, + _Z0: (_: unknown) => _, + _E: (_: never) => _, + _Z: (_: never) => _ + } +} + +/** @internal */ +export type Primitive = + | Done + | Await + +/** @internal */ +export type Op = + & MergeDecision.MergeDecision + & Body + & { + readonly _tag: Tag + } + +/** @internal */ +export interface Done extends + Op + }> +{} + +/** @internal */ +export interface Await extends + Op): Effect.Effect + }> +{} + +/** @internal */ +export const Done = ( + effect: Effect.Effect +): MergeDecision.MergeDecision => { + const op = Object.create(proto) + op._tag = OpCodes.OP_DONE + op.effect = effect + return op +} + +/** @internal */ +export const Await = ( + f: (exit: Exit.Exit) => Effect.Effect +): MergeDecision.MergeDecision => { + const op = Object.create(proto) + op._tag = OpCodes.OP_AWAIT + op.f = f + return op +} + +/** @internal */ +export const AwaitConst = ( + effect: Effect.Effect +): MergeDecision.MergeDecision => Await(() => effect) + +/** @internal */ +export const isMergeDecision = ( + u: unknown +): u is MergeDecision.MergeDecision => hasProperty(u, MergeDecisionTypeId) + +/** @internal */ +export const match = dual< + ( + options: { + readonly onDone: (effect: Effect.Effect) => Z2 + readonly onAwait: (f: (exit: Exit.Exit) => Effect.Effect) => Z2 + } + ) => (self: MergeDecision.MergeDecision) => Z2, + ( + self: MergeDecision.MergeDecision, + options: { + readonly onDone: (effect: Effect.Effect) => Z2 + readonly onAwait: (f: (exit: Exit.Exit) => Effect.Effect) => Z2 + } + ) => Z2 +>(2, ( + self: MergeDecision.MergeDecision, + { onAwait, onDone }: { + readonly onDone: (effect: Effect.Effect) => Z2 + readonly onAwait: (f: (exit: Exit.Exit) => Effect.Effect) => Z2 + } +): Z2 => { + const op = self as Primitive + switch (op._tag) { + case OpCodes.OP_DONE: + return onDone(op.effect) + case OpCodes.OP_AWAIT: + return onAwait(op.f) + } +}) diff --git a/repos/effect/packages/effect/src/internal/channel/mergeState.ts b/repos/effect/packages/effect/src/internal/channel/mergeState.ts new file mode 100644 index 0000000..4d26e38 --- /dev/null +++ b/repos/effect/packages/effect/src/internal/channel/mergeState.ts @@ -0,0 +1,120 @@ +import type * as Effect from "../../Effect.js" +import type * as Either from "../../Either.js" +import type * as Exit from "../../Exit.js" +import type * as Fiber from "../../Fiber.js" +import { dual } from "../../Function.js" +import type * as MergeState from "../../MergeState.js" +import { hasProperty } from "../../Predicate.js" +import * as OpCodes from "../opCodes/channelMergeState.js" + +/** @internal */ +const MergeStateSymbolKey = "effect/ChannelMergeState" + +/** @internal */ +export const MergeStateTypeId: MergeState.MergeStateTypeId = Symbol.for( + MergeStateSymbolKey +) as MergeState.MergeStateTypeId + +/** @internal */ +const proto = { + [MergeStateTypeId]: MergeStateTypeId +} + +/** @internal */ +export const BothRunning = ( + left: Fiber.Fiber, Err>, + right: Fiber.Fiber, Err1> +): MergeState.MergeState => { + const op = Object.create(proto) + op._tag = OpCodes.OP_BOTH_RUNNING + op.left = left + op.right = right + return op +} + +/** @internal */ +export const LeftDone = ( + f: (exit: Exit.Exit) => Effect.Effect +): MergeState.MergeState => { + const op = Object.create(proto) + op._tag = OpCodes.OP_LEFT_DONE + op.f = f + return op +} + +/** @internal */ +export const RightDone = ( + f: (exit: Exit.Exit) => Effect.Effect +): MergeState.MergeState => { + const op = Object.create(proto) + op._tag = OpCodes.OP_RIGHT_DONE + op.f = f + return op +} + +/** @internal */ +export const isMergeState = ( + u: unknown +): u is MergeState.MergeState => + hasProperty(u, MergeStateTypeId) + +/** @internal */ +export const isBothRunning = ( + self: MergeState.MergeState +): self is MergeState.BothRunning => { + return self._tag === OpCodes.OP_BOTH_RUNNING +} + +/** @internal */ +export const isLeftDone = ( + self: MergeState.MergeState +): self is MergeState.LeftDone => { + return self._tag === OpCodes.OP_LEFT_DONE +} + +/** @internal */ +export const isRightDone = ( + self: MergeState.MergeState +): self is MergeState.RightDone => { + return self._tag === OpCodes.OP_RIGHT_DONE +} + +/** @internal */ +export const match = dual< + ( + options: { + readonly onBothRunning: ( + left: Fiber.Fiber, Err>, + right: Fiber.Fiber, Err1> + ) => Z + readonly onLeftDone: (f: (exit: Exit.Exit) => Effect.Effect) => Z + readonly onRightDone: (f: (exit: Exit.Exit) => Effect.Effect) => Z + } + ) => (self: MergeState.MergeState) => Z, + ( + self: MergeState.MergeState, + options: { + readonly onBothRunning: ( + left: Fiber.Fiber, Err>, + right: Fiber.Fiber, Err1> + ) => Z + readonly onLeftDone: (f: (exit: Exit.Exit) => Effect.Effect) => Z + readonly onRightDone: (f: (exit: Exit.Exit) => Effect.Effect) => Z + } + ) => Z +>(2, ( + self, + { onBothRunning, onLeftDone, onRightDone } +) => { + switch (self._tag) { + case OpCodes.OP_BOTH_RUNNING: { + return onBothRunning(self.left, self.right) + } + case OpCodes.OP_LEFT_DONE: { + return onLeftDone(self.f) + } + case OpCodes.OP_RIGHT_DONE: { + return onRightDone(self.f) + } + } +}) diff --git a/repos/effect/packages/effect/src/internal/channel/mergeStrategy.ts b/repos/effect/packages/effect/src/internal/channel/mergeStrategy.ts new file mode 100644 index 0000000..ad72977 --- /dev/null +++ b/repos/effect/packages/effect/src/internal/channel/mergeStrategy.ts @@ -0,0 +1,72 @@ +import { dual } from "../../Function.js" +import type * as MergeStrategy from "../../MergeStrategy.js" +import { hasProperty } from "../../Predicate.js" +import * as OpCodes from "../opCodes/channelMergeStrategy.js" + +/** @internal */ +const MergeStrategySymbolKey = "effect/ChannelMergeStrategy" + +/** @internal */ +export const MergeStrategyTypeId: MergeStrategy.MergeStrategyTypeId = Symbol.for( + MergeStrategySymbolKey +) as MergeStrategy.MergeStrategyTypeId + +/** @internal */ +const proto = { + [MergeStrategyTypeId]: MergeStrategyTypeId +} + +/** @internal */ +export const BackPressure = (_: void): MergeStrategy.MergeStrategy => { + const op = Object.create(proto) + op._tag = OpCodes.OP_BACK_PRESSURE + return op +} + +/** @internal */ +export const BufferSliding = (_: void): MergeStrategy.MergeStrategy => { + const op = Object.create(proto) + op._tag = OpCodes.OP_BUFFER_SLIDING + return op +} + +/** @internal */ +export const isMergeStrategy = (u: unknown): u is MergeStrategy.MergeStrategy => hasProperty(u, MergeStrategyTypeId) + +/** @internal */ +export const isBackPressure = (self: MergeStrategy.MergeStrategy): self is MergeStrategy.BackPressure => + self._tag === OpCodes.OP_BACK_PRESSURE + +/** @internal */ +export const isBufferSliding = (self: MergeStrategy.MergeStrategy): self is MergeStrategy.BufferSliding => + self._tag === OpCodes.OP_BUFFER_SLIDING + +/** @internal */ +export const match = dual< + (options: { + readonly onBackPressure: () => A + readonly onBufferSliding: () => A + }) => (self: MergeStrategy.MergeStrategy) => A, + ( + self: MergeStrategy.MergeStrategy, + options: { + readonly onBackPressure: () => A + readonly onBufferSliding: () => A + } + ) => A +>(2, ( + self: MergeStrategy.MergeStrategy, + { onBackPressure, onBufferSliding }: { + readonly onBackPressure: () => A + readonly onBufferSliding: () => A + } +): A => { + switch (self._tag) { + case OpCodes.OP_BACK_PRESSURE: { + return onBackPressure() + } + case OpCodes.OP_BUFFER_SLIDING: { + return onBufferSliding() + } + } +}) diff --git a/repos/effect/packages/effect/src/internal/channel/singleProducerAsyncInput.ts b/repos/effect/packages/effect/src/internal/channel/singleProducerAsyncInput.ts new file mode 100644 index 0000000..17d1196 --- /dev/null +++ b/repos/effect/packages/effect/src/internal/channel/singleProducerAsyncInput.ts @@ -0,0 +1,259 @@ +import * as Cause from "../../Cause.js" +import * as Deferred from "../../Deferred.js" +import * as Effect from "../../Effect.js" +import * as Either from "../../Either.js" +import * as Exit from "../../Exit.js" +import { pipe } from "../../Function.js" +import * as Ref from "../../Ref.js" +import type * as SingleProducerAsyncInput from "../../SingleProducerAsyncInput.js" + +/** @internal */ +type State = + | Empty + | Emit + | Error + | Done<_Done> + +/** @internal */ +const OP_STATE_EMPTY = "Empty" as const + +/** @internal */ +type OP_STATE_EMPTY = typeof OP_STATE_EMPTY + +/** @internal */ +const OP_STATE_EMIT = "Emit" as const + +/** @internal */ +type OP_STATE_EMIT = typeof OP_STATE_EMIT + +/** @internal */ +const OP_STATE_ERROR = "Error" as const + +/** @internal */ +type OP_STATE_ERROR = typeof OP_STATE_ERROR + +/** @internal */ +const OP_STATE_DONE = "Done" as const + +/** @internal */ +type OP_STATE_DONE = typeof OP_STATE_DONE + +/** @internal */ +interface Empty { + readonly _tag: OP_STATE_EMPTY + readonly notifyProducer: Deferred.Deferred +} + +/** @internal */ +interface Emit { + readonly _tag: OP_STATE_EMIT + readonly notifyConsumers: ReadonlyArray, Err>> +} + +/** @internal */ +interface Error { + readonly _tag: OP_STATE_ERROR + readonly cause: Cause.Cause +} + +/** @internal */ +interface Done<_Done> { + readonly _tag: OP_STATE_DONE + readonly done: _Done +} + +/** @internal */ +const stateEmpty = (notifyProducer: Deferred.Deferred): State => ({ + _tag: OP_STATE_EMPTY, + notifyProducer +}) + +/** @internal */ +const stateEmit = ( + notifyConsumers: ReadonlyArray, Err>> +): State => ({ + _tag: OP_STATE_EMIT, + notifyConsumers +}) + +/** @internal */ +const stateError = (cause: Cause.Cause): State => ({ + _tag: OP_STATE_ERROR, + cause +}) + +/** @internal */ +const stateDone = (done: Done): State => ({ + _tag: OP_STATE_DONE, + done +}) + +/** @internal */ +class SingleProducerAsyncInputImpl + implements SingleProducerAsyncInput.SingleProducerAsyncInput +{ + constructor(readonly ref: Ref.Ref>) { + } + + awaitRead(): Effect.Effect { + return Effect.flatten( + Ref.modify(this.ref, (state) => + state._tag === OP_STATE_EMPTY ? + [Deferred.await(state.notifyProducer), state as State] : + [Effect.void, state]) + ) + } + + get close(): Effect.Effect { + return Effect.fiberIdWith((fiberId) => this.error(Cause.interrupt(fiberId))) + } + + done(value: Done): Effect.Effect { + return Effect.flatten( + Ref.modify(this.ref, (state) => { + switch (state._tag) { + case OP_STATE_EMPTY: { + return [Deferred.await(state.notifyProducer), state] + } + case OP_STATE_EMIT: { + return [ + Effect.forEach( + state.notifyConsumers, + (deferred) => Deferred.succeed(deferred, Either.left(value)), + { discard: true } + ), + stateDone(value) as State + ] + } + case OP_STATE_ERROR: { + return [Effect.interrupt, state] + } + case OP_STATE_DONE: { + return [Effect.interrupt, state] + } + } + }) + ) + } + + emit(element: Elem): Effect.Effect { + return Effect.flatMap(Deferred.make(), (deferred) => + Effect.flatten( + Ref.modify(this.ref, (state) => { + switch (state._tag) { + case OP_STATE_EMPTY: { + return [Deferred.await(state.notifyProducer), state] + } + case OP_STATE_EMIT: { + const notifyConsumer = state.notifyConsumers[0] + const notifyConsumers = state.notifyConsumers.slice(1) + if (notifyConsumer !== undefined) { + return [ + Deferred.succeed(notifyConsumer, Either.right(element)), + (notifyConsumers.length === 0 ? + stateEmpty(deferred) : + stateEmit(notifyConsumers)) as State + ] + } + throw new Error( + "Bug: Channel.SingleProducerAsyncInput.emit - Queue was empty! please report an issue at https://github.com/Effect-TS/effect/issues" + ) + } + case OP_STATE_ERROR: { + return [Effect.interrupt, state] + } + case OP_STATE_DONE: { + return [Effect.interrupt, state] + } + } + }) + )) + } + + error(cause: Cause.Cause): Effect.Effect { + return Effect.flatten( + Ref.modify(this.ref, (state) => { + switch (state._tag) { + case OP_STATE_EMPTY: { + return [Deferred.await(state.notifyProducer), state] + } + case OP_STATE_EMIT: { + return [ + Effect.forEach( + state.notifyConsumers, + (deferred) => Deferred.failCause(deferred, cause), + { discard: true } + ), + stateError(cause) as State + ] + } + case OP_STATE_ERROR: { + return [Effect.interrupt, state] + } + case OP_STATE_DONE: { + return [Effect.interrupt, state] + } + } + }) + ) + } + + get take(): Effect.Effect>> { + return this.takeWith( + (cause) => Exit.failCause(Cause.map(cause, Either.left)), + (elem) => Exit.succeed(elem) as Exit.Exit>, + (done) => Exit.fail(Either.right(done)) + ) + } + + takeWith( + onError: (cause: Cause.Cause) => A, + onElement: (element: Elem) => A, + onDone: (value: Done) => A + ): Effect.Effect { + return Effect.flatMap(Deferred.make, Err>(), (deferred) => + Effect.flatten( + Ref.modify(this.ref, (state) => { + switch (state._tag) { + case OP_STATE_EMPTY: { + return [ + Effect.zipRight( + Deferred.succeed(state.notifyProducer, void 0), + Effect.matchCause(Deferred.await(deferred), { + onFailure: onError, + onSuccess: Either.match({ onLeft: onDone, onRight: onElement }) + }) + ), + stateEmit([deferred]) + ] + } + case OP_STATE_EMIT: { + return [ + Effect.matchCause(Deferred.await(deferred), { + onFailure: onError, + onSuccess: Either.match({ onLeft: onDone, onRight: onElement }) + }), + stateEmit([...state.notifyConsumers, deferred]) + ] + } + case OP_STATE_ERROR: { + return [Effect.succeed(onError(state.cause)), state] + } + case OP_STATE_DONE: { + return [Effect.succeed(onDone(state.done)), state] + } + } + }) + )) + } +} + +/** @internal */ +export const make = (): Effect.Effect< + SingleProducerAsyncInput.SingleProducerAsyncInput +> => + pipe( + Deferred.make(), + Effect.flatMap((deferred) => Ref.make(stateEmpty(deferred) as State)), + Effect.map((ref) => new SingleProducerAsyncInputImpl(ref)) + ) diff --git a/repos/effect/packages/effect/src/internal/channel/subexecutor.ts b/repos/effect/packages/effect/src/internal/channel/subexecutor.ts new file mode 100644 index 0000000..4cb1d9a --- /dev/null +++ b/repos/effect/packages/effect/src/internal/channel/subexecutor.ts @@ -0,0 +1,229 @@ +import type * as ChildExecutorDecision from "../../ChildExecutorDecision.js" +import * as Effect from "../../Effect.js" +import * as Exit from "../../Exit.js" +import { pipe } from "../../Function.js" +import type * as UpstreamPullRequest from "../../UpstreamPullRequest.js" +import type * as UpstreamPullStrategy from "../../UpstreamPullStrategy.js" +import type { ErasedChannel, ErasedExecutor } from "./channelExecutor.js" + +/** @internal */ +export interface Subexecutor { + close(exit: Exit.Exit): Effect.Effect | undefined + enqueuePullFromChild(child: PullFromChild): Subexecutor +} + +/** @internal */ +export type Primitive = PullFromChild | PullFromUpstream | DrainChildExecutors | Emit + +/** @internal */ +export const OP_PULL_FROM_CHILD = "PullFromChild" as const + +/** @internal */ +export type OP_PULL_FROM_CHILD = typeof OP_PULL_FROM_CHILD + +/** @internal */ +export const OP_PULL_FROM_UPSTREAM = "PullFromUpstream" as const + +/** @internal */ +export type OP_PULL_FROM_UPSTREAM = typeof OP_PULL_FROM_UPSTREAM + +/** @internal */ +export const OP_DRAIN_CHILD_EXECUTORS = "DrainChildExecutors" as const + +/** @internal */ +export type OP_DRAIN_CHILD_EXECUTORS = typeof OP_DRAIN_CHILD_EXECUTORS + +/** @internal */ +export const OP_EMIT = "Emit" as const + +/** @internal */ +export type OP_EMIT = typeof OP_EMIT + +/** + * Execute the `childExecutor` and on each emitted value, decide what to do by + * `onEmit`. + * + * @internal + */ +export class PullFromChild implements Subexecutor { + readonly _tag: OP_PULL_FROM_CHILD = OP_PULL_FROM_CHILD + + constructor( + readonly childExecutor: ErasedExecutor, + readonly parentSubexecutor: Subexecutor, + readonly onEmit: (value: unknown) => ChildExecutorDecision.ChildExecutorDecision + ) { + } + + close(exit: Exit.Exit): Effect.Effect | undefined { + const fin1 = this.childExecutor.close(exit) + const fin2 = this.parentSubexecutor.close(exit) + if (fin1 !== undefined && fin2 !== undefined) { + return Effect.zipWith( + Effect.exit(fin1), + Effect.exit(fin2), + (exit1, exit2) => pipe(exit1, Exit.zipRight(exit2)) + ) + } else if (fin1 !== undefined) { + return fin1 + } else if (fin2 !== undefined) { + return fin2 + } else { + return undefined + } + } + + enqueuePullFromChild(_child: PullFromChild): Subexecutor { + return this + } +} + +/** + * Execute `upstreamExecutor` and for each emitted element, spawn a child + * channel and continue with processing it by `PullFromChild`. + * + * @internal + */ +export class PullFromUpstream implements Subexecutor { + readonly _tag: OP_PULL_FROM_UPSTREAM = OP_PULL_FROM_UPSTREAM + + constructor( + readonly upstreamExecutor: ErasedExecutor, + readonly createChild: (value: unknown) => ErasedChannel, + readonly lastDone: unknown, + readonly activeChildExecutors: ReadonlyArray | undefined>, + readonly combineChildResults: (x: unknown, y: unknown) => unknown, + readonly combineWithChildResult: (x: unknown, y: unknown) => unknown, + readonly onPull: ( + request: UpstreamPullRequest.UpstreamPullRequest + ) => UpstreamPullStrategy.UpstreamPullStrategy, + readonly onEmit: (value: unknown) => ChildExecutorDecision.ChildExecutorDecision + ) { + } + + close(exit: Exit.Exit): Effect.Effect | undefined { + const fin1 = this.upstreamExecutor.close(exit) + const fins = [ + ...this.activeChildExecutors.map((child) => + child !== undefined ? + child.childExecutor.close(exit) : + undefined + ), + fin1 + ] + const result = fins.reduce( + (acc: Effect.Effect, never, R> | undefined, next) => { + if (acc !== undefined && next !== undefined) { + return Effect.zipWith( + acc, + Effect.exit(next), + (exit1, exit2) => Exit.zipRight(exit1, exit2) + ) + } else if (acc !== undefined) { + return acc + } else if (next !== undefined) { + return Effect.exit(next) + } else { + return undefined + } + }, + undefined + ) + return result === undefined ? result : result + } + + enqueuePullFromChild(child: PullFromChild): Subexecutor { + return new PullFromUpstream( + this.upstreamExecutor, + this.createChild, + this.lastDone, + [...this.activeChildExecutors, child], + this.combineChildResults, + this.combineWithChildResult, + this.onPull, + this.onEmit + ) + } +} + +/** + * Transformed from `PullFromUpstream` when upstream has finished but there + * are still active child executors. + * + * @internal + */ +export class DrainChildExecutors implements Subexecutor { + readonly _tag: OP_DRAIN_CHILD_EXECUTORS = OP_DRAIN_CHILD_EXECUTORS + + constructor( + readonly upstreamExecutor: ErasedExecutor, + readonly lastDone: unknown, + readonly activeChildExecutors: ReadonlyArray | undefined>, + readonly upstreamDone: Exit.Exit, + readonly combineChildResults: (x: unknown, y: unknown) => unknown, + readonly combineWithChildResult: (x: unknown, y: unknown) => unknown, + readonly onPull: ( + request: UpstreamPullRequest.UpstreamPullRequest + ) => UpstreamPullStrategy.UpstreamPullStrategy + ) { + } + + close(exit: Exit.Exit): Effect.Effect | undefined { + const fin1 = this.upstreamExecutor.close(exit) + const fins = [ + ...this.activeChildExecutors.map((child) => (child !== undefined ? + child.childExecutor.close(exit) : + undefined) + ), + fin1 + ] + const result = fins.reduce( + (acc: Effect.Effect, never, R> | undefined, next) => { + if (acc !== undefined && next !== undefined) { + return Effect.zipWith( + acc, + Effect.exit(next), + (exit1, exit2) => Exit.zipRight(exit1, exit2) + ) + } else if (acc !== undefined) { + return acc + } else if (next !== undefined) { + return Effect.exit(next) + } else { + return undefined + } + }, + undefined + ) + return result === undefined ? result : result + } + + enqueuePullFromChild(child: PullFromChild): Subexecutor { + return new DrainChildExecutors( + this.upstreamExecutor, + this.lastDone, + [...this.activeChildExecutors, child], + this.upstreamDone, + this.combineChildResults, + this.combineWithChildResult, + this.onPull + ) + } +} + +/** @internal */ +export class Emit implements Subexecutor { + readonly _tag: OP_EMIT = OP_EMIT + + constructor(readonly value: unknown, readonly next: Subexecutor) { + } + + close(exit: Exit.Exit): Effect.Effect | undefined { + const result = this.next.close(exit) + return result === undefined ? result : result + } + + enqueuePullFromChild(_child: PullFromChild): Subexecutor { + return this + } +} diff --git a/repos/effect/packages/effect/src/internal/channel/upstreamPullRequest.ts b/repos/effect/packages/effect/src/internal/channel/upstreamPullRequest.ts new file mode 100644 index 0000000..5f800b2 --- /dev/null +++ b/repos/effect/packages/effect/src/internal/channel/upstreamPullRequest.ts @@ -0,0 +1,84 @@ +import { dual } from "../../Function.js" +import { hasProperty } from "../../Predicate.js" +import type * as UpstreamPullRequest from "../../UpstreamPullRequest.js" +import * as OpCodes from "../opCodes/channelUpstreamPullRequest.js" + +/** @internal */ +const UpstreamPullRequestSymbolKey = "effect/ChannelUpstreamPullRequest" + +/** @internal */ +export const UpstreamPullRequestTypeId: UpstreamPullRequest.UpstreamPullRequestTypeId = Symbol.for( + UpstreamPullRequestSymbolKey +) as UpstreamPullRequest.UpstreamPullRequestTypeId + +const upstreamPullRequestVariance = { + /* c8 ignore next */ + _A: (_: never) => _ +} + +/** @internal */ +const proto = { + [UpstreamPullRequestTypeId]: upstreamPullRequestVariance +} + +/** @internal */ +export const Pulled = (value: A): UpstreamPullRequest.UpstreamPullRequest => { + const op = Object.create(proto) + op._tag = OpCodes.OP_PULLED + op.value = value + return op +} + +/** @internal */ +export const NoUpstream = (activeDownstreamCount: number): UpstreamPullRequest.UpstreamPullRequest => { + const op = Object.create(proto) + op._tag = OpCodes.OP_NO_UPSTREAM + op.activeDownstreamCount = activeDownstreamCount + return op +} + +/** @internal */ +export const isUpstreamPullRequest = (u: unknown): u is UpstreamPullRequest.UpstreamPullRequest => + hasProperty(u, UpstreamPullRequestTypeId) + +/** @internal */ +export const isPulled = ( + self: UpstreamPullRequest.UpstreamPullRequest +): self is UpstreamPullRequest.Pulled => self._tag === OpCodes.OP_PULLED + +/** @internal */ +export const isNoUpstream = ( + self: UpstreamPullRequest.UpstreamPullRequest +): self is UpstreamPullRequest.NoUpstream => self._tag === OpCodes.OP_NO_UPSTREAM + +/** @internal */ +export const match = dual< + ( + options: { + readonly onPulled: (value: A) => Z + readonly onNoUpstream: (activeDownstreamCount: number) => Z + } + ) => (self: UpstreamPullRequest.UpstreamPullRequest) => Z, + ( + self: UpstreamPullRequest.UpstreamPullRequest, + options: { + readonly onPulled: (value: A) => Z + readonly onNoUpstream: (activeDownstreamCount: number) => Z + } + ) => Z +>(2, ( + self: UpstreamPullRequest.UpstreamPullRequest, + { onNoUpstream, onPulled }: { + readonly onPulled: (value: A) => Z + readonly onNoUpstream: (activeDownstreamCount: number) => Z + } +): Z => { + switch (self._tag) { + case OpCodes.OP_PULLED: { + return onPulled(self.value) + } + case OpCodes.OP_NO_UPSTREAM: { + return onNoUpstream(self.activeDownstreamCount) + } + } +}) diff --git a/repos/effect/packages/effect/src/internal/channel/upstreamPullStrategy.ts b/repos/effect/packages/effect/src/internal/channel/upstreamPullStrategy.ts new file mode 100644 index 0000000..b250f7c --- /dev/null +++ b/repos/effect/packages/effect/src/internal/channel/upstreamPullStrategy.ts @@ -0,0 +1,87 @@ +import { dual } from "../../Function.js" +import type * as Option from "../../Option.js" +import { hasProperty } from "../../Predicate.js" +import type * as UpstreamPullStrategy from "../../UpstreamPullStrategy.js" +import * as OpCodes from "../opCodes/channelUpstreamPullStrategy.js" + +/** @internal */ +const UpstreamPullStrategySymbolKey = "effect/ChannelUpstreamPullStrategy" + +/** @internal */ +export const UpstreamPullStrategyTypeId: UpstreamPullStrategy.UpstreamPullStrategyTypeId = Symbol.for( + UpstreamPullStrategySymbolKey +) as UpstreamPullStrategy.UpstreamPullStrategyTypeId + +const upstreamPullStrategyVariance = { + /* c8 ignore next */ + _A: (_: never) => _ +} + +/** @internal */ +const proto = { + [UpstreamPullStrategyTypeId]: upstreamPullStrategyVariance +} + +/** @internal */ +export const PullAfterNext = (emitSeparator: Option.Option): UpstreamPullStrategy.UpstreamPullStrategy => { + const op = Object.create(proto) + op._tag = OpCodes.OP_PULL_AFTER_NEXT + op.emitSeparator = emitSeparator + return op +} + +/** @internal */ +export const PullAfterAllEnqueued = ( + emitSeparator: Option.Option +): UpstreamPullStrategy.UpstreamPullStrategy => { + const op = Object.create(proto) + op._tag = OpCodes.OP_PULL_AFTER_ALL_ENQUEUED + op.emitSeparator = emitSeparator + return op +} + +/** @internal */ +export const isUpstreamPullStrategy = (u: unknown): u is UpstreamPullStrategy.UpstreamPullStrategy => + hasProperty(u, UpstreamPullStrategyTypeId) + +/** @internal */ +export const isPullAfterNext = ( + self: UpstreamPullStrategy.UpstreamPullStrategy +): self is UpstreamPullStrategy.PullAfterNext => self._tag === OpCodes.OP_PULL_AFTER_NEXT + +/** @internal */ +export const isPullAfterAllEnqueued = ( + self: UpstreamPullStrategy.UpstreamPullStrategy +): self is UpstreamPullStrategy.PullAfterAllEnqueued => self._tag === OpCodes.OP_PULL_AFTER_ALL_ENQUEUED + +/** @internal */ +export const match = dual< + ( + options: { + readonly onNext: (emitSeparator: Option.Option) => Z + readonly onAllEnqueued: (emitSeparator: Option.Option) => Z + } + ) => (self: UpstreamPullStrategy.UpstreamPullStrategy) => Z, + ( + self: UpstreamPullStrategy.UpstreamPullStrategy, + options: { + readonly onNext: (emitSeparator: Option.Option) => Z + readonly onAllEnqueued: (emitSeparator: Option.Option) => Z + } + ) => Z +>(2, ( + self: UpstreamPullStrategy.UpstreamPullStrategy, + { onAllEnqueued, onNext }: { + readonly onNext: (emitSeparator: Option.Option) => Z + readonly onAllEnqueued: (emitSeparator: Option.Option) => Z + } +): Z => { + switch (self._tag) { + case OpCodes.OP_PULL_AFTER_NEXT: { + return onNext(self.emitSeparator) + } + case OpCodes.OP_PULL_AFTER_ALL_ENQUEUED: { + return onAllEnqueued(self.emitSeparator) + } + } +}) diff --git a/repos/effect/packages/effect/src/internal/clock.ts b/repos/effect/packages/effect/src/internal/clock.ts new file mode 100644 index 0000000..30fc73b --- /dev/null +++ b/repos/effect/packages/effect/src/internal/clock.ts @@ -0,0 +1,95 @@ +import type * as Clock from "../Clock.js" +import * as Context from "../Context.js" +import * as Duration from "../Duration.js" +import type * as Effect from "../Effect.js" +import { constFalse } from "../Function.js" +import * as core from "./core.js" + +/** @internal */ +const ClockSymbolKey = "effect/Clock" + +/** @internal */ +export const ClockTypeId: Clock.ClockTypeId = Symbol.for(ClockSymbolKey) as Clock.ClockTypeId + +/** @internal */ +export const clockTag: Context.Tag = Context.GenericTag("effect/Clock") + +/** @internal */ +export const MAX_TIMER_MILLIS = 2 ** 31 - 1 + +/** @internal */ +export const globalClockScheduler: Clock.ClockScheduler = { + unsafeSchedule(task: Clock.Task, duration: Duration.Duration): Clock.CancelToken { + const millis = Duration.toMillis(duration) + // If the duration is greater than the value allowable by the JS timer + // functions, treat the value as an infinite duration + if (millis > MAX_TIMER_MILLIS) { + return constFalse + } + let completed = false + const handle = setTimeout(() => { + completed = true + task() + }, millis) + return () => { + clearTimeout(handle) + return !completed + } + } +} + +const performanceNowNanos = (function() { + const bigint1e6 = BigInt(1_000_000) + if (typeof performance === "undefined" || typeof performance.now !== "function") { + return () => BigInt(Date.now()) * bigint1e6 + } + let origin: bigint + return () => { + if (origin === undefined) { + origin = (BigInt(Date.now()) * bigint1e6) - BigInt(Math.round(performance.now() * 1_000_000)) + } + return origin + BigInt(Math.round(performance.now() * 1000000)) + } +})() +const processOrPerformanceNow = (function() { + const processHrtime = + typeof process === "object" && "hrtime" in process && typeof process.hrtime.bigint === "function" ? + process.hrtime : + undefined + if (!processHrtime) { + return performanceNowNanos + } + const origin = performanceNowNanos() - processHrtime.bigint() + return () => origin + processHrtime.bigint() +})() + +/** @internal */ +class ClockImpl implements Clock.Clock { + readonly [ClockTypeId]: Clock.ClockTypeId = ClockTypeId + + unsafeCurrentTimeMillis(): number { + return Date.now() + } + + unsafeCurrentTimeNanos(): bigint { + return processOrPerformanceNow() + } + + currentTimeMillis: Effect.Effect = core.sync(() => this.unsafeCurrentTimeMillis()) + + currentTimeNanos: Effect.Effect = core.sync(() => this.unsafeCurrentTimeNanos()) + + scheduler(): Effect.Effect { + return core.succeed(globalClockScheduler) + } + + sleep(duration: Duration.Duration): Effect.Effect { + return core.async((resume) => { + const canceler = globalClockScheduler.unsafeSchedule(() => resume(core.void), duration) + return core.asVoid(core.sync(canceler)) + }) + } +} + +/** @internal */ +export const make = (): Clock.Clock => new ClockImpl() diff --git a/repos/effect/packages/effect/src/internal/completedRequestMap.ts b/repos/effect/packages/effect/src/internal/completedRequestMap.ts new file mode 100644 index 0000000..76cd0bf --- /dev/null +++ b/repos/effect/packages/effect/src/internal/completedRequestMap.ts @@ -0,0 +1,9 @@ +import { globalValue } from "../GlobalValue.js" +import type * as Request from "../Request.js" +import { fiberRefUnsafeMake } from "./core.js" + +/** @internal */ +export const currentRequestMap = globalValue( + Symbol.for("effect/FiberRef/currentRequestMap"), + () => fiberRefUnsafeMake(new Map>()) +) diff --git a/repos/effect/packages/effect/src/internal/concurrency.ts b/repos/effect/packages/effect/src/internal/concurrency.ts new file mode 100644 index 0000000..61ca254 --- /dev/null +++ b/repos/effect/packages/effect/src/internal/concurrency.ts @@ -0,0 +1,54 @@ +import type { Effect } from "../Effect.js" +import type { Concurrency } from "../Types.js" +import * as core from "./core.js" + +/** @internal */ +export const match = ( + concurrency: Concurrency | undefined, + sequential: () => Effect, + unbounded: () => Effect, + bounded: (limit: number) => Effect +): Effect => { + switch (concurrency) { + case undefined: + return sequential() + case "unbounded": + return unbounded() + case "inherit": + return core.fiberRefGetWith( + core.currentConcurrency, + (concurrency) => + concurrency === "unbounded" ? + unbounded() : + concurrency > 1 ? + bounded(concurrency) : + sequential() + ) + default: + return concurrency > 1 ? bounded(concurrency) : sequential() + } +} + +/** @internal */ +export const matchSimple = ( + concurrency: Concurrency | undefined, + sequential: () => Effect, + concurrent: () => Effect +): Effect => { + switch (concurrency) { + case undefined: + return sequential() + case "unbounded": + return concurrent() + case "inherit": + return core.fiberRefGetWith( + core.currentConcurrency, + (concurrency) => + concurrency === "unbounded" || concurrency > 1 ? + concurrent() : + sequential() + ) + default: + return concurrency > 1 ? concurrent() : sequential() + } +} diff --git a/repos/effect/packages/effect/src/internal/config.ts b/repos/effect/packages/effect/src/internal/config.ts new file mode 100644 index 0000000..0ac67dc --- /dev/null +++ b/repos/effect/packages/effect/src/internal/config.ts @@ -0,0 +1,716 @@ +import type * as Brand from "../Brand.js" +import * as Chunk from "../Chunk.js" +import type * as Config from "../Config.js" +import * as ConfigError from "../ConfigError.js" +import * as Duration from "../Duration.js" +import * as Either from "../Either.js" +import type { LazyArg } from "../Function.js" +import { constTrue, dual, pipe } from "../Function.js" +import type * as HashMap from "../HashMap.js" +import * as HashSet from "../HashSet.js" +import { formatUnknown } from "../Inspectable.js" +import type * as LogLevel from "../LogLevel.js" +import * as Option from "../Option.js" +import { hasProperty, type Predicate, type Refinement } from "../Predicate.js" +import type * as Redacted from "../Redacted.js" +import type * as Secret from "../Secret.js" +import * as configError from "./configError.js" +import * as core from "./core.js" +import * as defaultServices from "./defaultServices.js" +import * as effectable from "./effectable.js" +import * as OpCodes from "./opCodes/config.js" +import * as redacted_ from "./redacted.js" +import * as InternalSecret from "./secret.js" + +const ConfigSymbolKey = "effect/Config" + +/** @internal */ +export const ConfigTypeId: Config.ConfigTypeId = Symbol.for( + ConfigSymbolKey +) as Config.ConfigTypeId + +/** @internal */ +export type ConfigPrimitive = + | Constant + | Described + | Fallback + | Fail + | Lazy + | MapOrFail + | Nested + | Primitive + | Sequence + | Table + | Zipped + +const configVariance = { + /* c8 ignore next */ + _A: (_: never) => _ +} + +const proto = { + ...effectable.CommitPrototype, + [ConfigTypeId]: configVariance, + commit(this: Config.Config) { + return defaultServices.config(this) + } +} + +/** @internal */ +export type Op = Config.Config & Body & { + readonly _tag: Tag +} + +/** @internal */ +export interface Constant extends + Op + }> +{} + +/** @internal */ +export interface Described extends + Op + readonly description: string + }> +{} + +/** @internal */ +export interface Fallback extends + Op + readonly second: Config.Config + readonly condition: Predicate + }> +{} + +/** @internal */ +export interface Fail extends + Op + }> +{} + +/** @internal */ +export interface Lazy extends + Op + }> +{} + +/** @internal */ +export interface MapOrFail extends + Op + mapOrFail(value: unknown): Either.Either + }> +{} + +/** @internal */ +export interface Nested extends + Op + }> +{} + +/** @internal */ +export interface Primitive extends + Op + }> +{} + +/** @internal */ +export interface Sequence extends + Op + }> +{} + +/** @internal */ +export interface Table extends + Op + }> +{} + +/** @internal */ +export interface Zipped extends + Op + readonly right: Config.Config + zip(a: unknown, b: unknown): unknown + }> +{} + +/** @internal */ +export const boolean = (name?: string): Config.Config => { + const config = primitive( + "a boolean property", + (text) => { + switch (text) { + case "true": + case "yes": + case "on": + case "1": { + return Either.right(true) + } + case "false": + case "no": + case "off": + case "0": { + return Either.right(false) + } + default: { + const error = configError.InvalidData( + [], + `Expected a boolean value but received ${formatUnknown(text)}` + ) + return Either.left(error) + } + } + } + ) + return name === undefined ? config : nested(config, name) +} + +/** @internal */ +export const url = (name?: string): Config.Config => { + const config = primitive( + "an URL property", + (text) => + Either.try({ + try: () => new URL(text), + catch: (_) => configError.InvalidData([], `Expected an URL value but received ${formatUnknown(text)}`) + }) + ) + return name === undefined ? config : nested(config, name) +} + +/** @internal */ +export const port = (name?: string): Config.Config => { + const config = primitive( + "a network port property", + (text) => { + const result = Number(text) + + if ( + Number.isNaN(result) || + result.toString() !== text.toString() || + !Number.isInteger(result) || + result < 1 || + result > 65535 + ) { + return Either.left( + configError.InvalidData( + [], + `Expected a network port value but received ${formatUnknown(text)}` + ) + ) + } + return Either.right(result) + } + ) + return name === undefined ? config : nested(config, name) +} + +/** @internal */ +export const array = (config: Config.Config, name?: string): Config.Config> => { + return pipe(chunk(config, name), map(Chunk.toArray)) +} + +/** @internal */ +export const chunk = (config: Config.Config, name?: string): Config.Config> => { + return map(name === undefined ? repeat(config) : nested(repeat(config), name), Chunk.unsafeFromArray) +} + +/** @internal */ +export const date = (name?: string): Config.Config => { + const config = primitive( + "a date property", + (text) => { + const result = Date.parse(text) + if (Number.isNaN(result)) { + return Either.left( + configError.InvalidData( + [], + `Expected a Date value but received ${formatUnknown(text)}` + ) + ) + } + return Either.right(new Date(result)) + } + ) + return name === undefined ? config : nested(config, name) +} + +/** @internal */ +export const fail = (message: string): Config.Config => { + const fail = Object.create(proto) + fail._tag = OpCodes.OP_FAIL + fail.message = message + fail.parse = () => Either.left(configError.Unsupported([], message)) + return fail +} + +/** @internal */ +export const number = (name?: string): Config.Config => { + const config = primitive( + "a number property", + (text) => { + const result = Number(text) + if (Number.isNaN(result)) { + return Either.left( + configError.InvalidData( + [], + `Expected a number value but received ${formatUnknown(text)}` + ) + ) + } + return Either.right(result) + } + ) + return name === undefined ? config : nested(config, name) +} + +/** @internal */ +export const integer = (name?: string): Config.Config => { + const config = primitive( + "an integer property", + (text) => { + const result = Number(text) + if (!Number.isInteger(result)) { + return Either.left( + configError.InvalidData( + [], + `Expected an integer value but received ${formatUnknown(text)}` + ) + ) + } + return Either.right(result) + } + ) + return name === undefined ? config : nested(config, name) +} + +/** @internal */ +export const literal = >(...literals: Literals) => +( + name?: string +): Config.Config => { + const valuesString = literals.map(String).join(", ") + const config = primitive(`one of (${valuesString})`, (text) => { + const found = literals.find((value) => String(value) === text) + if (found === undefined) { + return Either.left( + configError.InvalidData( + [], + `Expected one of (${valuesString}) but received ${formatUnknown(text)}` + ) + ) + } + return Either.right(found) + }) + return name === undefined ? config : nested(config, name) +} + +/** @internal */ +export const logLevel = (name?: string): Config.Config => { + const config = mapOrFail(string(), (value) => { + const label = value.toUpperCase() + const level = core.allLogLevels.find((level) => level.label === label) + return level === undefined + ? Either.left( + configError.InvalidData([], `Expected a log level but received ${formatUnknown(value)}`) + ) + : Either.right(level) + }) + return name === undefined ? config : nested(config, name) +} + +/** @internal */ +export const duration = (name?: string): Config.Config => { + const config = mapOrFail(string(), (value) => { + const duration = Duration.decodeUnknown(value) + return Either.fromOption( + duration, + () => configError.InvalidData([], `Expected a duration but received ${formatUnknown(value)}`) + ) + }) + return name === undefined ? config : nested(config, name) +} + +/** @internal */ +export const map = dual< + (f: (a: A) => B) => (self: Config.Config) => Config.Config, + (self: Config.Config, f: (a: A) => B) => Config.Config +>(2, (self, f) => mapOrFail(self, (a) => Either.right(f(a)))) + +/** @internal */ +export const mapAttempt = dual< + (f: (a: A) => B) => (self: Config.Config) => Config.Config, + (self: Config.Config, f: (a: A) => B) => Config.Config +>(2, (self, f) => + mapOrFail(self, (a) => { + try { + return Either.right(f(a)) + } catch (error) { + return Either.left( + configError.InvalidData( + [], + error instanceof Error ? error.message : `${error}` + ) + ) + } + })) + +/** @internal */ +export const mapOrFail = dual< + (f: (a: A) => Either.Either) => (self: Config.Config) => Config.Config, + (self: Config.Config, f: (a: A) => Either.Either) => Config.Config +>(2, (self, f) => { + const mapOrFail = Object.create(proto) + mapOrFail._tag = OpCodes.OP_MAP_OR_FAIL + mapOrFail.original = self + mapOrFail.mapOrFail = f + return mapOrFail +}) + +/** @internal */ +export const nested = dual< + (name: string) => (self: Config.Config) => Config.Config, + (self: Config.Config, name: string) => Config.Config +>(2, (self, name) => { + const nested = Object.create(proto) + nested._tag = OpCodes.OP_NESTED + nested.name = name + nested.config = self + return nested +}) + +/** @internal */ +export const orElse = dual< + (that: LazyArg>) => (self: Config.Config) => Config.Config, + (self: Config.Config, that: LazyArg>) => Config.Config +>(2, (self, that) => { + const fallback = Object.create(proto) + fallback._tag = OpCodes.OP_FALLBACK + fallback.first = self + fallback.second = suspend(that) + fallback.condition = constTrue + return fallback +}) + +/** @internal */ +export const orElseIf = dual< + ( + options: { + readonly if: Predicate + readonly orElse: LazyArg> + } + ) => (self: Config.Config) => Config.Config, + ( + self: Config.Config, + options: { + readonly if: Predicate + readonly orElse: LazyArg> + } + ) => Config.Config +>(2, (self, options) => { + const fallback = Object.create(proto) + fallback._tag = OpCodes.OP_FALLBACK + fallback.first = self + fallback.second = suspend(options.orElse) + fallback.condition = options.if + return fallback +}) + +/** @internal */ +export const option = (self: Config.Config): Config.Config> => { + return pipe( + self, + map(Option.some), + orElseIf({ orElse: () => succeed(Option.none()), if: ConfigError.isMissingDataOnly }) + ) +} + +/** @internal */ +export const primitive = ( + description: string, + parse: (text: string) => Either.Either +): Config.Config => { + const primitive = Object.create(proto) + primitive._tag = OpCodes.OP_PRIMITIVE + primitive.description = description + primitive.parse = parse + return primitive +} + +/** @internal */ +export const repeat = (self: Config.Config): Config.Config> => { + const repeat = Object.create(proto) + repeat._tag = OpCodes.OP_SEQUENCE + repeat.config = self + return repeat +} + +/** @internal */ +export const secret = (name?: string): Config.Config => { + const config = primitive( + "a secret property", + (text) => Either.right(InternalSecret.fromString(text)) + ) + return name === undefined ? config : nested(config, name) +} + +/** @internal */ +export const redacted = ( + nameOrConfig?: string | Config.Config +): Config.Config> => { + const config: Config.Config = isConfig(nameOrConfig) ? nameOrConfig : string(nameOrConfig) + return map(config, redacted_.make) +} + +/** @internal */ +export const branded: { + >( + constructor: Brand.Brand.Constructor + ): (config: Config.Config) => Config.Config + >( + name: string | undefined, + constructor: Brand.Brand.Constructor + ): Config.Config + >( + config: Config.Config, + constructor: Brand.Brand.Constructor + ): Config.Config +} = dual(2, >( + nameOrConfig: Config.Config> | string | undefined, + constructor: B +) => { + const config: Config.Config = isConfig(nameOrConfig) ? nameOrConfig : string(nameOrConfig) + + return mapOrFail(config, (a) => + constructor.either(a).pipe( + Either.mapLeft((brandErrors) => + configError.InvalidData([], brandErrors.map((brandError) => brandError.message).join("\n")) + ) + )) +}) + +/** @internal */ +export const hashSet = (config: Config.Config, name?: string): Config.Config> => { + const newConfig = map(chunk(config), HashSet.fromIterable) + return name === undefined ? newConfig : nested(newConfig, name) +} + +/** @internal */ +export const string = (name?: string): Config.Config => { + const config = primitive( + "a text property", + Either.right + ) + return name === undefined ? config : nested(config, name) +} + +/** @internal */ +export const nonEmptyString = (name?: string): Config.Config => { + const config = primitive( + "a non-empty text property", + Either.liftPredicate((text) => text.length > 0, () => configError.MissingData([], "Expected a non-empty string")) + ) + + return name === undefined ? config : nested(config, name) +} + +/** @internal */ +export const all = > | Record>>( + arg: Arg +): Config.Config< + [Arg] extends [ReadonlyArray>] ? { + -readonly [K in keyof Arg]: [Arg[K]] extends [Config.Config] ? A : never + } + : [Arg] extends [Iterable>] ? Array + : [Arg] extends [Record>] ? { + -readonly [K in keyof Arg]: [Arg[K]] extends [Config.Config] ? A : never + } + : never +> => { + if (Array.isArray(arg)) { + return tuple(arg) as any + } else if (Symbol.iterator in arg) { + return tuple([...(arg as Iterable>)]) as any + } + return struct(arg) as any +} + +const struct = >>(r: NER): Config.Config< + { + [K in keyof NER]: [NER[K]] extends [{ [ConfigTypeId]: { _A: (_: never) => infer A } }] ? A : never + } +> => { + const entries = Object.entries(r) + let result = pipe(entries[0][1], map((value) => ({ [entries[0][0]]: value }))) + if (entries.length === 1) { + return result as any + } + const rest = entries.slice(1) + for (const [key, config] of rest) { + result = pipe( + result, + zipWith(config, (record, value) => ({ ...record, [key]: value })) + ) + } + return result as any +} + +/** @internal */ +export const succeed = (value: A): Config.Config => { + const constant = Object.create(proto) + constant._tag = OpCodes.OP_CONSTANT + constant.value = value + constant.parse = () => Either.right(value) + return constant +} + +/** @internal */ +export const suspend = (config: LazyArg>): Config.Config => { + const lazy = Object.create(proto) + lazy._tag = OpCodes.OP_LAZY + lazy.config = config + return lazy +} + +/** @internal */ +export const sync = (value: LazyArg): Config.Config => { + return suspend(() => succeed(value())) +} + +/** @internal */ +export const hashMap = (config: Config.Config, name?: string): Config.Config> => { + const table = Object.create(proto) + table._tag = OpCodes.OP_HASHMAP + table.valueConfig = config + return name === undefined ? table : nested(table, name) +} + +/** @internal */ +export const isConfig = (u: unknown): u is Config.Config => hasProperty(u, ConfigTypeId) + +/** @internal */ +const tuple = >>(tuple: T): Config.Config< + { + [K in keyof T]: [T[K]] extends [Config.Config] ? A : never + } +> => { + if (tuple.length === 0) { + return succeed([]) as any + } + if (tuple.length === 1) { + return map(tuple[0], (x) => [x]) as any + } + let result = map(tuple[0], (x) => [x]) + for (let i = 1; i < tuple.length; i++) { + const config = tuple[i] + result = pipe( + result, + zipWith(config, (tuple, value) => [...tuple, value]) + ) as any + } + return result as any +} + +/** + * @internal + */ +export const unwrap = (wrapped: Config.Config.Wrap): Config.Config => { + if (isConfig(wrapped)) { + return wrapped + } + return struct( + Object.fromEntries( + Object.entries(wrapped).map(([k, a]) => [k, unwrap(a as any)]) + ) + ) as any +} + +/** @internal */ +export const validate = dual< + { + (options: { + readonly message: string + readonly validation: Refinement + }): (self: Config.Config) => Config.Config + (options: { + readonly message: string + readonly validation: Predicate + }): (self: Config.Config) => Config.Config + }, + { + (self: Config.Config, options: { + readonly message: string + readonly validation: Refinement + }): Config.Config + (self: Config.Config, options: { + readonly message: string + readonly validation: Predicate + }): Config.Config + } +>(2, (self: Config.Config, { message, validation }: { + readonly message: string + readonly validation: Predicate +}) => + mapOrFail(self, (a) => { + if (validation(a)) { + return Either.right(a) + } + return Either.left(configError.InvalidData([], message)) + })) + +/** @internal */ +export const withDefault = dual< + (def: A2) => (self: Config.Config) => Config.Config, + (self: Config.Config, def: A2) => Config.Config +>(2, (self, def) => + orElseIf(self, { + orElse: () => succeed(def), + if: ConfigError.isMissingDataOnly + })) + +/** @internal */ +export const withDescription = dual< + (description: string) => (self: Config.Config) => Config.Config, + (self: Config.Config, description: string) => Config.Config +>(2, (self, description) => { + const described = Object.create(proto) + described._tag = OpCodes.OP_DESCRIBED + described.config = self + described.description = description + return described +}) + +/** @internal */ +export const zip = dual< + (that: Config.Config) => (self: Config.Config) => Config.Config<[A, B]>, + (self: Config.Config, that: Config.Config) => Config.Config<[A, B]> +>(2, (self, that) => zipWith(self, that, (a, b) => [a, b])) + +/** @internal */ +export const zipWith = dual< + (that: Config.Config, f: (a: A, b: B) => C) => (self: Config.Config) => Config.Config, + (self: Config.Config, that: Config.Config, f: (a: A, b: B) => C) => Config.Config +>(3, (self, that, f) => { + const zipWith = Object.create(proto) + zipWith._tag = OpCodes.OP_ZIP_WITH + zipWith.left = self + zipWith.right = that + zipWith.zip = f + return zipWith +}) diff --git a/repos/effect/packages/effect/src/internal/configError.ts b/repos/effect/packages/effect/src/internal/configError.ts new file mode 100644 index 0000000..436b107 --- /dev/null +++ b/repos/effect/packages/effect/src/internal/configError.ts @@ -0,0 +1,304 @@ +import * as RA from "../Array.js" +import type * as Cause from "../Cause.js" +import type * as ConfigError from "../ConfigError.js" +import * as Either from "../Either.js" +import { constFalse, constTrue, dual, pipe } from "../Function.js" +import { hasProperty } from "../Predicate.js" +import * as OpCodes from "./opCodes/configError.js" + +/** @internal */ +const ConfigErrorSymbolKey = "effect/ConfigError" + +/** @internal */ +export const ConfigErrorTypeId: ConfigError.ConfigErrorTypeId = Symbol.for( + ConfigErrorSymbolKey +) as ConfigError.ConfigErrorTypeId + +/** @internal */ +export const proto = { + _tag: "ConfigError", + [ConfigErrorTypeId]: ConfigErrorTypeId +} + +/** @internal */ +export const And = (self: ConfigError.ConfigError, that: ConfigError.ConfigError): ConfigError.ConfigError => { + const error = Object.create(proto) + error._op = OpCodes.OP_AND + error.left = self + error.right = that + Object.defineProperty(error, "toString", { + enumerable: false, + value(this: ConfigError.And) { + return `${this.left} and ${this.right}` + } + }) + Object.defineProperty(error, "message", { + enumerable: false, + get(this: ConfigError.And) { + return this.toString() + } + }) + return error +} + +/** @internal */ +export const Or = (self: ConfigError.ConfigError, that: ConfigError.ConfigError): ConfigError.ConfigError => { + const error = Object.create(proto) + error._op = OpCodes.OP_OR + error.left = self + error.right = that + Object.defineProperty(error, "toString", { + enumerable: false, + value(this: ConfigError.Or) { + return `${this.left} or ${this.right}` + } + }) + Object.defineProperty(error, "message", { + enumerable: false, + get(this: ConfigError.Or) { + return this.toString() + } + }) + return error +} + +/** @internal */ +export const InvalidData = ( + path: ReadonlyArray, + message: string, + options: ConfigError.Options = { pathDelim: "." } +): ConfigError.ConfigError => { + const error = Object.create(proto) + error._op = OpCodes.OP_INVALID_DATA + error.path = path + error.message = message + Object.defineProperty(error, "toString", { + enumerable: false, + value(this: ConfigError.InvalidData) { + const path = pipe(this.path, RA.join(options.pathDelim)) + return `(Invalid data at ${path}: "${this.message}")` + } + }) + return error +} + +/** @internal */ +export const MissingData = ( + path: ReadonlyArray, + message: string, + options: ConfigError.Options = { pathDelim: "." } +): ConfigError.ConfigError => { + const error = Object.create(proto) + error._op = OpCodes.OP_MISSING_DATA + error.path = path + error.message = message + Object.defineProperty(error, "toString", { + enumerable: false, + value(this: ConfigError.MissingData) { + const path = pipe(this.path, RA.join(options.pathDelim)) + return `(Missing data at ${path}: "${this.message}")` + } + }) + return error +} + +/** @internal */ +export const SourceUnavailable = ( + path: ReadonlyArray, + message: string, + cause: Cause.Cause, + options: ConfigError.Options = { pathDelim: "." } +): ConfigError.ConfigError => { + const error = Object.create(proto) + error._op = OpCodes.OP_SOURCE_UNAVAILABLE + error.path = path + error.message = message + error.cause = cause + Object.defineProperty(error, "toString", { + enumerable: false, + value(this: ConfigError.SourceUnavailable) { + const path = pipe(this.path, RA.join(options.pathDelim)) + return `(Source unavailable at ${path}: "${this.message}")` + } + }) + return error +} + +/** @internal */ +export const Unsupported = ( + path: ReadonlyArray, + message: string, + options: ConfigError.Options = { pathDelim: "." } +): ConfigError.ConfigError => { + const error = Object.create(proto) + error._op = OpCodes.OP_UNSUPPORTED + error.path = path + error.message = message + Object.defineProperty(error, "toString", { + enumerable: false, + value(this: ConfigError.Unsupported) { + const path = pipe(this.path, RA.join(options.pathDelim)) + return `(Unsupported operation at ${path}: "${this.message}")` + } + }) + return error +} + +/** @internal */ +export const isConfigError = (u: unknown): u is ConfigError.ConfigError => hasProperty(u, ConfigErrorTypeId) + +/** @internal */ +export const isAnd = (self: ConfigError.ConfigError): self is ConfigError.And => self._op === OpCodes.OP_AND + +/** @internal */ +export const isOr = (self: ConfigError.ConfigError): self is ConfigError.Or => self._op === OpCodes.OP_OR + +/** @internal */ +export const isInvalidData = (self: ConfigError.ConfigError): self is ConfigError.InvalidData => + self._op === OpCodes.OP_INVALID_DATA + +/** @internal */ +export const isMissingData = (self: ConfigError.ConfigError): self is ConfigError.MissingData => + self._op === OpCodes.OP_MISSING_DATA + +/** @internal */ +export const isSourceUnavailable = (self: ConfigError.ConfigError): self is ConfigError.SourceUnavailable => + self._op === OpCodes.OP_SOURCE_UNAVAILABLE + +/** @internal */ +export const isUnsupported = (self: ConfigError.ConfigError): self is ConfigError.Unsupported => + self._op === OpCodes.OP_UNSUPPORTED + +/** @internal */ +export const prefixed: { + (prefix: ReadonlyArray): (self: ConfigError.ConfigError) => ConfigError.ConfigError + (self: ConfigError.ConfigError, prefix: ReadonlyArray): ConfigError.ConfigError +} = dual< + (prefix: ReadonlyArray) => (self: ConfigError.ConfigError) => ConfigError.ConfigError, + (self: ConfigError.ConfigError, prefix: ReadonlyArray) => ConfigError.ConfigError +>(2, (self, prefix) => { + switch (self._op) { + case OpCodes.OP_AND: { + return And(prefixed(self.left, prefix), prefixed(self.right, prefix)) + } + case OpCodes.OP_OR: { + return Or(prefixed(self.left, prefix), prefixed(self.right, prefix)) + } + case OpCodes.OP_INVALID_DATA: { + return InvalidData([...prefix, ...self.path], self.message) + } + case OpCodes.OP_MISSING_DATA: { + return MissingData([...prefix, ...self.path], self.message) + } + case OpCodes.OP_SOURCE_UNAVAILABLE: { + return SourceUnavailable([...prefix, ...self.path], self.message, self.cause) + } + case OpCodes.OP_UNSUPPORTED: { + return Unsupported([...prefix, ...self.path], self.message) + } + } +}) + +/** @internal */ +const IsMissingDataOnlyReducer: ConfigError.ConfigErrorReducer = { + andCase: (_, left, right) => left && right, + orCase: (_, left, right) => left && right, + invalidDataCase: constFalse, + missingDataCase: constTrue, + sourceUnavailableCase: constFalse, + unsupportedCase: constFalse +} + +/** @internal */ +type ConfigErrorCase = AndCase | OrCase + +/** @internal */ +interface AndCase { + readonly _op: "AndCase" +} + +/** @internal */ +interface OrCase { + readonly _op: "OrCase" +} + +/** @internal */ +export const reduceWithContext = dual< + (context: C, reducer: ConfigError.ConfigErrorReducer) => (self: ConfigError.ConfigError) => Z, + (self: ConfigError.ConfigError, context: C, reducer: ConfigError.ConfigErrorReducer) => Z +>(3, (self: ConfigError.ConfigError, context: C, reducer: ConfigError.ConfigErrorReducer) => { + const input: Array = [self] + const output: Array> = [] + while (input.length > 0) { + const error = input.pop()! + switch (error._op) { + case OpCodes.OP_AND: { + input.push(error.right) + input.push(error.left) + output.push(Either.left({ _op: "AndCase" })) + break + } + case OpCodes.OP_OR: { + input.push(error.right) + input.push(error.left) + output.push(Either.left({ _op: "OrCase" })) + break + } + case OpCodes.OP_INVALID_DATA: { + output.push(Either.right(reducer.invalidDataCase(context, error.path, error.message))) + break + } + case OpCodes.OP_MISSING_DATA: { + output.push(Either.right(reducer.missingDataCase(context, error.path, error.message))) + break + } + case OpCodes.OP_SOURCE_UNAVAILABLE: { + output.push(Either.right(reducer.sourceUnavailableCase(context, error.path, error.message, error.cause))) + break + } + case OpCodes.OP_UNSUPPORTED: { + output.push(Either.right(reducer.unsupportedCase(context, error.path, error.message))) + break + } + } + } + const accumulator: Array = [] + while (output.length > 0) { + const either = output.pop()! + switch (either._op) { + case "Left": { + switch (either.left._op) { + case "AndCase": { + const left = accumulator.pop()! + const right = accumulator.pop()! + const value = reducer.andCase(context, left, right) + accumulator.push(value) + break + } + case "OrCase": { + const left = accumulator.pop()! + const right = accumulator.pop()! + const value = reducer.orCase(context, left, right) + accumulator.push(value) + break + } + } + break + } + case "Right": { + accumulator.push(either.right) + break + } + } + } + if (accumulator.length === 0) { + throw new Error( + "BUG: ConfigError.reduceWithContext - please report an issue at https://github.com/Effect-TS/effect/issues" + ) + } + return accumulator.pop()! +}) + +/** @internal */ +export const isMissingDataOnly = (self: ConfigError.ConfigError): boolean => + reduceWithContext(self, void 0, IsMissingDataOnlyReducer) diff --git a/repos/effect/packages/effect/src/internal/configProvider.ts b/repos/effect/packages/effect/src/internal/configProvider.ts new file mode 100644 index 0000000..c6590dc --- /dev/null +++ b/repos/effect/packages/effect/src/internal/configProvider.ts @@ -0,0 +1,799 @@ +import * as Arr from "../Array.js" +import type * as Config from "../Config.js" +import type * as ConfigError from "../ConfigError.js" +import type * as ConfigProvider from "../ConfigProvider.js" +import type * as PathPatch from "../ConfigProviderPathPatch.js" +import * as Context from "../Context.js" +import type * as Effect from "../Effect.js" +import * as Either from "../Either.js" +import type { LazyArg } from "../Function.js" +import { dual, pipe } from "../Function.js" +import * as HashMap from "../HashMap.js" +import * as HashSet from "../HashSet.js" +import * as number from "../Number.js" +import * as Option from "../Option.js" +import { pipeArguments } from "../Pipeable.js" +import * as Predicate from "../Predicate.js" +import * as regexp from "../RegExp.js" +import type * as _config from "./config.js" +import * as configError from "./configError.js" +import * as pathPatch from "./configProvider/pathPatch.js" +import * as core from "./core.js" +import * as OpCodes from "./opCodes/config.js" +import * as StringUtils from "./string-utils.js" + +type KeyComponent = ConfigProvider.ConfigProvider.KeyComponent + +const concat = (l: ReadonlyArray, r: ReadonlyArray): ReadonlyArray => [...l, ...r] + +/** @internal */ +const ConfigProviderSymbolKey = "effect/ConfigProvider" + +/** @internal */ +export const ConfigProviderTypeId: ConfigProvider.ConfigProviderTypeId = Symbol.for( + ConfigProviderSymbolKey +) as ConfigProvider.ConfigProviderTypeId + +/** @internal */ +export const configProviderTag: Context.Tag = Context + .GenericTag( + "effect/ConfigProvider" + ) + +/** @internal */ +const FlatConfigProviderSymbolKey = "effect/ConfigProviderFlat" + +/** @internal */ +export const FlatConfigProviderTypeId: ConfigProvider.FlatConfigProviderTypeId = Symbol.for( + FlatConfigProviderSymbolKey +) as ConfigProvider.FlatConfigProviderTypeId + +/** @internal */ +export const make = ( + options: { + readonly load: (config: Config.Config) => Effect.Effect + readonly flattened: ConfigProvider.ConfigProvider.Flat + } +): ConfigProvider.ConfigProvider => ({ + [ConfigProviderTypeId]: ConfigProviderTypeId, + pipe() { + return pipeArguments(this, arguments) + }, + ...options +}) + +/** @internal */ +export const makeFlat = ( + options: { + readonly load: ( + path: ReadonlyArray, + config: Config.Config.Primitive, + split: boolean + ) => Effect.Effect, ConfigError.ConfigError> + readonly enumerateChildren: ( + path: ReadonlyArray + ) => Effect.Effect, ConfigError.ConfigError> + readonly patch: PathPatch.PathPatch + } +): ConfigProvider.ConfigProvider.Flat => ({ + [FlatConfigProviderTypeId]: FlatConfigProviderTypeId, + patch: options.patch, + load: (path, config, split = true) => options.load(path, config, split), + enumerateChildren: options.enumerateChildren +}) + +/** @internal */ +export const fromFlat = (flat: ConfigProvider.ConfigProvider.Flat): ConfigProvider.ConfigProvider => + make({ + load: (config) => + core.flatMap(fromFlatLoop(flat, Arr.empty(), config, false), (chunk) => + Option.match(Arr.head(chunk), { + onNone: () => + core.fail( + configError.MissingData( + Arr.empty(), + `Expected a single value having structure: ${config}` + ) + ), + onSome: core.succeed + })), + flattened: flat + }) + +/** @internal */ +export const fromEnv = ( + options?: Partial +): ConfigProvider.ConfigProvider => { + const { pathDelim, seqDelim } = Object.assign({}, { pathDelim: "_", seqDelim: "," }, options) + const makePathString = (path: ReadonlyArray): string => pipe(path, Arr.join(pathDelim)) + const unmakePathString = (pathString: string): ReadonlyArray => pathString.split(pathDelim) + + const getEnv = () => + typeof process !== "undefined" && "env" in process && typeof process.env === "object" ? process.env : {} + + const load = ( + path: ReadonlyArray, + primitive: Config.Config.Primitive, + split = true + ): Effect.Effect, ConfigError.ConfigError> => { + const pathString = makePathString(path) + const current = getEnv() + const valueOpt = pathString in current ? Option.some(current[pathString]!) : Option.none() + return pipe( + valueOpt, + core.mapError(() => configError.MissingData(path, `Expected ${pathString} to exist in the process context`)), + core.flatMap((value) => parsePrimitive(value, path, primitive, seqDelim, split)) + ) + } + + const enumerateChildren = ( + path: ReadonlyArray + ): Effect.Effect, ConfigError.ConfigError> => + core.sync(() => { + const current = getEnv() + const keys = Object.keys(current) + const keyPaths = keys.map((value) => unmakePathString(value.toUpperCase())) + const filteredKeyPaths = keyPaths.filter((keyPath) => { + for (let i = 0; i < path.length; i++) { + const pathComponent = pipe(path, Arr.unsafeGet(i)) + const currentElement = keyPath[i] + if (currentElement === undefined || pathComponent !== currentElement) { + return false + } + } + return true + }).flatMap((keyPath) => keyPath.slice(path.length, path.length + 1)) + return HashSet.fromIterable(filteredKeyPaths) + }) + + return fromFlat(makeFlat({ load, enumerateChildren, patch: pathPatch.empty })) +} + +/** @internal */ +export const fromMap = ( + map: Map, + config?: Partial +): ConfigProvider.ConfigProvider => { + const { pathDelim, seqDelim } = Object.assign({ seqDelim: ",", pathDelim: "." }, config) + const makePathString = (path: ReadonlyArray): string => pipe(path, Arr.join(pathDelim)) + const unmakePathString = (pathString: string): ReadonlyArray => pathString.split(pathDelim) + const mapWithIndexSplit = splitIndexInKeys( + map, + (str) => unmakePathString(str), + makePathString + ) + const load = ( + path: ReadonlyArray, + primitive: Config.Config.Primitive, + split = true + ): Effect.Effect, ConfigError.ConfigError> => { + const pathString = makePathString(path) + const valueOpt = mapWithIndexSplit.has(pathString) ? + Option.some(mapWithIndexSplit.get(pathString)!) : + Option.none() + return pipe( + valueOpt, + core.mapError(() => configError.MissingData(path, `Expected ${pathString} to exist in the provided map`)), + core.flatMap((value) => parsePrimitive(value, path, primitive, seqDelim, split)) + ) + } + const enumerateChildren = ( + path: ReadonlyArray + ): Effect.Effect, ConfigError.ConfigError> => + core.sync(() => { + const keyPaths = Arr.fromIterable(mapWithIndexSplit.keys()).map(unmakePathString) + const filteredKeyPaths = keyPaths.filter((keyPath) => { + for (let i = 0; i < path.length; i++) { + const pathComponent = pipe(path, Arr.unsafeGet(i)) + const currentElement = keyPath[i] + if (currentElement === undefined || pathComponent !== currentElement) { + return false + } + } + return true + }).flatMap((keyPath) => keyPath.slice(path.length, path.length + 1)) + return HashSet.fromIterable(filteredKeyPaths) + }) + + return fromFlat(makeFlat({ load, enumerateChildren, patch: pathPatch.empty })) +} + +const extend = ( + leftDef: (n: number) => A, + rightDef: (n: number) => B, + left: ReadonlyArray, + right: ReadonlyArray +): [ReadonlyArray, ReadonlyArray] => { + const leftPad = Arr.unfold( + left.length, + (index) => + index >= right.length ? + Option.none() : + Option.some([leftDef(index), index + 1]) + ) + const rightPad = Arr.unfold( + right.length, + (index) => + index >= left.length ? + Option.none() : + Option.some([rightDef(index), index + 1]) + ) + const leftExtension = concat(left, leftPad) + const rightExtension = concat(right, rightPad) + return [leftExtension, rightExtension] +} + +const appendConfigPath = (path: ReadonlyArray, config: Config.Config): ReadonlyArray => { + let op = config as _config.ConfigPrimitive + if (op._tag === "Nested") { + const out = path.slice() + while (op._tag === "Nested") { + out.push(op.name) + op = op.config as _config.ConfigPrimitive + } + return out + } + return path +} + +const fromFlatLoop = ( + flat: ConfigProvider.ConfigProvider.Flat, + prefix: ReadonlyArray, + config: Config.Config, + split: boolean +): Effect.Effect, ConfigError.ConfigError> => { + const op = config as _config.ConfigPrimitive + switch (op._tag) { + case OpCodes.OP_CONSTANT: { + return core.succeed(Arr.of(op.value)) as Effect.Effect, ConfigError.ConfigError> + } + case OpCodes.OP_DESCRIBED: { + return core.suspend( + () => fromFlatLoop(flat, prefix, op.config, split) + ) as unknown as Effect.Effect, ConfigError.ConfigError> + } + case OpCodes.OP_FAIL: { + return core.fail(configError.MissingData(prefix, op.message)) as Effect.Effect< + Array, + ConfigError.ConfigError + > + } + case OpCodes.OP_FALLBACK: { + return pipe( + core.suspend(() => fromFlatLoop(flat, prefix, op.first, split)), + core.catchAll((error1) => { + if (op.condition(error1)) { + return pipe( + fromFlatLoop(flat, prefix, op.second, split), + core.catchAll((error2) => core.fail(configError.Or(error1, error2))) + ) + } + return core.fail(error1) + }) + ) as unknown as Effect.Effect, ConfigError.ConfigError> + } + case OpCodes.OP_LAZY: { + return core.suspend(() => fromFlatLoop(flat, prefix, op.config(), split)) as Effect.Effect< + Array, + ConfigError.ConfigError + > + } + case OpCodes.OP_MAP_OR_FAIL: { + return core.suspend(() => + pipe( + fromFlatLoop(flat, prefix, op.original, split), + core.flatMap( + core.forEachSequential((a) => + pipe( + op.mapOrFail(a), + core.mapError(configError.prefixed(appendConfigPath(prefix, op.original))) + ) + ) + ) + ) + ) as unknown as Effect.Effect, ConfigError.ConfigError> + } + case OpCodes.OP_NESTED: { + return core.suspend(() => + fromFlatLoop( + flat, + concat(prefix, Arr.of(op.name)), + op.config, + split + ) + ) as unknown as Effect.Effect, ConfigError.ConfigError> + } + case OpCodes.OP_PRIMITIVE: { + return pipe( + pathPatch.patch(prefix, flat.patch), + core.flatMap((prefix) => + pipe( + flat.load(prefix, op, split), + core.flatMap((values) => { + if (values.length === 0) { + const name = pipe(Arr.last(prefix), Option.getOrElse(() => "")) + return core.fail(configError.MissingData([], `Expected ${op.description} with name ${name}`)) + } + return core.succeed(values) + }) + ) + ) + ) as unknown as Effect.Effect, ConfigError.ConfigError> + } + case OpCodes.OP_SEQUENCE: { + return pipe( + pathPatch.patch(prefix, flat.patch), + core.flatMap((patchedPrefix) => + pipe( + flat.enumerateChildren(patchedPrefix), + core.flatMap(indicesFrom), + core.flatMap((indices) => { + if (indices.length === 0) { + return core.suspend(() => + core.map(fromFlatLoop(flat, prefix, op.config, true), Arr.of) + ) as unknown as Effect.Effect, ConfigError.ConfigError> + } + return pipe( + core.forEachSequential( + indices, + (index) => fromFlatLoop(flat, Arr.append(prefix, `[${index}]`), op.config, true) + ), + core.map((chunkChunk) => { + const flattened = Arr.flatten(chunkChunk) + if (flattened.length === 0) { + return Arr.of(Arr.empty()) + } + return Arr.of(flattened) + }) + ) as unknown as Effect.Effect, ConfigError.ConfigError> + }) + ) + ) + ) + } + case OpCodes.OP_HASHMAP: { + return core.suspend(() => + pipe( + pathPatch.patch(prefix, flat.patch), + core.flatMap((prefix) => + pipe( + flat.enumerateChildren(prefix), + core.flatMap((keys) => { + return pipe( + keys, + core.forEachSequential((key) => + fromFlatLoop( + flat, + concat(prefix, Arr.of(key)), + op.valueConfig, + split + ) + ), + core.map((matrix) => { + if (matrix.length === 0) { + return Arr.of(HashMap.empty()) + } + return pipe( + transpose(matrix), + Arr.map((values) => HashMap.fromIterable(Arr.zip(Arr.fromIterable(keys), values))) + ) + }) + ) + }) + ) + ) + ) + ) as unknown as Effect.Effect, ConfigError.ConfigError> + } + case OpCodes.OP_ZIP_WITH: { + return core.suspend(() => + pipe( + fromFlatLoop(flat, prefix, op.left, split), + core.either, + core.flatMap((left) => + pipe( + fromFlatLoop(flat, prefix, op.right, split), + core.either, + core.flatMap((right) => { + if (Either.isLeft(left) && Either.isLeft(right)) { + return core.fail(configError.And(left.left, right.left)) + } + if (Either.isLeft(left) && Either.isRight(right)) { + return core.fail(left.left) + } + if (Either.isRight(left) && Either.isLeft(right)) { + return core.fail(right.left) + } + if (Either.isRight(left) && Either.isRight(right)) { + const path = pipe(prefix, Arr.join(".")) + const fail = fromFlatLoopFail(prefix, path) + const [lefts, rights] = extend( + fail, + fail, + pipe(left.right, Arr.map(Either.right)), + pipe(right.right, Arr.map(Either.right)) + ) + return pipe( + lefts, + Arr.zip(rights), + core.forEachSequential(([left, right]) => + pipe( + core.zip(left, right), + core.map(([left, right]) => op.zip(left, right)) + ) + ) + ) + } + throw new Error( + "BUG: ConfigProvider.fromFlatLoop - please report an issue at https://github.com/Effect-TS/effect/issues" + ) + }) + ) + ) + ) + ) as unknown as Effect.Effect, ConfigError.ConfigError> + } + } +} + +const fromFlatLoopFail = + (prefix: ReadonlyArray, path: string) => (index: number): Either.Either => + Either.left( + configError.MissingData( + prefix, + `The element at index ${index} in a sequence at path "${path}" was missing` + ) + ) + +/** @internal */ +export const mapInputPath = dual< + (f: (path: string) => string) => (self: ConfigProvider.ConfigProvider) => ConfigProvider.ConfigProvider, + (self: ConfigProvider.ConfigProvider, f: (path: string) => string) => ConfigProvider.ConfigProvider +>(2, (self, f) => fromFlat(mapInputPathFlat(self.flattened, f))) + +const mapInputPathFlat = ( + self: ConfigProvider.ConfigProvider.Flat, + f: (path: string) => string +): ConfigProvider.ConfigProvider.Flat => + makeFlat({ + load: (path, config, split = true) => self.load(path, config, split), + enumerateChildren: (path) => self.enumerateChildren(path), + patch: pathPatch.mapName(self.patch, f) + }) + +/** @internal */ +export const nested = dual< + (name: string) => (self: ConfigProvider.ConfigProvider) => ConfigProvider.ConfigProvider, + (self: ConfigProvider.ConfigProvider, name: string) => ConfigProvider.ConfigProvider +>(2, (self, name) => + fromFlat(makeFlat({ + load: (path, config) => self.flattened.load(path, config, true), + enumerateChildren: (path) => self.flattened.enumerateChildren(path), + patch: pathPatch.nested(self.flattened.patch, name) + }))) + +/** @internal */ +export const unnested = dual< + (name: string) => (self: ConfigProvider.ConfigProvider) => ConfigProvider.ConfigProvider, + (self: ConfigProvider.ConfigProvider, name: string) => ConfigProvider.ConfigProvider +>(2, (self, name) => + fromFlat(makeFlat({ + load: (path, config) => self.flattened.load(path, config, true), + enumerateChildren: (path) => self.flattened.enumerateChildren(path), + patch: pathPatch.unnested(self.flattened.patch, name) + }))) + +/** @internal */ +export const orElse = dual< + ( + that: LazyArg + ) => ( + self: ConfigProvider.ConfigProvider + ) => ConfigProvider.ConfigProvider, + ( + self: ConfigProvider.ConfigProvider, + that: LazyArg + ) => ConfigProvider.ConfigProvider +>(2, (self, that) => fromFlat(orElseFlat(self.flattened, () => that().flattened))) + +const orElseFlat = ( + self: ConfigProvider.ConfigProvider.Flat, + that: LazyArg +): ConfigProvider.ConfigProvider.Flat => + makeFlat({ + load: (path, config, split) => + pipe( + pathPatch.patch(path, self.patch), + core.flatMap((patch) => self.load(patch, config, split)), + core.catchAll((error1) => + pipe( + core.sync(that), + core.flatMap((that) => + pipe( + pathPatch.patch(path, that.patch), + core.flatMap((patch) => that.load(patch, config, split)), + core.catchAll((error2) => core.fail(configError.Or(error1, error2))) + ) + ) + ) + ) + ), + enumerateChildren: (path) => + pipe( + pathPatch.patch(path, self.patch), + core.flatMap((patch) => self.enumerateChildren(patch)), + core.either, + core.flatMap((left) => + pipe( + core.sync(that), + core.flatMap((that) => + pipe( + pathPatch.patch(path, that.patch), + core.flatMap((patch) => that.enumerateChildren(patch)), + core.either, + core.flatMap((right) => { + if (Either.isLeft(left) && Either.isLeft(right)) { + return core.fail(configError.And(left.left, right.left)) + } + if (Either.isLeft(left) && Either.isRight(right)) { + return core.succeed(right.right) + } + if (Either.isRight(left) && Either.isLeft(right)) { + return core.succeed(left.right) + } + if (Either.isRight(left) && Either.isRight(right)) { + return core.succeed(pipe(left.right, HashSet.union(right.right))) + } + throw new Error( + "BUG: ConfigProvider.orElseFlat - please report an issue at https://github.com/Effect-TS/effect/issues" + ) + }) + ) + ) + ) + ) + ), + patch: pathPatch.empty + }) + +/** @internal */ +export const constantCase = (self: ConfigProvider.ConfigProvider): ConfigProvider.ConfigProvider => + mapInputPath(self, StringUtils.constantCase) + +/** @internal */ +export const kebabCase = (self: ConfigProvider.ConfigProvider): ConfigProvider.ConfigProvider => + mapInputPath(self, StringUtils.kebabCase) + +/** @internal */ +export const lowerCase = (self: ConfigProvider.ConfigProvider): ConfigProvider.ConfigProvider => + mapInputPath(self, StringUtils.lowerCase) + +/** @internal */ +export const snakeCase = (self: ConfigProvider.ConfigProvider): ConfigProvider.ConfigProvider => + mapInputPath(self, StringUtils.snakeCase) + +/** @internal */ +export const upperCase = (self: ConfigProvider.ConfigProvider): ConfigProvider.ConfigProvider => + mapInputPath(self, StringUtils.upperCase) + +/** @internal */ +export const within = dual< + ( + path: ReadonlyArray, + f: (self: ConfigProvider.ConfigProvider) => ConfigProvider.ConfigProvider + ) => (self: ConfigProvider.ConfigProvider) => ConfigProvider.ConfigProvider, + ( + self: ConfigProvider.ConfigProvider, + path: ReadonlyArray, + f: (self: ConfigProvider.ConfigProvider) => ConfigProvider.ConfigProvider + ) => ConfigProvider.ConfigProvider +>(3, (self, path, f) => { + const unnest = Arr.reduce(path, self, (provider, name) => unnested(provider, name)) + const nest = Arr.reduceRight(path, f(unnest), (provider, name) => nested(provider, name)) + return orElse(nest, () => self) +}) + +const splitPathString = (text: string, delim: string): Array => { + const split = text.split(new RegExp(`\\s*${regexp.escape(delim)}\\s*`)) + return split +} + +const parsePrimitive = ( + text: string, + path: ReadonlyArray, + primitive: Config.Config.Primitive, + delimiter: string, + split: boolean +): Effect.Effect, ConfigError.ConfigError> => { + if (!split) { + return pipe( + primitive.parse(text), + core.mapBoth({ + onFailure: configError.prefixed(path), + onSuccess: Arr.of + }) + ) + } + return pipe( + splitPathString(text, delimiter), + core.forEachSequential((char) => primitive.parse(char.trim())), + core.mapError(configError.prefixed(path)) + ) +} + +const transpose = (array: ReadonlyArray>): Array> => { + return Object.keys(array[0]).map((column) => array.map((row) => row[column as any])) +} + +const indicesFrom = (quotedIndices: HashSet.HashSet): Effect.Effect> => + pipe( + core.forEachSequential(quotedIndices, parseQuotedIndex), + core.mapBoth({ + onFailure: () => Arr.empty(), + onSuccess: Arr.sort(number.Order) + }), + core.either, + core.map(Either.merge) + ) + +const STR_INDEX_REGEX = /(^.+)(\[(\d+)\])$/ +const QUOTED_INDEX_REGEX = /^(\[(\d+)\])$/ + +const parseQuotedIndex = (str: string): Option.Option => { + const match = str.match(QUOTED_INDEX_REGEX) + if (match !== null) { + const matchedIndex = match[2] + return pipe( + matchedIndex !== undefined && matchedIndex.length > 0 ? + Option.some(matchedIndex) : + Option.none(), + Option.flatMap(parseInteger) + ) + } + return Option.none() +} + +const splitIndexInKeys = ( + map: Map, + unmakePathString: (str: string) => ReadonlyArray, + makePathString: (chunk: ReadonlyArray) => string +): Map => { + const newMap: Map = new Map() + for (const [pathString, value] of map) { + const keyWithIndex = pipe( + unmakePathString(pathString), + Arr.flatMap((key) => + Option.match(splitIndexFrom(key), { + onNone: () => Arr.of(key), + onSome: ([key, index]) => Arr.make(key, `[${index}]`) + }) + ) + ) + newMap.set(makePathString(keyWithIndex), value) + } + return newMap +} + +const splitIndexFrom = (key: string): Option.Option<[string, number]> => { + const match = key.match(STR_INDEX_REGEX) + if (match !== null) { + const matchedString = match[1] + const matchedIndex = match[3] + const optionalString = matchedString !== undefined && matchedString.length > 0 ? + Option.some(matchedString) : + Option.none() + const optionalIndex = pipe( + matchedIndex !== undefined && matchedIndex.length > 0 ? + Option.some(matchedIndex) : + Option.none(), + Option.flatMap(parseInteger) + ) + return Option.all([optionalString, optionalIndex]) + } + return Option.none() +} + +const parseInteger = (str: string): Option.Option => { + const parsedIndex = Number.parseInt(str) + return Number.isNaN(parsedIndex) ? + Option.none() : + Option.some(parsedIndex) +} + +const keyName = (name: string): KeyComponent => ({ + _tag: "KeyName", + name +}) + +const keyIndex = (index: number): KeyComponent => ({ + _tag: "KeyIndex", + index +}) + +interface JsonMap { + [member: string]: string | number | boolean | null | JsonArray | JsonMap +} +interface JsonArray extends Array {} + +/** @internal */ +export const fromJson = (json: unknown): ConfigProvider.ConfigProvider => { + const hiddenDelimiter = "\ufeff" + const indexedEntries = Arr.map( + getIndexedEntries(json as JsonMap), + ([key, value]): [string, string] => [configPathToString(key).join(hiddenDelimiter), value] + ) + return fromMap(new Map(indexedEntries), { + pathDelim: hiddenDelimiter, + seqDelim: hiddenDelimiter + }) +} + +const configPathToString = (path: ReadonlyArray): ReadonlyArray => { + const output: Array = [] + let i = 0 + while (i < path.length) { + const component = path[i] + if (component._tag === "KeyName") { + if (i + 1 < path.length) { + const nextComponent = path[i + 1] + if (nextComponent._tag === "KeyIndex") { + output.push(`${component.name}[${nextComponent.index}]`) + i += 2 + } else { + output.push(component.name) + i += 1 + } + } else { + output.push(component.name) + i += 1 + } + } + } + return output +} + +const getIndexedEntries = ( + config: JsonMap +): ReadonlyArray<[path: ReadonlyArray, value: string]> => { + const loopAny = ( + path: ReadonlyArray, + value: string | number | boolean | JsonMap | JsonArray | null + ): ReadonlyArray<[path: ReadonlyArray, value: string]> => { + if (typeof value === "string") { + return Arr.make([path, value] as [ReadonlyArray, string]) + } + if (typeof value === "number" || typeof value === "boolean") { + return Arr.make([path, String(value)] as [ReadonlyArray, string]) + } + if (Arr.isArray(value)) { + return loopArray(path, value) + } + if (typeof value === "object" && value !== null) { + return loopObject(path, value) + } + return Arr.empty<[ReadonlyArray, string]>() + } + const loopArray = ( + path: ReadonlyArray, + values: JsonArray + ): ReadonlyArray<[path: ReadonlyArray, value: string]> => + Arr.match(values, { + onEmpty: () => Arr.make([path, ""] as [ReadonlyArray, string]), + onNonEmpty: Arr.flatMap((value, index) => loopAny(Arr.append(path, keyIndex(index)), value)) + }) + const loopObject = ( + path: ReadonlyArray, + value: JsonMap + ): ReadonlyArray<[path: ReadonlyArray, value: string]> => + Object.entries(value) + .filter(([, value]) => Predicate.isNotNullable(value)) + .flatMap(([key, value]) => { + const newPath = Arr.append(path, keyName(key)) + const result = loopAny(newPath, value) + if (Arr.isEmptyReadonlyArray(result)) { + return Arr.make([newPath, ""] as [ReadonlyArray, string]) + } + return result + }) + return loopObject(Arr.empty(), config) +} diff --git a/repos/effect/packages/effect/src/internal/configProvider/pathPatch.ts b/repos/effect/packages/effect/src/internal/configProvider/pathPatch.ts new file mode 100644 index 0000000..f4f463a --- /dev/null +++ b/repos/effect/packages/effect/src/internal/configProvider/pathPatch.ts @@ -0,0 +1,97 @@ +import * as RA from "../../Array.js" +import type * as ConfigError from "../../ConfigError.js" +import type * as PathPatch from "../../ConfigProviderPathPatch.js" +import * as Either from "../../Either.js" +import { dual, pipe } from "../../Function.js" +import * as List from "../../List.js" +import * as Option from "../../Option.js" +import * as configError from "../configError.js" + +/** @internal */ +export const empty: PathPatch.PathPatch = { + _tag: "Empty" +} + +/** @internal */ +export const andThen = dual< + (that: PathPatch.PathPatch) => (self: PathPatch.PathPatch) => PathPatch.PathPatch, + (self: PathPatch.PathPatch, that: PathPatch.PathPatch) => PathPatch.PathPatch +>(2, (self, that) => ({ + _tag: "AndThen", + first: self, + second: that +})) + +/** @internal */ +export const mapName = dual< + (f: (string: string) => string) => (self: PathPatch.PathPatch) => PathPatch.PathPatch, + (self: PathPatch.PathPatch, f: (string: string) => string) => PathPatch.PathPatch +>(2, (self, f) => andThen(self, { _tag: "MapName", f })) + +/** @internal */ +export const nested = dual< + (name: string) => (self: PathPatch.PathPatch) => PathPatch.PathPatch, + (self: PathPatch.PathPatch, name: string) => PathPatch.PathPatch +>(2, (self, name) => andThen(self, { _tag: "Nested", name })) + +/** @internal */ +export const unnested = dual< + (name: string) => (self: PathPatch.PathPatch) => PathPatch.PathPatch, + (self: PathPatch.PathPatch, name: string) => PathPatch.PathPatch +>(2, (self, name) => andThen(self, { _tag: "Unnested", name })) + +/** @internal */ +export const patch = dual< + ( + patch: PathPatch.PathPatch + ) => ( + path: ReadonlyArray + ) => Either.Either, ConfigError.ConfigError>, + ( + path: ReadonlyArray, + patch: PathPatch.PathPatch + ) => Either.Either, ConfigError.ConfigError> +>(2, (path, patch) => { + let input: List.List = List.of(patch) + let output: ReadonlyArray = path + while (List.isCons(input)) { + const patch: PathPatch.PathPatch = input.head + switch (patch._tag) { + case "Empty": { + input = input.tail + break + } + case "AndThen": { + input = List.cons(patch.first, List.cons(patch.second, input.tail)) + break + } + case "MapName": { + output = RA.map(output, patch.f) + input = input.tail + break + } + case "Nested": { + output = RA.prepend(output, patch.name) + input = input.tail + break + } + case "Unnested": { + const containsName = pipe( + RA.head(output), + Option.contains(patch.name) + ) + if (containsName) { + output = RA.tailNonEmpty(output as RA.NonEmptyArray) + input = input.tail + } else { + return Either.left(configError.MissingData( + output, + `Expected ${patch.name} to be in path in ConfigProvider#unnested` + )) + } + break + } + } + } + return Either.right(output) +}) diff --git a/repos/effect/packages/effect/src/internal/console.ts b/repos/effect/packages/effect/src/internal/console.ts new file mode 100644 index 0000000..402e263 --- /dev/null +++ b/repos/effect/packages/effect/src/internal/console.ts @@ -0,0 +1,153 @@ +import type * as Console from "../Console.js" +import * as Context from "../Context.js" +import type * as Effect from "../Effect.js" +import { dual } from "../Function.js" +import type * as Layer from "../Layer.js" +import type * as Scope from "../Scope.js" +import * as core from "./core.js" +import * as defaultServices from "./defaultServices.js" +import * as defaultConsole from "./defaultServices/console.js" +import * as fiberRuntime from "./fiberRuntime.js" +import * as layer from "./layer.js" + +/** @internal */ +export const console: Effect.Effect = core.map( + core.fiberRefGet(defaultServices.currentServices), + Context.get(defaultConsole.consoleTag) +) + +/** @internal */ +export const consoleWith = (f: (console: Console.Console) => Effect.Effect) => + core.fiberRefGetWith( + defaultServices.currentServices, + (services) => f(Context.get(services, defaultConsole.consoleTag)) + ) + +/** @internal */ +export const withConsole = dual< + (console: C) => (effect: Effect.Effect) => Effect.Effect, + (effect: Effect.Effect, console: C) => Effect.Effect +>(2, (effect, value) => + core.fiberRefLocallyWith( + effect, + defaultServices.currentServices, + Context.add(defaultConsole.consoleTag, value) + )) + +/** @internal */ +export const withConsoleScoped = (console: A): Effect.Effect => + fiberRuntime.fiberRefLocallyScopedWith( + defaultServices.currentServices, + Context.add(defaultConsole.consoleTag, console) + ) + +/** @internal */ +export const setConsole = (console: A): Layer.Layer => + layer.scopedDiscard( + fiberRuntime.fiberRefLocallyScopedWith( + defaultServices.currentServices, + Context.add(defaultConsole.consoleTag, console) + ) + ) + +/** @internal */ +export const assert = (condition: boolean, ...args: ReadonlyArray) => + consoleWith((_) => _.assert(condition, ...args)) + +/** @internal */ +export const clear = consoleWith((_) => _.clear) + +/** @internal */ +export const count = (label?: string) => consoleWith((_) => _.count(label)) + +/** @internal */ +export const countReset = (label?: string) => consoleWith((_) => _.countReset(label)) + +/** @internal */ +export const debug = (...args: ReadonlyArray) => consoleWith((_) => _.debug(...args)) + +/** @internal */ +export const dir = (item: any, options?: any) => consoleWith((_) => _.dir(item, options)) + +/** @internal */ +export const dirxml = (...args: ReadonlyArray) => consoleWith((_) => _.dirxml(...args)) + +/** @internal */ +export const error = (...args: ReadonlyArray) => consoleWith((_) => _.error(...args)) + +/** @internal */ +export const group = (options?: { + label?: string | undefined + collapsed?: boolean | undefined +}) => + consoleWith((_) => + fiberRuntime.acquireRelease( + _.group(options), + () => _.groupEnd + ) + ) + +/** @internal */ +export const info = (...args: ReadonlyArray) => consoleWith((_) => _.info(...args)) + +/** @internal */ +export const log = (...args: ReadonlyArray) => consoleWith((_) => _.log(...args)) + +/** @internal */ +export const table = (tabularData: any, properties?: ReadonlyArray) => + consoleWith((_) => _.table(tabularData, properties)) + +/** @internal */ +export const time = (label?: string) => + consoleWith((_) => + fiberRuntime.acquireRelease( + _.time(label), + () => _.timeEnd(label) + ) + ) + +/** @internal */ +export const timeLog = (label?: string, ...args: ReadonlyArray) => consoleWith((_) => _.timeLog(label, ...args)) + +/** @internal */ +export const trace = (...args: ReadonlyArray) => consoleWith((_) => _.trace(...args)) + +/** @internal */ +export const warn = (...args: ReadonlyArray) => consoleWith((_) => _.warn(...args)) + +/** @internal */ +export const withGroup = dual< + ( + options?: { + readonly label?: string | undefined + readonly collapsed?: boolean | undefined + } + ) => (self: Effect.Effect) => Effect.Effect, + ( + self: Effect.Effect, + options?: { + readonly label?: string | undefined + readonly collapsed?: boolean | undefined + } + ) => Effect.Effect +>((args) => core.isEffect(args[0]), (self, options) => + consoleWith((_) => + core.acquireUseRelease( + _.group(options), + () => self, + () => _.groupEnd + ) + )) + +/** @internal */ +export const withTime = dual< + (label?: string) => (self: Effect.Effect) => Effect.Effect, + (self: Effect.Effect, label?: string) => Effect.Effect +>((args) => core.isEffect(args[0]), (self, label) => + consoleWith((_) => + core.acquireUseRelease( + _.time(label), + () => self, + () => _.timeEnd(label) + ) + )) diff --git a/repos/effect/packages/effect/src/internal/context.ts b/repos/effect/packages/effect/src/internal/context.ts new file mode 100644 index 0000000..e77c1cd --- /dev/null +++ b/repos/effect/packages/effect/src/internal/context.ts @@ -0,0 +1,337 @@ +import type * as C from "../Context.js" +import * as Equal from "../Equal.js" +import type { LazyArg } from "../Function.js" +import { dual } from "../Function.js" +import { globalValue } from "../GlobalValue.js" +import * as Hash from "../Hash.js" +import { format, NodeInspectSymbol, toJSON } from "../Inspectable.js" +import type * as O from "../Option.js" +import { pipeArguments } from "../Pipeable.js" +import { hasProperty } from "../Predicate.js" +import type * as STM from "../STM.js" +import type { NoInfer } from "../Types.js" +import { EffectPrototype, effectVariance } from "./effectable.js" +import * as option from "./option.js" + +/** @internal */ +export const TagTypeId: C.TagTypeId = Symbol.for("effect/Context/Tag") as C.TagTypeId + +/** @internal */ +export const ReferenceTypeId: C.ReferenceTypeId = Symbol.for("effect/Context/Reference") as C.ReferenceTypeId + +/** @internal */ +const STMSymbolKey = "effect/STM" + +/** @internal */ +export const STMTypeId: STM.STMTypeId = Symbol.for( + STMSymbolKey +) as STM.STMTypeId + +/** @internal */ +export const TagProto: any = { + ...EffectPrototype, + _op: "Tag", + [STMTypeId]: effectVariance, + [TagTypeId]: { + _Service: (_: unknown) => _, + _Identifier: (_: unknown) => _ + }, + toString() { + return format(this.toJSON()) + }, + toJSON(this: C.Tag) { + return { + _id: "Tag", + key: this.key, + stack: this.stack + } + }, + [NodeInspectSymbol]() { + return this.toJSON() + }, + of(self: Service): Service { + return self + }, + context( + this: C.Tag, + self: Service + ): C.Context { + return make(this, self) + } +} + +export const ReferenceProto: any = { + ...TagProto, + [ReferenceTypeId]: ReferenceTypeId +} + +/** @internal */ +export const makeGenericTag = (key: string): C.Tag => { + const limit = Error.stackTraceLimit + Error.stackTraceLimit = 2 + const creationError = new Error() + Error.stackTraceLimit = limit + const tag = Object.create(TagProto) + Object.defineProperty(tag, "stack", { + get() { + return creationError.stack + } + }) + tag.key = key + return tag +} + +/** @internal */ +export const Tag = (id: Id) => (): C.TagClass => { + const limit = Error.stackTraceLimit + Error.stackTraceLimit = 2 + const creationError = new Error() + Error.stackTraceLimit = limit + + function TagClass() {} + Object.setPrototypeOf(TagClass, TagProto) + TagClass.key = id + Object.defineProperty(TagClass, "stack", { + get() { + return creationError.stack + } + }) + return TagClass as any +} + +/** @internal */ +export const Reference = () => +(id: Id, options: { + readonly defaultValue: () => Service +}): C.ReferenceClass => { + const limit = Error.stackTraceLimit + Error.stackTraceLimit = 2 + const creationError = new Error() + Error.stackTraceLimit = limit + + function ReferenceClass() {} + Object.setPrototypeOf(ReferenceClass, ReferenceProto) + ReferenceClass.key = id + ReferenceClass.defaultValue = options.defaultValue + Object.defineProperty(ReferenceClass, "stack", { + get() { + return creationError.stack + } + }) + return ReferenceClass as any +} + +/** @internal */ +export const TypeId: C.TypeId = Symbol.for("effect/Context") as C.TypeId + +/** @internal */ +export const ContextProto: Omit, "unsafeMap"> = { + [TypeId]: { + _Services: (_: unknown) => _ + }, + [Equal.symbol](this: C.Context, that: unknown): boolean { + if (isContext(that)) { + if (this.unsafeMap.size === that.unsafeMap.size) { + for (const k of this.unsafeMap.keys()) { + if (!that.unsafeMap.has(k) || !Equal.equals(this.unsafeMap.get(k), that.unsafeMap.get(k))) { + return false + } + } + return true + } + } + return false + }, + [Hash.symbol](this: C.Context): number { + return Hash.cached(this, Hash.number(this.unsafeMap.size)) + }, + pipe(this: C.Context) { + return pipeArguments(this, arguments) + }, + toString() { + return format(this.toJSON()) + }, + toJSON(this: C.Context) { + return { + _id: "Context", + services: Array.from(this.unsafeMap).map(toJSON) + } + }, + [NodeInspectSymbol]() { + return (this as any).toJSON() + } +} + +/** @internal */ +export const makeContext = (unsafeMap: Map): C.Context => { + const context = Object.create(ContextProto) + context.unsafeMap = unsafeMap + return context +} + +const serviceNotFoundError = (tag: C.Tag) => { + const error = new Error(`Service not found${tag.key ? `: ${String(tag.key)}` : ""}`) + if (tag.stack) { + const lines = tag.stack.split("\n") + if (lines.length > 2) { + const afterAt = lines[2].match(/at (.*)/) + if (afterAt) { + error.message = error.message + ` (defined at ${afterAt[1]})` + } + } + } + if (error.stack) { + const lines = error.stack.split("\n") + lines.splice(1, 3) + error.stack = lines.join("\n") + } + return error +} + +/** @internal */ +export const isContext = (u: unknown): u is C.Context => hasProperty(u, TypeId) + +/** @internal */ +export const isTag = (u: unknown): u is C.Tag => hasProperty(u, TagTypeId) + +/** @internal */ +export const isReference = (u: unknown): u is C.Reference => hasProperty(u, ReferenceTypeId) + +const _empty = makeContext(new Map()) + +/** @internal */ +export const empty = (): C.Context => _empty + +/** @internal */ +export const make = (tag: C.Tag, service: NoInfer): C.Context => + makeContext(new Map([[tag.key, service]])) + +/** @internal */ +export const add = dual< + ( + tag: C.Tag, + service: NoInfer + ) => ( + self: C.Context + ) => C.Context, + ( + self: C.Context, + tag: C.Tag, + service: NoInfer + ) => C.Context +>(3, (self, tag, service) => { + const map = new Map(self.unsafeMap) + map.set(tag.key, service) + return makeContext(map) +}) + +const defaultValueCache = globalValue("effect/Context/defaultValueCache", () => new Map()) +const getDefaultValue = (tag: C.Reference) => { + if (defaultValueCache.has(tag.key)) { + return defaultValueCache.get(tag.key) + } + const value = tag.defaultValue() + defaultValueCache.set(tag.key, value) + return value +} + +/** @internal */ +export const unsafeGetReference = (self: C.Context, tag: C.Reference): S => { + return self.unsafeMap.has(tag.key) ? self.unsafeMap.get(tag.key) : getDefaultValue(tag) +} + +/** @internal */ +export const unsafeGet = dual< + (tag: C.Tag) => (self: C.Context) => S, + (self: C.Context, tag: C.Tag) => S +>(2, (self, tag) => { + if (!self.unsafeMap.has(tag.key)) { + if (ReferenceTypeId in tag) return getDefaultValue(tag as any) + throw serviceNotFoundError(tag) + } + return self.unsafeMap.get(tag.key)! as any +}) + +/** @internal */ +export const get: { + (tag: C.Reference): (self: C.Context) => S + (tag: C.Tag): (self: C.Context) => S + (self: C.Context, tag: C.Reference): S + (self: C.Context, tag: C.Tag): S +} = unsafeGet as any + +/** @internal */ +export const getOrElse = dual< + (tag: C.Tag, orElse: LazyArg) => (self: C.Context) => S | B, + (self: C.Context, tag: C.Tag, orElse: LazyArg) => S | B +>(3, (self, tag, orElse) => { + if (!self.unsafeMap.has(tag.key)) { + return isReference(tag) ? getDefaultValue(tag) : orElse() + } + return self.unsafeMap.get(tag.key)! as any +}) + +/** @internal */ +export const getOption = dual< + (tag: C.Tag) => (self: C.Context) => O.Option, + (self: C.Context, tag: C.Tag) => O.Option +>(2, (self, tag) => { + if (!self.unsafeMap.has(tag.key)) { + return isReference(tag) ? option.some(getDefaultValue(tag)) : option.none + } + return option.some(self.unsafeMap.get(tag.key)! as any) +}) + +/** @internal */ +export const merge = dual< + (that: C.Context) => (self: C.Context) => C.Context, + (self: C.Context, that: C.Context) => C.Context +>(2, (self, that) => { + const map = new Map(self.unsafeMap) + for (const [tag, s] of that.unsafeMap) { + map.set(tag, s) + } + return makeContext(map) +}) + +/** @internal */ +export const mergeAll = >( + ...ctxs: [...{ [K in keyof T]: C.Context }] +): C.Context => { + const map = new Map() + for (let i = 0; i < ctxs.length; i++) { + ctxs[i].unsafeMap.forEach((value, key) => { + map.set(key, value) + }) + } + return makeContext(map) +} + +/** @internal */ +export const pick = + >>(...tags: Tags) => + (self: C.Context): C.Context< + Services & C.Tag.Identifier + > => { + const tagSet = new Set(tags.map((_) => _.key)) + const newEnv = new Map() + for (const [tag, s] of self.unsafeMap.entries()) { + if (tagSet.has(tag)) { + newEnv.set(tag, s) + } + } + return makeContext(newEnv) + } + +/** @internal */ +export const omit = + >>(...tags: Tags) => + (self: C.Context): C.Context< + Exclude> + > => { + const newEnv = new Map(self.unsafeMap) + for (const tag of tags) { + newEnv.delete(tag.key) + } + return makeContext(newEnv) + } diff --git a/repos/effect/packages/effect/src/internal/core-effect.ts b/repos/effect/packages/effect/src/internal/core-effect.ts new file mode 100644 index 0000000..0ef68fb --- /dev/null +++ b/repos/effect/packages/effect/src/internal/core-effect.ts @@ -0,0 +1,2304 @@ +import * as Arr from "../Array.js" +import type * as Cause from "../Cause.js" +import * as Chunk from "../Chunk.js" +import * as Clock from "../Clock.js" +import * as Context from "../Context.js" +import * as Duration from "../Duration.js" +import type * as Effect from "../Effect.js" +import type { Exit } from "../Exit.js" +import type * as Fiber from "../Fiber.js" +import type * as FiberId from "../FiberId.js" +import type * as FiberRef from "../FiberRef.js" +import * as FiberRefs from "../FiberRefs.js" +import type * as FiberRefsPatch from "../FiberRefsPatch.js" +import type { LazyArg } from "../Function.js" +import { constFalse, constTrue, constVoid, dual, identity, pipe } from "../Function.js" +import * as HashMap from "../HashMap.js" +import * as HashSet from "../HashSet.js" +import * as List from "../List.js" +import * as LogLevel from "../LogLevel.js" +import * as LogSpan from "../LogSpan.js" +import type * as Metric from "../Metric.js" +import type * as MetricLabel from "../MetricLabel.js" +import * as Option from "../Option.js" +import * as Predicate from "../Predicate.js" +import type * as Random from "../Random.js" +import * as Ref from "../Ref.js" +import type * as runtimeFlagsPatch from "../RuntimeFlagsPatch.js" +import * as Tracer from "../Tracer.js" +import type * as Types from "../Types.js" +import type { Unify } from "../Unify.js" +import { internalCall } from "../Utils.js" +import * as internalCause from "./cause.js" +import { clockTag } from "./clock.js" +import * as core from "./core.js" +import * as defaultServices from "./defaultServices.js" +import * as doNotation from "./doNotation.js" +import * as fiberRefsPatch from "./fiberRefs/patch.js" +import type { FiberRuntime } from "./fiberRuntime.js" +import * as metricLabel from "./metric/label.js" +import * as runtimeFlags from "./runtimeFlags.js" +import * as internalTracer from "./tracer.js" + +/* @internal */ +export const annotateLogs = dual< + { + (key: string, value: unknown): (effect: Effect.Effect) => Effect.Effect + ( + values: Record + ): (effect: Effect.Effect) => Effect.Effect + }, + { + (effect: Effect.Effect, key: string, value: unknown): Effect.Effect + (effect: Effect.Effect, values: Record): Effect.Effect + } +>( + (args) => core.isEffect(args[0]), + function() { + const args = arguments + return core.fiberRefLocallyWith( + args[0] as Effect.Effect, + core.currentLogAnnotations, + typeof args[1] === "string" + ? HashMap.set(args[1], args[2]) + : (annotations) => + Object.entries(args[1] as Record).reduce( + (acc, [key, value]) => HashMap.set(acc, key, value), + annotations + ) + ) + } +) + +/* @internal */ +export const asSome = (self: Effect.Effect): Effect.Effect, E, R> => + core.map(self, Option.some) + +/* @internal */ +export const asSomeError = (self: Effect.Effect): Effect.Effect, R> => + core.mapError(self, Option.some) + +/* @internal */ +export const try_: { + (options: { + readonly try: LazyArg + readonly catch: (error: unknown) => E + }): Effect.Effect + (thunk: LazyArg): Effect.Effect +} = ( + arg: LazyArg | { + readonly try: LazyArg + readonly catch: (error: unknown) => E + } +) => { + let evaluate: LazyArg + let onFailure: ((error: unknown) => E) | undefined = undefined + if (typeof arg === "function") { + evaluate = arg + } else { + evaluate = arg.try + onFailure = arg.catch + } + return core.suspend(() => { + try { + return core.succeed(internalCall(evaluate)) + } catch (error) { + return core.fail( + onFailure + ? internalCall(() => onFailure(error)) + : new core.UnknownException(error, "An unknown error occurred in Effect.try") + ) + } + }) +} + +/* @internal */ +export const _catch: { + ( + discriminator: N, + options: { + readonly failure: K + readonly onFailure: (error: Extract) => Effect.Effect + } + ): (self: Effect.Effect) => Effect.Effect< + A | A1, + Exclude | E1, + R | R1 + > + ( + self: Effect.Effect, + discriminator: N, + options: { + readonly failure: K + readonly onFailure: (error: Extract) => Effect.Effect + } + ): Effect.Effect | E1, R | R1> +} = dual( + 3, + (self, tag, options) => + core.catchAll(self, (e) => { + if (Predicate.hasProperty(e, tag) && e[tag] === options.failure) { + return options.onFailure(e) + } + return core.fail(e) + }) +) + +/* @internal */ +export const catchAllDefect = dual< + ( + f: (defect: unknown) => Effect.Effect + ) => (self: Effect.Effect) => Effect.Effect, + ( + self: Effect.Effect, + f: (defect: unknown) => Effect.Effect + ) => Effect.Effect +>(2, ( + self: Effect.Effect, + f: (defect: unknown) => Effect.Effect +): Effect.Effect => + core.catchAllCause( + self, + (cause): Effect.Effect => { + const option = internalCause.find(cause, (_) => internalCause.isDieType(_) ? Option.some(_) : Option.none()) + switch (option._tag) { + case "None": { + return core.failCause(cause) + } + case "Some": { + return f(option.value.defect) + } + } + } + )) + +/* @internal */ +export const catchSomeCause: { + ( + f: (cause: Cause.Cause>) => Option.Option> + ): (self: Effect.Effect) => Effect.Effect + ( + self: Effect.Effect, + f: (cause: Cause.Cause>) => Option.Option> + ): Effect.Effect +} = dual( + 2, + ( + self: Effect.Effect, + f: (cause: Cause.Cause>) => Option.Option> + ): Effect.Effect => + core.matchCauseEffect(self, { + onFailure: (cause): Effect.Effect => { + const option = f(cause) + switch (option._tag) { + case "None": { + return core.failCause(cause) + } + case "Some": { + return option.value + } + } + }, + onSuccess: core.succeed + }) +) + +/* @internal */ +export const catchSomeDefect = dual< + ( + pf: (defect: unknown) => Option.Option> + ) => (self: Effect.Effect) => Effect.Effect, + ( + self: Effect.Effect, + pf: (defect: unknown) => Option.Option> + ) => Effect.Effect +>( + 2, + ( + self: Effect.Effect, + pf: (defect: unknown) => Option.Option> + ): Effect.Effect => + core.catchAllCause( + self, + (cause): Effect.Effect => { + const option = internalCause.find(cause, (_) => internalCause.isDieType(_) ? Option.some(_) : Option.none()) + switch (option._tag) { + case "None": { + return core.failCause(cause) + } + case "Some": { + const optionEffect = pf(option.value.defect) + return optionEffect._tag === "Some" ? optionEffect.value : core.failCause(cause) + } + } + } + ) +) + +/* @internal */ +export const catchTag: { + < + E, + const K extends Arr.NonEmptyReadonlyArray, + A1, + E1, + R1 + >( + ...args: [ + ...tags: K, + f: (e: Extract, { _tag: K[number] }>) => Effect.Effect + ] + ): (self: Effect.Effect) => Effect.Effect | E1, R | R1> + < + A, + E, + R, + const K extends Arr.NonEmptyReadonlyArray, + A1, + E1, + R1 + >( + self: Effect.Effect, + ...args: [ + ...tags: K, + f: (e: Extract, { _tag: K[number] }>) => Effect.Effect + ] + ): Effect.Effect | E1, R | R1> +} = dual( + (args: any) => core.isEffect(args[0]), + , R1, E1, A1>( + self: Effect.Effect, + ...args: [ + ...tags: K & { [I in keyof K]: E extends { _tag: string } ? E["_tag"] : never }, + f: (e: Extract, { _tag: K[number] }>) => Effect.Effect + ] + ): Effect.Effect | E1, R | R1> => { + const f = args[args.length - 1] as any + let predicate: Predicate.Predicate + if (args.length === 2) { + predicate = Predicate.isTagged(args[0] as string) + } else { + predicate = (e) => { + const tag = Predicate.hasProperty(e, "_tag") ? e["_tag"] : undefined + if (!tag) return false + for (let i = 0; i < args.length - 1; i++) { + if (args[i] === tag) return true + } + return false + } + } + return core.catchIf(self, predicate as Predicate.Refinement>, f) as any + } +) as any + +/** @internal */ +export const catchTags: { + < + E, + Cases extends (E extends { _tag: string } ? { + [K in E["_tag"]]+?: (error: Extract) => Effect.Effect + } : + {}) + >( + cases: Cases + ): (self: Effect.Effect) => Effect.Effect< + | A + | { + [K in keyof Cases]: Cases[K] extends ((...args: Array) => Effect.Effect) ? A : never + }[keyof Cases], + | Exclude + | { + [K in keyof Cases]: Cases[K] extends ((...args: Array) => Effect.Effect) ? E : never + }[keyof Cases], + | R + | { + [K in keyof Cases]: Cases[K] extends ((...args: Array) => Effect.Effect) ? R : never + }[keyof Cases] + > + < + R, + E, + A, + Cases extends (E extends { _tag: string } ? { + [K in E["_tag"]]+?: (error: Extract) => Effect.Effect + } : + {}) + >( + self: Effect.Effect, + cases: Cases + ): Effect.Effect< + | A + | { + [K in keyof Cases]: Cases[K] extends ((...args: Array) => Effect.Effect) ? A : never + }[keyof Cases], + | Exclude + | { + [K in keyof Cases]: Cases[K] extends ((...args: Array) => Effect.Effect) ? E : never + }[keyof Cases], + | R + | { + [K in keyof Cases]: Cases[K] extends ((...args: Array) => Effect.Effect) ? R : never + }[keyof Cases] + > +} = dual(2, (self, cases) => { + let keys: Array + return core.catchIf( + self, + (e): e is { readonly _tag: string } => { + keys ??= Object.keys(cases) + return Predicate.hasProperty(e, "_tag") && Predicate.isString(e["_tag"]) && keys.includes(e["_tag"]) + }, + (e) => cases[e["_tag"]](e) + ) +}) + +/* @internal */ +export const cause = (self: Effect.Effect): Effect.Effect, never, R> => + core.matchCause(self, { onFailure: identity, onSuccess: () => internalCause.empty }) + +/* @internal */ +export const clockWith: (f: (clock: Clock.Clock) => Effect.Effect) => Effect.Effect = + Clock.clockWith + +/* @internal */ +export const clock: Effect.Effect = clockWith(core.succeed) + +/* @internal */ +export const delay = dual< + (duration: Duration.DurationInput) => (self: Effect.Effect) => Effect.Effect, + (self: Effect.Effect, duration: Duration.DurationInput) => Effect.Effect +>(2, (self, duration) => core.zipRight(Clock.sleep(duration), self)) + +/* @internal */ +export const descriptorWith = ( + f: (descriptor: Fiber.Fiber.Descriptor) => Effect.Effect +): Effect.Effect => + core.withFiberRuntime((state, status) => + f({ + id: state.id(), + status, + interruptors: internalCause.interruptors(state.getFiberRef(core.currentInterruptedCause)) + }) + ) as Effect.Effect + +/* @internal */ +export const allowInterrupt: Effect.Effect = descriptorWith( + (descriptor) => + HashSet.size(descriptor.interruptors) > 0 + ? core.interrupt + : core.void +) + +/* @internal */ +export const descriptor: Effect.Effect = descriptorWith(core.succeed) + +/* @internal */ +export const diffFiberRefs = ( + self: Effect.Effect +): Effect.Effect<[FiberRefsPatch.FiberRefsPatch, A], E, R> => summarized(self, fiberRefs, fiberRefsPatch.diff) + +/* @internal */ +export const diffFiberRefsAndRuntimeFlags = ( + self: Effect.Effect +): Effect.Effect<[[FiberRefsPatch.FiberRefsPatch, runtimeFlagsPatch.RuntimeFlagsPatch], A], E, R> => + summarized( + self, + core.zip(fiberRefs, core.runtimeFlags), + ([refs, flags], [refsNew, flagsNew]) => [fiberRefsPatch.diff(refs, refsNew), runtimeFlags.diff(flags, flagsNew)] + ) + +/* @internal */ +export const Do: Effect.Effect<{}> = core.succeed({}) + +/* @internal */ +export const bind: { + ( + name: Exclude, + f: (a: Types.NoInfer) => Effect.Effect + ): ( + self: Effect.Effect + ) => Effect.Effect<{ [K in N | keyof A]: K extends keyof A ? A[K] : B }, E2 | E1, R2 | R1> + ( + self: Effect.Effect, + name: Exclude, + f: (a: Types.NoInfer) => Effect.Effect + ): Effect.Effect<{ [K in N | keyof A]: K extends keyof A ? A[K] : B }, E1 | E2, R1 | R2> +} = doNotation.bind(core.map, core.flatMap) + +/* @internal */ +export const bindTo: { + (name: N): (self: Effect.Effect) => Effect.Effect<{ [K in N]: A }, E, R> + (self: Effect.Effect, name: N): Effect.Effect<{ [K in N]: A }, E, R> +} = doNotation.bindTo(core.map) + +/* @internal */ +export const let_: { + ( + name: Exclude, + f: (a: Types.NoInfer) => B + ): ( + self: Effect.Effect + ) => Effect.Effect<{ [K in N | keyof A]: K extends keyof A ? A[K] : B }, E, R> + ( + self: Effect.Effect, + name: Exclude, + f: (a: Types.NoInfer) => B + ): Effect.Effect<{ [K in N | keyof A]: K extends keyof A ? A[K] : B }, E, R> +} = doNotation.let_(core.map) + +/* @internal */ +export const dropUntil: { + ( + predicate: (a: Types.NoInfer, i: number) => Effect.Effect + ): (elements: Iterable) => Effect.Effect, E, R> + ( + elements: Iterable, + predicate: (a: A, i: number) => Effect.Effect + ): Effect.Effect, E, R> +} = dual( + 2, + ( + elements: Iterable, + predicate: (a: A, i: number) => Effect.Effect + ): Effect.Effect, E, R> => + core.suspend(() => { + const iterator = elements[Symbol.iterator]() + const builder: Array = [] + let next: IteratorResult + let dropping: Effect.Effect = core.succeed(false) + let i = 0 + while ((next = iterator.next()) && !next.done) { + const a = next.value + const index = i++ + dropping = core.flatMap(dropping, (bool) => { + if (bool) { + builder.push(a) + return core.succeed(true) + } + return predicate(a, index) + }) + } + return core.map(dropping, () => builder) + }) +) + +/* @internal */ +export const dropWhile: { + ( + predicate: (a: Types.NoInfer, i: number) => Effect.Effect + ): (elements: Iterable) => Effect.Effect, E, R> + ( + elements: Iterable, + predicate: (a: A, i: number) => Effect.Effect + ): Effect.Effect, E, R> +} = dual( + 2, + ( + elements: Iterable, + predicate: (a: A, i: number) => Effect.Effect + ): Effect.Effect, E, R> => + core.suspend(() => { + const iterator = elements[Symbol.iterator]() + const builder: Array = [] + let next + let dropping: Effect.Effect = core.succeed(true) + let i = 0 + while ((next = iterator.next()) && !next.done) { + const a = next.value + const index = i++ + dropping = core.flatMap(dropping, (d) => + core.map(d ? predicate(a, index) : core.succeed(false), (b) => { + if (!b) { + builder.push(a) + } + return b + })) + } + return core.map(dropping, () => builder) + }) +) + +/* @internal */ +export const contextWith = (f: (context: Context.Context) => A): Effect.Effect => + core.map(core.context(), f) + +/* @internal */ +export const eventually = (self: Effect.Effect): Effect.Effect => + core.orElse(self, () => core.flatMap(core.yieldNow(), () => eventually(self))) + +/* @internal */ +export const filterMap = dual< + , B>( + pf: (a: Effect.Effect.Success) => Option.Option + ) => (elements: Iterable) => Effect.Effect, Effect.Effect.Error, Effect.Effect.Context>, + , B>( + elements: Iterable, + pf: (a: Effect.Effect.Success) => Option.Option + ) => Effect.Effect, Effect.Effect.Error, Effect.Effect.Context> +>(2, (elements, pf) => + core.map( + core.forEachSequential(elements, identity), + Arr.filterMap(pf) + )) + +/* @internal */ +export const filterOrDie: { + ( + refinement: Predicate.Refinement, B>, + orDieWith: (a: Types.EqualsWith>) => unknown + ): (self: Effect.Effect) => Effect.Effect + ( + predicate: Predicate.Predicate>, + orDieWith: (a: Types.NoInfer) => unknown + ): (self: Effect.Effect) => Effect.Effect + ( + self: Effect.Effect, + refinement: Predicate.Refinement, + orDieWith: (a: Types.EqualsWith>) => unknown + ): Effect.Effect + ( + self: Effect.Effect, + predicate: Predicate.Predicate, + orDieWith: (a: A) => unknown + ): Effect.Effect +} = dual( + 3, + ( + self: Effect.Effect, + predicate: Predicate.Predicate, + orDieWith: (a: A) => unknown + ): Effect.Effect => filterOrElse(self, predicate, (a) => core.dieSync(() => orDieWith(a))) +) + +/* @internal */ +export const filterOrDieMessage: { + ( + refinement: Predicate.Refinement, B>, + message: string + ): (self: Effect.Effect) => Effect.Effect + ( + predicate: Predicate.Predicate>, + message: string + ): (self: Effect.Effect) => Effect.Effect + ( + self: Effect.Effect, + refinement: Predicate.Refinement, + message: string + ): Effect.Effect + ( + self: Effect.Effect, + predicate: Predicate.Predicate, + message: string + ): Effect.Effect +} = dual( + 3, + (self: Effect.Effect, predicate: Predicate.Predicate, message: string): Effect.Effect => + filterOrElse(self, predicate, () => core.dieMessage(message)) +) + +/* @internal */ +export const filterOrElse: { + ( + refinement: Predicate.Refinement, B>, + orElse: (a: Types.EqualsWith, Exclude, B>>) => Effect.Effect + ): (self: Effect.Effect) => Effect.Effect + ( + predicate: Predicate.Predicate>, + orElse: (a: Types.NoInfer) => Effect.Effect + ): (self: Effect.Effect) => Effect.Effect + ( + self: Effect.Effect, + refinement: Predicate.Refinement, + orElse: (a: Types.EqualsWith>) => Effect.Effect + ): Effect.Effect + ( + self: Effect.Effect, + predicate: Predicate.Predicate, + orElse: (a: A) => Effect.Effect + ): Effect.Effect +} = dual(3, ( + self: Effect.Effect, + predicate: Predicate.Predicate, + orElse: (a: A) => Effect.Effect +): Effect.Effect => + core.flatMap( + self, + (a) => predicate(a) ? core.succeed(a) : orElse(a) + )) + +/** @internal */ +export const liftPredicate = dual< + ( + predicate: Predicate.Refinement | Predicate.Predicate, + orFailWith: (a: Types.EqualsWith>) => E + ) => (a: A) => Effect.Effect, E>, + ( + self: A, + predicate: Predicate.Refinement | Predicate.Predicate, + orFailWith: (a: Types.EqualsWith>) => E + ) => Effect.Effect +>( + 3, + ( + self: A, + predicate: Predicate.Refinement | Predicate.Predicate, + orFailWith: (a: Types.EqualsWith>) => E + ): Effect.Effect => + core.suspend(() => predicate(self) ? core.succeed(self as B) : core.fail(orFailWith(self as any))) +) + +/* @internal */ +export const filterOrFail: { + ( + refinement: Predicate.Refinement, B>, + orFailWith: (a: Types.EqualsWith, Exclude, B>>) => E2 + ): (self: Effect.Effect) => Effect.Effect, E2 | E, R> + ( + predicate: Predicate.Predicate>, + orFailWith: (a: Types.NoInfer) => E2 + ): (self: Effect.Effect) => Effect.Effect + ( + self: Effect.Effect, + refinement: Predicate.Refinement, + orFailWith: (a: Types.EqualsWith>) => E2 + ): Effect.Effect, E2 | E, R> + ( + self: Effect.Effect, + predicate: Predicate.Predicate, + orFailWith: (a: A) => E2 + ): Effect.Effect + ( + refinement: Predicate.Refinement, B> + ): (self: Effect.Effect) => Effect.Effect, Cause.NoSuchElementException | E, R> + ( + predicate: Predicate.Predicate> + ): (self: Effect.Effect) => Effect.Effect + ( + self: Effect.Effect, + refinement: Predicate.Refinement + ): Effect.Effect, E | Cause.NoSuchElementException, R> + ( + self: Effect.Effect, + predicate: Predicate.Predicate + ): Effect.Effect +} = dual((args) => core.isEffect(args[0]), ( + self: Effect.Effect, + predicate: Predicate.Predicate, + orFailWith?: (a: A) => E2 +): Effect.Effect => + filterOrElse( + self, + predicate, + (a): Effect.Effect => + orFailWith === undefined ? core.fail(new core.NoSuchElementException()) : core.failSync(() => orFailWith(a)) + )) + +/* @internal */ +export const findFirst: { + ( + predicate: (a: Types.NoInfer, i: number) => Effect.Effect + ): (elements: Iterable) => Effect.Effect, E, R> + ( + elements: Iterable, + predicate: (a: Types.NoInfer, i: number) => Effect.Effect + ): Effect.Effect, E, R> +} = dual( + 2, + ( + elements: Iterable, + predicate: (a: Types.NoInfer, i: number) => Effect.Effect + ): Effect.Effect, E, R> => + core.suspend(() => { + const iterator = elements[Symbol.iterator]() + const next = iterator.next() + if (!next.done) { + return findLoop(iterator, 0, predicate, next.value) + } + return core.succeed(Option.none()) + }) +) + +const findLoop = ( + iterator: Iterator, + index: number, + f: (a: A, i: number) => Effect.Effect, + value: A +): Effect.Effect, E, R> => + core.flatMap(f(value, index), (result) => { + if (result) { + return core.succeed(Option.some(value)) + } + const next = iterator.next() + if (!next.done) { + return findLoop(iterator, index + 1, f, next.value) + } + return core.succeed(Option.none()) + }) + +/* @internal */ +export const firstSuccessOf = >( + effects: Iterable +): Effect.Effect, Effect.Effect.Error, Effect.Effect.Context> => + core.suspend(() => { + const list = Chunk.fromIterable(effects) + if (!Chunk.isNonEmpty(list)) { + return core.dieSync(() => new core.IllegalArgumentException(`Received an empty collection of effects`)) + } + return pipe( + Chunk.tailNonEmpty(list), + Arr.reduce(Chunk.headNonEmpty(list), (left, right) => core.orElse(left, () => right) as Eff) + ) + }) + +/* @internal */ +export const flipWith: { + ( + f: (effect: Effect.Effect) => Effect.Effect + ): (self: Effect.Effect) => Effect.Effect + ( + self: Effect.Effect, + f: (effect: Effect.Effect) => Effect.Effect + ): Effect.Effect +} = dual(2, ( + self: Effect.Effect, + f: (effect: Effect.Effect) => Effect.Effect +): Effect.Effect => core.flip(f(core.flip(self)))) + +/* @internal */ +export const match: { + ( + options: { + readonly onFailure: (error: E) => A2 + readonly onSuccess: (value: A) => A3 + } + ): (self: Effect.Effect) => Effect.Effect + ( + self: Effect.Effect, + options: { + readonly onFailure: (error: E) => A2 + readonly onSuccess: (value: A) => A3 + } + ): Effect.Effect +} = dual(2, ( + self: Effect.Effect, + options: { + readonly onFailure: (error: E) => A2 + readonly onSuccess: (value: A) => A3 + } +): Effect.Effect => + core.matchEffect(self, { + onFailure: (e) => core.succeed(options.onFailure(e)), + onSuccess: (a) => core.succeed(options.onSuccess(a)) + })) + +/* @internal */ +export const every: { + ( + predicate: (a: A, i: number) => Effect.Effect + ): (elements: Iterable) => Effect.Effect + ( + elements: Iterable, + predicate: (a: A, i: number) => Effect.Effect + ): Effect.Effect +} = dual( + 2, + ( + elements: Iterable, + predicate: (a: A, i: number) => Effect.Effect + ): Effect.Effect => core.suspend(() => forAllLoop(elements[Symbol.iterator](), 0, predicate)) +) + +const forAllLoop = ( + iterator: Iterator, + index: number, + f: (a: A, i: number) => Effect.Effect +): Effect.Effect => { + const next = iterator.next() + return next.done + ? core.succeed(true) + : core.flatMap( + f(next.value, index), + (b) => b ? forAllLoop(iterator, index + 1, f) : core.succeed(b) + ) +} + +/* @internal */ +export const forever = (self: Effect.Effect): Effect.Effect => { + const loop: Effect.Effect = core.flatMap(core.flatMap(self, () => core.yieldNow()), () => loop) + return loop +} + +/* @internal */ +export const fiberRefs: Effect.Effect = core.withFiberRuntime((state) => + core.succeed(state.getFiberRefs()) +) + +/* @internal */ +export const head = ( + self: Effect.Effect, E, R> +): Effect.Effect => + core.flatMap(self, (as) => { + const iterator = as[Symbol.iterator]() + const next = iterator.next() + if (next.done) { + return core.fail(new core.NoSuchElementException()) + } + return core.succeed(next.value) + }) + +/* @internal */ +export const ignore = (self: Effect.Effect): Effect.Effect => + match(self, { onFailure: constVoid, onSuccess: constVoid }) + +/* @internal */ +export const ignoreLogged = (self: Effect.Effect): Effect.Effect => + core.matchCauseEffect(self, { + onFailure: (cause) => logDebug(cause, "An error was silently ignored because it is not anticipated to be useful"), + onSuccess: () => core.void + }) + +/* @internal */ +export const inheritFiberRefs = (childFiberRefs: FiberRefs.FiberRefs) => + updateFiberRefs((parentFiberId, parentFiberRefs) => FiberRefs.joinAs(parentFiberRefs, parentFiberId, childFiberRefs)) + +/* @internal */ +export const isFailure = (self: Effect.Effect): Effect.Effect => + match(self, { onFailure: constTrue, onSuccess: constFalse }) + +/* @internal */ +export const isSuccess = (self: Effect.Effect): Effect.Effect => + match(self, { onFailure: constFalse, onSuccess: constTrue }) + +/* @internal */ +export const iterate: { + ( + initial: A, + options: { + readonly while: Predicate.Refinement + readonly body: (b: B) => Effect.Effect + } + ): Effect.Effect + ( + initial: A, + options: { + readonly while: Predicate.Predicate + readonly body: (a: A) => Effect.Effect + } + ): Effect.Effect +} = ( + initial: A, + options: { + readonly while: Predicate.Predicate + readonly body: (z: A) => Effect.Effect + } +): Effect.Effect => + core.suspend(() => { + if (options.while(initial)) { + return core.flatMap(options.body(initial), (z2) => iterate(z2, options)) + } + return core.succeed(initial) + }) + +/** @internal */ +export const logWithLevel = (level?: LogLevel.LogLevel) => +( + ...message: ReadonlyArray +): Effect.Effect => { + const levelOption = Option.fromNullable(level) + let cause: Cause.Cause | undefined = undefined + for (let i = 0, len = message.length; i < len; i++) { + const msg = message[i] + if (internalCause.isCause(msg)) { + if (cause !== undefined) { + cause = internalCause.sequential(cause, msg) + } else { + cause = msg + } + message = [...message.slice(0, i), ...message.slice(i + 1)] + i-- + } + } + if (cause === undefined) { + cause = internalCause.empty + } + return core.withFiberRuntime((fiberState) => { + fiberState.log(message, cause, levelOption) + return core.void + }) +} + +/** @internal */ +export const log: (...message: ReadonlyArray) => Effect.Effect = logWithLevel() + +/** @internal */ +export const logTrace: (...message: ReadonlyArray) => Effect.Effect = logWithLevel( + LogLevel.Trace +) + +/** @internal */ +export const logDebug: (...message: ReadonlyArray) => Effect.Effect = logWithLevel( + LogLevel.Debug +) + +/** @internal */ +export const logInfo: (...message: ReadonlyArray) => Effect.Effect = logWithLevel( + LogLevel.Info +) + +/** @internal */ +export const logWarning: (...message: ReadonlyArray) => Effect.Effect = logWithLevel( + LogLevel.Warning +) + +/** @internal */ +export const logError: (...message: ReadonlyArray) => Effect.Effect = logWithLevel( + LogLevel.Error +) + +/** @internal */ +export const logFatal: (...message: ReadonlyArray) => Effect.Effect = logWithLevel( + LogLevel.Fatal +) + +/* @internal */ +export const withLogSpan = dual< + (label: string) => (effect: Effect.Effect) => Effect.Effect, + (effect: Effect.Effect, label: string) => Effect.Effect +>(2, (effect, label) => + core.flatMap(Clock.currentTimeMillis, (now) => + core.fiberRefLocallyWith( + effect, + core.currentLogSpan, + List.prepend(LogSpan.make(label, now)) + ))) + +/* @internal */ +export const logAnnotations: Effect.Effect> = core + .fiberRefGet( + core.currentLogAnnotations + ) + +/* @internal */ +export const loop: { + ( + initial: A, + options: { + readonly while: Predicate.Refinement + readonly step: (b: B) => A + readonly body: (b: B) => Effect.Effect + readonly discard?: false | undefined + } + ): Effect.Effect, E, R> + ( + initial: A, + options: { + readonly while: (a: A) => boolean + readonly step: (a: A) => A + readonly body: (a: A) => Effect.Effect + readonly discard?: false | undefined + } + ): Effect.Effect, E, R> + ( + initial: A, + options: { + readonly while: Predicate.Refinement + readonly step: (b: B) => A + readonly body: (b: B) => Effect.Effect + readonly discard: true + } + ): Effect.Effect + ( + initial: A, + options: { + readonly while: (a: A) => boolean + readonly step: (a: A) => A + readonly body: (a: A) => Effect.Effect + readonly discard: true + } + ): Effect.Effect +} = ( + initial: A, + options: { + readonly while: Predicate.Predicate + readonly step: (a: A) => A + readonly body: (a: A) => Effect.Effect + readonly discard?: boolean | undefined + } +): any => + options.discard + ? loopDiscard(initial, options.while, options.step, options.body) + : core.map(loopInternal(initial, options.while, options.step, options.body), Arr.fromIterable) + +const loopInternal = ( + initial: Z, + cont: Predicate.Predicate, + inc: (z: Z) => Z, + body: (z: Z) => Effect.Effect +): Effect.Effect, E, R> => + core.suspend(() => + cont(initial) + ? core.flatMap(body(initial), (a) => + core.map( + loopInternal(inc(initial), cont, inc, body), + List.prepend(a) + )) + : core.sync(() => List.empty()) + ) + +const loopDiscard = ( + initial: S, + cont: Predicate.Predicate, + inc: (s: S) => S, + body: (s: S) => Effect.Effect +): Effect.Effect => + core.suspend(() => + cont(initial) + ? core.flatMap( + body(initial), + () => loopDiscard(inc(initial), cont, inc, body) + ) + : core.void + ) + +/* @internal */ +export const mapAccum: { + = Iterable>( + initial: S, + f: (state: S, a: A, i: number) => Effect.Effect + ): (elements: I) => Effect.Effect<[S, Arr.ReadonlyArray.With], E, R> + = Iterable>( + elements: I, + initial: S, + f: (state: S, a: A, i: number) => Effect.Effect + ): Effect.Effect<[S, Arr.ReadonlyArray.With], E, R> +} = dual(3, = Iterable>( + elements: I, + initial: S, + f: (state: S, a: A, i: number) => Effect.Effect +): Effect.Effect<[S, Array], E, R> => + core.suspend(() => { + const iterator = elements[Symbol.iterator]() + const builder: Array = [] + let result: Effect.Effect = core.succeed(initial) + let next: IteratorResult + let i = 0 + while (!(next = iterator.next()).done) { + const index = i++ + const value = next.value + result = core.flatMap(result, (state) => + core.map(f(state, value, index), ([z, b]) => { + builder.push(b) + return z + })) + } + return core.map(result, (z) => [z, builder]) + })) + +/* @internal */ +export const mapErrorCause: { + ( + f: (cause: Cause.Cause) => Cause.Cause + ): (self: Effect.Effect) => Effect.Effect + (self: Effect.Effect, f: (cause: Cause.Cause) => Cause.Cause): Effect.Effect +} = dual( + 2, + (self: Effect.Effect, f: (cause: Cause.Cause) => Cause.Cause): Effect.Effect => + core.matchCauseEffect(self, { + onFailure: (c) => core.failCauseSync(() => f(c)), + onSuccess: core.succeed + }) +) + +/* @internal */ +export const memoize = ( + self: Effect.Effect +): Effect.Effect> => + pipe( + core.deferredMake<[[FiberRefsPatch.FiberRefsPatch, runtimeFlagsPatch.RuntimeFlagsPatch], A], E>(), + core.flatMap((deferred) => + pipe( + diffFiberRefsAndRuntimeFlags(self), + core.intoDeferred(deferred), + once, + core.map((complete) => + core.zipRight( + complete, + pipe( + core.deferredAwait(deferred), + core.flatMap(([patch, a]) => + core.as(core.zip(patchFiberRefs(patch[0]), core.updateRuntimeFlags(patch[1])), a) + ) + ) + ) + ) + ) + ) + ) + +/* @internal */ +export const merge = (self: Effect.Effect): Effect.Effect => + core.matchEffect(self, { + onFailure: (e) => core.succeed(e), + onSuccess: core.succeed + }) + +/* @internal */ +export const negate = (self: Effect.Effect): Effect.Effect => + core.map(self, (b) => !b) + +/* @internal */ +export const none = ( + self: Effect.Effect, E, R> +): Effect.Effect => + core.flatMap(self, (option) => { + switch (option._tag) { + case "None": + return core.void + case "Some": + return core.fail(new core.NoSuchElementException()) + } + }) + +/* @internal */ +export const once = ( + self: Effect.Effect +): Effect.Effect> => + core.map( + Ref.make(true), + (ref) => core.asVoid(core.whenEffect(self, Ref.getAndSet(ref, false))) + ) + +/* @internal */ +export const option = (self: Effect.Effect): Effect.Effect, never, R> => + core.matchEffect(self, { + onFailure: () => core.succeed(Option.none()), + onSuccess: (a) => core.succeed(Option.some(a)) + }) + +/* @internal */ +export const orElseFail = dual< + (evaluate: LazyArg) => (self: Effect.Effect) => Effect.Effect, + (self: Effect.Effect, evaluate: LazyArg) => Effect.Effect +>(2, (self, evaluate) => core.orElse(self, () => core.failSync(evaluate))) + +/* @internal */ +export const orElseSucceed = dual< + (evaluate: LazyArg) => (self: Effect.Effect) => Effect.Effect, + (self: Effect.Effect, evaluate: LazyArg) => Effect.Effect +>(2, (self, evaluate) => core.orElse(self, () => core.sync(evaluate))) + +/* @internal */ +export const parallelErrors = (self: Effect.Effect): Effect.Effect, R> => + core.matchCauseEffect(self, { + onFailure: (cause) => { + const errors = Arr.fromIterable(internalCause.failures(cause)) + return errors.length === 0 + ? core.failCause(cause as Cause.Cause) + : core.fail(errors) + }, + onSuccess: core.succeed + }) + +/* @internal */ +export const patchFiberRefs = (patch: FiberRefsPatch.FiberRefsPatch): Effect.Effect => + updateFiberRefs((fiberId, fiberRefs) => pipe(patch, fiberRefsPatch.patch(fiberId, fiberRefs))) + +/* @internal */ +export const promise = (evaluate: (signal: AbortSignal) => PromiseLike): Effect.Effect => + evaluate.length >= 1 + ? core.async((resolve, signal) => { + try { + evaluate(signal) + .then((a) => resolve(core.succeed(a)), (e) => resolve(core.die(e))) + } catch (e) { + resolve(core.die(e)) + } + }) + : core.async((resolve) => { + try { + ;(evaluate as LazyArg>)() + .then((a) => resolve(core.succeed(a)), (e) => resolve(core.die(e))) + } catch (e) { + resolve(core.die(e)) + } + }) + +/* @internal */ +export const provideService = dual< + ( + tag: Context.Tag, + service: Types.NoInfer + ) => (self: Effect.Effect) => Effect.Effect>, + ( + self: Effect.Effect, + tag: Context.Tag, + service: Types.NoInfer + ) => Effect.Effect> +>( + 3, + ( + self: Effect.Effect, + tag: Context.Tag, + service: Types.NoInfer + ): Effect.Effect> => + core.contextWithEffect((env) => + core.provideContext( + self as Effect.Effect>, + Context.add(env, tag, service) + ) + ) +) + +/* @internal */ +export const provideServiceEffect = dual< + ( + tag: Context.Tag, + effect: Effect.Effect, E1, R1> + ) => (self: Effect.Effect) => Effect.Effect>, + ( + self: Effect.Effect, + tag: Context.Tag, + effect: Effect.Effect, E1, R1> + ) => Effect.Effect> +>(3, ( + self: Effect.Effect, + tag: Context.Tag, + effect: Effect.Effect, E1, R1> +) => + core.contextWithEffect((env: Context.Context>) => + core.flatMap( + effect, + (service) => core.provideContext(self, pipe(env, Context.add(tag, service)) as Context.Context) + ) + )) + +/* @internal */ +export const random: Effect.Effect = defaultServices.randomWith(core.succeed) + +/* @internal */ +export const reduce = dual< + ( + zero: Z, + f: (z: Z, a: A, i: number) => Effect.Effect + ) => (elements: Iterable) => Effect.Effect, + ( + elements: Iterable, + zero: Z, + f: (z: Z, a: A, i: number) => Effect.Effect + ) => Effect.Effect +>( + 3, + ( + elements: Iterable, + zero: Z, + f: (z: Z, a: A, i: number) => Effect.Effect + ) => + Arr.fromIterable(elements).reduce( + (acc, el, i) => core.flatMap(acc, (a) => f(a, el, i)), + core.succeed(zero) as Effect.Effect + ) +) + +/* @internal */ +export const reduceRight = dual< + ( + zero: Z, + f: (a: A, z: Z, i: number) => Effect.Effect + ) => (elements: Iterable) => Effect.Effect, + ( + elements: Iterable, + zero: Z, + f: (a: A, z: Z, i: number) => Effect.Effect + ) => Effect.Effect +>( + 3, + (elements: Iterable, zero: Z, f: (a: A, z: Z, i: number) => Effect.Effect) => + Arr.fromIterable(elements).reduceRight( + (acc, el, i) => core.flatMap(acc, (a) => f(el, a, i)), + core.succeed(zero) as Effect.Effect + ) +) + +/* @internal */ +export const reduceWhile = dual< + ( + zero: Z, + options: { + readonly while: Predicate.Predicate + readonly body: (s: Z, a: A, i: number) => Effect.Effect + } + ) => (elements: Iterable) => Effect.Effect, + ( + elements: Iterable, + zero: Z, + options: { + readonly while: Predicate.Predicate + readonly body: (s: Z, a: A, i: number) => Effect.Effect + } + ) => Effect.Effect +>(3, ( + elements: Iterable, + zero: Z, + options: { + readonly while: Predicate.Predicate + readonly body: (s: Z, a: A, i: number) => Effect.Effect + } +) => + core.flatMap( + core.sync(() => elements[Symbol.iterator]()), + (iterator) => reduceWhileLoop(iterator, 0, zero, options.while, options.body) + )) + +const reduceWhileLoop = ( + iterator: Iterator, + index: number, + state: Z, + predicate: Predicate.Predicate, + f: (s: Z, a: A, i: number) => Effect.Effect +): Effect.Effect => { + const next = iterator.next() + if (!next.done && predicate(state)) { + return core.flatMap( + f(state, next.value, index), + (nextState) => reduceWhileLoop(iterator, index + 1, nextState, predicate, f) + ) + } + return core.succeed(state) +} + +/* @internal */ +export const repeatN = dual< + (n: number) => (self: Effect.Effect) => Effect.Effect, + (self: Effect.Effect, n: number) => Effect.Effect +>(2, (self, n) => core.suspend(() => repeatNLoop(self, n))) + +/* @internal */ +const repeatNLoop = (self: Effect.Effect, n: number): Effect.Effect => + core.flatMap(self, (a) => + n <= 0 + ? core.succeed(a) + : core.zipRight(core.yieldNow(), repeatNLoop(self, n - 1))) + +/* @internal */ +export const sandbox = (self: Effect.Effect): Effect.Effect, R> => + core.matchCauseEffect(self, { + onFailure: core.fail, + onSuccess: core.succeed + }) + +/* @internal */ +export const setFiberRefs = (fiberRefs: FiberRefs.FiberRefs): Effect.Effect => + core.suspend(() => FiberRefs.setAll(fiberRefs)) + +/* @internal */ +export const sleep: (duration: Duration.DurationInput) => Effect.Effect = Clock.sleep + +/* @internal */ +export const succeedNone: Effect.Effect> = core.succeed(Option.none()) + +/* @internal */ +export const succeedSome = (value: A): Effect.Effect> => core.succeed(Option.some(value)) + +/* @internal */ +export const summarized: { + ( + summary: Effect.Effect, + f: (start: B, end: B) => C + ): (self: Effect.Effect) => Effect.Effect<[C, A], E2 | E, R2 | R> + ( + self: Effect.Effect, + summary: Effect.Effect, + f: (start: B, end: B) => C + ): Effect.Effect<[C, A], E2 | E, R2 | R> +} = dual( + 3, + ( + self: Effect.Effect, + summary: Effect.Effect, + f: (start: B, end: B) => C + ): Effect.Effect<[C, A], E2 | E, R2 | R> => + core.flatMap( + summary, + (start) => core.flatMap(self, (value) => core.map(summary, (end) => [f(start, end), value])) + ) +) + +/* @internal */ +export const tagMetrics = dual< + { + (key: string, value: string): (effect: Effect.Effect) => Effect.Effect + ( + values: Record + ): (effect: Effect.Effect) => Effect.Effect + }, + { + (effect: Effect.Effect, key: string, value: string): Effect.Effect + (effect: Effect.Effect, values: Record): Effect.Effect + } +>((args) => core.isEffect(args[0]), function() { + return labelMetrics( + arguments[0], + typeof arguments[1] === "string" + ? [metricLabel.make(arguments[1], arguments[2])] + : Object.entries(arguments[1]).map(([k, v]) => metricLabel.make(k, v)) + ) +}) + +/* @internal */ +export const labelMetrics = dual< + (labels: Iterable) => (self: Effect.Effect) => Effect.Effect, + (self: Effect.Effect, labels: Iterable) => Effect.Effect +>( + 2, + (self, labels) => core.fiberRefLocallyWith(self, core.currentMetricLabels, (old) => Arr.union(old, labels)) +) + +/* @internal */ +export const takeUntil: { + ( + predicate: (a: Types.NoInfer, i: number) => Effect.Effect + ): (elements: Iterable) => Effect.Effect, E, R> + ( + elements: Iterable, + predicate: (a: Types.NoInfer, i: number) => Effect.Effect + ): Effect.Effect, E, R> +} = dual( + 2, + ( + elements: Iterable, + predicate: (a: Types.NoInfer, i: number) => Effect.Effect + ): Effect.Effect, E, R> => + core.suspend(() => { + const iterator = elements[Symbol.iterator]() + const builder: Array = [] + let next: IteratorResult + let effect: Effect.Effect = core.succeed(false) + let i = 0 + while ((next = iterator.next()) && !next.done) { + const a = next.value + const index = i++ + effect = core.flatMap(effect, (bool) => { + if (bool) { + return core.succeed(true) + } + builder.push(a) + return predicate(a, index) + }) + } + return core.map(effect, () => builder) + }) +) + +/* @internal */ +export const takeWhile = dual< + ( + predicate: (a: Types.NoInfer, i: number) => Effect.Effect + ) => (elements: Iterable) => Effect.Effect, E, R>, + ( + elements: Iterable, + predicate: (a: Types.NoInfer, i: number) => Effect.Effect + ) => Effect.Effect, E, R> +>( + 2, + (elements: Iterable, predicate: (a: Types.NoInfer, i: number) => Effect.Effect) => + core.suspend(() => { + const iterator = elements[Symbol.iterator]() + const builder: Array = [] + let next: IteratorResult + let taking: Effect.Effect = core.succeed(true) + let i = 0 + while ((next = iterator.next()) && !next.done) { + const a = next.value + const index = i++ + taking = core.flatMap(taking, (taking) => + pipe( + taking ? predicate(a, index) : core.succeed(false), + core.map((bool) => { + if (bool) { + builder.push(a) + } + return bool + }) + )) + } + return core.map(taking, () => builder) + }) +) + +/* @internal */ +export const tapBoth = dual< + ( + options: { + readonly onFailure: (e: Types.NoInfer) => Effect.Effect + readonly onSuccess: (a: Types.NoInfer) => Effect.Effect + } + ) => (self: Effect.Effect) => Effect.Effect, + ( + self: Effect.Effect, + options: { + readonly onFailure: (e: E) => Effect.Effect + readonly onSuccess: (a: A) => Effect.Effect + } + ) => Effect.Effect +>(2, (self, { onFailure, onSuccess }) => + core.matchCauseEffect(self, { + onFailure: (cause) => { + const either = internalCause.failureOrCause(cause) + switch (either._tag) { + case "Left": { + return core.zipRight(onFailure(either.left as any), core.failCause(cause)) + } + case "Right": { + return core.failCause(cause) + } + } + }, + onSuccess: (a) => core.as(onSuccess(a as any), a) + })) + +/* @internal */ +export const tapDefect = dual< + ( + f: (cause: Cause.Cause) => Effect.Effect + ) => (self: Effect.Effect) => Effect.Effect, + ( + self: Effect.Effect, + f: (cause: Cause.Cause) => Effect.Effect + ) => Effect.Effect +>(2, (self, f) => + core.catchAllCause(self, (cause) => + Option.match(internalCause.keepDefects(cause), { + onNone: () => core.failCause(cause), + onSome: (a) => core.zipRight(f(a), core.failCause(cause)) + }))) + +/* @internal */ +export const tapError = dual< + ( + f: (e: Types.NoInfer) => Effect.Effect + ) => (self: Effect.Effect) => Effect.Effect, + ( + self: Effect.Effect, + f: (e: E) => Effect.Effect + ) => Effect.Effect +>(2, (self, f) => + core.matchCauseEffect(self, { + onFailure: (cause) => { + const either = internalCause.failureOrCause(cause) + switch (either._tag) { + case "Left": + return core.zipRight(f(either.left as any), core.failCause(cause)) + case "Right": + return core.failCause(cause) + } + }, + onSuccess: core.succeed + })) + +/* @internal */ +export const tapErrorTag = dual< + ( + k: K, + f: (e: Extract) => Effect.Effect + ) => (self: Effect.Effect) => Effect.Effect, + ( + self: Effect.Effect, + k: K, + f: (e: Extract) => Effect.Effect + ) => Effect.Effect +>(3, (self, k, f) => + tapError(self, (e) => { + if (Predicate.isTagged(e, k)) { + return f(e as any) + } + return core.void as any + })) + +/* @internal */ +export const tapErrorCause = dual< + ( + f: (cause: Cause.Cause>) => Effect.Effect + ) => (self: Effect.Effect) => Effect.Effect, + ( + self: Effect.Effect, + f: (cause: Cause.Cause) => Effect.Effect + ) => Effect.Effect +>(2, (self, f) => + core.matchCauseEffect(self, { + onFailure: (cause) => core.zipRight(f(cause), core.failCause(cause)), + onSuccess: core.succeed + })) + +/* @internal */ +export const timed = ( + self: Effect.Effect +): Effect.Effect<[duration: Duration.Duration, result: A], E, R> => timedWith(self, Clock.currentTimeNanos) + +/* @internal */ +export const timedWith = dual< + ( + nanoseconds: Effect.Effect + ) => (self: Effect.Effect) => Effect.Effect<[Duration.Duration, A], E | E1, R | R1>, + ( + self: Effect.Effect, + nanoseconds: Effect.Effect + ) => Effect.Effect<[Duration.Duration, A], E | E1, R | R1> +>( + 2, + (self, nanos) => summarized(self, nanos, (start, end) => Duration.nanos(end - start)) +) + +/* @internal */ +export const tracerWith: (f: (tracer: Tracer.Tracer) => Effect.Effect) => Effect.Effect = + Tracer.tracerWith + +/** @internal */ +export const tracer: Effect.Effect = tracerWith(core.succeed) + +/* @internal */ +export const tryPromise: { + ( + options: { + readonly try: (signal: AbortSignal) => PromiseLike + readonly catch: (error: unknown) => E + } + ): Effect.Effect + (evaluate: (signal: AbortSignal) => PromiseLike): Effect.Effect +} = ( + arg: ((signal: AbortSignal) => PromiseLike) | { + readonly try: (signal: AbortSignal) => PromiseLike + readonly catch: (error: unknown) => E + } +): Effect.Effect => { + let evaluate: (signal?: AbortSignal) => PromiseLike + let catcher: ((error: unknown) => E) | undefined = undefined + if (typeof arg === "function") { + evaluate = arg as (signal?: AbortSignal) => PromiseLike + } else { + evaluate = arg.try as (signal?: AbortSignal) => PromiseLike + catcher = arg.catch + } + const fail = (e: unknown) => + catcher + ? core.failSync(() => catcher(e)) + : core.fail(new core.UnknownException(e, "An unknown error occurred in Effect.tryPromise")) + + if (evaluate.length >= 1) { + return core.async((resolve, signal) => { + try { + evaluate(signal).then( + (a) => resolve(core.succeed(a)), + (e) => resolve(fail(e)) + ) + } catch (e) { + resolve(fail(e)) + } + }) + } + + return core.async((resolve) => { + try { + evaluate() + .then( + (a) => resolve(core.succeed(a)), + (e) => resolve(fail(e)) + ) + } catch (e) { + resolve(fail(e)) + } + }) +} + +/* @internal */ +export const tryMap = dual< + ( + options: { + readonly try: (a: A) => B + readonly catch: (error: unknown) => E1 + } + ) => (self: Effect.Effect) => Effect.Effect, + ( + self: Effect.Effect, + options: { + readonly try: (a: A) => B + readonly catch: (error: unknown) => E1 + } + ) => Effect.Effect +>(2, (self, options) => + core.flatMap(self, (a) => + try_({ + try: () => options.try(a), + catch: options.catch + }))) + +/* @internal */ +export const tryMapPromise = dual< + ( + options: { + readonly try: (a: A, signal: AbortSignal) => PromiseLike + readonly catch: (error: unknown) => E1 + } + ) => (self: Effect.Effect) => Effect.Effect, + ( + self: Effect.Effect, + options: { + readonly try: (a: A, signal: AbortSignal) => PromiseLike + readonly catch: (error: unknown) => E1 + } + ) => Effect.Effect +>(2, ( + self: Effect.Effect, + options: { + readonly try: (a: A, signal: AbortSignal) => PromiseLike + readonly catch: (error: unknown) => E1 + } +) => + core.flatMap(self, (a) => + tryPromise({ + try: options.try.length >= 1 + ? (signal) => options.try(a, signal) + : () => (options.try as (a: A) => PromiseLike)(a), + catch: options.catch + }))) + +/* @internal */ +export const unless = dual< + (condition: LazyArg) => (self: Effect.Effect) => Effect.Effect, E, R>, + (self: Effect.Effect, condition: LazyArg) => Effect.Effect, E, R> +>(2, (self, condition) => + core.suspend(() => + condition() + ? succeedNone + : asSome(self) + )) + +/* @internal */ +export const unlessEffect = dual< + ( + condition: Effect.Effect + ) => (self: Effect.Effect) => Effect.Effect, E | E2, R | R2>, + ( + self: Effect.Effect, + condition: Effect.Effect + ) => Effect.Effect, E | E2, R | R2> +>(2, (self, condition) => core.flatMap(condition, (b) => (b ? succeedNone : asSome(self)))) + +/* @internal */ +export const unsandbox = (self: Effect.Effect, R>) => + mapErrorCause(self, internalCause.flatten) + +/* @internal */ +export const updateFiberRefs = ( + f: (fiberId: FiberId.Runtime, fiberRefs: FiberRefs.FiberRefs) => FiberRefs.FiberRefs +): Effect.Effect => + core.withFiberRuntime((state) => { + state.setFiberRefs(f(state.id(), state.getFiberRefs())) + return core.void + }) + +/* @internal */ +export const updateService = dual< + ( + tag: Context.Tag, + f: (service: Types.NoInfer) => Types.NoInfer + ) => (self: Effect.Effect) => Effect.Effect, + ( + self: Effect.Effect, + tag: Context.Tag, + f: (service: Types.NoInfer) => Types.NoInfer + ) => Effect.Effect +>(3, ( + self: Effect.Effect, + tag: Context.Tag, + f: (service: Types.NoInfer) => Types.NoInfer +) => + core.mapInputContext(self, (context) => + Context.add( + context, + tag, + f(Context.unsafeGet(context, tag)) + )) as Effect.Effect) + +/* @internal */ +export const when = dual< + (condition: LazyArg) => (self: Effect.Effect) => Effect.Effect, E, R>, + (self: Effect.Effect, condition: LazyArg) => Effect.Effect, E, R> +>(2, (self, condition) => + core.suspend(() => + condition() + ? core.map(self, Option.some) + : core.succeed(Option.none()) + )) + +/* @internal */ +export const whenFiberRef = dual< + ( + fiberRef: FiberRef.FiberRef, + predicate: Predicate.Predicate + ) => (self: Effect.Effect) => Effect.Effect<[S, Option.Option], E, R>, + ( + self: Effect.Effect, + fiberRef: FiberRef.FiberRef, + predicate: Predicate.Predicate + ) => Effect.Effect<[S, Option.Option], E, R> +>( + 3, + ( + self: Effect.Effect, + fiberRef: FiberRef.FiberRef, + predicate: Predicate.Predicate + ) => + core.flatMap(core.fiberRefGet(fiberRef), (s) => + predicate(s) + ? core.map(self, (a) => [s, Option.some(a)]) + : core.succeed<[S, Option.Option]>([s, Option.none()])) +) + +/* @internal */ +export const whenRef = dual< + ( + ref: Ref.Ref, + predicate: Predicate.Predicate + ) => (self: Effect.Effect) => Effect.Effect<[S, Option.Option], E, R>, + ( + self: Effect.Effect, + ref: Ref.Ref, + predicate: Predicate.Predicate + ) => Effect.Effect<[S, Option.Option], E, R> +>( + 3, + (self: Effect.Effect, ref: Ref.Ref, predicate: Predicate.Predicate) => + core.flatMap(Ref.get(ref), (s) => + predicate(s) + ? core.map(self, (a) => [s, Option.some(a)]) + : core.succeed<[S, Option.Option]>([s, Option.none()])) +) + +/* @internal */ +export const withMetric = dual< + ( + metric: Metric.Metric + ) => (self: Effect.Effect) => Effect.Effect, + ( + self: Effect.Effect, + metric: Metric.Metric + ) => Effect.Effect +>(2, (self, metric) => metric(self)) + +/** @internal */ +export const serviceFunctionEffect = , Args extends Array, A, E, R>( + getService: T, + f: (_: Effect.Effect.Success) => (...args: Args) => Effect.Effect +) => +(...args: Args): Effect.Effect, R | Effect.Effect.Context> => + core.flatMap(getService, (a) => f(a)(...args)) + +/** @internal */ +export const serviceFunction = , Args extends Array, A>( + getService: T, + f: (_: Effect.Effect.Success) => (...args: Args) => A +) => +(...args: Args): Effect.Effect, Effect.Effect.Context> => + core.map(getService, (a) => f(a)(...args)) + +/** @internal */ +export const serviceFunctions = ( + getService: Effect.Effect +): { + [k in keyof S as S[k] extends (...args: Array) => Effect.Effect ? k : never]: S[k] extends + (...args: infer Args) => Effect.Effect + ? (...args: Args) => Effect.Effect + : never +} => + new Proxy({} as any, { + get(_target: any, prop: any, _receiver) { + return (...args: Array) => core.flatMap(getService, (s: any) => s[prop](...args)) + } + }) + +/** @internal */ +export const serviceConstants = ( + getService: Effect.Effect +): { + [k in { [k in keyof S]: k }[keyof S]]: S[k] extends Effect.Effect ? + Effect.Effect : + Effect.Effect +} => + new Proxy({} as any, { + get(_target: any, prop: any, _receiver) { + return core.flatMap(getService, (s: any) => core.isEffect(s[prop]) ? s[prop] : core.succeed(s[prop])) + } + }) + +/** @internal */ +export const serviceMembers = (getService: Effect.Effect): { + functions: { + [k in keyof S as S[k] extends (...args: Array) => Effect.Effect ? k : never]: S[k] extends + (...args: infer Args) => Effect.Effect + ? (...args: Args) => Effect.Effect + : never + } + constants: { + [k in { [k in keyof S]: k }[keyof S]]: S[k] extends Effect.Effect ? + Effect.Effect : + Effect.Effect + } +} => ({ + functions: serviceFunctions(getService) as any, + constants: serviceConstants(getService) +}) + +/** @internal */ +export const serviceOption = (tag: Context.Tag) => core.map(core.context(), Context.getOption(tag)) + +/** @internal */ +export const serviceOptional = (tag: Context.Tag) => + core.flatMap(core.context(), Context.getOption(tag)) + +// ----------------------------------------------------------------------------- +// tracing +// ----------------------------------------------------------------------------- + +/* @internal */ +export const annotateCurrentSpan: { + (key: string, value: unknown): Effect.Effect + (values: Record): Effect.Effect +} = function(): Effect.Effect { + const args = arguments + return ignore(core.flatMap( + currentPropagatedSpan, + (span) => + core.sync(() => { + if (typeof args[0] === "string") { + span.attribute(args[0], args[1]) + } else { + for (const key in args[0]) { + span.attribute(key, args[0][key]) + } + } + }) + )) +} + +/* @internal */ +export const linkSpanCurrent: { + (span: Tracer.AnySpan, attributes?: Readonly> | undefined): Effect.Effect + (links: ReadonlyArray): Effect.Effect +} = function(): Effect.Effect { + const args = arguments + const links: ReadonlyArray = Array.isArray(args[0]) + ? args[0] + : [{ _tag: "SpanLink", span: args[0], attributes: args[1] ?? {} }] + return ignore(core.flatMap( + currentSpan, + (span) => core.sync(() => span.addLinks(links)) + )) +} + +/* @internal */ +export const annotateSpans = dual< + { + (key: string, value: unknown): (effect: Effect.Effect) => Effect.Effect + ( + values: Record + ): (effect: Effect.Effect) => Effect.Effect + }, + { + (effect: Effect.Effect, key: string, value: unknown): Effect.Effect + (effect: Effect.Effect, values: Record): Effect.Effect + } +>( + (args) => core.isEffect(args[0]), + function() { + const args = arguments + return core.fiberRefLocallyWith( + args[0] as Effect.Effect, + core.currentTracerSpanAnnotations, + typeof args[1] === "string" + ? HashMap.set(args[1], args[2]) + : (annotations) => + Object.entries(args[1] as Record).reduce( + (acc, [key, value]) => HashMap.set(acc, key, value), + annotations + ) + ) + } +) + +/** @internal */ +export const currentParentSpan: Effect.Effect = serviceOptional( + internalTracer.spanTag +) + +/** @internal */ +export const currentSpan: Effect.Effect = core.flatMap( + core.context(), + (context) => { + const span = context.unsafeMap.get(internalTracer.spanTag.key) as Tracer.AnySpan | undefined + return span !== undefined && span._tag === "Span" + ? core.succeed(span) + : core.fail(new core.NoSuchElementException()) + } +) + +export const currentPropagatedSpan: Effect.Effect = core.flatMap( + core.context(), + (context) => { + const span = filterDisablePropagation(Context.getOption(context, internalTracer.spanTag)) + return span._tag === "Some" && span.value._tag === "Span" + ? core.succeed(span.value) + : core.fail(new core.NoSuchElementException()) + } +) + +/* @internal */ +export const linkSpans = dual< + ( + span: Tracer.AnySpan, + attributes?: Record + ) => (self: Effect.Effect) => Effect.Effect, + ( + self: Effect.Effect, + span: Tracer.AnySpan, + attributes?: Record + ) => Effect.Effect +>( + (args) => core.isEffect(args[0]), + (self, span, attributes) => + core.fiberRefLocallyWith( + self, + core.currentTracerSpanLinks, + Chunk.append( + { + _tag: "SpanLink", + span, + attributes: attributes ?? {} + } as const + ) + ) +) + +const bigint0 = BigInt(0) + +export const filterDisablePropagation: (self: Option.Option) => Option.Option = Option + .flatMap( + (span) => + Context.get(span.context, internalTracer.DisablePropagation) + ? span._tag === "Span" ? filterDisablePropagation(span.parent) : Option.none() + : Option.some(span) + ) + +/** @internal */ +export const unsafeMakeSpan = ( + fiber: FiberRuntime, + name: string, + options: Tracer.SpanOptions +) => { + const disablePropagation = !fiber.getFiberRef(core.currentTracerEnabled) || + (options.context && Context.get(options.context, internalTracer.DisablePropagation)) + const context = fiber.getFiberRef(core.currentContext) + const parent = options.parent + ? Option.some(options.parent) + : options.root + ? Option.none() + : filterDisablePropagation(Context.getOption(context, internalTracer.spanTag)) + + let span: Tracer.Span + + if (disablePropagation) { + span = core.noopSpan({ + name, + parent, + context: Context.add(options.context ?? Context.empty(), internalTracer.DisablePropagation, true) + }) + } else { + const services = fiber.getFiberRef(defaultServices.currentServices) + + const tracer = Context.get(services, internalTracer.tracerTag) + const clock = Context.get(services, Clock.Clock) + const timingEnabled = fiber.getFiberRef(core.currentTracerTimingEnabled) + + const fiberRefs = fiber.getFiberRefs() + const annotationsFromEnv = FiberRefs.get(fiberRefs, core.currentTracerSpanAnnotations) + const linksFromEnv = FiberRefs.get(fiberRefs, core.currentTracerSpanLinks) + + const links = linksFromEnv._tag === "Some" ? + options.links !== undefined ? + [ + ...Chunk.toReadonlyArray(linksFromEnv.value), + ...(options.links ?? []) + ] : + Chunk.toReadonlyArray(linksFromEnv.value) : + options.links ?? Arr.empty() + + span = tracer.span( + name, + parent, + options.context ?? Context.empty(), + links, + timingEnabled ? clock.unsafeCurrentTimeNanos() : bigint0, + options.kind ?? "internal", + options + ) + + if (annotationsFromEnv._tag === "Some") { + HashMap.forEach(annotationsFromEnv.value, (value, key) => span.attribute(key, value)) + } + if (options.attributes !== undefined) { + Object.entries(options.attributes).forEach(([k, v]) => span.attribute(k, v)) + } + } + + if (typeof options.captureStackTrace === "function") { + internalCause.spanToTrace.set(span, options.captureStackTrace) + } + + return span +} + +/** @internal */ +export const makeSpan = ( + name: string, + options?: Tracer.SpanOptions +): Effect.Effect => { + options = internalTracer.addSpanStackTrace(options) + return core.withFiberRuntime((fiber) => core.succeed(unsafeMakeSpan(fiber, name, options))) +} + +/* @internal */ +export const spanAnnotations: Effect.Effect> = core + .fiberRefGet(core.currentTracerSpanAnnotations) + +/* @internal */ +export const spanLinks: Effect.Effect> = core + .fiberRefGet(core.currentTracerSpanLinks) + +/** @internal */ +export const endSpan = (span: Tracer.Span, exit: Exit, clock: Clock.Clock, timingEnabled: boolean) => + core.sync(() => { + if (span.status._tag === "Ended") { + return + } + if (core.exitIsFailure(exit) && internalCause.spanToTrace.has(span)) { + // https://opentelemetry.io/docs/specs/semconv/registry/attributes/code/#code-stacktrace + span.attribute("code.stacktrace", internalCause.spanToTrace.get(span)!()) + } + span.end(timingEnabled ? clock.unsafeCurrentTimeNanos() : bigint0, exit) + }) + +/** @internal */ +export const useSpan: { + (name: string, evaluate: (span: Tracer.Span) => Effect.Effect): Effect.Effect + ( + name: string, + options: Tracer.SpanOptions, + evaluate: (span: Tracer.Span) => Effect.Effect + ): Effect.Effect +} = ( + name: string, + ...args: [evaluate: (span: Tracer.Span) => Effect.Effect] | [ + options: any, + evaluate: (span: Tracer.Span) => Effect.Effect + ] +) => { + const options = internalTracer.addSpanStackTrace(args.length === 1 ? undefined : args[0]) + const evaluate: (span: Tracer.Span) => Effect.Effect = args[args.length - 1] + + return core.withFiberRuntime((fiber) => { + const span = unsafeMakeSpan(fiber, name, options) + const timingEnabled = fiber.getFiberRef(core.currentTracerTimingEnabled) + const clock = Context.get(fiber.getFiberRef(defaultServices.currentServices), clockTag) + return core.onExit(evaluate(span), (exit) => endSpan(span, exit, clock, timingEnabled)) + }) +} + +/** @internal */ +export const withParentSpan = dual< + ( + span: Tracer.AnySpan + ) => (self: Effect.Effect) => Effect.Effect>, + (self: Effect.Effect, span: Tracer.AnySpan) => Effect.Effect> +>(2, (self, span) => provideService(self, internalTracer.spanTag, span)) + +/** @internal */ +export const withSpan: { + ( + name: string, + options?: Tracer.SpanOptions | undefined + ): (self: Effect.Effect) => Effect.Effect> + ( + self: Effect.Effect, + name: string, + options?: Tracer.SpanOptions | undefined + ): Effect.Effect> +} = function() { + const dataFirst = typeof arguments[0] !== "string" + const name = dataFirst ? arguments[1] : arguments[0] + const options = internalTracer.addSpanStackTrace(dataFirst ? arguments[2] : arguments[1]) + if (dataFirst) { + const self = arguments[0] + return useSpan(name, options, (span) => withParentSpan(self, span)) + } + return (self: Effect.Effect) => useSpan(name, options, (span) => withParentSpan(self, span)) +} as any + +export const functionWithSpan = , Ret extends Effect.Effect>( + options: { + readonly body: (...args: Args) => Ret + readonly options: Effect.FunctionWithSpanOptions | ((...args: Args) => Effect.FunctionWithSpanOptions) + readonly captureStackTrace?: boolean | undefined + } +): (...args: Args) => Unify => + (function(this: any) { + let captureStackTrace: LazyArg | boolean = options.captureStackTrace ?? false + if (options.captureStackTrace !== false) { + const limit = Error.stackTraceLimit + Error.stackTraceLimit = 2 + const error = new Error() + Error.stackTraceLimit = limit + let cache: false | string = false + captureStackTrace = () => { + if (cache !== false) { + return cache + } + if (error.stack) { + const stack = error.stack.trim().split("\n") + cache = stack.slice(2).join("\n").trim() + return cache + } + } + } + return core.suspend(() => { + const opts = typeof options.options === "function" + ? options.options.apply(null, arguments as any) + : options.options + return withSpan( + core.suspend(() => internalCall(() => options.body.apply(this, arguments as any))), + opts.name, + { + ...opts, + captureStackTrace + } + ) + }) + }) as any + +// ------------------------------------------------------------------------------------- +// optionality +// ------------------------------------------------------------------------------------- + +/* @internal */ +export const fromNullable = (value: A): Effect.Effect, Cause.NoSuchElementException> => + value == null ? core.fail(new core.NoSuchElementException()) : core.succeed(value as NonNullable) + +/* @internal */ +export const optionFromOptional = ( + self: Effect.Effect +): Effect.Effect, Exclude, R> => + core.catchAll( + core.map(self, Option.some), + (error) => + core.isNoSuchElementException(error) ? + succeedNone : + core.fail(error as Exclude) + ) diff --git a/repos/effect/packages/effect/src/internal/core-stream.ts b/repos/effect/packages/effect/src/internal/core-stream.ts new file mode 100644 index 0000000..ebec63f --- /dev/null +++ b/repos/effect/packages/effect/src/internal/core-stream.ts @@ -0,0 +1,998 @@ +import * as Cause from "../Cause.js" +import type * as Channel from "../Channel.js" +import type * as ChildExecutorDecision from "../ChildExecutorDecision.js" +import * as Chunk from "../Chunk.js" +import type * as Context from "../Context.js" +import * as Effect from "../Effect.js" +import * as Either from "../Either.js" +import type * as Exit from "../Exit.js" +import { constVoid, dual, identity } from "../Function.js" +import type { LazyArg } from "../Function.js" +import * as Option from "../Option.js" +import { pipeArguments } from "../Pipeable.js" +import { hasProperty } from "../Predicate.js" +import type * as SingleProducerAsyncInput from "../SingleProducerAsyncInput.js" +import type * as UpstreamPullRequest from "../UpstreamPullRequest.js" +import type * as UpstreamPullStrategy from "../UpstreamPullStrategy.js" +import * as childExecutorDecision from "./channel/childExecutorDecision.js" +import type { ErasedContinuationK } from "./channel/continuation.js" +import { ContinuationKImpl } from "./channel/continuation.js" +import * as upstreamPullStrategy from "./channel/upstreamPullStrategy.js" +import * as OpCodes from "./opCodes/channel.js" + +/** @internal */ +const ChannelSymbolKey = "effect/Channel" + +/** @internal */ +export const ChannelTypeId: Channel.ChannelTypeId = Symbol.for( + ChannelSymbolKey +) as Channel.ChannelTypeId + +const channelVariance = { + /* c8 ignore next */ + _Env: (_: never) => _, + /* c8 ignore next */ + _InErr: (_: unknown) => _, + /* c8 ignore next */ + _InElem: (_: unknown) => _, + /* c8 ignore next */ + _InDone: (_: unknown) => _, + /* c8 ignore next */ + _OutErr: (_: never) => _, + /* c8 ignore next */ + _OutElem: (_: never) => _, + /* c8 ignore next */ + _OutDone: (_: never) => _ +} + +/** @internal */ +const proto = { + [ChannelTypeId]: channelVariance, + pipe() { + return pipeArguments(this, arguments) + } +} + +/** @internal */ +type ErasedChannel = Channel.Channel + +/** @internal */ +export type Op = + & ErasedChannel + & Body + & { readonly _tag: Tag } + +export type Primitive = + | BracketOut + | Bridge + | ConcatAll + | Emit + | Ensuring + | Fail + | Fold + | FromEffect + | PipeTo + | Provide + | Read + | Succeed + | SucceedNow + | Suspend + +/** @internal */ +export interface BracketOut extends + Op + finalizer(resource: unknown, exit: Exit.Exit): Effect.Effect + }> +{} + +/** @internal */ +export interface Bridge extends + Op + readonly channel: ErasedChannel + }> +{} + +/** @internal */ +export interface ConcatAll extends + Op + ): UpstreamPullStrategy.UpstreamPullStrategy + onEmit(outElem: unknown): ChildExecutorDecision.ChildExecutorDecision + value(): ErasedChannel + k(outElem: unknown): ErasedChannel + }> +{} + +/** @internal */ +export interface Emit extends + Op +{} + +/** @internal */ +export interface Ensuring extends + Op): Effect.Effect + }> +{} + +/** @internal */ +export interface Fail extends + Op + }> +{} + +/** @internal */ +export interface Fold extends + Op +{} + +/** @internal */ +export interface FromEffect extends + Op + }> +{} + +/** @internal */ +export interface PipeTo extends + Op +{} + +/** @internal */ +export interface Provide extends + Op + readonly inner: ErasedChannel + }> +{} + +/** @internal */ +export interface Read extends + Op +{} + +/** @internal */ +export interface Succeed extends + Op +{} + +/** @internal */ +export interface SucceedNow extends + Op +{} + +/** @internal */ +export interface Suspend extends + Op +{} + +/** @internal */ +export const isChannel = (u: unknown): u is Channel.Channel< + unknown, + unknown, + unknown, + unknown, + unknown, + unknown, + unknown +> => hasProperty(u, ChannelTypeId) || Effect.isEffect(u) + +/** @internal */ +export const acquireReleaseOut = dual< + ( + release: (z: Z, e: Exit.Exit) => Effect.Effect + ) => (self: Effect.Effect) => Channel.Channel, + ( + self: Effect.Effect, + release: (z: Z, e: Exit.Exit) => Effect.Effect + ) => Channel.Channel +>(2, (self, release) => { + const op = Object.create(proto) + op._tag = OpCodes.OP_BRACKET_OUT + op.acquire = () => self + op.finalizer = release + return op +}) + +/** @internal */ +export const catchAllCause = dual< + ( + f: (cause: Cause.Cause) => Channel.Channel + ) => ( + self: Channel.Channel + ) => Channel.Channel< + OutElem1 | OutElem, + InElem & InElem1, + OutErr1, + InErr & InErr1, + OutDone1 | OutDone, + InDone & InDone1, + Env1 | Env + >, + ( + self: Channel.Channel, + f: (cause: Cause.Cause) => Channel.Channel + ) => Channel.Channel< + OutElem1 | OutElem, + InElem & InElem1, + OutErr1, + InErr & InErr1, + OutDone1 | OutDone, + InDone & InDone1, + Env1 | Env + > +>( + 2, + ( + self: Channel.Channel, + f: (cause: Cause.Cause) => Channel.Channel + ): Channel.Channel< + OutElem | OutElem1, + InElem & InElem1, + OutErr1, + InErr & InErr1, + OutDone | OutDone1, + InDone & InDone1, + Env | Env1 + > => { + const op = Object.create(proto) + op._tag = OpCodes.OP_FOLD + op.channel = self + op.k = new ContinuationKImpl(succeed, f) + return op + } +) + +/** @internal */ +export const collectElements = ( + self: Channel.Channel +): Channel.Channel, OutDone], InDone, Env> => { + return suspend(() => { + const builder: Array = [] + return flatMap( + pipeTo(self, collectElementsReader(builder)), + (value) => sync(() => [Chunk.fromIterable(builder), value]) + ) + }) +} + +/** @internal */ +const collectElementsReader = ( + builder: Array +): Channel.Channel => + readWith({ + onInput: (outElem) => + flatMap( + sync(() => { + builder.push(outElem) + }), + () => collectElementsReader(builder) + ), + onFailure: fail, + onDone: succeedNow + }) + +/** @internal */ +export const concatAll = ( + channels: Channel.Channel< + Channel.Channel, + InElem, + OutErr, + InErr, + any, + InDone, + Env + > +): Channel.Channel => concatAllWith(channels, constVoid, constVoid) + +/** @internal */ +export const concatAllWith = < + OutElem, + InElem2, + OutErr2, + InErr2, + OutDone, + InDone2, + Env2, + InElem, + OutErr, + InErr, + OutDone2, + InDone, + Env, + OutDone3 +>( + channels: Channel.Channel< + Channel.Channel, + InElem, + OutErr, + InErr, + OutDone2, + InDone, + Env + >, + f: (o: OutDone, o1: OutDone) => OutDone, + g: (o: OutDone, o2: OutDone2) => OutDone3 +): Channel.Channel< + OutElem, + InElem & InElem2, + OutErr | OutErr2, + InErr & InErr2, + OutDone3, + InDone & InDone2, + Env | Env2 +> => { + const op = Object.create(proto) + op._tag = OpCodes.OP_CONCAT_ALL + op.combineInners = f + op.combineAll = g + op.onPull = () => upstreamPullStrategy.PullAfterNext(Option.none()) + op.onEmit = () => childExecutorDecision.Continue + op.value = () => channels + op.k = identity + return op +} + +/** @internal */ +export const concatMapWith = dual< + ( + f: (o: OutElem) => Channel.Channel, + g: (o: OutDone, o1: OutDone) => OutDone, + h: (o: OutDone, o2: OutDone2) => OutDone3 + ) => ( + self: Channel.Channel + ) => Channel.Channel< + OutElem2, + InElem & InElem2, + OutErr2 | OutErr, + InErr & InErr2, + OutDone3, + InDone & InDone2, + Env2 | Env + >, + < + OutElem, + InElem, + OutErr, + InErr, + OutDone2, + InDone, + Env, + OutElem2, + InElem2, + OutErr2, + InErr2, + OutDone, + InDone2, + Env2, + OutDone3 + >( + self: Channel.Channel, + f: (o: OutElem) => Channel.Channel, + g: (o: OutDone, o1: OutDone) => OutDone, + h: (o: OutDone, o2: OutDone2) => OutDone3 + ) => Channel.Channel< + OutElem2, + InElem & InElem2, + OutErr2 | OutErr, + InErr & InErr2, + OutDone3, + InDone & InDone2, + Env2 | Env + > +>(4, < + Env, + InErr, + InElem, + InDone, + OutErr, + OutElem, + OutElem2, + OutDone, + OutDone2, + OutDone3, + Env2, + InErr2, + InElem2, + InDone2, + OutErr2 +>( + self: Channel.Channel, + f: ( + o: OutElem + ) => Channel.Channel, + g: (o: OutDone, o1: OutDone) => OutDone, + h: (o: OutDone, o2: OutDone2) => OutDone3 +): Channel.Channel< + OutElem2, + InElem & InElem2, + OutErr | OutErr2, + InErr & InErr2, + OutDone3, + InDone & InDone2, + Env | Env2 +> => { + const op = Object.create(proto) + op._tag = OpCodes.OP_CONCAT_ALL + op.combineInners = g + op.combineAll = h + op.onPull = () => upstreamPullStrategy.PullAfterNext(Option.none()) + op.onEmit = () => childExecutorDecision.Continue + op.value = () => self + op.k = f + return op +}) + +/** @internal */ +export const concatMapWithCustom = dual< + ( + f: (o: OutElem) => Channel.Channel, + g: (o: OutDone, o1: OutDone) => OutDone, + h: (o: OutDone, o2: OutDone2) => OutDone3, + onPull: ( + upstreamPullRequest: UpstreamPullRequest.UpstreamPullRequest + ) => UpstreamPullStrategy.UpstreamPullStrategy, + onEmit: (elem: OutElem2) => ChildExecutorDecision.ChildExecutorDecision + ) => ( + self: Channel.Channel + ) => Channel.Channel< + OutElem2, + InElem & InElem2, + OutErr2 | OutErr, + InErr & InErr2, + OutDone3, + InDone & InDone2, + Env2 | Env + >, + < + OutElem, + InElem, + OutErr, + InErr, + OutDone2, + InDone, + Env, + OutElem2, + InElem2, + OutErr2, + InErr2, + OutDone, + InDone2, + Env2, + OutDone3 + >( + self: Channel.Channel, + f: (o: OutElem) => Channel.Channel, + g: (o: OutDone, o1: OutDone) => OutDone, + h: (o: OutDone, o2: OutDone2) => OutDone3, + onPull: ( + upstreamPullRequest: UpstreamPullRequest.UpstreamPullRequest + ) => UpstreamPullStrategy.UpstreamPullStrategy, + onEmit: (elem: OutElem2) => ChildExecutorDecision.ChildExecutorDecision + ) => Channel.Channel< + OutElem2, + InElem & InElem2, + OutErr2 | OutErr, + InErr & InErr2, + OutDone3, + InDone & InDone2, + Env2 | Env + > +>(6, < + Env, + InErr, + InElem, + InDone, + OutErr, + OutElem, + OutElem2, + OutDone, + OutDone2, + OutDone3, + Env2, + InErr2, + InElem2, + InDone2, + OutErr2 +>( + self: Channel.Channel, + f: ( + o: OutElem + ) => Channel.Channel, + g: (o: OutDone, o1: OutDone) => OutDone, + h: (o: OutDone, o2: OutDone2) => OutDone3, + onPull: ( + upstreamPullRequest: UpstreamPullRequest.UpstreamPullRequest + ) => UpstreamPullStrategy.UpstreamPullStrategy, + onEmit: (elem: OutElem2) => ChildExecutorDecision.ChildExecutorDecision +): Channel.Channel< + OutElem2, + InElem & InElem2, + OutErr | OutErr2, + InErr & InErr2, + OutDone3, + InDone & InDone2, + Env | Env2 +> => { + const op = Object.create(proto) + op._tag = OpCodes.OP_CONCAT_ALL + op.combineInners = g + op.combineAll = h + op.onPull = onPull + op.onEmit = onEmit + op.value = () => self + op.k = f + return op +}) + +/** @internal */ +export const embedInput = dual< + ( + input: SingleProducerAsyncInput.AsyncInputProducer + ) => ( + self: Channel.Channel + ) => Channel.Channel, + ( + self: Channel.Channel, + input: SingleProducerAsyncInput.AsyncInputProducer + ) => Channel.Channel +>( + 2, + ( + self: Channel.Channel, + input: SingleProducerAsyncInput.AsyncInputProducer + ): Channel.Channel => { + const op = Object.create(proto) + op._tag = OpCodes.OP_BRIDGE + op.input = input + op.channel = self + return op + } +) + +/** @internal */ +export const ensuringWith = dual< + ( + finalizer: (e: Exit.Exit) => Effect.Effect + ) => ( + self: Channel.Channel + ) => Channel.Channel, + ( + self: Channel.Channel, + finalizer: (e: Exit.Exit) => Effect.Effect + ) => Channel.Channel +>( + 2, + ( + self: Channel.Channel, + finalizer: (e: Exit.Exit) => Effect.Effect + ): Channel.Channel => { + const op = Object.create(proto) + op._tag = OpCodes.OP_ENSURING + op.channel = self + op.finalizer = finalizer + return op + } +) + +/** @internal */ +export const fail = (error: E): Channel.Channel => + failCause(Cause.fail(error)) + +/** @internal */ +export const failSync = ( + evaluate: LazyArg +): Channel.Channel => failCauseSync(() => Cause.fail(evaluate())) + +/** @internal */ +export const failCause = ( + cause: Cause.Cause +): Channel.Channel => failCauseSync(() => cause) + +/** @internal */ +export const failCauseSync = ( + evaluate: LazyArg> +): Channel.Channel => { + const op = Object.create(proto) + op._tag = OpCodes.OP_FAIL + op.error = evaluate + return op +} + +/** @internal */ +export const flatMap = dual< + ( + f: (d: OutDone) => Channel.Channel + ) => ( + self: Channel.Channel + ) => Channel.Channel< + OutElem1 | OutElem, + InElem & InElem1, + OutErr1 | OutErr, + InErr & InErr1, + OutDone2, + InDone & InDone1, + Env1 | Env + >, + ( + self: Channel.Channel, + f: (d: OutDone) => Channel.Channel + ) => Channel.Channel< + OutElem1 | OutElem, + InElem & InElem1, + OutErr1 | OutErr, + InErr & InErr1, + OutDone2, + InDone & InDone1, + Env1 | Env + > +>( + 2, + ( + self: Channel.Channel, + f: (d: OutDone) => Channel.Channel + ): Channel.Channel< + OutElem | OutElem1, + InElem & InElem1, + OutErr | OutErr1, + InErr & InErr1, + OutDone2, + InDone & InDone1, + Env | Env1 + > => { + const op = Object.create(proto) + op._tag = OpCodes.OP_FOLD + op.channel = self + op.k = new ContinuationKImpl(f, failCause) + return op + } +) + +/** @internal */ +export const foldCauseChannel = dual< + < + OutErr, + OutElem1, + InElem1, + OutErr2, + InErr1, + OutDone2, + InDone1, + Env1, + OutDone, + OutElem2, + InElem2, + OutErr3, + InErr2, + OutDone3, + InDone2, + Env2 + >( + options: { + readonly onFailure: ( + c: Cause.Cause + ) => Channel.Channel + readonly onSuccess: (o: OutDone) => Channel.Channel + } + ) => ( + self: Channel.Channel + ) => Channel.Channel< + OutElem1 | OutElem2 | OutElem, + InElem & InElem1 & InElem2, + OutErr2 | OutErr3, + InErr & InErr1 & InErr2, + OutDone2 | OutDone3, + InDone & InDone1 & InDone2, + Env1 | Env2 | Env + >, + < + OutElem, + InElem, + OutErr, + InErr, + OutDone, + InDone, + Env, + OutElem1, + InElem1, + OutErr2, + InErr1, + OutDone2, + InDone1, + Env1, + OutElem2, + InElem2, + OutErr3, + InErr2, + OutDone3, + InDone2, + Env2 + >( + self: Channel.Channel, + options: { + readonly onFailure: ( + c: Cause.Cause + ) => Channel.Channel + readonly onSuccess: (o: OutDone) => Channel.Channel + } + ) => Channel.Channel< + OutElem1 | OutElem2 | OutElem, + InElem & InElem1 & InElem2, + OutErr2 | OutErr3, + InErr & InErr1 & InErr2, + OutDone2 | OutDone3, + InDone & InDone1 & InDone2, + Env1 | Env2 | Env + > +>( + 2, + < + OutElem, + InElem, + OutErr, + InErr, + OutDone, + InDone, + Env, + OutElem1, + InElem1, + OutErr2, + InErr1, + OutDone2, + InDone1, + Env1, + OutElem2, + InElem2, + OutErr3, + InErr2, + OutDone3, + InDone2, + Env2 + >( + self: Channel.Channel, + options: { + readonly onFailure: ( + c: Cause.Cause + ) => Channel.Channel + readonly onSuccess: (o: OutDone) => Channel.Channel + } + ): Channel.Channel< + OutElem | OutElem1 | OutElem2, + InElem & InElem1 & InElem2, + OutErr2 | OutErr3, + InErr & InErr1 & InErr2, + OutDone2 | OutDone3, + InDone & InDone1 & InDone2, + Env | Env1 | Env2 + > => { + const op = Object.create(proto) + op._tag = OpCodes.OP_FOLD + op.channel = self + op.k = new ContinuationKImpl(options.onSuccess, options.onFailure as any) + return op + } +) + +/** @internal */ +export const fromEffect = ( + effect: Effect.Effect +): Channel.Channel => { + const op = Object.create(proto) + op._tag = OpCodes.OP_FROM_EFFECT + op.effect = () => effect + return op +} + +/** @internal */ +export const pipeTo = dual< + ( + that: Channel.Channel + ) => ( + self: Channel.Channel + ) => Channel.Channel, + ( + self: Channel.Channel, + that: Channel.Channel + ) => Channel.Channel +>( + 2, + ( + self: Channel.Channel, + that: Channel.Channel + ): Channel.Channel => { + const op = Object.create(proto) + op._tag = OpCodes.OP_PIPE_TO + op.left = () => self + op.right = () => that + return op + } +) + +/** @internal */ +export const provideContext = dual< + ( + env: Context.Context + ) => ( + self: Channel.Channel + ) => Channel.Channel, + ( + self: Channel.Channel, + env: Context.Context + ) => Channel.Channel +>( + 2, + ( + self: Channel.Channel, + env: Context.Context + ): Channel.Channel => { + const op = Object.create(proto) + op._tag = OpCodes.OP_PROVIDE + op.context = () => env + op.inner = self + return op + } +) + +/** @internal */ +export const readOrFail = ( + error: E +): Channel.Channel => { + const op = Object.create(proto) + op._tag = OpCodes.OP_READ + op.more = succeed + op.done = new ContinuationKImpl(() => fail(error), () => fail(error)) + return op +} + +/** @internal */ +export const readWith = < + InElem, + OutElem, + OutErr, + InErr, + OutDone, + InDone, + Env, + OutElem2, + OutErr2, + OutDone2, + Env2, + OutElem3, + OutErr3, + OutDone3, + Env3 +>( + options: { + readonly onInput: (input: InElem) => Channel.Channel + readonly onFailure: (error: InErr) => Channel.Channel + readonly onDone: (done: InDone) => Channel.Channel + } +): Channel.Channel< + OutElem | OutElem2 | OutElem3, + InElem, + OutErr | OutErr2 | OutErr3, + InErr, + OutDone | OutDone2 | OutDone3, + InDone, + Env | Env2 | Env3 +> => + readWithCause({ + onInput: options.onInput, + onFailure: (cause) => Either.match(Cause.failureOrCause(cause), { onLeft: options.onFailure, onRight: failCause }), + onDone: options.onDone + }) + +/** @internal */ +export const readWithCause = < + InElem, + OutElem, + OutErr, + InErr, + OutDone, + InDone, + Env, + OutElem2, + OutErr2, + OutDone2, + Env2, + OutElem3, + OutErr3, + OutDone3, + Env3 +>( + options: { + readonly onInput: (input: InElem) => Channel.Channel + readonly onFailure: ( + cause: Cause.Cause + ) => Channel.Channel + readonly onDone: (done: InDone) => Channel.Channel + } +): Channel.Channel< + OutElem | OutElem2 | OutElem3, + InElem, + OutErr | OutErr2 | OutErr3, + InErr, + OutDone | OutDone2 | OutDone3, + InDone, + Env | Env2 | Env3 +> => { + const op = Object.create(proto) + op._tag = OpCodes.OP_READ + op.more = options.onInput + op.done = new ContinuationKImpl(options.onDone, options.onFailure as any) + return op +} + +/** @internal */ +export const succeed = ( + value: A +): Channel.Channel => sync(() => value) + +/** @internal */ +export const succeedNow = ( + result: OutDone +): Channel.Channel => { + const op = Object.create(proto) + op._tag = OpCodes.OP_SUCCEED_NOW + op.terminal = result + return op +} + +/** @internal */ +export const suspend = ( + evaluate: LazyArg> +): Channel.Channel => { + const op = Object.create(proto) + op._tag = OpCodes.OP_SUSPEND + op.channel = evaluate + return op +} + +export const sync = ( + evaluate: LazyArg +): Channel.Channel => { + const op = Object.create(proto) + op._tag = OpCodes.OP_SUCCEED + op.evaluate = evaluate + return op +} + +const void_: Channel.Channel = succeedNow(void 0) +export { + /** @internal */ + void_ as void +} + +/** @internal */ +export const write = ( + out: OutElem +): Channel.Channel => { + const op = Object.create(proto) + op._tag = OpCodes.OP_EMIT + op.out = out + return op +} diff --git a/repos/effect/packages/effect/src/internal/core.ts b/repos/effect/packages/effect/src/internal/core.ts new file mode 100644 index 0000000..55068f1 --- /dev/null +++ b/repos/effect/packages/effect/src/internal/core.ts @@ -0,0 +1,3166 @@ +import * as Arr from "../Array.js" +import type * as Cause from "../Cause.js" +import * as Chunk from "../Chunk.js" +import * as Context from "../Context.js" +import type * as Deferred from "../Deferred.js" +import type * as Differ from "../Differ.js" +import * as Duration from "../Duration.js" +import type * as Effect from "../Effect.js" +import * as Either from "../Either.js" +import * as Equal from "../Equal.js" +import type * as ExecutionStrategy from "../ExecutionStrategy.js" +import type * as Exit from "../Exit.js" +import type * as Fiber from "../Fiber.js" +import * as FiberId from "../FiberId.js" +import type * as FiberRef from "../FiberRef.js" +import type * as FiberStatus from "../FiberStatus.js" +import type { LazyArg } from "../Function.js" +import { dual, identity, pipe } from "../Function.js" +import { globalValue } from "../GlobalValue.js" +import * as Hash from "../Hash.js" +import * as HashMap from "../HashMap.js" +import type * as HashSet from "../HashSet.js" +import { format, NodeInspectSymbol, toJSON } from "../Inspectable.js" +import * as List from "../List.js" +import type * as LogLevel from "../LogLevel.js" +import type * as LogSpan from "../LogSpan.js" +import type * as MetricLabel from "../MetricLabel.js" +import * as MutableRef from "../MutableRef.js" +import * as Option from "../Option.js" +import { pipeArguments } from "../Pipeable.js" +import { hasProperty, isObject, isPromiseLike, type Predicate, type Refinement } from "../Predicate.js" +import type * as Request from "../Request.js" +import type * as BlockedRequests from "../RequestBlock.js" +import type * as RequestResolver from "../RequestResolver.js" +import type * as RuntimeFlags from "../RuntimeFlags.js" +import * as RuntimeFlagsPatch from "../RuntimeFlagsPatch.js" +import type * as Scope from "../Scope.js" +import type * as Tracer from "../Tracer.js" +import type { NoInfer, NotFunction } from "../Types.js" +import { internalCall, YieldWrap } from "../Utils.js" +import * as blockedRequests_ from "./blockedRequests.js" +import * as internalCause from "./cause.js" +import * as deferred from "./deferred.js" +import * as internalDiffer from "./differ.js" +import { CommitPrototype, effectVariance, StructuralCommitPrototype } from "./effectable.js" +import { getBugErrorMessage } from "./errors.js" +import type * as FiberRuntime from "./fiberRuntime.js" +import type * as fiberScope from "./fiberScope.js" +import * as DeferredOpCodes from "./opCodes/deferred.js" +import * as OpCodes from "./opCodes/effect.js" +import * as runtimeFlags_ from "./runtimeFlags.js" +import { SingleShotGen } from "./singleShotGen.js" + +// ----------------------------------------------------------------------------- +// Effect +// ----------------------------------------------------------------------------- + +/** + * @internal + */ +export const blocked = ( + blockedRequests: BlockedRequests.RequestBlock, + _continue: Effect.Effect +): Effect.Blocked => { + const effect = new EffectPrimitive("Blocked") as any + effect.effect_instruction_i0 = blockedRequests + effect.effect_instruction_i1 = _continue + return effect +} + +/** + * @internal + */ +export const runRequestBlock = ( + blockedRequests: BlockedRequests.RequestBlock +): Effect.Effect => { + const effect = new EffectPrimitive("RunBlocked") as any + effect.effect_instruction_i0 = blockedRequests + return effect +} + +/** @internal */ +export const EffectTypeId: Effect.EffectTypeId = Symbol.for("effect/Effect") as Effect.EffectTypeId + +/** @internal */ +export type Primitive = + | Async + | Commit + | Failure + | OnFailure + | OnSuccess + | OnStep + | OnSuccessAndFailure + | Success + | Sync + | UpdateRuntimeFlags + | While + | FromIterator + | WithRuntime + | Yield + | OpTag + | Blocked + | RunBlocked + | Either.Either + | Option.Option + +/** @internal */ +export type Continuation = + | OnSuccess + | OnStep + | OnSuccessAndFailure + | OnFailure + | While + | FromIterator + | RevertFlags + +/** @internal */ +export class RevertFlags { + readonly _op = OpCodes.OP_REVERT_FLAGS + constructor( + readonly patch: RuntimeFlagsPatch.RuntimeFlagsPatch, + readonly op: Primitive & { _op: OpCodes.OP_UPDATE_RUNTIME_FLAGS } + ) { + } +} + +class EffectPrimitive { + public effect_instruction_i0 = undefined + public effect_instruction_i1 = undefined + public effect_instruction_i2 = undefined + public trace = undefined; + [EffectTypeId] = effectVariance + constructor(readonly _op: Primitive["_op"]) {} + [Equal.symbol](this: {}, that: unknown) { + return this === that + } + [Hash.symbol](this: {}) { + return Hash.cached(this, Hash.random(this)) + } + pipe() { + return pipeArguments(this, arguments) + } + toJSON() { + return { + _id: "Effect", + _op: this._op, + effect_instruction_i0: toJSON(this.effect_instruction_i0), + effect_instruction_i1: toJSON(this.effect_instruction_i1), + effect_instruction_i2: toJSON(this.effect_instruction_i2) + } + } + toString() { + return format(this.toJSON()) + } + [NodeInspectSymbol]() { + return this.toJSON() + } + [Symbol.iterator]() { + return new SingleShotGen(new YieldWrap(this)) + } +} + +/** @internal */ +class EffectPrimitiveFailure { + public effect_instruction_i0 = undefined + public effect_instruction_i1 = undefined + public effect_instruction_i2 = undefined + public trace = undefined; + [EffectTypeId] = effectVariance + constructor(readonly _op: Primitive["_op"]) { + // @ts-expect-error + this._tag = _op + } + [Equal.symbol](this: {}, that: unknown) { + return exitIsExit(that) && that._op === "Failure" && + // @ts-expect-error + Equal.equals(this.effect_instruction_i0, that.effect_instruction_i0) + } + [Hash.symbol](this: {}) { + return pipe( + // @ts-expect-error + Hash.string(this._tag), + // @ts-expect-error + Hash.combine(Hash.hash(this.effect_instruction_i0)), + Hash.cached(this) + ) + } + get cause() { + return this.effect_instruction_i0 + } + pipe() { + return pipeArguments(this, arguments) + } + toJSON() { + return { + _id: "Exit", + _tag: this._op, + cause: (this.cause as any).toJSON() + } + } + toString() { + return format(this.toJSON()) + } + [NodeInspectSymbol]() { + return this.toJSON() + } + [Symbol.iterator]() { + return new SingleShotGen(new YieldWrap(this)) + } +} + +/** @internal */ +class EffectPrimitiveSuccess { + public effect_instruction_i0 = undefined + public effect_instruction_i1 = undefined + public effect_instruction_i2 = undefined + public trace = undefined; + [EffectTypeId] = effectVariance + constructor(readonly _op: Primitive["_op"]) { + // @ts-expect-error + this._tag = _op + } + [Equal.symbol](this: {}, that: unknown) { + return exitIsExit(that) && that._op === "Success" && + // @ts-expect-error + Equal.equals(this.effect_instruction_i0, that.effect_instruction_i0) + } + [Hash.symbol](this: {}) { + return pipe( + // @ts-expect-error + Hash.string(this._tag), + // @ts-expect-error + Hash.combine(Hash.hash(this.effect_instruction_i0)), + Hash.cached(this) + ) + } + get value() { + return this.effect_instruction_i0 + } + pipe() { + return pipeArguments(this, arguments) + } + toJSON() { + return { + _id: "Exit", + _tag: this._op, + value: toJSON(this.value) + } + } + toString() { + return format(this.toJSON()) + } + [NodeInspectSymbol]() { + return this.toJSON() + } + [Symbol.iterator]() { + return new SingleShotGen(new YieldWrap(this)) + } +} + +/** @internal */ +export type Op = Effect.Effect & Body & { + readonly _op: Tag +} + +/** @internal */ +export interface Async extends + Op void): void + readonly effect_instruction_i1: FiberId.FiberId + }> +{} + +/** @internal */ +export interface Blocked extends + Op<"Blocked", { + readonly effect_instruction_i0: BlockedRequests.RequestBlock + readonly effect_instruction_i1: Effect.Effect + }> +{} + +/** @internal */ +export interface RunBlocked extends + Op<"RunBlocked", { + readonly effect_instruction_i0: BlockedRequests.RequestBlock + }> +{} + +/** @internal */ +export interface Failure extends + Op + }> +{} + +/** @internal */ +export interface OpTag extends Op {} + +/** @internal */ +export interface Commit extends + Op + }> +{} + +/** @internal */ +export interface OnFailure extends + Op): Primitive + }> +{} + +/** @internal */ +export interface OnSuccess extends + Op +{} + +/** @internal */ +export interface OnStep extends Op<"OnStep", { readonly effect_instruction_i0: Primitive }> {} + +/** @internal */ +export interface OnSuccessAndFailure extends + Op): Primitive + effect_instruction_i2(a: unknown): Primitive + }> +{} + +/** @internal */ +export interface Success extends + Op +{} + +/** @internal */ +export interface Sync extends + Op +{} + +/** @internal */ +export interface UpdateRuntimeFlags extends + Op Primitive + }> +{} + +/** @internal */ +export interface While extends + Op +{} + +/** @internal */ +export interface FromIterator extends + Op, any> + }> +{} + +/** @internal */ +export interface WithRuntime extends + Op, status: FiberStatus.Running): Primitive + }> +{} + +/** @internal */ +export interface Yield extends Op {} + +/** @internal */ +export const isEffect = (u: unknown): u is Effect.Effect => hasProperty(u, EffectTypeId) + +/* @internal */ +export const withFiberRuntime = ( + withRuntime: (fiber: FiberRuntime.FiberRuntime, status: FiberStatus.Running) => Effect.Effect +): Effect.Effect => { + const effect = new EffectPrimitive(OpCodes.OP_WITH_RUNTIME) as any + effect.effect_instruction_i0 = withRuntime + return effect +} + +/* @internal */ +export const acquireUseRelease: { + ( + use: (a: A) => Effect.Effect, + release: (a: A, exit: Exit.Exit) => Effect.Effect + ): (acquire: Effect.Effect) => Effect.Effect + ( + acquire: Effect.Effect, + use: (a: A) => Effect.Effect, + release: (a: A, exit: Exit.Exit) => Effect.Effect + ): Effect.Effect +} = dual(3, ( + acquire: Effect.Effect, + use: (a: A) => Effect.Effect, + release: (a: A, exit: Exit.Exit) => Effect.Effect +): Effect.Effect => + uninterruptibleMask((restore) => + flatMap( + acquire, + (a) => + flatMap(exit(suspend(() => restore(use(a)))), (exit): Effect.Effect => { + return suspend(() => release(a, exit)).pipe( + matchCauseEffect({ + onFailure: (cause) => { + switch (exit._tag) { + case OpCodes.OP_FAILURE: + return failCause(internalCause.sequential(exit.effect_instruction_i0, cause)) + case OpCodes.OP_SUCCESS: + return failCause(cause) + } + }, + onSuccess: () => exit + }) + ) + }) + ) + )) + +/* @internal */ +export const as: { + (value: B): (self: Effect.Effect) => Effect.Effect + (self: Effect.Effect, value: B): Effect.Effect +} = dual( + 2, + (self: Effect.Effect, value: B): Effect.Effect => flatMap(self, () => succeed(value)) +) + +/* @internal */ +export const asVoid = (self: Effect.Effect): Effect.Effect => as(self, void 0) + +/* @internal */ +export const custom: { + (i0: X, body: (this: { effect_instruction_i0: X }) => Effect.Effect): Effect.Effect + ( + i0: X, + i1: Y, + body: (this: { effect_instruction_i0: X; effect_instruction_i1: Y }) => Effect.Effect + ): Effect.Effect + ( + i0: X, + i1: Y, + i2: Z, + body: ( + this: { effect_instruction_i0: X; effect_instruction_i1: Y; effect_instruction_i2: Z } + ) => Effect.Effect + ): Effect.Effect +} = function() { + const wrapper = new EffectPrimitive(OpCodes.OP_COMMIT) as any + switch (arguments.length) { + case 2: { + wrapper.effect_instruction_i0 = arguments[0] + wrapper.commit = arguments[1] + break + } + case 3: { + wrapper.effect_instruction_i0 = arguments[0] + wrapper.effect_instruction_i1 = arguments[1] + wrapper.commit = arguments[2] + break + } + case 4: { + wrapper.effect_instruction_i0 = arguments[0] + wrapper.effect_instruction_i1 = arguments[1] + wrapper.effect_instruction_i2 = arguments[2] + wrapper.commit = arguments[3] + break + } + default: { + throw new Error(getBugErrorMessage("you're not supposed to end up here")) + } + } + return wrapper +} + +/* @internal */ +export const unsafeAsync = ( + register: ( + callback: (_: Effect.Effect) => void + ) => void | Effect.Effect, + blockingOn: FiberId.FiberId = FiberId.none +): Effect.Effect => { + const effect = new EffectPrimitive(OpCodes.OP_ASYNC) as any + let cancelerRef: Effect.Effect | void = undefined + effect.effect_instruction_i0 = (resume: (_: Effect.Effect) => void) => { + cancelerRef = register(resume) + } + effect.effect_instruction_i1 = blockingOn + return onInterrupt(effect, (_) => isEffect(cancelerRef) ? cancelerRef : void_) +} + +/* @internal */ +export const asyncInterrupt = ( + register: ( + callback: (_: Effect.Effect) => void + ) => void | Effect.Effect, + blockingOn: FiberId.FiberId = FiberId.none +): Effect.Effect => suspend(() => unsafeAsync(register, blockingOn)) + +const async_ = ( + resume: ( + callback: (_: Effect.Effect) => void, + signal: AbortSignal + ) => void | Effect.Effect, + blockingOn: FiberId.FiberId = FiberId.none +): Effect.Effect => { + return custom(resume, function() { + let backingResume: ((_: Effect.Effect) => void) | undefined = undefined + let pendingEffect: Effect.Effect | undefined = undefined + function proxyResume(effect: Effect.Effect) { + if (backingResume) { + backingResume(effect) + } else if (pendingEffect === undefined) { + pendingEffect = effect + } + } + const effect = new EffectPrimitive(OpCodes.OP_ASYNC) as any + effect.effect_instruction_i0 = (resume: (_: Effect.Effect) => void) => { + backingResume = resume + if (pendingEffect) { + resume(pendingEffect) + } + } + effect.effect_instruction_i1 = blockingOn + let cancelerRef: Effect.Effect | void = undefined + let controllerRef: AbortController | void = undefined + if (this.effect_instruction_i0.length !== 1) { + controllerRef = new AbortController() + cancelerRef = internalCall(() => this.effect_instruction_i0(proxyResume, controllerRef!.signal)) + } else { + cancelerRef = internalCall(() => (this.effect_instruction_i0 as any)(proxyResume)) + } + return (cancelerRef || controllerRef) ? + onInterrupt(effect, (_) => { + if (controllerRef) { + controllerRef.abort() + } + return cancelerRef ?? void_ + }) : + effect + }) +} +export { + /** @internal */ + async_ as async +} + +/* @internal */ +export const catchAllCause = dual< + ( + f: (cause: Cause.Cause) => Effect.Effect + ) => (self: Effect.Effect) => Effect.Effect, + ( + self: Effect.Effect, + f: (cause: Cause.Cause) => Effect.Effect + ) => Effect.Effect +>(2, (self, f) => { + const effect = new EffectPrimitive(OpCodes.OP_ON_FAILURE) as any + effect.effect_instruction_i0 = self + effect.effect_instruction_i1 = f + return effect +}) + +/* @internal */ +export const catchAll: { + ( + f: (e: E) => Effect.Effect + ): (self: Effect.Effect) => Effect.Effect + ( + self: Effect.Effect, + f: (e: E) => Effect.Effect + ): Effect.Effect +} = dual( + 2, + ( + self: Effect.Effect, + f: (e: E) => Effect.Effect + ): Effect.Effect => matchEffect(self, { onFailure: f, onSuccess: succeed }) +) + +/* @internal */ +export const catchIf: { + ( + refinement: Refinement, EB>, + f: (e: EB) => Effect.Effect + ): (self: Effect.Effect) => Effect.Effect, R2 | R> + ( + predicate: Predicate>, + f: (e: NoInfer) => Effect.Effect + ): (self: Effect.Effect) => Effect.Effect + ( + self: Effect.Effect, + refinement: Refinement, + f: (e: EB) => Effect.Effect + ): Effect.Effect, R2 | R> + ( + self: Effect.Effect, + predicate: Predicate, + f: (e: E) => Effect.Effect + ): Effect.Effect +} = dual(3, ( + self: Effect.Effect, + predicate: Predicate, + f: (e: E) => Effect.Effect +): Effect.Effect => + catchAllCause(self, (cause): Effect.Effect => { + const either = internalCause.failureOrCause(cause) + switch (either._tag) { + case "Left": + return predicate(either.left) ? f(either.left) : failCause(cause) + case "Right": + return failCause(either.right) + } + })) + +/* @internal */ +export const catchSome = dual< + ( + pf: (e: NoInfer) => Option.Option> + ) => (self: Effect.Effect) => Effect.Effect, + ( + self: Effect.Effect, + pf: (e: NoInfer) => Option.Option> + ) => Effect.Effect +>(2, ( + self: Effect.Effect, + pf: (e: NoInfer) => Option.Option> +) => + catchAllCause(self, (cause): Effect.Effect => { + const either = internalCause.failureOrCause(cause) + switch (either._tag) { + case "Left": + return pipe(pf(either.left), Option.getOrElse(() => failCause(cause))) + case "Right": + return failCause(either.right) + } + })) + +/* @internal */ +export const checkInterruptible = ( + f: (isInterruptible: boolean) => Effect.Effect +): Effect.Effect => withFiberRuntime((_, status) => f(runtimeFlags_.interruption(status.runtimeFlags))) + +const originalSymbol = Symbol.for("effect/OriginalAnnotation") + +/* @internal */ +export const originalInstance = (obj: E): E => { + if (hasProperty(obj, originalSymbol)) { + // @ts-expect-error + return obj[originalSymbol] + } + return obj +} + +/* @internal */ +export const capture = (obj: E & object, span: Option.Option): E => { + if (Option.isSome(span)) { + return new Proxy(obj, { + has(target, p) { + return p === internalCause.spanSymbol || p === originalSymbol || p in target + }, + get(target, p) { + if (p === internalCause.spanSymbol) { + return span.value + } + if (p === originalSymbol) { + return obj + } + // @ts-expect-error + return target[p] + } + }) + } + return obj +} + +/* @internal */ +export const die = (defect: unknown): Effect.Effect => + isObject(defect) && !(internalCause.spanSymbol in defect) ? + withFiberRuntime((fiber) => failCause(internalCause.die(capture(defect, currentSpanFromFiber(fiber))))) + : failCause(internalCause.die(defect)) + +/* @internal */ +export const dieMessage = (message: string): Effect.Effect => + failCauseSync(() => internalCause.die(new RuntimeException(message))) + +/* @internal */ +export const dieSync = (evaluate: LazyArg): Effect.Effect => flatMap(sync(evaluate), die) + +/* @internal */ +export const either = (self: Effect.Effect): Effect.Effect, never, R> => + matchEffect(self, { + onFailure: (e) => succeed(Either.left(e)), + onSuccess: (a) => succeed(Either.right(a)) + }) + +/* @internal */ +export const exit = (self: Effect.Effect): Effect.Effect, never, R> => + matchCause(self, { + onFailure: exitFailCause, + onSuccess: exitSucceed + }) + +/* @internal */ +export const fail = (error: E): Effect.Effect => + isObject(error) && !(internalCause.spanSymbol in error) ? + withFiberRuntime((fiber) => failCause(internalCause.fail(capture(error, currentSpanFromFiber(fiber))))) + : failCause(internalCause.fail(error)) + +/* @internal */ +export const failSync = (evaluate: LazyArg): Effect.Effect => flatMap(sync(evaluate), fail) + +/* @internal */ +export const failCause = (cause: Cause.Cause): Effect.Effect => { + const effect = new EffectPrimitiveFailure(OpCodes.OP_FAILURE) as any + effect.effect_instruction_i0 = cause + return effect +} + +/* @internal */ +export const failCauseSync = ( + evaluate: LazyArg> +): Effect.Effect => flatMap(sync(evaluate), failCause) + +/* @internal */ +export const fiberId: Effect.Effect = withFiberRuntime((state) => succeed(state.id())) + +/* @internal */ +export const fiberIdWith = ( + f: (descriptor: FiberId.Runtime) => Effect.Effect +): Effect.Effect => withFiberRuntime((state) => f(state.id())) + +/* @internal */ +export const flatMap = dual< + ( + f: (a: A) => Effect.Effect + ) => (self: Effect.Effect) => Effect.Effect, + ( + self: Effect.Effect, + f: (a: A) => Effect.Effect + ) => Effect.Effect +>( + 2, + (self, f) => { + const effect = new EffectPrimitive(OpCodes.OP_ON_SUCCESS) as any + effect.effect_instruction_i0 = self + effect.effect_instruction_i1 = f + return effect + } +) + +/* @internal */ +export const andThen: { + ( + f: (a: NoInfer) => X + ): ( + self: Effect.Effect + ) => [X] extends [Effect.Effect] ? Effect.Effect + : [X] extends [PromiseLike] ? Effect.Effect + : Effect.Effect + ( + f: NotFunction + ): ( + self: Effect.Effect + ) => [X] extends [Effect.Effect] ? Effect.Effect + : [X] extends [PromiseLike] ? Effect.Effect + : Effect.Effect + ( + self: Effect.Effect, + f: (a: NoInfer) => X + ): [X] extends [Effect.Effect] ? Effect.Effect + : [X] extends [PromiseLike] ? Effect.Effect + : Effect.Effect + ( + self: Effect.Effect, + f: NotFunction + ): [X] extends [Effect.Effect] ? Effect.Effect + : [X] extends [PromiseLike] ? Effect.Effect + : Effect.Effect +} = dual(2, (self, f) => + flatMap(self, (a) => { + const b = typeof f === "function" ? (f as any)(a) : f + if (isEffect(b)) { + return b + } else if (isPromiseLike(b)) { + return unsafeAsync((resume) => { + b.then((a) => resume(succeed(a)), (e) => + resume(fail(new UnknownException(e, "An unknown error occurred in Effect.andThen")))) + }) + } + return succeed(b) + })) + +/* @internal */ +export const step = ( + self: Effect.Effect +): Effect.Effect | Effect.Blocked, never, R> => { + const effect = new EffectPrimitive("OnStep") as any + effect.effect_instruction_i0 = self + return effect +} + +/* @internal */ +export const flatten = ( + self: Effect.Effect, E, R> +): Effect.Effect => flatMap(self, identity) + +/* @internal */ +export const flip = (self: Effect.Effect): Effect.Effect => + matchEffect(self, { onFailure: succeed, onSuccess: fail }) + +/* @internal */ +export const matchCause: { + ( + options: { + readonly onFailure: (cause: Cause.Cause) => A2 + readonly onSuccess: (a: A) => A3 + } + ): (self: Effect.Effect) => Effect.Effect + ( + self: Effect.Effect, + options: { + readonly onFailure: (cause: Cause.Cause) => A2 + readonly onSuccess: (a: A) => A3 + } + ): Effect.Effect +} = dual(2, ( + self: Effect.Effect, + options: { + readonly onFailure: (cause: Cause.Cause) => A2 + readonly onSuccess: (a: A) => A3 + } +): Effect.Effect => + matchCauseEffect(self, { + onFailure: (cause) => succeed(options.onFailure(cause)), + onSuccess: (a) => succeed(options.onSuccess(a)) + })) + +/* @internal */ +export const matchCauseEffect: { + ( + options: { + readonly onFailure: (cause: Cause.Cause) => Effect.Effect + readonly onSuccess: (a: A) => Effect.Effect + } + ): (self: Effect.Effect) => Effect.Effect + ( + self: Effect.Effect, + options: { + readonly onFailure: (cause: Cause.Cause) => Effect.Effect + readonly onSuccess: (a: A) => Effect.Effect + } + ): Effect.Effect +} = dual(2, ( + self: Effect.Effect, + options: { + readonly onFailure: (cause: Cause.Cause) => Effect.Effect + readonly onSuccess: (a: A) => Effect.Effect + } +): Effect.Effect => { + const effect = new EffectPrimitive(OpCodes.OP_ON_SUCCESS_AND_FAILURE) as any + effect.effect_instruction_i0 = self + effect.effect_instruction_i1 = options.onFailure + effect.effect_instruction_i2 = options.onSuccess + return effect +}) + +/* @internal */ +export const matchEffect: { + ( + options: { + readonly onFailure: (e: E) => Effect.Effect + readonly onSuccess: (a: A) => Effect.Effect + } + ): (self: Effect.Effect) => Effect.Effect + ( + self: Effect.Effect, + options: { + readonly onFailure: (e: E) => Effect.Effect + readonly onSuccess: (a: A) => Effect.Effect + } + ): Effect.Effect +} = dual(2, ( + self: Effect.Effect, + options: { + readonly onFailure: (e: E) => Effect.Effect + readonly onSuccess: (a: A) => Effect.Effect + } +): Effect.Effect => + matchCauseEffect(self, { + onFailure: (cause) => { + const defects = internalCause.defects(cause) + if (defects.length > 0) { + return failCause(internalCause.electFailures(cause)) + } + const failures = internalCause.failures(cause) + if (failures.length > 0) { + return options.onFailure(Chunk.unsafeHead(failures)) + } + return failCause(cause as Cause.Cause) + }, + onSuccess: options.onSuccess + })) + +/* @internal */ +export const forEachSequential: { + (f: (a: A, i: number) => Effect.Effect): (self: Iterable) => Effect.Effect, E, R> + (self: Iterable, f: (a: A, i: number) => Effect.Effect): Effect.Effect, E, R> +} = dual( + 2, + (self: Iterable, f: (a: A, i: number) => Effect.Effect): Effect.Effect, E, R> => + suspend(() => { + const arr = Arr.fromIterable(self) + const ret = Arr.allocate(arr.length) + let i = 0 + return as( + whileLoop({ + while: () => i < arr.length, + body: () => f(arr[i], i), + step: (b) => { + ret[i++] = b + } + }), + ret as Array + ) + }) +) + +/* @internal */ +export const forEachSequentialDiscard: { + (f: (a: A, i: number) => Effect.Effect): (self: Iterable) => Effect.Effect + (self: Iterable, f: (a: A, i: number) => Effect.Effect): Effect.Effect +} = dual( + 2, + (self: Iterable, f: (a: A, i: number) => Effect.Effect): Effect.Effect => + suspend(() => { + const arr = Arr.fromIterable(self) + let i = 0 + return whileLoop({ + while: () => i < arr.length, + body: () => f(arr[i], i), + step: () => { + i++ + } + }) + }) +) + +/* @internal */ +export const if_ = dual< + ( + options: { + readonly onTrue: LazyArg> + readonly onFalse: LazyArg> + } + ) => ( + self: Effect.Effect | boolean + ) => Effect.Effect, + ( + self: Effect.Effect | boolean, + options: { + readonly onTrue: LazyArg> + readonly onFalse: LazyArg> + } + ) => Effect.Effect +>( + (args) => typeof args[0] === "boolean" || isEffect(args[0]), + ( + self: Effect.Effect | boolean, + options: { + readonly onTrue: LazyArg> + readonly onFalse: LazyArg> + } + ): Effect.Effect => + isEffect(self) + ? flatMap(self, (b): Effect.Effect => (b ? options.onTrue() : options.onFalse())) + : self + ? options.onTrue() + : options.onFalse() +) + +/* @internal */ +export const interrupt: Effect.Effect = flatMap(fiberId, (fiberId) => interruptWith(fiberId)) + +/* @internal */ +export const interruptWith = (fiberId: FiberId.FiberId): Effect.Effect => + failCause(internalCause.interrupt(fiberId)) + +/* @internal */ +export const interruptible = (self: Effect.Effect): Effect.Effect => { + const effect = new EffectPrimitive(OpCodes.OP_UPDATE_RUNTIME_FLAGS) as any + effect.effect_instruction_i0 = RuntimeFlagsPatch.enable(runtimeFlags_.Interruption) + effect.effect_instruction_i1 = () => self + return effect +} + +/* @internal */ +export const interruptibleMask = ( + f: (restore: (effect: Effect.Effect) => Effect.Effect) => Effect.Effect +): Effect.Effect => + custom(f, function() { + const effect = new EffectPrimitive(OpCodes.OP_UPDATE_RUNTIME_FLAGS) as any + effect.effect_instruction_i0 = RuntimeFlagsPatch.enable(runtimeFlags_.Interruption) + effect.effect_instruction_i1 = (oldFlags: RuntimeFlags.RuntimeFlags) => + runtimeFlags_.interruption(oldFlags) + ? internalCall(() => this.effect_instruction_i0(interruptible)) + : internalCall(() => this.effect_instruction_i0(uninterruptible)) + return effect + }) + +/* @internal */ +export const intoDeferred: { + (deferred: Deferred.Deferred): (self: Effect.Effect) => Effect.Effect + (self: Effect.Effect, deferred: Deferred.Deferred): Effect.Effect +} = dual( + 2, + (self: Effect.Effect, deferred: Deferred.Deferred): Effect.Effect => + uninterruptibleMask((restore) => + flatMap( + exit(restore(self)), + (exit) => deferredDone(deferred, exit) + ) + ) +) + +/* @internal */ +export const map: { + (f: (a: A) => B): (self: Effect.Effect) => Effect.Effect + (self: Effect.Effect, f: (a: A) => B): Effect.Effect +} = dual( + 2, + (self: Effect.Effect, f: (a: A) => B): Effect.Effect => + flatMap(self, (a) => sync(() => f(a))) +) + +/* @internal */ +export const mapBoth: { + ( + options: { readonly onFailure: (e: E) => E2; readonly onSuccess: (a: A) => A2 } + ): (self: Effect.Effect) => Effect.Effect + ( + self: Effect.Effect, + options: { readonly onFailure: (e: E) => E2; readonly onSuccess: (a: A) => A2 } + ): Effect.Effect +} = dual(2, ( + self: Effect.Effect, + options: { readonly onFailure: (e: E) => E2; readonly onSuccess: (a: A) => A2 } +): Effect.Effect => + matchEffect(self, { + onFailure: (e) => failSync(() => options.onFailure(e)), + onSuccess: (a) => sync(() => options.onSuccess(a)) + })) + +/* @internal */ +export const mapError: { + (f: (e: E) => E2): (self: Effect.Effect) => Effect.Effect + (self: Effect.Effect, f: (e: E) => E2): Effect.Effect +} = dual( + 2, + (self: Effect.Effect, f: (e: E) => E2): Effect.Effect => + matchCauseEffect(self, { + onFailure: (cause) => { + const either = internalCause.failureOrCause(cause) + switch (either._tag) { + case "Left": { + return failSync(() => f(either.left)) + } + case "Right": { + return failCause(either.right) + } + } + }, + onSuccess: succeed + }) +) + +/* @internal */ +export const onError: { + ( + cleanup: (cause: Cause.Cause) => Effect.Effect + ): (self: Effect.Effect) => Effect.Effect + ( + self: Effect.Effect, + cleanup: (cause: Cause.Cause) => Effect.Effect + ): Effect.Effect +} = dual(2, ( + self: Effect.Effect, + cleanup: (cause: Cause.Cause) => Effect.Effect +): Effect.Effect => + onExit(self, (exit) => exitIsSuccess(exit) ? void_ : cleanup(exit.effect_instruction_i0))) + +/* @internal */ +export const onExit: { + ( + cleanup: (exit: Exit.Exit) => Effect.Effect + ): (self: Effect.Effect) => Effect.Effect + ( + self: Effect.Effect, + cleanup: (exit: Exit.Exit) => Effect.Effect + ): Effect.Effect +} = dual(2, ( + self: Effect.Effect, + cleanup: (exit: Exit.Exit) => Effect.Effect +): Effect.Effect => + uninterruptibleMask((restore) => + matchCauseEffect(restore(self), { + onFailure: (cause1) => { + const result = exitFailCause(cause1) + return matchCauseEffect(cleanup(result), { + onFailure: (cause2) => exitFailCause(internalCause.sequential(cause1, cause2)), + onSuccess: () => result + }) + }, + onSuccess: (success) => { + const result = exitSucceed(success) + return zipRight(cleanup(result), result) + } + }) + )) + +/* @internal */ +export const onInterrupt: { + ( + cleanup: (interruptors: HashSet.HashSet) => Effect.Effect + ): (self: Effect.Effect) => Effect.Effect + ( + self: Effect.Effect, + cleanup: (interruptors: HashSet.HashSet) => Effect.Effect + ): Effect.Effect +} = dual(2, ( + self: Effect.Effect, + cleanup: (interruptors: HashSet.HashSet) => Effect.Effect +): Effect.Effect => + onExit( + self, + exitMatch({ + onFailure: (cause) => + internalCause.isInterruptedOnly(cause) + ? asVoid(cleanup(internalCause.interruptors(cause))) + : void_, + onSuccess: () => void_ + }) + )) + +/* @internal */ +export const orElse: { + ( + that: LazyArg> + ): (self: Effect.Effect) => Effect.Effect + ( + self: Effect.Effect, + that: LazyArg> + ): Effect.Effect +} = dual( + 2, + ( + self: Effect.Effect, + that: LazyArg> + ): Effect.Effect => attemptOrElse(self, that, succeed) +) + +/* @internal */ +export const orDie = (self: Effect.Effect): Effect.Effect => orDieWith(self, identity) + +/* @internal */ +export const orDieWith: { + (f: (error: E) => unknown): (self: Effect.Effect) => Effect.Effect + (self: Effect.Effect, f: (error: E) => unknown): Effect.Effect +} = dual( + 2, + (self: Effect.Effect, f: (error: E) => unknown): Effect.Effect => + matchEffect(self, { + onFailure: (e) => die(f(e)), + onSuccess: succeed + }) +) + +/* @internal */ +export const partitionMap: ( + elements: Iterable, + f: (a: A) => Either.Either +) => [left: Array, right: Array] = Arr.partitionMap +/* @internal */ +export const runtimeFlags: Effect.Effect = withFiberRuntime((_, status) => + succeed(status.runtimeFlags) +) + +/* @internal */ +export const succeed = (value: A): Effect.Effect => { + const effect = new EffectPrimitiveSuccess(OpCodes.OP_SUCCESS) as any + effect.effect_instruction_i0 = value + return effect +} + +/* @internal */ +export const suspend = (evaluate: LazyArg>): Effect.Effect => { + const effect = new EffectPrimitive(OpCodes.OP_COMMIT) as any + effect.commit = evaluate + return effect +} + +/* @internal */ +export const sync = (thunk: LazyArg): Effect.Effect => { + const effect = new EffectPrimitive(OpCodes.OP_SYNC) as any + effect.effect_instruction_i0 = thunk + return effect +} + +/* @internal */ +export const tap = dual< + { + ( + f: (a: NoInfer) => X + ): ( + self: Effect.Effect + ) => [X] extends [Effect.Effect] ? Effect.Effect + : [X] extends [PromiseLike] ? Effect.Effect + : Effect.Effect + ( + f: (a: NoInfer) => Effect.Effect, + options: { onlyEffect: true } + ): ( + self: Effect.Effect + ) => Effect.Effect + ( + f: NotFunction + ): ( + self: Effect.Effect + ) => [X] extends [Effect.Effect] ? Effect.Effect + : [X] extends [PromiseLike] ? Effect.Effect + : Effect.Effect + ( + f: Effect.Effect, + options: { onlyEffect: true } + ): ( + self: Effect.Effect + ) => Effect.Effect + }, + { + ( + self: Effect.Effect, + f: (a: NoInfer) => X + ): [X] extends [Effect.Effect] ? Effect.Effect + : [X] extends [PromiseLike] ? Effect.Effect + : Effect.Effect + ( + self: Effect.Effect, + f: (a: NoInfer) => Effect.Effect, + options: { onlyEffect: true } + ): Effect.Effect + ( + self: Effect.Effect, + f: NotFunction + ): [X] extends [Effect.Effect] ? Effect.Effect + : [X] extends [PromiseLike] ? Effect.Effect + : Effect.Effect + ( + self: Effect.Effect, + f: Effect.Effect, + options: { onlyEffect: true } + ): Effect.Effect + } +>( + (args) => args.length === 3 || args.length === 2 && !(isObject(args[1]) && "onlyEffect" in args[1]), + (self: Effect.Effect, f: X) => + flatMap(self, (a) => { + const b = typeof f === "function" ? (f as any)(a) : f + if (isEffect(b)) { + return as(b, a) + } else if (isPromiseLike(b)) { + return unsafeAsync((resume) => { + b.then((_) => resume(succeed(a)), (e) => + resume(fail(new UnknownException(e, "An unknown error occurred in Effect.tap")))) + }) + } + return succeed(a) + }) +) + +/* @internal */ +export const transplant = ( + f: (grafter: (effect: Effect.Effect) => Effect.Effect) => Effect.Effect +): Effect.Effect => + withFiberRuntime((state) => { + const scopeOverride = state.getFiberRef(currentForkScopeOverride) + const scope = pipe(scopeOverride, Option.getOrElse(() => state.scope())) + return f(fiberRefLocally(currentForkScopeOverride, Option.some(scope))) + }) + +/* @internal */ +export const attemptOrElse: { + ( + that: LazyArg>, + onSuccess: (a: A) => Effect.Effect + ): (self: Effect.Effect) => Effect.Effect + ( + self: Effect.Effect, + that: LazyArg>, + onSuccess: (a: A) => Effect.Effect + ): Effect.Effect +} = dual(3, ( + self: Effect.Effect, + that: LazyArg>, + onSuccess: (a: A) => Effect.Effect +): Effect.Effect => + matchCauseEffect(self, { + onFailure: (cause) => { + const defects = internalCause.defects(cause) + if (defects.length > 0) { + return failCause(Option.getOrThrow(internalCause.keepDefectsAndElectFailures(cause))) + } + return that() + }, + onSuccess + })) + +/* @internal */ +export const uninterruptible: (self: Effect.Effect) => Effect.Effect = ( + self: Effect.Effect +): Effect.Effect => { + const effect = new EffectPrimitive(OpCodes.OP_UPDATE_RUNTIME_FLAGS) as any + effect.effect_instruction_i0 = RuntimeFlagsPatch.disable(runtimeFlags_.Interruption) + effect.effect_instruction_i1 = () => self + return effect +} + +/* @internal */ +export const uninterruptibleMask = ( + f: (restore: (effect: Effect.Effect) => Effect.Effect) => Effect.Effect +): Effect.Effect => + custom(f, function() { + const effect = new EffectPrimitive(OpCodes.OP_UPDATE_RUNTIME_FLAGS) as any + effect.effect_instruction_i0 = RuntimeFlagsPatch.disable(runtimeFlags_.Interruption) + effect.effect_instruction_i1 = (oldFlags: RuntimeFlags.RuntimeFlags) => + runtimeFlags_.interruption(oldFlags) + ? internalCall(() => this.effect_instruction_i0(interruptible)) + : internalCall(() => this.effect_instruction_i0(uninterruptible)) + return effect + }) + +const void_: Effect.Effect = succeed(void 0) +export { + /* @internal */ + void_ as void +} + +/* @internal */ +export const updateRuntimeFlags = (patch: RuntimeFlagsPatch.RuntimeFlagsPatch): Effect.Effect => { + const effect = new EffectPrimitive(OpCodes.OP_UPDATE_RUNTIME_FLAGS) as any + effect.effect_instruction_i0 = patch + effect.effect_instruction_i1 = void 0 + return effect +} + +/* @internal */ +export const whenEffect: { + ( + condition: Effect.Effect + ): ( + effect: Effect.Effect + ) => Effect.Effect, E | E2, R | R2> + ( + self: Effect.Effect, + condition: Effect.Effect + ): Effect.Effect, E | E2, R | R2> +} = dual(2, ( + self: Effect.Effect, + condition: Effect.Effect +): Effect.Effect, E | E2, R | R2> => + flatMap(condition, (b) => { + if (b) { + return pipe(self, map(Option.some)) + } + return succeed(Option.none()) + })) + +/* @internal */ +export const whileLoop = ( + options: { + readonly while: LazyArg + readonly body: LazyArg> + readonly step: (a: A) => void + } +): Effect.Effect => { + const effect = new EffectPrimitive(OpCodes.OP_WHILE) as any + effect.effect_instruction_i0 = options.while + effect.effect_instruction_i1 = options.body + effect.effect_instruction_i2 = options.step + return effect +} + +/* @internal */ +export const fromIterator = >, AEff>( + iterator: LazyArg> +): Effect.Effect< + AEff, + [Eff] extends [never] ? never : [Eff] extends [YieldWrap>] ? E : never, + [Eff] extends [never] ? never : [Eff] extends [YieldWrap>] ? R : never +> => + suspend(() => { + const effect = new EffectPrimitive(OpCodes.OP_ITERATOR) as any + effect.effect_instruction_i0 = iterator() + return effect + }) + +/* @internal */ +export const gen: typeof Effect.gen = function() { + const f = arguments.length === 1 ? arguments[0] : arguments[1].bind(arguments[0]) + return fromIterator(() => f(pipe)) +} + +/** @internal */ +export const fnUntraced: Effect.fn.Untraced = (body: Function, ...pipeables: Array) => + Object.defineProperty( + pipeables.length === 0 + ? function(this: any, ...args: Array) { + return fromIterator(() => body.apply(this, args)) + } + : function(this: any, ...args: Array) { + let effect = fromIterator(() => body.apply(this, args)) + for (const x of pipeables) { + effect = x(effect, ...args) + } + return effect + }, + "length", + { value: body.length, configurable: true } + ) + +/* @internal */ +export const withConcurrency = dual< + (concurrency: number | "unbounded") => (self: Effect.Effect) => Effect.Effect, + (self: Effect.Effect, concurrency: number | "unbounded") => Effect.Effect +>(2, (self, concurrency) => fiberRefLocally(self, currentConcurrency, concurrency)) + +/* @internal */ +export const withRequestBatching = dual< + (requestBatching: boolean) => (self: Effect.Effect) => Effect.Effect, + (self: Effect.Effect, requestBatching: boolean) => Effect.Effect +>(2, (self, requestBatching) => fiberRefLocally(self, currentRequestBatching, requestBatching)) + +/* @internal */ +export const withRuntimeFlags = dual< + (update: RuntimeFlagsPatch.RuntimeFlagsPatch) => (self: Effect.Effect) => Effect.Effect, + (self: Effect.Effect, update: RuntimeFlagsPatch.RuntimeFlagsPatch) => Effect.Effect +>(2, (self, update) => { + const effect = new EffectPrimitive(OpCodes.OP_UPDATE_RUNTIME_FLAGS) as any + effect.effect_instruction_i0 = update + effect.effect_instruction_i1 = () => self + return effect +}) + +/** @internal */ +export const withTracerEnabled = dual< + (enabled: boolean) => (effect: Effect.Effect) => Effect.Effect, + (effect: Effect.Effect, enabled: boolean) => Effect.Effect +>(2, (effect, enabled) => + fiberRefLocally( + effect, + currentTracerEnabled, + enabled + )) + +/** @internal */ +export const withTracerTiming = dual< + (enabled: boolean) => (effect: Effect.Effect) => Effect.Effect, + (effect: Effect.Effect, enabled: boolean) => Effect.Effect +>(2, (effect, enabled) => + fiberRefLocally( + effect, + currentTracerTimingEnabled, + enabled + )) + +/* @internal */ +export const yieldNow = (options?: { + readonly priority?: number | undefined +}): Effect.Effect => { + const effect = new EffectPrimitive(OpCodes.OP_YIELD) as any + return typeof options?.priority !== "undefined" ? + withSchedulingPriority(effect, options.priority) : + effect +} + +/* @internal */ +export const zip = dual< + ( + that: Effect.Effect + ) => ( + self: Effect.Effect + ) => Effect.Effect<[A, A2], E | E2, R | R2>, + ( + self: Effect.Effect, + that: Effect.Effect + ) => Effect.Effect<[A, A2], E | E2, R | R2> +>(2, ( + self: Effect.Effect, + that: Effect.Effect +): Effect.Effect<[A, A2], E | E2, R | R2> => flatMap(self, (a) => map(that, (b) => [a, b]))) + +/* @internal */ +export const zipFlatten: { + ( + that: Effect.Effect + ): , E, R>( + self: Effect.Effect + ) => Effect.Effect<[...A, A2], E | E2, R | R2> + , E, R, A2, E2, R2>( + self: Effect.Effect, + that: Effect.Effect + ): Effect.Effect<[...A, A2], E | E2, R | R2> +} = dual(2, , E, R, A2, E2, R2>( + self: Effect.Effect, + that: Effect.Effect +): Effect.Effect<[...A, A2], E | E2, R | R2> => flatMap(self, (a) => map(that, (b) => [...a, b]))) + +/* @internal */ +export const zipLeft: { + ( + that: Effect.Effect + ): (self: Effect.Effect) => Effect.Effect + ( + self: Effect.Effect, + that: Effect.Effect + ): Effect.Effect +} = dual(2, ( + self: Effect.Effect, + that: Effect.Effect +): Effect.Effect => flatMap(self, (a) => as(that, a))) + +/* @internal */ +export const zipRight: { + ( + that: Effect.Effect + ): (self: Effect.Effect) => Effect.Effect + ( + self: Effect.Effect, + that: Effect.Effect + ): Effect.Effect +} = dual(2, ( + self: Effect.Effect, + that: Effect.Effect +): Effect.Effect => flatMap(self, () => that)) + +/* @internal */ +export const zipWith: { + ( + that: Effect.Effect, + f: (a: A, b: A2) => B + ): (self: Effect.Effect) => Effect.Effect + ( + self: Effect.Effect, + that: Effect.Effect, + f: (a: A, b: A2) => B + ): Effect.Effect +} = dual(3, ( + self: Effect.Effect, + that: Effect.Effect, + f: (a: A, b: A2) => B +): Effect.Effect => flatMap(self, (a) => map(that, (b) => f(a, b)))) + +/* @internal */ +export const never: Effect.Effect = asyncInterrupt(() => { + const interval = setInterval(() => { + // + }, 2 ** 31 - 1) + return sync(() => clearInterval(interval)) +}) + +// ----------------------------------------------------------------------------- +// Fiber +// ----------------------------------------------------------------------------- + +/* @internal */ +export const interruptFiber = (self: Fiber.Fiber): Effect.Effect> => + flatMap(fiberId, (fiberId) => pipe(self, interruptAsFiber(fiberId))) + +/* @internal */ +export const interruptAsFiber = dual< + (fiberId: FiberId.FiberId) => (self: Fiber.Fiber) => Effect.Effect>, + (self: Fiber.Fiber, fiberId: FiberId.FiberId) => Effect.Effect> +>(2, (self, fiberId) => flatMap(self.interruptAsFork(fiberId), () => self.await)) + +// ----------------------------------------------------------------------------- +// LogLevel +// ----------------------------------------------------------------------------- + +/** @internal */ +export const logLevelAll: LogLevel.LogLevel = { + _tag: "All", + syslog: 0, + label: "ALL", + ordinal: Number.MIN_SAFE_INTEGER, + pipe() { + return pipeArguments(this, arguments) + } +} + +/** @internal */ +export const logLevelFatal: LogLevel.LogLevel = { + _tag: "Fatal", + syslog: 2, + label: "FATAL", + ordinal: 50000, + pipe() { + return pipeArguments(this, arguments) + } +} + +/** @internal */ +export const logLevelError: LogLevel.LogLevel = { + _tag: "Error", + syslog: 3, + label: "ERROR", + ordinal: 40000, + pipe() { + return pipeArguments(this, arguments) + } +} + +/** @internal */ +export const logLevelWarning: LogLevel.LogLevel = { + _tag: "Warning", + syslog: 4, + label: "WARN", + ordinal: 30000, + pipe() { + return pipeArguments(this, arguments) + } +} + +/** @internal */ +export const logLevelInfo: LogLevel.LogLevel = { + _tag: "Info", + syslog: 6, + label: "INFO", + ordinal: 20000, + pipe() { + return pipeArguments(this, arguments) + } +} + +/** @internal */ +export const logLevelDebug: LogLevel.LogLevel = { + _tag: "Debug", + syslog: 7, + label: "DEBUG", + ordinal: 10000, + pipe() { + return pipeArguments(this, arguments) + } +} + +/** @internal */ +export const logLevelTrace: LogLevel.LogLevel = { + _tag: "Trace", + syslog: 7, + label: "TRACE", + ordinal: 0, + pipe() { + return pipeArguments(this, arguments) + } +} + +/** @internal */ +export const logLevelNone: LogLevel.LogLevel = { + _tag: "None", + syslog: 7, + label: "OFF", + ordinal: Number.MAX_SAFE_INTEGER, + pipe() { + return pipeArguments(this, arguments) + } +} + +/** @internal */ +export const allLogLevels: ReadonlyArray = [ + logLevelAll, + logLevelTrace, + logLevelDebug, + logLevelInfo, + logLevelWarning, + logLevelError, + logLevelFatal, + logLevelNone +] + +// ----------------------------------------------------------------------------- +// FiberRef +// ----------------------------------------------------------------------------- + +/** @internal */ +const FiberRefSymbolKey = "effect/FiberRef" + +/** @internal */ +export const FiberRefTypeId: FiberRef.FiberRefTypeId = Symbol.for( + FiberRefSymbolKey +) as FiberRef.FiberRefTypeId + +const fiberRefVariance = { + /* c8 ignore next */ + _A: (_: any) => _ +} + +/* @internal */ +export const fiberRefGet = (self: FiberRef.FiberRef): Effect.Effect => + withFiberRuntime((fiber) => exitSucceed(fiber.getFiberRef(self))) + +/* @internal */ +export const fiberRefGetAndSet = dual< + (value: A) => (self: FiberRef.FiberRef) => Effect.Effect, + (self: FiberRef.FiberRef, value: A) => Effect.Effect +>(2, (self, value) => fiberRefModify(self, (v) => [v, value] as const)) + +/* @internal */ +export const fiberRefGetAndUpdate = dual< + (f: (a: A) => A) => (self: FiberRef.FiberRef) => Effect.Effect, + (self: FiberRef.FiberRef, f: (a: A) => A) => Effect.Effect +>(2, (self, f) => fiberRefModify(self, (v) => [v, f(v)] as const)) + +/* @internal */ +export const fiberRefGetAndUpdateSome = dual< + ( + pf: (a: A) => Option.Option + ) => (self: FiberRef.FiberRef) => Effect.Effect, + ( + self: FiberRef.FiberRef, + pf: (a: A) => Option.Option + ) => Effect.Effect +>(2, (self, pf) => fiberRefModify(self, (v) => [v, Option.getOrElse(pf(v), () => v)] as const)) + +/* @internal */ +export const fiberRefGetWith = dual< + (f: (a: A) => Effect.Effect) => (self: FiberRef.FiberRef) => Effect.Effect, + (self: FiberRef.FiberRef, f: (a: A) => Effect.Effect) => Effect.Effect +>(2, (self, f) => flatMap(fiberRefGet(self), f)) + +/* @internal */ +export const fiberRefSet = dual< + (value: A) => (self: FiberRef.FiberRef) => Effect.Effect, + (self: FiberRef.FiberRef, value: A) => Effect.Effect +>(2, (self, value) => fiberRefModify(self, () => [void 0, value] as const)) + +/* @internal */ +export const fiberRefDelete = (self: FiberRef.FiberRef): Effect.Effect => + withFiberRuntime((state) => { + state.unsafeDeleteFiberRef(self) + return void_ + }) + +/* @internal */ +export const fiberRefReset = (self: FiberRef.FiberRef): Effect.Effect => fiberRefSet(self, self.initial) + +/* @internal */ +export const fiberRefModify = dual< + (f: (a: A) => readonly [B, A]) => (self: FiberRef.FiberRef) => Effect.Effect, + (self: FiberRef.FiberRef, f: (a: A) => readonly [B, A]) => Effect.Effect +>(2, ( + self: FiberRef.FiberRef, + f: (a: A) => readonly [B, A] +): Effect.Effect => + withFiberRuntime((state) => { + const [b, a] = f(state.getFiberRef(self) as A) + state.setFiberRef(self, a) + return succeed(b) + })) + +/* @internal */ +export const fiberRefModifySome = ( + self: FiberRef.FiberRef, + def: B, + f: (a: A) => Option.Option +): Effect.Effect => fiberRefModify(self, (v) => Option.getOrElse(f(v), () => [def, v] as const)) + +/* @internal */ +export const fiberRefUpdate = dual< + (f: (a: A) => A) => (self: FiberRef.FiberRef) => Effect.Effect, + (self: FiberRef.FiberRef, f: (a: A) => A) => Effect.Effect +>(2, (self, f) => fiberRefModify(self, (v) => [void 0, f(v)] as const)) + +/* @internal */ +export const fiberRefUpdateSome = dual< + (pf: (a: A) => Option.Option) => (self: FiberRef.FiberRef) => Effect.Effect, + (self: FiberRef.FiberRef, pf: (a: A) => Option.Option) => Effect.Effect +>(2, (self, pf) => fiberRefModify(self, (v) => [void 0, Option.getOrElse(pf(v), () => v)] as const)) + +/* @internal */ +export const fiberRefUpdateAndGet = dual< + (f: (a: A) => A) => (self: FiberRef.FiberRef) => Effect.Effect, + (self: FiberRef.FiberRef, f: (a: A) => A) => Effect.Effect +>(2, (self, f) => + fiberRefModify(self, (v) => { + const result = f(v) + return [result, result] as const + })) + +/* @internal */ +export const fiberRefUpdateSomeAndGet = dual< + (pf: (a: A) => Option.Option) => (self: FiberRef.FiberRef) => Effect.Effect, + (self: FiberRef.FiberRef, pf: (a: A) => Option.Option) => Effect.Effect +>(2, (self, pf) => + fiberRefModify(self, (v) => { + const result = Option.getOrElse(pf(v), () => v) + return [result, result] as const + })) + +// circular +/** @internal */ +const RequestResolverSymbolKey = "effect/RequestResolver" + +/** @internal */ +export const RequestResolverTypeId: RequestResolver.RequestResolverTypeId = Symbol.for( + RequestResolverSymbolKey +) as RequestResolver.RequestResolverTypeId + +const requestResolverVariance = { + /* c8 ignore next */ + _A: (_: unknown) => _, + /* c8 ignore next */ + _R: (_: never) => _ +} + +/** @internal */ +export class RequestResolverImpl implements RequestResolver.RequestResolver { + readonly [RequestResolverTypeId] = requestResolverVariance + constructor( + readonly runAll: ( + requests: Array>> + ) => Effect.Effect, + readonly target?: unknown + ) { + } + [Hash.symbol](): number { + return Hash.cached(this, this.target ? Hash.hash(this.target) : Hash.random(this)) + } + [Equal.symbol](that: unknown): boolean { + return this.target ? + isRequestResolver(that) && Equal.equals(this.target, (that as RequestResolverImpl).target) : + this === that + } + identified(...ids: Array): RequestResolver.RequestResolver { + return new RequestResolverImpl(this.runAll, Chunk.fromIterable(ids)) + } + pipe() { + return pipeArguments(this, arguments) + } +} + +/** @internal */ +export const isRequestResolver = (u: unknown): u is RequestResolver.RequestResolver => + hasProperty(u, RequestResolverTypeId) + +// end + +/** @internal */ +export const resolverLocally = dual< + ( + self: FiberRef.FiberRef, + value: A + ) => >( + use: RequestResolver.RequestResolver + ) => RequestResolver.RequestResolver, + , A>( + use: RequestResolver.RequestResolver, + self: FiberRef.FiberRef, + value: A + ) => RequestResolver.RequestResolver +>(3, , A>( + use: RequestResolver.RequestResolver, + self: FiberRef.FiberRef, + value: A +): RequestResolver.RequestResolver => + new RequestResolverImpl( + (requests) => + fiberRefLocally( + use.runAll(requests), + self, + value + ), + Chunk.make("Locally", use, self, value) + )) + +/** @internal */ +export const requestBlockLocally = ( + self: BlockedRequests.RequestBlock, + ref: FiberRef.FiberRef, + value: A +): BlockedRequests.RequestBlock => blockedRequests_.reduce(self, LocallyReducer(ref, value)) + +const LocallyReducer = ( + ref: FiberRef.FiberRef, + value: A +): BlockedRequests.RequestBlock.Reducer => ({ + emptyCase: () => blockedRequests_.empty, + parCase: (left, right) => blockedRequests_.par(left, right), + seqCase: (left, right) => blockedRequests_.seq(left, right), + singleCase: (dataSource, blockedRequest) => + blockedRequests_.single( + resolverLocally(dataSource, ref, value), + blockedRequest as any + ) +}) + +/* @internal */ +export const fiberRefLocally: { + (self: FiberRef.FiberRef, value: A): (use: Effect.Effect) => Effect.Effect + (use: Effect.Effect, self: FiberRef.FiberRef, value: A): Effect.Effect +} = dual( + 3, + (use: Effect.Effect, self: FiberRef.FiberRef, value: A): Effect.Effect => + acquireUseRelease( + zipLeft(fiberRefGet(self), fiberRefSet(self, value)), + () => use, + (oldValue) => fiberRefSet(self, oldValue) + ) +) + +/* @internal */ +export const fiberRefLocallyWith = dual< + (self: FiberRef.FiberRef, f: (a: A) => A) => (use: Effect.Effect) => Effect.Effect, + (use: Effect.Effect, self: FiberRef.FiberRef, f: (a: A) => A) => Effect.Effect +>(3, (use, self, f) => fiberRefGetWith(self, (a) => fiberRefLocally(use, self, f(a)))) + +/** @internal */ +export const fiberRefUnsafeMake = ( + initial: Value, + options?: { + readonly fork?: ((a: Value) => Value) | undefined + readonly join?: ((left: Value, right: Value) => Value) | undefined + } +): FiberRef.FiberRef => + fiberRefUnsafeMakePatch(initial, { + differ: internalDiffer.update(), + fork: options?.fork ?? identity, + join: options?.join + }) + +/** @internal */ +export const fiberRefUnsafeMakeHashSet = ( + initial: HashSet.HashSet +): FiberRef.FiberRef> => { + const differ = internalDiffer.hashSet() + return fiberRefUnsafeMakePatch(initial, { + differ, + fork: differ.empty + }) +} + +/** @internal */ +export const fiberRefUnsafeMakeReadonlyArray = ( + initial: ReadonlyArray +): FiberRef.FiberRef> => { + const differ = internalDiffer.readonlyArray(internalDiffer.update()) + return fiberRefUnsafeMakePatch(initial, { + differ, + fork: differ.empty + }) +} + +/** @internal */ +export const fiberRefUnsafeMakeContext = ( + initial: Context.Context +): FiberRef.FiberRef> => { + const differ = internalDiffer.environment() + return fiberRefUnsafeMakePatch(initial, { + differ, + fork: differ.empty + }) +} + +/** @internal */ +export const fiberRefUnsafeMakePatch = ( + initial: Value, + options: { + readonly differ: Differ.Differ + readonly fork: Patch + readonly join?: ((oldV: Value, newV: Value) => Value) | undefined + } +): FiberRef.FiberRef => { + const _fiberRef = { + ...CommitPrototype, + [FiberRefTypeId]: fiberRefVariance, + initial, + commit() { + return fiberRefGet(this) + }, + diff: (oldValue: Value, newValue: Value) => options.differ.diff(oldValue, newValue), + combine: (first: Patch, second: Patch) => options.differ.combine(first, second), + patch: (patch: Patch) => (oldValue: Value) => options.differ.patch(patch, oldValue), + fork: options.fork, + join: options.join ?? ((_, n) => n) + } + return _fiberRef +} + +/** @internal */ +export const fiberRefUnsafeMakeRuntimeFlags = ( + initial: RuntimeFlags.RuntimeFlags +): FiberRef.FiberRef => + fiberRefUnsafeMakePatch(initial, { + differ: runtimeFlags_.differ, + fork: runtimeFlags_.differ.empty + }) + +/** @internal */ +export const currentContext: FiberRef.FiberRef> = globalValue( + Symbol.for("effect/FiberRef/currentContext"), + () => fiberRefUnsafeMakeContext(Context.empty()) +) + +/** @internal */ +export const currentSchedulingPriority: FiberRef.FiberRef = globalValue( + Symbol.for("effect/FiberRef/currentSchedulingPriority"), + () => fiberRefUnsafeMake(0) +) + +/** @internal */ +export const currentMaxOpsBeforeYield: FiberRef.FiberRef = globalValue( + Symbol.for("effect/FiberRef/currentMaxOpsBeforeYield"), + () => fiberRefUnsafeMake(2048) +) + +/** @internal */ +export const currentLogAnnotations: FiberRef.FiberRef> = globalValue( + Symbol.for("effect/FiberRef/currentLogAnnotation"), + () => fiberRefUnsafeMake(HashMap.empty()) +) + +/** @internal */ +export const currentLogLevel: FiberRef.FiberRef = globalValue( + Symbol.for("effect/FiberRef/currentLogLevel"), + () => fiberRefUnsafeMake(logLevelInfo) +) + +/** @internal */ +export const currentLogSpan: FiberRef.FiberRef> = globalValue( + Symbol.for("effect/FiberRef/currentLogSpan"), + () => fiberRefUnsafeMake(List.empty()) +) + +/** @internal */ +export const withSchedulingPriority = dual< + (priority: number) => (self: Effect.Effect) => Effect.Effect, + (self: Effect.Effect, priority: number) => Effect.Effect +>(2, (self, scheduler) => fiberRefLocally(self, currentSchedulingPriority, scheduler)) + +/** @internal */ +export const withMaxOpsBeforeYield = dual< + (priority: number) => (self: Effect.Effect) => Effect.Effect, + (self: Effect.Effect, priority: number) => Effect.Effect +>(2, (self, scheduler) => fiberRefLocally(self, currentMaxOpsBeforeYield, scheduler)) + +/** @internal */ +export const currentConcurrency: FiberRef.FiberRef<"unbounded" | number> = globalValue( + Symbol.for("effect/FiberRef/currentConcurrency"), + () => fiberRefUnsafeMake<"unbounded" | number>("unbounded") +) + +/** + * @internal + */ +export const currentRequestBatching = globalValue( + Symbol.for("effect/FiberRef/currentRequestBatching"), + () => fiberRefUnsafeMake(true) +) + +/** @internal */ +export const currentUnhandledErrorLogLevel: FiberRef.FiberRef> = globalValue( + Symbol.for("effect/FiberRef/currentUnhandledErrorLogLevel"), + () => fiberRefUnsafeMake(Option.some(logLevelDebug)) +) + +/** @internal */ +export const currentVersionMismatchErrorLogLevel: FiberRef.FiberRef> = globalValue( + Symbol.for("effect/FiberRef/versionMismatchErrorLogLevel"), + () => fiberRefUnsafeMake(Option.some(logLevelWarning)) +) + +/** @internal */ +export const withUnhandledErrorLogLevel = dual< + (level: Option.Option) => (self: Effect.Effect) => Effect.Effect, + (self: Effect.Effect, level: Option.Option) => Effect.Effect +>(2, (self, level) => fiberRefLocally(self, currentUnhandledErrorLogLevel, level)) + +/** @internal */ +export const currentMetricLabels: FiberRef.FiberRef> = globalValue( + Symbol.for("effect/FiberRef/currentMetricLabels"), + () => fiberRefUnsafeMakeReadonlyArray(Arr.empty()) +) + +/* @internal */ +export const metricLabels: Effect.Effect> = fiberRefGet( + currentMetricLabels +) + +/** @internal */ +export const currentForkScopeOverride: FiberRef.FiberRef> = globalValue( + Symbol.for("effect/FiberRef/currentForkScopeOverride"), + () => + fiberRefUnsafeMake(Option.none(), { + fork: () => Option.none() as Option.Option, + join: (parent, _) => parent + }) +) + +/** @internal */ +export const currentInterruptedCause: FiberRef.FiberRef> = globalValue( + Symbol.for("effect/FiberRef/currentInterruptedCause"), + () => + fiberRefUnsafeMake(internalCause.empty, { + fork: () => internalCause.empty, + join: (parent, _) => parent + }) +) + +/** @internal */ +export const currentTracerEnabled: FiberRef.FiberRef = globalValue( + Symbol.for("effect/FiberRef/currentTracerEnabled"), + () => fiberRefUnsafeMake(true) +) + +/** @internal */ +export const currentTracerTimingEnabled: FiberRef.FiberRef = globalValue( + Symbol.for("effect/FiberRef/currentTracerTiming"), + () => fiberRefUnsafeMake(true) +) + +/** @internal */ +export const currentTracerSpanAnnotations: FiberRef.FiberRef> = globalValue( + Symbol.for("effect/FiberRef/currentTracerSpanAnnotations"), + () => fiberRefUnsafeMake(HashMap.empty()) +) + +/** @internal */ +export const currentTracerSpanLinks: FiberRef.FiberRef> = globalValue( + Symbol.for("effect/FiberRef/currentTracerSpanLinks"), + () => fiberRefUnsafeMake(Chunk.empty()) +) + +// ----------------------------------------------------------------------------- +// Scope +// ----------------------------------------------------------------------------- + +/** @internal */ +export const ScopeTypeId: Scope.ScopeTypeId = Symbol.for("effect/Scope") as Scope.ScopeTypeId + +/** @internal */ +export const CloseableScopeTypeId: Scope.CloseableScopeTypeId = Symbol.for( + "effect/CloseableScope" +) as Scope.CloseableScopeTypeId + +/* @internal */ +export const scopeAddFinalizer = ( + self: Scope.Scope, + finalizer: Effect.Effect +): Effect.Effect => self.addFinalizer(() => asVoid(finalizer)) + +/* @internal */ +export const scopeAddFinalizerExit = ( + self: Scope.Scope, + finalizer: Scope.Scope.Finalizer +): Effect.Effect => self.addFinalizer(finalizer) + +/* @internal */ +export const scopeClose = ( + self: Scope.Scope.Closeable, + exit: Exit.Exit +): Effect.Effect => self.close(exit) + +/* @internal */ +export const scopeFork = ( + self: Scope.Scope, + strategy: ExecutionStrategy.ExecutionStrategy +): Effect.Effect => self.fork(strategy) + +// ----------------------------------------------------------------------------- +// Cause +// ----------------------------------------------------------------------------- + +/** @internal */ +export const causeSquash = (self: Cause.Cause): unknown => { + return causeSquashWith(identity)(self) +} + +/** @internal */ +export const causeSquashWith = dual< + (f: (error: E) => unknown) => (self: Cause.Cause) => unknown, + (self: Cause.Cause, f: (error: E) => unknown) => unknown +>(2, (self, f) => { + const option = pipe(self, internalCause.failureOption, Option.map(f)) + switch (option._tag) { + case "None": { + return pipe( + internalCause.defects(self), + Chunk.head, + Option.match({ + onNone: () => { + const interrupts = Arr.fromIterable(internalCause.interruptors(self)).flatMap((fiberId) => + Arr.fromIterable(FiberId.ids(fiberId)).map((id) => `#${id}`) + ) + return new InterruptedException(interrupts ? `Interrupted by fibers: ${interrupts.join(", ")}` : void 0) + }, + onSome: identity + }) + ) + } + case "Some": { + return option.value + } + } +}) + +// ----------------------------------------------------------------------------- +// Errors +// ----------------------------------------------------------------------------- + +/** @internal */ +export const YieldableError: new(message?: string, options?: ErrorOptions) => Cause.YieldableError = (function() { + class YieldableError extends globalThis.Error { + commit() { + return fail(this) + } + toJSON() { + const obj = { ...this } + if (this.message) obj.message = this.message + if (this.cause) obj.cause = this.cause + return obj + } + [NodeInspectSymbol]() { + if (this.toString !== globalThis.Error.prototype.toString) { + return this.stack ? `${this.toString()}\n${this.stack.split("\n").slice(1).join("\n")}` : this.toString() + } else if ("Bun" in globalThis) { + return internalCause.pretty(internalCause.fail(this), { renderErrorCause: true }) + } + return this + } + } + // @effect-diagnostics-next-line floatingEffect:off + Object.assign(YieldableError.prototype, StructuralCommitPrototype) + return YieldableError as any +})() + +const makeException = ( + proto: Omit, + tag: T["_tag"] +): new(message?: string | undefined) => T => { + class Base extends YieldableError { + readonly _tag = tag + } + Object.assign(Base.prototype, proto) + ;(Base.prototype as any).name = tag + return Base as any +} + +/** @internal */ +export const RuntimeExceptionTypeId: Cause.RuntimeExceptionTypeId = Symbol.for( + "effect/Cause/errors/RuntimeException" +) as Cause.RuntimeExceptionTypeId + +/** @internal */ +export const RuntimeException = makeException({ + [RuntimeExceptionTypeId]: RuntimeExceptionTypeId +}, "RuntimeException") + +/** @internal */ +export const isRuntimeException = (u: unknown): u is Cause.RuntimeException => hasProperty(u, RuntimeExceptionTypeId) + +/** @internal */ +export const InterruptedExceptionTypeId: Cause.InterruptedExceptionTypeId = Symbol.for( + "effect/Cause/errors/InterruptedException" +) as Cause.InterruptedExceptionTypeId + +/** @internal */ +export const InterruptedException = makeException({ + [InterruptedExceptionTypeId]: InterruptedExceptionTypeId +}, "InterruptedException") + +/** @internal */ +export const isInterruptedException = (u: unknown): u is Cause.InterruptedException => + hasProperty(u, InterruptedExceptionTypeId) + +/** @internal */ +export const IllegalArgumentExceptionTypeId: Cause.IllegalArgumentExceptionTypeId = Symbol.for( + "effect/Cause/errors/IllegalArgument" +) as Cause.IllegalArgumentExceptionTypeId + +/** @internal */ +export const IllegalArgumentException = makeException({ + [IllegalArgumentExceptionTypeId]: IllegalArgumentExceptionTypeId +}, "IllegalArgumentException") + +/** @internal */ +export const isIllegalArgumentException = (u: unknown): u is Cause.IllegalArgumentException => + hasProperty(u, IllegalArgumentExceptionTypeId) + +/** @internal */ +export const NoSuchElementExceptionTypeId: Cause.NoSuchElementExceptionTypeId = Symbol.for( + "effect/Cause/errors/NoSuchElement" +) as Cause.NoSuchElementExceptionTypeId + +/** @internal */ +export const NoSuchElementException = makeException({ + [NoSuchElementExceptionTypeId]: NoSuchElementExceptionTypeId +}, "NoSuchElementException") + +/** @internal */ +export const isNoSuchElementException = (u: unknown): u is Cause.NoSuchElementException => + hasProperty(u, NoSuchElementExceptionTypeId) + +/** @internal */ +export const InvalidPubSubCapacityExceptionTypeId: Cause.InvalidPubSubCapacityExceptionTypeId = Symbol.for( + "effect/Cause/errors/InvalidPubSubCapacityException" +) as Cause.InvalidPubSubCapacityExceptionTypeId + +/** @internal */ +export const InvalidPubSubCapacityException = makeException({ + [InvalidPubSubCapacityExceptionTypeId]: InvalidPubSubCapacityExceptionTypeId +}, "InvalidPubSubCapacityException") + +/** @internal */ +export const ExceededCapacityExceptionTypeId: Cause.ExceededCapacityExceptionTypeId = Symbol.for( + "effect/Cause/errors/ExceededCapacityException" +) as Cause.ExceededCapacityExceptionTypeId + +/** @internal */ +export const ExceededCapacityException = makeException({ + [ExceededCapacityExceptionTypeId]: ExceededCapacityExceptionTypeId +}, "ExceededCapacityException") + +/** @internal */ +export const isExceededCapacityException = (u: unknown): u is Cause.ExceededCapacityException => + hasProperty(u, ExceededCapacityExceptionTypeId) + +/** @internal */ +export const isInvalidCapacityError = (u: unknown): u is Cause.InvalidPubSubCapacityException => + hasProperty(u, InvalidPubSubCapacityExceptionTypeId) + +/** @internal */ +export const TimeoutExceptionTypeId: Cause.TimeoutExceptionTypeId = Symbol.for( + "effect/Cause/errors/Timeout" +) as Cause.TimeoutExceptionTypeId + +/** @internal */ +export const TimeoutException = makeException({ + [TimeoutExceptionTypeId]: TimeoutExceptionTypeId +}, "TimeoutException") + +/** @internal */ +export const timeoutExceptionFromDuration = (duration: Duration.DurationInput): Cause.TimeoutException => + new TimeoutException(`Operation timed out after '${Duration.format(duration)}'`) + +/** @internal */ +export const isTimeoutException = (u: unknown): u is Cause.TimeoutException => hasProperty(u, TimeoutExceptionTypeId) + +/** @internal */ +export const UnknownExceptionTypeId: Cause.UnknownExceptionTypeId = Symbol.for( + "effect/Cause/errors/UnknownException" +) as Cause.UnknownExceptionTypeId + +/** @internal */ +export const UnknownException: new(cause: unknown, message?: string | undefined) => Cause.UnknownException = + (function() { + class UnknownException extends YieldableError { + readonly _tag = "UnknownException" + readonly error: unknown + constructor(cause: unknown, message?: string) { + super(message ?? "An unknown error occurred", { cause }) + this.error = cause + } + } + Object.assign(UnknownException.prototype, { + [UnknownExceptionTypeId]: UnknownExceptionTypeId, + name: "UnknownException" + }) + return UnknownException as any + })() + +/** @internal */ +export const isUnknownException = (u: unknown): u is Cause.UnknownException => hasProperty(u, UnknownExceptionTypeId) + +// ----------------------------------------------------------------------------- +// Exit +// ----------------------------------------------------------------------------- + +/** @internal */ +export const exitIsExit = (u: unknown): u is Exit.Exit => + isEffect(u) && "_tag" in u && (u._tag === "Success" || u._tag === "Failure") + +/** @internal */ +export const exitIsFailure = (self: Exit.Exit): self is Exit.Failure => self._tag === "Failure" + +/** @internal */ +export const exitIsSuccess = (self: Exit.Exit): self is Exit.Success => self._tag === "Success" + +/** @internal */ +export const exitIsInterrupted = (self: Exit.Exit): boolean => { + switch (self._tag) { + case OpCodes.OP_FAILURE: + return internalCause.isInterrupted(self.effect_instruction_i0) + case OpCodes.OP_SUCCESS: + return false + } +} + +/** @internal */ +export const exitAs = dual< + (value: A2) => (self: Exit.Exit) => Exit.Exit, + (self: Exit.Exit, value: A2) => Exit.Exit +>(2, (self: Exit.Exit, value: A2): Exit.Exit => { + switch (self._tag) { + case OpCodes.OP_FAILURE: { + return exitFailCause(self.effect_instruction_i0) + } + case OpCodes.OP_SUCCESS: { + return exitSucceed(value) as Exit.Exit + } + } +}) + +/** @internal */ +export const exitAsVoid = (self: Exit.Exit): Exit.Exit => exitAs(self, void 0) + +/** @internal */ +export const exitCauseOption = (self: Exit.Exit): Option.Option> => { + switch (self._tag) { + case OpCodes.OP_FAILURE: + return Option.some(self.effect_instruction_i0) + case OpCodes.OP_SUCCESS: + return Option.none() + } +} + +/** @internal */ +export const exitCollectAll = ( + exits: Iterable>, + options?: { + readonly parallel?: boolean | undefined + } +): Option.Option, E>> => + exitCollectAllInternal(exits, options?.parallel ? internalCause.parallel : internalCause.sequential) + +/** @internal */ +export const exitDie = (defect: unknown): Exit.Exit => + exitFailCause(internalCause.die(defect)) as Exit.Exit + +/** @internal */ +export const exitExists: { + (refinement: Refinement, B>): (self: Exit.Exit) => self is Exit.Exit + (predicate: Predicate>): (self: Exit.Exit) => boolean + (self: Exit.Exit, refinement: Refinement): self is Exit.Exit + (self: Exit.Exit, predicate: Predicate): boolean +} = dual(2, (self: Exit.Exit, refinement: Refinement): self is Exit.Exit => { + switch (self._tag) { + case OpCodes.OP_FAILURE: + return false + case OpCodes.OP_SUCCESS: + return refinement(self.effect_instruction_i0) + } +}) + +/** @internal */ +export const exitFail = (error: E): Exit.Exit => + exitFailCause(internalCause.fail(error)) as Exit.Exit + +/** @internal */ +export const exitFailCause = (cause: Cause.Cause): Exit.Exit => { + const effect = new EffectPrimitiveFailure(OpCodes.OP_FAILURE) as any + effect.effect_instruction_i0 = cause + return effect +} + +/** @internal */ +export const exitFlatMap = dual< + (f: (a: A) => Exit.Exit) => (self: Exit.Exit) => Exit.Exit, + (self: Exit.Exit, f: (a: A) => Exit.Exit) => Exit.Exit +>(2, (self: Exit.Exit, f: (a: A) => Exit.Exit): Exit.Exit => { + switch (self._tag) { + case OpCodes.OP_FAILURE: { + return exitFailCause(self.effect_instruction_i0) + } + case OpCodes.OP_SUCCESS: { + return f(self.effect_instruction_i0) + } + } +}) + +/** @internal */ +export const exitFlatMapEffect: { + ( + f: (a: A) => Effect.Effect, E2, R> + ): (self: Exit.Exit) => Effect.Effect, E2, R> + ( + self: Exit.Exit, + f: (a: A) => Effect.Effect, E2, R> + ): Effect.Effect, E2, R> +} = dual(2, ( + self: Exit.Exit, + f: (a: A) => Effect.Effect, E2, R> +): Effect.Effect, E2, R> => { + switch (self._tag) { + case OpCodes.OP_FAILURE: { + return succeed(exitFailCause(self.effect_instruction_i0)) + } + case OpCodes.OP_SUCCESS: { + return f(self.effect_instruction_i0) + } + } +}) + +/** @internal */ +export const exitFlatten = ( + self: Exit.Exit, E2> +): Exit.Exit => pipe(self, exitFlatMap(identity)) + +/** @internal */ +export const exitForEachEffect: { + ( + f: (a: A) => Effect.Effect + ): (self: Exit.Exit) => Effect.Effect, never, R> + ( + self: Exit.Exit, + f: (a: A) => Effect.Effect + ): Effect.Effect, never, R> +} = dual(2, ( + self: Exit.Exit, + f: (a: A) => Effect.Effect +): Effect.Effect, never, R> => { + switch (self._tag) { + case OpCodes.OP_FAILURE: { + return succeed(exitFailCause(self.effect_instruction_i0)) + } + case OpCodes.OP_SUCCESS: { + return exit(f(self.effect_instruction_i0)) + } + } +}) + +/** @internal */ +export const exitFromEither = (either: Either.Either): Exit.Exit => { + switch (either._tag) { + case "Left": + return exitFail(either.left) + case "Right": + return exitSucceed(either.right) + } +} + +/** @internal */ +export const exitFromOption = (option: Option.Option): Exit.Exit => { + switch (option._tag) { + case "None": + return exitFail(void 0) + case "Some": + return exitSucceed(option.value) + } +} + +/** @internal */ +export const exitGetOrElse = dual< + (orElse: (cause: Cause.Cause) => A2) => (self: Exit.Exit) => A | A2, + (self: Exit.Exit, orElse: (cause: Cause.Cause) => A2) => A | A2 +>(2, (self, orElse) => { + switch (self._tag) { + case OpCodes.OP_FAILURE: + return orElse(self.effect_instruction_i0) + case OpCodes.OP_SUCCESS: + return self.effect_instruction_i0 + } +}) + +/** @internal */ +export const exitInterrupt = (fiberId: FiberId.FiberId): Exit.Exit => + exitFailCause(internalCause.interrupt(fiberId)) + +/** @internal */ +export const exitMap = dual< + (f: (a: A) => B) => (self: Exit.Exit) => Exit.Exit, + (self: Exit.Exit, f: (a: A) => B) => Exit.Exit +>(2, (self, f) => { + switch (self._tag) { + case OpCodes.OP_FAILURE: + return exitFailCause(self.effect_instruction_i0) + case OpCodes.OP_SUCCESS: + return exitSucceed(f(self.effect_instruction_i0)) + } +}) + +/** @internal */ +export const exitMapBoth = dual< + ( + options: { + readonly onFailure: (e: E) => E2 + readonly onSuccess: (a: A) => A2 + } + ) => (self: Exit.Exit) => Exit.Exit, + ( + self: Exit.Exit, + options: { + readonly onFailure: (e: E) => E2 + readonly onSuccess: (a: A) => A2 + } + ) => Exit.Exit +>(2, (self, { onFailure, onSuccess }) => { + switch (self._tag) { + case OpCodes.OP_FAILURE: + return exitFailCause(pipe(self.effect_instruction_i0, internalCause.map(onFailure))) + case OpCodes.OP_SUCCESS: + return exitSucceed(onSuccess(self.effect_instruction_i0)) + } +}) + +/** @internal */ +export const exitMapError = dual< + (f: (e: E) => E2) => (self: Exit.Exit) => Exit.Exit, + (self: Exit.Exit, f: (e: E) => E2) => Exit.Exit +>(2, (self, f) => { + switch (self._tag) { + case OpCodes.OP_FAILURE: + return exitFailCause(pipe(self.effect_instruction_i0, internalCause.map(f))) + case OpCodes.OP_SUCCESS: + return exitSucceed(self.effect_instruction_i0) + } +}) + +/** @internal */ +export const exitMapErrorCause = dual< + (f: (cause: Cause.Cause) => Cause.Cause) => (self: Exit.Exit) => Exit.Exit, + (self: Exit.Exit, f: (cause: Cause.Cause) => Cause.Cause) => Exit.Exit +>(2, (self, f) => { + switch (self._tag) { + case OpCodes.OP_FAILURE: + return exitFailCause(f(self.effect_instruction_i0)) + case OpCodes.OP_SUCCESS: + return exitSucceed(self.effect_instruction_i0) + } +}) + +/** @internal */ +export const exitMatch = dual< + (options: { + readonly onFailure: (cause: Cause.Cause) => Z1 + readonly onSuccess: (a: A) => Z2 + }) => (self: Exit.Exit) => Z1 | Z2, + (self: Exit.Exit, options: { + readonly onFailure: (cause: Cause.Cause) => Z1 + readonly onSuccess: (a: A) => Z2 + }) => Z1 | Z2 +>(2, (self, { onFailure, onSuccess }) => { + switch (self._tag) { + case OpCodes.OP_FAILURE: + return onFailure(self.effect_instruction_i0) + case OpCodes.OP_SUCCESS: + return onSuccess(self.effect_instruction_i0) + } +}) + +/** @internal */ +export const exitMatchEffect = dual< + ( + options: { + readonly onFailure: (cause: Cause.Cause) => Effect.Effect + readonly onSuccess: (a: A) => Effect.Effect + } + ) => (self: Exit.Exit) => Effect.Effect, + ( + self: Exit.Exit, + options: { + readonly onFailure: (cause: Cause.Cause) => Effect.Effect + readonly onSuccess: (a: A) => Effect.Effect + } + ) => Effect.Effect +>(2, (self, { onFailure, onSuccess }) => { + switch (self._tag) { + case OpCodes.OP_FAILURE: + return onFailure(self.effect_instruction_i0) + case OpCodes.OP_SUCCESS: + return onSuccess(self.effect_instruction_i0) + } +}) + +/** @internal */ +export const exitSucceed = (value: A): Exit.Exit => { + const effect = new EffectPrimitiveSuccess(OpCodes.OP_SUCCESS) as any + effect.effect_instruction_i0 = value + return effect +} + +/** @internal */ +export const exitVoid: Exit.Exit = exitSucceed(void 0) + +/** @internal */ +export const exitZip = dual< + (that: Exit.Exit) => (self: Exit.Exit) => Exit.Exit<[A, A2], E | E2>, + (self: Exit.Exit, that: Exit.Exit) => Exit.Exit<[A, A2], E | E2> +>(2, (self, that) => + exitZipWith(self, that, { + onSuccess: (a, a2) => [a, a2], + onFailure: internalCause.sequential + })) + +/** @internal */ +export const exitZipLeft = dual< + (that: Exit.Exit) => (self: Exit.Exit) => Exit.Exit, + (self: Exit.Exit, that: Exit.Exit) => Exit.Exit +>(2, (self, that) => + exitZipWith(self, that, { + onSuccess: (a, _) => a, + onFailure: internalCause.sequential + })) + +/** @internal */ +export const exitZipRight = dual< + (that: Exit.Exit) => (self: Exit.Exit) => Exit.Exit, + (self: Exit.Exit, that: Exit.Exit) => Exit.Exit +>(2, (self, that) => + exitZipWith(self, that, { + onSuccess: (_, a2) => a2, + onFailure: internalCause.sequential + })) + +/** @internal */ +export const exitZipPar = dual< + (that: Exit.Exit) => (self: Exit.Exit) => Exit.Exit<[A, A2], E | E2>, + (self: Exit.Exit, that: Exit.Exit) => Exit.Exit<[A, A2], E | E2> +>(2, (self, that) => + exitZipWith(self, that, { + onSuccess: (a, a2) => [a, a2], + onFailure: internalCause.parallel + })) + +/** @internal */ +export const exitZipParLeft = dual< + (that: Exit.Exit) => (self: Exit.Exit) => Exit.Exit, + (self: Exit.Exit, that: Exit.Exit) => Exit.Exit +>(2, (self, that) => + exitZipWith(self, that, { + onSuccess: (a, _) => a, + onFailure: internalCause.parallel + })) + +/** @internal */ +export const exitZipParRight = dual< + (that: Exit.Exit) => (self: Exit.Exit) => Exit.Exit, + (self: Exit.Exit, that: Exit.Exit) => Exit.Exit +>(2, (self, that) => + exitZipWith(self, that, { + onSuccess: (_, a2) => a2, + onFailure: internalCause.parallel + })) + +/** @internal */ +export const exitZipWith = dual< + ( + that: Exit.Exit, + options: { + readonly onSuccess: (a: A, b: B) => C + readonly onFailure: (cause: Cause.Cause, cause2: Cause.Cause) => Cause.Cause + } + ) => (self: Exit.Exit) => Exit.Exit, + ( + self: Exit.Exit, + that: Exit.Exit, + options: { + readonly onSuccess: (a: A, b: B) => C + readonly onFailure: (cause: Cause.Cause, cause2: Cause.Cause) => Cause.Cause + } + ) => Exit.Exit +>(3, ( + self, + that, + { onFailure, onSuccess } +) => { + switch (self._tag) { + case OpCodes.OP_FAILURE: { + switch (that._tag) { + case OpCodes.OP_SUCCESS: + return exitFailCause(self.effect_instruction_i0) + case OpCodes.OP_FAILURE: { + return exitFailCause(onFailure(self.effect_instruction_i0, that.effect_instruction_i0)) + } + } + } + case OpCodes.OP_SUCCESS: { + switch (that._tag) { + case OpCodes.OP_SUCCESS: + return exitSucceed(onSuccess(self.effect_instruction_i0, that.effect_instruction_i0)) + case OpCodes.OP_FAILURE: + return exitFailCause(that.effect_instruction_i0) + } + } + } +}) + +const exitCollectAllInternal = ( + exits: Iterable>, + combineCauses: (causeA: Cause.Cause, causeB: Cause.Cause) => Cause.Cause +): Option.Option, E>> => { + const list = Chunk.fromIterable(exits) + if (!Chunk.isNonEmpty(list)) { + return Option.none() + } + return pipe( + Chunk.tailNonEmpty(list), + Arr.reduce( + pipe(Chunk.headNonEmpty(list), exitMap>(Chunk.of)), + (accumulator, current) => + pipe( + accumulator, + exitZipWith(current, { + onSuccess: (list, value) => pipe(list, Chunk.prepend(value)), + onFailure: combineCauses + }) + ) + ), + exitMap(Chunk.reverse), + exitMap((chunk) => Chunk.toReadonlyArray(chunk) as Array), + Option.some + ) +} + +// ----------------------------------------------------------------------------- +// Deferred +// ----------------------------------------------------------------------------- + +/** @internal */ +export const deferredUnsafeMake = (fiberId: FiberId.FiberId): Deferred.Deferred => { + const _deferred = { + ...CommitPrototype, + [deferred.DeferredTypeId]: deferred.deferredVariance, + state: MutableRef.make(deferred.pending([])), + commit() { + return deferredAwait(this) + }, + blockingOn: fiberId + } + return _deferred +} + +/* @internal */ +export const deferredMake = (): Effect.Effect> => + flatMap(fiberId, (id) => deferredMakeAs(id)) + +/* @internal */ +export const deferredMakeAs = (fiberId: FiberId.FiberId): Effect.Effect> => + sync(() => deferredUnsafeMake(fiberId)) + +/* @internal */ +export const deferredAwait = (self: Deferred.Deferred): Effect.Effect => + asyncInterrupt((resume) => { + const state = MutableRef.get(self.state) + switch (state._tag) { + case DeferredOpCodes.OP_STATE_DONE: { + return resume(state.effect) + } + case DeferredOpCodes.OP_STATE_PENDING: { + // we can push here as the internal state is mutable + state.joiners.push(resume) + return deferredInterruptJoiner(self, resume) + } + } + }, self.blockingOn) + +/* @internal */ +export const deferredComplete: { + (effect: Effect.Effect): (self: Deferred.Deferred) => Effect.Effect + (self: Deferred.Deferred, effect: Effect.Effect): Effect.Effect +} = dual( + 2, + (self: Deferred.Deferred, effect: Effect.Effect): Effect.Effect => + intoDeferred(effect, self) +) + +/* @internal */ +export const deferredCompleteWith = dual< + (effect: Effect.Effect) => (self: Deferred.Deferred) => Effect.Effect, + (self: Deferred.Deferred, effect: Effect.Effect) => Effect.Effect +>(2, (self, effect) => + sync(() => { + const state = MutableRef.get(self.state) + switch (state._tag) { + case DeferredOpCodes.OP_STATE_DONE: { + return false + } + case DeferredOpCodes.OP_STATE_PENDING: { + MutableRef.set(self.state, deferred.done(effect)) + for (let i = 0, len = state.joiners.length; i < len; i++) { + state.joiners[i](effect) + } + return true + } + } + })) + +/* @internal */ +export const deferredDone = dual< + (exit: Exit.Exit) => (self: Deferred.Deferred) => Effect.Effect, + (self: Deferred.Deferred, exit: Exit.Exit) => Effect.Effect +>(2, (self, exit) => deferredCompleteWith(self, exit)) + +/* @internal */ +export const deferredFail = dual< + (error: E) => (self: Deferred.Deferred) => Effect.Effect, + (self: Deferred.Deferred, error: E) => Effect.Effect +>(2, (self, error) => deferredCompleteWith(self, fail(error))) + +/* @internal */ +export const deferredFailSync = dual< + (evaluate: LazyArg) => (self: Deferred.Deferred) => Effect.Effect, + (self: Deferred.Deferred, evaluate: LazyArg) => Effect.Effect +>(2, (self, evaluate) => deferredCompleteWith(self, failSync(evaluate))) + +/* @internal */ +export const deferredFailCause = dual< + (cause: Cause.Cause) => (self: Deferred.Deferred) => Effect.Effect, + (self: Deferred.Deferred, cause: Cause.Cause) => Effect.Effect +>(2, (self, cause) => deferredCompleteWith(self, failCause(cause))) + +/* @internal */ +export const deferredFailCauseSync = dual< + (evaluate: LazyArg>) => (self: Deferred.Deferred) => Effect.Effect, + (self: Deferred.Deferred, evaluate: LazyArg>) => Effect.Effect +>(2, (self, evaluate) => deferredCompleteWith(self, failCauseSync(evaluate))) + +/* @internal */ +export const deferredDie = dual< + (defect: unknown) => (self: Deferred.Deferred) => Effect.Effect, + (self: Deferred.Deferred, defect: unknown) => Effect.Effect +>(2, (self, defect) => deferredCompleteWith(self, die(defect))) + +/* @internal */ +export const deferredDieSync = dual< + (evaluate: LazyArg) => (self: Deferred.Deferred) => Effect.Effect, + (self: Deferred.Deferred, evaluate: LazyArg) => Effect.Effect +>(2, (self, evaluate) => deferredCompleteWith(self, dieSync(evaluate))) + +/* @internal */ +export const deferredInterrupt = (self: Deferred.Deferred): Effect.Effect => + flatMap(fiberId, (fiberId) => deferredCompleteWith(self, interruptWith(fiberId))) + +/* @internal */ +export const deferredInterruptWith = dual< + (fiberId: FiberId.FiberId) => (self: Deferred.Deferred) => Effect.Effect, + (self: Deferred.Deferred, fiberId: FiberId.FiberId) => Effect.Effect +>(2, (self, fiberId) => deferredCompleteWith(self, interruptWith(fiberId))) + +/* @internal */ +export const deferredIsDone = (self: Deferred.Deferred): Effect.Effect => + sync(() => MutableRef.get(self.state)._tag === DeferredOpCodes.OP_STATE_DONE) + +/* @internal */ +export const deferredPoll = ( + self: Deferred.Deferred +): Effect.Effect>> => + sync(() => { + const state = MutableRef.get(self.state) + switch (state._tag) { + case DeferredOpCodes.OP_STATE_DONE: { + return Option.some(state.effect) + } + case DeferredOpCodes.OP_STATE_PENDING: { + return Option.none() + } + } + }) + +/* @internal */ +export const deferredSucceed = dual< + (value: A) => (self: Deferred.Deferred) => Effect.Effect, + (self: Deferred.Deferred, value: A) => Effect.Effect +>(2, (self, value) => deferredCompleteWith(self, succeed(value))) + +/* @internal */ +export const deferredSync = dual< + (evaluate: LazyArg) => (self: Deferred.Deferred) => Effect.Effect, + (self: Deferred.Deferred, evaluate: LazyArg) => Effect.Effect +>(2, (self, evaluate) => deferredCompleteWith(self, sync(evaluate))) + +/** @internal */ +export const deferredUnsafeDone = (self: Deferred.Deferred, effect: Effect.Effect): void => { + const state = MutableRef.get(self.state) + if (state._tag === DeferredOpCodes.OP_STATE_PENDING) { + MutableRef.set(self.state, deferred.done(effect)) + for (let i = 0, len = state.joiners.length; i < len; i++) { + state.joiners[i](effect) + } + } +} + +const deferredInterruptJoiner = ( + self: Deferred.Deferred, + joiner: (effect: Effect.Effect) => void +): Effect.Effect => + sync(() => { + const state = MutableRef.get(self.state) + if (state._tag === DeferredOpCodes.OP_STATE_PENDING) { + const index = state.joiners.indexOf(joiner) + if (index >= 0) { + // we can splice here as the internal state is mutable + state.joiners.splice(index, 1) + } + } + }) + +// ----------------------------------------------------------------------------- +// Context +// ----------------------------------------------------------------------------- + +const constContext = withFiberRuntime((fiber) => exitSucceed(fiber.currentContext)) + +/* @internal */ +export const context = (): Effect.Effect, never, R> => constContext as any + +/* @internal */ +export const contextWith = ( + f: (context: Context.Context) => A +): Effect.Effect => map(context(), f) + +/* @internal */ +export const contextWithEffect = ( + f: (context: Context.Context) => Effect.Effect +): Effect.Effect => flatMap(context(), f) + +/* @internal */ +export const provideContext = dual< + (context: Context.Context) => (self: Effect.Effect) => Effect.Effect, + (self: Effect.Effect, context: Context.Context) => Effect.Effect +>(2, (self: Effect.Effect, context: Context.Context) => + fiberRefLocally( + currentContext, + context + )(self as Effect.Effect)) + +/* @internal */ +export const provideSomeContext = dual< + (context: Context.Context) => (self: Effect.Effect) => Effect.Effect>, + (self: Effect.Effect, context: Context.Context) => Effect.Effect> +>(2, (self: Effect.Effect, context: Context.Context) => + fiberRefLocallyWith( + currentContext, + (parent) => Context.merge(parent, context) + )(self as Effect.Effect)) + +/* @internal */ +export const mapInputContext = dual< + ( + f: (context: Context.Context) => Context.Context + ) => (self: Effect.Effect) => Effect.Effect, + ( + self: Effect.Effect, + f: (context: Context.Context) => Context.Context + ) => Effect.Effect +>(2, ( + self: Effect.Effect, + f: (context: Context.Context) => Context.Context +) => contextWithEffect((context: Context.Context) => provideContext(self, f(context)))) + +// ----------------------------------------------------------------------------- +// Filtering +// ----------------------------------------------------------------------------- + +/** @internal */ +export const filterEffectOrElse: { + ( + options: { + readonly predicate: (a: NoInfer) => Effect.Effect + readonly orElse: (a: NoInfer) => Effect.Effect + } + ): (self: Effect.Effect) => Effect.Effect + ( + self: Effect.Effect, + options: { + readonly predicate: (a: A) => Effect.Effect + readonly orElse: (a: A) => Effect.Effect + } + ): Effect.Effect +} = dual(2, ( + self: Effect.Effect, + options: { + readonly predicate: (a: A) => Effect.Effect + readonly orElse: (a: A) => Effect.Effect + } +): Effect.Effect => + flatMap( + self, + (a) => + flatMap( + options.predicate(a), + (pass): Effect.Effect => pass ? succeed(a) : options.orElse(a) + ) + )) + +/** @internal */ +export const filterEffectOrFail: { + ( + options: { + readonly predicate: (a: NoInfer) => Effect.Effect + readonly orFailWith: (a: NoInfer) => E3 + } + ): (self: Effect.Effect) => Effect.Effect + ( + self: Effect.Effect, + options: { + readonly predicate: (a: A) => Effect.Effect + readonly orFailWith: (a: A) => E3 + } + ): Effect.Effect +} = dual(2, ( + self: Effect.Effect, + options: { + readonly predicate: (a: A) => Effect.Effect + readonly orFailWith: (a: A) => E3 + } +): Effect.Effect => + filterEffectOrElse(self, { + predicate: options.predicate, + orElse: (a) => fail(options.orFailWith(a)) + })) + +// ----------------------------------------------------------------------------- +// Tracing +// ----------------------------------------------------------------------------- + +/** @internal */ +export const currentSpanFromFiber = (fiber: Fiber.RuntimeFiber): Option.Option => { + const span = fiber.currentSpan + return span !== undefined && span._tag === "Span" ? Option.some(span) : Option.none() +} + +const NoopSpanProto: Omit = { + _tag: "Span", + spanId: "noop", + traceId: "noop", + sampled: false, + status: { + _tag: "Ended", + startTime: BigInt(0), + endTime: BigInt(0), + exit: exitVoid + }, + attributes: new Map(), + links: [], + kind: "internal", + attribute() {}, + event() {}, + end() {}, + addLinks() {} +} + +/** @internal */ +export const noopSpan = (options: { + readonly name: string + readonly parent: Option.Option + readonly context: Context.Context +}): Tracer.Span => Object.assign(Object.create(NoopSpanProto), options) diff --git a/repos/effect/packages/effect/src/internal/data.ts b/repos/effect/packages/effect/src/internal/data.ts new file mode 100644 index 0000000..7ef534a --- /dev/null +++ b/repos/effect/packages/effect/src/internal/data.ts @@ -0,0 +1,36 @@ +import * as Equal from "../Equal.js" +import * as Hash from "../Hash.js" +import type * as Types from "../Types.js" +import { StructuralPrototype } from "./effectable.js" + +/** @internal */ +export const ArrayProto: Equal.Equal = Object.assign(Object.create(Array.prototype), { + [Hash.symbol](this: Array) { + return Hash.cached(this, Hash.array(this)) + }, + [Equal.symbol](this: Array, that: Equal.Equal) { + if (Array.isArray(that) && this.length === that.length) { + return this.every((v, i) => Equal.equals(v, (that as Array)[i])) + } else { + return false + } + } +}) + +/** @internal */ +export const Structural: new( + args: Types.Equals, {}> extends true ? void + : { readonly [P in keyof A as P extends keyof Equal.Equal ? never : P]: A[P] } +) => {} = (function() { + function Structural(this: any, args: any) { + if (args) { + Object.assign(this, args) + } + } + Structural.prototype = StructuralPrototype + return Structural as any +})() + +/** @internal */ +export const struct = >>(as: As): As => + Object.assign(Object.create(StructuralPrototype), as) diff --git a/repos/effect/packages/effect/src/internal/dataSource.ts b/repos/effect/packages/effect/src/internal/dataSource.ts new file mode 100644 index 0000000..7dad112 --- /dev/null +++ b/repos/effect/packages/effect/src/internal/dataSource.ts @@ -0,0 +1,327 @@ +import * as RA from "../Array.js" +import * as Cause from "../Cause.js" +import * as Chunk from "../Chunk.js" +import type * as Context from "../Context.js" +import * as Effect from "../Effect.js" +import type * as Either from "../Either.js" +import { dual, pipe } from "../Function.js" +import type * as Request from "../Request.js" +import type * as RequestResolver from "../RequestResolver.js" +import type { NoInfer } from "../Types.js" +import * as core from "./core.js" +import { invokeWithInterrupt, zipWithOptions } from "./fiberRuntime.js" +import { complete } from "./request.js" + +/** @internal */ +export const make = ( + runAll: (requests: Array>) => Effect.Effect +): RequestResolver.RequestResolver => + new core.RequestResolverImpl((requests) => runAll(requests.map((_) => _.map((_) => _.request)))) + +/** @internal */ +export const makeWithEntry = ( + runAll: (requests: Array>>) => Effect.Effect +): RequestResolver.RequestResolver => new core.RequestResolverImpl((requests) => runAll(requests)) + +/** @internal */ +export const makeBatched = , R>( + run: (requests: RA.NonEmptyArray) => Effect.Effect +): RequestResolver.RequestResolver => + new core.RequestResolverImpl( + (requests) => { + if (requests.length > 1) { + return core.forEachSequentialDiscard(requests, (block) => { + const filtered = block.filter((_) => !_.state.completed).map((_) => _.request) + if (!RA.isNonEmptyArray(filtered)) { + return core.void + } + return invokeWithInterrupt(run(filtered), block) + }) + } else if (requests.length === 1) { + const filtered = requests[0].filter((_) => !_.state.completed).map((_) => _.request) + if (!RA.isNonEmptyArray(filtered)) { + return core.void + } + return run(filtered) + } + return core.void + } + ) + +/** @internal */ +export const around = dual< + ( + before: Effect.Effect, + after: (a: A2) => Effect.Effect + ) => ( + self: RequestResolver.RequestResolver + ) => RequestResolver.RequestResolver, + ( + self: RequestResolver.RequestResolver, + before: Effect.Effect, + after: (a: A2) => Effect.Effect + ) => RequestResolver.RequestResolver +>(3, (self, before, after) => + new core.RequestResolverImpl( + (requests) => + core.acquireUseRelease( + before, + () => self.runAll(requests), + after + ), + Chunk.make("Around", self, before, after) + )) + +/** @internal */ +export const aroundRequests = dual< + ( + before: (requests: ReadonlyArray>) => Effect.Effect, + after: (requests: ReadonlyArray>, _: A2) => Effect.Effect + ) => ( + self: RequestResolver.RequestResolver + ) => RequestResolver.RequestResolver, + ( + self: RequestResolver.RequestResolver, + before: (requests: ReadonlyArray>) => Effect.Effect, + after: (requests: ReadonlyArray>, _: A2) => Effect.Effect + ) => RequestResolver.RequestResolver +>(3, (self, before, after) => + new core.RequestResolverImpl( + (requests) => { + const flatRequests = requests.flatMap((chunk) => chunk.map((entry) => entry.request)) + return core.acquireUseRelease( + before(flatRequests), + () => self.runAll(requests), + (a2) => after(flatRequests, a2) + ) + }, + Chunk.make("AroundRequests", self, before, after) + )) + +/** @internal */ +export const batchN = dual< + (n: number) => ( + self: RequestResolver.RequestResolver + ) => RequestResolver.RequestResolver, + ( + self: RequestResolver.RequestResolver, + n: number + ) => RequestResolver.RequestResolver +>(2, ( + self: RequestResolver.RequestResolver, + n: number +): RequestResolver.RequestResolver => + new core.RequestResolverImpl( + (requests) => { + return n < 1 + ? core.die(new Cause.IllegalArgumentException("RequestResolver.batchN: n must be at least 1")) + : self.runAll( + Array.from(Chunk.map( + RA.reduce( + requests, + Chunk.empty>>(), + (acc, chunk) => Chunk.appendAll(acc, Chunk.chunksOf(Chunk.unsafeFromArray(chunk), n)) + ), + (chunk) => Array.from(chunk) + )) + ) + }, + Chunk.make("BatchN", self, n) + )) + +/** @internal */ +export const mapInputContext = dual< + ( + f: (context: Context.Context) => Context.Context + ) => >( + self: RequestResolver.RequestResolver + ) => RequestResolver.RequestResolver, + , R0>( + self: RequestResolver.RequestResolver, + f: (context: Context.Context) => Context.Context + ) => RequestResolver.RequestResolver +>(2, , R0>( + self: RequestResolver.RequestResolver, + f: (context: Context.Context) => Context.Context +) => + new core.RequestResolverImpl( + (requests) => + core.mapInputContext( + self.runAll(requests), + (context: Context.Context) => f(context) + ), + Chunk.make("MapInputContext", self, f) + )) + +/** @internal */ +export const eitherWith = dual< + < + A extends Request.Request, + R2, + B extends Request.Request, + C extends Request.Request + >( + that: RequestResolver.RequestResolver, + f: (_: Request.Entry) => Either.Either, Request.Entry> + ) => ( + self: RequestResolver.RequestResolver + ) => RequestResolver.RequestResolver, + < + R, + A extends Request.Request, + R2, + B extends Request.Request, + C extends Request.Request + >( + self: RequestResolver.RequestResolver, + that: RequestResolver.RequestResolver, + f: (_: Request.Entry) => Either.Either, Request.Entry> + ) => RequestResolver.RequestResolver +>(3, < + R, + A extends Request.Request, + R2, + B extends Request.Request, + C extends Request.Request +>( + self: RequestResolver.RequestResolver, + that: RequestResolver.RequestResolver, + f: (_: Request.Entry) => Either.Either, Request.Entry> +) => + new core.RequestResolverImpl( + (batch) => + core.forEachSequential(batch, (requests) => { + const [as, bs] = pipe( + requests, + RA.partitionMap(f) + ) + return zipWithOptions( + self.runAll(Array.of(as)), + that.runAll(Array.of(bs)), + () => void 0, + { concurrent: true } + ) + }), + Chunk.make("EitherWith", self, that, f) + )) + +/** @internal */ +export const fromFunction = >( + f: (request: A) => Request.Request.Success +): RequestResolver.RequestResolver => + makeBatched((requests: RA.NonEmptyArray) => + core.forEachSequentialDiscard( + requests, + (request) => complete(request, core.exitSucceed(f(request)) as any) + ) + ).identified("FromFunction", f) + +/** @internal */ +export const fromFunctionBatched = >( + f: (chunk: RA.NonEmptyArray) => Iterable> +): RequestResolver.RequestResolver => + makeBatched((as: RA.NonEmptyArray) => + Effect.forEach( + f(as), + (res, i) => complete(as[i], core.exitSucceed(res) as any), + { discard: true } + ) + ).identified("FromFunctionBatched", f) + +/** @internal */ +export const fromEffect = >( + f: (a: A) => Effect.Effect, Request.Request.Error, R> +): RequestResolver.RequestResolver => + makeBatched((requests: RA.NonEmptyArray) => + Effect.forEach( + requests, + (a) => Effect.flatMap(Effect.exit(f(a)), (e) => complete(a, e as any)), + { concurrency: "unbounded", discard: true } + ) + ).identified("FromEffect", f) + +/** @internal */ +export const fromEffectTagged = < + A extends Request.Request & { + readonly _tag: string + } +>() => +< + Fns extends { + readonly [Tag in A["_tag"]]: [Extract] extends [infer Req] ? + Req extends Request.Request ? + (requests: Array) => Effect.Effect, ReqE, any> + : never + : never + } +>( + fns: Fns +): RequestResolver.RequestResolver< + A, + ReturnType extends Effect.Effect ? R : never +> => + makeBatched((requests: RA.NonEmptyArray) => { + const grouped: Record> = {} + const tags: Array = [] + for (let i = 0, len = requests.length; i < len; i++) { + if (tags.includes(requests[i]._tag)) { + grouped[requests[i]._tag].push(requests[i]) + } else { + grouped[requests[i]._tag] = [requests[i]] + tags.push(requests[i]._tag) + } + } + return Effect.forEach( + tags, + (tag) => + Effect.matchCauseEffect((fns[tag] as any)(grouped[tag]) as Effect.Effect, unknown, unknown>, { + onFailure: (cause) => + Effect.forEach(grouped[tag], (req) => complete(req, core.exitFail(cause) as any), { discard: true }), + onSuccess: (res) => + Effect.forEach(grouped[tag], (req, i) => complete(req, core.exitSucceed(res[i]) as any), { discard: true }) + }), + { concurrency: "unbounded", discard: true } + ) + }).identified("FromEffectTagged", fns) + +/** @internal */ +export const never: RequestResolver.RequestResolver = make(() => Effect.never).identified("Never") + +/** @internal */ +export const provideContext = dual< + ( + context: Context.Context + ) => >( + self: RequestResolver.RequestResolver + ) => RequestResolver.RequestResolver, + >( + self: RequestResolver.RequestResolver, + context: Context.Context + ) => RequestResolver.RequestResolver +>(2, (self, context) => + mapInputContext( + self, + (_: Context.Context) => context + ).identified("ProvideContext", self, context)) + +/** @internal */ +export const race = dual< + , R2>( + that: RequestResolver.RequestResolver + ) => , R>( + self: RequestResolver.RequestResolver + ) => RequestResolver.RequestResolver, + , R, A2 extends Request.Request, R2>( + self: RequestResolver.RequestResolver, + that: RequestResolver.RequestResolver + ) => RequestResolver.RequestResolver +>(2, ( + self: RequestResolver.RequestResolver, + that: RequestResolver.RequestResolver +) => + new core.RequestResolverImpl((requests) => + Effect.race( + self.runAll(requests as Array>>), + that.runAll(requests as Array>>) + ) + ).identified("Race", self, that)) diff --git a/repos/effect/packages/effect/src/internal/dateTime.ts b/repos/effect/packages/effect/src/internal/dateTime.ts new file mode 100644 index 0000000..544ecd8 --- /dev/null +++ b/repos/effect/packages/effect/src/internal/dateTime.ts @@ -0,0 +1,1277 @@ +import { IllegalArgumentException } from "../Cause.js" +import * as Clock from "../Clock.js" +import type * as DateTime from "../DateTime.js" +import * as Duration from "../Duration.js" +import type * as Effect from "../Effect.js" +import * as Either from "../Either.js" +import * as Equal from "../Equal.js" +import * as equivalence from "../Equivalence.js" +import type { LazyArg } from "../Function.js" +import { dual, pipe } from "../Function.js" +import { globalValue } from "../GlobalValue.js" +import * as Hash from "../Hash.js" +import * as Inspectable from "../Inspectable.js" +import * as Option from "../Option.js" +import * as order from "../Order.js" +import { pipeArguments } from "../Pipeable.js" +import * as Predicate from "../Predicate.js" +import type { Mutable } from "../Types.js" +import * as internalEffect from "./core-effect.js" +import * as core from "./core.js" + +/** @internal */ +export const TypeId: DateTime.TypeId = Symbol.for("effect/DateTime") as DateTime.TypeId + +/** @internal */ +export const TimeZoneTypeId: DateTime.TimeZoneTypeId = Symbol.for("effect/DateTime/TimeZone") as DateTime.TimeZoneTypeId + +const Proto = { + [TypeId]: TypeId, + pipe() { + return pipeArguments(this, arguments) + }, + [Inspectable.NodeInspectSymbol](this: DateTime.DateTime) { + return this.toString() + }, + toJSON(this: DateTime.DateTime) { + return toDateUtc(this).toJSON() + } +} + +const ProtoUtc = { + ...Proto, + _tag: "Utc", + [Hash.symbol](this: DateTime.Utc) { + return Hash.cached(this, Hash.number(this.epochMillis)) + }, + [Equal.symbol](this: DateTime.Utc, that: unknown) { + return isDateTime(that) && that._tag === "Utc" && this.epochMillis === that.epochMillis + }, + toString(this: DateTime.Utc) { + return `DateTime.Utc(${toDateUtc(this).toJSON()})` + } +} + +const ProtoZoned = { + ...Proto, + _tag: "Zoned", + [Hash.symbol](this: DateTime.Zoned) { + return pipe( + Hash.number(this.epochMillis), + Hash.combine(Hash.hash(this.zone)), + Hash.cached(this) + ) + }, + [Equal.symbol](this: DateTime.Zoned, that: unknown) { + return isDateTime(that) && that._tag === "Zoned" && this.epochMillis === that.epochMillis && + Equal.equals(this.zone, that.zone) + }, + toString(this: DateTime.Zoned) { + return `DateTime.Zoned(${formatIsoZoned(this)})` + } +} + +const ProtoTimeZone = { + [TimeZoneTypeId]: TimeZoneTypeId, + [Inspectable.NodeInspectSymbol](this: DateTime.TimeZone) { + return this.toString() + } +} + +const ProtoTimeZoneNamed = { + ...ProtoTimeZone, + _tag: "Named", + [Hash.symbol](this: DateTime.TimeZone.Named) { + return Hash.cached(this, Hash.string(`Named:${this.id}`)) + }, + [Equal.symbol](this: DateTime.TimeZone.Named, that: unknown) { + return isTimeZone(that) && that._tag === "Named" && this.id === that.id + }, + toString(this: DateTime.TimeZone.Named) { + return `TimeZone.Named(${this.id})` + }, + toJSON(this: DateTime.TimeZone.Named) { + return { + _id: "TimeZone", + _tag: "Named", + id: this.id + } + } +} + +const ProtoTimeZoneOffset = { + ...ProtoTimeZone, + _tag: "Offset", + [Hash.symbol](this: DateTime.TimeZone.Offset) { + return Hash.cached(this, Hash.string(`Offset:${this.offset}`)) + }, + [Equal.symbol](this: DateTime.TimeZone.Offset, that: unknown) { + return isTimeZone(that) && that._tag === "Offset" && this.offset === that.offset + }, + toString(this: DateTime.TimeZone.Offset) { + return `TimeZone.Offset(${offsetToString(this.offset)})` + }, + toJSON(this: DateTime.TimeZone.Offset) { + return { + _id: "TimeZone", + _tag: "Offset", + offset: this.offset + } + } +} + +/** @internal */ +export const makeZonedProto = ( + epochMillis: number, + zone: DateTime.TimeZone, + partsUtc?: DateTime.DateTime.PartsWithWeekday +): DateTime.Zoned => { + const self = Object.create(ProtoZoned) + self.epochMillis = epochMillis + self.zone = zone + Object.defineProperty(self, "partsUtc", { + value: partsUtc, + enumerable: false, + writable: true + }) + Object.defineProperty(self, "adjustedEpochMillis", { + value: undefined, + enumerable: false, + writable: true + }) + Object.defineProperty(self, "partsAdjusted", { + value: undefined, + enumerable: false, + writable: true + }) + return self +} + +// ============================================================================= +// guards +// ============================================================================= + +/** @internal */ +export const isDateTime = (u: unknown): u is DateTime.DateTime => Predicate.hasProperty(u, TypeId) + +const isDateTimeArgs = (args: IArguments) => isDateTime(args[0]) + +/** @internal */ +export const isTimeZone = (u: unknown): u is DateTime.TimeZone => Predicate.hasProperty(u, TimeZoneTypeId) + +/** @internal */ +export const isTimeZoneOffset = (u: unknown): u is DateTime.TimeZone.Offset => isTimeZone(u) && u._tag === "Offset" + +/** @internal */ +export const isTimeZoneNamed = (u: unknown): u is DateTime.TimeZone.Named => isTimeZone(u) && u._tag === "Named" + +/** @internal */ +export const isUtc = (self: DateTime.DateTime): self is DateTime.Utc => self._tag === "Utc" + +/** @internal */ +export const isZoned = (self: DateTime.DateTime): self is DateTime.Zoned => self._tag === "Zoned" + +// ============================================================================= +// instances +// ============================================================================= + +/** @internal */ +export const Equivalence: equivalence.Equivalence = equivalence.make((a, b) => + a.epochMillis === b.epochMillis +) + +/** @internal */ +export const Order: order.Order = order.make((self, that) => + self.epochMillis < that.epochMillis ? -1 : self.epochMillis > that.epochMillis ? 1 : 0 +) + +/** @internal */ +export const clamp: { + ( + options: { readonly minimum: Min; readonly maximum: Max } + ): (self: A) => A | Min | Max + ( + self: A, + options: { readonly minimum: Min; readonly maximum: Max } + ): A | Min | Max +} = order.clamp(Order) + +// ============================================================================= +// constructors +// ============================================================================= + +const makeUtc = (epochMillis: number): DateTime.Utc => { + const self = Object.create(ProtoUtc) + self.epochMillis = epochMillis + Object.defineProperty(self, "partsUtc", { + value: undefined, + enumerable: false, + writable: true + }) + return self +} + +/** @internal */ +export const unsafeFromDate = (date: Date): DateTime.Utc => { + const epochMillis = date.getTime() + if (Number.isNaN(epochMillis)) { + throw new IllegalArgumentException("Invalid date") + } + return makeUtc(epochMillis) +} + +/** @internal */ +export const unsafeMake = (input: A): DateTime.DateTime.PreserveZone => { + if (isDateTime(input)) { + return input as DateTime.DateTime.PreserveZone + } else if (input instanceof Date) { + return unsafeFromDate(input) as DateTime.DateTime.PreserveZone + } else if (typeof input === "object") { + const date = new Date(0) + setPartsDate(date, input) + return unsafeFromDate(date) as DateTime.DateTime.PreserveZone + } else if (typeof input === "string" && !hasZone(input)) { + return unsafeFromDate(new Date(input + "Z")) as DateTime.DateTime.PreserveZone + } + return unsafeFromDate(new Date(input)) as DateTime.DateTime.PreserveZone +} + +const hasZone = (input: string): boolean => /Z|[+-]\d{2}$|[+-]\d{2}:?\d{2}$|\]$/.test(input) + +const minEpochMillis = -8640000000000000 + (12 * 60 * 60 * 1000) +const maxEpochMillis = 8640000000000000 - (14 * 60 * 60 * 1000) + +/** @internal */ +export const unsafeMakeZoned = (input: DateTime.DateTime.Input, options?: { + readonly timeZone?: number | string | DateTime.TimeZone | undefined + readonly adjustForTimeZone?: boolean | undefined + readonly disambiguation?: DateTime.Disambiguation | undefined +}): DateTime.Zoned => { + if (options?.timeZone === undefined && isDateTime(input) && isZoned(input)) { + return input + } + const self = unsafeMake(input) + if (self.epochMillis < minEpochMillis || self.epochMillis > maxEpochMillis) { + throw new RangeError(`Epoch millis out of range: ${self.epochMillis}`) + } + let zone: DateTime.TimeZone + if (options?.timeZone === undefined) { + const offset = new Date(self.epochMillis).getTimezoneOffset() * -60 * 1000 + zone = zoneMakeOffset(offset) + } else if (isTimeZone(options?.timeZone)) { + zone = options.timeZone + } else if (typeof options?.timeZone === "number") { + zone = zoneMakeOffset(options.timeZone) + } else { + const parsedZone = zoneFromString(options.timeZone) + if (Option.isNone(parsedZone)) { + throw new IllegalArgumentException(`Invalid time zone: ${options.timeZone}`) + } + zone = parsedZone.value + } + if (options?.adjustForTimeZone !== true) { + return makeZonedProto(self.epochMillis, zone, self.partsUtc) + } + return makeZonedFromAdjusted(self.epochMillis, zone, options?.disambiguation ?? "compatible") +} + +/** @internal */ +export const makeZoned: ( + input: DateTime.DateTime.Input, + options?: { + readonly timeZone?: number | string | DateTime.TimeZone | undefined + readonly adjustForTimeZone?: boolean | undefined + readonly disambiguation?: DateTime.Disambiguation | undefined + } +) => Option.Option = Option.liftThrowable(unsafeMakeZoned) + +/** @internal */ +export const make: (input: A) => Option.Option> = + Option.liftThrowable(unsafeMake) + +const zonedStringRegex = /^(.{17,35})\[(.+)\]$/ + +/** @internal */ +export const makeZonedFromString = (input: string): Option.Option => { + const match = zonedStringRegex.exec(input) + if (match === null) { + const offset = parseOffset(input) + return offset !== null ? makeZoned(input, { timeZone: offset }) : Option.none() + } + const [, isoString, timeZone] = match + return makeZoned(isoString, { timeZone }) +} + +/** @internal */ +export const now: Effect.Effect = core.map(Clock.currentTimeMillis, makeUtc) + +/** @internal */ +export const nowAsDate: Effect.Effect = core.map(Clock.currentTimeMillis, (millis) => new Date(millis)) + +/** @internal */ +export const unsafeNow: LazyArg = () => makeUtc(Date.now()) + +// ============================================================================= +// time zones +// ============================================================================= + +/** @internal */ +export const toUtc = (self: DateTime.DateTime): DateTime.Utc => makeUtc(self.epochMillis) + +/** @internal */ +export const setZone: { + (zone: DateTime.TimeZone, options?: { + readonly adjustForTimeZone?: boolean | undefined + readonly disambiguation?: DateTime.Disambiguation | undefined + }): (self: DateTime.DateTime) => DateTime.Zoned + (self: DateTime.DateTime, zone: DateTime.TimeZone, options?: { + readonly adjustForTimeZone?: boolean | undefined + readonly disambiguation?: DateTime.Disambiguation | undefined + }): DateTime.Zoned +} = dual(isDateTimeArgs, (self: DateTime.DateTime, zone: DateTime.TimeZone, options?: { + readonly adjustForTimeZone?: boolean | undefined + readonly disambiguation?: DateTime.Disambiguation | undefined +}): DateTime.Zoned => + options?.adjustForTimeZone === true + ? makeZonedFromAdjusted(self.epochMillis, zone, options?.disambiguation ?? "compatible") + : makeZonedProto(self.epochMillis, zone, self.partsUtc)) + +/** @internal */ +export const setZoneOffset: { + (offset: number, options?: { + readonly adjustForTimeZone?: boolean | undefined + readonly disambiguation?: DateTime.Disambiguation | undefined + }): (self: DateTime.DateTime) => DateTime.Zoned + (self: DateTime.DateTime, offset: number, options?: { + readonly adjustForTimeZone?: boolean | undefined + readonly disambiguation?: DateTime.Disambiguation | undefined + }): DateTime.Zoned +} = dual(isDateTimeArgs, (self: DateTime.DateTime, offset: number, options?: { + readonly adjustForTimeZone?: boolean | undefined + readonly disambiguation?: DateTime.Disambiguation | undefined +}): DateTime.Zoned => setZone(self, zoneMakeOffset(offset), options)) + +const validZoneCache = globalValue("effect/DateTime/validZoneCache", () => new Map()) + +const formatOptions: Intl.DateTimeFormatOptions = { + day: "numeric", + month: "numeric", + year: "numeric", + hour: "numeric", + minute: "numeric", + second: "numeric", + timeZoneName: "longOffset", + fractionalSecondDigits: 3, + hourCycle: "h23" +} + +const zoneMakeIntl = (format: Intl.DateTimeFormat): DateTime.TimeZone.Named => { + const zoneId = format.resolvedOptions().timeZone + if (validZoneCache.has(zoneId)) { + return validZoneCache.get(zoneId)! + } + const zone = Object.create(ProtoTimeZoneNamed) + zone.id = zoneId + zone.format = format + validZoneCache.set(zoneId, zone) + return zone +} + +/** @internal */ +export const zoneUnsafeMakeNamed = (zoneId: string): DateTime.TimeZone.Named => { + if (validZoneCache.has(zoneId)) { + return validZoneCache.get(zoneId)! + } + try { + return zoneMakeIntl( + new Intl.DateTimeFormat("en-US", { + ...formatOptions, + timeZone: zoneId + }) + ) + } catch { + throw new IllegalArgumentException(`Invalid time zone: ${zoneId}`) + } +} + +/** @internal */ +export const zoneMakeOffset = (offset: number): DateTime.TimeZone.Offset => { + const zone = Object.create(ProtoTimeZoneOffset) + zone.offset = offset + return zone +} + +/** @internal */ +export const zoneMakeNamed: (zoneId: string) => Option.Option = Option.liftThrowable( + zoneUnsafeMakeNamed +) + +/** @internal */ +export const zoneMakeNamedEffect = (zoneId: string): Effect.Effect => + internalEffect.try_({ + try: () => zoneUnsafeMakeNamed(zoneId), + catch: (e) => e as IllegalArgumentException + }) + +/** @internal */ +export const zoneMakeLocal = (): DateTime.TimeZone.Named => + zoneMakeIntl(new Intl.DateTimeFormat("en-US", formatOptions)) + +const offsetZoneRegex = /^(?:GMT|[+-])/ + +/** @internal */ +export const zoneFromString = (zone: string): Option.Option => { + if (offsetZoneRegex.test(zone)) { + const offset = parseOffset(zone) + return offset === null ? Option.none() : Option.some(zoneMakeOffset(offset)) + } + return zoneMakeNamed(zone) +} + +/** @internal */ +export const zoneToString = (self: DateTime.TimeZone): string => { + if (self._tag === "Offset") { + return offsetToString(self.offset) + } + return self.id +} + +/** @internal */ +export const setZoneNamed: { + (zoneId: string, options?: { + readonly adjustForTimeZone?: boolean | undefined + readonly disambiguation?: DateTime.Disambiguation | undefined + }): (self: DateTime.DateTime) => Option.Option + (self: DateTime.DateTime, zoneId: string, options?: { + readonly adjustForTimeZone?: boolean | undefined + readonly disambiguation?: DateTime.Disambiguation | undefined + }): Option.Option +} = dual( + isDateTimeArgs, + (self: DateTime.DateTime, zoneId: string, options?: { + readonly adjustForTimeZone?: boolean | undefined + readonly disambiguation?: DateTime.Disambiguation | undefined + }): Option.Option => Option.map(zoneMakeNamed(zoneId), (zone) => setZone(self, zone, options)) +) + +/** @internal */ +export const unsafeSetZoneNamed: { + (zoneId: string, options?: { + readonly adjustForTimeZone?: boolean | undefined + readonly disambiguation?: DateTime.Disambiguation | undefined + }): (self: DateTime.DateTime) => DateTime.Zoned + (self: DateTime.DateTime, zoneId: string, options?: { + readonly adjustForTimeZone?: boolean | undefined + readonly disambiguation?: DateTime.Disambiguation | undefined + }): DateTime.Zoned +} = dual(isDateTimeArgs, (self: DateTime.DateTime, zoneId: string, options?: { + readonly adjustForTimeZone?: boolean | undefined + readonly disambiguation?: DateTime.Disambiguation | undefined +}): DateTime.Zoned => setZone(self, zoneUnsafeMakeNamed(zoneId), options)) + +// ============================================================================= +// comparisons +// ============================================================================= + +/** @internal */ +export const distance: { + (other: DateTime.DateTime): (self: DateTime.DateTime) => number + (self: DateTime.DateTime, other: DateTime.DateTime): number +} = dual(2, (self: DateTime.DateTime, other: DateTime.DateTime): number => toEpochMillis(other) - toEpochMillis(self)) + +/** @internal */ +export const distanceDurationEither: { + (other: DateTime.DateTime): (self: DateTime.DateTime) => Either.Either + (self: DateTime.DateTime, other: DateTime.DateTime): Either.Either +} = dual( + 2, + (self: DateTime.DateTime, other: DateTime.DateTime): Either.Either => { + const diffMillis = distance(self, other) + return diffMillis > 0 + ? Either.right(Duration.millis(diffMillis)) + : Either.left(Duration.millis(-diffMillis)) + } +) + +/** @internal */ +export const distanceDuration: { + (other: DateTime.DateTime): (self: DateTime.DateTime) => Duration.Duration + (self: DateTime.DateTime, other: DateTime.DateTime): Duration.Duration +} = dual( + 2, + (self: DateTime.DateTime, other: DateTime.DateTime): Duration.Duration => + Duration.millis(Math.abs(distance(self, other))) +) + +/** @internal */ +export const min: { + (that: That): (self: Self) => Self | That + (self: Self, that: That): Self | That +} = order.min(Order) + +/** @internal */ +export const max: { + (that: That): (self: Self) => Self | That + (self: Self, that: That): Self | That +} = order.max(Order) + +/** @internal */ +export const greaterThan: { + (that: DateTime.DateTime): (self: DateTime.DateTime) => boolean + (self: DateTime.DateTime, that: DateTime.DateTime): boolean +} = order.greaterThan(Order) + +/** @internal */ +export const greaterThanOrEqualTo: { + (that: DateTime.DateTime): (self: DateTime.DateTime) => boolean + (self: DateTime.DateTime, that: DateTime.DateTime): boolean +} = order.greaterThanOrEqualTo(Order) + +/** @internal */ +export const lessThan: { + (that: DateTime.DateTime): (self: DateTime.DateTime) => boolean + (self: DateTime.DateTime, that: DateTime.DateTime): boolean +} = order.lessThan(Order) + +/** @internal */ +export const lessThanOrEqualTo: { + (that: DateTime.DateTime): (self: DateTime.DateTime) => boolean + (self: DateTime.DateTime, that: DateTime.DateTime): boolean +} = order.lessThanOrEqualTo(Order) + +/** @internal */ +export const between: { + (options: { minimum: DateTime.DateTime; maximum: DateTime.DateTime }): (self: DateTime.DateTime) => boolean + (self: DateTime.DateTime, options: { minimum: DateTime.DateTime; maximum: DateTime.DateTime }): boolean +} = order.between(Order) + +/** @internal */ +export const isFuture = (self: DateTime.DateTime): Effect.Effect => core.map(now, lessThan(self)) + +/** @internal */ +export const unsafeIsFuture = (self: DateTime.DateTime): boolean => lessThan(unsafeNow(), self) + +/** @internal */ +export const isPast = (self: DateTime.DateTime): Effect.Effect => core.map(now, greaterThan(self)) + +/** @internal */ +export const unsafeIsPast = (self: DateTime.DateTime): boolean => greaterThan(unsafeNow(), self) + +// ============================================================================= +// conversions +// ============================================================================= + +/** @internal */ +export const toDateUtc = (self: DateTime.DateTime): Date => new Date(self.epochMillis) + +/** @internal */ +export const toDate = (self: DateTime.DateTime): Date => { + if (self._tag === "Utc") { + return new Date(self.epochMillis) + } else if (self.zone._tag === "Offset") { + return new Date(self.epochMillis + self.zone.offset) + } else if (self.adjustedEpochMillis !== undefined) { + return new Date(self.adjustedEpochMillis) + } + const parts = self.zone.format.formatToParts(self.epochMillis).filter((_) => _.type !== "literal") + const date = new Date(0) + date.setUTCFullYear( + Number(parts[2].value), + Number(parts[0].value) - 1, + Number(parts[1].value) + ) + date.setUTCHours( + Number(parts[3].value), + Number(parts[4].value), + Number(parts[5].value), + Number(parts[6].value) + ) + self.adjustedEpochMillis = date.getTime() + return date +} + +/** @internal */ +export const zonedOffset = (self: DateTime.Zoned): number => { + const date = toDate(self) + return date.getTime() - toEpochMillis(self) +} + +const offsetToString = (offset: number): string => { + const abs = Math.abs(offset) + let hours = Math.floor(abs / (60 * 60 * 1000)) + let minutes = Math.round((abs % (60 * 60 * 1000)) / (60 * 1000)) + if (minutes === 60) { + hours += 1 + minutes = 0 + } + return `${offset < 0 ? "-" : "+"}${String(hours).padStart(2, "0")}:${String(minutes).padStart(2, "0")}` +} + +/** @internal */ +export const zonedOffsetIso = (self: DateTime.Zoned): string => offsetToString(zonedOffset(self)) + +/** @internal */ +export const toEpochMillis = (self: DateTime.DateTime): number => self.epochMillis + +/** @internal */ +export const removeTime = (self: DateTime.DateTime): DateTime.Utc => + withDate(self, (date) => { + date.setUTCHours(0, 0, 0, 0) + return makeUtc(date.getTime()) + }) + +// ============================================================================= +// parts +// ============================================================================= + +const dateToParts = (date: Date): DateTime.DateTime.PartsWithWeekday => ({ + millis: date.getUTCMilliseconds(), + seconds: date.getUTCSeconds(), + minutes: date.getUTCMinutes(), + hours: date.getUTCHours(), + day: date.getUTCDate(), + weekDay: date.getUTCDay(), + month: date.getUTCMonth() + 1, + year: date.getUTCFullYear() +}) + +/** @internal */ +export const toParts = (self: DateTime.DateTime): DateTime.DateTime.PartsWithWeekday => { + if (self._tag === "Utc") { + return toPartsUtc(self) + } else if (self.partsAdjusted !== undefined) { + return self.partsAdjusted + } + self.partsAdjusted = withDate(self, dateToParts) + return self.partsAdjusted +} + +/** @internal */ +export const toPartsUtc = (self: DateTime.DateTime): DateTime.DateTime.PartsWithWeekday => { + if (self.partsUtc !== undefined) { + return self.partsUtc + } + self.partsUtc = withDateUtc(self, dateToParts) + return self.partsUtc +} + +/** @internal */ +export const getPartUtc: { + (part: keyof DateTime.DateTime.PartsWithWeekday): (self: DateTime.DateTime) => number + (self: DateTime.DateTime, part: keyof DateTime.DateTime.PartsWithWeekday): number +} = dual(2, (self: DateTime.DateTime, part: keyof DateTime.DateTime.PartsWithWeekday): number => toPartsUtc(self)[part]) + +/** @internal */ +export const getPart: { + (part: keyof DateTime.DateTime.PartsWithWeekday): (self: DateTime.DateTime) => number + (self: DateTime.DateTime, part: keyof DateTime.DateTime.PartsWithWeekday): number +} = dual(2, (self: DateTime.DateTime, part: keyof DateTime.DateTime.PartsWithWeekday): number => toParts(self)[part]) + +const setPartsDate = (date: Date, parts: Partial): void => { + if (parts.year !== undefined) { + date.setUTCFullYear(parts.year) + } + if (parts.month !== undefined) { + date.setUTCMonth(parts.month - 1) + } + if (parts.day !== undefined) { + date.setUTCDate(parts.day) + } + if (parts.weekDay !== undefined) { + const diff = parts.weekDay - date.getUTCDay() + date.setUTCDate(date.getUTCDate() + diff) + } + if (parts.hours !== undefined) { + date.setUTCHours(parts.hours) + } + if (parts.minutes !== undefined) { + date.setUTCMinutes(parts.minutes) + } + if (parts.seconds !== undefined) { + date.setUTCSeconds(parts.seconds) + } + if (parts.millis !== undefined) { + date.setUTCMilliseconds(parts.millis) + } +} + +/** @internal */ +export const setParts: { + ( + parts: Partial + ): (self: A) => A + ( + self: A, + parts: Partial + ): A +} = dual( + 2, + (self: DateTime.DateTime, parts: Partial): DateTime.DateTime => + mutate(self, (date) => setPartsDate(date, parts)) +) + +/** @internal */ +export const setPartsUtc: { + ( + parts: Partial + ): (self: A) => A + ( + self: A, + parts: Partial + ): A +} = dual( + 2, + (self: DateTime.DateTime, parts: Partial): DateTime.DateTime => + mutateUtc(self, (date) => setPartsDate(date, parts)) +) + +// ============================================================================= +// mapping +// ============================================================================= + +const constDayMillis = 24 * 60 * 60 * 1000 + +const makeZonedFromAdjusted = ( + adjustedMillis: number, + zone: DateTime.TimeZone, + disambiguation: DateTime.Disambiguation +): DateTime.Zoned => { + if (zone._tag === "Offset") { + return makeZonedProto(adjustedMillis - zone.offset, zone) + } + const beforeOffset = calculateNamedOffset( + adjustedMillis - constDayMillis, + adjustedMillis, + zone + ) + const afterOffset = calculateNamedOffset( + adjustedMillis + constDayMillis, + adjustedMillis, + zone + ) + // If there is no transition, we can return early + if (beforeOffset === afterOffset) { + return makeZonedProto(adjustedMillis - beforeOffset, zone) + } + const isForwards = beforeOffset < afterOffset + const transitionMillis = beforeOffset - afterOffset + // If the transition is forwards, we only need to check if we should move the + // local wall clock time forward if it is inside the gap + if (isForwards) { + const currentAfterOffset = calculateNamedOffset( + adjustedMillis - afterOffset, + adjustedMillis, + zone + ) + if (currentAfterOffset === afterOffset) { + return makeZonedProto(adjustedMillis - afterOffset, zone) + } + const before = makeZonedProto(adjustedMillis - beforeOffset, zone) + const beforeAdjustedMillis = toDate(before).getTime() + // If the wall clock time has changed, we are inside the gap + if (adjustedMillis !== beforeAdjustedMillis) { + switch (disambiguation) { + case "reject": { + const formatted = new Date(adjustedMillis).toISOString() + throw new RangeError(`Gap time: ${formatted} does not exist in time zone ${zone.id}`) + } + case "earlier": + return makeZonedProto(adjustedMillis - afterOffset, zone) + + case "compatible": + case "later": + return before + } + } + // The wall clock time is in the earlier offset, so we use that + return before + } + + const currentBeforeOffset = calculateNamedOffset( + adjustedMillis - beforeOffset, + adjustedMillis, + zone + ) + // The wall clock time is in the earlier offset, so we use that + if (currentBeforeOffset === beforeOffset) { + if (disambiguation === "earlier" || disambiguation === "compatible") { + return makeZonedProto(adjustedMillis - beforeOffset, zone) + } + const laterOffset = calculateNamedOffset( + adjustedMillis - beforeOffset + transitionMillis, + adjustedMillis + transitionMillis, + zone + ) + if (laterOffset === beforeOffset) { + return makeZonedProto(adjustedMillis - beforeOffset, zone) + } + // If the offset changed in this period, then we are inside the period where + // the wall clock time occurs twice, once in the earlier offset and once in + // the later offset. + if (disambiguation === "reject") { + const formatted = new Date(adjustedMillis).toISOString() + throw new RangeError(`Ambiguous time: ${formatted} occurs twice in time zone ${zone.id}`) + } + // If the disambiguation is "later", we return the later offset below + } + return makeZonedProto(adjustedMillis - afterOffset, zone) +} + +const offsetRegex = /([+-])(\d{2}):(\d{2})$/ +const parseOffset = (offset: string): number | null => { + const match = offsetRegex.exec(offset) + if (match === null) { + return null + } + const [, sign, hours, minutes] = match + return (sign === "+" ? 1 : -1) * (Number(hours) * 60 + Number(minutes)) * 60 * 1000 +} + +const calculateNamedOffset = ( + utcMillis: number, + adjustedMillis: number, + zone: DateTime.TimeZone.Named +): number => { + const offset = zone.format.formatToParts(utcMillis).find((_) => _.type === "timeZoneName")?.value ?? "" + if (offset === "GMT") { + return 0 + } + const result = parseOffset(offset) + if (result === null) { + // fallback to using the adjusted date + return zonedOffset(makeZonedProto(adjustedMillis, zone)) + } + return result +} + +/** @internal */ +export const mutate: { + (f: (date: Date) => void, options?: { + readonly disambiguation?: DateTime.Disambiguation | undefined + }): (self: A) => A + (self: A, f: (date: Date) => void, options?: { + readonly disambiguation?: DateTime.Disambiguation | undefined + }): A +} = dual(isDateTimeArgs, (self: DateTime.DateTime, f: (date: Date) => void, options?: { + readonly disambiguation?: DateTime.Disambiguation | undefined +}): DateTime.DateTime => { + if (self._tag === "Utc") { + const date = toDateUtc(self) + f(date) + return makeUtc(date.getTime()) + } + const adjustedDate = toDate(self) + const newAdjustedDate = new Date(adjustedDate.getTime()) + f(newAdjustedDate) + return makeZonedFromAdjusted(newAdjustedDate.getTime(), self.zone, options?.disambiguation ?? "compatible") +}) + +/** @internal */ +export const mutateUtc: { + (f: (date: Date) => void): (self: A) => A + (self: A, f: (date: Date) => void): A +} = dual(2, (self: DateTime.DateTime, f: (date: Date) => void): DateTime.DateTime => + mapEpochMillis(self, (millis) => { + const date = new Date(millis) + f(date) + return date.getTime() + })) + +/** @internal */ +export const mapEpochMillis: { + (f: (millis: number) => number): (self: A) => A + (self: A, f: (millis: number) => number): A +} = dual(2, (self: DateTime.DateTime, f: (millis: number) => number): DateTime.DateTime => { + const millis = f(toEpochMillis(self)) + return self._tag === "Utc" ? makeUtc(millis) : makeZonedProto(millis, self.zone) +}) + +/** @internal */ +export const withDate: { + (f: (date: Date) => A): (self: DateTime.DateTime) => A + (self: DateTime.DateTime, f: (date: Date) => A): A +} = dual(2, (self: DateTime.DateTime, f: (date: Date) => A): A => f(toDate(self))) + +/** @internal */ +export const withDateUtc: { + (f: (date: Date) => A): (self: DateTime.DateTime) => A + (self: DateTime.DateTime, f: (date: Date) => A): A +} = dual(2, (self: DateTime.DateTime, f: (date: Date) => A): A => f(toDateUtc(self))) + +/** @internal */ +export const match: { + (options: { + readonly onUtc: (_: DateTime.Utc) => A + readonly onZoned: (_: DateTime.Zoned) => B + }): (self: DateTime.DateTime) => A | B + (self: DateTime.DateTime, options: { + readonly onUtc: (_: DateTime.Utc) => A + readonly onZoned: (_: DateTime.Zoned) => B + }): A | B +} = dual(2, (self: DateTime.DateTime, options: { + readonly onUtc: (_: DateTime.Utc) => A + readonly onZoned: (_: DateTime.Zoned) => B +}): A | B => self._tag === "Utc" ? options.onUtc(self) : options.onZoned(self)) + +// ============================================================================= +// math +// ============================================================================= + +/** @internal */ +export const addDuration: { + (duration: Duration.DurationInput): (self: A) => A + (self: A, duration: Duration.DurationInput): A +} = dual( + 2, + (self: DateTime.DateTime, duration: Duration.DurationInput): DateTime.DateTime => + mapEpochMillis(self, (millis) => millis + Duration.toMillis(duration)) +) + +/** @internal */ +export const subtractDuration: { + (duration: Duration.DurationInput): (self: A) => A + (self: A, duration: Duration.DurationInput): A +} = dual( + 2, + (self: DateTime.DateTime, duration: Duration.DurationInput): DateTime.DateTime => + mapEpochMillis(self, (millis) => millis - Duration.toMillis(duration)) +) + +const addMillis = (date: Date, amount: number): void => { + date.setTime(date.getTime() + amount) +} + +/** @internal */ +export const add: { + ( + parts: Partial + ): (self: A) => A + ( + self: A, + parts: Partial + ): A +} = dual( + 2, + (self: DateTime.DateTime, parts: Partial): DateTime.DateTime => + mutate(self, (date) => { + if (parts.millis) { + addMillis(date, parts.millis) + } + if (parts.seconds) { + addMillis(date, parts.seconds * 1000) + } + if (parts.minutes) { + addMillis(date, parts.minutes * 60 * 1000) + } + if (parts.hours) { + addMillis(date, parts.hours * 60 * 60 * 1000) + } + if (parts.days) { + date.setUTCDate(date.getUTCDate() + parts.days) + } + if (parts.weeks) { + date.setUTCDate(date.getUTCDate() + parts.weeks * 7) + } + if (parts.months) { + const day = date.getUTCDate() + date.setUTCMonth(date.getUTCMonth() + parts.months + 1, 0) + if (day < date.getUTCDate()) { + date.setUTCDate(day) + } + } + if (parts.years) { + const day = date.getUTCDate() + const month = date.getUTCMonth() + date.setUTCFullYear( + date.getUTCFullYear() + parts.years, + month + 1, + 0 + ) + if (day < date.getUTCDate()) { + date.setUTCDate(day) + } + } + }) +) + +/** @internal */ +export const subtract: { + ( + parts: Partial + ): (self: A) => A + ( + self: A, + parts: Partial + ): A +} = dual(2, (self: DateTime.DateTime, parts: Partial): DateTime.DateTime => { + const newParts = {} as Partial> + for (const key in parts) { + newParts[key as keyof DateTime.DateTime.PartsForMath] = -1 * parts[key as keyof DateTime.DateTime.PartsForMath]! + } + return add(self, newParts) +}) + +const startOfDate = (date: Date, part: DateTime.DateTime.UnitSingular, options?: { + readonly weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | undefined +}) => { + switch (part) { + case "second": { + date.setUTCMilliseconds(0) + break + } + case "minute": { + date.setUTCSeconds(0, 0) + break + } + case "hour": { + date.setUTCMinutes(0, 0, 0) + break + } + case "day": { + date.setUTCHours(0, 0, 0, 0) + break + } + case "week": { + const weekStartsOn = options?.weekStartsOn ?? 0 + const day = date.getUTCDay() + const diff = (day - weekStartsOn + 7) % 7 + date.setUTCDate(date.getUTCDate() - diff) + date.setUTCHours(0, 0, 0, 0) + break + } + case "month": { + date.setUTCDate(1) + date.setUTCHours(0, 0, 0, 0) + break + } + case "year": { + date.setUTCMonth(0, 1) + date.setUTCHours(0, 0, 0, 0) + break + } + } +} + +/** @internal */ +export const startOf: { + (part: DateTime.DateTime.UnitSingular, options?: { + readonly weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | undefined + }): (self: A) => A + (self: A, part: DateTime.DateTime.UnitSingular, options?: { + readonly weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | undefined + }): A +} = dual(isDateTimeArgs, (self: DateTime.DateTime, part: DateTime.DateTime.UnitSingular, options?: { + readonly weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | undefined +}): DateTime.DateTime => mutate(self, (date) => startOfDate(date, part, options))) + +const endOfDate = (date: Date, part: DateTime.DateTime.UnitSingular, options?: { + readonly weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | undefined +}) => { + switch (part) { + case "second": { + date.setUTCMilliseconds(999) + break + } + case "minute": { + date.setUTCSeconds(59, 999) + break + } + case "hour": { + date.setUTCMinutes(59, 59, 999) + break + } + case "day": { + date.setUTCHours(23, 59, 59, 999) + break + } + case "week": { + const weekStartsOn = options?.weekStartsOn ?? 0 + const day = date.getUTCDay() + const diff = (day - weekStartsOn + 7) % 7 + date.setUTCDate(date.getUTCDate() - diff + 6) + date.setUTCHours(23, 59, 59, 999) + break + } + case "month": { + date.setUTCMonth(date.getUTCMonth() + 1, 0) + date.setUTCHours(23, 59, 59, 999) + break + } + case "year": { + date.setUTCMonth(11, 31) + date.setUTCHours(23, 59, 59, 999) + break + } + } +} + +/** @internal */ +export const endOf: { + (part: DateTime.DateTime.UnitSingular, options?: { + readonly weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | undefined + }): (self: A) => A + (self: A, part: DateTime.DateTime.UnitSingular, options?: { + readonly weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | undefined + }): A +} = dual(isDateTimeArgs, (self: DateTime.DateTime, part: DateTime.DateTime.UnitSingular, options?: { + readonly weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | undefined +}): DateTime.DateTime => mutate(self, (date) => endOfDate(date, part, options))) + +/** @internal */ +export const nearest: { + (part: DateTime.DateTime.UnitSingular, options?: { + readonly weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | undefined + }): (self: A) => A + (self: A, part: DateTime.DateTime.UnitSingular, options?: { + readonly weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | undefined + }): A +} = dual(isDateTimeArgs, (self: DateTime.DateTime, part: DateTime.DateTime.UnitSingular, options?: { + readonly weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | undefined +}): DateTime.DateTime => + mutate(self, (date) => { + if (part === "milli") return + const millis = date.getTime() + const start = new Date(millis) + startOfDate(start, part, options) + const startMillis = start.getTime() + const end = new Date(millis) + endOfDate(end, part, options) + const endMillis = end.getTime() + 1 + const diffStart = millis - startMillis + const diffEnd = endMillis - millis + if (diffStart < diffEnd) { + date.setTime(startMillis) + } else { + date.setTime(endMillis) + } + })) + +// ============================================================================= +// formatting +// ============================================================================= + +const intlTimeZone = (self: DateTime.TimeZone): string => { + if (self._tag === "Named") { + return self.id + } + return offsetToString(self.offset) +} + +/** @internal */ +export const format: { + ( + options?: + | Intl.DateTimeFormatOptions & { + readonly locale?: Intl.LocalesArgument + } + | undefined + ): (self: DateTime.DateTime) => string + ( + self: DateTime.DateTime, + options?: + | Intl.DateTimeFormatOptions & { + readonly locale?: Intl.LocalesArgument + } + | undefined + ): string +} = dual(isDateTimeArgs, ( + self: DateTime.DateTime, + options?: + | Intl.DateTimeFormatOptions & { + readonly locale?: Intl.LocalesArgument + } + | undefined +): string => { + try { + return new Intl.DateTimeFormat(options?.locale, { + timeZone: self._tag === "Utc" ? "UTC" : intlTimeZone(self.zone), + ...options + }).format(self.epochMillis) + } catch { + return new Intl.DateTimeFormat(options?.locale, { + timeZone: "UTC", + ...options + }).format(toDate(self)) + } +}) + +/** @internal */ +export const formatLocal: { + ( + options?: + | Intl.DateTimeFormatOptions & { + readonly locale?: Intl.LocalesArgument + } + | undefined + ): (self: DateTime.DateTime) => string + ( + self: DateTime.DateTime, + options?: + | Intl.DateTimeFormatOptions & { + readonly locale?: Intl.LocalesArgument + } + | undefined + ): string +} = dual(isDateTimeArgs, ( + self: DateTime.DateTime, + options?: + | Intl.DateTimeFormatOptions & { + readonly locale?: Intl.LocalesArgument + } + | undefined +): string => new Intl.DateTimeFormat(options?.locale, options).format(self.epochMillis)) + +/** @internal */ +export const formatUtc: { + ( + options?: + | Intl.DateTimeFormatOptions & { + readonly locale?: Intl.LocalesArgument + } + | undefined + ): (self: DateTime.DateTime) => string + ( + self: DateTime.DateTime, + options?: + | Intl.DateTimeFormatOptions & { + readonly locale?: Intl.LocalesArgument + } + | undefined + ): string +} = dual(isDateTimeArgs, ( + self: DateTime.DateTime, + options?: + | Intl.DateTimeFormatOptions & { + readonly locale?: Intl.LocalesArgument + } + | undefined +): string => + new Intl.DateTimeFormat(options?.locale, { + ...options, + timeZone: "UTC" + }).format(self.epochMillis)) + +/** @internal */ +export const formatIntl: { + (format: Intl.DateTimeFormat): (self: DateTime.DateTime) => string + (self: DateTime.DateTime, format: Intl.DateTimeFormat): string +} = dual(2, (self: DateTime.DateTime, format: Intl.DateTimeFormat): string => format.format(self.epochMillis)) + +/** @internal */ +export const formatIso = (self: DateTime.DateTime): string => toDateUtc(self).toISOString() + +/** @internal */ +export const formatIsoDate = (self: DateTime.DateTime): string => toDate(self).toISOString().slice(0, 10) + +/** @internal */ +export const formatIsoDateUtc = (self: DateTime.DateTime): string => toDateUtc(self).toISOString().slice(0, 10) + +/** @internal */ +export const formatIsoOffset = (self: DateTime.DateTime): string => { + const date = toDate(self) + return self._tag === "Utc" ? date.toISOString() : `${date.toISOString().slice(0, -1)}${zonedOffsetIso(self)}` +} + +/** @internal */ +export const formatIsoZoned = (self: DateTime.Zoned): string => + self.zone._tag === "Offset" ? formatIsoOffset(self) : `${formatIsoOffset(self)}[${self.zone.id}]` diff --git a/repos/effect/packages/effect/src/internal/defaultServices.ts b/repos/effect/packages/effect/src/internal/defaultServices.ts new file mode 100644 index 0000000..5939841 --- /dev/null +++ b/repos/effect/packages/effect/src/internal/defaultServices.ts @@ -0,0 +1,163 @@ +import * as Array from "../Array.js" +import type * as Chunk from "../Chunk.js" +import type * as Clock from "../Clock.js" +import type * as Config from "../Config.js" +import type * as ConfigProvider from "../ConfigProvider.js" +import * as Context from "../Context.js" +import type * as DefaultServices from "../DefaultServices.js" +import * as Duration from "../Duration.js" +import type * as Effect from "../Effect.js" +import { dual, pipe } from "../Function.js" +import { globalValue } from "../GlobalValue.js" +import type * as Random from "../Random.js" +import type * as Tracer from "../Tracer.js" +import * as clock from "./clock.js" +import * as configProvider from "./configProvider.js" +import * as core from "./core.js" +import * as console_ from "./defaultServices/console.js" +import * as random from "./random.js" +import * as tracer from "./tracer.js" + +/** @internal */ +export const liveServices: Context.Context = pipe( + Context.empty(), + Context.add(clock.clockTag, clock.make()), + Context.add(console_.consoleTag, console_.defaultConsole), + Context.add(random.randomTag, random.make(Math.random())), + Context.add(configProvider.configProviderTag, configProvider.fromEnv()), + Context.add(tracer.tracerTag, tracer.nativeTracer) +) + +/** + * The `FiberRef` holding the default `Effect` services. + * + * @since 2.0.0 + * @category fiberRefs + */ +export const currentServices = globalValue( + Symbol.for("effect/DefaultServices/currentServices"), + () => core.fiberRefUnsafeMakeContext(liveServices) +) + +// circular with Clock + +/** @internal */ +export const sleep = (duration: Duration.DurationInput): Effect.Effect => { + const decodedDuration = Duration.decode(duration) + return clockWith((clock) => clock.sleep(decodedDuration)) +} + +/** @internal */ +export const defaultServicesWith = ( + f: (services: Context.Context) => Effect.Effect +) => core.withFiberRuntime((fiber) => f(fiber.currentDefaultServices)) + +/** @internal */ +export const clockWith = (f: (clock: Clock.Clock) => Effect.Effect): Effect.Effect => + defaultServicesWith((services) => f(services.unsafeMap.get(clock.clockTag.key))) + +/** @internal */ +export const currentTimeMillis: Effect.Effect = clockWith((clock) => clock.currentTimeMillis) + +/** @internal */ +export const currentTimeNanos: Effect.Effect = clockWith((clock) => clock.currentTimeNanos) + +/** @internal */ +export const withClock = dual< + (clock: C) => (effect: Effect.Effect) => Effect.Effect, + (effect: Effect.Effect, clock: C) => Effect.Effect +>(2, (effect, c) => + core.fiberRefLocallyWith( + currentServices, + Context.add(clock.clockTag, c) + )(effect)) + +// circular with ConfigProvider + +/** @internal */ +export const withConfigProvider = dual< + (provider: ConfigProvider.ConfigProvider) => (self: Effect.Effect) => Effect.Effect, + (self: Effect.Effect, provider: ConfigProvider.ConfigProvider) => Effect.Effect +>(2, (self, provider) => + core.fiberRefLocallyWith( + currentServices, + Context.add(configProvider.configProviderTag, provider) + )(self)) + +/** @internal */ +export const configProviderWith = ( + f: (provider: ConfigProvider.ConfigProvider) => Effect.Effect +): Effect.Effect => + defaultServicesWith((services) => f(services.unsafeMap.get(configProvider.configProviderTag.key))) + +/** @internal */ +export const config = (config: Config.Config) => configProviderWith((_) => _.load(config)) + +/** @internal */ +export const configOrDie = (config: Config.Config) => core.orDie(configProviderWith((_) => _.load(config))) + +// circular with Random + +/** @internal */ +export const randomWith = (f: (random: Random.Random) => Effect.Effect): Effect.Effect => + defaultServicesWith((services) => f(services.unsafeMap.get(random.randomTag.key))) + +/** @internal */ +export const withRandom = dual< + (value: X) => (effect: Effect.Effect) => Effect.Effect, + (effect: Effect.Effect, value: X) => Effect.Effect +>(2, (effect, value) => + core.fiberRefLocallyWith( + currentServices, + Context.add(random.randomTag, value) + )(effect)) + +/** @internal */ +export const next: Effect.Effect = randomWith((random) => random.next) + +/** @internal */ +export const nextInt: Effect.Effect = randomWith((random) => random.nextInt) + +/** @internal */ +export const nextBoolean: Effect.Effect = randomWith((random) => random.nextBoolean) + +/** @internal */ +export const nextRange = (min: number, max: number): Effect.Effect => + randomWith((random) => random.nextRange(min, max)) + +/** @internal */ +export const nextIntBetween = (min: number, max: number): Effect.Effect => + randomWith((random) => random.nextIntBetween(min, max)) + +/** @internal */ +export const shuffle = (elements: Iterable): Effect.Effect> => + randomWith((random) => random.shuffle(elements)) + +/** @internal */ +export const choice = >( + elements: Self +) => { + const array = Array.fromIterable(elements) + return core.map( + array.length === 0 + ? core.fail(new core.NoSuchElementException("Cannot select a random element from an empty array")) + : randomWith((random) => random.nextIntBetween(0, array.length)), + (i) => array[i] + ) as any +} + +// circular with Tracer + +/** @internal */ +export const tracerWith = (f: (tracer: Tracer.Tracer) => Effect.Effect): Effect.Effect => + defaultServicesWith((services) => f(services.unsafeMap.get(tracer.tracerTag.key))) + +/** @internal */ +export const withTracer = dual< + (value: Tracer.Tracer) => (effect: Effect.Effect) => Effect.Effect, + (effect: Effect.Effect, value: Tracer.Tracer) => Effect.Effect +>(2, (effect, value) => + core.fiberRefLocallyWith( + currentServices, + Context.add(tracer.tracerTag, value) + )(effect)) diff --git a/repos/effect/packages/effect/src/internal/defaultServices/console.ts b/repos/effect/packages/effect/src/internal/defaultServices/console.ts new file mode 100644 index 0000000..8e16420 --- /dev/null +++ b/repos/effect/packages/effect/src/internal/defaultServices/console.ts @@ -0,0 +1,100 @@ +/* eslint-disable no-console */ +import type * as Console from "../../Console.js" +import * as Context from "../../Context.js" +import * as core from "../core.js" + +/** @internal */ +export const TypeId: Console.TypeId = Symbol.for("effect/Console") as Console.TypeId + +/** @internal */ +export const consoleTag: Context.Tag = Context.GenericTag( + "effect/Console" +) + +/** @internal */ +export const defaultConsole: Console.Console = { + [TypeId]: TypeId, + assert(condition, ...args) { + return core.sync(() => { + console.assert(condition, ...args) + }) + }, + clear: core.sync(() => { + console.clear() + }), + count(label) { + return core.sync(() => { + console.count(label) + }) + }, + countReset(label) { + return core.sync(() => { + console.countReset(label) + }) + }, + debug(...args) { + return core.sync(() => { + console.debug(...args) + }) + }, + dir(item, options) { + return core.sync(() => { + console.dir(item, options) + }) + }, + dirxml(...args) { + return core.sync(() => { + console.dirxml(...args) + }) + }, + error(...args) { + return core.sync(() => { + console.error(...args) + }) + }, + group(options) { + return options?.collapsed ? + core.sync(() => console.groupCollapsed(options?.label)) : + core.sync(() => console.group(options?.label)) + }, + groupEnd: core.sync(() => { + console.groupEnd() + }), + info(...args) { + return core.sync(() => { + console.info(...args) + }) + }, + log(...args) { + return core.sync(() => { + console.log(...args) + }) + }, + table(tabularData, properties) { + return core.sync(() => { + console.table(tabularData, properties) + }) + }, + time(label) { + return core.sync(() => console.time(label)) + }, + timeEnd(label) { + return core.sync(() => console.timeEnd(label)) + }, + timeLog(label, ...args) { + return core.sync(() => { + console.timeLog(label, ...args) + }) + }, + trace(...args) { + return core.sync(() => { + console.trace(...args) + }) + }, + warn(...args) { + return core.sync(() => { + console.warn(...args) + }) + }, + unsafe: console +} diff --git a/repos/effect/packages/effect/src/internal/deferred.ts b/repos/effect/packages/effect/src/internal/deferred.ts new file mode 100644 index 0000000..a1c1df1 --- /dev/null +++ b/repos/effect/packages/effect/src/internal/deferred.ts @@ -0,0 +1,46 @@ +import type * as Deferred from "../Deferred.js" +import type * as Effect from "../Effect.js" +import * as OpCodes from "./opCodes/deferred.js" + +/** @internal */ +const DeferredSymbolKey = "effect/Deferred" + +/** @internal */ +export const DeferredTypeId: Deferred.DeferredTypeId = Symbol.for( + DeferredSymbolKey +) as Deferred.DeferredTypeId + +/** @internal */ +export const deferredVariance = { + /* c8 ignore next */ + _E: (_: any) => _, + /* c8 ignore next */ + _A: (_: any) => _ +} + +/** @internal */ +export type State = Pending | Done + +/** @internal */ +export interface Pending { + readonly _tag: "Pending" + readonly joiners: Array<(effect: Effect.Effect) => void> +} + +/** @internal */ +export interface Done { + readonly _tag: "Done" + readonly effect: Effect.Effect +} + +/** @internal */ +export const pending = ( + joiners: Array<(effect: Effect.Effect) => void> +): State => { + return { _tag: OpCodes.OP_STATE_PENDING, joiners } +} + +/** @internal */ +export const done = (effect: Effect.Effect): State => { + return { _tag: OpCodes.OP_STATE_DONE, effect } +} diff --git a/repos/effect/packages/effect/src/internal/differ.ts b/repos/effect/packages/effect/src/internal/differ.ts new file mode 100644 index 0000000..0c07ccc --- /dev/null +++ b/repos/effect/packages/effect/src/internal/differ.ts @@ -0,0 +1,200 @@ +import type { Chunk } from "../Chunk.js" +import type { Context } from "../Context.js" +import type * as Differ from "../Differ.js" +import type { Either } from "../Either.js" +import * as Equal from "../Equal.js" +import * as Dual from "../Function.js" +import { constant, identity } from "../Function.js" +import type { HashMap } from "../HashMap.js" +import type { HashSet } from "../HashSet.js" +import { pipeArguments } from "../Pipeable.js" +import * as ChunkPatch from "./differ/chunkPatch.js" +import * as ContextPatch from "./differ/contextPatch.js" +import * as HashMapPatch from "./differ/hashMapPatch.js" +import * as HashSetPatch from "./differ/hashSetPatch.js" +import * as OrPatch from "./differ/orPatch.js" +import * as ReadonlyArrayPatch from "./differ/readonlyArrayPatch.js" + +/** @internal */ +export const DifferTypeId: Differ.TypeId = Symbol.for("effect/Differ") as Differ.TypeId + +/** @internal */ +export const DifferProto = { + [DifferTypeId]: { + _P: identity, + _V: identity + }, + pipe() { + return pipeArguments(this, arguments) + } +} + +/** @internal */ +export const make = ( + params: { + readonly empty: Patch + readonly diff: (oldValue: Value, newValue: Value) => Patch + readonly combine: (first: Patch, second: Patch) => Patch + readonly patch: (patch: Patch, oldValue: Value) => Value + } +): Differ.Differ => { + const differ = Object.create(DifferProto) + differ.empty = params.empty + differ.diff = params.diff + differ.combine = params.combine + differ.patch = params.patch + return differ +} + +/** @internal */ +export const environment = (): Differ.Differ, Differ.Differ.Context.Patch> => + make({ + empty: ContextPatch.empty(), + combine: (first, second) => ContextPatch.combine(second)(first), + diff: (oldValue, newValue) => ContextPatch.diff(oldValue, newValue), + patch: (patch, oldValue) => ContextPatch.patch(oldValue)(patch) + }) + +/** @internal */ +export const chunk = ( + differ: Differ.Differ +): Differ.Differ, Differ.Differ.Chunk.Patch> => + make({ + empty: ChunkPatch.empty(), + combine: (first, second) => ChunkPatch.combine(second)(first), + diff: (oldValue, newValue) => ChunkPatch.diff({ oldValue, newValue, differ }), + patch: (patch, oldValue) => ChunkPatch.patch(oldValue, differ)(patch) + }) + +/** @internal */ +export const hashMap = ( + differ: Differ.Differ +): Differ.Differ, Differ.Differ.HashMap.Patch> => + make({ + empty: HashMapPatch.empty(), + combine: (first, second) => HashMapPatch.combine(second)(first), + diff: (oldValue, newValue) => HashMapPatch.diff({ oldValue, newValue, differ }), + patch: (patch, oldValue) => HashMapPatch.patch(oldValue, differ)(patch) + }) + +/** @internal */ +export const hashSet = (): Differ.Differ, Differ.Differ.HashSet.Patch> => + make({ + empty: HashSetPatch.empty(), + combine: (first, second) => HashSetPatch.combine(second)(first), + diff: (oldValue, newValue) => HashSetPatch.diff(oldValue, newValue), + patch: (patch, oldValue) => HashSetPatch.patch(oldValue)(patch) + }) + +/** @internal */ +export const orElseEither = Dual.dual< + (that: Differ.Differ) => ( + self: Differ.Differ + ) => Differ.Differ, Differ.Differ.Or.Patch>, + ( + self: Differ.Differ, + that: Differ.Differ + ) => Differ.Differ, Differ.Differ.Or.Patch> +>(2, (self, that) => + make({ + empty: OrPatch.empty(), + combine: (first, second) => OrPatch.combine(first, second), + diff: (oldValue, newValue) => + OrPatch.diff({ + oldValue, + newValue, + left: self, + right: that + }), + patch: (patch, oldValue) => + OrPatch.patch(patch, { + oldValue, + left: self, + right: that + }) + })) + +/** @internal */ +export const readonlyArray = ( + differ: Differ.Differ +): Differ.Differ, Differ.Differ.ReadonlyArray.Patch> => + make({ + empty: ReadonlyArrayPatch.empty(), + combine: (first, second) => ReadonlyArrayPatch.combine(first, second), + diff: (oldValue, newValue) => ReadonlyArrayPatch.diff({ oldValue, newValue, differ }), + patch: (patch, oldValue) => ReadonlyArrayPatch.patch(patch, oldValue, differ) + }) + +/** @internal */ +export const transform = Dual.dual< + ( + options: { + readonly toNew: (value: Value) => Value2 + readonly toOld: (value: Value2) => Value + } + ) => (self: Differ.Differ) => Differ.Differ, + ( + self: Differ.Differ, + options: { + readonly toNew: (value: Value) => Value2 + readonly toOld: (value: Value2) => Value + } + ) => Differ.Differ +>(2, (self, { toNew, toOld }) => + make({ + empty: self.empty, + combine: (first, second) => self.combine(first, second), + diff: (oldValue, newValue) => self.diff(toOld(oldValue), toOld(newValue)), + patch: (patch, oldValue) => toNew(self.patch(patch, toOld(oldValue))) + })) + +/** @internal */ +export const update = (): Differ.Differ A> => updateWith((_, a) => a) + +/** @internal */ +export const updateWith = (f: (x: A, y: A) => A): Differ.Differ A> => + make({ + empty: identity, + combine: (first, second) => { + if (first === identity) { + return second + } + if (second === identity) { + return first + } + return (a) => second(first(a)) + }, + diff: (oldValue, newValue) => { + if (Equal.equals(oldValue, newValue)) { + return identity + } + return constant(newValue) + }, + patch: (patch, oldValue) => f(oldValue, patch(oldValue)) + }) + +/** @internal */ +export const zip = Dual.dual< + (that: Differ.Differ) => ( + self: Differ.Differ + ) => Differ.Differ, + ( + self: Differ.Differ, + that: Differ.Differ + ) => Differ.Differ +>(2, (self, that) => + make({ + empty: [self.empty, that.empty] as const, + combine: (first, second) => [ + self.combine(first[0], second[0]), + that.combine(first[1], second[1]) + ], + diff: (oldValue, newValue) => [ + self.diff(oldValue[0], newValue[0]), + that.diff(oldValue[1], newValue[1]) + ], + patch: (patch, oldValue) => [ + self.patch(patch[0], oldValue[0]), + that.patch(patch[1], oldValue[1]) + ] + })) diff --git a/repos/effect/packages/effect/src/internal/differ/chunkPatch.ts b/repos/effect/packages/effect/src/internal/differ/chunkPatch.ts new file mode 100644 index 0000000..336c4c0 --- /dev/null +++ b/repos/effect/packages/effect/src/internal/differ/chunkPatch.ts @@ -0,0 +1,211 @@ +import * as Chunk from "../../Chunk.js" +import type * as Differ from "../../Differ.js" +import * as Equal from "../../Equal.js" +import * as Dual from "../../Function.js" +import { pipe } from "../../Function.js" +import * as Data from "../data.js" + +/** @internal */ +export const ChunkPatchTypeId: Differ.Differ.Chunk.TypeId = Symbol.for( + "effect/DifferChunkPatch" +) as Differ.Differ.Chunk.TypeId + +function variance(a: A): B { + return a as unknown as B +} + +const PatchProto = { + ...Data.Structural.prototype, + [ChunkPatchTypeId]: { + _Value: variance, + _Patch: variance + } +} + +interface Empty extends Differ.Differ.Chunk.Patch { + readonly _tag: "Empty" +} + +const EmptyProto = Object.assign(Object.create(PatchProto), { + _tag: "Empty" +}) + +const _empty = Object.create(EmptyProto) + +/** + * @internal + */ +export const empty = (): Differ.Differ.Chunk.Patch => _empty + +interface AndThen extends Differ.Differ.Chunk.Patch { + readonly _tag: "AndThen" + readonly first: Differ.Differ.Chunk.Patch + readonly second: Differ.Differ.Chunk.Patch +} + +const AndThenProto = Object.assign(Object.create(PatchProto), { + _tag: "AndThen" +}) + +const makeAndThen = ( + first: Differ.Differ.Chunk.Patch, + second: Differ.Differ.Chunk.Patch +): Differ.Differ.Chunk.Patch => { + const o = Object.create(AndThenProto) + o.first = first + o.second = second + return o +} + +interface Append extends Differ.Differ.Chunk.Patch { + readonly _tag: "Append" + readonly values: Chunk.Chunk +} + +const AppendProto = Object.assign(Object.create(PatchProto), { + _tag: "Append" +}) + +const makeAppend = (values: Chunk.Chunk): Differ.Differ.Chunk.Patch => { + const o = Object.create(AppendProto) + o.values = values + return o +} + +interface Slice extends Differ.Differ.Chunk.Patch { + readonly _tag: "Slice" + readonly from: number + readonly until: number +} + +const SliceProto = Object.assign(Object.create(PatchProto), { + _tag: "Slice" +}) + +const makeSlice = (from: number, until: number): Differ.Differ.Chunk.Patch => { + const o = Object.create(SliceProto) + o.from = from + o.until = until + return o +} + +interface Update extends Differ.Differ.Chunk.Patch { + readonly _tag: "Update" + readonly index: number + readonly patch: Patch +} + +const UpdateProto = Object.assign(Object.create(PatchProto), { + _tag: "Update" +}) + +const makeUpdate = (index: number, patch: Patch): Differ.Differ.Chunk.Patch => { + const o = Object.create(UpdateProto) + o.index = index + o.patch = patch + return o +} + +type Instruction = + | Empty + | AndThen + | Append + | Slice + | Update + +/** @internal */ +export const diff = ( + options: { + readonly oldValue: Chunk.Chunk + readonly newValue: Chunk.Chunk + readonly differ: Differ.Differ + } +): Differ.Differ.Chunk.Patch => { + let i = 0 + let patch = empty() + while (i < options.oldValue.length && i < options.newValue.length) { + const oldElement = Chunk.unsafeGet(i)(options.oldValue) + const newElement = Chunk.unsafeGet(i)(options.newValue) + const valuePatch = options.differ.diff(oldElement, newElement) + if (!Equal.equals(valuePatch, options.differ.empty)) { + patch = pipe(patch, combine(makeUpdate(i, valuePatch))) + } + i = i + 1 + } + if (i < options.oldValue.length) { + patch = pipe(patch, combine(makeSlice(0, i))) + } + if (i < options.newValue.length) { + patch = pipe(patch, combine(makeAppend(Chunk.drop(i)(options.newValue)))) + } + return patch +} + +/** @internal */ +export const combine = Dual.dual< + ( + that: Differ.Differ.Chunk.Patch + ) => ( + self: Differ.Differ.Chunk.Patch + ) => Differ.Differ.Chunk.Patch, + ( + self: Differ.Differ.Chunk.Patch, + that: Differ.Differ.Chunk.Patch + ) => Differ.Differ.Chunk.Patch +>(2, (self, that) => makeAndThen(self, that)) + +/** @internal */ +export const patch = Dual.dual< + ( + oldValue: Chunk.Chunk, + differ: Differ.Differ + ) => (self: Differ.Differ.Chunk.Patch) => Chunk.Chunk, + ( + self: Differ.Differ.Chunk.Patch, + oldValue: Chunk.Chunk, + differ: Differ.Differ + ) => Chunk.Chunk +>(3, ( + self: Differ.Differ.Chunk.Patch, + oldValue: Chunk.Chunk, + differ: Differ.Differ +) => { + if ((self as Instruction)._tag === "Empty") { + return oldValue + } + let chunk = oldValue + let patches: Chunk.Chunk> = Chunk.of(self) + while (Chunk.isNonEmpty(patches)) { + const head: Instruction = Chunk.headNonEmpty(patches) as Instruction + const tail = Chunk.tailNonEmpty(patches) + switch (head._tag) { + case "Empty": { + patches = tail + break + } + case "AndThen": { + patches = Chunk.prepend(head.first)(Chunk.prepend(head.second)(tail)) + break + } + case "Append": { + chunk = Chunk.appendAll(head.values)(chunk) + patches = tail + break + } + case "Slice": { + const array = Chunk.toReadonlyArray(chunk) + chunk = Chunk.unsafeFromArray(array.slice(head.from, head.until)) + patches = tail + break + } + case "Update": { + const array = Chunk.toReadonlyArray(chunk) as Array + array[head.index] = differ.patch(head.patch, array[head.index]!) + chunk = Chunk.unsafeFromArray(array) + patches = tail + break + } + } + } + return chunk +}) diff --git a/repos/effect/packages/effect/src/internal/differ/contextPatch.ts b/repos/effect/packages/effect/src/internal/differ/contextPatch.ts new file mode 100644 index 0000000..90b5736 --- /dev/null +++ b/repos/effect/packages/effect/src/internal/differ/contextPatch.ts @@ -0,0 +1,232 @@ +import * as Chunk from "../../Chunk.js" +import type { Context } from "../../Context.js" +import type { Differ } from "../../Differ.js" +import * as Equal from "../../Equal.js" +import * as Dual from "../../Function.js" +import { makeContext } from "../context.js" +import { Structural } from "../data.js" + +/** @internal */ +export const ContextPatchTypeId: Differ.Context.TypeId = Symbol.for( + "effect/DifferContextPatch" +) as Differ.Context.TypeId + +function variance(a: A): B { + return a as unknown as B +} + +/** @internal */ +const PatchProto = { + ...Structural.prototype, + [ContextPatchTypeId]: { + _Value: variance, + _Patch: variance + } +} + +interface Empty extends Differ.Context.Patch { + readonly _tag: "Empty" +} + +const EmptyProto = Object.assign(Object.create(PatchProto), { + _tag: "Empty" +}) + +const _empty = Object.create(EmptyProto) + +/** + * @internal + */ +export const empty = (): Differ.Context.Patch => _empty + +/** @internal */ +export interface AndThen extends Differ.Context.Patch { + readonly _tag: "AndThen" + readonly first: Differ.Context.Patch + readonly second: Differ.Context.Patch +} + +const AndThenProto = Object.assign(Object.create(PatchProto), { + _tag: "AndThen" +}) + +const makeAndThen = ( + first: Differ.Context.Patch, + second: Differ.Context.Patch +): Differ.Context.Patch => { + const o = Object.create(AndThenProto) + o.first = first + o.second = second + return o +} + +/** @internal */ +export interface AddService extends Differ.Context.Patch { + readonly _tag: "AddService" + readonly key: string + readonly service: T +} + +const AddServiceProto = Object.assign(Object.create(PatchProto), { + _tag: "AddService" +}) + +const makeAddService = ( + key: string, + service: T +): Differ.Context.Patch => { + const o = Object.create(AddServiceProto) + o.key = key + o.service = service + return o +} + +/** @internal */ +export interface RemoveService extends Differ.Context.Patch> { + readonly _tag: "RemoveService" + readonly key: string +} + +const RemoveServiceProto = Object.assign(Object.create(PatchProto), { + _tag: "RemoveService" +}) + +const makeRemoveService = ( + key: string +): Differ.Context.Patch> => { + const o = Object.create(RemoveServiceProto) + o.key = key + return o +} + +/** @internal */ +export interface UpdateService extends Differ.Context.Patch { + readonly _tag: "UpdateService" + readonly key: string + update(service: T): T +} + +const UpdateServiceProto = Object.assign(Object.create(PatchProto), { + _tag: "UpdateService" +}) + +const makeUpdateService = ( + key: string, + update: (service: T) => T +): Differ.Context.Patch => { + const o = Object.create(UpdateServiceProto) + o.key = key + o.update = update + return o +} + +type Instruction = + | Empty + | AndThen + | AddService + | RemoveService + | UpdateService + +/** @internal */ +export const diff = ( + oldValue: Context, + newValue: Context +): Differ.Context.Patch => { + const missingServices = new Map(oldValue.unsafeMap) + let patch = empty() + for (const [tag, newService] of newValue.unsafeMap.entries()) { + if (missingServices.has(tag)) { + const old = missingServices.get(tag)! + missingServices.delete(tag) + if (!Equal.equals(old, newService)) { + patch = combine(makeUpdateService(tag, () => newService))(patch) + } + } else { + missingServices.delete(tag) + patch = combine(makeAddService(tag, newService))(patch) + } + } + for (const [tag] of missingServices.entries()) { + patch = combine(makeRemoveService(tag))(patch) + } + return patch +} + +/** @internal */ +export const combine = Dual.dual< + ( + that: Differ.Context.Patch + ) => ( + self: Differ.Context.Patch + ) => Differ.Context.Patch, + ( + self: Differ.Context.Patch, + that: Differ.Context.Patch + ) => Differ.Context.Patch +>(2, (self, that) => makeAndThen(self, that)) + +/** @internal */ +export const patch = Dual.dual< + ( + context: Context + ) => ( + self: Differ.Context.Patch + ) => Context, + ( + self: Differ.Context.Patch, + context: Context + ) => Context +>(2, (self: Differ.Context.Patch, context: Context) => { + if ((self as Instruction)._tag === "Empty") { + return context as any + } + let wasServiceUpdated = false + let patches: Chunk.Chunk> = Chunk.of( + self as Differ.Context.Patch + ) + const updatedContext: Map = new Map(context.unsafeMap) + while (Chunk.isNonEmpty(patches)) { + const head: Instruction = Chunk.headNonEmpty(patches) as Instruction + const tail = Chunk.tailNonEmpty(patches) + switch (head._tag) { + case "Empty": { + patches = tail + break + } + case "AddService": { + updatedContext.set(head.key, head.service) + patches = tail + break + } + case "AndThen": { + patches = Chunk.prepend(Chunk.prepend(tail, head.second), head.first) + break + } + case "RemoveService": { + updatedContext.delete(head.key) + patches = tail + break + } + case "UpdateService": { + updatedContext.set(head.key, head.update(updatedContext.get(head.key))) + wasServiceUpdated = true + patches = tail + break + } + } + } + if (!wasServiceUpdated) { + return makeContext(updatedContext) as Context + } + const map = new Map() + for (const [tag] of context.unsafeMap) { + if (updatedContext.has(tag)) { + map.set(tag, updatedContext.get(tag)) + updatedContext.delete(tag) + } + } + for (const [tag, s] of updatedContext) { + map.set(tag, s) + } + return makeContext(map) as Context +}) diff --git a/repos/effect/packages/effect/src/internal/differ/hashMapPatch.ts b/repos/effect/packages/effect/src/internal/differ/hashMapPatch.ts new file mode 100644 index 0000000..0f5bfee --- /dev/null +++ b/repos/effect/packages/effect/src/internal/differ/hashMapPatch.ts @@ -0,0 +1,220 @@ +import * as Chunk from "../../Chunk.js" +import type * as Differ from "../../Differ.js" +import * as Equal from "../../Equal.js" +import * as Dual from "../../Function.js" +import * as HashMap from "../../HashMap.js" +import { Structural } from "../data.js" + +/** @internal */ +export const HashMapPatchTypeId: Differ.Differ.HashMap.TypeId = Symbol.for( + "effect/DifferHashMapPatch" +) as Differ.Differ.HashMap.TypeId + +function variance(a: A): B { + return a as unknown as B +} + +/** @internal */ +const PatchProto = { + ...Structural.prototype, + [HashMapPatchTypeId]: { + _Value: variance, + _Key: variance, + _Patch: variance + } +} + +interface Empty extends Differ.Differ.HashMap.Patch { + readonly _tag: "Empty" +} + +const EmptyProto = Object.assign(Object.create(PatchProto), { + _tag: "Empty" +}) + +const _empty = Object.create(EmptyProto) + +/** @internal */ +export const empty = (): Differ.Differ.HashMap.Patch => _empty + +interface AndThen extends Differ.Differ.HashMap.Patch { + readonly _tag: "AndThen" + readonly first: Differ.Differ.HashMap.Patch + readonly second: Differ.Differ.HashMap.Patch +} + +const AndThenProto = Object.assign(Object.create(PatchProto), { + _tag: "AndThen" +}) + +const makeAndThen = ( + first: Differ.Differ.HashMap.Patch, + second: Differ.Differ.HashMap.Patch +): Differ.Differ.HashMap.Patch => { + const o = Object.create(AndThenProto) + o.first = first + o.second = second + return o +} + +interface Add extends Differ.Differ.HashMap.Patch { + readonly _tag: "Add" + readonly key: Key + readonly value: Value +} + +const AddProto = Object.assign(Object.create(PatchProto), { + _tag: "Add" +}) + +const makeAdd = (key: Key, value: Value): Differ.Differ.HashMap.Patch => { + const o = Object.create(AddProto) + o.key = key + o.value = value + return o +} + +interface Remove extends Differ.Differ.HashMap.Patch { + readonly _tag: "Remove" + readonly key: Key +} + +const RemoveProto = Object.assign(Object.create(PatchProto), { + _tag: "Remove" +}) + +const makeRemove = (key: Key): Differ.Differ.HashMap.Patch => { + const o = Object.create(RemoveProto) + o.key = key + return o +} + +interface Update extends Differ.Differ.HashMap.Patch { + readonly _tag: "Update" + readonly key: Key + readonly patch: Patch +} + +const UpdateProto = Object.assign(Object.create(PatchProto), { + _tag: "Update" +}) + +const makeUpdate = (key: Key, patch: Patch): Differ.Differ.HashMap.Patch => { + const o = Object.create(UpdateProto) + o.key = key + o.patch = patch + return o +} + +type Instruction = + | Add + | Remove + | Update + | Empty + | AndThen + +/** @internal */ +export const diff = ( + options: { + readonly oldValue: HashMap.HashMap + readonly newValue: HashMap.HashMap + readonly differ: Differ.Differ + } +): Differ.Differ.HashMap.Patch => { + const [removed, patch] = HashMap.reduce( + [options.oldValue, empty()] as const, + ([map, patch], newValue: Value, key: Key) => { + const option = HashMap.get(key)(map) + switch (option._tag) { + case "Some": { + const valuePatch = options.differ.diff(option.value, newValue) + if (Equal.equals(valuePatch, options.differ.empty)) { + return [HashMap.remove(key)(map), patch] as const + } + return [ + HashMap.remove(key)(map), + combine(makeUpdate(key, valuePatch))(patch) + ] as const + } + case "None": { + return [map, combine(makeAdd(key, newValue))(patch)] as const + } + } + } + )(options.newValue) + return HashMap.reduce( + patch, + (patch, _, key: Key) => combine(makeRemove(key))(patch) + )(removed) +} + +/** @internal */ +export const combine = Dual.dual< + ( + that: Differ.Differ.HashMap.Patch + ) => ( + self: Differ.Differ.HashMap.Patch + ) => Differ.Differ.HashMap.Patch, + ( + self: Differ.Differ.HashMap.Patch, + that: Differ.Differ.HashMap.Patch + ) => Differ.Differ.HashMap.Patch +>(2, (self, that) => makeAndThen(self, that)) + +/** @internal */ +export const patch = Dual.dual< + ( + oldValue: HashMap.HashMap, + differ: Differ.Differ + ) => ( + self: Differ.Differ.HashMap.Patch + ) => HashMap.HashMap, + ( + self: Differ.Differ.HashMap.Patch, + oldValue: HashMap.HashMap, + differ: Differ.Differ + ) => HashMap.HashMap +>(3, ( + self: Differ.Differ.HashMap.Patch, + oldValue: HashMap.HashMap, + differ: Differ.Differ +) => { + if ((self as Instruction)._tag === "Empty") { + return oldValue + } + let map = oldValue + let patches: Chunk.Chunk> = Chunk.of(self) + while (Chunk.isNonEmpty(patches)) { + const head: Instruction = Chunk.headNonEmpty(patches) as Instruction + const tail = Chunk.tailNonEmpty(patches) + switch (head._tag) { + case "Empty": { + patches = tail + break + } + case "AndThen": { + patches = Chunk.prepend(head.first)(Chunk.prepend(head.second)(tail)) + break + } + case "Add": { + map = HashMap.set(head.key, head.value)(map) + patches = tail + break + } + case "Remove": { + map = HashMap.remove(head.key)(map) + patches = tail + break + } + case "Update": { + const option = HashMap.get(head.key)(map) + if (option._tag === "Some") { + map = HashMap.set(head.key, differ.patch(head.patch, option.value))(map) + } + patches = tail + break + } + } + } + return map +}) diff --git a/repos/effect/packages/effect/src/internal/differ/hashSetPatch.ts b/repos/effect/packages/effect/src/internal/differ/hashSetPatch.ts new file mode 100644 index 0000000..e33b382 --- /dev/null +++ b/repos/effect/packages/effect/src/internal/differ/hashSetPatch.ts @@ -0,0 +1,176 @@ +import * as Chunk from "../../Chunk.js" +import type { Differ } from "../../Differ.js" +import * as Dual from "../../Function.js" +import * as HashSet from "../../HashSet.js" +import { Structural } from "../data.js" + +/** @internal */ +export const HashSetPatchTypeId: Differ.HashSet.TypeId = Symbol.for( + "effect/DifferHashSetPatch" +) as Differ.HashSet.TypeId + +function variance(a: A): B { + return a as unknown as B +} + +/** @internal */ +const PatchProto = { + ...Structural.prototype, + [HashSetPatchTypeId]: { + _Value: variance, + _Key: variance, + _Patch: variance + } +} + +interface Empty extends Differ.HashSet.Patch { + readonly _tag: "Empty" +} + +const EmptyProto = Object.assign(Object.create(PatchProto), { + _tag: "Empty" +}) + +const _empty = Object.create(EmptyProto) + +/** @internal */ +export const empty = (): Differ.HashSet.Patch => _empty + +interface AndThen extends Differ.HashSet.Patch { + readonly _tag: "AndThen" + readonly first: Differ.HashSet.Patch + readonly second: Differ.HashSet.Patch +} + +const AndThenProto = Object.assign(Object.create(PatchProto), { + _tag: "AndThen" +}) + +/** @internal */ +export const makeAndThen = ( + first: Differ.HashSet.Patch, + second: Differ.HashSet.Patch +): Differ.HashSet.Patch => { + const o = Object.create(AndThenProto) + o.first = first + o.second = second + return o +} + +interface Add extends Differ.HashSet.Patch { + readonly _tag: "Add" + readonly value: Value +} + +const AddProto = Object.assign(Object.create(PatchProto), { + _tag: "Add" +}) + +/** @internal */ +export const makeAdd = ( + value: Value +): Differ.HashSet.Patch => { + const o = Object.create(AddProto) + o.value = value + return o +} + +interface Remove extends Differ.HashSet.Patch { + readonly _tag: "Remove" + readonly value: Value +} + +const RemoveProto = Object.assign(Object.create(PatchProto), { + _tag: "Remove" +}) + +/** @internal */ +export const makeRemove = ( + value: Value +): Differ.HashSet.Patch => { + const o = Object.create(RemoveProto) + o.value = value + return o +} + +type Instruction = + | Add + | AndThen + | Empty + | Remove + +/** @internal */ +export const diff = ( + oldValue: HashSet.HashSet, + newValue: HashSet.HashSet +): Differ.HashSet.Patch => { + const [removed, patch] = HashSet.reduce( + [oldValue, empty()] as const, + ([set, patch], value: Value) => { + if (HashSet.has(value)(set)) { + return [HashSet.remove(value)(set), patch] as const + } + return [set, combine(makeAdd(value))(patch)] as const + } + )(newValue) + return HashSet.reduce(patch, (patch, value: Value) => combine(makeRemove(value))(patch))(removed) +} + +/** @internal */ +export const combine = Dual.dual< + ( + that: Differ.HashSet.Patch + ) => ( + self: Differ.HashSet.Patch + ) => Differ.HashSet.Patch, + ( + self: Differ.HashSet.Patch, + that: Differ.HashSet.Patch + ) => Differ.HashSet.Patch +>(2, (self, that) => makeAndThen(self, that)) + +/** @internal */ +export const patch = Dual.dual< + ( + oldValue: HashSet.HashSet + ) => ( + self: Differ.HashSet.Patch + ) => HashSet.HashSet, + ( + self: Differ.HashSet.Patch, + oldValue: HashSet.HashSet + ) => HashSet.HashSet +>(2, ( + self: Differ.HashSet.Patch, + oldValue: HashSet.HashSet +) => { + if ((self as Instruction)._tag === "Empty") { + return oldValue + } + let set = oldValue + let patches: Chunk.Chunk> = Chunk.of(self) + while (Chunk.isNonEmpty(patches)) { + const head: Instruction = Chunk.headNonEmpty(patches) as Instruction + const tail = Chunk.tailNonEmpty(patches) + switch (head._tag) { + case "Empty": { + patches = tail + break + } + case "AndThen": { + patches = Chunk.prepend(head.first)(Chunk.prepend(head.second)(tail)) + break + } + case "Add": { + set = HashSet.add(head.value)(set) + patches = tail + break + } + case "Remove": { + set = HashSet.remove(head.value)(set) + patches = tail + } + } + } + return set +}) diff --git a/repos/effect/packages/effect/src/internal/differ/orPatch.ts b/repos/effect/packages/effect/src/internal/differ/orPatch.ts new file mode 100644 index 0000000..71e71cc --- /dev/null +++ b/repos/effect/packages/effect/src/internal/differ/orPatch.ts @@ -0,0 +1,311 @@ +import * as Chunk from "../../Chunk.js" +import type { Differ } from "../../Differ.js" +import type { Either } from "../../Either.js" +import * as E from "../../Either.js" +import * as Equal from "../../Equal.js" +import * as Dual from "../../Function.js" +import { Structural } from "../data.js" + +/** @internal */ +export const OrPatchTypeId: Differ.Or.TypeId = Symbol.for("effect/DifferOrPatch") as Differ.Or.TypeId + +function variance(a: A): B { + return a as unknown as B +} + +/** @internal */ +const PatchProto = { + ...Structural.prototype, + [OrPatchTypeId]: { + _Value: variance, + _Key: variance, + _Patch: variance + } +} + +/** @internal */ +export interface Empty + extends Differ.Or.Patch +{ + readonly _tag: "Empty" +} + +const EmptyProto = Object.assign(Object.create(PatchProto), { + _tag: "Empty" +}) + +const _empty = Object.create(EmptyProto) + +/** @internal */ +export const empty = (): Differ.Or.Patch< + Value, + Value2, + Patch, + Patch2 +> => _empty + +/** @internal */ +export interface AndThen + extends Differ.Or.Patch +{ + readonly _tag: "AndThen" + readonly first: Differ.Or.Patch + readonly second: Differ.Or.Patch +} + +const AndThenProto = Object.assign(Object.create(PatchProto), { + _tag: "AndThen" +}) + +/** @internal */ +export const makeAndThen = ( + first: Differ.Or.Patch, + second: Differ.Or.Patch +): Differ.Or.Patch< + Value, + Value2, + Patch, + Patch2 +> => { + const o = Object.create(AndThenProto) + o.first = first + o.second = second + return o +} + +/** @internal */ +export interface SetLeft + extends Differ.Or.Patch +{ + readonly _tag: "SetLeft" + readonly value: Value +} + +const SetLeftProto = Object.assign(Object.create(PatchProto), { + _tag: "SetLeft" +}) + +/** @internal */ +export const makeSetLeft = ( + value: Value +): Differ.Or.Patch< + Value, + Value2, + Patch, + Patch2 +> => { + const o = Object.create(SetLeftProto) + o.value = value + return o +} + +/** @internal */ +export interface SetRight + extends Differ.Or.Patch +{ + readonly _tag: "SetRight" + readonly value: Value2 +} + +const SetRightProto = Object.assign(Object.create(PatchProto), { + _tag: "SetRight" +}) + +/** @internal */ +export const makeSetRight = ( + value: Value2 +): Differ.Or.Patch< + Value, + Value2, + Patch, + Patch2 +> => { + const o = Object.create(SetRightProto) + o.value = value + return o +} + +/** @internal */ +export interface UpdateLeft + extends Differ.Or.Patch +{ + readonly _tag: "UpdateLeft" + readonly patch: Patch +} + +const UpdateLeftProto = Object.assign(Object.create(PatchProto), { + _tag: "UpdateLeft" +}) + +/** @internal */ +export const makeUpdateLeft = ( + patch: Patch +): Differ.Or.Patch< + Value, + Value2, + Patch, + Patch2 +> => { + const o = Object.create(UpdateLeftProto) + o.patch = patch + return o +} + +/** @internal */ +export interface UpdateRight + extends Differ.Or.Patch +{ + readonly _tag: "UpdateRight" + readonly patch: Patch2 +} + +const UpdateRightProto = Object.assign(Object.create(PatchProto), { + _tag: "UpdateRight" +}) + +/** @internal */ +export const makeUpdateRight = ( + patch: Patch2 +): Differ.Or.Patch< + Value, + Value2, + Patch, + Patch2 +> => { + const o = Object.create(UpdateRightProto) + o.patch = patch + return o +} + +type Instruction = + | AndThen + | Empty + | SetLeft + | SetRight + | UpdateLeft + | UpdateRight + +/** @internal */ +export const diff = ( + options: { + readonly oldValue: Either + readonly newValue: Either + readonly left: Differ + readonly right: Differ + } +): Differ.Or.Patch => { + switch (options.oldValue._tag) { + case "Left": { + switch (options.newValue._tag) { + case "Left": { + const valuePatch = options.left.diff(options.oldValue.left, options.newValue.left) + if (Equal.equals(valuePatch, options.left.empty)) { + return empty() + } + return makeUpdateLeft(valuePatch) + } + case "Right": { + return makeSetRight(options.newValue.right) + } + } + } + case "Right": { + switch (options.newValue._tag) { + case "Left": { + return makeSetLeft(options.newValue.left) + } + case "Right": { + const valuePatch = options.right.diff(options.oldValue.right, options.newValue.right) + if (Equal.equals(valuePatch, options.right.empty)) { + return empty() + } + return makeUpdateRight(valuePatch) + } + } + } + } +} + +/** @internal */ +export const combine = Dual.dual< + ( + that: Differ.Or.Patch + ) => ( + self: Differ.Or.Patch + ) => Differ.Or.Patch, + ( + self: Differ.Or.Patch, + that: Differ.Or.Patch + ) => Differ.Or.Patch +>(2, (self, that) => makeAndThen(self, that)) + +/** @internal */ +export const patch = Dual.dual< + ( + options: { + readonly oldValue: Either + readonly left: Differ + readonly right: Differ + } + ) => (self: Differ.Or.Patch) => Either, + ( + self: Differ.Or.Patch, + options: { + readonly oldValue: Either + readonly left: Differ + readonly right: Differ + } + ) => Either +>(2, ( + self: Differ.Or.Patch, + { left, oldValue, right }: { + oldValue: Either + left: Differ + right: Differ + } +) => { + if ((self as Instruction)._tag === "Empty") { + return oldValue + } + let patches: Chunk.Chunk> = Chunk.of(self) + let result = oldValue + while (Chunk.isNonEmpty(patches)) { + const head: Instruction = Chunk.headNonEmpty(patches) as Instruction + const tail = Chunk.tailNonEmpty(patches) + switch (head._tag) { + case "Empty": { + patches = tail + break + } + case "AndThen": { + patches = Chunk.prepend(head.first)(Chunk.prepend(head.second)(tail)) + break + } + case "UpdateLeft": { + if (result._tag === "Left") { + result = E.left(left.patch(head.patch, result.left)) + } + patches = tail + break + } + case "UpdateRight": { + if (result._tag === "Right") { + result = E.right(right.patch(head.patch, result.right)) + } + patches = tail + break + } + case "SetLeft": { + result = E.left(head.value) + patches = tail + break + } + case "SetRight": { + result = E.right(head.value) + patches = tail + break + } + } + } + return result +}) diff --git a/repos/effect/packages/effect/src/internal/differ/readonlyArrayPatch.ts b/repos/effect/packages/effect/src/internal/differ/readonlyArrayPatch.ts new file mode 100644 index 0000000..2cf6980 --- /dev/null +++ b/repos/effect/packages/effect/src/internal/differ/readonlyArrayPatch.ts @@ -0,0 +1,210 @@ +import * as Arr from "../../Array.js" +import type * as Differ from "../../Differ.js" +import * as Equal from "../../Equal.js" +import * as Dual from "../../Function.js" +import * as Data from "../data.js" + +/** @internal */ +export const ReadonlyArrayPatchTypeId: Differ.Differ.ReadonlyArray.TypeId = Symbol.for( + "effect/DifferReadonlyArrayPatch" +) as Differ.Differ.ReadonlyArray.TypeId + +function variance(a: A): B { + return a as unknown as B +} + +const PatchProto = { + ...Data.Structural.prototype, + [ReadonlyArrayPatchTypeId]: { + _Value: variance, + _Patch: variance + } +} + +interface Empty extends Differ.Differ.ReadonlyArray.Patch { + readonly _tag: "Empty" +} + +const EmptyProto = Object.assign(Object.create(PatchProto), { + _tag: "Empty" +}) + +const _empty = Object.create(EmptyProto) + +/** + * @internal + */ +export const empty = (): Differ.Differ.ReadonlyArray.Patch => _empty + +interface AndThen extends Differ.Differ.ReadonlyArray.Patch { + readonly _tag: "AndThen" + readonly first: Differ.Differ.ReadonlyArray.Patch + readonly second: Differ.Differ.ReadonlyArray.Patch +} + +const AndThenProto = Object.assign(Object.create(PatchProto), { + _tag: "AndThen" +}) + +const makeAndThen = ( + first: Differ.Differ.ReadonlyArray.Patch, + second: Differ.Differ.ReadonlyArray.Patch +): Differ.Differ.ReadonlyArray.Patch => { + const o = Object.create(AndThenProto) + o.first = first + o.second = second + return o +} + +interface Append extends Differ.Differ.ReadonlyArray.Patch { + readonly _tag: "Append" + readonly values: ReadonlyArray +} + +const AppendProto = Object.assign(Object.create(PatchProto), { + _tag: "Append" +}) + +const makeAppend = (values: ReadonlyArray): Differ.Differ.ReadonlyArray.Patch => { + const o = Object.create(AppendProto) + o.values = values + return o +} + +interface Slice extends Differ.Differ.ReadonlyArray.Patch { + readonly _tag: "Slice" + readonly from: number + readonly until: number +} + +const SliceProto = Object.assign(Object.create(PatchProto), { + _tag: "Slice" +}) + +const makeSlice = (from: number, until: number): Differ.Differ.ReadonlyArray.Patch => { + const o = Object.create(SliceProto) + o.from = from + o.until = until + return o +} + +interface Update extends Differ.Differ.ReadonlyArray.Patch { + readonly _tag: "Update" + readonly index: number + readonly patch: Patch +} + +const UpdateProto = Object.assign(Object.create(PatchProto), { + _tag: "Update" +}) + +const makeUpdate = (index: number, patch: Patch): Differ.Differ.ReadonlyArray.Patch => { + const o = Object.create(UpdateProto) + o.index = index + o.patch = patch + return o +} + +type Instruction = + | Empty + | AndThen + | Append + | Slice + | Update + +/** @internal */ +export const diff = ( + options: { + readonly oldValue: ReadonlyArray + readonly newValue: ReadonlyArray + readonly differ: Differ.Differ + } +): Differ.Differ.ReadonlyArray.Patch => { + let i = 0 + let patch = empty() + while (i < options.oldValue.length && i < options.newValue.length) { + const oldElement = options.oldValue[i]! + const newElement = options.newValue[i]! + const valuePatch = options.differ.diff(oldElement, newElement) + if (!Equal.equals(valuePatch, options.differ.empty)) { + patch = combine(patch, makeUpdate(i, valuePatch)) + } + i = i + 1 + } + if (i < options.oldValue.length) { + patch = combine(patch, makeSlice(0, i)) + } + if (i < options.newValue.length) { + patch = combine(patch, makeAppend(Arr.drop(i)(options.newValue))) + } + return patch +} + +/** @internal */ +export const combine = Dual.dual< + ( + that: Differ.Differ.ReadonlyArray.Patch + ) => ( + self: Differ.Differ.ReadonlyArray.Patch + ) => Differ.Differ.ReadonlyArray.Patch, + ( + self: Differ.Differ.ReadonlyArray.Patch, + that: Differ.Differ.ReadonlyArray.Patch + ) => Differ.Differ.ReadonlyArray.Patch +>(2, (self, that) => makeAndThen(self, that)) + +/** @internal */ +export const patch = Dual.dual< + ( + oldValue: ReadonlyArray, + differ: Differ.Differ + ) => (self: Differ.Differ.ReadonlyArray.Patch) => ReadonlyArray, + ( + self: Differ.Differ.ReadonlyArray.Patch, + oldValue: ReadonlyArray, + differ: Differ.Differ + ) => ReadonlyArray +>(3, ( + self: Differ.Differ.ReadonlyArray.Patch, + oldValue: ReadonlyArray, + differ: Differ.Differ +) => { + if ((self as Instruction)._tag === "Empty") { + return oldValue + } + let readonlyArray = oldValue.slice() + let patches: Array> = Arr.of(self) + while (Arr.isNonEmptyArray(patches)) { + const head: Instruction = Arr.headNonEmpty(patches) as Instruction + const tail = Arr.tailNonEmpty(patches) + switch (head._tag) { + case "Empty": { + patches = tail + break + } + case "AndThen": { + tail.unshift(head.first, head.second) + patches = tail + break + } + case "Append": { + for (const value of head.values) { + readonlyArray.push(value) + } + patches = tail + break + } + case "Slice": { + readonlyArray = readonlyArray.slice(head.from, head.until) + patches = tail + break + } + case "Update": { + readonlyArray[head.index] = differ.patch(head.patch, readonlyArray[head.index]!) + patches = tail + break + } + } + } + return readonlyArray +}) diff --git a/repos/effect/packages/effect/src/internal/doNotation.ts b/repos/effect/packages/effect/src/internal/doNotation.ts new file mode 100644 index 0000000..333895e --- /dev/null +++ b/repos/effect/packages/effect/src/internal/doNotation.ts @@ -0,0 +1,80 @@ +import { dual } from "../Function.js" +import type { Kind, TypeLambda } from "../HKT.js" +import type { NoInfer } from "../Types.js" + +type Map = { + (f: (a: A) => B): (self: Kind) => Kind + (self: Kind, f: (a: A) => B): Kind +} + +type FlatMap = { + ( + f: (a: A) => Kind + ): (self: Kind) => Kind + ( + self: Kind, + f: (a: A) => Kind + ): Kind +} + +/** @internal */ +export const let_ = ( + map: Map +): { + ( + name: Exclude, + f: (a: NoInfer) => B + ): ( + self: Kind + ) => Kind + ( + self: Kind, + name: Exclude, + f: (a: NoInfer) => B + ): Kind +} => + dual(3, ( + self: Kind, + name: Exclude, + f: (a: NoInfer) => B + ): Kind => + map(self, (a) => ({ ...a, [name]: f(a) }) as any)) + +/** @internal */ +export const bindTo = (map: Map): { + ( + name: N + ): (self: Kind) => Kind + ( + self: Kind, + name: N + ): Kind +} => + dual(2, ( + self: Kind, + name: N + ): Kind => map(self, (a) => ({ [name]: a } as { [K in N]: A }))) + +/** @internal */ +export const bind = (map: Map, flatMap: FlatMap): { + ( + name: Exclude, + f: (a: NoInfer) => Kind + ): ( + self: Kind + ) => Kind + ( + self: Kind, + name: Exclude, + f: (a: NoInfer) => Kind + ): Kind +} => + dual(3, ( + self: Kind, + name: Exclude, + f: (a: NoInfer) => Kind + ): Kind => + flatMap( + self, + (a) => map(f(a), (b) => ({ ...a, [name]: b }) as { [K in keyof A | N]: K extends keyof A ? A[K] : B }) + )) diff --git a/repos/effect/packages/effect/src/internal/effect/circular.ts b/repos/effect/packages/effect/src/internal/effect/circular.ts new file mode 100644 index 0000000..e64ea05 --- /dev/null +++ b/repos/effect/packages/effect/src/internal/effect/circular.ts @@ -0,0 +1,903 @@ +import type * as Cause from "../../Cause.js" +import type * as Deferred from "../../Deferred.js" +import * as Duration from "../../Duration.js" +import type * as Effect from "../../Effect.js" +import * as Effectable from "../../Effectable.js" +import * as Equal from "../../Equal.js" +import type { Equivalence } from "../../Equivalence.js" +import * as Exit from "../../Exit.js" +import type * as Fiber from "../../Fiber.js" +import * as FiberId from "../../FiberId.js" +import type * as FiberRefsPatch from "../../FiberRefsPatch.js" +import type { LazyArg } from "../../Function.js" +import { dual, pipe } from "../../Function.js" +import * as Hash from "../../Hash.js" +import * as MutableHashMap from "../../MutableHashMap.js" +import * as Option from "../../Option.js" +import { pipeArguments } from "../../Pipeable.js" +import * as Predicate from "../../Predicate.js" +import * as Readable from "../../Readable.js" +import type * as Ref from "../../Ref.js" +import { currentScheduler } from "../../Scheduler.js" +import type * as Scope from "../../Scope.js" +import type * as Supervisor from "../../Supervisor.js" +import type * as Synchronized from "../../SynchronizedRef.js" +import type * as Types from "../../Types.js" +import * as internalCause from "../cause.js" +import * as effect from "../core-effect.js" +import * as core from "../core.js" +import * as internalFiber from "../fiber.js" +import * as fiberRuntime from "../fiberRuntime.js" +import { globalScope } from "../fiberScope.js" +import * as internalRef from "../ref.js" +import * as supervisor from "../supervisor.js" + +/** @internal */ +class Semaphore { + public waiters = new Set<() => void>() + public taken = 0 + + constructor(public permits: number) {} + + get free() { + return this.permits - this.taken + } + + readonly take = (n: number): Effect.Effect => + core.asyncInterrupt((resume) => { + if (this.free < n) { + const observer = () => { + if (this.free < n) return + this.waiters.delete(observer) + resume(core.suspend(() => { + if (this.free < n) return this.take(n) + this.taken += n + return core.succeed(n) + })) + } + this.waiters.add(observer) + return core.sync(() => { + this.waiters.delete(observer) + }) + } + resume(core.suspend(() => { + if (this.free < n) return this.take(n) + this.taken += n + return core.succeed(n) + })) + }) + + updateTakenUnsafe(fiber: Fiber.RuntimeFiber, f: (n: number) => number): Effect.Effect { + this.taken = f(this.taken) + if (this.waiters.size > 0) { + fiber.getFiberRef(currentScheduler).scheduleTask( + () => { + const iter = this.waiters.values() + let item = iter.next() + while (item.done === false && this.free > 0) { + item.value() + item = iter.next() + } + }, + fiber.getFiberRef(core.currentSchedulingPriority), + fiber + ) + } + return core.succeed(this.free) + } + + updateTaken(f: (n: number) => number): Effect.Effect { + return core.withFiberRuntime((fiber) => this.updateTakenUnsafe(fiber, f)) + } + + readonly resize = (permits: number) => + core.asVoid( + core.withFiberRuntime((fiber) => { + this.permits = permits + if (this.free < 0) { + return core.void + } + return this.updateTakenUnsafe(fiber, (taken) => taken) + }) + ) + + readonly release = (n: number): Effect.Effect => this.updateTaken((taken) => taken - n) + + readonly releaseAll: Effect.Effect = this.updateTaken((_) => 0) + + readonly withPermits = (n: number) => (self: Effect.Effect) => + core.uninterruptibleMask((restore) => + core.flatMap( + restore(this.take(n)), + (permits) => fiberRuntime.ensuring(restore(self), this.release(permits)) + ) + ) + + readonly withPermitsIfAvailable = (n: number) => (self: Effect.Effect) => + core.uninterruptibleMask((restore) => + core.suspend(() => { + if (this.free < n) { + return effect.succeedNone + } + this.taken += n + return fiberRuntime.ensuring(restore(effect.asSome(self)), this.release(n)) + }) + ) +} + +/** @internal */ +export const unsafeMakeSemaphore = (permits: number): Effect.Semaphore => new Semaphore(permits) + +/** @internal */ +export const makeSemaphore = (permits: number) => core.sync(() => unsafeMakeSemaphore(permits)) + +class Latch extends Effectable.Class implements Effect.Latch { + waiters: Array<(_: Effect.Effect) => void> = [] + scheduled = false + constructor(private isOpen: boolean) { + super() + } + + commit() { + return this.await + } + + private unsafeSchedule(fiber: Fiber.RuntimeFiber) { + if (this.scheduled || this.waiters.length === 0) { + return core.void + } + this.scheduled = true + fiber.currentScheduler.scheduleTask(this.flushWaiters, fiber.getFiberRef(core.currentSchedulingPriority), fiber) + return core.void + } + private flushWaiters = () => { + this.scheduled = false + const waiters = this.waiters + this.waiters = [] + for (let i = 0; i < waiters.length; i++) { + waiters[i](core.exitVoid) + } + } + + open = core.withFiberRuntime((fiber) => { + if (this.isOpen) { + return core.void + } + this.isOpen = true + return this.unsafeSchedule(fiber) + }) + unsafeOpen() { + if (this.isOpen) return + this.isOpen = true + this.flushWaiters() + } + release = core.withFiberRuntime((fiber) => { + if (this.isOpen) { + return core.void + } + return this.unsafeSchedule(fiber) + }) + await = core.asyncInterrupt((resume) => { + if (this.isOpen) { + return resume(core.void) + } + this.waiters.push(resume) + return core.sync(() => { + const index = this.waiters.indexOf(resume) + if (index !== -1) { + this.waiters.splice(index, 1) + } + }) + }) + unsafeClose() { + this.isOpen = false + } + close = core.sync(() => { + this.isOpen = false + }) + whenOpen = (self: Effect.Effect): Effect.Effect => { + return core.zipRight(this.await, self) + } +} + +/** @internal */ +export const unsafeMakeLatch = (open?: boolean | undefined): Effect.Latch => new Latch(open ?? false) + +/** @internal */ +export const makeLatch = (open?: boolean | undefined) => core.sync(() => unsafeMakeLatch(open)) + +/** @internal */ +export const awaitAllChildren = (self: Effect.Effect): Effect.Effect => + ensuringChildren(self, fiberRuntime.fiberAwaitAll) + +/** @internal */ +export const cached: { + ( + timeToLive: Duration.DurationInput + ): (self: Effect.Effect) => Effect.Effect, never, R> + ( + self: Effect.Effect, + timeToLive: Duration.DurationInput + ): Effect.Effect, never, R> +} = dual( + 2, + ( + self: Effect.Effect, + timeToLive: Duration.DurationInput + ): Effect.Effect, never, R> => + core.map(cachedInvalidateWithTTL(self, timeToLive), (tuple) => tuple[0]) +) + +/** @internal */ +export const cachedInvalidateWithTTL: { + (timeToLive: Duration.DurationInput): ( + self: Effect.Effect + ) => Effect.Effect<[Effect.Effect, Effect.Effect], never, R> + ( + self: Effect.Effect, + timeToLive: Duration.DurationInput + ): Effect.Effect<[Effect.Effect, Effect.Effect], never, R> +} = dual( + 2, + ( + self: Effect.Effect, + timeToLive: Duration.DurationInput + ): Effect.Effect<[Effect.Effect, Effect.Effect], never, R> => { + const duration = Duration.decode(timeToLive) + return core.flatMap( + core.context(), + (env) => + core.map( + makeSynchronized]>>(Option.none()), + (cache) => + [ + core.provideContext(getCachedValue(self, duration, cache), env), + invalidateCache(cache) + ] as [Effect.Effect, Effect.Effect] + ) + ) + } +) + +/** @internal */ +const computeCachedValue = ( + self: Effect.Effect, + timeToLive: Duration.DurationInput, + start: number +): Effect.Effect]>, never, R> => { + const timeToLiveMillis = Duration.toMillis(Duration.decode(timeToLive)) + return pipe( + core.deferredMake(), + core.tap((deferred) => core.intoDeferred(self, deferred)), + core.map((deferred) => Option.some([start + timeToLiveMillis, deferred])) + ) +} + +/** @internal */ +const getCachedValue = ( + self: Effect.Effect, + timeToLive: Duration.DurationInput, + cache: Synchronized.SynchronizedRef]>> +): Effect.Effect => + core.uninterruptibleMask((restore) => + pipe( + effect.clockWith((clock) => clock.currentTimeMillis), + core.flatMap((time) => + updateSomeAndGetEffectSynchronized(cache, (option) => { + switch (option._tag) { + case "None": { + return Option.some(computeCachedValue(self, timeToLive, time)) + } + case "Some": { + const [end] = option.value + return end - time <= 0 + ? Option.some(computeCachedValue(self, timeToLive, time)) + : Option.none() + } + } + }) + ), + core.flatMap((option) => + Option.isNone(option) ? + core.dieMessage( + "BUG: Effect.cachedInvalidate - please report an issue at https://github.com/Effect-TS/effect/issues" + ) : + restore(core.deferredAwait(option.value[1])) + ) + ) + ) + +/** @internal */ +const invalidateCache = ( + cache: Synchronized.SynchronizedRef]>> +): Effect.Effect => internalRef.set(cache, Option.none()) + +/** @internal */ +export const ensuringChild = dual< + ( + f: (fiber: Fiber.Fiber, any>) => Effect.Effect + ) => ( + self: Effect.Effect + ) => Effect.Effect, + ( + self: Effect.Effect, + f: (fiber: Fiber.Fiber, any>) => Effect.Effect + ) => Effect.Effect +>(2, (self, f) => ensuringChildren(self, (children) => f(fiberRuntime.fiberAll(children)))) + +/** @internal */ +export const ensuringChildren = dual< + ( + children: (fibers: ReadonlyArray>) => Effect.Effect + ) => (self: Effect.Effect) => Effect.Effect, + ( + self: Effect.Effect, + children: (fibers: ReadonlyArray>) => Effect.Effect + ) => Effect.Effect +>(2, (self, children) => + core.flatMap(supervisor.track, (supervisor) => + pipe( + supervised(self, supervisor), + fiberRuntime.ensuring(core.flatMap(supervisor.value, children)) + ))) + +/** @internal */ +export const forkAll: { + ( + options?: { + readonly discard?: false | undefined + } + ): >( + effects: Iterable + ) => Effect.Effect< + Fiber.Fiber>, Effect.Effect.Error>, + never, + Effect.Effect.Context + > + (options: { + readonly discard: true + }): >( + effects: Iterable + ) => Effect.Effect> + >( + effects: Iterable, + options?: { + readonly discard?: false | undefined + } + ): Effect.Effect< + Fiber.Fiber>, Effect.Effect.Error>, + never, + Effect.Effect.Context + > + >(effects: Iterable, options: { + readonly discard: true + }): Effect.Effect> +} = dual((args) => Predicate.isIterable(args[0]), (effects: Iterable>, options: { + readonly discard: true +}): Effect.Effect => + options?.discard ? + core.forEachSequentialDiscard(effects, fiberRuntime.fork) : + core.map(core.forEachSequential(effects, fiberRuntime.fork), fiberRuntime.fiberAll)) + +/** @internal */ +export const forkIn = dual< + (scope: Scope.Scope) => (self: Effect.Effect) => Effect.Effect, never, R>, + (self: Effect.Effect, scope: Scope.Scope) => Effect.Effect, never, R> +>( + 2, + (self, scope) => + core.withFiberRuntime((parent, parentStatus) => { + const scopeImpl = scope as fiberRuntime.ScopeImpl + const fiber = fiberRuntime.unsafeFork(self, parent, parentStatus.runtimeFlags, globalScope) + if (scopeImpl.state._tag === "Open") { + const finalizer = () => + core.fiberIdWith((fiberId) => + Equal.equals(fiberId, fiber.id()) ? + core.void : + core.asVoid(core.interruptFiber(fiber)) + ) + const key = {} + scopeImpl.state.finalizers.set(key, finalizer) + fiber.addObserver(() => { + if (scopeImpl.state._tag === "Closed") return + scopeImpl.state.finalizers.delete(key) + }) + } else { + fiber.unsafeInterruptAsFork(parent.id()) + } + return core.succeed(fiber) + }) +) + +/** @internal */ +export const forkScoped = ( + self: Effect.Effect +): Effect.Effect, never, R | Scope.Scope> => + fiberRuntime.scopeWith((scope) => forkIn(self, scope)) + +/** @internal */ +export const fromFiber = (fiber: Fiber.Fiber): Effect.Effect => internalFiber.join(fiber) + +/** @internal */ +export const fromFiberEffect = (fiber: Effect.Effect, E, R>): Effect.Effect => + core.suspend(() => core.flatMap(fiber, internalFiber.join)) + +const memoKeySymbol = Symbol.for("effect/Effect/memoizeFunction.key") + +class Key implements Equal.Equal { + [memoKeySymbol] = memoKeySymbol + constructor(readonly a: A, readonly eq?: Equivalence) {} + [Equal.symbol](that: Equal.Equal) { + if (Predicate.hasProperty(that, memoKeySymbol)) { + if (this.eq) { + return this.eq(this.a, (that as unknown as Key).a) + } else { + return Equal.equals(this.a, (that as unknown as Key).a) + } + } + return false + } + [Hash.symbol]() { + return this.eq ? 0 : Hash.cached(this, Hash.hash(this.a)) + } +} + +/** @internal */ +export const cachedFunction = ( + f: (a: A) => Effect.Effect, + eq?: Equivalence +): Effect.Effect<(a: A) => Effect.Effect> => { + return pipe( + core.sync(() => MutableHashMap.empty, Deferred.Deferred>()), + core.flatMap(makeSynchronized), + core.map((ref) => (a: A) => + pipe( + ref.modifyEffect((map) => { + const result = pipe(map, MutableHashMap.get(new Key(a, eq))) + if (Option.isNone(result)) { + return pipe( + core.deferredMake(), + core.tap((deferred) => + pipe( + effect.diffFiberRefs(f(a)), + core.intoDeferred(deferred), + fiberRuntime.fork + ) + ), + core.map((deferred) => [deferred, pipe(map, MutableHashMap.set(new Key(a, eq), deferred))] as const) + ) + } + return core.succeed([result.value, map] as const) + }), + core.flatMap(core.deferredAwait), + core.flatMap(([patch, b]) => pipe(effect.patchFiberRefs(patch), core.as(b))) + ) + ) + ) +} + +/** @internal */ +export const raceFirst = dual< + ( + that: Effect.Effect + ) => ( + self: Effect.Effect + ) => Effect.Effect, + ( + self: Effect.Effect, + that: Effect.Effect + ) => Effect.Effect +>(2, ( + self: Effect.Effect, + that: Effect.Effect +) => + pipe( + core.exit(self), + fiberRuntime.race(core.exit(that)), + (effect: Effect.Effect, never, R | R2>) => core.flatten(effect) + )) + +/** @internal */ +export const supervised = dual< + (supervisor: Supervisor.Supervisor) => (self: Effect.Effect) => Effect.Effect, + (self: Effect.Effect, supervisor: Supervisor.Supervisor) => Effect.Effect +>(2, (self, supervisor) => { + const supervise = core.fiberRefLocallyWith(fiberRuntime.currentSupervisor, (s) => s.zip(supervisor)) + return supervise(self) +}) + +/** @internal */ +export const timeout = dual< + ( + duration: Duration.DurationInput + ) => (self: Effect.Effect) => Effect.Effect, + ( + self: Effect.Effect, + duration: Duration.DurationInput + ) => Effect.Effect +>(2, (self, duration) => + timeoutFail(self, { + onTimeout: () => core.timeoutExceptionFromDuration(duration), + duration + })) + +/** @internal */ +export const timeoutFail = dual< + ( + options: { + readonly onTimeout: LazyArg + readonly duration: Duration.DurationInput + } + ) => (self: Effect.Effect) => Effect.Effect, + ( + self: Effect.Effect, + options: { + readonly onTimeout: LazyArg + readonly duration: Duration.DurationInput + } + ) => Effect.Effect +>(2, (self, { duration, onTimeout }) => + core.flatten(timeoutTo(self, { + onTimeout: () => core.failSync(onTimeout), + onSuccess: core.succeed, + duration + }))) + +/** @internal */ +export const timeoutFailCause = dual< + ( + options: { + readonly onTimeout: LazyArg> + readonly duration: Duration.DurationInput + } + ) => (self: Effect.Effect) => Effect.Effect, + ( + self: Effect.Effect, + options: { + readonly onTimeout: LazyArg> + readonly duration: Duration.DurationInput + } + ) => Effect.Effect +>(2, (self, { duration, onTimeout }) => + core.flatten(timeoutTo(self, { + onTimeout: () => core.failCauseSync(onTimeout), + onSuccess: core.succeed, + duration + }))) + +/** @internal */ +export const timeoutOption = dual< + ( + duration: Duration.DurationInput + ) => (self: Effect.Effect) => Effect.Effect, E, R>, + ( + self: Effect.Effect, + duration: Duration.DurationInput + ) => Effect.Effect, E, R> +>(2, (self, duration) => + timeoutTo(self, { + duration, + onSuccess: Option.some, + onTimeout: Option.none + })) + +/** @internal */ +export const timeoutTo = dual< + ( + options: { + readonly onTimeout: LazyArg + readonly onSuccess: (a: A) => B + readonly duration: Duration.DurationInput + } + ) => (self: Effect.Effect) => Effect.Effect, + ( + self: Effect.Effect, + options: { + readonly onTimeout: LazyArg + readonly onSuccess: (a: A) => B + readonly duration: Duration.DurationInput + } + ) => Effect.Effect +>( + 2, + (self, { duration, onSuccess, onTimeout }) => + core.fiberIdWith((parentFiberId) => + core.uninterruptibleMask((restore) => + fiberRuntime.raceFibersWith( + restore(self), + core.interruptible(effect.sleep(duration)), + { + onSelfWin: (winner, loser) => + core.flatMap( + winner.await, + (exit) => { + if (exit._tag === "Success") { + return core.flatMap( + winner.inheritAll, + () => + core.as( + core.interruptAsFiber(loser, parentFiberId), + onSuccess(exit.value) + ) + ) + } else { + return core.flatMap( + core.interruptAsFiber(loser, parentFiberId), + () => core.exitFailCause(exit.cause) + ) + } + } + ), + onOtherWin: (winner, loser) => + core.flatMap( + winner.await, + (exit) => { + if (exit._tag === "Success") { + return core.flatMap( + winner.inheritAll, + () => + core.as( + core.interruptAsFiber(loser, parentFiberId), + onTimeout() + ) + ) + } else { + return core.flatMap( + core.interruptAsFiber(loser, parentFiberId), + () => core.exitFailCause(exit.cause) + ) + } + } + ), + otherScope: globalScope + } + ) + ) + ) +) + +// circular with Synchronized + +/** @internal */ +const SynchronizedSymbolKey = "effect/Ref/SynchronizedRef" + +/** @internal */ +export const SynchronizedTypeId: Synchronized.SynchronizedRefTypeId = Symbol.for( + SynchronizedSymbolKey +) as Synchronized.SynchronizedRefTypeId + +/** @internal */ +export const synchronizedVariance = { + /* c8 ignore next */ + _A: (_: any) => _ +} + +/** @internal */ +class SynchronizedImpl extends Effectable.Class implements Synchronized.SynchronizedRef { + readonly [SynchronizedTypeId] = synchronizedVariance + readonly [internalRef.RefTypeId] = internalRef.refVariance + readonly [Readable.TypeId]: Readable.TypeId = Readable.TypeId + constructor( + readonly ref: Ref.Ref, + readonly withLock: (self: Effect.Effect) => Effect.Effect + ) { + super() + this.get = internalRef.get(this.ref) + } + readonly get: Effect.Effect + commit() { + return this.get + } + modify(f: (a: A) => readonly [B, A]): Effect.Effect { + return this.modifyEffect((a) => core.succeed(f(a))) + } + modifyEffect(f: (a: A) => Effect.Effect): Effect.Effect { + return this.withLock( + pipe( + core.flatMap(internalRef.get(this.ref), f), + core.flatMap(([b, a]) => core.as(internalRef.set(this.ref, a), b)) + ) + ) + } +} + +/** @internal */ +export const makeSynchronized = (value: A): Effect.Effect> => + core.sync(() => unsafeMakeSynchronized(value)) + +/** @internal */ +export const unsafeMakeSynchronized = (value: A): Synchronized.SynchronizedRef => { + const ref = internalRef.unsafeMake(value) + const sem = unsafeMakeSemaphore(1) + return new SynchronizedImpl(ref, sem.withPermits(1)) +} + +/** @internal */ +export const updateSomeAndGetEffectSynchronized = dual< + ( + pf: (a: A) => Option.Option> + ) => (self: Synchronized.SynchronizedRef) => Effect.Effect, + ( + self: Synchronized.SynchronizedRef, + pf: (a: A) => Option.Option> + ) => Effect.Effect +>(2, (self, pf) => + self.modifyEffect((value) => { + const result = pf(value) + switch (result._tag) { + case "None": { + return core.succeed([value, value] as const) + } + case "Some": { + return core.map(result.value, (a) => [a, a] as const) + } + } + })) + +// circular with Fiber + +/** @internal */ +export const zipFiber = dual< + (that: Fiber.Fiber) => (self: Fiber.Fiber) => Fiber.Fiber<[A, A2], E | E2>, + (self: Fiber.Fiber, that: Fiber.Fiber) => Fiber.Fiber<[A, A2], E | E2> +>(2, (self, that) => zipWithFiber(self, that, (a, b) => [a, b])) + +/** @internal */ +export const zipLeftFiber = dual< + (that: Fiber.Fiber) => (self: Fiber.Fiber) => Fiber.Fiber, + (self: Fiber.Fiber, that: Fiber.Fiber) => Fiber.Fiber +>(2, (self, that) => zipWithFiber(self, that, (a, _) => a)) + +/** @internal */ +export const zipRightFiber = dual< + (that: Fiber.Fiber) => (self: Fiber.Fiber) => Fiber.Fiber, + (self: Fiber.Fiber, that: Fiber.Fiber) => Fiber.Fiber +>(2, (self, that) => zipWithFiber(self, that, (_, b) => b)) + +/** @internal */ +export const zipWithFiber = dual< + ( + that: Fiber.Fiber, + f: (a: A, b: B) => C + ) => (self: Fiber.Fiber) => Fiber.Fiber, + ( + self: Fiber.Fiber, + that: Fiber.Fiber, + f: (a: A, b: B) => C + ) => Fiber.Fiber +>(3, (self, that, f) => ({ + ...Effectable.CommitPrototype, + commit() { + return internalFiber.join(this) + }, + [internalFiber.FiberTypeId]: internalFiber.fiberVariance, + id: () => pipe(self.id(), FiberId.getOrElse(that.id())), + await: pipe( + self.await, + core.flatten, + fiberRuntime.zipWithOptions(core.flatten(that.await), f, { concurrent: true }), + core.exit + ), + children: self.children, + inheritAll: core.zipRight( + that.inheritAll, + self.inheritAll + ), + poll: core.zipWith( + self.poll, + that.poll, + (optionA, optionB) => + pipe( + optionA, + Option.flatMap((exitA) => + pipe( + optionB, + Option.map((exitB) => + Exit.zipWith(exitA, exitB, { + onSuccess: f, + onFailure: internalCause.parallel + }) + ) + ) + ) + ) + ), + interruptAsFork: (id) => + core.zipRight( + self.interruptAsFork(id), + that.interruptAsFork(id) + ), + pipe() { + return pipeArguments(this, arguments) + } +})) + +/* @internal */ +export const bindAll: { + < + A extends object, + X extends Record>, + O extends Types.NoExcessProperties<{ + readonly concurrency?: Types.Concurrency | undefined + readonly batching?: boolean | "inherit" | undefined + readonly mode?: "default" | "validate" | "either" | undefined + readonly concurrentFinalizers?: boolean | undefined + }, O> + >( + f: (a: A) => [Extract] extends [never] ? X : `Duplicate keys`, + options?: undefined | O + ): ( + self: Effect.Effect + ) => [Effect.All.ReturnObject>] extends + [Effect.Effect] ? Effect.Effect< + { + [K in keyof A | keyof Success]: K extends keyof A ? A[K] + : K extends keyof Success ? Success[K] + : never + }, + | E1 + | Error, + R1 | Context + > + : never + + < + A extends object, + X extends Record>, + O extends Types.NoExcessProperties<{ + readonly concurrency?: Types.Concurrency | undefined + readonly batching?: boolean | "inherit" | undefined + readonly mode?: "default" | "validate" | "either" | undefined + readonly concurrentFinalizers?: boolean | undefined + }, O>, + E1, + R1 + >( + self: Effect.Effect, + f: (a: A) => [Extract] extends [never] ? X : `Duplicate keys`, + options?: undefined | { + readonly concurrency?: Types.Concurrency | undefined + readonly batching?: boolean | "inherit" | undefined + readonly mode?: "default" | "validate" | "either" | undefined + readonly concurrentFinalizers?: boolean | undefined + } + ): [Effect.All.ReturnObject>] extends + [Effect.Effect] ? Effect.Effect< + { + [K in keyof A | keyof Success]: K extends keyof A ? A[K] + : K extends keyof Success ? Success[K] + : never + }, + | E1 + | Error, + R1 | Context + > + : never +} = dual((args) => core.isEffect(args[0]), < + A extends object, + X extends Record>, + O extends Types.NoExcessProperties<{ + readonly concurrency?: Types.Concurrency | undefined + readonly batching?: boolean | "inherit" | undefined + readonly mode?: "default" | "validate" | "either" | undefined + readonly concurrentFinalizers?: boolean | undefined + }, O>, + E1, + R1 +>( + self: Effect.Effect, + f: (a: A) => X, + options?: undefined | O +) => + core.flatMap( + self, + (a) => + (fiberRuntime.all(f(a), options) as Effect.All.ReturnObject< + X, + Effect.All.IsDiscard, + Effect.All.ExtractMode + >) + .pipe( + core.map((record) => Object.assign({}, a, record)) + ) + )) diff --git a/repos/effect/packages/effect/src/internal/effectable.ts b/repos/effect/packages/effect/src/internal/effectable.ts new file mode 100644 index 0000000..b5c9f22 --- /dev/null +++ b/repos/effect/packages/effect/src/internal/effectable.ts @@ -0,0 +1,131 @@ +import type * as Channel from "../Channel.js" +import type * as Effect from "../Effect.js" +import type * as Effectable from "../Effectable.js" +import * as Equal from "../Equal.js" +import * as Hash from "../Hash.js" +import { pipeArguments } from "../Pipeable.js" +import type * as Sink from "../Sink.js" +import type * as Stream from "../Stream.js" +import { SingleShotGen, YieldWrap } from "../Utils.js" +import * as OpCodes from "./opCodes/effect.js" +import * as version from "./version.js" + +/** @internal */ +export const EffectTypeId: Effect.EffectTypeId = Symbol.for("effect/Effect") as Effect.EffectTypeId + +/** @internal */ +export const StreamTypeId: Stream.StreamTypeId = Symbol.for("effect/Stream") as Stream.StreamTypeId + +/** @internal */ +export const SinkTypeId: Sink.SinkTypeId = Symbol.for("effect/Sink") as Sink.SinkTypeId + +/** @internal */ +export const ChannelTypeId: Channel.ChannelTypeId = Symbol.for("effect/Channel") as Channel.ChannelTypeId + +/** @internal */ +export const effectVariance = { + /* c8 ignore next */ + _R: (_: never) => _, + /* c8 ignore next */ + _E: (_: never) => _, + /* c8 ignore next */ + _A: (_: never) => _, + + _V: version.getCurrentVersion() +} + +const sinkVariance = { + /* c8 ignore next */ + _A: (_: never) => _, + /* c8 ignore next */ + _In: (_: unknown) => _, + /* c8 ignore next */ + _L: (_: never) => _, + /* c8 ignore next */ + _E: (_: never) => _, + /* c8 ignore next */ + _R: (_: never) => _ +} + +const channelVariance = { + /* c8 ignore next */ + _Env: (_: never) => _, + /* c8 ignore next */ + _InErr: (_: unknown) => _, + /* c8 ignore next */ + _InElem: (_: unknown) => _, + /* c8 ignore next */ + _InDone: (_: unknown) => _, + /* c8 ignore next */ + _OutErr: (_: never) => _, + /* c8 ignore next */ + _OutElem: (_: never) => _, + /* c8 ignore next */ + _OutDone: (_: never) => _ +} + +/** @internal */ +export const EffectPrototype: Effect.Effect & Equal.Equal = { + [EffectTypeId]: effectVariance, + [StreamTypeId]: effectVariance, + [SinkTypeId]: sinkVariance, + [ChannelTypeId]: channelVariance, + [Equal.symbol](that: any) { + return this === that + }, + [Hash.symbol]() { + return Hash.cached(this, Hash.random(this)) + }, + [Symbol.iterator]() { + return new SingleShotGen(new YieldWrap(this)) as any + }, + pipe() { + return pipeArguments(this, arguments) + } +} + +/** @internal */ +export const StructuralPrototype: Equal.Equal = { + [Hash.symbol]() { + return Hash.cached(this, Hash.structure(this)) + }, + [Equal.symbol](this: Equal.Equal, that: Equal.Equal) { + const selfKeys = Object.keys(this) + const thatKeys = Object.keys(that as object) + if (selfKeys.length !== thatKeys.length) { + return false + } + for (const key of selfKeys) { + if (!(key in (that as object) && Equal.equals((this as any)[key], (that as any)[key]))) { + return false + } + } + return true + } +} + +/** @internal */ +export const CommitPrototype: Effect.Effect = { + ...EffectPrototype, + _op: OpCodes.OP_COMMIT +} as any + +/** @internal */ +export const StructuralCommitPrototype: Effect.Effect = { + ...CommitPrototype, + ...StructuralPrototype +} as any + +/** @internal */ +export const Base: Effectable.CommitPrimitive = (function() { + function Base() {} + Base.prototype = CommitPrototype + return Base as any +})() + +/** @internal */ +export const StructuralBase: Effectable.CommitPrimitive = (function() { + function Base() {} + Base.prototype = StructuralCommitPrototype + return Base as any +})() diff --git a/repos/effect/packages/effect/src/internal/either.ts b/repos/effect/packages/effect/src/internal/either.ts new file mode 100644 index 0000000..6ef2f5f --- /dev/null +++ b/repos/effect/packages/effect/src/internal/either.ts @@ -0,0 +1,110 @@ +/** + * @since 2.0.0 + */ + +import type * as Either from "../Either.js" +import * as Equal from "../Equal.js" +import { dual } from "../Function.js" +import * as Hash from "../Hash.js" +import { format, NodeInspectSymbol, toJSON } from "../Inspectable.js" +import type { Option } from "../Option.js" +import { hasProperty } from "../Predicate.js" +import { EffectPrototype } from "./effectable.js" +import * as option from "./option.js" + +/** + * @internal + */ +export const TypeId: Either.TypeId = Symbol.for("effect/Either") as Either.TypeId + +const CommonProto = { + ...EffectPrototype, + [TypeId]: { + _R: (_: never) => _ + }, + [NodeInspectSymbol](this: Either.Either) { + return this.toJSON() + }, + toString(this: Either.Left) { + return format(this.toJSON()) + } +} + +const RightProto = Object.assign(Object.create(CommonProto), { + _tag: "Right", + _op: "Right", + [Equal.symbol](this: Either.Right, that: unknown): boolean { + return isEither(that) && isRight(that) && Equal.equals(this.right, that.right) + }, + [Hash.symbol](this: Either.Right) { + return Hash.combine(Hash.hash(this._tag))(Hash.hash(this.right)) + }, + toJSON(this: Either.Right) { + return { + _id: "Either", + _tag: this._tag, + right: toJSON(this.right) + } + } +}) + +const LeftProto = Object.assign(Object.create(CommonProto), { + _tag: "Left", + _op: "Left", + [Equal.symbol](this: Either.Left, that: unknown): boolean { + return isEither(that) && isLeft(that) && Equal.equals(this.left, that.left) + }, + [Hash.symbol](this: Either.Left) { + return Hash.combine(Hash.hash(this._tag))(Hash.hash(this.left)) + }, + toJSON(this: Either.Left) { + return { + _id: "Either", + _tag: this._tag, + left: toJSON(this.left) + } + } +}) + +/** @internal */ +export const isEither = (input: unknown): input is Either.Either => hasProperty(input, TypeId) + +/** @internal */ +export const isLeft = (ma: Either.Either): ma is Either.Left => ma._tag === "Left" + +/** @internal */ +export const isRight = (ma: Either.Either): ma is Either.Right => ma._tag === "Right" + +/** @internal */ +export const left = (left: L): Either.Either => { + const a = Object.create(LeftProto) + a.left = left + return a +} + +/** @internal */ +export const right = (right: R): Either.Either => { + const a = Object.create(RightProto) + a.right = right + return a +} + +/** @internal */ +export const getLeft = ( + self: Either.Either +): Option => (isRight(self) ? option.none : option.some(self.left)) + +/** @internal */ +export const getRight = ( + self: Either.Either +): Option => (isLeft(self) ? option.none : option.some(self.right)) + +/** @internal */ +export const fromOption: { + (onNone: () => L): (self: Option) => Either.Either + (self: Option, onNone: () => L): Either.Either +} = dual( + 2, + (self: Option, onNone: () => L): Either.Either => + option.isNone(self) ? left(onNone()) : right(self.value) +) diff --git a/repos/effect/packages/effect/src/internal/encoding/base64.ts b/repos/effect/packages/effect/src/internal/encoding/base64.ts new file mode 100644 index 0000000..e23d0f6 --- /dev/null +++ b/repos/effect/packages/effect/src/internal/encoding/base64.ts @@ -0,0 +1,286 @@ +import * as Either from "../../Either.js" +import type * as Encoding from "../../Encoding.js" +import { DecodeException } from "./common.js" + +/** @internal */ +export const encode = (bytes: Uint8Array) => { + const length = bytes.length + + let result = "" + let i: number + + for (i = 2; i < length; i += 3) { + result += base64abc[bytes[i - 2] >> 2] + result += base64abc[((bytes[i - 2] & 0x03) << 4) | (bytes[i - 1] >> 4)] + result += base64abc[((bytes[i - 1] & 0x0f) << 2) | (bytes[i] >> 6)] + result += base64abc[bytes[i] & 0x3f] + } + + if (i === length + 1) { + // 1 octet yet to write + result += base64abc[bytes[i - 2] >> 2] + result += base64abc[(bytes[i - 2] & 0x03) << 4] + result += "==" + } + + if (i === length) { + // 2 octets yet to write + result += base64abc[bytes[i - 2] >> 2] + result += base64abc[((bytes[i - 2] & 0x03) << 4) | (bytes[i - 1] >> 4)] + result += base64abc[(bytes[i - 1] & 0x0f) << 2] + result += "=" + } + + return result +} + +/** @internal */ +export const decode = (str: string): Either.Either => { + const stripped = stripCrlf(str) + const length = stripped.length + if (length % 4 !== 0) { + return Either.left( + DecodeException(stripped, `Length must be a multiple of 4, but is ${length}`) + ) + } + + const index = stripped.indexOf("=") + if (index !== -1 && ((index < length - 2) || (index === length - 2 && stripped[length - 1] !== "="))) { + return Either.left( + DecodeException(stripped, "Found a '=' character, but it is not at the end") + ) + } + + try { + const missingOctets = stripped.endsWith("==") ? 2 : stripped.endsWith("=") ? 1 : 0 + const result = new Uint8Array(3 * (length / 4) - missingOctets) + for (let i = 0, j = 0; i < length; i += 4, j += 3) { + const buffer = getBase64Code(stripped.charCodeAt(i)) << 18 | + getBase64Code(stripped.charCodeAt(i + 1)) << 12 | + getBase64Code(stripped.charCodeAt(i + 2)) << 6 | + getBase64Code(stripped.charCodeAt(i + 3)) + + result[j] = buffer >> 16 + result[j + 1] = (buffer >> 8) & 0xff + result[j + 2] = buffer & 0xff + } + + return Either.right(result) + } catch (e) { + return Either.left( + DecodeException(stripped, e instanceof Error ? e.message : "Invalid input") + ) + } +} + +/** @internal */ +export const stripCrlf = (str: string) => str.replace(/[\n\r]/g, "") + +/** @internal */ +function getBase64Code(charCode: number) { + if (charCode >= base64codes.length) { + throw new TypeError(`Invalid character ${String.fromCharCode(charCode)}`) + } + + const code = base64codes[charCode] + if (code === 255) { + throw new TypeError(`Invalid character ${String.fromCharCode(charCode)}`) + } + + return code +} + +/** @internal */ +const base64abc = [ + "A", + "B", + "C", + "D", + "E", + "F", + "G", + "H", + "I", + "J", + "K", + "L", + "M", + "N", + "O", + "P", + "Q", + "R", + "S", + "T", + "U", + "V", + "W", + "X", + "Y", + "Z", + "a", + "b", + "c", + "d", + "e", + "f", + "g", + "h", + "i", + "j", + "k", + "l", + "m", + "n", + "o", + "p", + "q", + "r", + "s", + "t", + "u", + "v", + "w", + "x", + "y", + "z", + "0", + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "+", + "/" +] + +/** @internal */ +const base64codes = [ + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 62, + 255, + 255, + 255, + 63, + 52, + 53, + 54, + 55, + 56, + 57, + 58, + 59, + 60, + 61, + 255, + 255, + 255, + 0, + 255, + 255, + 255, + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 255, + 255, + 255, + 255, + 255, + 255, + 26, + 27, + 28, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37, + 38, + 39, + 40, + 41, + 42, + 43, + 44, + 45, + 46, + 47, + 48, + 49, + 50, + 51 +] diff --git a/repos/effect/packages/effect/src/internal/encoding/base64Url.ts b/repos/effect/packages/effect/src/internal/encoding/base64Url.ts new file mode 100644 index 0000000..8906af2 --- /dev/null +++ b/repos/effect/packages/effect/src/internal/encoding/base64Url.ts @@ -0,0 +1,29 @@ +import * as Either from "../../Either.js" +import type * as Encoding from "../../Encoding.js" +import * as Base64 from "./base64.js" +import { DecodeException } from "./common.js" + +/** @internal */ +export const encode = (data: Uint8Array) => + Base64.encode(data).replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_") + +/** @internal */ +export const decode = (str: string): Either.Either => { + const stripped = Base64.stripCrlf(str) + const length = stripped.length + if (length % 4 === 1) { + return Either.left( + DecodeException(stripped, `Length should be a multiple of 4, but is ${length}`) + ) + } + + if (!/^[-_A-Z0-9]*?={0,2}$/i.test(stripped)) { + return Either.left(DecodeException(stripped, "Invalid input")) + } + + // Some variants allow or require omitting the padding '=' signs + let sanitized = length % 4 === 2 ? `${stripped}==` : length % 4 === 3 ? `${stripped}=` : stripped + sanitized = sanitized.replace(/-/g, "+").replace(/_/g, "/") + + return Base64.decode(sanitized) +} diff --git a/repos/effect/packages/effect/src/internal/encoding/common.ts b/repos/effect/packages/effect/src/internal/encoding/common.ts new file mode 100644 index 0000000..580cd6f --- /dev/null +++ b/repos/effect/packages/effect/src/internal/encoding/common.ts @@ -0,0 +1,51 @@ +import type * as Encoding from "../../Encoding.js" +import { hasProperty, isString } from "../../Predicate.js" +import type { Mutable } from "../../Types.js" + +/** @internal */ +export const DecodeExceptionTypeId: Encoding.DecodeExceptionTypeId = Symbol.for( + "effect/Encoding/errors/Decode" +) as Encoding.DecodeExceptionTypeId + +/** @internal */ +export const DecodeException = (input: string, message?: string): Encoding.DecodeException => { + const out: Mutable = { + _tag: "DecodeException", + [DecodeExceptionTypeId]: DecodeExceptionTypeId, + input + } + if (isString(message)) { + out.message = message + } + return out +} + +/** @internal */ +export const isDecodeException = (u: unknown): u is Encoding.DecodeException => hasProperty(u, DecodeExceptionTypeId) + +/** @internal */ +export const EncodeExceptionTypeId: Encoding.EncodeExceptionTypeId = Symbol.for( + "effect/Encoding/errors/Encode" +) as Encoding.EncodeExceptionTypeId + +/** @internal */ +export const EncodeException = (input: string, message?: string): Encoding.EncodeException => { + const out: Mutable = { + _tag: "EncodeException", + [EncodeExceptionTypeId]: EncodeExceptionTypeId, + input + } + if (isString(message)) { + out.message = message + } + return out +} + +/** @internal */ +export const isEncodeException = (u: unknown): u is Encoding.EncodeException => hasProperty(u, EncodeExceptionTypeId) + +/** @interal */ +export const encoder = new TextEncoder() + +/** @interal */ +export const decoder = new TextDecoder() diff --git a/repos/effect/packages/effect/src/internal/encoding/hex.ts b/repos/effect/packages/effect/src/internal/encoding/hex.ts new file mode 100644 index 0000000..a46d712 --- /dev/null +++ b/repos/effect/packages/effect/src/internal/encoding/hex.ts @@ -0,0 +1,315 @@ +import * as Either from "../../Either.js" +import type * as Encoding from "../../Encoding.js" +import { DecodeException } from "./common.js" + +/** @internal */ +export const encode = (bytes: Uint8Array) => { + let result = "" + for (let i = 0; i < bytes.length; ++i) { + result += bytesToHex[bytes[i]] + } + + return result +} + +/** @internal */ +export const decode = (str: string): Either.Either => { + const bytes = new TextEncoder().encode(str) + if (bytes.length % 2 !== 0) { + return Either.left(DecodeException(str, `Length must be a multiple of 2, but is ${bytes.length}`)) + } + + try { + const length = bytes.length / 2 + const result = new Uint8Array(length) + for (let i = 0; i < length; i++) { + const a = fromHexChar(bytes[i * 2]) + const b = fromHexChar(bytes[i * 2 + 1]) + result[i] = (a << 4) | b + } + + return Either.right(result) + } catch (e) { + return Either.left(DecodeException(str, e instanceof Error ? e.message : "Invalid input")) + } +} + +/** @internal */ +const bytesToHex = [ + "00", + "01", + "02", + "03", + "04", + "05", + "06", + "07", + "08", + "09", + "0a", + "0b", + "0c", + "0d", + "0e", + "0f", + "10", + "11", + "12", + "13", + "14", + "15", + "16", + "17", + "18", + "19", + "1a", + "1b", + "1c", + "1d", + "1e", + "1f", + "20", + "21", + "22", + "23", + "24", + "25", + "26", + "27", + "28", + "29", + "2a", + "2b", + "2c", + "2d", + "2e", + "2f", + "30", + "31", + "32", + "33", + "34", + "35", + "36", + "37", + "38", + "39", + "3a", + "3b", + "3c", + "3d", + "3e", + "3f", + "40", + "41", + "42", + "43", + "44", + "45", + "46", + "47", + "48", + "49", + "4a", + "4b", + "4c", + "4d", + "4e", + "4f", + "50", + "51", + "52", + "53", + "54", + "55", + "56", + "57", + "58", + "59", + "5a", + "5b", + "5c", + "5d", + "5e", + "5f", + "60", + "61", + "62", + "63", + "64", + "65", + "66", + "67", + "68", + "69", + "6a", + "6b", + "6c", + "6d", + "6e", + "6f", + "70", + "71", + "72", + "73", + "74", + "75", + "76", + "77", + "78", + "79", + "7a", + "7b", + "7c", + "7d", + "7e", + "7f", + "80", + "81", + "82", + "83", + "84", + "85", + "86", + "87", + "88", + "89", + "8a", + "8b", + "8c", + "8d", + "8e", + "8f", + "90", + "91", + "92", + "93", + "94", + "95", + "96", + "97", + "98", + "99", + "9a", + "9b", + "9c", + "9d", + "9e", + "9f", + "a0", + "a1", + "a2", + "a3", + "a4", + "a5", + "a6", + "a7", + "a8", + "a9", + "aa", + "ab", + "ac", + "ad", + "ae", + "af", + "b0", + "b1", + "b2", + "b3", + "b4", + "b5", + "b6", + "b7", + "b8", + "b9", + "ba", + "bb", + "bc", + "bd", + "be", + "bf", + "c0", + "c1", + "c2", + "c3", + "c4", + "c5", + "c6", + "c7", + "c8", + "c9", + "ca", + "cb", + "cc", + "cd", + "ce", + "cf", + "d0", + "d1", + "d2", + "d3", + "d4", + "d5", + "d6", + "d7", + "d8", + "d9", + "da", + "db", + "dc", + "dd", + "de", + "df", + "e0", + "e1", + "e2", + "e3", + "e4", + "e5", + "e6", + "e7", + "e8", + "e9", + "ea", + "eb", + "ec", + "ed", + "ee", + "ef", + "f0", + "f1", + "f2", + "f3", + "f4", + "f5", + "f6", + "f7", + "f8", + "f9", + "fa", + "fb", + "fc", + "fd", + "fe", + "ff" +] + +/** @internal */ +const fromHexChar = (byte: number) => { + // '0' <= byte && byte <= '9' + if (48 <= byte && byte <= 57) { + return byte - 48 + } + + // 'a' <= byte && byte <= 'f' + if (97 <= byte && byte <= 102) { + return byte - 97 + 10 + } + + // 'A' <= byte && byte <= 'F' + if (65 <= byte && byte <= 70) { + return byte - 65 + 10 + } + + throw new TypeError("Invalid input") +} diff --git a/repos/effect/packages/effect/src/internal/errors.ts b/repos/effect/packages/effect/src/internal/errors.ts new file mode 100644 index 0000000..b471e55 --- /dev/null +++ b/repos/effect/packages/effect/src/internal/errors.ts @@ -0,0 +1,7 @@ +/** + * @since 2.0.0 + */ + +/** @internal */ +export const getBugErrorMessage = (message: string) => + `BUG: ${message} - please report an issue at https://github.com/Effect-TS/effect/issues` diff --git a/repos/effect/packages/effect/src/internal/executionPlan.ts b/repos/effect/packages/effect/src/internal/executionPlan.ts new file mode 100644 index 0000000..3f625ba --- /dev/null +++ b/repos/effect/packages/effect/src/internal/executionPlan.ts @@ -0,0 +1,114 @@ +import type { Effect } from "../Effect.js" +import * as Either from "../Either.js" +import type * as Api from "../ExecutionPlan.js" +import { dual } from "../Function.js" +import * as Predicate from "../Predicate.js" +import * as core from "./core.js" +import * as layer from "./layer.js" +import * as InternalSchedule from "./schedule.js" + +/** @internal */ +export const TypeId: Api.TypeId = Symbol.for("effect/ExecutionPlan") as Api.TypeId + +/** @internal */ +export const isExecutionPlan = (u: unknown): u is Api.ExecutionPlan => Predicate.hasProperty(u, TypeId) + +/** @internal */ +export const withExecutionPlan: { + ( + plan: Api.ExecutionPlan<{ + provides: Provides + input: Input + error: PlanE + requirements: PlanR + }> + ): (effect: Effect) => Effect< + A, + E | PlanE, + Exclude | PlanR + > + ( + effect: Effect, + plan: Api.ExecutionPlan<{ + provides: Provides + input: Input + error: PlanE + requirements: PlanR + }> + ): Effect< + A, + E | PlanE, + Exclude | PlanR + > +} = dual(2, ( + effect: Effect, + plan: Api.ExecutionPlan<{ + provides: Provides + input: Input + error: PlanE + requirements: PlanR + }> +) => + core.suspend(() => { + let i = 0 + let result: Either.Either | undefined + return core.flatMap( + core.whileLoop({ + while: () => i < plan.steps.length && (result === undefined || Either.isLeft(result)), + body: () => { + const step = plan.steps[i] + let nextEffect: Effect = layer.effect_provide(effect, step.provide as any) + if (result) { + let attempted = false + const wrapped = nextEffect + // ensure the schedule is applied at least once + nextEffect = core.suspend(() => { + if (attempted) return wrapped + attempted = true + return result! + }) + nextEffect = InternalSchedule.scheduleDefectRefail( + InternalSchedule.retry_Effect(nextEffect, scheduleFromStep(step, false)!) + ) + } else { + const schedule = scheduleFromStep(step, true) + nextEffect = schedule + ? InternalSchedule.scheduleDefectRefail(InternalSchedule.retry_Effect(nextEffect, schedule)) + : nextEffect + } + return core.either(nextEffect) + }, + step: (either) => { + result = either + i++ + } + }), + () => result! + ) + })) + +/** @internal */ +export const scheduleFromStep = ( + step: Api.ExecutionPlan<{ + provides: Provides + input: In + error: PlanE + requirements: PlanR + }>["steps"][number], + first: boolean +) => { + if (!first) { + return InternalSchedule.fromRetryOptions({ + schedule: step.schedule ? step.schedule : step.attempts ? undefined : InternalSchedule.once, + times: step.attempts, + while: step.while + }) + } else if (step.attempts === 1 || !(step.schedule || step.attempts)) { + return undefined + } + return InternalSchedule.fromRetryOptions({ + schedule: step.schedule, + while: step.while, + times: step.attempts ? step.attempts - 1 : undefined + }) +} diff --git a/repos/effect/packages/effect/src/internal/executionStrategy.ts b/repos/effect/packages/effect/src/internal/executionStrategy.ts new file mode 100644 index 0000000..426a0d9 --- /dev/null +++ b/repos/effect/packages/effect/src/internal/executionStrategy.ts @@ -0,0 +1,74 @@ +import type * as ExecutionStrategy from "../ExecutionStrategy.js" +import { dual } from "../Function.js" +import type { LazyArg } from "../Function.js" + +/** @internal */ +export const OP_SEQUENTIAL = "Sequential" as const + +/** @internal */ +export type OP_SEQUENTIAL = typeof OP_SEQUENTIAL + +/** @internal */ +export const OP_PARALLEL = "Parallel" as const + +/** @internal */ +export type OP_PARALLEL = typeof OP_PARALLEL + +/** @internal */ +export const OP_PARALLEL_N = "ParallelN" as const + +/** @internal */ +export type OP_PARALLEL_N = typeof OP_PARALLEL_N + +/** @internal */ +export const sequential: ExecutionStrategy.ExecutionStrategy = { _tag: OP_SEQUENTIAL } + +/** @internal */ +export const parallel: ExecutionStrategy.ExecutionStrategy = { _tag: OP_PARALLEL } + +/** @internal */ +export const parallelN = (parallelism: number): ExecutionStrategy.ExecutionStrategy => ({ + _tag: OP_PARALLEL_N, + parallelism +}) + +/** @internal */ +export const isSequential = (self: ExecutionStrategy.ExecutionStrategy): self is ExecutionStrategy.Sequential => + self._tag === OP_SEQUENTIAL + +/** @internal */ +export const isParallel = (self: ExecutionStrategy.ExecutionStrategy): self is ExecutionStrategy.Parallel => + self._tag === OP_PARALLEL + +/** @internal */ +export const isParallelN = (self: ExecutionStrategy.ExecutionStrategy): self is ExecutionStrategy.ParallelN => + self._tag === OP_PARALLEL_N + +/** @internal */ +export const match = dual< + (options: { + readonly onSequential: LazyArg + readonly onParallel: LazyArg + readonly onParallelN: (n: number) => A + }) => (self: ExecutionStrategy.ExecutionStrategy) => A, + ( + self: ExecutionStrategy.ExecutionStrategy, + options: { + readonly onSequential: LazyArg + readonly onParallel: LazyArg + readonly onParallelN: (n: number) => A + } + ) => A +>(2, (self, options) => { + switch (self._tag) { + case OP_SEQUENTIAL: { + return options.onSequential() + } + case OP_PARALLEL: { + return options.onParallel() + } + case OP_PARALLEL_N: { + return options.onParallelN(self.parallelism) + } + } +}) diff --git a/repos/effect/packages/effect/src/internal/fiber.ts b/repos/effect/packages/effect/src/internal/fiber.ts new file mode 100644 index 0000000..c5a71b1 --- /dev/null +++ b/repos/effect/packages/effect/src/internal/fiber.ts @@ -0,0 +1,388 @@ +import type * as Cause from "../Cause.js" +import * as Clock from "../Clock.js" +import type * as Effect from "../Effect.js" +import * as Either from "../Either.js" +import * as Exit from "../Exit.js" +import type * as Fiber from "../Fiber.js" +import * as FiberId from "../FiberId.js" +import * as FiberStatus from "../FiberStatus.js" +import { dual, pipe } from "../Function.js" +import * as HashSet from "../HashSet.js" +import * as number from "../Number.js" +import * as Option from "../Option.js" +import * as order from "../Order.js" +import { pipeArguments } from "../Pipeable.js" +import { hasProperty } from "../Predicate.js" +import * as core from "./core.js" +import * as effectable from "./effectable.js" +import * as fiberScope from "./fiberScope.js" +import * as runtimeFlags from "./runtimeFlags.js" + +/** @internal */ +const FiberSymbolKey = "effect/Fiber" + +/** @internal */ +export const FiberTypeId: Fiber.FiberTypeId = Symbol.for( + FiberSymbolKey +) as Fiber.FiberTypeId + +/** @internal */ +export const fiberVariance = { + /* c8 ignore next */ + _E: (_: never) => _, + /* c8 ignore next */ + _A: (_: never) => _ +} + +/** @internal */ +const fiberProto = { + [FiberTypeId]: fiberVariance, + pipe() { + return pipeArguments(this, arguments) + } +} + +/** @internal */ +const RuntimeFiberSymbolKey = "effect/Fiber" + +/** @internal */ +export const RuntimeFiberTypeId: Fiber.RuntimeFiberTypeId = Symbol.for( + RuntimeFiberSymbolKey +) as Fiber.RuntimeFiberTypeId + +/** @internal */ +export const Order: order.Order> = pipe( + order.tuple(number.Order, number.Order), + order.mapInput((fiber: Fiber.RuntimeFiber) => + [ + (fiber.id() as FiberId.Runtime).startTimeMillis, + (fiber.id() as FiberId.Runtime).id + ] as const + ) +) + +/** @internal */ +export const isFiber = (u: unknown): u is Fiber.Fiber => hasProperty(u, FiberTypeId) + +/** @internal */ +export const isRuntimeFiber = (self: Fiber.Fiber): self is Fiber.RuntimeFiber => + RuntimeFiberTypeId in self + +/** @internal */ +export const _await = (self: Fiber.Fiber): Effect.Effect> => self.await + +/** @internal */ +export const children = ( + self: Fiber.Fiber +): Effect.Effect>> => self.children + +/** @internal */ +export const done = (exit: Exit.Exit): Fiber.Fiber => { + const _fiber = { + ...effectable.CommitPrototype, + commit() { + return join(this) + }, + ...fiberProto, + id: () => FiberId.none, + await: core.succeed(exit), + children: core.succeed([]), + inheritAll: core.void, + poll: core.succeed(Option.some(exit)), + interruptAsFork: () => core.void + } + + return _fiber +} + +/** @internal */ +export const dump = (self: Fiber.RuntimeFiber): Effect.Effect => + core.map(self.status, (status) => ({ id: self.id(), status })) + +/** @internal */ +export const dumpAll = ( + fibers: Iterable> +): Effect.Effect> => core.forEachSequential(fibers, dump) + +/** @internal */ +export const fail = (error: E): Fiber.Fiber => done(Exit.fail(error)) + +/** @internal */ +export const failCause = (cause: Cause.Cause): Fiber.Fiber => done(Exit.failCause(cause)) + +/** @internal */ +export const fromEffect = (effect: Effect.Effect): Effect.Effect> => + core.map(core.exit(effect), done) + +/** @internal */ +export const id = (self: Fiber.Fiber): FiberId.FiberId => self.id() + +/** @internal */ +export const inheritAll = (self: Fiber.Fiber): Effect.Effect => self.inheritAll + +/** @internal */ +export const interrupted = (fiberId: FiberId.FiberId): Fiber.Fiber => done(Exit.interrupt(fiberId)) + +/** @internal */ +export const interruptAll = (fibers: Iterable>): Effect.Effect => + core.flatMap(core.fiberId, (fiberId) => pipe(fibers, interruptAllAs(fiberId))) + +/** @internal */ +export const interruptAllAs = dual< + (fiberId: FiberId.FiberId) => (fibers: Iterable>) => Effect.Effect, + (fibers: Iterable>, fiberId: FiberId.FiberId) => Effect.Effect +>( + 2, + core.fnUntraced(function*(fibers, fiberId) { + for (const fiber of fibers) { + if (isRuntimeFiber(fiber)) { + fiber.unsafeInterruptAsFork(fiberId) + continue + } + yield* fiber.interruptAsFork(fiberId) + } + for (const fiber of fibers) { + if (isRuntimeFiber(fiber) && fiber.unsafePoll()) { + continue + } + yield* fiber.await + } + }) +) + +/** @internal */ +export const interruptAsFork = dual< + (fiberId: FiberId.FiberId) => (self: Fiber.Fiber) => Effect.Effect, + (self: Fiber.Fiber, fiberId: FiberId.FiberId) => Effect.Effect +>(2, (self, fiberId) => self.interruptAsFork(fiberId)) + +/** @internal */ +export const join = (self: Fiber.Fiber): Effect.Effect => + core.zipLeft(core.flatten(self.await), self.inheritAll) + +/** @internal */ +export const map = dual< + (f: (a: A) => B) => (self: Fiber.Fiber) => Fiber.Fiber, + (self: Fiber.Fiber, f: (a: A) => B) => Fiber.Fiber +>(2, (self, f) => mapEffect(self, (a) => core.sync(() => f(a)))) + +/** @internal */ +export const mapEffect = dual< + (f: (a: A) => Effect.Effect) => (self: Fiber.Fiber) => Fiber.Fiber, + (self: Fiber.Fiber, f: (a: A) => Effect.Effect) => Fiber.Fiber +>(2, (self, f) => { + const _fiber = { + ...effectable.CommitPrototype, + commit() { + return join(this) + }, + ...fiberProto, + id: () => self.id(), + await: core.flatMap(self.await, Exit.forEachEffect(f)), + children: self.children, + inheritAll: self.inheritAll, + poll: core.flatMap(self.poll, (result) => { + switch (result._tag) { + case "None": + return core.succeed(Option.none()) + case "Some": + return pipe( + Exit.forEachEffect(result.value, f), + core.map(Option.some) + ) + } + }), + interruptAsFork: (id: FiberId.FiberId) => self.interruptAsFork(id) + } + return _fiber +}) + +/** @internal */ +export const mapFiber = dual< + ( + f: (a: A) => Fiber.Fiber + ) => (self: Fiber.Fiber) => Effect.Effect>, + ( + self: Fiber.Fiber, + f: (a: A) => Fiber.Fiber + ) => Effect.Effect> +>(2, ( + self: Fiber.Fiber, + f: (a: A) => Fiber.Fiber +) => + core.map( + self.await, + Exit.match({ + onFailure: (cause): Fiber.Fiber => failCause(cause), + onSuccess: (a) => f(a) + }) + )) + +/** @internal */ +export const match = dual< + ( + options: { + readonly onFiber: (fiber: Fiber.Fiber) => Z + readonly onRuntimeFiber: (fiber: Fiber.RuntimeFiber) => Z + } + ) => (self: Fiber.Fiber) => Z, + ( + self: Fiber.Fiber, + options: { + readonly onFiber: (fiber: Fiber.Fiber) => Z + readonly onRuntimeFiber: (fiber: Fiber.RuntimeFiber) => Z + } + ) => Z +>(2, (self, { onFiber, onRuntimeFiber }) => { + if (isRuntimeFiber(self)) { + return onRuntimeFiber(self) + } + return onFiber(self) +}) + +/** @internal */ +const _never = { + ...effectable.CommitPrototype, + commit() { + return join(this) + }, + ...fiberProto, + id: () => FiberId.none, + await: core.never, + children: core.succeed([]), + inheritAll: core.never, + poll: core.succeed(Option.none()), + interruptAsFork: () => core.never +} + +/** @internal */ +export const never: Fiber.Fiber = _never + +/** @internal */ +export const orElse = dual< + (that: Fiber.Fiber) => (self: Fiber.Fiber) => Fiber.Fiber, + (self: Fiber.Fiber, that: Fiber.Fiber) => Fiber.Fiber +>(2, (self, that) => ({ + ...effectable.CommitPrototype, + commit() { + return join(this) + }, + ...fiberProto, + id: () => FiberId.getOrElse(self.id(), that.id()), + await: core.zipWith( + self.await, + that.await, + (exit1, exit2) => (Exit.isSuccess(exit1) ? exit1 : exit2) + ), + children: self.children, + inheritAll: core.zipRight(that.inheritAll, self.inheritAll), + poll: core.zipWith( + self.poll, + that.poll, + (option1, option2) => { + switch (option1._tag) { + case "None": { + return Option.none() + } + case "Some": { + return Exit.isSuccess(option1.value) ? option1 : option2 + } + } + } + ), + interruptAsFork: (id) => + pipe( + core.interruptAsFiber(self, id), + core.zipRight(pipe(that, core.interruptAsFiber(id))), + core.asVoid + ) +})) + +/** @internal */ +export const orElseEither = dual< + (that: Fiber.Fiber) => (self: Fiber.Fiber) => Fiber.Fiber, E | E2>, + (self: Fiber.Fiber, that: Fiber.Fiber) => Fiber.Fiber, E | E2> +>(2, (self, that) => orElse(map(self, Either.left), map(that, Either.right))) + +/** @internal */ +export const poll = (self: Fiber.Fiber): Effect.Effect>> => self.poll + +// forked from https://github.com/sindresorhus/parse-ms/blob/4da2ffbdba02c6e288c08236695bdece0adca173/index.js +// MIT License +// Copyright (c) Sindre Sorhus (sindresorhus.com) +/** @internal */ +const parseMs = (milliseconds: number) => { + const roundTowardsZero = milliseconds > 0 ? Math.floor : Math.ceil + return { + days: roundTowardsZero(milliseconds / 86400000), + hours: roundTowardsZero(milliseconds / 3600000) % 24, + minutes: roundTowardsZero(milliseconds / 60000) % 60, + seconds: roundTowardsZero(milliseconds / 1000) % 60, + milliseconds: roundTowardsZero(milliseconds) % 1000, + microseconds: roundTowardsZero(milliseconds * 1000) % 1000, + nanoseconds: roundTowardsZero(milliseconds * 1e6) % 1000 + } +} + +/** @internal */ +const renderStatus = (status: FiberStatus.FiberStatus): string => { + if (FiberStatus.isDone(status)) { + return "Done" + } + if (FiberStatus.isRunning(status)) { + return "Running" + } + + const isInterruptible = runtimeFlags.interruptible(status.runtimeFlags) ? + "interruptible" : + "uninterruptible" + return `Suspended(${isInterruptible})` +} + +/** @internal */ +export const pretty = (self: Fiber.RuntimeFiber): Effect.Effect => + core.flatMap(Clock.currentTimeMillis, (now) => + core.map(dump(self), (dump) => { + const time = now - dump.id.startTimeMillis + const { days, hours, milliseconds, minutes, seconds } = parseMs(time) + const lifeMsg = (days === 0 ? "" : `${days}d`) + + (days === 0 && hours === 0 ? "" : `${hours}h`) + + (days === 0 && hours === 0 && minutes === 0 ? "" : `${minutes}m`) + + (days === 0 && hours === 0 && minutes === 0 && seconds === 0 ? "" : `${seconds}s`) + + `${milliseconds}ms` + const waitMsg = FiberStatus.isSuspended(dump.status) ? + (() => { + const ids = FiberId.ids(dump.status.blockingOn) + return HashSet.size(ids) > 0 + ? `waiting on ` + Array.from(ids).map((id) => `${id}`).join(", ") + : "" + })() : + "" + const statusMsg = renderStatus(dump.status) + return `[Fiber](#${dump.id.id}) (${lifeMsg}) ${waitMsg}\n Status: ${statusMsg}` + })) + +/** @internal */ +export const unsafeRoots = (): Array> => Array.from(fiberScope.globalScope.roots) + +/** @internal */ +export const roots: Effect.Effect>> = core.sync(unsafeRoots) + +/** @internal */ +export const status = (self: Fiber.RuntimeFiber): Effect.Effect => self.status + +/** @internal */ +export const succeed = (value: A): Fiber.Fiber => done(Exit.succeed(value)) + +const void_: Fiber.Fiber = succeed(void 0) +export { + /** @internal */ + void_ as void +} + +/** @internal */ +export const currentFiberURI = "effect/FiberCurrent" + +/** @internal */ +export const getCurrentFiber = (): Option.Option> => + Option.fromNullable((globalThis as any)[currentFiberURI]) diff --git a/repos/effect/packages/effect/src/internal/fiberId.ts b/repos/effect/packages/effect/src/internal/fiberId.ts new file mode 100644 index 0000000..c2f1666 --- /dev/null +++ b/repos/effect/packages/effect/src/internal/fiberId.ts @@ -0,0 +1,267 @@ +import * as Equal from "../Equal.js" +import type * as FiberId from "../FiberId.js" +import { dual, pipe } from "../Function.js" +import { globalValue } from "../GlobalValue.js" +import * as Hash from "../Hash.js" +import * as HashSet from "../HashSet.js" +import { format, NodeInspectSymbol, toJSON } from "../Inspectable.js" +import * as MutableRef from "../MutableRef.js" +import * as Option from "../Option.js" +import { hasProperty } from "../Predicate.js" + +/** @internal */ +const FiberIdSymbolKey = "effect/FiberId" + +/** @internal */ +export const FiberIdTypeId: FiberId.FiberIdTypeId = Symbol.for( + FiberIdSymbolKey +) as FiberId.FiberIdTypeId + +/** @internal */ +const OP_NONE = "None" as const + +/** @internal */ +export type OP_NONE = typeof OP_NONE + +/** @internal */ +const OP_RUNTIME = "Runtime" as const + +/** @internal */ +export type OP_RUNTIME = typeof OP_RUNTIME + +/** @internal */ +const OP_COMPOSITE = "Composite" as const + +/** @internal */ +export type OP_COMPOSITE = typeof OP_COMPOSITE + +const emptyHash = Hash.string(`${FiberIdSymbolKey}-${OP_NONE}`) + +/** @internal */ +class None implements FiberId.None { + readonly [FiberIdTypeId]: FiberId.FiberIdTypeId = FiberIdTypeId + readonly _tag = OP_NONE + readonly id = -1 + readonly startTimeMillis = -1; + [Hash.symbol](): number { + return emptyHash + } + [Equal.symbol](that: unknown): boolean { + return isFiberId(that) && that._tag === OP_NONE + } + toString() { + return format(this.toJSON()) + } + toJSON() { + return { + _id: "FiberId", + _tag: this._tag + } + } + [NodeInspectSymbol]() { + return this.toJSON() + } +} + +/** @internal */ +class Runtime implements FiberId.Runtime { + readonly [FiberIdTypeId]: FiberId.FiberIdTypeId = FiberIdTypeId + readonly _tag = OP_RUNTIME + constructor( + readonly id: number, + readonly startTimeMillis: number + ) {} + [Hash.symbol](): number { + return Hash.cached(this, Hash.string(`${FiberIdSymbolKey}-${this._tag}-${this.id}-${this.startTimeMillis}`)) + } + [Equal.symbol](that: unknown): boolean { + return isFiberId(that) && + that._tag === OP_RUNTIME && + this.id === that.id && + this.startTimeMillis === that.startTimeMillis + } + toString() { + return format(this.toJSON()) + } + toJSON() { + return { + _id: "FiberId", + _tag: this._tag, + id: this.id, + startTimeMillis: this.startTimeMillis + } + } + [NodeInspectSymbol]() { + return this.toJSON() + } +} + +/** @internal */ +class Composite implements FiberId.Composite { + readonly [FiberIdTypeId]: FiberId.FiberIdTypeId = FiberIdTypeId + readonly _tag = OP_COMPOSITE + constructor( + readonly left: FiberId.FiberId, + readonly right: FiberId.FiberId + ) { + } + _hash: number | undefined; + [Hash.symbol](): number { + return pipe( + Hash.string(`${FiberIdSymbolKey}-${this._tag}`), + Hash.combine(Hash.hash(this.left)), + Hash.combine(Hash.hash(this.right)), + Hash.cached(this) + ) + } + [Equal.symbol](that: unknown): boolean { + return isFiberId(that) && + that._tag === OP_COMPOSITE && + Equal.equals(this.left, that.left) && + Equal.equals(this.right, that.right) + } + toString() { + return format(this.toJSON()) + } + toJSON() { + return { + _id: "FiberId", + _tag: this._tag, + left: toJSON(this.left), + right: toJSON(this.right) + } + } + [NodeInspectSymbol]() { + return this.toJSON() + } +} + +/** @internal */ +export const none: FiberId.None = new None() + +/** @internal */ +export const runtime = (id: number, startTimeMillis: number): FiberId.Runtime => { + return new Runtime(id, startTimeMillis) +} + +/** @internal */ +export const composite = (left: FiberId.FiberId, right: FiberId.FiberId): FiberId.Composite => { + return new Composite(left, right) +} + +/** @internal */ +export const isFiberId = (self: unknown): self is FiberId.FiberId => hasProperty(self, FiberIdTypeId) + +/** @internal */ +export const isNone = (self: FiberId.FiberId): self is FiberId.None => { + return self._tag === OP_NONE || pipe(toSet(self), HashSet.every((id) => isNone(id))) +} + +/** @internal */ +export const isRuntime = (self: FiberId.FiberId): self is FiberId.Runtime => { + return self._tag === OP_RUNTIME +} + +/** @internal */ +export const isComposite = (self: FiberId.FiberId): self is FiberId.Composite => { + return self._tag === OP_COMPOSITE +} + +/** @internal */ +export const combine = dual< + (that: FiberId.FiberId) => (self: FiberId.FiberId) => FiberId.FiberId, + (self: FiberId.FiberId, that: FiberId.FiberId) => FiberId.FiberId +>(2, (self, that) => { + if (self._tag === OP_NONE) { + return that + } + if (that._tag === OP_NONE) { + return self + } + return new Composite(self, that) +}) + +/** @internal */ +export const combineAll = (fiberIds: HashSet.HashSet): FiberId.FiberId => { + return pipe(fiberIds, HashSet.reduce(none as FiberId.FiberId, (a, b) => combine(b)(a))) +} + +/** @internal */ +export const getOrElse = dual< + (that: FiberId.FiberId) => (self: FiberId.FiberId) => FiberId.FiberId, + (self: FiberId.FiberId, that: FiberId.FiberId) => FiberId.FiberId +>(2, (self, that) => isNone(self) ? that : self) + +/** @internal */ +export const ids = (self: FiberId.FiberId): HashSet.HashSet => { + switch (self._tag) { + case OP_NONE: { + return HashSet.empty() + } + case OP_RUNTIME: { + return HashSet.make(self.id) + } + case OP_COMPOSITE: { + return pipe(ids(self.left), HashSet.union(ids(self.right))) + } + } +} + +const _fiberCounter = globalValue( + Symbol.for("effect/Fiber/Id/_fiberCounter"), + () => MutableRef.make(0) +) + +/** @internal */ +export const make = (id: number, startTimeSeconds: number): FiberId.FiberId => { + return new Runtime(id, startTimeSeconds) +} + +/** @internal */ +export const threadName = (self: FiberId.FiberId): string => { + const identifiers = Array.from(ids(self)).map((n) => `#${n}`).join(",") + return identifiers +} + +/** @internal */ +export const toOption = (self: FiberId.FiberId): Option.Option => { + const fiberIds = toSet(self) + if (HashSet.size(fiberIds) === 0) { + return Option.none() + } + let first = true + let acc: FiberId.FiberId + for (const fiberId of fiberIds) { + if (first) { + acc = fiberId + first = false + } else { + // @ts-expect-error + acc = pipe(acc, combine(fiberId)) + } + } + // @ts-expect-error + return Option.some(acc) +} + +/** @internal */ +export const toSet = (self: FiberId.FiberId): HashSet.HashSet => { + switch (self._tag) { + case OP_NONE: { + return HashSet.empty() + } + case OP_RUNTIME: { + return HashSet.make(self) + } + case OP_COMPOSITE: { + return pipe(toSet(self.left), HashSet.union(toSet(self.right))) + } + } +} + +/** @internal */ +export const unsafeMake = (): FiberId.Runtime => { + const id = MutableRef.get(_fiberCounter) + pipe(_fiberCounter, MutableRef.set(id + 1)) + return new Runtime(id, Date.now()) +} diff --git a/repos/effect/packages/effect/src/internal/fiberMessage.ts b/repos/effect/packages/effect/src/internal/fiberMessage.ts new file mode 100644 index 0000000..bad70df --- /dev/null +++ b/repos/effect/packages/effect/src/internal/fiberMessage.ts @@ -0,0 +1,82 @@ +import type * as Cause from "../Cause.js" +import type * as Effect from "../Effect.js" +import type * as FiberStatus from "../FiberStatus.js" +import type * as FiberRuntime from "./fiberRuntime.js" + +/** @internal */ +export type FiberMessage = InterruptSignal | Stateful | Resume | YieldNow + +/** @internal */ +export const OP_INTERRUPT_SIGNAL = "InterruptSignal" as const + +/** @internal */ +export type OP_INTERRUPT_SIGNAL = typeof OP_INTERRUPT_SIGNAL + +/** @internal */ +export const OP_STATEFUL = "Stateful" as const + +/** @internal */ +export type OP_STATEFUL = typeof OP_STATEFUL + +/** @internal */ +export const OP_RESUME = "Resume" as const + +/** @internal */ +export type OP_RESUME = typeof OP_RESUME + +/** @internal */ +export const OP_YIELD_NOW = "YieldNow" as const + +/** @internal */ +export type OP_YIELD_NOW = typeof OP_YIELD_NOW + +/** @internal */ +export interface InterruptSignal { + readonly _tag: OP_INTERRUPT_SIGNAL + readonly cause: Cause.Cause +} + +/** @internal */ +export interface Stateful { + readonly _tag: OP_STATEFUL + onFiber(fiber: FiberRuntime.FiberRuntime, status: FiberStatus.FiberStatus): void +} + +/** @internal */ +export interface Resume { + readonly _tag: OP_RESUME + readonly effect: Effect.Effect +} + +/** @internal */ +export interface YieldNow { + readonly _tag: OP_YIELD_NOW +} + +/** @internal */ +export const interruptSignal = (cause: Cause.Cause): FiberMessage => ({ + _tag: OP_INTERRUPT_SIGNAL, + cause +}) + +/** @internal */ +export const stateful = ( + onFiber: ( + fiber: FiberRuntime.FiberRuntime, + status: FiberStatus.FiberStatus + ) => void +): FiberMessage => ({ + _tag: OP_STATEFUL, + onFiber +}) + +/** @internal */ +export const resume = (effect: Effect.Effect): FiberMessage => ({ + _tag: OP_RESUME, + effect +}) + +/** @internal */ +export const yieldNow = (): FiberMessage => ({ + _tag: OP_YIELD_NOW +}) diff --git a/repos/effect/packages/effect/src/internal/fiberRefs.ts b/repos/effect/packages/effect/src/internal/fiberRefs.ts new file mode 100644 index 0000000..8d9ece3 --- /dev/null +++ b/repos/effect/packages/effect/src/internal/fiberRefs.ts @@ -0,0 +1,297 @@ +import * as Arr from "../Array.js" +import type * as Effect from "../Effect.js" +import * as Equal from "../Equal.js" +import type * as FiberId from "../FiberId.js" +import type * as FiberRef from "../FiberRef.js" +import type * as FiberRefs from "../FiberRefs.js" +import { dual, pipe } from "../Function.js" +import * as HashSet from "../HashSet.js" +import * as Option from "../Option.js" +import { pipeArguments } from "../Pipeable.js" +import * as core from "./core.js" + +/** @internal */ +export function unsafeMake( + fiberRefLocals: Map, Arr.NonEmptyReadonlyArray> +): FiberRefs.FiberRefs { + return new FiberRefsImpl(fiberRefLocals) +} + +/** @internal */ +export function empty(): FiberRefs.FiberRefs { + return unsafeMake(new Map()) +} + +/** @internal */ +export const FiberRefsSym: FiberRefs.FiberRefsSym = Symbol.for("effect/FiberRefs") as FiberRefs.FiberRefsSym + +/** @internal */ +export class FiberRefsImpl implements FiberRefs.FiberRefs { + readonly [FiberRefsSym]: FiberRefs.FiberRefsSym = FiberRefsSym + constructor( + readonly locals: Map< + FiberRef.FiberRef, + Arr.NonEmptyReadonlyArray + > + ) {} + pipe() { + return pipeArguments(this, arguments) + } +} + +/** @internal */ +const findAncestor = ( + _ref: FiberRef.FiberRef, + _parentStack: ReadonlyArray, + _childStack: ReadonlyArray, + _childModified = false +): readonly [unknown, boolean] => { + const ref = _ref + let parentStack = _parentStack + let childStack = _childStack + let childModified = _childModified + let ret: readonly [unknown, boolean] | undefined = undefined + while (ret === undefined) { + if (Arr.isNonEmptyReadonlyArray(parentStack) && Arr.isNonEmptyReadonlyArray(childStack)) { + const parentFiberId = Arr.headNonEmpty(parentStack)[0] + const parentAncestors = Arr.tailNonEmpty(parentStack) + const childFiberId = Arr.headNonEmpty(childStack)[0] + const childRefValue = Arr.headNonEmpty(childStack)[1] + const childAncestors = Arr.tailNonEmpty(childStack) + if (parentFiberId.startTimeMillis < childFiberId.startTimeMillis) { + childStack = childAncestors + childModified = true + } else if (parentFiberId.startTimeMillis > childFiberId.startTimeMillis) { + parentStack = parentAncestors + } else { + if (parentFiberId.id < childFiberId.id) { + childStack = childAncestors + childModified = true + } else if (parentFiberId.id > childFiberId.id) { + parentStack = parentAncestors + } else { + ret = [childRefValue, childModified] as const + } + } + } else { + ret = [ref.initial, true] as const + } + } + return ret +} + +/** @internal */ +export const joinAs = dual< + (fiberId: FiberId.Single, that: FiberRefs.FiberRefs) => (self: FiberRefs.FiberRefs) => FiberRefs.FiberRefs, + (self: FiberRefs.FiberRefs, fiberId: FiberId.Single, that: FiberRefs.FiberRefs) => FiberRefs.FiberRefs +>(3, (self, fiberId, that) => { + const parentFiberRefs = new Map(self.locals) + that.locals.forEach((childStack, fiberRef) => { + const childValue = childStack[0][1] + if (!childStack[0][0][Equal.symbol](fiberId)) { + if (!parentFiberRefs.has(fiberRef)) { + if (Equal.equals(childValue, fiberRef.initial)) { + return + } + parentFiberRefs.set( + fiberRef, + [[fiberId, fiberRef.join(fiberRef.initial, childValue)]] + ) + return + } + const parentStack = parentFiberRefs.get(fiberRef)! + const [ancestor, wasModified] = findAncestor( + fiberRef, + parentStack, + childStack + ) + if (wasModified) { + const patch = fiberRef.diff(ancestor, childValue) + const oldValue = parentStack[0][1] + const newValue = fiberRef.join(oldValue, fiberRef.patch(patch)(oldValue)) + if (!Equal.equals(oldValue, newValue)) { + let newStack: Arr.NonEmptyReadonlyArray + const parentFiberId = parentStack[0][0] + if (parentFiberId[Equal.symbol](fiberId)) { + newStack = [[parentFiberId, newValue] as const, ...parentStack.slice(1)] + } else { + newStack = [[fiberId, newValue] as const, ...parentStack] + } + parentFiberRefs.set(fiberRef, newStack) + } + } + } + }) + return new FiberRefsImpl(parentFiberRefs) +}) + +/** @internal */ +export const forkAs = dual< + (childId: FiberId.Single) => (self: FiberRefs.FiberRefs) => FiberRefs.FiberRefs, + (self: FiberRefs.FiberRefs, childId: FiberId.Single) => FiberRefs.FiberRefs +>(2, (self, childId) => { + const map = new Map, Arr.NonEmptyReadonlyArray>() + unsafeForkAs(self, map, childId) + return new FiberRefsImpl(map) +}) + +const unsafeForkAs = ( + self: FiberRefs.FiberRefs, + map: Map, Arr.NonEmptyReadonlyArray>, + fiberId: FiberId.Single +) => { + self.locals.forEach((stack, fiberRef) => { + const oldValue = stack[0][1] + const newValue = fiberRef.patch(fiberRef.fork)(oldValue) + if (Equal.equals(oldValue, newValue)) { + map.set(fiberRef, stack) + } else { + map.set(fiberRef, [[fiberId, newValue] as const, ...stack]) + } + }) +} + +/** @internal */ +export const fiberRefs = (self: FiberRefs.FiberRefs) => HashSet.fromIterable(self.locals.keys()) + +/** @internal */ +export const setAll = (self: FiberRefs.FiberRefs): Effect.Effect => + core.forEachSequentialDiscard( + fiberRefs(self), + (fiberRef) => core.fiberRefSet(fiberRef, getOrDefault(self, fiberRef)) + ) + +/** @internal */ +export const delete_ = dual< + (fiberRef: FiberRef.FiberRef) => (self: FiberRefs.FiberRefs) => FiberRefs.FiberRefs, + (self: FiberRefs.FiberRefs, fiberRef: FiberRef.FiberRef) => FiberRefs.FiberRefs +>(2, (self, fiberRef) => { + const locals = new Map(self.locals) + locals.delete(fiberRef) + return new FiberRefsImpl(locals) +}) + +/** @internal */ +export const get = dual< + (fiberRef: FiberRef.FiberRef) => (self: FiberRefs.FiberRefs) => Option.Option, + (self: FiberRefs.FiberRefs, fiberRef: FiberRef.FiberRef) => Option.Option +>(2, (self, fiberRef) => { + if (!self.locals.has(fiberRef)) { + return Option.none() + } + return Option.some(Arr.headNonEmpty(self.locals.get(fiberRef)!)[1]) +}) + +/** @internal */ +export const getOrDefault = dual< + (fiberRef: FiberRef.FiberRef) => (self: FiberRefs.FiberRefs) => A, + (self: FiberRefs.FiberRefs, fiberRef: FiberRef.FiberRef) => A +>(2, (self, fiberRef) => pipe(get(self, fiberRef), Option.getOrElse(() => fiberRef.initial))) + +/** @internal */ +export const updateAs = dual< + ( + options: { + readonly fiberId: FiberId.Single + readonly fiberRef: FiberRef.FiberRef + readonly value: A + } + ) => (self: FiberRefs.FiberRefs) => FiberRefs.FiberRefs, + ( + self: FiberRefs.FiberRefs, + options: { + readonly fiberId: FiberId.Single + readonly fiberRef: FiberRef.FiberRef + readonly value: A + } + ) => FiberRefs.FiberRefs +>(2, (self: FiberRefs.FiberRefs, { fiberId, fiberRef, value }: { + readonly fiberId: FiberId.Single + readonly fiberRef: FiberRef.FiberRef + readonly value: A +}) => { + if (self.locals.size === 0) { + return new FiberRefsImpl(new Map([[fiberRef, [[fiberId, value] as const]]])) + } + const locals = new Map(self.locals) + unsafeUpdateAs(locals, fiberId, fiberRef, value) + return new FiberRefsImpl(locals) +}) + +const unsafeUpdateAs = ( + locals: Map, Arr.NonEmptyReadonlyArray>, + fiberId: FiberId.Single, + fiberRef: FiberRef.FiberRef, + value: any +) => { + const oldStack: ReadonlyArray = locals.get(fiberRef) ?? [] + let newStack: Arr.NonEmptyReadonlyArray | undefined + + if (Arr.isNonEmptyReadonlyArray(oldStack)) { + const [currentId, currentValue] = Arr.headNonEmpty(oldStack) + if (currentId[Equal.symbol](fiberId)) { + if (Equal.equals(currentValue, value)) { + return + } else { + newStack = [ + [fiberId, value] as const, + ...oldStack.slice(1) + ] + } + } else { + newStack = [ + [fiberId, value] as const, + ...oldStack + ] + } + } else { + newStack = [[fiberId, value] as const] + } + + locals.set(fiberRef, newStack) +} + +/** @internal */ +export const updateManyAs = dual< + ( + options: { + readonly forkAs?: FiberId.Single | undefined + readonly entries: Arr.NonEmptyReadonlyArray< + readonly [FiberRef.FiberRef, Arr.NonEmptyReadonlyArray] + > + } + ) => (self: FiberRefs.FiberRefs) => FiberRefs.FiberRefs, + ( + self: FiberRefs.FiberRefs, + options: { + readonly forkAs?: FiberId.Single | undefined + readonly entries: Arr.NonEmptyReadonlyArray< + readonly [FiberRef.FiberRef, Arr.NonEmptyReadonlyArray] + > + } + ) => FiberRefs.FiberRefs +>(2, (self: FiberRefs.FiberRefs, { entries, forkAs }: { + readonly forkAs?: FiberId.Single | undefined + readonly entries: Arr.NonEmptyReadonlyArray< + readonly [FiberRef.FiberRef, Arr.NonEmptyReadonlyArray] + > +}) => { + if (self.locals.size === 0) { + return new FiberRefsImpl(new Map(entries)) + } + + const locals = new Map(self.locals) + if (forkAs !== undefined) { + unsafeForkAs(self, locals, forkAs) + } + entries.forEach(([fiberRef, values]) => { + if (values.length === 1) { + unsafeUpdateAs(locals, values[0][0], fiberRef, values[0][1]) + } else { + values.forEach(([fiberId, value]) => { + unsafeUpdateAs(locals, fiberId, fiberRef, value) + }) + } + }) + return new FiberRefsImpl(locals) +}) diff --git a/repos/effect/packages/effect/src/internal/fiberRefs/patch.ts b/repos/effect/packages/effect/src/internal/fiberRefs/patch.ts new file mode 100644 index 0000000..7a9115e --- /dev/null +++ b/repos/effect/packages/effect/src/internal/fiberRefs/patch.ts @@ -0,0 +1,144 @@ +import * as Arr from "../../Array.js" +import { equals } from "../../Equal.js" +import type * as FiberId from "../../FiberId.js" +import type * as FiberRefs from "../../FiberRefs.js" +import type * as FiberRefsPatch from "../../FiberRefsPatch.js" +import { dual } from "../../Function.js" +import * as fiberRefs_ from "../fiberRefs.js" + +/** @internal */ +export const OP_EMPTY = "Empty" as const + +/** @internal */ +export type OP_EMPTY = typeof OP_EMPTY + +/** @internal */ +export const OP_ADD = "Add" as const + +/** @internal */ +export type OP_ADD = typeof OP_ADD + +/** @internal */ +export const OP_REMOVE = "Remove" as const + +/** @internal */ +export type OP_REMOVE = typeof OP_REMOVE + +/** @internal */ +export const OP_UPDATE = "Update" as const + +/** @internal */ +export type OP_UPDATE = typeof OP_UPDATE + +/** @internal */ +export const OP_AND_THEN = "AndThen" as const + +/** @internal */ +export type OP_AND_THEN = typeof OP_AND_THEN + +/** @internal */ +export const empty: FiberRefsPatch.FiberRefsPatch = ({ + _tag: OP_EMPTY +}) as FiberRefsPatch.FiberRefsPatch + +/** @internal */ +export const diff = ( + oldValue: FiberRefs.FiberRefs, + newValue: FiberRefs.FiberRefs +): FiberRefsPatch.FiberRefsPatch => { + const missingLocals = new Map(oldValue.locals) + let patch = empty + for (const [fiberRef, pairs] of newValue.locals.entries()) { + const newValue = Arr.headNonEmpty(pairs)[1] + const old = missingLocals.get(fiberRef) + if (old !== undefined) { + const oldValue = Arr.headNonEmpty(old)[1] + if (!equals(oldValue, newValue)) { + patch = combine({ + _tag: OP_UPDATE, + fiberRef, + patch: fiberRef.diff(oldValue, newValue) + })(patch) + } + } else { + patch = combine({ + _tag: OP_ADD, + fiberRef, + value: newValue + })(patch) + } + missingLocals.delete(fiberRef) + } + for (const [fiberRef] of missingLocals.entries()) { + patch = combine({ + _tag: OP_REMOVE, + fiberRef + })(patch) + } + return patch +} + +/** @internal */ +export const combine = dual< + (that: FiberRefsPatch.FiberRefsPatch) => (self: FiberRefsPatch.FiberRefsPatch) => FiberRefsPatch.FiberRefsPatch, + (self: FiberRefsPatch.FiberRefsPatch, that: FiberRefsPatch.FiberRefsPatch) => FiberRefsPatch.FiberRefsPatch +>(2, (self, that) => ({ + _tag: OP_AND_THEN, + first: self, + second: that +})) + +/** @internal */ +export const patch = dual< + ( + fiberId: FiberId.Runtime, + oldValue: FiberRefs.FiberRefs + ) => (self: FiberRefsPatch.FiberRefsPatch) => FiberRefs.FiberRefs, + ( + self: FiberRefsPatch.FiberRefsPatch, + fiberId: FiberId.Runtime, + oldValue: FiberRefs.FiberRefs + ) => FiberRefs.FiberRefs +>(3, (self, fiberId, oldValue) => { + let fiberRefs: FiberRefs.FiberRefs = oldValue + let patches: ReadonlyArray = Arr.of(self) + while (Arr.isNonEmptyReadonlyArray(patches)) { + const head = Arr.headNonEmpty(patches) + const tail = Arr.tailNonEmpty(patches) + switch (head._tag) { + case OP_EMPTY: { + patches = tail + break + } + case OP_ADD: { + fiberRefs = fiberRefs_.updateAs(fiberRefs, { + fiberId, + fiberRef: head.fiberRef, + value: head.value + }) + patches = tail + break + } + case OP_REMOVE: { + fiberRefs = fiberRefs_.delete_(fiberRefs, head.fiberRef) + patches = tail + break + } + case OP_UPDATE: { + const value = fiberRefs_.getOrDefault(fiberRefs, head.fiberRef) + fiberRefs = fiberRefs_.updateAs(fiberRefs, { + fiberId, + fiberRef: head.fiberRef, + value: head.fiberRef.patch(head.patch)(value) + }) + patches = tail + break + } + case OP_AND_THEN: { + patches = Arr.prepend(head.first)(Arr.prepend(head.second)(tail)) + break + } + } + } + return fiberRefs +}) diff --git a/repos/effect/packages/effect/src/internal/fiberRuntime.ts b/repos/effect/packages/effect/src/internal/fiberRuntime.ts new file mode 100644 index 0000000..f93e75c --- /dev/null +++ b/repos/effect/packages/effect/src/internal/fiberRuntime.ts @@ -0,0 +1,3860 @@ +import * as RA from "../Array.js" +import * as Boolean from "../Boolean.js" +import type * as Cause from "../Cause.js" +import * as Chunk from "../Chunk.js" +import type * as Clock from "../Clock.js" +import type { ConfigProvider } from "../ConfigProvider.js" +import * as Context from "../Context.js" +import type { DefaultServices } from "../DefaultServices.js" +import type * as Duration from "../Duration.js" +import type * as Effect from "../Effect.js" +import * as Effectable from "../Effectable.js" +import type * as Either from "../Either.js" +import * as ExecutionStrategy from "../ExecutionStrategy.js" +import type * as Exit from "../Exit.js" +import type * as Fiber from "../Fiber.js" +import * as FiberId from "../FiberId.js" +import type * as FiberRef from "../FiberRef.js" +import * as FiberRefs from "../FiberRefs.js" +import * as FiberRefsPatch from "../FiberRefsPatch.js" +import * as FiberStatus from "../FiberStatus.js" +import type { LazyArg } from "../Function.js" +import { dual, identity, pipe } from "../Function.js" +import { globalValue } from "../GlobalValue.js" +import * as HashMap from "../HashMap.js" +import * as HashSet from "../HashSet.js" +import * as Inspectable from "../Inspectable.js" +import type { Logger } from "../Logger.js" +import * as LogLevel from "../LogLevel.js" +import type * as MetricLabel from "../MetricLabel.js" +import * as Micro from "../Micro.js" +import * as MRef from "../MutableRef.js" +import * as Option from "../Option.js" +import { pipeArguments } from "../Pipeable.js" +import * as Predicate from "../Predicate.js" +import type * as Random from "../Random.js" +import * as Ref from "../Ref.js" +import type { Entry, Request } from "../Request.js" +import type * as RequestBlock from "../RequestBlock.js" +import type * as RuntimeFlags from "../RuntimeFlags.js" +import * as RuntimeFlagsPatch from "../RuntimeFlagsPatch.js" +import { currentScheduler, type Scheduler } from "../Scheduler.js" +import type * as Scope from "../Scope.js" +import type * as Supervisor from "../Supervisor.js" +import type * as Tracer from "../Tracer.js" +import type { Concurrency, NoExcessProperties, NoInfer } from "../Types.js" +import { internalCall, yieldWrapGet } from "../Utils.js" +import * as RequestBlock_ from "./blockedRequests.js" +import * as internalCause from "./cause.js" +import * as clock from "./clock.js" +import { currentRequestMap } from "./completedRequestMap.js" +import * as concurrency from "./concurrency.js" +import { configProviderTag } from "./configProvider.js" +import * as internalEffect from "./core-effect.js" +import * as core from "./core.js" +import * as defaultServices from "./defaultServices.js" +import { consoleTag } from "./defaultServices/console.js" +import * as executionStrategy from "./executionStrategy.js" +import * as internalFiber from "./fiber.js" +import * as FiberMessage from "./fiberMessage.js" +import * as fiberRefs from "./fiberRefs.js" +import * as fiberScope from "./fiberScope.js" +import * as internalLogger from "./logger.js" +import * as metric from "./metric.js" +import * as metricBoundaries from "./metric/boundaries.js" +import * as metricLabel from "./metric/label.js" +import * as OpCodes from "./opCodes/effect.js" +import { randomTag } from "./random.js" +import { complete } from "./request.js" +import * as runtimeFlags_ from "./runtimeFlags.js" +import { OpSupervision } from "./runtimeFlags.js" +import * as supervisor from "./supervisor.js" +import * as SupervisorPatch from "./supervisor/patch.js" +import * as tracer from "./tracer.js" +import * as version from "./version.js" + +/** @internal */ +export const fiberStarted = metric.counter("effect_fiber_started", { incremental: true }) +/** @internal */ +export const fiberActive = metric.counter("effect_fiber_active") +/** @internal */ +export const fiberSuccesses = metric.counter("effect_fiber_successes", { incremental: true }) +/** @internal */ +export const fiberFailures = metric.counter("effect_fiber_failures", { incremental: true }) +/** @internal */ +export const fiberLifetimes = metric.tagged( + metric.histogram( + "effect_fiber_lifetimes", + metricBoundaries.exponential({ + start: 0.5, + factor: 2, + count: 35 + }) + ), + "time_unit", + "milliseconds" +) + +/** @internal */ +type EvaluationSignal = + | EvaluationSignalContinue + | EvaluationSignalDone + | EvaluationSignalYieldNow + +/** @internal */ +const EvaluationSignalContinue = "Continue" as const + +/** @internal */ +type EvaluationSignalContinue = typeof EvaluationSignalContinue + +/** @internal */ +const EvaluationSignalDone = "Done" as const + +/** @internal */ +type EvaluationSignalDone = typeof EvaluationSignalDone + +/** @internal */ +const EvaluationSignalYieldNow = "Yield" as const + +/** @internal */ +type EvaluationSignalYieldNow = typeof EvaluationSignalYieldNow + +const runtimeFiberVariance = { + /* c8 ignore next */ + _E: (_: never) => _, + /* c8 ignore next */ + _A: (_: never) => _ +} + +const absurd = (_: never): never => { + throw new Error( + `BUG: FiberRuntime - ${ + Inspectable.toStringUnknown(_) + } - please report an issue at https://github.com/Effect-TS/effect/issues` + ) +} + +const YieldedOp = Symbol.for("effect/internal/fiberRuntime/YieldedOp") +type YieldedOp = typeof YieldedOp +const yieldedOpChannel: { + currentOp: core.Primitive | null +} = globalValue("effect/internal/fiberRuntime/yieldedOpChannel", () => ({ + currentOp: null +})) + +const contOpSuccess = { + [OpCodes.OP_ON_SUCCESS]: ( + _: FiberRuntime, + cont: core.OnSuccess, + value: unknown + ) => { + return internalCall(() => cont.effect_instruction_i1(value)) + }, + ["OnStep"]: ( + _: FiberRuntime, + _cont: core.OnStep, + value: unknown + ) => { + return core.exitSucceed(core.exitSucceed(value)) + }, + [OpCodes.OP_ON_SUCCESS_AND_FAILURE]: ( + _: FiberRuntime, + cont: core.OnSuccessAndFailure, + value: unknown + ) => { + return internalCall(() => cont.effect_instruction_i2(value)) + }, + [OpCodes.OP_REVERT_FLAGS]: ( + self: FiberRuntime, + cont: core.RevertFlags, + value: unknown + ) => { + self.patchRuntimeFlags(self.currentRuntimeFlags, cont.patch) + if (runtimeFlags_.interruptible(self.currentRuntimeFlags) && self.isInterrupted()) { + return core.exitFailCause(self.getInterruptedCause()) + } else { + return core.exitSucceed(value) + } + }, + [OpCodes.OP_WHILE]: ( + self: FiberRuntime, + cont: core.While, + value: unknown + ) => { + internalCall(() => cont.effect_instruction_i2(value)) + if (internalCall(() => cont.effect_instruction_i0())) { + self.pushStack(cont) + return internalCall(() => cont.effect_instruction_i1()) + } else { + return core.void + } + }, + [OpCodes.OP_ITERATOR]: ( + self: FiberRuntime, + cont: core.FromIterator, + value: unknown + ) => { + while (true) { + const state = internalCall(() => cont.effect_instruction_i0.next(value)) + if (state.done) { + return core.exitSucceed(state.value) + } + const primitive = yieldWrapGet(state.value) + if (!core.exitIsExit(primitive)) { + self.pushStack(cont) + return primitive + } else if (primitive._tag === "Failure") { + return primitive + } + value = primitive.value + } + } +} + +const drainQueueWhileRunningTable = { + [FiberMessage.OP_INTERRUPT_SIGNAL]: ( + self: FiberRuntime, + runtimeFlags: RuntimeFlags.RuntimeFlags, + cur: Effect.Effect, + message: FiberMessage.FiberMessage & { _tag: FiberMessage.OP_INTERRUPT_SIGNAL } + ) => { + self.processNewInterruptSignal(message.cause) + return runtimeFlags_.interruptible(runtimeFlags) ? core.exitFailCause(message.cause) : cur + }, + [FiberMessage.OP_RESUME]: ( + _self: FiberRuntime, + _runtimeFlags: RuntimeFlags.RuntimeFlags, + _cur: Effect.Effect, + _message: FiberMessage.FiberMessage + ) => { + throw new Error("It is illegal to have multiple concurrent run loops in a single fiber") + }, + [FiberMessage.OP_STATEFUL]: ( + self: FiberRuntime, + runtimeFlags: RuntimeFlags.RuntimeFlags, + cur: Effect.Effect, + message: FiberMessage.FiberMessage & { _tag: FiberMessage.OP_STATEFUL } + ) => { + message.onFiber(self, FiberStatus.running(runtimeFlags)) + return cur + }, + [FiberMessage.OP_YIELD_NOW]: ( + _self: FiberRuntime, + _runtimeFlags: RuntimeFlags.RuntimeFlags, + cur: Effect.Effect, + _message: FiberMessage.FiberMessage & { _tag: FiberMessage.OP_YIELD_NOW } + ) => { + return core.flatMap(core.yieldNow(), () => cur) + } +} + +/** + * Executes all requests, submitting requests to each data source in parallel. + */ +const runBlockedRequests = (self: RequestBlock.RequestBlock) => + core.forEachSequentialDiscard( + RequestBlock_.flatten(self), + (requestsByRequestResolver) => + forEachConcurrentDiscard( + RequestBlock_.sequentialCollectionToChunk(requestsByRequestResolver), + ([dataSource, sequential]) => { + const map = new Map, Entry>() + const arr: Array>> = [] + for (const block of sequential) { + arr.push(Chunk.toReadonlyArray(block) as any) + for (const entry of block) { + map.set(entry.request as Request, entry) + } + } + const flat = arr.flat() + return core.fiberRefLocally( + invokeWithInterrupt(dataSource.runAll(arr), flat, () => + flat.forEach((entry) => { + entry.listeners.interrupted = true + })), + currentRequestMap, + map + ) + }, + false, + false + ) + ) + +/** @internal */ +export interface Snapshot { + refs: FiberRefs.FiberRefs + flags: RuntimeFlags.RuntimeFlags +} + +const _version = version.getCurrentVersion() + +/** @internal */ +export class FiberRuntime extends Effectable.Class + implements Fiber.RuntimeFiber +{ + readonly [internalFiber.FiberTypeId] = internalFiber.fiberVariance + readonly [internalFiber.RuntimeFiberTypeId] = runtimeFiberVariance + private _fiberRefs: FiberRefs.FiberRefs + private _fiberId: FiberId.Runtime + private _queue = new Array() + private _children: Set> | null = null + private _observers = new Array<(exit: Exit.Exit) => void>() + private _running = false + private _stack: Array = [] + private _asyncInterruptor: ((effect: Effect.Effect) => any) | null = null + private _asyncBlockingOn: FiberId.FiberId | null = null + private _exitValue: Exit.Exit | null = null + private _steps: Array = [] + private _isYielding = false + + public currentRuntimeFlags: RuntimeFlags.RuntimeFlags + public currentOpCount: number = 0 + public currentSupervisor!: Supervisor.Supervisor + public currentScheduler!: Scheduler + public currentTracer!: Tracer.Tracer + public currentSpan!: Tracer.AnySpan | undefined + public currentContext!: Context.Context + public currentDefaultServices!: Context.Context + + constructor( + fiberId: FiberId.Runtime, + fiberRefs0: FiberRefs.FiberRefs, + runtimeFlags0: RuntimeFlags.RuntimeFlags + ) { + super() + this.currentRuntimeFlags = runtimeFlags0 + this._fiberId = fiberId + this._fiberRefs = fiberRefs0 + if (runtimeFlags_.runtimeMetrics(runtimeFlags0)) { + const tags = this.getFiberRef(core.currentMetricLabels) + fiberStarted.unsafeUpdate(1, tags) + fiberActive.unsafeUpdate(1, tags) + } + this.refreshRefCache() + } + + commit(): Effect.Effect { + return internalFiber.join(this) + } + + /** + * The identity of the fiber. + */ + id(): FiberId.Runtime { + return this._fiberId + } + + /** + * Begins execution of the effect associated with this fiber on in the + * background. This can be called to "kick off" execution of a fiber after + * it has been created. + */ + resume(effect: Effect.Effect): void { + this.tell(FiberMessage.resume(effect)) + } + + /** + * The status of the fiber. + */ + get status(): Effect.Effect { + return this.ask((_, status) => status) + } + + /** + * Gets the fiber runtime flags. + */ + get runtimeFlags(): Effect.Effect { + return this.ask((state, status) => { + if (FiberStatus.isDone(status)) { + return state.currentRuntimeFlags + } + return status.runtimeFlags + }) + } + + /** + * Returns the current `FiberScope` for the fiber. + */ + scope(): fiberScope.FiberScope { + return fiberScope.unsafeMake(this) + } + + /** + * Retrieves the immediate children of the fiber. + */ + get children(): Effect.Effect>> { + return this.ask((fiber) => Array.from(fiber.getChildren())) + } + + /** + * Gets the fiber's set of children. + */ + getChildren(): Set> { + if (this._children === null) { + this._children = new Set() + } + return this._children + } + + /** + * Retrieves the interrupted cause of the fiber, which will be `Cause.empty` + * if the fiber has not been interrupted. + * + * **NOTE**: This method is safe to invoke on any fiber, but if not invoked + * on this fiber, then values derived from the fiber's state (including the + * log annotations and log level) may not be up-to-date. + */ + getInterruptedCause() { + return this.getFiberRef(core.currentInterruptedCause) + } + + /** + * Retrieves the whole set of fiber refs. + */ + fiberRefs(): Effect.Effect { + return this.ask((fiber) => fiber.getFiberRefs()) + } + + /** + * Returns an effect that will contain information computed from the fiber + * state and status while running on the fiber. + * + * This allows the outside world to interact safely with mutable fiber state + * without locks or immutable data. + */ + ask( + f: (runtime: FiberRuntime, status: FiberStatus.FiberStatus) => Z + ): Effect.Effect { + return core.suspend(() => { + const deferred = core.deferredUnsafeMake(this._fiberId) + this.tell( + FiberMessage.stateful((fiber, status) => { + core.deferredUnsafeDone(deferred, core.sync(() => f(fiber, status))) + }) + ) + return core.deferredAwait(deferred) + }) + } + + /** + * Adds a message to be processed by the fiber on the fiber. + */ + tell(message: FiberMessage.FiberMessage): void { + this._queue.push(message) + if (!this._running) { + this._running = true + this.drainQueueLaterOnExecutor() + } + } + + get await(): Effect.Effect> { + return core.async((resume) => { + const cb = (exit: Exit.Exit) => resume(core.succeed(exit)) + if (this._exitValue !== null) { + cb(this._exitValue!) + return + } + this.tell( + FiberMessage.stateful((fiber, _) => { + if (fiber._exitValue !== null) { + cb(this._exitValue!) + } else { + fiber.addObserver(cb) + } + }) + ) + return core.sync(() => + this.tell( + FiberMessage.stateful((fiber, _) => { + fiber.removeObserver(cb) + }) + ) + ) + }, this.id()) + } + + get inheritAll(): Effect.Effect { + return core.withFiberRuntime((parentFiber, parentStatus) => { + const parentFiberId = parentFiber.id() + const parentFiberRefs = parentFiber.getFiberRefs() + const parentRuntimeFlags = parentStatus.runtimeFlags + const childFiberRefs = this.getFiberRefs() + const updatedFiberRefs = fiberRefs.joinAs(parentFiberRefs, parentFiberId, childFiberRefs) + + parentFiber.setFiberRefs(updatedFiberRefs) + + const updatedRuntimeFlags = parentFiber.getFiberRef(currentRuntimeFlags) + + const patch = pipe( + runtimeFlags_.diff(parentRuntimeFlags, updatedRuntimeFlags), + // Do not inherit WindDown or Interruption! + RuntimeFlagsPatch.exclude(runtimeFlags_.Interruption), + RuntimeFlagsPatch.exclude(runtimeFlags_.WindDown) + ) + + return core.updateRuntimeFlags(patch) + }) + } + + /** + * Tentatively observes the fiber, but returns immediately if it is not + * already done. + */ + get poll(): Effect.Effect>> { + return core.sync(() => Option.fromNullable(this._exitValue)) + } + + /** + * Unsafely observes the fiber, but returns immediately if it is not + * already done. + */ + unsafePoll(): Exit.Exit | null { + return this._exitValue + } + + /** + * In the background, interrupts the fiber as if interrupted from the specified fiber. + */ + interruptAsFork(fiberId: FiberId.FiberId): Effect.Effect { + return core.sync(() => this.tell(FiberMessage.interruptSignal(internalCause.interrupt(fiberId)))) + } + + /** + * In the background, interrupts the fiber as if interrupted from the specified fiber. + */ + unsafeInterruptAsFork(fiberId: FiberId.FiberId) { + this.tell(FiberMessage.interruptSignal(internalCause.interrupt(fiberId))) + } + + /** + * Adds an observer to the list of observers. + * + * **NOTE**: This method must be invoked by the fiber itself. + */ + addObserver(observer: (exit: Exit.Exit) => void): void { + if (this._exitValue !== null) { + observer(this._exitValue!) + } else { + this._observers.push(observer) + } + } + + /** + * Removes the specified observer from the list of observers that will be + * notified when the fiber exits. + * + * **NOTE**: This method must be invoked by the fiber itself. + */ + removeObserver(observer: (exit: Exit.Exit) => void): void { + this._observers = this._observers.filter((o) => o !== observer) + } + /** + * Retrieves all fiber refs of the fiber. + * + * **NOTE**: This method is safe to invoke on any fiber, but if not invoked + * on this fiber, then values derived from the fiber's state (including the + * log annotations and log level) may not be up-to-date. + */ + getFiberRefs(): FiberRefs.FiberRefs { + this.setFiberRef(currentRuntimeFlags, this.currentRuntimeFlags) + return this._fiberRefs + } + + /** + * Deletes the specified fiber ref. + * + * **NOTE**: This method must be invoked by the fiber itself. + */ + unsafeDeleteFiberRef(fiberRef: FiberRef.FiberRef): void { + this._fiberRefs = fiberRefs.delete_(this._fiberRefs, fiberRef) + } + + /** + * Retrieves the state of the fiber ref, or else its initial value. + * + * **NOTE**: This method is safe to invoke on any fiber, but if not invoked + * on this fiber, then values derived from the fiber's state (including the + * log annotations and log level) may not be up-to-date. + */ + getFiberRef(fiberRef: FiberRef.FiberRef): X { + if (this._fiberRefs.locals.has(fiberRef)) { + return this._fiberRefs.locals.get(fiberRef)![0][1] as X + } + return fiberRef.initial + } + + /** + * Sets the fiber ref to the specified value. + * + * **NOTE**: This method must be invoked by the fiber itself. + */ + setFiberRef(fiberRef: FiberRef.FiberRef, value: X): void { + this._fiberRefs = fiberRefs.updateAs(this._fiberRefs, { + fiberId: this._fiberId, + fiberRef, + value + }) + this.refreshRefCache() + } + + refreshRefCache() { + this.currentDefaultServices = this.getFiberRef(defaultServices.currentServices) + this.currentTracer = this.currentDefaultServices.unsafeMap.get(tracer.tracerTag.key) + this.currentSupervisor = this.getFiberRef(currentSupervisor) + this.currentScheduler = this.getFiberRef(currentScheduler) + this.currentContext = this.getFiberRef(core.currentContext) + this.currentSpan = this.currentContext.unsafeMap.get(tracer.spanTag.key) + } + + /** + * Wholesale replaces all fiber refs of this fiber. + * + * **NOTE**: This method must be invoked by the fiber itself. + */ + setFiberRefs(fiberRefs: FiberRefs.FiberRefs): void { + this._fiberRefs = fiberRefs + this.refreshRefCache() + } + + /** + * Adds a reference to the specified fiber inside the children set. + * + * **NOTE**: This method must be invoked by the fiber itself. + */ + addChild(child: FiberRuntime) { + this.getChildren().add(child) + } + + /** + * Removes a reference to the specified fiber inside the children set. + * + * **NOTE**: This method must be invoked by the fiber itself. + */ + removeChild(child: FiberRuntime) { + this.getChildren().delete(child) + } + + /** + * Transfers all children of this fiber that are currently running to the + * specified fiber scope. + * + * **NOTE**: This method must be invoked by the fiber itself after it has + * evaluated the effects but prior to exiting. + */ + transferChildren(scope: fiberScope.FiberScope) { + const children = this._children + // Clear the children of the current fiber + this._children = null + if (children !== null && children.size > 0) { + for (const child of children) { + // If the child is still running, add it to the scope + if (child._exitValue === null) { + scope.add(this.currentRuntimeFlags, child) + } + } + } + } + + /** + * On the current thread, executes all messages in the fiber's inbox. This + * method may return before all work is done, in the event the fiber executes + * an asynchronous operation. + * + * **NOTE**: This method must be invoked by the fiber itself. + */ + drainQueueOnCurrentThread() { + let recurse = true + while (recurse) { + let evaluationSignal: EvaluationSignal = EvaluationSignalContinue + const prev = (globalThis as any)[internalFiber.currentFiberURI] + ;(globalThis as any)[internalFiber.currentFiberURI] = this + try { + while (evaluationSignal === EvaluationSignalContinue) { + evaluationSignal = this._queue.length === 0 ? + EvaluationSignalDone : + this.evaluateMessageWhileSuspended(this._queue.splice(0, 1)[0]!) + } + } finally { + this._running = false + ;(globalThis as any)[internalFiber.currentFiberURI] = prev + } + // Maybe someone added something to the queue between us checking, and us + // giving up the drain. If so, we need to restart the draining, but only + // if we beat everyone else to the restart: + if (this._queue.length > 0 && !this._running) { + this._running = true + if (evaluationSignal === EvaluationSignalYieldNow) { + this.drainQueueLaterOnExecutor() + recurse = false + } else { + recurse = true + } + } else { + recurse = false + } + } + } + + /** + * Schedules the execution of all messages in the fiber's inbox. + * + * This method will return immediately after the scheduling + * operation is completed, but potentially before such messages have been + * executed. + * + * **NOTE**: This method must be invoked by the fiber itself. + */ + drainQueueLaterOnExecutor() { + this.currentScheduler.scheduleTask( + this.run, + this.getFiberRef(core.currentSchedulingPriority), + this + ) + } + + /** + * Drains the fiber's message queue while the fiber is actively running, + * returning the next effect to execute, which may be the input effect if no + * additional effect needs to be executed. + * + * **NOTE**: This method must be invoked by the fiber itself. + */ + drainQueueWhileRunning( + runtimeFlags: RuntimeFlags.RuntimeFlags, + cur0: Effect.Effect + ) { + let cur = cur0 + while (this._queue.length > 0) { + const message = this._queue.splice(0, 1)[0] + // @ts-expect-error + cur = drainQueueWhileRunningTable[message._tag](this, runtimeFlags, cur, message) + } + return cur + } + + /** + * Determines if the fiber is interrupted. + * + * **NOTE**: This method is safe to invoke on any fiber, but if not invoked + * on this fiber, then values derived from the fiber's state (including the + * log annotations and log level) may not be up-to-date. + */ + isInterrupted(): boolean { + return !internalCause.isEmpty(this.getFiberRef(core.currentInterruptedCause)) + } + + /** + * Adds an interruptor to the set of interruptors that are interrupting this + * fiber. + * + * **NOTE**: This method must be invoked by the fiber itself. + */ + addInterruptedCause(cause: Cause.Cause) { + const oldSC = this.getFiberRef(core.currentInterruptedCause) + this.setFiberRef(core.currentInterruptedCause, internalCause.sequential(oldSC, cause)) + } + + /** + * Processes a new incoming interrupt signal. + * + * **NOTE**: This method must be invoked by the fiber itself. + */ + processNewInterruptSignal(cause: Cause.Cause): void { + this.addInterruptedCause(cause) + this.sendInterruptSignalToAllChildren() + } + + /** + * Interrupts all children of the current fiber, returning an effect that will + * await the exit of the children. This method will return null if the fiber + * has no children. + * + * **NOTE**: This method must be invoked by the fiber itself. + */ + sendInterruptSignalToAllChildren(): boolean { + if (this._children === null || this._children.size === 0) { + return false + } + let told = false + for (const child of this._children) { + child.tell(FiberMessage.interruptSignal(internalCause.interrupt(this.id()))) + told = true + } + return told + } + + /** + * Interrupts all children of the current fiber, returning an effect that will + * await the exit of the children. This method will return null if the fiber + * has no children. + * + * **NOTE**: This method must be invoked by the fiber itself. + */ + interruptAllChildren() { + if (this.sendInterruptSignalToAllChildren()) { + const it = this._children!.values() + this._children = null + let isDone = false + const body = () => { + const next = it.next() + if (!next.done) { + return core.asVoid(next.value.await) + } else { + return core.sync(() => { + isDone = true + }) + } + } + return core.whileLoop({ + while: () => !isDone, + body, + step: () => { + // + } + }) + } + return null + } + + reportExitValue(exit: Exit.Exit) { + if (runtimeFlags_.runtimeMetrics(this.currentRuntimeFlags)) { + const tags = this.getFiberRef(core.currentMetricLabels) + const startTimeMillis = this.id().startTimeMillis + const endTimeMillis = Date.now() + fiberLifetimes.unsafeUpdate(endTimeMillis - startTimeMillis, tags) + fiberActive.unsafeUpdate(-1, tags) + switch (exit._tag) { + case OpCodes.OP_SUCCESS: { + fiberSuccesses.unsafeUpdate(1, tags) + break + } + case OpCodes.OP_FAILURE: { + fiberFailures.unsafeUpdate(1, tags) + break + } + } + } + if (exit._tag === "Failure") { + const level = this.getFiberRef(core.currentUnhandledErrorLogLevel) + if (!internalCause.isInterruptedOnly(exit.cause) && level._tag === "Some") { + this.log("Fiber terminated with an unhandled error", exit.cause, level) + } + } + } + + setExitValue(exit: Exit.Exit) { + this._exitValue = exit + this.reportExitValue(exit) + for (let i = this._observers.length - 1; i >= 0; i--) { + this._observers[i](exit) + } + this._observers = [] + } + + getLoggers() { + return this.getFiberRef(currentLoggers) + } + + log( + message: unknown, + cause: Cause.Cause, + overrideLogLevel: Option.Option + ): void { + const logLevel = Option.isSome(overrideLogLevel) ? + overrideLogLevel.value : + this.getFiberRef(core.currentLogLevel) + const minimumLogLevel = this.getFiberRef(currentMinimumLogLevel) + if (LogLevel.greaterThan(minimumLogLevel, logLevel)) { + return + } + const spans = this.getFiberRef(core.currentLogSpan) + const annotations = this.getFiberRef(core.currentLogAnnotations) + const loggers = this.getLoggers() + const contextMap = this.getFiberRefs() + if (HashSet.size(loggers) > 0) { + const clockService = Context.get(this.getFiberRef(defaultServices.currentServices), clock.clockTag) + const date = new Date(clockService.unsafeCurrentTimeMillis()) + Inspectable.withRedactableContext(contextMap, () => { + for (const logger of loggers) { + logger.log({ + fiberId: this.id(), + logLevel, + message, + cause, + context: contextMap, + spans, + annotations, + date + }) + } + }) + } + } + + /** + * Evaluates a single message on the current thread, while the fiber is + * suspended. This method should only be called while evaluation of the + * fiber's effect is suspended due to an asynchronous operation. + * + * **NOTE**: This method must be invoked by the fiber itself. + */ + evaluateMessageWhileSuspended(message: FiberMessage.FiberMessage): EvaluationSignal { + switch (message._tag) { + case FiberMessage.OP_YIELD_NOW: { + return EvaluationSignalYieldNow + } + case FiberMessage.OP_INTERRUPT_SIGNAL: { + this.processNewInterruptSignal(message.cause) + if (this._asyncInterruptor !== null) { + this._asyncInterruptor(core.exitFailCause(message.cause)) + this._asyncInterruptor = null + } + return EvaluationSignalContinue + } + case FiberMessage.OP_RESUME: { + this._asyncInterruptor = null + this._asyncBlockingOn = null + this.evaluateEffect(message.effect) + return EvaluationSignalContinue + } + case FiberMessage.OP_STATEFUL: { + message.onFiber( + this, + this._exitValue !== null ? + FiberStatus.done : + FiberStatus.suspended(this.currentRuntimeFlags, this._asyncBlockingOn!) + ) + return EvaluationSignalContinue + } + default: { + return absurd(message) + } + } + } + + /** + * Evaluates an effect until completion, potentially asynchronously. + * + * **NOTE**: This method must be invoked by the fiber itself. + */ + evaluateEffect(effect0: Effect.Effect) { + this.currentSupervisor.onResume(this) + try { + let effect: Effect.Effect | null = + runtimeFlags_.interruptible(this.currentRuntimeFlags) && this.isInterrupted() ? + core.exitFailCause(this.getInterruptedCause()) : + effect0 + while (effect !== null) { + const eff: Effect.Effect = effect + const exit = this.runLoop(eff) + if (exit === YieldedOp) { + const op = yieldedOpChannel.currentOp! + yieldedOpChannel.currentOp = null + if (op._op === OpCodes.OP_YIELD) { + if (runtimeFlags_.cooperativeYielding(this.currentRuntimeFlags)) { + this.tell(FiberMessage.yieldNow()) + this.tell(FiberMessage.resume(core.exitVoid)) + effect = null + } else { + effect = core.exitVoid + } + } else if (op._op === OpCodes.OP_ASYNC) { + // Terminate this evaluation, async resumption will continue evaluation: + effect = null + } + } else { + this.currentRuntimeFlags = pipe(this.currentRuntimeFlags, runtimeFlags_.enable(runtimeFlags_.WindDown)) + const interruption = this.interruptAllChildren() + if (interruption !== null) { + effect = core.flatMap(interruption, () => exit) + } else { + if (this._queue.length === 0) { + // No more messages to process, so we will allow the fiber to end life: + this.setExitValue(exit) + } else { + // There are messages, possibly added by the final op executed by + // the fiber. To be safe, we should execute those now before we + // allow the fiber to end life: + this.tell(FiberMessage.resume(exit)) + } + effect = null + } + } + } + } finally { + this.currentSupervisor.onSuspend(this) + } + } + + /** + * Begins execution of the effect associated with this fiber on the current + * thread. This can be called to "kick off" execution of a fiber after it has + * been created, in hopes that the effect can be executed synchronously. + * + * This is not the normal way of starting a fiber, but it is useful when the + * express goal of executing the fiber is to synchronously produce its exit. + */ + start(effect: Effect.Effect): void { + if (!this._running) { + this._running = true + const prev = (globalThis as any)[internalFiber.currentFiberURI] + ;(globalThis as any)[internalFiber.currentFiberURI] = this + try { + this.evaluateEffect(effect) + } finally { + this._running = false + ;(globalThis as any)[internalFiber.currentFiberURI] = prev + // Because we're special casing `start`, we have to be responsible + // for spinning up the fiber if there were new messages added to + // the queue between the completion of the effect and the transition + // to the not running state. + if (this._queue.length > 0) { + this.drainQueueLaterOnExecutor() + } + } + } else { + this.tell(FiberMessage.resume(effect)) + } + } + + /** + * Begins execution of the effect associated with this fiber on in the + * background, and on the correct thread pool. This can be called to "kick + * off" execution of a fiber after it has been created, in hopes that the + * effect can be executed synchronously. + */ + startFork(effect: Effect.Effect): void { + this.tell(FiberMessage.resume(effect)) + } + + /** + * Takes the current runtime flags, patches them to return the new runtime + * flags, and then makes any changes necessary to fiber state based on the + * specified patch. + * + * **NOTE**: This method must be invoked by the fiber itself. + */ + patchRuntimeFlags(oldRuntimeFlags: RuntimeFlags.RuntimeFlags, patch: RuntimeFlagsPatch.RuntimeFlagsPatch) { + const newRuntimeFlags = runtimeFlags_.patch(oldRuntimeFlags, patch) + ;(globalThis as any)[internalFiber.currentFiberURI] = this + this.currentRuntimeFlags = newRuntimeFlags + return newRuntimeFlags + } + + /** + * Initiates an asynchronous operation, by building a callback that will + * resume execution, and then feeding that callback to the registration + * function, handling error cases and repeated resumptions appropriately. + * + * **NOTE**: This method must be invoked by the fiber itself. + */ + initiateAsync( + runtimeFlags: RuntimeFlags.RuntimeFlags, + asyncRegister: (resume: (effect: Effect.Effect) => void) => void + ) { + let alreadyCalled = false + const callback = (effect: Effect.Effect) => { + if (!alreadyCalled) { + alreadyCalled = true + this.tell(FiberMessage.resume(effect)) + } + } + if (runtimeFlags_.interruptible(runtimeFlags)) { + this._asyncInterruptor = callback + } + try { + asyncRegister(callback) + } catch (e) { + callback(core.failCause(internalCause.die(e))) + } + } + + pushStack(cont: core.Continuation) { + this._stack.push(cont) + if (cont._op === "OnStep") { + this._steps.push({ refs: this.getFiberRefs(), flags: this.currentRuntimeFlags }) + } + } + + popStack() { + const item = this._stack.pop() + if (item) { + if (item._op === "OnStep") { + this._steps.pop() + } + return item + } + return + } + + getNextSuccessCont() { + let frame = this.popStack() + while (frame) { + if (frame._op !== OpCodes.OP_ON_FAILURE) { + return frame + } + frame = this.popStack() + } + } + + getNextFailCont() { + let frame = this.popStack() + while (frame) { + if (frame._op !== OpCodes.OP_ON_SUCCESS && frame._op !== OpCodes.OP_WHILE && frame._op !== OpCodes.OP_ITERATOR) { + return frame + } + frame = this.popStack() + } + } + + [OpCodes.OP_TAG](op: core.Primitive & { _op: OpCodes.OP_SYNC }) { + return core.sync(() => Context.unsafeGet(this.currentContext, op as unknown as Context.Tag)) + } + + ["Left"](op: core.Primitive & { _op: "Left" }) { + return core.fail(op.left) + } + + ["None"](_: core.Primitive & { _op: "None" }) { + return core.fail(new core.NoSuchElementException()) + } + + ["Right"](op: core.Primitive & { _op: "Right" }) { + return core.exitSucceed(op.right) + } + + ["Some"](op: core.Primitive & { _op: "Some" }) { + return core.exitSucceed(op.value) + } + + ["Micro"](op: Micro.Micro & { _op: "Micro" }) { + return core.unsafeAsync((microResume) => { + let resume = microResume + const fiber = Micro.runFork(Micro.provideContext(op, this.currentContext)) + fiber.addObserver((exit) => { + if (exit._tag === "Success") { + return resume(core.exitSucceed(exit.value)) + } + switch (exit.cause._tag) { + case "Interrupt": { + return resume(core.exitFailCause(internalCause.interrupt(FiberId.none))) + } + case "Fail": { + return resume(core.fail(exit.cause.error)) + } + case "Die": { + return resume(core.die(exit.cause.defect)) + } + } + }) + return core.unsafeAsync((abortResume) => { + resume = (_: any) => { + abortResume(core.void) + } + fiber.unsafeInterrupt() + }) + }) + } + + [OpCodes.OP_SYNC](op: core.Primitive & { _op: OpCodes.OP_SYNC }) { + const value = internalCall(() => op.effect_instruction_i0()) + const cont = this.getNextSuccessCont() + if (cont !== undefined) { + if (!(cont._op in contOpSuccess)) { + // @ts-expect-error + absurd(cont) + } + // @ts-expect-error + return contOpSuccess[cont._op](this, cont, value) + } else { + yieldedOpChannel.currentOp = core.exitSucceed(value) as any + return YieldedOp + } + } + + [OpCodes.OP_SUCCESS](op: core.Primitive & { _op: OpCodes.OP_SUCCESS }) { + const oldCur = op + const cont = this.getNextSuccessCont() + if (cont !== undefined) { + if (!(cont._op in contOpSuccess)) { + // @ts-expect-error + absurd(cont) + } + // @ts-expect-error + return contOpSuccess[cont._op](this, cont, oldCur.effect_instruction_i0) + } else { + yieldedOpChannel.currentOp = oldCur + return YieldedOp + } + } + + [OpCodes.OP_FAILURE](op: core.Primitive & { _op: OpCodes.OP_FAILURE }) { + const cause = op.effect_instruction_i0 + const cont = this.getNextFailCont() + if (cont !== undefined) { + switch (cont._op) { + case OpCodes.OP_ON_FAILURE: + case OpCodes.OP_ON_SUCCESS_AND_FAILURE: { + if (!(runtimeFlags_.interruptible(this.currentRuntimeFlags) && this.isInterrupted())) { + return internalCall(() => cont.effect_instruction_i1(cause)) + } else { + return core.exitFailCause(internalCause.stripFailures(cause)) + } + } + case "OnStep": { + if (!(runtimeFlags_.interruptible(this.currentRuntimeFlags) && this.isInterrupted())) { + return core.exitSucceed(core.exitFailCause(cause)) + } else { + return core.exitFailCause(internalCause.stripFailures(cause)) + } + } + case OpCodes.OP_REVERT_FLAGS: { + this.patchRuntimeFlags(this.currentRuntimeFlags, cont.patch) + if (runtimeFlags_.interruptible(this.currentRuntimeFlags) && this.isInterrupted()) { + return core.exitFailCause(internalCause.sequential(cause, this.getInterruptedCause())) + } else { + return core.exitFailCause(cause) + } + } + default: { + absurd(cont) + } + } + } else { + yieldedOpChannel.currentOp = core.exitFailCause(cause) as any + return YieldedOp + } + } + + [OpCodes.OP_WITH_RUNTIME](op: core.Primitive & { _op: OpCodes.OP_WITH_RUNTIME }) { + return internalCall(() => + op.effect_instruction_i0( + this as FiberRuntime, + FiberStatus.running(this.currentRuntimeFlags) as FiberStatus.Running + ) + ) + } + + ["Blocked"](op: core.Primitive & { _op: "Blocked" }) { + const refs = this.getFiberRefs() + const flags = this.currentRuntimeFlags + if (this._steps.length > 0) { + const frames: Array = [] + const snap = this._steps[this._steps.length - 1] + let frame = this.popStack() + while (frame && frame._op !== "OnStep") { + frames.push(frame) + frame = this.popStack() + } + this.setFiberRefs(snap.refs) + this.currentRuntimeFlags = snap.flags + const patchRefs = FiberRefsPatch.diff(snap.refs, refs) + const patchFlags = runtimeFlags_.diff(snap.flags, flags) + return core.exitSucceed(core.blocked( + op.effect_instruction_i0, + core.withFiberRuntime((newFiber) => { + while (frames.length > 0) { + newFiber.pushStack(frames.pop()!) + } + newFiber.setFiberRefs( + FiberRefsPatch.patch(newFiber.id(), newFiber.getFiberRefs())(patchRefs) + ) + newFiber.currentRuntimeFlags = runtimeFlags_.patch(patchFlags)(newFiber.currentRuntimeFlags) + return op.effect_instruction_i1 + }) + )) + } + return core.uninterruptibleMask((restore) => + core.flatMap( + forkDaemon(core.runRequestBlock(op.effect_instruction_i0)), + () => restore(op.effect_instruction_i1) + ) + ) + } + + ["RunBlocked"](op: core.Primitive & { _op: "RunBlocked" }) { + return runBlockedRequests(op.effect_instruction_i0) + } + + [OpCodes.OP_UPDATE_RUNTIME_FLAGS](op: core.Primitive & { _op: OpCodes.OP_UPDATE_RUNTIME_FLAGS }) { + const updateFlags = op.effect_instruction_i0 + const oldRuntimeFlags = this.currentRuntimeFlags + const newRuntimeFlags = runtimeFlags_.patch(oldRuntimeFlags, updateFlags) + // One more chance to short circuit: if we're immediately going + // to interrupt. Interruption will cause immediate reversion of + // the flag, so as long as we "peek ahead", there's no need to + // set them to begin with. + if (runtimeFlags_.interruptible(newRuntimeFlags) && this.isInterrupted()) { + return core.exitFailCause(this.getInterruptedCause()) + } else { + // Impossible to short circuit, so record the changes + this.patchRuntimeFlags(this.currentRuntimeFlags, updateFlags) + if (op.effect_instruction_i1) { + // Since we updated the flags, we need to revert them + const revertFlags = runtimeFlags_.diff(newRuntimeFlags, oldRuntimeFlags) + this.pushStack(new core.RevertFlags(revertFlags, op)) + return internalCall(() => op.effect_instruction_i1!(oldRuntimeFlags)) + } else { + return core.exitVoid + } + } + } + + [OpCodes.OP_ON_SUCCESS](op: core.Primitive & { _op: OpCodes.OP_ON_SUCCESS }) { + this.pushStack(op) + return op.effect_instruction_i0 + } + + ["OnStep"](op: core.Primitive & { _op: "OnStep" }) { + this.pushStack(op) + return op.effect_instruction_i0 + } + + [OpCodes.OP_ON_FAILURE](op: core.Primitive & { _op: OpCodes.OP_ON_FAILURE }) { + this.pushStack(op) + return op.effect_instruction_i0 + } + + [OpCodes.OP_ON_SUCCESS_AND_FAILURE](op: core.Primitive & { _op: OpCodes.OP_ON_SUCCESS_AND_FAILURE }) { + this.pushStack(op) + return op.effect_instruction_i0 + } + + [OpCodes.OP_ASYNC](op: core.Primitive & { _op: OpCodes.OP_ASYNC }) { + this._asyncBlockingOn = op.effect_instruction_i1 + this.initiateAsync(this.currentRuntimeFlags, op.effect_instruction_i0) + yieldedOpChannel.currentOp = op + return YieldedOp + } + + [OpCodes.OP_YIELD](op: core.Primitive & { op: OpCodes.OP_YIELD }) { + this._isYielding = false + yieldedOpChannel.currentOp = op + return YieldedOp + } + + [OpCodes.OP_WHILE](op: core.Primitive & { _op: OpCodes.OP_WHILE }) { + const check = op.effect_instruction_i0 + const body = op.effect_instruction_i1 + if (check()) { + this.pushStack(op) + return body() + } else { + return core.exitVoid + } + } + + [OpCodes.OP_ITERATOR](op: core.Primitive & { _op: OpCodes.OP_ITERATOR }) { + return contOpSuccess[OpCodes.OP_ITERATOR](this, op, undefined) + } + + [OpCodes.OP_COMMIT](op: core.Primitive & { _op: OpCodes.OP_COMMIT }) { + return internalCall(() => op.commit()) + } + + /** + * The main run-loop for evaluating effects. + * + * **NOTE**: This method must be invoked by the fiber itself. + */ + runLoop(effect0: Effect.Effect): Exit.Exit | YieldedOp { + let cur: Effect.Effect | YieldedOp = effect0 + this.currentOpCount = 0 + + while (true) { + if ((this.currentRuntimeFlags & OpSupervision) !== 0) { + this.currentSupervisor.onEffect(this, cur) + } + if (this._queue.length > 0) { + cur = this.drainQueueWhileRunning(this.currentRuntimeFlags, cur) + } + if (!this._isYielding) { + this.currentOpCount += 1 + const shouldYield = this.currentScheduler.shouldYield(this) + if (shouldYield !== false) { + this._isYielding = true + this.currentOpCount = 0 + const oldCur = cur + cur = core.flatMap(core.yieldNow({ priority: shouldYield }), () => oldCur) + } + } + try { + // @ts-expect-error + cur = this.currentTracer.context( + () => { + if (_version !== (cur as core.Primitive)[core.EffectTypeId]._V) { + const level = this.getFiberRef(core.currentVersionMismatchErrorLogLevel) + if (level._tag === "Some") { + const effectVersion = (cur as core.Primitive)[core.EffectTypeId]._V + this.log( + `Executing an Effect versioned ${effectVersion} with a Runtime of version ${version.getCurrentVersion()}, you may want to dedupe the effect dependencies, you can use the language service plugin to detect this at compile time: https://github.com/Effect-TS/language-service`, + internalCause.empty, + level + ) + } + } + // @ts-expect-error + return this[(cur as core.Primitive)._op](cur as core.Primitive) + }, + this + ) + + if (cur === YieldedOp) { + const op = yieldedOpChannel.currentOp! + if ( + op._op === OpCodes.OP_YIELD || + op._op === OpCodes.OP_ASYNC + ) { + return YieldedOp + } + + yieldedOpChannel.currentOp = null + return ( + op._op === OpCodes.OP_SUCCESS || + op._op === OpCodes.OP_FAILURE + ) ? + op as unknown as Exit.Exit : + core.exitFailCause(internalCause.die(op)) + } + } catch (e) { + if (cur !== YieldedOp && !Predicate.hasProperty(cur, "_op") || !((cur as core.Primitive)._op in this)) { + cur = core.dieMessage(`Not a valid effect: ${Inspectable.toStringUnknown(cur)}`) + } else if (core.isInterruptedException(e)) { + cur = core.exitFailCause( + internalCause.sequential(internalCause.die(e), internalCause.interrupt(FiberId.none)) + ) + } else { + cur = core.die(e) + } + } + } + } + + run = () => { + this.drainQueueOnCurrentThread() + } +} + +// circular with Logger + +/** @internal */ +export const currentMinimumLogLevel: FiberRef.FiberRef = globalValue( + "effect/FiberRef/currentMinimumLogLevel", + () => core.fiberRefUnsafeMake(LogLevel.fromLiteral("Info")) +) + +/** @internal */ +export const loggerWithConsoleLog = (self: Logger): Logger => + internalLogger.makeLogger((opts) => { + const services = FiberRefs.getOrDefault(opts.context, defaultServices.currentServices) + Context.get(services, consoleTag).unsafe.log(self.log(opts)) + }) + +/** @internal */ +export const loggerWithLeveledLog = (self: Logger): Logger => + internalLogger.makeLogger((opts) => { + const services = FiberRefs.getOrDefault(opts.context, defaultServices.currentServices) + const unsafeLogger = Context.get(services, consoleTag).unsafe + switch (opts.logLevel._tag) { + case "Debug": + return unsafeLogger.debug(self.log(opts)) + case "Info": + return unsafeLogger.info(self.log(opts)) + case "Trace": + return unsafeLogger.trace(self.log(opts)) + case "Warning": + return unsafeLogger.warn(self.log(opts)) + case "Error": + case "Fatal": + return unsafeLogger.error(self.log(opts)) + default: + return unsafeLogger.log(self.log(opts)) + } + }) + +/** @internal */ +export const loggerWithConsoleError = (self: Logger): Logger => + internalLogger.makeLogger((opts) => { + const services = FiberRefs.getOrDefault(opts.context, defaultServices.currentServices) + Context.get(services, consoleTag).unsafe.error(self.log(opts)) + }) + +/** @internal */ +export const defaultLogger: Logger = globalValue( + Symbol.for("effect/Logger/defaultLogger"), + () => loggerWithConsoleLog(internalLogger.stringLogger) +) + +/** @internal */ +export const jsonLogger: Logger = globalValue( + Symbol.for("effect/Logger/jsonLogger"), + () => loggerWithConsoleLog(internalLogger.jsonLogger) +) + +/** @internal */ +export const logFmtLogger: Logger = globalValue( + Symbol.for("effect/Logger/logFmtLogger"), + () => loggerWithConsoleLog(internalLogger.logfmtLogger) +) + +/** @internal */ +export const prettyLogger: Logger = globalValue( + Symbol.for("effect/Logger/prettyLogger"), + () => internalLogger.prettyLoggerDefault +) + +/** @internal */ +export const structuredLogger: Logger = globalValue( + Symbol.for("effect/Logger/structuredLogger"), + () => loggerWithConsoleLog(internalLogger.structuredLogger) +) + +/** @internal */ +export const tracerLogger = globalValue( + Symbol.for("effect/Logger/tracerLogger"), + () => + internalLogger.makeLogger(({ + annotations, + cause, + context, + fiberId, + logLevel, + message + }) => { + const span = internalEffect.filterDisablePropagation(Context.getOption( + fiberRefs.getOrDefault(context, core.currentContext), + tracer.spanTag + )) + + if (span._tag === "None" || span.value._tag === "ExternalSpan") { + return + } + + const clockService = Context.unsafeGet( + fiberRefs.getOrDefault(context, defaultServices.currentServices), + clock.clockTag + ) + + const attributes: Record = {} + for (const [key, value] of annotations) { + attributes[key] = value + } + attributes["effect.fiberId"] = FiberId.threadName(fiberId) + attributes["effect.logLevel"] = logLevel.label + + if (cause !== null && cause._tag !== "Empty") { + attributes["effect.cause"] = internalCause.pretty(cause, { renderErrorCause: true }) + } + + span.value.event( + Inspectable.toStringUnknown(Array.isArray(message) && message.length === 1 ? message[0] : message), + clockService.unsafeCurrentTimeNanos(), + attributes + ) + }) +) + +/** @internal */ +export const loggerWithSpanAnnotations = (self: Logger): Logger => + internalLogger.mapInputOptions(self, (options: Logger.Options) => { + const span = Option.flatMap(fiberRefs.get(options.context, core.currentContext), Context.getOption(tracer.spanTag)) + if (span._tag === "None") { + return options + } + return { + ...options, + annotations: pipe( + options.annotations, + HashMap.set("effect.traceId", span.value.traceId as unknown), + HashMap.set("effect.spanId", span.value.spanId as unknown), + span.value._tag === "Span" ? HashMap.set("effect.spanName", span.value.name as unknown) : identity + ) + } + }) + +/** @internal */ +export const currentLoggers: FiberRef.FiberRef< + HashSet.HashSet> +> = globalValue( + Symbol.for("effect/FiberRef/currentLoggers"), + () => core.fiberRefUnsafeMakeHashSet(HashSet.make(defaultLogger, tracerLogger)) +) + +/** @internal */ +export const batchedLogger = dual< + ( + window: Duration.DurationInput, + f: (messages: Array>) => Effect.Effect + ) => ( + self: Logger + ) => Effect.Effect, never, Scope.Scope | R>, + ( + self: Logger, + window: Duration.DurationInput, + f: (messages: Array>) => Effect.Effect + ) => Effect.Effect, never, Scope.Scope | R> +>(3, ( + self: Logger, + window: Duration.DurationInput, + f: (messages: Array>) => Effect.Effect +): Effect.Effect, never, Scope.Scope | R> => + core.flatMap(scope, (scope) => { + let buffer: Array = [] + const flush = core.suspend(() => { + if (buffer.length === 0) { + return core.void + } + const arr = buffer + buffer = [] + return f(arr) + }) + + return core.uninterruptibleMask((restore) => + pipe( + internalEffect.sleep(window), + core.zipRight(flush), + internalEffect.forever, + restore, + forkDaemon, + core.flatMap((fiber) => core.scopeAddFinalizer(scope, core.interruptFiber(fiber))), + core.zipRight(addFinalizer(() => flush)), + core.as( + internalLogger.makeLogger((options) => { + buffer.push(self.log(options)) + }) + ) + ) + ) + })) + +export const annotateLogsScoped: { + (key: string, value: unknown): Effect.Effect + (values: Record): Effect.Effect +} = function() { + if (typeof arguments[0] === "string") { + return fiberRefLocallyScopedWith( + core.currentLogAnnotations, + HashMap.set(arguments[0], arguments[1]) + ) + } + const entries = Object.entries(arguments[0]) + return fiberRefLocallyScopedWith( + core.currentLogAnnotations, + HashMap.mutate((annotations) => { + for (let i = 0; i < entries.length; i++) { + const [key, value] = entries[i] + HashMap.set(annotations, key, value) + } + return annotations + }) + ) +} + +/** @internal */ +export const whenLogLevel = dual< + ( + level: LogLevel.LogLevel | LogLevel.Literal + ) => (effect: Effect.Effect) => Effect.Effect, E, R>, + ( + effect: Effect.Effect, + level: LogLevel.LogLevel | LogLevel.Literal + ) => Effect.Effect, E, R> +>(2, (effect, level) => { + const requiredLogLevel = typeof level === "string" ? LogLevel.fromLiteral(level) : level + + return core.withFiberRuntime((fiberState) => { + const minimumLogLevel = fiberState.getFiberRef(currentMinimumLogLevel) + + // Imitate the behaviour of `FiberRuntime.log` + if (LogLevel.greaterThan(minimumLogLevel, requiredLogLevel)) { + return core.succeed(Option.none()) + } + + return core.map(effect, Option.some) + }) +}) + +// circular with Effect + +/* @internal */ +export const acquireRelease: { + ( + release: (a: A, exit: Exit.Exit) => Effect.Effect + ): (acquire: Effect.Effect) => Effect.Effect + ( + acquire: Effect.Effect, + release: (a: A, exit: Exit.Exit) => Effect.Effect + ): Effect.Effect +} = dual((args) => core.isEffect(args[0]), (acquire, release) => + core.uninterruptible( + core.tap(acquire, (a) => addFinalizer((exit) => release(a, exit))) + )) + +/* @internal */ +export const acquireReleaseInterruptible: { + ( + release: (exit: Exit.Exit) => Effect.Effect + ): (acquire: Effect.Effect) => Effect.Effect + ( + acquire: Effect.Effect, + release: (exit: Exit.Exit) => Effect.Effect + ): Effect.Effect +} = dual((args) => core.isEffect(args[0]), (acquire, release) => + ensuring( + acquire, + addFinalizer((exit) => release(exit)) + )) + +/* @internal */ +export const addFinalizer = ( + finalizer: (exit: Exit.Exit) => Effect.Effect +): Effect.Effect => + core.withFiberRuntime( + (runtime) => { + const acquireRefs = runtime.getFiberRefs() + const acquireFlags = runtimeFlags_.disable(runtime.currentRuntimeFlags, runtimeFlags_.Interruption) + return core.flatMap(scope, (scope) => + core.scopeAddFinalizerExit(scope, (exit) => + core.withFiberRuntime((runtimeFinalizer) => { + const preRefs = runtimeFinalizer.getFiberRefs() + const preFlags = runtimeFinalizer.currentRuntimeFlags + const patchRefs = FiberRefsPatch.diff(preRefs, acquireRefs) + const patchFlags = runtimeFlags_.diff(preFlags, acquireFlags) + const inverseRefs = FiberRefsPatch.diff(acquireRefs, preRefs) + runtimeFinalizer.setFiberRefs( + FiberRefsPatch.patch(patchRefs, runtimeFinalizer.id(), acquireRefs) + ) + + return ensuring( + core.withRuntimeFlags(finalizer(exit) as Effect.Effect, patchFlags), + core.sync(() => { + runtimeFinalizer.setFiberRefs( + FiberRefsPatch.patch(inverseRefs, runtimeFinalizer.id(), runtimeFinalizer.getFiberRefs()) + ) + }) + ) + }))) + } + ) + +/* @internal */ +export const daemonChildren = (self: Effect.Effect): Effect.Effect => { + const forkScope = core.fiberRefLocally(core.currentForkScopeOverride, Option.some(fiberScope.globalScope)) + return forkScope(self) +} + +/** @internal */ +const _existsParFound = Symbol.for("effect/Effect/existsPar/found") + +/* @internal */ +export const exists: { + (predicate: (a: A, i: number) => Effect.Effect, options?: { + readonly concurrency?: Concurrency | undefined + readonly batching?: boolean | "inherit" | undefined + readonly concurrentFinalizers?: boolean | undefined + }): (elements: Iterable) => Effect.Effect + (elements: Iterable, predicate: (a: A, i: number) => Effect.Effect, options?: { + readonly concurrency?: Concurrency | undefined + readonly batching?: boolean | "inherit" | undefined + readonly concurrentFinalizers?: boolean | undefined + }): Effect.Effect +} = dual( + (args) => Predicate.isIterable(args[0]) && !core.isEffect(args[0]), + (elements: Iterable, predicate: (a: A, i: number) => Effect.Effect, options?: { + readonly concurrency?: Concurrency | undefined + readonly batching?: boolean | "inherit" | undefined + }) => + concurrency.matchSimple( + options?.concurrency, + () => core.suspend(() => existsLoop(elements[Symbol.iterator](), 0, predicate)), + () => + core.matchEffect( + forEach( + elements, + (a, i) => core.if_(predicate(a, i), { onTrue: () => core.fail(_existsParFound), onFalse: () => core.void }), + options + ), + { + onFailure: (e) => e === _existsParFound ? core.succeed(true) : core.fail(e), + onSuccess: () => core.succeed(false) + } + ) + ) +) + +const existsLoop = ( + iterator: Iterator, + index: number, + f: (a: A, i: number) => Effect.Effect +): Effect.Effect => { + const next = iterator.next() + if (next.done) { + return core.succeed(false) + } + return core.flatMap( + f(next.value, index), + (b) => b ? core.succeed(b) : existsLoop(iterator, index + 1, f) + ) +} + +/* @internal */ +export const filter = dual< + ( + predicate: (a: NoInfer, i: number) => Effect.Effect, + options?: { + readonly concurrency?: Concurrency | undefined + readonly batching?: boolean | "inherit" | undefined + readonly negate?: boolean | undefined + readonly concurrentFinalizers?: boolean | undefined + } + ) => (elements: Iterable) => Effect.Effect, E, R>, + (elements: Iterable, predicate: (a: NoInfer, i: number) => Effect.Effect, options?: { + readonly concurrency?: Concurrency | undefined + readonly batching?: boolean | "inherit" | undefined + readonly negate?: boolean | undefined + readonly concurrentFinalizers?: boolean | undefined + }) => Effect.Effect, E, R> +>( + (args) => Predicate.isIterable(args[0]) && !core.isEffect(args[0]), + (elements: Iterable, predicate: (a: NoInfer, i: number) => Effect.Effect, options?: { + readonly concurrency?: Concurrency | undefined + readonly batching?: boolean | "inherit" | undefined + readonly negate?: boolean | undefined + readonly concurrentFinalizers?: boolean | undefined + }) => { + const predicate_ = options?.negate ? (a: A, i: number) => core.map(predicate(a, i), Boolean.not) : predicate + return concurrency.matchSimple( + options?.concurrency, + () => + core.suspend(() => + RA.fromIterable(elements).reduceRight( + (effect, a, i) => + core.zipWith( + effect, + core.suspend(() => predicate_(a, i)), + (list, b) => b ? [a, ...list] : list + ), + core.sync(() => new Array()) as Effect.Effect, E, R> + ) + ), + () => + core.map( + forEach( + elements, + (a, i) => core.map(predicate_(a, i), (b) => (b ? Option.some(a) : Option.none())), + options + ), + RA.getSomes + ) + ) + } +) + +// === all + +const allResolveInput = ( + input: Iterable> | Record> +): [Iterable>, Option.Option<(as: ReadonlyArray) => any>] => { + if (Array.isArray(input) || Predicate.isIterable(input)) { + return [input, Option.none()] + } + const keys = Object.keys(input) + const size = keys.length + return [ + keys.map((k) => input[k]), + Option.some((values: ReadonlyArray) => { + const res = {} + for (let i = 0; i < size; i++) { + ;(res as any)[keys[i]] = values[i] + } + return res + }) + ] +} + +const allValidate = ( + effects: Iterable>, + reconcile: Option.Option<(as: ReadonlyArray) => any>, + options?: { + readonly concurrency?: Concurrency | undefined + readonly batching?: boolean | "inherit" | undefined + readonly discard?: boolean | undefined + readonly mode?: "default" | "validate" | "either" | undefined + readonly concurrentFinalizers?: boolean | undefined + } +) => { + const eitherEffects: Array, never, unknown>> = [] + for (const effect of effects) { + eitherEffects.push(core.either(effect)) + } + return core.flatMap( + forEach(eitherEffects, identity, { + concurrency: options?.concurrency, + batching: options?.batching, + concurrentFinalizers: options?.concurrentFinalizers + }), + (eithers) => { + const none = Option.none() + const size = eithers.length + const errors: Array = new Array(size) + const successes: Array = new Array(size) + let errored = false + for (let i = 0; i < size; i++) { + const either = eithers[i] as Either.Either + if (either._tag === "Left") { + errors[i] = Option.some(either.left) + errored = true + } else { + successes[i] = either.right + errors[i] = none + } + } + if (errored) { + return reconcile._tag === "Some" ? + core.fail(reconcile.value(errors)) : + core.fail(errors) + } else if (options?.discard) { + return core.void + } + return reconcile._tag === "Some" ? + core.succeed(reconcile.value(successes)) : + core.succeed(successes) + } + ) +} + +const allEither = ( + effects: Iterable>, + reconcile: Option.Option<(as: ReadonlyArray) => any>, + options?: { + readonly concurrency?: Concurrency | undefined + readonly batching?: boolean | "inherit" | undefined + readonly discard?: boolean | undefined + readonly mode?: "default" | "validate" | "either" | undefined + readonly concurrentFinalizers?: boolean | undefined + } +) => { + const eitherEffects: Array, never, unknown>> = [] + for (const effect of effects) { + eitherEffects.push(core.either(effect)) + } + + if (options?.discard) { + return forEach(eitherEffects, identity, { + concurrency: options?.concurrency, + batching: options?.batching, + discard: true, + concurrentFinalizers: options?.concurrentFinalizers + }) + } + + return core.map( + forEach(eitherEffects, identity, { + concurrency: options?.concurrency, + batching: options?.batching, + concurrentFinalizers: options?.concurrentFinalizers + }), + (eithers) => + reconcile._tag === "Some" ? + reconcile.value(eithers) : + eithers + ) +} + +/* @internal */ +export const all = < + const Arg extends Iterable> | Record>, + O extends NoExcessProperties<{ + readonly concurrency?: Concurrency | undefined + readonly batching?: boolean | "inherit" | undefined + readonly discard?: boolean | undefined + readonly mode?: "default" | "validate" | "either" | undefined + readonly concurrentFinalizers?: boolean | undefined + }, O> +>( + arg: Arg, + options?: O +): Effect.All.Return => { + const [effects, reconcile] = allResolveInput(arg) + + if (options?.mode === "validate") { + return allValidate(effects, reconcile, options) as any + } else if (options?.mode === "either") { + return allEither(effects, reconcile, options) as any + } + + return options?.discard !== true && reconcile._tag === "Some" + ? core.map( + forEach(effects, identity, options as any), + reconcile.value + ) as any + : forEach(effects, identity, options as any) as any +} + +/* @internal */ +export const allWith = < + O extends NoExcessProperties<{ + readonly concurrency?: Concurrency | undefined + readonly batching?: boolean | "inherit" | undefined + readonly discard?: boolean | undefined + readonly mode?: "default" | "validate" | "either" | undefined + readonly concurrentFinalizers?: boolean | undefined + }, O> +>(options?: O) => +> | Record>>( + arg: Arg +): Effect.All.Return => all(arg, options) + +/* @internal */ +export const allSuccesses = >( + elements: Iterable, + options?: { + readonly concurrency?: Concurrency | undefined + readonly batching?: boolean | "inherit" | undefined + readonly concurrentFinalizers?: boolean | undefined + } +): Effect.Effect>, never, Effect.Effect.Context> => + core.map( + all(RA.fromIterable(elements).map(core.exit), options), + RA.filterMap((exit) => core.exitIsSuccess(exit) ? Option.some(exit.effect_instruction_i0) : Option.none()) + ) + +/* @internal */ +export const replicate = dual< + (n: number) => (self: Effect.Effect) => Array>, + (self: Effect.Effect, n: number) => Array> +>(2, (self, n) => Array.from({ length: n }, () => self)) + +/* @internal */ +export const replicateEffect: { + ( + n: number, + options?: { + readonly concurrency?: Concurrency | undefined + readonly batching?: boolean | "inherit" | undefined + readonly discard?: false | undefined + readonly concurrentFinalizers?: boolean | undefined + } + ): (self: Effect.Effect) => Effect.Effect, E, R> + ( + n: number, + options: { + readonly concurrency?: Concurrency | undefined + readonly batching?: boolean | "inherit" | undefined + readonly discard: true + readonly concurrentFinalizers?: boolean | undefined + } + ): (self: Effect.Effect) => Effect.Effect + ( + self: Effect.Effect, + n: number, + options?: { + readonly concurrency?: Concurrency | undefined + readonly batching?: boolean | "inherit" | undefined + readonly discard?: false | undefined + readonly concurrentFinalizers?: boolean | undefined + } + ): Effect.Effect, E, R> + ( + self: Effect.Effect, + n: number, + options: { + readonly concurrency?: Concurrency | undefined + readonly batching?: boolean | "inherit" | undefined + readonly discard: true + readonly concurrentFinalizers?: boolean | undefined + } + ): Effect.Effect +} = dual( + (args) => core.isEffect(args[0]), + (self, n, options) => all(replicate(self, n), options) +) + +/* @internal */ +export const forEach: { + >( + f: (a: RA.ReadonlyArray.Infer, i: number) => Effect.Effect, + options?: { + readonly concurrency?: Concurrency | undefined + readonly batching?: boolean | "inherit" | undefined + readonly discard?: false | undefined + readonly concurrentFinalizers?: boolean | undefined + } | undefined + ): ( + self: S + ) => Effect.Effect, E, R> + ( + f: (a: A, i: number) => Effect.Effect, + options: { + readonly concurrency?: Concurrency | undefined + readonly batching?: boolean | "inherit" | undefined + readonly discard: true + readonly concurrentFinalizers?: boolean | undefined + } + ): (self: Iterable) => Effect.Effect + ( + self: RA.NonEmptyReadonlyArray, + f: (a: A, i: number) => Effect.Effect, + options?: { + readonly concurrency?: Concurrency | undefined + readonly batching?: boolean | "inherit" | undefined + readonly discard?: false | undefined + readonly concurrentFinalizers?: boolean | undefined + } | undefined + ): Effect.Effect, E, R> + ( + self: Iterable, + f: (a: A, i: number) => Effect.Effect, + options?: { + readonly concurrency?: Concurrency | undefined + readonly batching?: boolean | "inherit" | undefined + readonly discard?: false | undefined + readonly concurrentFinalizers?: boolean | undefined + } | undefined + ): Effect.Effect, E, R> + ( + self: Iterable, + f: (a: A, i: number) => Effect.Effect, + options: { + readonly concurrency?: Concurrency | undefined + readonly batching?: boolean | "inherit" | undefined + readonly discard: true + readonly concurrentFinalizers?: boolean | undefined + } + ): Effect.Effect +} = dual((args) => Predicate.isIterable(args[0]), ( + self: Iterable, + f: (a: A, i: number) => Effect.Effect, + options?: { + readonly concurrency?: Concurrency | undefined + readonly batching?: boolean | "inherit" | undefined + readonly discard?: boolean | undefined + readonly concurrentFinalizers?: boolean | undefined + } +) => + core.withFiberRuntime((r) => { + const isRequestBatchingEnabled = options?.batching === true || + (options?.batching === "inherit" && r.getFiberRef(core.currentRequestBatching)) + + if (options?.discard) { + return concurrency.match( + options.concurrency, + () => + finalizersMaskInternal(ExecutionStrategy.sequential, options?.concurrentFinalizers)((restore) => + isRequestBatchingEnabled + ? forEachConcurrentDiscard(self, (a, i) => restore(f(a, i)), true, false, 1) + : core.forEachSequentialDiscard(self, (a, i) => restore(f(a, i))) + ), + () => + finalizersMaskInternal(ExecutionStrategy.parallel, options?.concurrentFinalizers)((restore) => + forEachConcurrentDiscard(self, (a, i) => restore(f(a, i)), isRequestBatchingEnabled, false) + ), + (n) => + finalizersMaskInternal(ExecutionStrategy.parallelN(n), options?.concurrentFinalizers)((restore) => + forEachConcurrentDiscard(self, (a, i) => restore(f(a, i)), isRequestBatchingEnabled, false, n) + ) + ) + } + + return concurrency.match( + options?.concurrency, + () => + finalizersMaskInternal(ExecutionStrategy.sequential, options?.concurrentFinalizers)((restore) => + isRequestBatchingEnabled + ? forEachParN(self, 1, (a, i) => restore(f(a, i)), true) + : core.forEachSequential(self, (a, i) => restore(f(a, i))) + ), + () => + finalizersMaskInternal(ExecutionStrategy.parallel, options?.concurrentFinalizers)((restore) => + forEachParUnbounded(self, (a, i) => restore(f(a, i)), isRequestBatchingEnabled) + ), + (n) => + finalizersMaskInternal(ExecutionStrategy.parallelN(n), options?.concurrentFinalizers)((restore) => + forEachParN(self, n, (a, i) => restore(f(a, i)), isRequestBatchingEnabled) + ) + ) + })) + +/* @internal */ +export const forEachParUnbounded = ( + self: Iterable, + f: (a: A, i: number) => Effect.Effect, + batching: boolean +): Effect.Effect, E, R> => + core.suspend(() => { + const as = RA.fromIterable(self) + const array = new Array(as.length) + const fn = (a: A, i: number) => core.flatMap(f(a, i), (b) => core.sync(() => array[i] = b)) + return core.zipRight(forEachConcurrentDiscard(as, fn, batching, false), core.succeed(array)) + }) + +/** @internal */ +export const forEachConcurrentDiscard = ( + self: Iterable, + f: (a: A, i: number) => Effect.Effect, + batching: boolean, + processAll: boolean, + n?: number +): Effect.Effect => + core.uninterruptibleMask((restore) => + core.transplant((graft) => + core.withFiberRuntime((parent) => { + let todos = Array.from(self).reverse() + let target = todos.length + if (target === 0) { + return core.void + } + let counter = 0 + let interrupted = false + const fibersCount = n ? Math.min(todos.length, n) : todos.length + const fibers = new Set | Effect.Blocked>>() + const results = new Array() + const interruptAll = () => + fibers.forEach((fiber) => { + fiber.currentScheduler.scheduleTask( + () => { + fiber.unsafeInterruptAsFork(parent.id()) + }, + 0, + fiber + ) + }) + const startOrder = new Array | Effect.Blocked>>() + const joinOrder = new Array | Effect.Blocked>>() + const residual = new Array() + const collectExits = () => { + const exits: Array> = results + .filter(({ exit }) => exit._tag === "Failure") + .sort((a, b) => a.index < b.index ? -1 : a.index === b.index ? 0 : 1) + .map(({ exit }) => exit) + if (exits.length === 0) { + exits.push(core.exitVoid) + } + return exits + } + const runFiber = (eff: Effect.Effect, interruptImmediately = false) => { + const runnable = core.uninterruptible(graft(eff)) + const fiber = unsafeForkUnstarted( + runnable, + parent, + parent.currentRuntimeFlags, + fiberScope.globalScope + ) + parent.currentScheduler.scheduleTask( + () => { + if (interruptImmediately) { + fiber.unsafeInterruptAsFork(parent.id()) + } + fiber.resume(runnable) + }, + 0, + fiber + ) + return fiber + } + const onInterruptSignal = () => { + if (!processAll) { + target -= todos.length + todos = [] + } + interrupted = true + interruptAll() + } + const stepOrExit = batching ? core.step : core.exit + const processingFiber = runFiber( + core.async((resume) => { + const pushResult = (res: Exit.Exit | Effect.Blocked, index: number) => { + if (res._op === "Blocked") { + residual.push(res as core.Blocked) + } else { + results.push({ index, exit: res }) + if (res._op === "Failure" && !interrupted) { + onInterruptSignal() + } + } + } + const next = () => { + if (todos.length > 0) { + const a = todos.pop()! + let index = counter++ + const returnNextElement = () => { + const a = todos.pop()! + index = counter++ + return core.flatMap(core.yieldNow(), () => + core.flatMap( + stepOrExit(restore(f(a, index))), + onRes + )) + } + const onRes = ( + res: Exit.Exit | Effect.Blocked + ): Effect.Effect | Effect.Blocked, never, R> => { + if (todos.length > 0) { + pushResult(res, index) + if (todos.length > 0) { + return returnNextElement() + } + } + return core.succeed(res) + } + const todo = core.flatMap( + stepOrExit(restore(f(a, index))), + onRes + ) + const fiber = runFiber(todo) + startOrder.push(fiber) + fibers.add(fiber) + if (interrupted) { + fiber.currentScheduler.scheduleTask( + () => { + fiber.unsafeInterruptAsFork(parent.id()) + }, + 0, + fiber + ) + } + fiber.addObserver((wrapped) => { + let exit: Exit.Exit | core.Blocked + if (wrapped._op === "Failure") { + exit = wrapped + } else { + exit = wrapped.effect_instruction_i0 as any + } + joinOrder.push(fiber) + fibers.delete(fiber) + pushResult(exit, index) + if (results.length === target) { + resume(core.succeed(Option.getOrElse( + core.exitCollectAll(collectExits(), { parallel: true }), + () => core.exitVoid + ))) + } else if (residual.length + results.length === target) { + const exits = collectExits() + const requests = residual.map((blocked) => blocked.effect_instruction_i0).reduce(RequestBlock_.par) + resume(core.succeed(core.blocked( + requests, + forEachConcurrentDiscard( + [ + Option.getOrElse( + core.exitCollectAll(exits, { parallel: true }), + () => core.exitVoid + ), + ...residual.map((blocked) => blocked.effect_instruction_i1) + ], + (i) => i, + batching, + true, + n + ) + ))) + } else { + next() + } + }) + } + } + for (let i = 0; i < fibersCount; i++) { + next() + } + }) + ) + return core.asVoid( + core.onExit( + core.flatten(restore(internalFiber.join(processingFiber))), + core.exitMatch({ + onFailure: (cause) => { + onInterruptSignal() + const target = residual.length + 1 + const concurrency = Math.min(typeof n === "number" ? n : residual.length, residual.length) + const toPop = Array.from(residual) + return core.async((cb) => { + const exits: Array> = [] + let count = 0 + let index = 0 + const check = (index: number, hitNext: boolean) => (exit: Exit.Exit) => { + exits[index] = exit + count++ + if (count === target) { + cb(core.exitSucceed(core.exitFailCause(cause))) + } + if (toPop.length > 0 && hitNext) { + next() + } + } + const next = () => { + runFiber(toPop.pop()!, true).addObserver(check(index, true)) + index++ + } + processingFiber.addObserver(check(index, false)) + index++ + for (let i = 0; i < concurrency; i++) { + next() + } + }) as any + }, + onSuccess: () => core.forEachSequential(joinOrder, (f) => f.inheritAll) + }) + ) + ) + }) + ) + ) + +/* @internal */ +export const forEachParN = ( + self: Iterable, + n: number, + f: (a: A, i: number) => Effect.Effect, + batching: boolean +): Effect.Effect, E, R> => + core.suspend(() => { + const as = RA.fromIterable(self) + const array = new Array(as.length) + const fn = (a: A, i: number) => core.map(f(a, i), (b) => array[i] = b) + return core.zipRight(forEachConcurrentDiscard(as, fn, batching, false, n), core.succeed(array)) + }) + +/* @internal */ +export const fork = (self: Effect.Effect): Effect.Effect, never, R> => + core.withFiberRuntime((state, status) => core.succeed(unsafeFork(self, state, status.runtimeFlags))) + +/* @internal */ +export const forkDaemon = (self: Effect.Effect): Effect.Effect, never, R> => + forkWithScopeOverride(self, fiberScope.globalScope) + +/* @internal */ +export const forkWithErrorHandler = dual< + ( + handler: (e: E) => Effect.Effect + ) => (self: Effect.Effect) => Effect.Effect, never, R>, + ( + self: Effect.Effect, + handler: (e: E) => Effect.Effect + ) => Effect.Effect, never, R> +>(2, (self, handler) => + fork(core.onError(self, (cause) => { + const either = internalCause.failureOrCause(cause) + switch (either._tag) { + case "Left": + return handler(either.left) + case "Right": + return core.failCause(either.right) + } + }))) + +/** @internal */ +export const unsafeFork = ( + effect: Effect.Effect, + parentFiber: FiberRuntime, + parentRuntimeFlags: RuntimeFlags.RuntimeFlags, + overrideScope: fiberScope.FiberScope | null = null +): FiberRuntime => { + const childFiber = unsafeMakeChildFiber(effect, parentFiber, parentRuntimeFlags, overrideScope) + childFiber.resume(effect) + return childFiber +} + +/** @internal */ +export const unsafeForkUnstarted = ( + effect: Effect.Effect, + parentFiber: FiberRuntime, + parentRuntimeFlags: RuntimeFlags.RuntimeFlags, + overrideScope: fiberScope.FiberScope | null = null +): FiberRuntime => { + const childFiber = unsafeMakeChildFiber(effect, parentFiber, parentRuntimeFlags, overrideScope) + return childFiber +} + +/** @internal */ +export const unsafeMakeChildFiber = ( + effect: Effect.Effect, + parentFiber: FiberRuntime, + parentRuntimeFlags: RuntimeFlags.RuntimeFlags, + overrideScope: fiberScope.FiberScope | null = null +): FiberRuntime => { + const childId = FiberId.unsafeMake() + const parentFiberRefs = parentFiber.getFiberRefs() + const childFiberRefs = fiberRefs.forkAs(parentFiberRefs, childId) + const childFiber = new FiberRuntime(childId, childFiberRefs, parentRuntimeFlags) + const childContext = fiberRefs.getOrDefault( + childFiberRefs, + core.currentContext as unknown as FiberRef.FiberRef> + ) + const supervisor = childFiber.currentSupervisor + + supervisor.onStart( + childContext, + effect, + Option.some(parentFiber), + childFiber + ) + + childFiber.addObserver((exit) => supervisor.onEnd(exit, childFiber)) + + const parentScope = overrideScope !== null ? overrideScope : pipe( + parentFiber.getFiberRef(core.currentForkScopeOverride), + Option.getOrElse(() => parentFiber.scope()) + ) + + parentScope.add(parentRuntimeFlags, childFiber) + + return childFiber +} + +/* @internal */ +const forkWithScopeOverride = ( + self: Effect.Effect, + scopeOverride: fiberScope.FiberScope +): Effect.Effect, never, R> => + core.withFiberRuntime((parentFiber, parentStatus) => + core.succeed(unsafeFork(self, parentFiber, parentStatus.runtimeFlags, scopeOverride)) + ) + +/* @internal */ +export const mergeAll = dual< + >( + zero: Z, + f: (z: Z, a: Effect.Effect.Success, i: number) => Z, + options?: { + readonly concurrency?: Concurrency | undefined + readonly batching?: boolean | "inherit" | undefined + readonly concurrentFinalizers?: boolean | undefined + } + ) => (elements: Iterable) => Effect.Effect, Effect.Effect.Context>, + , Z>( + elements: Iterable, + zero: Z, + f: (z: Z, a: Effect.Effect.Success, i: number) => Z, + options?: { + readonly concurrency?: Concurrency | undefined + readonly batching?: boolean | "inherit" | undefined + readonly concurrentFinalizers?: boolean | undefined + } + ) => Effect.Effect, Effect.Effect.Context> +>( + (args) => Predicate.isFunction(args[2]), + (elements: Iterable>, zero: Z, f: (z: Z, a: A, i: number) => Z, options?: { + readonly concurrency?: Concurrency | undefined + readonly batching?: boolean | "inherit" | undefined + readonly concurrentFinalizers?: boolean | undefined + }) => + concurrency.matchSimple( + options?.concurrency, + () => + RA.fromIterable(elements).reduce( + (acc, a, i) => core.zipWith(acc, a, (acc, a) => f(acc, a, i)), + core.succeed(zero) as Effect.Effect + ), + () => + core.flatMap(Ref.make(zero), (acc) => + core.flatMap( + forEach( + elements, + (effect, i) => core.flatMap(effect, (a) => Ref.update(acc, (b) => f(b, a, i))), + options + ), + () => Ref.get(acc) + )) + ) +) + +/* @internal */ +export const partition = dual< + ( + f: (a: A, i: number) => Effect.Effect, + options?: { + readonly concurrency?: Concurrency | undefined + readonly batching?: boolean | "inherit" | undefined + readonly concurrentFinalizers?: boolean | undefined + } + ) => (elements: Iterable) => Effect.Effect<[excluded: Array, satisfying: Array], never, R>, + ( + elements: Iterable, + f: (a: A, i: number) => Effect.Effect, + options?: { + readonly concurrency?: Concurrency | undefined + readonly batching?: boolean | "inherit" | undefined + readonly concurrentFinalizers?: boolean | undefined + } + ) => Effect.Effect<[excluded: Array, satisfying: Array], never, R> +>((args) => Predicate.isIterable(args[0]), (elements, f, options) => + pipe( + forEach(elements, (a, i) => core.either(f(a, i)), options), + core.map((chunk) => core.partitionMap(chunk, identity)) + )) + +/* @internal */ +export const validateAll = dual< + { + ( + f: (a: A, i: number) => Effect.Effect, + options?: { + readonly concurrency?: Concurrency | undefined + readonly batching?: boolean | "inherit" | undefined + readonly discard?: false | undefined + readonly concurrentFinalizers?: boolean | undefined + } + ): (elements: Iterable) => Effect.Effect, RA.NonEmptyArray, R> + ( + f: (a: A, i: number) => Effect.Effect, + options: { + readonly concurrency?: Concurrency | undefined + readonly batching?: boolean | "inherit" | undefined + readonly discard: true + readonly concurrentFinalizers?: boolean | undefined + } + ): (elements: Iterable) => Effect.Effect, R> + }, + { + ( + elements: Iterable, + f: (a: A, i: number) => Effect.Effect, + options?: { + readonly concurrency?: Concurrency | undefined + readonly batching?: boolean | "inherit" | undefined + readonly discard?: false | undefined + readonly concurrentFinalizers?: boolean | undefined + } + ): Effect.Effect, RA.NonEmptyArray, R> + ( + elements: Iterable, + f: (a: A, i: number) => Effect.Effect, + options: { + readonly concurrency?: Concurrency | undefined + readonly batching?: boolean | "inherit" | undefined + readonly discard: true + readonly concurrentFinalizers?: boolean | undefined + } + ): Effect.Effect, R> + } +>( + (args) => Predicate.isIterable(args[0]), + (elements: Iterable, f: (a: A, i: number) => Effect.Effect, options?: { + readonly concurrency?: Concurrency | undefined + readonly batching?: boolean | "inherit" | undefined + readonly discard?: boolean | undefined + readonly concurrentFinalizers?: boolean | undefined + }): Effect.Effect, R> => + core.flatMap( + partition(elements, f, { + concurrency: options?.concurrency, + batching: options?.batching, + concurrentFinalizers: options?.concurrentFinalizers + }), + ([es, bs]) => + RA.isNonEmptyArray(es) + ? core.fail(es) + : options?.discard + ? core.void + : core.succeed(bs) + ) +) + +/* @internal */ +export const raceAll: >( + all: Iterable +) => Effect.Effect, Effect.Effect.Error, Effect.Effect.Context> = < + A, + E, + R +>(all: Iterable>): Effect.Effect => + core.withFiberRuntime((state, status) => + core.async((resume) => { + const fibers = new Set>() + let winner: FiberRuntime | undefined + let failures: Cause.Cause = internalCause.empty + const interruptAll = () => { + for (const fiber of fibers) { + fiber.unsafeInterruptAsFork(state.id()) + } + } + let latch = false + let empty = true + for (const self of all) { + empty = false + const fiber = unsafeFork( + core.interruptible(self), + state, + status.runtimeFlags + ) + fibers.add(fiber) + fiber.addObserver((exit) => { + fibers.delete(fiber) + if (!winner) { + if (exit._tag === "Success") { + latch = true + winner = fiber + failures = internalCause.empty + interruptAll() + } else { + failures = internalCause.parallel(exit.cause, failures) + } + } + if (latch && fibers.size === 0) { + resume( + winner ? core.zipRight(internalFiber.inheritAll(winner), winner.unsafePoll()!) : core.failCause(failures) + ) + } + }) + if (winner) break + } + if (empty) { + return resume(core.dieSync(() => new core.IllegalArgumentException(`Received an empty collection of effects`))) + } + latch = true + return internalFiber.interruptAllAs(fibers, state.id()) + }) + ) + +/* @internal */ +export const reduceEffect = dual< + >( + zero: Effect.Effect, + f: (z: NoInfer, a: Effect.Effect.Success, i: number) => Z, + options?: { + readonly concurrency?: Concurrency | undefined + readonly batching?: boolean | "inherit" | undefined + readonly concurrentFinalizers?: boolean | undefined + } + ) => (elements: Iterable) => Effect.Effect, R | Effect.Effect.Context>, + , Z, E, R>( + elements: Iterable, + zero: Effect.Effect, + f: (z: NoInfer, a: Effect.Effect.Success, i: number) => Z, + options?: { + readonly concurrency?: Concurrency | undefined + readonly batching?: boolean | "inherit" | undefined + readonly concurrentFinalizers?: boolean | undefined + } + ) => Effect.Effect, R | Effect.Effect.Context> +>((args) => Predicate.isIterable(args[0]) && !core.isEffect(args[0]), ( + elements: Iterable>, + zero: Effect.Effect, + f: (z: NoInfer, a: NoInfer, i: number) => Z, + options?: { + readonly concurrency?: Concurrency | undefined + readonly batching?: boolean | "inherit" | undefined + readonly concurrentFinalizers?: boolean | undefined + } +) => + concurrency.matchSimple( + options?.concurrency, + () => RA.fromIterable(elements).reduce((acc, a, i) => core.zipWith(acc, a, (acc, a) => f(acc, a, i)), zero), + () => + core.suspend(() => + pipe( + mergeAll( + [zero, ...elements], + Option.none(), + (acc, elem, i) => { + switch (acc._tag) { + case "None": { + return Option.some(elem as Z) + } + case "Some": { + return Option.some(f(acc.value, elem as A, i)) + } + } + }, + options + ), + core.map((option) => { + switch (option._tag) { + case "None": { + throw new Error( + "BUG: Effect.reduceEffect - please report an issue at https://github.com/Effect-TS/effect/issues" + ) + } + case "Some": { + return option.value + } + } + }) + ) + ) + )) + +/* @internal */ +export const parallelFinalizers = (self: Effect.Effect): Effect.Effect => + core.contextWithEffect((context) => + Option.match(Context.getOption(context, scopeTag), { + onNone: () => self, + onSome: (scope) => { + switch (scope.strategy._tag) { + case "Parallel": + return self + case "Sequential": + case "ParallelN": + return core.flatMap( + core.scopeFork(scope, ExecutionStrategy.parallel), + (inner) => scopeExtend(self, inner) + ) + } + } + }) + ) + +/* @internal */ +export const parallelNFinalizers = + (parallelism: number) => (self: Effect.Effect): Effect.Effect => + core.contextWithEffect((context) => + Option.match(Context.getOption(context, scopeTag), { + onNone: () => self, + onSome: (scope) => { + if (scope.strategy._tag === "ParallelN" && scope.strategy.parallelism === parallelism) { + return self + } + return core.flatMap( + core.scopeFork(scope, ExecutionStrategy.parallelN(parallelism)), + (inner) => scopeExtend(self, inner) + ) + } + }) + ) + +/* @internal */ +export const finalizersMask = (strategy: ExecutionStrategy.ExecutionStrategy) => +( + self: ( + restore: (self: Effect.Effect) => Effect.Effect + ) => Effect.Effect +): Effect.Effect => finalizersMaskInternal(strategy, true)(self) + +/* @internal */ +export const finalizersMaskInternal = + (strategy: ExecutionStrategy.ExecutionStrategy, concurrentFinalizers?: boolean | undefined) => + ( + self: ( + restore: (self: Effect.Effect) => Effect.Effect + ) => Effect.Effect + ): Effect.Effect => + core.contextWithEffect((context) => + Option.match(Context.getOption(context, scopeTag), { + onNone: () => self(identity), + onSome: (scope) => { + if (concurrentFinalizers === true) { + const patch = strategy._tag === "Parallel" + ? parallelFinalizers + : strategy._tag === "Sequential" + ? sequentialFinalizers + : parallelNFinalizers(strategy.parallelism) + switch (scope.strategy._tag) { + case "Parallel": + return patch(self(parallelFinalizers)) + case "Sequential": + return patch(self(sequentialFinalizers)) + case "ParallelN": + return patch(self(parallelNFinalizers(scope.strategy.parallelism))) + } + } else { + return self(identity) + } + } + }) + ) + +/* @internal */ +export const scopeWith = ( + f: (scope: Scope.Scope) => Effect.Effect +): Effect.Effect => core.flatMap(scopeTag, f) + +/** @internal */ +export const scopedWith = ( + f: (scope: Scope.Scope) => Effect.Effect +): Effect.Effect => core.flatMap(scopeMake(), (scope) => core.onExit(f(scope), (exit) => scope.close(exit))) + +/* @internal */ +export const scopedEffect = (effect: Effect.Effect): Effect.Effect> => + core.flatMap(scopeMake(), (scope) => scopeUse(effect, scope)) + +/* @internal */ +export const sequentialFinalizers = (self: Effect.Effect): Effect.Effect => + core.contextWithEffect((context) => + Option.match(Context.getOption(context, scopeTag), { + onNone: () => self, + onSome: (scope) => { + switch (scope.strategy._tag) { + case "Sequential": + return self + case "Parallel": + case "ParallelN": + return core.flatMap( + core.scopeFork(scope, ExecutionStrategy.sequential), + (inner) => scopeExtend(self, inner) + ) + } + } + }) + ) + +/* @internal */ +export const tagMetricsScoped = (key: string, value: string): Effect.Effect => + labelMetricsScoped([metricLabel.make(key, value)]) + +/* @internal */ +export const labelMetricsScoped = ( + labels: Iterable +): Effect.Effect => + fiberRefLocallyScopedWith(core.currentMetricLabels, (old) => RA.union(old, labels)) + +/* @internal */ +export const using = dual< + ( + use: (a: A) => Effect.Effect + ) => (self: Effect.Effect) => Effect.Effect | R2>, + ( + self: Effect.Effect, + use: (a: A) => Effect.Effect + ) => Effect.Effect | R2> +>(2, (self, use) => scopedWith((scope) => core.flatMap(scopeExtend(self, scope), use))) + +/** @internal */ +export const validate = dual< + ( + that: Effect.Effect, + options?: { + readonly concurrent?: boolean | undefined + readonly batching?: boolean | "inherit" | undefined + readonly concurrentFinalizers?: boolean | undefined + } + ) => (self: Effect.Effect) => Effect.Effect<[A, B], E | E1, R | R1>, + ( + self: Effect.Effect, + that: Effect.Effect, + options?: { + readonly concurrent?: boolean | undefined + readonly batching?: boolean | "inherit" | undefined + readonly concurrentFinalizers?: boolean | undefined + } + ) => Effect.Effect<[A, B], E | E1, R | R1> +>( + (args) => core.isEffect(args[1]), + (self, that, options) => validateWith(self, that, (a, b) => [a, b], options) +) + +/** @internal */ +export const validateWith = dual< + ( + that: Effect.Effect, + f: (a: A, b: B) => C, + options?: { + readonly concurrent?: boolean | undefined + readonly batching?: boolean | "inherit" | undefined + readonly concurrentFinalizers?: boolean | undefined + } + ) => (self: Effect.Effect) => Effect.Effect, + ( + self: Effect.Effect, + that: Effect.Effect, + f: (a: A, b: B) => C, + options?: { + readonly concurrent?: boolean | undefined + readonly batching?: boolean | "inherit" | undefined + readonly concurrentFinalizers?: boolean | undefined + } + ) => Effect.Effect +>((args) => core.isEffect(args[1]), (self, that, f, options) => + core.flatten(zipWithOptions( + core.exit(self), + core.exit(that), + (ea, eb) => + core.exitZipWith(ea, eb, { + onSuccess: f, + onFailure: (ca, cb) => options?.concurrent ? internalCause.parallel(ca, cb) : internalCause.sequential(ca, cb) + }), + options + ))) + +/* @internal */ +export const validateAllPar = dual< + ( + f: (a: A) => Effect.Effect + ) => (elements: Iterable) => Effect.Effect, Array, R>, + ( + elements: Iterable, + f: (a: A) => Effect.Effect + ) => Effect.Effect, Array, R> +>(2, (elements, f) => + core.flatMap( + partition(elements, f), + ([es, bs]) => + es.length === 0 + ? core.succeed(bs) + : core.fail(es) + )) + +/* @internal */ +export const validateAllParDiscard = dual< + ( + f: (a: A) => Effect.Effect + ) => (elements: Iterable) => Effect.Effect, R>, + (elements: Iterable, f: (a: A) => Effect.Effect) => Effect.Effect, R> +>(2, (elements, f) => + core.flatMap( + partition(elements, f), + ([es, _]) => + es.length === 0 + ? core.void + : core.fail(es) + )) + +/* @internal */ +export const validateFirst = dual< + (f: (a: A, i: number) => Effect.Effect, options?: { + readonly concurrency?: Concurrency | undefined + readonly batching?: boolean | "inherit" | undefined + readonly concurrentFinalizers?: boolean | undefined + }) => (elements: Iterable) => Effect.Effect, R>, + (elements: Iterable, f: (a: A, i: number) => Effect.Effect, options?: { + readonly concurrency?: Concurrency | undefined + readonly batching?: boolean | "inherit" | undefined + readonly concurrentFinalizers?: boolean | undefined + }) => Effect.Effect, R> +>( + (args) => Predicate.isIterable(args[0]), + (elements, f, options) => core.flip(forEach(elements, (a, i) => core.flip(f(a, i)), options)) +) + +/* @internal */ +export const withClockScoped = (c: C) => + fiberRefLocallyScopedWith(defaultServices.currentServices, Context.add(clock.clockTag, c)) + +/* @internal */ +export const withRandomScoped = (value: A) => + fiberRefLocallyScopedWith(defaultServices.currentServices, Context.add(randomTag, value)) + +/* @internal */ +export const withConfigProviderScoped = (provider: ConfigProvider) => + fiberRefLocallyScopedWith(defaultServices.currentServices, Context.add(configProviderTag, provider)) + +/* @internal */ +export const withEarlyRelease = ( + self: Effect.Effect +): Effect.Effect<[Effect.Effect, A], E, R | Scope.Scope> => + scopeWith((parent) => + core.flatMap(core.scopeFork(parent, executionStrategy.sequential), (child) => + pipe( + self, + scopeExtend(child), + core.map((value) => [ + core.fiberIdWith((fiberId) => core.scopeClose(child, core.exitInterrupt(fiberId))), + value + ]) + )) + ) + +/** @internal */ +export const zipOptions = dual< + ( + that: Effect.Effect, + options?: { + readonly concurrent?: boolean | undefined + readonly batching?: boolean | "inherit" | undefined + readonly concurrentFinalizers?: boolean | undefined + } + ) => ( + self: Effect.Effect + ) => Effect.Effect<[A, A2], E | E2, R | R2>, + ( + self: Effect.Effect, + that: Effect.Effect, + options?: { + readonly concurrent?: boolean | undefined + readonly batching?: boolean | "inherit" | undefined + readonly concurrentFinalizers?: boolean | undefined + } + ) => Effect.Effect<[A, A2], E | E2, R | R2> +>((args) => core.isEffect(args[1]), ( + self, + that, + options +) => zipWithOptions(self, that, (a, b) => [a, b], options)) + +/** @internal */ +export const zipLeftOptions = dual< + ( + that: Effect.Effect, + options?: { + readonly concurrent?: boolean | undefined + readonly batching?: boolean | "inherit" | undefined + readonly concurrentFinalizers?: boolean | undefined + } + ) => ( + self: Effect.Effect + ) => Effect.Effect, + ( + self: Effect.Effect, + that: Effect.Effect, + options?: { + readonly concurrent?: boolean | undefined + readonly batching?: boolean | "inherit" | undefined + readonly concurrentFinalizers?: boolean | undefined + } + ) => Effect.Effect +>( + (args) => core.isEffect(args[1]), + (self, that, options) => { + if (options?.concurrent !== true && (options?.batching === undefined || options.batching === false)) { + return core.zipLeft(self, that) + } + return zipWithOptions(self, that, (a, _) => a, options) + } +) + +/** @internal */ +export const zipRightOptions: { + ( + that: Effect.Effect, + options?: { + readonly concurrent?: boolean | undefined + readonly batching?: boolean | "inherit" | undefined + readonly concurrentFinalizers?: boolean | undefined + } + ): (self: Effect.Effect) => Effect.Effect + ( + self: Effect.Effect, + that: Effect.Effect, + options?: { + readonly concurrent?: boolean | undefined + readonly batching?: boolean | "inherit" | undefined + readonly concurrentFinalizers?: boolean | undefined + } + ): Effect.Effect +} = dual((args) => core.isEffect(args[1]), ( + self: Effect.Effect, + that: Effect.Effect, + options?: { + readonly concurrent?: boolean | undefined + readonly batching?: boolean | "inherit" | undefined + readonly concurrentFinalizers?: boolean | undefined + } +): Effect.Effect => { + if (options?.concurrent !== true && (options?.batching === undefined || options.batching === false)) { + return core.zipRight(self, that) + } + return zipWithOptions(self, that, (_, b) => b, options) +}) + +/** @internal */ +export const zipWithOptions: { + ( + that: Effect.Effect, + f: (a: A, b: A2) => B, + options?: { + readonly concurrent?: boolean | undefined + readonly batching?: boolean | "inherit" | undefined + readonly concurrentFinalizers?: boolean | undefined + } + ): (self: Effect.Effect) => Effect.Effect + ( + self: Effect.Effect, + that: Effect.Effect, + f: (a: A, b: A2) => B, + options?: { + readonly concurrent?: boolean | undefined + readonly batching?: boolean | "inherit" | undefined + readonly concurrentFinalizers?: boolean | undefined + } + ): Effect.Effect +} = dual((args) => core.isEffect(args[1]), ( + self: Effect.Effect, + that: Effect.Effect, + f: (a: A, b: A2) => B, + options?: { + readonly concurrent?: boolean | undefined + readonly batching?: boolean | "inherit" | undefined + readonly concurrentFinalizers?: boolean | undefined + } +): Effect.Effect => + core.map( + all([self, that], { + concurrency: options?.concurrent ? 2 : 1, + batching: options?.batching, + concurrentFinalizers: options?.concurrentFinalizers + }), + ([a, a2]) => f(a, a2) + )) + +/* @internal */ +export const withRuntimeFlagsScoped = ( + update: RuntimeFlagsPatch.RuntimeFlagsPatch +): Effect.Effect => { + if (update === RuntimeFlagsPatch.empty) { + return core.void + } + return pipe( + core.runtimeFlags, + core.flatMap((runtimeFlags) => { + const updatedRuntimeFlags = runtimeFlags_.patch(runtimeFlags, update) + const revertRuntimeFlags = runtimeFlags_.diff(updatedRuntimeFlags, runtimeFlags) + return pipe( + core.updateRuntimeFlags(update), + core.zipRight(addFinalizer(() => core.updateRuntimeFlags(revertRuntimeFlags))), + core.asVoid + ) + }), + core.uninterruptible + ) +} + +// circular with Scope + +/** @internal */ +export const scopeTag = Context.GenericTag("effect/Scope") + +/* @internal */ +export const scope: Effect.Effect = scopeTag + +/** @internal */ +export interface ScopeImpl extends Scope.CloseableScope { + state: { + readonly _tag: "Open" + readonly finalizers: Map<{}, Scope.Scope.Finalizer> + } | { + readonly _tag: "Closed" + readonly exit: Exit.Exit + } +} + +const scopeUnsafeAddFinalizer = (scope: ScopeImpl, fin: Scope.Scope.Finalizer): void => { + if (scope.state._tag === "Open") { + scope.state.finalizers.set({}, fin) + } +} + +const ScopeImplProto: Omit = { + [core.ScopeTypeId]: core.ScopeTypeId, + [core.CloseableScopeTypeId]: core.CloseableScopeTypeId, + pipe() { + return pipeArguments(this, arguments) + }, + fork(this: ScopeImpl, strategy) { + return core.sync(() => { + const newScope = scopeUnsafeMake(strategy) + if (this.state._tag === "Closed") { + newScope.state = this.state + return newScope + } + const key = {} + const fin = (exit: Exit.Exit) => newScope.close(exit) + this.state.finalizers.set(key, fin) + scopeUnsafeAddFinalizer(newScope, (_) => + core.sync(() => { + if (this.state._tag === "Open") { + this.state.finalizers.delete(key) + } + })) + return newScope + }) + }, + close(this: ScopeImpl, exit) { + return core.suspend(() => { + if (this.state._tag === "Closed") { + return core.void + } + const finalizers = Array.from(this.state.finalizers.values()).reverse() + this.state = { _tag: "Closed", exit } + if (finalizers.length === 0) { + return core.void + } + return executionStrategy.isSequential(this.strategy) ? + pipe( + core.forEachSequential(finalizers, (fin) => core.exit(fin(exit))), + core.flatMap((results) => + pipe( + core.exitCollectAll(results), + Option.map(core.exitAsVoid), + Option.getOrElse(() => core.exitVoid) + ) + ) + ) : + executionStrategy.isParallel(this.strategy) ? + pipe( + forEachParUnbounded(finalizers, (fin) => core.exit(fin(exit)), false), + core.flatMap((results) => + pipe( + core.exitCollectAll(results, { parallel: true }), + Option.map(core.exitAsVoid), + Option.getOrElse(() => core.exitVoid) + ) + ) + ) : + pipe( + forEachParN(finalizers, this.strategy.parallelism, (fin) => core.exit(fin(exit)), false), + core.flatMap((results) => + pipe( + core.exitCollectAll(results, { parallel: true }), + Option.map(core.exitAsVoid), + Option.getOrElse(() => core.exitVoid) + ) + ) + ) + }) + }, + addFinalizer(this: ScopeImpl, fin) { + return core.suspend(() => { + if (this.state._tag === "Closed") { + return fin(this.state.exit) + } + this.state.finalizers.set({}, fin) + return core.void + }) + } +} + +const scopeUnsafeMake = ( + strategy: ExecutionStrategy.ExecutionStrategy = executionStrategy.sequential +): ScopeImpl => { + const scope = Object.create(ScopeImplProto) + scope.strategy = strategy + scope.state = { _tag: "Open", finalizers: new Map() } + return scope +} + +/* @internal */ +export const scopeMake = ( + strategy: ExecutionStrategy.ExecutionStrategy = executionStrategy.sequential +): Effect.Effect => core.sync(() => scopeUnsafeMake(strategy)) + +/* @internal */ +export const scopeExtend = dual< + (scope: Scope.Scope) => (effect: Effect.Effect) => Effect.Effect>, + (effect: Effect.Effect, scope: Scope.Scope) => Effect.Effect> +>( + 2, + (effect: Effect.Effect, scope: Scope.Scope) => + core.mapInputContext>( + effect, + // @ts-expect-error + Context.merge(Context.make(scopeTag, scope)) + ) +) + +/* @internal */ +export const scopeUse = dual< + ( + scope: Scope.Scope.Closeable + ) => (effect: Effect.Effect) => Effect.Effect>, + ( + effect: Effect.Effect, + scope: Scope.Scope.Closeable + ) => Effect.Effect> +>(2, (effect, scope) => + pipe( + effect, + scopeExtend(scope), + core.onExit((exit) => scope.close(exit)) + )) + +// circular with Supervisor + +/** @internal */ +export const fiberRefUnsafeMakeSupervisor = ( + initial: Supervisor.Supervisor +): FiberRef.FiberRef> => + core.fiberRefUnsafeMakePatch(initial, { + differ: SupervisorPatch.differ, + fork: SupervisorPatch.empty + }) + +// circular with FiberRef + +/* @internal */ +export const fiberRefLocallyScoped = dual< + (value: A) => (self: FiberRef.FiberRef) => Effect.Effect, + (self: FiberRef.FiberRef, value: A) => Effect.Effect +>(2, (self, value) => + core.asVoid( + acquireRelease( + core.flatMap( + core.fiberRefGet(self), + (oldValue) => core.as(core.fiberRefSet(self, value), oldValue) + ), + (oldValue) => core.fiberRefSet(self, oldValue) + ) + )) + +/* @internal */ +export const fiberRefLocallyScopedWith = dual< + (f: (a: A) => A) => (self: FiberRef.FiberRef) => Effect.Effect, + (self: FiberRef.FiberRef, f: (a: A) => A) => Effect.Effect +>(2, (self, f) => core.fiberRefGetWith(self, (a) => fiberRefLocallyScoped(self, f(a)))) + +/* @internal */ +export const fiberRefMake = ( + initial: A, + options?: { + readonly fork?: ((a: A) => A) | undefined + readonly join?: ((left: A, right: A) => A) | undefined + } +): Effect.Effect, never, Scope.Scope> => + fiberRefMakeWith(() => core.fiberRefUnsafeMake(initial, options)) + +/* @internal */ +export const fiberRefMakeWith = ( + ref: LazyArg> +): Effect.Effect, never, Scope.Scope> => + acquireRelease( + core.tap(core.sync(ref), (ref) => core.fiberRefUpdate(ref, identity)), + (fiberRef) => core.fiberRefDelete(fiberRef) + ) + +/* @internal */ +export const fiberRefMakeContext = ( + initial: Context.Context +): Effect.Effect>, never, Scope.Scope> => + fiberRefMakeWith(() => core.fiberRefUnsafeMakeContext(initial)) + +/* @internal */ +export const fiberRefMakeRuntimeFlags = ( + initial: RuntimeFlags.RuntimeFlags +): Effect.Effect, never, Scope.Scope> => + fiberRefMakeWith(() => core.fiberRefUnsafeMakeRuntimeFlags(initial)) + +/** @internal */ +export const currentRuntimeFlags: FiberRef.FiberRef = core.fiberRefUnsafeMakeRuntimeFlags( + runtimeFlags_.none +) + +/** @internal */ +export const currentSupervisor: FiberRef.FiberRef> = fiberRefUnsafeMakeSupervisor( + supervisor.none +) + +// circular with Fiber + +/* @internal */ +export const fiberAwaitAll = >>( + fibers: T +): Effect.Effect< + [T] extends [ReadonlyArray] + ? number extends T["length"] ? Array ? Exit.Exit : never> + : { -readonly [K in keyof T]: T[K] extends Fiber.Fiber ? Exit.Exit : never } + : Array ? U extends Fiber.Fiber ? Exit.Exit : never : never> +> => forEach(fibers, internalFiber._await) as any + +/** @internal */ +export const fiberAll = (fibers: Iterable>): Fiber.Fiber, E> => { + const _fiberAll = { + ...Effectable.CommitPrototype, + commit() { + return internalFiber.join(this) + }, + [internalFiber.FiberTypeId]: internalFiber.fiberVariance, + id: () => + RA.fromIterable(fibers).reduce((id, fiber) => FiberId.combine(id, fiber.id()), FiberId.none as FiberId.FiberId), + await: core.exit(forEachParUnbounded(fibers, (fiber) => core.flatten(fiber.await), false)), + children: core.map(forEachParUnbounded(fibers, (fiber) => fiber.children, false), RA.flatten), + inheritAll: core.forEachSequentialDiscard(fibers, (fiber) => fiber.inheritAll), + poll: core.map( + core.forEachSequential(fibers, (fiber) => fiber.poll), + RA.reduceRight( + Option.some, E>>(core.exitSucceed(new Array())), + (optionB, optionA) => { + switch (optionA._tag) { + case "None": { + return Option.none() + } + case "Some": { + switch (optionB._tag) { + case "None": { + return Option.none() + } + case "Some": { + return Option.some( + core.exitZipWith(optionA.value, optionB.value, { + onSuccess: (a, chunk) => [a, ...chunk], + onFailure: internalCause.parallel + }) + ) + } + } + } + } + } + ) + ), + interruptAsFork: (fiberId: FiberId.FiberId) => + core.forEachSequentialDiscard(fibers, (fiber) => fiber.interruptAsFork(fiberId)) + } + return _fiberAll +} + +/* @internal */ +export const fiberInterruptFork = (self: Fiber.Fiber): Effect.Effect => + core.asVoid(forkDaemon(core.interruptFiber(self))) + +/* @internal */ +export const fiberJoinAll = (fibers: Iterable>): Effect.Effect, E> => + internalFiber.join(fiberAll(fibers)) + +/* @internal */ +export const fiberScoped = (self: Fiber.Fiber): Effect.Effect, never, Scope.Scope> => + acquireRelease(core.succeed(self), core.interruptFiber) + +// +// circular race +// + +/** @internal */ +export const raceWith = dual< + ( + other: Effect.Effect, + options: { + readonly onSelfDone: (exit: Exit.Exit, fiber: Fiber.Fiber) => Effect.Effect + readonly onOtherDone: (exit: Exit.Exit, fiber: Fiber.Fiber) => Effect.Effect + } + ) => (self: Effect.Effect) => Effect.Effect, + ( + self: Effect.Effect, + other: Effect.Effect, + options: { + readonly onSelfDone: (exit: Exit.Exit, fiber: Fiber.Fiber) => Effect.Effect + readonly onOtherDone: (exit: Exit.Exit, fiber: Fiber.Fiber) => Effect.Effect + } + ) => Effect.Effect +>(3, (self, other, options) => + raceFibersWith(self, other, { + onSelfWin: (winner, loser) => + core.flatMap(winner.await, (exit) => { + switch (exit._tag) { + case OpCodes.OP_SUCCESS: { + return core.flatMap( + winner.inheritAll, + () => options.onSelfDone(exit, loser) + ) + } + case OpCodes.OP_FAILURE: { + return options.onSelfDone(exit, loser) + } + } + }), + onOtherWin: (winner, loser) => + core.flatMap(winner.await, (exit) => { + switch (exit._tag) { + case OpCodes.OP_SUCCESS: { + return core.flatMap( + winner.inheritAll, + () => options.onOtherDone(exit, loser) + ) + } + case OpCodes.OP_FAILURE: { + return options.onOtherDone(exit, loser) + } + } + }) + })) + +/** @internal */ +export const disconnect = (self: Effect.Effect): Effect.Effect => + core.uninterruptibleMask((restore) => + core.fiberIdWith((fiberId) => + core.flatMap(forkDaemon(restore(self)), (fiber) => + pipe( + restore(internalFiber.join(fiber)), + core.onInterrupt(() => pipe(fiber, internalFiber.interruptAsFork(fiberId))) + )) + ) + ) + +/** @internal */ +export const race = dual< + ( + that: Effect.Effect + ) => ( + self: Effect.Effect + ) => Effect.Effect, + ( + self: Effect.Effect, + that: Effect.Effect + ) => Effect.Effect +>( + 2, + (self, that) => + core.fiberIdWith((parentFiberId) => + raceWith(self, that, { + onSelfDone: (exit, right) => + core.exitMatchEffect(exit, { + onFailure: (cause) => + pipe( + internalFiber.join(right), + internalEffect.mapErrorCause((cause2) => internalCause.parallel(cause, cause2)) + ), + onSuccess: (value) => + pipe( + right, + core.interruptAsFiber(parentFiberId), + core.as(value) + ) + }), + onOtherDone: (exit, left) => + core.exitMatchEffect(exit, { + onFailure: (cause) => + pipe( + internalFiber.join(left), + internalEffect.mapErrorCause((cause2) => internalCause.parallel(cause2, cause)) + ), + onSuccess: (value) => + pipe( + left, + core.interruptAsFiber(parentFiberId), + core.as(value) + ) + }) + }) + ) +) + +/** @internal */ +export const raceFibersWith = dual< + ( + other: Effect.Effect, + options: { + readonly onSelfWin: ( + winner: Fiber.RuntimeFiber, + loser: Fiber.RuntimeFiber + ) => Effect.Effect + readonly onOtherWin: ( + winner: Fiber.RuntimeFiber, + loser: Fiber.RuntimeFiber + ) => Effect.Effect + readonly selfScope?: fiberScope.FiberScope | undefined + readonly otherScope?: fiberScope.FiberScope | undefined + } + ) => (self: Effect.Effect) => Effect.Effect, + ( + self: Effect.Effect, + other: Effect.Effect, + options: { + readonly onSelfWin: ( + winner: Fiber.RuntimeFiber, + loser: Fiber.RuntimeFiber + ) => Effect.Effect + readonly onOtherWin: ( + winner: Fiber.RuntimeFiber, + loser: Fiber.RuntimeFiber + ) => Effect.Effect + readonly selfScope?: fiberScope.FiberScope | undefined + readonly otherScope?: fiberScope.FiberScope | undefined + } + ) => Effect.Effect +>(3, ( + self: Effect.Effect, + other: Effect.Effect, + options: { + readonly onSelfWin: ( + winner: Fiber.RuntimeFiber, + loser: Fiber.RuntimeFiber + ) => Effect.Effect + readonly onOtherWin: ( + winner: Fiber.RuntimeFiber, + loser: Fiber.RuntimeFiber + ) => Effect.Effect + readonly selfScope?: fiberScope.FiberScope | undefined + readonly otherScope?: fiberScope.FiberScope | undefined + } +) => + core.withFiberRuntime((parentFiber, parentStatus) => { + const parentRuntimeFlags = parentStatus.runtimeFlags + const raceIndicator = MRef.make(true) + const leftFiber: FiberRuntime = unsafeMakeChildFiber( + self, + parentFiber, + parentRuntimeFlags, + options.selfScope + ) + const rightFiber: FiberRuntime = unsafeMakeChildFiber( + other, + parentFiber, + parentRuntimeFlags, + options.otherScope + ) + return core.async((cb) => { + leftFiber.addObserver(() => completeRace(leftFiber, rightFiber, options.onSelfWin, raceIndicator, cb)) + rightFiber.addObserver(() => completeRace(rightFiber, leftFiber, options.onOtherWin, raceIndicator, cb)) + leftFiber.startFork(self) + rightFiber.startFork(other) + }, FiberId.combine(leftFiber.id(), rightFiber.id())) + })) + +const completeRace = ( + winner: Fiber.RuntimeFiber, + loser: Fiber.RuntimeFiber, + cont: (winner: Fiber.RuntimeFiber, loser: Fiber.RuntimeFiber) => Effect.Effect, + ab: MRef.MutableRef, + cb: (_: Effect.Effect) => void +): void => { + if (MRef.compareAndSet(true, false)(ab)) { + cb(cont(winner, loser)) + } +} + +/** @internal */ +export const ensuring: { + ( + finalizer: Effect.Effect + ): (self: Effect.Effect) => Effect.Effect + (self: Effect.Effect, finalizer: Effect.Effect): Effect.Effect +} = dual( + 2, + (self: Effect.Effect, finalizer: Effect.Effect): Effect.Effect => + core.uninterruptibleMask((restore) => + core.matchCauseEffect(restore(self), { + onFailure: (cause1) => + core.matchCauseEffect(finalizer, { + onFailure: (cause2) => core.failCause(internalCause.sequential(cause1, cause2)), + onSuccess: () => core.failCause(cause1) + }), + onSuccess: (a) => core.as(finalizer, a) + }) + ) +) + +/** @internal */ +export const invokeWithInterrupt: ( + self: Effect.Effect, + entries: ReadonlyArray>, + onInterrupt?: () => void +) => Effect.Effect = ( + self: Effect.Effect, + entries: ReadonlyArray>, + onInterrupt?: () => void +) => + core.fiberIdWith((id) => + ensuring( + core.flatMap( + forkDaemon(core.interruptible(self)), + (processing) => + core.async((cb) => { + const counts = entries.map((_) => _.listeners.count) + const checkDone = () => { + if (counts.every((count) => count === 0)) { + if ( + entries.every((_) => { + if (_.result.state.current._tag === "Pending") { + return true + } else if ( + _.result.state.current._tag === "Done" && + core.exitIsExit(_.result.state.current.effect) && + _.result.state.current.effect._tag === "Failure" && + internalCause.isInterrupted(_.result.state.current.effect.cause) + ) { + return true + } else { + return false + } + }) + ) { + cleanup.forEach((f) => f()) + onInterrupt?.() + cb(core.interruptFiber(processing)) + } + } + } + processing.addObserver((exit) => { + cleanup.forEach((f) => f()) + cb(exit) + }) + const cleanup = entries.map((r, i) => { + const observer = (count: number) => { + counts[i] = count + checkDone() + } + r.listeners.addObserver(observer) + return () => r.listeners.removeObserver(observer) + }) + checkDone() + return core.sync(() => { + cleanup.forEach((f) => f()) + }) + }) + ), + core.suspend(() => { + const residual = entries.flatMap((entry) => { + if (!entry.state.completed) { + return [entry] + } + return [] + }) + return core.forEachSequentialDiscard( + residual, + (entry) => complete(entry.request as any, core.exitInterrupt(id)) + ) + }) + ) + ) + +/** @internal */ +export const interruptWhenPossible = dual< + (all: Iterable>) => ( + self: Effect.Effect + ) => Effect.Effect, + ( + self: Effect.Effect, + all: Iterable> + ) => Effect.Effect +>(2, (self, all) => + core.fiberRefGetWith( + currentRequestMap, + (map) => + core.suspend(() => { + const entries = RA.fromIterable(all).flatMap((_) => map.has(_) ? [map.get(_)!] : []) + return invokeWithInterrupt(self, entries) + }) + )) + +// circular Tracer + +/** @internal */ +export const makeSpanScoped = ( + name: string, + options?: Tracer.SpanOptions | undefined +): Effect.Effect => { + options = tracer.addSpanStackTrace(options) + return core.uninterruptible( + core.withFiberRuntime((fiber) => { + const scope = Context.unsafeGet(fiber.getFiberRef(core.currentContext), scopeTag) + const span = internalEffect.unsafeMakeSpan(fiber, name, options) + const timingEnabled = fiber.getFiberRef(core.currentTracerTimingEnabled) + const clock_ = Context.get(fiber.getFiberRef(defaultServices.currentServices), clock.clockTag) + return core.as( + core.scopeAddFinalizerExit(scope, (exit) => internalEffect.endSpan(span, exit, clock_, timingEnabled)), + span + ) + }) + ) +} + +/* @internal */ +export const withTracerScoped = (value: Tracer.Tracer): Effect.Effect => + fiberRefLocallyScopedWith(defaultServices.currentServices, Context.add(tracer.tracerTag, value)) + +/** @internal */ +export const withSpanScoped: { + ( + name: string, + options?: Tracer.SpanOptions + ): (self: Effect.Effect) => Effect.Effect> + ( + self: Effect.Effect, + name: string, + options?: Tracer.SpanOptions + ): Effect.Effect> +} = function() { + const dataFirst = typeof arguments[0] !== "string" + const name = dataFirst ? arguments[1] : arguments[0] + const options = tracer.addSpanStackTrace(dataFirst ? arguments[2] : arguments[1]) + if (dataFirst) { + const self = arguments[0] + return core.flatMap( + makeSpanScoped(name, tracer.addSpanStackTrace(options)), + (span) => internalEffect.provideService(self, tracer.spanTag, span) + ) + } + return (self: Effect.Effect) => + core.flatMap( + makeSpanScoped(name, tracer.addSpanStackTrace(options)), + (span) => internalEffect.provideService(self, tracer.spanTag, span) + ) +} as any diff --git a/repos/effect/packages/effect/src/internal/fiberScope.ts b/repos/effect/packages/effect/src/internal/fiberScope.ts new file mode 100644 index 0000000..194873a --- /dev/null +++ b/repos/effect/packages/effect/src/internal/fiberScope.ts @@ -0,0 +1,71 @@ +import * as FiberId from "../FiberId.js" +import { globalValue } from "../GlobalValue.js" +import type * as RuntimeFlags from "../RuntimeFlags.js" +import * as FiberMessage from "./fiberMessage.js" +import type * as FiberRuntime from "./fiberRuntime.js" + +/** @internal */ +const FiberScopeSymbolKey = "effect/FiberScope" + +/** @internal */ +export const FiberScopeTypeId = Symbol.for(FiberScopeSymbolKey) + +export type FiberScopeTypeId = typeof FiberScopeTypeId + +/** + * A `FiberScope` represents the scope of a fiber lifetime. The scope of a + * fiber can be retrieved using `Effect.descriptor`, and when forking fibers, + * you can specify a custom scope to fork them on by using the `forkIn`. + * + * @since 2.0.0 + * @category models + */ +export interface FiberScope { + readonly [FiberScopeTypeId]: FiberScopeTypeId + get fiberId(): FiberId.FiberId + add(runtimeFlags: RuntimeFlags.RuntimeFlags, child: FiberRuntime.FiberRuntime): void +} + +/** @internal */ +class Global implements FiberScope { + readonly [FiberScopeTypeId]: FiberScopeTypeId = FiberScopeTypeId + readonly fiberId = FiberId.none + readonly roots = new Set>() + add(_runtimeFlags: RuntimeFlags.RuntimeFlags, child: FiberRuntime.FiberRuntime): void { + this.roots.add(child) + child.addObserver(() => { + this.roots.delete(child) + }) + } +} + +/** @internal */ +class Local implements FiberScope { + readonly [FiberScopeTypeId]: FiberScopeTypeId = FiberScopeTypeId + constructor( + readonly fiberId: FiberId.FiberId, + readonly parent: FiberRuntime.FiberRuntime + ) { + } + add(_runtimeFlags: RuntimeFlags.RuntimeFlags, child: FiberRuntime.FiberRuntime): void { + this.parent.tell( + FiberMessage.stateful((parentFiber) => { + parentFiber.addChild(child) + child.addObserver(() => { + parentFiber.removeChild(child) + }) + }) + ) + } +} + +/** @internal */ +export const unsafeMake = (fiber: FiberRuntime.FiberRuntime): FiberScope => { + return new Local(fiber.id(), fiber) +} + +/** @internal */ +export const globalScope = globalValue( + Symbol.for("effect/FiberScope/Global"), + () => new Global() +) diff --git a/repos/effect/packages/effect/src/internal/fiberStatus.ts b/repos/effect/packages/effect/src/internal/fiberStatus.ts new file mode 100644 index 0000000..a2ce8dc --- /dev/null +++ b/repos/effect/packages/effect/src/internal/fiberStatus.ts @@ -0,0 +1,119 @@ +import * as Equal from "../Equal.js" +import type { FiberId } from "../FiberId.js" +import type * as FiberStatus from "../FiberStatus.js" +import { pipe } from "../Function.js" +import * as Hash from "../Hash.js" +import { hasProperty } from "../Predicate.js" +import type { RuntimeFlags } from "../RuntimeFlags.js" + +const FiberStatusSymbolKey = "effect/FiberStatus" + +/** @internal */ +export const FiberStatusTypeId: FiberStatus.FiberStatusTypeId = Symbol.for( + FiberStatusSymbolKey +) as FiberStatus.FiberStatusTypeId + +/** @internal */ +export const OP_DONE = "Done" as const + +/** @internal */ +export type OP_DONE = typeof OP_DONE + +/** @internal */ +export const OP_RUNNING = "Running" as const + +/** @internal */ +export type OP_RUNNING = typeof OP_RUNNING + +/** @internal */ +export const OP_SUSPENDED = "Suspended" as const + +/** @internal */ +export type OP_SUSPENDED = typeof OP_SUSPENDED + +const DoneHash = Hash.string(`${FiberStatusSymbolKey}-${OP_DONE}`) + +/** @internal */ +class Done implements FiberStatus.Done { + readonly [FiberStatusTypeId]: FiberStatus.FiberStatusTypeId = FiberStatusTypeId + readonly _tag = OP_DONE; + [Hash.symbol](): number { + return DoneHash + } + [Equal.symbol](that: unknown): boolean { + return isFiberStatus(that) && that._tag === OP_DONE + } +} + +/** @internal */ +class Running implements FiberStatus.Running { + readonly [FiberStatusTypeId]: FiberStatus.FiberStatusTypeId = FiberStatusTypeId + readonly _tag = OP_RUNNING + constructor(readonly runtimeFlags: RuntimeFlags) {} + [Hash.symbol](): number { + return pipe( + Hash.hash(FiberStatusSymbolKey), + Hash.combine(Hash.hash(this._tag)), + Hash.combine(Hash.hash(this.runtimeFlags)), + Hash.cached(this) + ) + } + [Equal.symbol](that: unknown): boolean { + return ( + isFiberStatus(that) && + that._tag === OP_RUNNING && + this.runtimeFlags === that.runtimeFlags + ) + } +} + +/** @internal */ +class Suspended implements FiberStatus.Suspended { + readonly [FiberStatusTypeId]: FiberStatus.FiberStatusTypeId = FiberStatusTypeId + readonly _tag = OP_SUSPENDED + constructor( + readonly runtimeFlags: RuntimeFlags, + readonly blockingOn: FiberId + ) {} + [Hash.symbol](): number { + return pipe( + Hash.hash(FiberStatusSymbolKey), + Hash.combine(Hash.hash(this._tag)), + Hash.combine(Hash.hash(this.runtimeFlags)), + Hash.combine(Hash.hash(this.blockingOn)), + Hash.cached(this) + ) + } + [Equal.symbol](that: unknown): boolean { + return ( + isFiberStatus(that) && + that._tag === OP_SUSPENDED && + this.runtimeFlags === that.runtimeFlags && + Equal.equals(this.blockingOn, that.blockingOn) + ) + } +} + +/** @internal */ +export const done: FiberStatus.FiberStatus = new Done() + +/** @internal */ +export const running = (runtimeFlags: RuntimeFlags): FiberStatus.FiberStatus => new Running(runtimeFlags) + +/** @internal */ +export const suspended = ( + runtimeFlags: RuntimeFlags, + blockingOn: FiberId +): FiberStatus.FiberStatus => new Suspended(runtimeFlags, blockingOn) + +/** @internal */ +export const isFiberStatus = (u: unknown): u is FiberStatus.FiberStatus => hasProperty(u, FiberStatusTypeId) + +/** @internal */ +export const isDone = (self: FiberStatus.FiberStatus): self is FiberStatus.Done => self._tag === OP_DONE + +/** @internal */ +export const isRunning = (self: FiberStatus.FiberStatus): self is FiberStatus.Running => self._tag === OP_RUNNING + +/** @internal */ +export const isSuspended = (self: FiberStatus.FiberStatus): self is FiberStatus.Suspended => self._tag === OP_SUSPENDED diff --git a/repos/effect/packages/effect/src/internal/groupBy.ts b/repos/effect/packages/effect/src/internal/groupBy.ts new file mode 100644 index 0000000..ad53fc0 --- /dev/null +++ b/repos/effect/packages/effect/src/internal/groupBy.ts @@ -0,0 +1,530 @@ +import * as Cause from "../Cause.js" +import type * as Channel from "../Channel.js" +import * as Chunk from "../Chunk.js" +import * as Deferred from "../Deferred.js" +import * as Effect from "../Effect.js" +import * as Effectable from "../Effectable.js" +import * as Exit from "../Exit.js" +import { dual, pipe } from "../Function.js" +import type * as GroupBy from "../GroupBy.js" +import * as Option from "../Option.js" +import { pipeArguments } from "../Pipeable.js" +import { hasProperty, type Predicate } from "../Predicate.js" +import * as Queue from "../Queue.js" +import * as Ref from "../Ref.js" +import * as Scope from "../Scope.js" +import type * as Stream from "../Stream.js" +import type * as Take from "../Take.js" +import type { NoInfer } from "../Types.js" +import * as channel from "./channel.js" +import * as channelExecutor from "./channel/channelExecutor.js" +import * as core from "./core-stream.js" +import * as stream from "./stream.js" +import * as take from "./take.js" + +/** @internal */ +const GroupBySymbolKey = "effect/GroupBy" + +/** @internal */ +export const GroupByTypeId: GroupBy.GroupByTypeId = Symbol.for( + GroupBySymbolKey +) as GroupBy.GroupByTypeId + +const groupByVariance = { + /* c8 ignore next */ + _R: (_: never) => _, + /* c8 ignore next */ + _E: (_: never) => _, + /* c8 ignore next */ + _K: (_: never) => _, + /* c8 ignore next */ + _V: (_: never) => _ +} + +/** @internal */ +export const isGroupBy = (u: unknown): u is GroupBy.GroupBy => + hasProperty(u, GroupByTypeId) + +/** @internal */ +export const evaluate = dual< + ( + f: (key: K, stream: Stream.Stream) => Stream.Stream, + options?: { + readonly bufferSize?: number | undefined + } + ) => (self: GroupBy.GroupBy) => Stream.Stream, + ( + self: GroupBy.GroupBy, + f: (key: K, stream: Stream.Stream) => Stream.Stream, + options?: { + readonly bufferSize?: number | undefined + } + ) => Stream.Stream +>( + (args) => isGroupBy(args[0]), + ( + self: GroupBy.GroupBy, + f: (key: K, stream: Stream.Stream) => Stream.Stream, + options?: { + readonly bufferSize?: number | undefined + } + ): Stream.Stream => + stream.flatMap( + self.grouped, + ([key, queue]) => f(key, stream.flattenTake(stream.fromQueue(queue, { shutdown: true }))), + { concurrency: "unbounded", bufferSize: options?.bufferSize ?? 16 } + ) +) + +/** @internal */ +export const filter = dual< + (predicate: Predicate>) => (self: GroupBy.GroupBy) => GroupBy.GroupBy, + (self: GroupBy.GroupBy, predicate: Predicate) => GroupBy.GroupBy +>(2, (self: GroupBy.GroupBy, predicate: Predicate): GroupBy.GroupBy => + make( + pipe( + self.grouped, + stream.filterEffect((tuple) => { + if (predicate(tuple[0])) { + return pipe(Effect.succeed(tuple), Effect.as(true)) + } + return pipe(Queue.shutdown(tuple[1]), Effect.as(false)) + }) + ) + )) + +/** @internal */ +export const first = dual< + (n: number) => (self: GroupBy.GroupBy) => GroupBy.GroupBy, + (self: GroupBy.GroupBy, n: number) => GroupBy.GroupBy +>(2, (self: GroupBy.GroupBy, n: number): GroupBy.GroupBy => + make( + pipe( + stream.zipWithIndex(self.grouped), + stream.filterEffect((tuple) => { + const index = tuple[1] + const queue = tuple[0][1] + if (index < n) { + return pipe(Effect.succeed(tuple), Effect.as(true)) + } + return pipe(Queue.shutdown(queue), Effect.as(false)) + }), + stream.map((tuple) => tuple[0]) + ) + )) + +/** @internal */ +export const make = ( + grouped: Stream.Stream>], E, R> +): GroupBy.GroupBy => ({ + [GroupByTypeId]: groupByVariance, + pipe() { + return pipeArguments(this, arguments) + }, + grouped +}) + +// Circular with Stream + +/** @internal */ +export const groupBy = dual< + ( + f: (a: A) => Effect.Effect, + options?: { + readonly bufferSize?: number | undefined + } + ) => (self: Stream.Stream) => GroupBy.GroupBy, + ( + self: Stream.Stream, + f: (a: A) => Effect.Effect, + options?: { + readonly bufferSize?: number | undefined + } + ) => GroupBy.GroupBy +>( + (args) => stream.isStream(args[0]), + ( + self: Stream.Stream, + f: (a: A) => Effect.Effect, + options?: { + readonly bufferSize?: number | undefined + } + ): GroupBy.GroupBy => + make( + stream.unwrapScoped( + Effect.gen(function*() { + const decider = yield* Deferred.make<(key: K, value: V) => Effect.Effect>>() + const output = yield* Effect.acquireRelease( + Queue.bounded>], Option.Option>>( + options?.bufferSize ?? 16 + ), + (queue) => Queue.shutdown(queue) + ) + const ref = yield* Ref.make>(new Map()) + const add = yield* pipe( + stream.mapEffectSequential(self, f), + stream.distributedWithDynamicCallback( + options?.bufferSize ?? 16, + ([key, value]) => Effect.flatMap(Deferred.await(decider), (f) => f(key, value)), + (exit) => Queue.offer(output, exit) + ) + ) + yield* Deferred.succeed(decider, (key, _) => + pipe( + Ref.get(ref), + Effect.map((map) => Option.fromNullable(map.get(key))), + Effect.flatMap(Option.match({ + onNone: () => + Effect.flatMap(add, ([index, queue]) => + Effect.zipRight( + Ref.update(ref, (map) => map.set(key, index)), + pipe( + Queue.offer( + output, + Exit.succeed( + [ + key, + mapDequeue(queue, (exit) => + new take.TakeImpl(pipe( + exit, + Exit.map((tuple) => Chunk.of(tuple[1])) + ))) + ] as const + ) + ), + Effect.as>((n: number) => n === index) + ) + )), + onSome: (index) => Effect.succeed>((n: number) => n === index) + })) + )) + return stream.flattenExitOption(stream.fromQueue(output, { shutdown: true })) + }) + ) + ) +) + +/** @internal */ +export const mapEffectOptions = dual< + { + ( + f: (a: A) => Effect.Effect, + options?: { + readonly concurrency?: number | "unbounded" | undefined + readonly unordered?: boolean | undefined + } + ): (self: Stream.Stream) => Stream.Stream + ( + f: (a: A) => Effect.Effect, + options: { + readonly key: (a: A) => K + readonly bufferSize?: number | undefined + } + ): (self: Stream.Stream) => Stream.Stream + }, + { + ( + self: Stream.Stream, + f: (a: A) => Effect.Effect, + options?: { + readonly concurrency?: number | "unbounded" | undefined + readonly unordered?: boolean | undefined + } + ): Stream.Stream + ( + self: Stream.Stream, + f: (a: A) => Effect.Effect, + options: { + readonly key: (a: A) => K + readonly bufferSize?: number | undefined + } + ): Stream.Stream + } +>( + (args) => typeof args[0] !== "function", + (( + self: Stream.Stream, + f: (a: A) => Effect.Effect, + options?: { + readonly key?: ((a: A) => K) | undefined + readonly concurrency?: number | "unbounded" | undefined + readonly unordered?: boolean | undefined + readonly bufferSize?: number | undefined + } + ): Stream.Stream => { + if (options?.key) { + return evaluate( + groupByKey(self, options.key, { bufferSize: options.bufferSize }), + (_, s) => stream.mapEffectSequential(s, f) + ) + } + + return stream.matchConcurrency( + options?.concurrency, + () => stream.mapEffectSequential(self, f), + (n) => + options?.unordered ? + stream.flatMap(self, (a) => stream.fromEffect(f(a)), { concurrency: n }) : + stream.mapEffectPar(self, n, f) + ) + }) as any +) + +/** @internal */ +export const bindEffect = dual< + ( + tag: Exclude, + f: (_: NoInfer) => Effect.Effect, + options?: { + readonly concurrency?: number | "unbounded" | undefined + readonly bufferSize?: number | undefined + } + ) => (self: Stream.Stream) => Stream.Stream< + { [K in keyof A | N]: K extends keyof A ? A[K] : B }, + E | E2, + R | R2 + >, + ( + self: Stream.Stream, + tag: Exclude, + f: (_: NoInfer) => Effect.Effect, + options?: { + readonly concurrency?: number | "unbounded" | undefined + readonly unordered?: boolean | undefined + } + ) => Stream.Stream< + { [K in keyof A | N]: K extends keyof A ? A[K] : B }, + E | E2, + R | R2 + > +>((args) => typeof args[0] !== "string", ( + self: Stream.Stream, + tag: Exclude, + f: (_: A) => Effect.Effect, + options?: { + readonly concurrency?: number | "unbounded" | undefined + readonly unordered?: boolean | undefined + } +) => + mapEffectOptions(self, (k) => + Effect.map( + f(k), + (a) => ({ ...k, [tag]: a } as { [K in keyof A | N]: K extends keyof A ? A[K] : B }) + ), options)) + +const mapDequeue = (dequeue: Queue.Dequeue, f: (a: A) => B): Queue.Dequeue => new MapDequeue(dequeue, f) + +class MapDequeue extends Effectable.Class implements Queue.Dequeue { + readonly [Queue.DequeueTypeId] = { + _Out: (_: never) => _ + } + + constructor( + readonly dequeue: Queue.Dequeue, + readonly f: (a: A) => B + ) { + super() + } + + capacity(): number { + return Queue.capacity(this.dequeue) + } + + get size(): Effect.Effect { + return Queue.size(this.dequeue) + } + + unsafeSize(): Option.Option { + return this.dequeue.unsafeSize() + } + + get awaitShutdown(): Effect.Effect { + return Queue.awaitShutdown(this.dequeue) + } + + isActive(): boolean { + return this.dequeue.isActive() + } + + get isShutdown(): Effect.Effect { + return Queue.isShutdown(this.dequeue) + } + + get shutdown(): Effect.Effect { + return Queue.shutdown(this.dequeue) + } + + get isFull(): Effect.Effect { + return Queue.isFull(this.dequeue) + } + + get isEmpty(): Effect.Effect { + return Queue.isEmpty(this.dequeue) + } + + get take(): Effect.Effect { + return pipe(Queue.take(this.dequeue), Effect.map((a) => this.f(a))) + } + + get takeAll(): Effect.Effect> { + return pipe(Queue.takeAll(this.dequeue), Effect.map(Chunk.map((a) => this.f(a)))) + } + + takeUpTo(max: number): Effect.Effect> { + return pipe(Queue.takeUpTo(this.dequeue, max), Effect.map(Chunk.map((a) => this.f(a)))) + } + + takeBetween(min: number, max: number): Effect.Effect> { + return pipe(Queue.takeBetween(this.dequeue, min, max), Effect.map(Chunk.map((a) => this.f(a)))) + } + + takeN(n: number): Effect.Effect> { + return pipe(Queue.takeN(this.dequeue, n), Effect.map(Chunk.map((a) => this.f(a)))) + } + + poll(): Effect.Effect> { + return pipe(Queue.poll(this.dequeue), Effect.map(Option.map((a) => this.f(a)))) + } + + pipe() { + return pipeArguments(this, arguments) + } + + commit() { + return this.take + } +} + +/** @internal */ +export const groupByKey = dual< + ( + f: (a: A) => K, + options?: { + readonly bufferSize?: number | undefined + } + ) => (self: Stream.Stream) => GroupBy.GroupBy, + ( + self: Stream.Stream, + f: (a: A) => K, + options?: { + readonly bufferSize?: number | undefined + } + ) => GroupBy.GroupBy +>( + (args) => typeof args[0] !== "function", + ( + self: Stream.Stream, + f: (a: A) => K, + options?: { + readonly bufferSize?: number | undefined + } + ): GroupBy.GroupBy => { + const loop = ( + map: Map>>, + outerQueue: Queue.Queue>], E>> + ): Channel.Channel, E, E, unknown, unknown, R> => + core.readWithCause({ + onInput: (input: Chunk.Chunk) => + core.flatMap( + core.fromEffect( + Effect.forEach(groupByIterable(input, f), ([key, values]) => { + const innerQueue = map.get(key) + if (innerQueue === undefined) { + return pipe( + Queue.bounded>(options?.bufferSize ?? 16), + Effect.flatMap((innerQueue) => + pipe( + Effect.sync(() => { + map.set(key, innerQueue) + }), + Effect.zipRight( + Queue.offer(outerQueue, take.of([key, innerQueue] as const)) + ), + Effect.zipRight( + pipe( + Queue.offer(innerQueue, take.chunk(values)), + Effect.catchSomeCause((cause) => + Cause.isInterruptedOnly(cause) ? + Option.some(Effect.void) : + Option.none() + ) + ) + ) + ) + ) + ) + } + return Effect.catchSomeCause( + Queue.offer(innerQueue, take.chunk(values)), + (cause) => + Cause.isInterruptedOnly(cause) ? + Option.some(Effect.void) : + Option.none() + ) + }, { discard: true }) + ), + () => loop(map, outerQueue) + ), + onFailure: (cause) => core.fromEffect(Queue.offer(outerQueue, take.failCause(cause))), + onDone: () => + core.fromEffect( + pipe( + Effect.forEach(map.entries(), ([_, innerQueue]) => + pipe( + Queue.offer(innerQueue, take.end), + Effect.catchSomeCause((cause) => + Cause.isInterruptedOnly(cause) ? + Option.some(Effect.void) : + Option.none() + ) + ), { discard: true }), + Effect.zipRight(Queue.offer(outerQueue, take.end)) + ) + ) + }) + return make(stream.unwrapScopedWith((scope) => + Effect.gen(function*() { + const map = new Map>>() + const queue = yield* Queue.unbounded>], E>>() + yield* Scope.addFinalizer(scope, Queue.shutdown(queue)) + return yield* stream.toChannel(self).pipe( + core.pipeTo(loop(map, queue)), + channel.drain, + channelExecutor.runIn(scope), + Effect.forkIn(scope), + Effect.as(stream.flattenTake(stream.fromQueue(queue, { shutdown: true }))) + ) + }) + )) + } +) + +/** + * A variant of `groupBy` that retains the insertion order of keys. + * + * @internal + */ +const groupByIterable = dual< + (f: (value: V) => K) => (iterable: Iterable) => Chunk.Chunk<[K, Chunk.Chunk]>, + (iterable: Iterable, f: (value: V) => K) => Chunk.Chunk<[K, Chunk.Chunk]> +>(2, (iterable: Iterable, f: (value: V) => K): Chunk.Chunk<[K, Chunk.Chunk]> => { + const builder: Array<[K, Array]> = [] + const iterator = iterable[Symbol.iterator]() + const map = new Map>() + let next: IteratorResult + while ((next = iterator.next()) && !next.done) { + const value = next.value + const key = f(value) + if (map.has(key)) { + const innerBuilder = map.get(key)! + innerBuilder.push(value) + } else { + const innerBuilder: Array = [value] + builder.push([key, innerBuilder]) + map.set(key, innerBuilder) + } + } + return Chunk.unsafeFromArray( + builder.map((tuple) => [tuple[0], Chunk.unsafeFromArray(tuple[1])]) + ) +}) diff --git a/repos/effect/packages/effect/src/internal/hashMap.ts b/repos/effect/packages/effect/src/internal/hashMap.ts new file mode 100644 index 0000000..75efbc5 --- /dev/null +++ b/repos/effect/packages/effect/src/internal/hashMap.ts @@ -0,0 +1,586 @@ +import * as Equal from "../Equal.js" +import * as Dual from "../Function.js" +import { identity, pipe } from "../Function.js" +import * as Hash from "../Hash.js" +import type * as HM from "../HashMap.js" +import { format, NodeInspectSymbol, toJSON } from "../Inspectable.js" +import * as Option from "../Option.js" +import { pipeArguments } from "../Pipeable.js" +import { hasProperty } from "../Predicate.js" +import type { NoInfer } from "../Types.js" +import { fromBitmap, hashFragment, toBitmap } from "./hashMap/bitwise.js" +import { SIZE } from "./hashMap/config.js" +import * as Node from "./hashMap/node.js" + +const HashMapSymbolKey = "effect/HashMap" + +/** @internal */ +export const HashMapTypeId: HM.TypeId = Symbol.for(HashMapSymbolKey) as HM.TypeId + +type TraversalFn = (k: K, v: V) => A + +type Cont = + | [ + len: number, + children: Array>, + i: number, + f: TraversalFn, + cont: Cont + ] + | undefined + +interface VisitResult { + value: A + cont: Cont +} + +/** @internal */ +export interface HashMapImpl extends HM.HashMap { + _editable: boolean // mutable by design + _edit: number // mutable by design + _root: Node.Node // mutable by design + _size: number // mutable by design +} + +const HashMapProto: HM.HashMap = { + [HashMapTypeId]: HashMapTypeId, + [Symbol.iterator](this: HashMapImpl): Iterator<[K, V]> { + return new HashMapIterator(this, (k, v) => [k, v]) + }, + [Hash.symbol](this: HM.HashMap): number { + let hash = Hash.hash(HashMapSymbolKey) + for (const item of this) { + hash ^= pipe(Hash.hash(item[0]), Hash.combine(Hash.hash(item[1]))) + } + return Hash.cached(this, hash) + }, + [Equal.symbol](this: HashMapImpl, that: unknown): boolean { + if (isHashMap(that)) { + if ((that as HashMapImpl)._size !== this._size) { + return false + } + for (const item of this) { + const elem = pipe( + that as HM.HashMap, + getHash(item[0], Hash.hash(item[0])) + ) + if (Option.isNone(elem)) { + return false + } else { + if (!Equal.equals(item[1], elem.value)) { + return false + } + } + } + return true + } + return false + }, + toString(this: HashMapImpl) { + return format(this.toJSON()) + }, + toJSON() { + return { + _id: "HashMap", + values: Array.from(this).map(toJSON) + } + }, + [NodeInspectSymbol]() { + return this.toJSON() + }, + pipe() { + return pipeArguments(this, arguments) + } +} + +const makeImpl = ( + editable: boolean, + edit: number, + root: Node.Node, + size: number +): HashMapImpl => { + const map = Object.create(HashMapProto) + map._editable = editable + map._edit = edit + map._root = root + map._size = size + return map +} + +class HashMapIterator implements IterableIterator { + v: Option.Option> + + constructor(readonly map: HashMapImpl, readonly f: TraversalFn) { + this.v = visitLazy(this.map._root, this.f, undefined) + } + + next(): IteratorResult { + if (Option.isNone(this.v)) { + return { done: true, value: undefined } + } + const v0 = this.v.value + this.v = applyCont(v0.cont) + return { done: false, value: v0.value } + } + + [Symbol.iterator](): IterableIterator { + return new HashMapIterator(this.map, this.f) + } +} + +const applyCont = (cont: Cont): Option.Option> => + cont + ? visitLazyChildren(cont[0], cont[1], cont[2], cont[3], cont[4]) + : Option.none() + +const visitLazy = ( + node: Node.Node, + f: TraversalFn, + cont: Cont = undefined +): Option.Option> => { + switch (node._tag) { + case "LeafNode": { + if (Option.isSome(node.value)) { + return Option.some({ + value: f(node.key, node.value.value), + cont + }) + } + return applyCont(cont) + } + case "CollisionNode": + case "ArrayNode": + case "IndexedNode": { + const children = node.children + return visitLazyChildren(children.length, children, 0, f, cont) + } + default: { + return applyCont(cont) + } + } +} + +const visitLazyChildren = ( + len: number, + children: Array>, + i: number, + f: TraversalFn, + cont: Cont +): Option.Option> => { + while (i < len) { + const child = children[i++] + if (child && !Node.isEmptyNode(child)) { + return visitLazy(child, f, [len, children, i, f, cont]) + } + } + return applyCont(cont) +} + +const _empty = makeImpl(false, 0, new Node.EmptyNode(), 0) + +/** @internal */ +export const empty = (): HM.HashMap => _empty + +/** @internal */ +export const make = >( + ...entries: Entries +): HM.HashMap< + Entries[number] extends readonly [infer K, any] ? K : never, + Entries[number] extends readonly [any, infer V] ? V : never +> => fromIterable(entries) + +/** @internal */ +export const fromIterable = (entries: Iterable): HM.HashMap => { + const map = beginMutation(empty()) + for (const entry of entries) { + set(map, entry[0], entry[1]) + } + return endMutation(map) +} + +/** @internal */ +export const isHashMap: { + (u: Iterable): u is HM.HashMap + (u: unknown): u is HM.HashMap +} = (u: unknown): u is HM.HashMap => hasProperty(u, HashMapTypeId) + +/** @internal */ +export const isEmpty = (self: HM.HashMap): boolean => + self && Node.isEmptyNode((self as HashMapImpl)._root) + +/** @internal */ +export const get = Dual.dual< + (key: K1) => (self: HM.HashMap) => Option.Option, + (self: HM.HashMap, key: K1) => Option.Option +>(2, (self, key) => getHash(self, key, Hash.hash(key))) + +/** @internal */ +export const getHash = Dual.dual< + (key: K1, hash: number) => (self: HM.HashMap) => Option.Option, + (self: HM.HashMap, key: K1, hash: number) => Option.Option +>(3, (self: HM.HashMap, key: K1, hash: number) => { + let node = (self as HashMapImpl)._root + let shift = 0 + + while (true) { + switch (node._tag) { + case "LeafNode": { + return Equal.equals(key, node.key) ? node.value : Option.none() + } + case "CollisionNode": { + if (hash === node.hash) { + const children = node.children + for (let i = 0, len = children.length; i < len; ++i) { + const child = children[i]! + if ("key" in child && Equal.equals(key, child.key)) { + return child.value + } + } + } + return Option.none() + } + case "IndexedNode": { + const frag = hashFragment(shift, hash) + const bit = toBitmap(frag) + if (node.mask & bit) { + node = node.children[fromBitmap(node.mask, bit)]! + shift += SIZE + break + } + return Option.none() + } + case "ArrayNode": { + node = node.children[hashFragment(shift, hash)]! + if (node) { + shift += SIZE + break + } + return Option.none() + } + default: + return Option.none() + } + } +}) + +/** @internal */ +export const unsafeGet = Dual.dual< + (key: K1) => (self: HM.HashMap) => V, + (self: HM.HashMap, key: K1) => V +>(2, (self, key) => { + const element = getHash(self, key, Hash.hash(key)) + if (Option.isNone(element)) { + throw new Error("Expected map to contain key") + } + return element.value +}) + +/** @internal */ +export const has = Dual.dual< + (key: K1) => (self: HM.HashMap) => boolean, + (self: HM.HashMap, key: K1) => boolean +>(2, (self, key) => Option.isSome(getHash(self, key, Hash.hash(key)))) + +/** @internal */ +export const hasHash = Dual.dual< + (key: K1, hash: number) => (self: HM.HashMap) => boolean, + (self: HM.HashMap, key: K1, hash: number) => boolean +>(3, (self, key, hash) => Option.isSome(getHash(self, key, hash))) + +/** @internal */ +export const hasBy = Dual.dual< + (predicate: (value: NoInfer, key: NoInfer) => boolean) => (self: HM.HashMap) => boolean, + (self: HM.HashMap, predicate: (value: NoInfer, key: NoInfer) => boolean) => boolean +>(2, (self, predicate) => Option.isSome(findFirst(self, predicate))) + +/** @internal */ +export const set = Dual.dual< + (key: K, value: V) => (self: HM.HashMap) => HM.HashMap, + (self: HM.HashMap, key: K, value: V) => HM.HashMap +>(3, (self, key, value) => modifyAt(self, key, () => Option.some(value))) + +/** @internal */ +export const setTree = Dual.dual< + (newRoot: Node.Node, newSize: number) => (self: HM.HashMap) => HM.HashMap, + (self: HM.HashMap, newRoot: Node.Node, newSize: number) => HM.HashMap +>(3, (self: HM.HashMap, newRoot: Node.Node, newSize: number) => { + if ((self as HashMapImpl)._editable) { + ;(self as HashMapImpl)._root = newRoot + ;(self as HashMapImpl)._size = newSize + return self + } + return newRoot === (self as HashMapImpl)._root + ? self + : makeImpl( + (self as HashMapImpl)._editable, + (self as HashMapImpl)._edit, + newRoot, + newSize + ) +}) + +/** @internal */ +export const keys = (self: HM.HashMap): IterableIterator => + new HashMapIterator(self as HashMapImpl, (key) => key) + +/** @internal */ +export const values = (self: HM.HashMap): IterableIterator => + new HashMapIterator(self as HashMapImpl, (_, value) => value) + +/** @internal */ +export const entries = (self: HM.HashMap): IterableIterator<[K, V]> => + new HashMapIterator(self as HashMapImpl, (key, value) => [key, value]) + +/** @internal */ +export const size = (self: HM.HashMap): number => (self as HashMapImpl)._size + +/** @internal */ +export const countBy = Dual.dual< + (predicate: (value: NoInfer, key: NoInfer) => boolean) => (self: HM.HashMap) => number, + (self: HM.HashMap, predicate: (value: NoInfer, key: NoInfer) => boolean) => number +>(2, (self, f) => { + let count = 0 + for (const [k, a] of self) { + if (f(a, k)) { + count++ + } + } + return count +}) + +/** @internal */ +export const beginMutation = (self: HM.HashMap): HM.HashMap => + makeImpl( + true, + (self as HashMapImpl)._edit + 1, + (self as HashMapImpl)._root, + (self as HashMapImpl)._size + ) + +/** @internal */ +export const endMutation = (self: HM.HashMap): HM.HashMap => { + ;(self as HashMapImpl)._editable = false + return self +} + +/** @internal */ +export const mutate = Dual.dual< + (f: (self: HM.HashMap) => void) => (self: HM.HashMap) => HM.HashMap, + (self: HM.HashMap, f: (self: HM.HashMap) => void) => HM.HashMap +>(2, (self, f) => { + const transient = beginMutation(self) + f(transient) + return endMutation(transient) +}) + +/** @internal */ +export const modifyAt = Dual.dual< + (key: K, f: HM.HashMap.UpdateFn) => (self: HM.HashMap) => HM.HashMap, + (self: HM.HashMap, key: K, f: HM.HashMap.UpdateFn) => HM.HashMap +>(3, (self, key, f) => modifyHash(self, key, Hash.hash(key), f)) + +/** @internal */ +export const modifyHash = Dual.dual< + (key: K, hash: number, f: HM.HashMap.UpdateFn) => (self: HM.HashMap) => HM.HashMap, + (self: HM.HashMap, key: K, hash: number, f: HM.HashMap.UpdateFn) => HM.HashMap +>(4, (self: HM.HashMap, key: K, hash: number, f: HM.HashMap.UpdateFn) => { + const size = { value: (self as HashMapImpl)._size } + const newRoot = (self as HashMapImpl)._root.modify( + (self as HashMapImpl)._editable ? + (self as HashMapImpl)._edit : + NaN, + 0, + f, + hash, + key, + size + ) + return pipe(self, setTree(newRoot, size.value)) +}) + +/** @internal */ +export const modify = Dual.dual< + (key: K, f: (v: V) => V) => (self: HM.HashMap) => HM.HashMap, + (self: HM.HashMap, key: K, f: (v: V) => V) => HM.HashMap +>(3, (self, key, f) => modifyAt(self, key, Option.map(f))) + +/** @internal */ +export const union = Dual.dual< + ( + that: HM.HashMap + ) => (self: HM.HashMap) => HM.HashMap, + ( + self: HM.HashMap, + that: HM.HashMap + ) => HM.HashMap +>(2, (self: HM.HashMap, that: HM.HashMap) => { + const result: HM.HashMap = beginMutation(self) + forEach(that, (v, k) => set(result, k, v)) + return endMutation(result) +}) + +/** @internal */ +export const remove = Dual.dual< + (key: K) => (self: HM.HashMap) => HM.HashMap, + (self: HM.HashMap, key: K) => HM.HashMap +>(2, (self, key) => modifyAt(self, key, Option.none)) + +/** @internal */ +export const removeMany = Dual.dual< + (keys: Iterable) => (self: HM.HashMap) => HM.HashMap, + (self: HM.HashMap, keys: Iterable) => HM.HashMap +>(2, (self, keys) => + mutate(self, (map) => { + for (const key of keys) { + remove(key)(map) + } + })) + +/** + * Maps over the entries of the `HashMap` using the specified function. + * + * @since 2.0.0 + * @category mapping + */ +export const map = Dual.dual< + (f: (value: V, key: K) => A) => (self: HM.HashMap) => HM.HashMap, + (self: HM.HashMap, f: (value: V, key: K) => A) => HM.HashMap +>(2, (self, f) => + reduce( + self, + empty(), + (map, value, key) => set(map, key, f(value, key)) + )) + +/** @internal */ +export const flatMap = Dual.dual< + ( + f: (value: A, key: K) => HM.HashMap + ) => (self: HM.HashMap) => HM.HashMap, + (self: HM.HashMap, f: (value: A, key: K) => HM.HashMap) => HM.HashMap +>( + 2, + (self, f) => + reduce(self, empty(), (zero, value, key) => + mutate( + zero, + (map) => forEach(f(value, key), (value, key) => set(map, key, value)) + )) +) + +/** @internal */ +export const forEach = Dual.dual< + (f: (value: V, key: K) => void) => (self: HM.HashMap) => void, + (self: HM.HashMap, f: (value: V, key: K) => void) => void +>(2, (self, f) => reduce(self, void 0 as void, (_, value, key) => f(value, key))) + +/** @internal */ +export const reduce = Dual.dual< + (zero: Z, f: (accumulator: Z, value: V, key: K) => Z) => (self: HM.HashMap) => Z, + (self: HM.HashMap, zero: Z, f: (accumulator: Z, value: V, key: K) => Z) => Z +>(3, (self: HM.HashMap, zero: Z, f: (accumulator: Z, value: V, key: K) => Z) => { + const root = (self as HashMapImpl)._root + if (root._tag === "LeafNode") { + return Option.isSome(root.value) ? f(zero, root.value.value, root.key) : zero + } + if (root._tag === "EmptyNode") { + return zero + } + const toVisit = [root.children] + let children + while ((children = toVisit.pop())) { + for (let i = 0, len = children.length; i < len;) { + const child = children[i++] + if (child && !Node.isEmptyNode(child)) { + if (child._tag === "LeafNode") { + if (Option.isSome(child.value)) { + zero = f(zero, child.value.value, child.key) + } + } else { + toVisit.push(child.children) + } + } + } + } + return zero +}) + +/** @internal */ +export const filter: { + (f: (a: NoInfer, k: K) => a is B): (self: HM.HashMap) => HM.HashMap + (f: (a: NoInfer, k: K) => boolean): (self: HM.HashMap) => HM.HashMap + (self: HM.HashMap, f: (a: A, k: K) => a is B): HM.HashMap + (self: HM.HashMap, f: (a: A, k: K) => boolean): HM.HashMap +} = Dual.dual( + 2, + (self: HM.HashMap, f: (a: A, k: K) => boolean): HM.HashMap => + mutate(empty(), (map) => { + for (const [k, a] of self) { + if (f(a, k)) { + set(map, k, a) + } + } + }) +) + +/** @internal */ +export const compact = (self: HM.HashMap>) => filterMap(self, identity) + +/** @internal */ +export const filterMap = Dual.dual< + ( + f: (value: A, key: K) => Option.Option + ) => (self: HM.HashMap) => HM.HashMap, + (self: HM.HashMap, f: (value: A, key: K) => Option.Option) => HM.HashMap +>(2, (self, f) => + mutate(empty(), (map) => { + for (const [k, a] of self) { + const option = f(a, k) + if (Option.isSome(option)) { + set(map, k, option.value) + } + } + })) + +/** @internal */ +export const findFirst: { + (predicate: (a: NoInfer, k: K) => a is B): (self: HM.HashMap) => Option.Option<[K, B]> + (predicate: (a: NoInfer, k: K) => boolean): (self: HM.HashMap) => Option.Option<[K, A]> + (self: HM.HashMap, predicate: (a: A, k: K) => a is B): Option.Option<[K, B]> + (self: HM.HashMap, predicate: (a: A, k: K) => boolean): Option.Option<[K, A]> +} = Dual.dual( + 2, + (self: HM.HashMap, predicate: (a: A, k: K) => boolean): Option.Option<[K, A]> => { + for (const ka of self) { + if (predicate(ka[1], ka[0])) { + return Option.some(ka) + } + } + return Option.none() + } +) + +/** @internal */ +export const some: { + (predicate: (a: NoInfer, k: K) => boolean): (self: HM.HashMap) => boolean + (self: HM.HashMap, predicate: (a: A, k: K) => boolean): boolean +} = Dual.dual( + 2, + (self: HM.HashMap, predicate: (a: A, k: K) => boolean): boolean => { + for (const ka of self) { + if (predicate(ka[1], ka[0])) { + return true + } + } + return false + } +) + +/** @internal */ +export const every: { + (predicate: (a: NoInfer, k: K) => boolean): (self: HM.HashMap) => boolean + (self: HM.HashMap, predicate: (a: A, k: K) => boolean): boolean +} = Dual.dual( + 2, + (self: HM.HashMap, predicate: (a: A, k: K) => boolean): boolean => !some(self, (a, k) => !predicate(a, k)) +) diff --git a/repos/effect/packages/effect/src/internal/hashMap/array.ts b/repos/effect/packages/effect/src/internal/hashMap/array.ts new file mode 100644 index 0000000..45b3e40 --- /dev/null +++ b/repos/effect/packages/effect/src/internal/hashMap/array.ts @@ -0,0 +1,49 @@ +/** @internal */ +export function arrayUpdate(mutate: boolean, at: number, v: A, arr: Array) { + let out = arr + if (!mutate) { + const len = arr.length + out = new Array(len) + for (let i = 0; i < len; ++i) out[i] = arr[i]! + } + out[at] = v + return out +} + +/** @internal */ +export function arraySpliceOut(mutate: boolean, at: number, arr: Array) { + const newLen = arr.length - 1 + let i = 0 + let g = 0 + let out = arr + if (mutate) { + i = g = at + } else { + out = new Array(newLen) + while (i < at) out[g++] = arr[i++]! + } + ++i + while (i <= newLen) out[g++] = arr[i++]! + if (mutate) { + out.length = newLen + } + return out +} + +/** @internal */ +export function arraySpliceIn(mutate: boolean, at: number, v: A, arr: Array) { + const len = arr.length + if (mutate) { + let i = len + while (i >= at) arr[i--] = arr[i]! + arr[at] = v + return arr + } + let i = 0, + g = 0 + const out = new Array(len + 1) + while (i < at) out[g++] = arr[i++]! + out[at] = v + while (i < len) out[++g] = arr[i++]! + return out +} diff --git a/repos/effect/packages/effect/src/internal/hashMap/bitwise.ts b/repos/effect/packages/effect/src/internal/hashMap/bitwise.ts new file mode 100644 index 0000000..1381dcd --- /dev/null +++ b/repos/effect/packages/effect/src/internal/hashMap/bitwise.ts @@ -0,0 +1,32 @@ +import { MASK } from "./config.js" + +/** + * Hamming weight. + * + * Taken from: http://jsperf.com/hamming-weight + * + * @internal + */ +export function popcount(x: number) { + x -= (x >> 1) & 0x55555555 + x = (x & 0x33333333) + ((x >> 2) & 0x33333333) + x = (x + (x >> 4)) & 0x0f0f0f0f + x += x >> 8 + x += x >> 16 + return x & 0x7f +} + +/** @internal */ +export function hashFragment(shift: number, h: number) { + return (h >>> shift) & MASK +} + +/** @internal */ +export function toBitmap(x: number) { + return 1 << x +} + +/** @internal */ +export function fromBitmap(bitmap: number, bit: number) { + return popcount(bitmap & (bit - 1)) +} diff --git a/repos/effect/packages/effect/src/internal/hashMap/config.ts b/repos/effect/packages/effect/src/internal/hashMap/config.ts new file mode 100644 index 0000000..20f0940 --- /dev/null +++ b/repos/effect/packages/effect/src/internal/hashMap/config.ts @@ -0,0 +1,14 @@ +/** @internal */ +export const SIZE = 5 + +/** @internal */ +export const BUCKET_SIZE = Math.pow(2, SIZE) + +/** @internal */ +export const MASK = BUCKET_SIZE - 1 + +/** @internal */ +export const MAX_INDEX_NODE = BUCKET_SIZE / 2 + +/** @internal */ +export const MIN_ARRAY_NODE = BUCKET_SIZE / 4 diff --git a/repos/effect/packages/effect/src/internal/hashMap/keySet.ts b/repos/effect/packages/effect/src/internal/hashMap/keySet.ts new file mode 100644 index 0000000..f49e132 --- /dev/null +++ b/repos/effect/packages/effect/src/internal/hashMap/keySet.ts @@ -0,0 +1,8 @@ +import type { HashMap } from "../../HashMap.js" +import type { HashSet } from "../../HashSet.js" +import { makeImpl } from "../hashSet.js" + +/** @internal */ +export function keySet(self: HashMap): HashSet { + return makeImpl(self) +} diff --git a/repos/effect/packages/effect/src/internal/hashMap/node.ts b/repos/effect/packages/effect/src/internal/hashMap/node.ts new file mode 100644 index 0000000..681ca99 --- /dev/null +++ b/repos/effect/packages/effect/src/internal/hashMap/node.ts @@ -0,0 +1,391 @@ +import { equals } from "../../Equal.js" +import type { HashMap } from "../../HashMap.js" +import * as O from "../../Option.js" +import { isTagged } from "../../Predicate.js" +import * as Stack from "../stack.js" +import { arraySpliceIn, arraySpliceOut, arrayUpdate } from "./array.js" +import { fromBitmap, hashFragment, toBitmap } from "./bitwise.js" +import { MAX_INDEX_NODE, MIN_ARRAY_NODE, SIZE } from "./config.js" + +/** @internal */ +export type Node = + | EmptyNode + | LeafNode + | CollisionNode + | IndexedNode + | ArrayNode + +/** @internal */ +export interface SizeRef { + value: number // mutable by design +} + +/** @internal */ +export class EmptyNode { + readonly _tag = "EmptyNode" + + modify( + edit: number, + _shift: number, + f: HashMap.UpdateFn, + hash: number, + key: K, + size: SizeRef + ): Node { + const v = f(O.none()) + if (O.isNone(v)) return new EmptyNode() + ++size.value + return new LeafNode(edit, hash, key, v) + } +} + +/** @internal */ +export function isEmptyNode(a: unknown): a is EmptyNode { + return isTagged(a, "EmptyNode") +} + +/** @internal */ +export function isLeafNode( + node: Node +): node is EmptyNode | LeafNode | CollisionNode { + return isEmptyNode(node) || node._tag === "LeafNode" || node._tag === "CollisionNode" +} + +/** @internal */ +export function canEditNode(node: Node, edit: number): boolean { + return isEmptyNode(node) ? false : edit === node.edit +} + +/** @internal */ +export class LeafNode { + readonly _tag = "LeafNode" + + constructor( + readonly edit: number, + readonly hash: number, + readonly key: K, + public value: O.Option + ) {} + + modify( + edit: number, + shift: number, + f: HashMap.UpdateFn, + hash: number, + key: K, + size: SizeRef + ): Node { + if (equals(key, this.key)) { + const v = f(this.value) + if (v === this.value) return this + else if (O.isNone(v)) { + --size.value + return new EmptyNode() + } + if (canEditNode(this, edit)) { + this.value = v + return this + } + return new LeafNode(edit, hash, key, v) + } + const v = f(O.none()) + if (O.isNone(v)) return this + ++size.value + return mergeLeaves( + edit, + shift, + this.hash, + this, + hash, + new LeafNode(edit, hash, key, v) + ) + } +} + +/** @internal */ +export class CollisionNode { + readonly _tag = "CollisionNode" + + constructor( + readonly edit: number, + readonly hash: number, + readonly children: Array> + ) {} + + modify( + edit: number, + shift: number, + f: HashMap.UpdateFn, + hash: number, + key: K, + size: SizeRef + ): Node { + if (hash === this.hash) { + const canEdit = canEditNode(this, edit) + const list = this.updateCollisionList( + canEdit, + edit, + this.hash, + this.children, + f, + key, + size + ) + if (list === this.children) return this + + return list.length > 1 ? new CollisionNode(edit, this.hash, list) : list[0]! // collapse single element collision list + } + const v = f(O.none()) + if (O.isNone(v)) return this + ++size.value + return mergeLeaves( + edit, + shift, + this.hash, + this, + hash, + new LeafNode(edit, hash, key, v) + ) + } + + updateCollisionList( + mutate: boolean, + edit: number, + hash: number, + list: Array>, + f: HashMap.UpdateFn, + key: K, + size: SizeRef + ) { + const len = list.length + for (let i = 0; i < len; ++i) { + const child = list[i]! + if ("key" in child && equals(key, child.key)) { + const value = child.value + const newValue = f(value) + if (newValue === value) return list + if (O.isNone(newValue)) { + --size.value + return arraySpliceOut(mutate, i, list) + } + return arrayUpdate(mutate, i, new LeafNode(edit, hash, key, newValue), list) + } + } + + const newValue = f(O.none()) + if (O.isNone(newValue)) return list + ++size.value + return arrayUpdate(mutate, len, new LeafNode(edit, hash, key, newValue), list) + } +} + +/** @internal */ +export class IndexedNode { + readonly _tag = "IndexedNode" + + constructor( + readonly edit: number, + public mask: number, + public children: Array> + ) {} + + modify( + edit: number, + shift: number, + f: HashMap.UpdateFn, + hash: number, + key: K, + size: SizeRef + ): Node { + const mask = this.mask + const children = this.children + const frag = hashFragment(shift, hash) + const bit = toBitmap(frag) + const indx = fromBitmap(mask, bit) + const exists = mask & bit + const canEdit = canEditNode(this, edit) + + if (!exists) { + const _newChild = new EmptyNode().modify(edit, shift + SIZE, f, hash, key, size) + if (!_newChild) return this + return children.length >= MAX_INDEX_NODE ? + expand(edit, frag, _newChild, mask, children) : + new IndexedNode(edit, mask | bit, arraySpliceIn(canEdit, indx, _newChild, children)) + } + + const current = children[indx]! + const child = current.modify(edit, shift + SIZE, f, hash, key, size) + + if (current === child) return this + let bitmap = mask + let newChildren + if (isEmptyNode(child)) { + // remove + bitmap &= ~bit + if (!bitmap) return new EmptyNode() + if (children.length <= 2 && isLeafNode(children[indx ^ 1]!)) { + return children[indx ^ 1]! // collapse + } + + newChildren = arraySpliceOut(canEdit, indx, children) + } else { + // modify + newChildren = arrayUpdate(canEdit, indx, child, children) + } + + if (canEdit) { + this.mask = bitmap + this.children = newChildren + return this + } + + return new IndexedNode(edit, bitmap, newChildren) + } +} + +/** @internal */ +export class ArrayNode { + readonly _tag = "ArrayNode" + + constructor( + readonly edit: number, + public size: number, + public children: Array> + ) {} + + modify( + edit: number, + shift: number, + f: HashMap.UpdateFn, + hash: number, + key: K, + size: SizeRef + ): Node { + let count = this.size + const children = this.children + const frag = hashFragment(shift, hash) + const child = children[frag] + const newChild = (child || new EmptyNode()).modify( + edit, + shift + SIZE, + f, + hash, + key, + size + ) + + if (child === newChild) return this + + const canEdit = canEditNode(this, edit) + let newChildren + if (isEmptyNode(child) && !isEmptyNode(newChild)) { + // add + ++count + newChildren = arrayUpdate(canEdit, frag, newChild, children) + } else if (!isEmptyNode(child) && isEmptyNode(newChild)) { + // remove + --count + if (count <= MIN_ARRAY_NODE) { + return pack(edit, count, frag, children) + } + newChildren = arrayUpdate(canEdit, frag, new EmptyNode(), children) + } else { + // modify + newChildren = arrayUpdate(canEdit, frag, newChild, children) + } + + if (canEdit) { + this.size = count + this.children = newChildren + return this + } + return new ArrayNode(edit, count, newChildren) + } +} + +function pack( + edit: number, + count: number, + removed: number, + elements: Array> +) { + const children = new Array>(count - 1) + let g = 0 + let bitmap = 0 + for (let i = 0, len = elements.length; i < len; ++i) { + if (i !== removed) { + const elem = elements[i] + if (elem && !isEmptyNode(elem)) { + children[g++] = elem + bitmap |= 1 << i + } + } + } + return new IndexedNode(edit, bitmap, children) +} + +function expand( + edit: number, + frag: number, + child: Node, + bitmap: number, + subNodes: Array> +) { + const arr = [] + let bit = bitmap + let count = 0 + for (let i = 0; bit; ++i) { + if (bit & 1) arr[i] = subNodes[count++]! + bit >>>= 1 + } + arr[frag] = child + return new ArrayNode(edit, count + 1, arr) +} + +function mergeLeavesInner( + edit: number, + shift: number, + h1: number, + n1: Node, + h2: number, + n2: Node +): Node | ((child: Node) => Node) { + if (h1 === h2) return new CollisionNode(edit, h1, [n2, n1]) + const subH1 = hashFragment(shift, h1) + const subH2 = hashFragment(shift, h2) + + if (subH1 === subH2) { + return (child) => new IndexedNode(edit, toBitmap(subH1) | toBitmap(subH2), [child]) + } else { + const children = subH1 < subH2 ? [n1, n2] : [n2, n1] + return new IndexedNode(edit, toBitmap(subH1) | toBitmap(subH2), children) + } +} + +function mergeLeaves( + edit: number, + shift: number, + h1: number, + n1: Node, + h2: number, + n2: Node +): Node { + let stack: Stack.Stack<(node: Node) => Node> | undefined = undefined + let currentShift = shift + + while (true) { + const res = mergeLeavesInner(edit, currentShift, h1, n1, h2, n2) + + if (typeof res === "function") { + stack = Stack.make(res, stack) + currentShift = currentShift + SIZE + } else { + let final = res + while (stack != null) { + final = stack.value(final) + stack = stack.previous + } + return final + } + } +} diff --git a/repos/effect/packages/effect/src/internal/hashSet.ts b/repos/effect/packages/effect/src/internal/hashSet.ts new file mode 100644 index 0000000..a60b87d --- /dev/null +++ b/repos/effect/packages/effect/src/internal/hashSet.ts @@ -0,0 +1,323 @@ +import * as Equal from "../Equal.js" +import { dual } from "../Function.js" +import * as Hash from "../Hash.js" +import type { HashMap } from "../HashMap.js" +import type * as HS from "../HashSet.js" +import { format, NodeInspectSymbol, toJSON } from "../Inspectable.js" +import { pipeArguments } from "../Pipeable.js" +import type { Predicate, Refinement } from "../Predicate.js" +import { hasProperty } from "../Predicate.js" +import type { NoInfer } from "../Types.js" +import * as HM from "./hashMap.js" + +const HashSetSymbolKey = "effect/HashSet" + +/** @internal */ +export const HashSetTypeId: HS.TypeId = Symbol.for(HashSetSymbolKey) as HS.TypeId + +/** @internal */ +export interface HashSetImpl extends HS.HashSet { + readonly _keyMap: HashMap +} + +const HashSetProto: Omit, "_keyMap"> = { + [HashSetTypeId]: HashSetTypeId, + [Symbol.iterator](this: HashSetImpl): Iterator { + return HM.keys(this._keyMap) + }, + [Hash.symbol](this: HashSetImpl): number { + return Hash.cached( + this, + Hash.combine(Hash.hash(this._keyMap))(Hash.hash(HashSetSymbolKey)) + ) + }, + [Equal.symbol](this: HashSetImpl, that: unknown): boolean { + if (isHashSet(that)) { + return ( + HM.size(this._keyMap) === HM.size((that as HashSetImpl)._keyMap) && + Equal.equals(this._keyMap, (that as HashSetImpl)._keyMap) + ) + } + return false + }, + toString() { + return format(this.toJSON()) + }, + toJSON() { + return { + _id: "HashSet", + values: Array.from(this).map(toJSON) + } + }, + [NodeInspectSymbol]() { + return this.toJSON() + }, + pipe() { + return pipeArguments(this, arguments) + } +} + +/** @internal */ +export const makeImpl = (keyMap: HashMap): HashSetImpl => { + const set = Object.create(HashSetProto) + set._keyMap = keyMap + return set +} + +/** @internal */ +export const isHashSet: { + (u: Iterable): u is HS.HashSet + (u: unknown): u is HS.HashSet +} = (u: unknown): u is HS.HashSet => hasProperty(u, HashSetTypeId) + +const _empty = makeImpl(HM.empty()) + +/** @internal */ +export const empty = (): HS.HashSet => _empty + +/** @internal */ +export const fromIterable = (elements: Iterable): HS.HashSet => { + const set = beginMutation(empty()) + for (const value of elements) { + add(set, value) + } + return endMutation(set) +} + +/** @internal */ +export const make = >(...elements: As): HS.HashSet => { + const set = beginMutation(empty()) + for (const value of elements) { + add(set, value) + } + return endMutation(set) +} + +/** @internal */ +export const has = dual< + (value: A) => (self: HS.HashSet) => boolean, + (self: HS.HashSet, value: A) => boolean +>(2, (self: HS.HashSet, value: A) => HM.has((self as HashSetImpl)._keyMap, value)) + +/** @internal */ +export const some = dual< + (f: Predicate) => (self: HS.HashSet) => boolean, + (self: HS.HashSet, f: Predicate) => boolean +>(2, (self, f) => { + let found = false + for (const value of self) { + found = f(value) + if (found) { + break + } + } + return found +}) + +/** @internal */ +export const every: { + (refinement: Refinement, B>): (self: HS.HashSet) => self is HS.HashSet + (predicate: Predicate): (self: HS.HashSet) => boolean + (self: HS.HashSet, refinement: Refinement): self is HS.HashSet + (self: HS.HashSet, predicate: Predicate): boolean +} = dual( + 2, + (self: HS.HashSet, refinement: Refinement): self is HS.HashSet => + !some(self, (a) => !refinement(a)) +) + +/** @internal */ +export const isSubset = dual< + (that: HS.HashSet) => (self: HS.HashSet) => boolean, + (self: HS.HashSet, that: HS.HashSet) => boolean +>(2, (self, that) => every(self, (value) => has(that, value))) + +/** @internal */ +export const values = (self: HS.HashSet): IterableIterator => HM.keys((self as HashSetImpl)._keyMap) + +/** @internal */ +export const size = (self: HS.HashSet): number => HM.size((self as HashSetImpl)._keyMap) + +/** @internal */ +export const beginMutation = (self: HS.HashSet): HS.HashSet => + makeImpl(HM.beginMutation((self as HashSetImpl)._keyMap)) + +/** @internal */ +export const endMutation = (self: HS.HashSet): HS.HashSet => { + ;((self as HashSetImpl)._keyMap as HM.HashMapImpl)._editable = false + return self +} + +/** @internal */ +export const mutate = dual< + (f: (set: HS.HashSet) => void) => (self: HS.HashSet) => HS.HashSet, + (self: HS.HashSet, f: (set: HS.HashSet) => void) => HS.HashSet +>(2, (self, f) => { + const transient = beginMutation(self) + f(transient) + return endMutation(transient) +}) + +/** @internal */ +export const add = dual< + (value: A) => (self: HS.HashSet) => HS.HashSet, + (self: HS.HashSet, value: A) => HS.HashSet +>( + 2, + (self: HS.HashSet, value: A) => + ((self as HashSetImpl)._keyMap as HM.HashMapImpl)._editable + ? (HM.set(value as A, true as unknown)((self as HashSetImpl)._keyMap), self) + : makeImpl(HM.set(value as A, true as unknown)((self as HashSetImpl)._keyMap)) +) + +/** @internal */ +export const remove = dual< + (value: A) => (self: HS.HashSet) => HS.HashSet, + (self: HS.HashSet, value: A) => HS.HashSet +>( + 2, + (self: HS.HashSet, value: A) => + (((self as HashSetImpl)._keyMap) as HM.HashMapImpl)._editable + ? (HM.remove(value)((self as HashSetImpl)._keyMap), self) + : makeImpl(HM.remove(value)((self as HashSetImpl)._keyMap)) +) + +/** @internal */ +export const difference = dual< + (that: Iterable) => (self: HS.HashSet) => HS.HashSet, + (self: HS.HashSet, that: Iterable) => HS.HashSet +>(2, (self, that) => + mutate(self, (set) => { + for (const value of that) { + remove(set, value) + } + })) + +/** @internal */ +export const intersection = dual< + (that: Iterable) => (self: HS.HashSet) => HS.HashSet, + (self: HS.HashSet, that: Iterable) => HS.HashSet +>(2, (self, that) => + mutate(empty(), (set) => { + for (const value of that) { + if (has(value)(self)) { + add(value)(set) + } + } + })) + +/** @internal */ +export const union = dual< + (that: Iterable) => (self: HS.HashSet) => HS.HashSet, + (self: HS.HashSet, that: Iterable) => HS.HashSet +>(2, (self, that) => + mutate(empty(), (set) => { + forEach(self, (value) => add(set, value)) + for (const value of that) { + add(set, value) + } + })) + +/** @internal */ +export const toggle = dual< + (value: A) => (self: HS.HashSet) => HS.HashSet, + (self: HS.HashSet, value: A) => HS.HashSet +>(2, (self, value) => has(self, value) ? remove(self, value) : add(self, value)) + +/** @internal */ +export const map = dual< + (f: (a: A) => B) => (self: HS.HashSet) => HS.HashSet, + (self: HS.HashSet, f: (a: A) => B) => HS.HashSet +>(2, (self, f) => + mutate(empty(), (set) => { + forEach(self, (a) => { + const b = f(a) + if (!has(set, b)) { + add(set, b) + } + }) + })) + +/** @internal */ +export const flatMap = dual< + (f: (a: A) => Iterable) => (self: HS.HashSet) => HS.HashSet, + (self: HS.HashSet, f: (a: A) => Iterable) => HS.HashSet +>(2, (self, f) => + mutate(empty(), (set) => { + forEach(self, (a) => { + for (const b of f(a)) { + if (!has(set, b)) { + add(set, b) + } + } + }) + })) + +/** @internal */ +export const forEach = dual< + (f: (value: A) => void) => (self: HS.HashSet) => void, + (self: HS.HashSet, f: (value: A) => void) => void +>(2, (self: HS.HashSet, f: (value: A) => void) => + HM.forEach( + (self as HashSetImpl)._keyMap, + (_, k) => f(k) + )) + +/** @internal */ +export const reduce = dual< + (zero: Z, f: (accumulator: Z, value: A) => Z) => (self: HS.HashSet) => Z, + (self: HS.HashSet, zero: Z, f: (accumulator: Z, value: A) => Z) => Z +>(3, (self: HS.HashSet, zero: Z, f: (accumulator: Z, value: A) => Z) => + HM.reduce( + (self as HashSetImpl)._keyMap, + zero, + (z, _, a) => f(z, a) + )) + +/** @internal */ +export const filter: { + (refinement: Refinement, B>): (self: HS.HashSet) => HS.HashSet + (predicate: Predicate>): (self: HS.HashSet) => HS.HashSet + (self: HS.HashSet, refinement: Refinement): HS.HashSet + (self: HS.HashSet, predicate: Predicate): HS.HashSet +} = dual(2, (self: HS.HashSet, f: Predicate) => { + return mutate(empty(), (set) => { + const iterator = values(self) + let next: IteratorResult + while (!(next = iterator.next()).done) { + const value = next.value + if (f(value)) { + add(set, value) + } + } + }) +}) + +/** @internal */ +export const partition: { + ( + refinement: Refinement, B> + ): (self: HS.HashSet) => [excluded: HS.HashSet>, satisfying: HS.HashSet] + ( + predicate: Predicate> + ): (self: HS.HashSet) => [excluded: HS.HashSet, satisfying: HS.HashSet] + ( + self: HS.HashSet, + refinement: Refinement + ): [excluded: HS.HashSet>, satisfying: HS.HashSet] + (self: HS.HashSet, predicate: Predicate): [excluded: HS.HashSet, satisfying: HS.HashSet] +} = dual(2, (self: HS.HashSet, predicate: Predicate): [excluded: HS.HashSet, satisfying: HS.HashSet] => { + const iterator = values(self) + let next: IteratorResult + const right = beginMutation(empty()) + const left = beginMutation(empty()) + while (!(next = iterator.next()).done) { + const value = next.value + if (predicate(value)) { + add(right, value) + } else { + add(left, value) + } + } + return [endMutation(left), endMutation(right)] +}) diff --git a/repos/effect/packages/effect/src/internal/keyedPool.ts b/repos/effect/packages/effect/src/internal/keyedPool.ts new file mode 100644 index 0000000..b91988b --- /dev/null +++ b/repos/effect/packages/effect/src/internal/keyedPool.ts @@ -0,0 +1,244 @@ +import type * as Deferred from "../Deferred.js" +import * as Duration from "../Duration.js" +import type * as Effect from "../Effect.js" +import * as Equal from "../Equal.js" +import { dual, pipe } from "../Function.js" +import * as Hash from "../Hash.js" +import * as HashMap from "../HashMap.js" +import type * as KeyedPool from "../KeyedPool.js" +import * as MutableRef from "../MutableRef.js" +import * as Option from "../Option.js" +import { pipeArguments } from "../Pipeable.js" +import type * as Pool from "../Pool.js" +import * as Predicate from "../Predicate.js" +import type * as Scope from "../Scope.js" +import * as core from "./core.js" +import * as fiberRuntime from "./fiberRuntime.js" +import * as pool from "./pool.js" + +/** @internal */ +const KeyedPoolSymbolKey = "effect/KeyedPool" + +/** @internal */ +export const KeyedPoolTypeId: KeyedPool.KeyedPoolTypeId = Symbol.for( + KeyedPoolSymbolKey +) as KeyedPool.KeyedPoolTypeId + +const KeyedPoolMapValueSymbol = Symbol.for("effect/KeyedPool/MapValue") +type KeyedPoolMapValueSymbol = typeof KeyedPoolMapValueSymbol + +const keyedPoolVariance = { + /* c8 ignore next */ + _K: (_: unknown) => _, + /* c8 ignore next */ + _E: (_: never) => _, + /* c8 ignore next */ + _A: (_: any) => _ +} + +class KeyedPoolImpl implements KeyedPool.KeyedPool { + readonly [KeyedPoolTypeId] = keyedPoolVariance + constructor( + readonly getOrCreatePool: (key: K) => Effect.Effect>, + readonly activePools: Effect.Effect>> + ) {} + get(key: K): Effect.Effect { + return core.flatMap(this.getOrCreatePool(key), pool.get) + } + invalidate(item: A): Effect.Effect { + return core.flatMap(this.activePools, core.forEachSequentialDiscard((pool) => pool.invalidate(item))) + } + pipe() { + return pipeArguments(this, arguments) + } +} + +type MapValue = Complete | Pending + +class Complete implements Equal.Equal { + readonly _tag = "Complete" + readonly [KeyedPoolMapValueSymbol]: KeyedPoolMapValueSymbol = KeyedPoolMapValueSymbol + constructor(readonly pool: Pool.Pool) {} + [Hash.symbol](): number { + return pipe( + Hash.string("effect/KeyedPool/Complete"), + Hash.combine(Hash.hash(this.pool)), + Hash.cached(this) + ) + } + [Equal.symbol](u: unknown): boolean { + return isComplete(u) && Equal.equals(this.pool, u.pool) + } +} + +const isComplete = (u: unknown): u is Complete => + Predicate.isTagged(u, "Complete") && KeyedPoolMapValueSymbol in u + +class Pending implements Equal.Equal { + readonly _tag = "Pending" + readonly [KeyedPoolMapValueSymbol]: KeyedPoolMapValueSymbol = KeyedPoolMapValueSymbol + constructor(readonly deferred: Deferred.Deferred>) {} + [Hash.symbol](): number { + return pipe( + Hash.string("effect/KeyedPool/Pending"), + Hash.combine(Hash.hash(this.deferred)), + Hash.cached(this) + ) + } + [Equal.symbol](u: unknown): boolean { + return isPending(u) && Equal.equals(this.deferred, u.deferred) + } +} + +const isPending = (u: unknown): u is Pending => + Predicate.isTagged(u, "Pending") && KeyedPoolMapValueSymbol in u + +const makeImpl = ( + get: (key: K) => Effect.Effect, + min: (key: K) => number, + max: (key: K) => number, + timeToLive: (key: K) => Option.Option +): Effect.Effect, never, R | Scope.Scope> => + pipe( + fiberRuntime.all([ + core.context(), + core.fiberId, + core.sync(() => MutableRef.make(HashMap.empty>())), + fiberRuntime.scopeMake() + ]), + core.map(([context, fiberId, map, scope]) => { + const getOrCreatePool = (key: K): Effect.Effect> => + core.suspend(() => { + let value: MapValue | undefined = Option.getOrUndefined(HashMap.get(MutableRef.get(map), key)) + if (value === undefined) { + return core.uninterruptibleMask((restore) => { + const deferred = core.deferredUnsafeMake>(fiberId) + value = new Pending(deferred) + let previous: MapValue | undefined = undefined + if (HashMap.has(MutableRef.get(map), key)) { + previous = Option.getOrUndefined(HashMap.get(MutableRef.get(map), key)) + } else { + MutableRef.update(map, HashMap.set(key, value as MapValue)) + } + if (previous === undefined) { + return pipe( + restore( + fiberRuntime.scopeExtend( + pool.makeWithTTL({ + acquire: core.provideContext(get(key), context), + min: min(key), + max: max(key), + timeToLive: Option.getOrElse(timeToLive(key), () => Duration.infinity) + }), + scope + ) + ), + core.matchCauseEffect({ + onFailure: (cause) => { + const current = Option.getOrUndefined(HashMap.get(MutableRef.get(map), key)) + if (Equal.equals(current, value)) { + MutableRef.update(map, HashMap.remove(key)) + } + return core.zipRight( + core.deferredFailCause(deferred, cause), + core.failCause(cause) + ) + }, + onSuccess: (pool) => { + MutableRef.update(map, HashMap.set(key, new Complete(pool) as MapValue)) + return core.as( + core.deferredSucceed(deferred, pool), + pool + ) + } + }) + ) + } + switch (previous._tag) { + case "Complete": { + return core.succeed(previous.pool) + } + case "Pending": { + return restore(core.deferredAwait(previous.deferred)) + } + } + }) + } + switch (value._tag) { + case "Complete": { + return core.succeed(value.pool) + } + case "Pending": { + return core.deferredAwait(value.deferred) + } + } + }) + const activePools: Effect.Effect>> = core.suspend(() => + core.forEachSequential(HashMap.toValues(MutableRef.get(map)), (value) => { + switch (value._tag) { + case "Complete": { + return core.succeed(value.pool) + } + case "Pending": { + return core.deferredAwait(value.deferred) + } + } + }) + ) + return new KeyedPoolImpl(getOrCreatePool, activePools) + }) + ) + +/** @internal */ +export const make = ( + options: { + readonly acquire: (key: K) => Effect.Effect + readonly size: number + } +): Effect.Effect, never, R | Scope.Scope> => + makeImpl(options.acquire, () => options.size, () => options.size, () => Option.none()) + +/** @internal */ +export const makeWith = ( + options: { + readonly acquire: (key: K) => Effect.Effect + readonly size: (key: K) => number + } +): Effect.Effect, never, R | Scope.Scope> => + makeImpl(options.acquire, options.size, options.size, () => Option.none()) + +/** @internal */ +export const makeWithTTL = ( + options: { + readonly acquire: (key: K) => Effect.Effect + readonly min: (key: K) => number + readonly max: (key: K) => number + readonly timeToLive: Duration.DurationInput + } +): Effect.Effect, never, R | Scope.Scope> => { + const timeToLive = Duration.decode(options.timeToLive) + return makeImpl(options.acquire, options.min, options.max, () => Option.some(timeToLive)) +} + +/** @internal */ +export const makeWithTTLBy = ( + options: { + readonly acquire: (key: K) => Effect.Effect + readonly min: (key: K) => number + readonly max: (key: K) => number + readonly timeToLive: (key: K) => Duration.DurationInput + } +): Effect.Effect, never, R | Scope.Scope> => + makeImpl(options.acquire, options.min, options.max, (key) => Option.some(Duration.decode(options.timeToLive(key)))) + +/** @internal */ +export const get = dual< + (key: K) => (self: KeyedPool.KeyedPool) => Effect.Effect, + (self: KeyedPool.KeyedPool, key: K) => Effect.Effect +>(2, (self, key) => self.get(key)) + +/** @internal */ +export const invalidate = dual< + (item: A) => (self: KeyedPool.KeyedPool) => Effect.Effect, + (self: KeyedPool.KeyedPool, item: A) => Effect.Effect +>(2, (self, item) => self.invalidate(item)) diff --git a/repos/effect/packages/effect/src/internal/layer.ts b/repos/effect/packages/effect/src/internal/layer.ts new file mode 100644 index 0000000..685766c --- /dev/null +++ b/repos/effect/packages/effect/src/internal/layer.ts @@ -0,0 +1,1483 @@ +import type * as Arr from "../Array.js" +import * as Cause from "../Cause.js" +import * as Clock from "../Clock.js" +import * as Context from "../Context.js" +import * as Duration from "../Duration.js" +import type * as Effect from "../Effect.js" +import type * as Exit from "../Exit.js" +import type { FiberRef } from "../FiberRef.js" +import * as FiberRefsPatch from "../FiberRefsPatch.js" +import type { LazyArg } from "../Function.js" +import { constTrue, dual, pipe } from "../Function.js" +import * as HashMap from "../HashMap.js" +import type * as Layer from "../Layer.js" +import type * as ManagedRuntime from "../ManagedRuntime.js" +import { pipeArguments } from "../Pipeable.js" +import { hasProperty } from "../Predicate.js" +import type * as Runtime from "../Runtime.js" +import type * as Schedule from "../Schedule.js" +import * as ScheduleDecision from "../ScheduleDecision.js" +import * as Intervals from "../ScheduleIntervals.js" +import * as Scope from "../Scope.js" +import type * as Synchronized from "../SynchronizedRef.js" +import type * as Tracer from "../Tracer.js" +import type * as Types from "../Types.js" +import * as effect from "./core-effect.js" +import * as core from "./core.js" +import * as circular from "./effect/circular.js" +import * as ExecutionStrategy from "./executionStrategy.js" +import * as fiberRuntime from "./fiberRuntime.js" +import * as circularManagedRuntime from "./managedRuntime/circular.js" +import * as EffectOpCodes from "./opCodes/effect.js" +import * as OpCodes from "./opCodes/layer.js" +import * as ref from "./ref.js" +import * as runtime from "./runtime.js" +import * as runtimeFlags from "./runtimeFlags.js" +import * as synchronized from "./synchronizedRef.js" +import * as tracer from "./tracer.js" + +/** @internal */ +const LayerSymbolKey = "effect/Layer" + +/** @internal */ +export const LayerTypeId: Layer.LayerTypeId = Symbol.for( + LayerSymbolKey +) as Layer.LayerTypeId + +const layerVariance = { + /* c8 ignore next */ + _RIn: (_: never) => _, + /* c8 ignore next */ + _E: (_: never) => _, + /* c8 ignore next */ + _ROut: (_: unknown) => _ +} + +/** @internal */ +export const proto = { + [LayerTypeId]: layerVariance, + pipe() { + return pipeArguments(this, arguments) + } +} + +/** @internal */ +const MemoMapTypeIdKey = "effect/Layer/MemoMap" + +/** @internal */ +export const MemoMapTypeId: Layer.MemoMapTypeId = Symbol.for( + MemoMapTypeIdKey +) as Layer.MemoMapTypeId + +/** @internal */ +export const CurrentMemoMap = Context.Reference()("effect/Layer/CurrentMemoMap", { + defaultValue: () => unsafeMakeMemoMap() +}) + +/** @internal */ +export type Primitive = + | ExtendScope + | Fold + | Fresh + | FromEffect + | Scoped + | Suspend + | Locally + | ProvideTo + | ZipWith + | ZipWithPar + | MergeAll + +/** @internal */ +export type Op = Layer.Layer & Body & { + readonly _op_layer: Tag +} + +/** @internal */ +export interface ExtendScope extends + Op + }> +{} + +/** @internal */ +export interface Fold extends + Op + failureK(cause: Cause.Cause): Layer.Layer + successK(context: Context.Context): Layer.Layer + }> +{} + +/** @internal */ +export interface Fresh extends + Op + }> +{} + +/** @internal */ +export interface FromEffect extends + Op> + }> +{} + +/** @internal */ +export interface Scoped extends + Op> + }> +{} + +/** @internal */ +export interface Suspend extends + Op + }> +{} + +/** @internal */ +export interface Locally extends + Op<"Locally", { + readonly self: Layer.Layer + f(_: Effect.Effect): Effect.Effect + }> +{} + +/** @internal */ +export interface ProvideTo extends + Op + readonly second: Layer.Layer + }> +{} + +/** @internal */ +export interface ZipWith extends + Op + readonly second: Layer.Layer + zipK(left: Context.Context, right: Context.Context): Context.Context + }> +{} + +/** @internal */ +export interface ZipWithPar extends + Op + readonly second: Layer.Layer + zipK(left: Context.Context, right: Context.Context): Context.Context + }> +{} + +/** @internal */ +export interface MergeAll extends + Op> + }> +{} + +/** @internal */ +export const isLayer = (u: unknown): u is Layer.Layer => hasProperty(u, LayerTypeId) + +/** @internal */ +export const isFresh = (self: Layer.Layer): boolean => { + return (self as Primitive)._op_layer === OpCodes.OP_FRESH +} + +// ----------------------------------------------------------------------------- +// MemoMap +// ----------------------------------------------------------------------------- + +/** @internal */ +class MemoMapImpl implements Layer.MemoMap { + readonly [MemoMapTypeId]: Layer.MemoMapTypeId + constructor( + readonly ref: Synchronized.SynchronizedRef< + Map< + Layer.Layer, + readonly [Effect.Effect, Scope.Scope.Finalizer] + > + > + ) { + this[MemoMapTypeId] = MemoMapTypeId + } + + /** + * Checks the memo map to see if a layer exists. If it is, immediately + * returns it. Otherwise, obtains the layer, stores it in the memo map, + * and adds a finalizer to the `Scope`. + */ + getOrElseMemoize( + layer: Layer.Layer, + scope: Scope.Scope + ): Effect.Effect, E, RIn> { + return pipe( + synchronized.modifyEffect(this.ref, (map) => { + const inMap = map.get(layer) + if (inMap !== undefined) { + const [acquire, release] = inMap + const cached: Effect.Effect, E> = pipe( + acquire as Effect.Effect], E>, + core.flatMap(([patch, b]) => pipe(effect.patchFiberRefs(patch), core.as(b))), + core.onExit(core.exitMatch({ + onFailure: () => core.void, + onSuccess: () => core.scopeAddFinalizerExit(scope, release) + })) + ) + return core.succeed([cached, map] as const) + } + return pipe( + ref.make(0), + core.flatMap((observers) => + pipe( + core.deferredMake], E>(), + core.flatMap((deferred) => + pipe( + ref.make(() => core.void), + core.map((finalizerRef) => { + const resource = core.uninterruptibleMask((restore) => + pipe( + fiberRuntime.scopeMake(), + core.flatMap((innerScope) => + pipe( + restore(core.flatMap( + makeBuilder(layer, innerScope, true), + (f) => effect.diffFiberRefs(f(this)) + )), + core.exit, + core.flatMap((exit) => { + switch (exit._tag) { + case EffectOpCodes.OP_FAILURE: { + return pipe( + core.deferredFailCause(deferred, exit.effect_instruction_i0), + core.zipRight(core.scopeClose(innerScope, exit)), + core.zipRight(core.failCause(exit.effect_instruction_i0)) + ) + } + case EffectOpCodes.OP_SUCCESS: { + return pipe( + ref.set(finalizerRef, (exit) => + pipe( + core.scopeClose(innerScope, exit), + core.whenEffect( + ref.modify(observers, (n) => [n === 1, n - 1] as const) + ), + core.asVoid + )), + core.zipRight(ref.update(observers, (n) => n + 1)), + core.zipRight( + core.scopeAddFinalizerExit(scope, (exit) => + pipe( + core.sync(() => map.delete(layer)), + core.zipRight(ref.get(finalizerRef)), + core.flatMap((finalizer) => finalizer(exit)) + )) + ), + core.zipRight(core.deferredSucceed(deferred, exit.effect_instruction_i0)), + core.as(exit.effect_instruction_i0[1]) + ) + } + } + }) + ) + ) + ) + ) + const memoized = [ + pipe( + core.deferredAwait(deferred), + core.onExit(core.exitMatchEffect({ + onFailure: () => core.void, + onSuccess: () => ref.update(observers, (n) => n + 1) + })) + ), + (exit: Exit.Exit) => + pipe( + ref.get(finalizerRef), + core.flatMap((finalizer) => finalizer(exit)) + ) + ] as const + return [ + resource, + isFresh(layer) ? map : map.set(layer, memoized) + ] as const + }) + ) + ) + ) + ) + ) + }), + core.flatten + ) + } +} + +/** @internal */ +export const makeMemoMap: Effect.Effect = core.suspend(() => + core.map( + circular.makeSynchronized< + Map< + Layer.Layer, + readonly [ + Effect.Effect, + Scope.Scope.Finalizer + ] + > + >(new Map()), + (ref) => new MemoMapImpl(ref) + ) +) + +/** @internal */ +export const unsafeMakeMemoMap = (): Layer.MemoMap => new MemoMapImpl(circular.unsafeMakeSynchronized(new Map())) + +/** @internal */ +export const build = ( + self: Layer.Layer +): Effect.Effect, E, RIn | Scope.Scope> => + fiberRuntime.scopeWith((scope) => buildWithScope(self, scope)) + +/** @internal */ +export const buildWithScope = dual< + ( + scope: Scope.Scope + ) => (self: Layer.Layer) => Effect.Effect, E, RIn>, + ( + self: Layer.Layer, + scope: Scope.Scope + ) => Effect.Effect, E, RIn> +>(2, (self, scope) => + core.flatMap( + makeMemoMap, + (memoMap) => buildWithMemoMap(self, memoMap, scope) + )) + +/** @internal */ +export const buildWithMemoMap = dual< + ( + memoMap: Layer.MemoMap, + scope: Scope.Scope + ) => (self: Layer.Layer) => Effect.Effect, E, RIn>, + ( + self: Layer.Layer, + memoMap: Layer.MemoMap, + scope: Scope.Scope + ) => Effect.Effect, E, RIn> +>( + 3, + (self, memoMap, scope) => + core.flatMap( + makeBuilder(self, scope), + (run) => effect.provideService(run(memoMap), CurrentMemoMap, memoMap) + ) +) + +const makeBuilder = ( + self: Layer.Layer, + scope: Scope.Scope, + inMemoMap = false +): Effect.Effect<(memoMap: Layer.MemoMap) => Effect.Effect, E, RIn>> => { + const op = self as Primitive + switch (op._op_layer) { + case "Locally": { + return core.sync(() => (memoMap: Layer.MemoMap) => op.f(memoMap.getOrElseMemoize(op.self, scope))) + } + case "ExtendScope": { + return core.sync(() => (memoMap: Layer.MemoMap) => + fiberRuntime.scopeWith( + (scope) => memoMap.getOrElseMemoize(op.layer, scope) + ) as unknown as Effect.Effect, E, RIn> + ) + } + case "Fold": { + return core.sync(() => (memoMap: Layer.MemoMap) => + pipe( + memoMap.getOrElseMemoize(op.layer, scope), + core.matchCauseEffect({ + onFailure: (cause) => memoMap.getOrElseMemoize(op.failureK(cause), scope), + onSuccess: (value) => memoMap.getOrElseMemoize(op.successK(value), scope) + }) + ) + ) + } + case "Fresh": { + return core.sync(() => (_: Layer.MemoMap) => pipe(op.layer, buildWithScope(scope))) + } + case "FromEffect": { + return inMemoMap + ? core.sync(() => (_: Layer.MemoMap) => op.effect as Effect.Effect, E, RIn>) + : core.sync(() => (memoMap: Layer.MemoMap) => memoMap.getOrElseMemoize(self, scope)) + } + case "Provide": { + return core.sync(() => (memoMap: Layer.MemoMap) => + pipe( + memoMap.getOrElseMemoize(op.first, scope), + core.flatMap((env) => + pipe( + memoMap.getOrElseMemoize(op.second, scope), + core.provideContext(env) + ) + ) + ) + ) + } + case "Scoped": { + return inMemoMap + ? core.sync(() => (_: Layer.MemoMap) => + fiberRuntime.scopeExtend( + op.effect as Effect.Effect, E, RIn>, + scope + ) + ) + : core.sync(() => (memoMap: Layer.MemoMap) => memoMap.getOrElseMemoize(self, scope)) + } + case "Suspend": { + return core.sync(() => (memoMap: Layer.MemoMap) => + memoMap.getOrElseMemoize( + op.evaluate(), + scope + ) + ) + } + case "ProvideMerge": { + return core.sync(() => (memoMap: Layer.MemoMap) => + pipe( + memoMap.getOrElseMemoize(op.first, scope), + core.zipWith( + memoMap.getOrElseMemoize(op.second, scope), + op.zipK + ) + ) + ) + } + case "ZipWith": { + return core.gen(function*() { + const parallelScope = yield* core.scopeFork(scope, ExecutionStrategy.parallel) + const firstScope = yield* core.scopeFork(parallelScope, ExecutionStrategy.sequential) + const secondScope = yield* core.scopeFork(parallelScope, ExecutionStrategy.sequential) + return (memoMap: Layer.MemoMap) => + pipe( + memoMap.getOrElseMemoize(op.first, firstScope), + fiberRuntime.zipWithOptions( + memoMap.getOrElseMemoize(op.second, secondScope), + op.zipK, + { concurrent: true } + ) + ) + }) + } + case "MergeAll": { + const layers = op.layers + return core.map( + core.scopeFork(scope, ExecutionStrategy.parallel), + (parallelScope) => (memoMap: Layer.MemoMap) => { + const contexts = new Array>(layers.length) + return core.map( + fiberRuntime.forEachConcurrentDiscard( + layers, + core.fnUntraced(function*(layer, i) { + const scope = yield* core.scopeFork(parallelScope, ExecutionStrategy.sequential) + const context = yield* memoMap.getOrElseMemoize(layer, scope) + contexts[i] = context + }), + false, + false + ), + () => Context.mergeAll(...contexts) + ) + } + ) + } + } +} + +// ----------------------------------------------------------------------------- +// Layer +// ----------------------------------------------------------------------------- + +/** @internal */ +export const catchAll = dual< + ( + onError: (error: E) => Layer.Layer + ) => (self: Layer.Layer) => Layer.Layer, + ( + self: Layer.Layer, + onError: (error: E) => Layer.Layer + ) => Layer.Layer +>(2, (self, onFailure) => match(self, { onFailure, onSuccess: succeedContext })) + +/** @internal */ +export const catchAllCause = dual< + ( + onError: (cause: Cause.Cause) => Layer.Layer + ) => (self: Layer.Layer) => Layer.Layer, + ( + self: Layer.Layer, + onError: (cause: Cause.Cause) => Layer.Layer + ) => Layer.Layer +>(2, (self, onFailure) => matchCause(self, { onFailure, onSuccess: succeedContext })) + +/** @internal */ +export const die = (defect: unknown): Layer.Layer => failCause(Cause.die(defect)) + +/** @internal */ +export const dieSync = (evaluate: LazyArg): Layer.Layer => failCauseSync(() => Cause.die(evaluate())) + +/** @internal */ +export const discard = (self: Layer.Layer): Layer.Layer => + map(self, () => Context.empty()) + +/** @internal */ +export const context = (): Layer.Layer => fromEffectContext(core.context()) + +/** @internal */ +export const extendScope = ( + self: Layer.Layer +): Layer.Layer => { + const extendScope = Object.create(proto) + extendScope._op_layer = OpCodes.OP_EXTEND_SCOPE + extendScope.layer = self + return extendScope +} + +/** @internal */ +export const fail = (error: E): Layer.Layer => failCause(Cause.fail(error)) + +/** @internal */ +export const failSync = (evaluate: LazyArg): Layer.Layer => + failCauseSync(() => Cause.fail(evaluate())) + +/** @internal */ +export const failCause = (cause: Cause.Cause): Layer.Layer => fromEffectContext(core.failCause(cause)) + +/** @internal */ +export const failCauseSync = (evaluate: LazyArg>): Layer.Layer => + fromEffectContext(core.failCauseSync(evaluate)) + +/** @internal */ +export const flatMap = dual< + ( + f: (context: Context.Context) => Layer.Layer + ) => (self: Layer.Layer) => Layer.Layer, + ( + self: Layer.Layer, + f: (context: Context.Context) => Layer.Layer + ) => Layer.Layer +>(2, (self, f) => match(self, { onFailure: fail, onSuccess: f })) + +/** @internal */ +export const flatten = dual< + ( + tag: Context.Tag> + ) => ( + self: Layer.Layer + ) => Layer.Layer, + ( + self: Layer.Layer, + tag: Context.Tag> + ) => Layer.Layer +>(2, (self, tag) => flatMap(self, Context.get(tag as any) as any)) + +/** @internal */ +export const fresh = (self: Layer.Layer): Layer.Layer => { + const fresh = Object.create(proto) + fresh._op_layer = OpCodes.OP_FRESH + fresh.layer = self + return fresh +} + +/** @internal */ +export const fromEffect = dual< + ( + tag: Context.Tag + ) => ( + effect: Effect.Effect, E, R> + ) => Layer.Layer, + ( + tag: Context.Tag, + effect: Effect.Effect, E, R> + ) => Layer.Layer +>(2, (a, b) => { + const tagFirst = Context.isTag(a) + const tag = (tagFirst ? a : b) as Context.Tag + const effect = tagFirst ? b : a + return fromEffectContext(core.map(effect, (service) => Context.make(tag, service))) +}) + +/** @internal */ +export const fromEffectDiscard = (effect: Effect.Effect) => + fromEffectContext(core.map(effect, () => Context.empty())) + +/** @internal */ +export function fromEffectContext( + effect: Effect.Effect, E, R> +): Layer.Layer { + const fromEffect = Object.create(proto) + fromEffect._op_layer = OpCodes.OP_FROM_EFFECT + fromEffect.effect = effect + return fromEffect +} + +/** @internal */ +export const fiberRefLocally = dual< + (ref: FiberRef, value: X) => (self: Layer.Layer) => Layer.Layer, + (self: Layer.Layer, ref: FiberRef, value: X) => Layer.Layer +>(3, (self, ref, value) => locallyEffect(self, core.fiberRefLocally(ref, value))) + +/** @internal */ +export const locallyEffect = dual< + ( + f: (_: Effect.Effect>) => Effect.Effect> + ) => (self: Layer.Layer) => Layer.Layer, + ( + self: Layer.Layer, + f: (_: Effect.Effect>) => Effect.Effect> + ) => Layer.Layer +>(2, (self, f) => { + const locally = Object.create(proto) + locally._op_layer = "Locally" + locally.self = self + locally.f = f + return locally +}) + +/** @internal */ +export const fiberRefLocallyWith = dual< + (ref: FiberRef, value: (_: X) => X) => (self: Layer.Layer) => Layer.Layer, + (self: Layer.Layer, ref: FiberRef, value: (_: X) => X) => Layer.Layer +>(3, (self, ref, value) => locallyEffect(self, core.fiberRefLocallyWith(ref, value))) + +/** @internal */ +export const fiberRefLocallyScoped = (self: FiberRef, value: A): Layer.Layer => + scopedDiscard(fiberRuntime.fiberRefLocallyScoped(self, value)) + +/** @internal */ +export const fiberRefLocallyScopedWith = (self: FiberRef, value: (_: A) => A): Layer.Layer => + scopedDiscard(fiberRuntime.fiberRefLocallyScopedWith(self, value)) + +/** @internal */ +export const fromFunction = ( + tagA: Context.Tag, + tagB: Context.Tag, + f: (a: Types.NoInfer) => Types.NoInfer +): Layer.Layer => fromEffectContext(core.map(tagA, (a) => Context.make(tagB, f(a)))) + +/** @internal */ +export const launch = (self: Layer.Layer): Effect.Effect => + fiberRuntime.scopedEffect( + core.zipRight( + fiberRuntime.scopeWith((scope) => pipe(self, buildWithScope(scope))), + core.never + ) + ) + +/** @internal */ +export const mock: { + (tag: Context.Tag): (service: Layer.PartialEffectful) => Layer.Layer + (tag: Context.Tag, service: Layer.PartialEffectful): Layer.Layer +} = function() { + if (arguments.length === 1) { + return (service: Layer.PartialEffectful) => mockImpl(arguments[0], service) + } + return mockImpl(arguments[0], arguments[1]) +} as any + +const mockImpl = (tag: Context.Tag, service: Layer.PartialEffectful): Layer.Layer => + succeed( + tag, + new Proxy({ ...service as object } as S, { + get(target, prop, _receiver) { + if (prop in target) { + return target[prop as keyof S] + } + const prevLimit = Error.stackTraceLimit + Error.stackTraceLimit = 2 + const error = new Error(`${tag.key}: Unimplemented method "${prop.toString()}"`) + Error.stackTraceLimit = prevLimit + error.name = "UnimplementedError" + return makeUnimplemented(error) + }, + has: constTrue + }) + ) + +const makeUnimplemented = (error: Error) => { + const dead = core.die(error) + function unimplemented() { + return dead + } + // @effect-diagnostics-next-line floatingEffect:off + Object.assign(unimplemented, dead) + Object.setPrototypeOf(unimplemented, Object.getPrototypeOf(dead)) + return unimplemented +} + +/** @internal */ +export const map = dual< + ( + f: (context: Context.Context) => Context.Context + ) => (self: Layer.Layer) => Layer.Layer, + ( + self: Layer.Layer, + f: (context: Context.Context) => Context.Context + ) => Layer.Layer +>(2, (self, f) => flatMap(self, (context) => succeedContext(f(context)))) + +/** @internal */ +export const mapError = dual< + (f: (error: E) => E2) => (self: Layer.Layer) => Layer.Layer, + (self: Layer.Layer, f: (error: E) => E2) => Layer.Layer +>(2, (self, f) => catchAll(self, (error) => failSync(() => f(error)))) + +/** @internal */ +export const matchCause = dual< + ( + options: { + readonly onFailure: (cause: Cause.Cause) => Layer.Layer + readonly onSuccess: (context: Context.Context) => Layer.Layer + } + ) => (self: Layer.Layer) => Layer.Layer, + ( + self: Layer.Layer, + options: { + readonly onFailure: (cause: Cause.Cause) => Layer.Layer + readonly onSuccess: (context: Context.Context) => Layer.Layer + } + ) => Layer.Layer +>(2, (self, { onFailure, onSuccess }) => { + const fold = Object.create(proto) + fold._op_layer = OpCodes.OP_FOLD + fold.layer = self + fold.failureK = onFailure + fold.successK = onSuccess + return fold +}) + +/** @internal */ +export const match = dual< + ( + options: { + readonly onFailure: (error: E) => Layer.Layer + readonly onSuccess: (context: Context.Context) => Layer.Layer + } + ) => (self: Layer.Layer) => Layer.Layer, + ( + self: Layer.Layer, + options: { + readonly onFailure: (error: E) => Layer.Layer + readonly onSuccess: (context: Context.Context) => Layer.Layer + } + ) => Layer.Layer +>(2, (self, { onFailure, onSuccess }) => + matchCause(self, { + onFailure: (cause) => { + const failureOrCause = Cause.failureOrCause(cause) + switch (failureOrCause._tag) { + case "Left": { + return onFailure(failureOrCause.left) + } + case "Right": { + return failCause(failureOrCause.right) + } + } + }, + onSuccess + })) + +/** @internal */ +export const memoize = ( + self: Layer.Layer +): Effect.Effect, never, Scope.Scope> => + fiberRuntime.scopeWith((scope) => + core.map( + effect.memoize(buildWithScope(self, scope)), + fromEffectContext + ) + ) + +/** @internal */ +export const merge = dual< + ( + that: Layer.Layer + ) => (self: Layer.Layer) => Layer.Layer< + ROut | ROut2, + E1 | E2, + RIn | RIn2 + >, + (self: Layer.Layer, that: Layer.Layer) => Layer.Layer< + ROut | ROut2, + E1 | E2, + RIn | RIn2 + > +>(2, (self, that) => zipWith(self, that, (a, b) => Context.merge(a, b))) + +/** @internal */ +export const mergeAll = < + Layers extends readonly [Layer.Layer, ...Array>] +>( + ...layers: Layers +): Layer.Layer< + { [k in keyof Layers]: Layer.Layer.Success }[number], + { [k in keyof Layers]: Layer.Layer.Error }[number], + { [k in keyof Layers]: Layer.Layer.Context }[number] +> => { + const mergeAll = Object.create(proto) + mergeAll._op_layer = OpCodes.OP_MERGE_ALL + mergeAll.layers = layers + return mergeAll as any +} + +/** @internal */ +export const orDie = (self: Layer.Layer): Layer.Layer => + catchAll(self, (defect) => die(defect)) + +/** @internal */ +export const orElse = dual< + ( + that: LazyArg> + ) => (self: Layer.Layer) => Layer.Layer, + ( + self: Layer.Layer, + that: LazyArg> + ) => Layer.Layer +>(2, (self, that) => catchAll(self, that)) + +/** @internal */ +export const passthrough = (self: Layer.Layer): Layer.Layer => + merge(context(), self) + +/** @internal */ +export const project = dual< + ( + tagA: Context.Tag, + tagB: Context.Tag, + f: (a: Types.NoInfer) => Types.NoInfer + ) => (self: Layer.Layer) => Layer.Layer, + ( + self: Layer.Layer, + tagA: Context.Tag, + tagB: Context.Tag, + f: (a: Types.NoInfer) => Types.NoInfer + ) => Layer.Layer +>(4, (self, tagA, tagB, f) => map(self, (context) => Context.make(tagB, f(Context.unsafeGet(context, tagA))))) + +/** @internal */ +export const retry = dual< + ( + schedule: Schedule.Schedule + ) => ( + self: Layer.Layer + ) => Layer.Layer, + ( + self: Layer.Layer, + schedule: Schedule.Schedule + ) => Layer.Layer +>(2, (self, schedule) => + suspend(() => { + const stateTag = Context.GenericTag<{ state: unknown }>("effect/Layer/retry/{ state: unknown }") + return pipe( + succeed(stateTag, { state: schedule.initial }), + flatMap((env: Context.Context<{ state: unknown }>) => + retryLoop(self, schedule, stateTag, pipe(env, Context.get(stateTag)).state) + ) + ) + })) + +const retryLoop = ( + self: Layer.Layer, + schedule: Schedule.Schedule, + stateTag: Context.Tag<{ state: unknown }, { state: unknown }>, + state: unknown +): Layer.Layer => { + return pipe( + self, + catchAll((error) => + pipe( + retryUpdate(schedule, stateTag, error, state), + flatMap((env) => fresh(retryLoop(self, schedule, stateTag, pipe(env, Context.get(stateTag)).state))) + ) + ) + ) +} + +const retryUpdate = ( + schedule: Schedule.Schedule, + stateTag: Context.Tag<{ state: unknown }, { state: unknown }>, + error: E, + state: unknown +): Layer.Layer<{ state: unknown }, E, RIn> => { + return fromEffect( + stateTag, + pipe( + Clock.currentTimeMillis, + core.flatMap((now) => + pipe( + schedule.step(now, error, state), + core.flatMap(([state, _, decision]) => + ScheduleDecision.isDone(decision) ? + core.fail(error) : + pipe( + Clock.sleep(Duration.millis(Intervals.start(decision.intervals) - now)), + core.as({ state }) + ) + ) + ) + ) + ) + ) +} + +/** @internal */ +export const scoped = dual< + ( + tag: Context.Tag + ) => ( + effect: Effect.Effect, E, R> + ) => Layer.Layer>, + ( + tag: Context.Tag, + effect: Effect.Effect, E, R> + ) => Layer.Layer> +>(2, (a, b) => { + const tagFirst = Context.isTag(a) + const tag = (tagFirst ? a : b) as Context.Tag + const effect = tagFirst ? b : a + return scopedContext(core.map(effect, (service) => Context.make(tag, service))) +}) + +/** @internal */ +export const scopedDiscard = ( + effect: Effect.Effect +): Layer.Layer> => scopedContext(pipe(effect, core.as(Context.empty()))) + +/** @internal */ +export const scopedContext = ( + effect: Effect.Effect, E, R> +): Layer.Layer> => { + const scoped = Object.create(proto) + scoped._op_layer = OpCodes.OP_SCOPED + scoped.effect = effect + return scoped +} + +/** @internal */ +export const scope: Layer.Layer = scopedContext( + core.map( + fiberRuntime.acquireRelease( + fiberRuntime.scopeMake(), + (scope, exit) => scope.close(exit) + ), + (scope) => Context.make(Scope.Scope, scope) + ) +) + +/** @internal */ +export const service = ( + tag: Context.Tag +): Layer.Layer => fromEffect(tag, tag) + +/** @internal */ +export const succeed = dual< + ( + tag: Context.Tag + ) => ( + resource: Types.NoInfer + ) => Layer.Layer, + ( + tag: Context.Tag, + resource: Types.NoInfer + ) => Layer.Layer +>(2, (a, b) => { + const tagFirst = Context.isTag(a) + const tag = (tagFirst ? a : b) as Context.Tag + const resource = tagFirst ? b : a + return fromEffectContext(core.succeed(Context.make(tag, resource))) +}) + +/** @internal */ +export const succeedContext = ( + context: Context.Context +): Layer.Layer => { + return fromEffectContext(core.succeed(context)) +} + +/** @internal */ +export const empty = succeedContext(Context.empty()) + +/** @internal */ +export const suspend = ( + evaluate: LazyArg> +): Layer.Layer => { + const suspend = Object.create(proto) + suspend._op_layer = OpCodes.OP_SUSPEND + suspend.evaluate = evaluate + return suspend +} + +/** @internal */ +export const sync = dual< + ( + tag: Context.Tag + ) => ( + evaluate: LazyArg> + ) => Layer.Layer, + ( + tag: Context.Tag, + evaluate: LazyArg> + ) => Layer.Layer +>(2, (a, b) => { + const tagFirst = Context.isTag(a) + const tag = (tagFirst ? a : b) as Context.Tag + const evaluate = tagFirst ? b : a + return fromEffectContext(core.sync(() => Context.make(tag, evaluate()))) +}) + +/** @internal */ +export const syncContext = (evaluate: LazyArg>): Layer.Layer => { + return fromEffectContext(core.sync(evaluate)) +} + +/** @internal */ +export const tap = dual< + ( + f: (context: Context.Context) => Effect.Effect + ) => (self: Layer.Layer) => Layer.Layer, + ( + self: Layer.Layer, + f: (context: Context.Context) => Effect.Effect + ) => Layer.Layer +>(2, (self, f) => flatMap(self, (context) => fromEffectContext(core.as(f(context), context)))) + +/** @internal */ +export const tapError = dual< + ( + f: (e: XE) => Effect.Effect + ) => (self: Layer.Layer) => Layer.Layer, + ( + self: Layer.Layer, + f: (e: XE) => Effect.Effect + ) => Layer.Layer +>(2, (self, f) => + catchAll( + self, + (e) => fromEffectContext(core.flatMap(f(e as any), () => core.fail(e))) + )) + +/** @internal */ +export const tapErrorCause = dual< + ( + f: (cause: Cause.Cause) => Effect.Effect + ) => (self: Layer.Layer) => Layer.Layer, + ( + self: Layer.Layer, + f: (cause: Cause.Cause) => Effect.Effect + ) => Layer.Layer +>(2, (self, f) => + catchAllCause( + self, + (cause) => fromEffectContext(core.flatMap(f(cause as any), () => core.failCause(cause))) + )) + +/** @internal */ +export const toRuntime = ( + self: Layer.Layer +): Effect.Effect, E, RIn | Scope.Scope> => + pipe( + fiberRuntime.scopeWith((scope) => buildWithScope(self, scope)), + core.flatMap((context) => + pipe( + runtime.runtime(), + core.provideContext(context) + ) + ) + ) + +/** @internal */ +export const toRuntimeWithMemoMap = dual< + ( + memoMap: Layer.MemoMap + ) => (self: Layer.Layer) => Effect.Effect, E, RIn | Scope.Scope>, + ( + self: Layer.Layer, + memoMap: Layer.MemoMap + ) => Effect.Effect, E, RIn | Scope.Scope> +>(2, (self, memoMap) => + core.flatMap( + fiberRuntime.scopeWith((scope) => buildWithMemoMap(self, memoMap, scope)), + (context) => + pipe( + runtime.runtime(), + core.provideContext(context) + ) + )) + +/** @internal */ +export const provide = dual< + { + ( + that: Layer.Layer + ): ( + self: Layer.Layer + ) => Layer.Layer> + ]>( + that: Layers + ): ( + self: Layer.Layer + ) => Layer.Layer< + A, + E | { [k in keyof Layers]: Layer.Layer.Error }[number], + | { [k in keyof Layers]: Layer.Layer.Context }[number] + | Exclude }[number]> + > + }, + { + ( + self: Layer.Layer, + that: Layer.Layer + ): Layer.Layer> + ]>( + self: Layer.Layer, + that: Layers + ): Layer.Layer< + A, + E | { [k in keyof Layers]: Layer.Layer.Error }[number], + | { [k in keyof Layers]: Layer.Layer.Context }[number] + | Exclude }[number]> + > + } +>(2, ( + self: Layer.Layer.Any, + that: Layer.Layer.Any | ReadonlyArray +) => + suspend(() => { + const provideTo = Object.create(proto) + provideTo._op_layer = OpCodes.OP_PROVIDE + provideTo.first = Object.create(proto, { + _op_layer: { value: OpCodes.OP_PROVIDE_MERGE, enumerable: true }, + first: { value: context(), enumerable: true }, + second: { value: Array.isArray(that) ? mergeAll(...that as any) : that }, + zipK: { value: (a: Context.Context, b: Context.Context) => pipe(a, Context.merge(b)) } + }) + provideTo.second = self + return provideTo + })) + +/** @internal */ +export const provideMerge = dual< + ( + self: Layer.Layer + ) => ( + that: Layer.Layer + ) => Layer.Layer>, + ( + that: Layer.Layer, + self: Layer.Layer + ) => Layer.Layer> +>(2, (that: Layer.Layer, self: Layer.Layer) => { + const zipWith = Object.create(proto) + zipWith._op_layer = OpCodes.OP_PROVIDE_MERGE + zipWith.first = self + zipWith.second = provide(that, self) + zipWith.zipK = (a: Context.Context, b: Context.Context): Context.Context => { + return pipe(a, Context.merge(b)) + } + return zipWith +}) + +/** @internal */ +export const zipWith = dual< + ( + that: Layer.Layer, + f: (a: Context.Context, b: Context.Context) => Context.Context + ) => (self: Layer.Layer) => Layer.Layer, + ( + self: Layer.Layer, + that: Layer.Layer, + f: (a: Context.Context, b: Context.Context) => Context.Context + ) => Layer.Layer +>(3, (self, that, f) => + suspend(() => { + const zipWith = Object.create(proto) + zipWith._op_layer = OpCodes.OP_ZIP_WITH + zipWith.first = self + zipWith.second = that + zipWith.zipK = f + return zipWith + })) + +/** @internal */ +export const unwrapEffect = ( + self: Effect.Effect, E, R> +): Layer.Layer => { + const tag = Context.GenericTag>("effect/Layer/unwrapEffect/Layer.Layer") + return flatMap(fromEffect(tag, self), (context) => Context.get(context, tag)) +} + +/** @internal */ +export const unwrapScoped = ( + self: Effect.Effect, E, R> +): Layer.Layer> => { + const tag = Context.GenericTag>("effect/Layer/unwrapScoped/Layer.Layer") + return flatMap(scoped(tag, self), (context) => Context.get(context, tag)) +} + +// ----------------------------------------------------------------------------- +// logging +// ----------------------------------------------------------------------------- + +export const annotateLogs = dual< + { + (key: string, value: unknown): (self: Layer.Layer) => Layer.Layer + ( + values: Record + ): (self: Layer.Layer) => Layer.Layer + }, + { + (self: Layer.Layer, key: string, value: unknown): Layer.Layer + (self: Layer.Layer, values: Record): Layer.Layer + } +>( + (args) => isLayer(args[0]), + function() { + const args = arguments + return fiberRefLocallyWith( + args[0] as Layer.Layer, + core.currentLogAnnotations, + typeof args[1] === "string" + ? HashMap.set(args[1], args[2]) + : (annotations) => + Object.entries(args[1] as Record).reduce( + (acc, [key, value]) => HashMap.set(acc, key, value), + annotations + ) + ) + } +) + +// ----------------------------------------------------------------------------- +// tracing +// ----------------------------------------------------------------------------- + +export const annotateSpans = dual< + { + (key: string, value: unknown): (self: Layer.Layer) => Layer.Layer + ( + values: Record + ): (self: Layer.Layer) => Layer.Layer + }, + { + (self: Layer.Layer, key: string, value: unknown): Layer.Layer + (self: Layer.Layer, values: Record): Layer.Layer + } +>( + (args) => isLayer(args[0]), + function() { + const args = arguments + return fiberRefLocallyWith( + args[0] as Layer.Layer, + core.currentTracerSpanAnnotations, + typeof args[1] === "string" + ? HashMap.set(args[1], args[2]) + : (annotations) => + Object.entries(args[1] as Record).reduce( + (acc, [key, value]) => HashMap.set(acc, key, value), + annotations + ) + ) + } +) + +/** @internal */ +export const withSpan: { + ( + name: string, + options?: Tracer.SpanOptions & { + readonly onEnd?: + | ((span: Tracer.Span, exit: Exit.Exit) => Effect.Effect) + | undefined + } + ): (self: Layer.Layer) => Layer.Layer> + ( + self: Layer.Layer, + name: string, + options?: Tracer.SpanOptions & { + readonly onEnd?: + | ((span: Tracer.Span, exit: Exit.Exit) => Effect.Effect) + | undefined + } + ): Layer.Layer> +} = function() { + const dataFirst = typeof arguments[0] !== "string" + const name = dataFirst ? arguments[1] : arguments[0] + const options = tracer.addSpanStackTrace(dataFirst ? arguments[2] : arguments[1]) as Tracer.SpanOptions & { + readonly onEnd?: + | ((span: Tracer.Span, exit: Exit.Exit) => Effect.Effect) + | undefined + } + if (dataFirst) { + const self = arguments[0] + return unwrapScoped( + core.map( + options?.onEnd + ? core.tap( + fiberRuntime.makeSpanScoped(name, options), + (span) => fiberRuntime.addFinalizer((exit) => options.onEnd!(span, exit)) + ) + : fiberRuntime.makeSpanScoped(name, options), + (span) => withParentSpan(self, span) + ) + ) + } + return (self: Layer.Layer) => + unwrapScoped( + core.map( + options?.onEnd + ? core.tap( + fiberRuntime.makeSpanScoped(name, options), + (span) => fiberRuntime.addFinalizer((exit) => options.onEnd!(span, exit)) + ) + : fiberRuntime.makeSpanScoped(name, options), + (span) => withParentSpan(self, span) + ) + ) +} as any + +/** @internal */ +export const withParentSpan = dual< + ( + span: Tracer.AnySpan + ) => (self: Layer.Layer) => Layer.Layer>, + (self: Layer.Layer, span: Tracer.AnySpan) => Layer.Layer> +>(2, (self, span) => provide(self, succeedContext(Context.make(tracer.spanTag, span)))) + +// circular with Effect + +const provideSomeLayer = dual< + ( + layer: Layer.Layer + ) => (self: Effect.Effect) => Effect.Effect>, + ( + self: Effect.Effect, + layer: Layer.Layer + ) => Effect.Effect> +>(2, (self, layer) => + fiberRuntime.scopedWith((scope) => + core.flatMap( + buildWithScope(layer, scope), + (context) => core.provideSomeContext(self, context) + ) + )) + +const provideSomeRuntime = dual< + (context: Runtime.Runtime) => (self: Effect.Effect) => Effect.Effect>, + (self: Effect.Effect, context: Runtime.Runtime) => Effect.Effect> +>(2, (self, rt) => { + const patchRefs = FiberRefsPatch.diff(runtime.defaultRuntime.fiberRefs, rt.fiberRefs) + const patchFlags = runtimeFlags.diff(runtime.defaultRuntime.runtimeFlags, rt.runtimeFlags) + return core.uninterruptibleMask((restore) => + core.withFiberRuntime((fiber) => { + const oldContext = fiber.getFiberRef(core.currentContext) + const oldRefs = fiber.getFiberRefs() + const newRefs = FiberRefsPatch.patch(fiber.id(), oldRefs)(patchRefs) + const oldFlags = fiber.currentRuntimeFlags + const newFlags = runtimeFlags.patch(patchFlags)(oldFlags) + const rollbackRefs = FiberRefsPatch.diff(newRefs, oldRefs) + const rollbackFlags = runtimeFlags.diff(newFlags, oldFlags) + fiber.setFiberRefs(newRefs) + fiber.currentRuntimeFlags = newFlags + return fiberRuntime.ensuring( + core.provideSomeContext(restore(self), Context.merge(oldContext, rt.context)), + core.withFiberRuntime((fiber) => { + fiber.setFiberRefs(FiberRefsPatch.patch(fiber.id(), fiber.getFiberRefs())(rollbackRefs)) + fiber.currentRuntimeFlags = runtimeFlags.patch(rollbackFlags)(fiber.currentRuntimeFlags) + return core.void + }) + ) + }) + ) +}) + +/** @internal */ +export const effect_provide = dual< + { + ]>( + layers: Layers + ): ( + self: Effect.Effect + ) => Effect.Effect< + A, + E | { [k in keyof Layers]: Layer.Layer.Error }[number], + | { [k in keyof Layers]: Layer.Layer.Context }[number] + | Exclude }[number]> + > + ( + layer: Layer.Layer + ): (self: Effect.Effect) => Effect.Effect> + ( + context: Context.Context + ): (self: Effect.Effect) => Effect.Effect> + ( + runtime: Runtime.Runtime + ): (self: Effect.Effect) => Effect.Effect> + ( + managedRuntime: ManagedRuntime.ManagedRuntime + ): (self: Effect.Effect) => Effect.Effect> + }, + { + ]>( + self: Effect.Effect, + layers: Layers + ): Effect.Effect< + A, + E | { [k in keyof Layers]: Layer.Layer.Error }[number], + | { [k in keyof Layers]: Layer.Layer.Context }[number] + | Exclude }[number]> + > + ( + self: Effect.Effect, + layer: Layer.Layer + ): Effect.Effect> + ( + self: Effect.Effect, + context: Context.Context + ): Effect.Effect> + ( + self: Effect.Effect, + runtime: Runtime.Runtime + ): Effect.Effect> + ( + self: Effect.Effect, + managedRuntime: ManagedRuntime.ManagedRuntime + ): Effect.Effect> + } +>( + 2, + ( + self: Effect.Effect, + source: + | Layer.Layer + | Context.Context + | Runtime.Runtime + | ManagedRuntime.ManagedRuntime + | Array + ): Effect.Effect> => { + if (Array.isArray(source)) { + // @ts-expect-error + return provideSomeLayer(self, mergeAll(...source)) + } else if (isLayer(source)) { + return provideSomeLayer(self, source as Layer.Layer) + } else if (Context.isContext(source)) { + return core.provideSomeContext(self, source) + } else if (circularManagedRuntime.TypeId in source) { + return core.flatMap( + (source as ManagedRuntime.ManagedRuntime).runtimeEffect, + (rt) => provideSomeRuntime(self, rt) + ) + } else { + return provideSomeRuntime(self, source as Runtime.Runtime) + } + } +) diff --git a/repos/effect/packages/effect/src/internal/layer/circular.ts b/repos/effect/packages/effect/src/internal/layer/circular.ts new file mode 100644 index 0000000..7085200 --- /dev/null +++ b/repos/effect/packages/effect/src/internal/layer/circular.ts @@ -0,0 +1,214 @@ +import type * as ConfigProvider from "../../ConfigProvider.js" +import * as Context from "../../Context.js" +import type * as Effect from "../../Effect.js" +import type * as Exit from "../../Exit.js" +import { dual } from "../../Function.js" +import * as HashSet from "../../HashSet.js" +import type * as Layer from "../../Layer.js" +import type * as Logger from "../../Logger.js" +import type * as LogLevel from "../../LogLevel.js" +import type { Scope } from "../../Scope.js" +import type * as Supervisor from "../../Supervisor.js" +import type * as Tracer from "../../Tracer.js" +import * as core from "../core.js" +import * as fiberRuntime from "../fiberRuntime.js" +import * as layer from "../layer.js" +import * as runtimeFlags from "../runtimeFlags.js" +import * as runtimeFlagsPatch from "../runtimeFlagsPatch.js" +import * as supervisor_ from "../supervisor.js" +import * as tracer from "../tracer.js" + +// circular with Logger + +/** @internal */ +export const minimumLogLevel = (level: LogLevel.LogLevel): Layer.Layer => + layer.scopedDiscard( + fiberRuntime.fiberRefLocallyScoped( + fiberRuntime.currentMinimumLogLevel, + level + ) + ) + +/** @internal */ +export const withMinimumLogLevel = dual< + (level: LogLevel.LogLevel) => (self: Effect.Effect) => Effect.Effect, + (self: Effect.Effect, level: LogLevel.LogLevel) => Effect.Effect +>(2, (self, level) => + core.fiberRefLocally( + fiberRuntime.currentMinimumLogLevel, + level + )(self)) + +/** @internal */ +export const addLogger = (logger: Logger.Logger): Layer.Layer => + layer.scopedDiscard( + fiberRuntime.fiberRefLocallyScopedWith( + fiberRuntime.currentLoggers, + HashSet.add(logger) + ) + ) + +/** @internal */ +export const addLoggerEffect = ( + effect: Effect.Effect, E, R> +): Layer.Layer => + layer.unwrapEffect( + core.map(effect, addLogger) + ) + +/** @internal */ +export const addLoggerScoped = ( + effect: Effect.Effect, E, R> +): Layer.Layer> => + layer.unwrapScoped( + core.map(effect, addLogger) + ) + +/** @internal */ +export const removeLogger = (logger: Logger.Logger): Layer.Layer => + layer.scopedDiscard( + fiberRuntime.fiberRefLocallyScopedWith( + fiberRuntime.currentLoggers, + HashSet.remove(logger) + ) + ) + +/** @internal */ +export const replaceLogger = dual< + (that: Logger.Logger) => (self: Logger.Logger) => Layer.Layer, + (self: Logger.Logger, that: Logger.Logger) => Layer.Layer +>(2, (self, that) => layer.flatMap(removeLogger(self), () => addLogger(that))) + +/** @internal */ +export const replaceLoggerEffect = dual< + ( + that: Effect.Effect, E, R> + ) => (self: Logger.Logger) => Layer.Layer, + ( + self: Logger.Logger, + that: Effect.Effect, E, R> + ) => Layer.Layer +>(2, (self, that) => layer.flatMap(removeLogger(self), () => addLoggerEffect(that))) + +/** @internal */ +export const replaceLoggerScoped = dual< + ( + that: Effect.Effect, E, R> + ) => (self: Logger.Logger) => Layer.Layer>, + ( + self: Logger.Logger, + that: Effect.Effect, E, R> + ) => Layer.Layer> +>(2, (self, that) => layer.flatMap(removeLogger(self), () => addLoggerScoped(that))) + +/** @internal */ +export const addSupervisor = (supervisor: Supervisor.Supervisor): Layer.Layer => + layer.scopedDiscard( + fiberRuntime.fiberRefLocallyScopedWith( + fiberRuntime.currentSupervisor, + (current) => new supervisor_.Zip(current, supervisor) + ) + ) + +/** @internal */ +export const enableCooperativeYielding: Layer.Layer = layer.scopedDiscard( + fiberRuntime.withRuntimeFlagsScoped( + runtimeFlagsPatch.enable(runtimeFlags.CooperativeYielding) + ) +) + +/** @internal */ +export const enableInterruption: Layer.Layer = layer.scopedDiscard( + fiberRuntime.withRuntimeFlagsScoped( + runtimeFlagsPatch.enable(runtimeFlags.Interruption) + ) +) + +/** @internal */ +export const enableOpSupervision: Layer.Layer = layer.scopedDiscard( + fiberRuntime.withRuntimeFlagsScoped( + runtimeFlagsPatch.enable(runtimeFlags.OpSupervision) + ) +) + +/** @internal */ +export const enableRuntimeMetrics: Layer.Layer = layer.scopedDiscard( + fiberRuntime.withRuntimeFlagsScoped( + runtimeFlagsPatch.enable(runtimeFlags.RuntimeMetrics) + ) +) + +/** @internal */ +export const enableWindDown: Layer.Layer = layer.scopedDiscard( + fiberRuntime.withRuntimeFlagsScoped( + runtimeFlagsPatch.enable(runtimeFlags.WindDown) + ) +) + +/** @internal */ +export const disableCooperativeYielding: Layer.Layer = layer.scopedDiscard( + fiberRuntime.withRuntimeFlagsScoped( + runtimeFlagsPatch.disable(runtimeFlags.CooperativeYielding) + ) +) + +/** @internal */ +export const disableInterruption: Layer.Layer = layer.scopedDiscard( + fiberRuntime.withRuntimeFlagsScoped( + runtimeFlagsPatch.disable(runtimeFlags.Interruption) + ) +) + +/** @internal */ +export const disableOpSupervision: Layer.Layer = layer.scopedDiscard( + fiberRuntime.withRuntimeFlagsScoped( + runtimeFlagsPatch.disable(runtimeFlags.OpSupervision) + ) +) + +/** @internal */ +export const disableRuntimeMetrics: Layer.Layer = layer.scopedDiscard( + fiberRuntime.withRuntimeFlagsScoped( + runtimeFlagsPatch.disable(runtimeFlags.RuntimeMetrics) + ) +) + +/** @internal */ +export const disableWindDown: Layer.Layer = layer.scopedDiscard( + fiberRuntime.withRuntimeFlagsScoped( + runtimeFlagsPatch.disable(runtimeFlags.WindDown) + ) +) + +/** @internal */ +export const setConfigProvider = (configProvider: ConfigProvider.ConfigProvider): Layer.Layer => + layer.scopedDiscard(fiberRuntime.withConfigProviderScoped(configProvider)) + +/** @internal */ +export const parentSpan = (span: Tracer.AnySpan): Layer.Layer => + layer.succeedContext(Context.make(tracer.spanTag, span)) + +/** @internal */ +export const span = ( + name: string, + options?: Tracer.SpanOptions & { + readonly onEnd?: + | ((span: Tracer.Span, exit: Exit.Exit) => Effect.Effect) + | undefined + } +): Layer.Layer => { + options = tracer.addSpanStackTrace(options) as any + return layer.scoped( + tracer.spanTag, + options?.onEnd + ? core.tap( + fiberRuntime.makeSpanScoped(name, options), + (span) => fiberRuntime.addFinalizer((exit) => options.onEnd!(span, exit)) + ) + : fiberRuntime.makeSpanScoped(name, options) + ) +} + +/** @internal */ +export const setTracer = (tracer: Tracer.Tracer): Layer.Layer => + layer.scopedDiscard(fiberRuntime.withTracerScoped(tracer)) diff --git a/repos/effect/packages/effect/src/internal/logSpan.ts b/repos/effect/packages/effect/src/internal/logSpan.ts new file mode 100644 index 0000000..f58acfb --- /dev/null +++ b/repos/effect/packages/effect/src/internal/logSpan.ts @@ -0,0 +1,20 @@ +import type * as LogSpan from "../LogSpan.js" + +/** @internal */ +export const make = (label: string, startTime: number): LogSpan.LogSpan => ({ + label, + startTime +}) + +/** + * Sanitize a given string by replacing spaces, equal signs, and double quotes with underscores. + * + * @internal + */ +export const formatLabel = (key: string) => key.replace(/[\s="]/g, "_") + +/** @internal */ +export const render = (now: number) => (self: LogSpan.LogSpan): string => { + const label = formatLabel(self.label) + return `${label}=${now - self.startTime}ms` +} diff --git a/repos/effect/packages/effect/src/internal/logger-circular.ts b/repos/effect/packages/effect/src/internal/logger-circular.ts new file mode 100644 index 0000000..ba72e75 --- /dev/null +++ b/repos/effect/packages/effect/src/internal/logger-circular.ts @@ -0,0 +1,24 @@ +import * as Cause from "../Cause.js" +import { dual } from "../Function.js" +import * as HashMap from "../HashMap.js" +import * as List from "../List.js" +import type * as Logger from "../Logger.js" +import * as core from "./core.js" +import * as fiberId_ from "./fiberId.js" +import * as fiberRefs from "./fiberRefs.js" + +/** @internal */ +export const test = dual< + (input: Message) => (self: Logger.Logger) => Output, + (self: Logger.Logger, input: Message) => Output +>(2, (self, input) => + self.log({ + fiberId: fiberId_.none, + logLevel: core.logLevelInfo, + message: input, + cause: Cause.empty, + context: fiberRefs.empty(), + spans: List.empty(), + annotations: HashMap.empty(), + date: new Date() + })) diff --git a/repos/effect/packages/effect/src/internal/logger.ts b/repos/effect/packages/effect/src/internal/logger.ts new file mode 100644 index 0000000..b416a8f --- /dev/null +++ b/repos/effect/packages/effect/src/internal/logger.ts @@ -0,0 +1,485 @@ +import * as Arr from "../Array.js" +import * as Context from "../Context.js" +import * as FiberRefs from "../FiberRefs.js" +import type { LazyArg } from "../Function.js" +import { constVoid, dual } from "../Function.js" +import { globalValue } from "../GlobalValue.js" +import * as HashMap from "../HashMap.js" +import * as Inspectable from "../Inspectable.js" +import * as List from "../List.js" +import type * as Logger from "../Logger.js" +import type * as LogLevel from "../LogLevel.js" +import * as Option from "../Option.js" +import { pipeArguments } from "../Pipeable.js" +import * as Cause from "./cause.js" +import * as defaultServices from "./defaultServices.js" +import { consoleTag } from "./defaultServices/console.js" +import * as fiberId_ from "./fiberId.js" +import * as logSpan_ from "./logSpan.js" + +/** @internal */ +const LoggerSymbolKey = "effect/Logger" + +/** @internal */ +export const LoggerTypeId: Logger.LoggerTypeId = Symbol.for( + LoggerSymbolKey +) as Logger.LoggerTypeId + +const loggerVariance = { + /* c8 ignore next */ + _Message: (_: unknown) => _, + /* c8 ignore next */ + _Output: (_: never) => _ +} + +/** @internal */ +export const makeLogger = ( + log: (options: Logger.Logger.Options) => Output +): Logger.Logger => ({ + [LoggerTypeId]: loggerVariance, + log, + pipe() { + return pipeArguments(this, arguments) + } +}) + +/** @internal */ +export const mapInput = dual< + ( + f: (message: Message2) => Message + ) => (self: Logger.Logger) => Logger.Logger, + ( + self: Logger.Logger, + f: (message: Message2) => Message + ) => Logger.Logger +>(2, (self, f) => + makeLogger( + (options) => self.log({ ...options, message: f(options.message) }) + )) + +/** @internal */ +export const mapInputOptions = dual< + ( + f: (options: Logger.Logger.Options) => Logger.Logger.Options + ) => (self: Logger.Logger) => Logger.Logger, + ( + self: Logger.Logger, + f: (options: Logger.Logger.Options) => Logger.Logger.Options + ) => Logger.Logger +>(2, (self, f) => makeLogger((options) => self.log(f(options)))) + +/** @internal */ +export const filterLogLevel = dual< + ( + f: (logLevel: LogLevel.LogLevel) => boolean + ) => (self: Logger.Logger) => Logger.Logger>, + ( + self: Logger.Logger, + f: (logLevel: LogLevel.LogLevel) => boolean + ) => Logger.Logger> +>(2, (self, f) => + makeLogger((options) => + f(options.logLevel) + ? Option.some(self.log(options)) + : Option.none() + )) + +/** @internal */ +export const map = dual< + ( + f: (output: Output) => Output2 + ) => (self: Logger.Logger) => Logger.Logger, + ( + self: Logger.Logger, + f: (output: Output) => Output2 + ) => Logger.Logger +>(2, (self, f) => makeLogger((options) => f(self.log(options)))) + +/** @internal */ +export const none: Logger.Logger = { + [LoggerTypeId]: loggerVariance, + log: constVoid, + pipe() { + return pipeArguments(this, arguments) + } +} as Logger.Logger + +/** @internal */ +export const simple = (log: (a: A) => B): Logger.Logger => ({ + [LoggerTypeId]: loggerVariance, + log: ({ message }) => log(message), + pipe() { + return pipeArguments(this, arguments) + } +}) + +/** @internal */ +export const succeed = (value: A): Logger.Logger => { + return simple(() => value) +} + +/** @internal */ +export const sync = (evaluate: LazyArg): Logger.Logger => { + return simple(evaluate) +} + +/** @internal */ +export const zip = dual< + ( + that: Logger.Logger + ) => ( + self: Logger.Logger + ) => Logger.Logger, + ( + self: Logger.Logger, + that: Logger.Logger + ) => Logger.Logger +>(2, (self, that) => makeLogger((options) => [self.log(options), that.log(options)])) + +/** @internal */ +export const zipLeft = dual< + ( + that: Logger.Logger + ) => ( + self: Logger.Logger + ) => Logger.Logger, + ( + self: Logger.Logger, + that: Logger.Logger + ) => Logger.Logger +>(2, (self, that) => map(zip(self, that), (tuple) => tuple[0])) + +/** @internal */ +export const zipRight = dual< + ( + that: Logger.Logger + ) => ( + self: Logger.Logger + ) => Logger.Logger, + ( + self: Logger.Logger, + that: Logger.Logger + ) => Logger.Logger +>(2, (self, that) => map(zip(self, that), (tuple) => tuple[1])) + +/** + * Match strings that do not contain any whitespace characters, double quotes, + * or equal signs. + * + * @internal + */ +const textOnly = /^[^\s"=]*$/ + +/** + * Used by both {@link stringLogger} and {@link logfmtLogger} to render a log + * message. + * + * @internal + */ +const format = (quoteValue: (s: string) => string, whitespace?: number | string | undefined) => +( + { annotations, cause, date, fiberId, logLevel, message, spans }: Logger.Logger.Options +): string => { + const formatValue = (value: string): string => value.match(textOnly) ? value : quoteValue(value) + const format = (label: string, value: string): string => `${logSpan_.formatLabel(label)}=${formatValue(value)}` + const append = (label: string, value: string): string => " " + format(label, value) + + let out = format("timestamp", date.toISOString()) + out += append("level", logLevel.label) + out += append("fiber", fiberId_.threadName(fiberId)) + + const messages = Arr.ensure(message) + for (let i = 0; i < messages.length; i++) { + out += append("message", Inspectable.toStringUnknown(messages[i], whitespace)) + } + + if (!Cause.isEmptyType(cause)) { + out += append("cause", Cause.pretty(cause, { renderErrorCause: true })) + } + + for (const span of spans) { + out += " " + logSpan_.render(date.getTime())(span) + } + + for (const [label, value] of annotations) { + out += append(label, Inspectable.toStringUnknown(value, whitespace)) + } + + return out +} + +/** @internal */ +const escapeDoubleQuotes = (s: string) => `"${s.replace(/\\([\s\S])|(")/g, "\\$1$2")}"` + +/** @internal */ +export const stringLogger: Logger.Logger = makeLogger(format(escapeDoubleQuotes)) + +/** @internal */ +export const logfmtLogger: Logger.Logger = makeLogger(format(JSON.stringify, 0)) + +/** @internal */ +export const structuredLogger = makeLogger + readonly spans: Record +}>( + ({ annotations, cause, date, fiberId, logLevel, message, spans }) => { + const now = date.getTime() + const annotationsObj: Record = {} + const spansObj: Record = {} + + if (HashMap.size(annotations) > 0) { + for (const [k, v] of annotations) { + annotationsObj[k] = structuredMessage(v) + } + } + + if (List.isCons(spans)) { + for (const span of spans) { + spansObj[span.label] = now - span.startTime + } + } + + const messageArr = Arr.ensure(message) + return { + message: messageArr.length === 1 ? structuredMessage(messageArr[0]) : messageArr.map(structuredMessage), + logLevel: logLevel.label, + timestamp: date.toISOString(), + cause: Cause.isEmpty(cause) ? undefined : Cause.pretty(cause, { renderErrorCause: true }), + annotations: annotationsObj, + spans: spansObj, + fiberId: fiberId_.threadName(fiberId) + } + } +) + +/** @internal */ +export const structuredMessage = (u: unknown): unknown => { + switch (typeof u) { + case "bigint": + case "function": + case "symbol": { + return String(u) + } + default: { + return Inspectable.toJSON(u) + } + } +} + +/** @internal */ +export const jsonLogger = map(structuredLogger, Inspectable.stringifyCircular) + +/** @internal */ +export const isLogger = (u: unknown): u is Logger.Logger => { + return typeof u === "object" && u != null && LoggerTypeId in u +} + +const withColor = (text: string, ...colors: ReadonlyArray) => { + let out = "" + for (let i = 0; i < colors.length; i++) { + out += `\x1b[${colors[i]}m` + } + return out + text + "\x1b[0m" +} +const withColorNoop = (text: string, ..._colors: ReadonlyArray) => text +const colors = { + bold: "1", + red: "31", + green: "32", + yellow: "33", + blue: "34", + cyan: "36", + white: "37", + gray: "90", + black: "30", + bgBrightRed: "101" +} as const + +const logLevelColors: Record> = { + None: [], + All: [], + Trace: [colors.gray], + Debug: [colors.blue], + Info: [colors.green], + Warning: [colors.yellow], + Error: [colors.red], + Fatal: [colors.bgBrightRed, colors.black] +} +const logLevelStyle: Record = { + None: "", + All: "", + Trace: "color:gray", + Debug: "color:blue", + Info: "color:green", + Warning: "color:orange", + Error: "color:red", + Fatal: "background-color:red;color:white" +} + +const defaultDateFormat = (date: Date): string => + `${date.getHours().toString().padStart(2, "0")}:${date.getMinutes().toString().padStart(2, "0")}:${ + date.getSeconds().toString().padStart(2, "0") + }.${date.getMilliseconds().toString().padStart(3, "0")}` + +const hasProcessStdout = typeof process === "object" && + process !== null && + typeof process.stdout === "object" && + process.stdout !== null +const processStdoutIsTTY = hasProcessStdout && + process.stdout.isTTY === true +const hasProcessStdoutOrDeno = hasProcessStdout || "Deno" in globalThis + +/** @internal */ +export const prettyLogger = (options?: { + readonly colors?: "auto" | boolean | undefined + readonly stderr?: boolean | undefined + readonly formatDate?: ((date: Date) => string) | undefined + readonly mode?: "browser" | "tty" | "auto" | undefined +}) => { + const mode_ = options?.mode ?? "auto" + const mode = mode_ === "auto" ? (hasProcessStdoutOrDeno ? "tty" : "browser") : mode_ + const isBrowser = mode === "browser" + const showColors = typeof options?.colors === "boolean" ? options.colors : processStdoutIsTTY || isBrowser + const formatDate = options?.formatDate ?? defaultDateFormat + return isBrowser + ? prettyLoggerBrowser({ colors: showColors, formatDate }) + : prettyLoggerTty({ colors: showColors, formatDate, stderr: options?.stderr === true }) +} + +const prettyLoggerTty = (options: { + readonly colors: boolean + readonly stderr: boolean + readonly formatDate: (date: Date) => string +}) => { + const color = options.colors ? withColor : withColorNoop + return makeLogger( + ({ annotations, cause, context, date, fiberId, logLevel, message: message_, spans }) => { + const services = FiberRefs.getOrDefault(context, defaultServices.currentServices) + const console = Context.get(services, consoleTag).unsafe + const log = options.stderr === true ? console.error : console.log + + const message = Arr.ensure(message_) + + let firstLine = color(`[${options.formatDate(date)}]`, colors.white) + + ` ${color(logLevel.label, ...logLevelColors[logLevel._tag])}` + + ` (${fiberId_.threadName(fiberId)})` + + if (List.isCons(spans)) { + const now = date.getTime() + const render = logSpan_.render(now) + for (const span of spans) { + firstLine += " " + render(span) + } + } + + firstLine += ":" + let messageIndex = 0 + if (message.length > 0) { + const firstMaybeString = structuredMessage(message[0]) + if (typeof firstMaybeString === "string") { + firstLine += " " + color(firstMaybeString, colors.bold, colors.cyan) + messageIndex++ + } + } + + log(firstLine) + console.group() + + if (!Cause.isEmpty(cause)) { + log(Cause.pretty(cause, { renderErrorCause: true })) + } + + if (messageIndex < message.length) { + for (; messageIndex < message.length; messageIndex++) { + log(Inspectable.redact(message[messageIndex])) + } + } + + if (HashMap.size(annotations) > 0) { + for (const [key, value] of annotations) { + log(color(`${key}:`, colors.bold, colors.white), Inspectable.redact(value)) + } + } + + console.groupEnd() + } + ) +} + +const prettyLoggerBrowser = (options: { + readonly colors: boolean + readonly formatDate: (date: Date) => string +}) => { + const color = options.colors ? "%c" : "" + return makeLogger( + ({ annotations, cause, context, date, fiberId, logLevel, message: message_, spans }) => { + const services = FiberRefs.getOrDefault(context, defaultServices.currentServices) + const console = Context.get(services, consoleTag).unsafe + const message = Arr.ensure(message_) + + let firstLine = `${color}[${options.formatDate(date)}]` + const firstParams = [] + if (options.colors) { + firstParams.push("color:gray") + } + firstLine += ` ${color}${logLevel.label}${color} (${fiberId_.threadName(fiberId)})` + if (options.colors) { + firstParams.push(logLevelStyle[logLevel._tag], "") + } + if (List.isCons(spans)) { + const now = date.getTime() + const render = logSpan_.render(now) + for (const span of spans) { + firstLine += " " + render(span) + } + } + + firstLine += ":" + + let messageIndex = 0 + if (message.length > 0) { + const firstMaybeString = structuredMessage(message[0]) + if (typeof firstMaybeString === "string") { + firstLine += ` ${color}${firstMaybeString}` + if (options.colors) { + firstParams.push("color:deepskyblue") + } + messageIndex++ + } + } + + console.groupCollapsed(firstLine, ...firstParams) + + if (!Cause.isEmpty(cause)) { + console.error(...Cause.prettyErrors(cause)) + } + + if (messageIndex < message.length) { + for (; messageIndex < message.length; messageIndex++) { + console.log(Inspectable.redact(message[messageIndex])) + } + } + + if (HashMap.size(annotations) > 0) { + for (const [key, value] of annotations) { + const redacted = Inspectable.redact(value) + if (options.colors) { + console.log(`%c${key}:`, "color:gray", redacted) + } else { + console.log(`${key}:`, redacted) + } + } + } + + console.groupEnd() + } + ) +} + +/** @internal */ +export const prettyLoggerDefault = globalValue("effect/Logger/prettyLoggerDefault", () => prettyLogger()) diff --git a/repos/effect/packages/effect/src/internal/mailbox.ts b/repos/effect/packages/effect/src/internal/mailbox.ts new file mode 100644 index 0000000..57f5b77 --- /dev/null +++ b/repos/effect/packages/effect/src/internal/mailbox.ts @@ -0,0 +1,561 @@ +import * as Arr from "../Array.js" +import type { Cause } from "../Cause.js" +import { NoSuchElementException } from "../Cause.js" +import type { Channel } from "../Channel.js" +import * as Chunk from "../Chunk.js" +import type { Effect } from "../Effect.js" +import * as Effectable from "../Effectable.js" +import type { Exit } from "../Exit.js" +import { dual } from "../Function.js" +import * as Inspectable from "../Inspectable.js" +import type * as Api from "../Mailbox.js" +import * as Option from "../Option.js" +import { pipeArguments } from "../Pipeable.js" +import { hasProperty } from "../Predicate.js" +import type { Scheduler } from "../Scheduler.js" +import type { Scope } from "../Scope.js" +import type { Stream } from "../Stream.js" +import * as channel from "./channel.js" +import * as channelExecutor from "./channel/channelExecutor.js" +import * as coreChannel from "./core-stream.js" +import * as core from "./core.js" +import * as circular from "./effect/circular.js" +import * as fiberRuntime from "./fiberRuntime.js" +import * as stream from "./stream.js" + +/** @internal */ +export const TypeId: Api.TypeId = Symbol.for("effect/Mailbox") as Api.TypeId + +/** @internal */ +export const ReadonlyTypeId: Api.ReadonlyTypeId = Symbol.for("effect/Mailbox/ReadonlyMailbox") as Api.ReadonlyTypeId + +/** @internal */ +export const isMailbox = (u: unknown): u is Api.Mailbox => hasProperty(u, TypeId) + +/** @internal */ +export const isReadonlyMailbox = (u: unknown): u is Api.ReadonlyMailbox => + hasProperty(u, ReadonlyTypeId) + +type MailboxState = { + readonly _tag: "Open" + readonly takers: Set<(_: Effect) => void> + readonly offers: Set> + readonly awaiters: Set<(_: Effect) => void> +} | { + readonly _tag: "Closing" + readonly takers: Set<(_: Effect) => void> + readonly offers: Set> + readonly awaiters: Set<(_: Effect) => void> + readonly exit: Exit +} | { + readonly _tag: "Done" + readonly exit: Exit +} + +type OfferEntry = { + readonly _tag: "Array" + readonly remaining: Array + offset: number + readonly resume: (_: Effect>) => void +} | { + readonly _tag: "Single" + readonly message: A + readonly resume: (_: Effect) => void +} + +const empty = Chunk.empty() +const exitEmpty = core.exitSucceed(empty) +const exitFalse = core.exitSucceed(false) +const exitTrue = core.exitSucceed(true) +const constDone = [empty, true] as const + +class MailboxImpl extends Effectable.Class, done: boolean], E> + implements Api.Mailbox +{ + readonly [TypeId]: Api.TypeId = TypeId + readonly [ReadonlyTypeId]: Api.ReadonlyTypeId = ReadonlyTypeId + private state: MailboxState = { + _tag: "Open", + takers: new Set(), + offers: new Set(), + awaiters: new Set() + } + private messages: Array = [] + private messagesChunk = Chunk.empty() + constructor( + readonly scheduler: Scheduler, + private capacity: number, + readonly strategy: "suspend" | "dropping" | "sliding" + ) { + super() + } + + offer(message: A): Effect { + return core.suspend(() => { + if (this.state._tag !== "Open") { + return exitFalse + } else if (this.messages.length + this.messagesChunk.length >= this.capacity) { + switch (this.strategy) { + case "dropping": + return exitFalse + case "suspend": + if (this.capacity <= 0 && this.state.takers.size > 0) { + this.messages.push(message) + this.releaseTaker() + return exitTrue + } + return this.offerRemainingSingle(message) + case "sliding": + this.unsafeTake() + this.messages.push(message) + return exitTrue + } + } + this.messages.push(message) + this.scheduleReleaseTaker() + return exitTrue + }) + } + unsafeOffer(message: A): boolean { + if (this.state._tag !== "Open") { + return false + } else if (this.messages.length + this.messagesChunk.length >= this.capacity) { + if (this.strategy === "sliding") { + this.unsafeTake() + this.messages.push(message) + return true + } else if (this.capacity <= 0 && this.state.takers.size > 0) { + this.messages.push(message) + this.releaseTaker() + return true + } + return false + } + this.messages.push(message) + this.scheduleReleaseTaker() + return true + } + offerAll(messages: Iterable): Effect> { + return core.suspend(() => { + if (this.state._tag !== "Open") { + return core.succeed(Chunk.fromIterable(messages)) + } + const remaining = this.unsafeOfferAllArray(messages) + if (remaining.length === 0) { + return exitEmpty + } else if (this.strategy === "dropping") { + return core.succeed(Chunk.unsafeFromArray(remaining)) + } + return this.offerRemainingArray(remaining) + }) + } + unsafeOfferAll(messages: Iterable): Chunk.Chunk { + return Chunk.unsafeFromArray(this.unsafeOfferAllArray(messages)) + } + unsafeOfferAllArray(messages: Iterable): Array { + if (this.state._tag !== "Open") { + return Arr.fromIterable(messages) + } else if (this.capacity === Number.POSITIVE_INFINITY || this.strategy === "sliding") { + if (this.messages.length > 0) { + this.messagesChunk = Chunk.appendAll(this.messagesChunk, Chunk.unsafeFromArray(this.messages)) + } + if (this.strategy === "sliding") { + this.messagesChunk = this.messagesChunk.pipe( + Chunk.appendAll(Chunk.fromIterable(messages)), + Chunk.takeRight(this.capacity) + ) + } else if (Chunk.isChunk(messages)) { + this.messagesChunk = Chunk.appendAll(this.messagesChunk, messages) + } else { + this.messages = Arr.fromIterable(messages) + } + this.scheduleReleaseTaker() + return [] + } + const free = this.capacity <= 0 + ? this.state.takers.size + : this.capacity - this.messages.length - this.messagesChunk.length + if (free === 0) { + return Arr.fromIterable(messages) + } + const remaining: Array = [] + let i = 0 + for (const message of messages) { + if (i < free) { + this.messages.push(message) + } else { + remaining.push(message) + } + i++ + } + this.scheduleReleaseTaker() + return remaining + } + fail(error: E) { + return this.done(core.exitFail(error)) + } + failCause(cause: Cause) { + return this.done(core.exitFailCause(cause)) + } + unsafeDone(exit: Exit): boolean { + if (this.state._tag !== "Open") { + return false + } else if (this.state.offers.size === 0 && this.messages.length === 0 && this.messagesChunk.length === 0) { + this.finalize(exit) + return true + } + this.state = { ...this.state, _tag: "Closing", exit } + return true + } + shutdown: Effect = core.sync(() => { + if (this.state._tag === "Done") { + return true + } + this.messages = [] + this.messagesChunk = empty + const offers = this.state.offers + this.finalize(this.state._tag === "Open" ? core.exitVoid : this.state.exit) + if (offers.size > 0) { + for (const entry of offers) { + if (entry._tag === "Single") { + entry.resume(exitFalse) + } else { + entry.resume(core.exitSucceed(Chunk.unsafeFromArray(entry.remaining.slice(entry.offset)))) + } + } + offers.clear() + } + return true + }) + done(exit: Exit) { + return core.sync(() => this.unsafeDone(exit)) + } + end = this.done(core.exitVoid) + clear: Effect, E> = core.suspend(() => { + if (this.state._tag === "Done") { + return core.exitAs(this.state.exit, empty) + } + const messages = this.unsafeTakeAll() + this.releaseCapacity() + return core.succeed(messages) + }) + takeAll: Effect, done: boolean], E> = core.suspend(() => { + if (this.state._tag === "Done") { + return core.exitAs(this.state.exit, constDone) + } + const messages = this.unsafeTakeAll() + if (messages.length === 0) { + return core.zipRight(this.awaitTake, this.takeAll) + } + return core.succeed([messages, this.releaseCapacity()]) + }) + takeN(n: number): Effect, done: boolean], E> { + return core.suspend(() => { + if (this.state._tag === "Done") { + return core.exitAs(this.state.exit, constDone) + } else if (n <= 0) { + return core.succeed([empty, false]) + } + n = Math.min(n, this.capacity) + let messages: Chunk.Chunk + if (n <= this.messagesChunk.length) { + messages = Chunk.take(this.messagesChunk, n) + this.messagesChunk = Chunk.drop(this.messagesChunk, n) + } else if (n <= this.messages.length + this.messagesChunk.length) { + this.messagesChunk = Chunk.appendAll(this.messagesChunk, Chunk.unsafeFromArray(this.messages)) + this.messages = [] + messages = Chunk.take(this.messagesChunk, n) + this.messagesChunk = Chunk.drop(this.messagesChunk, n) + } else { + return core.zipRight(this.awaitTake, this.takeN(n)) + } + return core.succeed([messages, this.releaseCapacity()]) + }) + } + unsafeTake(): Exit | undefined { + if (this.state._tag === "Done") { + return core.exitZipRight(this.state.exit, core.exitFail(new NoSuchElementException())) + } + let message: A + if (this.messagesChunk.length > 0) { + message = Chunk.unsafeHead(this.messagesChunk) + this.messagesChunk = Chunk.drop(this.messagesChunk, 1) + } else if (this.messages.length > 0) { + message = this.messages[0] + this.messagesChunk = Chunk.drop(Chunk.unsafeFromArray(this.messages), 1) + this.messages = [] + } else if (this.capacity <= 0 && this.state.offers.size > 0) { + this.capacity = 1 + this.releaseCapacity() + this.capacity = 0 + return this.messages.length > 0 ? core.exitSucceed(this.messages.pop()!) : undefined + } else { + return undefined + } + this.releaseCapacity() + return core.exitSucceed(message) + } + take: Effect = core.suspend(() => + this.unsafeTake() ?? core.zipRight(this.awaitTake, this.take) + ) + await: Effect = core.asyncInterrupt((resume) => { + if (this.state._tag === "Done") { + return resume(this.state.exit) + } + this.state.awaiters.add(resume) + return core.sync(() => { + if (this.state._tag !== "Done") { + this.state.awaiters.delete(resume) + } + }) + }) + unsafeSize(): Option.Option { + const size = this.messages.length + this.messagesChunk.length + return this.state._tag === "Done" ? Option.none() : Option.some(size) + } + size = core.sync(() => this.unsafeSize()) + + commit() { + return this.takeAll + } + pipe() { + return pipeArguments(this, arguments) + } + toJSON() { + return { + _id: "effect/Mailbox", + state: this.state._tag, + size: this.unsafeSize().toJSON() + } + } + toString(): string { + return Inspectable.format(this) + } + [Inspectable.NodeInspectSymbol]() { + return Inspectable.format(this) + } + + private offerRemainingSingle(message: A) { + return core.asyncInterrupt((resume) => { + if (this.state._tag !== "Open") { + return resume(exitFalse) + } + const entry: OfferEntry = { _tag: "Single", message, resume } + this.state.offers.add(entry) + return core.sync(() => { + if (this.state._tag === "Open") { + this.state.offers.delete(entry) + } + }) + }) + } + private offerRemainingArray(remaining: Array) { + return core.asyncInterrupt>((resume) => { + if (this.state._tag !== "Open") { + return resume(core.exitSucceed(Chunk.unsafeFromArray(remaining))) + } + const entry: OfferEntry = { _tag: "Array", remaining, offset: 0, resume } + this.state.offers.add(entry) + return core.sync(() => { + if (this.state._tag === "Open") { + this.state.offers.delete(entry) + } + }) + }) + } + private releaseCapacity(): boolean { + if (this.state._tag === "Done") { + return this.state.exit._tag === "Success" + } else if (this.state.offers.size === 0) { + if (this.state._tag === "Closing" && this.messages.length === 0 && this.messagesChunk.length === 0) { + this.finalize(this.state.exit) + return this.state.exit._tag === "Success" + } + return false + } + let n = this.capacity - this.messages.length - this.messagesChunk.length + for (const entry of this.state.offers) { + if (n === 0) return false + else if (entry._tag === "Single") { + this.messages.push(entry.message) + n-- + entry.resume(exitTrue) + this.state.offers.delete(entry) + } else { + for (; entry.offset < entry.remaining.length; entry.offset++) { + if (n === 0) return false + this.messages.push(entry.remaining[entry.offset]) + n-- + } + entry.resume(exitEmpty) + this.state.offers.delete(entry) + } + } + return false + } + private awaitTake = core.asyncInterrupt((resume) => { + if (this.state._tag === "Done") { + return resume(this.state.exit) + } + this.state.takers.add(resume) + return core.sync(() => { + if (this.state._tag !== "Done") { + this.state.takers.delete(resume) + } + }) + }) + + private scheduleRunning = false + private scheduleReleaseTaker() { + if (this.scheduleRunning) { + return + } + this.scheduleRunning = true + this.scheduler.scheduleTask(this.releaseTaker, 0) + } + private releaseTaker = () => { + this.scheduleRunning = false + if (this.state._tag === "Done") { + return + } else if (this.state.takers.size === 0) { + return + } + for (const taker of this.state.takers) { + this.state.takers.delete(taker) + taker(core.exitVoid) + if (this.messages.length + this.messagesChunk.length === 0) { + break + } + } + } + + private unsafeTakeAll() { + if (this.messagesChunk.length > 0) { + const messages = this.messages.length > 0 ? + Chunk.appendAll(this.messagesChunk, Chunk.unsafeFromArray(this.messages)) : + this.messagesChunk + this.messagesChunk = empty + this.messages = [] + return messages + } else if (this.messages.length > 0) { + const messages = Chunk.unsafeFromArray(this.messages) + this.messages = [] + return messages + } else if (this.state._tag !== "Done" && this.state.offers.size > 0) { + this.capacity = 1 + this.releaseCapacity() + this.capacity = 0 + return Chunk.of(this.messages.pop()!) + } + return empty + } + + private finalize(exit: Exit) { + if (this.state._tag === "Done") { + return + } + const openState = this.state + this.state = { _tag: "Done", exit } + for (const taker of openState.takers) { + taker(exit) + } + openState.takers.clear() + for (const awaiter of openState.awaiters) { + awaiter(exit) + } + openState.awaiters.clear() + } +} + +/** @internal */ +export const make = ( + capacity?: number | { + readonly capacity?: number | undefined + readonly strategy?: "suspend" | "dropping" | "sliding" | undefined + } | undefined +): Effect> => + core.withFiberRuntime((fiber) => + core.succeed( + new MailboxImpl( + fiber.currentScheduler, + typeof capacity === "number" ? capacity : capacity?.capacity ?? Number.POSITIVE_INFINITY, + typeof capacity === "number" ? "suspend" : capacity?.strategy ?? "suspend" + ) + ) + ) + +/** @internal */ +export const into: { + ( + self: Api.Mailbox + ): (effect: Effect) => Effect + ( + effect: Effect, + self: Api.Mailbox + ): Effect +} = dual( + 2, + ( + effect: Effect, + self: Api.Mailbox + ): Effect => + core.uninterruptibleMask((restore) => + core.matchCauseEffect(restore(effect), { + onFailure: (cause) => self.failCause(cause), + onSuccess: (_) => self.end + }) + ) +) + +/** @internal */ +export const toChannel = (self: Api.ReadonlyMailbox): Channel, unknown, E> => { + const loop: Channel, unknown, E> = coreChannel.flatMap(self.takeAll, ([messages, done]) => + done + ? messages.length === 0 ? coreChannel.void : coreChannel.write(messages) + : channel.zipRight(coreChannel.write(messages), loop)) + return loop +} + +/** @internal */ +export const toStream = (self: Api.ReadonlyMailbox): Stream => stream.fromChannel(toChannel(self)) + +/** @internal */ +export const fromStream: { + (options?: { + readonly capacity?: number | undefined + readonly strategy?: "suspend" | "dropping" | "sliding" | undefined + }): (self: Stream) => Effect, never, R | Scope> + ( + self: Stream, + options?: { + readonly capacity?: number | undefined + readonly strategy?: "suspend" | "dropping" | "sliding" | undefined + } + ): Effect, never, R | Scope> +} = dual((args) => stream.isStream(args[0]), ( + self: Stream, + options?: { + readonly capacity?: number | undefined + readonly strategy?: "suspend" | "dropping" | "sliding" | undefined + } +): Effect, never, R | Scope> => + core.tap( + fiberRuntime.acquireRelease( + make(options), + (mailbox) => mailbox.shutdown + ), + (mailbox) => { + const writer: Channel, never, E> = coreChannel.readWithCause({ + onInput: (input: Chunk.Chunk) => coreChannel.flatMap(mailbox.offerAll(input), () => writer), + onFailure: (cause: Cause) => mailbox.failCause(cause), + onDone: () => mailbox.end + }) + return fiberRuntime.scopeWith((scope) => + stream.toChannel(self).pipe( + coreChannel.pipeTo(writer), + channelExecutor.runIn(scope), + circular.forkIn(scope) + ) + ) + } + )) diff --git a/repos/effect/packages/effect/src/internal/managedRuntime.ts b/repos/effect/packages/effect/src/internal/managedRuntime.ts new file mode 100644 index 0000000..8addd71 --- /dev/null +++ b/repos/effect/packages/effect/src/internal/managedRuntime.ts @@ -0,0 +1,137 @@ +import type * as Effect from "../Effect.js" +import * as Effectable from "../Effectable.js" +import type { Exit } from "../Exit.js" +import type * as Fiber from "../Fiber.js" +import type * as Layer from "../Layer.js" +import type * as M from "../ManagedRuntime.js" +import { pipeArguments } from "../Pipeable.js" +import { hasProperty } from "../Predicate.js" +import type * as Runtime from "../Runtime.js" +import * as Scheduler from "../Scheduler.js" +import * as Scope from "../Scope.js" +import type { Mutable } from "../Types.js" +import * as core from "./core.js" +import * as fiberRuntime from "./fiberRuntime.js" +import * as internalLayer from "./layer.js" +import * as circular from "./managedRuntime/circular.js" +import * as internalRuntime from "./runtime.js" + +interface ManagedRuntimeImpl extends M.ManagedRuntime { + readonly scope: Scope.CloseableScope + cachedRuntime: Runtime.Runtime | undefined +} + +/** @internal */ +export const isManagedRuntime = (u: unknown): u is M.ManagedRuntime => hasProperty(u, circular.TypeId) + +function provide( + managed: ManagedRuntimeImpl, + effect: Effect.Effect +): Effect.Effect { + return core.flatMap( + managed.runtimeEffect, + (rt) => + core.withFiberRuntime((fiber) => { + fiber.setFiberRefs(rt.fiberRefs) + fiber.currentRuntimeFlags = rt.runtimeFlags + return core.provideContext(effect, rt.context) + }) + ) +} + +const ManagedRuntimeProto = { + ...Effectable.CommitPrototype, + [circular.TypeId]: circular.TypeId, + pipe() { + return pipeArguments(this, arguments) + }, + commit(this: ManagedRuntimeImpl) { + return this.runtimeEffect + } +} + +/** @internal */ +export const make = ( + layer: Layer.Layer, + memoMap?: Layer.MemoMap +): M.ManagedRuntime => { + memoMap = memoMap ?? internalLayer.unsafeMakeMemoMap() + const scope = internalRuntime.unsafeRunSyncEffect(fiberRuntime.scopeMake()) + let buildFiber: Fiber.RuntimeFiber, ER> | undefined + const runtimeEffect = core.suspend(() => { + if (!buildFiber) { + const scheduler = new Scheduler.SyncScheduler() + buildFiber = internalRuntime.unsafeForkEffect( + core.tap( + Scope.extend( + internalLayer.toRuntimeWithMemoMap(layer, memoMap), + scope + ), + (rt) => { + self.cachedRuntime = rt + } + ), + { scope, scheduler } + ) + scheduler.flush() + } + return core.flatten(buildFiber.await) + }) + const self: ManagedRuntimeImpl = Object.assign(Object.create(ManagedRuntimeProto), { + memoMap, + scope, + runtimeEffect, + cachedRuntime: undefined, + runtime() { + return self.cachedRuntime === undefined ? + internalRuntime.unsafeRunPromiseEffect(self.runtimeEffect) : + Promise.resolve(self.cachedRuntime) + }, + dispose(): Promise { + return internalRuntime.unsafeRunPromiseEffect(self.disposeEffect) + }, + disposeEffect: core.suspend(() => { + ;(self as Mutable>).runtimeEffect = core.die("ManagedRuntime disposed") + self.cachedRuntime = undefined + return Scope.close(self.scope, core.exitVoid) + }), + runFork(effect: Effect.Effect, options?: Runtime.RunForkOptions): Fiber.RuntimeFiber { + return self.cachedRuntime === undefined ? + internalRuntime.unsafeForkEffect(provide(self, effect), options) : + internalRuntime.unsafeFork(self.cachedRuntime)(effect, options) + }, + runSyncExit(effect: Effect.Effect): Exit { + return self.cachedRuntime === undefined ? + internalRuntime.unsafeRunSyncExitEffect(provide(self, effect)) : + internalRuntime.unsafeRunSyncExit(self.cachedRuntime)(effect) + }, + runSync(effect: Effect.Effect): A { + return self.cachedRuntime === undefined ? + internalRuntime.unsafeRunSyncEffect(provide(self, effect)) : + internalRuntime.unsafeRunSync(self.cachedRuntime)(effect) + }, + runPromiseExit(effect: Effect.Effect, options?: { + readonly signal?: AbortSignal | undefined + }): Promise> { + return self.cachedRuntime === undefined ? + internalRuntime.unsafeRunPromiseExitEffect(provide(self, effect), options) : + internalRuntime.unsafeRunPromiseExit(self.cachedRuntime)(effect, options) + }, + runCallback( + effect: Effect.Effect, + options?: Runtime.RunCallbackOptions | undefined + ): Runtime.Cancel { + return self.cachedRuntime === undefined ? + internalRuntime.unsafeRunCallback(internalRuntime.defaultRuntime)(provide(self, effect), options) : + internalRuntime.unsafeRunCallback(self.cachedRuntime)(effect, options) + }, + runPromise(effect: Effect.Effect, options?: { + readonly signal?: AbortSignal | undefined + }): Promise { + return self.cachedRuntime === undefined ? + internalRuntime.unsafeRunPromiseEffect(provide(self, effect), options) : + internalRuntime.unsafeRunPromise(self.cachedRuntime)(effect, options) + } + }) + return self +} diff --git a/repos/effect/packages/effect/src/internal/managedRuntime/circular.ts b/repos/effect/packages/effect/src/internal/managedRuntime/circular.ts new file mode 100644 index 0000000..9abefc9 --- /dev/null +++ b/repos/effect/packages/effect/src/internal/managedRuntime/circular.ts @@ -0,0 +1,6 @@ +import type * as M from "../../ManagedRuntime.js" + +// circular with Layer + +/** @internal */ +export const TypeId: M.TypeId = Symbol.for("effect/ManagedRuntime") as M.TypeId diff --git a/repos/effect/packages/effect/src/internal/matcher.ts b/repos/effect/packages/effect/src/internal/matcher.ts new file mode 100644 index 0000000..3bfe589 --- /dev/null +++ b/repos/effect/packages/effect/src/internal/matcher.ts @@ -0,0 +1,652 @@ +import * as Either from "../Either.js" +import { dual, identity } from "../Function.js" +import type { + Case, + Matcher, + MatcherTypeId, + Not, + SafeRefinement, + TypeMatcher, + Types, + ValueMatcher, + When +} from "../Match.js" +import * as Option from "../Option.js" +import { pipeArguments } from "../Pipeable.js" +import type * as Predicate from "../Predicate.js" +import type { Unify } from "../Unify.js" + +/** @internal */ +export const TypeId: MatcherTypeId = Symbol.for( + "@effect/matcher/Matcher" +) as MatcherTypeId + +const TypeMatcherProto: Omit, "cases"> = { + [TypeId]: { + _input: identity, + _filters: identity, + _remaining: identity, + _result: identity, + _return: identity + }, + _tag: "TypeMatcher", + add( + this: TypeMatcher, + _case: Case + ): TypeMatcher { + return makeTypeMatcher([...this.cases, _case]) + }, + pipe() { + return pipeArguments(this, arguments) + } +} + +function makeTypeMatcher( + cases: ReadonlyArray +): TypeMatcher { + const matcher = Object.create(TypeMatcherProto) + matcher.cases = cases + return matcher +} + +const ValueMatcherProto: Omit< + ValueMatcher, + "provided" | "value" +> = { + [TypeId]: { + _input: identity, + _filters: identity, + _remaining: identity, + _result: identity, + _provided: identity, + _return: identity + }, + _tag: "ValueMatcher", + add( + this: ValueMatcher, + _case: Case + ): ValueMatcher { + if (this.value._tag === "Right") { + return this + } + + if (_case._tag === "When" && _case.guard(this.provided) === true) { + return makeValueMatcher( + this.provided, + Either.right(_case.evaluate(this.provided)) + ) + } else if (_case._tag === "Not" && _case.guard(this.provided) === false) { + return makeValueMatcher( + this.provided, + Either.right(_case.evaluate(this.provided)) + ) + } + + return this + }, + pipe() { + return pipeArguments(this, arguments) + } +} + +function makeValueMatcher( + provided: Pr, + value: Either.Either +): ValueMatcher { + const matcher = Object.create(ValueMatcherProto) + matcher.provided = provided + matcher.value = value + return matcher +} + +const makeWhen = ( + guard: (u: unknown) => boolean, + evaluate: (input: unknown) => any +): When => ({ + _tag: "When", + guard, + evaluate +}) + +const makeNot = ( + guard: (u: unknown) => boolean, + evaluate: (input: unknown) => any +): Not => ({ + _tag: "Not", + guard, + evaluate +}) + +const makePredicate = (pattern: unknown): Predicate.Predicate => { + if (typeof pattern === "function") { + return pattern as Predicate.Predicate + } else if (Array.isArray(pattern)) { + const predicates = pattern.map(makePredicate) + const len = predicates.length + + return (u: unknown) => { + if (!Array.isArray(u)) { + return false + } + + for (let i = 0; i < len; i++) { + if (predicates[i](u[i]) === false) { + return false + } + } + + return true + } + } else if (pattern !== null && typeof pattern === "object") { + const keysAndPredicates = Object.entries(pattern).map( + ([k, p]) => [k, makePredicate(p)] as const + ) + const len = keysAndPredicates.length + + return (u: unknown) => { + if (typeof u !== "object" || u === null) { + return false + } + + for (let i = 0; i < len; i++) { + const [key, predicate] = keysAndPredicates[i] + if (!(key in u) || predicate((u as any)[key]) === false) { + return false + } + } + + return true + } + } + + return (u: unknown) => u === pattern +} + +const makeOrPredicate = ( + patterns: ReadonlyArray +): Predicate.Predicate => { + const predicates = patterns.map(makePredicate) + const len = predicates.length + + return (u: unknown) => { + for (let i = 0; i < len; i++) { + if (predicates[i](u) === true) { + return true + } + } + + return false + } +} + +const makeAndPredicate = ( + patterns: ReadonlyArray +): Predicate.Predicate => { + const predicates = patterns.map(makePredicate) + const len = predicates.length + + return (u: unknown) => { + for (let i = 0; i < len; i++) { + if (predicates[i](u) === false) { + return false + } + } + + return true + } +} + +/** @internal */ +export const type = (): Matcher< + I, + Types.Without, + I, + never, + never +> => makeTypeMatcher([]) + +/** @internal */ +export const value = ( + i: I +): Matcher, I, never, I> => makeValueMatcher(i, Either.left(i)) + +/** @internal */ +export const valueTags: { + < + const I, + P extends + & { readonly [Tag in Types.Tags<"_tag", I> & string]: (_: Extract) => any } + & { readonly [Tag in Exclude>]: never } + >(fields: P): (input: I) => Unify> + < + const I, + P extends + & { readonly [Tag in Types.Tags<"_tag", I> & string]: (_: Extract) => any } + & { readonly [Tag in Exclude>]: never } + >(input: I, fields: P): Unify> +} = dual( + 2, + < + const I, + P extends + & { readonly [Tag in Types.Tags<"_tag", I> & string]: (_: Extract) => any } + & { readonly [Tag in Exclude>]: never } + >(input: I, fields: P): Unify> => { + const match: any = tagsExhaustive(fields as any)(makeTypeMatcher([])) + return match(input) + } +) + +/** @internal */ +export const typeTags = () => +< + P extends { + readonly [Tag in Types.Tags<"_tag", I> & string]: ( + _: Extract + ) => any + } +>( + fields: P +) => { + const match: any = tagsExhaustive(fields as any)(makeTypeMatcher([])) + return (input: I): Unify> => match(input) +} + +/** @internal */ +export const withReturnType = () => +(self: Matcher): [Ret] extends [ + [A] extends [never] ? any : A +] ? Matcher + : "withReturnType constraint does not extend Result type" => self as any + +/** @internal */ +export const when = < + R, + const P extends Types.PatternPrimitive | Types.PatternBase, + Ret, + Fn extends (_: Types.WhenMatch) => Ret +>( + pattern: P, + f: Fn +) => +( + self: Matcher +): Matcher< + I, + Types.AddWithout>, + Types.ApplyFilters>>, + A | ReturnType, + Pr, + Ret +> => (self as any).add(makeWhen(makePredicate(pattern), f as any)) + +/** @internal */ +export const whenOr = < + R, + const P extends ReadonlyArray< + Types.PatternPrimitive | Types.PatternBase + >, + Ret, + Fn extends (_: Types.WhenMatch) => Ret +>( + ...args: [...patterns: P, f: Fn] +) => +( + self: Matcher +): Matcher< + I, + Types.AddWithout>, + Types.ApplyFilters>>, + A | ReturnType, + Pr, + Ret +> => { + const onMatch = args[args.length - 1] as any + const patterns = args.slice(0, -1) as unknown as P + return (self as any).add(makeWhen(makeOrPredicate(patterns), onMatch)) +} + +/** @internal */ +export const whenAnd = < + R, + const P extends ReadonlyArray< + Types.PatternPrimitive | Types.PatternBase + >, + Ret, + Fn extends (_: Types.WhenMatch>) => Ret +>( + ...args: [...patterns: P, f: Fn] +) => +( + self: Matcher +): Matcher< + I, + Types.AddWithout>>, + Types.ApplyFilters< + I, + Types.AddWithout>> + >, + A | ReturnType, + Pr +> => { + const onMatch = args[args.length - 1] as any + const patterns = args.slice(0, -1) as unknown as P + return (self as any).add(makeWhen(makeAndPredicate(patterns), onMatch)) +} + +/** @internal */ +export const discriminator = (field: D) => +< + R, + P extends Types.Tags & string, + Ret, + Fn extends (_: Extract>) => Ret +>( + ...pattern: [ + first: P, + ...values: Array

, f: Fn] +) => ( + self: Matcher +) => Matcher< + I, + Types.AddWithout>>, + Types.ApplyFilters>>>, + A | ReturnType, + Pr, + Ret +> = internal.discriminator + +/** + * Matches values where a specified field starts with a given prefix. + * + * **Details** + * + * This function is useful for working with discriminated unions where the + * discriminant field follows a hierarchical or namespaced structure. It allows + * you to match values based on whether the specified field starts with a given + * prefix, making it easier to handle grouped cases. + * + * Instead of checking for exact matches, this function lets you match values + * that share a common prefix. For example, if your discriminant field contains + * hierarchical names like `"A"`, `"A.A"`, and `"B"`, you can match all values + * starting with `"A"` using a single rule. + * + * @example + * ```ts + * import { Match, pipe } from "effect" + * + * const match = pipe( + * Match.type<{ type: "A" } | { type: "B" } | { type: "A.A" } | {}>(), + * Match.discriminatorStartsWith("type")("A", (_) => 1 as const), + * Match.discriminatorStartsWith("type")("B", (_) => 2 as const), + * Match.orElse((_) => 3 as const) + * ) + * + * console.log(match({ type: "A" })) // 1 + * console.log(match({ type: "B" })) // 2 + * console.log(match({ type: "A.A" })) // 1 + * ``` + * + * @category Defining patterns + * @since 1.0.0 + */ +export const discriminatorStartsWith: ( + field: D +) => >) => Ret>( + pattern: P, + f: Fn +) => ( + self: Matcher +) => Matcher< + I, + Types.AddWithout>>, + Types.ApplyFilters>>>, + A | ReturnType, + Pr, + Ret +> = internal.discriminatorStartsWith + +/** + * Matches values based on a field that serves as a discriminator, mapping each + * possible value to a corresponding handler. + * + * **Details** + * + * This function simplifies working with discriminated unions by letting you + * define a set of handlers for each possible value of a given field. Instead of + * chaining multiple calls to {@link discriminator}, this function allows + * defining all possible cases at once using an object where the keys are the + * possible values of the field, and the values are the corresponding handler + * functions. + * + * @example + * ```ts + * import { Match, pipe } from "effect" + * + * const match = pipe( + * Match.type<{ type: "A"; a: string } | { type: "B"; b: number } | { type: "C"; c: boolean }>(), + * Match.discriminators("type")({ + * A: (a) => a.a, + * B: (b) => b.b, + * C: (c) => c.c + * }), + * Match.exhaustive + * ) + * ``` + * + * @category Defining patterns + * @since 1.0.0 + */ +export const discriminators: ( + field: D +) => < + R, + Ret, + P extends + & { readonly [Tag in Types.Tags & string]?: ((_: Extract>) => Ret) | undefined } + & { readonly [Tag in Exclude>]: never } +>( + fields: P +) => ( + self: Matcher +) => Matcher< + I, + Types.AddWithout>>, + Types.ApplyFilters>>>, + A | ReturnType, + Pr, + Ret +> = internal.discriminators + +/** + * Matches values based on a discriminator field and **ensures all cases are + * handled**. + * + * **Details*+ + * + * This function is similar to {@link discriminators}, but **requires that all + * possible cases** are explicitly handled. It is useful when working with + * discriminated unions, where a specific field (e.g., `"type"`) determines the + * shape of an object. Each possible value of the field must have a + * corresponding handler, ensuring **exhaustiveness checking** at compile time. + * + * This function **does not require** `Match.exhaustive` at the end of the + * pipeline because it enforces exhaustiveness by design. + * + * @example + * ```ts + * import { Match, pipe } from "effect" + * + * const match = pipe( + * Match.type<{ type: "A"; a: string } | { type: "B"; b: number } | { type: "C"; c: boolean }>(), + * Match.discriminatorsExhaustive("type")({ + * A: (a) => a.a, + * B: (b) => b.b, + * C: (c) => c.c + * }) + * ) + * ``` + * + * @category Defining patterns + * @since 1.0.0 + */ +export const discriminatorsExhaustive: ( + field: D +) => < + R, + Ret, + P extends + & { readonly [Tag in Types.Tags & string]: (_: Extract>) => Ret } + & { readonly [Tag in Exclude>]: never } +>( + fields: P +) => ( + self: Matcher +) => [Pr] extends [never] ? (u: I) => Unify> : Unify> = + internal.discriminatorsExhaustive + +/** + * The `Match.tag` function allows pattern matching based on the `_tag` field in + * a [Discriminated Union](https://www.typescriptlang.org/docs/handbook/typescript-in-5-minutes-func.html#discriminated-unions). + * You can specify multiple tags to match within a single pattern. + * + * **Note** + * + * The `Match.tag` function relies on the convention within the Effect ecosystem + * of naming the tag field as `"_tag"`. Ensure that your discriminated unions + * follow this naming convention for proper functionality. + * + * **Example** (Matching a Discriminated Union by Tag) + * + * ```ts + * import { Match } from "effect" + * + * type Event = + * | { readonly _tag: "fetch" } + * | { readonly _tag: "success"; readonly data: string } + * | { readonly _tag: "error"; readonly error: Error } + * | { readonly _tag: "cancel" } + * + * // Create a Matcher for Either + * const match = Match.type().pipe( + * // Match either "fetch" or "success" + * Match.tag("fetch", "success", () => `Ok!`), + * // Match "error" and extract the error message + * Match.tag("error", (event) => `Error: ${event.error.message}`), + * // Match "cancel" + * Match.tag("cancel", () => "Cancelled"), + * Match.exhaustive + * ) + * + * console.log(match({ _tag: "success", data: "Hello" })) + * // Output: "Ok!" + * + * console.log(match({ _tag: "error", error: new Error("Oops!") })) + * // Output: "Error: Oops!" + * ``` + * + * @category Defining patterns + * @since 1.0.0 + */ +export const tag: < + R, + P extends Types.Tags<"_tag", R> & string, + Ret, + Fn extends (_: Extract>) => Ret +>( + ...pattern: [first: P, ...values: Array